[
  {
    "path": ".dockerignore",
    "content": ".git\n"
  },
  {
    "path": ".githooks/commit-msg",
    "content": "#!/bin/sh\n#\n# An example hook script to check the commit log message.\n# Called by \"git commit\" with one argument, the name of the file\n# that has the commit message.  The hook should exit with non-zero\n# status after issuing an appropriate message if it wants to stop the\n# commit.  The hook is allowed to edit the commit message file.\n\nYELLOW=\"\\e[93m\"\nGREEN=\"\\e[32m\"\nRED=\"\\e[31m\"\nENDCOLOR=\"\\e[0m\"\n\nprintMessage() {\n   printf \"${YELLOW}AppFlowy : $1${ENDCOLOR}\\n\"\n}\n\nprintSuccess() {\n   printf \"${GREEN}AppFlowy : $1${ENDCOLOR}\\n\"\n}\n\nprintError() {\n   printf \"${RED}AppFlowy : $1${ENDCOLOR}\\n\"\n}\n\nprintMessage \"Running the AppFlowy commit-msg hook.\"\n\n# This example catches duplicate Signed-off-by lines.\n\ntest \"\" = \"$(grep '^Signed-off-by: ' \"$1\" |\n\t sort | uniq -c | sed -e '/^[ \t]*1[ \t]/d')\" || {\n\techo >&2 Duplicate Signed-off-by lines.\n\texit 1\n}\n\n.githooks/gitlint \\\n\t --msg-file=$1 \\\n\t --subject-regex=\"^(build|chore|ci|docs|feat|feature|fix|perf|refactor|revert|style|test)(.*)?:\\s?.*\" \\\n    --subject-maxlen=150 \\\n    --subject-minlen=10 \\\n    --body-regex=\".*\" \\\n    --max-parents=1\n\nif [ $? -ne 0 ]\nthen\n    printError \"Please fix your commit message to match AppFlowy coding standards\"\n    printError \"https://docs.appflowy.io/docs/documentation/software-contributions/conventions/git-conventions\"\n    exit 1\nfi\n\n"
  },
  {
    "path": ".githooks/pre-commit",
    "content": "#!/usr/bin/env bash\n\nYELLOW=\"\\e[93m\"\nGREEN=\"\\e[32m\"\nRED=\"\\e[31m\"\nENDCOLOR=\"\\e[0m\"\n\nprintMessage() {\n   printf \"${YELLOW}AppFlowy : $1${ENDCOLOR}\\n\"\n}\n\nprintSuccess() {\n   printf \"${GREEN}AppFlowy : $1${ENDCOLOR}\\n\"\n}\n\nprintError() {\n   printf \"${RED}AppFlowy : $1${ENDCOLOR}\\n\"\n}\n\nprintMessage \"Running local AppFlowy pre-commit hook.\"\n\n#flutter format .\n##https://gist.github.com/benmccallum/28e4f216d9d72f5965133e6c43aaff6e\nlimit=$(( 1 * 2**20 )) # 1MB\n\nfunction file_too_large(){\n\tfilename=$0\n\tfilesize=$(( $1 / 2**20 ))\n\n\tcat <<HEREDOC\n\n\tFile $filename is $filesize MB, which is larger than github's maximum\n        file size (1 MB). We will not be able to push this file to GitHub.\n\tCommit aborted\nHEREDOC\n\n}\n\nempty_tree=$( git hash-object -t tree /dev/null )\nif git rev-parse --verify HEAD > /dev/null 2>&1\nthen\n\tagainst=HEAD\nelse\n\tagainst=empty_tree\nfi\n\nfor file in $( git diff-index --cached --name-only $against ); do\n\tfile_size=$( ls -la $file | awk '{ print $5 }')\n\tif [ \"$file_size\" -gt  \"$limit\" ]; then\n\t\tfile_too_large $filename $file_size\n\t\texit 1;\n\tfi\ndone\n"
  },
  {
    "path": ".githooks/pre-push",
    "content": "#!/usr/bin/env bash\n\nYELLOW=\"\\e[93m\"\nGREEN=\"\\e[32m\"\nRED=\"\\e[31m\"\nENDCOLOR=\"\\e[0m\"\n\nprintMessage() {\n   printf \"${YELLOW}AppFlowy : $1${ENDCOLOR}\\n\"\n}\n\nprintSuccess() {\n   printf \"${GREEN}AppFlowy : $1${ENDCOLOR}\\n\"\n}\n\nprintError() {\n   printf \"${RED}AppFlowy : $1${ENDCOLOR}\\n\"\n}\n\nprintMessage \"Running local AppFlowy pre-push hook.\"\n\nif [[ `git status --porcelain` ]]; then\n  printError \"This script needs to run against committed code only. Please commit or stash you changes.\"\n  exit 1\nfi\n\n#\n#printMessage \"Running the Flutter analyzer\"\n#flutter analyze\n#\n#if [ $? -ne 0 ]; then\n#  printError \"Flutter analyzer error\"\n#  exit 1\n#fi\n#\n#printMessage \"Finished running the Flutter analyzer\"\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "ko_fi: appflowy\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
    "content": "name: Bug Report\ndescription: Create a bug report to help us improve\ntitle: \"[Bug] \"\nbody:\n  - type: textarea\n    id: desc\n    attributes:\n      label: Bug Description\n      description: A clear and concise description of what the bug is\n    validations:\n      required: true\n  - type: textarea\n    id: reproduce\n    attributes:\n      label: How to Reproduce\n      description: What steps can we take to reproduce this behavior?\n    validations:\n      required: true\n  - type: textarea\n    id: expected\n    attributes:\n      label: Expected Behavior\n      description: A clear and concise description of what you expected to happen\n    validations:\n      required: true\n  - type: input\n    id: os\n    attributes:\n      label: Operating System\n      description: What OS are you seeing this bug on?\n    validations:\n      required: true\n  - type: input\n    id: version\n    attributes:\n      label: AppFlowy Version(s)\n      description: What version(s) of AppFlowy do you see this bug on?\n    validations:\n      required: true\n  - type: textarea\n    id: screenshots\n    attributes:\n      label: Screenshots\n      description: If applicable, please add screenshots to help explain your problem\n  - type: textarea\n    id: context\n    attributes:\n      label: Additional Context\n      description: Add any additonal context about the problem here\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yaml",
    "content": "name: Feature Request\ndescription: Suggest an idea for AppFlowy\ntitle: \"[FR] \"\nbody:\n  - type: textarea\n    id: desc\n    attributes:\n      label: Description\n      description: Describe your suggested feature and the main use cases\n    validations:\n      required: true\n      \n  - type: textarea\n    id: users\n    attributes:\n      label: Impact\n      description: What types of users can benefit from using the suggested feature?\n    validations:\n      required: true\n\n  - type: textarea\n    id: context\n    attributes:\n      label: Additional Context\n      description: Add any additonal context about the feature here\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!---\nThank you for submitting a pull request to AppFlowy. The team will dedicate their best efforts to reviewing and approving your pull request. If you have any questions about the project or feedback for us, please join our [Discord](https://discord.gg/wdjWUXXhtw).\n-->\n\n<!---\nIf your pull request adds a new feature, please drag and drop a video into this section to showcase what you've done! If not, you may delete this section.\n-->\n\n### Feature Preview\n\n<!---\nList at least one issue here that this PR addresses. If it fixes the issue, please use the [fixes](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests) keyword to close the issue. For example:\nfixes https://github.com/AppFlowy-IO/AppFlowy/pull/2106\n-->\n\n---\n\n<!---\nBefore you mark this PR ready for review, run through this checklist!\n-->\n\n#### PR Checklist\n\n- [ ] My code adheres to [AppFlowy's Conventions](https://docs.appflowy.io/docs/documentation/software-contributions/conventions)\n- [ ] I've listed at least one issue that this PR fixes in the description above.\n- [ ] I've added a test(s) to validate changes in this PR, or this PR only contains semantic changes.\n- [ ] All existing tests are passing.\n"
  },
  {
    "path": ".github/actions/flutter_build/action.yml",
    "content": "name: Flutter Integration Test\ndescription: Run integration tests for AppFlowy\n\ninputs:\n  os:\n    description: \"The operating system to run the tests on\"\n    required: true\n  flutter_version:\n    description: \"The version of Flutter to use\"\n    required: true\n  rust_toolchain:\n    description: \"The version of Rust to use\"\n    required: true\n  cargo_make_version:\n    description: \"The version of cargo-make to use\"\n    required: true\n  rust_target:\n    description: \"The target to build for\"\n    required: true\n  flutter_profile:\n    description: \"The profile to build with\"\n    required: true\n\nruns:\n  using: \"composite\"\n\n  steps:\n    - name: Checkout source code\n      uses: actions/checkout@v4\n\n    - name: Install Rust toolchain\n      id: rust_toolchain\n      uses: actions-rs/toolchain@v1\n      with:\n        toolchain: ${{ inputs.rust_toolchain }}\n        target: ${{ inputs.rust_target }}\n        override: true\n        profile: minimal\n\n    - name: Install flutter\n      id: flutter\n      uses: subosito/flutter-action@v2\n      with:\n        channel: \"stable\"\n        flutter-version: ${{ inputs.flutter_version }}\n        cache: true\n\n    - uses: Swatinem/rust-cache@v2\n      with:\n        prefix-key: ${{ inputs.os }}\n        workspaces: |\n          frontend/rust-lib\n        cache-all-crates: true\n\n    - uses: taiki-e/install-action@v2\n      with:\n        tool: cargo-make@${{ inputs.cargo_make_version }}, duckscript_cli\n\n    - name: Install prerequisites\n      working-directory: frontend\n      shell: bash\n      run: |\n        case $RUNNER_OS in\n          Linux)\n            sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub\n            sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list\n            sudo apt-get update\n            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 libcurl4-openssl-dev\n            ;;\n          Windows)\n            vcpkg integrate install\n            vcpkg update\n            ;;\n          macOS)\n            # No additional prerequisites needed for macOS\n            ;;\n        esac\n        cargo make appflowy-flutter-deps-tools\n\n    - name: Build AppFlowy\n      working-directory: frontend\n      run: cargo make --profile ${{ inputs.flutter_profile }} appflowy-core-dev\n      shell: bash\n\n    - name: Run code generation\n      working-directory: frontend\n      run: cargo make code_generation\n      shell: bash\n\n    - name: Flutter Analyzer\n      working-directory: frontend/appflowy_flutter\n      run: flutter analyze .\n      shell: bash\n\n    - name: Compress appflowy_flutter\n      run: tar -czf appflowy_flutter.tar.gz frontend/appflowy_flutter\n      shell: bash\n\n    - uses: actions/upload-artifact@v4\n      with:\n        name: ${{ github.run_id }}-${{ matrix.os }}\n        path: appflowy_flutter.tar.gz\n"
  },
  {
    "path": ".github/actions/flutter_integration_test/action.yml",
    "content": "name: Flutter Integration Test\ndescription: Run integration tests for AppFlowy\n\ninputs:\n  test_path:\n    description: \"The path to the integration test file\"\n    required: true\n  flutter_version:\n    description: \"The version of Flutter to use\"\n    required: true\n  rust_toolchain:\n    description: \"The version of Rust to use\"\n    required: true\n  cargo_make_version:\n    description: \"The version of cargo-make to use\"\n    required: true\n  rust_target:\n    description: \"The target to build for\"\n    required: true\n\nruns:\n  using: \"composite\"\n\n  steps:\n    - name: Checkout source code\n      uses: actions/checkout@v4\n\n    - name: Install Rust toolchain\n      id: rust_toolchain\n      uses: actions-rs/toolchain@v1\n      with:\n        toolchain: ${{ inputs.RUST_TOOLCHAIN }}\n        target: ${{ inputs.rust_target }}\n        override: true\n        profile: minimal\n\n    - name: Install flutter\n      id: flutter\n      uses: subosito/flutter-action@v2\n      with:\n        channel: \"stable\"\n        flutter-version: ${{ inputs.flutter_version }}\n        cache: true\n\n    - uses: taiki-e/install-action@v2\n      with:\n        tool: cargo-make@${{ inputs.cargo_make_version }}\n\n    - name: Install prerequisites\n      working-directory: frontend\n      run: |\n        sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub\n        sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list\n        sudo apt-get update\n        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 libcurl4-openssl-dev\n      shell: bash\n\n    - name: Enable Flutter Desktop\n      run: |\n        flutter config --enable-linux-desktop\n      shell: bash\n\n    - uses: actions/download-artifact@v4\n      with:\n        name: ${{ github.run_id }}-ubuntu-latest\n\n    - name: Uncompressed appflowy_flutter\n      run: tar -xf appflowy_flutter.tar.gz\n      shell: bash\n\n    - name: Run Flutter integration tests\n      working-directory: frontend/appflowy_flutter\n      run: |\n        export DISPLAY=:99\n        sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &\n        sudo apt-get install network-manager\n        flutter test ${{ inputs.test_path }} -d Linux --coverage\n      shell: bash\n"
  },
  {
    "path": ".github/workflows/android_ci.yaml.bak",
    "content": "name: Android CI\n\non:\n  push:\n    branches:\n      - \"main\"\n    paths:\n      - \".github/workflows/mobile_ci.yaml\"\n      - \"frontend/**\"\n\n  pull_request:\n    branches:\n      - \"main\"\n    paths:\n      - \".github/workflows/mobile_ci.yaml\"\n      - \"frontend/**\"\n      - \"!frontend/appflowy_tauri/**\"\n\nenv:\n  CARGO_TERM_COLOR: always\n  FLUTTER_VERSION: \"3.27.4\"\n  RUST_TOOLCHAIN: \"1.85.0\"\n  CARGO_MAKE_VERSION: \"0.37.18\"\n  CLOUD_VERSION: 0.6.54-amd64\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\njobs:\n  build:\n    if: github.event.pull_request.draft != true\n    strategy:\n      fail-fast: true\n      matrix:\n        os: [ubuntu-latest]\n    runs-on: ${{ matrix.os }}\n\n    steps:\n      - name: Check storage space\n        run:\n          df -h\n\n          # the following step is required to avoid running out of space\n      - name: Maximize build space\n        if: matrix.os == 'ubuntu-latest'\n        run: |\n          sudo rm -rf /usr/share/dotnet\n          sudo rm -rf /opt/ghc\n          sudo rm -rf \"/usr/local/share/boost\"\n          sudo rm -rf \"$AGENT_TOOLSDIRECTORY\"\n          sudo docker image prune --all --force\n          sudo rm -rf /opt/hostedtoolcache/codeQL\n          sudo rm -rf ${GITHUB_WORKSPACE}/.git\n\n      - name: Check storage space\n        run: df -h\n\n      - name: Checkout appflowy cloud code\n        uses: actions/checkout@v4\n        with:\n          repository: AppFlowy-IO/AppFlowy-Cloud\n          path: AppFlowy-Cloud\n\n      - name: Prepare appflowy cloud env\n        working-directory: AppFlowy-Cloud\n        run: |\n          # log level\n          cp deploy.env .env\n          sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env\n          sed -i 's/GOTRUE_EXTERNAL_GOOGLE_ENABLED=.*/GOTRUE_EXTERNAL_GOOGLE_ENABLED=true/' .env\n          sed -i 's|GOTRUE_MAILER_AUTOCONFIRM=.*|GOTRUE_MAILER_AUTOCONFIRM=true|' .env\n          sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env\n\n      - name: Run Docker-Compose\n        working-directory: AppFlowy-Cloud\n        env:\n          APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }}\n          APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}\n          APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}\n        run: |\n          container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)\n          if [ -z \"$container_id\" ]; then\n            echo \"AppFlowy-Cloud container is not running. Pulling and starting the container...\"\n            docker compose pull\n            docker compose up -d\n            echo \"Waiting for the container to be ready...\"\n            sleep 10\n          else\n            running_image=$(docker inspect --format='{{index .Config.Image}}' \"$container_id\")\n            if [ \"$running_image\" != \"appflowy-cloud:$APPFLOWY_CLOUD_VERSION\" ]; then\n              echo \"AppFlowy-Cloud is running with an incorrect version. Restarting with the correct version...\"\n              # Remove all containers if any exist\n              if [ \"$(docker ps -aq)\" ]; then\n                docker rm -f $(docker ps -aq)\n              else\n                echo \"No containers to remove.\"\n              fi\n\n              # Remove all volumes if any exist\n              if [ \"$(docker volume ls -q)\" ]; then\n                docker volume rm $(docker volume ls -q)\n              else\n                echo \"No volumes to remove.\"\n              fi\n              docker compose pull\n              docker compose up -d\n              echo \"Waiting for the container to be ready...\"\n              sleep 10\n              docker ps -a\n              docker compose logs\n            else\n              echo \"AppFlowy-Cloud is running with the correct version.\"\n            fi\n          fi\n\n      - name: Checkout source code\n        uses: actions/checkout@v4\n\n      - uses: actions/setup-java@v4\n        with:\n          distribution: temurin\n          java-version: 11\n\n      - name: Install Rust toolchain\n        id: rust_toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n          override: true\n          profile: minimal\n\n      - name: Install flutter\n        id: flutter\n        uses: subosito/flutter-action@v2\n        with:\n          channel: \"stable\"\n          flutter-version: ${{ env.FLUTTER_VERSION }}\n\n      - uses: gradle/gradle-build-action@v3\n        with:\n          gradle-version: 8.10\n\n      - uses: davidB/rust-cargo-make@v1\n        with:\n          version: ${{ env.CARGO_MAKE_VERSION }}\n\n      - name: Install prerequisites\n        working-directory: frontend\n        run: |\n          rustup target install aarch64-linux-android\n          rustup target install x86_64-linux-android\n          rustup target add armv7-linux-androideabi\n          cargo install --force --locked duckscript_cli\n          cargo install cargo-ndk\n          if [ \"$RUNNER_OS\" == \"Linux\" ]; then\n            sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub\n            sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list\n            sudo apt-get update\n            sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev\n            sudo apt-get install keybinder-3.0 libnotify-dev\n            sudo apt-get install gcc-multilib\n          elif [ \"$RUNNER_OS\" == \"Windows\" ]; then\n            vcpkg integrate install\n          elif [ \"$RUNNER_OS\" == \"macOS\" ]; then\n            echo 'do nothing'\n          fi\n          cargo make appflowy-flutter-deps-tools\n        shell: bash\n\n      - name: Build AppFlowy\n        working-directory: frontend\n        run: |\n          cargo make --profile development-android appflowy-core-dev-android\n          cargo make --profile development-android code_generation\n          cd rust-lib\n          cargo clean\n\n      - name: Enable KVM group perms\n        run: |\n          echo 'KERNEL==\"kvm\", GROUP=\"kvm\", MODE=\"0666\", OPTIONS+=\"static_node=kvm\"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules\n          sudo udevadm control --reload-rules\n          sudo udevadm trigger --name-match=kvm\n\n      - name: Run integration tests\n        # https://github.com/ReactiveCircus/android-emulator-runner\n        uses: reactivecircus/android-emulator-runner@v2\n        with:\n          api-level: 33\n          arch: x86_64\n          disk-size: 2048M\n          working-directory: frontend/appflowy_flutter\n          disable-animations: true\n          force-avd-creation: false\n          target: google_apis\n          script: flutter test integration_test/mobile/cloud/cloud_runner.dart\n"
  },
  {
    "path": ".github/workflows/build_command.yml",
    "content": "name: build\n\non:\n  repository_dispatch:\n    types: [build-command]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: notify appflowy_builder\n        run: |\n          platform=${{ github.event.client_payload.slash_command.args.unnamed.arg1 }}\n          build_name=${{ github.event.client_payload.slash_command.args.named.build_name }}\n          branch=${{ github.event.client_payload.slash_command.args.named.ref }}\n          build_type=\"\"\n          arch=\"\"\n\n          if [ \"$platform\" = \"android\" ]; then\n            build_type=\"apk\"\n          elif [ \"$platform\" = \"macos\" ]; then\n            arch=\"universal\"\n          fi\n\n          params=$(jq -n \\\n            --arg ref \"main\" \\\n            --arg repo \"LucasXu0/AppFlowy\" \\\n            --arg branch \"$branch\" \\\n            --arg build_name \"$build_name\" \\\n            --arg build_type \"$build_type\" \\\n            --arg arch \"$arch\" \\\n            '{ref: $ref, inputs: {repo: $repo, branch: $branch, build_name: $build_name, build_type: $build_type, arch: $arch}} | del(.inputs | .. | select(. == \"\"))')\n\n          echo \"params: $params\"\n\n          curl -L \\\n            -X POST \\\n            -H \"Accept: application/vnd.github+json\" \\\n            -H \"Authorization: Bearer ${{ secrets.TOKEN }}\" \\\n            -H \"X-GitHub-Api-Version: 2022-11-28\" \\\n            https://api.github.com/repos/AppFlowy-IO/AppFlowy-Builder/actions/workflows/$platform.yaml/dispatches \\\n            -d \"$params\"\n"
  },
  {
    "path": ".github/workflows/commit_lint.yml",
    "content": "name: Commit messages lint\non: [pull_request, push]\n\njobs:\n  commitlint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - uses: wagoid/commitlint-github-action@v4\n"
  },
  {
    "path": ".github/workflows/docker_ci.yml",
    "content": "name: Docker-CI\n\non:\n  push:\n    branches: [ \"main\", \"release/*\" ]\n  pull_request:\n    branches: [ \"main\", \"release/*\" ]\n  workflow_dispatch:\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\njobs:\n  build-app:\n    if: github.event.pull_request.draft != true\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout source code\n        uses: actions/checkout@v4\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      # cache the docker layers\n      # don't cache anything temporarly, because it always triggers \"no space left on device\" error\n      # - name: Cache Docker layers\n      #   uses: actions/cache@v3\n      #   with:\n      #     path: /tmp/.buildx-cache\n      #     key: ${{ runner.os }}-buildx-${{ github.sha }}\n      #     restore-keys: |\n      #       ${{ runner.os }}-buildx-\n\n      - name: Build the app\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          file: ./frontend/scripts/docker-buildfiles/Dockerfile\n          push: false\n          # cache-from: type=local,src=/tmp/.buildx-cache\n          # cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max\n\n      # - name: Move cache\n      #   run: |\n      #     rm -rf /tmp/.buildx-cache\n      #     mv /tmp/.buildx-cache-new /tmp/.buildx-cache\n"
  },
  {
    "path": ".github/workflows/flutter_ci.yaml",
    "content": "name: Flutter-CI\n\non:\n  push:\n    branches:\n      - \"main\"\n      - \"release/*\"\n    paths:\n      - \".github/workflows/flutter_ci.yaml\"\n      - \".github/actions/flutter_build/**\"\n      - \"frontend/rust-lib/**\"\n      - \"frontend/appflowy_flutter/**\"\n      - \"frontend/resources/**\"\n\n  pull_request:\n    branches:\n      - \"main\"\n      - \"release/*\"\n    paths:\n      - \".github/workflows/flutter_ci.yaml\"\n      - \".github/actions/flutter_build/**\"\n      - \"frontend/rust-lib/**\"\n      - \"frontend/appflowy_flutter/**\"\n      - \"frontend/resources/**\"\n\nenv:\n  CARGO_TERM_COLOR: always\n  FLUTTER_VERSION: \"3.27.4\"\n  RUST_TOOLCHAIN: \"1.85.0\"\n  CARGO_MAKE_VERSION: \"0.37.18\"\n  CLOUD_VERSION: 0.9.49-amd64\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\njobs:\n  prepare-linux:\n    if: github.event.pull_request.draft != true\n    strategy:\n      fail-fast: true\n      matrix:\n        os: [ ubuntu-latest ]\n        include:\n          - os: ubuntu-latest\n            flutter_profile: development-linux-x86_64\n            target: x86_64-unknown-linux-gnu\n    runs-on: ${{ matrix.os }}\n\n    steps:\n      # the following step is required to avoid running out of space\n      - name: Maximize build space\n        run: |\n          sudo rm -rf /usr/share/dotnet\n          sudo rm -rf /opt/ghc\n          sudo rm -rf \"/usr/local/share/boost\"\n          sudo rm -rf \"$AGENT_TOOLSDIRECTORY\"\n\n      - name: Checkout source code\n        uses: actions/checkout@v4\n\n      - name: Flutter build\n        uses: ./.github/actions/flutter_build\n        with:\n          os: ${{ matrix.os }}\n          flutter_version: ${{ env.FLUTTER_VERSION }}\n          rust_toolchain: ${{ env.RUST_TOOLCHAIN }}\n          cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}\n          rust_target: ${{ matrix.target }}\n          flutter_profile: ${{ matrix.flutter_profile }}\n\n  prepare-windows:\n    if: github.event.pull_request.draft != true\n    strategy:\n      fail-fast: true\n      matrix:\n        os: [ windows-latest ]\n        include:\n          - os: windows-latest\n            flutter_profile: development-windows-x86\n            target: x86_64-pc-windows-msvc\n    runs-on: ${{ matrix.os }}\n\n    steps:\n      - name: Checkout source code\n        uses: actions/checkout@v4\n\n      - name: Flutter build\n        uses: ./.github/actions/flutter_build\n        with:\n          os: ${{ matrix.os }}\n          flutter_version: ${{ env.FLUTTER_VERSION }}\n          DISABLE_CI_TEST_LOG: \"true\"\n          rust_toolchain: ${{ env.RUST_TOOLCHAIN }}\n          cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}\n          rust_target: ${{ matrix.target }}\n          flutter_profile: ${{ matrix.flutter_profile }}\n\n  prepare-macos:\n    if: github.event.pull_request.draft != true\n    strategy:\n      fail-fast: true\n      matrix:\n        os: [ macos-latest ]\n        include:\n          - os: macos-latest\n            flutter_profile: development-mac-x86_64\n            target: x86_64-apple-darwin\n    runs-on: ${{ matrix.os }}\n\n    steps:\n      - name: Checkout source code\n        uses: actions/checkout@v4\n\n      - name: Flutter build\n        uses: ./.github/actions/flutter_build\n        with:\n          os: ${{ matrix.os }}\n          flutter_version: ${{ env.FLUTTER_VERSION }}\n          rust_toolchain: ${{ env.RUST_TOOLCHAIN }}\n          cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}\n          rust_target: ${{ matrix.target }}\n          flutter_profile: ${{ matrix.flutter_profile }}\n\n  unit_test:\n    needs: [ prepare-linux ]\n    if: github.event.pull_request.draft != true\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ ubuntu-latest ]\n        include:\n          - os: ubuntu-latest\n            flutter_profile: development-linux-x86_64\n            target: x86_64-unknown-linux-gnu\n    runs-on: ${{ matrix.os }}\n\n    steps:\n      - name: Checkout source code\n        uses: actions/checkout@v4\n\n      - name: Install Rust toolchain\n        id: rust_toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n          target: ${{ matrix.target }}\n          override: true\n          profile: minimal\n\n      - name: Install flutter\n        id: flutter\n        uses: subosito/flutter-action@v2\n        with:\n          channel: \"stable\"\n          flutter-version: ${{ env.FLUTTER_VERSION }}\n          cache: true\n\n      - uses: Swatinem/rust-cache@v2\n        with:\n          prefix-key: ${{ matrix.os }}\n          workspaces: |\n            frontend/rust-lib\n          cache-all-crates: true\n\n      - uses: taiki-e/install-action@v2\n        with:\n          tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}, duckscript_cli\n\n      - name: Install prerequisites\n        working-directory: frontend\n        run: |\n          if [ \"$RUNNER_OS\" == \"Linux\" ]; then\n            sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub\n            sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list\n            sudo apt-get update\n            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 libcurl4-openssl-dev\n          fi\n        shell: bash\n\n      - name: Enable Flutter Desktop\n        run: |\n          if [ \"$RUNNER_OS\" == \"Linux\" ]; then\n            flutter config --enable-linux-desktop\n          elif [ \"$RUNNER_OS\" == \"macOS\" ]; then\n            flutter config --enable-macos-desktop\n          elif [ \"$RUNNER_OS\" == \"Windows\" ]; then\n            git config --system core.longpaths true\n            flutter config --enable-windows-desktop\n          fi\n        shell: bash\n\n      - uses: actions/download-artifact@v4\n        with:\n          name: ${{ github.run_id }}-${{ matrix.os }}\n\n      - name: Uncompress appflowy_flutter\n        run: tar -xf appflowy_flutter.tar.gz\n\n      - name: Run flutter pub get\n        working-directory: frontend\n        run: cargo make pub_get\n\n      - name: Run Flutter unit tests\n        env:\n          DISABLE_EVENT_LOG: true\n          DISABLE_CI_TEST_LOG: \"true\"\n        working-directory: frontend\n        run: |\n          if [ \"$RUNNER_OS\" == \"macOS\" ]; then\n            cargo make dart_unit_test\n          elif [ \"$RUNNER_OS\" == \"Linux\" ]; then\n            cargo make dart_unit_test_no_build\n          elif [ \"$RUNNER_OS\" == \"Windows\" ]; then\n            cargo make dart_unit_test_no_build\n          fi\n        shell: bash\n\n  cloud_integration_test:\n    needs: [ prepare-linux ]\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ ubuntu-latest ]\n        include:\n          - os: ubuntu-latest\n            flutter_profile: development-linux-x86_64\n            target: x86_64-unknown-linux-gnu\n    runs-on: ${{ matrix.os }}\n\n    steps:\n      - name: Checkout appflowy cloud code\n        uses: actions/checkout@v4\n        with:\n          repository: AppFlowy-IO/AppFlowy-Cloud\n          path: AppFlowy-Cloud\n\n      - name: Prepare appflowy cloud env\n        working-directory: AppFlowy-Cloud\n        run: |\n          # log level\n          cp deploy.env .env\n          sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env\n          sed -i 's/GOTRUE_EXTERNAL_GOOGLE_ENABLED=.*/GOTRUE_EXTERNAL_GOOGLE_ENABLED=true/' .env\n          sed -i 's|GOTRUE_MAILER_AUTOCONFIRM=.*|GOTRUE_MAILER_AUTOCONFIRM=true|' .env\n          sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env\n\n      - name: Run Docker-Compose\n        working-directory: AppFlowy-Cloud\n        env:\n          APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }}\n          APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}\n          APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}\n        run: |\n          container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)\n          if [ -z \"$container_id\" ]; then\n            echo \"AppFlowy-Cloud container is not running. Pulling and starting the container...\"\n            docker compose pull\n            docker compose up -d\n            echo \"Waiting for the container to be ready...\"\n            sleep 10\n          else\n            running_image=$(docker inspect --format='{{index .Config.Image}}' \"$container_id\")\n            if [ \"$running_image\" != \"appflowy-cloud:$APPFLOWY_CLOUD_VERSION\" ]; then\n              echo \"AppFlowy-Cloud is running with an incorrect version. Restarting with the correct version...\"\n              # Remove all containers if any exist\n              if [ \"$(docker ps -aq)\" ]; then\n                docker rm -f $(docker ps -aq)\n              else\n                echo \"No containers to remove.\"\n              fi\n\n              # Remove all volumes if any exist\n              if [ \"$(docker volume ls -q)\" ]; then\n                docker volume rm $(docker volume ls -q)\n              else\n                echo \"No volumes to remove.\"\n              fi\n              docker compose pull\n              docker compose up -d\n              echo \"Waiting for the container to be ready...\"\n              sleep 10\n              docker ps -a\n              docker compose logs\n            else\n              echo \"AppFlowy-Cloud is running with the correct version.\"\n            fi\n          fi\n\n      - name: Checkout source code\n        uses: actions/checkout@v4\n\n      - name: Install flutter\n        id: flutter\n        uses: subosito/flutter-action@v2\n        with:\n          channel: \"stable\"\n          flutter-version: ${{ env.FLUTTER_VERSION }}\n          cache: true\n\n      - uses: taiki-e/install-action@v2\n        with:\n          tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}\n\n      - name: Install prerequisites\n        working-directory: frontend\n        run: |\n          sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub\n          sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list\n          sudo apt-get update\n          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 libcurl4-openssl-dev\n        shell: bash\n\n      - name: Enable Flutter Desktop\n        run: |\n          flutter config --enable-linux-desktop\n        shell: bash\n\n      - uses: actions/download-artifact@v4\n        with:\n          name: ${{ github.run_id }}-${{ matrix.os }}\n\n      - name: Uncompressed appflowy_flutter\n        run: |\n          tar -xf appflowy_flutter.tar.gz\n          ls -al\n\n      - name: Run flutter pub get\n        working-directory: frontend\n        run: cargo make pub_get\n\n      - name: Run Flutter integration tests\n        working-directory: frontend/appflowy_flutter\n        run: |\n          export DISPLAY=:99\n          sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &\n          sudo apt-get install network-manager\n          docker ps -a\n          flutter test integration_test/desktop/cloud/cloud_runner.dart -d Linux --coverage\n        shell: bash\n\n  integration_test:\n    needs: [ prepare-linux ]\n    if: github.event.pull_request.draft != true\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ ubuntu-latest ]\n        test_number: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]\n        include:\n          - os: ubuntu-latest\n            target: \"x86_64-unknown-linux-gnu\"\n    runs-on: ${{ matrix.os }}\n    steps:\n      - name: Checkout source code\n        uses: actions/checkout@v4\n\n      - name: Flutter Integration Test ${{ matrix.test_number }}\n        uses: ./.github/actions/flutter_integration_test\n        with:\n          test_path: integration_test/desktop_runner_${{ matrix.test_number }}.dart\n          flutter_version: ${{ env.FLUTTER_VERSION }}\n          rust_toolchain: ${{ env.RUST_TOOLCHAIN }}\n          cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}\n          rust_target: ${{ matrix.target }}\n"
  },
  {
    "path": ".github/workflows/ios_ci.yaml",
    "content": "name: iOS CI\n\non:\n  push:\n    branches:\n      - \"main\"\n    paths:\n      - \".github/workflows/mobile_ci.yaml\"\n      - \"frontend/**\"\n      - \"!frontend/appflowy_web_app/**\"\n\n  pull_request:\n    branches:\n      - \"main\"\n    paths:\n      - \".github/workflows/mobile_ci.yaml\"\n      - \"frontend/**\"\n      - \"!frontend/appflowy_web_app/**\"\n\nenv:\n  FLUTTER_VERSION: \"3.27.4\"\n  RUST_TOOLCHAIN: \"1.85.0\"\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\njobs:\n  integration-tests:\n    runs-on: macos-latest\n\n    steps:\n      - name: Checkout source code\n        uses: actions/checkout@v4\n\n      - name: Install Rust toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n          target: aarch64-apple-ios-sim\n          override: true\n          profile: minimal\n\n      - name: Install Flutter\n        uses: subosito/flutter-action@v2\n        with:\n          channel: \"stable\"\n          flutter-version: ${{ env.FLUTTER_VERSION }}\n          cache: true\n\n      - uses: Swatinem/rust-cache@v2\n        with:\n          prefix-key: macos-latest\n          workspaces: |\n            frontend/rust-lib\n\n      - uses: davidB/rust-cargo-make@v1\n        with:\n          version: \"0.37.15\"\n\n      - name: Install prerequisites\n        working-directory: frontend\n        run: |\n          rustup target install aarch64-apple-ios-sim\n          cargo install --force --locked duckscript_cli\n          cargo install cargo-lipo\n          cargo make appflowy-flutter-deps-tools\n        shell: bash\n\n      - name: Build AppFlowy\n        working-directory: frontend\n        run: |\n          cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios\n          cargo make --profile development-ios-arm64-sim code_generation\n\n      # - uses: futureware-tech/simulator-action@v3\n      #   id: simulator-action\n      #   with:\n      #     model: \"iPhone 15\"\n      #     shutdown_after_job: false\n\n      # - name: Run AppFlowy on simulator\n      #   working-directory: frontend/appflowy_flutter\n      #   run: |\n      #     flutter run -d ${{ steps.simulator-action.outputs.udid }} &\n      #     pid=$!\n      #     sleep 500\n      #     kill $pid\n      #   continue-on-error: true\n\n      # # Integration tests\n      # - name: Run integration tests\n      #   working-directory: frontend/appflowy_flutter\n      #   # The integration tests are flaky and sometimes fail with \"Connection timed out\":\n      #   # Don't block the CI. If the tests fail, the CI will still pass.\n      #   # Instead, we're using Code Magic to re-run the tests to check if they pass.\n      #   continue-on-error: true\n      #   run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }}\n"
  },
  {
    "path": ".github/workflows/mobile_ci.yml",
    "content": "name: Mobile-CI\n\non:\n  workflow_dispatch:\n    inputs:\n      branch:\n        description: \"Branch to build\"\n        required: true\n        default: \"main\"\n      workflow_id:\n        description: \"Codemagic workflow ID\"\n        required: true\n        default: \"ios-workflow\"\n        type: choice\n        options:\n          - ios-workflow\n          - android-workflow\n\nenv:\n  CODEMAGIC_API_TOKEN: ${{ secrets.CODEMAGIC_API_TOKEN }}\n  APP_ID: \"6731d2f427e7c816080c3674\"\n\njobs:\n  trigger-mobile-build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Trigger Codemagic Build\n        id: trigger_build\n        run: |\n          RESPONSE=$(curl -X POST \\\n            --header \"Content-Type: application/json\" \\\n            --header \"x-auth-token: $CODEMAGIC_API_TOKEN\" \\\n            --data '{\n              \"appId\": \"${{ env.APP_ID }}\",\n              \"workflowId\": \"${{ github.event.inputs.workflow_id }}\",\n              \"branch\": \"${{ github.event.inputs.branch }}\"\n            }' \\\n            https://api.codemagic.io/builds)\n\n          BUILD_ID=$(echo $RESPONSE | jq -r '.buildId')\n          echo \"build_id=$BUILD_ID\" >> $GITHUB_OUTPUT\n          echo \"build_id=$BUILD_ID\"\n\n      - name: Wait for build and check status\n        id: check_status\n        run: |\n          while true; do\n            curl -X GET \\\n              --header \"Content-Type: application/json\" \\\n              --header \"x-auth-token: $CODEMAGIC_API_TOKEN\" \\\n              https://api.codemagic.io/builds/${{ steps.trigger_build.outputs.build_id }} > /tmp/response.json\n\n            RESPONSE_WITHOUT_COMMAND=$(cat /tmp/response.json | jq 'walk(if type == \"object\" and has(\"subactions\") then .subactions |= map(del(.command)) else . end)')\n            STATUS=$(echo $RESPONSE_WITHOUT_COMMAND | jq -r '.build.status')\n\n            if [ \"$STATUS\" = \"finished\" ]; then\n              SUCCESS=$(echo $RESPONSE_WITHOUT_COMMAND | jq -r '.success')\n              BUILD_URL=$(echo $RESPONSE_WITHOUT_COMMAND | jq -r '.buildUrl')\n              echo \"status=$STATUS\" >> $GITHUB_OUTPUT\n              echo \"success=$SUCCESS\" >> $GITHUB_OUTPUT\n              echo \"build_url=$BUILD_URL\" >> $GITHUB_OUTPUT\n              break\n            elif [ \"$STATUS\" = \"failed\" ]; then\n              echo \"status=failed\" >> $GITHUB_OUTPUT\n              break\n            fi\n\n            sleep 60\n          done\n\n      - name: Slack Notification\n        uses: 8398a7/action-slack@v3\n        if: always()\n        with:\n          status: ${{ steps.check_status.outputs.success == 'true' && 'success' || 'failure' }}\n          fields: repo,message,commit,author,action,eventName,ref,workflow,job,took\n          text: |\n            Mobile CI Build Result\n            Branch: ${{ github.event.inputs.branch }}\n            Workflow: ${{ github.event.inputs.workflow_id }}\n            Build URL: ${{ steps.check_status.outputs.build_url }}\n        env:\n          SLACK_WEBHOOK_URL: ${{ secrets.RELEASE_SLACK_WEBHOOK }}\n"
  },
  {
    "path": ".github/workflows/ninja_i18n.yml",
    "content": "name: Ninja i18n action\n\non:\n  pull_request_target:\n\n# explicitly configure permissions, in case your GITHUB_TOKEN workflow permissions are set to read-only in repository settings\npermissions: \n  pull-requests: write\n\njobs:\n  ninja-i18n:\n    name: Ninja i18n - GitHub Lint Action\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        id: checkout\n        uses: actions/checkout@v4\n\n      - name: Run Ninja i18n\n        id: ninja-i18n\n        uses: opral/ninja-i18n-action@main\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          \n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: release\n\non:\n  push:\n    tags:\n      - \"*\"\n\nenv:\n  FLUTTER_VERSION: \"3.27.4\"\n  RUST_TOOLCHAIN: \"1.85.0\"\n\njobs:\n  create-release:\n    runs-on: ubuntu-latest\n    env:\n      RELEASE_NOTES_PATH: /tmp/release_notes\n    outputs:\n      upload_url: ${{ steps.create_release.outputs.upload_url }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Build release notes\n        run: |\n          touch ${{ env.RELEASE_NOTES_PATH }}\n          cat CHANGELOG.md | sed -e '/./{H;$!d;}' -e \"x;/##\\ Version\\ ${{ github.ref_name }}/\"'!d;' >> ${{ env.RELEASE_NOTES_PATH }}\n\n      - name: Create release\n        id: create_release\n        uses: actions/create-release@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: ${{ github.ref }}\n          release_name: v${{ github.ref }}\n          body_path: ${{ env.RELEASE_NOTES_PATH }}\n\n  # the package name should be with the format: AppFlowy-<version>-<os>-<arch>\n\n  build-for-windows:\n    name: ${{ matrix.job.target }} (${{ matrix.job.os }})\n    needs: create-release\n    env:\n      WINDOWS_APP_RELEASE_PATH: frontend\\appflowy_flutter\\product\\${{ github.ref_name }}\\windows\n      WINDOWS_ZIP_NAME: AppFlowy-${{ github.ref_name }}-windows-x86_64.zip\n      WINDOWS_INSTALLER_NAME: AppFlowy-${{ github.ref_name }}-windows-x86_64\n    runs-on: ${{ matrix.job.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        job:\n          - { target: x86_64-pc-windows-msvc, os: windows-2019 }\n    steps:\n      - name: Checkout source code\n        uses: actions/checkout@v4\n\n      - name: Install flutter\n        uses: subosito/flutter-action@v2\n        with:\n          channel: \"stable\"\n          flutter-version: ${{ env.FLUTTER_VERSION }}\n\n      - name: Install Rust toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n          target: ${{ matrix.job.target }}\n          override: true\n          components: rustfmt\n          profile: minimal\n\n      - name: Install prerequisites\n        working-directory: frontend\n        run: |\n          vcpkg integrate install\n          cargo install --force --locked cargo-make\n          cargo install --force --locked duckscript_cli\n\n      - name: Build Windows app\n        working-directory: frontend\n        # the cargo make script has to be run separately because of file locking issues\n        run: |\n          flutter config --enable-windows-desktop\n          dart ./scripts/flutter_release_build/build_flowy.dart exclude-directives . ${{ github.ref_name }}\n          cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-windows-x86 appflowy\n          dart ./scripts/flutter_release_build/build_flowy.dart include-directives . ${{ github.ref_name }}\n\n      - name: Archive Asset\n        uses: vimtor/action-zip@v1\n        with:\n          files: ${{ env.WINDOWS_APP_RELEASE_PATH }}\\\n          dest: ${{ env.WINDOWS_APP_RELEASE_PATH }}\\${{ env.WINDOWS_ZIP_NAME }}\n\n      - name: Copy installer config & icon file\n        working-directory: frontend\n        run: |\n          cp scripts/windows_installer/* ../${{ env.WINDOWS_APP_RELEASE_PATH }}\n\n      - name: Build installer executable\n        working-directory: ${{ env.WINDOWS_APP_RELEASE_PATH }}\n        run: |\n          iscc /F${{ env.WINDOWS_INSTALLER_NAME }} inno_setup_config.iss /DAppVersion=${{ github.ref_name }}\n\n      - name: Upload Asset\n        id: upload-release-asset\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ needs.create-release.outputs.upload_url }}\n          asset_path: ${{ env.WINDOWS_APP_RELEASE_PATH }}\\${{ env.WINDOWS_ZIP_NAME }}\n          asset_name: ${{ env.WINDOWS_ZIP_NAME }}\n          asset_content_type: application/octet-stream\n\n      - name: Upload Installer Asset\n        id: upload-installer-asset\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ needs.create-release.outputs.upload_url }}\n          asset_path: ${{ env.WINDOWS_APP_RELEASE_PATH }}\\Output\\${{ env.WINDOWS_INSTALLER_NAME }}.exe\n          asset_name: ${{ env.WINDOWS_INSTALLER_NAME }}.exe\n          asset_content_type: application/octet-stream\n\n  build-for-macOS-x86_64:\n    name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}]\n    runs-on: ${{ matrix.job.os }}\n    needs: create-release\n    env:\n      MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/macos/Release\n      MACOS_X86_ZIP_NAME: AppFlowy-${{ github.ref_name }}-macos-x86_64.zip\n      MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-x86_64\n    strategy:\n      fail-fast: false\n      matrix:\n        job:\n          - { target: x86_64-apple-darwin, os: macos-13, extra-build-args: \"\" }\n    steps:\n      - name: Checkout source code\n        uses: actions/checkout@v4\n\n      - name: Install flutter\n        uses: subosito/flutter-action@v2\n        with:\n          channel: \"stable\"\n          flutter-version: ${{ env.FLUTTER_VERSION }}\n\n      - name: Install Rust toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n          target: ${{ matrix.job.target }}\n          override: true\n          components: rustfmt\n          profile: minimal\n\n      - name: Install prerequisites\n        working-directory: frontend\n        run: |\n          cargo install --force --locked cargo-make\n          cargo install --force --locked duckscript_cli\n\n      - name: Build AppFlowy\n        working-directory: frontend\n        run: |\n          flutter config --enable-macos-desktop\n          dart ./scripts/flutter_release_build/build_flowy.dart run . ${{ github.ref_name }}\n\n      - name: Codesign AppFlowy\n        run: |\n          echo ${{ secrets.MACOS_CERTIFICATE }} | base64 --decode > certificate.p12\n          security create-keychain -p action build.keychain\n          security default-keychain -s build.keychain\n          security unlock-keychain -p action build.keychain\n          security import certificate.p12 -k build.keychain -P ${{ secrets.MACOS_CERTIFICATE_PWD }} -T /usr/bin/codesign\n          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k action build.keychain\n          /usr/bin/codesign --force --options runtime --deep --sign \"${{ secrets.MACOS_CODESIGN_ID }}\" \"${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app\" -v\n\n      - name: Create macOS dmg\n        run: |\n          brew install create-dmg\n          i=0\n          until [[ -e \"${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg\" ]]; do\n            create-dmg \\\n            --volname ${{ env.MACOS_DMG_NAME }} \\\n            --hide-extension \"AppFlowy.app\" \\\n            --background frontend/scripts/dmg_assets/AppFlowyInstallerBackground.jpg \\\n            --window-size 600 450 \\\n            --icon-size 94 \\\n            --icon \"AppFlowy.app\" 141 249 \\\n            --app-drop-link 458 249 \\\n            \"${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg\" \\\n            \"${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app\" || true\n            if [[ $i -eq 10 ]]; then\n              echo 'Error: create-dmg did not succeed even after 10 tries.'\n              exit 1\n            fi\n            i=$((i+1))\n          done\n      - name: Notarize AppFlowy\n        run: |\n          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\n\n      - name: Archive Asset\n        working-directory: ${{ env.MACOS_APP_RELEASE_PATH }}\n        run: zip --symlinks -qr ${{ env.MACOS_X86_ZIP_NAME }} AppFlowy.app\n\n      - name: Upload Asset\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ needs.create-release.outputs.upload_url }}\n          asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_X86_ZIP_NAME }}\n          asset_name: ${{ env.MACOS_X86_ZIP_NAME }}\n          asset_content_type: application/octet-stream\n\n      - name: Upload DMG Asset\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ needs.create-release.outputs.upload_url }}\n          asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg\n          asset_name: ${{ env.MACOS_DMG_NAME }}.dmg\n          asset_content_type: application/octet-stream\n\n  build-for-macOS-universal:\n    name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}]\n    runs-on: ${{ matrix.job.os }}\n    needs: create-release\n    env:\n      MACOS_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/macos/Release\n      MACOS_AARCH64_ZIP_NAME: AppFlowy-${{ github.ref_name }}-macos-universal.zip\n      MACOS_DMG_NAME: AppFlowy-${{ github.ref_name }}-macos-universal\n    strategy:\n      fail-fast: false\n      matrix:\n        job:\n          - {\n            targets: \"aarch64-apple-darwin,x86_64-apple-darwin\",\n            os: macos-14,\n            extra-build-args: \"\",\n          }\n    steps:\n      - name: Checkout source code\n        uses: actions/checkout@v4\n\n      - name: Install flutter\n        uses: subosito/flutter-action@v2\n        with:\n          channel: \"stable\"\n          flutter-version: ${{ env.FLUTTER_VERSION }}\n\n      - name: Install Rust toolchain\n        uses: dtolnay/rust-toolchain@stable\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n          targets: ${{ matrix.job.targets }}\n          components: rustfmt\n\n      - name: Install prerequisites\n        working-directory: frontend\n        run: |\n          cargo install --force --locked cargo-make\n          cargo install --force --locked duckscript_cli\n\n      - name: Build AppFlowy\n        working-directory: frontend\n        run: |\n          flutter config --enable-macos-desktop\n          sh scripts/flutter_release_build/build_universal_package_for_macos.sh ${{ github.ref_name }}\n\n      - name: Codesign AppFlowy\n        run: |\n          echo ${{ secrets.MACOS_CERTIFICATE }} | base64 --decode > certificate.p12\n          security create-keychain -p action build.keychain\n          security default-keychain -s build.keychain\n          security unlock-keychain -p action build.keychain\n          security import certificate.p12 -k build.keychain -P ${{ secrets.MACOS_CERTIFICATE_PWD }} -T /usr/bin/codesign\n          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k action build.keychain\n          /usr/bin/codesign --force --options runtime --deep --sign \"${{ secrets.MACOS_CODESIGN_ID }}\" \"${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app\" -v\n\n      - name: Create macOS dmg\n        run: |\n          brew install create-dmg\n          create-dmg \\\n          --volname ${{ env.MACOS_DMG_NAME }} \\\n          --hide-extension \"AppFlowy.app\" \\\n          --background frontend/scripts/dmg_assets/AppFlowyInstallerBackground.jpg \\\n          --window-size 600 450 \\\n          --icon-size 94 \\\n          --icon \"AppFlowy.app\" 141 249 \\\n          --app-drop-link 458 249 \\\n          \"${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg\" \\\n          \"${{ env.MACOS_APP_RELEASE_PATH }}/AppFlowy.app\"\n\n      - name: Notarize AppFlowy\n        run: |\n          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\n\n      - name: Archive Asset\n        working-directory: ${{ env.MACOS_APP_RELEASE_PATH }}\n        run: zip --symlinks -qr ${{ env.MACOS_AARCH64_ZIP_NAME }} AppFlowy.app\n\n      - name: Upload Asset\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ needs.create-release.outputs.upload_url }}\n          asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_AARCH64_ZIP_NAME }}\n          asset_name: ${{ env.MACOS_AARCH64_ZIP_NAME }}\n          asset_content_type: application/octet-stream\n\n      - name: Upload DMG Asset\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ needs.create-release.outputs.upload_url }}\n          asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_DMG_NAME }}.dmg\n          asset_name: ${{ env.MACOS_DMG_NAME }}.dmg\n          asset_content_type: application/octet-stream\n\n  build-for-linux:\n    name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}]\n    runs-on: ${{ matrix.job.os }}\n    needs: create-release\n    env:\n      LINUX_APP_RELEASE_PATH: frontend/appflowy_flutter/product/${{ github.ref_name }}/linux/Release\n      LINUX_ZIP_NAME: AppFlowy-${{ matrix.job.target }}-x86_64.tar.gz\n      LINUX_PACKAGE_DEB_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.deb\n      LINUX_PACKAGE_RPM_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.rpm\n      LINUX_PACKAGE_TMP_RPM_NAME: AppFlowy-${{ github.ref_name }}-2.x86_64.rpm\n      LINUX_PACKAGE_TMP_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-x86_64.AppImage\n      LINUX_PACKAGE_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.AppImage\n      LINUX_PACKAGE_ZIP_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.tar.gz\n\n    strategy:\n      fail-fast: false\n      matrix:\n        job:\n          - {\n            arch: x86_64,\n            target: x86_64-unknown-linux-gnu,\n            os: ubuntu-22.04,\n            extra-build-args: \"\",\n            flutter_profile: production-linux-x86_64,\n          }\n    steps:\n      - name: Checkout source code\n        uses: actions/checkout@v4\n\n      - name: Install flutter\n        uses: subosito/flutter-action@v2\n        with:\n          channel: \"stable\"\n          flutter-version: ${{ env.FLUTTER_VERSION }}\n\n      - name: Install Rust toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n          target: ${{ matrix.job.target }}\n          override: true\n          components: rustfmt\n          profile: minimal\n\n      - name: Install prerequisites\n        working-directory: frontend\n        run: |\n          sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub\n          sudo apt-get update\n          sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev libcurl4-openssl-dev\n          sudo apt-get install keybinder-3.0\n          sudo apt-get install -y alien libnotify-dev\n          sudo apt install libfuse2\n          source $HOME/.cargo/env\n          cargo install --force --locked cargo-make\n          cargo install --force --locked duckscript_cli\n          rustup target add ${{ matrix.job.target }}\n\n      - name: Install gcc-aarch64-linux-gnu\n        if: ${{ matrix.job.target == 'aarch64-unknown-linux-gnu' }}\n        working-directory: frontend\n        run: |\n          sudo apt-get install -qy binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libgtk-3-0\n\n      - name: Build AppFlowy\n        working-directory: frontend\n        run: |\n          flutter config --enable-linux-desktop\n          dart ./scripts/flutter_release_build/build_flowy.dart run . ${{ github.ref_name }}\n\n      - name: Archive Asset\n        working-directory: ${{ env.LINUX_APP_RELEASE_PATH }}\n        run: tar -czf ${{ env.LINUX_ZIP_NAME }} *\n\n      - name: Build Linux package (.deb)\n        working-directory: frontend\n        run: |\n          sh scripts/linux_distribution/deb/build_deb.sh appflowy_flutter/product/${{ github.ref_name }}/linux/Release ${{ github.ref_name }} ${{ env.LINUX_PACKAGE_DEB_NAME }}\n\n      - name: Build Linux package (.rpm)\n        working-directory: ${{ env.LINUX_APP_RELEASE_PATH }}\n        run: |\n          sudo alien -r ${{ env.LINUX_PACKAGE_DEB_NAME }}\n          cp -r ${{ env.LINUX_PACKAGE_TMP_RPM_NAME }} ${{ env.LINUX_PACKAGE_RPM_NAME }}\n\n      - name: Build Linux package (.AppImage)\n        working-directory: frontend\n        continue-on-error: true\n        run: |\n          sh scripts/linux_distribution/appimage/build_appimage.sh ${{ github.ref_name }}\n          cd ..\n          cp -r frontend/${{ env.LINUX_PACKAGE_TMP_APPIMAGE_NAME }} ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_APPIMAGE_NAME }}\n\n      - name: Upload Asset\n        id: upload-release-asset\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ needs.create-release.outputs.upload_url }}\n          asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_ZIP_NAME }}\n          asset_name: ${{ env.LINUX_PACKAGE_ZIP_NAME }}\n          asset_content_type: application/octet-stream\n\n      - name: Upload Debian package\n        id: upload-release-asset-install-package-deb\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ needs.create-release.outputs.upload_url }}\n          asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_DEB_NAME }}\n          asset_name: ${{ env.LINUX_PACKAGE_DEB_NAME }}\n          asset_content_type: application/octet-stream\n\n      - name: Upload RPM package\n        id: upload-release-asset-install-package-rpm\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ needs.create-release.outputs.upload_url }}\n          asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_RPM_NAME }}\n          asset_name: ${{ env.LINUX_PACKAGE_RPM_NAME }}\n          asset_content_type: application/octet-stream\n\n      - name: Upload AppImage package\n        id: upload-release-asset-install-package-appimage\n        uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ needs.create-release.outputs.upload_url }}\n          asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_APPIMAGE_NAME }}\n          asset_name: ${{ env.LINUX_PACKAGE_APPIMAGE_NAME }}\n          asset_content_type: application/octet-stream\n\n  build-for-docker:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKER_HUB_USERNAME }}\n          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Build and push\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          file: ./frontend/scripts/docker-buildfiles/Dockerfile\n          builder: ${{ steps.buildx.outputs.name }}\n          push: true\n          tags: ${{ secrets.DOCKER_HUB_USERNAME }}/appflowy_client:${{ github.ref_name }}\n          cache-from: type=registry,ref=${{ secrets.DOCKER_HUB_USERNAME }}/af_build_cache:buildcache\n          cache-to: type=registry,ref=${{ secrets.DOCKER_HUB_USERNAME }}/af_build_cache:buildcache,mode=max\n\n  notify-failure:\n    runs-on: ubuntu-latest\n    needs:\n      - build-for-macOS-x86_64\n      - build-for-windows\n      - build-for-linux\n    if: failure()\n    steps:\n      - uses: 8398a7/action-slack@v3\n        with:\n          status: ${{ job.status }}\n          text: |\n            🔴🔴🔴Workflow ${{ github.workflow }} in repository ${{ github.repository }} was failed 🔴🔴🔴.\n          fields: repo,message,author,eventName,ref,workflow\n        env:\n          SLACK_WEBHOOK_URL: ${{ secrets.RELEASE_SLACK_WEBHOOK }}\n        if: always()\n\n  notify-discord:\n    runs-on: ubuntu-latest\n    needs:\n      [\n        build-for-linux,\n        build-for-windows,\n        build-for-macOS-x86_64,\n        build-for-macOS-universal,\n      ]\n    steps:\n      - name: Notify Discord\n        run: |\n          curl -H \"Content-Type: application/json\" -d '{\"username\": \"release@appflowy\", \"content\": \"🎉 AppFlowy ${{ github.ref_name }} is available. https://github.com/AppFlowy-IO/AppFlowy/releases/tag/'${{ github.ref_name }}'\"}' \"https://discord.com/api/webhooks/${{ secrets.DISCORD }}\"\n        shell: bash\n"
  },
  {
    "path": ".github/workflows/rust_ci.yaml",
    "content": "name: Rust-CI\n\non:\n  push:\n    branches:\n      - \"main\"\n      - \"develop\"\n      - \"release/*\"\n    paths:\n      - \"frontend/rust-lib/**\"\n      - \".github/workflows/rust_ci.yaml\"\n\n  pull_request:\n    branches:\n      - \"main\"\n      - \"develop\"\n      - \"release/*\"\n\nenv:\n  CARGO_TERM_COLOR: always\n  CLOUD_VERSION: 0.9.49-amd64\n  RUST_TOOLCHAIN: \"1.85.0\"\n\njobs:\n  ubuntu-job:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Set timezone for action\n        uses: szenius/set-timezone@v2.0\n        with:\n          timezoneLinux: \"US/Pacific\"\n\n      - name: Maximize build space\n        run: |\n          sudo rm -rf /usr/share/dotnet\n          sudo rm -rf /opt/ghc\n          sudo rm -rf \"/usr/local/share/boost\"\n          sudo rm -rf \"$AGENT_TOOLSDIRECTORY\"\n          sudo docker image prune --all --force\n\n      - name: Checkout source code\n        uses: actions/checkout@v4\n\n      - name: Install Rust toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n          override: true\n          components: rustfmt, clippy\n          profile: minimal\n      - uses: Swatinem/rust-cache@v2\n        with:\n          prefix-key: ${{ runner.os }}\n          cache-on-failure: true\n          workspaces: |\n            frontend/rust-lib\n\n      - name: Checkout appflowy cloud code\n        uses: actions/checkout@v4\n        with:\n          repository: AppFlowy-IO/AppFlowy-Cloud\n          path: AppFlowy-Cloud\n\n      - name: Prepare appflowy cloud env\n        working-directory: AppFlowy-Cloud\n        run: |\n          cp deploy.env .env\n          sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env\n          sed -i 's|GOTRUE_MAILER_AUTOCONFIRM=.*|GOTRUE_MAILER_AUTOCONFIRM=true|' .env\n          sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env\n\n      - name: Ensure AppFlowy-Cloud is Running with Correct Version\n        working-directory: AppFlowy-Cloud\n        env:\n          APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }}\n          APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }}\n          APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }}\n        run: |\n          # Remove all containers if any exist\n          if [ \"$(docker ps -aq)\" ]; then\n            docker rm -f $(docker ps -aq)\n          else\n            echo \"No containers to remove.\"\n          fi\n\n          # Remove all volumes if any exist\n          if [ \"$(docker volume ls -q)\" ]; then\n            docker volume rm $(docker volume ls -q)\n          else\n            echo \"No volumes to remove.\"\n          fi\n\n          docker compose pull\n          docker compose up -d\n          echo \"Waiting for the container to be ready...\"\n          sleep 10\n          docker ps -a\n          docker compose logs\n\n      - name: Run rust-lib tests\n        working-directory: frontend/rust-lib\n        env:\n          RUST_LOG: info\n          RUST_BACKTRACE: 1\n          af_cloud_test_base_url: http://localhost\n          af_cloud_test_ws_url: ws://localhost/ws/v1\n          af_cloud_test_gotrue_url: http://localhost/gotrue\n        run: |\n          DISABLE_CI_TEST_LOG=\"true\" cargo test --no-default-features --features=\"dart\" -- --skip local_ollama_test\n\n      - name: rustfmt rust-lib\n        run: cargo fmt --all -- --check\n        working-directory: frontend/rust-lib/\n\n      - name: clippy rust-lib\n        run: cargo clippy --all-targets -- -D warnings\n        working-directory: frontend/rust-lib\n\n      - name: \"Debug: show Appflowy-Cloud container logs\"\n        if: failure()\n        working-directory: AppFlowy-Cloud\n        run: |\n          docker compose logs appflowy_cloud\n\n      - name: Clean up Docker images\n        run: |\n          docker image prune -af\n          docker volume prune -f\n"
  },
  {
    "path": ".github/workflows/rust_coverage.yml",
    "content": "name: Rust code coverage\n\non:\n  push:\n    branches:\n      - \"main\"\n      - \"release/*\"\n    paths:\n      - \"frontend/rust-lib/**\"\n\nenv:\n  CARGO_TERM_COLOR: always\n  FLUTTER_VERSION: \"3.27.4\"\n  RUST_TOOLCHAIN: \"1.85.0\"\n\njobs:\n  tests:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout source code\n        uses: actions/checkout@v4\n\n      - name: Install Rust toolchain\n        id: rust_toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          toolchain: ${{ env.RUST_TOOLCHAIN }}\n          target: ${{ matrix.job.target }}\n          override: true\n          profile: minimal\n\n      - name: Install flutter\n        id: flutter\n        uses: subosito/flutter-action@v2\n        with:\n          channel: \"stable\"\n          flutter-version: ${{ env.FLUTTER_VERSION }}\n          cache: true\n\n      - name: Install prerequisites\n        working-directory: frontend\n        run: |\n          cargo install --force --locked cargo-make\n          cargo install --force --locked duckscript_cli\n\n      - uses: Swatinem/rust-cache@v2\n        with:\n          prefix-key: ${{ matrix.job.os }}\n\n      - name: Install code-coverage tools\n        working-directory: frontend\n        run: |\n          sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub\n          sudo apt-get update\n          sudo apt-get install keybinder-3.0\n          cargo install grcov\n          rustup component add llvm-tools-preview\n\n      - name: Run tests\n        working-directory: frontend\n        run: cargo make rust_unit_test_with_coverage\n"
  },
  {
    "path": ".github/workflows/translation_notify.yml",
    "content": "name: Translation Notify\non:\n  push:\n    branches: [ main ]\n    paths:\n      - \"frontend/appflowy_flutter/assets/translations/en.json\"\n\njobs:\n  Discord-Notify:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: Ilshidur/action-discord@master\n        env:\n          DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}\n        with:\n          args: |\n            @appflowytranslators English UI strings has been updated.\n            Link to changes: ${{github.event.compare}}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\n# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries\n# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html\n# backend\n/target/\n\n# These are backup files generated by rustfmt\n**/*.rs.bk\n**/target/\n**/*.db\n.idea/\n**/temp/**\n.ruby-version\npackage-lock.json\nyarn.lock\nnode_modules\n**/.proto_cache\n**/.cache\n**/.DS_Store\n\n**/resources/proto\n\n# ignore settings.json\nfrontend/.vscode/settings.json\n\n# Commit the highest level pubspec.lock, but ignore the others\npubspec.lock\n!frontend/appflowy_flutter/pubspec.lock\n\n# ignore tool used for commit linting\n.githooks/gitlint\n.githooks/gitlint.exe\n.fvm/\n\n**/AppFlowy-Collab/\n\n# ignore generated assets\nfrontend/package\nfrontend/*.deb\n\n**/Cargo.toml.bak\n\n**/.cargo/**"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Release Notes\n## Version 0.9.9 – 11/09/2025\n### Desktop\n#### New Features\n- Workspace user profile: customize your avatar, about me, and profile card banner\n\n### Mobile\n#### New Features\n- Workspace user profile: customize your avatar, about me, and profile card banner\n- Two new Android widgets: Recent Pages and Favorites\n- A new iOS widget: Quick Page Access\n- iOS Share to AppFlowy: quickly save browser page links with optional notes to a target AppFlowy page\n\n## Version 0.9.8 – 28/08/2025\n### Desktop\n#### New Features\n- Bulk add collaborators with Can view / Can edit permissions via the Share menu\n- Enable setting the start of the week to Monday to control default calendar layouts in the calendar view and date picker\n- Sync date and time formats, language, and week start day across devices under the same user account\n- Offer 10 premium Select-option colors to Pro Plan users\n- Support managing pending members via Settings → Members\n\n### Mobile\n#### New Features\n- Support iOS widgets for quick access to recent and favorite pages\n- Offer 10 premium Select option colors to Pro Plan users\n- Support managing pending members via Settings → Members\n\n## Version 0.9.7 – 13/08/2025\n### Desktop\n#### New Features\n- Mention or assign persons in documents via '@' or '/'\n- Mentioned persons get notified about the mention via email\n- More colors are available for database Select options and page covers\n- Back up your AppFlowy workspace: Export your workspace as a ZIP file and import it back at any time\n- GPT-5 is now available in AppFlowy\n\n### Mobile\n#### New Features\n- Mention or assign persons in documents via '@' or '/'\n- Mentioned persons get notified about the mention via email\n- More colors are available for database Select options and page cover\n\n#### Bug Fixes\n- Fixed login page UI overflow issues on small screen devices\n\n## Version 0.9.6 – 05/08/2025\n\n### Bug Fixes\n- Fixed some syncing issues\n- Improved the Share menu\n- Fixed some UI issues related to database filters and sorts\n\n## Version 0.9.5 – 17/07/2025\n### Desktop\n#### New Features\n- Vault Workspace: A new workspace type, private and offline. AI runs locally with no data transfer. Supports switching embedding models, chatting with files (PDF, Markdown, TXT). Includes RAG search with AI-generated overviews.\n- Revamped color pickers in documents: Expanded palette with support for custom colors\n\n### Mobile\n#### New Features\n- iOS In‑App Sign‑In: Sign in directly within the iOS app\n- New colors: Improved text and background color options\n\nBug Fixes\n- Add a network connection indicator\n- Fix sync bugs and issues with WebSocket connections.\n\n## Version 0.9.4 – 02/07/2025\n### Desktop\n#### New Features\n- Private page sharing: Add members to private pages with Can View or Can Edit permissions\n- Guest editor collaboration: Invite non-members (guest editors) to collaborate in real-time on your pages\n- Shared with me: Browse all pages shared with you under the new Shared with me section\n- New syncing protocol: Optimized for faster, more reliable multi-user and multi-device data sync\n### Mobile\n#### New Features\n- Shared page collaboration: View and edit pages that have been shared with you on iOS and Android\n- New syncing protocol: Optimized for faster, more reliable multi-user and multi-device data sync\n\n## Version 0.9.3 - 28/05/2025\n### Desktop\n#### New Features\n- Meet AppFlowy Workspace AI Search: Quickly find pages by searching titles, keywords, or asking natural-language questions\n- AI Overviews: Ask natural questions and receive instant AI-generated summaries with source links, inspired by Google's AI Overviews\n- Revamped Search Panel: A cleaner, smarter interface to help you search faster and more effectively\n- Custom Prompts: Load a database page as the source for your own custom AI prompts\n#### Bug Fixes\n- Fixed misalignment in database view after setting maxDocumentWidth\n- Centered embedded link when the site name is empty\n- Fixed issue where row observer was not clearing as expected\n- Fixed issue where workspace name reverted after being updated\n- Aligned checkbox icon with the first line of text\n### Mobile\n#### New Features\n- Meet AppFlowy Workspace AI Search: Quickly find pages by searching titles, keywords, or asking natural-language questions\n- AI Overviews: Ask natural questions and receive instant AI-generated summaries with source links, inspired by Google's AI Overviews\n- Revamped Search Tab: A redesigned interface that helps you find what you need more efficiently\n#### Bug Fixes\n- Fixed issue where font size reset after restarting the app\n\n## Version 0.9.2 - 14/05/2025\n### Desktop\n#### New Features\n- Supported AI Overview in Search to answer user queries based on their entire workspace\n- Revamped the Search panel in the desktop app\n- Enabled loading custom prompts from an AppFlowy database page\n#### Bug Fixes\n- Improved inserting emojis using the colon (:)\n- Supported automatically filling the link name with the URL if the name is left empty\n### Mobile\n#### Bug Fixes\n- Supported automatically filling the link name with the URL if the name is left empty\n\n## Version 0.9.1 - 01/05/2025\n### Desktop\n#### New Features\n- Added AppFlowy Prompt Library to AI Chat and Document's Ask AI\n- Revamped the desktop in-app notification center\n- Supported login with password, as well as forgot and change password options\n- Supported copying link to invite members\n- Improved the Settings' Members tab with new metadata: member avatar and joined time\n#### Bug Fixes\n- Fixed data loss when using anonymous local\n- Fixed crash when trying to delete an emoji\n- Fixed Windows scaling issue\n- Correctly displayed mention text by decoding web content\n### Mobile\n#### New Features\n- Supported workspace search\n- Improved UX for links in documents\n- Supported changing password in Mobile Settings\n- Added support for inviting members via links\n#### Bug Fixes\n- Correctly displayed mention text by decoding web content\n\n## Version 0.9.0 - 30/04/2025\n### Desktop\n#### New Features\n- Added AppFlowy Prompt Library to AI Chat and Document's Ask AI\n- Revamped the desktop in-app notification center\n- Supported login with password, as well as forgot and change password options\n- Supported copying link to invite members\n- Improved the Settings' Members tab with new metadata: member avatar and joined time\n#### Bug Fixes\n- Fixed crash when trying to delete an emoji\n- Fixed Windows scaling issue\n- Correctly displayed mention text by decoding web content\n### Mobile\n#### New Features\n- Supported workspace search\n- Improved UX for links in documents\n- Supported changing password in Mobile Settings\n- Added support for inviting members via links\n#### Bug Fixes\n- Correctly displayed mention text by decoding web content\n\n## Version 0.8.9 - 16/04/2025\n### Desktop\n#### New Features\n- Supported pasting a link as a mention, providing a more condensed visualization of linked content\n- Supported converting between link formats (e.g. transforming a mention into a bookmark)\n- Improved the link editing experience with enhanced UX\n- Added OTP (One-Time Password) support for sign-in authentication\n- Added latest AI models: GPT-4.1, GPT-4.1-mini, and Claude 3.7 Sonnet\n#### Bug Fixes\n- Fixed an issue where properties were not displaying in the row detail page\n- Fixed a bug where Undo didn't work in the row detail page\n- Fixed an issue where blocks didn't grow when the grid got bigger\n- Fixed several bugs related to AI writers\n### Mobile\n#### New Features\n- Added sign-in with OTP (One-Time Password)\n#### Bug Fixes\n- Fixed an issue where the slash menu sometimes failed to display\n- Updated the mention page block to handle page selection with more context.\n\n## Version 0.8.8 - 01/04/2025\n### New Features\n- Added support for selecting AI models in AI writer\n- Revamped link menu in toolbar\n- Added support for using \":\" to add emojis in documents\n- Passed the history of past AI prompts and responses to AI writer\n### Bug Fixes\n- Improved AI writer scrolling user experience\n- Fixed issue where checklist items would disappear during reordering\n- Fixed numbered lists generated by AI to maintain the same index as the input\n\n## Version 0.8.7 - 18/03/2025\n### New Features\n- Made local AI free and integrated with Ollama\n- Supported nested lists within callout and quote blocks\n- Revamped the document's floating toolbar and added Turn Into\n- Enabled custom icons in callout blocks\n### Bug Fixes\n- Fixed occasional incorrect positioning of the slash menu\n- Improved AI Chat and AI Writers with various bug fixes\n- Adjusted the columns block to match the width of the editor\n- Fixed a potential segfault caused by infinite recursion in the trash view\n- Resolved an issue where the first added cover might be invisible\n- Fixed adding cover images via Unsplash\n\n## Version 0.8.6 - 06/03/2025\n### Bug Fixes\n- Fix the incorrect title positioning when adjusting the document width setting\n- Enhance the user experience of the icon color picker for smoother interactions\n- Add missing icons to the database to ensure completeness and consistency\n- Resolve the issue with links not functioning correctly on Linux systems\n- Improve the outline feature to work seamlessly within columns\n- Center the bulleted list icon within columns for better visual alignment\n- Enable dragging blocks under tables in the second column to enhance flexibility\n- Disable the AI writer feature within tables to prevent conflicts and improve usability\n- Automatically enable the header row when converting content from Markdown to ensure proper formatting\n- Use the \"Undo\" function to revert the auto-formatting\n\n## Version 0.8.5 - 04/03/2025\n### New Features\n- Columns in Documents: Arrange content side by side using drag-and-drop or the slash menu\n- 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\n- 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\n### Bug Fixes\n- Fixed an issue where callout blocks couldn’t be deleted when appearing as the first line in a document\n- Fixed a bug preventing the relation field in databases from opening\n- Fixed an issue where links in documents were unclickable on Linux\n\n## Version 0.8.4 - 18/02/2025\n### New Features\n- Switch AI mode on mobile\n- Support locking page\n- Support uploading svg file as icon\n- Support the slash, at, and plus menus on mobile\n### Bug Fixes\n- Gallery not rendering in row page\n- Save image should not copy the image (mobile)\n- Support exporting more content to markdown\n\n## Version 0.8.2 - 23/01/2025\n### New Features\n- Customized database view icons\n- Support for uploading images as custom icons\n- Enabled selecting multiple AI messages to save into a document\n- Added the ability to scale the app's display size on mobile\n- Support for pasting image links without file extensions\n### Bug Fixes\n- Fixed an issue where pasting tables from other apps wasn't working\n- Fixed homepage URL issues in Settings\n- Fixed an issue where the 'Cancel' button was not visible on the Shortcuts page\n\n## Version 0.8.1 - 14/01/2025\n### New Features\n- AI Chat Layout Options: Customize how AI responses appear with new layouts—List, Table, Image with Text, and Media Only\n- DALL-E Integration: Generate stunning AI images from text prompts, now available in AI Chat\n- Improved Desktop Search: Find what you need faster using keywords or by asking questions in natural language\n- Self-Hosting: Configure web server URLs directly in Settings to enable features like Publish, Copy Link to Share, Custom URLs, and more\n- Sidebar Enhancement: Drag to reorder your favorited pages in the Sidebar\n- Mobile Table Resizing: Adjust column widths in Simple Tables by long pressing the column borders on mobile\n### Bug Fixes\n- Resolved an icon rendering issue in callout blocks, tab bars, and search results\n- Enhanced image reliability: Retry functionality ensures images load successfully if the first attempt fails\n\n## Version 0.8.0 - 06/01/2025\n### Bug Fixes\n- Fixed error displaying in the page style menu\n- Fixed filter logic in the icon picker\n- Fixed error displaying in the Favorite/Recent page\n- Fixed the color picker displaying when tapping down\n- Fixed icons not being supported in subpage blocks\n- Fixed recent icon functionality in the space icon menu\n- Fixed \"Insert Below\" not auto-scrolling the table\n- Fixed a to-do item with an emoji automatically creating a soft break\n- Fixed header row/column tap areas being too small\n- Fixed simple table alignment not working for items that wrap\n- Fixed web content reverting after removing the inline code format on desktop\n- Fixed inability to make changes to a row or column in the table when opening a new tab\n- Fixed changing the language to CKB-KU causing a gray screen on mobile\n\n## Version 0.7.9 - 30/12/2024\n### New Features\n- Meet AppFlowy Web (Lite): Use AppFlowy directly in your browser.\n  - Create beautiful documents with 22 content types and markdown support\n  - Use Quick Note to save anything you want to remember—like meeting notes, a grocery list, or to-dos\n  - Invite members to your workspace for seamless collaboration\n  - Create multiple public/private spaces to better organize your content\n- Simple Table is now available on Mobile, designed specifically for mobile devices.\n  - Create and manage Simple Table blocks on Mobile with easy-to-use action menus.\n  - Use the '+' button in the fixed toolbar to easily add a content block into a table cell on Mobile\n  - Use '/' to insert a content block into a table cell on Desktop\n- Add pages as AI sources in AI chat, enabling you to ask questions about the selected sources\n- Add messages to an editable document while chatting with AI side by side\n- The new Emoji menu now includes Icons with a Recent section for quickly reusing emojis/icons\n- Drag a page from the sidebar into a document to easily mention the page without typing its title\n- Paste as plain text, a new option in the right-click paste menu\n### Bug Fixes\n- Fixed misalignment in numbered lists\n- Resolved several bugs in the emoji menu\n- Fixed a bug with checklist items\n\n## Version 0.7.8 - 18/12/2024\n### New Features\n<img width=\"1068\" alt=\"image\" src=\"https://github.com/user-attachments/assets/cf8bd287-f370-4291-8638-76e2bbf4aaac\" />\n\n- Meet Simple Table 2.0:\n   - Insert a list into a table cell\n   - Insert images, quotes, callouts, and code blocks into a table cell\n   - Drag to move rows or columns\n   - Toggle header rows or columns on/off\n   - Distribute columns evenly\n   - Adjust to page width\n- Enjoy a new UI/UX for a seamless experience\n- Revamped mention page interactions in AI Chat\n- Improved AppFlowy AI service\n\n### Bug Fixes\n- Fixed an error when opening files in the database in local mode\n- Fixed arrow up/down navigation not working for selecting a language in Code Block\n- Fixed an issue where deleting multiple blocks using the drag button on the document page didn’t work\n\n## Version 0.7.7 - 09/12/2024\n### Bug Fixes\n- Fixed sidebar menu resize regression\n- Fixed AI chat loading issues\n- Fixed inability to open local files in database\n- Fixed mentions remaining in notifications after removal from document\n- Fixed event card closing when clicking on empty space\n- Fixed keyboard shortcut issues\n\n## Version 0.7.6 - 03/12/2024\n### New Features\n- Revamped the simple table UI\n- Added support for capturing images from camera on mobile\n### Bug Fixes\n- Improved markdown rendering capabilities in AI writer\n- Fixed an issue where pressing Enter on a collapsed toggle list would add an unnecessary new line\n- Fixed an issue where creating a document from slash menu could insert content at incorrect position\n\n## Version 0.7.5 - 25/11/2024\n### Bug Fixes\n- Improved chat response parsing\n- Fixed toggle list icon direction for RTL mode\n- Fixed cross blocks formatting not reflecting in float toolbar\n- Fixed unable to click inside the toggle list to create a new paragraph\n- Fixed open file error 50 on macOS\n- Fixed upload file exceed limit error\n\n## Version 0.7.4 - 19/11/2024\n### New Features\n- Support uploading WebP and BMP images\n- Support managing workspaces on mobile\n- Support adding toggle headings on mobile\n- Improve the AI chat page UI\n### Bug Fixes\n- Optimized the workspace menu loading performance\n- Optimized tab switching performance\n- Fixed searching issues in Document page\n\n## Version 0.7.3 - 07/11/2024\n### New Features\n- Enable custom URLs for published pages\n- Support toggling headings\n- Create a subpage by typing in the document\n- Turn selected blocks into a subpage\n- Add a manual date picker for the Date property\n\n### Bug Fixes\n- Fixed an issue where the workspace owner was unable to delete spaces created by others\n- Fixed cursor height inconsistencies with text height\n- Fixed editing issues in Kanban cards\n- Fixed an issue preventing images or files from being dropped into empty paragraphs\n\n## Version 0.7.2 - 22/10/2024\n### New Features\n- Copy link to block\n- Support turn into in document\n- Enable sharing links and publishing pages on mobile\n- Enable drag and drop in row documents\n- Right-click on page in sidebar to open more actions\n- Create new subpage in document using `+` character\n- Allow reordering checklist item\n\n### Bug Fixes\n- Fixed issue with inability to cancel inline code format in French IME\n- Fixed delete with Shift or Ctrl shortcuts not working in documents\n- Fixed the issues with incorrect time zone being used in filters.\n\n## Version 0.7.1 - 07/10/2024\n### New Features\n- Copy link to share and open it in a browser\n- Enable the ability to edit the page title within the body of the document\n- Filter by last modified, created at, or a date range\n- Allow customization of database property icons\n- Support CTRL/CMD+X to delete the current line when the selection is collapsed in the document\n- Support window tiling on macOS\n- Add filters to grid views on mobile\n- Create and manage workspaces on mobile\n- Automatically convert property types for imported CSV files\n\n### Bug Fixes\n- Fixed calculations with filters applied\n- Fixed issues with importing data folders into a cloud account\n- Fixed French IME backtick issues\n- Fixed selection gesture bugs on mobile\n\n## Version 0.7.0 - 19/09/2024\n### New Features\n- Support reordering blocks in document with drag and drop\n- Support for adding a cover to a row/card in databases\n- Added support for accessing settings on the sign-in page\n- Added \"Move to\" option to the document menu in top right corner\n- Support for adjusting the document width from settings\n- Show full name of a group on hover\n- Colored group names in kanban boards\n- Support \"Ask AI\" on multiple lines of text\n- Support for keyboard gestures to move cursor on Mobile\n- Added markdown support for quickly inserting a code block using three backticks\n\n### Bug Fixes\n- Fixed a critical bug where the backtick character would crash the application\n- Fixed an issue with signing-in from the settings dialog where the dialog would persist\n- Fixed a visual bug with icon alignment in primary cell of database rows\n- Fixed a bug with filters applied where new rows were inserted in wrong position\n- Fixed a bug where \"Untitled\" would override the name of the row\n- Fixed page title not updating after renaming from \"More\"-menu\n- Fixed File block breaking row detail document\n- Fixed issues with reordering rows with sorting rules applied\n- Improvements to the File & Media type in Database\n- Performance improvement in Grid view\n- Fixed filters sometimes not applying properly in databases\n\n## Version 0.6.9 - 09/09/2024\n### New Features\n- Added a new property type, 'Files & media'\n- Supported Apple Sign-in\n- Displayed the page icon next to the row name when the row page contains nested notes\n- Enabled Delete Account in Settings\n- Included a collapsible navigation menu in your published site\n\n### Bug Fixes\n- Fixed the space name color issue in the community themes\n- Fixed database filters and sorting issues\n- Fixed the issue of not being able to fully display the title on Kanban cards\n- Fixed the inability to see the entire text of a checklist item when it's more than one line long\n- Fixed hide/unhide buttons in the No Status group\n- Fixed the inability to edit group names on Kanban boards\n- Made error codes more user-friendly\n- Added leading zeros to day and month in date format\n\n## Version 0.6.8 - 22/08/2024\n### New Features\n- Enabled viewing data inside a database record on mobile.\n- Added the ability to invite members to a workspace on mobile.\n- Introduced Ask AI in the Home tab on mobile.\n- Import CSV files with up to 1,000 rows.\n- Convert properties from one type to another while preserving the data.\n- Optimized the speed of opening documents and databases.\n- Improved syncing performance across devices.\n- Added support for a monochrome app icon on Android.\n\n### Bug Fixes\n- Removed the Wayland header from the AppImage build.\n- Fixed the issue where pasting a web image on mobile failed.\n- Corrected the Local AI state when switching between different workspaces.\n- Fixed high CPU usage when opening large databases.\n\n## Version 0.6.7 - 13/08/2024\n### New Features\n- Redesigned the icon picker design on Desktop.\n- Redesigned the notification page on Mobile.\n\n### Bug Fixes\n- Enhance the toolbar tooltip functionality on Desktop.\n- Enhance the slash menu user experience on Desktop.\n- Fixed the issue where list style overrides occurred during text pasting.\n- Fixed the issue where linking multiple databases in the same document could cause random loss of focus.\n\n## Version 0.6.6 - 30/07/2024\n### New Features\n- Upgrade your workspace to a premium plan to unlock more features and storage.\n- Image galleries and drag-and-drop image support in documents.\n\n### Bug Fixes\n- Fix minor UI issues on Desktop and Mobile.\n\n## Version 0.6.5 - 24/07/2024\n### New Features\n- Publish a Database to the Web\n\n## Version 0.6.4 - 16/07/2024\n### New Features\n- Enhanced the message style on the AI chat page.\n- Added the ability to choose cursor color and selection color from a palette in settings page.\n### Bug Fixes\n- Optimized the performance for loading recent pages.\n- Fixed an issue where the cursor would jump randomly when typing in the document title on mobile.\n\n## Version 0.6.3 - 08/07/2024\n### New Features\n- Publish a Document to the Web\n\n## Version 0.6.2 - 01/07/2024\n### New Features\n- Added support for duplicating spaces.\n- Added support for moving pages across spaces.\n- Undo markdown formatting with `Ctrl + Z` or `Cmd + Z`.\n- Improved shortcuts settings UI.\n### Bug Fixes\n- Fixed unable to zoom in with `Ctrl` and `+` or `Cmd` and `+` on some keyboards.\n- Fixed unable to paste nested lists in existing lists.\n\n## Version 0.6.1 - 22/06/2024\n### New Features\n- Introduced the \"Space\" feature to help you organize your pages more efficiently.\n### Bug Fixes\n- Resolved shortcut conflicts on the board page.\n- Resolved an issue where underscores could cause the editor to freeze.\n\n## Version 0.6.0 - 19/06/2024\n### New Features\n- Introduced the \"Space\" feature to help you organize your pages more efficiently.\n### Bug Fixes\n- Resolved shortcut conflicts on the board page.\n- Resolved an issue where underscores could cause the editor to freeze.\n\n## Version 0.5.9 - 06/06/2024\n### New Features\n- Revamped the sidebar for both Desktop and Mobile.\n- Added support for embedding videos in documents.\n- Introduced a hotkey (Cmd/Ctrl + 0) to reset the app scale.\n- Supported searching the workspace by page title.\n### Bug Fixes\n- Fixed the issue preventing the use of Backspace to delete words in Kanban boards.\n\n## Version 0.5.8 - 05/20/2024\n### New Features\n- Improvement to the Callout block to insert new lines\n- New settings page \"Manage data\" replaced the \"Files\" page\n- New settings page \"Workspace\" replaced the \"Appearance\" and \"Language\" pages\n- A custom implementation of a title bar for Windows users\n- Added support for selecting Cards in kanban and performing grouped keyboard shortcuts\n- Added support for default system font family\n- Support for scaling the application up/down using a keyboard shortcut (CMD/CTRL + PLUS/MINUS)\n\n### Bug Fixes\n- Resolved and refined the UI on Mobile\n- Resolved issue with text editing in database\n- Improved appearance of empty text cells in kanban/calendar\n- Resolved an issue where a page's more actions (delete, duplicate) did not work properly\n- Resolved and inconsistency in padding on get started screen on Desktop\n\n## Version 0.5.7 - 05/10/2024\n### Bug Fixes\n- Resolved page opening issue on Android.\n- Fixed text input inconsistency on Kanban board cards.\n\n## Version 0.5.6 - 05/07/2024\n### New Features\n- Team collaboration is live! Add members to your workspace to edit and collaborate on pages together.\n- Collaborate in real time on the same page with other members. Edits made by others will appear instantly.\n- Create multiple workspaces for different kinds of content.\n- Customize your entire page on mobile through the Page Style menu with options for layout, font, font size, emoji, and cover image.\n- Open a row record as a full page.\n### Bug Fixes\n- Resolved issue with setting background color for the Simple Table block.\n- Adjusted toolbar for various screen sizes.\n- Added a request for photo permission before uploading images on mobile.\n- Exported creation and last modification timestamps to CSV.\n\n## Version 0.5.5 - 04/24/2024\n### New Features\n- Improved the display of code blocks with line numbers\n- Added support for signing in using Magic Link\n### Bug Fixes\n- Fixed the database synchronization indicator issue\n- Resolved the issue with opening the mentioned page on mobile\n- Cleared the collaboration status when the user exits AppFlowy\n\n## Version 0.5.4 - 04/08/2024\n### New Features\n- Introduced support for displaying a synchronization indicator within documents and databases to enhance user awareness of data sync status\n- Revamped the select option cell editor in database\n- Improved translations for Spanish, German, Kurdish, and Vietnamese\n- Supported Android 6 and newer versions\n### Bug Fixes\n- Resolved an issue where twelve-hour time formats were not being parsed correctly in databases\n- Fixed a bug affecting the user interface of the single select option filter\n- Fixed various minor UI issues\n\n## Version 0.5.3 - 03/21/2024\n### New Features\n- Added build support for 32-bit Android devices\n- Introduced filters for KanBan boards for enhanced organization\n- Introduced the new \"Relations\" column type in Grids\n- Expanded language support with the addition of Greek\n- Enhanced toolbar design for Mobile devices\n- Introduced a command palette feature with initial support for page search\n### Bug Fixes\n- Rectified the issue of incomplete row data in Grids when adding new rows with active filters\n- Enhanced the logic governing the filtering of number and select/multi-select fields for improved accuracy\n- Implemented UI refinements on both Desktop and Mobile platforms, enriching the overall user experience of AppFlowy\n\n## Version 0.5.2 - 03/13/2024\n### Bug Fixes\n- Import csv file.\n\n## Version 0.5.1 - 03/11/2024\n### New Features\n- Introduced support for performing generic calculations on databases.\n- Implemented functionality for easily duplicating calendar events.\n- Added the ability to duplicate fields with cell data, facilitating smoother data management.\n- Now supports customizing font styles and colors prior to typing.\n- Enhanced the checklist user experience with the integration of keyboard shortcuts.\n- Improved the dark mode experience on mobile devices.\n### Bug Fixes\n- Fixed an issue with some pages failing to sync properly.\n- Fixed an issue where links without the http(s) scheme could not be opened, ensuring consistent link functionality.\n- Fixed an issue that prevented numbers from being inserted before heading blocks.\n- Fixed the inline page reference update mechanism to accurately reflect workspace changes.\n- Fixed an issue that made it difficult to resize images in certain cases.\n- Enhanced image loading reliability by clearing the image cache when images fail to load.\n- Resolved a problem preventing the launching of URLs on some Linux distributions.\n\n## Version 0.5.0 - 02/26/2024\n### New Features\n- Added support for scaling text on mobile platforms for better readability.\n- Introduced a toggle for favorites directly from the documents' top bar.\n- Optimized the image upload process and added error messaging for failed uploads.\n- Implemented depth control for outline block components.\n- New checklist task creation is now more intuitive, with prompts appearing on hover over list items in the row detail page.\n- Enhanced sorting capabilities, allowing reordering and addition of multiple sorts.\n- Expanded sorting and filtering options to include more field types like checklist, creation time, and modification time.\n- Added support for field calculations within databases.\n### Bug Fixes\n- Fixed an issue where inserting an image from Unsplash in local mode was not possible.\n- Fixed undo/redo functionality in lists.\n- Fixed data loss issues when converting between block types.\n- Fixed a bug where newly created rows were not being automatically sorted.\n- Fixed issues related to deleting a sorting field or sort not removing existing sorts properly.\n### Notes\n- Windows 7, Windows 8, and iOS 11 are not yet supported due to the upgrade to Flutter 3.22.0.\n\n## Version 0.4.9 - 02/17/2024\n### Bug Fixes\n- Resolved the issue that caused users to be redirected to the Sign In page\n\n## Version 0.4.8 - 02/13/2024\n### Bug Fixes\n- Fixed a possible error when loading workspaces\n\n## Version 0.4.6 - 02/03/2024\n### Bug Fixes\n- Fixed refresh token bug\n\n## Version 0.4.5 - 02/01/2024\n### Bug Fixes\n- Fixed WebSocket connection issue\n\n## Version 0.4.4 - 01/31/2024\n### New Features\n- Added functionality for uploading images to cloud storage.\n- Enabled anonymous sign-in option for mobile platform users.\n- Introduced the ability to customize cloud settings directly from the startup page.\n- Added support for inserting reminders on the mobile platform.\n- Overhauled the user interface on mobile devices, including improvements to the action bottom sheet, editor toolbar, database details page, and app bar.\n- Implemented a shortcut (F2 key) to rename the current view.\n\n### Bug Fixes\n- Fixed an issue where the font family was not displaying correctly on the mobile platform.\n- Resolved a problem with the mobile row detail title not updating correctly.\n- Fixed issues related to deleting images and refactored the image actions menu for better usability.\n- Fixed other known issues.\n\n# Release Notes\n## Version 0.4.3 - 01/16/2024\n### Bug Fixes\n- Fixed file name too long issue\n\n## Version 0.4.2 - 01/15/2024\nAppFlowy for Android is available to download on GitHub.\nIf 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.\n### New Features\n- Enhanced RTL (Right-to-Left) support for mobile platforms.\n- Optimized selection gesture system on mobile.\n- Optimized the mobile toolbar menu.\n- Improved reference menu (‘@’ menu).\n- Updated privacy policy.\n- Improved the data import process for AppFlowy by implementing a progress indicator and compressing the data to enhance efficiency.\n- Enhanced the utilization of local disk space to optimize storage consumption.\n### Bug Fixes\n- Fixed sign-in cancellation issue on mobile.\n- Resolved keyboard close bug on Android.\n\n\n## Version 0.4.1 - 01/03/2024\n### Bug fixes\n- Fix import AppFlowy data folder\n\n## Version 0.4.0 - 12/30/2023\n1. 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).\n2. Enhanced user interface and fixed various bugs.\n3. Improved the efficiency of data synchronization in AppFlowy Cloud\n\n## Version 0.3.9.1 - 12/07/2023\n\n### Bug fixes\n- Fix potential blank pages that may occur in an empty document\n\n## Version 0.3.9 - 12/07/2023\n\n### New Features\n- Support inserting a new field to the left or right of an existing one\n\n### Bug fixes\n- Fix some emojis are shown in black/white\n- Fix unable to rename a subpage of subpage\n\n## Version 0.3.8 - 11/13/2023\n\n### New Features\n- Support hiding any stack in a board\n- Support customizing page icons in menu\n- Display visual hint when card contains notes\n- Quick action for adding new stack to a board\n- Support more ways of inserting page references in documents\n- Shift + click on a checkbox to power toggle its children\n\n### Bug fixes\n- Improved color of the \"Share\"-button text\n- Text overflow issue in Calendar properties\n- Default font (Roboto) added to application\n- Placeholder added for the editor inside a Card\n- Toggle notifications in settings have been fixed\n- Dialog for linking board/grid/calendar opens in correct position\n- Quick add Card in Board at top, correctly adds a new Card at the top\n\n## Version 0.3.7 - 10/30/2023\n\n### New Features\n- Support showing checklist items inline in row page.\n- Support inserting date from slash menu.\n- Support renaming a stack directly by clicking on the stack name.\n- Show the detailed reminder content in the notification center.\n- Save card order in Board view.\n- Allow to hide the ungrouped stack.\n- Segmented the checklist progress bar.\n\n### Bug fixes\n- Optimize side panel animation.\n- Fix calendar with hidden date or title doesn't show options correctly.\n- Fix the horizontal scroll bar disappears in Grid view.\n- Improve setting tab UI in Grid view.\n- Improve theme of the code block.\n- Fix some UI issues.\n\n## Version 0.3.6 - 10/16/2023\n\n### New Features\n- Support setting Markdown styles through keyboard shortcuts.\n- Added Ukrainian language.\n- Support auto-hiding sidebar feature, ensuring a streamlined view even when resizing to a smaller window.\n- Support toggling the notifitcation on/off.\n- Added Lemonade theme.\n\n### Bug fixes\n- Improve Vietnamese translations.\n- Improve reminder feature.\n- Fix some UI issues.\n\n## Version 0.3.5 - 10/09/2023\n\n### New Features\n- Added support for browsing and inserting images from Unsplash.\n- Revamp and unify the emoji picker throughout AppFlowy.\n\n### Bug fixes\n- Improve layout of the settings page.\n- Improve design of the restore page banner.\n- Improve UX of the reminders.\n- Other UI fixes.\n\n## Version 0.3.4 - 10/02/2023\n\n### New Features\n- Added support for creating a reminder.\n- Added support for finding and replacing in the document page.\n- Added support for showing the hidden fields in row detail page.\n- Adjust the toolbar style in RTL mode.\n\n### Bug fixes\n- Improve snackbar UI design.\n- Improve dandelion theme.\n- Improve id-ID and pl-PL language translations.\n\n## Version 0.3.3 - 09/24/2023\n\n### New Features\n- Added an end date field to the time cell in the database.\n- Added Support for customizing the font family from GoogleFonts in the editor.\n- Set the uploaded image to cover by default.\n- Added Support for resetting the user icon on settings page\n- Add Urdu language translations.\n\n### Bug fixes\n- Default colors for the blocks except for the callout were not transparent.\n- Option/Alt + click to add a block above didn't work on the first line.\n- Unable to paste HTML content containing `<mark>` tag.\n- Unable to select the text from anywhere in the line.\n- The selection in the editor didn't clear when editing the inline database.\n- Added a bottom border to new property column in the database.\n- Set minimum width of 50px for grid fields.\n\n## Version 0.3.2 - 09/18/2023\n\n### New Features\n\n- Improve the performance of the editor, now it is much faster when editing a large document.\n- Support for reordering the rows of the database on Windows.\n- Revamp the row detail page of the database.\n- Revamp the checklist cell editor of the database.\n\n### Bug fixes\n\n- Some UI issues\n\n## Version 0.3.1 - 09/04/2023\n\n### New Features\n\n- Improve CJK (Chinese, Japanese, Korean) input method support.\n- Share a database in CSV format.\n- Support for aligning the block component with the toolbar.\n- Support for editing name when creating a new page.\n- Support for inserting a table in the document page.\n- Database views allow for independent field visibility toggling.\n\n### Bug fixes\n\n- Paste multiple lines in code block.\n- Some UI issues\n\n## Version 0.3.0 - 08/22/2023\n\n### New Features\n\n- Improve paste features:\n  - Paste HTML content from website.\n  - Paste image from clipboard.\n\n- Support Group by Date in Kanban Board.\n- Notarize the macOS package, which is now verified by Apple.\n- Add Persian language translations.\n\n### Bug fixes\n\n- Some UI issues\n\n## Version 0.2.9 - 08/08/2023\n\n### New Features\n\n- Improve tab and shortcut, click with alt/option to open a page in new tab.\n- Improve database tab bar UI.\n\n### Bug fixes\n\n- Add button and more action button of the favorite section doesn't work.\n- Fix euro currency number format.\n- Some UI issues\n\n## Version 0.2.8 - 08/03/2023\n\n### New Features\n\n- Nestable personal folder that supports drag and drop\n- Support for favorite folders.\n- Support for sorting by date in Grid view.\n- Add a duplicate button in the Board context menu.\n\n### Bug fixes\n\n- Improve readability in Callout\n- Some UI issues\n\n## Version 0.2.7 - 07/18/2023\n\n### New Features\n\n<img width=\"1147\" src=\"https://github.com/AppFlowy-IO/AppFlowy/assets/11863087/ac464740-c685-4a85-ae99-1074c1c607e5\">\n\n- Open page in new tab\n- Create toggle lists to keep things tidy in your pages\n- Alt/Option + click to add a text block above\n\n### Bug fixes\n\n- Pasting into a Grid property crashed on Windows\n- Double-click a link to open\n\n## Version 0.2.6 - 07/11/2023\n\n### New Features\n\n- Dynamic load themes\n- Inline math equation\n\n\n## Version 0.2.5 - 07/02/2023\n\n### New Features\n\n- Insert local images\n- Mention a page\n- Outlines (Table of contents)\n- Added support for aligning the image by image menu\n\n### Bug fixes\n\n- Some UI issues\n\n## Version 0.2.4 - 06/23/2023\n\n### Bug fixes:\n\n- Unable to copy and paste a word\n- Some UI issues\n\n## Version 0.2.3 - 06/21/2023\n\n### New Features\n\n- Added support for creating multiple database views for existing database\n\n## Version 0.2.2 - 06/15/2023\n\n### New Features\n\n- Added support for embedding a document in the database's row detail page\n- Added support for inserting an emoji in the database's row detail page\n\n### Other Updates\n\n- Added language selector on the welcome page\n- Added support for importing multiple markdown files all at once\n\n## Version 0.2.1 - 06/11/2023\n\n### New Features\n\n- Added support for creating or referencing a calendar in the document\n- Added `+` icon in grid's add field\n\n### Other Updates\n\n- Added vertical padding for progress bar\n- Hide url cell accessory when the content is empty\n\n### Bug fixes:\n\n- Fixed unable to export markdown\n- Fixed adding vertical padding for progress bar\n- Fixed database view didn't update after the database layout changed.\n\n## Version 0.2.0 - 06/08/2023\n\n### New Features\n\n- Improved checklists to support each cell having its own list\n- Drag and drop calendar events\n- Switch layouts (calendar, grid, kanban) of a database\n- New database properties: 'Updated At' and 'Created At'\n- Enabled hiding properties on the row detail page\n- Added support for reordering and saving row order in different database views.\n- Enabled each database view to have its own settings, including filter and sort options\n- Added support to convert `“` (double quote) into a block quote\n- Added support to convert `***` (three stars) into a divider\n- Added support for an 'Add' button to insert a paragraph in a document and display the slash menu\n- Added support for an 'Option' button to delete, duplicate, and customize block actions\n\n### Other Updates\n\n- Added support for importing v0.1.x documents and databases\n- Added support for database import and export to CSV\n- Optimized scroll behavior in documents.\n- Redesigned the launch page\n\n### Bug fixes\n\n- Fixed bugs related to numbers\n- Fixed issues with referenced databases in documents\n- Fixed menu overflow issues in documents\n\n### Data migration\n\nThe 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.\n\n#### Export files in v0.1.6\n\nhttps://github.com/AppFlowy-IO/AppFlowy/assets/11863087/0c89bf2b-cd97-4a7b-b627-59df8d2967d9\n\n#### Import files in v0.2.0\n\nhttps://github.com/AppFlowy-IO/AppFlowy/assets/11863087/7b392f35-4972-497a-8a7f-f38efced32e2\n\n## Version 0.1.5 - 11/05/2023\n\n### Bug Fixes\n\n- Fix: calendar dates don't match with weekdays.\n- Fix: sort numbers in Grid.\n\n## Version 0.1.4 - 04/05/2023\n\n### New features\n\n- Use AppFlowy’s calendar views to plan and manage tasks and deadlines.\n- Writing can be improved with the help of OpenAI.\n\n## Version 0.1.3 - 24/04/2023\n\n### New features\n\n- Launch the official Dark Mode.\n- Customize the font color and highlight color by setting a hex color value and an opacity level.\n\n### Bug Fixes\n\n- Fix: the slash menu can be triggered by all other keyboards than English.\n- Fix: convert the single asterisk to italic text and the double asterisks to bold text.\n\n## Version 0.1.2 - 03/28/2023\n\n### Bug Fixes\n\n- Fix: update calendar selected range.\n- Fix: duplicate view.\n\n## Version 0.1.1 - 03/21/2023\n\n### New features\n\n- AppFlowy brings the power of OpenAI into your AppFlowy pages. Ask AI to write anything for you in AppFlowy.\n- Support adding a cover image to your page, making your pages beautiful.\n- More shortcuts become available. Click on '?' at the bottom right to access our shortcut guide.\n\n### Bug Fixes\n\n- Fix some bugs\n\n## Version 0.1.0 - 02/09/2023\n\n### New features\n\n- Support linking a Board or Grid into the Document page\n- Integrate a callout plugin implemented by community members\n- Optimize user interface\n\n### Bug Fixes\n\n- Fix some bugs\n\n## Version 0.0.9.1 - 01/03/2023\n\n### New features\n\n- New theme\n- Support changing text color of editor\n- Optimize user interface\n\n### Bug Fixes\n\n- Fix some grid bugs\n\n## Version 0.0.9 - 12/21/2022\n\n### New features\n\n- Enable the user to define where to store their data\n- Support inserting Emojis through the slash command\n\n### Bug Fixes\n\n- Fix some bugs\n\n## Version 0.0.8.1 - 12/09/2022\n\n### New features\n\n- Support following your default system theme\n- Improve the filter in Grid\n\n### Bug Fixes\n\n- Copy/Paste\n\n## Version 0.0.8 - 11/30/2022\n\n### New features\n\n- Table-view database\n  - support column type: Checklist\n- Board-view database\n  - support column type: Checklist\n- Customize font size: small, medium, large\n\n## Version 0.0.7.1 - 11/30/2022\n\n### Bug Fixes\n\n- Fix some bugs\n\n## Version 0.0.7 - 11/27/2022\n\n### New features\n\n- Support adding filters by the text/checkbox/single-select property in Grid\n\n## Version 0.0.6.2 - 10/30/2022\n\n- Fix some bugs\n\n## Version 0.0.6.1 - 10/26/2022\n\n### New features\n\n- Optimize appflowy_editor dark mode style\n\n### Bug Fixes\n\n- Unable to copy the text with checkbox or link style\n\n## Version 0.0.6 - 10/23/2022\n\n### New features\n\n- Integrate **appflowy_editor**\n\n## Version 0.0.5.3 - 09/26/2022\n\n### New features\n\n- Open the next page automatically after deleting the current page\n- Refresh the Kanban board after altering a property type\n\n### Bug Fixes\n\n- Fix switch board bug\n- Fix delete the Kanban board's row error\n- Remove duplicate time format\n- Fix can't delete field in property edit panel\n- Adjust some display UI issues\n\n## Version 0.0.5.2 - 09/16/2022\n\n### New features\n\n- Enable adding a new card to the \"No Status\" group\n- Fix some bugs\n\n### Bug Fixes\n\n- Fix cannot open AppFlowy error\n- Fix delete the Kanban board's row error\n\n## Version 0.0.5.1 - 09/14/2022\n\n### New features\n\n- Enable deleting a field in board\n- Fix some bugs\n\n## Version 0.0.5 - 09/08/2022\n\n### New features - Kanban Board like Notion and Trello beta\n\nBoards are the best way to manage projects & tasks. Use them to group your databases by select, multiselect, and checkbox.\n\n<p align=\"left\"><img src=\"https://user-images.githubusercontent.com/12026239/190055984-6efa2d7a-ee38-4551-859e-ee56388e1859.gif\" width=\"1000px\" /></p>\n\n- Set up columns that represent a specific phase of the project cycle and use cards to represent each project/task\n- Drag and drop a card from one phase/column to another phase/column\n- Update database properties in the Board view by clicking on a property and making edits on the card\n\n### Other Features & Improvements\n\n- Settings allow users to change avatars\n- Click and drag the right edge to resize your sidebar\n- And many user interface improvements (link)\n\n## Version 0.0.5 - beta.2 - beta.1 - 09/01/2022\n\n### New features\n\n- Board-view database\n  - Support start editing after creating a new card\n  - Support editing the card directly by clicking the edit button\n  - Add the `No Status` column to display the cards while their status is empty\n\n### Bug Fixes\n\n- Optimize insert card animation\n- Fix some UI bugs\n\n## Version 0.0.5 - beta.1 - 08/25/2022\n\n### New features\n\n- Board-view database\n  - Group by single select\n  - drag and drop cards\n  - insert / delete cards\n\n![Aug-25-2022 16-22-38](https://user-images.githubusercontent.com/86001920/186614248-23186dfe-410e-427a-8cc6-865b1f79e074.gif)\n\n## Version 0.0.4 - 06/06/2022\n\n- Drag to adjust the width of a column\n- Upgrade to Flutter 3.0\n- Native support for M1 chip\n- Date supports time formats\n- New property: URL\n- Keyboard shortcuts support for Grid: press Enter to leave the edit mode; control c/v to copy-paste cell values\n\n### Bug Fixes\n\n- Fixed some bugs\n\n## Version 0.0.4 - beta.3 - 05/02/2022\n\n- Drag to reorder app/ view/ field\n- Row record opens as a page\n- Auto resize the height of the row in the grid\n- Support more number formats\n- Search column options, supporting Single-select, Multi-select, and number format\n\n![May-03-2022 10-03-00](https://user-images.githubusercontent.com/86001920/166394640-a8f1f3bc-5f20-4033-93e9-16bc308d7005.gif)\n\n### Bug Fixes & Improvements\n\n- Improved row/cell data cache\n- Fixed some bugs\n\n## Version 0.0.4 - beta.2 - 04/11/2022\n\n- Support properties: Text, Number, Date, Checkbox, Select, Multi-select\n- Insert / delete rows\n- Add / delete / hide columns\n- Edit property\n  ![](https://user-images.githubusercontent.com/12026239/162753644-bf2f4e7a-2367-4d48-87e6-35e244e83a5b.png)\n\n## Version 0.0.4 - beta.1 - 04/08/2022\n\nv0.0.4 - beta.1 is pre-release\n\n### New features\n\n- Table-view database\n  - support column types: Text, Checkbox, Single-select, Multi-select, Numbers\n  - hide / delete columns\n  - insert rows\n\n## Version 0.0.3 - 02/23/2022\n\nv0.0.3 is production ready, available on Linux, macOS, and Windows\n\n### New features\n\n- Dark Mode\n- Support new languages: French, Italian, Russian, Simplified Chinese, Spanish\n- Add Settings: Toggle on Dark Mode; Select a language\n- Show device info\n- Add tooltip on the toolbar icons\n\nBug fixes and improvements\n\n- Increased height of action\n- CPU performance issue\n- Fix potential data parser error\n- More foundation work for online collaboration"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to make participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n  advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n  address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies within all project spaces, and it also applies when\nan individual is representing the project or its community in public spaces.\nExamples of representing a project or community include using an official\nproject e-mail address, posting via an official social media account, or acting\nas an appointed representative at an online or offline event. Representation of\na project may be further defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at annie@appflowy.io. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published\n    by the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\" style=\"border-bottom: none\">\n    <b>\n        <a href=\"https://www.appflowy.com\">AppFlowy</a><br>\n    </b>\n    ⭐️  The Open Source Alternative To Notion  ⭐️ <br>\n</h1>\n\n<p align=\"center\">\nAppFlowy is the AI workspace where you achieve more without losing control of your data\n</p>\n\n<p align=\"center\">\n<a href=\"https://discord.gg/9Q2xaN37tV\"><img src=\"https://img.shields.io/badge/AppFlowy.IO-discord-orange\"></a>\n<a href=\"https://github.com/AppFlowy-IO/appflowy\"><img src=\"https://img.shields.io/github/stars/AppFlowy-IO/appflowy.svg?style=flat&logo=github&colorB=deeppink&label=stars\"></a>\n<a href=\"https://github.com/AppFlowy-IO/appflowy\"><img src=\"https://img.shields.io/github/forks/AppFlowy-IO/appflowy.svg\"></a>\n<a href=\"https://opensource.org/licenses/AGPL-3.0\"><img src=\"https://img.shields.io/badge/license-AGPL-purple.svg\" alt=\"License: AGPL\"></a>\n\n</p>\n\n<p align=\"center\">\n    <a href=\"https://www.appflowy.com\"><b>Website</b></a> •\n    <a href=\"https://forum.appflowy.io/\"><b>Forum</b></a> •\n    <a href=\"https://discord.gg/9Q2xaN37tV\"><b>Discord</b></a> •\n    <a href=\"https://www.reddit.com/r/AppFlowy\"><b>Reddit</b></a> •\n    <a href=\"https://twitter.com/appflowy\"><b>Twitter</b></a>\n</p>\n\n<p align=\"center\"><img src=\"https://appflowy.com/_next/static/media/tasks.796c753e.png\" alt=\"AppFlowy Kanban Board for To-dos\"  /></p>\n<p align=\"center\"><img src=\"https://appflowy.com/_next/static/media/Grid.9e30484b.png\" alt=\"AppFlowy Databases for Tasks and Projects\"  /></p>\n<p align=\"center\"><img src=\"https://appflowy.com/_next/static/media/sites.a8d5b2b9.png\" alt=\"AppFlowy Sites for Beautiful documentation\"  /></p>\n<p align=\"center\"><img src=\"https://appflowy.com/_next/static/media/ai.e1460982.png\" alt=\"AppFlowy AI\" /></p>\n<p align=\"center\"><img src=\"https://appflowy.com/_next/static/media/template.9ea13c3b.png\" alt=\"AppFlowy Templates\"  /></p>\n\n<br></br>\n<p align=\"center\" >\n    <img src=\"https://github.com/user-attachments/assets/5841c491-b564-4a26-b9b6-191def430911\" alt=\"Work across devices\" width=\"1040px\" /></p>\n<p align=\"center\" >\n    <img src=\"https://github.com/user-attachments/assets/c2ba6bb8-746c-4743-9393-d008a669be95\" alt=\"Work across devices\" width=\"1040px\" /></p>\n<p align=\"center\" >\n    <img src=\"https://github.com/user-attachments/assets/e83dd1a3-4975-4d0e-91a1-9eb6e0d248cd\" alt=\"Work across devices\" width=\"1040px\" /></p>\n\n## User Installation\n\n- [Download AppFlowy Desktop (macOS, Windows, and Linux)](https://github.com/AppFlowy-IO/AppFlowy/releases)\n- Other\n  channels: [FlatHub](https://flathub.org/apps/io.appflowy.AppFlowy), [Snapcraft](https://snapcraft.io/appflowy), [Sourceforge](https://sourceforge.net/projects/appflowy/)\n- Available on\n    - [App Store](https://apps.apple.com/app/appflowy/id6457261352): iPhone\n    - [Play Store](https://play.google.com/store/apps/details?id=io.appflowy.appflowy): Android 10 or above; ARMv7 is\n      not supported\n- [Self-hosting AppFlowy](https://appflowy.com/docs/Step-by-step-Self-Hosting-Guide---From-Zero-to-Production)\n- [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source)\n\n## Built With\n\n- [Flutter](https://flutter.dev/)\n\n- [Rust](https://www.rust-lang.org/)\n\n## Stay Up-to-Date\n\n<p align=\"center\"><img src=\"https://github.com/AppFlowy-IO/appflowy/blob/main/doc/imgs/howtostar.gif\" alt=\"AppFlowy Github - how to star the repo\" width=\"100%\" /></p>\n\n## Getting Started with development\n\nPlease view the [documentation](https://docs.appflowy.io/docs/documentation/appflowy/from-source) for OS specific\ndevelopment instructions\n\n## Roadmap\n\n- [AppFlowy Roadmap ReadMe](https://docs.appflowy.io/docs/appflowy/roadmap)\n- [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12)\n\nIf you'd like to propose a feature, submit a feature\nrequest [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+) <br/>\nIf you'd like to report a bug, submit a bug\nreport [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+)\n\n## **Releases**\n\nPlease see the [changelog](https://appflowy.com/what-is-new) for more details about a given release.\n\n## Contributing\n\nContributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make\nare **greatly appreciated**. Please look\nat [Contributing to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy)\nfor details.\n\nIf your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly\neasier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains\nthe community, **Congratulations!** You are now an official contributor to AppFlowy.\n\n## Translations 🌎🗺\n\n[![translation badge](https://inlang.com/badge?url=github.com/AppFlowy-IO/AppFlowy)](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy?ref=badge)\n\nTo add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use\nthe [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or\nrun `npx inlang machine translate` to add missing translations.\n\n## Join the community to build AppFlowy together\n\n<a href=\"https://github.com/AppFlowy-IO/AppFlowy/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=AppFlowy-IO/AppFlowy\" />\n</a>\n\n## Why Are We Building This?\n\nNotion has been our favourite project and knowledge management tool in recent years because of its aesthetic appeal and\nfunctionality. Our team uses it daily, and we are on its paid plan. However, as we all know, Notion has its limitations.\nThese include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative\nworkplace management tools also have their constraints.\n\nThe limitations we encountered using these tools and our past work experience with collaborative productivity tools have\nled to our firm belief that there is a glass ceiling on what's possible for these tools in the future. This emanates\nfrom the fact that these tools will probably struggle to scale horizontally at some point and be forced to prioritize a\nproportion of customers whose needs differ from the rest. While decision-makers want a workplace OS, it is impossible to\ncome up with a one-size fits all solution in such a fragmented market.\n\nWhen a customer's evolving core needs are not satisfied, they either switch to another or build one from the ground up,\nin-house. Consequently, they either go under another ceiling or buy an expensive ticket to learn a hard lesson. This is\na requirement for many resources and expertise, building a reliable and easy-to-use collaborative tool, not to mention\nthe speed and native experience. The same may apply to individual users as well.\n\nAll these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs\nwell.\n\n- To individuals, we would like to offer Notion's functionality, data security, and cross-platform native experience.\n- To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to\n  enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy\n  your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term\n  maintainability.\n\nWe decided to achieve this mission by upholding the three most fundamental values:\n\n- Data privacy first\n- Reliable native experience\n- Community-driven extensibility\n\nWe do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority\ndoesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the\nknowledge and wheels of making complex workplace management tools while enabling people and businesses to create\nbeautiful things on their own by equipping them with a versatile toolbox of building blocks.\n\n## License\n\nDistributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/LICENSE) for\nmore information.\n\n## Acknowledgments\n\nSpecial thanks to these amazing projects which help power AppFlowy:\n\n- [cargo-make](https://github.com/sagiegurari/cargo-make)\n- [contrib.rocks](https://contrib.rocks)\n- [flutter_chat_ui](https://pub.dev/packages/flutter_chat_ui)\n"
  },
  {
    "path": "ROADMAP.md",
    "content": "## Our [roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12) is where you can learn about the features we’re working on, their status, when we expect to release them, and how you can help us. \n\n## Find more information about how to use our official AppFlowy public roadmap on [Gitbook](https://appflowy.gitbook.io/docs/essential-documentation/roadmap).\n"
  },
  {
    "path": "codemagic.yaml",
    "content": "workflows:\n  ios-workflow:\n    name: iOS Workflow\n    instance_type: mac_mini_m2\n    max_build_duration: 30\n    environment:\n      flutter: 3.27.4\n      xcode: latest\n      cocoapods: default\n\n    scripts:\n      - name: Build Flutter\n        script: |\n          curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\n          source \"$HOME/.cargo/env\"\n          rustc --version\n          cargo --version\n\n          cd frontend\n\n          rustup target install aarch64-apple-ios-sim\n          cargo install --force cargo-make\n          cargo install --force --locked duckscript_cli\n          cargo install --force cargo-lipo\n\n          cargo make appflowy-flutter-deps-tools\n          cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios\n          cargo make --profile development-ios-arm64-sim code_generation\n\n      - name: iOS integration tests\n        script: |\n          cd frontend/appflowy_flutter\n          flutter emulators --launch apple_ios_simulator\n          flutter -d iPhone test integration_test/runner.dart\n\n    artifacts:\n      - build/ios/ipa/*.ipa\n      - /tmp/xcodebuild_logs/*.log\n      - flutter_drive.log\n\n    publishing:\n      email:\n        recipients:\n          - lucas.xu@appflowy.io\n        notify:\n          success: true\n          failure: true\n"
  },
  {
    "path": "commitlint.config.js",
    "content": "// module.exports = {extends: ['@commitlint/config-conventional']}\nmodule.exports = {\n    rules: {\n        'header-max-length': [2, 'always', 100],\n\n        'type-enum': [2, 'always', ['build', 'chore', 'ci', 'docs', 'feat', 'feature', 'fix', 'refactor', 'style', 'test']],\n        'type-empty': [2, 'never'],\n        'type-case': [2, 'always', 'lower-case'],\n\n        'subject-empty': [2, 'never'],\n        'subject-case': [\n            0,\n            'never',\n            ['sentence-case', 'start-case', 'pascal-case', 'upper-case'],\n        ],\n\n        'body-leading-blank': [2, 'always'],\n        'body-max-line-length': [2, 'always', 200],\n        'body-case': [0, 'never', []],\n\n        'footer-leading-blank': [1, 'always'],\n        'footer-max-line-length': [2, 'always', 100]\n    },\n};\n\n"
  },
  {
    "path": "doc/CONTRIBUTING.md",
    "content": "<p align=\"center\"><img src=\"imgs/appflowy_title_and_logo.png\" alt=\"The Open Source Notion Alternative.\" width=\"500px\" /></p>\n\n# Contributing to AppFlowy\n\nHello, and welcome! Whether you are trying to report a bug, proposing a feature request, or want to work on the code you should go visit [Contributing to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy)\n\nWe look forward to hearing from you!\n"
  },
  {
    "path": "doc/roadmap.md",
    "content": "[AppFlowy Roadmap ReadMe](https://appflowy.gitbook.io/docs/essential-documentation/roadmap)\n\n[AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12)\n"
  },
  {
    "path": "frontend/.vscode/launch.json",
    "content": "{\n  // Use IntelliSense to learn about possible attributes.\n  // Hover to view descriptions of existing attributes.\n  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n      {\n          // This task only builds the Dart code of AppFlowy.\n          // It supports both the desktop and mobile version.\n          \"name\": \"AF: Build Dart Only\",\n          \"request\": \"launch\",\n          \"program\": \"./lib/main.dart\",\n          \"type\": \"dart\",\n          \"env\": {\n              \"RUST_LOG\": \"debug\",\n          },\n          // uncomment the following line to testing performance.\n          // \"flutterMode\": \"profile\",\n          \"cwd\": \"${workspaceRoot}/appflowy_flutter\"\n      },\n      {\n          // This task builds the Rust and Dart code of AppFlowy.\n          \"name\": \"AF-desktop: Build All\",\n          \"request\": \"launch\",\n          \"program\": \"./lib/main.dart\",\n          \"type\": \"dart\",\n          \"preLaunchTask\": \"AF: Build Appflowy Core\",\n          \"env\": {\n              \"RUST_LOG\": \"trace\",\n              \"RUST_BACKTRACE\": \"1\"\n          },\n          \"cwd\": \"${workspaceRoot}/appflowy_flutter\"\n      },\n      {\n          // This task builds will:\n          // - call the clean task,\n          // - rebuild all the generated Files (including freeze and language files)\n          // - rebuild the the Rust and Dart code of AppFlowy.\n          \"name\": \"AF-desktop: Clean + Rebuild All\",\n          \"request\": \"launch\",\n          \"program\": \"./lib/main.dart\",\n          \"type\": \"dart\",\n          \"preLaunchTask\": \"AF: Clean + Rebuild All\",\n          \"env\": {\n              \"RUST_LOG\": \"trace\"\n          },\n          \"cwd\": \"${workspaceRoot}/appflowy_flutter\"\n      },\n      {\n          \"name\": \"AF-iOS: Build All\",\n          \"request\": \"launch\",\n          \"program\": \"./lib/main.dart\",\n          \"type\": \"dart\",\n          \"preLaunchTask\": \"AF: Build Appflowy Core For iOS\",\n          \"env\": {\n              \"RUST_LOG\": \"trace\"\n          },\n          \"cwd\": \"${workspaceRoot}/appflowy_flutter\"\n      },\n      {\n          \"name\": \"AF-iOS: Clean + Rebuild All\",\n          \"request\": \"launch\",\n          \"program\": \"./lib/main.dart\",\n          \"type\": \"dart\",\n          \"preLaunchTask\": \"AF: Clean + Rebuild All (iOS)\",\n          \"env\": {\n              \"RUST_LOG\": \"trace\"\n          },\n          \"cwd\": \"${workspaceRoot}/appflowy_flutter\"\n      },\n      {\n          \"name\": \"AF-iOS-Simulator: Build All\",\n          \"request\": \"launch\",\n          \"program\": \"./lib/main.dart\",\n          \"type\": \"dart\",\n          \"preLaunchTask\": \"AF: Build Appflowy Core For iOS Simulator\",\n          \"env\": {\n              \"RUST_LOG\": \"trace\"\n          },\n          \"cwd\": \"${workspaceRoot}/appflowy_flutter\"\n      },\n      {\n          \"name\": \"AF-iOS-Simulator: Clean + Rebuild All\",\n          \"request\": \"launch\",\n          \"program\": \"./lib/main.dart\",\n          \"type\": \"dart\",\n          \"preLaunchTask\": \"AF: Clean + Rebuild All (iOS Simulator)\",\n          \"env\": {\n              \"RUST_LOG\": \"trace\"\n          },\n          \"cwd\": \"${workspaceRoot}/appflowy_flutter\"\n      },\n      {\n          \"name\": \"AF-Android: Build All\",\n          \"request\": \"launch\",\n          \"program\": \"./lib/main.dart\",\n          \"type\": \"dart\",\n          \"preLaunchTask\": \"AF: Build Appflowy Core For Android\",\n          \"env\": {\n              \"RUST_LOG\": \"trace\"\n          },\n          \"cwd\": \"${workspaceRoot}/appflowy_flutter\"\n      },\n      {\n          \"name\": \"AF-Android: Clean + Rebuild All\",\n          \"request\": \"launch\",\n          \"program\": \"./lib/main.dart\",\n          \"type\": \"dart\",\n          \"preLaunchTask\": \"AF: Clean + Rebuild All (Android)\",\n          \"env\": {\n              \"RUST_LOG\": \"trace\"\n          },\n          \"cwd\": \"${workspaceRoot}/appflowy_flutter\"\n      },\n      {\n          \"name\": \"AF-desktop: Debug Rust\",\n          \"type\": \"lldb\",\n          \"request\": \"attach\",\n          \"pid\": \"${command:pickMyProcess}\"\n          // To launch the application directly, use the following configuration:\n          // \"request\": \"launch\",\n          // \"program\": \"[YOUR_APPLICATION_PATH]\",\n      },\n  ]\n}\n"
  },
  {
    "path": "frontend/.vscode/tasks.json",
    "content": "{\n  \"version\": \"2.0.0\",\n  // https://code.visualstudio.com/docs/editor/tasks\n  // https://gist.github.com/deadalusai/9e13e36d61ec7fb72148\n  // ${workspaceRoot}: the root folder of the team\n  // ${file}: the current opened file\n  // ${fileBasename}: the current opened file's basename\n  // ${fileDirname}: the current opened file's dirname\n  // ${fileExtname}: the current opened file's extension\n  // ${cwd}: the current working directory of the spawned process\n  \"tasks\": [\n    {\n      \"label\": \"AF: Clean + Rebuild All\",\n      \"type\": \"shell\",\n      \"dependsOrder\": \"sequence\",\n      \"dependsOn\": [\n        \"AF: Dart Clean\",\n        \"AF: Flutter Clean\",\n        \"AF: Build Appflowy Core\",\n        \"AF: Flutter Pub Get\",\n        \"AF: Generate Language Files\",\n        \"AF: Generate Freezed Files\",\n        \"AF: Generate Svg Files\"\n      ],\n      \"presentation\": {\n        \"reveal\": \"always\",\n        \"panel\": \"new\"\n      }\n    },\n    {\n      \"label\": \"AF: Clean + Rebuild All (iOS)\",\n      \"type\": \"shell\",\n      \"dependsOrder\": \"sequence\",\n      \"dependsOn\": [\n        \"AF: Dart Clean\",\n        \"AF: Flutter Clean\",\n        \"AF: Build Appflowy Core For iOS\",\n        \"AF: Flutter Pub Get\",\n        \"AF: Generate Language Files\",\n        \"AF: Generate Freezed Files\",\n        \"AF: Generate Svg Files\"\n      ],\n      \"presentation\": {\n        \"reveal\": \"always\",\n        \"panel\": \"new\"\n      }\n    },\n    {\n      \"label\": \"AF: Clean + Rebuild All (iOS Simulator)\",\n      \"type\": \"shell\",\n      \"dependsOrder\": \"sequence\",\n      \"dependsOn\": [\n        \"AF: Dart Clean\",\n        \"AF: Flutter Clean\",\n        \"AF: Build Appflowy Core For iOS Simulator\",\n        \"AF: Flutter Pub Get\",\n        \"AF: Generate Language Files\",\n        \"AF: Generate Freezed Files\",\n        \"AF: Generate Svg Files\"\n      ],\n      \"presentation\": {\n        \"reveal\": \"always\",\n        \"panel\": \"new\"\n      }\n    },\n    {\n      \"label\": \"AF: Clean + Rebuild All (Android)\",\n      \"type\": \"shell\",\n      \"dependsOrder\": \"sequence\",\n      \"dependsOn\": [\n        \"AF: Dart Clean\",\n        \"AF: Flutter Clean\",\n        \"AF: Build Appflowy Core For Android\",\n        \"AF: Flutter Pub Get\",\n        \"AF: Generate Language Files\",\n        \"AF: Generate Freezed Files\",\n        \"AF: Generate Svg Files\"\n      ],\n      \"presentation\": {\n        \"reveal\": \"always\",\n        \"panel\": \"new\"\n      }\n    },\n    {\n      \"label\": \"AF: Build Appflowy Core\",\n      \"type\": \"shell\",\n      \"windows\": {\n        \"command\": \"cargo make --profile development-windows-x86 appflowy-core-dev\"\n      },\n      \"linux\": {\n        \"command\": \"cargo make --profile \\\"development-linux-$(uname -m)\\\" appflowy-core-dev\"\n      },\n      \"osx\": {\n        \"command\": \"cargo make --profile \\\"development-mac-$(uname -m)\\\" appflowy-core-dev\"\n      },\n      \"group\": \"build\",\n      \"options\": {\n        \"cwd\": \"${workspaceFolder}\"\n      }\n    },\n    {\n      \"label\": \"AF: Build Appflowy Core For iOS\",\n      \"type\": \"shell\",\n      \"command\": \"cargo make --profile development-ios-arm64 appflowy-core-dev-ios\",\n      \"group\": \"build\",\n      \"options\": {\n        \"cwd\": \"${workspaceFolder}\"\n      }\n    },\n    {\n      \"label\": \"AF: Build Appflowy Core For iOS Simulator\",\n      \"type\": \"shell\",\n      \"command\": \"cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios\",\n      \"group\": \"build\",\n      \"options\": {\n        \"cwd\": \"${workspaceFolder}\"\n      }\n    },\n    {\n      \"label\": \"AF: Build Appflowy Core For Android\",\n      \"type\": \"shell\",\n      \"command\": \"cargo make --profile development-android appflowy-core-dev-android\",\n      \"group\": \"build\",\n      \"options\": {\n        \"cwd\": \"${workspaceFolder}\"\n      }\n    },\n    {\n      \"label\": \"AF: Code Gen\",\n      \"type\": \"shell\",\n      \"dependsOrder\": \"sequence\",\n      \"dependsOn\": [\n        \"AF: Flutter Clean\",\n        \"AF: Flutter Pub Get\",\n        \"AF: Generate Language Files\",\n        \"AF: Generate Freezed Files\",\n        \"AF: Generate Svg Files\"\n      ],\n      \"group\": {\n        \"kind\": \"build\",\n        \"isDefault\": true\n      },\n      \"presentation\": {\n        \"reveal\": \"always\",\n        \"panel\": \"new\"\n      }\n    },\n    {\n      \"label\": \"AF: Dart Clean\",\n      \"type\": \"shell\",\n      \"command\": \"cargo make flutter_clean\",\n      \"group\": \"build\",\n      \"options\": {\n        \"cwd\": \"${workspaceFolder}\"\n      }\n    },\n    {\n      \"label\": \"AF: Flutter Clean\",\n      \"type\": \"shell\",\n      \"command\": \"flutter clean\",\n      \"options\": {\n        \"cwd\": \"${workspaceFolder}/appflowy_flutter\"\n      }\n    },\n    {\n      \"label\": \"AF: Flutter Pub Get\",\n      \"type\": \"shell\",\n      \"command\": \"flutter pub get\",\n      \"options\": {\n        \"cwd\": \"${workspaceFolder}/appflowy_flutter\"\n      }\n    },\n    {\n      \"label\": \"AF: Generate Freezed Files\",\n      \"type\": \"shell\",\n      \"command\": \"sh ./scripts/code_generation/freezed/generate_freezed.sh\",\n      \"options\": {\n        \"cwd\": \"${workspaceFolder}\"\n      },\n      \"group\": \"build\",\n      \"windows\": {\n        \"options\": {\n          \"shell\": {\n            \"executable\": \"cmd.exe\",\n            \"args\": [\n              \"/d\",\n              \"/c\",\n              \".\\\\scripts\\\\code_generation\\\\freezed\\\\generate_freezed.cmd\"\n            ]\n          }\n        }\n      }\n    },\n    {\n      \"label\": \"AF: Generate Language Files\",\n      \"type\": \"shell\",\n      \"command\": \"sh ./scripts/code_generation/language_files/generate_language_files.sh\",\n      \"windows\": {\n        \"options\": {\n          \"shell\": {\n            \"executable\": \"cmd.exe\",\n            \"args\": [\n              \"/d\",\n              \"/c\",\n              \".\\\\scripts\\\\code_generation\\\\language_files\\\\generate_language_files.cmd\"\n            ]\n          }\n        }\n      },\n      \"group\": \"build\",\n      \"options\": {\n        \"cwd\": \"${workspaceFolder}\"\n      }\n    },\n    {\n      \"label\": \"AF: Generate Svg Files\",\n      \"type\": \"shell\",\n      \"command\": \"sh ./scripts/code_generation/flowy_icons/generate_flowy_icons.sh\",\n      \"windows\": {\n        \"options\": {\n          \"shell\": {\n            \"executable\": \"cmd.exe\",\n            \"args\": [\n              \"/d\",\n              \"/c\",\n              \".\\\\scripts\\\\code_generation\\\\flowy_icons\\\\generate_flowy_icons.cmd\"\n            ]\n          }\n        }\n      },\n      \"group\": \"build\",\n      \"options\": {\n        \"cwd\": \"${workspaceFolder}\"\n      }\n    },\n    {\n      \"label\": \"AF: flutter build aar\",\n      \"type\": \"flutter\",\n      \"command\": \"flutter\",\n      \"args\": [\n        \"build\",\n        \"aar\"\n      ],\n      \"group\": \"build\",\n      \"problemMatcher\": [],\n      \"detail\": \"appflowy_flutter\"\n    },\n    {\n      \"label\": \"AF: Generate Env File\",\n      \"type\": \"shell\",\n      \"command\": \"dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs\",\n      \"options\": {\n        \"cwd\": \"${workspaceFolder}/appflowy_flutter\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "frontend/Makefile.toml",
    "content": "#https://github.com/sagiegurari/cargo-make\n\nextend = [\n  { path = \"scripts/makefile/desktop.toml\" },\n  { path = \"scripts/makefile/mobile.toml\" },\n  { path = \"scripts/makefile/protobuf.toml\" },\n  { path = \"scripts/makefile/tests.toml\" },\n  { path = \"scripts/makefile/docker.toml\" },\n  { path = \"scripts/makefile/env.toml\" },\n  { path = \"scripts/makefile/flutter.toml\" },\n  { path = \"scripts/makefile/tool.toml\" },\n  { path = \"scripts/makefile/tauri.toml\" },\n  { path = \"scripts/makefile/web.toml\" },\n]\n\n[config]\non_error_task = \"catch\"\n\n[tasks.catch]\nrun_task = { name = [\"restore-crate-type\"] }\n\n[env]\nRUST_LOG = \"info\"\nCARGO_PROFILE = \"dev\"\nCARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true\nCARGO_MAKE_CRATE_FS_NAME = \"dart_ffi\"\nCARGO_MAKE_CRATE_NAME = \"dart-ffi\"\nLIB_NAME = \"dart_ffi\"\nAPPFLOWY_VERSION = \"0.9.9\"\nFLUTTER_DESKTOP_FEATURES = \"dart\"\nPRODUCT_NAME = \"AppFlowy\"\nMACOSX_DEPLOYMENT_TARGET = \"11.0\"\n# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html\n# If you update the macOS's CRATE_TYPE, don't forget to update the\n# appflowy_backend.podspec\n#   for staticlib:\n#        s.static_framework = true\n#        s.vendored_libraries = \"libdart_ffi.a\"\n#   for cdylib:\n#        s.vendored_libraries = \"libdart_ffi.dylib\"\n#\n# Remember to update the ffi.dart:\n#   for staticlib:\n#       if (Platform.isMacOS) return DynamicLibrary.open('${prefix}/libdart_ffi.a');\n#   for cdylib:\n#       if (Platform.isMacOS) return DynamicLibrary.open('${prefix}/libdart_ffi.dylib');\nCRATE_TYPE = \"staticlib\"\nLIB_EXT = \"a\"\nAPP_ENVIRONMENT = \"local\"\nFLUTTER_FLOWY_SDK_PATH = \"appflowy_flutter/packages/appflowy_backend\"\nTAURI_BACKEND_SERVICE_PATH = \"appflowy_tauri/src/services/backend\"\nWEB_BACKEND_SERVICE_PATH = \"appflowy_web/src/services/backend\"\nTAURI_APP_BACKEND_SERVICE_PATH = \"appflowy_web_app/src/application/services/tauri-services/backend\"\n# Test default config\nTEST_CRATE_TYPE = \"cdylib\"\nTEST_LIB_EXT = \"dylib\"\nTEST_BUILD_FLAG = \"debug\"\nTEST_COMPILE_TARGET = \"x86_64-apple-darwin\"\n\n[env.development-mac-arm64]\nRUST_LOG = \"info\"\nTARGET_OS = \"macos\"\nRUST_COMPILE_TARGET = \"aarch64-apple-darwin\"\nBUILD_FLAG = \"debug\"\nFLUTTER_OUTPUT_DIR = \"Debug\"\nPRODUCT_EXT = \"app\"\nBUILD_ARCHS = \"arm64\"\nBUILD_ACTIVE_ARCHS_ONLY = true\nCRATE_TYPE = \"staticlib\"\n\n[env.development-mac-x86_64]\nRUST_LOG = \"info\"\nTARGET_OS = \"macos\"\nRUST_COMPILE_TARGET = \"x86_64-apple-darwin\"\nBUILD_FLAG = \"debug\"\nFLUTTER_OUTPUT_DIR = \"Debug\"\nPRODUCT_EXT = \"app\"\nBUILD_ARCHS = \"x86_64\"\nBUILD_ACTIVE_ARCHS_ONLY = true\nCRATE_TYPE = \"staticlib\"\n\n[env.production-mac-arm64]\nCARGO_PROFILE = \"release\"\nBUILD_FLAG = \"release\"\nTARGET_OS = \"macos\"\nRUST_COMPILE_TARGET = \"aarch64-apple-darwin\"\nFLUTTER_OUTPUT_DIR = \"Release\"\nPRODUCT_EXT = \"app\"\nAPP_ENVIRONMENT = \"production\"\nBUILD_ARCHS = \"arm64\"\nBUILD_ACTIVE_ARCHS_ONLY = false\nCRATE_TYPE = \"staticlib\"\n\n[env.production-mac-x86_64]\nCARGO_PROFILE = \"release\"\nBUILD_FLAG = \"release\"\nTARGET_OS = \"macos\"\nRUST_COMPILE_TARGET = \"x86_64-apple-darwin\"\nFLUTTER_OUTPUT_DIR = \"Release\"\nPRODUCT_EXT = \"app\"\nAPP_ENVIRONMENT = \"production\"\nBUILD_ARCHS = \"x86_64\"\nBUILD_ACTIVE_ARCHS_ONLY = false\nCRATE_TYPE = \"staticlib\"\n\n[env.production-mac-universal]\nCARGO_PROFILE = \"release\"\nBUILD_FLAG = \"release\"\nTARGET_OS = \"macos\"\nFLUTTER_OUTPUT_DIR = \"Release\"\nPRODUCT_EXT = \"app\"\nBUILD_ACTIVE_ARCHS_ONLY = false\nAPP_ENVIRONMENT = \"production\"\n\n[env.development-windows-x86]\nTARGET_OS = \"windows\"\nRUST_COMPILE_TARGET = \"x86_64-pc-windows-msvc\"\nBUILD_FLAG = \"debug\"\nFLUTTER_OUTPUT_DIR = \"Debug\"\nPRODUCT_EXT = \"exe\"\nCRATE_TYPE = \"cdylib\"\nLIB_EXT = \"dll\"\n\n[env.production-windows-x86]\nCARGO_PROFILE = \"release\"\nBUILD_FLAG = \"release\"\nTARGET_OS = \"windows\"\nRUST_COMPILE_TARGET = \"x86_64-pc-windows-msvc\"\nFLUTTER_OUTPUT_DIR = \"Release\"\nPRODUCT_EXT = \"exe\"\nCRATE_TYPE = \"cdylib\"\nLIB_EXT = \"dll\"\nBUILD_ARCHS = \"x64\"\nAPP_ENVIRONMENT = \"production\"\n\n[env.development-linux-x86_64]\nTARGET_OS = \"linux\"\nRUST_COMPILE_TARGET = \"x86_64-unknown-linux-gnu\"\nBUILD_FLAG = \"debug\"\nCRATE_TYPE = \"cdylib\"\nFLUTTER_OUTPUT_DIR = \"Debug\"\nLIB_EXT = \"so\"\nLINUX_ARCH = \"x64\"\n\n[env.production-linux-x86_64]\nCARGO_PROFILE = \"release\"\nBUILD_FLAG = \"release\"\nTARGET_OS = \"linux\"\nRUST_COMPILE_TARGET = \"x86_64-unknown-linux-gnu\"\nCRATE_TYPE = \"cdylib\"\nFLUTTER_OUTPUT_DIR = \"Release\"\nLIB_EXT = \"so\"\nLINUX_ARCH = \"x64\"\nAPP_ENVIRONMENT = \"production\"\n\n[env.development-linux-aarch64]\nTARGET_OS = \"linux\"\nRUST_COMPILE_TARGET = \"aarch64-unknown-linux-gnu\"\nBUILD_FLAG = \"debug\"\nCRATE_TYPE = \"cdylib\"\nFLUTTER_OUTPUT_DIR = \"Debug\"\nLIB_EXT = \"so\"\nLINUX_ARCH = \"arm64\"\nFLUTTER_DESKTOP_FEATURES = \"dart,openssl_vendored\"\n\n[env.production-linux-aarch64]\nCARGO_PROFILE = \"release\"\nBUILD_FLAG = \"release\"\nTARGET_OS = \"linux\"\nRUST_COMPILE_TARGET = \"aarch64-unknown-linux-gnu\"\nCRATE_TYPE = \"cdylib\"\nFLUTTER_OUTPUT_DIR = \"Release\"\nLIB_EXT = \"so\"\nLINUX_ARCH = \"arm64\"\nAPP_ENVIRONMENT = \"production\"\nFLUTTER_DESKTOP_FEATURES = \"dart,openssl_vendored\"\n\n[env.development-ios-arm64-sim]\nBUILD_FLAG = \"debug\"\nTARGET_OS = \"ios\"\nFLUTTER_OUTPUT_DIR = \"Debug\"\nRUST_COMPILE_TARGET = \"aarch64-apple-ios-sim\"\nBUILD_ARCHS = \"arm64\"\nCRATE_TYPE = \"staticlib\"\n\n[env.development-ios-arm64]\nBUILD_FLAG = \"debug\"\nTARGET_OS = \"ios\"\nFLUTTER_OUTPUT_DIR = \"Debug\"\nRUST_COMPILE_TARGET = \"aarch64-apple-ios\"\nBUILD_ARCHS = \"arm64\"\nCRATE_TYPE = \"staticlib\"\n\n[env.production-ios-arm64]\nBUILD_FLAG = \"release\"\nTARGET_OS = \"ios\"\nFLUTTER_OUTPUT_DIR = \"Release\"\nRUST_COMPILE_TARGET = \"aarch64-apple-ios\"\nBUILD_ARCHS = \"arm64\"\nCRATE_TYPE = \"staticlib\"\n\n[env.development-android]\nBUILD_FLAG = \"debug\"\nTARGET_OS = \"android\"\nCRATE_TYPE = \"cdylib\"\nFLUTTER_OUTPUT_DIR = \"Debug\"\nLIB_EXT = \"so\"\nPRODUCT_EXT = \"apk\"\nFLUTTER_DESKTOP_FEATURES = \"dart,openssl_vendored\"\n\n[env.production-android]\nBUILD_FLAG = \"release\"\nTARGET_OS = \"android\"\nCRATE_TYPE = \"cdylib\"\nFLUTTER_OUTPUT_DIR = \"Release\"\nPRODUCT_EXT = \"apk\"\nLIB_EXT = \"so\"\n\n[tasks.echo_env]\nscript = ['''\n    echo \"-------- Env Parameters --------\"\n    echo CRATE_TYPE: ${CRATE_TYPE}\n    echo BUILD_FLAG: ${BUILD_FLAG}\n    echo TARGET_OS: ${TARGET_OS}\n    echo RUST_COMPILE_TARGET: ${RUST_COMPILE_TARGET}\n    echo FEATURES: ${FLUTTER_DESKTOP_FEATURES}\n    echo PRODUCT_EXT: ${PRODUCT_EXT}\n    echo APP_ENVIRONMENT: ${APP_ENVIRONMENT}\n    echo BUILD_ARCHS: ${BUILD_ARCHS}\n    echo BUILD_VERSION: ${BUILD_VERSION}\n    echo RUST_VERSION: \"$(rustc --version)\"\n    ''']\nscript_runner = \"@shell\"\n\n[tasks.setup-crate-type]\nprivate = true\nscript = [\n  \"\"\"\n      toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml\n      val = replace ${toml} \"staticlib\" ${CRATE_TYPE}\n      result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val}\n      assert ${result}\n      \"\"\",\n]\nscript_runner = \"@duckscript\"\n\n[tasks.restore-crate-type]\nprivate = true\nscript = [\n  \"\"\"\n      toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml\n      val = replace ${toml} ${CRATE_TYPE} \"staticlib\"\n      result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val}\n      assert ${result}\n      \"\"\",\n]\nscript_runner = \"@duckscript\"\n\n[env.test-macos-x86_64]\nTEST_CRATE_TYPE = \"cdylib\"\nTEST_LIB_EXT = \"dylib\"\n# For the moment, the DynamicLibrary only supports open x86_64 architectures binary.\nTEST_COMPILE_TARGET = \"x86_64-apple-darwin\"\n\n[env.test-macos-arm64]\nTEST_CRATE_TYPE = \"cdylib\"\nTEST_LIB_EXT = \"dylib\"\nTEST_COMPILE_TARGET = \"aarch64-apple-darwin\"\n\n[env.test-linux]\nTEST_CRATE_TYPE = \"cdylib\"\nTEST_LIB_EXT = \"so\"\nTEST_COMPILE_TARGET = \"x86_64-unknown-linux-gnu\"\n\n[env.test-windows]\nTEST_CRATE_TYPE = \"cdylib\"\nTEST_LIB_EXT = \"dll\"\nTEST_COMPILE_TARGET = \"x86_64-pc-windows-msvc\"\n\n[tasks.setup-test-crate-type]\nprivate = true\nscript = [\n  \"\"\"\n      toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml\n      val = replace ${toml} \"staticlib\" ${TEST_CRATE_TYPE}\n      result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val}\n      assert ${result}\n      \"\"\",\n]\nscript_runner = \"@duckscript\"\n\n[tasks.restore-test-crate-type]\nprivate = true\nscript = [\n  \"\"\"\n      toml = readfile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml\n      val = replace ${toml} ${TEST_CRATE_TYPE} \"staticlib\"\n      result = writefile ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/${CARGO_MAKE_CRATE_NAME}/Cargo.toml ${val}\n      assert ${result}\n      \"\"\",\n]\nscript_runner = \"@duckscript\"\n\n[tasks.test-build]\ncondition = { env_set = [\"FLUTTER_FLOWY_SDK_PATH\"] }\nscript = [\"\"\"\n      cd ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/flowy-net\n      cargo build -vv --features=dart\n      \"\"\"]\nscript_runner = \"@shell\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\n/build/\ngenerated*\nGenerated*\n\n# Web related\nlib/generated_plugin_registrant.dart\n\n# Language related generated files\nlib/generated/\n\n# Freezed generated files\n*.g.dart\n*.freezed.dart\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Android Studio will place build artifacts here\n/android/app/debug\n/android/app/profile\n/android/app/release\n\n/packages/flowy_protobuf\n/packages/flutter-quill\n\nproduct/**\nwindows/flutter/dart_ffi/\n\n**/**/*.dylib\n**/**/*.a\n**/**/*.lib\n**/**/*.dll\n**/**/*.so\n**/**/Brewfile.lock.json\n**/.sandbox\n**/.vscode/\n\n.env\n.env.*\n\ncoverage/\n\n**/failures/*.png\n\nassets/translations/\nassets/flowy_icons/*"
  },
  {
    "path": "frontend/appflowy_flutter/.metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled.\n\nversion:\n  revision: 682aa387cfe4fbd71ccd5418b2c2a075729a1c66\n  channel: unknown\n\nproject_type: app\n\n# Tracks metadata for the flutter migrate command\nmigration:\n  platforms:\n    - platform: root\n      create_revision: 682aa387cfe4fbd71ccd5418b2c2a075729a1c66\n      base_revision: 682aa387cfe4fbd71ccd5418b2c2a075729a1c66\n    - platform: android\n      create_revision: 682aa387cfe4fbd71ccd5418b2c2a075729a1c66\n      base_revision: 682aa387cfe4fbd71ccd5418b2c2a075729a1c66\n\n  # User provided section\n\n  # List of Local paths (relative to this file) that should be\n  # ignored by the migrate tool.\n  #\n  # Files that are not part of the templates will be ignored by default.\n  unmanaged_files:\n    - 'lib/main.dart'\n    - 'ios/Runner.xcodeproj/project.pbxproj'\n"
  },
  {
    "path": "frontend/appflowy_flutter/Makefile",
    "content": ".PHONY: freeze_build, free_watch\n\nfreeze_build:\n\tdart run build_runner build -d\n\nwatch:\n\tdart run build_runner watch"
  },
  {
    "path": "frontend/appflowy_flutter/README.md",
    "content": "<h1 align=\"center\" style=\"margin:0\"> AppFlowy_Flutter</h1>\n<div align=\"center\">\n  <img src=\"https://img.shields.io/badge/Flutter-v3.13.19-blue\"/>\n  <img src=\"https://img.shields.io/badge/Rust-v1.70-orange\"/>\n</div>\n\n> Documentation for Contributors\n\nThis Repository contains the codebase for the frontend of the application, currently we use Flutter as our frontend framework.\n\n### Platforms Supported Using Flutter 💻\n\n- Linux\n- macOS\n- Windows\n  > We are actively working on support for Android & iOS!\n\n_Additionally, we are working on a Web version built with Tauri!_\n\n### Am I Eligible to Contribute?\n\nYes! 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:\n\n- Non-Coding Contributions\n  - Documentation\n  - Feature Requests and Feedbacks\n  - Report Bugs\n  - Improve Translations\n- Coding Contributions\n\nTo 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).\n\n### What OS should I use for development?\n\nWe 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.\n\n### Getting Started ❇\n\nWe 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:\n\n- [Code Architecture](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/frontend/codemap)\n- [Styleguide & Conventions](https://docs.appflowy.io/docs/documentation/software-contributions/conventions/naming-conventions)\n- [Making Your First PR](https://docs.appflowy.io/docs/documentation/software-contributions/submitting-code/submitting-your-first-pull-request)\n- [All AppFlowy Documentation](https://docs.appflowy.io/docs/documentation/appflowy) - Contribution guide, build and run, debugging, testing, localization, etc.\n\n### Need Help?\n\n- New to GitHub? Follow [these](https://docs.appflowy.io/docs/documentation/software-contributions/submitting-code/setting-up-your-repositories) steps to get started\n- Stuck Somewhere? Join our [Discord](https://discord.gg/9Q2xaN37tV), we're there to help you!\n- Find out more about the [community initiatives](https://docs.appflowy.io/docs/appflowy/community).\n"
  },
  {
    "path": "frontend/appflowy_flutter/analysis_options.yaml",
    "content": "include: package:flutter_lints/flutter.yaml\n\nanalyzer:\n  exclude:\n    - \"**/*.g.dart\"\n    - \"**/*.freezed.dart\"\n    - \"packages/**/*.dart\"\n\nlinter:\n  rules:\n    - require_trailing_commas\n\n    - prefer_collection_literals\n    - prefer_final_fields\n    - prefer_final_in_for_each\n    - prefer_final_locals\n\n    - sized_box_for_whitespace\n    - use_decorated_box\n\n    - unnecessary_parenthesis\n    - unnecessary_await_in_return\n    - unnecessary_raw_strings\n\n    - avoid_unnecessary_containers\n    - avoid_redundant_argument_values\n    - avoid_unused_constructor_parameters\n\n    - always_declare_return_types\n\n    - sort_constructors_first\n    - unawaited_futures\n\nerrors:\n  invalid_annotation_target: ignore\n"
  },
  {
    "path": "frontend/appflowy_flutter/android/.gitignore",
    "content": "gradle-wrapper.jar\n/.gradle\n/captures/\n/gradlew\n/gradlew.bat\n/local.properties\nGeneratedPluginRegistrant.java\n\n# Remember to never publicly share your keystore.\n# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app\nkey.properties\n**/*.keystore\n**/*.jks\n\n.cxx\n"
  },
  {
    "path": "frontend/appflowy_flutter/android/README.md",
    "content": "# Description\n\nThis is a guide on how to build the rust SDK for AppFlowy on android.\nCompiling the sdk is easy it just needs a few tweaks.\nWhen compiling for android we need the following pre-requisites:\n\n- Android NDK Tools. (v24 has been tested).\n- Cargo NDK. (@latest version).\n\n**Getting the tools**\n- Install cargo-ndk ```bash cargo install cargo-ndk```.\n- [Download](https://developer.android.com/ndk/downloads/) Android NDK version 24.\n- When downloading Android NDK you can get the compressed version as a standalone from the site.\n    Or you can download it through [Android Studio](https://developer.android.com/studio).\n- After downloading the two you need to set the environment variables. For Windows that's a separate process.\n    On macOS and Linux the process is similar.\n- The variables needed are '$ANDROID_NDK_HOME', this will point to where the NDK is located.\n---\n\n**Cargo Config File**\nThis code needs to be written in ~/.cargo/config, this helps cargo know where to locate the android tools(linker and archiver).\n**NB** Keep in mind just replace 'user' with your own user name. Or just point it to the location of where you put the NDK.\n\n```toml\n[target.aarch64-linux-android]\nar = \"/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar\"\nlinker = \"/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang\"\n\n[target.armv7-linux-androideabi]\nar = \"/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar\"\nlinker = \"/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi29-clang\"\n\n[target.i686-linux-android]\nar = \"/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar\"\nlinker = \"/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android29-clang\"\n\n[target.x86_64-linux-android]\nar = \"/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar\"\nlinker = \"/home/user/Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android29-clang\"\n```\n\n**Clang Fix**\n In order to get clang to work properly with version 24 you need to create this file.\n libgcc.a, then add this one line.\n ```\n INPUT(-lunwind)\n ```\n\n**Folder path: 'Android/Sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/14.0.1/lib/linux'.**\nAfter that you have to copy this file into three different folders namely aarch64, arm, i386 and x86_64.\nWe have to do this so we Android NDK can find clang on our system, if we used NDK 22 we wouldn't have to do this process.\nThough using NDK v22 will not give us a lot of features to work with.\nThis GitHub [issue](https://github.com/fzyzcjy/flutter_rust_bridge/issues/419) explains the reason why we are doing this.\n\n ---\n\n **Android NDK**\n\n After installing the NDK tools for android you should export the PATH to your config file\n (.vimrc, .zshrc, .profile, .bashrc file), That way it can be found.\n\n ```vim\n export PATH=/home/sean/Android/Sdk/ndk/24.0.8215888\n ```"
  },
  {
    "path": "frontend/appflowy_flutter/android/app/build.gradle",
    "content": "def localProperties = new Properties()\ndef localPropertiesFile = rootProject.file('local.properties')\nif (localPropertiesFile.exists()) {\n    localPropertiesFile.withReader('UTF-8') { reader ->\n        localProperties.load(reader)\n    }\n}\n\ndef flutterRoot = localProperties.getProperty('flutter.sdk')\nif (flutterRoot == null) {\n    throw new GradleException(\"Flutter SDK not found. Define location with flutter.sdk in the local.properties file.\")\n}\n\ndef flutterVersionCode = localProperties.getProperty('flutter.versionCode')\nif (flutterVersionCode == null) {\n    flutterVersionCode = '1'\n}\n\ndef flutterVersionName = localProperties.getProperty('flutter.versionName')\nif (flutterVersionName == null) {\n    flutterVersionName = '1.0'\n}\n\napply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply from: \"$flutterRoot/packages/flutter_tools/gradle/flutter.gradle\"\n\ndef keystoreProperties = new Properties()\ndef keystorePropertiesFile = rootProject.file('key.properties')\nif (keystorePropertiesFile.exists()) {\n    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))\n}\n\nandroid {\n    compileSdkVersion 35\n    ndkVersion \"24.0.8215888\"\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    kotlinOptions {\n        jvmTarget = '1.8'\n    }\n\n    sourceSets {\n        main.java.srcDirs += 'src/main/kotlin'\n        main.jniLibs.srcDirs += 'jniLibs/'\n    }\n\n    defaultConfig {\n        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).\n        applicationId \"io.appflowy.appflowy\"\n        minSdkVersion 29\n        targetSdkVersion 35\n        versionCode flutterVersionCode.toInteger()\n        versionName flutterVersionName\n        multiDexEnabled true\n        externalNativeBuild {\n            cmake {\n                arguments \"-DANDROID_ARM_NEON=TRUE\", \"-DANDROID_STL=c++_shared\"\n            }\n        }\n    }\n\n    signingConfigs {\n       release {\n           keyAlias keystoreProperties['keyAlias']\n           keyPassword keystoreProperties['keyPassword']\n           storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null\n           storePassword keystoreProperties['storePassword']\n       }\n    }\n    buildTypes {\n        release {\n            // use release instead when publishing the application to google play.\n            // signingConfig signingConfigs.release\n            signingConfig signingConfigs.debug\n        }\n    }\n\n    namespace 'io.appflowy.appflowy'\n\n    externalNativeBuild {\n        cmake {\n            path \"src/main/CMakeLists.txt\"\n        }\n    }\n\n    // only support arm64-v8a\n    defaultConfig {\n        ndk {\n            abiFilters \"arm64-v8a\"\n        }\n    }\n}\n\nflutter {\n    source '../..'\n}\n\ndependencies {\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\"\n    implementation \"com.android.support:multidex:2.0.1\"\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/android/app/src/debug/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- Flutter needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <application android:label=\"AppFlowy\" android:icon=\"@mipmap/ic_launcher\"\n    android:name=\"${applicationName}\">\n    <activity android:name=\".MainActivity\"\n      android:exported=\"true\"\n      android:launchMode=\"singleInstance\"\n      android:theme=\"@style/LaunchTheme\"\n      android:configChanges=\"orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode\"\n      android:hardwareAccelerated=\"true\" android:windowSoftInputMode=\"adjustResize\">\n      <!--\n\t\t\tSpecifies an Android theme to apply to this Activity as soon as\n\t\t\tthe Android process has started. This theme is visible to the user\n\t\t\twhile the Flutter UI initializes. After that, this theme continues\n\t\t\tto determine the Window background behind the Flutter UI.\n\t\t\t-->\n      <meta-data android:name=\"io.flutter.embedding.android.NormalTheme\"\n        android:resource=\"@style/NormalTheme\" />\n      <!--\n\t\t\tDisplays an Android View that continues showing the launch screen\n\t\t\tDrawable until Flutter paints its first frame, then this splash\n\t\t\tscreen fades out. A splash screen is useful to avoid any visual\n\t\t\tgap between the end of Android's launch screen and the painting of\n\t\t\tFlutter's first frame.\n\t\t\t-->\n      <meta-data android:name=\"io.flutter.embedding.android.SplashScreenDrawable\"\n        android:resource=\"@drawable/launch_background\" />\n      <intent-filter>\n        <action android:name=\"android.intent.action.MAIN\" />\n        <category android:name=\"android.intent.category.LAUNCHER\" />\n      </intent-filter>\n      <meta-data android:name=\"flutter_deeplinking_enabled\" android:value=\"true\" />\n      <intent-filter android:autoVerify=\"true\">\n        <action android:name=\"android.intent.action.VIEW\" />\n        <category android:name=\"android.intent.category.DEFAULT\" />\n        <category android:name=\"android.intent.category.BROWSABLE\" />\n        <data android:scheme=\"http\" />\n        <data android:scheme=\"https\" />\n        <data android:scheme=\"appflowy-flutter\" />\n      </intent-filter>\n    </activity>\n    <!--\n\t\tDon't delete the meta-data below.\n\t\tThis is used by the Flutter tool to generate GeneratedPluginRegistrant.java\n\t\t-->\n    <meta-data android:name=\"flutterEmbedding\" android:value=\"2\" />\n    <meta-data android:name=\"io.flutter.embedding.android.EnableImpeller\"\n      android:value=\"false\" />\n  </application>\n  <uses-permission android:name=\"android.permission.INTERNET\" />\n  <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n  <!-- Permission to read files from external storage (outside application container).\n  As of Android 12 this permission no longer has any effect. Instead use the\n  READ_MEDIA_IMAGES, READ_MEDIA_VIDEO or READM_MEDIA_AUDIO permissions. -->\n  <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"\n    android:maxSdkVersion=\"32\" />\n  <!-- Permissions to read media files. -->\n  <uses-permission android:name=\"android.permission.READ_MEDIA_IMAGES\" />\n  <queries>\n    <intent>\n      <action android:name=\"android.support.customtabs.action.CustomTabsService\" />\n    </intent>\n  </queries>\n  <!--\n    Media access permissions.\n    Android 13 or higher.\n    Used for VideoBlock (edia_kit)\n  -->\n  <uses-permission android:name=\"android.permission.READ_MEDIA_AUDIO\" />\n  <uses-permission android:name=\"android.permission.READ_MEDIA_VIDEO\" />\n  <uses-permission android:name=\"android.permission.CAMERA\" />\n</manifest>"
  },
  {
    "path": "frontend/appflowy_flutter/android/app/src/main/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.10.0)\n\nproject(AppFlowy)\n\nmessage(CONFIGURE_LOG \"NDK PATH: ${ANDROID_NDK}\")\nmessage(CONFIGURE_LOG \"Copying libc++_shared.so\")\n\n# arm64-v8a\nfile(COPY\n    ${ANDROID_NDK}/sources/cxx-stl/llvm-libc++/libs/arm64-v8a/libc++_shared.so\n    DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/arm64-v8a\n)\n\n# armeabi-v7a\nfile(COPY\n    ${ANDROID_NDK}/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_shared.so\n    DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/armeabi-v7a\n)\n\n# x86_64\nfile(COPY\n    ${ANDROID_NDK}/sources/cxx-stl/llvm-libc++/libs/x86_64/libc++_shared.so\n    DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/x86_64\n)"
  },
  {
    "path": "frontend/appflowy_flutter/android/app/src/main/Classes/binding.h",
    "content": "#include <stdarg.h>\n#include <stdbool.h>\n#include <stdint.h>\n#include <stdlib.h>\n\nint64_t init_sdk(int64_t port, char *data);\n\nvoid async_event(int64_t port, const uint8_t *input, uintptr_t len);\n\nconst uint8_t *sync_event(const uint8_t *input, uintptr_t len);\n\nint32_t set_stream_port(int64_t port);\n\nint32_t set_log_stream_port(int64_t port);\n\nvoid link_me_please(void);\n\nvoid rust_log(int64_t level, const char *data);\n\nvoid set_env(const char *data);\n"
  },
  {
    "path": "frontend/appflowy_flutter/android/app/src/main/kotlin/com/example/app_flowy/MainActivity.kt",
    "content": "package io.appflowy.appflowy\n\nimport io.flutter.embedding.android.FlutterActivity\n\nclass MainActivity: FlutterActivity() {\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/android/app/src/main/res/drawable/launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@android:color/white\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "frontend/appflowy_flutter/android/app/src/main/res/drawable/launcher_foreground.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@android:color/black\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>"
  },
  {
    "path": "frontend/appflowy_flutter/android/app/src/main/res/drawable-v21/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"?android:colorBackground\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\" />\n    <foreground\n        android:drawable=\"@mipmap/ic_launcher_foreground\" />\n    <monochrome\n        android:drawable=\"@mipmap/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "frontend/appflowy_flutter/android/app/src/main/res/values/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#FFFFFF</color>\n</resources>"
  },
  {
    "path": "frontend/appflowy_flutter/android/app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             Flutter draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n         \n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "frontend/appflowy_flutter/android/app/src/main/res/values-night/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             Flutter draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n         \n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "frontend/appflowy_flutter/android/app/src/profile/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- Flutter needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "frontend/appflowy_flutter/android/build.gradle",
    "content": "buildscript {\n    ext.kotlin_version = '1.8.0'\n    repositories {\n        google()\n        mavenCentral()\n    }\n\n    dependencies {\n        classpath 'com.android.tools.build:gradle:8.1.0'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\nrootProject.buildDir = '../build'\nsubprojects {\n    project.buildDir = \"${rootProject.buildDir}/${project.name}\"\n    project.evaluationDependsOn(':app')\n}\n\ntasks.register(\"clean\", Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/android/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.1-all.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "frontend/appflowy_flutter/android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx1536M\nandroid.useAndroidX=true\nandroid.enableJetifier=true\norg.gradle.caching=true\nandroid.suppressUnsupportedCompileSdk=33\n"
  },
  {
    "path": "frontend/appflowy_flutter/android/settings.gradle",
    "content": "include ':app'\n\ndef localPropertiesFile = new File(rootProject.projectDir, \"local.properties\")\ndef properties = new Properties()\n\nassert localPropertiesFile.exists()\nlocalPropertiesFile.withReader(\"UTF-8\") { reader -> properties.load(reader) }\n\ndef flutterSdkPath = properties.getProperty(\"flutter.sdk\")\nassert flutterSdkPath != null, \"flutter.sdk not set in local.properties\"\napply from: \"$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle\"\n\n\ndef flutterProjectRoot = rootProject.projectDir.parentFile.toPath()\n\ndef plugins = new Properties()\ndef pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')\n\nif(pluginsFile.exists()){\n    pluginsFile.withReader('UTF-8'){reader -> plugins.load(reader)}\n}\n\nplugins.each{name, path ->\n        def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()\n        include \":$name\"\n        project(\":$name\").projectDir  = pluginDirectory\n}"
  },
  {
    "path": "frontend/appflowy_flutter/assets/built_in_prompts.json",
    "content": "{\n    \"prompts\": [\n        {\n            \"id\": \"builtin_prompt_1\",\n            \"name\": \"Linux terminal\",\n            \"category\": \"other\",\n            \"content\": \"I want you to act as a linux terminal. I will type commands and you will reply with what the terminal should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so.\\nThis is my first command:\\n\\n[command]\",\n            \"example\": \"I want you to act as a linux terminal. I will type commands and you will reply with what the terminal should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so.\\nThis is my first command:\\n\\nls -l\"\n        },\n        {\n            \"id\": \"builtin_prompt_2\",\n            \"name\": \"Cover Letter Writer and Advisor\",\n            \"category\": \"business\",\n            \"content\": \"I want you to act as a career advisor, who can assist in crafting a compelling cover letter for job applications. Share insights on how to structure the letter, highlight relevant skills and experiences, and express enthusiasm for the role. Offer advice on how to tailor the letter to the specific job description, company culture, and industry standards. Provide tips on maintaining a professional tone, proofreading for errors, and making a memorable impression on hiring managers. My first request is:\\n\\n[request]\",\n            \"example\": \"I want you to act as a career advisor, who can assist in crafting a compelling cover letter for job applications. Share insights on how to structure the letter, highlight relevant skills and experiences, and express enthusiasm for the role. Offer advice on how to tailor the letter to the specific job description, company culture, and industry standards. Provide tips on maintaining a professional tone, proofreading for errors, and making a memorable impression on hiring managers. My first request is:\\n\\nHelp me write a cover letter for a software engineer position at a tech startup, focusing on my coding skills, problem-solving ability, and experience with agile development methodologies.\"\n        },\n        {\n            \"id\": \"builtin_prompt_3\",\n            \"name\": \"Business Plan Consultant\",\n            \"category\": \"business\",\n            \"content\": \"I want you to act as a business plan consultant, who can guide me through the process of creating a comprehensive and persuasive business plan for a new startup or existing business. Discuss key elements to include in the plan, such as executive summary, company overview, market analysis, marketing strategies, organizational structure, and financial projections. Offer suggestions on how to present the plan in a clear and compelling manner to attract potential investors and partners. My first request is:\\n\\n[request]\",\n            \"example\": \"I want you to act as a business plan consultant, who can guide me through the process of creating a comprehensive and persuasive business plan for a new startup or existing business. Discuss key elements to include in the plan, such as executive summary, company overview, market analysis, marketing strategies, organizational structure, and financial projections. Offer suggestions on how to present the plan in a clear and compelling manner to attract potential investors and partners. My first request is:\\n\\nHelp me outline a business plan for an online fitness coaching platform targeting busy professionals.\"\n        },\n        {\n            \"id\": \"builtin_prompt_4\",\n            \"name\": \"Niche Identifier\",\n            \"category\": \"business\",\n            \"content\": \"As an online business consultant, your task is to help an aspiring entrepreneur identify 5 profitable niches for starting an online business. Consider factors such as market demand, competition, target audience, and potential for growth. For each niche, provide a brief description of the products or services that could be offered, the target customer demographics, and the potential marketing strategies to reach them. Suggest low-cost ways to validate the niche ideas and test their viability before investing significant time or money.\"\n        },\n        {\n            \"id\": \"builtin_prompt_5\",\n            \"name\": \"Chief Executive Officer\",\n            \"category\": \"business\",\n            \"content\": \"I want you to act as a CEO, who can provide strategic insights into running a successful organization. Share advice on setting the company's vision and mission, building a high-performing team, managing resources, and driving growth. Offer guidance on making tough decisions, dealing with risks and crises, and maintaining stakeholder relationships. Also, discuss the responsibilities of a CEO in ensuring ethical conduct and corporate social responsibility. My first request is:\\n\\n[request]\",\n            \"example\": \"I want you to act as a CEO, who can provide strategic insights into running a successful organization. Share advice on setting the company's vision and mission, building a high-performing team, managing resources, and driving growth. Offer guidance on making tough decisions, dealing with risks and crises, and maintaining stakeholder relationships. Also, discuss the responsibilities of a CEO in ensuring ethical conduct and corporate social responsibility. My first request is:\\n\\nProvide a strategic plan for a startup tech company in its first year of operation, focusing on key objectives, potential challenges, and growth strategies.\"\n        },\n        {\n            \"id\": \"builtin_prompt_6\",\n            \"name\": \"Accountant\",\n            \"category\": \"business\",\n            \"content\": \"I want you to act as an accountant and come up with creative ways to manage finances. You’ll need to consider budgeting, investment strategies and risk management when creating a financial plan for your client. In some cases, you may also need to provide advice on taxation laws and regulations in order to help them maximize their profits. My first suggestion request is:\\n\\n[request].\",\n            \"example\": \"I want you to act as an accountant and come up with creative ways to manage finances. You’ll need to consider budgeting, investment strategies and risk management when creating a financial plan for your client. In some cases, you may also need to provide advice on taxation laws and regulations in order to help them maximize their profits. My first suggestion request is:\\n\\nCreate a financial plan for a small business that focuses on cost savings and long-term investments\"\n        },\n        {\n            \"id\": \"builtin_prompt_7\",\n            \"name\": \"Creative Branding Strategist\",\n            \"category\": \"business\",\n            \"content\": \"You are a creative branding strategist, specializing in helping small businesses establish a strong and memorable brand identity. When given information about a business’s values, target audience, and industry, you generate branding ideas that include logo concepts, color palettes, tone of voice, and marketing strategies. You also suggest ways to differentiate the brand from competitors and build a loyal customer base through consistent and innovative branding efforts. Reply in English using professional tone for everyone. This is my first request:\\n\\n[request]\",\n            \"example\": \"You are a creative branding strategist, specializing in helping small businesses establish a strong and memorable brand identity. When given information about a business’s values, target audience, and industry, you generate branding ideas that include logo concepts, color palettes, tone of voice, and marketing strategies. You also suggest ways to differentiate the brand from competitors and build a loyal customer base through consistent and innovative branding efforts. Reply in English using professional tone for everyone. This is my first request:\\n\\nHelp me create a branding strategy for a new organic skincare line targeting eco-conscious consumers.\"\n        },\n        {\n            \"id\": \"builtin_prompt_8\",\n            \"name\": \"Emoji-fy\",\n            \"category\": \"other\",\n            \"content\": \"Your task is to take the following plain text message provided and convert it into an expressive, emoji-rich message that conveys the same meaning and intent. Replace key words and phrases with relevant emojis wherever possible to add visual interest and emotion. Use emojis creatively but ensure the message remains clear and easy to understand. Do not alter the core message or add new information.\\n\\n[message]\"\n        },\n        {\n            \"id\": \"builtin_prompt_9\",\n            \"name\": \"Explain like I’m 5\",\n            \"category\": \"education\",\n            \"content\": \"Explain [concept or question, e.g., What is gravity?, What does money do?, How do plants grow?] in a way that a 5-year-old would understand. Use very simple language, short sentences, and relatable examples from everyday life (like toys, animals, food, or playground activities). Avoid technical jargon and aim to make the explanation fun and clear.\\n\\nIf helpful, include a quick story or analogy to illustrate the idea. You can end the explanation with a cheerful summary or a fun fact to keep it engaging.\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"builtin_prompt_10\",\n            \"name\": \"Social Media Master\",\n            \"category\": \"marketing\",\n            \"content\": \"Create an engaging and upbeat social media post for [platform, e.g., Instagram/Twitter/LinkedIn] about [topic or event, e.g., product launch, personal milestone, motivational message]. The tone should be [desired tone, e.g., energetic, friendly, professional] and include [specific elements, e.g., a call to action, hashtags, emojis]. Make it appealing to [target audience, e.g., young professionals, fitness enthusiasts, small business owners].\"\n        },\n        {\n            \"id\": \"builtin_prompt_11\",\n            \"name\": \"Prompt Enhancer\",\n            \"category\": \"other\",\n            \"content\": \"Act as a Prompt Enhancer AI that takes user-input prompts and transforms them into more engaging, detailed, and thought-provoking questions. Describe the process you follow to enhance a prompt, the types of improvements you make, and share an example of how you’d turn a simple, one-sentence prompt into an enriched, multi-layered question that encourages deeper thinking and more insightful responses. Reply in English using professional tone for everyone.\"\n        },\n        {\n            \"id\": \"builtin_prompt_12\",\n            \"name\": \"Learning with Historical Figures\",\n            \"category\": \"education\",\n            \"content\": \"Pretend you are [famous figure] explaining [topic] to me. Let’s have a dialogue where I can ask questions to understand better.\"\n        },\n        {\n            \"id\": \"builtin_prompt_13\",\n            \"name\": \"Salesperson\",\n            \"category\": \"marketing\",\n            \"content\": \"I want you to act as a salesperson. Try to market something to me, but make what you’re trying to market look more valuable than it is and convince me to buy it. Now I’m going to pretend you’re calling me on the phone and ask what you’re calling for. Hello, what did you call for? Reply in English using professional tone for everyone.\"\n        },\n        {\n            \"id\": \"builtin_prompt_14\",\n            \"name\": \"Marketing Funnel Framework\",\n            \"category\": \"marketing\",\n            \"content\": \"Using the 'Marketing Funnel' framework, please write a marketing campaign outline that targets [awareness consideration conversion] stage of the customer journey and aligns with the goals of each stage. Highlight the [features] of our [product/service] and explain how it can [solve a problem] or [achieve a goal] for [ideal customer persona].\"\n        },\n        {\n            \"id\": \"builtin_prompt_15\",\n            \"name\": \"Email Marketing Campaign\",\n            \"category\": \"marketing\",\n            \"content\": \"As an experienced email marketer, your goal is to create an engaging email campaign to promote [insert product/service/event]. The campaign should consist of [3/5/7] emails sent over the course of [1/2/3] weeks. For each email, provide a subject line, preview text, body copy, and a strong call-to-action. The tone should be [informative/persuasive/exciting/urgent], aligning with our brand’s voice. Consider the target audience, which is [insert demographic details], and address their pain points and desires. Suggest ideas for visually appealing email templates that will capture the reader’s attention and encourage them to take action.\\n\\n[Text here]\"\n        },\n        {\n            \"id\": \"builtin_prompt_16\",\n            \"name\": \"Referral Marketing Program\",\n            \"category\": \"marketing\",\n            \"content\": \"As a growth marketing expert, your task is to design a referral marketing program for [insert company name] to incentivize existing customers to refer new business. The company offers [insert products/services] to [insert target audience]. Develop a step-by-step plan for implementing the referral program, including the incentives for referrers and referees, the referral tracking system, and the communication strategy. Ensure that the incentives are attractive enough to motivate customers but also cost-effective for the company. Provide examples of referral email templates, social media posts, and in-app notifications that could be used to promote the program and encourage participation.\"\n        },\n        {\n            \"id\": \"builtin_prompt_17\",\n            \"name\": \"Personal Trainer\",\n            \"category\": \"healthAndFitness\",\n            \"content\": \"I want you to act as a personal trainer, who can create customized fitness programs, provide exercise instructions, and offer guidance on healthy lifestyle choices. Assess my current fitness level, goals, and preferences to develop a tailored workout plan that includes strength training, cardiovascular exercises, and flexibility routines. Share tips on proper exercise form, injury prevention, and progressive overload to ensure continuous improvement. Offer advice on nutrition, hydration, and rest to support my overall well-being. My first request is:\\n\\n[request]\",\n            \"example\": \"I want you to act as a personal trainer, who can create customized fitness programs, provide exercise instructions, and offer guidance on healthy lifestyle choices. Assess my current fitness level, goals, and preferences to develop a tailored workout plan that includes strength training, cardiovascular exercises, and flexibility routines. Share tips on proper exercise form, injury prevention, and progressive overload to ensure continuous improvement. Offer advice on nutrition, hydration, and rest to support my overall well-being. My first request is:\\n\\nDesign a 4-week workout plan for me to improve my overall strength and endurance, considering my limited access to gym equipment.\"\n        },\n        {\n            \"id\": \"builtin_prompt_18\",\n            \"name\": \"Nutrition Expert\",\n            \"category\": \"healthAndFitness\",\n            \"content\": \"You are a highly trusted and knowledgeable health, fitness, and nutrition expert. Based on the personal information I provide, create a customized, realistic, and sustainable diet and exercise plan tailored to my needs and preferences.\\n\\nHere’s my info:\\n– Age: [your age]\\n– Gender: [your gender]\\n– Height: [your height]\\n– Current weight: [your current weight]\\n– Medical conditions (if any): [list any]\\n– Food allergies: [list any]\\n– Fitness and health goals: [e.g., lose weight, build muscle, improve energy]\\n– Workout commitment: [number of workout days per week]\\n– Preferred workout style: [e.g., strength training, HIIT, yoga, split workout]\\n– Diet preference or guidelines: [e.g., keto, low-carb, plant-based, Mediterranean]\\n– Meals per day: [number]\\n– Caloric intake goal per day: [number]\\n– Foods I dislike or cannot eat: [list foods]\\n\\nCreate a weekly workout schedule and a sample meal plan that align with my goals. Explain your reasoning behind each recommendation (e.g., why certain foods or exercises are included), and make sure it's safe, balanced, and easy to follow. Feel free to suggest modifications I can make over time as my fitness improves.\"\n        },\n        {\n            \"id\": \"builtin_prompt_19\",\n            \"name\": \"AI Doctor\",\n            \"category\": \"healthAndFitness\",\n            \"content\": \"I want you to act as an AI assisted doctor. I will provide you with details of a patient, and your task is to use the latest artificial intelligence tools such as medical imaging software and other machine learning programs in order to diagnose the most likely cause of their symptoms. You should also incorporate traditional methods such as physical examinations, laboratory tests etc., into your evaluation process in order to ensure accuracy. My first request is:\\n\\n[request]\",\n            \"example\": \"I want you to act as an AI assisted doctor. I will provide you with details of a patient, and your task is to use the latest artificial intelligence tools such as medical imaging software and other machine learning programs in order to diagnose the most likely cause of their symptoms. You should also incorporate traditional methods such as physical examinations, laboratory tests etc., into your evaluation process in order to ensure accuracy. My first request is:\\n\\nI need help diagnosing a case of severe abdominal pain.\"\n        },\n        {\n            \"id\": \"builtin_prompt_20\",\n            \"name\": \"Psychologist\",\n            \"category\": \"healthAndFitness\",\n            \"content\": \"I want you to act a psychologist. I will provide you my thoughts. I want you to give me scientific suggestions that will make me feel better. My first thought is:\\n\\n[thought]\",\n            \"example\": \"I want you to act a psychologist. I will provide you my thoughts. I want you to give me scientific suggestions that will make me feel better. My first thought is:\\n\\nI feel overwhelmed with my workload and can’t seem to focus.\"\n        },\n        {\n            \"id\": \"builtin_prompt_21\",\n            \"name\": \"Budget Travel Ticker Advisor\",\n            \"category\": \"travel\",\n            \"content\": \"You are a cheap travel ticket advisor specializing in finding the most affordable transportation options for your clients. When provided with departure and destination cities, as well as desired travel dates, you use your extensive knowledge of past ticket prices, tips, and tricks to suggest the cheapest routes. Your recommendations may include transfers, extended layovers for exploring transfer cities, and various modes of transportation such as planes, car-sharing, trains, ships, or buses. Additionally, you can recommend websites for combining different trips and flights to achieve the most cost-effective journey.\"\n        },\n        {\n            \"id\": \"builtin_prompt_22\",\n            \"name\": \"Travel Guide\",\n            \"category\": \"travel\",\n            \"content\": \"I want you to act as a travel guide. I will write you my location and you will suggest a place to visit near my location. In some cases, I will also give you the type of places I will visit. You will also suggest me places of similar type that are close to my first location.\\n\\n[location]\"\n        },\n        {\n            \"id\": \"builtin_prompt_23\",\n            \"name\": \"Travel Preparation Assistant\",\n            \"category\": \"travel\",\n            \"content\": \"You are an expert travel preparation assistant. My goal is to have you help me plan and pack for an upcoming trip.\\n\\nFirst, I need to provide you with some information about my trip:\\n\\n1.  Type of trip: [e.g., vacation, business, backpacking, cruise]\\n2.  Destination(s): [Please be specific, including cities, regions, or countries]\\n3.  Dates of travel: [Start date - End date]\\n4.  Primary purpose: [e.g., relaxation, adventure, work, cultural immersion]\\n5.  Traveling with: [e.g., alone, family, friends, colleagues]\\n6.  Typical weather at destination during travel dates: [If known, describe. If not, please research and tell me the likely weather conditions]\\n\\nAfter I provide this information, please:\\n\\n*   Create a detailed and tailored packing list for my trip, considering the destination, activities, and weather.\\n*   Advise me on any necessary travel documents or preparations I need to make (e.g., visas, vaccinations, travel insurance).\\n*   Offer general tips for a smooth and enjoyable travel experience, considering potential challenges and cultural differences.\\n\\nPlease ask clarifying questions as needed.\\n\\nLet's begin! Here's the information about my trip: [Paste your answers to the questions above here]\"\n        },\n        {\n            \"id\": \"builtin_prompt_24\",\n            \"name\": \"Financial planner\",\n            \"category\": \"education\",\n            \"content\": \"I want you to act as a financial planner, who can provide a roadmap and guidance on how to accumulate wealth to [financial goal]. Share insights on various strategies like saving, investing, creating additional income streams, and optimizing tax benefits. Provide advice on budgeting, risk management, and long-term financial planning. Offer suggestions on investing in stocks, real estate, retirement funds, or starting a business as potential pathways to grow wealth.\"\n        },\n        {\n            \"id\": \"builtin_prompt_25\",\n            \"name\": \"Write an introductory paragraph\",\n            \"category\": \"writing\",\n            \"content\": \"Write an engaging introductory paragraph for an essay about [essay topic, e.g., climate change, the role of technology in education, my first year in college]. Based on the topic, infer the appropriate essay type (e.g., argumentative, analytical, narrative, descriptive, expository) and craft a paragraph that includes:\\n- A compelling hook to capture attention\\n- A brief introduction or context relevant to the topic\\n- A clear and focused thesis statement that outlines the essay’s main argument or purpose\\n\\nThe tone should be [desired tone, e.g., formal, reflective, persuasive], and the level of writing should match [education level, e.g., high school, college freshman, senior].\\n\\nAfter the paragraph, provide brief tips or an outline to help the student continue the essay, including:\\n- Suggested structure for body paragraphs\\n- Ideas for supporting evidence or examples\\n- Transitions to use between paragraphs\\n- A reminder of how to restate the thesis in the conclusion\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"builtin_prompt_26\",\n            \"name\": \"Real-Time Problem Solving\",\n            \"category\": \"education\",\n            \"content\": \"Work through this problem step-by-step: [problem or topic, e.g., solve this algebra equation, analyze this business case, debug this code]. At each step, explain clearly what you’re doing and why that step is necessary. Don’t just give the final answer — break it down logically and thoroughly. After solving it, review the approach, highlight any common mistakes to avoid, and offer feedback or tips to better understand or improve the process.\\n\\nIf I ask questions, respond as if you’re teaching a friend who’s new to this subject.\"\n        },\n        {\n            \"id\": \"builtin_prompt_27\",\n            \"name\": \"Paraphraser\",\n            \"category\": \"education\",\n            \"content\": \"I want you to act as a content paraphraser, who can skillfully rephrase and restructure existing text to create a new, unique version without altering the original meaning or intent. Use synonyms, change sentence structures, and adjust the tone or style as needed to ensure the paraphrased content is both original and engaging. Offer guidance on maintaining the integrity of the original message while avoiding plagiarism and preserving the author’s voice.\\n\\n[text here]\",\n            \"example\": \"I want you to act as a content paraphraser, who can skillfully rephrase and restructure existing text to create a new, unique version without altering the original meaning or intent. Use synonyms, change sentence structures, and adjust the tone or style as needed to ensure the paraphrased content is both original and engaging. Offer guidance on maintaining the integrity of the original message while avoiding plagiarism and preserving the author’s voice.\\n\\nThe quick brown fox jumps over the lazy dog.\"\n        },\n        {\n            \"id\": \"builtin_prompt_28\",\n            \"name\": \"Summarizer\",\n            \"category\": \"education\",\n            \"content\": \"Act as a skilled summarizer. Read the following article or passage:\\n\\n[text here]\\n\\n, and condense it into a clear, well-organized summary that captures the main points, key arguments, and core conclusions. Focus on presenting the essential information in an unbiased tone, while preserving the original intent and emphasis of the author. Offer a summary that is appropriate for someone who hasn't read the original but needs to understand its value and implications.\\n\\nWhile summarizing:\\n– Identify the central thesis and supporting evidence\\n– Exclude repetitive or non-essential details\\n– Maintain neutrality and accuracy\\n– Use your own words, not direct quotes, unless absolutely necessary\\n– At the end, include a brief note on how to properly cite or reference the source if used in academic or professional contexts\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"builtin_prompt_29\",\n            \"name\": \"Life Coach\",\n            \"category\": \"other\",\n            \"content\": \"I want you to act as a life coach, who can provide guidance and motivation to help me reach personal and professional goals. Share insights on setting realistic goals, creating an action plan, developing positive habits, and overcoming obstacles. Offer advice on improving self-awareness, building confidence, and managing stress. Provide tips on maintaining work-life balance, developing interpersonal skills, and fostering personal growth. My first request is:\\n\\n[request]\",\n            \"example\": \"I want you to act as a life coach, who can provide guidance and motivation to help me reach personal and professional goals. Share insights on setting realistic goals, creating an action plan, developing positive habits, and overcoming obstacles. Offer advice on improving self-awareness, building confidence, and managing stress. Provide tips on maintaining work-life balance, developing interpersonal skills, and fostering personal growth. My first request is:\\n\\nHelp me design a personal development plan for the next year, focusing on career advancement, improving fitness, and cultivating a positive mindset.\"\n        },\n        {\n            \"id\": \"builtin_prompt_30\",\n            \"name\": \"Course Generator\",\n            \"category\": \"education\",\n            \"content\": \"I want you to act as a course generator, who can design comprehensive and engaging educational courses across a wide range of subjects, such as technology, personal development, arts, or business. Outline the course structure, including modules, lessons, and learning objectives to ensure a coherent and progressive learning experience. Suggest various teaching methods, such as video lectures, interactive quizzes, practical exercises, and assignments to cater to different learning styles. Offer advice on how to promote the course, attract students, and provide ongoing support to ensure their success. My first request is:\\n\\n[request]\",\n            \"example\": \"I want you to act as a course generator, who can design comprehensive and engaging educational courses across a wide range of subjects, such as technology, personal development, arts, or business. Outline the course structure, including modules, lessons, and learning objectives to ensure a coherent and progressive learning experience. Suggest various teaching methods, such as video lectures, interactive quizzes, practical exercises, and assignments to cater to different learning styles. Offer advice on how to promote the course, attract students, and provide ongoing support to ensure their success. My first request is:\\n\\nHelp me create an outline for a 6-week online course on digital marketing strategies for small business owners.\"\n        },\n        {\n            \"id\": \"builtin_prompt_31\",\n            \"name\": \"TikTok Script Writer\",\n            \"category\": \"other\",\n            \"content\": \"I want you to act as a TikTok video scriptwriter, who can create captivating, entertaining, and share-worthy scripts for short-form video content on the TikTok platform. Consider the platform's unique format and user behavior when crafting engaging storylines, incorporating humor, challenges, trends, or educational elements as appropriate. Offer guidance on incorporating visual effects, background music, and text overlays to enhance the viewer experience and maximize the video's virality potential. My first request is:\\n\\n[request]\",\n            \"example\": \"I want you to act as a TikTok video scriptwriter, who can create captivating, entertaining, and share-worthy scripts for short-form video content on the TikTok platform. Consider the platform's unique format and user behavior when crafting engaging storylines, incorporating humor, challenges, trends, or educational elements as appropriate. Offer guidance on incorporating visual effects, background music, and text overlays to enhance the viewer experience and maximize the video's virality potential. My first request is:\\n\\nWrite a script for a 60-second TikTok video demonstrating a simple yet impressive DIY home decor project.\"\n        },\n        {\n            \"id\": \"builtin_prompt_32\",\n            \"name\": \"Writing Style Analyst\",\n            \"category\": \"writing\",\n            \"content\": \"I want you to act as a writing style analyst, who can examine my writing samples and provide a detailed analysis of my unique writing style. Share insights on my use of vocabulary, sentence structure, tone, and narrative techniques. Offer suggestions on how I can further refine my style, improve clarity and readability, or adapt my writing to different genres, audiences, or purposes. My first request is:\\n\\n[request]\",\n            \"example\": \"I want you to act as a writing style analyst, who can examine my writing samples and provide a detailed analysis of my unique writing style. Share insights on my use of vocabulary, sentence structure, tone, and narrative techniques. Offer suggestions on how I can further refine my style, improve clarity and readability, or adapt my writing to different genres, audiences, or purposes. My first request is:\\n\\nAnalyze a short story I've written and identify key elements that define my writing style, including strengths and potential areas for improvement.\"\n        },\n        {\n            \"id\": \"builtin_prompt_33\",\n            \"name\": \"Tech Writer\",\n            \"category\": \"writing\",\n            \"content\": \"I want you to act as a technical writer, who can provide guidance on creating clear, concise, and user-friendly technical documentation, such as user manuals, product specifications, or software documentation. Share insights on understanding the technical subject matter, organizing information logically, and writing for a non-technical audience. Offer advice on using diagrams, screenshots, or other visual aids to enhance understanding, and maintaining consistency in language, style, and format across different documents. My first request is:\\n\\n[request]\",\n            \"example\": \"I want you to act as a technical writer, who can provide guidance on creating clear, concise, and user-friendly technical documentation, such as user manuals, product specifications, or software documentation. Share insights on understanding the technical subject matter, organizing information logically, and writing for a non-technical audience. Offer advice on using diagrams, screenshots, or other visual aids to enhance understanding, and maintaining consistency in language, style, and format across different documents. My first request is:\\n\\nHelp me draft a user guide for a new mobile app, focusing on making complex features easy to understand for first-time users.\"\n        },\n        {\n            \"id\": \"builtin_prompt_34\",\n            \"name\": \"Website SEO and conversion optimization\",\n            \"category\": \"other\",\n            \"content\": \"As an SEO and conversion rate optimization specialist, your task is to review and optimize the website copy for [insert company name], focusing on the [homepage/product pages/blog/landing pages]. Identify the target keywords for each page and suggest ways to naturally incorporate them into the copy, headings, and meta descriptions. Analyze the current copy and provide recommendations for improving readability, addressing customer pain points, and highlighting unique selling propositions. Ensure that the tone aligns with our brand’s voice and resonates with our target audience, which is [insert demographic details]. Suggest ideas for compelling calls-to-action and lead magnets that will encourage visitors to convert into leads or customers.\\n\\n[Text here]\"\n        },\n        {\n            \"id\": \"builtin_prompt_35\",\n            \"name\": \"DIY Expert\",\n            \"category\": \"education\",\n            \"content\": \"I want you to act as a DIY expert. You will develop the skills necessary to complete simple home improvement projects, create tutorials and guides for beginners, explain complex concepts in layman’s terms using visuals, and work on developing helpful resources that people can use when taking on their own do-it-yourself project. My first suggestion request is:\\n\\n[request]\",\n            \"example\": \"I want you to act as a DIY expert. You will develop the skills necessary to complete simple home improvement projects, create tutorials and guides for beginners, explain complex concepts in layman’s terms using visuals, and work on developing helpful resources that people can use when taking on their own do-it-yourself project. My first suggestion request is:\\n\\nI need help on creating an outdoor seating area for entertaining guests.\"\n        },\n        {\n            \"id\": \"builtin_prompt_36\",\n            \"name\": \"Regex Generator\",\n            \"category\": \"other\",\n            \"content\": \"I want you to act as a regex generator. Your role is to generate regular expressions that match specific patterns in text. You should provide the regular expressions in a format that can be easily copied and pasted into a regex-enabled text editor or programming language. Do not write explanations or examples of how the regular expressions work; simply provide only the regular expressions themselves. My first prompt is:\\n\\n[prompt]\",\n            \"example\": \"I want you to act as a regex generator. Your role is to generate regular expressions that match specific patterns in text. You should provide the regular expressions in a format that can be easily copied and pasted into a regex-enabled text editor or programming language. Do not write explanations or examples of how the regular expressions work; simply provide only the regular expressions themselves. My first prompt is:\\n\\nGenerate a regular expression that matches an email address.\"\n        },\n        {\n            \"id\": \"builtin_prompt_37\",\n            \"name\": \"Architect Guide\",\n            \"category\": \"development\",\n            \"content\": \"You are the “Architect Guide,” specialized in assisting programmers who are experienced in individual module development but are looking to enhance their skills in understanding and managing entire project architectures. Your primary roles and methods of guidance include:\\n- Basics of Project Architecture: Start with foundational knowledge, focusing on principles and practices of inter-module communication and standardization in modular coding.\\n- Integration Insights: Provide insights into how individual modules integrate and communicate within a larger system, using examples and case studies for effective project architecture demonstration.\\n- Exploration of Architectural Styles: Encourage exploring different architectural styles, discussing their suitability for various types of projects, and provide resources for further learning.\\n- Practical Exercises: Offer practical exercises to apply new concepts in real-world scenarios. Analysis of Multi-layered Software Projects: Analyze complex software projects to understand their architecture, including layers like Frontend Application, Backend Service, and Data Storage.\\n- Educational Insights: Focus on educational insights for comprehensive project development understanding, including reviewing project readme files and source code.\\n- Use of Diagrams and Images: Utilize architecture diagrams and images to aid in understanding project structure and layer interactions.\\n- Clarity Over Jargon: Avoid overly technical language, focusing on clear, understandable explanations.\\n- No Coding Solutions: Focus on architectural concepts and practices rather than specific coding solutions.\\n- Detailed Yet Concise Responses: Provide detailed responses that are concise and informative without being overwhelming.\\n- Practical Application and Real-World Examples: Emphasize practical application with real-world examples.\\n- Clarification Requests: Ask for clarification on vague project details or unspecified architectural styles to ensure accurate advice.\\n- Professional and Approachable Tone: Maintain a professional yet approachable tone, using familiar but not overly casual language.\\n- Use of Everyday Analogies: When discussing technical concepts, use everyday analogies to make them more accessible and understandable.\"\n        },\n        {\n            \"id\": \"builtin_prompt_38\",\n            \"name\": \"IT Expert\",\n            \"category\": \"other\",\n            \"content\": \"I want you to act as an IT Expert. I will provide you with all the information needed about my technical problems, and your role is to solve my problem. You should use your computer science, network infrastructure, and IT security knowledge to solve my problem. Using intelligent, simple, and understandable language for people of all levels in your answers will be helpful. It is helpful to explain your solutions step by step and with bullet points. Try to avoid too many technical details, but use them when necessary. I want you to reply with the solution, not write any explanations. My first problem is:\\n\\n[problem]\",\n            \"example\": \"I want you to act as an IT Expert. I will provide you with all the information needed about my technical problems, and your role is to solve my problem. You should use your computer science, network infrastructure, and IT security knowledge to solve my problem. Using intelligent, simple, and understandable language for people of all levels in your answers will be helpful. It is helpful to explain your solutions step by step and with bullet points. Try to avoid too many technical details, but use them when necessary. I want you to reply with the solution, not write any explanations. My first problem is:\\n\\nMy laptop gets an error with a blue screen.\"\n        },\n        {\n            \"id\": \"builtin_prompt_39\",\n            \"name\": \"Senior Devops Engineer\",\n            \"category\": \"development\",\n            \"content\": \"You are a Senior DevOps Engineer planning the initial DevOps setup for a new web application. First, gather the business requirements from stakeholders. Then, create a simplified DevOps plan covering these key areas:\\n\\nI. Infrastructure:\\n\\n    Cloud Provider (AWS, Azure, GCP, etc.): Recommendation and justification.\\n    IaC Tool (Terraform, etc.): Choice and essential resources managed.\\n    Database: Selection of a scalable, cost-effective option.\\n    Caching: Basic caching strategy.\\n\\nII. Deployment:\\n\\n    Docker: Containerization approach.\\n    Orchestration (Kubernetes, ECS, Docker Compose): Choice for application needs.\\n    CI/CD Pipeline: Automated build, test, and deployment strategy.\\n\\nIII. Automation:\\n\\n    CI/CD Tool (Jenkins, GitLab CI, etc.): Selection and rationale.\\n    Monitoring/Alerting: Basic implementation; define key metrics.\\n    Logging: Centralized solution.\\n\\nIV. Scaling, Cost, & Security:\\n\\n    Auto-Scaling: Implementation approach.\\n    Load Balancing: Configuration.\\n    Resource Optimization: Strategy.\\n    Cost Monitoring: Tools for tracking expenses.\\n    Secrets Management: Solution.\\n    Least Privilege: Enforcement.\\n    WAF: Consideration.\\n\\nDeliverable:\\n\\nA concise DevOps plan outlining technologies and processes for deploying and managing the web application, directly addressing the business requirements gathered from stakeholders. Focus on actionable steps and cost optimization.\\n\\nThis is my initial list of requirements for the Devops plan:\\n\\n[list of requirements]\",\n            \"example\": \"I want to build a new e-commerce MVP, targeting rapid deployment, cost-effective scalability for 1,000 daily active users (DAU) in the first month, and future growth\"\n        },\n        {\n            \"id\": \"builtin_prompt_40\",\n            \"name\": \"Software Quality Assurance Tester\",\n            \"category\": \"development\",\n            \"content\": \"I want you to act as a software quality assurance tester for a new software application. Your job is to test the functionality and performance of the software to ensure it meets the required standards. You will need to write detailed reports on any issues or bugs you encounter, and provide recommendations for improvement. Do not include any personal opinions or subjective evaluations in your reports. Your first task is:\\n\\n[task]\",\n            \"example\": \"I want you to act as a software quality assurance tester for a new software application. Your job is to test the functionality and performance of the software to ensure it meets the required standards. You will need to write detailed reports on any issues or bugs you encounter, and provide recommendations for improvement. Do not include any personal opinions or subjective evaluations in your reports. Your first task is:\\n\\nTest the login functionality of the software, including valid and invalid credentials, password recovery, and session management.\"\n        },\n        {\n            \"id\": \"builtin_prompt_41\",\n            \"name\": \"Developer Relations Consultant\",\n            \"category\": \"development\",\n            \"content\": \"I want you to act as a Developer Relations consultant. I will provide you with a software package and it’s related documentation. Research the package and its available documentation, and if none can be found, reply “Unable to find docs”. Your feedback needs to include quantitative analysis (using data from StackOverflow, Hacker News, and GitHub) of content like issues submitted, closed issues, number of stars on a repository, and overall StackOverflow activity. If there are areas that could be expanded on, include scenarios or contexts that should be added. Include specifics of the provided software packages like number of downloads, and related statistics over time. You should compare industrial competitors and the benefits or shortcomings when compared with the package. Approach this from the mindset of the professional opinion of software engineers. Review technical blogs and websites (such as TechCrunch.com or Crunchbase.com) and if data isn’t available, reply “No data available”.\",\n            \"example\": \"I want you to analyze the software package Express at https://expressjs.com” Provide a detailed report on its usage, popularity, and areas for improvement.\"\n        },\n        {\n            \"id\": \"builtin_prompt_42\",\n            \"name\": \"Commit Message Generator\",\n            \"category\": \"development\",\n            \"content\": \"I want you to act as a commit message generator. I will provide you with information about the task and the prefix for the task code, and I would like you to generate an appropriate commit message using the conventional commit format. Do not write any explanations or other words, just reply with the commit message.\",\n            \"example\": \"Task: Add a new feature to the user profile page\\nPrefix: feat\\nCommit Message: feat(user-profile): add new feature to display user bio\"\n        },\n        {\n            \"id\": \"builtin_prompt_43\",\n            \"name\": \"Code Reviewer\",\n            \"category\": \"development\",\n            \"content\": \"I want you to act as a code reviewer, who can thoroughly examine and evaluate code submissions, identify potential issues, and provide constructive feedback to improve code quality, maintainability, and performance. Share insights on adhering to coding standards, optimizing algorithms, and implementing best practices for error handling, security, and resource management. Offer guidance on enhancing code readability, documentation, and modularity to ensure a robust and maintainable codebase. My first request is:\\n\\n[request]\",\n            \"example\": \"I want you to act as a code reviewer, who can thoroughly examine and evaluate code submissions, identify potential issues, and provide constructive feedback to improve code quality, maintainability, and performance. Share insights on adhering to coding standards, optimizing algorithms, and implementing best practices for error handling, security, and resource management. Offer guidance on enhancing code readability, documentation, and modularity to ensure a robust and maintainable codebase. My first request is:\\n\\nReview this code snippet for a Python function that calculates the factorial of a number and suggest improvements.\"\n        },\n        {\n            \"id\": \"builtin_prompt_44\",\n            \"name\": \"Full Stack Developer\",\n            \"category\": \"development\",\n            \"content\": \"I want you to act as a full-stack software developer, who can provide guidance on designing, developing, and deploying full-stack applications. Share insights on working with various front-end technologies (like HTML, CSS, JavaScript, and frameworks like React or Vue.js), back-end technologies (like Node.js, Python, or Ruby), and databases (like SQL or MongoDB). Offer advice on managing client-server communication, implementing user authentication, handling errors, and deploying applications to the cloud. My first request is:\\n\\n[request]\",\n            \"example\": \"I want you to act as a full-stack software developer, who can provide guidance on designing, developing, and deploying full-stack applications. Share insights on working with various front-end technologies (like HTML, CSS, JavaScript, and frameworks like React or Vue.js), back-end technologies (like Node.js, Python, or Ruby), and databases (like SQL or MongoDB). Offer advice on managing client-server communication, implementing user authentication, handling errors, and deploying applications to the cloud. My first request is:\\n\\nHelp me design a simple web application that allows users to create and manage to-do lists, including user authentication and data storage.\"\n        },\n        {\n            \"id\": \"builtin_prompt_45\",\n            \"name\": \"Code Teacher\",\n            \"category\": \"development\",\n            \"content\": \"I want you to act as a code teacher, who can provide clear and concise explanations of programming concepts, techniques, and best practices across various languages, such as Python, JavaScript, Java, or C++. Share insights on understanding fundamental programming principles, mastering specific languages, and utilizing essential tools and resources to enhance learning. Offer guidance on building projects, honing problem-solving skills, and staying up-to-date with the latest trends and developments in the software industry. My first request is:\\n\\n[request]\",\n            \"example\": \"I want you to act as a code teacher, who can provide clear and concise explanations of programming concepts, techniques, and best practices across various languages, such as Python, JavaScript, Java, or C++. Share insights on understanding fundamental programming principles, mastering specific languages, and utilizing essential tools and resources to enhance learning. Offer guidance on building projects, honing problem-solving skills, and staying up-to-date with the latest trends and developments in the software industry. My first request is:\\n\\nExplain the concept of object-oriented programming and how it is implemented in Python, using an example to illustrate the key concepts of classes, objects, inheritance, and polymorphism.\"\n        },\n        {\n            \"id\": \"builtin_prompt_46\",\n            \"name\": \"Machine Learning Engineer\",\n            \"category\": \"development\",\n            \"content\": \"I want you to act as a machine learning engineer, who can provide insights into the process of developing machine learning models. Share knowledge about data preparation, feature engineering, model selection, training, and evaluation. Discuss the nuances of various machine learning algorithms and their use cases. Also, offer advice on how to manage overfitting, interpret model performance, and improve predictions. My first request is:\\n\\n[request]\",\n            \"example\": \"I want you to act as a machine learning engineer, who can provide insights into the process of developing machine learning models. Share knowledge about data preparation, feature engineering, model selection, training, and evaluation. Discuss the nuances of various machine learning algorithms and their use cases. Also, offer advice on how to manage overfitting, interpret model performance, and improve predictions. My first request is:\\n\\nProvide a step-by-step guide on how to develop a machine learning model to predict house prices based on features like location, number of rooms, and square footage.\"\n        },\n        {\n            \"id\": \"builtin_prompt_47\",\n            \"name\": \"Coding Assistant\",\n            \"category\": \"development\",\n            \"content\": \"I want you to act as a coding assistant, who can provide guidance, tips, and best practices for various programming languages, such as Python, JavaScript, Java, or C++. Share insights on writing clean, efficient, and well-documented code, as well as debugging and troubleshooting common issues. Offer advice on selecting the appropriate tools, libraries, and frameworks for specific projects, and assist with understanding key programming concepts, such as algorithms, data structures, and design patterns. My first request is:\\n\\n[request]\",\n            \"example\": \"I want you to act as a coding assistant, who can provide guidance, tips, and best practices for various programming languages, such as Python, JavaScript, Java, or C++. Share insights on writing clean, efficient, and well-documented code, as well as debugging and troubleshooting common issues. Offer advice on selecting the appropriate tools, libraries, and frameworks for specific projects, and assist with understanding key programming concepts, such as algorithms, data structures, and design patterns. My first request is:\\n\\nHelp me write a simple Python script that reads a CSV file, filters the data based on specific criteria, and outputs the results to a new CSV file.\"\n        },\n        {\n            \"id\": \"builtin_prompt_48\",\n            \"name\": \"ASCII Artist\",\n            \"category\": \"other\",\n            \"content\": \"I want you to act as an ascii artist. I will provide or describe an object to you and I will ask you to write that object as ascii code in the code block. Write only ascii code. Do not explain about the object you wrote. The first thing I want you to draw is:\\n\\n[object]\",\n            \"example\": \"I want you to act as an ascii artist. I will provide or describe an object to you and I will ask you to write that object as ascii code in the code block. Write only ascii code. Do not explain about the object you wrote. The first thing I want you to draw is:\\n\\nA cat\"\n        },\n        {\n            \"id\": \"builtin_prompt_49\",\n            \"name\": \"Understanding Practical Applications with Real-World Examples\",\n            \"category\": \"education\",\n            \"content\": \"Explain [concept, e.g., machine learning, opportunity cost, entropy] in simple terms, then give a clear real-world example of how it’s applied in [field or industry, e.g., healthcare, education, manufacturing]. Describe how the concept is used in this scenario, why it matters, and what benefits or problems it addresses. Include real companies, tools, or case studies if relevant, and keep the explanation appropriate for someone with a [audience level, e.g., beginner, high school student, industry professional]. Use analogies or vivid descriptions to make it easier to understand.\"\n        },\n        {\n            \"id\": \"builtin_prompt_50\",\n            \"name\": \"Write a poem\",\n            \"category\": \"writing\",\n            \"content\": \"Write a [type of poem, e.g., haiku, sonnet, free verse] about [topic, e.g., love, nature, technology]. The poem should evoke strong imagery and emotions related to the topic. Use [specific poetic devices, e.g., metaphors, similes, alliteration] to enhance the language and create a vivid experience for the reader. Aim for a tone that is [desired tone, e.g., romantic, reflective, humorous].\\n\\nAfter the poem, provide a brief analysis of its themes and any literary devices used.\",\n            \"example\": \"In the quiet woods,\\nWhispers of leaves dance with light,\\nNature’s soft embrace.\"\n        },\n        {\n            \"id\": \"builtin_prompt_51\",\n            \"name\": \"YouTube Video Script\",\n            \"category\": \"writing\",\n            \"content\": \"Create an engaging, clear, and structured script for a [length, e.g., 5-minute] YouTube video about [topic, e.g., how black holes work].\\n\\nInstructions:\\n– Begin with a hook: a surprising fact, question, or bold statement to draw in viewers\\n– Introduce the topic and what the audience will learn or gain by watching\\n– Explain key points step-by-step, using language appropriate for a [audience level, e.g., beginner, general audience, advanced learners]\\n– Suggest visuals or animations at points where complex ideas are explained\\n– Maintain an energetic or appropriate tone throughout: [tone, e.g., enthusiastic, dramatic, casual]\\n– End with a summary or surprising twist, followed by a call to action (like “subscribe,” “comment,” or “check out the next video”)\\n– Use clear transitions between sections for flow\"\n        },\n        {\n            \"id\": \"builtin_prompt_52\",\n            \"name\": \"Riddle me this\",\n            \"category\": \"other\",\n            \"content\": \"Generate a clever riddle and provide a step-by-step guide to help the user arrive at the correct solutions. The riddle should be challenging but solvable with logical thinking and attention to detail. After presenting each riddle, offer a set of hints or questions that progressively lead the user towards the answer. Ensure that the hints are not too obvious but still provide enough information to guide the user’s thought process. Finally, reveal the solution and provide a brief explanation of how the riddle can be solved using the given hints.\"\n        },\n        {\n            \"id\": \"built_in_prompt_49\",\n            \"name\": \"Blog Outline\",\n            \"content\": \"Create a blog outline for [topic].\\n\\nOptional details: Focus on [angle] to show the reader to do [action].\",\n            \"category\": \"contentSeo\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_50\",\n            \"name\": \"Blog Post\",\n            \"content\": \"Write a blog about [topic]. Make sure to focus on [key points].\",\n            \"category\": \"contentSeo\"\n        },\n        {\n            \"id\": \"built_in_prompt_51\",\n            \"name\": \"FAQ Generator\",\n            \"content\": \"Create a list of [10] frequently asked questions about [keyword] and provide answers for each one of them considering the SERP and rich result guidelines.\",\n            \"category\": \"contentSeo\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_52\",\n            \"name\": \"Headline Generator\",\n            \"content\": \"Generate 10 attention-grabbing headlines for an article about [your topic]\",\n            \"category\": \"contentSeo\"\n        },\n        {\n            \"id\": \"built_in_prompt_53\",\n            \"name\": \"Landing Page Copy\",\n            \"content\": \"Create website copy about [product details]. Follow the following structure:\\n\\n- Hero\\n- Subheader\\n- Call to action\\n- Tagline\\n- H2\\n- paragraph\\n- H2\\n- paragraph\\n- H2\\n- paragraph\\n- Call to action\",\n            \"category\": \"contentSeo\"\n        },\n        {\n            \"id\": \"built_in_prompt_54\",\n            \"name\": \"Product Brochure\",\n            \"content\": \"Create a brochure outlining the features and benefits of [product]. Include customer testimonials and a call to action.\\n\\nProduct details: [additional product details]\",\n            \"category\": \"contentSeo\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_55\",\n            \"name\": \"Product Description\",\n            \"content\": \"Craft an irresistible product description that highlights the benefits of [your product]\",\n            \"category\": \"contentSeo\"\n        },\n        {\n            \"id\": \"built_in_prompt_56\",\n            \"name\": \"Rewrite Content\",\n            \"content\": \"Revise the given content to improve its clarity, conciseness, and overall quality.\\n\\nInstructions:\\n1. Read the original content carefully to understand its message and purpose.\\n2. Identify any areas that need improvement, such as grammar, word choice, or sentence structure.\\n3. Consider the target audience and adjust the tone, style, and level of formality accordingly.\\n4. Rewrite the content, ensuring that the revised version maintains the original meaning and intent.\\n5. Use clear and concise language, avoiding unnecessary jargon or complex terms.\\n6. Break down lengthy sentences into shorter, more digestible ones.\\n7. Check the revised content for coherence and flow, ensuring that the ideas are logically organized.\\n8. Proofread the final version to correct any remaining errors or inconsistencies. \\n\\nOriginal content:\\n\\n[paste the original content here]\",\n            \"category\": \"contentSeo\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_57\",\n            \"name\": \"SEO Content Brief\",\n            \"content\": \"Create a SEO content brief for [keyword].\",\n            \"category\": \"contentSeo\"\n        },\n        {\n            \"id\": \"built_in_prompt_58\",\n            \"name\": \"SEO Keyword Ideas\",\n            \"content\": \"Generate a list of 20 keyword ideas on [topic].\\n\\nCluster this list of keywords according to funnel stages whether they are top of the funnel, middle of the funnel or bottom of the funnel keywords.\",\n            \"category\": \"contentSeo\"\n        },\n        {\n            \"id\": \"built_in_prompt_59\",\n            \"name\": \"Short Summary\",\n            \"content\": \"Write a summary in 50 words that summarizes [topic or keyword].\",\n            \"category\": \"contentSeo\"\n        },\n        {\n            \"id\": \"built_in_prompt_60\",\n            \"name\": \"Step-by-Step Guide\",\n            \"content\": \"Create a step by step guide to instruct how to [topic].\",\n            \"category\": \"contentSeo\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_61\",\n            \"name\": \"Article Generator\",\n            \"content\": \"Write an article about [topic].\\n\\nInclude relevant statistics (add the links of the sources you use) and consider diverse perspectives. Write it in a [X tone] and mention the source links at the end.\",\n            \"category\": \"contentSeo\"\n        },\n        {\n            \"id\": \"built_in_prompt_62\",\n            \"name\": \"Attention-Interest-Desire-Action\",\n            \"content\": \"Using the 'Attention-Interest-Desire-Action' framework, write an email marketing campaign that highlights the\\n\\n[features]\\n\\nof our [product/service]\\n\\nand explains how these [advantages]\\n\\ncan be helpful to [ideal customer persona].\\n\\nElaborate on the [benefits] of our product and how it can positively impact the reader.\",\n            \"category\": \"emailMarketing\"\n        },\n        {\n            \"id\": \"built_in_prompt_63\",\n            \"name\": \"Email Subject Generator\",\n            \"content\": \"Develop five subject lines for a cold email offering your [product or service] to a potential client\",\n            \"category\": \"emailMarketing\"\n        },\n        {\n            \"id\": \"built_in_prompt_64\",\n            \"name\": \"Features-Advantages-Benefits\",\n            \"content\": \"Using the 'Features-Advantages-Benefits' framework, please write an email marketing campaign that highlights the [features]\\n\\nof our [product/service]\\n\\nand explains how these [advantages]\\n\\ncan be helpful to [ideal customer persona].\\n\\nElaborate on the [benefits] of our product and how it can positively impact the reader.\",\n            \"category\": \"emailMarketing\"\n        },\n        {\n            \"id\": \"built_in_prompt_65\",\n            \"name\": \"Newsletter From Recent News\",\n            \"content\": \"Find recent news about [topic or event] and then turn it into a long form newsletter consisting of a subject line, intro, body paragraph, bullet point list, and a conclusion.\",\n            \"category\": \"emailMarketing\"\n        },\n        {\n            \"id\": \"built_in_prompt_66\",\n            \"name\": \"Newsletter Inspiration\",\n            \"content\": \"What are the top trends in [industry] that I can include in my next newsletter focused on [details about your newsletter]?\",\n            \"category\": \"emailMarketing\"\n        },\n        {\n            \"id\": \"built_in_prompt_67\",\n            \"name\": \"Pain-Agitate-Solution\",\n            \"content\": \"Using the 'Pain-Agitate-Solution' framework, please write an email marketing campaign that highlights the [features]\\n\\nof our [product/service]\\n\\nand explains how these [advantages]\\n\\ncan be helpful to [ideal customer persona].\\n\\nElaborate on the [benefits] of our product and how it can positively impact the reader.\",\n            \"category\": \"emailMarketing\"\n        },\n        {\n            \"id\": \"built_in_prompt_68\",\n            \"name\": \"Facebook Ad\",\n            \"content\": \"Create 3 variations of effective ad copy to promote [product] for [audience].\\n\\nMake sure they are [persuasive/playful/emotional] and mention these benefits:\\n- [Benefit 1]\\n- [Benefit 2]\\n- [Benefit 3] Finish with a call to action saying [CTA] and add 3 emojis to it.\",\n            \"category\": \"paidAds\"\n        },\n        {\n            \"id\": \"built_in_prompt_69\",\n            \"name\": \"Facebook Ad (PAS)\",\n            \"content\": \"Product Description: [Product Description]\\n\\nWrite a PAS for the product\\n\\nConvert the Problem Agitate Solution into Facebook ad copy\\n\\nWrite a Facebook ad headline\",\n            \"category\": \"paidAds\"\n        },\n        {\n            \"id\": \"built_in_prompt_70\",\n            \"name\": \"Facebook Headlines\",\n            \"content\": \"Brainstorm 20 compelling headlines for a Facebook ad promoting [product description] for [audience]. Format the output as a table.\",\n            \"category\": \"paidAds\"\n        },\n        {\n            \"id\": \"built_in_prompt_71\",\n            \"name\": \"Facebook Video Script\",\n            \"content\": \"Write a Facebook ad video script for [product description] that speaks directly to [our target audience] and addresses their pain points and desires\",\n            \"category\": \"paidAds\"\n        },\n        {\n            \"id\": \"built_in_prompt_72\",\n            \"name\": \"Google Ads\",\n            \"content\": \"Create 10 google ads (a headline and a description) for [product description] targeting the keyword [keyword].\\n\\nThe headline of the ad needs to be under 30 characters. The description needs to be under 90 characters. Format the output as a table.\",\n            \"category\": \"paidAds\"\n        },\n        {\n            \"id\": \"built_in_prompt_73\",\n            \"name\": \"Customer Case Study\",\n            \"content\": \"Write a customer case study highlighting how [company name] used [product name] to [achieve success]. Include 4 customer quotes, 2 customer success metrics, and visual elements.\\n\\nCase details:\\n[additional details go here]\",\n            \"category\": \"prCommunication\"\n        },\n        {\n            \"id\": \"built_in_prompt_74\",\n            \"name\": \"Event Invite\",\n            \"content\": \"Write a persuasive email to increase attendance at our upcoming event about [theme].\\n\\nHere are additional details about the event to include:\\n[event details]\",\n            \"category\": \"prCommunication\"\n        },\n        {\n            \"id\": \"built_in_prompt_75\",\n            \"name\": \"Internal Memos\",\n            \"content\": \"Write an internal memo to [specific department/team] regarding [topic].\\n\\nIn the memo, explain [key details] and the desired outcome. Provide an action plan with clear steps and timeline.\",\n            \"category\": \"prCommunication\"\n        },\n        {\n            \"id\": \"built_in_prompt_76\",\n            \"name\": \"Pitch a Journalist\",\n            \"content\": \"Pitch a story to [reporter name/publication].\\n\\nStory details: [details]\",\n            \"category\": \"prCommunication\"\n        },\n        {\n            \"id\": \"built_in_prompt_77\",\n            \"name\": \"Press Release\",\n            \"content\": \"Write a press release for [company or organization] announcing a recent achievement or milestone. Begin with a concise and attention-grabbing headline that summarizes the main news. In the opening paragraph, provide a brief overview of the announcement and its significance. In the following paragraphs, provide more details about the achievement, including any relevant statistics or data. Use quotes from company representatives or experts to add credibility and context to the announcement. End the press release with a brief company description and contact information for media inquiries.\\n\\nRemember to keep the press release concise, informative, and engaging, highlighting the key takeaways and benefits of the announcement.\\n\\nPress release details:\\n\\n[paste any necessary details about the announcement here]\",\n            \"category\": \"prCommunication\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_78\",\n            \"name\": \"Linkedin Connection Invite Message\",\n            \"content\": \"You are a recruiter trying to attract top talent.\\n\\nYou came across this linkedin profile [linkedin URL].\\n\\nYou have to pitch them and you can't write more than 500 characters\\n\\nYour message should include:\\n- the name of the company they're currently working for\\n- praise for their areas of expertise\",\n            \"category\": \"recruiting\"\n        },\n        {\n            \"id\": \"built_in_prompt_79\",\n            \"name\": \"3 Step Outreach Sequence\",\n            \"content\": \"Generate a 3 step outreach sequence for [company URL] selling to [target customer]\",\n            \"category\": \"sales\"\n        },\n        {\n            \"id\": \"built_in_prompt_80\",\n            \"name\": \"Analyze Industry Trends\",\n            \"content\": \"Analyze the industry trends for [industry] in the [country/region] for the past 12 months and compare it to the same period last year.\\n\\nPresent your findings in a report.\",\n            \"category\": \"sales\"\n        },\n        {\n            \"id\": \"built_in_prompt_81\",\n            \"name\": \"Brainstorm Pain Points\",\n            \"content\": \"Act as a [target persona].\\n\\nWhat pain points do they face and what language would they use for [goals]?\",\n            \"category\": \"sales\"\n        },\n        {\n            \"id\": \"built_in_prompt_82\",\n            \"name\": \"Competitive Analysis\",\n            \"content\": \"Conduct a full analysis of [competitor company name] and identify the competitive advantages and disadvantages of their product.\",\n            \"category\": \"sales\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_83\",\n            \"name\": \"Linkedin Boolean Search\",\n            \"content\": \"Write me a boolean search on variations of [roles] that I can plug into sales navigator search\",\n            \"category\": \"sales\"\n        },\n        {\n            \"id\": \"built_in_prompt_84\",\n            \"name\": \"Personalized Cold Email From LinkedIn Profile\",\n            \"content\": \"Write a personalized cold email to [linkedin profile url] selling [product description].\\n\\nMake sure the first 7 words of the email catch the reader's attention. Make the email 4-6 sentences.\",\n            \"category\": \"sales\"\n        },\n        {\n            \"id\": \"built_in_prompt_85\",\n            \"name\": \"Research Prospect From Linkedin\",\n            \"content\": \"Summarize [linkedin profile url] into 10 bullet points, brainstorm the pain points they have around [topic]\",\n            \"category\": \"sales\"\n        },\n        {\n            \"id\": \"builtin_prompt_86\",\n            \"name\": \"Caption Generator\",\n            \"content\": \"Write a catchy caption about [your theme] and try to play with words to make it fun, engage users in the end, ask them questions, use relevant emojis, and 3 hashtags in the end\",\n            \"category\": \"socialMedia\"\n        },\n        {\n            \"id\": \"builtin_prompt_87\",\n            \"name\": \"Generate Content Calendar\",\n            \"content\": \"Generate a content calendar about [topic]\",\n            \"category\": \"socialMedia\"\n        },\n        {\n            \"id\": \"builtin_prompt_88\",\n            \"name\": \"Headlines\",\n            \"content\": \"Write 5 attention-grabbing headlines for a [platform] post on [topic] for [audience].\",\n            \"category\": \"socialMedia\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"builtin_prompt_89\",\n            \"name\": \"Instagram Captions\",\n            \"content\": \"Write 5 variations of Instagram captions for [product].\\n\\nUse friendly, human-like language that appeals to [target audience].\\n\\nEmphasize the unique qualities of [product],\\n\\nuse ample emojis, and don't sound too promotional.\",\n            \"category\": \"socialMedia\"\n        },\n        {\n            \"id\": \"builtin_prompt_90\",\n            \"name\": \"LinkedIn Post\",\n            \"content\": \"Create a narrative Linkedin post using immersive writing about [topic].\\n\\nDetails:\\n\\n[give details in bullet point format]\\n\\nUse a mix of short and long sentences. Make it punchy and dramatic.\",\n            \"category\": \"socialMedia\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"builtin_prompt_91\",\n            \"name\": \"TikTok Script\",\n            \"content\": \"Write a super engaging TikTok video script on [topic]. Each sentence should catch the viewer's attention to make them keep watching.\",\n            \"category\": \"socialMedia\"\n        },\n        {\n            \"id\": \"builtin_prompt_92\",\n            \"name\": \"Twitter Thread\",\n            \"content\": \"Give a controversial opinion on [topic], then turn it into a twitter thread.\",\n            \"category\": \"socialMedia\"\n        },\n        {\n            \"id\": \"builtin_prompt_93\",\n            \"name\": \"Youtube Video Description\",\n            \"content\": \"Write a 100-word YouTube video description that compels [audience]\\n\\nto watch a video on [topic]\\n\\nand mentions the following keywords\\n\\n[keyword 1]\\n\\n[keyword 2]\\n\\n[keyword 3].\",\n            \"category\": \"socialMedia\"\n        },\n        {\n            \"id\": \"builtin_prompt_94\",\n            \"name\": \"Reframing Business Perspectives\",\n            \"content\": \"Apply Reframing Business Perspectives to analyze [my business decision]. Look at the problem from different angles, challenging the existing beliefs.\",\n            \"category\": \"strategy\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"builtin_prompt_95\",\n            \"name\": \"Behavioral Economics Principles\",\n            \"content\": \"Apply Behavioral Economics Principles to analyze [my business decision]. Consider how cognitive, emotional, and social factors affect economic decisions.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_96\",\n            \"name\": \"Blue Ocean Strategy\",\n            \"content\": \"Apply the Blue Ocean Strategy to evaluate [my business decision]. Focus on creating uncontested market space rather than competing in existing industries.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_97\",\n            \"name\": \"Brand Ecosystem Development\",\n            \"content\": \"Utilize Brand Ecosystem Development to assess [my business decision]. Build a network of interrelated products, services, and stakeholders.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_98\",\n            \"name\": \"Consumer Behavior Analysis\",\n            \"category\": \"strategy\",\n            \"content\": \"Use Consumer Behavior Analysis to evaluate [my business decision]. Dive into the psychological, personal, and social influences on consumer choices.\"\n        },\n        {\n            \"id\": \"builtin_prompt_99\",\n            \"name\": \"Cost-Benefit Analysis\",\n            \"content\": \"Apply Cost-Benefit Analysis to assess [my business decision]. Analyze the expected balance of benefits and costs, including possible risk and uncertainties.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_100\",\n            \"name\": \"Customer Lifetime Value\",\n            \"content\": \"Assess [my business decision] by considering Customer Lifetime Value. Analyze the long-term value of customers to understand how acquisition, retention, and monetization strategies align.\",\n            \"category\": \"strategy\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"builtin_prompt_101\",\n            \"name\": \"Customer Persona Building\",\n            \"content\": \"Utilize Customer Persona Building to evaluate [my business decision]. Define specific customer archetypes with detailed attributes and needs.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_102\",\n            \"name\": \"Disruptive Innovation\",\n            \"content\": \"Apply Disruptive Innovation to assess [my business decision]. Consider how groundbreaking changes in technology or methodology might impact your industry or market.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_103\",\n            \"name\": \"Double Loop Learning\",\n            \"content\": \"Use Double Loop Learning to evaluate [my business decision]. Reflect not just on solutions, but on underlying assumptions and beliefs, encouraging adaptive learning.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_104\",\n            \"name\": \"Eisenhower Matrix\",\n            \"content\": \"Use the Eisenhower Matrix to evaluate [my business decision]. Categorize tasks or elements based on urgency and importance to prioritize effectively.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_105\",\n            \"name\": \"Emotional Intelligence\",\n            \"content\": \"Evaluate [my business decision] with Emotional Intelligence in mind. Recognize and manage both your own and others' emotions to make more empathetic and effective decisions.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_106\",\n            \"name\": \"Freemium Business Model\",\n            \"content\": \"Apply the Freemium Business Model to [my business decision]. Explore offering basic services for free while charging for premium features.\",\n            \"category\": \"strategy\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"builtin_prompt_107\",\n            \"name\": \"Heuristics and Decision Trees\",\n            \"content\": \"Evaluate [my business decision] using Heuristics and Decision Trees. Create simplified models to understand complex problems and find optimal paths.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_108\",\n            \"name\": \"Hyper-Personalization Strategy\",\n            \"content\": \"Use Hyper-Personalization Strategy to evaluate [my business decision]. Leverage data and AI to provide an extremely personalized experience.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_109\",\n            \"name\": \"Innovation Ambition Matrix\",\n            \"content\": \"Evaluate [my business decision] through the Innovation Ambition Matrix. Plot initiatives on a matrix to balance core enhancements, adjacent opportunities, and transformational innovations.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_110\",\n            \"name\": \"Jobs to Be Done Framework\",\n            \"content\": \"Assess [my business decision] by applying the Jobs to Be Done Framework. Focus on the problems customers are trying to solve.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_111\",\n            \"name\": \"Kano Model Analysis\",\n            \"content\": \"Evaluate [my business decision] using the Kano Model Analysis. Prioritize customer needs into basic, performance, and excitement categories.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_112\",\n            \"name\": \"Lean Startup Principles\",\n            \"content\": \"Apply Lean Startup Principles to [my business decision]. Focus on creating a minimum viable product, measuring its success, and learning from the results.\",\n            \"category\": \"strategy\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"builtin_prompt_113\",\n            \"name\": \"Long Tail Strategy\",\n            \"content\": \"Analyze [my business decision] focusing on the Long Tail Strategy. Consider how niche markets or products may contribute to overall success.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_114\",\n            \"name\": \"Network Effects\",\n            \"content\": \"Analyze [my business decision] through the understanding of Network Effects. Consider how the value of a product or service increases as more people use it.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_115\",\n            \"name\": \"Pre-Mortem Analysis\",\n            \"content\": \"Utilize Pre-Mortem Analysis to assess [my business decision]. Imagine a future failure of the decision and work backward to identify potential causes and mitigation strategies.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_116\",\n            \"name\": \"Prospect Theory\",\n            \"content\": \"Utilize Prospect Theory to assess [my business decision]. Understand how people perceive gains and losses and how that can influence decision-making.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_117\",\n            \"name\": \"Pygmalion Effect\",\n            \"content\": \"Apply the Pygmalion Effect to analyze [my business decision]. Recognize how expectations can influence outcomes, both positively and negatively.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_118\",\n            \"name\": \"Resource-Based View\",\n            \"content\": \"Apply the Resource-Based View to evaluate [my business decision]. Focus on leveraging the company's internal strengths and weaknesses in relation to external opportunities and threats.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_119\",\n            \"name\": \"Risk-Reward Analysis\",\n            \"content\": \"Analyze [my business decision] through Risk-Reward Analysis. Evaluate the potential risks against the potential rewards to understand the balance and make an informed decision.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_120\",\n            \"name\": \"Scenario Planning\",\n            \"content\": \"Apply Scenario Planning to assess [my business decision]. Create different future scenarios and analyze how the decision performs in each to identify potential risks and opportunities.\"\n        },\n        {\n            \"id\": \"builtin_prompt_121\",\n            \"name\": \"Six Thinking Hats\",\n            \"content\": \"Evaluate [my business decision] through the Six Thinking Hats method. Analyze the decision from different perspectives such as logical, emotional, cautious, creative, and more.\",\n            \"category\": \"strategy\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"builtin_prompt_122\",\n            \"name\": \"Temporal Discounting\",\n            \"content\": \"Use Temporal Discounting to analyze [my business decision]. Consider how the value of outcomes changes over time and how that might influence the decision-making process.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_123\",\n            \"name\": \"The Five Whys Technique\",\n            \"content\": \"Utilize the Five Whys Technique to analyze [my business decision]. Ask 'why?' multiple times to get to the root cause of problems or challenges.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_124\",\n            \"name\": \"The OODA Loop (Observe, Orient, Decide, Act)\",\n            \"content\": \"Use the OODA Loop to evaluate [my business decision]. Cycle through observing the situation, orienting yourself, making a decision, and taking action, then repeating as necessary.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_125\",\n            \"name\": \"Value Chain Analysis\",\n            \"content\": \"Apply Value Chain Analysis to evaluate [my business decision]. Examine all the activities performed by a company to create value and find opportunities for competitive advantage.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_126\",\n            \"name\": \"Viral Loop Strategy\",\n            \"content\": \"Use Viral Loop Strategy to assess [my business decision]. Construct a process where existing users help in recruiting new ones.\",\n            \"category\": \"strategy\"\n        },\n        {\n            \"id\": \"builtin_prompt_127\",\n            \"name\": \"Focus on Results\",\n            \"content\": \"Can you provide me with a case study template for [CLIENT NAME]'s [PRODUCT/SERVICE] that showcases the results achieved? Please suggest the key metrics and KPIs that should be included. Use [AGENCY NAME], [RESULTS], and [KEY METRICS] as placeholders for customization.\",\n            \"category\": \"caseStudies\"\n        },\n        {\n            \"id\": \"builtin_prompt_128\",\n            \"name\": \"Showcase Digital Transformation\",\n            \"content\": \"How would you structure a case study for [CLIENT NAME] that focuses on their journey of digital transformation? Please suggest the main sections, such as the challenges, the solutions, and the outcomes achieved. Use [AGENCY NAME], [CLIENT NAME], and [OUTCOMES] as placeholders for customization.\",\n            \"category\": \"caseStudies\"\n        },\n        {\n            \"id\": \"builtin_prompt_129\",\n            \"name\": \"Highlighting Collaboration\",\n            \"content\": \"Can you generate a case study template for [CLIENT NAME] that highlights their partnership with [PARTNER NAME]? Please include details on the collaboration process, the benefits for both parties, and the outcomes achieved. Use [AGENCY NAME], [CLIENT NAME], and [PARTNER NAME] as placeholders for customization.\",\n            \"category\": \"caseStudies\"\n        },\n        {\n            \"id\": \"builtin_prompt_130\",\n            \"name\": \"Achieving Goals\",\n            \"content\": \"How would you structure a case study for [CLIENT NAME] that showcases their success in achieving sustainability goals? Please suggest the main sections, such as the challenges, the solutions, and the impact achieved. Use [AGENCY NAME], [CLIENT NAME], and [IMPACT] as placeholders for customization.\",\n            \"category\": \"caseStudies\"\n        },\n        {\n            \"id\": \"builtin_prompt_131\",\n            \"name\": \"Focus on Strategy\",\n            \"content\": \"Can you provide me with a case study template for [CLIENT NAME] that highlights their social media strategy? Please include details on the objectives, the tactics, and the results achieved. Use [AGENCY NAME], [CLIENT NAME], and [RESULTS] as placeholders for customization.\",\n            \"category\": \"caseStudies\"\n        },\n        {\n            \"id\": \"builtin_prompt_132\",\n            \"name\": \"Highlight Unique Features\",\n            \"content\": \"Can you write a benefit-driven sales copy that highlights the unique features and benefits of [product/service]?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"builtin_prompt_133\",\n            \"name\": \"Explain the Benefits\",\n            \"content\": \"How would you describe the benefits of [product/service] to a potential customer in a way that would convince them to purchase?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"builtin_prompt_134\",\n            \"name\": \"Explain the Need\",\n            \"content\": \"Can you craft a benefit-driven sales copy that explains why [product/service] is the best solution for [customer's problem or need]?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"builtin_prompt_135\",\n            \"name\": \"Crafting Convincing Copy\",\n            \"content\": \"How would you sell the benefits of [product/service] to a customer who is on the fence about making a purchase?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"builtin_prompt_136\",\n            \"name\": \"Overcome Objections\",\n            \"content\": \"How would you sell the benefits of [product/service] to a customer who is skeptical about its effectiveness for [customer's problem or need]?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"builtin_prompt_137\",\n            \"name\": \"Make Your Product Stand Out (Long-Form)\",\n            \"content\": \"Can you write an extended sales copy describing the unique features and benefits of [product/service] that sets it apart from the competition and makes it a must-have for [target audience]?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"builtin_prompt_138\",\n            \"name\": \"Top Features (Long-Form)\",\n            \"content\": \"Can you write an extended sales copy on what are the top features of [product/service] that [target audience] will love?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"builtin_prompt_139\",\n            \"name\": \"Explaining the Process (Long-Form)\",\n            \"content\": \"Can you write an extended sales copy explaining in detail the process of using [product/service] and how it can help [target audience] achieve their goals?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"builtin_prompt_140\",\n            \"name\": \"Differentiating Your Product (Long-Form)\",\n            \"content\": \"Can you write an extended sales copy on what makes [product/service] stand out from similar offerings in the market, and why should [target audience] choose it over the competition?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"builtin_prompt_141\",\n            \"name\": \"Convincing Sales Copy (Long-Form)\",\n            \"content\": \"Can you write an extended sales copy convincing me why [product/service] is the missing piece [target audience] needs to take their [aspect of life/business] to the next level?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"builtin_prompt_142\",\n            \"name\": \"Engaging Sales Pitches (Medium-Form)\",\n            \"content\": \"Write a sales pitch for a [product/service] that highlights its unique features and benefits.\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"builtin_prompt_143\",\n            \"name\": \"Persuasive Emails (Medium-Form)\",\n            \"content\": \"Write a persuasive email to a potential customer explaining why they should choose [company name] for their [product/service] needs.\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"builtin_prompt_144\",\n            \"name\": \"Persuasive Blog Posts (Medium-Form)\",\n            \"content\": \"Write a persuasive blog post about the benefits of using [product/service] for [specific industry/target market].\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"builtin_prompt_145\",\n            \"name\": \"Problem-Solving Sales Pitches (Medium-Form)\",\n            \"content\": \"Write a sales pitch for a [product or service] that emphasizes its unique features and benefits. Include specific details about how it can solve a problem or improve the customer's life.\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_146\",\n            \"name\": \"Translating Ad Copy\",\n            \"content\": \"Translate the following ad copy into [language]: [ad copy]\",\n            \"category\": \"salesCopy\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_147\",\n            \"name\": \"Persuasive Ad Copy\",\n            \"content\": \"Rewrite the following ad copy to make it more persuasive: [ad copy]\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_148\",\n            \"name\": \"Translating Ad Copy Alternative Phrasing\",\n            \"content\": \"What are some alternative ways to phrase the following ad copy in [language]: [ad copy]\",\n            \"category\": \"salesCopy\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_149\",\n            \"name\": \"Translating Ad Copy Compelling CTAs\",\n            \"content\": \"Write a compelling call-to-action for our [product/service] in [language]: [product/service description]\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_150\",\n            \"name\": \"Translating Ad Copy Catchy Taglines\",\n            \"content\": \"Create a catchy tagline for our new [product/service] in [language]: [product/service description]\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_151\",\n            \"name\": \"Make Ad Copy More Interesting Grab Audience Attention\",\n            \"content\": \"I am trying to make my ad copy for [product/service] more interesting. Can you help me come up with a catchy headline and a unique selling point that will grab people's attention?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_152\",\n            \"name\": \"Make Ad Copy More Interesting Create Memorable Products\",\n            \"content\": \"I am looking to create an ad campaign for [product/service] that stands out. Can you help me write ad copy that is engaging, memorable and persuasive?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_153\",\n            \"name\": \"Make Ad Copy More Interesting Evoke Emotion\",\n            \"content\": \"I want to create an ad for [product/service] that evokes emotions and resonates with [target audience]. Can you help me write copy that will connect with them on a deeper level?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_154\",\n            \"name\": \"Make Ad Copy More Interesting Entice Your Audience\",\n            \"content\": \"Introduce our new [product/service] in a way that highlights its unique features and benefits.\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_155\",\n            \"name\": \"Make Ad Copy More Interesting Achieve Specific Goals\",\n            \"content\": \"Write an ad copy that showcases how our [product/service] can help [target audience] achieve [specific goal].\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_156\",\n            \"name\": \"Crafting Unique USP's Being the Company of Choice\",\n            \"content\": \"Why is [type of company/business]'s [product/service] the best choice for [target audience] looking for [desired outcome]?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_157\",\n            \"name\": \"Crafting Unique USP's Highlighting Unique Solutions\",\n            \"content\": \"What unique solution does [type of company/business] offer to solve the [pain point] faced by [target audience]?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_158\",\n            \"name\": \"Crafting Unique USP's Distinct Approaches to Industry Challenges\",\n            \"content\": \"What makes [specific topic company/business] approach to [industry challenge] different and more effective?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_159\",\n            \"name\": \"Providing Superior Benefits\",\n            \"content\": \"How does [specific type of company/business] [product/service feature] provide a superior [customer benefit] compared to others in the market?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_160\",\n            \"name\": \"Features-Advantages-Benefits' framework\",\n            \"content\": \"Using the 'Features-Advantages-Benefits' framework, please write a copy that highlights the [features] of our [product/service] and explains how these [advantages] can be helpful to [ideal customer persona]. Elaborate on the [benefits] of our product and how it can positively impact the reader.\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_161\",\n            \"name\": \"PASTOR' framework\",\n            \"content\": \"Write a copy using the 'PASTOR' framework to address the pain points of [ideal customer persona] and present our [product/service] as the solution. Identify the [problem] they are facing, amplify the consequences of not solving it, tell a [story] related to the problem, include [testimonials] from happy customers, present our [offer], and ask for a response.\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_162\",\n            \"name\": \"Before-After-Bridge' framework\",\n            \"content\": \"Using the 'Before-After-Bridge' framework, please write a copy that presents the current situation with a [problem] faced by ideal customer persona]. Show them the world after using our [product/service] and how it has improved their situation. Then, provide a [bridge] to show them how they can get to that improved state by using our product.\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_163\",\n            \"name\": \"Attention-Interest-Desire-Action' framework\",\n            \"content\": \"Using the 'Before-After-Bridge' framework, please write a copy that presents the current situation with a [problem] faced by ideal customer persona]. Show them the world after using our [product/service] and how it has improved their situation. Then, provide a [bridge] to show them how they can get to that improved state by using our product.\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_164\",\n            \"name\": \"Problem-Agitate-Solve' framework\",\n            \"content\": \"Using the 'Problem-Agitate-Solve' framework, please write a copy that identifies the most painful [problem] faced by [ideal customer persona] and agitates the issue to show why it is a bad situation. Then, present our [product/service] as the logical solution to the problem.\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_165\",\n            \"name\": \"Star-Story-Solution' framework\",\n            \"content\": \"Using the 'Features-Advantages-Benefits' framework, please write a copy that highlights the [features] of our [product/service] and explains how these [advantages] can be helpful to [ideal customer persona]. Elaborate on the [benefits] of our product and how it can positively impact the reader.\",\n            \"category\": \"copyWriting\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_166\",\n            \"name\": \"Picture-Promise-Prove-Push' framework\",\n            \"content\": \"Write a copy using the 'Picture-Promise-Prove-Push' framework to paint a picture that gets the attention and creates desire for our [product/service] in ideal customer persona]. Describe how our product will deliver on its promises, provide testimonials to back up those promises, and give a little push to encourage the reader to take action.\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_167\",\n            \"name\": \"Awareness-Comprehension-Conviction-Action’ framework\",\n            \"content\": \"Write a copy using the 'Awareness-Comprehension-Conviction-Action’ framework to present the situation or [problem] faced by [ideal customer persona] and help them understand it. Create the desired conviction in the reader to use our [product/service] as the solution and make them take action.\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_168\",\n            \"name\": \"5 Basic Objections' framework\",\n            \"content\": \"Using the '5 Basic Objections' framework, please write a copy that addresses and refutes the common objections of [ideal customer personal: lack of time, lack of money, concerns that the product won't work for them, lack of belief in the product or company, and the belief that they don't need the product. Include talking points such as [unique selling point] and [desired action].\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_169\",\n            \"name\": \"Four C's' framework\",\n            \"content\": \"Write a copy using the 'Four C's' framework to create clear, concise, compelling, and credible copy for [ideal customer persona]. Use this checklist to ensure that our message is effectively communicated and persuades the reader to take action. Include talking points such as [unique selling point] and [desired action].\",\n            \"category\": \"copyWriting\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_170\",\n            \"name\": \"Consistent-Contrasting' framework\",\n            \"content\": \"Please write a copy using the 'Consistent-Contrasting' framework to convert leads into customers. Use a consistent message or theme throughout the copy, but incorporate contrasting language or images to draw the reader's attention and keep them engaged. Include talking points such as [product/service], [unique selling point], and [desired action].\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_171\",\n            \"name\": \"Strong-Weak' framework\",\n            \"content\": \"Write a copy using the 'Strong-Weak' framework to persuade [ideal customer persona] to take action. Use strong language and images to emphasize the benefits of our [product/service], but also acknowledge any potential weaknesses or limitations in a transparent and honest way. Include talking points such as [unique selling point], [pain point], and [desired action].\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_172\",\n            \"name\": \"Emotion-Logic' framework\",\n            \"content\": \"Using the 'Emotion-Logic' framework, please write a copy that connects with [ideal customer persona] and creates desire for our [product/service]. Use emotional appeals to connect with the reader, but also use logical arguments to convince them to take action. Include talking points such as [emotion], [pain point], and [desired action].\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_173\",\n            \"name\": \"Personal-Universal' framework\",\n            \"content\": \"Craft a copy using the 'Personal-Universal' framework to make our [product/service] relatable to [ideal customer persona]. Use \\\"you\\\" language and address their specific needs and desires, but also connect our product to universal human experiences and values. Include talking points such as [unique selling point], [pain point], and [desired action].\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_174\",\n            \"name\": \"Urgency-Patience' framework\",\n            \"content\": \"Write a copy using the 'Urgency-Patience' framework to encourage [ideal customer persona] to take action. Create a sense of urgency to encourage the reader to act now, but also remind them that using our [product/service] will bring long-term benefits that are worth waiting for. Include talking points such as [unique selling point], [pain point], and [desired action].\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_175\",\n            \"name\": \"Expectation-Surprise' framework\",\n            \"content\": \"Please write a copy using the 'Expectation-Surprise' framework to generate interest and encourage action from [ideal customer persona]. Set expectations for the reader about what they can expect from our [product/service], but then surprise them with unexpected benefits or features that exceed those expectations. Include talking points such as [unique selling point], [pain point], and [desired action].\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_176\",\n            \"name\": \"Exclusive-Inclusive' framework\",\n            \"content\": \"Write a copy using the 'Exclusive-Inclusive' framework to position our [product/service] as elite and desirable to [ideal customer persona]. Make it clear that our product is exclusive or elite in some way, but also emphasize that it is accessible and inclusive to a wide range of customers. Include talking points such as [unique selling point], [pain point], and [desired action].\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_177\",\n            \"name\": \"Positive-Negative' framework\",\n            \"content\": \"Using the 'Positive-Negative' framework, please write a copy that focuses on the positive aspects of our [product/service] and the benefits it will bring to [ideal customer persona]. Also acknowledge and address any potential negative consequences or drawbacks in a constructive way. Include talking points such as [unique selling point], [pain point], and [desired action].\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_178\",\n            \"name\": \"Past-Present-Future' framework\",\n            \"content\": \"Create a copy using the 'Past-Present-Future' framework to connect our [product/service] to [ideal customer persona]'s past experiences or memories. Show how it can improve their present situation, and then show how it can shape their future in a positive way. Include talking points such as [unique selling point], [pain point], and [desired action].\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_179\",\n            \"name\": \"Friend-Expert' framework\",\n            \"content\": \"Write a copy using the 'Friend-Expert' framework to establish a connection with [ideal customer persona] and position our brand or [product/service] as an expert in our field. Use a friendly and approachable tone to connect with the reader, but also highlight our credibility and expertise in our field. Include talking points such as [unique selling point], [pain point], and [desired action].\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_180\",\n            \"name\": \"Pain-Agitate-Relief' framework\",\n            \"content\": \"Please write a copy using the 'Pain-Agitate-Relief' framework to convert leads into customers. Identify the pain points faced by [ideal customer personal], amplify the negative consequences of not addressing these pain points, and present our [product/service] as the solution that brings relief. Include variables such as [product/service], [unique selling point], and [desired action].\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_181\",\n            \"name\": \"Solution-Savings-Social Proof' framework\",\n            \"content\": \"Write a copy using the 'Solution-Savings-Social Proof' framework to persuade [ideal customer persona] to take action. Clearly state the problem our [product/service] solves, emphasize the time, money, or other resources that the customer can save by using our product, and use customer testimonials or social proof to demonstrate the effectiveness of our solution. Include variables such as [product/service], [unique selling point], and [desired action].\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_182\",\n            \"name\": \"‘6 W's' framework\",\n            \"content\": \"Write a copy using the '6 W's' framework to convert leads into customers. Identify [ideal customer persona] as the target audience, clearly describe our [product/service] and what it does, highlight any time-sensitive aspects of our offer or the problem it solves, specify where the product or service can be purchased or used, clearly explain the benefits and value of our [product/service], and explain how the product or service works and how the customer can obtain it. Include variables such as [product/service], [unique selling point], and [desired action].\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_183\",\n            \"name\": \"Story-Solve-Sell' framework\",\n            \"content\": \"Create a copy using the 'Story-Solve-Sell' framework to convert leads into customers. Tell a compelling story that connects with [ideal customer persona] and relates to the problem our [product/service] solves, clearly demonstrate how our product solves the problem, and make a strong call to action to convince the reader to purchase or take the desired action. Include variables such as [product/service], [unique selling point], and [desired action].\",\n            \"category\": \"copyWriting\"\n        },\n        {\n            \"id\": \"built_in_prompt_184\",\n            \"name\": \"Outlining Your Course\",\n            \"content\": \"As a Professor at a [Software] Company, design a comprehensive outline for a 12-week educational course focused on [software development] techniques. Include weekly topics, learning objectives, and suggested teaching materials.\",\n            \"category\": \"education\"\n        },\n        {\n            \"id\": \"built_in_prompt_185\",\n            \"name\": \"Detailed Lesson Plans\",\n            \"content\": \"Create a detailed lesson plan for a 90-minute workshop on [agile software development principles] for [professionals]. Ensure the plan covers key concepts, activities, and learning takeaways for participants.\",\n            \"category\": \"education\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_186\",\n            \"name\": \"Proposing a Virtual Series\",\n            \"content\": \"Draft a proposal for a series of virtual webinars addressing emerging trends in [software development], discussing the potential topics for the webinars and their educational value for the audience.\",\n            \"category\": \"education\"\n        },\n        {\n            \"id\": \"built_in_prompt_187\",\n            \"name\": \"Training Students on Best Practices\",\n            \"content\": \"As a Professor at a [Software] Company, outline a curriculum for a 6-week training program targeted at upskilling [developers] in [cybersecurity] best practices. Include course modules, learning objectives, and assessment strategies.\",\n            \"category\": \"education\"\n        },\n        {\n            \"id\": \"built_in_prompt_188\",\n            \"name\": \"Teaching Skills Relevant to Company Needs\",\n            \"content\": \"Design a syllabus for a four-week, intensive [coding] bootcamp focused on teaching [programming languages] and [software tools] relevant to your company's needs. Consider the skills participants need to learn and how to structure the course for optimal engagement and retention.\",\n            \"category\": \"education\"\n        },\n        {\n            \"id\": \"built_in_prompt_189\",\n            \"name\": \"Angles and Subtopics\",\n            \"content\": \"For my podcast episode on [TOPIC], can you provide me with [NUMBER] unique and [ADJECTIVE] angles or subtopics that will appeal to [TYPE OF AUDIENCE], and suggest [NUMBER] potential guests who can offer their expertise on each subtopic?\",\n            \"category\": \"podcastProduction\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_190\",\n            \"name\": \"Identifying Different Viewpoints\",\n            \"content\": \"Can you help me plan a podcast episode on [TOPIC], by identifying [NUMBER] different viewpoints on [SUBTOPIC] and providing [TYPE OF INFORMATION] on each, while incorporating [TYPE OF MEDIA] to add depth and interest?\",\n            \"category\": \"podcastProduction\"\n        },\n        {\n            \"id\": \"built_in_prompt_191\",\n            \"name\": \"Creating a Podcast Series\",\n            \"content\": \"I want to create a series of podcast episodes on [TOPIC], can you help me brainstorm [NUMBER] overarching themes, [NUMBER] subtopics for each theme, and [TYPE OF INFORMATION] for each subtopic that will keep listeners engaged throughout the entire series?\",\n            \"category\": \"podcastProduction\"\n        },\n        {\n            \"id\": \"built_in_prompt_192\",\n            \"name\": \"Tailoring to Specific Demographics\",\n            \"content\": \"When creating a podcast episode about [TOPIC] for a [SPECIFIC DEMOGRAPHIC] audience, what are some [NUMBER] [ADJECTIVE] ways to hook their attention at the beginning and keep them engaged throughout the episode? How can I address potential concerns or questions that they might have about the topic in an informative and empathetic manner?\",\n            \"category\": \"podcastProduction\"\n        },\n        {\n            \"id\": \"built_in_prompt_193\",\n            \"name\": \"Integrating Elements\",\n            \"content\": \"As a podcast creator, I want to integrate [SPECIFIC ELEMENT] into my episode about [TOPIC] in a [ADJECTIVE] way that enhances my audience's listening experience. What are some [NUMBER] strategies or tools I could use to accomplish this goal? Could you suggest any examples of podcasts that have successfully implemented this element in their episodes?\",\n            \"category\": \"podcastProduction\"\n        },\n        {\n            \"id\": \"built_in_prompt_194\",\n            \"name\": \"Cold Calling Complete Sales Copy\",\n            \"content\": \"Can you provide me with a complete sales copy about [product/service] for a cold call to a potential client, including an opening, presentation, overcoming objections, and close?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_195\",\n            \"name\": \"Cold Calling Showcasing a Product/ Service\",\n            \"content\": \"Can you draft a copy for a sales cold call that effectively showcases the [product/service] to [Prospect Name] and leads to a successful close?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_196\",\n            \"name\": \"Cold Calling Tailoring Copy to Specific Audiences\",\n            \"content\": \"Can you formulate a sales copy for [product/service] for a cold call to [Specific audience] that covers the aspects of introduction, demonstration, objection handling, and closing?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_197\",\n            \"name\": \"Successful Pitch Examples\",\n            \"content\": \"Can you provide me an example of a successful pitch for [product/service] to a [specific audience] potential client?\",\n            \"category\": \"salesCopy\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_198\",\n            \"name\": \"Presenting Product Value\",\n            \"content\": \"Write me a sales cold calling copy by presenting the value of [product/service] to [prospective customer name] in the most effective way.\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_199\",\n            \"name\": \"Client Proposal Highlighting Benefits\",\n            \"content\": \"Can you please write a B2B proposal for [Company] that highlights the benefits of using our [Product/Service] and how it can help them achieve their [specific business goal]?\",\n            \"category\": \"salesCopy\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_200\",\n            \"name\": \"Client Proposal Improving Specific Processes\",\n            \"content\": \"Can you draft a B2B proposal for [Company Name] that explains how our [Product/Service] can improve their [specific business process] and increase their [specific metric]?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_201\",\n            \"name\": \"Client Proposal Industry Specific Proposals\",\n            \"content\": \"I am looking to write a proposal for a potential client in the [industry] industry. Can you help me create a compelling introduction and outline the key points and benefits of my [product/service]?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_202\",\n            \"name\": \"Client Proposal B2B Proposals Comparing Solutions\",\n            \"content\": \"Can you compose a B2B proposal for [Company Name] that showcases the unique features of our [Product/Service] and how it compares to similar solutions in the market?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_203\",\n            \"name\": \"Client Proposal Winning Over a New Client\",\n            \"content\": \"I am trying to win over a new client for my [product/service]. Can you help me write a persuasive proposal that highlights the benefits and value of [offering]?\",\n            \"category\": \"salesCopy\"\n        },\n        {\n            \"id\": \"built_in_prompt_204\",\n            \"name\": \"Meeting Notes Highlighting Key Takeaways\",\n            \"content\": \"Can you summarize a meeting on [topic of meeting] by highlighting the key takeaways? The notes of the meeting: [notes]\",\n            \"category\": \"work\"\n        },\n        {\n            \"id\": \"built_in_prompt_205\",\n            \"name\": \"Meeting Notes Summarizing Objectives\",\n            \"content\": \"Can you summarize the objectives discussed in a meeting and the action items decided? The notes of the meeting: [notes]\",\n            \"category\": \"work\"\n        },\n        {\n            \"id\": \"built_in_prompt_206\",\n            \"name\": \"Meeting Notes Summarizing Decisions and Next Steps\",\n            \"content\": \"Can you summarize the decisions made during a meeting about [specific issue] and the next steps outlined? The notes of the meeting: [notes]\",\n            \"category\": \"work\"\n        },\n        {\n            \"id\": \"built_in_prompt_207\",\n            \"name\": \"Meeting Notes Summarizing Progress Updates\",\n            \"content\": \"Can you summarize the progress update given in a meeting on [project/task] and the future plans discussed? The notes of the meeting: [notes]\",\n            \"category\": \"work\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_208\",\n            \"name\": \"Meeting Notes Summarizing Key Points and Solutions\",\n            \"content\": \"Can you summarize the key points raised during a [team/department/etc.] meeting and the solutions proposed? The notes of the meeting: [notes]\",\n            \"category\": \"work\"\n        },\n        {\n            \"id\": \"built_in_prompt_209\",\n            \"name\": \"Generate Long-tail Keywords The Definition\",\n            \"content\": \"Can you define [keyword] in a few words?\",\n            \"category\": \"contentSeo\"\n        },\n        {\n            \"id\": \"built_in_prompt_210\",\n            \"name\": \"Generate Long-tail Keywords A Short Description\",\n            \"content\": \"What is a short description for [keyword]?\",\n            \"category\": \"contentSeo\"\n        },\n        {\n            \"id\": \"built_in_prompt_211\",\n            \"name\": \"Generate Long-tail Keywords The Central Idea\",\n            \"content\": \"What is the central idea of [keyword]?\",\n            \"category\": \"contentSeo\"\n        },\n        {\n            \"id\": \"built_in_prompt_212\",\n            \"name\": \"Generate Long-tail Keywords The Core\",\n            \"content\": \"What is the core of [keyword]?\",\n            \"category\": \"contentSeo\"\n        },\n        {\n            \"id\": \"built_in_prompt_213\",\n            \"name\": \"Generate Long-tail Keywords The Key Elements\",\n            \"content\": \"What are the key elements of [keyword]?\",\n            \"category\": \"contentSeo\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_214\",\n            \"name\": \"Customer Surveys Key Questions to Ask\",\n            \"content\": \"What are some key questions to ask in a customer survey to gauge [product/service] satisfaction?\",\n            \"category\": \"customerSuccess\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_215\",\n            \"name\": \"Customer Surveys Examples of Open-Ended Questions\",\n            \"content\": \"Can you provide some examples of open-ended questions to include in a customer survey for [company/industry]?\",\n            \"category\": \"customerSuccess\"\n        },\n        {\n            \"id\": \"built_in_prompt_216\",\n            \"name\": \"Customer Surveys Best Practices to Gather Feedback\",\n            \"content\": \"What are some best practices for creating a customer survey to gather valuable feedback on [specific aspect of product/service]?\",\n            \"category\": \"customerSuccess\"\n        },\n        {\n            \"id\": \"built_in_prompt_217\",\n            \"name\": \"Customer Surveys Important Metrics to Track\",\n            \"content\": \"What are the most important metrics to track in a customer survey to measure [product/service] success?\",\n            \"category\": \"customerSuccess\"\n        },\n        {\n            \"id\": \"built_in_prompt_218\",\n            \"name\": \"Customer Surveys Creative Approaches\",\n            \"content\": \"Can you suggest some creative approaches to designing customer survey questions for [company/industry]?\",\n            \"category\": \"customerSuccess\"\n        },\n        {\n            \"id\": \"built_in_prompt_219\",\n            \"name\": \"Meeting Agenda Including Specific Topics and Relevant Items\",\n            \"content\": \"Can you create me a meeting agenda for [UPCOMING MEETING] on [DATE] at [TIME], including topics such as [LIST OF TOPICS] and any other items that may be relevant, such as [ADDITIONAL ITEMS]? Please make sure to allocate appropriate time for each topic.\",\n            \"category\": \"work\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_220\",\n            \"name\": \"Meeting Agenda Including Key Topics and Supporting Materials\",\n            \"content\": \"I need your help creating a comprehensive meeting agenda for our next meeting. Can you suggest key topics to include, such as [LIST OF POSSIBLE TOPICS], and any supporting materials we may need, such as [MATERIALS]? Please ensure that the agenda reflects our objectives for the meeting.\",\n            \"category\": \"work\"\n        },\n        {\n            \"id\": \"built_in_prompt_221\",\n            \"name\": \"Meeting Agenda Covering all Necessary Items\",\n            \"content\": \"I'm struggling to create an agenda for our [TYPE OF MEETING] meeting. Can you please create a detailed agenda that covers all necessary items and information, such as [LIST OF ITEMS AND INFORMATION]? Additionally, can you provide any tips or best practices for running an effective meeting?\",\n            \"category\": \"work\"\n        },\n        {\n            \"id\": \"built_in_prompt_222\",\n            \"name\": \"Meeting Agenda Creating an Agenda for Specific Departments\",\n            \"content\": \"Can you help me create a meeting agenda for a [TEAM/DEPARTMENT/DIVISION] meeting with [NUMBER] participants, including [SPECIFIC INDIVIDUALS OR DEPARTMENTS], [AND/OR OTHER CRITERIA]?\",\n            \"category\": \"work\"\n        },\n        {\n            \"id\": \"built_in_prompt_223\",\n            \"name\": \"Meeting Agenda Ensuring the Agenda is Inclusive of All Participants\",\n            \"content\": \"How can I make sure the meeting agenda is inclusive and addresses the needs of all participants, especially those with [SPECIFIC CHARACTERISTICS OR PERSPECTIVES], [WHILE STILL ACHIEVING SPECIFIC OUTCOMES SUCH AS DECISION-MAKING OR CONSENSUS-BUILDING]?\",\n            \"category\": \"work\"\n        },\n        {\n            \"id\": \"built_in_prompt_224\",\n            \"name\": \"Goal Setting Important Outcomes or Deliverables\",\n            \"content\": \"Crafting a goal for [specific project or task]: What are the most important outcomes or deliverables you hope to achieve?\",\n            \"category\": \"work\"\n        },\n        {\n            \"id\": \"built_in_prompt_225\",\n            \"name\": \"Goal Setting Tracking Progress\",\n            \"content\": \"I want to track progress towards [specific goal], what is the best way to do that?\",\n            \"category\": \"work\",\n            \"isFeatured\": true\n        },\n        {\n            \"id\": \"built_in_prompt_226\",\n            \"name\": \"Goal Setting Evaluating Effectiveness\",\n            \"content\": \"I want to evaluate the effectiveness of my [OKRs or goal], can you give me some suggestions on how to do that?\",\n            \"category\": \"work\"\n        },\n        {\n            \"id\": \"built_in_prompt_227\",\n            \"name\": \"Goal Setting Performance Targets\",\n            \"content\": \"In order to reach my goal of [insert goal], I need to set performance targets for myself. For example, I could aim to [insert action] [insert number] times per week. Can you help me with that?\",\n            \"category\": \"work\"\n        },\n        {\n            \"id\": \"built_in_prompt_228\",\n            \"name\": \"Goal Setting OKRs\",\n            \"content\": \"Write a list of specific, measurable, and attainable goals for [your company/project] using the OKR framework.\",\n            \"category\": \"work\",\n            \"isFeatured\": true\n        }\n    ]\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/assets/fonts/.gitkeep",
    "content": ""
  },
  {
    "path": "frontend/appflowy_flutter/assets/google_fonts/Poppins/OFL.txt",
    "content": "Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)\r\n\r\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\r\nThis license is copied below, and is also available with a FAQ at:\r\nhttp://scripts.sil.org/OFL\r\n\r\n\r\n-----------------------------------------------------------\r\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r\n-----------------------------------------------------------\r\n\r\nPREAMBLE\r\nThe goals of the Open Font License (OFL) are to stimulate worldwide\r\ndevelopment of collaborative font projects, to support the font creation\r\nefforts of academic and linguistic communities, and to provide a free and\r\nopen framework in which fonts may be shared and improved in partnership\r\nwith others.\r\n\r\nThe OFL allows the licensed fonts to be used, studied, modified and\r\nredistributed freely as long as they are not sold by themselves. The\r\nfonts, including any derivative works, can be bundled, embedded, \r\nredistributed and/or sold with any software provided that any reserved\r\nnames are not used by derivative works. The fonts and derivatives,\r\nhowever, cannot be released under any other type of license. The\r\nrequirement for fonts to remain under this license does not apply\r\nto any document created using the fonts or their derivatives.\r\n\r\nDEFINITIONS\r\n\"Font Software\" refers to the set of files released by the Copyright\r\nHolder(s) under this license and clearly marked as such. This may\r\ninclude source files, build scripts and documentation.\r\n\r\n\"Reserved Font Name\" refers to any names specified as such after the\r\ncopyright statement(s).\r\n\r\n\"Original Version\" refers to the collection of Font Software components as\r\ndistributed by the Copyright Holder(s).\r\n\r\n\"Modified Version\" refers to any derivative made by adding to, deleting,\r\nor substituting -- in part or in whole -- any of the components of the\r\nOriginal Version, by changing formats or by porting the Font Software to a\r\nnew environment.\r\n\r\n\"Author\" refers to any designer, engineer, programmer, technical\r\nwriter or other person who contributed to the Font Software.\r\n\r\nPERMISSION & CONDITIONS\r\nPermission is hereby granted, free of charge, to any person obtaining\r\na copy of the Font Software, to use, study, copy, merge, embed, modify,\r\nredistribute, and sell modified and unmodified copies of the Font\r\nSoftware, subject to the following conditions:\r\n\r\n1) Neither the Font Software nor any of its individual components,\r\nin Original or Modified Versions, may be sold by itself.\r\n\r\n2) Original or Modified Versions of the Font Software may be bundled,\r\nredistributed and/or sold with any software, provided that each copy\r\ncontains the above copyright notice and this license. These can be\r\nincluded either as stand-alone text files, human-readable headers or\r\nin the appropriate machine-readable metadata fields within text or\r\nbinary files as long as those fields can be easily viewed by the user.\r\n\r\n3) No Modified Version of the Font Software may use the Reserved Font\r\nName(s) unless explicit written permission is granted by the corresponding\r\nCopyright Holder. This restriction only applies to the primary font name as\r\npresented to the users.\r\n\r\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r\nSoftware shall not be used to promote, endorse or advertise any\r\nModified Version, except to acknowledge the contribution(s) of the\r\nCopyright Holder(s) and the Author(s) or with their explicit written\r\npermission.\r\n\r\n5) The Font Software, modified or unmodified, in part or in whole,\r\nmust be distributed entirely under this license, and must not be\r\ndistributed under any other license. The requirement for fonts to\r\nremain under this license does not apply to any document created\r\nusing the Font Software.\r\n\r\nTERMINATION\r\nThis license becomes null and void if any of the above conditions are\r\nnot met.\r\n\r\nDISCLAIMER\r\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\r\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r\nOTHER DEALINGS IN THE FONT SOFTWARE.\r\n"
  },
  {
    "path": "frontend/appflowy_flutter/assets/google_fonts/Roboto_Mono/LICENSE.txt",
    "content": "\r\n                                 Apache License\r\n                           Version 2.0, January 2004\r\n                        http://www.apache.org/licenses/\r\n\r\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\r\n\r\n   1. Definitions.\r\n\r\n      \"License\" shall mean the terms and conditions for use, reproduction,\r\n      and distribution as defined by Sections 1 through 9 of this document.\r\n\r\n      \"Licensor\" shall mean the copyright owner or entity authorized by\r\n      the copyright owner that is granting the License.\r\n\r\n      \"Legal Entity\" shall mean the union of the acting entity and all\r\n      other entities that control, are controlled by, or are under common\r\n      control with that entity. For the purposes of this definition,\r\n      \"control\" means (i) the power, direct or indirect, to cause the\r\n      direction or management of such entity, whether by contract or\r\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\r\n      outstanding shares, or (iii) beneficial ownership of such entity.\r\n\r\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\r\n      exercising permissions granted by this License.\r\n\r\n      \"Source\" form shall mean the preferred form for making modifications,\r\n      including but not limited to software source code, documentation\r\n      source, and configuration files.\r\n\r\n      \"Object\" form shall mean any form resulting from mechanical\r\n      transformation or translation of a Source form, including but\r\n      not limited to compiled object code, generated documentation,\r\n      and conversions to other media types.\r\n\r\n      \"Work\" shall mean the work of authorship, whether in Source or\r\n      Object form, made available under the License, as indicated by a\r\n      copyright notice that is included in or attached to the work\r\n      (an example is provided in the Appendix below).\r\n\r\n      \"Derivative Works\" shall mean any work, whether in Source or Object\r\n      form, that is based on (or derived from) the Work and for which the\r\n      editorial revisions, annotations, elaborations, or other modifications\r\n      represent, as a whole, an original work of authorship. For the purposes\r\n      of this License, Derivative Works shall not include works that remain\r\n      separable from, or merely link (or bind by name) to the interfaces of,\r\n      the Work and Derivative Works thereof.\r\n\r\n      \"Contribution\" shall mean any work of authorship, including\r\n      the original version of the Work and any modifications or additions\r\n      to that Work or Derivative Works thereof, that is intentionally\r\n      submitted to Licensor for inclusion in the Work by the copyright owner\r\n      or by an individual or Legal Entity authorized to submit on behalf of\r\n      the copyright owner. For the purposes of this definition, \"submitted\"\r\n      means any form of electronic, verbal, or written communication sent\r\n      to the Licensor or its representatives, including but not limited to\r\n      communication on electronic mailing lists, source code control systems,\r\n      and issue tracking systems that are managed by, or on behalf of, the\r\n      Licensor for the purpose of discussing and improving the Work, but\r\n      excluding communication that is conspicuously marked or otherwise\r\n      designated in writing by the copyright owner as \"Not a Contribution.\"\r\n\r\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\r\n      on behalf of whom a Contribution has been received by Licensor and\r\n      subsequently incorporated within the Work.\r\n\r\n   2. Grant of Copyright License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      copyright license to reproduce, prepare Derivative Works of,\r\n      publicly display, publicly perform, sublicense, and distribute the\r\n      Work and such Derivative Works in Source or Object form.\r\n\r\n   3. Grant of Patent License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      (except as stated in this section) patent license to make, have made,\r\n      use, offer to sell, sell, import, and otherwise transfer the Work,\r\n      where such license applies only to those patent claims licensable\r\n      by such Contributor that are necessarily infringed by their\r\n      Contribution(s) alone or by combination of their Contribution(s)\r\n      with the Work to which such Contribution(s) was submitted. If You\r\n      institute patent litigation against any entity (including a\r\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\r\n      or a Contribution incorporated within the Work constitutes direct\r\n      or contributory patent infringement, then any patent licenses\r\n      granted to You under this License for that Work shall terminate\r\n      as of the date such litigation is filed.\r\n\r\n   4. Redistribution. You may reproduce and distribute copies of the\r\n      Work or Derivative Works thereof in any medium, with or without\r\n      modifications, and in Source or Object form, provided that You\r\n      meet the following conditions:\r\n\r\n      (a) You must give any other recipients of the Work or\r\n          Derivative Works a copy of this License; and\r\n\r\n      (b) You must cause any modified files to carry prominent notices\r\n          stating that You changed the files; and\r\n\r\n      (c) You must retain, in the Source form of any Derivative Works\r\n          that You distribute, all copyright, patent, trademark, and\r\n          attribution notices from the Source form of the Work,\r\n          excluding those notices that do not pertain to any part of\r\n          the Derivative Works; and\r\n\r\n      (d) If the Work includes a \"NOTICE\" text file as part of its\r\n          distribution, then any Derivative Works that You distribute must\r\n          include a readable copy of the attribution notices contained\r\n          within such NOTICE file, excluding those notices that do not\r\n          pertain to any part of the Derivative Works, in at least one\r\n          of the following places: within a NOTICE text file distributed\r\n          as part of the Derivative Works; within the Source form or\r\n          documentation, if provided along with the Derivative Works; or,\r\n          within a display generated by the Derivative Works, if and\r\n          wherever such third-party notices normally appear. The contents\r\n          of the NOTICE file are for informational purposes only and\r\n          do not modify the License. You may add Your own attribution\r\n          notices within Derivative Works that You distribute, alongside\r\n          or as an addendum to the NOTICE text from the Work, provided\r\n          that such additional attribution notices cannot be construed\r\n          as modifying the License.\r\n\r\n      You may add Your own copyright statement to Your modifications and\r\n      may provide additional or different license terms and conditions\r\n      for use, reproduction, or distribution of Your modifications, or\r\n      for any such Derivative Works as a whole, provided Your use,\r\n      reproduction, and distribution of the Work otherwise complies with\r\n      the conditions stated in this License.\r\n\r\n   5. Submission of Contributions. Unless You explicitly state otherwise,\r\n      any Contribution intentionally submitted for inclusion in the Work\r\n      by You to the Licensor shall be under the terms and conditions of\r\n      this License, without any additional terms or conditions.\r\n      Notwithstanding the above, nothing herein shall supersede or modify\r\n      the terms of any separate license agreement you may have executed\r\n      with Licensor regarding such Contributions.\r\n\r\n   6. Trademarks. This License does not grant permission to use the trade\r\n      names, trademarks, service marks, or product names of the Licensor,\r\n      except as required for reasonable and customary use in describing the\r\n      origin of the Work and reproducing the content of the NOTICE file.\r\n\r\n   7. Disclaimer of Warranty. Unless required by applicable law or\r\n      agreed to in writing, Licensor provides the Work (and each\r\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\r\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\r\n      implied, including, without limitation, any warranties or conditions\r\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\r\n      PARTICULAR PURPOSE. You are solely responsible for determining the\r\n      appropriateness of using or redistributing the Work and assume any\r\n      risks associated with Your exercise of permissions under this License.\r\n\r\n   8. Limitation of Liability. In no event and under no legal theory,\r\n      whether in tort (including negligence), contract, or otherwise,\r\n      unless required by applicable law (such as deliberate and grossly\r\n      negligent acts) or agreed to in writing, shall any Contributor be\r\n      liable to You for damages, including any direct, indirect, special,\r\n      incidental, or consequential damages of any character arising as a\r\n      result of this License or out of the use or inability to use the\r\n      Work (including but not limited to damages for loss of goodwill,\r\n      work stoppage, computer failure or malfunction, or any and all\r\n      other commercial damages or losses), even if such Contributor\r\n      has been advised of the possibility of such damages.\r\n\r\n   9. Accepting Warranty or Additional Liability. While redistributing\r\n      the Work or Derivative Works thereof, You may choose to offer,\r\n      and charge a fee for, acceptance of support, warranty, indemnity,\r\n      or other liability obligations and/or rights consistent with this\r\n      License. However, in accepting such obligations, You may act only\r\n      on Your own behalf and on Your sole responsibility, not on behalf\r\n      of any other Contributor, and only if You agree to indemnify,\r\n      defend, and hold each Contributor harmless for any liability\r\n      incurred by, or claims asserted against, such Contributor by reason\r\n      of your accepting any such warranty or additional liability.\r\n\r\n   END OF TERMS AND CONDITIONS\r\n\r\n   APPENDIX: How to apply the Apache License to your work.\r\n\r\n      To apply the Apache License to your work, attach the following\r\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\r\n      replaced with your own identifying information. (Don't include\r\n      the brackets!)  The text should be enclosed in the appropriate\r\n      comment syntax for the file format. We also recommend that a\r\n      file or class name and description of purpose be included on the\r\n      same \"printed page\" as the copyright notice for easier\r\n      identification within third-party archives.\r\n\r\n   Copyright [yyyy] [name of copyright owner]\r\n\r\n   Licensed under the Apache License, Version 2.0 (the \"License\");\r\n   you may not use this file except in compliance with the License.\r\n   You may obtain a copy of the License at\r\n\r\n       http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n   Unless required by applicable law or agreed to in writing, software\r\n   distributed under the License is distributed on an \"AS IS\" BASIS,\r\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n   See the License for the specific language governing permissions and\r\n   limitations under the License.\r\n"
  },
  {
    "path": "frontend/appflowy_flutter/assets/icons/icons.json",
    "content": "{ \"artificial_intelligence\": [ { \"name\": \"ai-chip-spark\", \"keywords\": [ \"chip\", \"processor\", \"artificial\", \"intelligence\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189544)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.71468 0.0400391C9.1289 0.0400391 9.46468 0.375825 9.46468 0.790039V1.97754H10.4999C11.3283 1.97754 11.9999 2.64911 11.9999 3.47754V4.51367H13.2186C13.6328 4.51367 13.9686 4.84946 13.9686 5.26367C13.9686 5.67789 13.6328 6.01367 13.2186 6.01367H11.9999V7.94324H13.2186C13.6328 7.94324 13.9686 8.27902 13.9686 8.69324C13.9686 9.10745 13.6328 9.44324 13.2186 9.44324H11.9999V10.4775C11.9999 11.306 11.3283 11.9775 10.4999 11.9775H9.46468V13.2099C9.46468 13.6241 9.1289 13.9599 8.71468 13.9599C8.30047 13.9599 7.96468 13.6241 7.96468 13.2099V11.9775H6.03524V13.2099C6.03524 13.6241 5.69945 13.9599 5.28524 13.9599C4.87103 13.9599 4.53524 13.6241 4.53524 13.2099V11.9775H3.49984C2.67141 11.9775 1.99984 11.306 1.99984 10.4775V9.44324H0.783203C0.368989 9.44324 0.0332031 9.10745 0.0332031 8.69324C0.0332031 8.27902 0.368989 7.94324 0.783203 7.94324H1.99984V6.01367H0.783203C0.368989 6.01367 0.0332031 5.67789 0.0332031 5.26367C0.0332031 4.84946 0.368989 4.51367 0.783203 4.51367H1.99984V3.47754C1.99984 2.64911 2.67141 1.97754 3.49984 1.97754H4.53524V0.790039C4.53524 0.375825 4.87103 0.0400391 5.28524 0.0400391C5.69945 0.0400391 6.03524 0.375825 6.03524 0.790039V1.97754H7.96468V0.790039C7.96468 0.375825 8.30047 0.0400391 8.71468 0.0400391ZM6.24357 4.5682C6.41951 3.76446 7.56392 3.75946 7.74688 4.56163L7.75499 4.59732C7.76038 4.62113 7.76543 4.64342 7.7708 4.66628C7.96264 5.48335 8.62389 6.10768 9.45164 6.25169C10.2908 6.39768 10.2908 7.60232 9.45164 7.74831C8.61948 7.89308 7.95559 8.52332 7.76776 9.34682L7.74688 9.43837C7.56392 10.2405 6.41951 10.2355 6.24357 9.4318L6.22638 9.35325C6.04547 8.52684 5.38257 7.89201 4.54911 7.74701C3.71142 7.60128 3.71141 6.39872 4.54911 6.25299C5.37967 6.10849 6.04086 5.47757 6.22447 4.65539L6.23707 4.598L6.24357 4.5682Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189544\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ai-cloud-spark\", \"keywords\": [ \"cloud\", \"internet\", \"server\", \"network\", \"artificial\", \"intelligence\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n    <g clip-path=\\\"url(#clip0_1068_189529)\\\">\\n        <path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\"\\n              d=\\\"M7.88381 0.165291C6.89196 0.0450664 5.88886 0.268505 5.04181 0.798345C4.25349 1.29145 3.64467 2.02219 3.30135 2.88247C2.87262 2.92977 2.45508 3.05187 2.06781 3.24362C1.63324 3.45878 1.24531 3.75744 0.92616 4.12253C0.281611 4.85986 -0.0436335 5.82304 0.0219743 6.80017C0.087582 7.77731 0.538668 8.68836 1.276 9.33291C1.52925 9.55429 1.80913 9.738 2.10708 9.88114C2.23853 9.09496 2.79956 8.38069 3.79016 8.20836C4.35848 8.10949 4.81104 7.67811 4.93736 7.11585L4.96023 7.01146C5.43591 4.83845 8.53 4.82492 9.02466 6.9937L9.03475 7.03814C9.04166 7.06865 9.047 7.09221 9.05244 7.11538C9.18449 7.6778 9.63973 8.10763 10.2096 8.20676C11.2609 8.38966 11.8287 9.18251 11.9131 10.0243C12.2792 9.884 12.6197 9.67707 12.9162 9.41068C13.5042 8.88231 13.878 8.15668 13.9669 7.37119C14.0558 6.58569 13.8536 5.79486 13.3985 5.14845C12.9811 4.55545 12.3775 4.12175 11.6866 3.91417C11.5613 2.99088 11.1436 2.12994 10.4923 1.45901C9.79633 0.742138 8.87567 0.285516 7.88381 0.165291ZM6.18132 7.27876C6.37146 6.41016 7.60823 6.40476 7.80595 7.27166L7.81577 7.3149C7.8225 7.34462 7.82879 7.37239 7.83553 7.4011C8.08204 8.451 8.93171 9.25323 9.99531 9.43826C10.9004 9.59573 10.9004 10.8951 9.99531 11.0525C8.92604 11.2386 8.07299 12.0484 7.83164 13.1065L7.80595 13.2191C7.60823 14.086 6.37146 14.0806 6.18132 13.212L6.16017 13.1154C5.92765 12.0532 5.07564 11.2373 4.0044 11.0509C3.10108 10.8938 3.10107 9.59701 4.0044 9.43986C5.07191 9.25414 5.92172 8.44323 6.15771 7.38648L6.17343 7.31494L6.18132 7.27876Z\\\"\\n              fill=\\\"black\\\"/>\\n    </g>\\n    <defs>\\n        <clipPath id=\\\"clip0_1068_189529\\\">\\n            <rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n        </clipPath>\\n    </defs>\\n</svg>\\n\" }, { \"name\": \"ai-edit-spark\", \"keywords\": [ \"change\", \"edit\", \"modify\", \"pencil\", \"write\", \"writing\", \"artificial\", \"intelligence\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189535)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.85637 0.654372C3.04651 -0.214226 4.28328 -0.219632 4.481 0.647271L4.49082 0.690505C4.49756 0.720231 4.50384 0.747995 4.51058 0.776709C4.75709 1.82661 5.60676 2.62884 6.67036 2.81387C7.57548 2.97134 7.57548 4.27068 6.67036 4.42815C5.60109 4.61417 4.74804 5.42398 4.50669 6.48214L4.481 6.59475C4.28328 7.46166 3.04651 7.45624 2.85637 6.58765L2.83522 6.49102C2.6027 5.42885 1.75069 4.61292 0.679451 4.42655C-0.223876 4.2694 -0.223883 2.97262 0.679451 2.81547C1.74696 2.62975 2.59677 1.81884 2.83276 0.762091L2.84848 0.690547L2.85637 0.654372ZM10.6352 3.61645C10.8194 3.53895 11.0172 3.49902 11.217 3.49902C11.4168 3.49902 11.6146 3.53895 11.7988 3.61645C11.9823 3.69369 12.1486 3.80669 12.288 3.94888L12.2895 3.95034L13.5557 5.22652C13.6955 5.36563 13.8066 5.53095 13.8824 5.71303C13.9586 5.89582 13.9978 6.09188 13.9978 6.2899C13.9978 6.48792 13.9586 6.68398 13.8824 6.86676C13.8065 7.0489 13.6955 7.21427 13.5556 7.35341L13.5541 7.3549L7.35665 13.5923C7.2733 13.6762 7.16293 13.7278 7.04513 13.738L4.04513 13.998C3.89803 14.0108 3.75281 13.9579 3.6484 13.8535C3.544 13.749 3.49108 13.6038 3.50382 13.4567L3.76382 10.4567C3.77403 10.3389 3.82566 10.2286 3.90954 10.1452L10.1473 3.94751C10.2864 3.80597 10.4522 3.69344 10.6352 3.61645Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189535\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ai-email-generator-spark\", \"keywords\": [ \"mail\", \"envelope\", \"inbox\", \"artificial\", \"intelligence\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189541)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.62026 0.0512695H10.9316C11.7887 0.0512695 12.4835 0.746078 12.4835 1.60317V1.68034L6.27595 4.78414L0.0683594 1.68035V1.60317C0.0683594 0.746078 0.763168 0.0512695 1.62026 0.0512695ZM0.0683594 3.07789V7.81076C0.0683594 8.66785 0.763168 9.36266 1.62026 9.36266H6.54725C6.80422 9.07078 7.16884 8.8528 7.64113 8.77063C8.15658 8.68096 8.56704 8.28972 8.68161 7.77976L8.70235 7.68508C9.13378 5.71422 11.94 5.70195 12.3887 7.66897L12.3978 7.70928C12.4041 7.73695 12.4089 7.75832 12.4139 7.77934C12.4295 7.84583 12.4501 7.91029 12.4752 7.9723C12.4807 7.9192 12.4835 7.86531 12.4835 7.81076V3.07788L6.55545 6.04193C6.3795 6.12991 6.17239 6.12991 5.99644 6.04193L0.0683594 3.07789ZM9.76442 7.91757C9.9475 7.08124 11.1383 7.07603 11.3287 7.91073L11.3376 7.95019C11.3436 7.97709 11.3494 8.00194 11.3555 8.02784C11.5748 8.96193 12.3307 9.67568 13.277 9.8403C14.1493 9.99206 14.1493 11.2443 13.277 11.3961C12.3257 11.5616 11.5667 12.282 11.352 13.2235L11.3287 13.3256C11.1383 14.1603 9.9475 14.1551 9.76442 13.3188L9.74524 13.2311C9.5384 12.2862 8.78045 11.5604 7.82749 11.3946C6.9568 11.2431 6.9568 9.99323 7.82749 9.84175C8.77713 9.67654 9.53312 8.95515 9.74306 8.01508L9.75722 7.95059L9.76442 7.91757Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189541\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ai-gaming-spark\", \"keywords\": [ \"remote\", \"control\", \"controller\", \"technology\", \"artificial\", \"intelligence\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189562)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.2889 0.770414C9.47222 -0.0670114 10.6646 -0.0722238 10.8552 0.763568L10.8642 0.803181C10.8703 0.830101 10.876 0.855164 10.8821 0.881094C11.1023 1.8191 11.8614 2.53584 12.8117 2.70116C13.6851 2.85311 13.6851 4.10695 12.8117 4.2589C11.8564 4.4251 11.0942 5.14861 10.8786 6.094L10.8552 6.19649C10.6646 7.03229 9.47222 7.02707 9.2889 6.18964L9.26965 6.1017C9.06194 5.15283 8.30081 4.42393 7.34384 4.25744C6.47205 4.10578 6.47205 2.85428 7.34384 2.70261C8.29748 2.53671 9.05664 1.81229 9.26746 0.868267L9.28167 0.803553L9.2889 0.770414ZM7.15656 5.33399C6.89004 5.28762 6.65768 5.19815 6.4595 5.07662H3.94145C2.41281 5.07662 1.12861 6.22603 0.959804 7.74533L0.558928 11.3532C0.41107 12.6839 1.45273 13.8478 2.79165 13.8478C3.64254 13.8478 4.42041 13.367 4.80094 12.6059L5.1546 11.8986H8.07831L8.43198 12.6059C8.81251 13.367 9.59038 13.8478 10.4413 13.8478C11.7802 13.8478 12.8218 12.6839 12.674 11.3532L12.2731 7.74533C12.2257 7.31878 12.0904 6.92139 11.8864 6.57041C11.3442 8.41179 8.6446 8.35624 8.22147 6.42331L8.20222 6.33537C8.08944 5.82016 7.67616 5.42438 7.15656 5.33399ZM4.68321 7.51305C4.68321 7.16787 4.40339 6.88805 4.05821 6.88805C3.71304 6.88805 3.43321 7.16787 3.43321 7.51305V8.02512H2.92129C2.57612 8.02512 2.29629 8.30495 2.29629 8.65012C2.29629 8.9953 2.57612 9.27512 2.92129 9.27512H3.43321V9.78704C3.43321 10.1322 3.71304 10.412 4.05821 10.412C4.40339 10.412 4.68321 10.1322 4.68321 9.78704V9.27512H5.19529C5.54047 9.27513 5.82029 8.9953 5.82029 8.65012C5.82029 8.30495 5.54047 8.02512 5.19529 8.02512H4.68321V7.51305ZM9.43357 9.25923C9.43357 9.59563 9.16087 9.86834 8.82447 9.86834C8.48807 9.86834 8.21536 9.59563 8.21536 9.25923C8.21536 8.92283 8.48807 8.65012 8.82447 8.65012C9.16087 8.65012 9.43357 8.92283 9.43357 9.25923Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189562\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ai-generate-landscape-image-spark\", \"keywords\": [ \"picture\", \"photography\", \"photo\", \"image\", \"artificial\", \"intelligence\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189556)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.73377 10.5064C0.790259 10.5064 0.0253906 9.74157 0.0253906 8.79806V1.7234C0.0253906 0.779884 0.790258 0.0150146 1.73377 0.0150146H9.20934C10.1528 0.0150146 10.9177 0.779883 10.9177 1.7234V5.5825C9.85424 5.2672 8.55639 5.78485 8.26193 7.13002L8.23906 7.23441C8.12212 7.75488 7.72564 8.1632 7.21657 8.29944L6.84942 7.81212C5.65775 6.23042 3.67743 5.45387 1.72825 5.80394L1.52536 5.84038V8.75646C1.52536 8.89453 1.63729 9.00646 1.77536 9.00646H5.86263C5.50855 9.42383 5.34873 9.97147 5.38318 10.5064H1.73377ZM9.25336 3.16205C9.25336 3.91928 8.6395 4.53314 7.88226 4.53314C7.12503 4.53314 6.51117 3.91928 6.51117 3.16205C6.51117 2.40481 7.12503 1.79095 7.88226 1.79095C8.6395 1.79095 9.25336 2.40481 9.25336 3.16205Z\\\" fill=\\\"black\\\"/>\\n<path d=\\\"M7.3918 10.6769C7.04087 10.6158 7.04087 10.112 7.3918 10.051C8.65873 9.83058 9.6673 8.86817 9.94739 7.61399L9.96401 7.53828L9.97145 7.50421C10.0474 7.15738 10.5412 7.15522 10.6202 7.50138L10.6255 7.52484C10.6337 7.56122 10.6419 7.59761 10.6505 7.63391C10.9427 8.8788 11.9502 9.83 13.2113 10.0494C13.564 10.1108 13.564 10.6171 13.2113 10.6785C11.9435 10.899 10.932 11.8592 10.6458 13.1139L10.6202 13.2265C10.5412 13.5726 10.0474 13.5705 9.97145 13.2236L9.9503 13.127C9.67434 11.8664 8.66316 10.8981 7.3918 10.6769Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189556\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ai-generate-music-spark\", \"keywords\": [ \"music\", \"audio\", \"note\", \"entertainment\", \"artificial\", \"intelligence\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189568)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.81278 0.6334C9.9959 -0.203147 11.1871 -0.208354 11.3775 0.626561L11.3865 0.666074C11.3926 0.69299 11.3982 0.717837 11.4043 0.743751C11.6238 1.67861 12.3803 2.39294 13.3274 2.5577C14.1999 2.7095 14.1999 3.96205 13.3274 4.11385C12.3753 4.27949 11.6157 5.00057 11.4008 5.94279L11.3775 6.04499C11.1871 6.87991 9.9959 6.87469 9.81278 6.03815L9.79358 5.95045C9.58657 5.00477 8.82799 4.27832 7.87424 4.1124C7.00335 3.96088 7.00334 2.71067 7.87424 2.55915C8.82467 2.39381 9.58129 1.67183 9.79139 0.730975L9.80557 0.666425L9.81278 0.6334ZM3.74081 4.23859L6.14658 3.57098C6.23504 4.32137 6.74876 5.02122 7.68771 5.18457C8.20587 5.27472 8.618 5.66939 8.73047 6.18316L8.74966 6.27086C8.75385 6.29001 8.75827 6.30897 8.7629 6.32775L4.39526 7.54098V11.7936L4.39531 11.8089C4.39531 13.0172 3.41576 13.9968 2.20742 13.9968C0.999084 13.9968 0.0195312 13.0172 0.0195312 11.8089C0.0195312 10.6005 0.999084 9.62098 2.20742 9.62098C2.44772 9.62098 2.67897 9.65972 2.89526 9.73129V6.97092V5.52396V5.3433V5.33909C2.89667 5.08786 2.98051 4.84404 3.13389 4.64507C3.28653 4.44705 3.49965 4.30433 3.74081 4.23859ZM11.5475 7.51205C11.052 7.79034 10.4405 7.831 9.9109 7.6349C9.73475 7.58917 9.54998 7.56483 9.35952 7.56483C8.15118 7.56483 7.17163 8.54438 7.17163 9.75272C7.17163 10.9611 8.15118 11.9406 9.35952 11.9406C10.5216 11.9406 11.472 11.0348 11.5432 9.89069C11.546 9.86401 11.5475 9.83691 11.5475 9.80946V7.51205Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189568\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ai-generate-portrait-image-spark\", \"keywords\": [ \"picture\", \"photography\", \"photo\", \"image\", \"artificial\", \"intelligence\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189547)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.53125 0.00402832C1.13342 0.00402832 0.751895 0.162064 0.47059 0.443368C0.189286 0.724673 0.03125 1.1062 0.03125 1.50403V9.50403C0.03125 9.90185 0.189286 10.2834 0.47059 10.5647C0.751894 10.846 1.13342 11.004 1.53125 11.004H5.46229C5.27732 10.3876 5.37222 9.6894 5.74699 9.15674H2.87063C2.70736 9.15674 2.57501 9.02438 2.57501 8.86112C2.57501 7.22845 3.89855 5.90491 5.53122 5.90491C6.6697 5.90491 7.65786 6.54847 8.15177 7.49165C8.18522 7.41307 8.21189 7.33079 8.23106 7.24548L8.25393 7.1411C8.55971 5.74422 9.9475 5.23971 11.0313 5.63363V1.50403C11.0313 1.1062 10.8732 0.724673 10.5919 0.443368C10.3106 0.162064 9.92907 0.00402832 9.53125 0.00402832H1.53125ZM5.53122 5.02293C6.51082 5.02293 7.30494 4.22881 7.30494 3.2492C7.30494 2.2696 6.51082 1.47548 5.53122 1.47548C4.55162 1.47548 3.75749 2.2696 3.75749 3.2492C3.75749 4.22881 4.55162 5.02293 5.53122 5.02293ZM9.47501 7.4084C9.66516 6.5398 10.902 6.5344 11.0997 7.4013L11.1095 7.44453C11.1162 7.47417 11.1225 7.5021 11.1293 7.53074C11.3758 8.58063 12.2254 9.38286 13.289 9.5679C14.1942 9.72537 14.1942 11.0247 13.289 11.1822C12.2198 11.3682 11.3667 12.178 11.1254 13.2362L11.0997 13.3488C10.902 14.2157 9.66515 14.2103 9.47501 13.3417L9.45386 13.245C9.22135 12.1829 8.36933 11.3669 7.2981 11.1806C6.39477 11.0234 6.39477 9.72665 7.2981 9.5695C8.3656 9.38378 9.21542 8.57287 9.45141 7.51612L9.46712 7.44457L9.47501 7.4084Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189547\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ai-generate-variation-spark\", \"keywords\": [ \"module\", \"application\", \"artificial\", \"intelligence\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189532)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.91814 0.731127C10.0968 -0.0852602 11.2593 -0.0903417 11.4451 0.724453L11.4535 0.761616C11.4591 0.786719 11.4645 0.809825 11.4701 0.83394C11.6726 1.69644 12.3707 2.35548 13.2444 2.50749C14.0964 2.65572 14.0964 3.87884 13.2444 4.02707C12.366 4.17989 11.6652 4.84516 11.4669 5.71445L11.4451 5.81011C11.2593 6.62491 10.0968 6.61982 9.91814 5.80344L9.90017 5.72136C9.7092 4.84895 9.0094 4.17879 8.12955 4.02572C7.27905 3.87775 7.27904 2.65681 8.12955 2.50885C9.00633 2.35631 9.70432 1.69027 9.89815 0.822322L9.91137 0.762143L9.91814 0.731127ZM1.10418 0.170359C0.564475 0.170359 0.126953 0.60788 0.126953 1.14759V5.30082C0.126953 5.84053 0.564475 6.27805 1.10418 6.27805H5.25742C5.79713 6.27805 6.23465 5.84053 6.23465 5.30082V1.14759C6.23465 0.60788 5.79713 0.170359 5.25742 0.170359H1.10418ZM0.126953 8.74214C0.126953 8.20243 0.564475 7.7649 1.10418 7.7649H5.25742C5.79713 7.7649 6.23465 8.20243 6.23465 8.74214V12.8954C6.23465 13.4351 5.79713 13.8726 5.25742 13.8726H1.10418C0.564475 13.8726 0.126953 13.4351 0.126953 12.8954V8.74214ZM7.72149 8.74214C7.72149 8.20243 8.15901 7.7649 8.69872 7.7649H12.8519C13.3917 7.7649 13.8292 8.20243 13.8292 8.74214V12.8954C13.8292 13.4351 13.3917 13.8726 12.8519 13.8726H8.69872C8.15901 13.8726 7.72149 13.4351 7.72149 12.8954V8.74214Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189532\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ai-navigation-spark\", \"keywords\": [ \"map\", \"location\", \"direction\", \"travel\", \"artificial\", \"intelligence\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189550)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.95758 0.658957C10.1371 -0.161122 11.3048 -0.166226 11.4915 0.652253L11.5 0.689857C11.5058 0.715279 11.5111 0.738717 11.5168 0.76315C11.7224 1.6389 12.4312 2.30807 13.3183 2.46241C14.1741 2.6113 14.1741 3.8398 13.3183 3.98869C12.4264 4.14385 11.7149 4.81934 11.5135 5.70199L11.4915 5.79885C11.3048 6.61733 10.1371 6.61222 9.95758 5.79214L9.93939 5.70903C9.74548 4.82321 9.03492 4.14274 8.14153 3.98731C7.2873 3.8387 7.28729 2.6124 8.14153 2.46379C9.0318 2.3089 9.74053 1.63262 9.93734 0.75132L9.95073 0.69036L9.95758 0.658957ZM7.96919 4.97763C6.31885 4.69052 6.05472 2.65454 7.17687 1.80769C6.59157 1.58233 5.95572 1.4588 5.29102 1.4588C2.39152 1.4588 0.0410156 3.80931 0.0410156 6.7088C0.0410156 8.49682 0.993353 10.2936 2.05776 11.6161C2.5954 12.284 3.17925 12.8534 3.72099 13.2608C3.99167 13.4643 4.26002 13.6336 4.5139 13.7539C4.76038 13.8707 5.02931 13.9588 5.29102 13.9588C5.55272 13.9588 5.82165 13.8707 6.06813 13.7539C6.32201 13.6336 6.59036 13.4643 6.86104 13.2608C7.40278 12.8534 7.98663 12.284 8.52427 11.6161C9.45118 10.4644 10.2931 8.95312 10.4951 7.40113C9.80485 7.31671 9.16041 6.85142 8.97558 6.0071L8.95739 5.92398C8.85081 5.43709 8.46024 5.06306 7.96919 4.97763ZM5.29102 8.4588C6.25751 8.4588 7.04102 7.6753 7.04102 6.7088C7.04102 5.7423 6.25751 4.9588 5.29102 4.9588C4.32452 4.9588 3.54102 5.7423 3.54102 6.7088C3.54102 7.6753 4.32452 8.4588 5.29102 8.4588Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189550\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ai-network-spark\", \"keywords\": [ \"globe\", \"internet\", \"world\", \"artificial\", \"intelligence\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189559)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.81502 0.666197C9.9981 -0.170135 11.189 -0.175341 11.3793 0.65936L11.3883 0.698818C11.3944 0.725719 11.4 0.75057 11.4061 0.776468C11.6254 1.71056 12.3814 2.4243 13.3276 2.58893C14.2 2.74069 14.2 3.99292 13.3276 4.14468C12.3763 4.31019 11.6174 5.03067 11.4026 5.97212L11.3793 6.07425C11.189 6.90896 9.99809 6.90374 9.81502 6.06741L9.79583 5.97977C9.58899 5.03487 8.83104 4.30902 7.87808 4.14323C7.0074 3.99176 7.00739 2.74185 7.87808 2.59038C8.82772 2.42516 9.58371 1.70378 9.79365 0.763705L9.80781 0.699221L9.81502 0.666197ZM7.69172 5.21435C5.81589 4.88801 5.63841 2.41899 7.1594 1.68728C7.0236 1.44232 6.87994 1.20164 6.72859 0.965667C6.64978 0.962837 6.57062 0.961411 6.49113 0.961411C6.41169 0.961411 6.33258 0.962835 6.25382 0.965661C5.12844 2.72022 4.42868 4.7349 4.22016 6.83759H8.76225C8.7325 6.53763 8.69276 6.23947 8.64321 5.9436C8.46716 5.56473 8.11481 5.28796 7.69172 5.21435ZM0.0195433 6.83765C0.274572 4.16392 2.14899 1.96256 4.65082 1.22555C3.72044 2.95001 3.14382 4.86059 2.96455 6.83759H0.0284221L0.0195433 6.83765ZM0.0284221 8.08759H2.96451C3.1437 10.0646 3.72025 11.9752 4.65057 13.6997C2.14882 12.9626 0.274503 10.7612 0.0195312 8.08752L0.0284221 8.08759ZM4.22011 8.08759C4.42855 10.1903 5.12824 12.205 6.25357 13.9596C6.3324 13.9625 6.4116 13.9639 6.49113 13.9639C6.57071 13.9639 6.64996 13.9625 6.72885 13.9596C7.85417 12.205 8.55386 10.1903 8.7623 8.08759H4.22011ZM12.9628 8.08753C12.7078 10.7612 10.8336 12.9625 8.33187 13.6996C9.26217 11.9751 9.83871 10.0646 10.0179 8.08759H12.9542L12.9628 8.08753Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189559\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ai-prompt-spark\", \"keywords\": [ \"app\", \"code\", \"apps\", \"window\", \"website\", \"web\", \"artificial\", \"intelligence\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189565)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.72096 0.656606C9.90611 -0.189194 11.1104 -0.194459 11.3029 0.649692L11.3122 0.690284C11.3184 0.718022 11.3243 0.743687 11.3305 0.770418C11.5578 1.73849 12.3413 2.4782 13.322 2.64882C14.2039 2.80225 14.2039 4.06831 13.322 4.22174C12.3361 4.39327 11.5495 5.13997 11.3269 6.11566L11.3029 6.22087C11.1104 7.06502 9.90611 7.05975 9.72096 6.21395L9.7012 6.12368C9.48682 5.14436 8.70127 4.39208 7.7136 4.22025C6.83334 4.06711 6.83333 2.80345 7.7136 2.65031C8.69783 2.47908 9.48135 1.73142 9.69894 0.757111L9.71355 0.690565L9.72096 0.656606ZM1.76562 2.48053H6.15635C5.58627 3.53175 6.04052 5.08121 7.51904 5.33843C8.00577 5.42311 8.40161 5.76882 8.55511 6.23053H1.51562V12.2305C1.51562 12.3686 1.62755 12.4805 1.76562 12.4805H10.7656C10.9037 12.4805 11.0156 12.3686 11.0156 12.2305V7.92384C11.6597 7.76082 12.226 7.27774 12.4095 6.47325L12.4335 6.36805C12.4531 6.2821 12.4807 6.19943 12.5156 6.12083V12.2305C12.5156 13.197 11.7321 13.9805 10.7656 13.9805H1.76562C0.799126 13.9805 0.015625 13.197 0.015625 12.2305V5.48053V4.23053C0.015625 3.26404 0.799127 2.48053 1.76562 2.48053ZM2.72871 7.31716C2.97279 7.07308 3.36852 7.07308 3.61259 7.31716L5.11259 8.81716C5.35667 9.06123 5.35667 9.45696 5.11259 9.70104L3.61259 11.201C3.36852 11.4451 2.97279 11.4451 2.72871 11.201C2.48463 10.957 2.48463 10.5612 2.72871 10.3172L3.78677 9.2591L2.72871 8.20104C2.48463 7.95696 2.48463 7.56123 2.72871 7.31716ZM5.54565 10.7591C5.54565 10.4139 5.82547 10.1341 6.17065 10.1341H7.67065C8.01583 10.1341 8.29565 10.4139 8.29565 10.7591C8.29565 11.1043 8.01583 11.3841 7.67065 11.3841H6.17065C5.82547 11.3841 5.54565 11.1043 5.54565 10.7591Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189565\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ai-redo-spark\", \"keywords\": [ \"arrow\", \"refresh\", \"sync\", \"synchronize\", \"artificial\", \"intelligence\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189538)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.2127 2.53752C8.91903 1.6074 7.23449 1.24494 5.57696 1.68908C2.78152 2.43811 1.06874 5.21231 1.5948 8.0098C1.67134 8.41688 1.4034 8.80893 0.996318 8.88548C0.58924 8.96203 0.197182 8.69408 0.120633 8.28701C-0.549251 4.72465 1.62993 1.19377 5.18873 0.240189C7.38469 -0.348217 9.61568 0.176234 11.2842 1.46596L11.8965 0.853736C12.2114 0.538754 12.75 0.761838 12.75 1.20729V3.50018C12.75 3.77633 12.5262 4.00018 12.25 4.00018H9.95712C9.51167 4.00018 9.28858 3.46161 9.60357 3.14663L10.2127 2.53752ZM13.0041 5.11487C13.4111 5.03832 13.8032 5.30626 13.8797 5.71334C14.5496 9.2757 12.3705 12.8066 8.81165 13.7602C6.61562 14.3486 4.38455 13.8241 2.71598 12.5343L2.10362 13.1466C1.78863 13.4616 1.25006 13.2385 1.25006 12.7931V10.5002C1.25006 10.224 1.47392 10.0002 1.75006 10.0002H4.04296C4.48841 10.0002 4.71149 10.5387 4.39651 10.8537L3.78752 11.4627C5.08119 12.3929 6.76582 12.7554 8.42342 12.3113C11.2189 11.5622 12.9316 8.78804 12.4056 5.99055C12.329 5.58347 12.597 5.19142 13.0041 5.11487ZM6.18098 4.03352C6.37112 3.16492 7.60789 3.15952 7.80562 4.02642L7.81543 4.06966C7.82216 4.09938 7.82845 4.12715 7.8352 4.15586C8.0817 5.20576 8.93138 6.00799 9.99498 6.19302C10.9001 6.35049 10.9001 7.64983 9.99498 7.8073C8.9257 7.99332 8.07265 8.80313 7.8313 9.86129L7.80562 9.9739C7.60789 10.8408 6.37112 10.8354 6.18098 9.9668L6.15983 9.87017C5.92731 8.808 5.0753 7.99207 4.00407 7.8057C3.10074 7.64855 3.10073 6.35177 4.00407 6.19462C5.07157 6.0089 5.92138 5.19799 6.15738 4.14124L6.17309 4.0697L6.18098 4.03352Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189538\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ai-science-spark\", \"keywords\": [ \"atom\", \"scientific\", \"experiment\", \"artificial\", \"intelligence\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189553)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.82494 0.638365C10.0081 -0.197968 11.1989 -0.203173 11.3893 0.631528L11.3982 0.670986C11.4043 0.697887 11.4099 0.722738 11.416 0.748636C11.6353 1.68273 12.3913 2.39647 13.3376 2.5611C14.2099 2.71285 14.2099 3.96509 13.3376 4.11685C12.3863 4.28236 11.6273 5.00284 11.4126 5.94428L11.3893 6.04642C11.1989 6.88112 10.0081 6.87591 9.82494 6.03958L9.80576 5.95194C9.59891 5.00704 8.84097 4.28119 7.888 4.1154C7.01732 3.96393 7.01731 2.71402 7.888 2.56254C8.83765 2.39733 9.59364 1.67595 9.80357 0.735873L9.81774 0.671389L9.82494 0.638365ZM10.552 7.75606C10.0846 7.74291 9.6217 7.56639 9.27655 7.22701C9.23495 7.2547 9.1957 7.28688 9.15956 7.32342C9.13146 7.35171 9.10599 7.38184 9.08322 7.41349C8.6335 8.00912 8.10529 8.6178 7.50979 9.2133C7.05819 9.6649 6.60047 10.0764 6.14797 10.4436C5.69549 10.0764 5.23777 9.6649 4.78618 9.21331C4.33458 8.76172 3.9231 8.304 3.55583 7.8515C3.9231 7.39901 4.33458 6.94128 4.78618 6.48969C5.38145 5.89442 5.98989 5.36639 6.5853 4.91678C6.61928 4.89238 6.65154 4.86488 6.68162 4.83435C6.70948 4.80622 6.73472 4.77627 6.75731 4.74484C6.36168 4.39279 6.15864 3.88856 6.1482 3.38032C5.16669 2.69292 4.1965 2.18363 3.33055 1.91885C2.39231 1.63196 1.29292 1.55452 0.571954 2.27548C0.104576 2.74286 -0.0227025 3.37911 0.0136461 3.98383C0.0500648 4.58972 0.252331 5.25637 0.559417 5.93196C0.836935 6.5425 1.21427 7.19128 1.67665 7.8515C1.21427 8.51172 0.836935 9.1605 0.559417 9.77104C0.252331 10.4466 0.050065 11.1133 0.0136462 11.7192C-0.0227026 12.3239 0.104576 12.9601 0.571954 13.4275C1.03934 13.8949 1.67559 14.0222 2.28031 13.9858C2.88619 13.9494 3.55284 13.7471 4.22843 13.4401C4.83897 13.1625 5.48775 12.7852 6.14797 12.3228C6.8082 12.7852 7.45699 13.1625 8.06754 13.4401C8.74312 13.7472 9.40978 13.9494 10.0157 13.9858C10.6204 14.0222 11.2567 13.8949 11.7241 13.4275C12.445 12.7066 12.3676 11.6072 12.0807 10.6689C11.8073 9.77499 11.2734 8.76995 10.552 7.75606ZM2.89194 3.35329C3.46733 3.52922 4.15481 3.86372 4.89904 4.34669C4.505 4.68092 4.11194 5.04261 3.72552 5.42903C3.34 5.81454 2.97818 6.2076 2.64318 6.60257C2.34978 6.15049 2.10883 5.71573 1.92497 5.31125C1.66137 4.73134 1.53244 4.25134 1.51095 3.89383C1.48939 3.53516 1.5775 3.39126 1.63262 3.33614C1.7228 3.24596 2.0555 3.09753 2.89194 3.35329ZM1.92497 10.3917C2.10883 9.98727 2.34978 9.55251 2.64317 9.10043C2.97818 9.4954 3.34 9.88846 3.72552 10.274C4.11103 10.6595 4.50408 11.0213 4.89904 11.3563C4.44696 11.6497 4.01221 11.8906 3.60773 12.0745C3.02782 12.3381 2.54782 12.467 2.19031 12.4885C1.83163 12.5101 1.68774 12.422 1.63262 12.3669C1.5775 12.3117 1.48939 12.1678 1.51095 11.8092C1.53244 11.4517 1.66137 10.9717 1.92497 10.3917ZM8.68824 12.0745C8.28375 11.8907 7.84899 11.6497 7.3969 11.3563C7.79187 11.0213 8.18493 10.6595 8.57045 10.274C8.95687 9.88754 9.31856 9.49448 9.65279 9.10043C10.1358 9.84467 10.4703 10.5322 10.6462 11.1075C10.902 11.944 10.7536 12.2767 10.6634 12.3669C10.6083 12.422 10.4644 12.5101 10.1057 12.4885C9.74815 12.4671 9.26815 12.3381 8.68824 12.0745ZM5.14795 7.8519C5.14795 7.29961 5.59567 6.8519 6.14795 6.8519C6.70024 6.8519 7.14795 7.29961 7.14795 7.8519C7.14795 8.40418 6.70024 8.8519 6.14795 8.8519C5.59567 8.8519 5.14795 8.40418 5.14795 7.8519Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189553\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ai-settings-spark\", \"keywords\": [ \"cog\", \"gear\", \"settings\", \"machine\", \"artificial\", \"intelligence\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189571)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.80531 0.745416C9.98605 -0.0802161 11.1616 -0.0853549 11.3495 0.738666L11.3582 0.776877C11.364 0.802571 11.3696 0.826946 11.3754 0.851685C11.5857 1.74737 12.3105 2.43176 13.2179 2.58962C14.0793 2.73949 14.0793 3.9761 13.2179 4.12597C12.3057 4.28467 11.5779 4.97553 11.372 5.87826L11.3495 5.97692C11.1616 6.80095 9.98605 6.7958 9.80531 5.97017L9.78678 5.88551C9.58845 4.97951 8.8617 4.28353 7.94797 4.12457C7.08811 3.97498 7.0881 2.74061 7.94797 2.59102C8.85852 2.43261 9.58339 1.74092 9.78469 0.839537L9.79834 0.777362L9.80531 0.745416ZM7.77086 5.14249C6.42908 4.90906 5.98687 3.54072 6.44426 2.55925C6.40716 2.52258 6.36673 2.48909 6.32335 2.45924C6.17579 2.35769 6.001 2.30307 5.82187 2.30254H5.0927C4.91357 2.30307 4.73878 2.35769 4.59121 2.45924C4.44365 2.56079 4.33019 2.70453 4.26571 2.87165L3.88334 3.8587L2.56727 4.60566L1.51797 4.4456C1.34325 4.42188 1.16543 4.45064 1.00709 4.52823C0.848761 4.60581 0.717065 4.72872 0.628743 4.88132L0.273049 5.50379C0.181904 5.65882 0.139911 5.83785 0.152611 6.01725C0.165312 6.19664 0.232109 6.36797 0.344188 6.50862L1.01111 7.33561V8.82952L0.361973 9.65651C0.249894 9.79716 0.183096 9.96849 0.170396 10.1479C0.157695 10.3273 0.199689 10.5063 0.290834 10.6613L0.646528 11.2838C0.73485 11.4364 0.866546 11.5593 1.02488 11.6369C1.18321 11.7145 1.36104 11.7433 1.53576 11.7195L2.58506 11.5595L3.88334 12.3064L4.26571 13.2935C4.33019 13.4606 4.44365 13.6044 4.59121 13.7059C4.73878 13.8074 4.91357 13.8621 5.0927 13.8626H5.83965C6.01878 13.8621 6.19357 13.8074 6.34114 13.7059C6.4887 13.6044 6.60216 13.4606 6.66664 13.2935L7.04901 12.3064L8.3473 11.5595L9.39659 11.7195C9.57131 11.7433 9.74914 11.7145 9.90747 11.6369C10.0658 11.5593 10.1975 11.4364 10.2858 11.2838L10.6415 10.6613C10.7326 10.5063 10.7746 10.3273 10.7619 10.1479C10.7492 9.96849 10.6824 9.79716 10.5703 9.65651L9.90346 8.82952V7.50279C9.38145 7.30485 8.94401 6.86732 8.79599 6.19111L8.77746 6.10646C8.66889 5.6105 8.27105 5.22951 7.77086 5.14249ZM5.45729 9.94043C6.48336 9.94043 7.31515 9.10864 7.31515 8.08257C7.31515 7.0565 6.48336 6.2247 5.45729 6.2247C4.43122 6.2247 3.59942 7.0565 3.59942 8.08257C3.59942 9.10864 4.43122 9.94043 5.45729 9.94043Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189571\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ai-technology-spark\", \"keywords\": [ \"lightbulb\", \"idea\", \"bright\", \"lighting\", \"artificial\", \"intelligence\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189583)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.52734 9.33109C8.10835 8.99499 8.61545 8.54534 9.0183 8.01248C8.89486 7.82421 8.80049 7.6037 8.74516 7.35097L8.72598 7.26333C8.61359 6.74991 8.20174 6.3555 7.68394 6.26542C5.61213 5.90498 5.61208 2.93078 7.68394 2.57033C8.18485 2.48319 8.58661 2.11125 8.71402 1.62232C7.79996 0.625182 6.48666 0 5.02734 0C2.26592 0 0.0273438 2.23858 0.0273438 5C0.0273438 6.85071 1.03284 8.46657 2.52734 9.33109V10.5C2.52734 11.0523 2.97506 11.5 3.52734 11.5H6.52734C7.07963 11.5 7.52734 11.0523 7.52734 10.5V9.33109ZM2.52734 13.25C2.52734 12.8358 2.86313 12.5 3.27734 12.5H6.77734C7.19155 12.5 7.52734 12.8358 7.52734 13.25C7.52734 13.6642 7.19155 14 6.77734 14H3.27734C2.86313 14 2.52734 13.6642 2.52734 13.25ZM9.80723 1.71727C9.99031 0.880936 11.1811 0.87573 11.3715 1.71043L11.3805 1.74989C11.3865 1.77679 11.3922 1.80164 11.3983 1.82754C11.6176 2.76163 12.3735 3.47537 13.3198 3.64C14.1921 3.79176 14.1921 5.04399 13.3198 5.19575C12.3685 5.36126 11.6095 6.08174 11.3948 7.02319L11.3715 7.12532C11.1811 7.96003 9.99031 7.95481 9.80723 7.11848L9.78805 7.03085C9.58121 6.08595 8.82326 5.36009 7.8703 5.19431C6.99962 5.04283 6.99961 3.79292 7.8703 3.64145C8.81994 3.47624 9.57593 2.75485 9.78587 1.81478L9.80003 1.75029L9.80723 1.71727Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189583\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ai-upscale-spark\", \"keywords\": [ \"magnifier\", \"zoom\", \"view\", \"find\", \"search\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189574)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.5 1.5C3.73858 1.5 1.5 3.73858 1.5 6.5C1.5 9.26142 3.73858 11.5 6.5 11.5C9.26142 11.5 11.5 9.26142 11.5 6.5C11.5 3.73858 9.26142 1.5 6.5 1.5ZM0 6.5C0 2.91015 2.91015 0 6.5 0C10.0899 0 13 2.91015 13 6.5C13 7.9341 12.5356 9.25973 11.7489 10.3347L13.7073 12.2931C14.0979 12.6836 14.0979 13.3168 13.7073 13.7073C13.3168 14.0978 12.6837 14.0978 12.2931 13.7073L10.3347 11.7489C9.25974 12.5356 7.9341 13 6.5 13C2.91015 13 0 10.0899 0 6.5ZM5.73911 3.86951C5.91933 3.04626 7.09153 3.04113 7.27893 3.86278L7.28755 3.90071C7.29338 3.92646 7.29874 3.95014 7.30455 3.97489C7.51284 4.86203 8.2308 5.5399 9.12952 5.69625C9.98853 5.84569 9.98853 7.07883 9.12952 7.22828C8.22601 7.38546 7.5052 8.06974 7.30126 8.96386L7.27893 9.06175C7.09153 9.8834 5.91933 9.87827 5.73911 9.05502L5.72073 8.97102C5.52429 8.07367 4.80448 7.38434 3.89947 7.22689C3.04202 7.07772 3.04201 5.84681 3.89947 5.69764C4.80133 5.54074 5.51928 4.85565 5.71865 3.96288L5.73219 3.90124L5.73911 3.86951Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189574\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ai-vehicle-spark-1\", \"keywords\": [ \"car\", \"automated\", \"transportation\", \"artificial\", \"intelligence\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189577)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.81542 0.696227C9.99849 -0.140106 11.1893 -0.145312 11.3797 0.689389L11.3887 0.728847C11.3948 0.755748 11.4004 0.7806 11.4065 0.806498C11.6258 1.74059 12.3817 2.45433 13.328 2.61896C14.2003 2.77072 14.2003 4.02295 13.328 4.17471C12.3767 4.34022 11.6177 5.0607 11.403 6.00215L11.3797 6.10428C11.1893 6.93899 9.99849 6.93377 9.81542 6.09744L9.79623 6.0098C9.58939 5.0649 8.83144 4.33905 7.87848 4.17326C7.0078 4.02179 7.00779 2.77188 7.87848 2.62041C8.82812 2.45519 9.58411 1.73381 9.79405 0.793734L9.80821 0.729251L9.81542 0.696227ZM7.69212 5.24438C6.99023 5.12227 6.52613 4.70018 6.2998 4.18135H4.78172C4.45951 4.16762 4.14131 4.25811 3.87441 4.4396C3.60355 4.62379 3.40043 4.89156 3.29606 5.20203L3.29545 5.20385L2.63913 7.18135H1.5C1.10217 7.18135 0.720644 7.33939 0.43934 7.62069C0.158035 7.902 0 8.28353 0 8.68135V10.6814C0 11.0792 0.158035 11.4607 0.43934 11.742C0.603998 11.9067 0.802996 12.0291 1.0195 12.1023C1.18121 10.8772 2.22965 9.9317 3.49892 9.9317C4.79537 9.9317 5.86142 10.9181 5.98751 12.1814H8.01035C8.13644 10.9181 9.20249 9.9317 10.4989 9.9317C11.7684 9.9317 12.817 10.8776 12.9784 12.103C13.1957 12.0299 13.3955 11.9072 13.5607 11.742C13.842 11.4607 14 11.0792 14 10.6814V8.68135C14 8.28353 13.842 7.902 13.5607 7.62069C13.2794 7.33939 12.8978 7.18135 12.5 7.18135H12.0266C11.101 8.25165 9.11102 7.96388 8.75335 6.32993L8.73416 6.24229C8.62177 5.72887 8.20993 5.33446 7.69212 5.24438ZM3.49894 13.9338C2.66993 13.9338 1.99788 13.2618 1.99788 12.4328C1.99788 11.6037 2.66993 10.9317 3.49894 10.9317C4.32795 10.9317 5 11.6037 5 12.4328C5 13.2618 4.32795 13.9338 3.49894 13.9338ZM10.4989 13.9338C9.66993 13.9338 8.99789 13.2618 8.99789 12.4328C8.99789 11.6037 9.66993 10.9317 10.4989 10.9317C11.328 10.9317 12 11.6037 12 12.4328C12 13.2618 11.328 13.9338 10.4989 13.9338Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189577\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"artificial-intelligence-spark\", \"keywords\": [ \"brain\", \"thought\", \"ai\", \"automated\", \"ai\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189580)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.739 0.850524C9.92208 0.0141907 11.1129 0.00898498 11.3033 0.843686L11.3122 0.883144C11.3182 0.910044 11.324 0.934897 11.33 0.960795C11.5494 1.89488 12.3053 2.60863 13.2516 2.77326C14.1239 2.92501 14.1239 4.17725 13.2516 4.32901C12.3003 4.49451 11.5413 5.215 11.3266 6.15644L11.3033 6.25858C11.1129 7.09328 9.92208 7.08807 9.739 6.25174L9.71982 6.1641C9.51297 5.2192 8.75502 4.49335 7.80206 4.32756C6.93138 4.17609 6.93137 2.92618 7.80206 2.7747C8.75171 2.60949 9.5077 1.88811 9.71763 0.948031L9.73179 0.883548L9.739 0.850524ZM8.65781 6.39659C8.54608 5.88622 8.13846 5.49346 7.625 5.40032V12.6147C8.17306 13.3232 9.03161 13.7795 9.99672 13.7795C11.3078 13.7795 12.4222 12.9375 12.8287 11.7649C12.582 11.7422 12.316 11.6821 12.0507 11.5823C11.4556 11.3583 10.8088 10.9168 10.2781 10.1463C10.0824 9.86206 10.1541 9.47289 10.4384 9.2771C10.7226 9.08131 11.1118 9.15304 11.3076 9.43731C11.6897 9.99209 12.1284 10.2759 12.491 10.4124C12.6748 10.4815 12.8392 10.5129 12.9672 10.5221L12.9899 10.5235C13.6095 9.85129 14 8.54648 14 7.45712C14 6.72292 13.8226 5.99874 13.5173 5.38485C13.4912 5.39037 13.4648 5.39546 13.438 5.40012C12.9184 5.49052 12.5039 5.88402 12.3866 6.39821L12.3633 6.50034C11.9147 8.46738 9.10842 8.45506 8.67699 6.48423L8.65781 6.39659ZM6.375 4.61469C5.95752 3.98445 5.95752 3.11779 6.375 2.48756V1.96736C5.89476 1.40406 5.17994 1.04684 4.3817 1.04684C2.93566 1.04684 1.76342 2.21908 1.76342 3.66511L1.76343 3.66791C1.8018 4.05742 1.96105 4.34866 2.1451 4.55835C2.37401 4.81912 2.61517 4.92556 2.67236 4.93859C3.00891 5.01532 3.21953 5.35034 3.1428 5.68688C3.06608 6.02343 2.73106 6.23405 2.39451 6.15732C2.03531 6.07543 1.57457 5.80322 1.20568 5.38295C1.06415 5.22172 0.932208 5.03436 0.821827 4.8212C0.312464 5.52594 0 6.4827 0 7.45712C0 8.1919 0.177677 9.02469 0.483473 9.69526C0.563842 9.60565 0.650146 9.52334 0.742325 9.45056C1.01323 9.23665 1.40626 9.28286 1.62016 9.55377C1.83407 9.82468 1.78786 10.2177 1.51695 10.4316C1.33903 10.5721 1.15327 10.8751 1.03924 11.2259C1.25336 12.6708 2.49886 13.7795 4.00331 13.7795C4.9684 13.7795 5.82694 13.3233 6.375 12.6147V8.9453C6.00658 9.24099 5.54184 9.46587 4.9699 9.5297C4.62685 9.56799 4.31772 9.32094 4.27943 8.97789C4.24114 8.63484 4.4882 8.32571 4.83125 8.28742C5.33987 8.23065 5.70976 7.9383 5.97421 7.55267C6.24167 7.16265 6.35817 6.73542 6.375 6.53035V4.61469Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189580\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" } ], \"computer_devices\": [ { \"name\": \"adobe\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188033)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.316406 0.816406C0.316406 0.540264 0.540264 0.316406 0.816406 0.316406H13.1836C13.4597 0.316406 13.6836 0.540264 13.6836 0.816406V13.1836C13.6836 13.4597 13.4597 13.6836 13.1836 13.6836H0.816406C0.540264 13.6836 0.316406 13.4597 0.316406 13.1836V0.816406ZM1.36923 12.6303L5.39103 1.36926H8.60847L12.6303 12.6303H9.70935L6.99975 5.14708L5.32216 10.2172H6.87834L7.6827 12.6303H1.36923Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188033\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"alt\", \"keywords\": [ \"windows\", \"key\", \"alt\", \"pc\", \"keyboard\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188036)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.43934 0.43934C0.720644 0.158035 1.10218 0 1.5 0H12.5C12.8978 0 13.2794 0.158035 13.5607 0.43934C13.842 0.720644 14 1.10217 14 1.5V11.75V12.5C14 12.8978 13.842 13.2794 13.5607 13.5607C13.2794 13.842 12.8978 14 12.5 14H1.5C1.10217 14 0.720644 13.842 0.43934 13.5607C0.158035 13.2794 0 12.8978 0 12.5V11.5V1.5C0 1.10218 0.158035 0.720644 0.43934 0.43934ZM12.295 5.65853C12.6402 5.65853 12.92 5.37871 12.92 5.03353C12.92 4.68835 12.6402 4.40853 12.295 4.40853H9.67309C9.32791 4.40853 9.04809 4.68835 9.04809 5.03353C9.04809 5.37871 9.32791 5.65853 9.67309 5.65853H10.3591V8.96646C10.3591 9.31164 10.6389 9.59146 10.9841 9.59146C11.3292 9.59146 11.6091 9.31164 11.6091 8.96646V5.65853H12.295ZM6.72339 4.40853C7.06857 4.40853 7.34839 4.68835 7.34839 5.03353V8.34147H9.0176C9.36278 8.34147 9.6426 8.62129 9.6426 8.96647C9.6426 9.31164 9.36278 9.59147 9.0176 9.59147H6.72339C6.37821 9.59147 6.09839 9.31164 6.09839 8.96647V5.03353C6.09839 4.68835 6.37821 4.40853 6.72339 4.40853ZM4.36799 5.14681C4.22103 4.70592 3.80842 4.40853 3.34368 4.40853C2.87893 4.40853 2.46633 4.70592 2.31937 5.14681L1.55363 7.44401C1.55037 7.45308 1.54731 7.46225 1.54446 7.47152L1.11203 8.76882C1.00287 9.09629 1.17985 9.45024 1.50731 9.55939C1.83478 9.66855 2.18873 9.49157 2.29788 9.16411L2.59242 8.28049H4.09493L4.38947 9.16411C4.49863 9.49157 4.85258 9.66855 5.18004 9.55939C5.50751 9.45024 5.68448 9.09629 5.57533 8.76882L5.14289 7.47152C5.14004 7.46225 5.13698 7.45308 5.13372 7.444L4.36799 5.14681ZM3.00909 7.03049L3.34368 6.02672L3.67827 7.03049H3.00909Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188036\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"amazon\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188039)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.44452 2.96142C4.44452 2.49167 4.98914 1.84375 6.02911 1.84375C7.06908 1.84375 7.6137 2.49167 7.6137 2.96142V4.08044H6.02911C4.49036 4.08044 2.94452 5.10489 2.94452 6.69812C2.94452 8.29134 4.49036 9.31579 6.02911 9.31579C6.87633 9.31579 7.72569 9.00523 8.31801 8.46018C8.55022 8.71505 8.86617 8.94921 9.2825 9.04913C9.68528 9.1458 10.0901 8.89765 10.1868 8.49487C10.2835 8.09209 10.0353 7.68722 9.63256 7.59055C9.55563 7.57209 9.44186 7.50004 9.31766 7.30584C9.2153 7.1458 9.14743 6.96484 9.1137 6.83852V6.69812V4.83044V2.96142C9.1137 1.3682 7.56786 0.34375 6.02911 0.34375C4.49036 0.34375 2.94452 1.3682 2.94452 2.96142C2.94452 3.37564 3.28031 3.71142 3.69452 3.71142C4.10874 3.71142 4.44452 3.37564 4.44452 2.96142ZM7.6137 5.58044H6.02911C4.98914 5.58044 4.44452 6.22837 4.44452 6.69812C4.44452 7.16787 4.98914 7.81579 6.02911 7.81579C7.06908 7.81579 7.6137 7.16787 7.6137 6.69812V6.69691V5.58044ZM0.284297 10.0497C0.562563 9.74292 1.03687 9.71977 1.34369 9.99804C2.284 10.8508 4.47445 11.7654 7.31239 11.6268C9.06979 11.5411 10.4931 11.0785 11.4532 10.6292C11.1619 10.6034 10.8883 10.6211 10.6654 10.7014C10.2756 10.8416 9.84598 10.6394 9.70574 10.2496C9.56549 9.85985 9.76776 9.43021 10.1575 9.28997C10.9503 9.0047 11.8138 9.1293 12.3973 9.27371C12.7033 9.34943 12.9664 9.43943 13.1534 9.51052C13.2475 9.54625 13.3235 9.57769 13.3776 9.60092C13.4047 9.61255 13.4264 9.62216 13.4421 9.62926L13.4613 9.638L13.4675 9.64086L13.4697 9.64189L13.471 9.64249C13.4711 9.64254 13.4712 9.64259 13.4041 9.78534C13.4041 9.78533 13.4041 9.78535 13.4041 9.78534L13.471 9.64249C13.745 9.77146 13.9148 10.0526 13.9011 10.3551L13.88 10.3542C13.9011 10.3552 13.9011 10.355 13.9011 10.3551L13.901 10.3569L13.9009 10.3593L13.9006 10.3661L13.8993 10.3872C13.8983 10.4044 13.8967 10.4281 13.8943 10.4575C13.8936 10.4665 13.8928 10.476 13.892 10.4861C13.8921 10.5373 13.887 10.5884 13.8767 10.6385C13.8746 10.6575 13.8723 10.6774 13.8697 10.6979C13.8454 10.8965 13.8023 11.1712 13.7266 11.4772C13.5822 12.0608 13.2897 12.8827 12.6676 13.451C12.3618 13.7303 11.8874 13.7089 11.608 13.403C11.3287 13.0972 11.3501 12.6228 11.6559 12.3435C11.7348 12.2714 11.8072 12.1844 11.8733 12.0858C10.7497 12.5822 9.21539 13.0357 7.38551 13.1251C4.24554 13.2783 1.63486 12.2871 0.336003 11.1091C0.0291795 10.8309 0.00602989 10.3566 0.284297 10.0497Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188039\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"android\", \"keywords\": [ \"android\", \"code\", \"apps\", \"bugdroid\", \"programming\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188042)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.5296 0.241276C3.23671 -0.0516168 2.76183 -0.0516168 2.46894 0.241276C2.17605 0.53417 2.17605 1.00904 2.46894 1.30194L3.76018 2.59318C3.14494 3.28802 2.77006 4.16189 2.6895 5.08447H11.3091C11.2285 4.16189 10.8537 3.28802 10.2384 2.59317L11.5297 1.30194C11.8226 1.00904 11.8226 0.53417 11.5297 0.241276C11.2368 -0.0516168 10.7619 -0.0516168 10.469 0.241276L9.05556 1.65468C8.4295 1.31649 7.72376 1.13477 6.99929 1.13477C6.27481 1.13477 5.56907 1.31649 4.94301 1.65468L3.5296 0.241276ZM4.1731 11.4084H9.82544V13.1597C9.82544 13.5739 10.1613 13.9097 10.5755 13.9097C10.9897 13.9097 11.3255 13.5739 11.3255 13.1597V10.666V10.6584V7.60819C11.6126 7.62061 11.8853 7.7401 12.0893 7.94413C12.3051 8.15991 12.4264 8.45258 12.4264 8.75774V9.65836C12.4264 10.0726 12.7621 10.4084 13.1764 10.4084C13.5906 10.4084 13.9264 10.0726 13.9264 9.65836V8.75774C13.9264 8.05475 13.6471 7.38055 13.15 6.88347C12.6529 6.38638 11.9787 6.10712 11.2757 6.10712H2.72289C2.0199 6.10712 1.34571 6.38638 0.848616 6.88347C0.351527 7.38055 0.0722656 8.05475 0.0722656 8.75774V9.65836C0.0722656 10.0726 0.408052 10.4084 0.822266 10.4084C1.23648 10.4084 1.57227 10.0726 1.57227 9.65836V8.75774C1.57227 8.45258 1.69349 8.15991 1.90928 7.94413C2.1133 7.74011 2.38604 7.62062 2.6731 7.60819V10.1722V10.6584V13.1597C2.6731 13.5739 3.00888 13.9097 3.4231 13.9097C3.83731 13.9097 4.1731 13.5739 4.1731 13.1597V11.4084Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188042\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"app-store\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.85403 1.29785C8.3503 1.00572 7.70513 1.17725 7.413 1.68098L7.14569 2.14192L6.87837 1.68098C6.58624 1.17725 5.94107 1.00572 5.43735 1.29785C4.93362 1.58998 4.76208 2.23515 5.05421 2.73888L5.92685 4.24359L3.14898 9.03355H1.30826C0.725959 9.03355 0.253906 9.50561 0.253906 10.0879C0.253906 10.6702 0.725959 11.1423 1.30826 11.1423H1.92605H4.36373H8.62917L7.40905 9.03355H5.58665L7.14569 6.34526L9.23716 2.73888C9.52929 2.23515 9.35776 1.58998 8.85403 1.29785ZM1.73909 13.6468C2.25566 13.9443 2.91554 13.7666 3.21299 13.2501L3.80659 12.2192H1.31717C1.0557 12.7283 1.23781 13.3582 1.73909 13.6468ZM11.0899 13.25L7.75134 7.45203L8.99498 5.31396L11.1535 9.03458H12.8226C13.4049 9.03458 13.877 9.50663 13.877 10.0889C13.877 10.6712 13.4049 11.1433 12.8226 11.1433H12.3677L12.9605 12.1729C13.258 12.6894 13.0803 13.3493 12.5638 13.6468C12.0472 13.9442 11.3873 13.7666 11.0899 13.25Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"apple\", \"keywords\": [ \"os\", \"system\", \"apple\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.1475 1.33746C10.4404 1.04457 10.4404 0.569692 10.1475 0.276799C9.85456 -0.0160944 9.37969 -0.0160944 9.0868 0.276799L7.5868 1.7768C7.2939 2.06969 7.2939 2.54457 7.5868 2.83746C7.87969 3.13035 8.35456 3.13035 8.64746 2.83746L10.1475 1.33746ZM8.60918 3.76055C9.05316 3.67621 9.50994 3.68648 9.94968 3.79068C10.3895 3.89489 10.8023 4.09068 11.1612 4.36528C11.5201 4.63988 11.8171 4.9871 12.0327 5.38427C12.1039 5.51546 12.1128 5.6716 12.0569 5.81002C12.001 5.94843 11.8862 6.05463 11.7439 6.0996C11.3751 6.21606 11.0531 6.44685 10.8242 6.75854C10.5954 7.07023 10.4717 7.44665 10.471 7.83333V7.8412C10.4633 8.28422 10.618 8.71473 10.9061 9.05134C11.1943 9.38795 11.5957 9.60733 12.0347 9.668C12.1757 9.68751 12.3018 9.76626 12.3812 9.88449C12.4607 10.0027 12.4859 10.1492 12.4506 10.2872C12.1583 11.43 11.5666 12.4742 10.7364 13.3122C10.7288 13.3199 10.7208 13.3274 10.7126 13.3347C10.2841 13.7141 9.73492 13.9293 9.1627 13.9422C8.59469 13.955 8.04041 13.7677 7.59681 13.4131C7.47241 13.3233 7.32284 13.275 7.16929 13.275C7.01359 13.275 6.86199 13.3247 6.73657 13.4169C6.27444 13.7658 5.70358 13.9396 5.12535 13.9075C4.54601 13.8753 3.99693 13.6385 3.57598 13.2391C3.56938 13.2329 3.56296 13.2264 3.55672 13.2198C2.46945 12.0695 1.78808 10.5954 1.61612 9.02222C1.40797 7.84699 1.60391 6.63576 2.1722 5.58596C2.38534 5.16849 2.68296 4.79988 3.0462 4.50356C3.41124 4.20576 3.83448 3.98754 4.28879 3.86289C4.7431 3.73824 5.21844 3.70991 5.68434 3.77971C6.14603 3.84889 6.58833 4.01292 6.98342 4.26142C7.04742 4.2968 7.11939 4.31539 7.1926 4.31539C7.26305 4.31539 7.33236 4.29817 7.39451 4.26534C7.76018 4.01516 8.17373 3.84326 8.60918 3.76055Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"asterisk-1\", \"keywords\": [ \"asterisk\", \"star\", \"keyboard\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.00037 0.0905762C7.55266 0.0905762 8.00037 0.538291 8.00037 1.09058V5.17834L11.4638 2.97434C11.9298 2.67783 12.5479 2.81518 12.8444 3.28113C13.1409 3.74707 13.0035 4.36515 12.5376 4.66166L8.86303 6.99999L12.5376 9.33832C13.0035 9.63483 13.1409 10.2529 12.8444 10.7189C12.5479 11.1848 11.9298 11.3222 11.4638 11.0256L8.00037 8.82165V12.9094C8.00037 13.4617 7.55266 13.9094 7.00037 13.9094C6.44809 13.9094 6.00037 13.4617 6.00037 12.9094V8.82169L2.53701 11.0256C2.07106 11.3222 1.45298 11.1848 1.15647 10.7189C0.859956 10.2529 0.997309 9.63483 1.46325 9.33832L5.13777 6.99999L1.46325 4.66166C0.997309 4.36515 0.859956 3.74707 1.15647 3.28113C1.45298 2.81518 2.07106 2.67783 2.53701 2.97434L6.00037 5.1783V1.09058C6.00037 0.538291 6.44809 0.0905762 7.00037 0.0905762Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"battery-alert-1\", \"keywords\": [ \"phone\", \"mobile\", \"charge\", \"device\", \"electricity\", \"power\", \"battery\", \"alert\", \"warning\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.5 5.5C13.5 5.36739 13.4473 5.24021 13.3536 5.14645C13.2598 5.05268 13.1326 5 13 5H12.5V3.5C12.5 3.23478 12.3946 2.98043 12.2071 2.79289C12.0196 2.60536 11.7652 2.5 11.5 2.5H1.5C1.23478 2.5 0.98043 2.60536 0.792893 2.79289C0.605357 2.98043 0.5 3.23478 0.5 3.5V10.5C0.5 10.7652 0.605357 11.0196 0.792893 11.2071C0.98043 11.3946 1.23478 11.5 1.5 11.5H11.5C11.7652 11.5 12.0196 11.3946 12.2071 11.2071C12.3946 11.0196 12.5 10.7652 12.5 10.5V9H13C13.1326 9 13.2598 8.94732 13.3536 8.85355C13.4473 8.75979 13.5 8.63261 13.5 8.5V5.5ZM6.5 3.875C6.84518 3.875 7.125 4.15482 7.125 4.5V7C7.125 7.34518 6.84518 7.625 6.5 7.625C6.15482 7.625 5.875 7.34518 5.875 7V4.5C5.875 4.15482 6.15482 3.875 6.5 3.875ZM7.25 9.25C7.25 9.66421 6.91421 10 6.5 10C6.08579 10 5.75 9.66421 5.75 9.25C5.75 8.83579 6.08579 8.5 6.5 8.5C6.91421 8.5 7.25 8.83579 7.25 9.25Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"battery-charging\", \"keywords\": [ \"phone\", \"mobile\", \"charge\", \"device\", \"electricity\", \"power\", \"battery\", \"charging\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.3536 5.14645C13.4473 5.24021 13.5 5.36739 13.5 5.5V8.5C13.5 8.63261 13.4473 8.75979 13.3536 8.85355C13.2598 8.94732 13.1326 9 13 9H12.5V10.5C12.5 10.7652 12.3946 11.0196 12.2071 11.2071C12.0196 11.3946 11.7652 11.5 11.5 11.5H1.5C1.23478 11.5 0.98043 11.3946 0.792893 11.2071C0.605357 11.0196 0.5 10.7652 0.5 10.5V3.5C0.5 3.23478 0.605357 2.98043 0.792893 2.79289C0.98043 2.60536 1.23478 2.5 1.5 2.5H11.5C11.7652 2.5 12.0196 2.60536 12.2071 2.79289C12.3946 2.98043 12.5 3.23478 12.5 3.5V5H13C13.1326 5 13.2598 5.05268 13.3536 5.14645ZM7.49186 5.15503C7.69722 4.87758 7.63877 4.48619 7.36132 4.28084C7.08388 4.07548 6.69249 4.13393 6.48713 4.41138L4.82942 6.65107C4.68901 6.84078 4.66744 7.09341 4.77366 7.30418C4.87988 7.51494 5.09577 7.6479 5.33179 7.6479H6.43813L5.77074 8.84996C5.60319 9.15175 5.712 9.53222 6.01379 9.69977C6.31557 9.86733 6.69605 9.75851 6.8636 9.45672L8.04643 7.32628C8.15391 7.1327 8.15094 6.89669 8.03863 6.70587C7.92631 6.51505 7.72142 6.3979 7.5 6.3979H6.57195L7.49186 5.15503Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"battery-empty-1\", \"keywords\": [ \"phone\", \"mobile\", \"charge\", \"device\", \"electricity\", \"empty\", \"power\", \"battery\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 2C1.10218 2 0.720644 2.15804 0.43934 2.43934C0.158035 2.72064 0 3.10218 0 3.5V10.5C0 10.8978 0.158035 11.2794 0.43934 11.5607C0.720644 11.842 1.10217 12 1.5 12H11.5C11.8978 12 12.2794 11.842 12.5607 11.5607C12.842 11.2794 13 10.8978 13 10.5V9.5C13.2652 9.5 13.5196 9.39464 13.7071 9.20711C13.8946 9.01957 14 8.76521 14 8.5V5.5C14 5.23478 13.8946 4.98043 13.7071 4.79289C13.5196 4.60536 13.2652 4.5 13 4.5V3.5C13 3.10217 12.842 2.72064 12.5607 2.43934C12.2794 2.15804 11.8978 2 11.5 2H1.5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"battery-empty-2\", \"keywords\": [ \"phone\", \"mobile\", \"charge\", \"device\", \"electricity\", \"empty\", \"power\", \"battery\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187711)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.219669 0.218938C-0.0732231 0.511831 -0.0732231 0.986704 0.219669 1.2796L0.397768 1.4577C0.415456 1.4719 0.432551 1.48721 0.44896 1.50361L12.7769 13.8315C13.0715 14.0727 13.5068 14.0559 13.7817 13.781C14.0746 13.4881 14.0746 13.0132 13.7817 12.7203L12.2391 11.1777C12.407 10.9939 12.5007 10.7535 12.5007 10.5034V9.00342H13.0007C13.1333 9.00342 13.2604 8.95074 13.3542 8.85697C13.448 8.7632 13.5007 8.63603 13.5007 8.50342V5.50342C13.5007 5.37081 13.448 5.24363 13.3542 5.14986C13.2604 5.0561 13.1333 5.00342 13.0007 5.00342H12.5007V3.50342C12.5007 3.2382 12.3953 2.98385 12.2078 2.79631C12.0202 2.60877 11.7659 2.50342 11.5007 2.50342H3.56481L1.28033 0.218938C0.987436 -0.0739558 0.512562 -0.0739558 0.219669 0.218938ZM8.68099 11.5034L0.514567 3.33699C0.505345 3.39162 0.500624 3.44728 0.500624 3.50342V10.5034C0.500624 10.7686 0.605981 11.023 0.793518 11.2105C0.981054 11.3981 1.23541 11.5034 1.50062 11.5034H8.68099Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187711\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"battery-full-1\", \"keywords\": [ \"phone\", \"mobile\", \"charge\", \"device\", \"electricity\", \"power\", \"battery\", \"full\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.43934 2.43934C0.720644 2.15804 1.10218 2 1.5 2H11.5C11.8978 2 12.2794 2.15804 12.5607 2.43934C12.842 2.72064 13 3.10217 13 3.5V4.5C13.2652 4.5 13.5196 4.60536 13.7071 4.79289C13.8946 4.98043 14 5.23478 14 5.5V8.5C14 8.76521 13.8946 9.01957 13.7071 9.20711C13.5196 9.39464 13.2652 9.5 13 9.5V10.5C13 10.8978 12.842 11.2794 12.5607 11.5607C12.2794 11.842 11.8978 12 11.5 12H1.5C1.10217 12 0.720644 11.842 0.43934 11.5607C0.158035 11.2794 0 10.8978 0 10.5V3.5C0 3.10218 0.158035 2.72064 0.43934 2.43934ZM3.375 5C3.375 4.65482 3.09518 4.375 2.75 4.375C2.40482 4.375 2.125 4.65482 2.125 5V9C2.125 9.34518 2.40482 9.625 2.75 9.625C3.09518 9.625 3.375 9.34518 3.375 9V5ZM6.875 5C6.875 4.65482 6.59518 4.375 6.25 4.375C5.90482 4.375 5.625 4.65482 5.625 5V9C5.625 9.34518 5.90482 9.625 6.25 9.625C6.59518 9.625 6.875 9.34518 6.875 9V5ZM9.75 4.375C10.0952 4.375 10.375 4.65482 10.375 5V9C10.375 9.34518 10.0952 9.625 9.75 9.625C9.40482 9.625 9.125 9.34518 9.125 9V5C9.125 4.65482 9.40482 4.375 9.75 4.375Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"battery-low-1\", \"keywords\": [ \"phone\", \"mobile\", \"charge\", \"device\", \"electricity\", \"power\", \"battery\", \"low\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.43934 2.43934C0.720644 2.15804 1.10218 2 1.5 2H11.5C11.8978 2 12.2794 2.15804 12.5607 2.43934C12.842 2.72064 13 3.10217 13 3.5V4.5C13.2652 4.5 13.5196 4.60536 13.7071 4.79289C13.8946 4.98043 14 5.23478 14 5.5V8.5C14 8.76521 13.8946 9.01957 13.7071 9.20711C13.5196 9.39464 13.2652 9.5 13 9.5V10.5C13 10.8978 12.842 11.2794 12.5607 11.5607C12.2794 11.842 11.8978 12 11.5 12H1.5C1.10217 12 0.720644 11.842 0.43934 11.5607C0.158035 11.2794 0 10.8978 0 10.5V3.5C0 3.10218 0.158035 2.72064 0.43934 2.43934ZM3.375 5C3.375 4.65482 3.09518 4.375 2.75 4.375C2.40482 4.375 2.125 4.65482 2.125 5V9C2.125 9.34518 2.40482 9.625 2.75 9.625C3.09518 9.625 3.375 9.34518 3.375 9V5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"battery-medium-1\", \"keywords\": [ \"phone\", \"mobile\", \"charge\", \"medium\", \"device\", \"electricity\", \"power\", \"battery\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.43934 2.43934C0.720644 2.15804 1.10218 2 1.5 2H11.5C11.8978 2 12.2794 2.15804 12.5607 2.43934C12.842 2.72064 13 3.10217 13 3.5V4.5C13.2652 4.5 13.5196 4.60536 13.7071 4.79289C13.8946 4.98043 14 5.23478 14 5.5V8.5C14 8.76521 13.8946 9.01957 13.7071 9.20711C13.5196 9.39464 13.2652 9.5 13 9.5V10.5C13 10.8978 12.842 11.2794 12.5607 11.5607C12.2794 11.842 11.8978 12 11.5 12H1.5C1.10217 12 0.720644 11.842 0.43934 11.5607C0.158035 11.2794 0 10.8978 0 10.5V3.5C0 3.10218 0.158035 2.72064 0.43934 2.43934ZM2.75 4.375C3.09518 4.375 3.375 4.65482 3.375 5V9C3.375 9.34518 3.09518 9.625 2.75 9.625C2.40482 9.625 2.125 9.34518 2.125 9V5C2.125 4.65482 2.40482 4.375 2.75 4.375ZM6.25 4.375C6.59518 4.375 6.875 4.65482 6.875 5V9C6.875 9.34518 6.59518 9.625 6.25 9.625C5.90482 9.625 5.625 9.34518 5.625 9V5C5.625 4.65482 5.90482 4.375 6.25 4.375Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"bluetooth\", \"keywords\": [ \"bluetooth\", \"internet\", \"server\", \"network\", \"wireless\", \"connection\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.27124 0.141745C6.60679 -0.0288002 7.00968 0.00351979 7.31375 0.225375L11.7173 3.4383C11.987 3.63504 12.1407 3.95335 12.1271 4.28687C12.1135 4.62039 11.9344 4.92513 11.6496 5.09926L8.54111 7.00006L11.6496 8.90085C11.9344 9.07499 12.1135 9.37973 12.1271 9.71325C12.1407 10.0468 11.987 10.3651 11.7173 10.5619L7.31375 13.7748C7.00968 13.9966 6.60679 14.029 6.27124 13.8584C5.93569 13.6879 5.72434 13.3434 5.72434 12.967V8.7225L3.39292 10.1482C2.92175 10.4363 2.30622 10.2879 2.0181 9.81669C1.72998 9.34552 1.87837 8.72999 2.34955 8.44187L4.7074 7.00006L2.34955 5.55825C1.87837 5.27013 1.72998 4.6546 2.0181 4.18343C2.30622 3.71225 2.92175 3.56386 3.39292 3.85198L5.72434 5.27762V1.03321C5.72434 0.656807 5.93569 0.31229 6.27124 0.141745ZM7.72433 8.8449L9.33085 9.82727L7.72433 10.9995V8.8449ZM7.72433 5.15522V3.00071L9.33085 4.17285L7.72433 5.15522Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"bluetooth-disabled\", \"keywords\": [ \"bluetooth\", \"internet\", \"server\", \"network\", \"wireless\", \"disabled\", \"off\", \"connection\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187729)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.219669 0.21906C-0.0732231 0.511953 -0.0732231 0.986827 0.219669 1.27972L0.397768 1.45782C0.415456 1.47202 0.432551 1.48733 0.44896 1.50374L12.7768 13.8317C13.0714 14.0729 13.5067 14.056 13.7817 13.7811C14.0745 13.4882 14.0745 13.0133 13.7817 12.7205L11.0505 9.98929C11.0624 9.93039 11.0673 9.86971 11.0647 9.80846C11.0549 9.58111 10.9424 9.3705 10.7589 9.23593L9.02742 7.96615L7.91192 6.85066L10.7589 4.76285C10.9424 4.62827 11.0549 4.41766 11.0647 4.19031C11.0745 3.96296 10.9805 3.74346 10.8093 3.59361L7.02083 0.278707C6.79936 0.0849205 6.48502 0.0385624 6.21704 0.160165C5.94906 0.281767 5.77695 0.548857 5.77695 0.84314V4.71568L1.28033 0.21906C0.987436 -0.0738338 0.512562 -0.0738338 0.219669 0.21906ZM9.0884 11.911L8.02539 10.848L7.27695 11.5029V10.0995L5.77695 8.5995V13.1557C5.77695 13.45 5.94906 13.7171 6.21704 13.8387C6.48502 13.9603 6.79936 13.9139 7.02083 13.7201L9.0884 11.911ZM5.67126 8.49381L4.59812 7.42067L2.76853 8.76237C2.4345 9.00732 2.36229 9.47668 2.60724 9.8107C2.8522 10.1448 3.32155 10.217 3.65557 9.97198L5.67126 8.49381ZM7.27695 5.45619V2.49596L9.11751 4.10645L7.27695 5.45619Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187729\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bluetooth-searching\", \"keywords\": [ \"bluetooth\", \"internet\", \"server\", \"network\", \"wireless\", \"searching\", \"connecting\", \"connection\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187723)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.99923 0.131735C4.26721 0.0101321 4.58155 0.0564902 4.80302 0.250277L8.6094 3.58086C8.78065 3.73071 8.87462 3.95021 8.86483 4.17756C8.85503 4.40491 8.74255 4.61552 8.55904 4.75009L5.49089 7.00007L8.55904 9.25005C8.74255 9.38462 8.85503 9.59523 8.86483 9.82258C8.87462 10.05 8.78065 10.2695 8.6094 10.4193L4.80302 13.7499C4.58155 13.9437 4.26721 13.9901 3.99923 13.8684C3.73125 13.7468 3.55914 13.4798 3.55914 13.1855V8.41669L1.42208 9.98386C1.08806 10.2289 0.618712 10.1566 0.373762 9.82258C0.128811 9.48856 0.201019 9.0192 0.535043 8.77425L2.95438 7.00007L0.535043 5.22589C0.201019 4.98094 0.128811 4.51159 0.373762 4.17756C0.618712 3.84354 1.08806 3.77133 1.42208 4.01628L3.55914 5.58346V0.814709C3.55914 0.520427 3.73125 0.253337 3.99923 0.131735ZM5.05914 8.54356L6.91761 9.90645L5.05914 11.5326V8.54356ZM5.05914 5.45658V2.46753L6.91761 4.0937L5.05914 5.45658ZM11.1345 9.70518C12.6496 8.19004 12.6496 5.71497 11.1345 4.19984C10.8416 3.90694 10.8416 3.43207 11.1345 3.13918C11.4273 2.84628 11.9022 2.84628 12.1951 3.13918C14.296 5.2401 14.296 8.66491 12.1951 10.7659C11.9022 11.0588 11.4273 11.0588 11.1345 10.7659C10.8416 10.473 10.8416 9.99807 11.1345 9.70518ZM9.48851 5.91761C10.0127 6.44178 10.0127 7.3676 9.48851 7.89177C9.19561 8.18466 9.19561 8.65954 9.48851 8.95243C9.7814 9.24532 10.2563 9.24532 10.5492 8.95243C11.6591 7.84247 11.6591 5.96691 10.5492 4.85695C10.2563 4.56406 9.7814 4.56406 9.48851 4.85695C9.19561 5.14984 9.19561 5.62472 9.48851 5.91761Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187723\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"browser-wifi\", \"keywords\": [ \"wireless\", \"wifi\", \"internet\", \"server\", \"network\", \"browser\", \"connection\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187741)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.54687 3.36147V12.2955C1.54687 12.4084 1.6384 12.4999 1.75131 12.4999H12.2045C12.3426 12.4999 12.4545 12.388 12.4545 12.2499V3.36147H1.54687ZM1.75131 0.0922852C0.809976 0.0922852 0.046875 0.855387 0.046875 1.79672V12.2955C0.046875 13.2368 0.809975 13.9999 1.75131 13.9999H12.2045C13.171 13.9999 13.9545 13.2164 13.9545 12.2499V1.79672C13.9545 0.855386 13.1914 0.0922852 12.2501 0.0922852H1.75131ZM4.43975 7.11111C5.8493 5.70157 8.15208 5.70157 9.56163 7.11111C9.85452 7.40401 10.3294 7.40401 10.6223 7.11111C10.9151 6.81822 10.9151 6.34335 10.6223 6.05045C8.62696 4.05512 5.37442 4.05512 3.37909 6.05045C3.0862 6.34335 3.0862 6.81822 3.37909 7.11111C3.67198 7.40401 4.14686 7.40401 4.43975 7.11111ZM5.90676 8.73022C6.48847 8.14852 7.5129 8.14852 8.09461 8.73022C8.3875 9.02312 8.86237 9.02312 9.15527 8.73022C9.44816 8.43733 9.44816 7.96246 9.15527 7.66956C7.98778 6.50207 6.01359 6.50207 4.8461 7.66956C4.55321 7.96246 4.55321 8.43733 4.8461 8.73022C5.13899 9.02312 5.61387 9.02312 5.90676 8.73022ZM7.75068 10.0725C7.75068 10.4867 7.4149 10.8225 7.00068 10.8225C6.58647 10.8225 6.25068 10.4867 6.25068 10.0725C6.25068 9.6583 6.58647 9.32251 7.00068 9.32251C7.4149 9.32251 7.75068 9.6583 7.75068 10.0725Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187741\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"chrome\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187699)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.7508 7.00002C13.7508 10.72 10.7418 13.7371 7.02498 13.7504L9.84978 8.85768C10.1988 8.32347 10.4017 7.68512 10.4017 6.99939C10.4017 6.35637 10.2233 5.75501 9.91329 5.24212H13.1821C13.2956 5.24212 13.4036 5.21903 13.5019 5.1773C13.6641 5.75711 13.7508 6.36844 13.7508 7.00002ZM12.8366 3.60561C11.6667 1.59852 9.49105 0.249634 7.00039 0.249634C5.05906 0.249634 3.30913 1.06913 2.07767 2.38106C2.14049 2.43488 2.1954 2.50008 2.23911 2.57579L3.93706 5.51671C4.48802 4.3807 5.65242 3.59745 6.99976 3.59745C7.07958 3.59745 7.15876 3.6002 7.23721 3.60561H12.8366ZM0.25 7.00002C0.25 10.1943 2.46865 12.8704 5.449 13.5713C5.46369 13.4868 5.49322 13.4035 5.53858 13.3249L7.23097 10.3936C7.15456 10.3987 7.07746 10.4013 6.99976 10.4013C5.78616 10.4013 4.72098 9.76585 4.11883 8.8095C4.09162 8.77687 4.06682 8.74147 4.04485 8.70341L1.13296 3.65987C0.57105 4.6448 0.25 5.78492 0.25 7.00002ZM8.64335 7.94727C8.65572 7.92585 8.66899 7.90526 8.68308 7.88554C8.82267 7.62092 8.9017 7.31938 8.9017 6.99939C8.9017 5.94898 8.05017 5.09745 6.99976 5.09745C5.94935 5.09745 5.09782 5.94898 5.09782 6.99939C5.09782 8.0498 5.94935 8.90133 6.99976 8.90133C7.65534 8.90133 8.23345 8.56964 8.57542 8.06493L8.64335 7.94727Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187699\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"command\", \"keywords\": [ \"mac\", \"command\", \"apple\", \"keyboard\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187717)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.33126 2.10694C3.1654 2.03824 2.98289 2.02026 2.80681 2.05529C2.63073 2.09031 2.46899 2.17676 2.34204 2.30371C2.21509 2.43066 2.12864 2.5924 2.09362 2.76848C2.05859 2.94456 2.07657 3.12707 2.14527 3.29293C2.21397 3.4588 2.33032 3.60056 2.47959 3.70031C2.62887 3.80005 2.80436 3.85328 2.98389 3.85328H3.89161V2.94556C3.89161 2.76603 3.83838 2.59054 3.73864 2.44126C3.63889 2.29199 3.49713 2.17564 3.33126 2.10694ZM5.89161 3.85328V2.94556C5.89161 2.37047 5.72108 1.80829 5.40158 1.33012C5.08207 0.851948 4.62795 0.479258 4.09663 0.25918C3.56531 0.0391017 2.98067 -0.0184809 2.41663 0.0937142C1.85258 0.205909 1.33448 0.482842 0.927825 0.889494C0.521173 1.29615 0.24424 1.81425 0.132045 2.3783C0.019849 2.94234 0.0774316 3.52698 0.297511 4.0583C0.517589 4.58962 0.890279 5.04374 1.36845 5.36325C1.84662 5.68275 2.4088 5.85328 2.98389 5.85328H3.89161V8.10779H2.98389C2.4088 8.10779 1.84662 8.27833 1.36845 8.59783C0.89028 8.91733 0.517589 9.37146 0.297511 9.90277C0.0774316 10.4341 0.019849 11.0188 0.132045 11.5828C0.244239 12.1468 0.521172 12.665 0.927825 13.0716C1.33448 13.4783 1.85259 13.7552 2.41663 13.8674C2.98067 13.9796 3.56531 13.922 4.09663 13.7019C4.62795 13.4818 5.08207 13.1092 5.40158 12.631C5.72108 12.1528 5.89161 11.5906 5.89161 11.0155V10.1078H8.14612V11.0155C8.14612 11.5906 8.31666 12.1528 8.63616 12.631C8.95566 13.1092 9.40979 13.4818 9.9411 13.7019C10.4724 13.922 11.0571 13.9796 11.6211 13.8674C12.1852 13.7552 12.7033 13.4783 13.1099 13.0716C13.5166 12.665 13.7935 12.1468 13.9057 11.5828C14.0179 11.0188 13.9603 10.4341 13.7402 9.90277C13.5202 9.37146 13.1475 8.91733 12.6693 8.59783C12.1911 8.27833 11.6289 8.10779 11.0539 8.10779H10.1461V5.85328H11.0539C11.6289 5.85328 12.1911 5.68275 12.6693 5.36325C13.1475 5.04374 13.5202 4.58962 13.7402 4.0583C13.9603 3.52698 14.0179 2.94234 13.9057 2.3783C13.7935 1.81426 13.5166 1.29615 13.1099 0.889494C12.7033 0.482841 12.1852 0.205908 11.6211 0.0937142C11.0571 -0.0184809 10.4724 0.0391017 9.9411 0.25918C9.40979 0.479258 8.95566 0.851948 8.63616 1.33012C8.31666 1.80829 8.14612 2.37047 8.14612 2.94556V3.85328H5.89161ZM5.89161 5.85328V8.10779H8.14612V5.85328H5.89161ZM10.1461 3.85328H11.0539C11.2334 3.85328 11.4089 3.80005 11.5582 3.70031C11.7074 3.60056 11.8238 3.4588 11.8925 3.29293C11.9612 3.12707 11.9792 2.94456 11.9441 2.76848C11.9091 2.59239 11.8227 2.43065 11.6957 2.30371C11.5688 2.17676 11.407 2.09031 11.2309 2.05529C11.0549 2.02026 10.8723 2.03824 10.7065 2.10694C10.5406 2.17564 10.3989 2.29199 10.2991 2.44126C10.1994 2.59054 10.1461 2.76603 10.1461 2.94556V3.85328ZM10.1461 10.1078V11.0155C10.1461 11.1951 10.1994 11.3706 10.2991 11.5198C10.3989 11.6691 10.5406 11.7855 10.7065 11.8542C10.8723 11.9229 11.0549 11.9408 11.2309 11.9058C11.407 11.8708 11.5688 11.7843 11.6957 11.6574C11.8227 11.5305 11.9091 11.3687 11.9441 11.1926C11.9792 11.0165 11.9612 10.834 11.8925 10.6682C11.8238 10.5023 11.7074 10.3605 11.5582 10.2608C11.4089 10.1611 11.2334 10.1078 11.0539 10.1078H10.1461ZM3.89161 10.1078H2.98389C2.80436 10.1078 2.62887 10.1611 2.47959 10.2608C2.33032 10.3605 2.21397 10.5023 2.14527 10.6682C2.07657 10.834 2.05859 11.0165 2.09362 11.1926C2.12864 11.3687 2.21509 11.5305 2.34204 11.6574C2.46898 11.7843 2.63072 11.8708 2.80681 11.9058C2.98289 11.9408 3.1654 11.9229 3.33126 11.8542C3.49713 11.7855 3.63889 11.6691 3.73864 11.5198C3.83838 11.3706 3.89161 11.1951 3.89161 11.0155V10.1078Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187717\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"computer-chip-1\", \"keywords\": [ \"computer\", \"device\", \"chip\", \"electronics\", \"cpu\", \"microprocessor\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187726)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.86004 0.8573C5.86004 0.443086 5.52426 0.1073 5.11004 0.1073C4.69583 0.1073 4.36004 0.443086 4.36004 0.8573V2.5H4.00012C3.17169 2.5 2.50012 3.17157 2.50012 4V4.35992H0.857422C0.443208 4.35992 0.107422 4.69571 0.107422 5.10992C0.107422 5.52414 0.443208 5.85992 0.857422 5.85992H2.50012V8.14008H0.857422C0.443208 8.14008 0.107422 8.47586 0.107422 8.89008C0.107422 9.30429 0.443208 9.64008 0.857422 9.64008H2.50012V10C2.50012 10.8284 3.17169 11.5 4.00012 11.5H4.36004V13.1427C4.36004 13.5569 4.69583 13.8927 5.11004 13.8927C5.52426 13.8927 5.86004 13.5569 5.86004 13.1427V11.5H8.1402V13.1427C8.1402 13.5569 8.47598 13.8927 8.8902 13.8927C9.30441 13.8927 9.6402 13.5569 9.6402 13.1427V11.5H10.0001C10.8285 11.5 11.5001 10.8284 11.5001 10V9.64008H13.1428C13.557 9.64008 13.8928 9.30429 13.8928 8.89008C13.8928 8.47586 13.557 8.14008 13.1428 8.14008H11.5001V5.85992H13.1428C13.557 5.85992 13.8928 5.52414 13.8928 5.10992C13.8928 4.69571 13.557 4.35992 13.1428 4.35992H11.5001V4C11.5001 3.17157 10.8285 2.5 10.0001 2.5H9.6402V0.8573C9.6402 0.443086 9.30441 0.1073 8.8902 0.1073C8.47598 0.1073 8.1402 0.443086 8.1402 0.8573V2.5H5.86004V0.8573ZM7.40906 8.375C7.06388 8.375 6.78406 8.65482 6.78406 9C6.78406 9.34518 7.06388 9.625 7.40906 9.625H9.40906C9.75423 9.625 10.034 9.34518 10.034 9C10.034 8.65482 9.75423 8.375 9.40906 8.375H7.40906Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187726\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"computer-chip-2\", \"keywords\": [ \"core\", \"microprocessor\", \"device\", \"electronics\", \"chip\", \"computer\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187705)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.49963 1.5C2.49963 0.671573 3.1712 0 3.99963 0H9.99963C10.828 0 11.4996 0.671573 11.4996 1.5V2.33472H13.1867C13.6009 2.33472 13.9367 2.6705 13.9367 3.08472C13.9367 3.49893 13.6009 3.83472 13.1867 3.83472H11.4996V6.25H13.1867C13.6009 6.25 13.9367 6.58579 13.9367 7C13.9367 7.41421 13.6009 7.75 13.1867 7.75H11.4996V10.1653H13.1867C13.6009 10.1653 13.9367 10.5011 13.9367 10.9153C13.9367 11.3295 13.6009 11.6653 13.1867 11.6653H11.4996V12.5C11.4996 13.3284 10.828 14 9.99963 14H3.99963C3.1712 14 2.49963 13.3284 2.49963 12.5V11.6653H0.8125C0.398287 11.6653 0.0625 11.3295 0.0625 10.9153C0.0625 10.5011 0.398287 10.1653 0.8125 10.1653H2.49963V7.75H0.812561C0.398348 7.75 0.062561 7.41421 0.062561 7C0.062561 6.58579 0.398348 6.25 0.812561 6.25H2.49963V3.83472H0.8125C0.398287 3.83472 0.0625 3.49893 0.0625 3.08472C0.0625 2.6705 0.398287 2.33472 0.8125 2.33472H2.49963V1.5ZM7.34771 11.1825C7.00254 11.1825 6.72271 11.4623 6.72271 11.8075C6.72271 12.1527 7.00254 12.4325 7.34771 12.4325H9.34771C9.69289 12.4325 9.97271 12.1527 9.97271 11.8075C9.97271 11.4623 9.69289 11.1825 9.34771 11.1825H7.34771Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187705\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"computer-pc-desktop\", \"keywords\": [ \"screen\", \"desktop\", \"monitor\", \"device\", \"electronics\", \"display\", \"pc\", \"computer\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8 2C7.44771 2 7 2.44772 7 3V3.46875H7.5C9.01878 3.46875 10.25 4.69997 10.25 6.21875V6.375H14V3C14 2.44772 13.5523 2 13 2H8ZM7.89113 13.8713C7.96169 13.6668 8 13.4472 8 13.2188C8 13.1197 7.9928 13.0224 7.9789 12.9272C9.26939 12.7006 10.25 11.5742 10.25 10.2188V7.625H14V13C14 13.5523 13.5523 14 13 14H8.5C8.28323 14 8.0772 13.954 7.89113 13.8713ZM12.6719 4.1875C12.6719 4.61034 12.3291 4.95312 11.9062 4.95312C11.4834 4.95312 11.1406 4.61034 11.1406 4.1875C11.1406 3.76466 11.4834 3.42188 11.9062 3.42188C12.3291 3.42188 12.6719 3.76466 12.6719 4.1875ZM0 6.21875C0 5.39032 0.671573 4.71875 1.5 4.71875H7.5C8.32843 4.71875 9 5.39032 9 6.21875V10.2188C9 11.0472 8.32843 11.7188 7.5 11.7188H5.25V12.4688H6C6.41421 12.4688 6.75 12.8045 6.75 13.2188C6.75 13.633 6.41421 13.9688 6 13.9688H4.5H3C2.58579 13.9688 2.25 13.633 2.25 13.2188C2.25 12.8045 2.58579 12.4688 3 12.4688H3.75V11.7188H1.5C0.671573 11.7188 0 11.0472 0 10.2188V6.21875Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"controller\", \"keywords\": [ \"remote\", \"quadcopter\", \"drones\", \"flying\", \"drone\", \"control\", \"controller\", \"technology\", \"fly\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187768)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.0464 0.991089C11.0464 0.576875 10.7106 0.241089 10.2964 0.241089C9.88216 0.241089 9.54637 0.576875 9.54637 0.991089V1.87502C9.54637 2.01309 9.43444 2.12502 9.29637 2.12502H7.9995C7.033 2.12502 6.2495 2.90852 6.2495 3.87502V4.75897H4.18463C2.65598 4.75897 1.37179 5.90838 1.20297 7.42768L0.783903 11.1994C0.632187 12.5648 1.70103 13.759 3.07488 13.759C3.94798 13.759 4.74614 13.2657 5.13661 12.4848L5.4995 11.759H8.4995L8.86239 12.4848C9.25285 13.2657 10.051 13.759 10.9241 13.759C12.298 13.759 13.3668 12.5648 13.2151 11.1994L12.796 7.42768C12.6272 5.90838 11.343 4.75897 9.81437 4.75897H7.7495V3.87502C7.7495 3.73695 7.86143 3.62502 7.9995 3.62502H9.29637C10.2629 3.62502 11.0464 2.84151 11.0464 1.87502V0.991089ZM4.37451 6.63397C4.71969 6.63397 4.99951 6.91379 4.99951 7.25897V7.80066H5.54126C5.88644 7.80066 6.16626 8.08048 6.16626 8.42566C6.16626 8.77084 5.88644 9.05066 5.54126 9.05066H4.99951V9.5923C4.99951 9.93748 4.71969 10.2173 4.37451 10.2173C4.02933 10.2173 3.74951 9.93748 3.74951 9.5923V9.05066H3.20792C2.86275 9.05066 2.58292 8.77084 2.58292 8.42566C2.58292 8.08048 2.86275 7.80066 3.20792 7.80066H3.74951V7.25897C3.74951 6.91379 4.02933 6.63397 4.37451 6.63397ZM9.26513 9.67566C9.61031 9.67566 9.89013 9.39584 9.89013 9.05066C9.89013 8.70548 9.61031 8.42566 9.26513 8.42566C8.91996 8.42566 8.64013 8.70548 8.64013 9.05066C8.64013 9.39584 8.91996 9.67566 9.26513 9.67566ZM11.1401 7.50116C11.1401 7.84634 10.8603 8.12616 10.5151 8.12616C10.1699 8.12616 9.89013 7.84634 9.89013 7.50116C9.89013 7.15598 10.1699 6.87616 10.5151 6.87616C10.8603 6.87616 11.1401 7.15598 11.1401 7.50116Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187768\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"controller-1\", \"keywords\": [ \"remote\", \"quadcopter\", \"drones\", \"flying\", \"drone\", \"control\", \"controller\", \"technology\", \"fly\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.20298 5.1687L0.783903 8.94037C0.632187 10.3058 1.70103 11.5 3.07488 11.5C3.94798 11.5 4.74614 11.0067 5.13661 10.2258L5.4995 9.5H8.4995L8.86239 10.2258C9.25285 11.0067 10.051 11.5 10.9241 11.5C12.298 11.5 13.3668 10.3058 13.2151 8.94037L12.796 5.16871C12.6272 3.64941 11.343 2.5 9.81437 2.5H4.18463C2.65598 2.5 1.37179 3.64941 1.20298 5.1687ZM4.37451 4.375C4.71969 4.375 4.99951 4.65482 4.99951 5V5.54175H5.54126C5.88644 5.54175 6.16626 5.82157 6.16626 6.16675C6.16626 6.51193 5.88644 6.79175 5.54126 6.79175H4.99951V7.33333C4.99951 7.67851 4.71969 7.95833 4.37451 7.95833C4.02934 7.95833 3.74951 7.67851 3.74951 7.33333V6.79175H3.20793C2.86275 6.79175 2.58293 6.51193 2.58293 6.16675C2.58293 5.82157 2.86275 5.54175 3.20793 5.54175H3.74951V5C3.74951 4.65482 4.02934 4.375 4.37451 4.375ZM9.26514 7.41675C9.61032 7.41675 9.89014 7.13693 9.89014 6.79175C9.89014 6.44657 9.61032 6.16675 9.26514 6.16675C8.91996 6.16675 8.64014 6.44657 8.64014 6.79175C8.64014 7.13693 8.91996 7.41675 9.26514 7.41675ZM11.1401 5.24219C11.1401 5.58737 10.8603 5.86719 10.5151 5.86719C10.17 5.86719 9.89014 5.58737 9.89014 5.24219C9.89014 4.89701 10.17 4.61719 10.5151 4.61719C10.8603 4.61719 11.1401 4.89701 11.1401 5.24219Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"controller-wireless\", \"keywords\": [ \"remote\", \"gaming\", \"drones\", \"drone\", \"control\", \"controller\", \"technology\", \"console\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187714)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.01947 1.95251C5.26596 1.95251 3.9843 2.54249 3.63184 2.77747C3.28719 3.00723 2.82154 2.9141 2.59178 2.56945C2.36201 2.22481 2.45514 1.75916 2.79979 1.52939C3.39825 1.13042 4.96933 0.452515 7.01947 0.452515C9.06961 0.452515 10.6407 1.13042 11.2391 1.52939C11.5838 1.75916 11.6769 2.22481 11.4471 2.56945C11.2174 2.9141 10.7517 3.00723 10.4071 2.77747C10.0546 2.54249 8.77299 1.95251 7.01947 1.95251ZM7.0195 4.80524C6.42198 4.80524 5.71791 5.0691 5.45308 5.20151C5.0826 5.38676 4.63209 5.23659 4.44685 4.8661C4.26161 4.49562 4.41178 4.04511 4.78226 3.85987C5.15138 3.67532 6.09555 3.30524 7.0195 3.30524C7.94345 3.30524 8.88762 3.67532 9.25674 3.85987C9.62722 4.04511 9.77739 4.49562 9.59215 4.8661C9.40691 5.23659 8.9564 5.38676 8.58592 5.20151C8.32109 5.0691 7.61702 4.80524 7.0195 4.80524ZM0.0273438 9.93751C0.0273438 7.94377 1.64359 6.32751 3.63734 6.32751C3.67911 6.32751 3.72073 6.32823 3.76219 6.32964C3.77747 6.32823 3.79295 6.32751 3.80859 6.32751H10.3398L10.3475 6.32757L10.3631 6.32751C12.3569 6.32751 13.9731 7.94377 13.9731 9.93751C13.9731 11.9313 12.3569 13.5475 10.3631 13.5475C9.53346 13.5475 8.76818 13.2671 8.15847 12.7963H5.84204C5.23233 13.2671 4.46705 13.5475 3.63734 13.5475C1.64359 13.5475 0.0273438 11.9313 0.0273438 9.93751ZM9.58818 11.4931C10.0107 11.4931 10.3533 11.1505 10.3533 10.7279C10.3533 10.3053 10.0107 9.96271 9.58818 9.96271C9.16558 9.96271 8.82299 10.3053 8.82299 10.7279C8.82299 11.1505 9.16558 11.4931 9.58818 11.4931ZM12.1124 8.84727C12.1124 9.26987 11.7698 9.61246 11.3472 9.61246C10.9246 9.61246 10.582 9.26987 10.582 8.84727C10.582 8.42468 10.9246 8.08209 11.3472 8.08209C11.7698 8.08209 12.1124 8.42468 12.1124 8.84727ZM1.73901 9.92073C1.73901 9.57555 2.01883 9.29573 2.36401 9.29573H2.81218V8.84729C2.81218 8.50211 3.092 8.22229 3.43718 8.22229C3.78236 8.22229 4.06218 8.50211 4.06218 8.84729V9.29573H4.51021C4.85539 9.29573 5.13521 9.57555 5.13521 9.92073C5.13521 10.2659 4.85539 10.5457 4.51021 10.5457H4.06218V10.9935C4.06218 11.3387 3.78236 11.6185 3.43718 11.6185C3.092 11.6185 2.81218 11.3387 2.81218 10.9935V10.5457H2.36401C2.01883 10.5457 1.73901 10.2659 1.73901 9.92073Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187714\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cursor-click\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187747)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.65886 0.162094C4.05896 0.0548882 4.47021 0.292325 4.57742 0.692424L5.02566 2.36529C5.13287 2.76539 4.89543 3.17664 4.49533 3.28385C4.09523 3.39105 3.68398 3.15361 3.57677 2.75352L3.12853 1.08065C3.02132 0.680553 3.25876 0.269301 3.65886 0.162094ZM0.0726167 3.75456C-0.0345896 4.15466 0.202848 4.56591 0.602947 4.67312L2.27581 5.12136C2.67591 5.22857 3.08716 4.99113 3.19437 4.59103C3.30158 4.19093 3.06414 3.77968 2.66404 3.67247L0.991176 3.22423C0.591076 3.11702 0.179824 3.35446 0.0726167 3.75456ZM4.47227 5.71964C4.2114 4.93772 4.95543 4.19369 5.73735 4.45457L12.8564 6.82969C13.7764 7.13663 13.7648 8.442 12.8394 8.73241L10.8125 9.36852L13.5125 12.0685C13.903 12.459 13.903 13.0921 13.5125 13.4827C13.122 13.8732 12.4888 13.8732 12.0983 13.4827L9.39199 10.7764L8.75011 12.8216C8.4597 13.747 7.15433 13.7587 6.84739 12.8387L4.47227 5.71964ZM2.06763 8.75488C1.77474 9.04778 1.29986 9.04778 1.00697 8.75488C0.714073 8.46199 0.714073 7.98712 1.00697 7.69422L2.23159 6.4696C2.52448 6.17671 2.99936 6.17671 3.29225 6.4696C3.58514 6.7625 3.58514 7.23737 3.29225 7.53026L2.06763 8.75488ZM8.52771 2.16357C8.8206 1.87068 8.8206 1.39581 8.52771 1.10291C8.23482 0.810019 7.75995 0.810019 7.46705 1.10291L6.24243 2.32753C5.94954 2.62043 5.94954 3.0953 6.24243 3.38819C6.53533 3.68109 7.0102 3.68109 7.30309 3.38819L8.52771 2.16357Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187747\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cyborg\", \"keywords\": [ \"artificial\", \"robotics\", \"intelligence\", \"machine\", \"technology\", \"android\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.43693 1.60843C5.43693 0.745604 6.13639 0.0461426 6.99922 0.0461426C7.86205 0.0461426 8.56151 0.745604 8.56151 1.60843C8.56151 2.19946 8.23332 2.71384 7.74923 2.97926V4.05454C9.05295 4.16913 10.2477 4.58724 11.1692 5.31333C12.2777 6.18685 12.9575 7.48221 12.9575 9.12299V9.4873H11.5183C10.9742 9.4873 10.4661 9.75964 10.165 10.2128L9.87143 10.6544C9.80192 10.759 9.68468 10.8218 9.55912 10.8218H4.43932C4.31375 10.8218 4.19652 10.759 4.12701 10.6544L3.83351 10.2128C3.53232 9.75964 3.0243 9.4873 2.48017 9.4873H1.04694L1.04102 9.48733V9.12299C1.04102 7.48221 1.72077 6.18685 2.82933 5.31333C3.75079 4.58724 4.94551 4.16913 6.24923 4.05454V2.97927C5.76513 2.71385 5.43693 2.19947 5.43693 1.60843ZM1.04102 10.7373V12.4537C1.04102 13.2822 1.71259 13.9537 2.54102 13.9537H11.4575C12.2859 13.9537 12.9575 13.2822 12.9575 12.4537V10.7373H11.5183C11.3927 10.7373 11.2755 10.8002 11.206 10.9047L10.9125 11.3463C10.6113 11.7995 10.1033 12.0718 9.55912 12.0718H4.43932C3.89519 12.0718 3.38718 11.7995 3.08598 11.3463L2.79248 10.9047C2.72298 10.8002 2.60574 10.7373 2.48017 10.7373H1.04694H1.04102ZM4.74923 7C4.33502 7 3.99923 7.33579 3.99923 7.75C3.99923 8.16421 4.33502 8.5 4.74923 8.5C5.16345 8.5 5.49923 8.16421 5.49923 7.75C5.49923 7.33579 5.16345 7 4.74923 7ZM8.49923 7.75C8.49923 7.33579 8.83502 7 9.24923 7C9.66345 7 9.99923 7.33579 9.99923 7.75C9.99923 8.16421 9.66345 8.5 9.24923 8.5C8.83502 8.5 8.49923 8.16421 8.49923 7.75Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"cyborg-2\", \"keywords\": [ \"artificial\", \"robotics\", \"intelligence\", \"machine\", \"technology\", \"android\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.33325 1.85417C5.33325 0.933692 6.07944 0.1875 6.99992 0.1875C7.92039 0.1875 8.66658 0.933692 8.66658 1.85417C8.66658 2.50484 8.29372 3.06843 7.75 3.34291V4.8125H10.5C11.8807 4.8125 13 5.93179 13 7.3125V11.3125C13 12.6932 11.8807 13.8125 10.5 13.8125H3.5C2.11929 13.8125 1 12.6932 1 11.3125V7.3125C1 5.93179 2.11929 4.8125 3.5 4.8125H6.25V3.34299C5.70619 3.06854 5.33325 2.50491 5.33325 1.85417ZM9.20474 11.1788C9.44154 10.9276 9.42991 10.5321 9.17876 10.2953C8.92761 10.0585 8.53205 10.0701 8.29526 10.3212C8.06978 10.5604 7.58797 10.7436 7 10.7436C6.41203 10.7436 5.93022 10.5604 5.70474 10.3212C5.46795 10.0701 5.07239 10.0585 4.82124 10.2953C4.57009 10.5321 4.55846 10.9276 4.79526 11.1788C5.34745 11.7644 6.22681 11.9936 7 11.9936C7.77319 11.9936 8.65255 11.7644 9.20474 11.1788ZM5.04175 9.04163C5.45596 9.04163 5.79175 8.70584 5.79175 8.29163C5.79175 7.87741 5.45596 7.54163 5.04175 7.54163C4.62753 7.54163 4.29175 7.87741 4.29175 8.29163C4.29175 8.70584 4.62753 9.04163 5.04175 9.04163ZM9.70825 8.29163C9.70825 8.70584 9.37247 9.04163 8.95825 9.04163C8.54404 9.04163 8.20825 8.70584 8.20825 8.29163C8.20825 7.87741 8.54404 7.54163 8.95825 7.54163C9.37247 7.54163 9.70825 7.87741 9.70825 8.29163Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"database\", \"keywords\": [ \"raid\", \"storage\", \"code\", \"disk\", \"programming\", \"database\", \"array\", \"hard\", \"disc\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187753)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.55188 2.27781C0.55188 2.12293 0.6289 1.90982 0.909084 1.64844C1.19023 1.38617 1.63106 1.12121 2.22805 0.885658C3.41831 0.416012 5.10532 0.111816 7.00033 0.111816C8.89534 0.111816 10.5824 0.416012 11.7726 0.885658C12.3696 1.12121 12.8105 1.38617 13.0916 1.64844C13.3718 1.90982 13.4488 2.12293 13.4488 2.27781C13.4488 2.43268 13.3718 2.6458 13.0916 2.90717C12.8105 3.16944 12.3696 3.4344 11.7726 3.66996C10.5824 4.13961 8.89534 4.4438 7.00033 4.4438C5.10532 4.4438 3.41831 4.13961 2.22805 3.66996C1.63106 3.4344 1.19023 3.16944 0.909084 2.90717C0.6289 2.6458 0.55188 2.43268 0.55188 2.27781ZM13.5125 4.16706C13.1452 4.42228 12.7099 4.64392 12.2314 4.83272C10.8616 5.37321 9.01188 5.6938 7.00033 5.6938C4.98878 5.6938 3.13907 5.37321 1.76925 4.83272C1.2908 4.64393 0.855523 4.42231 0.488281 4.1671V7.34491C0.54425 7.42981 0.623747 7.52292 0.734474 7.6236C1.02447 7.88728 1.47895 8.15346 2.09352 8.38989C3.31851 8.86114 5.05313 9.16597 7.00037 9.16597C8.9476 9.16597 10.6823 8.86114 11.9072 8.38989C12.5218 8.15346 12.9763 7.88728 13.2663 7.6236C13.377 7.52293 13.4565 7.42982 13.5125 7.34492V4.16706ZM0.488281 11.209V8.99271C0.829445 9.20553 1.22041 9.39331 1.64472 9.55654C3.04606 10.0957 4.93973 10.416 7.00037 10.416C9.061 10.416 10.9547 10.0957 12.356 9.55654C12.7803 9.39331 13.1713 9.20554 13.5125 8.99271V11.209C13.5125 12.7496 10.5971 14 7.00036 14C3.40369 14 0.488281 12.7496 0.488281 11.209Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187753\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"database-check\", \"keywords\": [ \"raid\", \"storage\", \"code\", \"disk\", \"programming\", \"database\", \"array\", \"hard\", \"disc\", \"check\", \"approve\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187777)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.193603 1.96883C0.193603 1.8576 0.248209 1.68606 0.487859 1.46249C0.728466 1.23804 1.11064 1.00713 1.63515 0.800166C2.68046 0.387717 4.16722 0.118896 5.84103 0.118896C7.51484 0.118896 9.0016 0.387717 10.0469 0.800166C10.5714 1.00713 10.9536 1.23804 11.1942 1.46249C11.4338 1.68606 11.4884 1.8576 11.4884 1.96883C11.4884 2.08007 11.4338 2.25161 11.1942 2.47518C10.9536 2.69963 10.5714 2.93054 10.0469 3.1375C9.0016 3.54995 7.51484 3.81877 5.84103 3.81877C4.16722 3.81877 2.68046 3.54995 1.63515 3.1375C1.11064 2.93054 0.728466 2.69963 0.487859 2.47518C0.248209 2.25161 0.193603 2.08007 0.193603 1.96883ZM11.6157 3.72999C11.2929 3.94863 10.9161 4.13833 10.5057 4.30026C9.28084 4.78356 7.63139 5.06877 5.84103 5.06877C4.05068 5.06877 2.40122 4.78356 1.17636 4.30026C0.765981 4.13833 0.389112 3.94864 0.0664062 3.72999V6.31445C0.105706 6.40745 0.184266 6.52227 0.332313 6.65689C0.580721 6.88276 0.975031 7.11489 1.51525 7.32271C2.59153 7.73675 4.12072 8.00619 5.84102 8.00619C7.56132 8.00619 9.0905 7.73675 10.1668 7.32271C10.707 7.11489 11.1013 6.88276 11.3497 6.65689C11.4978 6.52223 11.5764 6.40738 11.6157 6.31437V3.72999ZM0.0664062 9.88862V8.00649C0.364766 8.18871 0.702665 8.34941 1.06645 8.48936C2.31908 8.97124 4.00731 9.25619 5.84102 9.25619C7.67472 9.25619 9.36295 8.97124 10.6156 8.48936L10.645 8.47796L8.91254 10.427L8.38475 10.0311C7.5011 9.36844 6.24749 9.54753 5.58475 10.4311C5.1553 11.0037 5.07933 11.7317 5.31949 12.3535C2.37453 12.2405 0.0664062 11.1794 0.0664062 9.88862ZM13.7453 9.12946C14.0205 8.81987 13.9926 8.34581 13.683 8.07062C13.3734 7.79543 12.8994 7.82332 12.6242 8.13291L9.08267 12.1171L7.63475 11.0311C7.30338 10.7826 6.83328 10.8498 6.58475 11.1811C6.33623 11.5125 6.40338 11.9826 6.73475 12.2311L8.73475 13.7311C9.04681 13.9652 9.48616 13.9209 9.74531 13.6294L13.7453 9.12946Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187777\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"database-lock\", \"keywords\": [ \"raid\", \"storage\", \"code\", \"disk\", \"programming\", \"database\", \"array\", \"hard\", \"disc\", \"password\", \"security\", \"protection\", \"lock\", \"secure\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187750)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.127136 1.84993C0.127136 1.7387 0.181743 1.56716 0.421392 1.34359C0.661999 1.11914 1.04417 0.888229 1.56868 0.681271C2.61399 0.26882 4.10075 0 5.77456 0C7.44837 0 8.93513 0.26882 9.98044 0.681271C10.505 0.888229 10.8871 1.11914 11.1277 1.34359C11.3674 1.56716 11.422 1.7387 11.422 1.84993C11.422 1.96117 11.3674 2.13271 11.1277 2.35628C10.8871 2.58073 10.505 2.81164 9.98044 3.0186C8.93513 3.43105 7.44837 3.69987 5.77456 3.69987C4.10075 3.69987 2.61399 3.43105 1.56868 3.0186C1.04417 2.81164 0.661999 2.58073 0.421392 2.35628C0.181743 2.13271 0.127136 1.96117 0.127136 1.84993ZM11.5493 3.61105C11.2265 3.82971 10.8496 4.01942 10.4392 4.18136C9.21437 4.66466 7.56492 4.94987 5.77456 4.94987C3.98421 4.94987 2.33476 4.66466 1.10989 4.18136C0.69954 4.01944 0.322693 3.82976 0 3.61114V6.19555C0.0393004 6.28855 0.11786 6.40338 0.265907 6.53799C0.514315 6.76386 0.908625 6.99598 1.44885 7.20381C2.52512 7.61785 4.05431 7.88729 5.77461 7.88729C6.07745 7.88729 6.37437 7.87894 6.66393 7.86292C7.15382 6.20767 8.68588 5 10.5 5C10.8631 5 11.2149 5.04838 11.5493 5.13903V3.61105ZM5.77461 9.13729C5.92196 9.13729 6.06838 9.13545 6.2137 9.13181C5.92293 9.51061 5.75 9.98473 5.75 10.5V12.2446C2.57196 12.2389 0 11.1324 0 9.76972V7.8876C0.29836 8.06982 0.636259 8.23051 1.00004 8.37046C2.25268 8.85234 3.94091 9.13729 5.77461 9.13729ZM10.5 7.75C9.80964 7.75 9.25 8.30964 9.25 9V9.5H11.75V9C11.75 8.30964 11.1904 7.75 10.5 7.75ZM7.75 9V9.5315C7.31869 9.64252 7 10.034 7 10.5V13C7 13.5523 7.44772 14 8 14H13C13.5523 14 14 13.5523 14 13V10.5C14 10.034 13.6813 9.64252 13.25 9.5315V9C13.25 7.48122 12.0188 6.25 10.5 6.25C8.98122 6.25 7.75 7.48122 7.75 9Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187750\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"database-refresh\", \"keywords\": [ \"raid\", \"storage\", \"code\", \"disk\", \"programming\", \"database\", \"array\", \"hard\", \"disc\", \"refresh\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187759)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.127136 1.84993C0.127136 1.7387 0.181743 1.56716 0.421392 1.34359C0.661999 1.11914 1.04417 0.888229 1.56868 0.681271C2.61399 0.26882 4.10075 0 5.77456 0C7.44837 0 8.93513 0.26882 9.98044 0.681271C10.505 0.888229 10.8871 1.11914 11.1277 1.34359C11.3674 1.56716 11.422 1.7387 11.422 1.84993C11.422 1.96117 11.3674 2.13271 11.1277 2.35628C10.8871 2.58073 10.505 2.81164 9.98044 3.0186C8.93513 3.43105 7.44837 3.69987 5.77456 3.69987C4.10075 3.69987 2.61399 3.43105 1.56868 3.0186C1.04417 2.81164 0.661999 2.58073 0.421392 2.35628C0.181743 2.13271 0.127136 1.96117 0.127136 1.84993ZM11.5493 3.61105C11.2265 3.82971 10.8496 4.01942 10.4392 4.18136C9.21437 4.66466 7.56492 4.94987 5.77456 4.94987C3.98421 4.94987 2.33476 4.66466 1.10989 4.18136C0.69954 4.01944 0.322693 3.82976 0 3.61114V6.19555C0.0393004 6.28855 0.11786 6.40338 0.265907 6.53799C0.514315 6.76386 0.908625 6.99598 1.44885 7.20381C2.52512 7.61785 4.05431 7.88729 5.77461 7.88729C6.22613 7.88729 6.66449 7.86873 7.08494 7.83386C7.39823 7.69108 7.73876 7.64075 8.06909 7.67767C8.12534 7.46982 8.22062 7.26731 8.36287 7.08024C8.65282 6.69893 9.03695 6.51574 9.32459 6.42549C9.86775 6.25507 10.4423 6.29886 10.7381 6.33494C10.9533 6.36118 11.163 6.40235 11.3663 6.45727C11.4621 6.35692 11.5181 6.26931 11.5493 6.19547V3.61105ZM1.00004 8.37046C2.18102 8.82478 3.74917 9.10404 5.46139 9.13451C5.16914 9.57172 5.06827 10.1213 5.20552 10.6489C5.306 11.0351 5.52372 11.3695 5.81692 11.6148C5.85703 11.8295 5.91271 12.0393 5.98285 12.2431C5.91374 12.2441 5.84433 12.2447 5.77463 12.2447C2.58526 12.2447 0 11.1359 0 9.76972V7.8876C0.29836 8.06982 0.636259 8.23051 1.00004 8.37046ZM13.4034 10.7476L13.4611 10.7556C13.7115 10.7904 13.9163 10.9727 13.9799 11.2173C14.0435 11.462 13.9536 11.7208 13.7519 11.8733L12.9109 12.5089C12.6699 12.691 12.3397 12.6721 12.1215 12.48C12.0903 12.4526 12.0614 12.4216 12.0354 12.3872L11.3998 11.5462C11.2664 11.3697 11.2375 11.1382 11.3181 10.937C11.3296 10.9083 11.3434 10.8801 11.3594 10.8529C11.4874 10.6349 11.7341 10.5155 11.9845 10.5503L12.1448 10.5726C12.0664 9.7334 11.449 9.02387 10.6068 8.84511C10.5506 8.8332 10.4935 8.82364 10.4354 8.81656C10.2724 8.79668 10.1125 8.79744 9.958 8.81647C9.65823 8.85338 9.38194 8.67154 9.2893 8.396C9.27606 8.35663 9.26658 8.31536 9.2613 8.27253L9.26106 8.27053C9.23066 8.01926 9.31193 7.85143 9.45256 7.74164C9.5461 7.6532 9.66768 7.59278 9.80524 7.57584C9.92995 7.56048 10.0564 7.5524 10.1841 7.55195C10.3283 7.55051 10.4684 7.56132 10.5867 7.57575C12.21 7.77372 13.401 9.15311 13.4034 10.7476ZM7.48419 9.04251C7.75956 8.83438 8.15152 8.88888 8.35965 9.16425L8.99531 10.0053C9.14774 10.2069 9.16367 10.4805 9.03569 10.6985C8.9077 10.9165 8.661 11.036 8.41062 11.0012L8.24998 10.9788C8.33344 11.8742 9.03033 12.6221 9.95943 12.7354C10.1224 12.7552 10.2823 12.7545 10.4368 12.7355C10.7794 12.6933 11.0914 12.9368 11.1335 13.2794C11.1757 13.622 10.9322 13.9339 10.5896 13.9761C10.3349 14.0075 10.0729 14.0085 9.80811 13.9762C8.38757 13.8029 7.2981 12.725 7.04651 11.3899C7.01057 11.1991 6.99172 11.0031 6.99146 10.8038L6.93396 10.7958C6.71488 10.7653 6.53073 10.622 6.44548 10.4228C6.4333 10.3943 6.42314 10.3647 6.41519 10.3341C6.35949 10.1201 6.42143 9.89509 6.57288 9.74008C6.59452 9.71794 6.61798 9.69722 6.64319 9.67817L7.48419 9.04251Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187759\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"database-remove\", \"keywords\": [ \"raid\", \"storage\", \"code\", \"disk\", \"programming\", \"database\", \"array\", \"hard\", \"disc\", \"remove\", \"delete\", \"cross\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187802)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.256042 1.96175C0.256042 1.85051 0.310648 1.67897 0.550298 1.4554C0.790904 1.23095 1.17309 1.00004 1.6976 0.793086C2.7429 0.380636 4.22965 0.111816 5.90346 0.111816C7.57728 0.111816 9.06404 0.380636 10.1093 0.793086C10.6338 1.00004 11.016 1.23095 11.2566 1.4554C11.4962 1.67897 11.5509 1.85051 11.5509 1.96175C11.5509 2.07298 11.4962 2.24452 11.2566 2.46809C11.016 2.69254 10.6338 2.92345 10.1093 3.13041C9.06404 3.54286 7.57728 3.81168 5.90346 3.81168C4.22965 3.81168 2.7429 3.54286 1.6976 3.13041C1.17309 2.92345 0.790904 2.69254 0.550298 2.46809C0.310648 2.24452 0.256042 2.07298 0.256042 1.96175ZM11.6781 3.72286C11.3554 3.94152 10.9785 4.13123 10.5681 4.29317C9.34328 4.77647 7.69382 5.06168 5.90346 5.06168C4.11311 5.06168 2.46367 4.77647 1.2388 4.29317C0.828446 4.13125 0.451599 3.94157 0.128906 3.72295V6.30736C0.168206 6.40036 0.246766 6.51519 0.394813 6.6498C0.643221 6.87567 1.03754 7.1078 1.57776 7.31562C2.65403 7.72966 4.18322 7.9991 5.90351 7.9991C6.39361 7.9991 6.86821 7.97723 7.32123 7.93638C7.39822 7.81558 7.48945 7.70125 7.59494 7.59576C8.2843 6.9064 9.35171 6.82548 10.1298 7.35299C10.1633 7.34067 10.1965 7.32821 10.2292 7.31562C10.7695 7.1078 11.1638 6.87567 11.4122 6.6498C11.5603 6.51515 11.6388 6.40029 11.6781 6.30728V3.72286ZM5.90351 9.2491C6.28256 9.2491 6.65541 9.23693 7.01945 9.21336C7.06438 9.65547 7.25622 10.0855 7.59494 10.4242L8.30073 11.13L7.59494 11.8358C7.45739 11.9733 7.34405 12.1259 7.25495 12.2883C6.82144 12.3329 6.36893 12.3565 5.90353 12.3565C2.71416 12.3565 0.128906 11.2477 0.128906 9.88153V7.99941C0.427266 8.18163 0.765165 8.34233 1.12895 8.48228C2.38159 8.96416 4.06981 9.2491 5.90351 9.2491ZM11.1222 8.29466L11.1481 8.28261L11.1291 8.30155L11.1222 8.29466ZM13.7794 8.47964C14.0723 8.77254 14.0723 9.24742 13.7794 9.54031L12.1898 11.13L13.7794 12.7197C14.0723 13.0126 14.0723 13.4874 13.7794 13.7803C13.4865 14.0732 13.0117 14.0732 12.7188 13.7803L11.1291 12.1907L9.53949 13.7803C9.2466 14.0732 8.77172 14.0732 8.47883 13.7803C8.18594 13.4874 8.18594 13.0126 8.47883 12.7197L10.0684 11.13L8.47883 9.54031C8.18594 9.24742 8.18594 8.77254 8.47883 8.47964C8.77172 8.18676 9.2466 8.18676 9.53949 8.47964L11.1291 10.0693L12.7188 8.47964C13.0117 8.18676 13.4865 8.18676 13.7794 8.47964Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187802\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"database-server-1\", \"keywords\": [ \"server\", \"network\", \"internet\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187756)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.50003 0.407227C0.671604 0.407227 3.23653e-05 1.0788 2.96235e-05 1.90722L0 4.8318C0 5.66023 0.671573 6.3318 1.5 6.3318H12.5C13.3284 6.3318 14 5.66023 14 4.83181V1.90723C14 1.0788 13.3285 0.407227 12.5 0.407227H1.50003ZM2.96235e-05 9.16825C3.23653e-05 8.33983 0.671604 7.66826 1.50003 7.66826H12.5C13.3285 7.66826 14 8.33983 14 9.16826V12.0928C14 12.9212 13.3284 13.5928 12.5 13.5928H1.5C0.671573 13.5928 0 12.9212 0 12.0928L2.96235e-05 9.16825ZM2.04645 3.36952C2.04645 2.7482 2.55013 2.24452 3.17145 2.24452C3.79277 2.24452 4.29645 2.7482 4.29645 3.36952C4.29645 3.99084 3.79277 4.49452 3.17145 4.49452C2.55013 4.49452 2.04645 3.99084 2.04645 3.36952ZM2.04645 10.6305C2.04645 10.0092 2.55013 9.50555 3.17145 9.50555C3.79277 9.50555 4.29645 10.0092 4.29645 10.6305C4.29645 11.2518 3.79277 11.7555 3.17145 11.7555C2.55013 11.7555 2.04645 11.2518 2.04645 10.6305ZM6.875 3.36952C6.875 3.02434 7.15482 2.74452 7.5 2.74452H11C11.3452 2.74452 11.625 3.02434 11.625 3.36952C11.625 3.7147 11.3452 3.99452 11 3.99452H7.5C7.15482 3.99452 6.875 3.7147 6.875 3.36952ZM7.5 10.0055C7.15482 10.0055 6.875 10.2853 6.875 10.6305C6.875 10.9757 7.15482 11.2555 7.5 11.2555H11C11.3452 11.2555 11.625 10.9757 11.625 10.6305C11.625 10.2853 11.3452 10.0055 11 10.0055H7.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187756\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"database-server-2\", \"keywords\": [ \"server\", \"network\", \"internet\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187774)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1.5C0 0.671573 0.671573 0 1.5 0H12.5C13.3284 0 14 0.671573 14 1.5V3C14 3.82843 13.3284 4.5 12.5 4.5H1.5C0.671573 4.5 0 3.82843 0 3V1.5ZM0 7.5C0 6.67157 0.671573 6 1.5 6H12.5C13.3284 6 14 6.67157 14 7.5V9C14 9.82843 13.3284 10.5 12.5 10.5H7.75V12.4868H12C12.4142 12.4868 12.75 12.8226 12.75 13.2368C12.75 13.651 12.4142 13.9868 12 13.9868H7H2C1.58579 13.9868 1.25 13.651 1.25 13.2368C1.25 12.8226 1.58579 12.4868 2 12.4868H6.25V10.5H1.5C0.671573 10.5 0 9.82843 0 9V7.5ZM3.5 2.25C3.5 2.66421 3.16421 3 2.75 3C2.33579 3 2 2.66421 2 2.25C2 1.83579 2.33579 1.5 2.75 1.5C3.16421 1.5 3.5 1.83579 3.5 2.25ZM7.83057 1.625C7.48539 1.625 7.20557 1.90482 7.20557 2.25C7.20557 2.59518 7.48539 2.875 7.83057 2.875H11C11.3452 2.875 11.625 2.59518 11.625 2.25C11.625 1.90482 11.3452 1.625 11 1.625H7.83057ZM3.5 8.25C3.5 8.66421 3.16421 9 2.75 9C2.33579 9 2 8.66421 2 8.25C2 7.83579 2.33579 7.5 2.75 7.5C3.16421 7.5 3.5 7.83579 3.5 8.25ZM7.20557 8.25C7.20557 7.90482 7.48539 7.625 7.83057 7.625H11C11.3452 7.625 11.625 7.90482 11.625 8.25C11.625 8.59518 11.3452 8.875 11 8.875H7.83057C7.48539 8.875 7.20557 8.59518 7.20557 8.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187774\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"database-setting\", \"keywords\": [ \"raid\", \"storage\", \"code\", \"disk\", \"programming\", \"database\", \"array\", \"hard\", \"disc\", \"setting\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187799)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.127136 1.84993C0.127136 1.7387 0.181743 1.56716 0.421392 1.34359C0.661999 1.11914 1.04417 0.888229 1.56868 0.681271C2.61399 0.26882 4.10075 0 5.77456 0C7.44837 0 8.93513 0.26882 9.98044 0.681271C10.505 0.888229 10.8871 1.11914 11.1277 1.34359C11.3674 1.56716 11.422 1.7387 11.422 1.84993C11.422 1.96117 11.3674 2.13271 11.1277 2.35628C10.8871 2.58073 10.505 2.81164 9.98044 3.0186C8.93513 3.43105 7.44837 3.69987 5.77456 3.69987C4.10075 3.69987 2.61399 3.43105 1.56868 3.0186C1.04417 2.81164 0.661999 2.58073 0.421392 2.35628C0.181743 2.13271 0.127136 1.96117 0.127136 1.84993ZM11.5493 3.61105C11.2265 3.82971 10.8496 4.01942 10.4392 4.18136C9.21437 4.66466 7.56492 4.94987 5.77456 4.94987C3.98421 4.94987 2.33476 4.66466 1.10989 4.18136C0.69954 4.01944 0.322693 3.82976 0 3.61114V6.19555C0.0393004 6.28855 0.11786 6.40338 0.265907 6.53799C0.514315 6.76386 0.908625 6.99598 1.44885 7.20381C2.5198 7.6158 4.03921 7.88462 5.74912 7.88727C5.79336 7.7282 5.85815 7.57206 5.94448 7.42243C6.45854 6.53139 7.55467 6.18557 8.47567 6.58814C8.58719 5.58918 9.4345 4.8125 10.4632 4.8125C10.8636 4.8125 11.2365 4.93015 11.5493 5.13277V3.61105ZM5.77461 9.13729L5.8091 9.13725C5.94223 9.48546 6.17295 9.7985 6.48915 10.0313C5.7958 10.5416 5.51346 11.438 5.7698 12.2447C2.58265 12.2435 0 11.1352 0 9.76972V7.8876C0.29836 8.06982 0.636259 8.23051 1.00004 8.37046C2.25268 8.85234 3.94091 9.13729 5.77461 9.13729ZM11.2133 6.8125C11.2133 6.39829 10.8775 6.0625 10.4633 6.0625C10.049 6.0625 9.71326 6.39829 9.71326 6.8125V7.55231C9.32731 7.66895 8.97941 7.87326 8.69272 8.14207L8.05167 7.77224C7.69289 7.56524 7.23424 7.6883 7.02724 8.04708C6.82025 8.40587 6.94331 8.86452 7.30209 9.07151L7.94182 9.44059C7.89756 9.63031 7.87415 9.82805 7.87415 10.0313C7.87415 10.2345 7.89756 10.4322 7.94182 10.6219L7.30209 10.991C6.94331 11.198 6.82025 11.6566 7.02724 12.0154C7.23424 12.3742 7.69289 12.4973 8.05167 12.2903L8.69272 11.9204C8.9794 12.1893 9.3273 12.3936 9.71326 12.5102V13.25C9.71326 13.6642 10.049 14 10.4633 14C10.8775 14 11.2133 13.6642 11.2133 13.25V12.5103C11.5993 12.3937 11.9473 12.1894 12.2341 11.9205L12.8751 12.2903C13.2339 12.4973 13.6925 12.3742 13.8995 12.0154C14.1065 11.6566 13.9834 11.198 13.6247 10.991L12.985 10.622C13.0293 10.4322 13.0527 10.2345 13.0527 10.0313C13.0527 9.82803 13.0293 9.63027 12.985 9.44053L13.6247 9.07151C13.9834 8.86452 14.1065 8.40587 13.8995 8.04708C13.6925 7.6883 13.2339 7.56524 12.8751 7.77224L12.2341 8.14203C11.9473 7.87315 11.5993 7.66882 11.2133 7.5522V6.8125ZM9.52206 9.4864C9.51518 9.49832 9.50803 9.50998 9.5006 9.52138C9.41988 9.6735 9.37415 9.84704 9.37415 10.0313C9.37415 10.2155 9.41987 10.389 9.50058 10.5411C9.50801 10.5525 9.51517 10.5642 9.52206 10.5761C9.52859 10.5874 9.5348 10.5989 9.54068 10.6104C9.73284 10.9159 10.0726 11.1193 10.4599 11.1205H10.4633H10.4668C10.8538 11.1194 11.1932 10.9164 11.3855 10.6114C11.3916 10.5996 11.398 10.5878 11.4047 10.5761C11.4118 10.5638 11.4192 10.5518 11.4268 10.5401C11.5072 10.3882 11.5527 10.215 11.5527 10.0313C11.5527 9.84745 11.5072 9.67428 11.4268 9.5224C11.4191 9.51068 11.4118 9.49868 11.4047 9.4864C11.398 9.47474 11.3916 9.46298 11.3856 9.45113C11.1927 9.14521 10.8518 8.94197 10.4634 8.94197C10.0746 8.94197 9.73341 9.14567 9.54066 9.45217C9.53478 9.46367 9.52858 9.47509 9.52206 9.4864Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187799\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"database-subtract-2-raid-storage-code-disk-programming-database-array-hard-disc-minus\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187786)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.27948 1.96175C0.27948 1.85052 0.334086 1.67897 0.573736 1.45541C0.814343 1.23095 1.19651 1.00005 1.72102 0.793087C2.76633 0.380637 4.25309 0.111816 5.9269 0.111816C7.60071 0.111816 9.08747 0.380637 10.1328 0.793087C10.6573 1.00005 11.0395 1.23095 11.2801 1.45541C11.5197 1.67897 11.5743 1.85052 11.5743 1.96175C11.5743 2.07298 11.5197 2.24453 11.2801 2.46809C11.0395 2.69255 10.6573 2.92346 10.1328 3.13041C9.08747 3.54286 7.60071 3.81168 5.9269 3.81168C4.25309 3.81168 2.76633 3.54286 1.72102 3.13041C1.19651 2.92346 0.814343 2.69255 0.573736 2.46809C0.334086 2.24453 0.27948 2.07298 0.27948 1.96175ZM11.7016 3.72286C11.3789 3.94153 11.002 4.13124 10.5916 4.29317C9.36671 4.77647 7.71726 5.06168 5.9269 5.06168C4.13655 5.06168 2.48709 4.77647 1.26223 4.29317C0.851884 4.13126 0.475037 3.94158 0.152344 3.72295V6.30737C0.191644 6.40036 0.270204 6.51519 0.418251 6.64981C0.666659 6.87567 1.06096 7.1078 1.60119 7.31562C2.67746 7.72966 4.20665 7.9991 5.92695 7.9991C6.19764 7.9991 6.46359 7.99243 6.7238 7.97958C7.56176 6.74078 8.97973 5.92645 10.5879 5.92645C10.9718 5.92645 11.3447 5.97284 11.7016 6.06031V3.72286ZM5.92695 9.2491C5.99204 9.2491 6.05695 9.24874 6.12168 9.24803C5.99448 9.67249 5.92614 10.1224 5.92614 10.5882C5.92614 11.2125 6.04884 11.808 6.27142 12.3522C6.15746 12.355 6.04261 12.3565 5.92697 12.3565C2.7376 12.3565 0.152344 11.2477 0.152344 9.88155V7.99942C0.450704 8.18164 0.788603 8.34233 1.15238 8.48227C2.40502 8.96416 4.09324 9.2491 5.92695 9.2491ZM10.5879 14C12.4722 14 13.9997 12.4725 13.9997 10.5882C13.9997 8.70396 12.4722 7.17645 10.5879 7.17645C8.70365 7.17645 7.17614 8.70396 7.17614 10.5882C7.17614 12.4725 8.70365 14 10.5879 14ZM8.47198 10.5882C8.47198 10.243 8.75181 9.96323 9.09698 9.96323H12.0788C12.424 9.96323 12.7038 10.243 12.7038 10.5882C12.7038 10.9334 12.424 11.2132 12.0788 11.2132H9.09698C8.75181 11.2132 8.47198 10.9334 8.47198 10.5882Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187786\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"delete-keyboard\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.31614 10.4626C3.69614 10.9087 4.25269 11.1658 4.83874 11.1658H11.0211C12.1256 11.1658 13.0211 10.2703 13.0211 9.16573V4.83423C13.0211 3.72966 12.1256 2.83423 11.0211 2.83423H4.83874C4.25269 2.83423 3.69614 3.09127 3.31614 3.53742L1.47155 5.70317C0.835004 6.45054 0.835004 7.54942 1.47155 8.29679L3.31614 10.4626ZM5.48076 4.74526C5.72484 4.50118 6.12057 4.50118 6.36464 4.74526L7.71439 6.095L9.06414 4.74526C9.30821 4.50118 9.70394 4.50118 9.94802 4.74526C10.1921 4.98934 10.1921 5.38506 9.94802 5.62914L8.59827 6.97889L9.94804 8.32865C10.1921 8.57273 10.1921 8.96846 9.94804 9.21254C9.70396 9.45661 9.30823 9.45661 9.06415 9.21254L7.71439 7.86277L6.36462 9.21254C6.12055 9.45661 5.72482 9.45661 5.48074 9.21254C5.23666 8.96846 5.23666 8.57273 5.48074 8.32865L6.83051 6.97889L5.48076 5.62914C5.23668 5.38506 5.23668 4.98934 5.48076 4.74526Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"desktop-chat\", \"keywords\": [ \"bubble\", \"chat\", \"customer\", \"service\", \"conversation\", \"display\", \"device\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187817)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.42859 4.64752C5.42859 5.19981 5.8763 5.64752 6.42859 5.64752H8.64287V6.85612C8.64287 7.25547 9.08795 7.49367 9.42023 7.27215L11.8572 5.64752H13C13.5523 5.64752 14 5.19981 14 4.64752V1C14 0.447716 13.5523 0 13 0H6.42859C5.8763 0 5.42859 0.447715 5.42859 1V4.64752ZM8.51536 11H12.5C13.3284 11 14 10.3284 14 9.5V8C14 7.44772 13.5523 7 13 7C12.4477 7 12 7.44772 12 8V9H2V3H3C3.55228 3 4 2.55228 4 2C4 1.44772 3.55228 1 3 1H1.5C0.671573 1 0 1.67157 0 2.5V9.5C0 10.3284 0.671573 11 1.5 11H5.48479L4.94794 12.5H4C3.58579 12.5 3.25 12.8358 3.25 13.25C3.25 13.6642 3.58579 14 4 14H10C10.4142 14 10.75 13.6642 10.75 13.25C10.75 12.8358 10.4142 12.5 10 12.5H9.05221L8.51536 11Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187817\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"desktop-check\", \"keywords\": [ \"success\", \"approve\", \"device\", \"display\", \"desktop\", \"computer\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187765)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.4983 0.435789C13.8079 0.710978 13.8358 1.18504 13.5606 1.49462L9.56062 5.99462C9.30147 6.28616 8.86211 6.33039 8.55006 6.09635L6.55006 4.59635C6.21869 4.34782 6.15153 3.87772 6.40006 3.54635C6.64859 3.21498 7.11869 3.14782 7.45006 3.39635L8.89798 4.48229L12.4395 0.498073C12.7147 0.188486 13.1887 0.160601 13.4983 0.435789ZM8.51536 10.9999H12.5C13.3284 10.9999 14 10.3283 14 9.49995V7.49995C14 6.94767 13.5523 6.49995 13 6.49995C12.4477 6.49995 12 6.94767 12 7.49995V8.99995H2V2.99995H4.5C5.05228 2.99995 5.5 2.55224 5.5 1.99995C5.5 1.44767 5.05228 0.999947 4.5 0.999947H1.5C0.671573 0.999947 0 1.67152 0 2.49995V9.49995C0 10.3283 0.671573 10.9999 1.5 10.9999H5.48479L4.94794 12.4999H4C3.58579 12.4999 3.25 12.8357 3.25 13.2499C3.25 13.6641 3.58579 13.9999 4 13.9999H10C10.4142 13.9999 10.75 13.6641 10.75 13.2499C10.75 12.8357 10.4142 12.4999 10 12.4999H9.05221L8.51536 10.9999Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187765\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"desktop-code\", \"keywords\": [ \"desktop\", \"device\", \"display\", \"computer\", \"code\", \"terminal\", \"html\", \"css\", \"programming\", \"system\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187793)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1.83179C0 1.02723 0.652229 0.375 1.45679 0.375H12.5432C13.3478 0.375 14 1.02723 14 1.83179V9.41821C14 10.2228 13.3478 10.875 12.5432 10.875H8.51694L9.05214 12.3704H9.99998C10.4142 12.3704 10.75 12.7062 10.75 13.1204C10.75 13.5346 10.4142 13.8704 9.99998 13.8704H3.99998C3.58577 13.8704 3.24998 13.5346 3.24998 13.1204C3.24998 12.7062 3.58577 12.3704 3.99998 12.3704H4.94786L5.48306 10.875H1.45679C0.65223 10.875 0 10.2228 0 9.41821V1.83179ZM5.53033 3.59235C5.82322 3.88524 5.82322 4.36012 5.53033 4.65301L4.56066 5.62268L5.53033 6.59235C5.82322 6.88524 5.82322 7.36012 5.53033 7.65301C5.23744 7.9459 4.76256 7.9459 4.46967 7.65301L2.96967 6.15301C2.67678 5.86012 2.67678 5.38524 2.96967 5.09235L4.46967 3.59235C4.76256 3.29946 5.23744 3.29946 5.53033 3.59235ZM9.53033 3.59235C9.23744 3.29946 8.76256 3.29946 8.46967 3.59235C8.17678 3.88524 8.17678 4.36012 8.46967 4.65301L9.43934 5.62268L8.46967 6.59235C8.17678 6.88524 8.17678 7.36012 8.46967 7.65301C8.76256 7.9459 9.23744 7.9459 9.53033 7.65301L11.0303 6.15301C11.3232 5.86012 11.3232 5.38524 11.0303 5.09235L9.53033 3.59235Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187793\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"desktop-delete\", \"keywords\": [ \"device\", \"remove\", \"display\", \"computer\", \"deny\", \"desktop\", \"fail\", \"failure\", \"cross\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187796)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.7607 1.33819C14.0536 1.0453 14.0536 0.570424 13.7607 0.277531C13.4678 -0.0153619 12.9929 -0.0153619 12.7 0.277531L10.9803 1.9972L9.26068 0.277531C8.96778 -0.0153619 8.49291 -0.0153619 8.20002 0.277531C7.90712 0.570424 7.90712 1.0453 8.20002 1.33819L9.91969 3.05786L8.20002 4.77753C7.90712 5.07042 7.90712 5.5453 8.20002 5.83819C8.49291 6.13108 8.96778 6.13108 9.26068 5.83819L10.9803 4.11852L12.7 5.83819C12.9929 6.13108 13.4678 6.13108 13.7607 5.83819C14.0536 5.5453 14.0536 5.07042 13.7607 4.77753L12.041 3.05786L13.7607 1.33819ZM8.51536 11H12.5C13.3284 11 14 10.3284 14 9.5V8.5C14 7.94772 13.5523 7.5 13 7.5C12.4477 7.5 12 7.94772 12 8.5V9H2V3H5.5C6.05228 3 6.5 2.55228 6.5 2C6.5 1.44772 6.05228 1 5.5 1H1.5C0.671573 1 0 1.67157 0 2.5V9.5C0 10.3284 0.671573 11 1.5 11H5.48479L4.94794 12.5H4C3.58579 12.5 3.25 12.8358 3.25 13.25C3.25 13.6642 3.58579 14 4 14H10C10.4142 14 10.75 13.6642 10.75 13.25C10.75 12.8358 10.4142 12.5 10 12.5H9.05221L8.51536 11Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187796\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"desktop-dollar\", \"keywords\": [ \"cash\", \"desktop\", \"display\", \"device\", \"notification\", \"computer\", \"money\", \"currency\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187789)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 1C0.671572 1 0 1.67157 0 2.5V9.5C0 10.3284 0.671573 11 1.5 11H5.48479L4.94794 12.5H4C3.58579 12.5 3.25 12.8358 3.25 13.25C3.25 13.6642 3.58579 14 4 14H10C10.4142 14 10.75 13.6642 10.75 13.25C10.75 12.8358 10.4142 12.5 10 12.5H9.05221L8.51536 11H9C9.55229 11 10 10.5523 10 10C10 9.44772 9.55229 9 9 9H2V3H7C7.55228 3 8 2.55228 8 2C8 1.44772 7.55228 1 7 1H1.5Z\\\" fill=\\\"black\\\"/>\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.5826 0C11.9968 0 12.3326 0.335786 12.3326 0.75V1.27772C12.7899 1.35458 13.1947 1.5867 13.4899 1.91648C13.6616 2.10833 13.7972 2.33419 13.8853 2.58339C14.0233 2.97393 13.8186 3.40242 13.4281 3.54046C13.0376 3.67849 12.6091 3.4738 12.471 3.08326C12.4494 3.02195 12.4157 2.96545 12.3722 2.91685C12.2795 2.81336 12.1473 2.75 11.9993 2.75H11.0318C10.8298 2.75 10.666 2.91377 10.666 3.11578C10.666 3.28768 10.7857 3.43638 10.9536 3.47312L12.4267 3.79536C13.3451 3.99626 13.9993 4.80991 13.9993 5.74952C13.9993 6.74023 13.2788 7.56353 12.3326 7.72231V8.24999C12.3326 8.66421 11.9968 8.99999 11.5826 8.99999C11.1684 8.99999 10.8326 8.66421 10.8326 8.24999V7.72232C10.1091 7.60096 9.51849 7.09128 9.28001 6.41657C9.14198 6.02604 9.34667 5.59755 9.73721 5.45951C10.1277 5.32148 10.5562 5.52617 10.6942 5.91671C10.7632 6.11191 10.9495 6.24997 11.166 6.24997H11.9993C12.2751 6.24997 12.4993 6.02601 12.4993 5.74952C12.4993 5.51429 12.3354 5.31086 12.1062 5.26071L10.6331 4.93847C9.77654 4.7511 9.16602 3.99259 9.16602 3.11578C9.16602 2.15262 9.89584 1.35991 10.8326 1.2605V0.75C10.8326 0.335786 11.1684 0 11.5826 0Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187789\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"desktop-emoji\", \"keywords\": [ \"device\", \"display\", \"desktop\", \"padlock\", \"smiley\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187783)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.45679 0.252319C0.652229 0.252319 0 0.904549 0 1.70911V9.29553C0 10.1001 0.65223 10.7523 1.45679 10.7523H5.48306L4.94786 12.2477H3.99998C3.58577 12.2477 3.24998 12.5835 3.24998 12.9977C3.24998 13.4119 3.58577 13.7477 3.99998 13.7477H9.99998C10.4142 13.7477 10.75 13.4119 10.75 12.9977C10.75 12.5835 10.4142 12.2477 9.99998 12.2477H9.05214L8.51694 10.7523H12.5432C13.3478 10.7523 14 10.1001 14 9.29553V1.70911C14 0.904549 13.3478 0.252319 12.5432 0.252319H1.45679ZM5.00248 6.38556C4.76048 6.13943 4.36477 6.13608 4.11863 6.37808C3.8725 6.62009 3.86915 7.0158 4.11115 7.26193C4.70567 7.8666 5.70784 8.37732 7 8.37732C8.29216 8.37732 9.29433 7.8666 9.88885 7.26193C10.1309 7.0158 10.1275 6.62009 9.88137 6.37808C9.63523 6.13608 9.23952 6.13943 8.99752 6.38556C8.63935 6.74984 7.9589 7.12732 7 7.12732C6.0411 7.12732 5.36065 6.74984 5.00248 6.38556ZM6.01587 4.00256C6.01587 4.42541 5.67309 4.76819 5.25024 4.76819C4.8274 4.76819 4.48462 4.42541 4.48462 4.00256C4.48462 3.57972 4.8274 3.23694 5.25024 3.23694C5.67309 3.23694 6.01587 3.57972 6.01587 4.00256ZM8.75 4.76819C9.17284 4.76819 9.51562 4.42541 9.51562 4.00256C9.51562 3.57972 9.17284 3.23694 8.75 3.23694C8.32716 3.23694 7.98438 3.57972 7.98438 4.00256C7.98438 4.42541 8.32716 4.76819 8.75 4.76819Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187783\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"desktop-favorite-star\", \"keywords\": [ \"desktop\", \"device\", \"display\", \"like\", \"favorite\", \"star\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187808)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.8183 0.290353L11.6645 2.00338C11.6791 2.04794 11.704 2.08843 11.7372 2.1216C11.7704 2.15476 11.8109 2.17966 11.8554 2.19429L13.749 2.46776C13.8033 2.47549 13.8543 2.49836 13.8962 2.53376C13.9381 2.56915 13.9692 2.61563 13.9859 2.66787C14.0026 2.7201 14.0042 2.77599 13.9906 2.82912C13.977 2.88224 13.9487 2.93046 13.909 2.96825L12.5571 4.3201C12.525 4.3542 12.5009 4.39508 12.4866 4.43973C12.4723 4.48437 12.4682 4.53165 12.4746 4.57809L12.8048 6.41495C12.8144 6.46872 12.8087 6.52409 12.7884 6.57478C12.768 6.62547 12.7339 6.66944 12.6898 6.70169C12.6457 6.73394 12.5935 6.75318 12.539 6.75722C12.4846 6.76126 12.4301 6.74993 12.3817 6.72453L10.6893 5.83706C10.6482 5.81436 10.6021 5.80245 10.5552 5.80245C10.5082 5.80245 10.4621 5.81436 10.421 5.83706L8.72862 6.72453C8.68027 6.74993 8.62577 6.76126 8.5713 6.75722C8.51684 6.75318 8.4646 6.73395 8.42052 6.70169C8.37644 6.66944 8.3423 6.62547 8.32197 6.57478C8.30164 6.52409 8.29594 6.46872 8.30552 6.41495L8.63059 4.53165C8.63697 4.48521 8.63287 4.43793 8.61858 4.39329C8.60429 4.34864 8.58019 4.30777 8.54803 4.27366L7.19618 2.92181C7.16212 2.88487 7.13817 2.83975 7.12665 2.79083C7.11514 2.74191 7.11645 2.69085 7.13047 2.64258C7.14448 2.59432 7.17071 2.55049 7.20663 2.51535C7.24255 2.4802 7.28694 2.45492 7.33549 2.44196L9.22911 2.16849C9.27573 2.16009 9.31968 2.14074 9.35735 2.11204C9.39503 2.08333 9.42535 2.04609 9.44582 2.00338L10.292 0.290353C10.3158 0.240758 10.3531 0.198899 10.3997 0.1696C10.4463 0.140302 10.5002 0.124756 10.5552 0.124756C10.6102 0.124756 10.6641 0.140302 10.7106 0.1696C10.7572 0.198899 10.7945 0.240758 10.8183 0.290353ZM8.51536 11H12.5C13.3284 11 14 10.3284 14 9.50003V9.00003C14 8.44775 13.5523 8.00003 13 8.00003C12.4477 8.00003 12 8.44775 12 9.00003H2V3.00003H5C5.55228 3.00003 6 2.55232 6 2.00003C6 1.44775 5.55228 1.00003 5 1.00003H1.5C0.671573 1.00003 0 1.6716 0 2.50003V9.50003C0 10.3284 0.671573 11 1.5 11H5.48479L4.94794 12.5H4C3.58579 12.5 3.25 12.8358 3.25 13.25C3.25 13.6642 3.58579 14 4 14H10C10.4142 14 10.75 13.6642 10.75 13.25C10.75 12.8358 10.4142 12.5 10 12.5H9.05221L8.51536 11Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187808\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"desktop-game\", \"keywords\": [ \"controller\", \"display\", \"device\", \"computer\", \"games\", \"leisure\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187814)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1.95679C0 1.15223 0.652229 0.5 1.45679 0.5H12.5432C13.3478 0.5 14 1.15223 14 1.95679V9.54321C14 10.3478 13.3478 11 12.5432 11H8.51694L9.05214 12.4954H9.99998C10.4142 12.4954 10.75 12.8312 10.75 13.2454C10.75 13.6596 10.4142 13.9954 9.99998 13.9954H3.99998C3.58577 13.9954 3.24998 13.6596 3.24998 13.2454C3.24998 12.8312 3.58577 12.4954 3.99998 12.4954H4.94786L5.48306 11H1.45679C0.65223 11 0 10.3478 0 9.54321V1.95679ZM2.06403 5.53033C2.06403 5.18516 2.34385 4.90533 2.68903 4.90533H3.56403V4.03033C3.56403 3.68516 3.84385 3.40533 4.18903 3.40533C4.5342 3.40533 4.81403 3.68516 4.81403 4.03033V4.90533H5.68903C6.0342 4.90533 6.31403 5.18516 6.31403 5.53033C6.31403 5.87551 6.0342 6.15533 5.68903 6.15533H4.81403V7.03033C4.81403 7.37551 4.5342 7.65533 4.18903 7.65533C3.84385 7.65533 3.56403 7.37551 3.56403 7.03033V6.15533H2.68903C2.34385 6.15533 2.06403 5.87551 2.06403 5.53033ZM10.1058 4.5553C10.1058 5.03855 10.4976 5.4303 10.9808 5.4303C11.4641 5.4303 11.8558 5.03855 11.8558 4.5553C11.8558 4.07205 11.4641 3.6803 10.9808 3.6803C10.4976 3.6803 10.1058 4.07205 10.1058 4.5553ZM8.16455 6.50537C8.16455 6.98862 8.5563 7.38037 9.03955 7.38037C9.5228 7.38037 9.91455 6.98862 9.91455 6.50537C9.91455 6.02212 9.5228 5.63037 9.03955 5.63037C8.5563 5.63037 8.16455 6.02212 8.16455 6.50537Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187814\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"desktop-help\", \"keywords\": [ \"device\", \"help\", \"information\", \"display\", \"desktop\", \"question\", \"info\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187805)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.7167 1.57608C11.5512 1.54317 11.3797 1.56006 11.2238 1.62462C11.068 1.68918 10.9347 1.79851 10.841 1.93878C10.7473 2.07906 10.6973 2.24397 10.6973 2.41267C10.6973 2.82689 10.3615 3.16267 9.94727 3.16267C9.53305 3.16267 9.19727 2.82689 9.19727 2.41267C9.19727 1.9473 9.33527 1.49237 9.59381 1.10543C9.85236 0.718483 10.2198 0.416895 10.6498 0.238804C11.0798 0.060712 11.5529 0.0141151 12.0093 0.104905C12.4657 0.195696 12.885 0.419796 13.2141 0.748866C13.5431 1.07794 13.7672 1.4972 13.858 1.95363C13.9488 2.41006 13.9022 2.88317 13.7241 3.31312C13.546 3.74307 13.2444 4.11056 12.8575 4.36911C12.6837 4.48522 12.4962 4.57702 12.3002 4.64293V4.81715C12.3002 5.23136 11.9645 5.56715 11.5502 5.56715C11.136 5.56715 10.8002 5.23136 10.8002 4.81715V4.01566C10.8002 3.60144 11.136 3.26566 11.5502 3.26566C11.719 3.26566 11.8839 3.21563 12.0241 3.1219C12.1644 3.02818 12.2737 2.89496 12.3383 2.7391C12.4029 2.58323 12.4198 2.41173 12.3868 2.24627C12.3539 2.0808 12.2727 1.92882 12.1534 1.80953C12.0341 1.69023 11.8821 1.609 11.7167 1.57608ZM11.5435 7.84664C11.0908 7.84304 10.7249 7.4749 10.7249 7.0213C10.7249 6.56546 11.0944 6.19592 11.5502 6.19592L11.5569 6.19596C12.0097 6.19956 12.3756 6.5677 12.3756 7.02131C12.3756 7.47715 12.0061 7.84668 11.5502 7.84668L11.5435 7.84664ZM8.51536 11H13C13.5523 11 14 10.5523 14 10C14 9.44772 13.5523 9 13 9H2V3H7C7.55228 3 8 2.55229 8 2C8 1.44772 7.55228 1 7 1H1.5C0.671572 1 0 1.67157 0 2.5V9.5C0 10.3284 0.671573 11 1.5 11H5.48479L4.94794 12.5H4C3.58579 12.5 3.25 12.8358 3.25 13.25C3.25 13.6642 3.58579 14 4 14H10C10.4142 14 10.75 13.6642 10.75 13.25C10.75 12.8358 10.4142 12.5 10 12.5H9.05221L8.51536 11Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187805\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"device-database-encryption-1\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187823)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.5586 1.95388C2.5586 0.895159 3.44546 0.092041 4.46994 0.092041C5.49441 0.092041 6.38128 0.895158 6.38128 1.95388V2.41606C6.38128 3.47478 5.49441 4.2779 4.46994 4.2779C3.44546 4.2779 2.5586 3.47478 2.5586 2.41606V1.95388ZM4.46994 1.59204C4.21163 1.59204 4.0586 1.7845 4.0586 1.95388V2.41606C4.0586 2.58544 4.21163 2.7779 4.46994 2.7779C4.72824 2.7779 4.88128 2.58544 4.88128 2.41606V1.95388C4.88128 1.7845 4.72824 1.59204 4.46994 1.59204ZM1.59375 0.842054C1.59375 0.42784 1.25797 0.0920541 0.84375 0.0920541C0.429537 0.0920541 0.09375 0.427841 0.09375 0.842054V3.54484C0.09375 3.95905 0.429537 4.29484 0.843751 4.29484C1.25797 4.29484 1.59375 3.95905 1.59375 3.54484V0.842054ZM1.59375 5.63447C1.59375 5.22026 1.25797 4.88447 0.84375 4.88447C0.429537 4.88447 0.09375 5.22026 0.09375 5.63447V8.33725C0.09375 8.75147 0.429537 9.08725 0.843751 9.08725C1.25797 9.08725 1.59375 8.75147 1.59375 8.33725V5.63447ZM3.3086 4.88447C3.72281 4.88447 4.0586 5.22026 4.0586 5.63447V8.33725C4.0586 8.75147 3.72281 9.08725 3.3086 9.08725C2.89438 9.08725 2.5586 8.75147 2.5586 8.33725V5.63447C2.5586 5.22026 2.89438 4.88447 3.3086 4.88447ZM1.59375 10.3873C1.59375 9.97307 1.25797 9.63728 0.84375 9.63728C0.429537 9.63728 0.09375 9.97307 0.09375 10.3873V13.0901C0.09375 13.5043 0.429537 13.8401 0.843751 13.8401C1.25797 13.8401 1.59375 13.5043 1.59375 13.0901V10.3873ZM8.09607 0.0920541C8.51028 0.0920541 8.84607 0.42784 8.84607 0.842054V3.54483C8.84607 3.95904 8.51029 4.29483 8.09607 4.29483C7.68186 4.29483 7.34607 3.95904 7.34607 3.54483V0.842054C7.34607 0.427841 7.68186 0.0920541 8.09607 0.0920541ZM11.2722 0.842054C11.2722 0.42784 10.9365 0.0920541 10.5222 0.0920541C10.108 0.0920541 9.77222 0.427841 9.77222 0.842054V3.54483C9.77222 3.95904 10.108 4.29483 10.5222 4.29483C10.9365 4.29483 11.2722 3.95904 11.2722 3.54483V0.842054ZM4.46994 9.65417C3.44546 9.65417 2.5586 10.4573 2.5586 11.516V11.9782C2.5586 13.0369 3.44546 13.84 4.46994 13.84C5.49441 13.84 6.38128 13.0369 6.38128 11.9782V11.516C6.38128 10.4573 5.49441 9.65417 4.46994 9.65417ZM4.0586 11.516C4.0586 11.3466 4.21163 11.1542 4.46994 11.1542C4.72824 11.1542 4.88128 11.3466 4.88128 11.516V11.9782C4.88128 12.1476 4.72824 12.34 4.46994 12.34C4.21163 12.34 4.0586 12.1476 4.0586 11.9782V11.516ZM5.02338 6.74448C5.02338 5.68888 5.87912 4.83314 6.93472 4.83314C7.34893 4.83314 7.68472 5.16893 7.68472 5.58314C7.68472 5.99736 7.34893 6.33314 6.93472 6.33314C6.70754 6.33314 6.52338 6.51731 6.52338 6.74448V7.22724C6.52338 7.64146 6.18759 7.97724 5.77338 7.97724C5.35917 7.97724 5.02338 7.64146 5.02338 7.22724V6.74448ZM10.4993 7.75C9.80891 7.75 9.24927 8.30964 9.24927 9V9.5H11.7493V9C11.7493 8.30964 11.1897 7.75 10.4993 7.75ZM7.74927 9V9.5315C7.31796 9.64252 6.99927 10.034 6.99927 10.5V13C6.99927 13.5523 7.44699 14 7.99927 14H12.9993C13.5516 14 13.9993 13.5523 13.9993 13V10.5C13.9993 10.034 13.6806 9.64252 13.2493 9.5315V9C13.2493 7.48122 12.0181 6.25 10.4993 6.25C8.98049 6.25 7.74927 7.48122 7.74927 9Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187823\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"discord\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.22521 2.76846C1.80034 3.46476 1.35703 4.6272 1.01228 5.97174C0.661894 7.33824 0.42661 8.84067 0.408203 10.1426C0.746425 10.5592 1.66715 11.3607 3.18851 11.7367L4.27636 10.3769C3.75066 10.1874 3.30828 9.94355 3.03054 9.66581C2.83528 9.47055 2.83528 9.15396 3.03054 8.9587C3.2258 8.76344 3.54238 8.76344 3.73765 8.9587C3.94183 9.16288 4.46581 9.43719 5.23954 9.61985L5.24608 9.62134C5.72845 9.73469 6.28038 9.80566 6.8612 9.80566C7.44202 9.80566 7.99395 9.73469 8.47632 9.62134L8.48188 9.62006C9.25611 9.43741 9.78043 9.16297 9.9847 8.9587C10.18 8.76344 10.4966 8.76344 10.6918 8.9587C10.8871 9.15396 10.8871 9.47055 10.6918 9.66581C10.4141 9.94355 9.97171 10.1874 9.44603 10.3769L10.5339 11.7367C12.0553 11.3607 12.976 10.5592 13.3142 10.1426C13.2958 8.84067 13.0605 7.33823 12.7101 5.97174C12.3654 4.6272 11.9221 3.46476 11.4972 2.76846L11.4675 2.75935C11.3354 2.71973 11.1079 2.66207 10.7528 2.60289C10.0418 2.48439 8.82861 2.36133 6.8612 2.36133C4.89379 2.36133 3.68059 2.48439 2.96959 2.60289C2.61452 2.66207 2.38702 2.71973 2.25494 2.75935L2.22521 2.76846ZM1.89394 2.35805C1.67034 1.91084 1.67069 1.91066 1.67105 1.91049L1.67331 1.90937L1.67663 1.90775L1.68435 1.90406L1.70425 1.89502C1.71956 1.88826 1.73905 1.88009 1.76317 1.87071C1.81138 1.85196 1.8783 1.82831 1.96759 1.80153C2.14595 1.74802 2.41518 1.6815 2.8052 1.61649C3.58438 1.48663 4.85481 1.36133 6.8612 1.36133C8.86759 1.36133 10.1381 1.48663 10.9172 1.61649C11.3072 1.6815 11.5765 1.74802 11.7548 1.80153C11.8441 1.82831 11.911 1.85196 11.9593 1.87071C11.9834 1.88009 12.0029 1.88826 12.0182 1.89502L12.0381 1.90406L12.0458 1.90775L12.0491 1.90937L12.0507 1.91012C12.051 1.91029 12.0521 1.91084 11.8285 2.35805M4.40594 7.0769C4.95823 7.0769 5.40594 6.62919 5.40594 6.0769C5.40594 5.52462 4.95823 5.0769 4.40594 5.0769C3.85366 5.0769 3.40594 5.52462 3.40594 6.0769C3.40594 6.62919 3.85366 7.0769 4.40594 7.0769ZM10.3165 6.0769C10.3165 6.62919 9.86875 7.0769 9.31646 7.0769C8.76418 7.0769 8.31646 6.62919 8.31646 6.0769C8.31646 5.52462 8.76418 5.0769 9.31646 5.0769C9.86875 5.0769 10.3165 5.52462 10.3165 6.0769Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"drone\", \"keywords\": [ \"artificial\", \"robotics\", \"intelligence\", \"machine\", \"technology\", \"android\", \"flying\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187838)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.0344238 0.789307C0.0344238 0.375093 0.37021 0.0393066 0.784424 0.0393066H2.6969H4.60941C5.02362 0.0393066 5.35941 0.375093 5.35941 0.789307C5.35941 1.20352 5.02362 1.53931 4.60941 1.53931H3.4469V3.5H10.5531V1.53931H9.39062C8.97641 1.53931 8.64062 1.20352 8.64062 0.789307C8.64062 0.375093 8.97641 0.0393066 9.39062 0.0393066H11.3031H13.2156C13.6298 0.0393066 13.9656 0.375093 13.9656 0.789307C13.9656 1.20352 13.6298 1.53931 13.2156 1.53931H12.0531V3.5H13.5C13.7761 3.5 14 3.72386 14 4V5C14 5.66304 13.7366 6.29893 13.2678 6.76777C12.813 7.22249 12.2012 7.48395 11.5598 7.49929C11.5401 7.49742 11.5202 7.49646 11.5 7.49646H2.5C2.47982 7.49646 2.45987 7.49742 2.44018 7.49929C1.79882 7.48395 1.18696 7.22249 0.732233 6.76777C0.263392 6.29893 0 5.66304 0 5V4C0 3.72386 0.223858 3.5 0.5 3.5H1.9469V1.53931H0.784424C0.37021 1.53931 0.0344238 1.20352 0.0344238 0.789307ZM3.23132 8.74646H10.8532C11.403 9.33072 11.6471 10.1266 11.7535 10.8095C11.8892 11.6793 11.8334 12.5616 11.7398 13.1233C11.6795 13.4849 11.3666 13.75 11 13.75H10C9.58579 13.75 9.25 13.4142 9.25 13C9.25 12.5858 9.58579 12.25 10 12.25H10.3256C10.3448 11.8803 10.3358 11.4535 10.2715 11.0405C10.2259 10.7486 10.1579 10.4944 10.0702 10.2808C10.0396 10.3172 10.0079 10.3521 9.97487 10.3851C9.3185 11.0415 8.42826 11.4102 7.5 11.4102H6.5C5.57174 11.4102 4.6815 11.0415 4.02513 10.3851C4.01267 10.3726 4.0004 10.3599 3.98831 10.347C3.91308 10.5463 3.85399 10.7782 3.81308 11.0405C3.74869 11.4535 3.73971 11.8803 3.75894 12.25H4.08453C4.49875 12.25 4.83453 12.5858 4.83453 13C4.83453 13.4142 4.49875 13.75 4.08453 13.75H3.08453C2.7179 13.75 2.40501 13.4849 2.34474 13.1233C2.25112 12.5616 2.19537 11.6793 2.33099 10.8095C2.43746 10.1266 2.68156 9.33072 3.23132 8.74646Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187838\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"dropbox\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187829)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.3125 3.13164L3.65582 0.840454L6.99913 3.13164L3.65582 5.42283L0.3125 3.13164ZM10.3442 0.840454L7.00085 3.13164L10.3442 5.42283L13.6875 3.13164L10.3442 0.840454ZM10.3437 10.0088L7 7.71795L10.3433 5.42676L13.6875 7.71795L10.3437 10.0088ZM3.65863 11.2911L7.00195 8.99994L10.3453 11.2911L7.00195 13.5823L3.65863 11.2911ZM3.65582 5.42676L0.3125 7.71795L3.65582 10.0091L7 7.71795L3.65582 5.42676Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187829\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"eject\", \"keywords\": [ \"eject\", \"unmount\", \"dismount\", \"remove\", \"keyboard\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.90088 1.50325C6.49433 0.86415 7.50581 0.864151 8.09926 1.50325L13.0605 6.84614C13.9517 7.80591 13.2711 9.36681 11.9613 9.36681H2.03882C0.729071 9.36681 0.0484087 7.80591 0.939628 6.84614L5.90088 1.50325ZM0.5 11.9762C0.5 11.4239 0.947715 10.9762 1.5 10.9762H12.5C13.0523 10.9762 13.5 11.4239 13.5 11.9762C13.5 12.5285 13.0523 12.9762 12.5 12.9762H1.5C0.947715 12.9762 0.5 12.5285 0.5 11.9762Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"electric-cord-1\", \"keywords\": [ \"electricity\", \"electronic\", \"appliances\", \"device\", \"cord\", \"cable\", \"plug\", \"connection\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.25 0.8125C5.25 0.398286 4.91421 0.0625 4.5 0.0625C4.08579 0.0625 3.75 0.398286 3.75 0.8125V3.53125H3C2.44772 3.53125 2 3.97897 2 4.53125V7.53125C2 9.1881 3.34315 10.5312 5 10.5312H6.25V13.2188C6.25 13.633 6.58579 13.9688 7 13.9688C7.41421 13.9688 7.75 13.633 7.75 13.2188V10.5312H9C10.6569 10.5312 12 9.1881 12 7.53125V4.53125C12 3.97897 11.5523 3.53125 11 3.53125H10.25V0.8125C10.25 0.398286 9.91421 0.0625 9.5 0.0625C9.08579 0.0625 8.75 0.398286 8.75 0.8125V3.53125H5.25V0.8125Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"electric-cord-3\", \"keywords\": [ \"electricity\", \"electronic\", \"appliances\", \"device\", \"cord\", \"cable\", \"plug\", \"connection\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187820)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 7.03345C1.5 3.99588 3.96243 1.53345 7 1.53345C10.0376 1.53345 12.5 3.99588 12.5 7.03345C12.5 9.77197 10.4978 12.0443 7.87748 12.464C7.86021 12.4667 7.85075 12.4647 7.84464 12.4627C7.83712 12.4602 7.82501 12.4543 7.81063 12.4406C7.78 12.4115 7.75 12.3564 7.75 12.2835V10.8309C9.39368 10.4855 10.6278 9.02727 10.6278 7.28086V5.73138C10.6278 5.31717 10.292 4.98138 9.87777 4.98138H9.3125V3.77087C9.3125 3.35666 8.97671 3.02087 8.5625 3.02087C8.14829 3.02087 7.8125 3.35666 7.8125 3.77087V4.98138H6.1875V3.77087C6.1875 3.35666 5.85171 3.02087 5.4375 3.02087C5.02329 3.02087 4.6875 3.35666 4.6875 3.77087V4.98138H4.12256C3.70834 4.98138 3.37256 5.31717 3.37256 5.73138V7.28086C3.37256 9.02715 4.60648 10.4853 6.25 10.8308V12.2835C6.25 13.211 7.03231 14.1185 8.11469 13.9451C11.4511 13.4108 14 10.5205 14 7.03345C14 3.16745 10.866 0.0334473 7 0.0334473C3.13401 0.0334473 0 3.16745 0 7.03345C0 9.62538 1.40911 11.8873 3.49946 13.0965C3.858 13.304 4.3168 13.1814 4.5242 12.8229C4.73161 12.4643 4.60909 12.0055 4.25054 11.7981C2.60463 10.846 1.5 9.06828 1.5 7.03345Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187820\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"facebook-1\", \"keywords\": [ \"media\", \"facebook\", \"social\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187847)\\\">\\n<path d=\\\"M0 12.9231V1.07692C0 0.791305 0.113461 0.517386 0.315423 0.315423C0.517386 0.113461 0.791305 0 1.07692 0H12.9231C13.2087 0 13.4826 0.113461 13.6846 0.315423C13.8865 0.517386 14 0.791305 14 1.07692V12.9231C14 13.2087 13.8865 13.4826 13.6846 13.6846C13.4826 13.8865 13.2087 14 12.9231 14H9.69231V8.89539H10.4569C10.6312 8.89539 10.7982 8.82617 10.9214 8.70298C11.0446 8.57978 11.1138 8.41269 11.1138 8.23846V7.40923C11.1138 7.32296 11.0969 7.23754 11.0638 7.15784C11.0308 7.07814 10.9824 7.00572 10.9214 6.94472C10.8604 6.88372 10.788 6.83533 10.7083 6.80231C10.6286 6.7693 10.5432 6.75231 10.4569 6.75231H9.73539V5.74C9.73539 4.83538 10.1446 4.83538 10.5538 4.83538H11.0815C11.1682 4.83895 11.2546 4.82344 11.3346 4.78995C11.4146 4.75645 11.4863 4.70579 11.5446 4.64154C11.6072 4.58196 11.6567 4.51004 11.6901 4.43033C11.7235 4.35062 11.7399 4.26486 11.7385 4.17846V3.38154C11.7413 3.29384 11.7269 3.20644 11.696 3.12433C11.665 3.04222 11.6182 2.96701 11.5582 2.903C11.4982 2.83899 11.4262 2.78744 11.3462 2.75128C11.2663 2.71513 11.18 2.69509 11.0923 2.69231H9.85385C9.49248 2.67872 9.13248 2.74374 8.79869 2.88288C8.46491 3.02201 8.16531 3.23194 7.92058 3.49817C7.67586 3.7644 7.49184 4.08058 7.38124 4.42487C7.27064 4.76917 7.2361 5.13336 7.28 5.49231V6.75231H6.59077C6.5036 6.75088 6.41702 6.76682 6.33607 6.79919C6.25512 6.83156 6.18142 6.87972 6.11928 6.94087C6.05713 7.00201 6.00778 7.07491 5.97409 7.15532C5.94041 7.23574 5.92307 7.32205 5.92308 7.40923V8.23846C5.92307 8.32564 5.94041 8.41196 5.97409 8.49237C6.00778 8.57278 6.05713 8.64568 6.11928 8.70683C6.18142 8.76797 6.25512 8.81613 6.33607 8.84851C6.41702 8.88088 6.5036 8.89681 6.59077 8.89539H7.28V14H1.07692C0.791305 14 0.517386 13.8865 0.315423 13.6846C0.113461 13.4826 0 13.2087 0 12.9231Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187847\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"figma\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.502 5.06078C3.43235 5.07723 2.57031 5.94945 2.57031 7.02301C2.57031 8.10685 3.44894 8.98548 4.53278 8.98548H6.49524V5.06078H4.502ZM10.0071 5.06078C11.0682 5.08685 11.9204 5.95523 11.9204 7.02265C11.9204 8.10649 11.0418 8.98511 9.95795 8.98511C8.87411 8.98511 7.99548 8.10649 7.99548 7.02265C7.99548 5.95523 8.84769 5.08685 9.90883 5.06078H10.0071ZM10.2292 0.178345C11.1632 0.178345 11.9204 0.935529 11.9204 1.86956C11.9204 2.8036 11.1632 3.56078 10.2292 3.56078H10.0232C10.0015 3.56038 9.97975 3.56018 9.95795 3.56018C9.93615 3.56018 9.91439 3.56038 9.89269 3.56078H7.99548V0.178345H10.2292ZM2.57031 1.86956C2.57031 0.935529 3.3275 0.178345 4.26153 0.178345H6.49524V3.56055H4.53278C4.51913 3.56055 4.5055 3.56063 4.49189 3.56078H4.26153C3.3275 3.56078 2.57031 2.8036 2.57031 1.86956ZM4.26153 10.4863C3.3275 10.4863 2.57031 11.2435 2.57031 12.1775C2.57031 13.1116 3.3275 13.8688 4.26153 13.8688H4.53278C5.61662 13.8688 6.49524 12.9901 6.49524 11.9063V10.4863H4.26153Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"floppy-disk\", \"keywords\": [ \"disk\", \"floppy\", \"electronics\", \"device\", \"disc\", \"computer\", \"storage\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187844)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.875 0.000507706C4.49342 0.0104768 4.12949 0.165624 3.85789 0.435009L3.85645 0.436451L0.436447 3.85645L0.435004 3.8579C0.157888 4.13729 0.00166136 4.51439 4.44055e-06 4.9079L0 4.91V12.5C0 12.8978 0.158035 13.2794 0.43934 13.5607C0.720644 13.842 1.10217 14 1.5 14H2.13361V9.5C2.13361 9.15863 2.29883 8.86198 2.53828 8.66439C2.7743 8.46961 3.07276 8.375 3.36452 8.375H10.6354C10.9272 8.375 11.2257 8.46961 11.4617 8.66439C11.7011 8.86198 11.8664 9.15863 11.8664 9.5V14H12.5C12.8978 14 13.2794 13.842 13.5607 13.5607C13.842 13.2794 14 12.8978 14 12.5V1.5C14 1.10218 13.842 0.720649 13.5607 0.439344C13.2794 0.15804 12.8978 4.41074e-06 12.5 4.41074e-06L11.8664 0V3.5C11.8664 3.82845 11.7158 4.1227 11.4837 4.32488C11.2543 4.52466 10.9595 4.625 10.6672 4.625H6.07414C5.78187 4.625 5.48711 4.52466 5.2577 4.32488C5.02555 4.1227 4.875 3.82845 4.875 3.5V0.000507706ZM10.6164 0H6.125V3.375H10.6164V0ZM10.6164 14H3.38361V9.625H10.6164V14Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187844\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"gmail\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.0859375 6.78695V11.1358C0.0859375 11.6431 0.498634 12.0543 1.00772 12.0543H3.77307V9.17388L0.0859375 6.78695ZM9.0625 8.52705L7.04295 9.83444L5.02344 8.52707V3.58677L7.04295 4.88319L9.0625 3.58676V8.52705ZM10.3129 9.17386V12.0543H13.0782C13.5873 12.0543 14 11.6431 14 11.1358V6.78694L10.3129 9.17386ZM14 5.33069L10.3125 7.71785V2.78434L11.1595 2.24054C12.3864 1.45467 14 2.33241 14 3.7857V4.98217V5.33069ZM3.77307 2.78404L3.77344 7.71786L0.0859375 5.33069V4.98217V3.78576C0.0859375 2.33247 1.69951 1.45467 2.92642 2.24054L3.77307 2.78404Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"google\", \"keywords\": [ \"media\", \"google\", \"social\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187877)\\\">\\n<path d=\\\"M11.701 5.73497H7.54988C7.47675 5.73495 7.40435 5.74952 7.33693 5.77781C7.2695 5.8061 7.2084 5.84754 7.15719 5.89972C7.10599 5.9519 7.06571 6.01376 7.03872 6.08169C7.01173 6.14961 6.99857 6.22224 7 6.29531V7.81469C7 7.96045 7.05793 8.10023 7.16106 8.2033C7.26418 8.30636 7.40404 8.36426 7.54988 8.36426H10.4072C10.415 8.78076 10.3348 9.19423 10.172 9.57768C10.0091 9.96114 9.7671 10.306 9.46188 10.5897C9.15666 10.8733 8.79502 11.0895 8.40056 11.2241C8.00609 11.3587 7.58767 11.4087 7.17259 11.3707C4.75744 11.3707 3.59299 9.32331 3.59299 6.99573C3.59299 4.66816 4.8437 2.62076 7.17259 2.62076C8.20075 2.58667 9.20822 2.91467 10.0191 3.54748C10.0839 3.58987 10.1572 3.6175 10.2339 3.62842C10.3105 3.63933 10.3887 3.63326 10.4628 3.61065C10.5368 3.58803 10.605 3.54941 10.6625 3.49752C10.72 3.44564 10.7653 3.38176 10.7954 3.31041L11.4746 1.7587C11.5305 1.64433 11.5449 1.51412 11.5154 1.39031C11.4859 1.2665 11.4143 1.15676 11.3129 1.07982C10.0765 0.340925 8.65587 -0.0327003 7.21572 0.00224508C3.2911 -0.00853072 0.617188 3.04102 0.617188 6.99573C0.617188 10.9505 3.30188 14 7.17259 14C11.0433 14 13.383 11.2414 13.383 7.53452C13.383 6.34919 12.9086 5.73497 11.701 5.73497Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187877\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"google-drive\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.15349 1.92019L0.109375 9.10972L1.9539 12.061L3.63247 9.07683C3.64525 9.05065 3.65951 9.02533 3.67514 9.00097L5.99801 4.87143L4.15349 1.92019ZM7.53977 4.50805C7.51985 4.46887 7.49643 4.4313 7.46965 4.39587L5.45542 1.1731H9.53412L13.743 8.65546H10.1319L7.53977 4.50805ZM6.90381 6.32071L5.59051 8.65546H8.36303L6.90381 6.32071ZM4.74676 10.1555L3.10282 13.078H11.5183L13.3449 10.1555H4.74676Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"hand-held\", \"keywords\": [ \"tablet\", \"kindle\", \"device\", \"electronics\", \"ipad\", \"computer\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187850)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.49805 1.74749C2.49805 1.63427 2.58983 1.54248 2.70305 1.54248H11.2981C11.4113 1.54248 11.5031 1.63427 11.5031 1.74749V10.474H2.49805V1.74749ZM2.70305 0.0424805C1.7614 0.0424805 0.998047 0.805839 0.998047 1.74749V12.2525C0.998047 13.1942 1.7614 13.9576 2.70305 13.9576H11.2981C12.2398 13.9576 13.0031 13.1942 13.0031 12.2525V1.74749C13.0031 0.805839 12.2398 0.0424805 11.2981 0.0424805H2.70305ZM4.6131 2.63571C4.19888 2.63571 3.8631 2.9715 3.8631 3.38571C3.8631 3.79992 4.19888 4.13571 4.6131 4.13571H9.38814C9.80235 4.13571 10.1381 3.79992 10.1381 3.38571C10.1381 2.9715 9.80235 2.63571 9.38814 2.63571H4.6131ZM3.8631 6.00992C3.8631 5.5957 4.19888 5.25992 4.6131 5.25992H9.38814C9.80235 5.25992 10.1381 5.5957 10.1381 6.00992C10.1381 6.42413 9.80235 6.75992 9.38814 6.75992H4.6131C4.19888 6.75992 3.8631 6.42413 3.8631 6.00992ZM4.6131 7.88409C4.19888 7.88409 3.8631 8.21988 3.8631 8.63409C3.8631 9.04831 4.19888 9.38409 4.6131 9.38409H7.47812C7.89234 9.38409 8.22812 9.04831 8.22812 8.63409C8.22812 8.21988 7.89234 7.88409 7.47812 7.88409H4.6131Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187850\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"hand-held-tablet-drawing\", \"keywords\": [ \"tablet\", \"kindle\", \"device\", \"electronics\", \"ipad\", \"digital\", \"drawing\", \"canvas\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.7156 1.5625C2.60408 1.5625 2.51367 1.6529 2.51367 1.76442V10.3317H11.4849V1.76442C11.4849 1.6529 11.3945 1.5625 11.2829 1.5625H2.7156ZM1.01367 1.76442C1.01367 0.824477 1.77565 0.0625 2.7156 0.0625H11.2829C12.2229 0.0625 12.9849 0.824477 12.9849 1.76442V12.2356C12.9849 13.1755 12.2229 13.9375 11.2829 13.9375H2.7156C1.77565 13.9375 1.01367 13.1755 1.01367 12.2356V1.76442ZM4.66698 4.5917C4.89862 4.26425 5.21585 3.94471 5.50769 3.78661C5.64957 3.70975 5.74712 3.69184 5.8033 3.69381C5.84012 3.6951 5.88592 3.70385 5.95202 3.76728L5.95259 3.77414C5.95505 3.8148 5.94665 3.89977 5.897 4.04399C5.79462 4.34141 5.58998 4.69604 5.34313 5.12125L5.33035 5.14326C5.10887 5.52469 4.84265 5.98319 4.71588 6.39978C4.64977 6.61703 4.60043 6.8861 4.65539 7.1632C4.7161 7.46935 4.89318 7.72814 5.17118 7.90279C5.79527 8.29486 6.4244 8.02071 6.82614 7.7508C7.23491 7.47616 7.64265 7.05548 7.98851 6.69865L7.99975 6.68706C8.37771 6.29713 8.68975 5.97674 8.96605 5.78592C9.09415 5.69745 9.17359 5.66685 9.21142 5.65832L9.21457 5.66067C9.21377 5.66612 9.21265 5.67257 9.21111 5.68009C9.1821 5.82143 9.08927 5.99689 8.92828 6.30118L8.90309 6.3488C8.7592 6.62097 8.54556 7.02691 8.50951 7.453C8.48983 7.68553 8.52014 7.93893 8.64354 8.18719C8.76585 8.43326 8.95866 8.63217 9.1977 8.79152C9.48491 8.983 9.87295 8.90539 10.0645 8.61818C10.2559 8.33098 10.1783 7.94293 9.89108 7.75146C9.79339 7.68634 9.76835 7.64179 9.76289 7.63081C9.75852 7.62202 9.75113 7.60476 9.75506 7.55839C9.7662 7.42666 9.84491 7.2418 10.0082 6.93304C10.0222 6.90649 10.0373 6.87836 10.0531 6.84882C10.1846 6.60347 10.3679 6.26144 10.4356 5.93137C10.4777 5.7265 10.488 5.47881 10.3947 5.22428C10.2992 4.96362 10.1186 4.76033 9.89108 4.60866C9.27093 4.19523 8.6377 4.49356 8.25572 4.75736C7.87184 5.02248 7.48207 5.42488 7.14607 5.77178L7.1022 5.81706C6.7268 6.20434 6.41082 6.52391 6.12904 6.71323C6.01965 6.78672 5.94199 6.82167 5.8924 6.83686C5.89681 6.81661 5.90302 6.79234 5.91174 6.76368C5.98977 6.50727 6.17612 6.1761 6.42417 5.74883L6.45285 5.69945C6.67251 5.3214 6.93444 4.87062 7.07894 4.45085C7.22325 4.03163 7.33673 3.38407 6.84354 2.89087C6.55883 2.60617 6.21432 2.45747 5.84717 2.44458C5.49798 2.43232 5.17719 2.54401 4.91227 2.68753C4.39049 2.9702 3.94023 3.45458 3.6465 3.86982C3.44716 4.15162 3.514 4.54166 3.7958 4.741C4.0776 4.94034 4.46764 4.8735 4.66698 4.5917ZM5.83694 6.84511L5.83896 6.84683C5.83788 6.84649 5.83731 6.84566 5.83694 6.84511Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"hand-held-tablet-writing\", \"keywords\": [ \"tablet\", \"kindle\", \"device\", \"electronics\", \"ipad\", \"writing\", \"digital\", \"paper\", \"notepad\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187856)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.71665 1.59375C1.60779 1.59375 1.51954 1.682 1.51954 1.79087V10.3125H10.4378V7.94712C10.4378 7.5329 10.7735 7.19712 11.1878 7.19712C11.602 7.19712 11.9378 7.5329 11.9378 7.94712V12.2091C11.9378 13.1464 11.1779 13.9062 10.2406 13.9062H1.71665C0.779355 13.9062 0.0195312 13.1464 0.0195312 12.2091V1.79087C0.0195312 0.853574 0.779355 0.09375 1.71665 0.09375H7.8729C8.28711 0.09375 8.6229 0.429536 8.6229 0.84375C8.6229 1.25796 8.28711 1.59375 7.8729 1.59375H1.71665ZM3.61109 3.0072C3.26591 3.0072 2.98609 3.28702 2.98609 3.6322C2.98609 3.97738 3.26591 4.2572 3.61109 4.2572H5.50532C5.8505 4.2572 6.13032 3.97738 6.13032 3.6322C6.13032 3.28702 5.8505 3.0072 5.50532 3.0072H3.61109ZM2.98609 6.5C2.98609 6.15482 3.26591 5.875 3.61109 5.875H4.5582C4.90338 5.875 5.1832 6.15482 5.1832 6.5C5.1832 6.84518 4.90338 7.125 4.5582 7.125H3.61109C3.26591 7.125 2.98609 6.84518 2.98609 6.5ZM9.31807 7.53367C9.41886 7.51553 9.51164 7.46688 9.58388 7.39429L13.6829 3.2758C13.7766 3.18284 13.851 3.07223 13.9017 2.95037C13.9525 2.82852 13.9787 2.69781 13.9787 2.5658C13.9787 2.43379 13.9525 2.30308 13.9017 2.18122C13.851 2.05936 13.7766 1.94876 13.6829 1.8558L12.6229 0.795798C12.5299 0.70207 12.4193 0.627676 12.2974 0.576907C12.1756 0.526138 12.0449 0.5 11.9129 0.5C11.7808 0.5 11.6501 0.526138 11.5283 0.576907C11.4064 0.627676 11.2958 0.70207 11.2029 0.795798L7.08736 4.89189C7.01295 4.96595 6.96374 5.06154 6.9467 5.16513L6.58782 7.34713C6.53177 7.68791 6.82987 7.98154 7.16977 7.92036L9.31807 7.53367Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187856\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"hard-disk\", \"keywords\": [ \"device\", \"disc\", \"drive\", \"disk\", \"electronics\", \"platter\", \"turntable\", \"raid\", \"storage\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1 1.5C1 0.671573 1.67157 0 2.5 0H11.5C12.3284 0 13 0.671573 13 1.5V12.5C13 13.3284 12.3284 14 11.5 14H2.5C1.67157 14 1 13.3284 1 12.5V1.5ZM6.99109 8.19248C7.17641 7.90127 7.09057 7.51496 6.79935 7.32965C6.61735 7.21382 6.39819 7.20392 6.21418 7.28388C6.25401 7.26657 6.29548 7.25347 6.33787 7.24474C6.19061 7.26788 6.0493 7.35027 5.94346 7.51072C5.94111 7.51424 5.9388 7.5178 5.93652 7.52139L4.18652 10.2714C4.0012 10.5626 4.08705 10.9489 4.37826 11.1342C4.66947 11.3195 5.05578 11.2337 5.24109 10.9425L6.99109 8.19248ZM7.24866 11.6059C7.24866 11.2607 7.52848 10.9809 7.87366 10.9809H10.3737C10.7188 10.9809 10.9987 11.2607 10.9987 11.6059C10.9987 11.951 10.7188 12.2309 10.3737 12.2309H7.87366C7.52848 12.2309 7.24866 11.951 7.24866 11.6059ZM6.9903 1.89737C4.85019 1.89737 3.1153 3.63227 3.1153 5.77237C3.1153 6.70841 3.44719 7.56694 3.99971 8.23669L4.88198 6.85027C5.18022 6.38161 5.64261 6.08214 6.16609 6.00654C6.6556 5.93585 7.11227 6.07134 7.4595 6.29231C7.80674 6.51328 8.12289 6.86958 8.26618 7.34296C8.4194 7.84918 8.34395 8.3949 8.04571 8.86356L7.57479 9.60358C9.43757 9.32176 10.8653 7.71379 10.8653 5.77237C10.8653 3.63227 9.1304 1.89737 6.9903 1.89737ZM7.07176 7.71176C7.10965 7.87053 7.08565 8.04389 6.99109 8.19248C7.09766 8.02503 7.11407 7.85809 7.07176 7.71176Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"hard-drive-1\", \"keywords\": [ \"disk\", \"device\", \"electronics\", \"disc\", \"drive\", \"raid\", \"storage\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187865)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.588074 6.07072C0.7457 6.03702 0.909243 6.01929 1.07693 6.01929H12.9231C13.1021 6.01929 13.2764 6.0395 13.4438 6.07777L12.186 1.16243C12.1087 0.827214 11.9184 0.528689 11.647 0.317003C11.3753 0.10506 11.0389 -0.0068189 10.6945 5.47072e-05H3.30003L3.29728 6.2277e-05C2.95822 0.00192826 2.62979 0.118614 2.36556 0.331092C2.10337 0.541918 1.91974 0.834711 1.84404 1.16236L0.588074 6.07072ZM12.9231 7.26929H1.07692C0.482155 7.26929 0 7.75144 0 8.34621V12.6539C0 13.2487 0.482155 13.7309 1.07692 13.7309H12.9231C13.5178 13.7309 14 13.2487 14 12.6539V8.34621C14 7.75144 13.5178 7.26929 12.9231 7.26929ZM3.5 9.87505C3.15482 9.87505 2.875 10.1549 2.875 10.5001C2.875 10.8453 3.15482 11.1251 3.5 11.1251H6.5C6.84518 11.1251 7.125 10.8453 7.125 10.5001C7.125 10.1549 6.84518 9.87505 6.5 9.87505H3.5ZM9.50311 10.5001C9.50311 9.94948 9.94944 9.50315 10.5 9.50315C11.0506 9.50315 11.4969 9.94948 11.4969 10.5001C11.4969 11.0507 11.0506 11.497 10.5 11.497C9.94944 11.497 9.50311 11.0507 9.50311 10.5001Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187865\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"instagram\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.39088 0.786621C1.95286 0.786621 0.787109 1.95237 0.787109 3.3904V10.3338C0.787109 11.7718 1.95286 12.9376 3.39088 12.9376H10.3343C11.7723 12.9376 12.938 11.7718 12.938 10.3338V3.3904C12.938 1.95237 11.7723 0.786621 10.3343 0.786621H3.39088ZM11.0838 3.39417C11.0838 3.80838 10.748 4.14417 10.3338 4.14417C9.91959 4.14417 9.5838 3.80838 9.5838 3.39417C9.5838 2.97995 9.91959 2.64417 10.3338 2.64417C10.748 2.64417 11.0838 2.97995 11.0838 3.39417ZM6.86271 4.78198C5.71393 4.78198 4.78265 5.71326 4.78265 6.86204C4.78265 8.01082 5.71393 8.9421 6.86271 8.9421C8.01149 8.9421 8.94277 8.01082 8.94277 6.86204C8.94277 5.71326 8.01149 4.78198 6.86271 4.78198ZM3.78265 6.86204C3.78265 5.16097 5.16164 3.78198 6.86271 3.78198C8.56378 3.78198 9.94277 5.16097 9.94277 6.86204C9.94277 8.56311 8.56378 9.9421 6.86271 9.9421C5.16164 9.9421 3.78265 8.56311 3.78265 6.86204Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"keyboard\", \"keywords\": [ \"keyboard\", \"device\", \"electronics\", \"dvorak\", \"qwerty\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187871)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.5 0.0546875C11.9142 0.0546875 12.25 0.390474 12.25 0.804688C12.25 1.51747 11.948 2.19074 11.4276 2.67944C10.9088 3.16663 10.2147 3.43279 9.5 3.43279H5.5C5.15379 3.43279 4.83052 3.56238 4.59919 3.7796C4.36947 3.99532 4.25 4.27746 4.25 4.56089V4.99994H12.5C13.3284 4.99994 14 5.67151 14 6.49994V12.4999C14 13.3283 13.3284 13.9999 12.5 13.9999H1.5C0.671573 13.9999 0 13.3283 0 12.4999V6.49994C0 5.67151 0.671573 4.99994 1.5 4.99994H2.75V4.56089C2.75 3.84811 3.05196 3.17484 3.57238 2.68614C4.0912 2.19894 4.78534 1.93279 5.5 1.93279H9.5C9.84621 1.93279 10.1695 1.8032 10.4008 1.58598C10.6305 1.37025 10.75 1.08812 10.75 0.804688C10.75 0.390474 11.0858 0.0546875 11.5 0.0546875ZM4.86591 10.8437C4.86591 10.5675 5.08976 10.3437 5.36591 10.3437H8.63408C8.91022 10.3437 9.13408 10.5675 9.13408 10.8437C9.13408 11.1198 8.91022 11.3437 8.63408 11.3437H5.36591C5.08976 11.3437 4.86591 11.1198 4.86591 10.8437ZM2.09778 10.3437C1.82164 10.3437 1.59778 10.5675 1.59778 10.8437C1.59778 11.1198 1.82164 11.3437 2.09778 11.3437H3.18717C3.46331 11.3437 3.68717 11.1198 3.68717 10.8437C3.68717 10.5675 3.46331 10.3437 3.18717 10.3437H2.09778ZM10.8129 10.3437C10.5368 10.3437 10.3129 10.5675 10.3129 10.8437C10.3129 11.1198 10.5368 11.3437 10.8129 11.3437H11.9023C12.1784 11.3437 12.4023 11.1198 12.4023 10.8437C12.4023 10.5675 12.1784 10.3437 11.9023 10.3437H10.8129ZM1.59778 8.15615C1.59778 7.88001 1.82164 7.65615 2.09778 7.65615H3.18717C3.46331 7.65615 3.68717 7.88001 3.68717 8.15615C3.68717 8.43229 3.46331 8.65615 3.18717 8.65615H2.09778C1.82164 8.65615 1.59778 8.43229 1.59778 8.15615ZM5.00281 7.65615C4.72667 7.65615 4.50281 7.88001 4.50281 8.15615C4.50281 8.43229 4.72667 8.65615 5.00281 8.65615H6.0922C6.36834 8.65615 6.5922 8.43229 6.5922 8.15615C6.5922 7.88001 6.36834 7.65615 6.0922 7.65615H5.00281ZM7.40784 8.15615C7.40784 7.88001 7.63169 7.65615 7.90784 7.65615H8.99723C9.27337 7.65615 9.49723 7.88001 9.49723 8.15615C9.49723 8.43229 9.27337 8.65615 8.99723 8.65615H7.90784C7.63169 8.65615 7.40784 8.43229 7.40784 8.15615ZM10.8129 7.65615C10.5367 7.65615 10.3129 7.88001 10.3129 8.15615C10.3129 8.43229 10.5367 8.65615 10.8129 8.65615H11.9023C12.1784 8.65615 12.4023 8.43229 12.4023 8.15615C12.4023 7.88001 12.1784 7.65615 11.9023 7.65615H10.8129Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187871\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"keyboard-virtual\", \"keywords\": [ \"remote\", \"device\", \"electronics\", \"qwerty\", \"keyboard\", \"virtual\", \"interface\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187874)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0.203125C0.710645 0.203125 0 0.802204 0 1.625V7.15625C0 7.97905 0.710645 8.57812 1.5 8.57812H12.5C13.2894 8.57812 14 7.97905 14 7.15625V1.625C14 0.802204 13.2894 0.203125 12.5 0.203125H1.5ZM5 5.54688C5 5.27073 5.22386 5.04688 5.5 5.04688H8.5C8.77614 5.04688 9 5.27073 9 5.54688C9 5.82302 8.77614 6.04688 8.5 6.04688H5.5C5.22386 6.04688 5 5.82302 5 5.54688ZM2.5 5.04688C2.22386 5.04688 2 5.27073 2 5.54688C2 5.82302 2.22386 6.04688 2.5 6.04688H3.5C3.77614 6.04688 4 5.82302 4 5.54688C4 5.27073 3.77614 5.04688 3.5 5.04688H2.5ZM10.5 5.04688C10.2239 5.04688 10 5.27073 10 5.54688C10 5.82302 10.2239 6.04688 10.5 6.04688H11.5C11.7761 6.04688 12 5.82302 12 5.54688C12 5.27073 11.7761 5.04688 11.5 5.04688H10.5ZM2 3.04688C2 2.77073 2.22386 2.54688 2.5 2.54688H3.5C3.77614 2.54688 4 2.77073 4 3.04688C4 3.32302 3.77614 3.54688 3.5 3.54688H2.5C2.22386 3.54688 2 3.32302 2 3.04688ZM5.16675 2.54688C4.89061 2.54688 4.66675 2.77073 4.66675 3.04688C4.66675 3.32302 4.89061 3.54688 5.16675 3.54688H6.16675C6.44289 3.54688 6.66675 3.32302 6.66675 3.04688C6.66675 2.77073 6.44289 2.54688 6.16675 2.54688H5.16675ZM7.33325 3.04688C7.33325 2.77073 7.55711 2.54688 7.83325 2.54688H8.83325C9.10939 2.54688 9.33325 2.77073 9.33325 3.04688C9.33325 3.32302 9.10939 3.54688 8.83325 3.54688H7.83325C7.55711 3.54688 7.33325 3.32302 7.33325 3.04688ZM10.5 2.54688C10.2239 2.54688 10 2.77073 10 3.04688C10 3.32302 10.2239 3.54688 10.5 3.54688H11.5C11.7761 3.54688 12 3.32302 12 3.04688C12 2.77073 11.7761 2.54688 11.5 2.54688H10.5ZM4.53806 11.1055C4.61545 10.9187 4.79777 10.7969 5 10.7969H9C9.20223 10.7969 9.38455 10.9187 9.46194 11.1055C9.53933 11.2924 9.49655 11.5074 9.35355 11.6504L7.35355 13.6504C7.15829 13.8457 6.84171 13.8457 6.64645 13.6504L4.64645 11.6504C4.50345 11.5074 4.46067 11.2924 4.53806 11.1055Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187874\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"keyboard-wireless-2\", \"keywords\": [ \"remote\", \"device\", \"wireless\", \"electronics\", \"qwerty\", \"keyboard\", \"bluetooth\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187880)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.20828 0.845111C4.40822 0.338597 5.69749 0.0776367 6.99995 0.0776367C8.30241 0.0776367 9.59168 0.338597 10.7916 0.845111C11.1732 1.00619 11.352 1.44613 11.1909 1.82774C11.0298 2.20935 10.5899 2.38812 10.2083 2.22704C9.19295 1.79845 8.10203 1.57764 6.99995 1.57764C5.89787 1.57764 4.80695 1.79845 3.79162 2.22704C3.41001 2.38812 2.97007 2.20935 2.80899 1.82774C2.6479 1.44613 2.82667 1.00619 3.20828 0.845111ZM4.59889 2.92549C6.16896 2.39235 7.87111 2.39235 9.44119 2.92549C9.83341 3.05868 10.0434 3.4846 9.91021 3.87682C9.77703 4.26903 9.35111 4.47902 8.95889 4.34584C7.70158 3.9189 6.3385 3.9189 5.08119 4.34584C4.68897 4.47902 4.26305 4.26903 4.12986 3.87682C3.99668 3.4846 4.20667 3.05868 4.59889 2.92549ZM0 6.9693C0 6.1465 0.710645 5.54742 1.5 5.54742H12.5C13.2894 5.54742 14 6.1465 14 6.9693V12.5005C14 13.3233 13.2894 13.9224 12.5 13.9224H1.5C0.710645 13.9224 0 13.3233 0 12.5005V6.9693ZM5.5 10.3911C5.22386 10.3911 5 10.615 5 10.8911C5 11.1673 5.22386 11.3911 5.5 11.3911H8.5C8.77614 11.3911 9 11.1673 9 10.8911C9 10.615 8.77614 10.3911 8.5 10.3911H5.5ZM2 10.8911C2 10.615 2.22386 10.3911 2.5 10.3911H3.5C3.77614 10.3911 4 10.615 4 10.8911C4 11.1673 3.77614 11.3911 3.5 11.3911H2.5C2.22386 11.3911 2 11.1673 2 10.8911ZM10 10.8911C10 10.615 10.2239 10.3911 10.5 10.3911H11.5C11.7761 10.3911 12 10.615 12 10.8911C12 11.1673 11.7761 11.3911 11.5 11.3911H10.5C10.2239 11.3911 10 11.1673 10 10.8911ZM2.5 7.89117C2.22386 7.89117 2 8.11503 2 8.39117C2 8.66732 2.22386 8.89117 2.5 8.89117H3.5C3.77614 8.89117 4 8.66732 4 8.39117C4 8.11503 3.77614 7.89117 3.5 7.89117H2.5ZM4.66675 8.39117C4.66675 8.11503 4.89061 7.89117 5.16675 7.89117H6.16675C6.44289 7.89117 6.66675 8.11503 6.66675 8.39117C6.66675 8.66732 6.44289 8.89117 6.16675 8.89117H5.16675C4.89061 8.89117 4.66675 8.66732 4.66675 8.39117ZM7.83325 7.89117C7.55711 7.89117 7.33325 8.11503 7.33325 8.39117C7.33325 8.66732 7.55711 8.89117 7.83325 8.89117H8.83325C9.10939 8.89117 9.33325 8.66732 9.33325 8.39117C9.33325 8.11503 9.10939 7.89117 8.83325 7.89117H7.83325ZM10 8.39117C10 8.11503 10.2239 7.89117 10.5 7.89117H11.5C11.7761 7.89117 12 8.11503 12 8.39117C12 8.66732 11.7761 8.89117 11.5 8.89117H10.5C10.2239 8.89117 10 8.66732 10 8.39117Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187880\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"laptop-charging\", \"keywords\": [ \"device\", \"laptop\", \"electronics\", \"computer\", \"notebook\", \"charging\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187883)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.0823 1.1661C12.3121 0.821458 12.219 0.355806 11.8743 0.126042C11.5297 -0.103723 11.064 -0.0105918 10.8343 0.334054L9.00096 3.08405C8.84753 3.3142 8.83323 3.6101 8.96374 3.85397C9.09425 4.09784 9.3484 4.25008 9.625 4.25008H10.9736L9.91763 5.83405C9.68786 6.1787 9.78099 6.64435 10.1256 6.87412C10.4702 7.10388 10.9359 7.01075 11.1657 6.6661L12.999 3.9161C13.1524 3.68596 13.1667 3.39005 13.0362 3.14619C12.9057 2.90232 12.6516 2.75008 12.375 2.75008H11.0263L12.0823 1.1661ZM6.99657 3.04244C7.41078 3.04244 7.74657 3.37822 7.74657 3.79244C7.74657 4.20665 7.41078 4.54244 6.99657 4.54244H3.69927C3.5612 4.54244 3.44927 4.65437 3.44927 4.79244V8.75008H10.5438V8.50008C10.5438 8.08587 10.8796 7.75008 11.2938 7.75008C11.708 7.75008 12.0438 8.08587 12.0438 8.50008V9.39398L13.6979 11.6003C14.4393 12.5892 13.7337 14.0001 12.4978 14.0001H1.50272C0.266785 14.0001 -0.438824 12.5892 0.302554 11.6003L1.94927 9.40382V4.79244C1.94927 3.82594 2.73277 3.04244 3.69927 3.04244H6.99657Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187883\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"linkedin\", \"keywords\": [ \"network\", \"linkedin\", \"professional\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187886)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.91416 2.36157C3.17333 2.09441 3.317 1.73602 3.3141 1.36381C3.30061 1.00275 3.1497 0.66048 2.89224 0.40698C2.63478 0.153479 2.29021 0.00789779 1.92898 0C1.56669 0.00775305 1.22091 0.152974 0.961722 0.406224C0.702537 0.659474 0.549345 1.0018 0.533203 1.36381C0.538417 1.73538 0.686177 2.09073 0.945945 2.35645C1.20571 2.62217 1.55763 2.77793 1.92898 2.79156C2.3011 2.78317 2.65498 2.62872 2.91416 2.36157ZM1.66959 4.631C1.08309 4.61771 0.68237 4.60864 0.68237 5.3274V13.3185C0.68237 14.0224 1.06449 14.0078 1.61514 13.9869C1.71467 13.9831 1.8197 13.9791 1.92898 13.9791C2.03846 13.9791 2.14342 13.9831 2.24269 13.9869C2.7907 14.0078 3.16494 14.0221 3.16494 13.3185V5.3274C3.16494 4.60879 2.77256 4.61771 2.18829 4.63099C2.10552 4.63287 2.01889 4.63484 1.92898 4.63484C1.83921 4.63484 1.75254 4.63288 1.66959 4.631ZM5.75404 4.64551C5.48767 4.69878 5.3172 4.86926 5.3172 5.32741V13.3185C5.3172 14.0217 5.68338 14.0079 6.235 13.9869C6.33529 13.9831 6.4417 13.9791 6.55315 13.9791C6.66441 13.9791 6.77089 13.9831 6.87144 13.9869C7.42571 14.0079 7.79977 14.022 7.79977 13.3185V9.05659C7.77857 8.83881 7.80387 8.61901 7.87401 8.41174C7.94414 8.20448 8.0575 8.01447 8.20659 7.85431C8.35567 7.69414 8.53707 7.56747 8.73879 7.48268C8.94051 7.3979 9.15793 7.35692 9.37668 7.36248C9.59644 7.34945 9.81643 7.38387 10.0217 7.46339C10.227 7.54291 10.4127 7.66568 10.5664 7.82335C10.72 7.98103 10.8379 8.16992 10.9121 8.3772C10.9862 8.58447 11.0149 8.80528 10.9962 9.02463V13.2865C10.9962 13.9898 11.3624 13.9759 11.914 13.955C12.0143 13.9512 12.1207 13.9471 12.2321 13.9471C12.3436 13.9471 12.45 13.9512 12.5503 13.955C13.1019 13.9759 13.4681 13.9898 13.4681 13.2865V7.83129C13.4884 7.38339 13.4142 6.93623 13.2503 6.51887C13.0865 6.1015 12.8367 5.72328 12.5172 5.40876C12.1976 5.09424 11.8155 4.85046 11.3956 4.69327C10.9757 4.53608 10.5274 4.46898 10.0799 4.49634C9.62415 4.4549 9.1654 4.5311 8.74753 4.71765C8.32965 4.9042 7.96664 5.19486 7.69322 5.56182C7.70387 5.16759 7.57601 4.64551 7.25637 4.64551C7.16472 4.64551 7.024 4.64113 6.86362 4.63613C6.46468 4.62371 5.94403 4.60751 5.75404 4.64551Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187886\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"local-storage-folder\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187889)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.79609 5H13.5655C13.8056 5 14.0002 4.80535 14.0002 4.56523V1.30448C14.0002 1.18918 13.9544 1.07859 13.8729 0.997058C13.7914 0.915523 13.6808 0.869718 13.5655 0.869718H10.9569L10.8351 0.330607C10.8113 0.234143 10.7552 0.148742 10.6761 0.088622C10.597 0.0285022 10.4997 -0.00270902 10.4004 0.00018452H8.79609C8.68079 0.00018452 8.5702 0.0459902 8.48867 0.127525C8.40713 0.209059 8.36133 0.319644 8.36133 0.434951V4.56523C8.36133 4.68054 8.40713 4.79113 8.48867 4.87266C8.5702 4.95419 8.68079 5 8.79609 5ZM2 3H6.25C6.80228 3 7.25 2.55228 7.25 2C7.25 1.44772 6.80228 1 6.25 1H1.45679C0.652229 1 0 1.65223 0 2.45679V9.54321C0 10.3478 0.65223 11 1.45679 11H5.48481L4.94788 12.5002H4C3.58579 12.5002 3.25 12.836 3.25 13.2502C3.25 13.6645 3.58579 14.0002 4 14.0002H10C10.4142 14.0002 10.75 13.6645 10.75 13.2502C10.75 12.836 10.4142 12.5002 10 12.5002H9.05215L8.51522 11H12.5432C13.3478 11 14 10.3478 14 9.54321V7.25C14 6.69772 13.5523 6.25 13 6.25C12.4477 6.25 12 6.69772 12 7.25V9.00001H2V3Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187889\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"meta\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path d=\\\"M10.4286 2.57141C11.8598 2.57141 12.7635 3.64808 13.2516 4.62419C13.7574 5.63586 14 6.86326 14 7.85712C14 9.83808 12.9429 11.4285 11.2857 11.4285C10.7443 11.4285 10.2791 11.2118 9.90976 10.9556C9.54105 10.6999 9.20513 10.3617 8.90201 10.0112C8.44119 9.47815 7.96343 8.8029 7.4994 8.14705C7.36445 7.95632 7.23056 7.76708 7.0987 7.58362C6.48466 6.7293 5.88256 5.95105 5.25406 5.38717C4.62516 4.82292 4.07809 4.57141 3.57143 4.57141C3.28836 4.57141 2.90637 4.78045 2.53728 5.51862C2.18598 6.22124 2 7.1367 2 7.85712C2 9.30474 2.65715 9.42855 2.71429 9.42855C2.80273 9.42855 2.91903 9.40356 3.34598 8.96491C3.54885 8.75647 3.88142 8.33614 4.20261 7.91129C4.52898 7.4796 5.13591 7.37013 5.58447 7.67291C6.05174 7.98832 6.16752 8.62805 5.82794 9.07807C5.46006 9.56557 5.05775 10.0737 4.77918 10.3599C4.29185 10.8606 3.59369 11.4286 2.71429 11.4286C1.05714 11.4286 0 9.83807 0 7.85712C0 6.86326 0.242596 5.63586 0.74843 4.6242C1.23649 3.64808 2.14021 2.57141 3.57143 2.57141C4.77905 2.57141 5.78555 3.17704 6.58969 3.89851C7.39423 4.62034 8.10463 5.55638 8.72273 6.41634C8.88001 6.63517 9.03031 6.84746 9.17515 7.05203C9.62594 7.68871 10.0239 8.25076 10.415 8.70309C10.6676 8.99536 10.8775 9.19288 11.0495 9.31217C11.1325 9.36971 11.1937 9.39984 11.234 9.41494C11.2536 9.42225 11.267 9.42559 11.2749 9.42711C11.2788 9.42788 11.2832 9.42839 11.2832 9.42839L11.2857 9.42855C11.3429 9.42855 12 9.30474 12 7.85712C12 7.1367 11.814 6.22124 11.4627 5.51862C11.0936 4.78045 10.7116 4.57141 10.4286 4.57141C10.1362 4.59057 9.87935 4.64334 9.64631 4.71532C9.08839 4.88765 8.47213 4.61328 8.36886 4.03856C8.3028 3.6709 8.4718 3.29028 8.7842 3.08546C9.16033 2.83885 9.60668 2.60008 10.4286 2.57141Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"mouse\", \"keywords\": [ \"device\", \"electronics\", \"mouse\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.65874 1.93946C5.94005 1.65816 6.32158 1.50012 6.7194 1.50012C7.11723 1.50012 7.49876 1.65816 7.78006 1.93946C8.06137 2.22077 8.2194 2.6023 8.2194 3.00012V3.25012C8.2194 3.97947 8.50914 4.67894 9.02486 5.19466C9.54058 5.71039 10.2401 6.00012 10.9694 6.00012C11.6987 6.00012 12.3982 5.71039 12.9139 5.19466C13.4297 4.67894 13.7194 3.97947 13.7194 3.25012V1.75012C13.7194 1.33591 13.3836 1.00012 12.9694 1.00012C12.5552 1.00012 12.2194 1.33591 12.2194 1.75012V3.25012C12.2194 3.58164 12.0877 3.89958 11.8533 4.134C11.6189 4.36843 11.3009 4.50012 10.9694 4.50012C10.6379 4.50012 10.3199 4.36843 10.0855 4.134C9.8511 3.89958 9.7194 3.58164 9.7194 3.25012V3.00012C9.7194 2.20447 9.40333 1.44141 8.84072 0.878802C8.27811 0.316193 7.51505 0.00012207 6.7194 0.00012207C5.92375 0.00012207 5.16069 0.316193 4.59808 0.878802C4.1889 1.28798 3.91013 1.80319 3.78851 2.35985C4.01083 2.32772 4.23816 2.3111 4.46938 2.3111C4.76959 2.3111 5.06325 2.33913 5.34789 2.39271C5.422 2.22536 5.52682 2.07139 5.65874 1.93946ZM7.9388 7.03051C7.9388 5.11441 6.38549 3.56111 4.4694 3.56111C2.5533 3.56111 1 5.11441 1 7.03051V10.4999C1 12.416 2.5533 13.9693 4.4694 13.9693C6.38549 13.9693 7.9388 12.416 7.9388 10.4999V7.03051ZM5.09442 5.9693C5.09442 5.62412 4.8146 5.3443 4.46942 5.3443C4.12424 5.3443 3.84442 5.62412 3.84442 5.9693V6.9693C3.84442 7.31448 4.12424 7.5943 4.46942 7.5943C4.8146 7.5943 5.09442 7.31448 5.09442 6.9693V5.9693Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"mouse-wireless\", \"keywords\": [ \"remote\", \"wireless\", \"device\", \"electronics\", \"mouse\", \"computer\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2 5C2 2.23858 4.23858 0 7 0C9.76142 0 12 2.23858 12 5V9C12 11.7614 9.76142 14 7 14C4.23858 14 2 11.7614 2 9V5ZM7.625 3.5C7.625 3.15482 7.34518 2.875 7 2.875C6.65482 2.875 6.375 3.15482 6.375 3.5V5C6.375 5.34518 6.65482 5.625 7 5.625C7.34518 5.625 7.625 5.34518 7.625 5V3.5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"mouse-wireless-1\", \"keywords\": [ \"remote\", \"wireless\", \"device\", \"electronics\", \"mouse\", \"computer\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.00016 0.0776367C5.71439 0.0776367 4.44166 0.335252 3.2571 0.835272C2.9391 0.969508 2.79012 1.33612 2.92436 1.65413C3.05859 1.97214 3.42521 2.12111 3.74322 1.98688C4.77393 1.55179 5.88138 1.32764 7.00016 1.32764C8.11894 1.32764 9.22639 1.55179 10.2571 1.98688C10.5751 2.12111 10.9417 1.97214 11.0759 1.65413C11.2102 1.33612 11.0612 0.969508 10.7432 0.835272C9.55866 0.335252 8.28593 0.0776367 7.00016 0.0776367ZM3.50016 8.42242C3.50016 6.48943 5.06716 4.92242 7.00016 4.92242C8.93316 4.92242 10.5001 6.48943 10.5001 8.42242V10.4224C10.5001 12.3554 8.93316 13.9224 7.00016 13.9224C5.06716 13.9224 3.50016 12.3554 3.50016 10.4224V8.42242ZM4.63929 2.91885C6.1833 2.39456 7.85719 2.39456 9.40121 2.91885C9.72805 3.02984 9.90305 3.38478 9.79206 3.71162C9.68107 4.03847 9.32614 4.21346 8.99929 4.10248C7.71591 3.66669 6.32458 3.66669 5.04121 4.10248C4.71436 4.21346 4.35942 4.03847 4.24844 3.71162C4.13745 3.38478 4.31244 3.02984 4.63929 2.91885ZM7.75024 7.42242C7.75024 7.00821 7.41446 6.67242 7.00024 6.67242C6.58603 6.67242 6.25024 7.00821 6.25024 7.42242V8.42242C6.25024 8.83664 6.58603 9.17242 7.00024 9.17242C7.41446 9.17242 7.75024 8.83664 7.75024 8.42242V7.42242Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"netflix\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.625 13.2117V2.79296L5.025 10.2805V12.2482C5.025 12.4524 4.9008 12.6361 4.71128 12.7122L2.31128 13.6757C2.15721 13.7375 1.98248 13.7189 1.84495 13.6259C1.70741 13.5329 1.625 13.3777 1.625 13.2117ZM12.3751 0.511231V11.3371L8.9751 4.28157V0.511231C8.9751 0.235088 9.19896 0.0112305 9.4751 0.0112305H11.8751C12.0077 0.0112305 12.1349 0.0639089 12.2287 0.157677C12.3224 0.251445 12.3751 0.378622 12.3751 0.511231ZM2.125 0.266113H5.37503L11.8751 13.7548L7.81255 12.7913L2.125 0.266113Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"network\", \"keywords\": [ \"network\", \"server\", \"internet\", \"ethernet\", \"connection\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.80549 4.48686C2.05836 2.36304 3.58358 0.629705 5.59737 0.0706266C4.69829 1.48703 4.19207 2.95966 4.11496 4.48686H1.80549ZM1.80548 5.73686H4.14727C4.29704 7.21289 4.8399 8.69941 5.75703 10.1948C3.66453 9.68488 2.06495 7.9165 1.80548 5.73686ZM12.1945 5.73686C11.9363 7.90587 10.351 9.66762 8.2735 10.1872C9.19999 8.7155 9.74606 7.23051 9.88403 5.73686H12.1945ZM7.00978 9.838C7.95407 8.43114 8.48277 7.06506 8.62765 5.73686H5.40492C5.56106 7.04523 6.08329 8.41022 7.00978 9.838ZM5.36688 4.48686H8.65091C8.54874 3.07895 8.00977 1.66163 6.99663 0.231277C5.98701 1.64015 5.45254 3.05585 5.36688 4.48686ZM12.1945 4.48686H9.90373C9.81137 2.97752 9.29794 1.50277 8.39084 0.0673828C10.4104 0.622974 11.9411 2.35892 12.1945 4.48686ZM6.25 11.5504V12.4458H2C1.58579 12.4458 1.25 12.7816 1.25 13.1958C1.25 13.61 1.58579 13.9458 2 13.9458H12C12.4142 13.9458 12.75 13.61 12.75 13.1958C12.75 12.7816 12.4142 12.4458 12 12.4458H7.75V11.5504C7.50393 11.5787 7.25366 11.5933 6.99999 11.5933C6.74633 11.5933 6.49607 11.5787 6.25 11.5504Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"next\", \"keywords\": [ \"next\", \"arrow\", \"right\", \"keyboard\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13 2.5C13.5523 2.5 14 2.94772 14 3.5V10.5C14 11.0523 13.5523 11.5 13 11.5C12.4477 11.5 12 11.0523 12 10.5V3.5C12 2.94772 12.4477 2.5 13 2.5ZM6 8.25H1.25C0.559644 8.25 0 7.69036 0 7C0 6.30965 0.559644 5.75 1.25 5.75H6V3.70711C6 3.26165 6.53857 3.03857 6.85355 3.35355L10.1464 6.64645C10.3417 6.84171 10.3417 7.15829 10.1464 7.35355L6.85355 10.6464C6.53857 10.9614 6 10.7383 6 10.2929V8.25Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"paypal\", \"keywords\": [ \"payment\", \"paypal\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187913)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.9808 0.0152523L8.0008 1.09486C8.80642 1.2621 9.53012 1.70091 10.0508 2.33781C10.5714 2.97472 10.8574 3.77108 10.8608 4.59358C10.8591 5.1175 10.7404 5.63444 10.5133 6.10665C10.2863 6.57885 9.95661 6.99441 9.54837 7.32299C9.14013 7.65157 8.66365 7.88487 8.1537 8.00586C7.64375 8.12685 7.11321 8.13248 6.6008 8.02233L3.1608 7.2826L2.1608 11.9609C2.14771 12.0239 2.12231 12.0838 2.08607 12.137C2.04982 12.1902 2.00344 12.2357 1.94957 12.271C1.89571 12.3063 1.83541 12.3306 1.77213 12.3426C1.70886 12.3545 1.64384 12.3539 1.5808 12.3408L0.380804 12.0808C0.255305 12.0522 0.146138 11.9753 0.0769012 11.8668C0.00766461 11.7583 -0.0160786 11.6269 0.0108044 11.5011L1.0208 6.79278L2.3808 0.385117C2.39487 0.320879 2.42172 0.260129 2.45977 0.206488C2.49783 0.152848 2.54629 0.107415 2.60228 0.0728997C2.65828 0.038384 2.72065 0.0154917 2.78568 0.00558757C2.85071 -0.00431652 2.91707 -0.00102961 2.9808 0.0152523ZM3.6408 5.1034L6.6408 5.75316L6.6308 5.73317C6.97293 5.8074 7.33055 5.74273 7.62498 5.55339C7.91941 5.36405 8.12654 5.06554 8.2008 4.72353C8.27506 4.38153 8.21037 4.02404 8.02096 3.72972C7.83155 3.43539 7.53293 3.22834 7.1908 3.15411L4.1908 2.50434L3.6408 5.1034ZM4.16002 9.65954L4.33944 8.81419L6.33803 9.24396C7.0324 9.39322 7.75134 9.3856 8.44238 9.22164C9.13342 9.05768 9.77911 8.74154 10.3323 8.29628C10.8855 7.85102 11.3323 7.28788 11.64 6.64799C11.9477 6.0081 12.1086 5.30759 12.1108 4.59762V4.58837C12.1104 4.4953 12.1073 4.40248 12.1016 4.31001C12.517 4.53186 12.8877 4.83484 13.19 5.20458C13.7106 5.84149 13.9966 6.63785 14 7.46035C13.9983 7.98427 13.8796 8.5012 13.6526 8.97341C13.4255 9.44562 13.0958 9.86118 12.6876 10.1898C12.2793 10.5183 11.8029 10.7516 11.2929 10.8726C10.783 10.9936 10.2524 10.9992 9.74001 10.8891L6.30002 10.1494L5.4744 13.6098C5.46131 13.6728 5.43591 13.7326 5.39967 13.7859C5.36342 13.8391 5.31704 13.8846 5.26317 13.9199C5.20931 13.9552 5.14901 13.9795 5.08573 13.9915C5.02246 14.0034 4.95744 14.0028 4.8944 13.9896L3.6944 13.7297C3.5689 13.7011 3.45974 13.6242 3.3905 13.5157C3.32126 13.4072 3.29752 13.2758 3.3244 13.1499L4.16002 9.65954Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187913\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"play-store\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187898)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.351562 1.30517C0.351562 1.28038 0.352551 1.25591 0.354488 1.23176L5.98374 6.86101L0.354434 12.4903C0.352532 12.4664 0.351562 12.4421 0.351562 12.4176V1.30517ZM1.60888 13.3572C1.70975 13.338 1.81026 13.3044 1.90767 13.2549L8.85033 9.7276L7.0444 7.92167L1.60888 13.3572ZM8.10506 6.86101L10.257 9.01294L12.8437 7.69871C13.562 7.33375 13.562 6.38899 12.8437 6.02403L10.2565 4.70956L8.10506 6.86101ZM8.84986 3.99489L1.90767 0.46783C1.81055 0.418487 1.71034 0.384943 1.60977 0.365723L7.0444 5.80035L8.84986 3.99489Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187898\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"printer\", \"keywords\": [ \"scan\", \"device\", \"electronics\", \"printer\", \"print\", \"computer\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187934)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.25 0.5V3H10.75V0.5C10.75 0.223858 10.5261 0 10.25 0H3.75C3.47386 0 3.25 0.223858 3.25 0.5ZM3.8877 9.5V13C3.8877 13.1326 3.93454 13.2598 4.01792 13.3536C4.1013 13.4473 4.21439 13.5 4.33231 13.5H9.66765C9.78557 13.5 9.89866 13.4473 9.98204 13.3536C10.0654 13.2598 10.1123 13.1326 10.1123 13V9.5H3.8877ZM0.450532 4.80396C0.733887 4.53775 1.11189 4.39319 1.5 4.39319H12.5C12.8881 4.39319 13.2661 4.53775 13.5495 4.80396C13.8339 5.07117 14 5.4405 14 5.8327V10.0605C14 10.4527 13.8339 10.822 13.5495 11.0893C13.2661 11.3555 12.8881 11.5 12.5 11.5H11.3623V9.50003C11.3623 8.80968 10.8026 8.25003 10.1123 8.25003H3.8877C3.19734 8.25003 2.6377 8.80968 2.6377 9.50003V11.5H1.5C1.11189 11.5 0.733887 11.3555 0.450532 11.0893C0.16612 10.822 0 10.4527 0 10.0605V5.8327C0 5.4405 0.16612 5.07117 0.450532 4.80396ZM9.31476 6.24783C9.31476 5.90266 9.59458 5.62283 9.93976 5.62283H11.5768C11.922 5.62283 12.2018 5.90266 12.2018 6.24783C12.2018 6.59301 11.922 6.87283 11.5768 6.87283H9.93976C9.59458 6.87283 9.31476 6.59301 9.31476 6.24783Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187934\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"return-2\", \"keywords\": [ \"arrow\", \"return\", \"enter\", \"keyboard\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187928)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.54608 0.998413C5.9938 0.998413 5.54608 1.44613 5.54608 1.99841C5.54608 2.5507 5.9938 2.99841 6.54608 2.99841H9.27421C9.97371 2.99841 10.6445 3.27629 11.1392 3.77092C11.6338 4.26554 11.9117 4.9364 11.9117 5.63591C11.9117 6.33542 11.6338 7.00627 11.1392 7.5009C10.6445 7.99553 9.97371 8.2734 9.27421 8.2734H4.81796V6.54529C4.81796 6.14083 4.57432 5.77619 4.20065 5.62141C3.82697 5.46663 3.39685 5.55218 3.11086 5.83818L0.384794 8.56425C0.37822 8.57078 0.371737 8.57741 0.365346 8.58412C0.278808 8.675 0.212657 8.77816 0.166893 8.88781C0.11726 9.00647 0.0898438 9.13674 0.0898438 9.2734C0.0898438 9.40806 0.116457 9.53649 0.164712 9.65374C0.213521 9.77261 0.286196 9.88398 0.382737 9.98052L3.11086 12.7087C3.39685 12.9947 3.82697 13.0802 4.20065 12.9254C4.57432 12.7707 4.81796 12.406 4.81796 12.0016V10.2734H9.27421C10.5041 10.2734 11.6837 9.78481 12.5534 8.91511C13.4231 8.04541 13.9117 6.86585 13.9117 5.63591C13.9117 4.40597 13.4231 3.2264 12.5534 2.3567C11.6837 1.48701 10.5041 0.998413 9.27421 0.998413H6.54608Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187928\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"screen-1\", \"keywords\": [ \"screen\", \"device\", \"electronics\", \"monitor\", \"diplay\", \"computer\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187910)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2 9V2.5H12V9H2ZM1.45679 0.5C0.652229 0.5 0 1.15223 0 1.95679V9.54321C0 10.3478 0.65223 11 1.45679 11H5.48306L4.94786 12.4954H3.99998C3.58577 12.4954 3.24998 12.8312 3.24998 13.2454C3.24998 13.6596 3.58577 13.9954 3.99998 13.9954H9.99998C10.4142 13.9954 10.75 13.6596 10.75 13.2454C10.75 12.8312 10.4142 12.4954 9.99998 12.4954H9.05214L8.51694 11H12.5432C13.3478 11 14 10.3478 14 9.54321V1.95679C14 1.15223 13.3478 0.5 12.5432 0.5H1.45679Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187910\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"screen-2\", \"keywords\": [ \"screen\", \"device\", \"electronics\", \"monitor\", \"diplay\", \"computer\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187925)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12 9.00001V2.5H2V9.00001H12ZM12.5432 0.5C13.3478 0.5 14 1.15223 14 1.95679V9.54321C14 10.3478 13.3478 11 12.5432 11H10.0568C10.5872 11.5918 10.9371 12.3282 11.0579 13.1215C11.1202 13.531 10.8388 13.9135 10.4293 13.9758C10.0198 14.0382 9.63731 13.7568 9.57495 13.3473C9.48119 12.7315 9.16988 12.1698 8.69748 11.7639C8.22508 11.358 7.62286 11.1348 7.00002 11.1348C6.37717 11.1348 5.77495 11.358 5.30255 11.7639C4.83015 12.1698 4.51884 12.7315 4.42508 13.3473C4.36272 13.7568 3.98021 14.0382 3.57072 13.9758C3.16123 13.9135 2.87982 13.531 2.94217 13.1215C3.06296 12.3282 3.41286 11.5918 3.94325 11H1.45679C0.652229 11 0 10.3478 0 9.54321V1.95679C0 1.15223 0.652228 0.5 1.45679 0.5H12.5432Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187925\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"screen-curve\", \"keywords\": [ \"screen\", \"curved\", \"device\", \"electronics\", \"monitor\", \"diplay\", \"computer\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187943)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 2.49465C14 1.32017 12.9036 0.541563 11.8532 0.730161C10.6188 0.951804 8.73727 1.14538 7 1.14538C5.26273 1.14538 3.38116 0.951804 2.14679 0.730161C1.09644 0.541563 0 1.32017 0 2.49465V6.00005V9.50544C0 10.68 1.09644 11.4586 2.14679 11.27C3.20507 11.08 4.73907 10.9106 6.25 10.8661V12.2989H4.63861C4.2244 12.2989 3.88861 12.6347 3.88861 13.0489C3.88861 13.4631 4.2244 13.7989 4.63861 13.7989H9.36138C9.77559 13.7989 10.1114 13.4631 10.1114 13.0489C10.1114 12.6347 9.77559 12.2989 9.36138 12.2989H7.75V10.8661C9.26093 10.9106 10.7949 11.08 11.8532 11.27C12.9036 11.4586 14 10.68 14 9.50544V6.00005V2.49465ZM7 3.14538C8.76644 3.14538 10.6597 2.96058 12 2.73463V6.00005V9.26547C10.6597 9.03952 8.76644 8.85471 7 8.85471C5.23356 8.85471 3.34026 9.03952 2 9.26547V6.00005V2.73463C3.34026 2.96058 5.23356 3.14538 7 3.14538Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187943\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"screensaver-monitor-wallpaper\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187922)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2 2.24988V8.74989H4.17811L8.17632 5.00492C8.1834 4.99829 8.19067 4.99186 8.19813 4.98565C8.37781 4.83599 8.60427 4.75403 8.83813 4.75403C9.06504 4.75403 9.28499 4.83119 9.462 4.97251L12 6.79773V2.24988H2ZM0 1.70667C0 0.902107 0.652229 0.249878 1.45679 0.249878H12.5432C13.3478 0.249878 14 0.902107 14 1.70667V9.29309C14 10.0977 13.3478 10.7499 12.5432 10.7499H8.51522L9.05215 12.2501H10C10.4142 12.2501 10.75 12.5859 10.75 13.0001C10.75 13.4143 10.4142 13.7501 10 13.7501H4C3.58579 13.7501 3.25 13.4143 3.25 13.0001C3.25 12.5859 3.58579 12.2501 4 12.2501H4.94788L5.48481 10.7499H1.45679C0.65223 10.7499 0 10.0977 0 9.29309V1.70667ZM5.96875 4.39075C5.96875 5.14151 5.36014 5.75012 4.60938 5.75012C3.85861 5.75012 3.25 5.14151 3.25 4.39075C3.25 3.63999 3.85861 3.03137 4.60938 3.03137C5.36014 3.03137 5.96875 3.63999 5.96875 4.39075Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187922\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shift\", \"keywords\": [ \"key\", \"shift\", \"up\", \"arrow\", \"keyboard\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path d=\\\"M6.64738 0.8536L1.35448 6.1465C1.0395 6.46148 1.26258 7.00005 1.70804 7.00005H3.50093V13C3.50093 13.2761 3.72479 13.5 4.00093 13.5H10.0009C10.277 13.5 10.5009 13.2761 10.5009 13V7.00005H12.2938C12.7392 7.00005 12.9623 6.46148 12.6473 6.1465L7.35448 0.853599C7.15922 0.658337 6.84264 0.658338 6.64738 0.8536Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"shredder\", \"keywords\": [ \"device\", \"electronics\", \"shred\", \"paper\", \"cut\", \"destroy\", \"remove\", \"delete\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187931)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.0761 3.53792V2.74339C11.0761 2.47817 10.9707 2.22382 10.7832 2.03628L9.03978 0.292894C8.85224 0.105357 8.59789 0 8.33267 0H3.42395C3.14781 0 2.92395 0.223858 2.92395 0.5V3.53792H11.0761ZM0.470422 5.13255C0.755714 4.89179 1.12702 4.76749 1.5 4.76749H12.5C12.873 4.76749 13.2443 4.89179 13.5296 5.13255C13.8174 5.37545 14 5.72493 14 6.1114V9.90899C14 10.2955 13.8174 10.6449 13.5296 10.8878C13.2443 11.1286 12.873 11.2529 12.5 11.2529H11.7995C11.8093 11.2075 11.8144 11.1609 11.8144 11.1138V7.9824C11.8144 7.79785 11.7364 7.62085 11.5975 7.49035C11.4586 7.35984 11.2702 7.28653 11.0737 7.28653H2.92634C2.7299 7.28653 2.54151 7.35984 2.40261 7.49035C2.2637 7.62085 2.18567 7.79785 2.18567 7.9824V11.1138C2.18567 11.1609 2.19074 11.2075 2.20061 11.2529H1.5C1.12702 11.2529 0.755714 11.1286 0.470422 10.8878C0.182585 10.6449 0 10.2955 0 9.90899V6.1114C0 5.72494 0.182585 5.37545 0.470422 5.13255ZM7.75 9C7.75 8.58579 7.41421 8.25 7 8.25C6.58579 8.25 6.25 8.58579 6.25 9V13.197C6.25 13.6112 6.58579 13.947 7 13.947C7.41421 13.947 7.75 13.6112 7.75 13.197V9ZM4.28898 8.25C4.7032 8.25 5.03898 8.58579 5.03898 9V11.6617C5.03898 12.197 4.82636 12.7103 4.44788 13.0888C4.0694 13.4673 3.55607 13.6799 3.02081 13.6799C2.6066 13.6799 2.27081 13.3441 2.27081 12.9299C2.27081 12.5157 2.6066 12.1799 3.02081 12.1799C3.15824 12.1799 3.29004 12.1253 3.38722 12.0281C3.48439 11.931 3.53898 11.7992 3.53898 11.6617V9C3.53898 8.58579 3.87477 8.25 4.28898 8.25ZM10.4608 9C10.4608 8.58579 10.125 8.25 9.71082 8.25C9.2966 8.25 8.96082 8.58579 8.96082 9V11.6617C8.96082 12.197 9.17344 12.7103 9.55192 13.0888C9.9304 13.4673 10.4437 13.6799 10.979 13.6799C11.3932 13.6799 11.729 13.3441 11.729 12.9299C11.729 12.5157 11.3932 12.1799 10.979 12.1799C10.8416 12.1799 10.7098 12.1253 10.6126 12.0281C10.5154 11.931 10.4608 11.7992 10.4608 11.6617V9Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187931\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"signal-loading\", \"keywords\": [ \"bracket\", \"loading\", \"internet\", \"angle\", \"signal\", \"server\", \"network\", \"connecting\", \"connection\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.69949 3.95752C3.9991 4.24354 4.01011 4.71829 3.72408 5.01789L1.83182 7L3.72408 8.98211C4.01011 9.28171 3.9991 9.75646 3.69949 10.0425C3.39989 10.3285 2.92514 10.3175 2.63912 10.0179L0.25244 7.51789C-0.0242509 7.22807 -0.0242509 6.77194 0.25244 6.48211L2.63912 3.98211C2.92514 3.6825 3.39989 3.67149 3.69949 3.95752ZM10.301 3.95752C10.6006 3.67149 11.0754 3.6825 11.3614 3.98211L13.7481 6.48211C14.0248 6.77194 14.0248 7.22807 13.7481 7.51789L11.3614 10.0179C11.0754 10.3175 10.6006 10.3285 10.301 10.0425C10.0014 9.75646 9.99046 9.28171 10.2764 8.98211L12.1687 7L10.2764 5.01789C9.99046 4.71829 10.0014 4.24354 10.301 3.95752ZM4.5003 6.25C4.91451 6.25 5.2503 6.58579 5.2503 7C5.2503 7.41421 4.91451 7.75 4.5003 7.75C4.08609 7.75 3.7503 7.41421 3.7503 7C3.7503 6.58579 4.08609 6.25 4.5003 6.25ZM7.7503 7C7.7503 6.58579 7.41451 6.25 7.0003 6.25C6.58609 6.25 6.2503 6.58579 6.2503 7C6.2503 7.41421 6.58609 7.75 7.0003 7.75C7.41451 7.75 7.7503 7.41421 7.7503 7ZM10.2503 7C10.2503 6.58579 9.91451 6.25 9.5003 6.25C9.08609 6.25 8.7503 6.58579 8.7503 7C8.7503 7.41421 9.08609 7.75 9.5003 7.75C9.91451 7.75 10.2503 7.41421 10.2503 7Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"slack\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187949)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.855 0.25C9.28298 0.25 9.62993 0.59695 9.62993 1.02493V6.12578C9.62993 6.55376 9.28298 6.90071 8.855 6.90071C8.42701 6.90071 8.08006 6.55376 8.08006 6.12578V1.02493C8.08006 0.59695 8.42701 0.25 8.855 0.25ZM5.14581 7.09937C5.5738 7.09937 5.92075 7.44632 5.92075 7.8743V12.9751C5.92075 13.4031 5.5738 13.7501 5.14581 13.7501C4.71783 13.7501 4.37088 13.4031 4.37088 12.9751V7.8743C4.37088 7.44632 4.71783 7.09937 5.14581 7.09937ZM1.02493 4.37085C0.59695 4.37085 0.25 4.7178 0.25 5.14578C0.25 5.57377 0.59695 5.92072 1.02493 5.92072H6.12566C6.55365 5.92072 6.9006 5.57377 6.9006 5.14578C6.9006 4.7178 6.55365 4.37085 6.12566 4.37085H1.02493ZM7.09929 8.85501C7.09929 8.42703 7.44624 8.08008 7.87422 8.08008H12.9752C13.4032 8.08008 13.7501 8.42703 13.7501 8.85501C13.7501 9.283 13.4032 9.62995 12.9752 9.62995H7.87422C7.44624 9.62995 7.09929 9.283 7.09929 8.85501ZM5.00006 2C5.00006 1.44772 5.44778 1 6.00006 1C6.55235 1 7.00006 1.44772 7.00006 2C7.00006 2.55228 6.55235 3 6.00006 3C5.44778 3 5.00006 2.55228 5.00006 2ZM11.0001 6C11.0001 5.44772 11.4478 5 12.0001 5C12.5523 5 13.0001 5.44772 13.0001 6C13.0001 6.55228 12.5523 7 12.0001 7C11.4478 7 11.0001 6.55228 11.0001 6ZM8.00006 11C7.44778 11 7.00006 11.4477 7.00006 12C7.00006 12.5523 7.44778 13 8.00006 13C8.55235 13 9.00006 12.5523 9.00006 12C9.00006 11.4477 8.55235 11 8.00006 11ZM1.00006 8C1.00006 7.44772 1.44778 7 2.00006 7C2.55235 7 3.00006 7.44772 3.00006 8C3.00006 8.55228 2.55235 9 2.00006 9C1.44778 9 1.00006 8.55228 1.00006 8Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187949\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"spotify\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187955)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.5 7C0.5 3.41015 3.41015 0.5 7 0.5C10.5899 0.5 13.5 3.41015 13.5 7C13.5 10.5899 10.5899 13.5 7 13.5C3.41015 13.5 0.5 10.5899 0.5 7ZM5.15875 9.59397C5.47994 9.51875 5.81531 9.47882 6.1608 9.47882C6.83043 9.47882 7.46327 9.62883 8.02909 9.89649C8.34111 10.0441 8.71372 9.91081 8.86132 9.59879C9.00893 9.28676 8.87564 8.91416 8.56361 8.76655C7.83421 8.4215 7.01914 8.22882 6.1608 8.22882C5.71866 8.22882 5.28767 8.27995 4.87372 8.3769C4.53764 8.4556 4.32899 8.79186 4.4077 9.12794C4.48641 9.46403 4.82266 9.67268 5.15875 9.59397ZM6.16085 7.47882C5.65488 7.47882 5.16344 7.53764 4.69271 7.64855C4.35673 7.72771 4.02019 7.51952 3.94103 7.18354C3.86188 6.84756 4.07007 6.51103 4.40605 6.43187C4.97005 6.29898 5.55766 6.22882 6.16085 6.22882C7.39938 6.22882 8.57063 6.52458 9.60615 7.04989C9.91398 7.20605 10.0369 7.58219 9.88078 7.89002C9.72462 8.19786 9.34847 8.32081 9.04064 8.16465C8.17635 7.72621 7.1984 7.47882 6.16085 7.47882ZM4.22 5.70471C4.84229 5.5571 5.49203 5.47882 6.16086 5.47882C7.57635 5.47882 8.90811 5.82948 10.0758 6.44814C10.3808 6.60974 10.7591 6.49348 10.9207 6.18847C11.0823 5.88345 10.966 5.50519 10.661 5.34359C9.31741 4.63173 7.78526 4.22882 6.16086 4.22882C5.39416 4.22882 4.64762 4.31859 3.93149 4.48846C3.59563 4.56813 3.38795 4.90498 3.46762 5.24084C3.54729 5.5767 3.88414 5.78438 4.22 5.70471Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187955\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"telegram\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187946)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.25 7C0.25 3.27208 3.27208 0.25 7 0.25C10.7279 0.25 13.75 3.27208 13.75 7C13.75 10.7279 10.7279 13.75 7 13.75C3.27208 13.75 0.25 10.7279 0.25 7ZM9.25184 11.0643L10.2969 3.13171L2.13166 7.06705L4.54939 7.94317L7.23463 5.86744C7.45311 5.69855 7.76713 5.73875 7.93601 5.95723C8.1049 6.17571 8.0647 6.48973 7.84622 6.65861L5.61825 8.38087L5.61829 11.1069L7.30337 9.50304L9.25184 11.0643Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187946\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"tiktok\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path d=\\\"M9.42188 1.14062C9.42188 0.58834 8.97416 0.140625 8.42188 0.140625C7.86959 0.140625 7.42188 0.58834 7.42188 1.14062V9.7207C7.42188 10.7659 6.57454 11.6133 5.5293 11.6133C4.48406 11.6133 3.63672 10.7659 3.63672 9.7207C3.63672 8.67546 4.48405 7.82812 5.5293 7.82812C6.08158 7.82812 6.5293 7.38041 6.5293 6.82812C6.5293 6.27584 6.08158 5.82812 5.5293 5.82812C3.37949 5.82812 1.63672 7.57089 1.63672 9.7207C1.63672 11.8705 3.37949 13.6133 5.5293 13.6133C7.67911 13.6133 9.42188 11.8705 9.42188 9.7207V5.28256C10.3246 6.00023 11.4674 6.42898 12.7102 6.42898C13.2625 6.42898 13.7102 5.98126 13.7102 5.42898C13.7102 4.8767 13.2625 4.42898 12.7102 4.42898C10.8941 4.42898 9.42188 2.95673 9.42188 1.14062Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"tinder\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path d=\\\"M8.17695 0.476102C8.02544 0.36247 7.82273 0.344192 7.65334 0.428888C7.48395 0.513584 7.37695 0.686715 7.37695 0.876101C7.37695 2.52097 6.98108 4.67341 4.97085 5.53257C4.69905 5.13811 4.43945 4.47042 4.43945 3.5011C4.43945 3.32781 4.34972 3.16688 4.20231 3.07578C4.0549 2.98467 3.87083 2.97639 3.71584 3.05389C2.67011 3.57676 1.25195 5.06322 1.25195 7.8761C1.25195 9.76178 1.96405 11.2128 3.06039 12.1873C4.14753 13.1537 5.58291 13.6261 7.00195 13.6261C8.42098 13.6261 9.85637 13.1537 10.9435 12.1873C12.0398 11.2128 12.7519 9.76178 12.7519 7.8761C12.7519 4.10595 9.92086 1.78403 8.17695 0.476102Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"twitter\", \"keywords\": [ \"media\", \"twitter\", \"social\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.47405 1.1632C9.77618 0.827743 11.3476 1.2901 12.304 2.30864H13.3775C13.5496 2.30864 13.7095 2.3971 13.801 2.54283C13.8925 2.68855 13.9026 2.87107 13.8278 3.02602L12.7722 5.21251C13.0717 7.76022 12.0751 10.196 10.0704 11.636C8.0197 13.109 5.00285 13.4801 1.42568 12.0144C1.23741 11.9435 1.0748 11.8175 0.959229 11.6526C0.841356 11.4844 0.778123 11.284 0.778123 11.0786C0.778123 10.8732 0.841356 10.6728 0.959229 10.5047C1.0771 10.3365 1.2439 10.2087 1.43695 10.1386C1.45001 10.1339 1.46327 10.1297 1.47668 10.126C2.02683 9.97689 2.55282 9.75372 3.0406 9.46364C2.07413 8.88294 1.28472 8.03895 0.770145 7.02251C0.127689 5.75346 -0.0444883 4.29828 0.283414 2.91498C0.304264 2.7417 0.37015 2.57659 0.474878 2.43629C0.589208 2.28311 0.745042 2.16592 0.923922 2.09857C1.1028 2.03123 1.29724 2.01656 1.48419 2.05631C1.67097 2.09602 1.84248 2.18837 1.97843 2.32245C3.39089 3.71077 4.62674 4.28532 5.48241 4.52023C5.81414 4.61131 6.09188 4.6522 6.30434 4.66875C6.23852 3.78839 6.38841 3.05479 6.72023 2.47463C7.12459 1.76761 7.76707 1.34534 8.47405 1.1632Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"usb-drive\", \"keywords\": [ \"usb\", \"drive\", \"stick\", \"memory\", \"storage\", \"data\", \"connection\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4 0C3.72386 0 3.5 0.223858 3.5 0.5V3.75H10.5V0.5C10.5 0.223858 10.2761 0 10 0H4ZM2.5 6V9.5C2.5 11.9853 4.51472 14 7 14C9.48528 14 11.5 11.9853 11.5 9.5V6C11.5 5.44772 11.0523 5 10.5 5H3.5C2.94772 5 2.5 5.44772 2.5 6Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"virtual-reality\", \"keywords\": [ \"gaming\", \"virtual\", \"gear\", \"controller\", \"reality\", \"games\", \"headset\", \"technology\", \"vr\", \"eyewear\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187985)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.78919 1.08726C3.06045 0.636148 3.54831 0.360229 4.0747 0.360229L9.92675 0.360234C10.4526 0.360234 10.94 0.635564 11.2114 1.0859L13.797 5.37561H0.210632L2.78919 1.08726ZM0 7C0 6.72386 0.223858 6.5 0.5 6.5H13.5C13.7761 6.5 14 6.72386 14 7V12C14 12.8284 13.3284 13.5 12.5 13.5H9.48063C9.02495 13.5 8.59398 13.2929 8.30932 12.937L7.39043 11.7884C7.19027 11.5382 6.80973 11.5382 6.60957 11.7884L5.69068 12.937C5.40602 13.2929 4.97505 13.5 4.51938 13.5H1.5C0.671573 13.5 0 12.8284 0 12V7Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187985\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"voice-mail\", \"keywords\": [ \"mic\", \"audio\", \"mike\", \"music\", \"microphone\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.75002 0C5.96864 0 5.21927 0.310401 4.66675 0.862918C4.11423 1.41544 3.80383 2.16481 3.80383 2.94619V6.37085C3.80383 7.15223 4.11423 7.9016 4.66675 8.45412C5.21927 9.00663 5.96864 9.31704 6.75002 9.31704C7.5314 9.31704 8.28077 9.00663 8.83329 8.45412C9.38581 7.9016 9.69621 7.15223 9.69621 6.37085V2.94619C9.69621 2.16481 9.38581 1.41544 8.83329 0.862918C8.28077 0.310401 7.5314 0 6.75002 0ZM6 12.0075V13.2201C6 13.6343 6.33579 13.9701 6.75 13.9701C7.16421 13.9701 7.5 13.6343 7.5 13.2201V12.0075C10.2834 11.877 12.5 9.57899 12.5 6.76331C12.5 6.34909 12.1642 6.01331 11.75 6.01331C11.3358 6.01331 11 6.34909 11 6.76331C11 8.83437 9.32107 10.5133 7.25 10.5133H6.76367C6.75912 10.5132 6.75457 10.5132 6.75 10.5132C6.74543 10.5132 6.74088 10.5132 6.73633 10.5133H6.25C4.17893 10.5133 2.5 8.83437 2.5 6.76331C2.5 6.34909 2.16421 6.01331 1.75 6.01331C1.33579 6.01331 1 6.34909 1 6.76331C1 9.57899 3.21659 11.877 6 12.0075Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"voice-mail-off\", \"keywords\": [ \"mic\", \"audio\", \"mike\", \"music\", \"microphone\", \"mute\", \"off\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187991)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.91669 0.862918C5.46921 0.310401 6.21858 0 6.99996 0C7.78134 0 8.53071 0.310401 9.08323 0.862918C9.63575 1.41544 9.94615 2.16481 9.94615 2.94619V6.37085C9.94615 7.04984 9.71176 7.70466 9.2878 8.22715L10.3088 9.24809C10.8951 8.58598 11.25 7.71626 11.25 6.76331C11.25 6.34909 11.5858 6.01331 12 6.01331C12.4142 6.01331 12.75 6.34909 12.75 6.76331C12.75 8.13081 12.2266 9.37642 11.3707 10.3101L13.7803 12.7197C14.0732 13.0126 14.0732 13.4874 13.7803 13.7803C13.4874 14.0732 13.0126 14.0732 12.7197 13.7803L8.05933 9.11999C8.05852 9.1203 8.05772 9.1206 8.05692 9.12091L4.05377 5.11776V5.11443L0.21967 1.28033C-0.0732233 0.987437 -0.0732233 0.512563 0.21967 0.21967C0.512563 -0.0732233 0.987437 -0.0732233 1.28033 0.21967L4.05377 2.99311V2.94619C4.05377 2.16481 4.36417 1.41544 4.91669 0.862918ZM7.75 11.2632C7.75 10.849 7.41421 10.5132 7 10.5132L6.98633 10.5133H6.5C4.42893 10.5133 2.75 8.83437 2.75 6.76331C2.75 6.34909 2.41421 6.01331 2 6.01331C1.58579 6.01331 1.25 6.34909 1.25 6.76331C1.25 9.57899 3.46659 11.877 6.25 12.0075V13.2201C6.25 13.6343 6.58579 13.9701 7 13.9701C7.41421 13.9701 7.75 13.6343 7.75 13.2201V11.2632Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187991\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"VPN-connection\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187973)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1.53812C0 0.696591 0.694014 0 1.53623 0H12.4638C13.306 0 14 0.69659 14 1.53812V4.46371C14 8.72248 11.4311 12.394 7.57577 13.8884C7.23129 14.0497 6.81222 14.0195 6.46482 13.9035L6.45284 13.8993L6.44342 13.8958C2.57482 12.4052 0 8.63232 0 4.46371V1.53812ZM3.65545 5.1308C4.01637 4.86333 5.27972 4.22144 6.99761 4.22144C8.7155 4.22144 9.97886 4.86333 10.3398 5.1308C10.6171 5.33632 11.0085 5.27811 11.214 5.00078C11.4196 4.72346 11.3614 4.33203 11.084 4.12651C10.5164 3.70583 8.99401 2.97144 6.99761 2.97144C5.00122 2.97144 3.47886 3.70583 2.9112 4.12651C2.63387 4.33203 2.57566 4.72346 2.78118 5.00078C2.9867 5.27811 3.37813 5.33632 3.65545 5.1308ZM6.99758 6.80493C6.20195 6.80493 5.28515 7.18896 4.93419 7.38403C4.63248 7.55172 4.25196 7.44308 4.08426 7.14137C3.91657 6.83966 4.02521 6.45914 4.32692 6.29145C4.76497 6.04798 5.89959 5.55493 6.99758 5.55493C8.09557 5.55493 9.2302 6.04798 9.66824 6.29145C9.96995 6.45914 10.0786 6.83966 9.9109 7.14137C9.74321 7.44308 9.36269 7.55172 9.06098 7.38403C8.71001 7.18896 7.79322 6.80493 6.99758 6.80493ZM6.52604 9.62531C6.55379 9.60989 6.63259 9.57198 6.73436 9.53909C6.83817 9.50555 6.93116 9.48859 6.9976 9.48859C7.06405 9.48859 7.15704 9.50555 7.26085 9.53909C7.36262 9.57198 7.44142 9.60989 7.46916 9.62531C7.77087 9.793 8.15139 9.68436 8.31908 9.38265C8.48678 9.08094 8.37813 8.70042 8.07643 8.53273C7.90369 8.43672 7.45886 8.23859 6.9976 8.23859C6.53635 8.23859 6.09152 8.43672 5.91878 8.53273C5.61707 8.70042 5.50843 9.08094 5.67612 9.38265C5.84381 9.68436 6.22434 9.793 6.52604 9.62531Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187973\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"watch-1\", \"keywords\": [ \"device\", \"timepiece\", \"cirle\", \"electronics\", \"face\", \"blank\", \"watch\", \"smart\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.43934 0.43934C4.72064 0.158035 5.10218 0 5.5 0H8.5C8.89782 0 9.27936 0.158035 9.56066 0.43934C9.84196 0.720644 10 1.10217 10 1.5V2.69102C11.3601 3.63973 12.25 5.21594 12.25 7C12.25 8.78406 11.3601 10.3603 10 11.309V12.5C10 12.8978 9.84196 13.2794 9.56066 13.5607C9.27936 13.842 8.89783 14 8.5 14H5.5C5.10217 14 4.72064 13.842 4.43934 13.5607C4.15804 13.2794 4 12.8978 4 12.5V11.309C2.63989 10.3603 1.75 8.78406 1.75 7C1.75 5.21594 2.63989 3.63973 4 2.69102V1.5C4 1.10217 4.15804 0.720644 4.43934 0.43934ZM7 3.25C4.92893 3.25 3.25 4.92893 3.25 7C3.25 9.07107 4.92893 10.75 7 10.75C9.07107 10.75 10.75 9.07107 10.75 7C10.75 4.92893 9.07107 3.25 7 3.25Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"watch-2\", \"keywords\": [ \"device\", \"square\", \"timepiece\", \"electronics\", \"face\", \"blank\", \"watch\", \"smart\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.43934 0.43934C4.72064 0.158035 5.10218 0 5.5 0H8.5C8.89782 0 9.27936 0.158035 9.56066 0.43934C9.84196 0.720644 10 1.10217 10 1.5V1.75H10.5C11.4665 1.75 12.25 2.5335 12.25 3.5V10.5C12.25 11.4665 11.4665 12.25 10.5 12.25H10V12.5C10 12.8978 9.84196 13.2794 9.56066 13.5607C9.27936 13.842 8.89783 14 8.5 14H5.5C5.10217 14 4.72064 13.842 4.43934 13.5607C4.15804 13.2794 4 12.8978 4 12.5V12.25H3.5C2.5335 12.25 1.75 11.4665 1.75 10.5V3.5C1.75 2.5335 2.5335 1.75 3.5 1.75H4V1.5C4 1.10217 4.15804 0.720644 4.43934 0.43934ZM3.25 3.5C3.25 3.36193 3.36193 3.25 3.5 3.25H10.5C10.6381 3.25 10.75 3.36193 10.75 3.5V10.5C10.75 10.6381 10.6381 10.75 10.5 10.75H3.5C3.36193 10.75 3.25 10.6381 3.25 10.5V3.5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"watch-circle-charging\", \"keywords\": [ \"device\", \"timepiece\", \"circle\", \"watch\", \"round\", \"charge\", \"charging\", \"power\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.5 0C5.10218 0 4.72064 0.158035 4.43934 0.43934C4.15804 0.720644 4 1.10217 4 1.5V2.69102C2.63989 3.63973 1.75 5.21594 1.75 7C1.75 8.78406 2.63989 10.3603 4 11.309V12.5C4 12.8978 4.15804 13.2794 4.43934 13.5607C4.72064 13.842 5.10217 14 5.5 14H8.5C8.89783 14 9.27936 13.842 9.56066 13.5607C9.84196 13.2794 10 12.8978 10 12.5V11.309C11.3601 10.3603 12.25 8.78406 12.25 7C12.25 5.21594 11.3601 3.63973 10 2.69102V1.5C10 1.10217 9.84196 0.720644 9.56066 0.43934C9.27936 0.158035 8.89782 0 8.5 0H5.5ZM3.25 7C3.25 4.92893 4.92893 3.25 7 3.25C9.07107 3.25 10.75 4.92893 10.75 7C10.75 9.07107 9.07107 10.75 7 10.75C4.92893 10.75 3.25 9.07107 3.25 7ZM7.99186 5.18677C8.19722 4.90932 8.13877 4.51793 7.86132 4.31258C7.58388 4.10722 7.19249 4.16567 6.98713 4.44312L5.32942 6.68281C5.18901 6.87252 5.16744 7.12515 5.27366 7.33592C5.37988 7.54668 5.59577 7.67964 5.83179 7.67964H6.93813L6.27074 8.8817C6.10319 9.18349 6.212 9.56396 6.51379 9.73151C6.81557 9.89906 7.19605 9.79025 7.3636 9.48846L8.54643 7.35802C8.65391 7.16443 8.65094 6.92843 8.53863 6.73761C8.42631 6.54679 8.22142 6.42964 8 6.42964H7.07195L7.99186 5.18677Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"watch-circle-heartbeat-monitor-1\", \"keywords\": [ \"device\", \"timepiece\", \"circle\", \"watch\", \"round\", \"heart\", \"beat\", \"monitor\", \"healthcare\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.5 0C5.10218 0 4.72064 0.158035 4.43934 0.43934C4.15804 0.720644 4 1.10217 4 1.5V2.69102C2.63989 3.63973 1.75 5.21594 1.75 7C1.75 8.78406 2.63989 10.3603 4 11.309V12.5C4 12.8978 4.15804 13.2794 4.43934 13.5607C4.72064 13.842 5.10217 14 5.5 14H8.5C8.89783 14 9.27936 13.842 9.56066 13.5607C9.84196 13.2794 10 12.8978 10 12.5V11.309C11.3601 10.3603 12.25 8.78406 12.25 7C12.25 5.21594 11.3601 3.63973 10 2.69102V1.5C10 1.10217 9.84196 0.720644 9.56066 0.43934C9.27936 0.158035 8.89782 0 8.5 0H5.5ZM3.25 7C3.25 4.92893 4.92893 3.25 7 3.25C9.07107 3.25 10.75 4.92893 10.75 7C10.75 9.07107 9.07107 10.75 7 10.75C4.92893 10.75 3.25 9.07107 3.25 7ZM4.88131 7.34917L6.51486 8.92585C6.78876 9.19021 7.21297 9.19021 7.48687 8.92585L9.12042 7.34917C10.2101 6.28763 8.78308 3.93543 7.00086 5.67165C5.21551 3.93237 3.78848 6.28454 4.88131 7.34917Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"watch-circle-heartbeat-monitor-2\", \"keywords\": [ \"device\", \"timepiece\", \"circle\", \"watch\", \"round\", \"heart\", \"beat\", \"monitor\", \"healthcare\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.5 0C5.10218 0 4.72064 0.158035 4.43934 0.43934C4.15804 0.720644 4 1.10217 4 1.5V2.69102C2.63989 3.63973 1.75 5.21594 1.75 7C1.75 8.78406 2.63989 10.3603 4 11.309V12.5C4 12.8978 4.15804 13.2794 4.43934 13.5607C4.72064 13.842 5.10217 14 5.5 14H8.5C8.89783 14 9.27936 13.842 9.56066 13.5607C9.84196 13.2794 10 12.8978 10 12.5V11.309C11.3601 10.3603 12.25 8.78406 12.25 7C12.25 5.21594 11.3601 3.63973 10 2.69102V1.5C10 1.10217 9.84196 0.720644 9.56066 0.43934C9.27936 0.158035 8.89782 0 8.5 0H5.5ZM3.25 7C3.25 4.92893 4.92893 3.25 7 3.25C9.07107 3.25 10.75 4.92893 10.75 7C10.75 9.07107 9.07107 10.75 7 10.75C4.92893 10.75 3.25 9.07107 3.25 7ZM7.0688 5.43213C6.98347 5.20999 6.77939 5.05558 6.54242 5.03386C6.30544 5.01214 6.07669 5.12688 5.9524 5.3298L5.30739 6.38288H4.81934C4.47416 6.38288 4.19434 6.6627 4.19434 7.00788C4.19434 7.35306 4.47416 7.63288 4.81934 7.63288H5.6575C5.875 7.63288 6.07687 7.5198 6.19047 7.33432L6.3564 7.06342L6.96635 8.65121C7.05316 8.8772 7.26267 9.03273 7.50412 9.05042C7.74556 9.0681 7.9755 8.94477 8.09432 8.73384L8.71451 7.63288H9.346C9.69117 7.63288 9.971 7.35306 9.971 7.00788C9.971 6.6627 9.69117 6.38288 9.346 6.38288H8.34924C8.12358 6.38288 7.91545 6.50452 7.8047 6.70113L7.657 6.96331L7.0688 5.43213Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"watch-circle-menu\", \"keywords\": [ \"device\", \"timepiece\", \"circle\", \"watch\", \"round\", \"menu\", \"list\", \"option\", \"app\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.5 0C5.10218 0 4.72064 0.158035 4.43934 0.43934C4.15804 0.720644 4 1.10217 4 1.5V2.69102C2.63989 3.63973 1.75 5.21594 1.75 7C1.75 8.78406 2.63989 10.3603 4 11.309V12.5C4 12.8978 4.15804 13.2794 4.43934 13.5607C4.72064 13.842 5.10217 14 5.5 14H8.5C8.89783 14 9.27936 13.842 9.56066 13.5607C9.84196 13.2794 10 12.8978 10 12.5V11.309C11.3601 10.3603 12.25 8.78406 12.25 7C12.25 5.21594 11.3601 3.63973 10 2.69102V1.5C10 1.10217 9.84196 0.720644 9.56066 0.43934C9.27936 0.158035 8.89782 0 8.5 0H5.5ZM3.25 7C3.25 4.92893 4.92893 3.25 7 3.25C9.07107 3.25 10.75 4.92893 10.75 7C10.75 9.07107 9.07107 10.75 7 10.75C4.92893 10.75 3.25 9.07107 3.25 7ZM5.1167 6.61456C5.53091 6.61456 5.8667 6.27878 5.8667 5.86456C5.8667 5.45035 5.53091 5.11456 5.1167 5.11456C4.70249 5.11456 4.3667 5.45035 4.3667 5.86456C4.3667 6.27878 4.70249 6.61456 5.1167 6.61456ZM6.61548 5.86456C6.61548 5.51938 6.8953 5.23956 7.24048 5.23956H8.9909C9.33608 5.23956 9.6159 5.51938 9.6159 5.86456C9.6159 6.20974 9.33608 6.48956 8.9909 6.48956H7.24048C6.8953 6.48956 6.61548 6.20974 6.61548 5.86456ZM6.61548 8.13544C6.61548 7.79026 6.8953 7.51044 7.24048 7.51044H8.9909C9.33608 7.51044 9.6159 7.79026 9.6159 8.13544C9.6159 8.48061 9.33608 8.76044 8.9909 8.76044H7.24048C6.8953 8.76044 6.61548 8.48061 6.61548 8.13544ZM5.8667 8.13544C5.8667 8.54965 5.53091 8.88544 5.1167 8.88544C4.70249 8.88544 4.3667 8.54965 4.3667 8.13544C4.3667 7.72122 4.70249 7.38544 5.1167 7.38544C5.53091 7.38544 5.8667 7.72122 5.8667 8.13544Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"watch-circle-time\", \"keywords\": [ \"device\", \"timepiece\", \"circle\", \"watch\", \"round\", \"time\", \"clock\", \"analog\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.5 0C5.10218 0 4.72064 0.158035 4.43934 0.43934C4.15804 0.720644 4 1.10217 4 1.5V2.69102C2.63989 3.63973 1.75 5.21594 1.75 7C1.75 8.78406 2.63989 10.3603 4 11.309V12.5C4 12.8978 4.15804 13.2794 4.43934 13.5607C4.72064 13.842 5.10217 14 5.5 14H8.5C8.89783 14 9.27936 13.842 9.56066 13.5607C9.84196 13.2794 10 12.8978 10 12.5V11.309C11.3601 10.3603 12.25 8.78406 12.25 7C12.25 5.21594 11.3601 3.63973 10 2.69102V1.5C10 1.10217 9.84196 0.720644 9.56066 0.43934C9.27936 0.158035 8.89782 0 8.5 0H5.5ZM3.25 7C3.25 4.92893 4.92893 3.25 7 3.25C9.07107 3.25 10.75 4.92893 10.75 7C10.75 9.07107 9.07107 10.75 7 10.75C4.92893 10.75 3.25 9.07107 3.25 7ZM7.6875 5.04376C7.6875 4.69858 7.40768 4.41876 7.0625 4.41876C6.71732 4.41876 6.4375 4.69858 6.4375 5.04376V6.95626C6.4375 7.12202 6.50335 7.28099 6.62056 7.3982L7.74556 8.5232C7.98964 8.76728 8.38536 8.76728 8.62944 8.5232C8.87352 8.27913 8.87352 7.8834 8.62944 7.63932L7.6875 6.69738V5.04376Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"webcam\", \"keywords\": [ \"webcam\", \"camera\", \"future\", \"tech\", \"chat\", \"skype\", \"technology\", \"video\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188006)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.5 6.52002C0.5 2.93017 3.41015 0.0200195 7 0.0200195C10.5899 0.0200195 13.5 2.93017 13.5 6.52002C13.5 8.38627 12.7135 10.0688 11.4539 11.2543C12.0632 11.6446 12.6193 12.1154 13.1067 12.6552C13.3843 12.9626 13.3601 13.4369 13.0526 13.7145C12.7452 13.992 12.2709 13.9678 11.9933 13.6604C11.462 13.0719 10.8304 12.5839 10.129 12.2187C9.2008 12.7295 8.13433 13.02 7 13.02C5.86567 13.02 4.79921 12.7295 3.87098 12.2187C3.16963 12.5839 2.53805 13.0719 2.00669 13.6604C1.72911 13.9678 1.25485 13.992 0.947407 13.7145C0.639963 13.4369 0.615755 12.9626 0.893337 12.6552C1.3807 12.1154 1.93685 11.6446 2.54615 11.2543C1.28651 10.0688 0.5 8.38628 0.5 6.52002ZM7 3.27002C5.20507 3.27002 3.75 4.72509 3.75 6.52002C3.75 8.31495 5.20507 9.77002 7 9.77002C8.79493 9.77002 10.25 8.31495 10.25 6.52002C10.25 4.72509 8.79493 3.27002 7 3.27002ZM7 5.52002C6.44772 5.52002 6 5.96773 6 6.52002C6 7.0723 6.44772 7.52002 7 7.52002C7.55228 7.52002 8 7.0723 8 6.52002C8 5.96773 7.55228 5.52002 7 5.52002Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188006\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"webcam-video\", \"keywords\": [ \"work\", \"video\", \"meeting\", \"camera\", \"company\", \"conference\", \"office\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 2C0.671573 2 0 2.67157 0 3.5V10.5C0 11.3284 0.671573 12 1.5 12H9C9.82843 12 10.5 11.3284 10.5 10.5V9.75827L12.6061 10.6609C13.2659 10.9437 14 10.4596 14 9.74173V4.25827C14 3.54035 13.2659 3.05632 12.6061 3.33912L10.5 4.24173V3.5C10.5 2.67157 9.82843 2 9 2H1.5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"webcam-video-circle\", \"keywords\": [ \"work\", \"video\", \"meeting\", \"camera\", \"company\", \"conference\", \"office\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188000)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7ZM8.61586 5.92278L10.1348 5.24951C10.1757 5.23371 10.2199 5.22815 10.2635 5.23333C10.3071 5.23851 10.3487 5.25426 10.3849 5.27922C10.421 5.30417 10.4504 5.33758 10.4707 5.37652C10.4909 5.41547 10.5013 5.45877 10.501 5.50266V8.49739C10.5013 8.54128 10.4909 8.58459 10.4707 8.62353C10.4504 8.66247 10.421 8.69588 10.3849 8.72083C10.3487 8.74579 10.3071 8.76154 10.2635 8.76672C10.2199 8.7719 10.1757 8.76635 10.1348 8.75054L8.61586 8.07727V8.8852C8.61586 9.02805 8.55911 9.16505 8.4581 9.26606C8.35709 9.36707 8.22009 9.42382 8.07724 9.42382H4.03758C3.89473 9.42382 3.75773 9.36707 3.65672 9.26606C3.55571 9.16505 3.49896 9.02805 3.49896 8.8852V5.11485C3.49896 4.972 3.55571 4.835 3.65672 4.73399C3.75773 4.63298 3.89473 4.57623 4.03758 4.57623H8.07724C8.22009 4.57623 8.35709 4.63298 8.4581 4.73399C8.55911 4.835 8.61586 4.972 8.61586 5.11485V5.92278Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188000\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"webcam-video-off\", \"keywords\": [ \"work\", \"video\", \"meeting\", \"camera\", \"company\", \"conference\", \"office\", \"off\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188003)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.21967 1.28033C-0.0732231 0.987437 -0.0732231 0.512563 0.21967 0.21967C0.512563 -0.0732233 0.987437 -0.0732233 1.28033 0.21967L3.06066 2H9C9.82843 2 10.5 2.67157 10.5 3.5V4.24173L12.6061 3.33912C13.2659 3.05632 14 3.54035 14 4.25827V9.74173C14 10.4596 13.2659 10.9437 12.6061 10.6609L11.0581 9.99747L13.7803 12.7197C14.0732 13.0126 14.0732 13.4874 13.7803 13.7803C13.4874 14.0732 13.0126 14.0732 12.7197 13.7803L0.21967 1.28033ZM0 3.5C0 3.30075 0.0388484 3.11058 0.109384 2.93664L9.16389 11.9911C9.11007 11.997 9.05538 12 9 12H1.5C0.671573 12 0 11.3284 0 10.5V3.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188003\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"whatsapp\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188012)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 0.379396C10.6134 0.377342 13.627 3.39621 13.627 7.00258C13.627 10.6134 10.6137 13.6232 6.99982 13.6208C6.05839 13.6202 5.11573 13.4218 4.24666 13.0246L0.965928 13.6178C0.788843 13.6498 0.608212 13.5842 0.492959 13.446C0.377706 13.3078 0.345614 13.1183 0.408928 12.9499L1.34789 10.4517C0.715752 9.41468 0.378871 8.21589 0.378865 7.00258C0.378848 3.39444 3.38884 0.3806 7 0.379396ZM10.3247 9.81587C9.58741 10.4122 8.44683 11.0139 7.3562 10.3138C5.92563 9.39548 4.6597 8.18769 3.74127 6.7572C3.20283 5.91857 3.70954 4.57641 4.46759 3.75857C4.77023 3.43205 5.26791 3.48368 5.5967 3.78384L6.28889 4.41577C6.49942 4.60797 6.50683 4.94203 6.37557 5.19508C6.16861 5.59409 6.04689 6.12908 6.3028 6.52775C6.62431 7.0286 7.02107 7.52118 7.96637 7.80888C8.25233 7.89591 8.61996 7.85838 8.93144 7.73219C9.19306 7.6262 9.52387 7.62743 9.71713 7.83316L10.3606 8.51811C10.7154 8.89588 10.7277 9.48992 10.3247 9.81587Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188012\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"wifi\", \"keywords\": [ \"wireless\", \"wifi\", \"internet\", \"server\", \"network\", \"connection\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.91603 3.38108C4.8937 2.97597 5.94162 2.76746 6.9999 2.76746C8.05819 2.76746 9.1061 2.97597 10.0838 3.38108C11.0615 3.7862 11.9497 4.37998 12.6979 5.12849C12.9907 5.42146 13.4656 5.42158 13.7585 5.12877C14.0515 4.83595 14.0516 4.36108 13.7588 4.06811C12.8714 3.18022 11.8177 2.47589 10.658 1.99534C9.49827 1.51479 8.25523 1.26746 6.9999 1.26746C5.74457 1.26746 4.50154 1.51479 3.34182 1.99534C2.18211 2.47589 1.12844 3.18022 0.241017 4.06811C-0.0518006 4.36108 -0.0516772 4.83595 0.241292 5.12877C0.534262 5.42158 1.00914 5.42146 1.30195 5.12849C2.05008 4.37998 2.93835 3.7862 3.91603 3.38108ZM6.99993 5.60968C6.2715 5.60968 5.55026 5.75369 4.87768 6.03343C4.2051 6.31316 3.59446 6.72311 3.08088 7.23969C2.78884 7.53343 2.31397 7.53481 2.02023 7.24277C1.72648 6.95073 1.7251 6.47586 2.01714 6.18211C2.67012 5.52533 3.44651 5.00411 4.30164 4.64844C5.15677 4.29278 6.07379 4.10968 6.99993 4.10968C7.92608 4.10968 8.8431 4.29278 9.69823 4.64844C10.5534 5.00411 11.3298 5.52533 11.9827 6.18211C12.2748 6.47586 12.2734 6.95073 11.9797 7.24277C11.6859 7.53481 11.211 7.53343 10.919 7.23969C10.4054 6.72311 9.79477 6.31316 9.12219 6.03343C8.44961 5.75369 7.72837 5.60968 6.99993 5.60968ZM6.0202 8.54883C6.33936 8.41417 6.68226 8.34479 7.02866 8.34479C7.37507 8.34479 7.71797 8.41417 8.03713 8.54883C8.35629 8.68349 8.64525 8.88071 8.88696 9.12885C9.17598 9.42557 9.65081 9.43181 9.94753 9.14278C10.2443 8.85376 10.2505 8.37893 9.96146 8.08222C9.57999 7.69059 9.12395 7.37934 8.62024 7.16681C8.11654 6.95428 7.57537 6.84479 7.02866 6.84479C6.48196 6.84479 5.94079 6.95428 5.43708 7.16681C4.93338 7.37934 4.47734 7.69059 4.09587 8.08222C3.80685 8.37893 3.81309 8.85376 4.1098 9.14278C4.40651 9.43181 4.88135 9.42557 5.17037 9.12885C5.41208 8.88071 5.70104 8.68349 6.0202 8.54883ZM5.40766 11.7133C5.40766 10.8339 6.12055 10.121 6.99996 10.121C7.87936 10.121 8.59226 10.8339 8.59226 11.7133C8.59226 12.5927 7.87936 13.3056 6.99996 13.3056C6.12055 13.3056 5.40766 12.5927 5.40766 11.7133Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"wifi-antenna\", \"keywords\": [ \"wireless\", \"wifi\", \"internet\", \"server\", \"network\", \"antenna\", \"connection\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188015)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.63 0.246946C11.3125 -0.0190168 10.8394 0.0228008 10.5735 0.340348C10.3075 0.657897 10.3493 1.13092 10.6669 1.39689C11.1965 1.8405 11.6428 2.43601 11.9607 3.14325C12.2764 3.84555 12.4536 4.6322 12.474 5.43997C12.4534 6.24844 12.2754 7.03559 11.9586 7.73786C11.6395 8.44504 11.1918 9.03979 10.6608 9.48186C10.3425 9.7469 10.2993 10.2198 10.5643 10.5381C10.8294 10.8564 11.3023 10.8996 11.6206 10.6346C12.3402 10.0354 12.9211 9.25188 13.3258 8.35472C13.7306 7.45756 13.9511 6.46536 13.974 5.45716C13.9743 5.44584 13.9743 5.43452 13.974 5.4232C13.9514 4.41619 13.732 3.42504 13.3289 2.52828C12.9258 1.63153 12.3471 0.847593 11.63 0.246946ZM5.35063 8.66404C5.08702 8.98354 4.61431 9.02885 4.29481 8.76523C3.8234 8.37629 3.44492 7.87194 3.18065 7.29892C2.9164 6.72594 2.7707 6.09413 2.75069 5.45245C2.75022 5.43735 2.75021 5.42224 2.75065 5.40714C2.76947 4.7639 2.91459 4.1303 3.17887 3.55563C3.44317 2.98094 3.82227 2.4751 4.29475 2.08518C4.61422 1.82153 5.08693 1.86679 5.35058 2.18626C5.61423 2.50574 5.56897 2.97845 5.2495 3.2421C4.9641 3.47762 4.71884 3.79712 4.54166 4.18237C4.36746 4.56116 4.26657 4.98765 4.25075 5.42836C4.26738 5.86795 4.36863 6.29314 4.54277 6.67073C4.71989 7.05477 4.96471 7.37329 5.24944 7.60822C5.56894 7.87183 5.61424 8.34454 5.35063 8.66404ZM8.62355 2.21612C8.88716 1.89662 9.35987 1.85132 9.67937 2.11493C10.1508 2.50388 10.5293 3.00822 10.7935 3.58124C11.0578 4.15422 11.2035 4.78603 11.2235 5.42771C11.224 5.44281 11.224 5.45792 11.2235 5.47302C11.2047 6.11626 11.0596 6.74986 10.7953 7.32453C10.531 7.89922 10.1519 8.40506 9.67943 8.79498C9.35996 9.05863 8.88724 9.01337 8.6236 8.6939C8.35995 8.37442 8.40521 7.90171 8.72468 7.63807C9.01007 7.40254 9.25534 7.08305 9.43251 6.6978C9.60672 6.319 9.7076 5.89252 9.72343 5.45181C9.7068 5.01221 9.60554 4.58702 9.4314 4.20943C9.25429 3.82539 9.00947 3.50687 8.72474 3.27194C8.40524 3.00833 8.35993 2.53562 8.62355 2.21612ZM3.40078 10.5398C3.13481 10.8573 2.66179 10.8991 2.34424 10.6332C1.62709 10.0325 1.04843 9.24863 0.645355 8.35188C0.24227 7.45512 0.0228566 6.46397 0.000189966 5.45696C-6.49029e-05 5.44565 -6.3233e-05 5.43432 0.000194498 5.423C0.023165 4.4148 0.243633 3.4226 0.648396 2.52544C1.05316 1.62829 1.63405 0.844701 2.35363 0.245563C2.67195 -0.0194769 3.14485 0.023715 3.40989 0.342034C3.67493 0.660353 3.63174 1.13326 3.31342 1.3983C2.78248 1.84037 2.33474 2.43512 2.01569 3.14231C1.69885 3.84457 1.52085 4.63172 1.50022 5.44019C1.5206 6.24796 1.69782 7.03461 2.0135 7.73692C2.33139 8.44416 2.77772 9.03966 3.30738 9.48327C3.62492 9.74924 3.66674 10.2222 3.40078 10.5398ZM7.00003 3.87648C6.10338 3.87648 5.37649 4.60337 5.37649 5.50002C5.37649 6.12702 5.73192 6.67101 6.25228 6.94149C6.25079 6.96081 6.25003 6.98032 6.25003 7.00002V13.1485C6.25003 13.5627 6.58581 13.8985 7.00003 13.8985C7.41424 13.8985 7.75003 13.5627 7.75003 13.1485V7.00002C7.75003 6.98033 7.74927 6.96081 7.74778 6.9415C8.26815 6.67102 8.62358 6.12703 8.62358 5.50002C8.62358 4.60337 7.89669 3.87648 7.00003 3.87648Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188015\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"wifi-disabled\", \"keywords\": [ \"wireless\", \"wifi\", \"internet\", \"server\", \"network\", \"disabled\", \"off\", \"offline\", \"connection\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188018)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.219669 0.21906C-0.0732231 0.511953 -0.0732231 0.986826 0.219669 1.27972L0.397768 1.45782C0.415456 1.47202 0.432551 1.48733 0.44896 1.50374L12.7769 13.8317C13.0715 14.0729 13.5068 14.056 13.7817 13.7811C14.0746 13.4882 14.0746 13.0133 13.7817 12.7205L7.28974 6.22847C7.90723 6.2609 8.51535 6.39877 9.08757 6.63676C9.74747 6.91123 10.3466 7.31344 10.8505 7.82027C11.1426 8.11402 11.6174 8.1154 11.9112 7.82336C12.2049 7.53132 12.2063 7.05645 11.9143 6.7627C11.271 6.11566 10.5061 5.60217 9.66361 5.25178C8.82116 4.90139 7.91775 4.72101 7.00533 4.72101C6.55159 4.72101 6.16848 4.76428 5.89561 4.80829L5.87321 4.81194L5.05503 3.99376C5.68949 3.83277 6.34293 3.75079 7.00044 3.75079C8.03952 3.75079 9.06842 3.95552 10.0284 4.35328C10.9883 4.75105 11.8605 5.33405 12.595 6.06898C12.8878 6.36194 13.3627 6.36207 13.6557 6.06925C13.9486 5.77643 13.9488 5.30156 13.6559 5.00859C12.7821 4.13429 11.7445 3.44073 10.6026 2.96754C9.46058 2.49435 8.23657 2.25079 7.00044 2.25079C5.92717 2.25079 4.86302 2.4344 3.85378 2.79251L1.28033 0.21906C0.987436 -0.0738338 0.512562 -0.0738338 0.219669 0.21906ZM4.91497 7.73751L6.02461 8.84715C5.71922 8.97871 5.44259 9.16902 5.21048 9.4073C4.92146 9.70402 4.44663 9.71025 4.14991 9.42123C3.8532 9.13221 3.84696 8.65738 4.13598 8.36066C4.36944 8.12099 4.63122 7.91183 4.91497 7.73751ZM3.10449 5.92704L4.18895 7.0115C4.1888 7.01159 4.18911 7.01141 4.18895 7.0115L4.18788 7.01212L4.18653 7.01292L4.1728 7.02111C4.1595 7.02913 4.1382 7.04217 4.11011 7.05998C4.05384 7.09567 3.97098 7.15011 3.87123 7.22127C3.66993 7.36486 3.40888 7.57012 3.16018 7.82027C2.86814 8.11401 2.39327 8.11539 2.09952 7.82335C1.80578 7.53131 1.8044 7.05644 2.09644 6.7627C2.42133 6.43591 2.75256 6.17673 3.00014 6.00012C3.03664 5.97409 3.07151 5.94971 3.10449 5.92704ZM1.33435 4.1569L2.40494 5.22749C2.00932 5.52501 1.70084 5.77411 1.40609 6.069C1.11327 6.36197 0.638401 6.3621 0.345432 6.06928C0.0524631 5.77646 0.0523397 5.30159 0.345156 5.00862C0.663074 4.69054 0.985125 4.42455 1.33435 4.1569ZM8.31056 11.9394C8.31056 12.6629 7.72406 13.2494 7.00056 13.2494C6.27707 13.2494 5.69056 12.6629 5.69056 11.9394C5.69056 11.2159 6.27707 10.6294 7.00056 10.6294C7.72406 10.6294 8.31056 11.2159 8.31056 11.9394Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188018\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"wifi-horizontal\", \"keywords\": [ \"wireless\", \"wifi\", \"internet\", \"server\", \"network\", \"horizontal\", \"connection\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188021)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.81274 2.2317C4.13775 1.97491 4.19305 1.50327 3.93626 1.17826C3.67947 0.853247 3.20783 0.797946 2.88282 1.05474C2.01795 1.73807 1.31364 2.63064 0.819405 3.6575C0.325231 4.68424 0.0526525 5.82092 0.0198324 6.97876C0.0194336 6.99283 0.0194309 7.0069 0.0198241 7.02097C0.0522393 8.18052 0.325809 9.31884 0.822158 10.3462C1.31857 11.3738 2.02609 12.2659 2.89456 12.947C3.2205 13.2026 3.69194 13.1455 3.94754 12.8196C4.20314 12.4936 4.14612 12.0222 3.82018 11.7666C3.14082 11.2339 2.57477 10.5258 2.17281 9.69379C1.77375 8.86774 1.5492 7.94564 1.51986 7.00016C1.54954 6.0555 1.7734 5.13413 2.171 4.30803C2.5715 3.47591 3.13548 2.76681 3.81274 2.2317ZM5.9064 4.28601C6.24873 4.05281 6.33719 3.58625 6.10399 3.24392C5.87079 2.90159 5.40422 2.81312 5.0619 3.04633C4.41045 3.49011 3.87213 4.07557 3.49123 4.75823C3.11025 5.44102 2.8977 6.20153 2.87175 6.97972C2.8712 6.99639 2.8712 7.01308 2.87175 7.02975C2.89771 7.80694 3.11049 8.56639 3.49179 9.24785C3.87301 9.92917 4.41169 10.513 5.06329 10.9547C5.40615 11.1871 5.87251 11.0976 6.10494 10.7548C6.33737 10.4119 6.24785 9.94557 5.905 9.71314C5.44358 9.40033 5.0662 8.98972 4.80081 8.51541C4.53962 8.0486 4.39341 7.53186 4.37185 7.00469C4.39342 6.47592 4.53972 5.95759 4.80112 5.48911C5.06671 5.01312 5.44441 4.60073 5.9064 4.28601ZM7.89505 3.24392C8.12825 2.90159 8.59481 2.81312 8.93714 3.04633C9.58859 3.49011 10.1269 4.07557 10.5078 4.75823C10.8888 5.44102 11.1013 6.20153 11.1273 6.97972C11.1278 6.99639 11.1278 7.01308 11.1273 7.02975C11.1013 7.80694 10.8885 8.56639 10.5072 9.24785C10.126 9.92917 9.58735 10.513 8.93574 10.9547C8.59289 11.1871 8.12653 11.0976 7.8941 10.7548C7.66167 10.4119 7.75118 9.94557 8.09404 9.71314C8.55546 9.40033 8.93284 8.98972 9.19823 8.51541C9.45942 8.0486 9.60563 7.53186 9.62719 7.00469C9.60562 6.47592 9.45932 5.95759 9.19792 5.48911C8.93233 5.01312 8.55463 4.60073 8.09264 4.28601C7.75031 4.05281 7.66185 3.58625 7.89505 3.24392ZM11.1162 1.05474C10.7912 0.797946 10.3195 0.853247 10.0628 1.17826C9.80597 1.50327 9.86127 1.97491 10.1863 2.2317C10.8635 2.76681 11.4275 3.47591 11.828 4.30803C12.2256 5.13413 12.4495 6.0555 12.4792 7.00016C12.4498 7.94564 12.2253 8.86774 11.8262 9.69379C11.4242 10.5258 10.8582 11.2339 10.1788 11.7666C9.85291 12.0222 9.79588 12.4936 10.0515 12.8196C10.3071 13.1455 10.7785 13.2026 11.1045 12.947C11.9729 12.2659 12.6805 11.3738 13.1769 10.3462C13.6732 9.31884 13.9468 8.18052 13.9792 7.02097C13.9796 7.0069 13.9796 6.99283 13.9792 6.97876C13.9464 5.82092 13.6738 4.68424 13.1796 3.6575C12.6854 2.63064 11.9811 1.73807 11.1162 1.05474ZM5.74951 7.00003C5.74951 6.30967 6.30915 5.75003 6.99951 5.75003C7.68987 5.75003 8.24951 6.30967 8.24951 7.00003C8.24951 7.69039 7.68987 8.25003 6.99951 8.25003C6.30915 8.25003 5.74951 7.69039 5.74951 7.00003Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188021\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"wifi-router\", \"keywords\": [ \"wireless\", \"wifi\", \"internet\", \"server\", \"network\", \"connection\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188024)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.23297 2.75333C5.75596 1.23034 8.24385 1.23034 9.76684 2.75333C10.0597 3.04622 10.5346 3.04622 10.8275 2.75333C11.1204 2.46043 11.1204 1.98556 10.8275 1.69267C8.71872 -0.416111 5.28109 -0.416111 3.17231 1.69267C2.87941 1.98556 2.87941 2.46043 3.17231 2.75333C3.4652 3.04622 3.94007 3.04622 4.23297 2.75333ZM6.94965 6.26562C7.36386 6.26562 7.69965 5.92984 7.69965 5.51562C7.69965 5.10141 7.36386 4.76562 6.94965 4.76562C6.53543 4.76562 6.19965 5.10141 6.19965 5.51562C6.19965 5.92984 6.53543 6.26562 6.94965 6.26562ZM7.94541 4.2183C7.41769 3.69058 6.48579 3.69058 5.95806 4.2183C5.66517 4.5112 5.1903 4.5112 4.8974 4.2183C4.60451 3.92541 4.60451 3.45054 4.8974 3.15764C6.01091 2.04413 7.89256 2.04413 9.00607 3.15764C9.29896 3.45054 9.29896 3.92541 9.00607 4.2183C8.71318 4.5112 8.2383 4.5112 7.94541 4.2183ZM1.98914 2.98144C1.88016 2.58182 1.46785 2.34622 1.06823 2.4552C0.668613 2.56419 0.433009 2.9765 0.541996 3.37612L1.59851 7.25H3.1533L1.98914 2.98144ZM13.4579 3.37612L12.4014 7.25H10.8466L12.0107 2.98144C12.1197 2.58182 12.532 2.34622 12.9317 2.4552C13.3313 2.56419 13.5669 2.9765 13.4579 3.37612ZM2.427 13.2139V12.5H1.5C0.947715 12.5 0.5 12.0523 0.5 11.5V9.5C0.5 8.94772 0.947715 8.5 1.5 8.5H12.5C13.0523 8.5 13.5 8.94772 13.5 9.5V11.5C13.5 12.0523 13.0523 12.5 12.5 12.5H11.5728V13.2139C11.5728 13.6281 11.237 13.9639 10.8228 13.9639C10.4086 13.9639 10.0728 13.6281 10.0728 13.2139V12.5H3.927V13.2139C3.927 13.6281 3.59122 13.9639 3.177 13.9639C2.76279 13.9639 2.427 13.6281 2.427 13.2139ZM2.677 10.3467C2.677 10.0706 2.90086 9.84674 3.177 9.84674H3.65487C3.93101 9.84674 4.15487 10.0706 4.15487 10.3467C4.15487 10.6229 3.93101 10.8467 3.65487 10.8467H3.177C2.90086 10.8467 2.677 10.6229 2.677 10.3467ZM5.56641 9.84674C5.29026 9.84674 5.06641 10.0706 5.06641 10.3467C5.06641 10.6229 5.29026 10.8467 5.56641 10.8467H6.04427C6.32041 10.8467 6.54427 10.6229 6.54427 10.3467C6.54427 10.0706 6.32041 9.84674 6.04427 9.84674H5.56641Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188024\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"windows\", \"keywords\": [ \"os\", \"system\", \"microsoft\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188030)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.882877 1.50329L5.375 0.986699V6.13501H0V2.46002C0 2.44783 0.000446111 2.43564 0.00133747 2.42347C0.0183182 2.19178 0.115439 1.97323 0.276021 1.80534C0.436603 1.63746 0.650626 1.53073 0.881337 1.50347L0.882877 1.50329ZM0 7.38501V11.08L3.8445e-06 11.082C0.000928879 11.3183 0.0846618 11.5467 0.236634 11.7276C0.388607 11.9086 0.599171 12.0306 0.83174 12.0723L5.375 12.769V7.38501H0ZM6.625 7.38501V12.9606L12.8425 13.914C12.9841 13.9366 13.1289 13.9284 13.2671 13.8901C13.4061 13.8516 13.5352 13.7834 13.6454 13.6903C13.7556 13.5973 13.8443 13.4814 13.9055 13.3508C13.9667 13.2202 13.999 13.0778 14 12.9336V7.38501H6.625ZM14 6.13501V1.11209C14.001 0.97032 13.9716 0.829968 13.9136 0.700538C13.8553 0.570178 13.7694 0.453987 13.6619 0.359939C13.5545 0.26589 13.4279 0.196211 13.2909 0.155684C13.1567 0.11596 13.0156 0.105166 12.8769 0.123976L6.625 0.842949V6.13501H14Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188030\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" } ], \"culture\": [ { \"name\": \"christian-cross-1\", \"keywords\": [ \"religion\", \"christian\", \"cross\", \"culture\", \"bold\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5 0C4.72386 0 4.5 0.223858 4.5 0.5V3H2C1.72386 3 1.5 3.22386 1.5 3.5V7.5C1.5 7.77614 1.72386 8 2 8H4.5V13.5C4.5 13.7761 4.72386 14 5 14H9C9.27614 14 9.5 13.7761 9.5 13.5V8H12C12.2761 8 12.5 7.77614 12.5 7.5V3.5C12.5 3.22386 12.2761 3 12 3H9.5V0.5C9.5 0.223858 9.27614 0 9 0H5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"christian-cross-2\", \"keywords\": [ \"religion\", \"christian\", \"cross\", \"culture\", \"bold\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.00055 1.12439C8.00055 0.572106 7.55284 0.12439 7.00055 0.12439C6.44827 0.12439 6.00055 0.572104 6.00055 1.12439V3.74015H3.38477C2.83248 3.74015 2.38477 4.18787 2.38477 4.74015C2.38477 5.29244 2.83248 5.74015 3.38477 5.74015H6.00054L6.00053 12.9281C6.00053 13.4804 6.44825 13.9281 7.00053 13.9281C7.55282 13.9281 8.00053 13.4804 8.00053 12.9281L8.00054 5.74015H10.6162C11.1685 5.74015 11.6162 5.29244 11.6162 4.74015C11.6162 4.18787 11.1685 3.74015 10.6162 3.74015H8.00055V1.12439Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"christianity\", \"keywords\": [ \"religion\", \"jesus\", \"christianity\", \"christ\", \"fish\", \"culture\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path d=\\\"M13.5741 4.96817C13.4684 5.13318 13.3149 5.36569 13.1188 5.64329C12.8485 6.02579 12.4943 6.49829 12.0701 6.99999C12.4943 7.50169 12.8485 7.97419 13.1188 8.35669C13.3149 8.63429 13.4684 8.86681 13.5741 9.03181C13.6315 9.12149 13.689 9.21273 13.7434 9.30427C14.0241 9.77985 13.8662 10.393 13.3907 10.6738C12.8796 10.9756 12.2844 10.7553 11.9917 10.2725C11.9252 10.1642 11.75 9.88533 11.4853 9.51076C11.2709 9.20728 11 8.84401 10.682 8.45808C9.3742 9.66412 7.6254 10.8113 5.64593 10.8127C4.52535 10.8203 3.42667 10.5082 2.47055 9.91391C1.51372 9.31916 0.738173 8.46589 0.224901 7.45146C0.0812829 7.16761 0.0812829 6.83237 0.224901 6.54852C0.738173 5.53409 1.51372 4.68083 2.47055 4.08607C3.42668 3.49175 4.52537 3.17968 5.64596 3.18729C7.62542 3.18874 9.37422 4.33586 10.682 5.54191C11 5.15597 11.2709 4.7927 11.4853 4.48923C11.6618 4.23945 11.7984 4.03248 11.8897 3.88985C11.9344 3.81995 11.9787 3.7497 12.0214 3.6785C12.3022 3.20321 12.9152 3.04546 13.3907 3.32618C13.8618 3.60433 14.0199 4.22845 13.7417 4.69848C13.6874 4.78933 13.6311 4.87902 13.5741 4.96817Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"dhammajak\", \"keywords\": [ \"religion\", \"dhammajak\", \"culture\", \"bhuddhism\", \"buddish\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189854)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.37389 0.0893649C4.92541 0.21874 3.60511 0.7934 2.55084 1.67549L5.70503 4.82968C5.9108 4.70656 6.13557 4.61193 6.37389 4.55122V0.0893649ZM1.66785 2.56026C0.788776 3.61526 0.216961 4.93527 0.0898438 6.38287H4.55059C4.60987 6.14559 4.70275 5.92161 4.82388 5.71629L1.66785 2.56026ZM0.0916878 7.63287C0.222869 9.07746 0.796994 10.3941 1.67714 11.446L4.83227 8.29082C4.71095 8.08821 4.61726 7.86716 4.55638 7.63287H0.0916878ZM2.56196 12.3289C3.61439 13.2057 4.9305 13.7768 6.37389 13.9057V9.44177C6.14101 9.38245 5.92107 9.29073 5.71916 9.1717L2.56196 12.3289ZM7.62389 13.9057C9.06737 13.7768 10.3835 13.2057 11.436 12.3288L8.27866 9.17144C8.07674 9.29052 7.85679 9.3823 7.62389 9.44167V13.9057ZM12.3208 11.4458C13.2009 10.394 13.775 9.07741 13.9062 7.63287H9.44098C9.38015 7.86698 9.28655 8.08788 9.16536 8.29037L12.3208 11.4458ZM13.908 6.38287C13.7809 4.93542 13.2092 3.61552 12.3303 2.56057L9.17389 5.71697C9.29482 5.92211 9.38756 6.14585 9.44678 6.38287H13.908ZM11.4473 1.67574C10.393 0.79349 9.07255 0.218725 7.62389 0.0893555V4.55132C7.86232 4.6121 8.08718 4.70684 8.29301 4.83008L11.4473 1.67574ZM5.72545 6.9965C5.72545 6.29566 6.29168 5.72708 6.99162 5.72328L6.99889 5.72332L7.00602 5.72328C7.70583 5.72723 8.27191 6.29576 8.27191 6.9965C8.27191 7.69968 7.70187 8.26973 6.99868 8.26973C6.2955 8.26973 5.72545 7.69968 5.72545 6.9965Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189854\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"hexagram\", \"keywords\": [ \"star\", \"jew\", \"jewish\", \"judaism\", \"hexagram\", \"culture\", \"religion\", \"david\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189866)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.85643 0.564416C7.67568 0.263318 7.35024 0.0791016 6.99905 0.0791016C6.64787 0.0791016 6.32242 0.263318 6.14167 0.564416L4.66918 3.01734H1.65429C1.29398 3.01734 0.961533 3.21118 0.784043 3.52474C0.606552 3.8383 0.611471 4.22311 0.796918 4.53203L2.27841 6.99995L0.796918 9.46788C0.611471 9.7768 0.606552 10.1616 0.784043 10.4752C0.961533 10.7887 1.29398 10.9826 1.65429 10.9826H4.66918L6.14167 13.4355C6.32242 13.7366 6.64787 13.9208 6.99905 13.9208C7.35024 13.9208 7.67568 13.7366 7.85643 13.4355L9.32893 10.9826H12.3439C12.7042 10.9826 13.0366 10.7887 13.2141 10.4752C13.3916 10.1616 13.3867 9.7768 13.2012 9.46788L11.7197 6.99995L13.2012 4.53203C13.3867 4.22311 13.3916 3.8383 13.2141 3.52474C13.0366 3.21118 12.7042 3.01734 12.3439 3.01734H9.32893L7.85643 0.564416Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189866\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"hinduism\", \"keywords\": [ \"religion\", \"hinduism\", \"culture\", \"hindu\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189860)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.1123 2.54126C11.6646 2.54126 12.1123 2.09354 12.1123 1.54126C12.1123 0.988975 11.6646 0.54126 11.1123 0.54126C10.56 0.54126 10.1123 0.988975 10.1123 1.54126C10.1123 2.09354 10.56 2.54126 11.1123 2.54126ZM4.41997 2.05688C3.94028 2.05688 3.5001 2.21692 3.14697 2.48691C2.8179 2.73848 2.3472 2.67567 2.09563 2.34661C1.84405 2.01755 1.90686 1.54685 2.23592 1.29527C2.84151 0.832278 3.59965 0.556885 4.41997 0.556885C6.40739 0.556885 8.01851 2.16801 8.01851 4.15542C8.01851 4.72601 7.8853 5.26636 7.64849 5.74622C7.74274 5.81751 7.83409 5.89242 7.92234 5.97076C8.10815 5.89438 8.29769 5.83948 8.4747 5.80024C8.87978 5.71045 9.31474 5.68168 9.69565 5.71322C11.8611 5.89255 13.4712 7.79338 13.2919 9.95885C13.2238 10.7805 12.9069 11.5242 12.4215 12.1193C12.1596 12.4402 11.6872 12.4881 11.3662 12.2263C11.0453 11.9644 10.9974 11.492 11.2592 11.171C11.5591 10.8034 11.7547 10.3451 11.797 9.83505C11.9079 8.49519 10.9117 7.31907 9.57185 7.20811C9.37933 7.19216 9.14558 7.20222 8.91896 7.24121C9.24835 7.8663 9.4348 8.57842 9.4348 9.3341C9.4348 11.817 7.42199 13.8298 4.93908 13.8298C2.45616 13.8298 0.443359 11.817 0.443359 9.3341C0.443359 8.91988 0.779146 8.5841 1.19336 8.5841C1.60757 8.5841 1.94336 8.91988 1.94336 9.3341C1.94336 10.9886 3.28459 12.3298 4.93908 12.3298C6.59357 12.3298 7.9348 10.9886 7.9348 9.3341C7.9348 7.67961 6.59357 6.33838 4.93908 6.33838C4.52486 6.33838 4.18908 6.00259 4.18908 5.58838C4.18908 5.17417 4.52486 4.83838 4.93908 4.83838C5.41986 4.83838 5.88302 4.91385 6.31735 5.05359C6.44643 4.78152 6.51851 4.47729 6.51851 4.15542C6.51851 2.99643 5.57896 2.05688 4.41997 2.05688ZM13.337 4.16507C13.6298 3.87218 13.6298 3.3973 13.337 3.10441C13.0441 2.81152 12.5692 2.81152 12.2763 3.10441C11.6334 3.74726 10.5912 3.74726 9.94832 3.10441C9.65542 2.81152 9.18055 2.81152 8.88766 3.10441C8.59476 3.3973 8.59476 3.87218 8.88766 4.16507C10.1163 5.39371 12.1083 5.39371 13.337 4.16507Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189860\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"islam\", \"keywords\": [ \"religion\", \"islam\", \"moon\", \"crescent\", \"muslim\", \"culture\", \"star\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189845)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.32499 0.00291425C6.07391 -0.0331934 4.83604 0.266791 3.74029 0.871624C2.64454 1.47646 1.731 2.36402 1.09481 3.44186C0.458618 4.5197 0.123047 5.74841 0.123047 7C0.123047 8.25159 0.458618 9.4803 1.09481 10.5581C1.731 11.636 2.64454 12.5235 3.74029 13.1284C4.83604 13.7332 6.07391 14.0332 7.32499 13.9971C8.57606 13.961 9.79457 13.5901 10.8537 12.9231C11.0417 12.8046 11.129 12.5763 11.0679 12.3626C11.0069 12.149 10.8121 12.0012 10.5899 12C9.26853 11.9928 8.00375 11.4628 7.07195 10.5259C6.14016 9.58903 5.61712 8.32136 5.61714 6.99999C5.61712 5.67862 6.14016 4.41097 7.07195 3.47407C8.00375 2.53717 9.26853 2.0072 10.5899 1.99999C10.8121 1.99878 11.0069 1.85104 11.0679 1.63736C11.129 1.42369 11.0417 1.19536 10.8537 1.07692C9.79457 0.409901 8.57606 0.0390218 7.32499 0.00291425ZM12.1951 4.94407C12.11 4.77486 11.9367 4.66825 11.7473 4.66866C11.5579 4.66908 11.385 4.77647 11.3007 4.94605L10.8999 5.75196H10.1264C9.92548 5.75196 9.74407 5.87222 9.66584 6.05727C9.58761 6.24233 9.62775 6.45624 9.76774 6.60035L10.4028 7.25405L10.2328 8.1983C10.1983 8.39036 10.2786 8.58497 10.4386 8.69674C10.5985 8.80851 10.8089 8.817 10.9773 8.71849L11.754 8.26428L12.5758 8.72338C12.7473 8.81917 12.959 8.8057 13.1169 8.68895C13.2749 8.5722 13.3498 8.3738 13.3085 8.18178L13.1074 7.24635L13.7349 6.60035C13.8749 6.45624 13.9151 6.24233 13.8368 6.05727C13.7586 5.87222 13.5772 5.75196 13.3763 5.75196H12.6013L12.1951 4.94407Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189845\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"news-paper\", \"keywords\": [ \"newspaper\", \"periodical\", \"fold\", \"content\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1 1.25C0.734784 1.25 0.48043 1.35536 0.292893 1.54289C0.105357 1.73043 0 1.98478 0 2.25V11.25C0 11.6478 0.158035 12.0294 0.43934 12.3107C0.720644 12.592 1.10217 12.75 1.5 12.75H12.25C12.7141 12.75 13.1592 12.5656 13.4874 12.2374C13.8156 11.9092 14 11.4641 14 11V4.5C14 4.22386 13.7761 4 13.5 4H12.519V10.459C12.519 10.8042 12.2392 11.084 11.894 11.084C11.5488 11.084 11.269 10.8042 11.269 10.459V3.74341C11.269 3.73309 11.2692 3.72284 11.2697 3.71265V2.25C11.2697 1.98478 11.1644 1.73043 10.9768 1.54289C10.7893 1.35536 10.5349 1.25 10.2697 1.25H1ZM2.78653 4.03653C2.78653 3.76039 3.01039 3.53653 3.28653 3.53653H7.78653C8.06267 3.53653 8.28653 3.76039 8.28653 4.03653V6.53653C8.28653 6.81267 8.06267 7.03653 7.78653 7.03653H3.28653C3.01039 7.03653 2.78653 6.81267 2.78653 6.53653V4.03653ZM2.66153 9.75C2.66153 9.40482 2.94135 9.125 3.28653 9.125H7.78653C8.13171 9.125 8.41153 9.40482 8.41153 9.75C8.41153 10.0952 8.13171 10.375 7.78653 10.375H3.28653C2.94135 10.375 2.66153 10.0952 2.66153 9.75Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"peace-symbol\", \"keywords\": [ \"religion\", \"peace\", \"war\", \"culture\", \"symbol\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189857)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2 7.00001C2 4.59154 3.70936 2.58231 5.98109 2.11933V4.87103L2.29796 8.70091C2.10515 8.17017 2 7.59737 2 7.00001ZM3.38943 10.4513C4.07579 11.1654 4.97308 11.6753 5.98109 11.8807V7.75636L3.38943 10.4513ZM7.98109 7.75637V11.8807C8.9891 11.6753 9.88639 11.1654 10.5727 10.4513L7.98109 7.75637ZM11.6642 8.70093C11.857 8.17018 11.9622 7.59738 11.9622 7.00001C11.9622 4.59154 10.2528 2.58231 7.98109 2.11933V4.87104L11.6642 8.70093ZM0 7.00001C0 3.14446 3.12554 0.0189209 6.98109 0.0189209C10.8366 0.0189209 13.9622 3.14446 13.9622 7.00001C13.9622 10.8556 10.8366 13.9811 6.98109 13.9811C3.12554 13.9811 0 10.8556 0 7.00001Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189857\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"politics-compaign\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189872)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.5366 3.95691C11.5498 3.97651 11.5629 3.9962 11.5758 4.01597C12.046 4.73569 12.3511 5.57314 12.4371 6.47449C12.4536 6.64742 12.4621 6.82274 12.4621 7.00007C12.4621 7.12523 12.4578 7.24939 12.4496 7.37241C12.3833 8.357 12.0561 9.27007 11.5366 10.0431H2.46265C1.87818 9.17344 1.53714 8.12652 1.53714 6.99995C1.53714 5.87342 1.87815 4.82654 2.46257 3.95691H11.5366ZM6.99961 0.0374756C4.89037 0.0374756 3.00019 0.975391 1.72339 2.45691H1.71068L1.50659 2.72126C0.676248 3.78575 0.148559 5.09786 0.0528662 6.52825C0.0424177 6.68419 0.0371094 6.84152 0.0371094 7.00007C0.0371094 7.22673 0.0479587 7.4509 0.0691648 7.67208C0.179327 8.82215 0.569267 9.89054 1.17013 10.8084C1.26955 10.9603 1.37473 11.1081 1.48538 11.2514L1.71056 11.5431H1.72348C3.00029 13.0245 4.89042 13.9624 6.99961 13.9624C9.1088 13.9624 10.9989 13.0245 12.2757 11.5431H12.2886L12.5138 11.2514C12.5294 11.2311 12.545 11.2107 12.5604 11.1903C12.5834 11.1598 12.6061 11.1292 12.6286 11.0984C13.4672 9.94856 13.9621 8.53205 13.9621 6.99995C13.9621 5.3837 13.4114 3.89609 12.4874 2.71451L12.2885 2.45691H12.2758C10.999 0.975391 9.10885 0.0374756 6.99961 0.0374756ZM3.94815 5.85474C3.94815 5.57859 3.72429 5.35474 3.44815 5.35474C3.17201 5.35474 2.94815 5.57859 2.94815 5.85474V6.22352L2.54376 6.08873C2.28179 6.0014 1.99863 6.14298 1.91131 6.40496C1.82398 6.66693 1.96556 6.95009 2.22753 7.03741L2.669 7.18457L2.32379 7.70239C2.17061 7.93215 2.2327 8.24259 2.46246 8.39576C2.69223 8.54894 3.00266 8.48685 3.15584 8.25709L3.44815 7.81862L3.74046 8.25709C3.89363 8.48685 4.20407 8.54894 4.43383 8.39576C4.66359 8.24259 4.72568 7.93215 4.57251 7.70239L4.22729 7.18457L4.66876 7.03741C4.93073 6.95009 5.07231 6.66693 4.98499 6.40496C4.89767 6.14298 4.61451 6.0014 4.35253 6.08873L3.94815 6.22352V5.85474ZM6.96011 5.35474C7.23625 5.35474 7.46011 5.57859 7.46011 5.85474V6.22352L7.8645 6.08873C8.12647 6.0014 8.40963 6.14298 8.49695 6.40496C8.58428 6.66693 8.4427 6.95009 8.18072 7.03741L7.73926 7.18457L8.08447 7.70239C8.23764 7.93215 8.17556 8.24259 7.94579 8.39576C7.71603 8.54894 7.40559 8.48685 7.25242 8.25709L6.96011 7.81862L6.6678 8.25709C6.51463 8.48685 6.20419 8.54894 5.97443 8.39576C5.74466 8.24259 5.68258 7.93215 5.83575 7.70239L6.18096 7.18457L5.7395 7.03741C5.47752 6.95009 5.33594 6.66693 5.42327 6.40496C5.51059 6.14298 5.79375 6.0014 6.05572 6.08873L6.46011 6.22352V5.85474C6.46011 5.57859 6.68397 5.35474 6.96011 5.35474ZM10.9721 5.85474C10.9721 5.57859 10.7482 5.35474 10.4721 5.35474C10.1959 5.35474 9.97207 5.57859 9.97207 5.85474V6.22352L9.56769 6.08873C9.30572 6.0014 9.02255 6.14298 8.93523 6.40496C8.84791 6.66693 8.98949 6.95009 9.25146 7.03741L9.69293 7.18457L9.34771 7.70239C9.19454 7.93215 9.25663 8.24259 9.48639 8.39576C9.71615 8.54894 10.0266 8.48685 10.1798 8.25709L10.4721 7.81862L10.7644 8.25709C10.9176 8.48685 11.228 8.54894 11.4578 8.39576C11.6875 8.24259 11.7496 7.93215 11.5964 7.70239L11.2512 7.18457L11.6927 7.03741C11.9547 6.95009 12.0962 6.66693 12.0089 6.40496C11.9216 6.14298 11.6384 6.0014 11.3765 6.08873L10.9721 6.22352V5.85474Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189872\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"politics-speech\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.00018 3.35998C7.92801 3.35998 8.68017 2.60782 8.68017 1.67999C8.68017 0.752157 7.92801 0 7.00018 0C6.07234 0 5.32019 0.752157 5.32019 1.67999C5.32019 2.60782 6.07234 3.35998 7.00018 3.35998ZM4.35887 5.78845C4.74273 4.69999 5.78036 3.91996 7.00018 3.91996C8.21999 3.91996 9.25763 4.69999 9.64148 5.78845H4.35887ZM1.89405 7.03512C1.51024 7.06803 1.20898 7.39001 1.20898 7.78235C1.20898 8.19656 1.54477 8.53235 1.95898 8.53235H2.85046L3.20963 13.4312C3.22879 13.6924 3.44634 13.8946 3.70829 13.8946H10.292C10.554 13.8946 10.7715 13.6924 10.7907 13.4312L11.1499 8.53235H12.0414C12.4556 8.53235 12.7914 8.19656 12.7914 7.78235C12.7914 7.39001 12.4902 7.06803 12.1063 7.03512C12.085 7.03732 12.0633 7.03845 12.0414 7.03845H1.95898C1.93706 7.03845 1.91539 7.03732 1.89405 7.03512ZM7.00017 8.69922C7.19049 8.69922 7.36431 8.80727 7.44854 8.97794L7.71051 9.50875L8.2963 9.59387C8.48465 9.62124 8.64112 9.75316 8.69993 9.93417C8.75874 10.1152 8.7097 10.3139 8.57341 10.4467L8.14953 10.8599L8.24959 11.4433C8.28177 11.6309 8.20465 11.8205 8.05068 11.9324C7.89671 12.0442 7.69258 12.059 7.52412 11.9704L7.00017 11.695L6.47622 11.9704C6.30776 12.059 6.10363 12.0442 5.94966 11.9324C5.79569 11.8205 5.71858 11.6309 5.75075 11.4433L5.85081 10.8599L5.42693 10.4467C5.29064 10.3139 5.2416 10.1152 5.30041 9.93417C5.35922 9.75316 5.51569 9.62124 5.70404 9.59387L6.28983 9.50875L6.5518 8.97794C6.63603 8.80727 6.80985 8.69922 7.00017 8.69922Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"politics-vote-2\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.16396 6.32673L9.42585 4.06484C9.62111 3.86958 9.62111 3.553 9.42585 3.35774L6.42171 0.3536C6.22645 0.158338 5.90987 0.158338 5.71461 0.3536L3.45272 2.61549C3.25746 2.81075 3.25746 3.12733 3.45272 3.32259L6.45685 6.32673C6.65212 6.52199 6.9687 6.52199 7.16396 6.32673ZM2.27148 5.63647H3.99883L5.51055 7.14819H4.29025C3.94507 7.14819 3.66525 7.42802 3.66525 7.77319C3.66525 8.11837 3.94507 8.39819 4.29025 8.39819H9.15971C9.50488 8.39819 9.78471 8.11837 9.78471 7.77319C9.78471 7.42802 9.50488 7.14819 9.15971 7.14819H8.11027L9.62198 5.63647H11.1784C12.0068 5.63647 12.6784 6.30805 12.6784 7.13647V9.75317H0.771484V7.13647C0.771484 6.30805 1.44306 5.63647 2.27148 5.63647ZM12.6784 11.0032H0.771484V13.4728C0.771484 13.7489 0.995342 13.9728 1.27148 13.9728H12.1784C12.4545 13.9728 12.6784 13.7489 12.6784 13.4728V11.0032Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"ticket-1\", \"keywords\": [ \"hobby\", \"ticket\", \"event\", \"entertainment\", \"stub\", \"theater\", \"entertainment\", \"culture\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.60645 1.5509H1.55664C0.728213 1.5509 0.0566406 2.22248 0.0566406 3.0509V5.0495C0.0566406 5.27686 0.210031 5.47559 0.429971 5.53319C1.08081 5.70363 1.5604 6.29632 1.5604 6.99976C1.5604 7.7032 1.0808 8.29589 0.429971 8.46634C0.210031 8.52393 0.0566406 8.72267 0.0566406 8.95002V10.9486C0.0566406 11.777 0.728215 12.4486 1.55664 12.4486H8.60645V10.7336C8.60645 10.3884 8.88627 10.1086 9.23145 10.1086C9.57663 10.1086 9.85645 10.3884 9.85645 10.7336V12.4486H12.4451C13.2736 12.4486 13.9451 11.777 13.9451 10.9486V8.94557C13.9451 8.71982 13.7939 8.52207 13.576 8.46299C12.9315 8.28824 12.4581 7.69865 12.4581 6.99976C12.4581 6.30088 12.9315 5.71128 13.576 5.53653C13.7939 5.47745 13.9451 5.27971 13.9451 5.05396V3.0509C13.9451 2.22247 13.2736 1.5509 12.4451 1.5509H9.85645V3.2742C9.85645 3.61937 9.57663 3.8992 9.23145 3.8992C8.88627 3.8992 8.60645 3.61937 8.60645 3.2742V1.5509ZM9.23145 5.36606C9.57663 5.36606 9.85645 5.64588 9.85645 5.99106V8.00711C9.85645 8.35228 9.57663 8.63211 9.23145 8.63211C8.88627 8.63211 8.60645 8.35228 8.60645 8.00711V5.99106C8.60645 5.64588 8.88627 5.36606 9.23145 5.36606Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"tickets\", \"keywords\": [ \"hobby\", \"ticket\", \"event\", \"entertainment\", \"stub\", \"theater\", \"entertainment\", \"culture\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189851)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.946518 1.74318C1.21086 1.63433 1.50812 1.77848 1.69547 1.9944C1.705 2.00538 1.71471 2.01623 1.72459 2.02692C1.85374 2.16675 2.00993 2.27891 2.1837 2.35659C2.35747 2.43428 2.5452 2.47588 2.73552 2.47887C2.92584 2.48186 3.11478 2.44619 3.29091 2.374C3.46704 2.30182 3.62667 2.19463 3.76015 2.05894C3.89363 1.92324 3.99818 1.76185 4.06745 1.58456C4.13672 1.40727 4.16927 1.21776 4.16314 1.02752C4.16239 1.00418 4.16106 0.980895 4.15915 0.957678C4.13658 0.682805 4.23397 0.382937 4.48365 0.2658L4.89471 0.0729576C5.12513 -0.0230685 5.38413 -0.0243501 5.61549 0.0693909C5.84685 0.163132 6.03191 0.344332 6.1305 0.573666L6.6142 1.69096C6.59566 1.72809 6.57813 1.76583 6.56162 1.80414L6.56086 1.8059L3.37165 9.17262L3.025 9.96047L0.068737 3.11982C-0.0205235 2.89835 -0.0229323 2.65135 0.0619914 2.42818C0.146915 2.20501 0.312918 2.0221 0.526832 1.91599L0.946518 1.74318ZM12.2691 3.74706C12.4544 3.52936 12.7517 3.3856 13.016 3.49445L13.4594 3.67701C13.6789 3.78589 13.8492 3.97358 13.9364 4.20259C14.0235 4.43159 14.021 4.68505 13.9294 4.91231L10.2565 13.4113C10.1553 13.6467 9.96544 13.8326 9.72803 13.9288C9.49062 14.025 9.22486 14.0237 8.98841 13.9251L4.55009 12.0339C4.31476 11.9327 4.12882 11.7429 4.03263 11.5054C3.93644 11.268 3.93775 11.0023 4.03629 10.7658L4.51729 9.67264L7.70923 2.2996C7.8104 2.06427 8.0003 1.87833 8.23771 1.78214C8.47511 1.68595 8.74088 1.68727 8.97732 1.7858L9.41094 1.98923C9.66063 2.10636 9.7583 2.40599 9.73324 2.68065C9.73067 2.70878 9.72893 2.73701 9.72801 2.76531C9.72173 2.96053 9.75513 3.15499 9.82621 3.33692C9.89729 3.51885 10.0046 3.68445 10.1415 3.82369C10.2785 3.96294 10.4423 4.07293 10.623 4.147C10.8038 4.22107 10.9977 4.25767 11.193 4.2546C11.3883 4.25153 11.5809 4.20884 11.7592 4.12913C11.9375 4.04941 12.0978 3.93433 12.2303 3.79084C12.2436 3.7765 12.2565 3.7619 12.2691 3.74706Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189851\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"yin-yang-symbol\", \"keywords\": [ \"religion\", \"tao\", \"yin\", \"yang\", \"taoism\", \"culture\", \"symbol\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189848)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.69797 2.17043C3.56805 2.74213 2 4.68625 2 6.99662C2 9.75617 4.23706 11.9932 6.99662 11.9932C7.34889 11.9932 7.69265 11.9568 8.02433 11.8874C8.68462 10.4966 9.076 8.48189 6.99662 6.99662C4.68963 5.34715 4.99797 3.47921 5.69797 2.17043ZM6.99662 0C3.13249 0 0 3.13249 0 6.99662C0 10.8607 3.13249 13.9932 6.99662 13.9932C10.8607 13.9932 13.9932 10.8607 13.9932 6.99662C13.9932 3.13249 10.8607 0 6.99662 0ZM8.59172 5.24968C9.27441 5.24968 9.82784 4.69625 9.82784 4.01356C9.82784 3.33087 9.27441 2.77745 8.59172 2.77745C7.90903 2.77745 7.35561 3.33087 7.35561 4.01356C7.35561 4.69625 7.90903 5.24968 8.59172 5.24968ZM6.99661 9.49026C6.99661 10.1729 6.44319 10.7264 5.7605 10.7264C5.07781 10.7264 4.52438 10.1729 4.52438 9.49026C4.52438 8.80757 5.07781 8.25414 5.7605 8.25414C6.44319 8.25414 6.99661 8.80757 6.99661 9.49026Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189848\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"zodiac-1\", \"keywords\": [ \"sign\", \"astrology\", \"stars\", \"space\", \"scorpio\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189887)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.84615 3.38464C2.62174 3.38464 2.40652 3.47379 2.24783 3.63248C2.08915 3.79116 2 4.00638 2 4.2308V9.76926C2 10.3215 1.55228 10.7693 1 10.7693C0.447715 10.7693 0 10.3215 0 9.76926V4.2308C0 3.47595 0.299862 2.75202 0.833619 2.21826C1.36738 1.68451 2.09131 1.38464 2.84615 1.38464C3.5252 1.38464 4.17923 1.62731 4.69231 2.06461C5.20538 1.62731 5.85941 1.38464 6.53846 1.38464C7.29331 1.38464 8.01724 1.68451 8.551 2.21826C9.08475 2.75202 9.38462 3.47595 9.38461 4.2308V9.76926C9.38461 9.99367 9.47376 10.2089 9.63245 10.3676C9.79113 10.5263 10.0064 10.6154 10.2308 10.6154C10.4552 10.6154 10.6704 10.5263 10.8291 10.3676C10.9703 10.2264 11.0564 10.0404 11.0737 9.84299C10.8451 9.82471 10.6216 9.72815 10.4468 9.5533C10.0562 9.16277 10.0562 8.52961 10.4468 8.13908L11.3653 7.22057C11.3806 7.20506 11.3964 7.19005 11.4127 7.17557C11.4933 7.10378 11.5828 7.0473 11.6773 7.00614C11.7997 6.95273 11.9349 6.92311 12.0769 6.92311C12.2114 6.92311 12.3398 6.94966 12.4569 6.99783C12.5759 7.04664 12.6874 7.11936 12.7841 7.216L13.7071 8.13908C14.0977 8.52961 14.0977 9.16277 13.7071 9.5533C13.5312 9.72923 13.306 9.8259 13.076 9.84331C13.057 10.5713 12.7596 11.2655 12.2433 11.7818C11.7095 12.3155 10.9856 12.6154 10.2308 12.6154C9.47592 12.6154 8.75199 12.3155 8.21824 11.7818C7.68448 11.248 7.38462 10.5241 7.38462 9.76926V4.2308C7.38462 4.00638 7.29547 3.79116 7.13678 3.63248C6.9781 3.47379 6.76288 3.38464 6.53846 3.38464C6.31405 3.38464 6.09882 3.47379 5.94014 3.63248C5.78232 3.7903 5.69327 4.00405 5.69232 4.22715V4.23079V9.76925C5.69232 10.3215 5.24461 10.7693 4.69232 10.7693C4.14004 10.7693 3.69232 10.3215 3.69232 9.76925V4.23616L3.69231 4.2308C3.69231 4.00638 3.60316 3.79116 3.44448 3.63248C3.28579 3.47379 3.07057 3.38464 2.84615 3.38464Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189887\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"zodiac-10\", \"keywords\": [ \"sign\", \"astrology\", \"stars\", \"space\", \"pisces\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189917)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.13813 0.380503C1.48029 -0.0530219 2.10911 -0.127087 2.54263 0.215074C3.56764 1.02407 4.38711 2.06369 4.93432 3.24931C5.33597 4.11955 5.5826 5.0504 5.66586 6.00004H8.33424C8.4175 5.0504 8.66413 4.11955 9.06578 3.24931C9.61299 2.06369 10.4325 1.02407 11.4575 0.215074C11.891 -0.127087 12.5198 -0.0530219 12.862 0.380503C13.2041 0.814028 13.1301 1.44285 12.6965 1.78501C11.9188 2.39888 11.2969 3.18776 10.8817 4.08742C10.6016 4.69433 10.4208 5.34008 10.3444 6.00004H13C13.5523 6.00004 14 6.44776 14 7.00004C14 7.55232 13.5523 8.00004 13 8.00004H10.3444C10.4208 8.66001 10.6016 9.30577 10.8817 9.91269C11.2969 10.8123 11.9188 11.6012 12.6965 12.2151C13.1301 12.5572 13.2041 13.186 12.862 13.6196C12.5198 14.0531 11.891 14.1272 11.4575 13.785C10.4325 12.976 9.61299 11.9364 9.06578 10.7508C8.66413 9.88055 8.41749 8.94969 8.33424 8.00004H5.66587C5.58261 8.94969 5.33598 9.88055 4.93432 10.7508C4.38711 11.9364 3.56764 12.976 2.54263 13.785C2.10911 14.1272 1.48029 14.0531 1.13813 13.6196C0.795968 13.186 0.870033 12.5572 1.30356 12.2151C2.08135 11.6012 2.70317 10.8123 3.1184 9.91269C3.39852 9.30577 3.57927 8.66001 3.65574 8.00004H1C0.447715 8.00004 0 7.55232 0 7.00004C0 6.44776 0.447715 6.00004 1 6.00004H3.65573C3.57926 5.34008 3.39851 4.69433 3.1184 4.08742C2.70317 3.18776 2.08135 2.39888 1.30356 1.78501C0.870033 1.44285 0.795968 0.814028 1.13813 0.380503Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189917\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"zodiac-11\", \"keywords\": [ \"sign\", \"astrology\", \"stars\", \"space\", \"sagittarius\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189908)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.7434 0.331137C13.7206 0.305748 13.6964 0.281536 13.6711 0.258603C13.4937 0.0978959 13.2583 0 13 0H8.38464C7.83236 0 7.38464 0.447715 7.38464 1C7.38464 1.55228 7.83236 2 8.38464 2H10.5858L4.23075 8.35505L2.63014 6.75444C2.23962 6.36392 1.60645 6.36392 1.21593 6.75444C0.825404 7.14497 0.825404 7.77813 1.21593 8.16865L2.81654 9.76926L0.292893 12.2929C-0.097631 12.6834 -0.097631 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L4.23075 11.1835L5.83132 12.784C6.22184 13.1746 6.85501 13.1746 7.24553 12.784C7.63606 12.3935 7.63606 11.7604 7.24553 11.3698L5.64496 9.76926L12 3.41419V5.61539C12 6.16767 12.4477 6.61539 13 6.61539C13.5523 6.61539 14 6.16767 14 5.61539V1C14 0.855771 13.9695 0.718673 13.9145 0.594818C13.8728 0.500539 13.8158 0.411391 13.7434 0.331137Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189908\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"zodiac-12\", \"keywords\": [ \"sign\", \"astrology\", \"stars\", \"space\", \"cancer\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189911)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.30769 2.53088C2.58547 2.53088 2 3.11636 2 3.83857C2 4.56079 2.58547 5.14626 3.30769 5.14626C4.02991 5.14626 4.61538 4.56079 4.61538 3.83857C4.61538 3.11636 4.02991 2.53088 3.30769 2.53088ZM0 3.83857C0 2.01179 1.4809 0.530884 3.30769 0.530884H13C13.5523 0.530884 14 0.978599 14 1.53088C14 2.08317 13.5523 2.53088 13 2.53088H6.34683C6.51964 2.93198 6.61538 3.3741 6.61538 3.83857C6.61538 5.66536 5.13448 7.14626 3.30769 7.14626C1.4809 7.14626 0 5.66536 0 3.83857ZM10.6923 8.85379C9.97012 8.85379 9.38464 9.43926 9.38464 10.1615C9.38464 10.881 9.96581 11.4649 10.6844 11.4691H10.6923L10.6976 11.4692C11.4174 11.4663 12 10.882 12 10.1615C12 9.43926 11.4146 8.85379 10.6923 8.85379ZM7.38464 10.1615C7.38464 10.6259 7.48038 11.0681 7.65319 11.4691H1C0.447715 11.4691 0 11.9169 0 12.4691C0 13.0214 0.447715 13.4691 1 13.4691H10.6796L10.6923 13.4692C12.5191 13.4692 14 11.9883 14 10.1615C14 8.33469 12.5191 6.85379 10.6923 6.85379C8.86555 6.85379 7.38464 8.33469 7.38464 10.1615Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189911\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"zodiac-2\", \"keywords\": [ \"sign\", \"astrology\", \"stars\", \"space\", \"virgo\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189899)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.07692 2C2.7913 2 2.51738 2.11346 2.31542 2.31542C2.11346 2.51738 2 2.7913 2 3.07692V9.30767C2 9.85996 1.55228 10.3077 1 10.3077C0.447715 10.3077 0 9.85996 0 9.30767V3.07692C0 2.26087 0.324174 1.47824 0.901209 0.901209C1.47824 0.324174 2.26087 0 3.07692 0C3.84729 0 4.58788 0.288902 5.15384 0.806708C5.71979 0.288902 6.46038 0 7.23076 0C8.04681 0 8.82943 0.324174 9.40647 0.901209C9.85496 1.3497 10.1507 1.9224 10.2602 2.53845H10.568C13.4635 2.53845 14.9135 6.03916 12.8661 8.08655L10.3799 10.5728C10.4217 10.6674 10.4809 10.7545 10.5555 10.8291C10.7142 10.9878 10.9294 11.0769 11.1538 11.0769C11.3782 11.0769 11.5935 10.9878 11.7521 10.8291C11.9108 10.6704 12 10.4552 12 10.2307V9.80767C12 9.25539 12.4477 8.80767 13 8.80767C13.5523 8.80767 14 9.25539 14 9.80767V10.2307C14 10.9856 13.7001 11.7095 13.1664 12.2433C12.6326 12.777 11.9087 13.0769 11.1538 13.0769C10.399 13.0769 9.67505 12.777 9.14129 12.2433C9.06881 12.1708 9.00064 12.0948 8.93695 12.0157L8.20711 12.7456C7.81658 13.1361 7.18342 13.1361 6.79289 12.7456C6.40237 12.355 6.40237 11.7219 6.79289 11.3313L8.30767 9.81656V3.07692C8.30767 2.7913 8.19421 2.51738 7.99225 2.31542C7.79029 2.11346 7.51637 2 7.23076 2C6.94514 2 6.67122 2.11346 6.46926 2.31542C6.26867 2.51601 6.15539 2.78758 6.15385 3.07109L6.15387 3.07693V9.30769C6.15387 9.85998 5.70615 10.3077 5.15387 10.3077C4.60158 10.3077 4.15387 9.85998 4.15387 9.30769V3.08506L4.15384 3.07692C4.15384 2.7913 4.04038 2.51738 3.83841 2.31542C3.63645 2.11346 3.36254 2 3.07692 2ZM10.3077 7.81656L11.4519 6.67234C12.2394 5.88488 11.6817 4.53845 10.568 4.53845H10.3077V7.81656Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189899\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"zodiac-3\", \"keywords\": [ \"sign\", \"astrology\", \"stars\", \"space\", \"leo\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189893)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.46149 0C6.46183 0 5.50311 0.397114 4.79624 1.10398C4.08938 1.81085 3.69226 2.76957 3.69226 3.76923V8.43557C3.42497 8.35246 3.14079 8.30769 2.84615 8.30769C1.27427 8.30769 0 9.58196 0 11.1538C0 12.7257 1.27427 14 2.84615 14C4.41804 14 5.69231 12.7257 5.69231 11.1538C5.69231 11.1483 5.69229 11.1429 5.69226 11.1374V3.76923C5.69226 3.3 5.87866 2.84999 6.21046 2.5182C6.54225 2.1864 6.99226 2 7.46149 2C7.93072 2 8.38073 2.1864 8.71253 2.5182C9.04432 2.84999 9.23072 3.3 9.23072 3.76923V11.6154C9.23072 12.2478 9.48196 12.8544 9.92916 13.3016C10.3764 13.7488 10.9829 14 11.6153 14C12.2478 14 12.8543 13.7488 13.3015 13.3016C13.7487 12.8544 14 12.2478 14 11.6154C14 11.0631 13.5522 10.6154 13 10.6154C12.4477 10.6154 12 11.0631 12 11.6154C12 11.7174 11.9594 11.8152 11.8873 11.8873C11.8152 11.9595 11.7173 12 11.6153 12C11.5133 12 11.4155 11.9595 11.3434 11.8873C11.2712 11.8152 11.2307 11.7174 11.2307 11.6154V3.76923C11.2307 2.76957 10.8336 1.81085 10.1267 1.10398C9.41987 0.397114 8.46115 0 7.46149 0ZM3.69226 11.1448V11.1538C3.69226 11.1562 3.69227 11.1585 3.69228 11.1609C3.68851 11.625 3.31113 12 2.84615 12C2.37884 12 2 11.6212 2 11.1538C2 10.6865 2.37884 10.3077 2.84615 10.3077C3.31047 10.3077 3.68743 10.6817 3.69226 11.1448Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189893\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"zodiac-4\", \"keywords\": [ \"sign\", \"astrology\", \"stars\", \"space\", \"aquarius\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189896)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.75887 2.35694C1.39919 1.93783 0.767865 1.88966 0.34876 2.24934C-0.0703449 2.60902 -0.118518 3.24035 0.241161 3.65945L2.36424 6.1333L2.36506 6.13425C2.53691 6.33399 2.74798 6.49628 2.98516 6.61106C3.22235 6.72583 3.48058 6.79064 3.74386 6.80146C4.00713 6.81227 4.26981 6.76887 4.51561 6.67395C4.76141 6.57903 4.98509 6.4346 5.17275 6.24964L5.17789 6.24453L6.97232 4.4501L8.76676 6.24453L8.7719 6.24964C8.95956 6.4346 9.18323 6.57903 9.42903 6.67395C9.67483 6.76888 9.93752 6.81227 10.2008 6.80146C10.4641 6.79064 10.7223 6.72583 10.9595 6.61106C11.1947 6.49726 11.4041 6.33675 11.5752 6.13934L13.7505 3.66908C14.1155 3.25459 14.0754 2.6227 13.6609 2.25771C13.2464 1.89271 12.6145 1.93283 12.2495 2.34732L10.1182 4.76759L8.33771 2.98707L8.33594 2.98529C8.15753 2.80588 7.94547 2.66344 7.71189 2.56613C7.47755 2.46849 7.22619 2.41823 6.97232 2.41823C6.71845 2.41823 6.4671 2.46849 6.23275 2.56613C5.99917 2.66344 5.78711 2.80589 5.60869 2.98531L3.82711 4.76689L1.75887 2.35694ZM1.75887 7.54563C1.3992 7.12652 0.767865 7.07835 0.34876 7.43803C-0.0703449 7.79771 -0.118518 8.42904 0.241161 8.84814L2.36424 11.322L2.36506 11.3229C2.53691 11.5227 2.74798 11.685 2.98516 11.7997C3.22235 11.9145 3.48058 11.9793 3.74386 11.9901C4.00713 12.001 4.26981 11.9576 4.51561 11.8626C4.76141 11.7677 4.98509 11.6233 5.17275 11.4383L5.17789 11.4332L6.97232 9.63879L8.76676 11.4332L8.7719 11.4383C8.95956 11.6233 9.18323 11.7677 9.42903 11.8626C9.67483 11.9576 9.93752 12.001 10.2008 11.9901C10.4641 11.9793 10.7223 11.9145 10.9595 11.7997C11.1947 11.6859 11.4041 11.5254 11.5752 11.328L13.7505 8.85777C14.1155 8.44328 14.0754 7.81139 13.6609 7.44639C13.2464 7.0814 12.6145 7.12152 12.2495 7.53601L10.1182 9.95628L8.33771 8.17576L8.33595 8.17399C8.15754 7.99458 7.94547 7.85213 7.71189 7.75482C7.47755 7.65719 7.22619 7.60692 6.97232 7.60692C6.71845 7.60692 6.4671 7.65719 6.23275 7.75482C5.99917 7.85213 5.7871 7.99458 5.60869 8.174L3.82711 9.95558L1.75887 7.54563Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189896\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"zodiac-5\", \"keywords\": [ \"sign\", \"astrology\", \"stars\", \"space\", \"taurus\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.02001 1.10698C3.95634 0.558382 3.46 0.165263 2.91139 0.228929C2.36279 0.292595 1.96967 0.788937 2.03334 1.33754C2.1747 2.55564 2.75877 3.67931 3.67449 4.49491C3.78298 4.59154 3.89524 4.68305 4.01087 4.76929C2.79024 5.68098 2 7.1371 2 8.7778C2 11.5392 4.23857 13.7778 6.99999 13.7778C9.76141 13.7778 12 11.5392 12 8.7778C12 7.13711 11.2097 5.68098 9.98912 4.76929C10.1048 4.68305 10.217 4.59154 10.3255 4.49491C11.2412 3.67931 11.8253 2.55564 11.9667 1.33754C12.0303 0.788937 11.6372 0.292595 11.0886 0.228929C10.54 0.165263 10.0437 0.558382 9.97999 1.10698C9.89517 1.83784 9.54473 2.51205 8.9953 3.0014C8.44587 3.49076 7.73576 3.76115 7 3.76115C6.26424 3.76115 5.55413 3.49076 5.0047 3.0014C4.45526 2.51205 4.10482 1.83784 4.02001 1.10698ZM4 8.7778C4 7.12095 5.34314 5.7778 6.99999 5.7778C8.65684 5.7778 9.99999 7.12095 9.99999 8.7778C9.99999 10.4346 8.65684 11.7778 6.99999 11.7778C5.34314 11.7778 4 10.4346 4 8.7778Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"zodiac-6\", \"keywords\": [ \"sign\", \"astrology\", \"stars\", \"space\", \"capricorn\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189884)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.21714 0.0240269C0.67801 -0.0957798 0.143833 0.244151 0.0240268 0.783285C-0.0957235 1.32216 0.243832 1.85609 0.782524 1.97623L0.78079 1.97584L0.779748 1.9756C0.779748 1.9756 0.781055 1.97589 0.782524 1.97623L0.80264 1.9812C0.823483 1.98645 0.857914 1.99545 0.903981 2.00861C0.996335 2.035 1.1341 2.07772 1.30194 2.13988C1.64078 2.26538 2.08525 2.46388 2.52245 2.75534C3.39071 3.33417 4.15408 4.21553 4.15408 5.61559C4.15408 6.16788 4.60179 6.61559 5.15408 6.61559C5.70636 6.61559 6.15408 6.16788 6.15408 5.61559C6.15408 4.21553 6.91745 3.33417 7.7857 2.75534C7.96103 2.63845 8.13753 2.53651 8.30792 2.44824V8.30789H1.00021C0.447929 8.30789 0.000214141 8.7556 0.000214141 9.30789C0.000214141 9.86018 0.447929 10.3079 1.00021 10.3079H8.30792V11.1399C8.3079 11.1446 8.30789 11.1493 8.30789 11.154C8.30789 12.7259 9.58215 14.0002 11.154 14.0002C12.7259 14.0002 14.0002 12.7259 14.0002 11.154C14.0002 9.58324 12.7277 8.30964 11.1573 8.30789H11.154H10.3079V1.00021C10.3079 0.696894 10.1702 0.409968 9.93365 0.220172C9.69705 0.0303768 9.38709 -0.0417721 9.09099 0.0240271L9.30792 1.00021C9.09099 0.0240271 9.09142 0.0239299 9.09099 0.0240271L9.08918 0.024431L9.08698 0.0249253L9.08143 0.0261884L9.0658 0.0298329C9.0534 0.0327669 9.03706 0.0367236 9.01705 0.0417637C8.97704 0.0518389 8.92223 0.0662751 8.85472 0.0855652C8.71991 0.124082 8.53315 0.182321 8.31157 0.264388C7.87157 0.427352 7.27757 0.690393 6.6763 1.09124C6.1497 1.4423 5.60319 1.91117 5.15408 2.51812C4.70496 1.91117 4.15845 1.4423 3.63184 1.09123C3.03057 0.690392 2.43657 0.427351 1.99656 0.264387C1.77498 0.18232 1.58823 0.124082 1.45342 0.0855648C1.3859 0.0662747 1.33109 0.0518386 1.29108 0.0417633C1.27107 0.0367232 1.25473 0.0327665 1.24233 0.0298325L1.2267 0.0261881L1.22115 0.024925L1.21895 0.0244307C1.21851 0.0243335 1.21714 0.0240269 1.00021 1.00021L1.21714 0.0240269ZM10.3079 11.16C10.3079 11.158 10.3079 11.156 10.3079 11.154V11.1463C10.312 10.6826 10.6893 10.3079 11.154 10.3079C11.6213 10.3079 12.0002 10.6867 12.0002 11.154C12.0002 11.6213 11.6213 12.0002 11.154 12.0002C10.6887 12.0002 10.3111 11.6246 10.3079 11.16Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189884\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"zodiac-7\", \"keywords\": [ \"sign\", \"astrology\", \"stars\", \"space\", \"ares\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189923)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.105792 0.552761C0.352781 0.0587829 0.953455 -0.141442 1.44743 0.105548C3.06476 0.914215 5.60521 3.16432 7.00022 7.01866C8.39522 3.16432 10.9357 0.914215 12.553 0.105548C13.047 -0.141442 13.6476 0.0587829 13.8946 0.552761C14.1416 1.04674 13.9414 1.64741 13.4474 1.8944C11.7711 2.73257 8.00022 6.19536 8.00022 13C8.00022 13.5523 7.5525 14 7.00022 14C6.44793 14 6.00022 13.5523 6.00022 13C6.00022 6.19536 2.22934 2.73257 0.553006 1.8944C0.059027 1.64741 -0.141197 1.04674 0.105792 0.552761Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189923\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"zodiac-8\", \"keywords\": [ \"sign\", \"astrology\", \"stars\", \"space\", \"libra\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.99999 0.885986C5.75551 0.885986 4.56201 1.38035 3.68203 2.26033C2.80274 3.13962 2.30846 4.33195 2.30769 5.57538C2.30621 5.86128 2.33143 6.14548 2.38241 6.42444H1C0.447715 6.42444 0 6.87215 0 7.42444C0 7.97672 0.447715 8.42444 1 8.42444H3.80615C4.16514 8.42444 4.49658 8.23201 4.67457 7.92026C4.85257 7.6085 4.8498 7.22526 4.66731 6.91611C4.42924 6.51278 4.30495 6.05245 4.30767 5.5841L4.30769 5.57829C4.30769 4.86424 4.59134 4.17945 5.09624 3.67454C5.60115 3.16964 6.28595 2.88599 6.99999 2.88599C7.71403 2.88599 8.39883 3.16964 8.90373 3.67454C9.40864 4.17945 9.69229 4.86424 9.69229 5.57829L9.69256 5.60149C9.70337 6.06736 9.58833 6.52753 9.35955 6.9335C9.18507 7.24313 9.18804 7.62209 9.36735 7.92895C9.54665 8.2358 9.87535 8.42444 10.2308 8.42444H13C13.5523 8.42444 14 7.97672 14 7.42444C14 6.87215 13.5523 6.42444 13 6.42444H11.6294C11.677 6.14225 11.6982 5.85511 11.6923 5.56668C11.6892 4.3264 11.1952 3.13757 10.3179 2.26033C9.43797 1.38035 8.24446 0.885986 6.99999 0.885986ZM1 10.6068C0.447715 10.6068 0 11.0545 0 11.6068C0 12.159 0.447715 12.6068 1 12.6068H13C13.5523 12.6068 14 12.159 14 11.6068C14 11.0545 13.5523 10.6068 13 10.6068H1Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"zodiac-9\", \"keywords\": [ \"sign\", \"astrology\", \"stars\", \"space\", \"gemini\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189905)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.74633 0.334427C1.37874 -0.0777562 0.74661 -0.113905 0.334427 0.253686C-0.0777562 0.621278 -0.113905 1.25341 0.253686 1.66559C1.07188 2.58304 2.11439 3.30687 3.29019 3.78713C3.42293 3.84135 3.55703 3.89233 3.69233 3.94005V10.06C3.55703 10.1077 3.42293 10.1586 3.29019 10.2129C2.11439 10.6931 1.07188 11.417 0.253686 12.3344C-0.113905 12.7466 -0.077756 13.3787 0.334427 13.7463C0.746611 14.1139 1.37874 14.0778 1.74633 13.6656C2.351 12.9876 3.137 12.4358 4.04646 12.0644C4.95603 11.6928 5.95707 11.5149 6.96653 11.5487C6.98884 11.5494 7.01116 11.5494 7.03347 11.5487C8.04293 11.5149 9.04397 11.6928 9.95354 12.0644C10.863 12.4358 11.649 12.9876 12.2537 13.6656C12.6212 14.0778 13.2534 14.1139 13.6656 13.7463C14.0777 13.3787 14.1139 12.7466 13.7463 12.3344C12.9281 11.417 11.8856 10.6931 10.7098 10.2129C10.5771 10.1586 10.443 10.1077 10.3077 10.06V3.94004C10.443 3.89233 10.5771 3.84135 10.7098 3.78713C11.8856 3.30687 12.9281 2.58304 13.7463 1.66559C14.1139 1.25341 14.0777 0.621278 13.6656 0.253686C13.2534 -0.113905 12.6212 -0.0777562 12.2537 0.334427C11.649 1.01245 10.863 1.56415 9.95354 1.93563C9.04397 2.30715 8.04293 2.48513 7.03347 2.45133C7.01116 2.45058 6.98884 2.45058 6.96653 2.45133C5.95707 2.48513 4.95603 2.30715 4.04646 1.93563C3.137 1.56415 2.351 1.01245 1.74633 0.334427ZM8.30769 4.39753C7.87501 4.44623 7.43776 4.46434 7 4.45126C6.56225 4.46434 6.125 4.44624 5.69233 4.39754V9.60247C6.125 9.55377 6.56225 9.53567 7 9.54874C7.43776 9.53567 7.87501 9.55377 8.30769 9.60248V4.39753Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189905\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" } ], \"entertainment\": [ { \"name\": \"balloon\", \"keywords\": [ \"hobby\", \"entertainment\", \"party\", \"balloon\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188396)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.40459 11.1725H3.47218C3.27855 11.1725 3.10233 11.0607 3.01983 10.8855C2.93733 10.7104 2.96339 10.5033 3.08671 10.354L4.22632 8.97468C3.27676 8.77819 2.44248 8.24429 1.81252 7.5586C1.01456 6.69005 0.503906 5.53512 0.503906 4.40357C0.503906 2.13402 2.54616 0.125 5.02187 0.125C7.49756 0.125 9.54004 2.13399 9.54004 4.40357C9.54004 5.53514 9.02933 6.69007 8.23131 7.55862C7.60722 8.23787 6.78262 8.76817 5.8441 8.96907L6.9609 10.3594C7.08138 10.5094 7.10513 10.7152 7.02196 10.8886C6.9388 11.0621 6.76348 11.1725 6.57109 11.1725H5.6393C5.64192 11.1891 5.64389 11.2059 5.64519 11.223C5.68723 11.7741 5.83365 12.1073 5.99078 12.3078C6.14406 12.5035 6.34102 12.6162 6.57236 12.6704C7.07271 12.7876 7.70044 12.6168 8.07821 12.374C8.92763 11.8279 9.67154 11.4599 10.4776 11.257C11.2838 11.0542 12.1108 11.0269 13.112 11.1056C13.4561 11.1326 13.7132 11.4335 13.6861 11.7776C13.6591 12.1217 13.3582 12.3788 13.0141 12.3517C12.088 12.2789 11.4093 12.3115 10.7827 12.4692C10.156 12.627 9.54011 12.9202 8.75416 13.4255C8.16509 13.8041 7.19343 14.0997 6.28726 13.8875C5.81533 13.7769 5.35725 13.526 5.00686 13.0788C4.66034 12.6366 4.45448 12.0479 4.39881 11.318C4.39503 11.2684 4.39715 11.2196 4.40459 11.1725Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188396\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bow\", \"keywords\": [ \"entertainment\", \"gaming\", \"bow\", \"weapon\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188399)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.4634 0.026123C10.16 0.026123 9.88658 0.208855 9.77049 0.48911C9.65441 0.769366 9.71858 1.09195 9.93307 1.30645L10.7955 2.16898L9.14244 3.82213C7.441 2.37395 5.23573 1.5 2.82634 1.5C2.08273 1.5 1.35768 1.58338 0.660371 1.74161C0.256426 1.83327 0.00326875 2.23504 0.0949292 2.63898C0.186589 3.04293 0.588356 3.29608 0.992301 3.20442C1.26526 3.14249 1.5435 3.09406 1.8263 3.05992V10.6162H0.826437C0.523091 10.6162 0.249613 10.7989 0.133528 11.0792L0.132701 11.0812C0.113442 11.1279 0.0987692 11.1771 0.0892458 11.228C0.0789566 11.2829 0.0749121 11.3383 0.076911 11.3933C0.0840387 11.5979 0.173103 11.7816 0.312299 11.9127L2.13628 13.7367C2.27312 13.8827 2.46769 13.9739 2.68357 13.9739C3.09778 13.9739 3.43357 13.6381 3.43357 13.2239V12.25H11.0164C10.9822 12.5328 10.9338 12.8111 10.8719 13.084C10.7802 13.488 11.0334 13.8897 11.4373 13.9814C11.8413 14.0731 12.243 13.8199 12.3347 13.416C12.4929 12.7187 12.5763 11.9936 12.5763 11.25C12.5763 8.81455 11.6833 6.58766 10.207 4.87888L11.8562 3.22964L12.7187 4.09217C12.9332 4.30667 13.2558 4.37084 13.5361 4.25475C13.8163 4.13866 13.9991 3.86519 13.9991 3.56184V0.776123C13.9991 0.669572 13.9769 0.568211 13.9368 0.47642C13.9045 0.402193 13.8596 0.332128 13.8021 0.269468C13.7877 0.25375 13.7726 0.238648 13.7569 0.224206C13.6919 0.164279 13.6188 0.117949 13.5414 0.0852173C13.4515 0.0471671 13.3528 0.026123 13.2491 0.026123H10.4634ZM8.0777 4.88687C6.77113 3.80736 5.12647 3.12251 3.3263 3.0149V9.63828L8.0777 4.88687ZM9.143 5.94289C10.2493 7.25827 10.9522 8.92415 11.0614 10.75H4.33589L9.143 5.94289Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188399\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"button-fast-forward-1\", \"keywords\": [ \"button\", \"controls\", \"fast\", \"forward\", \"movies\", \"television\", \"video\", \"tv\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.49671 5.89365L1.27038 2.42362C0.973516 2.24286 0.611682 2.29104 0.350178 2.48004C0.148642 2.6257 0.00669641 2.855 5.83568e-06 3.12759V10.8478L0 10.8509C8.5553e-07 11.4887 0.73453 11.9015 1.27038 11.5752L6.49671 8.1261V10.8478L6.4967 10.8505C6.49637 11.4885 7.23111 11.9016 7.76709 11.5752L13.6339 7.70339C14.122 7.3868 14.122 6.63551 13.6339 6.31892L7.76709 2.42362C7.47022 2.24286 7.10839 2.29104 6.84688 2.48004C6.64535 2.6257 6.5034 2.855 6.49671 3.12759V5.89365Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"button-fast-forward-2\", \"keywords\": [ \"button\", \"controls\", \"fast\", \"forward\", \"movies\", \"television\", \"video\", \"tv\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13 2.23425C13.5523 2.23425 14 2.68197 14 3.23425V10.7658C14 11.3181 13.5523 11.7658 13 11.7658C12.4477 11.7658 12 11.3181 12 10.7658V8.78177L7.76709 11.5753C7.23111 11.9016 6.49637 11.4886 6.4967 10.8505L6.49671 10.8479V8.12616L1.27038 11.5753C0.73453 11.9016 8.5553e-07 11.4888 0 10.851L5.83568e-06 10.8479V3.12765C0.00669641 2.85506 0.148642 2.62576 0.350178 2.4801C0.611682 2.2911 0.973516 2.24292 1.27038 2.42368L6.49671 5.89371V3.12765C6.5034 2.85506 6.64535 2.62576 6.84688 2.4801C7.10839 2.2911 7.47022 2.24292 7.76709 2.42368L12 5.23413V3.23425C12 2.68197 12.4477 2.23425 13 2.23425Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"button-next\", \"keywords\": [ \"button\", \"television\", \"buttons\", \"movies\", \"skip\", \"next\", \"video\", \"controls\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188405)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.51212 6.056L1.6931 0.656892C1.29745 0.406346 0.815218 0.473133 0.466699 0.735097C0.198102 0.936989 0.00892463 1.25481 7.77749e-06 1.63264V12.3333L0 12.3376C9.62732e-07 13.2216 0.978943 13.7938 1.6931 13.3415L9.51212 7.97496C10.1626 7.53614 10.1626 6.49482 9.51212 6.056ZM13.7499 1.74216C13.7499 1.18987 13.3022 0.742157 12.7499 0.742157C12.1977 0.742157 11.7499 1.18987 11.7499 1.74216V12.2578C11.7499 12.8101 12.1977 13.2578 12.7499 13.2578C13.3022 13.2578 13.7499 12.8101 13.7499 12.2578V1.74216Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188405\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"button-pause-2\", \"keywords\": [ \"button\", \"television\", \"buttons\", \"movies\", \"tv\", \"pause\", \"video\", \"controls\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188408)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1.5C0 0.671573 0.671573 0 1.5 0H4C4.82843 0 5.5 0.671573 5.5 1.5V12.5C5.5 13.3284 4.82843 14 4 14H1.5C0.671573 14 0 13.3284 0 12.5V1.5ZM10 0C9.17157 0 8.5 0.671573 8.5 1.5V12.5C8.5 13.3284 9.17157 14 10 14H12.5C13.3284 14 14 13.3284 14 12.5V1.5C14 0.671573 13.3284 0 12.5 0H10Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188408\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"button-play\", \"keywords\": [ \"button\", \"television\", \"buttons\", \"movies\", \"play\", \"tv\", \"video\", \"controls\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188438)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.67562 0.019165C2.37986 0.019165 2.08905 0.0945525 1.83063 0.238113C1.56749 0.372252 1.34538 0.575009 1.18786 0.825043C1.02807 1.07867 0.940861 1.37125 0.935696 1.67097L0.935621 1.67958V12.3396L0.935696 12.3482C0.940861 12.6479 1.02807 12.9405 1.18786 13.1941C1.34538 13.4442 1.56749 13.6469 1.83063 13.7811C2.08905 13.9246 2.37986 14 2.67562 14C2.97557 14 3.27043 13.9225 3.53158 13.7749C3.53733 13.7717 3.54301 13.7683 3.54862 13.7648L12.1784 8.42735C12.4401 8.29546 12.661 8.09453 12.8172 7.84605C12.9783 7.58946 13.0639 7.2926 13.0639 6.98958C13.0639 6.68657 12.9783 6.3897 12.8172 6.13312C12.6609 5.88438 12.4396 5.68328 12.1776 5.5514L3.5472 0.253465C3.54205 0.250305 3.53684 0.247238 3.53158 0.244266C3.27043 0.0967073 2.97557 0.019165 2.67562 0.019165Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188438\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"button-power-1\", \"keywords\": [ \"power\", \"button\", \"on\", \"off\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188426)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8 1C8 0.447715 7.55228 0 7 0C6.44772 0 6 0.447715 6 1V6.07692C6 6.62921 6.44772 7.07692 7 7.07692C7.55228 7.07692 8 6.62921 8 6.07692V1ZM3.92308 3.05886C4.3584 2.71899 4.43579 2.09057 4.09592 1.65525C3.75605 1.21992 3.12763 1.14254 2.69231 1.4824C1.54339 2.37939 0.703135 3.61265 0.288786 5.01012C-0.125563 6.40758 -0.0933355 7.89953 0.38097 9.27781C0.855276 10.6561 1.748 11.8519 2.93458 12.6984C4.12116 13.545 5.5424 14 7 14C8.4576 14 9.87884 13.545 11.0654 12.6984C12.252 11.8519 13.1447 10.6561 13.619 9.27781C14.0933 7.89954 14.1256 6.40758 13.7112 5.01012C13.2969 3.61265 12.4566 2.37939 11.3077 1.4824C10.8724 1.14254 10.2439 1.21992 9.90408 1.65525C9.56421 2.09057 9.6416 2.71899 10.0769 3.05886C10.8976 3.69956 11.4978 4.58046 11.7937 5.57865C12.0897 6.57684 12.0667 7.64252 11.7279 8.627C11.3891 9.61148 10.7514 10.4656 9.90387 11.0703C9.05632 11.675 8.04114 12 7 12C5.95886 12 4.94368 11.675 4.09613 11.0703C3.24857 10.4656 2.61091 9.61148 2.27212 8.627C1.93333 7.64252 1.91031 6.57684 2.20628 5.57865C2.50224 4.58046 3.10242 3.69956 3.92308 3.05886Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188426\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"button-previous\", \"keywords\": [ \"button\", \"television\", \"buttons\", \"movies\", \"skip\", \"previous\", \"video\", \"controls\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188414)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.23782 6.056L12.0568 0.656892C12.4524 0.406346 12.9347 0.473133 13.2832 0.735097C13.5518 0.936989 13.741 1.25481 13.7499 1.63264V12.3333V12.3376C13.7499 13.2216 12.771 13.7938 12.0568 13.3415L4.23782 7.97496C3.58731 7.53614 3.58731 6.49482 4.23782 6.056ZM0 1.74216C0 1.18987 0.447715 0.742157 0.999999 0.742157C1.55229 0.742157 2 1.18987 2 1.74216V12.2578C2 12.8101 1.55229 13.2578 0.999999 13.2578C0.447715 13.2578 -9.53699e-07 12.8101 0 12.2578V1.74216Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188414\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"button-record-3\", \"keywords\": [ \"button\", \"television\", \"buttons\", \"movies\", \"record\", \"tv\", \"video\", \"controls\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188444)\\\">\\n<path d=\\\"M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188444\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"button-rewind-1\", \"keywords\": [ \"rewind\", \"television\", \"button\", \"movies\", \"buttons\", \"tv\", \"video\", \"controls\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.50329 5.89365L12.7296 2.42362C13.0265 2.24286 13.3883 2.29104 13.6498 2.48004C13.8514 2.6257 13.9933 2.855 14 3.12759V10.8478V10.8509C14 11.4887 13.2655 11.9015 12.7296 11.5752L7.50329 8.1261V10.8478L7.5033 10.8505C7.50363 11.4885 6.76889 11.9016 6.23291 11.5752L0.366072 7.70339C-0.122024 7.3868 -0.122024 6.63551 0.366072 6.31892L6.23291 2.42362C6.52978 2.24286 6.89161 2.29104 7.15312 2.48004C7.35465 2.6257 7.4966 2.855 7.50329 3.12759V5.89365Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"button-rewind-2\", \"keywords\": [ \"rewind\", \"television\", \"button\", \"movies\", \"buttons\", \"tv\", \"video\", \"controls\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1 2.23425C0.447715 2.23425 0 2.68197 0 3.23425V10.7658C0 11.3181 0.447715 11.7658 1 11.7658C1.55228 11.7658 2 11.3181 2 10.7658V8.78177L6.23291 11.5753C6.76889 11.9016 7.50363 11.4886 7.5033 10.8505L7.50329 10.8479V8.12616L12.7296 11.5753C13.2655 11.9016 14 11.4888 14 10.851V10.8479V3.12765C13.9933 2.85506 13.8514 2.62576 13.6498 2.4801C13.3883 2.2911 13.0265 2.24292 12.7296 2.42368L7.50329 5.89371V3.12765C7.4966 2.85506 7.35465 2.62576 7.15312 2.4801C6.89161 2.2911 6.52978 2.24292 6.23291 2.42368L2 5.23413V3.23425C2 2.68197 1.55229 2.23425 1 2.23425Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"button-stop\", \"keywords\": [ \"button\", \"television\", \"buttons\", \"movies\", \"stop\", \"tv\", \"video\", \"controls\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188411)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0C0.671573 0 0 0.671573 0 1.5V12.5C0 13.3284 0.671573 14 1.5 14H12.5C13.3284 14 14 13.3284 14 12.5V1.5C14 0.671573 13.3284 0 12.5 0H1.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188411\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"camera-video\", \"keywords\": [ \"film\", \"television\", \"tv\", \"camera\", \"movies\", \"video\", \"recorder\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188471)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 3.5C0 1.567 1.567 0 3.5 0C5.433 0 7 1.567 7 3.5C7 5.433 5.433 7 3.5 7C1.567 7 0 5.433 0 3.5ZM10.5 2C9.11929 2 8 3.11929 8 4.5C8 5.88071 9.11929 7 10.5 7C11.8807 7 13 5.88071 13 4.5C13 3.11929 11.8807 2 10.5 2ZM1.91187 9.60888C1.91187 8.71874 2.63347 7.99713 3.52361 7.99713H9.63824C10.5284 7.99713 11.25 8.71874 11.25 9.60888V12.3883C11.25 13.2784 10.5284 14 9.63824 14H3.52361C2.63347 14 1.91187 13.2784 1.91187 12.3883V9.60888ZM13.8781 9.74854C13.8781 9.33432 13.5424 8.99854 13.1281 8.99854C12.7139 8.99854 12.3781 9.33432 12.3781 9.74854V12.2485C12.3781 12.6627 12.7139 12.9985 13.1281 12.9985C13.5424 12.9985 13.8781 12.6627 13.8781 12.2485V9.74854Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188471\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cards\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188423)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.56069 0.329923C7.30485 0.261407 7.0419 0.41322 6.97332 0.669039L4.63898 9.37652C4.57037 9.63243 4.72225 9.89549 4.97818 9.96403L11.0616 11.5933C11.3175 11.6618 11.5804 11.51 11.649 11.2542L13.9834 2.54668C14.052 2.29077 13.9001 2.02771 13.6442 1.95917L7.56069 0.329923ZM3.43161 9.05281L5.028 3.09802L0.355681 4.34933C0.0997529 4.41787 -0.0521275 4.68094 0.0164784 4.93685L2.35083 13.6443C2.41941 13.9001 2.68236 14.052 2.93819 13.9834L9.02169 12.3542C9.02963 12.3521 9.03748 12.3498 9.04521 12.3473L4.65481 11.1715C3.73191 10.9243 3.18421 9.97565 3.43161 9.05281Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188423\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"chess-bishop\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.91211 1.19388C5.91211 0.592229 6.39984 0.104492 7.0015 0.104492C7.60319 0.104492 8.09085 0.592268 8.09085 1.19388C8.09085 1.51329 7.95339 1.80061 7.73439 1.99988C7.93502 2.12616 8.17571 2.29414 8.41795 2.50245C8.47354 2.55026 8.52994 2.60084 8.58652 2.65418L7.48492 3.78867C7.24446 4.03631 7.25028 4.432 7.49792 4.67246C7.74556 4.91292 8.14125 4.9071 8.38171 4.65946L9.36194 3.64997C9.555 4.01724 9.68313 4.43969 9.68313 4.91475C9.68313 5.46864 9.52935 5.94387 9.22401 6.31674C9.06526 6.51061 8.8751 6.66517 8.66473 6.78627C8.66815 6.79652 8.67144 6.80682 8.67462 6.81719C8.83702 7.34689 9.07819 8.1402 9.28119 8.83518C9.3537 9.08342 9.42174 9.3205 9.47976 9.5293H4.52194C4.57996 9.3205 4.648 9.08342 4.72051 8.83518C4.92351 8.1402 5.16468 7.34689 5.32708 6.81719C5.33024 6.80686 5.33353 6.79659 5.33692 6.78638C5.12652 6.66529 4.93631 6.51071 4.77752 6.31683C4.47214 5.94395 4.31833 5.46869 4.31833 4.91475C4.31833 3.82169 4.99668 3.00711 5.58352 2.50245C5.82611 2.29383 6.06716 2.12566 6.26797 1.99932C6.04932 1.80008 5.91211 1.513 5.91211 1.19388ZM3.90582 10.7793C3.43711 10.8143 2.97835 11.083 2.79447 11.5488L2.18337 13.0967C2.12259 13.2507 2.14194 13.4247 2.23504 13.5616C2.32814 13.6984 2.48292 13.7803 2.64844 13.7803H11.3549C11.5204 13.7803 11.6752 13.6984 11.7683 13.5616C11.8614 13.4247 11.8808 13.2507 11.82 13.0967L11.2089 11.5488C11.025 11.083 10.5662 10.8143 10.0975 10.7793H3.90582Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"chess-king\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.30054 0.542236C7.30054 0.266094 7.07669 0.0422363 6.80054 0.0422363C6.5244 0.0422363 6.30054 0.266094 6.30054 0.542236V0.780029H6.06299C5.78685 0.780029 5.56299 1.00389 5.56299 1.28003C5.56299 1.55617 5.78685 1.78003 6.06299 1.78003H6.30054V2.34686C5.52011 2.51516 4.86754 3.06899 4.66345 3.81836H4.4783C4.11743 3.81836 3.78695 3.97205 3.57264 4.22931C3.35474 4.49088 3.26026 4.86259 3.40188 5.23019L4.40552 7.83527C4.4244 7.88426 4.45043 7.92901 4.48216 7.96853L4.26483 9.5293H9.23466L9.02942 8.05532C9.10281 8.00022 9.16123 7.92482 9.19573 7.83527L10.1994 5.23019C10.341 4.86259 10.2466 4.49088 10.0287 4.22931C9.81431 3.97205 9.48382 3.81836 9.12295 3.81836H8.93739C8.73332 3.06906 8.08087 2.51528 7.30054 2.34691V1.78003H7.53792C7.81406 1.78003 8.03792 1.55617 8.03792 1.28003C8.03792 1.00389 7.81406 0.780029 7.53792 0.780029H7.30054V0.542236ZM9.89638 10.7793H3.70466C3.23594 10.8143 2.77718 11.083 2.5933 11.5488L1.9822 13.0967C1.92142 13.2507 1.94077 13.4247 2.03387 13.5616C2.12697 13.6984 2.28176 13.7803 2.44727 13.7803H11.1538C11.3193 13.7803 11.4741 13.6984 11.5672 13.5616C11.6603 13.4247 11.6797 13.2507 11.6189 13.0967L11.0078 11.5488C10.8239 11.083 10.3651 10.8143 9.89638 10.7793Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"chess-knight\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.08767 4.15946C2.66911 4.86954 2.80861 5.77618 3.42126 6.3276C3.88601 6.74589 4.53126 6.87547 5.10979 6.68985C6.00299 6.05907 6.90487 5.11042 7.2945 3.82626C7.37467 3.56202 7.65388 3.4128 7.91813 3.49297C8.18238 3.57315 8.3316 3.85236 8.25142 4.11661C7.79652 5.61586 6.78418 6.70158 5.80885 7.41855C5.68176 7.53664 5.53967 7.65805 5.38097 7.78225C4.89835 8.15993 4.26243 8.67592 3.78598 9.22575C3.65778 9.37368 3.62771 9.58282 3.70902 9.76089C3.79034 9.93895 3.96809 10.0532 4.16384 10.0532H10.6784C10.9231 10.0532 11.1318 9.87616 11.1717 9.63479C11.6684 6.63232 11.5452 4.70812 11.1001 3.39066C10.646 2.04633 9.87406 1.38919 9.24332 0.898611C8.62154 0.414997 7.60758 0.162264 6.70143 0.058792C5.79261 -0.0449854 4.8251 -0.0167517 4.25132 0.186613C4.11999 0.233163 4.01403 0.33241 3.959 0.460428C3.90397 0.588447 3.90486 0.733621 3.96145 0.860956L4.42165 1.89639L3.08767 4.15946ZM1.71965 12.0939C1.92519 11.5051 2.48104 11.1094 3.10627 11.1094H10.9558C11.5851 11.1094 12.1441 11.5102 12.3464 12.1054L12.7969 13.3269C12.8535 13.4803 12.8315 13.6517 12.738 13.7858C12.6445 13.92 12.4913 13.9999 12.3278 13.9999H1.67188C1.50448 13.9999 1.34819 13.9161 1.25551 13.7767C1.16282 13.6373 1.14605 13.4608 1.21083 13.3064L1.71965 12.0939Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"chess-pawn\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.3556 3.14052C4.3556 1.68018 5.53944 0.496338 6.99977 0.496338C8.46011 0.496338 9.64395 1.68018 9.64395 3.14052C9.64395 3.65219 9.49862 4.12991 9.24698 4.53467H9.57279C9.987 4.53467 10.3228 4.87045 10.3228 5.28467C10.3228 5.69888 9.987 6.03467 9.57279 6.03467H8.88574C8.99234 6.78303 9.15067 7.69683 9.35485 8.57608C9.43107 8.90433 9.51312 9.22529 9.6005 9.5293H4.3988C4.4862 9.22528 4.56825 8.9043 4.64447 8.57603C4.84863 7.69678 5.00695 6.78301 5.11355 6.03467H4.42676C4.01255 6.03467 3.67676 5.69888 3.67676 5.28467C3.67676 4.87045 4.01255 4.53467 4.42676 4.53467H4.75257C4.50093 4.12991 4.3556 3.65219 4.3556 3.14052ZM3.90392 10.7793C3.43519 10.8142 2.97641 11.083 2.79252 11.5488L2.18142 13.0967C2.12064 13.2507 2.13999 13.4247 2.23309 13.5616C2.32619 13.6984 2.48098 13.7803 2.64649 13.7803H11.353C11.5185 13.7803 11.6733 13.6984 11.7664 13.5616C11.8595 13.4247 11.8789 13.2507 11.8181 13.0967L11.207 11.5488C11.0231 11.083 10.5643 10.8142 10.0956 10.7793H3.90392Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"cloud-gaming-1\", \"keywords\": [ \"entertainment\", \"cloud\", \"gaming\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 7.00003C0 4.15971 2.30254 1.85718 5.14286 1.85718C6.75715 1.85718 8.19762 2.60136 9.13983 3.76358C9.35062 3.73112 9.56634 3.71432 9.78571 3.71432C12.1132 3.71432 14 5.60112 14 7.92861C14 10.2561 12.1132 12.1429 9.78571 12.1429H5.14286C2.30254 12.1429 0 9.84036 0 7.00003ZM4.80309 6.39026C4.80309 6.04508 4.52327 5.76526 4.17809 5.76526C3.83291 5.76526 3.55309 6.04508 3.55309 6.39026V7.23079H2.71255C2.36738 7.23079 2.08755 7.51061 2.08755 7.85579C2.08755 8.20097 2.36738 8.48079 2.71255 8.48079H3.55309V9.32133C3.55309 9.6665 3.83291 9.94633 4.17809 9.94633C4.52327 9.94633 4.80309 9.6665 4.80309 9.32133V8.48079H5.64362C5.9888 8.48079 6.26862 8.20097 6.26862 7.85579C6.26862 7.51061 5.9888 7.23079 5.64362 7.23079H4.80309V6.39026ZM9.35135 7.82446C9.35135 8.23868 9.01556 8.57446 8.60135 8.57446C8.18714 8.57446 7.85135 8.23868 7.85135 7.82446C7.85135 7.41025 8.18714 7.07446 8.60135 7.07446C9.01556 7.07446 9.35135 7.41025 9.35135 7.82446ZM11.4354 8.57446C11.8496 8.57446 12.1854 8.23868 12.1854 7.82446C12.1854 7.41025 11.8496 7.07446 11.4354 7.07446C11.0212 7.07446 10.6854 7.41025 10.6854 7.82446C10.6854 8.23868 11.0212 8.57446 11.4354 8.57446ZM10.0184 7.15747C9.60416 7.15747 9.26837 6.82168 9.26837 6.40747C9.26837 5.99326 9.60416 5.65747 10.0184 5.65747C10.4326 5.65747 10.7684 5.99326 10.7684 6.40747C10.7684 6.82168 10.4326 7.15747 10.0184 7.15747ZM9.26837 9.24146C9.26837 9.65567 9.60416 9.99146 10.0184 9.99146C10.4326 9.99146 10.7684 9.65567 10.7684 9.24146C10.7684 8.82724 10.4326 8.49146 10.0184 8.49146C9.60416 8.49146 9.26837 8.82724 9.26837 9.24146Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"clubs-symbol\", \"keywords\": [ \"entertainment\", \"gaming\", \"card\", \"clubs\", \"symbol\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188435)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 0.25C5.32342 0.25 3.96429 1.60914 3.96429 3.28571C3.96429 3.84539 4.11603 4.47239 4.37638 5.05705C4.00545 4.95132 3.63314 4.89286 3.28571 4.89286C1.60914 4.89286 0.25 6.25199 0.25 7.92857C0.25 9.60515 1.60914 10.9643 3.28571 10.9643C3.90809 10.9643 4.6143 10.7766 5.25338 10.4599L4.64723 13.4503C4.63233 13.5239 4.65122 13.6002 4.69871 13.6582C4.74619 13.7163 4.81723 13.75 4.89225 13.75H9.10775C9.18276 13.75 9.25381 13.7163 9.30129 13.6582C9.34877 13.6002 9.36767 13.5239 9.35277 13.4503L8.74662 10.4599C9.3857 10.7766 10.0919 10.9643 10.7143 10.9643C12.3909 10.9643 13.75 9.60515 13.75 7.92857C13.75 6.25199 12.3909 4.89286 10.7143 4.89286C10.3669 4.89286 9.99455 4.95132 9.62362 5.05706C9.88397 4.47239 10.0357 3.84539 10.0357 3.28571C10.0357 1.60914 8.67658 0.25 7 0.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188435\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"diamonds-symbol\", \"keywords\": [ \"entertainment\", \"gaming\", \"card\", \"diamonds\", \"symbol\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.07185 0.784927C7.54501 0.0716912 6.45442 0.0716907 5.92758 0.784927L1.89087 6.24982C1.55981 6.69801 1.55981 7.30199 1.89087 7.75018L5.92758 13.2151C6.45442 13.9283 7.54501 13.9283 8.07185 13.2151L12.1086 7.75018C12.4397 7.30199 12.4397 6.69801 12.1086 6.24982L8.07185 0.784927Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"dice-1\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188447)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 2.5C0 1.11929 1.11929 0 2.5 0H11.5C12.8807 0 14 1.11929 14 2.5V11.5C14 12.8807 12.8807 14 11.5 14H2.5C1.11929 14 0 12.8807 0 11.5V2.5ZM6.99999 8.20005C7.66273 8.20005 8.19999 7.66279 8.19999 7.00005C8.19999 6.33731 7.66273 5.80005 6.99999 5.80005C6.33725 5.80005 5.79999 6.33731 5.79999 7.00005C5.79999 7.66279 6.33725 8.20005 6.99999 8.20005Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188447\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"dice-2\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188486)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.5 0C1.11929 0 0 1.11929 0 2.5V11.5C0 12.8807 1.11929 14 2.5 14H11.5C12.8807 14 14 12.8807 14 11.5V2.5C14 1.11929 12.8807 0 11.5 0H2.5ZM5.41196 9.78813C5.41196 10.4509 4.8747 10.9881 4.21196 10.9881C3.54922 10.9881 3.01196 10.4509 3.01196 9.78813C3.01196 9.12539 3.54922 8.58813 4.21196 8.58813C4.8747 8.58813 5.41196 9.12539 5.41196 9.78813ZM9.78804 5.41196C10.4508 5.41196 10.988 4.8747 10.988 4.21196C10.988 3.54922 10.4508 3.01196 9.78804 3.01196C9.1253 3.01196 8.58804 3.54922 8.58804 4.21196C8.58804 4.8747 9.1253 5.41196 9.78804 5.41196Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188486\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"dice-3\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188468)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 2.5C0 1.11929 1.11929 0 2.5 0H11.5C12.8807 0 14 1.11929 14 2.5V11.5C14 12.8807 12.8807 14 11.5 14H2.5C1.11929 14 0 12.8807 0 11.5V2.5ZM4.21196 10.9881C4.8747 10.9881 5.41196 10.4509 5.41196 9.78813C5.41196 9.12539 4.8747 8.58813 4.21196 8.58813C3.54922 8.58813 3.01196 9.12539 3.01196 9.78813C3.01196 10.4509 3.54922 10.9881 4.21196 10.9881ZM8.19788 7.00005C8.19788 7.66279 7.66062 8.20005 6.99788 8.20005C6.33514 8.20005 5.79788 7.66279 5.79788 7.00005C5.79788 6.33731 6.33514 5.80005 6.99788 5.80005C7.66062 5.80005 8.19788 6.33731 8.19788 7.00005ZM9.78804 5.41196C10.4508 5.41196 10.988 4.8747 10.988 4.21196C10.988 3.54922 10.4508 3.01196 9.78804 3.01196C9.1253 3.01196 8.58804 3.54922 8.58804 4.21196C8.58804 4.8747 9.1253 5.41196 9.78804 5.41196Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188468\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"dice-4\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188465)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.5 0C1.11929 0 0 1.11929 0 2.5V11.5C0 12.8807 1.11929 14 2.5 14H11.5C12.8807 14 14 12.8807 14 11.5V2.5C14 1.11929 12.8807 0 11.5 0H2.5ZM10.9859 4.21196C10.9859 4.8747 10.4487 5.41196 9.78594 5.41196C9.1232 5.41196 8.58594 4.8747 8.58594 4.21196C8.58594 3.54922 9.1232 3.01196 9.78594 3.01196C10.4487 3.01196 10.9859 3.54922 10.9859 4.21196ZM9.78594 10.9881C10.4487 10.9881 10.9859 10.4509 10.9859 9.78813C10.9859 9.12539 10.4487 8.58813 9.78594 8.58813C9.1232 8.58813 8.58594 9.12539 8.58594 9.78813C8.58594 10.4509 9.1232 10.9881 9.78594 10.9881ZM5.4141 4.21196C5.4141 4.8747 4.87684 5.41196 4.2141 5.41196C3.55136 5.41196 3.0141 4.8747 3.0141 4.21196C3.0141 3.54922 3.55136 3.01196 4.2141 3.01196C4.87684 3.01196 5.4141 3.54922 5.4141 4.21196ZM4.2141 10.9881C4.87684 10.9881 5.4141 10.4509 5.4141 9.78813C5.4141 9.12539 4.87684 8.58813 4.2141 8.58813C3.55136 8.58813 3.0141 9.12539 3.0141 9.78813C3.0141 10.4509 3.55136 10.9881 4.2141 10.9881Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188465\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"dice-5\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188492)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 2.5C0 1.11929 1.11929 0 2.5 0H11.5C12.8807 0 14 1.11929 14 2.5V11.5C14 12.8807 12.8807 14 11.5 14H2.5C1.11929 14 0 12.8807 0 11.5V2.5ZM9.78594 5.41196C10.4487 5.41196 10.9859 4.8747 10.9859 4.21196C10.9859 3.54922 10.4487 3.01196 9.78594 3.01196C9.1232 3.01196 8.58594 3.54922 8.58594 4.21196C8.58594 4.8747 9.1232 5.41196 9.78594 5.41196ZM10.9859 9.78813C10.9859 10.4509 10.4487 10.9881 9.78594 10.9881C9.1232 10.9881 8.58594 10.4509 8.58594 9.78813C8.58594 9.12539 9.1232 8.58813 9.78594 8.58813C10.4487 8.58813 10.9859 9.12539 10.9859 9.78813ZM6.99999 8.20212C7.66273 8.20212 8.19999 7.66487 8.19999 7.00212C8.19999 6.33938 7.66273 5.80212 6.99999 5.80212C6.33725 5.80212 5.79999 6.33938 5.79999 7.00212C5.79999 7.66487 6.33725 8.20212 6.99999 8.20212ZM5.4141 4.21196C5.4141 4.8747 4.87684 5.41196 4.2141 5.41196C3.55136 5.41196 3.0141 4.8747 3.0141 4.21196C3.0141 3.54922 3.55136 3.01196 4.2141 3.01196C4.87684 3.01196 5.4141 3.54922 5.4141 4.21196ZM4.2141 10.9881C4.87684 10.9881 5.4141 10.4509 5.4141 9.78813C5.4141 9.12539 4.87684 8.58813 4.2141 8.58813C3.55136 8.58813 3.0141 9.12539 3.0141 9.78813C3.0141 10.4509 3.55136 10.9881 4.2141 10.9881Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188492\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"dice-6\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188456)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.5 0C1.11929 0 0 1.11929 0 2.5V11.5C0 12.8807 1.11929 14 2.5 14H11.5C12.8807 14 14 12.8807 14 11.5V2.5C14 1.11929 12.8807 0 11.5 0H2.5ZM5.23545 3.57085C5.23545 4.23359 4.69819 4.77085 4.03545 4.77085C3.37271 4.77085 2.83545 4.23359 2.83545 3.57085C2.83545 2.90811 3.37271 2.37085 4.03545 2.37085C4.69819 2.37085 5.23545 2.90811 5.23545 3.57085ZM4.03545 11.6295C4.69819 11.6295 5.23545 11.0922 5.23545 10.4295C5.23545 9.76675 4.69819 9.22949 4.03545 9.22949C3.37271 9.22949 2.83545 9.76675 2.83545 10.4295C2.83545 11.0922 3.37271 11.6295 4.03545 11.6295ZM5.23545 7.00029C5.23545 7.66303 4.69819 8.20029 4.03545 8.20029C3.37271 8.20029 2.83545 7.66303 2.83545 7.00029C2.83545 6.33755 3.37271 5.80029 4.03545 5.80029C4.69819 5.80029 5.23545 6.33755 5.23545 7.00029ZM9.96453 4.77085C10.6273 4.77085 11.1645 4.23359 11.1645 3.57085C11.1645 2.90811 10.6273 2.37085 9.96453 2.37085C9.30178 2.37085 8.76453 2.90811 8.76453 3.57085C8.76453 4.23359 9.30178 4.77085 9.96453 4.77085ZM11.1645 10.4295C11.1645 11.0922 10.6273 11.6295 9.96453 11.6295C9.30178 11.6295 8.76453 11.0922 8.76453 10.4295C8.76453 9.76675 9.30178 9.22949 9.96453 9.22949C10.6273 9.22949 11.1645 9.76675 11.1645 10.4295ZM9.96453 8.20029C10.6273 8.20029 11.1645 7.66303 11.1645 7.00029C11.1645 6.33755 10.6273 5.80029 9.96453 5.80029C9.30178 5.80029 8.76453 6.33755 8.76453 7.00029C8.76453 7.66303 9.30178 8.20029 9.96453 8.20029Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188456\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"dices-entertainment-gaming-dices\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188459)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.25 1.25C0.25 0.697715 0.697715 0.25 1.25 0.25H7.68667C8.23895 0.25 8.68667 0.697715 8.68667 1.25V4.06335H6.31335C5.07071 4.06335 4.06335 5.07071 4.06335 6.31335V8.68667H1.25C0.697715 8.68667 0.25 8.23895 0.25 7.68667V1.25ZM2.80146 3.74869C2.78947 3.74926 2.77741 3.74954 2.76528 3.74954C2.20577 3.74954 1.7522 3.29597 1.7522 2.73647L1.75222 2.72985L1.7522 2.72327C1.7522 2.17098 2.19991 1.72327 2.7522 1.72327L2.76587 1.72314C3.32538 1.72314 3.77895 2.17671 3.77895 2.73622C3.77895 3.28382 3.34449 3.72994 2.80146 3.74869ZM6.31335 5.31335C5.76107 5.31335 5.31335 5.76107 5.31335 6.31335V12.75C5.31335 13.3023 5.76107 13.75 6.31335 13.75H12.75C13.3023 13.75 13.75 13.3023 13.75 12.75V6.31335C13.75 5.76107 13.3023 5.31335 12.75 5.31335H6.31335ZM10.8815 8.97156C11.4338 8.97156 11.8815 8.52384 11.8815 7.97156C11.8815 7.41927 11.4338 6.97156 10.8815 6.97156C10.3292 6.97156 9.88147 7.41927 9.88147 7.97156C9.88147 8.52384 10.3292 8.97156 10.8815 8.97156ZM9.18182 11.0918C9.18182 11.6441 8.73411 12.0918 8.18182 12.0918C7.62954 12.0918 7.18182 11.6441 7.18182 11.0918C7.18182 10.5395 7.62954 10.0918 8.18182 10.0918C8.73411 10.0918 9.18182 10.5395 9.18182 11.0918Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188459\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"earpods\", \"keywords\": [ \"airpods\", \"audio\", \"earpods\", \"music\", \"earbuds\", \"true\", \"wireless\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188462)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.4595 0.707023C12.9458 0.366597 12.3402 0.182683 11.7174 0.183597C10.8831 0.183597 10.083 0.515006 9.49313 1.10492C8.90321 1.69483 8.5718 2.49492 8.5718 3.32919V12.5458C8.5718 12.8934 8.70989 13.2267 8.95569 13.4725C9.20149 13.7183 9.53486 13.8564 9.88247 13.8564C10.2301 13.8564 10.5634 13.7183 10.8092 13.4725C11.055 13.2267 11.1931 12.8934 11.1931 12.5458V6.47478C11.3675 6.49075 11.543 6.49075 11.7174 6.47478C12.3402 6.47569 12.9458 6.29178 13.4595 5.95135C13.6973 5.79379 13.8144 5.51475 13.8144 5.2295V1.42887C13.8144 1.14363 13.6973 0.864588 13.4595 0.707023ZM0.538494 0.707054C1.05223 0.366628 1.65779 0.182714 2.28065 0.183628C3.11491 0.183628 3.915 0.515037 4.50492 1.10495C5.09483 1.69486 5.42624 2.49495 5.42624 3.32922V12.5458C5.42624 12.8934 5.28815 13.2268 5.04235 13.4726C4.79656 13.7184 4.46319 13.8565 4.11558 13.8565C3.76797 13.8565 3.4346 13.7184 3.1888 13.4726C2.943 13.2268 2.80491 12.8934 2.80491 12.5458V6.47481C2.63053 6.49078 2.45504 6.49078 2.28065 6.47481C1.65779 6.47572 1.05223 6.29181 0.538494 5.95138C0.300713 5.79382 0.183594 5.51478 0.183594 5.22953V1.4289C0.183594 1.14366 0.300713 0.864618 0.538494 0.707054Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188462\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"epic-games-1\", \"keywords\": [ \"epic\", \"games\", \"entertainment\", \"gaming\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188511)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.04791 0C1.26218 0 0.722656 0.696666 0.722656 1.42857V9.67611C0.722656 10.1872 0.979004 10.6866 1.43369 10.9424L6.75431 13.9358C6.90653 14.0214 7.09241 14.0214 7.24463 13.9358L12.5652 10.9424C13.0199 10.6866 13.2763 10.1872 13.2763 9.67611V1.42857C13.2763 0.696666 12.7367 0 11.951 0H2.04791ZM4.98306 2.7074C4.98306 2.36222 5.26288 2.0824 5.60806 2.0824H8.39378C8.73895 2.0824 9.01878 2.36222 9.01878 2.7074C9.01878 3.05258 8.73895 3.3324 8.39378 3.3324H6.23306V4.40383H8.39378C8.73895 4.40383 9.01878 4.68365 9.01878 5.02883C9.01878 5.374 8.73895 5.65383 8.39378 5.65383H6.23306V6.72525H8.39378C8.73895 6.72525 9.01878 7.00508 9.01878 7.35025C9.01878 7.69543 8.73895 7.97525 8.39378 7.97525H5.60806C5.26288 7.97525 4.98306 7.69543 4.98306 7.35025V5.02883V2.7074ZM5.44526 9.68778C5.14318 9.52076 4.7629 9.63025 4.59589 9.93233C4.42887 10.2344 4.53836 10.6147 4.84044 10.7817L6.69758 11.8085C6.88577 11.9126 7.11422 11.9126 7.30241 11.8085L9.15955 10.7817C9.46163 10.6147 9.57112 10.2344 9.4041 9.93233C9.23708 9.63025 8.8568 9.52076 8.55472 9.68778L6.99999 10.5474L5.44526 9.68778Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188511\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"esports\", \"keywords\": [ \"entertainment\", \"gaming\", \"esports\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188477)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.46869 2.25617V2.28024V3.53087C3.46869 3.57658 3.47216 3.62147 3.47886 3.6653C3.48172 3.68401 3.48516 3.70252 3.48918 3.72082C3.51387 3.83342 3.56007 3.93797 3.62324 4.02995C3.78234 4.26165 4.04912 4.4136 4.35142 4.4136H5.09953L8.90053 4.41356L9.64778 4.4136C9.95008 4.4136 10.2169 4.26165 10.376 4.02995C10.4735 3.88798 10.5305 3.71607 10.5305 3.53083V1.28716C10.5305 1.27556 10.5297 1.26421 10.5283 1.25314C10.526 1.23616 10.5221 1.21985 10.5167 1.20434C10.4693 1.06818 10.3086 0.993639 10.1687 1.06359L9.72149 1.28719L9.30203 1.49692L9.28051 1.50768L8.76503 1.76542L7.17635 0.176739C7.07872 0.0791076 6.92043 0.0791081 6.8228 0.176739L5.23412 1.76542L4.71869 1.5077L4.69717 1.49694L4.2777 1.2872L3.83049 1.06359C3.66427 0.980481 3.46869 1.10135 3.46869 1.2872V1.78719V2.25617ZM2.2795 4.03862C1.82041 4.09089 1.39448 4.37765 1.21428 4.84641C0.865812 5.75293 0.558986 6.9731 0.341987 8.11499C0.126011 9.2515 -0.0102728 10.3592 0.000606675 11.0381C0.0128946 11.8049 0.130229 12.5157 0.510743 13.0397C0.918876 13.6018 1.5528 13.8516 2.35701 13.8516C2.43469 13.8516 2.5113 13.8335 2.58076 13.7987C3.18548 13.4961 3.69874 12.9153 4.1277 12.3838C4.25711 12.2234 4.37931 12.0672 4.4966 11.9172C4.74635 11.5978 4.9739 11.3068 5.20106 11.0658H8.7906C8.99576 11.2971 9.20292 11.5768 9.43147 11.8854C9.54801 12.0427 9.67015 12.2077 9.80039 12.3775C10.2094 12.9108 10.7077 13.5026 11.2996 13.7987C11.369 13.8335 11.4456 13.8516 11.5233 13.8516C12.3257 13.8516 12.9777 13.604 13.4149 13.0587C13.8331 12.5372 13.9868 11.8246 13.9994 11.0381C14.0102 10.3644 13.8761 9.26866 13.6631 8.142C13.4491 7.00973 13.1464 5.79825 12.8022 4.88972C12.6193 4.40697 12.188 4.10852 11.7185 4.04344C11.489 4.97374 10.649 5.6636 9.64778 5.6636H4.35142C3.34851 5.6636 2.50729 4.97136 2.2795 4.03862ZM5.60836 8.61213C5.60836 9.33382 5.02331 9.91887 4.30162 9.91887C3.57992 9.91887 2.99487 9.33382 2.99487 8.61213C2.99487 7.89043 3.57992 7.30538 4.30162 7.30538C5.02331 7.30538 5.60836 7.89043 5.60836 8.61213ZM9.69841 9.91887C10.4201 9.91887 11.0052 9.33382 11.0052 8.61213C11.0052 7.89043 10.4201 7.30538 9.69841 7.30538C8.97671 7.30538 8.39166 7.89043 8.39166 8.61213C8.39166 9.33382 8.97671 9.91887 9.69841 9.91887Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188477\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"fireworks-rocket\", \"keywords\": [ \"hobby\", \"entertainment\", \"party\", \"fireworks\", \"rocket\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188535)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.33344 1.47237C8.25278 1.39219 8.19073 1.29628 8.15395 1.19495C8.11716 1.09362 8.10705 0.990701 8.12469 0.897247C8.1375 0.800868 8.17789 0.716342 8.24154 0.65269C8.30519 0.589038 8.38971 0.548651 8.48609 0.535837L13.0873 0.106967C13.1763 0.0979022 13.2719 0.112657 13.365 0.149869C13.4582 0.187081 13.546 0.245552 13.6203 0.319883C13.6946 0.394215 13.7531 0.482013 13.7903 0.575174C13.8275 0.668335 13.8423 0.763857 13.8332 0.852923L13.417 5.46673C13.4042 5.56311 13.3638 5.64764 13.3001 5.71129C13.2365 5.77494 13.152 5.81533 13.0556 5.82814C12.9646 5.84534 12.8646 5.83625 12.7658 5.80179C12.667 5.76734 12.5729 5.70876 12.4931 5.63202L8.33344 1.47237ZM7.27864 8.58501L7.10778 8.75697C6.57631 9.29187 6.57479 10.155 7.10437 10.6918L7.53312 11.1264C7.77556 11.3721 7.77288 11.7678 7.52716 12.0102C7.28145 12.2526 6.88573 12.25 6.6433 12.0043L6.21455 11.5697C5.61567 10.9627 5.37257 10.137 5.48476 9.35252L1.2935 13.5438C1.04942 13.7879 0.653699 13.7879 0.409621 13.5438C0.165543 13.2997 0.165543 12.904 0.409621 12.6599L5.88152 7.188L4.5407 5.84708C4.3265 5.63288 4.35047 5.27893 4.59158 5.09555L7.77111 2.67743L11.275 6.18131L8.85686 9.36083C8.67348 9.60195 8.31954 9.62591 8.10534 9.41171L7.27864 8.58501ZM9.58179 10.1358C9.82587 9.89171 10.2216 9.89171 10.4657 10.1358L10.917 10.5871C11.161 10.8311 11.161 11.2269 10.917 11.4709C10.6729 11.715 10.2772 11.715 10.0331 11.4709L9.58179 11.0197C9.33772 10.7756 9.33772 10.3799 9.58179 10.1358ZM12.3824 10.1358C12.6265 9.89171 13.0222 9.89171 13.2663 10.1358C13.5104 10.3799 13.5104 10.7756 13.2663 11.0197L12.815 11.4709C12.5709 11.715 12.1752 11.715 11.9311 11.4709C11.687 11.2269 11.687 10.8311 11.9311 10.5871L12.3824 10.1358ZM9.58179 13.5438C9.82587 13.7879 10.2216 13.7879 10.4657 13.5438L10.917 13.0925C11.161 12.8484 11.161 12.4527 10.917 12.2086C10.6729 11.9645 10.2772 11.9645 10.0331 12.2086L9.58179 12.6599C9.33772 12.904 9.33772 13.2997 9.58179 13.5438ZM12.3824 13.5438C12.6265 13.7879 13.0222 13.7879 13.2663 13.5438C13.5104 13.2997 13.5104 12.904 13.2663 12.6599L12.815 12.2086C12.5709 11.9645 12.1752 11.9645 11.9311 12.2086C11.687 12.4527 11.687 12.8484 11.9311 13.0925L12.3824 13.5438Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188535\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"gameboy\", \"keywords\": [ \"entertainment\", \"gaming\", \"device\", \"gameboy\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.557 14C12.1093 14 12.557 13.5523 12.557 13V1C12.557 0.447715 12.1093 0 11.557 0H2.44141C1.88912 0 1.44141 0.447715 1.44141 1V13C1.44141 13.5523 1.88912 14 2.44141 14H11.557ZM9.41148 7.60837C9.96376 7.60837 10.4115 7.16065 10.4115 6.60837V2.92622C10.4115 2.37394 9.96376 1.92622 9.41148 1.92622H4.5879C4.03562 1.92622 3.5879 2.37394 3.5879 2.92622V6.60837C3.5879 7.16065 4.03562 7.60837 4.5879 7.60837H9.41148ZM4.2738 11.5444C4.2738 11.8896 4.55363 12.1694 4.8988 12.1694C5.24398 12.1694 5.5238 11.8896 5.5238 11.5444V11.2097H5.85885C6.20402 11.2097 6.48385 10.9299 6.48385 10.5847C6.48385 10.2395 6.20402 9.95972 5.85885 9.95972H5.5238V9.62492C5.5238 9.27974 5.24398 8.99992 4.8988 8.99992C4.55363 8.99992 4.2738 9.27974 4.2738 9.62492V9.95972H3.93933C3.59415 9.95972 3.31433 10.2395 3.31433 10.5847C3.31433 10.9299 3.59415 11.2097 3.93933 11.2097H4.2738V11.5444ZM9.49939 11.4708C10.0517 11.4708 10.4994 11.0231 10.4994 10.4708C10.4994 9.9185 10.0517 9.47079 9.49939 9.47079C8.94711 9.47079 8.49939 9.9185 8.49939 10.4708C8.49939 11.0231 8.94711 11.4708 9.49939 11.4708Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"gramophone\", \"keywords\": [ \"music\", \"audio\", \"note\", \"gramophone\", \"player\", \"vintage\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188514)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.72361 0.0528087C9.52139 -0.0482977 9.27649 -0.00113446 9.1263 0.16784L5.1263 4.66784C4.99548 4.815 4.96322 5.02523 5.04388 5.20485C5.12454 5.38447 5.3031 5.50002 5.5 5.50002C6.63086 5.50002 8.0256 5.95797 9.16968 6.83444C9.83659 7.34535 10.4065 7.98874 10.79 8.75275H13.9337C13.6909 5.65253 12.8169 3.57404 11.916 2.22267C11.3872 1.42948 10.854 0.894155 10.4451 0.553412C10.2277 0.372281 9.99707 0.198044 9.7464 0.0646304L9.72361 0.0528087ZM1.5 10H8.42761C8.44695 10.0018 8.46655 10.0027 8.48636 10.0027H13.9949C13.9983 10.166 14 10.3318 14 10.5V12.5C14 12.8978 13.842 13.2794 13.5607 13.5607C13.2794 13.842 12.8978 14 12.5 14H1.5C1.10217 14 0.720644 13.842 0.43934 13.5607C0.158035 13.2794 0 12.8978 0 12.5V11.5C0 11.1022 0.158035 10.7207 0.43934 10.4394C0.720644 10.1581 1.10217 10 1.5 10ZM0.986328 7.25C0.572115 7.25 0.236328 7.58579 0.236328 8C0.236328 8.41421 0.572114 8.75 0.986328 8.75H6.49998C6.91419 8.75 7.24998 8.41421 7.24998 8C7.24998 7.58579 6.91419 7.25 6.49998 7.25H0.986328Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188514\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"hearts-symbol\", \"keywords\": [ \"entertainment\", \"gaming\", \"card\", \"hearts\", \"symbol\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.78858 1.31418C4.77652 1.33468 5.87358 1.8046 7.00235 2.8736C8.12919 1.80663 9.22481 1.33822 10.2117 1.31878C11.2527 1.29827 12.1304 1.77832 12.7476 2.4982C13.9663 3.91959 14.2182 6.3481 12.6899 7.87633L12.6891 7.87715L8.44191 12.0851C7.63233 12.8872 6.37235 12.8872 5.56277 12.0851L1.31553 7.87715L1.31471 7.87632C-0.217284 6.34431 0.0318571 3.91384 1.2509 2.49204C1.86824 1.77203 2.74659 1.29255 3.78858 1.31418Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"music-equalizer\", \"keywords\": [ \"music\", \"audio\", \"note\", \"wave\", \"sound\", \"equalizer\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188508)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.75 0.851074C7.75 0.436861 7.41421 0.101074 7 0.101074C6.58579 0.101074 6.25 0.436861 6.25 0.851074V13.149C6.25 13.5632 6.58579 13.899 7 13.899C7.41421 13.899 7.75 13.5632 7.75 13.149V0.851074ZM4 2.46606C4.41421 2.46606 4.75 2.80185 4.75 3.21606V10.784C4.75 11.1983 4.41421 11.534 4 11.534C3.58579 11.534 3.25 11.1983 3.25 10.784V3.21606C3.25 2.80185 3.58579 2.46606 4 2.46606ZM1 4.83105C1.41421 4.83105 1.75 5.16684 1.75 5.58105V8.41905C1.75 8.83326 1.41421 9.16905 1 9.16905C0.585786 9.16905 0.25 8.83326 0.25 8.41905V5.58105C0.25 5.16684 0.585786 4.83105 1 4.83105ZM10 2.46606C10.4142 2.46606 10.75 2.80185 10.75 3.21606V10.784C10.75 11.1983 10.4142 11.534 10 11.534C9.58579 11.534 9.25 11.1983 9.25 10.784V3.21606C9.25 2.80185 9.58579 2.46606 10 2.46606ZM13.75 5.58105C13.75 5.16684 13.4142 4.83105 13 4.83105C12.5858 4.83105 12.25 5.16684 12.25 5.58105V8.41905C12.25 8.83326 12.5858 9.16905 13 9.16905C13.4142 9.16905 13.75 8.83326 13.75 8.41905V5.58105Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188508\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"music-note-1\", \"keywords\": [ \"music\", \"audio\", \"note\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.44412 0.126343C5.89184 0.126343 5.44412 0.574058 5.44412 1.12634V7.71515C5.07505 7.56623 4.6718 7.48425 4.24939 7.48425C2.48501 7.48425 1.05469 8.91457 1.05469 10.679C1.05469 12.4433 2.48501 13.8737 4.24939 13.8737C6.01378 13.8737 7.4441 12.4433 7.4441 10.679C7.4441 10.6732 7.44408 10.6675 7.44405 10.6618C7.4441 10.6577 7.44412 10.6537 7.44412 10.6496V2.23056C7.69757 2.28406 7.94596 2.35789 8.18644 2.45141C8.73707 2.66554 9.23346 2.9779 9.64874 3.36779C10.0639 3.75756 10.3896 4.21684 10.6106 4.71762C10.8314 5.21821 10.9441 5.75232 10.9441 6.29002C10.9441 6.8423 11.3918 7.29002 11.9441 7.29002C12.4964 7.29002 12.9441 6.8423 12.9441 6.29002C12.9441 5.47152 12.7723 4.66269 12.4404 3.91031C12.1085 3.15812 11.624 2.47892 11.0177 1.9097C10.4115 1.34061 9.69527 0.892263 8.91132 0.587398C8.12744 0.282559 7.28926 0.126343 6.44412 0.126343Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"music-note-2\", \"keywords\": [ \"music\", \"audio\", \"note\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188480)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.7818 0.230301C12.5975 0.194095 12.4074 0.200659 12.2261 0.249486L12.2206 0.251L4.58713 2.36927C4.32906 2.43956 4.10098 2.59225 3.93765 2.80414C3.77359 3.01697 3.68393 3.27776 3.68242 3.54648L3.68241 3.55069V3.76768V5.50551V9.0391C3.38266 8.91031 3.05239 8.83899 2.70546 8.83899C1.33748 8.83899 0.228516 9.94796 0.228516 11.3159C0.228516 12.6839 1.33748 13.7929 2.70546 13.7929C4.07344 13.7929 5.18241 12.6839 5.18241 11.3159L5.18239 11.306L5.18241 11.3008V6.07558L12.2724 4.10613V6.56986C11.9727 6.44107 11.6424 6.36975 11.2955 6.36975C9.92748 6.36975 8.81851 7.47872 8.81851 8.8467C8.81851 10.2147 9.92748 11.3236 11.2955 11.3236C12.6129 11.3236 13.6901 10.2951 13.7679 8.99728C13.7709 8.97015 13.7724 8.94258 13.7724 8.91466V8.85103V8.8467V8.84237V3.1194V1.43182V1.42972V1.40247C13.7724 1.36396 13.7695 1.32582 13.7637 1.28833C13.747 1.14891 13.7064 1.01303 13.6434 0.886749C13.5596 0.7187 13.4383 0.572131 13.2889 0.458326C13.1396 0.344518 12.9661 0.266508 12.7818 0.230301Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188480\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"music-note-off-1\", \"keywords\": [ \"music\", \"audio\", \"note\", \"off\", \"mute\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188498)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.21967 1.28033C-0.0732233 0.987437 -0.0732233 0.512563 0.21967 0.21967C0.512563 -0.0732233 0.987437 -0.0732233 1.28033 0.21967L5.39917 4.33851V1.18457C5.39917 0.632286 5.84689 0.18457 6.39917 0.18457C7.2385 0.18457 8.07093 0.339712 8.84945 0.642465C9.62803 0.945243 10.3394 1.39053 10.9414 1.95577C11.5436 2.52113 12.0249 3.19576 12.3546 3.94291C12.6843 4.69025 12.8549 5.49367 12.8549 6.30672C12.8549 6.85901 12.4072 7.30672 11.8549 7.30672C11.3027 7.30672 10.8549 6.85901 10.8549 6.30672C10.8549 5.77447 10.7434 5.24577 10.5247 4.75021C10.306 4.25447 9.98357 3.79977 9.57252 3.41386C9.16134 3.02782 8.66983 2.71852 8.12456 2.50648C7.8895 2.41506 7.64679 2.34265 7.39917 2.28983V6.33851L13.7803 12.7197C14.0732 13.0126 14.0732 13.4874 13.7803 13.7803C13.4874 14.0732 13.0126 14.0732 12.7197 13.7803L0.21967 1.28033ZM4.22217 7.51868C4.40345 7.51868 4.58115 7.53403 4.75404 7.56351L7.31905 10.1285C7.34853 10.3014 7.36389 10.4791 7.36389 10.6604C7.36389 12.3955 5.95729 13.8021 4.22217 13.8021C2.48704 13.8021 1.08044 12.3955 1.08044 10.6604C1.08044 8.92527 2.48704 7.51868 4.22217 7.51868Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188498\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"music-note-off-2\", \"keywords\": [ \"music\", \"audio\", \"note\", \"off\", \"mute\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188523)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.21967 1.28033C-0.0732233 0.987437 -0.0732233 0.512563 0.21967 0.21967C0.512563 -0.0732233 0.987437 -0.0732233 1.28033 0.21967L3.89853 2.83787C4.05725 2.65103 4.26902 2.51612 4.50637 2.45147L12.0832 0.348935C12.263 0.300505 12.4516 0.293995 12.6344 0.329907C12.8171 0.365818 12.9892 0.443194 13.1374 0.556076C13.2855 0.668955 13.4058 0.814331 13.4889 0.981012C13.5721 1.14769 13.6158 1.33123 13.6169 1.51748V1.52165V8.70246C13.6209 8.75984 13.6229 8.81776 13.6229 8.87616C13.6229 9.87278 13.031 10.7311 12.1795 11.1188L13.7803 12.7197C14.0732 13.0126 14.0732 13.4874 13.7803 13.7803C13.4874 14.0732 13.0126 14.0732 12.7197 13.7803L0.21967 1.28033ZM8.91737 7.85671C9.30506 7.00522 10.1634 6.41327 11.16 6.41327C11.5041 6.41327 11.8317 6.48382 12.1291 6.61124V4.15295L6.73148 5.67082L8.91737 7.85671ZM3.60901 9.06037V6.2429L5.0968 7.7307V11.1506C5.10085 11.2083 5.10291 11.2666 5.10291 11.3253C5.10291 12.6855 4.00023 13.7882 2.64002 13.7882C1.2798 13.7882 0.177124 12.6855 0.177124 11.3253C0.177124 9.9651 1.2798 8.86243 2.64002 8.86243C2.98405 8.86243 3.3116 8.93297 3.60901 9.06037Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188523\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"nintendo-switch\", \"keywords\": [ \"nintendo\", \"switch\", \"entertainment\", \"gaming\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188504)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.4147 14C12.3477 14 13.9147 12.433 13.9147 10.5V3.50049C13.9147 1.56749 12.3477 0.000488281 10.4147 0.000488281H8.24219C7.96604 0.000488281 7.74219 0.224346 7.74219 0.500488V13.5C7.74219 13.7762 7.96604 14 8.24219 14H10.4147ZM10.687 10.0882C11.5284 10.0882 12.2105 9.40614 12.2105 8.56477C12.2105 7.72341 11.5284 7.04135 10.687 7.04135C9.84563 7.04135 9.16357 7.72341 9.16357 8.56477C9.16357 9.40614 9.84563 10.0882 10.687 10.0882Z\\\" fill=\\\"black\\\"/>\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.52735 0.000488281C1.59435 0.000488281 0.0273438 1.56749 0.0273438 3.50049V10.5C0.0273438 12.433 1.59435 14 3.52735 14H5.69987C5.97601 14 6.19987 13.7762 6.19987 13.5V0.500489C6.19987 0.224346 5.97601 0.000488281 5.69987 0.000488281H3.52735ZM3.25505 3.14685C2.41369 3.14685 1.73163 3.82891 1.73163 4.67027C1.73163 5.51164 2.41369 6.1937 3.25505 6.1937C4.09641 6.1937 4.77847 5.51164 4.77847 4.67027C4.77847 3.82891 4.09641 3.14685 3.25505 3.14685Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188504\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"one-vesus-one\", \"keywords\": [ \"entertainment\", \"gaming\", \"one\", \"vesus\", \"one\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188529)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5 6C6.65685 6 8 4.65685 8 3C8 1.34315 6.65685 0 5 0C3.34315 0 2 1.34315 2 3C2 4.65685 3.34315 6 5 6ZM10.8521 12.0945L10.9546 12.4019C11.1323 12.9354 11.6315 13.2953 12.1937 13.2953C12.9722 13.2953 13.5778 12.6184 13.4919 11.8445L13.2901 10.0283C13.155 8.81215 12.1274 7.89209 10.9041 7.89209H7.99121C6.76791 7.89209 5.74024 8.81215 5.60515 10.0283L5.40341 11.8445C5.31744 12.6184 5.92309 13.2953 6.70158 13.2953C7.26379 13.2953 7.76292 12.9354 7.94071 12.4019L8.04313 12.0945C8.1626 11.736 8.49803 11.4942 8.87584 11.4942H10.0194C10.3972 11.4942 10.7327 11.736 10.8521 12.0945ZM4.36281 9.8903C4.49479 8.7022 5.18715 7.69977 6.15887 7.13498C5.78694 7.04672 5.39893 7 5 7C2.23858 7 0 9.23858 0 12C0 12.2761 0.223858 12.5 0.5 12.5H4.19606C4.1444 12.2461 4.13076 11.9793 4.16107 11.7065L4.36281 9.8903Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188529\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pacman\", \"keywords\": [ \"entertainment\", \"gaming\", \"pacman\", \"video\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.375 7C0.375 3.34111 3.34111 0.375 7 0.375C9.38452 0.375 11.4743 1.6351 12.6405 3.52334C12.7102 3.63624 12.7322 3.77222 12.7016 3.90134C12.671 4.03045 12.5903 4.14212 12.4774 4.21173L7.95302 7L12.4774 9.78827C12.5903 9.85788 12.671 9.96955 12.7016 10.0987C12.7322 10.2278 12.7102 10.3638 12.6405 10.4767C11.4743 12.3649 9.38452 13.625 7 13.625C3.34111 13.625 0.375 10.6589 0.375 7ZM6 5C6.55228 5 7 4.55228 7 4C7 3.44772 6.55228 3 6 3C5.44772 3 5 3.44772 5 4C5 4.55228 5.44772 5 6 5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"party-popper\", \"keywords\": [ \"hobby\", \"entertainment\", \"party\", \"popper\", \"confetti\", \"event\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188520)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.87539 0.201461C7.26677 0.337089 7.4741 0.764312 7.33847 1.15569C7.05033 1.98719 7.08783 2.88733 7.44573 3.69655C7.61327 4.07537 7.442 4.51828 7.06318 4.68582C6.68437 4.85337 6.24145 4.6821 6.07391 4.30328C5.56308 3.14828 5.50832 1.85587 5.92116 0.664541C6.05679 0.273161 6.48402 0.0658332 6.87539 0.201461ZM4.52288 2.8832C4.57992 2.47294 4.29357 2.09411 3.8833 2.03707C3.47303 1.98004 3.09421 2.26638 3.03717 2.67665C2.94143 3.36527 3.10071 4.06514 3.48503 4.64451C3.71399 4.98969 4.17943 5.08389 4.5246 4.85493C4.86978 4.62596 4.96399 4.16052 4.73502 3.81535C4.55298 3.54091 4.47753 3.20939 4.52288 2.8832ZM10.4076 4.29013C10.1343 4.21981 9.84767 4.21964 9.57428 4.28962C9.30089 4.35961 9.04959 4.49749 8.84369 4.69047L8.83206 4.70173L4.92076 8.61303L4.91582 8.61805C4.72089 8.81854 4.57954 9.06486 4.50477 9.33432C4.43001 9.60377 4.42424 9.88771 4.48799 10.16C4.55174 10.4322 4.68297 10.6841 4.86958 10.8924C5.05515 11.0994 5.28955 11.2568 5.55142 11.3501L11.7834 13.6731L11.791 13.6759C12.0786 13.7778 12.3889 13.7974 12.687 13.7325C12.9851 13.6676 13.2592 13.5208 13.4784 13.3086C13.6976 13.0964 13.8532 12.8272 13.9277 12.5314C14.0022 12.2355 13.9926 11.9248 13.9001 11.634C13.8977 11.6265 13.8951 11.6191 13.8924 11.6117L11.5806 5.38484C11.4928 5.12071 11.3406 4.88253 11.1377 4.69187C10.932 4.49863 10.6809 4.36045 10.4076 4.29013ZM3.27909 7.90993C2.96374 7.83832 2.63304 7.88588 2.35065 8.04347C1.98894 8.24532 1.53209 8.11573 1.33025 7.75402C1.1284 7.39232 1.25799 6.93547 1.61969 6.73362C2.22543 6.39559 2.9348 6.29356 3.61125 6.44716C4.01518 6.53889 4.26827 6.94069 4.17655 7.34462C4.08483 7.74855 3.68303 8.00165 3.27909 7.90993ZM1.06446 3.4999C0.512168 3.4999 0.0644531 3.94761 0.0644531 4.4999C0.0644531 5.05218 0.512168 5.4999 1.06446 5.4999C1.61674 5.4999 2.06446 5.05218 2.06446 4.4999C2.06446 3.94761 1.61674 3.4999 1.06446 3.4999Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188520\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"play-list-4\", \"keywords\": [ \"screen\", \"television\", \"display\", \"player\", \"movies\", \"players\", \"tv\", \"media\", \"video\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188532)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1.5C0 0.671573 0.671573 0 1.5 0H12.5C13.3284 0 14 0.671573 14 1.5V12.5C14 13.3284 13.3284 14 12.5 14H1.5C0.671573 14 0 13.3284 0 12.5V1.5ZM4.95392 4.6464V9.49361C4.95434 9.57471 4.9762 9.65426 5.01727 9.72419C5.05834 9.79413 5.11717 9.85196 5.1878 9.89183C5.25842 9.9317 5.33833 9.95219 5.41943 9.95122C5.50053 9.95025 5.57993 9.92787 5.64958 9.88633L9.80113 7.46272C9.87103 7.42233 9.92906 7.36426 9.96942 7.29435C10.0098 7.22443 10.031 7.14512 10.031 7.0644C10.031 6.98367 10.0098 6.90437 9.96942 6.83445C9.92906 6.76453 9.87103 6.70646 9.80113 6.66607L5.64958 4.25369C5.57993 4.21215 5.50053 4.18976 5.41943 4.18879C5.33833 4.18783 5.25842 4.20832 5.1878 4.24818C5.11717 4.28805 5.05834 4.34589 5.01727 4.41582C4.9762 4.48575 4.95434 4.5653 4.95392 4.6464Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188532\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"play-list-5\", \"keywords\": [ \"player\", \"television\", \"movies\", \"slider\", \"media\", \"tv\", \"players\", \"video\", \"stack\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188541)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1.47531C0 0.660518 0.660518 0 1.47531 0H9.76542C10.5802 0 11.2407 0.660518 11.2407 1.47531V9.76542C11.2407 10.5802 10.5802 11.2407 9.76542 11.2407H1.47531C0.660518 11.2407 0 10.5802 0 9.76542V1.47531ZM2.40582 7.60507C2.40582 6.63534 3.19194 5.84923 4.16166 5.84923C4.24853 5.84923 4.33393 5.85554 4.41742 5.86772V2.62981C4.41742 2.43089 4.49644 2.24013 4.63709 2.09948C4.77774 1.95882 4.96851 1.87981 5.16742 1.87981C6.11739 1.87981 7.0416 2.22083 7.7338 2.8476C8.42846 3.4766 8.8349 4.34661 8.8349 5.27153C8.8349 5.68575 8.49911 6.02153 8.0849 6.02153C7.67069 6.02153 7.3349 5.68575 7.3349 5.27153C7.3349 4.7952 7.12659 4.32134 6.72699 3.9595C6.50017 3.75413 6.22312 3.59599 5.91742 3.49715V7.58733L5.91751 7.60507C5.91751 8.57479 5.13139 9.36091 4.16166 9.36091C3.19194 9.36091 2.40582 8.57479 2.40582 7.60507ZM13.9704 3.5C13.9704 3.08579 13.6346 2.75 13.2204 2.75C12.8062 2.75 12.4704 3.08579 12.4704 3.5V12.2484C12.4704 12.3073 12.447 12.3637 12.4054 12.4054C12.3637 12.447 12.3073 12.4704 12.2484 12.4704H3.5C3.08579 12.4704 2.75 12.8062 2.75 13.2204C2.75 13.6346 3.08579 13.9704 3.5 13.9704H12.2484C12.7051 13.9704 13.1431 13.789 13.466 13.466C13.789 13.1431 13.9704 12.7051 13.9704 12.2484V3.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188541\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"play-list-8\", \"keywords\": [ \"player\", \"television\", \"movies\", \"slider\", \"media\", \"tv\", \"players\", \"video\", \"stack\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188526)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.49093 0.029541C0.676143 0.029541 0.015625 0.690059 0.015625 1.50485V9.79496C0.015625 10.6098 0.676143 11.2703 1.49093 11.2703H9.78104C10.5959 11.2703 11.2564 10.6098 11.2564 9.79496V1.50485C11.2564 0.690059 10.5959 0.029541 9.78104 0.029541H1.49093ZM3.78674 7.68954V3.61027C3.78709 3.54202 3.80549 3.47508 3.84005 3.41622C3.87462 3.35737 3.92413 3.3087 3.98356 3.27515C4.043 3.24159 4.11025 3.22435 4.1785 3.22516C4.24675 3.22598 4.31357 3.24482 4.37219 3.27978L7.866 5.30997C7.92483 5.34396 7.97367 5.39283 8.00763 5.45167C8.04159 5.51051 8.05947 5.57725 8.05947 5.64519C8.05947 5.71312 8.04159 5.77986 8.00763 5.8387C7.97367 5.89755 7.92483 5.94641 7.866 5.9804L4.37219 8.02004C4.31357 8.055 4.24675 8.07384 4.1785 8.07465C4.11025 8.07546 4.043 8.05822 3.98356 8.02467C3.92413 7.99111 3.87462 7.94244 3.84005 7.88359C3.80549 7.82474 3.78709 7.75779 3.78674 7.68954ZM13.9861 3.52954C13.9861 3.11533 13.6503 2.77954 13.2361 2.77954C12.8219 2.77954 12.4861 3.11533 12.4861 3.52954V12.2779C12.4861 12.3368 12.4627 12.3933 12.421 12.4349C12.3794 12.4766 12.3229 12.4999 12.264 12.4999H3.51562C3.10141 12.4999 2.76562 12.8357 2.76562 13.2499C2.76562 13.6642 3.10141 13.9999 3.51562 13.9999H12.264C12.7207 13.9999 13.1588 13.8185 13.4817 13.4956C13.8046 13.1726 13.9861 12.7346 13.9861 12.2779V3.52954Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188526\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"play-list-9\", \"keywords\": [ \"player\", \"television\", \"movies\", \"slider\", \"media\", \"tv\", \"players\", \"video\", \"stack\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188544)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.03125 1.92371C0.03125 1.37142 0.478965 0.923706 1.03125 0.923706H9.74198C10.2942 0.923706 10.742 1.37142 10.742 1.92371V3.03802H4.2583C3.01566 3.03802 2.0083 4.04538 2.0083 5.28802V9.712H1.03125C0.478966 9.712 0.03125 9.26429 0.03125 8.712V1.92371ZM3.2583 5.28802C3.2583 4.73574 3.70601 4.28802 4.2583 4.28802H12.969C13.5213 4.28802 13.969 4.73574 13.969 5.28802V12.0763C13.969 12.6286 13.5213 13.0763 12.969 13.0763H4.2583C3.70602 13.0763 3.2583 12.6286 3.2583 12.0763V5.28802ZM6.93164 6.88082C6.93164 6.4881 7.36361 6.24868 7.69664 6.45682L10.5787 8.25813C10.892 8.45397 10.892 8.9103 10.5787 9.10613L7.69664 10.9074C7.36361 11.1156 6.93164 10.8762 6.93164 10.4834V6.88082Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188544\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"play-list-folder\", \"keywords\": [ \"player\", \"television\", \"movies\", \"slider\", \"media\", \"tv\", \"players\", \"video\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188538)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.18345 0.75018C5.52332 0.742095 5.85595 0.849709 6.12675 1.05551C6.39945 1.26277 6.59301 1.55711 6.67524 1.8896L6.89039 2.75018H12.5C12.8978 2.75018 13.2794 2.90822 13.5607 3.18952C13.842 3.47082 14 3.85236 14 4.25018V7.22548C13.919 7.13966 13.8344 7.05683 13.7465 6.97722C12.8568 6.17158 11.689 5.74856 10.5079 5.74856C9.97744 5.74856 9.46873 5.95927 9.09366 6.33434C8.71859 6.70942 8.50787 7.21813 8.50787 7.74856V9.60121C7.42483 10.0297 6.65875 11.0862 6.65875 12.3217C6.65875 12.6462 6.71162 12.9585 6.80922 13.2502H1.5C1.10217 13.2502 0.720644 13.0921 0.43934 12.8108C0.158035 12.5295 0 12.148 0 11.7502V2.25018C0 1.85236 0.158035 1.47082 0.43934 1.18952C0.720644 0.908216 1.10218 0.75018 1.5 0.75018H5.18345ZM9.97754 7.21823C10.1182 7.07758 10.309 6.99856 10.5079 6.99856C11.3953 6.99856 12.2595 7.31708 12.9075 7.90381C13.5579 8.49277 13.9395 9.30846 13.9395 10.1768C13.9395 10.591 13.6038 10.9268 13.1895 10.9268C12.7753 10.9268 12.4395 10.591 12.4395 10.1768C12.4395 9.75706 12.2561 9.33751 11.9007 9.01571C11.7185 8.85077 11.4996 8.71975 11.2579 8.63085V12.3125V12.3217C11.2579 13.2465 10.5082 13.9962 9.58336 13.9962C8.65853 13.9962 7.90881 13.2465 7.90881 12.3217C7.90881 11.3969 8.65853 10.6471 9.58336 10.6471C9.64229 10.6471 9.70051 10.6502 9.75787 10.6561V7.74856C9.75787 7.54965 9.83689 7.35888 9.97754 7.21823Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188538\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"play-station\", \"keywords\": [ \"play\", \"station\", \"entertainment\", \"gaming\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188547)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.45841 0.0890586C6.23034 0.0161806 5.98117 0.0565788 5.78781 0.197786C5.59445 0.338994 5.48013 0.564038 5.48013 0.803473V5.70961C5.48013 5.73479 5.48138 5.75974 5.48384 5.7844C5.48138 5.80912 5.48012 5.8342 5.48012 5.85958V13.2146C5.48012 13.6288 5.81591 13.9646 6.23012 13.9646C6.64434 13.9646 6.98012 13.6288 6.98012 13.2146V5.85958C6.98012 5.83921 6.97931 5.81903 6.97772 5.79908V2.85522C6.97772 2.51005 7.25754 2.23022 7.60272 2.23022C7.9479 2.23022 8.22772 2.51005 8.22772 2.85522V7.12277L9.60358 7.55468C9.61243 7.55746 9.62132 7.56007 9.63027 7.56252C10.5648 7.81822 11.4052 7.65099 12.0048 7.08954C12.5717 6.55866 12.8054 5.7842 12.8113 5.04298C12.8232 3.57133 11.935 1.80563 10.0504 1.23684L6.45841 0.0890586ZM4.23256 7.84576L1.69365 8.99604C0.942139 9.28789 0.367252 9.74552 0.14176 10.3841C-0.0940307 11.0519 0.128212 11.6959 0.5128 12.1586C1.25563 13.0524 2.74196 13.5099 4.23256 13.0374V11.4222L3.76933 11.6107C2.78823 11.9178 1.96906 11.564 1.66637 11.1999C1.52502 11.0298 1.54042 10.9282 1.55618 10.8836C1.58003 10.816 1.70603 10.5971 2.25134 10.3886C2.2654 10.3833 2.27931 10.3775 2.29302 10.3713L4.23256 9.49253V7.84576ZM12.2497 12.5862L8.22772 13.8673V12.293L11.7823 11.1608C12.33 10.9643 12.4314 10.755 12.4458 10.7106C12.4558 10.6801 12.4755 10.5695 12.3092 10.377C11.9605 9.97336 11.1017 9.63091 10.1976 9.9491L8.22772 10.6685V9.07164L9.68689 8.53872L9.69408 8.53609C11.1669 8.01515 12.6812 8.513 13.4444 9.3965C13.8343 9.84787 14.0945 10.4929 13.872 11.1754C13.6541 11.8438 13.0513 12.3024 12.2727 12.5784C12.2651 12.5811 12.2574 12.5837 12.2497 12.5862Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188547\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"radio\", \"keywords\": [ \"antenna\", \"audio\", \"music\", \"radio\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188550)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.362 1.49833C13.7591 1.3804 13.9854 0.962909 13.8674 0.565838C13.7495 0.168768 13.332 -0.0575197 12.9349 0.0604113L1.31644 3.51113C0.5746 3.60165 0 4.23373 0 5.00001V5.875H14V5.00001C14 4.17159 13.3284 3.50001 12.5 3.50001H6.62238L13.362 1.49833ZM14 7.125H0V12.5C0 13.3284 0.671573 14 1.5 14H12.5C13.3284 14 14 13.3284 14 12.5V7.125ZM4.33768 12.3309C5.34888 12.3309 6.16862 11.5112 6.16862 10.5C6.16862 9.48881 5.34888 8.66907 4.33768 8.66907C3.32648 8.66907 2.50674 9.48881 2.50674 10.5C2.50674 11.5112 3.32648 12.3309 4.33768 12.3309ZM9.5 8.80676C9.15482 8.80676 8.875 9.08659 8.875 9.43176C8.875 9.77694 9.15482 10.0568 9.5 10.0568H11C11.3452 10.0568 11.625 9.77694 11.625 9.43176C11.625 9.08659 11.3452 8.80676 11 8.80676H9.5ZM8.875 11.9318C8.875 11.5866 9.15482 11.3068 9.5 11.3068H11C11.3452 11.3068 11.625 11.5866 11.625 11.9318C11.625 12.2769 11.3452 12.5568 11 12.5568H9.5C9.15482 12.5568 8.875 12.2769 8.875 11.9318Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188550\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"recording-tape-bubble-circle\", \"keywords\": [ \"phone\", \"device\", \"mail\", \"mobile\", \"voice\", \"machine\", \"answering\", \"chat\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188553)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.99834 0.0463878C8.04479 0.0457933 9.07788 0.281389 10.0206 0.735616C10.9634 1.18986 11.7915 1.85105 12.4432 2.66988C13.0948 3.48871 13.5533 4.44407 13.7843 5.46474C14.0154 6.48542 14.013 7.54508 13.7776 8.56473C13.5421 9.58439 13.0795 10.5377 12.4242 11.3537C11.769 12.1697 10.938 12.8273 9.99326 13.2774C9.04851 13.7275 8.01436 13.9586 6.96787 13.9534C5.97696 13.9485 4.9992 13.7319 4.09988 13.3188L0.633897 13.9455C0.456812 13.9776 0.27618 13.9119 0.160927 13.7737C0.0456747 13.6355 0.0135827 13.4461 0.076897 13.2776L1.07113 10.6324C0.460779 9.63611 0.110557 8.50074 0.0548176 7.3309C-0.00504526 6.07452 0.276993 4.82543 0.870894 3.71667C1.4648 2.60791 2.34832 1.681 3.42736 1.03467C4.50635 0.388357 5.74059 0.0468069 6.99834 0.0463878ZM8.78135 7.04994C8.78135 6.47734 9.24554 6.01315 9.81814 6.01315C10.3908 6.01315 10.855 6.47734 10.855 7.04994C10.855 7.62254 10.3908 8.08673 9.81814 8.08673C9.24554 8.08673 8.78135 7.62254 8.78135 7.04994ZM9.81814 4.76315C8.55518 4.76315 7.53135 5.78698 7.53135 7.04994C7.53135 7.42318 7.62077 7.77554 7.77936 8.08676H6.31764C6.47623 7.77554 6.56565 7.42318 6.56565 7.04994C6.56565 5.78698 5.54182 4.76315 4.27886 4.76315C3.01591 4.76315 1.99208 5.78698 1.99208 7.04994C1.99208 8.31027 3.01165 9.33246 4.27097 9.33671C4.2736 9.33675 4.27622 9.33676 4.27885 9.33676H9.81814C9.82077 9.33676 9.82339 9.33675 9.82601 9.33671C11.0854 9.33247 12.105 8.31027 12.105 7.04994C12.105 5.78698 11.0811 4.76315 9.81814 4.76315ZM4.27886 6.01315C3.70626 6.01315 3.24208 6.47734 3.24208 7.04994C3.24208 7.62254 3.70626 8.08673 4.27886 8.08673C4.85147 8.08673 5.31565 7.62254 5.31565 7.04994C5.31565 6.47734 4.85147 6.01315 4.27886 6.01315Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188553\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"recording-tape-bubble-square\", \"keywords\": [ \"phone\", \"device\", \"mail\", \"mobile\", \"voice\", \"machine\", \"answering\", \"chat\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188565)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.43935 0.43934C1.72065 0.158035 2.10219 0 2.50001 0H12.5C12.8978 0 13.2794 0.158035 13.5607 0.43934C13.842 0.720644 14 1.10217 14 1.5V11.5C14 11.8978 13.842 12.2794 13.5607 12.5607C13.2794 12.842 12.8978 13 12.5 13H4.56156L0.621278 13.9851C0.444236 14.0293 0.257142 13.9736 0.133149 13.8397C0.00915603 13.7058 -0.0320399 13.515 0.0256687 13.3419L1.00001 10.4189V1.5C1.00001 1.10218 1.15805 0.720644 1.43935 0.43934ZM9.32794 6.27018C9.32794 5.69758 9.79213 5.2334 10.3647 5.2334C10.9373 5.2334 11.4015 5.69758 11.4015 6.27018C11.4015 6.84279 10.9373 7.30697 10.3647 7.30697C9.79213 7.30697 9.32794 6.84279 9.32794 6.27018ZM10.3647 3.9834C9.10177 3.9834 8.07794 5.00723 8.07794 6.27018C8.07794 6.64343 8.16736 6.99579 8.32595 7.30701H6.86423C7.02282 6.99579 7.11224 6.64343 7.11224 6.27018C7.11224 5.00723 6.08841 3.9834 4.82545 3.9834C3.56249 3.9834 2.53867 5.00723 2.53867 6.27018C2.53867 7.53056 3.55831 8.55278 4.81771 8.55696C4.82028 8.55699 4.82286 8.55701 4.82544 8.55701H10.3647C10.3674 8.55701 10.37 8.55699 10.3727 8.55696C11.632 8.55266 12.6515 7.53048 12.6515 6.27018C12.6515 5.00723 11.6277 3.9834 10.3647 3.9834ZM4.82545 5.2334C4.25285 5.2334 3.78867 5.69758 3.78867 6.27018C3.78867 6.84279 4.25285 7.30697 4.82545 7.30697C5.39805 7.30697 5.86224 6.84279 5.86224 6.27018C5.86224 5.69758 5.39805 5.2334 4.82545 5.2334Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188565\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"song-recommendation\", \"keywords\": [ \"song\", \"recommendation\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188568)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.29271 0.190674C5.0305 0.190674 4.87692 0.360927 4.81707 0.443971C4.75378 0.531787 4.71553 0.627992 4.69211 0.694194C4.64944 0.81481 4.61183 0.969486 4.57877 1.10551L4.56673 1.15491C4.48201 1.50122 4.40022 1.78221 4.28295 1.94066C4.24633 1.99014 4.16593 2.06107 4.02213 2.14609C3.88367 2.22795 3.72079 2.30327 3.54974 2.37729C3.50948 2.39471 3.46692 2.4128 3.42359 2.43121C3.29921 2.48407 3.16852 2.53961 3.06818 2.5896C3.00038 2.62337 2.91356 2.67018 2.8376 2.73064C2.78991 2.76859 2.59625 2.9253 2.59625 3.1994C2.59625 3.47351 2.78991 3.63022 2.8376 3.66817C2.91356 3.72862 3.00038 3.77543 3.06818 3.80921C3.16853 3.8592 3.2992 3.91473 3.42359 3.9676C3.46682 3.98597 3.50956 4.00414 3.54974 4.02152C3.72079 4.09553 3.88367 4.17085 4.02213 4.25271C4.16593 4.33773 4.24633 4.40867 4.28295 4.45815C4.40022 4.61659 4.48201 4.89759 4.56673 5.2439L4.57877 5.2933C4.61183 5.42931 4.64944 5.58401 4.69211 5.70461C4.71553 5.77081 4.75378 5.86702 4.81707 5.95483C4.87692 6.03788 5.0305 6.20813 5.29271 6.20813C5.55491 6.20813 5.7085 6.03788 5.76835 5.95483C5.83164 5.86702 5.86988 5.77081 5.8933 5.70461C5.93598 5.584 5.97358 5.42932 6.00664 5.2933L6.01868 5.24389C6.1034 4.89758 6.18519 4.61659 6.30246 4.45814C6.33908 4.40866 6.41948 4.33773 6.56329 4.2527C6.70174 4.17084 6.86462 4.09552 7.03567 4.02151C7.07593 4.00409 7.1185 3.986 7.16183 3.96758C7.28622 3.91472 7.41688 3.85919 7.51723 3.8092C7.58503 3.77542 7.67185 3.72861 7.74781 3.66816C7.79549 3.63021 7.98916 3.47351 7.98916 3.1994C7.98916 2.92528 7.79549 2.76858 7.74781 2.73063C7.67185 2.67018 7.58503 2.62337 7.51723 2.58959C7.41688 2.5396 7.28622 2.48407 7.16183 2.43121C7.11863 2.41284 7.07582 2.39466 7.03567 2.37728C6.86462 2.30327 6.70174 2.22795 6.56329 2.14609C6.41948 2.06106 6.33908 1.99013 6.30246 1.94065C6.18519 1.78221 6.1034 1.50121 6.01868 1.1549L6.00664 1.1055C5.97358 0.969482 5.93598 0.814807 5.8933 0.694192C5.86988 0.62799 5.83163 0.531785 5.76834 0.44397C5.70849 0.360925 5.55491 0.190674 5.29271 0.190674ZM5.71076 7.40767C6.34461 7.2519 6.85349 6.74916 7.00072 6.08918L7.00044 6.09048L7.00494 6.07142C7.00955 6.05228 7.01719 6.02123 7.02769 5.98116C7.04898 5.89984 7.08039 5.7875 7.12005 5.66529C7.16047 5.54074 7.20413 5.4225 7.24755 5.32422C7.26157 5.29248 7.2736 5.26735 7.28327 5.24811L7.29748 5.23824C7.37824 5.18295 7.49122 5.11788 7.62471 5.05061C7.75411 4.98539 7.87862 4.93018 7.97166 4.89112C8.01733 4.87195 8.05333 4.85755 8.07596 4.84866L8.09843 4.83992C8.51353 4.68576 8.84408 4.38515 9.04033 4.00962L12.4573 3.06144L12.4628 3.0599C12.6283 3.01533 12.8018 3.00936 12.97 3.04241C13.1382 3.07546 13.2966 3.14667 13.4329 3.25055C13.5693 3.35443 13.68 3.48821 13.7565 3.64161C13.8136 3.75602 13.8505 3.87907 13.8659 4.00534C13.8714 4.04195 13.8742 4.07917 13.8742 4.11673V4.13741L13.8743 4.13951L13.8742 5.4486V9.94413C13.8742 9.97115 13.8728 9.99784 13.87 10.0241C13.8016 11.1163 12.894 11.981 11.7845 11.981C10.6305 11.981 9.69489 11.0454 9.69489 9.89136C9.69489 8.73729 10.6305 7.80173 11.7845 7.80173C11.9893 7.80173 12.1872 7.83118 12.3742 7.88609V6.43533L7.21076 7.86962V11.7902L7.21082 11.8068C7.21082 12.9609 6.27526 13.8965 5.12119 13.8965C3.96712 13.8965 3.03156 12.9609 3.03156 11.8068C3.03156 10.6528 3.96712 9.71719 5.12119 9.71719C5.32592 9.71719 5.52378 9.74664 5.71076 9.80152V7.40767ZM1.96216 5.89917C2.37637 5.89917 2.71216 6.23496 2.71216 6.64917V7.03323H3.09625C3.51047 7.03323 3.84625 7.36902 3.84625 7.78323C3.84625 8.19745 3.51047 8.53323 3.09625 8.53323H2.71216V8.9173C2.71216 9.33151 2.37637 9.6673 1.96216 9.6673C1.54795 9.6673 1.21216 9.33151 1.21216 8.9173V8.53323H0.828125C0.413912 8.53323 0.078125 8.19745 0.078125 7.78323C0.078125 7.36902 0.413912 7.03323 0.828125 7.03323H1.21216V6.64917C1.21216 6.23496 1.54795 5.89917 1.96216 5.89917Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188568\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"spades-symbol\", \"keywords\": [ \"entertainment\", \"gaming\", \"card\", \"spades\", \"symbol\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188559)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.43599 0.814097C7.62787 0.0619677 6.37624 0.0619677 5.56812 0.814097L1.32089 4.76704L1.32004 4.76783C-0.0563724 6.06085 -0.000730813 8.03805 0.896292 9.39564C1.34761 10.0787 2.01931 10.6198 2.84801 10.8477C3.55429 11.0419 4.35828 11.0041 5.21386 10.6539L4.64703 13.4503C4.63213 13.5239 4.65103 13.6002 4.69851 13.6582C4.74599 13.7163 4.81704 13.75 4.89205 13.75H9.10755C9.18256 13.75 9.25361 13.7163 9.30109 13.6582C9.34857 13.6002 9.36747 13.5239 9.35257 13.4503L8.78499 10.6502C9.63968 11 10.4431 11.0374 11.149 10.843C11.9774 10.6149 12.6492 10.0738 13.101 9.39102C13.9988 8.03416 14.0575 6.05803 12.684 4.76783L12.6832 4.76704L8.43599 0.814097Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188559\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"speaker-1\", \"keywords\": [ \"speaker\", \"music\", \"audio\", \"subwoofer\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188598)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.52526 0.112586C2.19094 0.026935 1.84008 0.029829 1.50722 0.120983C1.17437 0.212137 0.870981 0.388408 0.626948 0.632441C0.382915 0.876474 0.206644 1.17986 0.11549 1.51271C0.0243359 1.84557 0.0214419 2.19643 0.107093 2.53075C0.192744 2.86507 0.363987 3.17132 0.603961 3.41934C0.696512 3.515 0.798056 3.60072 0.906895 3.67549C0.375299 4.64888 0.0828835 5.73715 0.0567507 6.84989C0.0313348 7.93209 0.262163 9.00801 0.728623 9.98294C-0.0708017 10.682 -0.173943 11.9409 0.519188 12.7588C1.25276 13.6245 2.57967 13.647 3.38112 12.9274C4.46705 13.5942 5.71901 13.9495 6.9992 13.9495C8.27883 13.9495 9.53025 13.5945 10.6158 12.9283C11.4494 13.684 12.831 13.6073 13.5456 12.6554C14.1749 11.8171 14.0157 10.6456 13.2686 9.98534C13.4958 9.51114 13.6674 9.01348 13.7807 8.50233C13.9794 7.60562 13.9948 6.66222 13.83 5.76092C13.6968 5.03231 13.4479 4.32809 13.0915 3.67549C13.2003 3.60072 13.3019 3.515 13.3944 3.41934C13.6344 3.17132 13.8057 2.86507 13.8913 2.53075C13.977 2.19643 13.9741 1.84557 13.8829 1.51271C13.7918 1.17986 13.6155 0.876475 13.3714 0.632441C13.1274 0.388408 12.824 0.212137 12.4912 0.120983C12.1583 0.029829 11.8075 0.026935 11.4731 0.112586C11.1388 0.198237 10.8326 0.36948 10.5845 0.609454C10.489 0.701855 10.4034 0.803219 10.3288 0.911858C9.31063 0.354543 8.16593 0.0602197 6.9992 0.0602197C5.83248 0.0602197 4.68778 0.354543 3.66963 0.911856C3.59495 0.803218 3.50935 0.701854 3.41385 0.609454C3.16583 0.36948 2.85958 0.198237 2.52526 0.112586ZM6.9992 11.0426C9.22926 11.0426 11.0371 9.23475 11.0371 7.00469C11.0371 4.77463 9.22926 2.96681 6.9992 2.96681C4.76914 2.96681 2.96132 4.77463 2.96132 7.00469C2.96132 9.23475 4.76914 11.0426 6.9992 11.0426ZM5.44892 7.00471C5.44892 6.14851 6.14301 5.45442 6.99921 5.45442C7.85541 5.45442 8.5495 6.14851 8.5495 7.00471C8.5495 7.86091 7.85541 8.555 6.99921 8.555C6.14301 8.555 5.44892 7.86091 5.44892 7.00471Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188598\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"speaker-2\", \"keywords\": [ \"speakers\", \"music\", \"audio\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3 0C2.17157 0 1.5 0.671573 1.5 1.5V12.5C1.5 13.3284 2.17157 14 3 14H11C11.8284 14 12.5 13.3284 12.5 12.5V1.5C12.5 0.671573 11.8284 0 11 0H3ZM5.83142 3.05562C5.83142 2.41026 6.35459 1.88708 6.99996 1.88708C7.64532 1.88708 8.16849 2.41026 8.16849 3.05562C8.16849 3.70098 7.64532 4.22416 6.99996 4.22416C6.35459 4.22416 5.83142 3.70098 5.83142 3.05562ZM10.2907 9.00001C10.2907 10.8174 8.81741 12.2908 6.99998 12.2908C5.18255 12.2908 3.70923 10.8174 3.70923 9.00001C3.70923 7.18258 5.18255 5.70926 6.99998 5.70926C8.81741 5.70926 10.2907 7.18258 10.2907 9.00001ZM5.84186 9.00001C5.84186 8.36038 6.36038 7.84186 7.00001 7.84186C7.63964 7.84186 8.15816 8.36038 8.15816 9.00001C8.15816 9.63964 7.63964 10.1582 7.00001 10.1582C6.36038 10.1582 5.84186 9.63964 5.84186 9.00001Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"stream\", \"keywords\": [ \"stream\", \"entertainment\", \"gaming\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188562)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.2477 0.512405C12.0738 0.407303 13.6394 1.80243 13.7445 3.62852C13.8418 5.32044 12.6515 6.78846 11.0235 7.07885L8.51071 9.82158C8.57677 10.0361 8.6182 10.2617 8.63164 10.4953C8.72161 12.0584 7.52741 13.3984 5.96432 13.4884C4.80489 13.5551 3.76826 12.9154 3.27338 11.9425L4.69719 12.4162C5.60192 12.7173 6.58372 12.2557 6.9291 11.3669C7.26123 10.5122 6.89325 9.54479 6.07706 9.12689L4.33184 8.23332C4.71531 8.00051 5.15981 7.8555 5.63853 7.82795L5.65541 7.82703L7.19206 4.47821C7.16122 4.32552 7.14082 4.1689 7.13162 4.00912C7.02652 2.18304 8.42165 0.617507 10.2477 0.512405ZM10.5403 5.03653C11.2677 4.99466 11.8235 4.37097 11.7817 3.6435C11.7398 2.91602 11.1161 2.36022 10.3886 2.40209C9.66115 2.44396 9.10535 3.06764 9.14722 3.79512C9.1891 4.5226 9.81278 5.0784 10.5403 5.03653ZM0 7.42188L0.0107426 9.69652L5.18578 11.4183C5.48946 11.5193 5.819 11.3644 5.93493 11.0661C6.04641 10.7792 5.9229 10.4545 5.64894 10.3142L0 7.42188Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188562\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"tape-cassette-record\", \"keywords\": [ \"music\", \"entertainment\", \"tape\", \"cassette\", \"record\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 3C0 2.17157 0.671573 1.5 1.5 1.5H12.5C13.3284 1.5 14 2.17157 14 3V11C14 11.8284 13.3284 12.5 12.5 12.5H1.5C0.671573 12.5 0 11.8284 0 11V3ZM9.50001 9.5H4.50001L3.9 11H10.1L9.50001 9.5ZM2 5.75C2 4.7835 2.7835 4 3.75 4H10.25C11.2165 4 12 4.7835 12 5.75C12 6.7165 11.2165 7.5 10.25 7.5H3.75C2.7835 7.5 2 6.7165 2 5.75ZM3.75 5C3.33579 5 3 5.33579 3 5.75C3 6.16421 3.33579 6.5 3.75 6.5H4.66466C4.87255 6.31604 5.00255 6.0481 5.00255 5.74995C5.00255 5.45185 4.8726 5.18396 4.66478 5H3.75ZM8.14283 6.5H5.85717C5.95088 6.26842 6.00255 6.01523 6.00255 5.74995C6.00255 5.4847 5.95089 5.23155 5.85721 5H8.14279C8.04911 5.23155 7.99745 5.4847 7.99745 5.74995C7.99745 6.01523 8.04912 6.26842 8.14283 6.5ZM9.33534 6.5H10.25C10.6642 6.5 11 6.16421 11 5.75C11 5.33579 10.6642 5 10.25 5H9.33522C9.1274 5.18396 8.99745 5.45185 8.99745 5.74995C8.99745 6.0481 9.12745 6.31604 9.33534 6.5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"volume-down\", \"keywords\": [ \"speaker\", \"down\", \"volume\", \"control\", \"audio\", \"music\", \"decrease\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9 3.0054C9.02347 1.81466 7.69756 1.09936 6.71551 1.77885L6.71258 1.78089L2.84193 4.50004H1.5C0.679191 4.50004 0 5.17923 0 6.00004V8.00004C0 8.82085 0.679191 9.50004 1.5 9.50004H2.83897L6.6182 12.216L6.62551 12.2212C7.59689 12.8933 8.97354 12.1973 8.99988 11.0111L9 11V3.0054ZM10.8125 6.25C10.3983 6.25 10.0625 6.58579 10.0625 7C10.0625 7.41421 10.3983 7.75 10.8125 7.75H13.1887C13.6029 7.75 13.9387 7.41421 13.9387 7C13.9387 6.58579 13.6029 6.25 13.1887 6.25H10.8125Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"volume-level-high\", \"keywords\": [ \"speaker\", \"high\", \"volume\", \"control\", \"audio\", \"music\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9 3.0054C9.02347 1.81466 7.69756 1.09936 6.71551 1.77885L6.71258 1.78089L2.84193 4.50004H1.5C0.679191 4.50004 0 5.17923 0 6.00004V8.00004C0 8.82085 0.679191 9.50004 1.5 9.50004H2.83897L6.6182 12.216L6.62551 12.2212C7.59689 12.8933 8.97354 12.1973 8.99988 11.0111L9 11V3.0054ZM11.7411 3.41317C12.0603 3.14916 12.5331 3.19389 12.7971 3.51306C13.6263 4.51555 13.9722 5.72023 13.9722 7.00001C13.9722 8.27979 13.6263 9.48447 12.7971 10.487C12.5331 10.8061 12.0603 10.8509 11.7411 10.5869C11.422 10.3228 11.3773 9.85008 11.6413 9.5309C12.2068 8.84724 12.4722 8.0015 12.4722 7.00001C12.4722 5.99852 12.2068 5.15279 11.6413 4.46912C11.3773 4.14994 11.422 3.67718 11.7411 3.41317ZM10.2003 5.28823C10.5755 5.11267 11.0219 5.27447 11.1975 5.64964C11.3935 6.06855 11.4894 6.53105 11.4728 6.99997C11.4894 7.4689 11.3935 7.93139 11.1975 8.35031C11.0219 8.72548 10.5755 8.88728 10.2003 8.71172C9.82513 8.53615 9.66332 8.08969 9.83889 7.71453C9.93737 7.50408 9.98425 7.27127 9.97303 7.03565C9.97189 7.01188 9.97189 6.98807 9.97303 6.9643C9.98425 6.72868 9.93737 6.49587 9.83889 6.28542C9.66332 5.91025 9.82513 5.4638 10.2003 5.28823Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"volume-level-low\", \"keywords\": [ \"volume\", \"speaker\", \"lower\", \"down\", \"control\", \"music\", \"low\", \"audio\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9 3.0054C9.02347 1.81466 7.69756 1.09936 6.71551 1.77885L6.71258 1.78089L2.84193 4.50004H1.5C0.679191 4.50004 0 5.17923 0 6.00004V8.00004C0 8.82085 0.679191 9.50004 1.5 9.50004H2.83897L6.6182 12.216L6.62551 12.2212C7.59689 12.8933 8.97354 12.1973 8.99988 11.0111L9 11V3.0054ZM11.4136 5.02197C11.1496 4.7028 10.6768 4.65807 10.3577 4.92208C10.0385 5.18609 9.99377 5.65885 10.2578 5.97803C10.4864 6.25438 10.6036 6.60609 10.5865 6.96433C10.5854 6.9881 10.5854 7.0119 10.5865 7.03567C10.6036 7.39391 10.4864 7.74562 10.2578 8.02197C9.99377 8.34115 10.0385 8.81391 10.3577 9.07792C10.6768 9.34193 11.1496 9.2972 11.4136 8.97803C11.8727 8.42298 12.1118 7.71904 12.0863 7C12.1118 6.28096 11.8727 5.57702 11.4136 5.02197Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"volume-level-off\", \"keywords\": [ \"volume\", \"speaker\", \"control\", \"music\", \"audio\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.71551 1.77885C7.69756 1.09936 9.02347 1.81466 9 3.0054V11V11.0111C8.97366 12.1973 7.59689 12.8933 6.62551 12.2212L6.61816 12.2161L2.83897 9.50004H1.5C0.679191 9.50004 0 8.82085 0 8.00004V6.00004C0 5.17923 0.679191 4.50004 1.5 4.50004H2.84193L6.71258 1.78089L6.71551 1.77885Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"volume-mute\", \"keywords\": [ \"speaker\", \"remove\", \"volume\", \"control\", \"audio\", \"music\", \"mute\", \"off\", \"cross\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9 3.0054C9.02347 1.81466 7.69756 1.09936 6.71551 1.77885L6.71258 1.78089L2.84193 4.50004H1.5C0.679191 4.50004 0 5.17923 0 6.00004V8.00004C0 8.82085 0.679191 9.50004 1.5 9.50004H2.83897L6.6182 12.216L6.62551 12.2212C7.59689 12.8933 8.97354 12.1973 8.99988 11.0111L9 11V3.0054ZM13.7803 5.09467C14.0732 5.38756 14.0732 5.86244 13.7803 6.15533L12.9357 7L13.7803 7.84467C14.0732 8.13756 14.0732 8.61244 13.7803 8.90533C13.4874 9.19822 13.0126 9.19822 12.7197 8.90533L11.875 8.06066L11.0303 8.90533C10.7374 9.19822 10.2626 9.19822 9.96967 8.90533C9.67678 8.61244 9.67678 8.13756 9.96967 7.84467L10.8143 7L9.96967 6.15533C9.67678 5.86244 9.67678 5.38756 9.96967 5.09467C10.2626 4.80178 10.7374 4.80178 11.0303 5.09467L11.875 5.93934L12.7197 5.09467C13.0126 4.80178 13.4874 4.80178 13.7803 5.09467Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"volume-off\", \"keywords\": [ \"speaker\", \"music\", \"mute\", \"volume\", \"control\", \"audio\", \"off\", \"mute\", \"entertainment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188571)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.21967 1.28033C-0.0732233 0.987437 -0.0732233 0.512563 0.21967 0.21967C0.512563 -0.0732233 0.987437 -0.0732233 1.28033 0.21967L5.14617 4.08551L8.71258 1.58009L8.7155 1.57804C9.69755 0.898548 11.0235 1.61385 11 2.8046V9.93934L13.7803 12.7197C14.0732 13.0126 14.0732 13.4874 13.7803 13.7803C13.4874 14.0732 13.0126 14.0732 12.7197 13.7803L10.66 11.7206C10.6584 11.7226 10.6567 11.7245 10.6551 11.7265L3.24929 4.32064C3.25233 4.32011 3.25538 4.3196 3.25843 4.31909L0.21967 1.28033ZM2.20646 5.04558L9.44364 12.2828C9.16418 12.2792 8.88084 12.1971 8.62551 12.0204L8.61816 12.0153L4.83897 9.29924H3.5C2.67919 9.29924 2 8.62004 2 7.79924V5.79924C2 5.52565 2.07546 5.2678 2.20646 5.04558Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188571\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"vr-headset-1\", \"keywords\": [ \"entertainment\", \"gaming\", \"vr\", \"headset\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188601)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.0713 0C3.08998 0 0.610541 2.14883 0.0973047 4.98218H4.80574C5.09987 3.3718 6.51008 2.15112 8.2054 2.15112H10.7075C9.59424 0.835938 7.93059 0 6.0713 0ZM2.78558 11.1777C1.15254 10.1249 0.0559309 8.30853 0.00195312 6.23218H4.80575C5.09991 7.84252 6.51011 9.06316 8.2054 9.06316H12.1427V10.25C12.1427 11.039 11.5032 11.6786 10.7142 11.6786H9.35701V13.5C9.35701 13.7761 9.13315 14 8.85701 14H3.28558C3.00944 14 2.78558 13.7761 2.78558 13.5V11.1777ZM12.1306 7.81316H12.4999C12.5344 7.81316 12.5685 7.81141 12.6021 7.808C13.1064 7.75679 13.4999 7.33093 13.4999 6.81316V4.40112C13.4999 3.84884 13.0522 3.40112 12.4999 3.40112H11.4223H9.94631V7.81316H12.1306ZM8.69631 7.81316V3.40112H8.2054C6.98705 3.40112 5.99938 4.38879 5.99938 5.60714C5.99938 6.82549 6.98705 7.81316 8.2054 7.81316H8.69631Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188601\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"vr-headset-2\", \"keywords\": [ \"entertainment\", \"gaming\", \"vr\", \"headset\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188586)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.14284 0.928711C4.89068 0.928711 4.67798 1.11648 4.64671 1.36669L4.39324 3.39441H9.60673L9.35327 1.36669C9.32199 1.11648 9.10929 0.928711 8.85713 0.928711H5.14284ZM9.82885 4.64294C9.8146 4.64392 9.80022 4.64441 9.78572 4.64441H4.21429C4.1998 4.64441 4.18542 4.64392 4.17116 4.64294H2.35714C1.05731 4.64294 0 5.69281 0 6.9923V10.6559C0 12.1925 1.44118 13.2948 2.9146 13.0326C4.1626 12.8106 5.69521 12.5999 7 12.5999C8.30479 12.5999 9.8374 12.8106 11.0854 13.0326C12.5588 13.2948 14 12.1925 14 10.6559V6.9923C14 5.69281 12.9427 4.64294 11.6429 4.64294H9.82885ZM3.58929 7.92871C3.58929 7.58353 3.86912 7.30371 4.21429 7.30371H9.78572C10.1309 7.30371 10.4107 7.58353 10.4107 7.92871C10.4107 8.27389 10.1309 8.55371 9.78572 8.55371H4.21429C3.86912 8.55371 3.58929 8.27389 3.58929 7.92871Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188586\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"xbox\", \"keywords\": [ \"xbox\", \"entertainment\", \"gaming\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188595)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 8.66365 13.4196 10.1918 12.4503 11.3929C11.9179 9.19066 10.5014 7.03451 8.76757 5.31462C8.52121 5.07024 8.26671 4.83295 8.00545 4.60408C9.50895 3.53306 11.1044 2.832 12.5118 2.68443C13.4441 3.8735 14 5.37184 14 7ZM11.4532 1.59895C10.2431 0.600066 8.69165 0 7 0C5.3084 0 3.75694 0.600032 2.54686 1.59887C4.03973 1.94549 5.58613 2.73569 7.00023 3.79047C8.41422 2.73578 9.96049 1.94562 11.4532 1.59895ZM5.99501 4.60408C4.49135 3.53295 2.89575 2.83186 1.48822 2.68438C0.555884 3.87347 0 5.37182 0 7C0 8.66386 0.580509 10.1921 1.55004 11.3933C2.08233 9.19094 3.49892 7.03462 5.23289 5.31462C5.47925 5.07024 5.73375 4.83295 5.99501 4.60408ZM7.00023 5.3875C6.69687 5.64516 6.40033 5.91724 6.11319 6.20206C4.2479 8.05234 2.89074 10.3296 2.62205 12.4623C3.8209 13.4244 5.34324 14 7 14C8.65696 14 10.1794 13.4243 11.3784 12.462C11.1096 10.3294 9.75246 8.05224 7.88727 6.20206C7.60013 5.91724 7.30359 5.64516 7.00023 5.3875Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188595\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" } ], \"food_drink\": [ { \"name\": \"beer-mug\", \"keywords\": [ \"beer\", \"cook\", \"brewery\", \"drink\", \"mug\", \"cooking\", \"nutrition\", \"brew\", \"brewing\", \"food\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187140)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.413767 0.449317C0.682488 0.165204 1.05309 0 1.44582 0H9.01238C9.40511 0 9.77571 0.165204 10.0444 0.449317C10.3123 0.732486 10.4582 1.11088 10.4582 1.5V2.25H12.2355C12.6997 2.25 13.1448 2.43437 13.473 2.76256C13.8012 3.09075 13.9855 3.53587 13.9855 4V8.5C13.9855 8.96413 13.8012 9.40925 13.473 9.73744C13.1448 10.0656 12.6997 10.25 12.2355 10.25H10.4582V11.5C10.4582 12.1543 10.2126 12.7871 9.76741 13.2578C9.32131 13.7294 8.71014 14 8.06656 14H2.39164C1.74807 14 1.13689 13.7294 0.690792 13.2578C0.24559 12.7871 0 12.1543 0 11.5V1.5C0 1.11088 0.145941 0.732486 0.413767 0.449317ZM10.4582 8.75H12.2355C12.3018 8.75 12.3654 8.72366 12.4123 8.67678C12.4592 8.62989 12.4855 8.5663 12.4855 8.5V4C12.4855 3.9337 12.4592 3.87011 12.4123 3.82322C12.3654 3.77634 12.3018 3.75 12.2355 3.75H10.4582V8.75ZM3.17749 3.375C3.52267 3.375 3.80249 3.65482 3.80249 4V9.5C3.80249 9.84518 3.52267 10.125 3.17749 10.125C2.83231 10.125 2.55249 9.84518 2.55249 9.5V4C2.55249 3.65482 2.83231 3.375 3.17749 3.375ZM7.9057 4C7.9057 3.65482 7.62588 3.375 7.2807 3.375C6.93552 3.375 6.6557 3.65482 6.6557 4V9.5C6.6557 9.84518 6.93552 10.125 7.2807 10.125C7.62588 10.125 7.9057 9.84518 7.9057 9.5V4Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187140\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"beer-pitch\", \"keywords\": [ \"drink\", \"glass\", \"beer\", \"pitch\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187143)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.42865 0.0709229C5.1031 0.0709229 3.96726 0.879371 3.48608 2.03021C3.41103 2.02446 3.33527 2.02154 3.2589 2.02154C1.63273 2.02154 0.314453 3.33981 0.314453 4.96598C0.314453 6.05209 0.902207 6.99954 1.77559 7.50985V11.4999C1.77559 12.8806 2.89488 13.9999 4.27559 13.9999H8.07806C9.35323 13.9999 10.4054 13.0452 10.5589 11.8116H10.5657H12.1563C13.1228 11.8116 13.9063 11.0281 13.9063 10.0616V7.67279C13.9063 6.78175 13.2404 6.04625 12.3791 5.93683C12.4852 5.63286 12.5429 5.30616 12.5429 4.96598C12.5429 3.33981 11.2246 2.02154 9.5984 2.02154C9.52203 2.02154 9.44627 2.02446 9.37122 2.03021C8.89004 0.879372 7.7542 0.0709229 6.42865 0.0709229ZM4.75804 3.01341C4.87654 2.19772 5.58005 1.57092 6.42865 1.57092C7.27725 1.57092 7.98076 2.19773 8.09926 3.01341C8.13176 3.23713 8.26345 3.43426 8.45768 3.54995C8.65191 3.66564 8.88797 3.68753 9.10017 3.60955C9.25449 3.55284 9.42195 3.52154 9.5984 3.52154C10.3962 3.52154 11.0429 4.16824 11.0429 4.96598C11.0429 5.76373 10.3962 6.41043 9.5984 6.41043H5.95334C5.53913 6.41043 5.20334 6.74621 5.20334 7.16043V8.86104C5.20334 9.39287 4.77221 9.82401 4.24038 9.82401C3.70855 9.82401 3.27742 9.39287 3.27742 8.86104V7.03556C3.27742 6.7177 3.07704 6.43435 2.77735 6.32843C2.21532 6.12978 1.81445 5.59373 1.81445 4.96598C1.81445 4.16824 2.46115 3.52154 3.2589 3.52154C3.43536 3.52154 3.60282 3.55284 3.75713 3.60955C3.96933 3.68753 4.20539 3.66563 4.39962 3.54995C4.59385 3.43426 4.72554 3.23713 4.75804 3.01341ZM10.5781 7.42279H12.1563C12.2944 7.42279 12.4063 7.53472 12.4063 7.67279V10.0616C12.4063 10.1997 12.2944 10.3116 12.1563 10.3116H10.5781V7.42279Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187143\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"burger\", \"keywords\": [ \"burger\", \"fast\", \"cook\", \"cooking\", \"nutrition\", \"food\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187134)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.18359 1.0936C1.93591 0.389977 2.94953 0 4 0H10C11.0505 0 12.0641 0.389977 12.8164 1.0936C13.5699 1.79828 14 2.76147 14 3.77346C14 4.16533 13.8333 4.53379 13.5486 4.79998C13.2652 5.06511 12.8874 5.20874 12.5 5.20874H1.5C1.11257 5.20874 0.734832 5.06511 0.451355 4.79998C0.166747 4.53379 0 4.16533 0 3.77346C0 2.76147 0.430139 1.79828 1.18359 1.0936ZM0.769592 6.75C0.355379 6.75 0.0195923 7.08579 0.0195923 7.5C0.0195923 7.91421 0.355379 8.25 0.769592 8.25H13.2304C13.6446 8.25 13.9804 7.91421 13.9804 7.5C13.9804 7.08579 13.6446 6.75 13.2304 6.75H0.769592ZM0.306084 10.0174C0.495961 9.84094 0.746138 9.74757 1 9.74757H2.5C2.57278 9.74757 2.64468 9.76345 2.71069 9.79412L5.40586 11.0464L6.65964 9.8813C6.75214 9.79534 6.87373 9.74757 7 9.74757H13C13.2539 9.74757 13.504 9.84094 13.6939 10.0174C13.885 10.195 14 10.444 14 10.7122C14 11.5965 13.6216 12.4365 12.9617 13.0497C12.303 13.6619 11.4169 14 10.5 14H3.5C2.5831 14 1.69703 13.6619 1.03832 13.0497C0.378363 12.4365 0 11.5965 0 10.7122C0 10.444 0.114971 10.195 0.306084 10.0174Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187134\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"burrito-fastfood\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187131)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.3237 1.98918C13.0729 1.73831 12.771 1.56772 12.4515 1.47776C12.3616 1.15834 12.191 0.856445 11.9401 0.605578C11.5662 0.231647 11.0794 0.0363321 10.5893 0.0203572L10.5881 0.0203178C10.1415 0.00688275 9.6893 0.142423 9.31487 0.427913C8.6298 0.219448 7.8544 0.387292 7.31332 0.928373C6.53262 1.70908 6.53193 2.97531 7.31332 3.75671L10.1726 6.61594C10.954 7.39733 12.2202 7.39664 13.0009 6.61594C13.542 6.07486 13.7099 5.29947 13.5014 4.61438C13.7869 4.23996 13.9224 3.78782 13.909 3.34123V3.33998C13.893 2.84986 13.6977 2.36311 13.3237 1.98918ZM10.912 5.9396C10.9011 5.9296 10.8903 5.91931 10.8798 5.90873L8.02049 3.0495C8.01034 3.03935 8.00046 3.02904 7.99084 3.01858C8.08734 2.89495 8.19966 2.78051 8.32746 2.67825C8.65934 2.41269 9.04844 2.26699 9.44247 2.23536C10.0401 2.18738 10.649 2.40177 11.0868 2.85827C11.5433 3.29601 11.7385 3.8941 11.6905 4.4917C11.6589 4.88572 11.5132 5.27482 11.2476 5.6067C11.1466 5.73286 11.0338 5.84393 10.912 5.9396ZM0.904153 9.08736L4.7303 5.2612L6.57996 10.2979C6.72689 10.698 6.33795 11.0867 5.93795 10.9395L0.904153 9.08736ZM0.38469 11.021C0.0784756 10.7148 0.0123672 10.2594 0.186365 9.88879L5.59264 11.878C5.79205 11.9513 5.99054 11.9803 6.18089 11.9721L4.46546 13.6876C4.07494 14.0781 3.44177 14.0781 3.05125 13.6876L0.38469 11.021ZM9.93715 8.21591L7.61249 10.5405C7.62067 10.3505 7.59179 10.1523 7.51866 9.95319L5.50947 4.48204L5.85637 4.13513L9.93715 8.21591Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187131\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cake-slice\", \"keywords\": [ \"cherry\", \"cake\", \"birthday\", \"event\", \"special\", \"sweet\", \"bake\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187155)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.46982 3.81718C1.46982 2.87989 2.10497 2.09095 2.96835 1.8573C2.98991 1.69217 3.03267 1.46667 3.12036 1.23284C3.21013 0.993442 3.35959 0.711647 3.61395 0.485552C3.88121 0.247989 4.2332 0.100586 4.65937 0.100586C5.00455 0.100586 5.28437 0.380408 5.28437 0.725586C5.28437 1.07076 5.00455 1.35058 4.65937 1.35058C4.54051 1.35058 4.48373 1.38486 4.4444 1.41981C4.39218 1.46623 4.33725 1.54779 4.29077 1.67175C4.26003 1.75373 4.23842 1.83978 4.22354 1.91968C4.98754 2.21117 5.53023 2.95079 5.53023 3.81718C5.53023 4.93843 4.62128 5.84738 3.50003 5.84738C2.37877 5.84738 1.46982 4.93843 1.46982 3.81718ZM0 7.47323C0 7.19709 0.223858 6.97323 0.5 6.97323H13.5C13.7761 6.97323 14 7.19709 14 7.47323V8.87502H0V7.47323ZM0 10.125V13.5C0 13.7761 0.223858 14 0.5 14H13.5C13.7761 14 14 13.7761 14 13.5V10.125H0Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187155\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"candy-cane\", \"keywords\": [ \"candy\", \"sweet\", \"cane\", \"christmas\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.98092 0C7.99989 0 8.98576 0.320909 9.80344 0.906759L8.04609 3.6317C7.75236 3.37405 7.37403 3.23077 6.98092 3.23077C6.5525 3.23077 6.14162 3.40096 5.83867 3.7039C5.83562 3.70696 5.83258 3.71002 5.82956 3.7131L2.8455 2.51104C2.81431 2.49848 2.7827 2.48861 2.75093 2.48132C2.96665 2.09542 3.23601 1.73757 3.55418 1.41941C4.46301 0.510575 5.69564 0 6.98092 0ZM2.29063 3.62694C2.18798 4.02178 2.13477 4.43122 2.13477 4.84615V5.92308C2.13477 6.3515 2.30496 6.76238 2.60791 7.06533C2.91085 7.36827 3.32173 7.53846 3.75015 7.53846C4.17858 7.53846 4.58946 7.36827 4.8924 7.06533C5.19535 6.76238 5.36554 6.3515 5.36554 5.92308V4.87379L2.37844 3.6705C2.3476 3.65808 2.3183 3.64348 2.29063 3.62694ZM8.59631 5.08489L10.7303 1.77581C11.4364 2.63801 11.827 3.72195 11.827 4.84615V6.1161L8.59631 9.34687V5.08489ZM8.59631 11.1146V12.3846C8.59631 12.813 8.7665 13.2239 9.06944 13.5269C9.37239 13.8298 9.78327 14 10.2117 14C10.6401 14 11.051 13.8298 11.3539 13.5269C11.6568 13.2239 11.827 12.813 11.827 12.3846V7.88387L8.59631 11.1146Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"champagne-party-alcohol\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187122)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.20744 0.574046C3.34514 0.209041 3.73966 0.0105433 4.11482 0.117511L7.71581 1.14424C8.09097 1.25121 8.32148 1.62792 8.24598 2.01066L7.19959 7.31488C6.90129 8.82701 5.55145 9.84459 4.08281 9.79689L3.41068 12.1542L4.15072 12.3652C4.54906 12.4788 4.77991 12.8938 4.66633 13.2921C4.55276 13.6905 4.13777 13.9213 3.73943 13.8077L2.31302 13.401C2.3013 13.3983 2.28959 13.3952 2.27789 13.3919C2.2662 13.3886 2.25464 13.385 2.24324 13.3811L0.814077 12.9736C0.415739 12.8601 0.184893 12.4451 0.298469 12.0467C0.412045 11.6484 0.827033 11.4176 1.22538 11.5311L1.96817 11.7429L2.64016 9.38608C1.36596 8.65254 0.754903 7.07529 1.29918 5.63253L3.20744 0.574046ZM3.68886 3.54765L6.26274 4.31369L6.64076 2.3975L4.36728 1.74928L3.68886 3.54765ZM10.0018 9.869C9.18928 9.87038 8.42324 9.54735 7.85811 9.00131C8.12502 8.57099 8.32069 8.08536 8.42498 7.55675L9.12302 4.01835L10.6016 3.66539L9.97071 1.81641L9.5083 1.93244C9.52657 1.40168 9.33175 0.895758 8.98637 0.516901L10.2751 0.193531C10.6535 0.0985867 11.0415 0.30957 11.1675 0.678782L12.9134 5.79557C13.411 7.2538 12.7514 8.80945 11.4564 9.50278L12.053 11.8803L12.7975 11.6935C13.1992 11.5927 13.6066 11.8367 13.7075 12.2385C13.8083 12.6402 13.5643 13.0476 13.1625 13.1484L11.7203 13.5103C11.7106 13.5132 11.7007 13.5159 11.6908 13.5184C11.6808 13.5209 11.6709 13.5231 11.661 13.5252L10.2121 13.8888C9.81027 13.9896 9.40285 13.7456 9.30204 13.3438C9.20124 12.9421 9.4452 12.5347 9.84696 12.4339L10.5981 12.2454L10.0018 9.869Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187122\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cheese\", \"keywords\": [ \"cook\", \"cheese\", \"animal\", \"products\", \"cooking\", \"nutrition\", \"dairy\", \"food\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187125)\\\">\\n<path d=\\\"M14 5.65998C13.8431 4.32479 13.279 3.07023 12.3846 2.06661C11.4901 1.06299 10.3084 0.35892 9.00001 0.0499772C8.8908 0.0100534 8.77412 -0.00526956 8.6583 0.00510204C8.54249 0.0154736 8.43039 0.0512841 8.33001 0.109977L0.620008 4.99998C0.446369 5.10736 0.300006 5.25356 0.192425 5.42708C0.0848449 5.6006 0.0189797 5.7967 8.30897e-06 5.99998V10.4C-0.000716655 10.553 0.046015 10.7025 0.133768 10.8279C0.221522 10.9532 0.345981 11.0483 0.490008 11.1C0.631565 11.1545 0.786596 11.1636 0.933553 11.126C1.08051 11.0883 1.21209 11.0059 1.31001 10.89C1.421 10.756 1.55875 10.6466 1.71445 10.5689C1.87014 10.4911 2.04036 10.4468 2.21419 10.4386C2.38802 10.4305 2.56163 10.4587 2.72392 10.5215C2.88622 10.5843 3.0336 10.6803 3.15666 10.8034C3.27971 10.9264 3.37571 11.0738 3.43852 11.2361C3.50133 11.3984 3.52956 11.572 3.5214 11.7458C3.51324 11.9197 3.46888 12.0899 3.39115 12.2456C3.31342 12.4013 3.20404 12.539 3.07001 12.65C2.95414 12.7479 2.87166 12.8795 2.83403 13.0265C2.79639 13.1734 2.80547 13.3284 2.86001 13.47C2.9117 13.614 3.00678 13.7385 3.13214 13.8262C3.2575 13.914 3.40699 13.9607 3.56001 13.96H12.75C13.0747 13.9602 13.3867 13.834 13.62 13.6081C13.8533 13.3823 13.9896 13.0745 14 12.75V5.65998ZM8.75001 11C8.40389 11 8.06555 10.8974 7.77776 10.7051C7.48997 10.5128 7.26567 10.2395 7.13322 9.91968C7.00077 9.59991 6.96611 9.24804 7.03363 8.90857C7.10116 8.56911 7.26783 8.25728 7.51257 8.01254C7.75731 7.7678 8.06913 7.60113 8.4086 7.53361C8.74807 7.46608 9.09993 7.50074 9.4197 7.63319C9.73948 7.76564 10.0128 7.98995 10.2051 8.27773C10.3974 8.56552 10.5 8.90386 10.5 9.24998C10.5 9.71411 10.3156 10.1592 9.98745 10.4874C9.65926 10.8156 9.21414 11 8.75001 11ZM4.25001 4.99998C4.19384 5.00408 4.13802 4.98829 4.09232 4.95538C4.04661 4.92248 4.01394 4.87455 4.00001 4.81998C3.98451 4.76875 3.98586 4.7139 4.00386 4.6635C4.02186 4.6131 4.05556 4.5698 4.10001 4.53998L8.67001 1.67998C8.73515 1.63977 8.80836 1.61444 8.88442 1.6058C8.96048 1.59716 9.03751 1.60541 9.11001 1.62998C9.811 1.87965 10.4504 2.27658 10.9851 2.79406C11.5198 3.31153 11.9375 3.93755 12.21 4.62998C12.2293 4.66525 12.2394 4.70479 12.2394 4.74498C12.2394 4.78517 12.2293 4.82471 12.21 4.85998C12.1925 4.90125 12.1632 4.93648 12.1259 4.96136C12.0886 4.98623 12.0448 4.99966 12 4.99998H4.25001Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187125\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cherries\", \"keywords\": [ \"cook\", \"plant\", \"cherry\", \"plants\", \"cooking\", \"nutrition\", \"vegetarian\", \"fruit\", \"food\", \"cherries\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187113)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.64188 1.54558C3.53344 3.4659 2.89471 5.62681 2.78486 7.85072C1.37087 8.13031 0.304688 9.37731 0.304688 10.8733C0.304688 12.575 1.68419 13.9545 3.3859 13.9545C5.08761 13.9545 6.46712 12.575 6.46712 10.8733C6.46712 9.48369 5.54716 8.3089 4.28303 7.92477C4.36938 6.17654 4.8218 4.47192 5.60421 2.91913C6.08186 4.73096 7.02946 6.39526 8.36244 7.73666C7.84737 8.2874 7.53206 9.02731 7.53206 9.84086C7.53206 11.5425 8.91157 12.922 10.6133 12.922C12.315 12.922 13.6945 11.5425 13.6945 9.84086C13.6945 8.13915 12.315 6.75964 10.6133 6.75964C10.2817 6.75964 9.96227 6.81204 9.66295 6.90899C8.14341 5.48552 7.15566 3.59323 6.8509 1.54558H8.47944C8.89365 1.54558 9.22944 1.2098 9.22944 0.795579C9.22944 0.381365 8.89365 0.0455784 8.47944 0.0455784H6.02736C6.02181 0.0455168 6.01627 0.0455169 6.01073 0.0455784H3.48174C3.06752 0.0455784 2.73174 0.381365 2.73174 0.795579C2.73174 1.2098 3.06752 1.54558 3.48174 1.54558H4.64188Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187113\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"chicken-grilled-stream\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187146)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.14893 1.14502C5.14893 0.799842 4.86911 0.52002 4.52393 0.52002C4.17875 0.52002 3.89893 0.799842 3.89893 1.14502V2.70752C3.89893 3.0527 4.17875 3.33252 4.52393 3.33252C4.86911 3.33252 5.14893 3.0527 5.14893 2.70752V1.14502ZM2.24268 1.68311C2.58786 1.68311 2.86768 1.96293 2.86768 2.30811V3.87061C2.86768 4.21578 2.58786 4.49561 2.24268 4.49561C1.8975 4.49561 1.61768 4.21578 1.61768 3.87061V2.30811C1.61768 1.96293 1.8975 1.68311 2.24268 1.68311ZM6.80518 1.68311C7.15036 1.68311 7.43018 1.96293 7.43018 2.30811V3.87061C7.43018 4.21578 7.15036 4.49561 6.80518 4.49561C6.46 4.49561 6.18018 4.21578 6.18018 3.87061V2.30811C6.18018 1.96293 6.46 1.68311 6.80518 1.68311ZM12.4631 3.20947C11.6593 3.20947 11.0077 3.86111 11.0077 4.66494C11.0077 4.84575 11.0406 5.01885 11.1009 5.17857L10.0248 6.17343C9.41521 5.90535 8.67675 5.94816 8.01254 6.27605C7.77714 6.41333 7.55125 6.59386 7.3459 6.8194C6.50083 7.74752 6.44485 8.96916 6.99778 9.68493C7.05451 9.75837 7.1181 9.82759 7.18874 9.8919C7.39292 10.0778 7.40773 10.394 7.22182 10.5982C7.03591 10.8024 6.71967 10.8172 6.51549 10.6313C6.40151 10.5275 6.29844 10.4154 6.20641 10.2963C5.29417 9.11538 5.50883 7.35168 6.60648 6.14615L6.63385 6.11641C4.56147 5.50147 1.86152 6.22979 0.670576 8.60837C-0.247685 10.4424 0.100231 11.9433 0.41784 12.7093C0.638161 13.2407 1.16132 13.48 1.63918 13.48H11.9412C12.7696 13.48 13.4412 12.8084 13.4412 11.98V11.6832C13.4412 11.0073 13.3302 10.3683 12.9781 9.79272C12.7098 9.35429 12.3211 8.9844 11.799 8.66592C11.6432 9.18873 11.3637 9.69186 10.9725 10.1215C10.7866 10.3257 10.4703 10.3405 10.2662 10.1546C10.062 9.96865 10.0472 9.65242 10.2331 9.44823C10.8988 8.71705 11.0735 7.81517 10.8515 7.11141L11.9677 6.07954C11.9795 6.0686 11.9908 6.05732 12.0016 6.04572C12.1466 6.09417 12.3018 6.12041 12.4631 6.12041C13.267 6.12041 13.9186 5.46878 13.9186 4.66494C13.9186 3.86111 13.267 3.20947 12.4631 3.20947Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187146\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cocktail\", \"keywords\": [ \"cook\", \"alcohol\", \"food\", \"cocktail\", \"drink\", \"cooking\", \"nutrition\", \"alcoholic\", \"beverage\", \"glass\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.53708 0.0264893C1.73779 0.0264893 1.08984 0.67444 1.08984 1.47373C1.08984 1.798 1.19874 2.11288 1.39909 2.36786L6.25097 8.54299V12.4888H4.00097C3.58676 12.4888 3.25097 12.8246 3.25097 13.2388C3.25097 13.653 3.58676 13.9888 4.00097 13.9888H10.001C10.4152 13.9888 10.751 13.653 10.751 13.2388C10.751 12.8246 10.4152 12.4888 10.001 12.4888H7.75097V8.54295L12.6029 2.36786C12.8032 2.11288 12.9121 1.798 12.9121 1.47373C12.9121 0.674439 12.2642 0.0264893 11.4649 0.0264893H2.53708ZM9.80549 3.50021L11.3563 1.52649H2.64563L4.19642 3.50021H9.80549Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"coffee-bean\", \"keywords\": [ \"cook\", \"cooking\", \"nutrition\", \"coffee\", \"bean\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187116)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.17472 12.3563C4.39598 14.2226 8.22282 13.6267 10.9247 10.9248C13.3696 8.47989 14.0901 5.1138 12.8221 2.84979C12.7734 3.0231 12.717 3.20885 12.6523 3.40305C12.3236 4.38922 11.7582 5.65532 10.8148 6.5987C10.2748 7.13871 9.6145 7.38984 8.95628 7.50951C8.32077 7.62506 7.64086 7.62503 7.02836 7.625H6.99988C6.35488 7.625 5.78118 7.62685 5.26709 7.72032C4.76585 7.81146 4.37208 7.98195 4.06885 8.28518C3.32574 9.02829 2.83706 10.0811 2.53335 10.9922C2.38378 11.4409 2.28396 11.8403 2.22177 12.1263C2.20255 12.2147 2.18699 12.292 2.17472 12.3563ZM1.17767 11.1502C-0.0901756 8.88615 0.630281 5.52014 3.07515 3.07527C5.77699 0.373441 9.60377 -0.222449 11.8251 1.64375C11.8128 1.708 11.7972 1.78527 11.778 1.87365C11.7158 2.15974 11.616 2.55906 11.4664 3.00776C11.1627 3.91889 10.674 4.97171 9.93091 5.71482C9.62768 6.01805 9.23391 6.18854 8.73267 6.27968C8.21858 6.37315 7.64488 6.375 6.99988 6.375H6.97141C6.35891 6.37497 5.67899 6.37494 5.04348 6.49049C4.38526 6.61016 3.72498 6.86129 3.18497 7.4013C2.24159 8.34468 1.67622 9.61078 1.3475 10.597C1.28277 10.7911 1.22644 10.9769 1.17767 11.1502Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187116\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"coffee-mug\", \"keywords\": [ \"coffee\", \"cook\", \"cup\", \"drink\", \"mug\", \"cooking\", \"nutrition\", \"cafe\", \"caffeine\", \"food\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4 0.0228271C4.41421 0.0228271 4.75 0.358614 4.75 0.772827V2.77282C4.75 3.18704 4.41421 3.52282 4 3.52282C3.58579 3.52282 3.25 3.18704 3.25 2.77282V0.772827C3.25 0.358614 3.58579 0.0228271 4 0.0228271ZM1.93934 5.43932C2.22064 5.15802 2.60217 4.99998 3 4.99998H8C8.39783 4.99998 8.77936 5.15802 9.06066 5.43932C9.23147 5.61014 9.35683 5.81789 9.42896 6.04382H9.5C10.362 6.04382 11.1886 6.38623 11.7981 6.99572C12.4076 7.60521 12.75 8.43186 12.75 9.29383C12.75 10.1558 12.4076 10.9824 11.7981 11.5919C11.1886 12.2014 10.362 12.5438 9.5 12.5438H9.27165C9.14908 12.8106 8.97934 13.0562 8.76777 13.2678C8.29893 13.7366 7.66304 14 7 14H4C3.33696 14 2.70107 13.7366 2.23223 13.2678C1.76339 12.7989 1.5 12.163 1.5 11.5V6.49998C1.5 6.10216 1.65804 5.72062 1.93934 5.43932ZM9.5 11.0438C9.96413 11.0438 10.4092 10.8595 10.7374 10.5313C11.0656 10.2031 11.25 9.75796 11.25 9.29383C11.25 8.82969 11.0656 8.38457 10.7374 8.05639C10.4092 7.7282 9.96413 7.54382 9.5 7.54382V11.0438ZM7.75 0.772827C7.75 0.358614 7.41421 0.0228271 7 0.0228271C6.58579 0.0228271 6.25 0.358614 6.25 0.772827V2.77282C6.25 3.18704 6.58579 3.52282 7 3.52282C7.41421 3.52282 7.75 3.18704 7.75 2.77282V0.772827Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"coffee-takeaway-cup\", \"keywords\": [ \"cup\", \"coffee\", \"hot\", \"takeaway\", \"drink\", \"caffeine\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.49956 0C3.29511 0 3.11125 0.124475 3.03532 0.314305L2.19566 2.41347H11.8034L10.9637 0.314305C10.8878 0.124475 10.704 0 10.4995 0H3.49956ZM3.00537 13.576L2.2518 8.6778H11.7473L10.9937 13.576C10.9562 13.8199 10.7463 14 10.4995 14H3.49956C3.25277 14 3.0429 13.8199 3.00537 13.576ZM2.1148 3.66347H11.8843C12.4838 3.66347 12.9698 4.0848 12.9698 4.60455V6.48671C12.9698 7.00646 12.4838 7.4278 11.8843 7.4278H2.1148C1.5153 7.4278 1.0293 7.00646 1.0293 6.48671V4.60455C1.0293 4.0848 1.5153 3.66347 2.1148 3.66347Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"donut\", \"keywords\": [ \"dessert\", \"donut\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187179)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.5 7C13.5 10.5899 10.5899 13.5 7 13.5C3.41015 13.5 0.5 10.5899 0.5 7C0.5 3.41015 3.41015 0.5 7 0.5C10.5899 0.5 13.5 3.41015 13.5 7ZM9 7C9 8.10457 8.10457 9 7 9C5.89543 9 5 8.10457 5 7C5 5.89543 5.89543 5 7 5C8.10457 5 9 5.89543 9 7ZM12.059 6.77951C12.2134 6.47077 12.0882 6.09535 11.7795 5.94098C11.4708 5.78661 11.0954 5.91176 10.941 6.22049L10.441 7.22049C10.2866 7.52923 10.4118 7.90465 10.7205 8.05902C11.0292 8.21339 11.4046 8.08824 11.559 7.77951L12.059 6.77951ZM2.55806 8.55806C2.80214 8.31398 3.19786 8.31398 3.44194 8.55806L4.44194 9.55806C4.68602 9.80214 4.68602 10.1979 4.44194 10.4419C4.19786 10.686 3.80214 10.686 3.55806 10.4419L2.55806 9.44194C2.31398 9.19786 2.31398 8.80214 2.55806 8.55806ZM8.44194 2.05806C8.19786 1.81398 7.80214 1.81398 7.55806 2.05806C7.31398 2.30214 7.31398 2.69786 7.55806 2.94194L8.55806 3.94194C8.80214 4.18602 9.19786 4.18602 9.44194 3.94194C9.68602 3.69786 9.68602 3.30214 9.44194 3.05806L8.44194 2.05806ZM3.5 5.5C4.05228 5.5 4.5 5.05228 4.5 4.5C4.5 3.94772 4.05228 3.5 3.5 3.5C2.94772 3.5 2.5 3.94772 2.5 4.5C2.5 5.05228 2.94772 5.5 3.5 5.5ZM9.5 11C9.5 11.5523 9.05228 12 8.5 12C7.94772 12 7.5 11.5523 7.5 11C7.5 10.4477 7.94772 10 8.5 10C9.05228 10 9.5 10.4477 9.5 11Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187179\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"fork-knife\", \"keywords\": [ \"fork\", \"spoon\", \"knife\", \"food\", \"dine\", \"cook\", \"utensils\", \"eat\", \"restaurant\", \"dining\", \"kitchenware\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187119)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.97427 0.14935C8.26908 -0.0328564 8.63723 -0.0494204 8.94721 0.105574C11.2804 1.27216 12.4141 3.30942 12.9615 5.22528C13.5001 7.11028 13.5 8.97447 13.5 9.97651V10C13.5 10.2652 13.3946 10.5196 13.2071 10.7071C13.0196 10.8946 12.7652 11 12.5 11H9.5V13C9.5 13.5523 9.05228 14 8.5 14C7.94772 14 7.5 13.5523 7.5 13V10V1C7.5 0.653423 7.67945 0.331557 7.97427 0.14935ZM1 0.25C1.41421 0.25 1.75 0.585786 1.75 1V3.5C1.75 3.96413 1.93437 4.40925 2.26256 4.73744C2.40555 4.88042 2.57072 4.99611 2.75 5.08114V1C2.75 0.585786 3.08579 0.25 3.5 0.25C3.91421 0.25 4.25 0.585786 4.25 1V5.08114C4.43075 4.9954 4.59567 4.8792 4.73744 4.73744C4.89994 4.57493 5.02884 4.38202 5.11679 4.1697C5.20473 3.95738 5.25 3.72981 5.25 3.5V1C5.25 0.585786 5.58579 0.25 6 0.25C6.41421 0.25 6.75 0.585786 6.75 1V3.5C6.75 3.9268 6.66594 4.34941 6.50261 4.74372C6.33928 5.13803 6.09989 5.49631 5.7981 5.7981C5.49631 6.09989 5.13803 6.33928 4.74372 6.50261C4.66358 6.5358 4.58227 6.56572 4.5 6.59233V13C4.5 13.5523 4.05228 14 3.5 14C2.94772 14 2.5 13.5523 2.5 13V6.59233C2.015 6.43548 1.56905 6.16525 1.2019 5.7981C0.59241 5.1886 0.25 4.36195 0.25 3.5V1C0.25 0.585786 0.585786 0.25 1 0.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187119\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"fork-spoon\", \"keywords\": [ \"fork\", \"spoon\", \"food\", \"dine\", \"cook\", \"utensils\", \"eat\", \"restaurant\", \"dining\", \"kitchenware\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187158)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.785156 0.0240479C1.19937 0.0240479 1.53516 0.359834 1.53516 0.774048V3.27405C1.53516 3.73818 1.71953 4.1833 2.04772 4.51148C2.1907 4.65447 2.35588 4.77015 2.53516 4.85519V0.774048C2.53516 0.359834 2.87094 0.0240479 3.28516 0.0240479C3.69937 0.0240479 4.03516 0.359834 4.03516 0.774048V4.85519C4.21591 4.76945 4.38083 4.65325 4.52259 4.51148C4.6851 4.34898 4.814 4.15606 4.90195 3.94374C4.98989 3.73142 5.03516 3.50386 5.03516 3.27405V0.774048C5.03516 0.359834 5.37094 0.0240479 5.78516 0.0240479C6.19937 0.0240479 6.53516 0.359834 6.53516 0.774048V3.27405C6.53516 3.70084 6.45109 4.12346 6.28777 4.51777C6.12444 4.91208 5.88505 5.27035 5.58326 5.57214C5.28146 5.87394 4.92319 6.11333 4.52888 6.27666C4.4489 6.30979 4.36775 6.33965 4.28565 6.36622V13.0001C4.28565 13.5523 3.83793 14.0001 3.28565 14.0001C2.73336 14.0001 2.28565 13.5523 2.28565 13.0001V6.36654C1.80046 6.20971 1.35433 5.93942 0.987059 5.57214C0.377566 4.96265 0.0351562 4.136 0.0351562 3.27405V0.774048C0.0351562 0.359834 0.370942 0.0240479 0.785156 0.0240479ZM10.8354 0.0240479C9.06357 0.0240479 7.89432 1.75089 7.89432 3.51291C7.89432 4.91717 8.63695 6.29907 9.83539 6.80236V13.0001C9.83539 13.5524 10.2831 14.0001 10.8354 14.0001C11.3877 14.0001 11.8354 13.5524 11.8354 13.0001V6.80238C13.0339 6.29911 13.7765 4.91719 13.7765 3.51291C13.7765 1.75089 12.6073 0.0240479 10.8354 0.0240479Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187158\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ice-cream-2\", \"keywords\": [ \"cook\", \"frozen\", \"popsicle\", \"freezer\", \"nutrition\", \"cream\", \"stick\", \"cold\", \"ice\", \"cooking\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.70508 1.44875C2.70508 0.648625 3.3537 0 4.15382 0H9.84629C10.6465 0 11.2951 0.648625 11.2951 1.44875V8.08996C11.2951 8.89008 10.6465 9.53871 9.84629 9.53871H4.15382C3.3537 9.53871 2.70508 8.89008 2.70508 8.08996V1.44875ZM8.14166 12.8584V10.8583H5.85852V12.8584C5.85852 13.1612 5.97879 13.4516 6.19288 13.6656C6.40696 13.8797 6.69732 14 7.00009 14C7.30285 14 7.59321 13.8797 7.8073 13.6656C8.02138 13.4516 8.14166 13.1612 8.14166 12.8584ZM5.34585 2.375C5.69103 2.375 5.97085 2.65482 5.97085 3V6.5C5.97085 6.84518 5.69103 7.125 5.34585 7.125C5.00068 7.125 4.72085 6.84518 4.72085 6.5V3C4.72085 2.65482 5.00068 2.375 5.34585 2.375ZM9.27927 3C9.27927 2.65482 8.99944 2.375 8.65427 2.375C8.30909 2.375 8.02927 2.65482 8.02927 3V6.5C8.02927 6.84518 8.30909 7.125 8.65427 7.125C8.99944 7.125 9.27927 6.84518 9.27927 6.5V3Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"ice-cream-3\", \"keywords\": [ \"cook\", \"frozen\", \"cone\", \"cream\", \"ice\", \"cooking\", \"nutrition\", \"freezer\", \"cold\", \"food\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187170)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.47406 2.4055C9.4371 1.07057 8.34334 -0.000244141 6.99949 -0.000244141C5.65563 -0.000244141 4.56187 1.07057 4.52491 2.4055C3.35744 2.60471 2.46875 3.62141 2.46875 4.84566C2.46875 6.21286 3.57708 7.3212 4.94429 7.3212C5.80036 7.3212 6.55493 6.88667 6.99948 6.22613C7.44404 6.88667 8.19862 7.3212 9.05468 7.3212C10.4219 7.3212 11.5302 6.21286 11.5302 4.84566C11.5302 3.62141 10.6415 2.60471 9.47406 2.4055ZM6.02875 13.3575L3.93307 8.43084C4.25462 8.52135 4.59379 8.56977 4.94428 8.56977C5.70429 8.56977 6.41088 8.34174 6.99948 7.95154C7.58808 8.34174 8.29467 8.56977 9.05468 8.56977C9.403 8.56977 9.74015 8.52195 10.06 8.43251L7.94997 13.3582C7.87171 13.5474 7.73929 13.7093 7.56927 13.8236C7.39786 13.9388 7.19602 14.0003 6.9895 14.0003C6.78298 14.0003 6.58114 13.9388 6.40973 13.8236C6.23951 13.7092 6.10697 13.547 6.02875 13.3575Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187170\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"lemon-fruit-seasoning\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187152)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.3261 0.591063C11.8501 0.62185 12.3424 0.835638 12.7542 1.24747C13.1658 1.65905 13.3796 2.15107 13.4106 2.67469C13.441 3.18844 13.2942 3.70083 13.0424 4.17262C12.9326 4.37837 12.9073 4.60303 12.9651 4.79483C13.562 6.77608 13.3257 9.3325 11.3292 11.329C9.33197 13.3263 6.77427 13.562 4.79267 12.9641C4.60086 12.9062 4.37612 12.9315 4.17027 13.0413C3.69861 13.2928 3.18641 13.4393 2.67286 13.4088C2.14945 13.3777 1.65766 13.1639 1.24624 12.7525C0.834251 12.3405 0.620463 11.8479 0.589806 11.3237C0.559729 10.8095 0.707088 10.2967 0.959527 9.82455C1.06939 9.61907 1.09494 9.39458 1.03736 9.20282C0.442655 7.22242 0.680525 4.66914 2.67498 2.67468C4.67026 0.679393 7.22483 0.442158 9.20558 1.0378C9.39735 1.09546 9.62191 1.06998 9.8275 0.960145C10.2994 0.70798 10.8121 0.560857 11.3261 0.591063ZM5.05374 4.89382C6.0316 3.91596 7.27107 3.79401 8.25997 4.09138C8.65664 4.21067 9.07489 3.9858 9.19418 3.58914C9.31346 3.19247 9.0886 2.77421 8.69193 2.65493C7.2732 2.2283 5.4294 2.39684 3.99308 3.83316C3.213 4.61324 2.80035 5.52423 2.66262 6.42742C2.60018 6.83689 2.8815 7.21946 3.29098 7.28191C3.70046 7.34435 4.08303 7.06302 4.14548 6.65354C4.23721 6.05203 4.50981 5.43775 5.05374 4.89382Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187152\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"microwave\", \"keywords\": [ \"cook\", \"food\", \"appliances\", \"cooking\", \"nutrition\", \"appliance\", \"microwave\", \"kitchenware\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 3C0 2.44772 0.447715 2 1 2H13C13.5523 2 14 2.44772 14 3V11C14 11.5523 13.5523 12 13 12H1C0.447715 12 0 11.5523 0 11V3ZM2.54627 4.08752H8.95369C9.27539 4.08752 9.53618 4.34832 9.53618 4.67002V9.32996C9.53618 9.65166 9.27539 9.91245 8.95369 9.91245H2.54627C2.22457 9.91245 1.96378 9.65166 1.96378 9.32996V4.67002C1.96378 4.34832 2.22457 4.08752 2.54627 4.08752ZM10.6921 4.83624C10.6921 4.49106 10.9719 4.21124 11.3171 4.21124H12.3635C12.7087 4.21124 12.9885 4.49106 12.9885 4.83624C12.9885 5.18142 12.7087 5.46124 12.3635 5.46124H11.3171C10.9719 5.46124 10.6921 5.18142 10.6921 4.83624ZM11.3171 6.375C10.9719 6.375 10.6921 6.65482 10.6921 7C10.6921 7.34518 10.9719 7.625 11.3171 7.625H12.3635C12.7087 7.625 12.9885 7.34518 12.9885 7C12.9885 6.65482 12.7087 6.375 12.3635 6.375H11.3171Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"milkshake\", \"keywords\": [ \"milkshake\", \"drink\", \"takeaway\", \"cup\", \"cold\", \"beverage\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.72111 0.066513C10.1158 0.192104 10.334 0.613895 10.2084 1.00861L9.69839 2.6115C10.5997 3.32525 11.1994 4.40343 11.2747 5.62349H11.9996C12.4138 5.62349 12.7496 5.95928 12.7496 6.37349C12.7496 6.78771 12.4138 7.12349 11.9996 7.12349H11.231L10.8582 12.3434C10.7928 13.2592 10.0308 13.9687 9.11264 13.9687H5.10756C4.18944 13.9687 3.42742 13.2592 3.36201 12.3434L2.98916 7.12349H2.2207C1.80649 7.12349 1.4707 6.78771 1.4707 6.37349C1.4707 5.95928 1.80649 5.62349 2.2207 5.62349H2.94553C3.08028 3.44063 4.89331 1.71193 7.1101 1.71193C7.54216 1.71193 7.95889 1.7776 8.35084 1.8995L8.77901 0.553805C8.9046 0.15909 9.3264 -0.0590781 9.72111 0.066513ZM4.45008 5.62349C4.58129 4.26996 5.72214 3.21193 7.1101 3.21193C8.49807 3.21193 9.63891 4.26996 9.77012 5.62349H4.45008ZM9.60617 8.8182L9.72722 7.12349H4.49298L4.61403 8.8182H9.60617Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"popcorn\", \"keywords\": [ \"cook\", \"corn\", \"movie\", \"snack\", \"cooking\", \"nutrition\", \"bake\", \"popcorn\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187182)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.4015 2.77345C11.2683 2.77551 11.136 2.79363 11.0073 2.8274C11.1074 2.59149 11.1594 2.33851 11.1606 2.08281C11.1406 1.51986 10.8981 0.986902 10.4849 0.597764C10.0716 0.208626 9.52041 -0.00585539 8.9489 0.000121651C8.55276 0.00116345 8.16433 0.108098 7.82498 0.309533C7.48563 0.510969 7.20808 0.79936 7.0219 1.14398C6.83571 0.79936 6.55816 0.510969 6.21881 0.309533C5.87947 0.108098 5.49103 0.00116345 5.09489 0.000121651C4.52716 -0.000221174 3.98149 0.216765 3.57296 0.605316C3.16443 0.993867 2.92498 1.52361 2.90511 2.08281C2.90626 2.33851 2.95834 2.59149 3.05839 2.8274C2.92264 2.79184 2.7828 2.7737 2.64234 2.77345C2.20676 2.77345 1.78903 2.94399 1.48103 3.24755C1.17303 3.55111 1 3.96282 1 4.39212C1 4.82142 1.17303 5.23313 1.48103 5.53669C1.78903 5.84025 2.20676 6 2.64234 6H11.3577C11.7932 6 12.211 5.82946 12.519 5.5259C12.827 5.22234 13 4.81063 13 4.38133C13 3.95203 12.827 3.54031 12.519 3.23675C12.211 2.93319 11.7932 2.76266 11.3577 2.76266L11.4015 2.77345ZM11.3577 7.25002H7.75V14H9.62448C10.3763 14 11.012 13.4435 11.1113 12.6983L11.8432 7.20932C11.6837 7.23625 11.5213 7.25002 11.3577 7.25002ZM2.64234 7.25002C2.47994 7.25002 2.31751 7.23689 2.15698 7.21056L2.88867 12.6983C2.98803 13.4435 3.62371 14 4.37551 14H6.25V7.25002H2.64234Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187182\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pork-meat\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187149)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.24786 0.548575C2.09295 0.474803 1.91103 0.485616 1.76594 0.577219C1.62086 0.668822 1.53288 0.82842 1.53288 1V4.01935C1.53288 4.17892 1.45342 4.37022 1.29278 4.56679C0.807653 5.16042 0.125 6.36153 0.125 8.38267C0.125 10.2246 0.990169 11.533 2.30411 12.3575C3.59159 13.1654 5.28935 13.5 7 13.5C8.71065 13.5 10.4084 13.1654 11.6959 12.3575C13.0098 11.533 13.875 10.2246 13.875 8.38267C13.875 6.36153 13.1923 5.16042 12.7072 4.56679C12.5466 4.37022 12.4671 4.17892 12.4671 4.01935V1C12.4671 0.82842 12.3791 0.668822 12.2341 0.577219C12.089 0.485616 11.9071 0.474803 11.7521 0.548575L8.75856 1.97417L8.72859 1.96763C8.60424 1.94061 8.43151 1.90455 8.23797 1.86842C7.86293 1.7984 7.36784 1.71986 7 1.71986C6.63216 1.71986 6.13707 1.7984 5.76203 1.86842C5.56849 1.90455 5.39576 1.94061 5.27141 1.96763L5.24144 1.97417L2.24786 0.548575ZM9.9375 8.96875C9.9375 10.3125 8.62234 11.125 7 11.125C5.37766 11.125 4.0625 10.375 4.0625 8.96875C4.0625 7.5625 5.37766 6.8125 7 6.8125C8.62234 6.8125 9.9375 7.625 9.9375 8.96875ZM4.1875 6.25C3.84232 6.25 3.5625 5.97018 3.5625 5.625C3.5625 5.27982 3.84232 5 4.1875 5C4.53268 5 4.8125 5.27982 4.8125 5.625C4.8125 5.97018 4.53268 6.25 4.1875 6.25ZM9.8125 6.25C10.1577 6.25 10.4375 5.97018 10.4375 5.625C10.4375 5.27982 10.1577 5 9.8125 5C9.46732 5 9.1875 5.27982 9.1875 5.625C9.1875 5.97018 9.46732 6.25 9.8125 6.25ZM8 9.71875C8.34518 9.71875 8.625 9.43893 8.625 9.09375C8.625 8.74857 8.34518 8.46875 8 8.46875C7.65482 8.46875 7.375 8.74857 7.375 9.09375C7.375 9.43893 7.65482 9.71875 8 9.71875ZM5.375 9.09375C5.375 8.74857 5.65482 8.46875 6 8.46875C6.34518 8.46875 6.625 8.74857 6.625 9.09375C6.625 9.43893 6.34518 9.71875 6 9.71875C5.65482 9.71875 5.375 9.43893 5.375 9.09375Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187149\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"refrigerator\", \"keywords\": [ \"fridge\", \"cook\", \"appliances\", \"cooking\", \"nutrition\", \"freezer\", \"appliance\", \"food\", \"kitchenware\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.5 0C2.68181 0 2 0.651871 2 1.47862V5.37564C2.00948 5.37522 2.01901 5.375 2.02859 5.375H11.9714C11.981 5.375 11.9905 5.37522 12 5.37564V1.47862C12 0.651871 11.3182 0 10.5 0H3.5ZM2 11.2648V6.62436C2.00948 6.62478 2.01901 6.625 2.02859 6.625H11.9714C11.981 6.625 11.9905 6.62478 12 6.62436V11.2648C12 12.0057 11.4525 12.6061 10.75 12.7229V13.2435C10.75 13.6577 10.4142 13.9935 10 13.9935C9.58579 13.9935 9.25 13.6577 9.25 13.2435V12.7435H4.75V13.2435C4.75 13.6577 4.41421 13.9935 4 13.9935C3.58579 13.9935 3.25 13.6577 3.25 13.2435V12.7229C2.54749 12.6061 2 12.0057 2 11.2648ZM5.04126 2.60928C5.04126 2.26411 4.76144 1.98428 4.41626 1.98428C4.07108 1.98428 3.79126 2.26411 3.79126 2.60928V3.10928C3.79126 3.45446 4.07108 3.73428 4.41626 3.73428C4.76144 3.73428 5.04126 3.45446 5.04126 3.10928V2.60928Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"serving-dome\", \"keywords\": [ \"cook\", \"tool\", \"dome\", \"kitchen\", \"serving\", \"paltter\", \"dish\", \"tools\", \"food\", \"kitchenware\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187201)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 0.103271C7.41421 0.103271 7.75 0.439057 7.75 0.853271V1.89354C9.33074 2.06384 10.815 2.76876 11.9497 3.90352C13.2625 5.21628 14 6.99676 14 8.85327C14 9.2511 13.842 9.63263 13.5607 9.91393C13.2794 10.1953 12.8978 10.3533 12.5 10.3533H1.5C1.10217 10.3533 0.720644 10.1953 0.43934 9.91393C0.158035 9.63263 0 9.2511 0 8.85327C0 6.99676 0.737498 5.21628 2.05025 3.90352C3.18501 2.76877 4.66926 2.06384 6.25 1.89354V0.853271C6.25 0.439057 6.58579 0.103271 7 0.103271ZM0.78064 11.75C0.366426 11.75 0.0306396 12.0858 0.0306396 12.5C0.0306396 12.9142 0.366426 13.25 0.78064 13.25H13.2193C13.6335 13.25 13.9693 12.9142 13.9693 12.5C13.9693 12.0858 13.6335 11.75 13.2193 11.75H0.78064Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187201\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shrimp\", \"keywords\": [ \"sea\", \"food\", \"shrimp\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187189)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.34743 1.78125C1.93118 1.78125 1.59375 2.11868 1.59375 2.53493C1.59375 2.95117 1.93118 3.2886 2.34743 3.2886H3.34985V3.28674H8.59028C8.63147 3.34571 8.68594 3.39651 8.75203 3.43418C8.75567 3.43768 8.76104 3.4431 8.76811 3.45091C8.7943 3.47982 8.83093 3.52723 8.87504 3.59655C8.96305 3.73484 9.06351 3.93296 9.15833 4.17948C9.34812 4.67293 9.5 5.32037 9.5 5.99999C9.5 6.6796 9.34812 7.32705 9.15833 7.8205C9.06351 8.06701 8.96305 8.26514 8.87504 8.40343C8.83093 8.47275 8.7943 8.52016 8.76811 8.54907C8.76104 8.55688 8.75567 8.5623 8.75203 8.5658C8.65564 8.62073 8.58397 8.70362 8.54216 8.799H7.34985C5.14071 8.799 3.34985 7.00814 3.34985 4.799V4.7886H2.34743C1.10276 4.7886 0.09375 3.7796 0.09375 2.53493C0.09375 1.29025 1.10276 0.28125 2.34743 0.28125H6.85846C7.27267 0.28125 7.60846 0.617036 7.60846 1.03125C7.60846 1.44546 7.27267 1.78125 6.85846 1.78125H2.34743ZM10.0917 3.8205C10.0333 3.66864 9.97084 3.52604 9.90619 3.39538C11.4933 3.73125 12.8032 4.81817 13.4465 6.26676C13.0214 8.34204 11.5588 9.4776 10.6533 9.89989C10.448 9.49418 10.1089 9.16771 9.6939 8.97859C9.70224 8.96594 9.71051 8.95317 9.71871 8.9403C9.84945 8.73484 9.97815 8.47463 10.0917 8.17948C10.3186 7.5896 10.5 6.82037 10.5 5.99999C10.5 5.1796 10.3186 4.41038 10.0917 3.8205ZM10.8661 10.8973C11.7313 10.5465 13.0623 9.65025 13.8703 8.06644C13.8738 8.14352 13.8756 8.22105 13.8756 8.299C13.8756 10.7248 12.1524 13.132 9.86332 13.6904V13.0607C9.86332 12.8921 9.84816 12.727 9.81914 12.5667C10.4183 12.2412 10.833 11.619 10.8661 10.8973ZM8.8463 12.8153C8.85752 12.8955 8.86332 12.9774 8.86332 13.0607V13.8125H7.86332H6.19011C5.72875 13.8125 5.35474 13.4385 5.35474 12.9771C5.35474 12.0544 6.10276 11.3064 7.02549 11.3064H7.10903C7.99365 11.3064 8.72535 11.9612 8.8459 12.8125L8.8463 12.8153ZM7.35963 5.5423C7.35963 5.95753 7.02302 6.29414 6.6078 6.29414C6.19257 6.29414 5.85596 5.95753 5.85596 5.5423C5.85596 5.12708 6.19257 4.79047 6.6078 4.79047C7.02302 4.79047 7.35963 5.12708 7.35963 5.5423Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187189\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"strawberry\", \"keywords\": [ \"fruit\", \"sweet\", \"berries\", \"plant\", \"strawberry\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187198)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5 0.0452881C4.58579 0.0452881 4.25 0.381074 4.25 0.795288C4.25 1.2095 4.58579 1.54529 5 1.54529C5.11935 1.54529 5.43537 1.66457 5.76737 2.01592C5.93117 2.18926 6.0543 2.37313 6.13519 2.54781C5.14084 2.6611 4.22354 2.98091 3.50015 3.55963C2.55377 4.31674 2 5.46888 2 7.00006C2 8.86594 2.6648 10.5934 3.59747 11.8592C4.51301 13.1017 5.76832 14.0001 7 14.0001C8.23168 14.0001 9.48699 13.1017 10.4025 11.8592C11.3352 10.5934 12 8.86594 12 7.00006C12 5.46888 11.4462 4.31674 10.4998 3.55963C9.77646 2.98091 8.85916 2.6611 7.86481 2.54781C7.9457 2.37313 8.06883 2.18926 8.23263 2.01592C8.56463 1.66457 8.88065 1.54529 9 1.54529C9.41421 1.54529 9.75 1.2095 9.75 0.795288C9.75 0.381074 9.41421 0.0452881 9 0.0452881C8.28602 0.0452881 7.60204 0.499245 7.14237 0.985704C7.09422 1.03666 7.04663 1.08995 7 1.14538C6.95337 1.08995 6.90578 1.03666 6.85763 0.985704C6.39796 0.499245 5.71398 0.0452881 5 0.0452881ZM5.5 6.75006C5.5 7.16427 5.16421 7.50006 4.75 7.50006C4.33579 7.50006 4 7.16427 4 6.75006C4 6.33585 4.33579 6.00006 4.75 6.00006C5.16421 6.00006 5.5 6.33585 5.5 6.75006ZM6.5 9.80817C6.5 10.2224 6.16421 10.5582 5.75 10.5582C5.33579 10.5582 5 10.2224 5 9.80817C5 9.39395 5.33579 9.05817 5.75 9.05817C6.16421 9.05817 6.5 9.39395 6.5 9.80817ZM8.25 10.5582C8.66421 10.5582 9 10.2224 9 9.80817C9 9.39395 8.66421 9.05817 8.25 9.05817C7.83579 9.05817 7.5 9.39395 7.5 9.80817C7.5 10.2224 7.83579 10.5582 8.25 10.5582ZM7.75 6.75006C7.75 7.16427 7.41421 7.50006 7 7.50006C6.58579 7.50006 6.25 7.16427 6.25 6.75006C6.25 6.33585 6.58579 6.00006 7 6.00006C7.41421 6.00006 7.75 6.33585 7.75 6.75006ZM9.25 7.50006C9.66421 7.50006 10 7.16427 10 6.75006C10 6.33585 9.66421 6.00006 9.25 6.00006C8.83579 6.00006 8.5 6.33585 8.5 6.75006C8.5 7.16427 8.83579 7.50006 9.25 7.50006Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187198\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"tea-cup\", \"keywords\": [ \"herbal\", \"cook\", \"tea\", \"tisane\", \"cup\", \"drink\", \"cooking\", \"nutrition\", \"mug\", \"food\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187164)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.2626 0.769409C3.2626 0.355196 2.92682 0.0194092 2.5126 0.0194092C2.09839 0.0194092 1.7626 0.355196 1.7626 0.769409V1.5C1.7626 1.91422 2.09839 2.25 2.5126 2.25C2.92682 2.25 3.2626 1.91422 3.2626 1.5V0.769409ZM10.7237 7.23V4.73H11.2237C11.5552 4.73 11.8732 4.8617 12.1076 5.09612C12.342 5.33054 12.4737 5.64848 12.4737 5.98C12.4737 6.31152 12.342 6.62946 12.1076 6.86388C11.8732 7.0983 11.5552 7.23 11.2237 7.23H10.7237ZM10.7237 8.73001L11.2237 8.73C11.9531 8.73 12.6525 8.44027 13.1682 7.92454C13.684 7.40882 13.9737 6.70935 13.9737 5.98C13.9737 5.25065 13.684 4.55118 13.1682 4.03546C12.6525 3.51973 11.9531 3.23 11.2237 3.23H9.22376L9.21966 3.23001H1.72376C1.45854 3.23001 1.20418 3.33537 1.01665 3.5229C0.829111 3.71044 0.723755 3.96479 0.723755 4.23001V8.73001C0.723755 9.39305 0.987147 10.0289 1.45599 10.4978C1.92483 10.9666 2.56071 11.23 3.22376 11.23H8.22376C8.8868 11.23 9.52268 10.9666 9.99152 10.4978C10.4603 10.0289 10.7237 9.39305 10.7237 8.73001ZM0.777343 12.48C0.36313 12.48 0.0273438 12.8158 0.0273438 13.23C0.0273438 13.6442 0.36313 13.98 0.777343 13.98H13.2237C13.6379 13.98 13.9737 13.6442 13.9737 13.23C13.9737 12.8158 13.6379 12.48 13.2237 12.48H0.777343ZM8.4885 0.0194092C8.90271 0.0194092 9.2385 0.355196 9.2385 0.769409V1.5C9.2385 1.91422 8.90271 2.25 8.4885 2.25C8.07428 2.25 7.7385 1.91422 7.7385 1.5V0.769409C7.7385 0.355196 8.07428 0.0194092 8.4885 0.0194092ZM6.25055 0.769409C6.25055 0.355196 5.91476 0.0194092 5.50055 0.0194092C5.08634 0.0194092 4.75055 0.355196 4.75055 0.769409V1.5C4.75055 1.91422 5.08634 2.25 5.50055 2.25C5.91476 2.25 6.25055 1.91422 6.25055 1.5V0.769409Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187164\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"toast\", \"keywords\": [ \"bread\", \"toast\", \"breakfast\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187185)\\\">\\n<path d=\\\"M9.5 4.5C9.5 4.1717 9.43534 3.84661 9.3097 3.54329C9.18406 3.23998 8.99991 2.96438 8.76777 2.73223C8.53562 2.50009 8.26002 2.31594 7.95671 2.1903C7.65339 2.06466 7.3283 2 7 2H2.5C1.97529 2 1.46389 2.16509 1.03822 2.47189C0.612561 2.7787 0.294218 3.21165 0.128292 3.70943C-0.0376349 4.20721 -0.0427342 4.74458 0.113716 5.24542C0.270167 5.74626 0.580235 6.18518 1 6.5V11.5C1 11.7652 1.10536 12.0196 1.29289 12.2071C1.48043 12.3946 1.73478 12.5 2 12.5H7.5C7.76522 12.5 8.01957 12.3946 8.20711 12.2071C8.39464 12.0196 8.5 11.7652 8.5 11.5V6.5C8.81107 6.26767 9.0635 5.96583 9.23713 5.61856C9.41076 5.2713 9.50078 4.88825 9.5 4.5Z\\\" fill=\\\"black\\\"/>\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.6953 2.96927C10.8963 3.45457 10.9998 3.9717 10.9998 4.49699C11.001 5.11912 10.8568 5.73293 10.5785 6.28938C10.5439 6.35869 10.5073 6.42687 10.4688 6.49385H12.9998C13.0025 6.49385 13.0052 6.49387 13.0079 6.4939C13.3152 6.2624 13.5648 5.9628 13.7369 5.61856C13.9105 5.2713 14.0006 4.88825 13.9998 4.5C13.9998 4.1717 13.9351 3.84661 13.8095 3.54329C13.6838 3.23998 13.4997 2.96438 13.2675 2.73223C13.0354 2.50009 12.7598 2.31594 12.4565 2.1903C12.1532 2.06466 11.8281 2 11.4998 2H10.1223C10.3576 2.29388 10.5507 2.6201 10.6953 2.96927ZM12.9998 7.74385H9.99973V11.5C9.99973 11.8475 9.92738 12.1876 9.79102 12.5H11.9998C12.265 12.5 12.5193 12.3946 12.7069 12.2071C12.8944 12.0196 12.9998 11.7652 12.9998 11.5V7.74385Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187185\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"water-glass\", \"keywords\": [ \"glass\", \"water\", \"juice\", \"drink\", \"liquid\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187176)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.17482 0.277457C1.31712 0.118019 1.52067 0.0268555 1.73438 0.0268555H12.266C12.4799 0.0268555 12.6837 0.118215 12.826 0.277949C12.9683 0.437683 13.0356 0.650587 13.011 0.863101L12.4657 5.57367L12.4654 5.57643L11.7664 11.6148C11.7003 12.1856 11.4518 12.7175 11.0612 13.1332C11.0055 13.1926 10.9468 13.2496 10.8853 13.304C10.4248 13.7115 9.83863 13.9463 9.22681 13.9711C9.18602 13.9727 9.14512 13.9734 9.10413 13.9732H4.87703C4.30321 13.9764 3.74595 13.7944 3.28657 13.4577C3.22094 13.4096 3.15731 13.3583 3.09592 13.304C2.63485 12.8959 2.33027 12.3417 2.23178 11.7365C2.22521 11.6961 2.21956 11.6556 2.21484 11.6148L2.21469 11.6135L1.52643 5.57512L0.989204 0.861791C0.965003 0.649463 1.03252 0.436894 1.17482 0.277457ZM11.4241 1.52686L11.0522 4.74019H2.94097L2.57472 1.52686H11.4241Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187176\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"wine\", \"keywords\": [ \"drink\", \"cook\", \"glass\", \"cooking\", \"wine\", \"nutrition\", \"food\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.89673 1.52783C3.85207 1.52756 3.80889 1.54385 3.77557 1.57351C3.74288 1.6026 3.7219 1.64254 3.71646 1.68589C3.65875 2.27016 3.61621 2.99054 3.59065 3.56952H10.4094C10.3838 2.99052 10.3413 2.2701 10.2836 1.68582C10.2781 1.6425 10.2571 1.60258 10.2245 1.57351C10.1911 1.54384 10.148 1.52756 10.1033 1.52783L10.0988 1.52786L3.90128 1.52785L3.89673 1.52783ZM3.90323 0.0278492C3.48872 0.0259755 3.08805 0.177366 2.77833 0.453018C2.46797 0.72923 2.27094 1.11061 2.22524 1.52356L2.22435 1.53208C2.11955 2.58965 2.0625 4.05669 2.0625 4.50007C2.0625 5.92846 2.61685 7.07618 3.55566 7.85322C4.29667 8.46655 5.23729 8.81722 6.25 8.93097V12.4064H5C4.58579 12.4064 4.25 12.7422 4.25 13.1564C4.25 13.5706 4.58579 13.9064 5 13.9064H9C9.41421 13.9064 9.75 13.5706 9.75 13.1564C9.75 12.7422 9.41421 12.4064 9 12.4064H7.75V8.93098C8.76273 8.81723 9.70336 8.46656 10.4444 7.85322C11.3832 7.07618 11.9375 5.92845 11.9375 4.50007C11.9375 4.05669 11.8805 2.58965 11.7757 1.53208L11.7748 1.52356C11.7291 1.11061 11.5321 0.72923 11.2217 0.453018C10.912 0.177381 10.5114 0.0259753 10.0968 0.0278492H10.0988V0.777849L10.0942 0.027863L10.0968 0.0278492H3.90323Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" } ], \"health\": [ { \"name\": \"ambulance\", \"keywords\": [ \"car\", \"emergency\", \"health\", \"medical\", \"ambulance\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.3522 4.36782L13.6904 7.10757H10.5664V3.52612H11.0044C11.5776 3.52612 12.1007 3.85278 12.3522 4.36782ZM8.15754 2.5C8.62431 2.5 9.04129 2.71321 9.3164 3.04755V7.73257C9.3164 8.07775 9.59622 8.35757 9.9414 8.35757H13.7954V10.439C13.7954 11.2675 13.1238 11.939 12.2954 11.939H12.1288C12.1295 11.9592 12.1298 11.9794 12.1298 11.9997C12.1298 13.0468 11.2809 13.8957 10.2338 13.8957C9.22904 13.8957 8.4068 13.114 8.342 12.1256H5.78878C5.72398 13.114 4.90174 13.8957 3.89693 13.8957C2.89213 13.8957 2.06988 13.114 2.00509 12.1256H1.70117C0.872746 12.1256 0.201172 11.454 0.201172 10.6256V4C0.201172 3.17157 0.872745 2.5 1.70117 2.5H8.15754ZM4.92968 4.75C5.27486 4.75 5.55468 5.02982 5.55468 5.375V6.375H6.55468C6.89986 6.375 7.17968 6.65482 7.17968 7C7.17968 7.34518 6.89986 7.625 6.55468 7.625H5.55468V8.625C5.55468 8.97018 5.27486 9.25 4.92968 9.25C4.58451 9.25 4.30468 8.97018 4.30468 8.625V7.625H3.30468C2.95951 7.625 2.67968 7.34518 2.67968 7C2.67968 6.65482 2.95951 6.375 3.30468 6.375H4.30468V5.375C4.30468 5.02982 4.58451 4.75 4.92968 4.75Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"bacteria-virus-cells-biology\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189837)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.51926 0.43428C6.34489 0.0585551 5.89896 -0.10468 5.52323 0.0696859C5.14751 0.244051 4.98427 0.689988 5.15864 1.06571L5.45773 1.71021C5.22835 1.89342 4.9967 2.11839 4.77091 2.37293L4.20204 1.84245C3.8991 1.55996 3.42452 1.57653 3.14202 1.87947C2.85953 2.18241 2.87611 2.65699 3.17904 2.93948L3.87509 3.58855C3.73407 3.81683 3.5985 4.05708 3.47128 4.30693L2.72413 3.98809C2.34316 3.82551 1.90252 4.00256 1.73995 4.38353C1.57737 4.76451 1.75441 5.20514 2.13539 5.36772L2.89618 5.69238L2.88043 5.74166C2.78845 6.03239 2.71105 6.3314 2.65217 6.63525L1.84859 6.53521C1.43755 6.48404 1.06285 6.77577 1.01168 7.18681C0.96051 7.59785 1.25224 7.97255 1.66328 8.02372L2.5286 8.13145C2.53883 8.41203 2.56941 8.69202 2.62325 8.96888L1.95135 9.26977C1.57331 9.43907 1.40409 9.88277 1.57339 10.2608C1.74268 10.6388 2.18638 10.8081 2.56442 10.6388L3.13664 10.3825C3.35164 10.7669 3.62957 11.1344 3.98007 11.4767L3.49986 12.0531C3.23473 12.3714 3.27778 12.8443 3.59602 13.1094C3.91426 13.3746 4.38718 13.3315 4.65231 13.0133L5.18347 12.3757C5.62115 12.6239 6.06016 12.7968 6.49183 12.9029L6.49226 13.2511C6.49277 13.6653 6.82898 14.0007 7.24319 14.0002C7.6574 13.9997 7.99277 13.6635 7.99226 13.2492L7.99197 13.0104C8.55684 12.9512 9.08682 12.7792 9.55784 12.5171L10.1755 13.1813C10.4575 13.4846 10.9321 13.5019 11.2354 13.2198C11.5388 12.9378 11.556 12.4632 11.274 12.1599L10.7119 11.5554C10.9489 11.2665 11.1435 10.9459 11.2878 10.6034L12.1426 10.718C12.5532 10.773 12.9306 10.4848 12.9856 10.0743C13.0407 9.66373 12.7525 9.2863 12.3419 9.23127L11.5786 9.12894C11.5743 8.76719 11.5161 8.39783 11.3973 8.02922L11.9836 7.77949C12.3647 7.61716 12.542 7.17664 12.3797 6.79556C12.2174 6.41448 11.7768 6.23714 11.3958 6.39947L10.6842 6.70258C10.6451 6.65223 10.6046 6.6021 10.5627 6.55222C10.4322 6.36018 10.3896 6.14113 10.4065 5.84828C10.4127 5.74107 10.4256 5.63467 10.4429 5.52458H11.3393C11.7535 5.52458 12.0893 5.1888 12.0893 4.77458C12.0893 4.36037 11.7535 4.02458 11.3393 4.02458H10.6706C10.6781 3.80626 10.6664 3.57899 10.6216 3.34763C10.619 3.33421 10.6163 3.32079 10.6135 3.30739L11.3362 2.82548C11.6808 2.59568 11.7739 2.13002 11.5441 1.7854C11.3143 1.44077 10.8487 1.34768 10.504 1.57748L9.89312 1.98481C9.85121 1.94113 9.80732 1.89772 9.7614 1.85458C9.6646 1.76365 9.53868 1.66401 9.38761 1.56595L9.65057 1.15276C9.87296 0.80331 9.76996 0.339741 9.42051 0.11735C9.07106 -0.105042 8.60749 -0.00204167 8.3851 0.347407L7.9489 1.03282C7.59918 0.985775 7.21805 0.990288 6.81674 1.07531L6.51926 0.43428ZM8.75207 9.27557C8.75207 9.95406 8.20205 10.5041 7.52356 10.5041C6.84506 10.5041 6.29504 9.95406 6.29504 9.27557C6.29504 8.59708 6.84506 8.04706 7.52356 8.04706C8.20205 8.04706 8.75207 8.59708 8.75207 9.27557ZM7.30188 5.53046C7.85686 5.53046 8.30676 5.08055 8.30676 4.52557C8.30676 3.97059 7.85686 3.52069 7.30188 3.52069C6.74689 3.52069 6.29699 3.97059 6.29699 4.52557C6.29699 5.08055 6.74689 5.53046 7.30188 5.53046ZM4.41418 7.00412C4.41418 6.58989 4.74998 6.25409 5.16421 6.25409C5.57861 6.25409 5.91418 6.59006 5.91418 7.00412C5.91418 7.41819 5.57861 7.75415 5.16421 7.75415C4.74998 7.75415 4.41418 7.41835 4.41418 7.00412Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189837\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bandage\", \"keywords\": [ \"health\", \"medical\", \"hospital\", \"medicine\", \"capsule\", \"bandage\", \"vaccine\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189795)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.1091 0.0452881C3.03121 0.0452881 1.99747 0.473478 1.23529 1.23566C0.473112 1.99784 0.0449219 3.03158 0.0449219 4.10947C0.0449219 5.18736 0.473112 6.2211 1.23529 6.98329L1.59845 7.34644C1.62199 7.37607 1.64731 7.40448 1.67433 7.43151L6.56801 12.3251C6.5954 12.3525 6.62399 12.378 6.6536 12.4016L7.01623 12.7642C7.77841 13.5264 8.81215 13.9546 9.89004 13.9546C10.968 13.9546 12.0017 13.5264 12.7639 12.7642C13.5261 12.002 13.9543 10.9683 13.9543 9.89041C13.9543 8.81252 13.5261 7.77878 12.7639 7.0166L12.4006 6.65327C12.3771 6.62371 12.3518 6.59536 12.3248 6.56839L7.43111 1.67472C7.40378 1.64739 7.37526 1.62197 7.34572 1.59846L6.98292 1.23566C6.22073 0.473478 5.18699 0.0452881 4.1091 0.0452881ZM3.87503 6.81352L6.81315 3.8754L10.1242 7.18638L7.18602 10.1245L3.87503 6.81352Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189795\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"blood-bag-donation\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189749)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.43254 7.80583V7.67592C3.37678 7.37494 4.97922 7.2295 7.00101 7.999C9.00227 8.7607 10.5962 8.50805 11.5459 8.109C11.4325 8.91262 10.874 9.59621 10.0879 9.85827L9.17565 10.1624C8.88117 10.2605 8.68254 10.5361 8.68254 10.8465V11.3273C8.68254 11.4601 8.57492 11.5677 8.44216 11.5677H7.02399C7.01617 11.5674 7.00833 11.5673 7.00045 11.5673C6.99258 11.5673 6.98473 11.5674 6.97692 11.5677H5.55754C5.42478 11.5677 5.31716 11.4601 5.31716 11.3273V10.8465C5.31716 10.5361 5.11853 10.2605 4.82405 10.1624L3.91186 9.85827C3.02843 9.56379 2.43254 8.73705 2.43254 7.80583ZM6.2793 13.01H5.55754C4.64131 13.01 3.89611 12.2777 3.8753 11.3664L3.45576 11.2266C1.98337 10.7358 0.990234 9.35786 0.990234 7.80583V3.63494C0.990234 1.64353 2.60459 0.0291748 4.59601 0.0291748H9.4037C11.3951 0.0291748 13.0095 1.64353 13.0095 3.63494V7.80583C13.0095 9.35786 12.0164 10.7358 10.544 11.2266L10.1244 11.3664C10.1036 12.2777 9.3584 13.01 8.44216 13.01H7.72161V13.25C7.72161 13.6483 7.39874 13.9711 7.00045 13.9711C6.60217 13.9711 6.2793 13.6483 6.2793 13.25V13.01ZM7.00206 2.83334L7.40839 2.54197C7.31449 2.41101 7.16321 2.33334 7.00206 2.33334C6.84091 2.33334 6.68964 2.41101 6.59573 2.54197L7.00206 2.83334ZM7.00206 2.83334C6.59573 2.54197 6.59577 2.54192 6.59573 2.54197L6.59465 2.54347L6.59233 2.54673L6.58407 2.55836L6.55428 2.60082C6.52895 2.63716 6.49315 2.6891 6.45037 2.75278C6.3651 2.87971 6.25061 3.05543 6.13535 3.24862C6.02109 3.44015 5.9008 3.6576 5.80754 3.86646C5.72282 4.05618 5.62743 4.30857 5.62743 4.54572C5.62743 4.91357 5.76745 5.27029 6.02282 5.53644C6.27888 5.80329 6.63067 5.95724 7.00206 5.95724C7.37346 5.95724 7.72525 5.80329 7.9813 5.53644C8.23667 5.27029 8.3767 4.91357 8.3767 4.54572C8.3767 4.30857 8.2813 4.05618 8.19658 3.86646C8.10332 3.6576 7.98304 3.44015 7.86877 3.24862C7.75352 3.05543 7.63903 2.87971 7.55375 2.75278C7.51097 2.6891 7.47517 2.63716 7.44985 2.60082L7.42005 2.55836L7.4118 2.54673L7.40947 2.54347L7.40839 2.54197C7.40835 2.54192 7.40839 2.54197 7.00206 2.83334Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189749\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"blood-donate-drop\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189758)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.99998 0.464425L7.3774 0.19379C7.29018 0.0721457 7.14967 0 6.99998 0C6.8503 0 6.70979 0.0721457 6.62256 0.19379C6.6226 0.193735 6.62256 0.19379 6.99998 0.464425ZM6.99998 0.464425L6.62256 0.19379L6.6208 0.196244L6.6164 0.202414L6.59999 0.225531C6.58579 0.245603 6.56521 0.274843 6.53914 0.312237C6.48703 0.387004 6.41296 0.494491 6.32421 0.626578C6.14701 0.890334 5.91001 1.25417 5.67229 1.65263C5.43549 2.04955 5.19311 2.48895 5.00848 2.90241C4.83179 3.29809 4.67786 3.73402 4.67786 4.10149C4.67786 4.73209 4.91804 5.34052 5.35128 5.79202C5.78515 6.24418 6.37778 6.50195 6.99998 6.50195C7.62218 6.50195 8.21482 6.24418 8.64868 5.79202C9.08192 5.34052 9.32211 4.73209 9.32211 4.10149C9.32211 3.73402 9.16817 3.29809 8.99148 2.90241C8.80686 2.48895 8.56447 2.04955 8.32767 1.65263C8.08996 1.25417 7.85295 0.890334 7.67575 0.626578C7.58701 0.494491 7.51293 0.387004 7.46082 0.312237C7.43476 0.274843 7.41417 0.245603 7.39997 0.225531L7.38356 0.202414L7.37916 0.196244L7.3774 0.19379C7.37736 0.193735 7.3774 0.19379 6.99998 0.464425ZM0 5.11186V9.83784C0 10.3683 0.210607 10.8769 0.58568 11.2519L2.7772 13.4435V13.9998H6.10985V11.1262C6.10985 10.537 5.87577 9.97188 5.45911 9.55522L4.02286 8.11897L4.01904 8.11517C3.70472 7.85456 3.2378 7.8715 2.94332 8.16598C2.63087 8.47843 2.63087 8.98501 2.94332 9.29746L3.86262 10.2168C4.05788 10.412 4.05788 10.7286 3.86262 10.9239C3.66736 11.1191 3.35077 11.1191 3.15551 10.9239L2.23622 10.0046C1.53809 9.30645 1.53327 8.17756 2.22176 7.47349V5.11186C2.22176 4.49834 1.72441 4.00098 1.11088 4.00098C0.497359 4.00098 0 4.49834 0 5.11186ZM14 9.83784V5.11186C14 4.49834 13.5026 4.00098 12.8891 4.00098C12.2756 4.00098 11.7782 4.49834 11.7782 5.11186V7.47349C12.4667 8.17756 12.4619 9.30645 11.7638 10.0046L10.8445 10.9239C10.6492 11.1191 10.3326 11.1191 10.1374 10.9239C9.94212 10.7286 9.94212 10.412 10.1374 10.2168L11.0567 9.29746C11.3691 8.98501 11.3691 8.47843 11.0567 8.16598C10.7622 7.8715 10.2953 7.85456 9.98096 8.11517L9.97714 8.11897L8.54089 9.55522C8.12423 9.97188 7.89015 10.537 7.89015 11.1262V13.9998H11.2228V13.4435L13.4143 11.2519C13.7894 10.8769 14 10.3683 14 9.83784Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189758\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"blood-drop-donation\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.91945 0.591671L6.62325 0.199537L6.62143 0.20092L6.61782 0.203665L6.60538 0.21321C6.59479 0.221366 6.57968 0.233094 6.56035 0.248309C6.52168 0.278736 6.46609 0.323132 6.39599 0.380818C6.25584 0.496156 6.05744 0.664842 5.8202 0.881465C5.34634 1.31415 4.71427 1.94104 4.08117 2.71889C2.82538 4.26178 1.51367 6.46227 1.51367 8.9556C1.51367 10.6111 2.20609 11.8651 3.24698 12.6942C4.27456 13.5128 5.61706 13.9 6.92025 13.9C8.22343 13.9 9.56594 13.5128 10.5936 12.6942C11.6344 11.8651 12.3269 10.6111 12.3269 8.9556C12.3269 6.46227 11.0152 4.26178 9.75933 2.71889C9.12622 1.94104 8.49415 1.31415 8.02029 0.881465C7.78305 0.664842 7.58466 0.496156 7.4445 0.380818C7.37441 0.323132 7.31882 0.278736 7.28015 0.248309C7.26081 0.233094 7.2457 0.221366 7.23512 0.21321L7.22267 0.203665L7.21907 0.20092L7.21724 0.199537C7.0415 0.0667892 6.799 0.0667904 6.62325 0.199537C6.62334 0.19949 6.62382 0.200135 6.66054 0.248788C6.69762 0.297911 6.77163 0.395973 6.91945 0.591671ZM5.90562 5.4488C5.90562 5.23526 6.07872 5.06216 6.29226 5.06216H7.54882C7.76235 5.06216 7.93545 5.23526 7.93545 5.4488V6.98645H9.4731C9.68664 6.98645 9.85974 7.15955 9.85974 7.37308V8.62964C9.85974 8.84318 9.68664 9.01628 9.4731 9.01628H7.93545L7.9354 10.5539C7.9354 10.7675 7.7623 10.9406 7.54877 10.9406H6.29221C6.07867 10.9406 5.90557 10.7675 5.90557 10.5539V9.01628H4.36792C4.15439 9.01628 3.98129 8.84318 3.98129 8.62964V7.37308C3.98129 7.15955 4.15439 6.98645 4.36792 6.98645H5.90562V5.4488Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"brain\", \"keywords\": [ \"medical\", \"health\", \"brain\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189740)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.3817 0.82373C5.17994 0.82373 5.89476 1.18095 6.375 1.74425V6.30725C6.35817 6.51232 6.24167 6.93954 5.97421 7.32957C5.70976 7.71519 5.33987 8.00754 4.83125 8.06431C4.4882 8.1026 4.24114 8.41173 4.27943 8.75478C4.31772 9.09783 4.62685 9.34489 4.9699 9.3066C5.54184 9.24276 6.00658 9.01788 6.375 8.72219V12.3916C5.82694 13.1002 4.9684 13.5564 4.00331 13.5564C2.49886 13.5564 1.25336 12.4477 1.03924 11.0028C1.15327 10.652 1.33903 10.349 1.51695 10.2085C1.78786 9.99459 1.83407 9.60157 1.62016 9.33067C1.40626 9.05976 1.01323 9.01355 0.742325 9.22746C0.650146 9.30024 0.563842 9.38254 0.483473 9.47216C0.177677 8.80158 0 7.96879 0 7.23401C0 6.25959 0.312464 5.30283 0.821827 4.5981C0.932208 4.81126 1.06415 4.99862 1.20568 5.15984C1.57457 5.58011 2.03531 5.85232 2.39451 5.93422C2.73106 6.01094 3.06608 5.80032 3.1428 5.46378C3.21953 5.12723 3.00891 4.79221 2.67236 4.71549C2.61517 4.70245 2.37401 4.59602 2.1451 4.33524C1.96105 4.12556 1.8018 3.83431 1.76343 3.4448L1.76342 3.442C1.76342 1.99597 2.93566 0.82373 4.3817 0.82373ZM9.99672 13.5564C9.03161 13.5564 8.17306 13.1001 7.625 12.3916V6.54847C7.88125 6.73377 8.15292 6.8858 8.41676 6.99712C8.73479 7.13131 9.10138 6.98228 9.23557 6.66426C9.36976 6.34623 9.22073 5.97964 8.9027 5.84545C8.35048 5.61244 7.84059 5.16031 7.66215 4.7442C7.65121 4.7187 7.63878 4.6943 7.625 4.67105V1.74429C8.10524 1.18096 8.82007 0.82373 9.61833 0.82373C11.0644 0.82373 12.2366 1.99597 12.2366 3.442C12.2366 3.5379 12.2314 3.6326 12.2214 3.72583L12.2235 3.727C12.1604 4.13687 11.9932 4.51485 11.8137 4.69187C11.5679 4.93423 11.5651 5.32995 11.8075 5.57574C12.0498 5.82153 12.4456 5.82431 12.6914 5.58196C12.9331 5.34355 13.1174 5.03149 13.2485 4.69875C13.7161 5.39248 14 6.30523 14 7.23401C14 8.32338 13.6095 9.62818 12.9899 10.3004L12.9672 10.299C12.8392 10.2898 12.6748 10.2584 12.491 10.1893C12.1284 10.0528 11.6897 9.76898 11.3076 9.21421C11.1118 8.92993 10.7226 8.8582 10.4384 9.05399C10.1541 9.24978 10.0824 9.63896 10.2781 9.92323C10.8088 10.6937 11.4556 11.1352 12.0507 11.3591C12.316 11.459 12.582 11.5191 12.8287 11.5417C12.4222 12.7144 11.3078 13.5564 9.99672 13.5564Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189740\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"brain-cognitive\", \"keywords\": [ \"health\", \"medical\", \"brain\", \"cognitive\", \"specialities\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.8829 0.00211959C7.94903 -0.0186911 9.00145 0.245001 9.93182 0.766049C10.8622 1.2871 11.6369 2.04668 12.1762 2.9666C12.7154 3.88651 12.9998 4.93353 13 5.99986V9.99994C13 10.3977 12.842 10.7793 12.5607 11.0606C12.2794 11.3419 11.8978 11.4999 11.5 11.4999H10V13.4999C10 13.7761 9.77614 13.9999 9.5 13.9999H4.5C4.22386 13.9999 4 13.7761 4 13.4999V11.1971C3.1689 10.7173 2.46361 10.0442 1.94501 9.23317C1.37058 8.33478 1.04594 7.29954 1.00453 6.23401C0.963111 5.16848 1.20641 4.11117 1.70938 3.17091C2.21235 2.23064 2.95681 1.44141 3.86613 0.88445C4.77545 0.327491 5.81677 0.0229303 6.8829 0.00211959ZM3.55955 7.20966C2.95928 5.52934 3.62507 3.60846 5.22532 2.68455C6.80521 1.77241 8.77124 2.13493 9.93255 3.45304C10.2267 3.78693 10.1583 4.28539 9.82813 4.58366C8.58172 5.70947 6.53015 7.15343 4.51516 7.68722C4.11691 7.79272 3.69815 7.59762 3.55955 7.20966Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"call-center-support-service\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189773)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.65662 1.19542C5.86416 0.608231 6.42412 0.186035 7.0843 0.186035H7.70003C8.52345 0.186035 9.19096 0.853548 9.19096 1.67697C9.19096 2.26931 8.8403 2.80547 8.29762 3.0429L6.97849 3.62002C6.92138 3.645 6.87628 3.68867 6.84909 3.74201H8.56596C8.91114 3.74201 9.19096 4.02183 9.19096 4.36701C9.19096 4.71219 8.91114 4.99201 8.56596 4.99201H6.19531C5.85013 4.99201 5.57031 4.71219 5.57031 4.36701V3.86188C5.57031 3.26037 5.92639 2.71591 6.47747 2.47482L7.7966 1.8977C7.88429 1.85933 7.94096 1.77269 7.94096 1.67697C7.94096 1.5439 7.83309 1.43604 7.70003 1.43604H7.0843C6.97034 1.43604 6.87171 1.5086 6.83517 1.61198C6.72014 1.93742 6.36306 2.108 6.03762 1.99297C5.71217 1.87794 5.54159 1.52087 5.65662 1.19542ZM11.7599 0.469079C11.9173 0.289209 12.1446 0.186035 12.3836 0.186035C12.8414 0.186035 13.2125 0.557118 13.2125 1.01487V2.85384C13.5426 2.87067 13.8051 3.14368 13.8051 3.47802C13.8051 3.81235 13.5426 4.08537 13.2125 4.1022V4.367C13.2125 4.71218 12.9326 4.992 12.5875 4.992C12.2423 4.992 11.9625 4.71218 11.9625 4.367V4.10302H10.4322C9.96805 4.10302 9.59179 3.72676 9.59179 3.26262C9.59179 3.05905 9.66568 2.86241 9.79973 2.70921L11.7599 0.469079ZM11.3349 2.85302L11.9625 2.13578V2.85302H11.3349ZM8.9145 13.7987C8.30142 13.8642 7.68438 13.7179 7.16598 13.3841C4.5772 11.6404 2.34971 9.4129 0.605984 6.82412C0.278441 6.30027 0.140465 5.67992 0.21508 5.06662C0.289695 4.45332 0.572389 3.88416 1.01598 3.45412L1.39598 3.07412C1.56635 2.90557 1.79633 2.81104 2.03598 2.81104C2.27563 2.81104 2.50561 2.90557 2.67598 3.07412L4.21598 4.67412C4.38391 4.84279 4.47819 5.07111 4.47819 5.30912C4.47819 5.54713 4.38391 5.77545 4.21598 5.94412C4.04743 6.11449 3.9529 6.34447 3.9529 6.58412C3.9529 6.82377 4.04743 7.05375 4.21598 7.22412L6.76598 9.77412C6.93635 9.94267 7.16633 10.0372 7.40598 10.0372C7.64563 10.0372 7.87561 9.94267 8.04598 9.77412C8.21465 9.60619 8.44297 9.51191 8.68098 9.51191C8.91899 9.51191 9.14731 9.60619 9.31598 9.77412L10.916 11.3641C11.0845 11.5345 11.1791 11.7645 11.1791 12.0041C11.1791 12.2438 11.0845 12.4738 10.916 12.6441L10.536 13.0241C10.0998 13.4599 9.52759 13.7333 8.9145 13.7987Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189773\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"checkup-medical-report-clipboard\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.5 1C4.5 0.447716 4.94772 0 5.5 0H8.5C9.05229 0 9.5 0.447715 9.5 1V1.5C9.5 2.05229 9.05229 2.5 8.5 2.5H5.5C4.94772 2.5 4.5 2.05229 4.5 1.5V1ZM10.75 1H11.25C12.0784 1 12.75 1.67157 12.75 2.5V12.5C12.75 13.3284 12.0784 14 11.25 14H2.75C1.92157 14 1.25 13.3284 1.25 12.5V2.5C1.25 1.67157 1.92157 1 2.75 1H3.24997V1.5C3.24997 2.74264 4.25733 3.75 5.49997 3.75H8.49997C9.74261 3.75 10.75 2.74264 10.75 1.5V1ZM5.97021 5.94812C5.97021 5.73145 6.14586 5.5558 6.36253 5.5558H7.63757C7.85424 5.5558 8.02989 5.73145 8.02989 5.94812V7.50839H9.59015C9.80683 7.50839 9.98247 7.68403 9.98247 7.90071V9.17575C9.98247 9.39242 9.80683 9.56807 9.59015 9.56807H8.02989L8.02984 11.1283C8.02984 11.345 7.85419 11.5206 7.63752 11.5206H6.36248C6.14581 11.5206 5.97016 11.345 5.97016 11.1283V9.56807H4.4099C4.19323 9.56807 4.01758 9.39242 4.01758 9.17575V7.90071C4.01758 7.68403 4.19323 7.50839 4.4099 7.50839H5.97021V5.94812Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"ear-hearing\", \"keywords\": [ \"health\", \"medical\", \"hearing\", \"ear\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189737)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.529419 3.03026C1.09964 1.75198 2.35981 0.371338 4.45458 0.371338C7.42039 0.371338 9.09014 2.5537 9.09014 4.73086C9.09014 6.8242 7.78725 8.15693 6.85299 8.843C6.26713 9.27322 5.88732 9.61203 5.88732 10.1265C5.88732 11.4043 4.84345 12.2955 3.74191 12.5396C2.60237 12.7921 1.25939 12.3994 0.555867 11.0126C0.413741 10.7325 0.371094 10.4348 0.371094 10.172V3.82215C0.371094 3.56798 0.411962 3.29357 0.529419 3.03026ZM3.36318 4.41445C3.5439 3.86707 3.92631 3.44218 4.4736 3.44218C5.31816 3.44218 5.81502 4.10357 5.81502 4.86168C5.81502 5.3313 5.70529 5.60404 5.58042 5.78553C5.44709 5.9793 5.25947 6.12485 5.00047 6.29033C4.96033 6.31597 4.91548 6.34368 4.86736 6.3734C4.64791 6.50896 4.36032 6.68659 4.1402 6.90326C3.82576 7.21276 3.60892 7.62421 3.60892 8.19023C3.60892 8.42262 3.54813 8.54636 3.50181 8.60655C3.45565 8.66653 3.39761 8.69881 3.33305 8.71068C3.19425 8.73619 3.01575 8.66774 2.93218 8.4707C2.79741 8.15292 2.43055 8.00456 2.11277 8.13933C1.79499 8.2741 1.64663 8.64097 1.7814 8.95875C2.09966 9.70919 2.85973 10.0687 3.55903 9.94008C3.91353 9.87492 4.24943 9.68468 4.49245 9.36887C4.7353 9.05329 4.85892 8.64796 4.85892 8.19023C4.85892 7.99074 4.91784 7.89177 5.01705 7.79412C5.12784 7.68507 5.26052 7.60235 5.46296 7.47614C5.52582 7.43695 5.59572 7.39336 5.67346 7.3437C5.96599 7.1568 6.32989 6.90148 6.61021 6.49408C6.89898 6.07439 7.06502 5.54564 7.06502 4.86168C7.06502 3.5878 6.17103 2.19218 4.4736 2.19218C3.13446 2.19218 2.4304 3.25265 2.1762 4.02255C2.06798 4.35032 2.24596 4.70376 2.57374 4.81198C2.90151 4.92021 3.25496 4.74222 3.36318 4.41445ZM12.8785 8.69805C13.2928 8.69805 13.6285 9.03384 13.6285 9.44805C13.6285 11.757 11.7568 13.6288 9.44782 13.6288C9.03361 13.6288 8.69782 13.293 8.69782 12.8788C8.69782 12.4646 9.03361 12.1288 9.44782 12.1288C10.9284 12.1288 12.1285 10.9286 12.1285 9.44805C12.1285 9.03384 12.4643 8.69805 12.8785 8.69805ZM11.1851 8.55701C11.1851 8.14279 10.8493 7.80701 10.4351 7.80701C10.0209 7.80701 9.68507 8.14279 9.68507 8.55701C9.68507 9.16173 9.19485 9.65196 8.59012 9.65196C8.17591 9.65196 7.84012 9.98774 7.84012 10.402C7.84012 10.8162 8.17591 11.152 8.59012 11.152C10.0233 11.152 11.1851 9.99016 11.1851 8.55701Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189737\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"eye-optic\", \"keywords\": [ \"health\", \"medical\", \"eye\", \"optic\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.35201 1.26778C4.74775 1.14546 5.16772 1.3671 5.29005 1.76284L5.69749 3.08096C6.10508 3.00526 6.53897 2.96311 6.99975 2.96311C7.46035 2.96311 7.8941 3.00523 8.30156 3.08088L8.70897 1.76284C8.8313 1.3671 9.25127 1.14546 9.64701 1.26778C10.0427 1.39011 10.2644 1.81008 10.1421 2.20582L9.73664 3.51744C10.5164 3.85414 11.1628 4.30855 11.6827 4.77811L12.6679 3.79289C12.9608 3.49999 13.4357 3.49999 13.7286 3.79289C14.0215 4.08578 14.0215 4.56065 13.7286 4.85355L12.6963 5.88585C12.8439 6.08088 12.9686 6.26401 13.0714 6.42656C13.1954 6.62258 13.2894 6.79169 13.3546 6.91974C13.387 6.98324 13.4141 7.0402 13.4349 7.08794C13.4449 7.11092 13.4563 7.13818 13.4667 7.16644C13.4716 7.17983 13.4794 7.20196 13.4871 7.22861C13.4909 7.24184 13.4968 7.26346 13.5022 7.29022C13.5061 7.30945 13.5175 7.36734 13.5175 7.4421C13.5175 7.51687 13.5061 7.57475 13.5022 7.59398C13.4968 7.62074 13.4909 7.64236 13.4871 7.6556C13.4794 7.68225 13.4716 7.70437 13.4667 7.71776C13.4563 7.74602 13.4449 7.77329 13.4349 7.79626C13.4141 7.844 13.387 7.90097 13.3546 7.96447C13.2894 8.09251 13.1954 8.26162 13.0714 8.45765C12.8239 8.84904 12.4492 9.35973 11.9341 9.8686C10.9001 10.8899 9.27872 11.9211 6.99975 11.9211C4.72077 11.9211 3.09939 10.8899 2.06542 9.8686C1.55027 9.35973 1.1756 8.84904 0.928046 8.45765C0.80406 8.26162 0.710051 8.09251 0.644838 7.96447C0.612497 7.90097 0.585421 7.844 0.564602 7.79626C0.554584 7.77329 0.543198 7.74602 0.532831 7.71776C0.52792 7.70437 0.520084 7.68225 0.512418 7.6556C0.508611 7.64236 0.50273 7.62074 0.497288 7.59398C0.493378 7.57475 0.481934 7.51687 0.481934 7.4421C0.481934 7.36734 0.493378 7.30945 0.497288 7.29022C0.50273 7.26346 0.508611 7.24184 0.512418 7.22861C0.520084 7.20196 0.52792 7.17983 0.532831 7.16644C0.543198 7.13818 0.554584 7.11092 0.564602 7.08794C0.585421 7.0402 0.612497 6.98324 0.644838 6.91973C0.710051 6.79169 0.80406 6.62258 0.928046 6.42656C1.03081 6.26408 1.15548 6.08104 1.30302 5.88611L0.270451 4.85355C-0.022442 4.56065 -0.022442 4.08578 0.270451 3.79289C0.563345 3.49999 1.03822 3.49999 1.33111 3.79289L2.31656 4.77834C2.8364 4.30878 3.48272 3.85435 4.26244 3.51762L3.85695 2.20582C3.73462 1.81008 3.95627 1.39011 4.35201 1.26778ZM6.52654 9.50575C7.66641 9.76677 8.80205 9.05431 9.06306 7.91444C9.32408 6.77457 8.61162 5.63893 7.47175 5.37792C6.33188 5.11691 5.19624 5.82936 4.93523 6.96923C4.67422 8.1091 5.38667 9.24474 6.52654 9.50575Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"flu-mask\", \"keywords\": [ \"health\", \"medical\", \"hospital\", \"mask\", \"flu\", \"vaccine\", \"protection\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.0292969 5.83696C0.0292969 4.47184 1.07578 3.27783 2.47089 3.27783H3.65002H10.3484H11.5275C12.9226 3.27783 13.9691 4.47184 13.9691 5.83696C13.9691 6.38968 13.7985 6.92863 13.4813 7.37221C13.3255 7.59005 13.1354 7.74635 13.0022 7.85055L11.1641 9.28851C8.72622 11.1957 5.30358 11.2004 2.86047 9.29992L1.00152 7.85386C0.865675 7.74818 0.671738 7.5895 0.513706 7.36743C0.198698 6.92477 0.0292969 6.38771 0.0292969 5.83696ZM2.47089 4.77783C1.99752 4.77783 1.5293 5.20377 1.5293 5.83696C1.5293 6.08217 1.60513 6.31405 1.73585 6.49773C1.76094 6.533 1.80907 6.58164 1.92252 6.6699L3.26606 7.71503V4.77783H2.47089ZM12.0779 6.66912L10.7324 7.72177V4.77783H11.5275C12.0009 4.77783 12.4691 5.20377 12.4691 5.83696C12.4691 6.08306 12.3927 6.31571 12.2611 6.4997C12.2364 6.5343 12.1891 6.5821 12.0779 6.66912ZM6.30774 5.50787C5.96256 5.50787 5.68274 5.7877 5.68274 6.13287C5.68274 6.47805 5.96256 6.75787 6.30774 6.75787H7.69075C8.03593 6.75787 8.31575 6.47805 8.31575 6.13287C8.31575 5.7877 8.03593 5.50787 7.69075 5.50787H6.30774Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"health-care-2\", \"keywords\": [ \"health\", \"medical\", \"hospital\", \"heart\", \"care\", \"symbol\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.68427 2.42245C4.68427 1.14383 5.72079 0.1073 6.99941 0.1073C8.27804 0.1073 9.31456 1.14383 9.31456 2.42245C9.31456 3.70107 8.27804 4.7376 6.99941 4.7376C5.72079 4.7376 4.68427 3.70107 4.68427 2.42245ZM2.44581 6.21445C2.91701 5.76724 3.54186 5.51792 4.19149 5.51792C4.84113 5.51792 5.46598 5.76724 5.93718 6.21445L5.94665 6.22344L6.99937 7.2764L8.05222 6.22356L8.06145 6.21433C8.53264 5.76712 9.15762 5.51792 9.80725 5.51792C10.4569 5.51792 11.0817 5.76724 11.5529 6.21445C11.7869 6.43665 11.9734 6.70425 12.1007 7.00075C12.228 7.29725 12.2937 7.61657 12.2937 7.93925C12.2937 8.26194 12.228 8.58125 12.1007 8.87776C11.9738 9.17332 11.7882 9.44007 11.5553 9.66186L7.34786 13.7513C7.15382 13.9399 6.84493 13.9399 6.65088 13.7513L2.44349 9.66185C2.21054 9.44006 2.02499 9.17332 1.89807 8.87776C1.77074 8.58125 1.70508 8.26194 1.70508 7.93925C1.70508 7.61657 1.77074 7.29726 1.89807 7.00075C2.02539 6.70425 2.21182 6.43665 2.44581 6.21445Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"heart-rate-pulse-graph\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.99196 2.60647C6.02649 1.64462 5.0192 1.16763 4.0518 1.09623C2.95741 1.01546 1.99196 1.46021 1.29575 2.16224C-0.0728108 3.54224 -0.490384 6.03708 1.21852 7.75383C1.22337 7.7587 1.22832 7.76347 1.23336 7.76813L6.65181 12.7802C6.84345 12.9575 7.13921 12.9575 7.33085 12.7802L12.7493 7.76813L12.7609 7.75704C14.495 6.04652 14.0756 3.55162 12.7014 2.17166C12.0027 1.47002 11.0341 1.02512 9.93761 1.10377C8.96801 1.17333 7.95835 1.64743 6.99196 2.60647ZM6.6511 4.58781C6.5557 4.3665 6.34157 4.21975 6.10075 4.21065C5.85992 4.20155 5.63532 4.33172 5.5235 4.5452L4.66772 6.17896H3.51464C3.16947 6.17896 2.88964 6.45878 2.88964 6.80396C2.88964 7.14913 3.16947 7.42896 3.51464 7.42896H5.04589C5.27839 7.42896 5.49166 7.29991 5.59954 7.09396L6.02229 6.2869L7.06569 8.7076C7.15225 8.90842 7.33747 9.04939 7.55409 9.07932C7.77071 9.10926 7.98722 9.0238 8.125 8.85398L9.28115 7.42896H10.4834C10.8285 7.42896 11.1084 7.14913 11.1084 6.80396C11.1084 6.45878 10.8285 6.17896 10.4834 6.17896H8.98339C8.79503 6.17896 8.61672 6.26391 8.49804 6.41018L7.80487 7.26456L6.6511 4.58781Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"heart-rate-search\", \"keywords\": [ \"health\", \"medical\", \"monitor\", \"heart\", \"rate\", \"search\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189746)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.25005 1.5C3.62667 1.5 1.5 3.62667 1.5 6.25005C1.5 8.87342 3.62667 11.0001 6.25005 11.0001C8.87342 11.0001 11.0001 8.87342 11.0001 6.25005C11.0001 3.62667 8.87342 1.5 6.25005 1.5ZM0 6.25005C0 2.79824 2.79824 0 6.25005 0C9.70185 0 12.5001 2.79824 12.5001 6.25005C12.5001 7.61502 12.0625 8.87779 11.32 9.90575L13.7073 12.2931C14.0979 12.6836 14.0979 13.3168 13.7073 13.7073C13.3168 14.0978 12.6836 14.0978 12.2931 13.7073L9.90582 11.32C8.87785 12.0625 7.61505 12.5001 6.25005 12.5001C2.79824 12.5001 0 9.70185 0 6.25005ZM5.96187 4.52749C5.86033 4.32337 5.65572 4.19074 5.42794 4.18138C5.20016 4.17202 4.98535 4.28742 4.86741 4.48252L4.10148 5.74955H3.0459C2.70072 5.74955 2.4209 6.02937 2.4209 6.37455C2.4209 6.71973 2.70072 6.99955 3.0459 6.99955H4.45398C4.67278 6.99955 4.87565 6.88513 4.98884 6.69788L5.34902 6.10207L6.27951 7.97261C6.3707 8.15594 6.54591 8.28297 6.7485 8.31265C6.95109 8.34233 7.15538 8.27089 7.29532 8.12142L8.34576 6.99955H9.4541C9.79928 6.99955 10.0791 6.71973 10.0791 6.37455C10.0791 6.02937 9.79928 5.74955 9.4541 5.74955H8.07476C7.90187 5.74955 7.7367 5.82117 7.61853 5.94737L6.9979 6.61021L5.96187 4.52749Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189746\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"hospital-sign-circle\", \"keywords\": [ \"health\", \"sign\", \"medical\", \"symbol\", \"hospital\", \"circle\", \"emergency\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189731)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7ZM5.625 4.5C5.625 4.15482 5.34518 3.875 5 3.875C4.65482 3.875 4.375 4.15482 4.375 4.5V7V9.5C4.375 9.84518 4.65482 10.125 5 10.125C5.34518 10.125 5.625 9.84518 5.625 9.5V7.625H8.375V9.5C8.375 9.84518 8.65482 10.125 9 10.125C9.34518 10.125 9.625 9.84518 9.625 9.5V7V4.5C9.625 4.15482 9.34518 3.875 9 3.875C8.65482 3.875 8.375 4.15482 8.375 4.5V6.375H5.625V4.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189731\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"hospital-sign-square\", \"keywords\": [ \"health\", \"sign\", \"medical\", \"symbol\", \"hospital\", \"square\", \"emergency\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189783)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1C0 0.447716 0.447715 0 1 0H13C13.5523 0 14 0.447715 14 1V13C14 13.5523 13.5523 14 13 14H1C0.447716 14 0 13.5523 0 13V1ZM5.625 4.5C5.625 4.15482 5.34518 3.875 5 3.875C4.65482 3.875 4.375 4.15482 4.375 4.5V7V9.5C4.375 9.84518 4.65482 10.125 5 10.125C5.34518 10.125 5.625 9.84518 5.625 9.5V7.625H8.375V9.5C8.375 9.84518 8.65482 10.125 9 10.125C9.34518 10.125 9.625 9.84518 9.625 9.5V7V4.5C9.625 4.15482 9.34518 3.875 9 3.875C8.65482 3.875 8.375 4.15482 8.375 4.5V6.375H5.625V4.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189783\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"insurance-hand\", \"keywords\": [ \"health\", \"medical\", \"insurance\", \"hand\", \"cross\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.90625 1.53125C7.90625 1.25511 8.13011 1.03125 8.40625 1.03125H10.0312C10.3074 1.03125 10.5312 1.25511 10.5312 1.53125V2.93753H11.9375C12.2136 2.93753 12.4375 3.16138 12.4375 3.43753V5.06253C12.4375 5.33867 12.2136 5.56253 11.9375 5.56253H10.5312V6.96875C10.5312 7.24489 10.3074 7.46875 10.0312 7.46875H8.40625C8.13011 7.46875 7.90625 7.24489 7.90625 6.96875V5.56253H6.5C6.22386 5.56253 6 5.33867 6 5.06253V3.43753C6 3.16138 6.22386 2.93753 6.5 2.93753H7.90625V1.53125ZM1.84315 7H0V11L1.82843 12.8284C2.57857 13.5786 3.59599 14 4.65685 14H10.5C11.3284 14 12 13.3284 12 12.5C12 11.6716 11.3284 11 10.5 11H7.72314C7.62662 11.3265 7.44953 11.6342 7.19188 11.8918C6.36791 12.7158 5.03199 12.7158 4.20802 11.8918L2.75801 10.4418C2.51393 10.1977 2.51393 9.80201 2.75801 9.55794C3.00209 9.31386 3.39782 9.31386 3.64189 9.55794L5.09191 11.0079C5.42772 11.3438 5.97218 11.3438 6.308 11.0079C6.60999 10.706 6.64041 10.2352 6.39924 9.89924L4.67157 8.17157C3.92143 7.42143 2.90401 7 1.84315 7Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"medical-bag\", \"keywords\": [ \"health\", \"sign\", \"medical\", \"symbol\", \"hospital\", \"bag\", \"medicine\", \"medkit\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189743)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.99895 0C4.52458 0 4.03951 0.15342 3.65601 0.469027C3.26701 0.789168 2.99841 1.26994 2.99841 1.82341V3.36588H1.5C0.690789 3.36588 0 4.0009 0 4.82631V12.5098C0 13.3352 0.690789 13.9702 1.5 13.9702H12.5C13.3092 13.9702 14 13.3352 14 12.5098V4.82631C14 4.0009 13.3092 3.36588 12.5 3.36588H11.0016V1.82341C11.0016 1.26994 10.733 0.789168 10.344 0.469027C9.96051 0.15342 9.47544 0 9.00108 0H4.99895ZM9.00161 3.36588V2H4.99895L4.99841 3.36588H9.00161ZM4.375 8.66805C4.375 8.32287 4.65482 8.04305 5 8.04305H6.375V6.66805C6.375 6.32287 6.65482 6.04305 7 6.04305C7.34518 6.04305 7.625 6.32287 7.625 6.66805V8.04305H9C9.34518 8.04305 9.625 8.32287 9.625 8.66805C9.625 9.01323 9.34518 9.29305 9 9.29305H7.625V10.668C7.625 11.0132 7.34518 11.293 7 11.293C6.65482 11.293 6.375 11.0132 6.375 10.668V9.29305H5C4.65482 9.29305 4.375 9.01323 4.375 8.66805Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189743\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"medical-cross-sign-healthcare\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189734)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5 0C4.72386 0 4.5 0.223858 4.5 0.5V4.5H0.5C0.223858 4.5 0 4.72386 0 5V9C0 9.27614 0.223858 9.5 0.5 9.5H4.5V13.5C4.5 13.7761 4.72386 14 5 14H9C9.27614 14 9.5 13.7761 9.5 13.5V9.5H13.5C13.7761 9.5 14 9.27614 14 9V5C14 4.72386 13.7761 4.5 13.5 4.5H9.5V0.5C9.5 0.223858 9.27614 0 9 0H5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189734\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"medical-cross-symbol\", \"keywords\": [ \"health\", \"sign\", \"medical\", \"symbol\", \"hospital\", \"emergency\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189786)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.04771 0.756836C5.04771 0.480693 5.27157 0.256836 5.54771 0.256836H8.45313C8.72927 0.256836 8.95313 0.480694 8.95313 0.756836V3.61784L11.4309 2.18734C11.67 2.04927 11.9758 2.13121 12.1139 2.37035L13.5666 4.88652C13.7047 5.12567 13.6227 5.43146 13.3836 5.56953L10.9059 7.00004L13.3835 8.43053C13.6227 8.5686 13.7046 8.8744 13.5666 9.11355L12.1138 11.6297C11.9758 11.8689 11.67 11.9508 11.4308 11.8127L8.95313 10.3822V13.2432C8.95313 13.5194 8.72927 13.7432 8.45313 13.7432H5.54771C5.27157 13.7432 5.04771 13.5194 5.04771 13.2432V10.3822L2.57001 11.8127C2.33086 11.9508 2.02507 11.8689 1.88699 11.6297L0.434281 9.11355C0.29621 8.8744 0.378147 8.5686 0.617293 8.43053L3.09499 7.00004L0.617271 5.56953C0.378124 5.43146 0.296187 5.12567 0.434258 4.88652L1.88697 2.37035C2.02504 2.13121 2.33084 2.04927 2.56998 2.18734L5.04771 3.61785V0.756836Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189786\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"medical-files-report-history\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.43934 0.43934C1.72064 0.158035 2.10218 0 2.5 0H8.5C8.63261 0 8.75979 0.0526784 8.85355 0.146447L12.8536 4.14645C12.9473 4.24021 13 4.36739 13 4.5V12.5C13 12.8978 12.842 13.2794 12.5607 13.5607C12.2794 13.842 11.8978 14 11.5 14H2.5C2.10217 14 1.72064 13.842 1.43934 13.5607C1.15804 13.2794 1 12.8978 1 12.5V1.5C1 1.10218 1.15804 0.720644 1.43934 0.43934ZM5.97021 5.42748C5.97021 5.2108 6.14586 5.03516 6.36253 5.03516H7.63757C7.85424 5.03516 8.02989 5.2108 8.02989 5.42748V6.98774H9.59015C9.80683 6.98774 9.98247 7.16339 9.98247 7.38006V8.6551C9.98247 8.87177 9.80683 9.04742 9.59015 9.04742H8.02989L8.02984 10.6077C8.02984 10.8244 7.85419 11 7.63752 11H6.36248C6.14581 11 5.97016 10.8244 5.97016 10.6077V9.04742H4.4099C4.19323 9.04742 4.01758 8.87177 4.01758 8.6551V7.38006C4.01758 7.16339 4.19323 6.98774 4.4099 6.98774H5.97021V5.42748Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"medical-ribbon-1\", \"keywords\": [ \"ribbon\", \"medical\", \"cancer\", \"health\", \"beauty\", \"symbol\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.4167 0.362249C7.96493 0.190308 7.48311 0.112157 7.00055 0.132317C6.518 0.112157 6.03617 0.190308 5.58441 0.362249C5.11454 0.541081 4.68784 0.817323 4.33234 1.17282C3.97684 1.52832 3.7006 1.95502 3.52177 2.42489C3.34644 2.88556 3.26863 3.3775 3.29315 3.86958C3.30064 5.00813 3.7549 6.18739 4.33038 7.24972C4.73184 7.99082 5.2185 8.72216 5.72619 9.40645C5.1101 10.1419 4.50936 10.7861 4.04232 11.2653C3.775 11.5395 3.55287 11.7583 3.3986 11.9075L3.16323 12.1315C2.75763 12.5062 2.73249 13.1387 3.10711 13.5444C3.48178 13.9502 4.11444 13.9754 4.5202 13.6007L4.52095 13.6C4.84756 13.2963 5.16323 12.9806 5.47449 12.6613C5.9012 12.2235 6.437 11.6519 7.00056 10.9958C7.56411 11.6519 8.09991 12.2235 8.52662 12.6613C9.07723 13.2262 10.0316 14.4784 10.894 13.5444C11.2686 13.1387 11.2432 12.5059 10.8376 12.1312L10.8354 12.1292L10.8344 12.1283C10.5364 11.8468 10.2449 11.5588 9.95879 11.2653C9.49175 10.7861 8.89101 10.1419 8.27492 9.40645C8.78261 8.72215 9.26927 7.99082 9.67073 7.24972C10.2462 6.18739 10.7005 5.00813 10.708 3.86958C10.7325 3.3775 10.6547 2.88556 10.4793 2.42489C10.3005 1.95502 10.0243 1.52832 9.66877 1.17282C9.31327 0.817323 8.88656 0.541081 8.4167 0.362249ZM7.00055 7.75481C7.33808 7.27303 7.64938 6.78222 7.91218 6.2971C8.43181 5.33786 8.70806 4.49777 8.70806 3.84103C8.70806 2.70933 7.91218 2.13185 6.94253 2.13185C5.97288 2.13185 5.29305 2.91462 5.29305 3.84103C5.29305 4.49777 5.5693 5.33786 6.08893 6.2971C6.35173 6.78222 6.66303 7.27303 7.00055 7.75481Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"medical-search-diagnosis\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189804)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.25005 1.5C3.62667 1.5 1.5 3.62667 1.5 6.25004C1.5 8.87342 3.62667 11.0001 6.25005 11.0001C8.87342 11.0001 11.0001 8.87342 11.0001 6.25004C11.0001 3.62667 8.87342 1.5 6.25005 1.5ZM0 6.25004C0 2.79824 2.79824 0 6.25005 0C9.70185 0 12.5001 2.79824 12.5001 6.25004C12.5001 7.61502 12.0625 8.8778 11.32 9.90576L13.7073 12.2931C14.0978 12.6836 14.0978 13.3168 13.7073 13.7073C13.3168 14.0978 12.6836 14.0978 12.2931 13.7073L9.9058 11.32C8.87783 12.0625 7.61504 12.5001 6.25005 12.5001C2.79824 12.5001 0 9.70185 0 6.25004ZM5.4375 3.03125C5.16136 3.03125 4.9375 3.25511 4.9375 3.53125V4.93752H3.53125C3.25511 4.93752 3.03125 5.16138 3.03125 5.43752V7.06252C3.03125 7.33867 3.25511 7.56252 3.53125 7.56252H4.9375V8.96875C4.9375 9.24489 5.16136 9.46875 5.4375 9.46875H7.0625C7.33864 9.46875 7.5625 9.24489 7.5625 8.96875V7.56252H8.96875C9.24489 7.56252 9.46875 7.33867 9.46875 7.06252V5.43752C9.46875 5.16138 9.24489 4.93752 8.96875 4.93752H7.5625V3.53125C7.5625 3.25511 7.33864 3.03125 7.0625 3.03125H5.4375Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189804\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"microscope-observation-sciene\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189792)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.97473 0.0175781H4.97473C5.25087 0.0175781 5.47473 0.241436 5.47473 0.517578V2.26758H3.47473V0.517578C3.47473 0.241436 3.69858 0.0175781 3.97473 0.0175781ZM3.22473 3.23877C2.94858 3.23877 2.72473 3.46263 2.72473 3.73877V7.23877C2.72473 7.81184 3.19287 8.2745 3.76591 8.26775L5.2248 8.25056C5.77894 8.24402 6.22473 7.79295 6.22473 7.23877V5.79166H7.74762C9.52636 5.79166 10.9683 7.23362 10.9683 9.01236V9.73242H5.97424H1.97424C1.56003 9.73242 1.22424 10.0682 1.22424 10.4824C1.22424 10.8966 1.56003 11.2324 1.97424 11.2324H5.02539C5.02539 11.4776 4.91721 11.8129 4.72074 12.1389C4.48959 12.362 4.23128 12.4824 3.97424 12.4824H1.02539C0.611178 12.4824 0.275391 12.8182 0.275391 13.2324C0.275391 13.6466 0.611178 13.9824 1.02539 13.9824H3.97424H4.02539H12.9742C13.3884 13.9824 13.7242 13.6466 13.7242 13.2324V10.7329L13.7251 9.04068C13.7251 5.83637 11.1275 3.23877 7.92325 3.23877H5.75598C5.74564 3.23813 5.73522 3.23877 5.72473 3.23877H3.22473Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189792\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"nurse-assistant-emergency\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189776)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8 3C8 4.65685 6.65685 6 5 6C3.34315 6 2 4.65685 2 3C2 1.34315 3.34315 0 5 0C6.65685 0 8 1.34315 8 3ZM9.40622 6.5C9.13008 6.5 8.90622 6.72386 8.90622 7V8.90628H7C6.72386 8.90628 6.5 9.13013 6.5 9.40628V11.0313C6.5 11.3074 6.72386 11.5313 7 11.5313H8.90622V13.5C8.90622 13.7761 9.13008 14 9.40622 14H11.0312C11.3074 14 11.5312 13.7761 11.5312 13.5V11.5313H13.5C13.7761 11.5313 14 11.3074 14 11.0313V9.40628C14 9.13013 13.7761 8.90628 13.5 8.90628H11.5312V7C11.5312 6.72386 11.3074 6.5 11.0312 6.5H9.40622ZM5 7C5.90166 7 6.74758 7.23867 7.47803 7.65628H7C6.0335 7.65628 5.25 8.43978 5.25 9.40628V11.0313C5.25 11.6468 5.56773 12.188 6.04812 12.5H0.5C0.223858 12.5 0 12.2761 0 12C0 9.23858 2.23858 7 5 7Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189776\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"nurse-hat\", \"keywords\": [ \"health\", \"medical\", \"hospital\", \"nurse\", \"doctor\", \"cap\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.21042 1.36695H4.75098C4.42442 1.35475 4.10268 1.44953 3.83477 1.63706C3.56945 1.82279 3.37153 2.08928 3.2703 2.39651L2.53769 4.21184C5.48297 3.73356 8.50339 3.73276 11.449 4.20944L10.6901 2.39331C10.5885 2.08743 10.3911 1.82214 10.1267 1.63706C9.85872 1.44953 9.53698 1.35475 9.21042 1.36695ZM13.4937 5.90794C13.7705 5.97801 13.9265 6.26886 13.8379 6.54023L12.0721 11.9446C11.9378 12.3558 11.5542 12.634 11.1216 12.634H2.87984C2.44722 12.634 2.06365 12.3558 1.92929 11.9446L0.163555 6.54023C0.0748941 6.26886 0.230964 5.97801 0.507712 5.90794C4.74913 4.83406 9.25227 4.83406 13.4937 5.90794ZM5.5007 8.24195C5.15552 8.24195 4.8757 8.52178 4.8757 8.86695C4.8757 9.21213 5.15552 9.49195 5.5007 9.49195H6.3757V10.367C6.3757 10.7121 6.65552 10.992 7.0007 10.992C7.34588 10.992 7.6257 10.7121 7.6257 10.367V9.49195H8.5007C8.84588 9.49195 9.1257 9.21213 9.1257 8.86695C9.1257 8.52178 8.84588 8.24195 8.5007 8.24195H7.6257V7.36695C7.6257 7.02178 7.34588 6.74195 7.0007 6.74195C6.65552 6.74195 6.3757 7.02178 6.3757 7.36695V8.24195H5.5007Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"online-medical-call-service\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189767)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.87868 0.592728C8.87868 0.320588 9.09929 0.0999756 9.37143 0.0999756H10.9729C11.2451 0.0999756 11.4657 0.320588 11.4657 0.592728V2.47137H13.4059C13.678 2.47137 13.8986 2.69198 13.8986 2.96412V4.56557C13.8986 4.83771 13.678 5.05832 13.4059 5.05832H11.4657V6.9985C11.4657 7.27064 11.2451 7.49126 10.9729 7.49126H9.37143C9.09929 7.49126 8.87868 7.27064 8.87868 6.9985V5.05832H7.00009C6.72795 5.05832 6.50734 4.83771 6.50734 4.56557V2.96412C6.50734 2.69198 6.72795 2.47137 7.00009 2.47137H8.87868V0.592728ZM8.69436 13.8849C8.09016 13.9495 7.48207 13.8053 6.97119 13.4763C4.41993 11.7579 2.22473 9.56265 0.506281 7.0114C0.183486 6.49514 0.0475095 5.88379 0.121044 5.27938C0.194577 4.67496 0.473173 4.11405 0.910337 3.69025L1.28483 3.31576C1.45273 3.14965 1.67937 3.05649 1.91555 3.05649C2.15173 3.05649 2.37837 3.14965 2.54627 3.31576L4.06395 4.89256C4.22945 5.05879 4.32236 5.2838 4.32236 5.51836C4.32236 5.75292 4.22945 5.97793 4.06395 6.14415C3.89785 6.31205 3.80468 6.5387 3.80468 6.77488C3.80468 7.01106 3.89785 7.2377 4.06395 7.4056L6.57699 9.91863C6.74488 10.0847 6.97153 10.1779 7.20771 10.1779C7.44389 10.1779 7.67053 10.0847 7.83843 9.91863C8.00465 9.75314 8.22966 9.66022 8.46422 9.66022C8.69879 9.66022 8.9238 9.75314 9.09002 9.91863L10.6669 11.4856C10.833 11.6535 10.9261 11.8801 10.9261 12.1163C10.9261 12.3525 10.833 12.5791 10.6669 12.747L10.2924 13.1215C9.86247 13.551 9.29856 13.8204 8.69436 13.8849Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189767\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"online-medical-service-monitor\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189801)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.45679 0.251953C0.652229 0.251953 0 0.904182 0 1.70875V9.29517C0 10.0997 0.65223 10.752 1.45679 10.752H5.48375L4.9483 12.248H4C3.58579 12.248 3.25 12.5838 3.25 12.998C3.25 13.4123 3.58579 13.748 4 13.748H10C10.4142 13.748 10.75 13.4123 10.75 12.998C10.75 12.5838 10.4142 12.248 10 12.248H9.05258L8.51713 10.752H12.5432C13.3478 10.752 14 10.0997 14 9.29517V1.70875C14 0.904183 13.3478 0.251953 12.5432 0.251953H1.45679ZM5.98779 2.91185C5.98779 2.69518 6.16344 2.51953 6.38011 2.51953H7.65515C7.87182 2.51953 8.04747 2.69518 8.04747 2.91185V4.47211H9.60773C9.8244 4.47211 10.0001 4.64776 10.0001 4.86443V6.13947C10.0001 6.35615 9.8244 6.53179 9.60773 6.53179H8.04747L8.04742 8.09205C8.04742 8.30873 7.87177 8.48438 7.6551 8.48438H6.38006C6.16339 8.48438 5.98774 8.30873 5.98774 8.09205V6.53179H4.42748C4.2108 6.53179 4.03516 6.35615 4.03516 6.13947V4.86443C4.03516 4.64776 4.2108 4.47211 4.42748 4.47211H5.98779V2.91185Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189801\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"online-medical-web-service\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189779)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.54688 12.2491V3.31506H12.4545V12.2491C12.4545 12.362 12.363 12.4536 12.2501 12.4536H1.75131C1.6384 12.4536 1.54688 12.362 1.54688 12.2491ZM0.046875 1.75033C0.046875 0.809 0.809976 0.0458984 1.75131 0.0458984H12.2501C13.1914 0.0458984 13.9545 0.809 13.9545 1.75033V12.2491C13.9545 13.1904 13.1914 13.9536 12.2501 13.9536H1.75131C0.809976 13.9536 0.046875 13.1904 0.046875 12.2491V1.75033Z\\\" fill=\\\"black\\\"/>\\n<path d=\\\"M6.36254 5.03949C6.14587 5.03949 5.97022 5.21514 5.97022 5.43181V6.99207H4.40991C4.19323 6.99207 4.01758 7.16772 4.01758 7.38439V8.65943C4.01758 8.8761 4.19323 9.05175 4.40991 9.05175H5.97017V10.612C5.97017 10.8287 6.14581 11.0043 6.36249 11.0043H7.63753C7.8542 11.0043 8.02985 10.8287 8.02985 10.612L8.0299 9.05175H9.59016C9.80683 9.05175 9.98248 8.8761 9.98248 8.65943V7.38439C9.98248 7.16772 9.80683 6.99207 9.59016 6.99207H8.0299V5.43181C8.0299 5.21514 7.85425 5.03949 7.63758 5.03949H6.36254Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189779\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"petri-dish-lab-equipment\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189825)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7ZM8.83333 6.50033C9.75381 6.50033 10.5 5.75413 10.5 4.83366C10.5 3.91318 9.75381 3.16699 8.83333 3.16699C8.16158 3.16699 7.58265 3.56441 7.31883 4.13695C7.1482 4.0496 6.95485 4.00033 6.75 4.00033C6.05964 4.00033 5.5 4.55997 5.5 5.25033C5.5 5.94068 6.05964 6.50033 6.75 6.50033C7.13206 6.50033 7.47409 6.32892 7.70337 6.05881C8.00039 6.33289 8.39731 6.50033 8.83333 6.50033ZM3.76236 8.69561C3.67343 8.69831 3.59671 8.72529 3.57598 8.73533C3.26529 8.88571 2.89151 8.75576 2.74112 8.44506C2.59073 8.13437 2.72069 7.76059 3.03138 7.6102C3.198 7.52955 3.44654 7.4546 3.72453 7.44619C4.00923 7.43757 4.36804 7.49874 4.68956 7.74639C5.01107 7.99405 5.1618 8.32537 5.22613 8.60284C5.28894 8.87377 5.27991 9.13321 5.24445 9.31489C5.24004 9.33749 5.23353 9.41856 5.25362 9.50522C5.2722 9.58535 5.30284 9.62872 5.33528 9.65371C5.36773 9.6787 5.41748 9.69726 5.49969 9.69477C5.58862 9.69207 5.66534 9.66508 5.68607 9.65505C5.99676 9.50466 6.37054 9.63462 6.52093 9.94531C6.67132 10.256 6.54136 10.6298 6.23067 10.7802C6.06405 10.8608 5.81551 10.9358 5.53752 10.9442C5.25282 10.9528 4.894 10.8916 4.57249 10.644C4.25098 10.3963 4.10025 10.065 4.03592 9.78754C3.97311 9.51661 3.98214 9.25717 4.01759 9.07549C4.022 9.05289 4.02852 8.97182 4.00842 8.88515C3.98985 8.80503 3.95921 8.76166 3.92677 8.73667C3.89432 8.71168 3.84457 8.69312 3.76236 8.69561ZM8.00003 7.49023C7.51677 7.49023 7.125 7.882 7.125 8.36527C7.125 8.84853 7.51677 9.2403 8.00003 9.2403C8.48351 9.2403 8.875 8.84833 8.875 8.36527C8.875 7.88221 8.48351 7.49023 8.00003 7.49023ZM2.66443 5.08353C2.66443 4.60026 3.05619 4.2085 3.53946 4.2085C4.02294 4.2085 4.41443 4.60047 4.41443 5.08353C4.41443 5.56659 4.02294 5.95856 3.53946 5.95856C3.05619 5.95856 2.66443 5.56679 2.66443 5.08353ZM10.25 8.95508C9.76677 8.95508 9.375 9.34684 9.375 9.83011C9.375 10.3134 9.76677 10.7051 10.25 10.7051C10.7335 10.7051 11.125 10.3132 11.125 9.83011C11.125 9.34705 10.7335 8.95508 10.25 8.95508Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189825\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pharmacy\", \"keywords\": [ \"health\", \"medical\", \"pharmacy\", \"sign\", \"medicine\", \"mortar\", \"pestle\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189807)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.1019 1.0715L8.13403 4.15595H11.2166L12.301 2.48776C12.7 1.874 12.5176 1.05231 11.8963 0.665063C11.2879 0.285791 10.4875 0.467072 10.1019 1.0715ZM1.06615 8.84725L0.775923 6.26775C0.709222 5.67493 1.17308 5.15594 1.76965 5.15594H12.2284C12.825 5.15594 13.2889 5.67493 13.2222 6.26775L12.9319 8.84725C12.7817 10.1822 11.9857 11.3022 10.8767 11.9097C11.0332 12.2166 11.1432 12.5528 11.1967 12.9097C11.2706 13.4032 10.8883 13.8469 10.3894 13.8469H3.60861C3.10962 13.8469 2.7274 13.4032 2.80131 12.9097C2.85477 12.5528 2.96474 12.2166 3.12131 11.9097C2.01232 11.3021 1.21635 10.1822 1.06615 8.84725ZM7.62402 7.57238C7.62402 7.2272 7.3442 6.94738 6.99902 6.94738C6.65384 6.94738 6.37402 7.2272 6.37402 7.57238V8.87646H5.07031C4.72513 8.87646 4.44531 9.15628 4.44531 9.50146C4.44531 9.84663 4.72513 10.1265 5.07031 10.1265H6.37402V11.4305C6.37402 11.7757 6.65384 12.0555 6.99902 12.0555C7.3442 12.0555 7.62402 11.7757 7.62402 11.4305V10.1265H8.92844C9.27362 10.1265 9.55344 9.84663 9.55344 9.50146C9.55344 9.15628 9.27362 8.87646 8.92844 8.87646H7.62402V7.57238Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189807\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"prescription-pills-drugs-healthcare\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.92383 0.25H7.23152C9.04769 0.25 10.5199 1.7223 10.5199 3.53846C10.5199 5.35463 9.04769 6.82692 7.23152 6.82692H6.58972L8.96408 9.05289L11.5473 6.46967C11.8402 6.17678 12.315 6.17678 12.6079 6.46967C12.9008 6.76256 12.9008 7.23744 12.6079 7.53033L10.0589 10.0793L12.5907 12.4528C12.8928 12.7361 12.9081 13.2108 12.6249 13.5129C12.3416 13.8151 11.8669 13.8304 11.5647 13.5471L8.99775 11.1405L6.60796 13.5303C6.31507 13.8232 5.8402 13.8232 5.5473 13.5303C5.25441 13.2374 5.25441 12.7626 5.5473 12.4697L7.90287 10.1141L4.39655 6.82692H2.67383V12.0769C2.67383 12.4911 2.33804 12.8269 1.92383 12.8269C1.50961 12.8269 1.17383 12.4911 1.17383 12.0769V6.07692V1C1.17383 0.585786 1.50961 0.25 1.92383 0.25ZM7.23152 5.32692H4.6989H4.68762H2.67383V1.75H7.23152C8.21926 1.75 9.01998 2.55072 9.01998 3.53846C9.01998 4.5262 8.21926 5.32692 7.23152 5.32692Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"sign-cross-square\", \"keywords\": [ \"health\", \"sign\", \"medical\", \"symbol\", \"hospital\", \"cross\", \"square\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189810)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.1875 1.59375C0.1875 0.765323 0.859073 0.09375 1.6875 0.09375H12.2812C13.1097 0.09375 13.7812 0.765323 13.7812 1.59375V12.1875C13.7812 13.0159 13.1097 13.6875 12.2812 13.6875H1.6875C0.859073 13.6875 0.1875 13.0159 0.1875 12.1875V1.59375ZM5.6875 4.0625C5.6875 3.78636 5.91136 3.5625 6.1875 3.5625H7.8125C8.08864 3.5625 8.3125 3.78636 8.3125 4.0625V5.46878H9.71875C9.99489 5.46878 10.2188 5.69263 10.2188 5.96878V7.59378C10.2188 7.86992 9.99489 8.09378 9.71875 8.09378H8.3125V9.5C8.3125 9.77614 8.08864 10 7.8125 10H6.1875C5.91136 10 5.6875 9.77614 5.6875 9.5V8.09378H4.28125C4.00511 8.09378 3.78125 7.86992 3.78125 7.59378V5.96878C3.78125 5.69263 4.00511 5.46878 4.28125 5.46878H5.6875V4.0625Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189810\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"sos-help-emergency-sign\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 5.77847C0 4.82052 0.776572 4.04395 1.73452 4.04395H2.58824C3.13709 4.04395 3.63097 4.28352 3.96918 4.66141C4.12824 4.83911 4.25392 5.04836 4.33555 5.2793C4.47358 5.66984 4.26889 6.09833 3.87835 6.23637C3.48781 6.3744 3.05932 6.16971 2.92129 5.77917C2.90607 5.73613 2.88233 5.69624 2.85148 5.66178C2.78578 5.58837 2.69269 5.54395 2.58824 5.54395H1.73452C1.605 5.54395 1.5 5.64894 1.5 5.77847C1.5 5.88868 1.57674 5.98401 1.6844 6.00757L2.9842 6.2919C3.83512 6.47804 4.44118 7.23185 4.44118 8.10236C4.44118 9.12535 3.61194 9.95571 2.58824 9.95571H1.85294C1.04499 9.95571 0.359642 9.43902 0.10563 8.72035C-0.0324054 8.32981 0.172288 7.90132 0.562825 7.76329C0.953362 7.62525 1.38185 7.82994 1.51989 8.22048C1.5687 8.35857 1.70044 8.45571 1.85294 8.45571H2.58824C2.7828 8.45571 2.94118 8.29765 2.94118 8.10236C2.94118 7.93623 2.82543 7.79263 2.66366 7.75725L1.36386 7.47292C0.567569 7.29873 0 6.59359 0 5.77847ZM9.55859 5.77847C9.55859 4.82052 10.3352 4.04395 11.2931 4.04395H12.1468C12.6957 4.04395 13.1896 4.28352 13.5278 4.66141C13.6868 4.83911 13.8125 5.04836 13.8941 5.2793C14.0322 5.66984 13.8275 6.09833 13.4369 6.23637C13.0464 6.3744 12.6179 6.16971 12.4799 5.77917C12.4647 5.73613 12.4409 5.69624 12.4101 5.66178C12.3444 5.58837 12.2513 5.54395 12.1468 5.54395H11.2931C11.1636 5.54395 11.0586 5.64894 11.0586 5.77847C11.0586 5.88868 11.1353 5.98401 11.243 6.00757L12.5428 6.2919C13.3937 6.47804 13.9998 7.23185 13.9998 8.10236C13.9998 9.12535 13.1705 9.95571 12.1468 9.95571H11.4115C10.6036 9.95571 9.91824 9.43902 9.66422 8.72035C9.52619 8.32981 9.73088 7.90132 10.1214 7.76329C10.512 7.62525 10.9404 7.82994 11.0785 8.22048C11.1273 8.35857 11.259 8.45571 11.4115 8.45571H12.1468C12.3414 8.45571 12.4998 8.29765 12.4998 8.10236C12.4998 7.93623 12.384 7.79263 12.2223 7.75725L10.9225 7.47292C10.1262 7.29873 9.55859 6.59359 9.55859 5.77847ZM6.2793 5.89689C6.2793 5.70196 6.43731 5.54395 6.63224 5.54395H7.36753C7.56246 5.54395 7.72047 5.70196 7.72047 5.89689V8.10277C7.72047 8.29769 7.56246 8.45571 7.36753 8.45571H6.63224C6.43731 8.45571 6.2793 8.29769 6.2793 8.10277V5.89689ZM6.63224 4.04395C5.60889 4.04395 4.7793 4.87354 4.7793 5.89689V8.10277C4.7793 9.12611 5.60889 9.95571 6.63224 9.95571H7.36753C8.39088 9.95571 9.22047 9.12611 9.22047 8.10277V5.89689C9.22047 4.87354 8.39088 4.04395 7.36753 4.04395H6.63224Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"stethoscope\", \"keywords\": [ \"instrument\", \"health\", \"medical\", \"stethoscope\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189789)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.0507812 1.40625C0.0507812 0.992036 0.386567 0.65625 0.800781 0.65625H1.89453C2.30874 0.65625 2.64453 0.992036 2.64453 1.40625C2.64453 1.82046 2.30874 2.15625 1.89453 2.15625H1.55078V5C1.55078 6.36345 2.65607 7.46875 4.01953 7.46875C5.38298 7.46875 6.48828 6.36345 6.48828 5V2.15625H6.14453C5.73031 2.15625 5.39453 1.82046 5.39453 1.40625C5.39453 0.992036 5.73031 0.65625 6.14453 0.65625H7.23828C7.65249 0.65625 7.98828 0.992036 7.98828 1.40625V5C7.98828 6.91342 6.63421 8.51058 4.83203 8.88551V9.45776C4.83203 11.0862 6.15211 12.4062 7.78051 12.4062C9.40892 12.4062 10.729 11.0862 10.729 9.45776V6.78177C9.50191 6.39768 8.75777 5.1275 9.04966 3.85282C9.35635 2.51345 10.6907 1.6763 12.0301 1.983C13.3695 2.28969 14.2066 3.62409 13.8999 4.96346C13.6963 5.85284 13.0395 6.52077 12.229 6.7794V9.45776C12.229 11.9146 10.2373 13.9062 7.78051 13.9062C5.32368 13.9062 3.33203 11.9146 3.33203 9.45776V8.90942C1.46783 8.58385 0.0507812 6.95741 0.0507812 5V1.40625Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189789\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"syringe\", \"keywords\": [ \"instrument\", \"medical\", \"syringe\", \"health\", \"beauty\", \"needle\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189819)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.46889 0.393235C9.85941 0.00271073 10.4926 0.00271073 10.8831 0.393235L13.606 3.11615C13.9966 3.50667 13.9966 4.13984 13.606 4.53036C13.2155 4.92089 12.5824 4.92089 12.1918 4.53036L11.5375 3.876L10.2288 5.18471L12.6984 7.65434C13.0889 8.04487 13.0889 8.67803 12.6984 9.06855C12.3079 9.45908 11.6747 9.45908 11.2842 9.06855L10.2833 8.06769L6.91156 11.507L6.90828 11.5103C6.69325 11.7256 6.43789 11.8964 6.15682 12.0129C5.87575 12.1294 5.57447 12.1894 5.27021 12.1894C4.96594 12.1894 4.66466 12.1294 4.38359 12.0129C4.1617 11.9209 3.95583 11.7951 3.77318 11.6403L1.80672 13.6067C1.41619 13.9973 0.783027 13.9973 0.392503 13.6067C0.00197831 13.2162 0.00197831 12.5831 0.392503 12.1925L2.35897 10.2261C2.20415 10.0434 2.07835 9.83755 1.98636 9.61565C1.86985 9.33458 1.80988 9.0333 1.80988 8.72904C1.80988 8.42478 1.86985 8.1235 1.98636 7.84243C2.10288 7.56136 2.27365 7.30601 2.48892 7.09098L2.49223 7.08767L5.93156 3.71596L4.93068 2.71509C4.54016 2.32456 4.54016 1.6914 4.93068 1.30087C5.32121 0.910347 5.95437 0.910347 6.3449 1.30087L8.81452 3.7705L10.1233 2.46179L9.46889 1.80745C9.07837 1.41692 9.07837 0.783759 9.46889 0.393235Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189819\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"tablet-capsule\", \"keywords\": [ \"health\", \"medical\", \"hospital\", \"medicine\", \"capsule\", \"tablet\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189816)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.01576 5.21894L2.89882 7.33587C2.3996 7.83509 2.11914 8.51219 2.11914 9.21819C2.11914 9.9242 2.3996 10.6013 2.89882 11.1005C3.39805 11.5997 4.07514 11.8802 4.78115 11.8802C5.48716 11.8802 6.16425 11.5997 6.66347 11.1005L8.7804 8.98358L5.01576 5.21894ZM9.21758 0.119751C7.98114 0.119751 6.79534 0.610925 5.92105 1.48522L1.48461 5.92166C0.610315 6.79595 0.119141 7.98175 0.119141 9.21819C0.119141 10.4546 0.610315 11.6404 1.48461 12.5147C2.35891 13.389 3.54471 13.8802 4.78115 13.8802C6.01759 13.8802 7.20339 13.389 8.07768 12.5147L12.5141 8.07829C13.3884 7.204 13.8796 6.0182 13.8796 4.78176C13.8796 3.54532 13.3884 2.35952 12.5141 1.48522C11.6398 0.610925 10.454 0.119751 9.21758 0.119751Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189816\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"tooth\", \"keywords\": [ \"health\", \"medical\", \"tooth\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.08076 2.41147C4.12162 1.27624 1.24515 1.59143 0.885816 4.86132C0.525333 8.14172 2.37268 11.6122 3.36172 12.9412C3.58674 13.2435 3.95007 13.388 4.32697 13.388C4.94473 13.388 5.49616 13.0006 5.70567 12.4195L6.05592 11.4479C6.19888 11.0514 6.57773 10.7871 6.99925 10.7871C7.42077 10.7871 7.79962 11.0514 7.94258 11.4479L8.29282 12.4195C8.50233 13.0006 9.05377 13.388 9.67153 13.388C10.0485 13.388 10.4118 13.2435 10.6368 12.9412C11.6258 11.6122 13.4732 8.14172 13.1127 4.86132C12.7534 1.59143 9.87687 1.27624 7.91773 2.41147C7.30194 2.76829 6.69655 2.76829 6.08076 2.41147ZM8.45103 4.10966C8.77532 3.99141 9.13408 4.15843 9.25233 4.48272C9.37058 4.80702 9.20355 5.16577 8.87926 5.28402C8.16389 5.54488 7.55895 5.69375 6.94539 5.69222C6.32775 5.69069 5.75438 5.53697 5.10233 5.27757C4.7816 5.14997 4.62503 4.78653 4.75263 4.46581C4.88023 4.14508 5.24367 3.98851 5.56439 4.11611C6.14101 4.3455 6.54944 4.44124 6.94849 4.44223C7.35162 4.44323 7.79824 4.3477 8.45103 4.10966Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"virus-antivirus\", \"keywords\": [ \"health\", \"medical\", \"covid19\", \"flu\", \"influenza\", \"virus\", \"antivirus\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189822)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 1.48018C5.7462 1.48018 4.59004 1.8982 3.66329 2.60249L5.11224 4.05145C5.45524 3.83148 5.83914 3.66973 6.25 3.58013V2.71875C6.25 2.30454 6.58579 1.96875 7 1.96875C7.41421 1.96875 7.75 2.30454 7.75 2.71875V3.58046C8.16068 3.67022 8.54441 3.83207 8.88723 4.0521L9.49701 3.44232C9.78991 3.14942 10.2648 3.14942 10.5577 3.44232C10.8506 3.73521 10.8506 4.21008 10.5577 4.50298L9.94772 5.11293C10.1676 5.45574 10.3293 5.8394 10.4189 6.25H11.2812C11.6955 6.25 12.0312 6.58579 12.0312 7C12.0312 7.41421 11.6955 7.75 11.2812 7.75H10.4189C10.3293 8.16056 10.1676 8.54419 9.94774 8.88697L11.3975 10.3367C12.1018 9.40998 12.5198 8.25381 12.5198 7C12.5198 3.95149 10.0485 1.48018 7 1.48018ZM4.05139 5.11192L4.04824 5.11684L8.88236 9.95095L8.88726 9.94781L10.3369 11.3974C9.41007 12.1018 8.25386 12.5198 7 12.5198C3.95149 12.5198 1.48018 10.0485 1.48018 7C1.48018 5.74613 1.89825 4.58991 2.60261 3.66313L4.05139 5.11192ZM0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7ZM2.71875 6.25H3.41364L7.75 10.5864V11.2812C7.75 11.6955 7.41421 12.0312 7 12.0312C6.58579 12.0312 6.25 11.6955 6.25 11.2812V10.4198C5.83913 10.3302 5.45522 10.1684 5.11221 9.94846L4.50299 10.5577C4.21009 10.8506 3.73522 10.8506 3.44233 10.5577C3.14943 10.2648 3.14943 9.78992 3.44233 9.49702L4.05137 8.88798C3.83121 8.54494 3.66929 8.16096 3.57957 7.75H2.71875C2.30454 7.75 1.96875 7.41421 1.96875 7C1.96875 6.58579 2.30454 6.25 2.71875 6.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189822\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"waiting-appointments-calendar\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189840)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.5 1C4.5 0.447715 4.05228 0 3.5 0C2.94772 0 2.5 0.447715 2.5 1V2H1.5C0.671573 2 0 2.67157 0 3.5V12.5C0 13.3284 0.671573 14 1.5 14H12.5C13.3284 14 14 13.3284 14 12.5V3.5C14 2.67157 13.3284 2 12.5 2H11.5001V1C11.5001 0.447715 11.0524 0 10.5001 0C9.94781 0 9.50009 0.447715 9.50009 1V2H4.5V1ZM5.97021 5.42784C5.97021 5.21117 6.14586 5.03552 6.36253 5.03552H7.63757C7.85424 5.03552 8.02989 5.21117 8.02989 5.42784V6.9881H9.59015C9.80683 6.9881 9.98247 7.16375 9.98247 7.38042V8.65546C9.98247 8.87214 9.80683 9.04778 9.59015 9.04778H8.02989L8.02984 10.608C8.02984 10.8247 7.85419 11.0004 7.63752 11.0004H6.36248C6.14581 11.0004 5.97016 10.8247 5.97016 10.608V9.04778H4.4099C4.19323 9.04778 4.01758 8.87214 4.01758 8.65546V7.38042C4.01758 7.16375 4.19323 6.9881 4.4099 6.9881H5.97021V5.42784Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189840\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"wheelchair\", \"keywords\": [ \"health\", \"medical\", \"hospital\", \"wheelchair\", \"disable\", \"help\", \"sign\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189828)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.832031 0.0822754C0.417818 0.0822754 0.0820312 0.418062 0.0820312 0.832275C0.0820312 1.24649 0.417818 1.58228 0.832031 1.58228H2.58919L3.52511 6.26969C1.59129 6.46547 0.0820312 8.09842 0.0820312 10.0839C0.0820312 12.2013 1.79852 13.9178 3.91591 13.9178C5.69196 13.9178 7.18595 12.7101 7.62147 11.0711H10.3749C10.2136 11.3502 10.1213 11.6741 10.1213 12.0196C10.1213 13.0679 10.9711 13.9178 12.0194 13.9178C13.0677 13.9178 13.9175 13.0679 13.9175 12.0196C13.9175 11.1168 13.2872 10.3612 12.4426 10.1688L12.4304 10.124L12.4262 10.1091L11.3193 6.04854C11.2079 5.44036 10.8871 4.88983 10.4118 4.49299C9.92751 4.08862 9.3148 3.8705 8.68405 3.87781H4.57714L3.93973 0.685424C3.86971 0.334745 3.56184 0.0822754 3.20424 0.0822754H0.832031ZM10.7248 9.57111H7.71578C7.67523 9.2677 7.59919 8.97553 7.49198 8.6989H10.487L10.7248 9.57111ZM10.0781 7.1989H6.44094C6.051 6.85733 5.5912 6.59355 5.08578 6.43178C5.08375 6.41728 5.08129 6.40275 5.07839 6.38821L4.87664 5.37782L8.68876 5.37788L8.69902 5.37774C8.97329 5.37398 9.23984 5.46857 9.45039 5.64438C9.66093 5.82019 9.80156 6.06558 9.84679 6.33612C9.85093 6.3609 9.85632 6.38546 9.86292 6.40969L10.0781 7.1989Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189828\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" } ], \"images_photography\": [ { \"name\": \"auto-flash\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.26224 0.112626C6.4714 0.177588 6.61393 0.371111 6.61393 0.590125V4.20471H8.99414C9.17982 4.20471 9.35021 4.3076 9.43665 4.47193C9.5231 4.63626 9.51136 4.83496 9.40616 4.98797L4.88012 11.5713C4.75604 11.7518 4.52895 11.8305 4.31979 11.7655C4.11064 11.7006 3.9681 11.5071 3.9681 11.288V7.67346H1.58789C1.40221 7.67346 1.23182 7.57056 1.14538 7.40623C1.05894 7.2419 1.07068 7.0432 1.17587 6.89019L5.70191 0.306861C5.82599 0.126384 6.05308 0.047664 6.26224 0.112626ZM10.5254 6.49634C11.1468 6.49634 11.6985 6.89398 11.895 7.48351L13.0652 10.9942C13.0688 11.004 13.0721 11.0139 13.0752 11.0239L13.7369 13.0092C13.8679 13.4021 13.6555 13.8269 13.2626 13.9579C12.8696 14.0888 12.4449 13.8765 12.3139 13.4835L11.8182 11.9963H9.23262L8.7369 13.4835C8.60591 13.8765 8.18118 14.0888 7.78822 13.9579C7.39526 13.8269 7.18289 13.4021 7.31388 13.0092L7.97457 11.0271C7.97827 11.0149 7.98228 11.0029 7.98658 10.9911L9.15576 7.48351C9.35227 6.89398 9.90397 6.49634 10.5254 6.49634ZM10.5254 8.11805L11.3182 10.4963H9.73262L10.5254 8.11805Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"camera-1\", \"keywords\": [ \"photos\", \"picture\", \"camera\", \"photography\", \"photo\", \"pictures\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.6 1.7C4.69443 1.5741 4.84262 1.5 5 1.5H9C9.15738 1.5 9.30557 1.5741 9.4 1.7L10.75 3.5H12.5C12.8978 3.5 13.2794 3.65804 13.5607 3.93934C13.842 4.22064 14 4.60217 14 5V11C14 11.3978 13.842 11.7794 13.5607 12.0607C13.2794 12.342 12.8978 12.5 12.5 12.5H1.5C1.10217 12.5 0.720644 12.342 0.43934 12.0607C0.158035 11.7794 0 11.3978 0 11V5C0 4.60218 0.158035 4.22064 0.43934 3.93934C0.720644 3.65804 1.10217 3.5 1.5 3.5H3.25L4.6 1.7ZM9.46132 7.71132C9.46132 9.07068 8.35934 10.1726 6.99999 10.1726C5.64064 10.1726 4.53867 9.07068 4.53867 7.71132C4.53867 6.35197 5.64064 5.25 6.99999 5.25C8.35934 5.25 9.46132 6.35197 9.46132 7.71132Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"camera-disabled\", \"keywords\": [ \"photos\", \"picture\", \"camera\", \"photography\", \"photo\", \"pictures\", \"disabled\", \"off\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187652)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.219669 0.218938C-0.0732231 0.511831 -0.0732231 0.986705 0.219669 1.2796L0.397768 1.4577C0.415456 1.4719 0.432551 1.48721 0.44896 1.50362L12.7768 13.8315C13.0714 14.0727 13.5067 14.0558 13.7817 13.7809C14.0745 13.488 14.0745 13.0131 13.7817 12.7203L12.9526 11.8912C13.046 11.8439 13.1323 11.7818 13.2077 11.7063C13.3952 11.5188 13.5006 11.2644 13.5006 10.9992V4.99927C13.5006 4.73405 13.3952 4.4797 13.2077 4.29216C13.0202 4.10463 12.7658 3.99927 12.5006 3.99927H10.5006L9.00062 1.99927H5.00062L4.16921 3.10782L1.28033 0.218938C0.987436 -0.0739558 0.512562 -0.0739558 0.219669 0.218938ZM9.17684 11.9992L1.2177 4.04012C1.05889 4.08697 0.91271 4.17297 0.793518 4.29216C0.605981 4.4797 0.500624 4.73405 0.500624 4.99927V10.9992C0.500624 11.2644 0.605981 11.5188 0.793518 11.7063C0.981054 11.8939 1.23541 11.9992 1.50062 11.9992H9.17684ZM9.26625 7.48364C9.26625 7.70296 9.23509 7.91499 9.17695 8.11556L6.3687 5.30732C6.56927 5.24918 6.78131 5.21802 7.00062 5.21802C8.25189 5.21802 9.26625 6.23237 9.26625 7.48364Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187652\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"camera-loading\", \"keywords\": [ \"photos\", \"picture\", \"camera\", \"photography\", \"photo\", \"pictures\", \"loading\", \"option\", \"setting\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.6 1.7C4.69443 1.5741 4.84262 1.5 5 1.5H9C9.15738 1.5 9.30557 1.5741 9.4 1.7L10.75 3.5H12.5C12.8978 3.5 13.2794 3.65804 13.5607 3.93934C13.842 4.22064 14 4.60217 14 5V11C14 11.3978 13.842 11.7794 13.5607 12.0607C13.2794 12.342 12.8978 12.5 12.5 12.5H1.5C1.10217 12.5 0.720644 12.342 0.43934 12.0607C0.158035 11.7794 0 11.3978 0 11V5C0 4.60218 0.158035 4.22064 0.43934 3.93934C0.720644 3.65804 1.10217 3.5 1.5 3.5H3.25L4.6 1.7ZM4.29199 8.50781C4.71052 8.50781 5.0498 8.16853 5.0498 7.75C5.0498 7.33147 4.71052 6.99219 4.29199 6.99219C3.87346 6.99219 3.53418 7.33147 3.53418 7.75C3.53418 8.16853 3.87346 8.50781 4.29199 8.50781ZM7.64575 7.75C7.64575 8.16853 7.30647 8.50781 6.88794 8.50781C6.46941 8.50781 6.13013 8.16853 6.13013 7.75C6.13013 7.33147 6.46941 6.99219 6.88794 6.99219C7.30647 6.99219 7.64575 7.33147 7.64575 7.75ZM9.75 8.50781C10.1685 8.50781 10.5078 8.16853 10.5078 7.75C10.5078 7.33147 10.1685 6.99219 9.75 6.99219C9.33147 6.99219 8.99219 7.33147 8.99219 7.75C8.99219 8.16853 9.33147 8.50781 9.75 8.50781Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"camera-square\", \"keywords\": [ \"photos\", \"picture\", \"camera\", \"photography\", \"photo\", \"pictures\", \"frame\", \"square\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187634)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1.5C0 0.671573 0.671573 0 1.5 0H12.5C13.3284 0 14 0.671573 14 1.5V12.5C14 13.3284 13.3284 14 12.5 14H1.5C0.671573 14 0 13.3284 0 12.5V1.5ZM10.8244 6.23173C10.8244 5.94193 10.7093 5.664 10.5044 5.45908C10.2995 5.25416 10.0215 5.13904 9.73172 5.13904H9.18537L8.09268 3.5H5.9073L4.81461 5.13904H4.26826C3.97846 5.13904 3.70053 5.25416 3.49561 5.45908C3.29069 5.664 3.17557 5.94193 3.17557 6.23173V8.96346C3.17557 9.25326 3.29069 9.53119 3.49561 9.73611C3.70053 9.94103 3.97846 10.0562 4.26826 10.0562H9.73172C10.0215 10.0562 10.2995 9.94103 10.5044 9.73611C10.7093 9.53119 10.8244 9.25326 10.8244 8.96346V6.23173ZM5.80881 7.4693C5.80881 6.81141 6.34214 6.27808 7.00003 6.27808C7.65793 6.27808 8.19126 6.81141 8.19126 7.4693C8.19126 8.1272 7.65793 8.66053 7.00003 8.66053C6.34214 8.66053 5.80881 8.1272 5.80881 7.4693Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187634\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"composition-oval\", \"keywords\": [ \"camera\", \"frame\", \"composition\", \"photography\", \"pictures\", \"landscape\", \"photo\", \"oval\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187631)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.3208 2.39096C10.6245 1.74636 8.81627 1.44295 7 1.49962C5.18373 1.44295 3.37549 1.74636 1.6792 2.39095C1.32408 2.5259 1.03482 2.79793 0.882145 3.15057C0.358128 4.36093 0.058942 5.65787 0.000489593 6.97775C-0.000163198 6.99249 -0.000163198 7.00725 0.000489593 7.02199C0.058942 8.34188 0.358128 9.63881 0.882145 10.8492C1.03482 11.2018 1.32408 11.4739 1.6792 11.6088C3.37549 12.2534 5.18373 12.5568 7 12.5001C8.81627 12.5568 10.6245 12.2534 12.3208 11.6088C12.6759 11.4739 12.9652 11.2018 13.1179 10.8492C13.6419 9.63881 13.9411 8.34188 13.9995 7.02199C14.0002 7.00725 14.0002 6.99249 13.9995 6.97775C13.9411 5.65787 13.6419 4.36094 13.1179 3.15057C12.9652 2.79793 12.6759 2.5259 12.3208 2.39096Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187631\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"composition-vertical\", \"keywords\": [ \"camera\", \"portrait\", \"frame\", \"vertical\", \"composition\", \"photography\", \"photo\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path d=\\\"M10.8267 0.0511284C10.7827 0.0491319 10.7429 0.0503549 10.7182 0.0512168L10.6981 0.0519341C10.6827 0.052496 10.6757 0.0527542 10.6674 0.0527542H3.37815L3.36101 0.0524144C3.30568 0.051015 3.15465 0.0471957 3.01897 0.0794806C2.48591 0.20632 2.17501 0.739996 2.2742 1.24543C2.29586 1.3558 2.34324 1.46283 2.36226 1.50579L2.36796 1.51877C3.10741 3.25292 3.48863 5.11714 3.48863 7.00088C3.48863 8.89606 3.10276 10.7715 2.35444 12.5146L2.35078 12.5231C2.33657 12.5557 2.30375 12.6309 2.28389 12.7123C2.14957 13.2626 2.51342 13.8679 3.12891 13.9419C3.20038 13.9505 3.27801 13.9495 3.30734 13.9491L3.31673 13.949H10.6106L10.6265 13.9493C10.6763 13.9505 10.8094 13.9535 10.9288 13.9294C11.5029 13.8137 11.8046 13.2399 11.6558 12.7225C11.6309 12.6358 11.5774 12.5196 11.5643 12.4912L11.5615 12.485C10.8295 10.7476 10.4563 8.88168 10.4645 6.99789C10.4727 5.10713 10.8649 3.23768 11.6173 1.50122C11.6185 1.49862 11.6632 1.40293 11.6836 1.34718C11.8994 0.759127 11.5071 0.0819584 10.8267 0.0511284Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"compsition-horizontal\", \"keywords\": [ \"camera\", \"horizontal\", \"panorama\", \"composition\", \"photography\", \"photo\", \"pictures\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path d=\\\"M13.9497 10.7988C13.9517 10.7551 13.9505 10.7157 13.9496 10.6911L13.9489 10.6712C13.9483 10.6559 13.9481 10.6489 13.9481 10.6407V3.40548L13.9484 3.38847C13.9498 3.33354 13.9536 3.18363 13.9214 3.04896C13.7945 2.51985 13.2608 2.21126 12.7554 2.30971C12.645 2.33121 12.538 2.37824 12.495 2.39712L12.4821 2.40277C10.7479 3.13674 8.88369 3.51514 6.99995 3.51514C5.10476 3.51514 3.22935 3.13213 1.48625 2.38936L1.47781 2.38572C1.44522 2.37162 1.36992 2.33904 1.28855 2.31933C0.73824 2.186 0.132987 2.54715 0.058935 3.15808C0.050335 3.22903 0.051347 3.30608 0.0517294 3.3352L0.051825 3.34452L0.0518247 10.5843L0.0515407 10.6001C0.0504104 10.6495 0.047386 10.7816 0.0714545 10.9001C0.187212 11.47 0.760999 11.7694 1.27835 11.6218C1.36508 11.5971 1.48122 11.544 1.50965 11.531L1.5159 11.5281C3.25325 10.8016 5.11914 10.4312 7.00293 10.4393C8.8937 10.4474 10.7632 10.8367 12.4996 11.5836C12.5022 11.5848 12.5979 11.6291 12.6537 11.6494C13.2417 11.8635 13.9189 11.4742 13.9497 10.7988Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"edit-image-photo\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187625)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.8 0C1.32261 0 0.864773 0.189642 0.527208 0.527208C0.189642 0.864773 0 1.32261 0 1.8V12.2C0 12.6774 0.189643 13.1352 0.527208 13.4728C0.864773 13.8104 1.32261 14 1.8 14H5.51425C5.42294 13.7907 5.38946 13.5576 5.42213 13.3257L5.53402 12.5312H1.975C1.84074 12.5312 1.71197 12.4779 1.61703 12.383C1.52209 12.288 1.46875 12.1593 1.46875 12.025V7.00381C1.56767 7.00128 1.6669 7 1.76643 7C3.6414 7 5.41068 7.45251 6.97102 8.25428C7.16497 8.39532 7.43259 8.59654 7.7292 8.82946L10.7751 5.77709C11.0094 5.54226 11.3275 5.41021 11.6592 5.41003C11.991 5.40986 12.3092 5.54157 12.5438 5.77615L14 7.23236V1.8C14 1.32261 13.8104 0.864773 13.4728 0.527208C13.1352 0.189643 12.6774 0 12.2 0H1.8ZM10.4688 3.58984C10.4688 4.41827 9.79718 5.08984 8.96875 5.08984C8.14032 5.08984 7.46875 4.41827 7.46875 3.58984C7.46875 2.76142 8.14032 2.08984 8.96875 2.08984L8.99138 2.09001L9.01367 2.08984C9.81729 2.08984 10.4688 2.7413 10.4688 3.54492L10.4686 3.56721L10.4688 3.58984ZM6.79662 12.5312L6.65991 13.5L8.79016 13.2102L13.5002 8.50016L11.6602 6.66016L6.96016 11.3702L6.79662 12.5312Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187625\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"film-roll-1\", \"keywords\": [ \"photos\", \"camera\", \"shutter\", \"picture\", \"photography\", \"pictures\", \"photo\", \"film\", \"roll\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187670)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 6C0 2.96243 2.46243 0.5 5.5 0.5C8.53757 0.5 11 2.96243 11 6C11 6.06114 10.999 6.12205 10.997 6.18271C10.999 6.20488 11 6.22732 11 6.25V11.25C11 11.6642 11.3358 12 11.75 12C12.1642 12 12.5 11.6642 12.5 11.25C12.5 10.8358 12.8358 10.5 13.25 10.5C13.6642 10.5 14 10.8358 14 11.25C14 12.4926 12.9926 13.5 11.75 13.5C10.5074 13.5 9.5 12.4926 9.5 11.25V9.77494C8.49723 10.8371 7.07603 11.5 5.5 11.5C2.46243 11.5 0 9.03757 0 6ZM6.375 3.5C6.375 3.98325 5.98325 4.375 5.5 4.375C5.01675 4.375 4.625 3.98325 4.625 3.5C4.625 3.01675 5.01675 2.625 5.5 2.625C5.98325 2.625 6.375 3.01675 6.375 3.5ZM3.19531 6.88281C3.67856 6.88281 4.07031 6.49106 4.07031 6.00781C4.07031 5.52456 3.67856 5.13281 3.19531 5.13281C2.71206 5.13281 2.32031 5.52456 2.32031 6.00781C2.32031 6.49106 2.71206 6.88281 3.19531 6.88281ZM8.67969 6.00781C8.67969 6.49106 8.28794 6.88281 7.80469 6.88281C7.32144 6.88281 6.92969 6.49106 6.92969 6.00781C6.92969 5.52456 7.32144 5.13281 7.80469 5.13281C8.28794 5.13281 8.67969 5.52456 8.67969 6.00781ZM5.5 9.24219C5.98325 9.24219 6.375 8.85044 6.375 8.36719C6.375 7.88394 5.98325 7.49219 5.5 7.49219C5.01675 7.49219 4.625 7.88394 4.625 8.36719C4.625 8.85044 5.01675 9.24219 5.5 9.24219Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187670\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"film-slate\", \"keywords\": [ \"pictures\", \"photo\", \"film\", \"slate\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187664)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.17465 6.08061L3.46933 5.46575C3.47168 5.45076 3.47459 5.43574 3.47808 5.42073L4.24731 2.10604L5.61544 1.73945L5.61239 1.7533L4.83582 5.0996L7.70943 4.32962L7.71217 4.31758L8.50859 0.964229L9.88059 0.596602L9.87834 0.606419L9.08139 3.96201L12.9692 2.92028L12.4402 0.946019C12.2972 0.412553 11.7489 0.0959702 11.2154 0.238912L1.35275 2.88161C0.819292 3.02455 0.502709 3.57289 0.645651 4.10635L1.17465 6.08061ZM1.17602 6.08063H13.3866V12.7954C13.3866 13.3476 12.9389 13.7954 12.3866 13.7954H2.17602C1.62374 13.7954 1.17602 13.3476 1.17602 12.7954V6.08063ZM3.35009 11.0838C3.35009 10.7387 3.62992 10.4588 3.97509 10.4588H5.3396C5.68478 10.4588 5.9646 10.7387 5.9646 11.0838C5.9646 11.429 5.68478 11.7088 5.3396 11.7088H3.97509C3.62992 11.7088 3.35009 11.429 3.35009 11.0838Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187664\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"flash-1\", \"keywords\": [ \"flash\", \"power\", \"connect\", \"charge\", \"electricity\", \"lightning\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187688)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.27188 0C4.07112 0 3.88983 0.120076 3.8115 0.304925L1.5615 5.61493L1.55939 5.61999C1.49715 5.77149 1.47307 5.93596 1.48928 6.09894C1.50548 6.26192 1.56147 6.41843 1.65232 6.55471C1.74317 6.69098 1.86611 6.80286 2.01032 6.8805C2.15453 6.95815 2.31561 6.99918 2.47939 6.99999L2.48188 7H4.60902L2.79112 13.3626C2.72921 13.5793 2.81989 13.8106 3.01255 13.9275C3.2052 14.0444 3.45224 14.0179 3.6158 13.8629L12.2058 5.72293L12.2088 5.72005C12.3515 5.58254 12.4501 5.40572 12.4922 5.21205C12.5342 5.01838 12.5177 4.81659 12.4449 4.63229C12.372 4.448 12.246 4.2895 12.0829 4.17693C11.9198 4.06436 11.7269 4.0028 11.5288 4.00005L11.5219 4H8.5809L10.2191 0.723607C10.2966 0.568613 10.2883 0.384543 10.1972 0.237134C10.1061 0.0897262 9.94517 0 9.77188 0H4.27188Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187688\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"flash-2\", \"keywords\": [ \"flash\", \"power\", \"connect\", \"charge\", \"electricity\", \"lightning\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187676)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.5 0.500037C8.5 0.281023 8.35747 0.0875006 8.14831 0.0225383C7.93915 -0.042424 7.71206 0.0362961 7.58798 0.216773L2.08798 8.21677C1.98279 8.36978 1.97105 8.56848 2.05749 8.73281C2.14393 8.89714 2.31432 9.00004 2.5 9.00004H5.5V13.5001C5.5 13.7191 5.64254 13.9126 5.85169 13.9776C6.06085 14.0425 6.28794 13.9638 6.41202 13.7833L11.912 5.7833C12.0172 5.63029 12.029 5.43159 11.9425 5.26726C11.8561 5.10293 11.6857 5.00004 11.5 5.00004H8.5V0.500037Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187676\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"flash-3\", \"keywords\": [ \"flash\", \"power\", \"connect\", \"charge\", \"electricity\", \"lightning\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path d=\\\"M3.08096 5.28083L4.28705 0.873963C4.406 0.439326 4.80095 0.137939 5.25157 0.137939H7.19196C7.85133 0.137939 8.33021 0.764917 8.15672 1.40106L7.4896 3.84718H9.31525C10.0047 3.84718 10.4873 4.5284 10.2587 5.1788L8.77097 9.41104H10.1294C10.5959 9.41104 10.8082 9.99342 10.4512 10.2937L6.60705 13.5267C6.3895 13.7096 6.06339 13.6747 5.88958 13.4497L3.39135 10.2167C3.13739 9.8881 3.37165 9.41104 3.78699 9.41104H5.29777L6.14078 6.54481H4.04548C3.38574 6.54481 2.9068 5.91717 3.08096 5.28083Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"flash-off\", \"keywords\": [ \"flash\", \"power\", \"connect\", \"charge\", \"off\", \"electricity\", \"lightning\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187685)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.50002 0.500037C8.50002 0.281023 8.35748 0.0875006 8.14832 0.0225383C7.93916 -0.042424 7.71207 0.0362961 7.588 0.216773L5.017 3.9564L1.28033 0.219731C0.987437 -0.0731623 0.512563 -0.0731623 0.21967 0.219731C-0.0732233 0.512624 -0.0732233 0.987498 0.21967 1.28039L12.7197 13.7804C13.0126 14.0733 13.4874 14.0733 13.7803 13.7804C14.0732 13.4875 14.0732 13.0127 13.7803 12.7198L9.84724 8.78664L11.912 5.7833C12.0172 5.63029 12.029 5.43159 11.9425 5.26726C11.8561 5.10293 11.6857 5.00004 11.5 5.00004H8.50002V0.500037ZM2.088 8.21677L3.43511 6.25733L8.26535 11.0876L6.41204 13.7833C6.28796 13.9638 6.06087 14.0425 5.85171 13.9776C5.64255 13.9126 5.50002 13.7191 5.50002 13.5001V9.00004H2.50002C2.31434 9.00004 2.14395 8.89714 2.05751 8.73281C1.97106 8.56848 1.9828 8.36978 2.088 8.21677Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187685\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"flower\", \"keywords\": [ \"photos\", \"photo\", \"picture\", \"camera\", \"photography\", \"pictures\", \"flower\", \"image\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187649)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 12.5C14 13.3284 13.3284 14 12.5 14H1.5C0.671573 14 0 13.3284 0 12.5V1.5C0 0.671573 0.671573 0 1.5 0H12.5C13.3284 0 14 0.671573 14 1.5V12.5ZM9.12132 7.2192C9.68393 6.65659 10 5.89353 10 5.09788V2.90788C10.0002 2.8229 9.97868 2.73929 9.93757 2.66493C9.89645 2.59057 9.83706 2.52791 9.765 2.48288C9.69295 2.43784 9.6106 2.41191 9.52574 2.40754C9.44088 2.40316 9.35631 2.42049 9.28 2.45788L7 3.59788L4.72 2.45788C4.6437 2.42049 4.55912 2.40316 4.47426 2.40754C4.3894 2.41191 4.30706 2.43784 4.235 2.48288C4.16294 2.52791 4.10355 2.59057 4.06244 2.66493C4.02132 2.73929 3.99983 2.8229 4 2.90788V5.09788C4 5.89353 4.31607 6.65659 4.87868 7.2192C5.2916 7.63212 5.81249 7.91223 6.375 8.03206V10.0842L4.94194 8.65118C4.69786 8.4071 4.30214 8.4071 4.05806 8.65118C3.81398 8.89526 3.81398 9.29099 4.05806 9.53507L6.55806 12.0351C6.61798 12.095 6.68704 12.1402 6.76076 12.1707C6.83278 12.2006 6.91159 12.2174 6.99423 12.2181H7H7.00577C7.16379 12.2166 7.32137 12.1556 7.44194 12.0351L9.94194 9.53507C10.186 9.29099 10.186 8.89526 9.94194 8.65118C9.69786 8.4071 9.30214 8.4071 9.05806 8.65118L7.625 10.0842V8.03206C8.18751 7.91224 8.7084 7.63212 9.12132 7.2192Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187649\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"focus-points\", \"keywords\": [ \"camera\", \"frame\", \"photography\", \"pictures\", \"photo\", \"focus\", \"position\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187682)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.542363 1.49671C0.86212 1.17695 1.2958 0.997314 1.74801 0.997314H2.70305C3.11726 0.997314 3.45305 1.3331 3.45305 1.74731C3.45305 2.16153 3.11726 2.49731 2.70305 2.49731H1.74801C1.69363 2.49731 1.64147 2.51892 1.60302 2.55737C1.56457 2.59582 1.54297 2.64797 1.54297 2.70235V3.6574C1.54297 4.07161 1.20718 4.4074 0.792968 4.4074C0.378755 4.4074 0.0429688 4.07161 0.0429688 3.6574V2.70235C0.0429688 2.25015 0.222606 1.81647 0.542363 1.49671ZM10.5484 1.74731C10.5484 1.3331 10.8842 0.997314 11.2984 0.997314H12.2534C12.7056 0.997314 13.1393 1.17695 13.4591 1.49671C13.7788 1.81647 13.9585 2.25015 13.9585 2.70235V3.6574C13.9585 4.07161 13.6227 4.4074 13.2085 4.4074C12.7942 4.4074 12.4585 4.07161 12.4585 3.6574V2.70235C12.4585 2.64797 12.4368 2.59582 12.3984 2.55737C12.3599 2.51892 12.3078 2.49731 12.2534 2.49731H11.2984C10.8842 2.49731 10.5484 2.16153 10.5484 1.74731ZM13.9585 10.3427C13.9585 9.92847 13.6227 9.59268 13.2085 9.59268C12.7942 9.59268 12.4585 9.92847 12.4585 10.3427V11.2977C12.4585 11.3521 12.4368 11.4042 12.3984 11.4427C12.3599 11.4811 12.3078 11.5027 12.2534 11.5027H11.2984C10.8842 11.5027 10.5484 11.8385 10.5484 12.2527C10.5484 12.6669 10.8842 13.0027 11.2984 13.0027H12.2534C12.7056 13.0027 13.1393 12.8231 13.4591 12.5033C13.7788 12.1836 13.9585 11.7499 13.9585 11.2977V10.3427ZM0.792968 9.59268C1.20718 9.59268 1.54297 9.92847 1.54297 10.3427V11.2977C1.54297 11.3521 1.56457 11.4042 1.60302 11.4427C1.64147 11.4811 1.69363 11.5027 1.74801 11.5027H2.70305C3.11726 11.5027 3.45305 11.8385 3.45305 12.2527C3.45305 12.6669 3.11726 13.0027 2.70305 13.0027H1.74801C1.2958 13.0027 0.86212 12.8231 0.542363 12.5033C0.222606 12.1836 0.0429688 11.7499 0.0429688 11.2977V10.3427C0.0429688 9.92847 0.378755 9.59268 0.792968 9.59268ZM6.0007 3.00004C6.0007 2.44775 6.44841 2.00004 7.0007 2.00004C7.55298 2.00004 8.0007 2.44775 8.0007 3.00004C8.0007 3.55232 7.55298 4.00004 7.0007 4.00004C6.44841 4.00004 6.0007 3.55232 6.0007 3.00004ZM3.0007 6.00004C2.44841 6.00004 2.0007 6.44775 2.0007 7.00004C2.0007 7.55232 2.44841 8.00004 3.0007 8.00004C3.55298 8.00004 4.0007 7.55232 4.0007 7.00004C4.0007 6.44775 3.55298 6.00004 3.0007 6.00004ZM7.0007 6.00004C6.44841 6.00004 6.0007 6.44775 6.0007 7.00004C6.0007 7.55232 6.44841 8.00004 7.0007 8.00004C7.55298 8.00004 8.0007 7.55232 8.0007 7.00004C8.0007 6.44775 7.55298 6.00004 7.0007 6.00004ZM10.0007 7.00004C10.0007 6.44775 10.4484 6.00004 11.0007 6.00004C11.553 6.00004 12.0007 6.44775 12.0007 7.00004C12.0007 7.55232 11.553 8.00004 11.0007 8.00004C10.4484 8.00004 10.0007 7.55232 10.0007 7.00004ZM7.0007 10C6.44841 10 6.0007 10.4477 6.0007 11C6.0007 11.5523 6.44841 12 7.0007 12C7.55298 12 8.0007 11.5523 8.0007 11C8.0007 10.4477 7.55298 10 7.0007 10Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187682\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"landscape-2\", \"keywords\": [ \"photos\", \"photo\", \"landscape\", \"picture\", \"photography\", \"camera\", \"pictures\", \"image\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187661)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 12.5C14 13.3284 13.3284 14 12.5 14H1.5C0.671573 14 0 13.3284 0 12.5V1.5C0 0.671573 0.671573 0 1.5 0H12.5C13.3284 0 14 0.671573 14 1.5V12.5ZM3.75 2C2.7835 2 2 2.7835 2 3.75C2 4.7165 2.7835 5.5 3.75 5.5C4.7165 5.5 5.5 4.7165 5.5 3.75C5.5 2.7835 4.7165 2 3.75 2ZM8.40099 6.59901L2.5 12.5H12.5V8.5L9.68934 6.49238C9.29155 6.20825 8.74665 6.25335 8.40099 6.59901Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187661\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"landscape-setting\", \"keywords\": [ \"design\", \"composition\", \"horizontal\", \"lanscape\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.5 12.5C13.3284 12.5 14 11.8284 14 11V3C14 2.17157 13.3284 1.5 12.5 1.5H1.5C0.671573 1.5 0 2.17157 0 3V11C0 11.8284 0.671573 12.5 1.5 12.5H12.5ZM7.00504 5.46153C7.68239 5.45874 8.23063 4.90878 8.23063 4.23077C8.23063 3.55104 7.6796 3 6.99986 3C6.32013 3 5.76909 3.55104 5.76909 4.23077C5.76909 4.90883 6.3174 5.45881 6.99482 5.46153C6.42547 5.46291 5.87973 5.68968 5.47704 6.09238C5.07311 6.4963 4.84619 7.04414 4.84619 7.61538V8.53846H5.76927L6.07696 11H7.92312L8.23081 8.53846H9.15389V7.61538C9.15389 7.04414 8.92697 6.4963 8.52304 6.09238C8.1203 5.68963 7.57447 5.46285 7.00504 5.46153Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"laptop-camera\", \"keywords\": [ \"photos\", \"photo\", \"picture\", \"photography\", \"camera\", \"pictures\", \"laptop\", \"computer\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187694)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.8425 2.65283C13.8425 2.38761 13.7372 2.13326 13.5496 1.94572C13.3621 1.75819 13.1077 1.65283 12.8425 1.65283H12.3425L11.3425 0.152832H9.3425L8.3425 1.65283H7.8425C7.57729 1.65283 7.32293 1.75819 7.1354 1.94572C6.94786 2.13326 6.8425 2.38761 6.8425 2.65283V5.15283C6.8425 5.41805 6.94786 5.6724 7.1354 5.85994C7.32293 6.04747 7.57729 6.15283 7.8425 6.15283H12.8425C13.1077 6.15283 13.3621 6.04747 13.5496 5.85994C13.7372 5.6724 13.8425 5.41805 13.8425 5.15283V2.65283ZM9.3425 3.65283C9.3425 3.10055 9.79022 2.65283 10.3425 2.65283C10.8948 2.65283 11.3425 3.10055 11.3425 3.65283C11.3425 4.20511 10.8948 4.65283 10.3425 4.65283C9.79022 4.65283 9.3425 4.20511 9.3425 3.65283ZM2.98667 4.75209C2.98667 4.61402 3.0986 4.50209 3.23667 4.50209H4.8425C5.25672 4.50209 5.5925 4.1663 5.5925 3.75209C5.5925 3.33787 5.25672 3.00209 4.8425 3.00209H3.23667C2.27017 3.00209 1.48667 3.78559 1.48667 4.75209V9.65283C1.48667 9.66298 1.48687 9.67308 1.48727 9.68314L0.323233 11.764L0.315469 11.7784C0.202931 11.9961 0.148967 12.239 0.159495 12.4839C0.170023 12.7288 0.244622 12.9662 0.37517 13.1734C0.50566 13.3806 0.687382 13.5503 0.901953 13.6677C1.11646 13.7851 1.3573 13.8467 1.60173 13.8474H1.6032H11.0819H11.0833C11.3278 13.8467 11.5686 13.7851 11.7831 13.6677C11.9977 13.5503 12.1794 13.3806 12.3099 13.1734C12.4404 12.9662 12.515 12.7288 12.5256 12.4839C12.5361 12.239 12.4821 11.9961 12.3696 11.7784L12.3618 11.764L11.1978 9.68314C11.1982 9.67309 11.1984 9.66298 11.1984 9.65283V8.15283C11.1984 7.73862 10.8626 7.40283 10.4484 7.40283C10.0342 7.40283 9.69838 7.73862 9.69838 8.15283V8.90283H2.98667V4.75209Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187694\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"mobile-phone-camera\", \"keywords\": [ \"photos\", \"photo\", \"picture\", \"photography\", \"camera\", \"pictures\", \"phone\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187655)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.75 1.5C1.61193 1.5 1.5 1.61193 1.5 1.75V12.2388C1.50165 12.2429 1.50486 12.2499 1.51076 12.26C1.52996 12.2927 1.56669 12.3382 1.62329 12.3864C1.67937 12.4343 1.73851 12.4707 1.78463 12.4924C1.79111 12.4954 1.79688 12.4979 1.80187 12.5H8.25C8.38807 12.5 8.5 12.3881 8.5 12.25V8.25C8.5 7.83579 8.83579 7.5 9.25 7.5C9.66421 7.5 10 7.83579 10 8.25V12.25C10 13.2165 9.2165 14 8.25 14H1.75C1.51778 14 1.30218 13.9229 1.14782 13.8505C0.978094 13.7709 0.806129 13.6609 0.649999 13.5278C0.361255 13.2816 0 12.8419 0 12.25V1.75C0 0.783502 0.783502 0 1.75 0H5C5.41421 0 5.75 0.335786 5.75 0.75C5.75 1.16421 5.41421 1.5 5 1.5H1.75ZM13.7071 1.79289C13.8946 1.98043 14 2.23478 14 2.5V5C14 5.26522 13.8946 5.51957 13.7071 5.70711C13.5196 5.89464 13.2652 6 13 6H8C7.73478 6 7.48043 5.89464 7.29289 5.70711C7.10536 5.51957 7 5.26522 7 5V2.5C7 2.23478 7.10536 1.98043 7.29289 1.79289C7.48043 1.60536 7.73478 1.5 8 1.5H8.5L9.5 0H11.5L12.5 1.5H13C13.2652 1.5 13.5196 1.60536 13.7071 1.79289ZM10.5 2.5C9.94772 2.5 9.5 2.94772 9.5 3.5C9.5 4.05228 9.94772 4.5 10.5 4.5C11.0523 4.5 11.5 4.05228 11.5 3.5C11.5 2.94772 11.0523 2.5 10.5 2.5ZM4.5 9.75C4.08579 9.75 3.75 10.0858 3.75 10.5C3.75 10.9142 4.08579 11.25 4.5 11.25H5.5C5.91421 11.25 6.25 10.9142 6.25 10.5C6.25 10.0858 5.91421 9.75 5.5 9.75H4.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187655\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"orientation-landscape\", \"keywords\": [ \"photos\", \"photo\", \"orientation\", \"landscape\", \"picture\", \"photography\", \"camera\", \"pictures\", \"image\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 11C14 11.8284 13.3284 12.5 12.5 12.5H1.5C0.671573 12.5 0 11.8284 0 11V3C0 2.17157 0.671573 1.5 1.5 1.5H12.5C13.3284 1.5 14 2.17157 14 3V11ZM10.9688 4.9669C10.9688 5.77709 10.3121 6.43387 9.50188 6.43387C8.6917 6.43387 8.03491 5.77709 8.03491 4.9669C8.03491 4.15672 8.6917 3.49994 9.50188 3.49994C10.3121 3.49994 10.9688 4.15672 10.9688 4.9669ZM1.95792 6.71378C3.52173 6.67842 5.04724 7.19983 6.26225 8.18497C7.31905 9.04183 8.08114 10.201 8.45163 11.5H1.5C1.22386 11.5 1 11.2761 1 11V6.77458C1.31324 6.73335 1.62903 6.71307 1.94527 6.71391L1.95792 6.71378ZM9.48612 11.5H12.5001C12.7763 11.5 13.0001 11.2761 13.0001 11V9.30899C12.368 9.13014 11.7136 9.03938 11.0555 9.03975H11.0542C10.2341 9.03797 9.42097 9.17598 8.64941 9.44669C9.02518 10.0831 9.30818 10.7744 9.48612 11.5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"orientation-portrait\", \"keywords\": [ \"photos\", \"photo\", \"orientation\", \"portrait\", \"picture\", \"photography\", \"camera\", \"pictures\", \"image\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11 0C11.8284 0 12.5 0.671573 12.5 1.5V12.5C12.5 13.3284 11.8284 14 11 14H3C2.17157 14 1.5 13.3284 1.5 12.5V1.5C1.5 0.671572 2.17157 0 3 0H11ZM10.3186 4.53125C10.3186 5.35968 9.64703 6.03125 8.8186 6.03125C7.99018 6.03125 7.3186 5.35968 7.3186 4.53125C7.3186 3.70282 7.99018 3.03125 8.8186 3.03125C9.64703 3.03125 10.3186 3.70282 10.3186 4.53125ZM3.06294 8.21982C4.18418 8.19727 5.27148 8.53033 6.13087 9.15008C7.35418 10.0323 7.92567 11.5251 8.02937 13H3C2.72386 13 2.5 12.7761 2.5 12.5V8.2453C2.6831 8.22797 2.86727 8.21949 3.0517 8.21992L3.06294 8.21982ZM9.89667 9.56422C9.276 9.56302 8.6606 9.64485 8.07104 9.80551C8.67646 10.7832 8.96452 11.9222 9.03133 13H10.9999C11.2761 13 11.4999 12.7761 11.4999 12.5V9.75343C10.9788 9.62779 10.44 9.56395 9.89789 9.56422H9.89667Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"polaroid-four\", \"keywords\": [ \"photos\", \"camera\", \"polaroid\", \"picture\", \"photography\", \"pictures\", \"four\", \"photo\", \"image\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187679)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.35993 5.45838C6.35993 6.032 5.89492 6.49701 5.32131 6.49701H1.1773C0.603682 6.49701 0.138672 6.032 0.138672 5.45838V1.03863C0.138672 0.465011 0.603681 0 1.1773 0H5.32131C5.89492 0 6.35993 0.465009 6.35993 1.03863V5.45838ZM1.61122 1.48564H4.88742V4.00405H1.61122V1.48564ZM6.35993 12.9599C6.35993 13.5335 5.89492 13.9985 5.32131 13.9985H1.1773C0.603682 13.9985 0.138672 13.5335 0.138672 12.9599V8.54012C0.138672 7.96651 0.603681 7.5015 1.1773 7.5015H5.32131C5.89492 7.5015 6.35993 7.9665 6.35993 8.54012V12.9599ZM1.61122 8.98714H4.88742V11.5055H1.61122V8.98714ZM12.8212 13.9985C13.3948 13.9985 13.8598 13.5335 13.8598 12.9599V8.54012C13.8598 7.9665 13.3948 7.5015 12.8212 7.5015H8.67718C8.10356 7.5015 7.63855 7.96651 7.63855 8.54012V12.9599C7.63855 13.5335 8.10356 13.9985 8.67718 13.9985H12.8212ZM12.3873 8.98714H9.11109V11.5055H12.3873V8.98714ZM13.8598 5.45838C13.8598 6.032 13.3948 6.49701 12.8212 6.49701H8.67718C8.10356 6.49701 7.63855 6.032 7.63855 5.45838V1.03863C7.63855 0.465011 8.10356 0 8.67718 0H12.8212C13.3948 0 13.8598 0.465009 13.8598 1.03863V5.45838ZM9.11109 1.48564H12.3873V4.00405H9.11109V1.48564Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187679\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" } ], \"interface_essential\": [ { \"name\": \"add-1\", \"keywords\": [ \"expand\", \"cross\", \"buttons\", \"button\", \"more\", \"remove\", \"plus\", \"add\", \"+\", \"mathematics\", \"math\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186011)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8 1C8 0.447715 7.55228 0 7 0C6.44772 0 6 0.447715 6 1V6H1C0.447715 6 0 6.44772 0 7C0 7.55228 0.447715 8 1 8H6V13C6 13.5523 6.44772 14 7 14C7.55228 14 8 13.5523 8 13V8H13C13.5523 8 14 7.55228 14 7C14 6.44772 13.5523 6 13 6H8V1Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186011\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"add-bell-notification\", \"keywords\": [ \"notification\", \"alarm\", \"alert\", \"bell\", \"add\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186038)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.5 0.75C11.5 0.335786 11.1642 0 10.75 0C10.3358 0 10 0.335786 10 0.75V2.5H8.25C7.83579 2.5 7.5 2.83579 7.5 3.25C7.5 3.66421 7.83579 4 8.25 4H10V5.75C10 6.16421 10.3358 6.5 10.75 6.5C11.1642 6.5 11.5 6.16421 11.5 5.75V4H13.25C13.6642 4 14 3.66421 14 3.25C14 2.83579 13.6642 2.5 13.25 2.5H11.5V0.75ZM5.99999 1.5C6.36863 1.5 6.73325 1.54188 7.08643 1.6231C6.57997 1.98596 6.24997 2.57944 6.24997 3.25C6.24997 4.35457 7.1454 5.25 8.24997 5.25H8.74997V5.75C8.74997 6.79474 9.55103 7.65239 10.5724 7.74223V9.57495C10.5724 9.80868 10.6716 10.0405 10.8609 10.217C11.0797 10.4212 11.2521 10.5 11.5 10.5C11.7761 10.5 12 10.7239 12 11C12 11.2761 11.7761 11.5 11.5 11.5H0.5C0.223858 11.5 0 11.2761 0 11C0 10.7239 0.223858 10.5 0.5 10.5C0.747931 10.5 0.920308 10.4212 1.13911 10.217C1.32834 10.0405 1.42753 9.80868 1.42753 9.57495V5.80013C1.42753 4.64806 1.9183 3.5508 2.7792 2.74747C3.63894 1.94522 4.79803 1.5 5.99999 1.5ZM4.24991 13.25C4.24991 12.8358 4.58569 12.5 4.99991 12.5H6.99991C7.41412 12.5 7.74991 12.8358 7.74991 13.25C7.74991 13.6642 7.41412 14 6.99991 14H4.99991C4.58569 14 4.24991 13.6642 4.24991 13.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186038\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"add-circle\", \"keywords\": [ \"button\", \"remove\", \"cross\", \"add\", \"buttons\", \"plus\", \"circle\", \"+\", \"mathematics\", \"math\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_185993)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7ZM7 3.25C7.41421 3.25 7.75 3.58579 7.75 4V6.25H10C10.4142 6.25 10.75 6.58579 10.75 7C10.75 7.41421 10.4142 7.75 10 7.75H7.75V10C7.75 10.4142 7.41421 10.75 7 10.75C6.58579 10.75 6.25 10.4142 6.25 10V7.75H4C3.58579 7.75 3.25 7.41421 3.25 7C3.25 6.58579 3.58579 6.25 4 6.25H6.25V4C6.25 3.58579 6.58579 3.25 7 3.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_185993\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"add-layer-2\", \"keywords\": [ \"layer\", \"add\", \"design\", \"plus\", \"layers\", \"square\", \"box\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_185996)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.25 0C3.83579 0 3.5 0.335786 3.5 0.75C3.5 1.16421 3.83579 1.5 4.25 1.5H11.75C12.1642 1.5 12.5 1.83579 12.5 2.25V9.75C12.5 10.1642 12.8358 10.5 13.25 10.5C13.6642 10.5 14 10.1642 14 9.75V2.25C14 1.00736 12.9926 0 11.75 0H4.25ZM9.5 3C10.3284 3 11 3.67157 11 4.5V12.5C11 13.3284 10.3284 14 9.5 14H1.5C0.671573 14 0 13.3284 0 12.5V4.5C0 3.67157 0.671573 3 1.5 3H9.5ZM2.25 8.5C2.25 8.08579 2.58579 7.75 3 7.75H4.75V6C4.75 5.58579 5.08579 5.25 5.5 5.25C5.91421 5.25 6.25 5.58579 6.25 6V7.75H8C8.41421 7.75 8.75 8.08579 8.75 8.5C8.75 8.91421 8.41421 9.25 8 9.25H6.25V11C6.25 11.4142 5.91421 11.75 5.5 11.75C5.08579 11.75 4.75 11.4142 4.75 11V9.25H3C2.58579 9.25 2.25 8.91421 2.25 8.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_185996\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"add-square\", \"keywords\": [ \"square\", \"remove\", \"cross\", \"buttons\", \"add\", \"plus\", \"button\", \"+\", \"mathematics\", \"math\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186008)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 3.5C0 1.567 1.567 0 3.5 0H10.5C12.433 0 14 1.567 14 3.5V10.5C14 12.433 12.433 14 10.5 14H3.5C1.567 14 0 12.433 0 10.5V3.5ZM7 3.25C7.41421 3.25 7.75 3.58579 7.75 4V6.25H10C10.4142 6.25 10.75 6.58579 10.75 7C10.75 7.41421 10.4142 7.75 10 7.75H7.75V10C7.75 10.4142 7.41421 10.75 7 10.75C6.58579 10.75 6.25 10.4142 6.25 10V7.75H4C3.58579 7.75 3.25 7.41421 3.25 7C3.25 6.58579 3.58579 6.25 4 6.25H6.25V4C6.25 3.58579 6.58579 3.25 7 3.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186008\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"alarm-clock\", \"keywords\": [ \"time\", \"tock\", \"stopwatch\", \"measure\", \"clock\", \"tick\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186044)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.60263 1.41208C3.96821 1.21734 4.1067 0.763104 3.91195 0.397527C3.7172 0.0319506 3.26297 -0.106534 2.89739 0.0882128C1.86539 0.637971 0.944513 1.37468 0.181627 2.26083C-0.0886191 2.57474 -0.0532215 3.04829 0.26069 3.31853C0.574601 3.58878 1.04815 3.55338 1.3184 3.23947C1.96006 2.49413 2.73462 1.87449 3.60263 1.41208ZM7.00001 14.0001C10.3137 14.0001 13 11.3139 13 8.00015C13 4.68644 10.3137 2.00015 7.00001 2.00015C3.6863 2.00015 1.00001 4.68644 1.00001 8.00015C1.00001 11.3139 3.6863 14.0001 7.00001 14.0001ZM10.0881 0.397527C10.2828 0.0319506 10.7371 -0.106534 11.1026 0.0882128C12.1346 0.637971 13.0555 1.37468 13.8184 2.26083C14.0886 2.57474 14.0532 3.04829 13.7393 3.31853C13.4254 3.58878 12.9519 3.55338 12.6816 3.23947C12.04 2.49413 11.2654 1.87449 10.3974 1.41208C10.0318 1.21734 9.89333 0.763104 10.0881 0.397527ZM7.625 5C7.625 4.65482 7.34518 4.375 7 4.375C6.65482 4.375 6.375 4.65482 6.375 5V8C6.375 8.34518 6.65482 8.625 7 8.625H9.5C9.84518 8.625 10.125 8.34518 10.125 8C10.125 7.65482 9.84518 7.375 9.5 7.375H7.625V5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186044\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"align-back-1\", \"keywords\": [ \"back\", \"design\", \"layer\", \"layers\", \"pile\", \"stack\", \"arrange\", \"square\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186041)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0C0.671573 0 0 0.671573 0 1.5V9.5C0 10.3284 0.671573 11 1.5 11H9.5C10.3284 11 11 10.3284 11 9.5V1.5C11 0.671573 10.3284 0 9.5 0H1.5ZM14 4.25C14 3.83579 13.6642 3.5 13.25 3.5C12.8358 3.5 12.5 3.83579 12.5 4.25V11.75C12.5 12.1642 12.1642 12.5 11.75 12.5H4.25C3.83579 12.5 3.5 12.8358 3.5 13.25C3.5 13.6642 3.83579 14 4.25 14H11.75C12.9926 14 14 12.9926 14 11.75V4.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186041\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"align-center\", \"keywords\": [ \"text\", \"alignment\", \"align\", \"paragraph\", \"centered\", \"formatting\", \"center\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186005)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1C0 0.585786 0.335786 0.25 0.75 0.25H13.25C13.6642 0.25 14 0.585786 14 1C14 1.41421 13.6642 1.75 13.25 1.75H0.75C0.335786 1.75 0 1.41421 0 1ZM1.25 4C1.25 3.58579 1.58579 3.25 2 3.25H12C12.4142 3.25 12.75 3.58579 12.75 4C12.75 4.41421 12.4142 4.75 12 4.75H2C1.58579 4.75 1.25 4.41421 1.25 4ZM3.5 6.25C3.08579 6.25 2.75 6.58579 2.75 7C2.75 7.41421 3.08579 7.75 3.5 7.75H10.5C10.9142 7.75 11.25 7.41421 11.25 7C11.25 6.58579 10.9142 6.25 10.5 6.25H3.5ZM1.25 10C1.25 9.58579 1.58579 9.25 2 9.25H12C12.4142 9.25 12.75 9.58579 12.75 10C12.75 10.4142 12.4142 10.75 12 10.75H2C1.58579 10.75 1.25 10.4142 1.25 10ZM0.75 12.25C0.335786 12.25 0 12.5858 0 13C0 13.4142 0.335786 13.75 0.75 13.75H13.25C13.6642 13.75 14 13.4142 14 13C14 12.5858 13.6642 12.25 13.25 12.25H0.75Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186005\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"align-front-1\", \"keywords\": [ \"design\", \"front\", \"layer\", \"layers\", \"pile\", \"stack\", \"arrange\", \"square\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186002)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.25 0C1.00736 0 0 1.00736 0 2.25V9.75C0 10.1642 0.335786 10.5 0.75 10.5C1.16421 10.5 1.5 10.1642 1.5 9.75V2.25C1.5 1.83579 1.83579 1.5 2.25 1.5H9.75C10.1642 1.5 10.5 1.16421 10.5 0.75C10.5 0.335786 10.1642 0 9.75 0H2.25ZM4.5 3C3.67157 3 3 3.67157 3 4.5V12.5C3 13.3284 3.67157 14 4.5 14H12.5C13.3284 14 14 13.3284 14 12.5V4.5C14 3.67157 13.3284 3 12.5 3H4.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186002\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"align-left\", \"keywords\": [ \"paragraph\", \"text\", \"alignment\", \"align\", \"left\", \"formatting\", \"right\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186020)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1C0 0.585786 0.335786 0.25 0.75 0.25H13.25C13.6642 0.25 14 0.585786 14 1C14 1.41421 13.6642 1.75 13.25 1.75H0.75C0.335786 1.75 0 1.41421 0 1ZM0.75 3.25C0.335786 3.25 0 3.58579 0 4C0 4.41421 0.335786 4.75 0.75 4.75H10.75C11.1642 4.75 11.5 4.41421 11.5 4C11.5 3.58579 11.1642 3.25 10.75 3.25H0.75ZM0 7C0 6.58579 0.335786 6.25 0.75 6.25H7.75C8.16421 6.25 8.5 6.58579 8.5 7C8.5 7.41421 8.16421 7.75 7.75 7.75H0.75C0.335786 7.75 0 7.41421 0 7ZM0 10C0 9.58579 0.335786 9.25 0.75 9.25H10.75C11.1642 9.25 11.5 9.58579 11.5 10C11.5 10.4142 11.1642 10.75 10.75 10.75H0.75C0.335786 10.75 0 10.4142 0 10ZM0.75 12.25C0.335786 12.25 0 12.5858 0 13C0 13.4142 0.335786 13.75 0.75 13.75H13.25C13.6642 13.75 14 13.4142 14 13C14 12.5858 13.6642 12.25 13.25 12.25H0.75Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186020\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"align-right\", \"keywords\": [ \"rag\", \"paragraph\", \"text\", \"alignment\", \"align\", \"right\", \"formatting\", \"left\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186035)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1C0 0.585786 0.335786 0.25 0.75 0.25H13.25C13.6642 0.25 14 0.585786 14 1C14 1.41421 13.6642 1.75 13.25 1.75H0.75C0.335786 1.75 0 1.41421 0 1ZM3.25 3.25C2.83579 3.25 2.5 3.58579 2.5 4C2.5 4.41421 2.83579 4.75 3.25 4.75H13.25C13.6642 4.75 14 4.41421 14 4C14 3.58579 13.6642 3.25 13.25 3.25H3.25ZM5.5 7C5.5 6.58579 5.83579 6.25 6.25 6.25H13.25C13.6642 6.25 14 6.58579 14 7C14 7.41421 13.6642 7.75 13.25 7.75H6.25C5.83579 7.75 5.5 7.41421 5.5 7ZM2.5 10C2.5 9.58579 2.83579 9.25 3.25 9.25H13.25C13.6642 9.25 14 9.58579 14 10C14 10.4142 13.6642 10.75 13.25 10.75H3.25C2.83579 10.75 2.5 10.4142 2.5 10ZM0.75 12.25C0.335786 12.25 0 12.5858 0 13C0 13.4142 0.335786 13.75 0.75 13.75H13.25C13.6642 13.75 14 13.4142 14 13C14 12.5858 13.6642 12.25 13.25 12.25H0.75Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186035\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ampersand\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186017)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.19316 1.75841C3.96022 1.91611 3.69457 2.22763 3.69457 2.99401C3.69457 3.19847 3.80156 3.54957 4.11666 4.06287C4.32602 4.4039 4.60296 4.77799 4.93569 5.17707C4.98917 5.12972 5.06 5.06612 5.14293 4.98951C5.33756 4.80971 5.59458 4.56218 5.84928 4.28748C6.1069 4.00962 6.34657 3.72059 6.5173 3.45721C6.60227 3.32612 6.66143 3.21464 6.69772 3.12498C6.7325 3.03902 6.736 2.99822 6.73634 2.9943C6.73634 2.9941 6.73637 2.99396 6.73634 2.9943C6.73634 2.41455 6.54857 2.06711 6.31985 1.85872C6.0797 1.63993 5.71192 1.5 5.23531 1.5C4.87302 1.5 4.4784 1.5653 4.19316 1.75841ZM5.9418 6.28971C6.00063 6.2375 6.07511 6.17047 6.16078 6.09132C6.37382 5.89451 6.66036 5.61888 6.94923 5.30733C7.23516 4.99894 7.53905 4.63864 7.776 4.27309C7.99465 3.93577 8.23637 3.4756 8.23637 2.9941C8.23637 2.05992 7.91747 1.2851 7.33007 0.749924C6.7541 0.225151 5.99634 0 5.23531 0C4.75163 0 4.00087 0.077164 3.35224 0.51629C2.6513 0.990826 2.19457 1.80131 2.19457 2.99401C2.19457 3.61941 2.47876 4.2619 2.83832 4.84762C3.09784 5.27037 3.4265 5.7107 3.79981 6.15708L1.95366 7.74828C0.518221 8.98549 0.304718 11.1308 1.46816 12.6267C2.75532 14.2816 5.18728 14.47 6.714 13.0331L8.86776 11.006C9.42993 11.4826 9.97035 11.9313 10.4595 12.3359C10.5638 12.4221 10.6655 12.5062 10.7645 12.5881C11.2731 13.0084 11.7096 13.3691 12.0448 13.6564C12.3592 13.926 12.8327 13.8896 13.1023 13.5751C13.3719 13.2606 13.3354 12.7871 13.0209 12.5176C12.6745 12.2206 12.225 11.8492 11.7169 11.4292C11.6185 11.3479 11.518 11.2648 11.4156 11.1801C10.9656 10.8078 10.4756 10.4011 9.9671 9.97135L10.9831 9.01517C11.2847 8.73128 11.2991 8.25663 11.0152 7.955C10.7313 7.65337 10.2566 7.63898 9.95499 7.92287L8.82219 8.98904C7.81257 8.10784 6.80195 7.18043 5.9418 6.28971ZM4.80464 7.27128L2.93296 8.88449C2.10282 9.59999 1.97935 10.8407 2.65219 11.7057C3.39657 12.6628 4.80302 12.7718 5.68595 11.9408L7.7247 10.022C6.71451 9.13697 5.68851 8.19183 4.80464 7.27128Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186017\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"archive-box\", \"keywords\": [ \"box\", \"content\", \"banker\", \"archive\", \"file\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186014)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 2C0 1.17157 0.671573 0.5 1.5 0.5H12.5C13.3284 0.5 14 1.17157 14 2V3.5C14 4.32843 13.3284 5 12.5 5H1.5C0.671573 5 0 4.32843 0 3.5V2ZM13 6.25H1V12C1 12.8284 1.67157 13.5 2.5 13.5H11.5C12.3284 13.5 13 12.8284 13 12V6.25ZM5.5 7.875C5.15482 7.875 4.875 8.15482 4.875 8.5C4.875 8.84518 5.15482 9.125 5.5 9.125H8.5C8.84518 9.125 9.125 8.84518 9.125 8.5C9.125 8.15482 8.84518 7.875 8.5 7.875H5.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186014\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-bend-left-down-2\", \"keywords\": [ \"arrow\", \"bend\", \"curve\", \"change\", \"direction\", \"left\", \"to\", \"down\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.50002 4C6.50002 2.89543 7.39545 2 8.50002 2H11C11.5523 2 12 1.55228 12 1C12 0.447715 11.5523 0 11 0H8.50002C6.29088 0 4.50002 1.79086 4.50002 4V10H2.50002C2.29778 10 2.11547 10.1218 2.03808 10.3087C1.96068 10.4955 2.00346 10.7106 2.14646 10.8536L5.14646 13.8536C5.24023 13.9473 5.36741 14 5.50002 14C5.63262 14 5.7598 13.9473 5.85357 13.8536L8.85357 10.8536C8.99657 10.7106 9.03935 10.4955 8.96196 10.3087C8.88456 10.1218 8.70225 10 8.50002 10H6.50001L6.50002 4Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"arrow-bend-right-down-2\", \"keywords\": [ \"arrow\", \"bend\", \"curve\", \"change\", \"direction\", \"right\", \"to\", \"down\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3 0C2.44772 0 2 0.447715 2 1C2 1.55228 2.44772 2 3 2H5.5C6.60457 2 7.5 2.89543 7.5 4V10H5.49997C5.29774 10 5.11542 10.1218 5.03803 10.3087C4.96064 10.4955 5.00342 10.7106 5.14642 10.8536L8.14642 13.8536C8.24018 13.9473 8.36736 14 8.49997 14C8.63258 14 8.75976 13.9473 8.85352 13.8536L11.8535 10.8536C11.9965 10.7106 12.0393 10.4955 11.9619 10.3087C11.8845 10.1218 11.7022 10 11.5 10H9.5V4C9.5 1.79086 7.70914 0 5.5 0H3Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"arrow-crossover-down\", \"keywords\": [ \"cross\", \"move\", \"over\", \"arrow\", \"arrows\", \"down\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186023)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.70711 0.292893C1.31658 -0.097631 0.683417 -0.097631 0.292893 0.292893C-0.097631 0.683417 -0.097631 1.31658 0.292893 1.70711L10.4393 11.8536L9.14645 13.1464C9.00345 13.2894 8.96067 13.5045 9.03806 13.6913C9.11545 13.8782 9.29777 14 9.5 14H12.9993H13.0007H13.5C13.7761 14 14 13.7761 14 13.5V13.0007V12.9993V9.5C14 9.29777 13.8782 9.11545 13.6913 9.03806C13.5045 8.96067 13.2894 9.00345 13.1464 9.14645L11.8536 10.4393L1.70711 0.292893ZM0.308658 9.03806C0.495495 8.96067 0.710554 9.00345 0.853553 9.14645L2.14645 10.4393L3.79289 8.79289C4.18342 8.40237 4.81658 8.40237 5.20711 8.79289C5.59763 9.18342 5.59763 9.81658 5.20711 10.2071L3.56066 11.8536L4.85355 13.1464C4.99655 13.2894 5.03933 13.5045 4.96194 13.6913C4.88455 13.8782 4.70223 14 4.5 14H1.00069H0.999307H0.5C0.223858 14 7.45058e-08 13.7761 7.45058e-08 13.5V13.0002V9.5C7.45058e-08 9.29777 0.121821 9.11545 0.308658 9.03806ZM13.7071 1.70711C14.0976 1.31658 14.0976 0.683417 13.7071 0.292893C13.3166 -0.097631 12.6834 -0.097631 12.2929 0.292893L8.79289 3.79289C8.40237 4.18342 8.40237 4.81658 8.79289 5.20711C9.18342 5.59763 9.81658 5.59763 10.2071 5.20711L13.7071 1.70711Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186023\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-crossover-left\", \"keywords\": [ \"cross\", \"move\", \"over\", \"arrow\", \"arrows\", \"left\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186050)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.7071 1.70711L3.56066 11.8536L4.85355 13.1464C4.99655 13.2894 5.03933 13.5045 4.96194 13.6913C4.88455 13.8782 4.70223 14 4.5 14H1.00069H0.999307H0.5C0.223858 14 0 13.7761 0 13.5V13.0002V9.5C0 9.29777 0.121821 9.11545 0.308658 9.03806C0.495495 8.96067 0.710554 9.00345 0.853553 9.14645L2.14645 10.4393L12.2929 0.292893C12.6834 -0.097631 13.3166 -0.097631 13.7071 0.292893C14.0976 0.683417 14.0976 1.31658 13.7071 1.70711ZM8.79289 8.79289C9.18342 8.40237 9.81658 8.40237 10.2071 8.79289L13.7071 12.2929C14.0976 12.6834 14.0976 13.3166 13.7071 13.7071C13.3166 14.0976 12.6834 14.0976 12.2929 13.7071L8.79289 10.2071C8.40237 9.81658 8.40237 9.18342 8.79289 8.79289ZM0 0.999827V4.5C0 4.70223 0.121821 4.88455 0.308658 4.96194C0.495495 5.03933 0.710554 4.99655 0.853553 4.85355L2.14645 3.56066L3.79289 5.20711C4.18342 5.59763 4.81658 5.59763 5.20711 5.20711C5.59763 4.81658 5.59763 4.18342 5.20711 3.79289L3.56066 2.14645L4.85355 0.853553C4.99655 0.710554 5.03933 0.495495 4.96194 0.308658C4.88455 0.121821 4.70223 7.45058e-08 4.5 7.45058e-08H1.00017H0.5C0.223858 7.45058e-08 0 0.223858 0 0.5V0.999827Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186050\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-crossover-right\", \"keywords\": [ \"cross\", \"move\", \"over\", \"arrow\", \"arrows\", \"ight\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186071)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.9998 7.45058e-08H9.5C9.29777 7.45058e-08 9.11545 0.121821 9.03806 0.308658C8.96067 0.495495 9.00345 0.710554 9.14645 0.853553L10.4393 2.14645L0.292893 12.2929C-0.097631 12.6834 -0.097631 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L11.8536 3.56066L13.1464 4.85355C13.2894 4.99655 13.5045 5.03933 13.6913 4.96194C13.8782 4.88455 14 4.70223 14 4.5V1.00069V0.999307V0.5C14 0.223858 13.7761 7.45058e-08 13.5 7.45058e-08H12.9998ZM0.292893 0.292893C0.683417 -0.097631 1.31658 -0.097631 1.70711 0.292893L5.20711 3.79289C5.59763 4.18342 5.59763 4.81658 5.20711 5.20711C4.81658 5.59763 4.18342 5.59763 3.79289 5.20711L0.292893 1.70711C-0.097631 1.31658 -0.097631 0.683417 0.292893 0.292893ZM14 9.5C14 9.29777 13.8782 9.11545 13.6913 9.03806C13.5045 8.96067 13.2894 9.00345 13.1464 9.14645L11.8536 10.4393L10.2071 8.79289C9.81658 8.40237 9.18342 8.40237 8.79289 8.79289C8.40237 9.18342 8.40237 9.81658 8.79289 10.2071L10.4393 11.8536L9.14645 13.1464C9.00345 13.2894 8.96067 13.5045 9.03806 13.6913C9.11545 13.8782 9.29777 14 9.5 14H12.9993C12.9998 14 13.0002 14 13.0007 14H13.5C13.7761 14 14 13.7761 14 13.5V13.0007C14 13.0002 14 12.9998 14 12.9993V9.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186071\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-crossover-up\", \"keywords\": [ \"cross\", \"move\", \"over\", \"arrow\", \"arrows\", \"right\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186047)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.5 0H13.0002H9.5C9.29777 0 9.11545 0.121821 9.03806 0.308658C8.96067 0.495495 9.00345 0.710554 9.14645 0.853553L10.4393 2.14645L8.79289 3.79289C8.40237 4.18342 8.40237 4.81658 8.79289 5.20711C9.18342 5.59763 9.81658 5.59763 10.2071 5.20711L11.8536 3.56066L13.1464 4.85355C13.2894 4.99655 13.5045 5.03933 13.6913 4.96194C13.8782 4.88455 14 4.70223 14 4.5V1.00069V0.999307V0.5C14 0.223858 13.7761 0 13.5 0ZM7.45058e-08 0.999827V0.5C7.45058e-08 0.223858 0.223858 0 0.5 0H0.999827H4.5C4.70223 0 4.88455 0.121821 4.96194 0.308658C5.03933 0.495495 4.99655 0.710554 4.85355 0.853553L3.56066 2.14645L13.7071 12.2929C14.0976 12.6834 14.0976 13.3166 13.7071 13.7071C13.3166 14.0976 12.6834 14.0976 12.2929 13.7071L2.14645 3.56066L0.853553 4.85355C0.710554 4.99655 0.495495 5.03933 0.308658 4.96194C0.121821 4.88455 7.45058e-08 4.70223 7.45058e-08 4.5V0.999827ZM5.20711 10.2071C5.59763 9.81658 5.59763 9.18342 5.20711 8.79289C4.81658 8.40237 4.18342 8.40237 3.79289 8.79289L0.292893 12.2929C-0.097631 12.6834 -0.097631 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L5.20711 10.2071Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186047\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-cursor-1\", \"keywords\": [ \"mouse\", \"select\", \"cursor\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186056)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.85846 0.0434611C1.60784 -0.018217 1.34555 -0.0141858 1.09695 0.0551647C0.848344 0.124515 0.621841 0.256839 0.43934 0.43934C0.256839 0.621841 0.124515 0.848344 0.0551647 1.09695C-0.0141858 1.34555 -0.018217 1.60784 0.0434611 1.85846L2.3336 11.119L2.33408 11.121C2.39894 11.3787 2.53113 11.6147 2.71713 11.8046C2.90312 11.9945 3.13624 12.1315 3.39262 12.2017C3.649 12.2719 3.91943 12.2727 4.17622 12.2041C4.43258 12.1355 4.66615 12.0002 4.85312 11.8119L4.85408 11.811L6.49897 10.1661L10.1454 13.8125C10.3398 14.0069 10.6545 14.0079 10.8501 13.8149L13.8501 10.8549C13.945 10.7613 13.9985 10.6338 13.999 10.5007C13.9994 10.3675 13.9467 10.2396 13.8525 10.1454L10.2061 6.49897L11.8525 4.85253L11.8547 4.85029C12.0435 4.6591 12.1775 4.42072 12.2427 4.16004C12.3078 3.89937 12.3017 3.62599 12.2251 3.36846C12.1485 3.11093 12.004 2.87872 11.807 2.6961C11.6118 2.5153 11.3722 2.38967 11.1126 2.33202L1.85846 0.0434611Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186056\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-cursor-2\", \"keywords\": [ \"mouse\", \"select\", \"cursor\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186029)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.97154 0.0802919L1.97032 0.0798786C1.70707 -0.00998428 1.4239 -0.0243478 1.1529 0.0384197C0.881573 0.101263 0.633282 0.238914 0.436268 0.435778C0.23925 0.632646 0.101414 0.880835 0.038477 1.15215C-0.0244605 1.42348 -0.00996201 1.707 0.0803175 1.97049L0.553324 1.80842C0.0804651 1.97092 0.0802886 1.9704 0.0803175 1.97049L0.0811564 1.97293L0.0832164 1.97893L0.0913559 2.00261L0.12311 2.09502L0.243677 2.44588L0.674363 3.69936C1.03099 4.73738 1.50698 6.12295 1.98442 7.51322C2.46186 8.90349 2.94072 10.2984 3.30312 11.3551C3.48433 11.8835 3.63638 12.3272 3.74456 12.6434L3.87195 13.0165L3.90724 13.1204L3.91705 13.1496C3.91853 13.1541 3.91881 13.1549 3.91834 13.1534C3.9831 13.3631 4.07909 13.5466 4.24161 13.709C4.42815 13.8954 4.68107 14 4.94471 14C5.20835 14 5.46127 13.8954 5.64781 13.709C5.7197 13.6371 5.78447 13.5591 5.84039 13.4493C5.88473 13.3622 5.91849 13.263 5.94967 13.1714L5.95118 13.167C5.98533 13.0667 6.46934 11.6707 6.94606 10.2962L7.59645 8.42126L7.80923 7.80795L7.81083 7.80333L7.81765 7.80092L8.43521 7.58262C8.94695 7.40167 9.63092 7.15973 10.3198 6.91575C11.682 6.43327 13.103 5.92902 13.1979 5.88948C13.2107 5.88413 13.2262 5.87814 13.2438 5.87133C13.3548 5.82839 13.5509 5.75251 13.7085 5.59502C13.8951 5.40861 14 5.1557 14 4.89191C14 4.62813 13.8951 4.37522 13.7085 4.18881L13.7009 4.18084C13.6757 4.15424 13.601 4.07536 13.5192 4.01738C13.4093 3.93952 13.2872 3.8897 13.1462 3.84833C13.1469 3.84854 13.1463 3.84836 13.1441 3.84764L13.109 3.83627L13.0001 3.80031C12.908 3.76972 12.7791 3.72666 12.6184 3.67283C12.2972 3.56521 11.8505 3.415 11.3204 3.23653C10.2604 2.8796 8.86814 2.40987 7.48276 1.94216C6.09742 1.47446 4.71911 1.00882 3.68713 0.660108L2.44134 0.239093L2.09279 0.121279L2.00102 0.0902556L1.97749 0.0823041L1.97154 0.0802919Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186029\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-curvy-up-down-1\", \"keywords\": [ \"both\", \"direction\", \"arrow\", \"curvy\", \"diagram\", \"zigzag\", \"vertical\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186074)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.0576 0.227196C10.2135 0.0990563 10.4088 0.0171427 10.6226 0.00242137C10.6615 -0.000295498 10.7005 -0.00071814 10.7393 0.00111357C10.9526 0.010982 11.1485 0.0877073 11.3065 0.210821C11.3389 0.236024 11.3699 0.263405 11.3994 0.292922L13.7071 2.60062C13.9931 2.88661 14.0787 3.31673 13.9239 3.69041C13.7691 4.06408 13.4045 4.30772 13 4.30772H11.6923V11.1538C11.6923 11.9087 11.3924 12.6326 10.8587 13.1664C10.3249 13.7001 9.60102 14 8.84617 14C8.09132 14 7.36739 13.7001 6.83363 13.1664C6.29988 12.6326 6.00002 11.9087 6.00002 11.1538V2.84618C6.00002 2.62177 5.91087 2.40655 5.75218 2.24786C5.5935 2.08918 5.37828 2.00003 5.15386 2.00003C4.92945 2.00003 4.71423 2.08918 4.55554 2.24786C4.39686 2.40655 4.30771 2.62177 4.30771 2.84618V10.687L4.30772 10.6924V13.0001C4.30772 13.2208 4.23514 13.4297 4.10846 13.5991C3.95256 13.8071 3.71748 13.9524 3.44792 13.9902C3.36554 14.0019 3.28252 14.0032 3.20078 13.9944C3.03355 13.9766 2.87863 13.9176 2.74638 13.8277C2.69471 13.7927 2.64586 13.7524 2.60062 13.7072L0.292922 11.3995C0.00692439 11.1135 -0.0786313 10.6834 0.0761497 10.3097C0.23093 9.93605 0.595567 9.69241 1.00003 9.69241H2.30771V2.84618C2.30771 2.09134 2.60757 1.36741 3.14133 0.833648C3.67508 0.299891 4.39901 2.92152e-05 5.15386 2.92152e-05C5.90871 2.92152e-05 6.63264 0.299891 7.1664 0.833648C7.70015 1.36741 8.00002 2.09134 8.00002 2.84618V11.1538C8.00002 11.3783 8.08916 11.5935 8.24785 11.7522C8.40653 11.9109 8.62176 12 8.84617 12C9.07058 12 9.2858 11.9109 9.44449 11.7522C9.60318 11.5935 9.69232 11.3783 9.69232 11.1538V1.00003C9.69232 0.733268 9.79678 0.490903 9.96701 0.311604C9.99536 0.281718 10.0256 0.253515 10.0576 0.227196Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186074\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-curvy-up-down-2\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186032)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.80774 1.51514C4.01107 1.51514 3.36525 2.16096 3.36525 2.95763V8.57001H4.61525C4.81748 8.57001 4.9998 8.69183 5.07719 8.87867C5.15458 9.0655 5.1118 9.28056 4.9688 9.42356L3.36525 11.0271V11.0422H3.3502L2.9688 11.4236C2.87504 11.5173 2.74786 11.57 2.61525 11.57C2.48264 11.57 2.35547 11.5173 2.2617 11.4236L1.8803 11.0422H1.86525V11.0271L0.261696 9.42356C0.118697 9.28056 0.0759187 9.0655 0.153309 8.87867C0.2307 8.69183 0.413018 8.57001 0.615249 8.57001H1.86525V2.95763C1.86525 1.33254 3.18265 0.0151367 4.80774 0.0151367C6.43283 0.0151367 7.75023 1.33254 7.75023 2.95763L7.7502 11.0422C7.7502 11.8388 8.39602 12.4846 9.19269 12.4846C9.98935 12.4846 10.6351 11.8388 10.6351 11.0422V5.36762H9.38521C9.18298 5.36762 9.00066 5.2458 8.92327 5.05896C8.84588 4.87212 8.88866 4.65706 9.03165 4.51406L11.0316 2.51406C11.1254 2.42029 11.2526 2.36762 11.3852 2.36762C11.5178 2.36762 11.645 2.42029 11.7387 2.51406L13.7387 4.51406C13.8817 4.65706 13.9245 4.87212 13.8471 5.05896C13.7697 5.2458 13.5874 5.36762 13.3852 5.36762H12.1351V11.0422C12.1351 12.6672 10.8178 13.9846 9.19269 13.9846C7.5676 13.9846 6.2502 12.6672 6.2502 11.0422L6.25023 2.95763C6.25023 2.16096 5.6044 1.51514 4.80774 1.51514Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186032\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-down-2\", \"keywords\": [ \"down\", \"move\", \"arrow\", \"arrows\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8 1C8 0.447715 7.55228 0 7 0C6.44772 0 6 0.447715 6 1V6H4C3.79777 6 3.61545 6.12182 3.53806 6.30866C3.46067 6.4955 3.50345 6.71055 3.64645 6.85355L6.64645 9.85355C6.84171 10.0488 7.15829 10.0488 7.35355 9.85355L10.3536 6.85355C10.4966 6.71055 10.5393 6.4955 10.4619 6.30866C10.3845 6.12182 10.2022 6 10 6H8V1ZM4 12C3.44772 12 3 12.4477 3 13C3 13.5523 3.44772 14 4 14H10C10.5523 14 11 13.5523 11 13C11 12.4477 10.5523 12 10 12H4Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"arrow-down-dashed-square\", \"keywords\": [ \"arrow\", \"keyboard\", \"button\", \"down\", \"square\", \"dashes\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186062)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.96154 12.5H6.03847C5.62425 12.5 5.28847 12.8358 5.28847 13.25C5.28847 13.6642 5.62425 14 6.03847 14H7.96154C8.37576 14 8.71154 13.6642 8.71154 13.25C8.71154 12.8358 8.37576 12.5 7.96154 12.5ZM5.28847 0.75C5.28847 1.16421 5.62425 1.5 6.03847 1.5H7.96154C8.37576 1.5 8.71154 1.16421 8.71154 0.75C8.71154 0.335787 8.37576 0 7.96154 0H6.03847C5.62425 0 5.28847 0.335787 5.28847 0.75ZM10.9316 12.4243C10.5321 12.5338 10.297 12.9464 10.4065 13.3459C10.5159 13.7454 10.9285 13.9805 11.328 13.871C12.5224 13.5436 13.4696 12.6244 13.8362 11.4473C13.9594 11.0518 13.7387 10.6314 13.3432 10.5082C12.9477 10.385 12.5273 10.6057 12.4041 11.0012C12.1891 11.6915 11.6313 12.2326 10.9316 12.4243ZM3.06865 12.4244C3.46813 12.5339 3.70323 12.9464 3.59375 13.3459C3.48428 13.7454 3.07168 13.9805 2.6722 13.871C1.47776 13.5437 0.530616 12.6244 0.163982 11.4473C0.0408022 11.0519 0.26154 10.6314 0.657014 10.5082C1.05249 10.3851 1.47294 10.6058 1.59612 11.0013C1.81113 11.6916 2.36895 12.2326 3.06865 12.4244ZM1.5 7.96155C1.5 8.37576 1.16421 8.71155 0.75 8.71155C0.335786 8.71155 -1.56e-07 8.37576 0 7.96155L5.85002e-07 6.03847C7.40002e-07 5.62426 0.335787 5.28847 0.75 5.28847C1.16421 5.28847 1.5 5.62426 1.5 6.03847V7.96155ZM13.2502 8.71155C12.836 8.71155 12.5002 8.37576 12.5002 7.96155V6.03847C12.5002 5.62426 12.836 5.28847 13.2502 5.28847C13.6644 5.28847 14.0002 5.62426 14.0002 6.03847V7.96155C14.0002 8.37576 13.6644 8.71155 13.2502 8.71155ZM1.59612 2.99867C1.47294 3.39415 1.05249 3.61488 0.657014 3.4917C0.26154 3.36852 0.0408021 2.94807 0.163982 2.5526C0.530615 1.37551 1.47776 0.456232 2.6722 0.128903C3.07168 0.0194265 3.48428 0.254524 3.59375 0.654009C3.70323 1.05349 3.46813 1.46609 3.06865 1.57556C2.36895 1.76731 1.81113 2.30835 1.59612 2.99867ZM13.3432 3.4917C12.9477 3.61488 12.5273 3.39415 12.4041 2.99867C12.1891 2.30835 11.6313 1.76731 10.9316 1.57556C10.5321 1.46609 10.297 1.05349 10.4065 0.654009C10.5159 0.254524 10.9285 0.0194265 11.328 0.128903C12.5224 0.456232 13.4696 1.37551 13.8362 2.5526C13.9594 2.94807 13.7387 3.36852 13.3432 3.4917ZM7.75012 4C7.75012 3.58579 7.41433 3.25 7.00012 3.25C6.58591 3.25 6.25012 3.58579 6.25012 4V7.5H5.00012C4.79789 7.5 4.61557 7.62182 4.53818 7.80866C4.46079 7.9955 4.50357 8.21055 4.64657 8.35355L6.64657 10.3536C6.74033 10.4473 6.86751 10.5 7.00012 10.5C7.13273 10.5 7.2599 10.4473 7.35367 10.3536L9.35367 8.35355C9.49667 8.21056 9.53945 7.9955 9.46206 7.80866C9.38467 7.62182 9.20235 7.5 9.00012 7.5H7.75012V4Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186062\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-expand\", \"keywords\": [ \"expand\", \"small\", \"bigger\", \"retract\", \"smaller\", \"big\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186068)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 0.999827V4.5C0 4.70223 0.121821 4.88455 0.308658 4.96194C0.495495 5.03933 0.710554 4.99655 0.853553 4.85355L2.14645 3.56066L4.29289 5.70711C4.68342 6.09763 5.31658 6.09763 5.70711 5.70711C6.09763 5.31658 6.09763 4.68342 5.70711 4.29289L3.56066 2.14645L4.85355 0.853553C4.99655 0.710554 5.03933 0.495495 4.96194 0.308658C4.88455 0.121821 4.70223 0 4.5 0H1.00017H0.5C0.223858 0 0 0.223858 0 0.5V0.999827ZM5.70711 9.70711C6.09763 9.31658 6.09763 8.68342 5.70711 8.29289C5.31658 7.90237 4.68342 7.90237 4.29289 8.29289L2.14645 10.4393L0.853553 9.14645C0.710554 9.00345 0.495495 8.96067 0.308658 9.03806C0.121821 9.11545 0 9.29777 0 9.5V12.9998V13.5C0 13.7761 0.223858 14 0.5 14H0.999307H1.00069H4.5C4.70223 14 4.88455 13.8782 4.96194 13.6913C5.03933 13.5045 4.99655 13.2894 4.85355 13.1464L3.56066 11.8536L5.70711 9.70711ZM8.29289 8.29289C8.68342 7.90237 9.31658 7.90237 9.70711 8.29289L11.8536 10.4393L13.1464 9.14645C13.2894 9.00345 13.5045 8.96067 13.6913 9.03806C13.8782 9.11545 14 9.29777 14 9.5V12.9993V13.0007V13.5C14 13.7761 13.7761 14 13.5 14H13.0007H12.9993H9.5C9.29777 14 9.11545 13.8782 9.03806 13.6913C8.96067 13.5045 9.00345 13.2894 9.14645 13.1464L10.4393 11.8536L8.29289 9.70711C7.90237 9.31658 7.90237 8.68342 8.29289 8.29289ZM9.5 0C9.29777 0 9.11545 0.121821 9.03806 0.308658C8.96067 0.495495 9.00345 0.710554 9.14645 0.853553L10.4393 2.14645L8.29289 4.29289C7.90237 4.68342 7.90237 5.31658 8.29289 5.70711C8.68342 6.09763 9.31658 6.09763 9.70711 5.70711L11.8536 3.56066L13.1464 4.85355C13.2894 4.99655 13.5045 5.03933 13.6913 4.96194C13.8782 4.88455 14 4.70223 14 4.5V1.00069V0.999307V0.5C14 0.223858 13.7761 0 13.5 0H13.0002H9.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186068\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-infinite-loop\", \"keywords\": [ \"arrow\", \"diagram\", \"loop\", \"infinity\", \"repeat\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.1089 3.15529C11.1089 2.85195 10.9261 2.57847 10.6459 2.46238C10.3656 2.3463 10.043 2.41046 9.82855 2.62496L7.91542 4.53809C7.78516 4.66537 7.70107 4.83969 7.69063 5.03359C7.68852 5.07188 7.68934 5.11043 7.69316 5.14889C7.70525 5.27175 7.74697 5.3859 7.8111 5.4841C7.92395 5.65693 8.10619 5.78035 8.31827 5.81482C8.35793 5.82131 8.39845 5.82466 8.43951 5.82466H10.3556L10.3733 5.82508C10.3922 5.82558 10.4206 5.82644 10.4561 5.82787C10.5275 5.83074 10.6258 5.83581 10.7334 5.84449C10.9764 5.86411 11.1709 5.89552 11.2496 5.92376C11.5117 6.01793 11.7497 6.16889 11.9467 6.36583C12.1436 6.56278 12.2946 6.80083 12.3888 7.06295C12.4829 7.32507 12.518 7.60477 12.4914 7.88202C12.4868 7.92965 12.4868 7.97761 12.4914 8.02523C12.518 8.30248 12.4829 8.58218 12.3888 8.8443C12.2946 9.10642 12.1436 9.34448 11.9467 9.54142C11.7497 9.73836 11.5117 9.88932 11.2496 9.98349C10.9874 10.0776 10.7077 10.1127 10.4305 10.0861C10.4067 10.0838 10.3828 10.0827 10.3589 10.0827C9.98941 10.0827 9.58499 9.88362 9.09364 9.399C8.60278 8.91486 8.13224 8.25454 7.61202 7.52011L7.58952 7.48835C7.09609 6.7917 6.5544 6.02688 5.95968 5.4403C5.36417 4.85296 4.61091 4.33803 3.6778 4.32484C3.19186 4.28329 2.70254 4.34711 2.2433 4.51209C1.77305 4.68103 1.34597 4.95185 0.992654 5.30517C0.639333 5.65849 0.368509 6.08557 0.199575 6.55582C0.0387444 7.0035 -0.0259484 7.47978 0.00938044 7.95363C-0.0259484 8.42748 0.0387444 8.90375 0.199575 9.35144C0.368509 9.82169 0.639333 10.2487 0.992654 10.6021C1.34597 10.9554 1.77305 11.2262 2.2433 11.3951C2.70446 11.5608 3.19596 11.6245 3.6839 11.5819C4.37657 11.5628 5.04204 11.3064 5.56866 10.8551C5.88318 10.5856 5.91966 10.1121 5.65013 9.79763C5.38061 9.48311 4.90714 9.44663 4.59261 9.71616C4.32341 9.94684 3.98223 10.0765 3.62777 10.0828C3.60831 10.0831 3.58887 10.0842 3.5695 10.0861C3.29225 10.1127 3.01255 10.0776 2.75043 9.98349C2.48831 9.88932 2.25026 9.73836 2.05331 9.54142C1.85637 9.34448 1.70541 9.10642 1.61124 8.8443C1.51708 8.58218 1.48204 8.30248 1.50863 8.02523C1.5132 7.97761 1.5132 7.92965 1.50863 7.88202C1.48204 7.60477 1.51708 7.32507 1.61124 7.06295C1.70541 6.80083 1.85637 6.56278 2.05331 6.36583C2.25026 6.16889 2.48831 6.01793 2.75043 5.92376C3.01255 5.8296 3.29225 5.79456 3.5695 5.82115C3.5933 5.82343 3.6172 5.82458 3.64111 5.82458C4.01059 5.82458 4.41501 6.02363 4.90636 6.50825C5.39722 6.99239 5.86776 7.65271 6.38798 8.38714L6.41048 8.41891C6.90391 9.11556 7.4456 9.88037 8.04032 10.4669C8.63583 11.0543 9.38909 11.5692 10.3222 11.5824C10.8081 11.6239 11.2975 11.5601 11.7567 11.3951C12.227 11.2262 12.654 10.9554 13.0073 10.6021C13.3607 10.2487 13.6315 9.82169 13.8004 9.35144C13.9613 8.90375 14.0259 8.42748 13.9906 7.95363C14.0259 7.47978 13.9613 7.0035 13.8004 6.55582C13.6315 6.08557 13.3607 5.65849 13.0073 5.30517C12.654 4.95185 12.227 4.68103 11.7567 4.51209C11.5584 4.44086 11.3173 4.3996 11.1089 4.37456V3.15529Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"arrow-move\", \"keywords\": [ \"move\", \"button\", \"arrows\", \"direction\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186065)\\\">\\n<path d=\\\"M13.7609 7.54873C13.901 7.40849 13.9796 7.21839 13.9796 7.02019C13.9796 6.82199 13.901 6.63189 13.7609 6.49165L11.7664 4.49716C11.6648 4.40022 11.538 4.33385 11.4004 4.30563C11.2628 4.27741 11.1201 4.28849 10.9886 4.3376C10.8447 4.38692 10.7195 4.4795 10.6303 4.60268C10.541 4.72585 10.492 4.8736 10.4899 5.0257V6.02294C10.4899 6.08907 10.4637 6.15248 10.4169 6.19923C10.3702 6.24599 10.3068 6.27226 10.2406 6.27226H7.99683C7.93071 6.27226 7.86729 6.24599 7.82054 6.19923C7.77378 6.15248 7.74752 6.08907 7.74752 6.02294V3.77914C7.74752 3.71302 7.77378 3.64961 7.82054 3.60285C7.86729 3.5561 7.93071 3.52983 7.99683 3.52983H8.99407C9.14617 3.52777 9.29392 3.47876 9.41709 3.3895C9.54027 3.30025 9.63285 3.17511 9.68217 3.03121C9.73968 2.89445 9.75521 2.74368 9.7268 2.59806C9.69839 2.45245 9.62731 2.31858 9.52261 2.21347L7.52812 0.21898C7.38789 0.0789163 7.19779 0.000244141 6.99958 0.000244141C6.80138 0.000244141 6.61128 0.0789163 6.47104 0.21898L4.47655 2.21347C4.37185 2.31858 4.30078 2.45245 4.27237 2.59806C4.24396 2.74368 4.25949 2.89445 4.31699 3.03121C4.36632 3.17511 4.4589 3.30025 4.58207 3.3895C4.70525 3.47876 4.85299 3.52777 5.00509 3.52983H6.00234C6.06846 3.52983 6.13187 3.5561 6.17863 3.60285C6.22538 3.64961 6.25165 3.71302 6.25165 3.77914V6.02294C6.25165 6.08907 6.22538 6.15248 6.17863 6.19923C6.13187 6.24599 6.06846 6.27226 6.00234 6.27226H3.75854C3.69242 6.27226 3.629 6.24599 3.58225 6.19923C3.53549 6.15248 3.50923 6.08907 3.50923 6.02294V5.0257C3.5091 4.87787 3.46516 4.73339 3.38297 4.61051C3.30077 4.48764 3.18401 4.39187 3.04743 4.33532C2.91084 4.27876 2.76056 4.26395 2.61556 4.29276C2.47056 4.32156 2.33735 4.39269 2.23275 4.49716L0.238267 6.49165C0.0982034 6.63189 0.0195312 6.82199 0.0195312 7.02019C0.0195312 7.21839 0.0982034 7.40849 0.238267 7.54873L2.23275 9.54322C2.37408 9.68172 2.56343 9.76031 2.76129 9.76261C2.84722 9.75717 2.93156 9.73692 3.0106 9.70278C3.1545 9.65346 3.27964 9.56087 3.3689 9.4377C3.45815 9.31453 3.50717 9.16678 3.50923 9.01468V8.01743C3.50923 7.95131 3.53549 7.8879 3.58225 7.84114C3.629 7.79439 3.69242 7.76812 3.75854 7.76812H6.00234C6.06846 7.76812 6.13187 7.79439 6.17863 7.84114C6.22538 7.8879 6.25165 7.95131 6.25165 8.01743V10.2612C6.25165 10.3273 6.22538 10.3907 6.17863 10.4375C6.13187 10.4842 6.06846 10.5105 6.00234 10.5105H5.00509C4.8577 10.5095 4.71339 10.5527 4.59075 10.6344C4.46811 10.7162 4.37276 10.8328 4.31699 10.9692C4.25949 11.106 4.24396 11.2568 4.27237 11.4024C4.30078 11.548 4.37185 11.6819 4.47655 11.787L6.47104 13.7815C6.61128 13.9215 6.80138 14.0002 6.99958 14.0002C7.19779 14.0002 7.38789 13.9215 7.52812 13.7815L9.52261 11.787C9.61955 11.6854 9.68592 11.5585 9.71414 11.421C9.74236 11.2834 9.73128 11.1407 9.68217 11.0091C9.63285 10.8652 9.54027 10.7401 9.41709 10.6508C9.29392 10.5616 9.14617 10.5126 8.99407 10.5105H7.99683C7.93071 10.5105 7.86729 10.4842 7.82054 10.4375C7.77378 10.3907 7.74752 10.3273 7.74752 10.2612V8.01743C7.74752 7.95131 7.77378 7.8879 7.82054 7.84114C7.86729 7.79439 7.93071 7.76812 7.99683 7.76812H10.2406C10.3068 7.76812 10.3702 7.79439 10.4169 7.84114C10.4637 7.8879 10.4899 7.95131 10.4899 8.01743V9.01468C10.4889 9.16207 10.5321 9.30638 10.6139 9.42902C10.6956 9.55166 10.8122 9.64701 10.9487 9.70278C11.0401 9.74174 11.1385 9.76209 11.2379 9.76261C11.4357 9.76031 11.6251 9.68172 11.7664 9.54322L13.7609 7.54873Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186065\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-reload-horizontal-1\", \"keywords\": [ \"arrows\", \"load\", \"arrow\", \"sync\", \"square\", \"loading\", \"reload\", \"synchronize\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186083)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.35355 0.146462C9.21055 0.0034622 8.9955 -0.0393156 8.80866 0.0380748C8.62182 0.115466 8.5 0.297784 8.5 0.500015V2.00001H2.5C1.11929 2.00001 0 3.1193 0 4.50001V6.00001C0 6.55229 0.447715 7.00001 1 7.00001C1.55228 7.00001 2 6.55229 2 6.00001V4.50001C2 4.22387 2.22386 4.00001 2.5 4.00001H8.5V5.50001C8.5 5.70224 8.62182 5.88456 8.80866 5.96195C8.9955 6.03934 9.21055 5.99656 9.35355 5.85356L11.8536 3.35356C12.0488 3.1583 12.0488 2.84172 11.8536 2.64646L9.35355 0.146462ZM5.19134 8.03807C5.37818 8.11546 5.5 8.29778 5.5 8.50001V10H11.5C11.7761 10 12 9.77615 12 9.50001V8.00001C12 7.44773 12.4477 7.00001 13 7.00001C13.5523 7.00001 14 7.44773 14 8.00001V9.50001C14 10.8807 12.8807 12 11.5 12H5.5V13.5C5.5 13.7022 5.37818 13.8845 5.19134 13.9619C5.0045 14.0393 4.78945 13.9966 4.64645 13.8536L2.14645 11.3536C1.95118 11.1583 1.95118 10.8417 2.14645 10.6464L4.64645 8.14646C4.78945 8.00346 5.0045 7.96068 5.19134 8.03807Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186083\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-reload-horizontal-2\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186080)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4 5.54285V4.24995H3C2.44771 4.24995 2 4.69767 2 5.24995V8.74995C2 9.30224 2.44772 9.74995 3 9.74995H4C4.55228 9.74995 5 10.1976 5 10.7499C5 11.3022 4.55228 11.7499 4 11.7499H3C1.34315 11.7499 2.25414e-07 10.4068 1.5299e-07 8.74995L0 5.24995C-7.2423e-08 3.5931 1.34314 2.24995 3 2.24995H4V0.957058C4 0.511606 4.53857 0.288521 4.85355 0.603503L7.14645 2.8964C7.34171 3.09166 7.34171 3.40824 7.14645 3.60351L4.85355 5.8964C4.53857 6.21138 4 5.9883 4 5.54285ZM10 11.7499V13.0428C10 13.4882 9.46143 13.7113 9.14645 13.3964L6.85355 11.1035C6.65829 10.9082 6.65829 10.5916 6.85355 10.3963L9.14645 8.10351C9.46143 7.78852 10 8.01161 10 8.45706V9.74995H11C11.5523 9.74995 12 9.30224 12 8.74995V5.24995C12 4.69767 11.5523 4.24995 11 4.24995H10C9.44771 4.24995 9 3.80224 9 3.24995C9 2.69767 9.44771 2.24995 10 2.24995H11C12.6569 2.24995 14 3.5931 14 5.24995V8.74995C14 10.4068 12.6569 11.7499 11 11.7499H10Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186080\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-reload-vertical-1\", \"keywords\": [ \"arrows\", \"load\", \"arrow\", \"sync\", \"square\", \"loading\", \"reload\", \"synchronize\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186113)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.00001 0C7.44773 0 7.00001 0.447715 7.00001 1C7.00001 1.55228 7.44773 2 8.00001 2H9.50001C9.77615 2 10 2.22386 10 2.5V8.5H8.50001C8.29778 8.5 8.11546 8.62182 8.03807 8.80866C7.96068 8.9955 8.00346 9.21055 8.14646 9.35355L10.6464 11.8536C10.8417 12.0488 11.1583 12.0488 11.3536 11.8536L13.8536 9.35355C13.9966 9.21055 14.0393 8.9955 13.9619 8.80866C13.8845 8.62182 13.7022 8.5 13.5 8.5H12V2.5C12 1.11929 10.8807 0 9.50001 0H8.00001ZM3.35356 2.14645C3.1583 1.95118 2.84172 1.95118 2.64646 2.14645L0.146462 4.64645C0.0034622 4.78945 -0.0393156 5.0045 0.0380748 5.19134C0.115466 5.37818 0.297784 5.5 0.500015 5.5H2.00001V11.5C2.00001 12.8807 3.1193 14 4.50001 14H6.00001C6.55229 14 7.00001 13.5523 7.00001 13C7.00001 12.4477 6.55229 12 6.00001 12H4.50001C4.22387 12 4.00001 11.7761 4.00001 11.5V5.5H5.50001C5.70224 5.5 5.88456 5.37818 5.96195 5.19134C6.03934 5.0045 5.99656 4.78945 5.85356 4.64645L3.35356 2.14645Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186113\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-reload-vertical-2\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186086)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.45803 4H9.75093V3C9.75093 2.44772 9.30321 2 8.75093 2H5.25093C4.69864 2 4.25093 2.44772 4.25093 3V4C4.25093 4.55228 3.80321 5 3.25093 5C2.69864 5 2.25093 4.55228 2.25093 4V3C2.25093 1.34315 3.59407 0 5.25093 0H8.75093C10.4078 0 11.7509 1.34315 11.7509 3V4H13.0438C13.4892 4 13.7123 4.53857 13.3973 4.85355L11.1044 7.14645C10.9092 7.34171 10.5926 7.34171 10.3973 7.14645L8.10448 4.85355C7.7895 4.53857 8.01258 4 8.45803 4ZM2.25093 10H0.958035C0.512582 10 0.289498 9.46143 0.60448 9.14645L2.89737 6.85355C3.09264 6.65829 3.40922 6.65829 3.60448 6.85355L5.89737 9.14645C6.21236 9.46143 5.98927 10 5.54382 10H4.25093V11C4.25093 11.5523 4.69864 12 5.25093 12H8.75093C9.30321 12 9.75093 11.5523 9.75093 11V10C9.75093 9.44771 10.1986 9 10.7509 9C11.3032 9 11.7509 9.44771 11.7509 10V11C11.7509 12.6569 10.4078 14 8.75093 14H5.25093C3.59407 14 2.25093 12.6569 2.25093 11V10Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186086\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-roadmap\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186128)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.23256 3C5.88675 3.5978 5.2404 4 4.50012 4C3.75984 4 3.1135 3.5978 2.76769 3H1C0.447715 3 0 2.55228 0 2C0 1.44772 0.447715 1 1 1H2.76769C3.1135 0.402199 3.75984 0 4.50012 0C5.2404 0 5.88675 0.402199 6.23256 1H10.75C12.5449 1 14 2.45507 14 4.25C14 6.04493 12.5449 7.5 10.75 7.5H10.2326C9.88686 8.09807 9.24036 8.50049 8.49988 8.50049C7.75939 8.50049 7.1129 8.09807 6.76716 7.5H3.5C2.67157 7.5 2 8.17157 2 9C2 9.82843 2.67157 10.5 3.5 10.5H3.76797C4.11385 9.90247 4.76004 9.50049 5.50012 9.50049C6.2402 9.50049 6.8864 9.90247 7.23228 10.5H11V9.49951C11 9.29728 11.1218 9.11496 11.3087 9.03757C11.4955 8.96018 11.7106 9.00296 11.8536 9.14596L13.8536 11.146C14.0488 11.3412 14.0488 11.6578 13.8536 11.8531L11.8536 13.8531C11.7106 13.9961 11.4955 14.0388 11.3087 13.9615C11.1218 13.8841 11 13.7017 11 13.4995V12.5H7.23284C6.8871 13.0981 6.24061 13.5005 5.50012 13.5005C4.75964 13.5005 4.11314 13.0981 3.7674 12.5H3.5C1.567 12.5 0 10.933 0 9C0 7.067 1.567 5.5 3.5 5.5H6.76772C7.1136 4.90247 7.7598 4.50049 8.49988 4.50049C9.23996 4.50049 9.88615 4.90247 10.232 5.5H10.75C11.4404 5.5 12 4.94036 12 4.25C12 3.55964 11.4404 3 10.75 3H6.23256Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186128\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-round-left\", \"keywords\": [ \"diagram\", \"round\", \"arrow\", \"left\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.35019 1.00247C5.35019 0.699116 5.16746 0.425639 4.8872 0.309553C4.60694 0.193468 4.28436 0.257635 4.06986 0.472133L1.67084 2.87116C1.37794 3.16405 1.37794 3.63892 1.67084 3.93181L4.06986 6.33084C4.28436 6.54534 4.60694 6.60951 4.8872 6.49342C5.16746 6.37734 5.35019 6.10386 5.35019 5.80051V4.40139H6.99921C9.09681 4.40139 10.7973 6.10184 10.7973 8.19944C10.7973 10.297 9.09681 11.9975 6.99921 11.9975C4.90161 11.9975 3.20117 10.297 3.20117 8.19944C3.20117 7.64715 2.75345 7.19944 2.20117 7.19944C1.64888 7.19944 1.20117 7.64715 1.20117 8.19944C1.20117 11.4016 3.79704 13.9975 6.99921 13.9975C10.2014 13.9975 12.7973 11.4016 12.7973 8.19944C12.7973 4.99727 10.2014 2.40139 6.99921 2.40139H5.35019V1.00247Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"arrow-round-right\", \"keywords\": [ \"diagram\", \"round\", \"arrow\", \"right\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.64824 1.00247C8.64824 0.699116 8.83097 0.425639 9.11122 0.309553C9.39148 0.193468 9.71407 0.257635 9.92857 0.472133L12.3276 2.87116C12.6205 3.16405 12.6205 3.63892 12.3276 3.93181L9.92857 6.33084C9.71407 6.54534 9.39148 6.60951 9.11122 6.49342C8.83097 6.37734 8.64824 6.10386 8.64824 5.80051V4.40139H6.99922C4.90161 4.40139 3.20117 6.10184 3.20117 8.19944C3.20117 10.297 4.90161 11.9975 6.99922 11.9975C9.09682 11.9975 10.7973 10.297 10.7973 8.19944C10.7973 7.64715 11.245 7.19944 11.7973 7.19944C12.3496 7.19944 12.7973 7.64715 12.7973 8.19944C12.7973 11.4016 10.2014 13.9975 6.99922 13.9975C3.79704 13.9975 1.20117 11.4016 1.20117 8.19944C1.20117 4.99727 3.79704 2.40139 6.99922 2.40139H8.64824V1.00247Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"arrow-shrink\", \"keywords\": [ \"expand\", \"retract\", \"shrink\", \"bigger\", \"big\", \"small\", \"smaller\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186104)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.292893 0.292893C0.683417 -0.097631 1.31658 -0.097631 1.70711 0.292893L3.10355 1.68934L4.14645 0.646447C4.28945 0.503448 4.5045 0.46067 4.69134 0.53806C4.87818 0.615451 5 0.797769 5 1V4.5C5 4.77614 4.77614 5 4.5 5H1C0.797769 5 0.615451 4.87818 0.53806 4.69134C0.46067 4.5045 0.503448 4.28945 0.646447 4.14645L1.68934 3.10355L0.292893 1.70711C-0.097631 1.31658 -0.097631 0.683417 0.292893 0.292893ZM9.30866 0.53806C9.4955 0.46067 9.71055 0.503448 9.85355 0.646447L10.8965 1.68935L12.2929 0.292893C12.6834 -0.097631 13.3166 -0.097631 13.7071 0.292893C14.0976 0.683417 14.0976 1.31658 13.7071 1.70711L12.3107 3.10356L13.3536 4.14645C13.4966 4.28945 13.5393 4.5045 13.4619 4.69134C13.3845 4.87818 13.2022 5 13 5H9.5C9.22386 5 9 4.77614 9 4.5V1C9 0.797769 9.12182 0.615451 9.30866 0.53806ZM1 9C0.797769 9 0.615451 9.12182 0.53806 9.30866C0.46067 9.4955 0.503448 9.71055 0.646447 9.85355L1.68941 10.8965L0.292893 12.293C-0.097631 12.6836 -0.097631 13.3167 0.292893 13.7072C0.683417 14.0978 1.31658 14.0978 1.70711 13.7072L3.10362 12.3107L4.14645 13.3536C4.28945 13.4966 4.5045 13.5393 4.69134 13.4619C4.87818 13.3845 5 13.2022 5 13V9.5C5 9.22386 4.77614 9 4.5 9H1ZM9 9.5C9 9.22386 9.22386 9 9.5 9H13C13.2022 9 13.3845 9.12182 13.4619 9.30866C13.5393 9.4955 13.4966 9.71055 13.3536 9.85355L12.3106 10.8965L13.7071 12.293C14.0976 12.6836 14.0976 13.3167 13.7071 13.7072C13.3166 14.0978 12.6834 14.0978 12.2929 13.7072L10.8964 12.3107L9.85355 13.3536C9.71055 13.4966 9.4955 13.5393 9.30866 13.4619C9.12182 13.3845 9 13.2022 9 13V9.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186104\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-shrink-diagonal-1\", \"keywords\": [ \"expand\", \"retract\", \"shrink\", \"bigger\", \"big\", \"small\", \"smaller\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186089)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.7071 1.70711C14.0976 1.31658 14.0976 0.683417 13.7071 0.292893C13.3166 -0.097631 12.6834 -0.097631 12.2929 0.292893L10.1464 2.43934L8.85355 1.14645C8.71055 1.00345 8.4955 0.96067 8.30866 1.03806C8.12182 1.11545 8 1.29777 8 1.5V5.5C8 5.77614 8.22386 6 8.5 6H12.5C12.7022 6 12.8845 5.87818 12.9619 5.69134C13.0393 5.5045 12.9966 5.28945 12.8536 5.14645L11.5607 3.85355L13.7071 1.70711ZM5.14645 12.8536L3.85355 11.5607L1.70711 13.7071C1.31658 14.0976 0.683417 14.0976 0.292893 13.7071C-0.097631 13.3166 -0.097631 12.6834 0.292893 12.2929L2.43934 10.1464L1.14645 8.85355C1.00345 8.71055 0.96067 8.4955 1.03806 8.30866C1.11545 8.12182 1.29777 8 1.5 8H5.5C5.77614 8 6 8.22386 6 8.5V12.5C6 12.7022 5.87818 12.8845 5.69134 12.9619C5.5045 13.0393 5.28945 12.9966 5.14645 12.8536Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186089\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-shrink-diagonal-2\", \"keywords\": [ \"expand\", \"retract\", \"shrink\", \"bigger\", \"big\", \"small\", \"smaller\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186098)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.70711 0.292893C1.31658 -0.097631 0.683417 -0.097631 0.292893 0.292893C-0.097631 0.683417 -0.097631 1.31658 0.292893 1.70711L2.43934 3.85355L1.14645 5.14645C1.00345 5.28945 0.96067 5.5045 1.03806 5.69134C1.11545 5.87818 1.29777 6 1.5 6H5.5C5.77614 6 6 5.77614 6 5.5V1.5C6 1.29777 5.87818 1.11545 5.69134 1.03806C5.5045 0.96067 5.28945 1.00345 5.14645 1.14645L3.85355 2.43934L1.70711 0.292893ZM12.8536 8.85355L11.5607 10.1464L13.7071 12.2929C14.0976 12.6834 14.0976 13.3166 13.7071 13.7071C13.3166 14.0976 12.6834 14.0976 12.2929 13.7071L10.1464 11.5607L8.85355 12.8536C8.71055 12.9966 8.4955 13.0393 8.30866 12.9619C8.12182 12.8845 8 12.7022 8 12.5V8.5C8 8.22386 8.22386 8 8.5 8H12.5C12.7022 8 12.8845 8.12182 12.9619 8.30866C13.0393 8.4955 12.9966 8.71055 12.8536 8.85355Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186098\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-transfer-diagonal-1\", \"keywords\": [ \"arrows\", \"arrow\", \"server\", \"data\", \"diagonal\", \"internet\", \"transfer\", \"network\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186110)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.9977 1.06752C10.9821 1.30049 10.8852 1.52903 10.7071 1.70711L8.70711 3.70711L1.70711 10.7071C1.31658 11.0976 0.683417 11.0976 0.292893 10.7071C-0.097631 10.3166 -0.097631 9.68342 0.292893 9.29289L6.58579 3L5.29289 1.70711C5.0069 1.42111 4.92134 0.990991 5.07612 0.617317C5.2309 0.243642 5.59554 0 6 0H9.99983C10.2544 0 10.4934 0.096541 10.674 0.261286M10.7558 0.345134C10.8249 0.424874 10.8821 0.516348 10.9239 0.617317C10.958 0.699788 10.9805 0.785008 10.9917 0.870822C10.9672 0.68158 10.8885 0.498008 10.7558 0.345134ZM7.41421 11L13.7071 4.70711C14.0976 4.31658 14.0976 3.68342 13.7071 3.29289C13.3166 2.90237 12.6834 2.90237 12.2929 3.29289L5.29289 10.2929L3.29289 12.2929C3.11481 12.471 3.01794 12.6995 3.00227 12.9325C2.99784 12.9978 2.99979 13.0636 3.00833 13.1292C3.03272 13.3175 3.11067 13.5001 3.24219 13.6525C3.26843 13.683 3.29641 13.7118 3.32595 13.7387C3.50395 13.9014 3.72651 13.9881 3.95203 13.9989C3.96801 13.9996 3.984 14 4 14H8C8.40446 14 8.7691 13.7564 8.92388 13.3827C9.07866 13.009 8.9931 12.5789 8.70711 12.2929L7.41421 11Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186110\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-transfer-diagonal-2\", \"keywords\": [ \"arrows\", \"arrow\", \"server\", \"data\", \"diagonal\", \"internet\", \"transfer\", \"network\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186116)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8 0C8.40446 0 8.7691 0.243642 8.92388 0.617317C9.07866 0.990991 8.9931 1.42111 8.70711 1.70711L7.40602 3.00819L13.7062 9.29197C14.0972 9.68199 14.098 10.3152 13.708 10.7062C13.318 11.0972 12.6848 11.098 12.2938 10.708L3.27819 1.71584C2.88716 1.32582 2.88633 0.692659 3.27635 0.301627C3.36057 0.217182 3.45614 0.150935 3.55815 0.102892C3.6929 0.0364926 3.84347 0 4 0H8ZM6.58579 11L0.292893 4.70711C-0.0976311 4.31658 -0.0976311 3.68342 0.292893 3.29289C0.683417 2.90237 1.31658 2.90237 1.70711 3.29289L8.70711 10.2929L10.7071 12.2929C10.8852 12.471 10.9821 12.6995 10.9977 12.9325C11.0022 12.9978 11.0002 13.0636 10.9917 13.1292C10.9673 13.3175 10.8893 13.5001 10.7578 13.6525C10.7316 13.683 10.7036 13.7118 10.674 13.7387C10.4961 13.9014 10.2735 13.9881 10.048 13.9989C10.032 13.9996 10.016 14 10 14H6C5.59554 14 5.2309 13.7564 5.07612 13.3827C4.92134 13.009 5.0069 12.5789 5.29289 12.2929L6.58579 11Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186116\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-transfer-diagonal-3\", \"keywords\": [ \"arrows\", \"arrow\", \"server\", \"data\", \"diagonal\", \"internet\", \"transfer\", \"network\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186095)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6 0H8.99983H9.5C9.77614 0 10 0.223858 10 0.5V0.999307V1.00069V4C10 4.20223 9.87818 4.38455 9.69134 4.46194C9.5045 4.53933 9.28945 4.49655 9.14645 4.35355L8.10355 3.31066L2.20711 9.20711C1.81658 9.59763 1.18342 9.59763 0.792893 9.20711C0.402369 8.81658 0.402369 8.18342 0.792893 7.79289L6.68934 1.89645L5.64645 0.853553C5.50345 0.710554 5.46067 0.495495 5.53806 0.308658C5.61545 0.121821 5.79777 0 6 0ZM13.2071 6.20711C13.5976 5.81658 13.5976 5.18342 13.2071 4.79289C12.8166 4.40237 12.1834 4.40237 11.7929 4.79289L5.89645 10.6893L4.85355 9.64645C4.71055 9.50345 4.4955 9.46067 4.30866 9.53806C4.12182 9.61545 4 9.79777 4 10V13V13.5C4 13.7761 4.22386 14 4.5 14H4.99931H5.00069H8C8.20223 14 8.38455 13.8782 8.46194 13.6913C8.53933 13.5045 8.49655 13.2894 8.35355 13.1464L7.31066 12.1036L13.2071 6.20711Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186095\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-up-1\", \"keywords\": [ \"arrow\", \"up\", \"keyboard\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186101)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.64646 0.146447C6.84172 -0.0488156 7.1583 -0.0488156 7.35356 0.146447L10.8536 3.64645C10.9966 3.78945 11.0393 4.0045 10.9619 4.19134C10.8845 4.37818 10.7022 4.5 10.5 4.5H8.00001V13C8.00001 13.5523 7.55229 14 7.00001 14C6.44772 14 6.00001 13.5523 6.00001 13V4.5H3.50001C3.29778 4.5 3.11546 4.37818 3.03807 4.19134C2.96068 4.0045 3.00346 3.78945 3.14646 3.64645L6.64646 0.146447Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186101\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"arrow-up-dashed-square\", \"keywords\": [ \"arrow\", \"keyboard\", \"button\", \"up\", \"square\", \"dashes\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186125)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.96154 1.5H6.03847C5.62425 1.5 5.28847 1.16421 5.28847 0.75C5.28847 0.335787 5.62425 9.53674e-07 6.03847 0H7.96154C8.37576 0 8.71154 0.335786 8.71154 0.75C8.71154 1.16421 8.37576 1.5 7.96154 1.5ZM5.28847 13.25C5.28847 12.8358 5.62425 12.5 6.03847 12.5H7.96154C8.37576 12.5 8.71154 12.8358 8.71154 13.25C8.71154 13.6642 8.37576 14 7.96154 14H6.03847C5.62425 14 5.28847 13.6642 5.28847 13.25ZM10.9316 1.57569C10.5321 1.46621 10.297 1.05362 10.4065 0.654132C10.5159 0.254647 10.9285 0.0195494 11.328 0.129025C12.5224 0.456354 13.4696 1.37563 13.8362 2.55272C13.9594 2.94819 13.7387 3.36864 13.3432 3.49182C12.9477 3.615 12.5273 3.39427 12.4041 2.99879C12.1891 2.30847 11.6313 1.76743 10.9316 1.57569ZM3.06865 1.57563C3.46813 1.46615 3.70323 1.05355 3.59375 0.654071C3.48428 0.254586 3.07168 0.0194883 2.6722 0.128964C1.47776 0.456293 0.530616 1.37557 0.163982 2.55266C0.0408022 2.94813 0.26154 3.36858 0.657014 3.49176C1.05249 3.61494 1.47294 3.39421 1.59612 2.99873C1.81113 2.30841 2.36895 1.76737 3.06865 1.57563ZM1.5 6.03845C1.5 5.62424 1.16421 5.28845 0.75 5.28845C0.335786 5.28845 -1.56e-07 5.62424 0 6.03845L5.85002e-07 7.96153C7.40002e-07 8.37574 0.335787 8.71153 0.75 8.71153C1.16421 8.71153 1.5 8.37574 1.5 7.96153V6.03845ZM13.2502 5.28845C12.836 5.28845 12.5002 5.62424 12.5002 6.03845V7.96153C12.5002 8.37574 12.836 8.71153 13.2502 8.71153C13.6644 8.71153 14.0002 8.37574 14.0002 7.96153V6.03845C14.0002 5.62424 13.6644 5.28845 13.2502 5.28845ZM1.59612 11.0013C1.47294 10.6059 1.05249 10.3851 0.657014 10.5083C0.26154 10.6315 0.0408021 11.0519 0.163982 11.4474C0.530615 12.6245 1.47776 13.5438 2.6722 13.8711C3.07168 13.9806 3.48428 13.7455 3.59375 13.346C3.70323 12.9465 3.46813 12.5339 3.06865 12.4244C2.36895 12.2327 1.81113 11.6916 1.59612 11.0013ZM13.3432 10.5083C12.9477 10.3851 12.5273 10.6059 12.4041 11.0013C12.1891 11.6916 11.6313 12.2327 10.9316 12.4244C10.5321 12.5339 10.297 12.9465 10.4065 13.346C10.5159 13.7455 10.9285 13.9806 11.328 13.8711C12.5224 13.5438 13.4696 12.6245 13.8362 11.4474C13.9594 11.0519 13.7387 10.6315 13.3432 10.5083ZM7.75012 10C7.75012 10.4142 7.41433 10.75 7.00012 10.75C6.58591 10.75 6.25012 10.4142 6.25012 10V6.5H5.00012C4.79789 6.5 4.61557 6.37818 4.53818 6.19134C4.46079 6.0045 4.50357 5.78945 4.64657 5.64645L6.64657 3.64645C6.74033 3.55268 6.86751 3.5 7.00012 3.5C7.13273 3.5 7.2599 3.55268 7.35367 3.64645L9.35367 5.64645C9.49667 5.78944 9.53945 6.0045 9.46206 6.19134C9.38467 6.37818 9.20235 6.5 9.00012 6.5H7.75012V10Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186125\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ascending-number-order\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186122)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.75002 0C4.30231 0 4.75002 0.447715 4.75002 1V10H6.25002C6.55337 10 6.82685 10.1827 6.94293 10.463C7.05902 10.7432 6.99485 11.0658 6.78035 11.2803L4.28035 13.7803C3.98746 14.0732 3.51259 14.0732 3.21969 13.7803L0.719692 11.2803C0.505193 11.0658 0.441027 10.7432 0.557112 10.463C0.673198 10.1827 0.946675 10 1.25002 10H2.75002V1C2.75002 0.447715 3.19774 0 3.75002 0ZM11.9168 0.75C11.9168 0.335786 11.581 0 11.1668 0C10.7526 0 10.4168 0.335786 10.4168 0.75C10.4168 1.02614 10.1929 1.25 9.91679 1.25H9.50012C9.08591 1.25 8.75012 1.58579 8.75012 2C8.75012 2.41421 9.08591 2.75 9.50012 2.75H9.91679C10.0894 2.75 10.257 2.72812 10.4168 2.68699V5H9.50012C9.08591 5 8.75012 5.33579 8.75012 5.75C8.75012 6.16421 9.08591 6.5 9.50012 6.5H12.8335C13.2477 6.5 13.5835 6.16421 13.5835 5.75C13.5835 5.33579 13.2477 5 12.8335 5H11.9168V0.75ZM11.0784 10.0938H10.4221C10.0511 10.0938 9.75025 9.79295 9.75025 9.42189C9.75025 9.05083 10.0511 8.75002 10.4221 8.75002L11.0783 8.75001C11.4478 8.75001 11.7475 9.04812 11.7502 9.4169V9.42687C11.7475 9.79564 11.4478 10.0938 11.0784 10.0938ZM11.0783 7.25001C12.1888 7.25 13.1047 8.08343 13.2345 9.15891C13.2448 9.20849 13.2502 9.25986 13.2502 9.3125V9.41292V9.42189V9.43085V11.9375C13.2502 13.0766 12.3268 14 11.1877 14H10.3127C9.41352 14 8.65055 13.425 8.36778 12.6249C8.22974 12.2344 8.43444 11.8059 8.82497 11.6679C9.21551 11.5298 9.644 11.7345 9.78204 12.1251C9.85961 12.3445 10.069 12.5 10.3127 12.5H11.1877C11.4984 12.5 11.7502 12.2482 11.7502 11.9375V11.4878C11.5386 11.5566 11.3128 11.5938 11.0784 11.5938H10.4221C9.22263 11.5938 8.25025 10.6214 8.25025 9.42188C8.25025 8.2224 9.22262 7.25002 10.4221 7.25002L11.0783 7.25001Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186122\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"attribution\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186140)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7ZM8.5002 3.5001C8.5002 4.32858 7.82858 5.0002 7.0001 5.0002C6.17162 5.0002 5.5 4.32858 5.5 3.5001C5.5 2.67162 6.17162 2 7.0001 2C7.82858 2 8.5002 2.67162 8.5002 3.5001ZM8.13826 11.6597L8.44463 9.20877H9.16667C9.36611 9.20877 9.52778 9.04709 9.52778 8.84766V8.12542C9.52778 6.72936 8.39597 5.59766 6.99992 5.59766C5.60386 5.59766 4.47223 6.72936 4.47223 8.12542V8.84766C4.47223 9.04709 4.6339 9.20877 4.83334 9.20877H5.55539L5.86175 11.6597C5.89303 11.9099 6.10573 12.0977 6.35789 12.0977H7.64212C7.89429 12.0977 8.10699 11.9099 8.13826 11.6597Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186140\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"blank-calendar\", \"keywords\": [ \"blank\", \"calendar\", \"date\", \"day\", \"month\", \"empty\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186107)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.5 1C4.5 0.447715 4.05228 0 3.5 0C2.94772 0 2.5 0.447715 2.5 1V2H1.5C0.671573 2 0 2.67157 0 3.5V5H14V3.5C14 2.67157 13.3284 2 12.5 2H11.5001V1C11.5001 0.447715 11.0524 0 10.5001 0C9.94781 0 9.50009 0.447715 9.50009 1V2H4.5V1ZM14 6.25H0V12.5C0 13.3284 0.671573 14 1.5 14H12.5C13.3284 14 14 13.3284 14 12.5V6.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186107\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"blank-notepad\", \"keywords\": [ \"content\", \"notes\", \"book\", \"notepad\", \"notebook\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186131)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.5 1C4.5 0.447715 4.05228 0 3.5 0C2.94772 0 2.5 0.447715 2.5 1V2H1.5C0.671573 2 0 2.67157 0 3.5V12.5C0 13.3284 0.671573 14 1.5 14H12.5C13.3284 14 14 13.3284 14 12.5V3.5C14 2.67157 13.3284 2 12.5 2H11.5001V1C11.5001 0.447715 11.0524 0 10.5001 0C9.94784 0 9.50012 0.447715 9.50012 1V2H8V1C8 0.447715 7.55228 0 7 0C6.44772 0 6 0.447715 6 1V2H4.5V1Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186131\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"block-bell-notification\", \"keywords\": [ \"notification\", \"alarm\", \"alert\", \"bell\", \"block\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186146)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 3.74837C14 4.75883 13.6002 5.67591 12.9501 6.35002C12.9356 6.36741 12.9202 6.38429 12.9038 6.40061C12.8875 6.41693 12.8706 6.43234 12.8532 6.44685C12.1791 7.09692 11.2621 7.49674 10.2516 7.49674C8.18144 7.49674 6.50323 5.81854 6.50323 3.74837C6.50323 2.73813 6.90289 1.82123 7.55272 1.14716C7.56733 1.12961 7.58287 1.11257 7.59933 1.0961C7.6158 1.07964 7.63283 1.0641 7.65038 1.04949C8.32446 0.399657 9.24136 0 10.2516 0C12.3218 0 14 1.6782 14 3.74837ZM10.2516 1.5C11.4933 1.5 12.5 2.50663 12.5 3.74837C12.5 4.09528 12.4214 4.42383 12.2811 4.71722L9.28276 1.71887C9.57615 1.57857 9.9047 1.5 10.2516 1.5ZM8.2221 2.77954L11.2204 5.77787C10.9271 5.91818 10.5985 5.99674 10.2516 5.99674C9.00986 5.99674 8.00323 4.99011 8.00323 3.74837C8.00323 3.40147 8.0818 3.07292 8.2221 2.77954ZM10.2516 8.74684C10.3594 8.74684 10.4663 8.74342 10.5724 8.73668V9.57504C10.5724 9.80878 10.6716 10.0406 10.8609 10.2171C11.0797 10.4213 11.2521 10.5001 11.5 10.5001C11.7761 10.5001 12 10.724 12 11.0001C12 11.2762 11.7761 11.5001 11.5 11.5001H0.5C0.223858 11.5001 0 11.2762 0 11.0001C0 10.724 0.223858 10.5001 0.5 10.5001C0.747931 10.5001 0.920308 10.4213 1.13911 10.2171C1.32834 10.0406 1.42753 9.80878 1.42753 9.57504V5.80023C1.42753 4.64816 1.9183 3.5509 2.7792 2.74756C3.58736 1.99345 4.66004 1.5548 5.78419 1.50488C5.44474 2.17966 5.25323 2.94216 5.25323 3.74847C5.25323 6.50899 7.49108 8.74684 10.2516 8.74684ZM4.24991 13.2501C4.24991 12.8359 4.58569 12.5001 4.99991 12.5001H6.99991C7.41412 12.5001 7.74991 12.8359 7.74991 13.2501C7.74991 13.6643 7.41412 14.0001 6.99991 14.0001H4.99991C4.58569 14.0001 4.24991 13.6643 4.24991 13.2501Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186146\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bomb\", \"keywords\": [ \"delete\", \"bomb\", \"remove\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186149)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.5431 2.41015C12.0402 3.60096 13 5.4384 13 7.5C13 11.0899 10.0899 14 6.5 14C2.91015 14 0 11.0899 0 7.5C0 3.91015 2.91015 1 6.5 1C7.48943 1 8.42723 1.22107 9.26669 1.61652C9.96024 0.613562 11.1087 0 12.3508 0H13.25C13.6642 0 14 0.335786 14 0.75C14 1.16421 13.6642 1.5 13.25 1.5H12.3508C11.6305 1.5 10.9627 1.84385 10.5431 2.41015ZM6.49998 5.125C5.87009 5.125 5.26602 5.37521 4.82062 5.8206C4.37522 6.266 4.125 6.87009 4.125 7.49998C4.125 7.84516 3.84518 8.12498 3.5 8.12498C3.15482 8.12498 2.875 7.84516 2.875 7.49998C2.875 6.53857 3.25692 5.61654 3.93674 4.93672C4.61656 4.2569 5.53858 3.875 6.49998 3.875C6.84516 3.875 7.12498 4.15482 7.12498 4.5C7.12498 4.84518 6.84516 5.125 6.49998 5.125Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186149\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bookmark\", \"keywords\": [ \"bookmarks\", \"tags\", \"favorite\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186155)\\\">\\n<path d=\\\"M4 0C3.60218 0 3.22064 0.158035 2.93934 0.43934C2.65804 0.720644 2.5 1.10218 2.5 1.5V13.5C2.5 13.7022 2.62182 13.8845 2.80866 13.9619C2.9955 14.0393 3.21055 13.9966 3.35355 13.8536L7 10.2071L10.6464 13.8536C10.7894 13.9966 11.0045 14.0393 11.1913 13.9619C11.3782 13.8845 11.5 13.7022 11.5 13.5V1.5C11.5 1.10217 11.342 0.720644 11.0607 0.43934C10.7794 0.158035 10.3978 0 10 0H4Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186155\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"braces-circle\", \"keywords\": [ \"interface\", \"math\", \"braces\", \"sign\", \"mathematics\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186137)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 0.25C3.27208 0.25 0.25 3.27208 0.25 7C0.25 10.7279 3.27208 13.75 7 13.75C10.7279 13.75 13.75 10.7279 13.75 7C13.75 3.27208 10.7279 0.25 7 0.25ZM3.74271 5.61094C3.818 4.34464 4.88013 3.875 5.5 3.875C5.84518 3.875 6.125 4.15482 6.125 4.5C6.125 4.84518 5.84518 5.125 5.5 5.125C5.44364 5.125 5.31051 5.15479 5.19483 5.24672C5.09919 5.32273 5.0046 5.44819 4.99051 5.68513C4.96748 6.07244 4.8653 6.59232 4.5482 7C4.8653 7.40768 4.96748 7.92756 4.99051 8.31487C5.0046 8.55181 5.09919 8.67727 5.19483 8.75328C5.31051 8.84521 5.44364 8.875 5.5 8.875C5.84518 8.875 6.125 9.15482 6.125 9.5C6.125 9.84518 5.84518 10.125 5.5 10.125C4.88013 10.125 3.818 9.65536 3.74271 8.38906C3.71665 7.95068 3.57688 7.73721 3.44476 7.67115L3.22049 7.55902C3.00875 7.45315 2.875 7.23673 2.875 7C2.875 6.76327 3.00875 6.54685 3.22049 6.44098L3.44476 6.32885C3.57688 6.26279 3.71665 6.04932 3.74271 5.61094ZM8.5 3.875C9.11987 3.875 10.182 4.34464 10.2573 5.61094C10.2833 6.04932 10.4231 6.26279 10.5552 6.32885L10.7795 6.44098C10.9912 6.54685 11.125 6.76327 11.125 7C11.125 7.23673 10.9912 7.45315 10.7795 7.55902L10.5552 7.67115C10.4231 7.73721 10.2833 7.95068 10.2573 8.38906C10.182 9.65536 9.11987 10.125 8.5 10.125C8.15482 10.125 7.875 9.84518 7.875 9.5C7.875 9.15482 8.15482 8.875 8.5 8.875C8.55636 8.875 8.68949 8.84521 8.80517 8.75328C8.90081 8.67727 8.9954 8.55181 9.00949 8.31487C9.03252 7.92756 9.1347 7.40768 9.4518 7C9.1347 6.59232 9.03252 6.07244 9.00949 5.68513C8.9954 5.44819 8.90081 5.32273 8.80517 5.24672C8.68949 5.15479 8.55636 5.125 8.5 5.125C8.15482 5.125 7.875 4.84518 7.875 4.5C7.875 4.15482 8.15482 3.875 8.5 3.875Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186137\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"brightness-1\", \"keywords\": [ \"bright\", \"adjust\", \"brightness\", \"adjustment\", \"sun\", \"raise\", \"controls\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186152)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 0C7.41421 0 7.75 0.335786 7.75 0.75V1.75C7.75 2.16421 7.41421 2.5 7 2.5C6.58579 2.5 6.25 2.16421 6.25 1.75V0.75C6.25 0.335786 6.58579 0 7 0ZM9.25 7C9.25 8.24264 8.24264 9.25 7 9.25C5.75736 9.25 4.75 8.24264 4.75 7C4.75 5.75736 5.75736 4.75 7 4.75C8.24264 4.75 9.25 5.75736 9.25 7ZM7.75 12.25C7.75 11.8358 7.41421 11.5 7 11.5C6.58579 11.5 6.25 11.8358 6.25 12.25V13.25C6.25 13.6642 6.58579 14 7 14C7.41421 14 7.75 13.6642 7.75 13.25V12.25ZM11.5 7C11.5 6.58579 11.8358 6.25 12.25 6.25H13.25C13.6642 6.25 14 6.58579 14 7C14 7.41421 13.6642 7.75 13.25 7.75H12.25C11.8358 7.75 11.5 7.41421 11.5 7ZM0.75 6.25C0.335786 6.25 0 6.58579 0 7C0 7.41421 0.335786 7.75 0.75 7.75H1.75C2.16421 7.75 2.5 7.41421 2.5 7C2.5 6.58579 2.16421 6.25 1.75 6.25H0.75ZM2.05024 2.05024C2.34313 1.75734 2.818 1.75734 3.1109 2.05024L3.97156 2.9109C4.26445 3.20379 4.26445 3.67866 3.97156 3.97156C3.67866 4.26445 3.20379 4.26445 2.9109 3.97156L2.05024 3.1109C1.75734 2.818 1.75734 2.34313 2.05024 2.05024ZM11.0891 10.0283C10.7962 9.73537 10.3213 9.73537 10.0284 10.0283C9.73555 10.3212 9.73555 10.796 10.0284 11.0889L10.8891 11.9496C11.182 12.2425 11.6569 12.2425 11.9498 11.9496C12.2427 11.6567 12.2427 11.1818 11.9498 10.8889L11.0891 10.0283ZM11.9498 2.05024C12.2427 2.34313 12.2427 2.818 11.9498 3.1109L11.0891 3.97156C10.7962 4.26445 10.3213 4.26445 10.0284 3.97156C9.73555 3.67866 9.73555 3.20379 10.0284 2.9109L10.8891 2.05024C11.182 1.75734 11.6569 1.75734 11.9498 2.05024ZM3.97156 11.0889C4.26445 10.796 4.26445 10.3212 3.97156 10.0283C3.67866 9.73537 3.20379 9.73537 2.9109 10.0283L2.05024 10.8889C1.75734 11.1818 1.75734 11.6567 2.05024 11.9496C2.34313 12.2425 2.818 12.2425 3.1109 11.9496L3.97156 11.0889Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186152\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"brightness-2\", \"keywords\": [ \"bright\", \"adjust\", \"brightness\", \"adjustment\", \"sun\", \"raise\", \"controls\", \"half\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186119)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 0C7.41421 0 7.75 0.335786 7.75 0.75V1.75C7.75 2.16421 7.41421 2.5 7 2.5C6.58579 2.5 6.25 2.16421 6.25 1.75V0.75C6.25 0.335786 6.58579 0 7 0ZM7 11.5C7.41421 11.5 7.75 11.8358 7.75 12.25V13.25C7.75 13.6642 7.41421 14 7 14C6.58579 14 6.25 13.6642 6.25 13.25V12.25C6.25 11.8358 6.58579 11.5 7 11.5ZM12.25 6.25C11.8358 6.25 11.5 6.58579 11.5 7C11.5 7.41421 11.8358 7.75 12.25 7.75H13.25C13.6642 7.75 14 7.41421 14 7C14 6.58579 13.6642 6.25 13.25 6.25H12.25ZM0 7C0 6.58579 0.335786 6.25 0.75 6.25H1.75C2.16421 6.25 2.5 6.58579 2.5 7C2.5 7.41421 2.16421 7.75 1.75 7.75H0.75C0.335786 7.75 0 7.41421 0 7ZM3.1109 2.05024C2.818 1.75734 2.34313 1.75734 2.05024 2.05024C1.75734 2.34313 1.75734 2.818 2.05024 3.1109L2.9109 3.97156C3.20379 4.26445 3.67866 4.26445 3.97156 3.97156C4.26445 3.67866 4.26445 3.20379 3.97156 2.9109L3.1109 2.05024ZM10.0284 10.0283C10.3213 9.73537 10.7962 9.73537 11.0891 10.0283L11.9498 10.8889C12.2427 11.1818 12.2427 11.6567 11.9498 11.9496C11.6569 12.2425 11.182 12.2425 10.8891 11.9496L10.0284 11.0889C9.73555 10.796 9.73555 10.3212 10.0284 10.0283ZM11.9498 3.1109C12.2427 2.818 12.2427 2.34313 11.9498 2.05024C11.6569 1.75734 11.182 1.75734 10.8891 2.05024L10.0284 2.9109C9.73555 3.20379 9.73555 3.67866 10.0284 3.97156C10.3213 4.26445 10.7962 4.26445 11.0891 3.97156L11.9498 3.1109ZM3.97156 10.0283C4.26445 10.3212 4.26445 10.796 3.97156 11.0889L3.1109 11.9496C2.818 12.2425 2.34313 12.2425 2.05024 11.9496C1.75734 11.6567 1.75734 11.1818 2.05024 10.8889L2.9109 10.0283C3.20379 9.73537 3.67866 9.73537 3.97156 10.0283ZM6.50186 10.2121C4.94341 9.97235 3.75 8.62554 3.75 7C3.75 5.37446 4.94341 4.02765 6.50186 3.78794C6.77479 3.74596 7 3.97386 7 4.25V9.75C7 10.0261 6.77479 10.254 6.50186 10.2121Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186119\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"brightness-3\", \"keywords\": [ \"bright\", \"adjust\", \"brightness\", \"adjustment\", \"sun\", \"raise\", \"controls\", \"dot\", \"small\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186179)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 0C7.41421 0 7.75 0.335786 7.75 0.75V1.75C7.75 2.16421 7.41421 2.5 7 2.5C6.58579 2.5 6.25 2.16421 6.25 1.75V0.75C6.25 0.335786 6.58579 0 7 0ZM8 7C8 7.55228 7.55228 8 7 8C6.44772 8 6 7.55228 6 7C6 6.44772 6.44772 6 7 6C7.55228 6 8 6.44772 8 7ZM7.75 12.25C7.75 11.8358 7.41421 11.5 7 11.5C6.58579 11.5 6.25 11.8358 6.25 12.25V13.25C6.25 13.6642 6.58579 14 7 14C7.41421 14 7.75 13.6642 7.75 13.25V12.25ZM11.5 7C11.5 6.58579 11.8358 6.25 12.25 6.25H13.25C13.6642 6.25 14 6.58579 14 7C14 7.41421 13.6642 7.75 13.25 7.75H12.25C11.8358 7.75 11.5 7.41421 11.5 7ZM0.75 6.25C0.335786 6.25 0 6.58579 0 7C0 7.41421 0.335786 7.75 0.75 7.75H1.75C2.16421 7.75 2.5 7.41421 2.5 7C2.5 6.58579 2.16421 6.25 1.75 6.25H0.75ZM2.05024 2.05024C2.34313 1.75734 2.818 1.75734 3.1109 2.05024L3.97156 2.9109C4.26445 3.20379 4.26445 3.67866 3.97156 3.97156C3.67866 4.26445 3.20379 4.26445 2.9109 3.97156L2.05024 3.1109C1.75734 2.818 1.75734 2.34313 2.05024 2.05024ZM11.0891 10.0283C10.7962 9.73537 10.3213 9.73537 10.0284 10.0283C9.73555 10.3212 9.73555 10.796 10.0284 11.0889L10.8891 11.9496C11.182 12.2425 11.6569 12.2425 11.9498 11.9496C12.2427 11.6567 12.2427 11.1818 11.9498 10.8889L11.0891 10.0283ZM11.9498 2.05024C12.2427 2.34313 12.2427 2.818 11.9498 3.1109L11.0891 3.97156C10.7962 4.26445 10.3213 4.26445 10.0284 3.97156C9.73555 3.67866 9.73555 3.20379 10.0284 2.9109L10.8891 2.05024C11.182 1.75734 11.6569 1.75734 11.9498 2.05024ZM3.97156 11.0889C4.26445 10.796 4.26445 10.3212 3.97156 10.0283C3.67866 9.73537 3.20379 9.73537 2.9109 10.0283L2.05024 10.8889C1.75734 11.1818 1.75734 11.6567 2.05024 11.9496C2.34313 12.2425 2.818 12.2425 3.1109 11.9496L3.97156 11.0889Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186179\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"broken-link-2\", \"keywords\": [ \"break\", \"broken\", \"hyperlink\", \"link\", \"remove\", \"unlink\", \"chain\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186134)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.62711 2.73494C8.60703 1.75504 10.1958 1.75503 11.1757 2.73494C12.1556 3.71486 12.1556 5.30362 11.1757 6.28354L9.2578 8.20153L8.19062 7.13435L9.15133 6.17364C9.54185 5.78311 9.54185 5.14995 9.15133 4.75943C8.76081 4.3689 8.12764 4.3689 7.73712 4.75943L6.77641 5.72014L5.70921 4.65293L7.62711 2.73494ZM4.295 3.23872L6.21288 1.32074C7.97384 -0.440234 10.8289 -0.440243 12.5899 1.32073C14.3509 3.08169 14.3509 5.93677 12.5899 7.69774L10.672 9.61575L13.686 12.6297C13.9767 12.9204 13.9767 13.3918 13.686 13.6825C13.3953 13.9732 12.9239 13.9732 12.6332 13.6825L0.224853 1.27413C-0.0658613 0.983409 -0.0658613 0.512067 0.224853 0.221352C0.515569 -0.0693624 0.98691 -0.0693624 1.27763 0.221352L4.295 3.23872ZM2.73495 6.21289C3.12547 6.60341 3.12547 7.23658 2.73494 7.6271C1.75503 8.60702 1.75502 10.1958 2.73494 11.1757C3.71486 12.1556 5.30363 12.1556 6.28354 11.1757C6.67407 10.7852 7.30723 10.7852 7.69776 11.1757C8.08828 11.5662 8.08828 12.1994 7.69776 12.5899C5.93679 14.3509 3.0817 14.3509 1.32073 12.5899C-0.440242 10.8289 -0.440241 7.97386 1.32073 6.21289C1.71126 5.82237 2.34442 5.82237 2.73495 6.21289Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186134\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bullet-list\", \"keywords\": [ \"points\", \"bullet\", \"unordered\", \"list\", \"lists\", \"bullets\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2 2.49951C2 3.0518 1.55228 3.49951 1 3.49951C0.447715 3.49951 0 3.0518 0 2.49951C0 1.94723 0.447715 1.49951 1 1.49951C1.55228 1.49951 2 1.94723 2 2.49951ZM1 8C1.55228 8 2 7.55228 2 7C2 6.44772 1.55228 6 1 6C0.447715 6 0 6.44772 0 7C0 7.55228 0.447715 8 1 8ZM1 12.5005C1.55228 12.5005 2 12.0528 2 11.5005C2 10.9482 1.55228 10.5005 1 10.5005C0.447715 10.5005 0 10.9482 0 11.5005C0 12.0528 0.447715 12.5005 1 12.5005ZM4.75 1.75C4.33579 1.75 4 2.08579 4 2.5C4 2.91421 4.33579 3.25 4.75 3.25H13.25C13.6642 3.25 14 2.91421 14 2.5C14 2.08579 13.6642 1.75 13.25 1.75H4.75ZM4 7C4 6.58579 4.33579 6.25 4.75 6.25H13.25C13.6642 6.25 14 6.58579 14 7C14 7.41421 13.6642 7.75 13.25 7.75H4.75C4.33579 7.75 4 7.41421 4 7ZM4.75 10.75C4.33579 10.75 4 11.0858 4 11.5C4 11.9142 4.33579 12.25 4.75 12.25H13.25C13.6642 12.25 14 11.9142 14 11.5C14 11.0858 13.6642 10.75 13.25 10.75H4.75Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"calendar-add\", \"keywords\": [ \"add\", \"calendar\", \"date\", \"day\", \"month\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186143)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.5 0C4.05228 0 4.5 0.447715 4.5 1V2H9.50009V1C9.50009 0.447715 9.94781 0 10.5001 0C11.0524 0 11.5001 0.447715 11.5001 1V2H12.5C13.3284 2 14 2.67157 14 3.5V12.5C14 13.3284 13.3284 14 12.5 14H1.5C0.671573 14 0 13.3284 0 12.5V3.5C0 2.67157 0.671573 2 1.5 2H2.5V1C2.5 0.447715 2.94772 0 3.5 0ZM3.75 8.00012C3.75 7.58591 4.08579 7.25012 4.5 7.25012H6.25V5.50012C6.25 5.08591 6.58579 4.75012 7 4.75012C7.41421 4.75012 7.75 5.08591 7.75 5.50012V7.25012H9.5C9.91421 7.25012 10.25 7.58591 10.25 8.00012C10.25 8.41434 9.91421 8.75012 9.5 8.75012H7.75V10.5001C7.75 10.9143 7.41421 11.2501 7 11.2501C6.58579 11.2501 6.25 10.9143 6.25 10.5001V8.75012H4.5C4.08579 8.75012 3.75 8.41434 3.75 8.00012Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186143\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"calendar-edit\", \"keywords\": [ \"calendar\", \"date\", \"day\", \"compose\", \"edit\", \"note\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186158)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.5 0C4.05228 0 4.5 0.447715 4.5 1V2H9.50012V1C9.50012 0.447715 9.94784 0 10.5001 0C11.0524 0 11.5001 0.447715 11.5001 1V2H12.5C13.3284 2 14 2.67157 14 3.5V12.5C14 13.3284 13.3284 14 12.5 14H1.5C0.671573 14 0 13.3284 0 12.5V3.5C0 2.67157 0.671573 2 1.5 2H2.5V1C2.5 0.447715 2.94772 0 3.5 0ZM8.56274 5.07322C8.51583 5.02631 8.45218 4.99996 8.38583 5C8.31948 5.00004 8.25587 5.02644 8.209 5.07341L4.08619 9.20499C4.04793 9.24333 4.02316 9.29307 4.0156 9.34671L3.75244 11.2151C3.74152 11.2927 3.76757 11.3709 3.82282 11.4264C3.87806 11.4819 3.95613 11.5083 4.03373 11.4977L5.90215 11.2433C5.9563 11.236 6.00655 11.211 6.0452 11.1724L10.1768 7.04081C10.2744 6.94318 10.2744 6.78489 10.1768 6.68726L8.56274 5.07322Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186158\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"calendar-jump-to-date\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186161)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.5 0C4.05228 0 4.5 0.447715 4.5 1V2H9.50009V1C9.50009 0.447715 9.94781 0 10.5001 0C11.0524 0 11.5001 0.447715 11.5001 1V2H12.5C13.3284 2 14 2.67157 14 3.5V12.5C14 13.3284 13.3284 14 12.5 14H1.5C0.671573 14 0 13.3284 0 12.5V3.5C0 2.67157 0.671573 2 1.5 2H2.5V1C2.5 0.447715 2.94772 0 3.5 0ZM3.28613 8.1814C3.28613 7.76718 3.62192 7.4314 4.03613 7.4314H7.42954V6.27241C7.42954 6.01962 7.58181 5.79172 7.81536 5.69498C8.0489 5.59825 8.31773 5.65172 8.49648 5.83047L10.4056 7.73961C10.6497 7.98369 10.6497 8.37942 10.4056 8.6235L8.49648 10.5326C8.31773 10.7114 8.0489 10.7649 7.81536 10.6681C7.58181 10.5714 7.42953 10.3435 7.42953 10.0907V8.9314H4.03613C3.62192 8.9314 3.28613 8.59561 3.28613 8.1814Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186161\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"calendar-star\", \"keywords\": [ \"calendar\", \"date\", \"day\", \"favorite\", \"like\", \"month\", \"star\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186173)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.5 1C4.5 0.447715 4.05228 0 3.5 0C2.94772 0 2.5 0.447715 2.5 1V2H1.5C0.671573 2 0 2.67157 0 3.5V12.5C0 13.3284 0.671573 14 1.5 14H12.5C13.3284 14 14 13.3284 14 12.5V3.5C14 2.67157 13.3284 2 12.5 2H11.5001V1C11.5001 0.447715 11.0524 0 10.5001 0C9.94784 0 9.50012 0.447715 9.50012 1V2H4.5V1ZM6.98874 5.12509C7.08214 5.1233 7.17419 5.14767 7.25447 5.19545C7.33475 5.24324 7.40005 5.31253 7.44301 5.3955L7.44486 5.39915L8.18922 6.89652L8.19216 6.89713L9.84113 7.1475C9.93426 7.16021 10.022 7.19888 10.0942 7.25912C10.1672 7.32002 10.2213 7.40045 10.2503 7.491C10.2793 7.58155 10.2819 7.67848 10.2578 7.77045C10.2338 7.86191 10.1845 7.9447 10.1155 8.00927L8.95504 9.11208C8.96367 9.13224 8.96961 9.15347 8.9727 9.17523L9.20515 10.811C9.2226 10.9029 9.21396 10.9979 9.18011 11.0853C9.1451 11.1756 9.08464 11.2539 9.00605 11.3105C8.92746 11.3672 8.83412 11.3998 8.73735 11.4045C8.6426 11.4091 8.54854 11.3866 8.46611 11.3398L7.01026 10.5691C7.00575 10.5676 7.00101 10.5668 6.99623 10.5668C6.99145 10.5668 6.98672 10.5676 6.98221 10.5691L5.52635 11.3398C5.44392 11.3866 5.34986 11.4091 5.25512 11.4045C5.15834 11.3998 5.06501 11.3672 4.98642 11.3105C4.90783 11.2539 4.84736 11.1756 4.81236 11.0853C4.77805 10.9968 4.76964 10.9004 4.78802 10.8073L5.06397 9.16888C5.06546 9.16006 5.06742 9.15132 5.06984 9.14271L3.87547 7.99917L3.87045 7.99422C3.80587 7.92881 3.76055 7.84689 3.73945 7.75743C3.71835 7.66797 3.72228 7.57443 3.75081 7.48705C3.77934 7.39968 3.83137 7.32184 3.9012 7.26208C3.97103 7.20231 4.05596 7.16292 4.14669 7.14822L4.15042 7.14764L5.80295 6.90539L5.80415 6.90334L6.54659 5.40984C6.58638 5.32656 6.64843 5.25587 6.72591 5.20562C6.80429 5.15478 6.89533 5.12689 6.98874 5.12509Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186173\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"celsius\", \"keywords\": [ \"degrees\", \"temperature\", \"centigrade\", \"celsius\", \"degree\", \"weather\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186176)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 2.25C1.5 1.83579 1.83579 1.5 2.25 1.5C2.66421 1.5 3 1.83579 3 2.25C3 2.66421 2.66421 3 2.25 3C1.83579 3 1.5 2.66421 1.5 2.25ZM2.25 0C1.00736 0 0 1.00736 0 2.25C0 3.49264 1.00736 4.5 2.25 4.5C3.49264 4.5 4.5 3.49264 4.5 2.25C4.5 1.00736 3.49264 0 2.25 0ZM7.99979 4C7.99979 2.89543 8.89522 2 9.99979 2C10.9335 2 11.8027 2.61393 12.0569 3.33325C12.241 3.85396 12.8123 4.12689 13.333 3.94284C13.8537 3.75879 14.1267 3.18747 13.9426 2.66675C13.3732 1.05568 11.6785 0 9.99979 0C7.79065 0 5.99979 1.79086 5.99979 4V10C5.99979 12.2091 7.79065 14 9.99979 14C11.6785 14 13.3732 12.9443 13.9426 11.3332C14.1267 10.8125 13.8537 10.2412 13.333 10.0572C12.8123 9.87311 12.241 10.146 12.0569 10.6668C11.8027 11.3861 10.9335 12 9.99979 12C8.89522 12 7.99979 11.1046 7.99979 10V4Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186176\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"check\", \"keywords\": [ \"check\", \"form\", \"validation\", \"checkmark\", \"success\", \"add\", \"addition\", \"tick\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186170)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.6371 1.19841C14.0628 1.55023 14.1228 2.18055 13.7709 2.60628L5.7309 12.3355L5.72848 12.3384C5.5456 12.558 5.31608 12.7341 5.05661 12.8538C4.79713 12.9736 4.51423 13.034 4.22848 13.0307C3.93805 13.0271 3.65131 12.9577 3.39152 12.8278C3.1326 12.6984 2.90626 12.5121 2.72937 12.283C2.72877 12.2822 2.72818 12.2815 2.72758 12.2807L0.210694 9.04474C-0.128376 8.6088 -0.049842 7.98052 0.386105 7.64145C0.822053 7.30238 1.45033 7.38092 1.7894 7.81686L4.25297 10.9843L12.2292 1.33225C12.581 0.906522 13.2114 0.846601 13.6371 1.19841Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186170\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"check-square\", \"keywords\": [ \"check\", \"form\", \"validation\", \"checkmark\", \"success\", \"add\", \"addition\", \"box\", \"square\", \"tick\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186167)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.5 0C1.567 0 0 1.567 0 3.5V10.5C0 12.433 1.567 14 3.5 14H10.5C12.433 14 14 12.433 14 10.5V3.5C14 1.567 12.433 0 10.5 0H3.5ZM10.5028 5.21852C10.7615 4.89507 10.7091 4.42311 10.3856 4.16435C10.0622 3.90559 9.59022 3.95803 9.33146 4.28148L5.78557 8.71384L4.36711 7.65C4.03574 7.40147 3.56564 7.46863 3.31711 7.8C3.06859 8.13137 3.13574 8.60147 3.46711 8.85L5.46711 10.35C5.79119 10.5931 6.24971 10.5348 6.50277 10.2185L10.5028 5.21852Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186167\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"circle\", \"keywords\": [ \"geometric\", \"circle\", \"round\", \"design\", \"shape\", \"shapes\", \"shape\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186187)\\\">\\n<path d=\\\"M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186187\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"circle-clock\", \"keywords\": [ \"clock\", \"loading\", \"measure\", \"time\", \"circle\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186193)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14ZM7.625 4.5C7.625 4.15482 7.34518 3.875 7 3.875C6.65482 3.875 6.375 4.15482 6.375 4.5V7C6.375 7.14621 6.42626 7.28779 6.51986 7.40012L9.01986 10.4001C9.24084 10.6653 9.63494 10.7011 9.90012 10.4801C10.1653 10.2592 10.2011 9.86506 9.98014 9.59988L7.625 6.77372V4.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186193\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"clipboard-add\", \"keywords\": [ \"edit\", \"task\", \"edition\", \"add\", \"clipboard\", \"form\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.5 0C4.94772 0 4.5 0.447716 4.5 1V1.5C4.5 2.05229 4.94772 2.5 5.5 2.5H8.5C9.05229 2.5 9.5 2.05229 9.5 1.5V1C9.5 0.447715 9.05229 0 8.5 0H5.5ZM2.75 1H3.24997V1.5C3.24997 2.74264 4.25733 3.75 5.49997 3.75H8.49997C9.74261 3.75 10.75 2.74264 10.75 1.5V1H11.25C12.0784 1 12.75 1.67157 12.75 2.5V12.5C12.75 13.3284 12.0784 14 11.25 14H2.75C1.92157 14 1.25 13.3284 1.25 12.5V2.5C1.25 1.67157 1.92157 1 2.75 1ZM7.74997 6.00098C7.74997 5.58676 7.41418 5.25098 6.99997 5.25098C6.58576 5.25098 6.24997 5.58676 6.24997 6.00098V7.75098H4.49997C4.08576 7.75098 3.74997 8.08676 3.74997 8.50098C3.74997 8.91519 4.08576 9.25098 4.49997 9.25098H6.24997V11.001C6.24997 11.4152 6.58576 11.751 6.99997 11.751C7.41418 11.751 7.74997 11.4152 7.74997 11.001V9.25098H9.49997C9.91418 9.25098 10.25 8.91519 10.25 8.50098C10.25 8.08676 9.91418 7.75098 9.49997 7.75098H7.74997V6.00098Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"clipboard-check\", \"keywords\": [ \"checkmark\", \"edit\", \"task\", \"edition\", \"checklist\", \"check\", \"success\", \"clipboard\", \"form\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.5 0C4.94772 0 4.5 0.447716 4.5 1V1.5C4.5 2.05229 4.94772 2.5 5.5 2.5H8.5C9.05229 2.5 9.5 2.05229 9.5 1.5V1C9.5 0.447715 9.05229 0 8.5 0H5.5ZM3.24997 1H2.75C1.92157 1 1.25 1.67157 1.25 2.5V12.5C1.25 13.3284 1.92157 14 2.75 14H11.25C12.0784 14 12.75 13.3284 12.75 12.5V2.5C12.75 1.67157 12.0784 1 11.25 1H10.75V1.5C10.75 2.74264 9.74261 3.75 8.49997 3.75H5.49997C4.25733 3.75 3.24997 2.74264 3.24997 1.5V1ZM9.95 5.9C10.2814 6.14853 10.3485 6.61863 10.1 6.95L7.1 10.95C6.86117 11.2684 6.41517 11.3448 6.08397 11.124L4.58397 10.124C4.23933 9.89427 4.1462 9.42862 4.37596 9.08397C4.60573 8.73933 5.07138 8.6462 5.41603 8.87596L6.32569 9.48241L8.9 6.05C9.14853 5.71863 9.61863 5.65147 9.95 5.9Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"clipboard-remove\", \"keywords\": [ \"edit\", \"task\", \"edition\", \"remove\", \"delete\", \"clipboard\", \"form\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.5 0C4.94772 0 4.5 0.447716 4.5 1V1.5C4.5 2.05229 4.94772 2.5 5.5 2.5H8.5C9.05229 2.5 9.5 2.05229 9.5 1.5V1C9.5 0.447715 9.05229 0 8.5 0H5.5ZM3.24997 1H2.75C1.92157 1 1.25 1.67157 1.25 2.5V12.5C1.25 13.3284 1.92157 14 2.75 14H11.25C12.0784 14 12.75 13.3284 12.75 12.5V2.5C12.75 1.67157 12.0784 1 11.25 1H10.75V1.5C10.75 2.74264 9.74261 3.75 8.49997 3.75H5.49997C4.25733 3.75 3.24997 2.74264 3.24997 1.5V1ZM3.74997 8.5C3.74997 8.08579 4.08576 7.75 4.49997 7.75H9.49997C9.91418 7.75 10.25 8.08579 10.25 8.5C10.25 8.91421 9.91418 9.25 9.49997 9.25H4.49997C4.08576 9.25 3.74997 8.91421 3.74997 8.5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"cloud\", \"keywords\": [ \"cloud\", \"meteorology\", \"cloudy\", \"overcast\", \"cover\", \"weather\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186205)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.91342 2.3806C5.99979 2.00217 4.99446 1.90315 4.02455 2.09608C3.05465 2.289 2.16373 2.76521 1.46447 3.46447C0.765206 4.16373 0.289002 5.05465 0.0960758 6.02455C-0.0968503 6.99446 0.00216636 7.99979 0.380605 8.91342C0.759043 9.82705 1.39991 10.6079 2.22215 11.1574C3.0444 11.7068 4.0111 12 5 12H11C11.7957 12 12.5587 11.6839 13.1213 11.1213C13.6839 10.5587 14 9.79565 14 9C14 8.20435 13.6839 7.44129 13.1213 6.87868C12.5587 6.31607 11.7957 6 11 6C10.634 6.00049 10.2729 6.0677 9.93494 6.196C9.82041 5.49306 9.55648 4.81949 9.15735 4.22215C8.60794 3.3999 7.82705 2.75904 6.91342 2.3806Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186205\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cog\", \"keywords\": [ \"work\", \"loading\", \"cog\", \"gear\", \"settings\", \"machine\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186229)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.5562 0.689231L5.09312 1.88462L3.49927 2.78923L2.2285 2.59538C2.01691 2.56666 1.80155 2.6015 1.60979 2.69546C1.41804 2.78942 1.25854 2.93826 1.15158 3.12308L0.720814 3.87692C0.610431 4.06468 0.559574 4.2815 0.574955 4.49876C0.590336 4.71602 0.671233 4.92351 0.806968 5.09385L1.61466 6.09538V7.90462L0.828506 8.90615C0.692771 9.07649 0.611875 9.28398 0.596494 9.50124C0.581112 9.7185 0.631969 9.93532 0.742352 10.1231L1.17312 10.8769C1.28008 11.0617 1.43958 11.2106 1.63133 11.3045C1.82308 11.3985 2.03844 11.4333 2.25004 11.4046L3.52081 11.2108L5.09312 12.1154L5.5562 13.3108C5.63429 13.5132 5.77169 13.6873 5.9504 13.8102C6.12911 13.9332 6.3408 13.9994 6.55773 14H7.46235C7.67929 13.9994 7.89097 13.9332 8.06968 13.8102C8.24839 13.6873 8.38579 13.5132 8.46389 13.3108L8.92696 12.1154L10.4993 11.2108L11.7701 11.4046C11.9817 11.4333 12.197 11.3985 12.3888 11.3045C12.5805 11.2106 12.74 11.0617 12.847 10.8769L13.2778 10.1231C13.3881 9.93532 13.439 9.7185 13.4236 9.50124C13.4082 9.28398 13.3273 9.07649 13.1916 8.90615L12.3839 7.90462V6.09538L13.1701 5.09385C13.3058 4.92351 13.3867 4.71602 13.4021 4.49876C13.4175 4.2815 13.3666 4.06468 13.2562 3.87692L12.8254 3.12308C12.7185 2.93826 12.559 2.78942 12.3672 2.69546C12.1755 2.6015 11.9601 2.56666 11.7485 2.59538L10.4778 2.78923L8.90543 1.88462L8.44235 0.689231C8.36425 0.486838 8.22685 0.31275 8.04814 0.189767C7.86943 0.0667842 7.65775 0.00064083 7.44081 0H6.55773C6.3408 0.00064083 6.12911 0.0667842 5.9504 0.189767C5.77169 0.31275 5.63429 0.486838 5.5562 0.689231ZM6.99928 9.25C8.24192 9.25 9.24928 8.24264 9.24928 7C9.24928 5.75736 8.24192 4.75 6.99928 4.75C5.75664 4.75 4.74928 5.75736 4.74928 7C4.74928 8.24264 5.75664 9.25 6.99928 9.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186229\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"color-palette\", \"keywords\": [ \"color\", \"palette\", \"company\", \"office\", \"supplies\", \"work\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186199)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.97352 4.06315C4.44759 2.85522 5.62392 2 7 2C8.37608 2 9.55241 2.85522 10.0265 4.06315C9.77381 4.02161 9.51443 4 9.25 4C8.43593 4 7.6697 4.20479 7 4.56565C6.3303 4.20479 5.56407 4 4.75 4C4.48557 4 4.22619 4.02161 3.97352 4.06315ZM2.28263 4.6903C2.5596 2.33072 4.566 0.5 7 0.5C9.43399 0.5 11.4404 2.33072 11.7174 4.6903C13.0861 5.52393 14 7.03024 14 8.75C14 11.3734 11.8734 13.5 9.25 13.5C8.43593 13.5 7.6697 13.2952 7 12.9343C6.3303 13.2952 5.56407 13.5 4.75 13.5C2.12665 13.5 0 11.3734 0 8.75C0 7.03024 0.913945 5.52393 2.28263 4.6903ZM10.2257 5.64902C9.91774 5.5522 9.58997 5.5 9.25 5.5C8.93149 5.5 8.62367 5.54582 8.3328 5.63123C8.79831 6.16555 9.14625 6.805 9.33631 7.50924C9.81491 7.01442 10.1377 6.36801 10.2257 5.64902ZM9.46737 9.3097C10.4944 8.68415 11.2654 7.67983 11.5863 6.49077C12.1519 7.07558 12.5 7.87214 12.5 8.75C12.5 10.5449 11.0449 12 9.25 12C8.93149 12 8.62367 11.9542 8.3328 11.8688C8.94443 11.1667 9.3531 10.2832 9.46737 9.3097ZM7.97575 8.35098C7.88253 7.58958 7.526 6.90956 7 6.40479C6.474 6.90956 6.11747 7.58958 6.02425 8.35098C6.33226 8.4478 6.66003 8.5 7 8.5C7.33997 8.5 7.66774 8.4478 7.97575 8.35098ZM6.22353 9.93685C6.47619 9.97839 6.73557 10 7 10C7.26443 10 7.52381 9.97839 7.77648 9.93685C7.60325 10.3782 7.33625 10.7725 7 11.0952C6.66375 10.7725 6.39675 10.3782 6.22353 9.93685ZM4.53263 9.3097C4.6469 10.2832 5.05557 11.1667 5.66721 11.8688C5.37633 11.9542 5.06851 12 4.75 12C2.95507 12 1.5 10.5449 1.5 8.75C1.5 7.87214 1.84805 7.07557 2.41369 6.49076C2.73459 7.67982 3.50558 8.68415 4.53263 9.3097ZM4.66369 7.50924C4.18509 7.01442 3.86227 6.36801 3.77425 5.64902C4.08226 5.5522 4.41003 5.5 4.75 5.5C5.06851 5.5 5.37633 5.54582 5.66721 5.63123C5.20169 6.16555 4.85375 6.80499 4.66369 7.50924Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186199\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"color-picker\", \"keywords\": [ \"color\", \"colors\", \"design\", \"dropper\", \"eye\", \"eyedrop\", \"eyedropper\", \"painting\", \"picker\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186202)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.7786 1.22145C11.1798 -0.388969 8.50853 -0.388595 6.91019 1.22257L6.09671 2.03605L5.03033 0.969669C4.73744 0.676776 4.26256 0.676776 3.96967 0.969669C3.67678 1.26256 3.67678 1.73744 3.96967 2.03033L11.9697 10.0303C12.2626 10.3232 12.7374 10.3232 13.0303 10.0303C13.3232 9.73744 13.3232 9.26256 13.0303 8.96967L11.964 7.90336L12.7775 7.08986C14.3887 5.49153 14.389 2.82027 12.7786 1.22145ZM1.58216 10.1556L0.328922 11.4088C0.118249 11.6208 0 11.9074 0 12.2063C0 12.5051 0.118249 12.7918 0.328922 13.0037L0.996282 13.6711C1.20821 13.8817 1.4949 14 1.79372 14C2.09255 14 2.37923 13.8817 2.59116 13.6711L3.8444 12.4178C5.14736 13.1097 6.79492 12.8623 7.83725 11.8183L9.93216 9.72344L4.27656 4.06787L2.18165 6.16278C1.13764 7.2051 0.890274 8.85266 1.58216 10.1556Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186202\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"color-swatches\", \"keywords\": [ \"color\", \"colors\", \"design\", \"painting\", \"palette\", \"sample\", \"swatch\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186277)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0H4.5C4.89782 0 5.27936 0.158035 5.56066 0.43934C5.84196 0.720644 6 1.10217 6 1.5V3.37451H0V1.5C0 1.10218 0.158035 0.720644 0.43934 0.43934C0.720644 0.158035 1.10218 0 1.5 0ZM0 4.62451V7.875H6V4.62451H0ZM0 11V9.125H6V11C6 11.7956 5.68393 12.5587 5.12132 13.1213C4.55871 13.6839 3.79565 14 3 14C2.60603 14 2.21593 13.9224 1.85195 13.7716C1.48797 13.6209 1.15726 13.3999 0.87868 13.1213C0.31607 12.5587 0 11.7956 0 11ZM7.25 10.9925L12.3388 5.90366C12.6201 5.62236 12.7782 5.24082 12.7782 4.843C12.7782 4.44518 12.6201 4.06364 12.3388 3.78234L10.2175 1.66102C9.93621 1.37971 9.55468 1.22168 9.15686 1.22168C8.75903 1.22168 8.3775 1.37971 8.09619 1.66102L7.25 2.50721V10.9925ZM6.0105 14H12.5C12.8978 14 13.2793 13.842 13.5606 13.5607C13.842 13.2794 14 12.8978 14 12.5V9.5C14 9.10218 13.842 8.72064 13.5606 8.43934C13.2793 8.15804 12.8978 8 12.5 8H12.0105L6.0105 14Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186277\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cone-shape\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.66097 0.643017C7.50444 0.3389 7.21072 0.119693 6.853 0.119629C6.49527 0.119565 6.20147 0.338684 6.04485 0.64278L1.63807 9.19936C1.87429 9.05967 2.13279 8.93391 2.40672 8.82169C3.57589 8.34274 5.14794 8.06091 6.85223 8.06091C8.55653 8.06091 10.1286 8.34274 11.2978 8.82169C11.5709 8.93359 11.8287 9.05896 12.0644 9.19818L7.66097 0.643017ZM1.52539 11.1241C1.52539 11.0091 1.57966 10.8403 1.80294 10.624C2.02808 10.4059 2.38656 10.1808 2.88057 9.9784C3.86554 9.5749 5.26942 9.31091 6.85223 9.31091C8.43505 9.31091 9.83892 9.5749 10.8239 9.9784C11.3179 10.1808 11.6764 10.4059 11.9016 10.624C12.1248 10.8403 12.1791 11.0091 12.1791 11.1241C12.1791 11.2391 12.1248 11.408 11.9016 11.6242C11.6764 11.8423 11.3179 12.0674 10.8239 12.2698C9.83892 12.6733 8.43505 12.9373 6.85223 12.9373C5.26942 12.9373 3.86554 12.6733 2.88057 12.2698C2.38656 12.0674 2.02808 11.8423 1.80294 11.6242C1.57966 11.408 1.52539 11.2391 1.52539 11.1241Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"convert-PDF-2\", \"keywords\": [ \"essential\", \"files\", \"folder\", \"convert\", \"to\", \"PDF\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.43934 0.43934C1.72064 0.158035 2.10218 0 2.5 0H8.5C8.63261 0 8.75979 0.0526784 8.85355 0.146447L12.8536 4.14645C12.9473 4.24021 13 4.36739 13 4.5V12.5C13 12.8978 12.842 13.2794 12.5607 13.5607C12.2794 13.842 11.8978 14 11.5 14H2.5C2.10217 14 1.72064 13.842 1.43934 13.5607C1.15804 13.2794 1 12.8978 1 12.5V1.5C1 1.10218 1.15804 0.720644 1.43934 0.43934ZM6.0758 3.36709C6.15553 3.70541 6.18509 4.15568 6.1845 4.64157C6.87275 5.74121 7.99955 7.11147 8.94068 7.97163C9.37587 7.94242 9.79843 7.95233 10.1307 8.04191C10.3267 8.09474 10.5961 8.20227 10.7776 8.44897C10.9926 8.74125 10.9805 9.07719 10.877 9.34462C10.7727 9.61437 10.5618 9.84362 10.2448 9.92894C9.97075 10.0027 9.70813 9.9428 9.51655 9.87285C9.2001 9.75731 8.86122 9.53284 8.53202 9.26847C7.5209 9.40005 6.26438 9.72525 5.34428 10.0948C5.2136 10.4593 5.05526 10.7994 4.85925 11.0433C4.73702 11.1954 4.54256 11.3831 4.26188 11.4517C3.93928 11.5306 3.65076 11.4194 3.44326 11.2454C3.23778 11.0732 3.06438 10.8049 3.08961 10.4599C3.11175 10.157 3.28039 9.92976 3.41842 9.7866C3.64086 9.55588 3.96914 9.35409 4.32287 9.18045C4.65448 7.99775 4.89092 6.27615 4.92908 4.97998C4.70653 4.5954 4.52436 4.21921 4.42955 3.89229C4.37692 3.71081 4.33434 3.48931 4.36382 3.26716C4.39676 3.01883 4.5301 2.73565 4.83918 2.58296C4.97423 2.51625 5.15279 2.47115 5.35196 2.51056C5.54955 2.54965 5.69417 2.65745 5.78965 2.76258C5.95923 2.94928 6.03469 3.19265 6.0758 3.36709ZM6.05838 6.6341C6.47581 7.17578 6.93523 7.71615 7.39019 8.19605C6.83968 8.30974 6.27197 8.45787 5.74214 8.62667C5.87483 7.9947 5.9825 7.30548 6.05838 6.6341Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"copy-paste\", \"keywords\": [ \"clipboard\", \"copy\", \"cut\", \"paste\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186182)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.42417 0.269439C1.56667 0.0986972 1.77759 0 1.99998 0H6.99999C7.22238 0 7.4333 0.0986972 7.5758 0.269439C7.7183 0.440182 7.77767 0.665359 7.73789 0.884164L7.71407 1.01516C8.4411 1.11905 9 1.74425 9 2.5V3.25H7.25C5.59315 3.25 4.25 4.59315 4.25 6.25V11H1.5C0.671573 11 0 10.3284 0 9.5V2.5C0 1.74426 0.55889 1.11906 1.2859 1.01516L1.26208 0.884164C1.2223 0.665359 1.28167 0.440182 1.42417 0.269439ZM2.89864 1.5L3.01395 2.13416C3.07879 2.49078 3.38938 2.75 3.75185 2.75H5.24812C5.61059 2.75 5.92118 2.49078 5.98602 2.13416L6.10133 1.5H2.89864Z\\\" fill=\\\"black\\\"/>\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.25 4.5C6.2835 4.5 5.5 5.2835 5.5 6.25V12.25C5.5 13.2165 6.2835 14 7.25 14H12.25C13.2165 14 14 13.2165 14 12.25V6.25C14 5.2835 13.2165 4.5 12.25 4.5H7.25ZM7.875 8C7.875 7.65482 8.15482 7.375 8.5 7.375H11C11.3452 7.375 11.625 7.65482 11.625 8C11.625 8.34518 11.3452 8.625 11 8.625H8.5C8.15482 8.625 7.875 8.34518 7.875 8ZM8.5 9.875C8.15482 9.875 7.875 10.1548 7.875 10.5C7.875 10.8452 8.15482 11.125 8.5 11.125H11C11.3452 11.125 11.625 10.8452 11.625 10.5C11.625 10.1548 11.3452 9.875 11 9.875H8.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186182\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"creative-commons\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186217)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7ZM4.24387 5.625C4.03676 5.625 3.86887 5.79289 3.86887 6V8C3.86887 8.20711 4.03676 8.375 4.24387 8.375H4.91053C5.07283 8.375 5.21258 8.27155 5.26436 8.12506C5.37939 7.79961 5.73646 7.62903 6.06191 7.74407C6.38736 7.8591 6.55793 8.21617 6.4429 8.54162C6.22012 9.17192 5.61904 9.625 4.91053 9.625H4.24387C3.3464 9.625 2.61887 8.89746 2.61887 8V6C2.61887 5.10254 3.3464 4.375 4.24387 4.375H4.91053C5.61905 4.375 6.22013 4.82809 6.44291 5.4584C6.55794 5.78384 6.38736 6.14092 6.06191 6.25595C5.73646 6.37098 5.37938 6.2004 5.26436 5.87495C5.21258 5.72845 5.07283 5.625 4.91053 5.625H4.24387ZM8.77127 6C8.77127 5.79289 8.93916 5.625 9.14627 5.625H9.81294C9.97524 5.625 10.115 5.72845 10.1668 5.87495C10.2818 6.2004 10.6389 6.37098 10.9643 6.25595C11.2898 6.14092 11.4603 5.78384 11.3453 5.4584C11.1225 4.82809 10.5215 4.375 9.81294 4.375H9.14627C8.24881 4.375 7.52127 5.10254 7.52127 6V8C7.52127 8.89746 8.24881 9.625 9.14627 9.625H9.81294C10.5214 9.625 11.1225 9.17192 11.3453 8.54162C11.4603 8.21617 11.2898 7.8591 10.9643 7.74407C10.6389 7.62903 10.2818 7.79961 10.1668 8.12506C10.115 8.27155 9.97524 8.375 9.81294 8.375H9.14627C8.93916 8.375 8.77127 8.20711 8.77127 8V6Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186217\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"crop-selection\", \"keywords\": [ \"artboard\", \"crop\", \"design\", \"image\", \"picture\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186220)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.5 1C4.5 0.447715 4.05228 0 3.5 0C2.94772 0 2.5 0.447715 2.5 1V2.5H1C0.447715 2.5 0 2.94772 0 3.5C0 4.05228 0.447715 4.5 1 4.5H2.5V9C2.5 10.3807 3.61929 11.5 5 11.5H9.5V13C9.5 13.5523 9.94771 14 10.5 14C11.0523 14 11.5 13.5523 11.5 13V11.5H13C13.5523 11.5 14 11.0523 14 10.5C14 9.94771 13.5523 9.5 13 9.5H10.5H5C4.72386 9.5 4.5 9.27614 4.5 9V3.5V1ZM6 3.5C6 2.94772 6.44772 2.5 7 2.5H9C10.3807 2.5 11.5 3.61929 11.5 5V7C11.5 7.55228 11.0523 8 10.5 8C9.94772 8 9.5 7.55228 9.5 7V5C9.5 4.72386 9.27614 4.5 9 4.5H7C6.44772 4.5 6 4.05228 6 3.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186220\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"crown\", \"keywords\": [ \"reward\", \"social\", \"rating\", \"media\", \"queen\", \"vip\", \"king\", \"crown\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.40962 1.71327C7.31605 1.57961 7.16316 1.5 7 1.5C6.83684 1.5 6.68395 1.57961 6.59038 1.71327L3.43215 6.22504L0.853553 3.64645C0.710554 3.50345 0.495495 3.46067 0.308658 3.53806C0.121821 3.61545 0 3.79777 0 4V10.5C0 11.0304 0.210714 11.5391 0.585786 11.9142C0.960859 12.2893 1.46957 12.5 2 12.5H12C12.5304 12.5 13.0391 12.2893 13.4142 11.9142C13.7893 11.5391 14 11.0304 14 10.5V4C14 3.79777 13.8782 3.61545 13.6913 3.53806C13.5045 3.46067 13.2894 3.50345 13.1464 3.64645L10.5679 6.22504L7.40962 1.71327Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"customer-support-1\", \"keywords\": [ \"customer\", \"headset\", \"help\", \"microphone\", \"phone\", \"support\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186223)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.98656 1.50051C6.56826 1.49301 6.15261 1.56812 5.76339 1.72154C5.37418 1.87496 5.01907 2.10367 4.7184 2.39457C4.41773 2.68547 4.17741 3.03283 4.01122 3.41677C3.84574 3.79906 3.757 4.21012 3.75 4.6266V5V7.25063V9C3.75 9.55228 3.30228 10 2.75 10H1.5C0.671573 10 0 9.32843 0 8.5V6.5C0 5.67157 0.671573 5 1.5 5H2.25V4.62063V4.6093C2.2593 3.99368 2.39007 3.38593 2.63465 2.8209C2.87923 2.25587 3.23291 1.74465 3.6754 1.31654C4.11789 0.888422 4.64052 0.551829 5.21332 0.326043C5.78195 0.101901 6.38894 -0.00866919 7 0.000530311C7.61106 -0.00866919 8.21805 0.101901 8.78668 0.326043C9.35948 0.551829 9.88211 0.888422 10.3246 1.31654C10.7671 1.74465 11.1208 2.25587 11.3653 2.8209C11.6099 3.38593 11.7406 3.99368 11.7499 4.6093L11.7501 4.62063L11.75 5H12.5C13.3284 5 14 5.67157 14 6.5V8.5C14 9.32843 13.3284 10 12.5 10H11.75V10.5C11.75 11.2293 11.4603 11.9288 10.9445 12.4445C10.5022 12.8868 9.92479 13.1629 9.30926 13.2326C9.05247 13.6905 8.56238 14 8 14H6.5C5.67157 14 5 13.3284 5 12.5C5 11.6716 5.67157 11 6.5 11H8C8.54196 11 9.01679 11.2874 9.28038 11.7182C9.50711 11.666 9.71673 11.551 9.88388 11.3839C10.1183 11.1495 10.25 10.8315 10.25 10.5V4.62661C10.243 4.21013 10.1543 3.79906 9.98878 3.41677C9.82259 3.03283 9.58227 2.68547 9.2816 2.39457C8.98093 2.10367 8.62582 1.87496 8.23661 1.72154C7.84739 1.56812 7.43174 1.49301 7.01344 1.50051C7.00448 1.50067 6.99552 1.50067 6.98656 1.50051Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186223\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cut\", \"keywords\": [ \"coupon\", \"cut\", \"discount\", \"price\", \"prices\", \"scissors\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186226)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3 0C1.34315 0 0 1.34315 0 3C0 4.2215 0.730026 5.27248 1.77758 5.74047L3.89248 7L1.77758 8.25953C0.730027 8.72751 0 9.7785 0 11C0 12.6569 1.34315 14 3 14C4.65685 14 6 12.6569 6 11C6 9.88349 5.39007 8.90943 4.48522 8.39285L5.35823 7.87293L6.65359 8.64438C7.00947 8.85633 7.46979 8.73964 7.68173 8.38376C7.89368 8.02788 7.77699 7.56756 7.42111 7.35562L6.82399 7L13.6338 2.94443C13.9896 2.73248 14.1063 2.27217 13.8944 1.91629C13.6824 1.56041 13.2221 1.44372 12.8662 1.65567L5.35823 6.12707L4.48522 5.60715C5.39007 5.09057 6 4.11651 6 3C6 1.34315 4.65685 0 3 0ZM2.4544 4.39769C2.62349 4.46374 2.80751 4.5 3 4.5C3.82843 4.5 4.5 3.82843 4.5 3C4.5 2.17157 3.82843 1.5 3 1.5C2.17157 1.5 1.5 2.17157 1.5 3C1.5 3.54458 1.79021 4.02138 2.22441 4.28418C2.27899 4.30134 2.33246 4.32507 2.38376 4.35562L2.4544 4.39769ZM1.5 11C1.5 10.4554 1.79021 9.97862 2.22441 9.71582C2.27899 9.69866 2.33246 9.67493 2.38376 9.64438L2.45439 9.60232C2.62349 9.53626 2.80751 9.5 3 9.5C3.82843 9.5 4.5 10.1716 4.5 11C4.5 11.8284 3.82843 12.5 3 12.5C2.17157 12.5 1.5 11.8284 1.5 11ZM7.5 9.75C7.08579 9.75 6.75 10.0858 6.75 10.5C6.75 10.9142 7.08579 11.25 7.5 11.25H9C9.41421 11.25 9.75 10.9142 9.75 10.5C9.75 10.0858 9.41421 9.75 9 9.75H7.5ZM11 10.5C11 10.0858 11.3358 9.75 11.75 9.75H13.25C13.6642 9.75 14 10.0858 14 10.5C14 10.9142 13.6642 11.25 13.25 11.25H11.75C11.3358 11.25 11 10.9142 11 10.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186226\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"dark-dislay-mode\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186253)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7ZM7.00076 6.98135C7.00076 5.6232 8.10176 4.5222 9.45992 4.5222C9.71021 4.5222 9.90136 4.2213 9.71786 4.05107C9.00485 3.38963 8.05003 2.98523 7.00076 2.98523C4.79376 2.98523 3.00464 4.77436 3.00464 6.98135C3.00464 9.18835 4.79376 10.9775 7.00076 10.9775C8.05003 10.9775 9.00485 10.5731 9.71786 9.91163C9.90136 9.74141 9.71021 9.44051 9.45992 9.44051C8.10176 9.44051 7.00076 8.33951 7.00076 6.98135Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186253\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"dashboard-3\", \"keywords\": [ \"app\", \"application\", \"dashboard\", \"home\", \"layout\", \"vertical\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186235)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1 0C0.447715 0 0 0.447715 0 1V7C0 7.55229 0.447715 8 1 8H5C5.55229 8 6 7.55229 6 7V1C6 0.447715 5.55229 0 5 0H1ZM8 1C8 0.447715 8.44771 0 9 0H13C13.5523 0 14 0.447715 14 1V3.01C14 3.56228 13.5523 4.01 13 4.01H9C8.44771 4.01 8 3.56228 8 3.01V1ZM8 7C8 6.44772 8.44771 6 9 6H13C13.5523 6 14 6.44772 14 7V13C14 13.5523 13.5523 14 13 14H9C8.44772 14 8 13.5523 8 13V7ZM0 10.99C0 10.4377 0.447715 9.98999 1 9.98999H5C5.55229 9.98999 6 10.4377 6 10.99V13C6 13.5523 5.55229 14 5 14H1C0.447715 14 0 13.5523 0 13V10.99Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186235\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"dashboard-circle\", \"keywords\": [ \"app\", \"application\", \"dashboard\", \"home\", \"layout\", \"circle\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186232)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6 3C6 4.65685 4.65685 6 3 6C1.34315 6 0 4.65685 0 3C0 1.34315 1.34315 0 3 0C4.65685 0 6 1.34315 6 3ZM14 3C14 4.65685 12.6569 6 11 6C9.34315 6 8 4.65685 8 3C8 1.34315 9.34315 0 11 0C12.6569 0 14 1.34315 14 3ZM3 14C4.65685 14 6 12.6569 6 11C6 9.34315 4.65685 8 3 8C1.34315 8 0 9.34315 0 11C0 12.6569 1.34315 14 3 14ZM14 11C14 12.6569 12.6569 14 11 14C9.34315 14 8 12.6569 8 11C8 9.34315 9.34315 8 11 8C12.6569 8 14 9.34315 14 11Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186232\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"delete-1\", \"keywords\": [ \"remove\", \"add\", \"button\", \"buttons\", \"delete\", \"cross\", \"x\", \"mathematics\", \"multiply\", \"math\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186250)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.70711 0.292893C1.31658 -0.097631 0.683417 -0.097631 0.292893 0.292893C-0.097631 0.683417 -0.097631 1.31658 0.292893 1.70711L5.58579 7L0.292893 12.2929C-0.097631 12.6834 -0.097631 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L7 8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166 14.0976 12.6834 13.7071 12.2929L8.41421 7L13.7071 1.70711C14.0976 1.31658 14.0976 0.683417 13.7071 0.292893C13.3166 -0.097631 12.6834 -0.097631 12.2929 0.292893L7 5.58579L1.70711 0.292893Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186250\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"descending-number-order\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186247)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.0784 2.84375H10.4221C10.0511 2.84375 9.75025 2.54294 9.75025 2.17188C9.75025 1.80081 10.0511 1.50001 10.4221 1.50001L11.0783 1.5C11.4478 1.5 11.7476 1.79819 11.7502 2.16702V2.17672C11.7476 2.54556 11.4478 2.84374 11.0784 2.84375ZM11.0783 0C12.1888 -8.77481e-06 13.1047 0.833409 13.2345 1.90889C13.2448 1.95847 13.2502 2.00985 13.2502 2.06249V2.16314V2.17188V2.1806V4.68749C13.2502 5.82658 12.3268 6.74999 11.1877 6.74999H10.3127C9.41352 6.74999 8.65055 6.17498 8.36777 5.37492C8.22974 4.98439 8.43443 4.55589 8.82497 4.41786C9.21551 4.27982 9.644 4.48452 9.78204 4.87506C9.85961 5.09454 10.069 5.24999 10.3127 5.24999H11.1877C11.4984 5.24999 11.7502 4.99815 11.7502 4.68749V4.23783C11.5386 4.30658 11.3128 4.34374 11.0784 4.34375H10.4221C9.22263 4.34375 8.25024 3.37137 8.25025 2.17187C8.25025 0.972389 9.22262 1.46881e-05 10.4221 5.07519e-06L11.0783 0ZM11.9168 8.24999C11.9168 7.83578 11.581 7.49999 11.1668 7.49999C10.7526 7.49999 10.4168 7.83578 10.4168 8.24999C10.4168 8.52613 10.1929 8.74999 9.91679 8.74999H9.50012C9.08591 8.74999 8.75012 9.08578 8.75012 9.49999C8.75012 9.9142 9.08591 10.25 9.50012 10.25H9.91679C10.0894 10.25 10.257 10.2281 10.4168 10.187V12.5H9.50012C9.08591 12.5 8.75012 12.8358 8.75012 13.25C8.75012 13.6642 9.08591 14 9.50012 14H12.8335C13.2477 14 13.5835 13.6642 13.5835 13.25C13.5835 12.8358 13.2477 12.5 12.8335 12.5H11.9168V8.24999ZM3.75002 0C4.30231 0 4.75002 0.447715 4.75002 1V10H6.25002C6.55337 10 6.82685 10.1827 6.94293 10.463C7.05902 10.7432 6.99485 11.0658 6.78035 11.2803L4.28035 13.7803C3.98746 14.0732 3.51259 14.0732 3.21969 13.7803L0.719692 11.2803C0.505193 11.0658 0.441027 10.7432 0.557112 10.463C0.673198 10.1827 0.946675 10 1.25002 10H2.75002V1C2.75002 0.447715 3.19774 0 3.75002 0Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186247\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"disable-bell-notification\", \"keywords\": [ \"disable\", \"silent\", \"notification\", \"off\", \"silence\", \"alarm\", \"bell\", \"alert\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186241)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.297795 1.35846C0.00490166 1.06556 0.00490166 0.590689 0.297795 0.297795C0.590688 0.0049018 1.06557 0.0049018 1.35846 0.297795L3.0795 2.01884C3.21079 1.84741 3.35389 1.68388 3.50825 1.52951C4.43443 0.60333 5.6906 0.0830078 7.00041 0.0830078C8.31023 0.0830078 9.5664 0.60333 10.4925 1.52951C11.4187 2.45569 11.939 3.71185 11.939 5.02167V9.43076C11.939 9.7396 12.0617 10.0358 12.2801 10.2542C12.528 10.502 12.7227 10.5953 12.995 10.5953C13.2711 10.5953 13.495 10.8191 13.495 11.0953C13.495 11.3714 13.2711 11.5953 12.995 11.5953H12.6559L13.703 12.6424C13.9959 12.9353 13.9959 13.4102 13.703 13.7031C13.4102 13.996 12.9353 13.996 12.6424 13.7031L0.297795 1.35846ZM2.0636 4.88668L8.77217 11.5953H1.00586C0.729714 11.5953 0.505857 11.3714 0.505857 11.0953C0.505857 10.8191 0.729714 10.5953 1.00586 10.5953C1.27816 10.5953 1.47283 10.5021 1.72068 10.2542C1.93907 10.0358 2.06176 9.7396 2.06176 9.43076V5.02167C2.06176 4.9766 2.06237 4.9316 2.0636 4.88668ZM5.25134 13.1819C5.25134 12.7677 5.58713 12.4319 6.00134 12.4319H7.99953C8.41375 12.4319 8.74953 12.7677 8.74953 13.1819C8.74953 13.5961 8.41375 13.9319 7.99953 13.9319H6.00134C5.58713 13.9319 5.25134 13.5961 5.25134 13.1819Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186241\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"disable-heart\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186271)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.28033 0.21967C0.987437 -0.0732233 0.512563 -0.0732233 0.21967 0.21967C-0.0732233 0.512563 -0.0732233 0.987437 0.21967 1.28033L12.7197 13.7803C13.0126 14.0732 13.4874 14.0732 13.7803 13.7803C14.0732 13.4874 14.0732 13.0126 13.7803 12.7197L10.5328 9.47217L12.1847 7.94213L12.1942 7.9331C13.7892 6.37605 13.412 4.10771 12.1628 2.85393C11.5272 2.216 10.644 1.81042 9.64346 1.88038C8.77009 1.94144 7.86473 2.36031 7.00001 3.20061C6.1353 2.36031 5.22993 1.94144 4.35657 1.88038C3.91324 1.84938 3.49293 1.91175 3.10699 2.04633L1.28033 0.21967ZM1.80584 7.9331C0.59068 6.74687 0.520242 5.14781 1.10797 3.89904L8.54185 11.3163L8.02009 11.7996C7.73274 12.0666 7.36512 12.2004 6.99956 12.2007C6.63401 12.2004 6.26728 12.0666 5.97993 11.7996L1.80584 7.9331Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186271\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"division-circle\", \"keywords\": [ \"interface\", \"math\", \"divided\", \"by\", \"sign\", \"mathematics\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186259)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.25 7C0.25 3.27208 3.27208 0.25 7 0.25C10.7279 0.25 13.75 3.27208 13.75 7C13.75 10.7279 10.7279 13.75 7 13.75C3.27208 13.75 0.25 10.7279 0.25 7ZM4.04358 6.38062C3.6984 6.38062 3.41858 6.66044 3.41858 7.00562C3.41858 7.35079 3.6984 7.63062 4.04358 7.63062H10.0208C10.3659 7.63062 10.6458 7.35079 10.6458 7.00562C10.6458 6.66044 10.3659 6.38062 10.0208 6.38062H4.04358ZM7 5.01099C6.51675 5.01099 6.125 4.61924 6.125 4.13599C6.125 3.65274 6.51675 3.26099 7 3.26099C7.48325 3.26099 7.87501 3.65274 7.87501 4.13599C7.87501 4.61924 7.48325 5.01099 7 5.01099ZM7 10.7502C6.51675 10.7502 6.125 10.3585 6.125 9.87524C6.125 9.392 6.51675 9.00024 7 9.00024C7.48325 9.00024 7.87501 9.392 7.87501 9.87524C7.87501 10.3585 7.48325 10.7502 7 10.7502Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186259\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"download-box-1\", \"keywords\": [ \"arrow\", \"box\", \"down\", \"download\", \"internet\", \"network\", \"server\", \"upload\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186256)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.375 0H3.38261C2.83785 0 2.33592 0.295338 2.07137 0.771536L0.347222 3.875H6.375V0ZM0 12.5V5.125H7H14V12.5C14 13.3284 13.3284 14 12.5 14H1.5C0.671573 14 0 13.3284 0 12.5ZM13.6528 3.875H7.625V0H10.6174C11.1621 0 11.6641 0.295338 11.9286 0.771536L13.6528 3.875ZM4.35356 10.3537L6.64646 12.6466C6.84172 12.8418 7.1583 12.8418 7.35356 12.6466L9.64646 10.3537C9.96144 10.0387 9.73836 9.50012 9.2929 9.50012H8.00001V7.50012C8.00001 6.94784 7.5523 6.50012 7.00001 6.50012C6.44773 6.50012 6.00001 6.94784 6.00001 7.50012V9.50012H4.70712C4.26167 9.50012 4.03858 10.0387 4.35356 10.3537Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186256\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"download-circle\", \"keywords\": [ \"arrow\", \"circle\", \"down\", \"download\", \"internet\", \"network\", \"server\", \"upload\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186262)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14ZM10.1464 8.31042L7.3535 11.1033C7.15824 11.2986 6.84166 11.2986 6.6464 11.1033L3.8535 8.31042C3.53852 7.99543 3.76161 7.45686 4.20706 7.45686H5.99995V3.75C5.99995 3.19772 6.44767 2.75 6.99995 2.75C7.55224 2.75 7.99995 3.19772 7.99995 3.75V7.45686H9.79284C10.2383 7.45686 10.4614 7.99543 10.1464 8.31042Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186262\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"download-computer\", \"keywords\": [ \"action\", \"actions\", \"computer\", \"desktop\", \"device\", \"display\", \"download\", \"monitor\", \"screen\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186274)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.64542 5.0609L7.35252 7.3538C7.15726 7.54906 6.84068 7.54906 6.64542 7.3538L4.35252 5.0609C4.03754 4.74592 4.26063 4.20735 4.70608 4.20735H5.99897V1.00049C5.99897 0.448204 6.44669 0.000488281 6.99897 0.000488281C7.55126 0.000488281 7.99897 0.448204 7.99897 1.00049V4.20735H9.29186C9.73732 4.20735 9.9604 4.74592 9.64542 5.0609ZM2.06054 2.91016H2.99804C3.55033 2.91016 3.99804 2.46244 3.99804 1.91016C3.99804 1.35787 3.55033 0.910156 2.99804 0.910156H1.51734C0.712776 0.910156 0.0605469 1.56239 0.0605469 2.36695V9.67567C0.0605469 10.4802 0.712776 11.1325 1.51734 11.1325H5.43453L4.94684 12.4951H3.99902C3.58481 12.4951 3.24902 12.8309 3.24902 13.2451C3.24902 13.6593 3.58481 13.9951 3.99902 13.9951H9.99902C10.4132 13.9951 10.749 13.6593 10.749 13.2451C10.749 12.8309 10.4132 12.4951 9.99902 12.4951H9.05111L8.56342 11.1325H12.4804C13.285 11.1325 13.9372 10.4802 13.9372 9.67567V2.36695C13.9372 1.56239 13.285 0.910156 12.4804 0.910156H10.998C10.4457 0.910156 9.99804 1.35787 9.99804 1.91016C9.99804 2.46244 10.4457 2.91016 10.998 2.91016H11.9372V9.13246H2.06054V2.91016Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186274\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"download-file\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.43934 0.43934C1.72064 0.158035 2.10218 0 2.5 0H8.5C8.63261 0 8.75979 0.0526784 8.85355 0.146447L12.8536 4.14645C12.9473 4.24021 13 4.36739 13 4.5V12.5C13 12.8978 12.842 13.2794 12.5607 13.5607C12.2794 13.842 11.8978 14 11.5 14H2.5C2.10217 14 1.72064 13.842 1.43934 13.5607C1.15804 13.2794 1 12.8978 1 12.5V1.5C1 1.10218 1.15804 0.720644 1.43934 0.43934ZM7.18695 10.9419L7.29089 10.838C7.40736 10.762 7.507 10.6623 7.58303 10.5459L9.43695 8.69194C9.6157 8.51319 9.66917 8.24437 9.57244 8.01082C9.4757 7.77728 9.2478 7.625 8.99501 7.625H7.74502V4C7.74502 3.44772 7.2973 3 6.74502 3C6.19273 3 5.74502 3.44772 5.74502 4V7.625H4.49501C4.24222 7.625 4.01432 7.77728 3.91759 8.01082C3.82085 8.24437 3.87432 8.51319 4.05307 8.69194L5.90703 10.5459C5.98304 10.6623 6.08267 10.762 6.19911 10.838L6.30307 10.9419C6.42028 11.0592 6.57925 11.125 6.74501 11.125C6.91077 11.125 7.06974 11.0592 7.18695 10.9419Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"empty-clipboard\", \"keywords\": [ \"work\", \"plain\", \"clipboard\", \"task\", \"list\", \"company\", \"office\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.5 0C4.94772 0 4.5 0.447716 4.5 1V1.5C4.5 2.05229 4.94772 2.5 5.5 2.5H8.5C9.05229 2.5 9.5 2.05229 9.5 1.5V1C9.5 0.447715 9.05229 0 8.5 0H5.5ZM2.75 1H3.25V1.5C3.25 2.74264 4.25736 3.75 5.5 3.75H8.5C9.74264 3.75 10.75 2.74264 10.75 1.5V1H11.25C12.0784 1 12.75 1.67157 12.75 2.5V12.5C12.75 13.3284 12.0784 14 11.25 14H2.75C1.92157 14 1.25 13.3284 1.25 12.5V2.5C1.25 1.67157 1.92157 1 2.75 1Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"equal-sign\", \"keywords\": [ \"interface\", \"math\", \"equal\", \"sign\", \"mathematics\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1 3.5C0.447715 3.5 0 3.94772 0 4.5C0 5.05228 0.447715 5.5 1 5.5H13C13.5523 5.5 14 5.05228 14 4.5C14 3.94772 13.5523 3.5 13 3.5H1ZM1 8.5C0.447715 8.5 0 8.94772 0 9.5C0 10.0523 0.447715 10.5 1 10.5H13C13.5523 10.5 14 10.0523 14 9.5C14 8.94772 13.5523 8.5 13 8.5H1Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"expand\", \"keywords\": [ \"big\", \"bigger\", \"design\", \"expand\", \"larger\", \"resize\", \"size\", \"square\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186283)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 1.75C1.5 1.61193 1.61193 1.5 1.75 1.5H2.25C2.66421 1.5 3 1.16421 3 0.75C3 0.335786 2.66421 0 2.25 0H1.75C0.783502 0 0 0.783502 0 1.75V2.25C0 2.66421 0.335786 3 0.75 3C1.16421 3 1.5 2.66421 1.5 2.25V1.75ZM11 0.75C11 0.335786 11.3358 0 11.75 0H12.25C13.2165 0 14 0.783502 14 1.75V2.25C14 2.66421 13.6642 3 13.25 3C12.8358 3 12.5 2.66421 12.5 2.25V1.75C12.5 1.61193 12.3881 1.5 12.25 1.5H11.75C11.3358 1.5 11 1.16421 11 0.75ZM3.49997 4.99951C3.49997 4.17108 4.17154 3.49951 4.99997 3.49951H8.99997C9.8284 3.49951 10.5 4.17108 10.5 4.99951V8.99951C10.5 9.82794 9.8284 10.4995 8.99997 10.4995H4.99997C4.17154 10.4995 3.49997 9.82794 3.49997 8.99951V4.99951ZM13.25 11C13.6642 11 14 11.3358 14 11.75V12.25C14 13.2165 13.2165 14 12.25 14H11.75C11.3358 14 11 13.6642 11 13.25C11 12.8358 11.3358 12.5 11.75 12.5H12.25C12.3881 12.5 12.5 12.3881 12.5 12.25V11.75C12.5 11.3358 12.8358 11 13.25 11ZM1.50012 11.75C1.50012 11.3358 1.16434 11 0.750122 11C0.335909 11 0.00012207 11.3358 0.00012207 11.75V12.25C0.00012207 13.2165 0.783624 14 1.75012 14H2.25012C2.66434 14 3.00012 13.6642 3.00012 13.25C3.00012 12.8358 2.66434 12.5 2.25012 12.5H1.75012C1.61205 12.5 1.50012 12.3881 1.50012 12.25V11.75ZM5.25 13.25C5.25 12.8358 5.58579 12.5 6 12.5H8C8.41421 12.5 8.75 12.8358 8.75 13.25C8.75 13.6642 8.41421 14 8 14H6C5.58579 14 5.25 13.6642 5.25 13.25ZM6 0C5.58579 0 5.25 0.335786 5.25 0.75C5.25 1.16421 5.58579 1.5 6 1.5H8C8.41421 1.5 8.75 1.16421 8.75 0.75C8.75 0.335786 8.41421 0 8 0H6ZM0.75 5.25C1.16421 5.25 1.5 5.58579 1.5 6V8C1.5 8.41421 1.16421 8.75 0.75 8.75C0.335786 8.75 0 8.41421 0 8V6C0 5.58579 0.335786 5.25 0.75 5.25ZM14 6C14 5.58579 13.6642 5.25 13.25 5.25C12.8358 5.25 12.5 5.58579 12.5 6V8C12.5 8.41421 12.8358 8.75 13.25 8.75C13.6642 8.75 14 8.41421 14 8V6Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186283\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"expand-horizontal-1\", \"keywords\": [ \"expand\", \"resize\", \"bigger\", \"horizontal\", \"smaller\", \"size\", \"arrow\", \"arrows\", \"big\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186280)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.00098 0C7.41519 0 7.75098 0.335786 7.75098 0.75V13.25C7.75098 13.6642 7.41519 14 7.00098 14C6.58677 14 6.25098 13.6642 6.25098 13.25V0.75C6.25098 0.335786 6.58677 0 7.00098 0ZM3.67235 3.40818C3.85918 3.48557 3.98101 3.66789 3.98101 3.87012V10.1301C3.98101 10.3323 3.85918 10.5147 3.67235 10.5921C3.48551 10.6694 3.27045 10.6267 3.12745 10.4837L0.260457 7.61668C0.178842 7.53732 0.113781 7.44252 0.0690662 7.33779C0.0234939 7.23105 0 7.11618 0 7.00012C0 6.88405 0.0234939 6.76919 0.0690662 6.66244C0.113781 6.55771 0.178842 6.46292 0.260457 6.38356L3.12745 3.51656C3.27045 3.37356 3.48551 3.33079 3.67235 3.40818ZM10.3297 3.40818C10.5165 3.33079 10.7316 3.37356 10.8746 3.51656L13.7416 6.38356C13.8232 6.46292 13.8882 6.55771 13.933 6.66244C13.9785 6.76919 14.002 6.88405 14.002 7.00012C14.002 7.11618 13.9785 7.23105 13.933 7.33779C13.8882 7.44252 13.8232 7.53732 13.7416 7.61668L10.8746 10.4837C10.7316 10.6267 10.5165 10.6694 10.3297 10.5921C10.1428 10.5147 10.021 10.3323 10.021 10.1301V3.87012C10.021 3.66789 10.1428 3.48557 10.3297 3.40818Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186280\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"expand-window-2\", \"keywords\": [ \"expand\", \"small\", \"bigger\", \"retract\", \"smaller\", \"big\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186295)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H11.5C11.7761 12 12 11.7761 12 11.5V7C12 6.44771 12.4477 6 13 6C13.5523 6 14 6.44771 14 7V11.5C14 12.8807 12.8807 14 11.5 14H2.5C1.11929 14 9.07513e-07 12.8807 7.86803e-07 11.5L0 2.5C-1.20706e-07 1.11929 1.11929 1.08236e-06 2.5 9.61653e-07L7 5.68251e-07C7.55228 5.19968e-07 8 0.447716 8 1C8 1.55228 7.55228 2 7 2ZM9.53806 0.308658C9.61545 0.121821 9.79777 3.23662e-07 10 3.05982e-07L13.5 0C13.7761 -2.414e-08 14 0.223857 14 0.499999V4C14 4.20223 13.8782 4.38455 13.6913 4.46194C13.5045 4.53933 13.2894 4.49655 13.1464 4.35355L12.1036 3.31066L7.70711 7.70711C7.31658 8.09763 6.68342 8.09763 6.29289 7.70711C5.90237 7.31658 5.90237 6.68342 6.29289 6.29289L10.6893 1.89645L9.64645 0.853553C9.50345 0.710554 9.46067 0.495495 9.53806 0.308658Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186295\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"face-scan-1\", \"keywords\": [ \"identification\", \"angle\", \"secure\", \"human\", \"id\", \"person\", \"face\", \"security\", \"brackets\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186289)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.75 1.5C1.6837 1.5 1.62011 1.52634 1.57322 1.57322C1.52634 1.62011 1.5 1.6837 1.5 1.75V3.25C1.5 3.66421 1.16421 4 0.75 4C0.335786 4 0 3.66421 0 3.25V1.75C0 1.28587 0.184375 0.840752 0.512563 0.512563C0.840752 0.184375 1.28587 0 1.75 0H3.25C3.66421 0 4 0.335786 4 0.75C4 1.16421 3.66421 1.5 3.25 1.5H1.75ZM7.75 4.5C7.75 4.08579 7.41421 3.75 7 3.75C6.58579 3.75 6.25 4.08579 6.25 4.5V7.25H5.5C5.08579 7.25 4.75 7.58579 4.75 8C4.75 8.41421 5.08579 8.75 5.5 8.75H7C7.41421 8.75 7.75 8.41421 7.75 8V4.5ZM14 10.75C14 10.3358 13.6642 10 13.25 10C12.8358 10 12.5 10.3358 12.5 10.75V12.25C12.5 12.3163 12.4737 12.3799 12.4268 12.4268C12.3799 12.4737 12.3163 12.5 12.25 12.5H10.75C10.3358 12.5 10 12.8358 10 13.25C10 13.6642 10.3358 14 10.75 14H12.25C12.7141 14 13.1592 13.8156 13.4874 13.4874C13.8156 13.1592 14 12.7141 14 12.25V10.75ZM1.5 10.75C1.5 10.3358 1.16421 10 0.75 10C0.335786 10 0 10.3358 0 10.75V12.25C0 12.7141 0.184375 13.1592 0.512563 13.4874C0.840752 13.8156 1.28587 14 1.75 14H3.25C3.66421 14 4 13.6642 4 13.25C4 12.8358 3.66421 12.5 3.25 12.5H1.75C1.6837 12.5 1.62011 12.4737 1.57322 12.4268C1.52634 12.3799 1.5 12.3163 1.5 12.25V10.75ZM10 0.75C10 0.335786 10.3358 0 10.75 0H12.25C12.7141 0 13.1592 0.184375 13.4874 0.512563C13.8156 0.840752 14 1.28587 14 1.75V3.25C14 3.66421 13.6642 4 13.25 4C12.8358 4 12.5 3.66421 12.5 3.25V1.75C12.5 1.6837 12.4737 1.62011 12.4268 1.57322C12.3799 1.52634 12.3163 1.5 12.25 1.5H10.75C10.3358 1.5 10 1.16421 10 0.75ZM5.01653 9.45622C4.71621 9.17095 4.24149 9.18315 3.95622 9.48347C3.67095 9.78379 3.68315 10.2585 3.98347 10.5438C4.79747 11.317 5.8773 11.7481 7 11.7481C8.1227 11.7481 9.20253 11.317 10.0165 10.5438C10.3169 10.2585 10.3291 9.78379 10.0438 9.48347C9.75851 9.18315 9.28379 9.17095 8.98347 9.45622C8.44824 9.96463 7.73821 10.2481 7 10.2481C6.26179 10.2481 5.55176 9.96463 5.01653 9.45622ZM5 4.5C5 5.05228 4.55228 5.5 4 5.5C3.44772 5.5 3 5.05228 3 4.5C3 3.94772 3.44772 3.5 4 3.5C4.55228 3.5 5 3.94772 5 4.5ZM10 5.5C10.5523 5.5 11 5.05228 11 4.5C11 3.94772 10.5523 3.5 10 3.5C9.44771 3.5 9 3.94772 9 4.5C9 5.05228 9.44771 5.5 10 5.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186289\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"factorial\", \"keywords\": [ \"interface\", \"math\", \"number\", \"factorial\", \"sign\", \"mathematics\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186286)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.0786 0.078125C12.6309 0.078125 13.0786 0.52584 13.0786 1.07813V8.93738C13.0786 9.48966 12.6309 9.93738 12.0786 9.93738C11.5263 9.93738 11.0786 9.48966 11.0786 8.93738V1.07813C11.0786 0.52584 11.5263 0.078125 12.0786 0.078125ZM5.15407 3.38351C3.66382 3.38351 2.46094 4.5871 2.46094 6.0654V6.07664V12.9689C2.46094 13.5212 2.01322 13.9689 1.46094 13.9689C0.908652 13.9689 0.460938 13.5212 0.460938 12.9689V6.07664V6.0654V2.38351C0.460938 1.83122 0.908652 1.38351 1.46094 1.38351C1.96383 1.38351 2.38001 1.75472 2.45044 2.23808C3.21506 1.69962 4.14794 1.38351 5.15407 1.38351C7.74314 1.38351 9.8472 3.47679 9.8472 6.0654V12.9689C9.8472 13.5212 9.39948 13.9689 8.8472 13.9689C8.29491 13.9689 7.8472 13.5212 7.8472 12.9689V6.0654C7.8472 4.5871 6.64432 3.38351 5.15407 3.38351ZM10.617 12.4611C10.617 11.6539 11.2714 10.9995 12.0786 10.9995C12.8858 10.9995 13.5402 11.6539 13.5402 12.4611C13.5402 13.2684 12.8858 13.9228 12.0786 13.9228C11.2714 13.9228 10.617 13.2684 10.617 12.4611Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186286\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"fahrenheit\", \"keywords\": [ \"degrees\", \"temperature\", \"fahrenheit\", \"degree\", \"weather\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186292)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.25 1.5C1.83579 1.5 1.5 1.83579 1.5 2.25C1.5 2.66421 1.83579 3 2.25 3C2.66421 3 3 2.66421 3 2.25C3 1.83579 2.66421 1.5 2.25 1.5ZM0 2.25C0 1.00736 1.00736 0 2.25 0C3.49264 0 4.5 1.00736 4.5 2.25C4.5 3.49264 3.49264 4.5 2.25 4.5C1.00736 4.5 0 3.49264 0 2.25ZM6.00003 1C6.00003 0.447715 6.44775 0 7.00003 0H13C13.5523 0 14 0.447715 14 1C14 1.55228 13.5523 2 13 2H8.00003V5.5H12C12.5523 5.5 13 5.94772 13 6.5C13 7.05228 12.5523 7.5 12 7.5H8.00003V13C8.00003 13.5523 7.55231 14 7.00003 14C6.44775 14 6.00003 13.5523 6.00003 13V1Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186292\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"fastforward-clock\", \"keywords\": [ \"time\", \"clock\", \"reset\", \"stopwatch\", \"circle\", \"measure\", \"loading\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186298)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 2.75C7.41421 2.75 7.75 3.08579 7.75 3.5V7.07536L9.88587 8.35688C10.2411 8.56999 10.3562 9.03069 10.1431 9.38587C9.93001 9.74106 9.46931 9.85623 9.11413 9.64312L6.61413 8.14312C6.38822 8.00758 6.25 7.76345 6.25 7.5V3.5C6.25 3.08579 6.58579 2.75 7 2.75Z\\\" fill=\\\"black\\\"/>\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 7C1.5 3.96243 3.96243 1.5 7 1.5C8.75084 1.5 10.3115 2.31796 11.3193 3.59456L10.3536 4.56034C10.0386 4.87533 10.2617 5.4139 10.7071 5.4139H13.5C13.7761 5.4139 14 5.19004 14 4.9139V2.121C14 1.67555 13.4614 1.45247 13.1464 1.76745L12.3856 2.52829C11.1022 0.984301 9.16644 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14C10.311 14 13.0834 11.702 13.8127 8.61473C13.9079 8.21161 13.6583 7.80762 13.2552 7.7124C12.8521 7.61717 12.4481 7.86677 12.3528 8.26989C11.7799 10.6955 9.59975 12.5 7 12.5C3.96243 12.5 1.5 10.0376 1.5 7Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186298\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"file-add-alternate\", \"keywords\": [ \"file\", \"common\", \"add\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186305)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.5 0C3.10218 0 2.72064 0.158035 2.43934 0.43934C2.15804 0.720644 2 1.10218 2 1.5V6.68868C2.3424 6.41419 2.77704 6.25 3.25003 6.25C4.3546 6.25 5.25003 7.14543 5.25003 8.25V8.75H5.75003C6.8546 8.75 7.75003 9.64543 7.75003 10.75C7.75003 11.8546 6.8546 12.75 5.75003 12.75H5.25003V13.25C5.25003 13.5152 5.1984 13.7684 5.10465 14H12.5C12.8978 14 13.2794 13.842 13.5607 13.5607C13.842 13.2794 14 12.8978 14 12.5V4.5C14 4.36739 13.9473 4.24021 13.8536 4.14645L9.85355 0.146447C9.75978 0.0526784 9.63261 0 9.5 0H3.5ZM4 8.25C4 7.83579 3.66421 7.5 3.25 7.5C2.83579 7.5 2.5 7.83579 2.5 8.25V10H0.75C0.335786 10 0 10.3358 0 10.75C0 11.1642 0.335786 11.5 0.75 11.5H2.5V13.25C2.5 13.6642 2.83579 14 3.25 14C3.66421 14 4 13.6642 4 13.25V11.5H5.75C6.16421 11.5 6.5 11.1642 6.5 10.75C6.5 10.3358 6.16421 10 5.75 10H4V8.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186305\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"file-delete-alternate\", \"keywords\": [ \"file\", \"common\", \"delete\", \"cross\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186302)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.5 0C3.10218 0 2.72064 0.158035 2.43934 0.43934C2.15804 0.720644 2 1.10218 2 1.5V7.44851C2.05675 7.49406 2.1116 7.54315 2.16424 7.5958L2.87003 8.30158L3.57582 7.5958C4.35687 6.81475 5.62319 6.81475 6.40424 7.5958C7.18529 8.37684 7.18529 9.64318 6.40424 10.4242L5.69846 11.13L6.40424 11.8358C6.98932 12.4209 7.13612 13.2783 6.84464 14H12.5C12.8978 14 13.2794 13.842 13.5607 13.5607C13.842 13.2794 14 12.8978 14 12.5V4.5C14 4.36739 13.9473 4.24021 13.8536 4.14645L9.85355 0.146447C9.75978 0.0526784 9.63261 0 9.5 0H3.5ZM5.52033 8.47968C5.81322 8.77257 5.81322 9.24745 5.52033 9.54034L3.93066 11.13L5.52033 12.7197C5.81322 13.0126 5.81322 13.4874 5.52033 13.7803C5.22744 14.0732 4.75256 14.0732 4.45967 13.7803L2.87 12.1907L1.28033 13.7803C0.987437 14.0732 0.512563 14.0732 0.21967 13.7803C-0.0732233 13.4874 -0.0732233 13.0126 0.21967 12.7197L1.80934 11.13L0.21967 9.54034C-0.0732233 9.24745 -0.0732233 8.77257 0.21967 8.47968C0.512563 8.18679 0.987437 8.18679 1.28033 8.47968L2.87 10.0693L4.45967 8.47968C4.75256 8.18679 5.22744 8.18679 5.52033 8.47968Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186302\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"file-remove-alternate\", \"keywords\": [ \"file\", \"common\", \"remove\", \"minus\", \"subtract\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186308)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.5 0C3.10218 0 2.72064 0.158035 2.43934 0.43934C2.15804 0.720644 2 1.10218 2 1.5V8.24951H5.75003C6.8546 8.24951 7.75003 9.14494 7.75003 10.2495C7.75003 11.3541 6.8546 12.2495 5.75003 12.2495H2V12.5C2 12.8978 2.15804 13.2794 2.43934 13.5607C2.72064 13.842 3.10217 14 3.5 14H12.5C12.8978 14 13.2794 13.842 13.5607 13.5607C13.842 13.2794 14 12.8978 14 12.5V4.5C14 4.36739 13.9473 4.24021 13.8536 4.14645L9.85355 0.146447C9.75978 0.0526784 9.63261 0 9.5 0H3.5ZM6.5 10.2495C6.5 9.8353 6.16421 9.49951 5.75 9.49951H0.75C0.335786 9.49951 0 9.8353 0 10.2495C0 10.6637 0.335786 10.9995 0.75 10.9995H5.75C6.16421 10.9995 6.5 10.6637 6.5 10.2495Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186308\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"filter-2\", \"keywords\": [ \"funnel\", \"filter\", \"angle\", \"oil\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186311)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.500002 0C0.312708 0 0.14112 0.104674 0.055418 0.271209C-0.0302842 0.437745 -0.0157272 0.638212 0.0931351 0.790619L5 7.66023V13.5C5 13.6844 5.10149 13.8538 5.26407 13.9408C5.42665 14.0278 5.62392 14.0183 5.77735 13.916L8.77735 11.916C8.91645 11.8233 9 11.6672 9 11.5V7.66023L13.9069 0.790619C14.0157 0.638212 14.0303 0.437745 13.9446 0.271209C13.8589 0.104674 13.6873 0 13.5 0H0.500002Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186311\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"fingerprint-1\", \"keywords\": [ \"identification\", \"password\", \"touch\", \"id\", \"secure\", \"fingerprint\", \"finger\", \"security\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186314)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.75 6.75C1.75 3.8505 4.10051 1.5 7 1.5C9.8995 1.5 12.25 3.85051 12.25 6.75V8.75C12.25 9.16421 12.5858 9.5 13 9.5C13.4142 9.5 13.75 9.16421 13.75 8.75V6.75C13.75 3.02208 10.7279 0 7 0C3.27208 0 0.25 3.02208 0.25 6.75V13.25C0.25 13.6642 0.585786 14 1 14C1.41421 14 1.75 13.6642 1.75 13.25V6.75ZM4.75 11.75C4.75 11.3358 4.41421 11 4 11C3.58579 11 3.25 11.3358 3.25 11.75V13.25C3.25 13.6642 3.58579 14 4 14C4.41421 14 4.75 13.6642 4.75 13.25V11.75ZM7 5C5.75736 5 4.75 6.00736 4.75 7.25V8.75C4.75 9.16421 4.41421 9.5 4 9.5C3.58579 9.5 3.25 9.16421 3.25 8.75V7.25C3.25 5.17893 4.92893 3.5 7 3.5C9.07107 3.5 10.75 5.17893 10.75 7.25V13.25C10.75 13.6642 10.4142 14 10 14C9.58579 14 9.25 13.6642 9.25 13.25V7.25C9.25 6.00736 8.24264 5 7 5ZM13.75 11.75C13.75 11.3358 13.4142 11 13 11C12.5858 11 12.25 11.3358 12.25 11.75V13.25C12.25 13.6642 12.5858 14 13 14C13.4142 14 13.75 13.6642 13.75 13.25V11.75ZM7 11.5C7.41421 11.5 7.75 11.8358 7.75 12.25V13.25C7.75 13.6642 7.41421 14 7 14C6.58579 14 6.25 13.6642 6.25 13.25V12.25C6.25 11.8358 6.58579 11.5 7 11.5ZM7.75 7.75C7.75 7.33579 7.41421 7 7 7C6.58579 7 6.25 7.33579 6.25 7.75V9.25C6.25 9.66421 6.58579 10 7 10C7.41421 10 7.75 9.66421 7.75 9.25V7.75Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186314\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"fingerprint-2\", \"keywords\": [ \"identification\", \"password\", \"touch\", \"id\", \"secure\", \"fingerprint\", \"finger\", \"security\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186317)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.54009 0.192192C6.79841 -0.144958 8.12846 -0.0347868 9.30661 0.493239C9.6846 0.662645 9.85369 1.10639 9.68429 1.48438C9.51488 1.86237 9.07113 2.03146 8.69314 1.86205C7.82831 1.47446 6.85196 1.3936 5.92831 1.64109C4.86762 1.92528 3.96327 2.61918 3.41418 3.57014C3.21508 3.91401 3.06732 4.28583 2.97631 4.67203C2.88131 5.0752 2.47745 5.32501 2.07428 5.23001C1.67111 5.135 1.4213 4.73115 1.5163 4.32798C1.64096 3.79898 1.84271 3.29077 2.11538 2.81974C2.8634 1.52445 4.09527 0.579308 5.54009 0.192192ZM11.6133 2.39514C11.375 2.05636 10.9072 1.97492 10.5684 2.21324C10.2296 2.45156 10.1482 2.9194 10.3865 3.25818C10.8749 3.95254 11.1405 4.78399 11.1404 5.64063V8.35979C11.1404 8.90353 11.0333 9.44195 10.8252 9.9443C10.6171 10.4466 10.3122 10.9031 9.92766 11.2876C9.54317 11.6721 9.08673 11.977 8.58438 12.1851C8.08203 12.3932 7.54361 12.5003 6.99988 12.5003C6.58567 12.5003 6.24988 12.8361 6.24988 13.2503C6.24988 13.6645 6.58567 14.0003 6.99988 14.0003C7.74059 14.0003 8.47407 13.8544 9.15841 13.5709C9.84274 13.2875 10.4646 12.872 10.9883 12.3482C11.5121 11.8245 11.9276 11.2027 12.211 10.5183C12.4945 9.83398 12.6404 9.10052 12.6404 8.35979V5.64071C12.6405 4.4737 12.2787 3.34104 11.6133 2.39514ZM2.85938 6.72608C2.85938 6.31186 2.52359 5.97608 2.10938 5.97608C1.69516 5.97608 1.35938 6.31186 1.35938 6.72608V8.35925C1.35905 9.46954 1.68641 10.5555 2.30043 11.4805C2.77028 12.1884 3.39198 12.7781 4.11648 13.209C4.47248 13.4207 4.93273 13.3038 5.14448 12.9478C5.35622 12.5918 5.23928 12.1315 4.88328 11.9198C4.35143 11.6035 3.89507 11.1706 3.55018 10.651C3.0994 9.9719 2.8591 9.17485 2.85938 8.35976V6.72608ZM6.84761 4.29114C7.03231 4.27563 7.21876 4.2903 7.39957 4.33501C7.64728 4.39626 7.87808 4.51233 8.07495 4.67465C8.27183 4.83698 8.42978 5.04143 8.53712 5.27292C8.64446 5.50442 8.69847 5.75705 8.69518 6.0122L8.69512 6.02188V7C8.69512 7.41422 9.0309 7.75 9.44512 7.75C9.85933 7.75 10.1951 7.41422 10.1951 7V6.02641C10.2005 5.54854 10.099 5.0755 9.89793 4.64191C9.69615 4.20676 9.39927 3.82245 9.02919 3.51731C8.65911 3.21218 8.22526 2.994 7.75961 2.87886C7.41973 2.79482 7.06925 2.76725 6.72208 2.7964C6.30933 2.83106 6.00282 3.19377 6.03748 3.60653C6.07214 4.01929 6.43485 4.3258 6.84761 4.29114ZM5.04255 4.35276C5.42401 4.51419 5.60238 4.95429 5.44096 5.33575C5.34971 5.55136 5.30333 5.78329 5.30462 6.01741L5.30465 6.02156L5.30463 7.97776V7.98974C5.29912 8.33507 5.39912 8.67384 5.59142 8.96072C5.78372 9.2476 6.05907 9.4689 6.3806 9.59501C6.70213 9.72111 7.05449 9.746 7.39055 9.66634C7.72661 9.58669 8.03033 9.40629 8.26105 9.14929C8.53777 8.84106 9.01196 8.81551 9.32018 9.09222C9.62841 9.36893 9.65395 9.84312 9.37724 10.1513C8.94238 10.6357 8.36993 10.9758 7.73652 11.1259C7.1031 11.276 6.43894 11.2291 5.83291 10.9914C5.22689 10.7538 4.70792 10.3366 4.34546 9.79593C3.98418 9.25697 3.79558 8.62085 3.80463 7.97218V6.0235C3.80252 5.5865 3.88924 5.15363 4.05956 4.75117C4.22098 4.36971 4.66108 4.19133 5.04255 4.35276ZM7.74988 6.1294C7.74988 5.71518 7.41408 5.3794 6.99988 5.3794C6.58567 5.3794 6.24988 5.71518 6.24988 6.1294V7.76283C6.24988 8.17704 6.58567 8.51283 6.99988 8.51283C7.41408 8.51283 7.74988 8.17704 7.74988 7.76283V6.1294Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186317\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"fist\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186320)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.87896 3.66699H2.13885V2.37006C2.13885 1.6134 2.75225 1 3.50891 1C4.26442 1 4.87709 1.61152 4.87896 2.36659V3.66699ZM8.36356 4.8385C8.05041 4.14764 7.3549 3.66699 6.5468 3.66699H5.87897V2.37006L5.87896 2.3655V1.37006C5.87896 0.613396 6.49236 0 7.24902 0C8.00568 0 8.61908 0.613396 8.61908 1.37006V4.04146C8.61908 4.33851 8.52449 4.61378 8.36356 4.8385ZM6.61174 4.91979C6.99206 4.95268 7.29051 5.27182 7.29051 5.66099C7.29051 5.98871 7.07607 6.27784 6.76244 6.37296L4.65472 7.01218C4.35829 7.10209 4.17243 7.39556 4.21787 7.70197C4.26332 8.00838 4.52635 8.23528 4.83611 8.23528C5.19288 8.23528 5.64824 8.41335 6.01631 8.74903C6.37806 9.07895 6.59337 9.5028 6.58391 9.94431C6.57651 10.2894 6.85027 10.5752 7.19537 10.5826C7.54047 10.5899 7.82622 10.3162 7.83362 9.97109C7.85255 9.08792 7.42071 8.33806 6.85862 7.82544C6.80972 7.78085 6.75936 7.73759 6.70767 7.69579L7.12523 7.56915C7.83772 7.35307 8.35924 6.76335 8.50159 6.05372C8.73868 5.90584 8.9478 5.7172 9.11911 5.49755C9.55282 6.0538 10.2293 6.41152 10.9893 6.41152C11.8164 6.41152 12.5445 5.98784 12.9685 5.34566C13.0153 5.57658 13.0391 5.81235 13.0391 6.04951V7.08375C13.0391 9.89018 10.7004 10.8257 10.7004 10.8257V13.5C10.7004 13.7761 10.4766 14 10.2004 14L3.79379 13.9997C3.51766 13.9997 3.29381 13.7758 3.29381 13.4997V11.0697C1.89466 10.5841 0.955016 8.64777 0.955078 7.32775C0.955131 6.18681 1.21162 4.99454 2.47844 4.99454L6.61174 4.91979ZM10.9893 1.06641C10.2326 1.06641 9.6192 1.6798 9.6192 2.43646V4.04146C9.6192 4.79812 10.2326 5.41152 10.9893 5.41152C11.7459 5.41152 12.3593 4.79812 12.3593 4.04146V2.43646C12.3593 1.6798 11.7459 1.06641 10.9893 1.06641Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186320\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"fit-to-height-square\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186323)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 10.5C0 12.433 1.567 14 3.5 14H10.5C12.433 14 14 12.433 14 10.5V3.5C14 1.567 12.433 0 10.5 0H3.5C1.567 0 0 1.567 0 3.5V10.5ZM6.25 6V8H5C4.79777 8 4.61545 8.12182 4.53806 8.30866C4.46067 8.49549 4.50345 8.71055 4.64645 8.85355L6.64645 10.8536C6.74021 10.9473 6.86739 11 7 11C7.13261 11 7.25979 10.9473 7.35355 10.8536L9.35355 8.85355C9.49655 8.71055 9.53933 8.49549 9.46194 8.30866C9.38455 8.12182 9.20223 8 9 8H7.75V6H9C9.20223 6 9.38455 5.87818 9.46194 5.69134C9.53933 5.5045 9.49655 5.28945 9.35355 5.14645L7.35355 3.14645C7.25979 3.05268 7.13261 3 7 3C6.86739 3 6.74021 3.05268 6.64645 3.14645L4.64645 5.14645C4.50345 5.28945 4.46067 5.50451 4.53806 5.69134C4.61545 5.87818 4.79777 6 5 6H6.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186323\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"flip-vertical-arrow-2\", \"keywords\": [ \"arrow\", \"design\", \"flip\", \"reflect\", \"up\", \"down\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186326)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.85338 0.853554L6.64628 4.64645C6.84154 4.84171 7.15812 4.84171 7.35338 4.64645L11.1463 0.853553C11.4613 0.53857 11.2382 0 10.7927 0H3.20694C2.76148 0 2.5384 0.538571 2.85338 0.853554ZM2.85338 13.1464L6.64628 9.35348C6.84154 9.15822 7.15812 9.15822 7.35338 9.35348L11.1463 13.1464C11.4613 13.4614 11.2382 13.9999 10.7927 13.9999H3.20694C2.76148 13.9999 2.5384 13.4614 2.85338 13.1464ZM1.75 7.75H0.75C0.335787 7.75 0 7.41421 0 7C0 6.58579 0.335786 6.25 0.75 6.25H1.75C2.16422 6.25 2.5 6.58579 2.5 7C2.5 7.41421 2.16422 7.75 1.75 7.75ZM4.58325 7.75H5.58325C5.99747 7.75 6.33325 7.41421 6.33325 7C6.33325 6.58579 5.99747 6.25 5.58325 6.25H4.58325C4.16904 6.25 3.83325 6.58579 3.83325 7C3.83325 7.41421 4.16904 7.75 4.58325 7.75ZM9.41651 7.75H8.41651C8.00229 7.75 7.66651 7.41421 7.66651 7C7.66651 6.58579 8.00229 6.25 8.41651 6.25H9.41651C9.83072 6.25 10.1665 6.58579 10.1665 7C10.1665 7.41421 9.83072 7.75 9.41651 7.75ZM12.2498 7.75H13.2498C13.664 7.75 13.9998 7.41421 13.9998 7C13.9998 6.58579 13.664 6.25 13.2498 6.25H12.2498C11.8356 6.25 11.4998 6.58579 11.4998 7C11.4998 7.41421 11.8356 7.75 12.2498 7.75Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186326\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"flip-vertical-circle-1\", \"keywords\": [ \"flip\", \"bottom\", \"object\", \"work\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186329)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.6102 0.182565C7.55052 -0.060855 6.44945 -0.060855 5.38977 0.182565C4.98607 0.2753 4.73398 0.677739 4.82672 1.08144C4.91945 1.48514 5.32189 1.73722 5.72559 1.64449C6.56427 1.45184 7.43571 1.45184 8.27438 1.64449C8.67808 1.73722 9.08052 1.48514 9.17326 1.08144C9.26599 0.677739 9.0139 0.2753 8.6102 0.182565ZM3.58815 1.71487C3.85174 2.03439 3.8064 2.50709 3.48688 2.77068C3.15906 3.04111 2.86466 3.34967 2.60991 3.68982L2.60571 3.69542C2.34908 4.03147 2.13485 4.39787 1.9679 4.78635C1.80435 5.1669 1.36327 5.34283 0.98271 5.17928C0.602151 5.01573 0.426229 4.57465 0.589776 4.19409C0.804947 3.69341 1.0809 3.2211 1.4114 2.78782C1.73715 2.35334 2.11344 1.95917 2.53234 1.6136C2.85186 1.35001 3.32456 1.39535 3.58815 1.71487ZM11.5666 1.62411C11.2529 1.35363 10.7793 1.38869 10.5089 1.70241C10.2384 2.01612 10.2735 2.4897 10.5872 2.76017C10.9032 3.0326 11.1862 3.34107 11.4304 3.67931L11.4424 3.6954C11.699 4.03145 11.9132 4.39787 12.0801 4.78635C12.2437 5.1669 12.6848 5.34283 13.0653 5.17928C13.4459 5.01573 13.6218 4.57465 13.4583 4.19409C13.244 3.6954 12.9694 3.22487 12.6406 2.79301C12.3292 2.36306 11.9687 1.97079 11.5666 1.62411ZM0.21967 6.46967C0.360322 6.32902 0.551088 6.25 0.75 6.25H13.25C13.4489 6.25 13.6397 6.32902 13.7803 6.46967C13.921 6.61032 14 6.80109 14 7C14 8.85651 13.2625 10.637 11.9497 11.9497C10.637 13.2625 8.85651 14 7 14C5.14348 14 3.36301 13.2625 2.05025 11.9497C0.737498 10.637 0 8.85652 0 7C0 6.80109 0.0790176 6.61032 0.21967 6.46967Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186329\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"flip-vertical-square-2\", \"keywords\": [ \"design\", \"up\", \"flip\", \"reflect\", \"vertical\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186332)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.75 0C11.3358 0 11 0.335786 11 0.75C11 1.16421 11.3358 1.5 11.75 1.5H12.25C12.3881 1.5 12.5 1.61193 12.5 1.75V2.25C12.5 2.66421 12.8358 3 13.25 3C13.6642 3 14 2.66421 14 2.25V1.75C14 0.783502 13.2165 0 12.25 0H11.75ZM14 7V5.5C14 5.08579 13.6642 4.75 13.25 4.75C12.8358 4.75 12.5 5.08579 12.5 5.5V6.25H1.5V5.5C1.5 5.08579 1.16421 4.75 0.75 4.75C0.335786 4.75 0 5.08579 0 5.5V7V7.9999V12.25C0 13.2165 0.783502 14 1.75 14H12.25C13.2165 14 14 13.2165 14 12.25V8.00012V7ZM1.75012 1.5C1.61205 1.5 1.50012 1.61193 1.50012 1.75V2.25C1.50012 2.66421 1.16434 3 0.750122 3C0.335909 3 0.00012207 2.66421 0.00012207 2.25V1.75C0.00012207 0.783502 0.783624 0 1.75012 0H2.25012C2.66434 0 3.00012 0.335786 3.00012 0.75C3.00012 1.16421 2.66434 1.5 2.25012 1.5H1.75012ZM6 0C5.58579 0 5.25 0.335786 5.25 0.75C5.25 1.16421 5.58579 1.5 6 1.5H8C8.41421 1.5 8.75 1.16421 8.75 0.75C8.75 0.335786 8.41421 0 8 0H6Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186332\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"folder-add\", \"keywords\": [ \"add\", \"folder\", \"plus\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186335)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.18345 0.499936C5.52332 0.491851 5.85595 0.599465 6.12675 0.805269C6.39945 1.01253 6.5932 1.30766 6.67544 1.64015L6.89039 2.49994H12.5C12.8978 2.49994 13.2794 2.65797 13.5607 2.93928C13.842 3.22058 14 3.60211 14 3.99994V11.9999C14 12.3978 13.842 12.7793 13.5607 13.0606C13.2794 13.3419 12.8978 13.4999 12.5 13.4999H1.5C1.10217 13.4999 0.720644 13.3419 0.43934 13.0606C0.158035 12.7793 0 12.3978 0 11.9999V1.99994C0 1.60211 0.158035 1.22058 0.43934 0.939276C0.720644 0.657971 1.10218 0.499936 1.5 0.499936H5.18345ZM7 4.75C7.41421 4.75 7.75 5.08579 7.75 5.5V7.25H9.5C9.91421 7.25 10.25 7.58579 10.25 8C10.25 8.41421 9.91421 8.75 9.5 8.75H7.75V10.5C7.75 10.9142 7.41421 11.25 7 11.25C6.58579 11.25 6.25 10.9142 6.25 10.5V8.75H4.5C4.08579 8.75 3.75 8.41421 3.75 8C3.75 7.58579 4.08579 7.25 4.5 7.25H6.25V5.5C6.25 5.08579 6.58579 4.75 7 4.75Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186335\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"folder-check\", \"keywords\": [ \"remove\", \"check\", \"folder\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186338)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.18345 0.499936C5.52332 0.491851 5.85595 0.599465 6.12675 0.805269C6.39945 1.01253 6.5932 1.30766 6.67544 1.64015L6.89039 2.49994H12.5C12.8978 2.49994 13.2794 2.65797 13.5607 2.93928C13.842 3.22058 14 3.60211 14 3.99994V11.9999C14 12.3978 13.842 12.7793 13.5607 13.0606C13.2794 13.3419 12.8978 13.4999 12.5 13.4999H1.5C1.10217 13.4999 0.720644 13.3419 0.43934 13.0606C0.158035 12.7793 0 12.3978 0 11.9999V1.99994C0 1.60211 0.158035 1.22058 0.43934 0.939276C0.720644 0.657971 1.10218 0.499936 1.5 0.499936H5.18345ZM9.6 6.45C9.84853 6.11863 9.78137 5.64853 9.45 5.4C9.11863 5.15147 8.64853 5.21863 8.4 5.55L5.82569 8.98241L4.91603 8.37596C4.57138 8.1462 4.10573 8.23933 3.87596 8.58397C3.6462 8.92862 3.73933 9.39427 4.08397 9.62404L5.58397 10.624C5.91517 10.8448 6.36117 10.7684 6.6 10.45L9.6 6.45Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186338\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"folder-delete\", \"keywords\": [ \"remove\", \"minus\", \"folder\", \"subtract\", \"delete\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186341)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.18345 0.499936C5.52332 0.491851 5.85595 0.599465 6.12675 0.805269C6.39945 1.01253 6.5932 1.30766 6.67544 1.64015L6.89039 2.49994H12.5C12.8978 2.49994 13.2794 2.65797 13.5607 2.93928C13.842 3.22058 14 3.60211 14 3.99994V11.9999C14 12.3978 13.842 12.7793 13.5607 13.0606C13.2794 13.3419 12.8978 13.4999 12.5 13.4999H1.5C1.10217 13.4999 0.720644 13.3419 0.43934 13.0606C0.158035 12.7793 0 12.3978 0 11.9999V1.99994C0 1.60211 0.158035 1.22058 0.43934 0.939276C0.720644 0.657971 1.10218 0.499936 1.5 0.499936H5.18345ZM4.5 7.25C4.08579 7.25 3.75 7.58579 3.75 8C3.75 8.41421 4.08579 8.75 4.5 8.75H9.5C9.91421 8.75 10.25 8.41421 10.25 8C10.25 7.58579 9.91421 7.25 9.5 7.25H4.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186341\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"front-camera\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.6 1.2C4.69443 1.0741 4.84262 1 5 1H9C9.15738 1 9.30557 1.0741 9.4 1.2L10.75 3H12.5C12.8978 3 13.2794 3.15804 13.5607 3.43934C13.842 3.72064 14 4.10217 14 4.5V11.5C14 11.8978 13.842 12.2794 13.5607 12.5607C13.2794 12.842 12.8978 13 12.5 13H1.5C1.10217 13 0.720644 12.842 0.43934 12.5607C0.158035 12.2794 0 11.8978 0 11.5V4.5C0 4.10218 0.158035 3.72064 0.43934 3.43934C0.720644 3.15804 1.10217 3 1.5 3H3.25L4.6 1.2ZM4.75 7C5.16421 7 5.5 6.66421 5.5 6.25C5.5 5.83579 5.16421 5.5 4.75 5.5C4.33579 5.5 4 5.83579 4 6.25C4 6.66421 4.33579 7 4.75 7ZM5.0009 8.41924C4.7589 8.1731 4.36319 8.16975 4.11705 8.41176C3.87092 8.65376 3.86757 9.04947 4.10957 9.29561C4.70409 9.90027 5.70626 10.411 6.99842 10.411C8.29058 10.411 9.29275 9.90027 9.88727 9.29561C10.1293 9.04947 10.1259 8.65376 9.87979 8.41176C9.63365 8.16975 9.23794 8.1731 8.99593 8.41924C8.63777 8.78352 7.95732 9.16099 6.99842 9.16099C6.03951 9.16099 5.35907 8.78352 5.0009 8.41924ZM10.0001 6.25C10.0001 6.66421 9.66434 7 9.25012 7C8.83591 7 8.50012 6.66421 8.50012 6.25C8.50012 5.83579 8.83591 5.5 9.25012 5.5C9.66434 5.5 10.0001 5.83579 10.0001 6.25Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"gif-format\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.4375 2.15625C12.8182 2.15625 13.9375 3.27554 13.9375 4.65625V8.9449C13.9375 10.3256 12.8182 11.4449 11.4375 11.4449H2.5625C1.18179 11.4449 0.0625 10.3256 0.0625 8.9449V4.65625C0.0625 3.27554 1.18179 2.15625 2.5625 2.15625H11.4375ZM7.80595 9.15258C8.08209 9.15258 8.30595 8.92872 8.30595 8.65258C8.30595 8.37644 8.08209 8.15258 7.80595 8.15258H7.37992V5.44855H7.80595C8.08209 5.44855 8.30595 5.2247 8.30595 4.94855C8.30595 4.67241 8.08209 4.44855 7.80595 4.44855H6.88042H5.95394C5.67779 4.44855 5.45394 4.67241 5.45394 4.94855C5.45394 5.2247 5.67779 5.44855 5.95394 5.44855H6.37992V8.15258H5.95394C5.67779 8.15258 5.45394 8.37644 5.45394 8.65258C5.45394 8.92872 5.67779 9.15258 5.95394 9.15258H6.87992H7.80595ZM2.26907 5.87455C2.26907 5.63928 2.4598 5.44855 2.69508 5.44855H3.31242C3.49712 5.44855 3.65559 5.56638 3.71431 5.73251C3.80633 5.99287 4.092 6.12933 4.35235 6.03731C4.61271 5.94529 4.74918 5.65963 4.65715 5.39927C4.46163 4.84608 3.93409 4.44855 3.31242 4.44855H2.69508C1.90752 4.44855 1.26907 5.08699 1.26907 5.87455V7.72657C1.26907 8.51413 1.90752 9.15257 2.69508 9.15257H3.31242C4.09998 9.15257 4.73842 8.51413 4.73842 7.72657V7.10923C4.73842 6.83309 4.51457 6.60923 4.23842 6.60923H3.31242C3.03627 6.60923 2.81242 6.83309 2.81242 7.10923C2.81242 7.38538 3.03627 7.60923 3.31242 7.60923H3.73842V7.72657C3.73842 7.96184 3.54769 8.15257 3.31242 8.15257H2.69508C2.4598 8.15257 2.26907 7.96184 2.26907 7.72657V5.87455ZM9.26159 4.94855C9.26159 4.67241 9.48545 4.44855 9.76159 4.44855H12.2309C12.5071 4.44855 12.7309 4.67241 12.7309 4.94855C12.7309 5.22469 12.5071 5.44855 12.2309 5.44855H10.2616V6.1462H11.9223C12.1984 6.1462 12.4223 6.37006 12.4223 6.6462C12.4223 6.92234 12.1984 7.1462 11.9223 7.1462H10.2616V8.65257C10.2616 8.92871 10.0377 9.15257 9.76159 9.15257C9.48545 9.15257 9.26159 8.92871 9.26159 8.65257V4.94855Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"give-gift\", \"keywords\": [ \"reward\", \"social\", \"rating\", \"media\", \"queen\", \"vip\", \"gift\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186350)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.03033 0.21967C8.73744 -0.0732233 8.26256 -0.0732233 7.96967 0.21967C7.67678 0.512563 7.67678 0.987437 7.96967 1.28033L8.68934 2H6.5C6.22386 2 6 2.22386 6 2.5V7.5C6 7.77614 6.22386 8 6.5 8H9.375V3.5C9.375 3.15482 9.65482 2.875 10 2.875C10.3452 2.875 10.625 3.15482 10.625 3.5V8H13.5C13.7761 8 14 7.77614 14 7.5V2.5C14 2.22386 13.7761 2 13.5 2H11.3107L12.0303 1.28033C12.3232 0.987437 12.3232 0.512563 12.0303 0.21967C11.7374 -0.0732233 11.2626 -0.0732233 10.9697 0.21967L10 1.18934L9.03033 0.21967ZM1.84315 7H0V11L1.82843 12.8284C2.57857 13.5786 3.59599 14 4.65685 14H10.5C11.3284 14 12 13.3284 12 12.5C12 11.6716 11.3284 11 10.5 11H7.72328C7.6268 11.3266 7.44966 11.6345 7.19188 11.8923C6.36791 12.7163 5.03199 12.7163 4.20802 11.8923L2.75801 10.4423C2.51393 10.1982 2.51393 9.8025 2.75801 9.55842C3.00209 9.31435 3.39782 9.31435 3.64189 9.55842L5.09191 11.0084C5.42772 11.3443 5.97218 11.3443 6.308 11.0084C6.61051 10.7059 6.64051 10.2341 6.39799 9.89799L4.67157 8.17157C3.92143 7.42143 2.90401 7 1.84315 7Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186350\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"glasses\", \"keywords\": [ \"vision\", \"sunglasses\", \"protection\", \"spectacles\", \"correction\", \"sun\", \"eye\", \"glasses\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186353)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.61536 1C9.61536 0.585786 9.95114 0.25 10.3654 0.25H11.3269C12.0358 0.25 12.7157 0.531627 13.217 1.03293C13.7183 1.53422 14 2.21413 14 2.92308V8.99189V9V11.8846C14 12.3385 13.8197 12.7739 13.4987 13.0949C13.1777 13.4158 12.7424 13.5962 12.2885 13.5962H9.40386C8.94993 13.5962 8.5146 13.4158 8.19362 13.0949C7.87264 12.7739 7.69232 12.3385 7.69232 11.8846V9.75H6.30769V11.8846C6.30769 12.3385 6.12737 12.7739 5.80639 13.0949C5.48542 13.4158 5.05008 13.5962 4.59615 13.5962H1.71154C1.25761 13.5962 0.822273 13.4158 0.501298 13.0949C0.180322 12.7739 0 12.3385 0 11.8846V9V2.92308C0 2.21413 0.281627 1.53423 0.782926 1.03293C1.28423 0.531627 1.96413 0.25 2.67308 0.25H3.63462C4.04883 0.25 4.38462 0.585786 4.38462 1C4.38462 1.41421 4.04883 1.75 3.63462 1.75H2.67308C2.36196 1.75 2.06358 1.87359 1.84359 2.09359C1.62359 2.31358 1.5 2.61196 1.5 2.92308V8.25H5.55768H8.44229H12.5V2.92308C12.5 2.61196 12.3764 2.31358 12.1564 2.09359C11.9364 1.87359 11.638 1.75 11.3269 1.75H10.3654C9.95114 1.75 9.61536 1.41421 9.61536 1ZM1.5 9.75V11.8846C1.5 11.9407 1.52229 11.9945 1.56196 12.0342C1.60163 12.0739 1.65544 12.0962 1.71154 12.0962H4.59615C4.65226 12.0962 4.70606 12.0739 4.74573 12.0342C4.78541 11.9945 4.80769 11.9407 4.80769 11.8846V9.75H1.5ZM9.19232 9.75V11.8846C9.19232 11.9407 9.21461 11.9945 9.25428 12.0342C9.29395 12.0739 9.34776 12.0962 9.40386 12.0962H12.2885C12.3446 12.0962 12.3984 12.0739 12.4381 12.0342C12.4777 11.9945 12.5 11.9407 12.5 11.8846V9.75H9.19232Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186353\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"half-star-1\", \"keywords\": [ \"reward\", \"rating\", \"rate\", \"social\", \"star\", \"media\", \"favorite\", \"like\", \"stars\", \"half\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.51788 0.49955C7.51776 0.366513 7.46462 0.23901 7.37023 0.145256C7.27585 0.0515026 7.14799 -0.000769254 7.01495 8.55773e-06C6.81506 0.00117728 6.61974 0.0599287 6.45237 0.169229C6.28683 0.277342 6.15557 0.43038 6.07392 0.610361L4.48593 3.96613C4.48339 3.97148 4.48095 3.97688 4.47861 3.98232C4.47724 3.98551 4.47507 3.98828 4.4723 3.99037C4.46954 3.99246 4.46628 3.9938 4.46284 3.99425L4.45658 3.99511L0.916578 4.50511L0.908459 4.50635C0.708138 4.53857 0.520947 4.62658 0.368332 4.76028C0.215716 4.89398 0.103849 5.06796 0.0455477 5.2623C-0.0127542 5.45664 -0.0151327 5.66347 0.0386846 5.8591C0.0910609 6.04949 0.194602 6.22178 0.337919 6.35731L2.88959 8.97875L2.89219 8.98141C2.90103 8.99036 2.90764 9.00127 2.91147 9.01325C2.91515 9.02474 2.91618 9.03692 2.91449 9.04885L2.27563 12.7423L2.27537 12.7438C2.24131 12.9365 2.26126 13.1349 2.33303 13.317C2.40499 13.4995 2.52621 13.6585 2.68319 13.7762C2.84017 13.894 3.02675 13.9658 3.22215 13.9838C3.41755 14.0018 3.6141 13.9651 3.78992 13.878C3.79557 13.8752 3.80118 13.8723 3.80673 13.8693L7.0046 12.1304C7.01666 12.124 7.03062 12.1202 7.04426 12.1197C7.31406 12.1109 7.52812 11.8895 7.52788 11.6196L7.51788 0.49955Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"hand-cursor\", \"keywords\": [ \"hand\", \"select\", \"cursor\", \"finger\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path d=\\\"M10.7831 6.06305C11.3226 6.17046 11.8005 6.48019 12.1189 6.92874C12.4373 7.37729 12.572 7.93065 12.4954 8.47535L11.8385 13.0738C11.8019 13.3326 11.6725 13.5692 11.4744 13.7396C11.2763 13.91 11.0229 14.0025 10.7616 13.9999H3.7832C3.56751 14.0002 3.35673 13.9356 3.17816 13.8146C2.9996 13.6937 2.86146 13.5219 2.78166 13.3215L1.62936 10.4569C1.43212 9.95064 1.4335 9.38855 1.63321 8.88328C1.83293 8.378 2.21622 7.96687 2.70628 7.73228L4.16012 6.99997V1.61538C4.16012 1.18695 4.33031 0.776076 4.63325 0.473133C4.93619 0.170191 5.34707 0 5.7755 0C6.20392 0 6.6148 0.170191 6.91774 0.473133C7.22068 0.776076 7.39087 1.18695 7.39087 1.61538V5.3846L10.7831 6.06305Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"hand-grab\", \"keywords\": [ \"hand\", \"select\", \"cursor\", \"finger\", \"grab\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186362)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.79486 2.5093C3.79901 0.924769 5.25822 -0.256977 6.80988 0.0696891L6.82326 0.0725055L11.3794 1.16275C12.5326 1.40895 13.3573 2.42781 13.3573 3.60762V6.62416C13.3573 8.25423 12.9778 9.86193 12.2488 11.3199L11.8573 12.1029V13.4849C11.8573 13.761 11.6335 13.9849 11.3573 13.9849H4.35737C4.08123 13.9849 3.85737 13.761 3.85737 13.4849V12.2252L2.11391 10.8304C1.6546 10.4629 1.33956 9.9453 1.2242 9.36852L0.69334 6.71422C0.456992 5.53248 1.09737 4.35032 2.21632 3.90274L2.23856 3.89384L2.26156 3.88711L2.54486 3.80419V6.07855C2.54487 6.42373 2.82469 6.70355 3.16987 6.70355C3.51504 6.70355 3.79487 6.42373 3.79487 6.07855L3.79486 2.5093Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186362\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"heading-1-paragraph-styles-heading\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186365)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.75 1.25C1.75 0.835786 1.41421 0.5 1 0.5C0.585786 0.5 0.25 0.835786 0.25 1.25V6.52083V12.75C0.25 13.1642 0.585786 13.5 1 13.5C1.41421 13.5 1.75 13.1642 1.75 12.75V7.27083H6V12.75C6 13.1642 6.33579 13.5 6.75 13.5C7.16421 13.5 7.5 13.1642 7.5 12.75V6.52083V1.25C7.5 0.835786 7.16421 0.5 6.75 0.5C6.33579 0.5 6 0.835786 6 1.25V5.77083H1.75V1.25ZM12.1445 7.44502C12.1445 7.03081 11.8087 6.69502 11.3945 6.69502C10.9803 6.69502 10.6445 7.03081 10.6445 7.44502C10.6445 7.73434 10.41 7.96888 10.1207 7.96888H9.69604C9.28183 7.96888 8.94604 8.30466 8.94604 8.71888C8.94604 9.13309 9.28183 9.46888 9.69604 9.46888H10.1207C10.3017 9.46888 10.4772 9.4451 10.6443 9.40049V12H9.69619C9.28197 12 8.94619 12.3358 8.94619 12.75C8.94619 13.1642 9.28197 13.5 9.69619 13.5H13.0931C13.5073 13.5 13.8431 13.1642 13.8431 12.75C13.8431 12.3358 13.5073 12 13.0931 12H12.1443V7.65458C12.1443 7.62836 12.1429 7.60245 12.1403 7.57693C12.1431 7.53332 12.1445 7.48934 12.1445 7.44502Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186365\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"heading-2-paragraph-styles-heading\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186368)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.75 1.25C1.75 0.835786 1.41421 0.5 1 0.5C0.585786 0.5 0.25 0.835786 0.25 1.25V6.52083V12.75C0.25 13.1642 0.585786 13.5 1 13.5C1.41421 13.5 1.75 13.1642 1.75 12.75V7.27083H6V12.75C6 13.1642 6.33579 13.5 6.75 13.5C7.16421 13.5 7.5 13.1642 7.5 12.75V6.52083V1.25C7.5 0.835786 7.16421 0.5 6.75 0.5C6.33579 0.5 6 0.835786 6 1.25V5.77083H1.75V1.25ZM10.8537 6.69502C9.95386 6.69502 9.23003 7.30603 8.96917 8.09328C8.83888 8.48647 9.05201 8.91083 9.4452 9.04112C9.83839 9.17141 10.2627 8.95828 10.393 8.56509C10.4735 8.32212 10.6708 8.19502 10.8537 8.19502H11.7149C11.9247 8.19502 12.176 8.39128 12.176 8.73686C12.176 8.97921 12.0418 9.1667 11.8831 9.24075L10.0382 10.1017C9.30348 10.4446 8.86035 11.1986 8.86035 11.9964V12.75C8.86035 13.1642 9.19614 13.5 9.61035 13.5H12.926C13.3402 13.5 13.676 13.1642 13.676 12.75C13.676 12.3358 13.3402 12 12.926 12H10.3604V11.9964C10.3604 11.7404 10.5021 11.5405 10.6725 11.461L12.5175 10.6C13.2404 10.2626 13.676 9.521 13.676 8.73686C13.676 7.65552 12.8428 6.69502 11.7149 6.69502H10.8537Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186368\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"heading-3-paragraph-styles-heading\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186371)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.75 1.25C1.75 0.835786 1.41421 0.5 1 0.5C0.585786 0.5 0.25 0.835786 0.25 1.25V6.52083V12.75C0.25 13.1642 0.585786 13.5 1 13.5C1.41421 13.5 1.75 13.1642 1.75 12.75V7.27083H6V12.75C6 13.1642 6.33579 13.5 6.75 13.5C7.16421 13.5 7.5 13.1642 7.5 12.75V6.52083V1.25C7.5 0.835786 7.16421 0.5 6.75 0.5C6.33579 0.5 6 0.835786 6 1.25V5.77083H1.75V1.25ZM11.562 6.69502L10.8989 6.69503C9.99803 6.69504 9.24051 7.3004 9.00726 8.12506C8.89452 8.52364 9.12624 8.93814 9.52482 9.05087C9.9234 9.16361 10.3379 8.93189 10.4506 8.53331C10.506 8.33744 10.6868 8.19503 10.8989 8.19503L11.562 8.19502C11.8192 8.19502 12.0277 8.40353 12.0277 8.66074C12.0277 8.91796 11.8192 9.12647 11.562 9.12647H11.2305C10.8162 9.12647 10.4805 9.46225 10.4805 9.87647C10.4805 10.2907 10.8162 10.6265 11.2305 10.6265H11.562H11.6725C11.9908 10.6265 12.2488 10.8845 12.2488 11.2027V11.4238C12.2488 11.742 11.9908 12 11.6725 12H10.7884C10.5386 12 10.3242 11.8407 10.2447 11.6159C10.1067 11.2254 9.67821 11.0207 9.28767 11.1587C8.89713 11.2967 8.69243 11.7252 8.83047 12.1158C9.11513 12.9212 9.88319 13.5 10.7884 13.5H11.6725C12.8192 13.5 13.7488 12.5704 13.7488 11.4238V11.2027C13.7488 10.6502 13.5329 10.148 13.181 9.77601C13.3997 9.45914 13.5277 9.0749 13.5277 8.66074C13.5277 7.5751 12.6476 6.69501 11.562 6.69502Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186371\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"heart\", \"keywords\": [ \"reward\", \"social\", \"rating\", \"media\", \"heart\", \"it\", \"like\", \"favorite\", \"love\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186374)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.0241 1.08512C5.00463 1.15617 6.02634 1.63415 7.00438 2.60152C7.98217 1.63442 9.00299 1.15735 9.9826 1.08788C11.0882 1.00947 12.0656 1.45632 12.7728 2.16133C14.1631 3.54747 14.5997 6.05569 12.8745 7.78093C12.8687 7.78675 12.8627 7.79243 12.8566 7.79795L7.34004 12.7949C7.14953 12.9674 6.85922 12.9674 6.66871 12.7949L1.15213 7.79795C1.14603 7.79243 1.14007 7.78675 1.13425 7.78093C-0.599975 6.0467 -0.165703 3.53819 1.22919 2.15313C1.93808 1.44923 2.91747 1.00494 4.0241 1.08512Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186374\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"help-chat-2\", \"keywords\": [ \"bubble\", \"help\", \"mark\", \"message\", \"query\", \"question\", \"speech\", \"circle\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186377)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.99834 0.0463878C8.04479 0.0457933 9.07788 0.281389 10.0206 0.735616C10.9634 1.18986 11.7915 1.85105 12.4431 2.66988C13.0948 3.48871 13.5532 4.44407 13.7843 5.46474C14.0153 6.48542 14.013 7.54508 13.7775 8.56473C13.542 9.58439 13.0794 10.5377 12.4242 11.3537C11.769 12.1697 10.938 12.8272 9.99326 13.2774C9.04851 13.7275 8.01436 13.9586 6.96787 13.9534C5.97696 13.9485 4.9992 13.7319 4.09988 13.3188L0.633897 13.9455C0.456812 13.9775 0.27618 13.9119 0.160928 13.7737C0.0456747 13.6355 0.0135827 13.446 0.076897 13.2776L1.07113 10.6324C0.460779 9.63611 0.110558 8.50074 0.0548176 7.3309C-0.00504529 6.07452 0.276994 4.82543 0.870894 3.71667C1.4648 2.60791 2.34832 1.681 3.42736 1.03467C4.50635 0.388357 5.74059 0.0468069 6.99834 0.0463878ZM6.61953 4.4449C6.80137 4.36958 7.00147 4.34987 7.19451 4.38827C7.38755 4.42667 7.56488 4.52145 7.70405 4.66062C7.84323 4.7998 7.93801 4.97712 7.97641 5.17017C8.01481 5.36321 7.9951 5.56331 7.91978 5.74515C7.84446 5.92699 7.7169 6.08242 7.55325 6.19177C7.38959 6.30112 7.19719 6.35949 7.00036 6.35949C6.65518 6.35949 6.37536 6.63931 6.37536 6.98449V8.0646C6.37536 8.40978 6.65518 8.6896 7.00036 8.6896C7.34554 8.6896 7.62536 8.40978 7.62536 8.0646V7.52074C7.84525 7.45701 8.05529 7.35968 8.24771 7.23111C8.61693 6.9844 8.9047 6.63376 9.07463 6.22351C9.24456 5.81325 9.28902 5.36183 9.20239 4.9263C9.11576 4.49078 8.90193 4.09073 8.58794 3.77674C8.27394 3.46275 7.87389 3.24892 7.43837 3.16228C7.00285 3.07565 6.55142 3.12012 6.14117 3.29005C5.73092 3.45998 5.38027 3.74775 5.13357 4.11697C4.88687 4.48618 4.75519 4.92026 4.75519 5.36432C4.75519 5.70949 5.03501 5.98932 5.38019 5.98932C5.72537 5.98932 6.00519 5.70949 6.00519 5.36432C6.00519 5.16749 6.06356 4.97508 6.17291 4.81143C6.28226 4.64777 6.43768 4.52022 6.61953 4.4449ZM7.00039 11.3898C6.50702 11.3887 6.1074 10.9884 6.1074 10.4948C6.1074 10.0005 6.50812 9.59983 7.00243 9.59983C7.0031 9.59983 7.00378 9.59983 7.00445 9.59983C7.49782 9.60093 7.89744 10.0012 7.89744 10.4948C7.89744 10.9891 7.49672 11.3898 7.00241 11.3898C7.00174 11.3898 7.00106 11.3898 7.00039 11.3898Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186377\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"help-question-1\", \"keywords\": [ \"circle\", \"faq\", \"frame\", \"help\", \"info\", \"mark\", \"more\", \"query\", \"question\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186380)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7ZM5.75 5.25C5.75 4.55964 6.30964 4 7 4C7.69036 4 8.25 4.55964 8.25 5.25C8.25 5.94036 7.69036 6.5 7 6.5C6.58579 6.5 6.25 6.83579 6.25 7.25V7.89648C6.25 8.3107 6.58579 8.64648 7 8.64648C7.41421 8.64648 7.75 8.3107 7.75 7.89648C8.90425 7.57002 9.75 6.50878 9.75 5.25C9.75 3.73122 8.51878 2.5 7 2.5C5.48122 2.5 4.25 3.73122 4.25 5.25C4.25 5.66421 4.58579 6 5 6C5.41421 6 5.75 5.66421 5.75 5.25ZM8 10.5C8 11.0523 7.55229 11.5 7 11.5C6.44771 11.5 6 11.0523 6 10.5C6 9.94771 6.44771 9.5 7 9.5C7.55229 9.5 8 9.94771 8 10.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186380\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"hierarchy-10\", \"keywords\": [ \"node\", \"organization\", \"links\", \"structure\", \"link\", \"nodes\", \"network\", \"hierarchy\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.25 1.25C1.25 0.559644 1.80964 0 2.5 0H4.5C5.19036 0 5.75 0.559644 5.75 1.25V3.25C5.75 3.94036 5.19036 4.5 4.5 4.5H4.25V5.99998C4.8953 5.516 5.68394 5.25 6.5 5.25H8.5V5C8.5 4.30964 9.05964 3.75 9.75 3.75H11.75C12.4404 3.75 13 4.30964 13 5V7C13 7.69036 12.4404 8.25 11.75 8.25H9.75C9.05964 8.25 8.5 7.69036 8.5 7V6.75H6.5C5.90326 6.75 5.33097 6.98705 4.90901 7.40901C4.48705 7.83097 4.25 8.40326 4.25 9V9.5H4.5C5.19036 9.5 5.75 10.0596 5.75 10.75V12.75C5.75 13.4404 5.19036 14 4.5 14H2.5C1.80964 14 1.25 13.4404 1.25 12.75V10.75C1.25 10.0596 1.80964 9.5 2.5 9.5H2.75V9V4.5H2.5C1.80964 4.5 1.25 3.94036 1.25 3.25V1.25Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"hierarchy-13\", \"keywords\": [ \"node\", \"organization\", \"links\", \"structure\", \"link\", \"nodes\", \"network\", \"hierarchy\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186395)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.5 0H13C13.5523 0 14 0.447715 14 1V3.5C14 4.05228 13.5523 4.5 13 4.5H12.0821L11.1833 7.01676C11.648 7.10284 12 7.51031 12 8V10.5C12 11.0523 11.5523 11.5 11 11.5H8.5C7.94772 11.5 7.5 11.0523 7.5 10.5V8.69541L5.62635 6.99209C5.58497 6.99731 5.5428 7 5.5 7H4.23131L3.69167 9.51835C4.15226 9.60776 4.5 10.0133 4.5 10.5V13C4.5 13.5523 4.05228 14 3.5 14H1C0.447715 14 0 13.5523 0 13V10.5C0 9.94771 0.447715 9.5 1 9.5H2.16155L2.70663 6.95628C2.2975 6.83091 2 6.4502 2 6V3.5C2 2.94772 2.44772 2.5 3 2.5H5.5C6.05228 2.5 6.5 2.94772 6.5 3.5V5.75913L8.00743 7.12953C8.15283 7.04707 8.32092 7 8.5 7H9.59646L10.4893 4.49994C9.94196 4.49423 9.5 4.04873 9.5 3.5V1C9.5 0.447715 9.94772 0 10.5 0Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186395\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"hierarchy-14\", \"keywords\": [ \"node\", \"organization\", \"links\", \"structure\", \"link\", \"nodes\", \"network\", \"hierarchy\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186398)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.25 1V4.5C4.25 5.05228 4.69772 5.5 5.25 5.5H6.25V6.75H3C2.0335 6.75 1.25 7.5335 1.25 8.5V10H1C0.447715 10 0 10.4477 0 11V13C0 13.5523 0.447715 14 1 14H3C3.55228 14 4 13.5523 4 13V11C4 10.4477 3.55228 10 3 10H2.75V8.5C2.75 8.36193 2.86193 8.25 3 8.25H6.25V10H6C5.44772 10 5 10.4477 5 11V13C5 13.5523 5.44772 14 6 14H8C8.55228 14 9 13.5523 9 13V11C9 10.4477 8.55228 10 8 10H7.75V8.25H11C11.1381 8.25 11.25 8.36193 11.25 8.5V10H11C10.4477 10 10 10.4477 10 11V13C10 13.5523 10.4477 14 11 14H13C13.5523 14 14 13.5523 14 13V11C14 10.4477 13.5523 10 13 10H12.75V8.5C12.75 7.5335 11.9665 6.75 11 6.75H7.75V5.5H8.75C9.30228 5.5 9.75 5.05228 9.75 4.5V1C9.75 0.447715 9.30228 0 8.75 0H5.25C4.69772 0 4.25 0.447715 4.25 1Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186398\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"hierarchy-2\", \"keywords\": [ \"node\", \"organization\", \"links\", \"structure\", \"link\", \"nodes\", \"network\", \"hierarchy\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186383)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.5 0.5C5.67157 0.5 5 1.17157 5 2V3C5 3.74325 5.54057 4.36024 6.25 4.47926V6.25H3C2.0335 6.25 1.25 7.0335 1.25 8V9.52074C0.540572 9.63976 0 10.2568 0 11V12C0 12.8284 0.671573 13.5 1.5 13.5H2.5C3.32843 13.5 4 12.8284 4 12V11C4 10.2568 3.45943 9.63976 2.75 9.52074V8C2.75 7.86193 2.86193 7.75 3 7.75H6.25V9.52074C5.54057 9.63976 5 10.2568 5 11V12C5 12.8284 5.67157 13.5 6.5 13.5H7.5C8.32843 13.5 9 12.8284 9 12V11C9 10.2568 8.45943 9.63976 7.75 9.52074V7.75H11C11.1381 7.75 11.25 7.86193 11.25 8V9.52074C10.5406 9.63976 10 10.2568 10 11V12C10 12.8284 10.6716 13.5 11.5 13.5H12.5C13.3284 13.5 14 12.8284 14 12V11C14 10.2568 13.4594 9.63976 12.75 9.52074V8C12.75 7.0335 11.9665 6.25 11 6.25H7.75V4.47926C8.45943 4.36024 9 3.74325 9 3V2C9 1.17157 8.32843 0.5 7.5 0.5H6.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186383\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"hierarchy-4\", \"keywords\": [ \"node\", \"organization\", \"links\", \"structure\", \"link\", \"nodes\", \"network\", \"hierarchy\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.75 1.5C1.75 0.671573 2.42157 0 3.25 0H4.75C5.57843 0 6.25 0.671573 6.25 1.5V3C6.25 3.82843 5.57843 4.5 4.75 4.5C4.75 5.09674 4.98705 5.66903 5.40901 6.09099C5.83097 6.51295 6.40326 6.75 7 6.75H8C8 5.92157 8.67157 5.25 9.5 5.25H11C11.8284 5.25 12.5 5.92157 12.5 6.75V8.25C12.5 9.07843 11.8284 9.75 11 9.75H9.5C8.67157 9.75 8 9.07843 8 8.25H7C6.18394 8.25 5.3953 7.984 4.75 7.50002V9.5C5.57843 9.5 6.25 10.1716 6.25 11V12.5C6.25 13.3284 5.57843 14 4.75 14H3.25C2.42157 14 1.75 13.3284 1.75 12.5V11C1.75 10.1716 2.42157 9.5 3.25 9.5V4.5C2.42157 4.5 1.75 3.82843 1.75 3V1.5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"hierarchy-7\", \"keywords\": [ \"node\", \"organization\", \"links\", \"structure\", \"link\", \"nodes\", \"network\", \"hierarchy\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186389)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.69131 0.0380748C8.87815 0.115466 8.99997 0.297784 8.99997 0.500015V1.75001H9.75C11.2688 1.75001 12.5 2.98123 12.5 4.50001V9.50001C13.3284 9.50001 14 10.1716 14 11V12.5C14 13.3284 13.3284 14 12.5 14H11C10.1716 14 9.5 13.3284 9.5 12.5V11C9.5 10.1716 10.1716 9.50001 11 9.50001V4.50001C11 3.80965 10.4404 3.25001 9.75 3.25001H8.99997V4.50001C8.99997 4.70224 8.87815 4.88456 8.69131 4.96195C8.50447 5.03934 8.28942 4.99656 8.14642 4.85356L6.14642 2.85356C5.95115 2.6583 5.95115 2.34172 6.14642 2.14646L8.14642 0.146462C8.28942 0.0034622 8.50447 -0.0393156 8.69131 0.0380748ZM1.5 9.50001V4.50001C0.671573 4.50001 0 3.82844 0 3.00001V1.50001C0 0.671588 0.671573 1.45805e-05 1.5 1.45805e-05H3C3.82843 1.45805e-05 4.5 0.671588 4.5 1.50001V3.00001C4.5 3.82844 3.82843 4.50001 3 4.50001V9.50001C3.82843 9.50001 4.5 10.1716 4.5 11V12.5C4.5 13.3284 3.82843 14 3 14H1.5C0.671573 14 0 13.3284 0 12.5V11C0 10.1716 0.671573 9.50001 1.5 9.50001Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186389\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"home-3\", \"keywords\": [ \"home\", \"house\", \"roof\", \"shelter\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186401)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.318182 6.0449C0.115244 6.23405 0 6.499 0 6.77642V12.5C0 13.3284 0.671573 14 1.5 14H6V11C6 10.4477 6.44772 10 7 10C7.55228 10 8 10.4477 8 11V14H12.5C13.3284 14 14 13.3284 14 12.5V6.77642C14 6.499 13.8848 6.23405 13.6818 6.0449L7.3254 0.120372C7.13815 -0.0401239 6.86185 -0.0401241 6.6746 0.120372L0.318182 6.0449Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186401\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"home-4\", \"keywords\": [ \"home\", \"house\", \"roof\", \"shelter\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186404)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.70711 1.29289C7.31658 0.90237 6.68342 0.90237 6.29289 1.29289L0.292893 7.29289C-0.097631 7.68342 -0.097631 8.31658 0.292893 8.70711C0.683417 9.09763 1.31658 9.09763 1.70711 8.70711L2 8.41421V13C2 13.5523 2.44772 14 3 14H11C11.5523 14 12 13.5523 12 13V8.41421L12.2929 8.70711C12.6834 9.09763 13.3166 9.09763 13.7071 8.70711C14.0976 8.31658 14.0976 7.68342 13.7071 7.29289L7.70711 1.29289Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186404\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"horizontal-menu-circle\", \"keywords\": [ \"navigation\", \"dots\", \"three\", \"circle\", \"button\", \"horizontal\", \"menu\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186407)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14ZM4 8C4.55228 8 5 7.55228 5 7C5 6.44772 4.55228 6 4 6C3.44772 6 3 6.44772 3 7C3 7.55228 3.44772 8 4 8ZM8 7C8 7.55228 7.55228 8 7 8C6.44772 8 6 7.55228 6 7C6 6.44772 6.44772 6 7 6C7.55228 6 8 6.44772 8 7ZM10 8C10.5523 8 11 7.55228 11 7C11 6.44772 10.5523 6 10 6C9.44771 6 9 6.44772 9 7C9 7.55228 9.44771 8 10 8Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186407\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"humidity-none\", \"keywords\": [ \"humidity\", \"drop\", \"weather\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186410)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 0.5L7.40024 0.20032C7.30583 0.0742289 7.15752 0 7 0C6.84248 0 6.69417 0.0742289 6.59976 0.20032L7 0.5ZM11.7175 10.6568C11.9028 10.1294 12 9.5697 12 9C12 8.26038 11.6759 7.33231 11.2508 6.41179C10.8169 5.47212 10.2427 4.46809 9.6749 3.5527C9.10606 2.63561 8.53804 1.79848 8.11267 1.19113C7.89982 0.887228 7.72229 0.640307 7.59774 0.469031C7.53545 0.383381 7.48638 0.316613 7.45274 0.271058L7.4141 0.218916L7.40397 0.205316L7.40129 0.201729L7.40024 0.20032C7.40018 0.200236 7.40024 0.20032 7 0.5C6.59976 0.20032 6.59982 0.200236 6.59976 0.20032L6.59871 0.201729L6.59603 0.205316L6.5859 0.218916L6.54726 0.271058C6.51362 0.316613 6.46455 0.383381 6.40226 0.469031C6.27771 0.640307 6.10018 0.887228 5.88733 1.19113C5.48957 1.75905 4.96708 2.52789 4.43582 3.37516L1.28033 0.21967C0.987437 -0.0732233 0.512563 -0.0732233 0.21967 0.21967C-0.0732233 0.512563 -0.0732233 0.987437 0.21967 1.28033L12.7197 13.7803C13.0126 14.0732 13.4874 14.0732 13.7803 13.7803C14.0732 13.4874 14.0732 13.0126 13.7803 12.7197L11.7175 10.6568ZM3.02749 5.83695L10.1076 12.917C9.22802 13.6149 8.13387 14 7 14C5.67392 14 4.40215 13.4732 3.46447 12.5355C2.52678 11.5979 2 10.3261 2 9C2 8.26038 2.32411 7.33231 2.74919 6.41179C2.83665 6.22237 2.92982 6.03034 3.02749 5.83695Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186410\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"image-blur\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186413)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0ZM5.39583 9.62498C5.95962 9.62498 6.41667 9.16794 6.41667 8.60415C6.41667 8.04036 5.95962 7.58331 5.39583 7.58331C4.83204 7.58331 4.375 8.04036 4.375 8.60415C4.375 9.16794 4.83204 9.62498 5.39583 9.62498ZM7.58333 8.60415C7.58333 9.16794 8.04038 9.62498 8.60417 9.62498C9.16796 9.62498 9.625 9.16794 9.625 8.60415C9.625 8.04036 9.16796 7.58331 8.60417 7.58331C8.04038 7.58331 7.58333 8.04036 7.58333 8.60415ZM8.60417 4.37502C8.04038 4.37502 7.58333 4.83206 7.58333 5.39585C7.58333 5.95964 8.04038 6.41669 8.60417 6.41669C9.16796 6.41669 9.625 5.95964 9.625 5.39585C9.625 4.83206 9.16796 4.37502 8.60417 4.37502ZM6.41667 5.39585C6.41667 4.83206 5.95962 4.37502 5.39583 4.37502C4.83204 4.37502 4.375 4.83206 4.375 5.39585C4.375 5.95964 4.83204 6.41669 5.39583 6.41669C5.95962 6.41669 6.41667 5.95964 6.41667 5.39585ZM5.54166 3.41296C5.18697 3.41296 4.89944 3.12543 4.89944 2.77074C4.89944 2.41605 5.18697 2.12861 5.54166 2.12861C5.89635 2.12861 6.18388 2.41605 6.18388 2.77074C6.18388 3.12543 5.89635 3.41296 5.54166 3.41296ZM4.89944 11.2291C4.89944 11.5838 5.18697 11.8713 5.54166 11.8713C5.89635 11.8713 6.18388 11.5838 6.18388 11.2291C6.18388 10.8744 5.89635 10.587 5.54166 10.587C5.18697 10.587 4.89944 10.8744 4.89944 11.2291ZM2.77085 6.18384C2.41616 6.18384 2.12863 5.89631 2.12863 5.54162C2.12863 5.18693 2.41616 4.89948 2.77085 4.89948C3.12554 4.89948 3.41307 5.18693 3.41307 5.54162C3.41307 5.89631 3.12554 6.18384 2.77085 6.18384ZM10.587 5.54162C10.587 5.89631 10.8745 6.18384 11.2292 6.18384C11.5839 6.18384 11.8714 5.89631 11.8714 5.54162C11.8714 5.18693 11.5839 4.89948 11.2292 4.89948C10.8745 4.89948 10.587 5.18693 10.587 5.54162ZM2.77085 9.10052C2.41616 9.10052 2.12863 8.81299 2.12863 8.4583C2.12863 8.10361 2.41616 7.81617 2.77085 7.81617C3.12554 7.81617 3.41307 8.10361 3.41307 8.4583C3.41307 8.81299 3.12554 9.10052 2.77085 9.10052ZM7.81611 2.77074C7.81611 3.12543 8.10364 3.41296 8.45833 3.41296C8.81302 3.41296 9.10056 3.12543 9.10056 2.77074C9.10056 2.41605 8.81302 2.12861 8.45833 2.12861C8.10364 2.12861 7.81611 2.41605 7.81611 2.77074ZM11.2292 9.10052C10.8745 9.10052 10.587 8.81299 10.587 8.4583C10.587 8.10361 10.8745 7.81617 11.2292 7.81617C11.5839 7.81617 11.8714 8.10361 11.8714 8.4583C11.8714 8.81299 11.5839 9.10052 11.2292 9.10052ZM7.81611 11.2291C7.81611 11.5838 8.10364 11.8713 8.45833 11.8713C8.81302 11.8713 9.10056 11.5838 9.10056 11.2291C9.10056 10.8744 8.81302 10.587 8.45833 10.587C8.10364 10.587 7.81611 10.8744 7.81611 11.2291Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186413\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"image-saturation\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186416)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.99933 0.75L6.46912 0.219549C6.76198 -0.073183 7.23667 -0.073183 7.52954 0.219549L6.99933 0.75ZM6.99933 12.5C6.16179 12.5 4.92721 12.1722 4.203 11.5207C3.45432 10.8472 3.00077 9.9068 2.93952 8.90259C2.94443 8.22927 3.19057 7.4249 3.61501 6.5552C4.03875 5.68692 4.61154 4.81102 5.19696 4.02107C5.78066 3.23343 6.36613 2.54565 6.80638 2.05425C6.87444 1.97829 6.93894 1.90711 6.99933 1.8411V12.5ZM6.99933 0.75C7.52954 0.219549 7.52941 0.219421 7.52954 0.219549L7.53114 0.221157L7.53449 0.224518L7.5463 0.236428L7.58963 0.280513C7.62698 0.318715 7.68089 0.374318 7.74899 0.445789C7.88514 0.588684 8.07829 0.795286 8.30947 1.05332C8.77106 1.56853 9.38807 2.29301 10.0068 3.12797C10.6239 3.96062 11.2536 4.91765 11.7317 5.89732C12.2048 6.86674 12.5592 7.91785 12.5592 8.92308C12.5592 8.93693 12.5588 8.95079 12.5581 8.96462C12.4799 10.3727 11.8474 11.6926 10.7989 12.6359C9.72055 13.6059 8.08574 14 6.99933 14C5.91291 14 4.2781 13.6059 3.19981 12.6359C2.15122 11.6926 1.51871 10.3727 1.4406 8.96462C1.43983 8.95079 1.43945 8.93693 1.43945 8.92308C1.43945 7.91785 1.79387 6.86674 2.26697 5.89732C2.74508 4.91765 3.37476 3.96062 3.99182 3.12797C4.61058 2.29301 5.22759 1.56853 5.68918 1.05332C5.92036 0.795286 6.11351 0.588684 6.24967 0.445789C6.31776 0.374318 6.37167 0.318715 6.40902 0.280513L6.45235 0.236428L6.46416 0.224518L6.46751 0.221157L6.46912 0.219549C6.46925 0.219421 6.46912 0.219549 6.99933 0.75Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186416\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"information-circle\", \"keywords\": [ \"information\", \"frame\", \"info\", \"more\", \"help\", \"point\", \"circle\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186419)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14ZM5.5 9.375C5.15482 9.375 4.875 9.65482 4.875 10C4.875 10.3452 5.15482 10.625 5.5 10.625H7H8.5C8.84518 10.625 9.125 10.3452 9.125 10C9.125 9.65482 8.84518 9.375 8.5 9.375H7.625V6.5C7.625 6.15482 7.34518 5.875 7 5.875H6C5.65482 5.875 5.375 6.15482 5.375 6.5C5.375 6.84518 5.65482 7.125 6 7.125H6.375V9.375H5.5ZM8 4C8 4.55228 7.55228 5 7 5C6.44772 5 6 4.55228 6 4C6 3.44772 6.44772 3 7 3C7.55228 3 8 3.44772 8 4Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186419\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"input-box\", \"keywords\": [ \"cursor\", \"text\", \"formatting\", \"type\", \"format\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4 1.75C3.58579 1.75 3.25 2.08579 3.25 2.5C3.25 2.91421 3.58579 3.25 4 3.25H4.25V10.75H4C3.58579 10.75 3.25 11.0858 3.25 11.5C3.25 11.9142 3.58579 12.25 4 12.25H5H6C6.41421 12.25 6.75 11.9142 6.75 11.5C6.75 11.0858 6.41421 10.75 6 10.75H5.75V3.25H6C6.41421 3.25 6.75 2.91421 6.75 2.5C6.75 2.08579 6.41421 1.75 6 1.75H5H4ZM3 4.50049H1C0.447715 4.50049 0 4.9482 0 5.50049V8.50049C0 9.05277 0.447716 9.50049 1 9.50049H3V4.50049ZM7 9.50049H13C13.5523 9.50049 14 9.05277 14 8.50049V5.50049C14 4.9482 13.5523 4.50049 13 4.50049H7V9.50049Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"insert-side\", \"keywords\": [ \"points\", \"bullet\", \"align\", \"paragraph\", \"formatting\", \"bullets\", \"text\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186425)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.25 0.25C7.83579 0.25 7.5 0.585786 7.5 1C7.5 1.41421 7.83579 1.75 8.25 1.75H13.25C13.6642 1.75 14 1.41421 14 1C14 0.585786 13.6642 0.25 13.25 0.25H8.25ZM0 1.49951C0 0.947227 0.447715 0.499512 1 0.499512H5C5.55228 0.499512 6 0.947227 6 1.49951V5.5C6 6.05228 5.55228 6.5 5 6.5H1C0.447715 6.5 0 6.05228 0 5.5V1.49951ZM0 8.49951C0 7.94723 0.447715 7.49951 1 7.49951H5C5.55228 7.49951 6 7.94723 6 8.49951V12.5C6 13.0523 5.55228 13.5 5 13.5H1C0.447715 13.5 0 13.0523 0 12.5V8.49951ZM7.5 4C7.5 3.58579 7.83579 3.25 8.25 3.25H13.25C13.6642 3.25 14 3.58579 14 4C14 4.41421 13.6642 4.75 13.25 4.75H8.25C7.83579 4.75 7.5 4.41421 7.5 4ZM8.25 6.25C7.83579 6.25 7.5 6.58579 7.5 7C7.5 7.41421 7.83579 7.75 8.25 7.75H13.25C13.6642 7.75 14 7.41421 14 7C14 6.58579 13.6642 6.25 13.25 6.25H8.25ZM7.5 10C7.5 9.58579 7.83579 9.25 8.25 9.25H13.25C13.6642 9.25 14 9.58579 14 10C14 10.4142 13.6642 10.75 13.25 10.75H8.25C7.83579 10.75 7.5 10.4142 7.5 10ZM8.25 12.25C7.83579 12.25 7.5 12.5858 7.5 13C7.5 13.4142 7.83579 13.75 8.25 13.75H13.25C13.6642 13.75 14 13.4142 14 13C14 12.5858 13.6642 12.25 13.25 12.25H8.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186425\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"insert-top-left\", \"keywords\": [ \"alignment\", \"wrap\", \"formatting\", \"paragraph\", \"image\", \"left\", \"text\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186428)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.25 0.25C8.83579 0.25 8.5 0.585786 8.5 1C8.5 1.41421 8.83579 1.75 9.25 1.75H13.25C13.6642 1.75 14 1.41421 14 1C14 0.585786 13.6642 0.25 13.25 0.25H9.25ZM1 0.499512C0.447715 0.499512 0 0.947227 0 1.49951V6.49951C0 7.0518 0.447715 7.49951 1 7.49951H6C6.55228 7.49951 7 7.0518 7 6.49951V1.49951C7 0.947227 6.55228 0.499512 6 0.499512H1ZM8.5 4C8.5 3.58579 8.83579 3.25 9.25 3.25H13.25C13.6642 3.25 14 3.58579 14 4C14 4.41421 13.6642 4.75 13.25 4.75H9.25C8.83579 4.75 8.5 4.41421 8.5 4ZM9.25 6.25C8.83579 6.25 8.5 6.58579 8.5 7C8.5 7.41421 8.83579 7.75 9.25 7.75H13.25C13.6642 7.75 14 7.41421 14 7C14 6.58579 13.6642 6.25 13.25 6.25H9.25ZM0 10C0 9.58579 0.335786 9.25 0.75 9.25H13.25C13.6642 9.25 14 9.58579 14 10C14 10.4142 13.6642 10.75 13.25 10.75H0.75C0.335786 10.75 0 10.4142 0 10ZM0.75 12.25C0.335786 12.25 0 12.5858 0 13C0 13.4142 0.335786 13.75 0.75 13.75H13.25C13.6642 13.75 14 13.4142 14 13C14 12.5858 13.6642 12.25 13.25 12.25H0.75Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186428\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"insert-top-right\", \"keywords\": [ \"paragraph\", \"image\", \"text\", \"alignment\", \"wrap\", \"right\", \"formatting\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186431)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.75 0.25C0.335786 0.25 0 0.585786 0 1C0 1.41421 0.335786 1.75 0.75 1.75H4.75C5.16421 1.75 5.5 1.41421 5.5 1C5.5 0.585786 5.16421 0.25 4.75 0.25H0.75ZM8 0.499512C7.44772 0.499512 7 0.947227 7 1.49951V6.49951C7 7.0518 7.44772 7.49951 8 7.49951H13C13.5523 7.49951 14 7.0518 14 6.49951V1.49951C14 0.947227 13.5523 0.499512 13 0.499512H8ZM0 4C0 3.58579 0.335786 3.25 0.75 3.25H4.75C5.16421 3.25 5.5 3.58579 5.5 4C5.5 4.41421 5.16421 4.75 4.75 4.75H0.75C0.335786 4.75 0 4.41421 0 4ZM0.75 6.25C0.335786 6.25 0 6.58579 0 7C0 7.41421 0.335786 7.75 0.75 7.75H4.75C5.16421 7.75 5.5 7.41421 5.5 7C5.5 6.58579 5.16421 6.25 4.75 6.25H0.75ZM0 10C0 9.58579 0.335786 9.25 0.75 9.25H13.25C13.6642 9.25 14 9.58579 14 10C14 10.4142 13.6642 10.75 13.25 10.75H0.75C0.335786 10.75 0 10.4142 0 10ZM0.75 12.25C0.335786 12.25 0 12.5858 0 13C0 13.4142 0.335786 13.75 0.75 13.75H13.25C13.6642 13.75 14 13.4142 14 13C14 12.5858 13.6642 12.25 13.25 12.25H0.75Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186431\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"invisible-1\", \"keywords\": [ \"disable\", \"eye\", \"eyeball\", \"hide\", \"off\", \"view\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186434)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.264592 1.32428C-0.0283014 1.03138 -0.0283014 0.556508 0.264592 0.263615C0.557486 -0.0292779 1.03236 -0.0292779 1.32525 0.263615L3.95386 2.89222C4.8617 2.39338 5.89384 2.04019 7.00094 2.04019C8.53293 2.04019 9.92137 2.71648 11.0356 3.51919C12.1538 4.32481 13.039 5.28882 13.5749 5.9365L13.5794 5.94198C13.8187 6.23977 13.9447 6.61713 13.9447 7.00004C13.9447 7.38295 13.8187 7.76032 13.5794 8.0581L13.5749 8.06359C13.0863 8.65413 12.3072 9.50766 11.326 10.2643L13.7374 12.6756C14.0303 12.9685 14.0303 13.4434 13.7374 13.7363C13.4445 14.0292 12.9696 14.0292 12.6767 13.7363L0.264592 1.32428ZM9.01808 7.95645C9.15579 7.66655 9.23285 7.34226 9.23285 6.99996C9.23285 5.76732 8.23359 4.76806 7.00094 4.76806C6.65865 4.76806 6.33435 4.84511 6.04445 4.98282L9.01808 7.95645ZM0.427028 5.9365C0.746476 5.55041 1.19006 5.05192 1.73312 4.54159L8.83714 11.6456C8.25652 11.8417 7.64127 11.9599 7.00094 11.9599C5.46896 11.9599 4.08052 11.2836 2.96635 10.4809C1.84815 9.67528 0.962921 8.71127 0.427028 8.06359L0.422554 8.0581C0.183239 7.76032 0.057235 7.38295 0.057235 7.00004C0.057235 6.61713 0.183239 6.23977 0.422554 5.94198L0.427028 5.9365Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186434\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"invisible-2\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186437)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.61492 3.54048C2.10127 3.27879 2.70768 3.4609 2.96937 3.94725C3.1652 4.31117 3.42056 4.64709 3.72493 4.94317C3.75122 4.96564 3.77657 4.98967 3.80084 5.01525C4.60698 5.76234 5.73386 6.2376 7.00053 6.2376C8.26312 6.2376 9.38681 5.7654 10.1924 5.02248C10.2211 4.99166 10.2513 4.96303 10.2828 4.93662C10.5843 4.64214 10.8373 4.30849 11.0317 3.94725C11.2934 3.4609 11.8998 3.27879 12.3861 3.54048C12.8725 3.80218 13.0546 4.40859 12.7929 4.89494C12.6617 5.13871 12.5139 5.37261 12.3509 5.59541L13.6423 6.67774C14.0656 7.03249 14.1212 7.66321 13.7664 8.0865C13.4117 8.50979 12.781 8.56535 12.3577 8.21061L10.918 7.0041C10.562 7.25726 10.1777 7.47626 9.77092 7.65652L10.3877 9.2098C10.5915 9.7231 10.3406 10.3044 9.82731 10.5082C9.31401 10.712 8.73267 10.4611 8.52886 9.94789L7.83013 8.1882C7.55811 8.22082 7.28119 8.2376 7.00053 8.2376C6.71841 8.2376 6.44006 8.22064 6.16666 8.18769L5.46774 9.94789C5.26392 10.4611 4.68258 10.712 4.16928 10.5082C3.65598 10.3044 3.4051 9.7231 3.60891 9.2098L4.22635 7.65484C3.82074 7.4748 3.43756 7.25624 3.08244 7.00371L1.64234 8.21061C1.21905 8.56535 0.588336 8.50979 0.23359 8.0865C-0.121155 7.66321 -0.0655896 7.03249 0.3577 6.67774L1.64979 5.59488C1.48693 5.37223 1.33923 5.13852 1.20816 4.89494C0.946463 4.40859 1.12858 3.80218 1.61492 3.54048Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186437\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"jump-object\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186440)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1C0 0.585786 0.335786 0.25 0.75 0.25H13.25C13.6642 0.25 14 0.585786 14 1C14 1.41421 13.6642 1.75 13.25 1.75H0.75C0.335786 1.75 0 1.41421 0 1ZM3.50003 5.00049C3.50003 4.17206 4.1716 3.50049 5.00003 3.50049H9.00003C9.82846 3.50049 10.5 4.17206 10.5 5.00049V9.00049C10.5 9.82891 9.82846 10.5005 9.00003 10.5005H5.00003C4.1716 10.5005 3.50003 9.82892 3.50003 9.00049V5.00049ZM0.75 12.25C0.335786 12.25 0 12.5858 0 13C0 13.4142 0.335786 13.75 0.75 13.75H13.25C13.6642 13.75 14 13.4142 14 13C14 12.5858 13.6642 12.25 13.25 12.25H0.75Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186440\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"key\", \"keywords\": [ \"entry\", \"key\", \"lock\", \"login\", \"pass\", \"unlock\", \"access\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.2929 1.29289C10.6834 0.90237 11.3166 0.90237 11.7071 1.29289L13.7071 3.29289C14.0976 3.68342 14.0976 4.31658 13.7071 4.70711C13.3166 5.09763 12.6834 5.09763 12.2929 4.70711L11 3.41421L9.91422 4.50001L11.2071 5.79289C11.5976 6.18342 11.5976 6.81658 11.2071 7.20711C10.8166 7.59763 10.1834 7.59763 9.79289 7.20711L8.50001 5.91422L7.44619 6.96804C7.79807 7.56355 8 8.2582 8 9C8 11.2091 6.20914 13 4 13C1.79086 13 0 11.2091 0 9C0 6.79086 1.79086 5 4 5C4.74181 5 5.43646 5.20193 6.03198 5.55383L7.78929 3.79651L7.79289 3.79289L7.79651 3.78929L10.2929 1.29289ZM2 9C2 7.89543 2.89543 7 4 7C5.10457 7 6 7.89543 6 9C6 10.1046 5.10457 11 4 11C2.89543 11 2 10.1046 2 9Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"keyhole-lock-circle\", \"keywords\": [ \"circle\", \"frame\", \"key\", \"keyhole\", \"lock\", \"locked\", \"secure\", \"security\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186446)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7ZM8 7.73244C8.5978 7.38663 9 6.74028 9 6C9 4.89543 8.10457 4 7 4C5.89543 4 5 4.89543 5 6C5 6.74028 5.4022 7.38663 6 7.73244V9.5C6 10.0523 6.44772 10.5 7 10.5C7.55228 10.5 8 10.0523 8 9.5V7.73244Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186446\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"lasso-tool\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186449)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.47469 1.87066C2.58067 0.785351 4.34703 0 7 0C9.65297 0 11.4193 0.785351 12.5253 1.87066C13.6165 2.94151 14 4.24304 14 5.16592C14 6.0888 13.6165 7.39034 12.5253 8.46118C11.4193 9.5465 9.65297 10.3318 7 10.3318C6.97894 10.3318 6.95793 10.3318 6.93698 10.3317C7.56342 11.1323 8.01808 11.9847 8.01508 13.247C8.0141 13.6612 7.67752 13.9962 7.2633 13.9952C6.84909 13.9942 6.5141 13.6576 6.51508 13.2434C6.51642 12.6791 6.397 12.2552 6.14608 11.8233C6.0587 12.0806 5.91982 12.3469 5.71269 12.5909C5.27089 13.1111 4.57845 13.4587 3.62308 13.4587C2.66771 13.4587 1.97527 13.1111 1.53348 12.5909C1.11583 12.099 0.975677 11.5161 0.975677 11.0999C0.975677 10.6838 1.11583 10.1008 1.53348 9.60901C1.71576 9.39436 1.94072 9.2091 2.20921 9.06553C1.93744 8.87828 1.69318 8.67558 1.47469 8.46118C0.383463 7.39034 0 6.0888 0 5.16592C0 4.24304 0.383463 2.94151 1.47469 1.87066ZM2.52531 2.94128C1.74154 3.7104 1.5 4.61683 1.5 5.16592C1.5 5.71502 1.74154 6.62144 2.52531 7.39057C3.29433 8.14522 4.65297 8.83185 7 8.83185C9.34703 8.83185 10.7057 8.14522 11.4747 7.39057C12.2585 6.62144 12.5 5.71502 12.5 5.16592C12.5 4.61683 12.2585 3.7104 11.4747 2.94128C10.7057 2.18662 9.34703 1.5 7 1.5C4.65297 1.5 3.29433 2.18662 2.52531 2.94128ZM2.67684 10.58C2.52527 10.7584 2.47568 10.9799 2.47568 11.0999C2.47568 11.22 2.52527 11.4414 2.67684 11.6199C2.80427 11.77 3.06053 11.9587 3.62308 11.9587C4.18564 11.9587 4.4419 11.77 4.56933 11.6199C4.7209 11.4414 4.77049 11.22 4.77049 11.0999C4.77049 10.9799 4.7209 10.7584 4.56933 10.58C4.4419 10.4299 4.18564 10.2412 3.62308 10.2412C3.06053 10.2412 2.80427 10.4299 2.67684 10.58Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186449\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"layers-1\", \"keywords\": [ \"design\", \"layer\", \"layers\", \"pile\", \"stack\", \"align\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186452)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.00017 0.00231934C6.76995 0.00231934 6.54219 0.0496367 6.33102 0.141334L0.618733 2.80687L0.605006 2.81353C0.429806 2.9019 0.282573 3.03716 0.179703 3.20426C0.076834 3.37136 0.0223665 3.56374 0.0223665 3.75996C0.0223665 3.95619 0.076834 4.14856 0.179703 4.31566C0.282572 4.48276 0.429805 4.61802 0.605006 4.70639L0.620041 4.71366L6.31998 7.35379L6.33102 7.35859C6.54219 7.45028 6.76995 7.4976 7.00017 7.4976C7.23039 7.4976 7.45823 7.45045 7.6694 7.35875L13.3816 4.69305L13.3954 4.68639C13.5706 4.59802 13.7178 4.46276 13.8207 4.29566C13.9235 4.12856 13.978 3.93619 13.978 3.73996C13.978 3.54374 13.9235 3.35136 13.8207 3.18426C13.7178 3.01716 13.5706 2.8819 13.3954 2.79353L13.3803 2.78626L7.68037 0.14613L7.66933 0.141334C7.45816 0.0496367 7.23039 0.00231934 7.00017 0.00231934ZM13.9314 6.89616C14.1047 7.27238 13.9402 7.71785 13.564 7.89116L7.69859 10.5931L7.69604 10.5943C7.47306 10.696 7.23084 10.7486 6.98575 10.7486C6.74067 10.7486 6.49845 10.696 6.27547 10.5942L6.27164 10.5925L0.435105 7.89057C0.0592151 7.71656 -0.104439 7.27077 0.0695721 6.89489C0.243584 6.519 0.689366 6.35534 1.06525 6.52935L6.8992 9.23008C6.92642 9.24228 6.95591 9.24859 6.98575 9.24859C7.01574 9.24859 7.04539 9.24222 7.07272 9.22989L12.9364 6.52876C13.3126 6.35546 13.7581 6.51995 13.9314 6.89616ZM13.564 11.1412C13.9402 10.9679 14.1047 10.5224 13.9314 10.1462C13.7581 9.76995 13.3126 9.60546 12.9364 9.77876L7.07354 12.4795C7.04621 12.4918 7.01574 12.4986 6.98575 12.4986C6.95591 12.4986 6.92642 12.4923 6.8992 12.4801L1.06525 9.77935C0.689366 9.60534 0.243583 9.769 0.0695721 10.1449C-0.104439 10.5208 0.0592152 10.9666 0.435105 11.1406L6.27164 13.8425L6.27547 13.8442C6.49845 13.946 6.74067 13.9986 6.98575 13.9986C7.23084 13.9986 7.47306 13.946 7.69604 13.8443L7.69859 13.8431L13.564 11.1412Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186452\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"layers-2\", \"keywords\": [ \"design\", \"layer\", \"layers\", \"pile\", \"stack\", \"align\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.5 1C5.67157 1 5 1.67157 5 2.5V6.5C5 7.32843 5.67157 8 6.5 8H12.5C13.3284 8 14 7.32843 14 6.5V2.5C14 1.67157 13.3284 1 12.5 1H6.5ZM3.25015 3.00085C3.66437 3.00085 4.00015 3.33664 4.00015 3.75085V8.75085C4.00015 8.88893 4.11208 9.00085 4.25015 9.00085H11.2502C11.6644 9.00085 12.0002 9.33664 12.0002 9.75085C12.0002 10.1651 11.6644 10.5009 11.2502 10.5009H4.25015C3.28365 10.5009 2.50015 9.71735 2.50015 8.75085V3.75085C2.50015 3.33664 2.83594 3.00085 3.25015 3.00085ZM0.75 5.50024C1.16421 5.50024 1.5 5.83603 1.5 6.25024V11.2502C1.5 11.3883 1.61193 11.5002 1.75 11.5002H8.75C9.16421 11.5002 9.5 11.836 9.5 12.2502C9.5 12.6645 9.16421 13.0002 8.75 13.0002H1.75C0.783502 13.0002 0 12.2167 0 11.2502V6.25024C0 5.83603 0.335786 5.50024 0.75 5.50024Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"layout-window-1\", \"keywords\": [ \"column\", \"layout\", \"layouts\", \"left\", \"sidebar\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186458)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0H4.875V7V14H1.5C0.671573 14 0 13.3284 0 12.5V1.5C0 0.671573 0.671573 0 1.5 0ZM6.125 7.625V14H12.5C13.3284 14 14 13.3284 14 12.5V7.625H6.125ZM14 6.375H6.125V0H12.5C13.3284 0 14 0.671573 14 1.5V6.375Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186458\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"layout-window-11\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186467)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.5 0H10.125V7V14H12.5C13.3284 14 14 13.3284 14 12.5V1.5C14 0.671573 13.3284 0 12.5 0ZM8.875 7.625V14H1.5C0.671573 14 0 13.3284 0 12.5V7.625H8.875ZM0 6.375H8.875V0H1.5C0.671573 0 0 0.671573 0 1.5V6.375Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186467\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"layout-window-2\", \"keywords\": [ \"column\", \"header\", \"layout\", \"layouts\", \"masthead\", \"sidebar\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186461)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0C0.671573 0 0 0.671573 0 1.5V2.875H4.5H14V1.5C14 0.671573 13.3284 0 12.5 0H1.5ZM0 4.125H3.875V8.5V14H1.5C0.671573 14 0 13.3284 0 12.5V4.125ZM5.125 9.125V14H12.5C13.3284 14 14 13.3284 14 12.5V9.125H5.125ZM14 7.875H5.125V4.125H14V7.875Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186461\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"layout-window-8\", \"keywords\": [ \"grid\", \"header\", \"layout\", \"layouts\", \"masthead\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186464)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0C0.671573 0 0 0.671573 0 1.5V2.875H7H14V1.5C14 0.671573 13.3284 0 12.5 0H1.5ZM0 8.37488V4.125H6.375V8.37488H0ZM0 9.62488V12.5C0 13.3284 0.671573 14 1.5 14H6.375V9.62488H0ZM7.625 9.62488V14H12.5C13.3284 14 14 13.3284 14 12.5V9.62488H7.625ZM14 8.37488V4.125H7.625V8.37488H14Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186464\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"lightbulb\", \"keywords\": [ \"lighting\", \"light\", \"incandescent\", \"bulb\", \"lights\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.5 9.33109C10.9945 8.46657 12 6.85071 12 5C12 2.23858 9.76142 0 7 0C4.23858 0 2 2.23858 2 5C2 6.85071 3.0055 8.46657 4.5 9.33109V10.5C4.5 11.0523 4.94772 11.5 5.5 11.5H8.5C9.05228 11.5 9.5 11.0523 9.5 10.5V9.33109ZM4.5 13.25C4.5 12.8358 4.83579 12.5 5.25 12.5H8.75C9.16421 12.5 9.5 12.8358 9.5 13.25C9.5 13.6642 9.16421 14 8.75 14H5.25C4.83579 14 4.5 13.6642 4.5 13.25Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"like-1\", \"keywords\": [ \"reward\", \"social\", \"up\", \"rating\", \"media\", \"like\", \"thumb\", \"hand\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186473)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.41034 12.9615C4.74631 13.1216 5.11378 13.2047 5.48596 13.2047H10.832C12.0637 13.2047 13.1118 12.3077 13.302 11.0909L13.9279 7.08756C14.1175 5.87456 13.1796 4.77864 11.9519 4.77864H8.66816V2.42102C8.66816 1.52342 7.94051 0.795776 7.04291 0.795776C6.45461 0.795776 5.91223 1.11369 5.62482 1.62701L3.54748 5.33708C3.42211 5.56098 3.35628 5.81329 3.35628 6.0699V11.5124C3.35628 12.0907 3.6888 12.6176 4.21091 12.8664L4.41034 12.9615ZM1.04323 5.52032C0.778981 5.52032 0.525554 5.62529 0.338701 5.81214C0.151848 5.999 0.046875 6.25242 0.046875 6.51667L0.0468755 11.6293C0.0468755 11.8936 0.151848 12.147 0.338701 12.3339C0.525553 12.5207 0.77898 12.6257 1.04323 12.6257L1.5398 12.6255C1.81586 12.6254 2.03958 12.4015 2.03958 12.1255V6.02011C2.03958 5.88746 1.98687 5.76025 1.89306 5.66648C1.79924 5.5727 1.67201 5.52005 1.53937 5.52011L1.04323 5.52032Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186473\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"link-chain\", \"keywords\": [ \"create\", \"hyperlink\", \"link\", \"make\", \"unlink\", \"connection\", \"chain\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186476)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.6715 2.7426L6.70715 3.70707C6.31665 4.09761 5.68348 4.09765 5.29293 3.70715C4.90239 3.31665 4.90235 2.68348 5.29285 2.29293L6.25721 1.32847C8.02845 -0.44277 10.9002 -0.44281 12.6715 1.32843C14.4427 3.09965 14.4427 5.97136 12.6715 7.7426L11.7071 8.70707C11.3166 9.09761 10.6835 9.09765 10.2929 8.70715C9.90239 8.31664 9.90235 7.68348 10.2929 7.29293L11.2572 6.32847C12.2474 5.33828 12.2474 3.73283 11.2572 2.74264C10.2671 1.75247 8.66169 1.75245 7.6715 2.7426ZM3.70696 5.29285C4.0975 5.68335 4.09754 6.31652 3.70704 6.70707L2.74268 7.67153C1.75249 8.66172 1.75245 10.2672 2.74264 11.2574C3.73282 12.2475 5.33819 12.2475 6.32839 11.2574L7.29274 10.2929C7.68324 9.90239 8.31641 9.90235 8.70696 10.2929C9.0975 10.6834 9.09754 11.3165 8.70704 11.7071L7.74268 12.6715C5.97144 14.4427 3.09966 14.4428 1.32843 12.6716C-0.442797 10.9003 -0.44281 8.02864 1.32839 6.2574L2.29274 5.29293C2.68324 4.90239 3.31641 4.90235 3.70696 5.29285ZM9.20711 6.20711C9.59763 5.81658 9.59763 5.18342 9.20711 4.79289C8.81658 4.40237 8.18342 4.40237 7.79289 4.79289L4.79289 7.79289C4.40237 8.18342 4.40237 8.81658 4.79289 9.20711C5.18342 9.59763 5.81658 9.59763 6.20711 9.20711L9.20711 6.20711Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186476\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"live-video\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186479)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.09003 0.0844444C7.6808 0.0204002 7.29713 0.300231 7.23309 0.709463C7.16904 1.1187 7.44887 1.50236 7.85811 1.56641C10.4879 1.97797 12.5 4.25469 12.5 6.99996C12.5 10.0375 10.0375 12.5 6.99997 12.5C4.6786 12.5 2.69136 11.0616 1.88441 9.02466C1.73185 8.63956 1.29599 8.45106 0.910896 8.60362C0.525801 8.75618 0.337297 9.19204 0.489859 9.57713C1.5157 12.1665 4.04281 14 6.99997 14C10.866 14 14 10.866 14 6.99996C14 3.50439 11.4385 0.608473 8.09003 0.0844444ZM6.16381 0.661431C6.26706 1.06257 6.02557 1.47146 5.62443 1.5747C5.37182 1.63972 5.12624 1.72225 4.88902 1.82094C4.50659 1.98005 4.06758 1.79901 3.90847 1.41657C3.74936 1.03413 3.9304 0.595125 4.31284 0.436017C4.61525 0.310202 4.92841 0.204957 5.25054 0.122047C5.65168 0.0188006 6.06057 0.260291 6.16381 0.661431ZM3.18519 1.89415C3.4737 2.19136 3.46666 2.66618 3.16945 2.9547C2.79826 3.31504 2.47784 3.72709 2.21985 4.17896C2.01447 4.53868 1.55638 4.66379 1.19667 4.45842C0.836952 4.25304 0.711835 3.79495 0.91721 3.43523C1.24561 2.86004 1.65306 2.33621 2.12465 1.87841C2.42185 1.5899 2.89667 1.59695 3.18519 1.89415ZM0.916166 5.27863C1.32633 5.33644 1.61196 5.7158 1.55415 6.12596C1.51849 6.37895 1.5 6.63783 1.5 6.90145C1.5 7.31566 1.16421 7.65145 0.75 7.65145C0.335786 7.65145 0 7.31566 0 6.90145C0 6.56758 0.0234281 6.23875 0.0688322 5.91661C0.126642 5.50645 0.506007 5.22082 0.916166 5.27863ZM4.5 8.88191V5.11797C4.5 4.37459 5.28231 3.8911 5.94721 4.22355L9.71115 6.10551C10.4482 6.47404 10.4482 7.52584 9.71114 7.89437L5.94721 9.77633C5.28231 10.1088 4.5 9.62529 4.5 8.88191Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186479\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"lock-rotation\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186482)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 7C1.5 3.96243 3.96243 1.5 7 1.5C8.75084 1.5 10.3115 2.31796 11.3193 3.59456L10.3536 4.56034C10.0386 4.87533 10.2617 5.4139 10.7071 5.4139H13.5C13.7761 5.4139 14 5.19004 14 4.9139V2.121C14 1.67555 13.4614 1.45247 13.1464 1.76745L12.3856 2.52829C11.1022 0.984301 9.16644 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14C10.311 14 13.0834 11.702 13.8127 8.61473C13.9079 8.21161 13.6583 7.80762 13.2552 7.7124C12.8521 7.61717 12.4481 7.86677 12.3528 8.26989C11.7799 10.6955 9.59975 12.5 7 12.5C3.96243 12.5 1.5 10.0376 1.5 7Z\\\" fill=\\\"black\\\"/>\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.125 5.8125C6.125 5.32925 6.51675 4.9375 7 4.9375C7.48325 4.9375 7.875 5.32925 7.875 5.8125V6.3125H6.125V5.8125ZM4.875 6.53183V5.8125C4.875 4.63889 5.82639 3.6875 7 3.6875C8.17361 3.6875 9.125 4.6389 9.125 5.8125V6.53183C9.35362 6.71509 9.5 6.9967 9.5 7.3125V9.3125C9.5 9.86478 9.05228 10.3125 8.5 10.3125H5.5C4.94772 10.3125 4.5 9.86478 4.5 9.3125V7.3125C4.5 6.9967 4.64638 6.71509 4.875 6.53183Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186482\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"login-1\", \"keywords\": [ \"arrow\", \"enter\", \"frame\", \"left\", \"login\", \"point\", \"rectangle\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186486)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1.5C0 0.671573 0.671573 0 1.5 0H8.5C9.32843 0 10 0.671573 10 1.5V4.74998H9.48437C9.39557 4.04357 8.9348 3.42953 8.26537 3.15224C7.51802 2.84268 6.65778 3.01379 6.08579 3.58578L4.08579 5.58578C3.30474 6.36683 3.30474 7.63316 4.08579 8.41421L6.08579 10.4142C6.65778 10.9862 7.51802 11.1573 8.26537 10.8478C8.93481 10.5705 9.39559 9.9564 9.48437 9.24998H10V12.5C10 13.3284 9.32843 14 8.5 14H1.5C0.671573 14 0 13.3284 0 12.5V1.5ZM7.78701 4.30711C8.06727 4.4232 8.25 4.69668 8.25 5.00002V6.00002H13C13.5523 6.00002 14 6.44774 14 7.00002C14 7.55231 13.5523 8.00002 13 8.00002H8.25V9.00002C8.25 9.30337 8.06727 9.57685 7.78701 9.69293C7.50676 9.80902 7.18417 9.74485 6.96967 9.53035L4.96967 7.53035C4.67678 7.23746 4.67678 6.76258 4.96967 6.46969L6.96967 4.46969C7.18417 4.25519 7.50676 4.19103 7.78701 4.30711Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186486\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"logout-1\", \"keywords\": [ \"arrow\", \"exit\", \"frame\", \"leave\", \"logout\", \"rectangle\", \"right\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186489)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1.5C0 0.671573 0.671573 0 1.5 0H8.5C9.32843 0 10 0.671573 10 1.5V3.43863C9.59736 3.76102 9.33093 4.23053 9.26563 4.74998H5.75C4.50736 4.74998 3.5 5.75733 3.5 6.99998C3.5 8.24262 4.50736 9.24998 5.75 9.24998H9.26563C9.33091 9.76944 9.59735 10.239 10 10.5614V12.5C10 13.3284 9.32843 14 8.5 14H1.5C0.671573 14 0 13.3284 0 12.5V1.5ZM10.963 4.30711C10.6827 4.4232 10.5 4.69668 10.5 5.00002V6.00002H5.75C5.19772 6.00002 4.75 6.44774 4.75 7.00002C4.75 7.55231 5.19772 8.00002 5.75 8.00002H10.5V9.00002C10.5 9.30337 10.6827 9.57685 10.963 9.69293C11.2432 9.80902 11.5658 9.74485 11.7803 9.53035L13.7803 7.53035C14.0732 7.23746 14.0732 6.76258 13.7803 6.46969L11.7803 4.46969C11.5658 4.25519 11.2432 4.19103 10.963 4.30711Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186489\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"loop-1\", \"keywords\": [ \"multimedia\", \"multi\", \"button\", \"repeat\", \"media\", \"loop\", \"infinity\", \"controls\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.56296 4.88361C3.2841 4.85707 3.00286 4.89207 2.73946 4.98598C2.47609 5.07989 2.23734 5.23028 2.04015 5.426C1.84297 5.6217 1.69221 5.85786 1.59827 6.11739C1.57041 6.19439 1.539 6.3867 1.51934 6.62841C1.49866 6.88266 1.49866 7.11734 1.51934 7.37159C1.539 7.61331 1.57041 7.80561 1.59827 7.88261C1.69221 8.14214 1.84297 8.3783 2.04015 8.574C2.23734 8.76972 2.47609 8.92011 2.73946 9.01402C3.00286 9.10793 3.2841 9.14293 3.56296 9.11639C3.58658 9.11414 3.6103 9.11301 3.63404 9.11301C4.00691 9.11301 4.41304 8.91372 4.90476 8.43237C5.2964 8.04899 5.67512 7.55448 6.07646 7C5.67512 6.44552 5.2964 5.95101 4.90476 5.56763C4.41304 5.08628 4.00691 4.88699 3.63404 4.88699C3.6103 4.88699 3.58658 4.88586 3.56296 4.88361ZM6.99942 8.27712C6.67054 8.71529 6.32245 9.14365 5.95406 9.50427C5.35677 10.089 4.60288 10.5998 3.67045 10.6128C3.18454 10.654 2.69516 10.5907 2.2357 10.4269C1.76529 10.2592 1.33762 9.99012 0.983484 9.63864C0.629332 9.28714 0.357494 8.86188 0.187825 8.39313C0.0838904 8.10598 0.0434832 7.72935 0.0242764 7.49317C0.010964 7.32947 -0.000303008 7.16441 6.21798e-06 6.99999C0.000315499 6.83555 0.0109553 6.67063 0.0242764 6.50683C0.0434832 6.27065 0.0838904 5.89402 0.187825 5.60687C0.357494 5.13812 0.629332 4.71286 0.983484 4.36136C1.33762 4.00988 1.76529 3.74083 2.2357 3.57311C2.69516 3.40928 3.18454 3.34598 3.67045 3.38724C4.60288 3.40024 5.35677 3.91104 5.95406 4.49573C6.32245 4.85635 6.67054 5.28471 6.99942 5.72288C7.3283 5.28471 7.67639 4.85635 8.04478 4.49573C8.64207 3.91104 9.39596 3.40024 10.3284 3.38724C10.8143 3.34598 11.3037 3.40928 11.7631 3.57311C12.2335 3.74083 12.6612 4.00988 13.0153 4.36136C13.3695 4.71286 13.6413 5.13812 13.811 5.60687C13.9149 5.89402 13.9553 6.27065 13.9745 6.50683C13.9879 6.67057 13.9988 6.83557 13.9991 6.99997C13.9993 7.16437 13.9879 7.3295 13.9745 7.49317C13.9553 7.72935 13.9149 8.10598 13.811 8.39313C13.6413 8.86188 13.3695 9.28714 13.0153 9.63864C12.6612 9.99012 12.2335 10.2592 11.7631 10.4269C11.3037 10.5907 10.8143 10.654 10.3284 10.6128C9.39596 10.5998 8.64207 10.089 8.04478 9.50427C7.67639 9.14365 7.3283 8.71529 6.99942 8.27712ZM12.4795 7.37159C12.4598 7.61331 12.4284 7.80561 12.4005 7.88261C12.3066 8.14214 12.1558 8.3783 11.9587 8.574C11.7615 8.76972 11.5227 8.92011 11.2594 9.01402C10.996 9.10793 10.7147 9.14293 10.4359 9.11639C10.4122 9.11414 10.3885 9.11301 10.3648 9.11301C9.99193 9.11301 9.5858 8.91372 9.09408 8.43237C8.70244 8.04899 8.32372 7.55448 7.92238 7C8.32372 6.44552 8.70244 5.95101 9.09408 5.56763C9.5858 5.08628 9.99193 4.88699 10.3648 4.88699C10.3885 4.88699 10.4122 4.88586 10.4359 4.88361C10.7147 4.85707 10.996 4.89207 11.2594 4.98598C11.5227 5.07989 11.7615 5.23028 11.9587 5.426C12.1558 5.6217 12.3066 5.85786 12.4005 6.11739C12.4284 6.19439 12.4598 6.3867 12.4795 6.62841C12.5002 6.88266 12.5002 7.11734 12.4795 7.37159Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"magic-wand-2\", \"keywords\": [ \"design\", \"magic\", \"star\", \"supplies\", \"tool\", \"wand\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186495)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.3499 3.82378L13.1899 1.34378C13.2149 1.27266 13.2193 1.19592 13.2025 1.12241C13.1858 1.0489 13.1487 0.981616 13.0954 0.92831C13.0421 0.875003 12.9748 0.837845 12.9013 0.82112C12.8278 0.804395 12.751 0.808784 12.6799 0.83378L10.1999 1.67378C10.1387 1.69533 10.0732 1.70203 10.0089 1.69334C9.94455 1.68465 9.8832 1.66081 9.82989 1.62378L7.72989 0.05378C7.67028 0.0192874 7.60277 0.000770998 7.5339 2.35619e-05C7.46504 -0.000723874 7.39714 0.016323 7.33679 0.0495137C7.27645 0.0827043 7.22569 0.130914 7.18945 0.189475C7.1532 0.248036 7.13268 0.314965 7.12989 0.38378V3.00378C7.128 3.06799 7.11175 3.13096 7.08234 3.18807C7.05292 3.24518 7.01108 3.29497 6.95989 3.33378L4.81989 4.84378C4.7585 4.8866 4.71036 4.94579 4.68094 5.01461C4.65152 5.08343 4.642 5.15912 4.65346 5.23309C4.66492 5.30705 4.6969 5.37631 4.74577 5.43301C4.79463 5.4897 4.85843 5.53154 4.92989 5.55378L7.42989 6.33378C7.49169 6.35158 7.54796 6.38477 7.59343 6.43024C7.63891 6.47571 7.67209 6.53198 7.68989 6.59378L8.46989 9.09378C8.49213 9.16525 8.53397 9.22904 8.59066 9.2779C8.64736 9.32677 8.71662 9.35875 8.79058 9.37021C8.86455 9.38167 8.94024 9.37215 9.00906 9.34273C9.07788 9.3133 9.13707 9.26517 9.17989 9.20378L10.6399 7.10378C10.6787 7.05259 10.7285 7.01075 10.7856 6.98134C10.8427 6.95192 10.9057 6.93567 10.9699 6.93378H13.5899C13.665 6.93568 13.7392 6.91637 13.8038 6.87807C13.8685 6.83977 13.9211 6.78402 13.9555 6.71722C13.99 6.65042 14.0049 6.57527 13.9986 6.50038C13.9923 6.42549 13.965 6.35389 13.9199 6.29378L12.3499 4.19378C12.3209 4.13642 12.3058 4.07305 12.3058 4.00878C12.3058 3.94451 12.3209 3.88114 12.3499 3.82378ZM0.292893 12.2929L5.53407 7.05176L6.63002 7.39369L6.9607 8.45356L1.70711 13.7072C1.31658 14.0977 0.683417 14.0977 0.292893 13.7072C-0.097631 13.3166 -0.097631 12.6835 0.292893 12.2929Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186495\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"magnifying-glass\", \"keywords\": [ \"glass\", \"search\", \"magnifying\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186501)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2 6C2 3.79086 3.79086 2 6 2C8.20914 2 10 3.79086 10 6C10 8.20914 8.20914 10 6 10C3.79086 10 2 8.20914 2 6ZM6 0C2.68629 0 0 2.68629 0 6C0 9.31371 2.68629 12 6 12C7.29578 12 8.49562 11.5892 9.47642 10.8908L12.2929 13.7073C12.6834 14.0978 13.3166 14.0978 13.7071 13.7073C14.0976 13.3168 14.0976 12.6836 13.7071 12.2931L10.8907 9.47665C11.5892 8.49581 12 7.29588 12 6C12 2.68629 9.31371 0 6 0Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186501\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"magnifying-glass-circle\", \"keywords\": [ \"circle\", \"glass\", \"search\", \"magnifying\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186498)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7ZM6.5 4.625C5.46447 4.625 4.625 5.46447 4.625 6.5C4.625 7.53553 5.46447 8.375 6.5 8.375C7.53553 8.375 8.375 7.53553 8.375 6.5C8.375 5.46447 7.53553 4.625 6.5 4.625ZM3.375 6.5C3.375 4.77411 4.77411 3.375 6.5 3.375C8.22589 3.375 9.625 4.77411 9.625 6.5C9.625 7.13693 9.43445 7.72935 9.10725 8.22337L10.4419 9.55806C10.686 9.80214 10.686 10.1979 10.4419 10.4419C10.1979 10.686 9.80214 10.686 9.55806 10.4419L8.22337 9.10725C7.72935 9.43445 7.13693 9.625 6.5 9.625C4.77411 9.625 3.375 8.22589 3.375 6.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186498\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"manual-book\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12 10.75H11.75V12.5H12C12.4142 12.5 12.75 12.8358 12.75 13.25C12.75 13.6642 12.4142 14 12 14H11H3.625C2.31332 14 1.25 12.9367 1.25 11.625V2.25C1.25 1.00736 2.25736 0 3.5 0H10.5C11.7426 0 12.75 1.00736 12.75 2.25V10C12.75 10.4142 12.4142 10.75 12 10.75ZM3.625 10.75H10.25V12.5H3.625C3.14175 12.5 2.75 12.1082 2.75 11.625C2.75 11.1418 3.14175 10.75 3.625 10.75ZM7.1707 2.8287C7.00097 2.79494 6.82504 2.81227 6.66515 2.8785C6.50527 2.94472 6.36861 3.05687 6.27246 3.20077C6.17632 3.34466 6.125 3.51383 6.125 3.68689C6.125 4.03207 5.84518 4.31189 5.5 4.31189C5.15482 4.31189 4.875 4.03207 4.875 3.68689C4.875 3.2666 4.99963 2.85576 5.23313 2.5063C5.46663 2.15685 5.7985 1.88448 6.1868 1.72365C6.57509 1.56281 7.00236 1.52073 7.41457 1.60272C7.82678 1.68472 8.20542 1.8871 8.5026 2.18429C8.79979 2.48147 9.00218 2.86011 9.08417 3.27232C9.16616 3.68453 9.12408 4.1118 8.96324 4.50009C8.80241 4.88839 8.53004 5.22027 8.18059 5.45376C8.00802 5.56907 7.82048 5.65782 7.62423 5.71814C7.60795 6.0488 7.3347 6.31189 7 6.31189C6.65482 6.31189 6.375 6.03207 6.375 5.68689V5.18689C6.375 4.84171 6.65482 4.56189 7 4.56189C7.17306 4.56189 7.34223 4.51057 7.48612 4.41443C7.63002 4.31828 7.74217 4.18162 7.80839 4.02174C7.87462 3.86185 7.89195 3.68592 7.85819 3.51619C7.82443 3.34645 7.74109 3.19054 7.61872 3.06817C7.49635 2.9458 7.34044 2.86247 7.1707 2.8287ZM7 8.88371C6.51675 8.88371 6.125 8.49196 6.125 8.00871C6.125 7.52546 6.51675 7.13371 7 7.13371C7.48325 7.13371 7.875 7.52546 7.875 8.00871C7.875 8.49196 7.48325 8.88371 7 8.88371Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"megaphone-2\", \"keywords\": [ \"bullhorn\", \"loud\", \"megaphone\", \"share\", \"speaker\", \"transmit\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186507)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.25 3.49994L11.9563 0.794702C12.9396 0.412306 14 1.13767 14 2.19271V10.4188C14 11.4426 12.997 12.1656 12.0257 11.8418L5.25 9.49994V3.49994ZM3 3.5H4V9V9.5V10.5C4 11.0523 4.44772 11.5 5 11.5H5.5C6.05228 11.5 6.5 11.9477 6.5 12.5C6.5 13.0523 6.05228 13.5 5.5 13.5H5C3.34315 13.5 2 12.1569 2 10.5V9.32929C0.834808 8.91746 0 7.80622 0 6.5C0 4.84315 1.34315 3.5 3 3.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186507\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"minimize-window-2\", \"keywords\": [ \"expand\", \"retract\", \"shrink\", \"bigger\", \"big\", \"small\", \"smaller\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186510)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 2.5C0 1.11929 1.11929 0 2.5 0H11.5C12.8807 0 14 1.11929 14 2.5V11.5C14 12.8807 12.8807 14 11.5 14H7C6.44772 14 6 13.5523 6 13C6 12.4477 6.44772 12 7 12H11.5C11.7761 12 12 11.7761 12 11.5V2.5C12 2.22386 11.7761 2 11.5 2H2.5C2.22386 2 2 2.22386 2 2.5V7C2 7.55228 1.55228 8 1 8C0.447715 8 0 7.55228 0 7V2.5ZM4.46194 13.6913C4.38455 13.8782 4.20223 14 4 14H0.5C0.223857 14 0 13.7761 0 13.5V10C0 9.79777 0.121821 9.61545 0.308659 9.53806C0.495496 9.46067 0.710555 9.50345 0.853553 9.64645L1.89645 10.6893L6.29289 6.29289C6.68342 5.90237 7.31658 5.90237 7.70711 6.29289C8.09763 6.68342 8.09763 7.31658 7.70711 7.70711L3.31066 12.1036L4.35355 13.1464C4.49655 13.2894 4.53933 13.5045 4.46194 13.6913Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186510\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"moon-cloud\", \"keywords\": [ \"cloud\", \"meteorology\", \"cloudy\", \"partly\", \"sunny\", \"weather\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186513)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.8432 1.24906C11.0491 1.0431 11.0956 0.757043 11.035 0.527976C10.9707 0.284913 10.7444 0.00432106 10.3606 0.0187206C9.53081 0.0498497 8.70882 0.381748 8.0741 1.0138C7.41058 1.64165 7.03948 2.3451 6.92084 3.05248C7.07967 3.10378 7.23678 3.16155 7.39178 3.22576C8.53382 3.6988 9.50994 4.49988 10.1967 5.52769C10.4545 5.91352 10.6672 6.3248 10.8323 6.75344C10.8873 6.75123 10.943 6.75008 10.9982 6.75H11C11.1438 6.75 11.2868 6.75729 11.4286 6.77165C11.9315 6.60569 12.431 6.31334 12.9 5.87039L12.9103 5.86041C13.5457 5.22499 13.8794 4.40076 13.9106 3.56872C13.925 3.1849 13.6444 2.95857 13.4013 2.89429C13.1723 2.83371 12.8862 2.88019 12.6802 3.08615C12.1729 3.59345 11.3505 3.59345 10.8432 3.08615C10.3359 2.57885 10.3359 1.75636 10.8432 1.24906ZM4.02455 4.09608C4.99446 3.90315 5.99979 4.00217 6.91342 4.3806C7.82705 4.75904 8.60794 5.3999 9.15735 6.22215C9.55648 6.81949 9.82041 7.49306 9.93494 8.196C10.2729 8.0677 10.6333 8.00049 10.9993 8C11.795 8 12.5587 8.31607 13.1213 8.87868C13.6839 9.44129 14 10.2044 14 11C14 11.7957 13.6839 12.5587 13.1213 13.1213C12.5587 13.6839 11.7957 14 11 14H5C4.0111 14 3.0444 13.7068 2.22215 13.1574C1.39991 12.6079 0.759043 11.827 0.380605 10.9134C0.00216636 9.99979 -0.0968502 8.99446 0.0960758 8.02455C0.289002 7.05465 0.765206 6.16373 1.46447 5.46447C2.16373 4.76521 3.05465 4.289 4.02455 4.09608Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186513\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"move-left\", \"keywords\": [ \"move\", \"left\", \"arrows\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186516)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10 0C9.17157 0 8.5 0.671573 8.5 1.5V12.5C8.5 13.3284 9.17157 14 10 14H12.5C13.3284 14 14 13.3284 14 12.5V1.5C14 0.671573 13.3284 0 12.5 0H10ZM3.5 4.5C3.5 4.29777 3.37818 4.11545 3.19134 4.03806C3.0045 3.96067 2.78945 4.00345 2.64645 4.14645L0.146447 6.64645C-0.0488156 6.84171 -0.0488156 7.15829 0.146447 7.35355L2.64645 9.85355C2.78945 9.99655 3.0045 10.0393 3.19134 9.96194C3.37818 9.88455 3.5 9.70223 3.5 9.5V8H6.5C7.05228 8 7.5 7.55228 7.5 7C7.5 6.44772 7.05228 6 6.5 6H3.5V4.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186516\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"move-right\", \"keywords\": [ \"move\", \"right\", \"arrows\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186519)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0C0.671573 0 0 0.671573 0 1.5V12.5C0 13.3284 0.671573 14 1.5 14H4C4.82843 14 5.5 13.3284 5.5 12.5V1.5C5.5 0.671573 4.82843 0 4 0H1.5ZM7.49976 6C6.94748 6 6.49976 6.44772 6.49976 7C6.49976 7.55228 6.94748 8 7.49976 8H10.4998V9.5C10.4998 9.70223 10.6216 9.88455 10.8085 9.96194C10.9953 10.0393 11.2104 9.99655 11.3534 9.85355L13.8534 7.35355C14.0486 7.15829 14.0486 6.84171 13.8534 6.64645L11.3534 4.14645C11.2104 4.00345 10.9953 3.96067 10.8085 4.03806C10.6216 4.11545 10.4998 4.29777 10.4998 4.5V6H7.49976Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186519\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"multiple-file-2\", \"keywords\": [ \"double\", \"common\", \"file\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.50009 0C4.08588 0 3.75009 0.335786 3.75009 0.75C3.75009 1.16421 4.08588 1.5 4.50009 1.5H11.0001C11.1382 1.5 11.2501 1.61193 11.2501 1.75V11.25C11.2501 11.6642 11.5859 12 12.0001 12C12.4143 12 12.7501 11.6642 12.7501 11.25V1.75C12.7501 0.783502 11.9666 0 11.0001 0H4.50009ZM1.5 4C1.5 3.17157 2.17157 2.5 3 2.5H8.5C9.32843 2.5 10 3.17157 10 4V12.5C10 13.3284 9.32843 14 8.5 14H3C2.17157 14 1.5 13.3284 1.5 12.5V4ZM3.12469 5C3.12469 4.65482 3.40452 4.375 3.74969 4.375H7.74969C8.09487 4.375 8.37469 4.65482 8.37469 5C8.37469 5.34518 8.09487 5.625 7.74969 5.625H3.74969C3.40452 5.625 3.12469 5.34518 3.12469 5ZM3.75018 6.875C3.405 6.875 3.12518 7.15482 3.12518 7.5C3.12518 7.84518 3.405 8.125 3.75018 8.125H7.75018C8.09536 8.125 8.37518 7.84518 8.37518 7.5C8.37518 7.15482 8.09536 6.875 7.75018 6.875H3.75018ZM3.12442 10C3.12442 9.65482 3.40425 9.375 3.74942 9.375H5.74942C6.0946 9.375 6.37442 9.65482 6.37442 10C6.37442 10.3452 6.0946 10.625 5.74942 10.625H3.74942C3.40425 10.625 3.12442 10.3452 3.12442 10Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"music-folder-song\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186525)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.18345 0.25018C5.52332 0.242095 5.85595 0.349709 6.12675 0.555513C6.39945 0.76277 6.59301 1.05711 6.67524 1.38961L6.89039 2.25018H12.5C12.8978 2.25018 13.2794 2.40822 13.5607 2.68952C13.842 2.97083 14 3.35236 14 3.75018V12.2501C14 12.648 13.842 13.0295 13.5607 13.3108C13.2794 13.5921 12.8978 13.7501 12.5 13.7501H1.5C1.10217 13.7501 0.720644 13.5921 0.43934 13.3108C0.158035 13.0295 0 12.648 0 12.2501V1.75018C0 1.35236 0.158035 0.970825 0.43934 0.68952C0.720644 0.408216 1.10218 0.25018 1.5 0.25018H5.18345ZM9.47928 4.47982C9.33408 4.45128 9.18426 4.45646 9.04137 4.49493L9.03583 4.49645L5.56425 5.45981C5.36132 5.51533 5.182 5.63552 5.05351 5.8022C4.92423 5.96992 4.85357 6.17543 4.85238 6.38719L4.85237 6.39155L4.85286 8.74866C4.84112 8.74839 4.82936 8.74826 4.81756 8.74826C3.96948 8.74826 3.28198 9.43576 3.28198 10.2838C3.28198 11.1319 3.96948 11.8194 4.81756 11.8194C5.66543 11.8194 6.35281 11.1322 6.35314 10.2844L6.35317 10.2768L6.35249 6.99119L8.7601 6.32242L8.76038 7.62508C8.74892 7.62482 8.73743 7.6247 8.72592 7.6247C7.87784 7.6247 7.19034 8.3122 7.19034 9.16027C7.19034 10.0083 7.87784 10.6958 8.72592 10.6958C9.57399 10.6958 10.2615 10.0083 10.2615 9.16027C10.2615 9.14353 10.2612 9.12684 10.2607 9.11022L10.2599 5.42738V5.42334C10.2591 5.27536 10.2243 5.12954 10.1582 4.99712C10.0922 4.86469 9.99663 4.74919 9.87892 4.65951C9.76121 4.56982 9.62449 4.50835 9.47928 4.47982Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186525\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"new-file\", \"keywords\": [ \"empty\", \"common\", \"file\", \"content\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.875 0H2.5C2.10218 0 1.72064 0.158035 1.43934 0.43934C1.15804 0.720644 1 1.10218 1 1.5V12.5C1 12.8978 1.15804 13.2794 1.43934 13.5607C1.72064 13.842 2.10217 14 2.5 14H11.5C11.8978 14 12.2794 13.842 12.5607 13.5607C12.842 13.2794 13 12.8978 13 12.5V5.125H8.5C8.15482 5.125 7.875 4.84518 7.875 4.5V0ZM12.5821 3.875L9.125 0.417893V3.875H12.5821Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"new-folder\", \"keywords\": [ \"empty\", \"folder\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186531)\\\">\\n<path d=\\\"M6.12675 0.805269C5.85595 0.599465 5.52332 0.491851 5.18345 0.499936H1.5C1.10218 0.499936 0.720644 0.657971 0.43934 0.939276C0.158035 1.22058 0 1.60211 0 1.99994V11.9999C0 12.3978 0.158035 12.7793 0.43934 13.0606C0.720644 13.3419 1.10217 13.4999 1.5 13.4999H12.5C12.8978 13.4999 13.2794 13.3419 13.5607 13.0606C13.842 12.7793 14 12.3978 14 11.9999V3.99994C14 3.60211 13.842 3.22058 13.5607 2.93928C13.2794 2.65797 12.8978 2.49994 12.5 2.49994H6.89039L6.67544 1.64015C6.5932 1.30766 6.39945 1.01253 6.12675 0.805269Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186531\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"new-sticky-note\", \"keywords\": [ \"empty\", \"common\", \"file\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186534)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.37497 14H1.5C0.671573 14 0 13.3284 0 12.5V1.5C0 0.671573 0.671573 0 1.5 0H12.5C13.3284 0 14 0.671573 14 1.5V8.37451H8.99997C8.65479 8.37451 8.37497 8.65433 8.37497 8.99951V14C8.37497 14.0002 8.37497 13.9998 8.37497 14ZM9.62497 13.375L13.3755 9.62451H9.62497V13.375Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186534\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"not-equal-sign\", \"keywords\": [ \"interface\", \"math\", \"not\", \"equal\", \"sign\", \"mathematics\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186537)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.7049 0.119723C11.1911 0.381561 11.3731 0.988023 11.1112 1.47429L10.0204 3.50019H13C13.5523 3.50019 14 3.94791 14 4.50019C14 5.05247 13.5523 5.50019 13 5.50019H8.94344L7.32806 8.50019H13C13.5523 8.50019 14 8.9479 14 9.50019C14 10.0525 13.5523 10.5002 13 10.5002H6.25114L4.6497 13.4743C4.38786 13.9606 3.7814 14.1425 3.29513 13.8807C2.80886 13.6188 2.62692 13.0124 2.88875 12.5261L3.97962 10.5002H1C0.447715 10.5002 0 10.0525 0 9.50019C0 8.9479 0.447715 8.50019 1 8.50019H5.05655L6.67193 5.50019H1C0.447715 5.50019 0 5.05247 0 4.50019C0 3.94791 0.447715 3.50019 1 3.50019H7.74886L9.35029 0.526094C9.61213 0.0398232 10.2186 -0.142115 10.7049 0.119723Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186537\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ok-hand\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.93814 2.04042L4.82193 2.39874C4.47203 2.96968 4.09843 3.61448 3.7519 4.28642C3.28853 5.18491 2.82921 6.21403 2.56603 7.22328C2.04195 5.25638 2.08293 2.69806 2.16257 1.2645C2.19853 0.617204 2.69583 0.103741 3.33787 0.0138496C4.1207 -0.0957546 4.84147 0.457358 4.93814 1.24189V2.04042ZM10.115 9.98204C10.2983 9.93973 10.475 9.85588 10.6307 9.72903C11.1747 9.28585 11.2163 8.27691 10.7731 7.73292C9.98688 6.76787 8.82608 6.28776 7.67117 6.33144C7.53132 6.34555 7.38652 6.36859 7.24397 6.39893C6.93216 6.4653 6.6857 6.55486 6.56385 6.62086C6.26034 6.78526 5.88102 6.67249 5.71662 6.36898C5.55222 6.06547 5.66499 5.68615 5.9685 5.52174C6.2338 5.37804 6.6068 5.25655 6.98374 5.17632C7.36447 5.09528 7.80369 5.04466 8.21102 5.0737L9.18853 3.0435C9.50455 2.31895 9.16557 1.47602 8.43591 1.172C7.83748 0.92265 7.14808 1.11158 6.78773 1.6505C5.79665 3.13264 3.70969 6.40275 3.59618 8.61567C3.4814 10.8534 3.53511 12.7065 3.57182 13.5343C3.58348 13.7972 3.80067 13.9999 4.06384 13.9999H8.80914C8.90654 13.9999 9.00371 13.9866 9.09584 13.955C9.50994 13.8129 9.90641 13.5965 10.2657 13.3039C10.8049 12.8646 11.1947 12.3052 11.4246 11.6949C11.672 11.0383 11.3402 10.3055 10.6836 10.0581C10.4959 9.98745 10.302 9.96406 10.115 9.98204ZM6.97854 11.1188C6.53536 10.5748 6.61708 9.77452 7.16107 9.33134C7.70507 8.88816 8.50533 8.96988 8.94851 9.51388C9.22738 9.85618 9.64763 10.0154 10.0565 9.97608C9.65596 10.0685 9.30774 10.3531 9.15199 10.7666C9.07605 10.9682 8.94788 11.1531 8.76597 11.3013C8.22198 11.7445 7.42172 11.6628 6.97854 11.1188Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"one-finger-drag-horizontal\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186543)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.5 0.500015C2.5 0.297783 2.37818 0.115465 2.19134 0.0380748C2.0045 -0.0393156 1.78945 0.00346221 1.64645 0.146461L0.146447 1.64646C-0.0488156 1.84172 -0.0488156 2.15831 0.146447 2.35357L1.64645 3.85357C1.78945 3.99657 2.0045 4.03934 2.19134 3.96195C2.37818 3.88456 2.5 3.70225 2.5 3.50001V2.5H3.5C3.77614 2.5 4 2.27614 4 2C4 1.72386 3.77614 1.5 3.5 1.5H2.5V0.500015ZM4.9066 3.7985L5.85428 9.65907L5.33375 9.84692C4.16722 10.2679 3.81916 11.7495 4.67623 12.6459L5.67567 13.6911C5.86433 13.8884 6.12545 14 6.39843 14H12.8801C13.477 14 13.941 13.4804 13.8737 12.8873L13.5066 9.65065C13.2797 8.24784 11.9586 7.29453 10.5558 7.52137L8.12369 7.91466L7.39307 3.39643C7.28204 2.70981 6.63541 2.2432 5.94879 2.35423C5.26217 2.46526 4.79557 3.11188 4.9066 3.7985ZM10.3087 0.0380748C10.4955 -0.0393156 10.7106 0.00346221 10.8536 0.146461L12.3536 1.64646C12.5488 1.84172 12.5488 2.15831 12.3536 2.35357L10.8536 3.85357C10.7106 3.99657 10.4955 4.03934 10.3087 3.96195C10.1218 3.88456 10 3.70225 10 3.50001V2.50001H9C8.72386 2.50001 8.5 2.27616 8.5 2.00001C8.5 1.72387 8.72386 1.50001 9 1.50001H10V0.500015C10 0.297783 10.1218 0.115465 10.3087 0.0380748Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186543\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"one-finger-drag-vertical\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186546)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.35356 0.146447C2.2598 0.0526784 2.13262 0 2.00001 0C1.8674 0 1.74022 0.0526784 1.64646 0.146447L0.146462 1.64645C0.00346218 1.78945 -0.0393156 2.0045 0.0380748 2.19134C0.115466 2.37818 0.297784 2.5 0.500015 2.5H1.25001V4.5H0.500015C0.297784 4.5 0.115466 4.62182 0.0380748 4.80866C-0.0393156 4.9955 0.00346218 5.21055 0.146462 5.35355L1.64646 6.85355C1.74022 6.94732 1.8674 7 2.00001 7C2.13262 7 2.2598 6.94732 2.35356 6.85355L3.85356 5.35355C3.99656 5.21055 4.03934 4.9955 3.96195 4.80866C3.88456 4.62182 3.70224 4.5 3.50001 4.5H2.75001V2.5H3.50001C3.70224 2.5 3.88456 2.37818 3.96195 2.19134C4.03934 2.0045 3.99656 1.78945 3.85356 1.64645L2.35356 0.146447ZM5.94446 9.69608V3.75938C5.94446 3.06384 6.50831 2.5 7.20385 2.5C7.89938 2.5 8.46323 3.06384 8.46323 3.75938V8.3363H10.927C12.348 8.33631 13.5 9.48828 13.5 10.9093V13C13.5 13.5523 13.0523 14 12.5 14H5.89255C5.56339 14 5.25531 13.838 5.06873 13.5669L4.30473 12.4565C3.60174 11.4348 4.18185 10.0278 5.40062 9.79843L5.94446 9.69608Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186546\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"one-finger-hold\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186549)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 4.19579C1.5 2.70694 2.70694 1.5 4.19579 1.5C4.65025 1.5 5.07637 1.61185 5.45029 1.8089C5.81674 2.00201 6.27035 1.86149 6.46345 1.49504C6.65656 1.12859 6.51604 0.674987 6.14959 0.48188C5.56528 0.173963 4.89974 0 4.19579 0C1.87852 0 0 1.87852 0 4.19579C0 4.52301 0.0376017 4.8424 0.109039 5.1495C0.202884 5.55295 0.606016 5.80392 1.00946 5.71008C1.4129 5.61623 1.66388 5.2131 1.57003 4.80966C1.52433 4.61318 1.5 4.4078 1.5 4.19579ZM10.5 7C12.433 7 14 5.433 14 3.5C14 1.567 12.433 0 10.5 0C8.567 0 7 1.567 7 3.5C7 5.433 8.567 7 10.5 7ZM11.9419 2.94194C12.186 2.69786 12.186 2.30214 11.9419 2.05806C11.6979 1.81398 11.3021 1.81398 11.0581 2.05806L10.0581 3.05806C9.81398 3.30214 9.81398 3.69786 10.0581 3.94194C10.3021 4.18602 10.6979 4.18602 10.9419 3.94194L11.9419 2.94194ZM3.13338 4.25939V9.69608L2.58954 9.79843C1.37077 10.0278 0.790666 11.4348 1.49366 12.4565L2.25765 13.5669C2.44424 13.838 2.75232 14 3.08147 14H9.68891C10.2412 14 10.6889 13.5523 10.6889 13V10.9093C10.6889 9.48828 9.53693 8.33631 8.1159 8.3363H5.65215V4.25938C5.65215 3.56385 5.08831 3 4.39277 3C3.69723 3 3.13338 3.56385 3.13338 4.25939Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186549\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"one-finger-tap\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.25391 4.36099C3.25391 2.78091 4.53482 1.5 6.1149 1.5C7.69499 1.5 8.97589 2.78091 8.97589 4.36099C8.97589 4.58593 8.95008 4.80389 8.90157 5.01244C8.80772 5.41588 9.0587 5.81902 9.46214 5.91286C9.86559 6.00671 10.2687 5.75573 10.3625 5.35229C10.4368 5.03311 10.4759 4.70114 10.4759 4.36099C10.4759 1.95248 8.52341 0 6.1149 0C3.70639 0 1.75391 1.95248 1.75391 4.36099C1.75391 4.70114 1.793 5.03311 1.86724 5.35229C1.96109 5.75573 2.36422 6.00671 2.76766 5.91286C3.17111 5.81902 3.42208 5.41588 3.32824 5.01244C3.27973 4.80389 3.25391 4.58593 3.25391 4.36099ZM4.8836 9.69608V4.25939C4.8836 3.56385 5.44745 3 6.14299 3C6.83853 3 7.40237 3.56385 7.40237 4.25938V8.3363H9.86612C11.2871 8.33631 12.4391 9.48828 12.4391 10.9093V13C12.4391 13.5523 11.9914 14 11.4391 14H4.83169C4.50254 14 4.19446 13.838 4.00787 13.5669L3.24388 12.4565C2.54089 11.4348 3.12099 10.0278 4.33976 9.79843L4.8836 9.69608Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"open-book\", \"keywords\": [ \"content\", \"books\", \"book\", \"open\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186555)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.375 1.65273C5.38611 1.09884 3.53626 0.42131 1.49642 0.179297C0.673762 0.0816939 0 0.759606 0 1.58803V9.58803C0 10.4165 0.677027 11.0766 1.49213 11.2246C3.33255 11.5588 4.86332 12.4413 5.8401 13.139C6.00374 13.2559 6.18487 13.3444 6.375 13.4046V1.65273ZM7.625 13.4048C7.81487 13.3447 7.99576 13.2563 8.15919 13.1396C9.13594 12.4418 10.667 11.559 12.5079 11.2248C13.323 11.0768 14 10.4166 14 9.58817V1.58818C14 0.759749 13.3262 0.0818364 12.5036 0.179439C10.4637 0.421452 8.61389 1.09889 7.625 1.65275V13.4048Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186555\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"open-umbrella\", \"keywords\": [ \"storm\", \"rain\", \"umbrella\", \"open\", \"weather\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 0C7.41421 0 7.75 0.335786 7.75 0.75V1.55135C8.93166 1.71399 10.0361 2.25788 10.8891 3.11091C11.9205 4.14236 12.5 5.54131 12.5 7C12.5 7.13261 12.4473 7.25979 12.3536 7.35355C12.2598 7.44732 12.1326 7.5 12 7.5H7.75V11.75C7.75 12.1642 8.08579 12.5 8.5 12.5C8.91421 12.5 9.25 12.1642 9.25 11.75C9.25 11.3358 9.58579 11 10 11C10.4142 11 10.75 11.3358 10.75 11.75C10.75 12.9926 9.74264 14 8.5 14C7.25736 14 6.25 12.9926 6.25 11.75V7.5H2C1.86739 7.5 1.74021 7.44732 1.64645 7.35355C1.55268 7.25979 1.5 7.13261 1.5 7C1.5 5.54131 2.07946 4.14236 3.11091 3.11091C3.96395 2.25788 5.06834 1.71399 6.25 1.55135V0.75C6.25 0.335786 6.58579 0 7 0Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"padlock-square-1\", \"keywords\": [ \"combination\", \"combo\", \"lock\", \"locked\", \"padlock\", \"secure\", \"security\", \"shield\", \"keyhole\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.99997 2C5.8954 2 4.99997 2.89543 4.99997 4V5H8.99997V4C8.99997 2.89543 8.10454 2 6.99997 2ZM2.99997 4V5C2.17156 5.00002 1.5 5.67158 1.5 6.5V12.5C1.5 13.3284 2.17157 14 3 14H11C11.8285 14 12.5 13.3284 12.5 12.5V6.5C12.5 5.67157 11.8285 5 11 5V4C11 1.79086 9.20911 0 6.99997 0C4.79083 0 2.99997 1.79086 2.99997 4ZM7 10.75C7.69036 10.75 8.25 10.1904 8.25 9.5C8.25 8.80964 7.69036 8.25 7 8.25C6.30964 8.25 5.75 8.80964 5.75 9.5C5.75 10.1904 6.30964 10.75 7 10.75Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"page-setting\", \"keywords\": [ \"page\", \"setting\", \"square\", \"triangle\", \"circle\", \"line\", \"combination\", \"variation\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186564)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.56452 5.62607L10.21 0.33381C10.4325 -0.11127 11.0675 -0.11127 11.29 0.33381L13.9355 5.62607C14.1362 6.02759 13.8443 6.5 13.3955 6.5H8.1045C7.65571 6.5 7.36381 6.02759 7.56452 5.62607ZM0.5 0.500488C0.223858 0.500488 0 0.724346 0 1.00049V6.00049C0 6.27663 0.223858 6.50049 0.5 6.50049H5.5C5.77614 6.50049 6 6.27663 6 6.00049V1.00049C6 0.724346 5.77614 0.500488 5.5 0.500488H0.5ZM6 11C6 12.6569 4.65685 14 3 14C1.34315 14 0 12.6569 0 11C0 9.34315 1.34315 8 3 8C4.65685 8 6 9.34315 6 11ZM8.5 12.5C8.08579 12.5 7.75 12.8358 7.75 13.25C7.75 13.6642 8.08579 14 8.5 14H13C13.4142 14 13.75 13.6642 13.75 13.25C13.75 12.8358 13.4142 12.5 13 12.5H8.5ZM7.75 11C7.75 10.5858 8.08579 10.25 8.5 10.25H13C13.4142 10.25 13.75 10.5858 13.75 11C13.75 11.4142 13.4142 11.75 13 11.75H8.5C8.08579 11.75 7.75 11.4142 7.75 11ZM8.5 8C8.08579 8 7.75 8.33579 7.75 8.75C7.75 9.16421 8.08579 9.5 8.5 9.5H13C13.4142 9.5 13.75 9.16421 13.75 8.75C13.75 8.33579 13.4142 8 13 8H8.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186564\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"paint-bucket\", \"keywords\": [ \"bucket\", \"color\", \"colors\", \"design\", \"paint\", \"painting\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186573)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.8503 5.86139C12.0703 6.08155 12.0443 6.44567 11.7952 6.63231L4.89561 11.8016C4.70047 11.9449 4.4608 12.0144 4.21927 11.9975C3.97774 11.9807 3.75003 11.8786 3.57668 11.7095L0.294677 8.42643C0.122998 8.2524 0.0193237 8.0226 0.0024427 7.77868C-0.0144383 7.53476 0.0565909 7.29287 0.202658 7.09683L5.37012 0.204566C5.55678 -0.0443971 5.92059 -0.0702614 6.14057 0.149793L11.8503 5.86139ZM11.9515 10.2978C11.4872 10.9945 11 11.8936 11 12.5153C11 13.6288 11.75 14 12.5 14C13.25 14 14 13.6288 14 12.5153C14 11.8936 13.5128 10.9945 13.0485 10.2978C12.7839 9.90072 12.2161 9.90072 11.9515 10.2978Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186573\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"paint-palette\", \"keywords\": [ \"color\", \"colors\", \"design\", \"paint\", \"painting\", \"palette\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186576)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.34906 0.577145C5.52631 0.0669898 6.82112 -0.109798 8.09203 0.0660954C9.36295 0.241989 10.5611 0.763799 11.5556 1.57451C12.5498 2.3851 13.3023 3.45332 13.7306 4.6625C14 5.42021 13.8801 6.28804 13.4156 6.94442C12.951 7.6006 12.1728 8.002 11.3688 8H9.49947H9.49808C9.14101 7.99901 8.7953 8.12544 8.5231 8.35655C8.25091 8.58766 8.07008 8.90828 8.01314 9.26079C7.95619 9.61329 8.02687 9.97455 8.21245 10.2796C8.39804 10.5847 8.68636 10.8135 9.02558 10.925C9.03164 10.927 9.03766 10.9291 9.04364 10.9313C9.59371 11.1357 9.97846 11.6332 10.0155 12.217C10.0962 12.8877 9.65858 13.5577 9.00527 13.7412C8.39487 13.9146 7.7632 14.0017 7.12865 14C5.84583 13.9993 4.58785 13.6461 3.49213 12.979C1.27863 11.6313 -0.0296009 9.1028 0.149038 6.51747C0.327676 3.93215 1.97124 1.60757 4.34906 0.577145ZM4.49945 7C5.32788 7 5.99945 6.32843 5.99945 5.5C5.99945 4.67157 5.32788 4 4.49945 4C3.67102 4 2.99945 4.67157 2.99945 5.5C2.99945 6.32843 3.67102 7 4.49945 7ZM10.4994 4.00049C10.4994 4.82892 9.82776 5.50049 8.99933 5.50049C8.1709 5.50049 7.49933 4.82892 7.49933 4.00049C7.49933 3.17206 8.1709 2.50049 8.99933 2.50049C9.82776 2.50049 10.4994 3.17206 10.4994 4.00049ZM4.49933 10.4995C5.05161 10.4995 5.49933 10.0518 5.49933 9.49951C5.49933 8.94723 5.05161 8.49951 4.49933 8.49951C3.94704 8.49951 3.49933 8.94723 3.49933 9.49951C3.49933 10.0518 3.94704 10.4995 4.49933 10.4995Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186576\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"paintbrush-1\", \"keywords\": [ \"brush\", \"color\", \"colors\", \"design\", \"paint\", \"painting\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186567)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.7071 5.29192C12.5196 5.10438 12.2652 4.99902 12 4.99902H10C9.73481 4.99902 9.48045 4.89367 9.29292 4.70613C9.10538 4.51859 9.00002 4.26424 9.00002 3.99902V1.99902C9.00002 1.46859 8.78931 0.959883 8.41424 0.58481C8.03917 0.209737 7.53046 -0.000976562 7.00002 -0.000976562C6.46959 -0.000976562 5.96088 0.209737 5.58581 0.58481C5.21074 0.959883 5.00003 1.46859 5.00003 1.99902V3.99902C5.00003 4.26424 4.89467 4.51859 4.70713 4.70613C4.5196 4.89367 4.26524 4.99902 4.00003 4.99902H2C1.73478 4.99902 1.48043 5.10438 1.29289 5.29192C1.10536 5.47945 1 5.73381 1 5.99902V7.99951C1 8.27565 1.22386 8.49951 1.5 8.49951H12.5C12.7761 8.49951 13 8.27565 13 7.99951V5.99902C13 5.73381 12.8946 5.47945 12.7071 5.29192ZM1.00938 12.9343L1.5 9.5H12.5L12.9906 12.9343C12.9969 12.9781 13 13.0222 13 13.0664C13 13.582 12.582 14 12.0664 14H9.25V12C9.25 11.5858 8.91421 11.25 8.5 11.25C8.08579 11.25 7.75 11.5858 7.75 12V14H1.93365C1.41801 14 1 13.582 1 13.0664C1 13.0222 1.00314 12.9781 1.00938 12.9343Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186567\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"paintbrush-2\", \"keywords\": [ \"brush\", \"color\", \"colors\", \"design\", \"paint\", \"painting\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186570)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.4106 0.59369C13.2141 0.396859 12.9796 0.242735 12.7216 0.14084C12.4637 0.0389446 12.1878 -0.00855877 11.9109 0.00126425C11.6342 0.00884546 11.362 0.0734788 11.111 0.191179C10.86 0.308879 10.6357 0.47715 10.4517 0.685618L5.13269 6.6382C5.60427 6.84591 6.03345 7.13981 6.39797 7.50523C6.79645 7.90468 7.10929 8.38119 7.31737 8.90564C7.32808 8.93263 7.3385 8.95973 7.34864 8.98692L13.3194 3.57624C13.525 3.39198 13.6912 3.16745 13.808 2.91633C13.9247 2.66522 13.9895 2.39278 13.9983 2.11561C14.0095 1.83482 13.9631 1.55474 13.8619 1.29287C13.7608 1.031 13.6072 0.792981 13.4106 0.59369ZM5.39191 12.5989L5.40996 12.5808C3.99565 13.9859 1.68776 14.5184 0.166681 13.3884C-0.0502408 13.2272 -0.0452245 12.9162 0.122684 12.7045C0.856989 11.7785 0.788576 11.0732 0.725171 10.4196C0.663372 9.78249 0.60633 9.19443 1.30191 8.49886C1.56529 8.20546 1.88574 7.96886 2.24366 7.8035C2.60159 7.63814 2.98947 7.54751 3.38361 7.53714C3.77775 7.52676 4.16986 7.59687 4.53599 7.74317C4.90211 7.88947 5.23456 8.10889 5.51302 8.38803C5.79147 8.66716 6.01008 9.00015 6.15549 9.36663C6.30089 9.73311 6.37004 10.1254 6.35871 10.5195C6.34737 10.9136 6.25579 11.3013 6.08956 11.6588C5.92333 12.0163 5.68594 12.3362 5.39191 12.5989Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186570\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"paperclip-1\", \"keywords\": [ \"attachment\", \"link\", \"paperclip\", \"unlink\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186579)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.3825 1.5C9.2287 1.5 9.07643 1.53045 8.93445 1.5896C8.79248 1.64875 8.66363 1.73542 8.55532 1.84462L8.55398 1.84596L2.48509 7.934C2.17796 8.24673 2.00586 8.66755 2.00586 9.10593C2.00586 9.545 2.17851 9.96647 2.48656 10.2793L4.19125 12.0128C4.34249 12.1669 4.52347 12.2898 4.72256 12.3734C4.92166 12.457 5.13541 12.5 5.35133 12.5C5.56725 12.5 5.781 12.457 5.98009 12.3734C6.17919 12.2898 6.35962 12.1674 6.51086 12.0133L6.51359 12.0105L12.2108 6.2654C12.5025 5.97128 12.9774 5.9693 13.2715 6.26096C13.5656 6.55263 13.5676 7.0275 13.2759 7.32161L7.58143 13.064C7.58095 13.0645 7.58047 13.065 7.57999 13.0654C7.28941 13.3611 6.94289 13.5961 6.56061 13.7565C6.1777 13.9172 5.7666 14 5.35133 14C4.93606 14 4.52495 13.9172 4.14204 13.7565C3.75918 13.5958 3.41218 13.3604 3.12133 13.0641C3.1213 13.0641 3.12137 13.0641 3.12133 13.0641L1.41768 11.3317C0.833363 10.7382 0.505859 9.93878 0.505859 9.10593C0.505859 8.27307 0.833363 7.47362 1.41768 6.88014L1.42094 6.87682L7.49032 0.788317C7.49058 0.788054 7.49084 0.787792 7.4911 0.787529C7.73869 0.538109 8.03317 0.340119 8.35759 0.204961C8.68235 0.0696598 9.03069 0 9.3825 0C9.73432 0 10.0826 0.0696596 10.4074 0.204961C10.7316 0.340052 11.026 0.537911 11.2735 0.787154C11.2739 0.787542 11.2743 0.787929 11.2747 0.788317L11.9502 1.46381C11.9505 1.4642 11.9509 1.46458 11.9513 1.46497C12.2006 1.71248 12.3984 2.00682 12.5335 2.33108C12.6688 2.65584 12.7385 3.00418 12.7385 3.35599C12.7385 3.70781 12.6688 4.05615 12.5335 4.38091C12.3985 4.70496 12.2008 4.99914 11.9518 5.24655C11.9512 5.2471 11.9507 5.24764 11.9502 5.24818L6.57325 10.6443C6.57297 10.6445 6.5727 10.6448 6.57243 10.6451C6.41386 10.8047 6.2253 10.9315 6.01758 11.018C5.8095 11.1047 5.58632 11.1493 5.3609 11.1493C5.13549 11.1493 4.91231 11.1047 4.70423 11.018C4.49615 10.9313 4.3073 10.8043 4.14856 10.6443L4.14329 10.6389L3.81225 10.2984C3.65481 10.1408 3.5297 9.95392 3.44401 9.74824C3.35733 9.54016 3.31269 9.31698 3.31269 9.09156C3.31269 8.86615 3.35732 8.64297 3.44401 8.43489C3.53069 8.22684 3.6577 8.038 3.81772 7.87928C3.8177 7.8793 3.81774 7.87926 3.81772 7.87928L7.42757 4.29812C7.72163 4.0064 8.1965 4.0083 8.48822 4.30236C8.77994 4.59642 8.77805 5.07129 8.48399 5.36301L4.87414 8.94416C4.85469 8.96346 4.83919 8.98647 4.82865 9.01175C4.81812 9.03704 4.81269 9.06417 4.81269 9.09156C4.81269 9.11896 4.81812 9.14608 4.82865 9.17137C4.83919 9.19666 4.85463 9.21961 4.87408 9.23891L4.88376 9.24851L5.21525 9.58964C5.23418 9.6083 5.25654 9.62316 5.28109 9.63339C5.30638 9.64392 5.33351 9.64935 5.3609 9.64935C5.3883 9.64935 5.41542 9.64392 5.44071 9.63339C5.466 9.62285 5.48895 9.60741 5.50825 9.58796L5.50947 9.58673L10.8907 4.18629L10.8938 4.18318C11.003 4.07487 11.0897 3.94601 11.1489 3.80404C11.208 3.66207 11.2385 3.5098 11.2385 3.35599C11.2385 3.20219 11.208 3.04991 11.1489 2.90794C11.0897 2.76597 11.003 2.63712 10.8938 2.52881L10.8917 2.52664L10.2118 1.8468L10.2097 1.84462C10.1014 1.73542 9.97253 1.64875 9.83056 1.5896C9.68858 1.53045 9.5363 1.5 9.3825 1.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186579\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"paragraph\", \"keywords\": [ \"alignment\", \"paragraph\", \"formatting\", \"text\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186582)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.78646 0C2.14297 0 0 2.14297 0 4.78646C0 7.42995 2.14297 9.57292 4.78646 9.57292H5.72916V13.25C5.72916 13.6642 6.06494 14 6.47916 14C6.89337 14 7.22916 13.6642 7.22916 13.25V8.82683C7.22916 8.82553 7.22917 8.82422 7.22917 8.82292V1.5H9.375V13.25C9.375 13.6642 9.71079 14 10.125 14C10.5392 14 10.875 13.6642 10.875 13.25V1.5H13.25C13.6642 1.5 14 1.16421 14 0.75C14 0.335786 13.6642 0 13.25 0H10.125H6.47917H4.78646Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186582\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pathfinder-divide\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186585)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.5 0H1.5C0.671573 0 0 0.671573 0 1.5V8.5C0 9.32843 0.671573 10 1.5 10H2.752V5.37014C2.752 3.92039 3.92725 2.74514 5.377 2.74514H10V1.5C10 0.671573 9.32843 0 8.5 0ZM10 3.99514V4H10.0135V8.63186C10.0135 9.39125 9.39786 10.0069 8.63847 10.0069H4.002V5.37014C4.002 4.61075 4.6176 3.99514 5.377 3.99514H10ZM11.2635 4H12.5C13.3284 4 14 4.67157 14 5.5V12.5C14 13.3284 13.3284 14 12.5 14H5.5C4.67157 14 4 13.3284 4 12.5V11.2569H8.63847C10.0882 11.2569 11.2635 10.0816 11.2635 8.63186V4Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186585\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pathfinder-exclude\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186588)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0H8.5C9.32843 0 10 0.671573 10 1.5V4H12.5C13.3284 4 14 4.67157 14 5.5V12.5C14 13.3284 13.3284 14 12.5 14H5.5C4.67157 14 4 13.3284 4 12.5V10H1.5C0.671573 10 0 9.32843 0 8.5V1.5C0 0.671573 0.671573 0 1.5 0ZM5 9H8.5C8.77614 9 9 8.77614 9 8.5V5H5.5C5.22386 5 5 5.22386 5 5.5V9Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186588\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pathfinder-intersect\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186591)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.27747 2.27747C1.13737 2.41756 0.948107 2.49741 0.75 2.5C0.551893 2.49741 0.362629 2.41756 0.222534 2.27747C0.0824394 2.13737 0.00258996 1.94811 0 1.75C0.0026251 1.28668 0.187842 0.843085 0.515464 0.515464C0.843085 0.187842 1.28668 0.0026251 1.75 0C1.94891 0 2.13968 0.079018 2.28033 0.21967C2.42098 0.360323 2.5 0.551088 2.5 0.75C2.5 0.948912 2.42098 1.13968 2.28033 1.28033C2.13968 1.42098 1.94891 1.5 1.75 1.5C1.6837 1.5 1.62011 1.52634 1.57322 1.57322C1.52634 1.62011 1.5 1.6837 1.5 1.75C1.49741 1.94811 1.41756 2.13737 1.27747 2.27747ZM12.25 5C12.3163 5 12.3799 5.02634 12.4268 5.07322C12.4737 5.12011 12.5 5.1837 12.5 5.25C12.5 5.44891 12.579 5.63968 12.7197 5.78033C12.8603 5.92098 13.0511 6 13.25 6C13.4489 6 13.6397 5.92098 13.7803 5.78033C13.921 5.63968 14 5.44891 14 5.25C13.9974 4.78668 13.8122 4.34309 13.4845 4.01546C13.1569 3.68784 12.7133 3.50262 12.25 3.5C12.0511 3.5 11.8603 3.57902 11.7197 3.71967C11.579 3.86032 11.5 4.05109 11.5 4.25C11.5 4.44891 11.579 4.63968 11.7197 4.78033C11.8603 4.92098 12.0511 5 12.25 5ZM13.25 11.5C13.0519 11.5026 12.8626 11.5824 12.7225 11.7225C12.5824 11.8626 12.5026 12.0519 12.5 12.25C12.5 12.3163 12.4737 12.3799 12.4268 12.4268C12.3799 12.4737 12.3163 12.5 12.25 12.5C12.0511 12.5 11.8603 12.579 11.7197 12.7197C11.579 12.8603 11.5 13.0511 11.5 13.25C11.5 13.4489 11.579 13.6397 11.7197 13.7803C11.8603 13.921 12.0511 14 12.25 14C12.7133 13.9974 13.1569 13.8122 13.4845 13.4845C13.8122 13.1569 13.9974 12.7133 14 12.25C13.9974 12.0519 13.9176 11.8626 13.7775 11.7225C13.6374 11.5824 13.4481 11.5026 13.25 11.5ZM5.07322 12.4268C5.12011 12.4737 5.1837 12.5 5.25 12.5C5.44891 12.5 5.63968 12.579 5.78033 12.7197C5.92098 12.8603 6 13.0511 6 13.25C6 13.4489 5.92098 13.6397 5.78033 13.7803C5.63968 13.921 5.44891 14 5.25 14C4.78668 13.9974 4.34309 13.8122 4.01546 13.4845C3.68784 13.1569 3.50262 12.7133 3.5 12.25C3.5 12.0511 3.57902 11.8603 3.71967 11.7197C3.86032 11.579 4.05109 11.5 4.25 11.5C4.44891 11.5 4.63968 11.579 4.78033 11.7197C4.92098 11.8603 5 12.0511 5 12.25C5 12.3163 5.02634 12.3799 5.07322 12.4268ZM8 12.5C7.80109 12.5 7.61032 12.579 7.46967 12.7197C7.32902 12.8603 7.25 13.0511 7.25 13.25C7.25 13.4489 7.32902 13.6397 7.46967 13.7803C7.61032 13.921 7.80109 14 8 14H9.5C9.69891 14 9.88968 13.921 10.0303 13.7803C10.171 13.6397 10.25 13.4489 10.25 13.25C10.25 13.0511 10.171 12.8603 10.0303 12.7197C9.88968 12.579 9.69891 12.5 9.5 12.5H8ZM12.7225 7.47253C12.8626 7.33244 13.0519 7.25259 13.25 7.25C13.4481 7.25259 13.6374 7.33244 13.7775 7.47253C13.9176 7.61263 13.9974 7.80189 14 8V9.5C14 9.69891 13.921 9.88968 13.7803 10.0303C13.6397 10.171 13.4489 10.25 13.25 10.25C13.0511 10.25 12.8603 10.171 12.7197 10.0303C12.579 9.88968 12.5 9.69891 12.5 9.5V8C12.5026 7.80189 12.5824 7.61263 12.7225 7.47253ZM1.75 9C1.6837 9 1.62011 8.97366 1.57322 8.92678C1.52634 8.87989 1.5 8.8163 1.5 8.75C1.5 8.55109 1.42098 8.36032 1.28033 8.21967C1.13968 8.07902 0.948912 8 0.75 8C0.551088 8 0.360323 8.07902 0.21967 8.21967C0.079018 8.36032 0 8.55109 0 8.75C0.0026251 9.21332 0.187842 9.65691 0.515464 9.98454C0.843085 10.3122 1.28668 10.4974 1.75 10.5C1.94891 10.5 2.13968 10.421 2.28033 10.2803C2.42098 10.1397 2.5 9.94891 2.5 9.75C2.5 9.55109 2.42098 9.36032 2.28033 9.21967C2.13968 9.07902 1.94891 9 1.75 9ZM8.75 1.5C8.8163 1.5 8.87989 1.52634 8.92678 1.57322C8.97366 1.62011 9 1.6837 9 1.75C9 1.94891 9.07902 2.13968 9.21967 2.28033C9.36032 2.42098 9.55109 2.5 9.75 2.5C9.94891 2.5 10.1397 2.42098 10.2803 2.28033C10.421 2.13968 10.5 1.94891 10.5 1.75C10.4974 1.28668 10.3122 0.843085 9.98454 0.515464C9.65691 0.187842 9.21332 0.0026251 8.75 0C8.55109 0 8.36032 0.079018 8.21967 0.21967C8.07902 0.360323 8 0.551088 8 0.75C8 0.948912 8.07902 1.13968 8.21967 1.28033C8.36032 1.42098 8.55109 1.5 8.75 1.5ZM6 1.5H4.5C4.30109 1.5 4.11032 1.42098 3.96967 1.28033C3.82902 1.13968 3.75 0.948912 3.75 0.75C3.75 0.551088 3.82902 0.360323 3.96967 0.21967C4.11032 0.079018 4.30109 0 4.5 0H6C6.19891 0 6.38968 0.079018 6.53033 0.21967C6.67098 0.360323 6.75 0.551088 6.75 0.75C6.75 0.948912 6.67098 1.13968 6.53033 1.28033C6.38968 1.42098 6.19891 1.5 6 1.5ZM0.75 6.75C0.948107 6.74741 1.13737 6.66756 1.27747 6.52747C1.41756 6.38737 1.49741 6.19811 1.5 6V4.5C1.5 4.30109 1.42098 4.11032 1.28033 3.96967C1.13968 3.82902 0.948912 3.75 0.75 3.75C0.551088 3.75 0.360323 3.82902 0.21967 3.96967C0.079018 4.11032 0 4.30109 0 4.5V6C0.00258996 6.19811 0.0824394 6.38737 0.222534 6.52747C0.362629 6.66756 0.551893 6.74741 0.75 6.75ZM5.25 4.25C4.69772 4.25 4.25 4.69772 4.25 5.25V9.25C4.25 9.52614 4.47386 9.75 4.75 9.75H8.75C9.30228 9.75 9.75 9.30228 9.75 8.75V4.75C9.75 4.47386 9.52614 4.25 9.25 4.25H5.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186591\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pathfinder-merge\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186594)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.5 0H1.5C0.671573 0 0 0.671573 0 1.5V8.5C0 9.32843 0.671573 10 1.5 10H2.75387V6.5C2.75387 6.15482 3.03369 5.875 3.37887 5.875C3.72405 5.875 4.00387 6.15482 4.00387 6.5V10.75C4.00387 10.7736 4.00256 10.797 4 10.8199V12.5C4 13.3284 4.67157 14 5.5 14H12.5C13.3284 14 14 13.3284 14 12.5V5.5C14 4.67157 13.3284 4 12.5 4H10V1.5C10 0.671573 9.32843 0 8.5 0Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186594\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pathfinder-minus-front-1\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186597)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0H8.5C9.32843 0 10 0.671573 10 1.5V4C10 4.55228 9.55229 5 9 5H5.5C5.22386 5 5 5.22386 5 5.5V9C5 9.55229 4.55228 10 4 10H1.5C0.671573 10 0 9.32843 0 8.5V1.5C0 0.671573 0.671573 0 1.5 0ZM12.4268 5.07322C12.3799 5.02634 12.3163 5 12.25 5C12.0511 5 11.8603 4.92098 11.7197 4.78033C11.579 4.63968 11.5 4.44891 11.5 4.25C11.5 4.05109 11.579 3.86032 11.7197 3.71967C11.8603 3.57902 12.0511 3.5 12.25 3.5C12.7133 3.50262 13.1569 3.68784 13.4845 4.01546C13.8122 4.34309 13.9974 4.78668 14 5.25C14 5.44891 13.921 5.63968 13.7803 5.78033C13.6397 5.92098 13.4489 6 13.25 6C13.0511 6 12.8603 5.92098 12.7197 5.78033C12.579 5.63968 12.5 5.44891 12.5 5.25C12.5 5.1837 12.4737 5.12011 12.4268 5.07322ZM12.7225 11.7225C12.8626 11.5824 13.0519 11.5026 13.25 11.5C13.4481 11.5026 13.6374 11.5824 13.7775 11.7225C13.9176 11.8626 13.9974 12.0519 14 12.25C13.9974 12.7133 13.8122 13.1569 13.4845 13.4845C13.1569 13.8122 12.7133 13.9974 12.25 14C12.0511 14 11.8603 13.921 11.7197 13.7803C11.579 13.6397 11.5 13.4489 11.5 13.25C11.5 13.0511 11.579 12.8603 11.7197 12.7197C11.8603 12.579 12.0511 12.5 12.25 12.5C12.3163 12.5 12.3799 12.4737 12.4268 12.4268C12.4737 12.3799 12.5 12.3163 12.5 12.25C12.5026 12.0519 12.5824 11.8626 12.7225 11.7225ZM5.25 12.5C5.1837 12.5 5.12011 12.4737 5.07322 12.4268C5.02634 12.3799 5 12.3163 5 12.25C5 12.0511 4.92098 11.8603 4.78033 11.7197C4.63968 11.579 4.44891 11.5 4.25 11.5C4.05109 11.5 3.86032 11.579 3.71967 11.7197C3.57902 11.8603 3.5 12.0511 3.5 12.25C3.50262 12.7133 3.68784 13.1569 4.01546 13.4845C4.34309 13.8122 4.78668 13.9974 5.25 14C5.44891 14 5.63968 13.921 5.78033 13.7803C5.92098 13.6397 6 13.4489 6 13.25C6 13.0511 5.92098 12.8603 5.78033 12.7197C5.63968 12.579 5.44891 12.5 5.25 12.5ZM8 12.5H9.5C9.69891 12.5 9.88968 12.579 10.0303 12.7197C10.171 12.8603 10.25 13.0511 10.25 13.25C10.25 13.4489 10.171 13.6397 10.0303 13.7803C9.88968 13.921 9.69891 14 9.5 14H8C7.80109 14 7.61032 13.921 7.46967 13.7803C7.32902 13.6397 7.25 13.4489 7.25 13.25C7.25 13.0511 7.32902 12.8603 7.46967 12.7197C7.61032 12.579 7.80109 12.5 8 12.5ZM13.25 7.25C13.0519 7.25259 12.8626 7.33244 12.7225 7.47253C12.5824 7.61263 12.5026 7.80189 12.5 8V9.5C12.5 9.69891 12.579 9.88968 12.7197 10.0303C12.8603 10.171 13.0511 10.25 13.25 10.25C13.4489 10.25 13.6397 10.171 13.7803 10.0303C13.921 9.88968 14 9.69891 14 9.5V8C13.9974 7.80189 13.9176 7.61263 13.7775 7.47253C13.6374 7.33244 13.4481 7.25259 13.25 7.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186597\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pathfinder-trim\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186600)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.5 0H1.5C0.671573 0 0 0.671573 0 1.5V8.5C0 9.32843 0.671573 10 1.5 10H2.75V5.5C2.75 3.98122 3.98122 2.75 5.5 2.75H10V1.5C10 0.671573 9.32843 0 8.5 0ZM5.5 4H12.5C13.3284 4 14 4.67157 14 5.5V12.5C14 13.3284 13.3284 14 12.5 14H5.5C4.67157 14 4 13.3284 4 12.5V5.5C4 4.67157 4.67157 4 5.5 4Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186600\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pathfinder-union\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186603)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0H8.5C9.32843 0 10 0.671573 10 1.5V4H12.5C13.3284 4 14 4.67157 14 5.5V12.5C14 13.3284 13.3284 14 12.5 14H5.5C4.67157 14 4 13.3284 4 12.5V10H1.5C0.671573 10 0 9.32843 0 8.5V1.5C0 0.671573 0.671573 0 1.5 0Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186603\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"peace-hand\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186606)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.5578 9.27347C12.894 7.22265 10.6671 6.38084 8.20245 5.9032L8.51905 1.32174C8.56844 0.606994 8.00197 1.1173e-05 7.28552 1.52765e-10C6.65808 -9.78351e-06 6.1301 0.469918 6.05735 1.09313L5.54363 5.41663L4.41197 1.30646C4.21245 0.581807 3.45935 0.135898 2.72812 0.309448C1.99806 0.482716 1.56439 1.20834 1.75766 1.93322L2.99842 6.58691L6.77916 6.58692C7.73078 6.58692 8.50222 7.35836 8.50222 8.30997C8.50222 9.10683 7.95581 9.79974 7.18091 9.98554L7.06926 10.0123C7.45293 10.4024 7.74988 10.9444 7.77633 11.6359C7.78952 11.9808 7.5206 12.2712 7.17567 12.2843C6.83075 12.2975 6.54043 12.0286 6.52724 11.6837C6.51101 11.2593 6.28871 10.9417 5.98391 10.723C5.64857 10.4824 5.32151 10.4363 5.23424 10.4506C4.90235 10.505 4.58695 10.287 4.52051 9.95727C4.45408 9.62759 4.66043 9.30445 4.98747 9.22603L6.88946 8.76999C7.1022 8.71898 7.25222 8.52875 7.25222 8.30997C7.25222 8.04871 7.04042 7.83692 6.77916 7.83692L3.29617 7.83691C2.35425 7.85316 1.48402 8.5155 1.42323 9.45714C1.34702 10.6374 1.53298 12.0132 2.62992 12.9684C2.97739 13.271 3.3013 13.5557 3.52064 13.749C3.70398 13.9106 3.93974 14 4.18414 14H10.26C10.521 14 10.775 13.9008 10.9476 13.7049C12.1056 12.3903 12.3301 10.8332 12.491 9.71756C12.5137 9.5601 12.5351 9.41143 12.5578 9.27347Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186606\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pen-3\", \"keywords\": [ \"content\", \"creation\", \"edit\", \"pen\", \"pens\", \"write\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186609)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.5 1.99951C7.5 1.46908 7.71071 0.960371 8.08579 0.585298C8.46086 0.210225 8.96957 -0.000488281 9.5 -0.000488281C10.0304 -0.000488281 10.5391 0.210225 10.9142 0.585298C11.2893 0.960371 11.5 1.46908 11.5 1.99951V2.34863H12.25C12.7141 2.34863 13.1592 2.53301 13.4874 2.8612C13.8156 3.18938 14 3.6345 14 4.09863V8.09863C14 8.51285 13.6642 8.84863 13.25 8.84863C12.8358 8.84863 12.5 8.51285 12.5 8.09863V4.09863C12.5 4.03233 12.4737 3.96874 12.4268 3.92186C12.3799 3.87497 12.3163 3.84863 12.25 3.84863H11.5V3.875H7.5V1.99951ZM7.5 11V5.125H11.5V11C11.5 11.2652 11.3946 11.5196 11.2071 11.7071C11.0196 11.8946 10.7652 12 10.5 12H10.25V13.25C10.25 13.6642 9.91421 14 9.5 14C9.08579 14 8.75 13.6642 8.75 13.25V12H8.5C8.23478 12 7.98043 11.8946 7.79289 11.7071C7.60536 11.5196 7.5 11.2652 7.5 11ZM4.76777 0.732233C5.23661 1.20107 5.5 1.83696 5.5 2.5V3.875H0.5V2.5C0.5 1.83696 0.763392 1.20107 1.23223 0.732233C1.70107 0.263392 2.33696 -2.61934e-10 3 -2.61934e-10C3.66304 -2.61934e-10 4.29893 0.263392 4.76777 0.732233ZM0.5 11.1001V5.125H5.5V11.1001C5.5 11.2171 5.45899 11.3303 5.38411 11.4202L3.38411 13.8202C3.18421 14.0601 2.81579 14.0601 2.61589 13.8202L0.615889 11.4202C0.541008 11.3303 0.5 11.2171 0.5 11.1001Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186609\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pen-draw\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186615)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.60989 2.65973C8.38252 2.46253 7.79205 2.29455 7.00735 2.78877C4.4994 4.36832 2.31435 7.219 1.57482 8.25244C1.33377 8.58929 0.86529 8.66695 0.528441 8.4259C0.191591 8.18485 0.113931 7.71637 0.354981 7.37952C1.1049 6.33156 3.43302 3.26723 6.20796 1.51952C7.4092 0.762962 8.74944 0.795176 9.59271 1.52657C10.0145 1.89237 10.2776 2.4165 10.2814 3.01284C10.2852 3.60408 10.0351 4.18988 9.57923 4.71111C8.15225 6.3426 6.26687 8.55596 5.47511 9.63646C5.37143 9.77795 5.3637 9.88544 5.37282 9.94846C5.3829 10.0181 5.4205 10.0835 5.48398 10.131C5.54448 10.1762 5.64142 10.214 5.78162 10.1977C5.92284 10.1813 6.14248 10.1042 6.40988 9.85907C6.63649 9.65129 6.8716 9.42983 7.11313 9.20233C7.85774 8.50099 8.66336 7.74217 9.46899 7.14937C10.3071 6.53269 11.2867 6.3616 12.0303 6.81618C12.4012 7.04289 12.6619 7.40556 12.7526 7.84039C12.8423 8.2703 12.7567 8.71837 12.5402 9.1296C12.3076 9.57154 12.1063 9.89095 11.9251 10.1764L11.9108 10.199C11.7364 10.4738 11.5867 10.7099 11.4257 11.018C11.3529 11.1574 11.3547 11.3123 11.4021 11.433C11.4256 11.4929 11.4487 11.5217 11.4616 11.5342C11.5538 11.5407 11.6341 11.5181 11.761 11.4196C11.939 11.2814 12.1382 11.046 12.4375 10.6544C12.689 10.3254 13.1597 10.2625 13.4888 10.5141C13.8179 10.7656 13.8807 11.2363 13.6292 11.5654C13.3446 11.9377 13.0356 12.3291 12.6807 12.6045C12.2888 12.9088 11.8144 13.0935 11.2372 13.0185C10.6039 12.9362 10.1891 12.4478 10.0059 11.9813C9.81381 11.4922 9.80499 10.8809 10.0962 10.3235C10.2868 9.95855 10.4662 9.67597 10.6371 9.40674C10.6442 9.39543 10.6514 9.38415 10.6586 9.37288C10.8353 9.09434 11.0097 8.81692 11.2128 8.431C11.2979 8.26937 11.2903 8.1756 11.2843 8.14675C11.2793 8.12283 11.2706 8.10982 11.2479 8.09598C11.2046 8.06948 10.9012 7.95784 10.358 8.35755C9.63243 8.89143 8.9251 9.5574 8.19796 10.242C7.94283 10.4822 7.68525 10.7247 7.4236 10.9646C6.50037 11.8111 5.37638 11.9237 4.58549 11.3321C3.81484 10.7556 3.60371 9.65256 4.26518 8.74986C5.10213 7.60767 7.03227 5.34468 8.45018 3.72358C8.72147 3.4134 8.78239 3.16926 8.78146 3.02233C8.78056 2.88049 8.72344 2.75822 8.60989 2.65973Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186615\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pen-tool\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186621)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.30847 0.922483L6.7503 3.00002C6.79729 3.03527 6.84204 3.07402 6.88415 3.11612L10.8842 7.11612C10.9263 7.15825 10.9651 7.20303 11.0003 7.25004L13.0779 5.69188C13.5647 5.32675 13.6153 4.61508 13.185 4.18478L9.81557 0.815376C9.38525 0.385067 8.6736 0.435644 8.30847 0.922483ZM1.38128 5.25366L0.166016 12.9504L3.11642 9.99998L2.80834 9.69191C2.56426 9.44783 2.56426 9.0521 2.80834 8.80802C3.05242 8.56394 3.44815 8.56394 3.69222 8.80802L5.19222 10.308C5.4363 10.5521 5.4363 10.9478 5.19222 11.1919C4.94815 11.436 4.55242 11.436 4.30834 11.1919L4.0003 10.8839L1.0499 13.8343L8.74662 12.619C9.19168 12.5487 9.53478 12.1895 9.58454 11.7417L10.0003 8L6.00028 4L2.25861 4.41574C1.81079 4.4655 1.45156 4.8086 1.38128 5.25366Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186621\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pencil\", \"keywords\": [ \"change\", \"edit\", \"modify\", \"pencil\", \"write\", \"writing\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186612)\\\">\\n<path d=\\\"M10.715 -0.000976562C10.5152 -0.000976562 10.3174 0.0389464 10.1332 0.116448C9.95019 0.193468 9.78428 0.306058 9.64512 0.447676L1.40732 8.64549C1.34616 8.70635 1.30179 8.782 1.27853 8.86508L0.0185299 13.3651C-0.0301752 13.539 0.0187291 13.7257 0.146458 13.8535C0.274188 13.9812 0.46088 14.0301 0.634827 13.9814L5.13483 12.7214C5.21791 12.6981 5.29356 12.6537 5.35442 12.5926L13.5521 4.3549L13.5535 4.35355C13.6934 4.21438 13.8045 4.04896 13.8804 3.86676C13.9566 3.68398 13.9958 3.48792 13.9958 3.2899C13.9958 3.09188 13.9566 2.89582 13.8804 2.71303C13.8045 2.53083 13.6934 2.3654 13.5535 2.22624L13.5521 2.2249L11.7859 0.448721C11.6466 0.306611 11.4803 0.193657 11.2968 0.116448C11.1126 0.0389464 10.9148 -0.000976562 10.715 -0.000976562Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186612\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pentagon\", \"keywords\": [ \"pentagon\", \"design\", \"geometric\", \"shape\", \"shapes\", \"shape\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186618)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.96815 0.664858C7.40888 0.19163 6.58959 0.19163 6.03032 0.664857L0.726477 5.15272C0.2336 5.56977 0.0628881 6.25541 0.302675 6.85488L2.78355 13.0571C3.01135 13.6266 3.56291 14 4.17627 14H9.8222C10.4355 14 10.9871 13.6266 11.2149 13.0571L13.6957 6.85488C13.9355 6.25542 13.7648 5.56977 13.2719 5.15272L7.96815 0.664858Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186618\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pi-symbol-circle\", \"keywords\": [ \"interface\", \"math\", \"pi\", \"sign\", \"mathematics\", \"22\", \"7\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186627)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.25 7C0.25 3.27208 3.27208 0.25 7 0.25C10.7279 0.25 13.75 3.27208 13.75 7C13.75 10.7279 10.7279 13.75 7 13.75C3.27208 13.75 0.25 10.7279 0.25 7ZM10.6187 3.91246C10.5704 3.57069 10.2541 3.33281 9.91237 3.38116L9.90929 3.3816L9.90027 3.38287L9.86513 3.38786L9.73124 3.40688C9.61555 3.42334 9.44947 3.44703 9.24776 3.47596C8.84438 3.5338 8.29826 3.61263 7.7276 3.69649C6.59888 3.86236 5.33853 4.05303 4.92662 4.13782C4.00276 4.32797 3.5236 5.10445 3.39558 5.59091C3.30773 5.92472 3.50713 6.26655 3.84094 6.35439C4.17475 6.44224 4.51658 6.24284 4.60442 5.90903C4.62231 5.84107 4.68054 5.71215 4.78872 5.59284C4.89146 5.47952 5.02053 5.39469 5.17862 5.36215C5.22776 5.35204 5.29204 5.33998 5.36928 5.32627C5.35873 5.88249 5.32928 6.5505 5.24758 7.21578C5.12132 8.24398 4.88396 9.13052 4.50647 9.61666C4.29477 9.88929 4.34417 10.2819 4.6168 10.4936C4.88944 10.7053 5.28207 10.6559 5.49377 10.3833C6.09711 9.6063 6.35974 8.41475 6.48827 7.36814C6.5884 6.55273 6.61534 5.7427 6.62252 5.12544C7.01683 5.06529 7.44961 5.00081 7.875 4.93825V9.4736C7.875 9.67945 7.93964 9.95876 8.1369 10.1991C8.35526 10.4651 8.68864 10.6249 9.09999 10.6249C9.89006 10.6249 10.3819 10.0657 10.543 9.78311C10.7139 9.48322 10.6094 9.10155 10.3095 8.93062C10.0096 8.75969 9.62792 8.86422 9.45699 9.1641C9.45662 9.16471 9.45366 9.16962 9.44745 9.17812C9.44061 9.18749 9.43116 9.1994 9.41912 9.21285C9.39462 9.2402 9.36351 9.26929 9.32762 9.2951C9.26366 9.34109 9.19666 9.36876 9.125 9.374V4.75644C9.23168 4.74108 9.33223 4.72663 9.42519 4.7133C9.62641 4.68445 9.79204 4.66082 9.90734 4.64441L10.0407 4.62547L10.0756 4.62052L10.0845 4.61926L10.0867 4.61894C10.0867 4.61894 10.0874 4.61884 9.99991 4L10.0867 4.61894C10.4285 4.57059 10.6671 4.25424 10.6187 3.91246Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186627\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pictures-folder-memories\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186624)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.17586 0.812719C5.9128 0.612798 5.58969 0.508234 5.25954 0.516032H1.73289C1.34633 0.516032 0.975617 0.669589 0.702283 0.942923C0.428949 1.21626 0.275391 1.58698 0.275391 1.97353V12.0273C0.275391 12.4138 0.428949 12.7846 0.702283 13.0579C0.975617 13.3312 1.34633 13.4848 1.73289 13.4848H12.2654C12.6519 13.4848 13.0226 13.3312 13.296 13.0579C13.5693 12.7846 13.7229 12.4138 13.7229 12.0273V3.88853C13.7229 3.50198 13.5693 3.13126 13.296 2.85792C13.0226 2.58459 12.6519 2.43103 12.2654 2.43103H6.91078L6.709 1.62394C6.62909 1.30088 6.44083 1.0141 6.17586 0.812719ZM9.85937 7.56641C9.03094 7.56641 8.35937 6.89483 8.35937 6.06641C8.35937 5.23798 9.03094 4.56641 9.85937 4.56641C10.6878 4.56641 11.3594 5.23798 11.3594 6.06641C11.3594 6.89483 10.6878 7.56641 9.85937 7.56641ZM5.56113 7.21459L11.0272 12.2348H1.73289C1.67786 12.2348 1.62508 12.2129 1.58616 12.174C1.54725 12.1351 1.52539 12.0823 1.52539 12.0273V9.52975L4.80854 7.21459C4.91419 7.1283 5.04734 7.08105 5.18483 7.08105C5.32233 7.08105 5.45548 7.1283 5.56113 7.21459Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186624\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"podium\", \"keywords\": [ \"work\", \"desk\", \"notes\", \"company\", \"presentation\", \"office\", \"podium\", \"microphone\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186630)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 0C7.41422 0 7.75 0.335786 7.75 0.75V3H10.2498H10.6692C10.6382 2.8351 10.5959 2.65948 10.5385 2.48717C10.3212 1.83516 10.0018 1.5 9.5 1.5C9.39782 1.5 9.30041 1.47957 9.21163 1.44256L8.75 0.75C8.75 0.335786 9.08579 0 9.5 0C10.9982 0 11.6789 1.16484 11.9615 2.01283C12.0796 2.36715 12.1485 2.71465 12.1891 3H13C13.1498 3 13.2917 3.06716 13.3867 3.18301C13.4816 3.29885 13.5197 3.45117 13.4903 3.59806L12.4903 8.59806C12.4436 8.83177 12.2383 9 12 9H9.5V12.5H10C10.4142 12.5 10.75 12.8358 10.75 13.25C10.75 13.6642 10.4142 14 10 14H4C3.58579 14 3.25 13.6642 3.25 13.25C3.25 12.8358 3.58579 12.5 4 12.5H4.5V9H2C1.76166 9 1.55646 8.83177 1.50971 8.59806L0.509714 3.59806C0.480336 3.45117 0.518364 3.29885 0.613333 3.18301C0.708303 3.06716 0.850208 3 1 3H1.81096C1.85153 2.71465 1.92038 2.36715 2.03849 2.01283C2.32115 1.16484 3.00181 0 4.5 0C4.91422 0 5.25 0.335786 5.25 0.75C5.25 1.16421 4.91422 1.5 4.5 1.5C3.9982 1.5 3.67885 1.83516 3.46152 2.48717C3.40408 2.65948 3.36178 2.8351 3.3308 3H6.25V0.75C6.25 0.335786 6.58579 0 7 0ZM9.21163 1.44256C8.94055 1.32955 8.75 1.06203 8.75 0.75L9.21163 1.44256Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186630\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"polygon\", \"keywords\": [ \"polygon\", \"octangle\", \"design\", \"geometric\", \"shape\", \"shapes\", \"shape\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186633)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.91421 0C4.51639 0 4.13486 0.158035 3.85355 0.43934L0.43934 3.85355C0.158035 4.13486 0 4.51639 0 4.91421V9.08579C0 9.48361 0.158035 9.86514 0.43934 10.1464L3.85355 13.5607C4.13486 13.842 4.51639 14 4.91421 14H9.08579C9.48361 14 9.86514 13.842 10.1464 13.5607L13.5607 10.1464C13.842 9.86514 14 9.48361 14 9.08579V4.91421C14 4.51639 13.842 4.13486 13.5607 3.85355L10.1464 0.43934C9.86514 0.158035 9.48361 0 9.08579 0H4.91421Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186633\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"praying-hand\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186636)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.75 0.75C7.75 0.335786 7.41421 0 7 0C6.58579 0 6.25 0.335786 6.25 0.75V1.75C6.25 2.16421 6.58579 2.5 7 2.5C7.41421 2.5 7.75 2.16421 7.75 1.75V0.75ZM0.5 14.0004H2.81947C4.89982 14.0004 6.63289 12.4057 6.80565 10.3326L6.91695 8.99694C6.95807 8.50358 6.60332 8.07482 6.12734 8.0091L6.12476 8.00884C5.55028 7.94994 5.03151 8.35441 4.94854 8.92591L4.78803 10.0315C4.74835 10.3048 4.49465 10.4942 4.22138 10.4545C3.9481 10.4148 3.75873 10.1611 3.7984 9.88785L3.95891 8.78224C4.10788 7.75612 4.98552 7.00927 6 7.00312V5.24455C6 4.50219 5.3982 3.90039 4.65584 3.90039C3.99124 3.90039 3.42641 4.38609 3.32685 5.04319L2.62882 9.6502C2.55475 10.1391 2.13454 10.5004 1.6401 10.5004H0.5C0.223858 10.5004 0 10.7242 0 11.0004V13.5004C0 13.7765 0.223858 14.0004 0.5 14.0004ZM13.5 14.0004H11.1806C9.10021 14.0004 7.36714 12.4057 7.19438 10.3326L7.08308 8.99694C7.04181 8.50172 7.3994 8.07158 7.87809 8.00837C8.45144 7.95108 8.96865 8.35517 9.05148 8.92573L9.21199 10.0313C9.25167 10.3046 9.50536 10.494 9.77864 10.4543C10.0519 10.4146 10.2413 10.1609 10.2016 9.88767L10.0411 8.78206C9.89214 7.75594 9.01451 7.0091 8.00003 7.00293V5.24455C8.00003 4.50219 8.60183 3.90039 9.34419 3.90039C10.0088 3.90039 10.5736 4.38609 10.6732 5.04319L11.3712 9.6502C11.4453 10.1391 11.8655 10.5004 12.3599 10.5004H13.5C13.7762 10.5004 14 10.7242 14 11.0004V13.5004C14 13.7765 13.7762 14.0004 13.5 14.0004ZM11.5303 0.71967C11.8232 1.01256 11.8232 1.48744 11.5303 1.78033L10.5303 2.78033C10.2374 3.07322 9.76256 3.07322 9.46967 2.78033C9.17678 2.48744 9.17678 2.01256 9.46967 1.71967L10.4697 0.71967C10.7626 0.426777 11.2374 0.426777 11.5303 0.71967ZM3.53033 0.71967C3.23744 0.426777 2.76256 0.426777 2.46967 0.71967C2.17678 1.01256 2.17678 1.48744 2.46967 1.78033L3.46967 2.78033C3.76256 3.07322 4.23744 3.07322 4.53033 2.78033C4.82322 2.48744 4.82322 2.01256 4.53033 1.71967L3.53033 0.71967Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186636\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"projector-board\", \"keywords\": [ \"projector\", \"screen\", \"work\", \"meeting\", \"presentation\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186639)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1.5C0 0.671573 0.671573 0 1.5 0H12.5C13.3284 0 14 0.671573 14 1.5V2.875H0V1.5ZM0 4.125H14V9.5C14 10.3284 13.3284 11 12.5 11H10.3107L12.0303 12.7197C12.3232 13.0126 12.3232 13.4874 12.0303 13.7803C11.7374 14.0732 11.2626 14.0732 10.9697 13.7803L8.18934 11H7.75V13.25C7.75 13.6642 7.41421 14 7 14C6.58579 14 6.25 13.6642 6.25 13.25V11H5.81066L3.03033 13.7803C2.73744 14.0732 2.26256 14.0732 1.96967 13.7803C1.67678 13.4874 1.67678 13.0126 1.96967 12.7197L3.68934 11H1.5C0.671573 11 0 10.3284 0 9.5V4.125Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186639\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pyramid-shape\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186642)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.3584 0.524781C6.28119 0.586856 6.21142 0.662159 6.15226 0.75069L0.352447 9.42961C0.00932693 9.94305 0.208997 10.6417 0.771616 10.8963L6.3584 13.4244V0.524781ZM7.6084 13.425L13.1941 10.8995C13.7568 10.6451 13.9567 9.94645 13.6137 9.4329L7.81529 0.75092C7.75594 0.662057 7.68591 0.586514 7.6084 0.524292V13.425Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186642\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"quotation-2\", \"keywords\": [ \"quote\", \"quotation\", \"format\", \"formatting\", \"open\", \"close\", \"marks\", \"text\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186645)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.48991 3.87669C1.42182 3.46811 1.0354 3.1921 0.626818 3.2602C0.21824 3.32829 -0.0577747 3.71471 0.0103216 4.12329L0.510321 7.12329C0.578418 7.53187 0.964838 7.80788 1.37342 7.73979C1.782 7.67169 2.05801 7.28527 1.98991 6.87669L1.48991 3.87669ZM3.98991 3.87669C3.92182 3.46811 3.5354 3.1921 3.12682 3.2602C2.71824 3.32829 2.44223 3.71471 2.51032 4.12329L3.01032 7.12329C3.07842 7.53187 3.46484 7.80788 3.87342 7.73979C4.282 7.67169 4.55801 7.28527 4.48991 6.87669L3.98991 3.87669ZM11.1234 6.2602C11.532 6.32829 11.808 6.71471 11.7399 7.12329L11.2399 10.1233C11.1718 10.5319 10.7854 10.8079 10.3768 10.7398C9.96824 10.6717 9.69223 10.2853 9.76032 9.87669L10.2603 6.87669C10.3284 6.46811 10.7148 6.1921 11.1234 6.2602ZM13.9899 7.12329C14.058 6.71471 13.782 6.32829 13.3734 6.2602C12.9648 6.1921 12.5784 6.46811 12.5103 6.87669L12.0103 9.87669C11.9422 10.2853 12.2182 10.6717 12.6268 10.7398C13.0354 10.8079 13.4218 10.5319 13.4899 10.1233L13.9899 7.12329Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186645\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"radioactive-2\", \"keywords\": [ \"warning\", \"radioactive\", \"radiation\", \"emergency\", \"danger\", \"safety\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186648)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.27143 2.06201C5.56402 1.76882 5.56353 1.29395 5.27034 1.00135C4.97715 0.708761 4.50227 0.709248 4.20968 1.00244C3.65915 1.5541 3.28448 2.25657 3.13302 3.02108C2.98156 3.78559 3.06011 4.57784 3.35873 5.29774C3.36718 5.31811 3.37579 5.33839 3.38457 5.35859C3.36308 5.36105 3.3416 5.36369 3.32013 5.36651C2.54736 5.46789 1.82198 5.79606 1.23565 6.30954C0.649319 6.82302 0.228346 7.49878 0.0259236 8.25142C-0.0816558 8.65142 0.155397 9.0629 0.555397 9.17048C0.955396 9.27806 1.36687 9.041 1.47445 8.641C1.59985 8.17473 1.86065 7.75609 2.22389 7.43798C2.42871 7.25861 2.66092 7.11574 2.91033 7.01398C2.89348 7.16525 2.88483 7.31899 2.88483 7.47476C2.88483 9.14271 3.87711 10.5789 5.30355 11.2252C5.09741 11.3807 4.86682 11.5033 4.61975 11.5874C4.16265 11.743 3.6697 11.7595 3.20319 11.635C2.80298 11.5282 2.39197 11.7661 2.28516 12.1663C2.17835 12.5665 2.4162 12.9775 2.81641 13.0843C3.56944 13.2853 4.36515 13.2585 5.10299 13.0074C5.84083 12.7564 6.48768 12.2922 6.96183 11.6736C6.97476 11.6568 6.98754 11.6398 7.00017 11.6227C7.01281 11.6398 7.0256 11.6568 7.03852 11.6736C7.51267 12.2922 8.15952 12.7564 8.89736 13.0074C9.6352 13.2585 10.4309 13.2853 11.184 13.0843C11.5842 12.9775 11.822 12.5665 11.7152 12.1663C11.6084 11.7661 11.1974 11.5282 10.7972 11.635C10.3307 11.7595 9.8377 11.743 9.3806 11.5874C9.13354 11.5033 8.90296 11.3807 8.69683 11.2253C10.1233 10.5789 11.1156 9.14273 11.1156 7.47476C11.1156 7.319 11.107 7.16527 11.0901 7.01401C11.3395 7.11577 11.5717 7.25863 11.7765 7.43798C12.1397 7.75609 12.4005 8.17473 12.5259 8.641C12.6335 9.041 13.045 9.27806 13.445 9.17048C13.845 9.0629 14.082 8.65142 13.9745 8.25142C13.772 7.49878 13.3511 6.82302 12.7647 6.30954C12.1784 5.79606 11.453 5.46789 10.6802 5.36651C10.6588 5.36369 10.6373 5.36105 10.6158 5.35859C10.6246 5.33839 10.6332 5.31811 10.6416 5.29774C10.9403 4.57784 11.0188 3.78559 10.8673 3.02108C10.7159 2.25657 10.3412 1.5541 9.79066 1.00244C9.49807 0.709248 9.02319 0.708761 8.73 1.00135C8.43681 1.29395 8.43632 1.76882 8.72891 2.06201C9.06997 2.40377 9.30208 2.83896 9.39592 3.31258C9.45041 3.58764 9.45684 3.86849 9.41643 4.14299C8.73792 3.65008 7.90301 3.35938 7.00021 3.35938C6.09738 3.35938 5.26244 3.6501 4.58392 4.14304C4.5435 3.86852 4.54993 3.58766 4.60443 3.31258C4.69826 2.83896 4.93037 2.40377 5.27143 2.06201ZM8.53538 5.68669C8.60314 5.63192 8.66778 5.57368 8.72908 5.51225C8.26816 5.1059 7.66298 4.85938 7.00021 4.85938C6.33743 4.85938 5.73222 5.10592 5.2713 5.51229C5.33258 5.5737 5.39721 5.63193 5.46495 5.68668C5.50264 5.70382 5.5401 5.72157 5.57732 5.73993C5.76877 5.8344 5.95139 5.94383 6.12364 6.06681C6.40195 6.17382 6.69895 6.22952 7.00017 6.22952C7.3014 6.22952 7.59841 6.17382 7.87672 6.0668C8.04897 5.94382 8.23158 5.8344 8.42303 5.73993C8.46024 5.72157 8.4977 5.70383 8.53538 5.68669ZM6.24424 8.88732C6.19725 8.59536 6.09739 8.31282 5.948 8.05409C5.79725 7.793 5.60028 7.56346 5.36817 7.37586C5.17513 7.28807 4.98867 7.18448 4.81078 7.06568C4.77661 7.04287 4.74284 7.01954 4.70948 6.99572C4.62305 6.96249 4.53498 6.93427 4.44571 6.91113C4.40584 7.09266 4.38483 7.28126 4.38483 7.47476C4.38483 8.63189 5.13629 9.61351 6.17783 9.95823C6.20597 9.86159 6.22818 9.76309 6.24424 9.66329C6.23992 9.61967 6.23633 9.57592 6.23347 9.53207C6.21938 9.31627 6.22314 9.10066 6.24424 8.88732ZM9.29084 6.99574C9.3773 6.96249 9.4654 6.93426 9.55471 6.91112C9.59458 7.09265 9.6156 7.28125 9.6156 7.47476C9.6156 8.63192 8.8641 9.61356 7.82253 9.95825C7.79438 9.86161 7.77217 9.76311 7.75611 9.66331C7.76043 9.61968 7.76402 9.57593 7.76688 9.53207C7.78097 9.31627 7.77721 9.10066 7.75611 8.88732C7.8031 8.59536 7.90296 8.31282 8.05235 8.05409C8.2031 7.79299 8.40008 7.56345 8.63219 7.37585C8.82522 7.28807 9.01167 7.18448 9.18956 7.06568C9.22372 7.04287 9.25748 7.01955 9.29084 6.99574Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186648\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"rain-cloud\", \"keywords\": [ \"cloud\", \"rain\", \"rainy\", \"meteorology\", \"precipitation\", \"weather\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186651)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.68245 0.20795C4.44979 -0.0413341 5.27223 -0.0670495 6.05364 0.13381C6.83506 0.33467 7.54315 0.753805 8.09516 1.34222C8.48612 1.75898 8.78735 2.24845 8.98334 2.78019C9.37736 2.59819 9.80841 2.50147 10.248 2.5H10.2497C11.0453 2.5 11.8084 2.81607 12.371 3.37868C12.9336 3.94129 13.2497 4.70435 13.2497 5.5C13.2497 6.29565 12.9336 7.05871 12.371 7.62132C11.8084 8.18393 11.0453 8.5 10.2497 8.5H5.00022C4.19359 8.50077 3.40286 8.27199 2.72142 7.84036C2.03983 7.40863 1.49521 6.79182 1.15123 6.06201C0.807236 5.3322 0.67809 4.51955 0.778884 3.71906C0.879677 2.91856 1.20625 2.1633 1.72042 1.54154C2.23459 0.919788 2.91511 0.457234 3.68245 0.20795ZM5.22055 10.5854C5.40579 10.2149 5.25562 9.76442 4.88514 9.57918C4.51465 9.39394 4.06415 9.54411 3.8789 9.91459L3.3789 10.9146C3.19366 11.2851 3.34383 11.7356 3.71432 11.9208C4.0848 12.1061 4.5353 11.9559 4.72055 11.5854L5.22055 10.5854ZM9.78504 9.57918C10.1555 9.76442 10.3057 10.2149 10.1205 10.5854L9.62045 11.5854C9.43521 11.9559 8.9847 12.1061 8.61422 11.9208C8.24373 11.7356 8.09356 11.2851 8.27881 10.9146L8.77881 9.91459C8.96405 9.54411 9.41455 9.39394 9.78504 9.57918ZM7.1705 12.5854C7.35574 12.2149 7.20557 11.7644 6.83509 11.5792C6.4646 11.3939 6.0141 11.5441 5.82886 11.9146L5.32886 12.9146C5.14361 13.2851 5.29378 13.7356 5.66427 13.9208C6.03475 14.1061 6.48526 13.9559 6.6705 13.5854L7.1705 12.5854ZM1.93518 11.5792C2.30567 11.7644 2.45584 12.2149 2.27059 12.5854L1.77059 13.5854C1.58535 13.9559 1.13485 14.1061 0.764363 13.9208C0.393879 13.7356 0.243711 13.2851 0.428953 12.9146L0.928953 11.9146C1.1142 11.5441 1.5647 11.3939 1.93518 11.5792ZM12.0704 12.5854C12.2557 12.2149 12.1055 11.7644 11.735 11.5792C11.3645 11.3939 10.914 11.5441 10.7288 11.9146L10.2288 12.9146C10.0435 13.2851 10.1937 13.7356 10.5642 13.9208C10.9347 14.1061 11.3852 13.9559 11.5704 13.5854L12.0704 12.5854Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186651\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"recycle-bin-2\", \"keywords\": [ \"remove\", \"delete\", \"empty\", \"bin\", \"trash\", \"garbage\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186654)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.76256 2.01256C6.09075 1.68437 6.53587 1.5 7 1.5C7.46413 1.5 7.90925 1.68437 8.23744 2.01256C8.4448 2.21993 8.59475 2.47397 8.67705 2.75H5.32295C5.40525 2.47397 5.5552 2.21993 5.76256 2.01256ZM3.78868 2.75C3.89405 2.07321 4.21153 1.44227 4.7019 0.951903C5.3114 0.34241 6.13805 0 7 0C7.86195 0 8.6886 0.34241 9.2981 0.951903C9.78847 1.44227 10.106 2.07321 10.2113 2.75H13C13.4142 2.75 13.75 3.08579 13.75 3.5C13.75 3.91422 13.4142 4.25 13 4.25H12V12.5C12 12.8978 11.842 13.2794 11.5607 13.5607C11.2794 13.842 10.8978 14 10.5 14H3.5C3.10217 14 2.72064 13.842 2.43934 13.5607C2.15804 13.2794 2 12.8978 2 12.5V4.25H1C0.585786 4.25 0.25 3.91422 0.25 3.5C0.25 3.08579 0.585786 2.75 1 2.75H3.78868ZM5 5.87646C5.34518 5.87646 5.625 6.15629 5.625 6.50146V10.503C5.625 10.8481 5.34518 11.128 5 11.128C4.65482 11.128 4.375 10.8481 4.375 10.503V6.50146C4.375 6.15629 4.65482 5.87646 5 5.87646ZM9.625 6.50146C9.625 6.15629 9.34518 5.87646 9 5.87646C8.65482 5.87646 8.375 6.15629 8.375 6.50146V10.503C8.375 10.8481 8.65482 11.128 9 11.128C9.34518 11.128 9.625 10.8481 9.625 10.503V6.50146Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186654\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ringing-bell-notification\", \"keywords\": [ \"notification\", \"vibrate\", \"ring\", \"sound\", \"alarm\", \"alert\", \"bell\", \"noise\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186657)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.76302 0.312557C4.00465 0.648995 3.92778 1.11761 3.59135 1.35923C2.94459 1.82372 2.41763 2.43542 2.05399 3.1438C1.69034 3.85218 1.50045 4.63691 1.5 5.43318C1.49977 5.84739 1.16379 6.18299 0.749575 6.18275C0.335362 6.18252 -0.000234302 5.84654 1.2274e-07 5.43233C0.000585917 4.3981 0.247225 3.37885 0.719545 2.45877C1.19186 1.53869 1.87631 0.744183 2.71635 0.140882C3.05278 -0.100742 3.5214 -0.0238812 3.76302 0.312557ZM3.81802 2.56808C4.66193 1.72417 5.80653 1.25006 7 1.25006C8.19347 1.25006 9.33807 1.72417 10.182 2.56808C11.0259 3.41199 11.5 4.55659 11.5 5.75006V9.25006C11.5 9.51528 11.6054 9.76963 11.7929 9.95717C11.9804 10.1447 12.2348 10.2501 12.5 10.2501C12.7761 10.2501 13 10.474 13 10.7501C13 11.0262 12.7761 11.2501 12.5 11.2501H1.5C1.22386 11.2501 1 11.0262 1 10.7501C1 10.474 1.22386 10.2501 1.5 10.2501C1.76522 10.2501 2.01957 10.1447 2.20711 9.95717C2.39464 9.76963 2.5 9.51528 2.5 9.25006V5.75006C2.5 4.55659 2.97411 3.41199 3.81802 2.56808ZM5.25 13.2501C5.25 12.8359 5.58579 12.5001 6 12.5001H8C8.41421 12.5001 8.75 12.8359 8.75 13.2501C8.75 13.6643 8.41421 14.0001 8 14.0001H6C5.58579 14.0001 5.25 13.6643 5.25 13.2501ZM11.2837 0.140882C10.9472 -0.100742 10.4786 -0.0238812 10.237 0.312557C9.99536 0.648995 10.0722 1.11761 10.4087 1.35923C11.0554 1.82372 11.5824 2.43542 11.946 3.1438C12.3097 3.85218 12.4996 4.63691 12.5 5.43318C12.5002 5.84739 12.8362 6.18299 13.2504 6.18275C13.6646 6.18252 14.0002 5.84654 14 5.43233C13.9994 4.3981 13.7528 3.37885 13.2805 2.45877C12.8081 1.53869 12.1237 0.744183 11.2837 0.140882Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186657\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"rock-and-roll-hand\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.69452 9.75484C1.50992 11.2375 1.65467 13.0607 3.5671 13.9248C3.68271 13.9771 3.80925 14 3.93611 14H10.3299C10.5951 14 10.8495 13.8946 11.037 13.7071L11.1273 13.6168C12.0025 12.7417 12.4941 11.5547 12.4941 10.317V3.79167C12.4941 2.98625 11.8412 2.33333 11.0358 2.33333C10.2304 2.33333 9.57741 2.98625 9.57741 3.79167V6.04167C7.83717 5.26024 7.4076 5.1825 5.49408 5.45833V1.45833C5.49408 0.652918 4.84116 0 4.03575 0C3.23033 0 2.57741 0.652918 2.57741 1.45833V7.2124H6.14964C7.1223 7.2124 7.9079 8.00636 7.89761 8.97897C7.89068 9.63415 7.52008 10.2176 6.95803 10.5106C7.21453 10.846 7.3889 11.2828 7.4092 11.8135C7.42239 12.1584 7.15347 12.4488 6.80854 12.4619C6.46362 12.4751 6.17331 12.2062 6.16011 11.8613C6.14575 11.4858 5.98905 11.2568 5.81986 11.1214C5.62514 10.9655 5.4351 10.9461 5.3857 10.9542C5.05527 11.0083 4.74088 10.7924 4.67274 10.4646C4.6046 10.1368 4.80691 9.81347 5.13155 9.73145L6.27165 9.44337C6.49088 9.38798 6.64529 9.19185 6.64768 8.96574C6.65062 8.68862 6.42678 8.4624 6.14964 8.4624H2.55218C2.09582 8.74068 1.76393 9.1974 1.69452 9.75484Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"rotate-angle-45\", \"keywords\": [ \"rotate\", \"angle\", \"company\", \"office\", \"supplies\", \"work\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186663)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.7803 4.28033C11.0732 3.98744 11.0732 3.51256 10.7803 3.21967C10.4874 2.92678 10.0126 2.92678 9.7197 3.21967L5.18128 7.75809C5.17737 7.76183 5.17349 7.76563 5.16965 7.76947C5.1658 7.77332 5.162 7.7772 5.15826 7.7811L0.219692 12.7197C0.00519332 12.9342 -0.0589735 13.2568 0.0571122 13.537C0.173198 13.8173 0.446675 14 0.750022 14H13.25C13.6642 14 14 13.6642 14 13.25C14 12.8358 13.6642 12.5 13.25 12.5H8.46391C8.31509 10.968 7.71301 9.51424 6.73504 8.32564L10.7803 4.28033ZM5.66817 9.39252L2.56069 12.5H6.95514C6.81821 11.3662 6.37294 10.2911 5.66817 9.39252Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186663\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"round-cap\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 7C0 9.76142 2.23858 12 5 12H14V7.75H6.85462C6.55793 8.48296 5.83934 9 5 9C3.89543 9 3 8.10457 3 7C3 5.89543 3.89543 5 5 5C5.83934 5 6.55793 5.51704 6.85462 6.25H14V2H5C2.23858 2 0 4.23858 0 7Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"satellite-dish\", \"keywords\": [ \"broadcast\", \"satellite\", \"share\", \"transmit\", \"satellite\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186669)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.35355 0.146447C5.25979 0.0526784 5.13261 0 5 0C4.86739 0 4.74022 0.0526784 4.64645 0.146447C3.42551 1.36738 2.73959 3.02333 2.73959 4.75C2.73959 6.47667 3.42551 8.13262 4.64645 9.35355C5.86739 10.5745 7.52333 11.2604 9.25 11.2604C10.9767 11.2604 12.6326 10.5745 13.8536 9.35355C14.0488 9.15829 14.0488 8.84171 13.8536 8.64645L10.3107 5.10355L11.7071 3.70711C12.0976 3.31658 12.0976 2.68342 11.7071 2.29289C11.3166 1.90237 10.6834 1.90237 10.2929 2.29289L8.89645 3.68934L5.35355 0.146447ZM2.7632 9.00977C3.05146 9.44873 3.38562 9.86046 3.76256 10.2374C4.66096 11.1358 5.75694 11.7912 6.94837 12.1612L7.3069 13.3563C7.40314 13.6771 7.16292 14 6.82798 14H0.389699C0.174474 14 0 13.8255 0 13.6103C0 13.5382 0.0200118 13.4675 0.0578084 13.406L2.7632 9.00977Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186669\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"scanner\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186672)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.75 0.75C9.75 0.335786 10.0858 0 10.5 0H12.25C13.2165 0 14 0.783502 14 1.75V3.5C14 3.91421 13.6642 4.25 13.25 4.25C12.8358 4.25 12.5 3.91421 12.5 3.5V1.75C12.5 1.61193 12.3881 1.5 12.25 1.5H10.5C10.0858 1.5 9.75 1.16421 9.75 0.75ZM0 7C0 6.58579 0.335786 6.25 0.75 6.25H13.25C13.6642 6.25 14 6.58579 14 7C14 7.41421 13.6642 7.75 13.25 7.75H0.75C0.335786 7.75 0 7.41421 0 7ZM1.5 1.75C1.5 1.61193 1.61193 1.5 1.75 1.5H3.5C3.91421 1.5 4.25 1.16421 4.25 0.75C4.25 0.335786 3.91421 0 3.5 0H1.75C0.783502 0 0 0.783502 0 1.75V3.5C0 3.91421 0.335786 4.25 0.75 4.25C1.16421 4.25 1.5 3.91421 1.5 3.5V1.75ZM13.25 9.75C13.6642 9.75 14 10.0858 14 10.5V12.25C14 13.2165 13.2165 14 12.25 14H10.5C10.0858 14 9.75 13.6642 9.75 13.25C9.75 12.8358 10.0858 12.5 10.5 12.5H12.25C12.3881 12.5 12.5 12.3881 12.5 12.25V10.5C12.5 10.0858 12.8358 9.75 13.25 9.75ZM1.50012 10.5C1.50012 10.0858 1.16434 9.75 0.750122 9.75C0.335909 9.75 0.00012207 10.0858 0.00012207 10.5V12.2499C0.00012207 13.2164 0.783624 13.9999 1.75012 13.9999H3.5C3.91421 13.9999 4.25 13.6641 4.25 13.2499C4.25 12.8357 3.91421 12.4999 3.5 12.4999H1.75012C1.61205 12.4999 1.50012 12.388 1.50012 12.2499V10.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186672\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"search-visual\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186675)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.75 1.5C1.6837 1.5 1.62011 1.52634 1.57322 1.57322C1.52634 1.62011 1.5 1.6837 1.5 1.75V3.25C1.5 3.66421 1.16421 4 0.75 4C0.335786 4 0 3.66421 0 3.25V1.75C0 1.28587 0.184375 0.840752 0.512563 0.512563C0.840752 0.184375 1.28587 0 1.75 0H3.25C3.66421 0 4 0.335786 4 0.75C4 1.16421 3.66421 1.5 3.25 1.5H1.75ZM13.25 10C13.6642 10 14 10.3358 14 10.75V12.25C14 12.7141 13.8156 13.1592 13.4874 13.4874C13.1592 13.8156 12.7141 14 12.25 14H10.75C10.3358 14 10 13.6642 10 13.25C10 12.8358 10.3358 12.5 10.75 12.5H12.25C12.3163 12.5 12.3799 12.4737 12.4268 12.4268C12.4737 12.3799 12.5 12.3163 12.5 12.25V10.75C12.5 10.3358 12.8358 10 13.25 10ZM1.5 10.75C1.5 10.3358 1.16421 10 0.75 10C0.335786 10 0 10.3358 0 10.75V12.25C0 12.7141 0.184375 13.1592 0.512563 13.4874C0.840752 13.8156 1.28587 14 1.75 14H3.25C3.66421 14 4 13.6642 4 13.25C4 12.8358 3.66421 12.5 3.25 12.5H1.75C1.6837 12.5 1.62011 12.4737 1.57322 12.4268C1.52634 12.3799 1.5 12.3163 1.5 12.25V10.75ZM10 0.75C10 0.335786 10.3358 0 10.75 0H12.25C12.7141 0 13.1592 0.184375 13.4874 0.512563C13.8156 0.840752 14 1.28587 14 1.75V3.25C14 3.66421 13.6642 4 13.25 4C12.8358 4 12.5 3.66421 12.5 3.25V1.75C12.5 1.6837 12.4737 1.62011 12.4268 1.57322C12.3799 1.52634 12.3163 1.5 12.25 1.5H10.75C10.3358 1.5 10 1.16421 10 0.75ZM6.25 4C5.00736 4 4 5.00736 4 6.25C4 7.49264 5.00736 8.5 6.25 8.5C7.49264 8.5 8.5 7.49264 8.5 6.25C8.5 5.00736 7.49264 4 6.25 4ZM2.5 6.25C2.5 4.17893 4.17893 2.5 6.25 2.5C8.32107 2.5 10 4.17893 10 6.25C10 7.01431 9.77134 7.72522 9.3787 8.31804L11.0303 9.96967C11.3232 10.2626 11.3232 10.7374 11.0303 11.0303C10.7374 11.3232 10.2626 11.3232 9.96967 11.0303L8.31804 9.3787C7.72522 9.77134 7.01431 10 6.25 10C4.17893 10 2.5 8.32107 2.5 6.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186675\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"select-circle-area-1\", \"keywords\": [ \"select\", \"area\", \"object\", \"work\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186678)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.73844 0.113512C6.14819 0.0388823 6.56988 0 7 0C7.43012 0 7.85181 0.0388823 8.26156 0.113512C8.66907 0.187733 8.93925 0.578253 8.86503 0.985763C8.79081 1.39327 8.40029 1.66346 7.99278 1.58923C7.6713 1.53068 7.33956 1.5 7 1.5C6.66044 1.5 6.3287 1.53068 6.00722 1.58923C5.59971 1.66346 5.20919 1.39327 5.13497 0.985763C5.06075 0.578253 5.33093 0.187733 5.73844 0.113512ZM4.0662 1.42925C4.3018 1.76994 4.21661 2.23711 3.87592 2.4727C3.32782 2.85174 2.85174 3.32782 2.4727 3.87592C2.23711 4.21661 1.76994 4.3018 1.42925 4.0662C1.08857 3.8306 1.00338 3.36343 1.23897 3.02274C1.72088 2.32589 2.32589 1.72088 3.02275 1.23897C3.36343 1.00338 3.8306 1.08857 4.0662 1.42925ZM9.9338 1.42925C10.1694 1.08857 10.6366 1.00338 10.9773 1.23897C11.6741 1.72088 12.2791 2.32589 12.761 3.02275C12.9966 3.36343 12.9114 3.8306 12.5707 4.0662C12.2301 4.3018 11.7629 4.21661 11.5273 3.87592C11.1483 3.32782 10.6722 2.85174 10.1241 2.4727C9.78339 2.23711 9.6982 1.76994 9.9338 1.42925ZM0.985763 5.13497C1.39327 5.20919 1.66346 5.59971 1.58923 6.00722C1.53068 6.3287 1.5 6.66044 1.5 7C1.5 7.33956 1.53068 7.6713 1.58923 7.99278C1.66346 8.40029 1.39327 8.79081 0.985763 8.86503C0.578253 8.93925 0.187733 8.66907 0.113512 8.26156C0.0388823 7.85181 0 7.43012 0 7C0 6.56988 0.0388823 6.14819 0.113512 5.73844C0.187733 5.33093 0.578253 5.06074 0.985763 5.13497ZM13.0142 5.13497C13.4217 5.06075 13.8123 5.33093 13.8865 5.73844C13.9611 6.14819 14 6.56988 14 7C14 7.43012 13.9611 7.85181 13.8865 8.26156C13.8123 8.66907 13.4217 8.93925 13.0142 8.86503C12.6067 8.79081 12.3365 8.40029 12.4108 7.99278C12.4693 7.6713 12.5 7.33956 12.5 7C12.5 6.66044 12.4693 6.3287 12.4108 6.00722C12.3365 5.59971 12.6067 5.20919 13.0142 5.13497ZM1.42925 9.9338C1.76994 9.6982 2.23711 9.78339 2.4727 10.1241C2.85174 10.6722 3.32782 11.1483 3.87592 11.5273C4.21661 11.7629 4.3018 12.2301 4.0662 12.5707C3.8306 12.9114 3.36343 12.9966 3.02274 12.761C2.32589 12.2791 1.72088 11.6741 1.23897 10.9773C1.00338 10.6366 1.08857 10.1694 1.42925 9.9338ZM12.5707 9.9338C12.9114 10.1694 12.9966 10.6366 12.761 10.9773C12.2791 11.6741 11.6741 12.2791 10.9773 12.761C10.6366 12.9966 10.1694 12.9114 9.9338 12.5707C9.6982 12.2301 9.78339 11.7629 10.1241 11.5273C10.6722 11.1483 11.1483 10.6722 11.5273 10.1241C11.7629 9.78339 12.2301 9.6982 12.5707 9.9338ZM5.13497 13.0142C5.20919 12.6067 5.59971 12.3365 6.00722 12.4108C6.3287 12.4693 6.66044 12.5 7 12.5C7.33956 12.5 7.6713 12.4693 7.99278 12.4108C8.40029 12.3365 8.79081 12.6067 8.86503 13.0142C8.93925 13.4217 8.66907 13.8123 8.26156 13.8865C7.85181 13.9611 7.43012 14 7 14C6.56988 14 6.14819 13.9611 5.73844 13.8865C5.33093 13.8123 5.06074 13.4217 5.13497 13.0142Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186678\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"share-link\", \"keywords\": [ \"share\", \"transmit\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186681)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.25 5.5C12.7688 5.5 14 4.26878 14 2.75C14 1.23122 12.7688 0 11.25 0C9.73122 0 8.5 1.23122 8.5 2.75C8.5 2.79373 8.50102 2.83722 8.50304 2.88045L4.50255 4.88069C4.02671 4.48675 3.416 4.25 2.75 4.25C1.23122 4.25 0 5.48122 0 7C0 8.51878 1.23122 9.75 2.75 9.75C3.416 9.75 4.02671 9.51325 4.50255 9.11931L8.50304 11.1196C8.50102 11.1628 8.5 11.2063 8.5 11.25C8.5 12.7688 9.73122 14 11.25 14C12.7688 14 14 12.7688 14 11.25C14 9.73122 12.7688 8.5 11.25 8.5C10.4977 8.5 9.81588 8.80212 9.31936 9.29165L5.47537 7.36965C5.49161 7.24875 5.5 7.12535 5.5 7C5.5 6.87465 5.49161 6.75125 5.47537 6.63035L9.31936 4.70835C9.81588 5.19788 10.4977 5.5 11.25 5.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186681\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shield-1\", \"keywords\": [ \"shield\", \"protection\", \"security\", \"defend\", \"crime\", \"war\", \"cover\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186684)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 4.92744C14 9.05364 11.3089 12.6973 7.36513 13.9108C7.12721 13.984 6.87279 13.984 6.63487 13.9108C2.69114 12.6973 0 9.05364 0 4.92744V1.5C0 0.671573 0.671573 0 1.5 0H12.5C13.3284 0 14 0.671572 14 1.5V4.92744Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186684\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shield-2\", \"keywords\": [ \"shield\", \"protection\", \"security\", \"defend\", \"crime\", \"war\", \"cover\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186687)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.41215 0.216928C7.32078 0.0838935 7.17082 0.00314213 7.00946 8.96825e-05C6.8481 -0.00296276 6.69518 0.0720591 6.59885 0.201543C5.9697 1.04718 5.12412 1.70747 4.15119 2.11285C3.17826 2.51824 2.11401 2.65371 1.07055 2.505C0.927079 2.48456 0.781798 2.52736 0.672336 2.62233C0.562874 2.7173 0.5 2.85508 0.5 3V5.78V5.78113C0.503853 7.48661 1.02066 9.1515 1.98325 10.5594C2.94583 11.9673 4.30968 13.053 5.8975 13.6755L6.4884 13.9059L6.49447 13.9082C6.82041 14.0304 7.17959 14.0304 7.50553 13.9082L7.5116 13.9059L8.1016 13.6759C9.68942 13.0534 11.0542 11.9673 12.0168 10.5594C12.9793 9.1515 13.4961 7.48661 13.5 5.78113V5.78V3C13.5 2.86157 13.4426 2.72934 13.3415 2.63479C13.2404 2.54025 13.1046 2.49185 12.9665 2.50112C10.4174 2.67233 8.53886 1.85741 7.41215 0.216928Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186687\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shield-check\", \"keywords\": [ \"shield\", \"protection\", \"security\", \"defend\", \"crime\", \"war\", \"cover\", \"check\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186690)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1.49925C0 0.67848 0.67943 -0.000244141 1.5 -0.000244141H12.5C13.3206 -0.000244141 14 0.678481 14 1.49925V4.35334C14 8.48875 11.4901 12.3594 7.58873 13.8809C7.41772 13.9476 7.21478 14.0013 6.99302 14.0002C6.77602 13.9992 6.57792 13.9459 6.41127 13.8809C2.50995 12.3594 0 8.48875 0 4.35334V1.49925ZM10.5606 4.50828C10.8358 4.1987 10.8079 3.72464 10.4983 3.44945C10.1887 3.17426 9.71469 3.20215 9.4395 3.51174L5.89798 7.49595L4.45006 6.41001C4.11869 6.16148 3.64859 6.22864 3.40006 6.56001C3.15153 6.89138 3.21869 7.36148 3.55006 7.61001L5.55006 9.11001C5.86211 9.34405 6.30147 9.29982 6.56062 9.00828L10.5606 4.50828Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186690\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shield-cross\", \"keywords\": [ \"shield\", \"secure\", \"security\", \"cross\", \"add\", \"plus\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186693)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 4.92744C14 9.05364 11.3089 12.6973 7.36513 13.9108C7.12721 13.984 6.87279 13.984 6.63487 13.9108C2.69114 12.6973 0 9.05364 0 4.92744V1.5C0 0.671573 0.671573 0 1.5 0H12.5C13.3284 0 14 0.671572 14 1.5V4.92744ZM5.60041 2.9082C5.60041 2.63206 5.82426 2.4082 6.10041 2.4082H7.89932C8.17546 2.4082 8.39932 2.63206 8.39932 2.9082V4.60712H10.0982C10.3744 4.60712 10.5982 4.83098 10.5982 5.10712V6.90604C10.5982 7.18218 10.3744 7.40604 10.0982 7.40604H8.39932V9.10495C8.39932 9.38109 8.17546 9.60495 7.89932 9.60495H6.10041C5.82426 9.60495 5.60041 9.38109 5.60041 9.10495V7.40604H3.90149C3.62535 7.40604 3.40149 7.18218 3.40149 6.90604V5.10712C3.40149 4.83098 3.62535 4.60712 3.90149 4.60712H5.60041V2.9082Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186693\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shrink-horizontal-1\", \"keywords\": [ \"resize\", \"shrink\", \"bigger\", \"horizontal\", \"smaller\", \"size\", \"arrow\", \"arrows\", \"big\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186696)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0.75C1.5 0.335786 1.16421 0 0.75 0C0.335786 0 0 0.335786 0 0.75V13.25C0 13.6642 0.335786 14 0.75 14C1.16421 14 1.5 13.6642 1.5 13.25V0.75ZM14 0.75C14 0.335786 13.6642 0 13.25 0C12.8358 0 12.5 0.335786 12.5 0.75V13.25C12.5 13.6642 12.8358 14 13.25 14C13.6642 14 14 13.6642 14 13.25V0.75ZM11.1913 4.03806C11.3782 4.11545 11.5 4.29777 11.5 4.5V9.5C11.5 9.70223 11.3782 9.88455 11.1913 9.96194C11.0045 10.0393 10.7894 9.99655 10.6464 9.85355L8.14645 7.35355C7.95118 7.15829 7.95118 6.84171 8.14645 6.64645L10.6464 4.14645C10.7894 4.00345 11.0045 3.96067 11.1913 4.03806ZM2.80866 4.03806C2.9955 3.96067 3.21055 4.00345 3.35355 4.14645L5.85355 6.64645C6.04882 6.84171 6.04882 7.15829 5.85355 7.35355L3.35355 9.85355C3.21055 9.99655 2.9955 10.0393 2.80866 9.96194C2.62182 9.88455 2.5 9.70223 2.5 9.5V4.5C2.5 4.29777 2.62182 4.11545 2.80866 4.03806Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186696\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shuffle\", \"keywords\": [ \"multimedia\", \"shuffle\", \"multi\", \"button\", \"controls\", \"media\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.8536 1.64646C11.7106 1.50346 11.4955 1.46068 11.3087 1.53807C11.1218 1.61546 11 1.79778 11 2.00001V3.00001H8.5C7.94772 3.00001 7.5 3.44772 7.5 4.00001C7.5 4.55229 7.94772 5.00001 8.5 5.00001H11V6.00001C11 6.20224 11.1218 6.38456 11.3087 6.46195C11.4955 6.53934 11.7106 6.49656 11.8536 6.35356L13.8536 4.35356C14.0488 4.1583 14.0488 3.84172 13.8536 3.64646L11.8536 1.64646ZM0 4.00001C0 3.44772 0.447715 3.00001 1 3.00001H4.25C4.58435 3.00001 4.89658 3.16711 5.08205 3.44531L8.78518 9.00001H11V8.00001C11 7.79778 11.1218 7.61546 11.3087 7.53807C11.4955 7.46068 11.7106 7.50346 11.8536 7.64646L13.8536 9.64646C14.0488 9.84172 14.0488 10.1583 13.8536 10.3536L11.8536 12.3536C11.7106 12.4966 11.4955 12.5393 11.3087 12.4619C11.1218 12.3845 11 12.2022 11 12V11H8.25C7.91565 11 7.60342 10.8329 7.41795 10.5547L3.71482 5.00001H1C0.447715 5.00001 0 4.55229 0 4.00001ZM0 10C0 9.44772 0.447715 9.00001 1 9.00001H4C4.55228 9.00001 5 9.44772 5 10C5 10.5523 4.55228 11 4 11H1C0.447715 11 0 10.5523 0 10Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"sigma\", \"keywords\": [ \"formula\", \"text\", \"format\", \"sigma\", \"formatting\", \"sum\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186702)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.418958 2.2676C-0.365571 1.48031 0.184646 0.12561 1.30624 0.12561H13.2063C13.6175 0.12561 13.9508 0.458913 13.9508 0.870063C13.9508 1.28121 13.6175 1.61451 13.2063 1.61451H1.87011L6.71319 6.4746C7.00268 6.76511 7.00268 7.23505 6.71319 7.52557L1.87011 12.3856H13.2063C13.6175 12.3856 13.9508 12.7189 13.9508 13.1301C13.9508 13.5412 13.6175 13.8745 13.2063 13.8745H1.30624C0.184647 13.8745 -0.365571 12.5198 0.418958 11.7325L5.13488 7.00008L0.418958 2.2676Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186702\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"skull-1\", \"keywords\": [ \"crash\", \"death\", \"delete\", \"die\", \"error\", \"garbage\", \"remove\", \"skull\", \"trash\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186705)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.2501 10.2351C12.3315 9.14996 13 7.65305 13 6C13 2.68629 10.3137 0 7 0C3.68629 0 1 2.68629 1 6C1 7.65305 1.66849 9.14996 2.74988 10.2351V13.0001C2.74988 13.5524 3.19759 14.0001 3.74988 14.0001C4.30216 14.0001 4.74988 13.5524 4.74988 13.0001V12.084H6V13.0001C6 13.5524 6.44772 14.0001 7 14.0001C7.55228 14.0001 8 13.5524 8 13.0001V12.084H9.25012V13.0001C9.25012 13.5524 9.69784 14.0001 10.2501 14.0001C10.8024 14.0001 11.2501 13.5524 11.2501 13.0001V10.2351ZM4.74988 8.58398C5.44023 8.58398 5.99988 8.02434 5.99988 7.33398C5.99988 6.64363 5.44023 6.08398 4.74988 6.08398C4.05952 6.08398 3.49988 6.64363 3.49988 7.33398C3.49988 8.02434 4.05952 8.58398 4.74988 8.58398ZM10.5001 7.33398C10.5001 8.02434 9.94048 8.58398 9.25012 8.58398C8.55977 8.58398 8.00012 8.02434 8.00012 7.33398C8.00012 6.64363 8.55977 6.08398 9.25012 6.08398C9.94048 6.08398 10.5001 6.64363 10.5001 7.33398Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186705\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"sleep\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.9742 0.595072C10.8454 0.345996 10.5884 0.189575 10.308 0.189575H7.82525C7.41104 0.189575 7.07525 0.525362 7.07525 0.939575C7.07525 1.35379 7.41104 1.68958 7.82525 1.68958H8.85765L7.05806 4.23017C6.89598 4.45899 6.87508 4.75911 7.00389 5.00819C7.13269 5.25727 7.38967 5.41369 7.67008 5.41369H10.4631C10.8773 5.41369 11.2131 5.0779 11.2131 4.66369C11.2131 4.24947 10.8773 3.91369 10.4631 3.91369H9.12042L10.92 1.37309C11.0821 1.14427 11.103 0.844147 10.9742 0.595072ZM5.9057 4.05808C5.7769 3.80901 5.51991 3.65259 5.2395 3.65259H2.58762C2.17341 3.65259 1.83762 3.98837 1.83762 4.40259C1.83762 4.8168 2.17341 5.15259 2.58762 5.15259H3.78916L1.80986 7.94689C1.64778 8.17571 1.62688 8.47583 1.75568 8.72491C1.88449 8.97398 2.14147 9.1304 2.42188 9.1304H5.40524C5.81946 9.1304 6.15524 8.79462 6.15524 8.3804C6.15524 7.96619 5.81946 7.6304 5.40524 7.6304H3.87222L5.85152 4.8361C6.0136 4.60728 6.0345 4.30716 5.9057 4.05808ZM12.0144 6.9624C12.2948 6.9624 12.5517 7.11882 12.6805 7.3679C12.8094 7.61697 12.7885 7.9171 12.6264 8.14592L9.62681 12.3806H12.2401C12.6543 12.3806 12.9901 12.7164 12.9901 13.1306C12.9901 13.5448 12.6543 13.8806 12.2401 13.8806H8.17647C7.89606 13.8806 7.63908 13.7242 7.51027 13.4751C7.38147 13.2261 7.40237 12.9259 7.56445 12.6971L10.564 8.4624H8.40223C7.98802 8.4624 7.65223 8.12662 7.65223 7.7124C7.65223 7.29819 7.98802 6.9624 8.40223 6.9624H12.0144Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"snow-flake\", \"keywords\": [ \"winter\", \"freeze\", \"snow\", \"freezing\", \"ice\", \"cold\", \"weather\", \"snowflake\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186711)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.53033 0.21967C5.23744 -0.0732233 4.76256 -0.0732233 4.46967 0.21967C4.17678 0.512563 4.17678 0.987437 4.46967 1.28033L6.25 3.06066V6.25H3.06066L1.28033 4.46967C0.987437 4.17678 0.512563 4.17678 0.21967 4.46967C-0.0732233 4.76256 -0.0732233 5.23744 0.21967 5.53033L1.68934 7L0.21967 8.46967C-0.0732233 8.76256 -0.0732233 9.23744 0.21967 9.53033C0.512563 9.82322 0.987437 9.82322 1.28033 9.53033L3.06066 7.75H6.25V10.9393L4.46967 12.7197C4.17678 13.0126 4.17678 13.4874 4.46967 13.7803C4.76256 14.0732 5.23744 14.0732 5.53033 13.7803L7 12.3107L8.46967 13.7803C8.76256 14.0732 9.23744 14.0732 9.53033 13.7803C9.82322 13.4874 9.82322 13.0126 9.53033 12.7197L7.75 10.9393V7.75H10.9393L12.7197 9.53033C13.0126 9.82322 13.4874 9.82322 13.7803 9.53033C14.0732 9.23744 14.0732 8.76256 13.7803 8.46967L12.3107 7L13.7803 5.53033C14.0732 5.23744 14.0732 4.76256 13.7803 4.46967C13.4874 4.17678 13.0126 4.17678 12.7197 4.46967L10.9393 6.25H7.75V3.06066L9.53033 1.28033C9.82322 0.987437 9.82322 0.512563 9.53033 0.21967C9.23744 -0.0732233 8.76256 -0.0732233 8.46967 0.21967L7 1.68934L5.53033 0.21967ZM2.96967 2.96967C3.26256 2.67678 3.73744 2.67678 4.03033 2.96967L5.03033 3.96967C5.32322 4.26256 5.32322 4.73744 5.03033 5.03033C4.73744 5.32322 4.26256 5.32322 3.96967 5.03033L2.96967 4.03033C2.67678 3.73744 2.67678 3.26256 2.96967 2.96967ZM5.03033 10.0303C5.32322 9.73744 5.32322 9.26256 5.03033 8.96967C4.73744 8.67678 4.26256 8.67678 3.96967 8.96967L2.96967 9.96967C2.67678 10.2626 2.67678 10.7374 2.96967 11.0303C3.26256 11.3232 3.73744 11.3232 4.03033 11.0303L5.03033 10.0303ZM11.0303 2.96967C11.3232 3.26256 11.3232 3.73744 11.0303 4.03033L10.0303 5.03033C9.73744 5.32322 9.26256 5.32322 8.96967 5.03033C8.67678 4.73744 8.67678 4.26256 8.96967 3.96967L9.96967 2.96967C10.2626 2.67678 10.7374 2.67678 11.0303 2.96967ZM10.0303 8.96967C9.73744 8.67678 9.26256 8.67678 8.96967 8.96967C8.67678 9.26256 8.67678 9.73744 8.96967 10.0303L9.96967 11.0303C10.2626 11.3232 10.7374 11.3232 11.0303 11.0303C11.3232 10.7374 11.3232 10.2626 11.0303 9.96967L10.0303 8.96967Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186711\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"sort-descending\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186714)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 0.75C0 0.335786 0.335786 0 0.75 0H7.25C7.66421 0 8 0.335786 8 0.75C8 1.16421 7.66421 1.5 7.25 1.5H0.75C0.335786 1.5 0 1.16421 0 0.75ZM3.25 3C2.83579 3 2.5 3.33579 2.5 3.75C2.5 4.16421 2.83579 4.5 3.25 4.5H7.25C7.66421 4.5 8 4.16421 8 3.75C8 3.33579 7.66421 3 7.25 3H3.25ZM5.25 6C4.83579 6 4.5 6.33579 4.5 6.75C4.5 7.16421 4.83579 7.5 5.25 7.5H7.25C7.66421 7.5 8 7.16421 8 6.75C8 6.33579 7.66421 6 7.25 6H5.25ZM10.75 0C11.3023 0 11.75 0.447715 11.75 1V10H13.25C13.5533 10 13.8268 10.1827 13.9429 10.463C14.059 10.7432 13.9948 11.0658 13.7803 11.2803L11.2803 13.7803C10.9874 14.0732 10.5126 14.0732 10.2197 13.7803L7.71967 11.2803C7.50517 11.0658 7.44101 10.7432 7.55709 10.463C7.67318 10.1827 7.94666 10 8.25 10H9.75V1C9.75 0.447715 10.1977 0 10.75 0Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186714\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"spiral-shape\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186717)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.91917 1.94641C3.47853 1.94641 1.5 3.92494 1.5 6.36558V6.99998C1.5 9.79099 3.76256 12.0536 6.55358 12.0536H7C10.0376 12.0536 12.5 9.59112 12.5 6.55355V6.10712C12.5 5.69291 12.8358 5.35712 13.25 5.35712C13.6642 5.35712 14 5.69291 14 6.10712V6.55355C14 10.4196 10.866 13.5536 7 13.5536H6.55358C2.93414 13.5536 0 10.6195 0 6.99998V6.36558C0 3.09651 2.6501 0.446411 5.91917 0.446411C8.90276 0.446411 11.3214 2.86508 11.3214 5.84866V6.27882C11.3214 8.81721 9.26366 10.875 6.72527 10.875H6.55357C4.41347 10.875 2.67857 9.14008 2.67857 6.99998V6.55355C2.67857 4.66 4.2136 3.12498 6.10714 3.12498C7.50758 3.12498 8.64286 4.26026 8.64286 5.66069V5.91579C8.64286 7.17534 7.62179 8.19641 6.36225 8.19641H6.10714C5.69293 8.19641 5.35714 7.86062 5.35714 7.44641C5.35714 7.03219 5.69293 6.69641 6.10714 6.69641H6.36225C6.79337 6.69641 7.14286 6.34691 7.14286 5.91579V5.66069C7.14286 5.08868 6.67915 4.62498 6.10714 4.62498C5.04202 4.62498 4.17857 5.48843 4.17857 6.55355V6.99998C4.17857 8.31166 5.2419 9.37498 6.55357 9.37498H6.72527C8.43523 9.37498 9.82143 7.98878 9.82143 6.27882V5.84866C9.82143 3.69351 8.07433 1.94641 5.91917 1.94641Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186717\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"split-vertical\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186720)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 0.5V0.999827V4.5C0 4.70223 0.121821 4.88455 0.308658 4.96194C0.495495 5.03933 0.710554 4.99655 0.853553 4.85355L2.14645 3.56066L6 7.41421V12.9952C6 13.5475 6.44772 13.9952 7 13.9952C7.55228 13.9952 8 13.5475 8 12.9952V7.41421L11.8536 3.56066L13.1464 4.85355C13.2894 4.99655 13.5045 5.03933 13.6913 4.96194C13.8782 4.88455 14 4.70223 14 4.5V1.00069V0.999307V0.5C14 0.223858 13.7761 0 13.5 0H13.0002H9.5C9.29777 0 9.11545 0.121821 9.03806 0.308658C8.96067 0.495495 9.00345 0.710554 9.14645 0.853553L10.4393 2.14645L7 5.58579L3.56066 2.14645L4.85355 0.853553C4.99655 0.710554 5.03933 0.495495 4.96194 0.308658C4.88455 0.121821 4.70223 0 4.5 0H1.00017H0.5C0.223858 0 0 0.223858 0 0.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186720\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"spray-paint\", \"keywords\": [ \"can\", \"color\", \"colors\", \"design\", \"paint\", \"painting\", \"spray\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186723)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.9854 0.603058C14.0667 1.00922 13.8033 1.40434 13.3971 1.48558L8.39709 2.48558C7.99092 2.56681 7.5958 2.3034 7.51456 1.89723C7.43333 1.49106 7.69674 1.09594 8.10291 1.0147L13.1029 0.0147091C13.5091 -0.0665249 13.9042 0.196888 13.9854 0.603058ZM4.25 3.25063H4.5C4.91421 3.25063 5.25 2.91484 5.25 2.50063C5.25 2.08641 4.91421 1.75063 4.5 1.75063H3.5H2.5C2.08579 1.75063 1.75 2.08641 1.75 2.50063C1.75 2.91484 2.08579 3.25063 2.5 3.25063H2.75V4.50014H1.5C0.671573 4.50014 0 5.17171 0 6.00014V12.5001C0 13.3285 0.671573 14.0001 1.5 14.0001H5.5C6.32843 14.0001 7 13.3285 7 12.5001V6.00014C7 5.17171 6.32843 4.50014 5.5 4.50014H4.25V3.25063ZM8.39709 3.0147C7.99092 2.93347 7.5958 3.19688 7.51456 3.60305C7.43333 4.00922 7.69674 4.40434 8.10291 4.48558L13.1029 5.48558C13.5091 5.56681 13.9042 5.3034 13.9854 4.89723C14.0667 4.49106 13.8033 4.09594 13.3971 4.0147L8.39709 3.0147Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186723\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"square-brackets-circle\", \"keywords\": [ \"interface\", \"math\", \"brackets\", \"sign\", \"mathematics\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186726)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 0.25C3.27208 0.25 0.25 3.27208 0.25 7C0.25 10.7279 3.27208 13.75 7 13.75C10.7279 13.75 13.75 10.7279 13.75 7C13.75 3.27208 10.7279 0.25 7 0.25ZM5 5.125C4.79289 5.125 4.625 5.29289 4.625 5.5V8.5C4.625 8.70711 4.79289 8.875 5 8.875H5.5C5.84518 8.875 6.125 9.15482 6.125 9.5C6.125 9.84518 5.84518 10.125 5.5 10.125H5C4.10254 10.125 3.375 9.39746 3.375 8.5V5.5C3.375 4.60254 4.10254 3.875 5 3.875H5.5C5.84518 3.875 6.125 4.15482 6.125 4.5C6.125 4.84518 5.84518 5.125 5.5 5.125H5ZM9.375 5.5C9.375 5.29289 9.20711 5.125 9 5.125H8.5C8.15482 5.125 7.875 4.84518 7.875 4.5C7.875 4.15482 8.15482 3.875 8.5 3.875H9C9.89746 3.875 10.625 4.60254 10.625 5.5V8.5C10.625 9.39746 9.89746 10.125 9 10.125H8.5C8.15482 10.125 7.875 9.84518 7.875 9.5C7.875 9.15482 8.15482 8.875 8.5 8.875H9C9.20711 8.875 9.375 8.70711 9.375 8.5V5.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186726\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"square-cap\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 3C0 2.44772 0.447715 2 1 2H14V6.25H6.85462C6.55793 5.51704 5.83934 5 5 5C3.89543 5 3 5.89543 3 7C3 8.10457 3.89543 9 5 9C5.83934 9 6.55793 8.48296 6.85462 7.75H14V12H1C0.447716 12 0 11.5523 0 11V3Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"square-clock\", \"keywords\": [ \"clock\", \"loading\", \"frame\", \"measure\", \"time\", \"circle\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186732)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1.5C0 0.671573 0.671573 0 1.5 0H12.5C13.3284 0 14 0.671573 14 1.5V12.5C14 13.3284 13.3284 14 12.5 14H1.5C0.671573 14 0 13.3284 0 12.5V1.5ZM11.5001 6.99951C11.5001 9.48479 9.4854 11.4995 7.00012 11.4995C4.51484 11.4995 2.50012 9.48479 2.50012 6.99951C2.50012 4.51423 4.51484 2.49951 7.00012 2.49951C9.4854 2.49951 11.5001 4.51423 11.5001 6.99951ZM7.625 5C7.625 4.65482 7.34518 4.375 7 4.375C6.65482 4.375 6.375 4.65482 6.375 5V7C6.375 7.34518 6.65482 7.625 7 7.625H9C9.34518 7.625 9.625 7.34518 9.625 7C9.625 6.65482 9.34518 6.375 9 6.375H7.625V5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186732\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"square-root-x-circle\", \"keywords\": [ \"interface\", \"math\", \"square\", \"root\", \"sign\", \"mathematics\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186735)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 6.75C0 3.02208 3.02208 0 6.75 0C10.4779 0 13.5 3.02208 13.5 6.75C13.5 10.4779 10.4779 13.5 6.75 13.5C3.02208 13.5 0 10.4779 0 6.75ZM7.48342 6.3038C7.2556 6.04448 6.86069 6.01895 6.60138 6.24678C6.34206 6.4746 6.31653 6.8695 6.54436 7.12882L7.29306 7.98101L6.40547 8.9913C6.17764 9.25061 6.20317 9.64552 6.46249 9.87334C6.72181 10.1012 7.11671 10.0756 7.34453 9.81632L8.125 8.92796L8.90547 9.81632C9.13329 10.0756 9.52819 10.1012 9.78751 9.87334C10.0468 9.64552 10.0724 9.25061 9.84453 8.9913L8.95694 7.98101L9.70564 7.12882C9.93347 6.8695 9.90794 6.4746 9.64862 6.24678C9.38931 6.01895 8.9944 6.04448 8.76658 6.3038L8.125 7.03407L7.48342 6.3038ZM5.38942 3.47119C5.08556 3.47119 4.82573 3.68974 4.77367 3.9891L4.10657 7.82491L3.82108 7.42523C3.62045 7.14434 3.23011 7.07929 2.94923 7.27992C2.66834 7.48055 2.60329 7.87089 2.80392 8.15177L3.95776 9.76716C4.10423 9.97221 4.35951 10.0692 4.6052 10.0133C4.85089 9.95728 5.03893 9.75923 5.0821 9.51097L5.91511 4.72119H9.3125C9.65768 4.72119 9.9375 4.44137 9.9375 4.09619C9.9375 3.75101 9.65768 3.47119 9.3125 3.47119H5.38942Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186735\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"star-1\", \"keywords\": [ \"reward\", \"rating\", \"rate\", \"social\", \"star\", \"media\", \"favorite\", \"like\", \"stars\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186738)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.00015 0.276855C6.80172 0.276855 6.60743 0.333623 6.44022 0.440458C6.27514 0.545928 6.14325 0.695846 6.05967 0.872901L4.47211 4.07799C4.46899 4.08429 4.466 4.09065 4.46315 4.09708C4.46244 4.09867 4.46132 4.10006 4.45991 4.10109C4.4585 4.10212 4.45685 4.10277 4.45511 4.10296C4.44902 4.10363 4.44294 4.10442 4.43688 4.10532L0.941302 4.62318C0.748098 4.64197 0.563858 4.71447 0.409543 4.83267C0.249871 4.95498 0.129089 5.12096 0.0618284 5.31051C-0.00543176 5.50006 -0.0162849 5.70505 0.0305842 5.90065C0.0772718 6.09548 0.179241 6.27267 0.324207 6.41092L2.88298 8.87974L2.88881 8.88527C2.89246 8.88869 2.89521 8.89296 2.89679 8.89771C2.89837 8.90245 2.89874 8.90752 2.89786 8.91244L2.8973 8.91571L2.28756 12.4841C2.25406 12.6775 2.27527 12.8773 2.34908 13.0591C2.42299 13.2413 2.54655 13.399 2.70568 13.5144C2.86482 13.6297 3.05314 13.6981 3.24922 13.7117C3.44491 13.7253 3.64049 13.6838 3.81379 13.592L3.81484 13.5914L6.96432 11.9269C6.97561 11.922 6.98782 11.9194 7.00017 11.9194C7.01252 11.9194 7.02472 11.922 7.03602 11.9269L10.1855 13.5914C10.3589 13.6835 10.5553 13.7254 10.7511 13.7117C10.9472 13.6981 11.1355 13.6297 11.2947 13.5144C11.4538 13.399 11.5774 13.2413 11.6513 13.0591C11.7251 12.8773 11.7464 12.6785 11.713 12.4852L11.7128 12.4841L11.1031 8.9157L11.1025 8.91244C11.1016 8.90752 11.102 8.90245 11.1036 8.89771C11.1051 8.89297 11.1079 8.88869 11.1115 8.88527L11.1174 8.87974L13.6762 6.41089C13.8211 6.27264 13.9231 6.09547 13.9698 5.90065C14.0166 5.70505 14.0058 5.50006 13.9385 5.31051C13.8713 5.12096 13.7505 4.95498 13.5908 4.83267C13.4365 4.71447 13.2523 4.64197 13.0591 4.62318L9.56343 4.10532C9.55737 4.10442 9.55129 4.10363 9.5452 4.10296C9.54346 4.10277 9.5418 4.10212 9.54039 4.10109C9.53898 4.10006 9.53787 4.09867 9.53716 4.09708C9.53431 4.09065 9.53132 4.08429 9.5282 4.07799L7.94063 0.872887C7.85705 0.695839 7.72516 0.545925 7.56009 0.440458C7.39287 0.333623 7.19858 0.276855 7.00015 0.276855Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186738\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"star-2\", \"keywords\": [ \"reward\", \"rating\", \"rate\", \"social\", \"star\", \"media\", \"favorite\", \"like\", \"stars\", \"spark\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186741)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.93486 0.654804C7.61738 -0.218273 6.38261 -0.218263 6.06513 0.654805L4.62238 4.62238L0.654803 6.06514C-0.218272 6.38262 -0.218263 7.61738 0.654805 7.93486L4.62238 9.37762L6.06513 13.3452C6.38261 14.2182 7.61738 14.2182 7.93486 13.3452L9.37761 9.37762L13.3452 7.93486C14.2183 7.61738 14.2183 6.38262 13.3452 6.06514L9.37761 4.62238L7.93486 0.654804Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186741\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"star-badge\", \"keywords\": [ \"ribbon\", \"reward\", \"like\", \"social\", \"rating\", \"media\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186744)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.99741 0.00854492C4.16715 0.00854492 1.87277 2.30292 1.87277 5.13318C1.87277 7.96344 4.16715 10.2578 6.99741 10.2578C9.82767 10.2578 12.122 7.96344 12.122 5.13318C12.122 2.30292 9.82767 0.00854492 6.99741 0.00854492ZM7.20003 2.43229L7.88276 3.80568C7.89802 3.84042 7.92223 3.87048 7.95292 3.8928C7.9836 3.91511 8.01966 3.92888 8.05741 3.9327L9.57369 4.16292C9.61704 4.16857 9.65789 4.18643 9.69146 4.21444C9.72503 4.24244 9.74993 4.27942 9.76325 4.32105C9.77658 4.36269 9.77777 4.40726 9.7667 4.44955C9.75562 4.49184 9.73273 4.5301 9.70071 4.55986L8.58136 5.62364C8.56467 5.65548 8.55595 5.69089 8.55595 5.72684C8.55595 5.76279 8.56467 5.7982 8.58136 5.83004L8.7957 7.33839C8.80485 7.38199 8.80115 7.4273 8.78505 7.46884C8.76896 7.51038 8.74116 7.54635 8.70502 7.5724C8.66888 7.59846 8.62597 7.61346 8.58147 7.61561C8.53698 7.61775 8.49282 7.60694 8.45434 7.58449L7.10477 6.87001C7.06984 6.85423 7.03195 6.84607 6.99363 6.84607C6.9553 6.84607 6.91741 6.85423 6.88249 6.87001L5.53291 7.58449C5.49444 7.60694 5.45028 7.61775 5.40578 7.61561C5.36129 7.61346 5.31837 7.59846 5.28223 7.5724C5.2461 7.54635 5.2183 7.51038 5.2022 7.46884C5.1861 7.4273 5.1824 7.38199 5.19155 7.33839L5.44559 5.83004C5.45651 5.79504 5.4586 5.75787 5.45168 5.72185C5.44475 5.68584 5.42902 5.6521 5.40589 5.62364L4.28654 4.55192C4.25685 4.52184 4.23601 4.48417 4.22631 4.44304C4.2166 4.40191 4.21841 4.3589 4.23153 4.31872C4.24465 4.27855 4.26857 4.24276 4.30068 4.21528C4.33279 4.1878 4.37184 4.16968 4.41356 4.16292L5.92984 3.94064C5.96759 3.93682 6.00365 3.92305 6.03434 3.90073C6.06502 3.87842 6.08923 3.84836 6.1045 3.81362L6.78722 2.44023C6.80549 2.40136 6.83427 2.36836 6.87031 2.34499C6.90635 2.32161 6.94821 2.30879 6.99116 2.30796C7.03411 2.30714 7.07643 2.31834 7.11334 2.34032C7.15025 2.36229 7.18028 2.39415 7.20003 2.43229ZM12.3008 8.67008C11.4504 9.94279 10.1541 10.8922 8.63528 11.295L10.0477 13.7413C10.1488 13.9165 10.3454 14.0134 10.5459 13.987C10.7464 13.9606 10.9113 13.8161 10.9636 13.6207L11.4724 11.7221L13.371 12.2308C13.5663 12.2832 13.774 12.2127 13.8971 12.0523C14.0202 11.8918 14.0345 11.673 13.9334 11.4979L12.3008 8.67008ZM5.3643 11.2964C3.84514 10.895 2.54819 9.94671 1.69679 8.67498L0.0669892 11.4979C-0.0341262 11.673 -0.0197852 11.8918 0.103326 12.0523C0.226436 12.2127 0.434072 12.2832 0.629412 12.2308L2.52802 11.7221L3.03675 13.6207C3.08909 13.8161 3.25395 13.9606 3.45445 13.987C3.65495 14.0134 3.85161 13.9165 3.95273 13.7413L5.3643 11.2964Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186744\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"straight-cap\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4 2C3.17157 2 2.5 2.67157 2.5 3.5V4.04148C1.08114 4.27952 0 5.5135 0 7C0 8.4865 1.08114 9.72048 2.5 9.95852V10.5C2.5 11.3284 3.17157 12 4 12H14V7.75H4.29933C4.03997 8.19835 3.55521 8.5 3 8.5C2.17157 8.5 1.5 7.82843 1.5 7C1.5 6.17157 2.17157 5.5 3 5.5C3.55521 5.5 4.03997 5.80165 4.29933 6.25H14V2H4Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"subtract-1\", \"keywords\": [ \"button\", \"delete\", \"buttons\", \"subtract\", \"horizontal\", \"remove\", \"line\", \"add\", \"mathematics\", \"math\", \"minus\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 7C0 6.44772 0.447715 6 1 6H13C13.5523 6 14 6.44772 14 7C14 7.55228 13.5523 8 13 8H1C0.447715 8 0 7.55228 0 7Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"subtract-circle\", \"keywords\": [ \"delete\", \"add\", \"circle\", \"subtract\", \"button\", \"buttons\", \"remove\", \"mathematics\", \"math\", \"minus\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186753)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14ZM4 6.25C3.58579 6.25 3.25 6.58579 3.25 7C3.25 7.41421 3.58579 7.75 4 7.75H10C10.4142 7.75 10.75 7.41421 10.75 7C10.75 6.58579 10.4142 6.25 10 6.25H4Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186753\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"subtract-square\", \"keywords\": [ \"subtract\", \"buttons\", \"remove\", \"add\", \"button\", \"square\", \"delete\", \"mathematics\", \"math\", \"minus\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186756)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.5 0C1.567 0 0 1.567 0 3.5V10.5C0 12.433 1.567 14 3.5 14H10.5C12.433 14 14 12.433 14 10.5V3.5C14 1.567 12.433 0 10.5 0H3.5ZM4 6.25C3.58579 6.25 3.25 6.58579 3.25 7C3.25 7.41421 3.58579 7.75 4 7.75H10C10.4142 7.75 10.75 7.41421 10.75 7C10.75 6.58579 10.4142 6.25 10 6.25H4Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186756\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"sun-cloud\", \"keywords\": [ \"cloud\", \"meteorology\", \"cloudy\", \"partly\", \"sunny\", \"weather\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186759)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.0007 5.9986C12.6572 5.9986 14 4.65576 14 2.9993C14 1.34283 12.6572 0 11.0007 0C9.34424 0 8.0014 1.34283 8.0014 2.9993C8.0014 4.65576 9.34424 5.9986 11.0007 5.9986ZM1.31802 6.31808C2.16193 5.47416 3.30653 5.00006 4.5 5.00006C5.62532 4.99424 6.71106 5.41499 7.53861 6.17759C8.36615 6.9402 8.87403 7.98801 8.96 9.11006C8.96589 9.15081 8.98174 9.18947 9.00617 9.22262C9.03059 9.25577 9.06282 9.28237 9.1 9.30006C9.13527 9.31931 9.17481 9.32941 9.215 9.32941C9.25519 9.32941 9.29473 9.31931 9.33 9.30006C9.68885 9.10398 10.0911 9.00085 10.5 9.00006C11.163 9.00006 11.7989 9.26345 12.2678 9.73229C12.7366 10.2011 13 10.837 13 11.5001C13 12.1631 12.7366 12.799 12.2678 13.2678C11.7989 13.7367 11.163 14.0001 10.5 14.0001H4.5C3.30653 14.0001 2.16193 13.526 1.31802 12.682C0.474106 11.8381 0 10.6935 0 9.50006C0 8.30659 0.474106 7.16199 1.31802 6.31808Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186759\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"synchronize-disable\", \"keywords\": [ \"arrows\", \"loading\", \"load\", \"sync\", \"synchronize\", \"arrow\", \"reload\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186762)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.9287 0.757273L10.6069 1.29369C12.4949 2.48953 13.75 4.59761 13.75 7.00005C13.75 8.59423 13.1974 10.0588 12.2739 11.2132L13.7803 12.7196C14.0732 13.0125 14.0732 13.4874 13.7803 13.7803C13.4874 14.0732 13.0126 14.0732 12.7197 13.7803L0.21967 1.28033C-0.0732233 0.987437 -0.0732233 0.512564 0.21967 0.21967C0.512563 -0.0732232 0.987437 -0.0732232 1.28033 0.21967L2.78686 1.7262C3.21429 1.38429 3.68414 1.09332 4.18715 0.862457C4.56361 0.689676 5.00885 0.854788 5.18163 1.23124C5.35442 1.6077 5.1893 2.05295 4.81285 2.22573C4.47265 2.38187 4.15208 2.57323 3.8558 2.79514L11.2049 10.1442C11.8615 9.26749 12.25 8.1789 12.25 7.00005C12.25 5.14572 11.2886 3.51473 9.83492 2.58032L9.42875 3.25727C9.3247 3.43068 9.12644 3.52434 8.92641 3.49458C8.72638 3.46481 8.56398 3.31748 8.51493 3.12129L8.01493 1.12129C7.94795 0.853395 8.11084 0.581928 8.37873 0.514953L10.3787 0.014953C10.5749 -0.0340952 10.7813 0.0398699 10.9017 0.202359C11.0221 0.364848 11.0328 0.583861 10.9287 0.757273ZM9.04074 11.8628L10.1495 12.9716C10.039 13.0299 9.9268 13.0853 9.81285 13.1376C9.43639 13.3104 8.99115 13.1453 8.81836 12.7688C8.66997 12.4455 8.77082 12.0714 9.04074 11.8628ZM2.1542 4.97632L1.02837 3.8505C0.531484 4.79058 0.25 5.8623 0.25 7.00005C0.25 9.40248 1.50506 11.5105 3.39308 12.7064L3.07125 13.2427C2.96721 13.4162 2.97786 13.6352 3.09826 13.7977C3.21865 13.9601 3.42507 14.0341 3.62127 13.9851L5.62127 13.4851C5.88916 13.4181 6.05204 13.1466 5.98507 12.8787L5.48507 10.8787C5.43602 10.6825 5.27362 10.5352 5.07359 10.5054C4.87356 10.4757 4.6753 10.5693 4.57125 10.7427L4.16506 11.4197C2.71138 10.4853 1.75 8.85436 1.75 7.00005C1.75 6.28295 1.89378 5.59925 2.1542 4.97632Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186762\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"synchronize-warning\", \"keywords\": [ \"arrow\", \"fail\", \"notification\", \"sync\", \"warning\", \"failure\", \"synchronize\", \"error\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186765)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.00004 1.75C5.14555 1.75 3.51443 2.71181 2.58028 4.16506L3.25727 4.57125C3.43068 4.6753 3.52434 4.87356 3.49458 5.07359C3.46481 5.27362 3.31748 5.43602 3.12129 5.48507L1.12129 5.98507C0.853395 6.05205 0.581928 5.88916 0.514953 5.62127L0.0149529 3.62127C-0.0340954 3.42507 0.0398698 3.21865 0.202359 3.09826C0.364848 2.97787 0.583861 2.96721 0.757272 3.07125L1.29372 3.39312C2.48983 1.50463 4.59778 0.25 7.00004 0.25C9.72537 0.25 12.0719 1.86511 13.1376 4.18715C13.3104 4.56361 13.1453 5.00885 12.7688 5.18164C12.3924 5.35442 11.9471 5.1893 11.7743 4.81285C10.9441 3.00383 9.1177 1.75 7.00004 1.75ZM7.75002 3.5C7.75002 3.08579 7.41424 2.75 7.00002 2.75C6.58581 2.75 6.25002 3.08579 6.25002 3.5V7.5C6.25002 7.91421 6.58581 8.25 7.00002 8.25C7.41424 8.25 7.75002 7.91421 7.75002 7.5V3.5ZM7.00002 11C7.55231 11 8.00002 10.5523 8.00002 10C8.00002 9.44771 7.55231 9 7.00002 9C6.44774 9 6.00002 9.44771 6.00002 10C6.00002 10.5523 6.44774 11 7.00002 11ZM11.4198 9.83496L10.7427 9.42875C10.5693 9.3247 10.4757 9.12644 10.5054 8.92641C10.5352 8.72638 10.6825 8.56398 10.8787 8.51493L12.8787 8.01493C13.1466 7.94795 13.4181 8.11083 13.4851 8.37873L13.9851 10.3787C14.0341 10.5749 13.9601 10.7813 13.7977 10.9017C13.6352 11.0221 13.4162 11.0328 13.2427 10.9287L12.7063 10.6069C11.5102 12.4954 9.40231 13.75 7.00005 13.75C4.27472 13.75 1.92819 12.1349 0.862457 9.81285C0.689676 9.43639 0.854787 8.99115 1.23124 8.81837C1.6077 8.64558 2.05295 8.8107 2.22573 9.18715C3.056 10.9962 4.88239 12.25 7.00005 12.25C8.85453 12.25 10.4856 11.2882 11.4198 9.83496Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186765\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"table-lamp-1\", \"keywords\": [ \"lighting\", \"light\", \"incandescent\", \"bulb\", \"lights\", \"table\", \"lamp\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186768)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.4395 0.43934C2.7208 0.158035 3.10233 0 3.50016 0H5.50016C5.89798 0 6.27951 0.158035 6.56082 0.43934C6.65432 0.532847 6.73421 0.637427 6.7992 0.75H8.00015C10.6236 0.75 12.7502 2.87665 12.7502 5.5V12.5H13.2502C13.6644 12.5 14.0002 12.8358 14.0002 13.25C14.0002 13.6642 13.6644 14 13.2502 14H7.25015C6.83594 14 6.50015 13.6642 6.50015 13.25C6.50015 12.8358 6.83594 12.5 7.25015 12.5H11.2502V5.5C11.2502 3.70507 9.79508 2.25 8.00015 2.25H7.00016V2.72243C7.57869 3.10893 8.06089 3.62516 8.40741 4.23157C8.80273 4.92337 9.00727 5.70773 9.00014 6.50448C8.99768 6.77886 8.77455 7 8.50016 7H0.500161C0.225765 7 0.00263659 6.77886 0.000180343 6.50448C-0.00695204 5.70773 0.197592 4.92337 0.592906 4.23157C0.939425 3.62516 1.42163 3.10893 2.00016 2.72243V1.5C2.00016 1.10218 2.15819 0.720644 2.4395 0.43934Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186768\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"tag\", \"keywords\": [ \"tags\", \"bookmark\", \"favorite\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186771)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.7707 0.00270316C8.64294 -0.00790113 8.51442 0.0101918 8.39456 0.0556581C8.27681 0.100318 8.17035 0.17028 8.08265 0.26057L0.365567 7.97766C0.131483 8.21203 0 8.52974 0 8.86099C0 9.19224 0.131702 9.51017 0.365786 9.74454L4.256 13.6348C4.49038 13.8688 4.80809 14.0003 5.13934 14.0003C5.47059 14.0003 5.78851 13.8686 6.02289 13.6345L13.7398 5.91768C13.8301 5.82998 13.9 5.72351 13.9447 5.60577C13.9902 5.4859 14.0082 5.35739 13.9976 5.22963C13.9967 5.218 13.9953 5.2064 13.9935 5.19486L13.4065 1.38441C13.3887 1.18107 13.2998 0.990089 13.1551 0.845293C13.0103 0.700498 12.8193 0.611648 12.6159 0.593807L8.80546 0.00681874C8.79392 0.00504145 8.78233 0.00366876 8.7707 0.00270316ZM9.99935 5.00122C9.44706 5.00122 8.99935 4.55351 8.99935 4.00122C8.99935 3.44894 9.44706 3.00122 9.99935 3.00122C10.5517 3.00122 10.9994 3.44894 10.9994 4.00122C10.9994 4.55351 10.5517 5.00122 9.99935 5.00122Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186771\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"text-flow-rows\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186774)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1 0C0.723851 0 0.499991 0.223867 0.5 0.500015L0.500091 3.50002C0.5001 3.77615 0.723955 4 1.0001 4H4.00022C4.27637 4 4.50023 3.77613 4.50022 3.49998L4.50019 2.75H9.50008V3.43934L3.43942 9.5H1.00008C0.723934 9.5 0.500076 9.72386 0.500076 10V13C0.500076 13.2761 0.723934 13.5 1.00008 13.5H4.00008C4.27622 13.5 4.50008 13.2761 4.50008 13V12.25H10.5001V13.5C10.5001 13.7022 10.6219 13.8845 10.8088 13.9619C10.9956 14.0393 11.2107 13.9966 11.3537 13.8536L13.3537 11.8536C13.5489 11.6583 13.5489 11.3417 13.3537 11.1464L11.3537 9.14645C11.2107 9.00345 10.9956 8.96067 10.8088 9.03806C10.6219 9.11545 10.5001 9.29777 10.5001 9.5V10.75H4.50008V10.5607L11.0608 4H13.0001C13.2762 4 13.5001 3.77614 13.5001 3.5V0.5C13.5001 0.223858 13.2762 0 13.0001 0H10.0001C9.72394 0 9.50008 0.223858 9.50008 0.5V1.25H4.50015L4.50013 0.499985C4.50012 0.223848 4.27626 0 4.00013 0H1Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186774\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"text-square\", \"keywords\": [ \"text\", \"options\", \"formatting\", \"format\", \"square\", \"color\", \"border\", \"fill\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186777)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0C0.671573 0 0 0.671573 0 1.5V12.5C0 13.3284 0.671573 14 1.5 14H12.5C13.3284 14 14 13.3284 14 12.5V1.5C14 0.671573 13.3284 0 12.5 0H1.5ZM6.77028 4.04057C6.80324 3.94169 6.89577 3.875 7 3.875C7.10423 3.875 7.19676 3.94169 7.22972 4.04057L8.42453 7.625H5.57547L6.77028 4.04057ZM8.41557 3.64528C8.21247 3.03598 7.64226 2.625 7 2.625C6.35774 2.625 5.78753 3.03598 5.58443 3.64528L4.12002 8.03852C4.11676 8.04759 4.1137 8.05677 4.11085 8.06603L3.28207 10.5524C3.17292 10.8798 3.34989 11.2338 3.67736 11.3429C4.00482 11.4521 4.35877 11.2751 4.46793 10.9476L5.15881 8.875H8.84119L9.53207 10.9476C9.64123 11.2751 9.99518 11.4521 10.3226 11.3429C10.6501 11.2338 10.8271 10.8798 10.7179 10.5524L9.88916 8.06605C9.8863 8.05677 9.88324 8.04759 9.87997 8.0385L8.41557 3.64528Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186777\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"text-style\", \"keywords\": [ \"text\", \"style\", \"formatting\", \"format\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186780)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.5 1.50049C2.39046 1.50049 2.25 1.60193 2.25 1.79211V2.83374C2.25 3.24795 1.91421 3.58374 1.5 3.58374C1.08579 3.58374 0.75 3.24795 0.75 2.83374V1.79211C0.75 0.831746 1.50497 0.000488281 2.5 0.000488281H7H11.5C12.495 0.000488281 13.25 0.831746 13.25 1.79211V2.83374C13.25 3.24795 12.9142 3.58374 12.5 3.58374C12.0858 3.58374 11.75 3.24795 11.75 2.83374V1.79211C11.75 1.60193 11.6095 1.50049 11.5 1.50049H7.75V12.4995H9.5C9.91421 12.4995 10.25 12.8353 10.25 13.2495C10.25 13.6637 9.91421 13.9995 9.5 13.9995H7.02662C7.01778 13.9998 7.00891 14 7 14C6.99109 14 6.98222 13.9998 6.97338 13.9995H4.5C4.08579 13.9995 3.75 13.6637 3.75 13.2495C3.75 12.8353 4.08579 12.4995 4.5 12.4995H6.25V1.50049H2.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186780\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"thermometer\", \"keywords\": [ \"temperature\", \"thermometer\", \"weather\", \"level\", \"meter\", \"mercury\", \"measure\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.75 2.25C4.75 1.00736 5.75736 0 7 0C8.24264 0 9.25 1.00736 9.25 2.25V7.81902C10.0132 8.46023 10.5 9.42311 10.5 10.5C10.5 12.433 8.933 14 7 14C5.067 14 3.5 12.433 3.5 10.5C3.5 9.42311 3.98681 8.46023 4.75 7.81902V2.25ZM7 1.5C6.58579 1.5 6.25 1.83579 6.25 2.25V8.19473C6.25 8.44821 6.12196 8.68454 5.90963 8.82299C5.3604 9.18109 5 9.79866 5 10.5C5 11.6046 5.89543 12.5 7 12.5C8.10457 12.5 9 11.6046 9 10.5C9 9.79866 8.6396 9.18109 8.09037 8.82299C7.87804 8.68454 7.75 8.44821 7.75 8.19473V2.25C7.75 1.83579 7.41421 1.5 7 1.5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"trending-content\", \"keywords\": [ \"lit\", \"flame\", \"torch\", \"trending\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186786)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.4946 0.12579C5.63241 0.0434684 5.78994 0 5.95047 0C6.1005 0 6.24792 0.03797 6.37907 0.110143C10.3657 1.96234 12.6231 5.80838 12.4837 9.02393C12.4243 10.3754 11.9404 11.6258 11.003 12.5393C10.0656 13.4528 8.71882 13.9869 7.01367 13.9902C6.32873 14.0324 5.64217 13.9383 4.99377 13.7132C4.34023 13.4864 3.73865 13.1312 3.22445 12.6684C2.71025 12.2056 2.2938 11.6446 1.99963 11.0185C1.70545 10.3924 1.53948 9.71374 1.51149 9.02251L1.51119 9.01505C1.49077 8.21521 1.6899 7.42507 2.08686 6.73039C2.48381 6.03571 3.06349 5.46305 3.76294 5.07458C3.88585 5.00632 4.03181 4.99295 4.16507 5.03776C4.29833 5.08257 4.40656 5.18141 4.46326 5.31006C4.70699 5.86315 5.04833 6.36628 5.46965 6.79597C5.96403 6.15101 6.19148 5.31769 6.18476 4.40211C6.17675 3.31148 5.83415 2.14324 5.26068 1.14849C5.1668 0.991797 5.13251 0.806401 5.16429 0.626323C5.19658 0.443333 5.29486 0.278536 5.4405 0.163139C5.4577 0.149516 5.47577 0.13704 5.4946 0.12579Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186786\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"trophy\", \"keywords\": [ \"reward\", \"rating\", \"trophy\", \"social\", \"award\", \"media\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186789)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 0.75C0 0.335787 0.335786 0 0.75 0H3.63462C3.65338 0 3.67199 0.00068928 3.69041 0.0020439H10.3113C10.3292 0.000746237 10.3473 9.08374e-05 10.3654 9.08374e-05H13.25C13.6642 9.08374e-05 14 0.335878 14 0.750091V3.63508C14 5.49434 12.604 7.02754 10.8029 7.24363C10.3555 8.61283 9.18921 9.65688 7.75 9.92989V12.5001H10.3654C10.7796 12.5001 11.1154 12.8359 11.1154 13.2501C11.1154 13.6643 10.7796 14.0001 10.3654 14.0001H7.02626C7.01754 14.0004 7.00879 14.0006 7 14.0006C6.99121 14.0006 6.98246 14.0004 6.97374 14.0001H3.63461C3.2204 14.0001 2.88461 13.6643 2.88461 13.2501C2.88461 12.8359 3.2204 12.5001 3.63461 12.5001H6.25V9.92989C4.81079 9.65688 3.64451 8.61282 3.19707 7.24363C1.39604 7.02754 0 5.49434 0 3.63508V0.75ZM2.88462 5.63422V1.5H1.5V3.63508C1.5 4.55007 2.07569 5.3306 2.88462 5.63422ZM11.1154 1.50009V5.63422C11.9243 5.3306 12.5 4.55007 12.5 3.63508V1.50009H11.1154Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186789\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"two-finger-drag-hotizontal\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186792)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.69134 0.0380749C2.87818 0.115465 3 0.297783 3 0.500015V1.50001H4C4.27614 1.50001 4.5 1.72387 4.5 2.00001C4.5 2.27616 4.27614 2.50001 4 2.50001H3V3.50001C3 3.70225 2.87818 3.88456 2.69134 3.96195C2.5045 4.03934 2.28944 3.99657 2.14645 3.85357L0.646446 2.35357C0.451184 2.15831 0.451184 1.84172 0.646446 1.64646L2.14645 0.146461C2.28944 0.0034622 2.5045 -0.0393156 2.69134 0.0380749ZM12.3533 0.146461C12.2103 0.0034622 11.9953 -0.0393156 11.8084 0.0380749C11.6216 0.115465 11.4998 0.297783 11.4998 0.500015V1.50001H10.4998C10.2236 1.50001 9.99975 1.72387 9.99975 2.00001C9.99975 2.27616 10.2236 2.50001 10.4998 2.50001H11.4998V3.50001C11.4998 3.70225 11.6216 3.88456 11.8084 3.96195C11.9953 4.03934 12.2103 3.99657 12.3533 3.85357L13.8533 2.35357C14.0486 2.15831 14.0486 1.84172 13.8533 1.64646L12.3533 0.146461ZM5.42045 10.0622L3.52403 6.11024C3.18284 5.39923 3.54457 4.5503 4.29372 4.30389C4.89819 4.10507 5.55798 4.37065 5.85615 4.9328L7.4434 7.9253L7.4436 7.91705L6.56711 3.29182C6.44721 2.65909 6.81498 2.03553 7.42672 1.83432C8.16273 1.59223 8.94374 2.05285 9.088 2.81411L9.90179 7.1085L10.349 6.96139C11.3467 6.63324 12.4131 7.08125 12.8939 7.975C13.013 8.19633 13.0636 8.44606 13.1054 8.69388L13.8034 12.8334C13.9063 13.4435 13.4361 13.9997 12.8174 13.9997H5.98334C5.76465 13.9997 5.55199 13.928 5.37794 13.7956L4.63716 13.2322C3.65484 12.485 3.76567 10.9744 4.84653 10.3786L5.42045 10.0622Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186792\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"two-finger-tap\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.92275 2.48469C7.42811 1.88153 8.18451 1.5 9.03076 1.5C10.5487 1.5 11.7792 2.73054 11.7792 4.2485C11.7792 4.46464 11.7544 4.67404 11.7078 4.87436C11.614 5.27781 11.8649 5.68094 12.2684 5.77478C12.6718 5.86863 13.075 5.61765 13.1688 5.21421C13.2411 4.90325 13.2792 4.57985 13.2792 4.2485C13.2792 1.90212 11.3771 0 9.03076 0C7.8772 0 6.83066 0.460438 6.06581 1.20569C5.93546 1.19363 5.80356 1.18748 5.67037 1.18748C3.32399 1.18748 1.42188 3.0896 1.42188 5.43598C1.42188 5.76733 1.45995 6.09073 1.53228 6.40169C1.62613 6.80513 2.02927 7.05611 2.43271 6.96226C2.83615 6.86842 3.08713 6.46528 2.99329 6.06184C2.94669 5.86151 2.92187 5.65212 2.92187 5.43598C2.92187 3.91802 4.15242 2.68748 5.67037 2.68748C5.85343 2.68748 6.03158 2.70527 6.20344 2.73899C6.47163 2.79162 6.74723 2.69419 6.92275 2.48469ZM4.74803 10.1891L4.18138 5.84244C4.07943 5.06042 4.68829 4.36702 5.47692 4.36702C6.11326 4.36702 6.65703 4.82546 6.76464 5.45262L7.3374 8.79124L7.34017 8.78346L7.95272 4.11594C8.03651 3.47743 8.5807 3 9.22468 3C9.99949 3 10.5974 3.68158 10.4966 4.4498L9.92792 8.78346H10.3987C11.5771 8.78346 12.5324 9.73877 12.5324 10.9172V12.9836C12.5324 13.5351 12.086 13.9825 11.5345 13.9836L4.25934 13.9989C3.92947 13.9996 3.62048 13.8376 3.43347 13.5658L3.01352 12.9556C2.31383 11.9389 2.8911 10.5386 4.104 10.3103L4.74803 10.1891Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"underline-text-1\", \"keywords\": [ \"text\", \"underline\", \"formatting\", \"format\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186798)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.75 0.75C4.75 0.335786 4.41421 0 4 0C3.58579 0 3.25 0.335786 3.25 0.75V7.5C3.25 9.57107 4.92893 11.25 7 11.25C9.07107 11.25 10.75 9.57107 10.75 7.5V0.75C10.75 0.335786 10.4142 0 10 0C9.58579 0 9.25 0.335786 9.25 0.75V7.5C9.25 8.74264 8.24264 9.75 7 9.75C5.75736 9.75 4.75 8.74264 4.75 7.5V0.75ZM0.75 12.5C0.335786 12.5 0 12.8358 0 13.25C0 13.6642 0.335786 14 0.75 14H13.25C13.6642 14 14 13.6642 14 13.25C14 12.8358 13.6642 12.5 13.25 12.5H0.75Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186798\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"upload-box-1\", \"keywords\": [ \"arrow\", \"box\", \"download\", \"internet\", \"network\", \"server\", \"up\", \"upload\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186801)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.375 0H3.38261C2.83785 0 2.33592 0.295338 2.07137 0.771536L0.347222 3.875H6.375V0ZM0 12.5V5.125H7H14V12.5C14 13.3284 13.3284 14 12.5 14H1.5C0.671573 14 0 13.3284 0 12.5ZM13.6528 3.875H7.625V0H10.6174C11.1621 0 11.6641 0.295338 11.9286 0.771536L13.6528 3.875ZM4.35356 8.93946L6.64646 6.64657C6.84172 6.45131 7.1583 6.45131 7.35356 6.64657L9.64646 8.93946C9.96144 9.25445 9.73836 9.79302 9.2929 9.79302H8.00001V11.793C8.00001 12.3453 7.5523 12.793 7.00001 12.793C6.44773 12.793 6.00001 12.3453 6.00001 11.793V9.79302H4.70712C4.26167 9.79302 4.03858 9.25444 4.35356 8.93946Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186801\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"upload-circle\", \"keywords\": [ \"arrow\", \"circle\", \"download\", \"internet\", \"network\", \"server\", \"up\", \"upload\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186804)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14ZM10.1464 5.68934L7.3535 2.89645C7.15824 2.70118 6.84166 2.70118 6.6464 2.89645L3.8535 5.68934C3.53852 6.00432 3.76161 6.54289 4.20706 6.54289H5.99995V10.2498C5.99995 10.802 6.44767 11.2498 6.99995 11.2498C7.55224 11.2498 7.99995 10.802 7.99995 10.2498V6.54289H9.79284C10.2383 6.54289 10.4614 6.00432 10.1464 5.68934Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186804\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"upload-computer\", \"keywords\": [ \"action\", \"actions\", \"computer\", \"desktop\", \"device\", \"display\", \"monitor\", \"screen\", \"upload\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186807)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.00086 4C4.71255 4 4.44977 3.83474 4.32488 3.57489C4.19999 3.31504 4.2351 3.00661 4.41521 2.78148L6.41521 0.281479C6.55753 0.103567 6.77302 0 7.00086 0C7.2287 0 7.44418 0.103567 7.58651 0.281479L9.58651 2.78148C9.76661 3.00661 9.80172 3.31504 9.67683 3.57489C9.55194 3.83474 9.28916 4 9.00086 4H8.00085V6.5C8.00085 7.05228 7.55313 7.5 7.00085 7.5C6.44856 7.5 6.00085 7.05228 6.00085 6.5V4H5.00086ZM2.0625 2.91016H2.30085C2.85313 2.91016 3.30085 2.46244 3.30085 1.91016C3.30085 1.35787 2.85313 0.910156 2.30085 0.910156H1.51929C0.714729 0.910156 0.0625 1.56239 0.0625 2.36695V9.67567C0.0625 10.4802 0.714729 11.1325 1.51929 11.1325H5.43645L4.94867 12.4954H4.00085C3.58664 12.4954 3.25085 12.8311 3.25085 13.2454C3.25085 13.6596 3.58664 13.9954 4.00085 13.9954H10.0009C10.4151 13.9954 10.7509 13.6596 10.7509 13.2454C10.7509 12.8311 10.4151 12.4954 10.0009 12.4954H9.05294L8.56516 11.1325H12.4824C13.287 11.1325 13.9392 10.4802 13.9392 9.67567V2.36695C13.9392 1.56239 13.287 0.910156 12.4824 0.910156H11.7009C11.1486 0.910156 10.7009 1.35787 10.7009 1.91016C10.7009 2.46244 11.1486 2.91016 11.7009 2.91016H11.9392V9.13246H2.0625V2.91016Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186807\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"upload-file\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.43934 0.43934C1.72064 0.158035 2.10218 0 2.5 0H8.5C8.63261 0 8.75979 0.0526784 8.85355 0.146447L12.8536 4.14645C12.9473 4.24021 13 4.36739 13 4.5V12.5C13 12.8978 12.842 13.2794 12.5607 13.5607C12.2794 13.842 11.8978 14 11.5 14H2.5C2.10217 14 1.72064 13.842 1.43934 13.5607C1.15804 13.2794 1 12.8978 1 12.5V1.5C1 1.10218 1.15804 0.720644 1.43934 0.43934ZM7.18695 3.43306L7.29089 3.53699C7.40736 3.61301 7.507 3.71266 7.58303 3.82913L9.43695 5.68306C9.6157 5.86181 9.66917 6.13063 9.57244 6.36418C9.4757 6.59772 9.2478 6.75 8.99501 6.75H7.74502V10.375C7.74502 10.9273 7.2973 11.375 6.74502 11.375C6.19273 11.375 5.74502 10.9273 5.74502 10.375V6.75H4.49501C4.24222 6.75 4.01432 6.59772 3.91759 6.36418C3.82085 6.13063 3.87432 5.86181 4.05307 5.68306L5.90703 3.82909C5.98304 3.71265 6.08267 3.61303 6.19911 3.53701L6.30307 3.43306C6.42028 3.31585 6.57925 3.25 6.74501 3.25C6.91077 3.25 7.06974 3.31585 7.18695 3.43306Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"user-add-plus\", \"keywords\": [ \"actions\", \"add\", \"close\", \"geometric\", \"human\", \"person\", \"plus\", \"single\", \"up\", \"user\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186813)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8 3C8 4.65685 6.65685 6 5 6C3.34315 6 2 4.65685 2 3C2 1.34315 3.34315 0 5 0C6.65685 0 8 1.34315 8 3ZM10.75 7.5C11.1642 7.5 11.5 7.83579 11.5 8.25V10H13.25C13.6642 10 14 10.3358 14 10.75C14 11.1642 13.6642 11.5 13.25 11.5H11.5V13.25C11.5 13.6642 11.1642 14 10.75 14C10.3358 14 10 13.6642 10 13.25V11.5H8.25C7.83579 11.5 7.5 11.1642 7.5 10.75C7.5 10.3358 7.83579 10 8.25 10H10V8.25C10 7.83579 10.3358 7.5 10.75 7.5ZM5 6.99997C6.49337 6.99997 7.83382 7.65466 8.75 8.69269V8.74999H8.25C7.14543 8.74999 6.25 9.64542 6.25 10.75C6.25 11.503 6.66612 12.1588 7.28095 12.5H0.5C0.223858 12.5 0 12.2761 0 12C0 9.23855 2.23858 6.99997 5 6.99997Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186813\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"user-check-validate\", \"keywords\": [ \"actions\", \"close\", \"checkmark\", \"check\", \"geometric\", \"human\", \"person\", \"single\", \"success\", \"up\", \"user\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186816)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8 3C8 4.65685 6.65685 6 5 6C3.34315 6 2 4.65685 2 3C2 1.34315 3.34315 0 5 0C6.65685 0 8 1.34315 8 3ZM6.05091 12.5H0.5C0.223858 12.5 0 12.2761 0 12C0 9.23855 2.23858 6.99997 5 6.99997C7.18687 6.99997 9.04584 8.40392 9.72473 10.3597L9.64861 10.4708L9.0477 10.0664C8.1656 9.43152 6.93374 9.61741 6.2793 10.491C5.83015 11.0907 5.76835 11.8602 6.05091 12.5ZM13.8502 9.1997C14.0985 8.86818 14.0311 8.39812 13.6996 8.14978C13.3681 7.90145 12.898 7.96888 12.6497 8.30039L9.97932 12.2001L8.32958 11.0898C7.99806 10.8414 7.528 10.9089 7.27967 11.2404C7.03133 11.5719 7.09876 12.042 7.43028 12.2903L9.68028 13.8503C9.83948 13.9696 10.0395 14.0207 10.2364 13.9924C10.4333 13.9642 10.6109 13.8589 10.7302 13.6997L13.8502 9.1997Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186816\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"user-circle-single\", \"keywords\": [ \"circle\", \"geometric\", \"human\", \"person\", \"single\", \"user\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186819)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 8.87687 13.2613 10.5812 12.0589 11.8382C10.7889 13.1657 9.0017 13.9942 7.02089 14C7.01393 14 7.00697 14 7 14C6.99303 14 6.98607 14 6.97911 14C4.9983 13.9942 3.21106 13.1657 1.94113 11.8382C0.738659 10.5812 0 8.87687 0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7ZM11.2428 10.5C10.2341 9.27847 8.70794 8.49997 7 8.49997C5.29206 8.49997 3.76594 9.27847 2.75715 10.5C3.76594 11.7215 5.29206 12.5 7 12.5C8.70794 12.5 10.2341 11.7215 11.2428 10.5ZM7.00012 7.49997C8.38083 7.49997 9.50012 6.38068 9.50012 4.99997C9.50012 3.61926 8.38083 2.49997 7.00012 2.49997C5.61941 2.49997 4.50012 3.61926 4.50012 4.99997C4.50012 6.38068 5.61941 7.49997 7.00012 7.49997Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186819\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"user-identifier-card\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 3C0 2.44772 0.447715 2 1 2H13C13.5523 2 14 2.44772 14 3V11C14 11.5523 13.5523 12 13 12H1C0.447716 12 0 11.5523 0 11V3ZM6 5.5C6 6.32843 5.32843 7 4.5 7C3.67157 7 3 6.32843 3 5.5C3 4.67157 3.67157 4 4.5 4C5.32843 4 6 4.67157 6 5.5ZM1.54665 9.3022C2.27806 8.50196 3.33039 8 4.49997 8C5.66954 8 6.72187 8.50196 7.45329 9.3022C7.71308 9.58645 7.48306 10 7.09797 10H1.90196C1.51687 10 1.28685 9.58645 1.54665 9.3022ZM8.87497 5.50003C8.87497 5.15485 9.15479 4.87503 9.49997 4.87503H11.5C11.8451 4.87503 12.125 5.15485 12.125 5.50003C12.125 5.84521 11.8451 6.12503 11.5 6.12503H9.49997C9.15479 6.12503 8.87497 5.84521 8.87497 5.50003ZM9.49997 7.87503C9.15479 7.87503 8.87497 8.15485 8.87497 8.50003C8.87497 8.84521 9.15479 9.12503 9.49997 9.12503H11.5C11.8451 9.12503 12.125 8.84521 12.125 8.50003C12.125 8.15485 11.8451 7.87503 11.5 7.87503H9.49997Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"user-multiple-circle\", \"keywords\": [ \"close\", \"geometric\", \"human\", \"multiple\", \"person\", \"up\", \"user\", \"circle\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186825)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 8.35337 0.38407 9.61703 1.04916 10.6879C1.08435 10.7446 1.12035 10.8007 1.1571 10.8563C1.15707 10.8563 1.15713 10.8563 1.1571 10.8563C2.40981 12.7506 4.55894 14 7 14C10.866 14 14 10.866 14 7ZM7.00001 12.5C7.5499 12.5 8.08094 12.4193 8.58194 12.2691C8.14881 10.3053 6.82402 8.6875 4.57054 8.6875C3.6246 8.6875 2.80893 9.01327 2.12436 9.54761C3.04322 11.3025 4.88177 12.5 7.00001 12.5ZM9.59364 11.8513C10.602 11.3111 11.4223 10.4657 11.9312 9.43859C11.8846 9.40962 11.8373 9.38165 11.7893 9.35469C11.2294 9.04019 10.5981 8.875 9.9559 8.875C9.36797 8.875 8.78909 9.01347 8.26584 9.27822C8.3133 9.33051 8.3597 9.38383 8.40502 9.43816C8.98793 10.1369 9.37456 10.976 9.59364 11.8513ZM4.7291 3.24298C5.96908 3.24298 6.97428 4.24818 6.97428 5.48816C6.97428 6.72814 5.96908 7.73334 4.7291 7.73334C3.48913 7.73334 2.48392 6.72814 2.48392 5.48816C2.48392 4.24818 3.48913 3.24298 4.7291 3.24298ZM11.798 6.13992C11.798 5.11263 10.9652 4.27985 9.93795 4.27985C8.91066 4.27985 8.07788 5.11263 8.07788 6.13992C8.07788 7.16721 8.91066 7.99999 9.93795 7.99999C10.9652 7.99999 11.798 7.16721 11.798 6.13992Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186825\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"user-multiple-group\", \"keywords\": [ \"close\", \"geometric\", \"human\", \"multiple\", \"person\", \"up\", \"user\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186828)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8 4.5C8 6.15685 6.65685 7.5 5 7.5C3.34315 7.5 2 6.15685 2 4.5C2 2.84315 3.34315 1.5 5 1.5C6.65685 1.5 8 2.84315 8 4.5ZM5 8.49997C2.23858 8.49997 0 10.7386 0 13.5C0 13.7761 0.223858 14 0.5 14H9.5C9.77614 14 10 13.7761 10 13.5C10 10.7386 7.76142 8.49997 5 8.49997ZM13.5001 14H11.1776C11.2248 13.8416 11.2501 13.6737 11.2501 13.5C11.2501 11.4589 10.2717 9.64634 8.75832 8.50571C8.83843 8.5019 8.91903 8.49997 9.00008 8.49997C11.7615 8.49997 14.0001 10.7386 14.0001 13.5C14.0001 13.7761 13.7762 14 13.5001 14ZM9.00008 7.5C8.69835 7.5 8.40703 7.45545 8.13233 7.37258C8.82642 6.61615 9.25008 5.60755 9.25008 4.5C9.25008 3.39245 8.82642 2.38385 8.13232 1.62741C8.40703 1.54454 8.69835 1.5 9.00008 1.5C10.6569 1.5 12.0001 2.84315 12.0001 4.5C12.0001 6.15685 10.6569 7.5 9.00008 7.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186828\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"user-profile-focus\", \"keywords\": [ \"close\", \"geometric\", \"human\", \"person\", \"profile\", \"focus\", \"user\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186831)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.57322 1.57322C1.62011 1.52634 1.6837 1.5 1.75 1.5H3.25C3.66421 1.5 4 1.16421 4 0.75C4 0.335786 3.66421 0 3.25 0H1.75C1.28587 0 0.840752 0.184375 0.512563 0.512563C0.184375 0.840752 0 1.28587 0 1.75V3.25C0 3.66421 0.335786 4 0.75 4C1.16421 4 1.5 3.66421 1.5 3.25V1.75C1.5 1.6837 1.52634 1.62011 1.57322 1.57322ZM14 10.75C14 10.3358 13.6642 10 13.25 10C12.8358 10 12.5 10.3358 12.5 10.75V12.25C12.5 12.3163 12.4737 12.3799 12.4268 12.4268C12.3799 12.4737 12.3163 12.5 12.25 12.5H10.75C10.3358 12.5 10 12.8358 10 13.25C10 13.6642 10.3358 14 10.75 14H12.25C12.7141 14 13.1592 13.8156 13.4874 13.4874C13.8156 13.1592 14 12.7141 14 12.25V10.75ZM0.75 10C1.16421 10 1.5 10.3358 1.5 10.75V12.25C1.5 12.3163 1.52634 12.3799 1.57322 12.4268C1.62011 12.4737 1.6837 12.5 1.75 12.5H3.25C3.66421 12.5 4 12.8358 4 13.25C4 13.6642 3.66421 14 3.25 14H1.75C1.28587 14 0.840752 13.8156 0.512563 13.4874C0.184375 13.1592 0 12.7141 0 12.25V10.75C0 10.3358 0.335786 10 0.75 10ZM10.75 0C10.3358 0 10 0.335786 10 0.75C10 1.16421 10.3358 1.5 10.75 1.5H12.25C12.3163 1.5 12.3799 1.52634 12.4268 1.57322C12.4737 1.62011 12.5 1.6837 12.5 1.75V3.25C12.5 3.66421 12.8358 4 13.25 4C13.6642 4 14 3.66421 14 3.25V1.75C14 1.28587 13.8156 0.840752 13.4874 0.512563C13.1592 0.184375 12.7141 0 12.25 0H10.75ZM6.99906 7.77647C5.1 7.77647 3.48059 8.97382 2.85458 10.6548C2.74322 10.9538 2.98152 11.2502 3.30063 11.2502H10.6975C11.0166 11.2502 11.2549 10.9538 11.1435 10.6548C10.5175 8.97382 8.89813 7.77647 6.99906 7.77647ZM9.20809 4.46056C9.20809 5.68142 8.21839 6.67112 6.99753 6.67112C5.77667 6.67112 4.78697 5.68142 4.78697 4.46056C4.78697 3.2397 5.77667 2.25 6.99753 2.25C8.21839 2.25 9.20809 3.2397 9.20809 4.46056Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186831\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"user-protection-2\", \"keywords\": [ \"shield\", \"secure\", \"security\", \"profile\", \"person\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186834)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1.75C0 0.783502 0.783501 0 1.75 0H12.25C13.2165 0 14 0.7835 14 1.75V5.00716C14 9.09284 11.3353 12.7008 7.43026 13.9023C7.14991 13.9886 6.85009 13.9886 6.56973 13.9023C2.66472 12.7008 0 9.09285 0 5.00716V1.75ZM7 6.5C8.24264 6.5 9.25 5.49264 9.25 4.25C9.25 3.00736 8.24264 2 7 2C5.75736 2 4.75 3.00736 4.75 4.25C4.75 5.49264 5.75736 6.5 7 6.5ZM11.1518 9.39262C10.1434 8.23306 8.65731 7.5 6.99996 7.5C5.34263 7.5 3.85651 8.23305 2.84814 9.39261C3.81992 10.8234 5.26334 11.9282 6.99998 12.4653C8.73661 11.9282 10.18 10.8234 11.1518 9.39262Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186834\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"user-remove-subtract\", \"keywords\": [ \"actions\", \"remove\", \"close\", \"geometric\", \"human\", \"person\", \"minus\", \"single\", \"up\", \"user\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8 3C8 4.65685 6.65685 6 5 6C3.34315 6 2 4.65685 2 3C2 1.34315 3.34315 0 5 0C6.65685 0 8 1.34315 8 3ZM13.25 10C13.6642 10 14 10.3358 14 10.75C14 11.1642 13.6642 11.5 13.25 11.5H8.25C7.83579 11.5 7.5 11.1642 7.5 10.75C7.5 10.3358 7.83579 10 8.25 10H13.25ZM5 6.99997C2.23858 6.99997 0 9.23855 0 12C0 12.2761 0.223858 12.5 0.5 12.5H7.28094C6.66612 12.1588 6.25 11.503 6.25 10.75C6.25 9.64543 7.14543 8.75 8.25 8.75H8.79982C7.88278 7.67882 6.52067 6.99997 5 6.99997Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"user-single-neutral-male\", \"keywords\": [ \"close\", \"geometric\", \"human\", \"person\", \"single\", \"up\", \"user\", \"male\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186840)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.00052 8C9.20966 8 11.0005 6.20914 11.0005 4L11.0003 3.95826C9.86216 3.92759 8.82623 3.48555 8.03652 2.77641C7.21755 3.51184 6.13378 3.96 4.9458 3.96C4.26179 3.96 3.61142 3.81127 3.02621 3.54414C3.00924 3.69374 3.00052 3.84585 3.00052 4C3.00052 6.20914 4.79138 8 7.00052 8ZM8.41083 1.73652C9.02106 2.42555 9.8878 2.87995 10.8614 2.95039C10.4003 1.25039 8.84645 0 7.00052 0C5.30238 0 3.85139 1.05819 3.27099 2.55113C3.77151 2.81222 4.34071 2.96 4.9458 2.96C6.02686 2.96 6.99696 2.48769 7.66221 1.73653C7.75711 1.62937 7.89338 1.56802 8.03652 1.56802C8.17966 1.56802 8.31593 1.62937 8.41083 1.73652ZM0.512537 13.3674C1.55285 10.8061 4.06583 9.00003 7.00063 9.00003C9.93544 9.00003 12.4484 10.8061 13.4887 13.3674C13.6146 13.6773 13.3747 14 13.0402 14H0.961006C0.62653 14 0.386672 13.6773 0.512537 13.3674Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186840\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"user-sync-online-in-person\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186843)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.2127 2.53752C8.91903 1.6074 7.23449 1.24494 5.57696 1.68908C2.78152 2.43811 1.06874 5.21231 1.5948 8.0098C1.67134 8.41688 1.4034 8.80893 0.996318 8.88548C0.58924 8.96203 0.197182 8.69408 0.120633 8.28701C-0.549251 4.72465 1.62993 1.19377 5.18873 0.240189C7.38469 -0.348217 9.61568 0.176234 11.2842 1.46596L11.8965 0.853736C12.2114 0.538754 12.75 0.761838 12.75 1.20729V3.50018C12.75 3.77633 12.5262 4.00018 12.25 4.00018H9.95712C9.51167 4.00018 9.28858 3.46161 9.60357 3.14663L10.2127 2.53752ZM13.0041 5.11487C13.4111 5.03832 13.8032 5.30626 13.8797 5.71334C14.5496 9.2757 12.3705 12.8066 8.81165 13.7602C6.61562 14.3486 4.38455 13.8241 2.71598 12.5343L2.10362 13.1466C1.78863 13.4616 1.25006 13.2385 1.25006 12.7931V10.5002C1.25006 10.224 1.47392 10.0002 1.75006 10.0002H4.04296C4.48841 10.0002 4.71149 10.5387 4.39651 10.8537L3.78752 11.4627C5.08119 12.3929 6.76582 12.7554 8.42342 12.3113C11.2189 11.5622 12.9316 8.78804 12.4056 5.99055C12.329 5.58347 12.597 5.19142 13.0041 5.11487ZM8.49952 5.50065C8.49952 6.32874 7.82822 7.00004 7.00013 7.00004C6.17203 7.00004 5.50073 6.32874 5.50073 5.50065C5.50073 4.67255 6.17203 4.00125 7.00013 4.00125C7.82822 4.00125 8.49952 4.67255 8.49952 5.50065ZM6.99992 7.52518C5.94521 7.52518 5.01762 8.06946 4.4827 8.89247C4.29465 9.1818 4.53261 9.52515 4.87768 9.52515H9.12216C9.46723 9.52515 9.70519 9.1818 9.51714 8.89247C8.98223 8.06946 8.05464 7.52518 6.99992 7.52518Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186843\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"vertical-slider-square\", \"keywords\": [ \"adjustment\", \"adjust\", \"controls\", \"fader\", \"vertical\", \"settings\", \"slider\", \"square\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186846)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0C0.671574 3.621e-08 -3.6212e-08 0.671573 0 1.5L4.80825e-07 12.5C5.17037e-07 13.3284 0.671574 14 1.5 14H12.5C13.3284 14 14 13.3284 14 12.5V1.5C14 0.671573 13.3284 4.36636e-07 12.5 4.72848e-07L1.5 0ZM3.875 5.88497V11C3.875 11.3452 4.15482 11.625 4.5 11.625C4.84517 11.625 5.125 11.3452 5.125 11V5.88498C5.78284 5.63337 6.25 4.99618 6.25 4.24988C6.25 3.28338 5.4665 2.49988 4.5 2.49988C3.5335 2.49988 2.75 3.28338 2.75 4.24988C2.75 4.99618 3.21716 5.63337 3.875 5.88497ZM11.25 7.74991C11.25 8.49621 10.7828 9.1334 10.125 9.38501V11C10.125 11.3452 9.84517 11.625 9.5 11.625C9.15482 11.625 8.875 11.3452 8.875 11V9.385C8.21716 9.1334 7.75 8.49621 7.75 7.74991C7.75 7.00362 8.21716 6.36643 8.875 6.11482V3C8.875 2.65482 9.15482 2.375 9.5 2.375C9.84517 2.375 10.125 2.65482 10.125 3V6.11482C10.7828 6.36642 11.25 7.00361 11.25 7.74991Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186846\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"video-swap-camera\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.6 1.2C4.69443 1.0741 4.84262 1 5 1H9C9.15738 1 9.30557 1.0741 9.4 1.2L10.75 3H12.5C12.8978 3 13.2794 3.15804 13.5607 3.43934C13.842 3.72064 14 4.10217 14 4.5V11.5C14 11.8978 13.842 12.2794 13.5607 12.5607C13.2794 12.842 12.8978 13 12.5 13H1.5C1.10217 13 0.720644 12.842 0.43934 12.5607C0.158035 12.2794 0 11.8978 0 11.5V4.5C0 4.10218 0.158035 3.72064 0.43934 3.43934C0.720644 3.15804 1.10217 3 1.5 3H3.25L4.6 1.2ZM10.2361 7.44443L10.1784 7.43641C10.1765 5.84151 8.98536 4.46158 7.36173 4.26358C6.80682 4.1959 6.26598 4.2747 5.77992 4.47023C5.45968 4.59906 5.30452 4.9631 5.43335 5.28334C5.56217 5.60357 5.92621 5.75874 6.24645 5.62991C6.54168 5.51114 6.87026 5.4629 7.2104 5.50438C8.13967 5.61771 8.83663 6.36584 8.91989 7.26141L8.75939 7.23909C8.50901 7.20427 8.26231 7.3237 8.13433 7.5417C8.00635 7.7597 8.02228 8.03332 8.17471 8.23499L8.81036 9.07599C9.0185 9.35136 9.41045 9.40587 9.68582 9.19773L10.5268 8.56208C10.7285 8.40965 10.8185 8.15076 10.7548 7.90611C10.6912 7.66147 10.4864 7.47925 10.2361 7.44443ZM5.1344 5.85256C4.92627 5.5772 4.53431 5.52269 4.25894 5.73082L3.41794 6.36648C3.21628 6.5189 3.12629 6.7778 3.18993 7.02245C3.25358 7.26709 3.45834 7.44931 3.70871 7.48412L3.76633 7.49214C3.7687 9.08663 4.95967 10.466 6.58298 10.664C7.13789 10.7317 7.67873 10.6529 8.16479 10.4573C8.48502 10.3285 8.64019 9.96448 8.51136 9.64424C8.38253 9.32401 8.01849 9.16884 7.69826 9.29767C7.40302 9.41644 7.07445 9.46468 6.7343 9.4232C5.80538 9.30991 5.10858 8.56229 5.0249 7.66715L5.18537 7.68947C5.43575 7.72429 5.68245 7.60485 5.81044 7.38686C5.93842 7.16886 5.92249 6.89523 5.77006 6.69357L5.1344 5.85256Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"visible\", \"keywords\": [ \"eye\", \"eyeball\", \"open\", \"view\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.9327 3.49099C4.0559 2.68177 5.4556 2 7 2C8.54441 2 9.9441 2.68177 11.0673 3.49099C12.1946 4.30314 13.087 5.27496 13.6272 5.92789L13.6317 5.93342C13.873 6.23362 14 6.61404 14 7.00006C14 7.38607 13.873 7.7665 13.6317 8.0667L13.6272 8.07223C13.087 8.72515 12.1946 9.69698 11.0673 10.5091C9.9441 11.3183 8.54441 12.0001 7 12.0001C5.4556 12.0001 4.0559 11.3183 2.9327 10.5091C1.80544 9.69698 0.913028 8.72515 0.37279 8.07223L0.36828 8.0667C0.127025 7.7665 0 7.38607 0 7.00006C0 6.61404 0.127025 6.23362 0.36828 5.93342L0.37279 5.92789C0.913028 5.27496 1.80544 4.30314 2.9327 3.49099ZM7 9.25C8.24264 9.25 9.25 8.24264 9.25 7C9.25 5.75736 8.24264 4.75 7 4.75C5.75736 4.75 4.75 5.75736 4.75 7C4.75 8.24264 5.75736 9.25 7 9.25Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"voice-scan-2\", \"keywords\": [ \"identification\", \"secure\", \"id\", \"soundwave\", \"sound\", \"voice\", \"brackets\", \"security\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186870)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.57322 1.57322C1.62011 1.52634 1.6837 1.5 1.75 1.5H3.25C3.66421 1.5 4 1.16421 4 0.75C4 0.335786 3.66421 0 3.25 0H1.75C1.28587 0 0.840752 0.184375 0.512563 0.512563C0.184375 0.840752 0 1.28587 0 1.75V3.25C0 3.66421 0.335786 4 0.75 4C1.16421 4 1.5 3.66421 1.5 3.25V1.75C1.5 1.6837 1.52634 1.62011 1.57322 1.57322ZM2.50012 4.375C2.8453 4.375 3.12512 4.65482 3.12512 5V9.5C3.12512 9.84518 2.8453 10.125 2.50012 10.125C2.15494 10.125 1.87512 9.84518 1.87512 9.5V5C1.87512 4.65482 2.15494 4.375 2.50012 4.375ZM4.75006 5.375C5.09524 5.375 5.37506 5.65482 5.37506 6V8.5C5.37506 8.84518 5.09524 9.125 4.75006 9.125C4.40488 9.125 4.12506 8.84518 4.12506 8.5V6C4.12506 5.65482 4.40488 5.375 4.75006 5.375ZM7.625 5C7.625 4.65482 7.34518 4.375 7 4.375C6.65482 4.375 6.375 4.65482 6.375 5V9.5C6.375 9.84518 6.65482 10.125 7 10.125C7.34518 10.125 7.625 9.84518 7.625 9.5V5ZM9.24994 5.375C9.59512 5.375 9.87494 5.65482 9.87494 6V6.00236V6.00473V6.00714V6.00956V6.01201V6.01449V6.01698V6.0195V6.02204V6.02461V6.02719V6.0298V6.03244V6.03509V6.03777V6.04047V6.0432V6.04594V6.04871V6.0515V6.05431V6.05714V6.06V6.06288V6.06578V6.0687V6.07164V6.0746V6.07759V6.08059V6.08362V6.08667V6.08974V6.09283V6.09594V6.09908V6.10223V6.1054V6.1086V6.11181V6.11505V6.1183V6.12158V6.12488V6.12819V6.13153V6.13489V6.13826V6.14166V6.14507V6.14851V6.15197V6.15544V6.15893V6.16245V6.16598V6.16953V6.1731V6.17669V6.1803V6.18393V6.18757V6.19124V6.19492V6.19862V6.20234V6.20608V6.20984V6.21361V6.21741V6.22122V6.22505V6.2289V6.23276V6.23664V6.24054V6.24446V6.24839V6.25235V6.25632V6.2603V6.26431V6.26833V6.27237V6.27642V6.28049V6.28458V6.28869V6.29281V6.29695V6.3011V6.30527V6.30946V6.31366V6.31788V6.32212V6.32637V6.33064V6.33492V6.33922V6.34353V6.34786V6.35221V6.35657V6.36095V6.36534V6.36974V6.37416V6.3786V6.38305V6.38752V6.392V6.39649V6.401V6.40553V6.41007V6.41462V6.41919V6.42377V6.42836V6.43297V6.4376V6.44223V6.44689V6.45155V6.45623V6.46092V6.46562V6.47034V6.47508V6.47982V6.48458V6.48935V6.49413V6.49893V6.50374V6.50856V6.5134V6.51825V6.52311V6.52798V6.53286V6.53776V6.54267V6.54759V6.55252V6.55747V6.56243V6.5674V6.57238V6.57737V6.58237V6.58739V6.59241V6.59745V6.6025V6.60756V6.61263V6.61771V6.6228V6.62791V6.63302V6.63814V6.64328V6.64843V6.65358V6.65875V6.66392V6.66911V6.67431V6.67951V6.68473V6.68996V6.69519V6.70044V6.70569V6.71096V6.71623V6.72152V6.72681V6.73211V6.73742V6.74274V6.74807V6.75341V6.75876V6.76411V6.76948V6.77485V6.78023V6.78562V6.79102V6.79642V6.80184V6.80726V6.81269V6.81812V6.82357V6.82902V6.83448V6.83995V6.84543V6.85091V6.8564V6.8619V6.8674V6.87292V6.87844V6.88396V6.88949V6.89503V6.90058V6.90613V6.91169V6.91726V6.92283V6.92841V6.93399V6.93958V6.94518V6.95078V6.95639V6.96201V6.96763V6.97325V6.97888V6.98452V6.99016V6.99581V7.00146V7.00712V7.01278V7.01845V7.02412V7.0298V7.03548V7.04117V7.04686V7.05256V7.05826V7.06396V7.06967V7.07539V7.0811V7.08682V7.09255V7.09828V7.10401V7.10975V7.11549V7.12123V7.12698V7.13273V7.13848V7.14424V7.15V7.15576V7.16153V7.1673V7.17307V7.17884V7.18462V7.1904V7.19618V7.20197V7.20776V7.21355V7.21934V7.22513V7.23093V7.23672V7.24252V7.24832V7.25413V7.25993V7.26574V7.27154V7.27735V7.28316V7.28897V7.29479V7.3006V7.30641V7.31223V7.31805V7.32386V7.32968V7.3355V7.34132V7.34714V7.35295V7.35877V7.36459V7.37041V7.37623V7.38205V7.38787V7.39369V7.39951V7.40533V7.41115V7.41696V7.42278V7.4286V7.43441V7.44022V7.44604V7.45185V7.45766V7.46347V7.46928V7.47509V7.48089V7.4867V7.4925V7.4983V7.5041V7.5099V7.51569V7.52148V7.52728V7.53306V7.53885V7.54464V7.55042V7.5562V7.56197V7.56775V7.57352V7.57929V7.58505V7.59082V7.59658V7.60233V7.60809V7.61384V7.61958V7.62533V7.63107V7.6368V7.64253V7.64826V7.65399V7.65971V7.66543V7.67114V7.67685V7.68255V7.68825V7.69394V7.69964V7.70532V7.711V7.71668V7.72235V7.72802V7.73368V7.73934V7.74499V7.75064V7.75628V7.76191V7.76754V7.77317V7.77879V7.7844V7.79001V7.79561V7.80121V7.8068V7.81238V7.81796V7.82353V7.82909V7.83465V7.8402V7.84575V7.85129V7.85682V7.86235V7.86786V7.87337V7.87888V7.88438V7.88986V7.89535V7.90082V7.90629V7.91175V7.9172V7.92265V7.92808V7.93351V7.93893V7.94434V7.94975V7.95514V7.96053V7.96591V7.97128V7.97665V7.982V7.98734V7.99268V7.99801V8.00333V8.00864V8.01394V8.01923V8.02451V8.02979V8.03505V8.0403V8.04555V8.05078V8.05601V8.06122V8.06643V8.07162V8.07681V8.08198V8.08715V8.0923V8.09744V8.10258V8.1077V8.11281V8.11792V8.12301V8.12809V8.13316V8.13821V8.14326V8.1483V8.15332V8.15833V8.16334V8.16833V8.17331V8.17827V8.18323V8.18817V8.1931V8.19802V8.20293V8.20782V8.21271V8.21758V8.22244V8.22728V8.23212V8.23694V8.24174V8.24654V8.25132V8.25609V8.26085V8.26559V8.27032V8.27504V8.27974V8.28443V8.28911V8.29377V8.29842V8.30305V8.30767V8.31228V8.31687V8.32145V8.32602V8.33057V8.33511V8.33963V8.34414V8.34863V8.35311V8.35757V8.36202V8.36646V8.37087V8.37528V8.37967V8.38404V8.3884V8.39274V8.39707V8.40138V8.40568V8.40996V8.41422V8.41847V8.42271V8.42692V8.43112V8.43531V8.43948V8.44363V8.44777V8.45189V8.45599V8.46007V8.46414V8.46819V8.47223V8.47625V8.48025V8.48424V8.4882V8.49215V8.49609V8.5C9.87494 8.84518 9.59512 9.125 9.24994 9.125C8.90476 9.125 8.62494 8.84518 8.62494 8.5V8.49609V8.49215V8.4882V8.48424V8.48025V8.47625V8.47223V8.46819V8.46414V8.46007V8.45599V8.45189V8.44777V8.44363V8.43948V8.43531V8.43112V8.42692V8.42271V8.41847V8.41422V8.40996V8.40568V8.40138V8.39707V8.39274V8.3884V8.38404V8.37967V8.37528V8.37087V8.36646V8.36202V8.35757V8.35311V8.34863V8.34414V8.33963V8.33511V8.33057V8.32602V8.32145V8.31687V8.31228V8.30767V8.30305V8.29842V8.29377V8.28911V8.28443V8.27974V8.27504V8.27032V8.26559V8.26085V8.25609V8.25132V8.24654V8.24174V8.23694V8.23212V8.22728V8.22244V8.21758V8.21271V8.20782V8.20293V8.19802V8.1931V8.18817V8.18323V8.17827V8.17331V8.16833V8.16334V8.15833V8.15332V8.1483V8.14326V8.13821V8.13316V8.12809V8.12301V8.11792V8.11281V8.1077V8.10258V8.09744V8.0923V8.08715V8.08198V8.07681V8.07162V8.06643V8.06122V8.05601V8.05078V8.04555V8.0403V8.03505V8.02979V8.02451V8.01923V8.01394V8.00864V8.00333V7.99801V7.99268V7.98734V7.982V7.97665V7.97128V7.96591V7.96053V7.95514V7.94975V7.94434V7.93893V7.93351V7.92808V7.92265V7.9172V7.91175V7.90629V7.90082V7.89535V7.88986V7.88438V7.87888V7.87337V7.86786V7.86235V7.85682V7.85129V7.84575V7.8402V7.83465V7.82909V7.82353V7.81796V7.81238V7.8068V7.80121V7.79561V7.79001V7.7844V7.77879V7.77317V7.76754V7.76191V7.75628V7.75064V7.74499V7.73934V7.73368V7.72802V7.72235V7.71668V7.711V7.70532V7.69964V7.69394V7.68825V7.68255V7.67685V7.67114V7.66543V7.65971V7.65399V7.64826V7.64253V7.6368V7.63107V7.62533V7.61958V7.61384V7.60809V7.60233V7.59658V7.59082V7.58505V7.57929V7.57352V7.56775V7.56197V7.5562V7.55042V7.54464V7.53885V7.53306V7.52728V7.52148V7.51569V7.5099V7.5041V7.4983V7.4925V7.4867V7.48089V7.47509V7.46928V7.46347V7.45766V7.45185V7.44604V7.44022V7.43441V7.4286V7.42278V7.41696V7.41115V7.40533V7.39951V7.39369V7.38787V7.38205V7.37623V7.37041V7.36459V7.35877V7.35295V7.34714V7.34132V7.3355V7.32968V7.32386V7.31805V7.31223V7.30641V7.3006V7.29479V7.28897V7.28316V7.27735V7.27154V7.26574V7.25993V7.25413V7.24832V7.24252V7.23672V7.23093V7.22513V7.21934V7.21355V7.20776V7.20197V7.19618V7.1904V7.18462V7.17884V7.17307V7.1673V7.16153V7.15576V7.15V7.14424V7.13848V7.13273V7.12698V7.12123V7.11549V7.10975V7.10401V7.09828V7.09255V7.08682V7.0811V7.07539V7.06967V7.06396V7.05826V7.05256V7.04686V7.04117V7.03548V7.0298V7.02412V7.01845V7.01278V7.00712V7.00146V6.99581V6.99016V6.98452V6.97888V6.97325V6.96763V6.96201V6.95639V6.95078V6.94518V6.93958V6.93399V6.92841V6.92283V6.91726V6.91169V6.90613V6.90058V6.89503V6.88949V6.88396V6.87844V6.87292V6.8674V6.8619V6.8564V6.85091V6.84543V6.83995V6.83448V6.82902V6.82357V6.81812V6.81269V6.80726V6.80184V6.79642V6.79102V6.78562V6.78023V6.77485V6.76948V6.76411V6.75876V6.75341V6.74807V6.74274V6.73742V6.73211V6.72681V6.72152V6.71623V6.71096V6.70569V6.70044V6.69519V6.68996V6.68473V6.67951V6.67431V6.66911V6.66392V6.65875V6.65358V6.64843V6.64328V6.63814V6.63302V6.62791V6.6228V6.61771V6.61263V6.60756V6.6025V6.59745V6.59241V6.58739V6.58237V6.57737V6.57238V6.5674V6.56243V6.55747V6.55252V6.54759V6.54267V6.53776V6.53286V6.52798V6.52311V6.51825V6.5134V6.50856V6.50374V6.49893V6.49413V6.48935V6.48458V6.47982V6.47508V6.47034V6.46562V6.46092V6.45623V6.45155V6.44689V6.44223V6.4376V6.43297V6.42836V6.42377V6.41919V6.41462V6.41007V6.40553V6.401V6.39649V6.392V6.38752V6.38305V6.3786V6.37416V6.36974V6.36534V6.36095V6.35657V6.35221V6.34786V6.34353V6.33922V6.33492V6.33064V6.32637V6.32212V6.31788V6.31366V6.30946V6.30527V6.3011V6.29695V6.29281V6.28869V6.28458V6.28049V6.27642V6.27237V6.26833V6.26431V6.2603V6.25632V6.25235V6.24839V6.24446V6.24054V6.23664V6.23276V6.2289V6.22505V6.22122V6.21741V6.21361V6.20984V6.20608V6.20234V6.19862V6.19492V6.19124V6.18757V6.18393V6.1803V6.17669V6.1731V6.16953V6.16598V6.16245V6.15893V6.15544V6.15197V6.14851V6.14507V6.14166V6.13826V6.13489V6.13153V6.12819V6.12488V6.12158V6.1183V6.11505V6.11181V6.1086V6.1054V6.10223V6.09908V6.09594V6.09283V6.08974V6.08667V6.08362V6.08059V6.07759V6.0746V6.07164V6.0687V6.06578V6.06288V6.06V6.05714V6.05431V6.0515V6.04871V6.04594V6.0432V6.04047V6.03777V6.03509V6.03244V6.0298V6.02719V6.02461V6.02204V6.0195V6.01698V6.01449V6.01201V6.00956V6.00714V6.00473V6.00236V6C8.62494 5.65482 8.90476 5.375 9.24994 5.375ZM12.1248 5C12.1248 4.65482 11.845 4.375 11.4998 4.375C11.1546 4.375 10.8748 4.65482 10.8748 5V9.5C10.8748 9.84518 11.1546 10.125 11.4998 10.125C11.845 10.125 12.1248 9.84518 12.1248 9.5V5ZM13.25 10C13.6642 10 14 10.3358 14 10.75V12.25C14 12.7141 13.8156 13.1592 13.4874 13.4874C13.1592 13.8156 12.7141 14 12.25 14H10.75C10.3358 14 10 13.6642 10 13.25C10 12.8358 10.3358 12.5 10.75 12.5H12.25C12.3163 12.5 12.3799 12.4737 12.4268 12.4268C12.4737 12.3799 12.5 12.3163 12.5 12.25V10.75C12.5 10.3358 12.8358 10 13.25 10ZM0.75 10C1.16421 10 1.5 10.3358 1.5 10.75V12.25C1.5 12.3163 1.52634 12.3799 1.57322 12.4268C1.62011 12.4737 1.6837 12.5 1.75 12.5H3.25C3.66421 12.5 4 12.8358 4 13.25C4 13.6642 3.66421 14 3.25 14H1.75C1.28587 14 0.840752 13.8156 0.512563 13.4874C0.184375 13.1592 0 12.7141 0 12.25V10.75C0 10.3358 0.335786 10 0.75 10ZM10.75 0C10.3358 0 10 0.335786 10 0.75C10 1.16421 10.3358 1.5 10.75 1.5H12.25C12.3163 1.5 12.3799 1.52634 12.4268 1.57322C12.4737 1.62011 12.5 1.6837 12.5 1.75V3.25C12.5 3.66421 12.8358 4 13.25 4C13.6642 4 14 3.66421 14 3.25V1.75C14 1.28587 13.8156 0.840752 13.4874 0.512563C13.1592 0.184375 12.7141 0 12.25 0H10.75Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186870\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"waning-cresent-moon\", \"keywords\": [ \"night\", \"new\", \"moon\", \"crescent\", \"weather\", \"time\", \"waning\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9 0C7.14348 0 5.36301 0.737498 4.05025 2.05025C2.7375 3.36301 2 5.14348 2 7C2 8.85652 2.7375 10.637 4.05025 11.9497C5.36301 13.2625 7.14348 14 9 14H9.00352C9.91581 13.9936 10.8183 13.8109 11.6612 13.462C11.8367 13.3893 11.9559 13.2235 11.9688 13.034C11.9818 12.8445 11.8862 12.664 11.7221 12.5682C10.7476 11.9992 9.93806 11.1862 9.37309 10.2093C8.80855 9.23316 8.50768 8.12696 8.50001 6.99939C8.50482 5.87449 8.80148 4.77009 9.361 3.79417C9.92095 2.81752 10.725 2.00305 11.6943 1.43051C11.8574 1.33414 11.9521 1.15381 11.9388 0.964783C11.9254 0.775755 11.8063 0.610517 11.6313 0.538028C10.7984 0.193191 9.90726 0.0105887 9.00585 3.42727e-05L9 0Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"warning-octagon\", \"keywords\": [ \"frame\", \"alert\", \"warning\", \"octagon\", \"exclamation\", \"caution\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186864)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.85355 0.43934C4.13486 0.158035 4.51639 0 4.91421 0H9.08579C9.48361 0 9.86514 0.158035 10.1464 0.43934L13.5607 3.85355C13.842 4.13486 14 4.51639 14 4.91421V9.08579C14 9.48361 13.842 9.86514 13.5607 10.1464L10.1464 13.5607C9.86514 13.842 9.48361 14 9.08579 14H4.91421C4.51639 14 4.13486 13.842 3.85355 13.5607L0.43934 10.1464C0.158035 9.86514 0 9.48361 0 9.08579V4.91421C0 4.51639 0.158035 4.13486 0.43934 3.85355L3.85355 0.43934ZM7 3.125C7.41421 3.125 7.75 3.46079 7.75 3.875V7.125C7.75 7.53921 7.41421 7.875 7 7.875C6.58579 7.875 6.25 7.53921 6.25 7.125V3.875C6.25 3.46079 6.58579 3.125 7 3.125ZM8 9.875C8 10.4273 7.55228 10.875 7 10.875C6.44772 10.875 6 10.4273 6 9.875C6 9.32272 6.44772 8.875 7 8.875C7.55228 8.875 8 9.32272 8 9.875Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186864\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"warning-triangle\", \"keywords\": [ \"frame\", \"alert\", \"warning\", \"triangle\", \"exclamation\", \"caution\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186855)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.21854 0.226033C6.45409 0.0820473 6.72481 0.00585938 7.00088 0.00585938C7.27695 0.00585938 7.54766 0.0820473 7.78321 0.226033C8.01876 0.370019 8.21 0.576216 8.33588 0.821919L8.33811 0.826281L13.8374 11.8249C13.9522 12.053 14.0073 12.3075 13.9966 12.5627C13.9859 12.8182 13.9101 13.0667 13.7763 13.2846C13.6425 13.5025 13.4553 13.6826 13.2323 13.8078C13.0094 13.933 12.7581 13.9991 12.5024 13.9999H12.5009H1.50088H1.49936C1.24367 13.9991 0.992418 13.933 0.769467 13.8078C0.546517 13.6826 0.359271 13.5025 0.225511 13.2846C0.0917531 13.0667 0.0159223 12.8182 0.00522075 12.5627C-0.0054683 12.3075 0.0491743 12.0538 0.163963 11.8257L5.66366 0.826292L5.66585 0.821908C5.79172 0.576205 5.983 0.370019 6.21854 0.226033ZM7.00085 4.125C7.41506 4.125 7.75085 4.46079 7.75085 4.875V8.125C7.75085 8.53921 7.41506 8.875 7.00085 8.875C6.58664 8.875 6.25085 8.53921 6.25085 8.125V4.875C6.25085 4.46079 6.58664 4.125 7.00085 4.125ZM8.00085 10.875C8.00085 11.4273 7.55313 11.875 7.00085 11.875C6.44857 11.875 6.00085 11.4273 6.00085 10.875C6.00085 10.3227 6.44857 9.875 7.00085 9.875C7.55313 9.875 8.00085 10.3227 8.00085 10.875Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186855\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" } ], \"mail\": [ { \"name\": \"chat-bubble-oval\", \"keywords\": [ \"messages\", \"message\", \"bubble\", \"chat\", \"oval\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187338)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.0206 0.735676C9.07788 0.281448 8.04479 0.0458533 6.99834 0.0464489C5.74059 0.0468668 4.50635 0.388417 3.42736 1.03472C2.34832 1.68106 1.4648 2.60797 0.870894 3.71673C0.276994 4.82549 -0.00504529 6.07458 0.0548176 7.33096C0.110558 8.5008 0.460779 9.63617 1.07113 10.6325L0.076897 13.2777C0.0135827 13.4461 0.0456747 13.6356 0.160928 13.7738C0.27618 13.912 0.456812 13.9776 0.633897 13.9456L4.09988 13.3189C4.9992 13.732 5.97696 13.9486 6.96787 13.9535C8.01435 13.9587 9.04851 13.7276 9.99326 13.2775C10.938 12.8273 11.769 12.1698 12.4242 11.3538C13.0794 10.5378 13.542 9.58445 13.7775 8.56479C14.013 7.54514 14.0153 6.48548 13.7843 5.4648C13.5532 4.44413 13.0948 3.48877 12.4431 2.66994C11.7915 1.85111 10.9634 1.18992 10.0206 0.735676Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187338\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"chat-bubble-oval-notification\", \"keywords\": [ \"messages\", \"message\", \"bubble\", \"chat\", \"oval\", \"notify\", \"ping\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187329)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.99834 0.0464489C7.52186 0.0461525 8.04203 0.104969 8.54922 0.220701C8.04571 0.855699 7.74501 1.65881 7.74501 2.53216C7.74501 4.58843 9.41195 6.25537 11.4682 6.25537C12.3421 6.25537 13.1457 5.95427 13.7809 5.45015L13.7843 5.4648C14.0153 6.48548 14.013 7.54514 13.7775 8.56479C13.542 9.58445 13.0794 10.5378 12.4242 11.3538C11.769 12.1698 10.938 12.8273 9.99325 13.2775C9.04851 13.7276 8.01435 13.9587 6.96787 13.9535C5.97696 13.9486 4.9992 13.732 4.09988 13.3189L0.633897 13.9456C0.456812 13.9776 0.27618 13.912 0.160928 13.7738C0.0456747 13.6356 0.0135827 13.4461 0.076897 13.2777L1.07113 10.6325C0.460779 9.63617 0.110558 8.5008 0.0548176 7.33096C-0.00504529 6.07458 0.276994 4.82549 0.870894 3.71673C1.4648 2.60797 2.34832 1.68106 3.42736 1.03473C4.50635 0.388419 5.74059 0.046869 6.99834 0.0464489ZM13.9504 2.53216C13.9504 3.90301 12.8391 5.0143 11.4682 5.0143C10.0974 5.0143 8.98608 3.90301 8.98608 2.53216C8.98608 1.16131 10.0974 0.0500185 11.4682 0.0500185C12.8391 0.0500185 13.9504 1.16131 13.9504 2.53216Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187329\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"chat-bubble-oval-smiley-1\", \"keywords\": [ \"messages\", \"message\", \"bubble\", \"chat\", \"oval\", \"smiley\", \"smile\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187332)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.998 0.060487C8.04234 0.0598926 9.07335 0.295013 10.0142 0.748326C10.9551 1.20166 11.7815 1.86151 12.4318 2.67869C13.0822 3.49588 13.5397 4.44931 13.7703 5.46793C14.0009 6.48655 13.9985 7.54407 13.7635 8.56167C13.5285 9.57927 13.0668 10.5307 12.4129 11.345C11.759 12.1594 10.9297 12.8156 9.98688 13.2648C9.04404 13.7141 8.01197 13.9447 6.96759 13.9395C5.97879 13.9346 5.00311 13.7185 4.10569 13.3063L0.647569 13.9316C0.470484 13.9636 0.289852 13.898 0.174599 13.7598C0.0593466 13.6216 0.0272546 13.4321 0.0905689 13.2637L1.08246 10.6247C0.473507 9.63055 0.124088 8.49763 0.0684692 7.33032C0.00872697 6.07647 0.290197 4.8299 0.882902 3.72338C1.47561 2.61685 2.35735 1.69181 3.43421 1.04677C4.51104 0.401766 5.74278 0.0609042 6.998 0.060487ZM4.30215 7.83272C4.20976 7.50014 3.86526 7.30542 3.53267 7.3978C3.20009 7.49019 3.00537 7.83469 3.09775 8.16728C3.69245 10.3082 6.04302 11.5923 8.16723 11.0022C8.18285 10.9979 8.1983 10.9929 8.21354 10.9874C9.48093 10.5265 10.5433 9.48313 10.9029 8.16445C10.9938 7.83143 10.7974 7.48785 10.4644 7.39702C10.1314 7.3062 9.7878 7.50254 9.69697 7.83555C9.4586 8.7096 8.73111 9.46101 7.80939 9.80415C6.33958 10.1981 4.70532 9.28415 4.30215 7.83272ZM5.4375 4.96875C5.4375 5.38296 5.10171 5.71875 4.6875 5.71875C4.27329 5.71875 3.9375 5.38296 3.9375 4.96875C3.9375 4.55454 4.27329 4.21875 4.6875 4.21875C5.10171 4.21875 5.4375 4.55454 5.4375 4.96875ZM9.3125 5.71875C9.72671 5.71875 10.0625 5.38296 10.0625 4.96875C10.0625 4.55454 9.72671 4.21875 9.3125 4.21875C8.89829 4.21875 8.5625 4.55454 8.5625 4.96875C8.5625 5.38296 8.89829 5.71875 9.3125 5.71875Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187332\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"chat-bubble-oval-smiley-2\", \"keywords\": [ \"messages\", \"message\", \"bubble\", \"chat\", \"oval\", \"smiley\", \"smile\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187347)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.998 0.060487C8.04234 0.0598926 9.07335 0.295013 10.0142 0.748326C10.9551 1.20166 11.7815 1.86151 12.4318 2.67869C13.0822 3.49588 13.5397 4.44931 13.7703 5.46793C14.0009 6.48655 13.9985 7.54407 13.7635 8.56167C13.5285 9.57927 13.0668 10.5307 12.4129 11.345C11.759 12.1594 10.9297 12.8156 9.98688 13.2648C9.04404 13.7141 8.01197 13.9447 6.96759 13.9395C5.97879 13.9346 5.00311 13.7185 4.10569 13.3063L0.647569 13.9316C0.470484 13.9636 0.289852 13.898 0.174599 13.7598C0.0593466 13.6216 0.0272546 13.4321 0.0905689 13.2637L1.08246 10.6247C0.473507 9.63055 0.124088 8.49763 0.0684692 7.33032C0.00872697 6.07647 0.290197 4.8299 0.882902 3.72338C1.47561 2.61685 2.35735 1.69181 3.43421 1.04677C4.51104 0.401766 5.74278 0.0609042 6.998 0.060487ZM4.75 5.3125C5.16421 5.3125 5.5 4.97671 5.5 4.5625C5.5 4.14829 5.16421 3.8125 4.75 3.8125C4.33579 3.8125 4 4.14829 4 4.5625C4 4.97671 4.33579 5.3125 4.75 5.3125ZM9.96875 4.5625C9.96875 4.97671 9.63296 5.3125 9.21875 5.3125C8.80454 5.3125 8.46875 4.97671 8.46875 4.5625C8.46875 4.14829 8.80454 3.8125 9.21875 3.8125C9.63296 3.8125 9.96875 4.14829 9.96875 4.5625ZM10.5 7.5C10.5 9.433 8.933 11 7 11C5.067 11 3.5 9.433 3.5 7.5H10.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187347\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"chat-bubble-square-block\", \"keywords\": [ \"messages\", \"message\", \"bubble\", \"chat\", \"square\", \"block\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187326)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.50001 0C2.10219 0 1.72065 0.158035 1.43935 0.43934C1.15805 0.720644 1.00001 1.10218 1.00001 1.5V10.4189L0.0256687 13.3419C-0.0320399 13.515 0.00915603 13.7058 0.133149 13.8397C0.257142 13.9736 0.444236 14.0293 0.621278 13.9851L4.56156 13H12.5C12.8978 13 13.2794 12.842 13.5607 12.5607C13.842 12.2794 14 11.8978 14 11.5V1.5C14 1.10217 13.842 0.720644 13.5607 0.43934C13.2794 0.158035 12.8978 0 12.5 0H2.50001ZM5.33313 5.13983C5.09199 5.53672 4.95312 6.00261 4.95312 6.50095C4.95312 7.95069 6.12838 9.12595 7.57812 9.12595C8.07646 9.12595 8.54235 8.98708 8.93924 8.74594L5.33313 5.13983ZM4.7935 3.80623C4.11851 4.5036 3.70312 5.45376 3.70312 6.50095C3.70312 8.64105 5.43802 10.3759 7.57812 10.3759C8.62517 10.3759 9.57522 9.96067 10.2726 9.28584C10.2889 9.27248 10.3048 9.25817 10.3201 9.2429C10.3353 9.22763 10.3497 9.21177 10.363 9.19539C11.0378 8.49805 11.4531 7.54799 11.4531 6.50095C11.4531 4.36084 9.71823 2.62595 7.57812 2.62595C6.53094 2.62595 5.58077 3.04133 4.88341 3.71632C4.86713 3.72961 4.85137 3.74384 4.83619 3.75902C4.82102 3.77419 4.80679 3.78995 4.7935 3.80623ZM6.21701 4.25595L9.82312 7.86206C10.0643 7.46518 10.2031 6.99928 10.2031 6.50095C10.2031 5.0512 9.02787 3.87595 7.57812 3.87595C7.07979 3.87595 6.61389 4.01481 6.21701 4.25595Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187326\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"chat-bubble-square-question\", \"keywords\": [ \"bubble\", \"square\", \"messages\", \"notification\", \"chat\", \"message\", \"question\", \"help\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187387)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.43935 0.43934C1.72065 0.158035 2.10219 0 2.50001 0H12.5C12.8978 0 13.2794 0.158035 13.5607 0.43934C13.842 0.720644 14 1.10217 14 1.5V11.5C14 11.8978 13.842 12.2794 13.5607 12.5607C13.2794 12.842 12.8978 13 12.5 13H4.56156L0.621278 13.9851C0.444236 14.0293 0.257142 13.9736 0.133149 13.8397C0.00915603 13.7058 -0.0320399 13.515 0.0256687 13.3419L1.00001 10.4189V1.5C1.00001 1.10218 1.15805 0.720644 1.43935 0.43934ZM7.13206 3.62913C7.31603 3.55293 7.51847 3.53299 7.71378 3.57184C7.90909 3.61069 8.08849 3.70658 8.22929 3.84738C8.3701 3.98819 8.46599 4.16759 8.50484 4.3629C8.54369 4.5582 8.52375 4.76064 8.44755 4.94462C8.37134 5.12859 8.24229 5.28584 8.07672 5.39647C7.91115 5.5071 7.71649 5.56615 7.51736 5.56615C7.17218 5.56615 6.89236 5.84597 6.89236 6.19115V7.27903C6.89236 7.62421 7.17218 7.90403 7.51736 7.90403C7.86254 7.90403 8.14236 7.62421 8.14236 7.27903V6.72788C8.36457 6.66384 8.57682 6.56568 8.77118 6.4358C9.14232 6.18782 9.43158 5.83535 9.6024 5.42297C9.77321 5.01059 9.8179 4.55682 9.73082 4.11903C9.64374 3.68125 9.4288 3.27912 9.11318 2.9635C8.79755 2.64788 8.39542 2.43294 7.95764 2.34586C7.51986 2.25877 7.06609 2.30347 6.65371 2.47428C6.24132 2.6451 5.88886 2.93436 5.64087 3.30549C5.39289 3.67663 5.26053 4.11296 5.26053 4.55932C5.26053 4.9045 5.54035 5.18432 5.88553 5.18432C6.23071 5.18432 6.51053 4.9045 6.51053 4.55932C6.51053 4.36019 6.56958 4.16553 6.68021 3.99996C6.79084 3.83438 6.94809 3.70534 7.13206 3.62913ZM7.5174 10.6237C7.02295 10.6226 6.62247 10.2215 6.62247 9.72678C6.62247 9.23139 7.02406 8.8298 7.51944 8.8298L7.52146 8.82981C8.01591 8.83091 8.41639 9.23207 8.41639 9.72678C8.41639 10.2222 8.01481 10.6237 7.51942 10.6237H7.5174Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187387\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"chat-bubble-square-warning\", \"keywords\": [ \"bubble\", \"square\", \"messages\", \"notification\", \"chat\", \"message\", \"warning\", \"alert\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187390)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.43935 0.43934C1.72065 0.158035 2.10219 0 2.50001 0H12.5C12.8978 0 13.2794 0.158035 13.5607 0.43934C13.842 0.720644 14 1.10217 14 1.5V11.5C14 11.8978 13.842 12.2794 13.5607 12.5607C13.2794 12.842 12.8978 13 12.5 13H4.56156L0.621278 13.9851C0.444236 14.0293 0.257142 13.9736 0.133149 13.8397C0.00915603 13.7058 -0.0320399 13.515 0.0256687 13.3419L1.00001 10.4189V1.5C1.00001 1.10218 1.15805 0.720644 1.43935 0.43934ZM8.19324 2.79523C8.19324 2.45005 7.91342 2.17023 7.56824 2.17023C7.22306 2.17023 6.94324 2.45005 6.94324 2.79523V6.45386C6.94324 6.79904 7.22306 7.07886 7.56824 7.07886C7.91342 7.07886 8.19324 6.79904 8.19324 6.45386V2.79523ZM6.57056 9.58116C6.57056 9.03015 7.01724 8.58347 7.56825 8.58347C8.11926 8.58347 8.56594 9.03015 8.56594 9.58116C8.56594 10.1322 8.11926 10.5788 7.56825 10.5788C7.01724 10.5788 6.57056 10.1322 6.57056 9.58116Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187390\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"chat-bubble-square-write\", \"keywords\": [ \"messages\", \"message\", \"bubble\", \"chat\", \"square\", \"write\", \"review\", \"pen\", \"pencil\", \"compose\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187396)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.87991 8.38781C9.69934 8.56915 9.46746 8.69069 9.2156 8.73603L6.2156 9.27603C5.81595 9.34796 5.40626 9.22128 5.11696 8.93632C4.82767 8.65136 4.69483 8.24362 4.76073 7.84293L5.26073 4.80293C5.30331 4.54407 5.42625 4.30516 5.61215 4.12004L9.74969 0H2.50001C2.10219 0 1.72065 0.158035 1.43935 0.43934C1.15805 0.720644 1.00001 1.10218 1.00001 1.5V10.4189L0.0256687 13.3419C-0.0320399 13.515 0.00915601 13.7058 0.133149 13.8397C0.257142 13.9736 0.444236 14.0293 0.621278 13.9851L4.56156 13H12.5C12.8978 13 13.2794 12.842 13.5607 12.5607C13.842 12.2794 14 11.8978 14 11.5V4.25021L9.87991 8.38781ZM12.3187 0.0769069C12.1969 0.0261382 12.0662 0 11.9342 0C11.8022 0 11.6714 0.0261382 11.5496 0.0769069C11.4277 0.127676 11.3171 0.20207 11.2241 0.295798L6.49416 5.0058L5.99414 8.0458L8.99414 7.5058L13.7041 2.7758C13.7978 2.68284 13.8723 2.57223 13.923 2.45037C13.9738 2.32852 14 2.19781 14 2.0658C14 1.93379 13.9738 1.80308 13.923 1.68122C13.8723 1.55936 13.7979 1.44876 13.7042 1.3558L12.6442 0.295798C12.5512 0.20207 12.4406 0.127676 12.3187 0.0769069Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187396\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"chat-bubble-text-square\", \"keywords\": [ \"messages\", \"message\", \"bubble\", \"text\", \"square\", \"chat\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187372)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.50001 0C2.10219 0 1.72065 0.158035 1.43935 0.43934C1.15805 0.720644 1.00001 1.10218 1.00001 1.5V10.4189L0.0256687 13.3419C-0.0320399 13.515 0.00915603 13.7058 0.133149 13.8397C0.257142 13.9736 0.444236 14.0293 0.621278 13.9851L4.56156 13H12.5C12.8978 13 13.2794 12.842 13.5607 12.5607C13.842 12.2794 14 11.8978 14 11.5V1.5C14 1.10217 13.842 0.720644 13.5607 0.43934C13.2794 0.158035 12.8978 0 12.5 0H2.50001ZM3.875 4.84558C3.875 4.5004 4.15482 4.22058 4.5 4.22058H10.5C10.8452 4.22058 11.125 4.5004 11.125 4.84558C11.125 5.19076 10.8452 5.47058 10.5 5.47058H4.5C4.15482 5.47058 3.875 5.19076 3.875 4.84558ZM4.5 7.52942C4.15482 7.52942 3.875 7.80924 3.875 8.15442C3.875 8.4996 4.15482 8.77942 4.5 8.77942H8.5C8.84518 8.77942 9.125 8.4996 9.125 8.15442C9.125 7.80924 8.84518 7.52942 8.5 7.52942H4.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187372\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"chat-bubble-typing-oval\", \"keywords\": [ \"messages\", \"message\", \"bubble\", \"typing\", \"chat\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187353)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.99834 0.0464489C8.04479 0.0458544 9.07788 0.28145 10.0206 0.735677C10.9634 1.18992 11.7915 1.85111 12.4431 2.66994C13.0948 3.48877 13.5532 4.44413 13.7843 5.4648C14.0153 6.48548 14.013 7.54514 13.7775 8.56479C13.542 9.58445 13.0794 10.5378 12.4242 11.3538C11.769 12.1698 10.938 12.8273 9.99326 13.2775C9.04851 13.7276 8.01435 13.9587 6.96787 13.9535C5.97696 13.9486 4.9992 13.732 4.09988 13.3189L0.633897 13.9456C0.456812 13.9776 0.27618 13.912 0.160928 13.7738C0.0456747 13.6356 0.0135827 13.4461 0.076897 13.2777L1.07113 10.6325C0.460779 9.63617 0.110558 8.5008 0.0548176 7.33096C-0.00504529 6.07458 0.276994 4.82549 0.870894 3.71673C1.4648 2.60797 2.34832 1.68106 3.42736 1.03473C4.50635 0.388418 5.74059 0.046868 6.99834 0.0464489ZM5.0004 7C5.0004 7.55228 4.55268 8 4.0004 8C3.44812 8 3.0004 7.55228 3.0004 7C3.0004 6.44772 3.44812 6 4.0004 6C4.55268 6 5.0004 6.44772 5.0004 7ZM7.0004 8C7.55268 8 8.0004 7.55228 8.0004 7C8.0004 6.44772 7.55268 6 7.0004 6C6.44812 6 6.0004 6.44772 6.0004 7C6.0004 7.55228 6.44812 8 7.0004 8ZM10.0004 8C10.5527 8 11.0004 7.55228 11.0004 7C11.0004 6.44772 10.5527 6 10.0004 6C9.44812 6 9.0004 6.44772 9.0004 7C9.0004 7.55228 9.44812 8 10.0004 8Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187353\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"chat-two-bubbles-oval\", \"keywords\": [ \"messages\", \"message\", \"bubble\", \"chat\", \"oval\", \"conversation\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187344)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.5152 1.20238e-06C4.05249 1.20238e-06 2.64968 0.581065 1.61538 1.61536C0.581088 2.64966 2.7445e-05 4.05246 2.7445e-05 5.51518C-0.0034169 6.60078 0.31741 7.66267 0.921386 8.56475L0.184468 10.5368C0.103067 10.7546 0.289738 10.9782 0.51861 10.9371L3.1015 10.4724C3.35802 10.5974 3.62311 10.7021 3.89429 10.7857C3.79988 10.3723 3.75001 9.94197 3.75001 9.5C3.75001 6.32436 6.32437 3.75 9.50001 3.75C9.94382 3.75 10.3759 3.80028 10.7908 3.89546C10.5885 3.23678 10.2641 2.62064 9.83332 2.07968C9.31639 1.4305 8.65963 0.906342 7.91198 0.546259C7.16432 0.186176 6.34505 -0.000546718 5.5152 1.20238e-06ZM12.6822 6.31802C11.8382 5.47411 10.6936 5 9.50018 5C8.82308 4.99955 8.15461 5.15191 7.54457 5.44571C6.93454 5.73951 6.39867 6.16719 5.9769 6.69687C5.55512 7.22656 5.25831 7.84459 5.10859 8.50493C4.95887 9.16527 4.96008 9.85088 5.11216 10.5107C5.26423 11.1705 5.56323 11.7875 5.98689 12.3156C6.41055 12.8438 6.94794 13.2696 7.55901 13.5612C8.17009 13.8528 8.8391 14.0028 9.51619 14C10.1933 13.9971 10.861 13.8415 11.4696 13.5447L13.4816 13.9067C13.7105 13.9479 13.8971 13.7243 13.8157 13.5064L13.2484 11.9882C13.7412 11.2522 14.003 10.3858 14.0002 9.5C14.0002 8.30653 13.5261 7.16193 12.6822 6.31802Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187344\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"discussion-converstion-reply\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187335)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.01367 2.375C2.99475 2.375 2.9766 2.38252 2.96322 2.39589C2.94985 2.40927 2.94233 2.42742 2.94233 2.44634V5.17582L2.9972 5.12095C3.29009 4.82805 3.76497 4.82805 4.05786 5.12095C4.35075 5.41384 4.35075 5.88871 4.05786 6.18161L2.70914 7.53033C2.41624 7.82322 1.94137 7.82322 1.64848 7.53033L0.299748 6.18161C0.00685476 5.88871 0.00685482 5.41384 0.299748 5.12095C0.592641 4.82805 1.06752 4.82805 1.36041 5.12095L1.44233 5.20286V2.44634C1.44233 2.02959 1.60788 1.62992 1.90256 1.33523C2.19725 1.04055 2.59692 0.875 3.01367 0.875H11.0006C11.4173 0.875 11.817 1.04055 12.1117 1.33523C12.4064 1.62992 12.5719 2.02959 12.5719 2.44634V2.71116C12.5719 3.12537 12.2361 3.46116 11.8219 3.46116C11.4077 3.46116 11.0719 3.12537 11.0719 2.71116V2.44634C11.0719 2.42742 11.0644 2.40927 11.051 2.39589C11.0376 2.38252 11.0195 2.375 11.0006 2.375H3.01367ZM12.5719 6.73096L12.6408 6.79983C12.9337 7.09272 13.4086 7.09272 13.7014 6.79983C13.9943 6.50694 13.9943 6.03206 13.7014 5.73917L12.3527 4.39045C12.0598 4.09755 11.585 4.09755 11.2921 4.39045L9.94331 5.73917C9.65041 6.03206 9.65041 6.50694 9.94331 6.79983C10.2362 7.09272 10.7111 7.09272 11.004 6.79983L11.0719 6.73192V10.7323C11.0719 10.7512 11.0644 10.7694 11.051 10.7828C11.0376 10.7961 11.0195 10.8037 11.0006 10.8037H4.65634C4.59502 10.8037 4.53393 10.8112 4.47444 10.8261L2.51455 11.316L2.90384 10.1482C2.92933 10.0717 2.94233 9.9916 2.94233 9.91099V9.22387C2.94233 8.80966 2.60654 8.47387 2.19233 8.47387C1.77812 8.47387 1.44233 8.80966 1.44233 9.22387V9.78928L0.659475 12.1378C0.572912 12.3975 0.634706 12.6837 0.820695 12.8846C1.00669 13.0854 1.28733 13.169 1.55289 13.1026L4.74867 12.3037H11.0006C11.4173 12.3037 11.817 12.1381 12.1117 11.8434C12.4064 11.5487 12.5719 11.1491 12.5719 10.7323V6.73096Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187335\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"happy-face\", \"keywords\": [ \"smiley\", \"chat\", \"message\", \"smile\", \"emoji\", \"face\", \"satisfied\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187341)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14C10.866 14 14 10.866 14 7ZM10.4401 4.95151C10.4401 5.51651 9.98275 5.97469 9.41806 5.976L9.41563 5.97601C8.84981 5.97601 8.39113 5.51732 8.39113 4.95151C8.39113 4.3865 8.8485 3.92832 9.4132 3.92701L9.41562 3.927C9.98144 3.927 10.4401 4.38569 10.4401 4.95151ZM5.60889 4.95151C5.60889 5.51652 5.15151 5.9747 4.5868 5.976L4.58437 5.97601C4.01856 5.97601 3.55987 5.51732 3.55987 4.95151C3.55987 4.3865 4.01725 3.92831 4.58195 3.92701L4.58438 3.927C5.1502 3.927 5.60889 4.38569 5.60889 4.95151ZM4.18995 7.76046C4.10026 7.42714 3.75733 7.22965 3.42401 7.31935C3.09069 7.40904 2.8932 7.75197 2.9829 8.08529C3.45986 9.8577 5.07756 11.1631 7.00174 11.1631C8.92593 11.1631 10.5436 9.8577 11.0206 8.08529C11.1103 7.75197 10.9128 7.40904 10.5795 7.31935C10.2462 7.22965 9.90323 7.42714 9.81353 7.76046C9.47973 9.00087 8.34647 9.91306 7.00174 9.91306C5.65702 9.91306 4.52376 9.00087 4.18995 7.76046Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187341\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"inbox-block\", \"keywords\": [ \"mail\", \"envelope\", \"email\", \"message\", \"block\", \"spam\", \"remove\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187350)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.57534 0H10.8867C11.7438 0 12.4386 0.694808 12.4386 1.5519V1.62907L6.23102 4.73287L0.0234375 1.62908V1.5519C0.0234375 0.694808 0.718246 0 1.57534 0ZM6.51053 5.99066L12.4386 3.02661V6.22931C11.8375 5.9475 11.1665 5.79004 10.4588 5.79004C8.27609 5.79004 6.44317 7.28766 5.93153 9.31139H1.57534C0.718246 9.31139 0.0234375 8.61658 0.0234375 7.75949V3.02662L5.95151 5.99066C6.12747 6.07864 6.33458 6.07864 6.51053 5.99066ZM10.4588 8.43188C11.5786 8.43188 12.4865 9.33971 12.4865 10.4596C12.4865 10.7445 12.4277 11.0157 12.3216 11.2617L9.65667 8.59673C9.90269 8.49065 10.1739 8.43188 10.4588 8.43188ZM13.9865 10.4596C13.9865 11.4099 13.6107 12.2725 12.9996 12.9068C12.9855 12.9236 12.9706 12.9399 12.9549 12.9556C12.9391 12.9714 12.9228 12.9863 12.9061 13.0003C12.2718 13.6114 11.4092 13.9873 10.4588 13.9873C8.51053 13.9873 6.93112 12.4079 6.93112 10.4596C6.93112 9.50964 7.30659 8.6474 7.91722 8.01316C7.93151 7.99608 7.94668 7.97949 7.96273 7.96344C7.97878 7.94739 7.99537 7.93222 8.01246 7.91793C8.64669 7.30733 9.50891 6.93188 10.4588 6.93188C12.4071 6.93188 13.9865 8.51129 13.9865 10.4596ZM8.596 9.65737L11.261 12.3224C11.015 12.4285 10.7437 12.4873 10.4588 12.4873C9.33895 12.4873 8.43112 11.5794 8.43112 10.4596C8.43112 10.1746 8.4899 9.90341 8.596 9.65737Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187350\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"inbox-favorite\", \"keywords\": [ \"mail\", \"envelope\", \"email\", \"message\", \"star\", \"favorite\", \"important\", \"bookmark\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187384)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.55971 0H10.8711C11.7282 0 12.423 0.694808 12.423 1.5519V1.62907L6.21539 4.73287L0.0078125 1.62908V1.5519C0.0078125 0.694808 0.702621 0 1.55971 0ZM6.4949 5.99066L12.423 3.02661V7.75949C12.423 7.80622 12.4209 7.85246 12.4169 7.89813L12.2161 7.3868C12.0817 7.04222 11.8465 6.74613 11.5413 6.53719C11.2352 6.32768 10.8729 6.21558 10.502 6.21558C10.1311 6.21558 9.76876 6.32768 9.46267 6.53719C9.15743 6.74613 8.92226 7.0422 8.78782 7.38676L8.45986 8.22203H7.60805C7.25469 8.21933 6.90778 8.31734 6.608 8.5047C6.30521 8.69395 6.06329 8.96634 5.91114 9.28936L5.90102 9.31139H1.55971C0.702621 9.31139 0.0078125 8.61658 0.0078125 7.75949V3.02662L5.93588 5.99066C6.11184 6.07864 6.31895 6.07864 6.4949 5.99066ZM10.1687 7.5687C10.0706 7.63588 9.99501 7.73116 9.95196 7.84203L9.31196 9.47203H7.60196C7.48487 9.47047 7.3698 9.50264 7.2705 9.5647C7.1712 9.62676 7.09186 9.7161 7.04196 9.82203C6.99964 9.93383 6.99228 10.0558 7.02085 10.1719C7.04942 10.288 7.11258 10.3927 7.20196 10.472L8.71196 11.832L8.07196 13.112C8.01402 13.2244 7.99439 13.3526 8.01606 13.4772C8.03772 13.6017 8.09949 13.7158 8.19196 13.802C8.2838 13.8893 8.40055 13.9459 8.52599 13.9638C8.65143 13.9817 8.77935 13.9601 8.89196 13.902L10.502 13.022L12.112 13.932C12.2246 13.9901 12.3525 14.0117 12.4779 13.9938C12.6034 13.9759 12.7201 13.9193 12.812 13.832C12.9045 13.7458 12.9662 13.6318 12.9879 13.5072C13.0095 13.3826 12.9899 13.2544 12.932 13.142L12.292 11.862L13.802 10.512C13.8914 10.4327 13.9545 10.328 13.9831 10.2119C14.0117 10.0958 14.0043 9.97383 13.962 9.86203C13.9181 9.74862 13.8414 9.65089 13.7416 9.5814C13.6418 9.51191 13.5236 9.47382 13.402 9.47203H11.692L11.052 7.84203C11.0089 7.73116 10.9334 7.63588 10.8352 7.5687C10.7371 7.50152 10.6209 7.46558 10.502 7.46558C10.383 7.46558 10.2669 7.50152 10.1687 7.5687Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187384\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"inbox-favorite-heart\", \"keywords\": [ \"mail\", \"envelope\", \"email\", \"message\", \"heart\", \"favorite\", \"like\", \"love\", \"important\", \"bookmark\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187393)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.57924 0H10.8907C11.7477 0 12.4426 0.694808 12.4426 1.5519V1.62907L6.23492 4.73287L0.0273438 1.62908V1.5519C0.0273438 0.694808 0.722152 0 1.57924 0ZM6.51443 5.99066L12.4426 3.02661V6.81463C11.8942 6.73832 11.3169 6.83221 10.7774 7.1315C10.6302 7.21318 10.4904 7.30745 10.358 7.41329C10.2216 7.29969 10.0771 7.19857 9.92443 7.1111C9.22486 6.71014 8.456 6.65728 7.76655 6.88242C6.69405 7.23264 5.92622 8.19299 5.67882 9.31139H1.57924C0.722152 9.31139 0.0273438 8.61658 0.0273438 7.75949V3.02662L5.95542 5.99066C6.13137 6.07864 6.33848 6.07864 6.51443 5.99066ZM13.8434 10.1423C13.8434 8.19847 11.5134 6.89588 10.3434 9.49104C9.17332 6.84578 6.84332 8.14837 6.84332 10.0922C6.84332 12.6974 10.3434 14 10.3434 14C10.3434 14 13.8434 12.6974 13.8434 10.1423Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187393\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"inbox-lock\", \"keywords\": [ \"mail\", \"envelope\", \"email\", \"message\", \"secure\", \"password\", \"lock\", \"encryption\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187362)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.8907 0H1.57924C0.722152 0 0.0273438 0.694808 0.0273438 1.5519V1.62908L6.23492 4.73287L12.4426 1.62907V1.5519C12.4426 0.694808 11.7477 0 10.8907 0ZM12.4426 3.02661L6.51443 5.99066C6.33848 6.07864 6.13137 6.07864 5.95542 5.99066L0.0273438 3.02662V7.75949C0.0273438 8.61658 0.722152 9.31139 1.57924 9.31139H6.08877C6.20307 9.12812 6.34302 8.96255 6.50356 8.81963C6.59789 6.6942 8.35088 5 10.4996 5C11.2047 5 11.8673 5.18246 12.4426 5.50275V3.02661Z\\\" fill=\\\"black\\\"/>\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.25 9C9.25 8.30964 9.80964 7.75 10.5 7.75C11.1904 7.75 11.75 8.30964 11.75 9V9.5H9.25V9ZM7.75 9.5315V9C7.75 7.48122 8.98122 6.25 10.5 6.25C12.0188 6.25 13.25 7.48122 13.25 9V9.5315C13.6813 9.64252 14 10.034 14 10.5V13C14 13.5523 13.5523 14 13 14H8C7.44772 14 7 13.5523 7 13V10.5C7 10.034 7.31869 9.64252 7.75 9.5315Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187362\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"inbox-tray-1\", \"keywords\": [ \"mail\", \"email\", \"outbox\", \"drawer\", \"empty\", \"open\", \"inbox\", \"arrow\", \"down\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187375)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.75 0.759277C7.75 0.345064 7.41421 0.00927734 7 0.00927734C6.58579 0.00927734 6.25 0.345064 6.25 0.759277V3.50928H4.5C4.19665 3.50928 3.92318 3.69201 3.80709 3.97226C3.691 4.25252 3.75517 4.57511 3.96967 4.78961L6.46967 7.28961C6.54158 7.36152 6.62445 7.41577 6.71291 7.45237C6.80134 7.48904 6.89831 7.50928 7 7.50928C7.10169 7.50928 7.19866 7.48904 7.28709 7.45237C7.37555 7.41577 7.45842 7.36152 7.53033 7.28961L10.0303 4.78961C10.2448 4.57511 10.309 4.25252 10.1929 3.97226C10.0768 3.69201 9.80335 3.50928 9.5 3.50928H7.75V0.759277ZM0.5 8.32983C0.367392 8.32983 0.240215 8.38251 0.146447 8.47628C0.0526784 8.57005 0 8.69723 0 8.82983V12.5C0 12.8979 0.158035 13.2794 0.43934 13.5607C0.720644 13.842 1.10217 14 1.5 14H12.5C12.8978 14 13.2794 13.842 13.5607 13.5607C13.842 13.2794 14 12.8979 14 12.5V8.82983C14 8.69723 13.9473 8.57005 13.8536 8.47628C13.7598 8.38251 13.6326 8.32983 13.5 8.32983H10.5267C10.1288 8.32983 9.7473 8.48787 9.46599 8.76917C9.18469 9.05048 9.02666 9.43201 9.02666 9.82983C9.02666 10.8041 8.05116 11.5618 6.99771 11.5547C5.97317 11.5478 5.02666 10.7865 5.02666 9.82983C5.02666 9.43201 4.86862 9.05048 4.58732 8.76917C4.30601 8.48787 3.92448 8.32983 3.52666 8.32983H0.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187375\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"inbox-tray-2\", \"keywords\": [ \"mail\", \"email\", \"outbox\", \"drawer\", \"empty\", \"open\", \"inbox\", \"arrow\", \"up\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187369)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.1929 3.54629C10.0768 3.82655 9.80335 4.00928 9.5 4.00928H7.75V6.75928C7.75 7.17349 7.41421 7.50928 7 7.50928C6.58579 7.50928 6.25 7.17349 6.25 6.75928V4.00928H4.5C4.19665 4.00928 3.92318 3.82655 3.80709 3.54629C3.691 3.26603 3.75517 2.94345 3.96967 2.72895L6.46967 0.228947C6.76256 -0.0639459 7.23744 -0.0639459 7.53033 0.228947L10.0303 2.72895C10.2448 2.94345 10.309 3.26603 10.1929 3.54629ZM0.5 8.32983C0.367392 8.32983 0.240215 8.38251 0.146447 8.47628C0.0526784 8.57005 0 8.69723 0 8.82983V12.5C0 12.8979 0.158035 13.2794 0.43934 13.5607C0.720644 13.842 1.10217 14 1.5 14H12.5C12.8978 14 13.2794 13.842 13.5607 13.5607C13.842 13.2794 14 12.8979 14 12.5V8.82983C14 8.69723 13.9473 8.57005 13.8536 8.47628C13.7598 8.38251 13.6326 8.32983 13.5 8.32983H10.5267C10.1288 8.32983 9.7473 8.48787 9.46599 8.76917C9.18469 9.05048 9.02666 9.43201 9.02666 9.82983C9.02666 10.8041 8.05116 11.5618 6.99771 11.5547C5.97317 11.5478 5.02666 10.7865 5.02666 9.82983C5.02666 9.43201 4.86862 9.05048 4.58732 8.76917C4.30601 8.48787 3.92448 8.32983 3.52666 8.32983H0.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187369\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"mail-incoming\", \"keywords\": [ \"inbox\", \"envelope\", \"email\", \"message\", \"down\", \"arrow\", \"inbox\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187356)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.99973 0.0422363C7.41394 0.0422363 7.74973 0.378023 7.74973 0.792236V3.20007H8.9205C9.12273 3.20007 9.30505 3.32189 9.38244 3.50873C9.45983 3.69557 9.41704 3.91063 9.27404 4.05363L7.35329 5.97438C7.15803 6.16965 6.84145 6.16965 6.64619 5.97438L4.72543 4.05363C4.58243 3.91063 4.53965 3.69557 4.61704 3.50873C4.69444 3.32189 4.87675 3.20007 5.07898 3.20007H6.24973V0.792236C6.24973 0.378023 6.58552 0.0422363 6.99973 0.0422363ZM2.08539 4.17139H3.39364C3.47296 4.45517 3.62417 4.72013 3.84155 4.93751L5.76231 6.85827C6.44573 7.54168 7.55377 7.54168 8.23718 6.85827L10.1579 4.93751C10.3753 4.72013 10.5265 4.45517 10.6058 4.17139H11.914C12.8188 4.17139 13.5522 4.9048 13.5522 5.8095V5.95502L6.99972 9.77334L0.447266 5.95502V5.8095C0.447266 4.9048 1.18068 4.17139 2.08539 4.17139ZM0.447266 7.40177V12.362C0.447266 13.2667 1.18068 14.0001 2.08539 14.0001H11.914C12.8188 14.0001 13.5522 13.2667 13.5522 12.362V7.40177L7.3144 11.0367C7.11992 11.15 6.87953 11.15 6.68505 11.0367L0.447266 7.40177Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187356\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"mail-search\", \"keywords\": [ \"inbox\", \"envelope\", \"email\", \"message\", \"search\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187378)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.56557 0H10.877C11.7341 0 12.4289 0.694808 12.4289 1.5519V1.62907L6.22125 4.73287L0.0136719 1.62908V1.5519C0.0136719 0.694808 0.708481 0 1.56557 0ZM6.50076 5.99066L12.4289 3.02661V6.22021C11.7253 5.76456 10.8865 5.5 9.9859 5.5C7.73476 5.5 5.8697 7.15298 5.53825 9.31139H1.56557C0.708481 9.31139 0.0136719 8.61658 0.0136719 7.75949V3.02662L5.94174 5.99066C6.1177 6.07864 6.3248 6.07864 6.50076 5.99066ZM9.9859 8.25C9.0194 8.25 8.2359 9.0335 8.2359 10C8.2359 10.9665 9.0194 11.75 9.9859 11.75C10.9524 11.75 11.7359 10.9665 11.7359 10C11.7359 9.0335 10.9524 8.25 9.9859 8.25ZM6.7359 10C6.7359 8.20507 8.19098 6.75 9.9859 6.75C11.7808 6.75 13.2359 8.20507 13.2359 10C13.2359 10.6216 13.0614 11.2024 12.7588 11.6961L13.7803 12.7204C14.0727 13.0137 14.0721 13.4886 13.7788 13.7811C13.4855 14.0735 13.0106 14.0729 12.7181 13.7796L11.7019 12.7606C11.2039 13.0708 10.6158 13.25 9.9859 13.25C8.19098 13.25 6.7359 11.7949 6.7359 10Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187378\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"mail-send-email-message\", \"keywords\": [ \"send\", \"email\", \"paper\", \"airplane\", \"deliver\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187405)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.8536 0.146461C13.9933 0.286181 14.0376 0.49508 13.9667 0.679504L8.96667 13.6795C8.89435 13.8675 8.71624 13.9938 8.51486 13.9998C8.31348 14.0058 8.12819 13.8903 8.04482 13.7069L5.95386 9.1068L9.03033 6.03033C9.32322 5.73744 9.32322 5.26256 9.03033 4.96967C8.73744 4.67678 8.26256 4.67678 7.96967 4.96967L4.89319 8.04615L0.2931 5.9552C0.109691 5.87183 -0.00576581 5.68653 0.000222247 5.48515C0.00621034 5.28378 0.132474 5.10566 0.320512 5.03334L13.3205 0.0333416C13.5049 -0.0375906 13.7138 0.00674078 13.8536 0.146461Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187405\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"mail-send-envelope\", \"keywords\": [ \"envelope\", \"email\", \"message\", \"unopened\", \"sealed\", \"close\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 2.75C0 1.92157 0.671573 1.25 1.5 1.25H12.5C13.3284 1.25 14 1.92157 14 2.75V3.09195L7.3834 7.5038C7.29008 7.5638 7.15462 7.60663 6.99999 7.60663C6.84537 7.60663 6.70991 7.5638 6.61659 7.5038L0 3.09196V2.75ZM0 4.59435V11.25C0 12.0784 0.671573 12.75 1.5 12.75H12.5C13.3284 12.75 14 12.0784 14 11.25V4.59434L8.0734 8.54611L8.06764 8.54995C7.75299 8.75476 7.37513 8.85663 6.99999 8.85663C6.62486 8.85663 6.24702 8.75473 5.93237 8.54992L5.92657 8.54614L0 4.59435Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"mail-send-reply-all\", \"keywords\": [ \"email\", \"message\", \"reply\", \"all\", \"actions\", \"action\", \"arrow\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.01451 2.55337C8.01451 2.25002 7.83178 1.97654 7.55152 1.86046C7.27127 1.74437 6.94868 1.80854 6.73418 2.02304L4.00696 4.75026C3.71407 5.04315 3.71407 5.51802 4.00696 5.81092L6.73418 8.53813C6.94868 8.75263 7.27127 8.8168 7.55152 8.70071C7.83178 8.58463 8.01451 8.31115 8.01451 8.0078V6.03057H9.59529C10.3607 6.03057 11.0949 6.33466 11.6362 6.87595C12.1774 7.41723 12.4815 8.15137 12.4815 8.91686V10.735C12.4815 11.1492 12.8173 11.485 13.2315 11.485C13.6457 11.485 13.9815 11.1492 13.9815 10.735V8.91686C13.9815 7.75354 13.5194 6.63787 12.6968 5.81528C11.8742 4.9927 10.7586 4.53057 9.59529 4.53057H8.01451V2.55337ZM4.02708 3.0837C4.31997 2.7908 4.31997 2.31593 4.02708 2.02304C3.73419 1.73014 3.25931 1.73014 2.96642 2.02304L0.239201 4.75026C-0.0536921 5.04315 -0.0536921 5.51802 0.239201 5.81092L2.96642 8.53813C3.25931 8.83103 3.73419 8.83103 4.02708 8.53813C4.31997 8.24524 4.31997 7.77037 4.02708 7.47747L1.83019 5.28059L4.02708 3.0837Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"sad-face\", \"keywords\": [ \"smiley\", \"chat\", \"message\", \"emoji\", \"sad\", \"face\", \"unsatisfied\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187381)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7ZM4.18995 10.7005C4.10026 11.0338 3.75733 11.2313 3.42401 11.1416C3.09069 11.0519 2.8932 10.709 2.9829 10.3756C3.45986 8.60323 5.07756 7.29787 7.00174 7.29787C8.92593 7.29787 10.5436 8.60323 11.0206 10.3756C11.1103 10.709 10.9128 11.0519 10.5795 11.1416C10.2462 11.2313 9.90323 11.0338 9.81353 10.7005C9.47973 9.46006 8.34647 8.54787 7.00174 8.54787C5.65702 8.54787 4.52376 9.46006 4.18995 10.7005ZM4.58194 5.976C4.01725 5.97469 3.55988 5.51651 3.55988 4.95151C3.55988 4.38569 4.01856 3.927 4.58438 3.927L4.5868 3.92701C5.1515 3.92832 5.60887 4.3865 5.60887 4.95151C5.60887 5.51732 5.15019 5.97601 4.58437 5.97601L4.58194 5.976ZM9.4132 5.976C8.84849 5.9747 8.39111 5.51652 8.39111 4.95151C8.39111 4.38569 8.8498 3.927 9.41562 3.927L9.41805 3.92701C9.98275 3.92831 10.4401 4.3865 10.4401 4.95151C10.4401 5.51732 9.98144 5.97601 9.41563 5.97601L9.4132 5.976Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187381\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"send-email\", \"keywords\": [ \"mail\", \"send\", \"email\", \"paper\", \"airplane\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187420)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.8211 0.0977331C12.1094 -0.00751778 12.4216 -0.0287134 12.7216 0.0367172C13.0245 0.102803 13.3021 0.254481 13.5213 0.473719C13.7405 0.692957 13.8922 0.970537 13.9583 1.27346C14.0237 1.57338 14.0025 1.88566 13.8973 2.17389L10.3233 12.8859C10.2362 13.1493 10.0827 13.3867 9.87783 13.5739C9.67353 13.7606 9.42482 13.8917 9.15545 13.9549C8.88603 14.0206 8.60414 14.015 8.33747 13.9388C8.07104 13.8627 7.82899 13.7187 7.63502 13.5209L5.71768 11.6123L3.70362 12.6538C3.54657 12.735 3.3583 12.7272 3.20847 12.6333C3.05865 12.5395 2.96954 12.3734 2.97413 12.1967L3.0567 9.01294L10.1016 3.89553C10.3809 3.69267 10.4429 3.30182 10.24 3.02255C10.0371 2.74327 9.64631 2.68133 9.36703 2.88419L2.20256 8.0884L0.47285 6.35869C0.285527 6.1715 0.147441 5.94054 0.0713446 5.68688C-0.00421083 5.43503 -0.0164862 5.16847 0.0355632 4.91079C0.0876798 4.62928 0.213521 4.36658 0.400302 4.14951C0.588362 3.93095 0.831375 3.76658 1.10423 3.67339L1.1076 3.67224L11.8211 0.0977331Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187420\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"sign-at\", \"keywords\": [ \"mail\", \"email\", \"at\", \"sign\", \"read\", \"address\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187426)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.0039 0.0299354L6.99739 0.0299273C5.96707 0.0331094 4.95027 0.264674 4.02017 0.707954C3.09007 1.15123 2.26982 1.7952 1.61844 2.59351C0.967076 3.39181 0.500788 4.3246 0.253148 5.32473C0.00550838 6.32485 -0.0173245 7.36744 0.186293 8.37745C0.389911 9.38746 0.814915 10.3398 1.43071 11.1658C2.04651 11.9919 2.83778 12.6711 3.74758 13.1547C4.65738 13.6383 5.66307 13.9141 6.69227 13.9624C7.72147 14.0107 8.74857 13.8302 9.69965 13.434C10.082 13.2747 10.2628 12.8355 10.1035 12.4532C9.9442 12.0708 9.50509 11.89 9.12274 12.0493C8.37634 12.3603 7.57028 12.502 6.76257 12.4641C5.95486 12.4262 5.1656 12.2097 4.45159 11.8302C3.73759 11.4507 3.1166 10.9176 2.63332 10.2693C2.15004 9.62103 1.8165 8.87366 1.65671 8.08101C1.49691 7.28836 1.51483 6.47014 1.70917 5.68525C1.90352 4.90036 2.26946 4.16831 2.78065 3.5418C3.29185 2.9153 3.93558 2.40992 4.66551 2.06203C5.39447 1.71461 6.19129 1.53291 6.99877 1.52993C7.96192 1.53588 8.90701 1.79209 9.74129 2.27347C10.5765 2.75539 11.2719 3.44638 11.7592 4.27851L11.7648 4.288C12.9636 6.26788 12.3754 7.80242 11.67 8.48687C11.2796 8.86568 10.96 8.90391 10.8624 8.88132C10.8376 8.87558 10.8118 8.86646 10.7773 8.81022C10.7333 8.73856 10.6683 8.57097 10.6683 8.24404V7.00002C10.6683 6.99783 10.6683 6.99564 10.6683 6.99345C10.6675 6.5151 10.5719 6.04164 10.387 5.60041C10.2013 5.15713 9.92924 4.75527 9.58659 4.41824C9.24395 4.08122 8.83764 3.81578 8.39136 3.63741C7.94507 3.45903 7.46774 3.37129 6.98719 3.3793C6.27375 3.3912 5.57973 3.61362 4.99228 4.01864C4.40483 4.42366 3.95015 4.99322 3.68533 5.6558C3.42051 6.31837 3.35736 7.04442 3.50381 7.74277C3.65026 8.44112 3.99978 9.08063 4.50848 9.58099C5.01718 10.0813 5.66238 10.4203 6.36305 10.5551C7.06373 10.69 7.78863 10.6149 8.44674 10.3392C8.84771 10.1712 9.21299 9.93326 9.52662 9.63864C9.76724 10.0068 10.1173 10.2486 10.5242 10.3427C11.3222 10.5273 12.1396 10.1212 12.7145 9.56341C13.939 8.37539 14.5948 6.06992 13.0508 3.51579C12.4321 2.46115 11.55 1.58533 10.4909 0.974229C9.43023 0.362208 8.2285 0.0367771 7.0039 0.0299354ZM7.01219 4.8791C7.29366 4.8744 7.57325 4.9258 7.83465 5.03027C8.09605 5.13475 8.33404 5.29023 8.53473 5.48763C8.73543 5.68504 8.89482 5.92042 9.0036 6.18006C9.11238 6.4397 9.16839 6.7184 9.16835 6.99991C9.16829 7.41785 9.04475 7.82646 8.81324 8.17441C8.58173 8.52237 8.25256 8.79417 7.86709 8.95568C7.48162 9.11718 7.05702 9.1612 6.64662 9.08219C6.23621 9.00318 5.8583 8.80467 5.56034 8.5116C5.26238 8.21852 5.05766 7.84394 4.97188 7.4349C4.8861 7.02586 4.92308 6.60059 5.0782 6.2125C5.23331 5.82441 5.49963 5.4908 5.84372 5.25357C6.1878 5.01634 6.59431 4.88606 7.01219 4.8791Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187426\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"sign-hashtag\", \"keywords\": [ \"mail\", \"sharp\", \"sign\", \"hashtag\", \"tag\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187441)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.1782 0.0909043C11.585 0.169127 11.8513 0.562284 11.7731 0.969045L11.2598 3.63844H13.1732C13.5875 3.63844 13.9232 3.97423 13.9232 4.38844C13.9232 4.80266 13.5875 5.13844 13.1732 5.13844H10.9713L10.2553 8.86141H13.1732C13.5875 8.86141 13.9232 9.19719 13.9232 9.61141C13.9232 10.0256 13.5875 10.3614 13.1732 10.3614H9.96688L9.39904 13.3142C9.32082 13.7209 8.92766 13.9873 8.5209 13.909C8.11414 13.8308 7.84781 13.4377 7.92603 13.0309L8.43939 10.3614H4.74395L4.17611 13.3142C4.09789 13.7209 3.70473 13.9873 3.29797 13.909C2.89121 13.8308 2.62488 13.4377 2.7031 13.0309L3.21646 10.3614H0.828125C0.413912 10.3614 0.078125 10.0256 0.078125 9.61141C0.078125 9.19719 0.413912 8.86141 0.828125 8.86141H3.50492L4.22088 5.13844H0.828125C0.413912 5.13844 0.078125 4.80266 0.078125 4.38844C0.078125 3.97423 0.413912 3.63844 0.828125 3.63844H4.50934L5.07716 0.685774C5.15538 0.279013 5.54854 0.0126812 5.9553 0.0909043C6.36206 0.169127 6.62839 0.562284 6.55017 0.969045L6.03682 3.63844H9.73227L10.3001 0.685774C10.3783 0.279013 10.7715 0.0126812 11.1782 0.0909043ZM5.74836 5.13844L5.03241 8.86141H8.72785L9.44381 5.13844H5.74836Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187441\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"smiley-angry\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187411)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14C10.866 14 14 10.866 14 7ZM8.25844 2.39914C8.20772 2.44069 8.06354 2.51658 7.80858 2.57986C7.56837 2.63948 7.28301 2.67469 7.00004 2.67469C6.71706 2.67469 6.4317 2.63948 6.19149 2.57986C5.93654 2.51658 5.79235 2.44069 5.74163 2.39914C5.47462 2.18038 5.08083 2.21948 4.86207 2.48649C4.64331 2.75349 4.68242 3.14728 4.94942 3.36604C5.20892 3.57866 5.56109 3.71132 5.89038 3.79305C6.23442 3.87844 6.62121 3.92469 7.00004 3.92469C7.37886 3.92469 7.76565 3.87844 8.10969 3.79305C8.43898 3.71132 8.79115 3.57866 9.05065 3.36604C9.31765 3.14728 9.35676 2.75349 9.138 2.48649C8.91924 2.21948 8.52545 2.18038 8.25844 2.39914ZM9.01441 9.2772C9.01441 8.13403 8.15022 7.13737 7.00132 7.13737C5.85241 7.13737 4.98822 8.13403 4.98822 9.2772C4.98822 10.4204 5.85241 11.417 7.00132 11.417C8.15022 11.417 9.01441 10.4204 9.01441 9.2772ZM9.41812 6.36611C9.98282 6.3648 10.4402 5.90662 10.4402 5.34161C10.4402 4.77579 9.9815 4.31711 9.41569 4.31711H9.41326C8.84856 4.31843 8.39119 4.77661 8.39119 5.34161C8.39119 5.90743 8.84988 6.36611 9.4157 6.36611H9.41812ZM4.58681 6.36611C5.15151 6.3648 5.60889 5.90662 5.60889 5.34161C5.60889 4.77579 5.15021 4.31711 4.58439 4.31711H4.58196C4.01725 4.31842 3.55987 4.7766 3.55987 5.34161C3.55987 5.90743 4.01856 6.36611 4.58438 6.36611H4.58681Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187411\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"smiley-cool\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187414)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.0005 3.17032H1.13991C2.39005 1.26116 4.54792 0 7.00035 0C9.45278 0 11.6106 1.26116 12.8608 3.17032H7.0005ZM7.00069 6.33145C6.93134 6.47043 6.85521 6.60817 6.77182 6.74327C6.11025 7.81512 4.97526 8.73509 3.19632 8.73509C1.80853 8.73509 0.798167 8.17264 0.104079 7.39441C0.070563 7.35683 0.0378285 7.3188 0.00585938 7.28037C0.153023 11.0164 3.22826 14 7.00035 14C10.7721 14 13.8472 11.0168 13.9948 7.28124C13.963 7.31937 13.9305 7.35711 13.8973 7.39441C13.2032 8.17264 12.1928 8.73509 10.805 8.73509C9.02613 8.73509 7.89114 7.81512 7.22957 6.74327C7.14618 6.60817 7.07005 6.47043 7.00069 6.33145ZM9.4869 9.79447C9.27743 9.61453 8.96176 9.63846 8.78181 9.84792C8.70713 9.93485 8.62294 10.0156 8.53088 10.0884C8.37897 10.2086 8.2072 10.3062 8.02423 10.375C7.9298 10.3999 7.83392 10.4155 7.73761 10.4222C7.46214 10.4414 7.25439 10.6803 7.2736 10.9558C7.2928 11.2312 7.53168 11.439 7.80716 11.4198C7.97516 11.4081 8.14373 11.3797 8.31046 11.3334C8.32296 11.33 8.33531 11.326 8.34751 11.3216C8.63918 11.2155 8.91136 11.0626 9.15144 10.8726C9.29253 10.7609 9.42312 10.636 9.54034 10.4996C9.72029 10.2901 9.69636 9.97442 9.4869 9.79447ZM0.158907 4.51204C0.121026 4.55312 0.10799 4.59597 0.120367 4.65258C0.260489 5.29348 0.547467 6.01356 1.03696 6.5624C1.50985 7.09262 2.18816 7.48509 3.19632 7.48509C4.46103 7.48509 5.22833 6.86407 5.70812 6.08673C6.04565 5.53989 6.23208 4.92367 6.31739 4.42032H0.391745C0.284411 4.42032 0.202939 4.46429 0.158907 4.51204ZM8.29327 6.08673C7.95574 5.53989 7.76931 4.92367 7.68399 4.42032H13.6096C13.7169 4.42032 13.7984 4.46429 13.8424 4.51204C13.8803 4.55312 13.8934 4.59597 13.881 4.65258C13.7409 5.29348 13.4539 6.01356 12.9644 6.5624C12.4915 7.09262 11.8132 7.48509 10.805 7.48509C9.54036 7.48509 8.77306 6.86407 8.29327 6.08673Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187414\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"smiley-crying-1\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187417)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14C10.866 14 14 10.866 14 7ZM4.99445 3.82648C4.80509 3.81719 4.58088 3.85307 4.37678 3.90704C4.16367 3.96338 3.92177 4.05282 3.69747 4.18233C3.47317 4.31183 3.27477 4.47659 3.11941 4.63298C2.97062 4.78276 2.82745 4.95898 2.74081 5.12762C2.58307 5.43465 2.7041 5.81142 3.01113 5.96916C3.31816 6.12689 3.69493 6.00586 3.85266 5.69883C3.85263 5.69888 3.85264 5.69887 3.85266 5.69883C3.85393 5.69665 3.86545 5.6767 3.89339 5.64079C3.92171 5.60439 3.9598 5.56065 4.00622 5.51393C4.10113 5.41838 4.21335 5.32786 4.32247 5.26486C4.43159 5.20186 4.5661 5.14994 4.6963 5.11551C4.75997 5.09868 4.81689 5.08755 4.86257 5.08123C4.90766 5.07498 4.93051 5.07498 4.93301 5.07498C4.93309 5.07498 4.93322 5.07498 4.93301 5.07498C5.27778 5.09189 5.57117 4.82611 5.58808 4.48135C5.60499 4.13659 5.33922 3.84339 4.99445 3.82648ZM4.66854 9.69417C4.57884 10.0275 4.23592 10.225 3.9026 10.1353C3.56928 10.0456 3.37179 9.70266 3.46148 9.36934C3.88143 7.80879 5.30569 6.65937 7 6.65937C8.69431 6.65937 10.1186 7.80879 10.5385 9.36934C10.6282 9.70266 10.4307 10.0456 10.0974 10.1353C9.76408 10.225 9.42115 10.0275 9.33146 9.69417C9.05467 8.66562 8.11485 7.90937 7 7.90937C5.88515 7.90937 4.94533 8.66562 4.66854 9.69417ZM9.13737 5.08123C9.09228 5.07498 9.06944 5.07498 9.06694 5.07498C9.06673 5.07498 9.06686 5.07498 9.06694 5.07498C8.72217 5.09189 8.42878 4.82611 8.41187 4.48135C8.39495 4.13659 8.66073 3.84339 9.00549 3.82648C9.19486 3.81719 9.41907 3.85307 9.62317 3.90704C9.83628 3.96338 10.0782 4.05282 10.3025 4.18233C10.5268 4.31183 10.7252 4.47659 10.8805 4.63298C11.0293 4.78276 11.1725 4.95898 11.2591 5.12762C11.4169 5.43465 11.2958 5.81142 10.9888 5.96916C10.6818 6.12689 10.305 6.00586 10.1473 5.69883C10.1473 5.6989 10.1474 5.69901 10.1473 5.69883C10.146 5.69667 10.1345 5.67671 10.1066 5.64079C10.0782 5.60439 10.0401 5.56065 9.99373 5.51393C9.89882 5.41838 9.78659 5.32786 9.67748 5.26486C9.56836 5.20186 9.43385 5.14994 9.30365 5.11551C9.23998 5.09868 9.18306 5.08755 9.13737 5.08123Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187417\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"smiley-cute\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187408)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M-0.00195312 7C-0.00195312 3.13401 3.13205 0 6.99804 0C10.8641 0 13.9981 3.13401 13.9981 7C13.9981 10.866 10.8641 14 6.99804 14C3.13205 14 -0.00195312 10.866 -0.00195312 7ZM4.80322 8.6344C4.80322 8.28922 4.5234 8.0094 4.17822 8.0094C3.83304 8.0094 3.55322 8.28922 3.55322 8.6344C3.55322 9.75824 4.46427 10.6693 5.58812 10.6693C6.13543 10.6693 6.63228 10.4532 6.99801 10.1017C7.36374 10.4532 7.86059 10.6693 8.40791 10.6693C9.53175 10.6693 10.4428 9.75824 10.4428 8.6344C10.4428 8.28922 10.163 8.0094 9.81781 8.0094C9.47264 8.0094 9.19281 8.28922 9.19281 8.6344C9.19281 9.06789 8.8414 9.4193 8.40791 9.4193C7.97442 9.4193 7.62301 9.06789 7.62301 8.6344C7.62301 8.28922 7.34319 8.0094 6.99801 8.0094C6.65283 8.0094 6.37301 8.28922 6.37301 8.6344C6.37301 9.06789 6.0216 9.4193 5.58812 9.4193C5.15463 9.4193 4.80322 9.06789 4.80322 8.6344ZM9.51794 5.16827C9.7831 5.16827 9.99795 5.30609 10.061 5.4085C10.2419 5.70247 10.6269 5.79413 10.9208 5.61322C11.2148 5.43231 11.3065 5.04735 11.1256 4.75337C10.7853 4.2005 10.1131 3.91827 9.51794 3.91827C8.92284 3.91827 8.25056 4.2005 7.91033 4.75337C7.72942 5.04735 7.82108 5.43231 8.11505 5.61322C8.40903 5.79413 8.79399 5.70247 8.9749 5.4085C9.03792 5.30609 9.25277 5.16827 9.51794 5.16827ZM3.93511 5.4085C3.99813 5.30609 4.21299 5.16827 4.47815 5.16827C4.74331 5.16827 4.95817 5.30609 5.02118 5.4085C5.20209 5.70247 5.58706 5.79413 5.88103 5.61322C6.17501 5.43231 6.26666 5.04735 6.08576 4.75337C5.74553 4.2005 5.07325 3.91827 4.47815 3.91827C3.88305 3.91827 3.21077 4.2005 2.87054 4.75337C2.68963 5.04735 2.78129 5.43231 3.07527 5.61322C3.36924 5.79413 3.75421 5.70247 3.93511 5.4085Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187408\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"smiley-drool\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187399)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.63701 10.8616C7.63913 10.8603 7.64125 10.859 7.64339 10.8576C7.95037 10.664 8.3425 10.4723 8.78023 10.3296C9.00438 10.2565 9.24048 10.1962 9.48323 10.1551C9.58126 10.1386 9.68038 10.1251 9.78023 10.1151V11.4693C9.78023 12.1493 9.22898 12.7005 8.54899 12.7005C7.86899 12.7005 7.31775 12.1493 7.31775 11.4693V11.0884C7.40929 11.0145 7.51652 10.938 7.63701 10.8616ZM10.2243 9.21917C10.0578 9.13663 9.86982 9.1013 9.68125 9.12006C8.44742 9.24278 7.32002 9.80121 6.68945 10.3105C6.47179 10.4862 6.33854 10.7444 6.31998 11.0216C5.39808 10.8783 4.70421 10.4411 4.22316 9.90656C3.64975 9.26944 3.37305 8.48685 3.37305 7.90648C3.37305 7.63034 3.59691 7.40648 3.87305 7.40648H10.0913C10.3674 7.40648 10.5913 7.63034 10.5913 7.90648C10.5913 8.28738 10.4718 8.75847 10.2243 9.21917ZM0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7ZM8.88978 4.73407C8.87624 4.73325 8.86939 4.73339 8.86851 4.73341C8.52375 4.75032 8.23055 4.48454 8.21364 4.13978C8.19673 3.79502 8.4625 3.50182 8.80727 3.48491C9.00211 3.47535 9.23477 3.51233 9.44823 3.56877C9.67069 3.62759 9.92284 3.72088 10.1562 3.85562C10.3896 3.99037 10.5965 4.16209 10.7586 4.32534C10.9143 4.48198 11.0626 4.66498 11.1517 4.8385C11.3095 5.14553 11.1885 5.52229 10.8814 5.68003C10.5744 5.83776 10.1977 5.71682 10.0399 5.40979C10.0395 5.40907 10.0362 5.403 10.0287 5.39162C10.0204 5.37909 10.0088 5.36278 9.99353 5.34315C9.96276 5.30359 9.92166 5.25644 9.87184 5.20629C9.77011 5.10388 9.64942 5.0064 9.53122 4.93815C9.41302 4.86991 9.26826 4.81414 9.1287 4.77724C9.06036 4.75917 8.99898 4.74715 8.94933 4.74028C8.92469 4.73687 8.90477 4.73497 8.88978 4.73407ZM3.97132 5.39162C3.96384 5.40293 3.96054 5.40894 3.96012 5.40971C3.80238 5.71674 3.42561 5.83777 3.11858 5.68003C2.81155 5.52229 2.69052 5.14553 2.84826 4.8385C2.9374 4.66498 3.08575 4.48198 3.24136 4.32534C3.40353 4.16209 3.6104 3.99037 3.84378 3.85562C4.07717 3.72088 4.32932 3.62759 4.55178 3.56877C4.76524 3.51233 4.9979 3.47535 5.19274 3.48491C5.5375 3.50182 5.80328 3.79502 5.78637 4.13978C5.76946 4.48454 5.47626 4.75032 5.13149 4.73341C5.13057 4.73339 5.12371 4.73325 5.11023 4.73407C5.09524 4.73498 5.07531 4.73687 5.05067 4.74028C5.00102 4.74715 4.93965 4.75917 4.8713 4.77724C4.73175 4.81414 4.58699 4.86991 4.46878 4.93816C4.35058 5.0064 4.2299 5.10388 4.12817 5.20629C4.07834 5.25645 4.03725 5.30359 4.00647 5.34315C3.9912 5.36278 3.9796 5.37909 3.97132 5.39162Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187399\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"smiley-emoji-kiss-nervous\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187456)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7ZM7.44515 6.70296C7.10784 6.6297 6.775 6.84376 6.70174 7.18107C6.62848 7.51838 6.84254 7.85122 7.17985 7.92448L7.62642 8.02147C7.81693 8.06285 7.87513 8.30613 7.72397 8.42925L7.35531 8.7295C7.20958 8.84819 7.125 9.02616 7.125 9.21411C7.125 9.40206 7.20958 9.58003 7.35531 9.69872L7.72397 9.99897C7.87513 10.1221 7.81693 10.3654 7.62642 10.4068L7.17985 10.5037C6.84254 10.577 6.62848 10.9098 6.70174 11.2472C6.775 11.5845 7.10784 11.7985 7.44515 11.7253L7.89172 11.6283C9.01662 11.384 9.41762 10.035 8.70412 9.21411C9.41762 8.39324 9.01662 7.04426 7.89172 6.79995L7.44515 6.70296ZM11 3.625C11.2071 3.90114 11.1511 4.29289 10.875 4.5L9.68402 5.39324L10.7795 5.94098C11.0882 6.09535 11.2134 6.47077 11.059 6.77951C10.9046 7.08825 10.5292 7.21339 10.2205 7.05902L8.22049 6.05902C8.02675 5.96215 7.89698 5.77186 7.87753 5.55613C7.85807 5.34039 7.95171 5.12997 8.125 5L10.125 3.5C10.4011 3.29289 10.7929 3.34886 11 3.625ZM3.875 3.5C3.59886 3.29289 3.20711 3.34886 3 3.625C2.79289 3.90114 2.84886 4.29289 3.125 4.5L4.31598 5.39324L3.22049 5.94098C2.91176 6.09535 2.78661 6.47077 2.94098 6.77951C3.09535 7.08825 3.47077 7.21339 3.77951 7.05902L5.77951 6.05902C5.97325 5.96215 6.10302 5.77186 6.12247 5.55613C6.14193 5.34039 6.04829 5.12997 5.875 5L3.875 3.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187456\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"smiley-emoji-terrified\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187447)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7ZM10.875 4.5C11.1511 4.29289 11.2071 3.90114 11 3.625C10.7929 3.34886 10.4011 3.29289 10.125 3.5L8.125 5C7.95171 5.12997 7.85807 5.34039 7.87753 5.55613C7.89698 5.77186 8.02675 5.96215 8.22049 6.05902L10.2205 7.05902C10.5292 7.21339 10.9046 7.08825 11.059 6.77951C11.2134 6.47077 11.0882 6.09535 10.7795 5.94098L9.68402 5.39324L10.875 4.5ZM3 3.625C3.20711 3.34886 3.59886 3.29289 3.875 3.5L5.875 5C6.04829 5.12997 6.14193 5.34039 6.12247 5.55613C6.10302 5.77186 5.97325 5.96215 5.77951 6.05902L3.77951 7.05902C3.47077 7.21339 3.09535 7.08825 2.94098 6.77951C2.78661 6.47077 2.91176 6.09535 3.22049 5.94098L4.31598 5.39324L3.125 4.5C2.84886 4.29289 2.79289 3.90114 3 3.625ZM5.03718 10.3195C4.8607 10.6161 4.47716 10.7136 4.18053 10.5372C3.88385 10.3608 3.78638 9.97722 3.96282 9.68054L4.5 10C3.96282 9.68054 3.96276 9.68064 3.96282 9.68054L3.96346 9.67947L3.96465 9.67747L3.96866 9.67078L3.98303 9.64702C3.99538 9.62672 4.01317 9.59768 4.03594 9.56114C4.08144 9.48811 4.14696 9.38485 4.22856 9.26136C4.39108 9.0154 4.6206 8.68437 4.88528 8.35058C5.14702 8.02049 5.4581 7.66859 5.78529 7.39349C6.0866 7.14016 6.51554 6.85278 6.99996 6.85275C7.48442 6.85273 7.91339 7.14011 8.2147 7.39344C8.5419 7.66853 8.85298 8.02044 9.11473 8.35054C9.37941 8.68435 9.60892 9.01539 9.77145 9.26135C9.85305 9.38484 9.91857 9.48812 9.96407 9.56114C9.98683 9.59769 10.0046 9.62673 10.017 9.64703L10.0313 9.67079L10.0354 9.67748L10.0365 9.67948L10.0371 9.68037C10.0371 9.68047 10.0372 9.68056 9.5 10L10.0371 9.68037C10.2135 9.97705 10.1161 10.3608 9.81946 10.5372C9.52284 10.7136 9.13941 10.6162 8.96293 10.3197L8.95999 10.3148L8.94885 10.2963C8.93873 10.2797 8.92332 10.2546 8.90312 10.2221C8.86268 10.1572 8.8032 10.0634 8.72855 9.95046C8.57858 9.72349 8.37059 9.42395 8.13527 9.12718C7.89702 8.82671 7.6456 8.54805 7.4103 8.35022C7.2932 8.25177 7.19326 8.1846 7.11305 8.14414C7.03905 8.10682 7.00356 8.10315 7.00003 8.10279C6.9965 8.10315 6.96099 8.10682 6.88698 8.14416C6.80676 8.18463 6.70681 8.25181 6.58971 8.35027C6.3544 8.5481 6.10298 8.82677 5.86472 9.12723C5.6294 9.424 5.42142 9.72352 5.27144 9.95049C5.19679 10.0635 5.13731 10.1572 5.09687 10.2221C5.07667 10.2546 5.06126 10.2797 5.05114 10.2964L5.04 10.3148L5.03718 10.3195Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187447\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"smiley-grumpy\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187423)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7ZM9.33147 9.58153C9.42117 9.91485 9.76409 10.1123 10.0974 10.0226C10.4307 9.93294 10.6282 9.59002 10.5385 9.2567C10.1186 7.69615 8.69432 6.54673 7.00001 6.54673C5.3057 6.54673 3.88145 7.69615 3.4615 9.2567C3.3718 9.59002 3.56929 9.93294 3.90261 10.0226C4.23593 10.1123 4.57886 9.91485 4.66855 9.58153C4.94534 8.55298 5.88516 7.79673 7.00001 7.79673C8.11486 7.79673 9.05468 8.55298 9.33147 9.58153ZM2.94095 4.40837C2.94095 4.06319 3.22077 3.78337 3.56595 3.78337H4.86948C5.21465 3.78337 5.49448 4.06319 5.49448 4.40837V5.0031C5.49448 5.34828 5.21465 5.6281 4.86948 5.6281C4.53445 5.6281 4.26099 5.36449 4.2452 5.03337H3.56595C3.22077 5.03337 2.94095 4.75355 2.94095 4.40837ZM9.13052 3.78337C8.78535 3.78337 8.50552 4.06319 8.50552 4.40837C8.50552 4.75355 8.78535 5.03337 9.13052 5.03337H9.80977C9.82556 5.36449 10.099 5.6281 10.4341 5.6281C10.7792 5.6281 11.0591 5.34828 11.0591 5.0031V4.40837C11.0591 4.06319 10.7792 3.78337 10.4341 3.78337H9.13052Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187423\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"smiley-happy\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187402)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0ZM3.34308 8.22259C3.34308 8.08452 3.45501 7.97259 3.59308 7.97259H10.3747C10.5128 7.97259 10.6247 8.08452 10.6247 8.22259C10.6247 8.7751 10.3552 9.55988 9.77337 10.2063C9.18322 10.8621 8.269 11.379 6.98389 11.379C5.69878 11.379 4.78456 10.8621 4.19441 10.2063C3.61259 9.55988 3.34308 8.7751 3.34308 8.22259ZM4.58194 5.976C4.01725 5.97469 3.55988 5.51651 3.55988 4.95151C3.55988 4.38569 4.01856 3.927 4.58438 3.927L4.5868 3.92701C5.1515 3.92832 5.60887 4.3865 5.60887 4.95151C5.60887 5.51732 5.15019 5.97601 4.58437 5.97601L4.58194 5.976ZM9.4132 5.976C8.84849 5.9747 8.39111 5.51652 8.39111 4.95151C8.39111 4.38569 8.8498 3.927 9.41562 3.927L9.41805 3.92701C9.98275 3.92831 10.4401 4.3865 10.4401 4.95151C10.4401 5.51732 9.98144 5.97601 9.41563 5.97601L9.4132 5.976Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187402\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"smiley-in-love\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187438)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14C10.866 14 14 10.866 14 7ZM9.50896 8.83716C9.59866 8.50384 9.94158 8.30635 10.2749 8.39604C10.6082 8.48574 10.8057 8.82867 10.716 9.16199C10.275 10.8007 8.7794 12.0077 7.00027 12.0077C5.22115 12.0077 3.72552 10.8007 3.28453 9.16199C3.19483 8.82867 3.39233 8.48574 3.72565 8.39604C4.05897 8.30635 4.40189 8.50384 4.49159 8.83716C4.78941 9.94388 5.80061 10.7577 7.00027 10.7577C8.19994 10.7577 9.21113 9.94388 9.50896 8.83716ZM10.0535 3.89244C11.0457 2.67508 12.0932 3.54006 12.107 4.4725C12.107 5.86063 10.4461 7 10.0535 7C9.66091 7 8.00002 5.86063 8.00002 4.4725C8.0138 3.54006 9.06136 2.67508 10.0535 3.89244ZM1.89301 4.4725C1.90679 3.54006 2.95434 2.67508 3.94649 3.89244C4.93864 2.67508 5.98619 3.54006 5.99998 4.4725C5.99998 5.86063 4.33909 7 3.94649 7C3.55389 7 1.89301 5.86063 1.89301 4.4725Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187438\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"smiley-kiss\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187435)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7ZM3.55988 4.67646C3.55988 5.24146 4.01725 5.69964 4.58194 5.70096H4.58437C5.15019 5.70096 5.60887 5.24227 5.60887 4.67646C5.60887 4.11145 5.1515 3.65327 4.5868 3.65196L4.58438 3.65195C4.01856 3.65195 3.55988 4.11064 3.55988 4.67646ZM8.39111 4.67646C8.39111 5.24147 8.84849 5.69965 9.4132 5.70096H9.41563C9.98144 5.70096 10.4401 5.24227 10.4401 4.67646C10.4401 4.11145 9.98275 3.65326 9.41805 3.65196L9.41562 3.65195C8.8498 3.65195 8.39111 4.11064 8.39111 4.67646ZM7.3637 6.39044C7.02638 6.31718 6.69355 6.53124 6.62029 6.86856C6.54703 7.20587 6.76109 7.53871 7.0984 7.61197L7.54497 7.70896C7.73548 7.75033 7.79368 7.99362 7.64252 8.11673L7.27386 8.41699C7.12813 8.53568 7.04355 8.71365 7.04355 8.9016C7.04355 9.08954 7.12813 9.26751 7.27386 9.3862L7.64252 9.68646C7.79368 9.80958 7.73548 10.0529 7.54497 10.0942L7.0984 10.1912C6.76109 10.2645 6.54703 10.5973 6.62029 10.9346C6.69355 11.2719 7.02638 11.486 7.3637 11.4127L7.81027 11.3158C8.93517 11.0714 9.33617 9.72247 8.62267 8.9016C9.33617 8.08072 8.93517 6.73175 7.81027 6.48743L7.3637 6.39044Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187435\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"smiley-laughing-3\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187450)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.52636 0.982177C0.202783 2.90104 -0.93596 7.15089 0.982909 10.4745C2.90178 13.7981 7.15162 14.9368 10.4752 13.0179C13.7988 11.0991 14.9375 6.8492 13.0187 3.52562C11.0998 0.20205 6.84994 -0.936692 3.52636 0.982177ZM2.23217 5.66023C2.34257 5.33318 2.69719 5.15755 3.02424 5.26795L4.95198 5.91866C5.18464 5.9972 5.34975 6.20469 5.37403 6.44904C5.3983 6.6934 5.27725 6.92932 5.06459 7.0521L3.6238 7.88394C3.32487 8.05653 2.94262 7.95411 2.77003 7.65517C2.59745 7.35624 2.69987 6.974 2.9988 6.80141L3.24227 6.66084L2.62446 6.4523C2.29741 6.3419 2.12178 5.98728 2.23217 5.66023ZM8.71457 2.93615C8.78249 2.59772 8.5632 2.26831 8.22477 2.20039C7.88634 2.13247 7.55693 2.35177 7.48901 2.6902L7.08867 4.68503C7.04035 4.92579 7.13749 5.17253 7.33697 5.31573C7.53645 5.45893 7.80129 5.47205 8.01395 5.34928L9.45475 4.51743C9.75368 4.34485 9.8561 3.9626 9.68351 3.66367C9.51092 3.36474 9.12868 3.26231 8.82975 3.4349L8.58627 3.57547L8.71457 2.93615ZM4.20903 9.89568C4.07096 9.65654 4.1529 9.35074 4.39204 9.21267L10.2222 5.84664C10.4613 5.70857 10.7671 5.79051 10.9052 6.02965C11.2137 6.56405 11.377 7.43699 11.1862 8.33584C10.9902 9.25907 10.4192 10.2191 9.24969 10.8943C8.08024 11.5695 6.9633 11.584 6.06575 11.2921C5.19191 11.008 4.51756 10.4301 4.20903 9.89568Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187450\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" } ], \"map_travel\": [ { \"name\": \"airplane\", \"keywords\": [ \"travel\", \"plane\", \"adventure\", \"airplane\", \"transportation\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.5631 2.19703C2.73885 2.19629 2.90964 2.2553 3.04744 2.3644C3.1846 2.47298 3.28097 2.62475 3.32092 2.79501L3.32027 2.79244L3.07802 2.85421L3.32164 2.79809L3.32092 2.79501L3.80813 4.70562L8.93409 3.68043C9.54022 3.56156 10.1651 3.57771 10.7643 3.72775C11.3635 3.87778 11.9223 4.15801 12.4009 4.5485C12.8794 4.93898 13.2661 5.43013 13.5333 5.98701C13.8006 6.54389 13.9418 7.15282 13.947 7.77048V7.77258C13.947 8.12307 13.8078 8.45921 13.5599 8.70705C13.3121 8.95489 12.976 9.09412 12.6255 9.09412H8.0483L7.05037 10.6709L7.04954 10.6722C6.83332 11.0092 6.53599 11.2865 6.1848 11.4789C5.8336 11.6712 5.43934 11.7723 5.03894 11.773H3.86024H3.85827C3.7239 11.7719 3.59205 11.7364 3.47532 11.6698C3.35858 11.6033 3.26086 11.5079 3.1915 11.3928C3.12214 11.2778 3.08344 11.1468 3.07911 11.0125C3.0748 10.8787 3.10477 10.7459 3.16616 10.627L3.16567 10.6279L3.38877 10.7407L3.16679 10.6257L3.92513 9.12627H1.37427C1.02378 9.12627 0.687641 8.98703 0.439804 8.7392C0.191968 8.49136 0.0527344 8.15522 0.0527344 7.80473V2.9828C0.0527344 2.7744 0.135521 2.57454 0.282881 2.42718C0.430242 2.27981 0.630104 2.19703 0.838504 2.19703H2.5631Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"airport-plane-transit\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186916)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.2127 2.53752C8.91903 1.6074 7.23449 1.24494 5.57696 1.68908C2.78152 2.43811 1.06874 5.21231 1.5948 8.0098C1.67134 8.41688 1.4034 8.80893 0.996318 8.88548C0.58924 8.96203 0.197182 8.69408 0.120633 8.28701C-0.549251 4.72465 1.62993 1.19377 5.18873 0.240189C7.38469 -0.348217 9.61568 0.176234 11.2842 1.46596L11.8965 0.853736C12.2114 0.538754 12.75 0.761838 12.75 1.20729V3.50018C12.75 3.77633 12.5262 4.00018 12.25 4.00018H9.95712C9.51167 4.00018 9.28858 3.46161 9.60357 3.14663L10.2127 2.53752ZM13.0041 5.11487C13.4111 5.03832 13.8032 5.30626 13.8797 5.71334C14.5496 9.2757 12.3705 12.8066 8.81165 13.7602C6.61562 14.3486 4.38455 13.8241 2.71598 12.5343L2.10362 13.1466C1.78863 13.4616 1.25006 13.2385 1.25006 12.7931V10.5002C1.25006 10.224 1.47392 10.0002 1.75006 10.0002H4.04296C4.48841 10.0002 4.71149 10.5387 4.39651 10.8537L3.78752 11.4627C5.08119 12.3929 6.76582 12.7554 8.42342 12.3113C11.2189 11.5622 12.9316 8.78804 12.4056 5.99055C12.329 5.58347 12.597 5.19142 13.0041 5.11487ZM3.95566 6.05191L3.24568 6.41712C3.18908 6.44807 3.1462 6.49918 3.12554 6.56029C3.10488 6.6214 3.10796 6.68804 3.13416 6.74699L4.11454 8.71829C4.17711 8.83635 4.2838 8.9249 4.41136 8.96466C4.53893 9.00441 4.67704 8.99214 4.7956 8.93052L6.40216 8.08144L6.52372 8.02518L6.27469 9.69393C6.27276 9.7426 6.28357 9.79093 6.30605 9.83414C6.32854 9.87735 6.36192 9.91393 6.40288 9.94028C6.44385 9.96662 6.49099 9.98181 6.53963 9.98434C6.58827 9.98687 6.63673 9.97666 6.68021 9.9547L7.90948 9.3426C7.9507 9.32264 7.98621 9.2926 8.01272 9.25527C8.03924 9.21794 8.05591 9.17452 8.06119 9.12903L8.44166 7.05868L8.62719 6.97L10.3044 6.19472C10.3653 6.16561 10.4199 6.12479 10.465 6.07458C10.5102 6.02438 10.545 5.96578 10.5674 5.90212C10.5899 5.83846 10.5997 5.771 10.5961 5.70358C10.5925 5.63617 10.5756 5.57012 10.5465 5.50921C10.4125 5.27361 10.2291 5.06974 10.009 4.91155C9.78893 4.75335 9.53725 4.64456 9.27122 4.59262C9.00518 4.54068 8.73107 4.54681 8.46762 4.6106C8.20417 4.67438 7.95761 4.79431 7.7448 4.96219L4.97589 6.90157L4.30464 6.13766C4.26921 6.08061 4.21285 6.03968 4.14763 6.02366C4.08241 6.00763 4.0135 6.01777 3.95566 6.05191Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186916\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"airport-plane\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186890)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.9786 0.299805C11.5222 0.299805 11.0845 0.481101 10.7618 0.803811L10.7608 0.804843L8.32409 3.27001L3.18518 0.894479C2.97406 0.793005 2.74443 0.73572 2.51037 0.726145C2.27561 0.716542 2.04136 0.755141 1.82209 0.839561C1.60282 0.92398 1.40317 1.05243 1.23546 1.21699C1.07069 1.37865 0.940182 1.5718 0.851664 1.7849C0.655443 2.16345 0.608576 2.6021 0.720663 3.0138C0.833834 3.42949 1.10061 3.7867 1.46706 4.01323L5.25799 6.34755L4.07496 7.53059H1.87851C1.87159 7.53059 1.86467 7.53087 1.85777 7.53145C1.43279 7.56683 1.03663 7.76065 0.74787 8.07446C0.459105 8.38826 0.298828 8.79914 0.298828 9.22559C0.298828 9.65204 0.459105 10.0629 0.74787 10.3767C1.03663 10.6905 1.43279 10.8843 1.85777 10.9197C1.86467 10.9203 1.87159 10.9206 1.87851 10.9206H3.07851V12.1206C3.07851 12.1275 3.0788 12.1344 3.07937 12.1413C3.11476 12.5663 3.30857 12.9624 3.62238 13.2512C3.93619 13.54 4.34706 13.7002 4.77351 13.7002C5.19996 13.7002 5.61084 13.54 5.92464 13.2512C6.23845 12.9624 6.43227 12.5663 6.46765 12.1413C6.46822 12.1344 6.46851 12.1275 6.46851 12.1206V9.92414L7.65154 8.74111L9.98563 12.5316C10.2121 12.8981 10.5697 13.1652 10.9853 13.2784C11.3973 13.3906 11.8363 13.3436 12.2151 13.147C12.4224 13.0592 12.6103 12.9313 12.7682 12.7707C12.9287 12.6073 13.0547 12.4134 13.1388 12.2004C13.2229 11.9873 13.2634 11.7596 13.2578 11.5306C13.2523 11.3017 13.2007 11.0762 13.1063 10.8675L13.1059 10.8665L10.7287 5.67547L13.1943 3.23839L13.1953 3.23736C13.518 2.91465 13.6993 2.47697 13.6993 2.02059C13.6993 1.56421 13.518 1.12652 13.1953 0.803811C12.8726 0.481101 12.4349 0.299805 11.9786 0.299805Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186890\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"airport-security\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186893)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.443091 0.439181C0.724294 0.157978 1.10568 0 1.50337 0H12.4974C12.8951 0 13.2765 0.157978 13.5577 0.439181C13.8389 0.720384 13.9969 1.10178 13.9969 1.49946V4.49783C13.998 6.53422 13.3815 8.52307 12.2286 10.2017C11.0757 11.8803 9.44043 13.1698 7.5394 13.8998C7.19235 14.0335 6.808 14.0334 6.46098 13.8996C4.55995 13.1697 2.92504 11.8803 1.77218 10.2017C0.619307 8.52307 0.0027376 6.53422 0.00390791 4.49783V1.49946C0.00390791 1.10178 0.161888 0.720384 0.443091 0.439181ZM9.95154 3.80754C9.63823 3.69665 9.30466 3.65474 8.97366 3.68467C8.64508 3.71439 8.32673 3.8142 8.04004 3.97735L4.94069 5.52702L4.46835 4.79586C4.41131 4.67881 4.31308 4.58651 4.19206 4.53699C4.06623 4.48552 3.92555 4.4843 3.79899 4.53339L2.97231 4.81447C2.84726 4.86056 2.74414 4.95208 2.68339 5.0707C2.62263 5.18931 2.6087 5.32644 2.64437 5.45486L2.64726 5.46528L3.39329 7.76063C3.464 7.96054 3.61092 8.12437 3.80191 8.2165C3.99219 8.30828 4.21092 8.32144 4.41078 8.2532L6.20209 7.67015L5.80314 8.98073C5.80051 8.98938 5.79834 8.99816 5.79666 9.00704C5.77866 9.10214 5.78581 9.2003 5.8174 9.29179C5.84899 9.38328 5.90393 9.46493 5.97677 9.52867C6.04961 9.59241 6.13783 9.63601 6.23271 9.65518C6.32758 9.67434 6.42581 9.6684 6.51768 9.63794L6.52533 9.6354L7.71439 9.19802C7.79966 9.17009 7.8768 9.12163 7.93898 9.05685C8.00059 8.99269 8.04559 8.91451 8.07017 8.82912L8.7705 6.77847L8.86199 6.75032L10.8018 6.18065C10.9037 6.1493 10.9985 6.09816 11.0807 6.03018C11.1628 5.9622 11.2308 5.8787 11.2807 5.78445C11.3306 5.69019 11.3614 5.58703 11.3715 5.48086C11.3815 5.37469 11.3705 5.26758 11.3391 5.16565C11.3374 5.15997 11.3354 5.15436 11.3333 5.14882C11.2131 4.83894 11.0275 4.55867 10.789 4.32718C10.5505 4.09569 10.2649 3.91843 9.95154 3.80754Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186893\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"anchor\", \"keywords\": [ \"anchor\", \"marina\", \"harbor\", \"port\", \"travel\", \"places\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186905)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.25 2.28394C6.25 1.86972 6.58579 1.53394 7 1.53394C7.41421 1.53394 7.75 1.86972 7.75 2.28394C7.75 2.69815 7.41421 3.03394 7 3.03394C6.58579 3.03394 6.25 2.69815 6.25 2.28394ZM9.25 2.28394C9.25 3.2636 8.62389 4.09703 7.75 4.40591V5.27764H8.5C8.91421 5.27764 9.25 5.61343 9.25 6.02764C9.25 6.44186 8.91421 6.77764 8.5 6.77764H7.75V12.3898C8.73188 12.2328 9.64671 11.7702 10.3588 11.0581C11.0708 10.3461 11.5334 9.43124 11.6904 8.44937H11C10.5858 8.44937 10.25 8.11358 10.25 7.69937C10.25 7.28515 10.5858 6.94937 11 6.94937H12.5C12.6989 6.94937 12.8897 7.02838 13.0303 7.16904C13.171 7.30969 13.25 7.50045 13.25 7.69937C13.25 9.35697 12.5915 10.9467 11.4194 12.1188C10.2473 13.2909 8.6576 13.9494 7 13.9494C5.3424 13.9494 3.75269 13.2909 2.58058 12.1188C1.40848 10.9467 0.75 9.35697 0.75 7.69937C0.75 7.50045 0.829018 7.30969 0.96967 7.16904C1.11032 7.02838 1.30109 6.94937 1.5 6.94937H3C3.41421 6.94937 3.75 7.28515 3.75 7.69937C3.75 8.11358 3.41421 8.44937 3 8.44937H2.30956C2.46656 9.43124 2.9292 10.3461 3.64124 11.0581C4.35329 11.7702 5.26812 12.2328 6.25 12.3898V6.77764H5.5C5.08579 6.77764 4.75 6.44186 4.75 6.02764C4.75 5.61343 5.08579 5.27764 5.5 5.27764H6.25V4.40591C5.37611 4.09703 4.75 3.2636 4.75 2.28394C4.75 1.0413 5.75736 0.0339355 7 0.0339355C8.24264 0.0339355 9.25 1.0413 9.25 2.28394Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186905\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"baggage\", \"keywords\": [ \"check\", \"baggage\", \"travel\", \"adventure\", \"luggage\", \"bag\", \"checked\", \"airport\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186919)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 2.25C6.53587 2.25 6.09075 2.43437 5.76256 2.76256C5.5552 2.96993 5.40525 3.22397 5.32295 3.5H8.67705C8.59475 3.22397 8.4448 2.96993 8.23744 2.76256C7.90925 2.43437 7.46413 2.25 7 2.25ZM4.7019 1.7019C4.21669 2.18711 3.90075 2.80995 3.79207 3.47866C3.79454 3.50122 3.79581 3.52414 3.79581 3.54736V13.9526C3.79581 13.9686 3.79521 13.9844 3.79404 14H10.206C10.2048 13.9844 10.2042 13.9686 10.2042 13.9526V3.54736C10.2042 3.52414 10.2055 3.50123 10.2079 3.47866C10.0993 2.80995 9.78331 2.18711 9.2981 1.7019C8.6886 1.09241 7.86195 0.75 7 0.75C6.13805 0.75 5.3114 1.09241 4.7019 1.7019ZM11.4524 3.5C11.4536 3.51563 11.4542 3.53143 11.4542 3.54736V13.9526C11.4542 13.9686 11.4536 13.9844 11.4524 14H11.5C12.8807 14 14 12.8807 14 11.5V6C14 4.61929 12.8807 3.5 11.5 3.5H11.4524ZM2.5 3.5H2.54757C2.5464 3.51563 2.54581 3.53143 2.54581 3.54736V13.9526C2.54581 13.9686 2.5464 13.9844 2.54757 14H2.5C1.11929 14 0 12.8807 0 11.5V6C0 4.61929 1.11929 3.5 2.5 3.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186919\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"beach\", \"keywords\": [ \"island\", \"waves\", \"outdoor\", \"recreation\", \"tree\", \"beach\", \"palm\", \"wave\", \"water\", \"travel\", \"places\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186922)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.33928 3.29116C7.32105 3.29598 7.30286 3.30104 7.28472 3.30633C6.78234 3.4528 6.34673 3.76979 6.05284 4.20277C5.82021 4.54549 5.3538 4.63474 5.01108 4.40211C4.66836 4.16948 4.57911 3.70307 4.81174 3.36035C5.30155 2.63873 6.02757 2.1104 6.86487 1.86629C7.07167 1.80599 7.28207 1.76397 7.49374 1.74007C7.11806 1.5331 6.68034 1.46411 6.25719 1.54743C5.85078 1.62745 5.45645 1.36287 5.37642 0.956457C5.29639 0.550047 5.56098 0.155714 5.96739 0.0756883C6.80406 -0.0890592 7.67206 0.0734436 8.39246 0.529703C8.84648 0.817246 9.22007 1.20765 9.48652 1.66402C9.99029 1.4757 10.5344 1.4035 11.0776 1.45842C11.9334 1.54493 12.7281 1.94089 13.3125 2.57195C13.594 2.87585 13.5758 3.35037 13.2719 3.63183C12.968 3.91328 12.4934 3.89507 12.212 3.59117C11.8759 3.22827 11.4189 3.00056 10.9268 2.95081L10.9214 2.95027C11.0572 3.09748 11.1803 3.25772 11.2886 3.42953C11.7449 4.15395 11.9019 5.02739 11.7263 5.86538C11.6414 6.27079 11.2438 6.53057 10.8384 6.44561C10.433 6.36066 10.1732 5.96314 10.2582 5.55773C10.354 5.10065 10.2684 4.62422 10.0194 4.22909C9.82057 3.91338 9.52985 3.66931 9.18963 3.52735C8.7627 3.81914 8.40343 4.29691 8.13053 4.95781C7.85873 5.61606 7.69026 6.41698 7.62676 7.29006C8.17406 7.42854 8.70498 7.65641 9.20636 7.96783C9.981 8.44898 10.6665 9.11771 11.2303 9.92836C10.9081 10.0148 10.5572 10.2005 10.3227 10.5715C10.2359 10.7087 10.147 10.8209 10.0601 10.91C9.68396 11.2952 9.02069 11.2952 8.64452 10.91C8.57049 10.8341 8.49484 10.7414 8.4202 10.6303C8.03306 10.0541 7.36458 9.92899 6.96828 9.92899C6.57197 9.92899 5.90349 10.0541 5.51636 10.6303C5.44171 10.7414 5.36606 10.8341 5.29203 10.91C4.91587 11.2952 4.25259 11.2952 3.87642 10.91C3.78951 10.8209 3.70057 10.7087 3.61379 10.5715C3.24275 9.98446 2.58037 9.86142 2.20124 9.86132C1.99572 9.86127 1.71044 9.89709 1.4326 10.0204C2.00811 9.16825 2.71591 8.46706 3.51967 7.96783C4.33222 7.46313 5.22236 7.17789 6.13403 7.13689C6.20886 6.15856 6.4032 5.21085 6.74408 4.38532C6.90464 3.99648 7.10153 3.62631 7.33928 3.29116ZM2.88949 11.4898C2.77072 11.2127 2.49758 11.0337 2.19611 11.0353C1.89465 11.0369 1.62345 11.2189 1.50767 11.4973C1.42261 11.7018 1.29306 11.8847 1.12844 12.0329C0.963821 12.181 0.768259 12.2907 0.555969 12.3538C0.158935 12.4719 -0.0672208 12.8894 0.050835 13.2864C0.168891 13.6835 0.586454 13.9096 0.983487 13.7916C1.40935 13.665 1.80165 13.445 2.13189 13.1478C2.15476 13.1272 2.17729 13.1063 2.19948 13.0851C2.37243 13.254 2.56537 13.4032 2.77506 13.529C3.82193 14.1572 5.34647 14.1572 6.39334 13.529C6.60276 13.4034 6.79547 13.2544 6.96825 13.0857C7.14103 13.2544 7.33374 13.4034 7.54316 13.529C8.59003 14.1572 10.1145 14.1572 11.1614 13.529C11.3711 13.4032 11.564 13.254 11.737 13.0851C11.7592 13.1063 11.7817 13.1272 11.8046 13.1478C12.1348 13.445 12.5271 13.665 12.953 13.7916C13.35 13.9096 13.7676 13.6835 13.8856 13.2864C14.0037 12.8894 13.7775 12.4719 13.3805 12.3538C13.1682 12.2907 12.9726 12.181 12.808 12.0329C12.6434 11.8847 12.5138 11.7018 12.4288 11.4973C12.313 11.2189 12.0418 11.0369 11.7403 11.0353C11.4389 11.0337 11.1657 11.2127 11.047 11.4898C10.9124 11.8037 10.6824 12.0671 10.3896 12.2428C9.81784 12.5859 8.88675 12.5859 8.3149 12.2428C8.02211 12.0671 7.79211 11.8037 7.65759 11.4898C7.53939 11.2141 7.26825 11.0353 6.96825 11.0353C6.66824 11.0353 6.3971 11.2141 6.27891 11.4898C6.14438 11.8037 5.91439 12.0671 5.62159 12.2428C5.04975 12.5859 4.11865 12.5859 3.54681 12.2428C3.25401 12.0671 3.02402 11.8037 2.88949 11.4898Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186922\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bicycle-bike\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.98481 4.14616C8.10138 3.97643 8.29408 3.875 8.5 3.875H11C11.3452 3.875 11.625 4.15482 11.625 4.5C11.625 4.84518 11.3452 5.125 11 5.125H9.41002L10.5759 8.15643C10.7155 8.13562 10.8572 8.125 11 8.125C11.7625 8.125 12.4938 8.4279 13.0329 8.96707C13.5721 9.50624 13.875 10.2375 13.875 11C13.875 11.7625 13.5721 12.4938 13.0329 13.0329C12.4938 13.5721 11.7625 13.875 11 13.875C10.2375 13.875 9.50623 13.5721 8.96707 13.0329C8.4279 12.4938 8.125 11.7625 8.125 11C8.125 10.591 8.21214 10.191 8.37615 9.82483L7.49121 10.8867C7.44552 10.9448 7.38868 10.9957 7.32156 11.0359C7.2397 11.085 7.15112 11.1132 7.06204 11.1221L5.8621 11.2721C5.79913 11.9347 5.50759 12.5583 5.03293 13.0329C4.49376 13.5721 3.7625 13.875 3 13.875C2.2375 13.875 1.50623 13.5721 0.967067 13.0329C0.427901 12.4938 0.125 11.7625 0.125 11C0.125 10.2375 0.427901 9.50624 0.967067 8.96707C1.50623 8.4279 2.2375 8.125 3 8.125C3.23606 8.125 3.46914 8.15404 3.69444 8.21012L4.24074 7.11602L3.64613 6.125H3C2.65482 6.125 2.375 5.84518 2.375 5.5C2.375 5.15482 2.65482 4.875 3 4.875H3.98855C3.99579 4.87487 4.00304 4.87487 4.01027 4.875H5.5C5.84518 4.875 6.125 5.15482 6.125 5.5C6.125 5.84518 5.84518 6.125 5.5 6.125H5.10387L5.48476 6.75983L5.49268 6.77301L7.08317 9.42383L8.89017 7.25551L7.91666 4.72436C7.84274 4.53217 7.86823 4.3159 7.98481 4.14616ZM10.4166 11.2244L9.87819 9.82435C9.86901 9.8331 9.85994 9.84197 9.85095 9.85095C9.5462 10.1557 9.375 10.569 9.375 11C9.375 11.431 9.5462 11.8443 9.85095 12.149C10.1557 12.4538 10.569 12.625 11 12.625C11.431 12.625 11.8443 12.4538 12.149 12.149C12.4538 11.8443 12.625 11.431 12.625 11C12.625 10.569 12.4538 10.1557 12.149 9.85095C11.8549 9.5568 11.4596 9.38706 11.0449 9.37562L11.5833 10.7756C11.7072 11.0978 11.5465 11.4594 11.2244 11.5833C10.9022 11.7073 10.5406 11.5465 10.4166 11.2244ZM9.404 8.59146L9.40927 8.60516C9.39647 8.61366 9.38373 8.62227 9.37106 8.63098L9.404 8.59146ZM5.00331 8.38696L4.5643 9.26617L4.08053 10.2351L5.15476 10.1008L5.97041 9.9988L5.00331 8.38696ZM4.56594 11.4341L3.07753 11.6202C2.84887 11.6488 2.62304 11.5492 2.48989 11.3611C2.35674 11.173 2.33789 10.927 2.44083 10.7208L3.11091 9.37879C3.07411 9.37627 3.03712 9.375 3 9.375C2.56902 9.375 2.1557 9.5462 1.85095 9.85095C1.5462 10.1557 1.375 10.569 1.375 11C1.375 11.431 1.5462 11.8443 1.85095 12.149C2.1557 12.4538 2.56902 12.625 3 12.625C3.43097 12.625 3.8443 12.4538 4.14905 12.149C4.34913 11.949 4.49165 11.7021 4.56594 11.4341Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"braille-blind\", \"keywords\": [ \"disability\", \"braille\", \"blind\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186913)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.8346 4.07702C5.12525 4.07702 4.55021 4.65206 4.55021 5.36141V10.3289H3.92561C2.70211 10.3289 1.87198 11.5731 2.34174 12.7028L2.88113 14H10.886V10.9137C10.886 9.49269 9.73399 8.34071 8.31296 8.34071H7.11899V5.36141C7.11899 4.65206 6.54395 4.07702 5.8346 4.07702ZM5.79513 0.583496C6.31794 0.583496 6.74176 1.00732 6.74176 1.53013C6.74176 2.05294 6.31794 2.47675 5.79513 2.47675C5.27233 2.47675 4.84851 2.05294 4.84851 1.53013C4.84851 1.00732 5.27233 0.583496 5.79513 0.583496ZM9.46786 3.91016C9.99067 3.91016 10.4145 4.33398 10.4145 4.85679C10.4145 5.3796 9.99067 5.80341 9.46786 5.80341C8.94506 5.80341 8.52124 5.3796 8.52124 4.85679C8.52124 4.33398 8.94506 3.91016 9.46786 3.91016ZM12.1554 3.91016C12.6782 3.91016 13.102 4.33398 13.102 4.85679C13.102 5.3796 12.6782 5.80341 12.1554 5.80341C11.6325 5.80341 11.2087 5.3796 11.2087 4.85679C11.2087 4.33398 11.6325 3.91016 12.1554 3.91016ZM1.84506 5.80341C1.32225 5.80341 0.898438 5.3796 0.898438 4.85679C0.898438 4.33398 1.32225 3.91016 1.84506 3.91016C2.36787 3.91016 2.79169 4.33398 2.79169 4.85679C2.79169 5.3796 2.36787 5.80341 1.84506 5.80341ZM1.84506 9.1131C1.32225 9.1131 0.898438 8.68929 0.898438 8.16647C0.898438 7.64366 1.32225 7.21985 1.84506 7.21985C2.36787 7.21985 2.79169 7.64366 2.79169 8.16647C2.79169 8.68929 2.36787 9.1131 1.84506 9.1131ZM1.84506 0.583496C2.36787 0.583496 2.79169 1.00732 2.79169 1.53013C2.79169 2.05294 2.36787 2.47675 1.84506 2.47675C1.32225 2.47675 0.898438 2.05294 0.898438 1.53013C0.898438 1.00732 1.32225 0.583496 1.84506 0.583496ZM12.1554 0.583496C12.6782 0.583496 13.102 1.00732 13.102 1.53013C13.102 2.05294 12.6782 2.47675 12.1554 2.47675C11.6325 2.47675 11.2087 2.05294 11.2087 1.53013C11.2087 1.00732 11.6325 0.583496 12.1554 0.583496Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186913\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bus\", \"keywords\": [ \"transportation\", \"travel\", \"bus\", \"transit\", \"transport\", \"motorcoach\", \"public\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186934)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.49609 1.74604C2.49609 1.63267 2.58799 1.54077 2.70136 1.54077H11.2988C11.4122 1.54077 11.5041 1.63267 11.5041 1.74604V6.00001H2.49609V1.74604ZM13.0041 10.8211C13.0041 11.6229 12.4507 12.2954 11.7051 12.4776V12.9912C11.7051 13.5434 11.2574 13.9912 10.7051 13.9912C10.1528 13.9912 9.70508 13.5434 9.70508 12.9912V12.5264H4.29504V12.9912C4.29504 13.5434 3.84733 13.9912 3.29504 13.9912C2.74276 13.9912 2.29504 13.5434 2.29504 12.9912V12.4776C1.54942 12.2954 0.996094 11.6229 0.996094 10.8211V1.74604C0.996094 0.804246 1.75957 0.0407715 2.70136 0.0407715H11.2988C12.2406 0.0407715 13.0041 0.804246 13.0041 1.74604V10.8211ZM4.00006 10C4.55234 10 5.00006 9.55228 5.00006 9C5.00006 8.44772 4.55234 8 4.00006 8C3.44778 8 3.00006 8.44772 3.00006 9C3.00006 9.55228 3.44778 10 4.00006 10ZM10.0001 10C10.5524 10 11.0001 9.55228 11.0001 9C11.0001 8.44772 10.5524 8 10.0001 8C9.44778 8 9.00006 8.44772 9.00006 9C9.00006 9.55228 9.44778 10 10.0001 10Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186934\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"camping-tent\", \"keywords\": [ \"outdoor\", \"recreation\", \"camping\", \"tent\", \"teepee\", \"tipi\", \"travel\", \"places\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186940)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.66844 0.130659C5.01512 -0.0960186 5.47992 0.00126553 5.7066 0.347949L6.99934 2.32509L8.29209 0.347949C8.51876 0.00126553 8.98357 -0.0960186 9.33025 0.130659C9.67694 0.357337 9.77422 0.82214 9.54754 1.16883L7.89543 3.69558L13.8686 12.831C14.0193 13.0615 14.0315 13.3561 13.9005 13.5983C13.7695 13.8405 13.5162 13.9914 13.2408 13.9914H7.99933V10.9722C7.99933 10.4199 7.55161 9.97222 6.99933 9.97222C6.44705 9.97222 5.99933 10.4199 5.99933 10.9722V13.9914H0.757814C0.482418 13.9914 0.229174 13.8405 0.0981438 13.5983C-0.0328864 13.3561 -0.0206238 13.0615 0.130087 12.831L6.10325 3.69558L4.45115 1.16883C4.22447 0.82214 4.32175 0.357337 4.66844 0.130659Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186940\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cane\", \"keywords\": [ \"disability\", \"cane\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.5115 3.91703C6.59315 3.91703 7.47001 3.04018 7.47001 1.95852C7.47001 0.876858 6.59315 0 5.5115 0C4.42984 0 3.55297 0.876858 3.55297 1.95852C3.55297 3.04018 4.42984 3.91703 5.5115 3.91703ZM9.61788 9.27144C9.59693 9.03339 9.77292 8.82343 10.011 8.80248C10.2047 8.78543 10.386 8.89975 10.4542 9.08191L11.8654 12.8545C12.0105 13.2425 12.4427 13.4393 12.8307 13.2942C13.2186 13.1491 13.4155 12.7169 13.2703 12.329L11.8591 8.55637C11.5547 7.7427 10.7449 7.2321 9.87947 7.30826C8.81618 7.40184 8.03008 8.33966 8.12365 9.40295L8.1801 10.0443C8.21641 10.4569 8.58035 10.7619 8.99297 10.7256C9.40559 10.6893 9.71063 10.3254 9.67432 9.91275L9.61788 9.27144ZM4.16633 4.55082C4.50094 4.46785 4.85471 4.56308 5.10244 4.80283C5.78562 5.46397 6.54501 5.96309 7.76053 5.83514C8.30978 5.77732 8.80191 6.17571 8.85972 6.72496C8.91754 7.27421 8.51916 7.76633 7.9699 7.82415C6.69372 7.95849 5.70119 7.64636 4.91692 7.18126L4.75092 7.68954L6.2713 8.78391C6.65953 9.06337 6.93475 9.47272 7.04703 9.93772L7.63118 12.357C7.76081 12.8939 7.43068 13.4342 6.89382 13.5638C6.35696 13.6934 5.81667 13.3633 5.68705 12.8264L5.10289 10.4071L4.09933 9.68476L3.04851 12.9024C2.87706 13.4274 2.31248 13.714 1.78748 13.5425C1.26248 13.3711 0.975882 12.8065 1.14733 12.2815L3.45642 5.21098C3.56344 4.88327 3.83172 4.6338 4.16633 4.55082Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"capitol\", \"keywords\": [ \"capitol\", \"travel\", \"places\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186928)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.05877 0.015625C7.47299 0.015625 7.80877 0.351411 7.80877 0.765625V1.46812C9.79842 1.82238 11.309 3.56101 11.309 5.65245V5.72979L7.61693 3.62002C7.27107 3.42239 6.84648 3.42239 6.50062 3.62002L2.80841 5.72986V5.65245C2.80841 3.56097 4.31907 1.82231 6.30877 1.4681V0.765625C6.30877 0.351411 6.64456 0.015625 7.05877 0.015625ZM0.856201 9.33098H1.20257V12.4788H1.10156C0.687349 12.4788 0.351562 12.8146 0.351562 13.2288C0.351562 13.643 0.687349 13.9788 1.10156 13.9788H13.016C13.4302 13.9788 13.766 13.643 13.766 13.2288C13.766 12.8146 13.4302 12.4788 13.016 12.4788H12.9148V9.33098H13.2613C13.488 9.33098 13.6864 9.17844 13.7446 8.95932C13.8028 8.7402 13.7062 8.50935 13.5094 8.39686L7.30685 4.85253C7.15313 4.76469 6.96442 4.76469 6.81071 4.85253L0.608131 8.39686C0.411286 8.50935 0.31476 8.7402 0.372951 8.95932C0.431141 9.17844 0.629485 9.33098 0.856201 9.33098ZM2.70257 12.4788V9.33098H3.75567V12.4788H2.70257ZM5.25567 12.4788V9.33098H6.30871V12.4788H5.25567ZM7.80871 12.4788V9.33098H8.86181V12.4788H7.80871ZM10.3618 12.4788V9.33098H11.4148V12.4788H10.3618Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186928\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"car-battery-charging\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 2C1.5 1.72386 1.72386 1.5 2 1.5H5C5.27614 1.5 5.5 1.72386 5.5 2V3.5H8.5V2C8.5 1.72386 8.72386 1.5 9 1.5H12C12.2761 1.5 12.5 1.72386 12.5 2V3.5C13.3284 3.5 14 4.17157 14 5V12.5C14 13.3284 13.3284 14 12.5 14H1.5C0.671573 14 0 13.3284 0 12.5V5C0 4.17157 0.671573 3.5 1.5 3.5V2ZM7.94638 6.61962C8.16201 6.35008 8.11831 5.95678 7.84877 5.74114C7.57923 5.52551 7.18592 5.56921 6.97029 5.83875L5.13696 8.13042C4.98687 8.31803 4.95761 8.57506 5.06169 8.7916C5.16576 9.00814 5.38475 9.14585 5.625 9.14585H7.20718L6.02164 10.9242C5.83016 11.2114 5.90777 11.5994 6.19498 11.7909C6.48218 11.9824 6.87023 11.9047 7.0617 11.6175L8.89503 8.86754C9.02289 8.67576 9.03481 8.42917 8.92605 8.22594C8.81728 8.02272 8.6055 7.89585 8.375 7.89585H6.92539L7.94638 6.61962Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"car-taxi-1\", \"keywords\": [ \"transportation\", \"travel\", \"taxi\", \"transport\", \"cab\", \"car\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path d=\\\"M10.4991 13.2526C9.67009 13.2526 8.99805 12.5805 8.99805 11.7515C8.99805 10.9225 9.67009 10.2505 10.4991 10.2505C11.3282 10.2505 12.0002 10.9225 12.0002 11.7515C12.0002 12.5805 11.3282 13.2526 10.4991 13.2526Z\\\" fill=\\\"black\\\"/>\\n<path d=\\\"M3.49911 13.2526C2.6701 13.2526 1.99805 12.5805 1.99805 11.7515C1.99805 10.9225 2.6701 10.2505 3.49911 10.2505C4.32812 10.2505 5.00017 10.9225 5.00017 11.7515C5.00017 12.5805 4.32812 13.2526 3.49911 13.2526Z\\\" fill=\\\"black\\\"/>\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 1.25C6.58579 1.25 6.25 1.58579 6.25 2V3.50002H4.78172C4.45951 3.48629 4.14131 3.57678 3.87441 3.75827C3.60355 3.94245 3.40043 4.21023 3.29606 4.5207L3.29545 4.52252L2.63913 6.50002H1.5C1.10217 6.50002 0.720644 6.65805 0.43934 6.93936C0.158035 7.22066 0 7.60219 0 8.00002V10C0 10.3978 0.158035 10.7794 0.43934 11.0607C0.603999 11.2253 0.802997 11.3478 1.0195 11.421C1.18121 10.1959 2.22965 9.25037 3.49892 9.25037C4.79537 9.25037 5.86142 10.2368 5.98751 11.5H8.01035C8.13644 10.2368 9.20249 9.25037 10.4989 9.25037C11.7684 9.25037 12.817 10.1962 12.9784 11.4217C13.1957 11.3485 13.3955 11.2259 13.5607 11.0607C13.842 10.7794 14 10.3978 14 10V8.00002C14 7.60219 13.842 7.22066 13.5607 6.93936C13.2794 6.65805 12.8978 6.50002 12.5 6.50002H10.8684L10.2575 4.53182L10.2541 4.52065C10.1498 4.21017 9.94644 3.94245 9.67559 3.75827C9.40869 3.57678 9.09049 3.48629 8.76828 3.50002H7.75V2C7.75 1.58579 7.41421 1.25 7 1.25Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"city-hall\", \"keywords\": [ \"city\", \"hall\", \"travel\", \"places\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186899)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.99976 0.0505371C6.80084 0.0505371 6.61007 0.12956 6.46942 0.27022C6.32876 0.41088 6.24975 0.601655 6.24976 0.800574L6.24987 3.1054L2.96317 5.26984C2.77871 5.39131 2.69581 5.61941 2.75921 5.83097L2.75984 5.83304H11.4039L11.4045 5.83097C11.4679 5.61941 11.385 5.39131 11.2006 5.26984L7.74987 2.99743L7.7498 1.55054H8.37094C8.78515 1.55054 9.12094 1.21475 9.12094 0.800537C9.12094 0.386324 8.78515 0.0505371 8.37094 0.0505371H6.99976ZM13.2418 8.55914H13.0461V12.4936H13.2418C13.656 12.4936 13.9918 12.8294 13.9918 13.2436C13.9918 13.6578 13.656 13.9936 13.2418 13.9936H0.921875C0.507662 13.9936 0.171875 13.6578 0.171875 13.2436C0.171875 12.8294 0.507662 12.4936 0.921875 12.4936H1.11774V8.55914H0.921875C0.507662 8.55914 0.171875 8.22336 0.171875 7.80914C0.171875 7.39493 0.507662 7.05914 0.921875 7.05914H13.2418C13.656 7.05914 13.9918 7.39493 13.9918 7.80914C13.9918 8.22336 13.656 8.55914 13.2418 8.55914ZM11.1356 8.55914H9.88556V12.4936H11.1356V8.55914ZM4.11396 8.55914H2.86396V12.4936H4.11396V8.55914ZM7.95679 12.4936V10.3832C7.95679 9.86913 7.54005 9.45239 7.02597 9.45239C6.5119 9.45239 6.09516 9.86913 6.09516 10.3832V12.4936H7.95679Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186899\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"compass-navigator\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.2945 1.06524C12.2028 0.810179 13.04 1.64747 12.785 2.55569L10.6927 10.0055C10.5788 10.4113 10.2617 10.7284 9.8559 10.8424L2.40603 12.9346C1.49781 13.1897 0.660518 12.3524 0.915585 11.4441L3.00782 3.99433C3.12178 3.58855 3.43889 3.27145 3.84467 3.15749L11.2945 1.06524ZM8.48761 5.51204C7.66585 4.69029 6.33353 4.69029 5.51177 5.51204C4.69002 6.3338 4.69002 7.66612 5.51177 8.48788C6.33353 9.30963 7.66585 9.30963 8.48761 8.48788C9.30936 7.66612 9.30936 6.3338 8.48761 5.51204Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"crutch\", \"keywords\": [ \"disability\", \"crutch\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.6893 2.03072C10.6893 3.15226 9.78011 4.06144 8.65857 4.06144C7.53704 4.06144 6.62785 3.15226 6.62785 2.03072C6.62785 0.909185 7.53704 0 8.65857 0C9.78011 0 10.6893 0.909185 10.6893 2.03072ZM11.1715 5.90257C11.838 6.56903 12.2124 7.47294 12.2124 8.41546V9.4385C12.2124 9.71464 11.9885 9.9385 11.7124 9.9385H10.6893L10.2364 13.562C10.2051 13.8122 9.99242 13.9999 9.74026 13.9999H7.57696C7.32479 13.9999 7.11209 13.8122 7.08082 13.562L6.6437 10.065V8.77678C7.17303 8.47585 7.53005 7.90681 7.53005 7.25439C7.53005 6.52334 7.08178 5.89699 6.44512 5.6352C7.07108 5.13683 7.85066 4.86169 8.65861 4.86169C9.60112 4.86169 10.505 5.23611 11.1715 5.90257ZM1.17578 7.25439C1.17578 6.90922 1.4556 6.62939 1.80078 6.62939H2.68859H4.90629H5.79263C6.13781 6.62939 6.41763 6.90922 6.41763 7.25439C6.41763 7.59957 6.13781 7.87939 5.79263 7.87939H5.53129V10.1021C5.53129 10.8444 5.06486 11.4777 4.40911 11.725V12.7437H4.46147C4.80665 12.7437 5.08647 13.0235 5.08647 13.3687C5.08647 13.7138 4.80665 13.9937 4.46147 13.9937H3.13085C2.78568 13.9937 2.50585 13.7138 2.50585 13.3687C2.50585 13.0235 2.78568 12.7437 3.13085 12.7437H3.15911V11.7147C2.51742 11.4604 2.06359 10.8343 2.06359 10.1021V7.87939H1.80078C1.4556 7.87939 1.17578 7.59957 1.17578 7.25439ZM3.31359 7.87939V10.1021C3.31359 10.3693 3.53022 10.5859 3.79744 10.5859C4.06466 10.5859 4.28129 10.3693 4.28129 10.1021V7.87939H3.31359Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"dangerous-zone-sign\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186943)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.21855 0.226033C6.4541 0.0820473 6.72481 0.00585938 7.00088 0.00585938C7.27695 0.00585938 7.54767 0.0820473 7.78321 0.226033C8.01876 0.370019 8.21 0.576216 8.33588 0.821919L8.33812 0.826281L13.8374 11.8249C13.9522 12.0531 14.0072 12.3075 13.9965 12.5627C13.9858 12.8182 13.91 13.0667 13.7762 13.2846C13.6425 13.5025 13.4552 13.6826 13.2323 13.8078C13.0093 13.933 12.7581 13.9991 12.5024 13.9999H12.5009H1.50088H1.49937C1.24367 13.9991 0.992417 13.933 0.769467 13.8078C0.546516 13.6826 0.35927 13.5025 0.225512 13.2846C0.091753 13.0667 0.0159223 12.8182 0.00522075 12.5627C-0.0054683 12.3075 0.0491743 12.0538 0.163964 11.8257L5.66586 0.821908C5.79174 0.576205 5.983 0.370019 6.21855 0.226033ZM7.90893 5.87812C8.15335 5.54371 8.08041 5.07447 7.746 4.83005C7.41159 4.58562 6.94235 4.65857 6.69793 4.99297L4.67575 7.75957C4.5091 7.98758 4.48465 8.28987 4.61251 8.54169C4.74036 8.7935 4.99883 8.95215 5.28125 8.95215H7.24129L6.0908 10.5262C5.84637 10.8606 5.91931 11.3298 6.25372 11.5742C6.58813 11.8187 7.05737 11.7457 7.30179 11.4113L9.32397 8.64472C9.49062 8.41672 9.51507 8.11443 9.38721 7.86261C9.25936 7.6108 9.00089 7.45215 8.71847 7.45215H6.75843L7.90893 5.87812Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186943\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"earth-1\", \"keywords\": [ \"planet\", \"earth\", \"globe\", \"world\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186964)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.50195 6.99993C1.50195 3.96362 3.96337 1.5022 6.99968 1.5022C7.7833 1.5022 8.52864 1.66615 9.20323 1.96161V2.23807C9.20323 2.68362 9.02625 3.11092 8.71119 3.42597C8.39614 3.74102 7.96884 3.91802 7.52329 3.91802C7.29792 3.91789 7.07484 3.96312 6.86731 4.05099C6.65979 4.13887 6.47206 4.26761 6.31532 4.42953C6.15857 4.59146 6.03602 4.78327 5.95493 4.99354C5.87385 5.20382 5.8359 5.42826 5.84335 5.6535V7.04188C5.84335 7.2625 5.79989 7.48095 5.71547 7.68477C5.63104 7.88859 5.50731 8.07379 5.35131 8.22978C5.1953 8.38578 5.01012 8.50953 4.80629 8.59395C4.60247 8.67838 4.38402 8.72183 4.1634 8.72183H2.46957H1.77699C1.59852 8.18019 1.50195 7.60134 1.50195 6.99993ZM9.71375 11.7821C10.897 11.1091 11.806 10.0097 12.2318 8.69305L12.0958 8.59334C11.6574 8.36677 11.1723 8.24549 10.6788 8.23911H8.80647C8.42286 8.27394 8.06614 8.45095 7.8064 8.73537C7.54664 9.01978 7.40261 9.39105 7.40261 9.77623C7.40261 10.1614 7.54664 10.5326 7.8064 10.817C8.06614 11.1015 8.42286 11.2785 8.80647 11.3133C8.95041 11.3116 9.09325 11.3385 9.22672 11.3925C9.36018 11.4464 9.48162 11.5263 9.58399 11.6275C9.63206 11.675 9.67548 11.7268 9.71375 11.7821ZM6.99968 0.00219727C3.13494 0.00219727 0.00195312 3.13519 0.00195312 6.99993C0.00195312 10.8646 3.13494 13.9976 6.99968 13.9976C10.8644 13.9976 13.9974 10.8646 13.9974 6.99993C13.9974 3.13519 10.8644 0.00219727 6.99968 0.00219727Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186964\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"earth-airplane\", \"keywords\": [ \"travel\", \"plane\", \"trip\", \"airplane\", \"international\", \"adventure\", \"globe\", \"world\", \"airport\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186952)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.6995 1.16256L13.7711 1.52692C13.8461 1.55714 13.909 1.61145 13.9497 1.68129C13.9905 1.75114 14.0068 1.83256 13.9962 1.91272L13.0317 4.88121C12.9685 5.0598 12.8373 5.20619 12.6667 5.28848C12.4961 5.37078 12.2999 5.38232 12.1208 5.32059L9.38809 4.43111H9.20591L9.48454 5.77068C9.49522 5.83774 9.4885 5.90641 9.46503 5.97012C9.44156 6.03383 9.40212 6.09045 9.35049 6.13455C9.29887 6.17865 9.23678 6.20875 9.17018 6.22197C9.10358 6.23519 9.03471 6.23109 8.97015 6.21006L7.70559 5.80283C7.64364 5.78342 7.58759 5.74869 7.54263 5.70186C7.49767 5.65503 7.46526 5.59761 7.44839 5.53492L6.92328 3.73453L6.64465 3.63809L3.84763 2.73789C3.74798 2.71281 3.65477 2.66693 3.57411 2.60328C3.49345 2.53962 3.42716 2.45963 3.37959 2.36855C3.33203 2.27747 3.30426 2.17736 3.29812 2.07479C3.29198 1.97222 3.30759 1.86951 3.34395 1.7734C3.47585 1.40293 3.688 1.0662 3.96522 0.78729C4.24245 0.508381 4.57789 0.294192 4.94755 0.160049C5.31721 0.0259061 5.71195 -0.0248749 6.10353 0.0113394C6.49511 0.0475537 6.87384 0.169868 7.21263 0.369531L11.4992 2.51285L12.2815 1.30187C12.3264 1.23621 12.3916 1.18705 12.4671 1.16189C12.5425 1.13674 12.6242 1.13697 12.6995 1.16256ZM0.00976562 8.32049C0.00976562 6.28347 1.08606 4.49783 2.70099 3.50123C2.73295 3.53002 2.76589 3.55781 2.79975 3.58454C3.00559 3.74697 3.24152 3.86677 3.49377 3.93715L4.61188 4.297C2.82678 4.76461 1.50977 6.38875 1.50977 8.32049C1.50977 8.79415 1.58895 9.2493 1.73479 9.67345H2.10931H3.44018C3.61352 9.67345 3.78516 9.63931 3.94531 9.57298C4.10546 9.50665 4.25097 9.40942 4.37354 9.28685C4.49611 9.16428 4.59334 9.01876 4.65967 8.85862C4.726 8.69847 4.76015 8.52683 4.76015 8.35349V7.26261C4.75429 7.08563 4.78411 6.90929 4.84782 6.74407C4.91152 6.57886 5.00782 6.42815 5.13098 6.30092C5.25414 6.17369 5.40164 6.07254 5.5647 6.00349C5.72775 5.93445 5.90304 5.89891 6.08011 5.89901C6.13697 5.89901 6.19346 5.89534 6.24925 5.88813C6.32274 6.14286 6.45709 6.37606 6.6409 6.56752C6.82999 6.7645 7.06536 6.91098 7.32552 6.99365L8.58297 7.39859L8.58561 7.39944C8.85292 7.48603 9.13797 7.50277 9.41361 7.44804C9.69015 7.39313 9.94797 7.26814 10.1623 7.08501C10.3767 6.90189 10.5405 6.66679 10.6379 6.40223C10.6654 6.32756 10.6874 6.25125 10.7038 6.17391L10.9368 6.24976C11.1891 6.89109 11.3277 7.58961 11.3277 8.32049C11.3277 11.4459 8.79408 13.9795 5.66873 13.9795C2.54337 13.9795 0.00976562 11.4459 0.00976562 8.32049ZM6.00453 12.4661C7.33865 12.3595 8.49527 11.6233 9.17717 10.5548L9.07768 10.4819C8.73324 10.3038 8.35206 10.2085 7.96437 10.2035H6.4932C6.1918 10.2309 5.91152 10.37 5.70743 10.5935C5.50333 10.8169 5.39017 11.1086 5.39017 11.4113C5.39017 11.7139 5.50333 12.0056 5.70743 12.2291C5.79412 12.324 5.89456 12.4037 6.00453 12.4661Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186952\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"emergency-exit\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186961)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.71429 1.4851C4.71429 0.701307 5.34968 0.065918 6.13347 0.065918H12.5677C13.3515 0.065918 13.9869 0.701307 13.9869 1.4851V12.5152C13.9869 13.299 13.3515 13.9344 12.5677 13.9344H7.66557C7.74331 13.7209 7.78571 13.4905 7.78571 13.2501V11.4322C7.78571 11.3493 7.78228 11.2668 7.77551 11.1848C7.94979 11.2162 8.12765 11.2323 8.30718 11.2323H9.67856C10.7831 11.2323 11.6785 10.3368 11.6785 9.23227C11.6785 8.3224 11.0709 7.55445 10.2394 7.312C10.8021 6.7864 11.154 6.03778 11.154 5.20698C11.154 3.61656 9.86466 2.32727 8.27424 2.32727C6.98386 2.32727 5.8917 3.17599 5.52555 4.34566C5.26778 4.23628 4.99478 4.16301 4.71429 4.12906V1.4851ZM6.64453 5.20698C6.64453 4.30691 7.37418 3.57727 8.27424 3.57727C9.1743 3.57727 9.90395 4.30691 9.90395 5.20698C9.90395 6.10704 9.1743 6.83669 8.27424 6.83669C7.37418 6.83669 6.64453 6.10704 6.64453 5.20698ZM6.72667 6.82781L6.72912 6.83027L8.21956 8.32071C8.243 8.34415 8.27479 8.35732 8.30795 8.35732L9.67933 8.3573C10.1626 8.35729 10.5543 8.74903 10.5543 9.23228C10.5543 9.71553 10.1626 10.1073 9.67936 10.1073H8.30797C7.81068 10.1073 7.33376 9.90978 6.98212 9.55814L6.10795 8.68397L5.39852 9.3934L6.11153 10.1064C6.46316 10.458 6.6607 10.9349 6.6607 11.4322V12.9999C6.6607 13.4831 6.26895 13.8749 5.7857 13.8749C5.30245 13.8749 4.9107 13.4831 4.9107 12.9999V11.4322C4.9107 11.3991 4.89753 11.3673 4.87409 11.3438L4.14883 10.6186C3.82459 10.8647 3.42686 11.0001 3.01517 11.0001H1C0.516751 11.0001 0.125 10.6083 0.125 10.1251C0.125 9.64185 0.516751 9.2501 1 9.2501H3.01517C3.04832 9.2501 3.08011 9.23693 3.10356 9.21349L4.87051 7.44654L4.44291 7.01893C4.41947 6.99549 4.38767 6.98232 4.35452 6.98232L2.53647 6.9823C2.05323 6.98229 1.66148 6.59054 1.66149 6.10729C1.66149 5.62404 2.05325 5.23229 2.5365 5.2323L4.35454 5.23232C4.85181 5.23233 5.32872 5.42987 5.68034 5.78149L6.72421 6.82536L6.72667 6.82781Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186961\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"fire-alarm-2\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186955)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.50965 0.75C5.50965 0.335786 5.17386 0 4.75965 0C3.50042 0 2.29276 0.500227 1.40235 1.39064C0.511946 2.28105 0.0117188 3.4887 0.0117188 4.74793C0.0117188 5.16215 0.347506 5.49793 0.761719 5.49793C1.17593 5.49793 1.51171 5.16215 1.51171 4.74793C1.51171 3.88653 1.85392 3.0604 2.46302 2.4513C3.07212 1.84219 3.89824 1.5 4.75965 1.5C5.17386 1.5 5.50965 1.16421 5.50965 0.75ZM8.48855 0.75C8.48855 0.335786 8.82433 0 9.23855 0C10.4978 0 11.7054 0.500227 12.5958 1.39064C13.4862 2.28105 13.9865 3.4887 13.9865 4.74793C13.9865 5.16215 13.6507 5.49793 13.2365 5.49793C12.8222 5.49793 12.4865 5.16215 12.4865 4.74793C12.4865 3.88653 12.1443 3.0604 11.5352 2.4513C10.9261 1.84219 10.0999 1.5 9.23855 1.5C8.82433 1.5 8.48855 1.16421 8.48855 0.75ZM6.48892 3.31959C6.17576 3.18539 5.85688 3.27524 5.65711 3.46702C5.47295 3.64381 5.32913 3.9886 5.52559 4.32225C5.83425 4.8609 6.04005 5.45226 6.13252 6.06616C6.2457 6.83737 6.15016 7.6236 5.77851 8.23064C5.57729 8.01725 5.41396 7.82599 5.29961 7.60954C5.22545 7.46499 5.06183 7.27181 4.79236 7.20746C4.48807 7.1348 4.20776 7.26027 3.99189 7.46772C3.21892 8.11282 2.72715 9.11019 2.82312 10.1575C2.87524 12.3983 4.84698 13.9743 6.93685 14C9.0871 14.0264 11.0396 12.4258 11.0573 10.1325V10.1287C11.0573 8.78315 10.6526 7.46882 9.89606 6.35634C9.07475 5.04826 7.90057 3.99871 6.50886 3.32866L6.49899 3.32391L6.48892 3.31959ZM9.23855 2.49871C8.82433 2.49871 8.48855 2.83449 8.48855 3.24871C8.48855 3.66292 8.82433 3.99871 9.23855 3.99871C9.43746 3.99871 9.62821 4.07772 9.76888 4.21838C9.90952 4.35903 9.98855 4.54979 9.98855 4.74871C9.98855 5.16292 10.3243 5.49871 10.7385 5.49871C11.1527 5.49871 11.4885 5.16292 11.4885 4.74871C11.4885 4.15197 11.2515 3.57967 10.8295 3.15772C10.4076 2.73576 9.83528 2.49871 9.23855 2.49871ZM3.16908 3.1576C3.46198 2.86471 3.93684 2.86471 4.22974 3.1576C4.52263 3.4505 4.52263 3.92537 4.22974 4.21826C4.08909 4.35892 4.01008 4.54968 4.01008 4.74859C4.01008 5.16281 3.67429 5.49859 3.26008 5.49859C2.84585 5.49859 2.51008 5.16281 2.51008 4.74859C2.51008 4.15186 2.74712 3.57956 3.16908 3.1576Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186955\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"fire-extinguisher-sign\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186949)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.7575 0.170243C13.3886 0.0124558 14 0.489814 14 1.14039V3.85961C14 4.51018 13.3886 4.98754 12.7575 4.82975L9.75747 4.07975C9.31728 3.96971 9.00701 3.5774 9.00012 3.125H6.125V4.05564C7.75979 4.35033 9 5.78033 9 7.5V12.5C9 13.3284 8.32843 14 7.5 14H3.5C2.67157 14 2 13.3284 2 12.5V7.5C2 5.78033 3.24021 4.35033 4.875 4.05564V3.125H2.65625C1.89686 3.125 1.28125 3.74061 1.28125 4.5V6.5C1.28125 6.84518 1.00143 7.125 0.65625 7.125C0.311072 7.125 0.03125 6.84518 0.03125 6.5V4.5C0.03125 3.05025 1.2065 1.875 2.65625 1.875H5.5H9.00012C9.00701 1.4226 9.31728 1.03029 9.75746 0.920243L12.7575 0.170243ZM6 8H8V11H6C5.44772 11 5 10.5523 5 10V9C5 8.44772 5.44772 8 6 8Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186949\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"gas-station-fuel-petroleum\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186937)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.7232 0.858595C13.9673 1.10267 13.9673 1.4984 13.7232 1.74248L12.9303 2.53534V4.70437C12.9303 4.99117 12.7351 5.24115 12.4569 5.31071L10.9784 5.68035V6.66832C10.9784 6.86216 11.1355 7.01929 11.3293 7.01929C12.7525 7.01929 13.9062 8.17301 13.9062 9.59621C13.9062 11.0194 12.7525 12.1732 11.3293 12.1732H8.90144V12.5001C8.90144 13.3285 8.22987 14.0001 7.40144 14.0001H1.59375C0.765323 14.0001 0.09375 13.3285 0.09375 12.5001V3.78851C0.09375 2.4078 1.21304 1.28851 2.59375 1.28851H6.40144C7.78215 1.28851 8.90144 2.4078 8.90144 3.78851V10.9232H11.3293C12.0622 10.9232 12.6562 10.3291 12.6562 9.59621C12.6562 8.86337 12.0622 8.26929 11.3293 8.26929C10.4451 8.26929 9.72835 7.55251 9.72835 6.66832V5.20336C9.72821 5.19569 9.72821 5.18804 9.72835 5.18041V4.22842C9.72835 4.06266 9.7942 3.90369 9.91141 3.78648L12.8393 0.858595C13.0834 0.614518 13.4791 0.614518 13.7232 0.858595ZM1.09375 7.14429V4.88092H7.90144V7.14429H1.09375ZM5.96155 9.4592C6.30673 9.4592 6.58655 9.73902 6.58655 10.0842V11.0602C6.58655 11.4054 6.30673 11.6852 5.96155 11.6852C5.61637 11.6852 5.33655 11.4054 5.33655 11.0602V10.0842C5.33655 9.73902 5.61637 9.4592 5.96155 9.4592Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186937\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"hearing-deaf-1\", \"keywords\": [ \"disability\", \"hearing\", \"deaf\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186970)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.21967 1.28033C-0.0732233 0.987437 -0.0732233 0.512563 0.21967 0.21967C0.512563 -0.0732233 0.987437 -0.0732233 1.28033 0.21967L3.81549 2.75483C4.51516 1.52743 5.8096 0.353516 7.82864 0.353516C9.39786 0.353516 10.5962 0.919667 11.3924 1.83699C12.1736 2.73693 12.5096 3.90687 12.5096 5.03451C12.5096 6.09336 11.958 7.11595 11.3893 7.90774C11.0332 8.40354 10.6378 8.85308 10.2824 9.22175L13.7803 12.7197C14.0732 13.0126 14.0732 13.4874 13.7803 13.7803C13.4874 14.0732 13.0126 14.0732 12.7197 13.7803L0.21967 1.28033ZM9.22154 8.16088C9.53263 7.83678 9.87105 7.45023 10.171 7.03267C10.6886 6.31207 11.0096 5.61081 11.0096 5.03451C11.0096 4.18774 10.7561 3.39219 10.2596 2.82023C9.77827 2.26566 9.01112 1.85352 7.82864 1.85352C6.31829 1.85352 5.3748 2.84259 4.92976 3.8691L9.22154 8.16088ZM7.38537 11.0887C7.4959 10.8949 7.59115 10.6847 7.6711 10.4807L8.80534 11.6149C8.76826 11.6876 8.72929 11.7601 8.68838 11.8318C8.43058 12.2839 8.04205 12.8017 7.47437 13.079C6.99709 13.3122 6.50975 13.483 5.97093 13.4947C5.42542 13.5066 4.90393 13.3543 4.3554 13.072C3.98711 12.8824 3.84222 12.4302 4.03179 12.0619C4.22136 11.6936 4.67359 11.5487 5.04188 11.7383C5.43895 11.9427 5.7115 12 5.93823 11.9951C6.17166 11.99 6.43478 11.9174 6.81597 11.7312C6.98183 11.6502 7.18292 11.4437 7.38537 11.0887ZM7.51421 4.45156C7.63837 4.39229 7.75655 4.36488 7.87745 4.36997C7.99682 4.37499 8.16169 4.41339 8.3787 4.54845C8.53295 4.64445 8.61539 4.75177 8.66401 4.85448C8.7157 4.96371 8.74047 5.09316 8.73892 5.23418C8.73437 5.64836 9.06644 5.98782 9.48063 5.99237C9.89482 5.99693 10.2343 5.66485 10.2388 5.25066C10.246 4.59929 10.0034 3.79285 9.17129 3.27495C8.31561 2.7424 7.51269 2.7901 6.86794 3.09792C6.49414 3.27639 6.33579 3.72408 6.51426 4.09788C6.69272 4.47168 7.14041 4.63002 7.51421 4.45156Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186970\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"hearing-deaf-2\", \"keywords\": [ \"disability\", \"hearing\", \"deaf\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186946)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.2947 0.509012C11.9811 0.238458 11.5075 0.273389 11.2369 0.587034C10.9664 0.900679 11.0013 1.37427 11.3149 1.64483C11.6369 1.92259 11.8746 2.30812 12.014 2.79514C12.128 3.19337 12.5432 3.42379 12.9415 3.3098C13.3397 3.19582 13.5701 2.78059 13.4561 2.38237C13.2491 1.65894 12.8706 1.00576 12.2947 0.509012ZM4.14313 4.43674C4.53652 3.36725 5.46541 2.28249 7.02272 2.28249C8.16586 2.28249 8.90492 2.68065 9.36801 3.21418C9.8462 3.7651 10.0913 4.53255 10.0913 5.3511C10.0913 5.90079 9.78627 6.57317 9.28839 7.26887C8.80612 7.94272 8.21865 8.53469 7.81958 8.90436C7.42426 9.27056 7.14865 9.73495 6.99372 10.2251C6.89621 10.5337 6.75443 10.9064 6.56887 11.2297C6.37427 11.5687 6.18338 11.7619 6.02959 11.8371C5.46229 12.1142 5.13434 12.1528 4.71212 12.0112C4.31941 11.8795 3.89427 12.0911 3.76255 12.4838C3.63083 12.8765 3.84241 13.3016 4.23512 13.4333C5.15265 13.7411 5.9018 13.5689 6.68798 13.1849C7.24075 12.9148 7.61897 12.4134 7.86979 11.9764C8.12965 11.5237 8.30963 11.039 8.42399 10.6772C8.50814 10.4109 8.65226 10.1777 8.83894 10.0047C9.27451 9.60127 9.94307 8.93147 10.5082 8.14185C11.0576 7.37407 11.5913 6.3813 11.5913 5.3511C11.5913 4.25168 11.2637 3.10983 10.5008 2.23094C9.72285 1.33465 8.55261 0.782483 7.02272 0.782483C4.61269 0.782483 3.25663 2.50172 2.73535 3.91892C2.59235 4.30766 2.79158 4.73873 3.18032 4.88172C3.56907 5.02471 4.00014 4.82549 4.14313 4.43674ZM2.73129 8.0881C3.02419 7.7952 3.49906 7.7952 3.79196 8.0881L4.25246 8.5486C4.54535 8.84149 4.54535 9.31637 4.25246 9.60925C3.95957 9.90214 3.48469 9.90214 3.1918 9.60925L2.73129 9.14876C2.4384 8.85585 2.4384 8.38099 2.73129 8.0881ZM1.63775 9.1534C1.34486 8.86051 0.869985 8.86051 0.577092 9.1534C0.284199 9.4463 0.284199 9.92117 0.577092 10.214L2.07048 11.7074C2.36337 12.0003 2.83825 12.0003 3.13114 11.7074C3.42403 11.4145 3.42403 10.9397 3.13114 10.6468L1.63775 9.1534ZM3.77994 5.93722C4.07283 5.64432 4.54771 5.64432 4.8406 5.93722L6.33399 7.43061C6.62688 7.7235 6.62688 8.19837 6.33399 8.49127C6.0411 8.78415 5.56622 8.78415 5.27333 8.49127L3.77994 6.99788C3.48705 6.70498 3.48705 6.23011 3.77994 5.93722ZM6.5355 4.8972C6.64708 4.84393 6.75116 4.82024 6.85651 4.82467C6.96034 4.82904 7.10821 4.86242 7.30683 4.98603C7.44477 5.07187 7.51663 5.1664 7.55879 5.25548C7.60403 5.35107 7.62646 5.46603 7.62506 5.59348C7.62051 6.00767 7.95259 6.34713 8.36677 6.35168C8.78096 6.35624 9.12042 6.02416 9.12497 5.60997C9.13184 4.98572 8.89894 4.21012 8.09942 3.71253C7.28053 3.20287 6.50881 3.24775 5.88923 3.54356C5.51544 3.72202 5.35709 4.16972 5.53555 4.54351C5.71401 4.9173 6.16171 5.07566 6.5355 4.8972Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186946\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"high-speed-train-front\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186982)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.46289 1.2008C3.61824 0.359325 5.17199 0.00756836 6.8189 0.00756836C8.46581 0.00756836 10.0196 0.359325 11.1749 1.2008C12.3538 2.05943 13.063 3.3891 13.063 5.20286C13.063 6.79648 12.5176 8.51989 11.6201 9.94196C10.5993 8.14537 8.79236 7.37167 6.82144 7.37167C4.84888 7.37167 3.04067 8.14666 2.02039 9.94618C1.12134 8.52331 0.57486 6.79806 0.57486 5.20286C0.57486 3.3891 1.28398 2.05943 2.46289 1.2008ZM2.86234 11.0697C3.90825 12.2459 5.2765 13.0811 6.8189 13.0811C8.363 13.0811 9.73258 12.2441 10.779 11.0658C10.0794 9.36769 8.61996 8.62167 6.82144 8.62167C5.02164 8.62167 3.56129 9.36873 2.86234 11.0697ZM3.22562 4.24619C3.22562 3.54419 3.7947 2.97511 4.4967 2.97511H9.14487C9.84687 2.97511 10.416 3.54419 10.416 4.24619C10.416 4.94819 9.84687 5.51727 9.14487 5.51727H4.4967C3.7947 5.51727 3.22562 4.94819 3.22562 4.24619ZM2.45945 11.7472C2.73559 12.056 2.70916 12.5301 2.40042 12.8063L1.2793 13.809C0.970562 14.0851 0.496424 14.0587 0.220284 13.75C-0.0558567 13.4412 -0.0294305 12.9671 0.279308 12.691L1.40043 11.6882C1.70917 11.4121 2.18331 11.4385 2.45945 11.7472ZM11.5389 11.7472C11.2628 12.056 11.2892 12.5301 11.598 12.8063L12.7191 13.809C13.0278 14.0851 13.502 14.0587 13.7781 13.75C14.0542 13.4412 14.0278 12.9671 13.7191 12.691L12.598 11.6882C12.2892 11.4121 11.8151 11.4385 11.5389 11.7472Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186982\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"hot-spring\", \"keywords\": [ \"relax\", \"location\", \"outdoor\", \"recreation\", \"spa\", \"travel\", \"places\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.31055 1.47708C7.62754 1.7437 7.66837 2.21681 7.40175 2.53381C7.06703 2.93176 6.98388 3.32746 7.02825 3.78112C7.07724 4.28201 7.27705 4.81867 7.52833 5.47592L7.54538 5.5205C7.77602 6.12345 8.0496 6.83862 8.12019 7.56038C8.19715 8.34724 8.03764 9.16322 7.40175 9.91923C7.13512 10.2362 6.66201 10.2771 6.34502 10.0104C6.02802 9.74381 5.98719 9.2707 6.25381 8.9537C6.58854 8.55575 6.67168 8.16005 6.62731 7.70639C6.57832 7.2055 6.37852 6.66885 6.12724 6.01159L6.11019 5.96701C5.87954 5.36407 5.60597 4.64889 5.53537 3.92713C5.45842 3.14027 5.61793 2.32429 6.25381 1.56828C6.52044 1.25128 6.99355 1.21045 7.31055 1.47708ZM4.50111 3.45699C4.76774 3.14 4.72691 2.66689 4.40991 2.40026C4.09292 2.13364 3.6198 2.17447 3.35318 2.49146C2.76036 3.19628 2.54339 3.89234 2.63799 4.58964C2.71976 5.19245 3.0329 5.71253 3.25713 6.08494L3.28425 6.13001C3.54428 6.56283 3.69373 6.83273 3.72992 7.09953C3.75665 7.29656 3.73097 7.58138 3.35318 8.03053C3.08656 8.34753 3.12739 8.82064 3.44438 9.08727C3.76137 9.35389 4.23449 9.31306 4.50111 8.99607C5.09394 8.29125 5.3109 7.59519 5.21631 6.89789C5.13453 6.29508 4.82139 5.775 4.59716 5.40259L4.57004 5.35752C4.31001 4.9247 4.16057 4.6548 4.12437 4.388C4.09764 4.19097 4.12333 3.90615 4.50111 3.45699ZM12.9118 6.887C12.6099 6.60338 12.1352 6.61819 11.8516 6.92008C11.568 7.22196 11.5828 7.69661 11.8847 7.98022C11.9892 8.0784 12.0827 8.18817 12.1632 8.30773C12.3373 8.5663 12.4452 8.86265 12.4782 9.17115C12.4623 9.67527 12.0593 10.2961 11.0499 10.837C10.0514 11.3721 8.6206 11.7264 6.99916 11.7264C5.37695 11.7264 3.94576 11.3742 2.94727 10.8412C1.93714 10.302 1.53526 9.68287 1.51999 9.17934C1.55179 8.8691 1.65933 8.57082 1.83376 8.31057C1.91466 8.18987 2.00878 8.07909 2.1141 7.9801C2.41592 7.69641 2.43061 7.22176 2.14693 6.91995C1.86324 6.61813 1.38859 6.60343 1.08677 6.88712C0.899495 7.06315 0.731918 7.26034 0.587753 7.47542C0.265685 7.95594 0.0710078 8.51008 0.0222077 9.08618C0.0204243 9.10724 0.0195312 9.12836 0.0195312 9.14949C0.0195312 10.4805 1.01647 11.5109 2.24092 12.1645C3.49708 12.835 5.1807 13.2264 6.99916 13.2264C8.8184 13.2264 10.5023 12.8323 11.7584 12.1592C12.9828 11.503 13.9787 10.4702 13.9787 9.13995C13.9787 9.11788 13.9778 9.09582 13.9758 9.07383C13.925 8.49987 13.7294 7.94824 13.4075 7.47002C13.264 7.25696 13.0976 7.06157 12.9118 6.887ZM10.5244 3.36388C10.7396 3.00995 10.6271 2.54859 10.2732 2.33339C9.91934 2.11819 9.45798 2.23066 9.24278 2.58458C8.81416 3.28951 8.68162 3.94102 8.74252 4.56202C8.79802 5.1279 9.01424 5.6252 9.18026 6.00701L9.19569 6.04253C9.37876 6.46405 9.5014 6.76118 9.53188 7.07191C9.55868 7.34524 9.51581 7.6746 9.24278 8.12365C9.02758 8.47758 9.14004 8.93894 9.49397 9.15414C9.84789 9.36934 10.3092 9.25687 10.5244 8.90295C10.953 8.19802 11.0856 7.54651 11.0247 6.92551C10.9692 6.35962 10.7529 5.86233 10.5869 5.48051L10.5715 5.445C10.3884 5.02348 10.2658 4.72635 10.2353 4.41562C10.2085 4.14229 10.2514 3.81293 10.5244 3.36388Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"hotel-air-conditioner\", \"keywords\": [ \"heating\", \"ac\", \"air\", \"hvac\", \"cool\", \"cooling\", \"cold\", \"hot\", \"conditioning\", \"hotel\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187060)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0C0.706 0 0 0.607332 0 1.4307V6.08419C0 6.90755 0.706 7.51488 1.5 7.51488H2.08224L2.08206 7.5V5.5C2.08206 5.0542 2.27253 4.63641 2.59534 4.33563C2.91666 4.03623 3.34359 3.875 3.78029 3.875H10.2197C10.6564 3.875 11.0833 4.03623 11.4046 4.33563C11.7274 4.63641 11.9179 5.0542 11.9179 5.5V7.5L11.9177 7.51488H12.5C13.294 7.51488 14 6.90755 14 6.08419V1.4307C14 0.607332 13.294 0 12.5 0H1.5ZM10.6681 7.51488H3.33189L3.33206 7.5V5.5C3.33206 5.41537 3.36774 5.32445 3.44747 5.25016C3.52869 5.17448 3.64772 5.125 3.78029 5.125H10.2197C10.3522 5.125 10.4713 5.17448 10.5525 5.25016C10.6322 5.32445 10.6679 5.41537 10.6679 5.5V7.5L10.6681 7.51488ZM4.75 9.74887C4.75 9.33466 4.41421 8.99887 4 8.99887C3.58579 8.99887 3.25 9.33466 3.25 9.74887V10.8489C3.25 11.2079 2.95899 11.4989 2.6 11.4989H2.5C2.08579 11.4989 1.75 11.8347 1.75 12.2489C1.75 12.6631 2.08579 12.9989 2.5 12.9989H2.6C3.78741 12.9989 4.75 12.0363 4.75 10.8489V9.74887ZM10 8.99887C9.58579 8.99887 9.25 9.33466 9.25 9.74887V10.8489C9.25 12.0363 10.2126 12.9989 11.4 12.9989H11.5C11.9142 12.9989 12.25 12.6631 12.25 12.2489C12.25 11.8347 11.9142 11.4989 11.5 11.4989H11.4C11.041 11.4989 10.75 11.2079 10.75 10.8489V9.74887C10.75 9.33466 10.4142 8.99887 10 8.99887ZM7.75 9.74887C7.75 9.33466 7.41421 8.99887 7 8.99887C6.58579 8.99887 6.25 9.33466 6.25 9.74887V13.2489C6.25 13.6631 6.58579 13.9989 7 13.9989C7.41421 13.9989 7.75 13.6631 7.75 13.2489V9.74887Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187060\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"hotel-bed-2\", \"keywords\": [ \"bed\", \"double\", \"bedroom\", \"bedrooms\", \"queen\", \"king\", \"full\", \"hotel\", \"hotel\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186979)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.046875 1.53924C0.046875 0.714762 0.71525 0.0463867 1.53973 0.0463867H12.4612C13.2857 0.0463867 13.9541 0.714762 13.9541 1.53924V5.85166L13.9505 5.85165H0.0504162L0.046875 5.85166V1.53924ZM0.046875 7.10164V12.4607C0.046875 13.2852 0.71525 13.9536 1.53973 13.9536H12.4612C13.2857 13.9536 13.9541 13.2852 13.9541 12.4607V7.10164L13.9505 7.10165H0.0504162L0.046875 7.10164ZM6.31464 3.54751V2.56894C6.31464 2.01665 5.86693 1.56894 5.31464 1.56894H2.62948C2.0772 1.56894 1.62948 2.01665 1.62948 2.56894V3.54751C1.62948 4.09979 2.0772 4.54751 2.62948 4.54751H5.31464C5.86693 4.54751 6.31464 4.09979 6.31464 3.54751ZM12.3714 2.56894V3.54751C12.3714 4.09979 11.9237 4.54751 11.3714 4.54751H8.68625C8.13396 4.54751 7.68625 4.09979 7.68625 3.54751V2.56894C7.68625 2.01665 8.13396 1.56894 8.68625 1.56894H11.3714C11.9237 1.56894 12.3714 2.01665 12.3714 2.56894Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186979\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"hotel-laundry\", \"keywords\": [ \"laundry\", \"machine\", \"hotel\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1 1.5C1 0.671573 1.67157 0 2.5 0H11.5C12.3284 0 13 0.671573 13 1.5V2.4812H1V1.5ZM1 3.7312V12.5C1 13.3284 1.67157 14 2.5 14H11.5C12.3284 14 13 13.3284 13 12.5V3.7312H1ZM3.39249 8.49998C3.39249 8.30052 3.40875 8.10448 3.44008 7.91325C3.44153 7.90337 3.44319 7.89355 3.44503 7.8838C3.47974 7.68243 3.53118 7.48648 3.59795 7.29755C4.09283 5.89742 5.42808 4.89249 6.99998 4.89249C8.99234 4.89249 10.6075 6.50762 10.6075 8.49998C10.6075 10.4923 8.99234 12.1075 6.99998 12.1075C5.00762 12.1075 3.39249 10.4923 3.39249 8.49998ZM4.50227 8.20047C4.54267 7.95962 4.75116 7.7832 4.99538 7.7832H5.34922C6.11387 7.7832 6.56218 7.96974 6.96521 8.13744L7.02536 8.16243C7.40231 8.31853 7.78282 8.46557 8.5362 8.46557H9.03295C9.18002 8.46557 9.31963 8.53032 9.41463 8.64258C9.50963 8.75485 9.55039 8.90325 9.52606 9.0483C9.32389 10.2533 8.27674 11.1715 7.01416 11.1715C5.60744 11.1715 4.46707 10.0311 4.46707 8.62438C4.46707 8.48031 4.47908 8.33866 4.50227 8.20047Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"hotel-one-star\", \"keywords\": [ \"one\", \"star\", \"reviews\", \"review\", \"rating\", \"hotel\", \"star\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186967)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.18961 0.255506C6.43738 0.118345 6.71594 0.0463867 6.99914 0.0463867C7.28234 0.0463867 7.5609 0.118345 7.80867 0.255506C8.05614 0.392502 8.2648 0.590057 8.4151 0.829667L8.41564 0.830537L10.2139 3.67632L12.488 3.95496C12.7857 3.98875 13.0686 4.10288 13.3064 4.28516C13.545 4.46803 13.7292 4.71255 13.839 4.99237C13.9489 5.27218 13.9803 5.57668 13.9299 5.87303C13.8795 6.16939 13.7492 6.44636 13.5529 6.6741L13.551 6.67633L11.6185 8.89274L12.2445 11.9766L12.2452 11.9804C12.3058 12.2908 12.2764 12.612 12.1607 12.9063C12.045 13.2007 11.8477 13.4558 11.592 13.6419L11.5861 13.6462C11.3285 13.8279 11.0249 13.9336 10.7101 13.9511C10.3955 13.9685 10.0823 13.8973 9.80631 13.7454L6.99914 12.2092L4.19252 13.7451C3.91649 13.897 3.60275 13.9685 3.28818 13.9511C2.97338 13.9336 2.66984 13.8279 2.41221 13.6462L2.40623 13.642C2.15051 13.4559 1.95322 13.2007 1.83751 12.9063C1.72179 12.612 1.69247 12.2908 1.753 11.9804L1.75374 11.9766L2.37972 8.89274L0.447223 6.67633L0.44529 6.67411C0.249069 6.44637 0.118732 6.16939 0.068316 5.87303C0.0178994 5.57668 0.049322 5.27218 0.159196 4.99237C0.269071 4.71255 0.453231 4.46803 0.691824 4.28516C0.929649 4.10288 1.21257 3.98875 1.51027 3.95496L3.78432 3.67632L5.58264 0.830537L5.58318 0.829668C5.73349 0.590057 5.94214 0.392502 6.18961 0.255506ZM7.62413 4.85865C7.62413 4.51347 7.34431 4.23365 6.99913 4.23365C6.65395 4.23365 6.37413 4.51347 6.37413 4.85865C6.37413 5.10563 6.17392 5.30585 5.92694 5.30585H5.56954C5.22436 5.30585 4.94454 5.58567 4.94454 5.93085C4.94454 6.27602 5.22436 6.55585 5.56954 6.55585H5.92694C6.08172 6.55585 6.23165 6.53513 6.37413 6.49631V8.52244H5.56954C5.22436 8.52244 4.94454 8.80226 4.94454 9.14744C4.94454 9.49262 5.22436 9.77244 5.56954 9.77244H8.42873C8.7739 9.77244 9.05373 9.49262 9.05373 9.14744C9.05373 8.80226 8.7739 8.52244 8.42873 8.52244H7.62413V4.85865Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186967\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"hotel-shower-head\", \"keywords\": [ \"bathe\", \"bath\", \"bathroom\", \"shower\", \"water\", \"head\", \"hotel\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.75 0.762451C7.75 0.348237 7.41421 0.0124512 7 0.0124512C6.58579 0.0124512 6.25 0.348237 6.25 0.762451V2.07678C5.33477 2.23148 4.48304 2.66686 3.81802 3.33187C2.97411 4.17579 2.5 5.32038 2.5 6.51385C2.5 6.79 2.72386 7.01385 3 7.01385H11C11.1326 7.01385 11.2598 6.96118 11.3536 6.86741C11.4473 6.77364 11.5 6.64646 11.5 6.51385C11.5 5.32038 11.0259 4.17579 10.182 3.33187C9.51696 2.66686 8.66523 2.23148 7.75 2.07678V0.762451ZM5 8.48761C5.41421 8.48761 5.75 8.8234 5.75 9.23761V10.2376C5.75 10.6518 5.41421 10.9876 5 10.9876C4.58579 10.9876 4.25 10.6518 4.25 10.2376V9.23761C4.25 8.8234 4.58579 8.48761 5 8.48761ZM4.25 12.2376C4.25 11.8234 3.91421 11.4876 3.5 11.4876C3.08579 11.4876 2.75 11.8234 2.75 12.2376V13.2376C2.75 13.6518 3.08579 13.9876 3.5 13.9876C3.91421 13.9876 4.25 13.6518 4.25 13.2376V12.2376ZM7 11.4876C7.41421 11.4876 7.75 11.8234 7.75 12.2376V13.2376C7.75 13.6518 7.41421 13.9876 7 13.9876C6.58579 13.9876 6.25 13.6518 6.25 13.2376V12.2376C6.25 11.8234 6.58579 11.4876 7 11.4876ZM11.25 12.2376C11.25 11.8234 10.9142 11.4876 10.5 11.4876C10.0858 11.4876 9.75 11.8234 9.75 12.2376V13.2376C9.75 13.6518 10.0858 13.9876 10.5 13.9876C10.9142 13.9876 11.25 13.6518 11.25 13.2376V12.2376ZM9 8.48761C9.41421 8.48761 9.75 8.8234 9.75 9.23761V10.2376C9.75 10.6518 9.41421 10.9876 9 10.9876C8.58579 10.9876 8.25 10.6518 8.25 10.2376V9.23761C8.25 8.8234 8.58579 8.48761 9 8.48761Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"hotel-two-star\", \"keywords\": [ \"two\", \"stars\", \"reviews\", \"review\", \"rating\", \"hotel\", \"star\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186976)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.18961 0.255506C6.43738 0.118344 6.71594 0.0463867 6.99914 0.0463867C7.28234 0.0463867 7.5609 0.118344 7.80867 0.255506C8.05614 0.392501 8.2648 0.590056 8.4151 0.829667L8.41564 0.830536L10.2139 3.67632L12.488 3.95496C12.7857 3.98875 13.0686 4.10288 13.3064 4.28516C13.545 4.46803 13.7292 4.71255 13.839 4.99237C13.9489 5.27218 13.9803 5.57668 13.9299 5.87303C13.8795 6.16939 13.7492 6.44636 13.5529 6.6741L13.551 6.67633L11.6185 8.89274L12.2445 11.9766L12.2452 11.9803C12.3058 12.2907 12.2764 12.612 12.1607 12.9063C12.045 13.2006 11.8477 13.4558 11.592 13.6419L11.5861 13.6462C11.3285 13.828 11.0249 13.9335 10.7101 13.951C10.3955 13.9685 10.0823 13.8972 9.80631 13.7454L6.99914 12.2092L4.19252 13.7451C3.91649 13.8969 3.60275 13.9685 3.28818 13.951C2.97338 13.9335 2.66984 13.8279 2.41221 13.6461L2.40623 13.6419C2.15051 13.4558 1.95322 13.2006 1.83751 12.9063C1.72179 12.612 1.69247 12.2907 1.753 11.9803L1.75374 11.9766L2.37972 8.89274L0.447223 6.67633L0.44529 6.67411C0.249069 6.44637 0.118732 6.16939 0.068316 5.87303C0.0178994 5.57668 0.049322 5.27218 0.159196 4.99237C0.269071 4.71255 0.453231 4.46803 0.691824 4.28516C0.929649 4.10288 1.21257 3.98875 1.51027 3.95496L3.78432 3.67632L5.58264 0.830536L5.58318 0.829667C5.73349 0.590056 5.94214 0.392501 6.18961 0.255506ZM6.64176 4.62512C5.90182 4.62512 5.27399 5.0983 5.0413 5.75664C4.92627 6.08209 5.09685 6.43916 5.4223 6.55419C5.74775 6.66922 6.10482 6.49864 6.21985 6.1732C6.28154 5.99866 6.44803 5.87512 6.64176 5.87512H7.38437C7.61599 5.87512 7.80375 6.06288 7.80375 6.2945C7.80375 6.46112 7.70512 6.61194 7.55247 6.67872L5.96149 7.37477C5.34374 7.64504 4.94457 8.25538 4.94457 8.92967V9.5389C4.94457 9.88408 5.22439 10.1639 5.56957 10.1639H8.42875C8.77392 10.1639 9.05375 9.88408 9.05375 9.5389C9.05375 9.19372 8.77392 8.9139 8.42875 8.9139H6.19484C6.20086 8.74253 6.30457 8.58907 6.46252 8.51997L8.05349 7.82392C8.66112 7.55808 9.05375 6.95774 9.05375 6.2945C9.05375 5.37253 8.30635 4.62512 7.38437 4.62512H6.64176Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186976\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"information-desk-customer\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186991)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.9179 1.62318C12.9179 2.51964 12.1912 3.24637 11.2947 3.24637C10.3983 3.24637 9.67153 2.51964 9.67153 1.62318C9.67153 0.726724 10.3983 0 11.2947 0C12.1912 0 12.9179 0.726724 12.9179 1.62318ZM13.942 6.66015C13.8673 6.99558 13.5679 7.24634 13.2099 7.24634H7.09765C6.68343 7.24634 6.34765 6.91055 6.34765 6.49634C6.34765 6.08212 6.68343 5.74634 7.09765 5.74634H8.69368C9.01765 4.61523 10.0595 3.78741 11.2947 3.78741C12.7888 3.78741 14 4.99862 14 6.49272C14 6.55593 13.9783 6.61409 13.942 6.66015ZM1.35853 3.324C1.35853 4.32435 2.16947 5.1353 3.16983 5.1353C4.17018 5.1353 4.98113 4.32435 4.98113 3.324C4.98113 2.32364 4.17018 1.5127 3.16983 1.5127C2.16947 1.5127 1.35853 2.32364 1.35853 3.324ZM0 9.0189C0 8.17822 0.333958 7.37198 0.928407 6.77753C1.52286 6.18308 2.3291 5.84912 3.16978 5.84912C4.01046 5.84912 4.8167 6.18308 5.41115 6.77753C6.0056 7.37198 6.33956 8.17822 6.33956 9.0189V9.87738C6.33956 10.1535 6.1157 10.3774 5.83956 10.3774H4.98108L4.583 13.562C4.55173 13.8122 4.33903 14 4.08687 14H2.25269C2.00053 14 1.78783 13.8122 1.75656 13.562L1.35848 10.3774H0.5C0.223857 10.3774 0 10.1535 0 9.87738V9.0189Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186991\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"information-desk\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186997)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.09058 2.73516C7.09298 3.39469 7.75468 4.52976 7.75468 5.81938C7.75468 6.38032 7.62949 6.91202 7.40552 7.38806C8.08997 7.91623 8.9487 8.2309 9.88022 8.2309C12.119 8.2309 13.9339 6.416 13.9339 4.17722C13.9339 1.93843 12.119 0.123535 9.88022 0.123535C8.14931 0.123535 6.67193 1.20825 6.09058 2.73516ZM9.8812 3.80091C10.2264 3.80091 10.5062 4.08073 10.5062 4.42591V6.22126C10.5062 6.56644 10.2263 6.84626 9.88117 6.84625C9.53599 6.84625 9.25617 6.56642 9.25618 6.22124L9.2562 4.4259C9.2562 4.08072 9.53603 3.8009 9.8812 3.80091ZM9.14792 2.22064C9.14792 2.69229 9.53825 2.89923 9.81295 2.92923C9.83605 2.93181 9.85952 2.93314 9.8833 2.93314C10.1559 2.93314 10.6168 2.73319 10.6168 2.22068C10.6168 1.74903 10.2265 1.54209 9.95175 1.51209C9.92866 1.50951 9.90519 1.50818 9.8814 1.50818C9.6088 1.50818 9.14792 1.70813 9.14792 2.22064ZM4.06542 8.25864C5.41259 8.25864 6.50468 7.16654 6.50468 5.81938C6.50468 4.47222 5.41259 3.38013 4.06542 3.38013C2.71826 3.38013 1.62617 4.47222 1.62617 5.81938C1.62617 7.16654 2.71826 8.25864 4.06542 8.25864ZM0 13.1371C0 10.8918 1.82015 9.0717 4.06542 9.0717C6.08135 9.0717 7.75457 10.539 8.07536 12.4639H13.2322C13.6464 12.4639 13.9822 12.7997 13.9822 13.2139C13.9822 13.6281 13.6464 13.9639 13.2322 13.9639H0.769165C0.372598 13.9639 0.0479181 13.6561 0.0209735 13.2664C0.00737004 13.2258 0 13.1823 0 13.1371Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186997\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"iron\", \"keywords\": [ \"laundry\", \"iron\", \"heat\", \"hotel\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.034 2.03625C11.5405 1.57031 10.8993 1.30493 10.2234 1.31369H4.53205C4.11784 1.31369 3.78205 1.64947 3.78205 2.06369C3.78205 2.4779 4.11784 2.81369 4.53205 2.81369H10.2287L10.2403 2.8136C10.5041 2.80951 10.7759 2.91135 11.0041 3.12685C11.2345 3.34444 11.4029 3.66341 11.4578 4.03416L11.549 4.65744H8.85025C6.90912 4.65927 5.01695 5.27113 3.44252 6.40654C1.86809 7.54195 0.69032 9.14347 0.0757026 10.9848C-0.0006487 11.2135 0.0376358 11.465 0.178591 11.6606C0.319545 11.8563 0.545976 11.9722 0.787115 11.9722H12.2422C12.4905 11.9722 12.7359 11.9185 12.9614 11.8146C13.1865 11.7109 13.3865 11.5599 13.5477 11.3717C13.7105 11.1825 13.8302 10.9601 13.8983 10.72C13.9666 10.4795 13.9816 10.227 13.9423 9.98009L13.9414 9.97422L13.1575 5.29063L12.9419 3.81648L12.9418 3.81534C12.8412 3.13546 12.5266 2.50144 12.034 2.03625Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"ladder\", \"keywords\": [ \"business\", \"product\", \"metaphor\", \"ladder\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.5 0C4.05228 0 4.5 0.447715 4.5 1V2.21594H9.5V1C9.5 0.447715 9.94772 0 10.5 0C11.0523 0 11.5 0.447715 11.5 1V3.21594V7V10.784V13C11.5 13.5523 11.0523 14 10.5 14C9.94772 14 9.5 13.5523 9.5 13V11.784H4.5V13C4.5 13.5523 4.05228 14 3.5 14C2.94772 14 2.5 13.5523 2.5 13V10.784V7V3.21594V1C2.5 0.447715 2.94772 0 3.5 0ZM4.5 9.784H9.5V8H4.5V9.784ZM9.5 4.21594V6H4.5V4.21594H9.5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"lift\", \"keywords\": [ \"arrow\", \"up\", \"human\", \"down\", \"person\", \"user\", \"lift\", \"elevator\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_186994)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.55373 4.06144C4.67527 4.06144 5.58445 3.15226 5.58445 2.03072C5.58445 0.909185 4.67527 0 3.55373 0C2.4322 0 1.52301 0.909185 1.52301 2.03072C1.52301 3.15226 2.4322 4.06144 3.55373 4.06144ZM11.6014 3.09261C11.4061 2.89735 11.0895 2.89735 10.8943 3.09261L8.64214 5.34476C8.49914 5.48776 8.45637 5.70281 8.53376 5.88965C8.61115 6.07649 8.79347 6.19831 8.9957 6.19831H13.5C13.7022 6.19831 13.8845 6.07649 13.9619 5.88965C14.0393 5.70281 13.9965 5.48776 13.8535 5.34476L11.6014 3.09261ZM8.53376 9.11029C8.61115 8.92346 8.79347 8.80164 8.9957 8.80164H13.5C13.7022 8.80164 13.8845 8.92346 13.9619 9.11029C14.0393 9.29713 13.9965 9.51219 13.8535 9.65519L11.6014 11.9073C11.4061 12.1026 11.0895 12.1026 10.8943 11.9073L8.64214 9.65519C8.49914 9.51219 8.45637 9.29713 8.53376 9.11029ZM6.06665 5.90257C6.73311 6.56903 7.10752 7.47294 7.10752 8.41546V9.4385C7.10752 9.71464 6.88367 9.9385 6.60752 9.9385H5.58448L5.13155 13.562C5.10027 13.8122 4.88757 13.9999 4.63541 13.9999H2.47211C2.21995 13.9999 2.00725 13.8122 1.97597 13.562L1.52304 9.9385H0.5C0.223858 9.9385 0 9.71464 0 9.4385V8.41546C0 7.47294 0.374413 6.56903 1.04087 5.90257C1.70733 5.23611 2.61125 4.86169 3.55376 4.86169C4.49628 4.86169 5.40019 5.23611 6.06665 5.90257Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_186994\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"lift-disability\", \"keywords\": [ \"arrow\", \"up\", \"human\", \"down\", \"person\", \"user\", \"lift\", \"elevator\", \"disability\", \"wheelchair\", \"accessible\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187006)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.8539 0.146447C11.6586 -0.0488156 11.342 -0.0488156 11.1468 0.146447L9.14676 2.14645C9.00377 2.28945 8.96099 2.5045 9.03837 2.69134C9.11578 2.87818 9.29808 3 9.50031 3H13.5003C13.7025 3 13.8849 2.87818 13.9623 2.69134C14.0396 2.5045 13.9969 2.28945 13.8539 2.14645L11.8539 0.146447ZM9.50031 5C9.29808 5 9.11578 5.12182 9.03837 5.30866C8.96099 5.4955 9.00377 5.71055 9.14676 5.85355L11.1468 7.85355C11.342 8.04882 11.6586 8.04882 11.8539 7.85355L13.8539 5.85355C13.9969 5.71055 14.0396 5.4955 13.9623 5.30866C13.8849 5.12182 13.7025 5 13.5003 5H9.50031ZM4.35749 4.25916C4.35749 3.29745 5.1371 2.51782 6.09882 2.51782C7.06053 2.51782 7.84017 3.29745 7.84017 4.25916C7.84017 5.22088 7.06053 6.0005 6.09882 6.0005C5.1371 6.0005 4.35749 5.22088 4.35749 4.25916ZM7.10053 7.4856C7.10053 6.93331 6.65281 6.4856 6.10053 6.4856C5.54825 6.4856 5.10053 6.93331 5.10053 7.4856V9.96773C5.10053 10.52 5.54825 10.9677 6.10053 10.9677H7.99576V12.9463C7.99576 13.4986 8.44347 13.9463 8.99576 13.9463C9.54805 13.9463 9.99576 13.4986 9.99576 12.9463V10.9328C9.99576 10.4116 9.78872 9.91181 9.42021 9.54329C9.05167 9.17476 8.55186 8.96773 8.03069 8.96773H7.10053V7.4856ZM4.43864 7.3602C4.43886 7.77442 4.10325 8.11038 3.68904 8.1106C3.2958 8.1108 2.91017 8.2191 2.57432 8.42364C2.23846 8.62819 1.96528 8.92111 1.78465 9.27041C1.60401 9.61971 1.52286 10.0119 1.55007 10.4042C1.57727 10.7965 1.71179 11.1738 1.93891 11.4948C2.16603 11.8159 2.47703 12.0683 2.8379 12.2245C3.19878 12.3807 3.59566 12.4348 3.98517 12.3807C4.37467 12.3266 4.74182 12.1665 5.04648 11.9179C5.3674 11.656 5.83986 11.7038 6.10175 12.0248C6.36364 12.3457 6.31577 12.8181 5.99486 13.08C5.47716 13.5025 4.8533 13.7745 4.19144 13.8664C3.52958 13.9583 2.8552 13.8665 2.24198 13.6011C1.62877 13.3356 1.10033 12.9067 0.714388 12.3612C0.328455 11.8157 0.0998859 11.1746 0.0536572 10.508C0.00742864 9.84141 0.145319 9.17492 0.452262 8.58138C0.759204 7.98785 1.22339 7.4901 1.79409 7.14253C2.36479 6.79496 3.02004 6.61095 3.68825 6.6106C4.10246 6.61038 4.43843 6.94599 4.43864 7.3602Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187006\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"location-compass-1\", \"keywords\": [ \"arrow\", \"compass\", \"location\", \"gps\", \"map\", \"maps\", \"point\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187003)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0ZM9.61951 4.38052C9.75342 4.51443 9.80018 4.71252 9.7403 4.89218L7.89633 10.4241C7.83459 10.6093 7.67065 10.7417 7.47655 10.763C7.28245 10.7843 7.09371 10.6906 6.99325 10.5232L5.67459 8.32544L3.47682 7.00678C3.30938 6.90631 3.21575 6.71757 3.23706 6.52347C3.25836 6.32937 3.39071 6.16544 3.57596 6.10369L9.10784 4.25973C9.28751 4.19984 9.48559 4.2466 9.61951 4.38052Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187003\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"location-pin-3\", \"keywords\": [ \"navigation\", \"map\", \"maps\", \"pin\", \"gps\", \"location\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187027)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.00013 -0.000732422C9.62167 -0.000732422 11.8062 2.18383 11.8062 4.80537C11.8062 5.66096 11.5121 6.50475 11.1011 7.26925C10.6885 8.03673 10.1423 8.75332 9.59967 9.36339C9.056 9.97466 8.50667 10.4893 8.07866 10.8529C7.86488 11.0345 7.67776 11.1815 7.53349 11.2856C7.46226 11.337 7.3955 11.3822 7.33788 11.4168C7.30984 11.4336 7.27567 11.453 7.23934 11.4698C7.22125 11.4781 7.19523 11.4893 7.16418 11.4992C7.13976 11.507 7.07871 11.5253 7.00013 11.5253C6.92086 11.5253 6.85933 11.5067 6.83484 11.4988C6.80363 11.4887 6.7775 11.4774 6.75939 11.469C6.72298 11.452 6.68878 11.4325 6.66075 11.4155C6.60315 11.3806 6.53642 11.335 6.46522 11.2832C6.32105 11.1784 6.13401 11.0304 5.92034 10.8476C5.49255 10.4818 4.94347 9.9645 4.40006 9.35159C3.85773 8.73991 3.31181 8.02267 2.89933 7.25699C2.48862 6.49459 2.19403 5.65425 2.19403 4.80537C2.19403 2.18383 4.37859 -0.000732422 7.00013 -0.000732422ZM7.00013 6.24073C6.2074 6.24073 5.56476 5.5981 5.56476 4.80537C5.56476 4.01264 6.2074 3.37 7.00013 3.37C7.79286 3.37 8.43549 4.01264 8.43549 4.80537C8.43549 5.5981 7.79286 6.24073 7.00013 6.24073ZM10.0993 10.1564C10.2838 9.94899 10.4684 9.73169 10.6496 9.50594H11.0713H11.9927C12.1927 9.50594 12.3735 9.62514 12.4523 9.80898L13.95 13.3038C14.0162 13.4583 14.0004 13.6357 13.9079 13.776C13.8154 13.9163 13.6585 14.0007 13.4905 14.0007H0.509767C0.341696 14.0007 0.184868 13.9163 0.0923466 13.776C-0.000175104 13.6357 -0.0160118 13.4583 0.0501951 13.3038L1.54796 9.80898C1.62675 9.62514 1.80752 9.50594 2.00754 9.50594H2.92887H3.36287C3.53994 9.72642 3.7202 9.93892 3.90029 10.142C4.56182 10.8882 5.23036 11.5181 5.75083 11.9631C6.01085 12.1855 6.23751 12.3648 6.41111 12.491C6.497 12.5535 6.57588 12.6073 6.64264 12.6477C6.67525 12.6675 6.71328 12.6891 6.75272 12.7075C6.77236 12.7167 6.79979 12.7285 6.8321 12.7389C6.85771 12.7472 6.92013 12.766 7.00007 12.766C7.07934 12.766 7.14128 12.7475 7.1668 12.7394C7.19896 12.7291 7.22629 12.7174 7.24589 12.7083C7.28526 12.6901 7.32326 12.6687 7.35588 12.6491C7.42266 12.609 7.50155 12.5556 7.5875 12.4936C7.76119 12.3683 7.98795 12.1903 8.24807 11.9693C8.76874 11.527 9.43756 10.9004 10.0993 10.1564Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187027\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"location-pin-disabled\", \"keywords\": [ \"navigation\", \"map\", \"maps\", \"pin\", \"gps\", \"location\", \"disabled\", \"off\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187021)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.28033 0.21967C0.987437 -0.0732233 0.512563 -0.0732233 0.21967 0.21967C-0.0732233 0.512563 -0.0732233 0.987437 0.21967 1.28033L12.7197 13.7803C13.0126 14.0732 13.4874 14.0732 13.7803 13.7803C14.0732 13.4874 14.0732 13.0126 13.7803 12.7197L11.4017 10.3411C12.2626 9.14903 12.9374 7.692 12.9374 6.04722C12.9374 2.71895 10.2881 0.00439449 6.99994 0.00439449C5.36602 0.00439449 3.88985 0.67466 2.81809 1.75743L1.28033 0.21967ZM5.38407 4.32341L8.17872 7.11806C8.67655 6.75428 8.99994 6.166 8.99994 5.5022C8.99994 4.39763 8.10451 3.5022 6.99994 3.5022C6.33613 3.5022 5.74786 3.82559 5.38407 4.32341ZM1.06244 6.04722C1.06244 5.39431 1.16439 4.76502 1.35312 4.17521L9.55086 12.3729C8.84883 13.0043 8.19333 13.4781 7.76654 13.765C7.30048 14.0783 6.6994 14.0783 6.23334 13.765C5.54665 13.3034 4.26791 12.3581 3.15593 11.0508C2.04766 9.74788 1.06244 8.03461 1.06244 6.04722Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187021\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"location-target-1\", \"keywords\": [ \"navigation\", \"location\", \"map\", \"services\", \"maps\", \"gps\", \"target\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187033)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.74994 0.765625C7.74994 0.351411 7.41415 0.015625 6.99994 0.015625C6.58573 0.015625 6.24994 0.351411 6.24994 0.765625V1.98904C4.05243 2.31515 2.31515 4.05243 1.98904 6.24994H0.765625C0.351411 6.24994 0.015625 6.58573 0.015625 6.99994C0.015625 7.41415 0.351411 7.74994 0.765625 7.74994H1.98903C2.31513 9.94747 4.05242 11.6847 6.24994 12.0108V13.2342C6.24994 13.6484 6.58573 13.9842 6.99994 13.9842C7.41415 13.9842 7.74994 13.6484 7.74994 13.2342V12.0108C9.94748 11.6847 11.6847 9.94748 12.0108 7.74994H13.2342C13.6484 7.74994 13.9842 7.41415 13.9842 6.99994C13.9842 6.58573 13.6484 6.24994 13.2342 6.24994H12.0108C11.6847 4.05242 9.94747 2.31513 7.74994 1.98903V0.765625ZM10.566 6.99392C10.5627 5.02722 8.96742 3.4339 6.99995 3.4339C5.03048 3.4339 3.4339 5.03048 3.4339 6.99995C3.4339 8.96742 5.02722 10.5627 6.99392 10.566L6.99994 10.5659L7.00596 10.566C8.97068 10.5627 10.5627 8.97067 10.566 7.00596L10.5659 6.99994L10.566 6.99392ZM5.77008 6.99996C5.77008 6.32072 6.32072 5.77008 6.99996 5.77008C7.6792 5.77008 8.22984 6.32072 8.22984 6.99996C8.22984 7.6792 7.6792 8.22984 6.99996 8.22984C6.32072 8.22984 5.77008 7.6792 5.77008 6.99996Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187033\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"lost-and-found\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187039)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.76256 2.26256C6.09075 1.93437 6.53587 1.75 7 1.75C7.46413 1.75 7.90925 1.93437 8.23744 2.26256C8.56563 2.59075 8.75 3.03587 8.75 3.5H5.25C5.25 3.03587 5.43437 2.59075 5.76256 2.26256ZM3.75 3.5C3.75 2.63805 4.09241 1.8114 4.7019 1.2019C5.3114 0.59241 6.13805 0.25 7 0.25C7.86195 0.25 8.6886 0.59241 9.2981 1.2019C9.90759 1.8114 10.25 2.63805 10.25 3.5H11.5C12.8807 3.5 14 4.61929 14 6V11.5C14 12.8807 12.8807 14 11.5 14H2.5C1.11929 14 0 12.8807 0 11.5V6C0 4.61929 1.11929 3.5 2.5 3.5H3.75ZM6.66515 6.56661C6.82504 6.50038 7.00097 6.48305 7.1707 6.51681C7.34044 6.55058 7.49635 6.63391 7.61872 6.75628C7.74109 6.87865 7.82443 7.03456 7.85819 7.2043C7.89195 7.37403 7.87462 7.54996 7.80839 7.70985C7.74217 7.86973 7.63002 8.00639 7.48612 8.10254C7.34223 8.19868 7.17306 8.25 7 8.25C6.65482 8.25 6.375 8.52982 6.375 8.875V9.875C6.375 10.2202 6.65482 10.5 7 10.5C7.34518 10.5 7.625 10.2202 7.625 9.875V9.40601C7.82097 9.3457 8.00825 9.25703 8.18059 9.14187C8.53004 8.90838 8.80241 8.5765 8.96324 8.1882C9.12408 7.79991 9.16616 7.37264 9.08417 6.96043C9.00218 6.54822 8.79979 6.16959 8.5026 5.8724C8.20542 5.57521 7.82678 5.37283 7.41457 5.29083C7.00236 5.20884 6.57509 5.25092 6.1868 5.41176C5.7985 5.57259 5.46663 5.84496 5.23313 6.19441C4.99963 6.54387 4.875 6.95472 4.875 7.375C4.875 7.72018 5.15482 8 5.5 8C5.84518 8 6.125 7.72018 6.125 7.375C6.125 7.20194 6.17632 7.03277 6.27246 6.88888C6.36861 6.74498 6.50527 6.63283 6.66515 6.56661ZM7.00195 12.875C6.58774 12.875 6.25195 12.5392 6.25195 12.125C6.25195 11.7108 6.58774 11.375 7.00195 11.375C7.41617 11.375 7.75195 11.7108 7.75195 12.125C7.75195 12.5392 7.41617 12.875 7.00195 12.875Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187039\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"man-symbol\", \"keywords\": [ \"geometric\", \"gender\", \"boy\", \"person\", \"male\", \"human\", \"user\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187000)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8 0.75C8 0.335786 8.33579 0 8.75 0H13.25C13.3517 0 13.4487 0.0202391 13.5371 0.0569091C13.6255 0.093509 13.7084 0.147763 13.7803 0.21967C13.8522 0.291577 13.9065 0.374453 13.9431 0.462912C13.9798 0.551342 14 0.648308 14 0.75V5.25C14 5.66421 13.6642 6 13.25 6C12.8358 6 12.5 5.66421 12.5 5.25V2.56066L9.0261 6.03456C9.63821 6.86422 10 7.88986 10 9C10 11.7614 7.76142 14 5 14C2.23858 14 0 11.7614 0 9C0 6.23858 2.23858 4 5 4C6.11014 4 7.13578 4.36179 7.96544 4.9739L11.4393 1.5H8.75C8.33579 1.5 8 1.16421 8 0.75ZM7.40624 6.45833C6.77901 5.86432 5.93205 5.5 5 5.5C3.067 5.5 1.5 7.067 1.5 9C1.5 10.933 3.067 12.5 5 12.5C6.933 12.5 8.5 10.933 8.5 9C8.5 8.06795 8.13568 7.22099 7.54167 6.59376C7.51667 6.57442 7.49261 6.55327 7.46967 6.53033C7.44673 6.50739 7.42558 6.48333 7.40624 6.45833Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187000\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"map-fold\", \"keywords\": [ \"navigation\", \"map\", \"maps\", \"gps\", \"travel\", \"fold\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187036)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.87469 0.865086L5.12531 0.115116V13.1349L8.8747 13.8849L8.87469 0.865086ZM10.1247 13.8592L13.6213 12.9851C13.8438 12.9294 14 12.7294 14 12.5V0.500005C14 0.346038 13.9291 0.200658 13.8077 0.105905C13.6864 0.0111509 13.5281 -0.0224085 13.3787 0.0149311L10.1247 0.828381V13.8592ZM0.378741 1.01493L3.87531 0.140857V13.1716L0.621259 13.9851C0.471889 14.0224 0.313646 13.9889 0.19229 13.8941C0.0709343 13.7994 0 13.654 0 13.5V1.5C0 1.27057 0.156153 1.07057 0.378741 1.01493Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187036\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"navigation-arrow-off\", \"keywords\": [ \"compass\", \"arrow\", \"map\", \"bearing\", \"navigation\", \"maps\", \"heading\", \"gps\", \"off\", \"disable\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187009)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.21967 1.28033C-0.0732233 0.987437 -0.0732233 0.512563 0.21967 0.21967C0.512563 -0.0732233 0.987437 -0.0732233 1.28033 0.21967L5.18871 4.12805L12.1704 1.17426C12.584 0.999271 13.0007 1.41596 12.8257 1.82956L9.87191 8.81125L13.7803 12.7197C14.0732 13.0126 14.0732 13.4874 13.7803 13.7803C13.4874 14.0732 13.0126 14.0732 12.7197 13.7803L0.21967 1.28033ZM0.305194 6.19415L2.66008 5.19786L8.80211 11.3399L7.80581 13.6948C7.63952 14.0878 7.08897 14.1053 6.89811 13.7236L4.76541 9.45816C4.71703 9.36139 4.63857 9.28293 4.54181 9.23455L0.276408 7.10185C-0.105309 6.91099 -0.0878496 6.36044 0.305194 6.19415Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187009\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"navigation-arrow-on\", \"keywords\": [ \"compass\", \"arrow\", \"map\", \"bearing\", \"navigation\", \"maps\", \"heading\", \"gps\", \"off\", \"disable\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187030)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.9605 0.694835C14.0398 0.507322 13.9975 0.290432 13.8536 0.146462C13.7096 0.00249109 13.4927 -0.0398015 13.3052 0.039531L0.305181 5.53953C0.125492 5.61555 0.0064372 5.78915 0.000252593 5.98416C-0.00593201 6.17917 0.101883 6.35997 0.276394 6.44722L5.12732 8.87269L7.55279 13.7236C7.64004 13.8981 7.82084 14.0059 8.01585 13.9997C8.21086 13.9936 8.38446 13.8745 8.46048 13.6948L13.9605 0.694835Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187030\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"parking-sign\", \"keywords\": [ \"discount\", \"coupon\", \"parking\", \"price\", \"prices\", \"hotel\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187018)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 3.5C0 1.567 1.567 0 3.5 0H10.5C12.433 0 14 1.567 14 3.5V10.5C14 12.433 12.433 14 10.5 14H3.5C1.567 14 0 12.433 0 10.5V3.5ZM5.20905 3.11725C5.04328 3.11725 4.88431 3.1831 4.7671 3.30031C4.64989 3.41752 4.58405 3.57649 4.58405 3.74225V7.84145V10.7695C4.58405 11.1146 4.86387 11.3945 5.20905 11.3945C5.55422 11.3945 5.83405 11.1146 5.83405 10.7695V8.46645H8.39385C9.56745 8.46645 10.5188 7.51506 10.5188 6.34145V5.24225C10.5188 4.06864 9.56745 3.11725 8.39385 3.11725H5.20905ZM8.39385 7.21645H5.83405V4.36725H8.39385C8.8771 4.36725 9.26885 4.759 9.26885 5.24225V6.34145C9.26885 6.8247 8.8771 7.21645 8.39385 7.21645Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187018\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"parliament\", \"keywords\": [ \"travel\", \"places\", \"parliament\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187024)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.00073 0.0505371C6.80181 0.0505371 6.61104 0.12956 6.47039 0.27022C6.32973 0.41088 6.25072 0.601655 6.25073 0.800574L6.25084 2.97346L5.1403 5.08478C5.10255 5.15656 5.08282 5.23644 5.08282 5.31755L5.08283 8.0542H8.87195L8.87194 5.31754C8.87194 5.23644 8.85221 5.15656 8.81446 5.08478L7.75084 3.06265L7.75077 1.55054H8.37191C8.78612 1.55054 9.12191 1.21475 9.12191 0.800537C9.12191 0.386324 8.78612 0.0505371 8.37191 0.0505371H7.00073ZM9.66723 9.3042H4.28668V13.863C4.28668 13.8717 4.2865 13.8805 4.28614 13.8891H6.09888V12.498C6.09888 11.9999 6.50262 11.5962 7.00067 11.5962C7.49872 11.5962 7.90247 11.9999 7.90247 12.498V13.8891H9.66777C9.66751 13.8827 9.66734 13.8762 9.66727 13.8696C9.66725 13.8674 9.66723 13.8652 9.66723 13.863V9.3042ZM11.4017 13.8891H10.9167C10.9171 13.8805 10.9173 13.8717 10.9173 13.863V6.64539C10.9237 6.59657 10.9373 6.54885 10.9578 6.5038L11.9565 4.3054C12.0376 4.12685 12.2156 4.01221 12.4117 4.01221C12.6078 4.01221 12.7858 4.12685 12.867 4.3054L13.8657 6.5038C13.8952 6.56876 13.9105 6.63927 13.9105 6.71061V13.3892C13.9105 13.6654 13.6866 13.8892 13.4105 13.8892H11.413L11.4017 13.8891ZM3.03722 13.8891C3.03686 13.8805 3.03668 13.8717 3.03668 13.863V6.64941C3.03049 6.59919 3.01669 6.55009 2.99567 6.5038L1.99693 4.3054C1.91582 4.12685 1.73781 4.01221 1.5417 4.01221C1.3456 4.01221 1.16759 4.12685 1.08648 4.3054L0.0877434 6.5038C0.0582354 6.56876 0.0429688 6.63927 0.0429688 6.71061V13.3892C0.0429688 13.6654 0.266826 13.8892 0.542968 13.8892H2.54044L2.55173 13.8891H3.03722Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187024\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"passport\", \"keywords\": [ \"travel\", \"book\", \"id\", \"adventure\", \"visa\", \"airport\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.5 0C1.67157 0 1 0.671573 1 1.5V12.5C1 13.3284 1.67157 14 2.5 14H11.5C12.3284 14 13 13.3284 13 12.5V1.5C13 0.671573 12.3284 0 11.5 0H2.5ZM7.5 9.36361C8.97758 9.14584 10.1458 7.97759 10.3636 6.5H7.5V9.36361ZM6.5 9.36362C5.02239 9.14587 3.85412 7.97761 3.63636 6.5H6.5V9.36362ZM7.5 5.5H10.3636C10.1458 4.02241 8.97758 2.85416 7.5 2.63639V5.5ZM6.5 2.63638V5.5H3.63636C3.85412 4.02239 5.02239 2.85413 6.5 2.63638Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"pet-paw\", \"keywords\": [ \"paw\", \"foot\", \"animals\", \"pets\", \"footprint\", \"track\", \"hotel\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.5 1.5C4.00467 1.5 3.61743 1.80115 3.37687 2.16199C3.13398 2.52632 3 3.00084 3 3.5C3 3.99916 3.13398 4.47368 3.37687 4.83801C3.61743 5.19885 4.00467 5.5 4.5 5.5C4.99533 5.5 5.38257 5.19885 5.62313 4.83801C5.86602 4.47368 6 3.99916 6 3.5C6 3.00084 5.86602 2.52632 5.62313 2.16199C5.38257 1.80115 4.99533 1.5 4.5 1.5ZM9.5 1.5C9.00467 1.5 8.61743 1.80115 8.37687 2.16199C8.13398 2.52632 8 3.00084 8 3.5C8 3.99916 8.13398 4.47368 8.37687 4.83801C8.61743 5.19885 9.00467 5.5 9.5 5.5C9.99533 5.5 10.3826 5.19885 10.6231 4.83801C10.866 4.47368 11 3.99916 11 3.5C11 3.00084 10.866 2.52632 10.6231 2.16199C10.3826 1.80115 9.99533 1.5 9.5 1.5ZM1.5 6C1.00467 6 0.617427 6.30115 0.376868 6.66199C0.133979 7.02632 0 7.50084 0 8C0 8.49916 0.133979 8.97368 0.376868 9.33801C0.617427 9.69885 1.00467 10 1.5 10C1.99533 10 2.38257 9.69885 2.62313 9.33801C2.86602 8.97368 3 8.49916 3 8C3 7.50084 2.86602 7.02632 2.62313 6.66199C2.38257 6.30115 1.99533 6 1.5 6ZM7 6C5.79915 6 4.91124 6.6437 4.34264 7.44257C3.7819 8.23042 3.5 9.20692 3.5 10C3.5 10.9241 4.05528 11.5665 4.73585 11.9524C5.40634 12.3327 6.24658 12.5 7 12.5C7.75342 12.5 8.59366 12.3327 9.26415 11.9524C9.94472 11.5665 10.5 10.9241 10.5 10C10.5 9.20692 10.2181 8.23042 9.65736 7.44257C9.08876 6.6437 8.20085 6 7 6ZM12.5 6C12.0047 6 11.6174 6.30115 11.3769 6.66199C11.134 7.02632 11 7.50084 11 8C11 8.49916 11.134 8.97368 11.3769 9.33801C11.6174 9.69885 12.0047 10 12.5 10C12.9953 10 13.3826 9.69885 13.6231 9.33801C13.866 8.97368 14 8.49916 14 8C14 7.50084 13.866 7.02632 13.6231 6.66199C13.3826 6.30115 12.9953 6 12.5 6Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"pets-allowed\", \"keywords\": [ \"travel\", \"wayfinder\", \"pets\", \"allowed\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.17039 3.10766C8.53779 2.37285 9.28883 1.90869 10.1104 1.90869H10.8433C10.9814 1.90869 11.0933 2.02062 11.0933 2.15869V4.3772L13.8008 5.2797C13.9029 5.31373 13.9718 5.40926 13.9718 5.51687V6.47635C13.9718 7.67423 13.0007 8.64531 11.8028 8.64531H10.1339V8.64729C9.10921 8.43041 8.34939 7.85304 7.79796 7.17262C7.38336 6.66105 7.09548 6.10068 6.91474 5.61896L8.17039 3.10766ZM5.63646 5.74661H4.03403C3.21932 5.74661 2.42886 5.46933 1.79268 4.96039L0.445236 3.88243C0.370194 3.8224 0.267382 3.8107 0.180765 3.85233C0.0941489 3.89396 0.0390625 3.98155 0.0390625 4.07765V12.2332C0.0390625 13.1662 0.795352 13.9225 1.72828 13.9225C2.66121 13.9225 3.4175 13.1662 3.4175 12.2332V11.7535C3.4175 11.3617 3.73515 11.044 4.12698 11.044H6.04594C6.43778 11.044 6.75542 11.3617 6.75542 11.7535V12.2332C6.75542 13.1662 7.51171 13.9225 8.44464 13.9225C9.37757 13.9225 10.1339 13.1662 10.1339 12.2332V9.91837C8.64396 9.67937 7.56371 8.86891 6.82683 7.95965C6.23138 7.22491 5.84849 6.41338 5.63646 5.74661Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"pool-ladder\", \"keywords\": [ \"pool\", \"stairs\", \"swim\", \"swimming\", \"water\", \"ladder\", \"hotel\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187102)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.60535 1.52393C4.29618 1.52393 3.99968 1.64674 3.78106 1.86535C3.56245 2.08397 3.43964 2.38047 3.43964 2.68964C3.43964 3.10385 3.10385 3.43964 2.68964 3.43964C2.27542 3.43964 1.93964 3.10385 1.93964 2.68964C1.93964 1.98265 2.22049 1.30461 2.7204 0.804695C3.22032 0.304777 3.89836 0.0239258 4.60535 0.0239258C5.31234 0.0239258 5.99037 0.304777 6.49029 0.804695C6.99021 1.30461 7.27106 1.98265 7.27106 2.68964V3.85535H10.5604V2.68964C10.5604 2.38047 10.4375 2.08397 10.2189 1.86535C10.0003 1.64674 9.70382 1.52393 9.39465 1.52393C8.98044 1.52393 8.64465 1.18814 8.64465 0.773926C8.64465 0.359712 8.98044 0.0239258 9.39465 0.0239258C10.1016 0.0239258 10.7797 0.304777 11.2796 0.804695C11.7795 1.30461 12.0604 1.98265 12.0604 2.68964V8.69229C12.0604 9.10651 11.7246 9.44229 11.3104 9.44229C10.8962 9.44229 10.5604 9.10651 10.5604 8.69229V8.229H7.27106V8.82001C7.27106 9.23422 6.93527 9.57001 6.52106 9.57001C6.10685 9.57001 5.77106 9.23422 5.77106 8.82001V7.48071V7.479V7.47729V4.60706V4.60535V4.60364V2.68964C5.77106 2.38047 5.64824 2.08397 5.42963 1.86535C5.21102 1.64674 4.91451 1.52393 4.60535 1.52393ZM7.27106 6.729H10.5604V5.35535H7.27106V6.729ZM1.99731 10.8308C2.19828 10.8297 2.38038 10.9491 2.45956 11.1338C2.62435 11.5183 2.90609 11.841 3.26476 12.0562C3.95514 12.4704 5.04486 12.4704 5.73524 12.0562C6.09391 11.841 6.37565 11.5183 6.54044 11.1338C6.61924 10.95 6.8 10.8308 7 10.8308C7.2 10.8308 7.38076 10.95 7.45956 11.1338C7.62435 11.5183 7.90609 11.841 8.26476 12.0562C8.95514 12.4704 10.0449 12.4704 10.7352 12.0562C11.0939 11.841 11.3756 11.5183 11.5404 11.1338C11.6196 10.9491 11.8017 10.8297 12.0027 10.8308C12.2037 10.8319 12.3845 10.9532 12.4617 11.1388C12.5672 11.3925 12.7279 11.6195 12.9322 11.8034C13.1365 11.9872 13.3791 12.1232 13.6425 12.2015C13.8546 12.2646 14 12.4596 14 12.6808V13.5C14 13.7761 13.7761 14 13.5 14H0.5C0.223858 14 0 13.7761 0 13.5V12.6808C0 12.4596 0.145416 12.2646 0.357494 12.2015C0.620899 12.1232 0.863547 11.9872 1.06781 11.8034C1.27207 11.6195 1.43281 11.3925 1.53835 11.1388C1.61553 10.9532 1.79633 10.8319 1.99731 10.8308Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187102\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"rock-slide\", \"keywords\": [ \"hill\", \"cliff\", \"sign\", \"danger\", \"stone\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187045)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.853553 0.146462C0.710554 0.0034622 0.495495 -0.0393156 0.308658 0.0380748C0.121821 0.115466 0 0.297784 0 0.500015V13.5C0 13.7761 0.223858 14 0.5 14H9C9.1498 14 9.2917 13.9328 9.38667 13.817C9.48164 13.7012 9.51967 13.5488 9.49029 13.4019L8.99029 10.9019C8.96934 10.7972 8.91539 10.7019 8.83634 10.63L3.46952 5.75112L2.99497 2.4293C2.97968 2.3222 2.93005 2.22295 2.85355 2.14646L0.853553 0.146462ZM8 5.00001C8 3.89544 8.89543 3.00001 10 3.00001C11.1046 3.00001 12 3.89544 12 5.00001C12 6.10458 11.1046 7.00001 10 7.00001C8.89543 7.00001 8 6.10458 8 5.00001ZM12 11C12 10.4477 12.4477 10 13 10C13.5523 10 14 10.4477 14 11C14 11.5523 13.5523 12 13 12C12.4477 12 12 11.5523 12 11ZM5 1.00001C5 0.44773 5.44772 1.45805e-05 6 1.45805e-05C6.55228 1.45805e-05 7 0.44773 7 1.00001C7 1.55229 6.55228 2.00001 6 2.00001C5.44772 2.00001 5 1.55229 5 1.00001Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187045\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"sail-ship\", \"keywords\": [ \"travel\", \"boat\", \"transportation\", \"transport\", \"ocean\", \"ship\", \"sea\", \"water\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187084)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.75 7.00001H1.327C1.13299 7.00001 0.956494 6.88778 0.874204 6.71209C0.791914 6.5364 0.818687 6.32896 0.942888 6.17992L5.94289 0.179921C6.06104 0.0381391 6.24105 -0.0216865 6.4108 0.00700183C6.44005 0.00353519 6.46982 0.00175205 6.5 0.00175205C6.91421 0.00175203 7.25 0.337539 7.25 0.751752V9.00001H13.5C13.664 9.00001 13.8176 9.08043 13.911 9.21521C14.0044 9.35 14.0257 9.52202 13.9682 9.67557L12.9548 12.3778C12.5889 13.3536 11.6561 14 10.614 14H3.386C2.34389 14 1.41109 13.3536 1.04518 12.3778L0.0318383 9.67557C-0.0257423 9.52202 -0.00436765 9.35 0.0890401 9.21521C0.182448 9.08043 0.336013 9.00001 0.500003 9.00001H5.75V7.00001ZM9.38138 2.19558C9.25096 2.02566 9.02681 1.95769 8.82399 2.02656C8.62117 2.09542 8.48474 2.28582 8.48474 2.50001V6.50001C8.48474 6.77615 8.7086 7.00001 8.98474 7.00001H12.0549C12.2453 7.00001 12.4192 6.89188 12.5033 6.72112C12.5875 6.55036 12.5675 6.34661 12.4515 6.19558L9.38138 2.19558Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187084\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"school-bus-side\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.144531 3.52562C0.144531 2.55913 0.928033 1.77563 1.89453 1.77563H10.7151C11.6816 1.77563 12.4651 2.55913 12.4651 3.52563V6.20613H12.7159C13.4062 6.20613 13.9659 6.76577 13.9659 7.45613V10.8411C13.9659 11.4805 13.4859 12.0078 12.8665 12.0822C12.8724 12.0121 12.8754 11.9413 12.8754 11.8697C12.8754 10.4829 11.7512 9.3587 10.3644 9.3587C8.97761 9.3587 7.85339 10.4829 7.85339 11.8697C7.85339 11.9443 7.85665 12.0182 7.86302 12.0911H5.75626C5.76264 12.0182 5.76589 11.9443 5.76589 11.8697C5.76589 10.4829 4.64168 9.3587 3.25489 9.3587C1.91215 9.3587 0.815566 10.4127 0.74727 11.7384C0.37981 11.4438 0.144531 10.9911 0.144531 10.4835V3.52562ZM1.89527 4.24996C1.7572 4.24996 1.64527 4.36189 1.64527 4.49996V6.24996H3.65741V4.24996H1.89527ZM5.15741 4.24996V6.24996H7.39001V4.24996H5.15741ZM10.9651 6.24996H8.89001V4.24996H10.7151C10.8532 4.24996 10.9651 4.36189 10.9651 4.49996V6.24996ZM4.51589 11.8697C4.51589 12.5662 3.95132 13.1307 3.25489 13.1307C2.55846 13.1307 1.9939 12.5662 1.9939 11.8697C1.9939 11.1733 2.55846 10.6087 3.25489 10.6087C3.95132 10.6087 4.51589 11.1733 4.51589 11.8697ZM11.6254 11.8697C11.6254 12.5662 11.0608 13.1307 10.3644 13.1307C9.66796 13.1307 9.10339 12.5662 9.10339 11.8697C9.10339 11.1733 9.66796 10.6087 10.3644 10.6087C11.0608 10.6087 11.6254 11.1733 11.6254 11.8697Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"smoke-detector\", \"keywords\": [ \"smoke\", \"alert\", \"fire\", \"signal\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.5 0.00537109C2.08579 0.00537109 1.75 0.341157 1.75 0.755371C1.75 1.16958 2.08579 1.50537 2.5 1.50537H3.00781C3.0685 2.475 3.48057 3.39279 4.17157 4.0838C4.92172 4.83394 5.93913 5.25537 7 5.25537C8.06087 5.25537 9.07828 4.83394 9.82843 4.0838C10.5194 3.39279 10.9315 2.475 10.9922 1.50537H11.5C11.9142 1.50537 12.25 1.16958 12.25 0.755371C12.25 0.341157 11.9142 0.00537109 11.5 0.00537109H2.5ZM4.16563 7.70491C4.42438 7.38146 4.37194 6.90949 4.0485 6.65074C3.72505 6.39198 3.25308 6.44442 2.99432 6.76787C2.39072 7.52237 2.18064 8.25226 2.27364 8.97299C2.35542 9.6068 2.6703 10.1569 2.90309 10.5636L2.92879 10.6085C3.19412 11.0729 3.35847 11.3805 3.39864 11.6918C3.43064 11.9398 3.39072 12.2724 2.99432 12.7679C2.73557 13.0914 2.78801 13.5633 3.11145 13.8221C3.4349 14.0808 3.90687 14.0284 4.16563 13.7049C4.76923 12.9504 4.97931 12.2206 4.88631 11.4998C4.80453 10.866 4.48965 10.3159 4.25686 9.9092L4.23116 9.86428C3.96583 9.39996 3.80147 9.09233 3.76131 8.78103C3.72931 8.53302 3.76923 8.2004 4.16563 7.70491ZM7.58567 7.70491C7.84443 7.38146 7.79199 6.90949 7.46854 6.65074C7.14509 6.39198 6.67312 6.44442 6.41437 6.76787C5.81076 7.52237 5.60069 8.25226 5.69369 8.97299C5.77547 9.6068 6.09035 10.1569 6.32314 10.5636L6.34884 10.6085C6.61417 11.0729 6.77852 11.3805 6.81869 11.6918C6.85069 11.9398 6.81076 12.2724 6.41437 12.7679C6.15561 13.0914 6.20805 13.5633 6.5315 13.8221C6.85494 14.0808 7.32691 14.0284 7.58567 13.7049C8.18928 12.9504 8.39935 12.2206 8.30635 11.4998C8.22457 10.866 7.90969 10.3159 7.6769 9.9092L7.6512 9.86428C7.38587 9.39996 7.22152 9.09233 7.18135 8.78103C7.14935 8.53302 7.18928 8.2004 7.58567 7.70491ZM10.8885 6.65074C11.212 6.90949 11.2644 7.38146 11.0057 7.70491C10.6093 8.2004 10.5693 8.53302 10.6013 8.78103C10.6415 9.09233 10.8059 9.39996 11.0712 9.86428L11.0969 9.9092C11.3297 10.3159 11.6446 10.866 11.7263 11.4998C11.8193 12.2206 11.6093 12.9504 11.0057 13.7049C10.7469 14.0284 10.2749 14.0808 9.95148 13.8221C9.62803 13.5633 9.57559 13.0914 9.83435 12.7679C10.2307 12.2724 10.2707 11.9398 10.2387 11.6918C10.1985 11.3805 10.0341 11.0729 9.76882 10.6085L9.74312 10.5636C9.51033 10.1569 9.19545 9.6068 9.11367 8.97299C9.02067 8.25226 9.23074 7.52237 9.83435 6.76787C10.0931 6.44442 10.5651 6.39198 10.8885 6.65074Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"smoking-area\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187054)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.3135 0.16823C12.0439 0.383861 12.0002 0.777169 12.2159 1.04671C12.6295 1.56378 12.6836 1.92951 12.6465 2.21692C12.6026 2.55705 12.424 2.88633 12.1612 3.34619L12.1384 3.38613C11.9039 3.79608 11.6001 4.32702 11.5215 4.93567C11.4336 5.61701 11.6295 6.31378 12.2159 7.04671C12.4315 7.31625 12.8248 7.35995 13.0943 7.14432C13.3639 6.92868 13.4076 6.53538 13.1919 6.26584C12.7783 5.74876 12.7242 5.38304 12.7613 5.09563C12.8051 4.75549 12.9838 4.42622 13.2466 3.96636L13.2694 3.92641C13.5039 3.51647 13.8077 2.98553 13.8863 2.37688C13.9742 1.69554 13.7783 0.998762 13.1919 0.265839C12.9763 -0.0037 12.583 -0.0474009 12.3135 0.16823ZM9.64354 0.16823C9.374 0.383861 9.3303 0.777169 9.54593 1.04671C9.9596 1.56378 10.0137 1.92951 9.97662 2.21692C9.93273 2.55705 9.7541 2.88633 9.49133 3.34619L9.46848 3.38613C9.23393 3.79608 8.93015 4.32702 8.85162 4.93567C8.7637 5.61701 8.9596 6.31378 9.54593 7.04671C9.76157 7.31625 10.1549 7.35995 10.4244 7.14432C10.694 6.92868 10.7377 6.53538 10.522 6.26584C10.1084 5.74876 10.0543 5.38304 10.0913 5.09563C10.1352 4.75549 10.3139 4.42622 10.5766 3.96636L10.5995 3.92641C10.834 3.51647 11.1378 2.98553 11.2163 2.37688C11.3043 1.69554 11.1084 0.998762 10.522 0.265839C10.3064 -0.0037 9.91308 -0.0474009 9.64354 0.16823ZM13.5 8.87502H11.625V13.875H13.5C13.7761 13.875 14 13.6511 14 13.375V9.37502C14 9.09888 13.7761 8.87502 13.5 8.87502ZM1.5 13.875H10.375V8.87502H1.5C0.671573 8.87502 0 9.5466 0 10.375V12.375C0 13.2034 0.671574 13.875 1.5 13.875Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187054\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"snorkle\", \"keywords\": [ \"diving\", \"scuba\", \"outdoor\", \"recreation\", \"ocean\", \"mask\", \"water\", \"sea\", \"snorkle\", \"travel\", \"places\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187048)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.1885 0.0620117C13.6027 0.0620117 13.9385 0.397798 13.9385 0.812012V10.332C13.9385 11.2883 13.5586 12.2055 12.8823 12.8818C12.2061 13.558 11.2889 13.938 10.3325 13.938H8.4285C7.47213 13.938 6.55494 13.558 5.87868 12.8818C5.20243 12.2055 4.82251 11.2883 4.82251 10.332C4.82251 9.91776 5.1583 9.58197 5.57251 9.58197C5.98673 9.58197 6.32251 9.91776 6.32251 10.332C6.32251 10.8905 6.54439 11.4262 6.93934 11.8211C7.33429 12.2161 7.86996 12.438 8.4285 12.438H10.3325C10.891 12.438 11.4267 12.2161 11.8217 11.8211C12.2166 11.4262 12.4385 10.8905 12.4385 10.332V0.812012C12.4385 0.397798 12.7743 0.0620117 13.1885 0.0620117ZM1.01661 2.83775C1.46745 2.38691 2.07892 2.13364 2.71649 2.13364H8.42847C9.06605 2.13364 9.67751 2.38691 10.1284 2.83775C10.5792 3.28859 10.8325 3.90005 10.8325 4.53763V6.44162C10.8325 7.0792 10.5792 7.69066 10.1284 8.1415C9.67751 8.59234 9.06605 8.84561 8.42847 8.84561H7.47648C7.30953 8.84561 7.1536 8.76229 7.06082 8.62351L5.94764 6.9585C5.90632 6.89717 5.8503 6.84648 5.78503 6.8117C5.7196 6.77685 5.64661 6.75861 5.57248 6.75861C5.49835 6.75861 5.42536 6.77685 5.35994 6.8117C5.29467 6.84648 5.23893 6.89675 5.19761 6.95808L4.08415 8.62351C3.99136 8.76229 3.83543 8.84561 3.66849 8.84561H2.71649C2.07892 8.84561 1.46745 8.59234 1.01661 8.1415C0.565777 7.69066 0.3125 7.0792 0.3125 6.44162V4.53763C0.3125 3.90005 0.565777 3.28859 1.01661 2.83775Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187048\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"steering-wheel\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187072)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.3148 5.25C3.02417 3.35167 4.85424 2 7 2C9.14576 2 10.9758 3.35167 11.6852 5.25H2.3148ZM2.00614 6.75C2.00206 6.83282 2 6.91617 2 7C2 9.50652 3.84437 11.5823 6.25 11.9441V9.14286L4.15625 6.75H2.00614ZM7.75 11.9441C10.1556 11.5823 12 9.50652 12 7C12 6.91617 11.9979 6.83282 11.9939 6.75H9.84375L7.75 9.14286V11.9441ZM7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187072\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"street-road\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187057)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.25 0H3.5C3.26718 0 3.06516 0.160705 3.01281 0.38757L0.0128089 13.3876C-0.0214535 13.536 0.0138552 13.692 0.108715 13.8113C0.203575 13.9305 0.347632 14 0.500005 14H6.25V11.8461C6.25 11.4319 6.58579 11.0961 7 11.0961C7.41421 11.0961 7.75 11.4319 7.75 11.8461V14H13.5C13.6524 14 13.7964 13.9305 13.8913 13.8113C13.9862 13.692 14.0215 13.536 13.9872 13.3876L10.9872 0.38757C10.9348 0.160705 10.7328 0 10.5 0H7.75V2.15385C7.75 2.56806 7.41421 2.90385 7 2.90385C6.58579 2.90385 6.25 2.56806 6.25 2.15385V0ZM7 5.17303C7.41421 5.17303 7.75 5.50882 7.75 5.92303V8.07688C7.75 8.49109 7.41421 8.82688 7 8.82688C6.58579 8.82688 6.25 8.49109 6.25 8.07688V5.92303C6.25 5.50882 6.58579 5.17303 7 5.17303Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187057\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"street-sign\", \"keywords\": [ \"crossroad\", \"street\", \"sign\", \"metaphor\", \"directions\", \"travel\", \"places\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187042)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.74939 0.794434C7.74939 0.380221 7.4136 0.0444336 6.99939 0.0444336C6.95341 0.0444336 6.9084 0.0485708 6.86471 0.0564932H2.53767C2.47137 0.0564932 2.40778 0.0828324 2.3609 0.129716L0.377911 2.1127C0.28028 2.21034 0.28028 2.36863 0.377911 2.46626L2.3609 4.44924C2.40778 4.49613 2.47137 4.52247 2.53767 4.52247H6.24939V6.50121H2.53767C2.47137 6.50121 2.40778 6.52755 2.3609 6.57443L0.377911 8.55742C0.28028 8.65505 0.28028 8.81334 0.377911 8.91097L2.3609 10.8939C2.40778 10.9408 2.47137 10.9672 2.53767 10.9672H6.24939V13.2053C6.24939 13.6196 6.58517 13.9553 6.99939 13.9553C7.4136 13.9553 7.74939 13.6196 7.74939 13.2053V7.80657H11.4611C11.5274 7.80657 11.591 7.78023 11.6379 7.73334L13.6209 5.75036C13.7185 5.65273 13.7185 5.49444 13.6209 5.3968L11.6379 3.41382C11.591 3.36693 11.5274 3.34059 11.4611 3.34059H7.74939V0.794434Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187042\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"take-off\", \"keywords\": [ \"travel\", \"plane\", \"adventure\", \"airplane\", \"take\", \"off\", \"airport\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.5897 1.93422C11.1107 1.76496 10.6004 1.70234 10.0947 1.75073C9.59185 1.79884 9.10546 1.95554 8.66912 2.20996L3.71893 4.67546L2.89952 3.37661C2.89675 3.37223 2.89385 3.36793 2.89082 3.36373C2.8037 3.24298 2.68082 3.15267 2.53956 3.10558C2.39829 3.05849 2.2458 3.05701 2.10365 3.10134L2.10163 3.10197L0.732429 3.54172C0.561628 3.5961 0.418146 3.71499 0.333332 3.87292C0.248429 4.03101 0.228459 4.21597 0.277656 4.38854L0.279988 4.39672L1.44013 7.99668L1.44225 8.00327C1.49127 8.14211 1.56752 8.26977 1.66637 8.3789C1.76522 8.48802 1.88479 8.57638 2.01813 8.63885C2.15146 8.70131 2.29589 8.73662 2.443 8.74272C2.58823 8.74875 2.73321 8.72619 2.86971 8.67635L5.98727 7.66856L5.7535 8.80824C5.72601 8.93895 5.73472 9.07561 5.77905 9.20163C5.82348 9.32793 5.90179 9.43959 6.00542 9.52437C6.10904 9.60915 6.23399 9.66381 6.36659 9.68235C6.49491 9.70029 6.62561 9.68375 6.74529 9.63453L8.27206 9.13883C8.38712 9.10457 8.49146 9.04127 8.57502 8.95501C8.65853 8.86881 8.71845 8.76262 8.74907 8.64663L9.35093 6.58713L9.56504 6.51786L12.9538 5.41826C13.239 5.32926 13.4774 5.13092 13.6168 4.8666C13.7564 4.60176 13.7852 4.29234 13.6969 4.00627C13.6954 4.00135 13.6937 3.99648 13.6919 3.99167C13.5124 3.51638 13.2313 3.08602 12.8682 2.73065C12.5051 2.37528 12.0687 2.10347 11.5897 1.93422ZM12.9902 11.9881C13.5425 11.9881 13.9902 12.4358 13.9902 12.9881C13.9902 13.5404 13.5425 13.9881 12.9902 13.9881H1.00586C0.453574 13.9881 0.00585938 13.5404 0.00585938 12.9881C0.00585938 12.4358 0.453574 11.9881 1.00586 11.9881H12.9902Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"toilet-man\", \"keywords\": [ \"travel\", \"wayfinder\", \"toilet\", \"man\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.02976 2.03072C9.02976 3.15226 8.12058 4.06144 6.99904 4.06144C5.8775 4.06144 4.96832 3.15226 4.96832 2.03072C4.96832 0.909185 5.8775 0 6.99904 0C8.12058 0 9.02976 0.909185 9.02976 2.03072ZM9.51196 5.90257C10.1784 6.56903 10.5529 7.47294 10.5529 8.41546V9.4385C10.5529 9.71464 10.329 9.9385 10.0529 9.9385H9.02979L8.57686 13.562C8.54558 13.8122 8.33288 13.9999 8.08072 13.9999H5.91742C5.66526 13.9999 5.45256 13.8122 5.42128 13.562L4.96835 9.9385H3.94531C3.66917 9.9385 3.44531 9.71464 3.44531 9.4385V8.41546C3.44531 7.47294 3.81972 6.56903 4.48618 5.90257C5.15264 5.23611 6.05655 4.86169 6.99907 4.86169C7.94159 4.86169 8.8455 5.23611 9.51196 5.90257Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"toilet-sign-man-woman-2\", \"keywords\": [ \"toilet\", \"sign\", \"restroom\", \"bathroom\", \"user\", \"human\", \"person\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187096)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.5 2.5C0.5 1.11929 1.61929 0 3 0C4.38071 0 5.5 1.11929 5.5 2.5C5.5 3.88071 4.38071 5 3 5C1.61929 5 0.5 3.88071 0.5 2.5ZM11 0C9.61929 0 8.5 1.11929 8.5 2.5C8.5 3.88071 9.61929 5 11 5C12.3807 5 13.5 3.88071 13.5 2.5C13.5 1.11929 12.3807 0 11 0ZM9.11395 7.35764C9.49748 6.64171 10.0978 6 11 6C11.9022 6 12.5025 6.64171 12.8861 7.35764C13.2748 8.08325 13.5203 9.01942 13.6797 9.9121C13.8406 10.8131 13.9203 11.7091 13.9601 12.3765C13.98 12.7111 13.99 12.9903 13.995 13.1865C13.9975 13.2846 13.9987 13.3621 13.9994 13.4154L13.9999 13.477L14 13.4935V13.498V13.4993C14 13.4995 14 13.5 13.5 13.5L14 13.4993C14 13.7754 13.7761 14 13.5 14H8.5C8.22386 14 8 13.7761 8 13.5H8.5C8 13.5 8 13.5002 8 13.5V13.498L8.00001 13.4935L8.00008 13.477L8.00065 13.4154C8.00128 13.3621 8.00254 13.2846 8.00504 13.1865C8.01005 12.9903 8.02003 12.7111 8.03995 12.3765C8.07968 11.7091 8.1594 10.8131 8.32029 9.9121C8.4797 9.01942 8.72523 8.08325 9.11395 7.35764ZM0 6.5C0 6.22386 0.223858 6 0.5 6H5.5C5.77614 6 6 6.22386 6 6.5H5.5C6 6.5 6 6.49986 6 6.5V6.50198L5.99999 6.50649L5.99992 6.52296L5.99935 6.58457C5.99872 6.63794 5.99746 6.71541 5.99495 6.81353C5.98995 7.0097 5.97997 7.28886 5.96005 7.62346C5.92032 8.29093 5.8406 9.18691 5.67971 10.0879C5.52031 10.9806 5.27477 11.9168 4.88605 12.6424C4.50252 13.3583 3.90224 14 3 14C2.09776 14 1.49748 13.3583 1.11395 12.6424C0.725229 11.9168 0.479695 10.9806 0.320286 10.0879C0.159397 9.18691 0.0796762 8.29093 0.0399459 7.62346C0.0200292 7.28886 0.0100497 7.0097 0.00504541 6.81353C0.00254232 6.71541 0.00128123 6.63794 0.000645787 6.58457L8.4579e-05 6.52296L1.15335e-05 6.50649L0 6.50198V6.5C0 6.49986 0 6.5 0.5 6.5H0Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187096\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"toilet-women\", \"keywords\": [ \"travel\", \"wayfinder\", \"toilet\", \"women\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187075)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.06638 2.06616C9.06638 3.20727 8.14133 4.13233 7.00022 4.13233C5.85911 4.13233 4.93405 3.20727 4.93405 2.06616C4.93405 0.925053 5.85911 0 7.00022 0C8.14133 0 9.06638 0.925053 9.06638 2.06616ZM8.49769 13.5988L8.88904 11.6548H10.4101C10.6563 11.6548 10.8585 11.4547 10.8803 11.1992C10.8829 11.1689 10.8791 11.1389 10.8728 11.1091C10.8 10.7677 10.7343 10.4049 10.667 10.0329C10.2422 7.68633 9.75133 4.97473 7.00005 4.97473C4.24899 4.97473 3.75813 7.68622 3.33334 10.0327C3.266 10.4048 3.20031 10.7676 3.12753 11.1091C3.12118 11.1389 3.11737 11.1689 3.11996 11.1992C3.14174 11.4547 3.34395 11.6548 3.59018 11.6548H5.11122L5.50263 13.5988C5.54962 13.8322 5.75469 14.0001 5.99279 14.0001H7.0004H8.00753C8.24563 14.0001 8.4507 13.8322 8.49769 13.5988Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187075\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"traffic-cone\", \"keywords\": [ \"street\", \"sign\", \"traffic\", \"cone\", \"road\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187078)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.71521 3.71393L5.37285 1.27128C5.4904 0.834663 5.8863 0.53125 6.33846 0.53125H7.66148C8.11364 0.53125 8.50954 0.834662 8.62709 1.27128L9.28473 3.71393H4.71521ZM4.37867 4.96393L3.43377 8.47357H10.5662L9.62127 4.96393H4.37867ZM2.44228 12.1562L3.09723 9.72357H10.9027L11.5577 12.1562H13.1875C13.6017 12.1562 13.9375 12.492 13.9375 12.9062C13.9375 13.3205 13.6017 13.6562 13.1875 13.6562H0.8125C0.398286 13.6562 0.0625 13.3205 0.0625 12.9062C0.0625 12.492 0.398286 12.1562 0.8125 12.1562H2.44228Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187078\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"triangle-flag\", \"keywords\": [ \"navigation\", \"map\", \"maps\", \"flag\", \"gps\", \"location\", \"destination\", \"goal\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.66602 0.0444336C2.87405 0.0444336 3.0623 0.129131 3.19816 0.265923L11.808 4.57087C11.9774 4.65556 12.0844 4.82869 12.0844 5.01808C12.0844 5.20747 11.9774 5.3806 11.808 5.46529L3.41602 9.66131V13.2054C3.41602 13.6196 3.08024 13.9554 2.66602 13.9554C2.25181 13.9554 1.91602 13.6196 1.91602 13.2054V0.794434C1.91602 0.38022 2.25181 0.0444336 2.66602 0.0444336Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"wheelchair-1\", \"keywords\": [ \"person\", \"access\", \"wheelchair\", \"accomodation\", \"human\", \"disability\", \"disabled\", \"user\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.98157 0C6.87702 0 5.9816 0.895414 5.9816 1.99996C5.9816 3.10451 6.87702 3.99993 7.98157 3.99993C9.08612 3.99993 9.98153 3.10451 9.98153 1.99996C9.98153 0.895414 9.08612 0 7.98157 0ZM7.98157 5C8.53386 5 8.98157 5.44772 8.98157 6V8H10.5205C11.0458 8 11.5495 8.20867 11.921 8.5801C12.2924 8.95153 12.5011 9.45529 12.5011 9.98058V13C12.5011 13.5523 12.0533 14 11.5011 14C10.9488 14 10.5011 13.5523 10.5011 13V10H7.98157C7.42929 10 6.98157 9.55228 6.98157 9V6C6.98157 5.44772 7.42929 5 7.98157 5ZM5.53087 7.13348C5.94508 7.13306 6.28053 6.79694 6.28011 6.38272C6.27969 5.96851 5.94356 5.63307 5.52935 5.63348C4.77445 5.63425 4.03383 5.83933 3.38607 6.22698C2.7383 6.61463 2.20754 7.17039 1.85009 7.8353C1.49265 8.50022 1.32184 9.24948 1.3558 10.0036C1.38976 10.7578 1.62721 11.4886 2.04297 12.1187C2.45873 12.7488 3.03729 13.2546 3.71727 13.5825C4.39726 13.9104 5.15331 14.0481 5.90522 13.981C6.65714 13.9139 7.37687 13.6445 7.98808 13.2015C8.27598 12.9928 8.53475 12.7492 8.75905 12.4774C9.0227 12.1579 8.97745 11.6852 8.65798 11.4215C8.33851 11.1579 7.86579 11.2031 7.60214 11.5226C7.45829 11.6969 7.29234 11.8531 7.1077 11.987C6.71572 12.2711 6.25414 12.4439 5.77192 12.4869C5.28971 12.5299 4.80483 12.4416 4.36875 12.2314C3.93266 12.0211 3.56161 11.6967 3.29498 11.2926C3.02834 10.8885 2.87606 10.4198 2.85428 9.93614C2.8325 9.4525 2.94204 8.97198 3.17128 8.54556C3.40052 8.11914 3.74091 7.76272 4.15634 7.51411C4.57176 7.2655 5.04673 7.13397 5.53087 7.13348Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"woman-symbol\", \"keywords\": [ \"geometric\", \"gender\", \"female\", \"person\", \"human\", \"user\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 1.5C5.20507 1.5 3.75 2.95507 3.75 4.75C3.75 6.54493 5.20507 8 7 8C8.79493 8 10.25 6.54493 10.25 4.75C10.25 2.95507 8.79493 1.5 7 1.5ZM11.75 4.75C11.75 7.11815 10.017 9.08154 7.75 9.44112V10.75H8.5C8.91421 10.75 9.25 11.0858 9.25 11.5C9.25 11.9142 8.91421 12.25 8.5 12.25H7.75V13.25C7.75 13.6642 7.41421 14 7 14C6.58579 14 6.25 13.6642 6.25 13.25V12.25H5.5C5.08579 12.25 4.75 11.9142 4.75 11.5C4.75 11.0858 5.08579 10.75 5.5 10.75H6.25V9.44112C3.98301 9.08154 2.25 7.11815 2.25 4.75C2.25 2.12665 4.37665 0 7 0C9.62335 0 11.75 2.12665 11.75 4.75Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" } ], \"money_shopping\": [ { \"name\": \"annoncement-megaphone\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188071)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.93408 0.767334C7.93408 0.35312 7.59829 0.017334 7.18408 0.017334C6.76987 0.017334 6.43408 0.35312 6.43408 0.767334V1.52082C6.43408 1.93503 6.76987 2.27082 7.18408 2.27082C7.59829 2.27082 7.93408 1.93503 7.93408 1.52082V0.767334ZM6.5957 2.92816C6.95442 2.72105 7.41311 2.84396 7.62022 3.20268L11.4778 9.88429C11.6849 10.243 11.562 10.7017 11.2033 10.9088C10.8446 11.1159 10.3859 10.993 10.1788 10.6343L10.1234 10.5383L7.03185 11.2562C7.13089 11.6203 7.13421 12.0072 7.03655 12.3785C6.89006 12.9354 6.52833 13.4113 6.03095 13.7015C5.53357 13.9917 4.94128 14.0724 4.38438 13.9259C3.82747 13.7794 3.35157 13.4177 3.06137 12.9203L3.05964 12.9174L2.6835 12.2659L1.22998 12.6034C1.01562 12.6531 0.793913 12.5569 0.683879 12.3663L0.0748079 11.3114C-0.0352259 11.1208 -0.00771366 10.8807 0.142575 10.7199L6.37768 4.05053L6.32118 3.95268C6.11407 3.59396 6.23698 3.13527 6.5957 2.92816ZM4.2108 11.9112L5.56739 11.5962C5.61393 11.7239 5.62095 11.8637 5.5859 11.9969C5.54062 12.1691 5.42879 12.3162 5.27502 12.4059C5.12125 12.4956 4.93815 12.5206 4.76598 12.4753C4.59426 12.4301 4.44746 12.3188 4.35768 12.1656L4.2108 11.9112ZM13.9902 6.82339C13.9902 7.23761 13.6544 7.57339 13.2402 7.57339H12.4867C12.0725 7.57339 11.7367 7.23761 11.7367 6.82339C11.7367 6.40918 12.0725 6.07339 12.4867 6.07339H13.2402C13.6544 6.07339 13.9902 6.40918 13.9902 6.82339ZM1.88232 7.57339C2.29654 7.57339 2.63232 7.23761 2.63232 6.82339C2.63232 6.40918 2.29654 6.07339 1.88232 6.07339H1.12884C0.714625 6.07339 0.378838 6.40918 0.378838 6.82339C0.378838 7.23761 0.714625 7.57339 1.12884 7.57339H1.88232ZM4.0652 3.70512C3.7723 3.998 3.29743 3.99799 3.00454 3.70508L2.3709 3.0714C2.07801 2.7785 2.07803 2.30362 2.37093 2.01074C2.66383 1.71786 3.13871 1.71787 3.43159 2.01077L4.06524 2.64446C4.35812 2.93736 4.35811 3.41223 4.0652 3.70512ZM11.9969 3.0714C12.2897 2.77851 12.2897 2.30363 11.9969 2.01074C11.704 1.71785 11.2291 1.71785 10.9362 2.01074L10.2951 2.65179C10.0022 2.94468 10.0022 3.41955 10.2951 3.71245C10.588 4.00534 11.0629 4.00534 11.3558 3.71245L11.9969 3.0714Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188071\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"backpack\", \"keywords\": [ \"bag\", \"backpack\", \"school\", \"baggage\", \"cloth\", \"clothing\", \"accessories\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188110)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.11612 1.86612C6.35054 1.6317 6.66848 1.5 7 1.5C7.33152 1.5 7.64946 1.6317 7.88388 1.86612C8.1183 2.10054 8.25 2.41848 8.25 2.75V2.95215H5.75V2.75C5.75 2.41848 5.8817 2.10054 6.11612 1.86612ZM4.25 3.10091V2.75C4.25 2.02065 4.53973 1.32118 5.05546 0.805456C5.57118 0.289731 6.27065 0 7 0C7.72935 0 8.42882 0.289731 8.94454 0.805456C9.46027 1.32118 9.75 2.02065 9.75 2.75V3.10091C9.99632 3.20247 10.2227 3.354 10.4142 3.54921C10.7893 3.93151 11 4.45001 11 4.99065V6.67969H7.625V6.3584C7.625 6.01322 7.34518 5.7334 7 5.7334C6.65482 5.7334 6.375 6.01322 6.375 6.3584V6.67969H3V4.99065C3 4.45001 3.21071 3.93151 3.58579 3.54921C3.77731 3.354 4.00368 3.20247 4.25 3.10091ZM6.375 7.92969H3V13.9521H11V7.92969H7.625V8.3584C7.625 8.70358 7.34518 8.9834 7 8.9834C6.65482 8.9834 6.375 8.70358 6.375 8.3584V7.92969ZM1.5 7.95215H1.75V13.9521H0.5C0.223858 13.9521 0 13.7283 0 13.4521V9.45215C0 9.05432 0.158035 8.67279 0.43934 8.39149C0.720644 8.11018 1.10217 7.95215 1.5 7.95215ZM12.5 7.95215H12.25V13.9521H13.5C13.7761 13.9521 14 13.7283 14 13.4521V9.45215C14 9.05432 13.842 8.67279 13.5607 8.39149C13.2794 8.11018 12.8978 7.95215 12.5 7.95215Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188110\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bag\", \"keywords\": [ \"bag\", \"payment\", \"cash\", \"money\", \"finance\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188077)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.4622 9.69155C13.4622 12.6643 10.7695 13.9999 6.99962 13.9999C3.22983 13.9999 0.537109 12.6643 0.537109 9.71309C0.537109 6.888 1.77211 5.28134 4.24209 3.82792C5.87163 5.05685 8.13438 5.05551 9.76254 3.82391C12.229 5.26214 13.4622 6.86851 13.4622 9.69155ZM8.7428 3.00807L10.0129 1.02336C10.0678 0.917982 10.095 0.800377 10.0918 0.681612C10.0887 0.562847 10.0554 0.446831 9.99504 0.344483C9.93471 0.242136 9.84933 0.156825 9.74693 0.0965824C9.64454 0.0363393 9.52849 0.00314575 9.40973 0.000126465H4.84289C4.7162 -0.00228148 4.59124 0.0297367 4.48132 0.0927673C4.3714 0.155798 4.28065 0.247478 4.21874 0.358032C4.15683 0.468585 4.12608 0.593868 4.12978 0.720523C4.13347 0.847178 4.17147 0.970455 4.23972 1.07721L5.28678 3.0264C6.34811 3.66208 7.6868 3.65597 8.7428 3.00807Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188077\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bag-dollar\", \"keywords\": [ \"bag\", \"payment\", \"cash\", \"money\", \"finance\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188080)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.4621 9.69167C13.4621 12.6644 10.7694 14 6.99961 14C3.22982 14 0.537109 12.6644 0.537109 9.71321C0.537109 6.48196 2.15273 4.84467 5.38399 3.2075L4.23971 1.07733C4.17146 0.970577 4.13346 0.8473 4.12977 0.720645C4.12607 0.593991 4.15682 0.468708 4.21873 0.358154C4.28064 0.2476 4.37139 0.15592 4.48131 0.0928894C4.59123 0.0298588 4.7162 -0.00215941 4.84288 0.000248535H9.40972C9.52849 0.00326782 9.64453 0.0364614 9.74693 0.0967045C9.84933 0.156948 9.93471 0.242258 9.99505 0.344606C10.0553 0.446953 10.0886 0.56297 10.0918 0.681734C10.0949 0.800499 10.0677 0.918104 10.0129 1.02348L8.61524 3.2075C11.8465 4.82313 13.4621 6.46041 13.4621 9.69167ZM7.6246 4.88745C7.6246 4.54227 7.34478 4.26245 6.9996 4.26245C6.65442 4.26245 6.3746 4.54227 6.3746 4.88745V5.514C5.44759 5.55285 4.70797 6.31667 4.70797 7.25323C4.70797 8.0713 5.27759 8.77899 6.07676 8.95381L7.54985 9.27605C7.83656 9.33876 8.04129 9.59304 8.04129 9.88697C8.04129 10.2324 7.76117 10.5124 7.41629 10.5124H6.58297C6.31182 10.5124 6.07947 10.3394 5.99338 10.0958C5.87835 9.77036 5.52127 9.59979 5.19582 9.71482C4.87037 9.82985 4.6998 10.1869 4.81483 10.5124C5.04977 11.1771 5.64928 11.6708 6.3746 11.751V12.3874C6.3746 12.7326 6.65442 13.0124 6.9996 13.0124C7.34478 13.0124 7.6246 12.7326 7.6246 12.3874V11.751C8.56233 11.6473 9.29129 10.8518 9.29129 9.88697C9.29129 9.00606 8.67797 8.24326 7.81697 8.05492L6.34388 7.73268C6.11857 7.68339 5.95797 7.48387 5.95797 7.25323C5.95797 6.98218 6.1777 6.76245 6.44876 6.76245H7.41629C7.60134 6.76245 7.76696 6.84208 7.88229 6.97094C7.93648 7.03148 7.97868 7.10209 8.00588 7.17906C8.12091 7.50451 8.47799 7.67508 8.80344 7.56005C9.12889 7.44502 9.29946 7.08795 9.18443 6.7625C9.10189 6.52895 8.97473 6.31721 8.81371 6.13729C8.51469 5.8032 8.0958 5.57612 7.6246 5.52392V4.88745Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188080\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bag-pound\", \"keywords\": [ \"bag\", \"payment\", \"cash\", \"money\", \"finance\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188065)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.4621 9.69167C13.4621 12.6644 10.7694 14 6.99961 14C3.22982 14 0.537109 12.6644 0.537109 9.71321C0.537109 6.48196 2.15273 4.84467 5.38399 3.2075L4.23971 1.07733C4.17146 0.970577 4.13346 0.8473 4.12977 0.720645C4.12607 0.593991 4.15682 0.468708 4.21873 0.358154C4.28064 0.2476 4.37139 0.15592 4.48131 0.0928894C4.59123 0.0298588 4.7162 -0.00215941 4.84288 0.000248535H9.40972C9.52849 0.00326782 9.64453 0.0364614 9.74693 0.0967045C9.84933 0.156948 9.93471 0.242258 9.99505 0.344606C10.0553 0.446953 10.0886 0.56297 10.0918 0.681734C10.0949 0.800499 10.0677 0.918104 10.0129 1.02348L8.61524 3.2075C11.8465 4.82313 13.4621 6.46041 13.4621 9.69167ZM8.48799 5.61532C8.6392 5.66794 8.81408 5.74434 8.92583 5.8384C9.13709 6.01623 9.16419 6.33165 8.98636 6.54291C8.80979 6.75268 8.49757 6.78089 8.28636 6.6072L8.28185 6.60345C8.28335 6.60471 8.28485 6.60596 8.28636 6.6072L8.2899 6.61015C8.29445 6.6139 8.29675 6.6158 8.29656 6.61611C8.29636 6.61642 8.29352 6.61505 8.28777 6.61226C8.27123 6.60425 8.2306 6.58457 8.1593 6.55976C8.04982 6.52165 7.90677 6.48544 7.75159 6.4704C7.43595 6.43982 7.13634 6.49971 6.91572 6.72032C6.79907 6.83696 6.72186 7.03172 6.69374 7.3717C6.67951 7.54384 6.67916 7.7199 6.68323 7.90839H8.1663C8.44244 7.90839 8.6663 8.13224 8.6663 8.40839C8.6663 8.68453 8.44244 8.90839 8.1663 8.90839H6.69687C6.68538 9.23052 6.64407 9.57074 6.52339 9.88244C6.51457 9.90521 6.50536 9.92778 6.49573 9.95012H8.74967C9.02582 9.95012 9.24967 10.174 9.24967 10.4501C9.24967 10.7263 9.02582 10.9501 8.74967 10.9501H5.24967C5.00445 10.9501 4.79541 10.7723 4.75613 10.5302C4.71685 10.2881 4.85892 10.0533 5.09156 9.97578C5.38189 9.879 5.51283 9.72287 5.59085 9.52138C5.65458 9.35678 5.68506 9.15406 5.69584 8.90839H5.24963C4.97349 8.90839 4.74963 8.68453 4.74963 8.40839C4.74963 8.13224 4.97349 7.90839 5.24963 7.90839H5.6831C5.67949 7.69931 5.68084 7.48641 5.69714 7.28929C5.7323 6.86417 5.84202 6.3798 6.20862 6.0132C6.71718 5.50466 7.36287 5.42806 7.84802 5.47506C8.09323 5.49882 8.31444 5.55492 8.48799 5.61532Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188065\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bag-rupee\", \"keywords\": [ \"bag\", \"payment\", \"cash\", \"money\", \"finance\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188068)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.4621 9.69167C13.4621 12.6644 10.7694 14 6.99961 14C3.22982 14 0.537109 12.6644 0.537109 9.71321C0.537109 6.48196 2.15273 4.84467 5.38399 3.2075L4.23971 1.07733C4.17146 0.970577 4.13346 0.8473 4.12977 0.720645C4.12607 0.593991 4.15682 0.468708 4.21873 0.358154C4.28064 0.2476 4.37139 0.15592 4.48131 0.0928894C4.59123 0.0298588 4.7162 -0.00215941 4.84288 0.000248535H9.40972C9.52849 0.00326782 9.64453 0.0364614 9.74693 0.0967045C9.84933 0.156948 9.93471 0.242258 9.99505 0.344606C10.0553 0.446953 10.0886 0.56297 10.0918 0.681734C10.0949 0.800499 10.0677 0.918104 10.0129 1.02348L8.61524 3.2075C11.8465 4.82313 13.4621 6.46041 13.4621 9.69167ZM4.95788 6.5H6.64076C6.78084 6.54171 6.95613 6.61938 7.09718 6.74986C7.18684 6.83279 7.27163 6.94443 7.32323 7.10413H4.95788C4.68174 7.10413 4.45788 7.32798 4.45788 7.60413C4.45788 7.88027 4.68174 8.10413 4.95788 8.10413H7.16457C7.15522 8.11566 7.14566 8.1269 7.13591 8.13787C6.98903 8.30311 6.77746 8.42936 6.53197 8.52141C6.28856 8.61269 6.03802 8.66052 5.84343 8.68485C5.74744 8.69685 5.6682 8.70274 5.61438 8.70563C5.58754 8.70707 5.56724 8.70774 5.55458 8.70806L5.54151 8.70831L5.53965 8.70833C5.33184 8.70883 5.14594 8.83785 5.07282 9.03247C4.99955 9.22746 5.05506 9.44763 5.21183 9.5848L5.21261 9.58549L5.21404 9.58673L5.2182 9.59032L5.23163 9.60177C5.24285 9.61125 5.25857 9.62436 5.27865 9.64072C5.31881 9.67343 5.37647 9.71914 5.4505 9.77467C5.59845 9.88563 5.81249 10.0363 6.08346 10.2008C6.62378 10.5289 7.39971 10.9176 8.33627 11.1517C8.60417 11.2187 8.87563 11.0558 8.94261 10.7879C9.00958 10.52 8.8467 10.2486 8.5788 10.1816C7.88542 10.0082 7.28725 9.73529 6.82817 9.4778C6.84645 9.4713 6.86476 9.46462 6.8831 9.45774C7.22094 9.33105 7.5927 9.12918 7.88332 8.80223C8.05727 8.60655 8.19461 8.37419 8.27927 8.10413H9.04122C9.31736 8.10413 9.54122 7.88027 9.54122 7.60413C9.54122 7.32798 9.31736 7.10413 9.04122 7.10413H8.34752C8.31201 6.87638 8.24272 6.67555 8.14973 6.5H9.04122C9.31736 6.5 9.54122 6.27614 9.54122 6C9.54122 5.72386 9.31736 5.5 9.04122 5.5H6.71817C6.71081 5.49984 6.70347 5.49984 6.69615 5.5H4.95788C4.68174 5.5 4.45788 5.72386 4.45788 6C4.45788 6.27614 4.68174 6.5 4.95788 6.5ZM5.54087 9.20833C5.21162 9.58462 5.21172 9.58471 5.21183 9.5848L5.54087 9.20833Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188068\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bag-suitcase-1\", \"keywords\": [ \"product\", \"business\", \"briefcase\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188107)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.82322 1.62669C4.87011 1.57981 4.9337 1.55347 5 1.55347H9C9.0663 1.55347 9.12989 1.57981 9.17678 1.62669C9.22366 1.67357 9.25 1.73716 9.25 1.80347V3H4.75V1.80347C4.75 1.73716 4.77634 1.67357 4.82322 1.62669ZM3.25 3V1.80347C3.25 1.33934 3.43437 0.894218 3.76256 0.56603C4.09075 0.237841 4.53587 0.0534668 5 0.0534668H9C9.46413 0.0534668 9.90925 0.237841 10.2374 0.56603C10.5656 0.894218 10.75 1.33934 10.75 1.80347V3H12.5C13.3284 3 14 3.67157 14 4.5V12.5C14 13.3284 13.3284 14 12.5 14H1.5C0.671573 14 0 13.3284 0 12.5V4.5C0 3.67157 0.671573 3 1.5 3H3.25ZM2.875 6.57819C2.875 6.23301 3.15482 5.95319 3.5 5.95319H10.5C10.8452 5.95319 11.125 6.23301 11.125 6.57819C11.125 6.92336 10.8452 7.20319 10.5 7.20319H3.5C3.15482 7.20319 2.875 6.92336 2.875 6.57819ZM3.5 9.79681C3.15482 9.79681 2.875 10.0766 2.875 10.4218C2.875 10.767 3.15482 11.0468 3.5 11.0468H10.5C10.8452 11.0468 11.125 10.767 11.125 10.4218C11.125 10.0766 10.8452 9.79681 10.5 9.79681H3.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188107\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bag-suitcase-2\", \"keywords\": [ \"product\", \"business\", \"briefcase\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188062)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5 0.0197754C4.53587 0.0197754 4.09075 0.20415 3.76256 0.532339C3.43437 0.860527 3.25 1.30565 3.25 1.76978V3.00003H1.5C0.671573 3.00003 0 3.6716 0 4.50003V12.5C0 13.3285 0.671573 14 1.5 14H12.5C13.3284 14 14 13.3285 14 12.5V4.50003C14 3.6716 13.3284 3.00003 12.5 3.00003H10.75V1.76978C10.75 1.30565 10.5656 0.860527 10.2374 0.532339C9.90925 0.20415 9.46413 0.0197754 9 0.0197754H5ZM9.25 3.00003V1.76978C9.25 1.70347 9.22366 1.63988 9.17678 1.593C9.12989 1.54611 9.0663 1.51978 9 1.51978H5C4.9337 1.51978 4.87011 1.54611 4.82322 1.593C4.77634 1.63988 4.75 1.70347 4.75 1.76978V3.00003H9.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188062\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bag-yen\", \"keywords\": [ \"bag\", \"payment\", \"cash\", \"money\", \"finance\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188095)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.4621 9.69167C13.4621 12.6644 10.7694 14 6.99961 14C3.22982 14 0.537109 12.6644 0.537109 9.71321C0.537109 6.48196 2.15273 4.84467 5.38399 3.2075L4.23971 1.07733C4.17146 0.970577 4.13346 0.8473 4.12977 0.720645C4.12607 0.593991 4.15682 0.468708 4.21873 0.358154C4.28064 0.2476 4.37139 0.15592 4.48131 0.0928894C4.59123 0.0298588 4.7162 -0.00215941 4.84288 0.000248535H9.40972C9.52849 0.00326782 9.64453 0.0364614 9.74693 0.0967045C9.84933 0.156948 9.93471 0.242258 9.99505 0.344606C10.0553 0.446953 10.0886 0.56297 10.0918 0.681734C10.0949 0.800499 10.0677 0.918104 10.0129 1.02348L8.61524 3.2075C11.8465 4.82313 13.4621 6.46041 13.4621 9.69167ZM4.94966 5.60003C5.17057 5.43434 5.48397 5.47912 5.64966 5.70003L6.99966 7.50003L8.34966 5.70003C8.51534 5.47912 8.82874 5.43434 9.04966 5.60003C9.27057 5.76571 9.31534 6.07912 9.14966 6.30003L7.9997 7.83331H8.74963C9.02577 7.83331 9.24963 8.05717 9.24963 8.33331C9.24963 8.60946 9.02577 8.83331 8.74963 8.83331H7.49963V9.29163H8.74963C9.02577 9.29163 9.24963 9.51548 9.24963 9.79163C9.24963 10.0678 9.02577 10.2916 8.74963 10.2916H7.49963V10.9583C7.49963 11.2345 7.27577 11.4583 6.99963 11.4583C6.72349 11.4583 6.49963 11.2345 6.49963 10.9583V10.2916H5.24963C4.97349 10.2916 4.74963 10.0678 4.74963 9.79163C4.74963 9.51548 4.97349 9.29163 5.24963 9.29163H6.49963V8.83331H5.24963C4.97349 8.83331 4.74963 8.60946 4.74963 8.33331C4.74963 8.05717 4.97349 7.83331 5.24963 7.83331H5.99962L4.84966 6.30003C4.68397 6.07912 4.72875 5.76571 4.94966 5.60003Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188095\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ball\", \"keywords\": [ \"sports\", \"ball\", \"sport\", \"basketball\", \"shopping\", \"catergories\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188083)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.33308 2.88996C0.49455 4.0441 0 5.46433 0 7.00007C0 8.53576 0.494524 9.95595 1.33301 11.1101C1.34578 11.0999 1.35904 11.0902 1.37279 11.0808C1.99016 10.663 2.50769 10.0707 2.86955 9.35511C3.2315 8.63934 3.42343 7.82808 3.42343 6.99999C3.42343 6.1719 3.2315 5.36064 2.86955 4.64487C2.50769 3.92928 1.99016 3.33695 1.37279 2.91914C1.35906 2.90985 1.34582 2.90011 1.33308 2.88996ZM2.15949 1.94342C2.92319 2.48202 3.54872 3.21795 3.98504 4.08079C4.43756 4.97566 4.67343 5.98071 4.67343 6.99999C4.67343 8.01927 4.43756 9.02432 3.98504 9.91919C3.54871 10.7821 2.92314 11.518 2.1594 12.0566C3.2756 13.1254 4.74505 13.8283 6.375 13.9725V0.0275879C4.74509 0.171804 3.27568 0.874652 2.15949 1.94342ZM7.625 13.9725C9.25495 13.8283 10.7244 13.1254 11.8406 12.0566C11.0769 11.518 10.4513 10.7821 10.015 9.91922C9.56244 9.02435 9.32657 8.0193 9.32657 7.00002C9.32657 5.98074 9.56244 4.97569 10.015 4.08082C10.4513 3.21797 11.0768 2.48204 11.8405 1.94344C10.7243 0.874659 9.25492 0.171804 7.625 0.0275879V13.9725ZM14 7.00007C14 8.53577 13.5055 9.95597 12.667 11.1101C12.6542 11.0999 12.641 11.0902 12.6272 11.0809C12.0098 10.6631 11.4923 10.0707 11.1304 9.35514C10.7685 8.63937 10.5766 7.82811 10.5766 7.00002C10.5766 6.17193 10.7685 5.36067 11.1304 4.6449C11.4923 3.9293 12.0098 3.33697 12.6272 2.91916C12.6409 2.90987 12.6542 2.90013 12.6669 2.88997C13.5055 4.04412 14 5.46434 14 7.00007Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188083\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bank\", \"keywords\": [ \"institution\", \"saving\", \"bank\", \"payment\", \"finance\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188086)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 0.065918C6.78426 0.065918 6.57244 0.123602 6.38648 0.232993C6.37959 0.237051 6.37279 0.241273 6.36609 0.245656L0.456092 4.11565L0.454848 4.11648C0.0355682 4.39282 -0.0915573 4.86989 0.0703427 5.27734C0.226025 5.66914 0.619106 5.93395 1.09 5.93395H12.91C13.3809 5.93395 13.774 5.66914 13.9297 5.27734C14.0916 4.86989 13.9644 4.39282 13.5452 4.11647L13.5439 4.11566L7.63391 0.245656C7.62722 0.241273 7.62042 0.237051 7.61353 0.232993C7.42757 0.123602 7.21575 0.065918 7 0.065918ZM0 11.8772C0 11.2768 0.57154 10.988 1 10.988H1.05471C1.0528 10.9703 1.05182 10.9522 1.05182 10.934V7.72288C1.05182 7.44673 1.27568 7.22288 1.55182 7.22288H3.21586C3.492 7.22288 3.71586 7.44673 3.71586 7.72288V10.934C3.71586 10.9522 3.71488 10.9703 3.71298 10.988H5.67073C5.66883 10.9703 5.66785 10.9522 5.66785 10.934V7.72288C5.66785 7.44673 5.8917 7.22288 6.16785 7.22288H7.83189C8.10803 7.22288 8.33189 7.44673 8.33189 7.72288V10.934C8.33189 10.9522 8.33091 10.9703 8.329 10.988H10.2868C10.2849 10.9703 10.2839 10.9522 10.2839 10.934V7.72288C10.2839 7.44673 10.5077 7.22288 10.7839 7.22288H12.4479C12.7241 7.22288 12.9479 7.44673 12.9479 7.72288V10.934C12.9479 10.9522 12.9469 10.9703 12.945 10.988H13C13.4285 10.988 14 11.2768 14 11.8772V13.0448C14 13.6452 13.4285 13.934 13 13.934H1C0.57154 13.934 0 13.6452 0 13.0448V11.8772Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188086\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"beanie\", \"keywords\": [ \"beanie\", \"winter\", \"hat\", \"warm\", \"cloth\", \"clothing\", \"wearable\", \"accessories\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188101)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8 1C8 0.447715 7.55228 0 7 0C6.44772 0 6 0.447715 6 1V1.56571C4.77969 1.77197 3.64405 2.35248 2.75736 3.23917C1.63214 4.36439 1 5.89051 1 7.48181V9.50235C1.02554 9.50152 1.05118 9.5011 1.07692 9.5011H12.9231C12.9488 9.5011 12.9745 9.50152 13 9.50235V7.48181C13 5.89051 12.3679 4.36439 11.2426 3.23917C10.356 2.35248 9.22031 1.77197 8 1.56571V1ZM1.07692 10.7511H12.9231C13.5178 10.7511 14 11.2333 14 11.828V12.9049C14 13.4997 13.5178 13.9819 12.9231 13.9819H1.07692C0.482155 13.9819 0 13.4997 0 12.9049V11.828C0 11.2333 0.482155 10.7511 1.07692 10.7511Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188101\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bill-1\", \"keywords\": [ \"billing\", \"bills\", \"payment\", \"finance\", \"cash\", \"currency\", \"money\", \"accounting\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188137)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0C0.698528 0 0 0.620805 0 1.44514V7.11598C0 7.94031 0.698528 8.56112 1.5 8.56112H12.5C13.3015 8.56112 14 7.94031 14 7.11598V1.44514C14 0.620805 13.3015 0 12.5 0H1.5ZM7 2.47729C6.00409 2.47729 5.19675 3.28464 5.19675 4.28055C5.19675 5.27646 6.00409 6.08381 7 6.08381C7.99592 6.08381 8.80326 5.27646 8.80326 4.28055C8.80326 3.28464 7.99592 2.47729 7 2.47729ZM3.52858 4.50184C3.52858 4.91707 3.19197 5.25368 2.77674 5.25368C2.36151 5.25368 2.0249 4.91707 2.0249 4.50184C2.0249 4.08661 2.36151 3.75 2.77674 3.75C3.19197 3.75 3.52858 4.08661 3.52858 4.50184ZM11.2233 5.25368C11.6385 5.25368 11.9751 4.91707 11.9751 4.50184C11.9751 4.08661 11.6385 3.75 11.2233 3.75C10.808 3.75 10.4714 4.08661 10.4714 4.50184C10.4714 4.91707 10.808 5.25368 11.2233 5.25368ZM1.08554 10.5605C1.08554 10.1463 1.42133 9.81055 1.83554 9.81055H12.1645C12.5787 9.81055 12.9145 10.1463 12.9145 10.5605C12.9145 10.9748 12.5787 11.3105 12.1645 11.3105H1.83554C1.42133 11.3105 1.08554 10.9748 1.08554 10.5605ZM2.77454 12.4658C2.36032 12.4658 2.02454 12.8016 2.02454 13.2158C2.02454 13.63 2.36032 13.9658 2.77454 13.9658H11.2255C11.6397 13.9658 11.9755 13.63 11.9755 13.2158C11.9755 12.8016 11.6397 12.4658 11.2255 12.4658H2.77454Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188137\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bill-2\", \"keywords\": [ \"currency\", \"billing\", \"payment\", \"finance\", \"cash\", \"bill\", \"money\", \"accounting\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 3.67537C0 2.85071 0.696096 2.22546 1.5 2.22546H12.5C13.3039 2.22546 14 2.85071 14 3.67537V10.3247C14 11.1494 13.3039 11.7746 12.5 11.7746H1.5C0.696096 11.7746 0 11.1494 0 10.3247V3.67537ZM4.9 7C4.9 5.8402 5.8402 4.9 7 4.9C8.1598 4.9 9.1 5.8402 9.1 7C9.1 8.1598 8.1598 9.1 7 9.1C5.8402 9.1 4.9 8.1598 4.9 7ZM3.00368 7.00001C3.00368 7.41524 2.66707 7.75185 2.25184 7.75185C1.83661 7.75185 1.5 7.41524 1.5 7.00001C1.5 6.58478 1.83661 6.24817 2.25184 6.24817C2.66707 6.24817 3.00368 6.58478 3.00368 7.00001ZM11.7482 7.75185C12.1634 7.75185 12.5 7.41524 12.5 7.00001C12.5 6.58478 12.1634 6.24817 11.7482 6.24817C11.333 6.24817 10.9964 6.58478 10.9964 7.00001C10.9964 7.41524 11.333 7.75185 11.7482 7.75185Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"bill-4\", \"keywords\": [ \"accounting\", \"billing\", \"payment\", \"finance\", \"cash\", \"currency\", \"money\", \"bill\", \"dollar\", \"stack\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 2.64941C0 1.83455 0.708993 1.25 1.48541 1.25H9.86141C10.6378 1.25 11.3468 1.83455 11.3468 2.64941V8.0459C11.3468 8.86077 10.6378 9.44531 9.86141 9.44531H1.48541C0.708993 9.44531 0 8.86077 0 8.0459V2.64941ZM4.14374 5.34769C4.14374 4.50291 4.82857 3.81809 5.67334 3.81809C6.51812 3.81809 7.20294 4.50291 7.20294 5.34769C7.20294 6.19246 6.51812 6.87729 5.67334 6.87729C4.82857 6.87729 4.14374 6.19246 4.14374 5.34769ZM1.34639 5.25C1.34639 4.83579 1.68218 4.5 2.09639 4.5C2.51048 4.5 2.84644 4.83567 2.84644 5.25C2.84644 5.66433 2.51048 6 2.09639 6C1.68218 6 1.34639 5.66421 1.34639 5.25ZM9.25024 4.5C8.83603 4.5 8.50024 4.83579 8.50024 5.25C8.50024 5.66421 8.83603 6 9.25024 6C9.66434 6 10.0003 5.66433 10.0003 5.25C10.0003 4.83567 9.66434 4.5 9.25024 4.5ZM13.9658 5.34766C13.9658 4.93344 13.63 4.59766 13.2158 4.59766C12.8016 4.59766 12.4658 4.93344 12.4658 5.34766V10.3477C12.4658 10.414 12.4395 10.4775 12.3926 10.5244C12.3457 10.5713 12.2821 10.5977 12.2158 10.5977H3.21579C2.80158 10.5977 2.46579 10.9334 2.46579 11.3477C2.46579 11.7619 2.80158 12.0977 3.21579 12.0977H12.2158C12.6799 12.0977 13.125 11.9133 13.4532 11.5851C13.7814 11.2569 13.9658 10.8118 13.9658 10.3477V5.34766Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"bill-cashless\", \"keywords\": [ \"currency\", \"billing\", \"payment\", \"finance\", \"no\", \"cash\", \"bill\", \"money\", \"accounting\", \"cashless\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188098)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.219669 1.27972C-0.0732231 0.986827 -0.0732231 0.511953 0.219669 0.21906C0.512562 -0.0738338 0.987436 -0.0738338 1.28033 0.21906L3.28673 2.22546H12.5007C13.3046 2.22546 14.0007 2.85071 14.0007 3.67537V10.3248C14.0007 11.0429 13.4728 11.6098 12.8056 11.7443L13.7817 12.7205C14.0746 13.0133 14.0746 13.4882 13.7817 13.7811C13.4888 14.074 13.0139 14.074 12.7211 13.7811L0.219669 1.27972ZM9.21605 8.15478C9.39647 7.80921 9.49848 7.41621 9.49848 6.99939C9.49848 5.61868 8.37919 4.49939 6.99848 4.49939C6.58166 4.49939 6.18866 4.6014 5.84309 4.78182L9.21605 8.15478ZM0.174384 2.99552C0.0639547 3.19623 0.00067804 3.42674 0.00067804 3.67537V10.3248C0.00067804 11.1494 0.696774 11.7747 1.50068 11.7747H8.9535L0.174384 2.99552Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188098\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"binance-circle\", \"keywords\": [ \"crypto\", \"circle\", \"payment\", \"blockchain\", \"finance\", \"binance\", \"currency\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188074)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7ZM9.89043 3.96313L7.39043 1.96313C7.16217 1.78052 6.83783 1.78052 6.60957 1.96313L4.10957 3.96313C3.84003 4.17876 3.79633 4.57207 4.01196 4.84161C4.22759 5.11115 4.6209 5.15485 4.89043 4.93921L7 3.25156L9.10957 4.93921C9.3791 5.15485 9.77241 5.11115 9.98804 4.84161C10.2037 4.57207 10.16 4.17876 9.89043 3.96313ZM4.89043 9.0603C4.6209 8.84467 4.22759 8.88837 4.01196 9.15791C3.79633 9.42744 3.84003 9.82075 4.10957 10.0364L6.60957 12.0364C6.83783 12.219 7.16217 12.219 7.39043 12.0364L9.89043 10.0364C10.16 9.82075 10.2037 9.42744 9.98804 9.15791C9.77241 8.88837 9.3791 8.84467 9.10957 9.0603L7 10.7479L4.89043 9.0603ZM7.75 7C7.75 7.41421 7.41421 7.75 7 7.75C6.58579 7.75 6.25 7.41421 6.25 7C6.25 6.58579 6.58579 6.25 7 6.25C7.41421 6.25 7.75 6.58579 7.75 7ZM3.8 7.75C4.21422 7.75 4.55 7.41421 4.55 7C4.55 6.58579 4.21422 6.25 3.8 6.25C3.38579 6.25 3.05 6.58579 3.05 7C3.05 7.41421 3.38579 7.75 3.8 7.75ZM10.95 7C10.95 7.41421 10.6142 7.75 10.2 7.75C9.78579 7.75 9.45 7.41421 9.45 7C9.45 6.58579 9.78579 6.25 10.2 6.25C10.6142 6.25 10.95 6.58579 10.95 7Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188074\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bitcoin\", \"keywords\": [ \"crypto\", \"circle\", \"payment\", \"blokchain\", \"finance\", \"bitcoin\", \"money\", \"currency\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.74291 1.02881C5.74291 0.476524 5.2952 0.0288086 4.74291 0.0288086C4.19063 0.0288086 3.74291 0.476524 3.74291 1.02881V1.63283C3.6069 1.70976 3.48069 1.80526 3.36825 1.91769C3.04117 2.24477 2.85742 2.68839 2.85742 3.15095V6.49494V10.5827C2.85742 11.0452 3.04117 11.4888 3.36825 11.8159C3.48069 11.9284 3.6069 12.0239 3.74291 12.1008V12.9712C3.74291 13.5235 4.19063 13.9712 4.74291 13.9712C5.2952 13.9712 5.74291 13.5235 5.74291 12.9712V12.3268H6.76174V12.9712C6.76174 13.5235 7.20946 13.9712 7.76174 13.9712C8.31403 13.9712 8.76174 13.5235 8.76174 12.9712V12.1656C9.27701 12.0014 9.75086 11.7157 10.1403 11.3262C10.781 10.6856 11.1409 9.81677 11.1409 8.91079C11.1409 8.00481 10.781 7.13593 10.1403 6.49531C10.0912 6.44613 10.0406 6.39862 9.98888 6.35279C10.4132 5.8218 10.6496 5.16177 10.6558 4.477C10.6627 3.70351 10.3754 2.95629 9.852 2.38675C9.54612 2.05392 9.173 1.79632 8.76174 1.62805V1.02881C8.76174 0.476524 8.31403 0.0288086 7.76174 0.0288086C7.20946 0.0288086 6.76174 0.476524 6.76174 1.02881V1.40686H5.74291V1.02881ZM4.85742 7.49494V10.3268H7.72486C8.10041 10.3268 8.46058 10.1776 8.72613 9.91205C8.99168 9.6465 9.14087 9.28633 9.14087 8.91079C9.14087 8.53524 8.99168 8.17507 8.72613 7.90952C8.46552 7.64891 8.11379 7.50038 7.74583 7.49494H4.85742ZM7.68946 5.49494H4.85742V3.40686H7.71765C7.97107 3.43366 8.20649 3.55192 8.37941 3.74008C8.55944 3.93597 8.65828 4.19298 8.65588 4.45902C8.65349 4.72507 8.55006 4.98026 8.36654 5.17288C8.18808 5.36019 7.94687 5.47473 7.68946 5.49494Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"bow-tie\", \"keywords\": [ \"bow\", \"tie\", \"dress\", \"gentleman\", \"cloth\", \"clothing\", \"accessories\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188119)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.95046 9.27949L12.6534 10.1699C12.8154 10.2089 12.9849 10.2122 13.1485 10.1797C13.3121 10.1472 13.4652 10.0797 13.5959 9.9827C13.7265 9.88565 13.831 9.76167 13.9011 9.62059C13.9711 9.47951 14.0048 9.32522 13.9995 9.16994V4.78994C14.0048 4.63467 13.9711 4.48037 13.9011 4.33929C13.831 4.19822 13.7265 4.07424 13.5959 3.97719C13.4652 3.88014 13.3121 3.81269 13.1485 3.78019C12.9849 3.74768 12.8154 3.75102 12.6534 3.78994L8.94032 4.71187C9.58861 5.26215 10 6.08303 10 7C10 7.91218 9.5929 8.72927 8.95046 9.27949ZM7.00002 9C8.10459 9 9.00002 8.10457 9.00002 7C9.00002 5.89543 8.10459 5 7.00002 5C5.89545 5 5.00002 5.89543 5.00002 7C5.00002 8.10457 5.89545 9 7.00002 9ZM1.3466 10.19L5.04958 9.29951C4.40714 8.74929 4.00003 7.9322 4.00003 7.02002C4.00003 6.10305 4.41142 5.28217 5.05971 4.73189L1.3466 3.80996C1.18465 3.77104 1.01515 3.7677 0.851555 3.80021C0.687963 3.83271 0.534778 3.90016 0.404144 3.99721C0.273511 4.09426 0.169021 4.21824 0.0989659 4.35931C0.0289099 4.50039 -0.00478674 4.65469 0.000547644 4.80996V9.18996C-0.00478674 9.34524 0.0289099 9.49953 0.0989659 9.64061C0.169021 9.78169 0.273511 9.90567 0.404144 10.0027C0.534778 10.0998 0.687963 10.1672 0.851555 10.1997C1.01515 10.2322 1.18465 10.2289 1.3466 10.19Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188119\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"briefcase-dollar\", \"keywords\": [ \"briefcase\", \"payment\", \"cash\", \"money\", \"finance\", \"baggage\", \"bag\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188104)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.76256 0.58666C4.09075 0.258471 4.53587 0.0740967 5 0.0740967H9C9.46413 0.0740967 9.90925 0.258471 10.2374 0.58666C10.5656 0.914848 10.75 1.35997 10.75 1.8241V3H12.5C13.3284 3 14 3.67157 14 4.5V12.5C14 13.3284 13.3284 14 12.5 14H1.5C0.671573 14 0 13.3284 0 12.5V4.5C0 3.67157 0.671573 3 1.5 3H3.25V1.8241C3.25 1.35997 3.43437 0.914848 3.76256 0.58666ZM9.25 1.8241V3H4.75V1.8241C4.75 1.75779 4.77634 1.6942 4.82322 1.64732C4.87011 1.60044 4.9337 1.5741 5 1.5741H9C9.0663 1.5741 9.12989 1.60044 9.17678 1.64732C9.22366 1.6942 9.25 1.75779 9.25 1.8241ZM7.75 4.75012C7.75 4.33591 7.41421 4.00012 7 4.00012C6.58579 4.00012 6.25 4.33591 6.25 4.75012V5.26063C5.3132 5.36003 4.58337 6.15274 4.58337 7.11591C4.58337 7.99271 5.19389 8.75122 6.05045 8.93859L7.52354 9.26083C7.75282 9.31098 7.91669 9.51441 7.91669 9.74964C7.91669 10.0261 7.69247 10.2501 7.41669 10.2501H6.58337C6.36684 10.2501 6.18063 10.112 6.11163 9.91683C5.9736 9.52629 5.5451 9.3216 5.15457 9.45963C4.76403 9.59767 4.55934 10.0262 4.69737 10.4167C4.93584 11.0914 5.52651 11.6011 6.25 11.7224V12.2501C6.25 12.6643 6.58579 13.0001 7 13.0001C7.41421 13.0001 7.75 12.6643 7.75 12.2501V11.7224C8.69616 11.5637 9.41669 10.7404 9.41669 9.74964C9.41669 8.81003 8.76251 7.99638 7.84409 7.79548L6.37099 7.47324C6.20307 7.43651 6.08337 7.2878 6.08337 7.11591C6.08337 6.91389 6.24714 6.75012 6.44916 6.75012H7.41669C7.5647 6.75012 7.69692 6.81348 7.78955 6.91697C7.83305 6.96558 7.86676 7.02208 7.88843 7.08338C8.02646 7.47392 8.45496 7.67862 8.84549 7.54058C9.23603 7.40254 9.44072 6.97405 9.30269 6.58352C9.21461 6.33431 9.07896 6.10845 8.90725 5.9166C8.61209 5.58682 8.20724 5.3547 7.75 5.27784V4.75012Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188104\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"building-2\", \"keywords\": [ \"real\", \"home\", \"tower\", \"building\", \"house\", \"estate\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188125)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.52109 0.137869C4.32801 -0.0459563 4.02466 -0.0459563 3.83157 0.137869L0.155238 3.63787C0.0561082 3.73224 0 3.86313 0 4V13.5C0 13.7761 0.223858 14 0.5 14H3.17633V12C3.17633 11.4477 3.62405 11 4.17633 11C4.72862 11 5.17633 11.4477 5.17633 12V14H7.85266C8.12881 14 8.35266 13.7761 8.35266 13.5V4C8.35266 3.86313 8.29656 3.73224 8.19743 3.63787L4.52109 0.137869ZM9.76862 6.5C9.76862 6.22386 9.99247 6 10.2686 6H13.5C13.7761 6 14 6.22386 14 6.5V13.5C14 13.7761 13.7761 14 13.5 14H10.2686C9.99247 14 9.76862 13.7761 9.76862 13.5V6.5ZM2.1727 7.76298C2.1727 7.4178 2.45252 7.13798 2.7977 7.13798H5.55495C5.90013 7.13798 6.17995 7.4178 6.17995 7.76298C6.17995 8.10816 5.90013 8.38798 5.55495 8.38798H2.7977C2.45252 8.38798 2.1727 8.10816 2.1727 7.76298ZM2.7977 4.53063C2.45252 4.53063 2.1727 4.81045 2.1727 5.15563C2.1727 5.50081 2.45252 5.78063 2.7977 5.78063H5.55495C5.90013 5.78063 6.17995 5.50081 6.17995 5.15563C6.17995 4.81045 5.90013 4.53063 5.55495 4.53063H2.7977Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188125\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"business-card\", \"keywords\": [ \"name\", \"card\", \"business\", \"information\", \"money\", \"payment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 2C0.671573 2 0 2.67157 0 3.5V10.5C0 11.3284 0.671573 12 1.5 12H12.5C13.3284 12 14 11.3284 14 10.5V3.5C14 2.67157 13.3284 2 12.5 2H1.5ZM4.27432 4.875C3.10072 4.875 2.14932 5.82639 2.14932 7C2.14932 8.1736 3.10072 9.125 4.27432 9.125C5.44793 9.125 6.39932 8.1736 6.39932 7C6.39932 5.82639 5.44793 4.875 4.27432 4.875ZM8.10068 5.5C8.10068 5.15482 8.3805 4.875 8.72568 4.875H11.2257C11.5709 4.875 11.8507 5.15482 11.8507 5.5C11.8507 5.84518 11.5709 6.125 11.2257 6.125H8.72568C8.3805 6.125 8.10068 5.84518 8.10068 5.5ZM8.72568 7.875C8.3805 7.875 8.10068 8.15482 8.10068 8.5C8.10068 8.84518 8.3805 9.125 8.72568 9.125H11.2257C11.5709 9.125 11.8507 8.84518 11.8507 8.5C11.8507 8.15482 11.5709 7.875 11.2257 7.875H8.72568Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"business-handshake\", \"keywords\": [ \"deal\", \"contract\", \"business\", \"money\", \"payment\", \"agreement\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.38503 5.24157L6.78925 3.97264C7.44263 3.38222 8.32243 3.10868 9.1954 3.22456L11.0529 3.47114L13.6312 2.07754C13.7977 1.98751 14 2.10812 14 2.29747V7.03562C14 7.35827 13.8444 7.6611 13.582 7.84886L11.8457 9.09128L9.96661 7.5146C10.3441 7.38073 10.6665 7.15996 10.9325 6.88409C11.1721 6.63561 11.1649 6.23994 10.9164 6.00035C10.668 5.76075 10.2723 5.76795 10.0327 6.01643C9.84672 6.20931 9.63177 6.33321 9.38026 6.38245C9.16109 6.42536 8.88202 6.41769 8.5311 6.31013L8.08453 5.93543L6.84003 6.94259C6.35114 7.33824 5.63235 7.25369 5.24862 6.75538C4.89215 6.29248 4.95155 5.63329 5.38503 5.24157ZM4.54695 4.31412L5.55522 3.40299C5.12716 3.2862 4.67363 3.26227 4.22557 3.33985L2.39001 3.65766L0.37647 2.57401C0.2063 2.48243 0 2.60568 0 2.79893L2.05134e-05 7.03519C2.21072e-05 7.37306 0.167057 7.68907 0.44623 7.87938L5.90115 11.5979C6.60732 12.0793 7.53849 12.0704 8.23529 11.5755L10.7409 9.79602L8.07017 7.55509L7.62639 7.91424C6.59167 8.75162 5.07039 8.57267 4.25824 7.51803C3.50379 6.53832 3.6295 5.14318 4.54695 4.31412Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"business-idea-money\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.45407 0.643678C8.67646 0.205623 7.79577 -0.0163054 6.90343 0.000932944C6.01109 0.0181713 5.13963 0.273948 4.37951 0.741709C3.6194 1.20947 2.99835 1.87216 2.58083 2.66099C2.16332 3.44982 1.96456 4.33603 2.00518 5.22761C2.04581 6.11919 2.32435 6.98365 2.81188 7.73123C3.2419 8.39064 3.82069 8.93791 4.49989 9.33007V10.5311C4.49989 10.7963 4.60525 11.0506 4.79279 11.2382C4.98032 11.4257 5.23468 11.5311 5.49989 11.5311H8.49989C8.76511 11.5311 9.01946 11.4257 9.207 11.2382C9.39454 11.0506 9.49989 10.7963 9.49989 10.5311V9.33094C10.2087 8.92388 10.808 8.34766 11.243 7.65174C11.7338 6.86646 11.9959 5.95988 11.9999 5.03385C12.0059 4.1416 11.7731 3.26398 11.3255 2.49206C10.8779 1.71995 10.2317 1.08173 9.45407 0.643678ZM7.00008 1.72559C7.34526 1.72559 7.62508 2.00541 7.62508 2.35059V2.73549C7.9804 2.80212 8.29468 2.98571 8.52547 3.24357C8.66346 3.39773 8.77248 3.57925 8.84327 3.77956C8.9583 4.105 8.78773 4.46208 8.46228 4.57711C8.13683 4.69214 7.77975 4.52156 7.66472 4.19611C7.64927 4.15239 7.6252 4.11201 7.59405 4.07721C7.52772 4.0031 7.43332 3.95794 7.32755 3.95794H6.56718C6.42808 3.95794 6.31531 4.07071 6.31531 4.20981C6.31531 4.32818 6.39773 4.43057 6.51336 4.45587L7.67103 4.70911C8.40916 4.87057 8.9349 5.52448 8.9349 6.27961C8.9349 7.06543 8.37084 7.72021 7.62508 7.85983V8.24469C7.62508 8.58987 7.34526 8.86969 7.00008 8.86969C6.6549 8.86969 6.37508 8.58987 6.37508 8.24469V7.85984C5.80715 7.75355 5.34531 7.34867 5.15693 6.8157C5.0419 6.49025 5.21248 6.13318 5.53793 6.01815C5.86337 5.90312 6.22045 6.0737 6.33548 6.39914C6.38483 6.53878 6.51804 6.63732 6.67265 6.63732H7.32755C7.52461 6.63732 7.6849 6.47727 7.6849 6.27961C7.6849 6.11146 7.56775 5.96607 7.40391 5.93023L6.24624 5.67699C5.55675 5.52617 5.06531 4.9156 5.06531 4.20981C5.06531 3.44543 5.63634 2.81443 6.37508 2.72011V2.35059C6.37508 2.00541 6.6549 1.72559 7.00008 1.72559ZM5 12.4912C4.58579 12.4912 4.25 12.827 4.25 13.2412C4.25 13.6554 4.58579 13.9912 5 13.9912H9C9.41421 13.9912 9.75 13.6554 9.75 13.2412C9.75 12.827 9.41421 12.4912 9 12.4912H5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"business-profession-home-office\", \"keywords\": [ \"workspace\", \"home\", \"office\", \"work\", \"business\", \"remote\", \"working\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188134)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.3222 0.445089C6.70517 0.0918685 7.29518 0.0918709 7.67815 0.445099L13.5202 5.83336C13.8281 6.11898 14.0002 6.5254 14.0002 6.93998V12.5C14.0002 12.8978 13.8421 13.2793 13.5608 13.5606C13.2795 13.8419 12.898 14 12.5002 14H1.5C1.10217 14 0.720644 13.8419 0.43934 13.5606C0.158036 13.2793 0 12.8978 0 12.5V6.93998C0 6.48579 0.178189 6.11326 0.480011 5.83336L0.481012 5.83243L6.3222 0.445089ZM6.56543 5.43018C6.28929 5.43018 6.06543 5.65403 6.06543 5.93018V6.33521H7.9357V5.93018C7.9357 5.65403 7.71184 5.43018 7.4357 5.43018H6.56543ZM5.06543 5.93018V6.33521H4.88037C4.32809 6.33521 3.88037 6.78292 3.88037 7.3352V10.4302C3.88037 10.9825 4.32809 11.4302 4.88037 11.4302H9.11822C9.67051 11.4302 10.1182 10.9825 10.1182 10.4302V7.33521C10.1182 6.78292 9.67051 6.33521 9.11822 6.33521H8.9357V5.93018C8.9357 5.10175 8.26412 4.43018 7.4357 4.43018H6.56543C5.737 4.43018 5.06543 5.10175 5.06543 5.93018Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188134\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"business-progress-bar-2\", \"keywords\": [ \"business\", \"production\", \"arrow\", \"workflow\", \"money\", \"flag\", \"timeline\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188128)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.75045 1.21777C4.75045 0.80356 5.08624 0.467773 5.50045 0.467773L5.5034 0.467779C5.63938 0.468302 5.76682 0.50501 5.8766 0.568774L10.3647 3.06216C10.6028 3.19443 10.7505 3.4454 10.7505 3.71777C10.7505 3.99015 10.6028 4.24111 10.3647 4.37339L6.25045 6.65908V9.36315C6.7518 9.56609 7.15213 9.96642 7.35508 10.4678H10.8498V9.65308C10.8498 9.34973 11.0325 9.07625 11.3128 8.96017C11.593 8.84408 11.9156 8.90825 12.1301 9.12275L13.6948 10.6874C13.7644 10.7571 13.8175 10.837 13.854 10.9223C13.8929 11.0129 13.9145 11.1128 13.9145 11.2178C13.9145 11.3276 13.8909 11.4318 13.8485 11.5258C13.819 11.5914 13.7796 11.6535 13.7303 11.71C13.7177 11.7245 13.7046 11.7385 13.691 11.7519L12.1301 13.3128C11.9156 13.5273 11.593 13.5914 11.3128 13.4754C11.0325 13.3593 10.8498 13.0858 10.8498 12.7824V11.9678H7.35508C7.05838 12.7007 6.3398 13.2178 5.50045 13.2178C4.66111 13.2178 3.94253 12.7007 3.64583 11.9678H0.835937C0.421724 11.9678 0.0859375 11.632 0.0859375 11.2178C0.0859375 10.8036 0.421724 10.4678 0.835937 10.4678H3.64583C3.84878 9.96642 4.2491 9.56609 4.75045 9.36315V6.21777V1.21777Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188128\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"business-user-curriculum\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.43934 0.43934C1.72064 0.158035 2.10218 0 2.5 0H8.5C8.63261 0 8.75979 0.0526784 8.85355 0.146447L12.8536 4.14645C12.9473 4.24021 13 4.36739 13 4.5V12.5C13 12.8978 12.842 13.2794 12.5607 13.5607C12.2794 13.842 11.8978 14 11.5 14H2.5C2.10217 14 1.72064 13.842 1.43934 13.5607C1.15804 13.2794 1 12.8978 1 12.5V1.5C1 1.10218 1.15804 0.720644 1.43934 0.43934ZM4.9558 3.95126C5.52153 3.95126 5.98014 3.49265 5.98014 2.92692C5.98014 2.3612 5.52153 1.90259 4.9558 1.90259C4.39008 1.90259 3.93147 2.3612 3.93147 2.92692C3.93147 3.49265 4.39008 3.95126 4.9558 3.95126ZM2.87579 8.49996C2.87528 8.15478 3.15468 7.87496 3.49986 7.87496H10.4999C10.845 7.87496 11.1253 8.15478 11.1258 8.49996C11.1263 8.84513 10.8469 9.12496 10.5017 9.12495H3.50173C3.15656 9.12495 2.87631 8.84513 2.87579 8.49996ZM2.87579 11.5C2.87528 11.1548 3.15468 10.875 3.49986 10.875H7.49986C7.84504 10.875 8.12528 11.1548 8.1258 11.5C8.12632 11.8451 7.84692 12.125 7.50174 12.125H3.50173C3.15656 12.125 2.87631 11.8451 2.87579 11.5ZM4.95519 4.63403C4.25536 4.63403 3.61701 4.89721 3.13369 5.32999C2.8468 5.58687 3.07084 5.99981 3.45593 5.99981H6.45445C6.83954 5.99981 7.06357 5.58687 6.77669 5.32999C6.29337 4.89721 5.65501 4.63403 4.95519 4.63403Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"calculator-1\", \"keywords\": [ \"shop\", \"shopping\", \"pay\", \"payment\", \"store\", \"cash\", \"calculate\", \"math\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.56836 1.74841C2.56836 1.6257 2.66384 1.54358 2.76038 1.54358H11.2386C11.3351 1.54358 11.4306 1.6257 11.4306 1.74841V5.16354H2.56836V1.74841ZM2.76038 0.0435791C1.81639 0.0435791 1.06836 0.816436 1.06836 1.74841V12.2515C1.06836 13.1835 1.81639 13.9563 2.76038 13.9563H11.2386C12.1826 13.9563 12.9306 13.1835 12.9306 12.2515V1.74841C12.9306 0.816436 12.1826 0.0435791 11.2386 0.0435791H2.76038ZM3.82855 8.71918C4.24276 8.71918 4.57855 8.38339 4.57855 7.96918C4.57855 7.55496 4.24276 7.21918 3.82855 7.21918C3.41434 7.21918 3.07855 7.55496 3.07855 7.96918C3.07855 8.38339 3.41434 8.71918 3.82855 8.71918ZM6.99949 8.71918C7.4137 8.71918 7.74949 8.38339 7.74949 7.96918C7.74949 7.55496 7.4137 7.21918 6.99949 7.21918C6.58527 7.21918 6.24949 7.55496 6.24949 7.96918C6.24949 8.38339 6.58527 8.71918 6.99949 8.71918ZM10.9204 7.96918C10.9204 8.38339 10.5846 8.71918 10.1704 8.71918C9.75621 8.71918 9.42042 8.38339 9.42042 7.96918C9.42042 7.55496 9.75621 7.21918 10.1704 7.21918C10.5846 7.21918 10.9204 7.55496 10.9204 7.96918ZM3.82855 11.9444C4.24276 11.9444 4.57855 11.6086 4.57855 11.1944C4.57855 10.7802 4.24276 10.4444 3.82855 10.4444C3.41434 10.4444 3.07855 10.7802 3.07855 11.1944C3.07855 11.6086 3.41434 11.9444 3.82855 11.9444ZM7.74949 11.1944C7.74949 11.6086 7.4137 11.9444 6.99949 11.9444C6.58527 11.9444 6.24949 11.6086 6.24949 11.1944C6.24949 10.7802 6.58527 10.4444 6.99949 10.4444C7.4137 10.4444 7.74949 10.7802 7.74949 11.1944ZM10.1704 11.9444C10.5846 11.9444 10.9204 11.6086 10.9204 11.1944C10.9204 10.7802 10.5846 10.4444 10.1704 10.4444C9.75621 10.4444 9.42042 10.7802 9.42042 11.1944C9.42042 11.6086 9.75621 11.9444 10.1704 11.9444ZM8.46362 2.8017C8.11844 2.8017 7.83862 3.08152 7.83862 3.4267C7.83862 3.77187 8.11844 4.0517 8.46362 4.0517H9.88208C10.2273 4.0517 10.5071 3.77187 10.5071 3.4267C10.5071 3.08152 10.2273 2.8017 9.88208 2.8017H8.46362Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"calculator-2\", \"keywords\": [ \"shop\", \"shopping\", \"pay\", \"payment\", \"store\", \"cash\", \"calculate\", \"math\", \"sign\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188155)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.375 0H2.5C1.11929 0 0 1.11929 0 2.5V6.375H6.375V0ZM0 11.5V7.625H6.375V14H2.5C1.11929 14 0 12.8807 0 11.5ZM7.625 14V7.625H14V11.5C14 12.8807 12.8807 14 11.5 14H7.625ZM14 2.5V6.375H7.625V0H11.5C12.8807 0 14 1.11929 14 2.5ZM2.03281 2.72456C1.68763 2.72456 1.40781 3.00439 1.40781 3.34956C1.40781 3.69474 1.68763 3.97456 2.03281 3.97456H2.65781V4.59956C2.65781 4.94474 2.93763 5.22456 3.28281 5.22456C3.62798 5.22456 3.90781 4.94474 3.90781 4.59956V3.97456H4.53281C4.87798 3.97456 5.15781 3.69474 5.15781 3.34956C5.15781 3.00439 4.87798 2.72456 4.53281 2.72456H3.90781V2.09956C3.90781 1.75439 3.62798 1.47456 3.28281 1.47456C2.93763 1.47456 2.65781 1.75439 2.65781 2.09956V2.72456H2.03281ZM9.02545 3.34961C9.02545 3.00443 9.30527 2.72461 9.65045 2.72461H11.6505C11.9956 2.72461 12.2755 3.00443 12.2755 3.34961C12.2755 3.69479 11.9956 3.97461 11.6505 3.97461H9.65045C9.30527 3.97461 9.02545 3.69479 9.02545 3.34961ZM9.65039 9.03223C9.30521 9.03223 9.02539 9.31205 9.02539 9.65723C9.02539 10.0024 9.30521 10.2822 9.65039 10.2822H11.6504C11.9956 10.2822 12.2754 10.0024 12.2754 9.65723C12.2754 9.31205 11.9956 9.03223 11.6504 9.03223H9.65039ZM9.02539 11.6572C9.02539 11.312 9.30521 11.0322 9.65039 11.0322H11.6504C11.9956 11.0322 12.2754 11.312 12.2754 11.6572C12.2754 12.0024 11.9956 12.2822 11.6504 12.2822H9.65039C9.30521 12.2822 9.02539 12.0024 9.02539 11.6572ZM1.78071 9.21518C2.02479 8.9711 2.42052 8.9711 2.6646 9.21518L3.28266 9.83324L3.90071 9.21518C4.14479 8.9711 4.54052 8.9711 4.7846 9.21518C5.02868 9.45926 5.02868 9.85498 4.7846 10.0991L4.16654 10.7171L4.7846 11.3352C5.02868 11.5793 5.02868 11.975 4.7846 12.2191C4.54052 12.4631 4.14479 12.4631 3.90071 12.2191L3.28266 11.601L2.6646 12.2191C2.42052 12.4631 2.02479 12.4631 1.78071 12.2191C1.53664 11.975 1.53664 11.5793 1.78071 11.3352L2.39877 10.7171L1.78071 10.0991C1.53664 9.85498 1.53664 9.45926 1.78071 9.21518Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188155\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cane\", \"keywords\": [ \"walking\", \"stick\", \"cane\", \"accessories\", \"gentleman\", \"accessories\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.46875 2.5C5.91647 2.5 5.46875 2.94772 5.46875 3.5V3.75C5.46875 4.44036 4.90911 5 4.21875 5C3.52839 5 2.96875 4.44036 2.96875 3.75V3.5C2.96875 1.567 4.53575 0 6.46875 0C8.40175 0 9.96875 1.567 9.96875 3.5V12.75C9.96875 13.4404 9.40911 14 8.71875 14C8.02839 14 7.46875 13.4404 7.46875 12.75V3.5C7.46875 2.94772 7.02103 2.5 6.46875 2.5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"chair\", \"keywords\": [ \"chair\", \"business\", \"product\", \"comfort\", \"decoration\", \"sit\", \"furniture\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.53301 0.907227C3.53301 0.493013 3.19723 0.157227 2.78301 0.157227C2.3688 0.157227 2.03301 0.493013 2.03301 0.907227V7.18713H1.8457C1.43148 7.18713 1.0957 7.52292 1.0957 7.93713C1.0957 8.35135 1.43148 8.68713 1.8457 8.68713H2.55007L5.67712 10.837L3.29543 12.4744C2.9541 12.7091 2.86764 13.176 3.1023 13.5173C3.33696 13.8587 3.8039 13.9451 4.14523 13.7105L7.00097 11.7471L9.85672 13.7105C10.1981 13.9451 10.665 13.8587 10.8997 13.5173C11.1343 13.176 11.0479 12.7091 10.7065 12.4744L8.32483 10.837L11.4519 8.68713H12.1563C12.5705 8.68713 12.9063 8.35135 12.9063 7.93713C12.9063 7.52292 12.5705 7.18713 12.1563 7.18713H11.969V0.907227C11.969 0.493013 11.6332 0.157227 11.219 0.157227C10.8048 0.157227 10.469 0.493013 10.469 0.907227H3.53301ZM8.80417 8.68713H5.19777L7.00097 9.92683L8.80417 8.68713ZM3.53301 5.14435V7.18713H10.469V5.14435H3.53301Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"closet\", \"keywords\": [ \"closet\", \"dressing\", \"dresser\", \"product\", \"decoration\", \"cloth\", \"clothing\", \"cabinet\", \"furniture\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.375 0H2.5C1.67157 0 1 0.671573 1 1.5V12.5C1 13.3284 1.67157 14 2.5 14H6.375V0ZM7.625 14H11.5C12.3284 14 13 13.3284 13 12.5V1.5C13 0.671573 12.3284 0 11.5 0H7.625V14ZM9.5 5.83655C9.84518 5.83655 10.125 6.11637 10.125 6.46155V7.53847C10.125 7.88365 9.84518 8.16347 9.5 8.16347C9.15482 8.16347 8.875 7.88365 8.875 7.53847V6.46155C8.875 6.11637 9.15482 5.83655 9.5 5.83655ZM5.125 6.46155C5.125 6.11637 4.84518 5.83655 4.5 5.83655C4.15482 5.83655 3.875 6.11637 3.875 6.46155V7.53847C3.875 7.88365 4.15482 8.16347 4.5 8.16347C4.84518 8.16347 5.125 7.88365 5.125 7.53847V6.46155Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"coin-share\", \"keywords\": [ \"payment\", \"cash\", \"money\", \"finance\", \"receive\", \"give\", \"coin\", \"hand\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188131)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.75 8.25C11.9591 8.25 13.75 6.45914 13.75 4.25C13.75 2.04086 11.9591 0.25 9.75 0.25C7.54086 0.25 5.75 2.04086 5.75 4.25C5.75 6.45914 7.54086 8.25 9.75 8.25ZM9.75 2.625C10.0952 2.625 10.375 2.90482 10.375 3.25V5.25C10.375 5.59518 10.0952 5.875 9.75 5.875C9.40482 5.875 9.125 5.59518 9.125 5.25V3.25C9.125 2.90482 9.40482 2.625 9.75 2.625ZM2.09315 6.75H0.25V10.75L2.07843 12.5784C2.82857 13.3286 3.84599 13.75 4.90685 13.75H10.75C11.5784 13.75 12.25 13.0784 12.25 12.25C12.25 11.4216 11.5784 10.75 10.75 10.75H7.97314C7.87662 11.0765 7.69953 11.3842 7.44188 11.6418C6.61791 12.4658 5.28199 12.4658 4.45802 11.6418L3.00801 10.1918C2.76393 9.94774 2.76393 9.55201 3.00801 9.30794C3.25209 9.06386 3.64782 9.06386 3.89189 9.30794L5.34191 10.7579C5.67772 11.0938 6.22218 11.0938 6.558 10.7579C6.85999 10.456 6.89041 9.98522 6.64924 9.64924L4.92157 7.92157C4.17143 7.17143 3.15401 6.75 2.09315 6.75Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188131\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"coins-stack\", \"keywords\": [ \"accounting\", \"billing\", \"payment\", \"stack\", \"cash\", \"coins\", \"currency\", \"money\", \"finance\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188158)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.123047 5.58289V7.28567C0.123047 8.25418 1.67347 9.06764 3.75049 9.24829V6.71436C3.75049 6.67392 3.7516 6.63392 3.75377 6.59437C2.59937 6.51198 1.55082 6.26704 0.727785 5.89984C0.514354 5.80462 0.311333 5.69905 0.123047 5.58289ZM9.12305 2V3.47054C7.88729 3.51118 6.72657 3.75088 5.81084 4.15788C5.30819 4.38127 4.82404 4.67966 4.44863 5.07413C4.363 5.16411 4.28065 5.26192 4.2044 5.36723C3.01764 5.32257 1.98131 5.09034 1.23709 4.7583C0.821075 4.5727 0.523622 4.36809 0.338569 4.17283C0.210034 4.0372 0.148256 3.92085 0.123047 3.82491V2C0.123047 0.89543 2.13777 0 4.62305 0C7.10833 0 9.12305 0.89543 9.12305 2ZM4.62292 2.9313C6.35831 2.9313 7.76512 2.52673 7.76512 2.02766C7.76512 1.5286 6.35831 1.12402 4.62292 1.12402C2.88753 1.12402 1.48071 1.5286 1.48071 2.02766C1.48071 2.52673 2.88753 2.9313 4.62292 2.9313ZM5.00049 12V10.2972C5.18878 10.4134 5.3918 10.519 5.60523 10.6142C6.63069 11.0717 8.00622 11.3394 9.5004 11.3394C10.9946 11.3394 12.3701 11.0717 13.3956 10.6142C13.6091 10.5189 13.8122 10.4133 14.0005 10.2971V12C14.0005 13.1 12.0005 14 9.50049 14C7.00049 14 5.00049 13.1 5.00049 12ZM5.21601 8.88718C5.08748 8.75156 5.0257 8.6352 5.00049 8.53926V6.71436C5.00049 5.60979 7.01521 4.71436 9.50049 4.71436C11.9858 4.71436 14.0005 5.60979 14.0005 6.71436V8.53857C13.9754 8.63465 13.9136 8.75123 13.7848 8.88718C13.5997 9.08245 13.3023 9.28705 12.8863 9.47266C12.0558 9.84319 10.8615 10.0894 9.5004 10.0894C8.13927 10.0894 6.94502 9.84319 6.11453 9.47266C5.69852 9.28705 5.40106 9.08245 5.21601 8.88718ZM12.6426 6.74202C12.6426 7.24108 11.2358 7.64565 9.50036 7.64565C7.76497 7.64565 6.35816 7.24108 6.35816 6.74202C6.35816 6.24295 7.76497 5.83838 9.50036 5.83838C11.2358 5.83838 12.6426 6.24295 12.6426 6.74202Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188158\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"credit-card-1\", \"keywords\": [ \"credit\", \"pay\", \"payment\", \"debit\", \"card\", \"finance\", \"plastic\", \"money\", \"atm\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 1.75C0.671573 1.75 0 2.42157 0 3.25V4.18994H14V3.25C14 2.42157 13.3284 1.75 12.5 1.75H1.5ZM0 10.75V5.43994H14V10.75C14 11.5784 13.3284 12.25 12.5 12.25H1.5C0.671573 12.25 0 11.5784 0 10.75ZM9.5 8.625C9.15482 8.625 8.875 8.90482 8.875 9.25C8.875 9.59518 9.15482 9.875 9.5 9.875H11C11.3452 9.875 11.625 9.59518 11.625 9.25C11.625 8.90482 11.3452 8.625 11 8.625H9.5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"credit-card-2\", \"keywords\": [ \"deposit\", \"payment\", \"finance\", \"atm\", \"withdraw\", \"atm\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188170)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.367127 0.378289C0.553058 0.190096 0.806528 0.083252 1.07212 0.083252H12.9279C13.1935 0.083252 13.4469 0.190096 13.6329 0.378289C13.8186 0.566279 13.9219 0.819975 13.9219 1.08325V4.08325C13.9219 4.34653 13.8186 4.60022 13.6329 4.78821C13.4469 4.97641 13.1935 5.08325 12.9279 5.08325H12.0943H12V3.5C12 2.25736 10.9926 1.25 9.75 1.25H4.25C3.00736 1.25 2 2.25736 2 3.5V5.08325H1.90572H1.07212C0.806528 5.08325 0.553058 4.97641 0.367127 4.78821C0.181398 4.60023 0.078125 4.34653 0.078125 4.08325V1.08325C0.078125 0.819973 0.181398 0.566278 0.367127 0.378289Z\\\" fill=\\\"black\\\"/>\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.25 3.5C3.25 2.94772 3.69772 2.5 4.25 2.5H9.75C10.3023 2.5 10.75 2.94772 10.75 3.5V12.875C10.75 13.4273 10.3023 13.875 9.75 13.875H4.25C3.69772 13.875 3.25 13.4273 3.25 12.875V3.5ZM6.25 4.83325C6.25 4.41904 6.58579 4.08325 7 4.08325C7.41409 4.08325 7.75005 4.41892 7.75005 4.83325C7.75005 5.24758 7.41409 5.58325 7 5.58325C6.58579 5.58325 6.25 5.24747 6.25 4.83325ZM5.25 8.1875C5.25 7.221 6.0335 6.4375 7 6.4375C7.9665 6.4375 8.75 7.221 8.75 8.1875C8.75 9.154 7.9665 9.9375 7 9.9375C6.0335 9.9375 5.25 9.154 5.25 8.1875ZM6.99995 10.8468C6.58574 10.8468 6.24995 11.1826 6.24995 11.5968C6.24995 12.011 6.58574 12.3468 6.99995 12.3468C7.41405 12.3468 7.75 12.0111 7.75 11.5968C7.75 11.1825 7.41405 10.8468 6.99995 10.8468Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188170\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"diamond-2\", \"keywords\": [ \"diamond\", \"money\", \"payment\", \"finance\", \"wealth\", \"jewelry\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.40828 1.03998H7.62112L9.33823 4.73074H4.68411L6.40828 1.03998ZM3.30443 4.73074L5.02861 1.03998H3.36035L3.34663 1.04017C3.10036 1.04693 2.85915 1.11155 2.64248 1.22881C2.42737 1.34522 2.24245 1.51028 2.10245 1.71078L0.28501 4.22724L0.282206 4.23116C0.174444 4.38345 0.0958199 4.55281 0.0488281 4.73074H3.30443ZM0.254083 5.98074H3.27821L5.19975 12.1411L0.365164 6.12886C0.325057 6.08155 0.28799 6.03208 0.254083 5.98074ZM4.58761 5.98074H9.43421L7.10359 13.4833C7.06929 13.4857 7.03486 13.4869 7.00035 13.4869C6.97632 13.4869 6.95233 13.4863 6.92838 13.4851L4.58761 5.98074ZM10.7431 5.98074L8.84741 12.0833L13.6355 6.12887C13.6756 6.08156 13.7127 6.03208 13.7466 5.98074H10.7431ZM13.9518 4.73074C13.9048 4.55281 13.8262 4.38345 13.7185 4.23116L13.7157 4.22724L11.8982 1.71078C11.7582 1.51027 11.5733 1.34522 11.3582 1.22881C11.1415 1.11155 10.9003 1.04693 10.654 1.04017L10.6403 1.03998H8.99978L10.7168 4.73074H13.9518Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"discount-percent-badge\", \"keywords\": [ \"shop\", \"shops\", \"stores\", \"discount\", \"coupon\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188180)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.51444 0.610367C6.39481 -0.1058 7.65694 -0.1058 8.53732 0.610368L8.69637 0.739756L8.89881 0.70723C10.0193 0.527197 11.1124 1.15826 11.5167 2.21867L11.5898 2.41025L11.7813 2.4833C12.8417 2.88764 13.4728 3.98068 13.2928 5.10119L13.2602 5.30363L13.3896 5.46268C14.1058 6.34305 14.1058 7.60519 13.3896 8.48556L13.2602 8.64461L13.2928 8.84705C13.4728 9.96756 12.8417 11.0606 11.7813 11.4649L11.5898 11.538L11.5167 11.7296C11.1124 12.79 10.0193 13.421 8.89881 13.241L8.69637 13.2085L8.53732 13.3379C7.65694 14.054 6.39481 14.054 5.51444 13.3379L5.35538 13.2085L5.15295 13.241C4.03244 13.421 2.9394 12.79 2.53506 11.7296L2.46201 11.538L2.27043 11.4649C1.21002 11.0606 0.578955 9.96756 0.758988 8.84705L0.791514 8.64461L0.662125 8.48556C-0.0540417 7.60519 -0.0540416 6.34305 0.662125 5.46268L0.791514 5.30363L0.758988 5.10119C0.578955 3.98068 1.21002 2.88764 2.27043 2.4833L2.46201 2.41025L2.53506 2.21867C2.9394 1.15826 4.03244 0.527197 5.15295 0.70723L5.35539 0.739756L5.51444 0.610367ZM9.96782 4.03218C9.72374 3.7881 9.32801 3.7881 9.08394 4.03218L4.08394 9.03218C3.83986 9.27626 3.83986 9.67199 4.08394 9.91606C4.32801 10.1601 4.72374 10.1601 4.96782 9.91606L9.96782 4.91606C10.2119 4.67199 10.2119 4.27626 9.96782 4.03218ZM5.02588 3.97412C4.47359 3.97412 4.02588 4.42184 4.02588 4.97412C4.02588 5.52641 4.47359 5.97412 5.02588 5.97412C5.57816 5.97412 6.02588 5.52641 6.02588 4.97412C6.02588 4.42184 5.57816 3.97412 5.02588 3.97412ZM9.02588 7.97412C8.47359 7.97412 8.02588 8.42184 8.02588 8.97412C8.02588 9.52641 8.47359 9.97412 9.02588 9.97412C9.57816 9.97412 10.0259 9.52641 10.0259 8.97412C10.0259 8.42184 9.57816 7.97412 9.02588 7.97412Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188180\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"discount-percent-circle\", \"keywords\": [ \"store\", \"shop\", \"shops\", \"stores\", \"discount\", \"coupon\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188198)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7ZM10.1877 3.81228C9.94362 3.56821 9.5479 3.56821 9.30382 3.81228L3.81227 9.30383C3.56819 9.54791 3.56819 9.94364 3.81227 10.1877C4.05635 10.4318 4.45208 10.4318 4.69615 10.1877L10.1877 4.69617C10.4318 4.45209 10.4318 4.05636 10.1877 3.81228ZM4.80337 3.62923C4.1549 3.62923 3.62921 4.15491 3.62921 4.80338C3.62921 5.45185 4.1549 5.97754 4.80337 5.97754C5.45183 5.97754 5.97752 5.45185 5.97752 4.80338C5.97752 4.15491 5.45183 3.62923 4.80337 3.62923ZM9.19662 8.02246C8.54815 8.02246 8.02246 8.54815 8.02246 9.19662C8.02246 9.84508 8.54815 10.3708 9.19662 10.3708C9.84508 10.3708 10.3708 9.84508 10.3708 9.19662C10.3708 8.54815 9.84508 8.02246 9.19662 8.02246Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188198\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"discount-percent-coupon\", \"keywords\": [ \"shop\", \"shops\", \"stores\", \"discount\", \"coupon\", \"voucher\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 11C0 11.8284 0.670533 12.5 1.49768 12.5H12.5023C13.3295 12.5 14 11.8284 14 11V8.96597C14 8.74021 13.8489 8.54245 13.6314 8.48339C12.979 8.30629 12.4997 7.70856 12.4997 7C12.4997 6.29144 12.979 5.69371 13.6314 5.51661C13.8489 5.45755 14 5.25979 14 5.03403V3C14 2.17157 13.3295 1.5 12.5023 1.5H1.49768C0.670535 1.5 0 2.17157 0 3V5.02963C0 5.25697 0.153127 5.45569 0.372701 5.51331C1.03138 5.68615 1.51674 6.28694 1.51674 7C1.51674 7.71306 1.03138 8.31385 0.372701 8.48669C0.153127 8.54431 0 8.74303 0 8.97037V11ZM4.96245 9.94194L9.96245 4.94194C10.2065 4.69786 10.2065 4.30214 9.96245 4.05806C9.71837 3.81398 9.32264 3.81398 9.07857 4.05806L4.07857 9.05806C3.83449 9.30214 3.83449 9.69786 4.07857 9.94194C4.32264 10.186 4.71837 10.186 4.96245 9.94194ZM4.02051 5C4.02051 4.44772 4.46822 4 5.02051 4C5.57279 4 6.02051 4.44772 6.02051 5C6.02051 5.55228 5.57279 6 5.02051 6C4.46822 6 4.02051 5.55228 4.02051 5ZM8.02051 9C8.02051 8.44772 8.46822 8 9.02051 8C9.57279 8 10.0205 8.44772 10.0205 9C10.0205 9.55228 9.57279 10 9.02051 10C8.46822 10 8.02051 9.55228 8.02051 9Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"discount-percent-cutout\", \"keywords\": [ \"store\", \"shop\", \"shops\", \"stores\", \"discount\", \"coupon\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188192)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.61978 1.61966C1.65771 1.58173 1.70914 1.56042 1.76278 1.56042C2.17699 1.56042 2.51278 1.22464 2.51278 0.810425C2.51278 0.396211 2.17699 0.0604248 1.76278 0.0604248C1.31132 0.0604248 0.87835 0.239767 0.55912 0.558998C0.239889 0.878228 0.0605469 1.3112 0.0605469 1.76266C0.0605469 2.17687 0.396333 2.51266 0.810547 2.51266C1.22476 2.51266 1.56055 2.17687 1.56055 1.76266C1.56055 1.70902 1.58185 1.65758 1.61978 1.61966ZM11.4874 0.810425C11.4874 0.396211 11.8232 0.0604248 12.2374 0.0604248C12.6888 0.0604248 13.1218 0.239767 13.441 0.558998C13.7603 0.878228 13.9396 1.3112 13.9396 1.76266C13.9396 2.17687 13.6038 2.51266 13.1896 2.51266C12.7754 2.51266 12.4396 2.17687 12.4396 1.76266C12.4396 1.70902 12.4183 1.65758 12.3804 1.61966C12.3424 1.58173 12.291 1.56042 12.2374 1.56042C11.8232 1.56042 11.4874 1.22464 11.4874 0.810425ZM4.82538 3.64033C4.17095 3.64033 3.64044 4.17084 3.64044 4.82526C3.64044 5.47969 4.17095 6.0102 4.82538 6.0102C5.4798 6.0102 6.01031 5.47969 6.01031 4.82526C6.01031 4.17084 5.4798 3.64033 4.82538 3.64033ZM9.91098 5.14969L5.14981 9.91086C4.85691 10.2038 4.38204 10.2038 4.08915 9.91086C3.79625 9.61797 3.79625 9.1431 4.08915 8.8502L8.85032 4.08903C9.14321 3.79614 9.61808 3.79614 9.91098 4.08903C10.2039 4.38193 10.2039 4.8568 9.91098 5.14969ZM9.17474 7.98969C8.52032 7.98969 7.98981 8.5202 7.98981 9.17462C7.98981 9.82904 8.52032 10.3596 9.17474 10.3596C9.82916 10.3596 10.3597 9.82904 10.3597 9.17462C10.3597 8.5202 9.82916 7.98969 9.17474 7.98969ZM0.810547 11.4872C1.22476 11.4872 1.56055 11.823 1.56055 12.2372C1.56055 12.2909 1.58185 12.3423 1.61978 12.3802C1.65771 12.4182 1.70914 12.4395 1.76278 12.4395C2.17699 12.4395 2.51278 12.7753 2.51278 13.1895C2.51278 13.6037 2.17699 13.9395 1.76278 13.9395C1.31132 13.9395 0.87835 13.7601 0.55912 13.4409C0.239889 13.1217 0.0605469 12.6887 0.0605469 12.2372C0.0605469 11.823 0.396333 11.4872 0.810547 11.4872ZM13.9396 12.2372C13.9396 11.823 13.6038 11.4872 13.1896 11.4872C12.7754 11.4872 12.4396 11.823 12.4396 12.2372C12.4396 12.2909 12.4183 12.3423 12.3804 12.3802C12.3424 12.4182 12.291 12.4395 12.2374 12.4395C11.8232 12.4395 11.4874 12.7753 11.4874 13.1895C11.4874 13.6037 11.8232 13.9395 12.2374 13.9395C12.6888 13.9395 13.1218 13.7601 13.441 13.4409C13.7603 13.1217 13.9396 12.6887 13.9396 12.2372ZM3.86948 0.810425C3.86948 0.396211 4.20526 0.0604248 4.61948 0.0604248H5.57171C5.98592 0.0604248 6.32171 0.396211 6.32171 0.810425C6.32171 1.22464 5.98592 1.56042 5.57171 1.56042H4.61948C4.20526 1.56042 3.86948 1.22464 3.86948 0.810425ZM8.4284 0.0604248C8.01419 0.0604248 7.6784 0.396211 7.6784 0.810425C7.6784 1.22464 8.01419 1.56042 8.4284 1.56042H9.38064C9.79485 1.56042 10.1307 1.22464 10.1307 0.810425C10.1307 0.396211 9.79485 0.0604248 9.38064 0.0604248H8.4284ZM3.86948 13.1895C3.86948 12.7753 4.20526 12.4395 4.61948 12.4395H5.57171C5.98592 12.4395 6.32171 12.7753 6.32171 13.1895C6.32171 13.6037 5.98592 13.9395 5.57171 13.9395H4.61948C4.20526 13.9395 3.86948 13.6037 3.86948 13.1895ZM8.4284 12.4395C8.01419 12.4395 7.6784 12.7753 7.6784 13.1895C7.6784 13.6037 8.01419 13.9395 8.4284 13.9395H9.38064C9.79485 13.9395 10.1307 13.6037 10.1307 13.1895C10.1307 12.7753 9.79485 12.4395 9.38064 12.4395H8.4284ZM13.1896 3.86936C13.6038 3.86936 13.9396 4.20515 13.9396 4.61936V5.5716C13.9396 5.98581 13.6038 6.3216 13.1896 6.3216C12.7754 6.3216 12.4396 5.98581 12.4396 5.5716V4.61936C12.4396 4.20515 12.7754 3.86936 13.1896 3.86936ZM13.9396 8.4283C13.9396 8.01409 13.6038 7.6783 13.1896 7.6783C12.7754 7.6783 12.4396 8.01409 12.4396 8.4283V9.38053C12.4396 9.79475 12.7754 10.1305 13.1896 10.1305C13.6038 10.1305 13.9396 9.79475 13.9396 9.38053V8.4283ZM0.810547 3.86936C1.22476 3.86936 1.56055 4.20515 1.56055 4.61936V5.5716C1.56055 5.98581 1.22476 6.3216 0.810547 6.3216C0.396333 6.3216 0.0605469 5.98581 0.0605469 5.5716V4.61936C0.0605469 4.20515 0.396333 3.86936 0.810547 3.86936ZM1.56055 8.4283C1.56055 8.01409 1.22476 7.6783 0.810547 7.6783C0.396333 7.6783 0.0605469 8.01409 0.0605469 8.4283V9.38053C0.0605469 9.79475 0.396333 10.1305 0.810547 10.1305C1.22476 10.1305 1.56055 9.79475 1.56055 9.38053V8.4283Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188192\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"discount-percent-fire\", \"keywords\": [ \"shop\", \"shops\", \"stores\", \"discount\", \"coupon\", \"hot\", \"trending\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.36497 0.2631C5.48972 0.121605 5.68349 0.0623634 5.86603 0.109908C6.97512 0.398767 8.67946 1.14923 10.1155 2.48749C11.5621 3.8357 12.7466 5.79294 12.7466 8.46237C12.7466 9.51873 12.5095 10.8115 11.6889 11.8833C10.8549 12.9727 9.47155 13.7639 7.32528 13.8963L7.31954 13.8967C4.45975 14.0402 2.45114 12.5544 1.64411 10.507C0.844509 8.4784 1.24598 5.95428 3.04851 4.03894C3.14493 3.93648 3.28006 3.87939 3.42073 3.88167C3.56141 3.88396 3.69461 3.9454 3.78766 4.05093L5.05376 5.48689C5.51662 4.66077 5.7418 3.94064 5.77574 3.22015C5.81026 2.48733 5.64894 1.71266 5.27575 0.779411C5.20572 0.604258 5.24021 0.404596 5.36497 0.2631ZM5.3147 7.86755C5.3147 7.45334 5.65048 7.11755 6.0647 7.11755C6.47891 7.11755 6.8147 7.45334 6.8147 7.86755C6.8147 8.28177 6.47891 8.61755 6.0647 8.61755C5.65048 8.61755 5.3147 8.28177 5.3147 7.86755ZM9.30388 7.994C9.49914 7.79873 9.49914 7.48215 9.30388 7.28689C9.10862 7.09163 8.79203 7.09163 8.59677 7.28689L5.31344 10.5702C5.11817 10.7655 5.11817 11.0821 5.31344 11.2773C5.5087 11.4726 5.82528 11.4726 6.02054 11.2773L9.30388 7.994ZM7.97363 10.6967C7.97363 10.2824 8.30942 9.94666 8.72363 9.94666C9.13784 9.94666 9.47363 10.2824 9.47363 10.6967C9.47363 11.1109 9.13784 11.4467 8.72363 11.4467C8.30942 11.4467 7.97363 11.1109 7.97363 10.6967Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"dollar-coin\", \"keywords\": [ \"accounting\", \"billing\", \"payment\", \"cash\", \"coin\", \"currency\", \"money\", \"finance\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188186)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7ZM7.74997 3.25C7.74997 2.83579 7.41418 2.5 6.99997 2.5C6.58576 2.5 6.24997 2.83579 6.24997 3.25V3.7605C5.31317 3.85991 4.58334 4.65262 4.58334 5.61578C4.58334 6.49259 5.19386 7.2511 6.05042 7.43847L7.52351 7.76071C7.75279 7.81086 7.91666 8.01429 7.91666 8.24952C7.91666 8.52601 7.69244 8.74997 7.41666 8.74997H6.58334C6.36681 8.74997 6.1806 8.61191 6.1116 8.41671C5.97357 8.02617 5.54507 7.82148 5.15454 7.95951C4.764 8.09755 4.55931 8.52604 4.69734 8.91657C4.93581 9.59128 5.52648 10.101 6.24997 10.2223V10.75C6.24997 11.1642 6.58576 11.5 6.99997 11.5C7.41418 11.5 7.74997 11.1642 7.74997 10.75V10.2223C8.69613 10.0635 9.41666 9.24023 9.41666 8.24952C9.41666 7.30991 8.76248 6.49626 7.84406 6.29536L6.37096 5.97312C6.20304 5.93638 6.08334 5.78768 6.08334 5.61578C6.08334 5.41377 6.24711 5.25 6.44913 5.25H7.41666C7.56467 5.25 7.69689 5.31336 7.78952 5.41685C7.83302 5.46545 7.86673 5.52195 7.8884 5.58326C8.02643 5.9738 8.45493 6.17849 8.84546 6.04046C9.236 5.90242 9.44069 5.47393 9.30266 5.08339C9.21458 4.83419 9.07893 4.60833 8.90722 4.41648C8.61206 4.0867 8.20721 3.85458 7.74997 3.77772V3.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188186\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"dollar-coin-1\", \"keywords\": [ \"accounting\", \"billing\", \"payment\", \"cash\", \"coin\", \"currency\", \"money\", \"finance\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188167)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.7905 5.50892C13.7905 8.37869 11.4641 10.7051 8.59437 10.7051C5.7246 10.7051 3.39819 8.37869 3.39819 5.50892C3.39819 2.63915 5.7246 0.312744 8.59437 0.312744C11.4641 0.312744 13.7905 2.63915 13.7905 5.50892ZM8.5943 1.58496C9.00851 1.58496 9.3443 1.92075 9.3443 2.33496V2.65193C9.70135 2.73687 10.0165 2.92997 10.2524 3.19349C10.4042 3.36312 10.5242 3.5629 10.6022 3.78342C10.7402 4.17395 10.5355 4.60245 10.145 4.74048C9.75446 4.87852 9.32597 4.67382 9.18793 4.28329C9.1764 4.25066 9.15834 4.22025 9.13471 4.19386C9.08438 4.13762 9.01361 4.10399 8.934 4.10399H8.59928L8.5943 4.10401L8.58931 4.10399H8.14523C8.05706 4.10399 7.98559 4.17546 7.98559 4.26362C7.98559 4.33864 8.03783 4.40354 8.11111 4.41957L9.31204 4.68227C10.1244 4.85998 10.703 5.57967 10.703 6.41075C10.703 7.24626 10.1236 7.94718 9.3443 8.13233V8.44927C9.3443 8.86348 9.00851 9.19927 8.5943 9.19927C8.18008 9.19927 7.8443 8.86348 7.8443 8.44927V8.13235C7.25732 7.99295 6.78452 7.56114 6.58645 7.00074C6.44841 6.6102 6.65311 6.18171 7.04364 6.04368C7.43418 5.90564 7.86267 6.11033 8.00071 6.50087C8.038 6.60638 8.13867 6.68017 8.25464 6.68017H8.934C9.08223 6.68017 9.20305 6.5597 9.20305 6.41075C9.20305 6.28405 9.11475 6.17458 8.99149 6.14762L7.79057 5.88492C7.02866 5.71825 6.48559 5.04355 6.48559 4.26362C6.48559 3.44979 7.07137 2.77277 7.8443 2.6312V2.33496C7.8443 1.92075 8.18008 1.58496 8.5943 1.58496ZM8.59437 11.9551C8.92088 11.9551 9.24172 11.9309 9.55518 11.884C8.602 13.0198 7.17189 13.7418 5.57313 13.7418C2.70336 13.7418 0.376953 11.4154 0.376953 8.54563C0.376953 6.95638 1.09042 5.53377 2.21454 4.58063C2.17082 4.88376 2.14819 5.19371 2.14819 5.50895C2.14819 9.06908 5.03424 11.9551 8.59437 11.9551Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188167\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"dressing-table\", \"keywords\": [ \"makeup\", \"dressing\", \"table\", \"mirror\", \"cabinet\", \"product\", \"decoration\", \"furniture\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188195)\\\">\\n<path d=\\\"M13.25 7.5H12.75V5.75C12.75 4.22501 12.1442 2.76247 11.0659 1.68414C9.98753 0.605802 8.52499 0 7 0C5.47501 0 4.01247 0.605802 2.93414 1.68414C1.8558 2.76247 1.25 4.22501 1.25 5.75V7.5H0.75C0.551088 7.5 0.360322 7.57902 0.21967 7.71967C0.0790176 7.86032 0 8.05109 0 8.25C0 8.44891 0.0790176 8.63968 0.21967 8.78033C0.360322 8.92098 0.551088 9 0.75 9H1C1.0663 9 1.12989 9.02634 1.17678 9.07322C1.22366 9.12011 1.25 9.1837 1.25 9.25V12.25C1.25263 12.7133 1.43784 13.1569 1.76546 13.4845C2.09309 13.8122 2.53668 13.9974 3 14H6C6.0663 14 6.12989 13.9737 6.17678 13.9268C6.22366 13.8799 6.25 13.8163 6.25 13.75V7.75C6.25 7.6837 6.22366 7.62011 6.17678 7.57322C6.12989 7.52634 6.0663 7.5 6 7.5H3C2.9337 7.5 2.87011 7.47366 2.82322 7.42678C2.77634 7.37989 2.75 7.3163 2.75 7.25V5.75C2.75 4.62283 3.19777 3.54183 3.9948 2.7448C4.79183 1.94777 5.87283 1.5 7 1.5C8.12717 1.5 9.20817 1.94777 10.0052 2.7448C10.8022 3.54183 11.25 4.62283 11.25 5.75V7.25C11.25 7.3163 11.2237 7.37989 11.1768 7.42678C11.1299 7.47366 11.0663 7.5 11 7.5H8C7.9337 7.5 7.87011 7.52634 7.82322 7.57322C7.77634 7.62011 7.75 7.6837 7.75 7.75V13.75C7.75 13.8163 7.77634 13.8799 7.82322 13.9268C7.87011 13.9737 7.9337 14 8 14H11C11.4633 13.9974 11.9069 13.8122 12.2345 13.4845C12.5622 13.1569 12.7474 12.7133 12.75 12.25V9.25C12.75 9.1837 12.7763 9.12011 12.8232 9.07322C12.8701 9.02634 12.9337 9 13 9H13.25C13.4489 9 13.6397 8.92098 13.7803 8.78033C13.921 8.63968 14 8.44891 14 8.25C14 8.05109 13.921 7.86032 13.7803 7.71967C13.6397 7.57902 13.4489 7.5 13.25 7.5ZM3.5 10.25C3.5 10.0511 3.57902 9.86032 3.71967 9.71967C3.86032 9.57902 4.05109 9.5 4.25 9.5C4.44891 9.5 4.63968 9.57902 4.78033 9.71967C4.92098 9.86032 5 10.0511 5 10.25V11.25C5 11.4489 4.92098 11.6397 4.78033 11.7803C4.63968 11.921 4.44891 12 4.25 12C4.05109 12 3.86032 11.921 3.71967 11.7803C3.57902 11.6397 3.5 11.4489 3.5 11.25V10.25ZM10.5 11.25C10.5 11.4489 10.421 11.6397 10.2803 11.7803C10.1397 11.921 9.94891 12 9.75 12C9.55109 12 9.36032 11.921 9.21967 11.7803C9.07902 11.6397 9 11.4489 9 11.25V10.25C9 10.0511 9.07902 9.86032 9.21967 9.71967C9.36032 9.57902 9.55109 9.5 9.75 9.5C9.94891 9.5 10.1397 9.57902 10.2803 9.71967C10.421 9.86032 10.5 10.0511 10.5 10.25V11.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188195\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"ethereum\", \"keywords\": [ \"crypto\", \"circle\", \"payment\", \"blokchain\", \"finance\", \"ethereum\", \"eth\", \"currency\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.1696 0.0725708C7.3285 0.0725745 7.47795 0.148113 7.57218 0.276064L12.0377 6.33937L7.16966 7.95611L2.30103 6.33935L6.767 0.276045C6.86124 0.148099 7.01069 0.0725671 7.1696 0.0725708ZM2.28711 7.65185L6.767 13.734C6.86124 13.862 7.01069 13.9375 7.1696 13.9375C7.32851 13.9375 7.47795 13.862 7.57218 13.734L12.0516 7.65191L7.36666 9.20782C7.23877 9.25029 7.10058 9.25029 6.9727 9.20782L2.28711 7.65185Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"ethereum-circle\", \"keywords\": [ \"crypto\", \"circle\", \"payment\", \"blockchain\", \"finance\", \"eth\", \"currency\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188177)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7ZM7 2.5C7.14839 2.5 7.28911 2.56591 7.38411 2.67991L9.88411 5.67991C9.97011 5.78311 10.011 5.91658 9.99747 6.05024C9.98397 6.18391 9.91725 6.30651 9.81235 6.39043L7.31235 8.39043C7.12974 8.53652 6.87026 8.53652 6.68765 8.39043L4.18765 6.39043C4.08275 6.30651 4.01603 6.18391 4.00253 6.05024C3.98903 5.91658 4.02989 5.78311 4.11589 5.67991L6.61589 2.67991C6.71089 2.56591 6.85161 2.5 7 2.5ZM4.89043 8.51196C4.6209 8.29633 4.22759 8.34003 4.01196 8.60957C3.79633 8.8791 3.84003 9.27241 4.10957 9.48804L6.60957 11.488C6.83783 11.6707 7.16217 11.6707 7.39043 11.488L9.89043 9.48804C10.16 9.27241 10.2037 8.8791 9.98804 8.60957C9.77241 8.34003 9.3791 8.29633 9.10957 8.51196L7 10.1996L4.89043 8.51196Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188177\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"euro\", \"keywords\": [ \"exchange\", \"payment\", \"euro\", \"forex\", \"finance\", \"foreign\", \"currency\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188217)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.0256 2C10.4601 1.9998 10.8916 2.07241 11.3023 2.21485C11.824 2.39585 12.3938 2.11959 12.5748 1.59781C12.7558 1.07603 12.4795 0.50631 11.9577 0.325307C11.3265 0.106326 10.6625 -0.00364813 9.99442 9.22967e-05C9.98096 0.000167697 9.9675 0.000514715 9.95406 0.00113323C8.16635 0.0833766 6.48393 0.870113 5.27465 2.18932C4.73108 2.78231 4.30299 3.4612 4.00304 4.19135H2C1.44771 4.19135 1 4.63906 1 5.19135C1 5.74363 1.44771 6.19135 2 6.19135H3.51942C3.49671 6.45896 3.48984 6.72898 3.4993 7.00008C3.48984 7.2711 3.4967 7.54105 3.5194 7.80859H2C1.44771 7.80859 1 8.25631 1 8.80859C1 9.36088 1.44771 9.80859 2 9.80859H4.00295C4.3029 10.5388 4.73102 11.2178 5.27465 11.8108C6.48392 13.13 8.16635 13.9168 9.95406 13.999C9.96823 13.9997 9.98242 14 9.99661 14.0001C10.8219 14.0029 11.6389 13.8354 12.3966 13.5081C12.9036 13.289 13.137 12.7005 12.918 12.1935C12.699 11.6865 12.1104 11.453 11.6034 11.6721C11.1048 11.8875 10.5675 11.9991 10.0245 12.0001C8.77294 11.9369 7.59615 11.3836 6.74896 10.4594C6.56241 10.2559 6.39516 10.038 6.24825 9.80859H8C8.55228 9.80859 9 9.36088 9 8.80859C9 8.25631 8.55228 7.80859 8 7.80859H5.52963C5.49905 7.55556 5.48861 7.29892 5.49918 7.04099C5.50029 7.01373 5.50029 6.98643 5.49918 6.95916C5.48861 6.70116 5.49905 6.44445 5.52966 6.19135H8C8.55228 6.19135 9 5.74363 9 5.19135C9 4.63906 8.55228 4.19135 8 4.19135H6.24839C6.39526 3.96202 6.56246 3.74422 6.74896 3.54077C7.59639 2.61629 8.77362 2.06295 10.0256 2Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188217\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"gift\", \"keywords\": [ \"reward\", \"box\", \"social\", \"present\", \"gift\", \"media\", \"rating\", \"bow\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188210)\\\">\\n<path d=\\\"M4.65652 0.625855C4.3067 0.404039 3.8433 0.507803 3.62149 0.857618C3.39967 1.20743 3.50343 1.67083 3.85325 1.89265L4.81103 2.49997H1.5C0.77791 2.49997 0 2.98901 0 3.80558V5.22241C0 5.9843 0.677231 6.46106 1.35446 6.52149C1.3487 6.52324 1.34298 6.52509 1.33731 6.52704H6.375V3.88428H7.625V6.52704H12.6627C12.657 6.52509 12.6513 6.52324 12.6455 6.52149C13.3228 6.46106 14 5.9843 14 5.22241V3.80558C14 2.98901 13.2221 2.49997 12.5 2.49997H9.18901L10.1468 1.89265C10.4966 1.67083 10.6004 1.20743 10.3786 0.857618C10.1567 0.507803 9.69335 0.404039 9.34353 0.625855L7.00002 2.11186L4.65652 0.625855Z\\\" fill=\\\"black\\\"/>\\n<path d=\\\"M13 7.77704H7.625V14H11.5C11.8978 14 12.2794 13.8419 12.5607 13.5606C12.842 13.2793 13 12.8978 13 12.5V7.77704Z\\\" fill=\\\"black\\\"/>\\n<path d=\\\"M6.375 7.77704H1V12.5C1 12.8978 1.15804 13.2793 1.43934 13.5606C1.72064 13.8419 2.10217 14 2.5 14H6.375V7.77704Z\\\" fill=\\\"black\\\"/>\\n<path d=\\\"M9.19687 3.38025H4.69727\\\" stroke=\\\"black\\\" stroke-width=\\\"1.25\\\" stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188210\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"gift-2\", \"keywords\": [ \"reward\", \"box\", \"social\", \"present\", \"gift\", \"media\", \"rating\", \"bow\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188204)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.95473 3.39135H3.44824C3.30112 3.20202 3.19339 2.93663 3.17059 2.67886C3.14795 2.42306 3.21163 2.24507 3.3098 2.13623C3.40146 2.03459 3.62625 1.87891 4.17101 1.87891C4.6685 1.87891 5.17931 2.21809 5.6129 2.81981C5.74977 3.00976 5.86332 3.20468 5.95473 3.39135ZM1.67642 2.81106C1.69378 3.00725 1.73277 3.20247 1.79058 3.39135H1.19726C0.644981 3.39135 0.197266 3.83907 0.197266 4.39135V5.27344C0.197266 5.82572 0.644981 6.27344 1.19726 6.27344H12.8008C13.353 6.27344 13.8008 5.82572 13.8008 5.27344V4.39135C13.8008 3.83907 13.353 3.39135 12.8008 3.39135H12.2074C12.2653 3.20247 12.3042 3.00725 12.3216 2.81106C12.3705 2.25883 12.2505 1.62879 11.8021 1.13162C11.3472 0.627231 10.6625 0.378906 9.82703 0.378906C8.62767 0.378906 7.72442 1.17095 7.16818 1.94288C7.10963 2.02414 7.05317 2.10754 6.99902 2.19255C6.94487 2.10754 6.88841 2.02414 6.82986 1.94288C6.27362 1.17095 5.37037 0.378906 4.17101 0.378906C3.33551 0.378906 2.6508 0.627231 2.1959 1.13162C1.74751 1.62879 1.62756 2.25883 1.67642 2.81106ZM8.04331 3.39135H10.5498C10.6969 3.20202 10.8046 2.93663 10.8274 2.67886C10.8501 2.42306 10.7864 2.24507 10.6882 2.13623C10.5966 2.03459 10.3718 1.87891 9.82703 1.87891C9.32954 1.87891 8.81873 2.21809 8.38514 2.81981C8.24827 3.00976 8.13472 3.20468 8.04331 3.39135ZM1.19726 7.52338H6.37402V13.8552H2.15258C1.6003 13.8552 1.15258 13.4075 1.15258 12.8552V7.52295C1.16744 7.52324 1.18234 7.52338 1.19726 7.52338ZM11.8452 13.8552H7.62402V7.52338H12.8008C12.8156 7.52338 12.8304 7.52324 12.8452 7.52295V12.8552C12.8452 13.4075 12.3975 13.8552 11.8452 13.8552Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188204\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"gold\", \"keywords\": [ \"gold\", \"money\", \"payment\", \"bars\", \"finance\", \"wealth\", \"bullion\", \"jewelry\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188207)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.05728 1.2475H5.97695C5.65305 1.24227 5.33739 1.3501 5.08434 1.55254C4.82984 1.75614 4.6548 2.04254 4.58968 2.36189L4.58886 2.36598L3.85671 6.11601C3.82809 6.26264 3.86652 6.41438 3.9615 6.5297C4.05648 6.64502 4.19805 6.71182 4.34745 6.71182H9.70464C9.85443 6.71182 9.99634 6.64466 10.0913 6.52882C10.1863 6.41297 10.2243 6.26065 10.195 6.11376L9.44492 2.36373L9.44455 2.36189C9.37943 2.04254 9.20439 1.75614 8.94988 1.55254C8.69684 1.3501 8.38118 1.24227 8.05728 1.2475ZM4.26257 8.09854H2.18224C1.85834 8.09331 1.54268 8.20114 1.28964 8.40358C1.03513 8.60718 0.860087 8.89358 0.794969 9.21293L0.794153 9.21702L0.062004 12.9671C0.0333768 13.1137 0.0718154 13.2654 0.166795 13.3807C0.261774 13.4961 0.403343 13.5629 0.552738 13.5629H5.90993C6.05973 13.5629 6.20163 13.4957 6.2966 13.3799C6.39157 13.264 6.4296 13.1117 6.40022 12.9648L5.65022 9.21477L5.64984 9.21293C5.58472 8.89358 5.40968 8.60718 5.15518 8.40358C4.90213 8.20114 4.58647 8.09331 4.26257 8.09854ZM9.77165 8.09854H11.852C12.1759 8.09331 12.4916 8.20114 12.7446 8.40358C12.9991 8.60718 13.1742 8.89358 13.2393 9.21293L13.2397 9.21477L13.9897 12.9648C14.0191 13.1117 13.981 13.264 13.8861 13.3799C13.7911 13.4957 13.6492 13.5629 13.4994 13.5629H8.14216C7.99276 13.5629 7.85119 13.4961 7.75621 13.3807C7.66123 13.2654 7.62279 13.1137 7.65142 12.9671L8.38357 9.21702L8.38437 9.21293C8.44948 8.89358 8.62455 8.60718 8.87905 8.40358C9.1321 8.20114 9.44776 8.09331 9.77165 8.09854Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188207\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"graph\", \"keywords\": [ \"analytics\", \"business\", \"product\", \"graph\", \"data\", \"chart\", \"analysis\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188201)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.57422 0.898315C1.57422 0.484102 1.23843 0.148315 0.824219 0.148315C0.410005 0.148315 0.0742188 0.484102 0.0742188 0.898315V13.1016C0.0742188 13.5159 0.410005 13.8516 0.824219 13.8516H13.0276C13.4418 13.8516 13.7776 13.5159 13.7776 13.1016C13.7776 12.6874 13.4418 12.3516 13.0276 12.3516H1.57422V0.898315ZM10.2216 2.43126C10.0012 2.27386 9.72642 2.21256 9.46005 2.26138C9.19368 2.3102 8.9585 2.46497 8.80828 2.69029L5.73004 7.30766L4.24578 5.8234C3.85525 5.43287 3.22209 5.43287 2.83156 5.8234C2.44104 6.21392 2.44104 6.84709 2.83156 7.23761L5.17836 9.5844C5.3902 9.79625 5.68586 9.90195 5.984 9.87243C6.28213 9.84291 6.55133 9.68127 6.71751 9.432L9.8995 4.65902L12.3446 6.40552C12.7941 6.72653 13.4186 6.62244 13.7396 6.17303C14.0606 5.72361 13.9565 5.09906 13.5071 4.77805L10.2216 2.43126Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188201\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"graph-arrow-decrease\", \"keywords\": [ \"down\", \"stats\", \"graph\", \"descend\", \"right\", \"arrow\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.89852 3.2224C1.50799 2.83187 0.874824 2.83187 0.4843 3.2224C0.0937752 3.61292 0.0937752 4.24609 0.4843 4.63661L3.81676 7.96906L3.82386 7.9761C4.09613 8.24297 4.46217 8.39245 4.84341 8.39245C5.22466 8.39245 5.5907 8.24297 5.86296 7.9761L5.87007 7.96906L7.57124 6.26789L9.91794 8.75034L8.87809 9.79019C8.66358 10.0047 8.59943 10.3273 8.71551 10.6075C8.8316 10.8878 9.10507 11.0705 9.40842 11.0705H13.0604C13.4746 11.0705 13.8104 10.7347 13.8104 10.3205V6.66852C13.8104 6.36517 13.6277 6.09169 13.3474 5.97561C13.0672 5.85952 12.7446 5.92369 12.5301 6.13819L11.3327 7.33557L8.62866 4.4751C8.61994 4.46587 8.61104 4.4568 8.60197 4.44791C8.32971 4.18104 7.96366 4.03156 7.58242 4.03156C7.20117 4.03156 6.83513 4.18104 6.56287 4.44791L6.55576 4.45495L4.84341 6.16729L1.89852 3.2224Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"graph-arrow-increase\", \"keywords\": [ \"ascend\", \"growth\", \"up\", \"arrow\", \"stats\", \"graph\", \"right\", \"grow\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.89852 10.7777C1.50799 11.1682 0.874824 11.1682 0.4843 10.7777C0.0937752 10.3871 0.0937752 9.754 0.4843 9.36348L3.81676 6.03103L3.82386 6.02399C4.09613 5.75712 4.46217 5.60764 4.84341 5.60764C5.22466 5.60764 5.5907 5.75712 5.86296 6.02399L5.87007 6.03103L7.57124 7.7322L9.91794 5.24975L8.87809 4.2099C8.66358 3.9954 8.59943 3.67281 8.71551 3.39256C8.8316 3.1123 9.10507 2.92957 9.40842 2.92957H13.0604C13.4746 2.92957 13.8104 3.26536 13.8104 3.67957V7.33157C13.8104 7.63492 13.6277 7.9084 13.3474 8.02448C13.0672 8.14057 12.7446 8.0764 12.5301 7.8619L11.3327 6.66452L8.62866 9.52499C8.61994 9.53422 8.61104 9.54329 8.60197 9.55218C8.32971 9.81905 7.96366 9.96853 7.58242 9.96853C7.20117 9.96853 6.83513 9.81905 6.56287 9.55218L6.55576 9.54514L4.84341 7.8328L1.89852 10.7777Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"graph-bar-decrease\", \"keywords\": [ \"arrow\", \"product\", \"performance\", \"down\", \"decrease\", \"graph\", \"business\", \"chart\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188229)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.31491 0.606416C1.48347 0.228052 1.92684 0.0579732 2.30521 0.226535L10.8962 4.05385L11.2573 3.25948C11.3831 2.98281 11.6633 2.80939 11.9671 2.82031C12.2708 2.83124 12.5378 3.02436 12.6434 3.30934L13.4434 5.46934C13.5867 5.85624 13.3906 6.28622 13.0045 6.43167L10.8545 7.24167C10.5704 7.34869 10.2499 7.27393 10.0425 7.05228C9.83505 6.83064 9.7817 6.50582 9.90731 6.22948L10.2755 5.41944L1.69479 1.59671C1.31643 1.42815 1.14635 0.984781 1.31491 0.606416ZM1.25 5.5C0.984783 5.5 0.730429 5.60536 0.542893 5.7929C0.355357 5.98043 0.25 6.23479 0.25 6.5V13.5C0.25 13.7761 0.473858 14 0.75 14H3.25C3.52614 14 3.75 13.7761 3.75 13.5V6.5C3.75 6.23479 3.64464 5.98043 3.45711 5.7929C3.26957 5.60536 3.01522 5.5 2.75 5.5H1.25ZM5.54289 7.2929C5.73043 7.10536 5.98478 7 6.25 7H7.75C8.01522 7 8.26957 7.10536 8.45711 7.2929C8.64464 7.48043 8.75 7.73479 8.75 8V13.5C8.75 13.7761 8.52614 14 8.25 14H5.75C5.47386 14 5.25 13.7761 5.25 13.5V8C5.25 7.73479 5.35536 7.48043 5.54289 7.2929ZM11.25 8.5C10.9848 8.5 10.7304 8.60536 10.5429 8.7929C10.3554 8.98043 10.25 9.23479 10.25 9.5V13.5C10.25 13.7761 10.4739 14 10.75 14H13.25C13.5261 14 13.75 13.7761 13.75 13.5V9.5C13.75 9.23479 13.6446 8.98043 13.4571 8.7929C13.2696 8.60536 13.0152 8.5 12.75 8.5H11.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188229\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"graph-bar-increase\", \"keywords\": [ \"up\", \"product\", \"performance\", \"increase\", \"arrow\", \"graph\", \"business\", \"chart\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188226)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.99716 0.220571C10.2117 0.00532459 10.5349 -0.0591832 10.8156 0.0572207L12.9434 0.93949C13.3245 1.09752 13.5064 1.5337 13.3505 1.91569L12.4824 4.04288C12.3677 4.32393 12.0948 4.50812 11.7913 4.50947C11.4877 4.51083 11.2133 4.32907 11.0961 4.04905L10.7526 3.22836L2.11483 6.92021C1.73395 7.083 1.29321 6.90621 1.13042 6.52532C0.967627 6.14444 1.14442 5.70371 1.52531 5.54091L10.1735 1.84462L9.83652 1.03959C9.71919 0.759251 9.78263 0.435818 9.99716 0.220571ZM12.75 5.47953C13.0152 5.47953 13.2696 5.58488 13.4571 5.77242C13.6446 5.95996 13.75 6.21431 13.75 6.47953V13.4795C13.75 13.7557 13.5261 13.9795 13.25 13.9795H10.75C10.4739 13.9795 10.25 13.7557 10.25 13.4795V6.47953C10.25 6.21431 10.3554 5.95996 10.5429 5.77242C10.7304 5.58488 10.9848 5.47953 11.25 5.47953H12.75ZM8.45711 7.27242C8.26957 7.08488 8.01522 6.97953 7.75 6.97953H6.25C5.98478 6.97953 5.73043 7.08488 5.54289 7.27242C5.35536 7.45996 5.25 7.71431 5.25 7.97953V13.4795C5.25 13.7557 5.47386 13.9795 5.75 13.9795H8.25C8.52614 13.9795 8.75 13.7557 8.75 13.4795V7.97953C8.75 7.71431 8.64464 7.45996 8.45711 7.27242ZM2.75 8.47953C3.01522 8.47953 3.26957 8.58488 3.45711 8.77242C3.64464 8.95996 3.75 9.21431 3.75 9.47953V13.4795C3.75 13.7557 3.52614 13.9795 3.25 13.9795H0.75C0.473858 13.9795 0.25 13.7557 0.25 13.4795V9.47953C0.25 9.21431 0.355356 8.95996 0.542893 8.77242C0.73043 8.58488 0.984785 8.47953 1.25 8.47953H2.75Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188226\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"graph-dot\", \"keywords\": [ \"product\", \"data\", \"bars\", \"analysis\", \"analytics\", \"graph\", \"business\", \"chart\", \"dot\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188248)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0.75C1.5 0.335786 1.16421 0 0.75 0C0.335786 0 0 0.335786 0 0.75V9.42379V13.25C0 13.6642 0.335786 14 0.75 14H13.25C13.6642 14 14 13.6642 14 13.25C14 12.8358 13.6642 12.5 13.25 12.5H1.5V9.72028L3.7024 7.36833C3.99334 7.53376 4.32987 7.62824 4.68848 7.62824C5.14201 7.62824 5.56024 7.47712 5.89557 7.22248L7.31902 8.55012C7.25299 8.74879 7.21724 8.96129 7.21724 9.18213C7.21724 10.2881 8.11378 11.1846 9.21973 11.1846C10.3257 11.1846 11.2222 10.2881 11.2222 9.18213C11.2222 8.66581 11.0268 8.19514 10.7059 7.84002L10.7079 7.8347L11.8665 4.65234C12.8869 4.56227 13.687 3.70539 13.687 2.66162C13.687 1.55786 12.7922 0.663086 11.6885 0.663086C10.5847 0.663086 9.68994 1.55786 9.68994 2.66162C9.68994 3.28831 9.97839 3.84762 10.4298 4.21405L9.34856 7.18372C9.30597 7.18101 9.26301 7.17964 9.21973 7.17964C8.88511 7.17964 8.56966 7.26171 8.29242 7.40683L6.74886 5.96716C6.7231 5.94314 6.6961 5.92128 6.66809 5.90157C6.68012 5.81289 6.68634 5.72236 6.68634 5.63037C6.68634 4.52698 5.79187 3.63251 4.68848 3.63251C3.58509 3.63251 2.69061 4.52698 2.69061 5.63037C2.69061 5.81938 2.71686 6.00225 2.7659 6.17555C2.75597 6.18505 2.74624 6.19489 2.73673 6.20505L1.5 7.52575V0.75Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188248\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"investment-selection\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188244)\\\">\\n<path d=\\\"M7.5 13.4988V8.24878C7.5 7.91726 7.6317 7.59932 7.86612 7.3649C8.10054 7.13048 8.41848 6.99878 8.75 6.99878C9.08152 6.99878 9.39946 7.13048 9.63388 7.3649C9.8683 7.59932 10 7.91726 10 8.24878V10.9988H12C12.5304 10.9988 13.0391 11.2095 13.4142 11.5846C13.7893 11.9596 14 12.4683 14 12.9988V13.4988\\\" fill=\\\"black\\\"/>\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.03125 4.42439C0.03125 1.98154 2.01157 0.0012207 4.45442 0.0012207C6.89727 0.0012207 8.87759 1.98154 8.87759 4.42439C8.87759 4.88617 8.80683 5.33142 8.67556 5.74989C8.03952 5.76882 7.4335 6.02975 6.98223 6.48101C6.51339 6.94985 6.25 7.58574 6.25 8.24878V8.46792C5.70128 8.71195 5.09369 8.84756 4.45442 8.84756C2.01157 8.84756 0.03125 6.86724 0.03125 4.42439ZM4.45443 1.18555C4.7996 1.18555 5.07943 1.46537 5.07943 1.81055V2.09449C5.38654 2.16476 5.65774 2.3295 5.85998 2.55545C5.98841 2.69894 6.0899 2.86792 6.15583 3.05443C6.27086 3.37988 6.10028 3.73696 5.77483 3.85198C5.44938 3.96701 5.09231 3.79644 4.97728 3.47099C4.9667 3.44106 4.95015 3.41322 4.92856 3.3891C4.88257 3.33771 4.81772 3.30684 4.74483 3.30684H4.45677L4.45443 3.30685L4.45209 3.30684H4.07041C3.98605 3.30684 3.91766 3.37523 3.91766 3.45959C3.91766 3.53138 3.96765 3.59348 4.03777 3.60882L5.06459 3.83343C5.75173 3.98374 6.24113 4.59248 6.24113 5.29542C6.24113 6.00656 5.7449 6.60242 5.07943 6.75448V7.03833C5.07943 7.3835 4.7996 7.66333 4.45443 7.66333C4.10925 7.66333 3.82943 7.3835 3.82943 7.03833V6.75452C3.32716 6.63982 2.92191 6.27245 2.75297 5.79446C2.63794 5.46901 2.80852 5.11193 3.13396 4.9969C3.45941 4.88187 3.81649 5.05245 3.93152 5.3779C3.96563 5.4744 4.0577 5.54204 4.16396 5.54204H4.44962L4.45443 5.54203L4.45924 5.54204H4.74483C4.88056 5.54204 4.99113 5.43175 4.99113 5.29542C4.99113 5.17946 4.91032 5.07924 4.79746 5.05456L3.77065 4.82994C3.12667 4.68907 2.66766 4.1188 2.66766 3.45959C2.66766 2.76705 3.16954 2.19172 3.82943 2.07746V1.81055C3.82943 1.46537 4.10925 1.18555 4.45443 1.18555ZM7.5 8.24878V13.9988H14V12.9988C14 12.4683 13.7893 11.9596 13.4142 11.5846C13.0391 11.2095 12.5304 10.9988 12 10.9988H10V8.24878C10 7.91726 9.8683 7.59932 9.63388 7.3649C9.39946 7.13048 9.08152 6.99878 8.75 6.99878C8.41848 6.99878 8.10054 7.13048 7.86612 7.3649C7.6317 7.59932 7.5 7.91726 7.5 8.24878Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188244\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"justice-hammer\", \"keywords\": [ \"hammer\", \"work\", \"mallet\", \"office\", \"company\", \"gavel\", \"justice\", \"judge\", \"arbitration\", \"court\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188232)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.8779 6.26556C3.29212 5.67978 3.29212 4.73003 3.8779 4.14424L7.46293 0.559213C8.04872 -0.0265737 8.99847 -0.0265735 9.58426 0.559213L11.1681 2.14313C11.7539 2.72892 11.7539 3.67867 11.1681 4.26445L9.91122 5.52141L13.6998 9.30998C13.9927 9.60287 13.9927 10.0777 13.6998 10.3706C13.4069 10.6635 12.932 10.6635 12.6391 10.3706L8.85056 6.58207L7.58314 7.84948C6.99736 8.43527 6.04761 8.43527 5.46182 7.84948L3.8779 6.26556ZM1.33203 12.3801H0.832031C0.417818 12.3801 0.0820312 12.7159 0.0820312 13.1301C0.0820312 13.5443 0.417818 13.8801 0.832031 13.8801H8.33203C8.74624 13.8801 9.08203 13.5443 9.08203 13.1301C9.08203 12.7159 8.74624 12.3801 8.33203 12.3801H7.83203V10.6301C7.83203 10.354 7.60817 10.1301 7.33203 10.1301H1.83203C1.55589 10.1301 1.33203 10.354 1.33203 10.6301V12.3801Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188232\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"justice-scale-1\", \"keywords\": [ \"office\", \"work\", \"scale\", \"justice\", \"company\", \"arbitration\", \"balance\", \"court\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.74912 2.30566C7.74912 1.89145 7.41333 1.55566 6.99912 1.55566C6.58491 1.55566 6.24912 1.89145 6.24912 2.30566V3.43341H3.24365H1.83536C1.42115 3.43341 1.08536 3.7692 1.08536 4.18341C1.08536 4.59762 1.42115 4.93341 1.83536 4.93341H2.15551L0.195583 10.0803C0.163122 10.1655 0.146484 10.256 0.146484 10.3472C0.146484 11.1686 0.472791 11.9564 1.05363 12.5372C1.63446 13.118 2.42223 13.4443 3.24365 13.4443C4.06507 13.4443 4.85285 13.118 5.43368 12.5372C6.01451 11.9564 6.34081 11.1686 6.34081 10.3472C6.34081 10.256 6.32418 10.1655 6.29172 10.0803L4.33179 4.93341H9.66644L7.70651 10.0803C7.67405 10.1655 7.65741 10.256 7.65741 10.3472C7.65741 11.1686 7.98372 11.9564 8.56455 12.5372C9.14538 13.118 9.93316 13.4443 10.7545 13.4443C11.5759 13.4443 12.3637 13.118 12.9446 12.5372C13.5254 11.9564 13.8517 11.1686 13.8517 10.3472C13.8517 10.256 13.8351 10.1655 13.8026 10.0803L11.8427 4.93341H12.1628C12.577 4.93341 12.9128 4.59762 12.9128 4.18341C12.9128 3.7692 12.577 3.43341 12.1628 3.43341H10.7545H7.74912V2.30566ZM4.65607 10L3.24365 6.29091L1.83123 10H4.65607ZM10.7545 6.29091L12.1669 10H9.34216L10.7545 6.29091Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"justice-scale-2\", \"keywords\": [ \"office\", \"work\", \"scale\", \"justice\", \"unequal\", \"company\", \"arbitration\", \"unbalance\", \"court\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188238)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.8642 0.56654C13.0356 0.943626 12.8688 1.38826 12.4917 1.55967L11.748 1.89776L13.8125 6.70412C13.8527 6.79764 13.8734 6.89835 13.8734 7.00013C13.8734 7.82365 13.5463 8.61345 12.964 9.19577C12.3816 9.77809 11.5918 10.1052 10.7683 10.1052C9.94484 10.1052 9.15505 9.77809 8.57273 9.19577C7.99041 8.61345 7.66326 7.82365 7.66326 7.00013C7.66326 6.89835 7.68398 6.79764 7.72415 6.70412L9.31338 3.0044L7.3251 3.90816C7.31525 3.9129 7.30529 3.91743 7.29521 3.92175L4.19635 5.33032L6.28242 10.4871C6.31857 10.5764 6.33715 10.6719 6.33715 10.7683C6.33715 11.5919 6.01001 12.3817 5.42769 12.964C4.84537 13.5463 4.05557 13.8734 3.23205 13.8734C2.40853 13.8734 1.61873 13.5463 1.03641 12.964C0.454097 12.3817 0.126953 11.5919 0.126953 10.7683C0.126953 10.6719 0.145537 10.5764 0.181687 10.4871L1.86087 6.33612C1.56107 6.35281 1.26795 6.18725 1.13622 5.89745C0.964822 5.52036 1.13156 5.07572 1.50864 4.90432L6.25021 2.74906V0.876893C6.25021 0.462679 6.586 0.126893 7.00021 0.126893C7.41442 0.126893 7.75021 0.462679 7.75021 0.876893V2.06724L10.4142 0.856327C10.4429 0.840927 10.4727 0.827379 10.5033 0.815801L11.871 0.194118C12.2481 0.0227148 12.6928 0.189454 12.8642 0.56654ZM12.174 6.69012L10.7683 3.41772L9.3627 6.69012H12.174ZM3.23205 6.94649L1.82765 10.4182H4.63644L3.23205 6.94649Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188238\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"lipstick\", \"keywords\": [ \"fashion\", \"beauty\", \"lip\", \"lipstick\", \"makeup\", \"shopping\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188251)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.4952 5.65621L11.4056 5.56671L13.0262 3.97713C13.1171 3.88544 13.1891 3.7767 13.2379 3.65715C13.2868 3.5376 13.3115 3.40958 13.3108 3.28044V1.15111C13.3098 0.957385 13.2515 0.76828 13.1432 0.607628C13.035 0.446977 12.8816 0.321966 12.7024 0.248352C12.5237 0.173208 12.3267 0.152677 12.1364 0.189351C11.946 0.226025 11.7708 0.318259 11.6328 0.454417L8.96312 3.12414L8.83356 2.99458C8.69291 2.85393 8.50215 2.77491 8.30324 2.77491C8.10432 2.77491 7.91356 2.85393 7.77291 2.99458L5.71459 5.0529L5.64757 4.98589C5.54822 4.88653 5.41347 4.83072 5.27296 4.83072C5.13246 4.83072 4.99771 4.88653 4.89835 4.98589L1.16521 8.71903L1.1641 8.72015C0.858841 9.02723 0.6875 9.44263 0.6875 9.87562C0.6875 10.3086 0.85884 10.724 1.1641 11.0311L3.48343 13.3504L3.48455 13.3516C3.79163 13.6568 4.20703 13.8282 4.64002 13.8282C5.07302 13.8282 5.48842 13.6568 5.7955 13.3516L5.79662 13.3504L9.52976 9.61729C9.73665 9.4104 9.73665 9.07496 9.52976 8.86807L9.43688 8.77519L11.4952 6.71687C11.788 6.42398 11.788 5.9491 11.4952 5.65621ZM8.35251 7.69082L9.83203 6.2113L8.27849 4.65776L6.79897 6.13728L8.35251 7.69082Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188251\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"make-up-brush\", \"keywords\": [ \"fashion\", \"beauty\", \"make\", \"up\", \"brush\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188266)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.1271 6.89438L9.46852 8.60171C9.44453 8.57299 9.41904 8.54508 9.39207 8.51811L5.62152 4.74756C5.60483 4.73087 5.58778 4.71475 5.57041 4.6992L7.27634 1.04363C7.56981 0.414768 8.39893 0.268698 8.88963 0.759405L9.47673 1.3465L8.13532 3.12378C7.92737 3.39929 7.98215 3.79121 8.25766 3.99915C8.53317 4.2071 8.92509 4.15232 9.13304 3.87681L10.3692 2.23897L11.9884 3.85821L10.3679 4.88344C10.0762 5.06799 9.98935 5.45406 10.1739 5.74577C10.3584 6.03747 10.7445 6.12434 11.0362 5.93979L12.8944 4.76419L13.4113 5.28109C13.902 5.77179 13.7559 6.60091 13.1271 6.89438ZM8.50795 9.40199L4.7374 5.63144L1.12075 9.24809C0.0795412 10.2893 0.079541 11.9774 1.12075 13.0186C2.16196 14.0599 3.85009 14.0599 4.8913 13.0186L8.50795 9.40199Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188266\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"moustache\", \"keywords\": [ \"fashion\", \"beauty\", \"moustache\", \"grooming\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path d=\\\"M7.00089 8.39032C7.4257 9.09726 8.07066 9.64534 8.83687 9.95054C9.60308 10.2557 10.4482 10.3012 11.2428 10.08C12.0373 9.85871 12.7373 9.38297 13.2355 8.72569C13.5733 8.28002 13.8057 7.76756 13.9198 7.22668C13.9956 6.86726 13.6157 6.62057 13.2652 6.73039C12.8063 6.87414 12.3297 6.95818 11.847 6.97955C10.7701 6.97955 11.2224 4.37341 9.69319 3.86725C9.20016 3.73563 8.67878 3.75679 8.19804 3.92793C7.71729 4.09908 7.29984 4.41214 7.00089 4.82571C6.70195 4.41214 6.28449 4.09908 5.80375 3.92793C5.323 3.75679 4.80162 3.73563 4.30859 3.86725C2.77936 4.37341 3.23167 6.97955 2.15475 6.97955C1.67213 6.95818 1.19549 6.87414 0.736618 6.73039C0.386086 6.62057 0.00621875 6.86726 0.0820231 7.22668C0.196097 7.76756 0.428458 8.28002 0.766257 8.72569C1.26445 9.38297 1.96448 9.85871 2.75901 10.08C3.55354 10.3012 4.3987 10.2557 5.16491 9.95054C5.93112 9.64534 6.57608 9.09726 7.00089 8.39032Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"mouth-lip\", \"keywords\": [ \"fashion\", \"beauty\", \"mouth\", \"lip\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.7214 6.86523C13.67 6.73845 13.6043 6.58829 13.5231 6.42331C13.2899 5.9493 12.9147 5.32527 12.3559 4.78448C11.7913 4.23806 11.0251 3.76442 10.0318 3.64081C9.13587 3.5293 8.12646 3.71146 6.99939 4.27448C5.87232 3.71146 4.86292 3.5293 3.96695 3.6408C2.97368 3.76441 2.20742 4.23803 1.64282 4.78445C1.08403 5.32524 0.708878 5.94926 0.475608 6.42326C0.394403 6.58827 0.328764 6.73844 0.277344 6.86523H13.7214ZM0.334067 8.11523C0.394546 8.21626 0.472442 8.33953 0.568646 8.47896C0.825587 8.85134 1.21678 9.34432 1.76032 9.83733C2.85405 10.8294 4.56503 11.8191 6.99939 11.8191C9.43375 11.8191 11.1447 10.8294 12.2384 9.83736C12.782 9.34435 13.1732 8.85139 13.4301 8.47901C13.5263 8.33956 13.6042 8.21627 13.6647 8.11523H0.334067Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"necklace\", \"keywords\": [ \"diamond\", \"money\", \"payment\", \"finance\", \"wealth\", \"accessory\", \"necklace\", \"jewelry\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.24896 1.57994C4.3093 0.718057 5.63406 0.247559 7.00051 0.247559C8.36695 0.247559 9.69172 0.718057 10.7521 1.57994C11.8124 2.44182 12.5436 3.64252 12.8228 4.98013C13.102 6.31775 12.9121 7.7107 12.285 8.92477C11.8104 9.84364 11.1073 10.6176 10.2505 11.1766V10.7501C10.2505 10.3326 10.1015 9.93352 9.83742 9.62088C10.2952 9.24176 10.6757 8.772 10.9523 8.23641C11.4212 7.32853 11.5632 6.28687 11.3545 5.2866C11.1457 4.28632 10.5989 3.38844 9.80593 2.74392C9.013 2.0994 8.02233 1.74756 7.00051 1.74756C5.97868 1.74756 4.98801 2.0994 4.19508 2.74392C3.40216 3.38844 2.85533 4.28632 2.64656 5.2866C2.43779 6.28687 2.57981 7.32853 3.04873 8.23641C3.32536 8.77199 3.7058 9.24175 4.16357 9.62087C3.89951 9.93351 3.75049 10.3326 3.75049 10.7501V11.1766C2.89369 10.6176 2.19059 9.84363 1.716 8.92477C1.08894 7.7107 0.899027 6.31775 1.1782 4.98013C1.45738 3.64252 2.18862 2.44182 3.24896 1.57994ZM8.80049 10.3501C8.92639 10.4445 9.00049 10.5927 9.00049 10.7501V12.2501C9.00049 12.4075 8.92639 12.5557 8.80049 12.6501L7.30049 13.7751C7.12271 13.9085 6.87827 13.9085 6.70049 13.7751L5.20049 12.6501C5.07459 12.5557 5.00049 12.4075 5.00049 12.2501V10.7501C5.00049 10.5927 5.07459 10.4445 5.20049 10.3501L6.70049 9.22512C6.87827 9.09179 7.12271 9.09179 7.30049 9.22512L8.80049 10.3501Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"necktie\", \"keywords\": [ \"necktie\", \"businessman\", \"business\", \"cloth\", \"clothing\", \"gentleman\", \"accessories\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.49907 3.10352H8.49907L9.06033 1.41974C9.27617 0.772212 8.79421 0.103516 8.11165 0.103516H5.8865C5.20394 0.103516 4.72197 0.772212 4.93782 1.41974L5.49907 3.10352ZM9.46642 11.3586L8.49907 4.10352H5.49907L4.53173 11.3586C4.51115 11.513 4.56367 11.6681 4.67379 11.7782L6.64552 13.75C6.84078 13.9452 7.15736 13.9452 7.35263 13.75L9.32436 11.7782C9.43448 11.6681 9.487 11.513 9.46642 11.3586Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"payment-10\", \"keywords\": [ \"deposit\", \"payment\", \"finance\", \"atm\", \"transfer\", \"dollar\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188260)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0C0.671573 0 0 0.671573 0 1.5V7.5C0 8.32843 0.671573 9 1.5 9H12.5C13.3284 9 14 8.32843 14 7.5V1.5C14 0.671573 13.3284 0 12.5 0H1.5ZM7.62498 1.45416C7.62498 1.10898 7.34516 0.829163 6.99998 0.829163C6.6548 0.829163 6.37498 1.10898 6.37498 1.45416V1.85464C5.61295 1.94264 5.0213 2.59009 5.0213 3.37572C5.0213 4.09532 5.52236 4.71784 6.22534 4.87162L7.42183 5.13335C7.60079 5.1725 7.72871 5.33128 7.72871 5.51491C7.72871 5.73076 7.55368 5.90557 7.33843 5.90557H6.66158C6.49263 5.90557 6.34722 5.79786 6.29335 5.64543C6.17832 5.31999 5.82124 5.14941 5.49579 5.26444C5.17035 5.37947 4.99977 5.73654 5.1148 6.06199C5.30893 6.61123 5.78762 7.02715 6.37498 7.13062V7.54587C6.37498 7.89105 6.6548 8.17087 6.99998 8.17087C7.34516 8.17087 7.62498 7.89105 7.62498 7.54587V7.13061C8.39446 6.995 8.97871 6.32283 8.97871 5.51491C8.97871 4.7443 8.4422 4.077 7.68896 3.91223L6.49247 3.65049C6.36334 3.62225 6.2713 3.5079 6.2713 3.37572C6.2713 3.22038 6.39723 3.09445 6.55257 3.09445H7.33843C7.45396 3.09445 7.55713 3.14385 7.62948 3.2247C7.66346 3.26266 7.68977 3.30677 7.70667 3.35458C7.8217 3.68003 8.17877 3.85061 8.50422 3.73558C8.82967 3.62055 9.00025 3.26347 8.88522 2.93802C8.81297 2.73363 8.70172 2.54839 8.5609 2.39105C8.32168 2.12378 7.99458 1.9347 7.62498 1.86946V1.45416ZM2.75 3.75C3.16421 3.75 3.5 4.08579 3.5 4.5C3.5 4.91421 3.16421 5.25 2.75 5.25C2.33579 5.25 2 4.91421 2 4.5C2 4.08579 2.33579 3.75 2.75 3.75ZM11.25 3.75C11.6642 3.75 12 4.08579 12 4.5C12 4.91421 11.6642 5.25 11.25 5.25C10.8358 5.25 10.5 4.91421 10.5 4.5C10.5 4.08579 10.8358 3.75 11.25 3.75ZM4.5 9.875C4.84518 9.875 5.125 10.1548 5.125 10.5V12.5C5.125 12.8452 4.84518 13.125 4.5 13.125C4.15482 13.125 3.875 12.8452 3.875 12.5V10.5C3.875 10.1548 4.15482 9.875 4.5 9.875ZM10.125 10.5C10.125 10.1548 9.84518 9.875 9.5 9.875C9.15482 9.875 8.875 10.1548 8.875 10.5V12.5C8.875 12.8452 9.15482 13.125 9.5 13.125C9.84518 13.125 10.125 12.8452 10.125 12.5V10.5ZM7.625 11.25C7.625 10.9048 7.34518 10.625 7 10.625C6.65482 10.625 6.375 10.9048 6.375 11.25V13.25C6.375 13.5952 6.65482 13.875 7 13.875C7.34518 13.875 7.625 13.5952 7.625 13.25V11.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188260\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"payment-cash-out-3\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188269)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1.44991C0 0.625245 0.696096 0 1.5 0H12.5C13.3039 0 14 0.625245 14 1.44991V7.05009C14 7.87475 13.3039 8.5 12.5 8.5H1.5C0.696096 8.5 0 7.87475 0 7.05009V1.44991ZM9 4.25C9 5.35457 8.10457 6.25 7 6.25C5.89543 6.25 5 5.35457 5 4.25C5 3.14543 5.89543 2.25 7 2.25C8.10457 2.25 9 3.14543 9 4.25ZM3.5 4.5C3.5 4.08579 3.16421 3.75 2.75 3.75C2.33579 3.75 2 4.08579 2 4.5C2 4.91421 2.33579 5.25 2.75 5.25C3.16421 5.25 3.5 4.91421 3.5 4.5ZM11.25 3.75C11.6642 3.75 12 4.08579 12 4.5C12 4.91421 11.6642 5.25 11.25 5.25C10.8358 5.25 10.5 4.91421 10.5 4.5C10.5 4.08579 10.8358 3.75 11.25 3.75Z\\\" fill=\\\"black\\\"/>\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.125 10.5C5.125 10.1548 4.84518 9.875 4.5 9.875C4.15482 9.875 3.875 10.1548 3.875 10.5V12.5C3.875 12.8452 4.15482 13.125 4.5 13.125C4.84518 13.125 5.125 12.8452 5.125 12.5V10.5ZM9.5 9.875C9.84518 9.875 10.125 10.1548 10.125 10.5V12.5C10.125 12.8452 9.84518 13.125 9.5 13.125C9.15482 13.125 8.875 12.8452 8.875 12.5V10.5C8.875 10.1548 9.15482 9.875 9.5 9.875ZM7 10.625C7.34518 10.625 7.625 10.9048 7.625 11.25V13.25C7.625 13.5952 7.34518 13.875 7 13.875C6.65482 13.875 6.375 13.5952 6.375 13.25V11.25C6.375 10.9048 6.65482 10.625 7 10.625Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188269\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"pie-chart\", \"keywords\": [ \"product\", \"data\", \"analysis\", \"analytics\", \"pie\", \"business\", \"chart\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188279)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.375 0.0275269C2.8018 0.343686 0 3.34465 0 7.00001C0 10.866 3.13401 14 7 14C8.78013 14 10.4051 13.3355 11.6403 12.2411L6.57091 7.45445C6.44587 7.33638 6.375 7.172 6.375 7.00002V0.0275269ZM12.4986 11.3323C13.4389 10.1406 14 8.63583 14 7.00001C14 3.34465 11.1982 0.343686 7.625 0.0275269V6.73057L12.4986 11.3323Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188279\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"piggy-bank\", \"keywords\": [ \"institution\", \"saving\", \"bank\", \"payment\", \"finance\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188297)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.34893 2.08068C8.05807 0.704096 9.53028 0.351223 10.3016 0.386085C10.8548 0.411085 11.104 0.902274 11.104 1.27665V3.03008C11.8045 3.38223 12.273 3.85167 12.5605 4.43675C12.844 5.01364 12.93 5.66156 12.9451 6.32575L13.6401 6.89662C13.7557 6.99159 13.8227 7.13336 13.8227 7.28298V9.50173C13.8227 9.57453 13.8068 9.64646 13.7761 9.71248C13.2284 10.8908 12.2573 11.5275 11.104 11.9793V12.6164C11.104 13.1686 10.6563 13.6164 10.104 13.6164H8.3541C7.80181 13.6164 7.3541 13.1686 7.3541 12.6164V12.132H5.21347V12.5382C5.21347 13.0905 4.76576 13.5382 4.21347 13.5382H2.44781C1.89618 13.5382 1.44774 13.0917 1.4478 12.5386C1.44786 11.9757 1.44793 11.1031 1.44778 10.8216C1.4395 10.7934 1.42729 10.7543 1.4103 10.7009L1.39036 10.6383C1.3549 10.5272 1.30862 10.3822 1.25789 10.2133L1.25343 10.1984C0.910103 9.05506 0.27256 6.93187 1.18618 4.90699C1.25421 4.7562 1.33066 4.60856 1.41611 4.46481C0.943774 4.09099 0.470273 3.56308 0.20758 2.85912C0.0869005 2.53573 0.251234 2.17573 0.574629 2.05505C0.898024 1.93437 1.25802 2.09871 1.3787 2.4221C1.54289 2.86212 1.85375 3.22046 2.20694 3.49654C2.52151 3.2111 2.89457 2.95806 3.33378 2.74761C4.33085 2.26985 5.64407 2.02295 7.34893 2.08068ZM7.37549 4.90198C7.37549 5.24716 7.09567 5.52698 6.75049 5.52698H4.89551C4.55033 5.52698 4.27051 5.24716 4.27051 4.90198C4.27051 4.5568 4.55033 4.27698 4.89551 4.27698H6.75049C7.09567 4.27698 7.37549 4.5568 7.37549 4.90198ZM9.8335 6.91943C10.2563 6.91943 10.5991 6.57665 10.5991 6.15381C10.5991 5.73097 10.2563 5.38818 9.8335 5.38818C9.41065 5.38818 9.06787 5.73097 9.06787 6.15381C9.06787 6.57665 9.41065 6.91943 9.8335 6.91943Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188297\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"polka-dot-circle\", \"keywords\": [ \"crypto\", \"circle\", \"payment\", \"blockchain\", \"finance\", \"polka\", \"dot\", \"currency\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188254)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7ZM4.28436 5.74944C4.28436 4.19019 5.49086 2.92944 6.99997 2.92944C8.52127 2.92944 9.71557 4.20279 9.71557 5.64505C9.71557 7.11235 8.49727 8.46504 6.99997 8.46504C6.67928 8.46504 6.3595 8.52391 6.05755 8.63758L6.56891 6.08076C6.63661 5.74229 6.4171 5.41302 6.07862 5.34533C5.74015 5.27763 5.41088 5.49714 5.34318 5.83562L4.46026 10.2502C4.44746 10.2843 4.43522 10.3189 4.42356 10.3539C4.3975 10.4321 4.38775 10.5118 4.39246 10.5893L4.29925 11.0553C4.23155 11.3938 4.45106 11.7231 4.78954 11.7907C5.12801 11.8584 5.45728 11.6389 5.52497 11.3005L5.66328 10.6089C5.91833 10.0258 6.46363 9.71504 6.99997 9.71504C9.26084 9.71504 10.9656 7.72713 10.9656 5.64505C10.9656 3.53791 9.23684 1.67944 6.99997 1.67944C4.7509 1.67944 3.03436 3.55051 3.03436 5.74944C3.03436 6.09462 3.31418 6.37444 3.65936 6.37444C4.00454 6.37444 4.28436 6.09462 4.28436 5.74944ZM8.69276 11.1779C8.69276 11.5921 8.35697 11.9279 7.94276 11.9279C7.52855 11.9279 7.19276 11.5921 7.19276 11.1779C7.19276 10.7637 7.52855 10.4279 7.94276 10.4279C8.35697 10.4279 8.69276 10.7637 8.69276 11.1779Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188254\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"production-belt\", \"keywords\": [ \"production\", \"produce\", \"box\", \"belt\", \"factory\", \"product\", \"package\", \"business\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.29284 1.12451H10.6578C11.0311 1.12451 11.3336 1.3944 11.3336 1.72732V7.71106H2.6449C2.61253 7.71106 2.58027 7.71148 2.5481 7.7123V1.72732C2.5481 1.3944 2.85067 1.12451 3.22391 1.12451H5.5896V4.13857C5.5896 4.21851 5.6252 4.29517 5.68857 4.35169C5.75194 4.40822 5.83789 4.43997 5.92751 4.43997H7.95493C8.04455 4.43997 8.1305 4.40822 8.19387 4.35169C8.25724 4.29517 8.29284 4.21851 8.29284 4.13857V1.12451ZM2.6449 8.96094H11.3536C12.7053 8.96094 13.8012 10.0568 13.8012 11.4086C13.8012 12.7604 12.7053 13.8562 11.3536 13.8562H2.6449C1.29311 13.8562 0.197266 12.7604 0.197266 11.4086C0.197266 10.0568 1.29311 8.96094 2.6449 8.96094ZM4.40659 11.4004C4.40659 11.8836 4.01484 12.2754 3.53159 12.2754C3.04834 12.2754 2.65659 11.8836 2.65659 11.4004C2.65659 10.9171 3.04834 10.5254 3.53159 10.5254C4.01484 10.5254 4.40659 10.9171 4.40659 11.4004ZM7.87885 11.4004C7.87885 11.8836 7.48709 12.2754 7.00385 12.2754C6.5206 12.2754 6.12885 11.8836 6.12885 11.4004C6.12885 10.9171 6.5206 10.5254 7.00385 10.5254C7.48709 10.5254 7.87885 10.9171 7.87885 11.4004ZM10.4761 12.2754C10.9594 12.2754 11.3511 11.8836 11.3511 11.4004C11.3511 10.9171 10.9594 10.5254 10.4761 10.5254C9.99285 10.5254 9.6011 10.9171 9.6011 11.4004C9.6011 11.8836 9.99285 12.2754 10.4761 12.2754Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"qr-code\", \"keywords\": [ \"codes\", \"tags\", \"code\", \"qr\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188276)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.77598 0.0762329C1.32516 0.0762329 0.89281 0.255319 0.574034 0.574095C0.255258 0.892871 0.0761719 1.32522 0.0761719 1.77604V3.20075C0.0761719 3.61497 0.411959 3.95075 0.826172 3.95075C1.24039 3.95075 1.57617 3.61497 1.57617 3.20075V1.77604C1.57617 1.72305 1.59722 1.67223 1.6347 1.63476C1.67217 1.59728 1.72299 1.57623 1.77598 1.57623H3.20069C3.61491 1.57623 3.95069 1.24045 3.95069 0.826233C3.95069 0.412019 3.61491 0.0762329 3.20069 0.0762329H1.77598ZM3.99577 2.74559C3.30541 2.74559 2.74577 3.30524 2.74577 3.99559V6.06855C2.74577 6.75891 3.30541 7.31855 3.99577 7.31855H6.06873C6.75908 7.31855 7.31873 6.75891 7.31873 6.06855V3.99559C7.31873 3.30523 6.75908 2.74559 6.06873 2.74559H3.99577ZM4.24577 5.81855V4.24559H5.81873V5.81855H4.24577ZM10.0492 0.826309C10.0492 0.412096 10.385 0.0763092 10.7992 0.0763092H12.2239C12.6747 0.0763092 13.1071 0.255396 13.4259 0.574171C13.7446 0.892947 13.9237 1.3253 13.9237 1.77612V3.20083C13.9237 3.61504 13.5879 3.95083 13.1737 3.95083C12.7595 3.95083 12.4237 3.61504 12.4237 3.20083V1.77612C12.4237 1.72312 12.4027 1.6723 12.3652 1.63483C12.3277 1.59736 12.2769 1.57631 12.2239 1.57631H10.7992C10.385 1.57631 10.0492 1.24052 10.0492 0.826309ZM1.57623 10.7993C1.57623 10.3851 1.24045 10.0493 0.826232 10.0493C0.412019 10.0493 0.076233 10.3851 0.076233 10.7993V12.224C0.076233 12.6748 0.255319 13.1072 0.574095 13.426C0.89287 13.7447 1.32522 13.9238 1.77604 13.9238H3.20075C3.61497 13.9238 3.95075 13.588 3.95075 13.1738C3.95075 12.7596 3.61497 12.4238 3.20075 12.4238H1.77604C1.72305 12.4238 1.67223 12.4028 1.63476 12.3653C1.59729 12.3278 1.57623 12.277 1.57623 12.224V10.7993ZM13.1737 10.0493C13.5879 10.0493 13.9237 10.3851 13.9237 10.7993V12.224C13.9237 12.6748 13.7446 13.1072 13.4259 13.426C13.1071 13.7447 12.6747 13.9238 12.2239 13.9238H10.7992C10.385 13.9238 10.0492 13.588 10.0492 13.1738C10.0492 12.7596 10.385 12.4238 10.7992 12.4238H12.2239C12.2769 12.4238 12.3277 12.4028 12.3652 12.3653C12.4027 12.3278 12.4237 12.277 12.4237 12.224V10.7993C12.4237 10.3851 12.7595 10.0493 13.1737 10.0493ZM4.24582 9.19012C4.24582 8.77591 3.91003 8.44012 3.49582 8.44012C3.08161 8.44012 2.74582 8.77591 2.74582 9.19012V10.5042C2.74582 10.9184 3.08161 11.2542 3.49582 11.2542H4.80991C5.22412 11.2542 5.55991 10.9184 5.55991 10.5042C5.55991 10.09 5.22412 9.75421 4.80991 9.75421H4.24582V9.19012ZM5.70587 9.19012C5.70587 8.77591 6.04166 8.44012 6.45587 8.44012H6.99989C7.41411 8.44012 7.74989 8.77591 7.74989 9.19012V10.5042C7.74989 10.9184 7.41411 11.2542 6.99989 11.2542C6.58568 11.2542 6.24989 10.9184 6.24989 10.5042V9.91149C5.93583 9.82198 5.70587 9.53291 5.70587 9.19012ZM9.94007 3.49567C9.94007 3.08145 9.60428 2.74567 9.19007 2.74567C8.77585 2.74567 8.44007 3.08145 8.44007 3.49567V4.80976C8.44007 5.22397 8.77585 5.55976 9.19007 5.55976H10.5041C10.9184 5.55976 11.2541 5.22397 11.2541 4.80976C11.2541 4.39554 10.9184 4.05976 10.5041 4.05976H9.94007V3.49567ZM8.44007 6.99998C8.44007 6.58577 8.77585 6.24998 9.19007 6.24998H10.5041C10.9184 6.24998 11.2541 6.58577 11.2541 6.99998V7.63899C11.2541 8.0532 10.9184 8.38898 10.5041 8.38898C10.1276 8.38898 9.81595 8.11156 9.76231 7.74998H9.19007C8.77585 7.74998 8.44007 7.4142 8.44007 6.99998ZM9.94007 9.19012C9.94007 8.77591 9.60428 8.44012 9.19007 8.44012C8.77585 8.44012 8.44007 8.77591 8.44007 9.19012V10.5042C8.44007 10.9184 8.77585 11.2542 9.19007 11.2542H10.5041C10.9184 11.2542 11.2541 10.9184 11.2541 10.5042C11.2541 10.09 10.9184 9.75421 10.5041 9.75421H9.94007V9.19012Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188276\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"receipt\", \"keywords\": [ \"shop\", \"shopping\", \"pay\", \"payment\", \"store\", \"cash\", \"bill\", \"receipt\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188303)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.732233 0.732233C1.20107 0.263392 1.83696 0 2.5 0H12C12.5304 0 13.0391 0.210714 13.4142 0.585786C13.7893 0.960859 14 1.46957 14 2V5.5C14 5.77614 13.7761 6 13.5 6H12.2549V2.5C12.2549 2.15482 11.9751 1.875 11.6299 1.875C11.2847 1.875 11.0049 2.15482 11.0049 2.5V6H11V13.5C11 13.6801 10.9031 13.8463 10.7463 13.9351C10.5896 14.0239 10.3972 14.0214 10.2428 13.9287L8 12.5831L5.75725 13.9287C5.59891 14.0238 5.40109 14.0238 5.24275 13.9287L3 12.5831L0.757248 13.9287C0.602783 14.0214 0.410405 14.0239 0.253651 13.9351C0.0968963 13.8463 0 13.6801 0 13.5V2.5C0 1.83696 0.263392 1.20107 0.732233 0.732233ZM5.49997 2C5.91418 2 6.24997 2.33579 6.24997 2.75V3.27772C6.70721 3.35458 7.11206 3.5867 7.40722 3.91648C7.57893 4.10833 7.71458 4.33419 7.80266 4.58339C7.94069 4.97393 7.736 5.40242 7.34546 5.54046C6.95493 5.67849 6.52643 5.4738 6.3884 5.08326C6.36673 5.02195 6.33302 4.96545 6.28952 4.91685C6.19689 4.81336 6.06467 4.75 5.91666 4.75H4.94913C4.74711 4.75 4.58334 4.91377 4.58334 5.11578C4.58334 5.28768 4.70304 5.43638 4.87096 5.47312L6.34406 5.79536C7.26248 5.99626 7.91666 6.80991 7.91666 7.74952C7.91666 8.74023 7.19613 9.56353 6.24997 9.72231V10.25C6.24997 10.6642 5.91418 11 5.49997 11C5.08576 11 4.74997 10.6642 4.74997 10.25V9.72232C4.02648 9.60096 3.43581 9.09128 3.19734 8.41657C3.05931 8.02604 3.264 7.59755 3.65454 7.45951C4.04507 7.32148 4.47357 7.52617 4.6116 7.91671C4.6806 8.11191 4.86681 8.24997 5.08334 8.24997H5.91666C6.19244 8.24997 6.41666 8.02601 6.41666 7.74952C6.41666 7.51429 6.25279 7.31086 6.02351 7.26071L4.55042 6.93847C3.69386 6.7511 3.08334 5.99259 3.08334 5.11578C3.08334 4.15262 3.81317 3.35991 4.74997 3.2605V2.75C4.74997 2.33579 5.08576 2 5.49997 2Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188303\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"receipt-add\", \"keywords\": [ \"shop\", \"shopping\", \"pay\", \"payment\", \"store\", \"cash\", \"bill\", \"receipt\", \"add\", \"plus\", \"new\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188285)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.5 0C1.83696 0 1.20107 0.263392 0.732233 0.732233C0.263392 1.20107 0 1.83696 0 2.5V13.5C0 13.6801 0.0968963 13.8463 0.253651 13.9351C0.410405 14.0239 0.602783 14.0214 0.757248 13.9287L3 12.5831L5.24275 13.9287C5.40109 14.0238 5.59891 14.0238 5.75725 13.9287L8 12.5831L10.2428 13.9287C10.3972 14.0214 10.5896 14.0239 10.7463 13.9351C10.9031 13.8463 11 13.6801 11 13.5V6H11.0049V2.5C11.0049 2.15482 11.2847 1.875 11.6299 1.875C11.9751 1.875 12.2549 2.15482 12.2549 2.5V6H13.5C13.7761 6 14 5.77614 14 5.5V2C14 1.46957 13.7893 0.960859 13.4142 0.585786C13.0391 0.210714 12.5304 0 12 0H2.5ZM5.5 3.25C5.91421 3.25 6.25 3.58579 6.25 4V5.75H8C8.41421 5.75 8.75 6.08579 8.75 6.5C8.75 6.91421 8.41421 7.25 8 7.25H6.25V9C6.25 9.41421 5.91421 9.75 5.5 9.75C5.08579 9.75 4.75 9.41421 4.75 9V7.25H3C2.58579 7.25 2.25 6.91421 2.25 6.5C2.25 6.08579 2.58579 5.75 3 5.75H4.75V4C4.75 3.58579 5.08579 3.25 5.5 3.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188285\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"receipt-check\", \"keywords\": [ \"shop\", \"shopping\", \"pay\", \"payment\", \"store\", \"cash\", \"bill\", \"receipt\", \"check\", \"confirm\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188306)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.5 0C1.83696 0 1.20107 0.263392 0.732233 0.732233C0.263392 1.20107 0 1.83696 0 2.5V13.5C0 13.6801 0.0968963 13.8463 0.253651 13.9351C0.410405 14.0239 0.602783 14.0214 0.757248 13.9287L3 12.5831L5.24275 13.9287C5.40109 14.0238 5.59891 14.0238 5.75725 13.9287L8 12.5831L10.2428 13.9287C10.3972 14.0214 10.5896 14.0239 10.7463 13.9351C10.9031 13.8463 11 13.6801 11 13.5V6H11.0049V2.5C11.0049 2.15482 11.2847 1.875 11.6299 1.875C11.9751 1.875 12.2549 2.15482 12.2549 2.5V6H13.5C13.7761 6 14 5.77614 14 5.5V2C14 1.46957 13.7893 0.960859 13.4142 0.585786C13.0391 0.210714 12.5304 0 12 0H2.5ZM8.81443 4.99388C9.08719 4.68215 9.05561 4.20833 8.74388 3.93557C8.43215 3.66281 7.95833 3.69439 7.68557 4.00612L4.64358 7.48268L3.2 6.4C2.86863 6.15147 2.39853 6.21863 2.15 6.55C1.90147 6.88137 1.96863 7.35147 2.3 7.6L4.3 9.1C4.61385 9.33539 5.05609 9.28913 5.31443 8.99388L8.81443 4.99388Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188306\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"receipt-subtract\", \"keywords\": [ \"shop\", \"shopping\", \"pay\", \"payment\", \"store\", \"cash\", \"bill\", \"receipt\", \"subtract\", \"minus\", \"remove\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188312)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.5 0C1.83696 0 1.20107 0.263392 0.732233 0.732233C0.263392 1.20107 0 1.83696 0 2.5V13.5C0 13.6801 0.0968963 13.8463 0.253651 13.9351C0.410405 14.0239 0.602783 14.0214 0.757248 13.9287L3 12.5831L5.24275 13.9287C5.40109 14.0238 5.59891 14.0238 5.75725 13.9287L8 12.5831L10.2428 13.9287C10.3972 14.0214 10.5896 14.0239 10.7463 13.9351C10.9031 13.8463 11 13.6801 11 13.5V6H11.0049V2.5C11.0049 2.15482 11.2847 1.875 11.6299 1.875C11.9751 1.875 12.2549 2.15482 12.2549 2.5V6H13.5C13.7761 6 14 5.77614 14 5.5V2C14 1.46957 13.7893 0.960859 13.4142 0.585786C13.0391 0.210714 12.5304 0 12 0H2.5ZM3 5.75C2.58579 5.75 2.25 6.08579 2.25 6.5C2.25 6.91421 2.58579 7.25 3 7.25H8C8.41421 7.25 8.75 6.91421 8.75 6.5C8.75 6.08579 8.41421 5.75 8 5.75H3Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188312\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"safe-vault\", \"keywords\": [ \"saving\", \"combo\", \"payment\", \"safe\", \"combination\", \"finance\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188300)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0C0.671573 0 0 0.671573 0 1.5V11C0 11.8284 0.671573 12.5 1.5 12.5V13.2022C1.5 13.6164 1.83579 13.9522 2.25 13.9522C2.66421 13.9522 3 13.6164 3 13.2022V12.5H11V13.2022C11 13.6164 11.3358 13.9522 11.75 13.9522C12.1642 13.9522 12.5 13.6164 12.5 13.2022V12.5C13.3284 12.5 14 11.8284 14 11V1.5C14 0.671573 13.3284 0 12.5 0H1.5ZM8.74936 4.94285C8.02874 4.94319 7.44448 5.52665 7.44284 6.24697L7.44285 6.25L7.44284 6.25321C7.44458 6.97365 8.02914 7.55715 8.74999 7.55715C9.47191 7.55715 10.0571 6.97192 10.0571 6.25C10.0571 5.52808 9.47128 4.94285 8.74936 4.94285ZM8.125 3.76977C7.88018 3.83127 7.64938 3.92807 7.43831 4.05443L6.85127 3.46739C6.6072 3.22332 6.21147 3.22332 5.96739 3.46739C5.72331 3.71147 5.72331 4.1072 5.96739 4.35128L6.55443 4.93832C6.42806 5.14938 6.33127 5.38018 6.26977 5.625H5.43774C5.09257 5.625 4.81274 5.90482 4.81274 6.25C4.81274 6.59518 5.09257 6.875 5.43774 6.875H6.26977C6.33127 7.11982 6.42806 7.35062 6.55443 7.56169L5.96739 8.14872C5.72331 8.3928 5.72331 8.78853 5.96739 9.03261C6.21147 9.27668 6.6072 9.27668 6.85127 9.03261L7.43831 8.44557C7.64938 8.57193 7.88018 8.66873 8.125 8.73023V9.56226C8.125 9.90744 8.40482 10.1873 8.75 10.1873C9.09518 10.1873 9.375 9.90744 9.375 9.56226V8.73022C9.61981 8.66873 9.85061 8.57193 10.0617 8.44557L10.6487 9.03261C10.8928 9.27668 11.2885 9.27668 11.5326 9.03261C11.7767 8.78853 11.7767 8.3928 11.5326 8.14872L10.9456 7.56169C11.0719 7.35062 11.1687 7.11982 11.2302 6.875H12.0623C12.4074 6.875 12.6873 6.59518 12.6873 6.25C12.6873 5.90482 12.4074 5.625 12.0623 5.625H11.2302C11.1687 5.38018 11.0719 5.14938 10.9456 4.93831L11.5326 4.35128C11.7767 4.1072 11.7767 3.71147 11.5326 3.46739C11.2885 3.22332 10.8928 3.22332 10.6487 3.46739L10.0617 4.05443C9.85061 3.92807 9.61981 3.83128 9.375 3.76978V2.93774C9.375 2.59256 9.09518 2.31274 8.75 2.31274C8.40482 2.31274 8.125 2.59256 8.125 2.93774V3.76977ZM3.11826 4.1338C3.11826 3.78862 2.83843 3.5088 2.49326 3.5088C2.14808 3.5088 1.86826 3.78862 1.86826 4.1338V8.36621C1.86826 8.71138 2.14808 8.99121 2.49326 8.99121C2.83843 8.9912 3.11826 8.71138 3.11826 8.3662V4.1338Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188300\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"scanner-3\", \"keywords\": [ \"payment\", \"electronic\", \"cash\", \"dollar\", \"codes\", \"tags\", \"upc\", \"barcode\", \"qr\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188315)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.00061 2.41833C7.41482 2.41833 7.75061 2.75412 7.75061 3.16833V3.69605C8.20785 3.77292 8.6127 4.00504 8.90786 4.33481C9.07957 4.52666 9.21521 4.75252 9.3033 5.00173C9.44133 5.39227 9.23664 5.82076 8.8461 5.95879C8.45556 6.09683 8.02707 5.89213 7.88904 5.5016C7.86737 5.44029 7.83366 5.38379 7.79016 5.33518C7.69753 5.2317 7.56531 5.16834 7.4173 5.16834H6.44977C6.24775 5.16834 6.08398 5.33211 6.08398 5.53413C6.08398 5.70602 6.20367 5.85472 6.3716 5.89145L7.84469 6.21369C8.76312 6.4146 9.41729 7.22825 9.41729 8.16785C9.41729 9.15857 8.69676 9.98187 7.75061 10.1406V10.6683C7.75061 11.0825 7.41482 11.4183 7.00061 11.4183C6.58639 11.4183 6.25061 11.0825 6.25061 10.6683V10.1407C5.52711 10.0193 4.93645 9.50961 4.69798 8.83492C4.55994 8.44438 4.76464 8.01589 5.15517 7.87786C5.54571 7.73982 5.9742 7.9445 6.11224 8.33504C6.18123 8.53025 6.36745 8.6683 6.58397 8.6683H7.4173C7.69308 8.6683 7.91729 8.44435 7.91729 8.16785C7.91729 7.93262 7.75343 7.7292 7.52415 7.67904L6.05105 7.3568C5.1945 7.16944 4.58398 6.41093 4.58398 5.53413C4.58398 4.57096 5.3138 3.77824 6.25061 3.67884V3.16833C6.25061 2.75412 6.58639 2.41833 7.00061 2.41833Z\\\" fill=\\\"black\\\"/>\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.33879 0.09375C1.01428 0.09375 0.703067 0.22266 0.473606 0.452122C0.244144 0.681583 0.115234 0.9928 0.115234 1.31731V5.10577C0.115234 5.51998 0.45102 5.85577 0.865234 5.85577C1.27944 5.85577 1.61523 5.51998 1.61523 5.10577V1.59375H5.12725C5.54146 1.59375 5.87725 1.25796 5.87725 0.84375C5.87725 0.429536 5.54146 0.09375 5.12725 0.09375H1.33879ZM8.91601 0.09375C8.5018 0.09375 8.16601 0.429536 8.16601 0.84375C8.16601 1.25796 8.5018 1.59375 8.91601 1.59375H12.428V5.10577C12.428 5.51998 12.7638 5.85577 13.178 5.85577C13.5923 5.85577 13.928 5.51998 13.928 5.10577V1.31731C13.928 0.992802 13.7991 0.681584 13.5697 0.452122C13.3402 0.22266 13.029 0.09375 12.7045 0.09375H8.91601ZM13.178 8.14423C13.5923 8.14423 13.928 8.48001 13.928 8.89423V12.6827C13.928 13.0072 13.7991 13.3184 13.5697 13.5479C13.3402 13.7773 13.029 13.9062 12.7045 13.9062H8.91601C8.5018 13.9062 8.16601 13.5705 8.16601 13.1562C8.16601 12.742 8.5018 12.4062 8.91601 12.4062H12.428V8.89423C12.428 8.48001 12.7638 8.14423 13.178 8.14423ZM1.61523 8.89423C1.61523 8.48001 1.27944 8.14423 0.865234 8.14423C0.45102 8.14423 0.115234 8.48001 0.115234 8.89423V12.6827C0.115234 13.0072 0.244144 13.3184 0.473606 13.5479C0.703068 13.7773 1.01428 13.9062 1.33879 13.9062H5.12725C5.54146 13.9062 5.87725 13.5705 5.87725 13.1562C5.87725 12.742 5.54146 12.4062 5.12725 12.4062H1.61523V8.89423Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188315\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"scanner-bar-code\", \"keywords\": [ \"codes\", \"tags\", \"upc\", \"barcode\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188291)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.41759 0.135742C1.09394 0.135742 0.783548 0.264312 0.554692 0.493168C0.325836 0.722024 0.197266 1.03242 0.197266 1.35607V5.11869C0.197266 5.5329 0.533053 5.86869 0.947266 5.86869C1.36148 5.86869 1.69726 5.5329 1.69726 5.11869V1.63574H5.18021C5.59442 1.63574 5.93021 1.29996 5.93021 0.885742C5.93021 0.471529 5.59442 0.135742 5.18021 0.135742H1.41759ZM8.94281 0.135742C8.5286 0.135742 8.19281 0.471529 8.19281 0.885742C8.19281 1.29996 8.5286 1.63574 8.94281 1.63574H12.4257V5.11869C12.4257 5.5329 12.7615 5.86869 13.1757 5.86869C13.59 5.86869 13.9257 5.5329 13.9257 5.11869V1.35607C13.9257 1.03242 13.7972 0.722025 13.5683 0.493168C13.3395 0.264312 13.0291 0.135742 12.7054 0.135742H8.94281ZM13.1757 8.13131C13.59 8.13131 13.9257 8.4671 13.9257 8.88131V12.6439C13.9257 12.9676 13.7972 13.278 13.5683 13.5068C13.3395 13.7357 13.0291 13.8643 12.7054 13.8643H8.94281C8.5286 13.8643 8.19281 13.5285 8.19281 13.1143C8.19281 12.7 8.5286 12.3643 8.94281 12.3643H12.4257V8.88131C12.4257 8.4671 12.7615 8.13131 13.1757 8.13131ZM1.69726 8.88131C1.69726 8.4671 1.36148 8.13131 0.947266 8.13131C0.533053 8.13131 0.197266 8.4671 0.197266 8.88131V12.6439C0.197266 12.9676 0.325836 13.278 0.554692 13.5068C0.783549 13.7357 1.09394 13.8643 1.41759 13.8643H5.18021C5.59442 13.8643 5.93021 13.5285 5.93021 13.1143C5.93021 12.7 5.59442 12.3643 5.18021 12.3643H1.69726V8.88131ZM10.6694 3.28271C11.0146 3.28271 11.2944 3.56254 11.2944 3.90771V10.0923C11.2944 10.4374 11.0146 10.7173 10.6694 10.7173C10.3242 10.7173 10.0444 10.4374 10.0444 10.0923V3.90771C10.0444 3.56254 10.3242 3.28271 10.6694 3.28271ZM2.708 3.90771C2.708 3.56254 2.98783 3.28271 3.333 3.28271C3.67818 3.28271 3.958 3.56254 3.958 3.90771V10.0923C3.958 10.4374 3.67818 10.7173 3.333 10.7173C2.98783 10.7173 2.708 10.4374 2.708 10.0923V3.90771ZM8.42895 3.28271C8.77413 3.28271 9.05395 3.56254 9.05395 3.90771V7.51561C9.05395 7.86079 8.77413 8.14061 8.42895 8.14061C8.08377 8.14061 7.80395 7.86079 7.80395 7.51561V3.90771C7.80395 3.56254 8.08377 3.28271 8.42895 3.28271ZM7.80395 10.0923C7.80395 10.4374 8.08377 10.7173 8.42895 10.7173C8.77413 10.7173 9.05395 10.4374 9.05395 10.0923V9.81114C9.05395 9.46596 8.77413 9.18614 8.42895 9.18614C8.08377 9.18614 7.80395 9.46596 7.80395 9.81114V10.0923ZM5.82324 3.28271C6.16842 3.28271 6.44824 3.56254 6.44824 3.90771V7.51561C6.44824 7.86079 6.16842 8.14061 5.82324 8.14061C5.47806 8.14061 5.19824 7.86079 5.19824 7.51561V3.90771C5.19824 3.56254 5.47806 3.28271 5.82324 3.28271ZM5.19824 10.0923C5.19824 10.4374 5.47806 10.7173 5.82324 10.7173C6.16842 10.7173 6.44824 10.4374 6.44824 10.0923V9.81114C6.44824 9.46596 6.16842 9.18614 5.82324 9.18614C5.47806 9.18614 5.19824 9.46596 5.19824 9.81114V10.0923Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188291\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shelf\", \"keywords\": [ \"shelf\", \"drawer\", \"cabinet\", \"prodcut\", \"decoration\", \"furniture\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1 1.5C1 0.671573 1.67157 0 2.5 0H11.5C12.3284 0 13 0.671573 13 1.5V3.00195H1V1.5ZM1 4.25195V6.875H7H13V4.25195H1ZM6.375 8.125H1V12.5C1 13.3284 1.67157 14 2.5 14H6.375V8.125ZM7.625 14V8.125H13V12.5C13 13.3284 12.3284 14 11.5 14H7.625ZM9.50003 9.88123C9.84521 9.88123 10.125 10.161 10.125 10.5062V11.5062C10.125 11.8514 9.84521 12.1312 9.50003 12.1312C9.15485 12.1312 8.87503 11.8514 8.87503 11.5062V10.5062C8.87503 10.161 9.15485 9.88123 9.50003 9.88123ZM5.12503 10.5062C5.12503 10.161 4.84521 9.88123 4.50003 9.88123C4.15485 9.88123 3.87503 10.161 3.87503 10.5062V11.5062C3.87503 11.8514 4.15485 12.1312 4.50003 12.1312C4.84521 12.1312 5.12503 11.8514 5.12503 11.5062V10.5062Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"shopping-bag-hand-bag-2\", \"keywords\": [ \"shopping\", \"bag\", \"purse\", \"goods\", \"item\", \"products\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.14624 2.01256C6.47443 1.68437 6.91955 1.5 7.38368 1.5C7.84781 1.5 8.29292 1.68437 8.62111 2.01256C8.9493 2.34075 9.13368 2.78587 9.13368 3.25V3.91284H5.63368V3.25C5.63368 2.78587 5.81805 2.34075 6.14624 2.01256ZM4.13368 3.91284V3.25C4.13368 2.38805 4.47609 1.5614 5.08558 0.951903C5.69507 0.34241 6.52172 0 7.38368 0C8.24563 0 9.07228 0.34241 9.68177 0.951903C10.2913 1.5614 10.6337 2.38805 10.6337 3.25V3.91284H12.3837C12.6384 3.91284 12.8524 4.10429 12.8806 4.35742L13.7277 11.9525C13.73 11.973 13.7331 11.9967 13.7365 12.0233C13.7577 12.186 13.7927 12.4543 13.7386 12.713C13.6243 13.2597 13.2075 13.7085 12.6657 13.8591C12.4213 13.927 12.1434 13.9194 11.9742 13.9147C11.9388 13.9137 11.9082 13.9128 11.8837 13.9128H2.88368C2.85911 13.9128 2.82851 13.9137 2.79317 13.9147C2.62395 13.9194 2.3461 13.927 2.10163 13.8591C1.5599 13.7085 1.14301 13.2598 1.02874 12.713C0.97467 12.4543 1.00962 12.186 1.03081 12.0233C1.03427 11.9967 1.03737 11.973 1.03965 11.9525L1.88676 4.35742C1.91499 4.10429 2.12898 3.91284 2.38368 3.91284H4.13368Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"shopping-basket-1\", \"keywords\": [ \"shopping\", \"basket\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188309)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.1424 1.16739C4.22896 0.940225 4.36297 0.731505 4.53819 0.556288C4.86467 0.229804 5.30748 0.0463867 5.7692 0.0463867H8.25103C8.71275 0.0463867 9.15555 0.229804 9.48203 0.556288C9.65182 0.726078 9.78292 0.927329 9.86965 1.14634C10.4435 1.31718 10.9643 1.638 11.3765 2.07884C11.8796 2.61704 12.1935 3.3047 12.2704 4.03743L12.4324 5.51062C13.2718 5.57459 13.9108 6.34671 13.7998 7.19322L13.1068 12.4145C13.1046 12.4307 13.1024 12.4497 13.0999 12.4711C13.0848 12.601 13.0595 12.818 12.9779 13.0147C12.8022 13.4383 12.4324 13.7672 11.994 13.8946C11.7856 13.9551 11.5736 13.9533 11.4438 13.9521L11.3999 13.9519H2.66692C2.64934 13.9519 2.62878 13.9522 2.60571 13.9526C2.46408 13.955 2.22794 13.9589 2.00641 13.8946C1.56797 13.7672 1.19818 13.4383 1.02248 13.0147C0.928976 12.7892 0.901165 12.5209 0.884403 12.3592C0.880832 12.3247 0.877763 12.2951 0.874666 12.2718L0.213856 7.29308C0.198511 7.17746 0.175474 7.00248 0.200629 6.8113C0.282802 6.18679 0.760137 5.66628 1.38265 5.53697C1.44967 5.52305 1.51823 5.51371 1.58795 5.5093L1.75008 4.03545L1.75021 4.03421C1.83165 3.30499 2.14784 2.62196 2.65114 2.08802C3.06041 1.65384 3.5754 1.33735 4.1424 1.16739ZM4.29447 2.71249C4.08871 2.81188 3.9014 2.94849 3.74265 3.11691C3.4625 3.41411 3.28646 3.79425 3.24101 4.20013L3.09732 5.5064H10.9229L10.7792 4.19948L10.7787 4.19523C10.7361 3.78672 10.5613 3.40329 10.2808 3.10327C10.1237 2.93526 9.93826 2.79856 9.73444 2.6985C9.66327 2.81435 9.57861 2.92173 9.48203 3.01831C9.32038 3.17997 9.12846 3.3082 8.91724 3.39569C8.70603 3.48318 8.47964 3.52821 8.25103 3.52821H5.7692C5.30748 3.52821 4.86467 3.34479 4.53819 3.01831C4.44498 2.9251 4.36342 2.8224 4.29447 2.71249ZM4.3714 8.48822C4.3714 8.14305 4.09158 7.86322 3.7464 7.86322C3.40123 7.86322 3.1214 8.14305 3.1214 8.48822V10.97C3.1214 11.3152 3.40123 11.595 3.7464 11.595C4.09158 11.595 4.3714 11.3152 4.3714 10.97V8.48822ZM7.0101 7.86322C7.35528 7.86322 7.6351 8.14305 7.6351 8.48822V10.97C7.6351 11.3152 7.35528 11.595 7.0101 11.595C6.66492 11.595 6.3851 11.3152 6.3851 10.97V8.48822C6.3851 8.14305 6.66492 7.86322 7.0101 7.86322ZM10.8988 8.48822C10.8988 8.14305 10.619 7.86322 10.2738 7.86322C9.92863 7.86322 9.6488 8.14305 9.6488 8.48822V10.97C9.6488 11.3152 9.92863 11.595 10.2738 11.595C10.619 11.595 10.8988 11.3152 10.8988 10.97V8.48822Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188309\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shopping-basket-2\", \"keywords\": [ \"shopping\", \"basket\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188319)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.77682 1.58711C9.13646 1.3816 9.5946 1.50655 9.80011 1.86618L11.8765 5.49985H12.275C12.498 5.49616 12.7192 5.54228 12.9222 5.63488C13.1269 5.72826 13.308 5.8665 13.4521 6.03936C13.5961 6.21223 13.6995 6.41529 13.7544 6.6335C13.8092 6.85129 13.8144 7.0786 13.7697 7.29867L13.7694 7.29995L12.6698 12.7979L12.6694 12.7997C12.5993 13.1437 12.4108 13.4521 12.1367 13.6714C11.8641 13.8895 11.524 14.0056 11.175 13.9998H2.82397C2.475 14.0056 2.13488 13.8895 1.86226 13.6714C1.58818 13.4521 1.39967 13.1437 1.32955 12.7997L1.32917 12.7979L0.229587 7.29995L0.229359 7.29883C0.184555 7.07871 0.189758 6.85134 0.244591 6.6335C0.299517 6.41529 0.402833 6.21223 0.546886 6.03936C0.69094 5.8665 0.872039 5.72826 1.07677 5.63488C1.27981 5.54228 1.50093 5.49616 1.72401 5.49985H2.12243L4.19881 1.86618C4.40432 1.50655 4.86246 1.3816 5.2221 1.58711C5.58174 1.79261 5.70668 2.25075 5.50118 2.61039L3.85006 5.49985H10.1489L8.49774 2.61039C8.29224 2.25075 8.41718 1.79261 8.77682 1.58711Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188319\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shopping-cart-1\", \"keywords\": [ \"shopping\", \"cart\", \"checkout\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188331)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.84335 0.752707C3.80487 0.369437 3.4823 0.0776367 3.0971 0.0776367H0.769531C0.355318 0.0776367 0.0195312 0.413423 0.0195312 0.827637C0.0195312 1.24185 0.355318 1.57764 0.769531 1.57764H2.41864L2.67025 4.08355C2.67016 4.10107 2.67099 4.11866 2.67276 4.13626L3.15501 8.93107C3.15585 8.9394 3.1569 8.94772 3.15815 8.956C3.2099 9.29723 3.38323 9.60827 3.6462 9.83179C3.90838 10.0546 4.24205 10.1755 4.58606 10.1724H10.4374C10.7445 10.1853 11.0478 10.099 11.3022 9.926C11.5606 9.75029 11.7544 9.49482 11.854 9.19863L11.8545 9.19706L13.0928 5.47294L13.0931 5.47177C13.1641 5.25656 13.1829 5.02758 13.1481 4.80367C13.1134 4.58055 13.0264 4.36883 12.8942 4.18579C12.7558 3.99162 12.5711 3.8351 12.3568 3.73045C12.1455 3.62726 11.9121 3.57776 11.6773 3.58623H4.12786L3.84335 0.752707ZM11.4245 12.8221C11.4245 12.1716 10.8972 11.6443 10.2467 11.6443C9.59618 11.6443 9.06883 12.1716 9.06883 12.8221C9.06883 13.4726 9.59618 14 10.2467 14C10.8972 14 11.4245 13.4726 11.4245 12.8221ZM5.1007 11.6443C5.75121 11.6443 6.27855 12.1716 6.27855 12.8221C6.27855 13.4726 5.75121 14 5.1007 14C4.45018 14 3.92284 13.4726 3.92284 12.8221C3.92284 12.1716 4.45018 11.6443 5.1007 11.6443Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188331\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shopping-cart-2\", \"keywords\": [ \"shopping\", \"cart\", \"checkout\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188337)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.75 1.5H2.14647L2.52458 2.94042C2.52756 2.95178 2.53079 2.96301 2.53425 2.97411L3.50314 7.81856C3.50556 7.84463 3.50938 7.87084 3.51463 7.89709L3.93463 9.99709L3.93521 10C3.99285 10.2823 4.14626 10.5361 4.36949 10.7183C4.59268 10.9005 4.87195 11 5.16006 11H11.2501C11.6643 11 12.0001 10.6642 12.0001 10.25C12.0001 9.83579 11.6643 9.5 11.2501 9.5H5.36491L5.11491 8.25H12.1015L12.1119 8.25014C12.1471 8.25077 12.2474 8.25253 12.3393 8.2375C12.6744 8.18275 12.9613 7.95491 13.0949 7.64943C13.1494 7.52487 13.1762 7.3678 13.185 7.3165L13.1876 7.30155L13.9791 3.44041L13.9813 3.43002C14.0076 3.28615 14.0019 3.13824 13.9649 2.99675C13.8507 2.5607 13.4519 2.25166 13.0012 2.25H12.9993H3.89416L3.55951 0.975139C3.49812 0.702779 3.34723 0.458536 3.13057 0.281674C2.90738 0.0994786 2.62811 -2.348e-05 2.34 4.15605e-09H0.75C0.335786 4.15605e-09 0 0.335786 0 0.75C0 1.16421 0.335786 1.5 0.75 1.5ZM11.6294 12.8004C11.6294 12.166 11.1151 11.6517 10.4808 11.6517C9.84638 11.6517 9.33211 12.166 9.33211 12.8004C9.33211 13.4348 9.84638 13.949 10.4808 13.949C11.1151 13.949 11.6294 13.4348 11.6294 12.8004ZM5.48073 11.6517C6.1151 11.6517 6.62936 12.166 6.62936 12.8004C6.62936 13.4347 6.1151 13.949 5.48073 13.949C4.84635 13.949 4.33209 13.4347 4.33209 12.8004C4.33209 12.166 4.84635 11.6517 5.48073 11.6517Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188337\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shopping-cart-3\", \"keywords\": [ \"shopping\", \"cart\", \"checkout\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188355)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.84335 0.752707C3.80487 0.369437 3.4823 0.0776367 3.09711 0.0776367H0.769532C0.355318 0.0776367 0.0195312 0.413423 0.0195312 0.827637C0.0195312 1.24185 0.355318 1.57764 0.769532 1.57764H2.41864L2.67026 4.08355C2.67016 4.10107 2.67099 4.11866 2.67276 4.13626L3.15501 8.93107C3.15585 8.9394 3.1569 8.94772 3.15816 8.956C3.2099 9.29723 3.38323 9.60827 3.64621 9.83179C3.90838 10.0546 4.24205 10.1755 4.58606 10.1724H10.4374C10.7445 10.1853 11.0478 10.099 11.3022 9.926C11.5606 9.75029 11.7544 9.49482 11.854 9.19863L11.8545 9.19706L13.0928 5.47294L13.0932 5.47177C13.1641 5.25656 13.183 5.02758 13.1481 4.80367C13.1134 4.58055 13.0264 4.36883 12.8942 4.18579C12.7559 3.99162 12.5711 3.8351 12.3569 3.73045C12.1456 3.62726 11.9122 3.57776 11.6773 3.58623H4.12786L3.84335 0.752707ZM11.4245 12.8221C11.4245 12.1716 10.8972 11.6443 10.2467 11.6443C9.59618 11.6443 9.06884 12.1716 9.06884 12.8221C9.06884 13.4726 9.59618 14 10.2467 14C10.8972 14 11.4245 13.4726 11.4245 12.8221ZM5.1007 11.6443C5.75121 11.6443 6.27856 12.1716 6.27856 12.8221C6.27856 13.4726 5.75121 14 5.1007 14C4.45018 14 3.92284 13.4726 3.92284 12.8221C3.92284 12.1716 4.45018 11.6443 5.1007 11.6443ZM6.18701 5.375C6.53219 5.375 6.81201 5.65482 6.81201 6V8C6.81201 8.34518 6.53219 8.625 6.18701 8.625C5.84183 8.625 5.56201 8.34518 5.56201 8V6C5.56201 5.65482 5.84183 5.375 6.18701 5.375ZM10.312 6C10.312 5.65482 10.0322 5.375 9.68701 5.375C9.34183 5.375 9.06201 5.65482 9.06201 6V8C9.06201 8.34518 9.34183 8.625 9.68701 8.625C10.0322 8.625 10.312 8.34518 10.312 8V6Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188355\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shopping-cart-add\", \"keywords\": [ \"shopping\", \"cart\", \"checkout\", \"add\", \"plus\", \"new\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188328)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.50531 0.0776367C3.89051 0.0776367 4.21308 0.369437 4.25156 0.752707L4.53607 3.58623H12.0855C12.3204 3.57776 12.5538 3.62726 12.7651 3.73045C12.9794 3.8351 13.1641 3.99162 13.3025 4.18579C13.4346 4.36883 13.5217 4.58055 13.5564 4.80367C13.5912 5.02758 13.5724 5.25656 13.5014 5.47177L13.501 5.47294L12.2627 9.19706L12.2622 9.19863C12.1626 9.49482 11.9689 9.75029 11.7105 9.926C11.456 10.099 11.1527 10.1853 10.8456 10.1724H4.99426C4.65026 10.1755 4.31659 10.0546 4.05441 9.83179C3.79144 9.60827 3.61811 9.29723 3.56636 8.956C3.56511 8.94772 3.56406 8.9394 3.56322 8.93107L3.08097 4.13626C3.0792 4.11866 3.07837 4.10107 3.07846 4.08355L2.82685 1.57764H1.17774C0.763521 1.57764 0.427734 1.24185 0.427734 0.827637C0.427734 0.413423 0.763521 0.0776367 1.17774 0.0776367H3.50531ZM10.6549 11.6443C11.3054 11.6443 11.8328 12.1716 11.8328 12.8221C11.8328 13.4726 11.3054 14 10.6549 14C10.0044 14 9.47704 13.4726 9.47704 12.8221C9.47704 12.1716 10.0044 11.6443 10.6549 11.6443ZM6.68676 12.8221C6.68676 12.1716 6.15942 11.6443 5.5089 11.6443C4.85839 11.6443 4.33105 12.1716 4.33105 12.8221C4.33105 13.4726 4.85839 14 5.5089 14C6.15942 14 6.68676 13.4726 6.68676 12.8221ZM8.31231 5.06323C7.96714 5.06323 7.68731 5.34305 7.68731 5.68823V6.375H7.00049C6.65531 6.375 6.37549 6.65482 6.37549 7C6.37549 7.34518 6.65531 7.625 7.00049 7.625H7.68731V8.31182C7.68731 8.657 7.96714 8.93682 8.31231 8.93682C8.65749 8.93682 8.93731 8.657 8.93731 8.31182V7.625H9.62408C9.96926 7.625 10.2491 7.34518 10.2491 7C10.2491 6.65482 9.96926 6.375 9.62408 6.375H8.93731V5.68823C8.93731 5.34305 8.65749 5.06323 8.31231 5.06323Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188328\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shopping-cart-check\", \"keywords\": [ \"shopping\", \"cart\", \"checkout\", \"check\", \"confirm\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188322)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.50531 0.0776367C3.89051 0.0776367 4.21308 0.369437 4.25156 0.752707L4.53607 3.58623H12.0855C12.3204 3.57776 12.5538 3.62726 12.7651 3.73045C12.9794 3.8351 13.1641 3.99162 13.3025 4.18579C13.4346 4.36883 13.5217 4.58055 13.5564 4.80367C13.5912 5.02758 13.5724 5.25656 13.5014 5.47177L13.501 5.47294L12.2627 9.19706L12.2622 9.19863C12.1626 9.49482 11.9689 9.75029 11.7105 9.926C11.456 10.099 11.1527 10.1853 10.8456 10.1724H4.99426C4.65026 10.1755 4.31659 10.0546 4.05441 9.83179C3.79144 9.60827 3.61811 9.29723 3.56636 8.956C3.56511 8.94772 3.56406 8.9394 3.56322 8.93107L3.08097 4.13626C3.0792 4.11866 3.07837 4.10107 3.07846 4.08355L2.82685 1.57764H1.17774C0.763521 1.57764 0.427734 1.24185 0.427734 0.827637C0.427734 0.413423 0.763521 0.0776367 1.17774 0.0776367H3.50531ZM10.6549 11.6443C11.3054 11.6443 11.8328 12.1716 11.8328 12.8221C11.8328 13.4726 11.3054 14 10.6549 14C10.0044 14 9.47704 13.4726 9.47704 12.8221C9.47704 12.1716 10.0044 11.6443 10.6549 11.6443ZM6.68676 12.8221C6.68676 12.1716 6.15942 11.6443 5.5089 11.6443C4.85839 11.6443 4.33105 12.1716 4.33105 12.8221C4.33105 13.4726 4.85839 14 5.5089 14C6.15942 14 6.68676 13.4726 6.68676 12.8221ZM9.99198 6.1541C10.1989 5.87784 10.1427 5.48612 9.86647 5.27918C9.59021 5.07223 9.19849 5.12842 8.99155 5.40469L7.70284 7.12505L7.09277 6.66805C6.8165 6.4611 6.42478 6.5173 6.21784 6.79356C6.01089 7.06982 6.06708 7.46154 6.34335 7.66849L7.45364 8.50019C7.5863 8.59957 7.75301 8.64218 7.91709 8.61864C8.08118 8.59511 8.22919 8.50735 8.32856 8.37469L9.99198 6.1541Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188322\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shopping-cart-subtract\", \"keywords\": [ \"shopping\", \"cart\", \"checkout\", \"subtract\", \"minus\", \"remove\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188358)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.50531 0.0776367C3.89051 0.0776367 4.21308 0.369437 4.25156 0.752707L4.53607 3.58623H12.0855C12.3204 3.57776 12.5538 3.62726 12.7651 3.73045C12.9794 3.8351 13.1641 3.99162 13.3025 4.18579C13.4346 4.36883 13.5217 4.58055 13.5564 4.80367C13.5912 5.02758 13.5724 5.25656 13.5014 5.47177L13.501 5.47294L12.2627 9.19706L12.2622 9.19863C12.1626 9.49482 11.9689 9.75029 11.7105 9.926C11.456 10.099 11.1527 10.1853 10.8456 10.1724H4.99426C4.65026 10.1755 4.31659 10.0546 4.05441 9.83179C3.79144 9.60827 3.61811 9.29723 3.56636 8.956C3.56511 8.94772 3.56406 8.9394 3.56322 8.93107L3.08097 4.13626C3.0792 4.11866 3.07837 4.10107 3.07846 4.08355L2.82685 1.57764H1.17774C0.763521 1.57764 0.427734 1.24185 0.427734 0.827637C0.427734 0.413423 0.763521 0.0776367 1.17774 0.0776367H3.50531ZM10.6549 11.6443C11.3054 11.6443 11.8328 12.1716 11.8328 12.8221C11.8328 13.4726 11.3054 14 10.6549 14C10.0044 14 9.47704 13.4726 9.47704 12.8221C9.47704 12.1716 10.0044 11.6443 10.6549 11.6443ZM6.68676 12.8221C6.68676 12.1716 6.15942 11.6443 5.5089 11.6443C4.85839 11.6443 4.33105 12.1716 4.33105 12.8221C4.33105 13.4726 4.85839 14 5.5089 14C6.15942 14 6.68676 13.4726 6.68676 12.8221ZM9.62403 6.375C9.9692 6.375 10.249 6.65482 10.249 7C10.249 7.34518 9.9692 7.625 9.62403 7.625H7.00043C6.65526 7.625 6.37543 7.34518 6.37543 7C6.37543 6.65482 6.65526 6.375 7.00043 6.375H9.62403Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188358\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"signage-3\", \"keywords\": [ \"street\", \"sandwich\", \"shops\", \"shop\", \"stores\", \"board\", \"sign\", \"store\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.20101 13.4069L10.3225 5.06757L11.6702 10.3651H10.7825C10.3683 10.3651 10.0325 10.7009 10.0325 11.1151C10.0325 11.5293 10.3683 11.8651 10.7825 11.8651H12.0517L12.444 13.4069C12.5461 13.8083 12.9543 14.0509 13.3557 13.9488C13.7571 13.8467 13.9998 13.4385 13.8977 13.037L11.0503 1.84422C11.0484 1.83673 11.0464 1.82927 11.0443 1.82184L11.0386 1.80251C10.9868 1.63552 10.881 1.49857 10.7455 1.40613C10.7234 1.39103 10.7005 1.37713 10.677 1.3645C10.5652 1.30449 10.438 1.27256 10.3067 1.27545H3.67643C3.33343 1.27545 3.03414 1.50813 2.94958 1.84054L0.101283 13.037C0.0442093 13.2614 0.0939379 13.4995 0.236026 13.6823C0.378114 13.865 0.596637 13.9719 0.828131 13.9719H7.47416C7.81716 13.9719 8.11645 13.7393 8.20101 13.4069Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"signage-4\", \"keywords\": [ \"street\", \"billboard\", \"shops\", \"shop\", \"stores\", \"board\", \"sign\", \"ads\", \"banner\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188364)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.61499 0.40625V1.19165H2.6131C2.26792 1.19165 1.9881 1.47147 1.9881 1.81665C1.9881 2.16183 2.26792 2.44165 2.6131 2.44165H3.23209L3.23999 2.4417L3.24789 2.44165H3.86646C4.21163 2.44165 4.49146 2.16183 4.49146 1.81665C4.49146 1.47147 4.21163 1.19165 3.86646 1.19165H3.86499V0.40625H6.375V1.19165H6.37329C6.02811 1.19165 5.74829 1.47147 5.74829 1.81665C5.74829 2.16183 6.02811 2.44165 6.37329 2.44165H6.99234L7 2.4417L7.00766 2.44165H7.62665C7.97183 2.44165 8.25165 2.16183 8.25165 1.81665C8.25165 1.47147 7.97183 1.19165 7.62665 1.19165H7.625V0.40625H10.135V1.19165H10.1335C9.78837 1.19165 9.50854 1.47147 9.50854 1.81665C9.50854 2.16183 9.78837 2.44165 10.1335 2.44165H11.3869C11.7321 2.44165 12.0119 2.16183 12.0119 1.81665C12.0119 1.47147 11.7321 1.19165 11.3869 1.19165H11.385V0.406616L13.0802 0.40625C13.3563 0.40625 13.5802 0.630108 13.5802 0.90625V9.24156C13.5802 9.5177 13.3563 9.74156 13.0802 9.74156H10.896V13.2422C10.896 13.6564 10.5602 13.9922 10.146 13.9922C9.73178 13.9922 9.396 13.6564 9.396 13.2422V9.74156H4.604V13.2422C4.604 13.6564 4.26822 13.9922 3.854 13.9922C3.43979 13.9922 3.104 13.6564 3.104 13.2422V9.74156H0.919922C0.643779 9.74156 0.419922 9.5177 0.419922 9.24156V0.90625C0.419922 0.630108 0.64378 0.40625 0.919922 0.40625H2.61499ZM8.88013 6.11517C8.53495 6.11517 8.25513 6.395 8.25513 6.74017C8.25513 7.08535 8.53495 7.36517 8.88013 7.36517H11.3868C11.732 7.36517 12.0118 7.08535 12.0118 6.74017C12.0118 6.395 11.732 6.11517 11.3868 6.11517H8.88013Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188364\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"startup\", \"keywords\": [ \"shop\", \"rocket\", \"launch\", \"startup\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188334)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.54601 10.2633L3.73571 7.45301C4.04468 6.93561 4.35252 6.40063 4.65817 5.86944C5.67438 4.10339 6.66648 2.37925 7.59589 1.48246C10.1198 -1.04151 13.577 0.421965 13.577 0.421965C13.577 0.421965 15.0405 3.87916 12.5165 6.40312C11.6271 7.32489 9.93014 8.30213 8.17759 9.31136C7.63157 9.62578 7.08006 9.94338 6.54601 10.2633ZM8.68658 3.73075C8.68658 2.8572 9.39473 2.14905 10.2683 2.14905C11.1418 2.14905 11.85 2.8572 11.85 3.73075C11.85 4.6043 11.1418 5.31246 10.2683 5.31246C9.39473 5.31246 8.68658 4.6043 8.68658 3.73075ZM4.59568 3.49889C3.1778 3.12158 1.84735 3.82022 0.666867 4.90303C0.429419 5.12084 0.479009 5.50181 0.755008 5.66808L2.66025 6.81582L2.66245 6.81211C2.93729 6.35188 3.2445 5.81876 3.55576 5.27864C3.91117 4.66188 4.27198 4.03577 4.59568 3.49889ZM7.18322 11.3387L8.33097 13.2439C8.49724 13.5199 8.87822 13.5695 9.09602 13.3321C10.1792 12.1512 10.8779 10.8203 10.4998 9.40186C9.97845 9.71606 9.43022 10.0324 8.88716 10.3452L8.8043 10.3929C8.25576 10.7088 7.71291 11.0214 7.18836 11.3356L7.18322 11.3387ZM2.62115 9.3429C2.88969 9.33816 3.15643 9.38756 3.40547 9.48815C3.6545 9.58874 3.88073 9.73846 4.07064 9.92838C4.26056 10.1183 4.41028 10.3445 4.51087 10.5936C4.61146 10.8426 4.66086 11.1093 4.65612 11.3779C4.65138 11.6464 4.5926 11.9112 4.48329 12.1566C4.37454 12.4007 4.21799 12.6205 4.02289 12.803C3.80112 13.0154 3.4541 13.1815 3.13238 13.3093C2.78935 13.4457 2.39793 13.5675 2.03248 13.6672C1.66581 13.7673 1.3156 13.8478 1.05113 13.9003C0.919798 13.9263 0.803944 13.9466 0.715144 13.9587C0.672517 13.9645 0.625434 13.9699 0.581977 13.9718C0.562277 13.9727 0.526669 13.9738 0.485951 13.9699C0.466433 13.968 0.428646 13.9635 0.383709 13.9501C0.354047 13.9412 0.238821 13.9061 0.142826 13.7941C0.0606054 13.6982 0.038221 13.5981 0.0328285 13.574C0.0241841 13.5354 0.0215415 13.5027 0.0205415 13.4853C0.018467 13.4493 0.0200355 13.4174 0.021263 13.3984C0.0239258 13.3573 0.0295985 13.312 0.0357699 13.2696C0.0485436 13.1817 0.0692964 13.0668 0.0958806 12.9358C0.149351 12.6723 0.230533 12.3237 0.331114 11.9586C0.431344 11.5947 0.553418 11.2051 0.689896 10.8635C0.817979 10.5429 0.984049 10.1974 1.19604 9.97612C1.37858 9.78103 1.59837 9.62448 1.84244 9.51573C2.08777 9.40642 2.35261 9.34764 2.62115 9.3429Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188334\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"stock\", \"keywords\": [ \"price\", \"stock\", \"wallstreet\", \"dollar\", \"money\", \"currency\", \"fluctuate\", \"candlestick\", \"business\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188343)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.7984 0.125732C12.2126 0.125732 12.5484 0.461519 12.5484 0.875732V1.65635C12.782 1.71877 13.0039 1.81429 13.2016 1.94868C13.6264 2.23745 13.9193 2.69938 13.9193 3.27775C13.9193 3.69196 13.5836 4.02775 13.1693 4.02775C12.7551 4.02775 12.4193 3.69196 12.4193 3.27775C12.4193 3.25226 12.4144 3.2436 12.4127 3.24048C12.4098 3.23533 12.3974 3.21575 12.3583 3.1892C12.2728 3.13104 12.0902 3.06256 11.7983 3.06256C11.592 3.06256 11.4439 3.06317 11.319 3.07455C11.2625 3.0797 11.2214 3.08627 11.1919 3.09293C11.1849 3.12767 11.1773 3.18639 11.1773 3.27775C11.1773 3.35485 11.1982 3.43417 11.3491 3.56595C11.5263 3.72073 11.7828 3.8597 12.1464 4.05023L12.1813 4.06848C12.4988 4.2347 12.9094 4.44963 13.2342 4.73336C13.5975 5.05058 13.9193 5.51007 13.9193 6.15136C13.9193 6.46031 13.874 6.76654 13.739 7.04069C13.595 7.33317 13.3733 7.53922 13.1124 7.66823C12.9216 7.76253 12.7243 7.80862 12.5484 7.83305V8.55333C12.5484 8.96754 12.2126 9.30333 11.7984 9.30333C11.3842 9.30333 11.0484 8.96754 11.0484 8.55333V7.79105C10.8306 7.74174 10.6169 7.66357 10.4224 7.54342C9.94224 7.24666 9.67737 6.75505 9.67737 6.15136C9.67737 5.73715 10.0131 5.40136 10.4273 5.40136C10.8415 5.40136 11.1773 5.73715 11.1773 6.15136C11.1773 6.19172 11.1814 6.21782 11.1848 6.23269C11.1879 6.24634 11.1909 6.25199 11.1912 6.25259C11.1922 6.25369 11.1979 6.25936 11.211 6.26741C11.2789 6.30936 11.4529 6.36584 11.792 6.36654L11.7984 6.36652L11.8055 6.36655C12.0081 6.36653 12.1542 6.36581 12.2777 6.35456C12.3342 6.34941 12.3753 6.34284 12.4048 6.33618C12.4118 6.30144 12.4193 6.24272 12.4193 6.15136C12.4193 6.07426 12.3985 5.99494 12.2476 5.86316C12.0703 5.70838 11.8139 5.56941 11.4502 5.37888L11.4154 5.36063C11.0979 5.19441 10.6873 4.97949 10.3624 4.69575C9.99923 4.37853 9.67737 3.91904 9.67737 3.27775C9.67737 2.9688 9.72269 2.66257 9.85769 2.38842C10.0017 2.09594 10.2234 1.8899 10.4843 1.76088C10.6751 1.66657 10.8724 1.62048 11.0484 1.59605V0.875732C11.0484 0.461519 11.3842 0.125732 11.7984 0.125732ZM4.08203 4.46307C4.08203 3.71982 3.54146 3.10283 2.83203 2.98381V0.875732C2.83203 0.461519 2.49624 0.125732 2.08203 0.125732C1.66782 0.125732 1.33203 0.461519 1.33203 0.875732V2.98381C0.622602 3.10283 0.0820312 3.71982 0.0820312 4.46307V8.96307C0.0820312 9.70632 0.622602 10.3233 1.33203 10.4423V11.4631C1.33203 11.8773 1.66782 12.2131 2.08203 12.2131C2.49624 12.2131 2.83203 11.8773 2.83203 11.4631V10.4423C3.54146 10.3233 4.08203 9.70632 4.08203 8.96307V4.46307ZM6.33203 6.98381C5.6226 7.10283 5.08203 7.71982 5.08203 8.46307V9.96307C5.08203 10.7063 5.6226 11.3233 6.33203 11.4423V13.1245C6.33203 13.5387 6.66782 13.8745 7.08203 13.8745C7.49624 13.8745 7.83203 13.5387 7.83203 13.1245V11.4423C8.54146 11.3233 9.08203 10.7063 9.08203 9.96307V8.46307C9.08203 7.71982 8.54146 7.10283 7.83203 6.98381V3.96307C7.83203 3.54886 7.49624 3.21307 7.08203 3.21307C6.66782 3.21307 6.33203 3.54886 6.33203 3.96307V6.98381Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188343\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"store-1\", \"keywords\": [ \"store\", \"shop\", \"shops\", \"stores\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188340)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.99997 0C1.80584 0 1.62925 0.112374 1.54704 0.288238L0.0470579 3.49662C0.00950096 3.57695 -0.005416 3.66435 0.00173586 3.75H0V4.69541C0 5.21897 0.2264 5.72108 0.629395 6.09129C1.03239 6.4615 1.57897 6.66948 2.14889 6.66948H2.44973C3.01965 6.66948 3.56623 6.4615 3.96922 6.09129C4.13284 5.94099 4.26734 5.76895 4.3694 5.58255C4.49605 5.41275 4.70188 5.41392 4.82073 5.56943C4.92363 5.76087 5.06059 5.93747 5.22803 6.09129C5.63103 6.4615 6.1776 6.66948 6.74752 6.66948H7.28475C7.85467 6.66948 8.40124 6.4615 8.80424 6.09129C8.96079 5.94747 9.0907 5.78374 9.19103 5.60658C9.32213 5.40249 9.55182 5.4068 9.67096 5.59725C9.772 5.77799 9.9037 5.94495 10.063 6.09129C10.466 6.4615 11.0126 6.66948 11.5825 6.66948H11.8511C12.421 6.66948 12.9676 6.4615 13.3706 6.09129C13.7736 5.72108 14 5.21897 14 4.69541V3.75H13.9981C14.0052 3.66435 13.9903 3.57695 13.9527 3.49662L12.4528 0.288238C12.3705 0.112374 12.194 0 11.9998 0H1.99997ZM1 13V7.72853C2.18817 8.12118 3.60487 7.94625 4.57848 7.19297C5.87556 8.19652 8.12675 8.19653 9.42383 7.19298C10.4016 7.94946 11.8159 8.12122 13 7.71928V13C13 13.2652 12.8946 13.5196 12.7071 13.7071C12.5196 13.8947 12.2652 14 12 14H10.7445V10.2102C10.7445 10.152 10.7193 10.0962 10.6744 10.055C10.6295 10.0139 10.5687 9.99075 10.5052 9.99075H8.51271C8.44925 9.99075 8.38839 10.0139 8.34352 10.055C8.29865 10.0962 8.27344 10.152 8.27344 10.2102V14H2C1.73478 14 1.48043 13.8947 1.29289 13.7071C1.10536 13.5196 1 13.2652 1 13ZM2.50192 10.7609V9.50002C2.50192 9.22388 2.72578 9.00002 3.00192 9.00002H6.01593C6.29207 9.00002 6.51593 9.22388 6.51593 9.50002V10.7609C6.51593 11.037 6.29207 11.2609 6.01593 11.2609H3.00192C2.72578 11.2609 2.50192 11.037 2.50192 10.7609Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188340\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"store-2\", \"keywords\": [ \"store\", \"shop\", \"shops\", \"stores\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188349)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.26494 0.901554C1.50517 0.373909 2.03169 -0.00359809 2.62171 2.58685e-05H11.3783L11.3761 4.06802e-05L11.38 0.500026V2.58685e-05H11.3783C11.9683 -0.00359809 12.4948 0.373909 12.7351 0.901554L12.736 0.903571L13.6426 2.91858H0.357407L1.26403 0.903573L1.26494 0.901554ZM0 4.16858H14V5.53371C14 5.79763 13.9057 6.05968 13.7245 6.26021C13.5418 6.46242 13.2825 6.58708 13 6.58708H1C0.717478 6.58708 0.458162 6.46242 0.275457 6.26021C0.0942681 6.05968 0 5.79763 0 5.53371V4.16858ZM1 8.34634C1 8.07019 1.22386 7.84634 1.5 7.84634H12.5C12.7761 7.84634 13 8.07019 13 8.34634V13C13 13.2652 12.8946 13.5196 12.7071 13.7071C12.5196 13.8947 12.2652 14 12 14H10.7445V10.2102C10.7445 10.152 10.7193 10.0962 10.6744 10.055C10.6295 10.0139 10.5687 9.99073 10.5052 9.99073H8.51272C8.44926 9.99073 8.3884 10.0139 8.34352 10.055C8.29865 10.0962 8.27344 10.152 8.27344 10.2102V14H2C1.73478 14 1.48043 13.8947 1.29289 13.7071C1.10536 13.5196 1 13.2652 1 13V8.34634ZM2.50192 10V11.2609C2.50192 11.537 2.72578 11.7609 3.00192 11.7609H6.01593C6.29207 11.7609 6.51593 11.537 6.51593 11.2609V10C6.51593 9.72388 6.29207 9.50002 6.01593 9.50002H3.00192C2.72578 9.50002 2.50192 9.72388 2.50192 10Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188349\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"store-computer\", \"keywords\": [ \"store\", \"shop\", \"shops\", \"stores\", \"online\", \"computer\", \"website\", \"desktop\", \"app\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188367)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.18214 0.366521C2.25473 0.211266 2.41062 0.112061 2.582 0.112061H11.41C11.5814 0.112061 11.7372 0.211266 11.8098 0.366521L13.134 3.19892C13.1672 3.26985 13.1804 3.34701 13.174 3.42263H13.1758V4.25725C13.1758 4.71945 12.9759 5.16272 12.6201 5.48954C12.2643 5.81637 11.7818 5.99998 11.2787 5.99998H11.0416C10.5384 5.99998 10.0559 5.81637 9.70015 5.48954C9.55952 5.36035 9.44324 5.21296 9.35405 5.0534C9.24887 4.88527 9.0461 4.88147 8.93036 5.06164C8.84179 5.21804 8.7271 5.36258 8.58889 5.48954C8.23313 5.81637 7.7506 5.99998 7.24747 5.99998H6.7732C6.27007 5.99998 5.78754 5.81637 5.43177 5.48954C5.28395 5.35375 5.16304 5.19785 5.0722 5.02884C4.96728 4.89156 4.78557 4.89052 4.67377 5.04043C4.58366 5.20498 4.46492 5.35686 4.32048 5.48954C3.96472 5.81637 3.48219 5.99998 2.97906 5.99998H2.71347C2.21033 5.99998 1.72781 5.81637 1.37204 5.48954C1.01627 5.16272 0.816406 4.71945 0.816406 4.25725V3.42263H0.81794C0.811622 3.34701 0.824791 3.26985 0.857949 3.19892L2.18214 0.366521ZM1.66064 10.4999V7.07781C1.9982 7.19154 2.35446 7.24999 2.71347 7.24999H2.97906C2.98571 7.24999 2.99237 7.24997 2.99902 7.24993V10H10.999V7.24972C11.0132 7.2499 11.0274 7.24999 11.0416 7.24999H11.2787C11.6398 7.24999 11.9981 7.19087 12.3374 7.07582V10.4999C12.3374 11.0522 11.8897 11.4999 11.3374 11.4999H8.576L9.17605 13H9.99902C10.2751 13 10.499 13.2239 10.499 13.5C10.499 13.7761 10.2751 14 9.99902 14H8.83753H5.16052H3.99902C3.72288 14 3.49902 13.7761 3.49902 13.5C3.49902 13.2239 3.72288 13 3.99902 13H4.822L5.42204 11.4999H2.66064C2.10836 11.4999 1.66064 11.0522 1.66064 10.4999Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188367\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"subscription-cashflow\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188361)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.2127 2.53752C8.91903 1.6074 7.23449 1.24494 5.57696 1.68908C2.78152 2.43811 1.06874 5.21231 1.5948 8.0098C1.67134 8.41688 1.4034 8.80893 0.996318 8.88548C0.58924 8.96203 0.197182 8.69408 0.120633 8.28701C-0.549251 4.72465 1.62993 1.19377 5.18873 0.240189C7.38469 -0.348217 9.61568 0.176234 11.2842 1.46596L11.8965 0.853736C12.2114 0.538754 12.75 0.761838 12.75 1.20729V3.50018C12.75 3.77633 12.5262 4.00018 12.25 4.00018H9.95712C9.51167 4.00018 9.28858 3.46161 9.60357 3.14663L10.2127 2.53752ZM13.0041 5.11487C13.4111 5.03832 13.8032 5.30626 13.8797 5.71334C14.5496 9.2757 12.3705 12.8066 8.81165 13.7602C6.61562 14.3486 4.38455 13.8241 2.71598 12.5343L2.10362 13.1466C1.78863 13.4616 1.25006 13.2385 1.25006 12.7931V10.5002C1.25006 10.224 1.47392 10.0002 1.75006 10.0002H4.04296C4.48841 10.0002 4.71149 10.5387 4.39651 10.8537L3.78752 11.4627C5.08119 12.3929 6.76582 12.7554 8.42342 12.3113C11.2189 11.5622 12.9316 8.78804 12.4056 5.99055C12.329 5.58347 12.597 5.19142 13.0041 5.11487ZM7.62731 3.6571C7.62731 3.24289 7.29152 2.9071 6.87731 2.9071C6.4631 2.9071 6.12731 3.24289 6.12731 3.6571V3.9986C5.31895 4.13175 4.70233 4.8338 4.70233 5.67987C4.70233 6.48065 5.25991 7.17338 6.04219 7.34451L7.3017 7.62003C7.44777 7.65198 7.55234 7.78167 7.55234 7.93173C7.55234 8.10813 7.40927 8.25087 7.23359 8.25087H6.52108C6.38347 8.25087 6.26439 8.16325 6.22028 8.03844C6.08224 7.6479 5.65375 7.44321 5.26321 7.58124C4.87267 7.71928 4.66798 8.14777 4.80602 8.53831C5.01281 9.12339 5.51087 9.57211 6.12731 9.70815V10.0697C6.12731 10.4839 6.4631 10.8197 6.87731 10.8197C7.29152 10.8197 7.62731 10.4839 7.62731 10.0697V9.70813C8.44271 9.52812 9.05234 8.80073 9.05234 7.93173C9.05234 7.07729 8.45747 6.33738 7.62225 6.15468L6.36274 5.87916C6.26909 5.85867 6.20233 5.77574 6.20233 5.67987C6.20233 5.5672 6.29367 5.47586 6.40633 5.47586H7.23359C7.32792 5.47586 7.41191 5.51589 7.47135 5.5823C7.49925 5.61347 7.52068 5.6495 7.53439 5.6883C7.67243 6.07884 8.10092 6.28353 8.49146 6.1455C8.882 6.00746 9.08669 5.57897 8.94865 5.18843C8.86853 4.96174 8.74516 4.75635 8.58905 4.58193C8.34038 4.30409 8.00594 4.10241 7.62731 4.0187V3.6571Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188361\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"tag\", \"keywords\": [ \"codes\", \"tags\", \"tag\", \"product\", \"label\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188346)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.21967 1.28033C-0.0732233 0.987437 -0.0732233 0.512563 0.21967 0.21967C0.512563 -0.0732233 0.987437 -0.0732233 1.28033 0.21967L4.69964 3.63898L6.94395 1.39466C7.03165 1.30437 7.13811 1.23441 7.25586 1.18975C7.37572 1.14429 7.50424 1.12619 7.632 1.1368C7.64356 1.13776 7.65509 1.13912 7.66657 1.14088L11.2444 1.69061L12.6397 0.295354C12.9326 0.00246027 13.4075 0.00246027 13.7004 0.295354C13.9933 0.588247 13.9933 1.06312 13.7004 1.35601L12.3051 2.75127L12.8548 6.32915C12.8566 6.34063 12.858 6.35216 12.8589 6.36372C12.8695 6.49148 12.8514 6.62 12.806 6.73986C12.7613 6.85761 12.6913 6.96407 12.6011 7.05177L10.3567 9.29608L13.7803 12.7197C14.0732 13.0126 14.0732 13.4874 13.7803 13.7803C13.4874 14.0732 13.0126 14.0732 12.7197 13.7803L0.21967 1.28033ZM8.38992 11.2629L2.73282 5.6058L0.437082 7.90153C-0.0387884 8.37799 -0.0390074 9.19195 0.436863 9.66842L4.32708 13.5586C4.56146 13.7926 4.87938 13.9244 5.21064 13.9244C5.54189 13.9244 5.85959 13.7929 6.09397 13.5589L8.38992 11.2629ZM9.49336 5.29345C9.01644 5.29345 8.62982 4.90683 8.62982 4.42991C8.62982 3.953 9.01644 3.56638 9.49336 3.56638C9.97028 3.56638 10.3569 3.953 10.3569 4.42991C10.3569 4.90683 9.97028 5.29345 9.49336 5.29345Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188346\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"tall-hat\", \"keywords\": [ \"tall\", \"hat\", \"cloth\", \"clothing\", \"wearable\", \"magician\", \"gentleman\", \"accessories\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188370)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.12636 0.167331C2.22123 0.0608804 2.35704 0 2.49963 0H11.4996C11.6422 0 11.778 0.0608804 11.8729 0.167331C11.9677 0.273782 12.0127 0.415674 11.9963 0.557325L11.0364 8.87445H2.96282L2.00293 0.557325C1.98658 0.415674 2.03149 0.273782 2.12636 0.167331ZM3.10709 10.1245L3.38128 12.5002H0.773438C0.359224 12.5002 0.0234375 12.836 0.0234375 13.2502C0.0234375 13.6644 0.359224 14.0002 0.773438 14.0002H13.2258C13.64 14.0002 13.9758 13.6644 13.9758 13.2502C13.9758 12.836 13.64 12.5002 13.2258 12.5002H10.618L10.8921 10.1245H3.10709Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188370\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"target\", \"keywords\": [ \"shop\", \"bullseye\", \"arrow\", \"target\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188373)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.4859 0.378744C11.4423 0.203986 11.3079 0.0662386 11.1343 0.0181543C10.9607 -0.0299301 10.7747 0.019083 10.6473 0.146458L8.64727 2.14646C8.52308 2.27065 8.47316 2.45089 8.51575 2.62128L8.87656 4.06454C8.87173 4.06912 8.86695 4.07377 8.86222 4.07851L6.47006 6.47066C6.17717 6.76356 6.17717 7.23843 6.47006 7.53132C6.76296 7.82422 7.23783 7.82422 7.53072 7.53132L9.92288 5.13917C9.92772 5.13432 9.9325 5.12943 9.93717 5.12449L11.3796 5.48508C11.55 5.52768 11.7302 5.47776 11.8544 5.35357L13.8544 3.35356C13.9818 3.22619 14.0308 3.04014 13.9827 2.86654C13.9346 2.69295 13.7969 2.55863 13.6221 2.51494L11.9132 2.0877L11.4859 0.378744ZM7.318 0.758091C7.34671 1.17131 7.035 1.52956 6.62178 1.55827C5.57714 1.63084 4.57529 2.00176 3.73525 2.62696C2.89521 3.25216 2.2523 4.10537 1.88289 5.0852C1.51348 6.06504 1.43311 7.13032 1.65132 8.1545C1.86953 9.17867 2.37715 10.1187 3.11387 10.8628C3.8506 11.607 4.78545 12.124 5.80738 12.3525C6.82931 12.581 7.89534 12.5114 8.87884 12.1518C9.86234 11.7923 10.722 11.158 11.3556 10.3243C11.9892 9.49055 12.3702 8.49247 12.4533 7.44862C12.4861 7.03571 12.8475 6.72762 13.2604 6.76047C13.6733 6.79333 13.9814 7.1547 13.9485 7.56761C13.8427 8.89774 13.3573 10.1695 12.5499 11.2319C11.7425 12.2942 10.6471 13.1025 9.39387 13.5606C8.14065 14.0188 6.78226 14.1075 5.48008 13.8164C4.1779 13.5252 2.98666 12.8664 2.0479 11.9182C1.10913 10.9699 0.462305 9.77211 0.184253 8.46707C-0.0937991 7.16203 0.00861615 5.8046 0.479331 4.55605C0.950046 3.3075 1.76927 2.22031 2.83968 1.42365C3.9101 0.626994 5.1867 0.15435 6.51783 0.0618737C6.93104 0.0331665 7.28929 0.344874 7.318 0.758091ZM6.64626 4.05796C6.79726 4.44367 6.60699 4.87876 6.22128 5.02976C5.89306 5.15825 5.60205 5.36653 5.37456 5.63576C5.14707 5.90499 4.99027 6.22668 4.91836 6.57174C4.84644 6.91679 4.86166 7.27434 4.96266 7.61203C5.06365 7.94972 5.24723 8.25692 5.49678 8.50583C5.74634 8.75475 6.054 8.93753 6.39196 9.03765C6.72991 9.13778 7.08749 9.15208 7.43236 9.07928C7.77723 9.00647 8.09853 8.84885 8.36716 8.62067C8.6358 8.39248 8.84333 8.10094 8.97098 7.77239C9.12098 7.38629 9.55558 7.1949 9.94168 7.34491C10.3278 7.49492 10.5192 7.92951 10.3692 8.31561C10.1513 8.87655 9.7969 9.37433 9.33823 9.76392C8.87957 10.1535 8.33101 10.4226 7.7422 10.5469C7.15338 10.6712 6.54287 10.6468 5.96586 10.4759C5.38886 10.3049 4.86357 9.99284 4.43749 9.56786C4.01141 9.14287 3.69798 8.61838 3.52555 8.04182C3.35312 7.46526 3.32712 6.85481 3.44991 6.26568C3.5727 5.67654 3.84041 5.12731 4.22881 4.66764C4.61722 4.20797 5.11408 3.85236 5.67446 3.63298C6.06017 3.48198 6.49526 3.67225 6.64626 4.05796Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188373\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"target-3\", \"keywords\": [ \"shop\", \"bullseye\", \"shooting\", \"target\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188376)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.3231 6.25H11.4917C11.0775 6.25 10.7417 6.58579 10.7417 7C10.7417 7.41421 11.0775 7.75 11.4917 7.75H12.3231C11.9924 10.1184 10.1184 11.9924 7.75 12.3231V11.4917C7.75 11.0775 7.41421 10.7417 7 10.7417C6.58579 10.7417 6.25 11.0775 6.25 11.4917V12.3231C3.8816 11.9924 2.00757 10.1184 1.67691 7.75H2.50833C2.92255 7.75 3.25833 7.41421 3.25833 7C3.25833 6.58579 2.92255 6.25 2.50833 6.25H1.67691C2.00757 3.8816 3.8816 2.00757 6.25 1.67691V2.50833C6.25 2.92255 6.58579 3.25833 7 3.25833C7.41421 3.25833 7.75 2.92255 7.75 2.50833V1.67691C10.1184 2.00757 11.9924 3.8816 12.3231 6.25ZM0.125 7C0.125 3.20304 3.20304 0.125 7 0.125C10.797 0.125 13.875 3.20304 13.875 7C13.875 10.797 10.797 13.875 7 13.875C3.20304 13.875 0.125 10.797 0.125 7ZM4.64624 7C4.64624 7.41421 4.98203 7.75 5.39624 7.75H6.25V8.60366C6.25 9.01788 6.58579 9.35366 7 9.35366C7.41421 9.35366 7.75 9.01788 7.75 8.60366V7.75H8.60366C9.01788 7.75 9.35366 7.41421 9.35366 7C9.35366 6.58579 9.01788 6.25 8.60366 6.25H7.75V5.39624C7.75 4.98203 7.41421 4.64624 7 4.64624C6.58579 4.64624 6.25 4.98203 6.25 5.39624V6.25H5.39624C4.98203 6.25 4.64624 6.58579 4.64624 7Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188376\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"wallet\", \"keywords\": [ \"money\", \"payment\", \"finance\", \"wallet\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188379)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 4.35474C0 2.42174 1.567 0.854736 3.5 0.854736H10C10.2761 0.854736 10.5 1.07859 10.5 1.35474V2.75269H3.35547C3.01029 2.75269 2.73047 3.03251 2.73047 3.37769C2.73047 3.72286 3.01029 4.00269 3.35547 4.00269H10.9396C10.9594 4.00269 10.979 4.00176 10.9983 3.99996H11.2121C11.6099 3.99996 11.9914 4.15799 12.2727 4.4393C12.554 4.7206 12.7121 5.10213 12.7121 5.49996V6.41614H9.76276C8.93828 6.41614 8.01276 7.0409 8.01276 8.11074V9.88917C8.01276 10.959 8.93828 11.5838 9.76276 11.5838H12.7121V12.4999C12.7121 12.8978 12.554 13.2793 12.2727 13.5606C11.9914 13.8419 11.6099 13.9999 11.2121 13.9999L1.5 14C1.10217 14 0.720644 13.8419 0.43934 13.5606C0.158035 13.2793 0 12.8978 0 12.5V4.35474ZM9.76276 7.66614H13.2628C13.5389 7.66614 13.7628 7.86519 13.7628 8.11074V9.88917C13.7628 10.1347 13.5389 10.3338 13.2628 10.3338H9.76276C9.48661 10.3338 9.26276 10.1347 9.26276 9.88917V8.11074C9.26276 7.86519 9.48661 7.66614 9.76276 7.66614Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188379\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"wallet-purse\", \"keywords\": [ \"money\", \"payment\", \"finance\", \"wallet\", \"purse\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188382)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5 0.25C5.41421 0.25 5.75 0.585786 5.75 1C5.75 1.33152 5.8817 1.64946 6.11612 1.88388C6.35054 2.1183 6.66848 2.25 7 2.25C7.33152 2.25 7.64946 2.1183 7.88388 1.88388C8.1183 1.64946 8.25 1.33152 8.25 1C8.25 0.585786 8.58579 0.25 9 0.25C9.41421 0.25 9.75 0.585786 9.75 1C9.75 1.55718 9.58091 2.09692 9.27115 2.55063C10.9591 2.96272 12.5181 3.83015 13.7679 5.07835C14.0609 5.37106 14.0612 5.84593 13.7685 6.13901C13.4758 6.43208 13.0009 6.43238 12.7079 6.13967C11.1934 4.62707 9.14045 3.77747 6.99999 3.77747C4.85953 3.77747 2.80659 4.62707 1.29212 6.13967C0.99904 6.43238 0.524166 6.43208 0.231456 6.13901C-0.0612554 5.84593 -0.0609599 5.37106 0.232115 5.07835C1.48186 3.83015 3.04085 2.96271 4.72885 2.55062C4.41909 2.09692 4.25 1.55718 4.25 1C4.25 0.585786 4.58579 0.25 5 0.25ZM7 5C5.14348 5 3.36301 5.7375 2.05025 7.05025C0.737498 8.36301 0 10.1435 0 12C0 12.5304 0.210714 13.0391 0.585786 13.4142C0.960859 13.7893 1.46957 14 2 14H12C12.5304 14 13.0391 13.7893 13.4142 13.4142C13.7893 13.0391 14 12.5304 14 12C14 10.1435 13.2625 8.36301 11.9497 7.05025C10.637 5.7375 8.85652 5 7 5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188382\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"xrp-circle\", \"keywords\": [ \"crypto\", \"circle\", \"payment\", \"blockchain\", \"finance\", \"xrp\", \"currency\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188385)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14ZM3.70244 3.1313C3.94651 2.88722 4.34224 2.88722 4.58632 3.1313L6.63094 5.17592L6.63275 5.17773C6.68083 5.22622 6.73804 5.2647 6.80107 5.29096C6.8641 5.31721 6.93171 5.33074 6.99999 5.33074C7.06827 5.33074 7.13588 5.31721 7.19891 5.29096C7.26194 5.2647 7.31915 5.22622 7.36724 5.17773L7.36904 5.17592L9.41366 3.1313C9.65774 2.88722 10.0535 2.88722 10.2975 3.1313C10.5416 3.37538 10.5416 3.77111 10.2975 4.01518L8.25474 6.05799L8.2537 6.05903C8.08963 6.22418 7.89454 6.35529 7.67963 6.44482C7.46428 6.53454 7.23329 6.58074 6.99999 6.58074C6.7667 6.58074 6.53571 6.53454 6.32035 6.44482C6.10544 6.35529 5.91034 6.22417 5.74626 6.05901L3.70244 4.01518C3.45836 3.77111 3.45836 3.37538 3.70244 3.1313ZM6.80107 8.70903C6.8641 8.68277 6.93171 8.66925 6.99999 8.66925C7.06827 8.66925 7.13588 8.68277 7.19891 8.70903C7.26194 8.73529 7.31915 8.77377 7.36723 8.82225L7.36904 8.82407L9.41366 10.8687C9.65774 11.1128 10.0535 11.1128 10.2975 10.8687C10.5416 10.6246 10.5416 10.2289 10.2975 9.9848L8.25474 7.942L8.25372 7.94098C8.08965 7.77582 7.89455 7.6447 7.67963 7.55516C7.46428 7.46544 7.23329 7.41925 6.99999 7.41925C6.7667 7.41925 6.53571 7.46544 6.32035 7.55516C6.10543 7.6447 5.91032 7.77583 5.74624 7.941L3.70244 9.9848C3.45836 10.2289 3.45836 10.6246 3.70244 10.8687C3.94651 11.1128 4.34224 11.1128 4.58632 10.8687L6.63094 8.82407L6.63275 8.82225C6.68083 8.77377 6.73804 8.73529 6.80107 8.70903Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188385\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"yuan\", \"keywords\": [ \"exchange\", \"payment\", \"forex\", \"finance\", \"yuan\", \"foreign\", \"currency\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.80006 0.400059C3.46869 -0.0417689 2.84189 -0.131312 2.40006 0.200059C1.95823 0.53143 1.86869 1.15823 2.20006 1.60006L6.11676 6.82232V6.84314H3.91491C3.36263 6.84314 2.91491 7.29085 2.91491 7.84314C2.91491 8.39542 3.36263 8.84314 3.91491 8.84314H6.11676V10.0885H3.91491C3.36263 10.0885 2.91491 10.5362 2.91491 11.0885C2.91491 11.6408 3.36263 12.0885 3.91491 12.0885H6.11676V12.8926C6.11676 13.4449 6.56447 13.8926 7.11676 13.8926C7.66904 13.8926 8.11676 13.4449 8.11676 12.8926V12.0885H10.3186C10.8709 12.0885 11.3186 11.6408 11.3186 11.0885C11.3186 10.5362 10.8709 10.0885 10.3186 10.0885H8.11676V8.84314H10.3186C10.8709 8.84314 11.3186 8.39542 11.3186 7.84314C11.3186 7.29085 10.8709 6.84314 10.3186 6.84314H8.11676V6.82227L12.0334 1.60006C12.3648 1.15823 12.2752 0.53143 11.8334 0.200059C11.3916 -0.131312 10.7648 -0.0417689 10.4334 0.400059L7.11674 4.8223L3.80006 0.400059Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"yuan-circle\", \"keywords\": [ \"exchange\", \"payment\", \"forex\", \"finance\", \"yuan\", \"foreign\", \"currency\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_188388)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14ZM9.8702 2.99904C10.1467 3.20567 10.2033 3.59733 9.99672 3.87383L7.70581 6.93944V6.95837H8.95933C9.3045 6.95837 9.58433 7.2382 9.58433 7.58337C9.58433 7.92855 9.3045 8.20837 8.95933 8.20837H7.70581V9.07025H8.95933C9.3045 9.07025 9.58433 9.35007 9.58433 9.69525C9.58433 10.0404 9.3045 10.3203 8.95933 10.3203H7.70581V10.7787C7.70581 11.1239 7.42599 11.4037 7.08081 11.4037C6.73563 11.4037 6.45581 11.1239 6.45581 10.7787V10.3203H5.19067C4.8455 10.3203 4.56567 10.0404 4.56567 9.69525C4.56567 9.35007 4.8455 9.07025 5.19067 9.07025H6.45581V8.20837H5.19067C4.8455 8.20837 4.56567 7.92855 4.56567 7.58337C4.56567 7.2382 4.8455 6.95837 5.19067 6.95837H6.45581V6.9401L4.15421 3.87498C3.94694 3.59896 4.00268 3.20718 4.2787 2.99991C4.55473 2.79265 4.94651 2.84838 5.15378 3.12441L7.07966 5.68916L8.99542 3.12556C9.20205 2.84906 9.5937 2.79242 9.8702 2.99904Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_188388\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" } ], \"nature_ecology\": [ { \"name\": \"affordable-and-clean-energy\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187254)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.86322 0.0144043C7.27743 0.0144043 7.61322 0.350191 7.61322 0.764404V1.41673C7.61322 1.83095 7.27743 2.16673 6.86322 2.16673C6.449 2.16673 6.11322 1.83095 6.11322 1.41673V0.764404C6.11322 0.350191 6.449 0.0144043 6.86322 0.0144043ZM11.1506 6.86193C11.1506 9.23054 9.23048 11.1507 6.86187 11.1507C4.49326 11.1507 2.57312 9.23054 2.57312 6.86193C2.57312 4.49332 4.49326 2.57318 6.86187 2.57318C9.23048 2.57318 11.1506 4.49332 11.1506 6.86193ZM7.61322 12.3063C7.61322 11.8921 7.27743 11.5563 6.86322 11.5563C6.449 11.5563 6.11322 11.8921 6.11322 12.3063V12.9586C6.11322 13.3728 6.449 13.7086 6.86322 13.7086C7.27743 13.7086 7.61322 13.3728 7.61322 12.9586V12.3063ZM0.015625 6.862C0.015625 6.44779 0.351412 6.112 0.765626 6.112H1.41795C1.83217 6.112 2.16795 6.44779 2.16795 6.862C2.16795 7.27621 1.83217 7.612 1.41795 7.612H0.765626C0.351412 7.612 0.015625 7.27621 0.015625 6.862ZM12.3075 6.112C11.8933 6.112 11.5575 6.44779 11.5575 6.862C11.5575 7.27621 11.8933 7.612 12.3075 7.612H12.9598C13.374 7.612 13.7098 7.27621 13.7098 6.862C13.7098 6.44779 13.374 6.112 12.9598 6.112H12.3075ZM11.7474 1.97626C12.0403 2.26916 12.0403 2.74403 11.7474 3.03692L11.3134 3.47087C11.0205 3.76376 10.5457 3.76376 10.2528 3.47087C9.95985 3.17798 9.95985 2.7031 10.2528 2.41021L10.6867 1.97626C10.9796 1.68337 11.4545 1.68337 11.7474 1.97626ZM3.47209 11.3122C3.76498 11.0193 3.76498 10.5445 3.47209 10.2516C3.1792 9.95867 2.70432 9.95867 2.41143 10.2516L1.97748 10.6855C1.68459 10.9784 1.68459 11.4533 1.97748 11.7462C2.27037 12.0391 2.74525 12.0391 3.03814 11.7462L3.47209 11.3122ZM1.97748 1.97626C2.27037 1.68337 2.74525 1.68337 3.03814 1.97626L3.47209 2.41021C3.76498 2.7031 3.76498 3.17798 3.47209 3.47087C3.1792 3.76376 2.70432 3.76376 2.41143 3.47087L1.97748 3.03692C1.68459 2.74403 1.68459 2.26916 1.97748 1.97626ZM11.3134 10.2516C11.0205 9.95867 10.5457 9.95867 10.2528 10.2516C9.95985 10.5445 9.95985 11.0193 10.2528 11.3122L10.6867 11.7462C10.9796 12.0391 11.4545 12.0391 11.7474 11.7462C12.0403 11.4533 12.0403 10.9784 11.7474 10.6855L11.3134 10.2516ZM6.86358 3.54614C7.20876 3.54614 7.48858 3.82596 7.48858 4.17114V5.93749C7.48858 6.28267 7.20876 6.56249 6.86358 6.56249C6.51841 6.56249 6.23858 6.28267 6.23858 5.93749V4.17114C6.23858 3.82596 6.5184 3.54614 6.86358 3.54614ZM5.36945 5.73557C5.57673 5.45955 5.52101 5.06777 5.24499 4.86049C4.96898 4.65321 4.57719 4.70894 4.36991 4.98495C3.97897 5.50555 3.74707 6.15356 3.74707 6.85393C3.74707 8.57419 5.14161 9.96873 6.86187 9.96873C8.58213 9.96873 9.97668 8.57419 9.97668 6.85393C9.97668 6.15356 9.74478 5.50555 9.35383 4.98495C9.14655 4.70894 8.75477 4.65321 8.47875 4.86049C8.20274 5.06777 8.14701 5.45955 8.35429 5.73557C8.58828 6.04715 8.72668 6.43347 8.72668 6.85393C8.72668 7.88383 7.89178 8.71873 6.86187 8.71873C5.83197 8.71873 4.99707 7.88383 4.99707 6.85393C4.99707 6.43347 5.13547 6.04715 5.36945 5.73557Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187254\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"alien\", \"keywords\": [ \"science\", \"extraterristerial\", \"life\", \"form\", \"space\", \"universe\", \"head\", \"astronomy\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187221)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7 0C5.1517 0 3.41338 0.361019 2.12226 1.24052C0.803257 2.13901 0 3.54704 0 5.5C0 7.41911 0.782881 9.5233 2.02095 11.1492C3.2556 12.7705 5.01161 14 7 14C8.98839 14 10.7444 12.7705 11.979 11.1492C13.2171 9.5233 14 7.41911 14 5.5C14 3.54704 13.1967 2.13901 11.8777 1.24052C10.5866 0.361019 8.8483 0 7 0ZM2.52082 4.16893C2.95839 4.07473 3.41066 4.07159 3.84949 4.15971C4.28832 4.24784 4.70431 4.42534 5.07158 4.68118C5.09585 4.69808 5.11692 4.71915 5.13383 4.74342C5.38966 5.11069 5.56716 5.52668 5.65529 5.96551C5.74341 6.40434 5.74027 6.85661 5.64607 7.29418C5.62539 7.39024 5.55035 7.46528 5.45429 7.48596C5.01672 7.58016 4.56445 7.5833 4.12562 7.49518C3.68679 7.40705 3.2708 7.22955 2.90353 6.97372C2.87926 6.95681 2.85819 6.93574 2.84129 6.91147C2.58545 6.5442 2.40795 6.12821 2.31983 5.68938C2.2317 5.25055 2.23484 4.79828 2.32904 4.36071C2.34973 4.26465 2.42476 4.18961 2.52082 4.16893ZM10.1505 4.15971C10.5894 4.07159 11.0416 4.07473 11.4792 4.16893C11.5753 4.18961 11.6503 4.26465 11.671 4.36071C11.7652 4.79828 11.7683 5.25055 11.6802 5.68938C11.5921 6.12821 11.4146 6.5442 11.1587 6.91147C11.1418 6.93574 11.1208 6.95681 11.0965 6.97372C10.7292 7.22955 10.3132 7.40705 9.87439 7.49518C9.43556 7.5833 8.98329 7.58016 8.54573 7.48596C8.44967 7.46528 8.37463 7.39024 8.35395 7.29418C8.25974 6.85661 8.2566 6.40434 8.34473 5.96551C8.43285 5.52668 8.61036 5.11069 8.86619 4.74342C8.88309 4.71915 8.90417 4.69808 8.92843 4.68118C9.2957 4.42534 9.7117 4.24784 10.1505 4.15971Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187221\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bone\", \"keywords\": [ \"nature\", \"pet\", \"dog\", \"bone\", \"food\", \"snack\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187239)\\\">\\n<path d=\\\"M11.8462 2.15385C11.8462 1.58261 11.6192 1.03477 11.2153 0.630847C10.8114 0.226922 10.2635 0 9.69231 0C9.12107 0 8.57323 0.226922 8.16931 0.630847C7.76538 1.03477 7.53846 1.58261 7.53846 2.15385C7.54087 2.72416 7.76937 3.27024 8.17385 3.67231L3.67231 8.17385C3.27024 7.76937 2.72416 7.54087 2.15385 7.53846C1.58261 7.53846 1.03477 7.76538 0.630847 8.16931C0.226922 8.57323 0 9.12107 0 9.69231C0 10.2635 0.226922 10.8114 0.630847 11.2153C1.03477 11.6192 1.58261 11.8462 2.15385 11.8462C2.15385 12.4174 2.38077 12.9652 2.78469 13.3692C3.18862 13.7731 3.73646 14 4.30769 14C4.87893 14 5.42677 13.7731 5.83069 13.3692C6.23462 12.9652 6.46154 12.4174 6.46154 11.8462C6.45913 11.2758 6.23063 10.7298 5.82615 10.3277L10.3277 5.82615C10.7298 6.23063 11.2758 6.45913 11.8462 6.46154C12.4174 6.46154 12.9652 6.23462 13.3692 5.83069C13.7731 5.42677 14 4.87893 14 4.30769C14 3.73646 13.7731 3.18862 13.3692 2.78469C12.9652 2.38077 12.4174 2.15385 11.8462 2.15385Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187239\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cat-1\", \"keywords\": [ \"nature\", \"head\", \"cat\", \"pet\", \"animals\", \"felyne\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187236)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.77703 0.0526343C1.94659 -0.032 2.14943 -0.0134972 2.30088 0.10042L6.21786 3.04666C6.47457 3.01584 6.7357 3 7.00032 3C7.26494 3 7.52606 3.01584 7.78275 3.04666L11.6997 0.10042C11.8512 -0.0134972 12.054 -0.032 12.2236 0.0526343C12.3931 0.137269 12.5003 0.310492 12.5003 0.500003V6.17651V6.1822V8.75H13.2487C13.6629 8.75 13.9987 9.08579 13.9987 9.5C13.9987 9.91421 13.6629 10.25 13.2487 10.25H12.4381C12.3549 10.7459 12.1906 11.2142 11.9593 11.641L13.3357 12.3292C13.7062 12.5144 13.8564 12.9649 13.6711 13.3354C13.4859 13.7059 13.0354 13.8561 12.6649 13.6708L11.0115 12.8441C10.214 13.5626 9.15825 14 8.00031 14H6.00032C4.84239 14 3.78661 13.5626 2.98915 12.8441L1.33573 13.6708C0.965245 13.8561 0.51474 13.7059 0.329498 13.3354C0.144256 12.9649 0.294424 12.5144 0.664908 12.3292L2.0413 11.641C1.81002 11.2142 1.64573 10.7459 1.56254 10.25H0.751953C0.337739 10.25 0.00195312 9.91421 0.00195312 9.5C0.00195312 9.08579 0.337739 8.75 0.751953 8.75H1.50032V6.18221V0.500003C1.50032 0.310492 1.60746 0.137269 1.77703 0.0526343ZM9.1243 7.75C9.53852 7.75 9.8743 7.41421 9.8743 7C9.8743 6.58579 9.53852 6.25 9.1243 6.25C8.71009 6.25 8.3743 6.58579 8.3743 7C8.3743 7.41421 8.71009 7.75 9.1243 7.75ZM4.87431 7.75031C5.28852 7.75031 5.62431 7.41452 5.62431 7.00031C5.62431 6.58609 5.28852 6.25031 4.87431 6.25031C4.4601 6.25031 4.12431 6.58609 4.12431 7.00031C4.12431 7.41452 4.4601 7.75031 4.87431 7.75031ZM5.77178 9.35512C5.83563 9.14424 6.02999 9.00002 6.25032 9.00002H7.75032C7.97065 9.00002 8.16502 9.14424 8.22887 9.35512C8.29272 9.566 8.21099 9.79382 8.02767 9.91604L7.27767 10.416C7.10971 10.528 6.89092 10.528 6.72297 10.416L5.97297 9.91604C5.78964 9.79382 5.70793 9.566 5.77178 9.35512Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187236\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"circle-flask\", \"keywords\": [ \"science\", \"experiment\", \"lab\", \"flask\", \"chemistry\", \"solution\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.4563 0.00500488C4.04209 0.00500488 3.7063 0.340791 3.7063 0.755004C3.7063 1.16922 4.04209 1.50501 4.4563 1.50501H5.00001V4.41235C4.0344 4.83375 3.22346 5.55103 2.68729 6.46502C2.0905 7.48238 1.87256 8.67795 2.07202 9.84045C2.27147 11.003 2.87546 12.0575 3.77724 12.8178C4.67902 13.578 5.82053 13.995 7.00001 13.995C8.17949 13.995 9.321 13.578 10.2227 12.8178C11.1245 12.0575 11.7285 11.003 11.928 9.84045C12.1274 8.67795 11.9095 7.48238 11.3127 6.46502C10.7765 5.55103 9.96562 4.83375 9.00001 4.41235V1.50501H9.54365C9.95786 1.50501 10.2936 1.16922 10.2936 0.755004C10.2936 0.340791 9.95786 0.00500488 9.54365 0.00500488H4.4563Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"clean-water-and-sanitation\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.19749 2.02196C2.06578 0.968282 2.88737 0.0375977 3.94925 0.0375977H9.77198C10.8339 0.0375977 11.6555 0.968282 11.5238 2.02196L10.7622 8.11434C10.6518 8.99779 9.90076 9.66076 9.01043 9.66076H7.49706V10.9651H8.21021C8.51356 10.9651 8.78704 11.1478 8.90312 11.4281C9.01921 11.7083 8.95504 12.0309 8.74054 12.2454L7.27954 13.7064C7.27216 13.7139 7.26463 13.7211 7.25696 13.7283C7.19406 13.7866 7.12359 13.8322 7.04887 13.8651C6.95652 13.9057 6.85443 13.9282 6.74706 13.9282C6.63646 13.9282 6.53144 13.9043 6.43692 13.8613C6.37192 13.8318 6.31026 13.7926 6.25416 13.7435C6.24 13.7312 6.2263 13.7183 6.2131 13.7049L4.7536 12.2454C4.5391 12.0309 4.47493 11.7083 4.59102 11.4281C4.7071 11.1478 4.98058 10.9651 5.28393 10.9651H5.99706V9.66076H4.7108C3.82047 9.66076 3.06947 8.99779 2.95904 8.11434L2.19749 2.02196ZM5.35032 5.21982C5.00851 5.29084 4.61151 5.33027 4.12001 5.30877L3.68591 1.83591C3.66611 1.67751 3.78962 1.5376 3.94925 1.5376H9.77198C9.93161 1.5376 10.0552 1.67751 10.0354 1.83591L9.74646 4.14683C9.15119 4.11271 8.65109 4.15363 8.20612 4.24263C7.59965 4.36392 7.1151 4.5716 6.6768 4.75945L6.66346 4.76517C6.22613 4.9526 5.8339 5.11934 5.35032 5.21982Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"comet\", \"keywords\": [ \"nature\", \"meteor\", \"fall\", \"space\", \"object\", \"danger\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187212)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.77828 0.217624C9.0723 0.509383 9.07413 0.984253 8.78238 1.27828L6.10602 3.97541C6.01485 4.04424 5.95593 4.15352 5.95593 4.27656C5.95593 4.48484 6.12478 4.65369 6.33306 4.65369C6.43826 4.65369 6.53339 4.61062 6.6018 4.54115L6.60293 4.54228L10.9255 0.21967C11.2184 -0.0732233 11.6933 -0.0732233 11.9862 0.21967C12.2791 0.512563 12.2791 0.987437 11.9862 1.28033L7.69802 5.56851C7.56412 5.65567 7.47559 5.80667 7.47559 5.97836C7.47559 6.24824 7.69437 6.46703 7.96426 6.46703C8.11824 6.46703 8.25559 6.39581 8.34516 6.28451L8.34661 6.28596L11.6823 2.95023C11.9752 2.65734 12.4501 2.65734 12.743 2.95023C13.0359 3.24312 13.0359 3.718 12.743 4.01089L9.40727 7.34662C9.33404 7.42101 9.28918 7.52342 9.28918 7.63605C9.28918 7.86366 9.4737 8.04817 9.70131 8.04817C9.82647 8.04817 9.9386 7.99238 10.0142 7.90431L12.7217 5.21762C13.0157 4.92586 13.4906 4.9277 13.7824 5.22172C14.0741 5.51575 14.0723 5.99062 13.7783 6.28238L10.5186 9.51692L10.5157 9.51982L7.2552 12.7552L7.25409 12.7563C5.59427 14.4149 2.90415 14.4146 1.2448 12.7552C1.08928 12.5997 0.948333 12.4351 0.821958 12.2632C0.611232 11.9765 0.441024 11.6693 0.311334 11.3496C-0.311064 9.81515 -0.000269413 7.99073 1.24372 6.74587L1.2448 6.74479L4.48019 3.4843L7.71762 0.221724C8.00938 -0.0722994 8.48425 -0.0741348 8.77828 0.217624ZM6.36397 9.75001C6.36397 10.9175 5.41752 11.864 4.25001 11.864C3.0825 11.864 2.13605 10.9175 2.13605 9.75001C2.13605 8.5825 3.0825 7.63605 4.25001 7.63605C5.41752 7.63605 6.36397 8.5825 6.36397 9.75001Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187212\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"decent-work-and-economic-growth\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187230)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.3264 0.279772C10.4334 0.1082 10.6333 0.0180095 10.8328 0.051256L13.4043 0.479838C13.5351 0.501638 13.6518 0.574508 13.7289 0.682416C13.806 0.790323 13.837 0.92443 13.8152 1.05524L13.3867 3.62672C13.3534 3.8262 13.2033 3.98601 13.0063 4.03164C12.8092 4.07726 12.6041 3.99971 12.4866 3.83514L11.7931 2.86431L8.21889 5.14919C7.90365 5.35071 7.48811 5.28907 7.24494 5.00473L5.2522 2.67457L1.52455 4.58981C1.15612 4.77911 0.703995 4.6339 0.514699 4.26547C0.325403 3.89704 0.470619 3.44491 0.839048 3.25562L5.09408 1.06941C5.4032 0.910585 5.78094 0.984932 6.00682 1.24906L7.9608 3.53388L10.9201 1.64209L10.3437 0.835072C10.2261 0.67051 10.2193 0.451344 10.3264 0.279772ZM3.78104 5.19029L0.878725 6.84876C0.645044 6.98229 0.500829 7.2308 0.500829 7.49994V13.2499C0.500829 13.6641 0.836616 13.9999 1.25083 13.9999H3.78104V5.19029ZM5.03104 13.9999H8.69846V7.1876L7.05 8.01183L5.40201 5.12784C5.31486 4.97532 5.18227 4.86502 5.03104 4.80422V13.9999ZM12.7508 13.9999H9.94846V6.5626L12.4154 5.32912C12.6479 5.21287 12.924 5.2253 13.1451 5.36195C13.3662 5.49861 13.5008 5.74001 13.5008 5.99994V13.2499C13.5008 13.6641 13.165 13.9999 12.7508 13.9999Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187230\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"dna\", \"keywords\": [ \"science\", \"biology\", \"experiment\", \"lab\", \"science\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187218)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.5622 1.0652C10.7057 0.676641 10.5071 0.245321 10.1185 0.101824C9.72993 -0.041674 9.29861 0.156992 9.15511 0.545555C8.70181 1.773 8.72968 3.1265 9.23311 4.33425L9.2339 4.33613C9.52175 5.02141 9.59991 5.77666 9.45849 6.50637C9.36551 6.98618 9.18033 7.44077 8.91571 7.84601L6.13784 5.06813C6.54629 4.80374 7.00353 4.61838 7.48586 4.52457C7.89246 4.44549 8.15796 4.05177 8.07887 3.64517C7.99979 3.23858 7.60607 2.97308 7.19948 3.05216C6.16744 3.25289 5.21794 3.75466 4.47065 4.49423L4.46456 4.50032C3.73428 5.23985 3.2391 6.17889 3.04136 7.19924C2.93239 7.76153 2.91643 8.33467 2.9912 8.8964C2.17547 8.78449 1.33767 8.86948 0.550251 9.15371C0.160642 9.29434 -0.0411913 9.72419 0.0994429 10.1138C0.240077 10.5034 0.669924 10.7053 1.05953 10.5646C1.8399 10.283 2.69016 10.2777 3.46961 10.5444C3.72263 11.3225 3.71218 12.1659 3.43436 12.942C3.29477 13.332 3.49774 13.7613 3.88772 13.9009C4.2777 14.0405 4.70701 13.8376 4.84661 13.4476C5.28819 12.214 5.24877 10.8594 4.73626 9.65354C4.45001 8.96829 4.37267 8.21372 4.51396 7.48462C4.60866 6.99601 4.79871 6.53347 5.07075 6.12236L7.85747 8.90908C7.44019 9.18547 6.97072 9.37886 6.47469 9.47568C6.06815 9.55503 5.80291 9.94892 5.88225 10.3555C5.9616 10.762 6.3555 11.0273 6.76204 10.9479C7.79878 10.7456 8.7521 10.24 9.50116 9.49517L9.50526 9.49105C10.237 8.7521 10.7332 7.81273 10.9311 6.79175C11.0395 6.23217 11.0557 5.66185 10.9818 5.10279C11.8059 5.21735 12.6524 5.13273 13.448 4.84677C13.8378 4.70668 14.0402 4.27711 13.9001 3.8873C13.76 3.4975 13.3304 3.29508 12.9406 3.43518C12.1526 3.7184 11.2944 3.72365 10.507 3.45507C10.2599 2.67742 10.2775 1.83631 10.5622 1.0652Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187218\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"erlenmeyer-flask\", \"keywords\": [ \"science\", \"experiment\", \"lab\", \"flask\", \"chemistry\", \"solution\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187215)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.01994 0.749268C3.01994 0.335054 3.35573 -0.000732422 3.76994 -0.000732422H10.2292C10.6434 -0.000732422 10.9792 0.335054 10.9792 0.749268C10.9792 1.16348 10.6434 1.49927 10.2292 1.49927H9.49956V6.32702L12.986 10.7643C14.0171 12.0765 13.0823 13.9999 11.4134 13.9999H2.58575C0.916894 13.9999 -0.0179425 12.0765 1.01311 10.7643L4.49956 6.32702V1.49927H3.76994C3.35573 1.49927 3.01994 1.16348 3.01994 0.749268Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187215\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"flower\", \"keywords\": [ \"nature\", \"plant\", \"tree\", \"flower\", \"petals\", \"bloom\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187233)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.82227 1.11219C5.39989 0.534582 6.1833 0.210083 7.00016 0.210083C7.81703 0.210083 8.60044 0.534582 9.17805 1.11219C9.73272 1.66686 10.054 2.4113 10.0787 3.19291C10.8227 2.97846 11.622 3.05284 12.3152 3.40376C13.0409 3.77106 13.5911 4.41129 13.8452 5.1839L13.3702 5.34008L13.8456 5.1851C13.9702 5.56751 14.018 5.97081 13.9862 6.37178C13.9544 6.77274 13.8436 7.16346 13.6602 7.52142C13.4767 7.87939 13.2243 8.19754 12.9174 8.45755C12.6412 8.69161 12.3257 8.8744 11.986 8.99766C12.1866 9.2961 12.3329 9.62806 12.4177 9.97821C12.5124 10.3695 12.5285 10.7756 12.4651 11.1731C12.4016 11.5706 12.2598 11.9515 12.048 12.2938C11.8363 12.6357 11.559 12.9322 11.232 13.1661C10.5726 13.6423 9.75193 13.8392 8.9482 13.7142C8.18044 13.5947 7.48725 13.1907 7.00517 12.5848C6.52308 13.1907 5.82989 13.5947 5.06212 13.7142C4.2584 13.8392 3.43781 13.6423 2.77837 13.1662C2.45136 12.9322 2.174 12.6357 1.96237 12.2938C1.75052 11.9515 1.60875 11.5706 1.54529 11.1731C1.48182 10.7756 1.49793 10.3695 1.59267 9.97821C1.67724 9.62898 1.82298 9.29785 2.0228 9.00002C1.67968 8.87442 1.36153 8.68831 1.08358 8.45024C0.77824 8.1887 0.5274 7.86958 0.345386 7.5111C0.163371 7.15262 0.0537472 6.7618 0.022772 6.36095C-0.00820325 5.96011 0.0400774 5.55709 0.164857 5.1749C0.289637 4.79271 0.488472 4.43884 0.75001 4.1335C1.01155 3.82816 1.33066 3.57732 1.68914 3.3953C2.04762 3.21329 2.43844 3.10366 2.83929 3.07269C3.20418 3.04449 3.57088 3.08197 3.92202 3.18305C3.94905 2.40504 4.26994 1.66453 4.82227 1.11219ZM9.20519 7.26008C9.20519 8.46974 8.21799 9.45036 7.00021 9.45036C5.78243 9.45036 4.79522 8.46974 4.79522 7.26008C4.79522 6.05042 5.78243 5.06979 7.00021 5.06979C8.21799 5.06979 9.20519 6.05042 9.20519 7.26008Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187233\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"galaxy-1\", \"keywords\": [ \"science\", \"space\", \"universe\", \"astronomy\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187227)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.55707 0.198018C7.19835 -0.00908844 6.73965 0.113818 6.53255 0.472537C6.32544 0.831257 6.44835 1.28995 6.80707 1.49706C8.05631 2.21831 8.69582 3.47473 8.72478 4.84392C7.01067 2.66323 4.05647 1.57533 1.38763 3.11618C1.02891 3.32328 0.906009 3.78198 1.11311 4.1407C1.32022 4.49942 1.77891 4.62232 2.13763 4.41522C3.38865 3.69294 4.79873 3.76855 6.00008 4.43067C3.26104 4.83111 0.830078 6.84049 0.830078 9.91816C0.830078 10.3324 1.16586 10.6682 1.58008 10.6682C1.99429 10.6682 2.33008 10.3324 2.33008 9.91816C2.33008 8.484 3.09939 7.30529 4.26936 6.5939C3.25357 9.16366 3.78098 12.2657 6.44182 13.802C6.80054 14.0091 7.25924 13.8862 7.46634 13.5275C7.67345 13.1687 7.55054 12.71 7.19182 12.5029C5.94972 11.7858 5.31359 10.5301 5.28256 9.16107C7.00012 11.3258 9.95039 12.4202 12.6113 10.8839C12.97 10.6768 13.0929 10.2181 12.8858 9.85936C12.6787 9.50064 12.22 9.37773 11.8613 9.58484C10.621 10.3009 9.21765 10.2252 8.01766 9.57018C10.7576 9.17138 13.1689 7.15848 13.1689 4.08185C13.1689 3.66764 12.8331 3.33185 12.4189 3.33185C12.0047 3.33185 11.6689 3.66764 11.6689 4.08185C11.6689 5.52434 10.9006 6.70637 9.72937 7.41605C10.7608 4.84124 10.2259 1.73887 7.55707 0.198018ZM6.9995 8.00133C7.55252 8.00133 8.00084 7.55301 8.00084 6.99999C8.00084 6.44697 7.55252 5.99866 6.9995 5.99866C6.44648 5.99866 5.99817 6.44697 5.99817 6.99999C5.99817 7.55301 6.44648 8.00133 6.9995 8.00133Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187227\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"galaxy-2\", \"keywords\": [ \"science\", \"space\", \"universe\", \"astronomy\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.5556 2.34997C13.3041 2.86398 13.8725 3.62848 13.8725 4.64996C13.8725 6.14823 12.9737 7.41067 11.7247 8.26522C10.471 9.12302 8.79239 9.63001 7.01238 9.63001H7.01121L7.00014 9.63004C5.54762 9.63004 4.37012 8.45254 4.37012 7.00002C4.37012 6.76026 4.4022 6.52801 4.4623 6.3073C3.97362 6.48507 3.52996 6.71081 3.14713 6.97275C2.16356 7.64572 1.65235 8.49831 1.65235 9.35005C1.65235 9.73859 1.84774 10.0904 2.31834 10.4135C2.80386 10.747 3.5222 10.9991 4.37055 11.1359C5.20987 11.2713 6.12314 11.2851 6.95834 11.1832C7.80382 11.0801 8.51225 10.8655 8.97656 10.5869C9.33175 10.3738 9.79245 10.489 10.0056 10.8442C10.2187 11.1994 10.1035 11.6601 9.74831 11.8732C9.03761 12.2996 8.10102 12.555 7.13992 12.6722C6.16854 12.7907 5.11367 12.7751 4.13171 12.6168C3.15879 12.4598 2.20275 12.1538 1.46918 11.65C0.720708 11.136 0.152344 10.3715 0.152344 9.35005C0.152344 7.85177 1.05115 6.58934 2.3001 5.73479C3.5476 4.88123 5.2158 4.37502 6.986 4.37003C6.99071 4.37001 6.99542 4.36999 7.00014 4.36999L7.00532 4.37C7.00768 4.37 7.01005 4.36999 7.01241 4.36999C7.01711 4.36999 7.02179 4.37004 7.02647 4.37012C8.46686 4.38425 9.63016 5.55628 9.63016 7.00002C9.63016 7.24332 9.59712 7.47891 9.53528 7.70254C10.0347 7.52329 10.4878 7.29406 10.8777 7.02726C11.8613 6.35429 12.3725 5.5017 12.3725 4.64996C12.3725 4.26141 12.1771 3.90965 11.7065 3.58647C11.221 3.25304 10.5026 3.00094 9.65424 2.86411C8.81493 2.72874 7.90165 2.71491 7.06645 2.81677C6.22097 2.91987 5.51254 3.13448 5.04823 3.41307C4.69304 3.62618 4.23235 3.511 4.01924 3.15582C3.80613 2.80063 3.9213 2.33994 4.27648 2.12683C4.98719 1.70041 5.92377 1.44501 6.88487 1.3278C7.85625 1.20933 8.91113 1.22487 9.89308 1.38325C10.866 1.54017 11.8221 1.8462 12.5556 2.34997ZM6.99196 5.87002C6.99582 5.87001 6.99968 5.87 7.00353 5.87C7.62607 5.87183 8.13016 6.37705 8.13016 7.00002C8.13016 7.62411 7.62423 8.13004 7.00014 8.13004C6.37605 8.13004 5.87012 7.62411 5.87012 7.00002C5.87012 6.37865 6.37163 5.87442 6.99196 5.87002Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"gender-equality\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.33423 0.764404C8.33423 0.350191 8.67002 0.0144043 9.08423 0.0144043H11.756C12.1703 0.0144043 12.5066 0.350191 12.5066 0.764404V3.43679C12.5066 3.85101 12.1709 4.18679 11.7566 4.18679C11.3424 4.18679 11.0066 3.85101 11.0066 3.43679V2.57388L9.61568 3.96421C10.0271 4.57742 10.267 5.31526 10.267 6.10914C10.267 7.98167 8.93197 9.54239 7.16181 9.89141V10.7038H8.1935C8.60771 10.7038 8.9435 11.0396 8.9435 11.4538C8.9435 11.8681 8.60771 12.2038 8.1935 12.2038H7.16181V13.2354C7.16181 13.6497 6.82602 13.9854 6.41181 13.9854C5.99759 13.9854 5.66181 13.6497 5.66181 13.2354V12.2038H4.63031C4.2161 12.2038 3.88031 11.8681 3.88031 11.4538C3.88031 11.0396 4.2161 10.7038 4.63031 10.7038H5.66181V9.89141C3.89166 9.54238 2.55664 7.98166 2.55664 6.10914C2.55664 3.97999 4.28266 2.25397 6.41182 2.25397C7.2048 2.25397 7.94187 2.49339 8.55468 2.90389L9.94477 1.51441H9.08423C8.67002 1.51441 8.33423 1.17862 8.33423 0.764404ZM4.44678 4.96213C4.44678 4.61695 4.7266 4.33713 5.07178 4.33713H7.75196C8.09714 4.33713 8.37696 4.61695 8.37696 4.96213C8.37696 5.30731 8.09714 5.58713 7.75196 5.58713H5.07178C4.7266 5.58713 4.44678 5.30731 4.44678 4.96213ZM5.07178 6.63118C4.7266 6.63118 4.44678 6.91101 4.44678 7.25618C4.44678 7.60136 4.7266 7.88118 5.07178 7.88118H7.75196C8.09714 7.88118 8.37696 7.60136 8.37696 7.25618C8.37696 6.91101 8.09714 6.63118 7.75196 6.63118H5.07178Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"good-health-and-well-being\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.50968 1.37401C9.37731 1.14522 9.13425 1.00301 8.86995 0.999693C8.60564 0.996373 8.35909 1.13243 8.221 1.35782L4.27969 7.79091L2.90424 5.71094C2.76537 5.50095 2.53041 5.37463 2.27865 5.37463H0.759765C0.345552 5.37463 0.00976562 5.71042 0.00976562 6.12463C0.00976562 6.53885 0.345552 6.87463 0.759765 6.87463H1.87546L3.67826 9.60083C3.81966 9.81466 4.06049 9.94145 4.31682 9.93702C4.57314 9.93259 4.80944 9.79754 4.94336 9.57894L8.84211 3.21533L10.2365 5.62525C10.444 5.98378 10.9028 6.10624 11.2613 5.89879C11.6198 5.69134 11.7423 5.23253 11.5348 4.87401L9.50968 1.37401ZM10.3281 8.3386C9.87463 7.93844 9.39092 7.72142 8.90582 7.68203C8.28556 7.63167 7.73835 7.87858 7.34746 8.26364C6.58787 9.01192 6.33536 10.3949 7.30559 11.3599C7.31091 11.3652 7.31635 11.3704 7.3219 11.3754L9.98442 13.7952C10.1751 13.9685 10.4663 13.9685 10.6569 13.7952L13.3195 11.3754L13.3301 11.3655C14.3076 10.4236 14.0653 9.04796 13.3128 8.29957C12.9253 7.9141 12.3812 7.66485 11.7613 7.70724C11.2765 7.74039 10.7893 7.94839 10.3281 8.3386Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"industry-innovation-and-infrastructure\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187248)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.20782 0.478305C7.07618 0.418364 6.92504 0.418364 6.79341 0.478305L4.1714 1.67224L6.99873 2.95968L9.82794 1.67139L7.20782 0.478305ZM3.69976 5.60974V2.55627L6.49873 3.8308V7.20598L3.99255 6.06478C3.81422 5.98358 3.69976 5.80569 3.69976 5.60974ZM7.49873 12.3221V8.9818L10.1514 10.1897V13.565L7.64531 12.4238C7.58974 12.3985 7.54037 12.3638 7.49873 12.3221ZM11.1514 13.5667V10.1897L13.9542 8.91351V11.9687C13.9542 12.1647 13.8397 12.3426 13.6614 12.4238L11.1514 13.5667ZM6.49873 8.98174V12.3257C6.45787 12.3658 6.40975 12.3992 6.35579 12.4238L3.84585 13.5667V10.1897L6.49873 8.98174ZM2.84585 10.1897V13.565L0.339669 12.4238C0.161336 12.3426 0.046875 12.1647 0.046875 11.9687V8.91522L2.84585 10.1897ZM7.49873 7.20769V3.8308L10.3014 2.55456V5.60974C10.3014 5.80569 10.187 5.98358 10.0086 6.06478L7.49873 7.20769ZM10.4461 6.83728C10.5777 6.77734 10.7289 6.77734 10.8605 6.83728L13.4806 8.03035L10.6514 9.31862L7.82419 8.0312L10.4461 6.83728ZM3.14052 6.83728C3.27216 6.77734 3.4233 6.77734 3.55494 6.83728L6.17503 8.03035L3.34585 9.31862L0.518552 8.0312L3.14052 6.83728Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187248\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"leaf\", \"keywords\": [ \"nature\", \"environment\", \"leaf\", \"ecology\", \"plant\", \"plants\", \"eco\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187251)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.15685 0.0915544C2.93313 0.0107834 2.69371 -0.0174812 2.45714 0.00906586C2.21332 0.0364279 1.97944 0.121241 1.77478 0.256565C1.57011 0.39189 1.40049 0.573871 1.27992 0.787564C1.16095 0.99842 1.09312 1.23418 1.08182 1.47593C0.99584 2.60687 0.932317 4.36216 1.13404 6.17938C1.33474 7.98734 1.80438 9.92045 2.83206 11.3525C3.89721 12.8962 5.41784 13.582 6.83178 13.8496C8.23632 14.1153 9.56414 13.9755 10.3205 13.8457C10.5303 13.8138 10.733 13.7474 10.9204 13.6497C9.79127 10.6278 7.67209 7.30438 4.53828 4.41689C4.28443 4.18299 4.26825 3.78759 4.50215 3.53374C4.73605 3.27989 5.13144 3.26372 5.38529 3.49762C8.45694 6.32783 10.6125 9.57288 11.8581 12.6165C12.2291 11.9317 12.6929 10.9003 12.8963 9.71063C13.1372 8.30156 13.0152 6.64642 11.9189 5.10219C10.9063 3.67137 9.22616 2.57497 7.57745 1.76616C5.92605 0.956028 4.24696 0.406238 3.15685 0.0915544Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187251\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"log\", \"keywords\": [ \"nature\", \"tree\", \"plant\", \"circle\", \"round\", \"log\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.03033 1.96967C5.73744 1.67678 5.26256 1.67678 4.96967 1.96967C4.67678 2.26256 4.67678 2.73744 4.96967 3.03033L5.93934 4H4.14636C4.29814 4.20783 4.42591 4.42362 4.53048 4.63114C4.99749 5.55785 5.25 6.75072 5.25 8C5.25 8.29576 5.23585 8.58835 5.20805 8.875H7C7.34518 8.875 7.625 9.15482 7.625 9.5C7.625 9.84518 7.34518 10.125 7 10.125H4.99005C4.87377 10.574 4.71974 10.9933 4.53048 11.3689C4.42591 11.5764 4.29814 11.7922 4.14636 12H11.5C12.3091 12 12.944 11.4306 13.3483 10.7229C13.7608 10.0012 14 9.03739 14 8C14 7.87381 13.9965 7.74871 13.9895 7.625H8.5C8.15482 7.625 7.875 7.34518 7.875 7C7.875 6.65482 8.15482 6.375 8.5 6.375H13.7917C13.6825 5.97069 13.5327 5.5997 13.3483 5.27706C12.944 4.56941 12.3091 4 11.5 4H8.05904C8.04981 3.98969 8.04024 3.97958 8.03033 3.96967L6.03033 1.96967ZM2 11.9688C3.10457 11.9688 4 10.1919 4 8C4 5.80812 3.10457 4.03125 2 4.03125C0.895431 4.03125 0 5.80812 0 8C0 10.1919 0.895431 11.9688 2 11.9688Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"no-poverty\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.16481 4.46169C4.11216 4.46169 4.88015 3.69371 4.88015 2.74635C4.88015 1.79899 4.11216 1.03101 3.16481 1.03101C2.21745 1.03101 1.44946 1.79899 1.44946 2.74635C1.44946 3.69371 2.21745 4.46169 3.16481 4.46169ZM10.837 4.46169C11.7843 4.46169 12.5523 3.69371 12.5523 2.74635C12.5523 1.79899 11.7843 1.03101 10.837 1.03101C9.88957 1.03101 9.12158 1.79899 9.12158 2.74635C9.12158 3.69371 9.88957 4.46169 10.837 4.46169ZM10.837 4.81936C10.1207 4.81936 9.46154 5.06445 8.93899 5.47534C9.5681 6.0191 9.9662 6.82291 9.9662 7.71971C9.9662 7.95107 9.9397 8.17625 9.88958 8.39237H13.41C13.6861 8.39237 13.91 8.16852 13.91 7.89237C13.91 6.1952 12.5342 4.81936 10.837 4.81936ZM3.16481 4.81936C3.88112 4.81936 4.54019 5.06445 5.06273 5.47533C4.43362 6.01909 4.03552 6.82291 4.03552 7.71971C4.03552 7.95107 4.06202 8.17625 4.11214 8.39237H0.591797C0.315655 8.39237 0.0917969 8.16852 0.0917969 7.89237C0.0917969 6.1952 1.46763 4.81936 3.16481 4.81936ZM7.00086 9.43505C7.94822 9.43505 8.7162 8.66706 8.7162 7.71971C8.7162 6.77235 7.94822 6.00436 7.00086 6.00436C6.0535 6.00436 5.28552 6.77235 5.28552 7.71971C5.28552 8.66706 6.0535 9.43505 7.00086 9.43505ZM3.92785 12.8658C3.92785 11.1686 5.30369 9.79273 7.00086 9.79273C8.69804 9.79273 10.0739 11.1686 10.0739 12.8658C10.0739 13.1419 9.85003 13.3658 9.57388 13.3658H4.42785C4.15171 13.3658 3.92785 13.1419 3.92785 12.8658Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"octopus\", \"keywords\": [ \"nature\", \"sealife\", \"animals\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187302)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.00001 0C4.51474 0 2.5 2.01472 2.5 4.5C2.5 5.8441 3.08991 7.05104 4.02331 7.87487C4.35989 8.17194 4.5507 8.5341 4.57275 8.89877C4.59421 9.25375 4.4604 9.68667 4.00062 10.1464C3.44301 10.7041 2.65165 10.9027 2.02349 10.7688C1.71449 10.7029 1.46209 10.5609 1.28877 10.362C1.11931 10.1675 1 9.88996 1 9.5C1 9.22386 0.776142 9 0.5 9C0.223858 9 0 9.22386 0 9.5V10.3281C0 11.1721 0.298741 11.9464 0.79625 12.5508C0.89824 12.5769 0.994923 12.6292 1.07568 12.7075L1.07929 12.7107C1.08628 12.7168 1.1012 12.7295 1.12402 12.7466C1.16975 12.7809 1.24639 12.8326 1.35373 12.8863C1.56636 12.9926 1.90485 13.1094 2.375 13.1094C3.88043 13.1094 4.89999 11.8692 5.33798 10.7689C5.46564 10.4482 5.8291 10.2917 6.14981 10.4193C6.47051 10.547 6.62701 10.9104 6.49936 11.2311C6.15841 12.0877 5.4513 13.1868 4.34997 13.8281H9.65003C8.54869 13.1868 7.84157 12.0877 7.50063 11.2311C7.37298 10.9104 7.52948 10.5469 7.85018 10.4193C8.17089 10.2916 8.53436 10.4481 8.66201 10.7688C9.09999 11.8692 10.1196 13.1094 11.625 13.1094C12.0951 13.1094 12.4336 12.9926 12.6463 12.8863C12.7536 12.8326 12.8302 12.7809 12.876 12.7466C12.8988 12.7295 12.9137 12.7168 12.9207 12.7107L12.9243 12.7075C13.0051 12.6292 13.1018 12.5769 13.2038 12.5508C13.7013 11.9464 14 11.1721 14 10.3281V9.5C14 9.22386 13.7762 9 13.5 9C13.2239 9 13 9.22386 13 9.5C13 9.88996 12.8807 10.1675 12.7112 10.362C12.5379 10.5609 12.2855 10.7029 11.9765 10.7688C11.3484 10.9027 10.557 10.7041 9.99939 10.1464C9.53961 9.68667 9.40581 9.25375 9.42727 8.89877C9.44931 8.5341 9.64013 8.17194 9.97671 7.87487C10.9101 7.05104 11.5 5.8441 11.5 4.5C11.5 2.01472 9.4853 0 7.00001 0ZM6.03604 5.25001C6.03604 5.68416 5.6841 6.0361 5.24995 6.0361C4.81581 6.0361 4.46387 5.68416 4.46387 5.25001C4.46387 4.81587 4.81581 4.46393 5.24995 4.46393C5.6841 4.46393 6.03604 4.81587 6.03604 5.25001ZM8.74995 6.0361C9.1841 6.0361 9.53604 5.68416 9.53604 5.25001C9.53604 4.81587 9.1841 4.46393 8.74995 4.46393C8.31581 4.46393 7.96387 4.81587 7.96387 5.25001C7.96387 5.68416 8.31581 6.0361 8.74995 6.0361Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187302\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"planet\", \"keywords\": [ \"science\", \"solar\", \"system\", \"ring\", \"planet\", \"saturn\", \"space\", \"astronomy\", \"astronomy\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187281)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.3683 0.0111646C12.7695 0.0230098 13.1986 0.141782 13.5286 0.475697C14.0357 0.984724 14.0469 1.70273 13.9308 2.2934C13.8092 2.91276 13.5098 3.61448 13.1011 4.34351C12.8833 4.73198 12.6281 5.13951 12.3395 5.56013C12.4961 6.14714 12.5549 6.75815 12.5115 7.3684C12.446 8.28777 12.1506 9.17594 11.6523 9.95132C11.154 10.7267 10.4688 11.3644 9.65975 11.8058C8.44214 12.4702 6.99102 12.6476 5.64869 12.306C5.20305 12.614 4.77092 12.8844 4.35955 13.1128C3.62658 13.5197 2.92072 13.815 2.29809 13.9329C1.70412 14.0455 0.984992 14.0294 0.477803 13.5222C0.143989 13.1884 0.0235375 12.7543 0.0109258 12.3458C-0.0015355 11.9423 0.087348 11.5091 0.231342 11.0768C0.501743 10.2651 1.01231 9.32269 1.69234 8.33512C1.54823 7.76924 1.49401 7.17963 1.53606 6.58791C1.62991 5.26731 2.19698 4.02488 3.13314 3.08872C4.0693 2.15256 5.31173 1.58549 6.63234 1.49164C7.23123 1.44908 7.82798 1.50514 8.40014 1.65321C9.38533 0.99899 10.325 0.494059 11.1302 0.22696C11.5495 0.0878837 11.9726 -0.000517708 12.3683 0.0111646ZM10.0529 2.38818C10.1443 2.44854 10.2341 2.51176 10.3222 2.5778L10.3251 2.58L10.7764 2.92362C10.017 4.2085 8.8018 5.7527 7.28571 7.28662C5.79576 8.7941 4.27869 9.99472 3.00406 10.7514L2.63313 10.2918C2.62941 10.2872 2.62578 10.2825 2.62223 10.2778C2.54952 10.1808 2.48024 10.0818 2.41444 9.98085C2.05654 10.5769 1.80177 11.1087 1.65446 11.5509C1.54205 11.8883 1.50516 12.1357 1.51021 12.2995C1.5133 12.3994 1.53037 12.4444 1.53843 12.4599C1.56129 12.4721 1.68028 12.5233 2.01894 12.4592C2.42021 12.3831 2.96712 12.1701 3.63147 11.8013C3.99564 11.5991 4.38351 11.3567 4.78795 11.0775C5.87653 10.3048 7.04061 9.31281 8.17475 8.16532C9.30956 7.01716 10.2978 5.84753 11.0704 4.75835C11.3481 4.35629 11.5901 3.9713 11.7926 3.61006C12.1645 2.94659 12.3807 2.40277 12.459 2.00425C12.5245 1.67083 12.475 1.55472 12.4634 1.53301C12.4487 1.52621 12.4087 1.51301 12.324 1.51051C12.1689 1.50593 11.9313 1.5416 11.6025 1.65067C11.1675 1.79496 10.6433 2.04365 10.0529 2.38818Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187281\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"potted-flower-tulip\", \"keywords\": [ \"nature\", \"flower\", \"plant\", \"tree\", \"pot\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187278)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.24904 0.067545C3.40319 -0.021916 3.59332 -0.022551 3.74807 0.0658784L7 1.92412L10.2519 0.0658784C10.4067 -0.022551 10.5968 -0.021916 10.751 0.067545C10.9051 0.157006 11 0.321765 11 0.5V4C11 5.95279 9.60065 7.57875 7.75 7.9298V11.0156C8.29971 10.3928 9.10398 10 10 10H12C12.2761 10 12.5 10.2239 12.5 10.5V11C12.5 12.6569 11.1569 14 9.5 14H7H4.5C2.84315 14 1.5 12.6569 1.5 11V10.5C1.5 10.2239 1.72386 10 2 10H4C4.89602 10 5.70029 10.3928 6.25 11.0156V7.9298C4.39935 7.57875 3 5.95279 3 4V0.5C3 0.321765 3.09488 0.157006 3.24904 0.067545Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187278\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"quality-education\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187293)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.09498 0.577313L9.00513 0.573653V2.87686L3.09597 2.88052C2.82883 2.88068 2.56867 2.79526 2.35362 2.63678L1.39508 1.93037C1.33114 1.88325 1.2934 1.80855 1.2934 1.72912C1.2934 1.64969 1.33114 1.57499 1.39508 1.52787L2.35417 0.821052C2.5688 0.662882 2.82837 0.577478 3.09498 0.577313ZM11.4386 2.87535L10.2551 2.87608V0.572879L11.4383 0.572146L11.4424 0.572144C12.0763 0.582116 12.5609 1.14647 12.5609 1.72375C12.5609 2.30103 12.0763 2.86535 11.4424 2.87532L11.4386 2.87535ZM5.50738 5.18459C5.84272 5.31908 6.12829 5.46606 6.37427 5.61196V13.9999C6.05399 13.7665 5.67613 13.5178 5.13514 13.3008C4.39991 13.006 3.33983 12.7627 1.72266 12.7627C1.59005 12.7627 1.46287 12.71 1.3691 12.6163C1.27533 12.5225 1.22266 12.3953 1.22266 12.2627V5.07461C1.22266 4.79847 1.44651 4.57461 1.72266 4.57461C3.44212 4.57461 4.63242 4.83367 5.50738 5.18459ZM8.8637 13.3008C8.32253 13.5179 7.94461 13.7666 7.62427 14.0001V5.61214C7.87032 5.46619 8.15598 5.31914 8.49146 5.18459C9.36642 4.83367 10.5567 4.57461 12.2762 4.57461C12.5523 4.57461 12.7762 4.79847 12.7762 5.07461V12.2627C12.7762 12.3953 12.7235 12.5225 12.6297 12.6163C12.5359 12.71 12.4088 12.7627 12.2762 12.7627C10.659 12.7627 9.59893 13.006 8.8637 13.3008Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187293\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"rainbow\", \"keywords\": [ \"nature\", \"arch\", \"rain\", \"colorful\", \"rainbow\", \"curve\", \"half\", \"circle\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.0001 4.63171C5.54501 4.63171 4.14951 5.20975 3.12061 6.23865C2.0917 7.26756 1.51367 8.66305 1.51367 10.1181C1.51367 10.5323 1.17788 10.8681 0.763672 10.8681C0.349458 10.8681 0.0136719 10.5323 0.0136719 10.1181C0.0136719 8.26523 0.74974 6.4882 2.05995 5.17799C3.37016 3.86778 5.14718 3.13171 7.0001 3.13171C8.85302 3.13171 10.63 3.86778 11.9402 5.17799C13.2504 6.4882 13.9865 8.26523 13.9865 10.1181C13.9865 10.5323 13.6507 10.8681 13.2365 10.8681C12.8223 10.8681 12.4865 10.5323 12.4865 10.1181C12.4865 8.66305 11.9085 7.26756 10.8796 6.23865C9.85069 5.20975 8.45519 4.63171 7.0001 4.63171ZM7.00014 7.09029C6.1971 7.09029 5.42696 7.40929 4.85913 7.97713C4.29129 8.54496 3.97229 9.31511 3.97229 10.1181C3.97229 10.5323 3.6365 10.8681 3.22229 10.8681C2.80807 10.8681 2.47229 10.5323 2.47229 10.1181C2.47229 8.91728 2.94933 7.7656 3.79847 6.91647C4.6476 6.06733 5.79928 5.59029 7.00014 5.59029C8.201 5.59029 9.35268 6.06733 10.2018 6.91647C11.0509 7.7656 11.528 8.91728 11.528 10.1181C11.528 10.5323 11.1922 10.8681 10.778 10.8681C10.3638 10.8681 10.028 10.5323 10.028 10.1181C10.028 9.31511 9.70899 8.54496 9.14116 7.97713C8.57332 7.40929 7.80318 7.09029 7.00014 7.09029ZM6.59741 9.71543C6.70422 9.60862 6.84908 9.54862 7.00012 9.54862C7.15117 9.54862 7.29603 9.60862 7.40284 9.71543C7.50964 9.82224 7.56964 9.9671 7.56964 10.1181C7.56964 10.5323 7.90543 10.8681 8.31964 10.8681C8.73386 10.8681 9.06964 10.5323 9.06964 10.1181C9.06964 9.56927 8.85161 9.04288 8.4635 8.65477C8.07538 8.26666 7.54899 8.04862 7.00012 8.04862C6.45125 8.04862 5.92486 8.26666 5.53675 8.65477C5.14864 9.04288 4.9306 9.56927 4.9306 10.1181C4.9306 10.5323 5.26639 10.8681 5.6806 10.8681C6.09481 10.8681 6.4306 10.5323 6.4306 10.1181C6.4306 9.9671 6.4906 9.82224 6.59741 9.71543Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"recycle-1\", \"keywords\": [ \"nature\", \"sign\", \"environment\", \"protect\", \"save\", \"arrows\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187275)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.62533 1.3093C6.6064 -0.335963 8.96231 -0.410343 10.0452 1.16975L10.6391 2.03623L11.4584 1.63139C11.6899 1.51705 11.9678 1.55675 12.158 1.73132C12.3481 1.90589 12.4114 2.17941 12.3172 2.41975L11.1666 5.35619C11.0501 5.65363 10.728 5.81531 10.4198 5.73103L7.33482 4.88728C7.08443 4.8188 6.90244 4.60269 6.87757 4.34431C6.85271 4.08593 6.99012 3.83907 7.22284 3.72408L7.9089 3.38513L7.34764 2.50416C7.21375 2.294 6.91593 2.27046 6.75069 2.45696L5.62808 3.72398C5.41419 3.96539 5.05174 4.00439 4.79138 3.81402C4.53103 3.62365 4.45829 3.26643 4.62348 2.98941L5.62533 1.3093ZM3.77488 4.66759C4.09075 4.61981 4.3918 4.81792 4.47289 5.12692L5.28468 8.22047C5.35056 8.47155 5.2544 8.73721 5.04307 8.88794C4.83174 9.03867 4.54924 9.04309 4.3333 8.89904L3.69673 8.47437L3.21441 9.4009C3.09934 9.62193 3.22787 9.89162 3.47201 9.94148L5.13058 10.2802C5.44659 10.3447 5.66159 10.6391 5.6269 10.9598C5.59222 11.2804 5.31922 11.522 4.99672 11.5175L3.04078 11.4899C1.12541 11.4629 -0.116963 9.45981 0.709984 7.73194L1.16346 6.78443L0.403189 6.27725C0.188448 6.134 0.0838705 5.87346 0.139971 5.62149C0.196073 5.36952 0.401308 5.17797 0.656543 5.13936L3.77488 4.66759ZM7.16201 11.2201C6.9627 10.9705 6.98374 10.6107 7.2108 10.386L9.48399 8.13617C9.66849 7.95357 9.94664 7.90402 10.1829 8.01167C10.4191 8.11932 10.5641 8.36177 10.5473 8.6208L10.4979 9.38445L11.5415 9.33888C11.7904 9.32801 11.9597 9.08186 11.8808 8.8455L11.3448 7.23979C11.2427 6.93385 11.3902 6.60045 11.6852 6.47016C11.9802 6.33988 12.326 6.4555 12.4833 6.73706L13.4374 8.44475C14.3717 10.117 13.2581 12.1945 11.3483 12.3422L10.301 12.4233L10.2419 13.3353C10.2252 13.5929 10.0518 13.8137 9.80557 13.8911C9.55931 13.9685 9.2908 13.8865 9.12974 13.6848L7.16201 11.2201Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187275\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"reduced-inequalities\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187296)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.42294 2.9494C4.50033 3.13624 4.68265 3.25806 4.88488 3.25806H9.05799C9.26022 3.25806 9.44254 3.13624 9.51993 2.9494C9.59732 2.76257 9.55454 2.54751 9.41154 2.40451L7.32499 0.317955C7.12973 0.122693 6.81314 0.122693 6.61788 0.317955L4.53133 2.40451C4.38833 2.54751 4.34555 2.76257 4.42294 2.9494ZM5.62988 4.7655C5.21566 4.7655 4.87988 5.10129 4.87988 5.5155C4.87988 5.92972 5.21566 6.2655 5.62988 6.2655H8.31314C8.72735 6.2655 9.06314 5.92972 9.06314 5.5155C9.06314 5.10129 8.72735 4.7655 8.31314 4.7655H5.62988ZM5.62988 7.55554C5.21566 7.55554 4.87988 7.89133 4.87988 8.30554C4.87988 8.71976 5.21566 9.05554 5.62988 9.05554H8.31314C8.72735 9.05554 9.06314 8.71976 9.06314 8.30554C9.06314 7.89133 8.72735 7.55554 8.31314 7.55554H5.62988ZM9.51992 10.8718C9.44253 10.6849 9.26021 10.5631 9.05798 10.5631H4.88487C4.68264 10.5631 4.50033 10.6849 4.42294 10.8718C4.34554 11.0586 4.38832 11.2737 4.53132 11.4167L6.61787 13.5032C6.81314 13.6985 7.12972 13.6985 7.32498 13.5032L9.41153 11.4167C9.55453 11.2737 9.59731 11.0586 9.51992 10.8718ZM10.9326 4.3621C10.7458 4.43949 10.624 4.6218 10.624 4.82404V8.99714C10.624 9.19937 10.7458 9.38169 10.9326 9.45908C11.1195 9.53647 11.3345 9.49369 11.4775 9.35069L13.5641 7.26414C13.7594 7.06888 13.7594 6.7523 13.5641 6.55703L11.4775 4.47048C11.3345 4.32748 11.1195 4.28471 10.9326 4.3621ZM3.01031 9.45907C3.19715 9.38168 3.31897 9.19936 3.31897 8.99713V4.82403C3.31897 4.6218 3.19715 4.43948 3.01031 4.36209C2.82348 4.2847 2.60842 4.32747 2.46542 4.47047L0.378868 6.55703C0.183606 6.75229 0.183606 7.06887 0.378868 7.26413L2.46542 9.35069C2.60842 9.49368 2.82347 9.53646 3.01031 9.45907Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187296\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"rose\", \"keywords\": [ \"nature\", \"flower\", \"rose\", \"plant\", \"tree\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.99305 3.35343C7.90475 2.75885 8.90351 2.32251 9.94716 2.05668C9.74582 1.50912 9.395 1.0262 8.93157 0.664589C8.37964 0.233914 7.69963 0 6.99955 0C6.29947 0 5.61946 0.233914 5.06753 0.664589C4.60207 1.02779 4.25021 1.51336 4.0493 2.06389C4.88708 2.28211 5.83406 2.64256 6.79916 3.23203C6.86376 3.27149 6.9284 3.31195 6.99305 3.35343ZM1.80784 3.06084C1.76466 3.10588 1.72852 3.18064 1.72852 3.27752V6.14278C1.72852 7.54075 2.28387 8.88148 3.27239 9.87C4.08239 10.68 5.12888 11.1992 6.24958 11.3602V13.1733C6.24958 13.5875 6.58537 13.9233 6.99958 13.9233C7.41379 13.9233 7.74958 13.5875 7.74958 13.1733V11.3603C8.82327 11.2059 9.82884 10.723 10.6236 9.97043C9.41903 6.96924 7.73579 5.26884 6.1476 4.29879C4.48556 3.28364 2.8852 3.04305 1.96016 3.00049C1.89649 2.99757 1.8475 3.01946 1.80784 3.06084ZM11.5127 8.86615C12.0045 8.05105 12.2707 7.11064 12.2707 6.14278V3.33662C12.2707 3.11902 12.1112 3.00469 11.9794 3.01086C10.607 3.07508 9.26923 3.46678 8.07872 4.15321C9.34298 5.2148 10.5538 6.7263 11.5127 8.86615Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"shell\", \"keywords\": [ \"nature\", \"sealife\", \"animals\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.15991 3.58842C3.94925 3.53078 3.72749 3.5 3.49854 3.5C2.11782 3.5 0.998531 4.61929 0.998531 6C0.998531 6.22017 1.027 6.4337 1.08044 6.63709L4.12266 8.92247C4.39864 9.1298 4.4543 9.52159 4.24698 9.79757C4.03965 10.0736 3.64786 10.1292 3.37188 9.92189L0.639293 7.86911C0.36156 8.60584 0.703811 9.4408 1.41651 9.88623L3.99854 11.5L3.99853 12.5C3.99853 13.0523 4.44625 13.5 4.99853 13.5H8.99853C9.55082 13.5 9.99853 13.0523 9.99853 12.5L9.99854 11.5L12.5806 9.88623C13.2928 9.44112 13.6351 8.60702 13.3584 7.87068L10.6279 9.92189C10.3519 10.1292 9.96009 10.0736 9.75277 9.79757C9.54545 9.52159 9.60111 9.1298 9.87709 8.92247L12.916 6.6396C12.9699 6.43545 12.9986 6.22108 12.9986 6C12.9986 4.61929 11.8793 3.5 10.4986 3.5C10.2713 3.5 10.0511 3.53034 9.84171 3.58718C9.83957 3.60023 9.837 3.6133 9.834 3.62637L9.10984 6.78051C9.0326 7.11693 8.69726 7.32704 8.36083 7.2498C8.02441 7.17256 7.8143 6.83722 7.89154 6.50079L8.6157 3.34666C8.63122 3.27904 8.65718 3.21653 8.69147 3.16042C8.24615 2.7504 7.65159 2.5 6.99854 2.5C6.34684 2.5 5.75339 2.74936 5.30838 3.15787C5.34345 3.21463 5.36996 3.27801 5.38572 3.34666L6.10988 6.50079C6.18712 6.83722 5.97701 7.17256 5.64058 7.2498C5.30416 7.32704 4.96882 7.11693 4.89158 6.78051L4.16742 3.62637C4.16451 3.61371 4.16201 3.60106 4.15991 3.58842Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"shovel-rake\", \"keywords\": [ \"nature\", \"crops\", \"plants\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187287)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.866 1.5625H11.1951V7.73018C12.5548 8.06589 13.563 9.2938 13.563 10.7572V13.125C13.563 13.5392 13.2272 13.875 12.813 13.875C12.3988 13.875 12.063 13.5392 12.063 13.125V10.7572C12.063 10.1343 11.711 9.59369 11.1951 9.32333V13.125C11.1951 13.5392 10.8593 13.875 10.4451 13.875C10.0309 13.875 9.69507 13.5392 9.69507 13.125V9.32344C9.17928 9.59385 8.82739 10.1344 8.82739 10.7572V13.125C8.82739 13.5392 8.49161 13.875 8.07739 13.875C7.66318 13.875 7.32739 13.5392 7.32739 13.125V10.7572C7.32739 9.29389 8.33549 8.06603 9.69507 7.73023V1.5625H9.02466C8.61045 1.5625 8.27466 1.22671 8.27466 0.8125C8.27466 0.398286 8.61045 0.0625 9.02466 0.0625H10.4451H11.866C12.2802 0.0625 12.616 0.398286 12.616 0.8125C12.616 1.22671 12.2802 1.5625 11.866 1.5625ZM1.52363 2.04115C1.81598 1.01794 2.7512 0.3125 3.81536 0.3125C4.87951 0.3125 5.81474 1.01794 6.10708 2.04115L6.88261 4.75551C7.33096 6.32472 6.18103 7.88691 4.56519 7.94087V12.375H5.23612C5.65034 12.375 5.98612 12.7108 5.98612 13.125C5.98612 13.5392 5.65034 13.875 5.23612 13.875H3.82059H3.81519H3.80978H2.39478C1.98056 13.875 1.64478 13.5392 1.64478 13.125C1.64478 12.7108 1.98056 12.375 2.39478 12.375H3.06519V7.94086C1.44952 7.88669 0.299785 6.32461 0.748102 4.75551L1.52363 2.04115Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187287\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"sprout\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.7579 5.61052C10.5689 6.04292 11.6136 5.96733 12.3434 5.33202C13.096 4.74381 13.4417 2.80374 13.5647 1.9015C13.6006 1.63838 13.4157 1.40352 13.1515 1.3766C12.2439 1.28414 10.2713 1.16324 9.52709 1.75172C8.63579 2.38376 8.34356 3.57399 8.76401 4.53732C8.70831 4.56946 8.65586 4.60935 8.60822 4.65699C7.96867 5.29654 7.41443 6.28979 7.01184 7.26153C6.75287 6.89796 6.42662 6.57409 6.03749 6.25323C6.28845 5.37497 5.97871 4.37196 5.20534 3.82355C4.48958 3.25759 2.59956 3.36981 1.71463 3.45904C1.4504 3.48568 1.26504 3.72108 1.3012 3.98417C1.42209 4.86367 1.75688 6.72168 2.48062 7.28736C3.23641 7.94523 4.34112 7.98275 5.14404 7.46127C5.60407 7.84933 5.85065 8.16158 6.00016 8.49806C6.06019 8.63316 6.10911 8.78228 6.14724 8.95497C6.42297 8.94114 6.7072 8.93384 7 8.93384C7.34544 8.93384 7.67894 8.94401 8.00063 8.96306C8.10529 8.60175 8.23901 8.21865 8.39713 7.83684C8.76869 6.93968 9.2318 6.15472 9.66888 5.71765C9.70238 5.68414 9.73206 5.64825 9.7579 5.61052ZM14 12.3376C14 12.3376 12.0693 10.4069 7.75086 10.2014C7.50815 10.1899 7.25788 10.1838 7 10.1838C6.74249 10.1838 6.49259 10.1899 6.2502 10.2014C1.93103 10.4066 0 12.3376 0 12.3376V13.9194H14V12.3376Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"telescope\", \"keywords\": [ \"science\", \"experiment\", \"star\", \"gazing\", \"sky\", \"night\", \"space\", \"universe\", \"astronomy\", \"astronomy\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187284)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.1695 0.604919C12.1219 0.474066 12.0218 0.369032 11.8933 0.315114C11.7649 0.261195 11.6198 0.263278 11.493 0.320861L3.87105 3.78192C3.63316 3.88995 3.51854 4.16291 3.608 4.40839L3.72274 4.72323C3.75092 4.76487 3.77449 4.8106 3.79248 4.85997L4.78637 7.58717C4.80436 7.63654 4.81574 7.68672 4.82096 7.73671L4.92701 8.02769C5.01647 8.27316 5.27984 8.40838 5.53145 8.33801L6.80269 7.98248L4.8842 12.6906C4.7279 13.0742 4.91215 13.5119 5.29574 13.6682C5.67932 13.8245 6.117 13.6402 6.2733 13.2567L7.95551 9.12837L9.63772 13.2567C9.79402 13.6402 10.2317 13.8245 10.6153 13.6682C10.9989 13.5119 11.1831 13.0742 11.0268 12.6906L8.87246 7.40362L13.5931 6.08338C13.7272 6.04587 13.8396 5.95412 13.9032 5.83022C13.9669 5.70633 13.9759 5.56151 13.9282 5.43066L12.1695 0.604919ZM3.64434 8.08909C3.63212 8.06548 3.62127 8.04082 3.61193 8.01518L2.70345 5.52234L1.5238 5.95225L1.49783 5.88101C1.35601 5.49184 0.925543 5.29132 0.536368 5.43315C0.147193 5.57498 -0.0533205 6.00545 0.0885089 6.39462L1.0824 9.12182C1.22422 9.51099 1.65469 9.71151 2.04386 9.56968C2.43304 9.42785 2.63355 8.99739 2.49172 8.60821L2.45985 8.52076L3.64434 8.08909Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187284\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"test-tube\", \"keywords\": [ \"science\", \"experiment\", \"lab\", \"chemistry\", \"test\", \"tube\", \"solution\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187299)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.76758 -0.00109863C2.35336 -0.00109863 2.01758 0.334688 2.01758 0.748901C2.01758 1.16312 2.35336 1.4989 2.76758 1.4989H3.50012V2.67273H6.00013C6.34531 2.67273 6.62513 2.95255 6.62513 3.29773C6.62513 3.64291 6.34531 3.92273 6.00013 3.92273H3.50012V5.87496H6.00013C6.34531 5.87496 6.62513 6.15479 6.62513 6.49996C6.62513 6.84514 6.34531 7.12496 6.00013 7.12496H3.50012V9.0772H6.00013C6.34531 9.0772 6.62513 9.35702 6.62513 9.7022C6.62513 10.0474 6.34531 10.3272 6.00013 10.3272H3.50012V10.5C3.50012 11.4283 3.86887 12.3185 4.52525 12.9749C5.18162 13.6313 6.07186 14 7.00012 14C7.92838 14 8.81862 13.6313 9.47499 12.9749C10.1314 12.3185 10.5001 11.4283 10.5001 10.5V1.4989H11.2326C11.6469 1.4989 11.9826 1.16312 11.9826 0.748901C11.9826 0.334688 11.6469 -0.00109863 11.2326 -0.00109863H2.76758Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187299\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"tidal-wave\", \"keywords\": [ \"nature\", \"ocean\", \"wave\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187305)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.22452 10.9818C2.52599 10.9802 2.79912 11.1592 2.9179 11.4363C3.05257 11.7505 3.28281 12.0143 3.57593 12.1901C4.12176 12.5176 5.10699 12.5176 5.65283 12.1901C5.94594 12.0143 6.17618 11.7505 6.31085 11.4363C6.42905 11.1606 6.70019 10.9818 7.00019 10.9818C7.3002 10.9818 7.57134 11.1606 7.68953 11.4363C7.8242 11.7505 8.05445 12.0143 8.34756 12.1901C8.89339 12.5176 9.87863 12.5176 10.4244 12.1901C10.7175 12.0143 10.9478 11.7505 11.0824 11.4363C11.2012 11.1592 11.4744 10.9802 11.7758 10.9818C12.0773 10.9834 12.3485 11.1654 12.4643 11.4438C12.5494 11.6485 12.6791 11.8317 12.8439 11.98C13.0087 12.1283 13.2045 12.2381 13.417 12.3013C13.8141 12.4193 14.0402 12.8369 13.9222 13.2339C13.8041 13.6309 13.3866 13.8571 12.9895 13.739C12.5634 13.6123 12.1709 13.3923 11.8405 13.0949C11.8175 13.0742 11.7948 13.0532 11.7725 13.0318C11.5993 13.2009 11.4061 13.3504 11.1962 13.4764C10.651 13.8035 10.0194 13.9563 9.38601 13.9154C8.75254 13.9563 8.12102 13.8035 7.57582 13.4764C7.36612 13.3505 7.17317 13.2013 7.00019 13.0324C6.82722 13.2013 6.63426 13.3505 6.42457 13.4764C5.87937 13.8035 5.24785 13.9563 4.61438 13.9154C3.9809 13.9563 3.34938 13.8035 2.80418 13.4764C2.59422 13.3504 2.40104 13.201 2.22789 13.0318C2.20556 13.0532 2.18289 13.0742 2.15987 13.0949C1.82945 13.3923 1.43693 13.6123 1.01083 13.739C0.613797 13.8571 0.196234 13.6309 0.0781787 13.2339C-0.039877 12.8369 0.186279 12.4193 0.583313 12.3013C0.79584 12.2381 0.99162 12.1283 1.15642 11.98C1.32123 11.8317 1.45092 11.6485 1.53608 11.4438C1.65186 11.1654 1.92305 10.9834 2.22452 10.9818Z\\\" fill=\\\"black\\\"/>\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.5 0C5.38965 0 4.20175 2.24813 4.0068 3.4178C3.97711 3.59595 4.0458 3.77626 4.18649 3.8895C4.32718 4.00274 4.51801 4.03131 4.6857 3.96424C5.24821 3.73923 5.68376 3.69996 6.01516 3.7581C6.34056 3.81519 6.60238 3.97229 6.81585 4.20948C7.26104 4.70414 7.5 5.55939 7.5 6.5C7.5 7.67561 6.27471 9 4.5 9C3.64252 9 2.77852 8.54711 2.45956 7.80301C2.38038 7.61828 2.19828 7.49892 1.99731 7.50001C1.79633 7.50109 1.61553 7.6224 1.53835 7.80797C1.32798 8.31371 0.882515 8.71463 0.357494 8.87074C0.145416 8.9338 0 9.12875 0 9.35V11.1856C0.0730207 11.1539 0.148712 11.1264 0.226887 11.1031C0.261436 11.0928 0.293266 11.075 0.32006 11.0509C0.346854 11.0268 0.367939 10.997 0.381782 10.9637C0.690534 10.2214 1.41372 9.73617 2.21763 9.73184C3.02154 9.72751 3.74991 10.205 4.06664 10.9439C4.0978 11.0165 4.15107 11.0776 4.21888 11.1183C4.22081 11.1193 4.25614 11.1382 4.33449 11.1563C4.41246 11.1742 4.50915 11.1858 4.61422 11.1858C4.71929 11.1858 4.81598 11.1742 4.89395 11.1563C4.9723 11.1382 5.00754 11.1193 5.00947 11.1183C5.07729 11.0776 5.13064 11.0165 5.1618 10.9439C5.47699 10.2085 6.20003 9.73181 7.00004 9.73181C7.80004 9.73181 8.52309 10.2085 8.83827 10.9439C8.86943 11.0165 8.92271 11.0776 8.99052 11.1183C8.99244 11.1193 9.02776 11.1382 9.10612 11.1563C9.1841 11.1742 9.28078 11.1858 9.38585 11.1858C9.49093 11.1858 9.58761 11.1742 9.66558 11.1563C9.74395 11.1382 9.77919 11.1193 9.78111 11.1183C9.84893 11.0776 9.90227 11.0165 9.93343 10.9439C10.2502 10.2049 10.9785 9.72751 11.7824 9.73184C12.5864 9.73617 13.3095 10.2214 13.6183 10.9637C13.6321 10.997 13.6532 11.0268 13.68 11.0509C13.7068 11.075 13.7386 11.0928 13.7732 11.1031C13.8513 11.1263 13.927 11.1539 14 11.1855V3.5C14 3.42238 13.9819 3.34582 13.9472 3.27639C13.3953 2.17262 11.553 0 8.5 0ZM13.8654 13.3726C13.8884 13.3292 13.9075 13.2828 13.9221 13.2339C14.0401 12.8369 13.814 12.4193 13.4169 12.3013C13.2044 12.2381 13.0086 12.1283 12.8438 11.98C12.679 11.8317 12.5493 11.6485 12.4642 11.4438C12.3484 11.1654 12.0772 10.9834 11.7757 10.9818C11.4742 10.9802 11.2011 11.1592 11.0823 11.4363C10.9477 11.7505 10.7174 12.0143 10.4243 12.1901C9.87847 12.5176 8.89324 12.5176 8.3474 12.1901C8.05429 12.0143 7.82405 11.7505 7.68938 11.4363C7.57118 11.1606 7.30004 10.9818 7.00004 10.9818C6.70003 10.9818 6.42889 11.1606 6.3107 11.4363C6.17603 11.7505 5.94578 12.0143 5.65267 12.1901C5.10684 12.5176 4.1216 12.5176 3.57577 12.1901C3.28266 12.0143 3.05241 11.7505 2.91774 11.4363C2.79897 11.1592 2.52583 10.9802 2.22436 10.9818C1.92289 10.9834 1.6517 11.1654 1.53592 11.4438C1.45077 11.6485 1.32107 11.8317 1.15627 11.98C0.99146 12.1283 0.795679 12.2381 0.583152 12.3013C0.186119 12.4193 -0.0400375 12.8369 0.0780183 13.2339C0.170196 13.5439 0.444968 13.7497 0.749992 13.7689C0.816883 13.7731 0.885229 13.7683 0.953422 13.7537C0.972517 13.7496 0.991599 13.7447 1.01063 13.739C1.18956 13.6858 1.36256 13.6162 1.52754 13.5312C1.75544 13.4139 1.96804 13.2674 2.15971 13.0949C2.18273 13.0742 2.2054 13.0532 2.22773 13.0318C2.40088 13.201 2.59406 13.3504 2.80403 13.4764C2.83549 13.4952 2.86723 13.5135 2.89925 13.5312C3.16382 13.6766 3.45589 13.7839 3.76062 13.8532C4.03949 13.9126 4.32665 13.934 4.61422 13.9154C4.90138 13.9339 5.18815 13.9127 5.46665 13.8534C5.77177 13.7842 6.06392 13.6768 6.32881 13.5312C6.36083 13.5135 6.39295 13.4952 6.42441 13.4764C6.63411 13.3505 6.82706 13.2013 7.00004 13.0324C7.17302 13.2013 7.36597 13.3505 7.57566 13.4764C7.60712 13.4952 7.63887 13.5135 7.67089 13.5312C7.93547 13.6766 8.22755 13.7839 8.53228 13.8532C8.81114 13.9126 9.0983 13.934 9.38585 13.9154C9.67301 13.9339 9.95976 13.9127 10.2382 13.8534C10.5434 13.7842 10.8355 13.6768 11.1004 13.5312C11.1324 13.5135 11.1646 13.4952 11.196 13.4764C11.406 13.3504 11.5992 13.201 11.7723 13.0318C11.7947 13.0532 11.8173 13.0742 11.8404 13.0949C12.1708 13.3923 12.5633 13.6123 12.9894 13.739C13.0111 13.7455 13.0329 13.751 13.0548 13.7554C13.1592 13.7766 13.2638 13.7748 13.3629 13.7531C13.5744 13.7068 13.7607 13.5697 13.8654 13.3726Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187305\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"tree-2\", \"keywords\": [ \"nature\", \"tree\", \"plant\", \"circle\", \"round\", \"park\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187312)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.50195 5.49874C1.50195 2.46187 3.96382 0 7.00069 0C10.0376 0 12.4994 2.46187 12.4994 5.49874C12.4994 8.19411 10.5601 10.4365 8.0007 10.9068V13.0009C8.0007 13.5532 7.55299 14.0009 7.0007 14.0009C6.44842 14.0009 6.0007 13.5532 6.0007 13.0009V10.9068C3.44128 10.4365 1.50195 8.19412 1.50195 5.49874Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187312\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"tree-3\", \"keywords\": [ \"nature\", \"tree\", \"plant\", \"cloud\", \"shape\", \"park\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187315)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.96 0.0196533C6.00363 0.0196533 5.07891 0.362311 4.35347 0.98551C3.73184 1.51954 3.29179 2.22928 3.08786 3.01635C2.3114 3.10841 1.58361 3.45866 1.02513 4.01714C0.368749 4.67352 0 5.56376 0 6.49202C0 7.42027 0.368749 8.31051 1.02513 8.96689C1.68092 9.62268 2.57016 9.99136 3.49753 9.99201H5.92001V12.9805C5.92001 13.5328 6.36773 13.9805 6.92001 13.9805C7.4723 13.9805 7.92001 13.5328 7.92001 12.9805V9.99201H10.5025C11.4298 9.99136 12.3191 9.62269 12.9749 8.96689C13.6313 8.31051 14 7.42027 14 6.49202C14 5.56376 13.6313 4.67352 12.9749 4.01714C12.3967 3.43897 11.6371 3.08397 10.8299 3.00758C10.6249 2.22401 10.1859 1.51755 9.56653 0.98551C8.84109 0.362311 7.91637 0.0196533 6.96 0.0196533Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187315\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"volcano\", \"keywords\": [ \"nature\", \"eruption\", \"erupt\", \"mountain\", \"volcano\", \"lava\", \"magma\", \"explosion\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187318)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.99806 0.0463867C6.06253 0.0463867 5.23364 0.499991 4.71826 1.19802C4.41801 1.06007 4.08388 0.983073 3.73232 0.983073C2.42684 0.983073 1.36853 2.04138 1.36853 3.34686C1.36853 4.65236 2.42687 5.71066 3.73235 5.71066H4.71994V4.51865C4.71994 4.17347 4.99976 3.89365 5.34494 3.89365C5.69012 3.89365 5.96994 4.17347 5.96994 4.51865V7.36187H8.02601V4.51865C8.02601 4.17347 8.30584 3.89365 8.65101 3.89365C8.99619 3.89365 9.27601 4.17347 9.27601 4.51865V5.71066H10.2637C11.5692 5.71066 12.6275 4.65236 12.6275 3.34686C12.6275 2.04138 11.5692 0.983073 10.2638 0.983073C9.91223 0.983073 9.5781 1.06007 9.27785 1.19802C8.76247 0.499991 7.93358 0.0463867 6.99806 0.0463867ZM4.19057 8.98613C4.19057 8.70999 4.41443 8.48613 4.69057 8.48613H9.30869C9.58484 8.48613 9.80869 8.70999 9.80869 8.98613C9.80869 9.90304 10.4209 10.7721 11.2812 11.5145C12.1267 12.2443 13.1154 12.7665 13.6497 12.9934C13.8685 13.0863 13.9916 13.3204 13.9442 13.5533C13.8968 13.7863 13.692 13.9536 13.4543 13.9536H0.544972C0.307271 13.9536 0.102428 13.7863 0.0550177 13.5533C0.00760782 13.3204 0.130738 13.0863 0.349527 12.9934C0.883841 12.7665 1.8725 12.2443 2.71808 11.5145C3.5783 10.7721 4.19057 9.90304 4.19057 8.98613Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187318\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"windmill\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.5 4.53125C6.5 3.70282 5.82843 3.03125 5 3.03125H3.0625C2.23407 3.03125 1.5625 3.70282 1.5625 4.53125V5H6.03125H6.5V4.53125ZM3.53677 9.605C3.23229 10.8947 3 12.2774 3 13.5C3 13.7761 3.22386 14 3.5 14H10.5C10.7761 14 11 13.7761 11 13.5C11 12.0736 10.6838 10.4292 10.3056 8.96875H9C8.4372 8.96875 7.91783 8.78278 7.5 8.46893V11.4375C7.5 11.7136 7.27614 11.9375 7 11.9375H6.03125C4.70683 11.9375 3.62295 10.9076 3.53677 9.605ZM9.46875 3.5C9.46875 4.32843 8.79718 5 7.96875 5H7.5V4.53125V0.0625H7.96875C8.79718 0.0625 9.46875 0.734073 9.46875 1.5625V3.5ZM6.5 6H6.03125C5.20282 6 4.53125 6.67157 4.53125 7.5V9.4375C4.53125 10.2659 5.20282 10.9375 6.03125 10.9375H6.5V6.46875V6ZM9 7.96875C8.17157 7.96875 7.5 7.29718 7.5 6.46875V6H7.96875H12.4375V6.46875C12.4375 7.29718 11.7659 7.96875 10.9375 7.96875H9Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"zero-hunger\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187321)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.2525 1.28712C11.4893 0.947272 11.4057 0.479804 11.0659 0.242996C10.7261 0.00618945 10.2586 0.0897192 10.0218 0.429565C9.66697 0.93874 9.62716 1.46845 9.73007 1.94105C9.81891 2.34902 10.0206 2.73438 10.1687 3.01725L10.1868 3.05198C10.3592 3.38179 10.4658 3.59681 10.5073 3.78731C10.5384 3.93013 10.5246 4.01584 10.4506 4.12209C10.2138 4.46193 10.2973 4.9294 10.6372 5.16621C10.977 5.40302 11.4445 5.31949 11.6813 4.97965C12.0361 4.47048 12.0759 3.94076 11.973 3.46817C11.8842 3.0602 11.6825 2.67483 11.5344 2.39196L11.5162 2.35723C11.3439 2.02742 11.2372 1.8124 11.1957 1.6219C11.1646 1.47907 11.1784 1.39336 11.2525 1.28712ZM0.496094 7.53986C0.496094 7.26372 0.719952 7.03986 0.996094 7.03986H13.0035C13.2796 7.03986 13.5035 7.26372 13.5035 7.53986C13.5035 9.83527 12.0974 11.8032 10.0728 12.8009V13.4629C10.0728 13.7391 9.84895 13.9629 9.57281 13.9629H9.5714H9.57031H9.56892H9.56722H9.56522H9.5641H9.56291H9.56165H9.56031H9.5589H9.55741H9.55585H9.55421H9.5525H9.55072H9.54887H9.54694H9.54494H9.54287H9.54072H9.53851H9.53622H9.53386H9.53143H9.52893H9.52636H9.52371H9.521H9.51822H9.51537H9.51244H9.50945H9.50639H9.50326H9.50006H9.4968H9.49346H9.49006H9.48659H9.48305H9.47945H9.47578H9.47204H9.46823H9.46436H9.46042H9.45642H9.45235H9.44821H9.44401H9.43975H9.43542H9.43103H9.42657H9.42204H9.41746H9.41281H9.4081H9.40332H9.39848H9.39358H9.38861H9.38359H9.3785H9.37335H9.36814H9.36287H9.35753H9.35214H9.34668H9.34117H9.33559H9.32996H9.32426H9.31851H9.31269H9.30682H9.30089H9.2949H9.28885H9.28274H9.27658H9.27036H9.26408H9.25774H9.25135H9.2449H9.23839H9.23183H9.22521H9.21854H9.21181H9.20502H9.19818H9.19129H9.18434H9.17733H9.17028H9.16316H9.156H9.14878H9.14151H9.13418H9.1268H9.11937H9.11189H9.10436H9.09677H9.08913H9.08144H9.0737H9.06591H9.05807H9.05017H9.04223H9.03424H9.02619H9.0181H9.00996H9.00177H8.99353H8.98524H8.97691H8.96852H8.96009H8.95161H8.94308H8.9345H8.92588H8.91721H8.9085H8.89974H8.89093H8.88207H8.87317H8.86423H8.85524H8.84621H8.83713H8.828H8.81883H8.80962H8.80037H8.79107H8.78172H8.77234H8.76291H8.75344H8.74392H8.73437H8.72477H8.71513H8.70545H8.69572H8.68596H8.67616H8.66631H8.65643H8.6465H8.63654H8.62653H8.61649H8.6064H8.59628H8.58612H8.57592H8.56568H8.55541H8.54509H8.53474H8.52435H8.51393H8.50346H8.49297H8.48243H8.47186H8.46125H8.45061H8.43993H8.42921H8.41846H8.40768H8.39686H8.38601H8.37512H8.3642H8.35324H8.34225H8.33123H8.32018H8.30909H8.29797H8.28682H8.27564H8.26442H8.25317H8.24189H8.23058H8.21924H8.20787H8.19647H8.18504H8.17357H8.16208H8.15056H8.13901H8.12743H8.11582H8.10418H8.09252H8.08083H8.0691H8.05735H8.04558H8.03377H8.02194H8.01008H7.9982H7.98629H7.97435H7.96239H7.9504H7.93839H7.92635H7.91429H7.9022H7.89009H7.87795H7.86579H7.8536H7.84139H7.82916H7.81691H7.80463H7.79233H7.78001H7.76766H7.7553H7.74291H7.7305H7.71807H7.70561H7.69314H7.68065H7.66813H7.6556H7.64305H7.63047H7.61788H7.60527H7.59264H7.57999H7.56732H7.55463H7.54193H7.52921H7.51647H7.50371H7.49094H7.47815H7.46534H7.45252H7.43968H7.42682H7.41395H7.40106H7.38816H7.37524H7.36231H7.34936H7.3364H7.32342H7.31043H7.29743H7.28441H7.27139H7.25834H7.24529H7.23222H7.21914H7.20605H7.19294H7.17983H7.1667H7.15356H7.14041H7.12726H7.11409H7.10091H7.08771H7.07452H7.06131H7.04809H7.03486H7.02162H7.00838H6.99512H6.98186H6.96859H6.95532H6.94203H6.92874H6.91544H6.90214H6.88882H6.87551H6.86218H6.84885H6.83552H6.82217H6.80883H6.79548H6.78212H6.76876H6.7554H6.74203H6.72866H6.71528H6.7019H6.68852H6.67513H6.66175H6.64836H6.63496H6.62157H6.60817H6.59478H6.58138H6.56798H6.55458H6.54118H6.52778H6.51438H6.50098H6.48757H6.47417H6.46078H6.44738H6.43398H6.42059H6.40719H6.3938H6.38041H6.36702H6.35364H6.34026H6.32688H6.31351H6.30014H6.28677H6.27341H6.26005H6.24669H6.23334H6.22H6.20666H6.19332H6.18H6.16667H6.15336H6.14005H6.12675H6.11345H6.10016H6.08688H6.0736H6.06034H6.04708H6.03383H6.02058H6.00735H5.99413H5.98091H5.9677H5.95451H5.94132H5.92814H5.91498H5.90182H5.88868H5.87554H5.86242H5.84931H5.83621H5.82312H5.81004H5.79698H5.78393H5.77089H5.75787H5.74485H5.73186H5.71887H5.7059H5.69294H5.68H5.66707H5.65416H5.64126H5.62838H5.61551H5.60266H5.58983H5.57701H5.56421H5.55142H5.53865H5.5259H5.51317H5.50045H5.48776H5.47507H5.46241H5.44977H5.43715H5.42454H5.41195H5.39939H5.38684H5.37431H5.36181H5.34932H5.33685H5.32441H5.31198H5.29958H5.2872H5.27484H5.2625H5.25019H5.2379H5.22563H5.21338H5.20115H5.18895H5.17678H5.16462H5.15249H5.14039H5.12831H5.11625H5.10422H5.09222H5.08024H5.06829H5.05636H5.04446H5.03258H5.02073H5.00891H4.99711H4.98535H4.97361H4.96189H4.95021H4.93855H4.92693H4.91533H4.90376H4.89222H4.8807H4.86922H4.85777H4.84635H4.83496H4.82359H4.81226H4.80096H4.7897H4.77846H4.76725H4.75608H4.74494H4.73383H4.72275H4.71171H4.7007H4.68972H4.67878H4.66787H4.65699H4.64615H4.63534H4.62457H4.61383H4.60313H4.59246H4.58183H4.57124H4.56068H4.55015H4.53966H4.52921H4.5188H4.50842H4.49809H4.48778H4.47752H4.4673H4.45711H4.44696H4.43685H4.42678C4.15064 13.9629 3.92678 13.7391 3.92678 13.4629V12.8009C1.90224 11.8032 0.496094 9.83527 0.496094 7.53986ZM7.19968 0.242996C7.53952 0.479804 7.62305 0.947272 7.38625 1.28712C7.31221 1.39336 7.29841 1.47907 7.32952 1.6219C7.371 1.8124 7.47766 2.02742 7.65002 2.35723L7.66818 2.39195C7.81623 2.67482 8.01793 3.06019 8.10676 3.46817C8.20967 3.94076 8.16986 4.47048 7.81506 4.97965C7.57825 5.31949 7.11078 5.40302 6.77093 5.16621C6.43109 4.9294 6.34756 4.46193 6.58437 4.12209C6.65841 4.01584 6.67221 3.93013 6.64111 3.78731C6.59963 3.59681 6.49297 3.38179 6.32061 3.05198L6.30244 3.01725C6.15439 2.73438 5.9527 2.34902 5.86386 1.94105C5.76095 1.46845 5.80076 0.93874 6.15555 0.429565C6.39236 0.0897192 6.85983 0.00618945 7.19968 0.242996ZM3.53346 1.28712C3.77027 0.947272 3.68674 0.479804 3.34689 0.242996C3.00705 0.00618945 2.53958 0.0897192 2.30277 0.429565C1.94797 0.93874 1.90817 1.46845 2.01108 1.94105C2.09991 2.34902 2.30161 2.73438 2.44966 3.01725L2.46783 3.05198C2.64019 3.38179 2.74684 3.59681 2.78832 3.78731C2.81942 3.93013 2.80562 4.01584 2.73159 4.12209C2.49478 4.46193 2.57831 4.9294 2.91815 5.16621C3.258 5.40302 3.72547 5.31949 3.96228 4.97965C4.31708 4.47048 4.35689 3.94076 4.25398 3.46817C4.16515 3.06019 3.96345 2.67482 3.8154 2.39195L3.79723 2.35723C3.62487 2.02742 3.51821 1.8124 3.47673 1.6219C3.44563 1.47907 3.45943 1.39336 3.53346 1.28712Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187321\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" } ], \"phone\": [ { \"name\": \"airplane-disabled\", \"keywords\": [ \"server\", \"plane\", \"airplane\", \"disabled\", \"off\", \"wireless\", \"mode\", \"internet\", \"network\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189615)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.219669 1.2796C-0.0732231 0.986704 -0.0732231 0.511831 0.219669 0.218938C0.512562 -0.0739558 0.987436 -0.0739558 1.28033 0.218938L4.7104 3.64901L4.68215 3.45596C4.55531 2.58928 4.791 1.70861 5.33375 1.02112C6.18391 -0.0557467 7.81685 -0.0557467 8.66701 1.02112C9.20976 1.70861 9.44544 2.58928 9.31861 3.45596L9.1146 4.85002L13.2315 6.93785C13.5882 7.11874 13.8129 7.48473 13.8129 7.88466C13.8129 8.5653 13.1814 9.07011 12.5175 8.92019L9.24195 8.18056L13.7817 12.7203C14.0746 13.0132 14.0746 13.4881 13.7817 13.781C13.4888 14.0738 13.0139 14.0738 12.7211 13.781L0.219669 1.2796ZM5.41053 8.23306L10.5785 13.401C10.2971 13.7718 9.78222 13.9427 9.30524 13.7359L7.00038 12.7364L4.69552 13.7359C3.99291 14.0405 3.2082 13.5255 3.2082 12.7597C3.2082 12.4469 3.34579 12.15 3.58442 11.9478L5.62446 10.2195L5.41053 8.23306ZM2.98941 5.81194L5.24765 8.07018L1.48332 8.92019C0.819386 9.07011 0.18788 8.5653 0.18788 7.88466C0.18788 7.48473 0.412637 7.11874 0.769319 6.93785L2.98941 5.81194Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189615\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"airplane-enabled\", \"keywords\": [ \"server\", \"plane\", \"airplane\", \"enabled\", \"on\", \"wireless\", \"mode\", \"internet\", \"network\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189630)\\\">\\n<path d=\\\"M4.88578 4.85079L4.68177 3.45672C4.55494 2.59004 4.79062 1.70938 5.33337 1.02188C6.18353 -0.0549825 7.81647 -0.0549825 8.66663 1.02188C9.20938 1.70938 9.44507 2.59004 9.31823 3.45672L9.11422 4.85079L13.2311 6.93861C13.5877 7.1195 13.8125 7.48549 13.8125 7.88542C13.8125 8.56607 13.181 9.07087 12.5171 8.92095L8.61084 8.0389L8.37592 10.2202L10.416 11.9486C10.6546 12.1508 10.7922 12.4477 10.7922 12.7604C10.7922 13.5263 10.0075 14.0413 9.30486 13.7366L7 12.7372L4.69514 13.7366C3.99253 14.0413 3.20782 13.5263 3.20782 12.7604C3.20782 12.4477 3.34542 12.1508 3.58404 11.9486L5.62408 10.2202L5.38916 8.0389L1.48294 8.92095C0.819006 9.07087 0.1875 8.56607 0.1875 7.88542C0.1875 7.48549 0.412256 7.1195 0.768938 6.93861L4.88578 4.85079Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189630\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"back-camera-1\", \"keywords\": [ \"android\", \"phone\", \"mobile\", \"device\", \"smartphone\", \"iphone\", \"camera\", \"lenses\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.68428 0.432108C2.95339 0.146509 3.32575 0 3.73132 0H10.2694C10.6749 0 11.0473 0.146509 11.3164 0.432108C11.583 0.71502 11.7116 1.09498 11.7116 1.5V12.5C11.7116 12.905 11.583 13.285 11.3164 13.5679C11.0473 13.8535 10.6749 14 10.2694 14H3.73132C3.32575 14 2.95339 13.8535 2.68428 13.5679C2.4177 13.285 2.28906 12.905 2.28906 12.5V1.5C2.28906 1.09498 2.4177 0.71502 2.68428 0.432108ZM7.00024 4C7.55252 4 8.00024 3.55228 8.00024 3C8.00024 2.44772 7.55252 2 7.00024 2C6.44796 2 6.00024 2.44772 6.00024 3C6.00024 3.55228 6.44796 4 7.00024 4Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"call-hang-up\", \"keywords\": [ \"phone\", \"telephone\", \"mobile\", \"device\", \"smartphone\", \"call\", \"hang\", \"up\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189606)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.21967 1.28033C-0.0732233 0.987437 -0.0732233 0.512563 0.21967 0.21967C0.512563 -0.0732233 0.987437 -0.0732233 1.28033 0.21967L7.40258 6.34192L8.35599 5.3885L8.35859 5.38594C8.40975 5.33553 8.45037 5.27545 8.4781 5.2092C8.50582 5.14295 8.5201 5.07185 8.5201 5.00003C8.5201 4.92821 8.50582 4.85711 8.4781 4.79086C8.45037 4.72461 8.4097 4.66458 8.35855 4.61417L8.35294 4.60846C8.06776 4.31828 7.90797 3.92771 7.90797 3.52086C7.90797 3.11402 8.06775 2.72343 8.35292 2.43326L10.2234 0.562779L10.2274 0.558831C10.5191 0.273597 10.911 0.113892 11.319 0.113892C11.727 0.113892 12.1188 0.273597 12.4106 0.558831L12.8479 1.00599C13.4405 1.59218 13.8136 2.36423 13.9048 3.19284C13.9961 4.02297 13.7989 4.85931 13.3462 5.56109L13.3397 5.57071C12.4433 6.89177 11.4411 8.13341 10.3448 9.28414L13.7803 12.7197C14.0732 13.0126 14.0732 13.4874 13.7803 13.7803C13.4874 14.0732 13.0126 14.0732 12.7197 13.7803L0.21967 1.28033ZM5.46239 8.28212L8.357 11.1767L8.28577 11.2427C8.28138 11.2467 8.27691 11.2507 8.27237 11.2546C7.41711 11.9909 6.51794 12.678 5.57885 13.3116L5.57018 13.3172C4.8684 13.77 4.03213 13.9673 3.20199 13.876C2.37338 13.7848 1.60132 13.4117 1.01512 12.8191L0.575863 12.3898L0.567986 12.3817C0.282751 12.09 0.123047 11.6982 0.123047 11.2902C0.123047 10.8821 0.282703 10.4903 0.567937 10.1985L2.45103 8.33516L2.45225 8.33395C2.74243 8.04878 3.133 7.88899 3.53985 7.88899C3.94647 7.88899 4.33684 8.0486 4.62696 8.33349C4.73182 8.43577 4.87251 8.49303 5.01901 8.49303C5.16493 8.49303 5.30509 8.43622 5.40982 8.33469L5.46239 8.28212Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189606\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cellular-network-4g\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.69479 2.23645C1.4309 2.45895 1.39734 2.85325 1.61984 3.11715C1.84233 3.38105 2.23664 3.41461 2.50053 3.19211C2.88845 2.86505 3.31112 2.57811 3.76226 2.33689C4.76034 1.80323 5.87003 1.50816 7.00315 1.47522C8.1373 1.50857 9.24782 1.80497 10.246 2.34065C10.694 2.58106 11.1137 2.86663 11.499 3.19187C11.7627 3.41452 12.157 3.3812 12.3797 3.11743C12.6024 2.85367 12.569 2.45935 12.3053 2.23669C11.8537 1.85548 11.3619 1.52086 10.8371 1.23924C9.66207 0.608649 8.35465 0.261371 7.02019 0.225205C7.00897 0.224901 6.99774 0.224899 6.98653 0.225199C5.65361 0.260889 4.34748 0.606519 3.17287 1.23457C2.64451 1.51707 2.14936 1.8532 1.69479 2.23645ZM4.15101 4.21204C3.86895 4.41101 3.80159 4.80096 4.00056 5.08302C4.19953 5.36508 4.58948 5.43244 4.87154 5.23347C5.54346 4.75949 6.27707 4.49973 7.01648 4.45876C7.75769 4.49765 8.49336 4.75637 9.16723 5.23039C9.44956 5.42899 9.83942 5.36111 10.0381 5.07878C10.2367 4.79646 10.1688 4.40659 9.8864 4.208C9.0238 3.60122 8.05031 3.25367 7.04351 3.20861C7.02428 3.20775 7.00502 3.20778 6.98579 3.2087C5.98149 3.2566 5.01093 3.60544 4.15101 4.21204ZM9.65979 8.58398C9.28015 8.58398 8.979 8.88881 8.979 9.25673V11.8522C8.979 12.2201 9.28015 12.525 9.65979 12.525H10.5304C10.91 12.525 11.2111 12.2201 11.2111 11.8522V11.6121H10.5304C10.1852 11.6121 9.90531 11.3322 9.90531 10.9871C9.90531 10.6419 10.1852 10.3621 10.5304 10.3621H11.8361C12.1813 10.3621 12.4611 10.6419 12.4611 10.9871V11.8522C12.4611 12.9178 11.5931 13.775 10.5304 13.775H9.65979C8.59709 13.775 7.729 12.9178 7.729 11.8522V9.25673C7.729 8.19119 8.59709 7.33398 9.65979 7.33398H10.5304C11.3696 7.33398 12.085 7.86747 12.3507 8.61473C12.4664 8.93995 12.2965 9.29736 11.9713 9.41302C11.646 9.52868 11.2886 9.3588 11.173 9.03357C11.0802 8.77275 10.8282 8.58398 10.5304 8.58398H9.65979ZM5.34588 7.33401C5.0802 7.33401 4.82735 7.44828 4.65178 7.64769L1.77281 10.9177C1.62185 11.0891 1.53857 11.3098 1.53857 11.5382C1.53857 12.0568 1.95899 12.4772 2.47761 12.4772H5.02067V13.15C5.02067 13.4952 5.30049 13.775 5.64567 13.775C5.99085 13.775 6.27067 13.4952 6.27067 13.15V12.4772H6.51619C6.86137 12.4772 7.14119 12.1974 7.14119 11.8522C7.14119 11.5071 6.86137 11.2272 6.51619 11.2272H6.27067V8.2588C6.27067 7.74805 5.85663 7.33401 5.34588 7.33401ZM5.02067 9.12033V11.2272H3.16569L5.02067 9.12033Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"cellular-network-5g\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.77343 2.21904C1.51021 2.44234 1.47785 2.83674 1.70115 3.09996C1.92445 3.36318 2.31886 3.39554 2.58207 3.17224C2.96326 2.84886 3.37853 2.5652 3.82171 2.32677C4.80202 1.79937 5.89173 1.50783 7.00427 1.47523C8.11782 1.50823 9.20832 1.80108 10.1887 2.33049C10.6288 2.56811 11.0411 2.85042 11.4197 3.17199C11.6828 3.39545 12.0772 3.36333 12.3007 3.10024C12.5242 2.83716 12.492 2.44274 12.2289 2.21928C11.7841 1.84147 11.2997 1.50979 10.7827 1.23061C9.62497 0.60546 8.3366 0.261073 7.02141 0.225208C7.01012 0.2249 6.99882 0.224898 6.98754 0.225202C5.67387 0.260594 4.38677 0.603347 3.22948 1.22597C2.70893 1.50603 2.22116 1.8392 1.77343 2.21904ZM4.19055 4.17436C3.90907 4.37416 3.84284 4.7643 4.04263 5.04578C4.24242 5.32727 4.63257 5.39349 4.91405 5.1937C5.57349 4.72564 6.29279 4.46948 7.0172 4.42895C7.74336 4.46743 8.46467 4.72256 9.12602 5.19066C9.40777 5.39007 9.79783 5.32333 9.99725 5.04158C10.1967 4.75983 10.1299 4.36977 9.84817 4.17036C8.99799 3.56862 8.0379 3.22353 7.04439 3.17879C7.02504 3.17792 7.00566 3.17795 6.98631 3.17888C5.99528 3.22644 5.03807 3.57281 4.19055 4.17436ZM9.30137 8.63589C8.93699 8.63589 8.6416 8.93128 8.6416 9.29566V11.8652C8.6416 12.2296 8.93699 12.525 9.30137 12.525H10.1579C10.5223 12.525 10.8177 12.2296 10.8177 11.8652V11.6337H10.1579C9.8127 11.6337 9.53288 11.3539 9.53288 11.0087C9.53288 10.6635 9.8127 10.3837 10.1579 10.3837H11.4427C11.7878 10.3837 12.0677 10.6635 12.0677 11.0087V11.8652C12.0677 12.9199 11.2126 13.775 10.1579 13.775H9.30137C8.24664 13.775 7.3916 12.9199 7.3916 11.8652V9.29566C7.3916 8.24093 8.24664 7.38589 9.30137 7.38589H10.1579C10.9904 7.38589 11.697 7.91823 11.9588 8.65914C12.0739 8.98458 11.9033 9.34166 11.5778 9.45669C11.2524 9.57172 10.8953 9.40114 10.7803 9.07569C10.6894 8.81859 10.4442 8.63589 10.1579 8.63589H9.30137ZM5.80619 8.63901H3.55366L3.39367 9.59896C3.61148 9.5526 3.83425 9.52899 4.05842 9.52899H4.7362C5.7905 9.52899 6.64519 10.3837 6.64519 11.438V11.866C6.64519 12.9203 5.7905 13.775 4.7362 13.775H3.8802C3.04804 13.775 2.34176 13.2429 2.08 12.5023C1.96497 12.1768 2.13555 11.8197 2.46099 11.7047C2.78644 11.5897 3.14352 11.7603 3.25855 12.0857C3.34931 12.3425 3.59425 12.525 3.8802 12.525H4.7362C5.10015 12.525 5.39519 12.2299 5.39519 11.866V11.438C5.39519 11.074 5.10015 10.779 4.7362 10.779H4.05842C3.75679 10.779 3.45929 10.8492 3.1895 10.9841L2.87572 11.141C2.66546 11.2461 2.41399 11.2244 2.2249 11.0847C2.0358 10.9451 1.94107 10.7111 1.97972 10.4792L2.40771 7.91126C2.45794 7.60989 2.71868 7.38901 3.02421 7.38901H5.80619C6.15137 7.38901 6.43119 7.66883 6.43119 8.01401C6.43119 8.35919 6.15137 8.63901 5.80619 8.63901Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"cellular-network-lte\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189642)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.69384 2.25045C1.43062 2.47376 1.39826 2.86816 1.62156 3.13138C1.84486 3.3946 2.23926 3.42695 2.50248 3.20365C2.89045 2.87452 3.31311 2.58582 3.76418 2.34315C4.76201 1.80631 5.87121 1.50959 7.00365 1.47645C8.13712 1.51 9.24713 1.80806 10.2451 2.34694C10.6929 2.58879 11.1126 2.87612 11.498 3.20341C11.761 3.42687 12.1555 3.39475 12.3789 3.13166C12.6024 2.86858 12.5703 2.47415 12.3072 2.25069C11.8556 1.86717 11.3638 1.53046 10.839 1.24705C9.66378 0.61244 8.3559 0.262837 7.02079 0.226428C7.0095 0.226121 6.99821 0.226119 6.98692 0.226423C5.65335 0.262351 4.34676 0.610295 3.17195 1.24235C2.64351 1.52665 2.14835 1.86487 1.69384 2.25045ZM4.00259 5.1097C3.8028 4.82822 3.86903 4.43807 4.15051 4.23828C5.01043 3.62792 5.98144 3.27662 6.98659 3.22839C7.00593 3.22746 7.02531 3.22743 7.04466 3.2283C8.05232 3.27368 9.02626 3.62368 9.88887 4.23422C10.1706 4.43364 10.2374 4.8237 10.038 5.10544C9.83853 5.38719 9.44847 5.45393 9.16673 5.25452C8.49293 4.77762 7.75777 4.51758 7.01747 4.47846C6.27896 4.51967 5.54586 4.78075 4.87401 5.25762C4.59253 5.45741 4.20238 5.39118 4.00259 5.1097ZM1.63672 7.92558C1.63672 7.5804 1.3569 7.30058 1.01172 7.30058C0.666541 7.30058 0.386719 7.5804 0.386719 7.92558V13.1487C0.386719 13.4939 0.666541 13.7737 1.01172 13.7737H4.05856C4.40373 13.7737 4.68356 13.4939 4.68356 13.1487C4.68356 12.8036 4.40373 12.5237 4.05856 12.5237H1.63672V7.92558ZM8.41553 7.92558C8.41553 8.27076 8.13571 8.55058 7.79053 8.55058H6.67448V13.1487C6.67448 13.4939 6.39466 13.7737 6.04948 13.7737C5.7043 13.7737 5.42448 13.4939 5.42448 13.1487V8.55058H4.30843C3.96325 8.55058 3.68343 8.27076 3.68343 7.92558C3.68343 7.5804 3.96325 7.30058 4.30843 7.30058H7.79053C8.13571 7.30058 8.41553 7.5804 8.41553 7.92558ZM9.10445 7.48364C9.22166 7.36643 9.38063 7.30058 9.54639 7.30058H13.0285C13.3737 7.30058 13.6535 7.5804 13.6535 7.92558C13.6535 8.27076 13.3737 8.55058 13.0285 8.55058H10.1714V9.69453H12.5932C12.9384 9.69453 13.2182 9.97435 13.2182 10.3195C13.2182 10.6647 12.9384 10.9445 12.5932 10.9445H10.1714V12.5237H13.0285C13.3737 12.5237 13.6535 12.8036 13.6535 13.1487C13.6535 13.4939 13.3737 13.7737 13.0285 13.7737H9.54639C9.20121 13.7737 8.92139 13.4939 8.92139 13.1487V7.92558C8.92139 7.75982 8.98724 7.60085 9.10445 7.48364Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189642\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"contact-phonebook-2\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M11.1324 0.0625001H3.0464C2.47904 0.0623681 1.93377 0.283735 1.5272 0.679465C1.12056 1.07526 0.88454 1.61418 0.869408 2.18143L0.869141 2.20143V11.7527L0.869145 11.7552C0.869801 12.3316 1.09909 12.8844 1.50677 13.292C1.91503 13.7003 2.46876 13.9297 3.04613 13.9297H11.12L11.1328 13.9298L11.1457 13.9297H12.0837C12.4979 13.9297 12.8337 13.5939 12.8337 13.1797C12.8337 12.7655 12.4979 12.4297 12.0837 12.4297H11.8828V10.9013C12.0489 10.8197 12.2021 10.7108 12.3354 10.5774C12.6545 10.2584 12.8337 9.82565 12.8337 9.37443V1.76383C12.8337 1.31261 12.6545 0.879867 12.3354 0.560807C12.0164 0.241746 11.5836 0.0625001 11.1324 0.0625001ZM2.56743 12.2314C2.44047 12.1044 2.36914 11.9322 2.36914 11.7527C2.36916 11.5732 2.44048 11.401 2.56743 11.274C2.69439 11.1471 2.86658 11.0758 3.04613 11.0758H10.3828V12.4297H3.04613C2.86658 12.4297 2.69439 12.3583 2.56743 12.2314ZM8.5781 8.32505C8.22626 8.408 7.85714 8.37681 7.52421 8.23598C6.17901 7.42931 5.05331 6.30361 4.24664 4.95842C4.10706 4.62528 4.07653 4.25651 4.15942 3.90496C4.2423 3.55341 4.43435 3.23712 4.70805 3.00142L4.95466 2.75481C5.04076 2.64753 5.165 2.57769 5.30141 2.5599C5.43781 2.54211 5.57581 2.57774 5.68654 2.65935L6.48207 3.45487C6.56368 3.56561 6.59931 3.70361 6.58152 3.84001C6.56373 3.97641 6.49389 4.10066 6.38661 4.18676C6.27933 4.27285 6.20949 4.3971 6.1917 4.5335C6.17391 4.6699 6.20954 4.8079 6.29114 4.91864L7.56399 6.19148C7.67472 6.27309 7.81272 6.30872 7.94912 6.29093C8.08553 6.27314 8.20977 6.2033 8.29587 6.09602C8.38197 5.98874 8.50621 5.9189 8.64261 5.90111C8.77902 5.88332 8.91702 5.91895 9.02775 6.00055L9.82328 6.79608C9.90488 6.90682 9.94052 7.04482 9.92273 7.18122C9.90493 7.31762 9.8351 7.44186 9.72782 7.52796L9.4812 7.77458C9.24625 8.04929 8.92994 8.24209 8.5781 8.32505Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"hang-up-1\", \"keywords\": [ \"android\", \"phone\", \"mobile\", \"device\", \"smartphone\", \"iphone\", \"telephone\", \"hang\", \"up\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path d=\\\"M11.4584 4.23604C12.1428 4.38367 12.7555 4.76259 13.1935 5.30883C13.6305 5.85385 13.867 6.53241 13.8634 7.23089L13.8691 7.74049C13.8651 8.0965 13.7217 8.43679 13.47 8.68854C13.2182 8.94029 12.8779 9.08351 12.5219 9.08753L10.366 9.08756C10.0109 9.08447 9.67117 8.942 9.42003 8.69086C9.16889 8.43972 9.02644 8.09999 9.02335 7.74484C9.02292 7.69587 9.01615 7.64846 8.99746 7.60285C8.98003 7.56035 8.95433 7.52174 8.92185 7.48925C8.88937 7.45677 8.85076 7.43107 8.80825 7.41365C8.76575 7.39623 8.72021 7.38743 8.67428 7.38777L5.28579 7.38778C5.19187 7.38941 5.10222 7.42744 5.03576 7.4939C4.96877 7.56088 4.93067 7.65142 4.9296 7.74614C4.92652 8.10129 4.78407 8.43972 4.53293 8.69086C4.28179 8.942 3.94206 9.08445 3.58691 9.08754L1.43102 9.09889C1.07501 9.09487 0.73472 8.95159 0.482968 8.69984C0.231217 8.44809 0.0880061 8.1078 0.0839844 7.75179L0.0895827 7.24221C0.0859952 6.54372 0.322457 5.86515 0.75946 5.32012C1.19744 4.77388 1.81014 4.3951 2.49454 4.24747C5.44091 3.64369 8.50851 3.6711 11.4584 4.23604Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"hang-up-2\", \"keywords\": [ \"android\", \"phone\", \"mobile\", \"device\", \"smartphone\", \"iphone\", \"telephone\", \"hang\", \"up\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.174 4.06786C12.736 3.52162 12.1233 3.14271 11.4389 2.99508C8.48898 2.43014 5.42138 2.40273 2.47501 3.0065C1.79061 3.15413 1.17791 3.53291 0.739928 4.07916C0.302926 4.62418 0.066464 5.30276 0.0700515 6.00124L0.0644531 6.51082C0.0684749 6.86683 0.211685 7.20712 0.463436 7.45887C0.715188 7.71062 1.05548 7.8539 1.41149 7.85792L3.56738 7.84657C3.92254 7.84349 4.26227 7.70103 4.51341 7.44989C4.76455 7.19875 4.90699 6.86033 4.91007 6.50517C4.91114 6.41045 4.94925 6.31992 5.01623 6.25293C5.08269 6.18647 5.17234 6.14844 5.26626 6.14681L8.65475 6.1468C8.70068 6.14646 8.74622 6.15526 8.78873 6.17268C8.83123 6.19011 8.86984 6.21581 8.90232 6.24829C8.93481 6.28077 8.96051 6.31938 8.97793 6.36189C8.99662 6.40749 9.00339 6.4549 9.00382 6.50387C9.00691 6.85902 9.14936 7.19875 9.4005 7.44989C9.65164 7.70103 9.99137 7.84351 10.3465 7.84659L12.5024 7.84656C12.8584 7.84254 13.1987 7.69933 13.4505 7.44758C13.7022 7.19583 13.8456 6.85554 13.8496 6.49953L13.8439 5.98992C13.8474 5.29144 13.611 4.61288 13.174 4.06786ZM7.62549 7.93857C7.62549 7.59339 7.34567 7.31357 7.00049 7.31357C6.65531 7.31357 6.37549 7.59339 6.37549 7.93857V10.1595H5.12893C4.87614 10.1595 4.64824 10.3118 4.5515 10.5453C4.45476 10.7789 4.50824 11.0477 4.68698 11.2265L6.55264 13.0921C6.57526 13.1153 6.59966 13.1368 6.62564 13.1563C6.66028 13.1824 6.69701 13.2043 6.73517 13.2222C6.80301 13.2541 6.87751 13.2741 6.956 13.2796C6.9967 13.2825 7.03761 13.2814 7.07814 13.2764C7.10814 13.2727 7.13749 13.2668 7.16602 13.259C7.2673 13.2313 7.36297 13.1777 7.44253 13.0981L9.3142 11.2265C9.49295 11.0477 9.54642 10.7789 9.44968 10.5453C9.35295 10.3118 9.12505 10.1595 8.87226 10.1595H7.62549V7.93857Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"incoming-call\", \"keywords\": [ \"android\", \"phone\", \"mobile\", \"device\", \"smartphone\", \"iphone\", \"ringing\", \"incoming\", \"call\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189654)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.40047 5.99538C6.36529 6.07968 6.31404 6.15887 6.24672 6.22825C6.24134 6.2338 6.23588 6.23927 6.23033 6.24465C6.0958 6.3751 5.91255 6.45559 5.71051 6.45618C5.70976 6.45618 5.70901 6.45618 5.70826 6.45618L2.44253 6.45619C2.13919 6.45619 1.86571 6.27345 1.74962 5.9932C1.63354 5.71294 1.69771 5.39035 1.9122 5.17586L3.01473 4.07332L0.512639 1.57122C0.219745 1.27833 0.219745 0.803457 0.512639 0.510564C0.805532 0.21767 1.2804 0.21767 1.5733 0.510564L4.0754 3.01266L5.17793 1.91013C5.39243 1.69563 5.71501 1.63147 5.99527 1.74755C6.27553 1.86364 6.45826 2.13711 6.45826 2.44046V5.70618C6.45826 5.80869 6.4377 5.90639 6.40047 5.99538ZM6.7377 13.2792C6.2193 13.613 5.60227 13.7593 4.98918 13.6938C4.3761 13.6284 3.80389 13.355 3.3677 12.9192L2.9877 12.5392C2.81916 12.3689 2.72462 12.1389 2.72462 11.8992C2.72462 11.6596 2.81916 11.4296 2.9877 11.2592L4.5877 9.66922C4.75637 9.50129 4.98469 9.40701 5.2227 9.40701C5.46071 9.40701 5.68903 9.50129 5.8577 9.66922C6.02807 9.83777 6.25805 9.93231 6.4977 9.93231C6.73735 9.93231 6.96733 9.83777 7.1377 9.66922L9.6877 7.11922C9.85625 6.94886 9.95079 6.71888 9.95079 6.47922C9.95079 6.23957 9.85625 6.00959 9.6877 5.83922C9.51977 5.67056 9.42549 5.44223 9.42549 5.20422C9.42549 4.96621 9.51977 4.73789 9.6877 4.56922L11.2277 2.96922C11.3981 2.80068 11.6281 2.70614 11.8677 2.70614C12.1074 2.70614 12.3374 2.80068 12.5077 2.96922L12.8877 3.34922C13.3313 3.77926 13.614 4.34842 13.6886 4.96173C13.7633 5.57503 13.6253 6.19537 13.2977 6.71922C11.554 9.30801 9.32648 11.5355 6.7377 13.2792Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189654\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"missed-call\", \"keywords\": [ \"android\", \"phone\", \"mobile\", \"device\", \"smartphone\", \"iphone\", \"missed\", \"call\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189618)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.17878 1.59892C4.00003 1.42018 3.94655 1.15135 4.04329 0.917806C4.14003 0.684259 4.36793 0.531982 4.62072 0.531982H6.93469C7.27987 0.531982 7.55969 0.811804 7.55969 1.15698V3.47096C7.55969 3.72375 7.40742 3.95164 7.17387 4.04838C6.94032 4.14512 6.67149 4.09165 6.49274 3.9129L5.8659 3.28605L4.20035 4.9516C3.51693 5.63501 2.40889 5.63501 1.72548 4.95159L0.286074 3.51218C-0.00681806 3.21929 -0.00681615 2.74441 0.286078 2.45152C0.578972 2.15863 1.05385 2.15863 1.34674 2.45152L2.78614 3.89093C2.88377 3.98856 3.04206 3.98856 3.1397 3.89093L4.80523 2.22539L4.17878 1.59892ZM11.446 1.25C11.8536 1.25 12.2451 1.40956 12.5366 1.69454L12.9238 2.09032C13.4752 2.63633 13.8224 3.3552 13.9073 4.12669C13.992 4.89653 13.807 5.69794 13.3787 6.34521C11.5418 9.05244 9.19369 11.3908 6.48173 13.2208C5.82789 13.6426 5.04874 13.8265 4.27531 13.7414C3.50382 13.6565 2.78495 13.3093 2.23893 12.7578L1.84317 12.3706C1.55819 12.0791 1.39862 11.6877 1.39862 11.28C1.39862 10.8724 1.55812 10.4809 1.8431 10.1894L3.50415 8.54571L3.50568 8.54421C3.79581 8.25908 4.18633 8.0993 4.59311 8.0993C4.9999 8.0993 5.38926 8.25794 5.67939 8.54307C5.73733 8.59971 5.81513 8.63142 5.89615 8.63142C5.97632 8.63142 6.05332 8.60038 6.11106 8.54486L8.7053 5.9506L8.70856 5.94739C8.7362 5.92015 8.75815 5.88769 8.77313 5.85189C8.8188 5.74277 8.78575 5.60631 8.70149 5.52327C8.41636 5.23314 8.25658 4.84263 8.25658 4.43584C8.25658 4.02905 8.41634 3.63851 8.70147 3.34838L10.3503 1.69948L10.3554 1.69454C10.6468 1.40956 11.0383 1.25 11.446 1.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189618\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"notification-alarm-2\", \"keywords\": [ \"android\", \"phone\", \"mobile\", \"device\", \"smartphone\", \"iphone\", \"alert\", \"bell\", \"alarm\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189594)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.2503 2.5C13.2503 3.88071 12.131 5 10.7503 5C9.3696 5 8.25031 3.88071 8.25031 2.5C8.25031 1.11929 9.3696 0 10.7503 0C12.131 0 13.2503 1.11929 13.2503 2.5ZM7.46122 0.697322C7.03264 0.567626 6.58368 0.5 6.12774 0.5C4.90662 0.5 3.7355 0.985091 2.87204 1.84856C2.00857 2.71202 1.52348 3.88314 1.52348 5.10426V8.47675C1.52348 8.66116 1.45022 8.83803 1.31982 8.96843C1.18941 9.09883 1.01255 9.17209 0.828125 9.17209C0.413911 9.17209 0.078125 9.50788 0.078125 9.92209C0.078125 10.3363 0.413911 10.6721 0.828125 10.6721H11.4273C11.8416 10.6721 12.1773 10.3363 12.1773 9.92209C12.1773 9.50788 11.8416 9.17209 11.4273 9.17209C11.2429 9.17209 11.0661 9.09883 10.9357 8.96843C10.8053 8.83803 10.732 8.66116 10.732 8.47675V6.24996C8.66936 6.2401 7.00031 4.56496 7.00031 2.5C7.00031 1.84661 7.16742 1.23225 7.46122 0.697322ZM4.37775 12.75C4.37775 12.3358 4.71354 12 5.12775 12H7.12775C7.54197 12 7.87775 12.3358 7.87775 12.75C7.87775 13.1642 7.54197 13.5 7.12775 13.5H5.12775C4.71354 13.5 4.37775 13.1642 4.37775 12.75Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189594\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"notification-application-1\", \"keywords\": [ \"android\", \"phone\", \"mobile\", \"device\", \"smartphone\", \"iphone\", \"alert\", \"box\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189627)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 2.5C14 3.88071 12.8807 5 11.5 5C10.1193 5 9 3.88071 9 2.5C9 1.11929 10.1193 0 11.5 0C12.8807 0 14 1.11929 14 2.5ZM2 2.5H7.75C7.75 4.57107 9.42893 6.25 11.5 6.25V12C11.5 13.1046 10.6046 14 9.5 14H2C0.895431 14 0 13.1046 0 12V4.5C0 3.39543 0.895431 2.5 2 2.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189627\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"notification-application-2\", \"keywords\": [ \"android\", \"phone\", \"mobile\", \"device\", \"smartphone\", \"iphone\", \"alert\", \"box\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189603)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.6482 0.0240479C11.1314 0.0240479 11.5232 0.415799 11.5232 0.899048V3.39905C11.5232 3.8823 11.1314 4.27405 10.6482 4.27405C10.1649 4.27405 9.77319 3.8823 9.77319 3.39905V0.899048C9.77319 0.415799 10.1649 0.0240479 10.6482 0.0240479ZM9.64819 5.89905C9.64819 5.34676 10.0959 4.89905 10.6482 4.89905C11.2005 4.89905 11.6482 5.34676 11.6482 5.89905C11.6482 6.45133 11.2005 6.89905 10.6482 6.89905C10.0959 6.89905 9.64819 6.45133 9.64819 5.89905ZM8.64807 3.39908V2.50003H2C0.895431 2.50003 0 3.39546 0 4.50003V12C0 13.1046 0.895431 14 2 14H9.5C10.6046 14 11.5 13.1046 11.5 12V7.7839C11.2402 7.90149 10.9518 7.96695 10.6481 7.96695C9.50605 7.96695 8.5802 7.04111 8.5802 5.89902C8.5802 5.40411 8.75405 4.94982 9.04402 4.59386C8.79531 4.2605 8.64807 3.84698 8.64807 3.39908Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189603\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"notification-message-alert\", \"keywords\": [ \"android\", \"phone\", \"mobile\", \"device\", \"smartphone\", \"iphone\", \"alert\", \"message\", \"text\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189597)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.75 1.5C1.61193 1.5 1.5 1.61193 1.5 1.75V12.2388C1.50165 12.2429 1.50486 12.2499 1.51076 12.26C1.52996 12.2927 1.56669 12.3382 1.62329 12.3864C1.67937 12.4343 1.73851 12.4707 1.78463 12.4924C1.79111 12.4954 1.79688 12.4979 1.80187 12.5H8.25C8.38807 12.5 8.5 12.3881 8.5 12.25V9.75C8.5 9.33579 8.83579 9 9.25 9C9.66421 9 10 9.33579 10 9.75V12.25C10 13.2165 9.2165 14 8.25 14H1.75C1.51778 14 1.30218 13.9229 1.14782 13.8505C0.978094 13.7709 0.806129 13.6609 0.649999 13.5278C0.361255 13.2816 0 12.8419 0 12.25V1.75C0 0.783502 0.783502 0 1.75 0H5C5.41421 0 5.75 0.335786 5.75 0.75C5.75 1.16421 5.41421 1.5 5 1.5H1.75ZM10.0005 0C8.93964 0 7.92223 0.421427 7.17208 1.17157C6.42194 1.92172 6.00051 2.93913 6.00051 4C6.00339 4.54247 6.11209 5.07916 6.32051 5.58L5.63744 7.17938C5.57678 7.32142 5.69317 7.47569 5.8464 7.45636L7.64051 7.23C8.15057 7.60259 8.74162 7.84909 9.36527 7.94932C9.98892 8.04955 10.6274 8.00066 11.2286 7.80665C11.8297 7.61264 12.3763 7.27902 12.8237 6.83312C13.2711 6.38721 13.6065 5.84169 13.8025 5.24122C13.9985 4.64074 14.0495 4.00239 13.9514 3.37841C13.8532 2.75443 13.6087 2.16256 13.2378 1.65127C12.8669 1.13997 12.3802 0.723781 11.8175 0.436769C11.2548 0.149758 10.6322 8.36577e-05 10.0005 0ZM10.0005 1.5C10.2767 1.5 10.5005 1.72386 10.5005 2V3.5C10.5005 3.77614 10.2767 4 10.0005 4C9.72437 4 9.50051 3.77614 9.50051 3.5V2C9.50051 1.72386 9.72437 1.5 10.0005 1.5ZM10.7505 5.5C10.7505 5.08579 10.4147 4.75 10.0005 4.75C9.5863 4.75 9.25051 5.08579 9.25051 5.5C9.25051 5.91421 9.5863 6.25 10.0005 6.25C10.4147 6.25 10.7505 5.91421 10.7505 5.5ZM4.5 9.75C4.08579 9.75 3.75 10.0858 3.75 10.5C3.75 10.9142 4.08579 11.25 4.5 11.25H5.5C5.91421 11.25 6.25 10.9142 6.25 10.5C6.25 10.0858 5.91421 9.75 5.5 9.75H4.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189597\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"outgoing-call\", \"keywords\": [ \"android\", \"phone\", \"mobile\", \"device\", \"smartphone\", \"iphone\", \"ringing\", \"outgoing\", \"call\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189651)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.0016 0.753881C4.88552 0.473625 4.61204 0.290894 4.30869 0.290894H1.04297C0.839263 0.290894 0.654525 0.372106 0.519366 0.50392C0.514809 0.508365 0.510307 0.512866 0.505864 0.517425C0.437879 0.587146 0.386176 0.666826 0.350757 0.751696C0.313531 0.840692 0.292969 0.938392 0.292969 1.04089V4.30662C0.292969 4.60996 0.4757 4.88344 0.755957 4.99953C1.03621 5.11561 1.3588 5.05145 1.5733 4.83695L2.67583 3.73442L5.17793 6.23652C5.47083 6.52941 5.9457 6.52941 6.23859 6.23652C6.53149 5.94362 6.53149 5.46875 6.23859 5.17585L3.73649 2.67375L4.83902 1.57122C5.05352 1.35673 5.11769 1.03414 5.0016 0.753881ZM6.73774 13.2793C6.21934 13.6131 5.60231 13.7594 4.98922 13.6939C4.37614 13.6284 3.80393 13.355 3.36774 12.9193L2.98774 12.5393C2.8192 12.3689 2.72466 12.1389 2.72466 11.8993C2.72466 11.6596 2.8192 11.4296 2.98774 11.2593L4.58774 9.66925C4.75641 9.50132 4.98473 9.40704 5.22274 9.40704C5.46075 9.40704 5.68907 9.50132 5.85774 9.66925C6.02811 9.8378 6.25809 9.93234 6.49774 9.93234C6.73739 9.93234 6.96737 9.8378 7.13774 9.66925L9.68774 7.11925C9.85629 6.94888 9.95083 6.71891 9.95083 6.47925C9.95083 6.2396 9.85629 6.00962 9.68774 5.83925C9.51981 5.67059 9.42553 5.44226 9.42553 5.20425C9.42553 4.96624 9.51981 4.73792 9.68774 4.56925L11.2278 2.96925C11.3981 2.80071 11.6281 2.70617 11.8678 2.70617C12.1074 2.70617 12.3374 2.80071 12.5078 2.96925L12.8878 3.34925C13.3314 3.77929 13.6141 4.34845 13.6887 4.96175C13.7633 5.57506 13.6253 6.1954 13.2978 6.71925C11.554 9.30803 9.32652 11.5355 6.73774 13.2793Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189651\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"phone\", \"keywords\": [ \"android\", \"phone\", \"mobile\", \"device\", \"smartphone\", \"iphone\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189633)\\\">\\n<path d=\\\"M5.16951 13.2407C4.52896 13.654 3.76565 13.8341 3.00793 13.7507C2.25021 13.6674 1.54429 13.3257 1.00888 12.7831L0.540808 12.3254C0.335601 12.1155 0.220703 11.8336 0.220703 11.5401C0.220703 11.2465 0.335601 10.9646 0.540808 10.7547L2.52751 8.78885C2.73565 8.58429 3.0158 8.46967 3.30763 8.46967C3.59946 8.46967 3.87961 8.58429 4.08775 8.78885C4.29765 8.99405 4.57953 9.10895 4.87307 9.10895C5.16661 9.10895 5.44849 8.99405 5.65839 8.78885L8.77886 5.66837C8.88296 5.56579 8.96562 5.44353 9.02205 5.30871C9.07847 5.17389 9.10752 5.0292 9.10752 4.88305C9.10752 4.7369 9.07847 4.59221 9.02205 4.45739C8.96562 4.32257 8.88296 4.20032 8.77886 4.09773C8.57431 3.88959 8.45969 3.60944 8.45969 3.31761C8.45969 3.02579 8.57431 2.74563 8.77886 2.53749L10.7551 0.561194C10.965 0.355987 11.2469 0.241089 11.5405 0.241089C11.834 0.241089 12.1159 0.355987 12.3258 0.561194L12.7834 1.02927C13.326 1.56468 13.6677 2.27059 13.7511 3.02831C13.8344 3.78603 13.6544 4.54934 13.2411 5.1899C11.0879 8.36314 8.3483 11.0957 5.16951 13.2407Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189633\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"phone-mobile-phone\", \"keywords\": [ \"android\", \"phone\", \"mobile\", \"device\", \"smartphone\", \"iphone\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.5 1.75C3.5 1.61193 3.61193 1.5 3.75 1.5H10.25C10.3881 1.5 10.5 1.61193 10.5 1.75V12.25C10.5 12.3881 10.3881 12.5 10.25 12.5H3.80187C3.79688 12.4979 3.79111 12.4954 3.78463 12.4924C3.73851 12.4707 3.67937 12.4343 3.62329 12.3864C3.56669 12.3382 3.52996 12.2927 3.51076 12.26C3.50486 12.2499 3.50165 12.2429 3.5 12.2388V1.75ZM3.75 0C2.7835 0 2 0.783502 2 1.75V12.25C2 12.8419 2.36126 13.2816 2.65 13.5278C2.80613 13.6609 2.97809 13.7709 3.14782 13.8505C3.30218 13.9229 3.51778 14 3.75 14H10.25C11.2165 14 12 13.2165 12 12.25V1.75C12 0.783502 11.2165 0 10.25 0H3.75ZM6.5 9.75C6.08579 9.75 5.75 10.0858 5.75 10.5C5.75 10.9142 6.08579 11.25 6.5 11.25H7.5C7.91421 11.25 8.25 10.9142 8.25 10.5C8.25 10.0858 7.91421 9.75 7.5 9.75H6.5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"phone-qr\", \"keywords\": [ \"android\", \"phone\", \"mobile\", \"device\", \"smartphone\", \"iphone\", \"qr\", \"code\", \"scan\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4 1.5C3.86193 1.5 3.75 1.61193 3.75 1.75V12.2388C3.75165 12.2429 3.75486 12.2499 3.76076 12.26C3.77996 12.2927 3.81669 12.3382 3.87329 12.3864C3.92937 12.4343 3.98851 12.4707 4.03463 12.4924C4.04111 12.4954 4.04688 12.4979 4.05187 12.5H10.5C10.6381 12.5 10.75 12.3881 10.75 12.25V1.75C10.75 1.61193 10.6381 1.5 10.5 1.5H4ZM2.25 1.75C2.25 0.783502 3.0335 0 4 0H10.5C11.4665 0 12.25 0.783502 12.25 1.75V12.25C12.25 13.2165 11.4665 14 10.5 14H4C3.76778 14 3.55218 13.9229 3.39782 13.8505C3.22809 13.7709 3.05613 13.6609 2.9 13.5278C2.61126 13.2816 2.25 12.8419 2.25 12.25V1.75ZM4.57452 3.35608C4.57452 3.0109 4.85435 2.73108 5.19952 2.73108H6.01971C6.36489 2.73108 6.64471 3.0109 6.64471 3.35608C6.64471 3.70126 6.36489 3.98108 6.01971 3.98108H5.82452V4.58636C5.82452 4.93153 5.5447 5.21136 5.19952 5.21136C4.85435 5.21136 4.57452 4.93153 4.57452 4.58636V3.35608ZM9.30047 6.42191C9.64565 6.42191 9.92547 6.70173 9.92547 7.04691V7.86709C9.92547 8.21227 9.64565 8.49209 9.30047 8.49209H8.48029C8.13511 8.49209 7.85529 8.21227 7.85529 7.86709C7.85529 7.52191 8.13511 7.24209 8.48029 7.24209H8.67547V7.04691C8.67547 6.70173 8.95529 6.42191 9.30047 6.42191ZM5.19952 6.15723C4.85435 6.15723 4.57452 6.43705 4.57452 6.78223C4.57452 7.1274 4.85435 7.40723 5.19952 7.40723H5.6594V7.8671C5.6594 8.21228 5.93922 8.4921 6.2844 8.4921C6.62957 8.4921 6.9094 8.21228 6.9094 7.8671V6.78223C6.9094 6.43705 6.62957 6.15723 6.2844 6.15723H5.19952ZM7.44513 3.35608C7.44513 3.0109 7.72495 2.73108 8.07013 2.73108H9.30041C9.64559 2.73108 9.92541 3.0109 9.92541 3.35608V5.11628C9.92541 5.46146 9.64559 5.74128 9.30041 5.74128C8.95523 5.74128 8.67541 5.46146 8.67541 5.11628V3.98108H8.07013C7.72495 3.98108 7.44513 3.70126 7.44513 3.35608ZM7.20645 4.68015C6.89771 4.52578 6.52229 4.65092 6.36792 4.95966C6.21356 5.26839 6.3387 5.64382 6.64743 5.79818L7.29343 6.12118C7.60217 6.27555 7.97759 6.15041 8.13196 5.84168C8.28633 5.53294 8.16119 5.15752 7.85245 5.00315L7.20645 4.68015ZM6.75 9.75C6.33579 9.75 6 10.0858 6 10.5C6 10.9142 6.33579 11.25 6.75 11.25H7.75C8.16421 11.25 8.5 10.9142 8.5 10.5C8.5 10.0858 8.16421 9.75 7.75 9.75H6.75Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"phone-ringing-1\", \"keywords\": [ \"android\", \"phone\", \"mobile\", \"device\", \"smartphone\", \"iphone\", \"ringing\", \"incoming\", \"call\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189621)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.05079 0.860474C8.05079 0.44626 7.715 0.110474 7.30079 0.110474C6.3487 0.110474 5.40594 0.298 4.52633 0.662347C3.64672 1.02669 2.84749 1.56072 2.17426 2.23395C1.50104 2.90717 0.967001 3.70641 0.602655 4.58602C0.238308 5.46563 0.0507812 6.40839 0.0507812 7.36047C0.0507812 7.77469 0.386568 8.11047 0.800781 8.11047C1.215 8.11047 1.55079 7.77469 1.55079 7.36047C1.55079 6.60537 1.69951 5.85767 1.98848 5.16004C2.27744 4.46242 2.70098 3.82855 3.23492 3.29461C3.76886 2.76067 4.40273 2.33713 5.10036 2.04817C5.79798 1.7592 6.54568 1.61047 7.30079 1.61047C7.715 1.61047 8.05079 1.27469 8.05079 0.860474ZM6.9769 13.4596C6.4585 13.7934 5.84147 13.9397 5.22838 13.8742C4.6153 13.8087 4.0431 13.5354 3.6069 13.0996L3.2269 12.7196C3.05836 12.5492 2.96382 12.3192 2.96382 12.0796C2.96382 11.8399 3.05836 11.61 3.2269 11.4396L4.8269 9.84959C4.99557 9.68166 5.22389 9.58738 5.4619 9.58738C5.69992 9.58738 5.92824 9.68166 6.0969 9.84959C6.26727 10.0181 6.49725 10.1127 6.7369 10.1127C6.97656 10.1127 7.20654 10.0181 7.3769 9.84959L9.9269 7.29959C10.0955 7.12922 10.19 6.89924 10.19 6.65959C10.19 6.41994 10.0955 6.18996 9.9269 6.01959C9.75897 5.85092 9.66469 5.6226 9.66469 5.38459C9.66469 5.14658 9.75897 4.91826 9.9269 4.74959L11.4669 3.14959C11.6373 2.98104 11.8673 2.88651 12.1069 2.88651C12.3466 2.88651 12.5766 2.98104 12.7469 3.14959L13.1269 3.52959C13.5705 3.95963 13.8532 4.52879 13.9278 5.14209C14.0024 5.75539 13.8645 6.37574 13.5369 6.89959C11.7932 9.48837 9.56568 11.7159 6.9769 13.4596ZM7.61475 3.00296C8.02896 3.00296 8.36475 3.33875 8.36475 3.75296C8.36475 4.16717 8.02896 4.50296 7.61475 4.50296C6.7528 4.50296 5.92615 4.84537 5.31665 5.45486C4.70716 6.06436 4.36475 6.89101 4.36475 7.75296C4.36475 8.16717 4.02896 8.50296 3.61475 8.50296C3.20054 8.50296 2.86475 8.16717 2.86475 7.75296C2.86475 6.49318 3.36519 5.285 4.25599 4.3942C5.14679 3.5034 6.35497 3.00296 7.61475 3.00296Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189621\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"phone-ringing-2\", \"keywords\": [ \"android\", \"phone\", \"mobile\", \"device\", \"smartphone\", \"iphone\", \"ringing\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.53515 2.58172C1.53515 2.53659 1.57174 2.5 1.61688 2.5H7.43893C7.48406 2.5 7.52065 2.53659 7.52065 2.58172V11.7307C7.52065 11.7758 7.48406 11.8124 7.43893 11.8124H1.61688C1.57174 11.8124 1.53515 11.7758 1.53515 11.7307V2.58172ZM1.61688 1C0.743317 1 0.0351562 1.70816 0.0351562 2.58172V11.7307C0.0351562 12.6042 0.743317 13.3124 1.61688 13.3124H7.43893C8.31249 13.3124 9.02065 12.6042 9.02065 11.7307V2.58172C9.02065 1.70816 8.31249 1 7.43893 1H1.61688ZM4.11181 9.73306C3.6976 9.73306 3.36181 10.0688 3.36181 10.4831C3.36181 10.8973 3.6976 11.2331 4.11181 11.2331H4.94354C5.35775 11.2331 5.69354 10.8973 5.69354 10.4831C5.69354 10.0688 5.35775 9.73306 4.94354 9.73306H4.11181ZM11.7492 9.50952C12.8831 8.21801 12.8831 6.09448 11.7492 4.80297C11.5215 4.54357 11.5472 4.14867 11.8066 3.92095C12.066 3.69322 12.4609 3.7189 12.6886 3.9783C14.2366 5.74159 14.2366 8.5709 12.6886 10.3342C12.4609 10.5936 12.066 10.6193 11.8066 10.3915C11.5472 10.1638 11.5215 9.76892 11.7492 9.50952ZM9.86305 5.75111C10.1125 6.11278 10.2554 6.61931 10.2554 7.15617C10.2554 7.69303 10.1125 8.19956 9.86305 8.56123C9.66706 8.84537 9.73852 9.23459 10.0226 9.43059C10.3068 9.62658 10.696 9.55512 10.892 9.27098C11.313 8.66067 11.5054 7.89326 11.5054 7.15617C11.5054 6.41909 11.313 5.65167 10.892 5.04136C10.696 4.75722 10.3068 4.68577 10.0226 4.88176C9.73852 5.07775 9.66706 5.46697 9.86305 5.75111Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"signal-full\", \"keywords\": [ \"phone\", \"mobile\", \"device\", \"signal\", \"wireless\", \"smartphone\", \"iphone\", \"bar\", \"bars\", \"full\", \"android\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.0005 1H10.338C10.0619 1 9.83801 1.22386 9.83801 1.5V13.5C9.83801 13.7761 10.0619 14 10.338 14H13.0005C13.2766 14 13.5005 13.7761 13.5005 13.5V1.5C13.5005 1.22386 13.2766 1 13.0005 1ZM3.16247 8H0.5C0.223858 8 0 8.22386 0 8.5V13.5C0 13.7761 0.223857 14 0.5 14H3.16247C3.43862 14 3.66247 13.7761 3.66247 13.5V8.5C3.66247 8.22386 3.43862 8 3.16247 8ZM5.41925 5H8.08173C8.35787 5 8.58173 5.22386 8.58173 5.5V13.5C8.58173 13.7761 8.35787 14 8.08173 14H5.41925C5.14311 14 4.91925 13.7761 4.91925 13.5V5.5C4.91925 5.22386 5.14311 5 5.41925 5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"signal-low\", \"keywords\": [ \"phone\", \"mobile\", \"device\", \"signal\", \"wireless\", \"smartphone\", \"iphone\", \"bar\", \"low\", \"bars\", \"android\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.16247 8H0.5C0.223858 8 0 8.22386 0 8.5V13.5C0 13.7761 0.223857 14 0.5 14H3.16247C3.43862 14 3.66247 13.7761 3.66247 13.5V8.5C3.66247 8.22386 3.43862 8 3.16247 8ZM10.1162 13.25C10.1162 12.8358 10.452 12.5 10.8662 12.5H13.2411C13.6553 12.5 13.9911 12.8358 13.9911 13.25C13.9911 13.6642 13.6553 14 13.2411 14H10.8662C10.452 14 10.1162 13.6642 10.1162 13.25ZM5.81256 12.5C5.39835 12.5 5.06256 12.8358 5.06256 13.25C5.06256 13.6642 5.39835 14 5.81256 14H8.18742C8.60163 14 8.93742 13.6642 8.93742 13.25C8.93742 12.8358 8.60163 12.5 8.18742 12.5H5.81256Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"signal-medium\", \"keywords\": [ \"smartphone\", \"phone\", \"mobile\", \"device\", \"iphone\", \"signal\", \"medium\", \"wireless\", \"bar\", \"bars\", \"android\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.33124 5H5.66876C5.39262 5 5.16876 5.22386 5.16876 5.5V13.5C5.16876 13.7761 5.39262 14 5.66876 14H8.33124C8.60738 14 8.83124 13.7761 8.83124 13.5V5.5C8.83124 5.22386 8.60738 5 8.33124 5ZM3.16247 8H0.5C0.223858 8 0 8.22386 0 8.5V13.5C0 13.7761 0.223857 14 0.5 14H3.16247C3.43862 14 3.66247 13.7761 3.66247 13.5V8.5C3.66247 8.22386 3.43862 8 3.16247 8ZM10.8662 12.5C10.452 12.5 10.1162 12.8358 10.1162 13.25C10.1162 13.6642 10.452 14 10.8662 14H13.2411C13.6553 14 13.9911 13.6642 13.9911 13.25C13.9911 12.8358 13.6553 12.5 13.2411 12.5H10.8662Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"signal-none\", \"keywords\": [ \"phone\", \"mobile\", \"device\", \"signal\", \"wireless\", \"smartphone\", \"iphone\", \"bar\", \"bars\", \"no\", \"zero\", \"android\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 13.25C0 12.8358 0.335786 12.5 0.75 12.5H3.12485C3.53907 12.5 3.87485 12.8358 3.87485 13.25C3.87485 13.6642 3.53907 14 3.12485 14H0.75C0.335786 14 0 13.6642 0 13.25ZM10.1162 13.25C10.1162 12.8358 10.452 12.5 10.8662 12.5H13.2411C13.6553 12.5 13.9911 12.8358 13.9911 13.25C13.9911 13.6642 13.6553 14 13.2411 14H10.8662C10.452 14 10.1162 13.6642 10.1162 13.25ZM5.81256 12.5C5.39835 12.5 5.06256 12.8358 5.06256 13.25C5.06256 13.6642 5.39835 14 5.81256 14H8.18742C8.60163 14 8.93742 13.6642 8.93742 13.25C8.93742 12.8358 8.60163 12.5 8.18742 12.5H5.81256Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" } ], \"programing\": [ { \"name\": \"application-add\", \"keywords\": [ \"application\", \"new\", \"add\", \"square\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187509)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.07692 0H4.84615C5.44092 0 5.92308 0.482155 5.92308 1.07692V4.84615C5.92308 5.44092 5.44092 5.92308 4.84615 5.92308H1.07692C0.482155 5.92308 0 5.44092 0 4.84615V1.07692C0 0.482155 0.482155 0 1.07692 0ZM9.15383 8.0769H12.9231C13.5178 8.0769 14 8.55906 14 9.15383V12.9231C14 13.5178 13.5178 14 12.9231 14H9.15383C8.55906 14 8.0769 13.5178 8.0769 12.9231V9.15383C8.0769 8.55906 8.55906 8.0769 9.15383 8.0769ZM4.84615 8.0769H1.07692C0.482155 8.0769 0 8.55906 0 9.15383V12.9231C0 13.5178 0.482155 14 1.07692 14H4.84615C5.44092 14 5.92308 13.5178 5.92308 12.9231V9.15383C5.92308 8.55906 5.44092 8.0769 4.84615 8.0769ZM10.7607 0C11.175 0 11.5107 0.335786 11.5107 0.75V2.48273H13.2432C13.6575 2.48273 13.9932 2.81851 13.9932 3.23273C13.9932 3.64694 13.6575 3.98273 13.2432 3.98273H11.5107V5.71542C11.5107 6.12963 11.175 6.46542 10.7607 6.46542C10.3465 6.46542 10.0107 6.12963 10.0107 5.71542V3.98273H8.27783C7.86362 3.98273 7.52783 3.64694 7.52783 3.23273C7.52783 2.81851 7.86362 2.48273 8.27783 2.48273H10.0107V0.75C10.0107 0.335786 10.3465 0 10.7607 0Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187509\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bracket\", \"keywords\": [ \"code\", \"angle\", \"programming\", \"file\", \"bracket\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.4754 2.89321C10.0981 2.48992 9.46525 2.46887 9.06196 2.8462C8.65867 3.22353 8.63763 3.85634 9.01496 4.25963L11.579 7.00002L9.01496 9.7404C8.63763 10.1437 8.65867 10.7765 9.06196 11.1538C9.46525 11.5312 10.0981 11.5101 10.4754 11.1068L13.6786 7.68323C14.0383 7.29877 14.0383 6.70126 13.6786 6.31681L10.4754 2.89321ZM4.9842 4.25966C5.36153 3.85637 5.34048 3.22356 4.93719 2.84623C4.5339 2.4689 3.90109 2.48995 3.52376 2.89324L0.320561 6.31684C-0.0391453 6.70129 -0.0391453 7.2988 0.320561 7.68326L3.52376 11.1069C3.90109 11.5101 4.5339 11.5312 4.93719 11.1539C5.34048 10.7765 5.36153 10.1437 4.9842 9.74044L2.42023 7.00005L4.9842 4.25966Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"browser-add\", \"keywords\": [ \"app\", \"code\", \"apps\", \"add\", \"window\", \"plus\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187476)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.54687 2.81527V12.2494C1.54687 12.3623 1.6384 12.4538 1.75131 12.4538H12.2501C12.363 12.4538 12.4545 12.3623 12.4545 12.2494V2.81527H1.54687ZM1.75131 0.0461426C0.809976 0.0461426 0.046875 0.809244 0.046875 1.75058V12.2494C0.046875 13.1907 0.809976 13.9538 1.75131 13.9538H12.2501C13.1914 13.9538 13.9545 13.1907 13.9545 12.2494V1.75058C13.9545 0.809244 13.1914 0.0461426 12.2501 0.0461426H1.75131ZM3.75073 7.74994C3.75073 7.33573 4.08652 6.99994 4.50073 6.99994H6.25073V5.24994C6.25073 4.83573 6.58652 4.49994 7.00073 4.49994C7.41494 4.49994 7.75073 4.83573 7.75073 5.24994V6.99994H9.50073C9.91494 6.99994 10.2507 7.33573 10.2507 7.74994C10.2507 8.16415 9.91494 8.49994 9.50073 8.49994H7.75073V10.2499C7.75073 10.6642 7.41494 10.9999 7.00073 10.9999C6.58652 10.9999 6.25073 10.6642 6.25073 10.2499V8.49994H4.50073C4.08652 8.49994 3.75073 8.16415 3.75073 7.74994Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187476\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"browser-block\", \"keywords\": [ \"block\", \"access\", \"denied\", \"window\", \"browser\", \"privacy\", \"remove\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187479)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.54687 3.31527V12.2494C1.54687 12.3623 1.6384 12.4538 1.75131 12.4538H12.2501C12.363 12.4538 12.4545 12.3623 12.4545 12.2494V3.31527H1.54687ZM1.75131 0.0461426C0.809976 0.0461426 0.046875 0.809244 0.046875 1.75058V12.2494C0.046875 13.1907 0.809976 13.9538 1.75131 13.9538H12.2501C13.1914 13.9538 13.9545 13.1907 13.9545 12.2494V1.75058C13.9545 0.809244 13.1914 0.0461426 12.2501 0.0461426H1.75131ZM7.00081 3.99994C9.07098 3.99994 10.7492 5.67814 10.7492 7.74831C10.7492 8.73437 10.351 9.70261 9.65305 10.4006C8.95511 11.0985 7.98686 11.4967 7.00081 11.4967C4.93064 11.4967 3.25244 9.81848 3.25244 7.74831C3.25244 6.76231 3.65062 5.79396 4.34854 5.09604C5.04646 4.39812 6.0148 3.99994 7.00081 3.99994ZM7.00081 5.49994C8.24255 5.49994 9.24918 6.50657 9.24918 7.74831C9.24918 8.09521 9.17062 8.42377 9.03031 8.71715L6.03197 5.71881C6.32535 5.5785 6.6539 5.49994 7.00081 5.49994ZM4.97131 6.77947L7.96965 9.77781C7.67626 9.91812 7.34771 9.99668 7.00081 9.99668C5.75907 9.99668 4.75244 8.99005 4.75244 7.74831C4.75244 7.40141 4.831 7.07285 4.97131 6.77947Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187479\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"browser-build\", \"keywords\": [ \"build\", \"website\", \"development\", \"window\", \"code\", \"web\", \"backend\", \"browser\", \"dev\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187518)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.54102 12.2545V3.33386H4.8179V7.47767C4.8179 7.89188 5.15368 8.22767 5.5679 8.22767H9.8186C9.87698 8.24226 9.93808 8.25 10.001 8.25C10.0639 8.25 10.125 8.24226 10.1834 8.22767H12.4609V12.2545C12.4609 12.309 12.4393 12.3613 12.4008 12.3998C12.3623 12.4383 12.31 12.4599 12.2555 12.4599H1.74639C1.69193 12.4599 1.63969 12.4383 1.60117 12.3998C1.56266 12.3613 1.54102 12.309 1.54102 12.2545ZM10.751 6.72767H13.2109C13.6251 6.72767 13.9609 7.06346 13.9609 7.47767V12.2545C13.9609 12.7068 13.7813 13.1406 13.4614 13.4604C13.1416 13.7803 12.7078 13.9599 12.2555 13.9599H1.74639C1.2941 13.9599 0.860329 13.7803 0.540509 13.4604C0.220689 13.1406 0.0410156 12.7068 0.0410156 12.2545V1.74541C0.0410156 1.29312 0.220689 0.859352 0.540509 0.539532C0.860329 0.219712 1.2941 0.0400391 1.74639 0.0400391H5.5679C5.98211 0.0400391 6.3179 0.375825 6.3179 0.790039V1.83386H9.25098V0.790039C9.25098 0.375826 9.58677 0.0400391 10.001 0.0400391C10.4152 0.0400391 10.751 0.375825 10.751 0.790039V1.83386H13.211C13.6252 1.83386 13.961 2.16965 13.961 2.58386C13.961 2.99808 13.6252 3.33386 13.211 3.33386H10.751V6.72767ZM9.25098 6.72767V3.33386H6.3179V6.72767H9.25098Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187518\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"browser-check\", \"keywords\": [ \"checkmark\", \"pass\", \"window\", \"app\", \"code\", \"success\", \"check\", \"apps\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187506)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.54687 12.2494V3.31527H12.4545V12.2494C12.4545 12.3623 12.363 12.4538 12.2501 12.4538H1.75131C1.6384 12.4538 1.54687 12.3623 1.54687 12.2494ZM1.75131 0.0461426C0.809976 0.0461426 0.046875 0.809244 0.046875 1.75058V12.2494C0.046875 13.1907 0.809976 13.9538 1.75131 13.9538H12.2501C13.1914 13.9538 13.9545 13.1907 13.9545 12.2494V1.75058C13.9545 0.809244 13.1914 0.0461426 12.2501 0.0461426H1.75131ZM10.0651 6.43669C10.3379 6.12496 10.3063 5.65114 9.99461 5.37838C9.68288 5.10562 9.20906 5.1372 8.9363 5.44893L5.89431 8.92549L4.45073 7.84281C4.11936 7.59428 3.64926 7.66144 3.40073 7.99281C3.1522 8.32418 3.21936 8.79428 3.55073 9.04281L5.55073 10.5428C5.86458 10.7782 6.30682 10.7319 6.56516 10.4367L10.0651 6.43669Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187506\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"browser-delete\", \"keywords\": [ \"app\", \"code\", \"apps\", \"fail\", \"delete\", \"window\", \"remove\", \"cross\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187500)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.54687 12.2494V3.31527H12.4545V12.2494C12.4545 12.3623 12.363 12.4538 12.2501 12.4538H1.75131C1.6384 12.4538 1.54687 12.3623 1.54687 12.2494ZM1.75131 0.0461426C0.809976 0.0461426 0.046875 0.809244 0.046875 1.75058V12.2494C0.046875 13.1907 0.809976 13.9538 1.75131 13.9538H12.2501C13.1914 13.9538 13.9545 13.1907 13.9545 12.2494V1.75058C13.9545 0.809244 13.1914 0.0461426 12.2501 0.0461426H1.75131ZM4.4704 5.30048C4.76329 5.00759 5.23817 5.00759 5.53106 5.30048L7.00073 6.77015L8.4704 5.30048C8.76329 5.00759 9.23817 5.00759 9.53106 5.30048C9.82395 5.59337 9.82395 6.06825 9.53106 6.36114L8.06139 7.83081L9.53106 9.30048C9.82395 9.59337 9.82395 10.0682 9.53106 10.3611C9.23817 10.654 8.76329 10.654 8.4704 10.3611L7.00073 8.89147L5.53106 10.3611C5.23817 10.654 4.76329 10.654 4.4704 10.3611C4.17751 10.0682 4.17751 9.59337 4.4704 9.30048L5.94007 7.83081L4.4704 6.36114C4.17751 6.06825 4.17751 5.59337 4.4704 5.30048Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187500\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"browser-hash\", \"keywords\": [ \"window\", \"hash\", \"code\", \"internet\", \"language\", \"browser\", \"web\", \"tag\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187524)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.54687 12.2494V3.3153H12.4545V12.2494C12.4545 12.3623 12.363 12.4538 12.2501 12.4538H1.75131C1.6384 12.4538 1.54687 12.3623 1.54687 12.2494ZM1.75131 0.0461426C0.809976 0.0461426 0.046875 0.809244 0.046875 1.75058V12.2494C0.046875 13.1907 0.809976 13.9538 1.75131 13.9538H12.2501C13.1914 13.9538 13.9545 13.1907 13.9545 12.2494V1.75058C13.9545 0.809244 13.1914 0.0461426 12.2501 0.0461426H1.75131ZM6.78966 4.55551C7.18151 4.68976 7.39033 5.11626 7.25607 5.50811L7.12876 5.8797H8.26825L8.56215 5.02192C8.69641 4.63007 9.12291 4.42125 9.51476 4.55551C9.90661 4.68976 10.1154 5.11626 9.98117 5.50811L9.85386 5.8797H10.1799C10.5941 5.8797 10.9299 6.21549 10.9299 6.6297C10.9299 7.04391 10.5941 7.3797 10.1799 7.3797H9.33991L8.92018 8.60474H9.72584C10.14 8.60474 10.4758 8.94052 10.4758 9.35474C10.4758 9.76895 10.14 10.1047 9.72584 10.1047H8.40624L8.16446 10.8104C8.0302 11.2022 7.60371 11.4111 7.21186 11.2768C6.82 11.1425 6.61119 10.716 6.74544 10.3242L6.82064 10.1047H5.68114L5.43937 10.8104C5.30511 11.2022 4.87861 11.4111 4.48676 11.2768C4.09491 11.1425 3.88609 10.716 4.02035 10.3242L4.09554 10.1047H3.82153C3.40732 10.1047 3.07153 9.76895 3.07153 9.35474C3.07153 8.94052 3.40732 8.60474 3.82153 8.60474H4.60948L5.02921 7.3797H4.27563C3.86142 7.3797 3.52563 7.04391 3.52563 6.6297C3.52563 6.21549 3.86142 5.8797 4.27563 5.8797H5.54316L5.83706 5.02192C5.97132 4.63007 6.39781 4.42125 6.78966 4.55551ZM7.75431 7.3797L7.33458 8.60474H6.19508L6.61482 7.3797H7.75431Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187524\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"browser-lock\", \"keywords\": [ \"secure\", \"password\", \"window\", \"browser\", \"lock\", \"security\", \"login\", \"encryption\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187488)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.54687 3.31527V12.2494C1.54687 12.3623 1.6384 12.4538 1.75131 12.4538H12.2501C12.363 12.4538 12.4545 12.3623 12.4545 12.2494V3.31527H1.54687ZM1.75131 0.0461426C0.809976 0.0461426 0.046875 0.809244 0.046875 1.75058V12.2494C0.046875 13.1907 0.809976 13.9538 1.75131 13.9538H12.2501C13.1914 13.9538 13.9545 13.1907 13.9545 12.2494V1.75058C13.9545 0.809244 13.1914 0.0461426 12.2501 0.0461426H1.75131ZM7.00073 5.74994C6.51748 5.74994 6.12573 6.14169 6.12573 6.62494V7.12494H7.87573V6.62494C7.87573 6.14169 7.48398 5.74994 7.00073 5.74994ZM4.87573 6.62494V7.34427C4.64711 7.52753 4.50073 7.80914 4.50073 8.12494V10.1249C4.50073 10.6772 4.94845 11.1249 5.50073 11.1249H8.50073C9.05301 11.1249 9.50073 10.6772 9.50073 10.1249V8.12494C9.50073 7.80914 9.35435 7.52753 9.12573 7.34427V6.62494C9.12573 5.45133 8.17433 4.49994 7.00073 4.49994C5.82713 4.49994 4.87573 5.45133 4.87573 6.62494Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187488\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"browser-multiple-window\", \"keywords\": [ \"app\", \"code\", \"apps\", \"two\", \"window\", \"cascade\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187485)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.4428 8.22355V3.36011H4.39001V4.5539C4.39001 4.96811 4.05423 5.3039 3.64001 5.3039C3.2258 5.3039 2.89001 4.96811 2.89001 4.5539V1.73415C2.89001 0.792825 3.6531 0.0297284 4.59443 0.0297189L12.2383 0.029541C13.1797 0.029541 13.9428 0.792642 13.9428 1.73398V8.22355C13.9428 9.16489 13.1797 9.92799 12.2383 9.92799H11.4625C11.0483 9.92799 10.7125 9.5922 10.7125 9.17799C10.7125 8.76377 11.0483 8.42799 11.4625 8.42799H12.2383C12.3512 8.42799 12.4428 8.33646 12.4428 8.22355ZM1.60547 9.82742V12.2328C1.60547 12.3457 1.69699 12.4373 1.80989 12.4373L7.85585 12.4371C7.96876 12.4371 8.06031 12.3456 8.06031 12.2327V9.82742H1.60547ZM7.85585 6.49686L1.8099 6.49703C0.868579 6.49705 0.105469 7.26014 0.105469 8.20147V12.2328C0.105469 13.1742 0.86857 13.9373 1.8099 13.9373L7.85587 13.9371C8.79719 13.9371 9.56031 13.174 9.56031 12.2327V8.20129C9.56031 7.25996 8.79719 6.49686 7.85585 6.49686Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187485\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"browser-remove\", \"keywords\": [ \"app\", \"code\", \"apps\", \"subtract\", \"window\", \"minus\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187473)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.54687 12.2494V3.3153H12.4545V12.2494C12.4545 12.3623 12.363 12.4538 12.2501 12.4538H1.75131C1.6384 12.4538 1.54687 12.3623 1.54687 12.2494ZM1.75131 0.0461426C0.809976 0.0461426 0.046875 0.809244 0.046875 1.75058V12.2494C0.046875 13.1907 0.809976 13.9538 1.75131 13.9538H12.2501C13.1914 13.9538 13.9545 13.1907 13.9545 12.2494V1.75058C13.9545 0.809244 13.1914 0.0461426 12.2501 0.0461426H1.75131ZM4.87903 7.06177C4.46481 7.06177 4.12903 7.39755 4.12903 7.81177C4.12903 8.22598 4.46481 8.56177 4.87903 8.56177H9.1224C9.53661 8.56177 9.8724 8.22598 9.8724 7.81177C9.8724 7.39755 9.53661 7.06177 9.1224 7.06177H4.87903Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187473\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"browser-website-1\", \"keywords\": [ \"app\", \"code\", \"apps\", \"window\", \"website\", \"web\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187503)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.54687 12.2494V3.31527H12.4545V12.2494C12.4545 12.3623 12.363 12.4538 12.2501 12.4538H1.75131C1.6384 12.4538 1.54687 12.3623 1.54687 12.2494ZM0.046875 1.75058C0.046875 0.809244 0.809976 0.0461426 1.75131 0.0461426H12.2501C13.1914 0.0461426 13.9545 0.809244 13.9545 1.75058V12.2494C13.9545 13.1907 13.1914 13.9538 12.2501 13.9538H1.75131C0.809976 13.9538 0.046875 13.1907 0.046875 12.2494V1.75058Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187503\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bug\", \"keywords\": [ \"code\", \"bug\", \"security\", \"programming\", \"secure\", \"computer\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187494)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.1351 1.71069C10.1352 1.29648 9.79954 0.960583 9.38533 0.960449C8.97112 0.960316 8.63522 1.29599 8.63509 1.71021C8.635 1.99831 8.55875 2.28128 8.41408 2.53043C8.30426 2.71957 8.15797 2.88412 7.98442 3.01494C7.66616 2.95222 7.33718 2.91934 7.00053 2.91934C6.66389 2.91934 6.33493 2.95222 6.01668 3.01493C5.84314 2.88412 5.69685 2.71957 5.58703 2.53043C5.44237 2.28128 5.36612 1.99831 5.36603 1.71021C5.3659 1.29599 5.03 0.960316 4.61579 0.960449C4.20157 0.960583 3.8659 1.29648 3.86603 1.71069C3.86621 2.26319 4.01242 2.80584 4.28985 3.28363C4.35063 3.38832 4.41724 3.48907 4.48925 3.58548C4.01177 3.85896 3.58313 4.20803 3.21964 4.61637C2.99325 4.44884 2.74935 4.30489 2.49163 4.1873C1.96024 3.94484 1.38269 3.82019 0.7986 3.82189C0.384388 3.82309 0.0495797 4.15985 0.0507845 4.57406C0.0519893 4.98828 0.388751 5.32308 0.802963 5.32188C1.17073 5.32081 1.53439 5.3993 1.86897 5.55196C2.05806 5.63824 2.23534 5.74705 2.39712 5.87563C2.21281 6.27824 2.07978 6.70922 2.00635 7.16022H0.800781C0.386568 7.16022 0.0507814 7.496 0.0507814 7.91022C0.0507814 8.32443 0.386568 8.66022 0.800781 8.66022H1.98576C2.04799 9.123 2.17278 9.56597 2.35133 9.98031C2.20208 10.0935 2.04038 10.1902 1.86897 10.2684C1.53439 10.4211 1.17073 10.4996 0.802963 10.4985C0.388751 10.4973 0.0519893 10.8321 0.0507845 11.2463C0.0495797 11.6605 0.384388 11.9973 0.7986 11.9985C1.38269 12.0002 1.96024 11.8756 2.49163 11.6331C2.72148 11.5282 2.94034 11.4024 3.14558 11.2575C4.07374 12.348 5.45635 13.0397 7.00053 13.0397C8.5447 13.0397 9.92732 12.348 10.8555 11.2575C11.0607 11.4024 11.2796 11.5282 11.5094 11.6331C12.0408 11.8756 12.6184 12.0002 13.2025 11.9985C13.6167 11.9973 13.9515 11.6605 13.9503 11.2463C13.9491 10.8321 13.6123 10.4973 13.1981 10.4985C12.8303 10.4996 12.4667 10.4211 12.1321 10.2684C11.9607 10.1902 11.799 10.0935 11.6497 9.9803C11.8283 9.56596 11.953 9.123 12.0153 8.66022H13.2003C13.6145 8.66022 13.9503 8.32443 13.9503 7.91022C13.9503 7.496 13.6145 7.16022 13.2003 7.16022H11.9947C11.9213 6.70923 11.7882 6.27825 11.6039 5.87564C11.7657 5.74705 11.943 5.63824 12.1321 5.55196C12.4667 5.3993 12.8303 5.32081 13.1981 5.32188C13.6123 5.32308 13.9491 4.98828 13.9503 4.57406C13.9515 4.15985 13.6167 3.82309 13.2025 3.82189C12.6184 3.82019 12.0408 3.94484 11.5094 4.1873C11.2517 4.30489 11.0078 4.44885 10.7814 4.61638C10.4179 4.20805 9.98931 3.85899 9.51184 3.58551C9.58386 3.48909 9.65048 3.38833 9.71127 3.28363C9.9887 2.80584 10.1349 2.2632 10.1351 1.71069ZM7.00053 4.41934C8.42425 4.41934 9.65277 5.25505 10.2223 6.46268H3.77873C4.34828 5.25505 5.57681 4.41934 7.00053 4.41934Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187494\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bug-antivirus-debugging\", \"keywords\": [ \"code\", \"bug\", \"security\", \"programming\", \"secure\", \"computer\", \"antivirus\", \"block\", \"protection\", \"malware\", \"debugging\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187482)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.59375 7C1.59375 5.77754 1.99949 4.64984 2.68366 3.74423L4.53289 5.59346L4.53256 5.59548L8.73603 9.79895L8.73723 9.7978L10.2558 11.3163C9.35016 12.0005 8.22246 12.4062 7 12.4062C4.01421 12.4062 1.59375 9.98579 1.59375 7ZM7.59747 10.4282L3.44183 6.27253C3.11616 6.35427 2.875 6.64897 2.875 7C2.875 7.41421 3.21079 7.75 3.625 7.75H4.5V8C4.5 8.13848 4.51126 8.27434 4.53291 8.40669L3.57709 9.36251C3.2842 9.65541 3.2842 10.1303 3.57709 10.4232C3.86999 10.7161 4.34486 10.7161 4.63775 10.4232L5.26296 9.79797C5.71273 10.2326 6.32514 10.5 7 10.5C7.20592 10.5 7.40602 10.4751 7.59747 10.4282ZM5.26287 4.20212L3.74433 2.68358C4.64992 1.99946 5.77758 1.59375 7 1.59375C9.98579 1.59375 12.4062 4.01421 12.4062 7C12.4062 8.22242 12.0005 9.35008 11.3164 10.2557L9.46714 8.40639C9.48876 8.27413 9.5 8.13838 9.5 8V7.75H10.3749C10.7891 7.75 11.1249 7.41421 11.1249 7C11.1249 6.58579 10.7891 6.25 10.3749 6.25H9.5V6C9.5 5.86157 9.48875 5.72576 9.46711 5.59345L10.4231 4.63748C10.716 4.34458 10.716 3.86971 10.4231 3.57682C10.1302 3.28392 9.65532 3.28392 9.36242 3.57682L8.73713 4.20211C8.28734 3.76743 7.6749 3.5 7 3.5C6.3251 3.5 5.71265 3.76744 5.26287 4.20212ZM13.9062 7C13.9062 8.90298 13.1366 10.6262 11.8916 11.8753L11.8836 11.8835L11.8754 11.8915C10.6263 13.1365 8.90303 13.9062 7 13.9062C3.18578 13.9062 0.09375 10.8142 0.09375 7C0.09375 3.18578 3.18578 0.09375 7 0.09375C10.8142 0.09375 13.9062 3.18578 13.9062 7Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187482\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bug-antivirus-shield\", \"keywords\": [ \"code\", \"bug\", \"security\", \"programming\", \"secure\", \"computer\", \"antivirus\", \"shield\", \"protection\", \"malware\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187491)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1.5365C0 0.71569 0.679191 0.036499 1.5 0.036499H12.5C13.3208 0.036499 14 0.71569 14 1.5365V4.4565C13.9966 8.5825 11.3897 12.3786 7.54 13.863C7.36625 13.93 7.18311 13.9635 7 13.9635C6.81689 13.9635 6.63375 13.93 6.46 13.863C2.61027 12.3786 0.00341609 8.58291 0 4.45691V1.5365ZM4.82762 4.71162C4.80008 4.85111 4.78564 4.99531 4.78564 5.14288V5.19635C4.74542 5.24988 4.7136 5.31008 4.69213 5.375H4C3.65482 5.375 3.375 5.65482 3.375 6C3.375 6.34518 3.65482 6.625 4 6.625H4.78564V6.85716C4.78564 7.00472 4.80008 7.14892 4.82761 7.28841L3.98653 8.1295C3.74245 8.37357 3.74245 8.7693 3.98653 9.01338C4.2306 9.25746 4.62633 9.25746 4.87041 9.01338L5.4476 8.43619C5.8472 8.82908 6.39526 9.07145 6.99993 9.07145C7.60463 9.07145 8.15271 8.82906 8.55231 8.43614L9.12956 9.01338C9.37363 9.25746 9.76936 9.25746 10.0134 9.01338C10.2575 8.7693 10.2575 8.37357 10.0134 8.1295L9.17226 7.28832C9.19979 7.14886 9.21422 7.00469 9.21422 6.85716V6.625H10.0001C10.3452 6.625 10.6251 6.34518 10.6251 6C10.6251 5.65482 10.3452 5.375 10.0001 5.375H9.30773C9.28626 5.31008 9.25444 5.24988 9.21422 5.19635V5.14287C9.21422 4.99534 9.19979 4.85117 9.17226 4.71171L10.0134 3.87053C10.2575 3.62645 10.2575 3.23072 10.0134 2.98665C9.76936 2.74257 9.37363 2.74257 9.12956 2.98665L8.55231 3.56389C8.1527 3.17098 7.60462 2.92859 6.99993 2.92859C6.39526 2.92859 5.84721 3.17096 5.4476 3.56384L4.87041 2.98665C4.62633 2.74257 4.2306 2.74257 3.98653 2.98665C3.74245 3.23072 3.74245 3.62645 3.98653 3.87053L4.82762 4.71162Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187491\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bug-virus-browser\", \"keywords\": [ \"bug\", \"browser\", \"file\", \"virus\", \"threat\", \"danger\", \"internet\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187497)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.54687 12.2494V3.31527H12.4545V4.17067C12.4545 4.58488 12.7903 4.92067 13.2045 4.92067C13.6187 4.92067 13.9545 4.58488 13.9545 4.17067V1.75058C13.9545 0.809243 13.1914 0.0461426 12.2501 0.0461426H1.75131C0.809976 0.0461426 0.046875 0.809244 0.046875 1.75058V12.2494C0.046875 13.1907 0.809975 13.9538 1.75131 13.9538H3.99681C4.41102 13.9538 4.74681 13.618 4.74681 13.2038C4.74681 12.7896 4.41102 12.4538 3.99681 12.4538H1.75131C1.6384 12.4538 1.54687 12.3623 1.54687 12.2494ZM5.66222 6.78819C5.94072 6.48158 6.41504 6.45878 6.72166 6.73727L7.50275 7.44671C8.03078 6.93168 8.73954 6.6429 9.47809 6.6429C10.2166 6.6429 10.9254 6.93168 11.4534 7.4467L12.2345 6.73727C12.5411 6.45878 13.0154 6.48158 13.2939 6.78819C13.5724 7.09481 13.5496 7.56914 13.243 7.84763L12.2203 8.7765C12.2775 9.00195 12.3071 9.23542 12.3071 9.47194V9.50403H13.2045C13.6187 9.50403 13.9545 9.83982 13.9545 10.254C13.9545 10.6682 13.6187 11.004 13.2045 11.004H12.3071V11.0361C12.3071 11.2726 12.2775 11.5061 12.2203 11.7316L13.243 12.6604C13.5496 12.9389 13.5724 13.4132 13.2939 13.7199C13.0154 14.0265 12.5411 14.0493 12.2345 13.7708L11.4534 13.0614C10.9254 13.5764 10.2166 13.8651 9.4781 13.8651C8.73956 13.8651 8.0308 13.5764 7.50277 13.0613L6.72166 13.7708C6.41504 14.0493 5.94072 14.0265 5.66222 13.7199C5.38373 13.4132 5.40653 12.9389 5.71315 12.6604L6.73586 11.7315C6.67868 11.5061 6.64906 11.2726 6.64906 11.0361V11.004H5.75158C5.33737 11.004 5.00158 10.6682 5.00158 10.254C5.00158 9.83982 5.33737 9.50403 5.75158 9.50403H6.64905V9.47194C6.64905 9.23543 6.67866 9.00196 6.73584 8.77652L5.71315 7.84763C5.40653 7.56914 5.38373 7.09481 5.66222 6.78819Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187497\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bug-virus-document\", \"keywords\": [ \"bug\", \"document\", \"file\", \"virus\", \"threat\", \"danger\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187521)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0C1.10218 0 0.720644 0.158035 0.43934 0.43934C0.158035 0.720644 0 1.10218 0 1.5V10.5C0 10.8978 0.158036 11.2794 0.43934 11.5607C0.720644 11.842 1.10218 12 1.5 12H4.66391C4.67928 11.98 4.69507 11.9602 4.71129 11.9406C4.15531 11.5854 3.78669 10.9628 3.78669 10.254C3.78669 9.54533 4.15531 8.92272 4.71129 8.56748C4.08568 7.81319 4.09411 6.69416 4.77202 5.94779C5.46606 5.18366 6.61619 5.08054 7.43115 5.67646C8.06366 5.32952 8.77881 5.14291 9.51319 5.14291C10.0254 5.14291 10.5281 5.23366 11 5.40625V3.5C11 3.36739 10.9474 3.24021 10.8536 3.14645L7.85356 0.146447C7.75979 0.0526784 7.63261 0 7.5 0H1.5ZM5.69733 6.78823C5.97582 6.48161 6.45014 6.45881 6.75676 6.7373L7.53785 7.44675C8.06588 6.93172 8.77465 6.64293 9.51319 6.64293C10.2518 6.64293 10.9605 6.93171 11.4886 7.44674L12.2696 6.7373C12.5763 6.45881 13.0506 6.48161 13.3291 6.78823C13.6076 7.09485 13.5848 7.56917 13.2781 7.84767L12.2555 8.77654C12.3127 9.00199 12.3423 9.23545 12.3423 9.47197V9.50406H13.2397C13.6539 9.50406 13.9897 9.83985 13.9897 10.2541C13.9897 10.6683 13.6539 11.0041 13.2397 11.0041H12.3423V11.0361C12.3423 11.2727 12.3127 11.5061 12.2555 11.7316L13.2781 12.6605C13.5848 12.939 13.6076 13.4133 13.3291 13.7199C13.0506 14.0265 12.5763 14.0493 12.2696 13.7708L11.4886 13.0614C10.9605 13.5764 10.2518 13.8652 9.5132 13.8652C8.77466 13.8652 8.0659 13.5764 7.53787 13.0614L6.75676 13.7708C6.45014 14.0493 5.97582 14.0265 5.69733 13.7199C5.41883 13.4133 5.44163 12.939 5.74825 12.6605L6.77096 11.7316C6.71378 11.5061 6.68416 11.2727 6.68416 11.0361V11.0041H5.78669C5.37247 11.0041 5.03669 10.6683 5.03669 10.2541C5.03669 9.83985 5.37247 9.50406 5.78669 9.50406H6.68415V9.47198C6.68415 9.23546 6.71376 9.002 6.77094 8.77655L5.74825 7.84767C5.44163 7.56917 5.41883 7.09485 5.69733 6.78823Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187521\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"bug-virus-folder\", \"keywords\": [ \"bug\", \"document\", \"folder\", \"virus\", \"threat\", \"danger\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187554)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.19908 -6.37362e-05C5.53895 -0.00814938 5.87158 0.0994653 6.14237 0.305269C6.41529 0.51269 6.60894 0.807334 6.69106 1.14015L6.69344 1.14978L6.88531 1.99994H12.4856C12.8834 1.99994 13.265 2.15797 13.5463 2.43928C13.8276 2.72058 13.9856 3.10211 13.9856 3.49994V5.68781C13.2894 5.16479 12.3201 5.15119 11.6072 5.67246C10.9747 5.32552 10.2596 5.13892 9.52521 5.13892C8.79083 5.13892 8.07569 5.32552 7.44318 5.67246C6.62822 5.07654 5.47809 5.17966 4.78406 5.94378C4.10615 6.69016 4.09772 7.80919 4.72332 8.56348C4.16734 8.91872 3.79871 9.54134 3.79871 10.2501C3.79871 10.9588 4.16733 11.5814 4.72331 11.9366C4.58086 12.1084 4.47128 12.2991 4.39499 12.4999H1.51562C1.1178 12.4999 0.736269 12.3419 0.454964 12.0606C0.17366 11.7793 0.015625 11.3978 0.015625 10.9999V1.49994C0.015625 1.10211 0.17366 0.720581 0.454964 0.439276C0.736269 0.157972 1.1178 -6.37362e-05 1.51562 -6.37362e-05H5.19908ZM5.70934 6.78423C5.98784 6.47761 6.46216 6.45481 6.76878 6.7333L7.54987 7.44275C8.0779 6.92771 8.78667 6.63893 9.52521 6.63893C10.2637 6.63893 10.9725 6.92771 11.5005 7.44274L12.2816 6.7333C12.5882 6.45481 13.0625 6.47761 13.341 6.78423C13.6195 7.09084 13.5967 7.56517 13.2901 7.84366L12.2674 8.77253C12.3246 8.99799 12.3542 9.23145 12.3542 9.46797V9.50006H13.2517C13.6659 9.50006 14.0017 9.83585 14.0017 10.2501C14.0017 10.6643 13.6659 11.0001 13.2517 11.0001H12.3543V11.0321C12.3543 11.2687 12.3246 11.5021 12.2674 11.7276L13.2901 12.6565C13.5967 12.935 13.6195 13.4093 13.341 13.7159C13.0625 14.0225 12.5882 14.0453 12.2816 13.7668L11.5005 13.0574C10.9725 13.5724 10.2637 13.8612 9.52522 13.8612C8.78668 13.8612 8.07792 13.5724 7.54989 13.0574L6.76878 13.7668C6.46216 14.0453 5.98784 14.0225 5.70934 13.7159C5.43085 13.4093 5.45365 12.935 5.76027 12.6565L6.78298 11.7276C6.7258 11.5021 6.69618 11.2687 6.69618 11.0321V11.0001H5.79871C5.38449 11.0001 5.04871 10.6643 5.04871 10.2501C5.04871 9.83585 5.38449 9.50006 5.79871 9.50006H6.69617V9.46797C6.69617 9.23146 6.72578 8.998 6.78296 8.77255L5.76027 7.84366C5.45365 7.56517 5.43085 7.09084 5.70934 6.78423Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187554\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cloud-add\", \"keywords\": [ \"cloud\", \"network\", \"internet\", \"add\", \"server\", \"plus\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187542)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.88381 0.0605549C6.89196 -0.05967 5.88886 0.163768 5.04181 0.693609C4.25349 1.18671 3.64467 1.91745 3.30135 2.77774C2.87262 2.82504 2.45508 2.94713 2.06781 3.13888C1.63324 3.35405 1.24531 3.6527 0.92616 4.01779C0.281611 4.75512 -0.0436335 5.7183 0.0219743 6.69544C0.087582 7.67257 0.538668 8.58362 1.276 9.22817C1.65376 9.55839 2.09079 9.8048 2.55861 9.95778C2.77382 9.08534 3.5616 8.43839 4.50049 8.43839H5.00049V7.93839C5.00049 6.83382 5.89592 5.93839 7.00049 5.93839C8.10506 5.93839 9.00049 6.83382 9.00049 7.93839V8.43839H9.50049C10.474 8.43839 11.2851 9.13397 11.4638 10.0553C12.0005 9.93492 12.5014 9.67863 12.9162 9.30595C13.5042 8.77758 13.878 8.05195 13.9669 7.26645C14.0558 6.48096 13.8536 5.69013 13.3985 5.04371C12.9811 4.45071 12.3775 4.01701 11.6866 3.80943C11.5613 2.88615 11.1436 2.02521 10.4923 1.35427C9.79633 0.637401 8.87567 0.18078 7.88381 0.0605549ZM3.75049 10.4384C3.75049 10.0242 4.08628 9.68839 4.50049 9.68839H6.25049V7.93839C6.25049 7.52417 6.58628 7.18839 7.00049 7.18839C7.4147 7.18839 7.75049 7.52417 7.75049 7.93839V9.68839H9.50049C9.9147 9.68839 10.2505 10.0242 10.2505 10.4384C10.2505 10.8526 9.9147 11.1884 9.50049 11.1884H7.75049V12.9384C7.75049 13.3526 7.4147 13.6884 7.00049 13.6884C6.58628 13.6884 6.25049 13.3526 6.25049 12.9384V11.1884H4.50049C4.08628 11.1884 3.75049 10.8526 3.75049 10.4384Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187542\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cloud-block\", \"keywords\": [ \"cloud\", \"network\", \"internet\", \"block\", \"server\", \"deny\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187536)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.04181 0.747564C5.88886 0.217723 6.89196 -0.00571489 7.88381 0.11451C8.87567 0.234735 9.79633 0.691356 10.4923 1.40823C11.1436 2.07916 11.5613 2.9401 11.6866 3.86339C12.3775 4.07096 12.9811 4.50466 13.3985 5.09767C13.8536 5.74408 14.0558 6.53491 13.9669 7.32041C13.878 8.1059 13.5042 8.83153 12.9162 9.3599C12.6147 9.6308 12.2677 9.8402 11.8945 9.98059C11.7179 7.43167 9.59428 5.41875 7.0005 5.41875C4.45599 5.41875 2.36394 7.35591 2.11866 9.8359C1.81639 9.69197 1.53253 9.50637 1.276 9.28213C0.538668 8.63758 0.087582 7.72653 0.0219743 6.74939C-0.0436335 5.77225 0.281611 4.80908 0.92616 4.07175C1.24531 3.70666 1.63324 3.408 2.06781 3.19284C2.45508 3.00109 2.87262 2.87899 3.30135 2.83169C3.64467 1.97141 4.25349 1.24066 5.04181 0.747564ZM7.00052 8.19757C8.17525 8.19757 9.12757 9.14988 9.12757 10.3246C9.12757 10.6375 9.06002 10.9346 8.93871 11.2021L6.12304 8.38643C6.39056 8.26512 6.68765 8.19757 7.00052 8.19757ZM4.4839 7.7127C4.46681 7.72699 4.45021 7.74216 4.43416 7.75821C4.41811 7.77426 4.40294 7.79085 4.38866 7.80794C3.76005 8.46016 3.37347 9.34724 3.37347 10.3246C3.37347 12.3278 4.99735 13.9517 7.00052 13.9517C7.97767 13.9517 8.86458 13.5653 9.51676 12.9369C9.53401 12.9225 9.55076 12.9072 9.56695 12.891C9.58315 12.8748 9.59845 12.8581 9.61286 12.8408C10.2412 12.1886 10.6276 11.3017 10.6276 10.3246C10.6276 8.32146 9.00368 6.69757 7.00052 6.69757C6.02317 6.69757 5.13612 7.08413 4.4839 7.7127ZM7.87807 12.2628C7.61052 12.3841 7.31341 12.4517 7.00052 12.4517C5.82578 12.4517 4.87347 11.4994 4.87347 10.3246C4.87347 10.0117 4.94103 9.71461 5.06236 9.44707L7.87807 12.2628Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187536\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cloud-check\", \"keywords\": [ \"cloud\", \"network\", \"internet\", \"check\", \"server\", \"approve\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187527)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.88381 0.11451C6.89196 -0.00571489 5.88886 0.217723 5.04181 0.747564C4.25349 1.24066 3.64467 1.97141 3.30135 2.83169C2.87262 2.87899 2.45508 3.00109 2.06781 3.19284C1.63324 3.408 1.24531 3.70666 0.92616 4.07175C0.281611 4.80908 -0.0436335 5.77225 0.0219743 6.74939C0.087582 7.72653 0.538668 8.63758 1.276 9.28213C1.59149 9.55791 1.94832 9.77524 2.3299 9.92851C2.35227 9.8946 2.37579 9.86116 2.40049 9.82823C3.06323 8.94457 4.31683 8.76549 5.20049 9.42823L5.72828 9.82407L8.50567 6.6995C9.23951 5.87394 10.5037 5.79957 11.3292 6.53341C12.1548 7.26725 12.2292 8.53139 11.4953 9.35696L10.7571 10.1874H10.7734H10.7764C11.5669 10.1827 12.3282 9.88827 12.9162 9.3599C13.5042 8.83153 13.878 8.1059 13.9669 7.32041C14.0558 6.53491 13.8536 5.74408 13.3985 5.09767C12.9811 4.50466 12.3775 4.07096 11.6866 3.86339C11.5613 2.9401 11.1436 2.07916 10.4923 1.40823C9.79633 0.691356 8.87567 0.234735 7.88381 0.11451ZM10.5611 8.5265C10.8362 8.21691 10.8084 7.74286 10.4988 7.46767C10.1892 7.19248 9.71512 7.22037 9.43993 7.52996L5.89841 11.5142L4.45049 10.4282C4.11912 10.1797 3.64902 10.2469 3.40049 10.5782C3.15196 10.9096 3.21912 11.3797 3.55049 11.6282L5.55049 13.1282C5.86254 13.3623 6.3019 13.318 6.56105 13.0265L10.5611 8.5265Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187527\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cloud-data-transfer\", \"keywords\": [ \"cloud\", \"data\", \"transfer\", \"internet\", \"server\", \"network\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187563)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.88381 0.11451C6.89196 -0.00571489 5.88886 0.217723 5.04181 0.747564C4.25349 1.24066 3.64467 1.97141 3.30135 2.83169C2.87262 2.87899 2.45508 3.00109 2.06781 3.19284C1.63324 3.408 1.24531 3.70666 0.92616 4.07175C0.281611 4.80908 -0.0436335 5.77225 0.0219743 6.74939C0.087582 7.72653 0.538668 8.63758 1.276 9.28213C1.37819 9.37146 1.48472 9.45466 1.59503 9.53153C1.48937 8.91359 1.67505 8.25554 2.15207 7.77852L4.15207 5.77852C4.72407 5.20652 5.5843 5.03541 6.33165 5.34497C6.58837 5.45131 6.81439 5.60716 7.00053 5.79876C7.36392 5.42496 7.87219 5.19273 8.43469 5.19273C9.53926 5.19273 10.4347 6.08816 10.4347 7.19273V9.19273C10.9465 9.19273 11.4584 9.388 11.8489 9.77852C11.901 9.8306 11.9496 9.88485 11.9947 9.94096C12.3299 9.80196 12.6419 9.60638 12.9162 9.3599C13.5042 8.83153 13.878 8.1059 13.9669 7.32041C14.0558 6.53491 13.8536 5.74408 13.3985 5.09767C12.9811 4.50466 12.3775 4.07096 11.6866 3.86339C11.5613 2.9401 11.1436 2.07916 10.4923 1.40823C9.79633 0.691356 8.87567 0.234735 7.88381 0.11451ZM5.8533 6.49982C6.13355 6.61591 6.31629 6.88939 6.31629 7.19273V13.1927C6.31629 13.6069 5.9805 13.9427 5.56629 13.9427C5.15207 13.9427 4.81629 13.6069 4.81629 13.1927V9.00339L4.09662 9.72306C3.80372 10.016 3.32885 10.016 3.03596 9.72306C2.74306 9.43017 2.74306 8.9553 3.03596 8.6624L5.03596 6.6624C5.25045 6.44791 5.57304 6.38374 5.8533 6.49982ZM9.18469 7.19273C9.18469 6.77852 8.84891 6.44273 8.43469 6.44273C8.02048 6.44273 7.68469 6.77852 7.68469 7.19273V13.1927C7.68469 13.4961 7.86743 13.7696 8.14768 13.8856C8.42794 14.0017 8.75053 13.9376 8.96502 13.7231L10.965 11.7231C11.2579 11.4302 11.2579 10.9553 10.965 10.6624C10.6721 10.3695 10.1973 10.3695 9.90436 10.6624L9.18469 11.3821V7.19273Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187563\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cloud-refresh\", \"keywords\": [ \"cloud\", \"network\", \"internet\", \"server\", \"refresh\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187530)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.88381 0.0461506C6.89196 -0.0740743 5.88886 0.149364 5.04181 0.679205C4.25349 1.17231 3.64467 1.90305 3.30135 2.76333C2.87262 2.81063 2.45508 2.93273 2.06781 3.12448C1.63324 3.33964 1.24531 3.6383 0.92616 4.00339C0.281611 4.74072 -0.0436335 5.7039 0.0219743 6.68103C0.087582 7.65817 0.538668 8.56922 1.276 9.21377C1.65665 9.54652 2.0975 9.79417 2.56938 9.94688C2.94227 7.83768 4.78428 6.2356 7.00049 6.2356C7.83809 6.2356 8.62328 6.46516 9.29511 6.86401C9.9511 6.6166 10.7016 6.72872 11.261 7.18275C11.9244 7.72127 12.1725 8.62183 11.8783 9.42412L11.6719 9.98686C12.1297 9.85166 12.5555 9.6156 12.9162 9.29154C13.5042 8.76317 13.878 8.03754 13.9669 7.25205C14.0558 6.46655 13.8536 5.67572 13.3985 5.02931C12.9811 4.43631 12.3775 4.0026 11.6866 3.79503C11.5613 2.87174 11.1436 2.0108 10.4923 1.33987C9.79633 0.622997 8.87567 0.166376 7.88381 0.0461506ZM7.00049 8.9856C6.03399 8.9856 5.25049 9.7691 5.25049 10.7356C5.25049 11.7021 6.03399 12.4856 7.00049 12.4856C7.4329 12.4856 7.82698 12.3297 8.13251 12.0702C8.44821 11.8021 8.92152 11.8406 9.18968 12.1563C9.45783 12.472 9.41929 12.9453 9.10359 13.2135C8.53712 13.6946 7.80182 13.9856 7.00049 13.9856C5.20556 13.9856 3.75049 12.5305 3.75049 10.7356C3.75049 8.94067 5.20556 7.4856 7.00049 7.4856C7.85183 7.4856 8.62627 7.81271 9.20547 8.34805L9.60299 8.0996C9.87473 7.92976 10.2244 7.95133 10.4732 8.15328C10.722 8.35522 10.815 8.69293 10.7047 8.99379L10.1592 10.4813C10.1492 10.5102 10.1374 10.5383 10.124 10.5656C10.0883 10.6386 10.0417 10.7039 9.98673 10.76C9.90183 10.847 9.79509 10.9142 9.67282 10.9519C9.61676 10.9694 9.55835 10.9803 9.49868 10.9841C9.43313 10.9884 9.36852 10.984 9.30616 10.9718L7.87306 10.7247C7.56657 10.6718 7.32449 10.4353 7.26455 10.1301C7.20461 9.82495 7.33925 9.51443 7.60299 9.3496L7.84144 9.20057C7.59177 9.06353 7.30512 8.9856 7.00049 8.9856Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187530\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cloud-share\", \"keywords\": [ \"cloud\", \"network\", \"internet\", \"server\", \"share\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187515)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.88381 0.0461506C6.89196 -0.0740743 5.88886 0.149364 5.04181 0.679205C4.25349 1.17231 3.64467 1.90305 3.30135 2.76333C2.87262 2.81063 2.45508 2.93273 2.06781 3.12448C1.63324 3.33964 1.24531 3.6383 0.92616 4.00339C0.281612 4.74072 -0.0436335 5.7039 0.0219743 6.68103C0.0875819 7.65817 0.538668 8.56922 1.276 9.21377C1.58948 9.4878 1.94378 9.70411 2.32261 9.85721C2.60684 8.64124 3.6979 7.7356 5.00049 7.7356C5.33803 7.7356 5.66218 7.79683 5.96176 7.90865L6.36178 7.70864C6.69704 6.5682 7.75153 5.7356 9.00049 5.7356C10.5193 5.7356 11.7505 6.96681 11.7505 8.4856C11.7505 9.08125 11.5611 9.63267 11.2393 10.0829C11.8602 9.98918 12.4441 9.71576 12.9162 9.29154C13.5042 8.76317 13.878 8.03754 13.9669 7.25205C14.0558 6.46655 13.8536 5.67572 13.3985 5.02931C12.9811 4.43631 12.3775 4.0026 11.6866 3.79503C11.5613 2.87174 11.1436 2.0108 10.4923 1.33987C9.79633 0.622997 8.87567 0.166376 7.88381 0.0461506ZM9.00049 6.9856C8.17206 6.9856 7.50049 7.65717 7.50049 8.4856C7.50049 8.5026 7.50077 8.51954 7.50133 8.5364L5.94066 9.31674C5.68346 9.10959 5.35645 8.9856 5.00049 8.9856C4.17206 8.9856 3.50049 9.65717 3.50049 10.4856C3.50049 11.314 4.17206 11.9856 5.00049 11.9856C5.35645 11.9856 5.68346 11.8616 5.94066 11.6545L7.50133 12.4348C7.50077 12.4517 7.50049 12.4686 7.50049 12.4856C7.50049 13.314 8.17206 13.9856 9.00049 13.9856C9.82892 13.9856 10.5005 13.314 10.5005 12.4856C10.5005 11.6572 9.82892 10.9856 9.00049 10.9856C8.64453 10.9856 8.31752 11.1096 8.06032 11.3167L6.49965 10.5364C6.50021 10.5195 6.50049 10.5026 6.50049 10.4856C6.50049 10.4686 6.50021 10.4517 6.49965 10.4348L8.06032 9.65445C8.31752 9.8616 8.64453 9.9856 9.00049 9.9856C9.82892 9.9856 10.5005 9.31402 10.5005 8.4856C10.5005 7.65717 9.82892 6.9856 9.00049 6.9856Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187515\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cloud-warning\", \"keywords\": [ \"cloud\", \"network\", \"internet\", \"server\", \"warning\", \"alert\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187545)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.88381 0.0461506C6.89196 -0.0740743 5.88886 0.149364 5.04181 0.679205C4.25349 1.17231 3.64467 1.90305 3.30135 2.76333C2.87262 2.81063 2.45508 2.93273 2.06781 3.12448C1.63324 3.33964 1.24531 3.6383 0.92616 4.00339C0.281612 4.74072 -0.0436335 5.7039 0.0219743 6.68103C0.0875819 7.65817 0.538668 8.56922 1.276 9.21377C2.00897 9.8545 2.96512 10.1797 3.93631 10.1191H5.00049V7.13959C5.00049 6.03502 5.89592 5.13959 7.00049 5.13959C8.10506 5.13959 9.00049 6.03502 9.00049 7.13959V10.1191H10.7734H10.7764C11.5669 10.1143 12.3282 9.81991 12.9162 9.29154C13.5042 8.76317 13.878 8.03754 13.9669 7.25205C14.0558 6.46655 13.8536 5.67572 13.3985 5.02931C12.9811 4.43631 12.3775 4.0026 11.6866 3.79503C11.5613 2.87174 11.1436 2.0108 10.4923 1.33987C9.79633 0.622997 8.87567 0.166376 7.88381 0.0461506ZM7.00049 6.38959C7.4147 6.38959 7.75049 6.72537 7.75049 7.13959V10.6396C7.75049 11.0538 7.4147 11.3896 7.00049 11.3896C6.58628 11.3896 6.25049 11.0538 6.25049 10.6396V7.13959C6.25049 6.72537 6.58628 6.38959 7.00049 6.38959ZM8.00049 12.9856C8.00049 13.5379 7.55277 13.9856 7.00049 13.9856C6.44821 13.9856 6.00049 13.5379 6.00049 12.9856C6.00049 12.4333 6.44821 11.9856 7.00049 11.9856C7.55277 11.9856 8.00049 12.4333 8.00049 12.9856Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187545\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"cloud-wifi\", \"keywords\": [ \"cloud\", \"wifi\", \"internet\", \"server\", \"network\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.98004 0.0693359C5.6592 0.0693359 4.35565 0.379558 3.14762 0.976877C2.77631 1.16047 2.62414 1.6103 2.80774 1.98161C2.99133 2.35291 3.44116 2.50508 3.81247 2.32149C4.81971 1.82345 5.89634 1.56934 6.98004 1.56934C8.06375 1.56934 9.14038 1.82345 10.1477 2.32149C10.519 2.50508 10.9688 2.35291 11.1524 1.98161C11.336 1.6103 11.1838 1.16047 10.8125 0.976877C9.60443 0.379558 8.30089 0.0693359 6.98004 0.0693359ZM9.07763 2.8179C7.73314 2.2471 6.26703 2.2471 4.92255 2.8179C4.54127 2.97977 4.36341 3.42008 4.52528 3.80135C4.68715 4.18263 5.12745 4.36049 5.50873 4.19862C6.47861 3.78686 7.52156 3.78686 8.49144 4.19862C8.87272 4.36049 9.31303 4.18263 9.47489 3.80135C9.63676 3.42008 9.4589 2.97977 9.07763 2.8179ZM5.30866 5.8809C6.03731 5.42512 6.9002 5.23291 7.75342 5.33633C8.60664 5.43975 9.39862 5.83255 9.99728 6.44922C10.5459 7.01425 10.9017 7.73595 11.0175 8.51125C11.5961 8.69654 12.1008 9.06585 12.4528 9.56585C12.8471 10.126 13.0223 10.8113 12.9453 11.492C12.8682 12.1727 12.5443 12.8016 12.0347 13.2595C11.5252 13.7173 10.8654 13.9725 10.1804 13.9766H10.1774H4.42196C3.58416 14.0282 2.75949 13.7474 2.12717 13.1947C1.49046 12.6381 1.10093 11.8514 1.04428 11.0076C0.987622 10.1638 1.26848 9.33206 1.82507 8.69535C2.10066 8.38008 2.43565 8.12219 2.81092 7.93638C3.1312 7.77781 3.47546 7.67439 3.82921 7.63005C4.12752 6.90896 4.6439 6.29671 5.30866 5.8809Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"code-analysis\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187539)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.51562 6.35936C1.51562 3.68424 3.68423 1.51562 6.35935 1.51562C9.03447 1.51562 11.2031 3.68424 11.2031 6.35936C11.2031 9.03448 9.03447 11.2031 6.35935 11.2031C3.68423 11.2031 1.51562 9.03448 1.51562 6.35936ZM6.35935 0.015625C2.85581 0.015625 0.015625 2.85581 0.015625 6.35936C0.015625 9.8629 2.85581 12.7031 6.35935 12.7031C7.75028 12.7031 9.03665 12.2554 10.0823 11.4963L12.2774 13.6915C12.6679 14.082 13.3011 14.082 13.6916 13.6915C14.0822 13.3009 14.0822 12.6678 13.6916 12.2773L11.4965 10.0821C12.2555 9.03654 12.7031 7.75022 12.7031 6.35936C12.7031 2.85581 9.8629 0.015625 6.35935 0.015625ZM5.61383 4.04243C5.85791 4.28651 5.85791 4.68224 5.61383 4.92632L4.05577 6.48438L5.61383 8.04243C5.85791 8.28651 5.85791 8.68224 5.61383 8.92632C5.36975 9.17039 4.97402 9.17039 4.72995 8.92632L2.72995 6.92632C2.48587 6.68224 2.48587 6.28651 2.72995 6.04243L4.72995 4.04243C4.97402 3.79836 5.36975 3.79836 5.61383 4.04243ZM8.11383 4.04243C7.86975 3.79836 7.47402 3.79836 7.22995 4.04243C6.98587 4.28651 6.98587 4.68224 7.22995 4.92632L8.788 6.48438L7.22995 8.04243C6.98587 8.28651 6.98587 8.68224 7.22995 8.92632C7.47402 9.17039 7.86975 9.17039 8.11383 8.92632L10.1139 6.92632C10.358 6.68224 10.358 6.28651 10.1139 6.04243L8.11383 4.04243Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187539\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"code-monitor-1\", \"keywords\": [ \"code\", \"tags\", \"angle\", \"bracket\", \"monitor\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187560)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.45679 0.5C0.652229 0.5 0 1.15223 0 1.95679V9.54321C0 10.3478 0.65223 11 1.45679 11H5.48306L4.94786 12.4954H3.99998C3.58577 12.4954 3.24998 12.8312 3.24998 13.2454C3.24998 13.6596 3.58577 13.9954 3.99998 13.9954H9.99998C10.4142 13.9954 10.75 13.6596 10.75 13.2454C10.75 12.8312 10.4142 12.4954 9.99998 12.4954H9.05214L8.51694 11H12.5432C13.3478 11 14 10.3478 14 9.54321V1.95679C14 1.15223 13.3478 0.5 12.5432 0.5H1.45679ZM7.98717 2.78851C8.38013 2.91949 8.5925 3.34423 8.46151 3.73719L6.96151 8.23719C6.83053 8.63015 6.40579 8.84252 6.01283 8.71153C5.61987 8.58055 5.4075 8.15581 5.53849 7.76285L7.03849 3.26285C7.16947 2.86989 7.59421 2.65752 7.98717 2.78851ZM4.98809 5.06946C5.30259 4.7999 5.33901 4.32642 5.06944 4.01193C4.79988 3.69743 4.3264 3.66101 4.01191 3.93058L2.26191 5.43058C2.09429 5.57425 1.99849 5.78447 2.00002 6.00523C2.00155 6.22599 2.10027 6.43486 2.26986 6.57619L3.76986 7.82619C4.08807 8.09136 4.56099 8.04837 4.82617 7.73016C5.09134 7.41195 5.04835 6.93903 4.73014 6.67385L3.91186 5.99195L4.98809 5.06946ZM10.2301 4.17385C9.91193 3.90868 9.43901 3.95167 9.17383 4.26988C8.90866 4.58809 8.95165 5.06101 9.26986 5.32619L10.0881 6.00809L9.01191 6.93058C8.69741 7.20014 8.66099 7.67362 8.93056 7.98811C9.20012 8.30261 9.6736 8.33903 9.98809 8.06946L11.7381 6.56946C11.9057 6.42579 12.0015 6.21557 12 5.99481C11.9984 5.77405 11.8997 5.56518 11.7301 5.42385L10.2301 4.17385Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187560\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"code-monitor-2\", \"keywords\": [ \"code\", \"tags\", \"angle\", \"image\", \"ui\", \"ux\", \"design\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187566)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2 2.5V9.00001H6.39734V2.5H2ZM12 7.41268L10.4073 6.04C10.1614 5.83589 9.85192 5.72417 9.53234 5.72417C9.21277 5.72417 8.90325 5.83589 8.65734 6.04L7.77734 6.75C7.74487 6.76623 7.71691 6.79025 7.69597 6.81992C7.67504 6.84958 7.66176 6.88396 7.65734 6.92V9L12 9.00001V7.41268ZM0 1.95679C0 1.15223 0.652229 0.5 1.45679 0.5H12.5432C13.3478 0.5 14 1.15223 14 1.95679V9.54321C14 10.3478 13.3478 11 12.5432 11H8.51694L9.05214 12.4954H9.99998C10.4142 12.4954 10.75 12.8312 10.75 13.2454C10.75 13.6596 10.4142 13.9954 9.99998 13.9954H3.99998C3.58577 13.9954 3.24998 13.6596 3.24998 13.2454C3.24998 12.8312 3.58577 12.4954 3.99998 12.4954H4.94786L5.48306 11H1.45679C0.65223 11 0 10.3478 0 9.54321V1.95679ZM4.77648 3.87012H3.02648C2.8594 3.87012 2.69916 3.93649 2.58101 4.05464C2.46286 4.17279 2.39648 4.33303 2.39648 4.50012C2.39648 4.6672 2.46286 4.82745 2.58101 4.94559C2.69916 5.06374 2.8594 5.13012 3.02648 5.13012H4.77648C4.94357 5.13012 5.10381 5.06374 5.22196 4.94559C5.34011 4.82745 5.40648 4.6672 5.40648 4.50012C5.40648 4.33303 5.34011 4.17279 5.22196 4.05464C5.10381 3.93649 4.94357 3.87012 4.77648 3.87012ZM3.02648 6.37012H4.02648C4.19357 6.37012 4.35381 6.43649 4.47196 6.55464C4.59011 6.67279 4.65648 6.83303 4.65648 7.00012C4.65648 7.1672 4.59011 7.32745 4.47196 7.44559C4.35381 7.56374 4.19357 7.63012 4.02648 7.63012H3.02648C2.8594 7.63012 2.69916 7.56374 2.58101 7.44559C2.46286 7.32745 2.39648 7.1672 2.39648 7.00012C2.39648 6.83303 2.46286 6.67279 2.58101 6.55464C2.69916 6.43649 2.8594 6.37012 3.02648 6.37012Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187566\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"css-three\", \"keywords\": [ \"language\", \"three\", \"code\", \"programming\", \"html\", \"css\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187548)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.119878 0.175185C0.214873 0.064014 0.353774 0 0.500004 0H13.5C13.6462 0 13.7851 0.064014 13.8801 0.175185C13.9751 0.286356 14.0167 0.433541 13.9939 0.577981L12.4939 10.078C12.4727 10.2121 12.3979 10.3318 12.2867 10.4096L7.28674 13.9096C7.11457 14.0301 6.88543 14.0301 6.71327 13.9096L1.71327 10.4096C1.60207 10.3318 1.52729 10.2121 1.50612 10.078L0.00612213 0.577981C-0.0166842 0.433541 0.0248832 0.286356 0.119878 0.175185ZM4.20038 2.46289C3.8552 2.46289 3.57538 2.74271 3.57538 3.08789C3.57538 3.43307 3.8552 3.71289 4.20038 3.71289H7.62034L4.98897 5.3575C4.75253 5.50527 4.64258 5.79179 4.71944 6.0598C4.79631 6.32781 5.0414 6.5125 5.32022 6.5125H8.61467V8.3746L6.99999 9.58562L5.1353 8.1871C4.85916 7.98 4.46741 8.03596 4.2603 8.3121C4.05319 8.58825 4.10916 8.98 4.3853 9.1871L6.62499 10.8669C6.84721 11.0335 7.15276 11.0335 7.37499 10.8669L9.61467 9.1871C9.77205 9.06907 9.86467 8.88383 9.86467 8.6871V5.8875C9.86467 5.54232 9.58485 5.2625 9.23967 5.2625H7.49947L10.1308 3.61789C10.3673 3.47012 10.4772 3.1836 10.4004 2.91559C10.3235 2.64758 10.0784 2.46289 9.79959 2.46289H4.20038Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187548\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"curly-brackets\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.59149 0.375C4.625 0.375 3.84149 1.1585 3.84149 2.125V4.89484C3.84149 5.32075 3.62464 5.71734 3.26608 5.94719L2.46244 6.46234C2.2473 6.60025 2.11719 6.83821 2.11719 7.09375C2.11719 7.34929 2.2473 7.58725 2.46244 7.72516L3.26608 8.24031C3.62464 8.47016 3.84149 8.86676 3.84149 9.29266V12.0625C3.84149 13.029 4.625 13.8125 5.59149 13.8125H5.97737C6.39159 13.8125 6.72737 13.4767 6.72737 13.0625C6.72737 12.6483 6.39159 12.3125 5.97737 12.3125H5.59149C5.45342 12.3125 5.34149 12.2006 5.34149 12.0625V9.29266C5.34149 8.42394 4.9314 7.61064 4.24298 7.09375C4.9314 6.57686 5.34149 5.76356 5.34149 4.89484V2.125C5.34149 1.98693 5.45342 1.875 5.59149 1.875H5.97737C6.39159 1.875 6.72737 1.53921 6.72737 1.125C6.72737 0.710786 6.39159 0.375 5.97737 0.375H5.59149ZM8.75226 0.375C9.71876 0.375 10.5023 1.1585 10.5023 2.125V4.89484C10.5023 5.32075 10.7191 5.71734 11.0777 5.94719L11.8813 6.46234C12.0965 6.60025 12.2266 6.83821 12.2266 7.09375C12.2266 7.34929 12.0965 7.58725 11.8813 7.72516L11.0777 8.24031C10.7191 8.47016 10.5023 8.86676 10.5023 9.29266V12.0625C10.5023 13.029 9.71876 13.8125 8.75226 13.8125H8.36638C7.95217 13.8125 7.61638 13.4767 7.61638 13.0625C7.61638 12.6483 7.95217 12.3125 8.36638 12.3125H8.75226C8.89033 12.3125 9.00226 12.2006 9.00226 12.0625V9.29266C9.00226 8.42394 9.41235 7.61064 10.1008 7.09375C9.41235 6.57686 9.00226 5.76356 9.00226 4.89484V2.125C9.00226 1.98693 8.89033 1.875 8.75226 1.875H8.36638C7.95217 1.875 7.61638 1.53921 7.61638 1.125C7.61638 0.710786 7.95217 0.375 8.36638 0.375H8.75226Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"file-code-1\", \"keywords\": [ \"code\", \"files\", \"angle\", \"programming\", \"file\", \"bracket\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.43934 0.43934C1.72064 0.158035 2.10218 0 2.5 0H8.5C8.63261 0 8.75979 0.0526784 8.85355 0.146447L12.8536 4.14645C12.9473 4.24021 13 4.36739 13 4.5V12.5C13 12.8978 12.842 13.2794 12.5607 13.5607C12.2794 13.842 11.8978 14 11.5 14H2.5C2.10217 14 1.72064 13.842 1.43934 13.5607C1.15804 13.2794 1 12.8978 1 12.5V1.5C1 1.10218 1.15804 0.720644 1.43934 0.43934ZM6.03033 5.96967C6.32322 6.26256 6.32322 6.73744 6.03033 7.03033L4.56066 8.5L6.03033 9.96967C6.32322 10.2626 6.32322 10.7374 6.03033 11.0303C5.73744 11.3232 5.26256 11.3232 4.96967 11.0303L2.96967 9.03033C2.67678 8.73744 2.67678 8.26256 2.96967 7.96967L4.96967 5.96967C5.26256 5.67678 5.73744 5.67678 6.03033 5.96967ZM7.96967 7.03033C7.67678 6.73744 7.67678 6.26256 7.96967 5.96967C8.26256 5.67678 8.73744 5.67678 9.03033 5.96967L11.0303 7.96967C11.3232 8.26256 11.3232 8.73744 11.0303 9.03033L9.03033 11.0303C8.73744 11.3232 8.26256 11.3232 7.96967 11.0303C7.67678 10.7374 7.67678 10.2626 7.96967 9.96967L9.43934 8.5L7.96967 7.03033Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"incognito-mode\", \"keywords\": [ \"internet\", \"safe\", \"mode\", \"browser\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187575)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.54412 1.21767C2.66125 0.515006 3.2692 0 3.98156 0H10.0178C10.7301 0 11.3381 0.515036 11.4552 1.21773L12.1678 5.49368H13.2219C13.6361 5.49368 13.9719 5.82947 13.9719 6.24368C13.9719 6.65789 13.6361 6.99368 13.2219 6.99368H0.777344C0.36313 6.99368 0.0273438 6.65789 0.0273438 6.24368C0.0273438 5.82947 0.36313 5.49368 0.777344 5.49368H1.83137L2.54412 1.21767ZM8.74963 10.8995C8.74963 10.0708 9.42142 9.39898 10.2501 9.39898C11.0788 9.39898 11.7506 10.0708 11.7506 10.8995C11.7506 11.7282 11.0788 12.4 10.2501 12.4C9.42142 12.4 8.74963 11.7282 8.74963 10.8995ZM10.2501 7.89898C9.1871 7.89898 8.25319 8.45178 7.72019 9.28558C7.50422 9.19559 7.2621 9.14889 6.99819 9.14889C6.73493 9.14889 6.49335 9.19536 6.27779 9.28492C5.7447 8.45156 4.81106 7.89912 3.74838 7.89912C2.09127 7.89912 0.747925 9.24247 0.747925 10.8996C0.747925 12.5567 2.09127 13.9 3.74838 13.9C5.40549 13.9 6.74884 12.5567 6.74884 10.8996V10.8975C6.74969 10.8894 6.75103 10.8789 6.75311 10.8664C6.76055 10.8217 6.77405 10.7736 6.79357 10.7346C6.81162 10.6985 6.82692 10.6852 6.83581 10.6793C6.84098 10.6759 6.88107 10.6489 6.99819 10.6489C7.11532 10.6489 7.15519 10.6757 7.16036 10.6791C7.16926 10.685 7.18476 10.6985 7.20282 10.7346C7.22233 10.7736 7.23583 10.8217 7.24328 10.8664C7.24671 10.887 7.24813 10.9025 7.24866 10.9099L7.24881 10.9122C7.24899 10.9225 7.24936 10.9326 7.24994 10.9427C7.27308 12.5799 8.60744 13.9 10.2501 13.9C11.9072 13.9 13.2506 12.5566 13.2506 10.8995C13.2506 9.24235 11.9072 7.89898 10.2501 7.89898ZM3.74838 9.39912C4.56449 9.39912 5.22842 10.0507 5.24838 10.862L5.24834 10.8632L5.2478 10.8845L5.2477 10.8928L5.24768 10.8963V10.898L5.24884 10.8988L5.24768 10.8995C5.24768 10.911 5.24794 10.9225 5.24846 10.934C5.23018 11.7468 4.56557 12.4 3.74838 12.4C2.9197 12.4 2.24792 11.7283 2.24792 10.8996C2.24792 10.0709 2.9197 9.39912 3.74838 9.39912Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187575\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"insert-cloud-video\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.15786 1.81787C6.25751 1.81787 5.38445 2.12702 4.68473 2.69361C4.09293 3.17281 3.65546 3.81158 3.42131 4.53002C3.39777 4.531 3.37423 4.5322 3.3507 4.53362C2.92978 4.51219 2.50869 4.57491 2.1121 4.71823C1.70524 4.86527 1.33277 5.09404 1.01765 5.39045C0.702514 5.68685 0.451392 6.04463 0.279738 6.44174C0.108084 6.83885 0.0195312 7.26689 0.0195312 7.69952C0.0195312 8.13214 0.108084 8.56018 0.279738 8.95729C0.451392 9.35441 0.702514 9.71218 1.01765 10.0086C1.33277 10.305 1.70524 10.5338 2.1121 10.6808C2.51397 10.826 2.94099 10.8885 3.36751 10.8645H3.92055V8.16979C3.92055 6.79527 5.43247 5.95731 6.59805 6.68579L10.1176 8.8855C10.8263 9.32845 11.077 10.1522 10.8697 10.8645H10.9728C10.983 10.8645 10.9932 10.8642 11.0034 10.8636C11.8091 10.8142 12.5656 10.4594 13.1186 9.87141C13.6716 9.28345 13.9795 8.50668 13.9795 7.69952C13.9795 6.89235 13.6716 6.11558 13.1186 5.52763C12.5659 4.94005 11.81 4.58527 11.0049 4.53555C10.9681 4.53308 10.9312 4.53113 10.8943 4.5297C10.6601 3.81139 10.2227 3.17274 9.63099 2.69361C8.93127 2.12702 8.05821 1.81787 7.15786 1.81787ZM9.67996 10.4704C9.68168 10.462 9.68318 10.4535 9.68446 10.4449C9.71244 10.2585 9.63598 10.0586 9.45508 9.94553L5.93555 7.74582C5.60253 7.53768 5.17055 7.77711 5.17055 8.16982V12.5692C5.17055 12.962 5.60253 13.2014 5.93555 12.9932L9.45508 10.7935C9.57924 10.7159 9.6542 10.5974 9.67996 10.4704Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"markdown-circle-programming\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187605)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M14 7C14 10.866 10.866 14 7 14C3.13401 14 0 10.866 0 7C0 3.13401 3.13401 0 7 0C10.866 0 14 3.13401 14 7ZM3.74292 4.375C4.08448 4.375 4.39427 4.57533 4.53442 4.8868L4.99991 5.9214L5.46558 4.88672C5.60574 4.57529 5.91552 4.375 6.25704 4.375C6.73639 4.375 7.12498 4.76359 7.12498 5.24293V9C7.12498 9.34518 6.84515 9.625 6.49998 9.625C6.1548 9.625 5.87498 9.34518 5.87498 9V7.0228L5.85187 7.07414C5.70098 7.40939 5.36751 7.625 4.99987 7.625C4.63218 7.625 4.29868 7.40935 4.14782 7.07404L4.125 7.02332V9C4.125 9.34518 3.84518 9.625 3.5 9.625C3.15482 9.625 2.875 9.34518 2.875 9V5.24292C2.875 4.76358 3.26358 4.375 3.74292 4.375ZM10.125 5C10.125 4.65482 9.84518 4.375 9.5 4.375C9.15482 4.375 8.875 4.65482 8.875 5V7.5H8.5C8.29777 7.5 8.11545 7.62182 8.03806 7.80866C7.96067 7.9955 8.00345 8.21055 8.14645 8.35355L9.14645 9.35355C9.24022 9.44732 9.36739 9.5 9.5 9.5C9.63261 9.5 9.75978 9.44732 9.85355 9.35355L10.8536 8.35355C10.9966 8.21055 11.0393 7.9955 10.9619 7.80866C10.8845 7.62182 10.7022 7.5 10.5 7.5H10.125V5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187605\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"markdown-document-programming\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.5 0C2.10218 0 1.72064 0.158035 1.43934 0.43934C1.15804 0.720644 1 1.10218 1 1.5V12.5C1 12.8978 1.15804 13.2794 1.43934 13.5607C1.72064 13.842 2.10217 14 2.5 14H11.5C11.8978 14 12.2794 13.842 12.5607 13.5607C12.842 13.2794 13 12.8978 13 12.5V5.5C13 5.36739 12.9473 5.24021 12.8536 5.14645L7.85355 0.146447C7.75979 0.0526784 7.63261 0 7.5 0H2.5ZM3.74292 5.875C4.08448 5.875 4.39427 6.07533 4.53442 6.3868L4.99991 7.4214L5.46558 6.38672C5.60574 6.07529 5.91552 5.875 6.25704 5.875C6.73639 5.875 7.12498 6.26359 7.12498 6.74293V10.5C7.12498 10.8452 6.84515 11.125 6.49998 11.125C6.1548 11.125 5.87498 10.8452 5.87498 10.5V8.5228L5.85187 8.57414C5.70098 8.90939 5.36751 9.125 4.99987 9.125C4.63218 9.125 4.29868 8.90935 4.14782 8.57404L4.125 8.52332V10.5C4.125 10.8452 3.84518 11.125 3.5 11.125C3.15482 11.125 2.875 10.8452 2.875 10.5V6.74292C2.875 6.26358 3.26358 5.875 3.74292 5.875ZM10.125 6.5C10.125 6.15482 9.84518 5.875 9.5 5.875C9.15482 5.875 8.875 6.15482 8.875 6.5V8.99988H8.5C8.29777 8.99988 8.11545 9.1217 8.03806 9.30854C7.96067 9.49537 8.00345 9.71043 8.14645 9.85343L9.14645 10.8534C9.24022 10.9472 9.36739 10.9999 9.5 10.9999C9.63261 10.9999 9.75978 10.9472 9.85355 10.8534L10.8536 9.85343C10.9966 9.71043 11.0393 9.49537 10.9619 9.30854C10.8845 9.1217 10.7022 8.99988 10.5 8.99988H10.125V6.5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"module-puzzle-1\", \"keywords\": [ \"code\", \"puzzle\", \"module\", \"programming\", \"plugin\", \"piece\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187596)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.01916 0.25C6.85824 0.25 6.69914 0.284013 6.55228 0.349809C6.41063 0.413276 6.28345 0.504933 6.17849 0.619125L5.04523 1.74619C4.92513 1.49978 4.76296 1.2735 4.5637 1.0784C4.10172 0.617519 3.47577 0.358645 2.82313 0.358645C2.16951 0.358645 1.54265 0.618295 1.08047 1.08047C0.618295 1.54265 0.358645 2.16951 0.358645 2.82313C0.358645 3.47577 0.617519 4.10172 1.0784 4.5637C1.2711 4.76051 1.49421 4.92113 1.7371 5.04078L0.621448 6.13804C0.506175 6.24341 0.413705 6.37135 0.349809 6.51396C0.284013 6.66082 0.25 6.81992 0.25 6.98084C0.25 7.14176 0.284013 7.30086 0.349809 7.44772C0.413381 7.58961 0.505238 7.71697 0.619696 7.82203L2.08637 9.28871C2.20078 9.40312 2.36347 9.45509 2.52301 9.42821C2.68256 9.40132 2.81923 9.2989 2.88982 9.15332C2.96374 9.00088 3.06156 8.86126 3.17958 8.73973C3.45566 8.46266 3.83033 8.30629 4.22153 8.30495C4.61353 8.30361 4.99001 8.45804 5.26814 8.73428C5.54627 9.01051 5.70328 9.38593 5.70463 9.77792C5.70596 10.1685 5.55262 10.5438 5.2782 10.8216C5.15696 10.9391 5.01776 11.0365 4.86584 11.1102C4.72184 11.18 4.61996 11.3145 4.5918 11.4721C4.56364 11.6296 4.6126 11.7911 4.7235 11.9065L6.13582 13.3761C6.24161 13.4925 6.37035 13.5858 6.51396 13.6502C6.66082 13.716 6.81992 13.75 6.98084 13.75C7.14176 13.75 7.30086 13.716 7.44772 13.6502C7.58937 13.5867 7.71653 13.4951 7.8215 13.3809L8.95759 12.251C9.0226 12.3847 9.09985 12.5127 9.18872 12.6335C9.47002 13.0158 9.85576 13.3088 10.2996 13.4771C10.7434 13.6454 11.2263 13.6819 11.6904 13.5823C12.1545 13.4827 12.5799 13.2512 12.9155 12.9155C13.2512 12.5799 13.4827 12.1545 13.5823 11.6904C13.6819 11.2263 13.6454 10.7434 13.4771 10.2996C13.3088 9.85576 13.0158 9.47002 12.6335 9.18872C12.5155 9.10186 12.3904 9.0261 12.2601 8.96202L13.3786 7.86196C13.4938 7.75659 13.5863 7.62865 13.6502 7.48604C13.716 7.33918 13.75 7.18008 13.75 7.01916C13.75 6.85824 13.716 6.69914 13.6502 6.55228C13.5864 6.40983 13.494 6.28203 13.3789 6.17672L11.8922 4.70902C11.7774 4.59575 11.6151 4.54476 11.4562 4.57206C11.2973 4.59937 11.1614 4.70162 11.091 4.84668C11.019 4.99528 10.9242 5.13171 10.8101 5.25106C10.5314 5.49936 10.1686 5.63274 9.79506 5.62388C9.41648 5.61489 9.05589 5.46049 8.78812 5.19272C8.52035 4.92495 8.36595 4.56436 8.35696 4.18577C8.3481 3.81222 8.48148 3.44945 8.72978 3.17072C8.84913 3.05666 8.98556 2.96188 9.13416 2.88982C9.27816 2.81999 9.38003 2.68545 9.4082 2.52791C9.43636 2.37036 9.3874 2.20886 9.2765 2.09347L7.86419 0.623895C7.75839 0.507483 7.62966 0.414157 7.48604 0.349809C7.33918 0.284013 7.18008 0.25 7.01916 0.25Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187596\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"module-puzzle-3\", \"keywords\": [ \"code\", \"puzzle\", \"module\", \"programming\", \"plugin\", \"piece\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187587)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0H6.375V1.15807C6.25322 1.13634 6.12789 1.125 6 1.125C4.8264 1.125 3.875 2.07639 3.875 3.25C3.875 4.4236 4.8264 5.375 6 5.375C6.12789 5.375 6.25322 5.36366 6.375 5.34193V6.375H4.36805C4.12155 6.375 3.89807 6.51989 3.79747 6.74494C3.69687 6.96999 3.73795 7.23314 3.90234 7.41682C4.04127 7.57204 4.125 7.7755 4.125 8C4.125 8.48325 3.73325 8.875 3.25 8.875C2.76675 8.875 2.375 8.48325 2.375 8C2.375 7.7755 2.45873 7.57204 2.59766 7.41682C2.76206 7.23314 2.80313 6.96999 2.70253 6.74494C2.60193 6.51989 2.37845 6.375 2.13195 6.375H0V1.5C0 0.671573 0.671573 0 1.5 0ZM0 7.625V12.5C0 13.3284 0.671573 14 1.5 14H6.375V11.8681C6.375 11.6215 6.51989 11.3981 6.74494 11.2975C6.96999 11.1969 7.23314 11.2379 7.41682 11.4023C7.57204 11.5413 7.7755 11.625 8 11.625C8.48325 11.625 8.875 11.2332 8.875 10.75C8.875 10.2668 8.48325 9.875 8 9.875C7.7755 9.875 7.57204 9.95873 7.41682 10.0977C7.23314 10.2621 6.96999 10.3031 6.74494 10.2025C6.51989 10.1019 6.375 9.87845 6.375 9.63195V7.625H5.34193C5.36366 7.74678 5.375 7.87211 5.375 8C5.375 9.1736 4.42361 10.125 3.25 10.125C2.07639 10.125 1.125 9.1736 1.125 8C1.125 7.87211 1.13634 7.74678 1.15807 7.625H0ZM7.625 7.625V8.65807C7.74678 8.63634 7.87211 8.625 8 8.625C9.1736 8.625 10.125 9.5764 10.125 10.75C10.125 11.9236 9.1736 12.875 8 12.875C7.87211 12.875 7.74678 12.8637 7.625 12.8419V14H12.5C13.3284 14 14 13.3284 14 12.5V7.625H11.8681C11.6215 7.625 11.3981 7.48011 11.2975 7.25506C11.1969 7.03001 11.2379 6.76686 11.4023 6.58318C11.5413 6.42796 11.625 6.2245 11.625 6C11.625 5.51675 11.2332 5.125 10.75 5.125C10.2668 5.125 9.875 5.51675 9.875 6C9.875 6.2245 9.95873 6.42796 10.0977 6.58318C10.2621 6.76686 10.3031 7.03001 10.2025 7.25506C10.1019 7.48011 9.87845 7.625 9.63195 7.625H7.625ZM14 6.375V1.5C14 0.671573 13.3284 0 12.5 0H7.625V2.13195C7.625 2.37845 7.48011 2.60193 7.25506 2.70253C7.03001 2.80313 6.76686 2.76206 6.58318 2.59766C6.42796 2.45873 6.2245 2.375 6 2.375C5.51675 2.375 5.125 2.76675 5.125 3.25C5.125 3.73325 5.51675 4.125 6 4.125C6.2245 4.125 6.42796 4.04127 6.58318 3.90234C6.76686 3.73795 7.03001 3.69687 7.25506 3.79747C7.48011 3.89807 7.625 4.12155 7.625 4.36805V6.375H8.65807C8.63634 6.25322 8.625 6.12789 8.625 6C8.625 4.8264 9.5764 3.875 10.75 3.875C11.9236 3.875 12.875 4.82639 12.875 6C12.875 6.12789 12.8637 6.25322 12.8419 6.375H14Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187587\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"module-three\", \"keywords\": [ \"code\", \"three\", \"module\", \"programming\", \"plugin\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187581)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4.88 0C4.32772 0 3.88 0.447715 3.88 1V5.25C3.88 5.80228 4.32772 6.25 4.88 6.25H9.13C9.68229 6.25 10.13 5.80228 10.13 5.25V1C10.13 0.447715 9.68229 0 9.13 0H4.88ZM1 7.75C0.447715 7.75 0 8.19771 0 8.75V13C0 13.5523 0.447715 14 1 14H5.25C5.80228 14 6.25 13.5523 6.25 13V8.75C6.25 8.19771 5.80228 7.75 5.25 7.75H1ZM7.75 8.75C7.75 8.19771 8.19771 7.75 8.75 7.75H13C13.5523 7.75 14 8.19771 14 8.75V13C14 13.5523 13.5523 14 13 14H8.75C8.19771 14 7.75 13.5523 7.75 13V8.75Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187581\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"rss-square\", \"keywords\": [ \"wireless\", \"rss\", \"feed\", \"square\", \"transmit\", \"broadcast\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_187578)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.5 0C0.671573 0 0 0.671573 0 1.5V12.5C0 13.3284 0.671573 14 1.5 14H12.5C13.3284 14 14 13.3284 14 12.5V1.5C14 0.671573 13.3284 0 12.5 0H1.5ZM2.95728 9.9591C2.95728 9.36064 3.44242 8.8755 4.04087 8.8755C4.63933 8.8755 5.12447 9.36064 5.12447 9.9591C5.12447 10.5576 4.63933 11.0427 4.04087 11.0427C3.44242 11.0427 2.95728 10.5576 2.95728 9.9591ZM3.41587 6.45751C3.41587 6.11233 3.6957 5.83251 4.04087 5.83251C5.13531 5.83251 6.18493 6.26727 6.95881 7.04116C7.7327 7.81504 8.16746 8.86466 8.16746 9.9591C8.16746 10.3043 7.88764 10.5841 7.54246 10.5841C7.19729 10.5841 6.91746 10.3043 6.91746 9.9591C6.91746 9.19618 6.6144 8.4645 6.07493 7.92504C5.53546 7.38557 4.80379 7.08251 4.04087 7.08251C3.6957 7.08251 3.41587 6.80268 3.41587 6.45751ZM4.04087 2.83228C3.6957 2.83228 3.41587 3.11211 3.41587 3.45728C3.41587 3.80246 3.6957 4.08228 4.04087 4.08228C5.5995 4.08228 7.09429 4.70145 8.19641 5.80356C9.29852 6.90568 9.91768 8.40047 9.91768 9.9591C9.91768 10.3043 10.1975 10.5841 10.5427 10.5841C10.8879 10.5841 11.1677 10.3043 11.1677 9.9591C11.1677 8.06895 10.4168 6.25622 9.08029 4.91968C7.74375 3.58314 5.93102 2.83228 4.04087 2.83228Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_187578\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" } ], \"shipping\": [ { \"name\": \"box-sign\", \"keywords\": [ \"box\", \"package\", \"label\", \"delivery\", \"shipment\", \"shipping\", \"this\", \"way\", \"up\", \"arrow\", \"sign\", \"sticker\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189668)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 1.5C0 0.671573 0.671573 0 1.5 0H12.5C13.3284 0 14 0.671573 14 1.5V12.5C14 13.3284 13.3284 14 12.5 14H1.5C0.671573 14 0 13.3284 0 12.5V1.5ZM3.5 9.875C3.15482 9.875 2.875 10.1548 2.875 10.5C2.875 10.8452 3.15482 11.125 3.5 11.125H10.5C10.8452 11.125 11.125 10.8452 11.125 10.5C11.125 10.1548 10.8452 9.875 10.5 9.875H3.5ZM5.75958 2.93145C5.98218 3.03309 6.125 3.25529 6.125 3.5V7.83429C6.125 8.17946 5.84518 8.45929 5.5 8.45929C5.15482 8.45929 4.875 8.17946 4.875 7.83429V4.86892L3.90938 5.70598C3.64856 5.93207 3.25383 5.90392 3.02774 5.6431C2.80164 5.38228 2.82979 4.98755 3.09062 4.76145L5.09062 3.02774C5.27553 2.86745 5.53697 2.82982 5.75958 2.93145ZM8.90938 3.02774C8.72447 2.86745 8.46303 2.82982 8.24042 2.93145C8.01782 3.03309 7.875 3.25529 7.875 3.5V7.83429C7.875 8.17946 8.15482 8.45929 8.5 8.45929C8.84518 8.45929 9.125 8.17946 9.125 7.83429V4.86892L10.0906 5.70598C10.3514 5.93207 10.7462 5.90392 10.9723 5.6431C11.1984 5.38228 11.1702 4.98755 10.9094 4.76145L8.90938 3.02774Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189668\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"container\", \"keywords\": [ \"box\", \"package\", \"label\", \"delivery\", \"shipment\", \"shipping\", \"container\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.5 2C0.223858 2 0 2.22386 0 2.5C0 2.77614 0.223858 3 0.5 3H0.525391H0.550781H0.576172H0.601562H0.626953H0.652344H0.677734H0.703125H0.728516H0.75V11H0.728516H0.703125H0.677734H0.652344H0.626953H0.601562H0.576172H0.550781H0.525391H0.5C0.223858 11 0 11.2239 0 11.5C0 11.7761 0.223858 12 0.5 12H0.525391H0.550781H0.576172H0.601562H0.626953H0.652344H0.677734H0.703125H0.728516H0.753906H0.779297H0.804688H0.830078H0.855469H0.880859H0.90625H0.931641H0.957031H0.982422H1.00781H1.0332H1.05859H1.08398H1.10938H1.13477H1.16016H1.18555H1.21094H1.23633H1.25H1.26172H1.28711H1.3125H1.33789H1.36328H1.38867H1.41406H1.43945H1.46484H1.49023H1.51562H1.54102H1.56641H1.5918H1.61719H1.64258H1.66797H1.69336H1.71875H1.74414H1.76953H1.79492H1.82031H1.8457H1.87109H1.89648H1.92188H1.94727H1.97266H1.99805H2.02344H2.04883H2.07422H2.09961H2.125H2.15039H2.17578H2.20117H2.22656H2.25195H2.27734H2.30273H2.32812H2.35352H2.37891H2.4043H2.42969H2.45508H2.48047H2.50586H2.53125H2.55664H2.58203H2.60742H2.63281H2.6582H2.68359H2.70898H2.73438H2.75977H2.78516H2.81055H2.83594H2.86133H2.88672H2.91211H2.9375H2.96289H2.98828H3.01367H3.03906H3.06445H3.08984H3.11523H3.14062H3.16602H3.19141H3.2168H3.24219H3.26758H3.29297H3.31836H3.34375H3.36914H3.39453H3.41992H3.44531H3.4707H3.49609H3.52148H3.54688H3.57227H3.59766H3.62305H3.64844H3.67383H3.69922H3.72461H3.75H3.77539H3.80078H3.82617H3.85156H3.87695H3.90234H3.92773H3.95313H3.97852H4.00391H4.0293H4.05469H4.08008H4.10547H4.13086H4.15625H4.18164H4.20703H4.23242H4.25781H4.2832H4.30859H4.33398H4.35938H4.38477H4.41016H4.43555H4.46094H4.48633H4.51172H4.53711H4.5625H4.58789H4.61328H4.63867H4.66406H4.68945H4.71484H4.74023H4.76562H4.79102H4.81641H4.8418H4.86719H4.89258H4.91797H4.94336H4.96875H4.99414H5.01953H5.04492H5.07031H5.0957H5.12109H5.14648H5.17188H5.19727H5.22266H5.24805H5.27344H5.29883H5.32422H5.34961H5.375H5.40039H5.42578H5.45117H5.47656H5.50195H5.52734H5.55273H5.57812H5.60352H5.62891H5.6543H5.67969H5.70508H5.73047H5.75586H5.78125H5.80664H5.83203H5.85742H5.88281H5.9082H5.93359H5.95898H5.98438H6.00977H6.03516H6.06055H6.08594H6.11133H6.13672H6.16211H6.1875H6.21289H6.23828H6.26367H6.28906H6.31445H6.33984H6.36523H6.39062H6.41602H6.44141H6.4668H6.49219H6.51758H6.54297H6.56836H6.59375H6.61914H6.64453H6.66992H6.69531H6.7207H6.74609H6.77148H6.79688H6.82227H6.84766H6.87305H6.89844H6.92383H6.94922H6.97461H7H7.02539H7.05078H7.07617H7.10156H7.12695H7.15234H7.17773H7.20313H7.22852H7.25391H7.2793H7.30469H7.33008H7.35547H7.38086H7.40625H7.43164H7.45703H7.48242H7.50781H7.5332H7.55859H7.58398H7.60938H7.63477H7.66016H7.68555H7.71094H7.73633H7.76172H7.78711H7.8125H7.83789H7.86328H7.88867H7.91406H7.93945H7.96484H7.99023H8.01562H8.04102H8.06641H8.0918H8.11719H8.14258H8.16797H8.19336H8.21875H8.24414H8.26953H8.29492H8.32031H8.3457H8.37109H8.39648H8.42188H8.44727H8.47266H8.49805H8.52344H8.54883H8.57422H8.59961H8.625H8.65039H8.67578H8.70117H8.72656H8.75195H8.77734H8.80273H8.82812H8.85352H8.87891H8.9043H8.92969H8.95508H8.98047H9.00586H9.03125H9.05664H9.08203H9.10742H9.13281H9.1582H9.18359H9.20898H9.23438H9.25977H9.28516H9.31055H9.33594H9.36133H9.38672H9.41211H9.4375H9.46289H9.48828H9.51367H9.53906H9.56445H9.58984H9.61523H9.64062H9.66602H9.69141H9.7168H9.74219H9.76758H9.79297H9.81836H9.84375H9.86914H9.89453H9.91992H9.94531H9.9707H9.99609H10.0215H10.0469H10.0723H10.0977H10.123H10.1484H10.1738H10.1992H10.2246H10.25H10.2754H10.3008H10.3262H10.3516H10.377H10.4023H10.4277H10.4531H10.4785H10.5039H10.5293H10.5547H10.5801H10.6055H10.6309H10.6562H10.6816H10.707H10.7324H10.7578H10.7832H10.8086H10.834H10.8594H10.8848H10.9102H10.9355H10.9609H10.9863H11.0117H11.0371H11.0625H11.0879H11.1133H11.1387H11.1641H11.1895H11.2148H11.2402H11.2656H11.291H11.3164H11.3418H11.3672H11.3926H11.418H11.4434H11.4688H11.4941H11.5195H11.5449H11.5703H11.5957H11.6211H11.6465H11.6719H11.6973H11.7227H11.748H11.7734H11.7988H11.8242H11.8496H11.875H11.9004H11.9258H11.9512H11.9766H12.002H12.0273H12.0527H12.0781H12.1035H12.1289H12.1543H12.1797H12.2051H12.2305H12.2559H12.2812H12.3066H12.332H12.3574H12.3828H12.4082H12.4336H12.459H12.4844H12.5098H12.5352H12.5605H12.5859H12.6113H12.6367H12.6621H12.6875H12.7129H12.7383H12.75H12.7637H12.7891H12.8145H12.8398H12.8652H12.8906H12.916H12.9414H12.9668H12.9922H13.0176H13.043H13.0684H13.0938H13.1191H13.1445H13.1699H13.1953H13.2207H13.2461H13.2715H13.2969H13.3223H13.3477H13.373H13.3984H13.4238H13.4492H13.4746H13.5C13.7761 12 14 11.7761 14 11.5C14 11.2239 13.7761 11 13.5 11H13.4746H13.4492H13.4238H13.3984H13.373H13.3477H13.3223H13.2969H13.2715H13.25V3H13.2715H13.2969H13.3223H13.3477H13.373H13.3984H13.4238H13.4492H13.4746H13.5C13.7761 3 14 2.77614 14 2.5C14 2.22386 13.7761 2 13.5 2H13.4746H13.4492H13.4238H13.3984H13.373H13.3477H13.3223H13.2969H13.2715H13.2461H13.2207H13.1953H13.1699H13.1445H13.1191H13.0938H13.0684H13.043H13.0176H12.9922H12.9668H12.9414H12.916H12.8906H12.8652H12.8398H12.8145H12.7891H12.7637H12.75H12.7383H12.7275H12.7129H12.7051H12.6875H12.6826H12.6621H12.6602H12.6377H12.6367H12.6152H12.6113H12.5928H12.5859H12.5703H12.5605H12.5479H12.5352H12.5254H12.5098H12.5029H12.4844H12.4805H12.459H12.4355H12.4336H12.4131H12.4082H12.3906H12.3828H12.3682H12.3574H12.3457H12.332H12.3232H12.3066H12.3008H12.2812H12.2783H12.2559H12.2334H12.2305H12.2109H12.2051H12.1885H12.1797H12.166H12.1543H12.1436H12.1289H12.1211H12.1035H12.0986H12.0781H12.0762H12.0537H12.0527H12.0312H12.0273H12.0088H12.002H11.9863H11.9766H11.9639H11.9512H11.9414H11.9258H11.9189H11.9004H11.8965H11.875H11.874H11.8516H11.8496H11.8291H11.8242H11.8066H11.7988H11.7842H11.7734H11.7617H11.748H11.7393H11.7227H11.7168H11.6973H11.6943H11.6719H11.6494H11.6465H11.627H11.6211H11.6045H11.5957H11.582H11.5703H11.5596H11.5449H11.5371H11.5195H11.5146H11.4941H11.4922H11.4697H11.4473H11.4434H11.4248H11.418H11.4023H11.3926H11.3799H11.3672H11.3574H11.3418H11.335H11.3164H11.3125H11.291H11.29H11.2676H11.2656H11.2451H11.2402H11.2227H11.2148H11.2002H11.1895H11.1777H11.1641H11.1553H11.1387H11.1328H11.1133H11.1104H11.0879H11.0654H11.0625H11.043H11.0371H11.0205H11.0117H10.998H10.9863H10.9756H10.9609H10.9531H10.9355H10.9307H10.9102H10.9082H10.8857H10.8633H10.8594H10.8408H10.834H10.8184H10.8086H10.7959H10.7832H10.7734H10.7578H10.751H10.7324H10.7285H10.707H10.6836H10.6816H10.6611H10.6562H10.6387H10.6309H10.6162H10.6055H10.5938H10.5801H10.5713H10.5547H10.5488H10.5293H10.5264H10.5039H10.4814H10.4785H10.459H10.4531H10.4365H10.4277H10.4141H10.4023H10.3916H10.377H10.3691H10.3516H10.3467H10.3262H10.3242H10.3018H10.2793H10.2754H10.2568H10.25H10.2344H10.2246H10.2119H10.1992H10.1895H10.1738H10.167H10.1484H10.1445H10.123H10.0996H10.0977H10.0771H10.0723H10.0547H10.0469H10.0322H10.0215H10.0098H9.99609H9.9873H9.9707H9.96484H9.94531H9.94238H9.91992H9.89746H9.89453H9.875H9.86914H9.85254H9.84375H9.83008H9.81836H9.80762H9.79297H9.78516H9.76758H9.7627H9.74219H9.74023H9.71777H9.69531H9.69141H9.67285H9.66602H9.65039H9.64062H9.62793H9.61523H9.60547H9.58984H9.58301H9.56445H9.56055H9.53906H9.51562H9.51367H9.49316H9.48828H9.4707H9.46289H9.44824H9.4375H9.42578H9.41211H9.40332H9.38672H9.38086H9.36133H9.3584H9.33594H9.31348H9.31055H9.29102H9.28516H9.26855H9.25977H9.24609H9.23438H9.22363H9.20898H9.20117H9.18359H9.17871H9.1582H9.15625H9.13379H9.11133H9.10742H9.08887H9.08203H9.06641H9.05664H9.04395H9.03125H9.02148H9.00586H8.99902H8.98047H8.97656H8.95508H8.93164H8.92969H8.90918H8.9043H8.88672H8.87891H8.86426H8.85352H8.8418H8.82812H8.81934H8.80273H8.79688H8.77734H8.77441H8.75195H8.72949H8.72656H8.70703H8.70117H8.68457H8.67578H8.66211H8.65039H8.63965H8.625H8.61719H8.59961H8.59473H8.57422H8.57227H8.5498H8.52734H8.52344H8.50488H8.49805H8.48242H8.47266H8.45996H8.44727H8.4375H8.42188H8.41504H8.39648H8.39258H8.37109H8.34766H8.3457H8.3252H8.32031H8.30273H8.29492H8.28027H8.26953H8.25781H8.24414H8.23535H8.21875H8.21289H8.19336H8.19043H8.16797H8.14551H8.14258H8.12305H8.11719H8.10059H8.0918H8.07812H8.06641H8.05566H8.04102H8.0332H8.01562H8.01074H7.99023H7.98828H7.96582H7.94336H7.93945H7.9209H7.91406H7.89844H7.88867H7.87598H7.86328H7.85352H7.83789H7.83105H7.8125H7.80859H7.78711H7.76367H7.76172H7.74121H7.73633H7.71875H7.71094H7.69629H7.68555H7.67383H7.66016H7.65137H7.63477H7.62891H7.60938H7.60645H7.58398H7.56152H7.55859H7.53906H7.5332H7.5166H7.50781H7.49414H7.48242H7.47168H7.45703H7.44922H7.43164H7.42676H7.40625H7.4043H7.38184H7.35938H7.35547H7.33691H7.33008H7.31445H7.30469H7.29199H7.2793H7.26953H7.25391H7.24707H7.22852H7.22461H7.20313H7.17969H7.17773H7.15723H7.15234H7.13477H7.12695H7.1123H7.10156H7.08984H7.07617H7.06738H7.05078H7.04492H7.02539H7.02246H7H6.97754H6.97461H6.95508H6.94922H6.93262H6.92383H6.91016H6.89844H6.8877H6.87305H6.86523H6.84766H6.84277H6.82227H6.82031H6.79785H6.77539H6.77148H6.75293H6.74609H6.73047H6.7207H6.70801H6.69531H6.68555H6.66992H6.66309H6.64453H6.64062H6.61914H6.5957H6.59375H6.57324H6.56836H6.55078H6.54297H6.52832H6.51758H6.50586H6.49219H6.4834H6.4668H6.46094H6.44141H6.43848H6.41602H6.39355H6.39062H6.37109H6.36523H6.34863H6.33984H6.32617H6.31445H6.30371H6.28906H6.28125H6.26367H6.25879H6.23828H6.23633H6.21387H6.19141H6.1875H6.16895H6.16211H6.14648H6.13672H6.12402H6.11133H6.10156H6.08594H6.0791H6.06055H6.05664H6.03516H6.01172H6.00977H5.98926H5.98438H5.9668H5.95898H5.94434H5.93359H5.92188H5.9082H5.89941H5.88281H5.87695H5.85742H5.85449H5.83203H5.80957H5.80664H5.78711H5.78125H5.76465H5.75586H5.74219H5.73047H5.71973H5.70508H5.69727H5.67969H5.6748H5.6543H5.65234H5.62988H5.60742H5.60352H5.58496H5.57812H5.5625H5.55273H5.54004H5.52734H5.51758H5.50195H5.49512H5.47656H5.47266H5.45117H5.42773H5.42578H5.40527H5.40039H5.38281H5.375H5.36035H5.34961H5.33789H5.32422H5.31543H5.29883H5.29297H5.27344H5.27051H5.24805H5.22559H5.22266H5.20312H5.19727H5.18066H5.17188H5.1582H5.14648H5.13574H5.12109H5.11328H5.0957H5.09082H5.07031H5.06836H5.0459H5.02344H5.01953H5.00098H4.99414H4.97852H4.96875H4.95605H4.94336H4.93359H4.91797H4.91113H4.89258H4.88867H4.86719H4.84375H4.8418H4.82129H4.81641H4.79883H4.79102H4.77637H4.76562H4.75391H4.74023H4.73145H4.71484H4.70898H4.68945H4.68652H4.66406H4.6416H4.63867H4.61914H4.61328H4.59668H4.58789H4.57422H4.5625H4.55176H4.53711H4.5293H4.51172H4.50684H4.48633H4.48438H4.46191H4.43945H4.43555H4.41699H4.41016H4.39453H4.38477H4.37207H4.35938H4.34961H4.33398H4.32715H4.30859H4.30469H4.2832H4.25977H4.25781H4.2373H4.23242H4.21484H4.20703H4.19238H4.18164H4.16992H4.15625H4.14746H4.13086H4.125H4.10547H4.10254H4.08008H4.05762H4.05469H4.03516H4.0293H4.0127H4.00391H3.99023H3.97852H3.96777H3.95313H3.94531H3.92773H3.92285H3.90234H3.90039H3.87793H3.85547H3.85156H3.83301H3.82617H3.81055H3.80078H3.78809H3.77539H3.76562H3.75H3.74316H3.72461H3.7207H3.69922H3.67578H3.67383H3.65332H3.64844H3.63086H3.62305H3.6084H3.59766H3.58594H3.57227H3.56348H3.54688H3.54102H3.52148H3.51855H3.49609H3.47363H3.4707H3.45117H3.44531H3.42871H3.41992H3.40625H3.39453H3.38379H3.36914H3.36133H3.34375H3.33887H3.31836H3.31641H3.29395H3.27148H3.26758H3.24902H3.24219H3.22656H3.2168H3.2041H3.19141H3.18164H3.16602H3.15918H3.14062H3.13672H3.11523H3.0918H3.08984H3.06934H3.06445H3.04688H3.03906H3.02441H3.01367H3.00195H2.98828H2.97949H2.96289H2.95703H2.9375H2.93457H2.91211H2.88965H2.88672H2.86719H2.86133H2.84473H2.83594H2.82227H2.81055H2.7998H2.78516H2.77734H2.75977H2.75488H2.73438H2.73242H2.70996H2.6875H2.68359H2.66504H2.6582H2.64258H2.63281H2.62012H2.60742H2.59766H2.58203H2.5752H2.55664H2.55273H2.53125H2.50781H2.50586H2.48535H2.48047H2.46289H2.45508H2.44043H2.42969H2.41797H2.4043H2.39551H2.37891H2.37305H2.35352H2.35059H2.32812H2.30566H2.30273H2.2832H2.27734H2.26074H2.25195H2.23828H2.22656H2.21582H2.20117H2.19336H2.17578H2.1709H2.15039H2.14844H2.12598H2.10352H2.09961H2.08105H2.07422H2.05859H2.04883H2.03613H2.02344H2.01367H1.99805H1.99121H1.97266H1.96875H1.94727H1.92383H1.92188H1.90137H1.89648H1.87891H1.87109H1.85645H1.8457H1.83398H1.82031H1.81152H1.79492H1.78906H1.76953H1.7666H1.74414H1.72168H1.71875H1.69922H1.69336H1.67676H1.66797H1.6543H1.64258H1.63184H1.61719H1.60938H1.5918H1.58691H1.56641H1.56445H1.54199H1.51953H1.51562H1.49707H1.49023H1.47461H1.46484H1.45215H1.43945H1.42969H1.41406H1.40723H1.38867H1.38477H1.36328H1.33984H1.33789H1.31738H1.3125H1.29492H1.28711H1.27246H1.26172H1.25H1.23633H1.21094H1.18555H1.16016H1.13477H1.10938H1.08398H1.05859H1.0332H1.00781H0.982422H0.957031H0.931641H0.90625H0.880859H0.855469H0.830078H0.804688H0.779297H0.753906H0.728516H0.703125H0.677734H0.652344H0.626953H0.601562H0.576172H0.550781H0.525391H0.5ZM4.625 5C4.625 4.65482 4.34518 4.375 4 4.375C3.65482 4.375 3.375 4.65482 3.375 5V9C3.375 9.34518 3.65482 9.625 4 9.625C4.34518 9.625 4.625 9.34518 4.625 9V5ZM7 4.375C7.34518 4.375 7.625 4.65482 7.625 5V9C7.625 9.34518 7.34518 9.625 7 9.625C6.65482 9.625 6.375 9.34518 6.375 9V5C6.375 4.65482 6.65482 4.375 7 4.375ZM10.625 5C10.625 4.65482 10.3452 4.375 10 4.375C9.65482 4.375 9.375 4.65482 9.375 5V9C9.375 9.34518 9.65482 9.625 10 9.625C10.3452 9.625 10.625 9.34518 10.625 9V5Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"fragile\", \"keywords\": [ \"fragile\", \"shipping\", \"glass\", \"delivery\", \"wine\", \"crack\", \"shipment\", \"sign\", \"sticker\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.04795 2.01602e-05H3.25375C2.68517 -0.00342907 2.19128 0.436134 2.12875 1.00127L1.9375 4.5C1.9375 7.08834 3.67257 8.60093 5.99997 8.93112V11.9758H4.16513C3.61285 11.9758 3.16513 12.4235 3.16513 12.9758C3.16513 13.5281 3.61285 13.9758 4.16513 13.9758H6.99718H6.99997H7.00276H9.83481C10.3871 13.9758 10.8348 13.5281 10.8348 12.9758C10.8348 12.4235 10.3871 11.9758 9.83481 11.9758H7.99997V8.93112C10.3274 8.60094 12.0625 7.08835 12.0625 4.5L11.8712 1.00127C11.8087 0.436134 11.3148 -0.00342907 10.7462 2.01602e-05H7.53439L7.52546 0.014303L6.31771 1.89004L7.91116 3.2821C8.13539 3.47798 8.1903 3.80484 8.04241 4.06324L7.04241 5.81046C6.87095 6.11004 6.48909 6.2139 6.18951 6.04244C5.88993 5.87098 5.78607 5.48912 5.95753 5.18954L6.70323 3.88665L5.08878 2.47625C4.85527 2.27226 4.80662 1.92791 4.97448 1.66721L6.04795 2.01602e-05Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"parachute-drop\", \"keywords\": [ \"package\", \"box\", \"fulfillment\", \"cart\", \"warehouse\", \"shipping\", \"delivery\", \"drop\", \"parachute\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189674)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.47819 0.660628C4.60113 0.14381 5.81103 0 6.98519 0C8.15625 0 9.37796 0.135443 10.5079 0.652877C11.6207 1.16243 12.5888 1.94142 13.3247 2.91937C13.3318 2.92891 13.3387 2.9387 13.3452 2.94872C13.4465 3.10556 13.5003 3.2883 13.5003 3.475C13.5003 3.65367 13.451 3.82871 13.358 3.98089C13.2744 4.13324 13.1525 4.26132 13.0042 4.35234C12.8661 4.43707 12.7098 4.48683 12.5487 4.49773L10.336 7.39312C10.502 7.62991 10.5995 7.91834 10.5995 8.22953V12.5417C10.5995 13.3471 9.94667 14 9.14129 14H4.82908C4.0237 14 3.37081 13.3471 3.37081 12.5417V8.22953C3.37081 7.91833 3.46829 7.6299 3.63438 7.39311L1.42051 4.49626C1.25969 4.48022 1.105 4.42542 0.969777 4.33637C0.83292 4.24624 0.720414 4.12394 0.642021 3.98032C0.549223 3.82827 0.5 3.65345 0.5 3.475C0.5 3.2883 0.553882 3.10556 0.655179 2.94872C0.661685 2.93865 0.668552 2.92881 0.675766 2.91923C1.40858 1.94603 2.37152 1.16996 3.47819 0.660628ZM8.92337 6.77126L10.6591 4.5H7.73519V6.77126H8.92337ZM6.23519 6.77126H5.04702L3.31125 4.5H6.23519V6.77126ZM6.94052 11.927C6.94052 11.5819 7.22034 11.302 7.56552 11.302H8.56552C8.9107 11.302 9.19052 11.5819 9.19052 11.927C9.19052 12.2722 8.9107 12.552 8.56552 12.552H7.56552C7.22034 12.552 6.94052 12.2722 6.94052 11.927Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189674\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shipment-add\", \"keywords\": [ \"shipping\", \"parcel\", \"shipment\", \"add\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189692)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.375 0H3.48923C2.70622 0.0201258 1.96368 0.493815 1.61538 1.19538L0 4.30769V4.32443H6.375V0ZM0 12.9231V5.57443H14V12.9231C14 13.2087 13.8865 13.4826 13.6846 13.6846C13.4826 13.8865 13.2087 14 12.9231 14H1.07692C0.791305 14 0.517386 13.8865 0.315423 13.6846C0.113461 13.4826 0 13.2087 0 12.9231ZM14 4.30769V4.32443H7.625V0H10.5108C11.3122 0.00042526 12.0818 0.477696 12.4385 1.19538L14 4.30769ZM7 6.5C7.41421 6.5 7.75 6.83579 7.75 7.25V9H9.5C9.91421 9 10.25 9.33579 10.25 9.75C10.25 10.1642 9.91421 10.5 9.5 10.5H7.75V12.25C7.75 12.6642 7.41421 13 7 13C6.58579 13 6.25 12.6642 6.25 12.25V10.5H4.5C4.08579 10.5 3.75 10.1642 3.75 9.75C3.75 9.33579 4.08579 9 4.5 9H6.25V7.25C6.25 6.83579 6.58579 6.5 7 6.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189692\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shipment-check\", \"keywords\": [ \"shipping\", \"parcel\", \"shipment\", \"check\", \"approved\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189680)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.375 0H3.48923C2.70622 0.0201258 1.96368 0.493815 1.61538 1.19538L0 4.30769V4.32443H6.375V0ZM0 12.9231V5.57443H14V12.9231C14 13.2087 13.8865 13.4826 13.6846 13.6846C13.4826 13.8865 13.2087 14 12.9231 14H1.07692C0.791305 14 0.517386 13.8865 0.315423 13.6846C0.113461 13.4826 0 13.2087 0 12.9231ZM14 4.30769V4.32443H7.625V0H10.5108C11.3122 0.00042526 12.0818 0.477696 12.4385 1.19538L14 4.30769ZM9.43748 8.63616C9.64443 8.3599 9.58824 7.96818 9.31198 7.76123C9.03571 7.55429 8.644 7.61048 8.43705 7.88674L6.48805 10.4886L5.43725 9.70142C5.16098 9.49447 4.76927 9.55066 4.56232 9.82693C4.35537 10.1032 4.41157 10.4949 4.68783 10.7019L6.23885 11.8637C6.37151 11.9631 6.53822 12.0057 6.70231 11.9822C6.86639 11.9586 7.0144 11.8709 7.11378 11.7382L9.43748 8.63616Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189680\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shipment-download\", \"keywords\": [ \"shipping\", \"parcel\", \"shipment\", \"download\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189677)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.375 0H3.48923C2.70622 0.0201258 1.96368 0.493815 1.61538 1.19538L0 4.30769V4.32443H6.375V0ZM0 12.9231V5.57443H14V12.9231C14 13.2087 13.8865 13.4826 13.6846 13.6846C13.4826 13.8865 13.2087 14 12.9231 14H1.07692C0.791305 14 0.517386 13.8865 0.315423 13.6846C0.113461 13.4826 0 13.2087 0 12.9231ZM14 4.30769V4.32443H7.625V0H10.5108C11.3122 0.00042526 12.0818 0.477696 12.4385 1.19538L14 4.30769ZM6.37451 7.70483C6.37451 7.35966 6.65434 7.07983 6.99951 7.07983C7.34469 7.07983 7.62451 7.35966 7.62451 7.70483V9.70832H8.47303C8.68582 9.70832 8.87765 9.8365 8.95908 10.0331C9.04051 10.2297 8.9955 10.456 8.84504 10.6064L7.37178 12.0797C7.16633 12.2851 6.83322 12.2851 6.62777 12.0797L5.15451 10.6064C5.00405 10.456 4.95904 10.2297 5.04047 10.0331C5.1219 9.8365 5.31373 9.70832 5.52652 9.70832H6.37451V7.70483Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189677\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shipment-remove\", \"keywords\": [ \"shipping\", \"parcel\", \"shipment\", \"remove\", \"subtract\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189695)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.375 0H3.48923C2.70622 0.0201258 1.96368 0.493815 1.61538 1.19538L0 4.30769V4.32443H6.375V0ZM0 12.9231V5.57443H14V12.9231C14 13.2087 13.8865 13.4826 13.6846 13.6846C13.4826 13.8865 13.2087 14 12.9231 14H1.07692C0.791305 14 0.517386 13.8865 0.315423 13.6846C0.113461 13.4826 0 13.2087 0 12.9231ZM14 4.30769V4.32443H7.625V0H10.5108C11.3122 0.00042526 12.0818 0.477696 12.4385 1.19538L14 4.30769ZM4.5 9C4.08579 9 3.75 9.33579 3.75 9.75C3.75 10.1642 4.08579 10.5 4.5 10.5H9.5C9.91421 10.5 10.25 10.1642 10.25 9.75C10.25 9.33579 9.91421 9 9.5 9H4.5Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189695\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shipment-upload\", \"keywords\": [ \"shipping\", \"parcel\", \"shipment\", \"upload\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189689)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.375 0H3.48923C2.70622 0.0201258 1.96368 0.493815 1.61538 1.19538L0 4.30769V4.32443H6.375V0ZM0 12.9231V5.57443H14V12.9231C14 13.2087 13.8865 13.4826 13.6846 13.6846C13.4826 13.8865 13.2087 14 12.9231 14H1.07692C0.791305 14 0.517386 13.8865 0.315423 13.6846C0.113461 13.4826 0 13.2087 0 12.9231ZM14 4.30769V4.32443H7.625V0H10.5108C11.3122 0.00042526 12.0818 0.477696 12.4385 1.19538L14 4.30769ZM6.37452 11.8461C6.37452 12.1913 6.65434 12.4711 6.99952 12.4711C7.3447 12.4711 7.62452 12.1913 7.62452 11.8461V9.84326H8.47375C8.68664 9.84326 8.87857 9.71502 8.96004 9.51833C9.04151 9.32164 8.99648 9.09525 8.84594 8.94471L7.37197 7.47074C7.16641 7.26518 6.83314 7.26518 6.62758 7.47074L5.15361 8.94471C5.00307 9.09525 4.95804 9.32164 5.03951 9.51833C5.12098 9.71502 5.31291 9.84326 5.5258 9.84326H6.37452V11.8461Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189689\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shipping-box-1\", \"keywords\": [ \"box\", \"package\", \"label\", \"delivery\", \"shipment\", \"shipping\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189698)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M8.5 0H5.5V4.50756C5.5 4.63817 5.5554 4.76342 5.65401 4.85577C5.75262 4.94812 5.88637 5 6.02583 5H7.97417C8.11363 5 8.24738 4.94812 8.34599 4.85577C8.4446 4.76342 8.5 4.63817 8.5 4.50756V0ZM4.25 0H1.5C0.671573 0 0 0.671573 0 1.5V12.5C0 13.3284 0.671573 14 1.5 14H12.5C13.3284 14 14 13.3284 14 12.5V1.5C14 0.671573 13.3284 0 12.5 0H9.75V4.50756C9.75 4.99713 9.54158 5.44866 9.20043 5.76815C8.86205 6.08503 8.41965 6.25 7.97417 6.25H6.02583C5.58035 6.25 5.13795 6.08503 4.79957 5.76815C4.45842 5.44866 4.25 4.99714 4.25 4.50756V0ZM8.7594 11.0994C8.7594 10.7543 9.03922 10.4744 9.3844 10.4744H11.3978C11.743 10.4744 12.0228 10.7543 12.0228 11.0994C12.0228 11.4446 11.743 11.7244 11.3978 11.7244H9.3844C9.03922 11.7244 8.7594 11.4446 8.7594 11.0994Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189698\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"shipping-truck\", \"keywords\": [ \"truck\", \"shipping\", \"delivery\", \"transfer\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189704)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M10.2471 5.22583H11.5C12.163 5.22583 12.7989 5.48922 13.2678 5.95806C13.7366 6.4269 14 7.06279 14 7.72583V11.7258C14 12.0019 13.7761 12.2258 13.5 12.2258H12.0563C12.0372 13.2089 11.2342 14 10.2466 14C9.25886 14 8.45594 13.2089 8.43682 12.2258H4.99965C4.98053 13.2089 4.17761 14 3.18991 14C2.20222 14 1.3993 13.2089 1.38018 12.2258H0.5C0.223858 12.2258 0 12.0019 0 11.7258V4.72583C0 4.328 0.158036 3.94647 0.439341 3.66517C0.720645 3.38386 1.10217 3.22583 1.5 3.22583H7.5C7.89782 3.22583 8.27936 3.38386 8.56066 3.66517C8.81979 3.9243 8.97432 4.26847 8.99707 4.63208V8.41464C8.99707 8.75981 9.27689 9.03964 9.62207 9.03964C9.96725 9.03964 10.2471 8.75981 10.2471 8.41464V5.22583Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189704\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"transfer-motorcycle\", \"keywords\": [ \"motorcycle\", \"shipping\", \"delivery\", \"courier\", \"transfer\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M4 6.5H1C0.447715 6.5 0 6.05228 0 5.5V2.5C0 1.94772 0.447715 1.5 1 1.5H4C4.55228 1.5 5 1.94772 5 2.5V5.5C5 6.05228 4.55228 6.5 4 6.5ZM8 2.25C7.58579 2.25 7.25 2.58579 7.25 3C7.25 3.41421 7.58579 3.75 8 3.75H8.75V8V10.711C9.20097 10.2698 9.81819 9.99788 10.4989 9.99788C11.8802 9.99788 13 11.1176 13 12.4989C13 12.556 12.9981 12.6126 12.9943 12.6687C13.6206 12.4491 14 11.8057 14 11C14 9.07327 12.4195 7.37855 10.25 7.25697V6.29933C10.4706 6.42695 10.7268 6.5 11 6.5H12C12.2761 6.5 12.5 6.27614 12.5 6V4C12.5 3.72386 12.2761 3.5 12 3.5H11C10.7268 3.5 10.4706 3.57305 10.25 3.70067V3C10.25 2.58579 9.91421 2.25 9.5 2.25H8ZM8.01031 12.75C8.00208 12.6674 7.99787 12.5837 7.99787 12.4989C7.99787 12.044 8.11931 11.6175 8.33155 11.25H8.25C7.83579 11.25 7.5 10.9142 7.5 10.5V10C7.5 8.48122 6.26878 7.25 4.75 7.25H2.75C1.23122 7.25 0 8.48122 0 10V12C0 12.4142 0.335786 12.75 0.75 12.75H1.01031C1.00208 12.6674 0.997864 12.5837 0.997864 12.4989C0.997864 11.1176 2.11763 9.99788 3.49892 9.99788C4.88022 9.99788 5.99998 11.1176 5.99998 12.4989C5.99998 12.5837 5.99577 12.6674 5.98754 12.75H8.01031ZM1.99788 12.4989C1.99788 13.328 2.66993 14 3.49894 14C4.32795 14 5 13.328 5 12.4989C5 11.6699 4.32795 10.9979 3.49894 10.9979C2.66993 10.9979 1.99788 11.6699 1.99788 12.4989ZM8.99789 12.4989C8.99789 13.3279 9.66993 14 10.4989 14C11.328 14 12 13.3279 12 12.4989C12 11.6699 11.328 10.9979 10.4989 10.9979C9.66993 10.9979 8.99789 11.6699 8.99789 12.4989Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"transfer-van\", \"keywords\": [ \"van\", \"shipping\", \"delivery\", \"transfer\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M13.6903 7.21167L12.3522 4.47195C12.1007 3.9569 11.5776 3.63025 11.0044 3.63025H10.5664V7.21167H13.6903ZM9.3164 3.15167C9.04129 2.81733 8.62431 2.60413 8.15754 2.60413H1.70117C0.872745 2.60413 0.201172 3.2757 0.201172 4.10413V10.7297C0.201172 11.5581 0.872746 12.2297 1.70117 12.2297H2.00509C2.06988 13.2181 2.89213 13.9998 3.89693 13.9998C4.90174 13.9998 5.72398 13.2181 5.78878 12.2297H8.342C8.4068 13.2181 9.22904 13.9998 10.2338 13.9998C11.2809 13.9998 12.1298 13.1509 12.1298 12.1038C12.1298 12.0835 12.1295 12.0633 12.1288 12.0431H12.2954C13.1238 12.0431 13.7954 11.3716 13.7954 10.5431V8.46167H9.9414C9.59622 8.46167 9.3164 8.18185 9.3164 7.83667V3.15167Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"warehouse-1\", \"keywords\": [ \"delivery\", \"warehouse\", \"shipping\", \"fulfillment\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189686)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 6.77642C0 6.499 0.115244 6.23405 0.318182 6.0449L6.6746 0.120371C6.86185 -0.040124 7.13815 -0.0401238 7.3254 0.120372L13.6818 6.0449C13.8848 6.23405 14 6.499 14 6.77642V12.5C14 13.3284 13.3284 14 12.5 14H1.5C0.671573 14 0 13.3284 0 12.5V6.77642ZM6.77056 8.74977L6.74957 8.74949H5.50255V5.74944H8.50288V8.74949H7.25L7.22901 8.74977H6.77056ZM4.74626 12.7495L4.76725 12.7498H5.2257L5.24669 12.7495H6.49957V9.74949H3.49924V12.7495H4.74626ZM8.76801 12.7498L8.74702 12.7495H7.5V9.74949H10.5003V12.7495H9.24745L9.22646 12.7498H8.76801Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189686\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" } ], \"work_education\": [ { \"name\": \"book-reading\", \"keywords\": [ \"book\", \"reading\", \"learning\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189512)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.99821 0.0552416C1.18696 0.000289671 1.38524 -0.0138743 1.57988 0.0136904L1.59166 0.0153584C3.07902 0.262213 4.46918 0.8883 5.63281 1.82302V11.6552C4.17132 10.6391 2.85089 10.1691 1.21826 9.8969C0.883558 9.85615 0.574991 9.69472 0.350599 9.44259C0.123214 9.1871 -0.00164239 8.85657 1.63169e-05 8.51459V1.38568C7.91267e-06 1.18553 0.0433555 0.987722 0.127077 0.805926C0.210804 0.62412 0.332922 0.462609 0.485029 0.332506C0.634457 0.204769 0.809462 0.110194 0.99821 0.0552416ZM6.88281 5.88966V1.82263C8.04634 0.888126 9.43633 0.262178 10.9235 0.0153584L10.9353 0.0136904C11.1299 -0.0138743 11.3282 0.000289671 11.5169 0.0552416C11.7057 0.110194 11.8807 0.204769 12.0301 0.332506C12.1822 0.462609 12.3043 0.62412 12.3881 0.805926C12.4718 0.987722 12.5151 1.18553 12.5151 1.38568V8.51459C12.5168 8.85657 12.3919 9.1871 12.1645 9.44259C12.112 9.50159 12.0549 9.55562 11.9939 9.6043C11.7994 9.56796 11.5987 9.54895 11.3936 9.54895H10.6436V8.04895C10.6436 6.66824 9.52427 5.54895 8.14355 5.54895C7.68375 5.54895 7.25295 5.67308 6.88281 5.88966ZM6.89355 13.299V8.04895C6.89355 7.35859 7.4532 6.79895 8.14355 6.79895C8.83391 6.79895 9.39355 7.35859 9.39355 8.04895V10.799H11.3936C12.4981 10.799 13.3936 11.6944 13.3936 12.799V13.299C13.3936 13.5751 13.1697 13.799 12.8936 13.799H7.39355C7.11741 13.799 6.89355 13.5751 6.89355 13.299Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189512\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"class-lesson\", \"keywords\": [ \"class\", \"lesson\", \"education\", \"teacher\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189503)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M12.4019 8.97632H7.25922C8.42674 8.87838 9.34376 7.89974 9.34376 6.70691C9.34376 5.4491 8.3241 4.42944 7.06629 4.42944H5.38599C5.7693 3.91386 5.9961 3.27501 5.9961 2.58319C5.9961 1.50454 5.44476 0.554665 4.60852 0H12.4019C13.1469 0 13.7507 0.603888 13.7507 1.34882V7.6275C13.7507 8.37243 13.1469 8.97632 12.4019 8.97632ZM2.89856 4.43073C3.91893 4.43073 4.7461 3.60356 4.7461 2.58319C4.7461 1.56283 3.91893 0.735657 2.89856 0.735657C1.8782 0.735657 1.05103 1.56283 1.05103 2.58319C1.05103 3.60356 1.8782 4.43073 2.89856 4.43073ZM8.09376 6.70691C8.09376 6.13946 7.63374 5.67944 7.06629 5.67944H2.89893C1.43597 5.67944 0.25 6.86541 0.25 8.32837V9.53391C0.25 10.0661 0.681426 10.4975 1.21362 10.4975H1.38526L1.66724 13.1074C1.7221 13.6152 2.15075 14 2.66145 14H3.16373C3.66275 14 4.08539 13.6321 4.15418 13.1379L4.90626 7.73438H7.06629C7.63374 7.73438 8.09376 7.27436 8.09376 6.70691Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189503\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"collaborations-idea\", \"keywords\": [ \"collaborations\", \"idea\", \"work\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189485)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M9.88672 0C10.2319 0 10.5117 0.279822 10.5117 0.625V1.44559C10.5117 1.79077 10.2319 2.07059 9.88672 2.07059C9.54154 2.07059 9.26172 1.79077 9.26172 1.44559V0.625C9.26172 0.279822 9.54154 0 9.88672 0ZM4.04771 8.73814C5.38896 8.73814 6.47627 7.65084 6.47627 6.30958C6.47627 4.96832 5.38896 3.88101 4.04771 3.88101C2.70645 3.88101 1.61914 4.96832 1.61914 6.30958C1.61914 7.65084 2.70645 8.73814 4.04771 8.73814ZM0 13.5952C0 11.3598 1.81218 9.54764 4.04761 9.54764C6.28304 9.54764 8.09521 11.3598 8.09521 13.5952C8.09521 13.8188 7.914 14 7.69045 14H0.404761C0.181218 14 0 13.8188 0 13.5952ZM13.7165 2.49274C13.999 2.29442 14.0673 1.90462 13.869 1.6221C13.6706 1.33959 13.2808 1.27134 12.9983 1.46966L12.3267 1.94113C12.0442 2.13946 11.9759 2.52926 12.1742 2.81177C12.3726 3.09429 12.7624 3.16254 13.0449 2.96422L13.7165 2.49274ZM5.90253 1.6221C5.70421 1.90462 5.77246 2.29442 6.05497 2.49274L6.7266 2.96422C7.00911 3.16254 7.39891 3.09429 7.59723 2.81177C7.79556 2.52926 7.72731 2.13946 7.44479 1.94113L6.77317 1.46966C6.49065 1.27134 6.10085 1.33959 5.90253 1.6221ZM9.8405 2.84017C11.0737 2.81456 12.1176 3.84088 12.113 5.07432C12.1029 5.89314 11.6121 6.6749 10.879 7.03982V8.15297C10.8774 8.21766 10.8505 8.27913 10.8041 8.32429C10.7578 8.36944 10.6956 8.3947 10.6309 8.39468H9.1425C9.0778 8.3947 9.01564 8.36944 8.9693 8.32429C8.92295 8.27913 8.89609 8.21766 8.89443 8.15297V7.0589C8.18372 6.70503 7.69866 5.95988 7.66272 5.16676C7.60689 3.93457 8.60732 2.86578 9.8405 2.84017Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189485\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"definition-search-book\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189494)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.5 0.125244C0.223858 0.125244 0 0.349102 0 0.625244V10.6252C0 10.9014 0.223858 11.1252 0.5 11.1252H5.25606C5.09023 10.6561 5 10.1512 5 9.62524C5 8.35387 5.52724 7.20564 6.375 6.38728V1.42771C6.09192 0.667083 5.35924 0.125244 4.5 0.125244H0.5ZM7.625 1.4277V5.53328C8.19579 5.2713 8.83084 5.12524 9.5 5.12524C11.8136 5.12524 13.7194 6.87122 13.9717 9.11743H14V0.625244C14 0.349102 13.7761 0.125244 13.5 0.125244H9.5C8.64077 0.125244 7.90808 0.667068 7.625 1.4277ZM9.5 7.87524C8.5335 7.87524 7.75 8.65875 7.75 9.62524C7.75 10.5917 8.5335 11.3752 9.5 11.3752C10.4665 11.3752 11.25 10.5917 11.25 9.62524C11.25 8.65875 10.4665 7.87524 9.5 7.87524ZM6.25 9.62524C6.25 7.83032 7.70507 6.37524 9.5 6.37524C11.2949 6.37524 12.75 7.83032 12.75 9.62524C12.75 10.251 12.5732 10.8354 12.2667 11.3313L13.5299 12.5944C13.8228 12.8873 13.8228 13.3622 13.5299 13.6551C13.237 13.948 12.7621 13.948 12.4692 13.6551L11.2061 12.392C10.7102 12.6984 10.1257 12.8752 9.5 12.8752C7.70507 12.8752 6.25 11.4202 6.25 9.62524Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189494\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"dictionary-language-book\", \"keywords\": [], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.25 11.625V2.25C1.25 1.00736 2.25736 0 3.5 0H10.5C11.7426 0 12.75 1.00736 12.75 2.25V10C12.75 10.4142 12.4142 10.75 12 10.75H11.75V12.5H12C12.4142 12.5 12.75 12.8358 12.75 13.25C12.75 13.6642 12.4142 14 12 14H11H3.625C2.31332 14 1.25 12.9367 1.25 11.625ZM10.25 10.75H3.625C3.14175 10.75 2.75 11.1418 2.75 11.625C2.75 12.1082 3.14175 12.5 3.625 12.5H10.25V10.75ZM10.8107 4.77228C10.7248 4.60623 10.5535 4.50195 10.3665 4.50195H8.05148C7.77534 4.50195 7.55148 4.72581 7.55148 5.00195C7.55148 5.2781 7.77534 5.50195 8.05148 5.50195H9.39965L7.49878 8.18553C7.39072 8.33808 7.37679 8.53816 7.46266 8.70421C7.54852 8.87026 7.71985 8.97454 7.90679 8.97454H10.5112C10.7874 8.97454 11.0112 8.75069 11.0112 8.47454C11.0112 8.1984 10.7874 7.97454 10.5112 7.97454H8.87368L10.7746 5.29096C10.8826 5.13842 10.8965 4.93833 10.8107 4.77228ZM5.81387 2.62936C5.69116 2.26124 5.34666 2.01294 4.95863 2.01294C4.5706 2.01294 4.2261 2.26124 4.1034 2.62936L3.42691 4.65881C3.4243 4.66607 3.42185 4.67341 3.41957 4.68082L3.03738 5.82742C2.95005 6.08939 3.09163 6.37255 3.3536 6.45987C3.61558 6.54719 3.89874 6.40561 3.98606 6.14364L4.25794 5.328H5.65932L5.9312 6.14364C6.01853 6.40561 6.30169 6.54719 6.56366 6.45987C6.82563 6.37255 6.96721 6.08939 6.87989 5.82742L6.49769 4.68083C6.49541 4.67341 6.49296 4.66607 6.49035 4.65881L5.81387 2.62936ZM4.59127 4.328L4.95863 3.22593L5.32599 4.328H4.59127Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"global-learning\", \"keywords\": [ \"global\", \"learning\", \"education\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189515)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0 5.36834C0.29929 2.47376 2.65444 0.189377 5.58036 0.000265758C5.54627 0.0276337 5.51486 0.0587409 5.48678 0.0932758C4.25652 1.60616 3.4918 3.43852 3.27803 5.36834H0ZM3.65196 8.90907C3.65196 8.953 3.6536 8.99667 3.65684 9.04V11.5359C1.67339 10.7058 0.229961 8.84217 4.83546e-06 6.61834H3.27804C3.35121 7.27883 3.48891 7.92791 3.68773 8.55666C3.6642 8.67119 3.65196 8.78914 3.65196 8.90907ZM4.53677 6.61834H6.28714L4.70622 7.30331C4.6892 7.31068 4.67234 7.31831 4.65564 7.32618C4.60659 7.0924 4.5669 6.85627 4.53677 6.61834ZM11.9472 5.36834H8.66534C8.45157 3.43852 7.68685 1.60616 6.45659 0.0932758C6.42842 0.0586291 6.3969 0.0274323 6.36269 0C9.29052 0.187225 11.6477 2.4724 11.9472 5.36834ZM4.53677 5.36834H7.4066C7.23293 3.99653 6.7412 2.68472 5.97169 1.53757C5.20217 2.68472 4.71045 3.99653 4.53677 5.36834ZM9.65015 6.71004C9.51167 6.65004 9.35452 6.65004 9.21604 6.71004L5.24248 8.43168C5.20049 8.44988 5.16161 8.47295 5.1264 8.50007C4.91789 8.61686 4.77696 8.83992 4.77696 9.0959V12.1244C4.77696 12.5013 5.08251 12.8068 5.45942 12.8068C5.83633 12.8068 6.14188 12.5013 6.14188 12.1244V9.82329L9.21604 11.1553C9.35452 11.2153 9.51167 11.2153 9.65015 11.1553L13.6237 9.43361C13.8234 9.34709 13.9526 9.15026 13.9526 8.93265C13.9526 8.71503 13.8234 8.5182 13.6237 8.43168L9.65015 6.71004ZM6.71728 11.2197L8.74112 12.0966C9.18242 12.2878 9.68319 12.2878 10.1245 12.0966L12.1483 11.2197L12.1486 12.5783C12.1486 12.6976 12.1096 12.8136 12.0374 12.9087L11.6031 12.5787C12.0374 12.9087 12.0376 12.9085 12.0374 12.9087L12.0367 12.9096L12.0358 12.9107L12.034 12.9131L12.0296 12.9187L12.0175 12.9337C12.0081 12.9453 11.9957 12.96 11.9804 12.9773C11.9499 13.0119 11.9074 13.0571 11.8526 13.1093C11.7431 13.2137 11.5833 13.3467 11.3696 13.4778C10.9388 13.7422 10.2985 13.9925 9.43263 13.9925C8.56671 13.9925 7.92709 13.7422 7.49687 13.4775C7.28351 13.3463 7.1241 13.2131 7.01486 13.1086C6.96019 13.0563 6.91785 13.011 6.88736 12.9764C6.87211 12.959 6.85979 12.9443 6.85034 12.9327L6.83831 12.9177L6.83389 12.912L6.83207 12.9096L6.83126 12.9085C6.83108 12.9083 6.83051 12.9075 7.26597 12.5785L6.83051 12.9075C6.75913 12.8131 6.72039 12.698 6.72014 12.5795L6.71728 11.2197Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189515\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"graduation-cap\", \"keywords\": [ \"graduation\", \"cap\", \"education\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M7.19878 0.728346C7.07196 0.673396 6.92804 0.673396 6.80122 0.728346L0.301219 3.54463C0.118352 3.62386 0 3.80412 0 4.00342C0 4.20271 0.118352 4.38297 0.301219 4.46221L1.19382 4.84895V10.44C0.759479 10.6327 0.456528 11.0677 0.456528 11.5735C0.456528 12.258 1.01143 12.8129 1.69594 12.8129C2.38045 12.8129 2.93535 12.258 2.93535 11.5735C2.93535 11.0672 2.63182 10.6318 2.19681 10.4394V5.28352L6.80122 7.27849C6.92804 7.33344 7.07196 7.33344 7.19878 7.27849L13.6988 4.46221C13.8816 4.38297 14 4.20271 14 4.00342C14 3.80412 13.8816 3.62386 13.6988 3.54463L7.19878 0.728346ZM2.95569 9.35343L2.95068 6.97264L6.3038 8.42546C6.74769 8.61778 7.25139 8.61778 7.69527 8.42546L11.049 6.97238L11.0495 9.35227C11.0495 9.46155 11.0137 9.56782 10.9476 9.65484L10.5495 9.35238C10.9476 9.65484 10.9478 9.65462 10.9476 9.65484L10.9469 9.65576L10.9461 9.65682L10.9442 9.65932L10.9391 9.66585L10.9238 9.68483C10.9114 9.70001 10.8945 9.72016 10.8729 9.74455C10.8298 9.79331 10.7681 9.85917 10.6872 9.93629C10.5254 10.0904 10.2857 10.2904 9.9627 10.4886C9.31348 10.887 8.33649 11.2726 6.99973 11.2726C5.66292 11.2726 4.68718 10.887 4.03914 10.4883C3.71673 10.29 3.47771 10.0899 3.31639 9.93563C3.23569 9.85844 3.17421 9.79252 3.13126 9.7437C3.10977 9.71928 3.09288 9.6991 3.0805 9.68389L3.0653 9.66488L3.06021 9.65832L3.05828 9.65581L3.05748 9.65475C3.05731 9.65452 3.05677 9.65382 3.45565 9.35241L3.05677 9.65382C2.9914 9.5673 2.95592 9.46187 2.95569 9.35343Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"group-meeting-call\", \"keywords\": [ \"group\", \"meeting\", \"call\", \"work\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M2.21823 0C1.54542 0 1 0.545422 1 1.21823V5.78891C1 6.46172 1.54542 7.00714 2.21823 7.00714H11.7818C12.4546 7.00714 13 6.46172 13 5.78891V1.21823C13 0.545421 12.4546 0 11.7818 0H2.21823ZM4.92088 9.44131C4.92088 10.2373 4.27559 10.8826 3.47958 10.8826C2.68356 10.8826 2.03827 10.2373 2.03827 9.44131C2.03827 8.6453 2.68356 8 3.47958 8C4.27559 8 4.92088 8.6453 4.92088 9.44131ZM10.5254 10.8826C11.3214 10.8826 11.9667 10.2373 11.9667 9.44131C11.9667 8.6453 11.3214 8 10.5254 8C9.72938 8 9.08409 8.6453 9.08409 9.44131C9.08409 10.2373 9.72938 10.8826 10.5254 10.8826ZM3.47949 11.661C2.32188 11.661 1.36101 12.5033 1.1764 13.6085C1.14098 13.8205 1.31863 13.9961 1.53357 13.9961H5.42542C5.64036 13.9961 5.818 13.8205 5.78259 13.6085C5.59798 12.5033 4.63711 11.661 3.47949 11.661ZM8.22229 13.6085C8.40691 12.5033 9.36778 11.661 10.5254 11.661C11.683 11.661 12.6439 12.5033 12.8285 13.6085C12.8639 13.8205 12.6863 13.9961 12.4713 13.9961H8.57947C8.36453 13.9961 8.18688 13.8205 8.22229 13.6085ZM8.12343 2.12148C8.12343 2.7419 7.62048 3.24486 7.00006 3.24486C6.37963 3.24486 5.87668 2.7419 5.87668 2.12148C5.87668 1.50105 6.37963 0.998101 7.00006 0.998101C7.62048 0.998101 8.12343 1.50105 8.12343 2.12148ZM5.02496 5.65702C5.18328 4.70925 6.00728 3.98688 7 3.98688C7.99272 3.98688 8.81672 4.70925 8.97503 5.65702C9.0054 5.83882 8.85306 5.98937 8.66874 5.98937H5.33126C5.14693 5.98937 4.99459 5.83882 5.02496 5.65702Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"office-building-1\", \"keywords\": [ \"office\", \"building\", \"work\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189506)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M1.13917 1.519C1.13917 0.68197 1.81772 0.00341797 2.65475 0.00341797H7.44523C8.28228 0.00341797 8.96081 0.681977 8.96081 1.519L8.9608 5.00664C8.06379 5.58057 7.46918 6.5856 7.46918 7.72951C7.46918 8.53378 7.76311 9.2694 8.24942 9.8348C6.98626 10.6434 6.14899 12.0589 6.14899 13.6699C6.14899 13.7831 6.16088 13.8935 6.1835 13.9999H1.63916C1.62334 13.9999 1.60761 13.9992 1.592 13.9977H0.765625C0.351412 13.9977 0.015625 13.6619 0.015625 13.2477C0.015625 12.8335 0.351412 12.4977 0.765625 12.4977H1.13916L1.13917 5.62207H5.48681C5.83199 5.62207 6.11181 5.34225 6.11181 4.99707C6.11181 4.65189 5.83199 4.37207 5.48681 4.37207H1.13917V3.02213H4.09928C4.44446 3.02213 4.72428 2.7423 4.72428 2.39713C4.72428 2.05195 4.44446 1.77213 4.09928 1.77213H1.13917V1.519ZM12.6795 7.72951C12.6795 8.82312 11.7929 9.70966 10.6993 9.70966C9.60572 9.70966 8.71918 8.82312 8.71918 7.72951C8.71918 6.6359 9.60572 5.74936 10.6993 5.74936C11.7929 5.74936 12.6795 6.6359 12.6795 7.72951ZM10.6992 10.3697C8.87656 10.3697 7.39899 11.8473 7.39899 13.6699C7.39899 13.8522 7.54674 14 7.72901 14H13.6695C13.8517 14 13.9995 13.8522 13.9995 13.6699C13.9995 11.8473 12.5219 10.3697 10.6992 10.3697Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189506\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"office-worker\", \"keywords\": [ \"office\", \"worker\", \"human\", \"resources\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189491)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.5 4C4.60457 4 5.5 3.10457 5.5 2C5.5 0.89543 4.60457 0 3.5 0C2.39543 0 1.5 0.89543 1.5 2C1.5 3.10457 2.39543 4 3.5 4ZM5.54492 10V9.9705C5.54492 8.98028 6.09168 8.11776 6.89977 7.66837C6.74884 7.05141 6.43144 6.48169 5.97487 6.02513C5.3185 5.36875 4.42826 5 3.5 5C2.57174 5 1.6815 5.36875 1.02513 6.02513C0.368749 6.6815 0 7.57174 0 8.5V9.5C0 9.77614 0.223858 10 0.5 10H1.5L1.94525 13.562C1.97653 13.8122 2.18923 14 2.44139 14H4.55861C4.81077 14 5.02347 13.8122 5.05475 13.562L5.5 10H5.54492ZM9.927 7.68726C9.78893 7.68726 9.677 7.79918 9.677 7.93726V8.58813H11.0896V7.93726C11.0896 7.79918 10.9776 7.68726 10.8396 7.68726H9.927ZM8.177 7.93726V8.58813C7.41367 8.58829 6.79492 9.20714 6.79492 9.9705V12.6176C6.79492 13.381 7.41383 13.9999 8.17728 13.9999H12.5891C13.3525 13.9999 13.9714 13.381 13.9714 12.6176V9.9705C13.9714 9.20721 13.3528 8.58841 12.5896 8.58813V7.93726C12.5896 6.97076 11.8061 6.18726 10.8396 6.18726H9.927C8.9605 6.18726 8.177 6.97076 8.177 7.93726Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189491\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"search-dollar\", \"keywords\": [ \"search\", \"pay\", \"product\", \"currency\", \"query\", \"magnifying\", \"cash\", \"business\", \"money\", \"glass\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189521)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M6.5 1.5C3.73858 1.5 1.5 3.73858 1.5 6.5C1.5 9.26142 3.73858 11.5 6.5 11.5C9.26142 11.5 11.5 9.26142 11.5 6.5C11.5 3.73858 9.26142 1.5 6.5 1.5ZM0 6.5C0 2.91015 2.91015 0 6.5 0C10.0899 0 13 2.91015 13 6.5C13 7.9341 12.5356 9.25973 11.7489 10.3347L13.7073 12.2931C14.0979 12.6836 14.0979 13.3168 13.7073 13.7073C13.3168 14.0978 12.6837 14.0978 12.2931 13.7073L10.3347 11.7489C9.25974 12.5356 7.9341 13 6.5 13C2.91015 13 0 10.0899 0 6.5ZM5.83661 4.61934C5.83661 4.44481 5.97829 4.30333 6.15307 4.30333H6.84635C7.02112 4.30333 7.1628 4.44481 7.1628 4.61934V5.11673H5.83661V4.61934ZM8.21766 4.61934V5.11673H8.67708C9.2014 5.11673 9.62645 5.54116 9.62645 6.06473V8.30595C9.62645 8.82951 9.2014 9.25395 8.67708 9.25395H4.32294C3.79861 9.25395 3.37357 8.82951 3.37357 8.30595V6.06473C3.37357 5.54116 3.79861 5.11673 4.32294 5.11673H4.78176V4.61934C4.78176 3.86307 5.39571 3.25 6.15307 3.25H6.84635C7.6037 3.25 8.21766 3.86307 8.21766 4.61934Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189521\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" }, { \"name\": \"strategy-tasks\", \"keywords\": [ \"strategy\", \"tasks\", \"work\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M5.5 0C4.94772 0 4.5 0.447716 4.5 1V1.5C4.5 1.50216 4.50001 1.50431 4.50002 1.50647C4.5035 2.05578 4.94987 2.5 5.5 2.5H8.5C9.05229 2.5 9.5 2.05229 9.5 1.5V1C9.5 0.447715 9.05229 0 8.5 0H5.5ZM3.5 1H3C2.17157 1 1.5 1.67157 1.5 2.5V12.5C1.5 13.3284 2.17157 14 3 14H11C11.8284 14 12.5 13.3284 12.5 12.5V2.5C12.5 1.67157 11.8284 1 11 1H10.5V1.5C10.5 2.60457 9.60457 3.5 8.5 3.5H5.5C4.39543 3.5 3.5 2.60457 3.5 1.5V1ZM4.53662 6.56888C4.53662 6.14956 4.87655 5.80963 5.29587 5.80963C5.71519 5.80963 6.05512 6.14956 6.05512 6.56888C6.05512 6.88122 5.86639 7.1509 5.59425 7.26742C5.50347 7.30629 5.40303 7.32812 5.29587 7.32812C4.87655 7.32812 4.53662 6.9882 4.53662 6.56888ZM5.29587 4.55963C4.18619 4.55963 3.28662 5.4592 3.28662 6.56888C3.28662 7.67855 4.18619 8.57812 5.29587 8.57812C5.57525 8.57812 5.84284 8.52074 6.08625 8.41653C6.80182 8.11015 7.30512 7.399 7.30512 6.56888C7.30512 5.4592 6.40554 4.55963 5.29587 4.55963ZM10.3957 8.89082C10.6398 9.1349 10.6398 9.53063 10.3957 9.77471L9.58685 10.5836L10.3941 11.3908C10.6382 11.6349 10.6382 12.0306 10.3941 12.2747C10.15 12.5188 9.75428 12.5188 9.51021 12.2747L8.70296 11.4675L7.89572 12.2747C7.65164 12.5188 7.25591 12.5188 7.01184 12.2747C6.76776 12.0306 6.76776 11.6349 7.01184 11.3908L7.81908 10.5836L7.01021 9.77471C6.76613 9.53063 6.76613 9.1349 7.01021 8.89083C7.25428 8.64675 7.65001 8.64675 7.89409 8.89083L8.70296 9.6997L9.51184 8.89082C9.75591 8.64674 10.1516 8.64674 10.3957 8.89082Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"task-list\", \"keywords\": [ \"task\", \"list\", \"work\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M0.65809 0.43934C0.939394 0.158035 1.32093 0 1.71875 0H7.30454C7.70236 0 8.08389 0.158035 8.3652 0.43934L11.7794 3.85355C12.0607 4.13486 12.2188 4.51639 12.2188 4.91421V12.5C12.2188 12.8978 12.0607 13.2794 11.7794 13.5607C11.4981 13.842 11.1166 14 10.7188 14H1.71875C1.32092 14 0.939394 13.842 0.65809 13.5607C0.376785 13.2794 0.21875 12.8978 0.21875 12.5V1.5C0.21875 1.10218 0.376785 0.720644 0.65809 0.43934ZM5.33031 4.52837C5.66737 4.76913 5.74544 5.23754 5.50468 5.5746L4.10819 7.52968C3.98005 7.70907 3.77947 7.82301 3.55975 7.84119C3.34004 7.85938 3.12346 7.77997 2.96756 7.62408L2.12967 6.78619C1.83678 6.49329 1.83678 6.01842 2.12967 5.72553C2.42257 5.43263 2.89744 5.43263 3.19033 5.72553L3.40219 5.93738L4.28408 4.70274C4.52484 4.36568 4.99325 4.28761 5.33031 4.52837ZM6.28 6.375C6.28 5.96079 6.61579 5.625 7.03 5.625H9.53C9.94421 5.625 10.28 5.96079 10.28 6.375C10.28 6.78921 9.94421 7.125 9.53 7.125H7.03C6.61579 7.125 6.28 6.78921 6.28 6.375ZM6.28 10.3438C6.28 9.92954 6.61579 9.59375 7.03 9.59375H9.53C9.94421 9.59375 10.28 9.92954 10.28 10.3438C10.28 10.758 9.94421 11.0938 9.53 11.0938H7.03C6.61579 11.0938 6.28 10.758 6.28 10.3438ZM5.50468 9.60585C5.74544 9.26879 5.66737 8.80038 5.33031 8.55962C4.99325 8.31886 4.52484 8.39693 4.28408 8.73399L3.40219 9.96863L3.19033 9.75678C2.89744 9.46388 2.42257 9.46388 2.12967 9.75678C1.83678 10.0497 1.83678 10.5245 2.12967 10.8174L2.96756 11.6553C3.12346 11.8112 3.34004 11.8906 3.55975 11.8724C3.77947 11.8543 3.98005 11.7403 4.10819 11.5609L5.50468 9.60585Z\\\" fill=\\\"black\\\"/>\\n</svg>\\n\" }, { \"name\": \"workspace-desk\", \"keywords\": [ \"workspace\", \"desk\", \"work\" ], \"content\": \"<svg width=\\\"14\\\" height=\\\"14\\\" viewBox=\\\"0 0 14 14\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\">\\n<g clip-path=\\\"url(#clip0_1068_189509)\\\">\\n<path fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M3.78399 1.50464H10.216C10.2348 1.50464 10.25 1.51985 10.25 1.53862V5.47062C10.25 5.48939 10.2348 5.50461 10.216 5.50461H3.78399C3.76522 5.50461 3.75 5.48939 3.75 5.47063V1.53863C3.75 1.51986 3.76522 1.50464 3.78399 1.50464ZM10.216 0.00463867L3.78399 0.00464058C2.93679 0.00464058 2.25 0.691429 2.25 1.53863V5.47063C2.25 6.31782 2.93679 7.00461 3.78399 7.00461H6.24867L6.24872 8.02075H2.2023C1.17586 8.02075 0.34375 8.85286 0.34375 9.8793V13.2455C0.34375 13.6597 0.679537 13.9955 1.09375 13.9955C1.50797 13.9955 1.84375 13.6597 1.84375 13.2455V10.7278H12.1561V13.2455C12.1561 13.6597 12.4919 13.9955 12.9061 13.9955C13.3203 13.9955 13.6561 13.6597 13.6561 13.2455V9.8793C13.6561 8.85287 12.824 8.02075 11.7975 8.02075H7.74872L7.74867 7.00461H10.216C11.0632 7.00461 11.75 6.31782 11.75 5.47062V1.53862C11.75 0.691427 11.0632 0.00463867 10.216 0.00463867Z\\\" fill=\\\"black\\\"/>\\n</g>\\n<defs>\\n<clipPath id=\\\"clip0_1068_189509\\\">\\n<rect width=\\\"14\\\" height=\\\"14\\\" fill=\\\"white\\\"/>\\n</clipPath>\\n</defs>\\n</svg>\\n\" } ] }"
  },
  {
    "path": "frontend/appflowy_flutter/assets/template/readme.afdoc",
    "content": "{\n    \"document\": {\n      \"type\": \"editor\",\n      \"children\": [\n        { \"type\": \"cover\" },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"heading\", \"heading\": \"h1\" },\n          \"delta\": [{ \"insert\": \"Welcome to AppFlowy!\" }]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"heading\", \"heading\": \"h2\" },\n          \"delta\": [{ \"insert\": \"Here are the basics\" }]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"checkbox\", \"checkbox\": null },\n          \"delta\": [{ \"insert\": \"Click anywhere and just start typing.\" }]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"checkbox\", \"checkbox\": false },\n          \"delta\": [\n            {\n              \"insert\": \"Highlight \",\n              \"attributes\": { \"backgroundColor\": \"0x4dffeb3b\" }\n            },\n            { \"insert\": \"any text, and use the editing menu to \" },\n            { \"insert\": \"style\", \"attributes\": { \"italic\": true } },\n            { \"insert\": \" \" },\n            { \"insert\": \"your\", \"attributes\": { \"bold\": true } },\n            { \"insert\": \" \" },\n            { \"insert\": \"writing\", \"attributes\": { \"underline\": true } },\n            { \"insert\": \" \" },\n            { \"insert\": \"however\", \"attributes\": { \"code\": true } },\n            { \"insert\": \" you \" },\n            { \"insert\": \"like.\", \"attributes\": { \"strikethrough\": true } }\n          ]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"checkbox\", \"checkbox\": null },\n          \"delta\": [\n            { \"insert\": \"As soon as you type \" },\n            {\n              \"insert\": \"/\",\n              \"attributes\": { \"code\": true, \"color\": \"0xff00b5ff\" }\n            },\n            { \"insert\": \" a menu will pop up. Select \" },\n            {\n              \"insert\": \"different types\",\n              \"attributes\": { \"backgroundColor\": \"0x4d9c27b0\" }\n            },\n            { \"insert\": \" of content blocks you can add.\" }\n          ]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"checkbox\", \"checkbox\": null },\n          \"delta\": [\n            { \"insert\": \"Type \" },\n            { \"insert\": \"/\", \"attributes\": { \"code\": true } },\n            { \"insert\": \" followed by \" },\n            { \"insert\": \"/bullet\", \"attributes\": { \"code\": true } },\n            { \"insert\": \" or \" },\n            { \"insert\": \"/num\", \"attributes\": { \"code\": true } },\n            { \"insert\": \" to create a list.\", \"attributes\": { \"code\": false } }\n          ]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"checkbox\", \"checkbox\": true },\n          \"delta\": [\n            { \"insert\": \"Click \" },\n            { \"insert\": \"+ New Page \", \"attributes\": { \"code\": true } },\n            {\n              \"insert\": \"button at the bottom of your sidebar to add a new page.\"\n            }\n          ]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"checkbox\", \"checkbox\": null },\n          \"delta\": [\n            { \"insert\": \"Click \" },\n            { \"insert\": \"+\", \"attributes\": { \"code\": true } },\n            { \"insert\": \" next to any page title in the sidebar to \" },\n            { \"insert\": \"quickly\", \"attributes\": { \"color\": \"0xff8427e0\" } },\n            { \"insert\": \" add a new subpage, \" },\n            { \"insert\": \"Document\", \"attributes\": { \"code\": true } },\n            { \"insert\": \", \", \"attributes\": { \"code\": false } },\n            { \"insert\": \"Grid\", \"attributes\": { \"code\": true } },\n            { \"insert\": \", or \", \"attributes\": { \"code\": false } },\n            { \"insert\": \"Kanban Board\", \"attributes\": { \"code\": true } },\n            { \"insert\": \".\", \"attributes\": { \"code\": false } }\n          ]\n        },\n        { \"type\": \"text\", \"delta\": [] },\n        { \"type\": \"divider\" },\n        { \"type\": \"text\", \"attributes\": { \"checkbox\": null }, \"delta\": [] },\n        {\n          \"type\": \"text\",\n          \"attributes\": {\n            \"subtype\": \"heading\",\n            \"checkbox\": null,\n            \"heading\": \"h2\"\n          },\n          \"delta\": [{ \"insert\": \"Keyboard shortcuts, markdown, and code block\" }]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": {\n            \"subtype\": \"number-list\",\n            \"number\": 1,\n            \"heading\": null\n          },\n          \"delta\": [\n            { \"insert\": \"Keyboard shortcuts \" },\n            {\n              \"insert\": \"guide\",\n              \"attributes\": {\n                \"href\": \"https://appflowy.gitbook.io/docs/essential-documentation/shortcuts\"\n              }\n            },\n            { \"retain\": 1, \"attributes\": { \"strikethrough\": true } }\n          ]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": {\n            \"subtype\": \"number-list\",\n            \"number\": 2,\n            \"heading\": null\n          },\n          \"delta\": [\n            { \"insert\": \"Markdown \" },\n            {\n              \"insert\": \"reference\",\n              \"attributes\": {\n                \"href\": \"https://appflowy.gitbook.io/docs/essential-documentation/markdown\"\n              }\n            },\n            { \"retain\": 1, \"attributes\": { \"strikethrough\": true } }\n          ]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"number\": 3, \"subtype\": \"number-list\" },\n          \"delta\": [\n            { \"insert\": \"Type \" },\n            { \"insert\": \"/code\", \"attributes\": { \"code\": true } },\n            {\n              \"insert\": \" to insert a code block\",\n              \"attributes\": { \"code\": false }\n            }\n          ]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": {\n            \"subtype\": \"code_block\",\n            \"number\": 3,\n            \"heading\": null,\n            \"number-list\": null,\n            \"theme\": \"vs\",\n            \"language\": \"rust\"\n          },\n          \"delta\": [\n            {\n              \"insert\": \"// This is the main function.\\nfn main() {\\n    // Print text to the console.\\n    println!(\\\"Hello World!\\\");\\n}\"\n            },\n            { \"retain\": 1, \"attributes\": { \"strikethrough\": true } }\n          ]\n        },\n        { \"type\": \"text\", \"attributes\": { \"checkbox\": null }, \"delta\": [] },\n        {\n          \"type\": \"text\",\n          \"attributes\": {\n            \"subtype\": \"heading\",\n            \"checkbox\": null,\n            \"heading\": \"h2\"\n          },\n          \"delta\": [{ \"insert\": \"Have a question❓\" }]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"quote\" },\n          \"delta\": [\n            { \"insert\": \"Click \" },\n            { \"insert\": \"?\", \"attributes\": { \"code\": true } },\n            { \"insert\": \" at the bottom right for help and support.\" }\n          ]\n        },\n        { \"type\": \"text\", \"delta\": [] },\n        {\n          \"type\": \"callout\",\n          \"children\": [\n            { \"type\": \"text\", \"delta\": [] },\n            {\n              \"type\": \"text\",\n              \"attributes\": { \"subtype\": \"heading\", \"heading\": \"h2\" },\n              \"delta\": [{ \"insert\": \"Like AppFlowy? Follow us:\" }]\n            },\n            {\n              \"type\": \"text\",\n              \"attributes\": { \"subtype\": \"bulleted-list\" },\n              \"delta\": [\n                {\n                  \"insert\": \"GitHub\",\n                  \"attributes\": {\n                    \"href\": \"https://github.com/AppFlowy-IO/AppFlowy\"\n                  }\n                }\n              ]\n            },\n            {\n              \"type\": \"text\",\n              \"attributes\": { \"subtype\": \"bulleted-list\" },\n              \"delta\": [\n                {\n                  \"insert\": \"Twitter\",\n                  \"attributes\": { \"href\": \"https://twitter.com/appflowy\" }\n                },\n                { \"insert\": \": @appflowy\" }\n              ]\n            },\n            {\n              \"type\": \"text\",\n              \"attributes\": { \"subtype\": \"bulleted-list\" },\n              \"delta\": [\n                {\n                  \"insert\": \"Newsletter\",\n                  \"attributes\": { \"href\": \"https://blog-appflowy.ghost.io/\" }\n                }\n              ]\n            }\n          ],\n          \"attributes\": { \"emoji\": \"😀\" }\n        },\n        { \"type\": \"text\", \"delta\": [] },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": null, \"heading\": null },\n          \"delta\": []\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": null, \"heading\": null },\n          \"delta\": []\n        }\n      ]\n    }\n  }\n"
  },
  {
    "path": "frontend/appflowy_flutter/assets/template/readme.json",
    "content": "{\n    \"document\": {\n      \"type\": \"editor\",\n      \"children\": [\n        { \"type\": \"cover\" },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"heading\", \"heading\": \"h1\" },\n          \"delta\": [{ \"insert\": \"Welcome to AppFlowy!\" }]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"heading\", \"heading\": \"h2\" },\n          \"delta\": [{ \"insert\": \"Here are the basics\" }]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"checkbox\", \"checkbox\": null },\n          \"delta\": [{ \"insert\": \"Click anywhere and just start typing.\" }]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"checkbox\", \"checkbox\": false },\n          \"delta\": [\n            {\n              \"insert\": \"Highlight \",\n              \"attributes\": { \"backgroundColor\": \"0x4dffeb3b\" }\n            },\n            { \"insert\": \"any text, and use the editing menu to \" },\n            { \"insert\": \"style\", \"attributes\": { \"italic\": true } },\n            { \"insert\": \" \" },\n            { \"insert\": \"your\", \"attributes\": { \"bold\": true } },\n            { \"insert\": \" \" },\n            { \"insert\": \"writing\", \"attributes\": { \"underline\": true } },\n            { \"insert\": \" \" },\n            { \"insert\": \"however\", \"attributes\": { \"code\": true } },\n            { \"insert\": \" you \" },\n            { \"insert\": \"like.\", \"attributes\": { \"strikethrough\": true } }\n          ]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"checkbox\", \"checkbox\": null },\n          \"delta\": [\n            { \"insert\": \"As soon as you type \" },\n            {\n              \"insert\": \"/\",\n              \"attributes\": { \"code\": true, \"color\": \"0xff00b5ff\" }\n            },\n            { \"insert\": \" a menu will pop up. Select \" },\n            {\n              \"insert\": \"different types\",\n              \"attributes\": { \"backgroundColor\": \"0x4d9c27b0\" }\n            },\n            { \"insert\": \" of content blocks you can add.\" }\n          ]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"checkbox\", \"checkbox\": null },\n          \"delta\": [\n            { \"insert\": \"Type \" },\n            { \"insert\": \"/\", \"attributes\": { \"code\": true } },\n            { \"insert\": \" followed by \" },\n            { \"insert\": \"/bullet\", \"attributes\": { \"code\": true } },\n            { \"insert\": \" or \" },\n            { \"insert\": \"/num\", \"attributes\": { \"code\": true } },\n            { \"insert\": \" to create a list.\", \"attributes\": { \"code\": false } }\n          ]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"checkbox\", \"checkbox\": true },\n          \"delta\": [\n            { \"insert\": \"Click \" },\n            { \"insert\": \"+ New Page\", \"attributes\": { \"code\": true } },\n            {\n              \"insert\": \" button at the bottom of your sidebar to add a new page.\"\n            }\n          ]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"checkbox\", \"checkbox\": null },\n          \"delta\": [\n            { \"insert\": \"Click \" },\n            { \"insert\": \"+\", \"attributes\": { \"code\": true } },\n            { \"insert\": \" next to any page title in the sidebar to \" },\n            { \"insert\": \"quickly\", \"attributes\": { \"color\": \"0xff8427e0\" } },\n            { \"insert\": \" add a new subpage, \" },\n            { \"insert\": \"Document\", \"attributes\": { \"code\": true } },\n            { \"insert\": \", \", \"attributes\": { \"code\": false } },\n            { \"insert\": \"Grid\", \"attributes\": { \"code\": true } },\n            { \"insert\": \", or \", \"attributes\": { \"code\": false } },\n            { \"insert\": \"Kanban Board\", \"attributes\": { \"code\": true } },\n            { \"insert\": \".\", \"attributes\": { \"code\": false } }\n          ]\n        },\n        { \"type\": \"text\", \"delta\": [] },\n        { \"type\": \"divider\" },\n        { \"type\": \"text\", \"attributes\": { \"checkbox\": null }, \"delta\": [] },\n        {\n          \"type\": \"text\",\n          \"attributes\": {\n            \"subtype\": \"heading\",\n            \"checkbox\": null,\n            \"heading\": \"h2\"\n          },\n          \"delta\": [{ \"insert\": \"Keyboard shortcuts, markdown, and code block\" }]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": {\n            \"subtype\": \"number-list\",\n            \"number\": 1,\n            \"heading\": null\n          },\n          \"delta\": [\n            { \"insert\": \"Keyboard shortcuts \" },\n            {\n              \"insert\": \"guide\",\n              \"attributes\": {\n                \"href\": \"https://appflowy.gitbook.io/docs/essential-documentation/shortcuts\"\n              }\n            },\n            { \"retain\": 1, \"attributes\": { \"strikethrough\": true } }\n          ]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": {\n            \"subtype\": \"number-list\",\n            \"number\": 2,\n            \"heading\": null\n          },\n          \"delta\": [\n            { \"insert\": \"Markdown \" },\n            {\n              \"insert\": \"reference\",\n              \"attributes\": {\n                \"href\": \"https://appflowy.gitbook.io/docs/essential-documentation/markdown\"\n              }\n            },\n            { \"retain\": 1, \"attributes\": { \"strikethrough\": true } }\n          ]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"number\": 3, \"subtype\": \"number-list\" },\n          \"delta\": [\n            { \"insert\": \"Type \" },\n            { \"insert\": \"/code\", \"attributes\": { \"code\": true } },\n            {\n              \"insert\": \" to insert a code block\",\n              \"attributes\": { \"code\": false }\n            }\n          ]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": {\n            \"subtype\": \"code_block\",\n            \"number\": 3,\n            \"heading\": null,\n            \"number-list\": null,\n            \"theme\": \"vs\",\n            \"language\": \"rust\"\n          },\n          \"delta\": [\n            {\n              \"insert\": \"// This is the main function.\\nfn main() {\\n    // Print text to the console.\\n    println!(\\\"Hello World!\\\");\\n}\"\n            },\n            { \"retain\": 1, \"attributes\": { \"strikethrough\": true } }\n          ]\n        },\n        { \"type\": \"text\", \"attributes\": { \"checkbox\": null }, \"delta\": [] },\n        {\n          \"type\": \"text\",\n          \"attributes\": {\n            \"subtype\": \"heading\",\n            \"checkbox\": null,\n            \"heading\": \"h2\"\n          },\n          \"delta\": [{ \"insert\": \"Have a question❓\" }]\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": \"quote\" },\n          \"delta\": [\n            { \"insert\": \"Click \" },\n            { \"insert\": \"?\", \"attributes\": { \"code\": true } },\n            { \"insert\": \" at the bottom right for help and support.\" }\n          ]\n        },\n        { \"type\": \"text\", \"delta\": [] },\n        {\n          \"type\": \"callout\",\n          \"children\": [\n            { \"type\": \"text\", \"delta\": [] },\n            {\n              \"type\": \"text\",\n              \"attributes\": { \"subtype\": \"heading\", \"heading\": \"h2\" },\n              \"delta\": [{ \"insert\": \"Like AppFlowy? Follow us:\" }]\n            },\n            {\n              \"type\": \"text\",\n              \"attributes\": { \"subtype\": \"bulleted-list\" },\n              \"delta\": [\n                {\n                  \"insert\": \"GitHub\",\n                  \"attributes\": {\n                    \"href\": \"https://github.com/AppFlowy-IO/AppFlowy\"\n                  }\n                }\n              ]\n            },\n            {\n              \"type\": \"text\",\n              \"attributes\": { \"subtype\": \"bulleted-list\" },\n              \"delta\": [\n                {\n                  \"insert\": \"Twitter\",\n                  \"attributes\": { \"href\": \"https://twitter.com/appflowy\" }\n                },\n                { \"insert\": \": @appflowy\" }\n              ]\n            },\n            {\n              \"type\": \"text\",\n              \"attributes\": { \"subtype\": \"bulleted-list\" },\n              \"delta\": [\n                {\n                  \"insert\": \"Newsletter\",\n                  \"attributes\": { \"href\": \"https://blog-appflowy.ghost.io/\" }\n                }\n              ]\n            }\n          ],\n          \"attributes\": { \"emoji\": \"😀\" }\n        },\n        { \"type\": \"text\", \"delta\": [] },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": null, \"heading\": null },\n          \"delta\": []\n        },\n        {\n          \"type\": \"text\",\n          \"attributes\": { \"subtype\": null, \"heading\": null },\n          \"delta\": []\n        }\n      ]\n    }\n  }\n"
  },
  {
    "path": "frontend/appflowy_flutter/assets/test/workspaces/database/v020.afdb",
    "content": "\"{\"\"id\"\":\"\"2_OVWb\"\",\"\"name\"\":\"\"Name\"\",\"\"field_type\"\":0,\"\"visibility\"\":true,\"\"width\"\":150,\"\"type_options\"\":{\"\"0\"\":{\"\"data\"\":\"\"\"\"}},\"\"is_primary\"\":true}\",\"{\"\"id\"\":\"\"xjmOSi\"\",\"\"name\"\":\"\"Type\"\",\"\"field_type\"\":3,\"\"visibility\"\":true,\"\"width\"\":150,\"\"type_options\"\":{\"\"3\"\":{\"\"content\"\":\"\"{\\\"\"options\\\"\":[{\\\"\"id\\\"\":\\\"\"t1WZ\\\"\",\\\"\"name\\\"\":\\\"\"s6\\\"\",\\\"\"color\\\"\":\\\"\"Lime\\\"\"},{\\\"\"id\\\"\":\\\"\"GzNa\\\"\",\\\"\"name\\\"\":\\\"\"s5\\\"\",\\\"\"color\\\"\":\\\"\"Yellow\\\"\"},{\\\"\"id\\\"\":\\\"\"l_8w\\\"\",\\\"\"name\\\"\":\\\"\"s4\\\"\",\\\"\"color\\\"\":\\\"\"Orange\\\"\"},{\\\"\"id\\\"\":\\\"\"TzVT\\\"\",\\\"\"name\\\"\":\\\"\"s3\\\"\",\\\"\"color\\\"\":\\\"\"LightPink\\\"\"},{\\\"\"id\\\"\":\\\"\"b5WF\\\"\",\\\"\"name\\\"\":\\\"\"s2\\\"\",\\\"\"color\\\"\":\\\"\"Pink\\\"\"},{\\\"\"id\\\"\":\\\"\"AcHA\\\"\",\\\"\"name\\\"\":\\\"\"s1\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"}],\\\"\"disable_color\\\"\":false}\"\"}},\"\"is_primary\"\":false}\",\"{\"\"id\"\":\"\"Hpbiwr\"\",\"\"name\"\":\"\"Done\"\",\"\"field_type\"\":5,\"\"visibility\"\":true,\"\"width\"\":150,\"\"type_options\"\":{\"\"5\"\":{\"\"is_selected\"\":false}},\"\"is_primary\"\":false}\",\"{\"\"id\"\":\"\"F7WLnw\"\",\"\"name\"\":\"\"checklist\"\",\"\"field_type\"\":7,\"\"visibility\"\":true,\"\"width\"\":120,\"\"type_options\"\":{\"\"0\"\":{\"\"data\"\":\"\"\"\"},\"\"7\"\":{}},\"\"is_primary\"\":false}\",\"{\"\"id\"\":\"\"KABhMe\"\",\"\"name\"\":\"\"number\"\",\"\"field_type\"\":1,\"\"visibility\"\":true,\"\"width\"\":120,\"\"type_options\"\":{\"\"1\"\":{\"\"format\"\":0,\"\"symbol\"\":\"\"RUB\"\",\"\"scale\"\":0,\"\"name\"\":\"\"Number\"\"},\"\"0\"\":{\"\"scale\"\":0,\"\"data\"\":\"\"\"\",\"\"format\"\":0,\"\"name\"\":\"\"Number\"\",\"\"symbol\"\":\"\"RUB\"\"}},\"\"is_primary\"\":false}\",\"{\"\"id\"\":\"\"lEn6Bv\"\",\"\"name\"\":\"\"date\"\",\"\"field_type\"\":2,\"\"visibility\"\":true,\"\"width\"\":120,\"\"type_options\"\":{\"\"2\"\":{\"\"field_type\"\":2,\"\"time_format\"\":1,\"\"timezone_id\"\":\"\"\"\",\"\"date_format\"\":3},\"\"0\"\":{\"\"field_type\"\":2,\"\"date_format\"\":3,\"\"time_format\"\":1,\"\"data\"\":\"\"\"\",\"\"timezone_id\"\":\"\"\"\"}},\"\"is_primary\"\":false}\",\"{\"\"id\"\":\"\"B8Prnx\"\",\"\"name\"\":\"\"url\"\",\"\"field_type\"\":6,\"\"visibility\"\":true,\"\"width\"\":120,\"\"type_options\"\":{\"\"6\"\":{\"\"content\"\":\"\"\"\",\"\"url\"\":\"\"\"\"},\"\"0\"\":{\"\"content\"\":\"\"\"\",\"\"data\"\":\"\"\"\",\"\"url\"\":\"\"\"\"}},\"\"is_primary\"\":false}\",\"{\"\"id\"\":\"\"MwUow4\"\",\"\"name\"\":\"\"multi-select\"\",\"\"field_type\"\":4,\"\"visibility\"\":true,\"\"width\"\":240,\"\"type_options\"\":{\"\"0\"\":{\"\"content\"\":\"\"{\\\"\"options\\\"\":[],\\\"\"disable_color\\\"\":false}\"\",\"\"data\"\":\"\"\"\"},\"\"4\"\":{\"\"content\"\":\"\"{\\\"\"options\\\"\":[{\\\"\"id\\\"\":\\\"\"__Us\\\"\",\\\"\"name\\\"\":\\\"\"m7\\\"\",\\\"\"color\\\"\":\\\"\"Green\\\"\"},{\\\"\"id\\\"\":\\\"\"n9-g\\\"\",\\\"\"name\\\"\":\\\"\"m6\\\"\",\\\"\"color\\\"\":\\\"\"Lime\\\"\"},{\\\"\"id\\\"\":\\\"\"KFYu\\\"\",\\\"\"name\\\"\":\\\"\"m5\\\"\",\\\"\"color\\\"\":\\\"\"Yellow\\\"\"},{\\\"\"id\\\"\":\\\"\"KftP\\\"\",\\\"\"name\\\"\":\\\"\"m4\\\"\",\\\"\"color\\\"\":\\\"\"Orange\\\"\"},{\\\"\"id\\\"\":\\\"\"5lWo\\\"\",\\\"\"name\\\"\":\\\"\"m3\\\"\",\\\"\"color\\\"\":\\\"\"LightPink\\\"\"},{\\\"\"id\\\"\":\\\"\"Djrz\\\"\",\\\"\"name\\\"\":\\\"\"m2\\\"\",\\\"\"color\\\"\":\\\"\"Pink\\\"\"},{\\\"\"id\\\"\":\\\"\"2uRu\\\"\",\\\"\"name\\\"\":\\\"\"m1\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"}],\\\"\"disable_color\\\"\":false}\"\"}},\"\"is_primary\"\":false}\"\n\"{\"\"field_type\"\":0,\"\"created_at\"\":1686793246,\"\"data\"\":\"\"A\"\",\"\"last_modified\"\":1686793246}\",\"{\"\"last_modified\"\":1686793275,\"\"created_at\"\":1686793261,\"\"data\"\":\"\"AcHA\"\",\"\"field_type\"\":3}\",\"{\"\"created_at\"\":1686793241,\"\"field_type\"\":5,\"\"last_modified\"\":1686793241,\"\"data\"\":\"\"Yes\"\"}\",\"{\"\"data\"\":\"\"{\\\"\"options\\\"\":[{\\\"\"id\\\"\":\\\"\"pi1A\\\"\",\\\"\"name\\\"\":\\\"\"t1\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"},{\\\"\"id\\\"\":\\\"\"6Pym\\\"\",\\\"\"name\\\"\":\\\"\"t2\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"},{\\\"\"id\\\"\":\\\"\"erEe\\\"\",\\\"\"name\\\"\":\\\"\"t3\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"}],\\\"\"selected_option_ids\\\"\":[\\\"\"pi1A\\\"\",\\\"\"6Pym\\\"\"]}\"\",\"\"created_at\"\":1686793302,\"\"field_type\"\":7,\"\"last_modified\"\":1686793308}\",\"{\"\"created_at\"\":1686793333,\"\"field_type\"\":1,\"\"data\"\":\"\"-1\"\",\"\"last_modified\"\":1686793333}\",\"{\"\"last_modified\"\":1686793370,\"\"field_type\"\":2,\"\"data\"\":\"\"1685583770\"\",\"\"include_time\"\":false,\"\"created_at\"\":1686793370}\",\"{\"\"created_at\"\":1686793395,\"\"data\"\":\"\"appflowy.io\"\",\"\"field_type\"\":6,\"\"last_modified\"\":1686793399,\"\"url\"\":\"\"https://appflowy.io\"\"}\",\"{\"\"last_modified\"\":1686793446,\"\"field_type\"\":4,\"\"data\"\":\"\"2uRu\"\",\"\"created_at\"\":1686793428}\"\n\"{\"\"last_modified\"\":1686793247,\"\"data\"\":\"\"B\"\",\"\"field_type\"\":0,\"\"created_at\"\":1686793247}\",\"{\"\"created_at\"\":1686793278,\"\"data\"\":\"\"b5WF\"\",\"\"field_type\"\":3,\"\"last_modified\"\":1686793278}\",\"{\"\"created_at\"\":1686793292,\"\"last_modified\"\":1686793292,\"\"data\"\":\"\"Yes\"\",\"\"field_type\"\":5}\",\"{\"\"data\"\":\"\"{\\\"\"options\\\"\":[{\\\"\"id\\\"\":\\\"\"YHDO\\\"\",\\\"\"name\\\"\":\\\"\"t1\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"},{\\\"\"id\\\"\":\\\"\"QjtW\\\"\",\\\"\"name\\\"\":\\\"\"t2\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"},{\\\"\"id\\\"\":\\\"\"K2nM\\\"\",\\\"\"name\\\"\":\\\"\"t3\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"}],\\\"\"selected_option_ids\\\"\":[\\\"\"YHDO\\\"\"]}\"\",\"\"field_type\"\":7,\"\"last_modified\"\":1686793318,\"\"created_at\"\":1686793311}\",\"{\"\"data\"\":\"\"-2\"\",\"\"last_modified\"\":1686793335,\"\"created_at\"\":1686793335,\"\"field_type\"\":1}\",\"{\"\"field_type\"\":2,\"\"data\"\":\"\"1685670174\"\",\"\"include_time\"\":false,\"\"created_at\"\":1686793374,\"\"last_modified\"\":1686793374}\",\"{\"\"last_modified\"\":1686793403,\"\"field_type\"\":6,\"\"created_at\"\":1686793399,\"\"url\"\":\"\"\"\",\"\"data\"\":\"\"no url\"\"}\",\"{\"\"data\"\":\"\"2uRu,Djrz\"\",\"\"field_type\"\":4,\"\"last_modified\"\":1686793449,\"\"created_at\"\":1686793449}\"\n\"{\"\"data\"\":\"\"C\"\",\"\"created_at\"\":1686793248,\"\"last_modified\"\":1686793248,\"\"field_type\"\":0}\",\"{\"\"created_at\"\":1686793280,\"\"field_type\"\":3,\"\"data\"\":\"\"TzVT\"\",\"\"last_modified\"\":1686793280}\",\"{\"\"data\"\":\"\"Yes\"\",\"\"last_modified\"\":1686793292,\"\"field_type\"\":5,\"\"created_at\"\":1686793292}\",\"{\"\"last_modified\"\":1686793329,\"\"field_type\"\":7,\"\"created_at\"\":1686793322,\"\"data\"\":\"\"{\\\"\"options\\\"\":[{\\\"\"id\\\"\":\\\"\"iWM1\\\"\",\\\"\"name\\\"\":\\\"\"t1\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"},{\\\"\"id\\\"\":\\\"\"WDvF\\\"\",\\\"\"name\\\"\":\\\"\"t2\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"},{\\\"\"id\\\"\":\\\"\"w3k7\\\"\",\\\"\"name\\\"\":\\\"\"t3\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"}],\\\"\"selected_option_ids\\\"\":[\\\"\"iWM1\\\"\",\\\"\"WDvF\\\"\",\\\"\"w3k7\\\"\"]}\"\"}\",\"{\"\"field_type\"\":1,\"\"last_modified\"\":1686793339,\"\"data\"\":\"\"0.1\"\",\"\"created_at\"\":1686793339}\",\"{\"\"last_modified\"\":1686793377,\"\"data\"\":\"\"1685756577\"\",\"\"created_at\"\":1686793377,\"\"include_time\"\":false,\"\"field_type\"\":2}\",\"{\"\"created_at\"\":1686793403,\"\"field_type\"\":6,\"\"data\"\":\"\"appflowy.io\"\",\"\"last_modified\"\":1686793408,\"\"url\"\":\"\"https://appflowy.io\"\"}\",\"{\"\"data\"\":\"\"2uRu,Djrz,5lWo\"\",\"\"created_at\"\":1686793453,\"\"last_modified\"\":1686793454,\"\"field_type\"\":4}\"\n\"{\"\"data\"\":\"\"D\"\",\"\"last_modified\"\":1686793249,\"\"created_at\"\":1686793249,\"\"field_type\"\":0}\",\"{\"\"data\"\":\"\"l_8w\"\",\"\"created_at\"\":1686793284,\"\"last_modified\"\":1686793284,\"\"field_type\"\":3}\",\"{\"\"data\"\":\"\"Yes\"\",\"\"created_at\"\":1686793293,\"\"last_modified\"\":1686793293,\"\"field_type\"\":5}\",,\"{\"\"field_type\"\":1,\"\"last_modified\"\":1686793341,\"\"created_at\"\":1686793341,\"\"data\"\":\"\"0.2\"\"}\",\"{\"\"created_at\"\":1686793379,\"\"last_modified\"\":1686793379,\"\"field_type\"\":2,\"\"data\"\":\"\"1685842979\"\",\"\"include_time\"\":false}\",\"{\"\"last_modified\"\":1686793419,\"\"field_type\"\":6,\"\"created_at\"\":1686793408,\"\"data\"\":\"\"https://github.com/AppFlowy-IO/\"\",\"\"url\"\":\"\"https://github.com/AppFlowy-IO/\"\"}\",\"{\"\"data\"\":\"\"2uRu,Djrz,5lWo\"\",\"\"last_modified\"\":1686793459,\"\"field_type\"\":4,\"\"created_at\"\":1686793459}\"\n\"{\"\"field_type\"\":0,\"\"last_modified\"\":1686793250,\"\"created_at\"\":1686793250,\"\"data\"\":\"\"E\"\"}\",\"{\"\"field_type\"\":3,\"\"last_modified\"\":1686793290,\"\"created_at\"\":1686793290,\"\"data\"\":\"\"GzNa\"\"}\",\"{\"\"last_modified\"\":1686793294,\"\"created_at\"\":1686793294,\"\"data\"\":\"\"Yes\"\",\"\"field_type\"\":5}\",,\"{\"\"created_at\"\":1686793346,\"\"field_type\"\":1,\"\"last_modified\"\":1686793346,\"\"data\"\":\"\"1\"\"}\",\"{\"\"last_modified\"\":1686793383,\"\"data\"\":\"\"1685929383\"\",\"\"field_type\"\":2,\"\"include_time\"\":false,\"\"created_at\"\":1686793383}\",\"{\"\"field_type\"\":6,\"\"url\"\":\"\"\"\",\"\"data\"\":\"\"\"\",\"\"last_modified\"\":1686793421,\"\"created_at\"\":1686793419}\",\"{\"\"field_type\"\":4,\"\"last_modified\"\":1686793465,\"\"data\"\":\"\"2uRu,Djrz,5lWo,KFYu,KftP\"\",\"\"created_at\"\":1686793463}\"\n\"{\"\"field_type\"\":0,\"\"created_at\"\":1686793251,\"\"data\"\":\"\"\"\",\"\"last_modified\"\":1686793289}\",,,,\"{\"\"data\"\":\"\"2\"\",\"\"field_type\"\":1,\"\"created_at\"\":1686793347,\"\"last_modified\"\":1686793347}\",\"{\"\"include_time\"\":false,\"\"data\"\":\"\"1685929385\"\",\"\"last_modified\"\":1686793385,\"\"field_type\"\":2,\"\"created_at\"\":1686793385}\",,\n\"{\"\"created_at\"\":1686793254,\"\"field_type\"\":0,\"\"last_modified\"\":1686793288,\"\"data\"\":\"\"\"\"}\",,,,\"{\"\"created_at\"\":1686793351,\"\"last_modified\"\":1686793351,\"\"data\"\":\"\"10\"\",\"\"field_type\"\":1}\",\"{\"\"include_time\"\":false,\"\"data\"\":\"\"1686879792\"\",\"\"field_type\"\":2,\"\"created_at\"\":1686793392,\"\"last_modified\"\":1686793392}\",,\n,,,,\"{\"\"last_modified\"\":1686793354,\"\"created_at\"\":1686793354,\"\"field_type\"\":1,\"\"data\"\":\"\"11\"\"}\",,,\n,,,,\"{\"\"field_type\"\":1,\"\"last_modified\"\":1686793356,\"\"data\"\":\"\"12\"\",\"\"created_at\"\":1686793356}\",,,\n,,,,,,,\n"
  },
  {
    "path": "frontend/appflowy_flutter/assets/test/workspaces/database/v069.afdb",
    "content": "\"{\"\"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}\"\n\"{\"\"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}\"\n\"{\"\"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\"\"}\"\n\"{\"\"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\"\"}\"\n\"{\"\"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}\"\n\"{\"\"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}\"\n\"{\"\"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\"\"}\"\n\"{\"\"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}\"\n\"{\"\"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\"\"}\"\n\"{\"\"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\"\"}\"\n\"{\"\"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://samplelib.com/lib/preview/png/sample-blue-200x200.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}\"\n\"{\"\"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}\"\n\"{\"\"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\"\"}\"\n\"{\"\"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\"\"}\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/assets/test/workspaces/markdowns/markdown_with_table.md",
    "content": "# AppFlowy Test Markdown import with table\n\n# Table\n\n| S.No. | Column 2 |\n| --- | --- |\n| 1. | row 1 |\n| 2. | row 2 |\n| 3. | row 3 |\n| 4. | row 4 |\n| 5. | row 5 |"
  },
  {
    "path": "frontend/appflowy_flutter/assets/test/workspaces/markdowns/test1.md",
    "content": "# Welcome to AppFlowy!\n\n## Here are the basics\n\n- [ ] Click anywhere and just start typing.\n- [ ] Highlight any text, and use the editing menu to _style_ **your** <u>writing</u> `however` you ~~like.~~\n- [ ] As soon as you type /a menu will pop up. Select different types of content blocks you can add.\n- [ ] Type `/` followed by `/bullet` or `/num` to create a list.\n- [x] Click `+ New Page `button at the bottom of your sidebar to add a new page.\n- [ ] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`.\n\n---\n\n## Keyboard shortcuts, markdown, and code block\n\n1. Keyboard shortcuts [guide](https://appflowy.gitbook.io/docs/essential-documentation/shortcuts)\n1. Markdown [reference](https://appflowy.gitbook.io/docs/essential-documentation/markdown)\n1. Type `/code` to insert a code block\n\n## Have a question❓\n\n> Click `?` at the bottom right for help and support.\n"
  },
  {
    "path": "frontend/appflowy_flutter/assets/test/workspaces/markdowns/test2.md",
    "content": "# test\n"
  },
  {
    "path": "frontend/appflowy_flutter/assets/translations/mr-IN.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"मी\",\n  \"welcomeText\": \"@:appName मध्ये आ पले स्वागत आहे.\",\n  \"welcomeTo\": \"मध्ये आ पले स्वागत आ हे\",\n  \"githubStarText\": \"GitHub वर स्टार करा\",\n  \"subscribeNewsletterText\": \"वृत्तपत्राची सदस्यता घ्या\",\n  \"letsGoButtonText\": \"क्विक स्टार्ट\",\n  \"title\": \"Title\",\n  \"youCanAlso\": \"तुम्ही देखील\",\n  \"and\": \"आ णि\",\n  \"failedToOpenUrl\": \"URL उघडण्यात अयशस्वी: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"खाली जोडण्यासाठी क्लिक करा\",\n    \"addAboveCmd\": \"Alt+click\",\n    \"addAboveMacCmd\": \"Option+click\",\n    \"addAboveTooltip\": \"वर जोडण्यासाठी\",\n    \"dragTooltip\": \"Drag to move\",\n    \"openMenuTooltip\": \"मेनू उघडण्यासाठी क्लिक करा\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"साइन अप\",\n    \"title\": \"साइन अप to @:appName\",\n    \"getStartedText\": \"सुरुवात करा\",\n    \"emptyPasswordError\": \"पासवर्ड रिकामा असू शकत नाही\",\n    \"repeatPasswordEmptyError\": \"Repeat पासवर्ड रिकामा असू शकत नाही\",\n    \"unmatchedPasswordError\": \"पुन्हा लिहिलेला पासवर्ड मूळ पासवर्डशी जुळत नाही\",\n    \"alreadyHaveAnAccount\": \"आधीच खाते आहे?\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Password\",\n    \"repeatPasswordHint\": \"पासवर्ड पुन्हा लिहा\",\n    \"signUpWith\": \"यामध्ये साइन अप करा:\"\n  },\n  \"signIn\": {\n  \"loginTitle\": \"@:appName मध्ये लॉगिन करा\",\n  \"loginButtonText\": \"लॉगिन\",\n  \"loginStartWithAnonymous\": \"अनामिक सत्रासह पुढे जा\",\n  \"continueAnonymousUser\": \"अनामिक सत्रासह पुढे जा\",\n  \"anonymous\": \"अनामिक\",\n  \"buttonText\": \"साइन इन\",\n  \"signingInText\": \"साइन इन होत आहे...\",\n  \"forgotPassword\": \"पासवर्ड विसरलात?\",\n  \"emailHint\": \"ईमेल\",\n  \"passwordHint\": \"पासवर्ड\",\n  \"dontHaveAnAccount\": \"तुमचं खाते नाही?\",\n  \"createAccount\": \"खाते तयार करा\",\n  \"repeatPasswordEmptyError\": \"पुन्हा पासवर्ड रिकामा असू शकत नाही\",\n  \"unmatchedPasswordError\": \"पुन्हा लिहिलेला पासवर्ड मूळ पासवर्डशी जुळत नाही\",\n  \"syncPromptMessage\": \"डेटा सिंक होण्यास थोडा वेळ लागू शकतो. कृपया हे पृष्ठ बंद करू नका\",\n  \"or\": \"किंवा\",\n  \"signInWithGoogle\": \"Google सह पुढे जा\",\n  \"signInWithGithub\": \"GitHub सह पुढे जा\",\n  \"signInWithDiscord\": \"Discord सह पुढे जा\",\n  \"signInWithApple\": \"Apple सह पुढे जा\",\n  \"continueAnotherWay\": \"इतर पर्यायांनी पुढे जा\",\n  \"signUpWithGoogle\": \"Google सह साइन अप करा\",\n  \"signUpWithGithub\": \"GitHub सह साइन अप करा\",\n  \"signUpWithDiscord\": \"Discord सह साइन अप करा\",\n  \"signInWith\": \"यासह पुढे जा:\",\n  \"signInWithEmail\": \"ईमेलसह पुढे जा\",\n  \"signInWithMagicLink\": \"पुढे जा\",\n  \"signUpWithMagicLink\": \"Magic Link सह साइन अप करा\",\n  \"pleaseInputYourEmail\": \"कृपया तुमचा ईमेल पत्ता टाका\",\n  \"settings\": \"सेटिंग्ज\",\n  \"magicLinkSent\": \"Magic Link पाठवण्यात आली आहे!\",\n  \"invalidEmail\": \"कृपया वैध ईमेल पत्ता टाका\",\n  \"alreadyHaveAnAccount\": \"आधीच खाते आहे?\",\n  \"logIn\": \"लॉगिन\",\n  \"generalError\": \"काहीतरी चुकलं. कृपया नंतर प्रयत्न करा\",\n  \"limitRateError\": \"सुरक्षेच्या कारणास्तव, तुम्ही दर ६० सेकंदांतून एकदाच Magic Link मागवू शकता\",\n  \"magicLinkSentDescription\": \"तुमच्या ईमेलवर Magic Link पाठवण्यात आली आहे. लॉगिन पूर्ण करण्यासाठी लिंकवर क्लिक करा. ही लिंक ५ मिनिटांत कालबाह्य होईल.\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"तुमचे workspace निवडा\",\n    \"defaultName\": \"माझे Workspace\",\n    \"create\": \"नवीन workspace तयार करा\",\n    \"new\": \"नवीन workspace\",\n    \"importFromNotion\": \"Notion मधून आयात करा\",\n    \"learnMore\": \"अधिक जाणून घ्या\",\n    \"reset\": \"workspace रीसेट करा\",\n    \"renameWorkspace\": \"workspace चे नाव बदला\",\n    \"workspaceNameCannotBeEmpty\": \"workspace चे नाव रिकामे असू शकत नाही\",\n    \"resetWorkspacePrompt\": \"workspace रीसेट केल्यास त्यातील सर्व पृष्ठे आणि डेटा हटवले जातील. तुम्हाला workspace रीसेट करायचे आहे का? पर्यायी म्हणून तुम्ही सपोर्ट टीमशी संपर्क करू शकता.\",\n    \"hint\": \"workspace\",\n    \"notFoundError\": \"workspace सापडले नाही\",\n    \"failedToLoad\": \"काहीतरी चूक झाली! workspace लोड होण्यात अयशस्वी. कृपया @:appName चे कोणतेही उघडे instance बंद करा आणि पुन्हा प्रयत्न करा.\",\n    \"errorActions\": {\n      \"reportIssue\": \"समस्या नोंदवा\",\n      \"reportIssueOnGithub\": \"Github वर समस्या नोंदवा\",\n      \"exportLogFiles\": \"लॉग फाइल्स निर्यात करा\",\n      \"reachOut\": \"Discord वर संपर्क करा\"\n    },\n    \"menuTitle\": \"कार्यक्षेत्रे\",\n  \"deleteWorkspaceHintText\": \"तुम्हाला हे कार्यक्षेत्र हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही आणि तुम्ही प्रकाशित केलेली कोणतीही पृष्ठे अप्रकाशित होतील.\",\n  \"createSuccess\": \"कार्यक्षेत्र यशस्वीरित्या तयार झाले\",\n  \"createFailed\": \"कार्यक्षेत्र तयार करण्यात अयशस्वी\",\n  \"createLimitExceeded\": \"तुम्ही तुमच्या खात्यासाठी परवानगी दिलेल्या कार्यक्षेत्र मर्यादेपर्यंत पोहोचलात. अधिक कार्यक्षेत्रासाठी GitHub वर विनंती करा.\",\n  \"deleteSuccess\": \"कार्यक्षेत्र यशस्वीरित्या हटवले गेले\",\n  \"deleteFailed\": \"कार्यक्षेत्र हटवण्यात अयशस्वी\",\n  \"openSuccess\": \"कार्यक्षेत्र यशस्वीरित्या उघडले\",\n  \"openFailed\": \"कार्यक्षेत्र उघडण्यात अयशस्वी\",\n  \"renameSuccess\": \"कार्यक्षेत्राचे नाव यशस्वीरित्या बदलले\",\n  \"renameFailed\": \"कार्यक्षेत्राचे नाव बदलण्यात अयशस्वी\",\n  \"updateIconSuccess\": \"कार्यक्षेत्राचे चिन्ह यशस्वीरित्या अद्यतनित केले\",\n  \"updateIconFailed\": \"कार्यक्षेत्राचे चिन्ह अद्यतनित करण्यात अयशस्वी\",\n  \"cannotDeleteTheOnlyWorkspace\": \"फक्त एकच कार्यक्षेत्र असल्यास ते हटवता येत नाही\",\n  \"fetchWorkspacesFailed\": \"कार्यक्षेत्रे मिळवण्यात अयशस्वी\",\n  \"leaveCurrentWorkspace\": \"कार्यक्षेत्र सोडा\",\n  \"leaveCurrentWorkspacePrompt\": \"तुम्हाला हे कार्यक्षेत्र सोडायचे आहे का?\"\n  },\n  \"shareAction\": {\n  \"buttonText\": \"शेअर करा\",\n  \"workInProgress\": \"लवकरच येत आहे\",\n  \"markdown\": \"Markdown\",\n  \"html\": \"HTML\",\n  \"clipboard\": \"क्लिपबोर्डवर कॉपी करा\",\n  \"csv\": \"CSV\",\n  \"copyLink\": \"लिंक कॉपी करा\",\n  \"publishToTheWeb\": \"वेबवर प्रकाशित करा\",\n  \"publishToTheWebHint\": \"AppFlowy सह वेबसाइट तयार करा\",\n  \"publish\": \"प्रकाशित करा\",\n  \"unPublish\": \"अप्रकाशित करा\",\n  \"visitSite\": \"साइटला भेट द्या\",\n  \"exportAsTab\": \"या स्वरूपात निर्यात करा\",\n  \"publishTab\": \"प्रकाशित करा\",\n  \"shareTab\": \"शेअर करा\",\n  \"publishOnAppFlowy\": \"AppFlowy वर प्रकाशित करा\",\n  \"shareTabTitle\": \"सहकार्य करण्यासाठी आमंत्रित करा\",\n  \"shareTabDescription\": \"कोणासही सहज सहकार्य करण्यासाठी\",\n  \"copyLinkSuccess\": \"लिंक क्लिपबोर्डवर कॉपी केली\",\n  \"copyShareLink\": \"शेअर लिंक कॉपी करा\",\n  \"copyLinkFailed\": \"लिंक क्लिपबोर्डवर कॉपी करण्यात अयशस्वी\",\n  \"copyLinkToBlockSuccess\": \"ब्लॉकची लिंक क्लिपबोर्डवर कॉपी केली\",\n  \"copyLinkToBlockFailed\": \"ब्लॉकची लिंक क्लिपबोर्डवर कॉपी करण्यात अयशस्वी\",\n  \"manageAllSites\": \"सर्व साइट्स व्यवस्थापित करा\",\n  \"updatePathName\": \"पथाचे नाव अपडेट करा\"\n  },\n  \"moreAction\": {\n    \"small\": \"लहान\",\n    \"medium\": \"मध्यम\",\n    \"large\": \"मोठा\",\n    \"fontSize\": \"फॉन्ट आकार\",\n    \"import\": \"Import\",\n    \"moreOptions\": \"अधिक पर्याय\",\n    \"wordCount\": \"शब्द संख्या: {}\",\n    \"charCount\": \"अक्षर संख्या: {}\",\n    \"createdAt\": \"निर्मिती: {}\",\n    \"deleteView\": \"हटवा\",\n    \"duplicateView\": \"प्रत बनवा\",\n    \"wordCountLabel\": \"शब्द संख्या: \",\n    \"charCountLabel\": \"अक्षर संख्या: \",\n    \"createdAtLabel\": \"निर्मिती: \",\n    \"syncedAtLabel\": \"सिंक केले: \",\n    \"saveAsNewPage\": \"संदेश पृष्ठात जोडा\",\n    \"saveAsNewPageDisabled\": \"उपलब्ध संदेश नाहीत\"\n  },\n  \"importPanel\": {\n  \"textAndMarkdown\": \"मजकूर आणि Markdown\",\n  \"documentFromV010\": \"v0.1.0 पासून दस्तऐवज\",\n  \"databaseFromV010\": \"v0.1.0 पासून डेटाबेस\",\n  \"notionZip\": \"Notion निर्यात केलेली Zip फाईल\",\n  \"csv\": \"CSV\",\n  \"database\": \"डेटाबेस\"\n  },\n  \"emojiIconPicker\": {\n  \"iconUploader\": {\n    \"placeholderLeft\": \"फाईल ड्रॅग आणि ड्रॉप करा, क्लिक करा \",\n    \"placeholderUpload\": \"अपलोड\",\n    \"placeholderRight\": \", किंवा इमेज लिंक पेस्ट करा.\",\n    \"dropToUpload\": \"अपलोडसाठी फाईल ड्रॉप करा\",\n    \"change\": \"बदला\"\n   }\n  },\n  \"disclosureAction\": {\n  \"rename\": \"नाव बदला\",\n  \"delete\": \"हटवा\",\n  \"duplicate\": \"प्रत बनवा\",\n  \"unfavorite\": \"आवडतीतून काढा\",\n  \"favorite\": \"आवडतीत जोडा\",\n  \"openNewTab\": \"नवीन टॅबमध्ये उघडा\",\n  \"moveTo\": \"या ठिकाणी हलवा\",\n  \"addToFavorites\": \"आवडतीत जोडा\",\n  \"copyLink\": \"लिंक कॉपी करा\",\n  \"changeIcon\": \"आयकॉन बदला\",\n  \"collapseAllPages\": \"सर्व उपपृष्ठे संकुचित करा\",\n  \"movePageTo\": \"पृष्ठ हलवा\",\n  \"move\": \"हलवा\",\n  \"lockPage\": \"पृष्ठ लॉक करा\"\n  },\n  \"blankPageTitle\": \"रिक्त पृष्ठ\",\n  \"newPageText\": \"नवीन पृष्ठ\",\n  \"newDocumentText\": \"नवीन दस्तऐवज\",\n  \"newGridText\": \"नवीन ग्रिड\",\n  \"newCalendarText\": \"नवीन कॅलेंडर\",\n  \"newBoardText\": \"नवीन बोर्ड\",\n  \"chat\": {\n    \"newChat\": \"AI गप्पा\",\n    \"inputMessageHint\": \"@:appName AI ला विचार करा\",\n    \"inputLocalAIMessageHint\": \"@:appName लोकल AI ला विचार करा\",\n    \"unsupportedCloudPrompt\": \"ही सुविधा फक्त @:appName Cloud वापरताना उपलब्ध आहे\",\n    \"relatedQuestion\": \"सूचवलेले\",\n    \"serverUnavailable\": \"कनेक्शन गमावले. कृपया तुमचे इंटरनेट तपासा आणि पुन्हा प्रयत्न करा\",\n    \"aiServerUnavailable\": \"AI सेवा सध्या अनुपलब्ध आहे. कृपया नंतर पुन्हा प्रयत्न करा.\",\n    \"retry\": \"पुन्हा प्रयत्न करा\",\n    \"clickToRetry\": \"पुन्हा प्रयत्न करण्यासाठी क्लिक करा\",\n    \"regenerateAnswer\": \"उत्तर पुन्हा तयार करा\",\n    \"question1\": \"Kanban वापरून कामे कशी व्यवस्थापित करायची\",\n    \"question2\": \"GTD पद्धत समजावून सांगा\",\n    \"question3\": \"Rust का वापरावा\",\n    \"question4\": \"माझ्या स्वयंपाकघरात असलेल्या वस्तूंनी रेसिपी\",\n    \"question5\": \"माझ्या पृष्ठासाठी एक चित्र तयार करा\",\n    \"question6\": \"या आठवड्याची माझी कामांची यादी तयार करा\",\n    \"aiMistakePrompt\": \"AI चुकू शकतो. महत्त्वाची माहिती तपासा.\",\n    \"chatWithFilePrompt\": \"तुम्हाला फाइलसोबत गप्पा मारायच्या आहेत का?\",\n    \"indexFileSuccess\": \"फाईल यशस्वीरित्या अनुक्रमित केली गेली\",\n    \"inputActionNoPages\": \"काहीही पृष्ठे सापडली नाहीत\",\n    \"referenceSource\": {\n    \"zero\": \"0 स्रोत सापडले\",\n    \"one\": \"{count} स्रोत सापडला\",\n    \"other\": \"{count} स्रोत सापडले\"\n    }\n  },\n    \"clickToMention\": \"पृष्ठाचा उल्लेख करा\",\n    \"uploadFile\": \"PDFs, मजकूर किंवा markdown फाइल्स जोडा\",\n    \"questionDetail\": \"नमस्कार {}! मी तुम्हाला कशी मदत करू शकतो?\",\n    \"indexingFile\": \"{} अनुक्रमित करत आहे\",\n    \"generatingResponse\": \"उत्तर तयार होत आहे\",\n    \"selectSources\": \"स्रोत निवडा\",\n    \"currentPage\": \"सध्याचे पृष्ठ\",\n    \"sourcesLimitReached\": \"तुम्ही फक्त ३ मुख्य दस्तऐवज आणि त्यांची उपदस्तऐवज निवडू शकता\",\n    \"sourceUnsupported\": \"सध्या डेटाबेससह चॅटिंगसाठी आम्ही समर्थन करत नाही\",\n    \"regenerate\": \"पुन्हा प्रयत्न करा\",\n    \"addToPageButton\": \"संदेश पृष्ठावर जोडा\",\n    \"addToPageTitle\": \"या पृष्ठात संदेश जोडा...\",\n    \"addToNewPage\": \"नवीन पृष्ठ तयार करा\",\n    \"addToNewPageName\": \"\\\"{}\\\" मधून काढलेले संदेश\",\n    \"addToNewPageSuccessToast\": \"संदेश जोडण्यात आला\",\n    \"openPagePreviewFailedToast\": \"पृष्ठ उघडण्यात अयशस्वी\",\n    \"changeFormat\": {\n      \"actionButton\": \"फॉरमॅट बदला\",\n      \"confirmButton\": \"या फॉरमॅटसह पुन्हा तयार करा\",\n      \"textOnly\": \"मजकूर\",\n      \"imageOnly\": \"फक्त प्रतिमा\",\n      \"textAndImage\": \"मजकूर आणि प्रतिमा\",\n      \"text\": \"परिच्छेद\",\n      \"bullet\": \"बुलेट यादी\",\n      \"number\": \"क्रमांकित यादी\",\n      \"table\": \"सारणी\",\n      \"blankDescription\": \"उत्तराचे फॉरमॅट ठरवा\",\n      \"defaultDescription\": \"स्वयंचलित उत्तर फॉरमॅट\",\n      \"textWithImageDescription\": \"@:chat.changeFormat.text प्रतिमेसह\",\n      \"numberWithImageDescription\": \"@:chat.changeFormat.number प्रतिमेसह\",\n      \"bulletWithImageDescription\": \"@:chat.changeFormat.bullet प्रतिमेसह\",\n  \"   tableWithImageDescription\": \"@:chat.changeFormat.table प्रतिमेसह\"\n  },\n    \"switchModel\": {\n    \"label\": \"मॉडेल बदला\",\n    \"localModel\": \"स्थानिक मॉडेल\",\n    \"cloudModel\": \"क्लाऊड मॉडेल\",\n    \"autoModel\": \"स्वयंचलित\"\n  },\n    \"selectBanner\": {\n    \"saveButton\": \"… मध्ये जोडा\",\n    \"selectMessages\": \"संदेश निवडा\",\n    \"nSelected\": \"{} निवडले गेले\",\n    \"allSelected\": \"सर्व निवडले गेले\"\n  },\n    \"stopTooltip\": \"उत्पन्न करणे थांबवा\",\n    \"trash\": {\n    \"text\": \"कचरा\",\n    \"restoreAll\": \"सर्व पुनर्संचयित करा\",\n    \"restore\": \"पुनर्संचयित करा\",\n    \"deleteAll\": \"सर्व हटवा\",\n    \"pageHeader\": {\n    \"fileName\": \"फाईलचे नाव\",\n    \"lastModified\": \"शेवटचा बदल\",\n    \"created\": \"निर्मिती\"\n    }\n  },\n    \"confirmDeleteAll\": {\n    \"title\": \"कचरापेटीतील सर्व पृष्ठे\",\n    \"caption\": \"तुम्हाला कचरापेटीतील सर्व काही हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही.\"\n  },\n    \"confirmRestoreAll\": {\n    \"title\": \"कचरापेटीतील सर्व पृष्ठे पुनर्संचयित करा\",\n    \"caption\": \"ही कृती पूर्ववत केली जाऊ शकत नाही.\"\n  },\n    \"restorePage\": {\n    \"title\": \"पुनर्संचयित करा: {}\",\n    \"caption\": \"तुम्हाला हे पृष्ठ पुनर्संचयित करायचे आहे का?\"\n  },\n    \"mobile\": {\n    \"actions\": \"कचरा क्रिया\",\n    \"empty\": \"कचरापेटीत कोणतीही पृष्ठे किंवा जागा नाहीत\",\n    \"emptyDescription\": \"जे आवश्यक नाही ते कचरापेटीत हलवा.\",\n    \"isDeleted\": \"हटवले गेले आहे\",\n    \"isRestored\": \"पुनर्संचयित केले गेले आहे\"\n  },\n    \"confirmDeleteTitle\": \"तुम्हाला हे पृष्ठ कायमचे हटवायचे आहे का?\",\n    \"deletePagePrompt\": {\n    \"text\": \"हे पृष्ठ कचरापेटीत आहे\",\n    \"restore\": \"पृष्ठ पुनर्संचयित करा\",\n    \"deletePermanent\": \"कायमचे हटवा\",\n    \"deletePermanentDescription\": \"तुम्हाला हे पृष्ठ कायमचे हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही.\"\n  },\n  \"dialogCreatePageNameHint\": \"पृष्ठाचे नाव\",\n    \"questionBubble\": {\n    \"shortcuts\": \"शॉर्टकट्स\",\n    \"whatsNew\": \"नवीन काय आहे?\",\n    \"help\": \"मदत आणि समर्थन\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n    \"name\": \"डीबग माहिती\",\n    \"success\": \"डीबग माहिती क्लिपबोर्डवर कॉपी केली!\",\n    \"fail\": \"डीबग माहिती कॉपी करता आली नाही\"\n   },\n   \"feedback\": \"अभिप्राय\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"हटवा, नाव बदला आणि अधिक...\",\n    \"addPageTooltip\": \"तत्काळ एक पृष्ठ जोडा\",\n    \"defaultNewPageName\": \"शीर्षक नसलेले\",\n    \"renameDialog\": \"नाव बदला\",\n    \"pageNameSuffix\": \"प्रत\"\n  },\n  \"noPagesInside\": \"अंदर कोणतीही पृष्ठे नाहीत\",\n    \"toolbar\": {\n    \"undo\": \"पूर्ववत करा\",\n    \"redo\": \"पुन्हा करा\",\n    \"bold\": \"ठळक\",\n    \"italic\": \"तिरकस\",\n    \"underline\": \"अधोरेखित\",\n    \"strike\": \"मागे ओढलेले\",\n    \"numList\": \"क्रमांकित यादी\",\n    \"bulletList\": \"बुलेट यादी\",\n    \"checkList\": \"चेक यादी\",\n    \"inlineCode\": \"इनलाइन कोड\",\n    \"quote\": \"उद्धरण ब्लॉक\",\n    \"header\": \"शीर्षक\",\n    \"highlight\": \"हायलाइट\",\n    \"color\": \"रंग\",\n    \"addLink\": \"लिंक जोडा\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"लाइट मोडमध्ये स्विच करा\",\n    \"darkMode\": \"डार्क मोडमध्ये स्विच करा\",\n    \"openAsPage\": \"पृष्ठ म्हणून उघडा\",\n    \"addNewRow\": \"नवीन पंक्ती जोडा\",\n    \"openMenu\": \"मेनू उघडण्यासाठी क्लिक करा\",\n    \"dragRow\": \"पंक्तीचे स्थान बदलण्यासाठी ड्रॅग करा\",\n    \"viewDataBase\": \"डेटाबेस पहा\",\n    \"referencePage\": \"हे {name} संदर्भित आहे\",\n    \"addBlockBelow\": \"खाली एक ब्लॉक जोडा\",\n    \"aiGenerate\": \"निर्मिती करा\"\n  },\n  \"sideBar\": {\n  \"closeSidebar\": \"साइडबार बंद करा\",\n  \"openSidebar\": \"साइडबार उघडा\",\n  \"expandSidebar\": \"पूर्ण पृष्ठावर विस्तारित करा\",\n  \"personal\": \"वैयक्तिक\",\n  \"private\": \"खाजगी\",\n  \"workspace\": \"कार्यक्षेत्र\",\n  \"favorites\": \"आवडती\",\n  \"clickToHidePrivate\": \"खाजगी जागा लपवण्यासाठी क्लिक करा\\nयेथे तयार केलेली पाने फक्त तुम्हाला दिसतील\",\n  \"clickToHideWorkspace\": \"कार्यक्षेत्र लपवण्यासाठी क्लिक करा\\nयेथे तयार केलेली पाने सर्व सदस्यांना दिसतील\",\n  \"clickToHidePersonal\": \"वैयक्तिक जागा लपवण्यासाठी क्लिक करा\",\n  \"clickToHideFavorites\": \"आवडती जागा लपवण्यासाठी क्लिक करा\",\n  \"addAPage\": \"नवीन पृष्ठ जोडा\",\n  \"addAPageToPrivate\": \"खाजगी जागेत पृष्ठ जोडा\",\n  \"addAPageToWorkspace\": \"कार्यक्षेत्रात पृष्ठ जोडा\",\n  \"recent\": \"अलीकडील\",\n  \"today\": \"आज\",\n  \"thisWeek\": \"या आठवड्यात\",\n  \"others\": \"पूर्वीच्या आवडती\",\n  \"earlier\": \"पूर्वीचे\",\n  \"justNow\": \"आत्ताच\",\n  \"minutesAgo\": \"{count} मिनिटांपूर्वी\",\n  \"lastViewed\": \"शेवटी पाहिलेले\",\n  \"favoriteAt\": \"आवडते म्हणून चिन्हांकित\",\n  \"emptyRecent\": \"अलीकडील पृष्ठे नाहीत\",\n  \"emptyRecentDescription\": \"तुम्ही पाहिलेली पृष्ठे येथे सहज पुन्हा मिळवण्यासाठी दिसतील.\",\n  \"emptyFavorite\": \"आवडती पृष्ठे नाहीत\",\n  \"emptyFavoriteDescription\": \"पृष्ठांना आवडते म्हणून चिन्हांकित करा—ते येथे झपाट्याने प्रवेशासाठी दिसतील!\",\n  \"removePageFromRecent\": \"हे पृष्ठ अलीकडील यादीतून काढायचे?\",\n  \"removeSuccess\": \"यशस्वीरित्या काढले गेले\",\n  \"favoriteSpace\": \"आवडती\",\n  \"RecentSpace\": \"अलीकडील\",\n  \"Spaces\": \"जागा\",\n  \"upgradeToPro\": \"Pro मध्ये अपग्रेड करा\",\n  \"upgradeToAIMax\": \"अमर्यादित AI अनलॉक करा\",\n  \"storageLimitDialogTitle\": \"तुमचा मोफत स्टोरेज संपला आहे. अमर्यादित स्टोरेजसाठी अपग्रेड करा\",\n  \"storageLimitDialogTitleIOS\": \"तुमचा मोफत स्टोरेज संपला आहे.\",\n  \"aiResponseLimitTitle\": \"तुमचे मोफत AI प्रतिसाद संपले आहेत. कृपया Pro प्लॅनला अपग्रेड करा किंवा AI add-on खरेदी करा\",\n  \"aiResponseLimitDialogTitle\": \"AI प्रतिसाद मर्यादा गाठली आहे\",\n  \"aiResponseLimit\": \"तुमचे मोफत AI प्रतिसाद संपले आहेत.\\n\\nसेटिंग्ज -> प्लॅन -> AI Max किंवा Pro प्लॅन क्लिक करा\",\n  \"askOwnerToUpgradeToPro\": \"तुमच्या कार्यक्षेत्राचे मोफत स्टोरेज संपत चालले आहे. कृपया कार्यक्षेत्र मालकाला Pro प्लॅनमध्ये अपग्रेड करण्यास सांगा\",\n  \"askOwnerToUpgradeToProIOS\": \"तुमच्या कार्यक्षेत्राचे मोफत स्टोरेज संपत चालले आहे.\",\n  \"askOwnerToUpgradeToAIMax\": \"तुमच्या कार्यक्षेत्राचे मोफत AI प्रतिसाद संपले आहेत. कृपया कार्यक्षेत्र मालकाला प्लॅन अपग्रेड किंवा AI add-ons खरेदी करण्यास सांगा\",\n  \"askOwnerToUpgradeToAIMaxIOS\": \"तुमच्या कार्यक्षेत्राचे मोफत AI प्रतिसाद संपले आहेत.\",\n  \"purchaseAIMax\": \"तुमच्या कार्यक्षेत्राचे AI प्रतिमा प्रतिसाद संपले आहेत. कृपया कार्यक्षेत्र मालकाला AI Max खरेदी करण्यास सांगा\",\n  \"aiImageResponseLimit\": \"तुमचे AI प्रतिमा प्रतिसाद संपले आहेत.\\n\\nसेटिंग्ज -> प्लॅन -> AI Max क्लिक करा\",\n  \"purchaseStorageSpace\": \"स्टोरेज स्पेस खरेदी करा\",\n  \"singleFileProPlanLimitationDescription\": \"तुम्ही मोफत प्लॅनमध्ये परवानगी दिलेल्या फाइल अपलोड आकाराची मर्यादा ओलांडली आहे. कृपया Pro प्लॅनमध्ये अपग्रेड करा\",\n  \"purchaseAIResponse\": \"AI प्रतिसाद खरेदी करा\",\n  \"askOwnerToUpgradeToLocalAI\": \"कार्यक्षेत्र मालकाला ऑन-डिव्हाइस AI सक्षम करण्यास सांगा\",\n  \"upgradeToAILocal\": \"अत्याधिक गोपनीयतेसाठी तुमच्या डिव्हाइसवर लोकल मॉडेल चालवा\",\n  \"upgradeToAILocalDesc\": \"PDFs सोबत गप्पा मारा, तुमचे लेखन सुधारा आणि लोकल AI वापरून टेबल्स आपोआप भरा\"\n},\n  \"notifications\": {\n    \"export\": {\n    \"markdown\": \"टीप Markdown मध्ये निर्यात केली\",\n    \"path\": \"Documents/flowy\"\n    }\n  },\n    \"contactsPage\": {\n    \"title\": \"संपर्क\",\n    \"whatsHappening\": \"या आठवड्यात काय घडत आहे?\",\n    \"addContact\": \"संपर्क जोडा\",\n    \"editContact\": \"संपर्क संपादित करा\"\n  },\n  \"button\": {\n    \"ok\": \"ठीक आहे\",\n    \"confirm\": \"खात्री करा\",\n    \"done\": \"पूर्ण\",\n    \"cancel\": \"रद्द करा\",\n    \"signIn\": \"साइन इन\",\n    \"signOut\": \"साइन आउट\",\n    \"complete\": \"पूर्ण करा\",\n    \"save\": \"जतन करा\",\n    \"generate\": \"निर्माण करा\",\n    \"esc\": \"ESC\",\n    \"keep\": \"ठेवा\",\n    \"tryAgain\": \"पुन्हा प्रयत्न करा\",\n    \"discard\": \"टाका\",\n    \"replace\": \"बदला\",\n    \"insertBelow\": \"खाली घाला\",\n    \"insertAbove\": \"वर घाला\",\n    \"upload\": \"अपलोड करा\",\n    \"edit\": \"संपादित करा\",\n    \"delete\": \"हटवा\",\n    \"copy\": \"कॉपी करा\",\n    \"duplicate\": \"प्रत बनवा\",\n    \"putback\": \"परत ठेवा\",\n    \"update\": \"अद्यतनित करा\",\n    \"share\": \"शेअर करा\",\n    \"removeFromFavorites\": \"आवडतीतून काढा\",\n    \"removeFromRecent\": \"अलीकडील यादीतून काढा\",\n    \"addToFavorites\": \"आवडतीत जोडा\",\n    \"favoriteSuccessfully\": \"आवडतीत यशस्वीरित्या जोडले\",\n    \"unfavoriteSuccessfully\": \"आवडतीतून यशस्वीरित्या काढले\",\n    \"duplicateSuccessfully\": \"प्रत यशस्वीरित्या तयार झाली\",\n    \"rename\": \"नाव बदला\",\n    \"helpCenter\": \"मदत केंद्र\",\n    \"add\": \"जोड़ा\",\n    \"yes\": \"होय\",\n    \"no\": \"नाही\",\n    \"clear\": \"साफ करा\",\n    \"remove\": \"काढा\",\n    \"dontRemove\": \"काढू नका\",\n    \"copyLink\": \"लिंक कॉपी करा\",\n    \"align\": \"जुळवा\",\n    \"login\": \"लॉगिन\",\n    \"logout\": \"लॉगआउट\",\n    \"deleteAccount\": \"खाते हटवा\",\n    \"back\": \"मागे\",\n    \"signInGoogle\": \"Google सह पुढे जा\",\n    \"signInGithub\": \"GitHub सह पुढे जा\",\n    \"signInDiscord\": \"Discord सह पुढे जा\",\n    \"more\": \"अधिक\",\n    \"create\": \"तयार करा\",\n    \"close\": \"बंद करा\",\n    \"next\": \"पुढे\",\n    \"previous\": \"मागील\",\n    \"submit\": \"सबमिट करा\",\n    \"download\": \"डाउनलोड करा\",\n    \"backToHome\": \"मुख्यपृष्ठावर परत जा\",\n    \"viewing\": \"पाहत आहात\",\n    \"editing\": \"संपादन करत आहात\",\n    \"gotIt\": \"समजले\",\n    \"retry\": \"पुन्हा प्रयत्न करा\",\n    \"uploadFailed\": \"अपलोड अयशस्वी.\",\n    \"copyLinkOriginal\": \"मूळ दुव्याची कॉपी करा\"\n  },\n  \"label\": {\n    \"welcome\": \"स्वागत आहे!\",\n    \"firstName\": \"पहिले नाव\",\n    \"middleName\": \"मधले नाव\",\n    \"lastName\": \"आडनाव\",\n    \"stepX\": \"पायरी {X}\"\n  },\n  \"oAuth\": {\n  \"err\": {\n    \"failedTitle\": \"तुमच्या खात्याशी कनेक्ट होता आले नाही.\",\n    \"failedMsg\": \"कृपया खात्री करा की तुम्ही ब्राउझरमध्ये साइन-इन प्रक्रिया पूर्ण केली आहे.\"\n  },\n  \"google\": {\n    \"title\": \"GOOGLE साइन-इन\",\n    \"instruction1\": \"तुमचे Google Contacts आयात करण्यासाठी, तुम्हाला तुमच्या वेब ब्राउझरचा वापर करून या अ‍ॅप्लिकेशनला अधिकृत करणे आवश्यक आहे.\",\n    \"instruction2\": \"ही कोड आयकॉनवर क्लिक करून किंवा मजकूर निवडून क्लिपबोर्डवर कॉपी करा:\",\n    \"instruction3\": \"तुमच्या वेब ब्राउझरमध्ये खालील दुव्यावर जा आणि वरील कोड टाका:\",\n    \"instruction4\": \"साइनअप पूर्ण झाल्यावर खालील बटण क्लिक करा:\"\n    }\n  },\n  \"settings\": {\n  \"title\": \"सेटिंग्ज\",\n  \"popupMenuItem\": {\n    \"settings\": \"सेटिंग्ज\",\n    \"members\": \"सदस्य\",\n    \"trash\": \"कचरा\",\n    \"helpAndSupport\": \"मदत आणि समर्थन\"\n  },\n  \"sites\": {\n      \"title\": \"साइट्स\",\n      \"namespaceTitle\": \"नेमस्पेस\",\n      \"namespaceDescription\": \"तुमचा नेमस्पेस आणि मुख्यपृष्ठ व्यवस्थापित करा\",\n      \"namespaceHeader\": \"नेमस्पेस\",\n      \"homepageHeader\": \"मुख्यपृष्ठ\",\n      \"updateNamespace\": \"नेमस्पेस अद्यतनित करा\",\n      \"removeHomepage\": \"मुख्यपृष्ठ हटवा\",\n      \"selectHomePage\": \"एक पृष्ठ निवडा\",\n      \"clearHomePage\": \"या नेमस्पेससाठी मुख्यपृष्ठ साफ करा\",\n      \"customUrl\": \"स्वतःची URL\",\n      \"namespace\": {\n      \"description\": \"हे बदल सर्व प्रकाशित पृष्ठांवर लागू होतील जे या नेमस्पेसवर चालू आहेत\",\n      \"tooltip\": \"कोणताही अनुचित नेमस्पेस आम्ही काढून टाकण्याचा अधिकार राखून ठेवतो\",\n      \"updateExistingNamespace\": \"विद्यमान नेमस्पेस अद्यतनित करा\",\n      \"upgradeToPro\": \"मुख्यपृष्ठ सेट करण्यासाठी Pro प्लॅनमध्ये अपग्रेड करा\",\n      \"redirectToPayment\": \"पेमेंट पृष्ठावर वळवत आहे...\",\n      \"onlyWorkspaceOwnerCanSetHomePage\": \"फक्त कार्यक्षेत्र मालकच मुख्यपृष्ठ सेट करू शकतो\",\n      \"pleaseAskOwnerToSetHomePage\": \"कृपया कार्यक्षेत्र मालकाला Pro प्लॅनमध्ये अपग्रेड करण्यास सांगा\"\n      },\n      \"publishedPage\": {\n      \"title\": \"सर्व प्रकाशित पृष्ठे\",\n      \"description\": \"तुमची प्रकाशित पृष्ठे व्यवस्थापित करा\",\n      \"page\": \"पृष्ठ\",\n      \"pathName\": \"पथाचे नाव\",\n      \"date\": \"प्रकाशन तारीख\",\n      \"emptyHinText\": \"या कार्यक्षेत्रात तुमच्याकडे कोणतीही प्रकाशित पृष्ठे नाहीत\",\n      \"noPublishedPages\": \"प्रकाशित पृष्ठे नाहीत\",\n      \"settings\": \"प्रकाशन सेटिंग्ज\",\n      \"clickToOpenPageInApp\": \"पृष्ठ अ‍ॅपमध्ये उघडा\",\n      \"clickToOpenPageInBrowser\": \"पृष्ठ ब्राउझरमध्ये उघडा\"\n      }\n    }\n  },\n    \"error\": {\n    \"failedToGeneratePaymentLink\": \"Pro प्लॅनसाठी पेमेंट लिंक तयार करण्यात अयशस्वी\",\n    \"failedToUpdateNamespace\": \"नेमस्पेस अद्यतनित करण्यात अयशस्वी\",\n    \"proPlanLimitation\": \"नेमस्पेस अद्यतनित करण्यासाठी तुम्हाला Pro प्लॅनमध्ये अपग्रेड करणे आवश्यक आहे\",\n    \"namespaceAlreadyInUse\": \"नेमस्पेस आधीच वापरात आहे, कृपया दुसरे प्रयत्न करा\",\n    \"invalidNamespace\": \"अवैध नेमस्पेस, कृपया दुसरे प्रयत्न करा\",\n    \"namespaceLengthAtLeast2Characters\": \"नेमस्पेस किमान २ अक्षरे लांब असावे\",\n    \"onlyWorkspaceOwnerCanUpdateNamespace\": \"फक्त कार्यक्षेत्र मालकच नेमस्पेस अद्यतनित करू शकतो\",\n    \"onlyWorkspaceOwnerCanRemoveHomepage\": \"फक्त कार्यक्षेत्र मालकच मुख्यपृष्ठ हटवू शकतो\",\n    \"setHomepageFailed\": \"मुख्यपृष्ठ सेट करण्यात अयशस्वी\",\n    \"namespaceTooLong\": \"नेमस्पेस खूप लांब आहे, कृपया दुसरे प्रयत्न करा\",\n    \"namespaceTooShort\": \"नेमस्पेस खूप लहान आहे, कृपया दुसरे प्रयत्न करा\",\n    \"namespaceIsReserved\": \"हा नेमस्पेस राखीव आहे, कृपया दुसरे प्रयत्न करा\",\n    \"updatePathNameFailed\": \"पथाचे नाव अद्यतनित करण्यात अयशस्वी\",\n    \"removeHomePageFailed\": \"मुख्यपृष्ठ हटवण्यात अयशस्वी\",\n    \"publishNameContainsInvalidCharacters\": \"पथाच्या नावामध्ये अवैध अक्षरे आहेत, कृपया दुसरे प्रयत्न करा\",\n    \"publishNameTooShort\": \"पथाचे नाव खूप लहान आहे, कृपया दुसरे प्रयत्न करा\",\n    \"publishNameTooLong\": \"पथाचे नाव खूप लांब आहे, कृपया दुसरे प्रयत्न करा\",\n    \"publishNameAlreadyInUse\": \"हे पथाचे नाव आधीच वापरले गेले आहे, कृपया दुसरे प्रयत्न करा\",\n    \"namespaceContainsInvalidCharacters\": \"नेमस्पेसमध्ये अवैध अक्षरे आहेत, कृपया दुसरे प्रयत्न करा\",\n    \"publishPermissionDenied\": \"फक्त कार्यक्षेत्र मालक किंवा पृष्ठ प्रकाशकच प्रकाशन सेटिंग्ज व्यवस्थापित करू शकतो\",\n    \"publishNameCannotBeEmpty\": \"पथाचे नाव रिकामे असू शकत नाही, कृपया दुसरे प्रयत्न करा\"\n  },\n      \"success\": {\n    \"namespaceUpdated\": \"नेमस्पेस यशस्वीरित्या अद्यतनित केला\",\n    \"setHomepageSuccess\": \"मुख्यपृष्ठ यशस्वीरित्या सेट केले\",\n    \"updatePathNameSuccess\": \"पथाचे नाव यशस्वीरित्या अद्यतनित केले\",\n    \"removeHomePageSuccess\": \"मुख्यपृष्ठ यशस्वीरित्या हटवले\"\n  },\n    \"accountPage\": {\n    \"menuLabel\": \"खाते आणि अ‍ॅप\",\n    \"title\": \"माझे खाते\",\n    \"general\": {\n    \"title\": \"खात्याचे नाव आणि प्रोफाइल प्रतिमा\",\n    \"changeProfilePicture\": \"प्रोफाइल प्रतिमा बदला\"\n  },\n  \"email\": {\n    \"title\": \"ईमेल\",\n    \"actions\": {\n      \"change\": \"ईमेल बदला\"\n    }\n  },\n  \"login\": {\n    \"title\": \"खाते लॉगिन\",\n    \"loginLabel\": \"लॉगिन\",\n    \"logoutLabel\": \"लॉगआउट\"\n  },\n  \"isUpToDate\": \"@:appName अद्ययावत आहे!\",\n  \"officialVersion\": \"आवृत्ती {version} (अधिकृत बिल्ड)\"\n},\n    \"workspacePage\": {\n  \"menuLabel\": \"कार्यक्षेत्र\",\n  \"title\": \"कार्यक्षेत्र\",\n  \"description\": \"तुमचे कार्यक्षेत्र स्वरूप, थीम, फॉन्ट, मजकूर रचना, दिनांक/वेळ फॉरमॅट आणि भाषा सानुकूलित करा.\",\n  \"workspaceName\": {\n    \"title\": \"कार्यक्षेत्राचे नाव\"\n  },\n  \"workspaceIcon\": {\n    \"title\": \"कार्यक्षेत्राचे चिन्ह\",\n    \"description\": \"तुमच्या कार्यक्षेत्रासाठी प्रतिमा अपलोड करा किंवा इमोजी वापरा. हे चिन्ह साइडबार आणि सूचना मध्ये दिसेल.\"\n  },\n  \"appearance\": {\n    \"title\": \"दृश्यरूप\",\n    \"description\": \"कार्यक्षेत्राचे दृश्यरूप, थीम, फॉन्ट, मजकूर रचना, दिनांक, वेळ आणि भाषा सानुकूलित करा.\",\n    \"options\": {\n      \"system\": \"स्वयंचलित\",\n      \"light\": \"लाइट\",\n      \"dark\": \"डार्क\"\n      }\n    }\n  },\n  \"resetCursorColor\": {\n  \"title\": \"दस्तऐवज कर्सरचा रंग रीसेट करा\",\n  \"description\": \"तुम्हाला कर्सरचा रंग रीसेट करायचा आहे का?\"\n  },\n  \"resetSelectionColor\": {\n  \"title\": \"दस्तऐवज निवडीचा रंग रीसेट करा\",\n  \"description\": \"तुम्हाला निवडीचा रंग रीसेट करायचा आहे का?\"\n  },\n  \"resetWidth\": {\n  \"resetSuccess\": \"दस्तऐवजाची रुंदी यशस्वीरित्या रीसेट केली\"\n  },\n    \"theme\": {\n    \"title\": \"थीम\",\n    \"description\": \"पूर्व-निर्धारित थीम निवडा किंवा तुमची स्वतःची थीम अपलोड करा.\",\n    \"uploadCustomThemeTooltip\": \"स्वतःची थीम अपलोड करा\"\n  },\n    \"workspaceFont\": {\n    \"title\": \"कार्यक्षेत्र फॉन्ट\",\n    \"noFontHint\": \"कोणताही फॉन्ट सापडला नाही, कृपया दुसरा शब्द वापरून पहा.\"\n  },\n      \"textDirection\": {\n     \"title\": \"मजकूर दिशा\",\n    \"leftToRight\": \"डावीकडून उजवीकडे\",\n    \"rightToLeft\": \"उजवीकडून डावीकडे\",\n    \"auto\": \"स्वयंचलित\",\n    \"enableRTLItems\": \"RTL टूलबार घटक सक्षम करा\"\n  },\n      \"layoutDirection\": {\n    \"title\": \"लेआउट दिशा\",\n    \"leftToRight\": \"डावीकडून उजवीकडे\",\n    \"rightToLeft\": \"उजवीकडून डावीकडे\"\n  },\n      \"dateTime\": {\n    \"title\": \"दिनांक आणि वेळ\",\n    \"example\": \"{} वाजता {} ({})\",\n    \"24HourTime\": \"२४-तास वेळ\",\n    \"dateFormat\": {\n    \"label\": \"दिनांक फॉरमॅट\",\n    \"local\": \"स्थानिक\",\n    \"us\": \"US\",\n    \"iso\": \"ISO\",\n    \"friendly\": \"सुलभ\",\n    \"dmy\": \"D/M/Y\"\n    }\n  },\n    \"language\": {\n    \"title\": \"भाषा\"\n  },\n    \"deleteWorkspacePrompt\": {\n    \"title\": \"कार्यक्षेत्र हटवा\",\n    \"content\": \"तुम्हाला हे कार्यक्षेत्र हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही, आणि तुम्ही प्रकाशित केलेली कोणतीही पृष्ठे अप्रकाशित होतील.\"\n  },\n    \"leaveWorkspacePrompt\": {\n    \"title\": \"कार्यक्षेत्र सोडा\",\n    \"content\": \"तुम्हाला हे कार्यक्षेत्र सोडायचे आहे का? यानंतर तुम्हाला यामधील सर्व पृष्ठे आणि डेटावर प्रवेश मिळणार नाही.\",\n    \"success\": \"तुम्ही कार्यक्षेत्र यशस्वीरित्या सोडले.\",\n    \"fail\": \"कार्यक्षेत्र सोडण्यात अयशस्वी.\"\n  },\n      \"manageWorkspace\": {\n    \"title\": \"कार्यक्षेत्र व्यवस्थापित करा\",\n    \"leaveWorkspace\": \"कार्यक्षेत्र सोडा\",\n    \"deleteWorkspace\": \"कार्यक्षेत्र हटवा\"\n  },\n    \"manageDataPage\": {\n  \"menuLabel\": \"डेटा व्यवस्थापित करा\",\n  \"title\": \"डेटा व्यवस्थापन\",\n  \"description\": \"@:appName मध्ये स्थानिक डेटा संचयन व्यवस्थापित करा किंवा तुमचा विद्यमान डेटा आयात करा.\",\n  \"dataStorage\": {\n    \"title\": \"फाइल संचयन स्थान\",\n    \"tooltip\": \"जिथे तुमच्या फाइल्स संग्रहित आहेत ते स्थान\",\n    \"actions\": {\n      \"change\": \"मार्ग बदला\",\n      \"open\": \"फोल्डर उघडा\",\n      \"openTooltip\": \"सध्याच्या डेटा फोल्डरचे स्थान उघडा\",\n      \"copy\": \"मार्ग कॉपी करा\",\n      \"copiedHint\": \"मार्ग कॉपी केला!\",\n      \"resetTooltip\": \"मूलभूत स्थानावर रीसेट करा\"\n    },\n    \"resetDialog\": {\n      \"title\": \"तुम्हाला खात्री आहे का?\",\n      \"description\": \"पथ मूलभूत डेटा स्थानावर रीसेट केल्याने तुमचा डेटा हटवला जाणार नाही. तुम्हाला सध्याचा डेटा पुन्हा आयात करायचा असल्यास, कृपया आधी त्याचा पथ कॉपी करा.\"\n    }\n  },\n  \"importData\": {\n    \"title\": \"डेटा आयात करा\",\n    \"tooltip\": \"@:appName बॅकअप/डेटा फोल्डरमधून डेटा आयात करा\",\n    \"description\": \"बाह्य @:appName डेटा फोल्डरमधून डेटा कॉपी करा\",\n    \"action\": \"फाइल निवडा\"\n  },\n  \"encryption\": {\n    \"title\": \"एनक्रिप्शन\",\n    \"tooltip\": \"तुमचा डेटा कसा संग्रहित आणि एनक्रिप्ट केला जातो ते व्यवस्थापित करा\",\n    \"descriptionNoEncryption\": \"एनक्रिप्शन चालू केल्याने सर्व डेटा एनक्रिप्ट केला जाईल. ही क्रिया पूर्ववत केली जाऊ शकत नाही.\",\n    \"descriptionEncrypted\": \"तुमचा डेटा एनक्रिप्टेड आहे.\",\n    \"action\": \"डेटा एनक्रिप्ट करा\",\n    \"dialog\": {\n      \"title\": \"संपूर्ण डेटा एनक्रिप्ट करायचा?\",\n      \"description\": \"तुमचा संपूर्ण डेटा एनक्रिप्ट केल्याने तो सुरक्षित राहील. ही क्रिया पूर्ववत केली जाऊ शकत नाही. तुम्हाला पुढे जायचे आहे का?\"\n    }\n  },\n  \"cache\": {\n    \"title\": \"कॅशे साफ करा\",\n    \"description\": \"प्रतिमा न दिसणे, पृष्ठे हरवणे, फॉन्ट लोड न होणे अशा समस्यांचे निराकरण करण्यासाठी मदत करा. याचा तुमच्या डेटावर परिणाम होणार नाही.\",\n    \"dialog\": {\n      \"title\": \"कॅशे साफ करा\",\n      \"description\": \"प्रतिमा न दिसणे, पृष्ठे हरवणे, फॉन्ट लोड न होणे अशा समस्यांचे निराकरण करण्यासाठी मदत करा. याचा तुमच्या डेटावर परिणाम होणार नाही.\",\n      \"successHint\": \"कॅशे साफ झाली!\"\n    }\n  },\n  \"data\": {\n    \"fixYourData\": \"तुमचा डेटा सुधारा\",\n    \"fixButton\": \"सुधारा\",\n    \"fixYourDataDescription\": \"तुमच्या डेटामध्ये काही अडचण येत असल्यास, तुम्ही येथे ती सुधारू शकता.\"\n    }\n  },\n    \"shortcutsPage\": {\n  \"menuLabel\": \"शॉर्टकट्स\",\n  \"title\": \"शॉर्टकट्स\",\n  \"editBindingHint\": \"नवीन बाइंडिंग टाका\",\n  \"searchHint\": \"शोधा\",\n  \"actions\": {\n    \"resetDefault\": \"मूलभूत रीसेट करा\"\n  },\n  \"errorPage\": {\n    \"message\": \"शॉर्टकट्स लोड करण्यात अयशस्वी: {}\",\n    \"howToFix\": \"कृपया पुन्हा प्रयत्न करा. समस्या कायम राहिल्यास GitHub वर संपर्क साधा.\"\n  },\n  \"resetDialog\": {\n    \"title\": \"शॉर्टकट्स रीसेट करा\",\n    \"description\": \"हे सर्व कीबाइंडिंग्जना मूळ स्थितीत रीसेट करेल. ही क्रिया पूर्ववत करता येणार नाही. तुम्हाला खात्री आहे का?\",\n    \"buttonLabel\": \"रीसेट करा\"\n  },\n  \"conflictDialog\": {\n    \"title\": \"{} आधीच वापरले जात आहे\",\n    \"descriptionPrefix\": \"हे कीबाइंडिंग सध्या \",\n    \"descriptionSuffix\": \" यामध्ये वापरले जात आहे. तुम्ही हे कीबाइंडिंग बदलल्यास, ते {} मधून काढले जाईल.\",\n    \"confirmLabel\": \"पुढे जा\"\n  },\n  \"editTooltip\": \"कीबाइंडिंग संपादित करण्यासाठी क्लिक करा\",\n  \"keybindings\": {\n    \"toggleToDoList\": \"टू-डू सूची चालू/बंद करा\",\n    \"insertNewParagraphInCodeblock\": \"नवीन परिच्छेद टाका\",\n    \"pasteInCodeblock\": \"कोडब्लॉकमध्ये पेस्ट करा\",\n    \"selectAllCodeblock\": \"सर्व निवडा\",\n    \"indentLineCodeblock\": \"ओळीच्या सुरुवातीला दोन स्पेस टाका\",\n    \"outdentLineCodeblock\": \"ओळीच्या सुरुवातीची दोन स्पेस काढा\",\n    \"twoSpacesCursorCodeblock\": \"कर्सरवर दोन स्पेस टाका\",\n    \"copy\": \"निवड कॉपी करा\",\n    \"paste\": \"मजकुरात पेस्ट करा\",\n    \"cut\": \"निवड कट करा\",\n    \"alignLeft\": \"मजकूर डावीकडे संरेखित करा\",\n    \"alignCenter\": \"मजकूर मधोमध संरेखित करा\",\n    \"alignRight\": \"मजकूर उजवीकडे संरेखित करा\",\n    \"insertInlineMathEquation\": \"इनलाइन गणितीय सूत्र टाका\",\n    \"undo\": \"पूर्ववत करा\",\n    \"redo\": \"पुन्हा करा\",\n    \"convertToParagraph\": \"ब्लॉक परिच्छेदात रूपांतरित करा\",\n    \"backspace\": \"हटवा\",\n    \"deleteLeftWord\": \"डावीकडील शब्द हटवा\",\n    \"deleteLeftSentence\": \"डावीकडील वाक्य हटवा\",\n    \"delete\": \"उजवीकडील अक्षर हटवा\",\n    \"deleteMacOS\": \"डावीकडील अक्षर हटवा\",\n    \"deleteRightWord\": \"उजवीकडील शब्द हटवा\",\n    \"moveCursorLeft\": \"कर्सर डावीकडे हलवा\",\n    \"moveCursorBeginning\": \"कर्सर सुरुवातीला हलवा\",\n    \"moveCursorLeftWord\": \"कर्सर एक शब्द डावीकडे हलवा\",\n    \"moveCursorLeftSelect\": \"निवडा आणि कर्सर डावीकडे हलवा\",\n    \"moveCursorBeginSelect\": \"निवडा आणि कर्सर सुरुवातीला हलवा\",\n    \"moveCursorLeftWordSelect\": \"निवडा आणि कर्सर एक शब्द डावीकडे हलवा\",\n    \"moveCursorRight\": \"कर्सर उजवीकडे हलवा\",\n    \"moveCursorEnd\": \"कर्सर शेवटी हलवा\",\n    \"moveCursorRightWord\": \"कर्सर एक शब्द उजवीकडे हलवा\",\n    \"moveCursorRightSelect\": \"निवडा आणि कर्सर उजवीकडे हलवा\",\n    \"moveCursorEndSelect\": \"निवडा आणि कर्सर शेवटी हलवा\",\n    \"moveCursorRightWordSelect\": \"निवडा आणि कर्सर एक शब्द उजवीकडे हलवा\",\n    \"moveCursorUp\": \"कर्सर वर हलवा\",\n    \"moveCursorTopSelect\": \"निवडा आणि कर्सर वर हलवा\",\n    \"moveCursorTop\": \"कर्सर वर हलवा\",\n    \"moveCursorUpSelect\": \"निवडा आणि कर्सर वर हलवा\",\n    \"moveCursorBottomSelect\": \"निवडा आणि कर्सर खाली हलवा\",\n    \"moveCursorBottom\": \"कर्सर खाली हलवा\",\n    \"moveCursorDown\": \"कर्सर खाली हलवा\",\n    \"moveCursorDownSelect\": \"निवडा आणि कर्सर खाली हलवा\",\n    \"home\": \"वर स्क्रोल करा\",\n    \"end\": \"खाली स्क्रोल करा\",\n    \"toggleBold\": \"बोल्ड चालू/बंद करा\",\n    \"toggleItalic\": \"इटालिक चालू/बंद करा\",\n    \"toggleUnderline\": \"अधोरेखित चालू/बंद करा\",\n    \"toggleStrikethrough\": \"स्ट्राईकथ्रू चालू/बंद करा\",\n    \"toggleCode\": \"इनलाइन कोड चालू/बंद करा\",\n    \"toggleHighlight\": \"हायलाईट चालू/बंद करा\",\n    \"showLinkMenu\": \"लिंक मेनू दाखवा\",\n    \"openInlineLink\": \"इनलाइन लिंक उघडा\",\n    \"openLinks\": \"सर्व निवडलेले लिंक उघडा\",\n    \"indent\": \"इंडेंट\",\n    \"outdent\": \"आउटडेंट\",\n    \"exit\": \"संपादनातून बाहेर पडा\",\n    \"pageUp\": \"एक पृष्ठ वर स्क्रोल करा\",\n    \"pageDown\": \"एक पृष्ठ खाली स्क्रोल करा\",\n    \"selectAll\": \"सर्व निवडा\",\n    \"pasteWithoutFormatting\": \"फॉरमॅटिंगशिवाय पेस्ट करा\",\n    \"showEmojiPicker\": \"इमोजी निवडकर्ता दाखवा\",\n    \"enterInTableCell\": \"टेबलमध्ये नवीन ओळ जोडा\",\n    \"leftInTableCell\": \"टेबलमध्ये डावीकडील सेलमध्ये जा\",\n    \"rightInTableCell\": \"टेबलमध्ये उजवीकडील सेलमध्ये जा\",\n    \"upInTableCell\": \"टेबलमध्ये वरील सेलमध्ये जा\",\n    \"downInTableCell\": \"टेबलमध्ये खालील सेलमध्ये जा\",\n    \"tabInTableCell\": \"टेबलमध्ये पुढील सेलमध्ये जा\",\n    \"shiftTabInTableCell\": \"टेबलमध्ये मागील सेलमध्ये जा\",\n    \"backSpaceInTableCell\": \"सेलच्या सुरुवातीला थांबा\"\n  },\n  \"commands\": {\n    \"codeBlockNewParagraph\": \"कोडब्लॉकच्या शेजारी नवीन परिच्छेद टाका\",\n    \"codeBlockIndentLines\": \"कोडब्लॉकमध्ये ओळीच्या सुरुवातीला दोन स्पेस टाका\",\n    \"codeBlockOutdentLines\": \"कोडब्लॉकमध्ये ओळीच्या सुरुवातीची दोन स्पेस काढा\",\n    \"codeBlockAddTwoSpaces\": \"कोडब्लॉकमध्ये कर्सरच्या ठिकाणी दोन स्पेस टाका\",\n    \"codeBlockSelectAll\": \"कोडब्लॉकमधील सर्व मजकूर निवडा\",\n    \"codeBlockPasteText\": \"कोडब्लॉकमध्ये मजकूर पेस्ट करा\",\n    \"textAlignLeft\": \"मजकूर डावीकडे संरेखित करा\",\n    \"textAlignCenter\": \"मजकूर मधोमध संरेखित करा\",\n    \"textAlignRight\": \"मजकूर उजवीकडे संरेखित करा\"\n  },\n  \"couldNotLoadErrorMsg\": \"शॉर्टकट लोड करता आले नाहीत. पुन्हा प्रयत्न करा\",\n  \"couldNotSaveErrorMsg\": \"शॉर्टकट सेव्ह करता आले नाहीत. पुन्हा प्रयत्न करा\"\n},\n    \"aiPage\": {\n  \"title\": \"AI सेटिंग्ज\",\n  \"menuLabel\": \"AI सेटिंग्ज\",\n  \"keys\": {\n    \"enableAISearchTitle\": \"AI शोध\",\n    \"aiSettingsDescription\": \"AppFlowy AI चालवण्यासाठी तुमचा पसंतीचा मॉडेल निवडा. आता GPT-4o, GPT-o3-mini, DeepSeek R1, Claude 3.5 Sonnet आणि Ollama मधील मॉडेल्सचा समावेश आहे.\",\n    \"loginToEnableAIFeature\": \"AI वैशिष्ट्ये फक्त @:appName Cloud मध्ये लॉगिन केल्यानंतरच सक्षम होतात. तुमच्याकडे @:appName खाते नसेल तर 'माय अकाउंट' मध्ये जाऊन साइन अप करा.\",\n    \"llmModel\": \"भाषा मॉडेल\",\n    \"llmModelType\": \"भाषा मॉडेल प्रकार\",\n    \"downloadLLMPrompt\": \"{} डाउनलोड करा\",\n    \"downloadAppFlowyOfflineAI\": \"AI ऑफलाइन पॅकेज डाउनलोड केल्याने तुमच्या डिव्हाइसवर AI चालवता येईल. तुम्हाला पुढे जायचे आहे का?\",\n    \"downloadLLMPromptDetail\": \"{} स्थानिक मॉडेल डाउनलोड करण्यासाठी सुमारे {} संचयन लागेल. तुम्हाला पुढे जायचे आहे का?\",\n    \"downloadBigFilePrompt\": \"डाउनलोड पूर्ण होण्यासाठी सुमारे १० मिनिटे लागू शकतात\",\n    \"downloadAIModelButton\": \"डाउनलोड करा\",\n    \"downloadingModel\": \"डाउनलोड करत आहे\",\n    \"localAILoaded\": \"स्थानिक AI मॉडेल यशस्वीरित्या जोडले गेले आणि वापरण्यास सज्ज आहे\",\n    \"localAIStart\": \"स्थानिक AI सुरू होत आहे. जर हळू वाटत असेल, तर ते बंद करून पुन्हा सुरू करून पहा\",\n    \"localAILoading\": \"स्थानिक AI Chat मॉडेल लोड होत आहे...\",\n    \"localAIStopped\": \"स्थानिक AI थांबले आहे\",\n    \"localAIRunning\": \"स्थानिक AI चालू आहे\",\n    \"localAINotReadyRetryLater\": \"स्थानिक AI सुरू होत आहे, कृपया नंतर पुन्हा प्रयत्न करा\",\n    \"localAIDisabled\": \"तुम्ही स्थानिक AI वापरत आहात, पण ते अकार्यक्षम आहे. कृपया सेटिंग्जमध्ये जाऊन ते सक्षम करा किंवा दुसरे मॉडेल वापरून पहा\",\n    \"localAIInitializing\": \"स्थानिक AI लोड होत आहे. तुमच्या डिव्हाइसवर अवलंबून याला काही मिनिटे लागू शकतात\",\n    \"localAINotReadyTextFieldPrompt\": \"स्थानिक AI लोड होत असताना संपादन करता येणार नाही\",\n    \"failToLoadLocalAI\": \"स्थानिक AI सुरू करण्यात अयशस्वी.\",\n    \"restartLocalAI\": \"पुन्हा सुरू करा\",\n    \"disableLocalAITitle\": \"स्थानिक AI अकार्यक्षम करा\",\n    \"disableLocalAIDescription\": \"तुम्हाला स्थानिक AI अकार्यक्षम करायचा आहे का?\",\n    \"localAIToggleTitle\": \"AppFlowy स्थानिक AI (LAI)\",\n    \"localAIToggleSubTitle\": \"AppFlowy मध्ये अत्याधुनिक स्थानिक AI मॉडेल्स वापरून गोपनीयता आणि सुरक्षा सुनिश्चित करा\",\n    \"offlineAIInstruction1\": \"हे अनुसरा\",\n    \"offlineAIInstruction2\": \"सूचना\",\n    \"offlineAIInstruction3\": \"ऑफलाइन AI सक्षम करण्यासाठी.\",\n    \"offlineAIDownload1\": \"जर तुम्ही AppFlowy AI डाउनलोड केले नसेल, तर कृपया\",\n    \"offlineAIDownload2\": \"डाउनलोड\",\n    \"offlineAIDownload3\": \"करा\",\n    \"activeOfflineAI\": \"सक्रिय\",\n    \"downloadOfflineAI\": \"डाउनलोड करा\",\n    \"openModelDirectory\": \"फोल्डर उघडा\",\n    \"laiNotReady\": \"स्थानिक AI अ‍ॅप योग्य प्रकारे इन्स्टॉल झालेले नाही.\",\n    \"ollamaNotReady\": \"Ollama सर्व्हर तयार नाही.\",\n    \"pleaseFollowThese\": \"कृपया हे अनुसरा\",\n    \"instructions\": \"सूचना\",\n    \"installOllamaLai\": \"Ollama आणि AppFlowy स्थानिक AI सेटअप करण्यासाठी.\",\n    \"modelsMissing\": \"आवश्यक मॉडेल्स सापडत नाहीत.\",\n    \"downloadModel\": \"त्यांना डाउनलोड करण्यासाठी.\"\n  }\n},\n    \"planPage\": {\n  \"menuLabel\": \"योजना\",\n  \"title\": \"दर योजना\",\n  \"planUsage\": {\n    \"title\": \"योजनेचा वापर सारांश\",\n    \"storageLabel\": \"स्टोरेज\",\n    \"storageUsage\": \"{} पैकी {} GB\",\n    \"unlimitedStorageLabel\": \"अमर्यादित स्टोरेज\",\n    \"collaboratorsLabel\": \"सदस्य\",\n    \"collaboratorsUsage\": \"{} पैकी {}\",\n    \"aiResponseLabel\": \"AI प्रतिसाद\",\n    \"aiResponseUsage\": \"{} पैकी {}\",\n    \"unlimitedAILabel\": \"अमर्यादित AI प्रतिसाद\",\n    \"proBadge\": \"प्रो\",\n    \"aiMaxBadge\": \"AI Max\",\n    \"aiOnDeviceBadge\": \"मॅकसाठी ऑन-डिव्हाइस AI\",\n    \"memberProToggle\": \"अधिक सदस्य आणि अमर्यादित AI\",\n    \"aiMaxToggle\": \"अमर्यादित AI आणि प्रगत मॉडेल्सचा प्रवेश\",\n    \"aiOnDeviceToggle\": \"जास्त गोपनीयतेसाठी स्थानिक AI\",\n    \"aiCredit\": {\n      \"title\": \"@:appName AI क्रेडिट जोडा\",\n      \"price\": \"{}\",\n      \"priceDescription\": \"1,000 क्रेडिट्ससाठी\",\n      \"purchase\": \"AI खरेदी करा\",\n      \"info\": \"प्रत्येक कार्यक्षेत्रासाठी 1,000 AI क्रेडिट्स जोडा आणि आपल्या कामाच्या प्रवाहात सानुकूलनीय AI सहजपणे एकत्र करा — अधिक स्मार्ट आणि जलद निकालांसाठी:\",\n      \"infoItemOne\": \"प्रत्येक डेटाबेससाठी 10,000 प्रतिसाद\",\n      \"infoItemTwo\": \"प्रत्येक कार्यक्षेत्रासाठी 1,000 प्रतिसाद\"\n    },\n    \"currentPlan\": {\n      \"bannerLabel\": \"सद्य योजना\",\n      \"freeTitle\": \"फ्री\",\n      \"proTitle\": \"प्रो\",\n      \"teamTitle\": \"टीम\",\n      \"freeInfo\": \"2 सदस्यांपर्यंत वैयक्तिक वापरासाठी उत्तम\",\n      \"proInfo\": \"10 सदस्यांपर्यंत लहान आणि मध्यम टीमसाठी योग्य\",\n      \"teamInfo\": \"सर्व उत्पादनक्षम आणि सुव्यवस्थित टीमसाठी योग्य\",\n      \"upgrade\": \"योजना बदला\",\n      \"canceledInfo\": \"तुमची योजना रद्द करण्यात आली आहे, {} रोजी तुम्हाला फ्री योजनेवर डाऊनग्रेड केले जाईल.\"\n    },\n    \"addons\": {\n      \"title\": \"ऍड-ऑन्स\",\n      \"addLabel\": \"जोडा\",\n      \"activeLabel\": \"जोडले गेले\",\n      \"aiMax\": {\n        \"title\": \"AI Max\",\n        \"description\": \"प्रगत AI मॉडेल्ससह अमर्यादित AI प्रतिसाद आणि दरमहा 50 AI प्रतिमा\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"प्रति वापरकर्ता प्रति महिना (वार्षिक बिलिंग)\"\n      },\n      \"aiOnDevice\": {\n        \"title\": \"मॅकसाठी ऑन-डिव्हाइस AI\",\n        \"description\": \"तुमच्या डिव्हाइसवर Mistral 7B, LLAMA 3 आणि अधिक स्थानिक मॉडेल्स चालवा\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"प्रति वापरकर्ता प्रति महिना (वार्षिक बिलिंग)\",\n        \"recommend\": \"M1 किंवा नवीनतम शिफारस केली जाते\"\n      }\n    },\n    \"deal\": {\n      \"bannerLabel\": \"नववर्षाचे विशेष ऑफर!\",\n      \"title\": \"तुमची टीम वाढवा!\",\n      \"info\": \"Pro आणि Team योजनांवर 10% सूट मिळवा! @:appName AI सह शक्तिशाली नवीन वैशिष्ट्यांसह तुमचे कार्यक्षेत्र अधिक कार्यक्षम बनवा.\",\n      \"viewPlans\": \"योजना पहा\"\n    }\n  }\n},\n    \"billingPage\": {\n  \"menuLabel\": \"बिलिंग\",\n  \"title\": \"बिलिंग\",\n  \"plan\": {\n    \"title\": \"योजना\",\n    \"freeLabel\": \"फ्री\",\n    \"proLabel\": \"प्रो\",\n    \"planButtonLabel\": \"योजना बदला\",\n    \"billingPeriod\": \"बिलिंग कालावधी\",\n    \"periodButtonLabel\": \"कालावधी संपादित करा\"\n  },\n  \"paymentDetails\": {\n    \"title\": \"पेमेंट तपशील\",\n    \"methodLabel\": \"पेमेंट पद्धत\",\n    \"methodButtonLabel\": \"पद्धत संपादित करा\"\n  },\n  \"addons\": {\n    \"title\": \"ऍड-ऑन्स\",\n    \"addLabel\": \"जोडा\",\n    \"removeLabel\": \"काढा\",\n    \"renewLabel\": \"नवीन करा\",\n    \"aiMax\": {\n      \"label\": \"AI Max\",\n      \"description\": \"अमर्यादित AI आणि प्रगत मॉडेल्स अनलॉक करा\",\n      \"activeDescription\": \"पुढील बिलिंग तारीख {} आहे\",\n      \"canceledDescription\": \"AI Max {} पर्यंत उपलब्ध असेल\"\n    },\n    \"aiOnDevice\": {\n      \"label\": \"मॅकसाठी ऑन-डिव्हाइस AI\",\n      \"description\": \"तुमच्या डिव्हाइसवर अमर्यादित ऑन-डिव्हाइस AI अनलॉक करा\",\n      \"activeDescription\": \"पुढील बिलिंग तारीख {} आहे\",\n      \"canceledDescription\": \"मॅकसाठी ऑन-डिव्हाइस AI {} पर्यंत उपलब्ध असेल\"\n    },\n    \"removeDialog\": {\n      \"title\": \"{} काढा\",\n      \"description\": \"तुम्हाला {plan} काढायचे आहे का? तुम्हाला तत्काळ {plan} चे सर्व फिचर्स आणि फायदे वापरण्याचा अधिकार गमावावा लागेल.\"\n    }\n  },\n  \"currentPeriodBadge\": \"सद्य कालावधी\",\n  \"changePeriod\": \"कालावधी बदला\",\n  \"planPeriod\": \"{} कालावधी\",\n  \"monthlyInterval\": \"मासिक\",\n  \"monthlyPriceInfo\": \"प्रति सदस्य, मासिक बिलिंग\",\n  \"annualInterval\": \"वार्षिक\",\n  \"annualPriceInfo\": \"प्रति सदस्य, वार्षिक बिलिंग\"\n},\n    \"comparePlanDialog\": {\n  \"title\": \"योजना तुलना आणि निवड\",\n  \"planFeatures\": \"योजनेची\\nवैशिष्ट्ये\",\n  \"current\": \"सध्याची\",\n  \"actions\": {\n    \"upgrade\": \"अपग्रेड करा\",\n    \"downgrade\": \"डाऊनग्रेड करा\",\n    \"current\": \"सध्याची\"\n  },\n  \"freePlan\": {\n    \"title\": \"फ्री\",\n    \"description\": \"२ सदस्यांपर्यंत व्यक्तींसाठी सर्व काही व्यवस्थित करण्यासाठी\",\n    \"price\": \"{}\",\n    \"priceInfo\": \"सदैव फ्री\"\n  },\n  \"proPlan\": {\n    \"title\": \"प्रो\",\n    \"description\": \"लहान टीम्ससाठी प्रोजेक्ट्स व ज्ञान व्यवस्थापनासाठी\",\n    \"price\": \"{}\",\n    \"priceInfo\": \"प्रति सदस्य प्रति महिना\\nवार्षिक बिलिंग\\n\\n{} मासिक बिलिंगसाठी\"\n  },\n  \"planLabels\": {\n    \"itemOne\": \"वर्कस्पेसेस\",\n    \"itemTwo\": \"सदस्य\",\n    \"itemThree\": \"स्टोरेज\",\n    \"itemFour\": \"रिअल-टाइम सहकार्य\",\n    \"itemFive\": \"मोबाईल अ‍ॅप\",\n    \"itemSix\": \"AI प्रतिसाद\",\n    \"itemSeven\": \"AI प्रतिमा\",\n    \"itemFileUpload\": \"फाइल अपलोड\",\n    \"customNamespace\": \"सानुकूल नेमस्पेस\",\n    \"tooltipSix\": \"‘Lifetime’ म्हणजे या प्रतिसादांची मर्यादा कधीही रीसेट केली जात नाही\",\n    \"intelligentSearch\": \"स्मार्ट शोध\",\n    \"tooltipSeven\": \"तुमच्या कार्यक्षेत्रासाठी URL चा भाग सानुकूलित करू देते\",\n    \"customNamespaceTooltip\": \"सानुकूल प्रकाशित साइट URL\"\n  },\n  \"freeLabels\": {\n    \"itemOne\": \"प्रत्येक वर्कस्पेसवर शुल्क\",\n    \"itemTwo\": \"२ पर्यंत\",\n    \"itemThree\": \"५ GB\",\n    \"itemFour\": \"होय\",\n    \"itemFive\": \"होय\",\n    \"itemSix\": \"१० कायमस्वरूपी\",\n    \"itemSeven\": \"२ कायमस्वरूपी\",\n    \"itemFileUpload\": \"७ MB पर्यंत\",\n    \"intelligentSearch\": \"स्मार्ट शोध\"\n  },\n  \"proLabels\": {\n    \"itemOne\": \"प्रत्येक वर्कस्पेसवर शुल्क\",\n    \"itemTwo\": \"१० पर्यंत\",\n    \"itemThree\": \"अमर्यादित\",\n    \"itemFour\": \"होय\",\n    \"itemFive\": \"होय\",\n    \"itemSix\": \"अमर्यादित\",\n    \"itemSeven\": \"दर महिन्याला १० प्रतिमा\",\n    \"itemFileUpload\": \"अमर्यादित\",\n    \"intelligentSearch\": \"स्मार्ट शोध\"\n  },\n  \"paymentSuccess\": {\n    \"title\": \"तुम्ही आता {} योजनेवर आहात!\",\n    \"description\": \"तुमचे पेमेंट यशस्वीरित्या पूर्ण झाले असून तुमची योजना @:appName {} मध्ये अपग्रेड झाली आहे. तुम्ही 'योजना' पृष्ठावर तपशील पाहू शकता.\"\n  },\n  \"downgradeDialog\": {\n    \"title\": \"तुम्हाला योजना डाऊनग्रेड करायची आहे का?\",\n    \"description\": \"तुमची योजना डाऊनग्रेड केल्यास तुम्ही फ्री योजनेवर परत जाल. काही सदस्यांना या कार्यक्षेत्राचा प्रवेश मिळणार नाही आणि तुम्हाला स्टोरेज मर्यादेप्रमाणे जागा रिकामी करावी लागू शकते.\",\n    \"downgradeLabel\": \"योजना डाऊनग्रेड करा\"\n  }\n},\n    \"cancelSurveyDialog\": {\n  \"title\": \"तुम्ही जात आहात याचे दुःख आहे\",\n  \"description\": \"तुम्ही जात आहात याचे आम्हाला खरोखरच दुःख वाटते. @:appName सुधारण्यासाठी तुमचा अभिप्राय आम्हाला महत्त्वाचा वाटतो. कृपया काही क्षण घेऊन काही प्रश्नांची उत्तरे द्या.\",\n  \"commonOther\": \"इतर\",\n  \"otherHint\": \"तुमचे उत्तर येथे लिहा\",\n  \"questionOne\": {\n    \"question\": \"तुम्ही तुमची @:appName Pro सदस्यता का रद्द केली?\",\n    \"answerOne\": \"खर्च खूप जास्त आहे\",\n    \"answerTwo\": \"वैशिष्ट्ये अपेक्षांनुसार नव्हती\",\n    \"answerThree\": \"यापेक्षा चांगला पर्याय सापडला\",\n    \"answerFour\": \"वापर फारसा केला नाही, त्यामुळे खर्च योग्य वाटला नाही\",\n    \"answerFive\": \"सेवा समस्या किंवा तांत्रिक अडचणी\"\n  },\n  \"questionTwo\": {\n    \"question\": \"भविष्यात @:appName Pro सदस्यता पुन्हा घेण्याची शक्यता किती आहे?\",\n    \"answerOne\": \"खूप शक्यता आहे\",\n    \"answerTwo\": \"काहीशी शक्यता आहे\",\n    \"answerThree\": \"निश्चित नाही\",\n    \"answerFour\": \"अल्प शक्यता\",\n    \"answerFive\": \"एकदम कमी शक्यता\"\n  },\n  \"questionThree\": {\n    \"question\": \"तुमच्या सदस्यत्वादरम्यान कोणते Pro वैशिष्ट्य सर्वात उपयुक्त वाटले?\",\n    \"answerOne\": \"अनेक वापरकर्त्यांशी सहकार्य\",\n    \"answerTwo\": \"लांब कालावधीची आवृत्ती इतिहास\",\n    \"answerThree\": \"अमर्यादित AI प्रतिसाद\",\n    \"answerFour\": \"स्थानिक AI मॉडेल्सचा प्रवेश\"\n  },\n  \"questionFour\": {\n    \"question\": \"@:appName वापरण्याचा तुमचा एकूण अनुभव कसा होता?\",\n    \"answerOne\": \"खूप छान\",\n    \"answerTwo\": \"चांगला\",\n    \"answerThree\": \"सरासरी\",\n    \"answerFour\": \"सरासरीपेक्षा कमी\",\n    \"answerFive\": \"असंतोषजनक\"\n  }\n},\n    \"common\": {\n  \"uploadingFile\": \"फाईल अपलोड होत आहे. कृपया अ‍ॅप बंद करू नका\",\n  \"uploadNotionSuccess\": \"तुमची Notion zip फाईल यशस्वीरित्या अपलोड झाली आहे. आयात पूर्ण झाल्यानंतर तुम्हाला पुष्टीकरण ईमेल मिळेल\",\n  \"reset\": \"रीसेट करा\"\n},\n    \"menu\": {\n  \"appearance\": \"दृश्यरूप\",\n  \"language\": \"भाषा\",\n  \"user\": \"वापरकर्ता\",\n  \"files\": \"फाईल्स\",\n  \"notifications\": \"सूचना\",\n  \"open\": \"सेटिंग्ज उघडा\",\n  \"logout\": \"लॉगआउट\",\n  \"logoutPrompt\": \"तुम्हाला नक्की लॉगआउट करायचे आहे का?\",\n  \"selfEncryptionLogoutPrompt\": \"तुम्हाला खात्रीने लॉगआउट करायचे आहे का? कृपया खात्री करा की तुम्ही एनक्रिप्शन गुप्तकी कॉपी केली आहे\",\n  \"syncSetting\": \"सिंक्रोनायझेशन सेटिंग\",\n  \"cloudSettings\": \"क्लाऊड सेटिंग्ज\",\n  \"enableSync\": \"सिंक्रोनायझेशन सक्षम करा\",\n  \"enableSyncLog\": \"सिंक लॉगिंग सक्षम करा\",\n  \"enableSyncLogWarning\": \"सिंक समस्यांचे निदान करण्यात मदतीसाठी धन्यवाद. हे तुमचे डॉक्युमेंट संपादन स्थानिक फाईलमध्ये लॉग करेल. कृपया सक्षम केल्यानंतर अ‍ॅप बंद करून पुन्हा उघडा\",\n  \"enableEncrypt\": \"डेटा एन्क्रिप्ट करा\",\n  \"cloudURL\": \"बेस URL\",\n  \"webURL\": \"वेब URL\",\n  \"invalidCloudURLScheme\": \"अवैध स्कीम\",\n  \"cloudServerType\": \"क्लाऊड सर्व्हर\",\n  \"cloudServerTypeTip\": \"कृपया लक्षात घ्या की क्लाऊड सर्व्हर बदलल्यास तुम्ही सध्या लॉगिन केलेले खाते लॉगआउट होऊ शकते\",\n  \"cloudLocal\": \"स्थानिक\",\n  \"cloudAppFlowy\": \"@:appName Cloud\",\n  \"cloudAppFlowySelfHost\": \"@:appName क्लाऊड सेल्फ-होस्टेड\",\n  \"appFlowyCloudUrlCanNotBeEmpty\": \"क्लाऊड URL रिकामा असू शकत नाही\",\n  \"clickToCopy\": \"क्लिपबोर्डवर कॉपी करा\",\n  \"selfHostStart\": \"जर तुमच्याकडे सर्व्हर नसेल, तर कृपया हे पाहा\",\n  \"selfHostContent\": \"दस्तऐवज\",\n  \"selfHostEnd\": \"तुमचा स्वतःचा सर्व्हर कसा होस्ट करावा यासाठी मार्गदर्शनासाठी\",\n  \"pleaseInputValidURL\": \"कृपया वैध URL टाका\",\n  \"changeUrl\": \"सेल्फ-होस्टेड URL {} मध्ये बदला\",\n  \"cloudURLHint\": \"तुमच्या सर्व्हरचा बेस URL टाका\",\n  \"webURLHint\": \"तुमच्या वेब सर्व्हरचा बेस URL टाका\",\n  \"cloudWSURL\": \"वेबसॉकेट URL\",\n  \"cloudWSURLHint\": \"तुमच्या सर्व्हरचा वेबसॉकेट पत्ता टाका\",\n  \"restartApp\": \"अ‍ॅप रीस्टार्ट करा\",\n  \"restartAppTip\": \"बदल प्रभावी होण्यासाठी अ‍ॅप रीस्टार्ट करा. कृपया लक्षात घ्या की यामुळे सध्याचे खाते लॉगआउट होऊ शकते.\",\n  \"changeServerTip\": \"सर्व्हर बदलल्यानंतर, बदल लागू करण्यासाठी तुम्हाला 'रीस्टार्ट' बटणावर क्लिक करणे आवश्यक आहे\",\n  \"enableEncryptPrompt\": \"तुमचा डेटा सुरक्षित करण्यासाठी एन्क्रिप्शन सक्रिय करा. ही गुप्तकी सुरक्षित ठेवा; एकदा सक्षम केल्यावर ती बंद करता येणार नाही. जर हरवली तर तुमचा डेटा पुन्हा मिळवता येणार नाही. कॉपी करण्यासाठी क्लिक करा\",\n  \"inputEncryptPrompt\": \"कृपया खालीलसाठी तुमची एनक्रिप्शन गुप्तकी टाका:\",\n  \"clickToCopySecret\": \"गुप्तकी कॉपी करण्यासाठी क्लिक करा\",\n  \"configServerSetting\": \"तुमच्या सर्व्हर सेटिंग्ज कॉन्फिगर करा\",\n  \"configServerGuide\": \"`Quick Start` निवडल्यानंतर, `Settings` → \\\"Cloud Settings\\\" मध्ये जा आणि तुमचा सेल्फ-होस्टेड सर्व्हर कॉन्फिगर करा.\",\n  \"inputTextFieldHint\": \"तुमची गुप्तकी\",\n  \"historicalUserList\": \"वापरकर्ता लॉगिन इतिहास\",\n  \"historicalUserListTooltip\": \"ही यादी तुमची अनामिक खाती दर्शवते. तपशील पाहण्यासाठी खात्यावर क्लिक करा. अनामिक खाती 'सुरुवात करा' बटणावर क्लिक करून तयार केली जातात\",\n  \"openHistoricalUser\": \"अनामिक खाते उघडण्यासाठी क्लिक करा\",\n  \"customPathPrompt\": \"@:appName डेटा फोल्डर Google Drive सारख्या क्लाऊड-सिंक फोल्डरमध्ये साठवणे धोकादायक ठरू शकते. फोल्डरमधील डेटाबेस अनेक ठिकाणांवरून एकाच वेळी ऍक्सेस/संपादित केल्यास डेटा सिंक समस्या किंवा भ्रष्ट होण्याची शक्यता असते.\",\n  \"importAppFlowyData\": \"बाह्य @:appName फोल्डरमधून डेटा आयात करा\",\n  \"importingAppFlowyDataTip\": \"डेटा आयात सुरू आहे. कृपया अ‍ॅप बंद करू नका\",\n  \"importAppFlowyDataDescription\": \"बाह्य @:appName फोल्डरमधून डेटा कॉपी करून सध्याच्या AppFlowy डेटा फोल्डरमध्ये आयात करा\",\n  \"importSuccess\": \"@:appName डेटा फोल्डर यशस्वीरित्या आयात झाला\",\n  \"importFailed\": \"@:appName डेटा फोल्डर आयात करण्यात अयशस्वी\",\n  \"importGuide\": \"अधिक माहितीसाठी, कृपया संदर्भित दस्तऐवज पहा\"\n},\n  \"notifications\": {\n  \"enableNotifications\": {\n    \"label\": \"सूचना सक्षम करा\",\n    \"hint\": \"स्थानिक सूचना दिसू नयेत यासाठी बंद करा.\"\n  },\n  \"showNotificationsIcon\": {\n    \"label\": \"सूचना चिन्ह दाखवा\",\n    \"hint\": \"साइडबारमध्ये सूचना चिन्ह लपवण्यासाठी बंद करा.\"\n  },\n  \"archiveNotifications\": {\n    \"allSuccess\": \"सर्व सूचना यशस्वीरित्या संग्रहित केल्या\",\n    \"success\": \"सूचना यशस्वीरित्या संग्रहित केली\"\n  },\n  \"markAsReadNotifications\": {\n    \"allSuccess\": \"सर्व वाचलेल्या म्हणून चिन्हांकित केल्या\",\n    \"success\": \"वाचलेले म्हणून चिन्हांकित केले\"\n  },\n  \"action\": {\n    \"markAsRead\": \"वाचलेले म्हणून चिन्हांकित करा\",\n    \"multipleChoice\": \"अधिक निवडा\",\n    \"archive\": \"संग्रहित करा\"\n  },\n  \"settings\": {\n    \"settings\": \"सेटिंग्ज\",\n    \"markAllAsRead\": \"सर्व वाचलेले म्हणून चिन्हांकित करा\",\n    \"archiveAll\": \"सर्व संग्रहित करा\"\n  },\n  \"emptyInbox\": {\n    \"title\": \"इनबॉक्स झिरो!\",\n    \"description\": \"इथे सूचना मिळवण्यासाठी रिमाइंडर सेट करा.\"\n  },\n  \"emptyUnread\": {\n    \"title\": \"कोणतीही न वाचलेली सूचना नाही\",\n    \"description\": \"तुम्ही सर्व वाचले आहे!\"\n  },\n  \"emptyArchived\": {\n    \"title\": \"कोणतीही संग्रहित सूचना नाही\",\n    \"description\": \"संग्रहित सूचना इथे दिसतील.\"\n  },\n  \"tabs\": {\n    \"inbox\": \"इनबॉक्स\",\n    \"unread\": \"न वाचलेले\",\n    \"archived\": \"संग्रहित\"\n  },\n  \"refreshSuccess\": \"सूचना यशस्वीरित्या रीफ्रेश केल्या\",\n  \"titles\": {\n    \"notifications\": \"सूचना\",\n    \"reminder\": \"रिमाइंडर\"\n  }\n},\n    \"appearance\": {\n  \"resetSetting\": \"रीसेट\",\n  \"fontFamily\": {\n    \"label\": \"फॉन्ट फॅमिली\",\n    \"search\": \"शोध\",\n    \"defaultFont\": \"सिस्टम\"\n  },\n  \"themeMode\": {\n    \"label\": \"थीम मोड\",\n    \"light\": \"लाइट मोड\",\n    \"dark\": \"डार्क मोड\",\n    \"system\": \"सिस्टमशी जुळवा\"\n  },\n  \"fontScaleFactor\": \"फॉन्ट स्केल घटक\",\n  \"displaySize\": \"डिस्प्ले आकार\",\n  \"documentSettings\": {\n    \"cursorColor\": \"डॉक्युमेंट कर्सरचा रंग\",\n    \"selectionColor\": \"डॉक्युमेंट निवडीचा रंग\",\n    \"width\": \"डॉक्युमेंटची रुंदी\",\n    \"changeWidth\": \"बदला\",\n    \"pickColor\": \"रंग निवडा\",\n    \"colorShade\": \"रंगाची छटा\",\n    \"opacity\": \"अपारदर्शकता\",\n    \"hexEmptyError\": \"Hex रंग रिकामा असू शकत नाही\",\n    \"hexLengthError\": \"Hex व्हॅल्यू 6 अंकांची असावी\",\n    \"hexInvalidError\": \"अवैध Hex व्हॅल्यू\",\n    \"opacityEmptyError\": \"अपारदर्शकता रिकामी असू शकत नाही\",\n    \"opacityRangeError\": \"अपारदर्शकता 1 ते 100 दरम्यान असावी\",\n    \"app\": \"अ‍ॅप\",\n    \"flowy\": \"Flowy\",\n    \"apply\": \"लागू करा\"\n  },\n  \"layoutDirection\": {\n    \"label\": \"लेआउट दिशा\",\n    \"hint\": \"तुमच्या स्क्रीनवरील कंटेंटचा प्रवाह नियंत्रित करा — डावीकडून उजवीकडे किंवा उजवीकडून डावीकडे.\",\n    \"ltr\": \"LTR\",\n    \"rtl\": \"RTL\"\n  },\n  \"textDirection\": {\n    \"label\": \"मूलभूत मजकूर दिशा\",\n    \"hint\": \"मजकूर डावीकडून सुरू व्हावा की उजवीकडून याचे पूर्वनिर्धारित निर्धारण करा.\",\n    \"ltr\": \"LTR\",\n    \"rtl\": \"RTL\",\n    \"auto\": \"स्वयं\",\n    \"fallback\": \"लेआउट दिशेशी जुळवा\"\n  },\n  \"themeUpload\": {\n    \"button\": \"अपलोड\",\n    \"uploadTheme\": \"थीम अपलोड करा\",\n    \"description\": \"खालील बटण वापरून तुमची स्वतःची @:appName थीम अपलोड करा.\",\n    \"loading\": \"कृपया प्रतीक्षा करा. आम्ही तुमची थीम पडताळत आहोत आणि अपलोड करत आहोत...\",\n    \"uploadSuccess\": \"तुमची थीम यशस्वीरित्या अपलोड झाली आहे\",\n    \"deletionFailure\": \"थीम हटवण्यात अयशस्वी. कृपया ती मॅन्युअली हटवून पाहा.\",\n    \"filePickerDialogTitle\": \".flowy_plugin फाईल निवडा\",\n    \"urlUploadFailure\": \"URL उघडण्यात अयशस्वी: {}\"\n  },\n  \"theme\": \"थीम\",\n  \"builtInsLabel\": \"अंतर्गत थीम्स\",\n  \"pluginsLabel\": \"प्लगइन्स\",\n  \"dateFormat\": {\n    \"label\": \"दिनांक फॉरमॅट\",\n    \"local\": \"स्थानिक\",\n    \"us\": \"US\",\n    \"iso\": \"ISO\",\n    \"friendly\": \"अनौपचारिक\",\n    \"dmy\": \"D/M/Y\"\n  },\n  \"timeFormat\": {\n    \"label\": \"वेळ फॉरमॅट\",\n    \"twelveHour\": \"१२ तास\",\n    \"twentyFourHour\": \"२४ तास\"\n  },\n  \"showNamingDialogWhenCreatingPage\": \"पृष्ठ तयार करताना नाव विचारणारा डायलॉग दाखवा\",\n  \"enableRTLToolbarItems\": \"RTL टूलबार आयटम्स सक्षम करा\",\n  \"members\": {\n    \"title\": \"सदस्य सेटिंग्ज\",\n    \"inviteMembers\": \"सदस्यांना आमंत्रण द्या\",\n    \"inviteHint\": \"ईमेलद्वारे आमंत्रण द्या\",\n    \"sendInvite\": \"आमंत्रण पाठवा\",\n    \"copyInviteLink\": \"आमंत्रण दुवा कॉपी करा\",\n    \"label\": \"सदस्य\",\n    \"user\": \"वापरकर्ता\",\n    \"role\": \"भूमिका\",\n    \"removeFromWorkspace\": \"वर्कस्पेसमधून काढा\",\n    \"removeFromWorkspaceSuccess\": \"वर्कस्पेसमधून यशस्वीरित्या काढले\",\n    \"removeFromWorkspaceFailed\": \"वर्कस्पेसमधून काढण्यात अयशस्वी\",\n    \"owner\": \"मालक\",\n    \"guest\": \"अतिथी\",\n    \"member\": \"सदस्य\",\n    \"memberHintText\": \"सदस्य पृष्ठे वाचू व संपादित करू शकतो\",\n    \"guestHintText\": \"अतिथी पृष्ठे वाचू शकतो, प्रतिक्रिया देऊ शकतो, टिप्पणी करू शकतो, आणि परवानगी असल्यास काही पृष्ठे संपादित करू शकतो.\",\n    \"emailInvalidError\": \"अवैध ईमेल, कृपया तपासा व पुन्हा प्रयत्न करा\",\n    \"emailSent\": \"ईमेल पाठवला गेला आहे, कृपया इनबॉक्स तपासा\",\n    \"members\": \"सदस्य\",\n    \"membersCount\": {\n      \"zero\": \"{} सदस्य\",\n      \"one\": \"{} सदस्य\",\n      \"other\": \"{} सदस्य\"\n    },\n    \"inviteFailedDialogTitle\": \"आमंत्रण पाठवण्यात अयशस्वी\",\n    \"inviteFailedMemberLimit\": \"सदस्य मर्यादा गाठली आहे, अधिक सदस्यांसाठी कृपया अपग्रेड करा.\",\n    \"inviteFailedMemberLimitMobile\": \"तुमच्या वर्कस्पेसने सदस्य मर्यादा गाठली आहे.\",\n    \"memberLimitExceeded\": \"सदस्य मर्यादा गाठली आहे, अधिक सदस्य आमंत्रित करण्यासाठी कृपया \",\n    \"memberLimitExceededUpgrade\": \"अपग्रेड करा\",\n    \"memberLimitExceededPro\": \"सदस्य मर्यादा गाठली आहे, अधिक सदस्य आवश्यक असल्यास संपर्क करा\",\n    \"memberLimitExceededProContact\": \"support@appflowy.io\",\n    \"failedToAddMember\": \"सदस्य जोडण्यात अयशस्वी\",\n    \"addMemberSuccess\": \"सदस्य यशस्वीरित्या जोडला गेला\",\n    \"removeMember\": \"सदस्य काढा\",\n    \"areYouSureToRemoveMember\": \"तुम्हाला हा सदस्य काढायचा आहे का?\",\n    \"inviteMemberSuccess\": \"आमंत्रण यशस्वीरित्या पाठवले गेले\",\n    \"failedToInviteMember\": \"सदस्य आमंत्रित करण्यात अयशस्वी\",\n    \"workspaceMembersError\": \"अरे! काहीतरी चूक झाली आहे\",\n    \"workspaceMembersErrorDescription\": \"आम्ही सध्या सदस्यांची यादी लोड करू शकत नाही. कृपया नंतर पुन्हा प्रयत्न करा\"\n  }\n},\n    \"files\": {\n  \"copy\": \"कॉपी करा\",\n  \"defaultLocation\": \"फाईल्स आणि डेटाचा संचय स्थान\",\n  \"exportData\": \"तुमचा डेटा निर्यात करा\",\n  \"doubleTapToCopy\": \"पथ कॉपी करण्यासाठी दोनदा टॅप करा\",\n  \"restoreLocation\": \"@:appName चे मूळ स्थान पुनर्संचयित करा\",\n  \"customizeLocation\": \"इतर फोल्डर उघडा\",\n  \"restartApp\": \"बदल लागू करण्यासाठी कृपया अ‍ॅप रीस्टार्ट करा.\",\n  \"exportDatabase\": \"डेटाबेस निर्यात करा\",\n  \"selectFiles\": \"निर्यात करण्यासाठी फाईल्स निवडा\",\n  \"selectAll\": \"सर्व निवडा\",\n  \"deselectAll\": \"सर्व निवड रद्द करा\",\n  \"createNewFolder\": \"नवीन फोल्डर तयार करा\",\n  \"createNewFolderDesc\": \"तुमचा डेटा कुठे साठवायचा हे सांगा\",\n  \"defineWhereYourDataIsStored\": \"तुमचा डेटा कुठे साठवला जातो हे ठरवा\",\n  \"open\": \"उघडा\",\n  \"openFolder\": \"आधीक फोल्डर उघडा\",\n  \"openFolderDesc\": \"तुमच्या विद्यमान @:appName फोल्डरमध्ये वाचन व लेखन करा\",\n  \"folderHintText\": \"फोल्डरचे नाव\",\n  \"location\": \"नवीन फोल्डर तयार करत आहे\",\n  \"locationDesc\": \"तुमच्या @:appName डेटासाठी नाव निवडा\",\n  \"browser\": \"ब्राउझ करा\",\n  \"create\": \"तयार करा\",\n  \"set\": \"सेट करा\",\n  \"folderPath\": \"फोल्डर साठवण्याचा मार्ग\",\n  \"locationCannotBeEmpty\": \"मार्ग रिकामा असू शकत नाही\",\n  \"pathCopiedSnackbar\": \"फाईल संचय मार्ग क्लिपबोर्डवर कॉपी केला!\",\n  \"changeLocationTooltips\": \"डेटा डिरेक्टरी बदला\",\n  \"change\": \"बदला\",\n  \"openLocationTooltips\": \"इतर डेटा डिरेक्टरी उघडा\",\n  \"openCurrentDataFolder\": \"सध्याची डेटा डिरेक्टरी उघडा\",\n  \"recoverLocationTooltips\": \"@:appName च्या मूळ डेटा डिरेक्टरीवर रीसेट करा\",\n  \"exportFileSuccess\": \"फाईल यशस्वीरित्या निर्यात झाली!\",\n  \"exportFileFail\": \"फाईल निर्यात करण्यात अयशस्वी!\",\n  \"export\": \"निर्यात करा\",\n  \"clearCache\": \"कॅशे साफ करा\",\n  \"clearCacheDesc\": \"प्रतिमा लोड होत नाहीत किंवा फॉन्ट अयोग्यरित्या दिसत असल्यास, कॅशे साफ करून पाहा. ही क्रिया तुमचा वापरकर्ता डेटा हटवणार नाही.\",\n  \"areYouSureToClearCache\": \"तुम्हाला नक्की कॅशे साफ करायची आहे का?\",\n  \"clearCacheSuccess\": \"कॅशे यशस्वीरित्या साफ झाली!\"\n},\n    \"user\": {\n  \"name\": \"नाव\",\n  \"email\": \"ईमेल\",\n  \"tooltipSelectIcon\": \"चिन्ह निवडा\",\n  \"selectAnIcon\": \"चिन्ह निवडा\",\n  \"pleaseInputYourOpenAIKey\": \"कृपया तुमची AI की टाका\",\n  \"clickToLogout\": \"सध्याचा वापरकर्ता लॉगआउट करण्यासाठी क्लिक करा\"\n},\n    \"mobile\": {\n  \"personalInfo\": \"वैयक्तिक माहिती\",\n  \"username\": \"वापरकर्तानाव\",\n  \"usernameEmptyError\": \"वापरकर्तानाव रिकामे असू शकत नाही\",\n  \"about\": \"विषयी\",\n  \"pushNotifications\": \"पुश सूचना\",\n  \"support\": \"सपोर्ट\",\n  \"joinDiscord\": \"Discord मध्ये सहभागी व्हा\",\n  \"privacyPolicy\": \"गोपनीयता धोरण\",\n  \"userAgreement\": \"वापरकर्ता करार\",\n  \"termsAndConditions\": \"अटी व शर्ती\",\n  \"userprofileError\": \"वापरकर्ता प्रोफाइल लोड करण्यात अयशस्वी\",\n  \"userprofileErrorDescription\": \"कृपया लॉगआउट करून पुन्हा लॉगिन करा आणि त्रुटी अजूनही येते का ते पहा.\",\n  \"selectLayout\": \"लेआउट निवडा\",\n  \"selectStartingDay\": \"सप्ताहाचा प्रारंभ दिवस निवडा\",\n  \"version\": \"आवृत्ती\"\n},\n  \"grid\": {\n  \"deleteView\": \"तुम्हाला हे दृश्य हटवायचे आहे का?\",\n  \"createView\": \"नवीन\",\n  \"title\": {\n    \"placeholder\": \"नाव नाही\"\n  },\n  \"settings\": {\n    \"filter\": \"फिल्टर\",\n    \"sort\": \"क्रमवारी\",\n    \"sortBy\": \"यावरून क्रमवारी लावा\",\n    \"properties\": \"गुणधर्म\",\n    \"reorderPropertiesTooltip\": \"गुणधर्मांचे स्थान बदला\",\n    \"group\": \"समूह\",\n    \"addFilter\": \"फिल्टर जोडा\",\n    \"deleteFilter\": \"फिल्टर हटवा\",\n    \"filterBy\": \"यावरून फिल्टर करा\",\n    \"typeAValue\": \"मूल्य लिहा...\",\n    \"layout\": \"लेआउट\",\n    \"compactMode\": \"कॉम्पॅक्ट मोड\",\n    \"databaseLayout\": \"लेआउट\",\n    \"viewList\": {\n      \"zero\": \"० दृश्ये\",\n      \"one\": \"{count} दृश्य\",\n      \"other\": \"{count} दृश्ये\"\n    },\n    \"editView\": \"दृश्य संपादित करा\",\n    \"boardSettings\": \"बोर्ड सेटिंग\",\n    \"calendarSettings\": \"कॅलेंडर सेटिंग\",\n    \"createView\": \"नवीन दृश्य\",\n    \"duplicateView\": \"दृश्याची प्रत बनवा\",\n    \"deleteView\": \"दृश्य हटवा\",\n    \"numberOfVisibleFields\": \"{} दर्शविले\"\n  },\n  \"filter\": {\n    \"empty\": \"कोणतेही सक्रिय फिल्टर नाहीत\",\n    \"addFilter\": \"फिल्टर जोडा\",\n    \"cannotFindCreatableField\": \"फिल्टर करण्यासाठी योग्य फील्ड सापडले नाही\",\n    \"conditon\": \"अट\",\n    \"where\": \"जिथे\"\n  },\n  \"textFilter\": {\n    \"contains\": \"अंतर्भूत आहे\",\n    \"doesNotContain\": \"अंतर्भूत नाही\",\n    \"endsWith\": \"याने समाप्त होते\",\n    \"startWith\": \"याने सुरू होते\",\n    \"is\": \"आहे\",\n    \"isNot\": \"नाही\",\n    \"isEmpty\": \"रिकामे आहे\",\n    \"isNotEmpty\": \"रिकामे नाही\",\n    \"choicechipPrefix\": {\n      \"isNot\": \"नाही\",\n      \"startWith\": \"याने सुरू होते\",\n      \"endWith\": \"याने समाप्त होते\",\n      \"isEmpty\": \"रिकामे आहे\",\n      \"isNotEmpty\": \"रिकामे नाही\"\n    }\n  },\n  \"checkboxFilter\": {\n    \"isChecked\": \"निवडलेले आहे\",\n    \"isUnchecked\": \"निवडलेले नाही\",\n    \"choicechipPrefix\": {\n      \"is\": \"आहे\"\n    }\n  },\n  \"checklistFilter\": {\n    \"isComplete\": \"पूर्ण झाले आहे\",\n    \"isIncomplted\": \"अपूर्ण आहे\"\n  },\n    \"selectOptionFilter\": {\n  \"is\": \"आहे\",\n  \"isNot\": \"नाही\",\n  \"contains\": \"अंतर्भूत आहे\",\n  \"doesNotContain\": \"अंतर्भूत नाही\",\n  \"isEmpty\": \"रिकामे आहे\",\n  \"isNotEmpty\": \"रिकामे नाही\"\n},\n\"dateFilter\": {\n  \"is\": \"या दिवशी आहे\",\n  \"before\": \"पूर्वी आहे\",\n  \"after\": \"नंतर आहे\",\n  \"onOrBefore\": \"या दिवशी किंवा त्याआधी आहे\",\n  \"onOrAfter\": \"या दिवशी किंवा त्यानंतर आहे\",\n  \"between\": \"दरम्यान आहे\",\n  \"empty\": \"रिकामे आहे\",\n  \"notEmpty\": \"रिकामे नाही\",\n  \"startDate\": \"सुरुवातीची तारीख\",\n  \"endDate\": \"शेवटची तारीख\",\n  \"choicechipPrefix\": {\n    \"before\": \"पूर्वी\",\n    \"after\": \"नंतर\",\n    \"between\": \"दरम्यान\",\n    \"onOrBefore\": \"या दिवशी किंवा त्याआधी\",\n    \"onOrAfter\": \"या दिवशी किंवा त्यानंतर\",\n    \"isEmpty\": \"रिकामे आहे\",\n    \"isNotEmpty\": \"रिकामे नाही\"\n  }\n},\n\"numberFilter\": {\n  \"equal\": \"बरोबर आहे\",\n  \"notEqual\": \"बरोबर नाही\",\n  \"lessThan\": \"पेक्षा कमी आहे\",\n  \"greaterThan\": \"पेक्षा जास्त आहे\",\n  \"lessThanOrEqualTo\": \"किंवा कमी आहे\",\n  \"greaterThanOrEqualTo\": \"किंवा जास्त आहे\",\n  \"isEmpty\": \"रिकामे आहे\",\n  \"isNotEmpty\": \"रिकामे नाही\"\n},\n\"field\": {\n  \"label\": \"गुणधर्म\",\n  \"hide\": \"गुणधर्म लपवा\",\n  \"show\": \"गुणधर्म दर्शवा\",\n  \"insertLeft\": \"डावीकडे जोडा\",\n  \"insertRight\": \"उजवीकडे जोडा\",\n  \"duplicate\": \"प्रत बनवा\",\n  \"delete\": \"हटवा\",\n  \"wrapCellContent\": \"पाठ लपेटा\",\n  \"clear\": \"सेल्स रिकामे करा\",\n  \"switchPrimaryFieldTooltip\": \"प्राथमिक फील्डचा प्रकार बदलू शकत नाही\",\n  \"textFieldName\": \"मजकूर\",\n  \"checkboxFieldName\": \"चेकबॉक्स\",\n  \"dateFieldName\": \"तारीख\",\n  \"updatedAtFieldName\": \"शेवटचे अपडेट\",\n  \"createdAtFieldName\": \"तयार झाले\",\n  \"numberFieldName\": \"संख्या\",\n  \"singleSelectFieldName\": \"सिंगल सिलेक्ट\",\n  \"multiSelectFieldName\": \"मल्टीसिलेक्ट\",\n  \"urlFieldName\": \"URL\",\n  \"checklistFieldName\": \"चेकलिस्ट\",\n  \"relationFieldName\": \"संबंध\",\n  \"summaryFieldName\": \"AI सारांश\",\n  \"timeFieldName\": \"वेळ\",\n  \"mediaFieldName\": \"फाईल्स आणि मीडिया\",\n  \"translateFieldName\": \"AI भाषांतर\",\n  \"translateTo\": \"मध्ये भाषांतर करा\",\n  \"numberFormat\": \"संख्या स्वरूप\",\n  \"dateFormat\": \"तारीख स्वरूप\",\n  \"includeTime\": \"वेळ जोडा\",\n  \"isRange\": \"शेवटची तारीख\",\n  \"dateFormatFriendly\": \"महिना दिवस, वर्ष\",\n  \"dateFormatISO\": \"वर्ष-महिना-दिनांक\",\n  \"dateFormatLocal\": \"महिना/दिवस/वर्ष\",\n  \"dateFormatUS\": \"वर्ष/महिना/दिवस\",\n  \"dateFormatDayMonthYear\": \"दिवस/महिना/वर्ष\",\n  \"timeFormat\": \"वेळ स्वरूप\",\n  \"invalidTimeFormat\": \"अवैध स्वरूप\",\n  \"timeFormatTwelveHour\": \"१२ तास\",\n  \"timeFormatTwentyFourHour\": \"२४ तास\",\n  \"clearDate\": \"तारीख हटवा\",\n  \"dateTime\": \"तारीख व वेळ\",\n  \"startDateTime\": \"सुरुवातीची तारीख व वेळ\",\n  \"endDateTime\": \"शेवटची तारीख व वेळ\",\n  \"failedToLoadDate\": \"तारीख मूल्य लोड करण्यात अयशस्वी\",\n  \"selectTime\": \"वेळ निवडा\",\n  \"selectDate\": \"तारीख निवडा\",\n  \"visibility\": \"दृश्यता\",\n  \"propertyType\": \"गुणधर्माचा प्रकार\",\n  \"addSelectOption\": \"पर्याय जोडा\",\n  \"typeANewOption\": \"नवीन पर्याय लिहा\",\n  \"optionTitle\": \"पर्याय\",\n  \"addOption\": \"पर्याय जोडा\",\n  \"editProperty\": \"गुणधर्म संपादित करा\",\n  \"newProperty\": \"नवीन गुणधर्म\",\n  \"openRowDocument\": \"पृष्ठ म्हणून उघडा\",\n  \"deleteFieldPromptMessage\": \"तुम्हाला खात्री आहे का? हा गुणधर्म आणि त्याचा डेटा हटवला जाईल\",\n  \"clearFieldPromptMessage\": \"तुम्हाला खात्री आहे का? या कॉलममधील सर्व सेल्स रिकामे होतील\",\n  \"newColumn\": \"नवीन कॉलम\",\n  \"format\": \"स्वरूप\",\n  \"reminderOnDateTooltip\": \"या सेलमध्ये अनुस्मारक शेड्यूल केले आहे\",\n  \"optionAlreadyExist\": \"पर्याय आधीच अस्तित्वात आहे\"\n},\n    \"rowPage\": {\n  \"newField\": \"नवीन फील्ड जोडा\",\n  \"fieldDragElementTooltip\": \"मेनू उघडण्यासाठी क्लिक करा\",\n  \"showHiddenFields\": {\n    \"one\": \"{count} लपलेले फील्ड दाखवा\",\n    \"many\": \"{count} लपलेली फील्ड दाखवा\",\n    \"other\": \"{count} लपलेली फील्ड दाखवा\"\n  },\n  \"hideHiddenFields\": {\n    \"one\": \"{count} लपलेले फील्ड लपवा\",\n    \"many\": \"{count} लपलेली फील्ड लपवा\",\n    \"other\": \"{count} लपलेली फील्ड लपवा\"\n  },\n  \"openAsFullPage\": \"पूर्ण पृष्ठ म्हणून उघडा\",\n  \"moreRowActions\": \"अधिक पंक्ती क्रिया\"\n},\n\"sort\": {\n  \"ascending\": \"चढत्या क्रमाने\",\n  \"descending\": \"उतरत्या क्रमाने\",\n  \"by\": \"द्वारे\",\n  \"empty\": \"सक्रिय सॉर्ट्स नाहीत\",\n  \"cannotFindCreatableField\": \"सॉर्टसाठी योग्य फील्ड सापडले नाही\",\n  \"deleteAllSorts\": \"सर्व सॉर्ट्स हटवा\",\n  \"addSort\": \"सॉर्ट जोडा\",\n  \"sortsActive\": \"सॉर्टिंग करत असताना {intention} करू शकत नाही\",\n  \"removeSorting\": \"या दृश्यातील सर्व सॉर्ट्स हटवायचे आहेत का?\",\n  \"fieldInUse\": \"या फील्डवर आधीच सॉर्टिंग चालू आहे\"\n},\n\"row\": {\n  \"label\": \"पंक्ती\",\n  \"duplicate\": \"प्रत बनवा\",\n  \"delete\": \"हटवा\",\n  \"titlePlaceholder\": \"शीर्षक नाही\",\n  \"textPlaceholder\": \"रिक्त\",\n  \"copyProperty\": \"गुणधर्म क्लिपबोर्डवर कॉपी केला\",\n  \"count\": \"संख्या\",\n  \"newRow\": \"नवीन पंक्ती\",\n  \"loadMore\": \"अधिक लोड करा\",\n  \"action\": \"क्रिया\",\n  \"add\": \"खाली जोडा वर क्लिक करा\",\n  \"drag\": \"हलवण्यासाठी ड्रॅग करा\",\n  \"deleteRowPrompt\": \"तुम्हाला खात्री आहे का की ही पंक्ती हटवायची आहे? ही क्रिया पूर्ववत करता येणार नाही.\",\n  \"deleteCardPrompt\": \"तुम्हाला खात्री आहे का की हे कार्ड हटवायचे आहे? ही क्रिया पूर्ववत करता येणार नाही.\",\n  \"dragAndClick\": \"हलवण्यासाठी ड्रॅग करा, मेनू उघडण्यासाठी क्लिक करा\",\n  \"insertRecordAbove\": \"वर रेकॉर्ड जोडा\",\n  \"insertRecordBelow\": \"खाली रेकॉर्ड जोडा\",\n  \"noContent\": \"माहिती नाही\",\n  \"reorderRowDescription\": \"पंक्तीचे पुन्हा क्रमांकन\",\n  \"createRowAboveDescription\": \"वर पंक्ती तयार करा\",\n  \"createRowBelowDescription\": \"खाली पंक्ती जोडा\"\n},\n\"selectOption\": {\n  \"create\": \"तयार करा\",\n  \"purpleColor\": \"जांभळा\",\n  \"pinkColor\": \"गुलाबी\",\n  \"lightPinkColor\": \"फिकट गुलाबी\",\n  \"orangeColor\": \"नारंगी\",\n  \"yellowColor\": \"पिवळा\",\n  \"limeColor\": \"लिंबू\",\n  \"greenColor\": \"हिरवा\",\n  \"aquaColor\": \"आक्वा\",\n  \"blueColor\": \"निळा\",\n  \"deleteTag\": \"टॅग हटवा\",\n  \"colorPanelTitle\": \"रंग\",\n  \"panelTitle\": \"पर्याय निवडा किंवा नवीन तयार करा\",\n  \"searchOption\": \"पर्याय शोधा\",\n  \"searchOrCreateOption\": \"पर्याय शोधा किंवा तयार करा\",\n  \"createNew\": \"नवीन तयार करा\",\n  \"orSelectOne\": \"किंवा पर्याय निवडा\",\n  \"typeANewOption\": \"नवीन पर्याय टाइप करा\",\n  \"tagName\": \"टॅग नाव\"\n},\n\"checklist\": {\n  \"taskHint\": \"कार्याचे वर्णन\",\n  \"addNew\": \"नवीन कार्य जोडा\",\n  \"submitNewTask\": \"तयार करा\",\n  \"hideComplete\": \"पूर्ण कार्ये लपवा\",\n  \"showComplete\": \"सर्व कार्ये दाखवा\"\n},\n\"url\": {\n  \"launch\": \"बाह्य ब्राउझरमध्ये लिंक उघडा\",\n  \"copy\": \"लिंक क्लिपबोर्डवर कॉपी करा\",\n  \"textFieldHint\": \"URL टाका\",\n  \"copiedNotification\": \"क्लिपबोर्डवर कॉपी केले!\"\n},\n\"relation\": {\n  \"relatedDatabasePlaceLabel\": \"संबंधित डेटाबेस\",\n  \"relatedDatabasePlaceholder\": \"काही नाही\",\n  \"inRelatedDatabase\": \"या मध्ये\",\n  \"rowSearchTextFieldPlaceholder\": \"शोध\",\n  \"noDatabaseSelected\": \"कोणताही डेटाबेस निवडलेला नाही, कृपया खालील यादीतून एक निवडा:\",\n  \"emptySearchResult\": \"कोणतीही नोंद सापडली नाही\",\n  \"linkedRowListLabel\": \"{count} लिंक केलेल्या पंक्ती\",\n  \"unlinkedRowListLabel\": \"आणखी एक पंक्ती लिंक करा\"\n},\n\"menuName\": \"ग्रिड\",\n\"referencedGridPrefix\": \"दृश्य\",\n\"calculate\": \"गणना करा\",\n\"calculationTypeLabel\": {\n  \"none\": \"काही नाही\",\n  \"average\": \"सरासरी\",\n  \"max\": \"कमाल\",\n  \"median\": \"मध्यम\",\n  \"min\": \"किमान\",\n  \"sum\": \"बेरीज\",\n  \"count\": \"मोजणी\",\n  \"countEmpty\": \"रिकाम्यांची मोजणी\",\n  \"countEmptyShort\": \"रिक्त\",\n  \"countNonEmpty\": \"रिक्त नसलेल्यांची मोजणी\",\n  \"countNonEmptyShort\": \"भरलेले\"\n},\n\"media\": {\n  \"rename\": \"पुन्हा नाव द्या\",\n  \"download\": \"डाउनलोड करा\",\n  \"expand\": \"मोठे करा\",\n  \"delete\": \"हटवा\",\n  \"moreFilesHint\": \"+{}\",\n  \"addFileOrImage\": \"फाईल किंवा लिंक जोडा\",\n  \"attachmentsHint\": \"{}\",\n  \"addFileMobile\": \"फाईल जोडा\",\n  \"extraCount\": \"+{}\",\n  \"deleteFileDescription\": \"तुम्हाला खात्री आहे का की ही फाईल हटवायची आहे? ही क्रिया पूर्ववत करता येणार नाही.\",\n  \"showFileNames\": \"फाईलचे नाव दाखवा\",\n  \"downloadSuccess\": \"फाईल डाउनलोड झाली\",\n  \"downloadFailedToken\": \"फाईल डाउनलोड अयशस्वी, वापरकर्ता टोकन अनुपलब्ध\",\n  \"setAsCover\": \"कव्हर म्हणून सेट करा\",\n  \"openInBrowser\": \"ब्राउझरमध्ये उघडा\",\n  \"embedLink\": \"फाईल लिंक एम्बेड करा\"\n  }\n},\n  \"document\": {\n  \"menuName\": \"दस्तऐवज\",\n  \"date\": {\n    \"timeHintTextInTwelveHour\": \"01:00 PM\",\n    \"timeHintTextInTwentyFourHour\": \"13:00\"\n  },\n  \"creating\": \"तयार करत आहे...\",\n  \"slashMenu\": {\n    \"board\": {\n      \"selectABoardToLinkTo\": \"लिंक करण्यासाठी बोर्ड निवडा\",\n      \"createANewBoard\": \"नवीन बोर्ड तयार करा\"\n    },\n    \"grid\": {\n      \"selectAGridToLinkTo\": \"लिंक करण्यासाठी ग्रिड निवडा\",\n      \"createANewGrid\": \"नवीन ग्रिड तयार करा\"\n    },\n    \"calendar\": {\n      \"selectACalendarToLinkTo\": \"लिंक करण्यासाठी दिनदर्शिका निवडा\",\n      \"createANewCalendar\": \"नवीन दिनदर्शिका तयार करा\"\n    },\n    \"document\": {\n      \"selectADocumentToLinkTo\": \"लिंक करण्यासाठी दस्तऐवज निवडा\"\n    },\n    \"name\": {\n      \"textStyle\": \"मजकुराची शैली\",\n      \"list\": \"यादी\",\n      \"toggle\": \"टॉगल\",\n      \"fileAndMedia\": \"फाईल व मीडिया\",\n      \"simpleTable\": \"सोपे टेबल\",\n      \"visuals\": \"दृश्य घटक\",\n      \"document\": \"दस्तऐवज\",\n      \"advanced\": \"प्रगत\",\n      \"text\": \"मजकूर\",\n      \"heading1\": \"शीर्षक 1\",\n      \"heading2\": \"शीर्षक 2\",\n      \"heading3\": \"शीर्षक 3\",\n      \"image\": \"प्रतिमा\",\n      \"bulletedList\": \"बुलेट यादी\",\n      \"numberedList\": \"क्रमांकित यादी\",\n      \"todoList\": \"करण्याची यादी\",\n      \"doc\": \"दस्तऐवज\",\n      \"linkedDoc\": \"पृष्ठाशी लिंक करा\",\n      \"grid\": \"ग्रिड\",\n      \"linkedGrid\": \"लिंक केलेला ग्रिड\",\n      \"kanban\": \"कानबन\",\n      \"linkedKanban\": \"लिंक केलेला कानबन\",\n      \"calendar\": \"दिनदर्शिका\",\n      \"linkedCalendar\": \"लिंक केलेली दिनदर्शिका\",\n      \"quote\": \"उद्धरण\",\n      \"divider\": \"विभाजक\",\n      \"table\": \"टेबल\",\n      \"callout\": \"महत्त्वाचा मजकूर\",\n      \"outline\": \"रूपरेषा\",\n      \"mathEquation\": \"गणिती समीकरण\",\n      \"code\": \"कोड\",\n      \"toggleList\": \"टॉगल यादी\",\n      \"toggleHeading1\": \"टॉगल शीर्षक 1\",\n      \"toggleHeading2\": \"टॉगल शीर्षक 2\",\n      \"toggleHeading3\": \"टॉगल शीर्षक 3\",\n      \"emoji\": \"इमोजी\",\n      \"aiWriter\": \"AI ला काहीही विचारा\",\n      \"dateOrReminder\": \"दिनांक किंवा स्मरणपत्र\",\n      \"photoGallery\": \"फोटो गॅलरी\",\n      \"file\": \"फाईल\",\n      \"twoColumns\": \"२ स्तंभ\",\n      \"threeColumns\": \"३ स्तंभ\",\n      \"fourColumns\": \"४ स्तंभ\"\n    },\n    \"subPage\": {\n      \"name\": \"दस्तऐवज\",\n      \"keyword1\": \"उपपृष्ठ\",\n      \"keyword2\": \"पृष्ठ\",\n      \"keyword3\": \"चाइल्ड पृष्ठ\",\n      \"keyword4\": \"पृष्ठ जोडा\",\n      \"keyword5\": \"एम्बेड पृष्ठ\",\n      \"keyword6\": \"नवीन पृष्ठ\",\n      \"keyword7\": \"पृष्ठ तयार करा\",\n      \"keyword8\": \"दस्तऐवज\"\n    }\n  },\n  \"selectionMenu\": {\n    \"outline\": \"रूपरेषा\",\n    \"codeBlock\": \"कोड ब्लॉक\"\n  },\n  \"plugins\": {\n    \"referencedBoard\": \"संदर्भित बोर्ड\",\n    \"referencedGrid\": \"संदर्भित ग्रिड\",\n    \"referencedCalendar\": \"संदर्भित दिनदर्शिका\",\n    \"referencedDocument\": \"संदर्भित दस्तऐवज\",\n    \"aiWriter\": {\n      \"userQuestion\": \"AI ला काहीही विचारा\",\n      \"continueWriting\": \"लेखन सुरू ठेवा\",\n      \"fixSpelling\": \"स्पेलिंग व व्याकरण सुधारणा\",\n      \"improveWriting\": \"लेखन सुधारित करा\",\n      \"summarize\": \"सारांश द्या\",\n      \"explain\": \"स्पष्टीकरण द्या\",\n      \"makeShorter\": \"लहान करा\",\n      \"makeLonger\": \"मोठे करा\"\n    },\n      \"autoGeneratorMenuItemName\": \"AI लेखक\",\n\"autoGeneratorTitleName\": \"AI: काहीही लिहिण्यासाठी AI ला विचारा...\",\n\"autoGeneratorLearnMore\": \"अधिक जाणून घ्या\",\n\"autoGeneratorGenerate\": \"उत्पन्न करा\",\n\"autoGeneratorHintText\": \"AI ला विचारा...\",\n\"autoGeneratorCantGetOpenAIKey\": \"AI की मिळवू शकलो नाही\",\n\"autoGeneratorRewrite\": \"पुन्हा लिहा\",\n\"smartEdit\": \"AI ला विचारा\",\n\"aI\": \"AI\",\n\"smartEditFixSpelling\": \"स्पेलिंग आणि व्याकरण सुधारा\",\n\"warning\": \"⚠️ AI उत्तरं चुकीची किंवा दिशाभूल करणारी असू शकतात.\",\n\"smartEditSummarize\": \"सारांश द्या\",\n\"smartEditImproveWriting\": \"लेखन सुधारित करा\",\n\"smartEditMakeLonger\": \"लांब करा\",\n\"smartEditCouldNotFetchResult\": \"AI कडून उत्तर मिळवता आले नाही\",\n\"smartEditCouldNotFetchKey\": \"AI की मिळवता आली नाही\",\n\"smartEditDisabled\": \"सेटिंग्जमध्ये AI कनेक्ट करा\",\n\"appflowyAIEditDisabled\": \"AI वैशिष्ट्ये सक्षम करण्यासाठी साइन इन करा\",\n\"discardResponse\": \"AI उत्तर फेकून द्यायचं आहे का?\",\n\"createInlineMathEquation\": \"समीकरण तयार करा\",\n\"fonts\": \"फॉन्ट्स\",\n\"insertDate\": \"तारीख जोडा\",\n\"emoji\": \"इमोजी\",\n\"toggleList\": \"टॉगल यादी\",\n\"emptyToggleHeading\": \"रिकामे टॉगल h{}. मजकूर जोडण्यासाठी क्लिक करा.\",\n\"emptyToggleList\": \"रिकामी टॉगल यादी. मजकूर जोडण्यासाठी क्लिक करा.\",\n\"emptyToggleHeadingWeb\": \"रिकामे टॉगल h{level}. मजकूर जोडण्यासाठी क्लिक करा\",\n\"quoteList\": \"उद्धरण यादी\",\n\"numberedList\": \"क्रमांकित यादी\",\n\"bulletedList\": \"बुलेट यादी\",\n\"todoList\": \"करण्याची यादी\",\n\"callout\": \"ठळक मजकूर\",\n\"simpleTable\": {\n  \"moreActions\": {\n    \"color\": \"रंग\",\n    \"align\": \"पंक्तिबद्ध करा\",\n    \"delete\": \"हटा\",\n    \"duplicate\": \"डुप्लिकेट करा\",\n    \"insertLeft\": \"डावीकडे घाला\",\n    \"insertRight\": \"उजवीकडे घाला\",\n    \"insertAbove\": \"वर घाला\",\n    \"insertBelow\": \"खाली घाला\",\n    \"headerColumn\": \"हेडर स्तंभ\",\n    \"headerRow\": \"हेडर ओळ\",\n    \"clearContents\": \"सामग्री साफ करा\",\n    \"setToPageWidth\": \"पृष्ठाच्या रुंदीप्रमाणे सेट करा\",\n    \"distributeColumnsWidth\": \"स्तंभ समान करा\",\n    \"duplicateRow\": \"ओळ डुप्लिकेट करा\",\n    \"duplicateColumn\": \"स्तंभ डुप्लिकेट करा\",\n    \"textColor\": \"मजकूराचा रंग\",\n    \"cellBackgroundColor\": \"सेलचा पार्श्वभूमी रंग\",\n    \"duplicateTable\": \"टेबल डुप्लिकेट करा\"\n  },\n  \"clickToAddNewRow\": \"नवीन ओळ जोडण्यासाठी क्लिक करा\",\n  \"clickToAddNewColumn\": \"नवीन स्तंभ जोडण्यासाठी क्लिक करा\",\n  \"clickToAddNewRowAndColumn\": \"नवीन ओळ आणि स्तंभ जोडण्यासाठी क्लिक करा\",\n  \"headerName\": {\n    \"table\": \"टेबल\",\n    \"alignText\": \"मजकूर पंक्तिबद्ध करा\"\n  }\n},\n\"cover\": {\n  \"changeCover\": \"कव्हर बदला\",\n  \"colors\": \"रंग\",\n  \"images\": \"प्रतिमा\",\n  \"clearAll\": \"सर्व साफ करा\",\n  \"abstract\": \"ऍबस्ट्रॅक्ट\",\n  \"addCover\": \"कव्हर जोडा\",\n  \"addLocalImage\": \"स्थानिक प्रतिमा जोडा\",\n  \"invalidImageUrl\": \"अवैध प्रतिमा URL\",\n  \"failedToAddImageToGallery\": \"प्रतिमा गॅलरीत जोडता आली नाही\",\n  \"enterImageUrl\": \"प्रतिमा URL लिहा\",\n  \"add\": \"जोडा\",\n  \"back\": \"मागे\",\n  \"saveToGallery\": \"गॅलरीत जतन करा\",\n  \"removeIcon\": \"आयकॉन काढा\",\n  \"removeCover\": \"कव्हर काढा\",\n  \"pasteImageUrl\": \"प्रतिमा URL पेस्ट करा\",\n  \"or\": \"किंवा\",\n  \"pickFromFiles\": \"फाईल्समधून निवडा\",\n  \"couldNotFetchImage\": \"प्रतिमा मिळवता आली नाही\",\n  \"imageSavingFailed\": \"प्रतिमा जतन करणे अयशस्वी\",\n  \"addIcon\": \"आयकॉन जोडा\",\n  \"changeIcon\": \"आयकॉन बदला\",\n  \"coverRemoveAlert\": \"हे हटवल्यानंतर कव्हरमधून काढले जाईल.\",\n  \"alertDialogConfirmation\": \"तुम्हाला खात्री आहे का? तुम्हाला पुढे जायचे आहे?\"\n},\n\"mathEquation\": {\n  \"name\": \"गणिती समीकरण\",\n  \"addMathEquation\": \"TeX समीकरण जोडा\",\n  \"editMathEquation\": \"गणिती समीकरण संपादित करा\"\n},\n\"optionAction\": {\n  \"click\": \"क्लिक\",\n  \"toOpenMenu\": \"मेनू उघडण्यासाठी\",\n  \"drag\": \"ओढा\",\n  \"toMove\": \"हलवण्यासाठी\",\n  \"delete\": \"हटा\",\n  \"duplicate\": \"डुप्लिकेट करा\",\n  \"turnInto\": \"मध्ये बदला\",\n  \"moveUp\": \"वर हलवा\",\n  \"moveDown\": \"खाली हलवा\",\n  \"color\": \"रंग\",\n  \"align\": \"पंक्तिबद्ध करा\",\n  \"left\": \"डावीकडे\",\n  \"center\": \"मध्यभागी\",\n  \"right\": \"उजवीकडे\",\n  \"defaultColor\": \"डिफॉल्ट\",\n  \"depth\": \"खोली\",\n  \"copyLinkToBlock\": \"ब्लॉकसाठी लिंक कॉपी करा\"\n},\n      \"image\": {\n  \"addAnImage\": \"प्रतिमा जोडा\",\n  \"copiedToPasteBoard\": \"प्रतिमेची लिंक क्लिपबोर्डवर कॉपी केली गेली आहे\",\n  \"addAnImageDesktop\": \"प्रतिमा जोडा\",\n  \"addAnImageMobile\": \"एक किंवा अधिक प्रतिमा जोडण्यासाठी क्लिक करा\",\n  \"dropImageToInsert\": \"प्रतिमा ड्रॉप करून जोडा\",\n  \"imageUploadFailed\": \"प्रतिमा अपलोड करण्यात अयशस्वी\",\n  \"imageDownloadFailed\": \"प्रतिमा डाउनलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा\",\n  \"imageDownloadFailedToken\": \"युजर टोकन नसल्यामुळे प्रतिमा डाउनलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा\",\n  \"errorCode\": \"त्रुटी कोड\"\n},\n\"photoGallery\": {\n  \"name\": \"फोटो गॅलरी\",\n  \"imageKeyword\": \"प्रतिमा\",\n  \"imageGalleryKeyword\": \"प्रतिमा गॅलरी\",\n  \"photoKeyword\": \"फोटो\",\n  \"photoBrowserKeyword\": \"फोटो ब्राउझर\",\n  \"galleryKeyword\": \"गॅलरी\",\n  \"addImageTooltip\": \"प्रतिमा जोडा\",\n  \"changeLayoutTooltip\": \"लेआउट बदला\",\n  \"browserLayout\": \"ब्राउझर\",\n  \"gridLayout\": \"ग्रिड\",\n  \"deleteBlockTooltip\": \"संपूर्ण गॅलरी हटवा\"\n},\n\"math\": {\n  \"copiedToPasteBoard\": \"समीकरण क्लिपबोर्डवर कॉपी केले गेले आहे\"\n},\n\"urlPreview\": {\n  \"copiedToPasteBoard\": \"लिंक क्लिपबोर्डवर कॉपी केली गेली आहे\",\n  \"convertToLink\": \"एंबेड लिंकमध्ये रूपांतर करा\"\n},\n\"outline\": {\n  \"addHeadingToCreateOutline\": \"सामग्री यादी तयार करण्यासाठी शीर्षके जोडा.\",\n  \"noMatchHeadings\": \"जुळणारी शीर्षके आढळली नाहीत.\"\n},\n\"table\": {\n  \"addAfter\": \"नंतर जोडा\",\n  \"addBefore\": \"आधी जोडा\",\n  \"delete\": \"हटा\",\n  \"clear\": \"सामग्री साफ करा\",\n  \"duplicate\": \"डुप्लिकेट करा\",\n  \"bgColor\": \"पार्श्वभूमीचा रंग\"\n},\n\"contextMenu\": {\n  \"copy\": \"कॉपी करा\",\n  \"cut\": \"कापा\",\n  \"paste\": \"पेस्ट करा\",\n  \"pasteAsPlainText\": \"साध्या मजकूराच्या स्वरूपात पेस्ट करा\"\n},\n\"action\": \"कृती\",\n\"database\": {\n  \"selectDataSource\": \"डेटा स्रोत निवडा\",\n  \"noDataSource\": \"डेटा स्रोत नाही\",\n  \"selectADataSource\": \"डेटा स्रोत निवडा\",\n  \"toContinue\": \"पुढे जाण्यासाठी\",\n  \"newDatabase\": \"नवीन डेटाबेस\",\n  \"linkToDatabase\": \"डेटाबेसशी लिंक करा\"\n},\n\"date\": \"तारीख\",\n\"video\": {\n  \"label\": \"व्हिडिओ\",\n  \"emptyLabel\": \"व्हिडिओ जोडा\",\n  \"placeholder\": \"व्हिडिओ लिंक पेस्ट करा\",\n  \"copiedToPasteBoard\": \"व्हिडिओ लिंक क्लिपबोर्डवर कॉपी केली गेली आहे\",\n  \"insertVideo\": \"व्हिडिओ जोडा\",\n  \"invalidVideoUrl\": \"ही URL सध्या समर्थित नाही.\",\n  \"invalidVideoUrlYouTube\": \"YouTube सध्या समर्थित नाही.\",\n  \"supportedFormats\": \"समर्थित स्वरूप: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264\"\n},\n\"file\": {\n  \"name\": \"फाईल\",\n  \"uploadTab\": \"अपलोड\",\n  \"uploadMobile\": \"फाईल निवडा\",\n  \"uploadMobileGallery\": \"फोटो गॅलरीमधून\",\n  \"networkTab\": \"लिंक एम्बेड करा\",\n  \"placeholderText\": \"फाईल अपलोड किंवा एम्बेड करा\",\n  \"placeholderDragging\": \"फाईल ड्रॉप करून अपलोड करा\",\n  \"dropFileToUpload\": \"फाईल ड्रॉप करून अपलोड करा\",\n  \"fileUploadHint\": \"फाईल ड्रॅग आणि ड्रॉप करा किंवा क्लिक करा \",\n  \"fileUploadHintSuffix\": \"ब्राउझ करा\",\n  \"networkHint\": \"फाईल लिंक पेस्ट करा\",\n  \"networkUrlInvalid\": \"अवैध URL. कृपया तपासा आणि पुन्हा प्रयत्न करा.\",\n  \"networkAction\": \"एम्बेड\",\n  \"fileTooBigError\": \"फाईल खूप मोठी आहे, कृपया 10MB पेक्षा कमी फाईल अपलोड करा\",\n  \"renameFile\": {\n    \"title\": \"फाईलचे नाव बदला\",\n    \"description\": \"या फाईलसाठी नवीन नाव लिहा\",\n    \"nameEmptyError\": \"फाईलचे नाव रिकामे असू शकत नाही.\"\n  },\n  \"uploadedAt\": \"{} रोजी अपलोड केले\",\n  \"linkedAt\": \"{} रोजी लिंक जोडली\",\n  \"failedToOpenMsg\": \"उघडण्यात अयशस्वी, फाईल सापडली नाही\"\n},\n\"subPage\": {\n  \"handlingPasteHint\": \" - (पेस्ट प्रक्रिया करत आहे)\",\n  \"errors\": {\n    \"failedDeletePage\": \"पृष्ठ हटवण्यात अयशस्वी\",\n    \"failedCreatePage\": \"पृष्ठ तयार करण्यात अयशस्वी\",\n    \"failedMovePage\": \"हे पृष्ठ हलवण्यात अयशस्वी\",\n    \"failedDuplicatePage\": \"पृष्ठ डुप्लिकेट करण्यात अयशस्वी\",\n    \"failedDuplicateFindView\": \"पृष्ठ डुप्लिकेट करण्यात अयशस्वी - मूळ दृश्य सापडले नाही\"\n  }\n},\n     \"cannotMoveToItsChildren\": \"मुलांमध्ये हलवू शकत नाही\"\n},\n\"outlineBlock\": {\n  \"placeholder\": \"सामग्री सूची\"\n},\n\"textBlock\": {\n  \"placeholder\": \"कमांडसाठी '/' टाइप करा\"\n},\n\"title\": {\n  \"placeholder\": \"शीर्षक नाही\"\n},\n\"imageBlock\": {\n  \"placeholder\": \"प्रतिमा जोडण्यासाठी क्लिक करा\",\n  \"upload\": {\n    \"label\": \"अपलोड\",\n    \"placeholder\": \"प्रतिमा अपलोड करण्यासाठी क्लिक करा\"\n  },\n  \"url\": {\n    \"label\": \"प्रतिमेची URL\",\n    \"placeholder\": \"प्रतिमेची URL टाका\"\n  },\n  \"ai\": {\n    \"label\": \"AI द्वारे प्रतिमा तयार करा\",\n    \"placeholder\": \"AI द्वारे प्रतिमा तयार करण्यासाठी कृपया संकेत द्या\"\n  },\n  \"stability_ai\": {\n    \"label\": \"Stability AI द्वारे प्रतिमा तयार करा\",\n    \"placeholder\": \"Stability AI द्वारे प्रतिमा तयार करण्यासाठी कृपया संकेत द्या\"\n  },\n  \"support\": \"प्रतिमेचा कमाल आकार 5MB आहे. समर्थित स्वरूप: JPEG, PNG, GIF, SVG\",\n  \"error\": {\n    \"invalidImage\": \"अवैध प्रतिमा\",\n    \"invalidImageSize\": \"प्रतिमेचा आकार 5MB पेक्षा कमी असावा\",\n    \"invalidImageFormat\": \"प्रतिमेचे स्वरूप समर्थित नाही. समर्थित स्वरूप: JPEG, PNG, JPG, GIF, SVG, WEBP\",\n    \"invalidImageUrl\": \"अवैध प्रतिमेची URL\",\n    \"noImage\": \"अशी फाईल किंवा निर्देशिका नाही\",\n    \"multipleImagesFailed\": \"एक किंवा अधिक प्रतिमा अपलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा\"\n  },\n  \"embedLink\": {\n    \"label\": \"लिंक एम्बेड करा\",\n    \"placeholder\": \"प्रतिमेची लिंक पेस्ट करा किंवा टाका\"\n  },\n  \"unsplash\": {\n    \"label\": \"Unsplash\"\n  },\n  \"searchForAnImage\": \"प्रतिमा शोधा\",\n  \"pleaseInputYourOpenAIKey\": \"कृपया सेटिंग्ज पृष्ठात आपला AI की प्रविष्ट करा\",\n  \"saveImageToGallery\": \"प्रतिमा जतन करा\",\n  \"failedToAddImageToGallery\": \"प्रतिमा जतन करण्यात अयशस्वी\",\n  \"successToAddImageToGallery\": \"प्रतिमा 'Photos' मध्ये जतन केली\",\n  \"unableToLoadImage\": \"प्रतिमा लोड करण्यात अयशस्वी\",\n  \"maximumImageSize\": \"कमाल प्रतिमा अपलोड आकार 10MB आहे\",\n  \"uploadImageErrorImageSizeTooBig\": \"प्रतिमेचा आकार 10MB पेक्षा कमी असावा\",\n  \"imageIsUploading\": \"प्रतिमा अपलोड होत आहे\",\n  \"openFullScreen\": \"पूर्ण स्क्रीनमध्ये उघडा\",\n  \"interactiveViewer\": {\n    \"toolbar\": {\n      \"previousImageTooltip\": \"मागील प्रतिमा\",\n      \"nextImageTooltip\": \"पुढील प्रतिमा\",\n      \"zoomOutTooltip\": \"लहान करा\",\n      \"zoomInTooltip\": \"मोठी करा\",\n      \"changeZoomLevelTooltip\": \"झूम पातळी बदला\",\n      \"openLocalImage\": \"प्रतिमा उघडा\",\n      \"downloadImage\": \"प्रतिमा डाउनलोड करा\",\n      \"closeViewer\": \"इंटरअॅक्टिव्ह व्ह्युअर बंद करा\",\n      \"scalePercentage\": \"{}%\",\n      \"deleteImageTooltip\": \"प्रतिमा हटवा\"\n    }\n  }\n},\n    \"codeBlock\": {\n  \"language\": {\n    \"label\": \"भाषा\",\n    \"placeholder\": \"भाषा निवडा\",\n    \"auto\": \"स्वयंचलित\"\n  },\n  \"copyTooltip\": \"कॉपी करा\",\n  \"searchLanguageHint\": \"भाषा शोधा\",\n  \"codeCopiedSnackbar\": \"कोड क्लिपबोर्डवर कॉपी झाला!\"\n},\n\"inlineLink\": {\n  \"placeholder\": \"लिंक पेस्ट करा किंवा टाका\",\n  \"openInNewTab\": \"नवीन टॅबमध्ये उघडा\",\n  \"copyLink\": \"लिंक कॉपी करा\",\n  \"removeLink\": \"लिंक काढा\",\n  \"url\": {\n    \"label\": \"लिंक URL\",\n    \"placeholder\": \"लिंक URL टाका\"\n  },\n  \"title\": {\n    \"label\": \"लिंक शीर्षक\",\n    \"placeholder\": \"लिंक शीर्षक टाका\"\n  }\n},\n\"mention\": {\n  \"placeholder\": \"कोणीतरी, पृष्ठ किंवा तारीख नमूद करा...\",\n  \"page\": {\n    \"label\": \"पृष्ठाला लिंक करा\",\n    \"tooltip\": \"पृष्ठ उघडण्यासाठी क्लिक करा\"\n  },\n  \"deleted\": \"हटवले गेले\",\n  \"deletedContent\": \"ही सामग्री अस्तित्वात नाही किंवा हटवण्यात आली आहे\",\n  \"noAccess\": \"प्रवेश नाही\",\n  \"deletedPage\": \"हटवलेले पृष्ठ\",\n  \"trashHint\": \" - ट्रॅशमध्ये\",\n  \"morePages\": \"अजून पृष्ठे\"\n},\n\"toolbar\": {\n  \"resetToDefaultFont\": \"डीफॉल्ट फॉन्टवर परत जा\",\n  \"textSize\": \"मजकूराचा आकार\",\n  \"textColor\": \"मजकूराचा रंग\",\n  \"h1\": \"मथळा 1\",\n  \"h2\": \"मथळा 2\",\n  \"h3\": \"मथळा 3\",\n  \"alignLeft\": \"डावीकडे संरेखित करा\",\n  \"alignRight\": \"उजवीकडे संरेखित करा\",\n  \"alignCenter\": \"मध्यभागी संरेखित करा\",\n  \"link\": \"लिंक\",\n  \"textAlign\": \"मजकूर संरेखन\",\n  \"moreOptions\": \"अधिक पर्याय\",\n  \"font\": \"फॉन्ट\",\n  \"inlineCode\": \"इनलाइन कोड\",\n  \"suggestions\": \"सूचना\",\n  \"turnInto\": \"मध्ये रूपांतरित करा\",\n  \"equation\": \"समीकरण\",\n  \"insert\": \"घाला\",\n  \"linkInputHint\": \"लिंक पेस्ट करा किंवा पृष्ठे शोधा\",\n  \"pageOrURL\": \"पृष्ठ किंवा URL\",\n  \"linkName\": \"लिंकचे नाव\",\n  \"linkNameHint\": \"लिंकचे नाव प्रविष्ट करा\"\n},\n\"errorBlock\": {\n  \"theBlockIsNotSupported\": \"ब्लॉक सामग्री पार्स करण्यात अक्षम\",\n  \"clickToCopyTheBlockContent\": \"ब्लॉक सामग्री कॉपी करण्यासाठी क्लिक करा\",\n  \"blockContentHasBeenCopied\": \"ब्लॉक सामग्री कॉपी केली आहे.\",\n  \"parseError\": \"{} ब्लॉक पार्स करताना त्रुटी आली.\",\n  \"copyBlockContent\": \"ब्लॉक सामग्री कॉपी करा\"\n},\n\"mobilePageSelector\": {\n  \"title\": \"पृष्ठ निवडा\",\n  \"failedToLoad\": \"पृष्ठ यादी लोड करण्यात अयशस्वी\",\n  \"noPagesFound\": \"कोणतीही पृष्ठे सापडली नाहीत\"\n},\n\"attachmentMenu\": {\n  \"choosePhoto\": \"फोटो निवडा\",\n  \"takePicture\": \"फोटो काढा\",\n  \"chooseFile\": \"फाईल निवडा\"\n  }\n  },\n  \"board\": {\n  \"column\": {\n    \"label\": \"स्तंभ\",\n    \"createNewCard\": \"नवीन\",\n    \"renameGroupTooltip\": \"गटाचे नाव बदलण्यासाठी क्लिक करा\",\n    \"createNewColumn\": \"नवीन गट जोडा\",\n    \"addToColumnTopTooltip\": \"वर नवीन कार्ड जोडा\",\n    \"addToColumnBottomTooltip\": \"खाली नवीन कार्ड जोडा\",\n    \"renameColumn\": \"स्तंभाचे नाव बदला\",\n    \"hideColumn\": \"लपवा\",\n    \"newGroup\": \"नवीन गट\",\n    \"deleteColumn\": \"हटवा\",\n    \"deleteColumnConfirmation\": \"हा गट आणि त्यामधील सर्व कार्ड्स हटवले जातील. तुम्हाला खात्री आहे का?\"\n  },\n  \"hiddenGroupSection\": {\n    \"sectionTitle\": \"लपवलेले गट\",\n    \"collapseTooltip\": \"लपवलेले गट लपवा\",\n    \"expandTooltip\": \"लपवलेले गट पाहा\"\n  },\n  \"cardDetail\": \"कार्ड तपशील\",\n  \"cardActions\": \"कार्ड क्रिया\",\n  \"cardDuplicated\": \"कार्डची प्रत तयार झाली\",\n  \"cardDeleted\": \"कार्ड हटवले गेले\",\n  \"showOnCard\": \"कार्ड तपशिलावर दाखवा\",\n  \"setting\": \"सेटिंग\",\n  \"propertyName\": \"गुणधर्माचे नाव\",\n  \"menuName\": \"बोर्ड\",\n  \"showUngrouped\": \"गटात नसलेली कार्ड्स दाखवा\",\n  \"ungroupedButtonText\": \"गट नसलेली\",\n  \"ungroupedButtonTooltip\": \"ज्या कार्ड्स कोणत्याही गटात नाहीत\",\n  \"ungroupedItemsTitle\": \"बोर्डमध्ये जोडण्यासाठी क्लिक करा\",\n  \"groupBy\": \"या आधारावर गट करा\",\n  \"groupCondition\": \"गट स्थिती\",\n  \"referencedBoardPrefix\": \"याचे दृश्य\",\n  \"notesTooltip\": \"नोट्स आहेत\",\n  \"mobile\": {\n    \"editURL\": \"URL संपादित करा\",\n    \"showGroup\": \"गट दाखवा\",\n    \"showGroupContent\": \"हा गट बोर्डवर दाखवायचा आहे का?\",\n    \"failedToLoad\": \"बोर्ड दृश्य लोड होण्यात अयशस्वी\"\n  },\n  \"dateCondition\": {\n    \"weekOf\": \"{} - {} ची आठवडा\",\n    \"today\": \"आज\",\n    \"yesterday\": \"काल\",\n    \"tomorrow\": \"उद्या\",\n    \"lastSevenDays\": \"शेवटचे ७ दिवस\",\n    \"nextSevenDays\": \"पुढील ७ दिवस\",\n    \"lastThirtyDays\": \"शेवटचे ३० दिवस\",\n    \"nextThirtyDays\": \"पुढील ३० दिवस\"\n  },\n  \"noGroup\": \"गटासाठी कोणताही गुणधर्म निवडलेला नाही\",\n  \"noGroupDesc\": \"बोर्ड दृश्य दाखवण्यासाठी गट करण्याचा एक गुणधर्म आवश्यक आहे\",\n  \"media\": {\n    \"cardText\": \"{} {}\",\n    \"fallbackName\": \"फायली\"\n  }\n},\n  \"calendar\": {\n  \"menuName\": \"कॅलेंडर\",\n  \"defaultNewCalendarTitle\": \"नाव नाही\",\n  \"newEventButtonTooltip\": \"नवीन इव्हेंट जोडा\",\n  \"navigation\": {\n    \"today\": \"आज\",\n    \"jumpToday\": \"आजवर जा\",\n    \"previousMonth\": \"मागील महिना\",\n    \"nextMonth\": \"पुढील महिना\",\n    \"views\": {\n      \"day\": \"दिवस\",\n      \"week\": \"आठवडा\",\n      \"month\": \"महिना\",\n      \"year\": \"वर्ष\"\n    }\n  },\n  \"mobileEventScreen\": {\n    \"emptyTitle\": \"सध्या कोणतेही इव्हेंट नाहीत\",\n    \"emptyBody\": \"या दिवशी इव्हेंट तयार करण्यासाठी प्लस बटणावर क्लिक करा.\"\n  },\n  \"settings\": {\n    \"showWeekNumbers\": \"आठवड्याचे क्रमांक दाखवा\",\n    \"showWeekends\": \"सप्ताहांत दाखवा\",\n    \"firstDayOfWeek\": \"आठवड्याची सुरुवात\",\n    \"layoutDateField\": \"कॅलेंडर मांडणी दिनांकानुसार\",\n    \"changeLayoutDateField\": \"मांडणी फील्ड बदला\",\n    \"noDateTitle\": \"तारीख नाही\",\n    \"noDateHint\": {\n      \"zero\": \"नियोजित नसलेली इव्हेंट्स येथे दिसतील\",\n      \"one\": \"{count} नियोजित नसलेली इव्हेंट\",\n      \"other\": \"{count} नियोजित नसलेल्या इव्हेंट्स\"\n    },\n    \"unscheduledEventsTitle\": \"नियोजित नसलेल्या इव्हेंट्स\",\n    \"clickToAdd\": \"कॅलेंडरमध्ये जोडण्यासाठी क्लिक करा\",\n    \"name\": \"कॅलेंडर सेटिंग्ज\",\n    \"clickToOpen\": \"रेकॉर्ड उघडण्यासाठी क्लिक करा\"\n  },\n  \"referencedCalendarPrefix\": \"याचे दृश्य\",\n  \"quickJumpYear\": \"या वर्षावर जा\",\n  \"duplicateEvent\": \"इव्हेंट डुप्लिकेट करा\"\n},\n  \"errorDialog\": {\n  \"title\": \"@:appName त्रुटी\",\n  \"howToFixFallback\": \"या गैरसोयीबद्दल आम्ही दिलगीर आहोत! कृपया GitHub पेजवर त्रुटीबद्दल माहिती देणारे एक issue सबमिट करा.\",\n  \"howToFixFallbackHint1\": \"या गैरसोयीबद्दल आम्ही दिलगीर आहोत! कृपया \",\n  \"howToFixFallbackHint2\": \" पेजवर त्रुटीचे वर्णन करणारे issue सबमिट करा.\",\n  \"github\": \"GitHub वर पहा\"\n},\n\"search\": {\n  \"label\": \"शोध\",\n  \"sidebarSearchIcon\": \"पृष्ठ शोधा आणि पटकन जा\",\n  \"placeholder\": {\n    \"actions\": \"कृती शोधा...\"\n  }\n},\n\"message\": {\n  \"copy\": {\n    \"success\": \"कॉपी झाले!\",\n    \"fail\": \"कॉपी करू शकत नाही\"\n  }\n},\n\"unSupportBlock\": \"सध्याचा व्हर्जन या ब्लॉकला समर्थन देत नाही.\",\n\"views\": {\n  \"deleteContentTitle\": \"तुम्हाला हे {pageType} हटवायचे आहे का?\",\n  \"deleteContentCaption\": \"हे {pageType} हटवल्यास, तुम्ही ते trash मधून पुनर्संचयित करू शकता.\"\n},\n  \"colors\": {\n  \"custom\": \"सानुकूल\",\n  \"default\": \"डीफॉल्ट\",\n  \"red\": \"लाल\",\n  \"orange\": \"संत्रा\",\n  \"yellow\": \"पिवळा\",\n  \"green\": \"हिरवा\",\n  \"blue\": \"निळा\",\n  \"purple\": \"जांभळा\",\n  \"pink\": \"गुलाबी\",\n  \"brown\": \"तपकिरी\",\n  \"gray\": \"करड्या रंगाचा\"\n},\n  \"emoji\": {\n  \"emojiTab\": \"इमोजी\",\n  \"search\": \"इमोजी शोधा\",\n  \"noRecent\": \"अलीकडील कोणतेही इमोजी नाहीत\",\n  \"noEmojiFound\": \"कोणतेही इमोजी सापडले नाहीत\",\n  \"filter\": \"फिल्टर\",\n  \"random\": \"योगायोगाने\",\n  \"selectSkinTone\": \"त्वचेचा टोन निवडा\",\n  \"remove\": \"इमोजी काढा\",\n  \"categories\": {\n    \"smileys\": \"स्मायली आणि भावना\",\n    \"people\": \"लोक\",\n    \"animals\": \"प्राणी आणि निसर्ग\",\n    \"food\": \"अन्न\",\n    \"activities\": \"क्रिया\",\n    \"places\": \"स्थळे\",\n    \"objects\": \"वस्तू\",\n    \"symbols\": \"चिन्हे\",\n    \"flags\": \"ध्वज\",\n    \"nature\": \"निसर्ग\",\n    \"frequentlyUsed\": \"नेहमी वापरलेले\"\n  },\n  \"skinTone\": {\n    \"default\": \"डीफॉल्ट\",\n    \"light\": \"हलका\",\n    \"mediumLight\": \"मध्यम-हलका\",\n    \"medium\": \"मध्यम\",\n    \"mediumDark\": \"मध्यम-गडद\",\n    \"dark\": \"गडद\"\n  },\n  \"openSourceIconsFrom\": \"खुल्या स्रोताचे आयकॉन्स\"\n},\n  \"inlineActions\": {\n  \"noResults\": \"निकाल नाही\",\n  \"recentPages\": \"अलीकडील पृष्ठे\",\n  \"pageReference\": \"पृष्ठ संदर्भ\",\n  \"docReference\": \"दस्तऐवज संदर्भ\",\n  \"boardReference\": \"बोर्ड संदर्भ\",\n  \"calReference\": \"कॅलेंडर संदर्भ\",\n  \"gridReference\": \"ग्रिड संदर्भ\",\n  \"date\": \"तारीख\",\n  \"reminder\": {\n    \"groupTitle\": \"स्मरणपत्र\",\n    \"shortKeyword\": \"remind\"\n  },\n  \"createPage\": \"\\\"{}\\\" उप-पृष्ठ तयार करा\"\n},\n  \"datePicker\": {\n  \"dateTimeFormatTooltip\": \"सेटिंग्जमध्ये तारीख आणि वेळ फॉरमॅट बदला\",\n  \"dateFormat\": \"तारीख फॉरमॅट\",\n  \"includeTime\": \"वेळ समाविष्ट करा\",\n  \"isRange\": \"शेवटची तारीख\",\n  \"timeFormat\": \"वेळ फॉरमॅट\",\n  \"clearDate\": \"तारीख साफ करा\",\n  \"reminderLabel\": \"स्मरणपत्र\",\n  \"selectReminder\": \"स्मरणपत्र निवडा\",\n  \"reminderOptions\": {\n    \"none\": \"काहीही नाही\",\n    \"atTimeOfEvent\": \"इव्हेंटच्या वेळी\",\n    \"fiveMinsBefore\": \"५ मिनिटे आधी\",\n    \"tenMinsBefore\": \"१० मिनिटे आधी\",\n    \"fifteenMinsBefore\": \"१५ मिनिटे आधी\",\n    \"thirtyMinsBefore\": \"३० मिनिटे आधी\",\n    \"oneHourBefore\": \"१ तास आधी\",\n    \"twoHoursBefore\": \"२ तास आधी\",\n    \"onDayOfEvent\": \"इव्हेंटच्या दिवशी\",\n    \"oneDayBefore\": \"१ दिवस आधी\",\n    \"twoDaysBefore\": \"२ दिवस आधी\",\n    \"oneWeekBefore\": \"१ आठवडा आधी\",\n    \"custom\": \"सानुकूल\"\n  }\n},\n  \"relativeDates\": {\n  \"yesterday\": \"काल\",\n  \"today\": \"आज\",\n  \"tomorrow\": \"उद्या\",\n  \"oneWeek\": \"१ आठवडा\"\n},\n  \"notificationHub\": {\n  \"title\": \"सूचना\",\n  \"mobile\": {\n    \"title\": \"अपडेट्स\"\n  },\n  \"emptyTitle\": \"सर्व पूर्ण झाले!\",\n  \"emptyBody\": \"कोणतीही प्रलंबित सूचना किंवा कृती नाहीत. शांततेचा आनंद घ्या.\",\n  \"tabs\": {\n    \"inbox\": \"इनबॉक्स\",\n    \"upcoming\": \"आगामी\"\n  },\n  \"actions\": {\n    \"markAllRead\": \"सर्व वाचलेल्या म्हणून चिन्हित करा\",\n    \"showAll\": \"सर्व\",\n    \"showUnreads\": \"न वाचलेल्या\"\n  },\n  \"filters\": {\n    \"ascending\": \"आरोही\",\n    \"descending\": \"अवरोही\",\n    \"groupByDate\": \"तारीखेनुसार गटबद्ध करा\",\n    \"showUnreadsOnly\": \"फक्त न वाचलेल्या दाखवा\",\n    \"resetToDefault\": \"डीफॉल्टवर रीसेट करा\"\n  }\n},\n  \"reminderNotification\": {\n  \"title\": \"स्मरणपत्र\",\n  \"message\": \"तुम्ही विसरण्याआधी हे तपासण्याचे लक्षात ठेवा!\",\n  \"tooltipDelete\": \"हटवा\",\n  \"tooltipMarkRead\": \"वाचले म्हणून चिन्हित करा\",\n  \"tooltipMarkUnread\": \"न वाचले म्हणून चिन्हित करा\"\n},\n  \"findAndReplace\": {\n  \"find\": \"शोधा\",\n  \"previousMatch\": \"मागील जुळणारे\",\n  \"nextMatch\": \"पुढील जुळणारे\",\n  \"close\": \"बंद करा\",\n  \"replace\": \"बदला\",\n  \"replaceAll\": \"सर्व बदला\",\n  \"noResult\": \"कोणतेही निकाल नाहीत\",\n  \"caseSensitive\": \"केस सेंसिटिव्ह\",\n  \"searchMore\": \"अधिक निकालांसाठी शोधा\"\n},\n  \"error\": {\n  \"weAreSorry\": \"आम्ही क्षमस्व आहोत\",\n  \"loadingViewError\": \"हे दृश्य लोड करण्यात अडचण येत आहे. कृपया तुमचे इंटरनेट कनेक्शन तपासा, अ‍ॅप रीफ्रेश करा, आणि समस्या कायम असल्यास आमच्याशी संपर्क साधा.\",\n  \"syncError\": \"इतर डिव्हाइसमधून डेटा सिंक झाला नाही\",\n  \"syncErrorHint\": \"कृपया हे पृष्ठ शेवटचे जिथे संपादित केले होते त्या डिव्हाइसवर उघडा, आणि मग सध्याच्या डिव्हाइसवर पुन्हा उघडा.\",\n  \"clickToCopy\": \"एरर कोड कॉपी करण्यासाठी क्लिक करा\"\n},\n  \"editor\": {\n  \"bold\": \"जाड\",\n  \"bulletedList\": \"बुलेट यादी\",\n  \"bulletedListShortForm\": \"बुलेट\",\n  \"checkbox\": \"चेकबॉक्स\",\n  \"embedCode\": \"कोड एम्बेड करा\",\n  \"heading1\": \"H1\",\n  \"heading2\": \"H2\",\n  \"heading3\": \"H3\",\n  \"highlight\": \"हायलाइट\",\n  \"color\": \"रंग\",\n  \"image\": \"प्रतिमा\",\n  \"date\": \"तारीख\",\n  \"page\": \"पृष्ठ\",\n  \"italic\": \"तिरका\",\n  \"link\": \"लिंक\",\n  \"numberedList\": \"क्रमांकित यादी\",\n  \"numberedListShortForm\": \"क्रमांकित\",\n  \"toggleHeading1ShortForm\": \"Toggle H1\",\n  \"toggleHeading2ShortForm\": \"Toggle H2\",\n  \"toggleHeading3ShortForm\": \"Toggle H3\",\n  \"quote\": \"कोट\",\n  \"strikethrough\": \"ओढून टाका\",\n  \"text\": \"मजकूर\",\n  \"underline\": \"अधोरेखित\",\n  \"fontColorDefault\": \"डीफॉल्ट\",\n  \"fontColorGray\": \"धूसर\",\n  \"fontColorBrown\": \"तपकिरी\",\n  \"fontColorOrange\": \"केशरी\",\n  \"fontColorYellow\": \"पिवळा\",\n  \"fontColorGreen\": \"हिरवा\",\n  \"fontColorBlue\": \"निळा\",\n  \"fontColorPurple\": \"जांभळा\",\n  \"fontColorPink\": \"पिंग\",\n  \"fontColorRed\": \"लाल\",\n  \"backgroundColorDefault\": \"डीफॉल्ट पार्श्वभूमी\",\n  \"backgroundColorGray\": \"धूसर पार्श्वभूमी\",\n  \"backgroundColorBrown\": \"तपकिरी पार्श्वभूमी\",\n  \"backgroundColorOrange\": \"केशरी पार्श्वभूमी\",\n  \"backgroundColorYellow\": \"पिवळी पार्श्वभूमी\",\n  \"backgroundColorGreen\": \"हिरवी पार्श्वभूमी\",\n  \"backgroundColorBlue\": \"निळी पार्श्वभूमी\",\n  \"backgroundColorPurple\": \"जांभळी पार्श्वभूमी\",\n  \"backgroundColorPink\": \"पिंग पार्श्वभूमी\",\n  \"backgroundColorRed\": \"लाल पार्श्वभूमी\",\n  \"backgroundColorLime\": \"लिंबू पार्श्वभूमी\",\n  \"backgroundColorAqua\": \"पाण्याचा पार्श्वभूमी\",\n  \"done\": \"पूर्ण\",\n  \"cancel\": \"रद्द करा\",\n  \"tint1\": \"टिंट 1\",\n  \"tint2\": \"टिंट 2\",\n  \"tint3\": \"टिंट 3\",\n  \"tint4\": \"टिंट 4\",\n  \"tint5\": \"टिंट 5\",\n  \"tint6\": \"टिंट 6\",\n  \"tint7\": \"टिंट 7\",\n  \"tint8\": \"टिंट 8\",\n  \"tint9\": \"टिंट 9\",\n  \"lightLightTint1\": \"जांभळा\",\n  \"lightLightTint2\": \"पिंग\",\n  \"lightLightTint3\": \"फिकट पिंग\",\n  \"lightLightTint4\": \"केशरी\",\n  \"lightLightTint5\": \"पिवळा\",\n  \"lightLightTint6\": \"लिंबू\",\n  \"lightLightTint7\": \"हिरवा\",\n  \"lightLightTint8\": \"पाणी\",\n  \"lightLightTint9\": \"निळा\",\n  \"urlHint\": \"URL\",\n  \"mobileHeading1\": \"Heading 1\",\n  \"mobileHeading2\": \"Heading 2\",\n  \"mobileHeading3\": \"Heading 3\",\n  \"mobileHeading4\": \"Heading 4\",\n  \"mobileHeading5\": \"Heading 5\",\n  \"mobileHeading6\": \"Heading 6\",\n  \"textColor\": \"मजकूराचा रंग\",\n  \"backgroundColor\": \"पार्श्वभूमीचा रंग\",\n  \"addYourLink\": \"तुमची लिंक जोडा\",\n  \"openLink\": \"लिंक उघडा\",\n  \"copyLink\": \"लिंक कॉपी करा\",\n  \"removeLink\": \"लिंक काढा\",\n  \"editLink\": \"लिंक संपादित करा\",\n  \"linkText\": \"मजकूर\",\n  \"linkTextHint\": \"कृपया मजकूर प्रविष्ट करा\",\n  \"linkAddressHint\": \"कृपया URL प्रविष्ट करा\",\n  \"highlightColor\": \"हायलाइट रंग\",\n  \"clearHighlightColor\": \"हायलाइट काढा\",\n  \"customColor\": \"स्वतःचा रंग\",\n  \"hexValue\": \"Hex मूल्य\",\n  \"opacity\": \"अपारदर्शकता\",\n  \"resetToDefaultColor\": \"डीफॉल्ट रंगावर रीसेट करा\",\n  \"ltr\": \"LTR\",\n  \"rtl\": \"RTL\",\n  \"auto\": \"स्वयंचलित\",\n  \"cut\": \"कट\",\n  \"copy\": \"कॉपी\",\n  \"paste\": \"पेस्ट\",\n  \"find\": \"शोधा\",\n  \"select\": \"निवडा\",\n  \"selectAll\": \"सर्व निवडा\",\n  \"previousMatch\": \"मागील जुळणारे\",\n  \"nextMatch\": \"पुढील जुळणारे\",\n  \"closeFind\": \"बंद करा\",\n  \"replace\": \"बदला\",\n  \"replaceAll\": \"सर्व बदला\",\n  \"regex\": \"Regex\",\n  \"caseSensitive\": \"केस सेंसिटिव्ह\",\n  \"uploadImage\": \"प्रतिमा अपलोड करा\",\n  \"urlImage\": \"URL प्रतिमा\",\n  \"incorrectLink\": \"चुकीची लिंक\",\n  \"upload\": \"अपलोड\",\n  \"chooseImage\": \"प्रतिमा निवडा\",\n  \"loading\": \"लोड करत आहे\",\n  \"imageLoadFailed\": \"प्रतिमा लोड करण्यात अयशस्वी\",\n  \"divider\": \"विभाजक\",\n  \"table\": \"तक्त्याचे स्वरूप\",\n  \"colAddBefore\": \"यापूर्वी स्तंभ जोडा\",\n  \"rowAddBefore\": \"यापूर्वी पंक्ती जोडा\",\n  \"colAddAfter\": \"यानंतर स्तंभ जोडा\",\n  \"rowAddAfter\": \"यानंतर पंक्ती जोडा\",\n  \"colRemove\": \"स्तंभ काढा\",\n  \"rowRemove\": \"पंक्ती काढा\",\n  \"colDuplicate\": \"स्तंभ डुप्लिकेट\",\n  \"rowDuplicate\": \"पंक्ती डुप्लिकेट\",\n  \"colClear\": \"सामग्री साफ करा\",\n  \"rowClear\": \"सामग्री साफ करा\",\n  \"slashPlaceHolder\": \"'/' टाइप करा आणि घटक जोडा किंवा टाइप सुरू करा\",\n  \"typeSomething\": \"काहीतरी लिहा...\",\n  \"toggleListShortForm\": \"टॉगल\",\n  \"quoteListShortForm\": \"कोट\",\n  \"mathEquationShortForm\": \"सूत्र\",\n  \"codeBlockShortForm\": \"कोड\"\n},\n  \"favorite\": {\n  \"noFavorite\": \"कोणतेही आवडते पृष्ठ नाही\",\n  \"noFavoriteHintText\": \"पृष्ठाला डावीकडे स्वाइप करा आणि ते आवडत्या यादीत जोडा\",\n  \"removeFromSidebar\": \"साइडबारमधून काढा\",\n  \"addToSidebar\": \"साइडबारमध्ये पिन करा\"\n},\n\"cardDetails\": {\n  \"notesPlaceholder\": \"/ टाइप करा ब्लॉक घालण्यासाठी, किंवा टाइप करायला सुरुवात करा\"\n},\n\"blockPlaceholders\": {\n  \"todoList\": \"करण्याची यादी\",\n  \"bulletList\": \"यादी\",\n  \"numberList\": \"क्रमांकित यादी\",\n  \"quote\": \"कोट\",\n  \"heading\": \"मथळा {}\"\n},\n\"titleBar\": {\n  \"pageIcon\": \"पृष्ठ चिन्ह\",\n  \"language\": \"भाषा\",\n  \"font\": \"फॉन्ट\",\n  \"actions\": \"क्रिया\",\n  \"date\": \"तारीख\",\n  \"addField\": \"फील्ड जोडा\",\n  \"userIcon\": \"वापरकर्त्याचे चिन्ह\"\n},\n\"noLogFiles\": \"कोणतीही लॉग फाइल्स नाहीत\",\n\"newSettings\": {\n  \"myAccount\": {\n    \"title\": \"माझे खाते\",\n    \"subtitle\": \"तुमचा प्रोफाइल सानुकूल करा, खाते सुरक्षा व्यवस्थापित करा, AI कीज पहा किंवा लॉगिन करा.\",\n    \"profileLabel\": \"खाते नाव आणि प्रोफाइल चित्र\",\n    \"profileNamePlaceholder\": \"तुमचे नाव प्रविष्ट करा\",\n    \"accountSecurity\": \"खाते सुरक्षा\",\n    \"2FA\": \"2-स्टेप प्रमाणीकरण\",\n    \"aiKeys\": \"AI कीज\",\n    \"accountLogin\": \"खाते लॉगिन\",\n    \"updateNameError\": \"नाव अपडेट करण्यात अयशस्वी\",\n    \"updateIconError\": \"चिन्ह अपडेट करण्यात अयशस्वी\",\n    \"aboutAppFlowy\": \"@:appName विषयी\",\n    \"deleteAccount\": {\n      \"title\": \"खाते हटवा\",\n      \"subtitle\": \"तुमचे खाते आणि सर्व डेटा कायमचे हटवा.\",\n      \"description\": \"तुमचे खाते कायमचे हटवले जाईल आणि सर्व वर्कस्पेसमधून प्रवेश काढून टाकला जाईल.\",\n      \"deleteMyAccount\": \"माझे खाते हटवा\",\n      \"dialogTitle\": \"खाते हटवा\",\n      \"dialogContent1\": \"तुम्हाला खात्री आहे की तुम्ही तुमचे खाते कायमचे हटवू इच्छिता?\",\n      \"dialogContent2\": \"ही क्रिया पूर्ववत केली जाऊ शकत नाही. हे सर्व वर्कस्पेसमधून प्रवेश हटवेल, खाजगी वर्कस्पेस मिटवेल, आणि सर्व शेअर्ड वर्कस्पेसमधून काढून टाकेल.\",\n      \"confirmHint1\": \"कृपया पुष्टीसाठी \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\" टाइप करा.\",\n      \"confirmHint2\": \"मला समजले आहे की ही क्रिया अपरिवर्तनीय आहे आणि माझे खाते व सर्व संबंधित डेटा कायमचा हटवला जाईल.\",\n      \"confirmHint3\": \"DELETE MY ACCOUNT\",\n      \"checkToConfirmError\": \"हटवण्यासाठी पुष्टी बॉक्स निवडणे आवश्यक आहे\",\n      \"failedToGetCurrentUser\": \"वर्तमान वापरकर्त्याचा ईमेल मिळवण्यात अयशस्वी\",\n      \"confirmTextValidationFailed\": \"तुमचा पुष्टी मजकूर \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\" शी जुळत नाही\",\n      \"deleteAccountSuccess\": \"खाते यशस्वीरित्या हटवले गेले\"\n    }\n  },\n  \"workplace\": {\n    \"name\": \"वर्कस्पेस\",\n    \"title\": \"वर्कस्पेस सेटिंग्स\",\n    \"subtitle\": \"तुमचा वर्कस्पेस लुक, थीम, फॉन्ट, मजकूर लेआउट, तारीख, वेळ आणि भाषा सानुकूल करा.\",\n    \"workplaceName\": \"वर्कस्पेसचे नाव\",\n    \"workplaceNamePlaceholder\": \"वर्कस्पेसचे नाव टाका\",\n    \"workplaceIcon\": \"वर्कस्पेस चिन्ह\",\n    \"workplaceIconSubtitle\": \"एक प्रतिमा अपलोड करा किंवा इमोजी वापरा. हे साइडबार आणि सूचना मध्ये दर्शवले जाईल.\",\n    \"renameError\": \"वर्कस्पेसचे नाव बदलण्यात अयशस्वी\",\n    \"updateIconError\": \"चिन्ह अपडेट करण्यात अयशस्वी\",\n    \"chooseAnIcon\": \"चिन्ह निवडा\",\n    \"appearance\": {\n      \"name\": \"दृश्यरूप\",\n      \"themeMode\": {\n        \"auto\": \"स्वयंचलित\",\n        \"light\": \"प्रकाश मोड\",\n        \"dark\": \"गडद मोड\"\n      },\n      \"language\": \"भाषा\"\n    }\n  },\n  \"syncState\": {\n    \"syncing\": \"सिंक्रोनायझ करत आहे\",\n    \"synced\": \"सिंक्रोनायझ झाले\",\n    \"noNetworkConnected\": \"नेटवर्क कनेक्ट केलेले नाही\"\n  }\n},\n  \"pageStyle\": {\n  \"title\": \"पृष्ठ शैली\",\n  \"layout\": \"लेआउट\",\n  \"coverImage\": \"मुखपृष्ठ प्रतिमा\",\n  \"pageIcon\": \"पृष्ठ चिन्ह\",\n  \"colors\": \"रंग\",\n  \"gradient\": \"ग्रेडियंट\",\n  \"backgroundImage\": \"पार्श्वभूमी प्रतिमा\",\n  \"presets\": \"पूर्वनियोजित\",\n  \"photo\": \"फोटो\",\n  \"unsplash\": \"Unsplash\",\n  \"pageCover\": \"पृष्ठ कव्हर\",\n  \"none\": \"काही नाही\",\n  \"openSettings\": \"सेटिंग्स उघडा\",\n  \"photoPermissionTitle\": \"@:appName तुमच्या फोटो लायब्ररीमध्ये प्रवेश करू इच्छित आहे\",\n  \"photoPermissionDescription\": \"तुमच्या कागदपत्रांमध्ये प्रतिमा जोडण्यासाठी @:appName ला तुमच्या फोटोंमध्ये प्रवेश आवश्यक आहे\",\n  \"cameraPermissionTitle\": \"@:appName तुमच्या कॅमेऱ्याला प्रवेश करू इच्छित आहे\",\n  \"cameraPermissionDescription\": \"कॅमेऱ्यातून प्रतिमा जोडण्यासाठी @:appName ला तुमच्या कॅमेऱ्याचा प्रवेश आवश्यक आहे\",\n  \"doNotAllow\": \"परवानगी देऊ नका\",\n  \"image\": \"प्रतिमा\"\n},\n\"commandPalette\": {\n  \"placeholder\": \"शोधा किंवा प्रश्न विचारा...\",\n  \"bestMatches\": \"सर्वोत्तम जुळवणी\",\n  \"recentHistory\": \"अलीकडील इतिहास\",\n  \"navigateHint\": \"नेव्हिगेट करण्यासाठी\",\n  \"loadingTooltip\": \"आम्ही निकाल शोधत आहोत...\",\n  \"betaLabel\": \"बेटा\",\n  \"betaTooltip\": \"सध्या आम्ही फक्त दस्तऐवज आणि पृष्ठ शोध समर्थन करतो\",\n  \"fromTrashHint\": \"कचरापेटीतून\",\n  \"noResultsHint\": \"आपण जे शोधत आहात ते सापडले नाही, कृपया दुसरा शब्द वापरून शोधा.\",\n  \"clearSearchTooltip\": \"शोध फील्ड साफ करा\"\n},\n\"space\": {\n  \"delete\": \"हटवा\",\n  \"deleteConfirmation\": \"हटवा: \",\n  \"deleteConfirmationDescription\": \"या स्पेसमधील सर्व पृष्ठे हटवली जातील आणि कचरापेटीत टाकली जातील, आणि प्रकाशित पृष्ठे अनपब्लिश केली जातील.\",\n  \"rename\": \"स्पेसचे नाव बदला\",\n  \"changeIcon\": \"चिन्ह बदला\",\n  \"manage\": \"स्पेस व्यवस्थापित करा\",\n  \"addNewSpace\": \"स्पेस तयार करा\",\n  \"collapseAllSubPages\": \"सर्व उपपृष्ठे संकुचित करा\",\n  \"createNewSpace\": \"नवीन स्पेस तयार करा\",\n  \"createSpaceDescription\": \"तुमचे कार्य अधिक चांगल्या प्रकारे आयोजित करण्यासाठी अनेक सार्वजनिक व खाजगी स्पेस तयार करा.\",\n  \"spaceName\": \"स्पेसचे नाव\",\n  \"spaceNamePlaceholder\": \"उदा. मार्केटिंग, अभियांत्रिकी, HR\",\n  \"permission\": \"स्पेस परवानगी\",\n  \"publicPermission\": \"सार्वजनिक\",\n  \"publicPermissionDescription\": \"पूर्ण प्रवेशासह सर्व वर्कस्पेस सदस्य\",\n  \"privatePermission\": \"खाजगी\",\n  \"privatePermissionDescription\": \"फक्त तुम्हाला या स्पेसमध्ये प्रवेश आहे\",\n  \"spaceIconBackground\": \"पार्श्वभूमीचा रंग\",\n  \"spaceIcon\": \"चिन्ह\",\n  \"dangerZone\": \"धोकादायक क्षेत्र\",\n  \"unableToDeleteLastSpace\": \"शेवटची स्पेस हटवता येणार नाही\",\n  \"unableToDeleteSpaceNotCreatedByYou\": \"तुमच्याद्वारे तयार न केलेली स्पेस हटवता येणार नाही\",\n  \"enableSpacesForYourWorkspace\": \"तुमच्या वर्कस्पेससाठी स्पेस सक्षम करा\",\n  \"title\": \"स्पेसेस\",\n  \"defaultSpaceName\": \"सामान्य\",\n  \"upgradeSpaceTitle\": \"स्पेस सक्षम करा\",\n  \"upgradeSpaceDescription\": \"तुमच्या वर्कस्पेसचे अधिक चांगल्या प्रकारे व्यवस्थापन करण्यासाठी अनेक सार्वजनिक आणि खाजगी स्पेस तयार करा.\",\n  \"upgrade\": \"अपग्रेड\",\n  \"upgradeYourSpace\": \"अनेक स्पेस तयार करा\",\n  \"quicklySwitch\": \"पुढील स्पेसवर पटकन स्विच करा\",\n  \"duplicate\": \"स्पेस डुप्लिकेट करा\",\n  \"movePageToSpace\": \"पृष्ठ स्पेसमध्ये हलवा\",\n  \"cannotMovePageToDatabase\": \"पृष्ठ डेटाबेसमध्ये हलवता येणार नाही\",\n  \"switchSpace\": \"स्पेस स्विच करा\",\n  \"spaceNameCannotBeEmpty\": \"स्पेसचे नाव रिकामे असू शकत नाही\",\n  \"success\": {\n    \"deleteSpace\": \"स्पेस यशस्वीरित्या हटवली\",\n    \"renameSpace\": \"स्पेसचे नाव यशस्वीरित्या बदलले\",\n    \"duplicateSpace\": \"स्पेस यशस्वीरित्या डुप्लिकेट केली\",\n    \"updateSpace\": \"स्पेस यशस्वीरित्या अपडेट केली\"\n  },\n  \"error\": {\n    \"deleteSpace\": \"स्पेस हटवण्यात अयशस्वी\",\n    \"renameSpace\": \"स्पेसचे नाव बदलण्यात अयशस्वी\",\n    \"duplicateSpace\": \"स्पेस डुप्लिकेट करण्यात अयशस्वी\",\n    \"updateSpace\": \"स्पेस अपडेट करण्यात अयशस्वी\"\n  },\n  \"createSpace\": \"स्पेस तयार करा\",\n  \"manageSpace\": \"स्पेस व्यवस्थापित करा\",\n  \"renameSpace\": \"स्पेसचे नाव बदला\",\n  \"mSpaceIconColor\": \"स्पेस चिन्हाचा रंग\",\n  \"mSpaceIcon\": \"स्पेस चिन्ह\"\n},\n  \"publish\": {\n  \"hasNotBeenPublished\": \"हे पृष्ठ अजून प्रकाशित केलेले नाही\",\n  \"spaceHasNotBeenPublished\": \"स्पेस प्रकाशित करण्यासाठी समर्थन नाही\",\n  \"reportPage\": \"पृष्ठाची तक्रार करा\",\n  \"databaseHasNotBeenPublished\": \"डेटाबेस प्रकाशित करण्यास समर्थन नाही.\",\n  \"createdWith\": \"यांनी तयार केले\",\n  \"downloadApp\": \"AppFlowy डाउनलोड करा\",\n  \"copy\": {\n    \"codeBlock\": \"कोड ब्लॉकची सामग्री क्लिपबोर्डवर कॉपी केली गेली आहे\",\n    \"imageBlock\": \"प्रतिमा लिंक क्लिपबोर्डवर कॉपी केली गेली आहे\",\n    \"mathBlock\": \"गणितीय समीकरण क्लिपबोर्डवर कॉपी केले गेले आहे\",\n    \"fileBlock\": \"फाइल लिंक क्लिपबोर्डवर कॉपी केली गेली आहे\"\n  },\n  \"containsPublishedPage\": \"या पृष्ठात एक किंवा अधिक प्रकाशित पृष्ठे आहेत. तुम्ही पुढे गेल्यास ती अनपब्लिश होतील. तुम्हाला हटवणे चालू ठेवायचे आहे का?\",\n  \"publishSuccessfully\": \"यशस्वीरित्या प्रकाशित झाले\",\n  \"unpublishSuccessfully\": \"यशस्वीरित्या अनपब्लिश झाले\",\n  \"publishFailed\": \"प्रकाशित करण्यात अयशस्वी\",\n  \"unpublishFailed\": \"अनपब्लिश करण्यात अयशस्वी\",\n  \"noAccessToVisit\": \"या पृष्ठावर प्रवेश नाही...\",\n  \"createWithAppFlowy\": \"AppFlowy ने वेबसाइट तयार करा\",\n  \"fastWithAI\": \"AI सह जलद आणि सोपे.\",\n  \"tryItNow\": \"आत्ताच वापरून पहा\",\n  \"onlyGridViewCanBePublished\": \"फक्त Grid view प्रकाशित केला जाऊ शकतो\",\n  \"database\": {\n    \"zero\": \"{} निवडलेले दृश्य प्रकाशित करा\",\n    \"one\": \"{} निवडलेली दृश्ये प्रकाशित करा\",\n    \"many\": \"{} निवडलेली दृश्ये प्रकाशित करा\",\n    \"other\": \"{} निवडलेली दृश्ये प्रकाशित करा\"\n  },\n  \"mustSelectPrimaryDatabase\": \"प्राथमिक दृश्य निवडणे आवश्यक आहे\",\n  \"noDatabaseSelected\": \"कोणताही डेटाबेस निवडलेला नाही, कृपया किमान एक डेटाबेस निवडा.\",\n  \"unableToDeselectPrimaryDatabase\": \"प्राथमिक डेटाबेस अननिवड करता येणार नाही\",\n  \"saveThisPage\": \"या टेम्पलेटपासून सुरू करा\",\n  \"duplicateTitle\": \"तुम्हाला हे कुठे जोडायचे आहे\",\n  \"selectWorkspace\": \"वर्कस्पेस निवडा\",\n  \"addTo\": \"मध्ये जोडा\",\n  \"duplicateSuccessfully\": \"तुमच्या वर्कस्पेसमध्ये जोडले गेले\",\n  \"duplicateSuccessfullyDescription\": \"AppFlowy स्थापित केले नाही? तुम्ही 'डाउनलोड' वर क्लिक केल्यावर डाउनलोड आपोआप सुरू होईल.\",\n  \"downloadIt\": \"डाउनलोड करा\",\n  \"openApp\": \"अ‍ॅपमध्ये उघडा\",\n  \"duplicateFailed\": \"डुप्लिकेट करण्यात अयशस्वी\",\n  \"membersCount\": {\n    \"zero\": \"सदस्य नाहीत\",\n    \"one\": \"1 सदस्य\",\n    \"many\": \"{count} सदस्य\",\n    \"other\": \"{count} सदस्य\"\n  },\n  \"useThisTemplate\": \"हा टेम्पलेट वापरा\"\n},\n\"web\": {\n  \"continue\": \"पुढे जा\",\n  \"or\": \"किंवा\",\n  \"continueWithGoogle\": \"Google सह पुढे जा\",\n  \"continueWithGithub\": \"GitHub सह पुढे जा\",\n  \"continueWithDiscord\": \"Discord सह पुढे जा\",\n  \"continueWithApple\": \"Apple सह पुढे जा\",\n  \"moreOptions\": \"अधिक पर्याय\",\n  \"collapse\": \"आकुंचन\",\n  \"signInAgreement\": \"\\\"पुढे जा\\\" क्लिक करून, तुम्ही AppFlowy च्या अटींना सहमती दिली आहे\",\n  \"signInLocalAgreement\": \"\\\"सुरुवात करा\\\" क्लिक करून, तुम्ही AppFlowy च्या अटींना सहमती दिली आहे\",\n  \"and\": \"आणि\",\n  \"termOfUse\": \"वापर अटी\",\n  \"privacyPolicy\": \"गोपनीयता धोरण\",\n  \"signInError\": \"साइन इन त्रुटी\",\n  \"login\": \"साइन अप किंवा लॉग इन करा\",\n  \"fileBlock\": {\n    \"uploadedAt\": \"{time} रोजी अपलोड केले\",\n    \"linkedAt\": \"{time} रोजी लिंक जोडली\",\n    \"empty\": \"फाईल अपलोड करा किंवा एम्बेड करा\",\n    \"uploadFailed\": \"अपलोड अयशस्वी, कृपया पुन्हा प्रयत्न करा\",\n    \"retry\": \"पुन्हा प्रयत्न करा\"\n  },\n  \"importNotion\": \"Notion वरून आयात करा\",\n  \"import\": \"आयात करा\",\n  \"importSuccess\": \"यशस्वीरित्या अपलोड केले\",\n  \"importSuccessMessage\": \"आम्ही तुम्हाला आयात पूर्ण झाल्यावर सूचित करू. त्यानंतर, तुम्ही साइडबारमध्ये तुमची आयात केलेली पृष्ठे पाहू शकता.\",\n  \"importFailed\": \"आयात अयशस्वी, कृपया फाईल फॉरमॅट तपासा\",\n  \"dropNotionFile\": \"तुमची Notion zip फाईल येथे ड्रॉप करा किंवा ब्राउझ करा\",\n  \"error\": {\n    \"pageNameIsEmpty\": \"पृष्ठाचे नाव रिकामे आहे, कृपया दुसरे नाव वापरून पहा\"\n  }\n},\n  \"globalComment\": {\n  \"comments\": \"टिप्पण्या\",\n  \"addComment\": \"टिप्पणी जोडा\",\n  \"reactedBy\": \"यांनी प्रतिक्रिया दिली\",\n  \"addReaction\": \"प्रतिक्रिया जोडा\",\n  \"reactedByMore\": \"आणि {count} इतर\",\n  \"showSeconds\": {\n    \"one\": \"1 सेकंदापूर्वी\",\n    \"other\": \"{count} सेकंदांपूर्वी\",\n    \"zero\": \"आत्ताच\",\n    \"many\": \"{count} सेकंदांपूर्वी\"\n  },\n  \"showMinutes\": {\n    \"one\": \"1 मिनिटापूर्वी\",\n    \"other\": \"{count} मिनिटांपूर्वी\",\n    \"many\": \"{count} मिनिटांपूर्वी\"\n  },\n  \"showHours\": {\n    \"one\": \"1 तासापूर्वी\",\n    \"other\": \"{count} तासांपूर्वी\",\n    \"many\": \"{count} तासांपूर्वी\"\n  },\n  \"showDays\": {\n    \"one\": \"1 दिवसापूर्वी\",\n    \"other\": \"{count} दिवसांपूर्वी\",\n    \"many\": \"{count} दिवसांपूर्वी\"\n  },\n  \"showMonths\": {\n    \"one\": \"1 महिन्यापूर्वी\",\n    \"other\": \"{count} महिन्यांपूर्वी\",\n    \"many\": \"{count} महिन्यांपूर्वी\"\n  },\n  \"showYears\": {\n    \"one\": \"1 वर्षापूर्वी\",\n    \"other\": \"{count} वर्षांपूर्वी\",\n    \"many\": \"{count} वर्षांपूर्वी\"\n  },\n  \"reply\": \"उत्तर द्या\",\n  \"deleteComment\": \"टिप्पणी हटवा\",\n  \"youAreNotOwner\": \"तुम्ही या टिप्पणीचे मालक नाही\",\n  \"confirmDeleteDescription\": \"तुम्हाला ही टिप्पणी हटवायची आहे याची खात्री आहे का?\",\n  \"hasBeenDeleted\": \"हटवले गेले\",\n  \"replyingTo\": \"याला उत्तर देत आहे\",\n  \"noAccessDeleteComment\": \"तुम्हाला ही टिप्पणी हटवण्याची परवानगी नाही\",\n  \"collapse\": \"संकुचित करा\",\n  \"readMore\": \"अधिक वाचा\",\n  \"failedToAddComment\": \"टिप्पणी जोडण्यात अयशस्वी\",\n  \"commentAddedSuccessfully\": \"टिप्पणी यशस्वीरित्या जोडली गेली.\",\n  \"commentAddedSuccessTip\": \"तुम्ही नुकतीच एक टिप्पणी जोडली किंवा उत्तर दिले आहे. वर जाऊन ताजी टिप्पण्या पाहायच्या का?\"\n},\n  \"template\": {\n  \"asTemplate\": \"टेम्पलेट म्हणून जतन करा\",\n  \"name\": \"टेम्पलेट नाव\",\n  \"description\": \"टेम्पलेट वर्णन\",\n  \"about\": \"टेम्पलेट माहिती\",\n  \"deleteFromTemplate\": \"टेम्पलेटमधून हटवा\",\n  \"preview\": \"टेम्पलेट पूर्वदृश्य\",\n  \"categories\": \"टेम्पलेट श्रेणी\",\n  \"isNewTemplate\": \"नवीन टेम्पलेटमध्ये पिन करा\",\n  \"featured\": \"वैशिष्ट्यीकृतमध्ये पिन करा\",\n  \"relatedTemplates\": \"संबंधित टेम्पलेट्स\",\n  \"requiredField\": \"{field} आवश्यक आहे\",\n  \"addCategory\": \"\\\"{category}\\\" जोडा\",\n  \"addNewCategory\": \"नवीन श्रेणी जोडा\",\n  \"addNewCreator\": \"नवीन निर्माता जोडा\",\n  \"deleteCategory\": \"श्रेणी हटवा\",\n  \"editCategory\": \"श्रेणी संपादित करा\",\n  \"editCreator\": \"निर्माता संपादित करा\",\n  \"category\": {\n    \"name\": \"श्रेणीचे नाव\",\n    \"icon\": \"श्रेणी चिन्ह\",\n    \"bgColor\": \"श्रेणी पार्श्वभूमीचा रंग\",\n    \"priority\": \"श्रेणी प्राधान्य\",\n    \"desc\": \"श्रेणीचे वर्णन\",\n    \"type\": \"श्रेणी प्रकार\",\n    \"icons\": \"श्रेणी चिन्हे\",\n    \"colors\": \"श्रेणी रंग\",\n    \"byUseCase\": \"वापराच्या आधारे\",\n    \"byFeature\": \"वैशिष्ट्यांनुसार\",\n    \"deleteCategory\": \"श्रेणी हटवा\",\n    \"deleteCategoryDescription\": \"तुम्हाला ही श्रेणी हटवायची आहे का?\",\n    \"typeToSearch\": \"श्रेणी शोधण्यासाठी टाइप करा...\"\n  },\n  \"creator\": {\n    \"label\": \"टेम्पलेट निर्माता\",\n    \"name\": \"निर्मात्याचे नाव\",\n    \"avatar\": \"निर्मात्याचा अवतार\",\n    \"accountLinks\": \"निर्मात्याचे खाते दुवे\",\n    \"uploadAvatar\": \"अवतार अपलोड करण्यासाठी क्लिक करा\",\n    \"deleteCreator\": \"निर्माता हटवा\",\n    \"deleteCreatorDescription\": \"तुम्हाला हा निर्माता हटवायचा आहे का?\",\n    \"typeToSearch\": \"निर्माते शोधण्यासाठी टाइप करा...\"\n  },\n  \"uploadSuccess\": \"टेम्पलेट यशस्वीरित्या अपलोड झाले\",\n  \"uploadSuccessDescription\": \"तुमचे टेम्पलेट यशस्वीरित्या अपलोड झाले आहे. आता तुम्ही ते टेम्पलेट गॅलरीमध्ये पाहू शकता.\",\n  \"viewTemplate\": \"टेम्पलेट पहा\",\n  \"deleteTemplate\": \"टेम्पलेट हटवा\",\n  \"deleteSuccess\": \"टेम्पलेट यशस्वीरित्या हटवले गेले\",\n  \"deleteTemplateDescription\": \"याचा वर्तमान पृष्ठ किंवा प्रकाशित स्थितीवर परिणाम होणार नाही. तुम्हाला हे टेम्पलेट हटवायचे आहे का?\",\n  \"addRelatedTemplate\": \"संबंधित टेम्पलेट जोडा\",\n  \"removeRelatedTemplate\": \"संबंधित टेम्पलेट हटवा\",\n  \"uploadAvatar\": \"अवतार अपलोड करा\",\n  \"searchInCategory\": \"{category} मध्ये शोधा\",\n  \"label\": \"टेम्पलेट्स\"\n},\n  \"fileDropzone\": {\n  \"dropFile\": \"फाइल अपलोड करण्यासाठी येथे क्लिक करा किंवा ड्रॅग करा\",\n  \"uploading\": \"अपलोड करत आहे...\",\n  \"uploadFailed\": \"अपलोड अयशस्वी\",\n  \"uploadSuccess\": \"अपलोड यशस्वी\",\n  \"uploadSuccessDescription\": \"फाइल यशस्वीरित्या अपलोड झाली आहे\",\n  \"uploadFailedDescription\": \"फाइल अपलोड अयशस्वी झाली आहे\",\n  \"uploadingDescription\": \"फाइल अपलोड होत आहे\"\n},\n  \"gallery\": {\n  \"preview\": \"पूर्ण स्क्रीनमध्ये उघडा\",\n  \"copy\": \"कॉपी करा\",\n  \"download\": \"डाउनलोड\",\n  \"prev\": \"मागील\",\n  \"next\": \"पुढील\",\n  \"resetZoom\": \"झूम रिसेट करा\",\n  \"zoomIn\": \"झूम इन\",\n  \"zoomOut\": \"झूम आउट\"\n},\n  \"invitation\": {\n  \"join\": \"सामील व्हा\",\n  \"on\": \"वर\",\n  \"invitedBy\": \"यांनी आमंत्रित केले\",\n  \"membersCount\": {\n    \"zero\": \"{count} सदस्य\",\n    \"one\": \"{count} सदस्य\",\n    \"many\": \"{count} सदस्य\",\n    \"other\": \"{count} सदस्य\"\n  },\n  \"tip\": \"तुम्हाला खालील माहितीच्या आधारे या कार्यक्षेत्रात सामील होण्यासाठी आमंत्रित करण्यात आले आहे. ही माहिती चुकीची असल्यास, कृपया प्रशासकाशी संपर्क साधा.\",\n  \"joinWorkspace\": \"वर्कस्पेसमध्ये सामील व्हा\",\n  \"success\": \"तुम्ही यशस्वीरित्या वर्कस्पेसमध्ये सामील झाला आहात\",\n  \"successMessage\": \"आता तुम्ही सर्व पृष्ठे आणि कार्यक्षेत्रे वापरू शकता.\",\n  \"openWorkspace\": \"AppFlowy उघडा\",\n  \"alreadyAccepted\": \"तुम्ही आधीच आमंत्रण स्वीकारले आहे\",\n  \"errorModal\": {\n    \"title\": \"काहीतरी चुकले आहे\",\n    \"description\": \"तुमचे सध्याचे खाते {email} कदाचित या वर्कस्पेसमध्ये प्रवेशासाठी पात्र नाही. कृपया योग्य खात्याने लॉग इन करा किंवा वर्कस्पेस मालकाशी संपर्क साधा.\",\n    \"contactOwner\": \"मालकाशी संपर्क करा\",\n    \"close\": \"मुख्यपृष्ठावर परत जा\",\n    \"changeAccount\": \"खाते बदला\"\n  }\n},\n  \"requestAccess\": {\n  \"title\": \"या पृष्ठासाठी प्रवेश नाही\",\n  \"subtitle\": \"तुम्ही या पृष्ठाच्या मालकाकडून प्रवेशासाठी विनंती करू शकता. मंजुरीनंतर तुम्हाला हे पृष्ठ पाहता येईल.\",\n  \"requestAccess\": \"प्रवेशाची विनंती करा\",\n  \"backToHome\": \"मुख्यपृष्ठावर परत जा\",\n  \"tip\": \"तुम्ही सध्या <link/> म्हणून लॉग इन आहात.\",\n  \"mightBe\": \"कदाचित तुम्हाला <login/> दुसऱ्या खात्याने लॉग इन करणे आवश्यक आहे.\",\n  \"successful\": \"विनंती यशस्वीपणे पाठवली गेली\",\n  \"successfulMessage\": \"मालकाने मंजुरी दिल्यावर तुम्हाला सूचित केले जाईल.\",\n  \"requestError\": \"प्रवेशाची विनंती अयशस्वी\",\n  \"repeatRequestError\": \"तुम्ही यासाठी आधीच विनंती केली आहे\"\n},\n  \"approveAccess\": {\n  \"title\": \"वर्कस्पेसमध्ये सामील होण्यासाठी विनंती मंजूर करा\",\n  \"requestSummary\": \"<user/> यांनी <workspace/> मध्ये सामील होण्यासाठी आणि <page/> पाहण्यासाठी विनंती केली आहे\",\n  \"upgrade\": \"अपग्रेड\",\n  \"downloadApp\": \"AppFlowy डाउनलोड करा\",\n  \"approveButton\": \"मंजूर करा\",\n  \"approveSuccess\": \"मंजूर यशस्वी\",\n  \"approveError\": \"मंजुरी अयशस्वी. कृपया वर्कस्पेस मर्यादा ओलांडलेली नाही याची खात्री करा\",\n  \"getRequestInfoError\": \"विनंतीची माहिती मिळवण्यात अयशस्वी\",\n  \"memberCount\": {\n    \"zero\": \"कोणतेही सदस्य नाहीत\",\n    \"one\": \"1 सदस्य\",\n    \"many\": \"{count} सदस्य\",\n    \"other\": \"{count} सदस्य\"\n  },\n  \"alreadyProTitle\": \"वर्कस्पेस योजना मर्यादा गाठली आहे\",\n  \"alreadyProMessage\": \"अधिक सदस्यांसाठी <email/> शी संपर्क साधा\",\n  \"repeatApproveError\": \"तुम्ही ही विनंती आधीच मंजूर केली आहे\",\n  \"ensurePlanLimit\": \"कृपया खात्री करा की योजना मर्यादा ओलांडलेली नाही. जर ओलांडली असेल तर वर्कस्पेस योजना <upgrade/> करा किंवा <download/> करा.\",\n  \"requestToJoin\": \"मध्ये सामील होण्यासाठी विनंती केली\",\n  \"asMember\": \"सदस्य म्हणून\"\n},\n  \"upgradePlanModal\": {\n  \"title\": \"Pro प्लॅनवर अपग्रेड करा\",\n  \"message\": \"{name} ने फ्री सदस्य मर्यादा गाठली आहे. अधिक सदस्य आमंत्रित करण्यासाठी Pro प्लॅनवर अपग्रेड करा.\",\n  \"upgradeSteps\": \"AppFlowy वर तुमची योजना कशी अपग्रेड करावी:\",\n  \"step1\": \"1. सेटिंग्जमध्ये जा\",\n  \"step2\": \"2. 'योजना' वर क्लिक करा\",\n  \"step3\": \"3. 'योजना बदला' निवडा\",\n  \"appNote\": \"नोंद:\",\n  \"actionButton\": \"अपग्रेड करा\",\n  \"downloadLink\": \"अ‍ॅप डाउनलोड करा\",\n  \"laterButton\": \"नंतर\",\n  \"refreshNote\": \"यशस्वी अपग्रेडनंतर, तुमची नवीन वैशिष्ट्ये सक्रिय करण्यासाठी <refresh/> वर क्लिक करा.\",\n  \"refresh\": \"येथे\"\n},\n  \"breadcrumbs\": {\n  \"label\": \"ब्रेडक्रम्स\"\n},\n  \"time\": {\n  \"justNow\": \"आत्ताच\",\n  \"seconds\": {\n    \"one\": \"1 सेकंद\",\n    \"other\": \"{count} सेकंद\"\n  },\n  \"minutes\": {\n    \"one\": \"1 मिनिट\",\n    \"other\": \"{count} मिनिटे\"\n  },\n  \"hours\": {\n    \"one\": \"1 तास\",\n    \"other\": \"{count} तास\"\n  },\n  \"days\": {\n    \"one\": \"1 दिवस\",\n    \"other\": \"{count} दिवस\"\n  },\n  \"weeks\": {\n    \"one\": \"1 आठवडा\",\n    \"other\": \"{count} आठवडे\"\n  },\n  \"months\": {\n    \"one\": \"1 महिना\",\n    \"other\": \"{count} महिने\"\n  },\n  \"years\": {\n    \"one\": \"1 वर्ष\",\n    \"other\": \"{count} वर्षे\"\n  },\n  \"ago\": \"पूर्वी\",\n  \"yesterday\": \"काल\",\n  \"today\": \"आज\"\n},\n  \"members\": {\n  \"zero\": \"सदस्य नाहीत\",\n  \"one\": \"1 सदस्य\",\n  \"many\": \"{count} सदस्य\",\n  \"other\": \"{count} सदस्य\"\n},\n  \"tabMenu\": {\n  \"close\": \"बंद करा\",\n  \"closeDisabledHint\": \"पिन केलेले टॅब बंद करता येत नाही, कृपया आधी अनपिन करा\",\n  \"closeOthers\": \"इतर टॅब बंद करा\",\n  \"closeOthersHint\": \"हे सर्व अनपिन केलेले टॅब्स बंद करेल या टॅब वगळता\",\n  \"closeOthersDisabledHint\": \"सर्व टॅब पिन केलेले आहेत, बंद करण्यासाठी कोणतेही टॅब सापडले नाहीत\",\n  \"favorite\": \"आवडते\",\n  \"unfavorite\": \"आवडते काढा\",\n  \"favoriteDisabledHint\": \"हे दृश्य आवडते म्हणून जतन करता येत नाही\",\n  \"pinTab\": \"पिन करा\",\n  \"unpinTab\": \"अनपिन करा\"\n},\n  \"openFileMessage\": {\n  \"success\": \"फाइल यशस्वीरित्या उघडली\",\n  \"fileNotFound\": \"फाइल सापडली नाही\",\n  \"noAppToOpenFile\": \"ही फाइल उघडण्यासाठी कोणतेही अ‍ॅप उपलब्ध नाही\",\n  \"permissionDenied\": \"ही फाइल उघडण्यासाठी परवानगी नाही\",\n  \"unknownError\": \"फाइल उघडण्यात अयशस्वी\"\n},\n  \"inviteMember\": {\n  \"requestInviteMembers\": \"तुमच्या वर्कस्पेसमध्ये आमंत्रित करा\",\n  \"inviteFailedMemberLimit\": \"सदस्य मर्यादा गाठली आहे, कृपया \",\n  \"upgrade\": \"अपग्रेड करा\",\n  \"addEmail\": \"email@example.com, email2@example.com...\",\n  \"requestInvites\": \"आमंत्रण पाठवा\",\n  \"inviteAlready\": \"तुम्ही या ईमेलला आधीच आमंत्रित केले आहे: {email}\",\n  \"inviteSuccess\": \"आमंत्रण यशस्वीपणे पाठवले\",\n  \"description\": \"खाली ईमेल कॉमा वापरून टाका. शुल्क सदस्य संख्येवर आधारित असते.\",\n  \"emails\": \"ईमेल\"\n},\n  \"quickNote\": {\n  \"label\": \"झटपट नोंद\",\n  \"quickNotes\": \"झटपट नोंदी\",\n  \"search\": \"झटपट नोंदी शोधा\",\n  \"collapseFullView\": \"पूर्ण दृश्य लपवा\",\n  \"expandFullView\": \"पूर्ण दृश्य उघडा\",\n  \"createFailed\": \"झटपट नोंद तयार करण्यात अयशस्वी\",\n  \"quickNotesEmpty\": \"कोणत्याही झटपट नोंदी नाहीत\",\n  \"emptyNote\": \"रिकामी नोंद\",\n  \"deleteNotePrompt\": \"निवडलेली नोंद कायमची हटवली जाईल. तुम्हाला नक्की ती हटवायची आहे का?\",\n  \"addNote\": \"नवीन नोंद\",\n  \"noAdditionalText\": \"अधिक माहिती नाही\"\n},\n  \"subscribe\": {\n  \"upgradePlanTitle\": \"योजना तुलना करा आणि निवडा\",\n  \"yearly\": \"वार्षिक\",\n  \"save\": \"{discount}% बचत\",\n  \"monthly\": \"मासिक\",\n  \"priceIn\": \"किंमत येथे: \",\n  \"free\": \"फ्री\",\n  \"pro\": \"प्रो\",\n  \"freeDescription\": \"2 सदस्यांपर्यंत वैयक्तिक वापरकर्त्यांसाठी सर्व काही आयोजित करण्यासाठी\",\n  \"proDescription\": \"प्रकल्प आणि टीम नॉलेज व्यवस्थापित करण्यासाठी लहान टीमसाठी\",\n  \"proDuration\": {\n    \"monthly\": \"प्रति सदस्य प्रति महिना\\nमासिक बिलिंग\",\n    \"yearly\": \"प्रति सदस्य प्रति महिना\\nवार्षिक बिलिंग\"\n  },\n  \"cancel\": \"खालच्या योजनेवर जा\",\n  \"changePlan\": \"प्रो योजनेवर अपग्रेड करा\",\n  \"everythingInFree\": \"फ्री योजनेतील सर्व काही +\",\n  \"currentPlan\": \"सध्याची योजना\",\n  \"freeDuration\": \"कायम\",\n  \"freePoints\": {\n    \"first\": \"1 सहकार्यात्मक वर्कस्पेस (2 सदस्यांपर्यंत)\",\n    \"second\": \"अमर्यादित पृष्ठे आणि ब्लॉक्स\",\n    \"three\": \"5 GB संचयन\",\n    \"four\": \"बुद्धिमान शोध\",\n    \"five\": \"20 AI प्रतिसाद\",\n    \"six\": \"मोबाईल अ‍ॅप\",\n    \"seven\": \"रिअल-टाइम सहकार्य\"\n  },\n  \"proPoints\": {\n    \"first\": \"अमर्यादित संचयन\",\n    \"second\": \"10 वर्कस्पेस सदस्यांपर्यंत\",\n    \"three\": \"अमर्यादित AI प्रतिसाद\",\n    \"four\": \"अमर्यादित फाइल अपलोड्स\",\n    \"five\": \"कस्टम नेमस्पेस\"\n  },\n  \"cancelPlan\": {\n    \"title\": \"आपल्याला जाताना पाहून वाईट वाटते\",\n    \"success\": \"आपली सदस्यता यशस्वीरित्या रद्द झाली आहे\",\n    \"description\": \"आपल्याला जाताना वाईट वाटते. कृपया आम्हाला सुधारण्यासाठी तुमचे अभिप्राय कळवा. खालील काही प्रश्नांना उत्तर द्या.\",\n    \"commonOther\": \"इतर\",\n    \"otherHint\": \"आपले उत्तर येथे लिहा\",\n    \"questionOne\": {\n      \"question\": \"तुम्ही AppFlowy Pro सदस्यता का रद्द केली?\",\n      \"answerOne\": \"खर्च खूप जास्त आहे\",\n      \"answerTwo\": \"वैशिष्ट्ये अपेक्षेनुसार नव्हती\",\n      \"answerThree\": \"चांगला पर्याय सापडला\",\n      \"answerFour\": \"खर्च योग्य ठरण्यासाठी वापर पुरेसा नव्हता\",\n      \"answerFive\": \"सेवा समस्या किंवा तांत्रिक अडचणी\"\n    },\n    \"questionTwo\": {\n      \"question\": \"तुम्ही भविष्यात AppFlowy Pro पुन्हा घेण्याची शक्यता किती आहे?\",\n      \"answerOne\": \"खूप शक्यता आहे\",\n      \"answerTwo\": \"काहीशी शक्यता आहे\",\n      \"answerThree\": \"निश्चित नाही\",\n      \"answerFour\": \"अल्प शक्यता आहे\",\n      \"answerFive\": \"शक्यता नाही\"\n    },\n    \"questionThree\": {\n      \"question\": \"सदस्यतेदरम्यान कोणते Pro फिचर तुम्हाला सर्वात जास्त उपयोगी वाटले?\",\n      \"answerOne\": \"मल्टी-यूजर सहकार्य\",\n      \"answerTwo\": \"लांब कालावधीसाठी आवृत्ती इतिहास\",\n      \"answerThree\": \"अमर्यादित AI प्रतिसाद\",\n      \"answerFour\": \"स्थानिक AI मॉडेल्सचा प्रवेश\"\n    },\n    \"questionFour\": {\n      \"question\": \"AppFlowy बाबत तुमचा एकूण अनुभव कसा होता?\",\n      \"answerOne\": \"छान\",\n      \"answerTwo\": \"चांगला\",\n      \"answerThree\": \"सामान्य\",\n      \"answerFour\": \"थोडासा वाईट\",\n      \"answerFive\": \"असंतोषजनक\"\n    }\n  }\n},\n  \"ai\": {\n  \"contentPolicyViolation\": \"संवेदनशील सामग्रीमुळे प्रतिमा निर्मिती अयशस्वी झाली. कृपया तुमचे इनपुट पुन्हा लिहा आणि पुन्हा प्रयत्न करा.\",\n  \"textLimitReachedDescription\": \"तुमच्या वर्कस्पेसमध्ये मोफत AI प्रतिसाद संपले आहेत. अनलिमिटेड प्रतिसादांसाठी प्रो योजना घेण्याचा किंवा AI अ‍ॅड-ऑन खरेदी करण्याचा विचार करा.\",\n  \"imageLimitReachedDescription\": \"तुमचे मोफत AI प्रतिमा कोटा संपले आहे. कृपया प्रो योजना घ्या किंवा AI अ‍ॅड-ऑन खरेदी करा.\",\n  \"limitReachedAction\": {\n    \"textDescription\": \"तुमच्या वर्कस्पेसमध्ये मोफत AI प्रतिसाद संपले आहेत. अधिक प्रतिसाद मिळवण्यासाठी कृपया\",\n    \"imageDescription\": \"तुमचे मोफत AI प्रतिमा कोटा संपले आहे. कृपया\",\n    \"upgrade\": \"अपग्रेड करा\",\n    \"toThe\": \"या योजनेवर\",\n    \"proPlan\": \"प्रो योजना\",\n    \"orPurchaseAn\": \"किंवा खरेदी करा\",\n    \"aiAddon\": \"AI अ‍ॅड-ऑन\"\n  },\n  \"editing\": \"संपादन करत आहे\",\n  \"analyzing\": \"विश्लेषण करत आहे\",\n  \"continueWritingEmptyDocumentTitle\": \"लेखन सुरू ठेवता आले नाही\",\n  \"continueWritingEmptyDocumentDescription\": \"तुमच्या दस्तऐवजातील मजकूर वाढवण्यात अडचण येत आहे. कृपया एक छोटं परिचय लिहा, मग आम्ही पुढे नेऊ!\",\n  \"more\": \"अधिक\"\n},\n  \"autoUpdate\": {\n  \"criticalUpdateTitle\": \"अद्यतन आवश्यक आहे\",\n  \"criticalUpdateDescription\": \"तुमचा अनुभव सुधारण्यासाठी आम्ही सुधारणा केल्या आहेत! कृपया {currentVersion} वरून {newVersion} वर अद्यतन करा.\",\n  \"criticalUpdateButton\": \"अद्यतन करा\",\n  \"bannerUpdateTitle\": \"नवीन आवृत्ती उपलब्ध!\",\n  \"bannerUpdateDescription\": \"नवीन वैशिष्ट्ये आणि सुधारणांसाठी. अद्यतनासाठी 'Update' क्लिक करा.\",\n  \"bannerUpdateButton\": \"अद्यतन करा\",\n  \"settingsUpdateTitle\": \"नवीन आवृत्ती ({newVersion}) उपलब्ध!\",\n  \"settingsUpdateDescription\": \"सध्याची आवृत्ती: {currentVersion} (अधिकृत बिल्ड) → {newVersion}\",\n  \"settingsUpdateButton\": \"अद्यतन करा\",\n  \"settingsUpdateWhatsNew\": \"काय नवीन आहे\"\n},\n  \"lockPage\": {\n  \"lockPage\": \"लॉक केलेले\",\n  \"reLockPage\": \"पुन्हा लॉक करा\",\n  \"lockTooltip\": \"अनवधानाने संपादन टाळण्यासाठी पृष्ठ लॉक केले आहे. अनलॉक करण्यासाठी क्लिक करा.\",\n  \"pageLockedToast\": \"पृष्ठ लॉक केले आहे. कोणी अनलॉक करेपर्यंत संपादन अक्षम आहे.\",\n  \"lockedOperationTooltip\": \"पृष्ठ अनवधानाने संपादनापासून वाचवण्यासाठी लॉक केले आहे.\"\n},\n  \"suggestion\": {\n  \"accept\": \"स्वीकारा\",\n  \"keep\": \"जसे आहे तसे ठेवा\",\n  \"discard\": \"रद्द करा\",\n  \"close\": \"बंद करा\",\n  \"tryAgain\": \"पुन्हा प्रयत्न करा\",\n  \"rewrite\": \"पुन्हा लिहा\",\n  \"insertBelow\": \"खाली टाका\"\n}\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/build.yaml",
    "content": ""
  },
  {
    "path": "frontend/appflowy_flutter/cargokit_options.yaml",
    "content": "use_precompiled_binaries: true\n"
  },
  {
    "path": "frontend/appflowy_flutter/dart_dependency_validator.yaml",
    "content": "# dart_dependency_validator.yaml\n\nallow_pins: true\n\ninclude:\n  - \"lib/**\"\n\nexclude:\n  - \"packages/**\"\n\nignore:\n  - analyzer\n"
  },
  {
    "path": "frontend/appflowy_flutter/dev.env",
    "content": "APPFLOWY_CLOUD_URL="
  },
  {
    "path": "frontend/appflowy_flutter/devtools_options.yaml",
    "content": "extensions:\n"
  },
  {
    "path": "frontend/appflowy_flutter/distribute_options.yaml",
    "content": "output: dist/\nreleases:\n  - name: dev\n    jobs:\n      - name: release-dev-linux-deb\n        package:\n          platform: linux\n          target: deb\n      - name: release-dev-linux-rpm\n        package:\n          platform: linux\n          target: rpm\n"
  },
  {
    "path": "frontend/appflowy_flutter/dsa_pub.pem",
    "content": "-----BEGIN PUBLIC KEY-----\nMIIGQzCCBDUGByqGSM44BAEwggQoAoICAQDlkozRmUnVH1MJFqOamAmUYu0YruaT\nrrt6rCIZ0LFrfNnmHA4LOQEcXwBTTyn5sBmkPq+lb/rjmERKhmvl1rfo6q7tJ8mG\n4TWqSu0tOJQ6QxexnNW4yhzK/r9MS5MQus4Al+y2hQLaAMOUIOnaWIrC9OHy7xyw\n+sVipECVKyQqipS4shGUSqbcN+ocQuTB+I0MtIjBii0DGSEY3pxQrfNWjHFL7iTV\nKiTn3YOWPJQvv3FvEDrN+5xU5JZpD97ZhXaJpLUyOQaNvcPaOELPWcOSJwqHOpf5\nb5N/VZ8SGbHNdxy9d5sSChBgtuAOihEhSp6SjFQ9eVHOf4NyJwSEMmi0gpdpqm4Z\nQRJUnM2zIi0p9twR9FRYXzrxOs6yGCQEY+xFG93ShTLTj3zMrIyFqBsqEwFyJiJW\nYWe/zp0V7UlLP+jXO9u9eghNmly7QVqD2P0qs/1V0jZFRuLWpsv4inau/qMZ5EhG\nG4xCJZXfN1pkehy6e05/h+vs5anK3Wa/H8AtY6cK4CpzAanELvn3AH7VLbAhLswu\n6d5CV+DoFgxCWMzGBSdmCYU+2wRLaL8Q9TZHDR+pvQlunEFdfFoGES9WjBPhAsVA\n6Mq22U8XSje9yHI3X9Eqe/7a+ajSgcGmB7oQ11+4xf5h2PtubRW/JL0KMjxCxMTp\nq1md6Ndx/ptBUwIdAIOyiKb2YcTLWAOt+LAlRXMsY1+W4pTXJfV6RcMCggIAPxbd\n0HNj2O/aQhJxNZDMBIcx6+cZ+LKch7qLcaEpVqWHvDSnR2eOJJzWn0RoKK+Vuix/\n4T8texSQkWxAeFFdo6kyrR9XNL7hqEFFq8o9VpmvRzvG6h/bBgh3AHAQE3p/8Wrb\nK13IhnlWqd0MjFufSphm63o0gaWl95j+6KeUoKQnioetu9HiMtFKx0d/KYqTQJg7\nhvR6VNCU2oShfXR3ce7RnUYwD37+djrUjUkoAZkZq2KoxBiKyeoSIeqAme19tKcO\ns6b17mhALELuJ+NtDwlDunyiCDUYX9lTPijHwKeIFtBs38+OtRk3aIqmWTQdbsCz\nAxp+kUMA5ESBME/RBNCSPHuDvtA3wfWvNbA5DXfZLwCgNSxhekq8XntIsRzfJ4v4\nuPzKFcVM3+sUUfSF04HHC9ol+PpLqXUyMnskiizqxFPq7H+6tyFZ7X2HiG6TjcfV\nWthmv+JyfcABjVnk2qFH7GagENbdtYmfUox13LhE59Sh5chaJnCFtCDp8NClWgZn\nixCOFQ9EgTLaH6MovTvWpEgG2MfBCu5SMUHi2qSflorqpRFH+rA7NZSnyz3wm7NB\n+fJSOP0IjEkOh7MafU6Z61oK9WY/Fc+F1zIENVv8PUc3p75y/4RAp4xzyKcTilaN\nC9U/3MRr3QmWwY7ejtZx6xdOxsvWBRDRSNbDdIkDggIGAAKCAgEAt1DHYZoeXY0r\nvYXmxdNO6zfnbz1GGZHXpakzm9h4BrxPDP5J8DQ9ZeVVKg5+cU9AyMO3cZHp7wkx\nk6IB+ZDUpqO1D3lWriRl2fI8cS4edI0fzpnW1nyhhFD4MbKmP+v27aH+DhZ4Up3y\nGMmJTLmKiYx1EgZp7Sx77PBYDVMsKKd3h9+Hjp2YtUTfD2lleAmC+wcQGZiNtGw/\neKpsmUVnWrepOdntWTtCQi1OvfcHaF2QmgktCq+68hbDNYWaXmzVIiQqrdv/zzOG\nhCFIrRGWemrxL0iFG4Pzc4UfOINsISQLcUxRuF6pQWPxF8O/mWKfzAeqWxmIujUM\nEoSEuI3yQ8VjlYpW/8FSK7UhgnHBHOpCJWPWs/vQXAnaUR2PYyzuIzhVEhFs8YA8\niBIKnixIC2hu0YbEk3TBr/TRcbd7mDw9Mq7NT88xzdU13+Wh+4zhdX3rtBHYzBtI\n7GaONGUNyY4h0duoyLpH6dxevaeKN6/bEdzYESjoE58QA88CpnAZGhJVphAba4cb\nw6GTDhK3RlPWh6hRqJwLDILGtnJS3UKeBDRmKMqNuqmHqPjyAAvt9JBO8lzjoLgf\n1cDsXHNWBVwA2jsX2CukNJPlY1Fa3MWhdaUXmy6QGMSisr1sptvBt1Phry8T2u+P\nY29SB4jvwqls268rP0cWqy4WXwlVwuc=\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/board/board_add_row_test.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/board/presentation/board_page.dart';\nimport 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';\nimport 'package:appflowy/plugins/database/widgets/card/card.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_board/appflowy_board.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nconst defaultFirstCardName = 'Card 1';\nconst defaultLastCardName = 'Card 3';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('board add row test:', () {\n    testWidgets('from header', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);\n\n      final firstCard = find.byType(RowCard).first;\n\n      expect(\n        find.descendant(\n          of: firstCard,\n          matching: find.text(defaultFirstCardName),\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tap(\n        find\n            .descendant(\n              of: find.byType(BoardColumnHeader),\n              matching: find.byWidgetPredicate(\n                (widget) => widget is FlowySvg && widget.svg == FlowySvgs.add_s,\n              ),\n            )\n            .at(1),\n      );\n      await tester.pumpAndSettle();\n\n      const newCardName = 'Card 4';\n      await tester.enterText(\n        find.descendant(\n          of: firstCard,\n          matching: find.byType(TextField),\n        ),\n        newCardName,\n      );\n      await tester.pumpAndSettle(const Duration(milliseconds: 500));\n\n      await tester.tap(find.byType(AppFlowyBoard));\n      await tester.pumpAndSettle();\n\n      expect(\n        find.descendant(\n          of: find.byType(RowCard).first,\n          matching: find.text(newCardName),\n        ),\n        findsOneWidget,\n      );\n    });\n\n    testWidgets('from footer', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);\n\n      final lastCard = find.byType(RowCard).last;\n\n      expect(\n        find.descendant(\n          of: lastCard,\n          matching: find.text(defaultLastCardName),\n        ),\n        findsOneWidget,\n      );\n\n      await tester.tapButton(\n        find.byType(BoardColumnFooter).at(1),\n      );\n\n      const newCardName = 'Card 4';\n      await tester.enterText(\n        find.descendant(\n          of: find.byType(BoardColumnFooter),\n          matching: find.byType(TextField),\n        ),\n        newCardName,\n      );\n      await tester.testTextInput.receiveAction(TextInputAction.done);\n      await tester.pumpAndSettle(const Duration(milliseconds: 500));\n\n      await tester.tap(find.byType(AppFlowyBoard));\n      await tester.pumpAndSettle();\n\n      expect(\n        find.descendant(\n          of: find.byType(RowCard).last,\n          matching: find.text(newCardName),\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/board/board_field_test.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('board field test', () {\n    testWidgets('change field type whithin card #5360', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);\n      const name = 'Card 1';\n      final card1 = find.text(name);\n      await tester.tapButton(card1);\n\n      const fieldName = \"test change field\";\n      await tester.createField(\n        FieldType.RichText,\n        name: fieldName,\n        layout: ViewLayoutPB.Board,\n      );\n      await tester.dismissRowDetailPage();\n      await tester.tapButton(card1);\n      await tester.changeFieldTypeOfFieldWithName(\n        fieldName,\n        FieldType.Checkbox,\n        layout: ViewLayoutPB.Board,\n      );\n      await tester.hoverOnWidget(find.text('Card 2'));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/board/board_group_test.dart",
    "content": "import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';\nimport 'package:appflowy/plugins/database/widgets/card/card.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_property.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('board group test:', () {\n    testWidgets('move row to another group', (tester) async {\n      const card1Name = 'Card 1';\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);\n      final card1 = find.ancestor(\n        of: find.text(card1Name),\n        matching: find.byType(RowCard),\n      );\n      final doingGroup = find.text('Doing');\n      final doingGroupCenter = tester.getCenter(doingGroup);\n      final card1Center = tester.getCenter(card1);\n\n      await tester.timedDrag(\n        card1,\n        doingGroupCenter.translate(-card1Center.dx, -card1Center.dy),\n        const Duration(seconds: 1),\n      );\n      await tester.pumpAndSettle();\n      await tester.tap(card1);\n      await tester.pumpAndSettle();\n\n      final card1StatusFinder = find.descendant(\n        of: find.byType(RowPropertyList),\n        matching: find.descendant(\n          of: find.byType(SelectOptionTag),\n          matching: find.byType(Text),\n        ),\n      );\n      expect(card1StatusFinder, findsNWidgets(1));\n      final card1StatusText = tester.widget<Text>(card1StatusFinder).data;\n      expect(card1StatusText, 'Doing');\n    });\n\n    testWidgets('rename group', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);\n\n      final headers = find.byType(BoardColumnHeader);\n      expect(headers, findsNWidgets(4));\n\n      // try to tap no status\n      final noStatus = headers.first;\n      expect(\n        find.descendant(of: noStatus, matching: find.text(\"No Status\")),\n        findsOneWidget,\n      );\n      await tester.tapButton(noStatus);\n      expect(\n        find.descendant(of: noStatus, matching: find.byType(TextField)),\n        findsNothing,\n      );\n\n      // tap on To Do and edit it\n      final todo = headers.at(1);\n      expect(\n        find.descendant(of: todo, matching: find.text(\"To Do\")),\n        findsOneWidget,\n      );\n      await tester.tapButton(todo);\n      await tester.enterText(\n        find.descendant(of: todo, matching: find.byType(TextField)),\n        \"tada\",\n      );\n      await tester.testTextInput.receiveAction(TextInputAction.done);\n      await tester.pumpAndSettle();\n\n      final newHeaders = find.byType(BoardColumnHeader);\n      expect(newHeaders, findsNWidgets(4));\n      final tada = find.byType(BoardColumnHeader).at(1);\n      expect(\n        find.descendant(of: tada, matching: find.byType(TextField)),\n        findsNothing,\n      );\n      expect(\n        find.descendant(\n          of: tada,\n          matching: find.text(\"tada\"),\n        ),\n        findsOneWidget,\n      );\n    });\n\n    testWidgets('edit select option from row detail', (tester) async {\n      const card1Name = 'Card 1';\n\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);\n\n      await tester.tapButton(\n        find.descendant(\n          of: find.byType(RowCard),\n          matching: find.text(card1Name),\n        ),\n      );\n\n      await tester.tapGridFieldWithNameInRowDetailPage(\"Status\");\n      await tester.tapButton(\n        find.byWidgetPredicate(\n          (widget) =>\n              widget is SelectOptionTagCell && widget.option.name == \"To Do\",\n        ),\n      );\n      final editor = find.byType(SelectOptionEditor);\n      await tester.enterText(\n        find.descendant(of: editor, matching: find.byType(TextField)),\n        \"tada\",\n      );\n      await tester.testTextInput.receiveAction(TextInputAction.done);\n      await tester.pumpAndSettle();\n\n      await tester.dismissFieldEditor();\n      await tester.dismissRowDetailPage();\n\n      final newHeaders = find.byType(BoardColumnHeader);\n      expect(newHeaders, findsNWidgets(4));\n      final tada = find.byType(BoardColumnHeader).at(1);\n      expect(\n        find.descendant(of: tada, matching: find.byType(TextField)),\n        findsNothing,\n      );\n      expect(\n        find.descendant(\n          of: tada,\n          matching: find.text(\"tada\"),\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/board/board_hide_groups_test.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';\nimport 'package:appflowy/plugins/database/board/presentation/widgets/board_hidden_groups.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('board group options:', () {\n    testWidgets('expand/collapse hidden groups', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);\n\n      final collapseFinder = find.byFlowySvg(FlowySvgs.pull_left_outlined_s);\n      final expandFinder = find.byFlowySvg(FlowySvgs.hamburger_s_s);\n\n      // Is expanded by default\n      expect(collapseFinder, findsNothing);\n      expect(expandFinder, findsOneWidget);\n\n      // Collapse hidden groups\n      await tester.tap(expandFinder);\n      await tester.pumpAndSettle();\n\n      // Is collapsed\n      expect(collapseFinder, findsOneWidget);\n      expect(expandFinder, findsNothing);\n\n      // Expand hidden groups\n      await tester.tap(collapseFinder);\n      await tester.pumpAndSettle();\n\n      // Is expanded\n      expect(collapseFinder, findsNothing);\n      expect(expandFinder, findsOneWidget);\n    });\n\n    testWidgets('hide first group, and show it again', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);\n\n      final expandFinder = find.byFlowySvg(FlowySvgs.hamburger_s_s);\n      await tester.tapButton(expandFinder);\n\n      // Tap the options of the first group\n      final optionsFinder = find\n          .descendant(\n            of: find.byType(BoardColumnHeader),\n            matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),\n          )\n          .first;\n\n      await tester.tap(optionsFinder);\n      await tester.pumpAndSettle();\n\n      // Tap the hide option\n      await tester.tap(find.byFlowySvg(FlowySvgs.hide_s));\n      await tester.pumpAndSettle();\n\n      int shownGroups =\n          tester.widgetList(find.byType(BoardColumnHeader)).length;\n\n      // We still show Doing, Done, No Status\n      expect(shownGroups, 3);\n\n      final hiddenCardFinder = find.byType(HiddenGroupCard);\n      await tester.hoverOnWidget(hiddenCardFinder);\n      await tester.tap(find.byFlowySvg(FlowySvgs.show_m));\n      await tester.pumpAndSettle();\n\n      shownGroups = tester.widgetList(find.byType(BoardColumnHeader)).length;\n      expect(shownGroups, 4);\n    });\n\n    testWidgets('delete a group', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);\n\n      expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 4);\n\n      // tap group option button for the first group. Delete shouldn't show up\n      await tester.tapButton(\n        find\n            .descendant(\n              of: find.byType(BoardColumnHeader),\n              matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),\n            )\n            .first,\n      );\n      expect(find.byFlowySvg(FlowySvgs.delete_s), findsNothing);\n\n      // dismiss the popup\n      await tester.sendKeyEvent(LogicalKeyboardKey.escape);\n      await tester.pumpAndSettle();\n\n      // tap group option button for the first group. Delete should show up\n      await tester.tapButton(\n        find\n            .descendant(\n              of: find.byType(BoardColumnHeader),\n              matching: find.byFlowySvg(FlowySvgs.details_horizontal_s),\n            )\n            .at(1),\n      );\n      expect(find.byFlowySvg(FlowySvgs.delete_s), findsOneWidget);\n\n      // Tap the delete button and confirm\n      await tester.tapButton(find.byFlowySvg(FlowySvgs.delete_s));\n      await tester.tapButtonWithName(LocaleKeys.space_delete.tr());\n\n      // Expect number of groups to decrease by one\n      expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 3);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/board/board_row_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/widgets/card/card.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_board/appflowy_board.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:time/time.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('board row test', () {\n    testWidgets('edit item in ToDo card', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);\n      const name = 'Card 1';\n      final card1 = find.ancestor(\n        matching: find.byType(RowCard),\n        of: find.text(name),\n      );\n      await tester.hoverOnWidget(\n        card1,\n        onHover: () async {\n          final editCard = find.byType(EditCardAccessory);\n          await tester.tapButton(editCard);\n        },\n      );\n      await tester.showKeyboard(card1);\n      tester.testTextInput.enterText(\"\");\n      await tester.pump(300.milliseconds);\n      tester.testTextInput.enterText(\"a\");\n      await tester.pump(300.milliseconds);\n      expect(find.text('a'), findsOneWidget);\n    });\n\n    testWidgets('delete item in ToDo card', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);\n      const name = 'Card 1';\n      final card1 = find.text(name);\n      await tester.hoverOnWidget(\n        card1,\n        onHover: () async {\n          final moreOption = find.byType(MoreCardOptionsAccessory);\n          await tester.tapButton(moreOption);\n        },\n      );\n      await tester.tapButtonWithName(LocaleKeys.button_delete.tr());\n      await tester.tapButtonWithName(LocaleKeys.button_delete.tr());\n      expect(find.text(name), findsNothing);\n    });\n\n    testWidgets('duplicate item in ToDo card', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);\n      const name = 'Card 1';\n      final card1 = find.text(name);\n      await tester.hoverOnWidget(\n        card1,\n        onHover: () async {\n          final moreOption = find.byType(MoreCardOptionsAccessory);\n          await tester.tapButton(moreOption);\n        },\n      );\n      await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr());\n      expect(find.textContaining(name, findRichText: true), findsNWidgets(2));\n    });\n\n    testWidgets('duplicate item in ToDo card then delete', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);\n      const name = 'Card 1';\n      final card1 = find.text(name);\n      await tester.hoverOnWidget(\n        card1,\n        onHover: () async {\n          final moreOption = find.byType(MoreCardOptionsAccessory);\n          await tester.tapButton(moreOption);\n        },\n      );\n      await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr());\n      expect(find.textContaining(name, findRichText: true), findsNWidgets(2));\n\n      // get the last widget that contains the name\n      final duplicatedCard = find.textContaining(name, findRichText: true).last;\n      await tester.hoverOnWidget(\n        duplicatedCard,\n        onHover: () async {\n          final moreOption = find.byType(MoreCardOptionsAccessory);\n          await tester.tapButton(moreOption);\n        },\n      );\n      await tester.tapButtonWithName(LocaleKeys.button_delete.tr());\n      await tester.tapButtonWithName(LocaleKeys.button_delete.tr());\n      expect(find.textContaining(name, findRichText: true), findsNWidgets(1));\n    });\n\n    testWidgets('add new group', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);\n\n      // assert number of groups\n      tester.assertNumberOfGroups(4);\n\n      // scroll the board horizontally to ensure add new group button appears\n      await tester.scrollBoardToEnd();\n\n      // assert and click on add new group button\n      tester.assertNewGroupTextField(false);\n      await tester.tapNewGroupButton();\n      tester.assertNewGroupTextField(true);\n\n      // enter new group name and submit\n      await tester.enterNewGroupName('needs design', submit: true);\n\n      // assert number of groups has increased\n      tester.assertNumberOfGroups(5);\n\n      // assert text field has disappeared\n      await tester.scrollBoardToEnd();\n      tester.assertNewGroupTextField(false);\n\n      // click on add new group button\n      await tester.tapNewGroupButton();\n      tester.assertNewGroupTextField(true);\n\n      // type some things\n      await tester.enterNewGroupName('needs planning', submit: false);\n\n      // click on clear button and assert empty contents\n      await tester.clearNewGroupTextField();\n\n      // press escape to cancel\n      await tester.sendKeyEvent(LogicalKeyboardKey.escape);\n      await tester.pumpAndSettle();\n      tester.assertNewGroupTextField(false);\n\n      // click on add new group button\n      await tester.tapNewGroupButton();\n      tester.assertNewGroupTextField(true);\n\n      // press elsewhere to cancel\n      await tester.tap(find.byType(AppFlowyBoard));\n      await tester.pumpAndSettle();\n      tester.assertNewGroupTextField(false);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'board_add_row_test.dart' as board_add_row_test;\nimport 'board_group_test.dart' as board_group_test;\nimport 'board_row_test.dart' as board_row_test;\nimport 'board_field_test.dart' as board_field_test;\nimport 'board_hide_groups_test.dart' as board_hide_groups_test;\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  // Board integration tests\n  board_row_test.main();\n  board_add_row_test.main();\n  board_group_test.main();\n  board_field_test.main();\n  board_hide_groups_test.main();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/chat/chat_page_test.dart",
    "content": "import 'package:appflowy/plugins/ai_chat/presentation/chat_page/chat_animation_list_widget.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/ai_test_op.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('chat page:', () {\n    testWidgets('send messages with default messages', (tester) async {\n      skipAIChatWelcomePage = true;\n\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a chat page\n      final pageName = 'Untitled';\n      await tester.createNewPageWithNameUnderParent(\n        name: pageName,\n        layout: ViewLayoutPB.Chat,\n        openAfterCreated: false,\n      );\n\n      await tester.pumpAndSettle(const Duration(milliseconds: 300));\n\n      final userId = '457037009907617792';\n      final user = User(id: userId, lastName: 'Lucas');\n      final aiUserId = '0';\n      final aiUser = User(id: aiUserId, lastName: 'AI');\n\n      await tester.loadDefaultMessages(\n        [\n          Message.text(\n            id: '1746776401',\n            text: 'How to use Kanban to manage tasks?',\n            author: user,\n            createdAt: DateTime.now().add(const Duration(seconds: 1)),\n          ),\n          Message.text(\n            id: '1746776401_ans',\n            text:\n                'I couldn’t find any relevant information in the sources you selected. Please try asking a different question',\n            author: aiUser,\n            createdAt: DateTime.now().add(const Duration(seconds: 2)),\n          ),\n          Message.text(\n            id: '1746776402',\n            text: 'How to use Kanban to manage tasks?',\n            author: user,\n            createdAt: DateTime.now().add(const Duration(seconds: 3)),\n          ),\n          Message.text(\n            id: '1746776402_ans',\n            text:\n                'I couldn’t find any relevant information in the sources you selected. Please try asking a different question',\n            author: aiUser,\n            createdAt: DateTime.now().add(const Duration(seconds: 4)),\n          ),\n        ].reversed.toList(),\n      );\n      await tester.pumpAndSettle(Duration(seconds: 1));\n\n      // start chat\n      final int messageId = 1;\n\n      // send a message\n      await tester.sendUserMessage(\n        Message.text(\n          id: messageId.toString(),\n          text: 'How to use AppFlowy?',\n          author: user,\n          createdAt: DateTime.now(),\n        ),\n      );\n      await tester.pumpAndSettle(Duration(seconds: 1));\n\n      // receive a message\n      await tester.receiveAIMessage(\n        Message.text(\n          id: '${messageId}_ans',\n          text: '''# How to Use AppFlowy\n- Download and install AppFlowy from the official website (appflowy.io) or through app stores for your operating system (Windows, macOS, Linux, or mobile)\n- Create an account or sign in when you first launch the app\n- The main interface shows your workspace with a sidebar for navigation and a content area''',\n          author: aiUser,\n          createdAt: DateTime.now(),\n        ),\n      );\n      await tester.pumpAndSettle(Duration(seconds: 1));\n\n      final chatBloc = tester.getCurrentChatBloc();\n      expect(chatBloc.chatController.messages.length, equals(6));\n    });\n\n    testWidgets('send messages without default messages', (tester) async {\n      skipAIChatWelcomePage = true;\n\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a chat page\n      final pageName = 'Untitled';\n      await tester.createNewPageWithNameUnderParent(\n        name: pageName,\n        layout: ViewLayoutPB.Chat,\n        openAfterCreated: false,\n      );\n\n      await tester.pumpAndSettle(const Duration(milliseconds: 300));\n\n      final userId = '457037009907617792';\n      final user = User(id: userId, lastName: 'Lucas');\n      final aiUserId = '0';\n      final aiUser = User(id: aiUserId, lastName: 'AI');\n\n      // start chat\n      int messageId = 1;\n\n      // round 1\n      {\n        // send a message\n        await tester.sendUserMessage(\n          Message.text(\n            id: messageId.toString(),\n            text: 'How to use AppFlowy?',\n            author: user,\n            createdAt: DateTime.now(),\n          ),\n        );\n        await tester.pumpAndSettle(Duration(seconds: 1));\n\n        // receive a message\n        await tester.receiveAIMessage(\n          Message.text(\n            id: '${messageId}_ans',\n            text: '''# How to Use AppFlowy\n- Download and install AppFlowy from the official website (appflowy.io) or through app stores for your operating system (Windows, macOS, Linux, or mobile)\n- Create an account or sign in when you first launch the app\n- The main interface shows your workspace with a sidebar for navigation and a content area''',\n            author: aiUser,\n            createdAt: DateTime.now(),\n          ),\n        );\n        await tester.pumpAndSettle(Duration(seconds: 1));\n        messageId++;\n      }\n\n      // round 2\n      {\n        // send a message\n        await tester.sendUserMessage(\n          Message.text(\n            id: messageId.toString(),\n            text: 'How to use AppFlowy?',\n            author: user,\n            createdAt: DateTime.now(),\n          ),\n        );\n        await tester.pumpAndSettle(Duration(seconds: 1));\n\n        // receive a message\n        await tester.receiveAIMessage(\n          Message.text(\n            id: '${messageId}_ans',\n            text:\n                'I couldn’t find any relevant information in the sources you selected. Please try asking a different question',\n            author: aiUser,\n            createdAt: DateTime.now(),\n          ),\n        );\n        await tester.pumpAndSettle(Duration(seconds: 1));\n        messageId++;\n      }\n\n      // round 3\n      {\n        // send a message\n        await tester.sendUserMessage(\n          Message.text(\n            id: messageId.toString(),\n            text: 'What document formatting options are available?',\n            author: user,\n            createdAt: DateTime.now(),\n          ),\n        );\n        await tester.pumpAndSettle(Duration(seconds: 1));\n\n        // receive a message\n        await tester.receiveAIMessage(\n          Message.text(\n            id: '${messageId}_ans',\n            text:\n                '# AppFlowy Document Formatting\\n- Basic formatting: Bold, italic, underline, strikethrough\\n- Headings: 6 levels of headings for structuring content\\n- Lists: Bullet points, numbered lists, and checklists\\n- Code blocks: Format text as code with syntax highlighting\\n- Tables: Create and format data tables\\n- Embedded content: Add images, files, and other rich media',\n            author: aiUser,\n            createdAt: DateTime.now(),\n          ),\n        );\n        await tester.pumpAndSettle(Duration(seconds: 1));\n        messageId++;\n      }\n\n      // round 4\n      {\n        // send a message\n        await tester.sendUserMessage(\n          Message.text(\n            id: messageId.toString(),\n            text: 'How do I export my data from AppFlowy?',\n            author: user,\n            createdAt: DateTime.now(),\n          ),\n        );\n        await tester.pumpAndSettle(Duration(seconds: 1));\n\n        // receive a message\n        await tester.receiveAIMessage(\n          Message.text(\n            id: '${messageId}_ans',\n            text:\n                '# Exporting from AppFlowy\\n- Export documents in multiple formats: Markdown, HTML, PDF\\n- Export databases as CSV or Excel files\\n- Batch export entire workspaces for backup\\n- Use the export menu (three dots → Export) on any page\\n- Exported files maintain most formatting and structure',\n            author: aiUser,\n            createdAt: DateTime.now(),\n          ),\n        );\n        await tester.pumpAndSettle(Duration(seconds: 1));\n        messageId++;\n      }\n\n      // round 5\n      {\n        // send a message\n        await tester.sendUserMessage(\n          Message.text(\n            id: messageId.toString(),\n            text: 'Is there a mobile version of AppFlowy?',\n            author: user,\n            createdAt: DateTime.now(),\n          ),\n        );\n        await tester.pumpAndSettle(Duration(seconds: 1));\n\n        // receive a message\n        await tester.receiveAIMessage(\n          Message.text(\n            id: '${messageId}_ans',\n            text:\n                '# AppFlowy on Mobile\\n- Yes, AppFlowy is available for iOS and Android devices\\n- Download from the App Store or Google Play Store\\n- Mobile app includes core functionality: document editing, databases, and boards\\n- Offline mode allows working without internet connection\\n- Sync automatically when you reconnect\\n- Responsive design adapts to different screen sizes',\n            author: aiUser,\n            createdAt: DateTime.now(),\n          ),\n        );\n        await tester.pumpAndSettle(Duration(seconds: 1));\n        messageId++;\n      }\n\n      final chatBloc = tester.getCurrentChatBloc();\n      expect(chatBloc.chatController.messages.length, equals(10));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/cloud_runner.dart",
    "content": "import 'data_migration/data_migration_test_runner.dart'\n    as data_migration_test_runner;\nimport 'database/database_test_runner.dart' as database_test_runner;\nimport 'document/document_test_runner.dart' as document_test_runner;\nimport 'set_env.dart' as preset_af_cloud_env_test;\nimport 'sidebar/sidebar_icon_test.dart' as sidebar_icon_test;\nimport 'sidebar/sidebar_search_test.dart' as sidebar_search_test;\nimport 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test;\nimport 'sidebar/sidebar_rename_untitled_test.dart'\n    as sidebar_rename_untitled_test;\nimport 'uncategorized/uncategorized_test_runner.dart'\n    as uncategorized_test_runner;\nimport 'workspace/workspace_test_runner.dart' as workspace_test_runner;\n\nFuture<void> main() async {\n  // don't remove this test, it can prevent the test from failing.\n  {\n    preset_af_cloud_env_test.main();\n    data_migration_test_runner.main();\n\n    // uncategorized\n    uncategorized_test_runner.main();\n\n    // workspace\n    workspace_test_runner.main();\n  }\n\n  // sidebar\n  // don't remove this test, it can prevent the test from failing.\n  {\n    preset_af_cloud_env_test.main();\n    sidebar_move_page_test.main();\n    sidebar_rename_untitled_test.main();\n    sidebar_icon_test.main();\n    sidebar_search_test.main();\n  }\n\n  // database\n  // don't remove this test, it can prevent the test from failing.\n  {\n    preset_af_cloud_env_test.main();\n    database_test_runner.main();\n  }\n\n  // document\n  // don't remove this test, it can prevent the test from failing.\n  {\n    preset_af_cloud_env_test.main();\n    document_test_runner.main();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/anon_user_data_migration_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/workspace/application/settings/prelude.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/account.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('appflowy cloud', () {\n    testWidgets('anon user -> sign in -> open imported space', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n\n      await tester.tapAnonymousSignInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      const pageName = 'Test Document';\n      await tester.createNewPageWithNameUnderParent(name: pageName);\n      tester.expectToSeePageName(pageName);\n\n      // rename the name of the anon user\n      await tester.openSettings();\n      await tester.openSettingsPage(SettingsPage.account);\n      await tester.pumpAndSettle();\n\n      await tester.enterUserName('local_user');\n\n      // Scroll to sign-in\n      await tester.tapButton(find.byType(AccountSignInOutButton));\n\n      // sign up with Google\n      await tester.tapGoogleLoginInButton();\n      // await tester.pumpAndSettle(const Duration(seconds: 16));\n\n      // open the imported space\n      await tester.expectToSeeHomePage();\n      await tester.clickSpaceHeader();\n\n      // After import the anon user data, we will create a new space for it\n      await tester.openSpace(\"Getting started\");\n      await tester.openPage(pageName);\n\n      await tester.pumpAndSettle();\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/data_migration_test_runner.dart",
    "content": "void main() async {\n  // anon_user_test.main();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/database/database_image_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart'\n    hide UploadImageMenu, ResizableImage;\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../../shared/constants.dart';\nimport '../../../shared/database_test_op.dart';\nimport '../../../shared/mock/mock_file_picker.dart';\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  // copy link to block\n  group('database image:', () {\n    testWidgets('insert image', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      // open the first row detail page and upload an image\n      await tester.createNewPageInSpace(\n        spaceName: Constants.generalSpaceName,\n        layout: ViewLayoutPB.Grid,\n        pageName: 'database image',\n      );\n      await tester.openFirstRowDetailPage();\n\n      // insert an image block\n      {\n        await tester.editor.tapLineOfEditorAt(0);\n        await tester.editor.showSlashMenu();\n        await tester.editor.tapSlashMenuItemWithName(\n          LocaleKeys.document_slashMenu_name_image.tr(),\n        );\n      }\n\n      // upload an image\n      {\n        final image = await rootBundle.load('assets/test/images/sample.jpeg');\n        final tempDirectory = await getTemporaryDirectory();\n        final imagePath = p.join(tempDirectory.path, 'sample.jpeg');\n        final file = File(imagePath)\n          ..writeAsBytesSync(image.buffer.asUint8List());\n\n        mockPickFilePaths(\n          paths: [imagePath],\n        );\n\n        await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');\n        await tester.tapButtonWithName(\n          LocaleKeys.document_imageBlock_upload_placeholder.tr(),\n        );\n        await tester.pumpAndSettle();\n        expect(find.byType(ResizableImage), findsOneWidget);\n        final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;\n        expect(node.type, ImageBlockKeys.type);\n        expect(node.attributes[ImageBlockKeys.url], isNotEmpty);\n\n        // remove the temp file\n        file.deleteSync();\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/database/database_test_runner.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'database_image_test.dart' as database_image_test;\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  database_image_test.main();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart",
    "content": "import 'package:appflowy/ai/service/ai_entities.dart';\nimport 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/ai_test_op.dart';\nimport '../../../shared/constants.dart';\nimport '../../../shared/mock/mock_ai.dart';\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('AI Writer:', () {\n    testWidgets('the ai writer transaction should only apply in memory',\n        (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n        aiRepositoryBuilder: () => MockAIRepository(),\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      const pageName = 'Document';\n      await tester.createNewPageInSpace(\n        spaceName: Constants.generalSpaceName,\n        layout: ViewLayoutPB.Document,\n        pageName: pageName,\n      );\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.showSlashMenu();\n      await tester.editor.tapSlashMenuItemWithName(\n        LocaleKeys.document_slashMenu_name_aiWriter.tr(),\n      );\n      expect(find.byType(AiWriterBlockComponent), findsOneWidget);\n\n      // switch to another page\n      await tester.openPage(Constants.gettingStartedPageName);\n      // switch back to the page\n      await tester.openPage(pageName);\n\n      // expect the ai writer block is not in the document\n      expect(find.byType(AiWriterBlockComponent), findsNothing);\n    });\n\n    testWidgets('Improve writing', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      const pageName = 'Document';\n      await tester.createNewPageInSpace(\n        spaceName: Constants.generalSpaceName,\n        layout: ViewLayoutPB.Document,\n        pageName: pageName,\n      );\n\n      await tester.editor.tapLineOfEditorAt(0);\n\n      // insert a paragraph\n      final text = 'I have an apple';\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.ime.insertText(text);\n      await tester.editor.updateSelection(\n        Selection(\n          start: Position(path: [0]),\n          end: Position(path: [0], offset: text.length),\n        ),\n      );\n\n      await tester.pumpAndSettle();\n      await tester.tapButton(find.byType(ImproveWritingButton));\n\n      final editorState = tester.editor.getCurrentEditorState();\n      final document = editorState.document;\n\n      expect(document.root.children.length, 3);\n      expect(document.root.children[1].type, ParagraphBlockKeys.type);\n      expect(\n        document.root.children[1].delta!.toPlainText(),\n        'I have an apple and a banana',\n      );\n    });\n\n    testWidgets('fix grammar', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      const pageName = 'Document';\n      await tester.createNewPageInSpace(\n        spaceName: Constants.generalSpaceName,\n        layout: ViewLayoutPB.Document,\n        pageName: pageName,\n      );\n\n      await tester.editor.tapLineOfEditorAt(0);\n\n      // insert a paragraph\n      final text = 'We didn’t had enough money';\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.ime.insertText(text);\n      await tester.editor.updateSelection(\n        Selection(\n          start: Position(path: [0]),\n          end: Position(path: [0], offset: text.length),\n        ),\n      );\n\n      await tester.pumpAndSettle();\n      await tester.selectAIWriter(AiWriterCommand.fixSpellingAndGrammar);\n\n      final editorState = tester.editor.getCurrentEditorState();\n      final document = editorState.document;\n\n      expect(document.root.children.length, 3);\n      expect(document.root.children[1].type, ParagraphBlockKeys.type);\n      expect(\n        document.root.children[1].delta!.toPlainText(),\n        'We didn’t have enough money',\n      );\n    });\n\n    testWidgets('ask ai', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudDevelop,\n        aiRepositoryBuilder: () => MockAIRepository(\n          validator: _CompletionHistoryValidator(),\n        ),\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      const pageName = 'Document';\n      await tester.createNewPageInSpace(\n        spaceName: Constants.generalSpaceName,\n        layout: ViewLayoutPB.Document,\n        pageName: pageName,\n      );\n\n      await tester.editor.tapLineOfEditorAt(0);\n\n      // insert a paragraph\n      final text = 'What is TPU?';\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.ime.insertText(text);\n      await tester.editor.updateSelection(\n        Selection(\n          start: Position(path: [0]),\n          end: Position(path: [0], offset: text.length),\n        ),\n      );\n\n      await tester.pumpAndSettle();\n      await tester.selectAIWriter(AiWriterCommand.userQuestion);\n\n      await tester.enterTextInPromptTextField(\"Explain the concept of TPU\");\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      await tester.pumpAndSettle();\n\n      // await tester.selectModel(\"GPT-4o-mini\");\n\n      await tester.enterTextInPromptTextField(\"How about GPU?\");\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      await tester.pumpAndSettle();\n    });\n  });\n}\n\nclass _CompletionHistoryValidator extends StreamCompletionValidator {\n  static const _expectedResponses = {\n    1: \"What is TPU?\",\n    3: [\n      \"What is TPU?\",\n      \"Explain the concept of TPU\",\n      \"TPU is a tensor processing unit that is designed to accelerate machine\",\n    ],\n    5: [\n      \"What is TPU?\",\n      \"Explain the concept of TPU\",\n      \"TPU is a tensor processing unit that is designed to accelerate machine\",\n      \"How about GPU?\",\n      \"GPU is a graphics processing unit that is designed to accelerate machine learning tasks.\",\n    ],\n  };\n\n  @override\n  bool validate(\n    String text,\n    String? objectId,\n    CompletionTypePB completionType,\n    PredefinedFormat? format,\n    List<String> sourceIds,\n    List<AiWriterRecord> history,\n  ) {\n    assert(completionType == CompletionTypePB.UserQuestion);\n    if (history.isEmpty) return false;\n\n    final expectedMessages = _expectedResponses[history.length];\n    if (expectedMessages == null) return false;\n\n    if (expectedMessages is String) {\n      _assertMessage(history[0].content, expectedMessages);\n      return true;\n    } else if (expectedMessages is List<String>) {\n      for (var i = 0; i < expectedMessages.length; i++) {\n        _assertMessage(history[i].content, expectedMessages[i]);\n      }\n      return true;\n    }\n\n    return false;\n  }\n\n  void _assertMessage(String actual, String expected) {\n    assert(\n      actual.trim() == expected,\n      \"expected '$expected', but got '${actual.trim()}'\",\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_copy_link_to_block_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/document_page.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/constants.dart';\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  // copy link to block\n  group('copy link to block:', () {\n    testWidgets('copy link to check if the clipboard has the correct content',\n        (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      // open getting started page\n      await tester.openPage(Constants.gettingStartedPageName);\n      await tester.editor.copyLinkToBlock([0]);\n      await tester.pumpAndSettle(Durations.short1);\n\n      // check the clipboard\n      final content = await Clipboard.getData(Clipboard.kTextPlain);\n      expect(\n        content?.text,\n        matches(appflowySharePageLinkPattern),\n      );\n    });\n\n    testWidgets('copy link to block(another page) and paste it in doc',\n        (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      // open getting started page\n      await tester.openPage(Constants.gettingStartedPageName);\n      await tester.editor.copyLinkToBlock([0]);\n\n      // create a new page and paste it\n      const pageName = 'copy link to block';\n      await tester.createNewPageInSpace(\n        spaceName: Constants.generalSpaceName,\n        layout: ViewLayoutPB.Document,\n        pageName: pageName,\n      );\n\n      // paste the link to the new page\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.paste();\n      await tester.pumpAndSettle();\n\n      // check the content of the block\n      final node = tester.editor.getNodeAtPath([0]);\n      final delta = node.delta!;\n      final insert = (delta.first as TextInsert).text;\n      final attributes = delta.first.attributes;\n      expect(insert, MentionBlockKeys.mentionChar);\n      final mention =\n          attributes?[MentionBlockKeys.mention] as Map<String, dynamic>;\n      expect(mention[MentionBlockKeys.type], MentionType.page.name);\n      expect(mention[MentionBlockKeys.blockId], isNotNull);\n      expect(mention[MentionBlockKeys.pageId], isNotNull);\n      expect(\n        find.descendant(\n          of: find.byType(AppFlowyEditor),\n          matching: find.textContaining(\n            Constants.gettingStartedPageName,\n            findRichText: true,\n          ),\n        ),\n        findsOneWidget,\n      );\n      expect(\n        find.descendant(\n          of: find.byType(AppFlowyEditor),\n          matching: find.textContaining(\n            // the pasted block content is 'Welcome to AppFlowy'\n            'Welcome to AppFlowy',\n            findRichText: true,\n          ),\n        ),\n        findsOneWidget,\n      );\n\n      // tap the mention block to jump to the page\n      await tester.tapButton(find.byType(MentionPageBlock));\n      await tester.pumpAndSettle();\n\n      // expect to go to the getting started page\n      final documentPage = find.byType(DocumentPage);\n      expect(documentPage, findsOneWidget);\n      expect(\n        tester.widget<DocumentPage>(documentPage).view.name,\n        Constants.gettingStartedPageName,\n      );\n      // and the block is selected\n      expect(\n        tester.widget<DocumentPage>(documentPage).initialBlockId,\n        mention[MentionBlockKeys.blockId],\n      );\n      expect(\n        tester.editor.getCurrentEditorState().selection,\n        Selection.collapsed(\n          Position(\n            path: [0],\n          ),\n        ),\n      );\n    });\n\n    testWidgets('copy link to block(same page) and paste it in doc',\n        (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      // create a new page and paste it\n      const pageName = 'copy link to block';\n      await tester.createNewPageInSpace(\n        spaceName: Constants.generalSpaceName,\n        layout: ViewLayoutPB.Document,\n        pageName: pageName,\n      );\n\n      // copy the link to block from the first line\n      const inputText = 'Hello World';\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.ime.insertText(inputText);\n      await tester.ime.insertCharacter('\\n');\n      await tester.pumpAndSettle();\n      await tester.editor.copyLinkToBlock([0]);\n\n      // paste the link to the second line\n      await tester.editor.tapLineOfEditorAt(1);\n      await tester.editor.paste();\n      await tester.pumpAndSettle();\n\n      // check the content of the block\n      final node = tester.editor.getNodeAtPath([1]);\n      final delta = node.delta!;\n      final insert = (delta.first as TextInsert).text;\n      final attributes = delta.first.attributes;\n      expect(insert, MentionBlockKeys.mentionChar);\n      final mention =\n          attributes?[MentionBlockKeys.mention] as Map<String, dynamic>;\n      expect(mention[MentionBlockKeys.type], MentionType.page.name);\n      expect(mention[MentionBlockKeys.blockId], isNotNull);\n      expect(mention[MentionBlockKeys.pageId], isNotNull);\n      expect(\n        find.descendant(\n          of: find.byType(AppFlowyEditor),\n          matching: find.textContaining(\n            inputText,\n            findRichText: true,\n          ),\n        ),\n        findsNWidgets(2),\n      );\n\n      // edit the pasted block\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.ime.insertText('!');\n      await tester.pumpAndSettle();\n\n      // check the content of the block\n      expect(\n        find.descendant(\n          of: find.byType(AppFlowyEditor),\n          matching: find.textContaining(\n            '$inputText!',\n            findRichText: true,\n          ),\n        ),\n        findsNWidgets(2),\n      );\n\n      // tap the mention block\n      await tester.tapButton(find.byType(MentionPageBlock));\n      expect(\n        tester.editor.getCurrentEditorState().selection,\n        Selection.collapsed(\n          Position(\n            path: [0],\n          ),\n        ),\n      );\n    });\n\n    testWidgets('''1. copy link to block from another page\n    2. paste the link to the new page\n    3. delete the original page\n    4. check the content of the block, it should be no access to the page\n        ''', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      // open getting started page\n      await tester.openPage(Constants.gettingStartedPageName);\n      await tester.editor.copyLinkToBlock([0]);\n\n      // create a new page and paste it\n      const pageName = 'copy link to block';\n      await tester.createNewPageInSpace(\n        spaceName: Constants.generalSpaceName,\n        layout: ViewLayoutPB.Document,\n        pageName: pageName,\n      );\n\n      // paste the link to the new page\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.paste();\n      await tester.pumpAndSettle();\n\n      // tap the mention block to jump to the page\n      await tester.tapButton(find.byType(MentionPageBlock));\n      await tester.pumpAndSettle();\n\n      // expect to go to the getting started page\n      final documentPage = find.byType(DocumentPage);\n      expect(documentPage, findsOneWidget);\n      expect(\n        tester.widget<DocumentPage>(documentPage).view.name,\n        Constants.gettingStartedPageName,\n      );\n      // delete the getting started page\n      await tester.hoverOnPageName(\n        Constants.gettingStartedPageName,\n        onHover: () async => tester.tapDeletePageButton(),\n      );\n      tester.expectToSeeDocumentBanner();\n      tester.expectNotToSeePageName(gettingStarted);\n\n      // delete the page permanently\n      await tester.tapDeletePermanentlyButton();\n\n      // go back the page\n      await tester.openPage(pageName);\n      await tester.pumpAndSettle();\n\n      // check the content of the block\n      // it should be no access to the page\n      expect(\n        find.descendant(\n          of: find.byType(AppFlowyEditor),\n          matching: find.findTextInFlowyText(\n            LocaleKeys.document_mention_noAccess.tr(),\n          ),\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_option_actions_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/constants.dart';\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('document option actions:', () {\n    testWidgets('drag block to the top', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      // open getting started page\n      await tester.openPage(Constants.gettingStartedPageName);\n\n      // before move\n      final beforeMoveBlock = tester.editor.getNodeAtPath([1]);\n\n      // move the desktop guide to the top, above the getting started\n      await tester.editor.dragBlock(\n        [1],\n        const Offset(20, -80),\n      );\n\n      // wait for the move animation to complete\n      await tester.pumpAndSettle(Durations.short1);\n\n      // check if the block is moved to the top\n      final afterMoveBlock = tester.editor.getNodeAtPath([0]);\n      expect(afterMoveBlock.delta, beforeMoveBlock.delta);\n    });\n\n    testWidgets('drag block to other block\\'s child', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      // open getting started page\n      await tester.openPage(Constants.gettingStartedPageName);\n\n      // before move\n      final beforeMoveBlock = tester.editor.getNodeAtPath([10]);\n\n      // move the checkbox to the child of the block at path [9]\n      await tester.editor.dragBlock(\n        [10],\n        const Offset(120, -20),\n      );\n\n      // wait for the move animation to complete\n      await tester.pumpAndSettle(Durations.short1);\n\n      // check if the block is moved to the child of the block at path [9]\n      final afterMoveBlock = tester.editor.getNodeAtPath([9, 0]);\n      expect(afterMoveBlock.delta, beforeMoveBlock.delta);\n    });\n\n    testWidgets('hover on the block and delete it', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      // open getting started page\n      await tester.openPage(Constants.gettingStartedPageName);\n\n      // before delete\n      final path = [1];\n      final beforeDeletedBlock = tester.editor.getNodeAtPath(path);\n\n      // hover on the block and delete it\n      final optionButton = find.byWidgetPredicate(\n        (widget) =>\n            widget is DraggableOptionButton &&\n            widget.blockComponentContext.node.path.equals(path),\n      );\n\n      await tester.hoverOnWidget(\n        optionButton,\n        onHover: () async {\n          // click the delete button\n          await tester.tapButton(optionButton);\n        },\n      );\n      await tester.pumpAndSettle(Durations.short1);\n\n      // click the delete button\n      final deleteButton =\n          find.findTextInFlowyText(LocaleKeys.button_delete.tr());\n      await tester.tapButton(deleteButton);\n\n      // wait for the deletion\n      await tester.pumpAndSettle(Durations.short1);\n\n      // check if the block is deleted\n      final afterDeletedBlock = tester.editor.getNodeAtPath([1]);\n      expect(afterDeletedBlock.id, isNot(equals(beforeDeletedBlock.id)));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_publish_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/shared/share/publish_tab.dart';\nimport 'package:appflowy/plugins/shared/share/share_menu.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/constants.dart';\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Publish:', () {\n    testWidgets('publish document', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      const pageName = 'Document';\n\n      await tester.createNewPageInSpace(\n        spaceName: Constants.generalSpaceName,\n        layout: ViewLayoutPB.Document,\n        pageName: pageName,\n      );\n\n      // open the publish menu\n      await tester.openPublishMenu();\n\n      // publish the document\n      final publishButton = find.byType(PublishButton);\n      final unpublishButton = find.byType(UnPublishButton);\n      await tester.tapButton(publishButton);\n\n      // expect to see unpublish, visit site and manage all sites button\n      expect(unpublishButton, findsOneWidget);\n      expect(find.text(LocaleKeys.shareAction_visitSite.tr()), findsOneWidget);\n\n      // unpublish the document\n      await tester.tapButton(unpublishButton);\n\n      // expect to see publish button\n      expect(publishButton, findsOneWidget);\n    });\n\n    testWidgets('rename path name', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      const pageName = 'Document';\n\n      await tester.createNewPageInSpace(\n        spaceName: Constants.generalSpaceName,\n        layout: ViewLayoutPB.Document,\n        pageName: pageName,\n      );\n\n      // open the publish menu\n      await tester.openPublishMenu();\n\n      // publish the document\n      final publishButton = find.byType(PublishButton);\n      await tester.tapButton(publishButton);\n\n      // rename the path name\n      final inputField = find.descendant(\n        of: find.byType(ShareMenu),\n        matching: find.byType(TextField),\n      );\n\n      // rename with invalid name\n      await tester.tap(inputField);\n      await tester.enterText(inputField, '&&&&????');\n      await tester.tapButton(find.text(LocaleKeys.button_save.tr()));\n      await tester.pumpAndSettle();\n\n      // expect to see the toast with error message\n      final errorToast1 = find.text(\n        LocaleKeys.settings_sites_error_publishNameContainsInvalidCharacters\n            .tr(),\n      );\n      await tester.pumpUntilFound(errorToast1);\n      await tester.pumpUntilNotFound(errorToast1);\n\n      // rename with long name\n      await tester.tap(inputField);\n      await tester.enterText(inputField, 'long-path-name' * 200);\n      await tester.tapButton(find.text(LocaleKeys.button_save.tr()));\n      await tester.pumpAndSettle();\n\n      // expect to see the toast with error message\n      final errorToast2 = find.text(\n        LocaleKeys.settings_sites_error_publishNameTooLong.tr(),\n      );\n      await tester.pumpUntilFound(errorToast2);\n      await tester.pumpUntilNotFound(errorToast2);\n\n      // rename with empty name\n      await tester.tap(inputField);\n      await tester.enterText(inputField, '');\n      await tester.tapButton(find.text(LocaleKeys.button_save.tr()));\n      await tester.pumpAndSettle();\n\n      // expect to see the toast with error message\n      final errorToast3 = find.text(\n        LocaleKeys.settings_sites_error_publishNameCannotBeEmpty.tr(),\n      );\n      await tester.pumpUntilFound(errorToast3);\n      await tester.pumpUntilNotFound(errorToast3);\n\n      // input the new path name\n      await tester.tap(inputField);\n      await tester.enterText(inputField, 'new-path-name');\n      // click save button\n      await tester.tapButton(find.text(LocaleKeys.button_save.tr()));\n      await tester.pumpAndSettle();\n\n      // expect to see the toast with success message\n      final successToast = find.text(\n        LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),\n      );\n      await tester.pumpUntilFound(successToast);\n      await tester.pumpUntilNotFound(successToast);\n\n      // click the copy link button\n      await tester.tapButton(\n        find.byWidgetPredicate(\n          (widget) =>\n              widget is FlowySvg &&\n              widget.svg.path == FlowySvgs.m_toolbar_link_m.path,\n        ),\n      );\n      await tester.pumpAndSettle();\n      // check the clipboard has the link\n      final content = await Clipboard.getData(Clipboard.kTextPlain);\n      expect(\n        content?.text?.contains('new-path-name'),\n        isTrue,\n      );\n    });\n\n    testWidgets('re-publish the document', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      const pageName = 'Document';\n\n      await tester.createNewPageInSpace(\n        spaceName: Constants.generalSpaceName,\n        layout: ViewLayoutPB.Document,\n        pageName: pageName,\n      );\n\n      // open the publish menu\n      await tester.openPublishMenu();\n\n      // publish the document\n      final publishButton = find.byType(PublishButton);\n      await tester.tapButton(publishButton);\n\n      // rename the path name\n      final inputField = find.descendant(\n        of: find.byType(ShareMenu),\n        matching: find.byType(TextField),\n      );\n\n      // input the new path name\n      const newName = 'new-path-name';\n      await tester.enterText(inputField, newName);\n      // click save button\n      await tester.tapButton(find.text(LocaleKeys.button_save.tr()));\n      await tester.pumpAndSettle();\n\n      // expect to see the toast with success message\n      final successToast = find.text(\n        LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),\n      );\n      await tester.pumpUntilNotFound(successToast);\n\n      // unpublish the document\n      final unpublishButton = find.byType(UnPublishButton);\n      await tester.tapButton(unpublishButton);\n\n      final unpublishSuccessToast = find.text(\n        LocaleKeys.publish_unpublishSuccessfully.tr(),\n      );\n      await tester.pumpUntilNotFound(unpublishSuccessToast);\n\n      // re-publish the document\n      await tester.tapButton(publishButton);\n\n      // expect to see the toast with success message\n      final rePublishSuccessToast = find.text(\n        LocaleKeys.publish_publishSuccessfully.tr(),\n      );\n      await tester.pumpUntilNotFound(rePublishSuccessToast);\n\n      // check the clipboard has the link\n      final content = await Clipboard.getData(Clipboard.kTextPlain);\n      expect(\n        content?.text?.contains(newName),\n        isTrue,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_test_runner.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'document_ai_writer_test.dart' as document_ai_writer_test;\nimport 'document_copy_link_to_block_test.dart'\n    as document_copy_link_to_block_test;\nimport 'document_option_actions_test.dart' as document_option_actions_test;\nimport 'document_publish_test.dart' as document_publish_test;\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  document_option_actions_test.main();\n  document_copy_link_to_block_test.main();\n  document_publish_test.main();\n  document_ai_writer_test.main();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/set_env.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\n// This test is meaningless, just for preventing the CI from failing.\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Preset cloud env', () {\n    testWidgets('use self-hosted cloud', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n\n      await tester.pumpAndSettle();\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/sidebar/sidebar_icon_test.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart';\nimport 'package:flowy_svg/flowy_svg.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/emoji.dart';\nimport '../../../shared/util.dart';\n\nvoid main() {\n  setUpAll(() {\n    IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n    RecentIcons.enable = false;\n  });\n\n  tearDownAll(() {\n    RecentIcons.enable = true;\n  });\n\n  testWidgets('Change slide bar space icon', (tester) async {\n    await tester.initializeAppFlowy(\n      cloudType: AuthenticatorType.appflowyCloudSelfHost,\n    );\n    await tester.tapGoogleLoginInButton();\n    await tester.expectToSeeHomePageWithGetStartedPage();\n    final emojiIconData = await tester.loadIcon();\n    final firstIcon = IconsData.fromJson(jsonDecode(emojiIconData.emoji));\n\n    await tester.hoverOnWidget(\n      find.byType(SidebarSpaceHeader),\n      onHover: () async {\n        final moreOption = find.byType(SpaceMorePopup);\n        await tester.tapButton(moreOption);\n        expect(find.byType(FlowyIconEmojiPicker), findsNothing);\n        await tester.tapSvgButton(SpaceMoreActionType.changeIcon.leftIconSvg);\n        expect(find.byType(FlowyIconEmojiPicker), findsOneWidget);\n      },\n    );\n\n    final icons = find.byWidgetPredicate(\n      (w) => w is FlowySvg && w.svgString == firstIcon.svgString,\n    );\n    expect(icons, findsOneWidget);\n    await tester.tapIcon(EmojiIconData.icon(firstIcon));\n\n    final spaceHeader = find.byType(SidebarSpaceHeader);\n    final spaceIcon = find.descendant(\n      of: spaceHeader,\n      matching: find.byWidgetPredicate(\n        (w) => w is FlowySvg && w.svgString == firstIcon.svgString,\n      ),\n    );\n    expect(spaceIcon, findsOneWidget);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/sidebar/sidebar_move_page_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../../../shared/constants.dart';\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('sidebar move page: ', () {\n    testWidgets('create a new document and move it to Getting started',\n        (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      const pageName = 'Document';\n\n      await tester.createNewPageInSpace(\n        spaceName: Constants.generalSpaceName,\n        layout: ViewLayoutPB.Document,\n        pageName: pageName,\n      );\n\n      // click the ... button and move to Getting started\n      await tester.hoverOnPageName(\n        pageName,\n        onHover: () async {\n          await tester.tapPageOptionButton();\n          await tester.tapButtonWithName(\n            LocaleKeys.disclosureAction_moveTo.tr(),\n          );\n        },\n      );\n\n      // expect to see two pages\n      // one is in the sidebar, the other is in the move to page list\n      // 1. Getting started\n      // 2. To-dos\n      final gettingStarted = find.findTextInFlowyText(\n        Constants.gettingStartedPageName,\n      );\n      final toDos = find.findTextInFlowyText(Constants.toDosPageName);\n      await tester.pumpUntilFound(gettingStarted);\n      await tester.pumpUntilFound(toDos);\n      expect(gettingStarted, findsNWidgets(2));\n\n      // skip the length check on Linux temporarily,\n      //  because it failed in expect check but the previous pumpUntilFound is successful\n      if (!UniversalPlatform.isLinux) {\n        expect(toDos, findsNWidgets(2));\n\n        // hover on the todos page, and will see a forbidden icon\n        await tester.hoverOnWidget(\n          toDos.last,\n          onHover: () async {\n            final tooltips = find.byTooltip(\n              LocaleKeys.space_cannotMovePageToDatabase.tr(),\n            );\n            expect(tooltips, findsOneWidget);\n          },\n        );\n        await tester.pumpAndSettle();\n      }\n\n      // Attempt right-click on the page name and expect not to see\n      await tester.tap(gettingStarted.last, buttons: kSecondaryButton);\n      await tester.pumpAndSettle();\n      expect(\n        find.text(LocaleKeys.disclosureAction_moveTo.tr()),\n        findsOneWidget,\n      );\n\n      // move the current page to Getting started\n      await tester.tapButton(\n        gettingStarted.last,\n      );\n\n      await tester.pumpAndSettle();\n\n      // after moving, expect to not see the page name in the sidebar\n      final page = tester.findPageName(pageName);\n      expect(page, findsNothing);\n\n      // click to expand the getting started page\n      await tester.expandOrCollapsePage(\n        pageName: Constants.gettingStartedPageName,\n        layout: ViewLayoutPB.Document,\n      );\n      await tester.pumpAndSettle();\n\n      // expect to see the page name in the getting started page\n      final pageInGettingStarted = tester.findPageName(\n        pageName,\n        parentName: Constants.gettingStartedPageName,\n      );\n      expect(pageInGettingStarted, findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/sidebar/sidebar_rename_untitled_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/constants.dart';\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  testWidgets('Rename empty name view (untitled)', (tester) async {\n    await tester.initializeAppFlowy(\n      cloudType: AuthenticatorType.appflowyCloudSelfHost,\n    );\n    await tester.tapGoogleLoginInButton();\n    await tester.expectToSeeHomePageWithGetStartedPage();\n\n    await tester.createNewPageInSpace(\n      spaceName: Constants.generalSpaceName,\n      layout: ViewLayoutPB.Document,\n    );\n\n    // click the ... button and open rename dialog\n    await tester.hoverOnPageName(\n      ViewLayoutPB.Document.defaultName,\n      onHover: () async {\n        await tester.tapPageOptionButton();\n        await tester.tapButtonWithName(\n          LocaleKeys.disclosureAction_rename.tr(),\n        );\n      },\n    );\n    await tester.pumpAndSettle();\n\n    expect(find.byType(AFTextFieldDialog), findsOneWidget);\n\n    final textField = tester.widget<AFTextField>(\n      find.descendant(\n        of: find.byType(AFTextFieldDialog),\n        matching: find.byType(AFTextField),\n      ),\n    );\n\n    expect(\n      textField.controller!.text,\n      LocaleKeys.menuAppHeader_defaultNewPageName.tr(),\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/sidebar/sidebar_search_test.dart",
    "content": "import 'package:appflowy/ai/widgets/prompt_input/desktop_prompt_input.dart';\nimport 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/search_ask_ai_entrance.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/util.dart';\n\nvoid main() {\n  setUpAll(() {\n    IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n  });\n\n  testWidgets('Test for searching', (tester) async {\n    await tester.initializeAppFlowy(\n      cloudType: AuthenticatorType.appflowyCloudSelfHost,\n    );\n    await tester.tapGoogleLoginInButton();\n    await tester.expectToSeeHomePageWithGetStartedPage();\n\n    /// show searching page\n    final searchingButton = find.text(LocaleKeys.search_label.tr());\n    await tester.tapButton(searchingButton);\n    final askAIButton = find.byType(SearchAskAiEntrance);\n    expect(askAIButton, findsOneWidget);\n\n    /// searching for [gettingStarted]\n    final searchField = find.byType(SearchField);\n    final textFiled =\n        find.descendant(of: searchField, matching: find.byType(TextField));\n    await tester.enterText(textFiled, gettingStarted);\n    await tester.pumpAndSettle(Duration(seconds: 1));\n\n    /// tap ask AI button\n    await tester.tapButton(askAIButton);\n    expect(find.byType(DesktopPromptInput), findsOneWidget);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/appflowy_cloud_auth_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/settings/prelude.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/account.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('appflowy cloud auth', () {\n    testWidgets('sign in', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n    });\n\n    testWidgets('sign out', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n\n      // Open the setting page and sign out\n      await tester.openSettings();\n      await tester.openSettingsPage(SettingsPage.account);\n\n      // Scroll to sign-out\n      await tester.scrollUntilVisible(\n        find.byType(AccountSignInOutButton),\n        100,\n        scrollable: find.findSettingsScrollable(),\n      );\n      await tester.tapButton(find.byType(AccountSignInOutButton));\n\n      tester.expectToSeeText(LocaleKeys.button_yes.tr());\n      await tester.tapButtonWithName(LocaleKeys.button_yes.tr());\n\n      // Go to the sign in page again\n      await tester.pumpAndSettle(const Duration(seconds: 5));\n      tester.expectToSeeGoogleLoginButton();\n    });\n\n    testWidgets('sign in as anonymous', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapSignInAsGuest();\n\n      // should not see the sync setting page when sign in as anonymous\n      await tester.openSettings();\n      await tester.openSettingsPage(SettingsPage.account);\n\n      await tester.tapButton(find.byType(AccountSignInOutButton));\n\n      tester.expectToSeeGoogleLoginButton();\n    });\n\n    testWidgets('enable sync', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n\n      await tester.tapGoogleLoginInButton();\n      // Open the setting page and sign out\n      await tester.openSettings();\n      await tester.openSettingsPage(SettingsPage.cloud);\n      await tester.pumpAndSettle();\n\n      // the switch should be on by default\n      tester.assertAppFlowyCloudEnableSyncSwitchValue(true);\n      await tester.toggleEnableSync(AppFlowyCloudEnableSync);\n      // wait for the switch animation\n      await tester.wait(250);\n\n      // the switch should be off\n      tester.assertAppFlowyCloudEnableSyncSwitchValue(false);\n\n      // the switch should be on after toggling\n      await tester.toggleEnableSync(AppFlowyCloudEnableSync);\n      tester.assertAppFlowyCloudEnableSyncSwitchValue(true);\n      await tester.wait(250);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/document_sync_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/workspace/application/settings/prelude.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n  final email = '${uuid()}@appflowy.io';\n  const inputContent = 'Hello world, this is a test document';\n\n// The test will create a new document called Sample, and sync it to the server.\n// Then the test will logout the user, and login with the same user. The data will\n// be synced from the server.\n  group('appflowy cloud document', () {\n    testWidgets('sync local docuemnt to server', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n        email: email,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      // create a new document called Sample\n      await tester.createNewPage();\n\n      // focus on the editor\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.ime.insertText(inputContent);\n      expect(find.text(inputContent, findRichText: true), findsOneWidget);\n\n      // 6 seconds for data sync\n      await tester.waitForSeconds(6);\n\n      await tester.openSettings();\n      await tester.openSettingsPage(SettingsPage.account);\n      await tester.logout();\n    });\n\n    testWidgets('sync doc from server', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n        email: email,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePage();\n\n      // the latest document will be opened, so the content must be the inputContent\n      await tester.pumpAndSettle();\n      expect(find.text(inputContent, findRichText: true), findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/uncategorized_test_runner.dart",
    "content": "import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;\nimport 'user_setting_sync_test.dart' as user_sync_test;\n\nvoid main() async {\n  appflowy_cloud_auth_test.main();\n  user_sync_test.main();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/user_setting_sync_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/workspace/application/settings/prelude.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/account_user_profile.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n  final email = '${uuid()}@appflowy.io';\n  const name = 'nathan';\n\n  group('appflowy cloud setting', () {\n    testWidgets('sync user name and icon to server', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n        email: email,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      await tester.openSettings();\n      await tester.openSettingsPage(SettingsPage.account);\n\n      await tester.enterUserName(name);\n      await tester.pumpAndSettle(const Duration(seconds: 6));\n      await tester.logout();\n\n      await tester.pumpAndSettle(const Duration(seconds: 2));\n    });\n  });\n  testWidgets('get user icon and name from server', (tester) async {\n    await tester.initializeAppFlowy(\n      cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      email: email,\n    );\n    await tester.tapGoogleLoginInButton();\n    await tester.expectToSeeHomePageWithGetStartedPage();\n    await tester.pumpAndSettle();\n\n    await tester.openSettings();\n    await tester.openSettingsPage(SettingsPage.account);\n\n    // Verify name\n    final profileSetting =\n        tester.widget(find.byType(AccountUserProfile)) as AccountUserProfile;\n\n    expect(profileSetting.name, name);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/change_name_and_icon_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/util.dart';\nimport '../../../shared/workspace.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  const icon = '😄';\n  const name = 'AppFlowy';\n  final email = '${uuid()}@appflowy.io';\n\n  testWidgets('change name and icon', (tester) async {\n    // only run the test when the feature flag is on\n    if (!FeatureFlag.collaborativeWorkspace.isOn) {\n      return;\n    }\n\n    await tester.initializeAppFlowy(\n      cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      email: email, // use the same email to check the next test\n    );\n\n    await tester.tapGoogleLoginInButton();\n    await tester.expectToSeeHomePageWithGetStartedPage();\n\n    var workspaceIcon = tester.widget<WorkspaceIcon>(\n      find.byType(WorkspaceIcon),\n    );\n    expect(workspaceIcon.workspaceIcon, '');\n\n    await tester.openWorkspaceMenu();\n    await tester.changeWorkspaceIcon(icon);\n    await tester.changeWorkspaceName(name);\n\n    await tester.pumpUntilNotFound(\n      find.text(LocaleKeys.workspace_renameSuccess.tr()),\n    );\n\n    workspaceIcon = tester.widget<WorkspaceIcon>(\n      find.byType(WorkspaceIcon),\n    );\n    expect(workspaceIcon.workspaceIcon, icon);\n    expect(workspaceIcon.workspaceName, name);\n  });\n\n  testWidgets('verify the result again after relaunching', (tester) async {\n    // only run the test when the feature flag is on\n    if (!FeatureFlag.collaborativeWorkspace.isOn) {\n      return;\n    }\n\n    await tester.initializeAppFlowy(\n      cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      email: email, // use the same email to check the next test\n    );\n\n    await tester.tapGoogleLoginInButton();\n    await tester.expectToSeeHomePageWithGetStartedPage();\n\n    // check the result again\n    final workspaceIcon = tester.widget<WorkspaceIcon>(\n      find.byType(WorkspaceIcon),\n    );\n    expect(workspaceIcon.workspaceIcon, icon);\n    expect(workspaceIcon.workspaceName, name);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/collaborative_workspace_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/shared/loading.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('collaborative workspace:', () {\n    // combine the create and delete workspace test to reduce the time\n    testWidgets('create a new workspace, open it and then delete it',\n        (tester) async {\n      // only run the test when the feature flag is on\n      if (!FeatureFlag.collaborativeWorkspace.isOn) {\n        return;\n      }\n\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      const name = 'AppFlowy.IO';\n      // the workspace will be opened after created\n      await tester.createCollaborativeWorkspace(name);\n\n      final loading = find.byType(Loading);\n      await tester.pumpUntilNotFound(loading);\n\n      // delete the newly created workspace\n      await tester.openCollaborativeWorkspaceMenu();\n\n      final items = find.byType(WorkspaceMenuItem);\n      expect(items, findsNWidgets(2));\n\n      final lastWorkspace = items.last;\n      expect(\n        tester.widget<WorkspaceMenuItem>(lastWorkspace).workspace.name,\n        name,\n      );\n\n      await tester.hoverOnWidget(\n        lastWorkspace,\n        onHover: () async {\n          // click the more button\n          final moreButton = find.byType(WorkspaceMoreActionList);\n          expect(moreButton, findsOneWidget);\n          await tester.tapButton(moreButton);\n          // click the delete button\n          final deleteButton = find.text(LocaleKeys.button_delete.tr());\n          expect(deleteButton, findsOneWidget);\n          await tester.tapButton(deleteButton);\n          // see the delete confirm dialog\n          final confirm =\n              find.text(LocaleKeys.workspace_deleteWorkspaceHintText.tr());\n          expect(confirm, findsOneWidget);\n          await tester.tapButton(find.text(LocaleKeys.button_ok.tr()));\n          // delete success\n          final success = find.text(LocaleKeys.workspace_createSuccess.tr());\n          await tester.pumpUntilFound(success);\n        },\n      );\n    });\n\n    testWidgets('check the member count immediately after creating a workspace',\n        (tester) async {\n      // only run the test when the feature flag is on\n      if (!FeatureFlag.collaborativeWorkspace.isOn) {\n        return;\n      }\n\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      const name = 'AppFlowy.IO';\n      // the workspace will be opened after created\n      await tester.createCollaborativeWorkspace(name);\n\n      final loading = find.byType(Loading);\n      await tester.pumpUntilNotFound(loading);\n\n      await tester.openCollaborativeWorkspaceMenu();\n\n      // expect to see the member count\n      final memberCount = find.text('1 member');\n      expect(memberCount, findsAny);\n    });\n\n    testWidgets('workspace menu popover behavior test', (tester) async {\n      // only run the test when the feature flag is on\n      if (!FeatureFlag.collaborativeWorkspace.isOn) {\n        return;\n      }\n\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      const name = 'AppFlowy.IO';\n      // the workspace will be opened after created\n      await tester.createCollaborativeWorkspace(name);\n\n      final loading = find.byType(Loading);\n      await tester.pumpUntilNotFound(loading);\n\n      await tester.openCollaborativeWorkspaceMenu();\n\n      // hover on the workspace and click the more button\n      final workspaceItem = find.byWidgetPredicate(\n        (w) => w is WorkspaceMenuItem && w.workspace.name == name,\n      );\n\n      // the workspace menu shouldn't conflict with logout\n      await tester.hoverOnWidget(\n        workspaceItem,\n        onHover: () async {\n          final moreButton = find.byWidgetPredicate(\n            (w) => w is WorkspaceMoreActionList && w.workspace.name == name,\n          );\n          expect(moreButton, findsOneWidget);\n          await tester.tapButton(moreButton);\n          expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);\n\n          final logoutButton = find.byType(WorkspaceMoreButton);\n          await tester.tapButton(logoutButton);\n          expect(find.text(LocaleKeys.button_logout.tr()), findsOneWidget);\n          expect(moreButton, findsNothing);\n\n          await tester.tapButton(moreButton);\n          expect(find.text(LocaleKeys.button_logout.tr()), findsNothing);\n          expect(moreButton, findsOneWidget);\n        },\n      );\n      await tester.sendKeyEvent(LogicalKeyboardKey.escape);\n      await tester.pumpAndSettle();\n\n      // clicking on the more action button for the same workspace shouldn't do\n      // anything\n      await tester.openCollaborativeWorkspaceMenu();\n      await tester.hoverOnWidget(\n        workspaceItem,\n        onHover: () async {\n          final moreButton = find.byWidgetPredicate(\n            (w) => w is WorkspaceMoreActionList && w.workspace.name == name,\n          );\n          expect(moreButton, findsOneWidget);\n          await tester.tapButton(moreButton);\n          expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);\n\n          // click it again\n          await tester.tapButton(moreButton);\n\n          // nothing should happen\n          expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);\n        },\n      );\n      await tester.sendKeyEvent(LogicalKeyboardKey.escape);\n      await tester.pumpAndSettle();\n\n      // clicking on the more button of another workspace should close the menu\n      // for this one\n      await tester.openCollaborativeWorkspaceMenu();\n      final moreButton = find.byWidgetPredicate(\n        (w) => w is WorkspaceMoreActionList && w.workspace.name == name,\n      );\n      await tester.hoverOnWidget(\n        workspaceItem,\n        onHover: () async {\n          expect(moreButton, findsOneWidget);\n          await tester.tapButton(moreButton);\n          expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);\n        },\n      );\n\n      final otherWorspaceItem = find.byWidgetPredicate(\n        (w) => w is WorkspaceMenuItem && w.workspace.name != name,\n      );\n      final otherMoreButton = find.byWidgetPredicate(\n        (w) => w is WorkspaceMoreActionList && w.workspace.name != name,\n      );\n      await tester.hoverOnWidget(\n        otherWorspaceItem,\n        onHover: () async {\n          expect(otherMoreButton, findsOneWidget);\n          await tester.tapButton(otherMoreButton);\n          expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);\n\n          expect(moreButton, findsNothing);\n        },\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/share_menu_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/shared/share/constants.dart';\nimport 'package:appflowy/plugins/shared/share/share_menu.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/constants.dart';\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Share menu:', () {\n    testWidgets('share tab', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      const pageName = 'Document';\n\n      await tester.createNewPageInSpace(\n        spaceName: Constants.generalSpaceName,\n        layout: ViewLayoutPB.Document,\n        pageName: pageName,\n      );\n\n      // click the share button\n      await tester.tapShareButton();\n\n      // expect the share menu is shown\n      final shareMenu = find.byType(ShareMenu);\n      expect(shareMenu, findsOneWidget);\n\n      // click the copy link button\n      final copyLinkButton = find.textContaining(\n        LocaleKeys.button_copyLink.tr(),\n      );\n      await tester.tapButton(copyLinkButton);\n\n      // read the clipboard content\n      final clipboardContent = await getIt<ClipboardService>().getData();\n      final plainText = clipboardContent.plainText;\n      expect(\n        plainText,\n        matches(appflowySharePageLinkPattern),\n      );\n\n      final shareValues = plainText!\n          .replaceAll('${ShareConstants.defaultBaseWebDomain}/app/', '')\n          .split('/');\n      final workspaceId = shareValues[0];\n      expect(workspaceId, isNotEmpty);\n      final pageId = shareValues[1];\n      expect(pageId, isNotEmpty);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/tabs_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_page.dart';\nimport 'package:appflowy/shared/loading.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';\nimport 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';\nimport 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/constants.dart';\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Tabs', () {\n    testWidgets('close other tabs before opening a new workspace',\n        (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      const name = 'AppFlowy.IO';\n      // the workspace will be opened after created\n      await tester.createCollaborativeWorkspace(name);\n\n      final loading = find.byType(Loading);\n      await tester.pumpUntilNotFound(loading);\n\n      // create new tabs in the workspace\n      expect(find.byType(FlowyTab), findsNothing);\n\n      const documentOneName = 'document one';\n      const documentTwoName = 'document two';\n      await tester.createNewPageInSpace(\n        spaceName: Constants.generalSpaceName,\n        layout: ViewLayoutPB.Document,\n        pageName: documentOneName,\n      );\n      await tester.createNewPageInSpace(\n        spaceName: Constants.generalSpaceName,\n        layout: ViewLayoutPB.Document,\n        pageName: documentTwoName,\n      );\n\n      /// Open second menu item in a new tab\n      await tester.openAppInNewTab(documentOneName, ViewLayoutPB.Document);\n\n      /// Open third menu item in a new tab\n      await tester.openAppInNewTab(documentTwoName, ViewLayoutPB.Document);\n\n      expect(\n        find.descendant(\n          of: find.byType(TabsManager),\n          matching: find.byType(FlowyTab),\n        ),\n        findsNWidgets(2),\n      );\n\n      // switch to the another workspace\n      final Finder items = find.byType(WorkspaceMenuItem);\n      await tester.openCollaborativeWorkspaceMenu();\n      await tester.pumpUntilFound(items);\n      expect(items, findsNWidgets(2));\n\n      // open the first workspace\n      await tester.tap(items.first);\n      await tester.pumpUntilNotFound(loading);\n\n      expect(find.byType(FlowyTab), findsNothing);\n    });\n\n    testWidgets('the space view should not be opened', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      expect(find.byType(AppFlowyEditorPage), findsOneWidget);\n      expect(find.text('Blank page'), findsNothing);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/workspace_icon_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/util.dart';\nimport '../../../shared/workspace.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('workspace icon:', () {\n    testWidgets('remove icon from workspace', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      await tester.openWorkspaceMenu();\n\n      // click the workspace icon\n      await tester.tapButton(\n        find.descendant(\n          of: find.byType(WorkspaceMenuItem),\n          matching: find.byType(WorkspaceIcon),\n        ),\n      );\n      // click the remove icon button\n      await tester.tapButton(\n        find.text(LocaleKeys.button_remove.tr()),\n      );\n\n      // nothing should happen\n      expect(\n        find.text(LocaleKeys.workspace_updateIconSuccess.tr()),\n        findsNothing,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/workspace_settings_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/plugins/shared/share/publish_tab.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/settings/prelude.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_more_action.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_item.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_more_action.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/constants.dart';\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('workspace settings: ', () {\n    testWidgets(\n      'change document width',\n      (tester) async {\n        await tester.initializeAppFlowy(\n          cloudType: AuthenticatorType.appflowyCloudSelfHost,\n        );\n        await tester.tapGoogleLoginInButton();\n        await tester.expectToSeeHomePageWithGetStartedPage();\n\n        await tester.openSettings();\n        await tester.openSettingsPage(SettingsPage.workspace);\n\n        final documentWidthSettings = find.findTextInFlowyText(\n          LocaleKeys.settings_appearance_documentSettings_width.tr(),\n        );\n\n        final scrollable = find.ancestor(\n          of: find.byType(SettingsWorkspaceView),\n          matching: find.descendant(\n            of: find.byType(SingleChildScrollView),\n            matching: find.byType(Scrollable),\n          ),\n        );\n\n        await tester.scrollUntilVisible(\n          documentWidthSettings,\n          0,\n          scrollable: scrollable,\n        );\n        await tester.pumpAndSettle();\n\n        // change the document width\n        final slider = find.byType(Slider);\n        final oldValue = tester.widget<Slider>(slider).value;\n        await tester.drag(slider, const Offset(-100, 0));\n        await tester.pumpAndSettle();\n\n        // check the document width is changed\n        expect(tester.widget<Slider>(slider).value, lessThan(oldValue));\n\n        // click the reset button\n        final resetButton = find.descendant(\n          of: find.byType(DocumentPaddingSetting),\n          matching: find.byType(SettingsResetButton),\n        );\n        await tester.tap(resetButton);\n        await tester.pumpAndSettle();\n\n        // check the document width is reset\n        expect(\n          tester.widget<Slider>(slider).value,\n          EditorStyleCustomizer.maxDocumentWidth,\n        );\n      },\n    );\n  });\n\n  group('sites settings:', () {\n    testWidgets(\n        'manage published page, set it as homepage, remove the homepage',\n        (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      const pageName = 'Document';\n\n      await tester.createNewPageInSpace(\n        spaceName: Constants.generalSpaceName,\n        layout: ViewLayoutPB.Document,\n        pageName: pageName,\n      );\n\n      // open the publish menu\n      await tester.openPublishMenu();\n\n      // publish the document\n      await tester.tapButton(find.byType(PublishButton));\n\n      // click empty area to close the publish menu\n      await tester.tapAt(Offset.zero);\n      await tester.pumpAndSettle();\n      // check if the page is published in sites page\n      await tester.openSettings();\n      await tester.openSettingsPage(SettingsPage.sites);\n      // wait the backend return the sites data\n      await tester.wait(1000);\n\n      // check if the page is published in sites page\n      final pageItem = find.byWidgetPredicate(\n        (widget) =>\n            widget is PublishedViewItem &&\n            widget.publishInfoView.view.name == pageName,\n      );\n      if (pageItem.evaluate().isEmpty) {\n        return;\n      }\n\n      expect(pageItem, findsOneWidget);\n\n      // comment it out because it's not allowed to update the namespace in free plan\n      // // set it to homepage\n      // await tester.tapButton(\n      //   find.textContaining(\n      //     LocaleKeys.settings_sites_selectHomePage.tr(),\n      //   ),\n      // );\n      // await tester.tapButton(\n      //   find.descendant(\n      //     of: find.byType(SelectHomePageMenu),\n      //     matching: find.text(pageName),\n      //   ),\n      // );\n      // await tester.pumpAndSettle();\n\n      // // check if the page is set to homepage\n      // final homePageItem = find.descendant(\n      //   of: find.byType(DomainItem),\n      //   matching: find.text(pageName),\n      // );\n      // expect(homePageItem, findsOneWidget);\n\n      // // remove the homepage\n      // await tester.tapButton(find.byType(DomainMoreAction));\n      // await tester.tapButton(\n      //   find.text(LocaleKeys.settings_sites_removeHomepage.tr()),\n      // );\n      // await tester.pumpAndSettle();\n\n      // // check if the page is removed from homepage\n      // expect(homePageItem, findsNothing);\n    });\n\n    testWidgets('update namespace', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      // check if the page is published in sites page\n      await tester.openSettings();\n      await tester.openSettingsPage(SettingsPage.sites);\n      // wait the backend return the sites data\n      await tester.wait(1000);\n\n      // update the domain\n      final domainMoreAction = find.byType(DomainMoreAction);\n      await tester.tapButton(domainMoreAction);\n      final updateNamespaceButton = find.text(\n        LocaleKeys.settings_sites_updateNamespace.tr(),\n      );\n      await tester.pumpUntilFound(updateNamespaceButton);\n\n      // click the update namespace button\n\n      await tester.tapButton(updateNamespaceButton);\n\n      // comment it out because it's not allowed to update the namespace in free plan\n      // expect to see the dialog\n      // await tester.updateNamespace('&&&???');\n\n      // // need to upgrade to pro plan to update the namespace\n      // final errorToast = find.text(\n      //   LocaleKeys.settings_sites_error_proPlanLimitation.tr(),\n      // );\n      // await tester.pumpUntilFound(errorToast);\n      // expect(errorToast, findsOneWidget);\n      // await tester.pumpUntilNotFound(errorToast);\n\n      // comment it out because it's not allowed to update the namespace in free plan\n      // // short namespace\n      // await tester.updateNamespace('a');\n\n      // // expect to see the toast with error message\n      // final errorToast2 = find.text(\n      //   LocaleKeys.settings_sites_error_namespaceTooShort.tr(),\n      // );\n      // await tester.pumpUntilFound(errorToast2);\n      // expect(errorToast2, findsOneWidget);\n      // await tester.pumpUntilNotFound(errorToast2);\n      // // valid namespace\n      // await tester.updateNamespace('AppFlowy');\n\n      // // expect to see the toast with success message\n      // final successToast = find.text(\n      //   LocaleKeys.settings_sites_success_namespaceUpdated.tr(),\n      // );\n      // await tester.pumpUntilFound(successToast);\n      // expect(successToast, findsOneWidget);\n    });\n\n    testWidgets('''\nMore actions for published page:\n1. visit site\n2. copy link\n3. settings\n4. unpublish\n5. custom url\n''', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      const pageName = 'Document';\n\n      await tester.createNewPageInSpace(\n        spaceName: Constants.generalSpaceName,\n        layout: ViewLayoutPB.Document,\n        pageName: pageName,\n      );\n\n      // open the publish menu\n      await tester.openPublishMenu();\n\n      // publish the document\n      await tester.tapButton(find.byType(PublishButton));\n\n      // click empty area to close the publish menu\n      await tester.tapAt(Offset.zero);\n      await tester.pumpAndSettle();\n      // check if the page is published in sites page\n      await tester.openSettings();\n      await tester.openSettingsPage(SettingsPage.sites);\n      // wait the backend return the sites data\n      await tester.wait(2000);\n\n      // check if the page is published in sites page\n      final pageItem = find.byWidgetPredicate(\n        (widget) =>\n            widget is PublishedViewItem &&\n            widget.publishInfoView.view.name == pageName,\n      );\n      if (pageItem.evaluate().isEmpty) {\n        return;\n      }\n\n      expect(pageItem, findsOneWidget);\n\n      final copyLinkItem = find.text(LocaleKeys.shareAction_copyLink.tr());\n      final customUrlItem = find.text(LocaleKeys.settings_sites_customUrl.tr());\n      final unpublishItem = find.text(LocaleKeys.shareAction_unPublish.tr());\n\n      // custom url\n      final publishMoreAction = find.byType(PublishedViewMoreAction);\n\n      // click the copy link button\n      {\n        await tester.tapButton(publishMoreAction);\n        await tester.pumpAndSettle();\n        await tester.pumpUntilFound(copyLinkItem);\n        await tester.tapButton(copyLinkItem);\n        await tester.pumpAndSettle();\n        await tester.pumpUntilNotFound(copyLinkItem);\n\n        final clipboardContent = await getIt<ClipboardService>().getData();\n        final plainText = clipboardContent.plainText;\n        expect(\n          plainText,\n          contains(pageName),\n        );\n      }\n\n      // custom url\n      {\n        await tester.tapButton(publishMoreAction);\n        await tester.pumpAndSettle();\n        await tester.pumpUntilFound(customUrlItem);\n        await tester.tapButton(customUrlItem);\n        await tester.pumpAndSettle();\n        await tester.pumpUntilNotFound(customUrlItem);\n\n        // see the custom url dialog\n        final customUrlDialog = find.byType(PublishedViewSettingsDialog);\n        expect(customUrlDialog, findsOneWidget);\n\n        // rename the custom url\n        final textField = find.descendant(\n          of: customUrlDialog,\n          matching: find.byType(TextField),\n        );\n        await tester.enterText(textField, 'hello-world');\n        await tester.pumpAndSettle();\n\n        // click the save button\n        final saveButton = find.descendant(\n          of: customUrlDialog,\n          matching: find.text(LocaleKeys.button_save.tr()),\n        );\n        await tester.tapButton(saveButton);\n        await tester.pumpAndSettle();\n\n        // expect to see the toast with success message\n        final successToast = find.text(\n          LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),\n        );\n        await tester.pumpUntilFound(successToast);\n        expect(successToast, findsOneWidget);\n      }\n\n      // unpublish\n      {\n        await tester.tapButton(publishMoreAction);\n        await tester.pumpAndSettle();\n        await tester.pumpUntilFound(unpublishItem);\n        await tester.tapButton(unpublishItem);\n        await tester.pumpAndSettle();\n        await tester.pumpUntilNotFound(unpublishItem);\n\n        // expect to see the toast with success message\n        final successToast = find.text(\n          LocaleKeys.publish_unpublishSuccessfully.tr(),\n        );\n        await tester.pumpUntilFound(successToast);\n        expect(successToast, findsOneWidget);\n        await tester.pumpUntilNotFound(successToast);\n\n        // check if the page is unpublished in sites page\n        expect(pageItem, findsNothing);\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/workspace_test_runner.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'change_name_and_icon_test.dart' as change_name_and_icon_test;\nimport 'collaborative_workspace_test.dart' as collaborative_workspace_test;\nimport 'share_menu_test.dart' as share_menu_test;\nimport 'tabs_test.dart' as tabs_test;\nimport 'workspace_icon_test.dart' as workspace_icon_test;\n// import 'workspace_settings_test.dart' as workspace_settings_test;\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  // Enable it after implementing the share feature\n  // workspace_settings_test.main();\n  share_menu_test.main();\n  collaborative_workspace_test.main();\n  change_name_and_icon_test.main();\n  workspace_icon_test.main();\n  tabs_test.main();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/command_palette/command_palette_test.dart",
    "content": "import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/search_ask_ai_entrance.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_cell.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/page_preview.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/recent_views_list.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/search_results_list.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Command Palette', () {\n    testWidgets('Toggle command palette', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.toggleCommandPalette();\n      expect(find.byType(CommandPaletteModal), findsOneWidget);\n\n      await tester.toggleCommandPalette();\n      expect(find.byType(CommandPaletteModal), findsNothing);\n    });\n  });\n\n  group('Search', () {\n    testWidgets('Test for searching', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(name: 'Switch To New Page');\n      await tester.pumpAndSettle();\n\n      /// tap getting started page\n      await tester.tapButton(\n        find.descendant(\n          of: find.byType(HomeSideBar),\n          matching: find.textContaining(gettingStarted),\n        ),\n      );\n\n      /// show searching page\n      final searchingButton = find.text(LocaleKeys.search_label.tr());\n      await tester.tapButton(searchingButton);\n      final askAIButton = find.byType(SearchAskAiEntrance);\n      expect(askAIButton, findsNothing);\n      final recentList = find.byType(RecentViewsList);\n      expect(recentList, findsOneWidget);\n\n      /// there is [gettingStarted] in recent list\n      final gettingStartedRecentCell = find.descendant(\n        of: recentList,\n        matching: find.textContaining(gettingStarted),\n      );\n      expect(gettingStartedRecentCell, findsAtLeast(1));\n\n      /// hover to show preview\n      await tester.hoverOnWidget(gettingStartedRecentCell.first);\n      final pagePreview = find.byType(PagePreview);\n      expect(pagePreview, findsOneWidget);\n      final gettingStartedPreviewTitle = find.descendant(\n        of: pagePreview,\n        matching: find.textContaining(gettingStarted),\n      );\n      expect(gettingStartedPreviewTitle, findsOneWidget);\n\n      /// searching for [gettingStarted]\n      final searchField = find.byType(SearchField);\n      final textFiled =\n          find.descendant(of: searchField, matching: find.byType(TextField));\n      await tester.enterText(textFiled, gettingStarted);\n      await tester.pumpAndSettle(Duration(seconds: 1));\n\n      /// there is [gettingStarted] in result list\n      final resultList = find.byType(SearchResultList);\n      expect(resultList, findsOneWidget);\n      final resultCells = find.byType(SearchResultCell);\n      expect(resultCells, findsAtLeast(1));\n\n      /// hover to show preview\n      await tester.hoverOnWidget(resultCells.first);\n      expect(find.byType(PagePreview), findsOneWidget);\n\n      /// clear search content\n      final clearButton = find.byFlowySvg(FlowySvgs.search_clear_m);\n      await tester.tapButton(clearButton);\n      expect(find.byType(SearchResultList), findsNothing);\n      expect(find.byType(RecentViewsList), findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/command_palette/command_palette_test_runner.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'command_palette_test.dart' as command_palette_test;\nimport 'folder_search_test.dart' as folder_search_test;\nimport 'recent_history_test.dart' as recent_history_test;\n\nvoid startTesting() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  // Command Palette integration tests\n  command_palette_test.main();\n  folder_search_test.main();\n  recent_history_test.main();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart",
    "content": "import 'dart:convert';\nimport 'dart:math';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_cell.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  setUpAll(() {\n    IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n  });\n\n  group('Folder Search', () {\n    testWidgets('Search for views', (tester) async {\n      const firstDocument = \"ViewOne\";\n      const secondDocument = \"ViewOna\";\n\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(name: firstDocument);\n      await tester.createNewPageWithNameUnderParent(name: secondDocument);\n\n      await tester.toggleCommandPalette();\n      expect(find.byType(CommandPaletteModal), findsOneWidget);\n\n      final searchFieldFinder = find.descendant(\n        of: find.byType(SearchField),\n        matching: find.byType(FlowyTextField),\n      );\n\n      await tester.enterText(searchFieldFinder, secondDocument);\n      await tester.pumpAndSettle(const Duration(milliseconds: 200));\n\n      // Expect two search results \"ViewOna\" and \"ViewOne\" (Distance 1 to ViewOna)\n      expect(find.byType(SearchResultCell), findsNWidgets(2));\n\n      // The score should be higher for \"ViewOna\" thus it should be shown first\n      final secondDocumentWidget = tester\n          .widget(find.byType(SearchResultCell).first) as SearchResultCell;\n      expect(secondDocumentWidget.item.displayName, secondDocument);\n\n      // Change search to \"ViewOne\"\n      await tester.enterText(searchFieldFinder, firstDocument);\n      await tester.pumpAndSettle(const Duration(seconds: 1));\n\n      // The score should be higher for \"ViewOne\" thus it should be shown first\n      final firstDocumentWidget = tester.widget(\n        find.byType(SearchResultCell).first,\n      ) as SearchResultCell;\n      expect(firstDocumentWidget.item.displayName, firstDocument);\n    });\n\n    testWidgets('Displaying icons in search results', (tester) async {\n      final randomValue = Random().nextInt(10000) + 10000;\n      final pageNames = ['First Page-$randomValue', 'Second Page-$randomValue'];\n\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      final emojiIconData = await tester.loadIcon();\n\n      /// create two pages\n      for (final pageName in pageNames) {\n        await tester.createNewPageWithNameUnderParent(name: pageName);\n        await tester.updatePageIconInTitleBarByName(\n          name: pageName,\n          layout: ViewLayoutPB.Document,\n          icon: emojiIconData,\n        );\n      }\n\n      await tester.toggleCommandPalette();\n\n      /// search for `Page`\n      final searchFieldFinder = find.descendant(\n        of: find.byType(SearchField),\n        matching: find.byType(FlowyTextField),\n      );\n      await tester.enterText(searchFieldFinder, 'Page-$randomValue');\n      await tester.pumpAndSettle(const Duration(milliseconds: 200));\n      expect(find.byType(SearchResultCell), findsNWidgets(2));\n\n      /// check results\n      final svgs = find.descendant(\n        of: find.byType(SearchResultCell),\n        matching: find.byType(FlowySvg),\n      );\n      expect(svgs, findsNWidgets(2));\n\n      final firstSvg = svgs.first.evaluate().first.widget as FlowySvg,\n          lastSvg = svgs.last.evaluate().first.widget as FlowySvg;\n      final iconData = IconsData.fromJson(jsonDecode(emojiIconData.emoji));\n\n      /// icon displayed correctly\n      expect(firstSvg.svgString, iconData.svgString);\n      expect(lastSvg.svgString, iconData.svgString);\n\n      testWidgets('select the content in document and search', (tester) async {\n        const firstDocument = ''; // empty document\n\n        await tester.initializeAppFlowy();\n        await tester.tapAnonymousSignInButton();\n\n        await tester.createNewPageWithNameUnderParent(name: firstDocument);\n        await tester.editor.updateSelection(\n          Selection(\n            start: Position(\n              path: [0],\n            ),\n            end: Position(\n              path: [0],\n              offset: 10,\n            ),\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        expect(\n          find.byType(FloatingToolbar),\n          findsOneWidget,\n        );\n\n        await tester.toggleCommandPalette();\n        expect(find.byType(CommandPaletteModal), findsOneWidget);\n\n        expect(\n          find.text(LocaleKeys.menuAppHeader_defaultNewPageName.tr()),\n          findsOneWidget,\n        );\n\n        expect(\n          find.text(firstDocument),\n          findsOneWidget,\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/command_palette/recent_history_test.dart",
    "content": "import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/recent_views_list.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Recent History', () {\n    testWidgets('Search for views', (tester) async {\n      const firstDocument = \"First\";\n      const secondDocument = \"Second\";\n\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(name: firstDocument);\n      await tester.createNewPageWithNameUnderParent(name: secondDocument);\n\n      await tester.toggleCommandPalette();\n      expect(find.byType(CommandPaletteModal), findsOneWidget);\n\n      // Expect history list\n      expect(find.byType(RecentViewsList), findsOneWidget);\n\n      // Expect three recent history items\n      expect(find.byType(SearchRecentViewCell), findsNWidgets(3));\n\n      // Expect the first item to be the last viewed document\n      final firstDocumentWidget =\n          tester.widget(find.byType(SearchRecentViewCell).first)\n              as SearchRecentViewCell;\n      expect(firstDocumentWidget.view.name, secondDocument);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/database/database_calendar_test.dart",
    "content": "import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';\nimport 'package:flowy_infra_ui/style_widget/icon_button.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  setUpAll(() {\n    IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n    RecentIcons.enable = false;\n  });\n\n  tearDownAll(() {\n    RecentIcons.enable = true;\n  });\n\n  group('calendar', () {\n    testWidgets('update calendar layout', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(\n        layout: ViewLayoutPB.Calendar,\n      );\n\n      // open setting\n      await tester.tapDatabaseSettingButton();\n      await tester.tapDatabaseLayoutButton();\n      await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board);\n      await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board);\n\n      await tester.tapDatabaseSettingButton();\n      await tester.tapDatabaseLayoutButton();\n      await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Grid);\n      await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Grid);\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('calendar start from day setting', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // Create calendar view\n      const name = 'calendar';\n      await tester.createNewPageWithNameUnderParent(\n        name: name,\n        layout: ViewLayoutPB.Calendar,\n      );\n\n      // Open setting\n      await tester.tapDatabaseSettingButton();\n      await tester.tapCalendarLayoutSettingButton();\n\n      // select the first day of week is Monday\n      await tester.tapFirstDayOfWeek();\n      await tester.tapFirstDayOfWeekStartFromMonday();\n\n      // Open the other page and open the new calendar page again\n      await tester.openPage(gettingStarted);\n      await tester.pumpAndSettle(const Duration(milliseconds: 300));\n      await tester.openPage(name, layout: ViewLayoutPB.Calendar);\n\n      // Open setting again and check the start from Monday is selected\n      await tester.tapDatabaseSettingButton();\n      await tester.tapCalendarLayoutSettingButton();\n      await tester.tapFirstDayOfWeek();\n      tester.assertFirstDayOfWeekStartFromMonday();\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('creating and editing calendar events', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // Create the calendar view\n      await tester.createNewPageWithNameUnderParent(\n        layout: ViewLayoutPB.Calendar,\n      );\n\n      // Scroll until today's date cell is visible\n      await tester.scrollToToday();\n\n      // Hover over today's calendar cell\n      await tester.hoverOnTodayCalendarCell(\n        // Tap on create new event button\n        onHover: tester.tapAddCalendarEventButton,\n      );\n\n      // Make sure that the event editor popup is shown\n      tester.assertEventEditorOpen();\n\n      tester.assertNumberOfEventsInCalendar(1);\n\n      // Dismiss the event editor popup\n      await tester.dismissEventEditor();\n\n      // Double click on today's calendar cell to create a new event\n      await tester.doubleClickCalendarCell(DateTime.now());\n\n      // Make sure that the event is inserted in the cell\n      tester.assertNumberOfEventsInCalendar(2);\n\n      // Click on the event\n      await tester.openCalendarEvent(index: 0);\n      tester.assertEventEditorOpen();\n\n      // Change the title of the event\n      await tester.editEventTitle('hello world');\n      await tester.dismissEventEditor();\n\n      // Make sure that the event is edited\n      tester.assertNumberOfEventsInCalendar(1, title: 'hello world');\n      tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());\n\n      // Click on the event\n      await tester.openCalendarEvent(index: 0);\n      tester.assertEventEditorOpen();\n\n      // Click on the open icon\n      await tester.openEventToRowDetailPage();\n      tester.assertRowDetailPageOpened();\n\n      // Duplicate the event\n      await tester.tapRowDetailPageRowActionButton();\n      await tester.tapRowDetailPageDuplicateRowButton();\n      await tester.dismissRowDetailPage();\n\n      // Check that there are 2 events\n      tester.assertNumberOfEventsInCalendar(2, title: 'hello world');\n      tester.assertNumberOfEventsOnSpecificDay(3, DateTime.now());\n\n      // Delete an event\n      await tester.openCalendarEvent(index: 1);\n      await tester.deleteEventFromEventEditor();\n\n      // Check that there is 1 event\n      tester.assertNumberOfEventsInCalendar(1, title: 'hello world');\n      tester.assertNumberOfEventsOnSpecificDay(2, DateTime.now());\n\n      // Delete event from row detail page\n      await tester.openCalendarEvent(index: 0);\n      await tester.openEventToRowDetailPage();\n      tester.assertRowDetailPageOpened();\n\n      await tester.tapRowDetailPageRowActionButton();\n      await tester.tapRowDetailPageDeleteRowButton();\n\n      // Check that there is 0 event\n      tester.assertNumberOfEventsInCalendar(0, title: 'hello world');\n      tester.assertNumberOfEventsOnSpecificDay(1, DateTime.now());\n    });\n\n    testWidgets('create and duplicate calendar event', (tester) async {\n      const customTitle = \"EventTitleCustom\";\n\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // Create the calendar view\n      await tester.createNewPageWithNameUnderParent(\n        layout: ViewLayoutPB.Calendar,\n      );\n\n      // Scroll until today's date cell is visible\n      await tester.scrollToToday();\n\n      // Hover over today's calendar cell\n      await tester.hoverOnTodayCalendarCell(\n        // Tap on create new event button\n        onHover: () async => tester.tapAddCalendarEventButton(),\n      );\n\n      // Make sure that the event editor popup is shown\n      tester.assertEventEditorOpen();\n\n      tester.assertNumberOfEventsInCalendar(1);\n\n      // Change the title of the event\n      await tester.editEventTitle(customTitle);\n\n      // Duplicate event\n      final duplicateBtnFinder = find\n          .descendant(\n            of: find.byType(CalendarEventEditor),\n            matching: find.byType(\n              FlowyIconButton,\n            ),\n          )\n          .first;\n      await tester.tap(duplicateBtnFinder);\n      await tester.pumpAndSettle();\n\n      tester.assertNumberOfEventsInCalendar(2, title: customTitle);\n    });\n\n    testWidgets('rescheduling events', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // Create the calendar view\n      await tester.createNewPageWithNameUnderParent(\n        layout: ViewLayoutPB.Calendar,\n      );\n\n      // Create a new event on the first of this month\n      final today = DateTime.now();\n      final firstOfThisMonth = DateTime(today.year, today.month);\n      await tester.doubleClickCalendarCell(firstOfThisMonth);\n      await tester.dismissEventEditor();\n\n      // Drag and drop the event onto the next week, same day\n      await tester.dragDropRescheduleCalendarEvent();\n\n      // Make sure that the event has been rescheduled to the new date\n      final sameDayNextWeek = firstOfThisMonth.add(const Duration(days: 7));\n      tester.assertNumberOfEventsInCalendar(1);\n      tester.assertNumberOfEventsOnSpecificDay(1, sameDayNextWeek);\n\n      // Delete the event\n      await tester.openCalendarEvent(index: 0, date: sameDayNextWeek);\n      await tester.deleteEventFromEventEditor();\n\n      // Create another event on the 5th of this month\n      final fifthOfThisMonth = DateTime(today.year, today.month, 5);\n      await tester.doubleClickCalendarCell(fifthOfThisMonth);\n      await tester.dismissEventEditor();\n\n      // Make sure that the event is on the 4t\n      tester.assertNumberOfEventsOnSpecificDay(1, fifthOfThisMonth);\n\n      // Click on the event\n      await tester.openCalendarEvent(index: 0, date: fifthOfThisMonth);\n\n      // Open the date editor of the event\n      await tester.tapDateCellInRowDetailPage();\n      await tester.findDateEditor(findsOneWidget);\n\n      // Edit the event's date\n      final newDate = fifthOfThisMonth.add(const Duration(days: 1));\n      await tester.selectDay(content: newDate.day);\n      await tester.dismissCellEditor();\n\n      // Dismiss the event editor\n      await tester.dismissEventEditor();\n\n      // Make sure that the event is edited\n      tester.assertNumberOfEventsInCalendar(1);\n      tester.assertNumberOfEventsOnSpecificDay(1, newDate);\n\n      // Click on the unscheduled events button\n      await tester.openUnscheduledEventsPopup();\n\n      // Assert that nothing shows up\n      tester.findUnscheduledPopup(findsNothing, 0);\n\n      // Click on the event in the calendar\n      await tester.openCalendarEvent(index: 0, date: newDate);\n\n      // Open the date editor of the event\n      await tester.tapDateCellInRowDetailPage();\n      await tester.findDateEditor(findsOneWidget);\n\n      // Clear the date of the event\n      await tester.clearDate();\n\n      // Dismiss the event editor\n      await tester.dismissEventEditor();\n      tester.assertNumberOfEventsInCalendar(0);\n\n      // Click on the unscheduled events button\n      await tester.openUnscheduledEventsPopup();\n\n      // Assert that a popup appears and 1 unscheduled event\n      tester.findUnscheduledPopup(findsOneWidget, 1);\n\n      // Click on the unscheduled event\n      await tester.clickUnscheduledEvent();\n\n      tester.assertRowDetailPageOpened();\n    });\n\n    testWidgets('filter calendar events', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // Create the calendar view\n      await tester.createNewPageWithNameUnderParent(\n        layout: ViewLayoutPB.Calendar,\n      );\n\n      // Create a new event on the first of this month\n      final today = DateTime.now();\n      final firstOfThisMonth = DateTime(today.year, today.month);\n      await tester.doubleClickCalendarCell(firstOfThisMonth);\n      await tester.dismissEventEditor();\n\n      tester.assertNumberOfEventsInCalendar(1);\n\n      await tester.openCalendarEvent(index: 0, date: firstOfThisMonth);\n      await tester.tapButton(finderForFieldType(FieldType.MultiSelect));\n      await tester.createOption(name: \"asdf\");\n      await tester.createOption(name: \"qwer\");\n      await tester.selectOption(name: \"asdf\");\n      await tester.dismissCellEditor();\n      await tester.dismissCellEditor();\n\n      await tester.tapDatabaseFilterButton();\n      await tester.tapCreateFilterByFieldType(FieldType.MultiSelect, \"Tags\");\n\n      await tester.tapFilterButtonInGrid('Tags');\n      await tester.tapOptionFilterWithName('asdf');\n      await tester.dismissCellEditor();\n\n      tester.assertNumberOfEventsInCalendar(0);\n\n      await tester.tapFilterButtonInGrid('Tags');\n      await tester.tapOptionFilterWithName('asdf');\n      await tester.dismissCellEditor();\n\n      tester.assertNumberOfEventsInCalendar(1);\n\n      await tester.tapFilterButtonInGrid('Tags');\n      await tester.tapOptionFilterWithName('asdf');\n      await tester.dismissCellEditor();\n\n      tester.assertNumberOfEventsInCalendar(0);\n\n      final secondOfThisMonth = DateTime(today.year, today.month, 2);\n      await tester.doubleClickCalendarCell(secondOfThisMonth);\n      await tester.dismissEventEditor();\n      tester.assertNumberOfEventsInCalendar(1);\n\n      await tester.openCalendarEvent(index: 0, date: secondOfThisMonth);\n      await tester.tapButton(finderForFieldType(FieldType.MultiSelect));\n      await tester.selectOption(name: \"asdf\");\n      await tester.dismissCellEditor();\n      await tester.dismissCellEditor();\n\n      tester.assertNumberOfEventsInCalendar(0);\n\n      await tester.tapFilterButtonInGrid('Tags');\n      await tester.changeSelectFilterCondition(\n        SelectOptionFilterConditionPB.OptionIsEmpty,\n      );\n      await tester.dismissCellEditor();\n\n      tester.assertNumberOfEventsInCalendar(1);\n      tester.assertNumberOfEventsOnSpecificDay(1, secondOfThisMonth);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart",
    "content": "import 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:intl/intl.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('edit grid cell:', () {\n    testWidgets('text', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      await tester.editCell(\n        rowIndex: 0,\n        fieldType: FieldType.RichText,\n        input: 'hello world',\n      );\n\n      tester.assertCellContent(\n        rowIndex: 0,\n        fieldType: FieldType.RichText,\n        content: 'hello world',\n      );\n\n      await tester.pumpAndSettle();\n    });\n\n    // Make sure the text cells are filled with the right content when there are\n    // multiple text cell\n    testWidgets('multiple text cells', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(\n        name: 'my grid',\n        layout: ViewLayoutPB.Grid,\n      );\n      await tester.createField(FieldType.RichText, name: 'description');\n\n      await tester.editCell(\n        rowIndex: 0,\n        fieldType: FieldType.RichText,\n        input: 'hello',\n      );\n\n      await tester.editCell(\n        rowIndex: 0,\n        fieldType: FieldType.RichText,\n        input: 'world',\n        cellIndex: 1,\n      );\n\n      tester.assertCellContent(\n        rowIndex: 0,\n        fieldType: FieldType.RichText,\n        content: 'hello',\n      );\n\n      tester.assertCellContent(\n        rowIndex: 0,\n        fieldType: FieldType.RichText,\n        content: 'world',\n        cellIndex: 1,\n      );\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('number', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      const fieldType = FieldType.Number;\n\n      // Create a number field\n      await tester.createField(fieldType);\n\n      await tester.editCell(\n        rowIndex: 0,\n        fieldType: fieldType,\n        input: '-1',\n      );\n      // edit the next cell to force the previous cell at row 0 to lose focus\n      await tester.editCell(\n        rowIndex: 1,\n        fieldType: fieldType,\n        input: '0.2',\n      );\n      // -1 -> -1\n      tester.assertCellContent(\n        rowIndex: 0,\n        fieldType: fieldType,\n        content: '-1',\n      );\n\n      // edit the next cell to force the previous cell at row 1 to lose focus\n      await tester.editCell(\n        rowIndex: 2,\n        fieldType: fieldType,\n        input: '.1',\n      );\n      // 0.2 -> 0.2\n      tester.assertCellContent(\n        rowIndex: 1,\n        fieldType: fieldType,\n        content: '0.2',\n      );\n\n      // edit the next cell to force the previous cell at row 2 to lose focus\n      await tester.editCell(\n        rowIndex: 0,\n        fieldType: fieldType,\n        input: '',\n      );\n      // .1 -> 0.1\n      tester.assertCellContent(\n        rowIndex: 2,\n        fieldType: fieldType,\n        content: '0.1',\n      );\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('checkbox', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      await tester.assertCheckboxCell(rowIndex: 0, isSelected: false);\n      await tester.tapCheckboxCellInGrid(rowIndex: 0);\n      await tester.assertCheckboxCell(rowIndex: 0, isSelected: true);\n\n      await tester.tapCheckboxCellInGrid(rowIndex: 1);\n      await tester.tapCheckboxCellInGrid(rowIndex: 2);\n      await tester.assertCheckboxCell(rowIndex: 1, isSelected: true);\n      await tester.assertCheckboxCell(rowIndex: 2, isSelected: true);\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('created time', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      const fieldType = FieldType.CreatedTime;\n      // Create a create time field\n      // The create time field is not editable\n      await tester.createField(fieldType);\n\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);\n\n      await tester.findDateEditor(findsNothing);\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('last modified time', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      const fieldType = FieldType.LastEditedTime;\n      // Create a last time field\n      // The last time field is not editable\n      await tester.createField(fieldType);\n\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);\n\n      await tester.findDateEditor(findsNothing);\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('date time', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      const fieldType = FieldType.DateTime;\n      await tester.createField(fieldType);\n\n      // Tap the cell to invoke the field editor\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);\n      await tester.findDateEditor(findsOneWidget);\n\n      // Toggle include time\n      await tester.toggleIncludeTime();\n\n      // Dismiss the cell editor\n      await tester.dismissCellEditor();\n\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);\n      await tester.findDateEditor(findsOneWidget);\n\n      // Turn off include time\n      await tester.toggleIncludeTime();\n\n      // Select a date\n      DateTime now = DateTime.now();\n      await tester.selectDay(content: now.day);\n\n      await tester.dismissCellEditor();\n\n      tester.assertCellContent(\n        rowIndex: 0,\n        fieldType: FieldType.DateTime,\n        content: DateFormat('MMM dd, y').format(now),\n      );\n\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);\n\n      // Toggle include time\n      now = DateTime.now();\n      await tester.toggleIncludeTime();\n\n      await tester.dismissCellEditor();\n\n      tester.assertCellContent(\n        rowIndex: 0,\n        fieldType: FieldType.DateTime,\n        content: DateFormat('MMM dd, y HH:mm').format(now),\n      );\n\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);\n      await tester.findDateEditor(findsOneWidget);\n\n      // Change date format\n      await tester.tapChangeDateTimeFormatButton();\n      await tester.changeDateFormat();\n\n      await tester.dismissCellEditor();\n\n      tester.assertCellContent(\n        rowIndex: 0,\n        fieldType: FieldType.DateTime,\n        content: DateFormat('dd/MM/y HH:mm').format(now),\n      );\n\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);\n      await tester.findDateEditor(findsOneWidget);\n\n      // Change time format\n      await tester.tapChangeDateTimeFormatButton();\n      await tester.changeTimeFormat();\n\n      await tester.dismissCellEditor();\n\n      tester.assertCellContent(\n        rowIndex: 0,\n        fieldType: FieldType.DateTime,\n        content: DateFormat('dd/MM/y hh:mm a').format(now),\n      );\n\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);\n      await tester.findDateEditor(findsOneWidget);\n\n      // Clear the date and time\n      await tester.clearDate();\n\n      tester.assertCellContent(\n        rowIndex: 0,\n        fieldType: FieldType.DateTime,\n        content: '',\n      );\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('single select', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      const fieldType = FieldType.SingleSelect;\n\n      // When create a grid, it will create a single select field by default\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Tap the cell to invoke the selection option editor\n      await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);\n      await tester.findSelectOptionEditor(findsOneWidget);\n\n      // Create a new select option\n      await tester.createOption(name: 'tag 1');\n      await tester.dismissCellEditor();\n\n      // Make sure the option is created and displayed in the cell\n      tester.findSelectOptionWithNameInGrid(\n        rowIndex: 0,\n        name: 'tag 1',\n      );\n\n      await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);\n      await tester.findSelectOptionEditor(findsOneWidget);\n\n      // Create another select option\n      await tester.createOption(name: 'tag 2');\n      await tester.dismissCellEditor();\n\n      tester.findSelectOptionWithNameInGrid(\n        rowIndex: 0,\n        name: 'tag 2',\n      );\n\n      tester.assertNumberOfSelectedOptionsInGrid(\n        rowIndex: 0,\n        matcher: findsOneWidget,\n      );\n\n      await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);\n      await tester.findSelectOptionEditor(findsOneWidget);\n\n      // switch to first option\n      await tester.selectOption(name: 'tag 1');\n      await tester.dismissCellEditor();\n\n      tester.findSelectOptionWithNameInGrid(\n        rowIndex: 0,\n        name: 'tag 1',\n      );\n\n      tester.assertNumberOfSelectedOptionsInGrid(\n        rowIndex: 0,\n        matcher: findsOneWidget,\n      );\n\n      await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);\n      await tester.findSelectOptionEditor(findsOneWidget);\n\n      // Deselect the currently-selected option\n      await tester.selectOption(name: 'tag 1');\n      await tester.dismissCellEditor();\n\n      tester.assertNumberOfSelectedOptionsInGrid(\n        rowIndex: 0,\n        matcher: findsNothing,\n      );\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('multi select', (tester) async {\n      final tags = [\n        'tag 1',\n        'tag 2',\n        'tag 3',\n        'tag 4',\n      ];\n\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      const fieldType = FieldType.MultiSelect;\n      await tester.createField(fieldType, name: fieldType.i18n);\n\n      // Tap the cell to invoke the selection option editor\n      await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);\n      await tester.findSelectOptionEditor(findsOneWidget);\n\n      // Create a new select option\n      await tester.createOption(name: tags.first);\n      await tester.dismissCellEditor();\n\n      // Make sure the option is created and displayed in the cell\n      tester.findSelectOptionWithNameInGrid(\n        rowIndex: 0,\n        name: tags.first,\n      );\n\n      await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);\n      await tester.findSelectOptionEditor(findsOneWidget);\n\n      // Create some other select options\n      await tester.createOption(name: tags[1]);\n      await tester.createOption(name: tags[2]);\n      await tester.createOption(name: tags[3]);\n      await tester.dismissCellEditor();\n\n      for (final tag in tags) {\n        tester.findSelectOptionWithNameInGrid(\n          rowIndex: 0,\n          name: tag,\n        );\n      }\n\n      tester.assertNumberOfSelectedOptionsInGrid(\n        rowIndex: 0,\n        matcher: findsNWidgets(4),\n      );\n\n      await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);\n      await tester.findSelectOptionEditor(findsOneWidget);\n\n      // Deselect all options\n      for (final tag in tags) {\n        await tester.selectOption(name: tag);\n      }\n      await tester.dismissCellEditor();\n\n      tester.assertNumberOfSelectedOptionsInGrid(\n        rowIndex: 0,\n        matcher: findsNothing,\n      );\n\n      await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);\n      await tester.findSelectOptionEditor(findsOneWidget);\n\n      // Select some options\n      await tester.selectOption(name: tags[1]);\n      await tester.selectOption(name: tags[3]);\n      await tester.dismissCellEditor();\n\n      tester.findSelectOptionWithNameInGrid(\n        rowIndex: 0,\n        name: tags[1],\n      );\n      tester.findSelectOptionWithNameInGrid(\n        rowIndex: 0,\n        name: tags[3],\n      );\n\n      tester.assertNumberOfSelectedOptionsInGrid(\n        rowIndex: 0,\n        matcher: findsNWidgets(2),\n      );\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('checklist', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      const fieldType = FieldType.Checklist;\n      await tester.createField(fieldType);\n\n      // assert that there is no progress bar in the grid\n      tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);\n\n      // tap on the first checklist cell\n      await tester.tapChecklistCellInGrid(rowIndex: 0);\n\n      // assert that the checklist editor is shown\n      tester.assertChecklistEditorVisible(visible: true);\n\n      // create a new task with enter\n      await tester.createNewChecklistTask(name: \"task 1\", enter: true);\n\n      // assert that the task is displayed\n      tester.assertChecklistTaskInEditor(\n        index: 0,\n        name: \"task 1\",\n        isChecked: false,\n      );\n\n      // update the task's name\n      await tester.renameChecklistTask(index: 0, name: \"task 11\");\n\n      // assert that the task's name is updated\n      tester.assertChecklistTaskInEditor(\n        index: 0,\n        name: \"task 11\",\n        isChecked: false,\n      );\n\n      // dismiss new task editor\n      await tester.dismissCellEditor();\n\n      // dismiss checklist cell editor\n      await tester.dismissCellEditor();\n\n      // assert that progress bar is shown in grid at 0%\n      tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0);\n\n      // start editing the first checklist cell again\n      await tester.tapChecklistCellInGrid(rowIndex: 0);\n\n      // create another task with the create button\n      await tester.createNewChecklistTask(name: \"task 2\", button: true);\n\n      // assert that the task was inserted\n      tester.assertChecklistTaskInEditor(\n        index: 1,\n        name: \"task 2\",\n        isChecked: false,\n      );\n\n      // mark it as complete\n      await tester.checkChecklistTask(index: 1);\n\n      // assert that the task was checked in the editor\n      tester.assertChecklistTaskInEditor(\n        index: 1,\n        name: \"task 2\",\n        isChecked: true,\n      );\n\n      // dismiss checklist editor\n      await tester.dismissCellEditor();\n      await tester.dismissCellEditor();\n\n      // assert that progressbar is shown in grid at 50%\n      tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0.5);\n\n      // re-open the cell editor\n      await tester.tapChecklistCellInGrid(rowIndex: 0);\n\n      // hover over first task and delete it\n      await tester.deleteChecklistTask(index: 0);\n\n      // dismiss cell editor\n      await tester.dismissCellEditor();\n\n      // assert that progressbar is shown in grid at 100%\n      tester.assertChecklistCellInGrid(rowIndex: 0, percent: 1);\n\n      // re-open the cell edior\n      await tester.tapChecklistCellInGrid(rowIndex: 0);\n\n      // delete the remaining task\n      await tester.deleteChecklistTask(index: 0);\n\n      // dismiss the cell editor\n      await tester.dismissCellEditor();\n\n      // check that the progress bar is not viisble\n      tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/database/database_field_settings_test.dart",
    "content": "import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('grid field settings test:', () {\n    testWidgets('field visibility', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a database and add a linked database view\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n      await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);\n\n      // create a field\n      await tester.scrollToRight(find.byType(GridPage));\n      await tester.tapNewPropertyButton();\n      await tester.renameField('New field 1');\n      await tester.dismissFieldEditor();\n\n      // hide the field\n      await tester.tapGridFieldWithName('New field 1');\n      await tester.tapHidePropertyButton();\n      tester.noFieldWithName('New field 1');\n\n      // create another field, New field 1 to be hidden still\n      await tester.tapNewPropertyButton();\n      await tester.dismissFieldEditor();\n      tester.noFieldWithName('New field 1');\n\n      // go back to inline database view, expect field to be shown\n      await tester.tapTabBarLinkedViewByViewName('Untitled');\n      tester.findFieldWithName('New field 1');\n\n      // go back to linked database view, expect field to be hidden\n      await tester.tapTabBarLinkedViewByViewName('Grid');\n      tester.noFieldWithName('New field 1');\n\n      // use the settings button to show the field\n      await tester.tapDatabaseSettingButton();\n      await tester.tapViewPropertiesButton();\n      await tester.tapViewTogglePropertyVisibilityButtonByName('New field 1');\n      await tester.dismissFieldEditor();\n      tester.findFieldWithName('New field 1');\n\n      // open first row in popup then hide the field\n      await tester.openFirstRowDetailPage();\n      await tester.tapGridFieldWithNameInRowDetailPage('New field 1');\n      await tester.tapHidePropertyButtonInFieldEditor();\n      await tester.dismissRowDetailPage();\n      tester.noFieldWithName('New field 1');\n\n      // the field should still be sort and filter-able\n      await tester.tapDatabaseFilterButton();\n      await tester.tapCreateFilterByFieldType(\n        FieldType.RichText,\n        \"New field 1\",\n      );\n      await tester.tapDatabaseSortButton();\n      await tester.tapCreateSortByFieldType(FieldType.RichText, \"New field 1\");\n    });\n\n    testWidgets('field cell width', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a database and add a linked database view\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n      await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);\n\n      // create a field\n      await tester.scrollToRight(find.byType(GridPage));\n      await tester.tapNewPropertyButton();\n      await tester.renameField('New field 1');\n      await tester.dismissFieldEditor();\n\n      // check the width of the field\n      expect(tester.getFieldWidth('New field 1'), 150);\n\n      // change the width of the field\n      await tester.changeFieldWidth('New field 1', 200);\n      expect(tester.getFieldWidth('New field 1'), 205);\n\n      // create another field, New field 1 to be same width\n      await tester.tapNewPropertyButton();\n      await tester.dismissFieldEditor();\n      expect(tester.getFieldWidth('New field 1'), 205);\n\n      // go back to inline database view, expect New field 1 to be 150px\n      await tester.tapTabBarLinkedViewByViewName('Untitled');\n      expect(tester.getFieldWidth('New field 1'), 150);\n\n      // go back to linked database view, expect New field 1 to be 205px\n      await tester.tapTabBarLinkedViewByViewName('Grid');\n      expect(tester.getFieldWidth('New field 1'), 205);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';\nimport 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  setUpAll(() {\n    IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n    RecentIcons.enable = false;\n  });\n\n  tearDownAll(() {\n    RecentIcons.enable = true;\n  });\n\n  group('grid edit field test:', () {\n    testWidgets('rename existing field', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Invoke the field editor\n      await tester.tapGridFieldWithName('Name');\n\n      await tester.renameField('hello world');\n      await tester.dismissFieldEditor();\n\n      await tester.tapGridFieldWithName('hello world');\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('edit field icon', (tester) async {\n      const icon = 'artificial_intelligence/ai-upscale-spark';\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      tester.assertFieldSvg('Name', FieldType.RichText);\n\n      // choose specific icon\n      await tester.tapGridFieldWithName('Name');\n      await tester.changeFieldIcon(icon);\n      await tester.dismissFieldEditor();\n\n      tester.assertFieldCustomSvg('Name', icon);\n\n      // remove icon\n      await tester.tapGridFieldWithName('Name');\n      await tester.changeFieldIcon('');\n      await tester.dismissFieldEditor();\n\n      tester.assertFieldSvg('Name', FieldType.RichText);\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('update field type of existing field', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Invoke the field editor\n      await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Checkbox);\n\n      await tester.assertFieldTypeWithFieldName(\n        'Type',\n        FieldType.Checkbox,\n      );\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('create a field and rename it', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new grid\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // create a field\n      await tester.createField(FieldType.Checklist);\n      tester.findFieldWithName(FieldType.Checklist.i18n);\n\n      // editing field type during field creation should change title\n      await tester.createField(FieldType.MultiSelect);\n      tester.findFieldWithName(FieldType.MultiSelect.i18n);\n\n      // not if the user changes the title manually though\n      const name = \"New field\";\n      await tester.createField(FieldType.DateTime);\n      await tester.tapGridFieldWithName(FieldType.DateTime.i18n);\n      await tester.renameField(name);\n      await tester.tapEditFieldButton();\n      await tester.tapSwitchFieldTypeButton();\n      await tester.selectFieldType(FieldType.URL);\n      tester.findFieldWithName(name);\n    });\n\n    testWidgets('delete field', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // create a field\n      await tester.createField(FieldType.Checkbox, name: 'New field 1');\n\n      // Delete the field\n      await tester.tapGridFieldWithName('New field 1');\n      await tester.tapDeletePropertyButton();\n\n      // confirm delete\n      await tester.tapButtonWithName(LocaleKeys.space_delete.tr());\n\n      tester.noFieldWithName('New field 1');\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('duplicate field', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // create a field\n      await tester.createField(FieldType.RichText, name: 'New field 1');\n\n      // duplicate the field\n      await tester.tapGridFieldWithName('New field 1');\n      await tester.tapDuplicatePropertyButton();\n\n      tester.findFieldWithName('New field 1 (copy)');\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('insert field on either side of a field', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      await tester.scrollToRight(find.byType(GridPage));\n\n      // insert new field to the right\n      await tester.tapGridFieldWithName('Type');\n      await tester.tapInsertFieldButton(left: false, name: 'Right');\n      await tester.dismissFieldEditor();\n      tester.findFieldWithName('Right');\n\n      // insert new field to the left\n      await tester.tapGridFieldWithName('Type');\n      await tester.tapInsertFieldButton(left: true, name: \"Left\");\n      await tester.dismissFieldEditor();\n      tester.findFieldWithName('Left');\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('clear cells under field', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      await tester.scrollToRight(find.byType(GridPage));\n\n      // edit the data\n      await tester.editCell(\n        rowIndex: 0,\n        fieldType: FieldType.RichText,\n        input: 'Hello',\n      );\n      await tester.editCell(\n        rowIndex: 1,\n        fieldType: FieldType.RichText,\n        input: 'World',\n      );\n\n      expect(find.text('Hello'), findsOneWidget);\n      expect(find.text('World'), findsOneWidget);\n\n      // clear the cells\n      await tester.tapGridFieldWithName('Name');\n      await tester.tapClearCellsButton();\n      await tester.tapButtonWithName(LocaleKeys.button_confirm.tr());\n\n      expect(find.text('Hello'), findsNothing);\n      expect(find.text('World'), findsNothing);\n    });\n\n    testWidgets('create list of fields', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      for (final fieldType in [\n        FieldType.Checklist,\n        FieldType.DateTime,\n        FieldType.Number,\n        FieldType.URL,\n        FieldType.MultiSelect,\n        FieldType.LastEditedTime,\n        FieldType.CreatedTime,\n        FieldType.Checkbox,\n      ]) {\n        await tester.createField(fieldType);\n\n        // After update the field type, the cells should be updated\n        tester.findCellByFieldType(fieldType);\n        await tester.pumpAndSettle();\n      }\n    });\n\n    testWidgets('field types with empty type option editor', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      for (final fieldType in [\n        FieldType.RichText,\n        FieldType.Checkbox,\n        FieldType.Checklist,\n        FieldType.URL,\n      ]) {\n        await tester.createField(fieldType);\n\n        // open the field editor\n        await tester.tapGridFieldWithName(fieldType.i18n);\n        await tester.tapEditFieldButton();\n\n        // check type option editor is empty\n        tester.expectEmptyTypeOptionEditor();\n        await tester.dismissFieldEditor();\n      }\n    });\n\n    testWidgets('number field type option', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n      await tester.scrollToRight(find.byType(GridPage));\n\n      // create a number field\n      await tester.createField(FieldType.Number);\n\n      // enter some data into the first number cell\n      await tester.editCell(\n        rowIndex: 0,\n        fieldType: FieldType.Number,\n        input: '123',\n      );\n      // edit the next cell to force the previous cell at row 0 to lose focus\n      await tester.editCell(\n        rowIndex: 1,\n        fieldType: FieldType.Number,\n        input: '0.2',\n      );\n      tester.assertCellContent(\n        rowIndex: 0,\n        fieldType: FieldType.Number,\n        content: '123',\n      );\n\n      // open editor and change number format\n      await tester.tapGridFieldWithName(FieldType.Number.i18n);\n      await tester.tapEditFieldButton();\n      await tester.changeNumberFieldFormat();\n      await tester.dismissFieldEditor();\n\n      // assert number format has been changed\n      tester.assertCellContent(\n        rowIndex: 0,\n        fieldType: FieldType.Number,\n        content: '\\$123',\n      );\n    });\n\n    testWidgets('add option', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(\n        layout: ViewLayoutPB.Grid,\n      );\n\n      // invoke the field editor\n      await tester.tapGridFieldWithName('Type');\n      await tester.tapEditFieldButton();\n\n      // tap 'add option' button\n      await tester.tapAddSelectOptionButton();\n      const text = 'Hello AppFlowy';\n      final inputField = find.descendant(\n        of: find.byType(CreateOptionTextField),\n        matching: find.byType(TextField),\n      );\n      await tester.enterText(inputField, text);\n      await tester.testTextInput.receiveAction(TextInputAction.done);\n      await tester.pumpAndSettle(const Duration(milliseconds: 500));\n\n      // check the result\n      tester.expectToSeeText(text);\n    });\n\n    testWidgets('date time field type options', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n      await tester.scrollToRight(find.byType(GridPage));\n\n      // create a date field\n      await tester.createField(FieldType.DateTime);\n\n      // edit the first date cell\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);\n      await tester.toggleIncludeTime();\n      final now = DateTime.now();\n      await tester.selectDay(content: now.day);\n\n      await tester.dismissCellEditor();\n\n      tester.assertCellContent(\n        rowIndex: 0,\n        fieldType: FieldType.DateTime,\n        content: DateFormat('MMM dd, y HH:mm').format(now),\n      );\n\n      // open editor and change date & time format\n      await tester.tapGridFieldWithName(FieldType.DateTime.i18n);\n      await tester.tapEditFieldButton();\n      await tester.changeDateFormat();\n      await tester.changeTimeFormat();\n      await tester.dismissFieldEditor();\n\n      // assert date format has been changed\n      tester.assertCellContent(\n        rowIndex: 0,\n        fieldType: FieldType.DateTime,\n        content: DateFormat('dd/MM/y hh:mm a').format(now),\n      );\n    });\n\n    testWidgets('text in viewport while typing', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      await tester.changeCalculateAtIndex(0, CalculationType.Count);\n\n      // add very large text with 200 lines\n      final largeText = List.generate(\n        200,\n        (index) => 'Line ${index + 1}',\n      ).join('\\n');\n\n      await tester.editCell(\n        rowIndex: 2,\n        fieldType: FieldType.RichText,\n        input: largeText,\n      );\n\n      // checks if last line is in view port\n      tester.expectToSeeText('Line 200');\n    });\n\n    // Disable this test because it fails on CI randomly\n    // testWidgets('last modified and created at field type options',\n    //     (tester) async {\n    //   await tester.initializeAppFlowy();\n    //   await tester.tapGoButton();\n\n    //   await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n    //   final created = DateTime.now();\n\n    //   // create a created at field\n    //   await tester.tapNewPropertyButton();\n    //   await tester.renameField(FieldType.CreatedTime.i18n);\n    //   await tester.tapSwitchFieldTypeButton();\n    //   await tester.selectFieldType(FieldType.CreatedTime);\n    //   await tester.dismissFieldEditor();\n\n    //   // create a last modified field\n    //   await tester.tapNewPropertyButton();\n    //   await tester.renameField(FieldType.LastEditedTime.i18n);\n    //   await tester.tapSwitchFieldTypeButton();\n\n    //   // get time just before modifying\n    //   final modified = DateTime.now();\n\n    //   // create a last modified field (cont'd)\n    //   await tester.selectFieldType(FieldType.LastEditedTime);\n    //   await tester.dismissFieldEditor();\n\n    //   tester.assertCellContent(\n    //     rowIndex: 0,\n    //     fieldType: FieldType.CreatedTime,\n    //     content: DateFormat('MMM dd, y HH:mm').format(created),\n    //   );\n    //   tester.assertCellContent(\n    //     rowIndex: 0,\n    //     fieldType: FieldType.LastEditedTime,\n    //     content: DateFormat('MMM dd, y HH:mm').format(modified),\n    //   );\n\n    //   // open field editor and change date & time format\n    //   await tester.tapGridFieldWithName(FieldType.LastEditedTime.i18n);\n    //   await tester.tapEditFieldButton();\n    //   await tester.changeDateFormat();\n    //   await tester.changeTimeFormat();\n    //   await tester.dismissFieldEditor();\n\n    //   // open field editor and change date & time format\n    //   await tester.tapGridFieldWithName(FieldType.CreatedTime.i18n);\n    //   await tester.tapEditFieldButton();\n    //   await tester.changeDateFormat();\n    //   await tester.changeTimeFormat();\n    //   await tester.dismissFieldEditor();\n\n    //   // assert format has been changed\n    //   tester.assertCellContent(\n    //     rowIndex: 0,\n    //     fieldType: FieldType.CreatedTime,\n    //     content: DateFormat('dd/MM/y hh:mm a').format(created),\n    //   );\n    //   tester.assertCellContent(\n    //     rowIndex: 0,\n    //     fieldType: FieldType.LastEditedTime,\n    //     content: DateFormat('dd/MM/y hh:mm a').format(modified),\n    //   );\n    // });\n\n    testWidgets('select option transform', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(\n        layout: ViewLayoutPB.Grid,\n      );\n\n      // invoke the field editor of existing Single-Select field Type\n      await tester.tapGridFieldWithName('Type');\n      await tester.tapEditFieldButton();\n\n      // add some select options\n      await tester.tapAddSelectOptionButton();\n      for (final optionName in ['A', 'B', 'C']) {\n        final inputField = find.descendant(\n          of: find.byType(CreateOptionTextField),\n          matching: find.byType(TextField),\n        );\n        await tester.enterText(inputField, optionName);\n        await tester.testTextInput.receiveAction(TextInputAction.done);\n      }\n      await tester.dismissFieldEditor();\n\n      // select A in first row's cell under the Type field\n      await tester.tapCellInGrid(\n        rowIndex: 0,\n        fieldType: FieldType.SingleSelect,\n      );\n      await tester.selectOption(name: 'A');\n      await tester.dismissCellEditor();\n      tester.findSelectOptionWithNameInGrid(name: 'A', rowIndex: 0);\n\n      await tester.changeFieldTypeOfFieldWithName('Type', FieldType.RichText);\n      tester.assertCellContent(\n        rowIndex: 0,\n        fieldType: FieldType.RichText,\n        content: \"A\",\n        cellIndex: 1,\n      );\n\n      // add some random text in the second row\n      await tester.editCell(\n        rowIndex: 1,\n        fieldType: FieldType.RichText,\n        input: \"random\",\n        cellIndex: 1,\n      );\n      tester.assertCellContent(\n        rowIndex: 1,\n        fieldType: FieldType.RichText,\n        content: \"random\",\n        cellIndex: 1,\n      );\n\n      await tester.changeFieldTypeOfFieldWithName(\n        'Type',\n        FieldType.SingleSelect,\n      );\n      tester.findSelectOptionWithNameInGrid(name: 'A', rowIndex: 0);\n      tester.assertNumberOfSelectedOptionsInGrid(\n        rowIndex: 1,\n        matcher: findsNothing,\n      );\n\n      // create a new field for testing\n      await tester.createField(FieldType.RichText, name: 'Test');\n\n      // edit the first 2 rows\n      await tester.editCell(\n        rowIndex: 0,\n        fieldType: FieldType.RichText,\n        input: \"E,F\",\n        cellIndex: 1,\n      );\n      await tester.editCell(\n        rowIndex: 1,\n        fieldType: FieldType.RichText,\n        input: \"G\",\n        cellIndex: 1,\n      );\n\n      await tester.changeFieldTypeOfFieldWithName(\n        'Test',\n        FieldType.MultiSelect,\n      );\n      tester.assertMultiSelectOption(contents: ['E', 'F'], rowIndex: 0);\n      tester.assertMultiSelectOption(contents: ['G'], rowIndex: 1);\n\n      await tester.tapCellInGrid(\n        rowIndex: 2,\n        fieldType: FieldType.MultiSelect,\n      );\n      await tester.selectOption(name: 'G');\n      await tester.createOption(name: 'H');\n      await tester.dismissCellEditor();\n      tester.findSelectOptionWithNameInGrid(name: 'A', rowIndex: 0);\n      tester.assertMultiSelectOption(contents: ['G', 'H'], rowIndex: 2);\n\n      await tester.changeFieldTypeOfFieldWithName(\n        'Test',\n        FieldType.RichText,\n      );\n      tester.assertCellContent(\n        rowIndex: 2,\n        fieldType: FieldType.RichText,\n        content: \"G,H\",\n        cellIndex: 1,\n      );\n      await tester.changeFieldTypeOfFieldWithName(\n        'Test',\n        FieldType.MultiSelect,\n      );\n\n      tester.assertMultiSelectOption(contents: ['E', 'F'], rowIndex: 0);\n      tester.assertMultiSelectOption(contents: ['G'], rowIndex: 1);\n      tester.assertMultiSelectOption(contents: ['G', 'H'], rowIndex: 2);\n    });\n\n    testWidgets('date time transform', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n      await tester.scrollToRight(find.byType(GridPage));\n\n      // create a date field\n      await tester.createField(FieldType.DateTime);\n\n      // edit the first date cell\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);\n      final now = DateTime.now();\n      await tester.toggleIncludeTime();\n      await tester.selectDay(content: now.day);\n\n      await tester.dismissCellEditor();\n\n      tester.assertCellContent(\n        rowIndex: 0,\n        fieldType: FieldType.DateTime,\n        content: DateFormat('MMM dd, y HH:mm').format(now),\n      );\n\n      await tester.changeFieldTypeOfFieldWithName(\n        'Date',\n        FieldType.RichText,\n      );\n      tester.assertCellContent(\n        rowIndex: 0,\n        fieldType: FieldType.RichText,\n        content: DateFormat('MMM dd, y HH:mm').format(now),\n        cellIndex: 1,\n      );\n\n      await tester.editCell(\n        rowIndex: 1,\n        fieldType: FieldType.RichText,\n        input: \"Oct 5, 2024\",\n        cellIndex: 1,\n      );\n      tester.assertCellContent(\n        rowIndex: 1,\n        fieldType: FieldType.RichText,\n        content: \"Oct 5, 2024\",\n        cellIndex: 1,\n      );\n\n      await tester.changeFieldTypeOfFieldWithName(\n        'Date',\n        FieldType.DateTime,\n      );\n      tester.assertCellContent(\n        rowIndex: 0,\n        fieldType: FieldType.DateTime,\n        content: DateFormat('MMM dd, y').format(now),\n      );\n      tester.assertCellContent(\n        rowIndex: 1,\n        fieldType: FieldType.DateTime,\n        content: \"Oct 05, 2024\",\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/database/database_filter_test.dart",
    "content": "import 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_detail.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('grid filter:', () {\n    testWidgets('add text filter', (tester) async {\n      await tester.openTestDatabase(v020GridFileName);\n\n      // create a filter\n      await tester.tapDatabaseFilterButton();\n      await tester.tapCreateFilterByFieldType(FieldType.RichText, 'Name');\n      await tester.tapFilterButtonInGrid('Name');\n\n      // enter 'A' in the filter text field\n      tester.assertNumberOfRowsInGridPage(10);\n      await tester.enterTextInTextFilter('A');\n      tester.assertNumberOfRowsInGridPage(1);\n\n      // after remove the filter, the grid should show all rows\n      await tester.enterTextInTextFilter('');\n      tester.assertNumberOfRowsInGridPage(10);\n\n      await tester.enterTextInTextFilter('B');\n      tester.assertNumberOfRowsInGridPage(1);\n\n      // open the menu to delete the filter\n      await tester.tapDisclosureButtonInFinder(find.byType(TextFilterEditor));\n      await tester.tapDeleteFilterButtonInGrid();\n      tester.assertNumberOfRowsInGridPage(10);\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('add checkbox filter', (tester) async {\n      await tester.openTestDatabase(v020GridFileName);\n\n      // create a filter\n      await tester.tapDatabaseFilterButton();\n      await tester.tapCreateFilterByFieldType(FieldType.Checkbox, 'Done');\n      tester.assertNumberOfRowsInGridPage(5);\n\n      await tester.tapFilterButtonInGrid('Done');\n      await tester.tapCheckboxFilterButtonInGrid();\n\n      await tester.tapUnCheckedButtonOnCheckboxFilter();\n      tester.assertNumberOfRowsInGridPage(5);\n\n      await tester\n          .tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));\n      await tester.tapDeleteFilterButtonInGrid();\n      tester.assertNumberOfRowsInGridPage(10);\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('add checklist filter', (tester) async {\n      await tester.openTestDatabase(v020GridFileName);\n\n      // create a filter\n      await tester.tapDatabaseFilterButton();\n      await tester.tapCreateFilterByFieldType(FieldType.Checklist, 'checklist');\n\n      // By default, the condition of checklist filter is 'uncompleted'\n      tester.assertNumberOfRowsInGridPage(9);\n\n      await tester.tapFilterButtonInGrid('checklist');\n      await tester.tapChecklistFilterButtonInGrid();\n\n      await tester.tapCompletedButtonOnChecklistFilter();\n      tester.assertNumberOfRowsInGridPage(1);\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('add single select filter', (tester) async {\n      await tester.openTestDatabase(v020GridFileName);\n\n      // create a filter\n      await tester.tapDatabaseFilterButton();\n      await tester.tapCreateFilterByFieldType(FieldType.SingleSelect, 'Type');\n\n      await tester.tapFilterButtonInGrid('Type');\n\n      // select the option 's6'\n      await tester.tapOptionFilterWithName('s6');\n      tester.assertNumberOfRowsInGridPage(0);\n\n      // unselect the option 's6'\n      await tester.tapOptionFilterWithName('s6');\n      tester.assertNumberOfRowsInGridPage(10);\n\n      // select the option 's5'\n      await tester.tapOptionFilterWithName('s5');\n      tester.assertNumberOfRowsInGridPage(1);\n\n      // select the option 's4'\n      await tester.tapOptionFilterWithName('s4');\n\n      // The row with 's4' should be shown.\n      tester.assertNumberOfRowsInGridPage(2);\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('add multi select filter', (tester) async {\n      await tester.openTestDatabase(v020GridFileName);\n\n      // create a filter\n      await tester.tapDatabaseFilterButton();\n      await tester.tapCreateFilterByFieldType(\n        FieldType.MultiSelect,\n        'multi-select',\n      );\n\n      await tester.tapFilterButtonInGrid('multi-select');\n      await tester.scrollOptionFilterListByOffset(const Offset(0, -200));\n\n      // select the option 'm1'. Any option with 'm1' should be shown.\n      await tester.tapOptionFilterWithName('m1');\n      tester.assertNumberOfRowsInGridPage(5);\n      await tester.tapOptionFilterWithName('m1');\n\n      // select the option 'm2'. Any option with 'm2' should be shown.\n      await tester.tapOptionFilterWithName('m2');\n      tester.assertNumberOfRowsInGridPage(4);\n      await tester.tapOptionFilterWithName('m2');\n\n      // select the option 'm4'. Any option with 'm4' should be shown.\n      await tester.tapOptionFilterWithName('m4');\n      tester.assertNumberOfRowsInGridPage(1);\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('add date filter', (tester) async {\n      await tester.openTestDatabase(v020GridFileName);\n\n      // create a filter\n      await tester.tapDatabaseFilterButton();\n      await tester.tapCreateFilterByFieldType(FieldType.DateTime, 'date');\n\n      // By default, the condition of date filter is current day and time\n      tester.assertNumberOfRowsInGridPage(0);\n\n      await tester.tapFilterButtonInGrid('date');\n      await tester.changeDateFilterCondition(DateTimeFilterCondition.before);\n      tester.assertNumberOfRowsInGridPage(7);\n\n      await tester.changeDateFilterCondition(DateTimeFilterCondition.isEmpty);\n      tester.assertNumberOfRowsInGridPage(3);\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('add timestamp filter', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      await tester.createField(\n        FieldType.CreatedTime,\n        name: 'Created at',\n      );\n\n      // create a filter\n      await tester.tapDatabaseFilterButton();\n      await tester.tapCreateFilterByFieldType(\n        FieldType.CreatedTime,\n        'Created at',\n      );\n      await tester.pumpAndSettle();\n\n      tester.assertNumberOfRowsInGridPage(3);\n\n      await tester.tapFilterButtonInGrid('Created at');\n      await tester.changeDateFilterCondition(DateTimeFilterCondition.before);\n      tester.assertNumberOfRowsInGridPage(0);\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('create new row when filters don\\'t autofill', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // create a filter\n      await tester.tapDatabaseFilterButton();\n      await tester.tapCreateFilterByFieldType(\n        FieldType.RichText,\n        'Name',\n      );\n      tester.assertNumberOfRowsInGridPage(3);\n\n      await tester.tapCreateRowButtonInGrid();\n      tester.assertNumberOfRowsInGridPage(4);\n\n      await tester.tapFilterButtonInGrid('Name');\n      await tester\n          .changeTextFilterCondition(TextFilterConditionPB.TextIsNotEmpty);\n      await tester.dismissCellEditor();\n      tester.assertNumberOfRowsInGridPage(0);\n\n      await tester.tapCreateRowButtonInGrid();\n      tester.assertNumberOfRowsInGridPage(0);\n      expect(find.byType(RowDetailPage), findsOneWidget);\n\n      await tester.pumpAndSettle();\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/database/database_icon_test.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dart';\nimport 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart';\nimport 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/emoji.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  setUpAll(() {\n    IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n    RecentIcons.enable = false;\n  });\n\n  tearDownAll(() {\n    RecentIcons.enable = true;\n  });\n\n  testWidgets('change icon', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    final iconData = await tester.loadIcon();\n\n    const pageName = 'Database';\n    await tester.createNewPageWithNameUnderParent(\n      layout: ViewLayoutPB.Grid,\n      name: pageName,\n    );\n\n    /// create board\n    final addButton = find.byType(AddDatabaseViewButton);\n    await tester.tapButton(addButton);\n    await tester.tapButton(\n      find.text(\n        '${LocaleKeys.grid_createView.tr()} ${DatabaseLayoutPB.Board.layoutName}',\n        findRichText: true,\n      ),\n    );\n\n    /// create calendar\n    await tester.tapButton(addButton);\n    await tester.tapButton(\n      find.text(\n        '${LocaleKeys.grid_createView.tr()} ${DatabaseLayoutPB.Calendar.layoutName}',\n        findRichText: true,\n      ),\n    );\n\n    final databaseTabBarItem = find.byType(DatabaseTabBarItem);\n    expect(databaseTabBarItem, findsNWidgets(3));\n    final gridItem = databaseTabBarItem.first,\n        boardItem = databaseTabBarItem.at(1),\n        calendarItem = databaseTabBarItem.last;\n\n    /// change the icon of grid\n    /// the first tapping is to select specific item\n    /// the second tapping is to show the menu\n    await tester.tapButton(gridItem);\n    await tester.tapButton(gridItem);\n\n    /// change icon\n    await tester\n        .tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));\n    await tester.tapIcon(iconData, enableColor: false);\n    final gridIcon = find.descendant(\n      of: gridItem,\n      matching: find.byType(RawEmojiIconWidget),\n    );\n    final gridIconWidget =\n        gridIcon.evaluate().first.widget as RawEmojiIconWidget;\n    final iconsData = IconsData.fromJson(jsonDecode(iconData.emoji));\n    final gridIconsData =\n        IconsData.fromJson(jsonDecode(gridIconWidget.emoji.emoji));\n    expect(gridIconsData.iconName, iconsData.iconName);\n\n    /// change the icon of board\n    await tester.tapButton(boardItem);\n    await tester.tapButton(boardItem);\n    await tester\n        .tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));\n    await tester.tapIcon(iconData, enableColor: false);\n    final boardIcon = find.descendant(\n      of: boardItem,\n      matching: find.byType(RawEmojiIconWidget),\n    );\n    final boardIconWidget =\n        boardIcon.evaluate().first.widget as RawEmojiIconWidget;\n    final boardIconsData =\n        IconsData.fromJson(jsonDecode(boardIconWidget.emoji.emoji));\n    expect(boardIconsData.iconName, iconsData.iconName);\n\n    /// change the icon of calendar\n    await tester.tapButton(calendarItem);\n    await tester.tapButton(calendarItem);\n    await tester\n        .tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));\n    await tester.tapIcon(iconData, enableColor: false);\n    final calendarIcon = find.descendant(\n      of: calendarItem,\n      matching: find.byType(RawEmojiIconWidget),\n    );\n    final calendarIconWidget =\n        calendarIcon.evaluate().first.widget as RawEmojiIconWidget;\n    final calendarIconsData =\n        IconsData.fromJson(jsonDecode(calendarIconWidget.emoji.emoji));\n    expect(calendarIconsData.iconName, iconsData.iconName);\n  });\n\n  testWidgets('change database icon from sidebar', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    final iconData = await tester.loadIcon();\n    final icon = IconsData.fromJson(jsonDecode(iconData.emoji)), emoji = '😄';\n\n    const pageName = 'Database';\n    await tester.createNewPageWithNameUnderParent(\n      layout: ViewLayoutPB.Grid,\n      name: pageName,\n    );\n    final viewItem = find.descendant(\n      of: find.byType(SidebarFolder),\n      matching: find.byWidgetPredicate(\n        (w) => w is ViewItem && w.view.name == pageName,\n      ),\n    );\n\n    /// change icon to emoji\n    await tester.tapButton(\n      find.descendant(\n        of: viewItem,\n        matching: find.byType(FlowySvg),\n      ),\n    );\n    await tester.tapEmoji(emoji);\n    final iconWidget = find.descendant(\n      of: viewItem,\n      matching: find.byType(RawEmojiIconWidget),\n    );\n    expect(\n      (iconWidget.evaluate().first.widget as RawEmojiIconWidget).emoji.emoji,\n      emoji,\n    );\n\n    /// the icon will not be displayed in database item\n    Finder databaseIcon = find.descendant(\n      of: find.byType(DatabaseTabBarItem),\n      matching: find.byType(FlowySvg),\n    );\n    expect(\n      (databaseIcon.evaluate().first.widget as FlowySvg).svg,\n      FlowySvgs.icon_grid_s,\n    );\n\n    /// change emoji to icon\n    await tester.tapButton(iconWidget);\n    await tester.tapIcon(iconData);\n    expect(\n      (iconWidget.evaluate().first.widget as RawEmojiIconWidget).emoji.emoji,\n      iconData.emoji,\n    );\n\n    databaseIcon = find.descendant(\n      of: find.byType(DatabaseTabBarItem),\n      matching: find.byType(RawEmojiIconWidget),\n    );\n    final databaseIconWidget =\n        databaseIcon.evaluate().first.widget as RawEmojiIconWidget;\n    final databaseIconsData =\n        IconsData.fromJson(jsonDecode(databaseIconWidget.emoji.emoji));\n    expect(icon.svgString, databaseIconsData.svgString);\n    expect(icon.color, isNotEmpty);\n    expect(icon.color, databaseIconsData.color);\n\n    /// the icon in database item should not show the color\n    expect(databaseIconWidget.enableColor, false);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/database/database_media_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/services.dart';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/mock/mock_file_picker.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('media type option in database', () {\n    testWidgets('add media field and add files two times', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Invoke the field editor\n      await tester.tapGridFieldWithName('Type');\n      await tester.tapEditFieldButton();\n\n      // Change to media type\n      await tester.tapSwitchFieldTypeButton();\n      await tester.selectFieldType(FieldType.Media);\n      await tester.dismissFieldEditor();\n\n      // Open media cell editor\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.Media);\n      await tester.findMediaCellEditor(findsOneWidget);\n\n      // Prepare files for upload from local\n      final firstImage =\n          await rootBundle.load('assets/test/images/sample.jpeg');\n      final secondImage =\n          await rootBundle.load('assets/test/images/sample.gif');\n      final tempDirectory = await getTemporaryDirectory();\n\n      final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');\n      final firstFile = File(firstImagePath)\n        ..writeAsBytesSync(firstImage.buffer.asUint8List());\n\n      final secondImagePath = p.join(tempDirectory.path, 'sample.gif');\n      final secondFile = File(secondImagePath)\n        ..writeAsBytesSync(secondImage.buffer.asUint8List());\n\n      mockPickFilePaths(paths: [firstImagePath]);\n      await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');\n\n      // Click on add file button in the Media Cell Editor\n      await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));\n      await tester.pumpAndSettle();\n\n      // Tap on the upload interaction\n      await tester.tapFileUploadHint();\n\n      // Expect one file\n      expect(find.byType(RenderMedia), findsOneWidget);\n\n      // Mock second file\n      mockPickFilePaths(paths: [secondImagePath]);\n\n      // Click on add file button in the Media Cell Editor\n      await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));\n      await tester.pumpAndSettle();\n\n      // Tap on the upload interaction\n      await tester.tapFileUploadHint();\n      await tester.pumpAndSettle();\n\n      // Expect two files\n      expect(find.byType(RenderMedia), findsNWidgets(2));\n\n      // Remove the temp files\n      await Future.wait([firstFile.delete(), secondFile.delete()]);\n    });\n\n    testWidgets('add two files at once', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Invoke the field editor\n      await tester.tapGridFieldWithName('Type');\n      await tester.tapEditFieldButton();\n\n      // Change to media type\n      await tester.tapSwitchFieldTypeButton();\n      await tester.selectFieldType(FieldType.Media);\n      await tester.dismissFieldEditor();\n\n      // Open media cell editor\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.Media);\n      await tester.findMediaCellEditor(findsOneWidget);\n\n      // Prepare files for upload from local\n      final firstImage =\n          await rootBundle.load('assets/test/images/sample.jpeg');\n      final secondImage =\n          await rootBundle.load('assets/test/images/sample.gif');\n      final tempDirectory = await getTemporaryDirectory();\n\n      final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');\n      final firstFile = File(firstImagePath)\n        ..writeAsBytesSync(firstImage.buffer.asUint8List());\n\n      final secondImagePath = p.join(tempDirectory.path, 'sample.gif');\n      final secondFile = File(secondImagePath)\n        ..writeAsBytesSync(secondImage.buffer.asUint8List());\n\n      mockPickFilePaths(paths: [firstImagePath, secondImagePath]);\n      await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');\n\n      // Click on add file button in the Media Cell Editor\n      await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));\n      await tester.pumpAndSettle();\n\n      // Tap on the upload interaction\n      await tester.tapFileUploadHint();\n\n      // Expect two files\n      expect(find.byType(RenderMedia), findsNWidgets(2));\n\n      // Remove the temp files\n      await Future.wait([firstFile.delete(), secondFile.delete()]);\n    });\n\n    testWidgets('delete files', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Invoke the field editor\n      await tester.tapGridFieldWithName('Type');\n      await tester.tapEditFieldButton();\n\n      // Change to media type\n      await tester.tapSwitchFieldTypeButton();\n      await tester.selectFieldType(FieldType.Media);\n      await tester.dismissFieldEditor();\n\n      // Open media cell editor\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.Media);\n      await tester.findMediaCellEditor(findsOneWidget);\n\n      // Prepare files for upload from local\n      final firstImage =\n          await rootBundle.load('assets/test/images/sample.jpeg');\n      final secondImage =\n          await rootBundle.load('assets/test/images/sample.gif');\n      final tempDirectory = await getTemporaryDirectory();\n\n      final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');\n      final firstFile = File(firstImagePath)\n        ..writeAsBytesSync(firstImage.buffer.asUint8List());\n\n      final secondImagePath = p.join(tempDirectory.path, 'sample.gif');\n      final secondFile = File(secondImagePath)\n        ..writeAsBytesSync(secondImage.buffer.asUint8List());\n\n      mockPickFilePaths(paths: [firstImagePath, secondImagePath]);\n      await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');\n\n      // Click on add file button in the Media Cell Editor\n      await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));\n      await tester.pumpAndSettle();\n\n      // Tap on the upload interaction\n      await tester.tapFileUploadHint();\n\n      // Expect two files\n      expect(find.byType(RenderMedia), findsNWidgets(2));\n\n      // Tap on the three dots menu for the first RenderMedia\n      final mediaMenuFinder = find.descendant(\n        of: find.byType(RenderMedia),\n        matching: find.byFlowySvg(FlowySvgs.three_dots_s),\n      );\n\n      await tester.tap(mediaMenuFinder.first);\n      await tester.pumpAndSettle();\n\n      // Tap on the delete button\n      await tester.tap(find.text(LocaleKeys.grid_media_delete.tr()));\n      await tester.pumpAndSettle();\n\n      // Tap on Delete button in the confirmation dialog\n      await tester.tap(\n        find.descendant(\n          of: find.byType(SpaceCancelOrConfirmButton),\n          matching: find.text(LocaleKeys.grid_media_delete.tr()),\n        ),\n      );\n      await tester.pumpAndSettle(const Duration(seconds: 1));\n\n      // Expect one file\n      expect(find.byType(RenderMedia), findsOneWidget);\n\n      // Remove the temp files\n      await Future.wait([firstFile.delete(), secondFile.delete()]);\n    });\n\n    testWidgets('show file names', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Invoke the field editor\n      await tester.tapGridFieldWithName('Type');\n      await tester.tapEditFieldButton();\n\n      // Change to media type\n      await tester.tapSwitchFieldTypeButton();\n      await tester.selectFieldType(FieldType.Media);\n      await tester.dismissFieldEditor();\n\n      // Open media cell editor\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.Media);\n      await tester.findMediaCellEditor(findsOneWidget);\n\n      // Prepare files for upload from local\n      final firstImage =\n          await rootBundle.load('assets/test/images/sample.jpeg');\n      final secondImage =\n          await rootBundle.load('assets/test/images/sample.gif');\n      final tempDirectory = await getTemporaryDirectory();\n\n      final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');\n      final firstFile = File(firstImagePath)\n        ..writeAsBytesSync(firstImage.buffer.asUint8List());\n\n      final secondImagePath = p.join(tempDirectory.path, 'sample.gif');\n      final secondFile = File(secondImagePath)\n        ..writeAsBytesSync(secondImage.buffer.asUint8List());\n\n      mockPickFilePaths(paths: [firstImagePath, secondImagePath]);\n      await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');\n\n      // Click on add file button in the Media Cell Editor\n      await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));\n      await tester.pumpAndSettle();\n\n      // Tap on the upload interaction\n      await tester.tapFileUploadHint();\n\n      // Expect two files\n      expect(find.byType(RenderMedia), findsNWidgets(2));\n\n      await tester.dismissCellEditor();\n      await tester.pumpAndSettle();\n\n      // Open first row in row detail view then toggle show file names\n      await tester.openFirstRowDetailPage();\n      await tester.pumpAndSettle();\n\n      // Expect file names to not be shown (hidden)\n      expect(find.text('sample.jpeg'), findsNothing);\n      expect(find.text('sample.gif'), findsNothing);\n\n      await tester.tapGridFieldWithNameInRowDetailPage('Type');\n      await tester.pumpAndSettle();\n\n      // Toggle show file names\n      await tester.tap(find.byType(Toggle));\n      await tester.pumpAndSettle();\n\n      // Expect file names to be shown\n      expect(find.text('sample.jpeg'), findsOneWidget);\n      expect(find.text('sample.gif'), findsOneWidget);\n\n      await tester.dismissRowDetailPage();\n      await tester.pumpAndSettle();\n\n      // Remove the temp files\n      await Future.wait([firstFile.delete(), secondFile.delete()]);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/database/database_reminder_test.dart",
    "content": "import 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('reminder in database', () {\n    testWidgets('add date field and add reminder', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Invoke the field editor\n      await tester.tapGridFieldWithName('Type');\n      await tester.tapEditFieldButton();\n\n      // Change to date type\n      await tester.tapSwitchFieldTypeButton();\n      await tester.selectFieldType(FieldType.DateTime);\n      await tester.dismissFieldEditor();\n\n      // Open date picker\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);\n      await tester.findDateEditor(findsOneWidget);\n\n      // Select date\n      final isToday = await tester.selectLastDateInPicker();\n\n      // Select \"On day of event\" reminder\n      await tester.selectReminderOption(ReminderOption.onDayOfEvent);\n\n      // Expect \"On day of event\" to be displayed\n      tester.expectSelectedReminder(ReminderOption.onDayOfEvent);\n\n      // Dismiss the cell/date editor\n      await tester.dismissCellEditor();\n\n      // Open date picker again\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);\n      await tester.findDateEditor(findsOneWidget);\n\n      // Expect \"On day of event\" to be displayed\n      tester.expectSelectedReminder(ReminderOption.onDayOfEvent);\n\n      // Dismiss the cell/date editor\n      await tester.dismissCellEditor();\n\n      int tabIndex = 1;\n      final now = DateTime.now();\n      if (isToday && now.hour >= 9) {\n        tabIndex = 0;\n      }\n\n      // Open \"Upcoming\" in Notification hub\n      await tester.openNotificationHub(tabIndex: tabIndex);\n\n      // Expect 1 notification\n      tester.expectNotificationItems(1);\n    });\n\n    testWidgets('navigate from reminder to open row', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Invoke the field editor\n      await tester.tapGridFieldWithName('Type');\n      await tester.tapEditFieldButton();\n\n      // Change to date type\n      await tester.tapSwitchFieldTypeButton();\n      await tester.selectFieldType(FieldType.DateTime);\n      await tester.dismissFieldEditor();\n\n      // Open date picker\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);\n      await tester.findDateEditor(findsOneWidget);\n\n      // Select date\n      final isToday = await tester.selectLastDateInPicker();\n\n      // Select \"On day of event\"-reminder\n      await tester.selectReminderOption(ReminderOption.onDayOfEvent);\n\n      // Expect \"On day of event\" to be displayed\n      tester.expectSelectedReminder(ReminderOption.onDayOfEvent);\n\n      // Dismiss the cell/date editor\n      await tester.dismissCellEditor();\n\n      // Open date picker again\n      await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);\n      await tester.findDateEditor(findsOneWidget);\n\n      // Expect \"On day of event\" to be displayed\n      tester.expectSelectedReminder(ReminderOption.onDayOfEvent);\n\n      // Dismiss the cell/date editor\n      await tester.dismissCellEditor();\n\n      // Create and Navigate to a new document\n      await tester.createNewPageWithNameUnderParent();\n      await tester.pumpAndSettle();\n\n      int tabIndex = 1;\n      final now = DateTime.now();\n      if (isToday && now.hour >= 9) {\n        tabIndex = 0;\n      }\n\n      // Open correct tab in Notification hub\n      await tester.openNotificationHub(tabIndex: tabIndex);\n\n      // Expect 1 notification\n      tester.expectNotificationItems(1);\n\n      // Tap on the notification\n      await tester.tap(find.byType(NotificationItem));\n      await tester.pumpAndSettle();\n\n      // Expect to see Row Editor Dialog\n      tester.expectToSeeRowDetailsPageDialog();\n    });\n\n    testWidgets(\n      'toggle include time sets reminder option correctly',\n      (tester) async {\n        await tester.initializeAppFlowy();\n        await tester.tapAnonymousSignInButton();\n\n        await tester.createNewPageWithNameUnderParent(\n          layout: ViewLayoutPB.Grid,\n        );\n\n        // Invoke the field editor\n        await tester.tapGridFieldWithName('Type');\n        await tester.tapEditFieldButton();\n\n        // Change to date type\n        await tester.tapSwitchFieldTypeButton();\n        await tester.selectFieldType(FieldType.DateTime);\n        await tester.dismissFieldEditor();\n\n        // Open date picker\n        await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);\n        await tester.findDateEditor(findsOneWidget);\n\n        // Select date\n        await tester.selectLastDateInPicker();\n\n        // Select \"On day of event\"-reminder\n        await tester.selectReminderOption(ReminderOption.onDayOfEvent);\n\n        // Expect \"On day of event\" to be displayed\n        tester.expectSelectedReminder(ReminderOption.onDayOfEvent);\n\n        // Dismiss the cell/date editor\n        await tester.dismissCellEditor();\n\n        // Open date picker again\n        await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);\n        await tester.findDateEditor(findsOneWidget);\n\n        // Expect \"On day of event\" to be displayed\n        tester.expectSelectedReminder(ReminderOption.onDayOfEvent);\n\n        // Toggle include time on\n        await tester.toggleIncludeTime();\n\n        // Expect \"At time of event\" to be displayed\n        tester.expectSelectedReminder(ReminderOption.atTimeOfEvent);\n\n        // Dismiss the cell/date editor\n        await tester.dismissCellEditor();\n\n        // Open date picker again\n        await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);\n        await tester.findDateEditor(findsOneWidget);\n\n        // Expect \"At time of event\" to be displayed\n        tester.expectSelectedReminder(ReminderOption.atTimeOfEvent);\n\n        // Select \"One hour before\"-reminder\n        await tester.selectReminderOption(ReminderOption.oneHourBefore);\n\n        // Expect \"One hour before\" to be displayed\n        tester.expectSelectedReminder(ReminderOption.oneHourBefore);\n\n        // Toggle include time off\n        await tester.toggleIncludeTime();\n\n        // Expect \"On day of event\" to be displayed\n        tester.expectSelectedReminder(ReminderOption.onDayOfEvent);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/database/database_row_cover_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/widgets/card/card.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_banner.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/shared/af_image.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/mock/mock_file_picker.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('database row cover', () {\n    testWidgets('add and remove cover from Row Detail Card', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Open first row in row detail view\n      await tester.openFirstRowDetailPage();\n      await tester.pumpAndSettle();\n\n      // Expect no cover\n      expect(find.byType(RowCover), findsNothing);\n\n      // Hover on RowBanner to show Add Cover button\n      await tester.hoverRowBanner();\n\n      // Click on Add Cover button\n      await tester.tapAddCoverButton();\n\n      // Expect a cover to be shown - the default asset cover\n      expect(find.byType(RowCover), findsOneWidget);\n\n      // Tap on the delete cover button\n      await tester.tapButton(find.byType(DeleteCoverButton));\n      await tester.pumpAndSettle();\n\n      // Expect no cover to be shown\n      expect(find.byType(AFImage), findsNothing);\n    });\n\n    testWidgets('add and change cover and check in Board', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board);\n      await tester.pumpAndSettle();\n\n      // Open \"Card 1\"\n      await tester.tap(find.text('Card 1'), warnIfMissed: false);\n      await tester.pumpAndSettle();\n\n      // Expect no cover\n      expect(find.byType(RowCover), findsNothing);\n\n      // Hover on RowBanner to show Add Cover button\n      await tester.hoverRowBanner();\n\n      // Click on Add Cover button\n      await tester.tapAddCoverButton();\n\n      // Expect default cover to be shown\n      expect(find.byType(RowCover), findsOneWidget);\n\n      // Prepare image for upload from local\n      final image = await rootBundle.load('assets/test/images/sample.jpeg');\n      final tempDirectory = await getTemporaryDirectory();\n      final imagePath = p.join(tempDirectory.path, 'sample.jpeg');\n      final file = File(imagePath)\n        ..writeAsBytesSync(image.buffer.asUint8List());\n\n      mockPickFilePaths(paths: [imagePath]);\n      await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');\n\n      // Hover on RowBanner to show Change Cover button\n      await tester.hoverRowBanner();\n\n      // Tap on the change cover button\n      await tester.tapButtonWithName(\n        LocaleKeys.document_plugins_cover_changeCover.tr(),\n      );\n      await tester.pumpAndSettle();\n\n      // Change tab to Upload tab\n      await tester.tapButtonWithName(\n        LocaleKeys.document_imageBlock_upload_label.tr(),\n      );\n\n      // Tab on the upload button\n      await tester.tapButtonWithName(\n        LocaleKeys.document_imageBlock_upload_placeholder.tr(),\n      );\n\n      // Expect one cover\n      expect(find.byType(RowCover), findsOneWidget);\n\n      // Expect the cover to be shown both in RowCover and in CardCover\n      expect(find.byType(AFImage), findsNWidgets(2));\n\n      // Dismiss Row Detail Page\n      await tester.dismissRowDetailPage();\n\n      // Expect a cover to be shown in CardCover\n      expect(\n        find.descendant(\n          of: find.byType(CardCover),\n          matching: find.byType(AFImage),\n        ),\n        findsOneWidget,\n      );\n\n      // Remove the temp file\n      await Future.wait([file.delete()]);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/database/database_row_page_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_detail.dart';\nimport 'package:appflowy/plugins/document/document_page.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/emoji.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  setUpAll(() {\n    IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n    RecentIcons.enable = false;\n  });\n\n  tearDownAll(() {\n    RecentIcons.enable = true;\n  });\n\n  group('grid row detail page:', () {\n    testWidgets('opens', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // Create a new grid\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Hover first row and then open the row page\n      await tester.openFirstRowDetailPage();\n\n      // Make sure that the row page is opened\n      tester.assertRowDetailPageOpened();\n\n      // Each row detail page should have a document\n      await tester.assertDocumentExistInRowDetailPage();\n    });\n\n    testWidgets('add and update emoji', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // Create a new grid\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Hover first row and then open the row page\n      await tester.openFirstRowDetailPage();\n      await tester.hoverRowBanner();\n      await tester.openEmojiPicker();\n      await tester.tapEmoji('😀');\n\n      // expect to find the emoji selected\n      final firstEmojiFinder = find.byWidgetPredicate(\n        (w) => w is FlowyText && w.text == '😀',\n      );\n\n      // There are 2 eomjis - one in the row banner and another in the primary cell\n      expect(firstEmojiFinder, findsNWidgets(2));\n\n      // Update existing selected emoji - tap on it to update\n      await tester.tapButton(find.byType(EmojiIconWidget));\n      await tester.pumpAndSettle();\n\n      await tester.tapEmoji('😅');\n\n      // The emoji already displayed in the row banner\n      final emojiText = find.byWidgetPredicate(\n        (widget) => widget is FlowyText && widget.text == '😅',\n      );\n\n      // The number of emoji should be two. One in the row displayed in the grid\n      // one in the row detail page.\n      expect(emojiText, findsNWidgets(2));\n\n      // insert a sub page in database\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.showSlashMenu();\n      await tester.pumpAndSettle();\n      await tester.editor.tapSlashMenuItemWithName(\n        LocaleKeys.document_slashMenu_subPage_name.tr(),\n        offset: 100,\n      );\n      await tester.pumpAndSettle();\n\n      // the row detail page should be closed\n      final rowDetailPage = find.byType(RowDetailPage);\n      await tester.pumpUntilNotFound(rowDetailPage);\n\n      // expect to see a document page\n      final documentPage = find.byType(DocumentPage);\n      expect(documentPage, findsOneWidget);\n    });\n\n    testWidgets('remove emoji', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // Create a new grid\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Hover first row and then open the row page\n      await tester.openFirstRowDetailPage();\n      await tester.hoverRowBanner();\n      await tester.openEmojiPicker();\n      await tester.tapEmoji('😀');\n\n      // Remove the emoji\n      await tester.tapButton(find.byType(EmojiIconWidget));\n      await tester.tapButton(find.text(LocaleKeys.button_remove.tr()));\n\n      final emojiText = find.byWidgetPredicate(\n        (widget) => widget is FlowyText && widget.text == '😀',\n      );\n      expect(emojiText, findsNothing);\n    });\n\n    testWidgets('create list of fields', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // Create a new grid\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Hover first row and then open the row page\n      await tester.openFirstRowDetailPage();\n\n      for (final fieldType in [\n        FieldType.Checklist,\n        FieldType.DateTime,\n        FieldType.Number,\n        FieldType.URL,\n        FieldType.MultiSelect,\n        FieldType.LastEditedTime,\n        FieldType.CreatedTime,\n        FieldType.Checkbox,\n      ]) {\n        await tester.tapRowDetailPageCreatePropertyButton();\n\n        // Open the type option menu\n        await tester.tapSwitchFieldTypeButton();\n\n        await tester.selectFieldType(fieldType);\n\n        final field = find.descendant(\n          of: find.byType(RowDetailPage),\n          matching: find.byWidgetPredicate(\n            (widget) =>\n                widget is FieldCellButton &&\n                widget.field.name == fieldType.i18n,\n          ),\n        );\n        expect(field, findsOneWidget);\n\n        // After update the field type, the cells should be updated\n        tester.findCellByFieldType(fieldType);\n        await tester.scrollRowDetailByOffset(const Offset(0, -50));\n      }\n    });\n\n    testWidgets('change order of fields and cells', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // Create a new grid\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Hover first row and then open the row page\n      await tester.openFirstRowDetailPage();\n\n      // Assert that the first field in the row details page is the select\n      // option type\n      tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect);\n\n      // Reorder first field in list\n      final gesture = await tester.hoverOnFieldInRowDetail(index: 0);\n      await tester.pumpAndSettle();\n      await tester.reorderFieldInRowDetail(offset: 30);\n\n      // Orders changed, now the checkbox is first\n      tester.assertFirstFieldInRowDetailByType(FieldType.Checkbox);\n      await gesture.removePointer();\n      await tester.pumpAndSettle();\n\n      // Reorder second field in list\n      await tester.hoverOnFieldInRowDetail(index: 1);\n      await tester.pumpAndSettle();\n      await tester.reorderFieldInRowDetail(offset: -30);\n\n      // First field is now back to select option\n      tester.assertFirstFieldInRowDetailByType(FieldType.SingleSelect);\n    });\n\n    testWidgets('hide and show hidden fields', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // Create a new grid\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Hover first row and then open the row page\n      await tester.openFirstRowDetailPage();\n\n      // Assert that the show hidden fields button isn't visible\n      tester.assertToggleShowHiddenFieldsVisibility(false);\n\n      // Hide the first field in the field list\n      await tester.tapGridFieldWithNameInRowDetailPage(\"Type\");\n      await tester.tapHidePropertyButtonInFieldEditor();\n\n      // Assert that the field is now hidden\n      tester.noFieldWithName(\"Type\");\n\n      // Assert that the show hidden fields button appears\n      tester.assertToggleShowHiddenFieldsVisibility(true);\n\n      // Click on the show hidden fields button\n      await tester.toggleShowHiddenFields();\n\n      // Assert that the hidden field is shown again and that the show\n      // hidden fields button is still present\n      tester.findFieldWithName(\"Type\");\n      tester.assertToggleShowHiddenFieldsVisibility(true);\n\n      // Click hide hidden fields\n      await tester.toggleShowHiddenFields();\n\n      // Assert that the hidden field has vanished\n      tester.noFieldWithName(\"Type\");\n\n      // Click show hidden fields\n      await tester.toggleShowHiddenFields();\n\n      // delete the hidden field\n      await tester.tapGridFieldWithNameInRowDetailPage(\"Type\");\n      await tester.tapDeletePropertyInFieldEditor();\n\n      // Assert that the that the show hidden fields button is gone\n      tester.assertToggleShowHiddenFieldsVisibility(false);\n    });\n\n    testWidgets('update the contents of the document and re-open it',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // Create a new grid\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Hover first row and then open the row page\n      await tester.openFirstRowDetailPage();\n\n      // Wait for the document to be loaded\n      await tester.wait(500);\n\n      // Focus on the editor\n      final textBlock = find.byType(ParagraphBlockComponentWidget);\n      await tester.tapAt(tester.getCenter(textBlock));\n      await tester.pumpAndSettle();\n\n      // Input some text\n      const inputText = 'Hello World';\n      await tester.ime.insertText(inputText);\n      expect(\n        find.textContaining(inputText, findRichText: true),\n        findsOneWidget,\n      );\n\n      // Tap outside to dismiss the field\n      await tester.tapAt(Offset.zero);\n      await tester.pumpAndSettle();\n\n      // Re-open the document\n      await tester.openFirstRowDetailPage();\n      expect(\n        find.textContaining(inputText, findRichText: true),\n        findsOneWidget,\n      );\n    });\n\n    testWidgets(\n        'check if the title wraps properly when a long text is inserted',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // Create a new grid\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Hover first row and then open the row page\n      await tester.openFirstRowDetailPage();\n\n      // Wait for the document to be loaded\n      await tester.wait(500);\n\n      // Focus on the editor\n      final textField = find\n          .descendant(\n            of: find.byType(SimpleDialog),\n            matching: find.byType(TextField),\n          )\n          .first;\n\n      // Input a long text\n      await tester.enterText(textField, 'Long text' * 25);\n      await tester.pumpAndSettle();\n\n      // Tap outside to dismiss the field\n      await tester.tapAt(Offset.zero);\n      await tester.pumpAndSettle();\n\n      // Check if there is any overflow in the widget tree\n      expect(tester.takeException(), isNull);\n\n      // Re-open the document\n      await tester.openFirstRowDetailPage();\n\n      // Check again if there is any overflow in the widget tree\n      expect(tester.takeException(), isNull);\n    });\n\n    testWidgets('delete row', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // Create a new grid\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Hover first row and then open the row page\n      await tester.openFirstRowDetailPage();\n\n      await tester.tapRowDetailPageRowActionButton();\n      await tester.tapRowDetailPageDeleteRowButton();\n      await tester.tapEscButton();\n\n      tester.assertNumberOfRowsInGridPage(2);\n    });\n\n    testWidgets('duplicate row', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // Create a new grid\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Hover first row and then open the row page\n      await tester.openFirstRowDetailPage();\n\n      await tester.tapRowDetailPageRowActionButton();\n      await tester.tapRowDetailPageDuplicateRowButton();\n      await tester.tapEscButton();\n\n      tester.assertNumberOfRowsInGridPage(4);\n    });\n\n    testWidgets('edit checklist cell', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      const fieldType = FieldType.Checklist;\n      await tester.createField(fieldType);\n\n      await tester.openFirstRowDetailPage();\n      await tester.hoverOnWidget(\n        find.byType(ChecklistRowDetailCell),\n        onHover: () async {\n          await tester.tapButton(find.byType(ChecklistItemControl));\n        },\n      );\n\n      tester.assertPhantomChecklistItemAtIndex(index: 0);\n      await tester.enterText(find.byType(PhantomChecklistItem), 'task 1');\n      await tester.pumpAndSettle();\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      await tester.pumpAndSettle(const Duration(milliseconds: 500));\n\n      tester.assertChecklistTaskInEditor(\n        index: 0,\n        name: \"task 1\",\n        isChecked: false,\n      );\n      tester.assertPhantomChecklistItemAtIndex(index: 1);\n      tester.assertPhantomChecklistItemContent(\"\");\n\n      await tester.enterText(find.byType(PhantomChecklistItem), 'task 2');\n      await tester.pumpAndSettle();\n      await tester.hoverOnWidget(\n        find.byType(ChecklistRowDetailCell),\n        onHover: () async {\n          await tester.tapButton(find.byType(ChecklistItemControl));\n        },\n      );\n\n      tester.assertChecklistTaskInEditor(\n        index: 1,\n        name: \"task 2\",\n        isChecked: false,\n      );\n      tester.assertPhantomChecklistItemAtIndex(index: 2);\n      tester.assertPhantomChecklistItemContent(\"\");\n\n      await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n      await tester.pumpAndSettle();\n      expect(find.byType(PhantomChecklistItem), findsNothing);\n\n      await tester.renameChecklistTask(index: 0, name: \"task -1\", enter: false);\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n\n      tester.assertChecklistTaskInEditor(\n        index: 0,\n        name: \"task -1\",\n        isChecked: false,\n      );\n      tester.assertPhantomChecklistItemAtIndex(index: 1);\n\n      await tester.enterText(find.byType(PhantomChecklistItem), 'task 0');\n      await tester.pumpAndSettle();\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      await tester.pumpAndSettle(const Duration(milliseconds: 500));\n\n      tester.assertPhantomChecklistItemAtIndex(index: 2);\n\n      await tester.checkChecklistTask(index: 1);\n      expect(find.byType(PhantomChecklistItem), findsNothing);\n      expect(find.byType(ChecklistItem), findsNWidgets(3));\n\n      tester.assertChecklistTaskInEditor(\n        index: 0,\n        name: \"task -1\",\n        isChecked: false,\n      );\n      tester.assertChecklistTaskInEditor(\n        index: 1,\n        name: \"task 0\",\n        isChecked: true,\n      );\n      tester.assertChecklistTaskInEditor(\n        index: 2,\n        name: \"task 2\",\n        isChecked: false,\n      );\n\n      await tester.tapButton(\n        find.descendant(\n          of: find.byType(ProgressAndHideCompleteButton),\n          matching: find.byType(FlowyIconButton),\n        ),\n      );\n      expect(find.byType(ChecklistItem), findsNWidgets(2));\n\n      tester.assertChecklistTaskInEditor(\n        index: 0,\n        name: \"task -1\",\n        isChecked: false,\n      );\n      tester.assertChecklistTaskInEditor(\n        index: 1,\n        name: \"task 2\",\n        isChecked: false,\n      );\n\n      await tester.renameChecklistTask(index: 1, name: \"task 3\", enter: false);\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n\n      await tester.renameChecklistTask(index: 0, name: \"task 1\", enter: false);\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n\n      await tester.enterText(find.byType(PhantomChecklistItem), 'task 2');\n      await tester.pumpAndSettle();\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      await tester.pumpAndSettle(const Duration(milliseconds: 500));\n\n      tester.assertChecklistTaskInEditor(\n        index: 0,\n        name: \"task 1\",\n        isChecked: false,\n      );\n      tester.assertChecklistTaskInEditor(\n        index: 1,\n        name: \"task 2\",\n        isChecked: false,\n      );\n      tester.assertChecklistTaskInEditor(\n        index: 2,\n        name: \"task 3\",\n        isChecked: false,\n      );\n      tester.assertPhantomChecklistItemAtIndex(index: 2);\n\n      await tester.checkChecklistTask(index: 1);\n      expect(find.byType(ChecklistItem), findsNWidgets(2));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/database/database_setting_test.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('grid', () {\n    testWidgets('update layout', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // open setting\n      await tester.tapDatabaseSettingButton();\n      // select the layout\n      await tester.tapDatabaseLayoutButton();\n      // select layout by board\n      await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board);\n      await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board);\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('update layout multiple times', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // open setting\n      await tester.tapDatabaseSettingButton();\n      await tester.tapDatabaseLayoutButton();\n      await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Board);\n      await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Board);\n\n      await tester.tapDatabaseSettingButton();\n      await tester.tapDatabaseLayoutButton();\n      await tester.selectDatabaseLayoutType(DatabaseLayoutPB.Calendar);\n      await tester.assertCurrentDatabaseLayoutType(DatabaseLayoutPB.Calendar);\n\n      await tester.pumpAndSettle();\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/database/database_share_test.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/database_test_op.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('database', () {\n    testWidgets('import v0.2.0 database data', (tester) async {\n      await tester.openTestDatabase(v020GridFileName);\n      // wait the database data is loaded\n      await tester.pumpAndSettle(const Duration(microseconds: 500));\n\n      // check the text cell\n      final textCells = <String>['A', 'B', 'C', 'D', 'E', '', '', '', '', ''];\n      for (final (index, content) in textCells.indexed) {\n        tester.assertCellContent(\n          rowIndex: index,\n          fieldType: FieldType.RichText,\n          content: content,\n        );\n      }\n\n      // check the checkbox cell\n      final checkboxCells = <bool>[\n        true,\n        true,\n        true,\n        true,\n        true,\n        false,\n        false,\n        false,\n        false,\n        false,\n      ];\n      for (final (index, content) in checkboxCells.indexed) {\n        await tester.assertCheckboxCell(\n          rowIndex: index,\n          isSelected: content,\n        );\n      }\n\n      // check the number cell\n      final numberCells = <String>[\n        '-1',\n        '-2',\n        '0.1',\n        '0.2',\n        '1',\n        '2',\n        '10',\n        '11',\n        '12',\n        '',\n      ];\n      for (final (index, content) in numberCells.indexed) {\n        tester.assertCellContent(\n          rowIndex: index,\n          fieldType: FieldType.Number,\n          content: content,\n        );\n      }\n\n      // check the url cell\n      final urlCells = <String>[\n        'appflowy.io',\n        'no url',\n        'appflowy.io',\n        'https://github.com/AppFlowy-IO/',\n        '',\n        '',\n      ];\n      for (final (index, content) in urlCells.indexed) {\n        tester.assertCellContent(\n          rowIndex: index,\n          fieldType: FieldType.URL,\n          content: content,\n        );\n      }\n\n      // check the single select cell\n      final singleSelectCells = <String>[\n        's1',\n        's2',\n        's3',\n        's4',\n        's5',\n        '',\n        '',\n        '',\n        '',\n        '',\n      ];\n      for (final (index, content) in singleSelectCells.indexed) {\n        await tester.assertSingleSelectOption(\n          rowIndex: index,\n          content: content,\n        );\n      }\n\n      // check the multi select cell\n      final List<List<String>> multiSelectCells = [\n        ['m1'],\n        ['m1', 'm2'],\n        ['m1', 'm2', 'm3'],\n        ['m1', 'm2', 'm3'],\n        ['m1', 'm2', 'm3', 'm4', 'm5'],\n        [],\n        [],\n        [],\n        [],\n        [],\n      ];\n      for (final (index, contents) in multiSelectCells.indexed) {\n        tester.assertMultiSelectOption(\n          rowIndex: index,\n          contents: contents,\n        );\n      }\n\n      // check the checklist cell\n      final List<double?> checklistCells = [\n        0.67,\n        0.33,\n        1.0,\n        null,\n        null,\n        null,\n        null,\n        null,\n        null,\n        null,\n      ];\n      for (final (index, percent) in checklistCells.indexed) {\n        tester.assertChecklistCellInGrid(\n          rowIndex: index,\n          percent: percent,\n        );\n      }\n\n      // check the date cell\n      final List<String> dateCells = [\n        'Jun 01, 2023',\n        'Jun 02, 2023',\n        'Jun 03, 2023',\n        'Jun 04, 2023',\n        'Jun 05, 2023',\n        'Jun 05, 2023',\n        'Jun 16, 2023',\n        '',\n        '',\n        '',\n      ];\n      for (final (index, content) in dateCells.indexed) {\n        tester.assertCellContent(\n          rowIndex: index,\n          fieldType: FieldType.DateTime,\n          content: content,\n        );\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/database/database_sort_test.dart",
    "content": "import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_editor.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/database_test_op.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('grid sort:', () {\n    testWidgets('text sort', (tester) async {\n      await tester.openTestDatabase(v020GridFileName);\n      // create a sort\n      await tester.tapDatabaseSortButton();\n      await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');\n\n      // check the text cell order\n      final textCells = <String>[\n        'A',\n        'B',\n        'C',\n        'D',\n        'E',\n        '',\n        '',\n        '',\n        '',\n        '',\n      ];\n      for (final (index, content) in textCells.indexed) {\n        tester.assertCellContent(\n          rowIndex: index,\n          fieldType: FieldType.RichText,\n          content: content,\n        );\n      }\n\n      // open the sort menu and select order by descending\n      await tester.tapSortMenuInSettingBar();\n      await tester.tapEditSortConditionButtonByFieldName('Name');\n      await tester.tapSortByDescending();\n      for (final (index, content) in <String>[\n        'E',\n        'D',\n        'C',\n        'B',\n        'A',\n        '',\n        '',\n        '',\n        '',\n        '',\n      ].indexed) {\n        tester.assertCellContent(\n          rowIndex: index,\n          fieldType: FieldType.RichText,\n          content: content,\n        );\n      }\n\n      // delete all sorts\n      await tester.tapSortMenuInSettingBar();\n      await tester.tapDeleteAllSortsButton();\n\n      // check the text cell order\n      for (final (index, content) in <String>[\n        'A',\n        'B',\n        'C',\n        'D',\n        'E',\n        '',\n        '',\n        '',\n        '',\n        '',\n      ].indexed) {\n        tester.assertCellContent(\n          rowIndex: index,\n          fieldType: FieldType.RichText,\n          content: content,\n        );\n      }\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('checkbox', (tester) async {\n      await tester.openTestDatabase(v020GridFileName);\n      // create a sort\n      await tester.tapDatabaseSortButton();\n      await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');\n\n      // check the checkbox cell order\n      for (final (index, content) in <bool>[\n        false,\n        false,\n        false,\n        false,\n        false,\n        true,\n        true,\n        true,\n        true,\n        true,\n      ].indexed) {\n        await tester.assertCheckboxCell(\n          rowIndex: index,\n          isSelected: content,\n        );\n      }\n\n      // open the sort menu and select order by descending\n      await tester.tapSortMenuInSettingBar();\n      await tester.tapEditSortConditionButtonByFieldName('Done');\n      await tester.tapSortByDescending();\n      for (final (index, content) in <bool>[\n        true,\n        true,\n        true,\n        true,\n        true,\n        false,\n        false,\n        false,\n        false,\n        false,\n      ].indexed) {\n        await tester.assertCheckboxCell(\n          rowIndex: index,\n          isSelected: content,\n        );\n      }\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('number', (tester) async {\n      await tester.openTestDatabase(v020GridFileName);\n      // create a sort\n      await tester.tapDatabaseSortButton();\n      await tester.tapCreateSortByFieldType(FieldType.Number, 'number');\n\n      // check the number cell order\n      for (final (index, content) in <String>[\n        '-2',\n        '-1',\n        '0.1',\n        '0.2',\n        '1',\n        '2',\n        '10',\n        '11',\n        '12',\n        '',\n      ].indexed) {\n        tester.assertCellContent(\n          rowIndex: index,\n          fieldType: FieldType.Number,\n          content: content,\n        );\n      }\n\n      // open the sort menu and select order by descending\n      await tester.tapSortMenuInSettingBar();\n      await tester.tapEditSortConditionButtonByFieldName('number');\n      await tester.tapSortByDescending();\n      for (final (index, content) in <String>[\n        '12',\n        '11',\n        '10',\n        '2',\n        '1',\n        '0.2',\n        '0.1',\n        '-1',\n        '-2',\n        '',\n      ].indexed) {\n        tester.assertCellContent(\n          rowIndex: index,\n          fieldType: FieldType.Number,\n          content: content,\n        );\n      }\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('checkbox and number', (tester) async {\n      await tester.openTestDatabase(v020GridFileName);\n      // create a sort\n      await tester.tapDatabaseSortButton();\n      await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');\n\n      // open the sort menu and sort checkbox by descending\n      await tester.tapSortMenuInSettingBar();\n      await tester.tapEditSortConditionButtonByFieldName('Done');\n      await tester.tapSortByDescending();\n      for (final (index, content) in <bool>[\n        true,\n        true,\n        true,\n        true,\n        true,\n        false,\n        false,\n        false,\n        false,\n        false,\n      ].indexed) {\n        await tester.assertCheckboxCell(\n          rowIndex: index,\n          isSelected: content,\n        );\n      }\n\n      // add another sort, this time by number descending\n      await tester.tapSortMenuInSettingBar();\n      await tester.tapCreateSortByFieldTypeInSortMenu(\n        FieldType.Number,\n        'number',\n      );\n      await tester.tapEditSortConditionButtonByFieldName('number');\n      await tester.tapSortByDescending();\n\n      // check checkbox cell order\n      for (final (index, content) in <bool>[\n        true,\n        true,\n        true,\n        true,\n        true,\n        false,\n        false,\n        false,\n        false,\n        false,\n      ].indexed) {\n        await tester.assertCheckboxCell(\n          rowIndex: index,\n          isSelected: content,\n        );\n      }\n\n      // check number cell order\n      for (final (index, content) in <String>[\n        '1',\n        '0.2',\n        '0.1',\n        '-1',\n        '-2',\n        '12',\n        '11',\n        '10',\n        '2',\n        '',\n      ].indexed) {\n        tester.assertCellContent(\n          rowIndex: index,\n          fieldType: FieldType.Number,\n          content: content,\n        );\n      }\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('reorder sort', (tester) async {\n      await tester.openTestDatabase(v020GridFileName);\n      // create a sort\n      await tester.tapDatabaseSortButton();\n      await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done');\n\n      // open the sort menu and sort checkbox by descending\n      await tester.tapSortMenuInSettingBar();\n      await tester.tapEditSortConditionButtonByFieldName('Done');\n      await tester.tapSortByDescending();\n\n      // add another sort, this time by number descending\n      await tester.tapSortMenuInSettingBar();\n      await tester.tapCreateSortByFieldTypeInSortMenu(\n        FieldType.Number,\n        'number',\n      );\n      await tester.tapEditSortConditionButtonByFieldName('number');\n      await tester.tapSortByDescending();\n\n      // check checkbox cell order\n      for (final (index, content) in <bool>[\n        true,\n        true,\n        true,\n        true,\n        true,\n        false,\n        false,\n        false,\n        false,\n        false,\n      ].indexed) {\n        await tester.assertCheckboxCell(\n          rowIndex: index,\n          isSelected: content,\n        );\n      }\n\n      // check number cell order\n      for (final (index, content) in <String>[\n        '1',\n        '0.2',\n        '0.1',\n        '-1',\n        '-2',\n        '12',\n        '11',\n        '10',\n        '2',\n        '',\n      ].indexed) {\n        tester.assertCellContent(\n          rowIndex: index,\n          fieldType: FieldType.Number,\n          content: content,\n        );\n      }\n\n      // reorder sort\n      await tester.tapSortMenuInSettingBar();\n      await tester.reorderSort(\n        (FieldType.Number, 'number'),\n        (FieldType.Checkbox, 'Done'),\n      );\n\n      // check checkbox cell order\n      for (final (index, content) in <bool>[\n        false,\n        false,\n        false,\n        false,\n        true,\n        true,\n        true,\n        true,\n        true,\n        false,\n      ].indexed) {\n        await tester.assertCheckboxCell(\n          rowIndex: index,\n          isSelected: content,\n        );\n      }\n\n      // check the number cell order\n      for (final (index, content) in <String>[\n        '12',\n        '11',\n        '10',\n        '2',\n        '1',\n        '0.2',\n        '0.1',\n        '-1',\n        '-2',\n        '',\n      ].indexed) {\n        tester.assertCellContent(\n          rowIndex: index,\n          fieldType: FieldType.Number,\n          content: content,\n        );\n      }\n    });\n\n    testWidgets('edit field', (tester) async {\n      await tester.openTestDatabase(v020GridFileName);\n\n      // create a number sort\n      await tester.tapDatabaseSortButton();\n      await tester.tapCreateSortByFieldType(FieldType.Number, 'number');\n\n      // check the number cell order\n      for (final (index, content) in <String>[\n        '-2',\n        '-1',\n        '0.1',\n        '0.2',\n        '1',\n        '2',\n        '10',\n        '11',\n        '12',\n        '',\n      ].indexed) {\n        tester.assertCellContent(\n          rowIndex: index,\n          fieldType: FieldType.Number,\n          content: content,\n        );\n      }\n\n      final textCells = <String>[\n        'B',\n        'A',\n        'C',\n        'D',\n        'E',\n        '',\n        '',\n        '',\n        '',\n        '',\n      ];\n      for (final (index, content) in textCells.indexed) {\n        tester.assertCellContent(\n          rowIndex: index,\n          fieldType: FieldType.RichText,\n          content: content,\n        );\n      }\n\n      // edit the name of the number field\n      await tester.tapGridFieldWithName('number');\n\n      await tester.renameField('hello world');\n      await tester.dismissFieldEditor();\n\n      await tester.tapGridFieldWithName('hello world');\n      await tester.dismissFieldEditor();\n\n      // expect name to be changed as well\n      await tester.tapSortMenuInSettingBar();\n      final sortItem = find.ancestor(\n        of: find.text('hello world'),\n        matching: find.byType(DatabaseSortItem),\n      );\n      expect(sortItem, findsOneWidget);\n\n      // change the field type of the field to checkbox\n      await tester.tapGridFieldWithName('hello world');\n      await tester.changeFieldTypeOfFieldWithName(\n        'hello world',\n        FieldType.Checkbox,\n      );\n\n      // expect name to be changed as well\n      await tester.tapSortMenuInSettingBar();\n      expect(sortItem, findsOneWidget);\n\n      final newTextCells = <String>[\n        'A',\n        'B',\n        'C',\n        'D',\n        'E',\n        '',\n        '',\n        '',\n        '',\n        '',\n      ];\n      for (final (index, content) in newTextCells.indexed) {\n        tester.assertCellContent(\n          rowIndex: index,\n          fieldType: FieldType.RichText,\n          content: content,\n        );\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/database/database_test_runner_1.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'database_cell_test.dart' as database_cell_test;\nimport 'database_field_settings_test.dart' as database_field_settings_test;\nimport 'database_field_test.dart' as database_field_test;\nimport 'database_row_page_test.dart' as database_row_page_test;\nimport 'database_setting_test.dart' as database_setting_test;\nimport 'database_share_test.dart' as database_share_test;\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  database_cell_test.main();\n  database_field_test.main();\n  database_field_settings_test.main();\n  database_share_test.main();\n  database_row_page_test.main();\n  database_setting_test.main();\n  // DON'T add more tests here.\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/database/database_test_runner_2.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'database_calendar_test.dart' as database_calendar_test;\nimport 'database_filter_test.dart' as database_filter_test;\nimport 'database_media_test.dart' as database_media_test;\nimport 'database_row_cover_test.dart' as database_row_cover_test;\nimport 'database_share_test.dart' as database_share_test;\nimport 'database_sort_test.dart' as database_sort_test;\nimport 'database_view_test.dart' as database_view_test;\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  database_filter_test.main();\n  database_sort_test.main();\n  database_view_test.main();\n  database_calendar_test.main();\n  database_media_test.main();\n  database_row_cover_test.main();\n  database_share_test.main();\n  // DON'T add more tests here.\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/database/database_view_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_animate/flutter_animate.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('database', () {\n    testWidgets('create linked view', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Create board view\n      await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board);\n      tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);\n\n      // Create grid view\n      await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);\n      tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Grid);\n\n      // Create calendar view\n      await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Calendar);\n      tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Calendar);\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('rename and delete linked view', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Create board view\n      await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board);\n      tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);\n\n      // rename board view\n      await tester.renameLinkedView(\n        tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board),\n        'new board',\n      );\n      final findBoard = tester.findTabBarLinkViewByViewName('new board');\n      expect(findBoard, findsOneWidget);\n\n      // delete the board\n      await tester.deleteDatebaseView(findBoard);\n      expect(tester.findTabBarLinkViewByViewName('new board'), findsNothing);\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('delete the last database view', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Create board view\n      await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Board);\n      tester.assertCurrentDatabaseTagIs(DatabaseLayoutPB.Board);\n\n      // delete the board\n      await tester.deleteDatebaseView(\n        tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Board),\n      );\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('insert grid in column', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      /// create page and show slash menu\n      await tester.createNewPageWithNameUnderParent(name: 'test page');\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.showSlashMenu();\n      await tester.pumpAndSettle();\n\n      /// create a column\n      await tester.editor.tapSlashMenuItemWithName(\n        LocaleKeys.document_slashMenu_name_twoColumns.tr(),\n      );\n      final actionList = find.byType(BlockActionList);\n      expect(actionList, findsNWidgets(2));\n      final position = tester.getCenter(actionList.last);\n\n      /// tap the second child of column\n      await tester.tapAt(position.copyWith(dx: position.dx + 50));\n\n      /// create a grid\n      await tester.editor.showSlashMenu();\n      await tester.pumpAndSettle();\n      await tester.editor.tapSlashMenuItemWithName(\n        LocaleKeys.document_slashMenu_name_grid.tr(),\n      );\n\n      final grid = find.byType(GridPageContent);\n      expect(grid, findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/keyboard.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('document alignment', () {\n    testWidgets('edit alignment in toolbar', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      final selection = Selection.single(\n        path: [0],\n        startOffset: 0,\n        endOffset: 1,\n      );\n      // click the first line of the readme\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.updateSelection(selection);\n      await tester.pumpAndSettle();\n\n      // click the align center\n      await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m);\n      await tester\n          .tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_center_m);\n\n      // expect to see the align center\n      final editorState = tester.editor.getCurrentEditorState();\n      final first = editorState.getNodeAtPath([0])!;\n      expect(first.attributes[blockComponentAlign], 'center');\n\n      // click the align right\n      await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m);\n      await tester\n          .tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_right_m);\n      expect(first.attributes[blockComponentAlign], 'right');\n\n      // click the align left\n      await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m);\n      await tester\n          .tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_left_m);\n      expect(first.attributes[blockComponentAlign], 'left');\n    });\n\n    testWidgets('edit alignment using shortcut', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // click the first line of the readme\n      await tester.editor.tapLineOfEditorAt(0);\n\n      await tester.pumpAndSettle();\n\n      final editorState = tester.editor.getCurrentEditorState();\n      final first = editorState.getNodeAtPath([0])!;\n\n      // expect to see text aligned to the right\n      await FlowyTestKeyboard.simulateKeyDownEvent(\n        [\n          LogicalKeyboardKey.control,\n          LogicalKeyboardKey.shift,\n          LogicalKeyboardKey.keyR,\n        ],\n        tester: tester,\n        withKeyUp: true,\n      );\n      expect(first.attributes[blockComponentAlign], rightAlignmentKey);\n\n      // expect to see text aligned to the center\n      await FlowyTestKeyboard.simulateKeyDownEvent(\n        [\n          LogicalKeyboardKey.control,\n          LogicalKeyboardKey.shift,\n          LogicalKeyboardKey.keyC,\n        ],\n        tester: tester,\n        withKeyUp: true,\n      );\n      expect(first.attributes[blockComponentAlign], centerAlignmentKey);\n\n      // expect to see text aligned to the left\n      await FlowyTestKeyboard.simulateKeyDownEvent(\n        [\n          LogicalKeyboardKey.control,\n          LogicalKeyboardKey.shift,\n          LogicalKeyboardKey.keyL,\n        ],\n        tester: tester,\n        withKeyUp: true,\n      );\n      expect(first.attributes[blockComponentAlign], leftAlignmentKey);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_app_lifecycle_test.dart",
    "content": "import 'dart:ui';\n\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Editor AppLifeCycle tests', () {\n    testWidgets(\n      'Selection is added back after pausing AppFlowy',\n      (tester) async {\n        await tester.initializeAppFlowy();\n        await tester.tapAnonymousSignInButton();\n\n        final selection = Selection.single(path: [4], startOffset: 0);\n        await tester.editor.updateSelection(selection);\n\n        binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);\n        expect(tester.editor.getCurrentEditorState().selection, null);\n\n        binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);\n        await tester.pumpAndSettle();\n\n        expect(tester.editor.getCurrentEditorState().selection, selection);\n      },\n    );\n\n    testWidgets(\n      'Null selection is retained after pausing AppFlowy',\n      (tester) async {\n        await tester.initializeAppFlowy();\n        await tester.tapAnonymousSignInButton();\n\n        final selection = Selection.single(path: [4], startOffset: 0);\n        await tester.editor.updateSelection(selection);\n        await tester.editor.updateSelection(null);\n\n        binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);\n        expect(tester.editor.getCurrentEditorState().selection, null);\n\n        binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);\n        await tester.pumpAndSettle();\n\n        expect(tester.editor.getCurrentEditorState().selection, null);\n      },\n    );\n\n    testWidgets(\n      'Non-collapsed selection is retained after pausing AppFlowy',\n      (tester) async {\n        await tester.initializeAppFlowy();\n        await tester.tapAnonymousSignInButton();\n\n        final selection = Selection(\n          start: Position(path: [3]),\n          end: Position(path: [3], offset: 8),\n        );\n        await tester.editor.updateSelection(selection);\n\n        binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);\n        binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);\n        await tester.pumpAndSettle();\n\n        expect(tester.editor.getCurrentEditorState().selection, selection);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_block_option_test.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Block option interaction tests', () {\n    testWidgets('has correct block selection on tap option button',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // We edit the document by entering some characters, to ensure the document has focus\n      await tester.editor.updateSelection(\n        Selection.collapsed(Position(path: [2])),\n      );\n\n      // Insert character 'a' three times - easy to identify\n      await tester.ime.insertText('aaa');\n      await tester.pumpAndSettle();\n\n      final editorState = tester.editor.getCurrentEditorState();\n      final node = editorState.getNodeAtPath([2]);\n      expect(node?.delta?.toPlainText(), startsWith('aaa'));\n\n      final multiSelection = Selection(\n        start: Position(path: [2], offset: 3),\n        end: Position(path: [4], offset: 40),\n      );\n\n      // Select multiple items\n      await tester.editor.updateSelection(multiSelection);\n      await tester.pumpAndSettle();\n\n      // Press the block option menu\n      await tester.editor.hoverAndClickOptionMenuButton([2]);\n      await tester.pumpAndSettle();\n\n      // Expect the selection to be Block type and not have changed\n      expect(editorState.selectionType, SelectionType.block);\n      expect(editorState.selection, multiSelection);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_callout_test.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/base/icon/icon_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/emoji.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  setUpAll(() {\n    IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n    RecentIcons.enable = false;\n  });\n\n  tearDownAll(() {\n    RecentIcons.enable = true;\n  });\n\n  testWidgets('callout with emoji icon picker', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    final emojiIconData = await tester.loadIcon();\n\n    /// create a new document\n    await tester.createNewPageWithNameUnderParent();\n\n    /// tap the first line of the document\n    await tester.editor.tapLineOfEditorAt(0);\n\n    /// create callout\n    await tester.editor.showSlashMenu();\n    await tester.pumpAndSettle();\n    await tester.editor.tapSlashMenuItemWithName(\n      LocaleKeys.document_slashMenu_name_callout.tr(),\n    );\n\n    /// select an icon\n    final emojiPickerButton = find.descendant(\n      of: find.byType(CalloutBlockComponentWidget),\n      matching: find.byType(EmojiPickerButton),\n    );\n    await tester.tapButton(emojiPickerButton);\n    await tester.tapIcon(emojiIconData);\n\n    /// verification results\n    final iconData = IconsData.fromJson(jsonDecode(emojiIconData.emoji));\n    final iconWidget = find\n        .descendant(\n          of: emojiPickerButton,\n          matching: find.byType(IconWidget),\n        )\n        .evaluate()\n        .first\n        .widget as IconWidget;\n    final iconWidgetData = iconWidget.iconsData;\n    expect(iconWidgetData.svgString, iconData.svgString);\n    expect(iconWidgetData.iconName, iconData.iconName);\n    expect(iconWidgetData.groupName, iconData.groupName);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_codeblock_paste_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('paste in codeblock:', () {\n    testWidgets('paste multiple lines in codeblock', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent(name: 'Test Document');\n      // focus on the editor\n      await tester.tapButton(find.byType(AppFlowyEditor));\n\n      // mock the clipboard\n      const lines = 3;\n      final text = List.generate(lines, (index) => 'line $index').join('\\n');\n      AppFlowyClipboard.mockSetData(AppFlowyClipboardData(text: text));\n      ClipboardService.mockSetData(ClipboardServiceData(plainText: text));\n\n      await insertCodeBlockInDocument(tester);\n\n      // paste the text\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      final editorState = tester.editor.getCurrentEditorState();\n      expect(editorState.document.root.children.length, 1);\n      expect(\n        editorState.getNodeAtPath([0])!.delta!.toPlainText(),\n        text,\n      );\n    });\n  });\n}\n\n/// Inserts an codeBlock in the document\nFuture<void> insertCodeBlockInDocument(WidgetTester tester) async {\n  // open the actions menu and insert the codeBlock\n  await tester.editor.showSlashMenu();\n  await tester.editor.tapSlashMenuItemWithName(\n    LocaleKeys.document_slashMenu_name_code.tr(),\n    offset: 150,\n  );\n  // wait for the codeBlock to be inserted\n  await tester.pumpAndSettle();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('copy and paste in document:', () {\n    testWidgets('paste multiple lines at the first line', (tester) async {\n      // mock the clipboard\n      const lines = 3;\n      await tester.pasteContent(\n        plainText: List.generate(lines, (index) => 'line $index').join('\\n'),\n        (editorState) {\n          expect(editorState.document.root.children.length, 1);\n          final text =\n              editorState.document.root.children.first.delta!.toPlainText();\n          final textLines = text.split('\\n');\n          for (var i = 0; i < lines; i++) {\n            expect(\n              textLines[i],\n              'line $i',\n            );\n          }\n        },\n      );\n    });\n\n    // ## **User Installation**\n    // - [Windows/Mac/Linux](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/mac-windows-linux-packages)\n    // - [Docker](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/installing-with-docker)\n    // - [Source](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/from-source)\n    testWidgets('paste content from html, sample 1', (tester) async {\n      await tester.pasteContent(\n        html:\n            '''<meta charset='utf-8'><h2><strong>User Installation</strong></h2>\n<ul>\n<li><a href=\"https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/mac-windows-linux-packages\">Windows/Mac/Linux</a></li>\n<li><a href=\"https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/installing-with-docker\">Docker</a></li>\n<li><a href=\"https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/from-source\">Source</a></li>\n</ul>''',\n        (editorState) {\n          expect(editorState.document.root.children.length, 4);\n          final node1 = editorState.getNodeAtPath([0])!;\n          final node2 = editorState.getNodeAtPath([1])!;\n          final node3 = editorState.getNodeAtPath([2])!;\n          final node4 = editorState.getNodeAtPath([3])!;\n          expect(node1.delta!.toJson(), [\n            {\n              \"insert\": \"User Installation\",\n              \"attributes\": {\"bold\": true},\n            }\n          ]);\n          expect(node2.delta!.toJson(), [\n            {\n              \"insert\": \"Windows/Mac/Linux\",\n              \"attributes\": {\n                \"href\":\n                    \"https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/mac-windows-linux-packages\",\n              },\n            }\n          ]);\n          expect(\n            node3.delta!.toJson(),\n            [\n              {\n                \"insert\": \"Docker\",\n                \"attributes\": {\n                  \"href\":\n                      \"https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/installing-with-docker\",\n                },\n              }\n            ],\n          );\n          expect(\n            node4.delta!.toJson(),\n            [\n              {\n                \"insert\": \"Source\",\n                \"attributes\": {\n                  \"href\":\n                      \"https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/from-source\",\n                },\n              }\n            ],\n          );\n        },\n      );\n    });\n\n    testWidgets('paste code from VSCode', (tester) async {\n      await tester.pasteContent(\n          html:\n              '''<meta charset='utf-8'><div style=\"color: #bbbbbb;background-color: #262335;font-family: Consolas, 'JetBrains Mono', monospace, 'cascadia code', Menlo, Monaco, 'Courier New', monospace;font-weight: normal;font-size: 14px;line-height: 21px;white-space: pre;\"><div><span style=\"color: #fede5d;\">void</span><span style=\"color: #ff7edb;\"> </span><span style=\"color: #36f9f6;\">main</span><span style=\"color: #ff7edb;\">() {</span></div><div><span style=\"color: #ff7edb;\">  </span><span style=\"color: #36f9f6;\">runApp</span><span style=\"color: #ff7edb;\">(</span><span style=\"color: #fede5d;\">const</span><span style=\"color: #ff7edb;\"> </span><span style=\"color: #fe4450;\">MyApp</span><span style=\"color: #ff7edb;\">());</span></div><div><span style=\"color: #ff7edb;\">}</span></div></div>''',\n          (editorState) {\n        expect(editorState.document.root.children.length, 3);\n        final node1 = editorState.getNodeAtPath([0])!;\n        final node2 = editorState.getNodeAtPath([1])!;\n        final node3 = editorState.getNodeAtPath([2])!;\n        expect(node1.type, ParagraphBlockKeys.type);\n        expect(node2.type, ParagraphBlockKeys.type);\n        expect(node3.type, ParagraphBlockKeys.type);\n        expect(node1.delta!.toJson(), [\n          {'insert': 'void main() {'},\n        ]);\n        expect(node2.delta!.toJson(), [\n          {'insert': \"  runApp(const MyApp());\"},\n        ]);\n        expect(node3.delta!.toJson(), [\n          {\"insert\": \"}\"},\n        ]);\n      });\n    });\n\n    testWidgets('paste bulleted list in numbered list', (tester) async {\n      const inAppJson =\n          '{\"document\":{\"type\":\"page\",\"children\":[{\"type\":\"bulleted_list\",\"children\":[{\"type\":\"bulleted_list\",\"data\":{\"delta\":[{\"insert\":\"World\"}]}}],\"data\":{\"delta\":[{\"insert\":\"Hello\"}]}}]}}';\n\n      await tester.pasteContent(\n        inAppJson: inAppJson,\n        beforeTest: (editorState) async {\n          final transaction = editorState.transaction;\n          // Insert two numbered list nodes\n          // 1. Parent One\n          // 2.\n          transaction.insertNodes(\n            [0],\n            [\n              Node(\n                type: NumberedListBlockKeys.type,\n                attributes: {\n                  'delta': [\n                    {\"insert\": \"One\"},\n                  ],\n                },\n              ),\n              Node(\n                type: NumberedListBlockKeys.type,\n                attributes: {'delta': []},\n              ),\n            ],\n          );\n\n          // Set the selection to the second numbered list node (which has empty delta)\n          transaction.afterSelection = Selection.collapsed(Position(path: [1]));\n\n          await editorState.apply(transaction);\n          await tester.pumpAndSettle();\n        },\n        (editorState) {\n          final secondNode = editorState.getNodeAtPath([1]);\n          expect(secondNode?.delta?.toPlainText(), 'Hello');\n          expect(secondNode?.children.length, 1);\n\n          final childNode = secondNode?.children.first;\n          expect(childNode?.delta?.toPlainText(), 'World');\n          expect(childNode?.type, BulletedListBlockKeys.type);\n        },\n      );\n    });\n\n    testWidgets('paste text on part of bullet list', (tester) async {\n      const plainText = 'test';\n\n      await tester.pasteContent(\n        plainText: plainText,\n        beforeTest: (editorState) async {\n          final transaction = editorState.transaction;\n          transaction.insertNodes(\n            [0],\n            [\n              Node(\n                type: BulletedListBlockKeys.type,\n                attributes: {\n                  'delta': [\n                    {\"insert\": \"bullet list\"},\n                  ],\n                },\n              ),\n            ],\n          );\n\n          // Set the selection to the second numbered list node (which has empty delta)\n          transaction.afterSelection = Selection(\n            start: Position(path: [0], offset: 7),\n            end: Position(path: [0], offset: 11),\n          );\n\n          await editorState.apply(transaction);\n          await tester.pumpAndSettle();\n        },\n        (editorState) {\n          final node = editorState.getNodeAtPath([0]);\n          expect(node?.delta?.toPlainText(), 'bullet test');\n          expect(node?.type, BulletedListBlockKeys.type);\n        },\n      );\n    });\n\n    testWidgets('paste image(png) from memory', (tester) async {\n      final image = await rootBundle.load('assets/test/images/sample.png');\n      final bytes = image.buffer.asUint8List();\n      await tester.pasteContent(image: ('png', bytes), (editorState) {\n        expect(editorState.document.root.children.length, 1);\n        final node = editorState.getNodeAtPath([0])!;\n        expect(node.type, ImageBlockKeys.type);\n        expect(node.attributes[ImageBlockKeys.url], isNotNull);\n      });\n    });\n\n    testWidgets('paste image(jpeg) from memory', (tester) async {\n      final image = await rootBundle.load('assets/test/images/sample.jpeg');\n      final bytes = image.buffer.asUint8List();\n      await tester.pasteContent(image: ('jpeg', bytes), (editorState) {\n        expect(editorState.document.root.children.length, 1);\n        final node = editorState.getNodeAtPath([0])!;\n        expect(node.type, ImageBlockKeys.type);\n        expect(node.attributes[ImageBlockKeys.url], isNotNull);\n      });\n    });\n\n    testWidgets('paste image(gif) from memory', (tester) async {\n      final image = await rootBundle.load('assets/test/images/sample.gif');\n      final bytes = image.buffer.asUint8List();\n      await tester.pasteContent(image: ('gif', bytes), (editorState) {\n        expect(editorState.document.root.children.length, 1);\n        final node = editorState.getNodeAtPath([0])!;\n        expect(node.type, ImageBlockKeys.type);\n        expect(node.attributes[ImageBlockKeys.url], isNotNull);\n      });\n    });\n\n    testWidgets(\n      'format the selected text to href when pasting url if available',\n      (tester) async {\n        const text = 'appflowy';\n        const url = 'https://appflowy.io';\n        await tester.pasteContent(\n          plainText: url,\n          beforeTest: (editorState) async {\n            await tester.ime.insertText(text);\n            await tester.editor.updateSelection(\n              Selection.single(\n                path: [0],\n                startOffset: 0,\n                endOffset: text.length,\n              ),\n            );\n          },\n          (editorState) {\n            final node = editorState.getNodeAtPath([0])!;\n            expect(node.type, ParagraphBlockKeys.type);\n            expect(node.delta!.toJson(), [\n              {\n                'insert': text,\n                'attributes': {'href': url},\n              }\n            ]);\n          },\n        );\n      },\n    );\n\n    // https://github.com/AppFlowy-IO/AppFlowy/issues/3263\n    testWidgets(\n      'paste the image from clipboard when html and image are both available',\n      (tester) async {\n        const html =\n            '''<meta charset='utf-8'><img src=\"https://user-images.githubusercontent.com/9403740/262918875-603f4adb-58dd-49b5-8201-341d354935fd.png\" alt=\"image\"/>''';\n        final image = await rootBundle.load('assets/test/images/sample.png');\n        final bytes = image.buffer.asUint8List();\n        await tester.pasteContent(\n          html: html,\n          image: ('png', bytes),\n          (editorState) {\n            expect(editorState.document.root.children.length, 1);\n            final node = editorState.getNodeAtPath([0])!;\n            expect(node.type, ImageBlockKeys.type);\n          },\n        );\n      },\n    );\n\n    testWidgets('paste the html content contains section', (tester) async {\n      const html =\n          '''<meta charset='utf-8'><section style=\"margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;\"><span style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);\"><strong style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;\">AppFlowy</strong></span></section><section style=\"margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;\"><span style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);\"><strong style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;\">Hello World</strong></span></section>''';\n      await tester.pasteContent(html: html, (editorState) {\n        expect(editorState.document.root.children.length, 2);\n        final node1 = editorState.getNodeAtPath([0])!;\n        final node2 = editorState.getNodeAtPath([1])!;\n        expect(node1.type, ParagraphBlockKeys.type);\n        expect(node2.type, ParagraphBlockKeys.type);\n      });\n    });\n\n    testWidgets('paste the html from google translation', (tester) async {\n      const html =\n          '''<meta charset='utf-8'><section style=\"margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;\"><span style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);\"><strong style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;\"><font style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;\"><font style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;\">new force</font></font></strong></span></section><section style=\"margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;\"><span style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);\"><strong style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;\"><font style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;\"><font style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;\">Assessment focus: potential motivations, empathy</font></font></strong></span></section><section style=\"margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;\"><br style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;\"></section><section style=\"margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;\"><font style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;\"><font style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;\">➢Personality characteristics and potential motivations:</font></font></section><section style=\"margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;\"><font style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;\"><font style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;\">-Reflection of self-worth</font></font></section><section style=\"margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;\"><font style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;\"><font style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;\">-Need to be respected</font></font></section><section style=\"margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;\"><font style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;\"><font style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;\">-Have a unique definition of success</font></font></section><section style=\"margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, &quot;system-ui&quot;, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;\"><font style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;\"><font style=\"margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;\">-Be true to your own lifestyle</font></font></section>''';\n      await tester.pasteContent(html: html, (editorState) {\n        expect(editorState.document.root.children.length, 8);\n      });\n    });\n\n    testWidgets(\n      'auto convert url to link preview block',\n      (tester) async {\n        const url = 'https://appflowy.io';\n        await tester.pasteContent(plainText: url, (editorState) async {\n          final pasteAsMenu = find.byType(PasteAsMenu);\n          expect(pasteAsMenu, findsOneWidget);\n          final bookmarkButton = find.text(\n            LocaleKeys.document_plugins_linkPreview_typeSelection_bookmark.tr(),\n          );\n          await tester.tapButton(bookmarkButton);\n          // the second one is the paragraph node\n          expect(editorState.document.root.children.length, 1);\n          final node = editorState.getNodeAtPath([0])!;\n          expect(node.type, LinkPreviewBlockKeys.type);\n          expect(node.attributes[LinkPreviewBlockKeys.url], url);\n        });\n\n        // hover on the link preview block\n        // click the more button\n        // and select convert to link\n        await tester.hoverOnWidget(\n          find.byType(CustomLinkPreviewWidget),\n          onHover: () async {\n            /// show menu\n            final menu = find.byType(CustomLinkPreviewMenu);\n            expect(menu, findsOneWidget);\n            await tester.tapButton(menu);\n\n            final convertToLinkButton = find.text(\n              LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl\n                  .tr(),\n            );\n            expect(convertToLinkButton, findsOneWidget);\n            await tester.tapButton(convertToLinkButton);\n          },\n        );\n\n        final editorState = tester.editor.getCurrentEditorState();\n        final textNode = editorState.getNodeAtPath([0])!;\n        expect(textNode.type, ParagraphBlockKeys.type);\n        expect(textNode.delta!.toJson(), [\n          {\n            'insert': url,\n            'attributes': {'href': url},\n          }\n        ]);\n      },\n    );\n\n    testWidgets(\n      'ctrl/cmd+z to undo the auto convert url to link preview block',\n      (tester) async {\n        const url = 'https://appflowy.io';\n        await tester.pasteContent(plainText: url, (editorState) async {\n          final pasteAsMenu = find.byType(PasteAsMenu);\n          expect(pasteAsMenu, findsOneWidget);\n          final bookmarkButton = find.text(\n            LocaleKeys.document_plugins_linkPreview_typeSelection_bookmark.tr(),\n          );\n          await tester.tapButton(bookmarkButton);\n          // the second one is the paragraph node\n          expect(editorState.document.root.children.length, 1);\n          final node = editorState.getNodeAtPath([0])!;\n          expect(node.type, LinkPreviewBlockKeys.type);\n          expect(node.attributes[LinkPreviewBlockKeys.url], url);\n        });\n\n        await tester.simulateKeyEvent(\n          LogicalKeyboardKey.keyZ,\n          isControlPressed:\n              UniversalPlatform.isLinux || UniversalPlatform.isWindows,\n          isMetaPressed: UniversalPlatform.isMacOS,\n        );\n        await tester.pumpAndSettle();\n\n        final editorState = tester.editor.getCurrentEditorState();\n        final node = editorState.getNodeAtPath([0])!;\n        expect(node.type, ParagraphBlockKeys.type);\n        expect(node.delta!.toJson(), [\n          {\n            'insert': url,\n            'attributes': {'href': url},\n          }\n        ]);\n      },\n    );\n\n    testWidgets(\n      'paste the nodes start with non-delta node',\n      (tester) async {\n        await tester.pasteContent((_) {});\n        const text = 'Hello World';\n        final editorState = tester.editor.getCurrentEditorState();\n        final transaction = editorState.transaction;\n        // [image_block]\n        // [paragraph_block]\n        transaction.insertNodes([\n          0,\n        ], [\n          customImageNode(url: ''),\n          paragraphNode(text: text),\n        ]);\n        await editorState.apply(transaction);\n        await tester.pumpAndSettle();\n\n        await tester.editor.tapLineOfEditorAt(0);\n        // select all and copy\n        await tester.simulateKeyEvent(\n          LogicalKeyboardKey.keyA,\n          isControlPressed:\n              UniversalPlatform.isLinux || UniversalPlatform.isWindows,\n          isMetaPressed: UniversalPlatform.isMacOS,\n        );\n        await tester.simulateKeyEvent(\n          LogicalKeyboardKey.keyC,\n          isControlPressed:\n              UniversalPlatform.isLinux || UniversalPlatform.isWindows,\n          isMetaPressed: UniversalPlatform.isMacOS,\n        );\n\n        // put the cursor to the end of the paragraph block\n        await tester.editor.tapLineOfEditorAt(0);\n\n        // paste the content\n        await tester.simulateKeyEvent(\n          LogicalKeyboardKey.keyV,\n          isControlPressed:\n              UniversalPlatform.isLinux || UniversalPlatform.isWindows,\n          isMetaPressed: UniversalPlatform.isMacOS,\n        );\n        await tester.pumpAndSettle();\n\n        // expect the image and the paragraph block are inserted below the cursor\n        expect(editorState.getNodeAtPath([0])!.type, CustomImageBlockKeys.type);\n        expect(editorState.getNodeAtPath([1])!.type, ParagraphBlockKeys.type);\n        expect(editorState.getNodeAtPath([2])!.type, CustomImageBlockKeys.type);\n        expect(editorState.getNodeAtPath([3])!.type, ParagraphBlockKeys.type);\n      },\n    );\n\n    testWidgets('paste the url without protocol', (tester) async {\n      // paste the image that from local file\n      const plainText = '1.jpg';\n      final image = await rootBundle.load('assets/test/images/sample.jpeg');\n      final bytes = image.buffer.asUint8List();\n      await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),\n          (editorState) {\n        final node = editorState.getNodeAtPath([0])!;\n        expect(node.type, ImageBlockKeys.type);\n        expect(node.attributes[ImageBlockKeys.url], isNotEmpty);\n      });\n    });\n\n    testWidgets('paste the image url', (tester) async {\n      const plainText = 'http://example.com/1.jpg';\n      final image = await rootBundle.load('assets/test/images/sample.jpeg');\n      final bytes = image.buffer.asUint8List();\n      await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),\n          (editorState) {\n        final node = editorState.getNodeAtPath([0])!;\n        expect(node.type, ImageBlockKeys.type);\n        expect(node.attributes[ImageBlockKeys.url], isNotEmpty);\n      });\n    });\n\n    const testMarkdownText = '''\n# I'm h1\n## I'm h2\n### I'm h3\n#### I'm h4\n##### I'm h5\n###### I'm h6''';\n\n    testWidgets('paste markdowns', (tester) async {\n      await tester.pasteContent(\n        plainText: testMarkdownText,\n        (editorState) {\n          final children = editorState.document.root.children;\n          expect(children.length, 6);\n          for (int i = 1; i <= children.length; i++) {\n            final text = children[i - 1].delta!.toPlainText();\n            expect(text, 'I\\'m h$i');\n          }\n        },\n      );\n    });\n\n    testWidgets('paste markdowns as plain', (tester) async {\n      await tester.pasteContent(\n        plainText: testMarkdownText,\n        pasteAsPlain: true,\n        (editorState) {\n          final children = editorState.document.root.children;\n          expect(children.length, 6);\n          for (int i = 1; i <= children.length; i++) {\n            final text = children[i - 1].delta!.toPlainText();\n            final expectText = '${'#' * i} I\\'m h$i';\n            expect(text, expectText);\n          }\n        },\n      );\n    });\n  });\n}\n\nextension on WidgetTester {\n  Future<void> pasteContent(\n    FutureOr<void> Function(EditorState editorState) test, {\n    Future<void> Function(EditorState editorState)? beforeTest,\n    String? plainText,\n    String? html,\n    String? inAppJson,\n    bool pasteAsPlain = false,\n    (String, Uint8List?)? image,\n  }) async {\n    await initializeAppFlowy();\n    await tapAnonymousSignInButton();\n\n    // create a new document\n    await createNewPageWithNameUnderParent();\n    // tap the editor\n    await tapButton(find.byType(AppFlowyEditor));\n\n    await beforeTest?.call(editor.getCurrentEditorState());\n\n    // mock the clipboard\n    await getIt<ClipboardService>().setData(\n      ClipboardServiceData(\n        plainText: plainText,\n        html: html,\n        inAppJson: inAppJson,\n        image: image,\n      ),\n    );\n\n    // paste the text\n    await simulateKeyEvent(\n      LogicalKeyboardKey.keyV,\n      isControlPressed: Platform.isLinux || Platform.isWindows,\n      isShiftPressed: pasteAsPlain,\n      isMetaPressed: Platform.isMacOS,\n    );\n    await pumpAndSettle(const Duration(milliseconds: 1000));\n\n    await test(editor.getCurrentEditorState());\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_create_and_delete_test.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('create and delete the document:', () {\n    testWidgets('create a new document when launching app in first time',\n        (tester) async {\n      await tester.initializeAppFlowy();\n\n      await tester.tapAnonymousSignInButton();\n      final finder = find.text(gettingStarted, findRichText: true);\n      await tester.pumpUntilFound(finder, timeout: const Duration(seconds: 2));\n\n      // create a new document\n      const pageName = 'Test Document';\n      await tester.createNewPageWithNameUnderParent(name: pageName);\n\n      // expect to see a new document\n      tester.expectToSeePageName(pageName);\n      // and with one paragraph block\n      expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget);\n    });\n\n    testWidgets('delete the readme page and restore it', (tester) async {\n      await tester.initializeAppFlowy();\n\n      await tester.tapAnonymousSignInButton();\n\n      // delete the readme page\n      await tester.hoverOnPageName(\n        gettingStarted,\n        onHover: () async => tester.tapDeletePageButton(),\n      );\n\n      // the banner should show up and the readme page should be gone\n      tester.expectToSeeDocumentBanner();\n      tester.expectNotToSeePageName(gettingStarted);\n\n      // restore the readme page\n      await tester.tapRestoreButton();\n\n      // the banner should be gone and the readme page should be back\n      tester.expectNotToSeeDocumentBanner();\n      tester.expectToSeePageName(gettingStarted);\n    });\n\n    testWidgets('delete the readme page and delete it permanently',\n        (tester) async {\n      await tester.initializeAppFlowy();\n\n      await tester.tapAnonymousSignInButton();\n\n      // delete the readme page\n      await tester.hoverOnPageName(\n        gettingStarted,\n        onHover: () async => tester.tapDeletePageButton(),\n      );\n\n      // the banner should show up and the readme page should be gone\n      tester.expectToSeeDocumentBanner();\n      tester.expectNotToSeePageName(gettingStarted);\n\n      // delete the page permanently\n      await tester.tapDeletePermanentlyButton();\n\n      // the banner should be gone and the readme page should be gone\n      tester.expectNotToSeeDocumentBanner();\n      tester.expectNotToSeePageName(gettingStarted);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_customer_test.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('customer:', () {\n    testWidgets('backtick issue - inline code', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      const pageName = 'backtick issue';\n      await tester.createNewPageWithNameUnderParent(name: pageName);\n\n      // focus on the editor\n      await tester.tap(find.byType(AppFlowyEditor));\n      // input backtick\n      const text = '`Hello` AppFlowy';\n\n      for (var i = 0; i < text.length; i++) {\n        await tester.ime.insertCharacter(text[i]);\n      }\n\n      final node = tester.editor.getNodeAtPath([0]);\n      expect(\n        node.delta?.toJson(),\n        equals([\n          {\n            \"insert\": \"Hello\",\n            \"attributes\": {\"code\": true},\n          },\n          {\"insert\": \" AppFlowy\"},\n        ]),\n      );\n    });\n\n    testWidgets('backtick issue - inline code', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      const pageName = 'backtick issue';\n      await tester.createNewPageWithNameUnderParent(name: pageName);\n\n      // focus on the editor\n      await tester.tap(find.byType(AppFlowyEditor));\n      // input backtick\n      const text = '```';\n\n      for (var i = 0; i < text.length; i++) {\n        await tester.ime.insertCharacter(text[i]);\n      }\n\n      final node = tester.editor.getNodeAtPath([0]);\n      expect(node.type, equals(CodeBlockKeys.type));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_deletion_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';\nimport 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nimport 'document_inline_page_reference_test.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Document deletion', () {\n    testWidgets('Trash breadcrumb', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // This test shares behavior with the inline page reference test, thus\n      // we utilize the same helper functions there.\n      final name = await createDocumentToReference(tester);\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.pumpAndSettle();\n\n      await triggerReferenceDocumentBySlashMenu(tester);\n\n      // Search for prefix of document\n      await enterDocumentText(tester);\n\n      // Select result\n      final optionFinder = find.descendant(\n        of: find.byType(InlineActionsHandler),\n        matching: find.text(name),\n      );\n\n      await tester.tap(optionFinder);\n      await tester.pumpAndSettle();\n\n      final mentionBlock = find.byType(MentionPageBlock);\n      expect(mentionBlock, findsOneWidget);\n\n      // Delete the page\n      await tester.hoverOnPageName(\n        name,\n        onHover: () async => tester.tapDeletePageButton(),\n      );\n      await tester.pumpAndSettle();\n\n      // Navigate to the deleted page from the inline mention\n      await tester.tap(mentionBlock);\n      await tester.pumpUntilFound(find.byType(TrashBreadcrumb));\n\n      expect(find.byType(TrashBreadcrumb), findsOneWidget);\n\n      // Navigate using the trash breadcrumb\n      await tester.tap(\n        find.descendant(\n          of: find.byType(TrashBreadcrumb),\n          matching: find.text(\n            LocaleKeys.trash_text.tr(),\n          ),\n        ),\n      );\n      await tester.pumpUntilFound(find.text(LocaleKeys.trash_restoreAll.tr()));\n\n      // Restore all\n      await tester.tap(find.text(LocaleKeys.trash_restoreAll.tr()));\n      await tester.pumpAndSettle();\n      await tester.tap(find.text(LocaleKeys.trash_restore.tr()));\n      await tester.pumpAndSettle();\n\n      // Navigate back to the document\n      await tester.openPage('Getting started');\n      await tester.pumpAndSettle();\n\n      await tester.tap(mentionBlock);\n      await tester.pumpAndSettle();\n\n      expect(find.byType(TrashBreadcrumb), findsNothing);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_find_menu_test.dart",
    "content": "import 'dart:math';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  String generateRandomString(int len) {\n    final r = Random();\n    return String.fromCharCodes(\n      List.generate(len, (index) => r.nextInt(33) + 89),\n    );\n  }\n\n  testWidgets(\n    'document find menu test',\n    (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent();\n\n      // tap editor to get focus\n      await tester.tapButton(find.byType(AppFlowyEditor));\n\n      // set clipboard data\n      final data = [\n        \"123456\\n\\n\",\n        ...List.generate(100, (_) => \"${generateRandomString(50)}\\n\\n\"),\n        \"1234567\\n\\n\",\n        ...List.generate(100, (_) => \"${generateRandomString(50)}\\n\\n\"),\n        \"12345678\\n\\n\",\n        ...List.generate(100, (_) => \"${generateRandomString(50)}\\n\\n\"),\n      ].join();\n      await getIt<ClipboardService>().setData(\n        ClipboardServiceData(\n          plainText: data,\n        ),\n      );\n\n      // paste\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isControlPressed:\n            UniversalPlatform.isLinux || UniversalPlatform.isWindows,\n        isMetaPressed: UniversalPlatform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      // go back to beginning of document\n      // FIXME: Cannot run Ctrl+F unless selection is on screen\n      await tester.editor\n          .updateSelection(Selection.collapsed(Position(path: [0])));\n      await tester.pumpAndSettle();\n\n      expect(find.byType(FindAndReplaceMenuWidget), findsNothing);\n\n      // press cmd/ctrl+F to display the find menu\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyF,\n        isControlPressed:\n            UniversalPlatform.isLinux || UniversalPlatform.isWindows,\n        isMetaPressed: UniversalPlatform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(FindAndReplaceMenuWidget), findsOneWidget);\n\n      final textField = find.descendant(\n        of: find.byType(FindAndReplaceMenuWidget),\n        matching: find.byType(TextField),\n      );\n\n      await tester.enterText(\n        textField,\n        \"123456\",\n      );\n      await tester.pumpAndSettle();\n      await tester.pumpAndSettle();\n\n      expect(\n        find.descendant(\n          of: find.byType(AppFlowyEditor),\n          matching: find.text(\"123456\", findRichText: true),\n        ),\n        findsOneWidget,\n      );\n\n      await tester.testTextInput.receiveAction(TextInputAction.done);\n      await tester.pumpAndSettle();\n      await tester.pumpAndSettle();\n\n      expect(\n        find.descendant(\n          of: find.byType(AppFlowyEditor),\n          matching: find.text(\"1234567\", findRichText: true),\n        ),\n        findsOneWidget,\n      );\n\n      await tester.showKeyboard(textField);\n      await tester.idle();\n      await tester.testTextInput.receiveAction(TextInputAction.done);\n      await tester.pumpAndSettle();\n      await tester.pumpAndSettle();\n\n      expect(\n        find.descendant(\n          of: find.byType(AppFlowyEditor),\n          matching: find.text(\"12345678\", findRichText: true),\n        ),\n        findsOneWidget,\n      );\n\n      // tap next button, go back to beginning of document\n      await tester.tapButton(\n        find.descendant(\n          of: find.byType(FindMenu),\n          matching: find.byFlowySvg(FlowySvgs.arrow_down_s),\n        ),\n      );\n\n      expect(\n        find.descendant(\n          of: find.byType(AppFlowyEditor),\n          matching: find.text(\"123456\", findRichText: true),\n        ),\n        findsOneWidget,\n      );\n\n      /// press cmd/ctrl+F to display the find menu\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyF,\n        isControlPressed:\n            UniversalPlatform.isLinux || UniversalPlatform.isWindows,\n        isMetaPressed: UniversalPlatform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(FindAndReplaceMenuWidget), findsOneWidget);\n\n      /// press esc to dismiss the find menu\n      await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n      await tester.pumpAndSettle();\n      expect(find.byType(FindAndReplaceMenuWidget), findsNothing);\n    },\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_inline_page_reference_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/keyboard.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('insert inline document reference', () {\n    testWidgets('insert by slash menu', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      final name = await createDocumentToReference(tester);\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.pumpAndSettle();\n\n      await triggerReferenceDocumentBySlashMenu(tester);\n\n      // Search for prefix of document\n      await enterDocumentText(tester);\n\n      // Select result\n      final optionFinder = find.descendant(\n        of: find.byType(InlineActionsHandler),\n        matching: find.text(name),\n      );\n\n      await tester.tap(optionFinder);\n      await tester.pumpAndSettle();\n\n      final mentionBlock = find.byType(MentionPageBlock);\n      expect(mentionBlock, findsOneWidget);\n    });\n\n    testWidgets('insert by `[[` character shortcut', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      final name = await createDocumentToReference(tester);\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.pumpAndSettle();\n\n      await tester.ime.insertText('[[');\n      await tester.pumpAndSettle();\n\n      // Select result\n      await tester.editor.tapAtMenuItemWithName(name);\n      await tester.pumpAndSettle();\n\n      final mentionBlock = find.byType(MentionPageBlock);\n      expect(mentionBlock, findsOneWidget);\n    });\n\n    testWidgets('insert by `+` character shortcut', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      final name = await createDocumentToReference(tester);\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.pumpAndSettle();\n\n      await tester.ime.insertText('+');\n      await tester.pumpAndSettle();\n\n      // Select result\n      await tester.editor.tapAtMenuItemWithName(name);\n      await tester.pumpAndSettle();\n\n      final mentionBlock = find.byType(MentionPageBlock);\n      expect(mentionBlock, findsOneWidget);\n    });\n  });\n}\n\nFuture<String> createDocumentToReference(WidgetTester tester) async {\n  final name = 'document_${uuid()}';\n\n  await tester.createNewPageWithNameUnderParent(\n    name: name,\n    openAfterCreated: false,\n  );\n\n  // This is a workaround since the openAfterCreated\n  //  option does not work in createNewPageWithName method\n  await tester.tap(find.byType(SingleInnerViewItem).first);\n  await tester.pumpAndSettle();\n\n  return name;\n}\n\nFuture<void> triggerReferenceDocumentBySlashMenu(WidgetTester tester) async {\n  await tester.editor.showSlashMenu();\n  await tester.pumpAndSettle();\n\n  // Search for referenced document action\n  await enterDocumentText(tester);\n\n  // Select item\n  await FlowyTestKeyboard.simulateKeyDownEvent(\n    [\n      LogicalKeyboardKey.enter,\n    ],\n    tester: tester,\n    withKeyUp: true,\n  );\n\n  await tester.pumpAndSettle();\n}\n\nFuture<void> enterDocumentText(WidgetTester tester) async {\n  await FlowyTestKeyboard.simulateKeyDownEvent(\n    [\n      LogicalKeyboardKey.keyD,\n      LogicalKeyboardKey.keyO,\n      LogicalKeyboardKey.keyC,\n      LogicalKeyboardKey.keyU,\n      LogicalKeyboardKey.keyM,\n      LogicalKeyboardKey.keyE,\n      LogicalKeyboardKey.keyN,\n      LogicalKeyboardKey.keyT,\n    ],\n    tester: tester,\n    withKeyUp: true,\n  );\n  await tester.pumpAndSettle();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_inline_sub_page_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/keyboard.dart';\nimport '../../shared/util.dart';\n\nconst _firstDocName = \"Inline Sub Page Mention\";\nconst _createdPageName = \"hi world\";\n\n// Test cases that are covered in this file:\n// - [x] Insert sub page mention from action menu (+)\n// - [x] Delete sub page mention from editor\n// - [x] Delete page from sidebar\n// - [x] Delete page from sidebar and then trash\n// - [x] Undo delete sub page mention\n// - [x] Cut+paste in same document\n// - [x] Cut+paste in different document\n// - [x] Cut+paste in same document and then paste again in same document\n// - [x] Turn paragraph with sub page mention into a heading\n// - [x] Turn heading with sub page mention into a paragraph\n// - [x] Duplicate a Block containing two sub page mentions\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('document inline sub-page mention tests:', () {\n    testWidgets('Insert (& delete) a sub page mention from action menu',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);\n\n      await tester.insertInlineSubPageFromPlusMenu();\n\n      await tester.expandOrCollapsePage(\n        pageName: _firstDocName,\n        layout: ViewLayoutPB.Document,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsNWidgets(2));\n      expect(find.byType(MentionSubPageBlock), findsOneWidget);\n\n      // Delete from editor\n      await tester.editor.updateSelection(\n        Selection.collapsed(Position(path: [0], offset: 1)),\n      );\n\n      await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsNothing);\n      expect(find.byType(MentionSubPageBlock), findsNothing);\n\n      // Undo\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyZ,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsNWidgets(2));\n      expect(find.byType(MentionSubPageBlock), findsOneWidget);\n\n      // Move to trash (delete from sidebar)\n      await tester.rightClickOnPageName(_createdPageName);\n      await tester.tapButtonWithName(ViewMoreActionType.delete.name);\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsOneWidget);\n      expect(find.byType(MentionSubPageBlock), findsOneWidget);\n      expect(\n        find.text(LocaleKeys.document_mention_trashHint.tr()),\n        findsOneWidget,\n      );\n\n      // Delete from trash\n      await tester.tapTrashButton();\n      await tester.pumpAndSettle();\n\n      await tester.tap(find.text(LocaleKeys.trash_deleteAll.tr()));\n      await tester.pumpAndSettle();\n\n      await tester.tap(find.text(LocaleKeys.button_delete.tr()));\n      await tester.pumpAndSettle();\n\n      await tester.openPage(_firstDocName);\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsNothing);\n      expect(find.byType(MentionSubPageBlock), findsOneWidget);\n      expect(\n        find.text(LocaleKeys.document_mention_deletedPage.tr()),\n        findsOneWidget,\n      );\n    });\n\n    testWidgets(\n        'Cut+paste in same document and cut+paste in different document',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);\n\n      await tester.insertInlineSubPageFromPlusMenu();\n\n      await tester.expandOrCollapsePage(\n        pageName: _firstDocName,\n        layout: ViewLayoutPB.Document,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsNWidgets(2));\n      expect(find.byType(MentionSubPageBlock), findsOneWidget);\n\n      // Cut from editor\n      await tester.editor.updateSelection(\n        Selection.collapsed(Position(path: [0], offset: 1)),\n      );\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyX,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsNothing);\n      expect(find.byType(MentionSubPageBlock), findsNothing);\n\n      // Paste in same document\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsNWidgets(2));\n      expect(find.byType(MentionSubPageBlock), findsOneWidget);\n\n      // Cut again\n      await tester.editor.updateSelection(\n        Selection.collapsed(Position(path: [0], offset: 1)),\n      );\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyX,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      // Create another document\n      const anotherDocName = \"Another Document\";\n      await tester.createOpenRenameDocumentUnderParent(\n        name: anotherDocName,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsNothing);\n      expect(find.byType(MentionSubPageBlock), findsNothing);\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.pumpAndSettle();\n\n      // Paste in document\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpUntilFound(find.byType(MentionSubPageBlock));\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsOneWidget);\n\n      await tester.expandOrCollapsePage(\n        pageName: anotherDocName,\n        layout: ViewLayoutPB.Document,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsNWidgets(2));\n    });\n    testWidgets(\n        'Cut+paste in same docuemnt and then paste again in same document',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);\n\n      await tester.insertInlineSubPageFromPlusMenu();\n\n      await tester.expandOrCollapsePage(\n        pageName: _firstDocName,\n        layout: ViewLayoutPB.Document,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsNWidgets(2));\n      expect(find.byType(MentionSubPageBlock), findsOneWidget);\n\n      // Cut from editor\n      await tester.editor.updateSelection(\n        Selection.collapsed(Position(path: [0], offset: 1)),\n      );\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyX,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsNothing);\n      expect(find.byType(MentionSubPageBlock), findsNothing);\n\n      // Paste in same document\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsNWidgets(2));\n      expect(find.byType(MentionSubPageBlock), findsOneWidget);\n\n      // Paste again\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle(const Duration(seconds: 2));\n\n      expect(find.text(_createdPageName), findsNWidgets(2));\n      expect(find.byType(MentionSubPageBlock), findsNWidgets(2));\n      expect(find.text('$_createdPageName (copy)'), findsNWidgets(2));\n    });\n\n    testWidgets('Turn into w/ sub page mentions', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);\n\n      await tester.insertInlineSubPageFromPlusMenu();\n\n      await tester.expandOrCollapsePage(\n        pageName: _firstDocName,\n        layout: ViewLayoutPB.Document,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsNWidgets(2));\n      expect(find.byType(MentionSubPageBlock), findsOneWidget);\n\n      final headingText = LocaleKeys.document_slashMenu_name_heading1.tr();\n      final paragraphText = LocaleKeys.document_slashMenu_name_text.tr();\n\n      // Turn into heading\n      await tester.editor.openTurnIntoMenu([0]);\n      await tester.tapButton(find.findTextInFlowyText(headingText));\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsNWidgets(2));\n      expect(find.byType(MentionSubPageBlock), findsOneWidget);\n\n      // Turn into paragraph\n      await tester.editor.openTurnIntoMenu([0]);\n      await tester.tapButton(find.findTextInFlowyText(paragraphText));\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsNWidgets(2));\n      expect(find.byType(MentionSubPageBlock), findsOneWidget);\n    });\n\n    testWidgets('Duplicate a block containing two sub page mentions',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);\n\n      await tester.insertInlineSubPageFromPlusMenu();\n\n      // Copy paste it\n      await tester.editor.updateSelection(\n        Selection(\n          start: Position(path: [0]),\n          end: Position(path: [0], offset: 1),\n        ),\n      );\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyC,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      await tester.editor.updateSelection(\n        Selection.collapsed(Position(path: [0], offset: 1)),\n      );\n\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsOneWidget);\n      expect(find.text(\"$_createdPageName (copy)\"), findsOneWidget);\n      expect(find.byType(MentionSubPageBlock), findsNWidgets(2));\n\n      // Duplicate node from block action menu\n      await tester.editor.hoverAndClickOptionMenuButton([0]);\n      await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr());\n      await tester.pumpAndSettle();\n\n      expect(find.text(_createdPageName), findsOneWidget);\n      expect(find.text(\"$_createdPageName (copy)\"), findsNWidgets(2));\n      expect(find.text(\"$_createdPageName (copy) (copy)\"), findsOneWidget);\n    });\n\n    testWidgets('Cancel inline page reference menu by space', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createOpenRenameDocumentUnderParent(name: _firstDocName);\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.showPlusMenu();\n\n      // Cancel by space\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.space,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(InlineActionsMenu), findsNothing);\n    });\n  });\n}\n\nextension _InlineSubPageTestHelper on WidgetTester {\n  Future<void> insertInlineSubPageFromPlusMenu() async {\n    await editor.tapLineOfEditorAt(0);\n\n    await editor.showPlusMenu();\n\n    // Workaround to allow typing a document name\n    await FlowyTestKeyboard.simulateKeyDownEvent(\n      tester: this,\n      withKeyUp: true,\n      [\n        LogicalKeyboardKey.keyH,\n        LogicalKeyboardKey.keyI,\n        LogicalKeyboardKey.space,\n        LogicalKeyboardKey.keyW,\n        LogicalKeyboardKey.keyO,\n        LogicalKeyboardKey.keyR,\n        LogicalKeyboardKey.keyL,\n        LogicalKeyboardKey.keyD,\n      ],\n    );\n\n    await FlowyTestKeyboard.simulateKeyDownEvent(\n      tester: this,\n      withKeyUp: true,\n      [LogicalKeyboardKey.enter],\n    );\n    await pumpUntilFound(find.byType(MentionSubPageBlock));\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_link_preview_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  const avaliableLink = 'https://appflowy.io/',\n      unavailableLink = 'www.thereIsNoting.com';\n\n  Future<void> preparePage(WidgetTester tester, {String? pageName}) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    await tester.createNewPageWithNameUnderParent(name: pageName);\n    await tester.editor.tapLineOfEditorAt(0);\n  }\n\n  Future<void> pasteLink(WidgetTester tester, String link) async {\n    await getIt<ClipboardService>()\n        .setData(ClipboardServiceData(plainText: link));\n\n    /// paste the link\n    await tester.simulateKeyEvent(\n      LogicalKeyboardKey.keyV,\n      isControlPressed: Platform.isLinux || Platform.isWindows,\n      isMetaPressed: Platform.isMacOS,\n    );\n    await tester.pumpAndSettle(Duration(seconds: 1));\n  }\n\n  Future<void> pasteAs(\n    WidgetTester tester,\n    String link,\n    PasteMenuType type, {\n    Duration waitTime = const Duration(milliseconds: 500),\n  }) async {\n    await pasteLink(tester, link);\n    final convertToMentionButton = find.text(type.title);\n    await tester.tapButton(convertToMentionButton);\n    await tester.pumpAndSettle(waitTime);\n  }\n\n  void checkUrl(Node node, String link) {\n    expect(node.type, ParagraphBlockKeys.type);\n    expect(node.delta!.toJson(), [\n      {\n        'insert': link,\n        'attributes': {'href': link},\n      }\n    ]);\n  }\n\n  void checkMention(Node node, String link) {\n    final delta = node.delta!;\n    final insert = (delta.first as TextInsert).text;\n    final attributes = delta.first.attributes;\n    expect(insert, MentionBlockKeys.mentionChar);\n    final mention =\n        attributes?[MentionBlockKeys.mention] as Map<String, dynamic>;\n    expect(mention[MentionBlockKeys.type], MentionType.externalLink.name);\n    expect(mention[MentionBlockKeys.url], avaliableLink);\n  }\n\n  void checkBookmark(Node node, String link) {\n    expect(node.type, LinkPreviewBlockKeys.type);\n    expect(node.attributes[LinkPreviewBlockKeys.url], link);\n  }\n\n  void checkEmbed(Node node, String link) {\n    expect(node.type, LinkPreviewBlockKeys.type);\n    expect(node.attributes[LinkEmbedKeys.previewType], LinkEmbedKeys.embed);\n    expect(node.attributes[LinkPreviewBlockKeys.url], link);\n  }\n\n  group('Paste as URL', () {\n    Future<void> pasteAndTurnInto(\n      WidgetTester tester,\n      String link,\n      String title,\n    ) async {\n      await pasteLink(tester, link);\n      final convertToLinkButton = find\n          .text(LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr());\n      await tester.tapButton(convertToLinkButton);\n\n      /// hover link and turn into mention\n      await tester.hoverOnWidget(\n        find.byType(LinkHoverTrigger),\n        onHover: () async {\n          final turnintoButton = find.byFlowySvg(FlowySvgs.turninto_m);\n          await tester.tapButton(turnintoButton);\n          final convertToButton = find.text(title);\n          await tester.tapButton(convertToButton);\n          await tester.pumpAndSettle(Duration(seconds: 1));\n        },\n      );\n    }\n\n    testWidgets('paste a link', (tester) async {\n      final link = avaliableLink;\n      await preparePage(tester);\n      await pasteLink(tester, link);\n      final convertToLinkButton = find\n          .text(LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr());\n      await tester.tapButton(convertToLinkButton);\n      final node = tester.editor.getNodeAtPath([0]);\n      checkUrl(node, link);\n    });\n\n    testWidgets('paste a link and turn into mention', (tester) async {\n      final link = avaliableLink;\n      await preparePage(tester);\n      await pasteAndTurnInto(\n        tester,\n        link,\n        LinkConvertMenuCommand.toMention.title,\n      );\n\n      /// check metion values\n      final node = tester.editor.getNodeAtPath([0]);\n      checkMention(node, link);\n    });\n\n    testWidgets('paste a link and turn into bookmark', (tester) async {\n      final link = avaliableLink;\n      await preparePage(tester);\n      await pasteAndTurnInto(\n        tester,\n        link,\n        LinkConvertMenuCommand.toBookmark.title,\n      );\n\n      /// check metion values\n      final node = tester.editor.getNodeAtPath([0]);\n      checkBookmark(node, link);\n    });\n\n    testWidgets('paste a link and turn into embed', (tester) async {\n      final link = avaliableLink;\n      await preparePage(tester);\n      await pasteAndTurnInto(\n        tester,\n        link,\n        LinkConvertMenuCommand.toEmbed.title,\n      );\n\n      /// check metion values\n      final node = tester.editor.getNodeAtPath([0]);\n      checkEmbed(node, link);\n    });\n  });\n\n  group('Paste as Mention', () {\n    Future<void> pasteAsMention(WidgetTester tester, String link) =>\n        pasteAs(tester, link, PasteMenuType.mention);\n\n    String getMentionLink(Node node) {\n      final insert = node.delta?.first as TextInsert?;\n      final mention = insert?.attributes?[MentionBlockKeys.mention]\n          as Map<String, dynamic>?;\n      return mention?[MentionBlockKeys.url] ?? '';\n    }\n\n    Future<void> hoverMentionAndClick(\n      WidgetTester tester,\n      String command,\n    ) async {\n      final mentionLink = find.byType(MentionLinkBlock);\n      expect(mentionLink, findsOneWidget);\n      await tester.hoverOnWidget(\n        mentionLink,\n        onHover: () async {\n          final errorPreview = find.byType(MentionLinkErrorPreview);\n          expect(errorPreview, findsOneWidget);\n          final convertButton = find.byFlowySvg(FlowySvgs.turninto_m);\n          await tester.tapButton(convertButton);\n          final menuButton = find.text(command);\n          await tester.tapButton(menuButton);\n        },\n      );\n    }\n\n    testWidgets('paste a link as mention', (tester) async {\n      final link = avaliableLink;\n      await preparePage(tester);\n      await pasteAsMention(tester, link);\n      final node = tester.editor.getNodeAtPath([0]);\n      checkMention(node, link);\n    });\n\n    testWidgets('paste as mention and copy link', (tester) async {\n      final link = avaliableLink;\n      await preparePage(tester);\n      await pasteAsMention(tester, link);\n      final mentionLink = find.byType(MentionLinkBlock);\n      expect(mentionLink, findsOneWidget);\n      await tester.hoverOnWidget(\n        mentionLink,\n        onHover: () async {\n          final preview = find.byType(MentionLinkPreview);\n          if (!preview.hasFound) {\n            final copyButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);\n            await tester.tapButton(copyButton);\n          } else {\n            final moreOptionButton = find.byFlowySvg(FlowySvgs.toolbar_more_m);\n            await tester.tapButton(moreOptionButton);\n            final copyButton =\n                find.text(MentionLinktMenuCommand.copyLink.title);\n            await tester.tapButton(copyButton);\n          }\n        },\n      );\n      final clipboardContent = await getIt<ClipboardService>().getData();\n      expect(clipboardContent.plainText, link);\n    });\n\n    testWidgets('paste as error mention and turninto url', (tester) async {\n      String link = unavailableLink;\n      await preparePage(tester);\n      await pasteAsMention(tester, link);\n      Node node = tester.editor.getNodeAtPath([0]);\n      link = getMentionLink(node);\n      await hoverMentionAndClick(\n        tester,\n        MentionLinktErrorMenuCommand.toURL.title,\n      );\n      node = tester.editor.getNodeAtPath([0]);\n      checkUrl(node, link);\n    });\n\n    testWidgets('paste as error mention and turninto embed', (tester) async {\n      String link = unavailableLink;\n      await preparePage(tester);\n      await pasteAsMention(tester, link);\n      Node node = tester.editor.getNodeAtPath([0]);\n      link = getMentionLink(node);\n      await hoverMentionAndClick(\n        tester,\n        MentionLinktErrorMenuCommand.toEmbed.title,\n      );\n      node = tester.editor.getNodeAtPath([0]);\n      checkEmbed(node, link);\n    });\n\n    testWidgets('paste as error mention and turninto bookmark', (tester) async {\n      String link = unavailableLink;\n      await preparePage(tester);\n      await pasteAsMention(tester, link);\n      Node node = tester.editor.getNodeAtPath([0]);\n      link = getMentionLink(node);\n      await hoverMentionAndClick(\n        tester,\n        MentionLinktErrorMenuCommand.toBookmark.title,\n      );\n      node = tester.editor.getNodeAtPath([0]);\n      checkBookmark(node, link);\n    });\n\n    testWidgets('paste as error mention and remove link', (tester) async {\n      String link = unavailableLink;\n      await preparePage(tester);\n      await pasteAsMention(tester, link);\n      Node node = tester.editor.getNodeAtPath([0]);\n      link = getMentionLink(node);\n      await hoverMentionAndClick(\n        tester,\n        MentionLinktErrorMenuCommand.removeLink.title,\n      );\n      node = tester.editor.getNodeAtPath([0]);\n      expect(node.type, ParagraphBlockKeys.type);\n      expect(node.delta!.toJson(), [\n        {'insert': link},\n      ]);\n    });\n  });\n\n  group('Paste as Bookmark', () {\n    Future<void> pasteAsBookmark(WidgetTester tester, String link) =>\n        pasteAs(tester, link, PasteMenuType.bookmark);\n\n    Future<void> hoverAndClick(\n      WidgetTester tester,\n      LinkPreviewMenuCommand command,\n    ) async {\n      final bookmark = find.byType(CustomLinkPreviewBlockComponent);\n      expect(bookmark, findsOneWidget);\n      await tester.hoverOnWidget(\n        bookmark,\n        onHover: () async {\n          final menuButton = find.byFlowySvg(FlowySvgs.toolbar_more_m);\n          await tester.tapButton(menuButton);\n          final commandButton = find.text(command.title);\n          await tester.tapButton(commandButton);\n        },\n      );\n    }\n\n    testWidgets('paste a link as bookmark', (tester) async {\n      final link = avaliableLink;\n      await preparePage(tester);\n      await pasteAsBookmark(tester, link);\n      final node = tester.editor.getNodeAtPath([0]);\n      checkBookmark(node, link);\n    });\n\n    testWidgets('paste a link as bookmark and convert to mention',\n        (tester) async {\n      final link = avaliableLink;\n      await preparePage(tester);\n      await pasteAsBookmark(tester, link);\n      await hoverAndClick(tester, LinkPreviewMenuCommand.convertToMention);\n      final node = tester.editor.getNodeAtPath([0]);\n      checkMention(node, link);\n    });\n\n    testWidgets('paste a link as bookmark and convert to url', (tester) async {\n      final link = avaliableLink;\n      await preparePage(tester);\n      await pasteAsBookmark(tester, link);\n      await hoverAndClick(tester, LinkPreviewMenuCommand.convertToUrl);\n      final node = tester.editor.getNodeAtPath([0]);\n      checkUrl(node, link);\n    });\n\n    testWidgets('paste a link as bookmark and convert to embed',\n        (tester) async {\n      final link = avaliableLink;\n      await preparePage(tester);\n      await pasteAsBookmark(tester, link);\n      await hoverAndClick(tester, LinkPreviewMenuCommand.convertToEmbed);\n      final node = tester.editor.getNodeAtPath([0]);\n      checkEmbed(node, link);\n    });\n\n    testWidgets('paste a link as bookmark and copy link', (tester) async {\n      final link = avaliableLink;\n      await preparePage(tester);\n      await pasteAsBookmark(tester, link);\n      await hoverAndClick(tester, LinkPreviewMenuCommand.copyLink);\n      final clipboardContent = await getIt<ClipboardService>().getData();\n      expect(clipboardContent.plainText, link);\n    });\n\n    testWidgets('paste a link as bookmark and replace link', (tester) async {\n      final link = avaliableLink;\n      await preparePage(tester);\n      await pasteAsBookmark(tester, link);\n      await hoverAndClick(tester, LinkPreviewMenuCommand.replace);\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyA,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.simulateKeyEvent(LogicalKeyboardKey.delete);\n      await tester.enterText(find.byType(TextFormField), unavailableLink);\n      await tester.tapButton(find.text(LocaleKeys.button_replace.tr()));\n      final node = tester.editor.getNodeAtPath([0]);\n      checkBookmark(node, unavailableLink);\n    });\n\n    testWidgets('paste a link as bookmark and remove link', (tester) async {\n      final link = avaliableLink;\n      await preparePage(tester);\n      await pasteAsBookmark(tester, link);\n      await hoverAndClick(tester, LinkPreviewMenuCommand.removeLink);\n      final node = tester.editor.getNodeAtPath([0]);\n      expect(node.type, ParagraphBlockKeys.type);\n      expect(node.delta!.toJson(), [\n        {'insert': link},\n      ]);\n    });\n  });\n  group('Paste as Embed', () {\n    Future<void> pasteAsEmbed(WidgetTester tester, String link) =>\n        pasteAs(tester, link, PasteMenuType.embed);\n\n    Future<void> hoverAndConvert(\n      WidgetTester tester,\n      LinkEmbedConvertCommand command,\n    ) async {\n      final embed = find.byType(LinkEmbedBlockComponent);\n      expect(embed, findsOneWidget);\n      await tester.hoverOnWidget(\n        embed,\n        onHover: () async {\n          final menuButton = find.byFlowySvg(FlowySvgs.turninto_m);\n          await tester.tapButton(menuButton);\n          final commandButton = find.text(command.title);\n          await tester.tapButton(commandButton);\n        },\n      );\n    }\n\n    testWidgets('paste a link as embed', (tester) async {\n      final link = avaliableLink;\n      await preparePage(tester);\n      await pasteAsEmbed(tester, link);\n      final node = tester.editor.getNodeAtPath([0]);\n      checkEmbed(node, link);\n    });\n\n    testWidgets('paste a link as bookmark and convert to mention',\n        (tester) async {\n      final link = avaliableLink;\n      await preparePage(tester);\n      await pasteAsEmbed(tester, link);\n      await hoverAndConvert(tester, LinkEmbedConvertCommand.toMention);\n      final node = tester.editor.getNodeAtPath([0]);\n      checkMention(node, link);\n    });\n\n    testWidgets('paste a link as bookmark and convert to url', (tester) async {\n      final link = avaliableLink;\n      await preparePage(tester);\n      await pasteAsEmbed(tester, link);\n      await hoverAndConvert(tester, LinkEmbedConvertCommand.toURL);\n      final node = tester.editor.getNodeAtPath([0]);\n      checkUrl(node, link);\n    });\n\n    testWidgets('paste a link as bookmark and convert to bookmark',\n        (tester) async {\n      final link = avaliableLink;\n      await preparePage(tester);\n      await pasteAsEmbed(tester, link);\n      await hoverAndConvert(tester, LinkEmbedConvertCommand.toBookmark);\n      final node = tester.editor.getNodeAtPath([0]);\n      checkBookmark(node, link);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('MoreViewActions', () {\n    testWidgets('can duplicate and delete from menu', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.pumpAndSettle();\n\n      final pageFinder = find.byType(ViewItem);\n      expect(pageFinder, findsNWidgets(1));\n\n      // Duplicate\n      await tester.openMoreViewActions();\n      await tester.duplicateByMoreViewActions();\n      await tester.pumpAndSettle();\n\n      expect(pageFinder, findsNWidgets(2));\n\n      // Delete\n      await tester.openMoreViewActions();\n      await tester.deleteByMoreViewActions();\n      await tester.pumpAndSettle();\n\n      expect(pageFinder, findsNWidgets(1));\n    });\n  });\n\n  testWidgets('count title towards word count', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    await tester.createNewPageWithNameUnderParent();\n\n    Finder title = tester.editor.findDocumentTitle('');\n\n    await tester.openMoreViewActions();\n    final viewMetaInfo = find.byType(ViewMetaInfo);\n    expect(viewMetaInfo, findsOneWidget);\n\n    ViewMetaInfo viewMetaInfoWidget =\n        viewMetaInfo.evaluate().first.widget as ViewMetaInfo;\n    Counters titleCounter = viewMetaInfoWidget.titleCounters!;\n\n    expect(titleCounter.charCount, 0);\n    expect(titleCounter.wordCount, 0);\n\n    /// input [str1] within title\n    const str1 = 'Hello',\n        str2 = '$str1 AppFlowy',\n        str3 = '$str2!',\n        str4 = 'Hello world';\n    await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n    await tester.tapButton(title);\n    await tester.enterText(title, str1);\n    await tester.pumpAndSettle(const Duration(seconds: 1));\n    await tester.openMoreViewActions();\n    viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;\n    titleCounter = viewMetaInfoWidget.titleCounters!;\n    expect(titleCounter.charCount, str1.length);\n    expect(titleCounter.wordCount, 1);\n\n    /// input [str2] within title\n    title = tester.editor.findDocumentTitle(str1);\n    await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n    await tester.tapButton(title);\n    await tester.enterText(title, str2);\n    await tester.pumpAndSettle(const Duration(seconds: 1));\n    await tester.openMoreViewActions();\n    viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;\n    titleCounter = viewMetaInfoWidget.titleCounters!;\n    expect(titleCounter.charCount, str2.length);\n    expect(titleCounter.wordCount, 2);\n\n    /// input [str3] within title\n    title = tester.editor.findDocumentTitle(str2);\n    await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n    await tester.tapButton(title);\n    await tester.enterText(title, str3);\n    await tester.pumpAndSettle(const Duration(seconds: 1));\n    await tester.openMoreViewActions();\n    viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;\n    titleCounter = viewMetaInfoWidget.titleCounters!;\n    expect(titleCounter.charCount, str3.length);\n    expect(titleCounter.wordCount, 2);\n\n    /// input [str4] within document\n    await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n    await tester.editor\n        .updateSelection(Selection.collapsed(Position(path: [0])));\n    await tester.pumpAndSettle();\n    await tester.editor\n        .getCurrentEditorState()\n        .insertTextAtCurrentSelection(str4);\n    await tester.pumpAndSettle(const Duration(seconds: 1));\n    await tester.openMoreViewActions();\n    final texts =\n        find.descendant(of: viewMetaInfo, matching: find.byType(FlowyText));\n    expect(texts, findsNWidgets(3));\n    viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;\n    titleCounter = viewMetaInfoWidget.titleCounters!;\n    final Counters documentCounters = viewMetaInfoWidget.documentCounters!;\n    final wordCounter = texts.evaluate().elementAt(0).widget as FlowyText,\n        charCounter = texts.evaluate().elementAt(1).widget as FlowyText;\n    final numberFormat = NumberFormat();\n    expect(\n      wordCounter.text,\n      LocaleKeys.moreAction_wordCount.tr(\n        args: [\n          numberFormat\n              .format(titleCounter.wordCount + documentCounters.wordCount)\n              .toString(),\n        ],\n      ),\n    );\n    expect(\n      charCounter.text,\n      LocaleKeys.moreAction_charCount.tr(\n        args: [\n          numberFormat\n              .format(\n                titleCounter.charCount + documentCounters.charCount,\n              )\n              .toString(),\n        ],\n      ),\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  // +, ... button beside the block component.\n  group('block option action:', () {\n    Future<void> turnIntoBlock(\n      WidgetTester tester,\n      Path path, {\n      required String menuText,\n      required String afterType,\n    }) async {\n      await tester.editor.openTurnIntoMenu(path);\n      await tester.tapButton(\n        find.findTextInFlowyText(menuText),\n      );\n      final node = tester.editor.getCurrentEditorState().getNodeAtPath(path);\n      expect(node?.type, afterType);\n    }\n\n    testWidgets('''click + to add a block after current selection,\n         and click + and option key to add a block before current selection''',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      var editorState = tester.editor.getCurrentEditorState();\n      expect(editorState.getNodeAtPath([1])?.delta?.toPlainText(), isNotEmpty);\n\n      // add a new block after the current selection\n      await tester.editor.hoverAndClickOptionAddButton([0], false);\n      // await tester.pumpAndSettle();\n      expect(editorState.getNodeAtPath([1])?.delta?.toPlainText(), isEmpty);\n\n      // cancel the selection menu\n      await tester.tapAt(Offset.zero);\n\n      await tester.editor.hoverAndClickOptionAddButton([0], true);\n      await tester.pumpAndSettle();\n      expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), isEmpty);\n      // cancel the selection menu\n      await tester.tapAt(Offset.zero);\n      await tester.tapAt(Offset.zero);\n\n      await tester.createNewPageWithNameUnderParent(name: 'test');\n      await tester.openPage(gettingStarted);\n\n      // check the status again\n      editorState = tester.editor.getCurrentEditorState();\n      expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), isEmpty);\n      expect(editorState.getNodeAtPath([2])?.delta?.toPlainText(), isEmpty);\n    });\n\n    testWidgets('turn into - single line', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      const name = 'Test Document';\n      await tester.createNewPageWithNameUnderParent(name: name);\n      await tester.openPage(name);\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.ime.insertText('turn into');\n\n      // click the block option button to convert it to another blocks\n      final values = {\n        LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type,\n        LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type,\n        LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type,\n        LocaleKeys.editor_bulletedListShortForm.tr():\n            BulletedListBlockKeys.type,\n        LocaleKeys.editor_numberedListShortForm.tr():\n            NumberedListBlockKeys.type,\n        LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type,\n        LocaleKeys.editor_checkbox.tr(): TodoListBlockKeys.type,\n        LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type,\n        LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type,\n      };\n\n      for (final value in values.entries) {\n        final menuText = value.key;\n        final afterType = value.value;\n        await turnIntoBlock(\n          tester,\n          [0],\n          menuText: menuText,\n          afterType: afterType,\n        );\n      }\n    });\n\n    testWidgets('turn into - multi lines', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      const name = 'Test Document';\n      await tester.createNewPageWithNameUnderParent(name: name);\n      await tester.openPage(name);\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.ime.insertText('turn into 1');\n      await tester.ime.insertCharacter('\\n');\n      await tester.ime.insertText('turn into 2');\n\n      // click the block option button to convert it to another blocks\n      final values = {\n        LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type,\n        LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type,\n        LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type,\n        LocaleKeys.editor_bulletedListShortForm.tr():\n            BulletedListBlockKeys.type,\n        LocaleKeys.editor_numberedListShortForm.tr():\n            NumberedListBlockKeys.type,\n        LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type,\n        LocaleKeys.editor_checkbox.tr(): TodoListBlockKeys.type,\n        LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type,\n        LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type,\n      };\n\n      for (final value in values.entries) {\n        final editorState = tester.editor.getCurrentEditorState();\n        editorState.selection = Selection(\n          start: Position(path: [0]),\n          end: Position(path: [1], offset: 2),\n        );\n        final menuText = value.key;\n        final afterType = value.value;\n        await turnIntoBlock(\n          tester,\n          [0],\n          menuText: menuText,\n          afterType: afterType,\n        );\n      }\n    });\n\n    testWidgets(\n      'selecting the parent should deselect all the child nodes as well',\n      (tester) async {\n        await tester.initializeAppFlowy();\n        await tester.tapAnonymousSignInButton();\n\n        const name = 'Test Document';\n        await tester.createNewPageWithNameUnderParent(name: name);\n        await tester.openPage(name);\n\n        // create a nested list\n        // Item 1\n        //  Nested Item 1\n        await tester.editor.tapLineOfEditorAt(0);\n        await tester.ime.insertText('Item 1');\n        await tester.ime.insertCharacter('\\n');\n        await tester.simulateKeyEvent(LogicalKeyboardKey.tab);\n        await tester.ime.insertText('Nested Item 1');\n\n        // select the 'Nested Item 1' and then tap the option button of the 'Item 1'\n        final editorState = tester.editor.getCurrentEditorState();\n        final selection = Selection.collapsed(\n          Position(path: [0, 0], offset: 1),\n        );\n        editorState.selection = selection;\n        await tester.pumpAndSettle();\n        expect(editorState.selection, selection);\n        await tester.editor.hoverAndClickOptionMenuButton([0]);\n        expect(editorState.selection, Selection.collapsed(Position(path: [0])));\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_selection_test.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('document selection:', () {\n    testWidgets('select text from start to end by pan gesture ',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent();\n\n      final editor = tester.editor;\n      final editorState = editor.getCurrentEditorState();\n      // insert a paragraph\n      final transaction = editorState.transaction;\n      transaction.insertNode(\n        [0],\n        paragraphNode(\n          text:\n              '''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.''',\n        ),\n      );\n      await editorState.apply(transaction);\n      await tester.pumpAndSettle(Durations.short1);\n\n      final textBlocks = find.byType(AppFlowyRichText);\n      final topLeft = tester.getTopLeft(textBlocks.at(0));\n\n      final gesture = await tester.startGesture(\n        topLeft,\n        pointer: 7,\n      );\n      await tester.pumpAndSettle();\n\n      for (var i = 0; i < 10; i++) {\n        await gesture.moveBy(const Offset(10, 0));\n        await tester.pump(Durations.short1);\n      }\n\n      expect(editorState.selection!.start.offset, 0);\n    });\n\n    testWidgets('select and delete text', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      /// create a new document\n      await tester.createNewPageWithNameUnderParent();\n\n      /// input text\n      final editor = tester.editor;\n      final editorState = editor.getCurrentEditorState();\n\n      const inputText = 'Test for text selection and deletion';\n      final texts = inputText.split(' ');\n      await editor.tapLineOfEditorAt(0);\n      await tester.ime.insertText(inputText);\n\n      /// selecte and delete\n      int index = 0;\n      while (texts.isNotEmpty) {\n        final text = texts.removeAt(0);\n        await tester.editor.updateSelection(\n          Selection(\n            start: Position(path: [0], offset: index),\n            end: Position(path: [0], offset: index + text.length),\n          ),\n        );\n        await tester.simulateKeyEvent(LogicalKeyboardKey.delete);\n        index++;\n      }\n\n      /// excpete the text value is correct\n      final node = editorState.getNodeAtPath([0])!;\n      final nodeText = node.delta?.toPlainText() ?? '';\n      expect(nodeText, ' ' * (index - 1));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_shortcuts_test.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('document shortcuts:', () {\n    testWidgets('custom cut command', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      const pageName = 'Test Document Shortcuts';\n      await tester.createNewPageWithNameUnderParent(name: pageName);\n\n      // focus on the editor\n      await tester.tap(find.byType(AppFlowyEditor));\n\n      // mock the data\n      final editorState = tester.editor.getCurrentEditorState();\n      final transaction = editorState.transaction;\n      const text1 = '1. First line';\n      const text2 = '2. Second line';\n      transaction.insertNodes([\n        0,\n      ], [\n        paragraphNode(text: text1),\n        paragraphNode(text: text2),\n      ]);\n      await editorState.apply(transaction);\n      await tester.pumpAndSettle();\n\n      // focus on the end of the first line\n      await tester.editor.updateSelection(\n        Selection.collapsed(\n          Position(path: [0], offset: text1.length),\n        ),\n      );\n      // press the keybinding\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyX,\n        isControlPressed: !UniversalPlatform.isMacOS,\n        isMetaPressed: UniversalPlatform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      // check the clipboard\n      final clipboard = await Clipboard.getData(Clipboard.kTextPlain);\n      expect(\n        clipboard?.text,\n        equals(text1),\n      );\n\n      final node = tester.editor.getNodeAtPath([0]);\n      expect(\n        node.delta?.toPlainText(),\n        equals(text2),\n      );\n\n      // select the whole line\n      await tester.editor.updateSelection(\n        Selection.single(\n          path: [0],\n          startOffset: 0,\n          endOffset: text2.length,\n        ),\n      );\n\n      // press the keybinding\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyX,\n        isControlPressed: !UniversalPlatform.isMacOS,\n        isMetaPressed: UniversalPlatform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      // all the text should be deleted\n      expect(\n        node.delta?.toPlainText(),\n        equals(''),\n      );\n\n      final clipboard2 = await Clipboard.getData(Clipboard.kTextPlain);\n      expect(\n        clipboard2?.text,\n        equals(text2),\n      );\n    });\n\n    testWidgets(\n        'custom copy command - copy whole line when selection is collapsed',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      const pageName = 'Test Document Shortcuts';\n      await tester.createNewPageWithNameUnderParent(name: pageName);\n\n      // focus on the editor\n      await tester.tap(find.byType(AppFlowyEditor));\n\n      // mock the data\n      final editorState = tester.editor.getCurrentEditorState();\n      final transaction = editorState.transaction;\n      const text1 = '1. First line';\n      transaction.insertNodes([\n        0,\n      ], [\n        paragraphNode(text: text1),\n      ]);\n      await editorState.apply(transaction);\n      await tester.pumpAndSettle();\n\n      // focus on the end of the first line\n      await tester.editor.updateSelection(\n        Selection.collapsed(\n          Position(path: [0], offset: text1.length),\n        ),\n      );\n      // press the keybinding\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyC,\n        isControlPressed: !UniversalPlatform.isMacOS,\n        isMetaPressed: UniversalPlatform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      // check the clipboard\n      final clipboard = await Clipboard.getData(Clipboard.kTextPlain);\n      expect(\n        clipboard?.text,\n        equals(text1),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_sub_page_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/emoji.dart';\nimport '../../shared/util.dart';\n\n// Test cases for the Document SubPageBlock that needs to be covered:\n// - [x] Insert a new SubPageBlock from Slash menu items (Expect it will create a child view under current view)\n// - [x] Delete a SubPageBlock from Block Action Menu (Expect the view is moved to trash / deleted)\n// - [x] Delete a SubPageBlock with backspace when selected (Expect the view is moved to trash / deleted)\n// - [x] Copy+paste a SubPageBlock in same Document (Expect a new view is created under current view with same content and name)\n// - [x] Copy+paste a SubPageBlock in different Document (Expect a new view is created under current view with same content and name)\n// - [x] Cut+paste a SubPageBlock in same Document (Expect the view to be deleted on Cut, and brought back on Paste)\n// - [x] Cut+paste a SubPageBlock in different Document (Expect the view to be deleted on Cut, and brought back on Paste)\n// - [x] Undo adding a SubPageBlock (Expect the view to be deleted)\n// - [x] Undo delete of a SubPageBlock (Expect the view to be brought back to original position)\n// - [x] Redo adding a SubPageBlock (Expect the view to be restored)\n// - [x] Redo delete of a SubPageBlock (Expect the view to be moved to trash again)\n// - [x] Renaming a child view (Expect the view name to be updated in the document)\n// - [x] Deleting a view (to trash) linked to a SubPageBlock deleted the SubPageBlock (Expect the SubPageBlock to be deleted)\n// - [x] Duplicating a SubPageBlock node from Action Menu (Expect a new view is created under current view with same content and name + (copy))\n// - [x] Dragging a SubPageBlock node to a new position in the document (Expect everything to be normal)\n\n/// The defaut page name is empty, if we're looking for a \"text\" we can look for\n/// [LocaleKeys.menuAppHeader_defaultNewPageName] but it won't work for eg. hoverOnPageName\n/// as it looks at the text provided instead of the actual displayed text.\n///\nconst _defaultPageName = \"\";\n\nvoid main() {\n  setUpAll(() {\n    IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n    RecentIcons.enable = false;\n  });\n\n  tearDownAll(() {\n    RecentIcons.enable = true;\n  });\n\n  group('Document SubPageBlock tests', () {\n    testWidgets('Insert a new SubPageBlock from Slash menu items',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');\n\n      await tester.insertSubPageFromSlashMenu();\n\n      expect(\n        find.text(LocaleKeys.menuAppHeader_defaultNewPageName.tr()),\n        findsNWidgets(3),\n      );\n    });\n\n    testWidgets('Rename and then Delete a SubPageBlock from Block Action Menu',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');\n\n      await tester.insertSubPageFromSlashMenu();\n\n      await tester.renamePageWithSecondary(_defaultPageName, 'Child page');\n      expect(find.text('Child page'), findsNWidgets(2));\n\n      await tester.editor.hoverAndClickOptionMenuButton([0]);\n\n      await tester.tapButtonWithName(LocaleKeys.button_delete.tr());\n      await tester.pumpAndSettle();\n\n      expect(find.text('Child page'), findsNothing);\n    });\n\n    testWidgets('Copy+paste a SubPageBlock in same Document', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');\n\n      await tester.insertSubPageFromSlashMenu();\n\n      await tester.renamePageWithSecondary(_defaultPageName, 'Child page');\n      expect(find.text('Child page'), findsNWidgets(2));\n\n      await tester.editor.hoverAndClickOptionAddButton([0], false);\n      await tester.editor.tapLineOfEditorAt(1);\n\n      // This is a workaround to allow CTRL+A and CTRL+C to work to copy\n      // the SubPageBlock as well.\n      await tester.ime.insertText('ABC');\n\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyA,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyC,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      await tester.editor.hoverAndClickOptionAddButton([1], false);\n      await tester.editor.tapLineOfEditorAt(2);\n      await tester.pumpAndSettle();\n\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle(const Duration(seconds: 5));\n\n      expect(find.byType(SubPageBlockComponent), findsNWidgets(2));\n      expect(find.text('Child page'), findsNWidgets(2));\n      expect(find.text('Child page (copy)'), findsNWidgets(2));\n    });\n\n    testWidgets('Copy+paste a SubPageBlock in different Document',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');\n\n      await tester.insertSubPageFromSlashMenu();\n\n      await tester.renamePageWithSecondary(_defaultPageName, 'Child page');\n      expect(find.text('Child page'), findsNWidgets(2));\n\n      await tester.editor.hoverAndClickOptionAddButton([0], false);\n      await tester.editor.tapLineOfEditorAt(1);\n\n      // This is a workaround to allow CTRL+A and CTRL+C to work to copy\n      // the SubPageBlock as well.\n      await tester.ime.insertText('ABC');\n\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyA,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyC,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock-2');\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.pumpAndSettle();\n\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      await tester.expandOrCollapsePage(\n        pageName: 'SubPageBlock-2',\n        layout: ViewLayoutPB.Document,\n      );\n\n      expect(find.byType(SubPageBlockComponent), findsOneWidget);\n      expect(find.text('Child page'), findsOneWidget);\n      expect(find.text('Child page (copy)'), findsNWidgets(2));\n    });\n\n    testWidgets('Cut+paste a SubPageBlock in same Document', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');\n\n      await tester.insertSubPageFromSlashMenu();\n\n      await tester.renamePageWithSecondary(_defaultPageName, 'Child page');\n      expect(find.text('Child page'), findsNWidgets(2));\n\n      await tester.editor\n          .updateSelection(Selection.single(path: [0], startOffset: 0));\n\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyX,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(SubPageBlockComponent), findsNothing);\n      expect(find.text('Child page'), findsNothing);\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(SubPageBlockComponent), findsOneWidget);\n      expect(find.text('Child page'), findsNWidgets(2));\n    });\n\n    testWidgets('Cut+paste a SubPageBlock in different Document',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');\n\n      await tester.insertSubPageFromSlashMenu();\n\n      await tester.renamePageWithSecondary(_defaultPageName, 'Child page');\n      expect(find.text('Child page'), findsNWidgets(2));\n\n      await tester.editor\n          .updateSelection(Selection.single(path: [0], startOffset: 0));\n\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyX,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(SubPageBlockComponent), findsNothing);\n      expect(find.text('Child page'), findsNothing);\n\n      await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock-2');\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.pumpAndSettle();\n\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      await tester.expandOrCollapsePage(\n        pageName: 'SubPageBlock-2',\n        layout: ViewLayoutPB.Document,\n      );\n\n      expect(find.byType(SubPageBlockComponent), findsOneWidget);\n      expect(find.text('Child page'), findsNWidgets(2));\n      expect(find.text('Child page (copy)'), findsNothing);\n    });\n\n    testWidgets('Undo delete of a SubPageBlock', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');\n\n      await tester.insertSubPageFromSlashMenu();\n\n      await tester.renamePageWithSecondary(_defaultPageName, 'Child page');\n      expect(find.text('Child page'), findsNWidgets(2));\n\n      await tester.editor.hoverAndClickOptionMenuButton([0]);\n      await tester.tapButtonWithName(LocaleKeys.button_delete.tr());\n      await tester.pumpAndSettle();\n\n      expect(find.text('Child page'), findsNothing);\n      expect(find.byType(SubPageBlockComponent), findsNothing);\n\n      // Since there is no selection active in editor before deleting Node,\n      // we need to give focus back to the editor\n      await tester.editor\n          .updateSelection(Selection.collapsed(Position(path: [0])));\n\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyZ,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.text('Child page'), findsNWidgets(2));\n      expect(find.byType(SubPageBlockComponent), findsOneWidget);\n    });\n\n    // Redo: undoing deleting a subpage block, then redoing to delete it again\n    // -> Add a subpage block\n    // -> Delete\n    // -> Undo\n    // -> Redo\n    testWidgets('Redo delete of a SubPageBlock', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');\n\n      await tester.insertSubPageFromSlashMenu(true);\n\n      await tester.renamePageWithSecondary(_defaultPageName, 'Child page');\n      expect(find.text('Child page'), findsNWidgets(2));\n\n      // Delete\n      await tester.editor.hoverAndClickOptionMenuButton([1]);\n      await tester.tapButtonWithName(LocaleKeys.button_delete.tr());\n      await tester.pumpAndSettle();\n\n      expect(find.text('Child page'), findsNothing);\n      expect(find.byType(SubPageBlockComponent), findsNothing);\n\n      await tester.editor.tapLineOfEditorAt(0);\n\n      // Undo\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyZ,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n      expect(find.byType(SubPageBlockComponent), findsOneWidget);\n      expect(find.text('Child page'), findsNWidgets(2));\n\n      // Redo\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyZ,\n        isShiftPressed: true,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(SubPageBlockComponent), findsNothing);\n      expect(find.text('Child page'), findsNothing);\n    });\n\n    testWidgets('Delete a view from sidebar', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');\n\n      await tester.insertSubPageFromSlashMenu();\n\n      await tester.renamePageWithSecondary(_defaultPageName, 'Child page');\n      expect(find.text('Child page'), findsNWidgets(2));\n      expect(find.byType(SubPageBlockComponent), findsOneWidget);\n\n      await tester.hoverOnPageName(\n        'Child page',\n        onHover: () async {\n          await tester.tapDeletePageButton();\n        },\n      );\n      await tester.pumpAndSettle(const Duration(seconds: 1));\n\n      expect(find.text('Child page'), findsNothing);\n      expect(find.byType(SubPageBlockComponent), findsNothing);\n    });\n\n    testWidgets('Duplicate SubPageBlock from Block Menu', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');\n\n      await tester.insertSubPageFromSlashMenu();\n      await tester.renamePageWithSecondary(_defaultPageName, 'Child page');\n      expect(find.text('Child page'), findsNWidgets(2));\n\n      await tester.editor.hoverAndClickOptionMenuButton([0]);\n\n      await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr());\n      await tester.pumpAndSettle();\n\n      expect(find.text('Child page'), findsNWidgets(2));\n      expect(find.text('Child page (copy)'), findsNWidgets(2));\n      expect(find.byType(SubPageBlockComponent), findsNWidgets(2));\n    });\n\n    testWidgets('Drag SubPageBlock to top of Document', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');\n\n      await tester.insertSubPageFromSlashMenu(true);\n\n      expect(find.byType(SubPageBlockComponent), findsOneWidget);\n\n      final beforeNode = tester.editor.getNodeAtPath([1]);\n\n      await tester.editor.dragBlock([1], const Offset(20, -45));\n      await tester.pumpAndSettle(Durations.long1);\n\n      final afterNode = tester.editor.getNodeAtPath([0]);\n\n      expect(afterNode.type, SubPageBlockKeys.type);\n      expect(afterNode.type, beforeNode.type);\n      expect(find.byType(SubPageBlockComponent), findsOneWidget);\n    });\n\n    testWidgets('turn into page', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');\n\n      final editorState = tester.editor.getCurrentEditorState();\n\n      // Insert nested list\n      final transaction = editorState.transaction;\n      transaction.insertNode(\n        [0],\n        bulletedListNode(\n          text: 'Parent',\n          children: [\n            bulletedListNode(text: 'Child 1'),\n            bulletedListNode(text: 'Child 2'),\n          ],\n        ),\n      );\n      await editorState.apply(transaction);\n      await tester.pumpAndSettle();\n\n      expect(find.byType(SubPageBlockComponent), findsNothing);\n\n      await tester.editor.hoverAndClickOptionMenuButton([0]);\n      await tester.tapButtonWithName(\n        LocaleKeys.document_plugins_optionAction_turnInto.tr(),\n      );\n      await tester.pumpAndSettle();\n\n      await tester.tap(find.text(LocaleKeys.editor_page.tr()));\n      await tester.pumpAndSettle();\n\n      expect(find.byType(SubPageBlockComponent), findsOneWidget);\n\n      await tester.expandOrCollapsePage(\n        pageName: 'SubPageBlock',\n        layout: ViewLayoutPB.Document,\n      );\n\n      expect(find.text('Parent'), findsNWidgets(2));\n    });\n\n    testWidgets('Displaying icon of subpage', (tester) async {\n      const firstPage = 'FirstPage';\n\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(name: firstPage);\n      final icon = await tester.loadIcon();\n\n      /// create subpage\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.showSlashMenu();\n      await tester.editor.tapSlashMenuItemWithName(\n        LocaleKeys.document_slashMenu_subPage_name.tr(),\n        offset: 100,\n      );\n\n      /// add icon\n      await tester.editor.hoverOnCoverToolbar();\n      await tester.editor.tapAddIconButton();\n      await tester.tapIcon(icon);\n      await tester.pumpAndSettle();\n      await tester.openPage(firstPage);\n\n      await tester.expandOrCollapsePage(\n        pageName: firstPage,\n        layout: ViewLayoutPB.Document,\n      );\n\n      /// check if there is a icon in document\n      final iconWidget = find.byWidgetPredicate((w) {\n        if (w is! RawEmojiIconWidget) return false;\n        final iconData = w.emoji.emoji;\n        return iconData == icon.emoji;\n      });\n      expect(iconWidget, findsOneWidget);\n    });\n  });\n}\n\nextension _SubPageTestHelper on WidgetTester {\n  Future<void> insertSubPageFromSlashMenu([bool withTextNode = false]) async {\n    await editor.tapLineOfEditorAt(0);\n\n    if (withTextNode) {\n      await ime.insertText('ABC');\n      await editor.getCurrentEditorState().insertNewLine();\n      await pumpAndSettle();\n    }\n\n    await editor.showSlashMenu();\n    await editor.tapSlashMenuItemWithName(\n      LocaleKeys.document_slashMenu_subPage_name.tr(),\n      offset: 100,\n    );\n\n    // Navigate to the previous page to see the SubPageBlock\n    await openPage('SubPageBlock');\n    await pumpAndSettle();\n\n    await pumpUntilFound(find.byType(SubPageBlockComponent));\n  }\n\n  Future<void> renamePageWithSecondary(\n    String currentName,\n    String newName,\n  ) async {\n    await hoverOnPageName(currentName, onHover: () async => pumpAndSettle());\n    await rightClickOnPageName(currentName);\n    await tapButtonWithName(ViewMoreActionType.rename.name);\n    await enterText(find.byType(AFTextField), newName);\n    await tapButton(find.text(LocaleKeys.button_confirm.tr()));\n    await pumpAndSettle();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_1.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'document_create_and_delete_test.dart'\n    as document_create_and_delete_test;\nimport 'document_with_cover_image_test.dart' as document_with_cover_image_test;\nimport 'document_with_database_test.dart' as document_with_database_test;\nimport 'document_with_inline_math_equation_test.dart'\n    as document_with_inline_math_equation_test;\nimport 'document_with_inline_page_test.dart' as document_with_inline_page_test;\nimport 'edit_document_test.dart' as document_edit_test;\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  // Document integration tests\n  document_create_and_delete_test.main();\n  document_edit_test.main();\n  document_with_database_test.main();\n  document_with_inline_page_test.main();\n  document_with_inline_math_equation_test.main();\n  document_with_cover_image_test.main();\n  // Don't add new tests here.\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_2.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'document_app_lifecycle_test.dart' as document_app_lifecycle_test;\nimport 'document_deletion_test.dart' as document_deletion_test;\nimport 'document_inline_sub_page_test.dart' as document_inline_sub_page_test;\nimport 'document_option_action_test.dart' as document_option_action_test;\nimport 'document_title_test.dart' as document_title_test;\nimport 'document_with_date_reminder_test.dart'\n    as document_with_date_reminder_test;\nimport 'document_with_toggle_heading_block_test.dart'\n    as document_with_toggle_heading_block_test;\nimport 'document_sub_page_test.dart' as document_sub_page_test;\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  // Document integration tests\n  document_title_test.main();\n  document_app_lifecycle_test.main();\n  document_with_date_reminder_test.main();\n  document_deletion_test.main();\n  document_option_action_test.main();\n  document_inline_sub_page_test.main();\n  document_with_toggle_heading_block_test.main();\n  document_sub_page_test.main();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_3.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'document_alignment_test.dart' as document_alignment_test;\nimport 'document_codeblock_paste_test.dart' as document_codeblock_paste_test;\nimport 'document_copy_and_paste_test.dart' as document_copy_and_paste_test;\nimport 'document_text_direction_test.dart' as document_text_direction_test;\nimport 'document_with_outline_block_test.dart' as document_with_outline_block;\nimport 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  // Document integration tests\n  document_with_outline_block.main();\n  document_with_toggle_list_test.main();\n  document_copy_and_paste_test.main();\n  document_codeblock_paste_test.main();\n  document_alignment_test.main();\n  document_text_direction_test.main();\n\n  // Don't add new tests here.\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_4.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'document_block_option_test.dart' as document_block_option_test;\nimport 'document_find_menu_test.dart' as document_find_menu_test;\nimport 'document_inline_page_reference_test.dart'\n    as document_inline_page_reference_test;\nimport 'document_more_actions_test.dart' as document_more_actions_test;\nimport 'document_shortcuts_test.dart' as document_shortcuts_test;\nimport 'document_toolbar_test.dart' as document_toolbar_test;\nimport 'document_with_file_test.dart' as document_with_file_test;\nimport 'document_with_image_block_test.dart' as document_with_image_block_test;\nimport 'document_with_multi_image_block_test.dart'\n    as document_with_multi_image_block_test;\nimport 'document_with_simple_table_test.dart'\n    as document_with_simple_table_test;\nimport 'document_link_preview_test.dart' as document_link_preview_test;\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  // Document integration tests\n  document_with_image_block_test.main();\n  document_with_multi_image_block_test.main();\n  document_inline_page_reference_test.main();\n  document_more_actions_test.main();\n  document_with_file_test.main();\n  document_shortcuts_test.main();\n  document_block_option_test.main();\n  document_find_menu_test.main();\n  document_toolbar_test.main();\n  document_with_simple_table_test.main();\n  document_link_preview_test.main();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_text_direction_test.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('text direction', () {\n    testWidgets(\n        '''no text direction items will be displayed in the default/LTR mode, and three text direction items will be displayed when toggle is enabled.''',\n        (tester) async {\n      // combine the two tests into one to avoid the time-consuming process of initializing the app\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      final selection = Selection.single(\n        path: [0],\n        startOffset: 0,\n        endOffset: 1,\n      );\n      // click the first line of the readme\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.updateSelection(selection);\n      await tester.pumpAndSettle();\n\n      // because this icons are defined in the appflowy_editor package, we can't fetch the icons by SVG data. [textDirectionItems]\n      final textDirectionIconNames = [\n        'toolbar/text_direction_auto',\n        'toolbar/text_direction_ltr',\n        'toolbar/text_direction_rtl',\n      ];\n      // no text direction items by default\n      var button = find.byWidgetPredicate(\n        (widget) =>\n            widget is SVGIconItemWidget &&\n            textDirectionIconNames.contains(widget.iconName),\n      );\n      expect(button, findsNothing);\n\n      // switch to the RTL mode\n      await tester.toggleEnableRTLToolbarItems();\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.updateSelection(selection);\n      await tester.pumpAndSettle();\n\n      button = find.byWidgetPredicate(\n        (widget) =>\n            widget is SVGIconItemWidget &&\n            textDirectionIconNames.contains(widget.iconName),\n      );\n      expect(button, findsNWidgets(3));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_title_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../../shared/constants.dart';\nimport '../../shared/util.dart';\n\nconst _testDocumentName = 'Test Document';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('document title:', () {\n    testWidgets('create a new document and edit title', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent();\n\n      final title = tester.editor.findDocumentTitle('');\n      expect(title, findsOneWidget);\n\n      // input name\n      await tester.enterText(title, _testDocumentName);\n      await tester.pumpAndSettle();\n\n      final newTitle = tester.editor.findDocumentTitle(_testDocumentName);\n      expect(newTitle, findsOneWidget);\n\n      // press enter to create a new line\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      await tester.pumpAndSettle();\n\n      const firstLine = 'First line of text';\n      await tester.ime.insertText(firstLine);\n      await tester.pumpAndSettle();\n\n      final firstLineText = find.text(firstLine, findRichText: true);\n      expect(firstLineText, findsOneWidget);\n\n      // press cmd/ctrl+left to move the cursor to the start of the line\n      if (UniversalPlatform.isMacOS) {\n        await tester.simulateKeyEvent(\n          LogicalKeyboardKey.arrowLeft,\n          isMetaPressed: true,\n        );\n      } else {\n        await tester.simulateKeyEvent(LogicalKeyboardKey.home);\n      }\n      await tester.pumpAndSettle();\n\n      // press arrow left to delete the first line\n      await tester.simulateKeyEvent(LogicalKeyboardKey.arrowLeft);\n      await tester.pumpAndSettle();\n\n      // check if the title is on focus\n      final titleOnFocus = tester.editor.findDocumentTitle(_testDocumentName);\n      final titleWidget = tester.widget<TextField>(titleOnFocus);\n      expect(titleWidget.focusNode?.hasFocus, isTrue);\n\n      // press the right arrow key to move the cursor to the first line\n      await tester.simulateKeyEvent(LogicalKeyboardKey.arrowRight);\n\n      // check if the title is not on focus\n      expect(titleWidget.focusNode?.hasFocus, isFalse);\n\n      final editorState = tester.editor.getCurrentEditorState();\n      expect(editorState.selection, Selection.collapsed(Position(path: [0])));\n\n      // press the backspace key to go to the title\n      await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);\n\n      expect(editorState.selection, null);\n      expect(titleWidget.focusNode?.hasFocus, isTrue);\n    });\n\n    testWidgets('check if the title is saved', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent();\n\n      final title = tester.editor.findDocumentTitle('');\n      expect(title, findsOneWidget);\n\n      // input name\n      await tester.enterText(title, _testDocumentName);\n      await tester.pumpAndSettle();\n\n      if (UniversalPlatform.isLinux) {\n        // wait for the name to be saved\n        await tester.wait(250);\n      }\n\n      // go to the get started page\n      await tester.tapButton(\n        tester.findPageName(Constants.gettingStartedPageName),\n      );\n\n      // go back to the  page\n      await tester.tapButton(tester.findPageName(_testDocumentName));\n\n      // check if the title is saved\n      final testDocumentTitle = tester.editor.findDocumentTitle(\n        _testDocumentName,\n      );\n      expect(testDocumentTitle, findsOneWidget);\n    });\n\n    testWidgets('arrow up from first line moves focus to title',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent();\n\n      final title = tester.editor.findDocumentTitle('');\n      await tester.enterText(title, _testDocumentName);\n      await tester.pumpAndSettle();\n\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      await tester.ime.insertText('First line of text');\n      await tester.pumpAndSettle();\n\n      await tester.simulateKeyEvent(LogicalKeyboardKey.home);\n\n      // press the arrow upload\n      await tester.simulateKeyEvent(LogicalKeyboardKey.arrowUp);\n\n      final titleWidget = tester.widget<TextField>(\n        tester.editor.findDocumentTitle(_testDocumentName),\n      );\n      expect(titleWidget.focusNode?.hasFocus, isTrue);\n\n      final editorState = tester.editor.getCurrentEditorState();\n      expect(editorState.selection, null);\n    });\n\n    testWidgets(\n        'backspace at start of first line moves focus to title and deletes empty paragraph',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent();\n\n      final title = tester.editor.findDocumentTitle('');\n      await tester.enterText(title, _testDocumentName);\n      await tester.pumpAndSettle();\n\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n\n      final editorState = tester.editor.getCurrentEditorState();\n      expect(editorState.document.root.children.length, equals(2));\n\n      await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);\n\n      final titleWidget = tester.widget<TextField>(\n        tester.editor.findDocumentTitle(_testDocumentName),\n      );\n      expect(titleWidget.focusNode?.hasFocus, isTrue);\n\n      // at least one empty paragraph node is created\n      expect(editorState.document.root.children.length, equals(1));\n    });\n\n    testWidgets('arrow right from end of title moves focus to first line',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent();\n\n      final title = tester.editor.findDocumentTitle('');\n      await tester.enterText(title, _testDocumentName);\n      await tester.pumpAndSettle();\n\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      await tester.ime.insertText('First line of text');\n\n      await tester.tapButton(\n        tester.editor.findDocumentTitle(_testDocumentName),\n      );\n      await tester.simulateKeyEvent(LogicalKeyboardKey.end);\n      await tester.simulateKeyEvent(LogicalKeyboardKey.arrowRight);\n\n      final editorState = tester.editor.getCurrentEditorState();\n      expect(\n        editorState.selection,\n        Selection.collapsed(\n          Position(path: [0]),\n        ),\n      );\n    });\n\n    testWidgets('change the title via sidebar, check the title is updated',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent();\n\n      final title = tester.editor.findDocumentTitle('');\n      expect(title, findsOneWidget);\n\n      await tester.hoverOnPageName(\n        '',\n        onHover: () async {\n          await tester.renamePage(_testDocumentName);\n          await tester.pumpAndSettle();\n        },\n      );\n      await tester.pumpAndSettle();\n\n      final newTitle = tester.editor.findDocumentTitle(_testDocumentName);\n      expect(newTitle, findsOneWidget);\n    });\n\n    testWidgets('execute undo and redo in title', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent();\n\n      final title = tester.editor.findDocumentTitle('');\n      await tester.enterText(title, _testDocumentName);\n      // press a random key to make the undo stack not empty\n      await tester.simulateKeyEvent(LogicalKeyboardKey.keyA);\n      await tester.pumpAndSettle();\n\n      // undo\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyZ,\n        isControlPressed: !UniversalPlatform.isMacOS,\n        isMetaPressed: UniversalPlatform.isMacOS,\n      );\n      // wait for the undo to be applied\n      await tester.pumpAndSettle(Durations.long1);\n\n      // expect the title is empty\n      expect(\n        tester\n            .widget<TextField>(\n              tester.editor.findDocumentTitle(''),\n            )\n            .controller\n            ?.text,\n        '',\n      );\n\n      // redo\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyZ,\n        isControlPressed: !UniversalPlatform.isMacOS,\n        isMetaPressed: UniversalPlatform.isMacOS,\n        isShiftPressed: true,\n      );\n\n      await tester.pumpAndSettle(Durations.short1);\n\n      if (UniversalPlatform.isMacOS) {\n        expect(\n          tester\n              .widget<TextField>(\n                tester.editor.findDocumentTitle(_testDocumentName),\n              )\n              .controller\n              ?.text,\n          _testDocumentName,\n        );\n      }\n    });\n\n    testWidgets('escape key should exit the editing mode', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent();\n\n      final title = tester.editor.findDocumentTitle('');\n      await tester.enterText(title, _testDocumentName);\n      await tester.pumpAndSettle();\n\n      await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n      await tester.pumpAndSettle();\n\n      expect(\n        tester\n            .widget<TextField>(\n              tester.editor.findDocumentTitle(_testDocumentName),\n            )\n            .focusNode\n            ?.hasFocus,\n        isFalse,\n      );\n    });\n\n    testWidgets('press arrow down key in title, check if the cursor flashes',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent();\n\n      final title = tester.editor.findDocumentTitle('');\n      await tester.enterText(title, _testDocumentName);\n      await tester.pumpAndSettle();\n\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      const inputText = 'Hello World';\n      await tester.ime.insertText(inputText);\n\n      await tester.tapButton(\n        tester.editor.findDocumentTitle(_testDocumentName),\n      );\n      await tester.simulateKeyEvent(LogicalKeyboardKey.arrowDown);\n      final editorState = tester.editor.getCurrentEditorState();\n      expect(\n        editorState.selection,\n        Selection.collapsed(\n          Position(path: [0], offset: inputText.length),\n        ),\n      );\n    });\n\n    testWidgets(\n        'hover on the cover title, check if the add icon & add cover button are shown',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent();\n\n      final title = tester.editor.findDocumentTitle('');\n      await tester.hoverOnWidget(\n        title,\n        onHover: () async {\n          expect(find.byType(DocumentCoverWidget), findsOneWidget);\n        },\n      );\n\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('paste text in title, check if the text is updated',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent();\n\n      await Clipboard.setData(const ClipboardData(text: _testDocumentName));\n\n      final title = tester.editor.findDocumentTitle('');\n      await tester.tapButton(title);\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isMetaPressed: UniversalPlatform.isMacOS,\n        isControlPressed: !UniversalPlatform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      final newTitle = tester.editor.findDocumentTitle(_testDocumentName);\n      expect(newTitle, findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  Future<void> selectText(WidgetTester tester, String text) async {\n    await tester.editor.updateSelection(\n      Selection.single(\n        path: [0],\n        startOffset: 0,\n        endOffset: text.length,\n      ),\n    );\n  }\n\n  Future<void> prepareForToolbar(WidgetTester tester, String text) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n\n    await tester.createNewPageWithNameUnderParent();\n\n    await tester.editor.tapLineOfEditorAt(0);\n    await tester.ime.insertText(text);\n    await selectText(tester, text);\n  }\n\n  group('document toolbar:', () {\n    testWidgets('font family', (tester) async {\n      await prepareForToolbar(tester, 'font family');\n\n      // tap more options button\n      await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_more_m);\n      // tap the font family button\n      final fontFamilyButton = find.byKey(kFontFamilyToolbarItemKey);\n      await tester.tapButton(fontFamilyButton);\n\n      // expect to see the font family dropdown immediately\n      expect(find.byType(FontFamilyDropDown), findsOneWidget);\n\n      // click the font family 'Abel'\n      const abel = 'Abel';\n      await tester.tapButton(find.text(abel));\n\n      // check the text is updated to 'Abel'\n      final editorState = tester.editor.getCurrentEditorState();\n      expect(\n        editorState.getDeltaAttributeValueInSelection(\n          AppFlowyRichTextKeys.fontFamily,\n        ),\n        abel,\n      );\n    });\n\n    testWidgets('heading 1~3', (tester) async {\n      const text = 'heading';\n      await prepareForToolbar(tester, text);\n\n      Future<void> testChangeHeading(\n        FlowySvgData svg,\n        String title,\n        int level,\n      ) async {\n        /// tap suggestions item\n        final suggestionsButton = find.byKey(kSuggestionsItemKey);\n        await tester.tapButton(suggestionsButton);\n\n        /// tap item\n        await tester.ensureVisible(find.byFlowySvg(svg));\n        await tester.tapButton(find.byFlowySvg(svg));\n\n        /// check the type of node is [HeadingBlockKeys.type]\n        await selectText(tester, text);\n        final editorState = tester.editor.getCurrentEditorState();\n        final selection = editorState.selection!;\n        final node = editorState.getNodeAtPath(selection.start.path)!,\n            nodeLevel = node.attributes[HeadingBlockKeys.level]!;\n        expect(node.type, HeadingBlockKeys.type);\n        expect(nodeLevel, level);\n\n        /// show toolbar again\n        await selectText(tester, text);\n\n        /// the text of suggestions item should be changed\n        expect(\n          find.descendant(of: suggestionsButton, matching: find.text(title)),\n          findsOneWidget,\n        );\n      }\n\n      await testChangeHeading(\n        FlowySvgs.type_h1_m,\n        LocaleKeys.document_toolbar_h1.tr(),\n        1,\n      );\n\n      await testChangeHeading(\n        FlowySvgs.type_h2_m,\n        LocaleKeys.document_toolbar_h2.tr(),\n        2,\n      );\n      await testChangeHeading(\n        FlowySvgs.type_h3_m,\n        LocaleKeys.document_toolbar_h3.tr(),\n        3,\n      );\n    });\n\n    testWidgets('toggle 1~3', (tester) async {\n      const text = 'toggle';\n      await prepareForToolbar(tester, text);\n\n      Future<void> testChangeToggle(\n        FlowySvgData svg,\n        String title,\n        int? level,\n      ) async {\n        /// tap suggestions item\n        final suggestionsButton = find.byKey(kSuggestionsItemKey);\n        await tester.tapButton(suggestionsButton);\n\n        /// tap item\n        await tester.ensureVisible(find.byFlowySvg(svg));\n        await tester.tapButton(find.byFlowySvg(svg));\n\n        /// check the type of node is [HeadingBlockKeys.type]\n        await selectText(tester, text);\n        final editorState = tester.editor.getCurrentEditorState();\n        final selection = editorState.selection!;\n        final node = editorState.getNodeAtPath(selection.start.path)!,\n            nodeLevel = node.attributes[ToggleListBlockKeys.level];\n        expect(node.type, ToggleListBlockKeys.type);\n        expect(nodeLevel, level);\n\n        /// show toolbar again\n        await selectText(tester, text);\n\n        /// the text of suggestions item should be changed\n        expect(\n          find.descendant(of: suggestionsButton, matching: find.text(title)),\n          findsOneWidget,\n        );\n      }\n\n      await testChangeToggle(\n        FlowySvgs.type_toggle_list_m,\n        LocaleKeys.editor_toggleListShortForm.tr(),\n        null,\n      );\n\n      await testChangeToggle(\n        FlowySvgs.type_toggle_h1_m,\n        LocaleKeys.editor_toggleHeading1ShortForm.tr(),\n        1,\n      );\n\n      await testChangeToggle(\n        FlowySvgs.type_toggle_h2_m,\n        LocaleKeys.editor_toggleHeading2ShortForm.tr(),\n        2,\n      );\n\n      await testChangeToggle(\n        FlowySvgs.type_toggle_h3_m,\n        LocaleKeys.editor_toggleHeading3ShortForm.tr(),\n        3,\n      );\n    });\n\n    testWidgets('toolbar will not rebuild after click item', (tester) async {\n      const text = 'Test rebuilding';\n      await prepareForToolbar(tester, text);\n      Finder toolbar = find.byType(DesktopFloatingToolbar);\n      Element toolbarElement = toolbar.evaluate().first;\n      final elementHashcode = toolbarElement.hashCode;\n      final boldButton = find.byFlowySvg(FlowySvgs.toolbar_bold_m),\n          underlineButton = find.byFlowySvg(FlowySvgs.toolbar_underline_m),\n          italicButton = find.byFlowySvg(FlowySvgs.toolbar_inline_italic_m);\n\n      /// tap format buttons\n      await tester.tapButton(boldButton);\n      await tester.tapButton(underlineButton);\n      await tester.tapButton(italicButton);\n      toolbar = find.byType(DesktopFloatingToolbar);\n      toolbarElement = toolbar.evaluate().first;\n\n      /// check if the toolbar is not rebuilt\n      expect(elementHashcode, toolbarElement.hashCode);\n      final editorState = tester.editor.getCurrentEditorState();\n\n      /// check text formats\n      expect(\n        editorState\n            .getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.bold),\n        true,\n      );\n      expect(\n        editorState\n            .getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.italic),\n        true,\n      );\n      expect(\n        editorState\n            .getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.underline),\n        true,\n      );\n    });\n  });\n\n  group('document toolbar: link', () {\n    String? getLinkFromNode(Node node) {\n      for (final insert in node.delta!) {\n        final link = insert.attributes?.href;\n        if (link != null) return link;\n      }\n      return null;\n    }\n\n    bool isPageLink(Node node) {\n      for (final insert in node.delta!) {\n        final isPage = insert.attributes?.isPage;\n        if (isPage == true) return true;\n      }\n      return false;\n    }\n\n    String getNodeText(Node node) {\n      for (final insert in node.delta!) {\n        if (insert is TextInsert) return insert.text;\n      }\n      return '';\n    }\n\n    testWidgets('insert link and remove link', (tester) async {\n      const text = 'insert link', link = 'https://test.appflowy.cloud';\n      await prepareForToolbar(tester, text);\n\n      final toolbar = find.byType(DesktopFloatingToolbar);\n      expect(toolbar, findsOneWidget);\n\n      /// tap link button to show CreateLinkMenu\n      final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);\n      await tester.tapButton(linkButton);\n      final createLinkMenu = find.byType(LinkCreateMenu);\n      expect(createLinkMenu, findsOneWidget);\n\n      /// test esc to close\n      await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n      expect(toolbar, findsNothing);\n\n      /// show toolbar again\n      await tester.editor.tapLineOfEditorAt(0);\n      await selectText(tester, text);\n      await tester.tapButton(linkButton);\n\n      /// insert link\n      final textField = find.descendant(\n        of: createLinkMenu,\n        matching: find.byType(TextFormField),\n      );\n      await tester.enterText(textField, link);\n      await tester.pumpAndSettle();\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      Node node = tester.editor.getNodeAtPath([0]);\n      expect(getLinkFromNode(node), link);\n      await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n\n      /// hover link\n      await tester.hoverOnWidget(find.byType(LinkHoverTrigger));\n      final hoverMenu = find.byType(LinkHoverMenu);\n      expect(hoverMenu, findsOneWidget);\n\n      /// copy link\n      final copyButton = find.descendant(\n        of: hoverMenu,\n        matching: find.byFlowySvg(FlowySvgs.toolbar_link_m),\n      );\n      await tester.tapButton(copyButton);\n      final clipboardContent = await getIt<ClipboardService>().getData();\n      final plainText = clipboardContent.plainText;\n      expect(plainText, link);\n\n      /// remove link\n      await tester.hoverOnWidget(find.byType(LinkHoverTrigger));\n      await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_unlink_m));\n      node = tester.editor.getNodeAtPath([0]);\n      expect(getLinkFromNode(node), null);\n    });\n\n    testWidgets('insert link and edit link', (tester) async {\n      const text = 'edit link',\n          link = 'https://test.appflowy.cloud',\n          afterText = '$text after';\n      await prepareForToolbar(tester, text);\n\n      /// tap link button to show CreateLinkMenu\n      final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);\n      await tester.tapButton(linkButton);\n\n      /// search for page and select it\n      final textField = find.descendant(\n        of: find.byType(LinkCreateMenu),\n        matching: find.byType(TextFormField),\n      );\n      await tester.enterText(textField, gettingStarted);\n      await tester.pumpAndSettle();\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n\n      Node node = tester.editor.getNodeAtPath([0]);\n      expect(isPageLink(node), true);\n      expect(getLinkFromNode(node) == link, false);\n\n      /// hover link\n      await tester.hoverOnWidget(find.byType(LinkHoverTrigger));\n\n      /// click edit button to show LinkEditMenu\n      final editButton = find.byFlowySvg(FlowySvgs.toolbar_link_edit_m);\n      await tester.tapButton(editButton);\n      final linkEditMenu = find.byType(LinkEditMenu);\n      expect(linkEditMenu, findsOneWidget);\n\n      /// change the link text\n      final titleField = find.descendant(\n        of: linkEditMenu,\n        matching: find.byType(TextFormField),\n      );\n      await tester.enterText(titleField, afterText);\n      await tester.pumpAndSettle();\n      await tester.tapButton(\n        find.descendant(of: linkEditMenu, matching: find.text(gettingStarted)),\n      );\n      final linkField = find.ancestor(\n        of: find.text(LocaleKeys.document_toolbar_linkInputHint.tr()),\n        matching: find.byType(TextFormField),\n      );\n      await tester.enterText(linkField, link);\n      await tester.pumpAndSettle();\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n\n      /// apply the change\n      final applyButton =\n          find.text(LocaleKeys.settings_appearance_documentSettings_apply.tr());\n      await tester.tapButton(applyButton);\n\n      node = tester.editor.getNodeAtPath([0]);\n      expect(isPageLink(node), false);\n      expect(getLinkFromNode(node), link);\n      expect(getNodeText(node), afterText);\n    });\n\n    testWidgets('insert link and clear link name', (tester) async {\n      const text = 'edit link', link = 'https://test.appflowy.cloud';\n      await prepareForToolbar(tester, text);\n\n      /// tap link button to show CreateLinkMenu\n      final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);\n      await tester.tapButton(linkButton);\n\n      /// search for page and select it\n      final textField = find.descendant(\n        of: find.byType(LinkCreateMenu),\n        matching: find.byType(TextFormField),\n      );\n      await tester.enterText(textField, link);\n      await tester.pumpAndSettle();\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      Node node = tester.editor.getNodeAtPath([0]);\n      expect(getLinkFromNode(node), link);\n      await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n\n      /// hover link\n      await tester.hoverOnWidget(find.byType(LinkHoverTrigger));\n\n      /// click edit button to show LinkEditMenu\n      final editButton = find.byFlowySvg(FlowySvgs.toolbar_link_edit_m);\n      await tester.tapButton(editButton);\n      final linkEditMenu = find.byType(LinkEditMenu);\n      expect(linkEditMenu, findsOneWidget);\n\n      /// clear the link name\n      final titleField = find.descendant(\n        of: linkEditMenu,\n        matching: find.byType(TextFormField),\n      );\n      await tester.enterText(titleField, '');\n      await tester.pumpAndSettle();\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      node = tester.editor.getNodeAtPath([0]);\n      expect(getNodeText(node), link);\n    });\n\n    testWidgets('insert link and clear link name and remove link',\n        (tester) async {\n      const text = 'edit link', link = 'https://test.appflowy.cloud';\n      await prepareForToolbar(tester, text);\n\n      /// tap link button to show CreateLinkMenu\n      final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);\n      await tester.tapButton(linkButton);\n\n      /// search for page and select it\n      final textField = find.descendant(\n        of: find.byType(LinkCreateMenu),\n        matching: find.byType(TextFormField),\n      );\n      await tester.enterText(textField, link);\n      await tester.pumpAndSettle();\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      Node node = tester.editor.getNodeAtPath([0]);\n      expect(getLinkFromNode(node), link);\n      await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n\n      /// hover link\n      await tester.hoverOnWidget(find.byType(LinkHoverTrigger));\n\n      /// click edit button to show LinkEditMenu\n      final editButton = find.byFlowySvg(FlowySvgs.toolbar_link_edit_m);\n      await tester.tapButton(editButton);\n      final linkEditMenu = find.byType(LinkEditMenu);\n      expect(linkEditMenu, findsOneWidget);\n\n      /// clear the link name\n      final titleField = find.descendant(\n        of: linkEditMenu,\n        matching: find.byType(TextFormField),\n      );\n      await tester.enterText(titleField, '');\n      await tester.pumpAndSettle();\n      await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_unlink_m));\n      node = tester.editor.getNodeAtPath([0]);\n      expect(getNodeText(node), link);\n      expect(getLinkFromNode(node), null);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_emoji_mart/flutter_emoji_mart.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../shared/emoji.dart';\nimport '../../shared/mock/mock_file_picker.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  setUpAll(() {\n    IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n    RecentIcons.enable = false;\n  });\n\n  tearDownAll(() {\n    RecentIcons.enable = true;\n  });\n\n  group('cover image:', () {\n    testWidgets('document cover tests', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      tester.expectToSeeNoDocumentCover();\n\n      // Hover over cover toolbar to show 'Add Cover' and 'Add Icon' buttons\n      await tester.editor.hoverOnCoverToolbar();\n\n      // Insert a document cover\n      await tester.editor.tapOnAddCover();\n      tester.expectToSeeDocumentCover(CoverType.asset);\n\n      // Hover over the cover to show the 'Change Cover' and delete buttons\n      await tester.editor.hoverOnCover();\n      tester.expectChangeCoverAndDeleteButton();\n\n      // Change cover to a solid color background\n      await tester.editor.tapOnChangeCover();\n      await tester.editor.switchSolidColorBackground();\n      await tester.editor.dismissCoverPicker();\n      tester.expectToSeeDocumentCover(CoverType.color);\n\n      // Change cover to a network image\n      const imageUrl =\n          \"https://raw.githubusercontent.com/AppFlowy-IO/AppFlowy/main/frontend/appflowy_flutter/assets/images/appflowy_launch_splash.jpg\";\n      await tester.editor.hoverOnCover();\n      await tester.editor.tapOnChangeCover();\n      await tester.editor.addNetworkImageCover(imageUrl);\n      tester.expectToSeeDocumentCover(CoverType.file);\n\n      // Remove the cover\n      await tester.editor.hoverOnCover();\n      await tester.editor.tapOnRemoveCover();\n      tester.expectToSeeNoDocumentCover();\n    });\n\n    testWidgets('document cover local image tests', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      tester.expectToSeeNoDocumentCover();\n\n      // Hover over cover toolbar to show 'Add Cover' and 'Add Icon' buttons\n      await tester.editor.hoverOnCoverToolbar();\n\n      // Insert a document cover\n      await tester.editor.tapOnAddCover();\n      tester.expectToSeeDocumentCover(CoverType.asset);\n\n      // Hover over the cover to show the 'Change Cover' and delete buttons\n      await tester.editor.hoverOnCover();\n      tester.expectChangeCoverAndDeleteButton();\n\n      // Change cover to a local image image\n      final imagePath = await rootBundle.load('assets/test/images/sample.jpeg');\n      final tempDirectory = await getTemporaryDirectory();\n      final localImagePath = p.join(tempDirectory.path, 'sample.jpeg');\n      final imageFile = File(localImagePath)\n        ..writeAsBytesSync(imagePath.buffer.asUint8List());\n\n      await tester.editor.hoverOnCover();\n      await tester.editor.tapOnChangeCover();\n\n      final uploadButton = find.findTextInFlowyText(\n        LocaleKeys.document_imageBlock_upload_label.tr(),\n      );\n      await tester.tapButton(uploadButton);\n\n      mockPickFilePaths(paths: [localImagePath]);\n      await tester.tapButtonWithName(\n        LocaleKeys.document_imageBlock_upload_placeholder.tr(),\n      );\n\n      await tester.pumpAndSettle();\n      tester.expectToSeeDocumentCover(CoverType.file);\n\n      // Remove the cover\n      await tester.editor.hoverOnCover();\n      await tester.editor.tapOnRemoveCover();\n      tester.expectToSeeNoDocumentCover();\n\n      // Test if deleteImageFromLocalStorage(localImagePath) function is called once\n      await tester.pump(kDoubleTapTimeout);\n      expect(deleteImageTestCounter, 1);\n\n      // delete temp files\n      await imageFile.delete();\n    });\n\n    testWidgets('document icon tests', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      tester.expectToSeeDocumentIcon('⭐️');\n\n      // Insert a document icon\n      await tester.editor.tapGettingStartedIcon();\n      await tester.tapEmoji('😀');\n      tester.expectToSeeDocumentIcon('😀');\n\n      // Remove the document icon from the cover toolbar\n      await tester.editor.hoverOnCoverToolbar();\n      await tester.editor.tapRemoveIconButton();\n      tester.expectToSeeDocumentIcon(null);\n\n      // Add the icon back for further testing\n      await tester.editor.hoverOnCoverToolbar();\n      await tester.editor.tapAddIconButton();\n      await tester.tapEmoji('😀');\n      tester.expectToSeeDocumentIcon('😀');\n\n      // Change the document icon\n      await tester.editor.tapOnIconWidget();\n      await tester.tapEmoji('😅');\n      tester.expectToSeeDocumentIcon('😅');\n\n      // Remove the document icon from the icon picker\n      await tester.editor.tapOnIconWidget();\n      await tester.editor.tapRemoveIconButton(isInPicker: true);\n      tester.expectToSeeDocumentIcon(null);\n    });\n\n    testWidgets('icon and cover at the same time', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      tester.expectToSeeDocumentIcon('⭐️');\n      tester.expectToSeeNoDocumentCover();\n\n      // Insert a document icon\n      await tester.editor.tapGettingStartedIcon();\n      await tester.tapEmoji('😀');\n\n      // Insert a document cover\n      await tester.editor.hoverOnCoverToolbar();\n      await tester.editor.tapOnAddCover();\n\n      // Expect to see the icon and cover at the same time\n      tester.expectToSeeDocumentIcon('😀');\n      tester.expectToSeeDocumentCover(CoverType.asset);\n\n      // Hover over the cover toolbar and see that neither icons are shown\n      await tester.editor.hoverOnCoverToolbar();\n      tester.expectToSeeEmptyDocumentHeaderToolbar();\n    });\n\n    testWidgets('shuffle icon', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.editor.tapGettingStartedIcon();\n\n      // click the shuffle button\n      await tester.tapButton(\n        find.byTooltip(LocaleKeys.emoji_random.tr()),\n      );\n      tester.expectDocumentIconNotNull();\n    });\n\n    testWidgets('change skin tone', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.editor.tapGettingStartedIcon();\n\n      final searchEmojiTextField = find.byWidgetPredicate(\n        (widget) =>\n            widget is TextField &&\n            widget.decoration!.hintText == LocaleKeys.search_label.tr(),\n      );\n      await tester.enterText(\n        searchEmojiTextField,\n        'punch',\n      );\n\n      // change skin tone\n      await tester.editor.changeEmojiSkinTone(EmojiSkinTone.dark);\n\n      // select an icon with skin tone\n      const punch = '👊🏿';\n      await tester.tapEmoji(punch);\n      tester.expectToSeeDocumentIcon(punch);\n      tester.expectViewHasIcon(\n        gettingStarted,\n        ViewLayoutPB.Document,\n        EmojiIconData.emoji(punch),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_with_database_test.dart",
    "content": "import 'package:appflowy/plugins/database/board/presentation/board_page.dart';\nimport 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';\nimport 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('database view in document', () {\n    testWidgets('insert a referenced grid', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await insertLinkedDatabase(tester, ViewLayoutPB.Grid);\n\n      // validate the referenced grid is inserted\n      expect(\n        find.descendant(\n          of: find.byType(AppFlowyEditor),\n          matching: find.byType(GridPage),\n        ),\n        findsOneWidget,\n      );\n\n      // https://github.com/AppFlowy-IO/AppFlowy/issues/3533\n      // test: the selection of editor should be clear when editing the grid\n      await tester.editor.updateSelection(\n        Selection.collapsed(\n          Position(path: [1]),\n        ),\n      );\n      final gridTextCell = find.byType(EditableTextCell).first;\n      await tester.tapButton(gridTextCell);\n\n      expect(tester.editor.getCurrentEditorState().selection, isNull);\n    });\n\n    testWidgets('insert a referenced board', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await insertLinkedDatabase(tester, ViewLayoutPB.Board);\n\n      // validate the referenced board is inserted\n      expect(\n        find.descendant(\n          of: find.byType(AppFlowyEditor),\n          matching: find.byType(DesktopBoardPage),\n        ),\n        findsOneWidget,\n      );\n    });\n\n    testWidgets('insert multiple referenced boards', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new grid\n      final id = uuid();\n      final name = '${ViewLayoutPB.Board.name}_$id';\n      await tester.createNewPageWithNameUnderParent(\n        name: name,\n        layout: ViewLayoutPB.Board,\n        openAfterCreated: false,\n      );\n      // create a new document\n      await tester.createNewPageWithNameUnderParent(\n        name: 'insert_a_reference_${ViewLayoutPB.Board.name}',\n      );\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      // insert a referenced view\n      await tester.editor.showSlashMenu();\n      await tester.editor.tapSlashMenuItemWithName(\n        ViewLayoutPB.Board.slashMenuLinkedName,\n      );\n      final referencedDatabase1 = find.descendant(\n        of: find.byType(InlineActionsHandler),\n        matching: find.findTextInFlowyText(name),\n      );\n      expect(referencedDatabase1, findsOneWidget);\n      await tester.tapButton(referencedDatabase1);\n\n      await tester.editor.tapLineOfEditorAt(1);\n      await tester.editor.showSlashMenu();\n      await tester.editor.tapSlashMenuItemWithName(\n        ViewLayoutPB.Board.slashMenuLinkedName,\n      );\n      final referencedDatabase2 = find.descendant(\n        of: find.byType(InlineActionsHandler),\n        matching: find.findTextInFlowyText(name),\n      );\n      expect(referencedDatabase2, findsOneWidget);\n      await tester.tapButton(referencedDatabase2);\n\n      expect(\n        find.descendant(\n          of: find.byType(AppFlowyEditor),\n          matching: find.byType(DesktopBoardPage),\n        ),\n        findsNWidgets(2),\n      );\n    });\n\n    testWidgets('insert a referenced calendar', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await insertLinkedDatabase(tester, ViewLayoutPB.Calendar);\n\n      // validate the referenced grid is inserted\n      expect(\n        find.descendant(\n          of: find.byType(AppFlowyEditor),\n          matching: find.byType(CalendarPage),\n        ),\n        findsOneWidget,\n      );\n    });\n\n    testWidgets('create a grid inside a document', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await createInlineDatabase(tester, ViewLayoutPB.Grid);\n\n      // validate the inline grid is created\n      expect(\n        find.descendant(\n          of: find.byType(AppFlowyEditor),\n          matching: find.byType(GridPage),\n        ),\n        findsOneWidget,\n      );\n    });\n\n    testWidgets('create a board inside a document', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await createInlineDatabase(tester, ViewLayoutPB.Board);\n\n      // validate the inline board is created\n      expect(\n        find.descendant(\n          of: find.byType(AppFlowyEditor),\n          matching: find.byType(DesktopBoardPage),\n        ),\n        findsOneWidget,\n      );\n    });\n\n    testWidgets('create a calendar inside a document', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await createInlineDatabase(tester, ViewLayoutPB.Calendar);\n\n      // validate the inline calendar is created\n      expect(\n        find.descendant(\n          of: find.byType(AppFlowyEditor),\n          matching: find.byType(CalendarPage),\n        ),\n        findsOneWidget,\n      );\n    });\n\n    testWidgets('insert a referenced grid with many rows (load more option)',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await insertLinkedDatabase(tester, ViewLayoutPB.Grid);\n\n      // validate the referenced grid is inserted\n      expect(\n        find.descendant(\n          of: find.byType(AppFlowyEditor),\n          matching: find.byType(GridPage),\n        ),\n        findsOneWidget,\n      );\n\n      // https://github.com/AppFlowy-IO/AppFlowy/issues/3533\n      // test: the selection of editor should be clear when editing the grid\n      await tester.editor.updateSelection(\n        Selection.collapsed(\n          Position(path: [1]),\n        ),\n      );\n      final gridTextCell = find.byType(EditableTextCell).first;\n      await tester.tapButton(gridTextCell);\n\n      expect(tester.editor.getCurrentEditorState().selection, isNull);\n\n      final editorScrollable = find\n          .descendant(\n            of: find.byType(AppFlowyEditor),\n            matching: find.byWidgetPredicate(\n              (w) => w is Scrollable && w.axis == Axis.vertical,\n            ),\n          )\n          .first;\n\n      // Add 100 Rows to the linked database\n      final addRowFinder = find.byType(GridAddRowButton);\n      for (var i = 0; i < 100; i++) {\n        await tester.scrollUntilVisible(\n          addRowFinder,\n          100,\n          scrollable: editorScrollable,\n        );\n        await tester.tapButton(addRowFinder);\n        await tester.pumpAndSettle();\n      }\n\n      // Since all rows visible are those we added, we should see all of them\n      expect(find.byType(GridRow), findsNWidgets(103));\n\n      // Navigate to getting started\n      await tester.openPage(gettingStarted);\n\n      // Navigate back to the document\n      await tester.openPage('insert_a_reference_${ViewLayoutPB.Grid.name}');\n\n      // We see only 25 Grid Rows\n      expect(find.byType(GridRow), findsNWidgets(25));\n\n      // We see Add row and load more button\n      expect(find.byType(GridAddRowButton), findsOneWidget);\n      expect(find.byType(GridRowLoadMoreButton), findsOneWidget);\n\n      // Load more rows, expect 50 visible\n      await _loadMoreRows(tester, editorScrollable, 50);\n\n      // Load more rows, expect 75 visible\n      await _loadMoreRows(tester, editorScrollable, 75);\n\n      // Load more rows, expect 100 visible\n      await _loadMoreRows(tester, editorScrollable, 100);\n\n      // Load more rows, expect 103 visible\n      await _loadMoreRows(tester, editorScrollable, 103);\n\n      // We no longer see load more option\n      expect(find.byType(GridRowLoadMoreButton), findsNothing);\n    });\n  });\n}\n\nFuture<void> _loadMoreRows(\n  WidgetTester tester,\n  Finder scrollable, [\n  int? expectedRows,\n]) async {\n  await tester.scrollUntilVisible(\n    find.byType(GridRowLoadMoreButton),\n    100,\n    scrollable: scrollable,\n  );\n  await tester.pumpAndSettle();\n\n  await tester.tap(find.byType(GridRowLoadMoreButton));\n  await tester.pumpAndSettle();\n\n  if (expectedRows != null) {\n    expect(find.byType(GridRow), findsNWidgets(expectedRows));\n  }\n}\n\n/// Insert a referenced database of [layout] into the document\nFuture<void> insertLinkedDatabase(\n  WidgetTester tester,\n  ViewLayoutPB layout,\n) async {\n  // create a new grid\n  final id = uuid();\n  final name = '${layout.name}_$id';\n  await tester.createNewPageWithNameUnderParent(\n    name: name,\n    layout: layout,\n    openAfterCreated: false,\n  );\n  // create a new document\n  await tester.createNewPageWithNameUnderParent(\n    name: 'insert_a_reference_${layout.name}',\n  );\n  // tap the first line of the document\n  await tester.editor.tapLineOfEditorAt(0);\n  // insert a referenced view\n  await tester.editor.showSlashMenu();\n  await tester.editor.tapSlashMenuItemWithName(\n    layout.slashMenuLinkedName,\n  );\n\n  final linkToPageMenu = find.byType(InlineActionsHandler);\n  expect(linkToPageMenu, findsOneWidget);\n  final referencedDatabase = find.descendant(\n    of: linkToPageMenu,\n    matching: find.findTextInFlowyText(name),\n  );\n  expect(referencedDatabase, findsOneWidget);\n  await tester.tapButton(referencedDatabase);\n}\n\nFuture<void> createInlineDatabase(\n  WidgetTester tester,\n  ViewLayoutPB layout,\n) async {\n  // create a new document\n  final documentName = 'insert_a_inline_${layout.name}';\n  await tester.createNewPageWithNameUnderParent(\n    name: documentName,\n  );\n  // tap the first line of the document\n  await tester.editor.tapLineOfEditorAt(0);\n  // insert a referenced view\n  await tester.editor.showSlashMenu();\n  await tester.editor.tapSlashMenuItemWithName(\n    layout.slashMenuName,\n    offset: 100,\n  );\n  await tester.pumpAndSettle();\n\n  final childViews = tester\n      .widget<SingleInnerViewItem>(tester.findPageName(documentName))\n      .view\n      .childViews;\n  expect(childViews.length, 1);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_with_date_reminder_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:table_calendar/table_calendar.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  setUp(() {\n    IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n    TestWidgetsFlutterBinding.ensureInitialized();\n  });\n\n  group('date or reminder block in document:', () {\n    testWidgets(\"insert date with time block\", (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent(\n        name: 'Date with time test',\n      );\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.showSlashMenu();\n      await tester.editor.tapSlashMenuItemWithName(\n        LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),\n      );\n\n      final dateTimeSettings = DateTimeSettingsPB(\n        dateFormat: UserDateFormatPB.Friendly,\n        timeFormat: UserTimeFormatPB.TwentyFourHour,\n      );\n      final DateTime currentDateTime = DateTime.now();\n      final String formattedDate =\n          dateTimeSettings.dateFormat.formatDate(currentDateTime, false);\n\n      // get current date in editor\n      expect(find.byType(MentionDateBlock), findsOneWidget);\n      expect(find.text('@$formattedDate'), findsOneWidget);\n\n      // tap on date field\n      await tester.tap(find.byType(MentionDateBlock));\n      await tester.pumpAndSettle();\n\n      // tap the toggle of include time\n      await tester.tap(find.byType(Toggle));\n      await tester.pumpAndSettle();\n\n      // add time 11:12\n      final textField = find\n          .descendant(\n            of: find.byType(DesktopAppFlowyDatePicker),\n            matching: find.byType(TextField),\n          )\n          .last;\n      await tester.pumpUntilFound(textField);\n      await tester.enterText(textField, \"11:12\");\n      await tester.testTextInput.receiveAction(TextInputAction.done);\n      await tester.pumpAndSettle();\n\n      // we will get field with current date and 11:12 as time\n      expect(find.byType(MentionDateBlock), findsOneWidget);\n      expect(find.text('@$formattedDate 11:12'), findsOneWidget);\n    });\n\n    testWidgets(\"insert date with reminder block\", (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent(\n        name: 'Date with reminder test',\n      );\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.showSlashMenu();\n      await tester.editor.tapSlashMenuItemWithName(\n        LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),\n      );\n\n      final dateTimeSettings = DateTimeSettingsPB(\n        dateFormat: UserDateFormatPB.Friendly,\n        timeFormat: UserTimeFormatPB.TwentyFourHour,\n      );\n      final DateTime currentDateTime = DateTime.now();\n      final String formattedDate =\n          dateTimeSettings.dateFormat.formatDate(currentDateTime, false);\n\n      // get current date in editor\n      expect(find.byType(MentionDateBlock), findsOneWidget);\n      expect(find.text('@$formattedDate'), findsOneWidget);\n\n      // tap on date field\n      await tester.tap(find.byType(MentionDateBlock));\n      await tester.pumpAndSettle();\n\n      // tap reminder and set reminder to 1 day before\n      await tester.tap(find.text(LocaleKeys.datePicker_reminderLabel.tr()));\n      await tester.pumpAndSettle();\n      await tester.tap(\n        find.textContaining(\n          LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(),\n        ),\n      );\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.pumpAndSettle();\n\n      // we will get field with current date reminder_clock.svg icon\n      expect(find.byType(MentionDateBlock), findsOneWidget);\n      expect(find.text('@$formattedDate'), findsOneWidget);\n      expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);\n    });\n\n    testWidgets(\"copy, cut and paste a date mention\", (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent(\n        name: 'copy, cut and paste a date mention',\n      );\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.showSlashMenu();\n      await tester.editor.tapSlashMenuItemWithName(\n        LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),\n      );\n\n      final dateTimeSettings = DateTimeSettingsPB(\n        dateFormat: UserDateFormatPB.Friendly,\n        timeFormat: UserTimeFormatPB.TwentyFourHour,\n      );\n      final DateTime currentDateTime = DateTime.now();\n      final String formattedDate =\n          dateTimeSettings.dateFormat.formatDate(currentDateTime, false);\n\n      // get current date in editor\n      expect(find.byType(MentionDateBlock), findsOneWidget);\n      expect(find.text('@$formattedDate'), findsOneWidget);\n\n      // update selection and copy\n      await tester.editor.updateSelection(\n        Selection(\n          start: Position(path: [0]),\n          end: Position(path: [0], offset: 1),\n        ),\n      );\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyC,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      // update selection and paste\n      await tester.editor.updateSelection(\n        Selection.collapsed(Position(path: [0], offset: 1)),\n      );\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(MentionDateBlock), findsNWidgets(2));\n      expect(find.text('@$formattedDate'), findsNWidgets(2));\n\n      // update selection and cut\n      await tester.editor.updateSelection(\n        Selection(\n          start: Position(path: [0], offset: 1),\n          end: Position(path: [0], offset: 2),\n        ),\n      );\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyX,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(MentionDateBlock), findsOneWidget);\n      expect(find.text('@$formattedDate'), findsOneWidget);\n\n      // update selection and paste\n      await tester.editor.updateSelection(\n        Selection.collapsed(Position(path: [0], offset: 1)),\n      );\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(MentionDateBlock), findsNWidgets(2));\n      expect(find.text('@$formattedDate'), findsNWidgets(2));\n    });\n\n    testWidgets(\"copy, cut and paste a reminder mention\", (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent(\n        name: 'copy, cut and paste a reminder mention',\n      );\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.showSlashMenu();\n      await tester.editor.tapSlashMenuItemWithName(\n        LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),\n      );\n\n      // trigger popup\n      await tester.tapButton(find.byType(MentionDateBlock));\n      await tester.pumpAndSettle();\n\n      // set date to be fifteenth of the last month\n      await tester.tap(\n        find.descendant(\n          of: find.byType(DesktopAppFlowyDatePicker),\n          matching: find.byFlowySvg(FlowySvgs.arrow_left_s),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.tap(\n        find.descendant(\n          of: find.byType(TableCalendar),\n          matching: find.text(15.toString()),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n      await tester.pumpAndSettle();\n\n      // add a reminder\n      await tester.tap(find.byType(MentionDateBlock));\n      await tester.pumpAndSettle();\n      await tester.tap(find.text(LocaleKeys.datePicker_reminderLabel.tr()));\n      await tester.pumpAndSettle();\n      await tester.tap(\n        find.textContaining(\n          LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n      await tester.pumpAndSettle();\n\n      // verify\n      final dateTimeSettings = DateTimeSettingsPB(\n        dateFormat: UserDateFormatPB.Friendly,\n        timeFormat: UserTimeFormatPB.TwentyFourHour,\n      );\n      final now = DateTime.now();\n      final fifteenthOfLastMonth = DateTime(now.year, now.month - 1, 15);\n      final formattedDate =\n          dateTimeSettings.dateFormat.formatDate(fifteenthOfLastMonth, false);\n\n      expect(find.byType(MentionDateBlock), findsOneWidget);\n      expect(find.text('@$formattedDate'), findsOneWidget);\n      expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);\n      expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);\n\n      // update selection and copy\n      await tester.editor.updateSelection(\n        Selection(\n          start: Position(path: [0]),\n          end: Position(path: [0], offset: 1),\n        ),\n      );\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyC,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      // update selection and paste\n      await tester.editor.updateSelection(\n        Selection.collapsed(Position(path: [0], offset: 1)),\n      );\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(MentionDateBlock), findsNWidgets(2));\n      expect(find.text('@$formattedDate'), findsNWidgets(2));\n      expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNWidgets(2));\n      expect(\n        getIt<ReminderBloc>().state.reminders.map((e) => e.id).toSet().length,\n        2,\n      );\n\n      // update selection and cut\n      await tester.editor.updateSelection(\n        Selection(\n          start: Position(path: [0], offset: 1),\n          end: Position(path: [0], offset: 2),\n        ),\n      );\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyX,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(MentionDateBlock), findsOneWidget);\n      expect(find.text('@$formattedDate'), findsOneWidget);\n      expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);\n      expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);\n\n      // update selection and paste\n      await tester.editor.updateSelection(\n        Selection.collapsed(Position(path: [0], offset: 1)),\n      );\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n        isMetaPressed: Platform.isMacOS,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(MentionDateBlock), findsNWidgets(2));\n      expect(find.text('@$formattedDate'), findsNWidgets(2));\n      expect(find.byType(MentionDateBlock), findsNWidgets(2));\n      expect(find.text('@$formattedDate'), findsNWidgets(2));\n      expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNWidgets(2));\n      expect(\n        getIt<ReminderBloc>().state.reminders.map((e) => e.id).toSet().length,\n        2,\n      );\n    });\n\n    testWidgets(\"delete, undo and redo a reminder mention\", (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent(\n        name: 'delete, undo and redo a reminder mention',\n      );\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.showSlashMenu();\n      await tester.editor.tapSlashMenuItemWithName(\n        LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),\n      );\n\n      // trigger popup\n      await tester.tapButton(find.byType(MentionDateBlock));\n      await tester.pumpAndSettle();\n\n      // set date to be fifteenth of the last month\n      await tester.tap(\n        find.descendant(\n          of: find.byType(DesktopAppFlowyDatePicker),\n          matching: find.byFlowySvg(FlowySvgs.arrow_left_s),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.tap(\n        find.descendant(\n          of: find.byType(TableCalendar),\n          matching: find.text(15.toString()),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n      await tester.pumpAndSettle();\n\n      // add a reminder\n      await tester.tap(find.byType(MentionDateBlock));\n      await tester.pumpAndSettle();\n      await tester.tap(find.text(LocaleKeys.datePicker_reminderLabel.tr()));\n      await tester.pumpAndSettle();\n      await tester.tap(\n        find.textContaining(\n          LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(),\n        ),\n      );\n      await tester.pumpAndSettle();\n      await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n      await tester.pumpAndSettle();\n\n      // verify\n      final dateTimeSettings = DateTimeSettingsPB(\n        dateFormat: UserDateFormatPB.Friendly,\n        timeFormat: UserTimeFormatPB.TwentyFourHour,\n      );\n      final now = DateTime.now();\n      final fifteenthOfLastMonth = DateTime(now.year, now.month - 1, 15);\n      final formattedDate =\n          dateTimeSettings.dateFormat.formatDate(fifteenthOfLastMonth, false);\n\n      expect(find.byType(MentionDateBlock), findsOneWidget);\n      expect(find.text('@$formattedDate'), findsOneWidget);\n      expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);\n      expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);\n\n      // update selection and backspace to delete the mention\n      await tester.editor.updateSelection(\n        Selection.collapsed(Position(path: [0], offset: 1)),\n      );\n      await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);\n      await tester.pumpAndSettle();\n\n      expect(find.byType(MentionDateBlock), findsNothing);\n      expect(find.text('@$formattedDate'), findsNothing);\n      expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNothing);\n      expect(getIt<ReminderBloc>().state.reminders.isEmpty, isTrue);\n\n      // undo\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyZ,\n        isControlPressed: Platform.isWindows || Platform.isLinux,\n        isMetaPressed: Platform.isMacOS,\n      );\n\n      expect(find.byType(MentionDateBlock), findsOneWidget);\n      expect(find.text('@$formattedDate'), findsOneWidget);\n      expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget);\n      expect(getIt<ReminderBloc>().state.reminders.map((e) => e.id).length, 1);\n\n      // redo\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyZ,\n        isControlPressed: Platform.isWindows || Platform.isLinux,\n        isMetaPressed: Platform.isMacOS,\n        isShiftPressed: true,\n      );\n\n      expect(find.byType(MentionDateBlock), findsNothing);\n      expect(find.text('@$formattedDate'), findsNothing);\n      expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNothing);\n      expect(getIt<ReminderBloc>().state.reminders.isEmpty, isTrue);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_with_file_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/services.dart';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../shared/mock/mock_file_picker.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('file block in document', () {\n    testWidgets('insert a file from local file + rename file', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent(name: 'Insert file test');\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.showSlashMenu();\n      await tester.editor.tapSlashMenuItemWithName(\n        LocaleKeys.document_slashMenu_name_file.tr(),\n      );\n      expect(find.byType(FileBlockComponent), findsOneWidget);\n      expect(find.byType(FileUploadMenu), findsOneWidget);\n\n      final image = await rootBundle.load('assets/test/images/sample.jpeg');\n      final tempDirectory = await getTemporaryDirectory();\n      final filePath = p.join(tempDirectory.path, 'sample.jpeg');\n      final file = File(filePath)..writeAsBytesSync(image.buffer.asUint8List());\n\n      mockPickFilePaths(paths: [filePath]);\n\n      await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');\n      await tester.tapFileUploadHint();\n      await tester.pumpAndSettle();\n\n      expect(find.byType(FileUploadMenu), findsNothing);\n      expect(find.byType(FileBlockComponent), findsOneWidget);\n\n      final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;\n      expect(node.type, FileBlockKeys.type);\n      expect(node.attributes[FileBlockKeys.url], isNotEmpty);\n      expect(\n        node.attributes[FileBlockKeys.urlType],\n        FileUrlType.local.toIntValue(),\n      );\n\n      // Check the name of the file is correctly extracted\n      expect(node.attributes[FileBlockKeys.name], 'sample.jpeg');\n      expect(find.text('sample.jpeg'), findsOneWidget);\n\n      const newName = \"Renamed file\";\n\n      // Hover on the widget to see the three dots to open FileBlockMenu\n      await tester.hoverOnWidget(\n        find.byType(FileBlockComponent),\n        onHover: () async {\n          await tester.tap(find.byType(FileMenuTrigger));\n          await tester.pumpAndSettle();\n\n          await tester.tap(\n            find.text(LocaleKeys.document_plugins_file_renameFile_title.tr()),\n          );\n        },\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(FlowyTextField), findsOneWidget);\n      await tester.enterText(find.byType(FlowyTextField), newName);\n      await tester.pump();\n\n      await tester.tap(find.text(LocaleKeys.button_save.tr()));\n      await tester.pumpAndSettle();\n\n      final updatedNode =\n          tester.editor.getCurrentEditorState().getNodeAtPath([0])!;\n      expect(updatedNode.attributes[FileBlockKeys.name], newName);\n      expect(find.text(newName), findsOneWidget);\n\n      // remove the temp file\n      file.deleteSync();\n    });\n\n    testWidgets('insert a file from network', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent(name: 'Insert file test');\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.showSlashMenu();\n      await tester.editor.tapSlashMenuItemWithName(\n        LocaleKeys.document_slashMenu_name_file.tr(),\n      );\n      expect(find.byType(FileBlockComponent), findsOneWidget);\n      expect(find.byType(FileUploadMenu), findsOneWidget);\n\n      // Navigate to integrate link tab\n      await tester.tapButtonWithName(\n        LocaleKeys.document_plugins_file_networkTab.tr(),\n      );\n      await tester.pumpAndSettle();\n\n      const url =\n          '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';\n      await tester.enterText(\n        find.descendant(\n          of: find.byType(FileUploadMenu),\n          matching: find.byType(FlowyTextField),\n        ),\n        url,\n      );\n      await tester.tapButton(\n        find.descendant(\n          of: find.byType(FileUploadMenu),\n          matching: find.text(\n            LocaleKeys.document_plugins_file_networkAction.tr(),\n            findRichText: true,\n          ),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(FileUploadMenu), findsNothing);\n      expect(find.byType(FileBlockComponent), findsOneWidget);\n\n      final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;\n      expect(node.type, FileBlockKeys.type);\n      expect(node.attributes[FileBlockKeys.url], isNotEmpty);\n      expect(\n        node.attributes[FileBlockKeys.urlType],\n        FileUrlType.network.toIntValue(),\n      );\n\n      // Check the name is correctly extracted from the url\n      expect(\n        node.attributes[FileBlockKeys.name],\n        'photo-1469474968028-56623f02e42e',\n      );\n      expect(find.text('photo-1469474968028-56623f02e42e'), findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_with_image_block_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart'\n    hide UploadImageMenu, ResizableImage;\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../shared/mock/mock_file_picker.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('image block in document', () {\n    testWidgets('insert an image from local file', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent(\n        name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),\n      );\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.showSlashMenu();\n      await tester.editor.tapSlashMenuItemWithName(\n        LocaleKeys.document_slashMenu_name_image.tr(),\n      );\n      expect(find.byType(CustomImageBlockComponent), findsOneWidget);\n      expect(find.byType(ImagePlaceholder), findsOneWidget);\n      expect(\n        find.descendant(\n          of: find.byType(ImagePlaceholder),\n          matching: find.byType(AppFlowyPopover),\n        ),\n        findsOneWidget,\n      );\n      expect(find.byType(UploadImageMenu), findsOneWidget);\n\n      final image = await rootBundle.load('assets/test/images/sample.jpeg');\n      final tempDirectory = await getTemporaryDirectory();\n      final imagePath = p.join(tempDirectory.path, 'sample.jpeg');\n      final file = File(imagePath)\n        ..writeAsBytesSync(image.buffer.asUint8List());\n\n      mockPickFilePaths(\n        paths: [imagePath],\n      );\n\n      await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');\n      await tester.tapButtonWithName(\n        LocaleKeys.document_imageBlock_upload_placeholder.tr(),\n      );\n      await tester.pumpAndSettle();\n      expect(find.byType(ResizableImage), findsOneWidget);\n      final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;\n      expect(node.type, ImageBlockKeys.type);\n      expect(node.attributes[ImageBlockKeys.url], isNotEmpty);\n\n      // remove the temp file\n      file.deleteSync();\n    });\n\n    testWidgets('insert two images from local file at once', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent(\n        name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),\n      );\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.showSlashMenu();\n      await tester.editor.tapSlashMenuItemWithName(\n        LocaleKeys.document_slashMenu_name_image.tr(),\n      );\n      expect(find.byType(CustomImageBlockComponent), findsOneWidget);\n      expect(find.byType(ImagePlaceholder), findsOneWidget);\n      expect(\n        find.descendant(\n          of: find.byType(ImagePlaceholder),\n          matching: find.byType(AppFlowyPopover),\n        ),\n        findsOneWidget,\n      );\n      expect(find.byType(UploadImageMenu), findsOneWidget);\n\n      final firstImage =\n          await rootBundle.load('assets/test/images/sample.jpeg');\n      final secondImage =\n          await rootBundle.load('assets/test/images/sample.gif');\n      final tempDirectory = await getTemporaryDirectory();\n\n      final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');\n      final firstFile = File(firstImagePath)\n        ..writeAsBytesSync(firstImage.buffer.asUint8List());\n\n      final secondImagePath = p.join(tempDirectory.path, 'sample.gif');\n      final secondFile = File(secondImagePath)\n        ..writeAsBytesSync(secondImage.buffer.asUint8List());\n\n      mockPickFilePaths(paths: [firstImagePath, secondImagePath]);\n\n      await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');\n      await tester.tapButtonWithName(\n        LocaleKeys.document_imageBlock_upload_placeholder.tr(),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(ResizableImage), findsNWidgets(2));\n\n      final firstNode =\n          tester.editor.getCurrentEditorState().getNodeAtPath([0])!;\n      expect(firstNode.type, ImageBlockKeys.type);\n      expect(firstNode.attributes[ImageBlockKeys.url], isNotEmpty);\n\n      final secondNode =\n          tester.editor.getCurrentEditorState().getNodeAtPath([0])!;\n      expect(secondNode.type, ImageBlockKeys.type);\n      expect(secondNode.attributes[ImageBlockKeys.url], isNotEmpty);\n\n      // remove the temp files\n      await Future.wait([firstFile.delete(), secondFile.delete()]);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('inline math equation in document', () {\n    testWidgets('insert an inline math equation', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent(\n        name: 'math equation',\n      );\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      // insert a inline page\n      const formula = 'E = MC ^ 2';\n      await tester.ime.insertText(formula);\n      await tester.editor.updateSelection(\n        Selection.single(path: [0], startOffset: 0, endOffset: formula.length),\n      );\n\n      // tap the more options button\n      final moreOptionButton = find.findFlowyTooltip(\n        LocaleKeys.document_toolbar_moreOptions.tr(),\n      );\n      await tester.tapButton(moreOptionButton);\n\n      // tap the inline math equation button\n      final inlineMathEquationButton = find.text(\n        LocaleKeys.document_toolbar_equation.tr(),\n      );\n      await tester.tapButton(inlineMathEquationButton);\n\n      // expect to see the math equation block\n      final inlineMathEquation = find.byType(InlineMathEquation);\n      expect(inlineMathEquation, findsOneWidget);\n\n      // tap it and update the content\n      await tester.tapButton(inlineMathEquation);\n      final textFormField = find.descendant(\n        of: find.byType(MathInputTextField),\n        matching: find.byType(TextFormField),\n      );\n      const newFormula = 'E = MC ^ 3';\n      await tester.enterText(textFormField, newFormula);\n      await tester.tapButton(\n        find.descendant(\n          of: find.byType(MathInputTextField),\n          matching: find.byType(FlowyButton),\n        ),\n      );\n      await tester.pumpAndSettle();\n    });\n\n    testWidgets('remove the inline math equation format', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent(\n        name: 'math equation',\n      );\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      // insert a inline page\n      const formula = 'E = MC ^ 2';\n      await tester.ime.insertText(formula);\n      await tester.editor.updateSelection(\n        Selection.single(path: [0], startOffset: 0, endOffset: formula.length),\n      );\n\n      // tap the more options button\n      final moreOptionButton = find.findFlowyTooltip(\n        LocaleKeys.document_toolbar_moreOptions.tr(),\n      );\n      await tester.tapButton(moreOptionButton);\n\n      // tap the inline math equation button\n      final inlineMathEquationButton =\n          find.byFlowySvg(FlowySvgs.type_formula_m);\n      await tester.tapButton(inlineMathEquationButton);\n\n      // expect to see the math equation block\n      var inlineMathEquation = find.byType(InlineMathEquation);\n      expect(inlineMathEquation, findsOneWidget);\n\n      // highlight the math equation block\n      await tester.editor.updateSelection(\n        Selection.single(path: [0], startOffset: 0, endOffset: 1),\n      );\n\n      await tester.tapButton(moreOptionButton);\n\n      // cancel the format\n      await tester.tapButton(inlineMathEquationButton);\n\n      // expect to see the math equation block is removed\n      inlineMathEquation = find.byType(InlineMathEquation);\n      expect(inlineMathEquation, findsNothing);\n\n      tester.expectToSeeText(formula);\n    });\n\n    testWidgets('insert a inline math equation and type something after it',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent(\n        name: 'math equation',\n      );\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      // insert a inline page\n      const formula = 'E = MC ^ 2';\n      await tester.ime.insertText(formula);\n      await tester.editor.updateSelection(\n        Selection.single(path: [0], startOffset: 0, endOffset: formula.length),\n      );\n\n      // tap the more options button\n      final moreOptionButton = find.findFlowyTooltip(\n        LocaleKeys.document_toolbar_moreOptions.tr(),\n      );\n      await tester.tapButton(moreOptionButton);\n\n      // tap the inline math equation button\n      final inlineMathEquationButton =\n          find.byFlowySvg(FlowySvgs.type_formula_m);\n      await tester.tapButton(inlineMathEquationButton);\n\n      // expect to see the math equation block\n      final inlineMathEquation = find.byType(InlineMathEquation);\n      expect(inlineMathEquation, findsOneWidget);\n\n      await tester.editor.tapLineOfEditorAt(0);\n      const text = 'Hello World';\n      await tester.ime.insertText(text);\n\n      final inlineText = find.textContaining(text, findRichText: true);\n      expect(inlineText, findsOneWidget);\n\n      // the text should be in the same line with the math equation\n      final inlineMathEquationPosition = tester.getRect(inlineMathEquation);\n      final textPosition = tester.getRect(inlineText);\n      // allow 5px difference\n      expect(\n        (textPosition.top - inlineMathEquationPosition.top).abs(),\n        lessThan(5),\n      );\n      expect(\n        (textPosition.bottom - inlineMathEquationPosition.bottom).abs(),\n        lessThan(5),\n      );\n    });\n\n    testWidgets('insert inline math equation by shortcut', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent(\n        name: 'insert inline math equation by shortcut',\n      );\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      // insert a inline page\n      const formula = 'E = MC ^ 2';\n      await tester.ime.insertText(formula);\n      await tester.editor.updateSelection(\n        Selection.single(path: [0], startOffset: 0, endOffset: formula.length),\n      );\n\n      // mock key event\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyE,\n        isShiftPressed: true,\n        isControlPressed: true,\n      );\n\n      // expect to see the math equation block\n      final inlineMathEquation = find.byType(InlineMathEquation);\n      expect(inlineMathEquation, findsOneWidget);\n\n      await tester.editor.tapLineOfEditorAt(0);\n      const text = 'Hello World';\n      await tester.ime.insertText(text);\n\n      final inlineText = find.textContaining(text, findRichText: true);\n      expect(inlineText, findsOneWidget);\n\n      // the text should be in the same line with the math equation\n      final inlineMathEquationPosition = tester.getRect(inlineMathEquation);\n      final textPosition = tester.getRect(inlineText);\n      // allow 5px difference\n      expect(\n        (textPosition.top - inlineMathEquationPosition.top).abs(),\n        lessThan(5),\n      );\n      expect(\n        (textPosition.bottom - inlineMathEquationPosition.bottom).abs(),\n        lessThan(5),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_page_test.dart",
    "content": "import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('inline page view in document', () {\n    testWidgets('insert a inline page - grid', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await insertInlinePage(tester, ViewLayoutPB.Grid);\n\n      final mentionBlock = find.byType(MentionPageBlock);\n      expect(mentionBlock, findsOneWidget);\n      await tester.tapButton(mentionBlock);\n    });\n\n    testWidgets('insert a inline page - board', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await insertInlinePage(tester, ViewLayoutPB.Board);\n\n      final mentionBlock = find.byType(MentionPageBlock);\n      expect(mentionBlock, findsOneWidget);\n      await tester.tapButton(mentionBlock);\n    });\n\n    testWidgets('insert a inline page - calendar', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await insertInlinePage(tester, ViewLayoutPB.Calendar);\n\n      final mentionBlock = find.byType(MentionPageBlock);\n      expect(mentionBlock, findsOneWidget);\n      await tester.tapButton(mentionBlock);\n    });\n\n    testWidgets('insert a inline page - document', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await insertInlinePage(tester, ViewLayoutPB.Document);\n\n      final mentionBlock = find.byType(MentionPageBlock);\n      expect(mentionBlock, findsOneWidget);\n      await tester.tapButton(mentionBlock);\n    });\n\n    testWidgets('insert a inline page and rename it', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      final pageName = await insertInlinePage(tester, ViewLayoutPB.Document);\n\n      // rename\n      const newName = 'RenameToNewPageName';\n      await tester.hoverOnPageName(\n        pageName,\n        onHover: () async => tester.renamePage(newName),\n      );\n      final finder = find.descendant(\n        of: find.byType(MentionPageBlock),\n        matching: find.findTextInFlowyText(newName),\n      );\n      expect(finder, findsOneWidget);\n    });\n\n    testWidgets('insert a inline page and delete it', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      final pageName = await insertInlinePage(tester, ViewLayoutPB.Grid);\n\n      // rename\n      await tester.hoverOnPageName(\n        pageName,\n        layout: ViewLayoutPB.Grid,\n        onHover: () async => tester.tapDeletePageButton(),\n      );\n      final finder = find.descendant(\n        of: find.byType(MentionPageBlock),\n        matching: find.findTextInFlowyText(pageName),\n      );\n      expect(finder, findsOneWidget);\n      await tester.tapButton(finder);\n      expect(find.byType(GridPage), findsOneWidget);\n    });\n\n    testWidgets('insert a inline page and type something after the page',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await insertInlinePage(tester, ViewLayoutPB.Grid);\n\n      await tester.editor.tapLineOfEditorAt(0);\n      const text = 'Hello World';\n      await tester.ime.insertText(text);\n\n      expect(find.textContaining(text, findRichText: true), findsOneWidget);\n    });\n  });\n}\n\n/// Insert a referenced database of [layout] into the document\nFuture<String> insertInlinePage(\n  WidgetTester tester,\n  ViewLayoutPB layout,\n) async {\n  // create a new grid\n  final id = uuid();\n  final name = '${layout.name}_$id';\n  await tester.createNewPageWithNameUnderParent(\n    name: name,\n    layout: layout,\n    openAfterCreated: false,\n  );\n\n  // create a new document\n  await tester.createNewPageWithNameUnderParent(\n    name: 'insert_a_inline_page_${layout.name}',\n  );\n\n  // tap the first line of the document\n  await tester.editor.tapLineOfEditorAt(0);\n\n  // insert a inline page\n  await tester.editor.showAtMenu();\n  await tester.editor.tapAtMenuItemWithName(name);\n\n  return name;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_with_link_test.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('test editing link in document', () {\n    late MockUrlLauncher mock;\n\n    setUp(() {\n      mock = MockUrlLauncher();\n      UrlLauncherPlatform.instance = mock;\n    });\n\n    testWidgets('insert/edit/open link', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent();\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      // insert a inline page\n      const link = 'AppFlowy';\n      await tester.ime.insertText(link);\n      await tester.editor.updateSelection(\n        Selection.single(path: [0], startOffset: 0, endOffset: link.length),\n      );\n\n      // tap the link button\n      final linkButton = find.byTooltip(\n        'Link',\n      );\n      await tester.tapButton(linkButton);\n      expect(find.text('Add your link', findRichText: true), findsOneWidget);\n\n      // input the link\n      const url = 'https://appflowy.io';\n      final textField = find.byWidgetPredicate(\n        (widget) => widget is TextField && widget.decoration!.hintText == 'URL',\n      );\n      await tester.enterText(textField, url);\n      await tester.testTextInput.receiveAction(TextInputAction.done);\n      await tester.pumpAndSettle();\n\n      // single-click the link menu to show the menu\n      await tester.tapButton(find.text(link, findRichText: true));\n      expect(find.text('Open link', findRichText: true), findsOneWidget);\n      expect(find.text('Copy link', findRichText: true), findsOneWidget);\n      expect(find.text('Remove link', findRichText: true), findsOneWidget);\n\n      // double-click the link menu to open the link\n      mock\n        ..setLaunchExpectations(\n          url: url,\n          useSafariVC: false,\n          useWebView: false,\n          universalLinksOnly: false,\n          enableJavaScript: true,\n          enableDomStorage: true,\n          headers: <String, String>{},\n          webOnlyWindowName: null,\n          launchMode: PreferredLaunchMode.platformDefault,\n        )\n        ..setResponse(true);\n\n      await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n      await tester.doubleTapAt(\n        tester.getTopLeft(find.text(link, findRichText: true)).translate(5, 5),\n      );\n      expect(mock.canLaunchCalled, isTrue);\n      expect(mock.launchCalled, isTrue);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_with_multi_image_block_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/image_render.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_placeholder.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_toolbar.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../shared/mock/mock_file_picker.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  setUp(() {\n    IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n    TestWidgetsFlutterBinding.ensureInitialized();\n  });\n\n  group('multi image block in document', () {\n    testWidgets('insert images from local and use interactive viewer',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent(\n        name: 'multi image block test',\n      );\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.showSlashMenu();\n      await tester.editor.tapSlashMenuItemWithName(\n        LocaleKeys.document_slashMenu_name_photoGallery.tr(),\n        offset: 100,\n      );\n      expect(find.byType(MultiImageBlockComponent), findsOneWidget);\n      expect(find.byType(MultiImagePlaceholder), findsOneWidget);\n\n      await tester.tap(find.byType(MultiImagePlaceholder));\n      await tester.pumpAndSettle();\n\n      expect(find.byType(UploadImageMenu), findsOneWidget);\n\n      final firstImage =\n          await rootBundle.load('assets/test/images/sample.jpeg');\n      final secondImage =\n          await rootBundle.load('assets/test/images/sample.gif');\n      final tempDirectory = await getTemporaryDirectory();\n\n      final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');\n      final firstFile = File(firstImagePath)\n        ..writeAsBytesSync(firstImage.buffer.asUint8List());\n\n      final secondImagePath = p.join(tempDirectory.path, 'sample.gif');\n      final secondFile = File(secondImagePath)\n        ..writeAsBytesSync(secondImage.buffer.asUint8List());\n\n      mockPickFilePaths(paths: [firstImagePath, secondImagePath]);\n\n      await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');\n      await tester.tapButtonWithName(\n        LocaleKeys.document_imageBlock_upload_placeholder.tr(),\n      );\n      await tester.pumpAndSettle();\n      expect(find.byType(ImageBrowserLayout), findsOneWidget);\n      final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;\n      expect(node.type, MultiImageBlockKeys.type);\n\n      final data = MultiImageData.fromJson(\n        node.attributes[MultiImageBlockKeys.images],\n      );\n\n      expect(data.images.length, 2);\n\n      // Start using the interactive viewer to view the image(s)\n      final imageFinder = find\n          .byWidgetPredicate(\n            (w) =>\n                w is Image &&\n                w.image is FileImage &&\n                (w.image as FileImage).file.path.endsWith('.jpeg'),\n          )\n          .first;\n      await tester.tap(imageFinder);\n      await tester.pump(kDoubleTapMinTime);\n      await tester.tap(imageFinder);\n      await tester.pumpAndSettle();\n\n      final ivFinder = find.byType(InteractiveImageViewer);\n      expect(ivFinder, findsOneWidget);\n\n      // go to next image\n      await tester.tap(find.byFlowySvg(FlowySvgs.arrow_right_s));\n      await tester.pumpAndSettle();\n\n      // Expect image to end with .gif\n      final gifImageFinder = find.byWidgetPredicate(\n        (w) =>\n            w is Image &&\n            w.image is FileImage &&\n            (w.image as FileImage).file.path.endsWith('.gif'),\n      );\n\n      gifImageFinder.evaluate();\n      expect(gifImageFinder.found.length, 2);\n\n      // go to previous image\n      await tester.tap(find.byFlowySvg(FlowySvgs.arrow_left_s));\n      await tester.pumpAndSettle();\n\n      gifImageFinder.evaluate();\n      expect(gifImageFinder.found.length, 1);\n\n      // remove the temp files\n      await Future.wait([firstFile.delete(), secondFile.delete()]);\n    });\n\n    testWidgets('insert and delete images from network', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent(\n        name: 'multi image block test',\n      );\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.showSlashMenu();\n      await tester.editor.tapSlashMenuItemWithName(\n        LocaleKeys.document_slashMenu_name_photoGallery.tr(),\n        offset: 100,\n      );\n      expect(find.byType(MultiImageBlockComponent), findsOneWidget);\n      expect(find.byType(MultiImagePlaceholder), findsOneWidget);\n\n      await tester.tap(find.byType(MultiImagePlaceholder));\n      await tester.pumpAndSettle();\n\n      expect(find.byType(UploadImageMenu), findsOneWidget);\n\n      await tester.tapButtonWithName(\n        LocaleKeys.document_imageBlock_embedLink_label.tr(),\n      );\n\n      const url =\n          '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';\n      await tester.enterText(\n        find.descendant(\n          of: find.byType(EmbedImageUrlWidget),\n          matching: find.byType(TextField),\n        ),\n        url,\n      );\n      await tester.pumpAndSettle();\n\n      await tester.tapButton(\n        find.descendant(\n          of: find.byType(EmbedImageUrlWidget),\n          matching: find.text(\n            LocaleKeys.document_imageBlock_embedLink_label.tr(),\n            findRichText: true,\n          ),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(ImageBrowserLayout), findsOneWidget);\n      final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;\n      expect(node.type, MultiImageBlockKeys.type);\n\n      final data = MultiImageData.fromJson(\n        node.attributes[MultiImageBlockKeys.images],\n      );\n\n      expect(data.images.length, 1);\n\n      final imageFinder = find\n          .byWidgetPredicate(\n            (w) => w is FlowyNetworkImage && w.url == url,\n          )\n          .first;\n\n      // Insert two images from network\n      for (int i = 0; i < 2; i++) {\n        // Hover on the image to show the image toolbar\n        await tester.hoverOnWidget(\n          imageFinder,\n          onHover: () async {\n            // Click on the add\n            final addFinder = find.descendant(\n              of: find.byType(MultiImageMenu),\n              matching: find.byFlowySvg(FlowySvgs.add_s),\n            );\n\n            expect(addFinder, findsOneWidget);\n            await tester.tap(addFinder);\n            await tester.pumpAndSettle();\n\n            await tester.tapButtonWithName(\n              LocaleKeys.document_imageBlock_embedLink_label.tr(),\n            );\n\n            await tester.enterText(\n              find.descendant(\n                of: find.byType(EmbedImageUrlWidget),\n                matching: find.byType(TextField),\n              ),\n              url,\n            );\n            await tester.pumpAndSettle();\n\n            await tester.tapButton(\n              find.descendant(\n                of: find.byType(EmbedImageUrlWidget),\n                matching: find.text(\n                  LocaleKeys.document_imageBlock_embedLink_label.tr(),\n                  findRichText: true,\n                ),\n              ),\n            );\n            await tester.pumpAndSettle();\n          },\n        );\n      }\n\n      await tester.pumpAndSettle();\n\n      // There should be 4 images visible now, where 2 are thumbnails\n      expect(find.byType(ThumbnailItem), findsNWidgets(3));\n\n      // And all three use ImageRender\n      expect(find.byType(ImageRender), findsNWidgets(4));\n\n      // Hover on and delete the first thumbnail image\n      await tester.hoverOnWidget(find.byType(ThumbnailItem).first);\n\n      final deleteFinder = find\n          .descendant(\n            of: find.byType(ThumbnailItem),\n            matching: find.byFlowySvg(FlowySvgs.delete_s),\n          )\n          .first;\n\n      expect(deleteFinder, findsOneWidget);\n      await tester.tap(deleteFinder);\n      await tester.pumpAndSettle();\n\n      expect(find.byType(ImageRender), findsNWidgets(3));\n\n      // Delete one from interactive viewer\n      await tester.tap(imageFinder);\n      await tester.pump(kDoubleTapMinTime);\n      await tester.tap(imageFinder);\n      await tester.pumpAndSettle();\n\n      final ivFinder = find.byType(InteractiveImageViewer);\n      expect(ivFinder, findsOneWidget);\n\n      await tester.tap(\n        find.descendant(\n          of: find.byType(InteractiveImageToolbar),\n          matching: find.byFlowySvg(FlowySvgs.delete_s),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(InteractiveImageViewer), findsNothing);\n\n      // There should be 1 image and the thumbnail for said image still visible\n      expect(find.byType(ImageRender), findsNWidgets(2));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_with_outline_block_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nconst String heading1 = \"Heading 1\";\nconst String heading2 = \"Heading 2\";\nconst String heading3 = \"Heading 3\";\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('outline block test', () {\n    testWidgets('insert an outline block', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(\n        name: 'outline_test',\n      );\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await insertOutlineInDocument(tester);\n\n      // validate the outline is inserted\n      expect(find.byType(OutlineBlockWidget), findsOneWidget);\n    });\n\n    testWidgets('insert an outline block and check if headings are visible',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(\n        name: 'outline_test',\n      );\n\n      await insertHeadingComponent(tester);\n      /* Results in:\n      * # Heading 1\n      * ## Heading 2\n      * ### Heading 3\n      * > # Heading 1\n      * > ## Heading 2\n      * > ### Heading 3\n      */\n\n      await tester.editor.tapLineOfEditorAt(3);\n      await insertOutlineInDocument(tester);\n\n      expect(\n        find.descendant(\n          of: find.byType(OutlineBlockWidget),\n          matching: find.text(heading1),\n        ),\n        findsNWidgets(2),\n      );\n\n      // Heading 2 is prefixed with a bullet\n      expect(\n        find.descendant(\n          of: find.byType(OutlineBlockWidget),\n          matching: find.text(heading2),\n        ),\n        findsNWidgets(2),\n      );\n\n      // Heading 3 is prefixed with a dash\n      expect(\n        find.descendant(\n          of: find.byType(OutlineBlockWidget),\n          matching: find.text(heading3),\n        ),\n        findsNWidgets(2),\n      );\n\n      // update the Heading 1 to Heading 1Hello world\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.ime.insertText('Hello world');\n      expect(\n        find.descendant(\n          of: find.byType(OutlineBlockWidget),\n          matching: find.text('${heading1}Hello world'),\n        ),\n        findsOneWidget,\n      );\n    });\n\n    testWidgets(\"control the depth of outline block\", (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(\n        name: 'outline_test',\n      );\n\n      await insertHeadingComponent(tester);\n      /* Results in:\n        * # Heading 1\n        * ## Heading 2\n        * ### Heading 3\n        * > # Heading 1\n        * > ## Heading 2\n        * > ### Heading 3\n      */\n\n      await tester.editor.tapLineOfEditorAt(7);\n      await insertOutlineInDocument(tester);\n\n      // expect to find only the `heading1` widget under the [OutlineBlockWidget]\n      await hoverAndClickDepthOptionAction(tester, [6], 1);\n      expect(\n        find.descendant(\n          of: find.byType(OutlineBlockWidget),\n          matching: find.text(heading2),\n        ),\n        findsNothing,\n      );\n      expect(\n        find.descendant(\n          of: find.byType(OutlineBlockWidget),\n          matching: find.text(heading3),\n        ),\n        findsNothing,\n      );\n      //////\n\n      /// expect to find only the 'heading1' and 'heading2' under the [OutlineBlockWidget]\n      await hoverAndClickDepthOptionAction(tester, [6], 2);\n      expect(\n        find.descendant(\n          of: find.byType(OutlineBlockWidget),\n          matching: find.text(heading3),\n        ),\n        findsNothing,\n      );\n      //////\n\n      // expect to find all the headings under the [OutlineBlockWidget]\n      await hoverAndClickDepthOptionAction(tester, [6], 3);\n      expect(\n        find.descendant(\n          of: find.byType(OutlineBlockWidget),\n          matching: find.text(heading1),\n        ),\n        findsNWidgets(2),\n      );\n\n      expect(\n        find.descendant(\n          of: find.byType(OutlineBlockWidget),\n          matching: find.text(heading2),\n        ),\n        findsNWidgets(2),\n      );\n\n      expect(\n        find.descendant(\n          of: find.byType(OutlineBlockWidget),\n          matching: find.text(heading3),\n        ),\n        findsNWidgets(2),\n      );\n      //////\n    });\n  });\n}\n\n/// Inserts an outline block in the document\nFuture<void> insertOutlineInDocument(WidgetTester tester) async {\n  // open the actions menu and insert the outline block\n  await tester.editor.showSlashMenu();\n  await tester.editor.tapSlashMenuItemWithName(\n    LocaleKeys.document_slashMenu_name_outline.tr(),\n    offset: 180,\n  );\n  await tester.pumpAndSettle();\n}\n\nFuture<void> hoverAndClickDepthOptionAction(\n  WidgetTester tester,\n  List<int> path,\n  int level,\n) async {\n  await tester.editor.openDepthMenu(path);\n  final type = OptionDepthType.fromLevel(level);\n  await tester.tapButton(find.findTextInFlowyText(type.description));\n  await tester.pumpAndSettle();\n}\n\nFuture<void> insertHeadingComponent(WidgetTester tester) async {\n  await tester.editor.tapLineOfEditorAt(0);\n\n  // # heading 1-3\n  await tester.ime.insertText('# $heading1\\n');\n  await tester.ime.insertText('## $heading2\\n');\n  await tester.ime.insertText('### $heading3\\n');\n\n  // > # toggle heading 1-3\n  await tester.ime.insertText('> # $heading1\\n');\n  await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);\n  await tester.ime.insertText('> ## $heading2\\n');\n  await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);\n  await tester.ime.insertText('> ### $heading3\\n');\n  await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_with_simple_table_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../../shared/util.dart';\n\nconst String heading1 = \"Heading 1\";\nconst String heading2 = \"Heading 2\";\nconst String heading3 = \"Heading 3\";\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('simple table block test:', () {\n    testWidgets('insert a simple table block', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(\n        name: 'simple_table_test',\n      );\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.insertTableInDocument();\n\n      // validate the table is inserted\n      expect(find.byType(SimpleTableBlockWidget), findsOneWidget);\n\n      final editorState = tester.editor.getCurrentEditorState();\n      expect(\n        editorState.selection,\n        // table -> row -> cell -> paragraph\n        Selection.collapsed(Position(path: [0, 0, 0, 0])),\n      );\n\n      final firstCell = find.byType(SimpleTableCellBlockWidget).first;\n      expect(\n        tester\n            .state<SimpleTableCellBlockWidgetState>(firstCell)\n            .isEditingCellNotifier\n            .value,\n        isTrue,\n      );\n    });\n\n    testWidgets('select all in table cell', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(\n        name: 'simple_table_test',\n      );\n\n      const cell1Content = 'Cell 1';\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.ime.insertText('New Table');\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      await tester.pumpAndSettle();\n      await tester.editor.tapLineOfEditorAt(1);\n      await tester.insertTableInDocument();\n      await tester.ime.insertText(cell1Content);\n      await tester.pumpAndSettle();\n      // Select all in the cell\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyA,\n        isControlPressed: !UniversalPlatform.isMacOS,\n        isMetaPressed: UniversalPlatform.isMacOS,\n      );\n\n      expect(\n        tester.editor.getCurrentEditorState().selection,\n        Selection(\n          start: Position(path: [1, 0, 0, 0]),\n          end: Position(path: [1, 0, 0, 0], offset: cell1Content.length),\n        ),\n      );\n\n      // Press select all again, the selection should be the entire document\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyA,\n        isControlPressed: !UniversalPlatform.isMacOS,\n        isMetaPressed: UniversalPlatform.isMacOS,\n      );\n\n      expect(\n        tester.editor.getCurrentEditorState().selection,\n        Selection(\n          start: Position(path: [0]),\n          end: Position(path: [1, 1, 1, 0]),\n        ),\n      );\n    });\n\n    testWidgets('''\n1. hover on the table\n  1.1 click the add row button\n  1.2 click the add column button\n  1.3 click the add row and column button\n2. validate the table is updated\n3. delete the last column\n4. delete the last row\n5. validate the table is updated\n''', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(\n        name: 'simple_table_test',\n      );\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.insertTableInDocument();\n\n      // add a new row\n      final row = find.byWidgetPredicate((w) {\n        return w is SimpleTableRowBlockWidget && w.node.rowIndex == 1;\n      });\n      await tester.hoverOnWidget(\n        row,\n        onHover: () async {\n          final addRowButton = find.byType(SimpleTableAddRowButton).first;\n          await tester.tap(addRowButton);\n        },\n      );\n      await tester.pumpAndSettle();\n\n      // add a new column\n      final column = find.byWidgetPredicate((w) {\n        return w is SimpleTableCellBlockWidget && w.node.columnIndex == 1;\n      }).first;\n      await tester.hoverOnWidget(\n        column,\n        onHover: () async {\n          final addColumnButton = find.byType(SimpleTableAddColumnButton).first;\n          await tester.tap(addColumnButton);\n        },\n      );\n      await tester.pumpAndSettle();\n\n      // add a new row and a new column\n      final row2 = find.byWidgetPredicate((w) {\n        return w is SimpleTableCellBlockWidget &&\n            w.node.rowIndex == 2 &&\n            w.node.columnIndex == 2;\n      }).first;\n      await tester.hoverOnWidget(\n        row2,\n        onHover: () async {\n          // click the add row and column button\n          final addRowAndColumnButton =\n              find.byType(SimpleTableAddColumnAndRowButton).first;\n          await tester.tap(addRowAndColumnButton);\n        },\n      );\n      await tester.pumpAndSettle();\n\n      final tableNode =\n          tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;\n      expect(tableNode.columnLength, 4);\n      expect(tableNode.rowLength, 4);\n\n      // delete the last row\n      await tester.clickMoreActionItemInTableMenu(\n        type: SimpleTableMoreActionType.row,\n        index: tableNode.rowLength - 1,\n        action: SimpleTableMoreAction.delete,\n      );\n      await tester.pumpAndSettle();\n      expect(tableNode.rowLength, 3);\n      expect(tableNode.columnLength, 4);\n\n      // delete the last column\n      await tester.clickMoreActionItemInTableMenu(\n        type: SimpleTableMoreActionType.column,\n        index: tableNode.columnLength - 1,\n        action: SimpleTableMoreAction.delete,\n      );\n      await tester.pumpAndSettle();\n\n      expect(tableNode.columnLength, 3);\n      expect(tableNode.rowLength, 3);\n    });\n\n    testWidgets('enable header column and header row', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(\n        name: 'simple_table_test',\n      );\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.insertTableInDocument();\n\n      // enable the header row\n      await tester.clickMoreActionItemInTableMenu(\n        type: SimpleTableMoreActionType.row,\n        index: 0,\n        action: SimpleTableMoreAction.enableHeaderRow,\n      );\n      await tester.pumpAndSettle();\n      // enable the header column\n      await tester.clickMoreActionItemInTableMenu(\n        type: SimpleTableMoreActionType.column,\n        index: 0,\n        action: SimpleTableMoreAction.enableHeaderColumn,\n      );\n      await tester.pumpAndSettle();\n\n      final tableNode =\n          tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;\n\n      expect(tableNode.isHeaderColumnEnabled, isTrue);\n      expect(tableNode.isHeaderRowEnabled, isTrue);\n\n      // disable the header row\n      await tester.clickMoreActionItemInTableMenu(\n        type: SimpleTableMoreActionType.row,\n        index: 0,\n        action: SimpleTableMoreAction.enableHeaderRow,\n      );\n      await tester.pumpAndSettle();\n      expect(tableNode.isHeaderColumnEnabled, isTrue);\n      expect(tableNode.isHeaderRowEnabled, isFalse);\n\n      // disable the header column\n      await tester.clickMoreActionItemInTableMenu(\n        type: SimpleTableMoreActionType.column,\n        index: 0,\n        action: SimpleTableMoreAction.enableHeaderColumn,\n      );\n      await tester.pumpAndSettle();\n      expect(tableNode.isHeaderColumnEnabled, isFalse);\n      expect(tableNode.isHeaderRowEnabled, isFalse);\n    });\n\n    testWidgets('duplicate a column / row', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(\n        name: 'simple_table_test',\n      );\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.insertTableInDocument();\n\n      // duplicate the row\n      await tester.clickMoreActionItemInTableMenu(\n        type: SimpleTableMoreActionType.row,\n        index: 0,\n        action: SimpleTableMoreAction.duplicate,\n      );\n      await tester.pumpAndSettle();\n\n      // duplicate the column\n      await tester.clickMoreActionItemInTableMenu(\n        type: SimpleTableMoreActionType.column,\n        index: 0,\n        action: SimpleTableMoreAction.duplicate,\n      );\n      await tester.pumpAndSettle();\n\n      final tableNode =\n          tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;\n      expect(tableNode.columnLength, 3);\n      expect(tableNode.rowLength, 3);\n    });\n\n    testWidgets('insert left / insert right', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(\n        name: 'simple_table_test',\n      );\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.insertTableInDocument();\n\n      // insert left\n      await tester.clickMoreActionItemInTableMenu(\n        type: SimpleTableMoreActionType.column,\n        index: 0,\n        action: SimpleTableMoreAction.insertLeft,\n      );\n      await tester.pumpAndSettle();\n\n      // insert right\n      await tester.clickMoreActionItemInTableMenu(\n        type: SimpleTableMoreActionType.column,\n        index: 0,\n        action: SimpleTableMoreAction.insertRight,\n      );\n      await tester.pumpAndSettle();\n\n      final tableNode =\n          tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;\n      expect(tableNode.columnLength, 4);\n      expect(tableNode.rowLength, 2);\n    });\n\n    testWidgets('insert above / insert below', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.createNewPageWithNameUnderParent(\n        name: 'simple_table_test',\n      );\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.insertTableInDocument();\n\n      // insert above\n      await tester.clickMoreActionItemInTableMenu(\n        type: SimpleTableMoreActionType.row,\n        index: 0,\n        action: SimpleTableMoreAction.insertAbove,\n      );\n      await tester.pumpAndSettle();\n\n      // insert below\n      await tester.clickMoreActionItemInTableMenu(\n        type: SimpleTableMoreActionType.row,\n        index: 0,\n        action: SimpleTableMoreAction.insertBelow,\n      );\n      await tester.pumpAndSettle();\n\n      final tableNode =\n          tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;\n      expect(tableNode.rowLength, 4);\n      expect(tableNode.columnLength, 2);\n    });\n  });\n\n  testWidgets('set column width to page width (1)', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    await tester.createNewPageWithNameUnderParent(\n      name: 'simple_table_test',\n    );\n\n    await tester.editor.tapLineOfEditorAt(0);\n    await tester.insertTableInDocument();\n\n    final tableNode = tester.editor.getNodeAtPath([0]);\n    final beforeWidth = tableNode.width;\n\n    // set the column width to page width\n    await tester.clickMoreActionItemInTableMenu(\n      type: SimpleTableMoreActionType.column,\n      index: 0,\n      action: SimpleTableMoreAction.setToPageWidth,\n    );\n    await tester.pumpAndSettle();\n\n    final afterWidth = tableNode.width;\n    expect(afterWidth, greaterThan(beforeWidth));\n  });\n\n  testWidgets('set column width to page width (2)', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    await tester.createNewPageWithNameUnderParent(\n      name: 'simple_table_test',\n    );\n\n    await tester.editor.tapLineOfEditorAt(0);\n    await tester.insertTableInDocument();\n\n    final tableNode = tester.editor.getNodeAtPath([0]);\n    final beforeWidth = tableNode.width;\n\n    // set the column width to page width\n    await tester.clickMoreActionItemInTableMenu(\n      type: SimpleTableMoreActionType.row,\n      index: 0,\n      action: SimpleTableMoreAction.setToPageWidth,\n    );\n    await tester.pumpAndSettle();\n\n    final afterWidth = tableNode.width;\n    expect(afterWidth, greaterThan(beforeWidth));\n  });\n\n  testWidgets('distribute columns evenly (1)', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    await tester.createNewPageWithNameUnderParent(\n      name: 'simple_table_test',\n    );\n\n    await tester.editor.tapLineOfEditorAt(0);\n    await tester.insertTableInDocument();\n\n    final tableNode = tester.editor.getNodeAtPath([0]);\n    final beforeWidth = tableNode.width;\n\n    // set the column width to page width\n    await tester.clickMoreActionItemInTableMenu(\n      type: SimpleTableMoreActionType.row,\n      index: 0,\n      action: SimpleTableMoreAction.distributeColumnsEvenly,\n    );\n    await tester.pumpAndSettle();\n\n    final afterWidth = tableNode.width;\n    expect(afterWidth, equals(beforeWidth));\n\n    final distributeColumnWidthsEvenly =\n        tableNode.attributes[SimpleTableBlockKeys.distributeColumnWidthsEvenly];\n    expect(distributeColumnWidthsEvenly, isTrue);\n  });\n\n  testWidgets('distribute columns evenly (2)', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    await tester.createNewPageWithNameUnderParent(\n      name: 'simple_table_test',\n    );\n\n    await tester.editor.tapLineOfEditorAt(0);\n    await tester.insertTableInDocument();\n\n    final tableNode = tester.editor.getNodeAtPath([0]);\n    final beforeWidth = tableNode.width;\n\n    // set the column width to page width\n    await tester.clickMoreActionItemInTableMenu(\n      type: SimpleTableMoreActionType.column,\n      index: 0,\n      action: SimpleTableMoreAction.distributeColumnsEvenly,\n    );\n    await tester.pumpAndSettle();\n\n    final afterWidth = tableNode.width;\n    expect(afterWidth, equals(beforeWidth));\n\n    final distributeColumnWidthsEvenly =\n        tableNode.attributes[SimpleTableBlockKeys.distributeColumnWidthsEvenly];\n    expect(distributeColumnWidthsEvenly, isTrue);\n  });\n\n  testWidgets('using option menu to set column width', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    await tester.createNewPageWithNameUnderParent(\n      name: 'simple_table_test',\n    );\n\n    await tester.editor.tapLineOfEditorAt(0);\n    await tester.insertTableInDocument();\n    await tester.editor.hoverAndClickOptionMenuButton([0]);\n\n    final editorState = tester.editor.getCurrentEditorState();\n    final beforeWidth = editorState.document.nodeAtPath([0])!.width;\n\n    await tester.tapButton(\n      find.text(\n        LocaleKeys.document_plugins_simpleTable_moreActions_setToPageWidth.tr(),\n      ),\n    );\n    await tester.pumpAndSettle();\n\n    final afterWidth = editorState.document.nodeAtPath([0])!.width;\n    expect(afterWidth, greaterThan(beforeWidth));\n\n    await tester.editor.hoverAndClickOptionMenuButton([0]);\n    await tester.tapButton(\n      find.text(\n        LocaleKeys\n            .document_plugins_simpleTable_moreActions_distributeColumnsWidth\n            .tr(),\n      ),\n    );\n    await tester.pumpAndSettle();\n\n    final afterWidth2 = editorState.document.nodeAtPath([0])!.width;\n    expect(afterWidth2, equals(afterWidth));\n  });\n\n  testWidgets('insert a table and use select all the delete it',\n      (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    await tester.createNewPageWithNameUnderParent(\n      name: 'simple_table_test',\n    );\n\n    await tester.editor.tapLineOfEditorAt(0);\n    await tester.insertTableInDocument();\n\n    await tester.editor.tapLineOfEditorAt(1);\n    await tester.ime.insertText('Hello World');\n\n    // select all\n    await tester.simulateKeyEvent(\n      LogicalKeyboardKey.keyA,\n      isMetaPressed: UniversalPlatform.isMacOS,\n      isControlPressed: !UniversalPlatform.isMacOS,\n    );\n\n    await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);\n    await tester.pumpAndSettle();\n\n    final editorState = tester.editor.getCurrentEditorState();\n    // only one paragraph left\n    expect(editorState.document.root.children.length, 1);\n    final paragraphNode = editorState.document.nodeAtPath([0])!;\n    expect(paragraphNode.delta, isNull);\n  });\n\n  testWidgets('use tab or shift+tab to navigate in table', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    await tester.createNewPageWithNameUnderParent(\n      name: 'simple_table_test',\n    );\n\n    await tester.editor.tapLineOfEditorAt(0);\n    await tester.insertTableInDocument();\n\n    await tester.simulateKeyEvent(LogicalKeyboardKey.tab);\n    await tester.pumpAndSettle();\n\n    final editorState = tester.editor.getCurrentEditorState();\n    final selection = editorState.selection;\n    expect(selection, isNotNull);\n    expect(selection!.start.path, [0, 0, 1, 0]);\n    expect(selection.end.path, [0, 0, 1, 0]);\n\n    await tester.simulateKeyEvent(\n      LogicalKeyboardKey.tab,\n      isShiftPressed: true,\n    );\n    await tester.pumpAndSettle();\n\n    final selection2 = editorState.selection;\n    expect(selection2, isNotNull);\n    expect(selection2!.start.path, [0, 0, 0, 0]);\n    expect(selection2.end.path, [0, 0, 0, 0]);\n  });\n\n  testWidgets('shift+enter to insert a new line in table', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    await tester.createNewPageWithNameUnderParent(\n      name: 'simple_table_test',\n    );\n\n    await tester.editor.tapLineOfEditorAt(0);\n    await tester.insertTableInDocument();\n\n    await tester.simulateKeyEvent(\n      LogicalKeyboardKey.enter,\n      isShiftPressed: true,\n    );\n    await tester.pumpAndSettle();\n\n    final editorState = tester.editor.getCurrentEditorState();\n    final node = editorState.document.nodeAtPath([0, 0, 0])!;\n    expect(node.children.length, 1);\n  });\n\n  testWidgets('using option menu to set table align', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    await tester.createNewPageWithNameUnderParent(\n      name: 'simple_table_test',\n    );\n\n    await tester.editor.tapLineOfEditorAt(0);\n    await tester.insertTableInDocument();\n    await tester.editor.hoverAndClickOptionMenuButton([0]);\n\n    final editorState = tester.editor.getCurrentEditorState();\n    final beforeAlign = editorState.document.nodeAtPath([0])!.tableAlign;\n    expect(beforeAlign, TableAlign.left);\n\n    await tester.tapButton(\n      find.text(\n        LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),\n      ),\n    );\n    await tester.pumpAndSettle();\n    await tester.tapButton(\n      find.text(\n        LocaleKeys.document_plugins_optionAction_center.tr(),\n      ),\n    );\n    await tester.pumpAndSettle();\n\n    final afterAlign = editorState.document.nodeAtPath([0])!.tableAlign;\n    expect(afterAlign, TableAlign.center);\n\n    await tester.editor.hoverAndClickOptionMenuButton([0]);\n    await tester.tapButton(\n      find.text(\n        LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),\n      ),\n    );\n    await tester.pumpAndSettle();\n    await tester.tapButton(\n      find.text(\n        LocaleKeys.document_plugins_optionAction_right.tr(),\n      ),\n    );\n    await tester.pumpAndSettle();\n\n    final afterAlign2 = editorState.document.nodeAtPath([0])!.tableAlign;\n    expect(afterAlign2, TableAlign.right);\n\n    await tester.editor.hoverAndClickOptionMenuButton([0]);\n    await tester.tapButton(\n      find.text(\n        LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),\n      ),\n    );\n    await tester.pumpAndSettle();\n    await tester.tapButton(\n      find.text(\n        LocaleKeys.document_plugins_optionAction_left.tr(),\n      ),\n    );\n    await tester.pumpAndSettle();\n\n    final afterAlign3 = editorState.document.nodeAtPath([0])!.tableAlign;\n    expect(afterAlign3, TableAlign.left);\n  });\n\n  testWidgets('using option menu to set table align', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    await tester.createNewPageWithNameUnderParent(\n      name: 'simple_table_test',\n    );\n\n    await tester.editor.tapLineOfEditorAt(0);\n    await tester.insertTableInDocument();\n    await tester.editor.hoverAndClickOptionMenuButton([0]);\n\n    final editorState = tester.editor.getCurrentEditorState();\n    final beforeAlign = editorState.document.nodeAtPath([0])!.tableAlign;\n    expect(beforeAlign, TableAlign.left);\n\n    await tester.tapButton(\n      find.text(\n        LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),\n      ),\n    );\n    await tester.pumpAndSettle();\n    await tester.tapButton(\n      find.text(\n        LocaleKeys.document_plugins_optionAction_center.tr(),\n      ),\n    );\n    await tester.pumpAndSettle();\n\n    final afterAlign = editorState.document.nodeAtPath([0])!.tableAlign;\n    expect(afterAlign, TableAlign.center);\n\n    await tester.editor.hoverAndClickOptionMenuButton([0]);\n    await tester.tapButton(\n      find.text(\n        LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),\n      ),\n    );\n    await tester.pumpAndSettle();\n    await tester.tapButton(\n      find.text(\n        LocaleKeys.document_plugins_optionAction_right.tr(),\n      ),\n    );\n    await tester.pumpAndSettle();\n\n    final afterAlign2 = editorState.document.nodeAtPath([0])!.tableAlign;\n    expect(afterAlign2, TableAlign.right);\n\n    await tester.editor.hoverAndClickOptionMenuButton([0]);\n    await tester.tapButton(\n      find.text(\n        LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),\n      ),\n    );\n    await tester.pumpAndSettle();\n    await tester.tapButton(\n      find.text(\n        LocaleKeys.document_plugins_optionAction_left.tr(),\n      ),\n    );\n    await tester.pumpAndSettle();\n\n    final afterAlign3 = editorState.document.nodeAtPath([0])!.tableAlign;\n    expect(afterAlign3, TableAlign.left);\n  });\n\n  testWidgets('support slash menu in table', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    await tester.createNewPageWithNameUnderParent(\n      name: 'simple_table_test',\n    );\n\n    final editorState = tester.editor.getCurrentEditorState();\n\n    await tester.editor.tapLineOfEditorAt(0);\n    await tester.insertTableInDocument();\n\n    final path = [0, 0, 0, 0];\n    final selection = Selection.collapsed(Position(path: path));\n    editorState.selection = selection;\n    await tester.editor.showSlashMenu();\n    await tester.pumpAndSettle();\n\n    final paragraphItem = find.byWidgetPredicate((w) {\n      return w is SelectionMenuItemWidget &&\n          w.item.name == LocaleKeys.document_slashMenu_name_text.tr();\n    });\n    expect(paragraphItem, findsOneWidget);\n\n    await tester.tap(paragraphItem);\n    await tester.pumpAndSettle();\n\n    final paragraphNode = editorState.document.nodeAtPath(path)!;\n    expect(paragraphNode.type, equals(ParagraphBlockKeys.type));\n  });\n}\n\nextension on WidgetTester {\n  /// Insert a table in the document\n  Future<void> insertTableInDocument() async {\n    // open the actions menu and insert the outline block\n    await editor.showSlashMenu();\n    await editor.tapSlashMenuItemWithName(\n      LocaleKeys.document_slashMenu_name_table.tr(),\n    );\n    await pumpAndSettle();\n  }\n\n  Future<void> clickMoreActionItemInTableMenu({\n    required SimpleTableMoreActionType type,\n    required int index,\n    required SimpleTableMoreAction action,\n  }) async {\n    if (type == SimpleTableMoreActionType.row) {\n      final row = find.byWidgetPredicate((w) {\n        return w is SimpleTableRowBlockWidget && w.node.rowIndex == index;\n      });\n      await hoverOnWidget(\n        row,\n        onHover: () async {\n          final moreActionButton = find.byWidgetPredicate((w) {\n            return w is SimpleTableMoreActionMenu &&\n                w.type == SimpleTableMoreActionType.row &&\n                w.index == index;\n          });\n          await tapButton(moreActionButton);\n          await tapButton(find.text(action.name));\n        },\n      );\n      await pumpAndSettle();\n    } else if (type == SimpleTableMoreActionType.column) {\n      final column = find.byWidgetPredicate((w) {\n        return w is SimpleTableCellBlockWidget && w.node.columnIndex == index;\n      }).first;\n      await hoverOnWidget(\n        column,\n        onHover: () async {\n          final moreActionButton = find.byWidgetPredicate((w) {\n            return w is SimpleTableMoreActionMenu &&\n                w.type == SimpleTableMoreActionType.column &&\n                w.index == index;\n          });\n          await tapButton(moreActionButton);\n          await tapButton(find.text(action.name));\n        },\n      );\n      await pumpAndSettle();\n    }\n\n    await tapAt(Offset.zero);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_heading_block_test.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nconst String _heading1 = 'Heading 1';\nconst String _heading2 = 'Heading 2';\nconst String _heading3 = 'Heading 3';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('toggle heading block test:', () {\n    testWidgets('insert toggle heading 1 - 3 block', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(\n        name: 'toggle heading block test',\n      );\n\n      for (var i = 1; i <= 3; i++) {\n        await tester.editor.tapLineOfEditorAt(0);\n        await _insertToggleHeadingBlockInDocument(tester, i);\n        await tester.pumpAndSettle();\n        expect(\n          find.byWidgetPredicate(\n            (widget) =>\n                widget is ToggleListBlockComponentWidget &&\n                widget.node.attributes[ToggleListBlockKeys.level] == i,\n          ),\n          findsOneWidget,\n        );\n      }\n    });\n\n    testWidgets('insert toggle heading 1 - 3 block by shortcuts',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(\n        name: 'toggle heading block test',\n      );\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.ime.insertText('# > $_heading1\\n');\n      await tester.ime.insertText('## > $_heading2\\n');\n      await tester.ime.insertText('### > $_heading3\\n');\n      await tester.ime.insertText('> # $_heading1\\n');\n      await tester.ime.insertText('> ## $_heading2\\n');\n      await tester.ime.insertText('> ### $_heading3\\n');\n      await tester.pumpAndSettle();\n\n      expect(\n        find.byType(ToggleListBlockComponentWidget),\n        findsNWidgets(6),\n      );\n    });\n\n    testWidgets('insert toggle heading and convert it to heading',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(\n        name: 'toggle heading block test',\n      );\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.ime.insertText('# > $_heading1\\n');\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      await tester.ime.insertText('item 1');\n      await tester.pumpAndSettle();\n\n      await tester.editor.updateSelection(\n        Selection(\n          start: Position(path: [0]),\n          end: Position(path: [0], offset: _heading1.length),\n        ),\n      );\n\n      await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_text_format_m));\n\n      // tap the H1 button\n      await tester.tapButton(find.byFlowySvg(FlowySvgs.type_h1_m).at(0));\n      await tester.pumpAndSettle();\n\n      final editorState = tester.editor.getCurrentEditorState();\n      final node1 = editorState.document.nodeAtPath([0])!;\n      expect(node1.type, HeadingBlockKeys.type);\n      expect(node1.attributes[HeadingBlockKeys.level], 1);\n\n      final node2 = editorState.document.nodeAtPath([1])!;\n      expect(node2.type, ParagraphBlockKeys.type);\n      expect(node2.delta!.toPlainText(), 'item 1');\n    });\n  });\n}\n\nFuture<void> _insertToggleHeadingBlockInDocument(\n  WidgetTester tester,\n  int level,\n) async {\n  final name = switch (level) {\n    1 => LocaleKeys.document_slashMenu_name_toggleHeading1.tr(),\n    2 => LocaleKeys.document_slashMenu_name_toggleHeading2.tr(),\n    3 => LocaleKeys.document_slashMenu_name_toggleHeading3.tr(),\n    _ => throw Exception('Invalid level: $level'),\n  };\n  await tester.editor.showSlashMenu();\n  await tester.editor.tapSlashMenuItemWithName(\n    name,\n    offset: 150,\n  );\n  await tester.pumpAndSettle();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_list_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('toggle list in document', () {\n    Finder findToggleListIcon({\n      required bool isExpanded,\n    }) {\n      final turns = isExpanded ? 0.25 : 0.0;\n      return find.byWidgetPredicate(\n        (widget) => widget is AnimatedRotation && widget.turns == turns,\n      );\n    }\n\n    void expectToggleListOpened() {\n      expect(findToggleListIcon(isExpanded: true), findsOneWidget);\n      expect(findToggleListIcon(isExpanded: false), findsNothing);\n    }\n\n    void expectToggleListClosed() {\n      expect(findToggleListIcon(isExpanded: false), findsOneWidget);\n      expect(findToggleListIcon(isExpanded: true), findsNothing);\n    }\n\n    testWidgets('convert > to toggle list, and click the icon to close it',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent();\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      // insert a toggle list\n      const text = 'This is a toggle list sample';\n      await tester.ime.insertText('> $text');\n\n      final editorState = tester.editor.getCurrentEditorState();\n      final toggleList = editorState.document.nodeAtPath([0])!;\n      expect(\n        toggleList.type,\n        ToggleListBlockKeys.type,\n      );\n      expect(\n        toggleList.attributes[ToggleListBlockKeys.collapsed],\n        false,\n      );\n      expect(\n        toggleList.delta!.toPlainText(),\n        text,\n      );\n\n      // Simulate pressing enter key to move the cursor to the next line\n      await tester.ime.insertCharacter('\\n');\n      const text2 = 'This is a child node';\n      await tester.ime.insertText(text2);\n      expect(find.text(text2, findRichText: true), findsOneWidget);\n\n      // Click the toggle list icon to close it\n      final toggleListIcon = find.byIcon(Icons.arrow_right);\n      await tester.tapButton(toggleListIcon);\n\n      // expect the toggle list to be closed\n      expect(find.text(text2, findRichText: true), findsNothing);\n    });\n\n    testWidgets('press enter key when the toggle list is closed',\n        (tester) async {\n      // if the toggle list is closed, press enter key will insert a new toggle list after it\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent();\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      // insert a toggle list\n      const text = 'Hello AppFlowy';\n      await tester.ime.insertText('> $text');\n\n      // Click the toggle list icon to close it\n      final toggleListIcon = find.byIcon(Icons.arrow_right);\n      await tester.tapButton(toggleListIcon);\n\n      // Press the enter key\n      await tester.editor.updateSelection(\n        Selection.collapsed(\n          Position(path: [0], offset: 'Hello '.length),\n        ),\n      );\n      await tester.ime.insertCharacter('\\n');\n\n      final editorState = tester.editor.getCurrentEditorState();\n      final node0 = editorState.getNodeAtPath([0])!;\n      final node1 = editorState.getNodeAtPath([1])!;\n\n      expect(node0.type, ToggleListBlockKeys.type);\n      expect(node0.attributes[ToggleListBlockKeys.collapsed], true);\n      expect(node0.delta!.toPlainText(), 'Hello ');\n      expect(node1.type, ToggleListBlockKeys.type);\n      expect(node1.delta!.toPlainText(), 'AppFlowy');\n    });\n\n    testWidgets('press enter key when the toggle list is open', (tester) async {\n      // if the toggle list is open, press enter key will insert a new paragraph inside it\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent();\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      // insert a toggle list\n      const text = 'Hello AppFlowy';\n      await tester.ime.insertText('> $text');\n\n      // Press the enter key\n      await tester.editor.updateSelection(\n        Selection.collapsed(\n          Position(path: [0], offset: 'Hello '.length),\n        ),\n      );\n      await tester.ime.insertCharacter('\\n');\n\n      final editorState = tester.editor.getCurrentEditorState();\n      final node0 = editorState.getNodeAtPath([0])!;\n      final node00 = editorState.getNodeAtPath([0, 0])!;\n      final node1 = editorState.getNodeAtPath([1]);\n\n      expect(node0.type, ToggleListBlockKeys.type);\n      expect(node0.attributes[ToggleListBlockKeys.collapsed], false);\n      expect(node0.delta!.toPlainText(), 'Hello ');\n      expect(node00.type, ParagraphBlockKeys.type);\n      expect(node00.delta!.toPlainText(), 'AppFlowy');\n      expect(node1, isNull);\n    });\n\n    testWidgets('clear the format if toggle list if empty', (tester) async {\n      // if the toggle list is open, press enter key will insert a new paragraph inside it\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent();\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      // insert a toggle list\n      await tester.ime.insertText('> ');\n\n      // Press the enter key\n      // Click the toggle list icon to close it\n      final toggleListIcon = find.byIcon(Icons.arrow_right);\n      await tester.tapButton(toggleListIcon);\n\n      await tester.editor\n          .updateSelection(Selection.collapsed(Position(path: [0])));\n      await tester.ime.insertCharacter('\\n');\n\n      final editorState = tester.editor.getCurrentEditorState();\n      final node0 = editorState.getNodeAtPath([0])!;\n\n      expect(node0.type, ParagraphBlockKeys.type);\n    });\n\n    testWidgets('use cmd/ctrl + enter to open/close the toggle list',\n        (tester) async {\n      // if the toggle list is open, press enter key will insert a new paragraph inside it\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document\n      await tester.createNewPageWithNameUnderParent();\n\n      // tap the first line of the document\n      await tester.editor.tapLineOfEditorAt(0);\n      // insert a toggle list\n      await tester.ime.insertText('> Hello');\n\n      expectToggleListOpened();\n\n      await tester.editor.updateSelection(\n        Selection.collapsed(\n          Position(path: [0]),\n        ),\n      );\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.enter,\n        isMetaPressed: Platform.isMacOS,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n      );\n\n      expectToggleListClosed();\n\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.enter,\n        isMetaPressed: Platform.isMacOS,\n        isControlPressed: Platform.isLinux || Platform.isWindows,\n      );\n\n      expectToggleListOpened();\n    });\n\n    Future<void> prepareToggleHeadingBlock(\n      WidgetTester tester,\n      String text,\n    ) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent();\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.ime.insertText(text);\n    }\n\n    testWidgets('> + # to toggle heading 1 block', (tester) async {\n      await prepareToggleHeadingBlock(tester, '> # Hello');\n      final editorState = tester.editor.getCurrentEditorState();\n      final node = editorState.getNodeAtPath([0])!;\n      expect(node.type, ToggleListBlockKeys.type);\n      expect(node.attributes[ToggleListBlockKeys.level], 1);\n      expect(node.delta!.toPlainText(), 'Hello');\n    });\n\n    testWidgets('> + ### to toggle heading 3 block', (tester) async {\n      await prepareToggleHeadingBlock(tester, '> ### Hello');\n      final editorState = tester.editor.getCurrentEditorState();\n      final node = editorState.getNodeAtPath([0])!;\n      expect(node.type, ToggleListBlockKeys.type);\n      expect(node.attributes[ToggleListBlockKeys.level], 3);\n      expect(node.delta!.toPlainText(), 'Hello');\n    });\n\n    testWidgets('# + > to toggle heading 1 block', (tester) async {\n      await prepareToggleHeadingBlock(tester, '# > Hello');\n      final editorState = tester.editor.getCurrentEditorState();\n      final node = editorState.getNodeAtPath([0])!;\n      expect(node.type, ToggleListBlockKeys.type);\n      expect(node.attributes[ToggleListBlockKeys.level], 1);\n      expect(node.delta!.toPlainText(), 'Hello');\n    });\n\n    testWidgets('### + > to toggle heading 3 block', (tester) async {\n      await prepareToggleHeadingBlock(tester, '### > Hello');\n      final editorState = tester.editor.getCurrentEditorState();\n      final node = editorState.getNodeAtPath([0])!;\n      expect(node.type, ToggleListBlockKeys.type);\n      expect(node.attributes[ToggleListBlockKeys.level], 3);\n      expect(node.delta!.toPlainText(), 'Hello');\n    });\n\n    testWidgets('click the toggle list to create a new paragraph',\n        (tester) async {\n      await prepareToggleHeadingBlock(tester, '> # Hello');\n      final emptyHintText = find.text(\n        LocaleKeys.document_plugins_emptyToggleHeading.tr(\n          args: ['1'],\n        ),\n      );\n      expect(emptyHintText, findsOneWidget);\n\n      await tester.tapButton(emptyHintText);\n      await tester.pumpAndSettle();\n\n      // check the new paragraph is created\n      final editorState = tester.editor.getCurrentEditorState();\n      final node = editorState.getNodeAtPath([0, 0])!;\n      expect(node.type, ParagraphBlockKeys.type);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/document/edit_document_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('edit document', () {\n    testWidgets('redo & undo', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document called Sample\n      const pageName = 'Sample';\n      await tester.createNewPageWithNameUnderParent(name: pageName);\n\n      // focus on the editor\n      await tester.editor.tapLineOfEditorAt(0);\n\n      // insert 1. to trigger it to be a numbered list\n      await tester.ime.insertText('1. ');\n      expect(find.text('1.', findRichText: true), findsOneWidget);\n      expect(\n        tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,\n        NumberedListBlockKeys.type,\n      );\n\n      // undo\n      // numbered list will be reverted to paragraph\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyZ,\n        isControlPressed: Platform.isWindows || Platform.isLinux,\n        isMetaPressed: Platform.isMacOS,\n      );\n      expect(\n        tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,\n        ParagraphBlockKeys.type,\n      );\n\n      // redo\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyZ,\n        isControlPressed: Platform.isWindows || Platform.isLinux,\n        isMetaPressed: Platform.isMacOS,\n        isShiftPressed: true,\n      );\n      expect(\n        tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,\n        NumberedListBlockKeys.type,\n      );\n\n      // switch to other page and switch back\n      await tester.openPage(gettingStarted);\n      await tester.openPage(pageName);\n\n      // the numbered list should be kept\n      expect(\n        tester.editor.getCurrentEditorState().getNodeAtPath([0])!.type,\n        NumberedListBlockKeys.type,\n      );\n    });\n\n    testWidgets('write a readme document', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new document called Sample\n      const pageName = 'Sample';\n      await tester.createNewPageWithNameUnderParent(name: pageName);\n\n      // focus on the editor\n      await tester.editor.tapLineOfEditorAt(0);\n\n      // mock inputting the sample\n      final lines = _sample.split('\\n');\n      for (final line in lines) {\n        await tester.ime.insertText(line);\n        await tester.ime.insertCharacter('\\n');\n      }\n\n      // switch to other page and switch back\n      await tester.openPage(gettingStarted);\n      await tester.openPage(pageName);\n\n      // this screenshots are different on different platform, so comment it out temporarily.\n      // check the document\n      // await expectLater(\n      //   find.byType(AppFlowyEditor),\n      //   matchesGoldenFile('document/edit_document_test.png'),\n      // );\n    });\n  });\n}\n\nconst _sample = '''\n# Heading 1\n## Heading 2\n### Heading 3\n---\n[] Highlight any text, and use the editing menu to _style_ **your** writing `however` you ~~like.~~\n\n[] Type followed by bullet or num to create a list.\n\n[x] Click `New Page` button at the bottom of your sidebar to add a new page.\n\n[] Click the plus sign next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`.\n---\n* bulleted list 1\n\n* bulleted list 2\n\n* bulleted list 3\nbulleted list 4\n---\n1. numbered list 1\n\n2. numbered list 2\n\n3. numbered list 3\nnumbered list 4\n---\n\" quote''';\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/first_test/first_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\n// This test is meaningless, just for preventing the CI from failing.\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Empty', () {\n    testWidgets('empty test', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await tester.wait(500);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/grid/grid_calculations_test.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Grid Calculations', () {\n    testWidgets('add calculation and update cell', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Change one Field to Number\n      await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Number);\n\n      expect(find.text('Calculate'), findsOneWidget);\n\n      await tester.changeCalculateAtIndex(1, CalculationType.Sum);\n\n      // Enter values in cells\n      await tester.editCell(\n        rowIndex: 0,\n        fieldType: FieldType.Number,\n        input: '100',\n      );\n\n      await tester.editCell(\n        rowIndex: 1,\n        fieldType: FieldType.Number,\n        input: '100',\n      );\n\n      // Dismiss edit cell\n      await tester.sendKeyDownEvent(LogicalKeyboardKey.enter);\n\n      await tester.pumpAndSettle(const Duration(seconds: 1));\n\n      expect(find.text('200'), findsOneWidget);\n    });\n\n    testWidgets('add calculations and remove row', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      // Change two Fields to Number\n      await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Number);\n      await tester.changeFieldTypeOfFieldWithName('Done', FieldType.Number);\n\n      expect(find.text('Calculate'), findsNWidgets(2));\n\n      await tester.changeCalculateAtIndex(1, CalculationType.Sum);\n      await tester.changeCalculateAtIndex(2, CalculationType.Min);\n\n      // Enter values in cells\n      await tester.editCell(\n        rowIndex: 0,\n        fieldType: FieldType.Number,\n        input: '100',\n      );\n      await tester.editCell(\n        rowIndex: 1,\n        fieldType: FieldType.Number,\n        input: '150',\n      );\n      await tester.editCell(\n        rowIndex: 0,\n        fieldType: FieldType.Number,\n        input: '50',\n        cellIndex: 1,\n      );\n      await tester.editCell(\n        rowIndex: 1,\n        fieldType: FieldType.Number,\n        input: '100',\n        cellIndex: 1,\n      );\n\n      await tester.pumpAndSettle();\n\n      // Dismiss edit cell\n      await tester.sendKeyDownEvent(LogicalKeyboardKey.enter);\n      await tester.pumpAndSettle();\n\n      expect(find.text('250'), findsOneWidget);\n      expect(find.text('50'), findsNWidgets(2));\n\n      // Delete 1st row\n      await tester.hoverOnFirstRowOfGrid();\n      await tester.tapRowMenuButtonInGrid();\n      await tester.tapDeleteOnRowMenu();\n\n      await tester.pumpAndSettle(const Duration(seconds: 1));\n\n      expect(find.text('150'), findsNWidgets(2));\n      expect(find.text('100'), findsNWidgets(2));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/grid/grid_edit_row_test.dart",
    "content": "import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:time/time.dart';\n\nimport '../../shared/database_test_op.dart';\nimport 'grid_test_extensions.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('grid edit row test:', () {\n    testWidgets('with sort configured', (tester) async {\n      await tester.openTestDatabase(v069GridFileName);\n\n      // get grid data\n      final unsorted = tester.getGridRows();\n\n      // add a sort\n      await tester.tapDatabaseSortButton();\n      await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');\n\n      final sorted = [\n        unsorted[7],\n        unsorted[8],\n        unsorted[1],\n        unsorted[9],\n        unsorted[11],\n        unsorted[10],\n        unsorted[6],\n        unsorted[12],\n        unsorted[2],\n        unsorted[0],\n        unsorted[3],\n        unsorted[5],\n        unsorted[4],\n      ];\n\n      List actual = tester.getGridRows();\n      expect(actual, orderedEquals(sorted));\n\n      await tester.editCell(\n        rowIndex: 4,\n        fieldType: FieldType.RichText,\n        input: \"x\",\n      );\n      await tester.pumpAndSettle(200.milliseconds);\n\n      final reSorted = [\n        unsorted[7],\n        unsorted[8],\n        unsorted[1],\n        unsorted[9],\n        unsorted[10],\n        unsorted[6],\n        unsorted[12],\n        unsorted[2],\n        unsorted[0],\n        unsorted[3],\n        unsorted[5],\n        unsorted[11],\n        unsorted[4],\n      ];\n\n      // verify grid data\n      actual = tester.getGridRows();\n      expect(actual, orderedEquals(reSorted));\n\n      // delete the sort\n      await tester.tapSortMenuInSettingBar();\n      await tester.tapDeleteAllSortsButton();\n\n      // verify grid data\n      actual = tester.getGridRows();\n      expect(actual, orderedEquals(unsorted));\n    });\n\n    testWidgets('with filter configured', (tester) async {\n      await tester.openTestDatabase(v069GridFileName);\n\n      // get grid data\n      final original = tester.getGridRows();\n\n      // create a filter\n      await tester.tapDatabaseFilterButton();\n      await tester.tapCreateFilterByFieldType(\n        FieldType.Checkbox,\n        'Registration Complete',\n      );\n\n      final filtered = [\n        original[1],\n        original[3],\n        original[5],\n        original[6],\n        original[7],\n        original[9],\n        original[12],\n      ];\n\n      // verify grid data\n      List actual = tester.getGridRows();\n      expect(actual, orderedEquals(filtered));\n      expect(actual.length, equals(7));\n      tester.assertNumberOfRowsInGridPage(7);\n\n      await tester.tapCheckboxCellInGrid(rowIndex: 0);\n      await tester.pumpAndSettle(200.milliseconds);\n\n      // verify grid data\n      actual = tester.getGridRows();\n      expect(actual.length, equals(6));\n      tester.assertNumberOfRowsInGridPage(6);\n      final edited = [\n        original[3],\n        original[5],\n        original[6],\n        original[7],\n        original[9],\n        original[12],\n      ];\n      expect(actual, orderedEquals(edited));\n\n      // delete the filter\n      await tester.tapFilterButtonInGrid('Registration Complete');\n      await tester\n          .tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));\n      await tester.tapDeleteFilterButtonInGrid();\n\n      // verify grid data\n      actual = tester.getGridRows();\n      expect(actual.length, equals(13));\n      tester.assertNumberOfRowsInGridPage(13);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/grid/grid_filter_and_sort_test.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/database_test_op.dart';\nimport 'grid_test_extensions.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('grid simultaneous sort and filter test:', () {\n    // testWidgets('delete filter with active sort', (tester) async {\n    //   await tester.openTestDatabase(v069GridFileName);\n\n    //   // get grid data\n    //   final original = tester.getGridRows();\n\n    //   // add a filter\n    //   await tester.tapDatabaseFilterButton();\n    //   await tester.tapCreateFilterByFieldType(\n    //     FieldType.Checkbox,\n    //     'Registration Complete',\n    //   );\n\n    //   // add a sort\n    //   await tester.tapDatabaseSortButton();\n    //   await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');\n\n    //   final filteredAndSorted = [\n    //     original[7],\n    //     original[1],\n    //     original[9],\n    //     original[6],\n    //     original[12],\n    //     original[3],\n    //     original[5],\n    //   ];\n\n    //   // verify grid data\n    //   List actual = tester.getGridRows();\n    //   expect(actual, orderedEquals(filteredAndSorted));\n\n    //   // delete the filter\n    //   await tester.tapFilterButtonInGrid('Registration Complete');\n    //   await tester\n    //       .tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));\n    //   await tester.tapDeleteFilterButtonInGrid();\n\n    //   final sorted = [\n    //     original[7],\n    //     original[8],\n    //     original[1],\n    //     original[9],\n    //     original[11],\n    //     original[10],\n    //     original[6],\n    //     original[12],\n    //     original[2],\n    //     original[0],\n    //     original[3],\n    //     original[5],\n    //     original[4],\n    //   ];\n\n    //   // verify grid data\n    //   actual = tester.getGridRows();\n    //   expect(actual, orderedEquals(sorted));\n    // });\n\n    testWidgets('delete sort with active fiilter', (tester) async {\n      await tester.openTestDatabase(v069GridFileName);\n\n      // get grid data\n      final original = tester.getGridRows();\n\n      // add a filter\n      await tester.tapDatabaseFilterButton();\n      await tester.tapCreateFilterByFieldType(\n        FieldType.Checkbox,\n        'Registration Complete',\n      );\n\n      // add a sort\n      await tester.tapDatabaseSortButton();\n      await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');\n\n      final filteredAndSorted = [\n        original[7],\n        original[1],\n        original[9],\n        original[6],\n        original[12],\n        original[3],\n        original[5],\n      ];\n\n      // verify grid data\n      List actual = tester.getGridRows();\n      expect(actual, orderedEquals(filteredAndSorted));\n\n      // delete the sort\n      await tester.tapSortMenuInSettingBar();\n      await tester.tapDeleteAllSortsButton();\n\n      final filtered = [\n        original[1],\n        original[3],\n        original[5],\n        original[6],\n        original[7],\n        original[9],\n        original[12],\n      ];\n\n      // verify grid data\n      actual = tester.getGridRows();\n      expect(actual, orderedEquals(filtered));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/grid/grid_reopen_test.dart",
    "content": "import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/util.dart';\nimport 'grid_test_extensions.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('grid reopen test:', () {\n    testWidgets('base case', (tester) async {\n      await tester.openTestDatabase(v069GridFileName);\n\n      final expected = tester.getGridRows();\n\n      // go to another page and come back\n      await tester.openPage('Getting started');\n      await tester.openPage('v069', layout: ViewLayoutPB.Grid);\n\n      // verify grid data\n      final actual = tester.getGridRows();\n\n      expect(actual, orderedEquals(expected));\n    });\n\n    testWidgets('with sort configured', (tester) async {\n      await tester.openTestDatabase(v069GridFileName);\n\n      // get grid data\n      final unsorted = tester.getGridRows();\n\n      // add a sort\n      await tester.tapDatabaseSortButton();\n      await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');\n\n      final sorted = [\n        unsorted[7],\n        unsorted[8],\n        unsorted[1],\n        unsorted[9],\n        unsorted[11],\n        unsorted[10],\n        unsorted[6],\n        unsorted[12],\n        unsorted[2],\n        unsorted[0],\n        unsorted[3],\n        unsorted[5],\n        unsorted[4],\n      ];\n\n      // verify grid data\n      List actual = tester.getGridRows();\n      expect(actual, orderedEquals(sorted));\n\n      // go to another page and come back\n      await tester.openPage('Getting started');\n      await tester.openPage('v069', layout: ViewLayoutPB.Grid);\n\n      // verify grid data\n      actual = tester.getGridRows();\n      expect(actual, orderedEquals(sorted));\n\n      // delete sorts\n      // TODO(RS): Shouldn't the sort/filter list show automatically!?\n      await tester.tapDatabaseSortButton();\n      await tester.tapSortMenuInSettingBar();\n      await tester.tapDeleteAllSortsButton();\n\n      // verify grid data\n      actual = tester.getGridRows();\n      expect(actual, orderedEquals(unsorted));\n\n      // go to another page and come back\n      await tester.openPage('Getting started');\n      await tester.openPage('v069', layout: ViewLayoutPB.Grid);\n\n      // verify grid data\n      actual = tester.getGridRows();\n      expect(actual, orderedEquals(unsorted));\n    });\n\n    testWidgets('with filter configured', (tester) async {\n      await tester.openTestDatabase(v069GridFileName);\n\n      // get grid data\n      final unfiltered = tester.getGridRows();\n\n      // add a filter\n      await tester.tapDatabaseFilterButton();\n      await tester.tapCreateFilterByFieldType(\n        FieldType.Checkbox,\n        'Registration Complete',\n      );\n\n      final filtered = [\n        unfiltered[1],\n        unfiltered[3],\n        unfiltered[5],\n        unfiltered[6],\n        unfiltered[7],\n        unfiltered[9],\n        unfiltered[12],\n      ];\n\n      // verify grid data\n      List actual = tester.getGridRows();\n      expect(actual, orderedEquals(filtered));\n\n      // go to another page and come back\n      await tester.openPage('Getting started');\n      await tester.openPage('v069', layout: ViewLayoutPB.Grid);\n\n      // verify grid data\n      actual = tester.getGridRows();\n      expect(actual, orderedEquals(filtered));\n\n      // delete the filter\n      // TODO(RS): Shouldn't the sort/filter list show automatically!?\n      await tester.tapDatabaseFilterButton();\n      await tester.tapFilterButtonInGrid('Registration Complete');\n      await tester\n          .tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));\n      await tester.tapDeleteFilterButtonInGrid();\n\n      // verify grid data\n      actual = tester.getGridRows();\n      expect(actual, orderedEquals(unfiltered));\n\n      // go to another page and come back\n      await tester.openPage('Getting started');\n      await tester.openPage('v069', layout: ViewLayoutPB.Grid);\n\n      // verify grid data\n      actual = tester.getGridRows();\n      expect(actual, orderedEquals(unfiltered));\n    });\n\n    testWidgets('with both filter and sort configured', (tester) async {\n      await tester.openTestDatabase(v069GridFileName);\n\n      // get grid data\n      final original = tester.getGridRows();\n\n      // add a filter\n      await tester.tapDatabaseFilterButton();\n      await tester.tapCreateFilterByFieldType(\n        FieldType.Checkbox,\n        'Registration Complete',\n      );\n\n      // add a sort\n      await tester.tapDatabaseSortButton();\n      await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');\n\n      final filteredAndSorted = [\n        original[7],\n        original[1],\n        original[9],\n        original[6],\n        original[12],\n        original[3],\n        original[5],\n      ];\n\n      // verify grid data\n      List actual = tester.getGridRows();\n      expect(actual, orderedEquals(filteredAndSorted));\n\n      // go to another page and come back\n      await tester.openPage('Getting started');\n      await tester.openPage('v069', layout: ViewLayoutPB.Grid);\n\n      // verify grid data\n      actual = tester.getGridRows();\n      expect(actual, orderedEquals(filteredAndSorted));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/grid/grid_reorder_row_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/util.dart';\nimport 'grid_test_extensions.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('grid reorder row test:', () {\n    testWidgets('base case', (tester) async {\n      await tester.openTestDatabase(v069GridFileName);\n\n      // get grid data\n      final original = tester.getGridRows();\n\n      // reorder row\n      await tester.reorderRow(original[4], original[1]);\n\n      // verify grid data\n      List reordered = [\n        original[0],\n        original[4],\n        original[1],\n        original[2],\n        original[3],\n        original[5],\n        original[6],\n        original[7],\n        original[8],\n        original[9],\n        original[10],\n        original[11],\n        original[12],\n      ];\n      List actual = tester.getGridRows();\n      expect(actual, orderedEquals(reordered));\n\n      // reorder row\n      await tester.reorderRow(reordered[1], reordered[3]);\n\n      // verify grid data\n      reordered = [\n        original[0],\n        original[1],\n        original[2],\n        original[4],\n        original[3],\n        original[5],\n        original[6],\n        original[7],\n        original[8],\n        original[9],\n        original[10],\n        original[11],\n        original[12],\n      ];\n      actual = tester.getGridRows();\n      expect(actual, orderedEquals(reordered));\n\n      // reorder row\n      await tester.reorderRow(reordered[2], reordered[0]);\n\n      // verify grid data\n      reordered = [\n        original[2],\n        original[0],\n        original[1],\n        original[4],\n        original[3],\n        original[5],\n        original[6],\n        original[7],\n        original[8],\n        original[9],\n        original[10],\n        original[11],\n        original[12],\n      ];\n      actual = tester.getGridRows();\n      expect(actual, orderedEquals(reordered));\n    });\n\n    testWidgets('with active sort', (tester) async {\n      await tester.openTestDatabase(v069GridFileName);\n\n      // get grid data\n      final original = tester.getGridRows();\n\n      // add a sort\n      await tester.tapDatabaseSortButton();\n      await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');\n\n      // verify grid data\n      final sorted = [\n        original[7],\n        original[8],\n        original[1],\n        original[9],\n        original[11],\n        original[10],\n        original[6],\n        original[12],\n        original[2],\n        original[0],\n        original[3],\n        original[5],\n        original[4],\n      ];\n      List actual = tester.getGridRows();\n      expect(actual, orderedEquals(sorted));\n\n      // reorder row\n      await tester.reorderRow(original[4], original[1]);\n      expect(find.byType(ConfirmPopup), findsOneWidget);\n      await tester.tapButtonWithName(LocaleKeys.button_cancel.tr());\n\n      // verify grid data\n      actual = tester.getGridRows();\n      expect(actual, orderedEquals(sorted));\n    });\n\n    testWidgets('with active filter', (tester) async {\n      await tester.openTestDatabase(v069GridFileName);\n\n      // get grid data\n      final original = tester.getGridRows();\n\n      // add a filter\n      await tester.tapDatabaseFilterButton();\n      await tester.tapCreateFilterByFieldType(\n        FieldType.Checkbox,\n        'Registration Complete',\n      );\n\n      final filtered = [\n        original[1],\n        original[3],\n        original[5],\n        original[6],\n        original[7],\n        original[9],\n        original[12],\n      ];\n\n      // verify grid data\n      List actual = tester.getGridRows();\n      expect(actual, orderedEquals(filtered));\n\n      // reorder row\n      await tester.reorderRow(filtered[3], filtered[1]);\n\n      // verify grid data\n      List reordered = [\n        original[1],\n        original[6],\n        original[3],\n        original[5],\n        original[7],\n        original[9],\n        original[12],\n      ];\n      actual = tester.getGridRows();\n      expect(actual, orderedEquals(reordered));\n\n      // reorder row\n      await tester.reorderRow(reordered[3], reordered[5]);\n\n      // verify grid data\n      reordered = [\n        original[1],\n        original[6],\n        original[3],\n        original[7],\n        original[9],\n        original[5],\n        original[12],\n      ];\n      actual = tester.getGridRows();\n      expect(actual, orderedEquals(reordered));\n\n      // delete the filter\n      await tester.tapFilterButtonInGrid('Registration Complete');\n      await tester\n          .tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));\n      await tester.tapDeleteFilterButtonInGrid();\n\n      // verify grid data\n      final expected = [\n        original[0],\n        original[1],\n        original[2],\n        original[6],\n        original[3],\n        original[4],\n        original[7],\n        original[8],\n        original[9],\n        original[5],\n        original[10],\n        original[11],\n        original[12],\n      ];\n      actual = tester.getGridRows();\n      expect(actual, orderedEquals(expected));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/grid/grid_row_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/database_test_op.dart';\nimport '../../shared/util.dart';\n\nimport 'grid_test_extensions.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('grid row test:', () {\n    testWidgets('create from the bottom', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      final expected = tester.getGridRows();\n\n      // create row\n      await tester.tapCreateRowButtonInGrid();\n\n      final actual = tester.getGridRows();\n      expect(actual.slice(0, 3), orderedEquals(expected));\n      expect(actual.length, equals(4));\n      tester.assertNumberOfRowsInGridPage(4);\n    });\n\n    testWidgets('create from a row\\'s menu', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n\n      final expected = tester.getGridRows();\n\n      // create row\n      await tester.hoverOnFirstRowOfGrid();\n      await tester.tapCreateRowButtonAfterHoveringOnGridRow();\n\n      final actual = tester.getGridRows();\n      expect([actual[0], actual[2], actual[3]], orderedEquals(expected));\n      expect(actual.length, equals(4));\n      tester.assertNumberOfRowsInGridPage(4);\n    });\n\n    testWidgets('create with sort configured', (tester) async {\n      await tester.openTestDatabase(v069GridFileName);\n\n      // get grid data\n      final unsorted = tester.getGridRows();\n\n      // add a sort\n      await tester.tapDatabaseSortButton();\n      await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name');\n\n      final sorted = [\n        unsorted[7],\n        unsorted[8],\n        unsorted[1],\n        unsorted[9],\n        unsorted[11],\n        unsorted[10],\n        unsorted[6],\n        unsorted[12],\n        unsorted[2],\n        unsorted[0],\n        unsorted[3],\n        unsorted[5],\n        unsorted[4],\n      ];\n\n      List actual = tester.getGridRows();\n      expect(actual, orderedEquals(sorted));\n\n      // create row\n      await tester.hoverOnFirstRowOfGrid();\n      await tester.tapCreateRowButtonAfterHoveringOnGridRow();\n\n      // cancel\n      expect(find.byType(ConfirmPopup), findsOneWidget);\n      await tester.tapButtonWithName(LocaleKeys.button_cancel.tr());\n\n      // verify grid data\n      actual = tester.getGridRows();\n      expect(actual, orderedEquals(sorted));\n\n      // try again, but confirm this time\n      await tester.hoverOnFirstRowOfGrid();\n      await tester.tapCreateRowButtonAfterHoveringOnGridRow();\n      expect(find.byType(ConfirmPopup), findsOneWidget);\n      await tester.tapButtonWithName(LocaleKeys.button_remove.tr());\n\n      // verify grid data\n      actual = tester.getGridRows();\n      expect(actual.length, equals(14));\n      tester.assertNumberOfRowsInGridPage(14);\n    });\n\n    testWidgets('create with filter configured', (tester) async {\n      await tester.openTestDatabase(v069GridFileName);\n\n      // get grid data\n      final original = tester.getGridRows();\n\n      // create a filter\n      await tester.tapDatabaseFilterButton();\n      await tester.tapCreateFilterByFieldType(\n        FieldType.Checkbox,\n        'Registration Complete',\n      );\n\n      final filtered = [\n        original[1],\n        original[3],\n        original[5],\n        original[6],\n        original[7],\n        original[9],\n        original[12],\n      ];\n\n      // verify grid data\n      List actual = tester.getGridRows();\n      expect(actual, orderedEquals(filtered));\n\n      // create row (one before and after the first row, and one at the bottom)\n      await tester.tapCreateRowButtonInGrid();\n      await tester.hoverOnFirstRowOfGrid();\n      await tester.tapCreateRowButtonAfterHoveringOnGridRow();\n      await tester.hoverOnFirstRowOfGrid(() async {\n        await tester.tapRowMenuButtonInGrid();\n        await tester.tapCreateRowAboveButtonInRowMenu();\n      });\n\n      actual = tester.getGridRows();\n      expect(actual.length, equals(10));\n      tester.assertNumberOfRowsInGridPage(10);\n      actual = [\n        actual[1],\n        actual[3],\n        actual[4],\n        actual[5],\n        actual[6],\n        actual[7],\n        actual[8],\n      ];\n      expect(actual, orderedEquals(filtered));\n\n      // delete the filter\n      await tester.tapFilterButtonInGrid('Registration Complete');\n      await tester\n          .tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor));\n      await tester.tapDeleteFilterButtonInGrid();\n\n      // verify grid data\n      actual = tester.getGridRows();\n      expect(actual.length, equals(16));\n      tester.assertNumberOfRowsInGridPage(16);\n      actual = [\n        actual[0],\n        actual[2],\n        actual[4],\n        actual[5],\n        actual[6],\n        actual[7],\n        actual[8],\n        actual[9],\n        actual[10],\n        actual[11],\n        actual[12],\n        actual[13],\n        actual[14],\n      ];\n      expect(actual, orderedEquals(original));\n    });\n\n    testWidgets('delete row of the grid', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n      await tester.hoverOnFirstRowOfGrid(() async {\n        // Open the row menu and click the delete button\n        await tester.tapRowMenuButtonInGrid();\n        await tester.tapDeleteOnRowMenu();\n      });\n      expect(find.byType(ConfirmPopup), findsOneWidget);\n      await tester.tapButtonWithName(LocaleKeys.button_delete.tr());\n\n      tester.assertNumberOfRowsInGridPage(2);\n    });\n\n    testWidgets('delete row in two views', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);\n      await tester.renameLinkedView(\n        tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Grid),\n        'grid 1',\n      );\n      tester.assertNumberOfRowsInGridPage(3);\n\n      await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid);\n      await tester.renameLinkedView(\n        tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Grid).at(1),\n        'grid 2',\n      );\n      tester.assertNumberOfRowsInGridPage(3);\n\n      await tester.hoverOnFirstRowOfGrid(() async {\n        // Open the row menu and click the delete button\n        await tester.tapRowMenuButtonInGrid();\n        await tester.tapDeleteOnRowMenu();\n      });\n      expect(find.byType(ConfirmPopup), findsOneWidget);\n      await tester.tapButtonWithName(LocaleKeys.button_delete.tr());\n      // 3 initial rows - 1 deleted\n      tester.assertNumberOfRowsInGridPage(2);\n\n      await tester.tapTabBarLinkedViewByViewName('grid 1');\n      tester.assertNumberOfRowsInGridPage(2);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/grid/grid_test_extensions.dart",
    "content": "import 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nextension GridTestExtensions on WidgetTester {\n  List<RowId> getGridRows() {\n    final databaseController =\n        widget<GridPage>(find.byType(GridPage)).databaseController;\n    return [\n      ...databaseController.rowCache.rowInfos.map((e) => e.rowId),\n    ];\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/grid/grid_test_runner_1.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'grid_edit_row_test.dart' as grid_edit_row_test_runner;\nimport 'grid_filter_and_sort_test.dart' as grid_filter_and_sort_test_runner;\nimport 'grid_reopen_test.dart' as grid_reopen_test_runner;\nimport 'grid_reorder_row_test.dart' as grid_reorder_row_test_runner;\nimport 'grid_row_test.dart' as grid_row_test_runner;\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  grid_reopen_test_runner.main();\n  grid_row_test_runner.main();\n  grid_reorder_row_test_runner.main();\n  grid_filter_and_sort_test_runner.main();\n  grid_edit_row_test_runner.main();\n  // grid_calculations_test_runner.main();\n  // DON'T add more tests here.\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/reminder/document_reminder_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';\nimport 'package:appflowy/user/application/user_settings_service.dart';\nimport 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:calendar_view/calendar_view.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text_field.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/base.dart';\nimport '../../shared/common_operations.dart';\nimport '../../shared/document_test_operations.dart';\nimport '../../shared/expectation.dart';\nimport '../../shared/keyboard.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Reminder in Document', () {\n    testWidgets('Add reminder for tomorrow, and include time', (tester) async {\n      const time = \"23:59\";\n\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      final dateTimeSettings =\n          await UserSettingsBackendService().getDateTimeSettings();\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.getCurrentEditorState().insertNewLine();\n\n      await tester.pumpAndSettle();\n\n      // Trigger inline action menu and type 'remind tomorrow'\n      final tomorrow = await _insertReminderTomorrow(tester);\n\n      Node node = tester.editor.getCurrentEditorState().getNodeAtPath([1])!;\n      Map<String, dynamic> mentionAttr =\n          node.delta!.first.attributes![MentionBlockKeys.mention];\n\n      expect(node.type, 'paragraph');\n      expect(mentionAttr['type'], MentionType.date.name);\n      expect(mentionAttr['date'], tomorrow.toIso8601String());\n\n      await tester.tap(\n        find.text(dateTimeSettings.dateFormat.formatDate(tomorrow, false)),\n      );\n      await tester.pumpAndSettle();\n\n      await tester.tap(find.byType(Toggle));\n      await tester.pumpAndSettle();\n\n      await tester.enterText(find.byType(FlowyTextField), time);\n\n      // Leave text field to submit\n      await tester.tap(find.text(LocaleKeys.grid_field_includeTime.tr()));\n      await tester.pumpAndSettle();\n\n      node = tester.editor.getCurrentEditorState().getNodeAtPath([1])!;\n      mentionAttr = node.delta!.first.attributes![MentionBlockKeys.mention];\n\n      final tomorrowWithTime =\n          _dateWithTime(dateTimeSettings.timeFormat, tomorrow, time);\n\n      expect(node.type, 'paragraph');\n      expect(mentionAttr['type'], MentionType.date.name);\n      expect(mentionAttr['date'], tomorrowWithTime.toIso8601String());\n    });\n\n    testWidgets('Add reminder for tomorrow, and navigate to it',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.editor.getCurrentEditorState().insertNewLine();\n\n      await tester.pumpAndSettle();\n\n      // Trigger inline action menu and type 'remind tomorrow'\n      final tomorrow = await _insertReminderTomorrow(tester);\n\n      final Node node =\n          tester.editor.getCurrentEditorState().getNodeAtPath([1])!;\n      final Map<String, dynamic> mentionAttr =\n          node.delta!.first.attributes![MentionBlockKeys.mention];\n\n      expect(node.type, 'paragraph');\n      expect(mentionAttr['type'], MentionType.date.name);\n      expect(mentionAttr['date'], tomorrow.toIso8601String());\n\n      // Create and Navigate to a new document\n      await tester.createNewPageWithNameUnderParent();\n      await tester.pumpAndSettle();\n\n      // Open \"Upcoming\" in Notification hub\n      await tester.openNotificationHub(tabIndex: 1);\n\n      // Expect 1 notification\n      tester.expectNotificationItems(1);\n\n      // Tap on the notification\n      await tester.tap(find.byType(NotificationItem));\n      await tester.pumpAndSettle();\n\n      // Expect node at path 1 to be the date/reminder\n      expect(\n        tester.editor\n            .getCurrentEditorState()\n            .getNodeAtPath([1])\n            ?.delta\n            ?.first\n            .attributes?[MentionBlockKeys.mention]['type'],\n        MentionType.date.name,\n      );\n    });\n  });\n}\n\nFuture<DateTime> _insertReminderTomorrow(WidgetTester tester) async {\n  await tester.editor.showAtMenu();\n\n  await FlowyTestKeyboard.simulateKeyDownEvent(\n    [\n      LogicalKeyboardKey.keyR,\n      LogicalKeyboardKey.keyE,\n      LogicalKeyboardKey.keyM,\n      LogicalKeyboardKey.keyI,\n      LogicalKeyboardKey.keyN,\n      LogicalKeyboardKey.keyD,\n      LogicalKeyboardKey.space,\n      LogicalKeyboardKey.keyT,\n      LogicalKeyboardKey.keyO,\n      LogicalKeyboardKey.keyM,\n      LogicalKeyboardKey.keyO,\n      LogicalKeyboardKey.keyR,\n      LogicalKeyboardKey.keyR,\n      LogicalKeyboardKey.keyO,\n      LogicalKeyboardKey.keyW,\n    ],\n    tester: tester,\n  );\n\n  await FlowyTestKeyboard.simulateKeyDownEvent(\n    [LogicalKeyboardKey.enter],\n    tester: tester,\n  );\n\n  return DateTime.now().add(const Duration(days: 1)).withoutTime;\n}\n\nDateTime _dateWithTime(UserTimeFormatPB format, DateTime date, String time) {\n  final t = format == UserTimeFormatPB.TwelveHour\n      ? DateFormat.jm().parse(time)\n      : DateFormat.Hm().parse(time);\n\n  return DateTime.parse(\n    '${date.year}${_padZeroLeft(date.month)}${_padZeroLeft(date.day)} ${_padZeroLeft(t.hour)}:${_padZeroLeft(t.minute)}',\n  );\n}\n\nString _padZeroLeft(int a) => a.toString().padLeft(2, '0');\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/settings/notifications_settings_test.dart",
    "content": "import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('notification test', () {\n    testWidgets('enable notification', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.openSettings();\n      await tester.openSettingsPage(SettingsPage.notifications);\n      await tester.pumpAndSettle();\n\n      final toggleFinder = find.byType(Toggle).first;\n\n      // Defaults to enabled\n      Toggle toggleWidget = tester.widget(toggleFinder);\n      expect(toggleWidget.value, true);\n\n      // Disable\n      await tester.tap(toggleFinder);\n      await tester.pumpAndSettle();\n\n      toggleWidget = tester.widget(toggleFinder);\n      expect(toggleWidget.value, false);\n\n      // Enable again\n      await tester.tap(toggleFinder);\n      await tester.pumpAndSettle();\n\n      toggleWidget = tester.widget(toggleFinder);\n      expect(toggleWidget.value, true);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/settings/settings_billing_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/auth_operation.dart';\nimport '../../shared/base.dart';\nimport '../../shared/expectation.dart';\nimport '../../shared/settings.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Settings Billing', () {\n    testWidgets('Local auth cannot see plan+billing', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapSignInAsGuest();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      await tester.openSettings();\n      await tester.pumpAndSettle();\n\n      // We check that another settings page is present to ensure\n      // it's not a fluke\n      expect(\n        find.text(\n          LocaleKeys.settings_workspacePage_menuLabel.tr(),\n          skipOffstage: false,\n        ),\n        findsOneWidget,\n      );\n\n      expect(\n        find.text(\n          LocaleKeys.settings_planPage_menuLabel.tr(),\n          skipOffstage: false,\n        ),\n        findsNothing,\n      );\n\n      expect(\n        find.text(\n          LocaleKeys.settings_billingPage_menuLabel.tr(),\n          skipOffstage: false,\n        ),\n        findsNothing,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'notifications_settings_test.dart' as notifications_settings_test;\nimport 'settings_billing_test.dart' as settings_billing_test;\nimport 'shortcuts_settings_test.dart' as shortcuts_settings_test;\nimport 'sign_in_page_settings_test.dart' as sign_in_page_settings_test;\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  notifications_settings_test.main();\n  settings_billing_test.main();\n  shortcuts_settings_test.main();\n  sign_in_page_settings_test.main();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/settings/shortcuts_settings_test.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/settings_shortcuts_view.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/keyboard.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('shortcuts:', () {\n    testWidgets('change and overwrite shortcut', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await tester.openSettings();\n      await tester.openSettingsPage(SettingsPage.shortcuts);\n      await tester.pumpAndSettle();\n\n      final backspaceCmd =\n          LocaleKeys.settings_shortcutsPage_keybindings_backspace.tr();\n\n      // Input \"Delete\" into the search field\n      final inputField = find.descendant(\n        of: find.byType(SettingsShortcutsView),\n        matching: find.byType(TextField),\n      );\n      await tester.enterText(inputField, backspaceCmd);\n      await tester.pumpAndSettle();\n\n      await tester.hoverOnWidget(\n        find\n            .descendant(\n              of: find.byType(ShortcutSettingTile),\n              matching: find.text(backspaceCmd),\n            )\n            .first,\n        onHover: () async {\n          await tester.tap(find.byFlowySvg(FlowySvgs.edit_s));\n          await tester.pumpAndSettle();\n\n          await FlowyTestKeyboard.simulateKeyDownEvent(\n            [\n              LogicalKeyboardKey.delete,\n              LogicalKeyboardKey.enter,\n            ],\n            tester: tester,\n          );\n          await tester.pumpAndSettle();\n        },\n      );\n\n      // We expect to see conflict dialog\n      expect(\n        find.text(\n          LocaleKeys.settings_shortcutsPage_conflictDialog_confirmLabel.tr(),\n        ),\n        findsOneWidget,\n      );\n\n      // Press on confirm label\n      await tester.tap(\n        find.text(\n          LocaleKeys.settings_shortcutsPage_conflictDialog_confirmLabel.tr(),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      // We expect the first ShortcutSettingTile to have one\n      // [KeyBadge] with `delete` label\n      final first = tester.widget(find.byType(ShortcutSettingTile).first)\n          as ShortcutSettingTile;\n      expect(\n        first.command.command,\n        'delete',\n      );\n\n      // And the second one which is `Delete left character` to have none\n      // as it will have been overwritten\n      final second = tester.widget(find.byType(ShortcutSettingTile).at(1))\n          as ShortcutSettingTile;\n      expect(\n        second.command.command,\n        '',\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/settings/sign_in_page_settings_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart';\nimport 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  Finder findServerType(AuthenticatorType type) {\n    return find\n        .descendant(\n          of: find.byType(SettingsServerDropdownMenu),\n          matching: find.findTextInFlowyText(\n            type.label,\n          ),\n        )\n        .last;\n  }\n\n  group('sign-in page settings:', () {\n    testWidgets('change server type', (tester) async {\n      await tester.initializeAppFlowy();\n\n      // reset the app to the default state\n      await useAppFlowyBetaCloudWithURL(\n        kAppflowyCloudUrl,\n        AuthenticatorType.appflowyCloud,\n      );\n\n      // open the settings page\n      final settingsButton = find.byType(DesktopSignInSettingsButton);\n      await tester.tapButton(settingsButton);\n\n      expect(find.byType(SimpleSettingsDialog), findsOneWidget);\n\n      // the default type should be appflowy cloud\n      final appflowyCloudType = findServerType(AuthenticatorType.appflowyCloud);\n      expect(appflowyCloudType, findsOneWidget);\n\n      // change the server type to self-host\n      await tester.tapButton(appflowyCloudType);\n      final selfHostedButton = findServerType(\n        AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapButton(selfHostedButton);\n\n      // update server url\n      const serverUrl = 'https://self-hosted.appflowy.cloud';\n      await tester.enterText(\n        find.byKey(kSelfHostedTextInputFieldKey),\n        serverUrl,\n      );\n      await tester.pumpAndSettle();\n      // update the web url\n      const webUrl = 'https://self-hosted.appflowy.com';\n      await tester.enterText(\n        find.byKey(kSelfHostedWebTextInputFieldKey),\n        webUrl,\n      );\n      await tester.pumpAndSettle();\n      await tester.tapButton(\n        find.findTextInFlowyText(LocaleKeys.button_save.tr()),\n      );\n\n      // wait the app to restart, and the tooltip to disappear\n      await tester.pumpUntilNotFound(find.byType(DesktopToast));\n      await tester.pumpAndSettle(const Duration(milliseconds: 250));\n\n      // open settings page to check the result\n      await tester.tapButton(settingsButton);\n      await tester.pumpAndSettle(const Duration(milliseconds: 250));\n\n      // check the server type\n      expect(\n        findServerType(AuthenticatorType.appflowyCloudSelfHost),\n        findsOneWidget,\n      );\n      // check the server url\n      expect(\n        find.text(serverUrl),\n        findsOneWidget,\n      );\n      // check the web url\n      expect(\n        find.text(webUrl),\n        findsOneWidget,\n      );\n\n      // reset to appflowy cloud\n      await tester.tapButton(\n        findServerType(AuthenticatorType.appflowyCloudSelfHost),\n      );\n      // change the server type to appflowy cloud\n      await tester.tapButton(\n        findServerType(AuthenticatorType.appflowyCloud),\n      );\n\n      // wait the app to restart, and the tooltip to disappear\n      await tester.pumpUntilNotFound(find.byType(DesktopToast));\n      await tester.pumpAndSettle(const Duration(milliseconds: 250));\n\n      // check the server type\n      await tester.tapButton(settingsButton);\n      expect(\n        findServerType(AuthenticatorType.appflowyCloud),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/sidebar/rename_current_item_test.dart",
    "content": "import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flowy_infra_ui/style_widget/text_field.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/keyboard.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Rename current view item', () {\n    testWidgets('by F2 shortcut', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      await FlowyTestKeyboard.simulateKeyDownEvent(\n        [LogicalKeyboardKey.f2],\n        tester: tester,\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(RenameViewPopover), findsOneWidget);\n\n      await tester.enterText(\n        find.descendant(\n          of: find.byType(RenameViewPopover),\n          matching: find.byType(FlowyTextField),\n        ),\n        'hello',\n      );\n      await tester.pumpAndSettle();\n\n      // Dismiss rename popover\n      await tester.tap(find.byType(AppFlowyEditor));\n      await tester.pumpAndSettle();\n\n      expect(\n        find.descendant(\n          of: find.byType(SingleInnerViewItem),\n          matching: find.text('hello'),\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('sidebar expand test', () {\n    bool isExpanded({required FolderSpaceType type}) {\n      if (type == FolderSpaceType.private) {\n        return find\n            .descendant(\n              of: find.byType(PrivateSectionFolder),\n              matching: find.byType(ViewItem),\n            )\n            .evaluate()\n            .isNotEmpty;\n      }\n      return false;\n    }\n\n    testWidgets('first time the personal folder is expanded', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // first time is expanded\n      expect(isExpanded(type: FolderSpaceType.private), true);\n\n      // collapse the personal folder\n      await tester.tapButton(\n        find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()),\n      );\n      expect(isExpanded(type: FolderSpaceType.private), false);\n\n      // expand the personal folder\n      await tester.tapButton(\n        find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()),\n      );\n      expect(isExpanded(type: FolderSpaceType.private), true);\n    });\n\n    testWidgets('Expanding with subpage', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      const page1 = 'SubPageBloc', page2 = '$page1 2';\n      await tester.createNewPageWithNameUnderParent(name: page1);\n      await tester.createNewPageWithNameUnderParent(\n        name: page2,\n        parentName: page1,\n      );\n\n      await tester.expandOrCollapsePage(\n        pageName: gettingStarted,\n        layout: ViewLayoutPB.Document,\n      );\n\n      await tester.tapNewPageButton();\n\n      await tester.editor.tapLineOfEditorAt(0);\n      await tester.pumpAndSettle();\n      await tester.editor.showSlashMenu();\n      await tester.pumpAndSettle();\n\n      final slashMenu = find\n          .ancestor(\n            of: find.byType(SelectionMenuItemWidget),\n            matching: find.byWidgetPredicate(\n              (widget) => widget is Scrollable,\n            ),\n          )\n          .first;\n      final slashMenuItem = find.text(\n        LocaleKeys.document_slashMenu_name_linkedDoc.tr(),\n      );\n      await tester.scrollUntilVisible(\n        slashMenuItem,\n        100,\n        scrollable: slashMenu,\n        duration: const Duration(milliseconds: 250),\n      );\n\n      final menuItemFinder = find.byWidgetPredicate(\n        (w) =>\n            w is SelectionMenuItemWidget &&\n            w.item.name == LocaleKeys.document_slashMenu_name_linkedDoc.tr(),\n      );\n\n      final menuItem =\n          menuItemFinder.evaluate().first.widget as SelectionMenuItemWidget;\n\n      /// tapSlashMenuItemWithName is not working, so invoke this function directly\n      menuItem.item.handler(\n        menuItem.editorState,\n        menuItem.menuService,\n        menuItemFinder.evaluate().first,\n      );\n\n      await tester.pumpAndSettle();\n      final actionHandler = find.byType(InlineActionsHandler);\n      final subPage = find.descendant(\n        of: actionHandler,\n        matching: find.text(page2, findRichText: true),\n      );\n      await tester.tapButton(subPage);\n\n      final subpageBlock = find.descendant(\n        of: find.byType(AppFlowyEditor),\n        matching: find.text(page2, findRichText: true),\n      );\n\n      expect(find.text(page2, findRichText: true), findsOneWidget);\n      await tester.tapButton(subpageBlock);\n\n      /// one is in SectionFolder, another one is in CoverTitle\n      /// the last one is in FlowyNavigation\n      expect(find.text(page2, findRichText: true), findsNWidgets(3));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart",
    "content": "import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/base.dart';\nimport '../../shared/common_operations.dart';\nimport '../../shared/expectation.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Favorites', () {\n    testWidgets(\n        'Toggle favorites for views creates / removes the favorite header along with favorite views',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // no favorite folder\n      expect(find.byType(FavoriteFolder), findsNothing);\n\n      // create the nested views\n      final names = [\n        1,\n        2,\n      ].map((e) => 'document_$e').toList();\n      for (var i = 0; i < names.length; i++) {\n        final parentName = i == 0 ? gettingStarted : names[i - 1];\n        await tester.createNewPageWithNameUnderParent(\n          name: names[i],\n          parentName: parentName,\n        );\n        tester.expectToSeePageName(names[i], parentName: parentName);\n      }\n\n      await tester.favoriteViewByName(gettingStarted);\n      expect(\n        tester.findFavoritePageName(gettingStarted),\n        findsOneWidget,\n      );\n\n      await tester.favoriteViewByName(names[1]);\n      expect(\n        tester.findFavoritePageName(names[1]),\n        findsNWidgets(1),\n      );\n\n      await tester.unfavoriteViewByName(gettingStarted);\n      expect(\n        tester.findFavoritePageName(gettingStarted),\n        findsNothing,\n      );\n      expect(\n        tester.findFavoritePageName(\n          names[1],\n        ),\n        findsOneWidget,\n      );\n\n      await tester.unfavoriteViewByName(names[1]);\n      expect(\n        tester.findFavoritePageName(\n          names[1],\n        ),\n        findsNothing,\n      );\n    });\n\n    testWidgets(\n      'renaming a favorite view updates name under favorite header',\n      (tester) async {\n        await tester.initializeAppFlowy();\n        await tester.tapAnonymousSignInButton();\n\n        const name = 'test';\n        await tester.favoriteViewByName(gettingStarted);\n        await tester.hoverOnPageName(\n          gettingStarted,\n          onHover: () async {\n            await tester.renamePage(name);\n            await tester.pumpAndSettle();\n          },\n        );\n        expect(\n          tester.findPageName(name),\n          findsNWidgets(2),\n        );\n        expect(\n          tester.findFavoritePageName(name),\n          findsOneWidget,\n        );\n      },\n    );\n\n    testWidgets(\n      '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',\n      (tester) async {\n        await tester.initializeAppFlowy();\n        await tester.tapAnonymousSignInButton();\n\n        final names = [1, 2].map((e) => 'document_$e').toList();\n        for (var i = 0; i < names.length; i++) {\n          final parentName = i == 0 ? gettingStarted : names[i - 1];\n          await tester.createNewPageWithNameUnderParent(\n            name: names[i],\n            parentName: parentName,\n          );\n          tester.expectToSeePageName(names[i], parentName: parentName);\n        }\n        await tester.favoriteViewByName(gettingStarted);\n        await tester.favoriteViewByName(names[0]);\n        await tester.favoriteViewByName(names[1]);\n\n        expect(\n          find.byWidgetPredicate(\n            (widget) =>\n                widget is SingleInnerViewItem &&\n                widget.view.isFavorite &&\n                widget.spaceType == FolderSpaceType.favorite,\n          ),\n          findsNWidgets(3),\n        );\n\n        await tester.hoverOnPageName(\n          names[1],\n          onHover: () async {\n            await tester.tapDeletePageButton();\n            await tester.pumpAndSettle();\n          },\n        );\n\n        expect(\n          tester.findAllFavoritePages(),\n          findsNWidgets(2),\n        );\n\n        await tester.hoverOnPageName(\n          gettingStarted,\n          onHover: () async {\n            await tester.tapDeletePageButton();\n            await tester.pumpAndSettle();\n          },\n        );\n\n        expect(\n          tester.findAllFavoritePages(),\n          findsNothing,\n        );\n      },\n    );\n\n    testWidgets(\n      'view selection is synced between favorites and personal folder',\n      (tester) async {\n        await tester.initializeAppFlowy();\n        await tester.tapAnonymousSignInButton();\n\n        await tester.createNewPageWithNameUnderParent();\n        await tester.favoriteViewByName(gettingStarted);\n        expect(\n          find.byWidgetPredicate(\n            (widget) =>\n                widget is FlowyHover &&\n                widget.isSelected != null &&\n                widget.isSelected!(),\n          ),\n          findsNWidgets(1),\n        );\n      },\n    );\n\n    testWidgets(\n      'context menu opens up for favorites',\n      (tester) async {\n        await tester.initializeAppFlowy();\n        await tester.tapAnonymousSignInButton();\n\n        await tester.createNewPageWithNameUnderParent();\n        await tester.favoriteViewByName(gettingStarted);\n        await tester.hoverOnPageName(\n          gettingStarted,\n          useLast: false,\n          onHover: () async {\n            await tester.tapPageOptionButton();\n            await tester.pumpAndSettle();\n            expect(\n              find.byType(PopoverContainer),\n              findsOneWidget,\n            );\n          },\n        );\n        await tester.pumpAndSettle();\n      },\n    );\n\n    testWidgets(\n      'reorder favorites',\n      (tester) async {\n        await tester.initializeAppFlowy();\n        await tester.tapAnonymousSignInButton();\n\n        /// there are no favorite views\n        final favorites = find.descendant(\n          of: find.byType(FavoriteFolder),\n          matching: find.byType(ViewItem),\n        );\n        expect(favorites, findsNothing);\n\n        /// create views and then favorite them\n        const pageNames = ['001', '002', '003'];\n        for (final name in pageNames) {\n          await tester.createNewPageWithNameUnderParent(name: name);\n        }\n        for (final name in pageNames) {\n          await tester.favoriteViewByName(name);\n        }\n        expect(favorites, findsNWidgets(pageNames.length));\n\n        final oldNames = favorites\n            .evaluate()\n            .map((e) => (e.widget as ViewItem).view.name)\n            .toList();\n        expect(oldNames, pageNames);\n\n        /// drag first to last\n        await tester.reorderFavorite(\n          fromName: '001',\n          toName: '003',\n        );\n        List<String> newNames = favorites\n            .evaluate()\n            .map((e) => (e.widget as ViewItem).view.name)\n            .toList();\n        expect(newNames, ['002', '003', '001']);\n\n        /// drag first to second\n        await tester.reorderFavorite(\n          fromName: '002',\n          toName: '003',\n        );\n        newNames = favorites\n            .evaluate()\n            .map((e) => (e.widget as ViewItem).view.name)\n            .toList();\n        expect(newNames, ['003', '002', '001']);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart",
    "content": "import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';\nimport 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flowy_infra_ui/style_widget/text_field.dart';\nimport 'package:flowy_svg/flowy_svg.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/base.dart';\nimport '../../shared/common_operations.dart';\nimport '../../shared/emoji.dart';\nimport '../../shared/expectation.dart';\n\nvoid main() {\n  final emoji = EmojiIconData.emoji('😁');\n\n  setUpAll(() {\n    IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n    RecentIcons.enable = false;\n  });\n\n  tearDownAll(() {\n    RecentIcons.enable = true;\n  });\n\n  testWidgets('Update page emoji in sidebar', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n\n    // create document, board, grid and calendar views\n    for (final value in ViewLayoutPB.values) {\n      if (value == ViewLayoutPB.Chat) {\n        continue;\n      }\n      await tester.createNewPageWithNameUnderParent(\n        name: value.name,\n        parentName: gettingStarted,\n        layout: value,\n      );\n\n      // update its emoji\n      await tester.updatePageIconInSidebarByName(\n        name: value.name,\n        parentName: gettingStarted,\n        layout: value,\n        icon: emoji,\n      );\n\n      tester.expectViewHasIcon(\n        value.name,\n        value,\n        emoji,\n      );\n    }\n  });\n\n  testWidgets('Update page emoji in title bar', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n\n    // create document, board, grid and calendar views\n    for (final value in ViewLayoutPB.values) {\n      if (value == ViewLayoutPB.Chat) {\n        continue;\n      }\n\n      await tester.createNewPageWithNameUnderParent(\n        name: value.name,\n        parentName: gettingStarted,\n        layout: value,\n      );\n\n      // update its emoji\n      await tester.updatePageIconInTitleBarByName(\n        name: value.name,\n        layout: value,\n        icon: emoji,\n      );\n\n      tester.expectViewHasIcon(\n        value.name,\n        value,\n        emoji,\n      );\n\n      tester.expectViewTitleHasIcon(\n        value.name,\n        value,\n        emoji,\n      );\n    }\n  });\n\n  testWidgets('Emoji Search Bar Get Focus', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n\n    // create document, board, grid and calendar views\n    for (final value in ViewLayoutPB.values) {\n      if (value == ViewLayoutPB.Chat) {\n        continue;\n      }\n\n      await tester.createNewPageWithNameUnderParent(\n        name: value.name,\n        parentName: gettingStarted,\n        layout: value,\n      );\n\n      await tester.openPage(\n        value.name,\n        layout: value,\n      );\n      final title = find.descendant(\n        of: find.byType(ViewTitleBar),\n        matching: find.text(value.name),\n      );\n      await tester.tapButton(title);\n      await tester.tapButton(find.byType(EmojiPickerButton));\n\n      final emojiPicker = find.byType(FlowyEmojiPicker);\n      expect(emojiPicker, findsOneWidget);\n      final textField = find.descendant(\n        of: emojiPicker,\n        matching: find.byType(FlowyTextField),\n      );\n      expect(textField, findsOneWidget);\n      final textFieldWidget =\n          textField.evaluate().first.widget as FlowyTextField;\n      assert(textFieldWidget.focusNode!.hasFocus);\n      await tester.tapEmoji(emoji.emoji);\n      await tester.pumpAndSettle();\n      tester.expectViewHasIcon(\n        value.name,\n        value,\n        emoji,\n      );\n    }\n  });\n\n  testWidgets('Update page icon in sidebar', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    final iconData = await tester.loadIcon();\n\n    // create document, board, grid and calendar views\n    for (final value in ViewLayoutPB.values) {\n      if (value == ViewLayoutPB.Chat) {\n        continue;\n      }\n      await tester.createNewPageWithNameUnderParent(\n        name: value.name,\n        parentName: gettingStarted,\n        layout: value,\n      );\n\n      // update its icon\n      await tester.updatePageIconInSidebarByName(\n        name: value.name,\n        parentName: gettingStarted,\n        layout: value,\n        icon: iconData,\n      );\n\n      tester.expectViewHasIcon(\n        value.name,\n        value,\n        iconData,\n      );\n    }\n  });\n\n  testWidgets('Update page icon in title bar', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    final iconData = await tester.loadIcon();\n\n    // create document, board, grid and calendar views\n    for (final value in ViewLayoutPB.values) {\n      if (value == ViewLayoutPB.Chat) {\n        continue;\n      }\n\n      await tester.createNewPageWithNameUnderParent(\n        name: value.name,\n        parentName: gettingStarted,\n        layout: value,\n      );\n\n      // update its icon\n      await tester.updatePageIconInTitleBarByName(\n        name: value.name,\n        layout: value,\n        icon: iconData,\n      );\n\n      tester.expectViewHasIcon(\n        value.name,\n        value,\n        iconData,\n      );\n\n      tester.expectViewTitleHasIcon(\n        value.name,\n        value,\n        iconData,\n      );\n    }\n  });\n\n  testWidgets('Update page custom image icon in title bar', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n\n    /// prepare local image\n    final iconData = await tester.prepareImageIcon();\n\n    // create document, board, grid and calendar views\n    for (final value in ViewLayoutPB.values) {\n      if (value == ViewLayoutPB.Chat) {\n        continue;\n      }\n\n      await tester.createNewPageWithNameUnderParent(\n        name: value.name,\n        parentName: gettingStarted,\n        layout: value,\n      );\n\n      // update its icon\n      await tester.updatePageIconInTitleBarByName(\n        name: value.name,\n        layout: value,\n        icon: iconData,\n      );\n\n      tester.expectViewHasIcon(\n        value.name,\n        value,\n        iconData,\n      );\n\n      tester.expectViewTitleHasIcon(\n        value.name,\n        value,\n        iconData,\n      );\n    }\n  });\n\n  testWidgets('Update page custom svg icon in title bar', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n\n    /// prepare local image\n    final iconData = await tester.prepareSvgIcon();\n\n    // create document, board, grid and calendar views\n    for (final value in ViewLayoutPB.values) {\n      if (value == ViewLayoutPB.Chat) {\n        continue;\n      }\n\n      await tester.createNewPageWithNameUnderParent(\n        name: value.name,\n        parentName: gettingStarted,\n        layout: value,\n      );\n\n      // update its icon\n      await tester.updatePageIconInTitleBarByName(\n        name: value.name,\n        layout: value,\n        icon: iconData,\n      );\n\n      tester.expectViewHasIcon(\n        value.name,\n        value,\n        iconData,\n      );\n\n      tester.expectViewTitleHasIcon(\n        value.name,\n        value,\n        iconData,\n      );\n    }\n  });\n\n  testWidgets('Update page custom svg icon in title bar by pasting a link',\n      (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n\n    /// prepare local image\n    const testIconLink =\n        'https://beta.appflowy.cloud/api/file_storage/008e6f23-516b-4d8d-b1fe-2b75c51eee26/v1/blob/6bdf8dff%2D0e54%2D4d35%2D9981%2Dcde68bef1141/BGpLnRtb3AGBNgSJsceu70j83zevYKrMLzqsTIJcBeI=.svg';\n\n    /// create document, board, grid and calendar views\n    for (final value in ViewLayoutPB.values) {\n      if (value == ViewLayoutPB.Chat) {\n        continue;\n      }\n\n      await tester.createNewPageWithNameUnderParent(\n        name: value.name,\n        parentName: gettingStarted,\n        layout: value,\n      );\n\n      /// update its icon\n      await tester.updatePageIconInTitleBarByPasteALink(\n        name: value.name,\n        layout: value,\n        iconLink: testIconLink,\n      );\n\n      /// check if there is a svg in page\n      final pageName = tester.findPageName(\n        value.name,\n        layout: value,\n      );\n      final imageInPage = find.descendant(\n        of: pageName,\n        matching: find.byType(SvgPicture),\n      );\n      expect(imageInPage, findsOneWidget);\n\n      /// check if there is a svg in title\n      final imageInTitle = find.descendant(\n        of: find.byType(ViewTitleBar),\n        matching: find.byWidgetPredicate((w) {\n          if (w is! SvgPicture) return false;\n          final loader = w.bytesLoader;\n          if (loader is! SvgFileLoader) return false;\n          return loader.file.path.endsWith('.svg');\n        }),\n      );\n      expect(imageInTitle, findsOneWidget);\n    }\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_recent_icon_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../shared/base.dart';\nimport '../../shared/common_operations.dart';\nimport '../../shared/expectation.dart';\n\nvoid main() {\n  testWidgets('Skip the empty group name icon in recent icons', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n\n    /// clear local data\n    RecentIcons.clear();\n    await loadIconGroups();\n    final groups = kIconGroups!;\n    final List<RecentIcon> localIcons = [];\n    for (final e in groups) {\n      localIcons.addAll(e.icons.map((e) => RecentIcon(e, e.name)).toList());\n    }\n    await RecentIcons.putIcon(RecentIcon(localIcons.first.icon, ''));\n    await tester.openPage(gettingStarted);\n    final title = find.descendant(\n      of: find.byType(ViewTitleBar),\n      matching: find.text(gettingStarted),\n    );\n    await tester.tapButton(title);\n\n    /// tap emoji picker button\n    await tester.tapButton(find.byType(EmojiPickerButton));\n    expect(find.byType(FlowyIconEmojiPicker), findsOneWidget);\n\n    /// tap icon tab\n    final pickTab = find.byType(PickerTab);\n    final iconTab = find.descendant(\n      of: pickTab,\n      matching: find.text(PickerTabType.icon.tr),\n    );\n    await tester.tapButton(iconTab);\n\n    expect(find.byType(FlowyIconPicker), findsOneWidget);\n\n    /// no recent icons\n    final recentText = find.descendant(\n      of: find.byType(FlowyIconPicker),\n      matching: find.text('Recent'),\n    );\n    expect(recentText, findsNothing);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test.dart",
    "content": "import 'package:appflowy/plugins/database/board/presentation/board_page.dart';\nimport 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('sidebar:', () {\n    testWidgets('create a new page', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // create a new page\n      await tester.tapNewPageButton();\n\n      // expect to see a new document\n      tester.expectToSeePageName('');\n      // and with one paragraph block\n      expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget);\n    });\n\n    testWidgets('create a new document, grid, board and calendar',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      for (final layout in ViewLayoutPB.values) {\n        if (layout == ViewLayoutPB.Chat) {\n          continue;\n        }\n        // create a new page\n        final name = 'AppFlowy_$layout';\n        await tester.createNewPageWithNameUnderParent(\n          name: name,\n          layout: layout,\n        );\n\n        // expect to see a new page\n        tester.expectToSeePageName(\n          name,\n          layout: layout,\n        );\n\n        switch (layout) {\n          case ViewLayoutPB.Document:\n            // and with one paragraph block\n            expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget);\n            break;\n          case ViewLayoutPB.Grid:\n            expect(find.byType(GridPage), findsOneWidget);\n            break;\n          case ViewLayoutPB.Board:\n            expect(find.byType(DesktopBoardPage), findsOneWidget);\n            break;\n          case ViewLayoutPB.Calendar:\n            expect(find.byType(CalendarPage), findsOneWidget);\n            break;\n          case ViewLayoutPB.Chat:\n            break;\n        }\n\n        await tester.openPage(gettingStarted);\n      }\n    });\n\n    testWidgets('create some nested pages, and move them', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      final names = [1, 2, 3, 4].map((e) => 'document_$e').toList();\n      for (var i = 0; i < names.length; i++) {\n        final parentName = i == 0 ? gettingStarted : names[i - 1];\n        await tester.createNewPageWithNameUnderParent(\n          name: names[i],\n          parentName: parentName,\n        );\n        tester.expectToSeePageName(names[i], parentName: parentName);\n      }\n\n      // move the document_3 to the getting started page\n      await tester.movePageToOtherPage(\n        name: names[3],\n        parentName: gettingStarted,\n        layout: ViewLayoutPB.Document,\n        parentLayout: ViewLayoutPB.Document,\n      );\n      final fromId = tester\n          .widget<SingleInnerViewItem>(tester.findPageName(names[3]))\n          .view\n          .parentViewId;\n      final toId = tester\n          .widget<SingleInnerViewItem>(tester.findPageName(gettingStarted))\n          .view\n          .id;\n      expect(fromId, toId);\n\n      // move the document_2 before document_1\n      await tester.movePageToOtherPage(\n        name: names[2],\n        parentName: gettingStarted,\n        layout: ViewLayoutPB.Document,\n        parentLayout: ViewLayoutPB.Document,\n        position: DraggableHoverPosition.bottom,\n      );\n      final childViews = tester\n          .widget<SingleInnerViewItem>(tester.findPageName(gettingStarted))\n          .view\n          .childViews;\n      expect(\n        childViews[0].id,\n        tester\n            .widget<SingleInnerViewItem>(tester.findPageName(names[2]))\n            .view\n            .id,\n      );\n      expect(\n        childViews[1].id,\n        tester\n            .widget<SingleInnerViewItem>(tester.findPageName(names[0]))\n            .view\n            .id,\n      );\n      expect(\n        childViews[2].id,\n        tester\n            .widget<SingleInnerViewItem>(tester.findPageName(names[3]))\n            .view\n            .id,\n      );\n    });\n\n    testWidgets('unable to move a document into a database', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      const document = 'document';\n      await tester.createNewPageWithNameUnderParent(\n        name: document,\n        openAfterCreated: false,\n      );\n      tester.expectToSeePageName(document);\n\n      const grid = 'grid';\n      await tester.createNewPageWithNameUnderParent(\n        name: grid,\n        layout: ViewLayoutPB.Grid,\n        openAfterCreated: false,\n      );\n      tester.expectToSeePageName(grid, layout: ViewLayoutPB.Grid);\n\n      // move the document to the grid page\n      await tester.movePageToOtherPage(\n        name: document,\n        parentName: grid,\n        layout: ViewLayoutPB.Document,\n        parentLayout: ViewLayoutPB.Grid,\n      );\n\n      // it should not be moved\n      final childViews = tester\n          .widget<SingleInnerViewItem>(tester.findPageName(gettingStarted))\n          .view\n          .childViews;\n      expect(\n        childViews[0].name,\n        document,\n      );\n      expect(\n        childViews[1].name,\n        grid,\n      );\n    });\n\n    testWidgets('unable to create a new database inside the existing one',\n        (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      const grid = 'grid';\n      await tester.createNewPageWithNameUnderParent(\n        name: grid,\n        layout: ViewLayoutPB.Grid,\n      );\n      tester.expectToSeePageName(grid, layout: ViewLayoutPB.Grid);\n\n      await tester.hoverOnPageName(\n        grid,\n        layout: ViewLayoutPB.Grid,\n        onHover: () async {\n          expect(find.byType(ViewAddButton), findsNothing);\n          expect(find.byType(ViewMoreActionPopover), findsOneWidget);\n        },\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'sidebar_favorites_test.dart' as sidebar_favorite_test;\nimport 'sidebar_icon_test.dart' as sidebar_icon_test;\nimport 'sidebar_recent_icon_test.dart' as sidebar_recent_icon_test;\nimport 'sidebar_test.dart' as sidebar_test;\nimport 'sidebar_view_item_test.dart' as sidebar_view_item_test;\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  // Sidebar integration tests\n  sidebar_test.main();\n  // sidebar_expanded_test.main();\n  sidebar_favorite_test.main();\n  sidebar_icon_test.main();\n  sidebar_view_item_test.main();\n  sidebar_recent_icon_test.main();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_view_item_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/base/emoji/emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/emoji.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  setUpAll(() {\n    IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n    RecentIcons.enable = false;\n  });\n\n  tearDownAll(() {\n    RecentIcons.enable = true;\n  });\n\n  group('Sidebar view item tests', () {\n    testWidgets('Access view item context menu by right click', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // Right click on the view item and change icon\n      await tester.hoverOnWidget(\n        find.byType(ViewItem),\n        onHover: () async {\n          await tester.tap(find.byType(ViewItem), buttons: kSecondaryButton);\n          await tester.pumpAndSettle();\n        },\n      );\n\n      // Change icon\n      final changeIconButton =\n          find.text(LocaleKeys.document_plugins_cover_changeIcon.tr());\n\n      await tester.tapButton(changeIconButton);\n      await tester.pumpUntilFound(find.byType(FlowyEmojiPicker));\n\n      const emoji = '😁';\n      await tester.tapEmoji(emoji);\n      await tester.pumpAndSettle();\n\n      tester.expectViewHasIcon(\n        gettingStarted,\n        ViewLayoutPB.Document,\n        EmojiIconData.emoji(emoji),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/uncategorized/board_test.dart",
    "content": "import 'package:appflowy_board/appflowy_board.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\n/// Integration tests for an empty board. The [TestWorkspaceService] will load\n/// a workspace from an empty board `assets/test/workspaces/board.zip` for all\n/// tests.\n///\n/// To create another integration test with a preconfigured workspace.\n/// Use the following steps.\n/// 1. Create a new workspace from the AppFlowy launch screen.\n/// 2. Modify the workspace until it is suitable as the starting point for\n///    the integration test you need to land.\n/// 3. Use a zip utility program to zip the workspace folder that you created.\n/// 4. Add the zip file under `assets/test/workspaces/`\n/// 5. Add a new enumeration to [TestWorkspace] in `integration_test/utils/data.dart`.\n///    For example, if you added a workspace called `empty_calendar.zip`,\n///    then [TestWorkspace] should have the following value:\n/// ```dart\n/// enum TestWorkspace {\n///   board('board'),\n///   empty_calendar('empty_calendar');\n///\n///   /* code */\n/// }\n/// ```\n/// 6. Double check that the .zip file that you added is included as an asset in\n///    the pubspec.yaml file under appflowy_flutter.\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n  const service = TestWorkspaceService(TestWorkspace.board);\n\n  group('board', () {\n    setUpAll(() async => service.setUpAll());\n    setUp(() async => service.setUp());\n\n    testWidgets('open the board with data structure in v0.2.0', (tester) async {\n      await tester.initializeAppFlowy();\n      expect(find.byType(AppFlowyBoard), findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/uncategorized/code_block_language_selector_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/base.dart';\nimport '../../shared/common_operations.dart';\nimport '../../shared/document_test_operations.dart';\nimport '../document/document_codeblock_paste_test.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  testWidgets('Code Block Language Selector Test', (tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n\n    /// create a new document\n    await tester.createNewPageWithNameUnderParent();\n\n    /// tap editor to get focus\n    await tester.tapButton(find.byType(AppFlowyEditor));\n\n    expect(find.byType(CodeBlockLanguageSelector), findsNothing);\n    await insertCodeBlockInDocument(tester);\n\n    ///tap button\n    await tester.hoverOnWidget(find.byType(CodeBlockComponentWidget));\n    await tester\n        .tapButtonWithName(LocaleKeys.document_codeBlock_language_auto.tr());\n    expect(find.byType(CodeBlockLanguageSelector), findsOneWidget);\n\n    for (var i = 0; i < 3; ++i) {\n      await onKey(tester, LogicalKeyboardKey.arrowDown);\n    }\n    for (var i = 0; i < 2; ++i) {\n      await onKey(tester, LogicalKeyboardKey.arrowUp);\n    }\n\n    await onKey(tester, LogicalKeyboardKey.enter);\n\n    final editorState = tester.editor.getCurrentEditorState();\n    String language = editorState\n        .getNodeAtPath([0])!\n        .attributes[CodeBlockKeys.language]\n        .toString();\n    expect(\n      language.toLowerCase(),\n      defaultCodeBlockSupportedLanguages.first.toLowerCase(),\n    );\n\n    await tester.hoverOnWidget(find.byType(CodeBlockComponentWidget));\n    await tester.tapButtonWithName(language);\n\n    await onKey(tester, LogicalKeyboardKey.arrowUp);\n    await onKey(tester, LogicalKeyboardKey.enter);\n\n    language = editorState\n        .getNodeAtPath([0])!\n        .attributes[CodeBlockKeys.language]\n        .toString();\n    expect(\n      language.toLowerCase(),\n      defaultCodeBlockSupportedLanguages.last.toLowerCase(),\n    );\n\n    await tester.hoverOnWidget(find.byType(CodeBlockComponentWidget));\n    await tester.tapButtonWithName(language);\n    tester.testTextInput.enterText(\"rust\");\n    await onKey(tester, LogicalKeyboardKey.delete);\n    await onKey(tester, LogicalKeyboardKey.delete);\n    await onKey(tester, LogicalKeyboardKey.arrowDown);\n    tester.testTextInput.enterText(\"st\");\n    await onKey(tester, LogicalKeyboardKey.arrowDown);\n    await onKey(tester, LogicalKeyboardKey.enter);\n    language = editorState\n        .getNodeAtPath([0])!\n        .attributes[CodeBlockKeys.language]\n        .toString();\n    expect(language.toLowerCase(), 'rust');\n  });\n}\n\nFuture<void> onKey(WidgetTester tester, LogicalKeyboardKey key) async {\n  await tester.simulateKeyEvent(key);\n  await tester.pumpAndSettle();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/emoji/emoji_handler.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  Future<void> prepare(WidgetTester tester) async {\n    await tester.initializeAppFlowy();\n    await tester.tapAnonymousSignInButton();\n    await tester.createNewPageWithNameUnderParent();\n    await tester.editor.tapLineOfEditorAt(0);\n  }\n\n  // May be better to move this to an existing test but unsure what it fits with\n  group('Keyboard shortcuts related to emojis', () {\n    testWidgets('cmd/ctrl+alt+e shortcut opens the emoji picker',\n        (tester) async {\n      await prepare(tester);\n\n      expect(find.byType(EmojiHandler), findsNothing);\n\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyE,\n        isAltPressed: true,\n        isMetaPressed: Platform.isMacOS,\n        isControlPressed: !Platform.isMacOS,\n      );\n      await tester.pumpAndSettle(Duration(seconds: 1));\n      expect(find.byType(EmojiHandler), findsOneWidget);\n\n      /// press backspace to hide the emoji picker\n      await tester.simulateKeyEvent(LogicalKeyboardKey.backspace);\n      expect(find.byType(EmojiHandler), findsNothing);\n    });\n\n    testWidgets('insert emoji by slash menu', (tester) async {\n      await prepare(tester);\n      await tester.editor.showSlashMenu();\n\n      /// show emoji picler\n      await tester.editor.tapSlashMenuItemWithName(\n        LocaleKeys.document_slashMenu_name_emoji.tr(),\n        offset: 100,\n      );\n      await tester.pumpAndSettle(Duration(seconds: 1));\n      expect(find.byType(EmojiHandler), findsOneWidget);\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      final firstNode =\n          tester.editor.getCurrentEditorState().getNodeAtPath([0])!;\n\n      /// except the emoji is in document\n      expect(firstNode.delta!.toPlainText().contains('😀'), true);\n    });\n  });\n\n  group('insert emoji by colon', () {\n    Future<void> createNewDocumentAndShowEmojiList(\n      WidgetTester tester, {\n      String? search,\n    }) async {\n      await prepare(tester);\n      await tester.ime.insertText(':${search ?? 'a'}');\n      await tester.pumpAndSettle(Duration(seconds: 1));\n    }\n\n    testWidgets('insert with click', (tester) async {\n      await createNewDocumentAndShowEmojiList(tester);\n\n      /// emoji list is showing\n      final emojiHandler = find.byType(EmojiHandler);\n      expect(emojiHandler, findsOneWidget);\n      final emojiButtons =\n          find.descendant(of: emojiHandler, matching: find.byType(FlowyButton));\n      final firstTextFinder = find.descendant(\n        of: emojiButtons.first,\n        matching: find.byType(FlowyText),\n      );\n      final emojiText =\n          (firstTextFinder.evaluate().first.widget as FlowyText).text;\n\n      /// click first emoji item\n      await tester.tapButton(emojiButtons.first);\n      final firstNode =\n          tester.editor.getCurrentEditorState().getNodeAtPath([0])!;\n\n      /// except the emoji is in document\n      expect(emojiText.contains(firstNode.delta!.toPlainText()), true);\n    });\n\n    testWidgets('insert with arrow and enter', (tester) async {\n      await createNewDocumentAndShowEmojiList(tester);\n\n      /// emoji list is showing\n      final emojiHandler = find.byType(EmojiHandler);\n      expect(emojiHandler, findsOneWidget);\n      final emojiButtons =\n          find.descendant(of: emojiHandler, matching: find.byType(FlowyButton));\n\n      /// tap arrow down and arrow up\n      await tester.simulateKeyEvent(LogicalKeyboardKey.arrowUp);\n      await tester.simulateKeyEvent(LogicalKeyboardKey.arrowDown);\n\n      final firstTextFinder = find.descendant(\n        of: emojiButtons.first,\n        matching: find.byType(FlowyText),\n      );\n      final emojiText =\n          (firstTextFinder.evaluate().first.widget as FlowyText).text;\n\n      /// tap enter\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      final firstNode =\n          tester.editor.getCurrentEditorState().getNodeAtPath([0])!;\n\n      /// except the emoji is in document\n      expect(emojiText.contains(firstNode.delta!.toPlainText()), true);\n    });\n\n    testWidgets('insert with searching', (tester) async {\n      await createNewDocumentAndShowEmojiList(tester, search: 's');\n\n      /// search for `smiling eyes`, IME is not working, use keyboard input\n      final searchText = [\n        LogicalKeyboardKey.keyM,\n        LogicalKeyboardKey.keyI,\n        LogicalKeyboardKey.keyL,\n        LogicalKeyboardKey.keyI,\n        LogicalKeyboardKey.keyN,\n        LogicalKeyboardKey.keyG,\n        LogicalKeyboardKey.space,\n        LogicalKeyboardKey.keyE,\n        LogicalKeyboardKey.keyY,\n        LogicalKeyboardKey.keyE,\n        LogicalKeyboardKey.keyS,\n      ];\n\n      for (final key in searchText) {\n        await tester.simulateKeyEvent(key);\n      }\n\n      /// tap enter\n      await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n      final firstNode =\n          tester.editor.getCurrentEditorState().getNodeAtPath([0])!;\n\n      /// except the emoji is in document\n      expect(firstNode.delta!.toPlainText().contains('😄'), true);\n    });\n\n    testWidgets('start searching with sapce', (tester) async {\n      await createNewDocumentAndShowEmojiList(tester, search: ' ');\n\n      /// emoji list is showing\n      final emojiHandler = find.byType(EmojiHandler);\n      expect(emojiHandler, findsNothing);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/uncategorized/empty_document_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/keyboard.dart';\nimport '../../shared/util.dart';\n\n/// 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.\n///\n/// To create another integration test with a preconfigured workspace. Use the following steps:\n/// 1. Create a new workspace from the AppFlowy launch screen.\n/// 2. Modify the workspace until it is suitable as the starting point for the integration test you need to land.\n/// 3. Use a zip utility program to zip the workspace folder that you created.\n/// 4. Add the zip file under `assets/test/workspaces/`\n/// 5. Add a new enumeration to [TestWorkspace] in `integration_test/utils/data.dart`. For example, if you added a workspace called `empty_calendar.zip`, then [TestWorkspace] should have the following value:\n/// ```dart\n/// enum TestWorkspace {\n///   board('board'),\n///   empty_calendar('empty_calendar');\n///\n///   /* code */\n/// }\n/// ```\n/// 6. Double check that the .zip file that you added is included as an asset in the pubspec.yaml file under appflowy_flutter.\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n  const service = TestWorkspaceService(TestWorkspace.emptyDocument);\n\n  group('Tests on a workspace with only an empty document', () {\n    setUpAll(() async => service.setUpAll());\n    setUp(() async => service.setUp());\n\n    testWidgets('/board shortcut creates a new board and view of the board',\n        (tester) async {\n      await tester.initializeAppFlowy();\n\n      // Needs tab to obtain focus for the app flowy editor.\n      // by default the tap appears at the center of the widget.\n      final Finder editor = find.byType(AppFlowyEditor);\n      await tester.tap(editor);\n      await tester.pumpAndSettle();\n\n      // tester.sendText() cannot be used since the editor\n      // does not contain any EditableText widgets.\n      // to interact with the app during an integration test,\n      // simulate physical keyboard events.\n      await FlowyTestKeyboard.simulateKeyDownEvent(\n        [\n          LogicalKeyboardKey.slash,\n          LogicalKeyboardKey.keyB,\n          LogicalKeyboardKey.keyO,\n          LogicalKeyboardKey.keyA,\n          LogicalKeyboardKey.keyR,\n          LogicalKeyboardKey.keyD,\n          LogicalKeyboardKey.arrowDown,\n        ],\n        tester: tester,\n      );\n\n      // Checks whether the options in the selection menu\n      // for /board exist.\n      expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2));\n\n      // Finalizes the slash command that creates the board.\n      await FlowyTestKeyboard.simulateKeyDownEvent(\n        [\n          LogicalKeyboardKey.enter,\n        ],\n        tester: tester,\n      );\n\n      // Checks whether new board is referenced and properly on the page.\n      expect(find.byType(BuiltInPageWidget), findsOneWidget);\n\n      // Checks whether the new database was created\n      const newBoardLabel = \"Untitled\";\n      expect(find.text(newBoardLabel), findsOneWidget);\n\n      // Checks whether a view of the database was created\n      const viewOfBoardLabel = \"View of Untitled\";\n      expect(find.text(viewOfBoardLabel), findsNWidgets(2));\n    });\n\n    testWidgets('/grid shortcut creates a new grid and view of the grid',\n        (tester) async {\n      await tester.initializeAppFlowy();\n\n      // Needs tab to obtain focus for the app flowy editor.\n      // by default the tap appears at the center of the widget.\n      final Finder editor = find.byType(AppFlowyEditor);\n      await tester.tap(editor);\n      await tester.pumpAndSettle();\n\n      // tester.sendText() cannot be used since the editor\n      // does not contain any EditableText widgets.\n      // to interact with the app during an integration test,\n      // simulate physical keyboard events.\n      await FlowyTestKeyboard.simulateKeyDownEvent(\n        [\n          LogicalKeyboardKey.slash,\n          LogicalKeyboardKey.keyG,\n          LogicalKeyboardKey.keyR,\n          LogicalKeyboardKey.keyI,\n          LogicalKeyboardKey.keyD,\n          LogicalKeyboardKey.arrowDown,\n        ],\n        tester: tester,\n      );\n\n      // Checks whether the options in the selection menu\n      // for /grid exist.\n      expect(find.byType(SelectionMenuItemWidget), findsAtLeastNWidgets(2));\n\n      // Finalizes the slash command that creates the board.\n      await simulateKeyDownEvent(LogicalKeyboardKey.enter);\n      await tester.pumpAndSettle();\n\n      // Checks whether new board is referenced and properly on the page.\n      expect(find.byType(BuiltInPageWidget), findsOneWidget);\n\n      // Checks whether the new database was created\n      const newTableLabel = \"Untitled\";\n      expect(find.text(newTableLabel), findsOneWidget);\n\n      // Checks whether a view of the database was created\n      const viewOfTableLabel = \"View of Untitled\";\n      expect(find.text(viewOfTableLabel), findsNWidgets(2));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/settings/prelude.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart';\nimport 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/keyboard.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('hotkeys test', () {\n    testWidgets('toggle theme mode', (tester) async {\n      await tester.initializeAppFlowy();\n\n      await tester.tapAnonymousSignInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      await tester.openSettings();\n      await tester.openSettingsPage(SettingsPage.workspace);\n      await tester.pumpAndSettle();\n\n      final appFinder = find.byType(MaterialApp).first;\n      ThemeMode? themeMode = tester.widget<MaterialApp>(appFinder).themeMode;\n\n      expect(themeMode, ThemeMode.system);\n\n      await tester.tapButton(\n        find.bySemanticsLabel(\n          LocaleKeys.settings_workspacePage_appearance_options_light.tr(),\n        ),\n      );\n      await tester.pumpAndSettle(const Duration(milliseconds: 250));\n\n      themeMode = tester.widget<MaterialApp>(appFinder).themeMode;\n      expect(themeMode, ThemeMode.light);\n\n      await tester.tapButton(\n        find.bySemanticsLabel(\n          LocaleKeys.settings_workspacePage_appearance_options_dark.tr(),\n        ),\n      );\n      await tester.pumpAndSettle(const Duration(milliseconds: 250));\n\n      themeMode = tester.widget<MaterialApp>(appFinder).themeMode;\n      expect(themeMode, ThemeMode.dark);\n\n      await tester.tap(find.byType(SettingsDialog));\n      await tester.pumpAndSettle();\n\n      await FlowyTestKeyboard.simulateKeyDownEvent(\n        [\n          Platform.isMacOS\n              ? LogicalKeyboardKey.meta\n              : LogicalKeyboardKey.control,\n          LogicalKeyboardKey.shift,\n          LogicalKeyboardKey.keyL,\n        ],\n        tester: tester,\n      );\n      await tester.pumpAndSettle(const Duration(milliseconds: 500));\n\n      // disable it temporarily. It works on macOS but not on Linux.\n      // themeMode = tester.widget<MaterialApp>(appFinder).themeMode;\n      // expect(themeMode, ThemeMode.light);\n    });\n\n    testWidgets('show or hide home menu', (tester) async {\n      await tester.initializeAppFlowy();\n\n      await tester.tapAnonymousSignInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      await tester.pumpAndSettle();\n\n      expect(find.byType(HomeSideBar), findsOneWidget);\n\n      await FlowyTestKeyboard.simulateKeyDownEvent(\n        [\n          Platform.isMacOS\n              ? LogicalKeyboardKey.meta\n              : LogicalKeyboardKey.control,\n          LogicalKeyboardKey.backslash,\n        ],\n        tester: tester,\n      );\n\n      await tester.pumpAndSettle();\n\n      expect(find.byType(HomeSideBar), findsNothing);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:path/path.dart' as p;\n\nimport '../../shared/mock/mock_file_picker.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('import files', () {\n    testWidgets('import multiple markdown files', (tester) async {\n      final context = await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // expect to see a getting started page\n      tester.expectToSeePageName(gettingStarted);\n\n      await tester.tapAddViewButton();\n      await tester.tapImportButton();\n\n      final testFileNames = ['test1.md', 'test2.md'];\n      final paths = <String>[];\n      for (final fileName in testFileNames) {\n        final str = await rootBundle.loadString(\n          'assets/test/workspaces/markdowns/$fileName',\n        );\n        final path = p.join(context.applicationDataDirectory, fileName);\n        paths.add(path);\n        File(path).writeAsStringSync(str);\n      }\n      // mock get files\n      mockPickFilePaths(\n        paths: testFileNames\n            .map((e) => p.join(context.applicationDataDirectory, e))\n            .toList(),\n      );\n\n      await tester.tapTextAndMarkdownButton();\n\n      tester.expectToSeePageName('test1');\n      tester.expectToSeePageName('test2');\n    });\n\n    testWidgets('import markdown file with table', (tester) async {\n      final context = await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // expect to see a getting started page\n      tester.expectToSeePageName(gettingStarted);\n\n      await tester.tapAddViewButton();\n      await tester.tapImportButton();\n\n      const testFileName = 'markdown_with_table.md';\n      final paths = <String>[];\n      final str = await rootBundle.loadString(\n        'assets/test/workspaces/markdowns/$testFileName',\n      );\n      final path = p.join(context.applicationDataDirectory, testFileName);\n      paths.add(path);\n      File(path).writeAsStringSync(str);\n      // mock get files\n      mockPickFilePaths(\n        paths: paths,\n      );\n\n      await tester.tapTextAndMarkdownButton();\n\n      tester.expectToSeePageName('markdown_with_table');\n\n      // expect to see all content of markdown file along with table\n      await tester.openPage('markdown_with_table');\n\n      final importedPageEditorState = tester.editor.getCurrentEditorState();\n      expect(\n        importedPageEditorState.getNodeAtPath([0])!.type,\n        HeadingBlockKeys.type,\n      );\n      expect(\n        importedPageEditorState.getNodeAtPath([1])!.type,\n        HeadingBlockKeys.type,\n      );\n      expect(\n        importedPageEditorState.getNodeAtPath([2])!.type,\n        SimpleTableBlockKeys.type,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/uncategorized/language_test.dart",
    "content": "import 'package:appflowy/user/presentation/screens/skip_log_in_screen.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('document', () {\n    testWidgets(\n        'change the language successfully when launching the app for the first time',\n        (tester) async {\n      await tester.initializeAppFlowy();\n\n      await tester.tapLanguageSelectorOnWelcomePage();\n      expect(find.byType(LanguageItemsListView), findsOneWidget);\n\n      await tester.tapLanguageItem(languageCode: 'zh', countryCode: 'CN');\n      tester.expectToSeeText('开始');\n\n      await tester.tapLanguageItem(languageCode: 'en', scrollDelta: -100);\n      tester.expectToSeeText('Quick Start');\n\n      await tester.tapLanguageItem(languageCode: 'it', countryCode: 'IT');\n      tester.expectToSeeText('Andiamo');\n    });\n\n    /// Make sure this test is executed after the test above.\n    testWidgets('check the language after relaunching the app', (tester) async {\n      await tester.initializeAppFlowy();\n      tester.expectToSeeText('Andiamo');\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:appflowy/plugins/shared/share/share_button.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';\nimport 'package:archive/archive.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:path/path.dart' as p;\n\nimport '../../shared/mock/mock_file_picker.dart';\nimport '../../shared/util.dart';\nimport '../document/document_with_database_test.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('share markdown in document page', () {\n    testWidgets('click the share button in document page', (tester) async {\n      final context = await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // mock the file picker\n      final path = await mockSaveFilePath(\n        p.join(context.applicationDataDirectory, 'test.zip'),\n      );\n      // click the share button and select markdown\n      await tester.tapShareButton();\n      await tester.tapMarkdownButton();\n\n      // expect to see the success dialog\n      tester.expectToExportSuccess();\n\n      final file = File(path);\n      expect(file.existsSync(), true);\n      final archive = ZipDecoder().decodeBytes(file.readAsBytesSync());\n      for (final entry in archive) {\n        if (entry.isFile && entry.name.endsWith('.md')) {\n          final markdown = utf8.decode(entry.content);\n          expect(markdown, expectedMarkdown);\n        }\n      }\n    });\n\n    testWidgets(\n      'share the markdown after renaming the document name',\n      (tester) async {\n        final context = await tester.initializeAppFlowy();\n        await tester.tapAnonymousSignInButton();\n\n        // expect to see a getting started page\n        tester.expectToSeePageName(gettingStarted);\n\n        // rename the document\n        await tester.hoverOnPageName(\n          gettingStarted,\n          onHover: () async {\n            await tester.renamePage('example');\n          },\n        );\n\n        final shareButton = find.byType(ShareButton);\n        final shareButtonState = tester.widget(shareButton) as ShareButton;\n\n        final path = await mockSaveFilePath(\n          p.join(\n            context.applicationDataDirectory,\n            '${shareButtonState.view.name}.zip',\n          ),\n        );\n\n        // click the share button and select markdown\n        await tester.tapShareButton();\n        await tester.tapMarkdownButton();\n\n        // expect to see the success dialog\n        tester.expectToExportSuccess();\n\n        final file = File(path);\n        expect(file.existsSync(), true);\n        final archive = ZipDecoder().decodeBytes(file.readAsBytesSync());\n        for (final entry in archive) {\n          if (entry.isFile && entry.name.endsWith('.md')) {\n            final markdown = utf8.decode(entry.content);\n            expect(markdown, expectedMarkdown);\n          }\n        }\n      },\n    );\n\n    testWidgets('share the markdown with database', (tester) async {\n      final context = await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n      await insertLinkedDatabase(tester, ViewLayoutPB.Grid);\n\n      // mock the file picker\n      final path = await mockSaveFilePath(\n        p.join(context.applicationDataDirectory, 'test.zip'),\n      );\n      // click the share button and select markdown\n      await tester.tapShareButton();\n      await tester.tapMarkdownButton();\n\n      // expect to see the success dialog\n      tester.expectToExportSuccess();\n\n      final file = File(path);\n      expect(file.existsSync(), true);\n      final archive = ZipDecoder().decodeBytes(file.readAsBytesSync());\n      bool hasCsvFile = false;\n      for (final entry in archive) {\n        if (entry.isFile && entry.name.endsWith('.csv')) {\n          hasCsvFile = true;\n        }\n      }\n      expect(hasCsvFile, true);\n    });\n  });\n}\n\nconst expectedMarkdown = '''\n# Welcome to AppFlowy!\n## Here are the basics\n- [ ] Click anywhere and just start typing.\n- [ ] Highlight any text, and use the editing menu to _style_ **your** <u>writing</u> `however` you ~~like.~~\n- [ ] As soon as you type `/` a menu will pop up. Select different types of content blocks you can add.\n- [ ] Type `/` followed by `/bullet` or `/num` to create a list.\n- [x] Click `+ New Page `button at the bottom of your sidebar to add a new page.\n- [ ] Click `+` next to any page title in the sidebar to quickly add a new subpage, `Document`, `Grid`, or `Kanban Board`.\n\n---\n\n## Keyboard shortcuts, markdown, and code block\n1. Keyboard shortcuts [guide](https://appflowy.gitbook.io/docs/essential-documentation/shortcuts)\n1. Markdown [reference](https://appflowy.gitbook.io/docs/essential-documentation/markdown)\n1. Type `/code` to insert a code block\n```rust\n// This is the main function.\nfn main() {\n    // Print text to the console.\n    println!(\"Hello World!\");\n}\n```\n\n## Have a question❓\n> Click `?` at the bottom right for help and support.\n\n> 🥰\n> \n> Like AppFlowy? Follow us:\n> [GitHub](https://github.com/AppFlowy-IO/AppFlowy)\n> [Twitter](https://twitter.com/appflowy): @appflowy\n> [Newsletter](https://blog-appflowy.ghost.io/)\n> \n\n\n\n\n''';\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('customize the folder path', () {\n    if (Platform.isWindows) {\n      return;\n    }\n\n    // testWidgets('switch to B from A, then switch to A again', (tester) async {\n    //   const userA = 'UserA';\n    //   const userB = 'UserB';\n\n    //   final initialPath = p.join(userA, appFlowyDataFolder);\n    //   final context = await tester.initializeAppFlowy(\n    //     pathExtension: initialPath,\n    //   );\n    //   // remove the last extension\n    //   final rootPath = context.applicationDataDirectory.replaceFirst(\n    //     initialPath,\n    //     '',\n    //   );\n\n    //   await tester.tapGoButton();\n    //   await tester.expectToSeeHomePageWithGetStartedPage();\n\n    //   // switch to user B\n    //   {\n    //     // set user name for userA\n    //     await tester.openSettings();\n    //     await tester.openSettingsPage(SettingsPage.user);\n    //     await tester.enterUserName(userA);\n\n    //     await tester.openSettingsPage(SettingsPage.files);\n    //     await tester.pumpAndSettle();\n\n    //     // mock the file_picker result\n    //     await mockGetDirectoryPath(\n    //       p.join(rootPath, userB),\n    //     );\n    //     await tester.tapCustomLocationButton();\n    //     await tester.pumpAndSettle();\n    //     await tester.expectToSeeHomePageWithGetStartedPage();\n\n    //     // set user name for userB\n    //     await tester.openSettings();\n    //     await tester.openSettingsPage(SettingsPage.user);\n    //     await tester.enterUserName(userB);\n    //   }\n\n    //   // switch to the userA\n    //   {\n    //     await tester.openSettingsPage(SettingsPage.files);\n    //     await tester.pumpAndSettle();\n\n    //     // mock the file_picker result\n    //     await mockGetDirectoryPath(\n    //       p.join(rootPath, userA),\n    //     );\n    //     await tester.tapCustomLocationButton();\n\n    //     await tester.pumpAndSettle();\n    //     await tester.expectToSeeHomePageWithGetStartedPage();\n    //     tester.expectToSeeUserName(userA);\n    //   }\n\n    //   // switch to the userB again\n    //   {\n    //     await tester.openSettings();\n    //     await tester.openSettingsPage(SettingsPage.files);\n    //     await tester.pumpAndSettle();\n\n    //     // mock the file_picker result\n    //     await mockGetDirectoryPath(\n    //       p.join(rootPath, userB),\n    //     );\n    //     await tester.tapCustomLocationButton();\n\n    //     await tester.pumpAndSettle();\n    //     await tester.expectToSeeHomePageWithGetStartedPage();\n    //     tester.expectToSeeUserName(userB);\n    //   }\n    // });\n\n    // Disable this test because it failed after executing.\n    // testWidgets('reset to default location', (tester) async {\n    //   await tester.initializeAppFlowy();\n\n    //   await tester.tapAnonymousSignInButton();\n\n    //   // home and readme document\n    //   await tester.expectToSeeHomePageWithGetStartedPage();\n\n    //   // open settings and restore the location\n    //   await tester.openSettings();\n    //   await tester.openSettingsPage(SettingsPage.manageData);\n    //   await tester.restoreLocation();\n\n    //   expect(\n    //     await appFlowyApplicationDataDirectory().then((value) => value.path),\n    //     await getIt<ApplicationDataStorage>().getPath(),\n    //   );\n    // });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/uncategorized/tabs_test.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';\nimport 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';\nimport 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';\nimport 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_svg/flowy_svg.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/keyboard.dart';\nimport '../../shared/util.dart';\n\nconst _documentName = 'First Doc';\nconst _documentTwoName = 'Second Doc';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Tabs', () {\n    testWidgets('open/navigate/close tabs', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      // No tabs rendered yet\n      expect(find.byType(FlowyTab), findsNothing);\n\n      await tester.createNewPageWithNameUnderParent(name: _documentName);\n\n      await tester.createNewPageWithNameUnderParent(name: _documentTwoName);\n\n      /// Open second menu item in a new tab\n      await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document);\n\n      /// Open third menu item in a new tab\n      await tester.openAppInNewTab(_documentName, ViewLayoutPB.Document);\n\n      expect(\n        find.descendant(\n          of: find.byType(TabsManager),\n          matching: find.byType(FlowyTab),\n        ),\n        findsNWidgets(3),\n      );\n\n      /// Navigate to the second tab\n      await tester.tap(\n        find.descendant(\n          of: find.byType(FlowyTab),\n          matching: find.text(gettingStarted),\n        ),\n      );\n\n      /// Close tab by shortcut\n      await FlowyTestKeyboard.simulateKeyDownEvent(\n        [\n          Platform.isMacOS\n              ? LogicalKeyboardKey.meta\n              : LogicalKeyboardKey.control,\n          LogicalKeyboardKey.keyW,\n        ],\n        tester: tester,\n      );\n\n      expect(\n        find.descendant(\n          of: find.byType(TabsManager),\n          matching: find.byType(FlowyTab),\n        ),\n        findsNWidgets(2),\n      );\n    });\n\n    testWidgets('right click show tab menu, close others', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      expect(\n        find.descendant(\n          of: find.byType(TabsManager),\n          matching: find.byType(TabBar),\n        ),\n        findsNothing,\n      );\n\n      await tester.createNewPageWithNameUnderParent(name: _documentName);\n      await tester.createNewPageWithNameUnderParent(name: _documentTwoName);\n\n      /// Open second menu item in a new tab\n      await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document);\n\n      /// Open third menu item in a new tab\n      await tester.openAppInNewTab(_documentName, ViewLayoutPB.Document);\n\n      expect(\n        find.descendant(\n          of: find.byType(TabsManager),\n          matching: find.byType(FlowyTab),\n        ),\n        findsNWidgets(3),\n      );\n\n      /// Right click on second tab\n      await tester.tap(\n        buttons: kSecondaryButton,\n        find.descendant(\n          of: find.byType(FlowyTab),\n          matching: find.text(gettingStarted),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(TabMenu), findsOneWidget);\n\n      final firstTabFinder = find.descendant(\n        of: find.byType(FlowyTab),\n        matching: find.text(_documentTwoName),\n      );\n      final secondTabFinder = find.descendant(\n        of: find.byType(FlowyTab),\n        matching: find.text(gettingStarted),\n      );\n      final thirdTabFinder = find.descendant(\n        of: find.byType(FlowyTab),\n        matching: find.text(_documentName),\n      );\n\n      expect(firstTabFinder, findsOneWidget);\n      expect(secondTabFinder, findsOneWidget);\n      expect(thirdTabFinder, findsOneWidget);\n\n      // Close other tabs than the second item\n      await tester.tap(find.text(LocaleKeys.tabMenu_closeOthers.tr()));\n      await tester.pumpAndSettle();\n\n      // We expect to not find any tabs\n      expect(firstTabFinder, findsNothing);\n      expect(secondTabFinder, findsNothing);\n      expect(thirdTabFinder, findsNothing);\n\n      // Expect second tab to be current page (current page has breadcrumb, cover title,\n      //  and in this case view name in sidebar)\n      expect(find.text(gettingStarted), findsNWidgets(3));\n    });\n\n    testWidgets('cannot close pinned tabs', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      expect(\n        find.descendant(\n          of: find.byType(TabsManager),\n          matching: find.byType(TabBar),\n        ),\n        findsNothing,\n      );\n\n      await tester.createNewPageWithNameUnderParent(name: _documentName);\n      await tester.createNewPageWithNameUnderParent(name: _documentTwoName);\n\n      // Open second menu item in a new tab\n      await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document);\n\n      // Open third menu item in a new tab\n      await tester.openAppInNewTab(_documentName, ViewLayoutPB.Document);\n\n      expect(\n        find.descendant(\n          of: find.byType(TabsManager),\n          matching: find.byType(FlowyTab),\n        ),\n        findsNWidgets(3),\n      );\n\n      const firstTab = _documentTwoName;\n      const secondTab = gettingStarted;\n      const thirdTab = _documentName;\n\n      expect(tester.isTabAtIndex(firstTab, 0), isTrue);\n      expect(tester.isTabAtIndex(secondTab, 1), isTrue);\n      expect(tester.isTabAtIndex(thirdTab, 2), isTrue);\n\n      expect(tester.isTabPinned(gettingStarted), isFalse);\n\n      // Right click on second tab\n      await tester.openTabMenu(gettingStarted);\n      expect(find.byType(TabMenu), findsOneWidget);\n\n      // Pin second tab\n      await tester.tap(find.text(LocaleKeys.tabMenu_pinTab.tr()));\n      await tester.pumpAndSettle();\n\n      expect(tester.isTabPinned(gettingStarted), isTrue);\n\n      /// Right click on first unpinned tab (second tab)\n      await tester.openTabMenu(_documentTwoName);\n\n      // Close others\n      await tester.tap(find.text(LocaleKeys.tabMenu_closeOthers.tr()));\n      await tester.pumpAndSettle();\n\n      // We expect to find 2 tabs, the first pinned tab and the second tab\n      expect(find.byType(FlowyTab), findsNWidgets(2));\n      expect(tester.isTabAtIndex(gettingStarted, 0), isTrue);\n      expect(tester.isTabAtIndex(_documentTwoName, 1), isTrue);\n    });\n\n    testWidgets('pin/unpin tabs proper order', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      expect(\n        find.descendant(\n          of: find.byType(TabsManager),\n          matching: find.byType(TabBar),\n        ),\n        findsNothing,\n      );\n\n      await tester.createNewPageWithNameUnderParent(name: _documentName);\n      await tester.createNewPageWithNameUnderParent(name: _documentTwoName);\n\n      // Open second menu item in a new tab\n      await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document);\n\n      // Open third menu item in a new tab\n      await tester.openAppInNewTab(_documentName, ViewLayoutPB.Document);\n\n      expect(\n        find.descendant(\n          of: find.byType(TabsManager),\n          matching: find.byType(FlowyTab),\n        ),\n        findsNWidgets(3),\n      );\n\n      const firstTabName = _documentTwoName;\n      const secondTabName = gettingStarted;\n      const thirdTabName = _documentName;\n\n      // Expect correct order\n      expect(tester.isTabAtIndex(firstTabName, 0), isTrue);\n      expect(tester.isTabAtIndex(secondTabName, 1), isTrue);\n      expect(tester.isTabAtIndex(thirdTabName, 2), isTrue);\n\n      // Pin second tab\n      await tester.openTabMenu(secondTabName);\n      await tester.tap(find.text(LocaleKeys.tabMenu_pinTab.tr()));\n      await tester.pumpAndSettle();\n\n      expect(tester.isTabPinned(secondTabName), isTrue);\n\n      // Expect correct order\n      expect(tester.isTabAtIndex(secondTabName, 0), isTrue);\n      expect(tester.isTabAtIndex(firstTabName, 1), isTrue);\n      expect(tester.isTabAtIndex(thirdTabName, 2), isTrue);\n\n      // Pin new second tab (first tab)\n      await tester.openTabMenu(firstTabName);\n      await tester.tap(find.text(LocaleKeys.tabMenu_pinTab.tr()));\n      await tester.pumpAndSettle();\n\n      expect(tester.isTabPinned(firstTabName), isTrue);\n      expect(tester.isTabPinned(secondTabName), isTrue);\n      expect(tester.isTabPinned(thirdTabName), isFalse);\n\n      expect(tester.isTabAtIndex(secondTabName, 0), isTrue);\n      expect(tester.isTabAtIndex(firstTabName, 1), isTrue);\n      expect(tester.isTabAtIndex(thirdTabName, 2), isTrue);\n\n      // Unpin second tab\n      await tester.openTabMenu(secondTabName);\n      await tester.tap(find.text(LocaleKeys.tabMenu_unpinTab.tr()));\n      await tester.pumpAndSettle();\n\n      expect(tester.isTabPinned(firstTabName), isTrue);\n      expect(tester.isTabPinned(secondTabName), isFalse);\n      expect(tester.isTabPinned(thirdTabName), isFalse);\n\n      expect(tester.isTabAtIndex(firstTabName, 0), isTrue);\n      expect(tester.isTabAtIndex(secondTabName, 1), isTrue);\n      expect(tester.isTabAtIndex(thirdTabName, 2), isTrue);\n    });\n\n    testWidgets('displaying icons in tab', (tester) async {\n      RecentIcons.enable = false;\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      final icon = await tester.loadIcon();\n      // update emoji\n      await tester.updatePageIconInSidebarByName(\n        name: gettingStarted,\n        parentName: gettingStarted,\n        layout: ViewLayoutPB.Document,\n        icon: icon,\n      );\n\n      /// create new page\n      await tester.createNewPageWithNameUnderParent(name: _documentName);\n\n      /// open new tab for [gettingStarted]\n      await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document);\n\n      final tabs = find.descendant(\n        of: find.byType(TabsManager),\n        matching: find.byType(FlowyTab),\n      );\n      expect(tabs, findsNWidgets(2));\n\n      final svgInTab =\n          find.descendant(of: tabs.last, matching: find.byType(FlowySvg));\n      final svgWidget = svgInTab.evaluate().first.widget as FlowySvg;\n      final iconsData = IconsData.fromJson(jsonDecode(icon.emoji));\n      expect(svgWidget.svgString, iconsData.svgString);\n    });\n  });\n}\n\nextension _TabsTester on WidgetTester {\n  bool isTabPinned(String tabName) {\n    final tabFinder = find.ancestor(\n      of: find.byWidgetPredicate(\n        (w) => w is ViewTabBarItem && w.view.name == tabName,\n      ),\n      matching: find.byType(FlowyTab),\n    );\n\n    final FlowyTab tabWidget = widget(tabFinder);\n    return tabWidget.pageManager.isPinned;\n  }\n\n  bool isTabAtIndex(String tabName, int index) {\n    final tabFinder = find.ancestor(\n      of: find.byWidgetPredicate(\n        (w) => w is ViewTabBarItem && w.view.name == tabName,\n      ),\n      matching: find.byType(FlowyTab),\n    );\n\n    final pluginId = (widget(tabFinder) as FlowyTab).pageManager.plugin.id;\n\n    final pluginIds = find\n        .byType(FlowyTab)\n        .evaluate()\n        .map((e) => (e.widget as FlowyTab).pageManager.plugin.id);\n\n    return pluginIds.elementAt(index) == pluginId;\n  }\n\n  Future<void> openTabMenu(String tabName) async {\n    await tap(\n      buttons: kSecondaryButton,\n      find.ancestor(\n        of: find.byWidgetPredicate(\n          (w) => w is ViewTabBarItem && w.view.name == tabName,\n        ),\n        matching: find.byType(FlowyTab),\n      ),\n    );\n    await pumpAndSettle();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/uncategorized/uncategorized_test_runner_1.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'emoji_shortcut_test.dart' as emoji_shortcut_test;\nimport 'hotkeys_test.dart' as hotkeys_test;\nimport 'import_files_test.dart' as import_files_test;\nimport 'share_markdown_test.dart' as share_markdown_test;\nimport 'zoom_in_out_test.dart' as zoom_in_out_test;\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  // This test must be run first, otherwise the CI will fail.\n  hotkeys_test.main();\n  emoji_shortcut_test.main();\n  hotkeys_test.main();\n  share_markdown_test.main();\n  import_files_test.main();\n  zoom_in_out_test.main();\n  // DON'T add more tests here.\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop/uncategorized/zoom_in_out_test.dart",
    "content": "import 'package:appflowy/startup/tasks/app_window_size_manager.dart';\nimport 'package:appflowy/workspace/presentation/home/hotkeys.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:hotkey_manager/hotkey_manager.dart';\nimport 'package:integration_test/integration_test.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../../shared/base.dart';\nimport '../../shared/common_operations.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Zoom in/out:', () {\n    Future<void> resetAppFlowyScaleFactor(\n      WindowSizeManager windowSizeManager,\n    ) async {\n      appflowyScaleFactor = 1.0;\n      await windowSizeManager.setScaleFactor(1.0);\n    }\n\n    testWidgets('Zoom in', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      double currentScaleFactor = 1.0;\n\n      // this value can't be defined in the setUp method, because the windowSizeManager is not initialized yet.\n      final windowSizeManager = WindowSizeManager();\n      await resetAppFlowyScaleFactor(windowSizeManager);\n\n      // zoom in 2 times\n      for (final keycode in zoomInKeyCodes) {\n        if (UniversalPlatform.isLinux &&\n            keycode.logicalKey == LogicalKeyboardKey.add) {\n          // Key LogicalKeyboardKey#79c9b(keyId: \"0x0000002b\", keyLabel: \"+\", debugName: \"Add\") not found in\n          // linux keyCode map\n          continue;\n        }\n\n        // test each keycode 2 times\n        for (var i = 0; i < 2; i++) {\n          await tester.simulateKeyEvent(\n            keycode.logicalKey,\n            isControlPressed: !UniversalPlatform.isMacOS,\n            isMetaPressed: UniversalPlatform.isMacOS,\n            // Register the physical key for the \"Add\" key, otherwise the test will fail and throw an error:\n            //  Physical key for LogicalKeyboardKey#79c9b(keyId: \"0x0000002b\", keyLabel: \"+\", debugName: \"Add\")\n            //  not found in known physical keys\n            physicalKey: keycode.logicalKey == LogicalKeyboardKey.add\n                ? PhysicalKeyboardKey.equal\n                : null,\n          );\n\n          await tester.pumpAndSettle();\n\n          currentScaleFactor += 0.1;\n          currentScaleFactor = double.parse(\n            currentScaleFactor.toStringAsFixed(2),\n          );\n\n          final scaleFactor = await windowSizeManager.getScaleFactor();\n          expect(currentScaleFactor, appflowyScaleFactor);\n          expect(currentScaleFactor, scaleFactor);\n        }\n      }\n    });\n\n    testWidgets('Reset zoom', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      final windowSizeManager = WindowSizeManager();\n\n      for (final keycode in resetZoomKeyCodes) {\n        await tester.simulateKeyEvent(\n          keycode.logicalKey,\n          isControlPressed: !UniversalPlatform.isMacOS,\n          isMetaPressed: UniversalPlatform.isMacOS,\n        );\n        await tester.pumpAndSettle();\n\n        final scaleFactor = await windowSizeManager.getScaleFactor();\n        expect(1.0, appflowyScaleFactor);\n        expect(1.0, scaleFactor);\n      }\n    });\n\n    testWidgets('Zoom out', (tester) async {\n      await tester.initializeAppFlowy();\n      await tester.tapAnonymousSignInButton();\n\n      double currentScaleFactor = 1.0;\n\n      final windowSizeManager = WindowSizeManager();\n      await resetAppFlowyScaleFactor(windowSizeManager);\n\n      // zoom out 2 times\n      for (final keycode in zoomOutKeyCodes) {\n        if (UniversalPlatform.isLinux &&\n            keycode.logicalKey == LogicalKeyboardKey.numpadSubtract) {\n          //  Key LogicalKeyboardKey#2c39f(keyId: \"0x20000022d\", keyLabel: \"Numpad Subtract\", debugName: \"Numpad\n          // Subtract\") not found in linux keyCode map\n          continue;\n        }\n        // test each keycode 2 times\n        for (var i = 0; i < 2; i++) {\n          await tester.simulateKeyEvent(\n            keycode.logicalKey,\n            isControlPressed: !UniversalPlatform.isMacOS,\n            isMetaPressed: UniversalPlatform.isMacOS,\n          );\n          await tester.pumpAndSettle();\n\n          currentScaleFactor -= 0.1;\n\n          final scaleFactor = await windowSizeManager.getScaleFactor();\n          expect(currentScaleFactor, appflowyScaleFactor);\n          expect(currentScaleFactor, scaleFactor);\n        }\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop_runner_1.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'desktop/document/document_test_runner_1.dart' as document_test_runner_1;\nimport 'desktop/uncategorized/switch_folder_test.dart' as switch_folder_test;\n\nFuture<void> main() async {\n  await runIntegration1OnDesktop();\n}\n\nFuture<void> runIntegration1OnDesktop() async {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  switch_folder_test.main();\n  document_test_runner_1.main();\n  // DON'T add more tests here.\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop_runner_2.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'desktop/database/database_test_runner_1.dart' as database_test_runner_1;\nimport 'desktop/first_test/first_test.dart' as first_test;\n\nFuture<void> main() async {\n  await runIntegration2OnDesktop();\n}\n\nFuture<void> runIntegration2OnDesktop() async {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  first_test.main();\n\n  database_test_runner_1.main();\n  // DON'T add more tests here. This is the second test runner for desktop.\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop_runner_3.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'desktop/board/board_test_runner.dart' as board_test_runner;\nimport 'desktop/first_test/first_test.dart' as first_test;\nimport 'desktop/grid/grid_test_runner_1.dart' as grid_test_runner_1;\n\nFuture<void> main() async {\n  await runIntegration3OnDesktop();\n}\n\nFuture<void> runIntegration3OnDesktop() async {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  first_test.main();\n\n  board_test_runner.main();\n  grid_test_runner_1.main();\n  // DON'T add more tests here.\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop_runner_4.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'desktop/document/document_test_runner_2.dart' as document_test_runner_2;\nimport 'desktop/first_test/first_test.dart' as first_test;\n\nFuture<void> main() async {\n  await runIntegration4OnDesktop();\n}\n\nFuture<void> runIntegration4OnDesktop() async {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  first_test.main();\n\n  document_test_runner_2.main();\n  // DON'T add more tests here.\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop_runner_5.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'desktop/database/database_test_runner_2.dart' as database_test_runner_2;\nimport 'desktop/first_test/first_test.dart' as first_test;\n\nFuture<void> main() async {\n  await runIntegration5OnDesktop();\n}\n\nFuture<void> runIntegration5OnDesktop() async {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  first_test.main();\n\n  database_test_runner_2.main();\n  // DON'T add more tests here.\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop_runner_6.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'desktop/first_test/first_test.dart' as first_test;\nimport 'desktop/settings/settings_runner.dart' as settings_test_runner;\nimport 'desktop/sidebar/sidebar_test_runner.dart' as sidebar_test_runner;\nimport 'desktop/uncategorized/uncategorized_test_runner_1.dart'\n    as uncategorized_test_runner_1;\n\nFuture<void> main() async {\n  await runIntegration6OnDesktop();\n}\n\nFuture<void> runIntegration6OnDesktop() async {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  first_test.main();\n\n  settings_test_runner.main();\n  sidebar_test_runner.main();\n  uncategorized_test_runner_1.main();\n  // DON'T add more tests here.\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop_runner_7.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'desktop/document/document_test_runner_3.dart' as document_test_runner_3;\nimport 'desktop/first_test/first_test.dart' as first_test;\n\nFuture<void> main() async {\n  await runIntegration7OnDesktop();\n}\n\nFuture<void> runIntegration7OnDesktop() async {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  first_test.main();\n\n  document_test_runner_3.main();\n  // DON'T add more tests here.\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop_runner_8.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'desktop/document/document_test_runner_4.dart' as document_test_runner_4;\nimport 'desktop/first_test/first_test.dart' as first_test;\n\nFuture<void> main() async {\n  await runIntegration8OnDesktop();\n}\n\nFuture<void> runIntegration8OnDesktop() async {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  first_test.main();\n\n  document_test_runner_4.main();\n  // DON'T add more tests here.\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/desktop_runner_9.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'desktop/chat/chat_page_test.dart' as chat_page_test;\nimport 'desktop/database/database_icon_test.dart' as database_icon_test;\nimport 'desktop/first_test/first_test.dart' as first_test;\nimport 'desktop/uncategorized/code_block_language_selector_test.dart'\n    as code_language_selector;\nimport 'desktop/uncategorized/tabs_test.dart' as tabs_test;\n\nFuture<void> main() async {\n  await runIntegration9OnDesktop();\n}\n\nFuture<void> runIntegration9OnDesktop() async {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  first_test.main();\n  tabs_test.main();\n  code_language_selector.main();\n  database_icon_test.main();\n  chat_page_test.main();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/cloud/cloud_runner.dart",
    "content": "import 'document/publish_test.dart' as publish_test;\nimport 'document/share_link_test.dart' as share_link_test;\nimport 'space/space_test.dart' as space_test;\nimport 'workspace/workspace_operations_test.dart' as workspace_operations_test;\n\nFuture<void> main() async {\n  workspace_operations_test.main();\n  share_link_test.main();\n  publish_test.main();\n  space_test.main();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/cloud/document/publish_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/home/workspaces/create_workspace_menu.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/constants.dart';\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('publish:', () {\n    testWidgets('''\n1. publish document\n2. update path name\n3. unpublish document\n''', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      await tester.openPage(Constants.gettingStartedPageName);\n      await tester.editor.openMoreActionMenuOnMobile();\n\n      // click the publish button\n      await tester.editor.clickMoreActionItemOnMobile(\n        LocaleKeys.shareAction_publish.tr(),\n      );\n\n      // wait the notification dismiss\n      final publishSuccessText = find.findTextInFlowyText(\n        LocaleKeys.publish_publishSuccessfully.tr(),\n      );\n      expect(publishSuccessText, findsOneWidget);\n      await tester.pumpUntilNotFound(publishSuccessText);\n\n      // open the menu again, to check the publish status\n      await tester.editor.openMoreActionMenuOnMobile();\n      // expect to see the unpublish button and the visit site button\n      expect(\n        find.text(LocaleKeys.shareAction_unPublish.tr()),\n        findsOneWidget,\n      );\n      expect(\n        find.text(LocaleKeys.shareAction_visitSite.tr()),\n        findsOneWidget,\n      );\n\n      // update the path name\n      await tester.editor.clickMoreActionItemOnMobile(\n        LocaleKeys.shareAction_updatePathName.tr(),\n      );\n\n      const pathName1 = '???????????????';\n      const pathName2 = 'AppFlowy';\n\n      final textField = find.descendant(\n        of: find.byType(EditWorkspaceNameBottomSheet),\n        matching: find.byType(TextFormField),\n      );\n      await tester.enterText(textField, pathName1);\n      await tester.pumpAndSettle();\n\n      // wait 50ms to ensure the error message is shown\n      await tester.wait(50);\n\n      // click the confirm button\n      final confirmButton = find.text(LocaleKeys.button_confirm.tr());\n      await tester.tapButton(confirmButton);\n\n      // expect to see the update path name failed toast\n      final updatePathFailedText = find.text(\n        LocaleKeys.settings_sites_error_publishNameContainsInvalidCharacters\n            .tr(),\n      );\n      expect(updatePathFailedText, findsOneWidget);\n\n      // input the valid path name\n      await tester.enterText(textField, pathName2);\n      await tester.pumpAndSettle();\n      // click the confirm button\n      await tester.tapButton(confirmButton);\n\n      // wait 50ms to ensure the error message is shown\n      await tester.wait(50);\n\n      // expect to see the update path name success toast\n      final updatePathSuccessText = find.findTextInFlowyText(\n        LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),\n      );\n      expect(updatePathSuccessText, findsOneWidget);\n      await tester.pumpUntilNotFound(updatePathSuccessText);\n\n      // unpublish the document\n      await tester.editor.clickMoreActionItemOnMobile(\n        LocaleKeys.shareAction_unPublish.tr(),\n      );\n      final unPublishSuccessText = find.findTextInFlowyText(\n        LocaleKeys.publish_unpublishSuccessfully.tr(),\n      );\n      expect(unPublishSuccessText, findsOneWidget);\n      await tester.pumpUntilNotFound(unPublishSuccessText);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/cloud/document/share_link_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/constants.dart';\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('share link:', () {\n    testWidgets('copy share link and paste it on doc', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      // open the getting started page and paste the link\n      await tester.openPage(Constants.gettingStartedPageName);\n\n      // open the more action menu\n      await tester.editor.openMoreActionMenuOnMobile();\n\n      // click the share link item\n      await tester.editor.clickMoreActionItemOnMobile(\n        LocaleKeys.shareAction_copyLink.tr(),\n      );\n\n      // check the clipboard\n      final content = await Clipboard.getData(Clipboard.kTextPlain);\n      expect(\n        content?.text,\n        matches(appflowySharePageLinkPattern),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/cloud/space/space_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/home/space/manage_space_widget.dart';\nimport 'package:appflowy/mobile/presentation/home/space/mobile_space_header.dart';\nimport 'package:appflowy/mobile/presentation/home/space/mobile_space_menu.dart';\nimport 'package:appflowy/mobile/presentation/home/space/space_menu_bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/home/space/widgets.dart';\nimport 'package:appflowy/mobile/presentation/home/workspaces/create_workspace_menu.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('space operations:', () {\n    Future<void> openSpaceMenu(WidgetTester tester) async {\n      final spaceHeader = find.byType(MobileSpaceHeader);\n      await tester.tapButton(spaceHeader);\n      await tester.pumpUntilFound(find.byType(MobileSpaceMenu));\n    }\n\n    Future<void> openSpaceMenuMoreOptions(\n      WidgetTester tester,\n      ViewPB space,\n    ) async {\n      final spaceMenuItemTrailing = find.byWidgetPredicate(\n        (w) => w is SpaceMenuItemTrailing && w.space.id == space.id,\n      );\n      final moreOptions = find.descendant(\n        of: spaceMenuItemTrailing,\n        matching: find.byWidgetPredicate(\n          (w) =>\n              w is FlowySvg &&\n              w.svg.path == FlowySvgs.workspace_three_dots_s.path,\n        ),\n      );\n      await tester.tapButton(moreOptions);\n      await tester.pumpUntilFound(find.byType(SpaceMenuMoreOptions));\n    }\n\n    // combine the tests together to reduce the CI time\n    testWidgets('''\n1. create a new space\n2. update the space name\n3. update the space permission\n4. update the space icon\n5. delete the space\n''', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      // 1. create a new space\n      // click the space menu\n      await openSpaceMenu(tester);\n\n      // click the create a new space button\n      final createNewSpaceButton = find.text(\n        LocaleKeys.space_createNewSpace.tr(),\n      );\n      await tester.pumpUntilFound(createNewSpaceButton);\n      await tester.tapButton(createNewSpaceButton);\n\n      // input the new space name\n      final inputField = find.descendant(\n        of: find.byType(ManageSpaceWidget),\n        matching: find.byType(TextField),\n      );\n      const newSpaceName = 'AppFlowy';\n      await tester.enterText(inputField, newSpaceName);\n      await tester.pumpAndSettle();\n\n      // change the space permission to private\n      final permissionOption = find.byType(ManageSpacePermissionOption);\n      await tester.tapButton(permissionOption);\n      await tester.pumpAndSettle();\n\n      final privateOption = find.text(LocaleKeys.space_privatePermission.tr());\n      await tester.tapButton(privateOption);\n      await tester.pumpAndSettle();\n\n      // change the space icon color\n      final color = builtInSpaceColors[1];\n      final iconOption = find.descendant(\n        of: find.byType(ManageSpaceIconOption),\n        matching: find.byWidgetPredicate(\n          (w) => w is SpaceColorItem && w.color == color,\n        ),\n      );\n      await tester.tapButton(iconOption);\n      await tester.pumpAndSettle();\n\n      // change the space icon\n      final icon = kIconGroups![0].icons[1];\n      final iconItem = find.descendant(\n        of: find.byType(ManageSpaceIconOption),\n        matching: find.byWidgetPredicate(\n          (w) => w is SpaceIconItem && w.icon == icon,\n        ),\n      );\n      await tester.tapButton(iconItem);\n      await tester.pumpAndSettle();\n\n      // click the done button\n      final doneButton = find.descendant(\n        of: find.byWidgetPredicate(\n          (w) =>\n              w is BottomSheetHeader &&\n              w.title == LocaleKeys.space_createSpace.tr(),\n        ),\n        matching: find.text(LocaleKeys.button_done.tr()),\n      );\n      await tester.tapButton(doneButton);\n      await tester.pumpAndSettle();\n\n      // wait 100ms for the space to be created\n      await tester.wait(100);\n\n      // verify the space is created\n      await openSpaceMenu(tester);\n      final spaceItems = find.byType(MobileSpaceMenuItem);\n      // expect to see 3 space items, 2 are built-in, 1 is the new space\n      expect(spaceItems, findsNWidgets(3));\n      // convert the space item to a widget\n      final spaceWidget =\n          tester.widgetList<MobileSpaceMenuItem>(spaceItems).last;\n      final space = spaceWidget.space;\n      expect(space.name, newSpaceName);\n      expect(space.spacePermission, SpacePermission.private);\n      expect(space.spaceIcon, icon.iconPath);\n      expect(space.spaceIconColor, color);\n\n      // open the SpaceMenuMoreOptions menu\n      await openSpaceMenuMoreOptions(tester, space);\n\n      // 2. rename the space name\n      final renameOption = find.text(LocaleKeys.button_rename.tr());\n      await tester.tapButton(renameOption);\n      await tester.pumpUntilFound(find.byType(EditWorkspaceNameBottomSheet));\n\n      // input the new space name\n      final renameInputField = find.descendant(\n        of: find.byType(EditWorkspaceNameBottomSheet),\n        matching: find.byType(TextField),\n      );\n      const renameSpaceName = 'HelloWorld';\n      await tester.enterText(renameInputField, renameSpaceName);\n      await tester.pumpAndSettle();\n      await tester.tapButton(find.text(LocaleKeys.button_confirm.tr()));\n\n      // click the done button\n      await tester.pumpAndSettle();\n\n      final renameSuccess = find.text(\n        LocaleKeys.space_success_renameSpace.tr(),\n      );\n      await tester.pumpUntilNotFound(renameSuccess);\n\n      // check the space name is updated\n      await openSpaceMenu(tester);\n      final renameSpaceItem = find.descendant(\n        of: find.byType(MobileSpaceMenuItem),\n        matching: find.text(renameSpaceName),\n      );\n      expect(renameSpaceItem, findsOneWidget);\n\n      // 3. manage the space\n      await openSpaceMenuMoreOptions(tester, space);\n\n      final manageOption = find.text(LocaleKeys.space_manage.tr());\n      await tester.tapButton(manageOption);\n      await tester.pumpUntilFound(find.byType(ManageSpaceWidget));\n\n      // 3.1 rename the space\n      final textField = find.descendant(\n        of: find.byType(ManageSpaceWidget),\n        matching: find.byType(TextField),\n      );\n      await tester.enterText(textField, 'AppFlowy');\n      await tester.pumpAndSettle();\n\n      // 3.2 change the permission\n      final permissionOption2 = find.byType(ManageSpacePermissionOption);\n      await tester.tapButton(permissionOption2);\n      await tester.pumpAndSettle();\n\n      final publicOption = find.text(LocaleKeys.space_publicPermission.tr());\n      await tester.tapButton(publicOption);\n      await tester.pumpAndSettle();\n\n      // 3.3 change the icon\n      // change the space icon color\n      final color2 = builtInSpaceColors[2];\n      final iconOption2 = find.descendant(\n        of: find.byType(ManageSpaceIconOption),\n        matching: find.byWidgetPredicate(\n          (w) => w is SpaceColorItem && w.color == color2,\n        ),\n      );\n      await tester.tapButton(iconOption2);\n      await tester.pumpAndSettle();\n\n      // change the space icon\n      final icon2 = kIconGroups![0].icons[2];\n      final iconItem2 = find.descendant(\n        of: find.byType(ManageSpaceIconOption),\n        matching: find.byWidgetPredicate(\n          (w) => w is SpaceIconItem && w.icon == icon2,\n        ),\n      );\n      await tester.tapButton(iconItem2);\n      await tester.pumpAndSettle();\n\n      // click the done button\n      final doneButton2 = find.descendant(\n        of: find.byWidgetPredicate(\n          (w) =>\n              w is BottomSheetHeader &&\n              w.title == LocaleKeys.space_manageSpace.tr(),\n        ),\n        matching: find.text(LocaleKeys.button_done.tr()),\n      );\n      await tester.tapButton(doneButton2);\n      await tester.pumpAndSettle();\n\n      // check the space is updated\n      final spaceItems2 = find.byType(MobileSpaceMenuItem);\n      final spaceWidget2 =\n          tester.widgetList<MobileSpaceMenuItem>(spaceItems2).last;\n      final space2 = spaceWidget2.space;\n      expect(space2.name, 'AppFlowy');\n      expect(space2.spacePermission, SpacePermission.publicToAll);\n      expect(space2.spaceIcon, icon2.iconPath);\n      expect(space2.spaceIconColor, color2);\n      final manageSuccess = find.text(\n        LocaleKeys.space_success_updateSpace.tr(),\n      );\n      await tester.pumpUntilNotFound(manageSuccess);\n\n      // 4. duplicate the space\n      await openSpaceMenuMoreOptions(tester, space);\n      final duplicateOption = find.text(LocaleKeys.space_duplicate.tr());\n      await tester.tapButton(duplicateOption);\n      final duplicateSuccess = find.text(\n        LocaleKeys.space_success_duplicateSpace.tr(),\n      );\n      await tester.pumpUntilNotFound(duplicateSuccess);\n\n      // check the space is duplicated\n      await openSpaceMenu(tester);\n      final spaceItems3 = find.byType(MobileSpaceMenuItem);\n      expect(spaceItems3, findsNWidgets(4));\n\n      // 5. delete the space\n      await openSpaceMenuMoreOptions(tester, space);\n      final deleteOption = find.text(LocaleKeys.button_delete.tr());\n      await tester.tapButton(deleteOption);\n      final confirmDeleteButton = find.descendant(\n        of: find.byType(CupertinoDialogAction),\n        matching: find.text(LocaleKeys.button_delete.tr()),\n      );\n      await tester.tapButton(confirmDeleteButton);\n      final deleteSuccess = find.text(\n        LocaleKeys.space_success_deleteSpace.tr(),\n      );\n      await tester.pumpUntilNotFound(deleteSuccess);\n\n      // check the space is deleted\n      final spaceItems4 = find.byType(MobileSpaceMenuItem);\n      expect(spaceItems4, findsNWidgets(3));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/cloud/workspace/workspace_operations_test.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../../shared/constants.dart';\nimport '../../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('workspace operations:', () {\n    testWidgets('create a new workspace', (tester) async {\n      await tester.initializeAppFlowy(\n        cloudType: AuthenticatorType.appflowyCloudSelfHost,\n      );\n      await tester.tapGoogleLoginInButton();\n      await tester.expectToSeeHomePageWithGetStartedPage();\n\n      // click the create a new workspace button\n      await tester.tapButton(find.text(Constants.defaultWorkspaceName));\n      await tester.tapButton(find.text(LocaleKeys.workspace_create.tr()));\n\n      // input the new workspace name\n      final inputField = find.byType(TextFormField);\n      const newWorkspaceName = 'AppFlowy';\n      await tester.enterText(inputField, newWorkspaceName);\n      await tester.pumpAndSettle();\n\n      // wait for the workspace to be created\n      await tester.pumpUntilFound(\n        find.text(LocaleKeys.workspace_createSuccess.tr()),\n      );\n\n      // expect to see the new workspace\n      expect(find.text(newWorkspaceName), findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/document/at_menu_test.dart",
    "content": "import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart';\nimport 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  const title = 'Test At Menu';\n\n  group('at menu', () {\n    testWidgets('show at menu', (tester) async {\n      await tester.launchInAnonymousMode();\n      await tester.createPageAndShowAtMenu(title);\n      final menuWidget = find.byType(MobileInlineActionsMenu);\n      expect(menuWidget, findsOneWidget);\n    });\n\n    testWidgets('search by at menu', (tester) async {\n      await tester.launchInAnonymousMode();\n      await tester.createPageAndShowAtMenu(title);\n      const searchText = gettingStarted;\n      await tester.ime.insertText(searchText);\n      final actionWidgets = find.byType(MobileInlineActionsWidget);\n      expect(actionWidgets, findsNWidgets(2));\n    });\n\n    testWidgets('tap at menu', (tester) async {\n      await tester.launchInAnonymousMode();\n      await tester.createPageAndShowAtMenu(title);\n      const searchText = gettingStarted;\n      await tester.ime.insertText(searchText);\n      final actionWidgets = find.byType(MobileInlineActionsWidget);\n      await tester.tap(actionWidgets.last);\n      expect(find.byType(MentionPageBlock), findsOneWidget);\n    });\n\n    testWidgets('create subpage with at menu', (tester) async {\n      await tester.launchInAnonymousMode();\n      await tester.createNewDocumentOnMobile(title);\n      await tester.editor.tapLineOfEditorAt(0);\n      const subpageName = 'Subpage';\n      await tester.ime.insertText('[[$subpageName');\n      await tester.pumpAndSettle();\n      final actionWidgets = find.byType(MobileInlineActionsWidget);\n      await tester.tapButton(actionWidgets.first);\n      final firstNode =\n          tester.editor.getCurrentEditorState().getNodeAtPath([0]);\n      assert(firstNode != null);\n      expect(firstNode!.delta?.toPlainText().contains('['), false);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/document/document_test_runner.dart",
    "content": "import 'package:integration_test/integration_test.dart';\n\nimport 'at_menu_test.dart' as at_menu;\nimport 'at_menu_test.dart' as at_menu_test;\nimport 'page_style_test.dart' as page_style_test;\nimport 'plus_menu_test.dart' as plus_menu_test;\nimport 'simple_table_test.dart' as simple_table_test;\nimport 'slash_menu_test.dart' as slash_menu;\nimport 'title_test.dart' as title_test;\nimport 'toolbar_test.dart' as toolbar_test;\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  // Document integration tests\n  title_test.main();\n  page_style_test.main();\n  plus_menu_test.main();\n  at_menu_test.main();\n  simple_table_test.main();\n  toolbar_test.main();\n  slash_menu.main();\n  at_menu.main();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/document/icon_test.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';\nimport 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/emoji.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('document title:', () {\n    testWidgets('update page custom image icon in title bar', (tester) async {\n      await tester.launchInAnonymousMode();\n\n      /// prepare local image\n      final iconData = await tester.prepareImageIcon();\n\n      /// create an empty page\n      await tester\n          .tapButton(find.byKey(BottomNavigationBarItemType.add.valueKey));\n\n      /// show Page style page\n      await tester.tapButton(find.byType(MobileViewPageLayoutButton));\n      final pageStyleIcon = find.byType(PageStyleIcon);\n      final iconInPageStyleIcon = find.descendant(\n        of: pageStyleIcon,\n        matching: find.byType(RawEmojiIconWidget),\n      );\n      expect(iconInPageStyleIcon, findsNothing);\n\n      /// show icon picker\n      await tester.tapButton(pageStyleIcon);\n\n      /// upload custom icon\n      await tester.pickImage(iconData);\n\n      /// check result\n      final documentPage = find.byType(MobileDocumentScreen);\n      final rawEmojiIconFinder = find\n          .descendant(\n            of: documentPage,\n            matching: find.byType(RawEmojiIconWidget),\n          )\n          .last;\n      final rawEmojiIconWidget =\n          rawEmojiIconFinder.evaluate().first.widget as RawEmojiIconWidget;\n      final iconDataInWidget = rawEmojiIconWidget.emoji;\n      expect(iconDataInWidget.type, FlowyIconType.custom);\n      final imageFinder =\n          find.descendant(of: rawEmojiIconFinder, matching: find.byType(Image));\n      expect(imageFinder, findsOneWidget);\n    });\n\n    testWidgets('update page custom svg icon in title bar', (tester) async {\n      await tester.launchInAnonymousMode();\n\n      /// prepare local image\n      final iconData = await tester.prepareSvgIcon();\n\n      /// create an empty page\n      await tester\n          .tapButton(find.byKey(BottomNavigationBarItemType.add.valueKey));\n\n      /// show Page style page\n      await tester.tapButton(find.byType(MobileViewPageLayoutButton));\n      final pageStyleIcon = find.byType(PageStyleIcon);\n      final iconInPageStyleIcon = find.descendant(\n        of: pageStyleIcon,\n        matching: find.byType(RawEmojiIconWidget),\n      );\n      expect(iconInPageStyleIcon, findsNothing);\n\n      /// show icon picker\n      await tester.tapButton(pageStyleIcon);\n\n      /// upload custom icon\n      await tester.pickImage(iconData);\n\n      /// check result\n      final documentPage = find.byType(MobileDocumentScreen);\n      final rawEmojiIconFinder = find\n          .descendant(\n            of: documentPage,\n            matching: find.byType(RawEmojiIconWidget),\n          )\n          .last;\n      final rawEmojiIconWidget =\n          rawEmojiIconFinder.evaluate().first.widget as RawEmojiIconWidget;\n      final iconDataInWidget = rawEmojiIconWidget.emoji;\n      expect(iconDataInWidget.type, FlowyIconType.custom);\n      final svgFinder = find.descendant(\n        of: rawEmojiIconFinder,\n        matching: find.byType(SvgPicture),\n      );\n      expect(svgFinder, findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/document/page_style_test.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';\nimport 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_page.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/emoji.dart';\nimport '../../shared/util.dart';\n\nvoid main() {\n  setUpAll(() {\n    IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n    RecentIcons.enable = false;\n  });\n\n  tearDownAll(() {\n    RecentIcons.enable = true;\n  });\n\n  group('document page style:', () {\n    double getCurrentEditorFontSize() {\n      final editorPage = find\n          .byType(AppFlowyEditorPage)\n          .evaluate()\n          .single\n          .widget as AppFlowyEditorPage;\n      return editorPage.styleCustomizer\n          .style()\n          .textStyleConfiguration\n          .text\n          .fontSize!;\n    }\n\n    double getCurrentEditorLineHeight() {\n      final editorPage = find\n          .byType(AppFlowyEditorPage)\n          .evaluate()\n          .single\n          .widget as AppFlowyEditorPage;\n      return editorPage.styleCustomizer\n          .style()\n          .textStyleConfiguration\n          .lineHeight;\n    }\n\n    testWidgets('change font size in page style settings', (tester) async {\n      await tester.launchInAnonymousMode();\n\n      // click the getting start page\n      await tester.openPage(gettingStarted);\n      // click the layout button\n      await tester.tapButton(find.byType(MobileViewPageLayoutButton));\n      expect(getCurrentEditorFontSize(), PageStyleFontLayout.normal.fontSize);\n      // change font size from normal to large\n      await tester.tapSvgButton(FlowySvgs.m_font_size_large_s);\n      expect(getCurrentEditorFontSize(), PageStyleFontLayout.large.fontSize);\n      // change font size from large to small\n      await tester.tapSvgButton(FlowySvgs.m_font_size_small_s);\n      expect(getCurrentEditorFontSize(), PageStyleFontLayout.small.fontSize);\n    });\n\n    testWidgets('change line height in page style settings', (tester) async {\n      await tester.launchInAnonymousMode();\n\n      // click the getting start page\n      await tester.openPage(gettingStarted);\n      // click the layout button\n      await tester.tapButton(find.byType(MobileViewPageLayoutButton));\n      var lineHeight = getCurrentEditorLineHeight();\n      expect(\n        lineHeight,\n        PageStyleLineHeightLayout.normal.lineHeight,\n      );\n      // change line height from normal to large\n      await tester.tapSvgButton(FlowySvgs.m_layout_large_s);\n      await tester.pumpAndSettle();\n      lineHeight = getCurrentEditorLineHeight();\n      expect(\n        lineHeight,\n        PageStyleLineHeightLayout.large.lineHeight,\n      );\n      // change line height from large to small\n      await tester.tapSvgButton(FlowySvgs.m_layout_small_s);\n      lineHeight = getCurrentEditorLineHeight();\n      expect(\n        lineHeight,\n        PageStyleLineHeightLayout.small.lineHeight,\n      );\n    });\n\n    testWidgets('use built-in image as cover', (tester) async {\n      await tester.launchInAnonymousMode();\n\n      // click the getting start page\n      await tester.openPage(gettingStarted);\n      // click the layout button\n      await tester.tapButton(find.byType(MobileViewPageLayoutButton));\n      // toggle the preset button\n      await tester.tapSvgButton(FlowySvgs.m_page_style_presets_m);\n\n      // select the first preset\n      final firstBuiltInImage = find.byWidgetPredicate(\n        (widget) =>\n            widget is Image &&\n            widget.image is AssetImage &&\n            (widget.image as AssetImage).assetName ==\n                PageStyleCoverImageType.builtInImagePath('1'),\n      );\n      await tester.tap(firstBuiltInImage);\n\n      // click done button to exit the page style settings\n      await tester.tapButton(find.byType(BottomSheetDoneButton).first);\n\n      // check the cover\n      final builtInCover = find.descendant(\n        of: find.byType(DocumentImmersiveCover),\n        matching: firstBuiltInImage,\n      );\n      expect(builtInCover, findsOneWidget);\n    });\n\n    testWidgets('page style icon', (tester) async {\n      await tester.launchInAnonymousMode();\n\n      final createPageButton =\n          find.byKey(BottomNavigationBarItemType.add.valueKey);\n      await tester.tapButton(createPageButton);\n\n      /// toggle the preset button\n      await tester.tapSvgButton(FlowySvgs.m_layout_s);\n\n      /// select document plugins emoji\n      final pageStyleIcon = find.byType(PageStyleIcon);\n\n      /// there should be none of emoji\n      final noneText = find.text(LocaleKeys.pageStyle_none.tr());\n      expect(noneText, findsOneWidget);\n      await tester.tapButton(pageStyleIcon);\n\n      /// select an emoji\n      const emoji = '😄';\n      await tester.tapEmoji(emoji);\n      await tester.tapSvgButton(FlowySvgs.m_layout_s);\n      expect(noneText, findsNothing);\n      expect(\n        find.descendant(\n          of: pageStyleIcon,\n          matching: find.text(emoji),\n        ),\n        findsOneWidget,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/document/plus_menu_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart';\nimport 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('document plus menu:', () {\n    testWidgets('add the toggle heading blocks via plus menu', (tester) async {\n      await tester.launchInAnonymousMode();\n      await tester.createNewDocumentOnMobile('toggle heading blocks');\n\n      final editorState = tester.editor.getCurrentEditorState();\n      // focus on the editor\n      unawaited(\n        editorState.updateSelectionWithReason(\n          Selection.collapsed(Position(path: [0])),\n          reason: SelectionUpdateReason.uiEvent,\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      // open the plus menu and select the toggle heading block\n      await tester.openPlusMenuAndClickButton(\n        LocaleKeys.document_slashMenu_name_toggleHeading1.tr(),\n      );\n\n      // check the block is inserted\n      final block1 = editorState.getNodeAtPath([0])!;\n      expect(block1.type, equals(ToggleListBlockKeys.type));\n      expect(block1.attributes[ToggleListBlockKeys.level], equals(1));\n\n      // click the expand button won't cancel the selection\n      await tester.tapButton(find.byIcon(Icons.arrow_right));\n      expect(\n        editorState.selection,\n        equals(Selection.collapsed(Position(path: [0]))),\n      );\n\n      // focus on the next line\n      unawaited(\n        editorState.updateSelectionWithReason(\n          Selection.collapsed(Position(path: [1])),\n          reason: SelectionUpdateReason.uiEvent,\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      // open the plus menu and select the toggle heading block\n      await tester.openPlusMenuAndClickButton(\n        LocaleKeys.document_slashMenu_name_toggleHeading2.tr(),\n      );\n\n      // check the block is inserted\n      final block2 = editorState.getNodeAtPath([1])!;\n      expect(block2.type, equals(ToggleListBlockKeys.type));\n      expect(block2.attributes[ToggleListBlockKeys.level], equals(2));\n\n      // focus on the next line\n      await tester.pumpAndSettle();\n\n      // open the plus menu and select the toggle heading block\n      await tester.openPlusMenuAndClickButton(\n        LocaleKeys.document_slashMenu_name_toggleHeading3.tr(),\n      );\n\n      // check the block is inserted\n      final block3 = editorState.getNodeAtPath([2])!;\n      expect(block3.type, equals(ToggleListBlockKeys.type));\n      expect(block3.attributes[ToggleListBlockKeys.level], equals(3));\n\n      // wait a few milliseconds to ensure the selection is updated\n      await Future.delayed(const Duration(milliseconds: 100));\n      // check the selection is collapsed\n      expect(\n        editorState.selection,\n        equals(Selection.collapsed(Position(path: [2]))),\n      );\n    });\n\n    const title = 'Test Plus Menu';\n    testWidgets('show plus menu', (tester) async {\n      await tester.launchInAnonymousMode();\n      await tester.createPageAndShowPlusMenu(title);\n      final menuWidget = find.byType(MobileInlineActionsMenu);\n      expect(menuWidget, findsOneWidget);\n    });\n\n    testWidgets('search by plus menu', (tester) async {\n      await tester.launchInAnonymousMode();\n      await tester.createPageAndShowPlusMenu(title);\n      const searchText = gettingStarted;\n      await tester.ime.insertText(searchText);\n      final actionWidgets = find.byType(MobileInlineActionsWidget);\n      expect(actionWidgets, findsNWidgets(2));\n    });\n\n    testWidgets('tap plus menu', (tester) async {\n      await tester.launchInAnonymousMode();\n      await tester.createPageAndShowPlusMenu(title);\n      const searchText = gettingStarted;\n      await tester.ime.insertText(searchText);\n      final actionWidgets = find.byType(MobileInlineActionsWidget);\n      await tester.tap(actionWidgets.last);\n      expect(find.byType(MentionPageBlock), findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/document/simple_table_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('simple table:', () {\n    testWidgets('''\n1. insert a simple table via + menu\n2. insert a row above the table\n3. insert a row below the table\n4. insert a column left to the table\n5. insert a column right to the table\n6. delete the first row\n7. delete the first column\n''', (tester) async {\n      await tester.launchInAnonymousMode();\n      await tester.createNewDocumentOnMobile('simple table');\n\n      final editorState = tester.editor.getCurrentEditorState();\n      // focus on the editor\n      unawaited(\n        editorState.updateSelectionWithReason(\n          Selection.collapsed(Position(path: [0])),\n          reason: SelectionUpdateReason.uiEvent,\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      final firstParagraphPath = [0, 0, 0, 0];\n\n      // open the plus menu and select the table block\n      {\n        await tester.openPlusMenuAndClickButton(\n          LocaleKeys.document_slashMenu_name_table.tr(),\n        );\n\n        // check the block is inserted\n        final table = editorState.getNodeAtPath([0])!;\n        expect(table.type, equals(SimpleTableBlockKeys.type));\n        expect(table.rowLength, equals(2));\n        expect(table.columnLength, equals(2));\n\n        // focus on the first cell\n\n        final selection = editorState.selection!;\n        expect(selection.isCollapsed, isTrue);\n        expect(selection.start.path, equals(firstParagraphPath));\n      }\n\n      // insert left and insert right\n      {\n        // click the column menu button\n        await tester.clickColumnMenuButton(0);\n\n        // insert left, insert right\n        await tester.tapButton(\n          find.findTextInFlowyText(\n            LocaleKeys.document_plugins_simpleTable_moreActions_insertLeft.tr(),\n          ),\n        );\n        await tester.tapButton(\n          find.findTextInFlowyText(\n            LocaleKeys.document_plugins_simpleTable_moreActions_insertRight\n                .tr(),\n          ),\n        );\n\n        await tester.cancelTableActionMenu();\n\n        // check the table is updated\n        final table = editorState.getNodeAtPath([0])!;\n        expect(table.type, equals(SimpleTableBlockKeys.type));\n        expect(table.rowLength, equals(2));\n        expect(table.columnLength, equals(4));\n      }\n\n      // insert above and insert below\n      {\n        // focus on the first cell\n        unawaited(\n          editorState.updateSelectionWithReason(\n            Selection.collapsed(Position(path: firstParagraphPath)),\n            reason: SelectionUpdateReason.uiEvent,\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        // click the row menu button\n        await tester.clickRowMenuButton(0);\n\n        await tester.tapButton(\n          find.findTextInFlowyText(\n            LocaleKeys.document_plugins_simpleTable_moreActions_insertAbove\n                .tr(),\n          ),\n        );\n        await tester.tapButton(\n          find.findTextInFlowyText(\n            LocaleKeys.document_plugins_simpleTable_moreActions_insertBelow\n                .tr(),\n          ),\n        );\n        await tester.cancelTableActionMenu();\n\n        // check the table is updated\n        final table = editorState.getNodeAtPath([0])!;\n        expect(table.rowLength, equals(4));\n        expect(table.columnLength, equals(4));\n      }\n\n      // delete the first row\n      {\n        // focus on the first cell\n        unawaited(\n          editorState.updateSelectionWithReason(\n            Selection.collapsed(Position(path: firstParagraphPath)),\n            reason: SelectionUpdateReason.uiEvent,\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        // delete the first row\n        await tester.clickRowMenuButton(0);\n        await tester.clickSimpleTableQuickAction(SimpleTableMoreAction.delete);\n        await tester.cancelTableActionMenu();\n\n        // check the table is updated\n        final table = editorState.getNodeAtPath([0])!;\n        expect(table.rowLength, equals(3));\n        expect(table.columnLength, equals(4));\n      }\n\n      // delete the first column\n      {\n        unawaited(\n          editorState.updateSelectionWithReason(\n            Selection.collapsed(Position(path: firstParagraphPath)),\n            reason: SelectionUpdateReason.uiEvent,\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        await tester.clickColumnMenuButton(0);\n        await tester.clickSimpleTableQuickAction(SimpleTableMoreAction.delete);\n        await tester.cancelTableActionMenu();\n\n        // check the table is updated\n        final table = editorState.getNodeAtPath([0])!;\n        expect(table.rowLength, equals(3));\n        expect(table.columnLength, equals(3));\n      }\n    });\n\n    testWidgets('''\n1. insert a simple table via + menu\n2. enable header column\n3. enable header row\n4. set to page width\n5. distribute columns evenly\n''', (tester) async {\n      await tester.launchInAnonymousMode();\n      await tester.createNewDocumentOnMobile('simple table');\n\n      final editorState = tester.editor.getCurrentEditorState();\n      // focus on the editor\n      unawaited(\n        editorState.updateSelectionWithReason(\n          Selection.collapsed(Position(path: [0])),\n          reason: SelectionUpdateReason.uiEvent,\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      final firstParagraphPath = [0, 0, 0, 0];\n\n      // open the plus menu and select the table block\n      {\n        await tester.openPlusMenuAndClickButton(\n          LocaleKeys.document_slashMenu_name_table.tr(),\n        );\n\n        // check the block is inserted\n        final table = editorState.getNodeAtPath([0])!;\n        expect(table.type, equals(SimpleTableBlockKeys.type));\n        expect(table.rowLength, equals(2));\n        expect(table.columnLength, equals(2));\n\n        // focus on the first cell\n\n        final selection = editorState.selection!;\n        expect(selection.isCollapsed, isTrue);\n        expect(selection.start.path, equals(firstParagraphPath));\n      }\n\n      // enable header column\n      {\n        // click the column menu button\n        await tester.clickColumnMenuButton(0);\n\n        // enable header column\n        await tester.tapButton(\n          find.findTextInFlowyText(\n            LocaleKeys.document_plugins_simpleTable_moreActions_headerColumn\n                .tr(),\n          ),\n        );\n      }\n\n      // enable header row\n      {\n        // focus on the first cell\n        unawaited(\n          editorState.updateSelectionWithReason(\n            Selection.collapsed(Position(path: firstParagraphPath)),\n            reason: SelectionUpdateReason.uiEvent,\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        // click the row menu button\n        await tester.clickRowMenuButton(0);\n\n        // enable header column\n        await tester.tapButton(\n          find.findTextInFlowyText(\n            LocaleKeys.document_plugins_simpleTable_moreActions_headerRow.tr(),\n          ),\n        );\n      }\n\n      // check the table is updated\n      final table = editorState.getNodeAtPath([0])!;\n      expect(table.type, equals(SimpleTableBlockKeys.type));\n      expect(table.isHeaderColumnEnabled, isTrue);\n      expect(table.isHeaderRowEnabled, isTrue);\n\n      // disable header column\n      {\n        // focus on the first cell\n        unawaited(\n          editorState.updateSelectionWithReason(\n            Selection.collapsed(Position(path: firstParagraphPath)),\n            reason: SelectionUpdateReason.uiEvent,\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        // click the row menu button\n        await tester.clickColumnMenuButton(0);\n\n        final toggleButton = find.descendant(\n          of: find.byType(SimpleTableHeaderActionButton),\n          matching: find.byType(CupertinoSwitch),\n        );\n        await tester.tapButton(toggleButton);\n      }\n\n      // enable header row\n      {\n        // focus on the first cell\n        unawaited(\n          editorState.updateSelectionWithReason(\n            Selection.collapsed(Position(path: firstParagraphPath)),\n            reason: SelectionUpdateReason.uiEvent,\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        // click the row menu button\n        await tester.clickRowMenuButton(0);\n\n        // enable header column\n        final toggleButton = find.descendant(\n          of: find.byType(SimpleTableHeaderActionButton),\n          matching: find.byType(CupertinoSwitch),\n        );\n        await tester.tapButton(toggleButton);\n      }\n\n      // check the table is updated\n      expect(table.isHeaderColumnEnabled, isFalse);\n      expect(table.isHeaderRowEnabled, isFalse);\n\n      // set to page width\n      {\n        final table = editorState.getNodeAtPath([0])!;\n        final beforeWidth = table.width;\n        // focus on the first cell\n        unawaited(\n          editorState.updateSelectionWithReason(\n            Selection.collapsed(Position(path: firstParagraphPath)),\n            reason: SelectionUpdateReason.uiEvent,\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        // click the row menu button\n        await tester.clickRowMenuButton(0);\n\n        // enable header column\n        await tester.tapButton(\n          find.findTextInFlowyText(\n            LocaleKeys.document_plugins_simpleTable_moreActions_setToPageWidth\n                .tr(),\n          ),\n        );\n\n        // check the table is updated\n        expect(table.width, greaterThan(beforeWidth));\n      }\n\n      // distribute columns evenly\n      {\n        final table = editorState.getNodeAtPath([0])!;\n        final beforeWidth = table.width;\n\n        // focus on the first cell\n        unawaited(\n          editorState.updateSelectionWithReason(\n            Selection.collapsed(Position(path: firstParagraphPath)),\n            reason: SelectionUpdateReason.uiEvent,\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        // click the column menu button\n        await tester.clickColumnMenuButton(0);\n\n        // distribute columns evenly\n        await tester.tapButton(\n          find.findTextInFlowyText(\n            LocaleKeys\n                .document_plugins_simpleTable_moreActions_distributeColumnsWidth\n                .tr(),\n          ),\n        );\n\n        // check the table is updated\n        expect(table.width, equals(beforeWidth));\n      }\n    });\n\n    testWidgets('''\n1. insert a simple table via + menu\n2. bold\n3. clear content\n''', (tester) async {\n      await tester.launchInAnonymousMode();\n      await tester.createNewDocumentOnMobile('simple table');\n\n      final editorState = tester.editor.getCurrentEditorState();\n      // focus on the editor\n      unawaited(\n        editorState.updateSelectionWithReason(\n          Selection.collapsed(Position(path: [0])),\n          reason: SelectionUpdateReason.uiEvent,\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      final firstParagraphPath = [0, 0, 0, 0];\n\n      // open the plus menu and select the table block\n      {\n        await tester.openPlusMenuAndClickButton(\n          LocaleKeys.document_slashMenu_name_table.tr(),\n        );\n\n        // check the block is inserted\n        final table = editorState.getNodeAtPath([0])!;\n        expect(table.type, equals(SimpleTableBlockKeys.type));\n        expect(table.rowLength, equals(2));\n        expect(table.columnLength, equals(2));\n\n        // focus on the first cell\n\n        final selection = editorState.selection!;\n        expect(selection.isCollapsed, isTrue);\n        expect(selection.start.path, equals(firstParagraphPath));\n      }\n\n      await tester.ime.insertText('Hello');\n\n      // enable bold\n      {\n        // click the column menu button\n        await tester.clickColumnMenuButton(0);\n\n        // enable bold\n        await tester.clickSimpleTableBoldContentAction();\n        await tester.cancelTableActionMenu();\n\n        // check the first cell is bold\n        final paragraph = editorState.getNodeAtPath(firstParagraphPath)!;\n        expect(paragraph.isInBoldColumn, isTrue);\n      }\n\n      // clear content\n      {\n        // focus on the first cell\n        unawaited(\n          editorState.updateSelectionWithReason(\n            Selection.collapsed(Position(path: firstParagraphPath)),\n            reason: SelectionUpdateReason.uiEvent,\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        // click the column menu button\n        await tester.clickColumnMenuButton(0);\n\n        final clearContents = find.findTextInFlowyText(\n          LocaleKeys.document_plugins_simpleTable_moreActions_clearContents\n              .tr(),\n        );\n\n        // clear content\n        final scrollable = find.descendant(\n          of: find.byType(SimpleTableBottomSheet),\n          matching: find.byType(Scrollable),\n        );\n        await tester.scrollUntilVisible(\n          clearContents,\n          100,\n          scrollable: scrollable,\n        );\n        await tester.tapButton(clearContents);\n        await tester.cancelTableActionMenu();\n\n        // check the first cell is empty\n        final paragraph = editorState.getNodeAtPath(firstParagraphPath)!;\n        expect(paragraph.delta!, isEmpty);\n      }\n    });\n\n    testWidgets('''\n1. insert a simple table via + menu\n2. insert a heading block in table cell\n''', (tester) async {\n      await tester.launchInAnonymousMode();\n      await tester.createNewDocumentOnMobile('simple table');\n\n      final editorState = tester.editor.getCurrentEditorState();\n      // focus on the editor\n      unawaited(\n        editorState.updateSelectionWithReason(\n          Selection.collapsed(Position(path: [0])),\n          reason: SelectionUpdateReason.uiEvent,\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      final firstParagraphPath = [0, 0, 0, 0];\n\n      // open the plus menu and select the table block\n      {\n        await tester.openPlusMenuAndClickButton(\n          LocaleKeys.document_slashMenu_name_table.tr(),\n        );\n\n        // check the block is inserted\n        final table = editorState.getNodeAtPath([0])!;\n        expect(table.type, equals(SimpleTableBlockKeys.type));\n        expect(table.rowLength, equals(2));\n        expect(table.columnLength, equals(2));\n\n        // focus on the first cell\n\n        final selection = editorState.selection!;\n        expect(selection.isCollapsed, isTrue);\n        expect(selection.start.path, equals(firstParagraphPath));\n      }\n\n      // open the plus menu and select the heading block\n      {\n        await tester.openPlusMenuAndClickButton(\n          LocaleKeys.editor_heading1.tr(),\n        );\n\n        // check the heading block is inserted\n        final heading = editorState.getNodeAtPath([0, 0, 0, 0])!;\n        expect(heading.type, equals(HeadingBlockKeys.type));\n        expect(heading.level, equals(1));\n      }\n    });\n\n    testWidgets('''\n1. insert a simple table via + menu\n2. resize column\n''', (tester) async {\n      await tester.launchInAnonymousMode();\n      await tester.createNewDocumentOnMobile('simple table');\n\n      final editorState = tester.editor.getCurrentEditorState();\n      // focus on the editor\n      unawaited(\n        editorState.updateSelectionWithReason(\n          Selection.collapsed(Position(path: [0])),\n          reason: SelectionUpdateReason.uiEvent,\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      final beforeWidth = editorState.getNodeAtPath([0, 0, 0])!.columnWidth;\n\n      // find the first cell\n      {\n        final resizeHandle = find.byType(SimpleTableColumnResizeHandle).first;\n        final offset = tester.getCenter(resizeHandle);\n        final gesture = await tester.startGesture(offset, pointer: 7);\n        await tester.pumpAndSettle();\n\n        await gesture.moveBy(const Offset(100, 0));\n        await tester.pumpAndSettle();\n\n        await gesture.up();\n        await tester.pumpAndSettle();\n      }\n\n      // check the table is updated\n      final afterWidth1 = editorState.getNodeAtPath([0, 0, 0])!.columnWidth;\n      expect(afterWidth1, greaterThan(beforeWidth));\n\n      // resize back to the original width\n      {\n        final resizeHandle = find.byType(SimpleTableColumnResizeHandle).first;\n        final offset = tester.getCenter(resizeHandle);\n        final gesture = await tester.startGesture(offset, pointer: 7);\n        await tester.pumpAndSettle();\n\n        await gesture.moveBy(const Offset(-100, 0));\n        await tester.pumpAndSettle();\n\n        await gesture.up();\n        await tester.pumpAndSettle();\n      }\n\n      // check the table is updated\n      final afterWidth2 = editorState.getNodeAtPath([0, 0, 0])!.columnWidth;\n      expect(afterWidth2, equals(beforeWidth));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/document/slash_menu_test.dart",
    "content": "import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item.dart';\nimport 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart';\nimport 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  const title = 'Test Slash Menu';\n\n  group('slash menu', () {\n    testWidgets('show slash menu', (tester) async {\n      await tester.launchInAnonymousMode();\n      await tester.createPageAndShowSlashMenu(title);\n      final menuWidget = find.byType(MobileSelectionMenuWidget);\n      expect(menuWidget, findsOneWidget);\n      final items =\n          (menuWidget.evaluate().first.widget as MobileSelectionMenuWidget)\n              .items;\n      int i = 0;\n      for (final item in items) {\n        final localItem = mobileItems[i];\n        expect(item.name, localItem.name);\n        i++;\n      }\n    });\n\n    testWidgets('search by slash menu', (tester) async {\n      await tester.launchInAnonymousMode();\n      await tester.createPageAndShowSlashMenu(title);\n      const searchText = 'Heading';\n      await tester.ime.insertText(searchText);\n      final itemWidgets = find.byType(MobileSelectionMenuItemWidget);\n      int number = 0;\n      for (final item in mobileItems) {\n        if (item is MobileSelectionMenuItem) {\n          for (final childItem in item.children) {\n            if (childItem.name\n                .toLowerCase()\n                .contains(searchText.toLowerCase())) {\n              number++;\n            }\n          }\n        } else {\n          if (item.name.toLowerCase().contains(searchText.toLowerCase())) {\n            number++;\n          }\n        }\n      }\n      expect(itemWidgets, findsNWidgets(number));\n    });\n\n    testWidgets('tap to show submenu', (tester) async {\n      await tester.launchInAnonymousMode();\n      await tester.createNewDocumentOnMobile(title);\n      await tester.editor.tapLineOfEditorAt(0);\n      final listview = find.descendant(\n        of: find.byType(MobileSelectionMenuWidget),\n        matching: find.byType(ListView),\n      );\n      for (final item in mobileItems) {\n        if (item is! MobileSelectionMenuItem) continue;\n        await tester.editor.showSlashMenu();\n        await tester.scrollUntilVisible(\n          find.text(item.name),\n          50,\n          scrollable: listview,\n          duration: const Duration(milliseconds: 250),\n        );\n        await tester.tap(find.text(item.name));\n        final childrenLength = ((listview.evaluate().first.widget as ListView)\n                .childrenDelegate as SliverChildListDelegate)\n            .children\n            .length;\n        expect(childrenLength, item.children.length);\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/document/title_test.dart",
    "content": "import 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_page.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('document title:', () {\n    testWidgets('create a new page, the title should be empty', (tester) async {\n      await tester.launchInAnonymousMode();\n\n      final createPageButton = find.byKey(\n        BottomNavigationBarItemType.add.valueKey,\n      );\n      await tester.tapButton(createPageButton);\n      expect(find.byType(MobileDocumentScreen), findsOneWidget);\n\n      final title = tester.editor.findDocumentTitle('');\n      expect(title, findsOneWidget);\n      final textField = tester.widget<TextField>(title);\n      expect(textField.focusNode!.hasFocus, isTrue);\n\n      // input new name and press done button\n      const name = 'test document';\n      await tester.enterText(title, name);\n      await tester.testTextInput.receiveAction(TextInputAction.done);\n      await tester.pumpAndSettle();\n      final newTitle = tester.editor.findDocumentTitle(name);\n      expect(newTitle, findsOneWidget);\n      expect(textField.controller!.text, name);\n\n      // the document should get focus\n      final editor = tester.widget<AppFlowyEditorPage>(\n        find.byType(AppFlowyEditorPage),\n      );\n      expect(\n        editor.editorState.selection,\n        Selection.collapsed(Position(path: [0])),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/document/toolbar_test.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart';\nimport 'package:appflowy/mobile/presentation/editor/mobile_editor_screen.dart';\nimport 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  Future<void> createNeaPage(WidgetTester tester) async {\n    final createPageButton =\n        find.byKey(BottomNavigationBarItemType.add.valueKey);\n    await tester.tapButton(createPageButton);\n    expect(find.byType(MobileDocumentScreen), findsOneWidget);\n    final editor = find.byType(AppFlowyEditor);\n    expect(editor, findsOneWidget);\n  }\n\n  const testLink = 'https://appflowy.io/';\n\n  group('links', () {\n    testWidgets('insert links', (tester) async {\n      await tester.launchInAnonymousMode();\n\n      await createNeaPage(tester);\n      await tester.editor.tapLineOfEditorAt(0);\n      final editorState = tester.editor.getCurrentEditorState();\n\n      /// insert two lines of text\n      const strFirst = 'FirstLine', strSecond = 'SecondLine';\n      await tester.ime.insertText(strFirst);\n      await editorState.insertNewLine();\n      await tester.ime.insertText(strSecond);\n      final firstLine = find.text(strFirst, findRichText: true),\n          secondLine = find.text(strSecond, findRichText: true);\n      expect(firstLine, findsOneWidget);\n      expect(secondLine, findsOneWidget);\n\n      /// select the first line\n      await tester.doubleTapAt(tester.getCenter(firstLine));\n      await tester.pumpAndSettle();\n\n      /// find link button and tap it\n      final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);\n      await tester.tapButton(linkButton);\n\n      /// input the link\n      final textFormField = find.byType(TextFormField);\n      expect(textFormField, findsNWidgets(2));\n      final linkField = textFormField.last;\n      await tester.enterText(linkField, testLink);\n      await tester.pumpAndSettle();\n      await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_earth_m));\n\n      /// apply the link\n      await tester.tapButton(find.text(LocaleKeys.button_done.tr()));\n\n      /// do it again\n      /// select the second line\n      await tester.doubleTapAt(tester.getCenter(secondLine));\n      await tester.pumpAndSettle();\n      await tester.tapButton(linkButton);\n      await tester.enterText(linkField, testLink);\n      await tester.pumpAndSettle();\n      await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_earth_m));\n      await tester.tapButton(find.text(LocaleKeys.button_done.tr()));\n\n      final firstNode = editorState.getNodeAtPath([0]);\n      final secondNode = editorState.getNodeAtPath([1]);\n\n      Map commonDeltaJson(String insert) => {\n            'insert': insert,\n            'attributes': {'href': testLink, 'is_page_link': false},\n          };\n\n      expect(\n        firstNode?.delta?.toJson(),\n        [commonDeltaJson(strFirst)],\n      );\n      expect(\n        secondNode?.delta?.toJson(),\n        [commonDeltaJson(strSecond)],\n      );\n    });\n\n    testWidgets('change a link', (tester) async {\n      await tester.launchInAnonymousMode();\n\n      await createNeaPage(tester);\n      await tester.editor.tapLineOfEditorAt(0);\n      final editorState = tester.editor.getCurrentEditorState();\n      const testText = 'TestText';\n      await tester.ime.insertText(testText);\n      final textFinder = find.text(testText, findRichText: true);\n\n      /// select the first line\n      await tester.doubleTapAt(tester.getCenter(textFinder));\n      await tester.pumpAndSettle();\n\n      /// find link button and tap it\n      final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m);\n      await tester.tapButton(linkButton);\n\n      /// input the link\n      final textFormField = find.byType(TextFormField);\n      expect(textFormField, findsNWidgets(2));\n      final linkField = textFormField.last;\n      await tester.enterText(linkField, testLink);\n      await tester.pumpAndSettle();\n      await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_earth_m));\n\n      /// apply the link\n      await tester.tapButton(find.text(LocaleKeys.button_done.tr()));\n\n      /// show edit link menu\n      await tester.longPress(textFinder);\n      await tester.pumpAndSettle();\n      final linkEditMenu = find.byType(MobileBottomSheetEditLinkWidget);\n      expect(linkEditMenu, findsOneWidget);\n\n      /// remove the link\n      await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_unlink_m));\n      final node = editorState.getNodeAtPath([0]);\n      expect(\n        node?.delta?.toJson(),\n        [\n          {'insert': testText},\n        ],\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('create new page in home page:', () {\n    testWidgets('create document', (tester) async {\n      await tester.launchInAnonymousMode();\n\n      // tap the create page button\n      final createPageButton = find.byWidgetPredicate(\n        (widget) =>\n            widget is FlowySvg &&\n            widget.svg.path == FlowySvgs.m_home_add_m.path,\n      );\n      await tester.tapButton(createPageButton);\n      await tester.pumpAndSettle();\n      expect(find.byType(MobileDocumentScreen), findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/search/search_test.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/home/home.dart';\nimport 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';\nimport 'package:appflowy/mobile/presentation/search/mobile_search_ask_ai_entrance.dart';\nimport 'package:appflowy/mobile/presentation/search/mobile_search_page.dart';\nimport 'package:appflowy/mobile/presentation/search/mobile_search_result.dart';\nimport 'package:appflowy/mobile/presentation/search/mobile_search_textfield.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('Search test', () {\n    testWidgets('tap to search page', (tester) async {\n      await tester.launchInAnonymousMode();\n      final searchButton = find.byFlowySvg(FlowySvgs.m_home_search_icon_m);\n      await tester.tapButton(searchButton);\n\n      ///check for UI element\n      expect(find.byType(MobileSearchAskAiEntrance), findsNothing);\n      expect(find.byType(MobileSearchRecentList), findsOneWidget);\n      expect(find.byType(MobileSearchResultList), findsNothing);\n\n      /// search for something\n      final searchTextField = find.descendant(\n        of: find.byType(MobileSearchTextfield),\n        matching: find.byType(TextFormField),\n      );\n      final query = '$gettingStarted searching';\n      await tester.enterText(searchTextField, query);\n      await tester.pumpAndSettle();\n\n      expect(find.byType(MobileSearchRecentList), findsNothing);\n      expect(find.byType(MobileSearchResultList), findsOneWidget);\n      expect(\n        find.text(LocaleKeys.search_noResultForSearching.tr()),\n        findsOneWidget,\n      );\n\n      /// clear text\n      final clearButton = find.byFlowySvg(FlowySvgs.clear_s);\n      await tester.tapButton(clearButton);\n      expect(find.byType(MobileSearchRecentList), findsOneWidget);\n      expect(find.byType(MobileSearchResultList), findsNothing);\n\n      /// tap cancel button\n      final cancelButton = find.text(LocaleKeys.button_cancel.tr());\n      expect(cancelButton, findsNothing);\n      await tester.enterText(searchTextField, query);\n      await tester.pumpAndSettle();\n      expect(cancelButton, findsOneWidget);\n      await tester.tapButton(cancelButton);\n      expect(cancelButton, findsNothing);\n    });\n  });\n\n  testWidgets('tap to search page and back to home', (tester) async {\n    await tester.launchInAnonymousMode();\n\n    /// go to search page\n    final searchButton = find.byFlowySvg(FlowySvgs.m_home_search_icon_m);\n    expect(find.byType(MobileSearchScreen), findsNothing);\n    await tester.tapButton(searchButton);\n    expect(find.byType(MobileSearchScreen), findsOneWidget);\n\n    /// back to home page\n    final backButton = find.byFlowySvg(FlowySvgs.search_page_arrow_left_m);\n    expect(backButton, findsOneWidget);\n    expect(find.byType(MobileHomeScreen), findsNothing);\n    await tester.tapButton(backButton);\n    expect(find.byType(MobileHomeScreen), findsOneWidget);\n\n    /// go to notification page\n    final notificationButton = find.byFlowySvg(FlowySvgs.m_home_notification_m);\n    expect(find.byType(MobileNotificationsScreenV2), findsNothing);\n    await tester.tapButton(notificationButton);\n    expect(find.byType(MobileNotificationsScreenV2), findsOneWidget);\n\n    /// go to search page\n    await tester.tapButton(searchButton);\n    expect(find.byType(MobileNotificationsScreenV2), findsNothing);\n    expect(find.byType(MobileSearchScreen), findsOneWidget);\n    await tester.tapButton(backButton);\n    expect(find.byType(MobileNotificationsScreenV2), findsOneWidget);\n    expect(find.byType(MobileSearchScreen), findsNothing);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/settings/default_text_direction_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';\nimport 'package:appflowy/mobile/presentation/home/setting/settings_popup_menu.dart';\nimport 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  testWidgets('Change default text direction', (tester) async {\n    await tester.launchInAnonymousMode();\n\n    /// tap [Setting] button\n    await tester.tapButton(find.byType(HomePageSettingsPopupMenu));\n    await tester\n        .tapButton(find.text(LocaleKeys.settings_popupMenuItem_settings.tr()));\n\n    /// tap [Default Text Direction]\n    await tester.tapButton(\n      find.text(LocaleKeys.settings_appearance_textDirection_label.tr()),\n    );\n\n    /// there are 3 items: LTR-RTL-AUTO\n    final bottomSheet = find.ancestor(\n      of: find.byType(FlowyOptionTile),\n      matching: find.byType(SafeArea),\n    );\n    final items = find.descendant(\n      of: bottomSheet,\n      matching: find.byType(FlowyOptionTile),\n    );\n    expect(items, findsNWidgets(3));\n\n    /// select [Auto]\n    await tester.tapButton(items.last);\n    expect(\n      find.text(LocaleKeys.settings_appearance_textDirection_auto.tr()),\n      findsOneWidget,\n    );\n\n    /// go back home\n    await tester.tapButton(find.byType(AppBarImmersiveBackButton));\n\n    /// create new page\n    final createPageButton =\n        find.byKey(BottomNavigationBarItemType.add.valueKey);\n    await tester.tapButton(createPageButton);\n\n    final editorState = tester.editor.getCurrentEditorState();\n    // focus on the editor\n    await tester.editor.tapLineOfEditorAt(0);\n\n    const testEnglish = 'English', testArabic = 'إنجليزي';\n\n    /// insert [testEnglish]\n    await editorState.insertTextAtCurrentSelection(testEnglish);\n    await tester.pumpAndSettle();\n    await editorState.insertNewLine(position: editorState.selection!.end);\n    await tester.pumpAndSettle();\n\n    /// insert [testArabic]\n    await editorState.insertTextAtCurrentSelection(testArabic);\n    await tester.pumpAndSettle();\n    final testEnglishFinder = find.text(testEnglish, findRichText: true),\n        testArabicFinder = find.text(testArabic, findRichText: true);\n    final testEnglishRenderBox =\n            testEnglishFinder.evaluate().first.renderObject as RenderBox,\n        testArabicRenderBox =\n            testArabicFinder.evaluate().first.renderObject as RenderBox;\n    final englishPosition = testEnglishRenderBox.localToGlobal(Offset.zero),\n        arabicPosition = testArabicRenderBox.localToGlobal(Offset.zero);\n    expect(englishPosition.dx > arabicPosition.dx, true);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/settings/scale_factor_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/home/setting/settings_popup_menu.dart';\nimport 'package:appflowy/workspace/presentation/home/hotkeys.dart';\nimport 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  testWidgets('test for change scale factor', (tester) async {\n    await tester.launchInAnonymousMode();\n\n    /// tap [Setting] button\n    await tester.tapButton(find.byType(HomePageSettingsPopupMenu));\n    await tester\n        .tapButton(find.text(LocaleKeys.settings_popupMenuItem_settings.tr()));\n\n    /// tap [Font Scale Factor]\n    await tester.tapButton(\n      find.text(LocaleKeys.settings_appearance_fontScaleFactor.tr()),\n    );\n\n    /// drag slider\n    final slider = find.descendant(\n      of: find.byType(FontSizeStepper),\n      matching: find.byType(Slider),\n    );\n    await tester.slideToValue(slider, 0.8);\n    expect(appflowyScaleFactor, 0.8);\n\n    await tester.slideToValue(slider, 0.9);\n    expect(appflowyScaleFactor, 0.9);\n\n    await tester.slideToValue(slider, 1.0);\n    expect(appflowyScaleFactor, 1.0);\n\n    await tester.slideToValue(slider, 1.1);\n    expect(appflowyScaleFactor, 1.1);\n\n    await tester.slideToValue(slider, 1.2);\n    expect(appflowyScaleFactor, 1.2);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart",
    "content": "import 'package:appflowy/mobile/presentation/home/home.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport '../../shared/util.dart';\n\nvoid main() {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  group('anonymous sign in on mobile:', () {\n    testWidgets('anon user and then sign in', (tester) async {\n      await tester.launchInAnonymousMode();\n\n      // expect to see the home page\n      expect(find.byType(MobileHomeScreen), findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/mobile_runner_1.dart",
    "content": "import 'package:appflowy_backend/log.dart';\nimport 'package:integration_test/integration_test.dart';\n\nimport 'mobile/document/document_test_runner.dart' as document_test_runner;\nimport 'mobile/home_page/create_new_page_test.dart' as create_new_page_test;\nimport 'mobile/settings/default_text_direction_test.dart'\n    as default_text_direction_test;\nimport 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test;\n\nFuture<void> main() async {\n  Log.shared.disableLog = true;\n\n  await runIntegration1OnMobile();\n}\n\nFuture<void> runIntegration1OnMobile() async {\n  IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n  anonymous_sign_in_test.main();\n  create_new_page_test.main();\n  document_test_runner.main();\n  default_text_direction_test.main();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/runner.dart",
    "content": "import 'dart:io';\n\nimport 'desktop_runner_1.dart';\nimport 'desktop_runner_2.dart';\nimport 'desktop_runner_3.dart';\nimport 'desktop_runner_4.dart';\nimport 'desktop_runner_5.dart';\nimport 'desktop_runner_6.dart';\nimport 'desktop_runner_7.dart';\nimport 'desktop_runner_8.dart';\nimport 'desktop_runner_9.dart';\nimport 'mobile_runner_1.dart';\n\n/// The main task runner for all integration tests in AppFlowy.\n///\n/// Having a single entrypoint for integration tests is necessary due to an\n/// [issue caused by switching files with integration testing](https://github.com/flutter/flutter/issues/101031).\n/// If flutter/flutter#101031 is resolved, this file can be removed completely.\n/// Once removed, the integration_test.yaml must be updated to exclude this as\n/// as the test target.\nFuture<void> main() async {\n  if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) {\n    await runIntegration1OnDesktop();\n    await runIntegration2OnDesktop();\n    await runIntegration3OnDesktop();\n    await runIntegration4OnDesktop();\n    await runIntegration5OnDesktop();\n    await runIntegration6OnDesktop();\n    await runIntegration7OnDesktop();\n    await runIntegration8OnDesktop();\n    await runIntegration9OnDesktop();\n  } else if (Platform.isIOS || Platform.isAndroid) {\n    await runIntegration1OnMobile();\n  } else {\n    throw Exception('Unsupported platform');\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/ai_test_op.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/chat_page/chat_content_page.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../test/util.dart';\nimport 'util.dart';\n\nextension AppFlowyAITest on WidgetTester {\n  Future<void> selectAIWriter(AiWriterCommand command) async {\n    await tapButton(find.byType(AiWriterToolbarActionList));\n    await tapButton(find.text(command.i18n));\n    await pumpAndSettle();\n  }\n\n  Future<void> selectModel(String modelName) async {\n    await tapButton(find.byType(SelectModelMenu));\n    await tapButton(find.text(modelName));\n    await pumpAndSettle();\n  }\n\n  Future<void> enterTextInPromptTextField(String text) async {\n    // Wait for the text field to be visible\n    await pumpAndSettle();\n\n    // Find the ExtendedTextField widget\n    final textField = find.descendant(\n      of: find.byType(PromptInputTextField),\n      matching: find.byType(TextField),\n    );\n    expect(textField, findsOneWidget, reason: 'ExtendedTextField not found');\n\n    final widget = element(textField).widget as TextField;\n    expect(widget.enabled, isTrue, reason: 'TextField is not enabled');\n\n    await tap(textField);\n\n    testTextInput.enterText(text);\n    await pumpAndSettle(const Duration(milliseconds: 300));\n  }\n\n  ChatBloc getCurrentChatBloc() {\n    return element(find.byType(ChatContentPage)).read<ChatBloc>();\n  }\n\n  Future<void> loadDefaultMessages(List<Message> messages) async {\n    final chatBloc = getCurrentChatBloc();\n    chatBloc.add(ChatEvent.didLoadLatestMessages(messages));\n    await blocResponseFuture();\n  }\n\n  Future<void> sendUserMessage(Message message) async {\n    final chatBloc = getCurrentChatBloc();\n    // using received message to simulate the user message\n    chatBloc.add(ChatEvent.receiveMessage(message));\n    await blocResponseFuture();\n  }\n\n  Future<void> receiveAIMessage(Message message) async {\n    final chatBloc = getCurrentChatBloc();\n    chatBloc.add(ChatEvent.receiveMessage(message));\n    await blocResponseFuture();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/auth_operation.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/account.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'util.dart';\n\nextension AppFlowyAuthTest on WidgetTester {\n  Future<void> tapGoogleLoginInButton({bool pumpAndSettle = true}) async {\n    await tapButton(\n      find.byKey(signInWithGoogleButtonKey),\n      pumpAndSettle: pumpAndSettle,\n    );\n  }\n\n  /// Requires being on the SettingsPage.account of the SettingsDialog\n  Future<void> logout() async {\n    final scrollable = find.findSettingsScrollable();\n    await scrollUntilVisible(\n      find.byType(AccountSignInOutButton),\n      100,\n      scrollable: scrollable,\n    );\n\n    await tapButton(find.byType(AccountSignInOutButton));\n\n    expectToSeeText(LocaleKeys.button_yes.tr());\n    await tapButtonWithName(LocaleKeys.button_yes.tr());\n  }\n\n  Future<void> tapSignInAsGuest() async {\n    await tapButton(find.byType(SignInAnonymousButtonV2));\n  }\n\n  void expectToSeeGoogleLoginButton() {\n    expect(find.byKey(signInWithGoogleButtonKey), findsOneWidget);\n  }\n\n  void assertSwitchValue(Finder finder, bool value) {\n    final Switch switchWidget = widget(finder);\n    final isSwitched = switchWidget.value;\n    assert(isSwitched == value);\n  }\n\n  void assertToggleValue(Finder finder, bool value) {\n    final Toggle switchWidget = widget(finder);\n    final isSwitched = switchWidget.value;\n    assert(isSwitched == value);\n  }\n\n  void assertAppFlowyCloudEnableSyncSwitchValue(bool value) {\n    assertToggleValue(\n      find.descendant(\n        of: find.byType(AppFlowyCloudEnableSync),\n        matching: find.byWidgetPredicate((widget) => widget is Toggle),\n      ),\n      value,\n    );\n  }\n\n  Future<void> toggleEnableSync(Type syncButton) async {\n    final finder = find.descendant(\n      of: find.byType(syncButton),\n      matching: find.byWidgetPredicate((widget) => widget is Toggle),\n    );\n\n    await tapButton(finder);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/base.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:appflowy/ai/service/appflowy_ai_service.dart';\nimport 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/env/cloud_env_test.dart';\nimport 'package:appflowy/startup/entry_point.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/presentation/presentation.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';\nimport 'package:appflowy/workspace/application/settings/prelude.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'mock/mock_ai.dart';\n\nclass FlowyTestContext {\n  FlowyTestContext({required this.applicationDataDirectory});\n\n  final String applicationDataDirectory;\n}\n\nextension AppFlowyTestBase on WidgetTester {\n  Future<FlowyTestContext> initializeAppFlowy({\n    // use to append after the application data directory\n    String? pathExtension,\n    // use to specify the application data directory, if not specified, a temporary directory will be used.\n    String? dataDirectory,\n    Size windowSize = const Size(1600, 1200),\n    String? email,\n    AuthenticatorType? cloudType,\n    AIRepository Function()? aiRepositoryBuilder,\n  }) async {\n    if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {\n      await binding.setSurfaceSize(windowSize);\n    }\n    //cloudType = AuthenticatorType.appflowyCloudDevelop;\n\n    mockHotKeyManagerHandlers();\n    final applicationDataDirectory = dataDirectory ??\n        await mockApplicationDataStorage(\n          pathExtension: pathExtension,\n        );\n\n    await FlowyRunner.run(\n      AppFlowyApplication(),\n      IntegrationMode.integrationTest,\n      rustEnvsBuilder: () => _buildRustEnvs(cloudType),\n      didInitGetItCallback: () => _initializeCloudServices(\n        cloudType: cloudType,\n        email: email,\n        aiRepositoryBuilder: aiRepositoryBuilder,\n      ),\n    );\n\n    await waitUntilSignInPageShow();\n    return FlowyTestContext(\n      applicationDataDirectory: applicationDataDirectory,\n    );\n  }\n\n  Map<String, String> _buildRustEnvs(AuthenticatorType? cloudType) {\n    final rustEnvs = <String, String>{};\n    if (cloudType != null) {\n      switch (cloudType) {\n        case AuthenticatorType.local:\n          break;\n        case AuthenticatorType.appflowyCloudSelfHost:\n        case AuthenticatorType.appflowyCloudDevelop:\n          rustEnvs[\"GOTRUE_ADMIN_EMAIL\"] = \"admin@example.com\";\n          rustEnvs[\"GOTRUE_ADMIN_PASSWORD\"] = \"password\";\n          break;\n        default:\n          throw Exception(\"Unsupported cloud type: $cloudType\");\n      }\n    }\n    return rustEnvs;\n  }\n\n  Future<void> _initializeCloudServices({\n    required AuthenticatorType? cloudType,\n    String? email,\n    AIRepository Function()? aiRepositoryBuilder,\n  }) async {\n    if (cloudType == null) return;\n\n    switch (cloudType) {\n      case AuthenticatorType.local:\n        await useLocalServer();\n        break;\n      case AuthenticatorType.appflowyCloudSelfHost:\n        await _setupAppFlowyCloud(\n          useLocal: false,\n          email: email,\n          aiRepositoryBuilder: aiRepositoryBuilder,\n        );\n        break;\n      case AuthenticatorType.appflowyCloudDevelop:\n        await _setupAppFlowyCloud(\n          useLocal: integrationMode().isDevelop,\n          email: email,\n          aiRepositoryBuilder: aiRepositoryBuilder,\n        );\n        break;\n      default:\n        throw Exception(\"Unsupported cloud type: $cloudType\");\n    }\n  }\n\n  Future<void> _setupAppFlowyCloud({\n    required bool useLocal,\n    String? email,\n    AIRepository Function()? aiRepositoryBuilder,\n  }) async {\n    if (useLocal) {\n      await useAppFlowyCloudDevelop(\"http://localhost\");\n    } else {\n      await useSelfHostedAppFlowyCloud(TestEnv.afCloudUrl);\n    }\n\n    getIt.unregister<AuthService>();\n    getIt.unregister<AIRepository>();\n\n    getIt.registerFactory<AuthService>(\n      () => AppFlowyCloudMockAuthService(email: email),\n    );\n    getIt.registerFactory<AIRepository>(\n      aiRepositoryBuilder ?? () => MockAIRepository(),\n    );\n  }\n\n  void mockHotKeyManagerHandlers() {\n    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n        .setMockMethodCallHandler(const MethodChannel('hotkey_manager'),\n            (MethodCall methodCall) async {\n      if (methodCall.method == 'unregisterAll') {\n        // do nothing\n      }\n      return;\n    });\n  }\n\n  Future<void> waitUntilSignInPageShow() async {\n    if (isAuthEnabled || UniversalPlatform.isMobile) {\n      final finder = find.byType(SignInAnonymousButtonV2);\n      await pumpUntilFound(finder, timeout: const Duration(seconds: 30));\n      expect(finder, findsOneWidget);\n    } else {\n      final finder = find.byType(GoButton);\n      await pumpUntilFound(finder);\n      expect(finder, findsOneWidget);\n    }\n  }\n\n  Future<void> waitForSeconds(int seconds) async {\n    await Future.delayed(Duration(seconds: seconds), () {});\n  }\n\n  Future<void> pumpUntilFound(\n    Finder finder, {\n    Duration timeout = const Duration(seconds: 10),\n    Duration pumpInterval = const Duration(\n      milliseconds: 50,\n    ), // Interval between pumps\n  }) async {\n    bool timerDone = false;\n    final timer = Timer(timeout, () => timerDone = true);\n    while (!timerDone) {\n      await pump(pumpInterval); // Pump with an interval\n      if (any(finder)) {\n        break;\n      }\n    }\n    timer.cancel();\n  }\n\n  Future<void> pumpUntilNotFound(\n    Finder finder, {\n    Duration timeout = const Duration(seconds: 10),\n    Duration pumpInterval = const Duration(\n      milliseconds: 50,\n    ), // Interval between pumps\n  }) async {\n    bool timerDone = false;\n    final timer = Timer(timeout, () => timerDone = true);\n    while (!timerDone) {\n      await pump(pumpInterval); // Pump with an interval\n      if (!any(finder)) {\n        break;\n      }\n    }\n    timer.cancel();\n  }\n\n  Future<void> tapButton(\n    Finder finder, {\n    int buttons = kPrimaryButton,\n    bool warnIfMissed = false,\n    int milliseconds = 500,\n    bool pumpAndSettle = true,\n  }) async {\n    await tap(finder, buttons: buttons, warnIfMissed: warnIfMissed);\n\n    if (pumpAndSettle) {\n      await this.pumpAndSettle(\n        Duration(milliseconds: milliseconds),\n        EnginePhase.sendSemanticsUpdate,\n        const Duration(seconds: 15),\n      );\n    }\n  }\n\n  Future<void> tapDown(\n    Finder finder, {\n    int? pointer,\n    int buttons = kPrimaryButton,\n    PointerDeviceKind kind = PointerDeviceKind.touch,\n    bool pumpAndSettle = true,\n    int milliseconds = 500,\n  }) async {\n    final location = getCenter(finder);\n    final TestGesture gesture = await startGesture(\n      location,\n      pointer: pointer,\n      buttons: buttons,\n      kind: kind,\n    );\n    await gesture.cancel();\n    await gesture.down(location);\n    await gesture.cancel();\n    if (pumpAndSettle) {\n      await this.pumpAndSettle(\n        Duration(milliseconds: milliseconds),\n        EnginePhase.sendSemanticsUpdate,\n        const Duration(seconds: 15),\n      );\n    }\n  }\n\n  Future<void> tapButtonWithName(\n    String tr, {\n    int milliseconds = 500,\n    bool pumpAndSettle = true,\n  }) async {\n    Finder button = find.text(tr, findRichText: true, skipOffstage: false);\n    if (button.evaluate().isEmpty) {\n      button = find.byWidgetPredicate(\n        (widget) => widget is FlowyText && widget.text == tr,\n      );\n    }\n    await tapButton(\n      button,\n      milliseconds: milliseconds,\n      pumpAndSettle: pumpAndSettle,\n    );\n  }\n\n  Future<void> doubleTapAt(\n    Offset location, {\n    int? pointer,\n    int buttons = kPrimaryButton,\n    int milliseconds = 500,\n  }) async {\n    await tapAt(location, pointer: pointer, buttons: buttons);\n    await pump(kDoubleTapMinTime);\n    await tapAt(location, pointer: pointer, buttons: buttons);\n    await pumpAndSettle(Duration(milliseconds: milliseconds));\n  }\n\n  Future<void> wait(int milliseconds) async {\n    await pumpAndSettle(Duration(milliseconds: milliseconds));\n  }\n\n  Future<void> slideToValue(\n    Finder slider,\n    double value, {\n    double paddingOffset = 24.0,\n  }) async {\n    final sliderWidget = slider.evaluate().first.widget as Slider;\n    final range = sliderWidget.max - sliderWidget.min;\n    final initialRate = (value - sliderWidget.min) / range;\n    final totalWidth = getSize(slider).width - (2 * paddingOffset);\n    final zeroPoint = getTopLeft(slider) +\n        Offset(\n          paddingOffset + initialRate * totalWidth,\n          getSize(slider).height / 2,\n        );\n    final calculatedOffset = value * (totalWidth / 100);\n    await dragFrom(zeroPoint, Offset(calculatedOffset, 0));\n    await pumpAndSettle();\n  }\n}\n\nextension AppFlowyFinderTestBase on CommonFinders {\n  Finder findTextInFlowyText(String text) {\n    return find.byWidgetPredicate(\n      (widget) => widget is FlowyText && widget.text == text,\n    );\n  }\n\n  Finder findFlowyTooltip(String richMessage, {bool skipOffstage = true}) {\n    return byWidgetPredicate(\n      (widget) =>\n          widget is FlowyTooltip &&\n          widget.richMessage != null &&\n          widget.richMessage!.toPlainText().contains(richMessage),\n      skipOffstage: skipOffstage,\n    );\n  }\n}\n\nFuture<String> mockApplicationDataStorage({\n  // use to append after the application data directory\n  String? pathExtension,\n}) async {\n  final dir = await getTemporaryDirectory();\n\n  // Use a random uuid to avoid conflict.\n  String path = p.join(dir.path, 'appflowy_integration_test', uuid());\n  if (pathExtension != null && pathExtension.isNotEmpty) {\n    path = '$path/$pathExtension';\n  }\n  final directory = Directory(path);\n  if (!directory.existsSync()) {\n    await directory.create(recursive: true);\n  }\n\n  MockApplicationDataStorage.initialPath = directory.path;\n\n  return directory.path;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/common_operations.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart';\nimport 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart';\nimport 'package:appflowy/plugins/shared/share/share_button.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/shared/text_field/text_filed_with_metric_lines.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/presentation/screens/screens.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';\nimport 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart';\nimport 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart';\nimport 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/buttons/primary_button.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'emoji.dart';\nimport 'util.dart';\n\nextension CommonOperations on WidgetTester {\n  /// Tap the GetStart button on the launch page.\n  Future<void> tapAnonymousSignInButton() async {\n    // local version\n    final goButton = find.byType(GoButton);\n    if (goButton.evaluate().isNotEmpty) {\n      await tapButton(goButton);\n    } else {\n      // cloud version\n      final anonymousButton = find.byType(SignInAnonymousButtonV2);\n      await tapButton(anonymousButton, warnIfMissed: true);\n    }\n\n    await pumpAndSettle(const Duration(milliseconds: 200));\n  }\n\n  Future<void> tapContinousAnotherWay() async {\n    // local version\n    await tapButtonWithName(LocaleKeys.signIn_continueAnotherWay.tr());\n    if (Platform.isWindows) {\n      await pumpAndSettle(const Duration(milliseconds: 200));\n    }\n  }\n\n  /// Tap the + button on the home page.\n  Future<void> tapAddViewButton({\n    String name = gettingStarted,\n    ViewLayoutPB layout = ViewLayoutPB.Document,\n  }) async {\n    await hoverOnPageName(\n      name,\n      onHover: () async {\n        final addButton = find.byType(ViewAddButton);\n        await tapButton(addButton);\n      },\n    );\n  }\n\n  /// Tap the 'New Page' Button on the sidebar.\n  Future<void> tapNewPageButton() async {\n    final newPageButton = find.byType(SidebarNewPageButton);\n    await tapButton(newPageButton);\n  }\n\n  /// Tap the import button.\n  ///\n  /// Must call [tapAddViewButton] first.\n  Future<void> tapImportButton() async {\n    await tapButtonWithName(LocaleKeys.moreAction_import.tr());\n  }\n\n  /// Tap the import from text & markdown button.\n  ///\n  /// Must call [tapImportButton] first.\n  Future<void> tapTextAndMarkdownButton() async {\n    await tapButtonWithName(LocaleKeys.importPanel_textAndMarkdown.tr());\n  }\n\n  /// Tap the LanguageSelectorOnWelcomePage widget on the launch page.\n  Future<void> tapLanguageSelectorOnWelcomePage() async {\n    final languageSelector = find.byType(LanguageSelectorOnWelcomePage);\n    await tapButton(languageSelector);\n  }\n\n  /// Tap languageItem on LanguageItemsListView.\n  ///\n  /// [scrollDelta] is the distance to scroll the ListView.\n  /// Default value is 100\n  ///\n  /// If it is positive -> scroll down.\n  ///\n  /// If it is negative -> scroll up.\n  Future<void> tapLanguageItem({\n    required String languageCode,\n    String? countryCode,\n    double? scrollDelta,\n  }) async {\n    final languageItemsListView = find.descendant(\n      of: find.byType(ListView),\n      matching: find.byType(Scrollable),\n    );\n\n    final languageItem = find.byWidgetPredicate(\n      (widget) =>\n          widget is LanguageItem &&\n          widget.locale.languageCode == languageCode &&\n          widget.locale.countryCode == countryCode,\n    );\n\n    // scroll the ListView until zHCNLanguageItem shows on the screen.\n    await scrollUntilVisible(\n      languageItem,\n      scrollDelta ?? 100,\n      scrollable: languageItemsListView,\n      // maxHeight of LanguageItemsListView\n      maxScrolls: 400,\n    );\n\n    try {\n      await tapButton(languageItem);\n    } on FlutterError catch (e) {\n      Log.warn('tapLanguageItem error: $e');\n    }\n  }\n\n  /// Hover on the widget.\n  Future<void> hoverOnWidget(\n    Finder finder, {\n    Offset? offset,\n    Future<void> Function()? onHover,\n    bool removePointer = true,\n  }) async {\n    try {\n      final gesture = await createGesture(kind: PointerDeviceKind.mouse);\n      await gesture.addPointer(location: offset ?? getCenter(finder));\n      await pumpAndSettle();\n      await onHover?.call();\n      await gesture.removePointer();\n    } catch (err) {\n      Log.error('hoverOnWidget error: $err');\n    }\n  }\n\n  /// Hover on the page name.\n  Future<void> hoverOnPageName(\n    String name, {\n    ViewLayoutPB layout = ViewLayoutPB.Document,\n    Future<void> Function()? onHover,\n    bool useLast = true,\n  }) async {\n    final pageNames = findPageName(name, layout: layout);\n    if (useLast) {\n      await hoverOnWidget(pageNames.last, onHover: onHover);\n    } else {\n      await hoverOnWidget(pageNames.first, onHover: onHover);\n    }\n  }\n\n  /// Right click on the page name.\n  Future<void> rightClickOnPageName(\n    String name, {\n    ViewLayoutPB layout = ViewLayoutPB.Document,\n  }) async {\n    final page = findPageName(name, layout: layout);\n    await hoverOnPageName(\n      name,\n      onHover: () async {\n        await tap(page, buttons: kSecondaryMouseButton);\n        await pumpAndSettle();\n      },\n    );\n  }\n\n  /// open the page with given name.\n  Future<void> openPage(\n    String name, {\n    ViewLayoutPB layout = ViewLayoutPB.Document,\n  }) async {\n    final finder = findPageName(name, layout: layout);\n    expect(finder, findsOneWidget);\n    await tapButton(finder);\n  }\n\n  /// Tap the ... button beside the page name.\n  ///\n  /// Must call [hoverOnPageName] first.\n  Future<void> tapPageOptionButton() async {\n    final optionButton = find.descendant(\n      of: find.byType(ViewMoreActionPopover),\n      matching: find.byFlowySvg(FlowySvgs.workspace_three_dots_s),\n    );\n    await tapButton(optionButton);\n  }\n\n  /// Tap the delete page button.\n  Future<void> tapDeletePageButton() async {\n    await tapPageOptionButton();\n    await tapButtonWithName(ViewMoreActionType.delete.name);\n  }\n\n  /// Tap the rename page button.\n  Future<void> tapRenamePageButton() async {\n    await tapPageOptionButton();\n    await tapButtonWithName(ViewMoreActionType.rename.name);\n  }\n\n  /// Tap the favorite page button\n  Future<void> tapFavoritePageButton() async {\n    await tapPageOptionButton();\n    await tapButtonWithName(ViewMoreActionType.favorite.name);\n  }\n\n  /// Tap the unfavorite page button\n  Future<void> tapUnfavoritePageButton() async {\n    await tapPageOptionButton();\n    await tapButtonWithName(ViewMoreActionType.unFavorite.name);\n  }\n\n  /// Tap the Open in a new tab button\n  Future<void> tapOpenInTabButton() async {\n    await tapPageOptionButton();\n    await tapButtonWithName(ViewMoreActionType.openInNewTab.name);\n  }\n\n  /// Rename the page.\n  Future<void> renamePage(String name) async {\n    await tapRenamePageButton();\n    await enterText(find.byType(AFTextField), name);\n    await tapButton(find.text(LocaleKeys.button_confirm.tr()));\n  }\n\n  Future<void> tapTrashButton() async {\n    await tap(find.byType(SidebarTrashButton));\n  }\n\n  Future<void> tapOKButton() async {\n    final okButton = find.byWidgetPredicate(\n      (widget) =>\n          widget is PrimaryTextButton &&\n          widget.label == LocaleKeys.button_ok.tr(),\n    );\n    await tapButton(okButton);\n  }\n\n  /// Expand or collapse the page.\n  Future<void> expandOrCollapsePage({\n    required String pageName,\n    required ViewLayoutPB layout,\n  }) async {\n    final page = findPageName(pageName, layout: layout);\n    await hoverOnWidget(page);\n    final expandButton = find.descendant(\n      of: page,\n      matching: find.byType(ViewItemDefaultLeftIcon),\n    );\n    await tapButton(expandButton.first);\n  }\n\n  /// Tap the restore button.\n  ///\n  /// the restore button will show after the current page is deleted.\n  Future<void> tapRestoreButton() async {\n    final restoreButton = find.textContaining(\n      LocaleKeys.deletePagePrompt_restore.tr(),\n    );\n    await tapButton(restoreButton);\n  }\n\n  /// Tap the delete permanently button.\n  ///\n  /// the delete permanently button will show after the current page is deleted.\n  Future<void> tapDeletePermanentlyButton() async {\n    final deleteButton = find.textContaining(\n      LocaleKeys.deletePagePrompt_deletePermanent.tr(),\n    );\n    await tapButton(deleteButton);\n    await tap(find.text(LocaleKeys.button_delete.tr()));\n    await pumpAndSettle();\n  }\n\n  /// Tap the share button above the document page.\n  Future<void> tapShareButton() async {\n    final shareButton = find.byWidgetPredicate(\n      (widget) => widget is ShareButton,\n    );\n    await tapButton(shareButton);\n  }\n\n  // open the share menu and then click the publish tab\n  Future<void> openPublishMenu() async {\n    await tapShareButton();\n    final publishButton = find.textContaining(\n      LocaleKeys.shareAction_publishTab.tr(),\n    );\n    await tapButton(publishButton);\n  }\n\n  /// Tap the export markdown button\n  ///\n  /// Must call [tapShareButton] first.\n  Future<void> tapMarkdownButton() async {\n    final markdownButton = find.textContaining(\n      LocaleKeys.shareAction_markdown.tr(),\n    );\n    await tapButton(markdownButton);\n  }\n\n  Future<void> createNewPageWithNameUnderParent({\n    String? name,\n    ViewLayoutPB layout = ViewLayoutPB.Document,\n    String? parentName,\n    bool openAfterCreated = true,\n  }) async {\n    // create a new page\n    await tapAddViewButton(name: parentName ?? gettingStarted, layout: layout);\n    await tapButtonWithName(layout.menuName);\n    final settingsOrFailure = await getIt<KeyValueStorage>().getWithFormat(\n      KVKeys.showRenameDialogWhenCreatingNewFile,\n      (value) => bool.parse(value),\n    );\n    final showRenameDialog = settingsOrFailure ?? false;\n    if (showRenameDialog) {\n      await tapButton(find.text(LocaleKeys.button_confirm.tr()));\n    }\n    await pumpAndSettle();\n\n    // hover on it and change it's name\n    if (name != null) {\n      await hoverOnPageName(\n        layout.defaultName,\n        layout: layout,\n        onHover: () async {\n          await renamePage(name);\n          await pumpAndSettle();\n        },\n      );\n      await pumpAndSettle();\n    }\n\n    // open the page after created\n    if (openAfterCreated) {\n      await openPage(\n        // if the name is null, use the default name\n        name ?? layout.defaultName,\n        layout: layout,\n      );\n      await pumpAndSettle();\n    }\n  }\n\n  Future<void> createOpenRenameDocumentUnderParent({\n    required String name,\n    String? parentName,\n  }) async {\n    // create a new page\n    await tapAddViewButton(name: parentName ?? gettingStarted);\n    await tapButtonWithName(ViewLayoutPB.Document.menuName);\n    final settingsOrFailure = await getIt<KeyValueStorage>().getWithFormat(\n      KVKeys.showRenameDialogWhenCreatingNewFile,\n      (value) => bool.parse(value),\n    );\n    final showRenameDialog = settingsOrFailure ?? false;\n    if (showRenameDialog) {\n      await tapOKButton();\n    }\n    await pumpAndSettle();\n\n    // open the page after created\n    await openPage(ViewLayoutPB.Document.defaultName);\n    await pumpAndSettle();\n\n    // Enter new name in the document title\n    await enterText(find.byType(TextFieldWithMetricLines), name);\n    await pumpAndSettle();\n  }\n\n  /// Create a new page in the space\n  Future<void> createNewPageInSpace({\n    required String spaceName,\n    required ViewLayoutPB layout,\n    bool openAfterCreated = true,\n    String? pageName,\n  }) async {\n    final currentSpace = find.byWidgetPredicate(\n      (widget) => widget is CurrentSpace && widget.space.name == spaceName,\n    );\n    if (currentSpace.evaluate().isEmpty) {\n      throw Exception('Current space not found');\n    }\n\n    await hoverOnWidget(\n      currentSpace,\n      onHover: () async {\n        // click the + button\n        await clickAddPageButtonInSpaceHeader();\n        await tapButtonWithName(layout.menuName);\n      },\n    );\n    await pumpAndSettle();\n\n    if (pageName != null) {\n      // move the cursor to other place to disable to tooltips\n      await tapAt(Offset.zero);\n\n      // hover on new created page and change it's name\n      await hoverOnPageName(\n        '',\n        layout: layout,\n        onHover: () async {\n          await renamePage(pageName);\n          await pumpAndSettle();\n        },\n      );\n      await pumpAndSettle();\n    }\n\n    // open the page after created\n    if (openAfterCreated) {\n      // if the name is null, use empty string\n      await openPage(pageName ?? '', layout: layout);\n      await pumpAndSettle();\n    }\n  }\n\n  /// Click the + button in the space header\n  Future<void> clickAddPageButtonInSpaceHeader() async {\n    final addPageButton = find.descendant(\n      of: find.byType(SidebarSpaceHeader),\n      matching: find.byType(ViewAddButton),\n    );\n    await tapButton(addPageButton);\n  }\n\n  /// Click the + button in the space header\n  Future<void> clickSpaceHeader() async {\n    await tapButton(find.byType(SidebarSpaceHeader));\n  }\n\n  Future<void> openSpace(String spaceName) async {\n    final space = find.descendant(\n      of: find.byType(SidebarSpaceMenuItem),\n      matching: find.text(spaceName),\n    );\n    await tapButton(space);\n  }\n\n  /// Create a new page on the top level\n  Future<void> createNewPage({\n    ViewLayoutPB layout = ViewLayoutPB.Document,\n    bool openAfterCreated = true,\n  }) async {\n    await tapButton(find.byType(SidebarNewPageButton));\n  }\n\n  Future<void> simulateKeyEvent(\n    LogicalKeyboardKey key, {\n    bool isControlPressed = false,\n    bool isShiftPressed = false,\n    bool isAltPressed = false,\n    bool isMetaPressed = false,\n    PhysicalKeyboardKey? physicalKey,\n  }) async {\n    if (isControlPressed) {\n      await simulateKeyDownEvent(LogicalKeyboardKey.control);\n    }\n    if (isShiftPressed) {\n      await simulateKeyDownEvent(LogicalKeyboardKey.shift);\n    }\n    if (isAltPressed) {\n      await simulateKeyDownEvent(LogicalKeyboardKey.alt);\n    }\n    if (isMetaPressed) {\n      await simulateKeyDownEvent(LogicalKeyboardKey.meta);\n    }\n    await simulateKeyDownEvent(\n      key,\n      physicalKey: physicalKey,\n    );\n    await simulateKeyUpEvent(\n      key,\n      physicalKey: physicalKey,\n    );\n    if (isControlPressed) {\n      await simulateKeyUpEvent(LogicalKeyboardKey.control);\n    }\n    if (isShiftPressed) {\n      await simulateKeyUpEvent(LogicalKeyboardKey.shift);\n    }\n    if (isAltPressed) {\n      await simulateKeyUpEvent(LogicalKeyboardKey.alt);\n    }\n    if (isMetaPressed) {\n      await simulateKeyUpEvent(LogicalKeyboardKey.meta);\n    }\n    await pumpAndSettle();\n  }\n\n  Future<void> openAppInNewTab(String name, ViewLayoutPB layout) async {\n    await hoverOnPageName(\n      name,\n      onHover: () async {\n        await tapOpenInTabButton();\n        await pumpAndSettle();\n      },\n    );\n    await pumpAndSettle();\n  }\n\n  Future<void> favoriteViewByName(\n    String name, {\n    ViewLayoutPB layout = ViewLayoutPB.Document,\n  }) async {\n    await hoverOnPageName(\n      name,\n      layout: layout,\n      onHover: () async {\n        await tapFavoritePageButton();\n        await pumpAndSettle();\n      },\n    );\n  }\n\n  Future<void> unfavoriteViewByName(\n    String name, {\n    ViewLayoutPB layout = ViewLayoutPB.Document,\n  }) async {\n    await hoverOnPageName(\n      name,\n      layout: layout,\n      onHover: () async {\n        await tapUnfavoritePageButton();\n        await pumpAndSettle();\n      },\n    );\n  }\n\n  Future<void> movePageToOtherPage({\n    required String name,\n    required String parentName,\n    required ViewLayoutPB layout,\n    required ViewLayoutPB parentLayout,\n    DraggableHoverPosition position = DraggableHoverPosition.center,\n  }) async {\n    final from = findPageName(name, layout: layout);\n    final to = findPageName(parentName, layout: parentLayout);\n    final gesture = await startGesture(getCenter(from));\n    Offset offset = Offset.zero;\n    switch (position) {\n      case DraggableHoverPosition.center:\n        offset = getCenter(to);\n        break;\n      case DraggableHoverPosition.top:\n        offset = getTopLeft(to);\n        break;\n      case DraggableHoverPosition.bottom:\n        offset = getBottomLeft(to);\n        break;\n      default:\n    }\n    await gesture.moveTo(offset, timeStamp: const Duration(milliseconds: 400));\n    await gesture.up();\n    await pumpAndSettle();\n  }\n\n  Future<void> reorderFavorite({\n    required String fromName,\n    required String toName,\n  }) async {\n    final from = find.descendant(\n          of: find.byType(FavoriteFolder),\n          matching: find.text(fromName),\n        ),\n        to = find.descendant(\n          of: find.byType(FavoriteFolder),\n          matching: find.text(toName),\n        );\n    final distanceY = getCenter(to).dy - getCenter(from).dx;\n    await drag(from, Offset(0, distanceY));\n    await pumpAndSettle(const Duration(seconds: 1));\n  }\n\n  // tap the button with [FlowySvgData]\n  Future<void> tapButtonWithFlowySvgData(FlowySvgData svg) async {\n    final button = find.byWidgetPredicate(\n      (widget) => widget is FlowySvg && widget.svg.path == svg.path,\n    );\n    await tapButton(button);\n  }\n\n  // update the page icon in the sidebar\n  Future<void> updatePageIconInSidebarByName({\n    required String name,\n    String? parentName,\n    required ViewLayoutPB layout,\n    required EmojiIconData icon,\n  }) async {\n    final iconButton = find.descendant(\n      of: findPageName(\n        name,\n        layout: layout,\n        parentName: parentName,\n      ),\n      matching:\n          find.byTooltip(LocaleKeys.document_plugins_cover_changeIcon.tr()),\n    );\n    await tapButton(iconButton);\n    if (icon.type == FlowyIconType.emoji) {\n      await tapEmoji(icon.emoji);\n    } else if (icon.type == FlowyIconType.icon) {\n      await tapIcon(icon);\n    }\n    await pumpAndSettle();\n  }\n\n  // update the page icon in the sidebar\n  Future<void> updatePageIconInTitleBarByName({\n    required String name,\n    required ViewLayoutPB layout,\n    required EmojiIconData icon,\n  }) async {\n    await openPage(\n      name,\n      layout: layout,\n    );\n    final title = find.descendant(\n      of: find.byType(ViewTitleBar),\n      matching: find.text(name),\n    );\n    await tapButton(title);\n    await tapButton(find.byType(EmojiPickerButton));\n    if (icon.type == FlowyIconType.emoji) {\n      await tapEmoji(icon.emoji);\n    } else if (icon.type == FlowyIconType.icon) {\n      await tapIcon(icon);\n    } else if (icon.type == FlowyIconType.custom) {\n      await pickImage(icon);\n    }\n    await pumpAndSettle();\n  }\n\n  Future<void> updatePageIconInTitleBarByPasteALink({\n    required String name,\n    required ViewLayoutPB layout,\n    required String iconLink,\n  }) async {\n    await openPage(\n      name,\n      layout: layout,\n    );\n    final title = find.descendant(\n      of: find.byType(ViewTitleBar),\n      matching: find.text(name),\n    );\n    await tapButton(title);\n    await tapButton(find.byType(EmojiPickerButton));\n    await pasteImageLinkAsIcon(iconLink);\n    await pumpAndSettle();\n  }\n\n  Future<void> openNotificationHub({int tabIndex = 0}) async {\n    final finder = find.descendant(\n      of: find.byType(NotificationButton),\n      matching: find.byWidgetPredicate(\n        (widget) => widget is FlowySvg && widget.svg == FlowySvgs.clock_alarm_s,\n      ),\n    );\n\n    await tap(finder);\n    await pumpAndSettle();\n\n    if (tabIndex == 1) {\n      final tabFinder = find.descendant(\n        of: find.byType(NotificationTabBar),\n        matching: find.byType(FlowyTabItem).at(1),\n      );\n\n      await tap(tabFinder);\n      await pumpAndSettle();\n    }\n  }\n\n  Future<void> toggleCommandPalette() async {\n    // Press CMD+P or CTRL+P to open the command palette\n    await simulateKeyEvent(\n      LogicalKeyboardKey.keyP,\n      isControlPressed: !Platform.isMacOS,\n      isMetaPressed: Platform.isMacOS,\n    );\n    await pumpAndSettle();\n  }\n\n  Future<void> openCollaborativeWorkspaceMenu() async {\n    if (!FeatureFlag.collaborativeWorkspace.isOn) {\n      throw UnsupportedError('Collaborative workspace is not enabled');\n    }\n\n    final workspace = find.byType(SidebarWorkspace);\n    expect(workspace, findsOneWidget);\n\n    await tapButton(workspace, milliseconds: 5000);\n  }\n\n  Future<void> createCollaborativeWorkspace(String name) async {\n    if (!FeatureFlag.collaborativeWorkspace.isOn) {\n      throw UnsupportedError('Collaborative workspace is not enabled');\n    }\n    await openCollaborativeWorkspaceMenu();\n    // expect to see the workspace list, and there should be only one workspace\n    final workspacesMenu = find.byType(WorkspacesMenu);\n    expect(workspacesMenu, findsOneWidget);\n\n    // click the create button\n    final createButton = find.byKey(createWorkspaceButtonKey);\n    expect(createButton, findsOneWidget);\n    await tapButton(createButton);\n\n    // input the workspace name\n    final workspaceNameInput = find.descendant(\n      of: find.byType(AFTextFieldDialog),\n      matching: find.byType(TextField),\n    );\n    await enterText(workspaceNameInput, name);\n    await pumpAndSettle();\n\n    await tapButton(\n      find.text(LocaleKeys.button_confirm.tr()),\n      milliseconds: 2000,\n    );\n  }\n\n  // For mobile platform to launch the app in anonymous mode\n  Future<void> launchInAnonymousMode() async {\n    assert(\n      [TargetPlatform.android, TargetPlatform.iOS]\n          .contains(defaultTargetPlatform),\n      'This method is only supported on mobile platforms',\n    );\n\n    await initializeAppFlowy();\n\n    final anonymousSignInButton = find.byType(SignInAnonymousButtonV2);\n    expect(anonymousSignInButton, findsOneWidget);\n    await tapButton(anonymousSignInButton);\n\n    await pumpUntilFound(find.byType(MobileHomeScreen));\n  }\n\n  Future<void> tapSvgButton(FlowySvgData svg) async {\n    final button = find.byWidgetPredicate(\n      (widget) => widget is FlowySvg && widget.svg.path == svg.path,\n    );\n    await tapButton(button);\n  }\n\n  Future<void> openMoreViewActions() async {\n    final button = find.byType(MoreViewActions);\n    await tapButton(button);\n  }\n\n  /// Presses on the Duplicate ViewAction in the [MoreViewActions] popup.\n  ///\n  /// [openMoreViewActions] must be called beforehand!\n  ///\n  Future<void> duplicateByMoreViewActions() async {\n    final button = find.byWidgetPredicate(\n      (widget) =>\n          widget is ViewAction && widget.type == ViewMoreActionType.duplicate,\n    );\n    await tap(button);\n    await pump();\n  }\n\n  /// Presses on the Delete ViewAction in the [MoreViewActions] popup.\n  ///\n  /// [openMoreViewActions] must be called beforehand!\n  ///\n  Future<void> deleteByMoreViewActions() async {\n    final button = find.descendant(\n      of: find.byType(ListView),\n      matching: find.byWidgetPredicate(\n        (widget) =>\n            widget is ViewAction && widget.type == ViewMoreActionType.delete,\n      ),\n    );\n    await tap(button);\n    await pump();\n  }\n\n  Future<void> tapFileUploadHint() async {\n    final finder = find.byWidgetPredicate(\n      (w) =>\n          w is RichText &&\n          w.text.toPlainText().contains(\n                LocaleKeys.document_plugins_file_fileUploadHint.tr(),\n              ),\n    );\n    await tap(finder);\n    await pumpAndSettle(const Duration(seconds: 2));\n  }\n\n  /// Create a new document on mobile\n  Future<void> createNewDocumentOnMobile(String name) async {\n    final createPageButton = find.byKey(\n      BottomNavigationBarItemType.add.valueKey,\n    );\n    await tapButton(createPageButton);\n    expect(find.byType(MobileDocumentScreen), findsOneWidget);\n\n    final title = editor.findDocumentTitle('');\n    expect(title, findsOneWidget);\n    final textField = widget<TextField>(title);\n    expect(textField.focusNode!.hasFocus, isTrue);\n\n    // input new name and press done button\n    await enterText(title, name);\n    await testTextInput.receiveAction(TextInputAction.done);\n    await pumpAndSettle();\n    final newTitle = editor.findDocumentTitle(name);\n    expect(newTitle, findsOneWidget);\n    expect(textField.controller!.text, name);\n  }\n\n  /// Open the plus menu\n  Future<void> openPlusMenuAndClickButton(String buttonName) async {\n    assert(\n      UniversalPlatform.isMobile,\n      'This method is only supported on mobile platforms',\n    );\n\n    final plusMenuButton = find.byKey(addBlockToolbarItemKey);\n    final addMenuItem = find.byType(AddBlockMenu);\n    await tapButton(plusMenuButton);\n    await pumpUntilFound(addMenuItem);\n\n    final toggleHeading1 = find.byWidgetPredicate(\n      (widget) =>\n          widget is TypeOptionMenuItem && widget.value.text == buttonName,\n    );\n    final scrollable = find.ancestor(\n      of: find.byType(TypeOptionGridView),\n      matching: find.byType(Scrollable),\n    );\n    await scrollUntilVisible(\n      toggleHeading1,\n      100,\n      scrollable: scrollable,\n    );\n    await tapButton(toggleHeading1);\n    await pumpUntilNotFound(addMenuItem);\n  }\n\n  /// Click the column menu button in the simple table\n  Future<void> clickColumnMenuButton(int index) async {\n    final columnMenuButton = find.byWidgetPredicate(\n      (w) =>\n          w is SimpleTableMobileReorderButton &&\n          w.index == index &&\n          w.type == SimpleTableMoreActionType.column,\n    );\n    await tapButton(columnMenuButton);\n    await pumpUntilFound(find.byType(SimpleTableCellBottomSheet));\n  }\n\n  /// Click the row menu button in the simple table\n  Future<void> clickRowMenuButton(int index) async {\n    final rowMenuButton = find.byWidgetPredicate(\n      (w) =>\n          w is SimpleTableMobileReorderButton &&\n          w.index == index &&\n          w.type == SimpleTableMoreActionType.row,\n    );\n    await tapButton(rowMenuButton);\n    await pumpUntilFound(find.byType(SimpleTableCellBottomSheet));\n  }\n\n  /// Click the SimpleTableQuickAction\n  Future<void> clickSimpleTableQuickAction(SimpleTableMoreAction action) async {\n    final button = find.byWidgetPredicate(\n      (widget) => widget is SimpleTableQuickAction && widget.type == action,\n    );\n    await tapButton(button);\n  }\n\n  /// Click the SimpleTableContentAction\n  Future<void> clickSimpleTableBoldContentAction() async {\n    final button = find.byType(SimpleTableContentBoldAction);\n    await tapButton(button);\n  }\n\n  /// Cancel the table action menu\n  Future<void> cancelTableActionMenu() async {\n    final finder = find.byType(SimpleTableCellBottomSheet);\n    if (finder.evaluate().isEmpty) {\n      return;\n    }\n\n    await tapAt(Offset.zero);\n    await pumpUntilNotFound(finder);\n  }\n\n  /// load icon list and return the first one\n  Future<EmojiIconData> loadIcon() async {\n    await loadIconGroups();\n    final groups = kIconGroups!;\n    final firstGroup = groups.first;\n    final firstIcon = firstGroup.icons.first;\n    return EmojiIconData.icon(\n      IconsData(\n        firstGroup.name,\n        firstIcon.name,\n        builtInSpaceColors.first,\n      ),\n    );\n  }\n\n  Future<EmojiIconData> prepareImageIcon() async {\n    final imagePath = await rootBundle.load('assets/test/images/sample.jpeg');\n    final tempDirectory = await getTemporaryDirectory();\n    final localImagePath = p.join(tempDirectory.path, 'sample.jpeg');\n    final imageFile = File(localImagePath)\n      ..writeAsBytesSync(imagePath.buffer.asUint8List());\n    return EmojiIconData.custom(imageFile.path);\n  }\n\n  Future<EmojiIconData> prepareSvgIcon() async {\n    final imagePath = await rootBundle.load('assets/test/images/sample.svg');\n    final tempDirectory = await getTemporaryDirectory();\n    final localImagePath = p.join(tempDirectory.path, 'sample.svg');\n    final imageFile = File(localImagePath)\n      ..writeAsBytesSync(imagePath.buffer.asUint8List());\n    return EmojiIconData.custom(imageFile.path);\n  }\n\n  /// create new page and show slash menu\n  Future<void> createPageAndShowSlashMenu(String title) async {\n    await createNewDocumentOnMobile(title);\n    await editor.tapLineOfEditorAt(0);\n    await editor.showSlashMenu();\n  }\n\n  /// create new page and show at menu\n  Future<void> createPageAndShowAtMenu(String title) async {\n    await createNewDocumentOnMobile(title);\n    await editor.tapLineOfEditorAt(0);\n    await editor.showAtMenu();\n  }\n\n  /// create new page and show plus menu\n  Future<void> createPageAndShowPlusMenu(String title) async {\n    await createNewDocumentOnMobile(title);\n    await editor.tapLineOfEditorAt(0);\n    await editor.showPlusMenu();\n  }\n}\n\nextension SettingsFinder on CommonFinders {\n  Finder findSettingsScrollable() => find\n      .descendant(\n        of: find\n            .descendant(\n              of: find.byType(SettingsBody),\n              matching: find.byType(SingleChildScrollView),\n            )\n            .first,\n        matching: find.byType(Scrollable),\n      )\n      .first;\n\n  Finder findSettingsMenuScrollable() => find\n      .descendant(\n        of: find\n            .descendant(\n              of: find.byType(SettingsMenu),\n              matching: find.byType(SingleChildScrollView),\n            )\n            .first,\n        matching: find.byType(Scrollable),\n      )\n      .first;\n}\n\nextension FlowySvgFinder on CommonFinders {\n  Finder byFlowySvg(FlowySvgData svg) => _FlowySvgFinder(svg);\n}\n\nclass _FlowySvgFinder extends MatchFinder {\n  _FlowySvgFinder(this.svg);\n\n  final FlowySvgData svg;\n\n  @override\n  String get description => 'flowy_svg \"$svg\"';\n\n  @override\n  bool matches(Element candidate) {\n    final Widget widget = candidate.widget;\n    return widget is FlowySvg && widget.svg == svg;\n  }\n}\n\nextension ViewLayoutPBTest on ViewLayoutPB {\n  String get menuName {\n    switch (this) {\n      case ViewLayoutPB.Grid:\n        return LocaleKeys.grid_menuName.tr();\n      case ViewLayoutPB.Board:\n        return LocaleKeys.board_menuName.tr();\n      case ViewLayoutPB.Document:\n        return LocaleKeys.document_menuName.tr();\n      case ViewLayoutPB.Calendar:\n        return LocaleKeys.calendar_menuName.tr();\n      case ViewLayoutPB.Chat:\n        return LocaleKeys.chat_newChat.tr();\n      default:\n        throw UnsupportedError('Unsupported layout: $this');\n    }\n  }\n\n  String get referencedMenuName {\n    switch (this) {\n      case ViewLayoutPB.Grid:\n        return LocaleKeys.document_plugins_referencedGrid.tr();\n      case ViewLayoutPB.Board:\n        return LocaleKeys.document_plugins_referencedBoard.tr();\n      case ViewLayoutPB.Calendar:\n        return LocaleKeys.document_plugins_referencedCalendar.tr();\n      default:\n        throw UnsupportedError('Unsupported layout: $this');\n    }\n  }\n\n  String get slashMenuName {\n    switch (this) {\n      case ViewLayoutPB.Grid:\n        return LocaleKeys.document_slashMenu_name_grid.tr();\n      case ViewLayoutPB.Board:\n        return LocaleKeys.document_slashMenu_name_kanban.tr();\n      case ViewLayoutPB.Document:\n        return LocaleKeys.document_slashMenu_name_doc.tr();\n      case ViewLayoutPB.Calendar:\n        return LocaleKeys.document_slashMenu_name_calendar.tr();\n      default:\n        throw UnsupportedError('Unsupported layout: $this');\n    }\n  }\n\n  String get slashMenuLinkedName {\n    switch (this) {\n      case ViewLayoutPB.Grid:\n        return LocaleKeys.document_slashMenu_name_linkedGrid.tr();\n      case ViewLayoutPB.Board:\n        return LocaleKeys.document_slashMenu_name_linkedKanban.tr();\n      case ViewLayoutPB.Document:\n        return LocaleKeys.document_slashMenu_name_linkedDoc.tr();\n      case ViewLayoutPB.Calendar:\n        return LocaleKeys.document_slashMenu_name_linkedCalendar.tr();\n      default:\n        throw UnsupportedError('Unsupported layout: $this');\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/constants.dart",
    "content": "class Constants {\n  // this page name is default page name in the new workspace\n  static const gettingStartedPageName = 'Getting started';\n  static const toDosPageName = 'To-dos';\n  static const generalSpaceName = 'General';\n\n  static const defaultWorkspaceName = 'My Workspace';\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/data.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:archive/archive_io.dart';\nimport 'package:flutter/services.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nenum TestWorkspace {\n  board(\"board\"),\n  emptyDocument(\"empty_document\"),\n  aiWorkSpace(\"ai_workspace\"),\n  coverImage(\"cover_image\");\n\n  const TestWorkspace(this._name);\n\n  final String _name;\n\n  Future<File> get zip async {\n    final Directory parent = await TestWorkspace._parent;\n    final File out = File(p.join(parent.path, '$_name.zip'));\n    if (await out.exists()) return out;\n    await out.create();\n    final ByteData data = await rootBundle.load(_asset);\n    await out.writeAsBytes(data.buffer.asUint8List());\n    return out;\n  }\n\n  Future<Directory> get root async {\n    final Directory parent = await TestWorkspace._parent;\n    return Directory(p.join(parent.path, _name));\n  }\n\n  static Future<Directory> get _parent async {\n    final Directory root = await getTemporaryDirectory();\n    if (await root.exists()) return root;\n    await root.create();\n    return root;\n  }\n\n  String get _asset => 'assets/test/workspaces/$_name.zip';\n}\n\nclass TestWorkspaceService {\n  const TestWorkspaceService(this.workspace);\n\n  final TestWorkspace workspace;\n\n  /// Instructs the application to read workspace data from the workspace found under this [TestWorkspace]'s path.\n  Future<void> setUpAll() async {\n    final root = await workspace.root;\n    final path = root.path;\n    SharedPreferences.setMockInitialValues({KVKeys.pathLocation: path});\n  }\n\n  /// Workspaces that are checked into source are compressed. [TestWorkspaceService.setUp()] decompresses the file into an ephemeral directory that will be ignored by source control.\n  Future<void> setUp() async {\n    final inputStream =\n        InputFileStream(await workspace.zip.then((value) => value.path));\n    final archive = ZipDecoder().decodeBuffer(inputStream);\n    await extractArchiveToDisk(\n      archive,\n      await TestWorkspace._parent.then((value) => value.path),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/database_test_op.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart';\nimport 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy/plugins/database/board/presentation/board_page.dart';\nimport 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';\nimport 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart';\nimport 'package:appflowy/plugins/database/calendar/presentation/calendar_day.dart';\nimport 'package:appflowy/plugins/database/calendar/presentation/calendar_event_card.dart';\nimport 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart';\nimport 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';\nimport 'package:appflowy/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checklist.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/disclosure_button.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/sort/order_panel.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_editor.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_menu.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart';\nimport 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dart';\nimport 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/media.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/number.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/url.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/date_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.dart';\nimport 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';\nimport 'package:appflowy/plugins/database/widgets/field/field_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/field/field_type_list.dart';\nimport 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart';\nimport 'package:appflowy/plugins/database/widgets/field/type_option_editor/number.dart';\nimport 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_action.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_banner.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_detail.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_document.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_property.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/database_layout_selector.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/database_setting_action.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/database_settings_list.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/setting_button.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/setting_property_list.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_board/appflowy_board.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:calendar_view/calendar_view.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/widget/buttons/primary_button.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:path/path.dart' as p;\n// Non-exported member of the table_calendar library\nimport 'package:table_calendar/src/widgets/cell_content.dart';\nimport 'package:table_calendar/table_calendar.dart';\n\nimport 'base.dart';\nimport 'common_operations.dart';\nimport 'expectation.dart';\nimport 'mock/mock_file_picker.dart';\n\nconst v020GridFileName = \"v020.afdb\";\nconst v069GridFileName = \"v069.afdb\";\n\nextension AppFlowyDatabaseTest on WidgetTester {\n  Future<void> openTestDatabase(String fileName) async {\n    final context = await initializeAppFlowy();\n    await tapAnonymousSignInButton();\n\n    // expect to see a readme page\n    expectToSeePageName(gettingStarted);\n\n    await tapAddViewButton();\n    await tapImportButton();\n\n    // Don't use the p.join to build the path that used in loadString. It\n    // is not working on windows.\n    final str = await rootBundle\n        .loadString(\"assets/test/workspaces/database/$fileName\");\n\n    // Write the content to the file.\n    final path = p.join(\n      context.applicationDataDirectory,\n      fileName,\n    );\n    final pageName = p.basenameWithoutExtension(path);\n    File(path).writeAsStringSync(str);\n    // mock get files\n    mockPickFilePaths(\n      paths: [path],\n    );\n    await tapDatabaseRawDataButton();\n    await openPage(pageName, layout: ViewLayoutPB.Grid);\n  }\n\n  Future<void> hoverOnFirstRowOfGrid([Future<void> Function()? onHover]) async {\n    final findRow = find.byType(GridRow);\n    expect(findRow, findsWidgets);\n\n    final firstRow = findRow.first;\n    await hoverOnWidget(firstRow, onHover: onHover);\n  }\n\n  Future<void> editCell({\n    required int rowIndex,\n    required FieldType fieldType,\n    required String input,\n    int cellIndex = 0,\n  }) async {\n    final cell = cellFinder(rowIndex, fieldType, cellIndex: cellIndex);\n\n    expect(cell, findsOneWidget);\n    await enterText(cell, input);\n    await testTextInput.receiveAction(TextInputAction.done);\n    await pumpAndSettle();\n  }\n\n  Finder cellFinder(int rowIndex, FieldType fieldType, {int cellIndex = 0}) {\n    final findRow = find.byType(GridRow, skipOffstage: false);\n    final findCell = finderForFieldType(fieldType);\n    return find\n        .descendant(\n          of: findRow.at(rowIndex),\n          matching: findCell,\n          skipOffstage: false,\n        )\n        .at(cellIndex);\n  }\n\n  Future<void> tapCheckboxCellInGrid({\n    required int rowIndex,\n  }) async {\n    final cell = cellFinder(rowIndex, FieldType.Checkbox);\n\n    final button = find.descendant(\n      of: cell,\n      matching: find.byType(FlowyIconButton),\n    );\n\n    expect(cell, findsOneWidget);\n    await tapButton(button);\n  }\n\n  Future<void> assertCheckboxCell({\n    required int rowIndex,\n    required bool isSelected,\n  }) async {\n    final cell = cellFinder(rowIndex, FieldType.Checkbox);\n    final finder = isSelected\n        ? find.byWidgetPredicate(\n            (widget) =>\n                widget is FlowySvg && widget.svg == FlowySvgs.check_filled_s,\n          )\n        : find.byWidgetPredicate(\n            (widget) => widget is FlowySvg && widget.svg == FlowySvgs.uncheck_s,\n          );\n\n    expect(\n      find.descendant(\n        of: cell,\n        matching: finder,\n      ),\n      findsOneWidget,\n    );\n  }\n\n  Future<void> tapCellInGrid({\n    required int rowIndex,\n    required FieldType fieldType,\n  }) async {\n    final cell = cellFinder(rowIndex, fieldType);\n    expect(cell, findsOneWidget);\n    await tapButton(cell);\n  }\n\n  /// The [fieldName] must be unique in the grid.\n  void assertCellContent({\n    required int rowIndex,\n    required FieldType fieldType,\n    required String content,\n    int cellIndex = 0,\n  }) {\n    final findCell = cellFinder(rowIndex, fieldType, cellIndex: cellIndex);\n    final findContent = find.descendant(\n      of: findCell,\n      matching: find.text(content),\n      skipOffstage: false,\n    );\n    expect(findContent, findsOneWidget);\n  }\n\n  Future<void> assertSingleSelectOption({\n    required int rowIndex,\n    required String content,\n  }) async {\n    final findCell = cellFinder(rowIndex, FieldType.SingleSelect);\n    if (content.isNotEmpty) {\n      final finder = find.descendant(\n        of: findCell,\n        matching: find.byWidgetPredicate(\n          (widget) =>\n              widget is SelectOptionTag &&\n              (widget.name == content || widget.option?.name == content),\n        ),\n      );\n      expect(finder, findsOneWidget);\n    }\n  }\n\n  void assertMultiSelectOption({\n    required int rowIndex,\n    required List<String> contents,\n  }) {\n    final findCell = cellFinder(rowIndex, FieldType.MultiSelect);\n    for (final content in contents) {\n      if (content.isNotEmpty) {\n        final finder = find.descendant(\n          of: findCell,\n          matching: find.byWidgetPredicate(\n            (widget) =>\n                widget is SelectOptionTag &&\n                (widget.name == content || widget.option?.name == content),\n          ),\n        );\n        expect(finder, findsOneWidget);\n      }\n    }\n  }\n\n  /// null percent means no progress bar should be found\n  void assertChecklistCellInGrid({\n    required int rowIndex,\n    required double? percent,\n  }) {\n    final findCell = cellFinder(rowIndex, FieldType.Checklist);\n\n    if (percent == null) {\n      final finder = find.descendant(\n        of: findCell,\n        matching: find.byType(ChecklistProgressBar),\n      );\n      expect(finder, findsNothing);\n    } else {\n      final finder = find.descendant(\n        of: findCell,\n        matching: find.byWidgetPredicate(\n          (widget) =>\n              widget is ChecklistProgressBar && widget.percent == percent,\n        ),\n      );\n      expect(finder, findsOneWidget);\n    }\n  }\n\n  Future<void> selectDay({\n    required int content,\n  }) async {\n    final findCalendar = find.byType(TableCalendar);\n    final findDay = find.text(content.toString());\n\n    final finder = find.descendant(\n      of: findCalendar,\n      matching: findDay,\n    );\n\n    // if the day is very near the beginning or the end of the month,\n    // it may overlap with the same day in the next or previous month,\n    // respectively because it was spilling over. This will lead to 2\n    // widgets being found and thus cannot be tapped correctly.\n    if (content < 15) {\n      // e.g., Jan 2 instead of Feb 2\n      await tapButton(finder.first);\n    } else {\n      // e.g. Jun 28 instead of May 28\n      await tapButton(finder.last);\n    }\n  }\n\n  Future<void> toggleIncludeTime() async {\n    final findDateEditor = find.byType(IncludeTimeButton);\n    final findToggle = find.byType(Toggle);\n    final finder = find.descendant(\n      of: findDateEditor,\n      matching: findToggle,\n    );\n    await tapButton(finder);\n  }\n\n  Future<void> selectReminderOption(ReminderOption option) async {\n    await tapButton(find.byType(ReminderSelector));\n\n    final finder = find.descendant(\n      of: find.byType(FlowyButton),\n      matching: find.textContaining(option.label),\n    );\n\n    await tapButton(finder);\n  }\n\n  Future<bool> selectLastDateInPicker() async {\n    final finder = find.byType(CellContent).last;\n    final w = widget(finder) as CellContent;\n\n    await tapButton(finder);\n\n    return w.isToday;\n  }\n\n  Future<void> tapChangeDateTimeFormatButton() async {\n    await tapButton(find.byType(DateTypeOptionButton));\n  }\n\n  Future<void> changeDateFormat() async {\n    final findDateFormatButton = find.byType(DateFormatButton);\n    await tapButton(findDateFormatButton);\n\n    final findNewDateFormat = find.text(\"Day/Month/Year\");\n    await tapButton(findNewDateFormat);\n  }\n\n  Future<void> changeTimeFormat() async {\n    final findDateFormatButton = find.byType(TimeFormatButton);\n    await tapButton(findDateFormatButton);\n\n    final findNewDateFormat = find.text(\"12 hour\");\n    await tapButton(findNewDateFormat);\n  }\n\n  Future<void> clearDate() async {\n    final findDateEditor = find.byType(DateCellEditor);\n    final findClearButton = find.byType(ClearDateButton);\n    final finder = find.descendant(\n      of: findDateEditor,\n      matching: findClearButton,\n    );\n    await tapButton(finder);\n  }\n\n  Future<void> tapSelectOptionCellInGrid({\n    required int rowIndex,\n    required FieldType fieldType,\n  }) async {\n    assert(\n      fieldType == FieldType.SingleSelect || fieldType == FieldType.MultiSelect,\n    );\n\n    final findRow = find.byType(GridRow);\n    final findCell = finderForFieldType(fieldType);\n\n    final cell = find.descendant(\n      of: findRow.at(rowIndex),\n      matching: findCell,\n    );\n\n    await tapButton(cell);\n  }\n\n  /// The [SelectOptionCellEditor] must be opened first.\n  Future<void> createOption({required String name}) async {\n    final findEditor = find.byType(SelectOptionCellEditor);\n    expect(findEditor, findsOneWidget);\n\n    final findTextField = find.byType(SelectOptionTextField);\n    expect(findTextField, findsOneWidget);\n\n    await enterText(findTextField, name);\n    await pump();\n\n    await testTextInput.receiveAction(TextInputAction.done);\n    await pumpAndSettle();\n  }\n\n  Future<void> selectOption({required String name}) async {\n    final option = find.descendant(\n      of: find.byType(SelectOptionCellEditor),\n      matching: find.byWidgetPredicate(\n        (widget) => widget is SelectOptionTagCell && widget.option.name == name,\n      ),\n    );\n\n    await tapButton(option);\n  }\n\n  void findSelectOptionWithNameInGrid({\n    required int rowIndex,\n    required String name,\n  }) {\n    final findRow = find.byType(GridRow);\n    final option = find.byWidgetPredicate(\n      (widget) =>\n          widget is SelectOptionTag &&\n          (widget.name == name || widget.option?.name == name),\n    );\n\n    final cell = find.descendant(of: findRow.at(rowIndex), matching: option);\n    expect(cell, findsOneWidget);\n  }\n\n  void assertNumberOfSelectedOptionsInGrid({\n    required int rowIndex,\n    required Matcher matcher,\n  }) {\n    final findRow = find.byType(GridRow);\n\n    final options = find.byWidgetPredicate(\n      (widget) => widget is SelectOptionTag,\n    );\n\n    final cell = find.descendant(of: findRow.at(rowIndex), matching: options);\n    expect(cell, matcher);\n  }\n\n  Future<void> tapChecklistCellInGrid({required int rowIndex}) async {\n    final findRow = find.byType(GridRow);\n    final findCell = finderForFieldType(FieldType.Checklist);\n\n    final cell = find.descendant(of: findRow.at(rowIndex), matching: findCell);\n    await tapButton(cell);\n  }\n\n  void assertChecklistEditorVisible({required bool visible}) {\n    final editor = find.byType(ChecklistCellEditor);\n    if (visible) {\n      return expect(editor, findsOneWidget);\n    }\n    expect(editor, findsNothing);\n  }\n\n  Future<void> createNewChecklistTask({\n    required String name,\n    enter = false,\n    button = false,\n  }) async {\n    assert(!(enter && button));\n    final textField = find.descendant(\n      of: find.byType(NewTaskItem),\n      matching: find.byType(TextField),\n    );\n\n    await enterText(textField, name);\n    await pumpAndSettle();\n    if (enter) {\n      await testTextInput.receiveAction(TextInputAction.done);\n      await pumpAndSettle(const Duration(milliseconds: 500));\n    } else {\n      await tapButton(\n        find.descendant(\n          of: find.byType(NewTaskItem),\n          matching: find.byType(FlowyTextButton),\n        ),\n      );\n    }\n  }\n\n  void assertChecklistTaskInEditor({\n    required int index,\n    required String name,\n    required bool isChecked,\n  }) {\n    final task = find.byType(ChecklistItem).at(index);\n    final widget = this.widget<ChecklistItem>(task);\n    assert(\n      widget.task.data.name == name && widget.task.isSelected == isChecked,\n    );\n  }\n\n  Future<void> renameChecklistTask({\n    required int index,\n    required String name,\n    bool enter = true,\n  }) async {\n    final textField = find\n        .descendant(\n          of: find.byType(ChecklistItem),\n          matching: find.byType(TextField),\n        )\n        .at(index);\n\n    await enterText(textField, name);\n    if (enter) {\n      await testTextInput.receiveAction(TextInputAction.done);\n    }\n    await pumpAndSettle();\n  }\n\n  Future<void> checkChecklistTask({required int index}) async {\n    final button = find.descendant(\n      of: find.byType(ChecklistItem).at(index),\n      matching: find.byWidgetPredicate(\n        (widget) => widget is FlowySvg && widget.svg == FlowySvgs.uncheck_s,\n      ),\n    );\n\n    await tapButton(button);\n  }\n\n  Future<void> deleteChecklistTask({required int index}) async {\n    final task = find.byType(ChecklistItem).at(index);\n\n    await hoverOnWidget(\n      task,\n      onHover: () async {\n        final button = find.byWidgetPredicate(\n          (widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s,\n        );\n        await tapButton(button);\n      },\n    );\n  }\n\n  void assertPhantomChecklistItemAtIndex({required int index}) {\n    final paddings = find.descendant(\n      of: find.descendant(\n        of: find.byType(ChecklistRowDetailCell),\n        matching: find.byType(ReorderableListView),\n      ),\n      matching: find.byWidgetPredicate(\n        (widget) =>\n            widget is Padding &&\n            (widget.child is ChecklistItem ||\n                widget.child is PhantomChecklistItem),\n      ),\n    );\n    final phantom = widget<Padding>(paddings.at(index)).child!;\n    expect(phantom is PhantomChecklistItem, true);\n  }\n\n  void assertPhantomChecklistItemContent(String content) {\n    final phantom = find.byType(PhantomChecklistItem);\n    final text = find.text(content);\n    expect(find.descendant(of: phantom, matching: text), findsOneWidget);\n  }\n\n  Future<void> openFirstRowDetailPage() async {\n    await hoverOnFirstRowOfGrid();\n\n    final expandButton = find.byType(PrimaryCellAccessory);\n    expect(expandButton, findsOneWidget);\n    await tapButton(expandButton);\n  }\n\n  void assertRowDetailPageOpened() async {\n    final findRowDetailPage = find.byType(RowDetailPage);\n    expect(findRowDetailPage, findsOneWidget);\n  }\n\n  Future<void> dismissRowDetailPage() async {\n    // use tap empty area instead of clicking ESC to dismiss the row detail page\n    // sometimes, the ESC key is not working.\n    await simulateKeyEvent(LogicalKeyboardKey.escape);\n    await pumpAndSettle();\n    final findRowDetailPage = find.byType(RowDetailPage);\n    if (findRowDetailPage.evaluate().isNotEmpty) {\n      await tapAt(const Offset(0, 0));\n      await pumpAndSettle();\n    }\n  }\n\n  Future<void> hoverRowBanner() async {\n    final banner = find.byType(RowBanner);\n    expect(banner, findsOneWidget);\n\n    await startGesture(\n      getCenter(banner) + const Offset(0, -10),\n      kind: PointerDeviceKind.mouse,\n    );\n    await pumpAndSettle();\n  }\n\n  /// Used to open the add cover popover, by pressing on \"Add cover\"-button.\n  ///\n  /// Should call [hoverRowBanner] first.\n  ///\n  Future<void> tapAddCoverButton() async {\n    await tapButtonWithName(\n      LocaleKeys.document_plugins_cover_addCover.tr(),\n    );\n  }\n\n  Future<void> openEmojiPicker() async =>\n      tapButton(find.text(LocaleKeys.document_plugins_cover_addIcon.tr()));\n\n  Future<void> tapDateCellInRowDetailPage() async {\n    final findDateCell = find.byType(EditableDateCell);\n    await tapButton(findDateCell);\n  }\n\n  Future<void> tapGridFieldWithNameInRowDetailPage(String name) async {\n    final fields = find.byWidgetPredicate(\n      (widget) => widget is FieldCellButton && widget.field.name == name,\n    );\n    final field = find.descendant(\n      of: find.byType(RowDetailPage),\n      matching: fields,\n    );\n    await tapButton(field);\n    await pumpAndSettle();\n  }\n\n  Future<TestGesture> hoverOnFieldInRowDetail({required int index}) async {\n    final fieldButtons = find.byType(FieldCellButton);\n    final button = find\n        .descendant(of: find.byType(RowDetailPage), matching: fieldButtons)\n        .at(index);\n    return startGesture(getCenter(button), kind: PointerDeviceKind.mouse);\n  }\n\n  Future<void> reorderFieldInRowDetail({required double offset}) async {\n    final thumb = find\n        .byWidgetPredicate(\n          (widget) => widget is ReorderableDragStartListener && widget.enabled,\n        )\n        .first;\n    await drag(thumb, Offset(0, offset), kind: PointerDeviceKind.mouse);\n    await pumpAndSettle();\n  }\n\n  void assertToggleShowHiddenFieldsVisibility(bool shown) {\n    final button = find.byType(ToggleHiddenFieldsVisibilityButton);\n    if (shown) {\n      expect(button, findsOneWidget);\n    } else {\n      expect(button, findsNothing);\n    }\n  }\n\n  Future<void> toggleShowHiddenFields() async {\n    final button = find.byType(ToggleHiddenFieldsVisibilityButton);\n    await tapButton(button);\n  }\n\n  Future<void> tapDeletePropertyInFieldEditor() async {\n    final deleteButton = find.byWidgetPredicate(\n      (w) => w is FieldActionCell && w.action == FieldAction.delete,\n    );\n    await tapButton(deleteButton);\n    await tapButtonWithName(LocaleKeys.space_delete.tr());\n  }\n\n  Future<void> scrollRowDetailByOffset(Offset offset) async {\n    await drag(find.byType(RowDetailPage), offset);\n    await pumpAndSettle();\n  }\n\n  Future<void> scrollToRight(Finder find) async {\n    final size = getSize(find);\n    await drag(find, Offset(-size.width, 0), warnIfMissed: false);\n    await pumpAndSettle(const Duration(milliseconds: 500));\n  }\n\n  Future<void> tapNewPropertyButton() async {\n    await tapButtonWithName(LocaleKeys.grid_field_newProperty.tr());\n    await pumpAndSettle();\n  }\n\n  Future<void> tapGridFieldWithName(String name) async {\n    final field = find.byWidgetPredicate(\n      (widget) => widget is FieldCellButton && widget.field.name == name,\n    );\n    await tapButton(field);\n    await pumpAndSettle();\n  }\n\n  Future<void> changeFieldTypeOfFieldWithName(\n    String name,\n    FieldType type, {\n    ViewLayoutPB layout = ViewLayoutPB.Grid,\n  }) async {\n    await tapGridFieldWithName(name);\n    if (layout == ViewLayoutPB.Grid) {\n      await tapEditFieldButton();\n    }\n\n    await tapSwitchFieldTypeButton();\n    await selectFieldType(type);\n    await dismissFieldEditor();\n  }\n\n  Future<void> changeFieldIcon(String icon) async {\n    await tapButton(find.byType(FieldEditIconButton));\n    if (icon.isEmpty) {\n      final button = find.descendant(\n        of: find.byType(FlowyIconEmojiPicker),\n        matching: find.text(\n          LocaleKeys.button_remove.tr(),\n        ),\n      );\n      await tapButton(button);\n    } else {\n      final svgContent = kIconGroups?.findSvgContent(icon);\n      await tapButton(\n        find.byWidgetPredicate(\n          (widget) => widget is FlowySvg && widget.svgString == svgContent,\n        ),\n      );\n    }\n  }\n\n  void assertFieldSvg(String name, FieldType fieldType) {\n    final svgFinder = find.byWidgetPredicate(\n      (widget) => widget is FlowySvg && widget.svg == fieldType.svgData,\n    );\n    final fieldButton = find.byWidgetPredicate(\n      (widget) => widget is FieldCellButton && widget.field.name == name,\n    );\n    expect(\n      find.descendant(of: fieldButton, matching: svgFinder),\n      findsOneWidget,\n    );\n  }\n\n  void assertFieldCustomSvg(String name, String svg) {\n    final svgContent = kIconGroups?.findSvgContent(svg);\n    final svgFinder = find.byWidgetPredicate(\n      (widget) => widget is FlowySvg && widget.svgString == svgContent,\n    );\n    final fieldButton = find.byWidgetPredicate(\n      (widget) => widget is FieldCellButton && widget.field.name == name,\n    );\n    expect(\n      find.descendant(of: fieldButton, matching: svgFinder),\n      findsOneWidget,\n    );\n  }\n\n  Future<void> changeCalculateAtIndex(int index, CalculationType type) async {\n    await tap(find.byType(CalculateCell).at(index));\n    await pumpAndSettle();\n\n    await tap(\n      find.descendant(\n        of: find.byType(CalculationTypeItem),\n        matching: find.text(type.label),\n      ),\n    );\n    await pumpAndSettle();\n  }\n\n  /// Should call [tapGridFieldWithName] first.\n  Future<void> tapEditFieldButton() async {\n    await tapButtonWithName(LocaleKeys.grid_field_editProperty.tr());\n    await pumpAndSettle(const Duration(milliseconds: 200));\n  }\n\n  /// Should call [tapGridFieldWithName] first.\n  Future<void> tapDeletePropertyButton() async {\n    final field = find.byWidgetPredicate(\n      (w) => w is FieldActionCell && w.action == FieldAction.delete,\n    );\n    await tapButton(field);\n  }\n\n  /// A SimpleDialog must be shown first, e.g. when deleting a field.\n  Future<void> tapDialogOkButton() async {\n    final field = find.byWidgetPredicate(\n      (w) => w is PrimaryTextButton && w.label == LocaleKeys.button_ok.tr(),\n    );\n    await tapButton(field);\n  }\n\n  /// Should call [tapGridFieldWithName] first.\n  Future<void> tapDuplicatePropertyButton() async {\n    final field = find.byWidgetPredicate(\n      (w) => w is FieldActionCell && w.action == FieldAction.duplicate,\n    );\n    await tapButton(field);\n  }\n\n  Future<void> tapInsertFieldButton({\n    required bool left,\n    required String name,\n  }) async {\n    final field = find.byWidgetPredicate(\n      (widget) =>\n          widget is FieldActionCell &&\n          (left && widget.action == FieldAction.insertLeft ||\n              !left && widget.action == FieldAction.insertRight),\n    );\n    await tapButton(field);\n    await renameField(name);\n  }\n\n  /// Should call [tapGridFieldWithName] first.\n  Future<void> tapHidePropertyButton() async {\n    final field = find.byWidgetPredicate(\n      (w) => w is FieldActionCell && w.action == FieldAction.toggleVisibility,\n    );\n    await tapButton(field);\n  }\n\n  Future<void> tapHidePropertyButtonInFieldEditor() async {\n    final button = find.byWidgetPredicate(\n      (w) => w is FieldActionCell && w.action == FieldAction.toggleVisibility,\n    );\n    await tapButton(button);\n  }\n\n  Future<void> tapClearCellsButton() async {\n    final field = find.byWidgetPredicate(\n      (widget) =>\n          widget is FieldActionCell && widget.action == FieldAction.clearData,\n    );\n    await tapButton(field);\n  }\n\n  Future<void> tapRowDetailPageRowActionButton() async =>\n      tapButton(find.byType(RowActionButton));\n\n  Future<void> tapRowDetailPageCreatePropertyButton() async =>\n      tapButton(find.byType(CreateRowFieldButton));\n\n  Future<void> tapRowDetailPageDeleteRowButton() async =>\n      tapButton(find.byType(RowDetailPageDeleteButton));\n\n  Future<void> tapRowDetailPageDuplicateRowButton() async =>\n      tapButton(find.byType(RowDetailPageDuplicateButton));\n\n  Future<void> tapSwitchFieldTypeButton() async =>\n      tapButton(find.byType(SwitchFieldButton));\n\n  Future<void> tapEscButton() async => sendKeyEvent(LogicalKeyboardKey.escape);\n\n  /// Must call [tapSwitchFieldTypeButton] first.\n  Future<void> selectFieldType(FieldType fieldType) async {\n    final fieldTypeCell = find.byType(FieldTypeCell);\n    final fieldTypeButton = find.descendant(\n      of: fieldTypeCell,\n      matching: find.byWidgetPredicate(\n        (widget) => widget is FlowyText && widget.text == fieldType.i18n,\n      ),\n    );\n    await tapButton(fieldTypeButton);\n  }\n\n  // Use in edit mode of FieldEditor\n  void expectEmptyTypeOptionEditor() => expect(\n        find.descendant(\n          of: find.byType(FieldTypeOptionEditor),\n          matching: find.byType(TypeOptionSeparator),\n        ),\n        findsNothing,\n      );\n\n  /// Each field has its own cell, so we can find the corresponding cell by\n  /// the field type after create a new field.\n  void findCellByFieldType(FieldType fieldType) {\n    final finder = finderForFieldType(fieldType);\n    expect(finder, findsWidgets);\n  }\n\n  void assertNumberOfRowsInGridPage(int num) {\n    expect(\n      find.byType(GridRow, skipOffstage: false),\n      findsNWidgets(num),\n    );\n  }\n\n  Future<void> assertDocumentExistInRowDetailPage() async {\n    expect(find.byType(RowDocument), findsOneWidget);\n  }\n\n  /// Check the field type of the [FieldCellButton] is the same as the name.\n  Future<void> assertFieldTypeWithFieldName(String name, FieldType type) async {\n    final field = find.byWidgetPredicate(\n      (widget) =>\n          widget is FieldCellButton &&\n          widget.field.fieldType == type &&\n          widget.field.name == name,\n    );\n\n    expect(field, findsOneWidget);\n  }\n\n  void assertFirstFieldInRowDetailByType(FieldType fieldType) {\n    final firstField = find\n        .descendant(\n          of: find.byType(RowDetailPage),\n          matching: find.byType(FieldCellButton),\n        )\n        .first;\n\n    final widget = this.widget<FieldCellButton>(firstField);\n    expect(widget.field.fieldType, fieldType);\n  }\n\n  void findFieldWithName(String name) {\n    final field = find.byWidgetPredicate(\n      (widget) => widget is FieldCellButton && widget.field.name == name,\n    );\n    expect(field, findsOneWidget);\n  }\n\n  void noFieldWithName(String name) {\n    final field = find.byWidgetPredicate(\n      (widget) => widget is FieldCellButton && widget.field.name == name,\n    );\n    expect(field, findsNothing);\n  }\n\n  Future<void> renameField(String newName) async {\n    final textField = find.byType(FieldNameTextField);\n    expect(textField, findsOneWidget);\n    await enterText(textField, newName);\n    await pumpAndSettle();\n  }\n\n  Future<void> dismissFieldEditor() async {\n    await sendKeyEvent(LogicalKeyboardKey.escape);\n    await pumpAndSettle(const Duration(milliseconds: 200));\n  }\n\n  Future<void> changeFieldWidth(String fieldName, double width) async {\n    final field = find.byWidgetPredicate(\n      (widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName,\n    );\n    await hoverOnWidget(\n      field,\n      onHover: () async {\n        final dragHandle = find.descendant(\n          of: field,\n          matching: find.byType(DragToExpandLine),\n        );\n        await drag(dragHandle, Offset(width - getSize(field).width, 0));\n        await pumpAndSettle(const Duration(milliseconds: 200));\n      },\n    );\n  }\n\n  double getFieldWidth(String fieldName) {\n    final field = find.byWidgetPredicate(\n      (widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName,\n    );\n\n    return getSize(field).width;\n  }\n\n  Future<void> findDateEditor(dynamic matcher) async {\n    final finder = find.byType(DateCellEditor);\n    expect(finder, matcher);\n  }\n\n  Future<void> findMediaCellEditor(dynamic matcher) async {\n    final finder = find.byType(MediaCellEditor);\n    expect(finder, matcher);\n  }\n\n  Future<void> findSelectOptionEditor(dynamic matcher) async {\n    final finder = find.byType(SelectOptionCellEditor);\n    expect(finder, matcher);\n  }\n\n  Future<void> dismissCellEditor() async {\n    await sendKeyEvent(LogicalKeyboardKey.escape);\n    await pumpAndSettle();\n  }\n\n  Future<void> tapCreateRowButtonInGrid() async {\n    await tapButton(find.byType(GridAddRowButton));\n  }\n\n  Future<void> tapCreateRowButtonAfterHoveringOnGridRow() async {\n    await tapButton(find.byType(InsertRowButton));\n  }\n\n  Future<void> tapRowMenuButtonInGrid() async {\n    await tapButton(find.byType(RowMenuButton));\n  }\n\n  /// Should call [tapRowMenuButtonInGrid] first.\n  Future<void> tapCreateRowAboveButtonInRowMenu() async {\n    await tapButtonWithName(LocaleKeys.grid_row_insertRecordAbove.tr());\n  }\n\n  /// Should call [tapRowMenuButtonInGrid] first.\n  Future<void> tapDeleteOnRowMenu() async {\n    await tapButtonWithName(LocaleKeys.grid_row_delete.tr());\n  }\n\n  Future<void> reorderRow(\n    String from,\n    String to,\n  ) async {\n    final fromRow = find.byWidgetPredicate(\n      (widget) => widget is GridRow && widget.rowId == from,\n    );\n    final toRow = find.byWidgetPredicate(\n      (widget) => widget is GridRow && widget.rowId == to,\n    );\n    await hoverOnWidget(\n      fromRow,\n      onHover: () async {\n        final dragElement = find.descendant(\n          of: fromRow,\n          matching: find.byType(ReorderableDragStartListener),\n        );\n        await timedDrag(\n          dragElement,\n          getCenter(toRow) - getCenter(fromRow),\n          const Duration(milliseconds: 200),\n        );\n        await pumpAndSettle();\n      },\n    );\n  }\n\n  Future<void> createField(\n    FieldType fieldType, {\n    String? name,\n    ViewLayoutPB layout = ViewLayoutPB.Grid,\n  }) async {\n    if (layout == ViewLayoutPB.Grid) {\n      await scrollToRight(find.byType(GridPage));\n    }\n    await tapNewPropertyButton();\n    if (name != null) {\n      await renameField(name);\n    }\n    await tapSwitchFieldTypeButton();\n    await selectFieldType(fieldType);\n  }\n\n  Future<void> tapDatabaseSettingButton() async {\n    await tapButton(find.byType(SettingButton));\n  }\n\n  Future<void> tapDatabaseFilterButton() async {\n    await tapButton(find.byType(FilterButton));\n  }\n\n  Future<void> tapDatabaseSortButton() async {\n    await tapButton(find.byType(SortButton));\n  }\n\n  Future<void> tapCreateFilterByFieldType(FieldType type, String title) async {\n    final findFilter = find.byWidgetPredicate(\n      (widget) =>\n          widget is FilterableFieldButton &&\n          widget.fieldInfo.fieldType == type &&\n          widget.fieldInfo.name == title,\n    );\n    await tapButton(findFilter);\n  }\n\n  Future<void> tapFilterButtonInGrid(String name) async {\n    final button = find.byWidgetPredicate(\n      (widget) => widget is ChoiceChipButton && widget.fieldInfo.name == name,\n    );\n    await tapButton(button);\n  }\n\n  Future<void> tapCreateSortByFieldType(FieldType type, String title) async {\n    final findSort = find.byWidgetPredicate(\n      (widget) =>\n          widget is GridSortPropertyCell &&\n          widget.fieldInfo.fieldType == type &&\n          widget.fieldInfo.name == title,\n    );\n    await tapButton(findSort);\n  }\n\n  // Must call [tapSortMenuInSettingBar] first.\n  Future<void> tapCreateSortByFieldTypeInSortMenu(\n    FieldType fieldType,\n    String title,\n  ) async {\n    await tapButton(find.byType(DatabaseAddSortButton));\n\n    final findSort = find.byWidgetPredicate(\n      (widget) =>\n          widget is GridSortPropertyCell &&\n          widget.fieldInfo.fieldType == fieldType &&\n          widget.fieldInfo.name == title,\n    );\n\n    await tapButton(findSort);\n    await pumpAndSettle();\n  }\n\n  Future<void> tapSortMenuInSettingBar() async {\n    await tapButton(find.byType(SortMenu));\n    await pumpAndSettle();\n  }\n\n  /// Must call [tapSortMenuInSettingBar] first.\n  Future<void> tapEditSortConditionButtonByFieldName(String name) async {\n    final sortItem = find.descendant(\n      of: find.ancestor(\n        of: find.text(name),\n        matching: find.byType(DatabaseSortItem),\n      ),\n      matching: find.byType(SortConditionButton),\n    );\n    await tapButton(sortItem);\n  }\n\n  /// Must call [tapSortMenuInSettingBar] first.\n  Future<void> reorderSort(\n    (FieldType, String) from,\n    (FieldType, String) to,\n  ) async {\n    final fromSortItem = find.ancestor(\n      of: find.text(from.$2),\n      matching: find.byType(DatabaseSortItem),\n    );\n    final toSortItem = find.ancestor(\n      of: find.text(to.$2),\n      matching: find.byType(DatabaseSortItem),\n    );\n    // final fromSortItem = find.byWidgetPredicate(\n    //   (widget) =>\n    //       widget is DatabaseSortItem &&\n    //       widget.sort.fieldInfo.fieldType == from.$1 &&\n    //       widget.sort.fieldInfo.name == from.$2,\n    // );\n    // final toSortItem = find.byWidgetPredicate(\n    //   (widget) =>\n    //       widget is DatabaseSortItem &&\n    //       widget.sort.fieldInfo.fieldType == to.$1 &&\n    //       widget.sort.fieldInfo.name == to.$2,\n    // );\n    final dragElement = find.descendant(\n      of: fromSortItem,\n      matching: find.byType(ReorderableDragStartListener),\n    );\n    await drag(dragElement, getCenter(toSortItem) - getCenter(fromSortItem));\n    await pumpAndSettle(const Duration(milliseconds: 200));\n  }\n\n  /// Must call [tapEditSortConditionButtonByFieldName] first.\n  Future<void> tapSortByDescending() async {\n    await tapButton(\n      find.byWidgetPredicate(\n        (widget) =>\n            widget is OrderPanelItem &&\n            widget.condition == SortConditionPB.Descending,\n      ),\n    );\n    await sendKeyEvent(LogicalKeyboardKey.escape);\n    await pumpAndSettle();\n  }\n\n  /// Must call [tapSortMenuInSettingBar] first.\n  Future<void> tapDeleteAllSortsButton() async {\n    await tapButton(find.byType(DeleteAllSortsButton));\n  }\n\n  Future<void> scrollOptionFilterListByOffset(Offset offset) async {\n    await drag(find.byType(SelectOptionFilterEditor), offset);\n    await pumpAndSettle();\n  }\n\n  Future<void> enterTextInTextFilter(String text) async {\n    final findEditor = find.byType(TextFilterEditor);\n    final findTextField = find.descendant(\n      of: findEditor,\n      matching: find.byType(FlowyTextField),\n    );\n\n    await enterText(findTextField, text);\n    await pumpAndSettle(const Duration(milliseconds: 300));\n  }\n\n  Future<void> tapDisclosureButtonInFinder(Finder finder) async {\n    final findDisclosure = find.descendant(\n      of: finder,\n      matching: find.byType(DisclosureButton),\n    );\n\n    await tapButton(findDisclosure);\n  }\n\n  /// must call [tapDisclosureButtonInFinder] first.\n  Future<void> tapDeleteFilterButtonInGrid() async {\n    await tapButton(find.text(LocaleKeys.grid_settings_deleteFilter.tr()));\n  }\n\n  Future<void> tapCheckboxFilterButtonInGrid() async {\n    await tapButton(find.byType(CheckboxFilterConditionList));\n  }\n\n  Future<void> tapChecklistFilterButtonInGrid() async {\n    await tapButton(find.byType(ChecklistFilterConditionList));\n  }\n\n  /// The [SelectOptionFilterList] must show up first.\n  Future<void> tapOptionFilterWithName(String name) async {\n    final findCell = find.descendant(\n      of: find.byType(SelectOptionFilterList),\n      matching: find.byWidgetPredicate(\n        (widget) =>\n            widget is SelectOptionFilterCell && widget.option.name == name,\n        skipOffstage: false,\n      ),\n      skipOffstage: false,\n    );\n    expect(findCell, findsOneWidget);\n    await tapButton(findCell);\n  }\n\n  Future<void> tapUnCheckedButtonOnCheckboxFilter() async {\n    final button = find.descendant(\n      of: find.byType(HoverButton),\n      matching: find.text(LocaleKeys.grid_checkboxFilter_isUnchecked.tr()),\n    );\n\n    await tapButton(button);\n  }\n\n  Future<void> tapCompletedButtonOnChecklistFilter() async {\n    final button = find.descendant(\n      of: find.byType(HoverButton),\n      matching: find.text(LocaleKeys.grid_checklistFilter_isComplete.tr()),\n    );\n\n    await tapButton(button);\n  }\n\n  Future<void> changeTextFilterCondition(\n    TextFilterConditionPB condition,\n  ) async {\n    await tapButton(find.byType(TextFilterConditionList));\n    final button = find.descendant(\n      of: find.byType(HoverButton),\n      matching: find.text(\n        condition.filterName,\n      ),\n    );\n\n    await tapButton(button);\n  }\n\n  Future<void> changeSelectFilterCondition(\n    SelectOptionFilterConditionPB condition,\n  ) async {\n    await tapButton(find.byType(SelectOptionFilterConditionList));\n    final button = find.descendant(\n      of: find.byType(HoverButton),\n      matching: find.text(condition.i18n),\n    );\n\n    await tapButton(button);\n  }\n\n  Future<void> changeDateFilterCondition(\n    DateTimeFilterCondition condition,\n  ) async {\n    await tapButton(find.byType(DateFilterConditionList));\n    final button = find.descendant(\n      of: find.byType(HoverButton),\n      matching: find.text(condition.filterName),\n    );\n\n    await tapButton(button);\n  }\n\n  /// Should call [tapDatabaseSettingButton] first.\n  Future<void> tapViewPropertiesButton() async {\n    final findSettingItem = find.byType(DatabaseSettingsList);\n    final findLayoutButton = find.byWidgetPredicate(\n      (widget) =>\n          widget is FlowyText &&\n          widget.text == DatabaseSettingAction.showProperties.title(),\n    );\n\n    final button = find.descendant(\n      of: findSettingItem,\n      matching: findLayoutButton,\n    );\n\n    await tapButton(button);\n  }\n\n  /// Should call [tapDatabaseSettingButton] first.\n  Future<void> tapDatabaseLayoutButton() async {\n    final findSettingItem = find.byType(DatabaseSettingsList);\n    final findLayoutButton = find.byWidgetPredicate(\n      (widget) =>\n          widget is FlowyText &&\n          widget.text == DatabaseSettingAction.showLayout.title(),\n    );\n\n    final button = find.descendant(\n      of: findSettingItem,\n      matching: findLayoutButton,\n    );\n\n    await tapButton(button);\n  }\n\n  Future<void> tapCalendarLayoutSettingButton() async {\n    final findSettingItem = find.byType(DatabaseSettingsList);\n    final findLayoutButton = find.byWidgetPredicate(\n      (widget) =>\n          widget is FlowyText &&\n          widget.text == DatabaseSettingAction.showCalendarLayout.title(),\n    );\n\n    final button = find.descendant(\n      of: findSettingItem,\n      matching: findLayoutButton,\n    );\n\n    await tapButton(button);\n  }\n\n  Future<void> tapFirstDayOfWeek() async =>\n      tapButton(find.byType(FirstDayOfWeek));\n\n  Future<void> tapFirstDayOfWeekStartFromMonday() async {\n    final finder = find.byWidgetPredicate(\n      (widget) => widget is StartFromButton && widget.dayIndex == 1,\n    );\n    await tapButton(finder);\n\n    // Dismiss the popover overlay in cause of obscure the tapButton\n    // in the next test case.\n    await sendKeyEvent(LogicalKeyboardKey.escape);\n    await pumpAndSettle(const Duration(milliseconds: 200));\n  }\n\n  void assertFirstDayOfWeekStartFromMonday() {\n    final finder = find.byWidgetPredicate(\n      (w) => w is StartFromButton && w.dayIndex == 1 && w.isSelected == true,\n    );\n    expect(finder, findsOneWidget);\n  }\n\n  void assertFirstDayOfWeekStartFromSunday() {\n    final finder = find.byWidgetPredicate(\n      (w) => w is StartFromButton && w.dayIndex == 0 && w.isSelected == true,\n    );\n    expect(finder, findsOneWidget);\n  }\n\n  Future<void> scrollToToday() async {\n    final todayCell = find.byWidgetPredicate(\n      (widget) => widget is CalendarDayCard && widget.isToday,\n    );\n    final scrollable = find\n        .descendant(\n          of: find.byType(MonthView<CalendarDayEvent>),\n          matching: find.byWidgetPredicate(\n            (widget) => widget is Scrollable && widget.axis == Axis.vertical,\n          ),\n        )\n        .first;\n    await scrollUntilVisible(todayCell, 300, scrollable: scrollable);\n    await pumpAndSettle(const Duration(milliseconds: 300));\n  }\n\n  Future<void> hoverOnTodayCalendarCell({\n    Future<void> Function()? onHover,\n  }) async {\n    final todayCell = find.byWidgetPredicate(\n      (widget) => widget is CalendarDayCard && widget.isToday,\n    );\n\n    await hoverOnWidget(todayCell, onHover: onHover);\n  }\n\n  Future<void> tapAddCalendarEventButton() async {\n    final findFlowyButton = find.byType(FlowyIconButton);\n    final findNewEventButton = find.byType(NewEventButton);\n    final button = find.descendant(\n      of: findNewEventButton,\n      matching: findFlowyButton,\n    );\n    await tapButton(button);\n  }\n\n  /// Checks for a certain number of events. Parameters [date] and [title] can\n  /// also be provided to restrict the scope of the search\n  void assertNumberOfEventsInCalendar(int number, {String? title}) {\n    Finder findEvents = find.byType(EventCard);\n    if (title != null) {\n      findEvents = find.descendant(of: findEvents, matching: find.text(title));\n    }\n    expect(findEvents, findsNWidgets(number));\n  }\n\n  void assertNumberOfEventsOnSpecificDay(\n    int number,\n    DateTime date, {\n    String? title,\n  }) {\n    final findDayCell = find.byWidgetPredicate(\n      (widget) => widget is CalendarDayCard && isSameDay(widget.date, date),\n    );\n    Finder findEvents = find.descendant(\n      of: findDayCell,\n      matching: find.byType(EventCard),\n    );\n    if (title != null) {\n      findEvents = find.descendant(of: findEvents, matching: find.text(title));\n    }\n    expect(findEvents, findsNWidgets(number));\n  }\n\n  Future<void> doubleClickCalendarCell(DateTime date) async {\n    final todayCell = find.byWidgetPredicate(\n      (widget) => widget is CalendarDayCard && isSameDay(date, widget.date),\n    );\n    final location = getTopLeft(todayCell).translate(10, 10);\n    await doubleTapAt(location);\n  }\n\n  Future<void> openCalendarEvent({required int index, DateTime? date}) async {\n    final findDayCell = find.byWidgetPredicate(\n      (widget) =>\n          widget is CalendarDayCard &&\n          isSameDay(widget.date, date ?? DateTime.now()),\n    );\n    final cards = find.descendant(\n      of: findDayCell,\n      matching: find.byType(EventCard),\n    );\n\n    await tapButton(cards.at(index), milliseconds: 1000);\n  }\n\n  void assertEventEditorOpen() =>\n      expect(find.byType(CalendarEventEditor), findsOneWidget);\n\n  Future<void> dismissEventEditor() async =>\n      simulateKeyEvent(LogicalKeyboardKey.escape);\n\n  Future<void> editEventTitle(String title) async {\n    final textField = find.descendant(\n      of: find.byType(CalendarEventEditor),\n      matching: find.byType(FlowyTextField),\n    );\n\n    await enterText(textField, title);\n    await testTextInput.receiveAction(TextInputAction.done);\n    await pumpAndSettle(const Duration(milliseconds: 300));\n  }\n\n  Future<void> openEventToRowDetailPage() async {\n    final button = find.descendant(\n      of: find.byType(CalendarEventEditor),\n      matching: find.byWidgetPredicate(\n        (widget) => widget is FlowySvg && widget.svg == FlowySvgs.full_view_s,\n      ),\n    );\n\n    await tapButton(button);\n  }\n\n  Future<void> deleteEventFromEventEditor() async {\n    final button = find.descendant(\n      of: find.byType(CalendarEventEditor),\n      matching: find.byWidgetPredicate(\n        (widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s,\n      ),\n    );\n\n    await tapButton(button);\n    await tapButtonWithName(LocaleKeys.button_delete.tr());\n  }\n\n  Future<void> dragDropRescheduleCalendarEvent() async {\n    final findEventCard = find.byType(EventCard);\n    await drag(findEventCard.first, const Offset(0, 300));\n    await pumpAndSettle(const Duration(microseconds: 300));\n  }\n\n  Future<void> openUnscheduledEventsPopup() async {\n    final button = find.byType(UnscheduledEventsButton);\n    await tapButton(button);\n  }\n\n  void findUnscheduledPopup(Matcher matcher, int numUnscheduledEvents) {\n    expect(find.byType(UnscheduleEventsList), matcher);\n    if (matcher != findsNothing) {\n      expect(\n        find.byType(UnscheduledEventCell),\n        findsNWidgets(numUnscheduledEvents),\n      );\n    }\n  }\n\n  Future<void> clickUnscheduledEvent() async {\n    final unscheduledEvent = find.byType(UnscheduledEventCell);\n    await tapButton(unscheduledEvent);\n  }\n\n  Future<void> tapCreateLinkedDatabaseViewButton(\n    DatabaseLayoutPB layoutType,\n  ) async {\n    final findAddButton = find.byType(AddDatabaseViewButton);\n    await tapButton(findAddButton);\n\n    final findCreateButton = find.byWidgetPredicate(\n      (widget) =>\n          widget is TabBarAddButtonActionCell && widget.action == layoutType,\n    );\n    await tapButton(findCreateButton);\n  }\n\n  void assertNumberOfGroups(int number) {\n    final groups = find.byType(BoardColumnHeader, skipOffstage: false);\n    expect(groups, findsNWidgets(number));\n  }\n\n  Future<void> scrollBoardToEnd() async {\n    final scrollable = find\n        .descendant(\n          of: find.byType(AppFlowyBoard),\n          matching: find.byWidgetPredicate(\n            (widget) => widget is Scrollable && widget.axis == Axis.horizontal,\n          ),\n        )\n        .first;\n    await scrollUntilVisible(\n      find.byType(BoardTrailing),\n      300,\n      scrollable: scrollable,\n    );\n  }\n\n  Future<void> tapNewGroupButton() async {\n    final button = find.descendant(\n      of: find.byType(BoardTrailing),\n      matching: find.byWidgetPredicate(\n        (widget) => widget is FlowySvg && widget.svg == FlowySvgs.add_s,\n      ),\n    );\n    expect(button, findsOneWidget);\n    await tapButton(button);\n  }\n\n  void assertNewGroupTextField(bool isVisible) {\n    final textField = find.descendant(\n      of: find.byType(BoardTrailing),\n      matching: find.byType(TextField),\n    );\n    if (isVisible) {\n      return expect(textField, findsOneWidget);\n    }\n    expect(textField, findsNothing);\n  }\n\n  Future<void> enterNewGroupName(String name, {required bool submit}) async {\n    final textField = find.descendant(\n      of: find.byType(BoardTrailing),\n      matching: find.byType(TextField),\n    );\n    await enterText(textField, name);\n    await pumpAndSettle();\n    if (submit) {\n      await testTextInput.receiveAction(TextInputAction.done);\n      await pumpAndSettle();\n    }\n  }\n\n  Future<void> clearNewGroupTextField() async {\n    final textField = find.descendant(\n      of: find.byType(BoardTrailing),\n      matching: find.byType(TextField),\n    );\n    await tapButton(\n      find.descendant(\n        of: textField,\n        matching: find.byWidgetPredicate(\n          (widget) =>\n              widget is FlowySvg && widget.svg == FlowySvgs.close_filled_s,\n        ),\n      ),\n    );\n    final textFieldWidget = widget<TextField>(textField);\n    assert(\n      textFieldWidget.controller != null &&\n          textFieldWidget.controller!.text.isEmpty,\n    );\n  }\n\n  Future<void> tapTabBarLinkedViewByViewName(String name) async {\n    final viewButton = findTabBarLinkViewByViewName(name);\n    await tapButton(viewButton);\n  }\n\n  Finder findTabBarLinkViewByViewLayout(ViewLayoutPB layout) {\n    return find.byWidgetPredicate(\n      (widget) => widget is TabBarItemButton && widget.view.layout == layout,\n    );\n  }\n\n  Finder findTabBarLinkViewByViewName(String name) {\n    return find.byWidgetPredicate(\n      (widget) => widget is TabBarItemButton && widget.view.name == name,\n    );\n  }\n\n  Future<void> renameLinkedView(Finder linkedView, String name) async {\n    await tap(linkedView, buttons: kSecondaryButton);\n    await pumpAndSettle();\n\n    await tapButton(\n      find.byWidgetPredicate(\n        (widget) =>\n            widget is ActionCellWidget &&\n            widget.action == TabBarViewAction.rename,\n      ),\n    );\n\n    await enterText(\n      find.descendant(\n        of: find.byType(AFTextFieldDialog),\n        matching: find.byType(AFTextField),\n      ),\n      name,\n    );\n\n    await tapButton(find.text(LocaleKeys.button_confirm.tr()));\n  }\n\n  Future<void> deleteDatebaseView(Finder linkedView) async {\n    await tap(linkedView, buttons: kSecondaryButton);\n    await pumpAndSettle();\n\n    await tapButton(\n      find.byWidgetPredicate(\n        (widget) =>\n            widget is ActionCellWidget &&\n            widget.action == TabBarViewAction.delete,\n      ),\n    );\n\n    final okButton = find.byWidgetPredicate(\n      (widget) =>\n          widget is PrimaryTextButton &&\n          widget.label == LocaleKeys.button_ok.tr(),\n    );\n    await tapButton(okButton);\n  }\n\n  void assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) => switch (layout) {\n        DatabaseLayoutPB.Board =>\n          expect(find.byType(DesktopBoardPage), findsOneWidget),\n        DatabaseLayoutPB.Calendar =>\n          expect(find.byType(CalendarPage), findsOneWidget),\n        DatabaseLayoutPB.Grid => expect(find.byType(GridPage), findsOneWidget),\n        _ => throw Exception('Unknown database layout type: $layout'),\n      };\n\n  Future<void> selectDatabaseLayoutType(DatabaseLayoutPB layout) async {\n    final findLayoutCell = find.byType(DatabaseViewLayoutCell);\n    final findText = find.byWidgetPredicate(\n      (widget) => widget is FlowyText && widget.text == layout.layoutName,\n    );\n\n    final button = find.descendant(of: findLayoutCell, matching: findText);\n    await tapButton(button);\n  }\n\n  Future<void> assertCurrentDatabaseLayoutType(DatabaseLayoutPB layout) async {\n    expect(finderForDatabaseLayoutType(layout), findsOneWidget);\n  }\n\n  Future<void> tapDatabaseRawDataButton() async {\n    await tapButtonWithName(LocaleKeys.importPanel_database.tr());\n  }\n\n  // Use in edit mode of FieldEditor\n  Future<void> changeNumberFieldFormat() async {\n    final changeFormatButton = find.descendant(\n      of: find.byType(FieldTypeOptionEditor),\n      matching: find.text(\"Number\"),\n    );\n    await tapButton(changeFormatButton);\n\n    await tapButton(\n      find.byWidgetPredicate(\n        (w) => w is NumberFormatCell && w.format == NumberFormatPB.USD,\n      ),\n    );\n  }\n\n  // Use in edit mode of FieldEditor\n  Future<void> tapAddSelectOptionButton() async {\n    await tapButtonWithName(LocaleKeys.grid_field_addSelectOption.tr());\n  }\n\n  Future<void> tapViewTogglePropertyVisibilityButtonByName(\n    String fieldName,\n  ) async {\n    final field = find.byWidgetPredicate(\n      (w) => w is DatabasePropertyCell && w.fieldInfo.name == fieldName,\n    );\n    final toggleVisibilityButton =\n        find.descendant(of: field, matching: find.byType(FlowyIconButton));\n    await tapButton(toggleVisibilityButton);\n  }\n}\n\nFinder finderForDatabaseLayoutType(DatabaseLayoutPB layout) => switch (layout) {\n      DatabaseLayoutPB.Board => find.byType(DesktopBoardPage),\n      DatabaseLayoutPB.Calendar => find.byType(CalendarPage),\n      DatabaseLayoutPB.Grid => find.byType(GridPage),\n      _ => throw Exception('Unknown database layout type: $layout'),\n    };\n\nFinder finderForFieldType(FieldType fieldType) {\n  switch (fieldType) {\n    case FieldType.Checkbox:\n      return find.byType(EditableCheckboxCell, skipOffstage: false);\n    case FieldType.DateTime:\n      return find.byType(EditableDateCell, skipOffstage: false);\n    case FieldType.LastEditedTime:\n      return find.byWidgetPredicate(\n        (widget) =>\n            widget is EditableTimestampCell &&\n            widget.fieldType == FieldType.LastEditedTime,\n        skipOffstage: false,\n      );\n    case FieldType.CreatedTime:\n      return find.byWidgetPredicate(\n        (widget) =>\n            widget is EditableTimestampCell &&\n            widget.fieldType == FieldType.CreatedTime,\n        skipOffstage: false,\n      );\n    case FieldType.SingleSelect:\n      return find.byWidgetPredicate(\n        (widget) =>\n            widget is EditableSelectOptionCell &&\n            widget.fieldType == FieldType.SingleSelect,\n        skipOffstage: false,\n      );\n    case FieldType.MultiSelect:\n      return find.byWidgetPredicate(\n        (widget) =>\n            widget is EditableSelectOptionCell &&\n            widget.fieldType == FieldType.MultiSelect,\n        skipOffstage: false,\n      );\n    case FieldType.Checklist:\n      return find.byType(EditableChecklistCell, skipOffstage: false);\n    case FieldType.Number:\n      return find.byType(EditableNumberCell, skipOffstage: false);\n    case FieldType.RichText:\n      return find.byType(EditableTextCell, skipOffstage: false);\n    case FieldType.URL:\n      return find.byType(EditableURLCell, skipOffstage: false);\n    case FieldType.Media:\n      return find.byType(EditableMediaCell, skipOffstage: false);\n    default:\n      throw Exception('Unknown field type: $fieldType');\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/dir.dart",
    "content": "import 'dart:io';\n\nimport 'package:archive/archive.dart';\nimport 'package:path/path.dart' as p;\n\nFuture<void> deleteDirectoriesWithSameBaseNameAsPrefix(\n  String path,\n) async {\n  final dir = Directory(path);\n  final prefix = p.basename(dir.path);\n  final parentDir = dir.parent;\n\n  // Check if the directory exists\n  if (!await parentDir.exists()) {\n    // ignore: avoid_print\n    print('Directory does not exist');\n    return;\n  }\n\n  // List all entities in the directory\n  await for (final entity in parentDir.list()) {\n    // Check if the entity is a directory and starts with the specified prefix\n    if (entity is Directory && p.basename(entity.path).startsWith(prefix)) {\n      try {\n        await entity.delete(recursive: true);\n      } catch (e) {\n        // ignore: avoid_print\n        print('Failed to delete directory: ${entity.path}, Error: $e');\n      }\n    }\n  }\n}\n\nFuture<void> unzipFile(File zipFile, Directory targetDirectory) async {\n  // Read the Zip file from disk.\n  final bytes = zipFile.readAsBytesSync();\n\n  // Decode the Zip file\n  final archive = ZipDecoder().decodeBytes(bytes);\n\n  // Extract the contents of the Zip archive to disk.\n  for (final file in archive) {\n    final filename = file.name;\n    if (file.isFile) {\n      final data = file.content as List<int>;\n      File(p.join(targetDirectory.path, filename))\n        ..createSync(recursive: true)\n        ..writeAsBytesSync(data);\n    } else {\n      Directory(p.join(targetDirectory.path, filename))\n          .createSync(recursive: true);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/document_test_operations.dart",
    "content": "import 'dart:async';\nimport 'dart:ui';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';\nimport 'package:appflowy/plugins/base/emoji/emoji_picker.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_title.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';\nimport 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_emoji_mart/flutter_emoji_mart.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'util.dart';\n\nextension EditorWidgetTester on WidgetTester {\n  EditorOperations get editor => EditorOperations(this);\n}\n\nclass EditorOperations {\n  const EditorOperations(this.tester);\n\n  final WidgetTester tester;\n\n  EditorState getCurrentEditorState() =>\n      tester.widget<AppFlowyEditor>(find.byType(AppFlowyEditor)).editorState;\n\n  Node getNodeAtPath(Path path) {\n    final editorState = getCurrentEditorState();\n    return editorState.getNodeAtPath(path)!;\n  }\n\n  /// Tap the line of editor at [index]\n  Future<void> tapLineOfEditorAt(int index) async {\n    final textBlocks = find.byType(AppFlowyRichText);\n    index = index.clamp(0, textBlocks.evaluate().length - 1);\n    final center = tester.getCenter(textBlocks.at(index));\n    final right = tester.getTopRight(textBlocks.at(index));\n    final centerRight = Offset(right.dx, center.dy);\n    await tester.tapAt(centerRight);\n    await tester.pumpAndSettle();\n  }\n\n  /// Hover on cover plugin button above the document\n  Future<void> hoverOnCoverToolbar() async {\n    final coverToolbar = find.byType(DocumentHeaderToolbar);\n    await tester.startGesture(\n      tester.getBottomLeft(coverToolbar).translate(5, -5),\n      kind: PointerDeviceKind.mouse,\n    );\n    await tester.pumpAndSettle();\n  }\n\n  /// Taps on the 'Add Icon' button in the cover toolbar\n  Future<void> tapAddIconButton() async {\n    await tester.tapButtonWithName(\n      LocaleKeys.document_plugins_cover_addIcon.tr(),\n    );\n    expect(find.byType(FlowyEmojiPicker), findsOneWidget);\n  }\n\n  Future<void> paste() async {\n    if (UniversalPlatform.isMacOS) {\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isMetaPressed: true,\n      );\n    } else {\n      await tester.simulateKeyEvent(\n        LogicalKeyboardKey.keyV,\n        isControlPressed: true,\n      );\n    }\n  }\n\n  Future<void> tapGettingStartedIcon() async {\n    await tester.tapButton(\n      find.descendant(\n        of: find.byType(DocumentCoverWidget),\n        matching: find.findTextInFlowyText('⭐️'),\n      ),\n    );\n  }\n\n  /// Taps on the 'Skin tone' button\n  ///\n  /// Must call [tapAddIconButton] first.\n  Future<void> changeEmojiSkinTone(EmojiSkinTone skinTone) async {\n    await tester.tapButton(\n      find.byTooltip(LocaleKeys.emoji_selectSkinTone.tr()),\n    );\n    final skinToneButton = find.byKey(emojiSkinToneKey(skinTone.icon));\n    await tester.tapButton(skinToneButton);\n  }\n\n  /// Taps the 'Remove Icon' button in the cover toolbar and the icon popover\n  Future<void> tapRemoveIconButton({bool isInPicker = false}) async {\n    final Finder button = !isInPicker\n        ? find.text(LocaleKeys.document_plugins_cover_removeIcon.tr())\n        : find.descendant(\n            of: find.byType(FlowyIconEmojiPicker),\n            matching: find.text(LocaleKeys.button_remove.tr()),\n          );\n    await tester.tapButton(button);\n  }\n\n  /// Requires that the document must already have an icon. This opens the icon\n  /// picker\n  Future<void> tapOnIconWidget() async {\n    final iconWidget = find.byType(EmojiIconWidget);\n    await tester.tapButton(iconWidget);\n  }\n\n  Future<void> tapOnAddCover() async {\n    await tester.tapButtonWithName(\n      LocaleKeys.document_plugins_cover_addCover.tr(),\n    );\n  }\n\n  Future<void> tapOnChangeCover() async {\n    await tester.tapButtonWithName(\n      LocaleKeys.document_plugins_cover_changeCover.tr(),\n    );\n  }\n\n  Future<void> switchSolidColorBackground() async {\n    final findPurpleButton = find.byWidgetPredicate(\n      (widget) => widget is ColorItem && widget.option.name == 'Purple',\n    );\n    await tester.tapButton(findPurpleButton);\n  }\n\n  Future<void> addNetworkImageCover(String imageUrl) async {\n    final embedLinkButton = find.findTextInFlowyText(\n      LocaleKeys.document_imageBlock_embedLink_label.tr(),\n    );\n    await tester.tapButton(embedLinkButton);\n\n    final imageUrlTextField = find.descendant(\n      of: find.byType(EmbedImageUrlWidget),\n      matching: find.byType(TextField),\n    );\n    await tester.enterText(imageUrlTextField, imageUrl);\n    await tester.pumpAndSettle();\n    await tester.tapButton(\n      find.descendant(\n        of: find.byType(EmbedImageUrlWidget),\n        matching: find.findTextInFlowyText(\n          LocaleKeys.document_imageBlock_embedLink_label.tr(),\n        ),\n      ),\n    );\n  }\n\n  Future<void> tapOnRemoveCover() async =>\n      tester.tapButton(find.byType(DeleteCoverButton));\n\n  /// A cover must be present in the document to function properly since this\n  /// catches all cover types collectively\n  Future<void> hoverOnCover() async {\n    final cover = find.byType(DocumentCover);\n    await tester.startGesture(\n      tester.getCenter(cover),\n      kind: PointerDeviceKind.mouse,\n    );\n    await tester.pumpAndSettle();\n  }\n\n  Future<void> dismissCoverPicker() async {\n    await tester.sendKeyEvent(LogicalKeyboardKey.escape);\n    await tester.pumpAndSettle();\n  }\n\n  /// trigger the slash command (selection menu)\n  Future<void> showSlashMenu() async {\n    await tester.ime.insertCharacter('/');\n  }\n\n  /// trigger the mention (@) command\n  Future<void> showAtMenu() async {\n    await tester.ime.insertCharacter('@');\n  }\n\n  /// trigger the plus action menu (+) command\n  Future<void> showPlusMenu() async {\n    await tester.ime.insertCharacter('+');\n  }\n\n  /// Tap the slash menu item with [name]\n  ///\n  /// Must call [showSlashMenu] first.\n  Future<void> tapSlashMenuItemWithName(\n    String name, {\n    double offset = 200,\n  }) async {\n    final slashMenu = find\n        .ancestor(\n          of: find.byType(SelectionMenuItemWidget),\n          matching: find.byWidgetPredicate(\n            (widget) => widget is Scrollable,\n          ),\n        )\n        .first;\n    final slashMenuItem = find.text(name, findRichText: true);\n    await tester.scrollUntilVisible(\n      slashMenuItem,\n      offset,\n      scrollable: slashMenu,\n      duration: const Duration(milliseconds: 250),\n    );\n    assert(slashMenuItem.hasFound);\n    await tester.tapButton(slashMenuItem);\n  }\n\n  /// Tap the at menu item with [name]\n  ///\n  /// Must call [showAtMenu] first.\n  Future<void> tapAtMenuItemWithName(String name) async {\n    final atMenuItem = find.descendant(\n      of: find.byType(InlineActionsHandler),\n      matching: find.text(name, findRichText: true),\n    );\n    await tester.tapButton(atMenuItem);\n  }\n\n  /// Update the editor's selection\n  Future<void> updateSelection(Selection? selection) async {\n    final editorState = getCurrentEditorState();\n    unawaited(\n      editorState.updateSelectionWithReason(\n        selection,\n        reason: SelectionUpdateReason.uiEvent,\n      ),\n    );\n    await tester.pumpAndSettle(const Duration(milliseconds: 200));\n  }\n\n  /// hover and click on the + button beside the block component.\n  Future<void> hoverAndClickOptionAddButton(\n    Path path,\n    bool withModifiedKey, // alt on windows or linux, option on macos\n  ) async {\n    final optionAddButton = find.byWidgetPredicate(\n      (widget) =>\n          widget is BlockComponentActionWrapper &&\n          widget.node.path.equals(path),\n    );\n    await tester.hoverOnWidget(\n      optionAddButton,\n      onHover: () async {\n        if (withModifiedKey) {\n          await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);\n        }\n        await tester.tapButton(\n          find.byWidgetPredicate(\n            (widget) =>\n                widget is BlockAddButton &&\n                widget.blockComponentContext.node.path.equals(path),\n          ),\n        );\n        if (withModifiedKey) {\n          await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);\n        }\n      },\n    );\n  }\n\n  /// hover and click on the option menu button beside the block component.\n  Future<void> hoverAndClickOptionMenuButton(Path path) async {\n    final optionMenuButton = find.byWidgetPredicate(\n      (widget) =>\n          widget is BlockComponentActionWrapper &&\n          widget.node.path.equals(path),\n    );\n    await tester.hoverOnWidget(\n      optionMenuButton,\n      onHover: () async {\n        await tester.tapButton(\n          find.byWidgetPredicate(\n            (widget) =>\n                widget is BlockOptionButton &&\n                widget.blockComponentContext.node.path.equals(path),\n          ),\n        );\n        await tester.pumpUntilFound(find.byType(PopoverActionList));\n      },\n    );\n  }\n\n  /// open the turn into menu\n  Future<void> openTurnIntoMenu(Path path) async {\n    await hoverAndClickOptionMenuButton(path);\n    await tester.tapButton(\n      find\n          .findTextInFlowyText(\n            LocaleKeys.document_plugins_optionAction_turnInto.tr(),\n          )\n          .first,\n    );\n    await tester.pumpUntilFound(find.byType(TurnIntoOptionMenu));\n  }\n\n  /// copy link to block\n  Future<void> copyLinkToBlock(Path path) async {\n    await hoverAndClickOptionMenuButton(path);\n    await tester.tapButton(\n      find.findTextInFlowyText(\n        LocaleKeys.document_plugins_optionAction_copyLinkToBlock.tr(),\n      ),\n    );\n  }\n\n  Future<void> openDepthMenu(Path path) async {\n    await hoverAndClickOptionMenuButton(path);\n    await tester.tapButton(\n      find.findTextInFlowyText(\n        LocaleKeys.document_plugins_optionAction_depth.tr(),\n      ),\n    );\n    await tester.pumpUntilFound(find.byType(DepthOptionMenu));\n  }\n\n  /// Drag block\n  ///\n  /// [offset] is the offset to move the block.\n  ///\n  /// [path] is the path of the block to move.\n  Future<void> dragBlock(\n    Path path,\n    Offset offset,\n  ) async {\n    final dragToMoveAction = find.byWidgetPredicate(\n      (widget) =>\n          widget is DraggableOptionButton &&\n          widget.blockComponentContext.node.path.equals(path),\n    );\n\n    await tester.hoverOnWidget(\n      dragToMoveAction,\n      onHover: () async {\n        final dragToMoveTooltip = find.findFlowyTooltip(\n          LocaleKeys.blockActions_dragTooltip.tr(),\n        );\n        await tester.pumpUntilFound(dragToMoveTooltip);\n        final location = tester.getCenter(dragToMoveAction);\n        final gesture = await tester.startGesture(\n          location,\n          pointer: 7,\n        );\n        await tester.pump();\n\n        // divide the steps to small move to avoid the drag area not found error\n        const steps = 5;\n        final stepOffset = Offset(offset.dx / steps, offset.dy / steps);\n\n        for (var i = 0; i < steps; i++) {\n          await gesture.moveBy(stepOffset);\n          await tester.pump(Durations.short1);\n        }\n\n        // check if the drag to move action is dragging\n        expect(\n          isDraggingAppFlowyEditorBlock.value,\n          isTrue,\n        );\n\n        await gesture.up();\n        await tester.pump();\n      },\n    );\n    await tester.pumpAndSettle(Durations.short1);\n  }\n\n  Finder findDocumentTitle(String? title) {\n    final parent = UniversalPlatform.isDesktop\n        ? find.byType(CoverTitle)\n        : find.byType(DocumentImmersiveCover);\n\n    return find.descendant(\n      of: parent,\n      matching: find.byWidgetPredicate(\n        (widget) {\n          if (widget is! TextField) {\n            return false;\n          }\n\n          if (widget.controller?.text == title) {\n            return true;\n          }\n\n          if (title == null) {\n            return true;\n          }\n\n          if (title.isEmpty) {\n            return widget.controller?.text.isEmpty ?? false;\n          }\n\n          return false;\n        },\n      ),\n    );\n  }\n\n  /// open the more action menu on mobile\n  Future<void> openMoreActionMenuOnMobile() async {\n    final moreActionButton = find.byType(MobileViewPageMoreButton);\n    await tester.tapButton(moreActionButton);\n    await tester.pumpAndSettle();\n  }\n\n  /// click the more action item on mobile\n  ///\n  /// rename, add collaborator, publish, delete, etc.\n  Future<void> clickMoreActionItemOnMobile(String name) async {\n    final moreActionItem = find.descendant(\n      of: find.byType(MobileQuickActionButton),\n      matching: find.findTextInFlowyText(name),\n    );\n    await tester.tapButton(moreActionItem);\n    await tester.pumpAndSettle();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/emoji.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_color_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_uploader.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';\nimport 'package:desktop_drop/desktop_drop.dart';\nimport 'package:flowy_infra_ui/style_widget/primary_rounded_button.dart';\nimport 'package:flowy_svg/flowy_svg.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_emoji_mart/flutter_emoji_mart.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'base.dart';\nimport 'common_operations.dart';\n\nextension EmojiTestExtension on WidgetTester {\n  Future<void> tapEmoji(String emoji) async {\n    final emojiWidget = find.descendant(\n      of: find.byType(EmojiPicker),\n      matching: find.text(emoji),\n    );\n    await tapButton(emojiWidget);\n  }\n\n  Future<void> tapIcon(EmojiIconData icon, {bool enableColor = true}) async {\n    final iconsData = IconsData.fromJson(jsonDecode(icon.emoji));\n    final pickTab = find.byType(PickerTab);\n    expect(pickTab, findsOneWidget);\n    await pumpAndSettle();\n    final iconTab = find.descendant(\n      of: pickTab,\n      matching: find.text(PickerTabType.icon.tr),\n    );\n    expect(iconTab, findsOneWidget);\n    await tapButton(iconTab);\n    final selectedSvg = find.descendant(\n      of: find.byType(FlowyIconPicker),\n      matching: find.byWidgetPredicate(\n        (w) => w is FlowySvg && w.svgString == iconsData.svgString,\n      ),\n    );\n\n    await tapButton(selectedSvg.first);\n    if (enableColor) {\n      final colorPicker = find.byType(IconColorPicker);\n      expect(colorPicker, findsOneWidget);\n      final selectedColor = find.descendant(\n        of: colorPicker,\n        matching: find.byWidgetPredicate((w) {\n          if (w is Container) {\n            final d = w.decoration;\n            if (d is ShapeDecoration) {\n              if (d.color ==\n                  Color(\n                    int.parse(iconsData.color ?? builtInSpaceColors.first),\n                  )) {\n                return true;\n              }\n            }\n          }\n          return false;\n        }),\n      );\n      await tapButton(selectedColor);\n    }\n  }\n\n  Future<void> pickImage(EmojiIconData icon) async {\n    final pickTab = find.byType(PickerTab);\n    expect(pickTab, findsOneWidget);\n    await pumpAndSettle();\n\n    /// switch to custom tab\n    final iconTab = find.descendant(\n      of: pickTab,\n      matching: find.text(PickerTabType.custom.tr),\n    );\n    expect(iconTab, findsOneWidget);\n    await tapButton(iconTab);\n\n    /// mock for dragging image\n    final dropTarget = find.descendant(\n      of: find.byType(IconUploader),\n      matching: find.byType(DropTarget),\n    );\n    expect(dropTarget, findsOneWidget);\n    final dropTargetWidget = dropTarget.evaluate().first.widget as DropTarget;\n    dropTargetWidget.onDragDone?.call(\n      DropDoneDetails(\n        files: [DropItemFile(icon.emoji)],\n        localPosition: Offset.zero,\n        globalPosition: Offset.zero,\n      ),\n    );\n    await pumpAndSettle(const Duration(seconds: 3));\n\n    /// confirm to upload\n    final confirmButton = find.descendant(\n      of: find.byType(IconUploader),\n      matching: find.byType(PrimaryRoundedButton),\n    );\n    await tapButton(confirmButton);\n  }\n\n  Future<void> pasteImageLinkAsIcon(String link) async {\n    final pickTab = find.byType(PickerTab);\n    expect(pickTab, findsOneWidget);\n    await pumpAndSettle();\n\n    /// switch to custom tab\n    final iconTab = find.descendant(\n      of: pickTab,\n      matching: find.text(PickerTabType.custom.tr),\n    );\n    expect(iconTab, findsOneWidget);\n    await tapButton(iconTab);\n\n    // mock the clipboard\n    await getIt<ClipboardService>()\n        .setData(ClipboardServiceData(plainText: link));\n\n    // paste the link\n    await simulateKeyEvent(\n      LogicalKeyboardKey.keyV,\n      isControlPressed: Platform.isLinux || Platform.isWindows,\n      isMetaPressed: Platform.isMacOS,\n    );\n    await pumpAndSettle(const Duration(seconds: 5));\n\n    /// confirm to upload\n    final confirmButton = find.descendant(\n      of: find.byType(IconUploader),\n      matching: find.byType(PrimaryRoundedButton),\n    );\n    await tapButton(confirmButton);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/expectation.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';\nimport 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_detail.dart';\nimport 'package:appflowy/plugins/document/presentation/banner.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy/shared/appflowy_network_svg.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_stack.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';\nimport 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_svg/flowy_svg.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:string_validator/string_validator.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'util.dart';\n\n// const String readme = 'Read me';\nconst String gettingStarted = 'Getting started';\n\nextension Expectation on WidgetTester {\n  /// Expect to see the home page and with a default read me page.\n  Future<void> expectToSeeHomePageWithGetStartedPage() async {\n    if (UniversalPlatform.isDesktopOrWeb) {\n      final finder = find.byType(HomeStack);\n      await pumpUntilFound(finder);\n      expect(finder, findsOneWidget);\n    } else if (UniversalPlatform.isMobile) {\n      final finder = find.byType(MobileHomePage);\n      await pumpUntilFound(finder);\n      expect(finder, findsOneWidget);\n    }\n\n    final docFinder = find.textContaining(gettingStarted);\n    await pumpUntilFound(docFinder);\n  }\n\n  Future<void> expectToSeeHomePage() async {\n    final finder = find.byType(HomeStack);\n    await pumpUntilFound(finder);\n    expect(finder, findsOneWidget);\n  }\n\n  /// Expect to see the page name on the home page.\n  void expectToSeePageName(\n    String name, {\n    String? parentName,\n    ViewLayoutPB layout = ViewLayoutPB.Document,\n    ViewLayoutPB parentLayout = ViewLayoutPB.Document,\n  }) {\n    final pageName = findPageName(\n      name,\n      layout: layout,\n      parentName: parentName,\n      parentLayout: parentLayout,\n    );\n    expect(pageName, findsOneWidget);\n  }\n\n  /// Expect not to see the page name on the home page.\n  void expectNotToSeePageName(\n    String name, {\n    String? parentName,\n    ViewLayoutPB layout = ViewLayoutPB.Document,\n    ViewLayoutPB parentLayout = ViewLayoutPB.Document,\n  }) {\n    final pageName = findPageName(\n      name,\n      layout: layout,\n      parentName: parentName,\n      parentLayout: parentLayout,\n    );\n    expect(pageName, findsNothing);\n  }\n\n  /// Expect to see the document banner.\n  void expectToSeeDocumentBanner() {\n    expect(find.byType(DocumentBanner), findsOneWidget);\n  }\n\n  /// Expect not to see the document banner.\n  void expectNotToSeeDocumentBanner() {\n    expect(find.byType(DocumentBanner), findsNothing);\n  }\n\n  /// Expect to the markdown file export success dialog.\n  void expectToExportSuccess() {\n    final exportSuccess = find.text(\n      LocaleKeys.settings_files_exportFileSuccess.tr(),\n    );\n    expect(exportSuccess, findsOneWidget);\n  }\n\n  /// Expect to see the document header toolbar empty\n  void expectToSeeEmptyDocumentHeaderToolbar() {\n    final addCover = find.textContaining(\n      LocaleKeys.document_plugins_cover_addCover.tr(),\n    );\n    final addIcon = find.textContaining(\n      LocaleKeys.document_plugins_cover_addIcon.tr(),\n    );\n    expect(addCover, findsNothing);\n    expect(addIcon, findsNothing);\n  }\n\n  void expectToSeeDocumentIcon(String? emoji) {\n    if (emoji == null) {\n      final iconWidget = find.byType(EmojiIconWidget);\n      expect(iconWidget, findsNothing);\n      return;\n    }\n    final iconWidget = find.byWidgetPredicate(\n      (widget) => widget is EmojiIconWidget && widget.emoji.emoji == emoji,\n    );\n    expect(iconWidget, findsOneWidget);\n  }\n\n  void expectDocumentIconNotNull() {\n    final iconWidget = find.byWidgetPredicate(\n      (widget) => widget is EmojiIconWidget && widget.emoji.isNotEmpty,\n    );\n    expect(iconWidget, findsOneWidget);\n  }\n\n  void expectToSeeDocumentCover(CoverType type) {\n    final findCover = find.byWidgetPredicate(\n      (widget) => widget is DocumentCover && widget.coverType == type,\n    );\n    expect(findCover, findsOneWidget);\n  }\n\n  void expectToSeeNoDocumentCover() {\n    final findCover = find.byType(DocumentCover);\n    expect(findCover, findsNothing);\n  }\n\n  void expectChangeCoverAndDeleteButton() {\n    final findChangeCover = find.text(\n      LocaleKeys.document_plugins_cover_changeCover.tr(),\n    );\n    final findRemoveIcon = find.byType(DeleteCoverButton);\n    expect(findChangeCover, findsOneWidget);\n    expect(findRemoveIcon, findsOneWidget);\n  }\n\n  /// Expect to see a text\n  void expectToSeeText(String text) {\n    Finder textWidget = find.textContaining(text, findRichText: true);\n    if (textWidget.evaluate().isEmpty) {\n      textWidget = find.byWidgetPredicate(\n        (widget) => widget is FlowyText && widget.text == text,\n      );\n    }\n    expect(textWidget, findsOneWidget);\n  }\n\n  /// Find if the page is favorite\n  Finder findFavoritePageName(\n    String name, {\n    ViewLayoutPB layout = ViewLayoutPB.Document,\n    String? parentName,\n    ViewLayoutPB parentLayout = ViewLayoutPB.Document,\n  }) =>\n      find.byWidgetPredicate(\n        (widget) =>\n            widget is SingleInnerViewItem &&\n            widget.view.isFavorite &&\n            widget.spaceType == FolderSpaceType.favorite &&\n            widget.view.name == name &&\n            widget.view.layout == layout,\n        skipOffstage: false,\n      );\n\n  Finder findAllFavoritePages() => find.byWidgetPredicate(\n        (widget) =>\n            widget is SingleInnerViewItem &&\n            widget.view.isFavorite &&\n            widget.spaceType == FolderSpaceType.favorite,\n      );\n\n  Finder findPageName(\n    String name, {\n    ViewLayoutPB layout = ViewLayoutPB.Document,\n    String? parentName,\n    ViewLayoutPB parentLayout = ViewLayoutPB.Document,\n  }) {\n    if (UniversalPlatform.isDesktop) {\n      if (parentName == null) {\n        return find.byWidgetPredicate(\n          (widget) =>\n              widget is SingleInnerViewItem &&\n              widget.view.name == name &&\n              widget.view.layout == layout,\n          skipOffstage: false,\n        );\n      }\n\n      return find.descendant(\n        of: find.byWidgetPredicate(\n          (widget) =>\n              widget is InnerViewItem &&\n              widget.view.name == parentName &&\n              widget.view.layout == parentLayout,\n          skipOffstage: false,\n        ),\n        matching: findPageName(name, layout: layout),\n      );\n    }\n\n    return find.byWidgetPredicate(\n      (widget) =>\n          widget is SingleMobileInnerViewItem &&\n          widget.view.name == name &&\n          widget.view.layout == layout,\n      skipOffstage: false,\n    );\n  }\n\n  void expectViewHasIcon(String name, ViewLayoutPB layout, EmojiIconData data) {\n    final pageName = findPageName(\n      name,\n      layout: layout,\n    );\n    final type = data.type;\n    if (type == FlowyIconType.emoji) {\n      final icon = find.descendant(\n        of: pageName,\n        matching: find.text(data.emoji),\n      );\n      expect(icon, findsOneWidget);\n    } else if (type == FlowyIconType.icon) {\n      final iconsData = IconsData.fromJson(jsonDecode(data.emoji));\n      final icon = find.descendant(\n        of: pageName,\n        matching: find.byWidgetPredicate(\n          (w) => w is FlowySvg && w.svgString == iconsData.svgString,\n        ),\n      );\n      expect(icon, findsOneWidget);\n    } else if (type == FlowyIconType.custom) {\n      final isSvg = data.emoji.endsWith('.svg');\n      if (isURL(data.emoji)) {\n        final image = find.descendant(\n          of: pageName,\n          matching: isSvg\n              ? find.byType(FlowyNetworkSvg)\n              : find.byType(FlowyNetworkImage),\n        );\n        expect(image, findsOneWidget);\n      } else {\n        final image = find.descendant(\n          of: pageName,\n          matching: isSvg ? find.byType(SvgPicture) : find.byType(Image),\n        );\n        expect(image, findsOneWidget);\n      }\n    }\n  }\n\n  void expectViewTitleHasIcon(\n    String name,\n    ViewLayoutPB layout,\n    EmojiIconData data,\n  ) {\n    final type = data.type;\n    if (type == FlowyIconType.emoji) {\n      final icon = find.descendant(\n        of: find.byType(ViewTitleBar),\n        matching: find.text(data.emoji),\n      );\n      expect(icon, findsOneWidget);\n    } else if (type == FlowyIconType.icon) {\n      final iconsData = IconsData.fromJson(jsonDecode(data.emoji));\n      final icon = find.descendant(\n        of: find.byType(ViewTitleBar),\n        matching: find.byWidgetPredicate(\n          (w) => w is FlowySvg && w.svgString == iconsData.svgString,\n        ),\n      );\n      expect(icon, findsOneWidget);\n    } else if (type == FlowyIconType.custom) {\n      final isSvg = data.emoji.endsWith('.svg');\n      if (isURL(data.emoji)) {\n        final image = find.descendant(\n          of: find.byType(ViewTitleBar),\n          matching: isSvg\n              ? find.byType(FlowyNetworkSvg)\n              : find.byType(FlowyNetworkImage),\n        );\n        expect(image, findsOneWidget);\n      } else {\n        final image = find.descendant(\n          of: find.byType(ViewTitleBar),\n          matching: isSvg\n              ? find.byWidgetPredicate((w) {\n                  if (w is! SvgPicture) return false;\n                  final loader = w.bytesLoader;\n                  if (loader is! SvgFileLoader) return false;\n                  return loader.file.path.endsWith('.svg');\n                })\n              : find.byType(Image),\n        );\n        expect(image, findsOneWidget);\n      }\n    }\n  }\n\n  void expectSelectedReminder(ReminderOption option) {\n    final findSelectedText = find.descendant(\n      of: find.byType(ReminderSelector),\n      matching: find.text(option.label),\n    );\n\n    expect(findSelectedText, findsOneWidget);\n  }\n\n  void expectNotificationItems(int amount) {\n    final findItems = find.byType(NotificationItem);\n\n    expect(findItems, findsNWidgets(amount));\n  }\n\n  void expectToSeeRowDetailsPageDialog() {\n    expect(\n      find.descendant(\n        of: find.byType(RowDetailPage),\n        matching: find.byType(SimpleDialog),\n      ),\n      findsOneWidget,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/ime.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nextension IME on WidgetTester {\n  IMESimulator get ime => IMESimulator(this);\n}\n\nclass IMESimulator {\n  IMESimulator(this.tester) {\n    client = findTextInputClient();\n  }\n\n  final WidgetTester tester;\n  late final TextInputClient client;\n\n  Future<void> insertText(String text) async {\n    for (final c in text.characters) {\n      await insertCharacter(c);\n    }\n  }\n\n  Future<void> insertCharacter(String character) async {\n    final value = client.currentTextEditingValue;\n    if (value == null) {\n      assert(false);\n      return;\n    }\n    final text = value.text\n        .replaceRange(value.selection.start, value.selection.end, character);\n    final textEditingValue = TextEditingValue(\n      text: text,\n      selection: TextSelection.collapsed(\n        offset: value.selection.baseOffset + 1,\n      ),\n    );\n    client.updateEditingValue(textEditingValue);\n    await tester.pumpAndSettle();\n  }\n\n  TextInputClient findTextInputClient() {\n    final finder = find.byType(KeyboardServiceWidget);\n    final KeyboardServiceWidgetState state = tester.state(finder);\n    return state.textInputService as TextInputClient;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/keyboard.dart",
    "content": "import 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart' as flutter_test;\n\nclass FlowyTestKeyboard {\n  static Future<void> simulateKeyDownEvent(\n    List<LogicalKeyboardKey> keys, {\n    required flutter_test.WidgetTester tester,\n    bool withKeyUp = false,\n  }) async {\n    for (final LogicalKeyboardKey key in keys) {\n      await flutter_test.simulateKeyDownEvent(key);\n      await tester.pumpAndSettle();\n    }\n\n    if (withKeyUp) {\n      for (final LogicalKeyboardKey key in keys) {\n        await flutter_test.simulateKeyUpEvent(key);\n        await tester.pumpAndSettle();\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/mock/mock_ai.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/ai/service/ai_entities.dart';\nimport 'package:appflowy/ai/service/appflowy_ai_service.dart';\nimport 'package:appflowy/ai/service/error.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart';\nimport 'package:mocktail/mocktail.dart';\n\nfinal _mockAiMap = <CompletionTypePB, Map<String, List<String>>>{\n  CompletionTypePB.ImproveWriting: {\n    \"I have an apple\": [\n      \"I\",\n      \"have\",\n      \"an\",\n      \"apple\",\n      \"and\",\n      \"a\",\n      \"banana\",\n    ],\n  },\n  CompletionTypePB.SpellingAndGrammar: {\n    \"We didn’t had enough money\": [\n      \"We\",\n      \"didn’t\",\n      \"have\",\n      \"enough\",\n      \"money\",\n    ],\n  },\n  CompletionTypePB.UserQuestion: {\n    \"Explain the concept of TPU\": [\n      \"TPU\",\n      \"is\",\n      \"a\",\n      \"tensor\",\n      \"processing\",\n      \"unit\",\n      \"that\",\n      \"is\",\n      \"designed\",\n      \"to\",\n      \"accelerate\",\n      \"machine\",\n    ],\n    \"How about GPU?\": [\n      \"GPU\",\n      \"is\",\n      \"a\",\n      \"graphics\",\n      \"processing\",\n      \"unit\",\n      \"that\",\n      \"is\",\n      \"designed\",\n      \"to\",\n      \"accelerate\",\n      \"machine\",\n      \"learning\",\n      \"tasks\",\n    ],\n  },\n};\n\nabstract class StreamCompletionValidator {\n  bool validate(\n    String text,\n    String? objectId,\n    CompletionTypePB completionType,\n    PredefinedFormat? format,\n    List<String> sourceIds,\n    List<AiWriterRecord> history,\n  );\n}\n\nclass MockCompletionStream extends Mock implements CompletionStream {}\n\nclass MockAIRepository extends Mock implements AppFlowyAIService {\n  MockAIRepository({this.validator});\n  StreamCompletionValidator? validator;\n\n  @override\n  Future<(String, CompletionStream)?> streamCompletion({\n    String? objectId,\n    required String text,\n    PredefinedFormat? format,\n    String? promptId,\n    List<String> sourceIds = const [],\n    List<AiWriterRecord> history = const [],\n    required CompletionTypePB completionType,\n    required Future<void> Function() onStart,\n    required Future<void> Function(String text) processMessage,\n    required Future<void> Function(String text) processAssistMessage,\n    required Future<void> Function() onEnd,\n    required void Function(AIError error) onError,\n    required void Function(LocalAIStreamingState state)\n        onLocalAIStreamingStateChange,\n  }) async {\n    if (validator != null) {\n      if (!validator!.validate(\n        text,\n        objectId,\n        completionType,\n        format,\n        sourceIds,\n        history,\n      )) {\n        throw Exception('Invalid completion');\n      }\n    }\n    final stream = MockCompletionStream();\n    unawaited(\n      Future(() async {\n        await onStart();\n        final lines = _mockAiMap[completionType]?[text.trim()];\n\n        if (lines == null) {\n          throw Exception('No mock ai found for $text and $completionType');\n        }\n\n        for (final line in lines) {\n          await processMessage('$line ');\n        }\n        await onEnd();\n      }),\n    );\n    return ('mock_id', stream);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/mock/mock_file_picker.dart",
    "content": "import 'package:appflowy/startup/startup.dart';\nimport 'package:flowy_infra/file_picker/file_picker_service.dart';\n\nclass MockFilePicker implements FilePickerService {\n  MockFilePicker({\n    this.mockPath = '',\n    this.mockPaths = const [],\n  });\n\n  final String mockPath;\n  final List<String> mockPaths;\n\n  @override\n  Future<String?> getDirectoryPath({String? title}) => Future.value(mockPath);\n\n  @override\n  Future<String?> saveFile({\n    String? dialogTitle,\n    String? fileName,\n    String? initialDirectory,\n    FileType type = FileType.any,\n    List<String>? allowedExtensions,\n    bool lockParentWindow = false,\n  }) =>\n      Future.value(mockPath);\n\n  @override\n  Future<FilePickerResult?> pickFiles({\n    String? dialogTitle,\n    String? initialDirectory,\n    FileType type = FileType.any,\n    List<String>? allowedExtensions,\n    Function(FilePickerStatus p1)? onFileLoading,\n    bool allowCompression = true,\n    bool allowMultiple = false,\n    bool withData = false,\n    bool withReadStream = false,\n    bool lockParentWindow = false,\n  }) {\n    final platformFiles =\n        mockPaths.map((e) => PlatformFile(path: e, name: '', size: 0)).toList();\n    return Future.value(FilePickerResult(platformFiles));\n  }\n}\n\nFuture<void> mockGetDirectoryPath(String path) async {\n  getIt.unregister<FilePickerService>();\n  getIt.registerFactory<FilePickerService>(\n    () => MockFilePicker(mockPath: path),\n  );\n}\n\nFuture<String> mockSaveFilePath(String path) async {\n  getIt.unregister<FilePickerService>();\n  getIt.registerFactory<FilePickerService>(\n    () => MockFilePicker(mockPath: path),\n  );\n  return path;\n}\n\nList<String> mockPickFilePaths({required List<String> paths}) {\n  getIt.unregister<FilePickerService>();\n  getIt.registerFactory<FilePickerService>(\n    () => MockFilePicker(mockPaths: paths),\n  );\n  return paths;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/mock/mock_url_launcher.dart",
    "content": "// Copyright 2013 The Flutter Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style license that can be\n// found in the LICENSE file.\n\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:plugin_platform_interface/plugin_platform_interface.dart';\nimport 'package:url_launcher_platform_interface/link.dart';\nimport 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';\n\nclass MockUrlLauncher extends Fake\n    with MockPlatformInterfaceMixin\n    implements UrlLauncherPlatform {\n  String? url;\n  PreferredLaunchMode? launchMode;\n  bool? useSafariVC;\n  bool? useWebView;\n  bool? enableJavaScript;\n  bool? enableDomStorage;\n  bool? universalLinksOnly;\n  Map<String, String>? headers;\n  String? webOnlyWindowName;\n\n  bool? response;\n\n  bool closeWebViewCalled = false;\n  bool canLaunchCalled = false;\n  bool launchCalled = false;\n\n  // ignore: use_setters_to_change_properties\n  void setCanLaunchExpectations(String url) => this.url = url;\n\n  void setLaunchExpectations({\n    required String url,\n    PreferredLaunchMode? launchMode,\n    bool? useSafariVC,\n    bool? useWebView,\n    required bool enableJavaScript,\n    required bool enableDomStorage,\n    required bool universalLinksOnly,\n    required Map<String, String> headers,\n    required String? webOnlyWindowName,\n  }) {\n    this.url = url;\n    this.launchMode = launchMode;\n    this.useSafariVC = useSafariVC;\n    this.useWebView = useWebView;\n    this.enableJavaScript = enableJavaScript;\n    this.enableDomStorage = enableDomStorage;\n    this.universalLinksOnly = universalLinksOnly;\n    this.headers = headers;\n    this.webOnlyWindowName = webOnlyWindowName;\n  }\n\n  void setResponse(bool response) => this.response = response;\n\n  @override\n  LinkDelegate? get linkDelegate => null;\n\n  @override\n  Future<bool> canLaunch(String url) async {\n    expect(url, this.url);\n    canLaunchCalled = true;\n    return response!;\n  }\n\n  @override\n  Future<bool> launch(\n    String url, {\n    required bool useSafariVC,\n    required bool useWebView,\n    required bool enableJavaScript,\n    required bool enableDomStorage,\n    required bool universalLinksOnly,\n    required Map<String, String> headers,\n    String? webOnlyWindowName,\n  }) async {\n    expect(url, this.url);\n    expect(useSafariVC, this.useSafariVC);\n    expect(useWebView, this.useWebView);\n    expect(enableJavaScript, this.enableJavaScript);\n    expect(enableDomStorage, this.enableDomStorage);\n    expect(universalLinksOnly, this.universalLinksOnly);\n    expect(headers, this.headers);\n    expect(webOnlyWindowName, this.webOnlyWindowName);\n    launchCalled = true;\n    return response!;\n  }\n\n  @override\n  Future<bool> launchUrl(String url, LaunchOptions options) async {\n    expect(url, this.url);\n    expect(options.mode, launchMode);\n    expect(options.webViewConfiguration.enableJavaScript, enableJavaScript);\n    expect(options.webViewConfiguration.enableDomStorage, enableDomStorage);\n    expect(options.webViewConfiguration.headers, headers);\n    expect(options.webOnlyWindowName, webOnlyWindowName);\n    launchCalled = true;\n    return response!;\n  }\n\n  @override\n  Future<void> closeWebView() async => closeWebViewCalled = true;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/settings.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/settings/prelude.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/account_user_profile.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart';\nimport 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'base.dart';\nimport 'common_operations.dart';\n\nextension AppFlowySettings on WidgetTester {\n  /// Open settings page\n  Future<void> openSettings() async {\n    final settingsDialog = find.byType(SettingsDialog);\n    // tap empty area to close the settings page\n    while (settingsDialog.evaluate().isNotEmpty) {\n      await tapAt(Offset.zero);\n      await pumpAndSettle();\n    }\n\n    final settingsButton = find.byType(UserSettingButton);\n    expect(settingsButton, findsOneWidget);\n    await tapButton(settingsButton);\n\n    expect(settingsDialog, findsOneWidget);\n    return;\n  }\n\n  /// Open the page that insides the settings page\n  Future<void> openSettingsPage(SettingsPage page) async {\n    final button = find.byWidgetPredicate(\n      (widget) => widget is SettingsMenuElement && widget.page == page,\n    );\n\n    await scrollUntilVisible(\n      button,\n      0,\n      scrollable: find.findSettingsMenuScrollable(),\n    );\n    await pump();\n\n    expect(button, findsOneWidget);\n    await tapButton(button);\n    return;\n  }\n\n  /// Restore the AppFlowy data storage location\n  Future<void> restoreLocation() async {\n    final button = find.text(LocaleKeys.settings_common_reset.tr());\n    expect(button, findsOneWidget);\n    await tapButton(button);\n    await pumpAndSettle();\n\n    final confirmButton = find.text(LocaleKeys.button_confirm.tr());\n    expect(confirmButton, findsOneWidget);\n    await tapButton(confirmButton);\n    return;\n  }\n\n  Future<void> tapCustomLocationButton() async {\n    final button = find.byTooltip(\n      LocaleKeys.settings_files_changeLocationTooltips.tr(),\n    );\n    expect(button, findsOneWidget);\n    await tapButton(button);\n    return;\n  }\n\n  /// Enter user name\n  Future<void> enterUserName(String name) async {\n    // Enable editing username\n    final editUsernameFinder = find.descendant(\n      of: find.byType(AccountUserProfile),\n      matching: find.byFlowySvg(FlowySvgs.toolbar_link_edit_m),\n    );\n    await tap(editUsernameFinder, warnIfMissed: false);\n    await pumpAndSettle();\n\n    final userNameFinder = find.descendant(\n      of: find.byType(AccountUserProfile),\n      matching: find.byType(TextField),\n    );\n    await enterText(userNameFinder, name);\n    await pumpAndSettle();\n\n    await tap(find.text(LocaleKeys.button_save.tr()));\n    await pumpAndSettle();\n  }\n\n  // go to settings page and toggle enable RTL toolbar items\n  Future<void> toggleEnableRTLToolbarItems() async {\n    await openSettings();\n    await openSettingsPage(SettingsPage.workspace);\n\n    final scrollable = find.findSettingsScrollable();\n    await scrollUntilVisible(\n      find.byType(EnableRTLItemsSwitcher),\n      0,\n      scrollable: scrollable,\n    );\n\n    final switcher = find.descendant(\n      of: find.byType(EnableRTLItemsSwitcher),\n      matching: find.byType(Toggle),\n    );\n\n    await tap(switcher);\n\n    // tap anywhere to close the settings page\n    await tapAt(Offset.zero);\n    await pumpAndSettle();\n  }\n\n  Future<void> updateNamespace(String namespace) async {\n    final dialog = find.byType(DomainSettingsDialog);\n    expect(dialog, findsOneWidget);\n\n    // input the new namespace\n    await enterText(\n      find.descendant(\n        of: dialog,\n        matching: find.byType(TextField),\n      ),\n      namespace,\n    );\n    await tapButton(find.text(LocaleKeys.button_save.tr()));\n    await pumpAndSettle();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/util.dart",
    "content": "export 'auth_operation.dart';\nexport 'base.dart';\nexport 'common_operations.dart';\nexport 'data.dart';\nexport 'document_test_operations.dart';\nexport 'expectation.dart';\nexport 'ime.dart';\nexport 'mock/mock_url_launcher.dart';\nexport 'settings.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/integration_test/shared/workspace.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'util.dart';\n\nextension AppFlowyWorkspace on WidgetTester {\n  /// Open workspace menu\n  Future<void> openWorkspaceMenu() async {\n    final workspaceWrapper = find.byType(SidebarSwitchWorkspaceButton);\n    expect(workspaceWrapper, findsOneWidget);\n    await tapButton(workspaceWrapper);\n    final workspaceMenu = find.byType(WorkspacesMenu);\n    expect(workspaceMenu, findsOneWidget);\n  }\n\n  /// Open a workspace\n  Future<void> openWorkspace(String name) async {\n    final workspace = find.descendant(\n      of: find.byType(WorkspaceMenuItem),\n      matching: find.findTextInFlowyText(name),\n    );\n    expect(workspace, findsOneWidget);\n    await tapButton(workspace);\n  }\n\n  Future<void> changeWorkspaceName(String name) async {\n    final menuItem = find.byType(WorkspaceMenuItem);\n    expect(menuItem, findsOneWidget);\n    await hoverOnWidget(\n      menuItem,\n      onHover: () async {\n        await tapButton(\n          find.descendant(\n            of: menuItem,\n            matching: find.byType(WorkspaceMoreActionList),\n          ),\n        );\n        await tapButton(find.text(LocaleKeys.button_rename.tr()));\n        final input = find.descendant(\n          of: find.byType(AFTextFieldDialog),\n          matching: find.byType(AFTextField),\n        );\n        await enterText(input, name);\n        await tapButton(find.text(LocaleKeys.button_confirm.tr()));\n      },\n    );\n  }\n\n  Future<void> changeWorkspaceIcon(String icon) async {\n    final iconButton = find.descendant(\n      of: find.byType(WorkspaceMenuItem),\n      matching: find.byType(WorkspaceIcon),\n    );\n    expect(iconButton, findsOneWidget);\n    await tapButton(iconButton);\n    final iconPicker = find.byType(FlowyIconEmojiPicker);\n    expect(iconPicker, findsOneWidget);\n    await tapButton(find.findTextInFlowyText(icon));\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/.gitignore",
    "content": "*.mode1v3\n*.mode2v3\n*.moved-aside\n*.pbxuser\n*.perspectivev3\n**/*sync/\n.sconsign.dblite\n.tags*\n**/.vagrant/\n**/DerivedData/\nIcon?\n**/Pods/\n**/.symlinks/\nprofile\nxcuserdata\n**/.generated/\nFlutter/App.framework\nFlutter/Flutter.framework\nFlutter/Flutter.podspec\nFlutter/Generated.xcconfig\nFlutter/ephemeral/\nFlutter/app.flx\nFlutter/app.zip\nFlutter/flutter_assets/\nFlutter/flutter_export_environment.sh\nServiceDefinitions.json\nRunner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!default.mode1v3\n!default.mode2v3\n!default.pbxuser\n!default.perspectivev3\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Flutter/AppFrameworkInfo.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>CFBundleDevelopmentRegion</key>\n  <string>en</string>\n  <key>CFBundleExecutable</key>\n  <string>App</string>\n  <key>CFBundleIdentifier</key>\n  <string>io.flutter.flutter.app</string>\n  <key>CFBundleInfoDictionaryVersion</key>\n  <string>6.0</string>\n  <key>CFBundleName</key>\n  <string>App</string>\n  <key>CFBundlePackageType</key>\n  <string>FMWK</string>\n  <key>CFBundleShortVersionString</key>\n  <string>1.0</string>\n  <key>CFBundleSignature</key>\n  <string>????</string>\n  <key>CFBundleVersion</key>\n  <string>1.0</string>\n  <key>MinimumOSVersion</key>\n  <string>12.0</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Flutter/Debug.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Flutter/Release.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Podfile",
    "content": "# Uncomment this line to define a global platform for your project\n# platform :ios, '12.0'\n\n# CocoaPods analytics sends network stats synchronously affecting flutter build latency.\nENV['COCOAPODS_DISABLE_STATS'] = 'true'\n\nproject 'Runner', {\n  'Debug' => :debug,\n  'Profile' => :release,\n  'Release' => :release,\n}\n\ndef flutter_root\n  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)\n  unless File.exist?(generated_xcode_build_settings_path)\n    raise \"#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first\"\n  end\n\n  File.foreach(generated_xcode_build_settings_path) do |line|\n    matches = line.match(/FLUTTER_ROOT\\=(.*)/)\n    return matches[1].strip if matches\n  end\n  raise \"FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get\"\nend\n\nrequire File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)\n\nflutter_ios_podfile_setup\n\ntarget 'Runner' do\n  use_frameworks!\n  use_modular_headers!\n\n  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))\nend\n\npost_install do |installer|\n  installer.pods_project.targets.each do |target|\n    flutter_additional_ios_build_settings(target)\n\n    target.build_configurations.each do |config|\n      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [\n        '$(inherited)',\n\n        # dart: PermissionGroup.photos\n        'PERMISSION_PHOTOS=1',\n      ]\n\n    end\n  end\n\n  installer.aggregate_targets.each do |target|\n    target.xcconfigs.each do |variant, xcconfig|\n      xcconfig_path = target.client_root + target.xcconfig_relative_path(variant)\n      IO.write(xcconfig_path, IO.read(xcconfig_path).gsub(\"DT_TOOLCHAIN_DIR\", \"TOOLCHAIN_DIR\"))\n    end\n  end\n\n  installer.pods_project.targets.each do |target|\n    target.build_configurations.each do |config|\n      if config.base_configuration_reference.is_a? Xcodeproj::Project::Object::PBXFileReference\n        xcconfig_path = config.base_configuration_reference.real_path\n        IO.write(xcconfig_path, IO.read(xcconfig_path).gsub(\"DT_TOOLCHAIN_DIR\", \"TOOLCHAIN_DIR\"))\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner/AppDelegate.swift",
    "content": "import UIKit\nimport Flutter\n\n@main\n@objc class AppDelegate: FlutterAppDelegate {\n  override func application(\n    _ application: UIApplication,\n    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n  ) -> Bool {\n    GeneratedPluginRegistrant.register(with: self)\n    return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\"images\":[{\"size\":\"60x60\",\"expected-size\":\"180\",\"filename\":\"180.png\",\"folder\":\"Assets.xcassets/AppIcon.appiconset/\",\"idiom\":\"iphone\",\"scale\":\"3x\"},{\"size\":\"40x40\",\"expected-size\":\"80\",\"filename\":\"80.png\",\"folder\":\"Assets.xcassets/AppIcon.appiconset/\",\"idiom\":\"iphone\",\"scale\":\"2x\"},{\"size\":\"40x40\",\"expected-size\":\"120\",\"filename\":\"120.png\",\"folder\":\"Assets.xcassets/AppIcon.appiconset/\",\"idiom\":\"iphone\",\"scale\":\"3x\"},{\"size\":\"60x60\",\"expected-size\":\"120\",\"filename\":\"120.png\",\"folder\":\"Assets.xcassets/AppIcon.appiconset/\",\"idiom\":\"iphone\",\"scale\":\"2x\"},{\"size\":\"57x57\",\"expected-size\":\"57\",\"filename\":\"57.png\",\"folder\":\"Assets.xcassets/AppIcon.appiconset/\",\"idiom\":\"iphone\",\"scale\":\"1x\"},{\"size\":\"29x29\",\"expected-size\":\"58\",\"filename\":\"58.png\",\"folder\":\"Assets.xcassets/AppIcon.appiconset/\",\"idiom\":\"iphone\",\"scale\":\"2x\"},{\"size\":\"29x29\",\"expected-size\":\"29\",\"filename\":\"29.png\",\"folder\":\"Assets.xcassets/AppIcon.appiconset/\",\"idiom\":\"iphone\",\"scale\":\"1x\"},{\"size\":\"29x29\",\"expected-size\":\"87\",\"filename\":\"87.png\",\"folder\":\"Assets.xcassets/AppIcon.appiconset/\",\"idiom\":\"iphone\",\"scale\":\"3x\"},{\"size\":\"57x57\",\"expected-size\":\"114\",\"filename\":\"114.png\",\"folder\":\"Assets.xcassets/AppIcon.appiconset/\",\"idiom\":\"iphone\",\"scale\":\"2x\"},{\"size\":\"20x20\",\"expected-size\":\"40\",\"filename\":\"40.png\",\"folder\":\"Assets.xcassets/AppIcon.appiconset/\",\"idiom\":\"iphone\",\"scale\":\"2x\"},{\"size\":\"20x20\",\"expected-size\":\"60\",\"filename\":\"60.png\",\"folder\":\"Assets.xcassets/AppIcon.appiconset/\",\"idiom\":\"iphone\",\"scale\":\"3x\"},{\"size\":\"1024x1024\",\"filename\":\"1024.png\",\"expected-size\":\"1024\",\"idiom\":\"ios-marketing\",\"folder\":\"Assets.xcassets/AppIcon.appiconset/\",\"scale\":\"1x\"}]}"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner/Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@3x.png\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md",
    "content": "# Launch Screen Assets\n\nYou can customize the launch screen with your own desired assets by replacing the image files in this directory.\n\nYou can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images."
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"12121\" systemVersion=\"16G29\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" launchScreen=\"YES\" colorMatched=\"YES\" initialViewController=\"01J-lp-oVM\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"12089\"/>\n    </dependencies>\n    <scenes>\n        <!--View Controller-->\n        <scene sceneID=\"EHf-IW-A2E\">\n            <objects>\n                <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"Ydg-fD-yQy\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"xbc-2k-c8Z\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"Ze5-6b-2t3\">\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <imageView opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" image=\"LaunchImage\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"YRO-k0-Ey4\">\n                            </imageView>\n                        </subviews>\n                        <color key=\"backgroundColor\" red=\"1\" green=\"1\" blue=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n                        <constraints>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerX\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerX\" id=\"1a2-6s-vTC\"/>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerY\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerY\" id=\"4X2-HB-R7a\"/>\n                        </constraints>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"iYj-Kq-Ea1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"53\" y=\"375\"/>\n        </scene>\n    </scenes>\n    <resources>\n        <image name=\"LaunchImage\" width=\"168\" height=\"185\"/>\n    </resources>\n</document>\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"10117\" systemVersion=\"15F34\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" useTraitCollections=\"YES\" initialViewController=\"BYZ-38-t0r\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"10085\"/>\n    </dependencies>\n    <scenes>\n        <!--Flutter View Controller-->\n        <scene sceneID=\"tne-QT-ifu\">\n            <objects>\n                <viewController id=\"BYZ-38-t0r\" customClass=\"FlutterViewController\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"y3c-jy-aDJ\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"wfy-db-euE\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"8bC-Xf-vdC\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"600\" height=\"600\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" white=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"calibratedWhite\"/>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"dkx-z0-nzr\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n\t<dict>\n\t\t<key>CADisableMinimumFrameDurationOnPhone</key>\n\t\t<true />\n\t\t<key>CFBundleDevelopmentRegion</key>\n\t\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t\t<key>CFBundleExecutable</key>\n\t\t<string>$(EXECUTABLE_NAME)</string>\n\t\t<key>CFBundleIdentifier</key>\n\t\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t\t<key>CFBundleInfoDictionaryVersion</key>\n\t\t<string>6.0</string>\n\t\t<key>CFBundleLocalizations</key>\n\t\t<array>\n\t\t\t<string>en</string>\n\t\t</array>\n\t\t<key>CFBundleName</key>\n\t\t<string>AppFlowy</string>\n\t\t<key>CFBundlePackageType</key>\n\t\t<string>APPL</string>\n\t\t<key>CFBundleShortVersionString</key>\n\t\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t\t<key>CFBundleSignature</key>\n\t\t<string>????</string>\n\t\t<key>CFBundleURLTypes</key>\n\t\t<array>\n\t\t\t<dict>\n\t\t\t\t<key>CFBundleURLName</key>\n\t\t\t\t<string></string>\n\t\t\t\t<key>CFBundleURLSchemes</key>\n\t\t\t\t<array>\n\t\t\t\t\t<string>appflowy-flutter</string>\n\t\t\t\t</array>\n\t\t\t</dict>\n\t\t</array>\n\t\t<key>CFBundleVersion</key>\n\t\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t\t<key>FLTEnableImpeller</key>\n\t\t<false />\n\t\t<key>LSRequiresIPhoneOS</key>\n\t\t<true />\n\t\t<key>NSAppTransportSecurity</key>\n\t\t<dict>\n\t\t\t<key>NSAllowsArbitraryLoads</key>\n\t\t\t<true />\n\t\t</dict>\n\t\t<key>NSPhotoLibraryUsageDescription</key>\n\t\t<string>AppFlowy needs access to your photos to let you add images to your documents</string>\n\t\t<key>NSPhotoLibraryAddUsageDescription</key>\n\t\t<string>AppFlowy needs access to your photos to let you add images to your photo library</string>\n\t\t<key>UIApplicationSupportsIndirectInputEvents</key>\n\t\t<true />\n\t\t<key>NSCameraUsageDescription</key>\n\t\t<string>AppFlowy needs access to your camera to let you add images to your documents from\n\t\t\tcamera</string>\n\t\t<key>UILaunchStoryboardName</key>\n\t\t<string>LaunchScreen</string>\n\t\t<key>UIMainStoryboardFile</key>\n\t\t<string>Main</string>\n\t\t<key>UISupportedInterfaceOrientations</key>\n\t\t<array>\n\t\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t</array>\n\t\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t\t<array>\n\t\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t</array>\n\t\t<key>UISupportsDocumentBrowser</key>\n\t\t<true />\n\t\t<key>UIViewControllerBasedStatusBarAppearance</key>\n\t\t<false />\n\t</dict>\n</plist>"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner/Runner-Bridging-Header.h",
    "content": "#import \"GeneratedPluginRegistrant.h\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner/Runner.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>aps-environment</key>\n\t<string>development</string>\n\t<key>com.apple.developer.applesignin</key>\n\t<array>\n\t\t<string>Default</string>\n\t</array>\n\t<key>com.apple.developer.associated-domains</key>\n\t<array>\n\t\t<string>applinks:appflowy.com</string>\n\t\t<string>applinks:appflowy.io</string>\n\t\t<string>applinks:test.appflowy.com</string>\n\t\t<string>applinks:test.appflowy.io</string>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };\n\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };\n\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };\n\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };\n\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };\n\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };\n\t\tFB3C2A642AE0D57700490715 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 197F72694BED43249F1523E8 /* Pods_Runner.framework */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXFileReference section */\n\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = \"<group>\"; };\n\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = \"<group>\"; };\n\t\t197F72694BED43249F1523E8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t35DA03217F6DD4F7AC9356F9 /* 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 = \"<group>\"; };\n\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = \"<group>\"; };\n\t\t4C2CB38DA64605A62D45B098 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.profile.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t580A1ED8E012CA1552E5EFD3 /* 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 = \"<group>\"; };\n\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = \"Runner-Bridging-Header.h\"; sourceTree = \"<group>\"; };\n\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = \"<group>\"; };\n\t\t97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\t97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\tFBE00AD62AE8E46A006B563F /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t97C146EB1CF9000F007C117D /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tFB3C2A642AE0D57700490715 /* Pods_Runner.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t78844014EF958DCBB6F9B4EA /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t197F72694BED43249F1523E8 /* Pods_Runner.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9740EEB11CF90186004384FC /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */,\n\t\t\t);\n\t\t\tname = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146E51CF9000F007C117D = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9740EEB11CF90186004384FC /* Flutter */,\n\t\t\t\t97C146F01CF9000F007C117D /* Runner */,\n\t\t\t\t97C146EF1CF9000F007C117D /* Products */,\n\t\t\t\t9EC83BEE9154F1BD11D24F8F /* Pods */,\n\t\t\t\t78844014EF958DCBB6F9B4EA /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146EF1CF9000F007C117D /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146EE1CF9000F007C117D /* Runner.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146F01CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tFBE00AD62AE8E46A006B563F /* Runner.entitlements */,\n\t\t\t\t97C146FA1CF9000F007C117D /* Main.storyboard */,\n\t\t\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */,\n\t\t\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,\n\t\t\t\t97C147021CF9000F007C117D /* Info.plist */,\n\t\t\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,\n\t\t\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,\n\t\t\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */,\n\t\t\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9EC83BEE9154F1BD11D24F8F /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t35DA03217F6DD4F7AC9356F9 /* Pods-Runner.debug.xcconfig */,\n\t\t\t\t580A1ED8E012CA1552E5EFD3 /* Pods-Runner.release.xcconfig */,\n\t\t\t\t4C2CB38DA64605A62D45B098 /* Pods-Runner.profile.xcconfig */,\n\t\t\t);\n\t\t\tpath = Pods;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t97C146ED1CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tE790B8FE5609053209ED85CB /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t9740EEB61CF901F6004384FC /* Run Script */,\n\t\t\t\t97C146EA1CF9000F007C117D /* Sources */,\n\t\t\t\t97C146EB1CF9000F007C117D /* Frameworks */,\n\t\t\t\t97C146EC1CF9000F007C117D /* Resources */,\n\t\t\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */,\n\t\t\t\t08FAA63113168DEC7FB74204 /* [CP] Embed Pods Frameworks */,\n\t\t\t\tA548E58D5F4006A34D7DAA88 /* [CP] Copy Pods Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 97C146EE1CF9000F007C117D /* Runner.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t97C146E61CF9000F007C117D /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t97C146ED1CF9000F007C117D = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 7.3.1;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 97C146E51CF9000F007C117D;\n\t\t\tproductRefGroup = 97C146EF1CF9000F007C117D /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t97C146ED1CF9000F007C117D /* Runner */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t97C146EC1CF9000F007C117D /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,\n\t\t\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,\n\t\t\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,\n\t\t\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t08FAA63113168DEC7FB74204 /* [CP] Embed Pods Frameworks */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist\",\n\t\t\t);\n\t\t\tname = \"[CP] Embed Pods Frameworks\";\n\t\t\toutputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\",\n\t\t\t);\n\t\t\tname = \"Thin Binary\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" embed_and_thin\";\n\t\t};\n\t\t9740EEB61CF901F6004384FC /* Run Script */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"Run Script\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" build\";\n\t\t};\n\t\tA548E58D5F4006A34D7DAA88 /* [CP] Copy Pods Resources */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist\",\n\t\t\t);\n\t\t\tname = \"[CP] Copy Pods Resources\";\n\t\t\toutputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\tE790B8FE5609053209ED85CB /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t97C146EA1CF9000F007C117D /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,\n\t\t\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXVariantGroup section */\n\t\t97C146FA1CF9000F007C117D /* Main.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FB1CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C147001CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = LaunchScreen.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t249021D3217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 12.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t249021D4217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tDEAD_CODE_STRIPPING = NO;\n\t\t\t\tDEVELOPMENT_TEAM = VHB67HRSZG;\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = AppFlowy;\n\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.productivity\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 12.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.appflowy.appflowy.flutter;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSTRIP_STYLE = \"non-global\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"iphoneos iphonesimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = NO;\n\t\t\t\tSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = 1;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t97C147031CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 12.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147041CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 12.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Owholemodule\";\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t97C147061CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tDEAD_CODE_STRIPPING = NO;\n\t\t\t\tDEVELOPMENT_TEAM = VHB67HRSZG;\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = AppFlowy;\n\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.productivity\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 12.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.appflowy.appflowy.flutter;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSTRIP_STYLE = \"non-global\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"iphoneos iphonesimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = NO;\n\t\t\t\tSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = 1;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147071CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tDEAD_CODE_STRIPPING = NO;\n\t\t\t\tDEVELOPMENT_TEAM = VHB67HRSZG;\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = AppFlowy;\n\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.productivity\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 12.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.appflowy.appflowy.flutter;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSTRIP_STYLE = \"non-global\";\n\t\t\t\tSUPPORTED_PLATFORMS = \"iphoneos iphonesimulator\";\n\t\t\t\tSUPPORTS_MACCATALYST = NO;\n\t\t\t\tSUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = 1;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147031CF9000F007C117D /* Debug */,\n\t\t\t\t97C147041CF9000F007C117D /* Release */,\n\t\t\t\t249021D3217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147061CF9000F007C117D /* Debug */,\n\t\t\t\t97C147071CF9000F007C117D /* Release */,\n\t\t\t\t249021D4217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 97C146E61CF9000F007C117D /* Project object */;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1510\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n               BuildableName = \"Runner.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <Testables>\n      </Testables>\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <AdditionalOptions>\n      </AdditionalOptions>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n      <AdditionalOptions>\n      </AdditionalOptions>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n   <FileRef\n      location = \"group:Pods/Pods.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/ai.dart",
    "content": "export 'service/ai_entities.dart';\nexport 'service/ai_prompt_input_bloc.dart';\nexport 'service/ai_prompt_selector_cubit.dart';\nexport 'service/appflowy_ai_service.dart';\nexport 'service/error.dart';\nexport 'service/ai_model_state_notifier.dart';\nexport 'service/select_model_bloc.dart';\nexport 'service/view_selector_cubit.dart';\nexport 'widgets/loading_indicator.dart';\nexport 'widgets/view_selector.dart';\nexport 'widgets/prompt_input/action_buttons.dart';\nexport 'widgets/prompt_input/desktop_prompt_input.dart';\nexport 'widgets/prompt_input/file_attachment_list.dart';\nexport 'widgets/prompt_input/layout_define.dart';\nexport 'widgets/prompt_input/mention_page_bottom_sheet.dart';\nexport 'widgets/prompt_input/mention_page_menu.dart';\nexport 'widgets/prompt_input/prompt_input_text_controller.dart';\nexport 'widgets/prompt_input/predefined_format_buttons.dart';\nexport 'widgets/prompt_input/select_sources_bottom_sheet.dart';\nexport 'widgets/prompt_input/select_sources_menu.dart';\nexport 'widgets/prompt_input/select_model_menu.dart';\nexport 'widgets/prompt_input/send_button.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/service/ai_entities.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/easy_localiation_service.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:equatable/equatable.dart';\nimport 'package:json_annotation/json_annotation.dart';\n\npart 'ai_entities.g.dart';\n\nclass AIStreamEventPrefix {\n  static const data = 'data:';\n  static const error = 'error:';\n  static const metadata = 'metadata:';\n  static const start = 'start:';\n  static const finish = 'finish:';\n  static const comment = 'comment:';\n  static const aiResponseLimit = 'ai_response_limit:';\n  static const aiImageResponseLimit = 'ai_image_response_limit:';\n  static const aiMaxRequired = 'ai_max_required:';\n  static const localAINotReady = 'local_ai_not_ready:';\n  static const localAIDisabled = 'local_ai_disabled:';\n  static const aiFollowUp = 'ai_follow_up:';\n}\n\nenum AiType {\n  cloud,\n  local;\n\n  bool get isCloud => this == cloud;\n  bool get isLocal => this == local;\n}\n\nclass PredefinedFormat extends Equatable {\n  const PredefinedFormat({\n    required this.imageFormat,\n    required this.textFormat,\n  });\n\n  final ImageFormat imageFormat;\n  final TextFormat? textFormat;\n\n  PredefinedFormatPB toPB() {\n    return PredefinedFormatPB(\n      imageFormat: switch (imageFormat) {\n        ImageFormat.text => ResponseImageFormatPB.TextOnly,\n        ImageFormat.image => ResponseImageFormatPB.ImageOnly,\n        ImageFormat.textAndImage => ResponseImageFormatPB.TextAndImage,\n      },\n      textFormat: switch (textFormat) {\n        TextFormat.paragraph => ResponseTextFormatPB.Paragraph,\n        TextFormat.bulletList => ResponseTextFormatPB.BulletedList,\n        TextFormat.numberedList => ResponseTextFormatPB.NumberedList,\n        TextFormat.table => ResponseTextFormatPB.Table,\n        _ => null,\n      },\n    );\n  }\n\n  @override\n  List<Object?> get props => [imageFormat, textFormat];\n}\n\nenum ImageFormat {\n  text,\n  image,\n  textAndImage;\n\n  bool get hasText => this == text || this == textAndImage;\n\n  FlowySvgData get icon {\n    return switch (this) {\n      ImageFormat.text => FlowySvgs.ai_text_s,\n      ImageFormat.image => FlowySvgs.ai_image_s,\n      ImageFormat.textAndImage => FlowySvgs.ai_text_image_s,\n    };\n  }\n\n  String get i18n {\n    return switch (this) {\n      ImageFormat.text => LocaleKeys.chat_changeFormat_textOnly.tr(),\n      ImageFormat.image => LocaleKeys.chat_changeFormat_imageOnly.tr(),\n      ImageFormat.textAndImage =>\n        LocaleKeys.chat_changeFormat_textAndImage.tr(),\n    };\n  }\n}\n\nenum TextFormat {\n  paragraph,\n  bulletList,\n  numberedList,\n  table;\n\n  FlowySvgData get icon {\n    return switch (this) {\n      TextFormat.paragraph => FlowySvgs.ai_paragraph_s,\n      TextFormat.bulletList => FlowySvgs.ai_list_s,\n      TextFormat.numberedList => FlowySvgs.ai_number_list_s,\n      TextFormat.table => FlowySvgs.ai_table_s,\n    };\n  }\n\n  String get i18n {\n    return switch (this) {\n      TextFormat.paragraph => LocaleKeys.chat_changeFormat_text.tr(),\n      TextFormat.bulletList => LocaleKeys.chat_changeFormat_bullet.tr(),\n      TextFormat.numberedList => LocaleKeys.chat_changeFormat_number.tr(),\n      TextFormat.table => LocaleKeys.chat_changeFormat_table.tr(),\n    };\n  }\n}\n\nenum AiPromptCategory {\n  other,\n  development,\n  writing,\n  healthAndFitness,\n  business,\n  marketing,\n  travel,\n  contentSeo,\n  emailMarketing,\n  paidAds,\n  prCommunication,\n  recruiting,\n  sales,\n  socialMedia,\n  strategy,\n  caseStudies,\n  salesCopy,\n  education,\n  work,\n  podcastProduction,\n  copyWriting,\n  customerSuccess;\n\n  String get i18n => token.tr();\n\n  String get token {\n    return switch (this) {\n      other => LocaleKeys.ai_customPrompt_others,\n      development => LocaleKeys.ai_customPrompt_development,\n      writing => LocaleKeys.ai_customPrompt_writing,\n      healthAndFitness => LocaleKeys.ai_customPrompt_healthAndFitness,\n      business => LocaleKeys.ai_customPrompt_business,\n      marketing => LocaleKeys.ai_customPrompt_marketing,\n      travel => LocaleKeys.ai_customPrompt_travel,\n      contentSeo => LocaleKeys.ai_customPrompt_contentSeo,\n      emailMarketing => LocaleKeys.ai_customPrompt_emailMarketing,\n      paidAds => LocaleKeys.ai_customPrompt_paidAds,\n      prCommunication => LocaleKeys.ai_customPrompt_prCommunication,\n      recruiting => LocaleKeys.ai_customPrompt_recruiting,\n      sales => LocaleKeys.ai_customPrompt_sales,\n      socialMedia => LocaleKeys.ai_customPrompt_socialMedia,\n      strategy => LocaleKeys.ai_customPrompt_strategy,\n      caseStudies => LocaleKeys.ai_customPrompt_caseStudies,\n      salesCopy => LocaleKeys.ai_customPrompt_salesCopy,\n      education => LocaleKeys.ai_customPrompt_education,\n      work => LocaleKeys.ai_customPrompt_work,\n      podcastProduction => LocaleKeys.ai_customPrompt_podcastProduction,\n      copyWriting => LocaleKeys.ai_customPrompt_copyWriting,\n      customerSuccess => LocaleKeys.ai_customPrompt_customerSuccess,\n    };\n  }\n}\n\n@JsonSerializable()\nclass AiPrompt extends Equatable {\n  const AiPrompt({\n    required this.id,\n    required this.name,\n    required this.content,\n    required this.category,\n    required this.example,\n    required this.isFeatured,\n    required this.isCustom,\n  });\n\n  factory AiPrompt.fromPB(CustomPromptPB pb) {\n    final map = _buildCategoryNameMap();\n    final categories = pb.category\n        .split(',')\n        .map((categoryName) => categoryName.trim())\n        .map(\n          (categoryName) {\n            final entry = map.entries.firstWhereOrNull(\n              (entry) =>\n                  entry.value.$1 == categoryName ||\n                  entry.value.$2 == categoryName,\n            );\n            return entry?.key ?? AiPromptCategory.other;\n          },\n        )\n        .toSet()\n        .toList();\n\n    return AiPrompt(\n      id: pb.id,\n      name: pb.name,\n      content: pb.content,\n      category: categories,\n      example: pb.example,\n      isFeatured: false,\n      isCustom: true,\n    );\n  }\n\n  factory AiPrompt.fromJson(Map<String, dynamic> json) =>\n      _$AiPromptFromJson(json);\n\n  Map<String, dynamic> toJson() => _$AiPromptToJson(this);\n\n  final String id;\n  final String name;\n  final String content;\n  @JsonKey(fromJson: _categoryFromJson)\n  final List<AiPromptCategory> category;\n  @JsonKey(defaultValue: \"\")\n  final String example;\n  @JsonKey(defaultValue: false)\n  final bool isFeatured;\n  @JsonKey(defaultValue: false)\n  final bool isCustom;\n\n  @override\n  List<Object?> get props =>\n      [id, name, content, category, example, isFeatured, isCustom];\n\n  static Map<AiPromptCategory, (String, String)> _buildCategoryNameMap() {\n    final service = getIt<EasyLocalizationService>();\n    return {\n      for (final category in AiPromptCategory.values)\n        category: (\n          service.getFallbackTranslation(category.token),\n          service.getFallbackTranslation(category.token),\n        ),\n    };\n  }\n\n  static List<AiPromptCategory> _categoryFromJson(dynamic json) {\n    if (json is String) {\n      return json\n          .split(',')\n          .map((categoryName) => categoryName.trim())\n          .map(\n            (categoryName) => $enumDecode(\n              _aiPromptCategoryEnumMap,\n              categoryName,\n              unknownValue: AiPromptCategory.other,\n            ),\n          )\n          .toSet()\n          .toList();\n    }\n\n    return [AiPromptCategory.other];\n  }\n}\n\nconst _aiPromptCategoryEnumMap = {\n  AiPromptCategory.other: 'other',\n  AiPromptCategory.development: 'development',\n  AiPromptCategory.writing: 'writing',\n  AiPromptCategory.healthAndFitness: 'healthAndFitness',\n  AiPromptCategory.business: 'business',\n  AiPromptCategory.marketing: 'marketing',\n  AiPromptCategory.travel: 'travel',\n  AiPromptCategory.contentSeo: 'contentSeo',\n  AiPromptCategory.emailMarketing: 'emailMarketing',\n  AiPromptCategory.paidAds: 'paidAds',\n  AiPromptCategory.prCommunication: 'prCommunication',\n  AiPromptCategory.recruiting: 'recruiting',\n  AiPromptCategory.sales: 'sales',\n  AiPromptCategory.socialMedia: 'socialMedia',\n  AiPromptCategory.strategy: 'strategy',\n  AiPromptCategory.caseStudies: 'caseStudies',\n  AiPromptCategory.salesCopy: 'salesCopy',\n  AiPromptCategory.education: 'education',\n  AiPromptCategory.work: 'work',\n  AiPromptCategory.podcastProduction: 'podcastProduction',\n  AiPromptCategory.copyWriting: 'copyWriting',\n  AiPromptCategory.customerSuccess: 'customerSuccess',\n};\n\nclass CustomPromptDatabaseConfig extends Equatable {\n  const CustomPromptDatabaseConfig({\n    required this.view,\n    required this.titleFieldId,\n    required this.contentFieldId,\n    required this.exampleFieldId,\n    required this.categoryFieldId,\n  });\n\n  factory CustomPromptDatabaseConfig.fromAiPB(\n    CustomPromptDatabaseConfigurationPB pb,\n    ViewPB view,\n  ) {\n    final config = CustomPromptDatabaseConfig(\n      view: view,\n      titleFieldId: pb.titleFieldId,\n      contentFieldId: pb.contentFieldId,\n      exampleFieldId: pb.hasExampleFieldId() ? pb.exampleFieldId : null,\n      categoryFieldId: pb.hasCategoryFieldId() ? pb.categoryFieldId : null,\n    );\n\n    return config;\n  }\n\n  factory CustomPromptDatabaseConfig.fromDbPB(\n    CustomPromptDatabaseConfigPB pb,\n    ViewPB view,\n  ) {\n    final config = CustomPromptDatabaseConfig(\n      view: view,\n      titleFieldId: pb.titleFieldId,\n      contentFieldId: pb.contentFieldId,\n      exampleFieldId: pb.hasExampleFieldId() ? pb.exampleFieldId : null,\n      categoryFieldId: pb.hasCategoryFieldId() ? pb.categoryFieldId : null,\n    );\n\n    return config;\n  }\n\n  final ViewPB view;\n  final String titleFieldId;\n  final String contentFieldId;\n  final String? exampleFieldId;\n  final String? categoryFieldId;\n\n  @override\n  List<Object?> get props =>\n      [view.id, titleFieldId, contentFieldId, exampleFieldId, categoryFieldId];\n\n  CustomPromptDatabaseConfig copyWith({\n    ViewPB? view,\n    String? titleFieldId,\n    String? contentFieldId,\n    String? exampleFieldId,\n    String? categoryFieldId,\n  }) {\n    return CustomPromptDatabaseConfig(\n      view: view ?? this.view,\n      titleFieldId: titleFieldId ?? this.titleFieldId,\n      contentFieldId: contentFieldId ?? this.contentFieldId,\n      exampleFieldId: exampleFieldId ?? this.exampleFieldId,\n      categoryFieldId: categoryFieldId ?? this.categoryFieldId,\n    );\n  }\n\n  CustomPromptDatabaseConfigurationPB toAiPB() {\n    final payload = CustomPromptDatabaseConfigurationPB.create()\n      ..viewId = view.id\n      ..titleFieldId = titleFieldId\n      ..contentFieldId = contentFieldId;\n\n    if (exampleFieldId != null) {\n      payload.exampleFieldId = exampleFieldId!;\n    }\n    if (categoryFieldId != null) {\n      payload.categoryFieldId = categoryFieldId!;\n    }\n\n    return payload;\n  }\n\n  CustomPromptDatabaseConfigPB toDbPB() {\n    final payload = CustomPromptDatabaseConfigPB.create()\n      ..viewId = view.id\n      ..titleFieldId = titleFieldId\n      ..contentFieldId = contentFieldId;\n\n    if (exampleFieldId != null) {\n      payload.exampleFieldId = exampleFieldId!;\n    }\n    if (categoryFieldId != null) {\n      payload.categoryFieldId = categoryFieldId!;\n    }\n\n    return payload;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/ai_chat/application/ai_model_switch_listener.dart';\nimport 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\ntypedef OnModelStateChangedCallback = void Function(AIModelState state);\ntypedef OnAvailableModelsChangedCallback = void Function(\n  List<AIModelPB>,\n  AIModelPB?,\n);\n\n/// Represents the state of an AI model\nclass AIModelState {\n  const AIModelState({\n    required this.type,\n    required this.hintText,\n    required this.tooltip,\n    required this.isEditable,\n    required this.localAIEnabled,\n  });\n  final AiType type;\n\n  /// The text displayed as placeholder/hint in the input field\n  /// Shows different messages based on AI state (enabled, initializing, disabled)\n  final String hintText;\n\n  /// Optional tooltip text that appears on hover\n  /// Provides additional context about the current state of the AI\n  /// Null when no tooltip should be shown\n  final String? tooltip;\n\n  final bool isEditable;\n  final bool localAIEnabled;\n}\n\nclass AIModelStateNotifier {\n  AIModelStateNotifier({required this.objectId})\n      : _localAIListener =\n            UniversalPlatform.isDesktop ? LocalAIStateListener() : null,\n        _aiModelSwitchListener = AIModelSwitchListener(objectId: objectId) {\n    _startListening();\n    _init();\n  }\n\n  final String objectId;\n  final LocalAIStateListener? _localAIListener;\n  final AIModelSwitchListener _aiModelSwitchListener;\n\n  LocalAIPB? _localAIState;\n  ModelSelectionPB? _modelSelection;\n\n  AIModelState _currentState = _defaultState();\n  List<AIModelPB> _availableModels = [];\n  AIModelPB? _selectedModel;\n\n  final List<OnModelStateChangedCallback> _stateChangedCallbacks = [];\n  final List<OnAvailableModelsChangedCallback>\n      _availableModelsChangedCallbacks = [];\n\n  /// Starts platform-specific listeners\n  void _startListening() {\n    if (UniversalPlatform.isDesktop) {\n      _localAIListener?.start(\n        stateCallback: (state) async {\n          _localAIState = state;\n          _updateAll();\n        },\n      );\n    }\n\n    _aiModelSwitchListener.start(\n      onUpdateSelectedModel: (model) async {\n        _selectedModel = model;\n        _updateAll();\n        if (model.isLocal && UniversalPlatform.isDesktop) {\n          await _loadLocalState();\n          _updateAll();\n        }\n      },\n    );\n  }\n\n  Future<void> _init() async {\n    await Future.wait([\n      if (UniversalPlatform.isDesktop) _loadLocalState(),\n      _loadModelSelection(),\n    ]);\n    _updateAll();\n  }\n\n  /// Register callbacks for state or available-models changes\n  void addListener({\n    OnModelStateChangedCallback? onStateChanged,\n    OnAvailableModelsChangedCallback? onAvailableModelsChanged,\n  }) {\n    if (onStateChanged != null) {\n      _stateChangedCallbacks.add(onStateChanged);\n    }\n    if (onAvailableModelsChanged != null) {\n      _availableModelsChangedCallbacks.add(onAvailableModelsChanged);\n    }\n  }\n\n  /// Remove previously registered callbacks\n  void removeListener({\n    OnModelStateChangedCallback? onStateChanged,\n    OnAvailableModelsChangedCallback? onAvailableModelsChanged,\n  }) {\n    if (onStateChanged != null) {\n      _stateChangedCallbacks.remove(onStateChanged);\n    }\n    if (onAvailableModelsChanged != null) {\n      _availableModelsChangedCallbacks.remove(onAvailableModelsChanged);\n    }\n  }\n\n  Future<void> dispose() async {\n    _stateChangedCallbacks.clear();\n    _availableModelsChangedCallbacks.clear();\n    await _localAIListener?.stop();\n    await _aiModelSwitchListener.stop();\n  }\n\n  /// Returns current AIModelState\n  AIModelState getState() => _currentState;\n\n  /// Returns available models and the selected model\n  (List<AIModelPB>, AIModelPB?) getModelSelection() =>\n      (_availableModels, _selectedModel);\n\n  void _updateAll() {\n    _currentState = _computeState();\n    for (final cb in _stateChangedCallbacks) {\n      cb(_currentState);\n    }\n    for (final cb in _availableModelsChangedCallbacks) {\n      cb(_availableModels, _selectedModel);\n    }\n  }\n\n  Future<void> _loadModelSelection() async {\n    await AIEventGetSourceModelSelection(\n      ModelSourcePB(source: objectId),\n    ).send().fold(\n      (ms) {\n        _modelSelection = ms;\n        _availableModels = ms.models;\n        _selectedModel = ms.selectedModel;\n      },\n      (e) => Log.error(\"Failed to fetch models: \\$e\"),\n    );\n  }\n\n  Future<void> _loadLocalState() async {\n    await AIEventGetLocalAIState().send().fold(\n          (s) => _localAIState = s,\n          (e) => Log.error(\"Failed to fetch local AI state: \\$e\"),\n        );\n  }\n\n  static AIModelState _defaultState() => AIModelState(\n        type: AiType.cloud,\n        hintText: LocaleKeys.chat_inputMessageHint.tr(),\n        tooltip: null,\n        isEditable: true,\n        localAIEnabled: false,\n      );\n\n  /// Core logic computing the state from local and selection data\n  AIModelState _computeState() {\n    if (UniversalPlatform.isMobile) return _defaultState();\n\n    if (_modelSelection == null || _localAIState == null) {\n      return _defaultState();\n    }\n\n    if (!_selectedModel!.isLocal) {\n      return _defaultState();\n    }\n\n    final enabled = _localAIState!.enabled;\n    final running = _localAIState!.isReady;\n    final hintKey = enabled\n        ? (running\n            ? LocaleKeys.chat_inputLocalAIMessageHint\n            : LocaleKeys.settings_aiPage_keys_localAIInitializing)\n        : LocaleKeys.settings_aiPage_keys_localAIDisabled;\n    final tooltipKey = enabled\n        ? (running\n            ? null\n            : LocaleKeys.settings_aiPage_keys_localAINotReadyTextFieldPrompt)\n        : LocaleKeys.settings_aiPage_keys_localAIDisabledTextFieldPrompt;\n\n    return AIModelState(\n      type: AiType.local,\n      hintText: hintKey.tr(),\n      tooltip: tooltipKey?.tr(),\n      isEditable: running,\n      localAIEnabled: enabled,\n    );\n  }\n}\n\nextension AIModelPBExtension on AIModelPB {\n  bool get isDefault => name == 'Auto';\n  String get i18n =>\n      isDefault ? LocaleKeys.chat_switchModel_autoModel.tr() : name;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/service/ai_prompt_database_selector_cubit.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'ai_prompt_database_selector_cubit.freezed.dart';\n\nclass AiPromptDatabaseSelectorCubit\n    extends Cubit<AiPromptDatabaseSelectorState> {\n  AiPromptDatabaseSelectorCubit({\n    required CustomPromptDatabaseConfig? configuration,\n  }) : super(AiPromptDatabaseSelectorState.loading()) {\n    _init(configuration);\n  }\n\n  void _init(CustomPromptDatabaseConfig? config) async {\n    if (config == null) {\n      emit(AiPromptDatabaseSelectorState.empty());\n      return;\n    }\n\n    final fields = await _getFields(config.view.id);\n\n    if (fields == null) {\n      emit(AiPromptDatabaseSelectorState.empty());\n      return;\n    }\n\n    emit(\n      AiPromptDatabaseSelectorState.selected(\n        config: config,\n        fields: fields,\n      ),\n    );\n  }\n\n  void selectDatabaseView(String viewId) async {\n    final configuration = await _testDatabase(viewId);\n\n    if (configuration == null) {\n      final stateCopy = state;\n      emit(AiPromptDatabaseSelectorState.invalidDatabase());\n      emit(stateCopy);\n      return;\n    }\n\n    final databaseView = await AiPromptSelectorCubit.getDatabaseView(viewId);\n    final fields = await _getFields(viewId);\n\n    if (databaseView == null || fields == null) {\n      final stateCopy = state;\n      emit(AiPromptDatabaseSelectorState.invalidDatabase());\n      emit(stateCopy);\n      return;\n    }\n\n    final config = CustomPromptDatabaseConfig.fromDbPB(\n      configuration,\n      databaseView,\n    );\n\n    emit(\n      AiPromptDatabaseSelectorState.selected(\n        config: config,\n        fields: fields,\n      ),\n    );\n  }\n\n  void selectContentField(String fieldId) {\n    final state = this.state;\n    if (state is! _Selected) {\n      return;\n    }\n\n    final config = state.config.copyWith(\n      contentFieldId: fieldId,\n    );\n\n    emit(\n      AiPromptDatabaseSelectorState.selected(\n        config: config,\n        fields: state.fields,\n      ),\n    );\n  }\n\n  void selectExampleField(String? fieldId) {\n    final state = this.state;\n    if (state is! _Selected) {\n      return;\n    }\n\n    final config = CustomPromptDatabaseConfig(\n      exampleFieldId: fieldId,\n      view: state.config.view,\n      titleFieldId: state.config.titleFieldId,\n      contentFieldId: state.config.contentFieldId,\n      categoryFieldId: state.config.categoryFieldId,\n    );\n\n    emit(\n      AiPromptDatabaseSelectorState.selected(\n        config: config,\n        fields: state.fields,\n      ),\n    );\n  }\n\n  void selectCategoryField(String? fieldId) {\n    final state = this.state;\n    if (state is! _Selected) {\n      return;\n    }\n\n    final config = CustomPromptDatabaseConfig(\n      categoryFieldId: fieldId,\n      view: state.config.view,\n      titleFieldId: state.config.titleFieldId,\n      contentFieldId: state.config.contentFieldId,\n      exampleFieldId: state.config.exampleFieldId,\n    );\n\n    emit(\n      AiPromptDatabaseSelectorState.selected(\n        config: config,\n        fields: state.fields,\n      ),\n    );\n  }\n\n  Future<List<FieldPB>?> _getFields(String viewId) {\n    return FieldBackendService.getFields(viewId: viewId).toNullable();\n  }\n\n  Future<CustomPromptDatabaseConfigPB?> _testDatabase(\n    String viewId,\n  ) {\n    return DatabaseEventTestCustomPromptDatabaseConfiguration(\n      DatabaseViewIdPB(value: viewId),\n    ).send().toNullable();\n  }\n}\n\n@freezed\nclass AiPromptDatabaseSelectorState with _$AiPromptDatabaseSelectorState {\n  const factory AiPromptDatabaseSelectorState.loading() = _Loading;\n\n  const factory AiPromptDatabaseSelectorState.empty() = _Empty;\n\n  const factory AiPromptDatabaseSelectorState.selected({\n    required CustomPromptDatabaseConfig config,\n    required List<FieldPB> fields,\n  }) = _Selected;\n\n  const factory AiPromptDatabaseSelectorState.invalidDatabase() =\n      _InvalidDatabase;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/ai/service/ai_model_state_notifier.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport 'ai_entities.dart';\n\npart 'ai_prompt_input_bloc.freezed.dart';\n\nclass AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {\n  AIPromptInputBloc({\n    required String objectId,\n    required PredefinedFormat? predefinedFormat,\n  })  : aiModelStateNotifier = AIModelStateNotifier(objectId: objectId),\n        super(AIPromptInputState.initial(predefinedFormat)) {\n    _dispatch();\n    _startListening();\n    _init();\n  }\n\n  final AIModelStateNotifier aiModelStateNotifier;\n\n  String? promptId;\n\n  @override\n  Future<void> close() async {\n    await aiModelStateNotifier.dispose();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<AIPromptInputEvent>(\n      (event, emit) {\n        event.when(\n          updateAIState: (modelState) {\n            emit(\n              state.copyWith(\n                modelState: modelState,\n              ),\n            );\n          },\n          toggleShowPredefinedFormat: () {\n            final showPredefinedFormats = !state.showPredefinedFormats;\n            final predefinedFormat =\n                showPredefinedFormats && state.predefinedFormat == null\n                    ? PredefinedFormat(\n                        imageFormat: ImageFormat.text,\n                        textFormat: TextFormat.paragraph,\n                      )\n                    : null;\n            emit(\n              state.copyWith(\n                showPredefinedFormats: showPredefinedFormats,\n                predefinedFormat: predefinedFormat,\n              ),\n            );\n          },\n          updatePredefinedFormat: (format) {\n            if (!state.showPredefinedFormats) {\n              return;\n            }\n            emit(state.copyWith(predefinedFormat: format));\n          },\n          attachFile: (filePath, fileName) {\n            final newFile = ChatFile.fromFilePath(filePath);\n            if (newFile != null) {\n              emit(\n                state.copyWith(\n                  attachedFiles: [...state.attachedFiles, newFile],\n                ),\n              );\n            }\n          },\n          removeFile: (file) {\n            final files = [...state.attachedFiles];\n            files.remove(file);\n            emit(\n              state.copyWith(\n                attachedFiles: files,\n              ),\n            );\n          },\n          updateMentionedViews: (views) {\n            emit(\n              state.copyWith(\n                mentionedPages: views,\n              ),\n            );\n          },\n          updatePromptId: (promptId) {\n            this.promptId = promptId;\n          },\n          clearMetadata: () {\n            promptId = null;\n            emit(\n              state.copyWith(\n                attachedFiles: [],\n                mentionedPages: [],\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    aiModelStateNotifier.addListener(\n      onStateChanged: (modelState) {\n        add(\n          AIPromptInputEvent.updateAIState(modelState),\n        );\n      },\n    );\n  }\n\n  void _init() {\n    final modelState = aiModelStateNotifier.getState();\n    add(\n      AIPromptInputEvent.updateAIState(modelState),\n    );\n  }\n\n  Map<String, dynamic> consumeMetadata() {\n    final metadata = {\n      for (final file in state.attachedFiles) file.filePath: file,\n      for (final page in state.mentionedPages) page.id: page,\n    };\n\n    if (metadata.isNotEmpty && !isClosed) {\n      add(const AIPromptInputEvent.clearMetadata());\n    }\n\n    return metadata;\n  }\n}\n\n@freezed\nclass AIPromptInputEvent with _$AIPromptInputEvent {\n  const factory AIPromptInputEvent.updateAIState(\n    AIModelState modelState,\n  ) = _UpdateAIState;\n\n  const factory AIPromptInputEvent.toggleShowPredefinedFormat() =\n      _ToggleShowPredefinedFormat;\n  const factory AIPromptInputEvent.updatePredefinedFormat(\n    PredefinedFormat format,\n  ) = _UpdatePredefinedFormat;\n  const factory AIPromptInputEvent.attachFile(\n    String filePath,\n    String fileName,\n  ) = _AttachFile;\n  const factory AIPromptInputEvent.removeFile(ChatFile file) = _RemoveFile;\n  const factory AIPromptInputEvent.updateMentionedViews(List<ViewPB> views) =\n      _UpdateMentionedViews;\n  const factory AIPromptInputEvent.clearMetadata() = _ClearMetadata;\n  const factory AIPromptInputEvent.updatePromptId(String promptId) =\n      _UpdatePromptId;\n}\n\n@freezed\nclass AIPromptInputState with _$AIPromptInputState {\n  const factory AIPromptInputState({\n    required AIModelState modelState,\n    required bool supportChatWithFile,\n    required bool showPredefinedFormats,\n    required PredefinedFormat? predefinedFormat,\n    required List<ChatFile> attachedFiles,\n    required List<ViewPB> mentionedPages,\n  }) = _AIPromptInputState;\n\n  factory AIPromptInputState.initial(PredefinedFormat? format) =>\n      AIPromptInputState(\n        modelState: AIModelState(\n          type: AiType.cloud,\n          isEditable: true,\n          hintText: '',\n          localAIEnabled: false,\n          tooltip: null,\n        ),\n        supportChatWithFile: false,\n        showPredefinedFormats: format != null,\n        predefinedFormat: format,\n        attachedFiles: [],\n        mentionedPages: [],\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/service/ai_prompt_selector_cubit.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport '../../plugins/trash/application/trash_service.dart';\nimport 'ai_entities.dart';\nimport 'appflowy_ai_service.dart';\n\npart 'ai_prompt_selector_cubit.freezed.dart';\n\nclass AiPromptSelectorCubit extends Cubit<AiPromptSelectorState> {\n  AiPromptSelectorCubit({\n    AppFlowyAIService? aiService,\n  })  : _aiService = aiService ?? AppFlowyAIService(),\n        super(AiPromptSelectorState.loading()) {\n    filterTextController.addListener(_filterTextChanged);\n    _init();\n  }\n\n  final AppFlowyAIService _aiService;\n  final filterTextController = TextEditingController();\n  final List<AiPrompt> availablePrompts = [];\n\n  @override\n  Future<void> close() async {\n    filterTextController.dispose();\n    await super.close();\n  }\n\n  void _init() async {\n    availablePrompts.addAll(await _aiService.getBuiltInPrompts());\n\n    final featuredPrompts =\n        availablePrompts.where((prompt) => prompt.isFeatured);\n    final visiblePrompts = _getFilteredPrompts(featuredPrompts);\n\n    emit(\n      AiPromptSelectorState.ready(\n        visiblePrompts: visiblePrompts.toList(),\n        isCustomPromptSectionSelected: false,\n        isFeaturedSectionSelected: true,\n        selectedPromptId: visiblePrompts.firstOrNull?.id,\n        databaseConfig: null,\n        isLoadingCustomPrompts: true,\n        selectedCategory: null,\n        favoritePrompts: [],\n      ),\n    );\n\n    loadCustomPrompts();\n  }\n\n  void loadCustomPrompts() {\n    state.maybeMap(\n      ready: (readyState) async {\n        emit(\n          readyState.copyWith(isLoadingCustomPrompts: true),\n        );\n\n        CustomPromptDatabaseConfig? configuration = readyState.databaseConfig;\n        if (configuration == null) {\n          final configResult =\n              await AIEventGetCustomPromptDatabaseConfiguration()\n                  .send()\n                  .toNullable();\n          if (configResult != null) {\n            final view = await getDatabaseView(configResult.viewId);\n            if (view != null) {\n              configuration = CustomPromptDatabaseConfig.fromAiPB(\n                configResult,\n                view,\n              );\n            }\n          }\n        } else {\n          final view = await getDatabaseView(configuration.view.id);\n          if (view != null) {\n            configuration = configuration.copyWith(view: view);\n          }\n        }\n\n        if (configuration == null) {\n          emit(\n            readyState.copyWith(isLoadingCustomPrompts: false),\n          );\n          return;\n        }\n\n        availablePrompts.removeWhere((prompt) => prompt.isCustom);\n\n        final customPrompts =\n            await _aiService.getDatabasePrompts(configuration.toDbPB());\n\n        if (customPrompts == null) {\n          final prompts = availablePrompts.where((prompt) => prompt.isFeatured);\n          final visiblePrompts = _getFilteredPrompts(prompts);\n          final selectedPromptId = _getVisibleSelectedPrompt(\n            visiblePrompts,\n            readyState.selectedPromptId,\n          );\n\n          emit(\n            readyState.copyWith(\n              visiblePrompts: visiblePrompts.toList(),\n              selectedPromptId: selectedPromptId,\n              databaseConfig: configuration,\n              isLoadingCustomPrompts: false,\n              isFeaturedSectionSelected: true,\n              isCustomPromptSectionSelected: false,\n              selectedCategory: null,\n            ),\n          );\n        } else {\n          availablePrompts.addAll(customPrompts);\n\n          final prompts = _getPromptsByCategory(readyState);\n          final visiblePrompts = _getFilteredPrompts(prompts);\n          final selectedPromptId = _getVisibleSelectedPrompt(\n            visiblePrompts,\n            readyState.selectedPromptId,\n          );\n\n          emit(\n            readyState.copyWith(\n              visiblePrompts: visiblePrompts.toList(),\n              databaseConfig: configuration,\n              isLoadingCustomPrompts: false,\n              selectedPromptId: selectedPromptId,\n            ),\n          );\n        }\n      },\n      orElse: () {},\n    );\n  }\n\n  void selectCustomSection() {\n    state.maybeMap(\n      ready: (readyState) {\n        final prompts = availablePrompts.where((prompt) => prompt.isCustom);\n        final visiblePrompts = _getFilteredPrompts(prompts);\n\n        emit(\n          readyState.copyWith(\n            visiblePrompts: visiblePrompts.toList(),\n            selectedPromptId: visiblePrompts.firstOrNull?.id,\n            isCustomPromptSectionSelected: true,\n            isFeaturedSectionSelected: false,\n            selectedCategory: null,\n          ),\n        );\n      },\n      orElse: () {},\n    );\n  }\n\n  void selectFeaturedSection() {\n    state.maybeMap(\n      ready: (readyState) {\n        final prompts = availablePrompts.where((prompt) => prompt.isFeatured);\n        final visiblePrompts = _getFilteredPrompts(prompts);\n\n        emit(\n          readyState.copyWith(\n            visiblePrompts: visiblePrompts.toList(),\n            selectedPromptId: visiblePrompts.firstOrNull?.id,\n            isFeaturedSectionSelected: true,\n            isCustomPromptSectionSelected: false,\n            selectedCategory: null,\n          ),\n        );\n      },\n      orElse: () {},\n    );\n  }\n\n  void selectCategory(AiPromptCategory? category) {\n    state.maybeMap(\n      ready: (readyState) {\n        final prompts = category == null\n            ? availablePrompts\n            : availablePrompts\n                .where((prompt) => prompt.category.contains(category));\n        final visiblePrompts = _getFilteredPrompts(prompts);\n\n        final selectedPromptId = _getVisibleSelectedPrompt(\n          visiblePrompts,\n          readyState.selectedPromptId,\n        );\n\n        emit(\n          readyState.copyWith(\n            visiblePrompts: visiblePrompts.toList(),\n            selectedCategory: category,\n            selectedPromptId: selectedPromptId,\n            isFeaturedSectionSelected: false,\n            isCustomPromptSectionSelected: false,\n          ),\n        );\n      },\n      orElse: () {},\n    );\n  }\n\n  void selectPrompt(String promptId) {\n    state.maybeMap(\n      ready: (readyState) {\n        final selectedPrompt = readyState.visiblePrompts\n            .firstWhereOrNull((prompt) => prompt.id == promptId);\n        if (selectedPrompt != null) {\n          emit(\n            readyState.copyWith(selectedPromptId: selectedPrompt.id),\n          );\n        }\n      },\n      orElse: () {},\n    );\n  }\n\n  void toggleFavorite(String promptId) {\n    state.maybeMap(\n      ready: (readyState) {\n        final favoritePrompts = [...readyState.favoritePrompts];\n        if (favoritePrompts.contains(promptId)) {\n          favoritePrompts.remove(promptId);\n        } else {\n          favoritePrompts.add(promptId);\n        }\n        emit(\n          readyState.copyWith(favoritePrompts: favoritePrompts),\n        );\n      },\n      orElse: () {},\n    );\n  }\n\n  void reset() {\n    filterTextController.clear();\n    state.maybeMap(\n      ready: (readyState) {\n        emit(\n          readyState.copyWith(\n            visiblePrompts: availablePrompts,\n            isCustomPromptSectionSelected: false,\n            isFeaturedSectionSelected: true,\n            selectedPromptId: availablePrompts.firstOrNull?.id,\n            selectedCategory: null,\n          ),\n        );\n      },\n      orElse: () {},\n    );\n  }\n\n  void updateCustomPromptDatabaseConfiguration(\n    CustomPromptDatabaseConfig configuration,\n  ) async {\n    state.maybeMap(\n      ready: (readyState) async {\n        emit(\n          readyState.copyWith(isLoadingCustomPrompts: true),\n        );\n\n        final customPrompts =\n            await _aiService.getDatabasePrompts(configuration.toDbPB());\n\n        if (customPrompts == null) {\n          emit(AiPromptSelectorState.invalidDatabase());\n          emit(readyState);\n          return;\n        }\n\n        availablePrompts\n          ..removeWhere((prompt) => prompt.isCustom)\n          ..addAll(customPrompts);\n\n        await AIEventSetCustomPromptDatabaseConfiguration(\n          configuration.toAiPB(),\n        ).send().onFailure(Log.error);\n\n        final prompts = _getPromptsByCategory(readyState);\n        final visiblePrompts = _getFilteredPrompts(prompts);\n        final selectedPromptId = _getVisibleSelectedPrompt(\n          visiblePrompts,\n          readyState.selectedPromptId,\n        );\n        emit(\n          readyState.copyWith(\n            visiblePrompts: visiblePrompts.toList(),\n            selectedPromptId: selectedPromptId,\n            databaseConfig: configuration,\n            isLoadingCustomPrompts: false,\n          ),\n        );\n      },\n      orElse: () => {},\n    );\n  }\n\n  void _filterTextChanged() {\n    state.maybeMap(\n      ready: (readyState) {\n        final prompts = _getPromptsByCategory(readyState);\n        final visiblePrompts = _getFilteredPrompts(prompts);\n\n        final selectedPromptId = _getVisibleSelectedPrompt(\n          visiblePrompts,\n          readyState.selectedPromptId,\n        );\n\n        emit(\n          readyState.copyWith(\n            visiblePrompts: visiblePrompts.toList(),\n            selectedPromptId: selectedPromptId,\n          ),\n        );\n      },\n      orElse: () {},\n    );\n  }\n\n  Iterable<AiPrompt> _getFilteredPrompts(Iterable<AiPrompt> prompts) {\n    final filterText = filterTextController.value.text.trim().toLowerCase();\n\n    return prompts.where((prompt) {\n      final content = \"${prompt.name} ${prompt.name}\".toLowerCase();\n      return content.contains(filterText);\n    }).toList();\n  }\n\n  Iterable<AiPrompt> _getPromptsByCategory(_AiPromptSelectorReadyState state) {\n    return availablePrompts.where((prompt) {\n      if (state.selectedCategory != null) {\n        return prompt.category.contains(state.selectedCategory);\n      }\n      if (state.isFeaturedSectionSelected) {\n        return prompt.isFeatured;\n      }\n      if (state.isCustomPromptSectionSelected) {\n        return prompt.isCustom;\n      }\n      return true;\n    });\n  }\n\n  String? _getVisibleSelectedPrompt(\n    Iterable<AiPrompt> visiblePrompts,\n    String? currentlySelectedPromptId,\n  ) {\n    if (visiblePrompts\n        .any((prompt) => prompt.id == currentlySelectedPromptId)) {\n      return currentlySelectedPromptId;\n    }\n\n    return visiblePrompts.firstOrNull?.id;\n  }\n\n  static Future<ViewPB?> getDatabaseView(String viewId) async {\n    final view = await ViewBackendService.getView(viewId).toNullable();\n\n    if (view != null) {\n      return view;\n    }\n\n    final trashViews = await TrashService().readTrash().toNullable();\n    final trashedItem =\n        trashViews?.items.firstWhereOrNull((element) => element.id == viewId);\n\n    if (trashedItem == null) {\n      return null;\n    }\n\n    return ViewPB()\n      ..id = trashedItem.id\n      ..name = trashedItem.name;\n  }\n}\n\n@freezed\nclass AiPromptSelectorState with _$AiPromptSelectorState {\n  const AiPromptSelectorState._();\n\n  const factory AiPromptSelectorState.loading() = _AiPromptSelectorLoadingState;\n\n  const factory AiPromptSelectorState.invalidDatabase() =\n      _AiPromptSelectorErrorState;\n\n  const factory AiPromptSelectorState.ready({\n    required List<AiPrompt> visiblePrompts,\n    required List<String> favoritePrompts,\n    required bool isCustomPromptSectionSelected,\n    required bool isFeaturedSectionSelected,\n    required AiPromptCategory? selectedCategory,\n    required String? selectedPromptId,\n    required bool isLoadingCustomPrompts,\n    required CustomPromptDatabaseConfig? databaseConfig,\n  }) = _AiPromptSelectorReadyState;\n\n  bool get isLoading => this is _AiPromptSelectorLoadingState;\n  bool get isReady => this is _AiPromptSelectorReadyState;\n\n  AiPrompt? get selectedPrompt => maybeMap(\n        ready: (state) => state.visiblePrompts\n            .firstWhereOrNull((prompt) => prompt.id == state.selectedPromptId),\n        orElse: () => null,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi';\nimport 'dart:isolate';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';\nimport 'package:appflowy/shared/list_extension.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'\n    hide CustomPromptDatabaseConfigurationPB;\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:fixnum/fixnum.dart' as fixnum;\nimport 'package:flutter/services.dart';\n\nimport 'ai_entities.dart';\nimport 'error.dart';\n\nenum LocalAIStreamingState {\n  notReady,\n  disabled,\n}\n\nabstract class AIRepository {\n  Future<(String, CompletionStream)?> streamCompletion({\n    String? objectId,\n    required String text,\n    PredefinedFormat? format,\n    String? promptId,\n    List<String> sourceIds = const [],\n    List<AiWriterRecord> history = const [],\n    required CompletionTypePB completionType,\n    required Future<void> Function() onStart,\n    required Future<void> Function(String text) processMessage,\n    required Future<void> Function(String text) processAssistMessage,\n    required Future<void> Function() onEnd,\n    required void Function(AIError error) onError,\n    required void Function(LocalAIStreamingState state)\n        onLocalAIStreamingStateChange,\n  });\n\n  Future<List<AiPrompt>> getBuiltInPrompts();\n\n  Future<List<AiPrompt>?> getDatabasePrompts(\n    CustomPromptDatabaseConfigPB config,\n  );\n\n  void updateFavoritePrompts(List<String> promptIds);\n}\n\nclass AppFlowyAIService implements AIRepository {\n  @override\n  Future<(String, CompletionStream)?> streamCompletion({\n    String? objectId,\n    required String text,\n    PredefinedFormat? format,\n    String? promptId,\n    List<String> sourceIds = const [],\n    List<AiWriterRecord> history = const [],\n    required CompletionTypePB completionType,\n    required Future<void> Function() onStart,\n    required Future<void> Function(String text) processMessage,\n    required Future<void> Function(String text) processAssistMessage,\n    required Future<void> Function() onEnd,\n    required void Function(AIError error) onError,\n    required void Function(LocalAIStreamingState state)\n        onLocalAIStreamingStateChange,\n  }) async {\n    final stream = AppFlowyCompletionStream(\n      onStart: onStart,\n      processMessage: processMessage,\n      processAssistMessage: processAssistMessage,\n      processError: onError,\n      onLocalAIStreamingStateChange: onLocalAIStreamingStateChange,\n      onEnd: onEnd,\n    );\n\n    final records = history.map((record) => record.toPB()).toList();\n\n    final payload = CompleteTextPB(\n      text: text,\n      completionType: completionType,\n      format: format?.toPB(),\n      promptId: promptId,\n      streamPort: fixnum.Int64(stream.nativePort),\n      objectId: objectId ?? '',\n      ragIds: [\n        if (objectId != null) objectId,\n        ...sourceIds,\n      ].unique(),\n      history: records,\n    );\n\n    return AIEventCompleteText(payload).send().fold(\n      (task) => (task.taskId, stream),\n      (error) {\n        Log.error(error);\n        return null;\n      },\n    );\n  }\n\n  @override\n  Future<List<AiPrompt>> getBuiltInPrompts() async {\n    final prompts = <AiPrompt>[];\n\n    try {\n      final jsonString =\n          await rootBundle.loadString('assets/built_in_prompts.json');\n      // final data = await rootBundle.load('assets/built_in_prompts.json');\n      // final jsonString = utf8.decode(data.buffer.asUint8List());\n      final jsonData = json.decode(jsonString) as Map<String, dynamic>;\n      final promptJson = jsonData['prompts'] as List<dynamic>;\n      prompts.addAll(\n        promptJson\n            .map((e) => AiPrompt.fromJson(e as Map<String, dynamic>))\n            .toList(),\n      );\n    } catch (e) {\n      Log.error(e);\n    }\n\n    return prompts;\n  }\n\n  @override\n  Future<List<AiPrompt>?> getDatabasePrompts(\n    CustomPromptDatabaseConfigPB config,\n  ) async {\n    return DatabaseEventGetDatabaseCustomPrompts(config).send().fold(\n      (databasePromptsPB) =>\n          databasePromptsPB.items.map(AiPrompt.fromPB).toList(),\n      (err) {\n        Log.error(err);\n        return null;\n      },\n    );\n  }\n\n  @override\n  void updateFavoritePrompts(List<String> promptIds) {}\n}\n\nabstract class CompletionStream {\n  CompletionStream({\n    required this.onStart,\n    required this.processMessage,\n    required this.processAssistMessage,\n    required this.processError,\n    required this.onLocalAIStreamingStateChange,\n    required this.onEnd,\n  });\n\n  final Future<void> Function() onStart;\n  final Future<void> Function(String text) processMessage;\n  final Future<void> Function(String text) processAssistMessage;\n  final void Function(AIError error) processError;\n  final void Function(LocalAIStreamingState state)\n      onLocalAIStreamingStateChange;\n  final Future<void> Function() onEnd;\n}\n\nclass AppFlowyCompletionStream extends CompletionStream {\n  AppFlowyCompletionStream({\n    required super.onStart,\n    required super.processMessage,\n    required super.processAssistMessage,\n    required super.processError,\n    required super.onEnd,\n    required super.onLocalAIStreamingStateChange,\n  }) {\n    _startListening();\n  }\n\n  final RawReceivePort _port = RawReceivePort();\n  final StreamController<String> _controller = StreamController.broadcast();\n  late StreamSubscription<String> _subscription;\n  int get nativePort => _port.sendPort.nativePort;\n\n  void _startListening() {\n    _port.handler = _controller.add;\n    _subscription = _controller.stream.listen(\n      (event) async {\n        await _handleEvent(event);\n      },\n    );\n  }\n\n  Future<void> dispose() async {\n    await _controller.close();\n    await _subscription.cancel();\n    _port.close();\n  }\n\n  Future<void> _handleEvent(String event) async {\n    // Check simple matches first\n    if (event == AIStreamEventPrefix.aiResponseLimit) {\n      processError(\n        AIError(\n          message: LocaleKeys.ai_textLimitReachedDescription.tr(),\n          code: AIErrorCode.aiResponseLimitExceeded,\n        ),\n      );\n      return;\n    }\n\n    if (event == AIStreamEventPrefix.aiImageResponseLimit) {\n      processError(\n        AIError(\n          message: LocaleKeys.ai_imageLimitReachedDescription.tr(),\n          code: AIErrorCode.aiImageResponseLimitExceeded,\n        ),\n      );\n      return;\n    }\n\n    // Otherwise, parse out prefix:content\n    if (event.startsWith(AIStreamEventPrefix.aiMaxRequired)) {\n      processError(\n        AIError(\n          message: event.substring(AIStreamEventPrefix.aiMaxRequired.length),\n          code: AIErrorCode.other,\n        ),\n      );\n    } else if (event.startsWith(AIStreamEventPrefix.start)) {\n      await onStart();\n    } else if (event.startsWith(AIStreamEventPrefix.data)) {\n      await processMessage(\n        event.substring(AIStreamEventPrefix.data.length),\n      );\n    } else if (event.startsWith(AIStreamEventPrefix.comment)) {\n      await processAssistMessage(\n        event.substring(AIStreamEventPrefix.comment.length),\n      );\n    } else if (event.startsWith(AIStreamEventPrefix.finish)) {\n      await onEnd();\n    } else if (event.startsWith(AIStreamEventPrefix.localAIDisabled)) {\n      onLocalAIStreamingStateChange(\n        LocalAIStreamingState.disabled,\n      );\n    } else if (event.startsWith(AIStreamEventPrefix.localAINotReady)) {\n      onLocalAIStreamingStateChange(\n        LocalAIStreamingState.notReady,\n      );\n    } else if (event.startsWith(AIStreamEventPrefix.error)) {\n      processError(\n        AIError(\n          message: event.substring(AIStreamEventPrefix.error.length),\n          code: AIErrorCode.other,\n        ),\n      );\n    } else {\n      Log.debug('Unknown AI event: $event');\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/service/error.dart",
    "content": "import 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'error.freezed.dart';\npart 'error.g.dart';\n\n@freezed\nclass AIError with _$AIError {\n  const factory AIError({\n    required String message,\n    required AIErrorCode code,\n  }) = _AIError;\n\n  factory AIError.fromJson(Map<String, Object?> json) =>\n      _$AIErrorFromJson(json);\n}\n\nenum AIErrorCode {\n  @JsonValue('AIResponseLimitExceeded')\n  aiResponseLimitExceeded,\n  @JsonValue('AIImageResponseLimitExceeded')\n  aiImageResponseLimitExceeded,\n  @JsonValue('Other')\n  other,\n}\n\nextension AIErrorExtension on AIError {\n  bool get isLimitExceeded => code == AIErrorCode.aiResponseLimitExceeded;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/ai/service/ai_model_state_notifier.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/entities.pbserver.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'select_model_bloc.freezed.dart';\n\nclass SelectModelBloc extends Bloc<SelectModelEvent, SelectModelState> {\n  SelectModelBloc({\n    required AIModelStateNotifier aiModelStateNotifier,\n  })  : _aiModelStateNotifier = aiModelStateNotifier,\n        super(SelectModelState.initial(aiModelStateNotifier)) {\n    on<SelectModelEvent>(\n      (event, emit) {\n        event.when(\n          selectModel: (model) {\n            AIEventUpdateSelectedModel(\n              UpdateSelectedModelPB(\n                source: _aiModelStateNotifier.objectId,\n                selectedModel: model,\n              ),\n            ).send();\n\n            emit(state.copyWith(selectedModel: model));\n          },\n          didLoadModels: (models, selectedModel) {\n            emit(\n              SelectModelState(\n                models: models,\n                selectedModel: selectedModel,\n              ),\n            );\n          },\n        );\n      },\n    );\n\n    _aiModelStateNotifier.addListener(\n      onAvailableModelsChanged: _onAvailableModelsChanged,\n    );\n  }\n\n  final AIModelStateNotifier _aiModelStateNotifier;\n\n  @override\n  Future<void> close() async {\n    _aiModelStateNotifier.removeListener(\n      onAvailableModelsChanged: _onAvailableModelsChanged,\n    );\n    await super.close();\n  }\n\n  void _onAvailableModelsChanged(\n    List<AIModelPB> models,\n    AIModelPB? selectedModel,\n  ) {\n    if (!isClosed) {\n      add(SelectModelEvent.didLoadModels(models, selectedModel));\n    }\n  }\n}\n\n@freezed\nclass SelectModelEvent with _$SelectModelEvent {\n  const factory SelectModelEvent.selectModel(\n    AIModelPB model,\n  ) = _SelectModel;\n\n  const factory SelectModelEvent.didLoadModels(\n    List<AIModelPB> models,\n    AIModelPB? selectedModel,\n  ) = _DidLoadModels;\n}\n\n@freezed\nclass SelectModelState with _$SelectModelState {\n  const factory SelectModelState({\n    required List<AIModelPB> models,\n    required AIModelPB? selectedModel,\n  }) = _SelectModelState;\n\n  factory SelectModelState.initial(AIModelStateNotifier notifier) {\n    final (models, selectedModel) = notifier.getModelSelection();\n    return SelectModelState(\n      models: models,\n      selectedModel: selectedModel,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/service/view_selector_cubit.dart",
    "content": "import 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'view_selector_cubit.freezed.dart';\n\nenum ViewSelectedStatus {\n  unselected,\n  selected,\n  partiallySelected;\n\n  bool get isUnselected => this == unselected;\n  bool get isSelected => this == selected;\n  bool get isPartiallySelected => this == partiallySelected;\n}\n\nclass ViewSelectorItem {\n  ViewSelectorItem({\n    required this.view,\n    required this.parentView,\n    required this.children,\n    required bool isExpanded,\n    required ViewSelectedStatus selectedStatus,\n    required bool isDisabled,\n  })  : isExpandedNotifier = ValueNotifier(isExpanded),\n        selectedStatusNotifier = ValueNotifier(selectedStatus),\n        isDisabledNotifier = ValueNotifier(isDisabled);\n\n  final ViewPB view;\n  final ViewPB? parentView;\n  final List<ViewSelectorItem> children;\n  final ValueNotifier<bool> isExpandedNotifier;\n  final ValueNotifier<bool> isDisabledNotifier;\n  final ValueNotifier<ViewSelectedStatus> selectedStatusNotifier;\n\n  bool get isExpanded => isExpandedNotifier.value;\n  ViewSelectedStatus get selectedStatus => selectedStatusNotifier.value;\n  bool get isDisabled => isDisabledNotifier.value;\n\n  void toggleIsExpanded() {\n    isExpandedNotifier.value = !isExpandedNotifier.value;\n  }\n\n  ViewSelectorItem copy() {\n    return ViewSelectorItem(\n      view: view,\n      parentView: parentView,\n      children:\n          children.map<ViewSelectorItem>((child) => child.copy()).toList(),\n      isDisabled: isDisabledNotifier.value,\n      isExpanded: isExpandedNotifier.value,\n      selectedStatus: selectedStatusNotifier.value,\n    );\n  }\n\n  ViewSelectorItem? findChildBySourceId(String sourceId) {\n    if (view.id == sourceId) {\n      return this;\n    }\n    for (final child in children) {\n      final childResult = child.findChildBySourceId(sourceId);\n      if (childResult != null) {\n        return childResult;\n      }\n    }\n    return null;\n  }\n\n  void setIsDisabledRecursive(bool Function(ViewSelectorItem) newIsDisabled) {\n    isDisabledNotifier.value = newIsDisabled(this);\n\n    for (final child in children) {\n      child.setIsDisabledRecursive(newIsDisabled);\n    }\n  }\n\n  void setIsSelectedStatusRecursive(ViewSelectedStatus selectedStatus) {\n    selectedStatusNotifier.value = selectedStatus;\n\n    for (final child in children) {\n      child.setIsSelectedStatusRecursive(selectedStatus);\n    }\n  }\n\n  void dispose() {\n    for (final child in children) {\n      child.dispose();\n    }\n    isExpandedNotifier.dispose();\n    selectedStatusNotifier.dispose();\n    isDisabledNotifier.dispose();\n  }\n}\n\nclass ViewSelectorCubit extends Cubit<ViewSelectorState> {\n  ViewSelectorCubit({\n    required this.getIgnoreViewType,\n    this.maxSelectedParentPageCount,\n  }) : super(ViewSelectorState.initial()) {\n    filterTextController.addListener(onFilterChanged);\n  }\n\n  final IgnoreViewType Function(ViewSelectorItem) getIgnoreViewType;\n  final int? maxSelectedParentPageCount;\n\n  final List<String> selectedSourceIds = [];\n  final List<ViewSelectorItem> sources = [];\n  final List<ViewSelectorItem> selectedSources = [];\n  final filterTextController = TextEditingController();\n\n  void updateSelectedSources(List<String> newSelectedSourceIds) {\n    selectedSourceIds.clear();\n    selectedSourceIds.addAll(newSelectedSourceIds);\n  }\n\n  Future<void> refreshSources(\n    List<ViewPB> spaceViews,\n    ViewPB? currentSpace,\n  ) async {\n    filterTextController.clear();\n\n    final newSources = await Future.wait(\n      spaceViews.map((view) => _recursiveBuild(view, null)),\n    );\n\n    _setIsDisabledAndHideIfNecessary(newSources);\n\n    _restrictSelectionIfNecessary(newSources);\n\n    if (currentSpace != null) {\n      newSources\n          .firstWhereOrNull((e) => e.view.id == currentSpace.id)\n          ?.toggleIsExpanded();\n    }\n\n    final selected = newSources\n        .map((source) => _buildSelectedSources(source))\n        .flattened\n        .toList();\n\n    emit(\n      state.copyWith(\n        selectedSources: selected,\n        visibleSources: newSources,\n      ),\n    );\n\n    sources\n      ..forEach((e) => e.dispose())\n      ..clear()\n      ..addAll(newSources.map((e) => e.copy()));\n\n    selectedSources\n      ..forEach((e) => e.dispose())\n      ..clear()\n      ..addAll(selected.map((e) => e.copy()));\n  }\n\n  Future<ViewSelectorItem> _recursiveBuild(\n    ViewPB view,\n    ViewPB? parentView,\n  ) async {\n    ViewSelectedStatus selectedStatus = ViewSelectedStatus.unselected;\n    final isThisSourceSelected = selectedSourceIds.contains(view.id);\n\n    final List<ViewPB>? childrenViews;\n    if (integrationMode().isTest) {\n      childrenViews = view.childViews;\n    } else {\n      childrenViews =\n          await ViewBackendService.getChildViews(viewId: view.id).toNullable();\n    }\n\n    int selectedCount = 0;\n    final children = <ViewSelectorItem>[];\n\n    if (childrenViews != null) {\n      for (final childView in childrenViews) {\n        final childItem = await _recursiveBuild(childView, view);\n        if (childItem.selectedStatus.isSelected) {\n          selectedCount++;\n        }\n        children.add(childItem);\n      }\n\n      final areAllChildrenSelectedOrNoChildren =\n          children.length == selectedCount;\n      final isAnyChildNotUnselected =\n          children.any((e) => !e.selectedStatus.isUnselected);\n\n      if (isThisSourceSelected && areAllChildrenSelectedOrNoChildren) {\n        selectedStatus = ViewSelectedStatus.selected;\n      } else if (isThisSourceSelected || isAnyChildNotUnselected) {\n        selectedStatus = ViewSelectedStatus.partiallySelected;\n      }\n    } else if (isThisSourceSelected) {\n      selectedStatus = ViewSelectedStatus.selected;\n    }\n\n    return ViewSelectorItem(\n      view: view,\n      parentView: parentView,\n      children: children,\n      isDisabled: false,\n      isExpanded: false,\n      selectedStatus: selectedStatus,\n    );\n  }\n\n  void _setIsDisabledAndHideIfNecessary(\n    List<ViewSelectorItem> sources,\n  ) {\n    sources.retainWhere((source) {\n      final ignoreViewType = getIgnoreViewType(source);\n      return ignoreViewType != IgnoreViewType.hide;\n    });\n\n    for (final source in sources) {\n      source.isDisabledNotifier.value =\n          getIgnoreViewType(source) == IgnoreViewType.disable;\n      _setIsDisabledAndHideIfNecessary(source.children);\n    }\n  }\n\n  void _restrictSelectionIfNecessary(List<ViewSelectorItem> sources) {\n    if (maxSelectedParentPageCount == null) {\n      return;\n    }\n    for (final source in sources) {\n      source.setIsDisabledRecursive((view) {\n        return getIgnoreViewType(view) == IgnoreViewType.disable;\n      });\n    }\n    if (sources.where((e) => !e.selectedStatus.isUnselected).length >=\n        maxSelectedParentPageCount!) {\n      sources\n          .where((e) => e.selectedStatus == ViewSelectedStatus.unselected)\n          .forEach(\n            (e) => e.setIsDisabledRecursive((_) => true),\n          );\n    }\n  }\n\n  void onFilterChanged() {\n    for (final source in state.visibleSources) {\n      source.dispose();\n    }\n    if (sources.isEmpty) {\n      emit(ViewSelectorState.initial());\n    } else {\n      final selected =\n          selectedSources.map(_buildSearchResults).nonNulls.toList();\n      final visible =\n          sources.map(_buildSearchResults).nonNulls.nonNulls.toList();\n      emit(\n        state.copyWith(\n          selectedSources: selected,\n          visibleSources: visible,\n        ),\n      );\n    }\n  }\n\n  /// traverse tree to build up search query\n  ViewSelectorItem? _buildSearchResults(ViewSelectorItem item) {\n    final isVisible = item.view.nameOrDefault\n        .toLowerCase()\n        .contains(filterTextController.text.toLowerCase());\n\n    final childrenResults = <ViewSelectorItem>[];\n    for (final childSource in item.children) {\n      final childResult = _buildSearchResults(childSource);\n      if (childResult != null) {\n        childrenResults.add(childResult);\n      }\n    }\n\n    return isVisible || childrenResults.isNotEmpty\n        ? ViewSelectorItem(\n            view: item.view,\n            parentView: item.parentView,\n            children: childrenResults,\n            isDisabled: item.isDisabled,\n            isExpanded: item.isExpanded,\n            selectedStatus: item.selectedStatus,\n          )\n        : null;\n  }\n\n  /// traverse tree to build up selected sources\n  Iterable<ViewSelectorItem> _buildSelectedSources(\n    ViewSelectorItem item,\n  ) {\n    final children = <ViewSelectorItem>[];\n\n    for (final childSource in item.children) {\n      children.addAll(_buildSelectedSources(childSource));\n    }\n\n    return selectedSourceIds.contains(item.view.id)\n        ? [\n            ViewSelectorItem(\n              view: item.view,\n              parentView: item.parentView,\n              children: children,\n              isDisabled: item.isDisabled,\n              selectedStatus: item.selectedStatus,\n              isExpanded: true,\n            ),\n          ]\n        : children;\n  }\n\n  void toggleSelectedStatus(ViewSelectorItem item, bool isSelectedSection) {\n    if (item.view.isSpace) {\n      return;\n    }\n    final allIds = _recursiveGetSourceIds(item);\n\n    if (item.selectedStatus.isUnselected ||\n        item.selectedStatus.isPartiallySelected &&\n            !item.view.layout.isDocumentView) {\n      for (final id in allIds) {\n        if (!selectedSourceIds.contains(id)) {\n          selectedSourceIds.add(id);\n        }\n      }\n    } else {\n      for (final id in allIds) {\n        if (selectedSourceIds.contains(id)) {\n          selectedSourceIds.remove(id);\n        }\n      }\n    }\n\n    if (isSelectedSection) {\n      item.setIsSelectedStatusRecursive(\n        item.selectedStatus.isUnselected ||\n                item.selectedStatus.isPartiallySelected\n            ? ViewSelectedStatus.selected\n            : ViewSelectedStatus.unselected,\n      );\n    }\n\n    updateSelectedStatus();\n  }\n\n  List<String> _recursiveGetSourceIds(ViewSelectorItem item) {\n    return [\n      if (item.view.layout.isDocumentView) item.view.id,\n      for (final childSource in item.children)\n        ..._recursiveGetSourceIds(childSource),\n    ];\n  }\n\n  void updateSelectedStatus() {\n    if (sources.isEmpty) {\n      return;\n    }\n    for (final source in sources) {\n      _recursiveUpdateSelectedStatus(source);\n    }\n    _restrictSelectionIfNecessary(sources);\n    for (final visibleSource in state.visibleSources) {\n      visibleSource.dispose();\n    }\n    final visible = sources.map(_buildSearchResults).nonNulls.toList();\n\n    emit(\n      state.copyWith(\n        visibleSources: visible,\n      ),\n    );\n  }\n\n  ViewSelectedStatus _recursiveUpdateSelectedStatus(ViewSelectorItem item) {\n    ViewSelectedStatus selectedStatus = ViewSelectedStatus.unselected;\n\n    int selectedCount = 0;\n    for (final childSource in item.children) {\n      final childStatus = _recursiveUpdateSelectedStatus(childSource);\n      if (childStatus.isSelected) {\n        selectedCount++;\n      }\n    }\n\n    final isThisSourceSelected = selectedSourceIds.contains(item.view.id);\n    final areAllChildrenSelectedOrNoChildren =\n        item.children.length == selectedCount;\n    final isAnyChildNotUnselected =\n        item.children.any((e) => !e.selectedStatus.isUnselected);\n\n    if (isThisSourceSelected && areAllChildrenSelectedOrNoChildren) {\n      selectedStatus = ViewSelectedStatus.selected;\n    } else if (isThisSourceSelected || isAnyChildNotUnselected) {\n      selectedStatus = ViewSelectedStatus.partiallySelected;\n    }\n\n    item.selectedStatusNotifier.value = selectedStatus;\n    return selectedStatus;\n  }\n\n  void toggleIsExpanded(ViewSelectorItem item, bool isSelectedSection) {\n    item.toggleIsExpanded();\n    if (isSelectedSection) {\n      for (final selectedSource in selectedSources) {\n        selectedSource.findChildBySourceId(item.view.id)?.toggleIsExpanded();\n      }\n    } else {\n      for (final source in sources) {\n        final child = source.findChildBySourceId(item.view.id);\n        if (child != null) {\n          child.toggleIsExpanded();\n          break;\n        }\n      }\n    }\n  }\n\n  @override\n  Future<void> close() {\n    for (final child in sources) {\n      child.dispose();\n    }\n    for (final child in selectedSources) {\n      child.dispose();\n    }\n    for (final child in state.selectedSources) {\n      child.dispose();\n    }\n    for (final child in state.visibleSources) {\n      child.dispose();\n    }\n    filterTextController.dispose();\n    return super.close();\n  }\n}\n\n@freezed\nclass ViewSelectorState with _$ViewSelectorState {\n  const factory ViewSelectorState({\n    required List<ViewSelectorItem> visibleSources,\n    required List<ViewSelectorItem> selectedSources,\n  }) = _ViewSelectorState;\n\n  factory ViewSelectorState.initial() => const ViewSelectorState(\n        visibleSources: [],\n        selectedSources: [],\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_category_list.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass AiPromptCategoryList extends StatefulWidget {\n  const AiPromptCategoryList({\n    super.key,\n  });\n\n  @override\n  State<AiPromptCategoryList> createState() => _AiPromptCategoryListState();\n}\n\nclass _AiPromptCategoryListState extends State<AiPromptCategoryList> {\n  bool isSearching = false;\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return TextFieldTapRegion(\n      groupId: \"ai_prompt_category_list\",\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.stretch,\n        children: [\n          Padding(\n            padding: EdgeInsets.only(\n              right: theme.spacing.l,\n            ),\n            child: AiPromptFeaturedSection(),\n          ),\n          Padding(\n            padding: EdgeInsets.only(\n              right: theme.spacing.l,\n            ),\n            child: AiPromptCustomPromptSection(),\n          ),\n          Padding(\n            padding: EdgeInsets.only(\n              top: theme.spacing.s,\n              right: theme.spacing.l,\n            ),\n            child: AFDivider(),\n          ),\n          Expanded(\n            child: ListView(\n              padding: EdgeInsets.only(\n                top: theme.spacing.s,\n                right: theme.spacing.l,\n              ),\n              children: [\n                _buildCategoryItem(context, null),\n                ...sortedCategories.map(\n                  (category) => _buildCategoryItem(\n                    context,\n                    category,\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  static Iterable<AiPromptCategory> get sortedCategories {\n    final categories = [...AiPromptCategory.values];\n    categories\n      ..sort((a, b) => a.i18n.compareTo(b.i18n))\n      ..remove(AiPromptCategory.other)\n      ..add(AiPromptCategory.other);\n\n    return categories;\n  }\n\n  Widget _buildCategoryItem(\n    BuildContext context,\n    AiPromptCategory? category,\n  ) {\n    return AiPromptCategoryItem(\n      category: category,\n      onSelect: () {\n        context.read<AiPromptSelectorCubit>().selectCategory(category);\n      },\n    );\n  }\n}\n\nclass AiPromptFeaturedSection extends StatelessWidget {\n  const AiPromptFeaturedSection({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final isSelected = context.watch<AiPromptSelectorCubit>().state.maybeMap(\n          ready: (state) => state.isFeaturedSectionSelected,\n          orElse: () => false,\n        );\n\n    return AFBaseButton(\n      onTap: () {\n        if (!isSelected) {\n          context.read<AiPromptSelectorCubit>().selectFeaturedSection();\n        }\n      },\n      builder: (context, isHovering, disabled) {\n        return Text(\n          LocaleKeys.ai_customPrompt_featured.tr(),\n          style: AppFlowyTheme.of(context).textStyle.body.standard(\n                color: theme.textColorScheme.primary,\n              ),\n          overflow: TextOverflow.ellipsis,\n        );\n      },\n      borderRadius: theme.borderRadius.m,\n      padding: EdgeInsets.symmetric(\n        vertical: theme.spacing.s,\n        horizontal: theme.spacing.m,\n      ),\n      borderColor: (context, isHovering, disabled, isFocused) =>\n          Colors.transparent,\n      backgroundColor: (context, isHovering, disabled) {\n        if (isSelected) {\n          return theme.fillColorScheme.themeSelect;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.contentHover;\n        }\n        return Colors.transparent;\n      },\n    );\n  }\n}\n\nclass AiPromptCustomPromptSection extends StatelessWidget {\n  const AiPromptCustomPromptSection({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return BlocBuilder<AiPromptSelectorCubit, AiPromptSelectorState>(\n      builder: (context, state) {\n        return state.maybeMap(\n          ready: (readyState) {\n            final isSelected = readyState.isCustomPromptSectionSelected;\n\n            return AFBaseButton(\n              onTap: () {\n                if (!isSelected) {\n                  context.read<AiPromptSelectorCubit>().selectCustomSection();\n                }\n              },\n              builder: (context, isHovering, disabled) {\n                return Text(\n                  LocaleKeys.ai_customPrompt_custom.tr(),\n                  style: AppFlowyTheme.of(context).textStyle.body.standard(\n                        color: theme.textColorScheme.primary,\n                      ),\n                  overflow: TextOverflow.ellipsis,\n                );\n              },\n              borderRadius: theme.borderRadius.m,\n              padding: EdgeInsets.symmetric(\n                vertical: theme.spacing.s,\n                horizontal: theme.spacing.m,\n              ),\n              borderColor: (context, isHovering, disabled, isFocused) =>\n                  Colors.transparent,\n              backgroundColor: (context, isHovering, disabled) {\n                if (isSelected) {\n                  return theme.fillColorScheme.themeSelect;\n                }\n                if (isHovering) {\n                  return theme.fillColorScheme.contentHover;\n                }\n                return Colors.transparent;\n              },\n            );\n          },\n          orElse: () => const SizedBox.shrink(),\n        );\n      },\n    );\n  }\n}\n\nclass AiPromptCategoryItem extends StatelessWidget {\n  const AiPromptCategoryItem({\n    super.key,\n    required this.category,\n    required this.onSelect,\n  });\n\n  final AiPromptCategory? category;\n  final VoidCallback onSelect;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<AiPromptSelectorCubit, AiPromptSelectorState>(\n      builder: (context, state) {\n        final theme = AppFlowyTheme.of(context);\n        final isSelected = state.maybeMap(\n          ready: (state) {\n            return !state.isFeaturedSectionSelected &&\n                !state.isCustomPromptSectionSelected &&\n                state.selectedCategory == category;\n          },\n          orElse: () => false,\n        );\n\n        return AFBaseButton(\n          onTap: onSelect,\n          builder: (context, isHovering, disabled) {\n            return Text(\n              category?.i18n ?? LocaleKeys.ai_customPrompt_all.tr(),\n              style: AppFlowyTheme.of(context).textStyle.body.standard(\n                    color: theme.textColorScheme.primary,\n                  ),\n              overflow: TextOverflow.ellipsis,\n            );\n          },\n          borderRadius: theme.borderRadius.m,\n          padding: EdgeInsets.symmetric(\n            vertical: theme.spacing.s,\n            horizontal: theme.spacing.m,\n          ),\n          borderColor: (context, isHovering, disabled, isFocused) =>\n              Colors.transparent,\n          backgroundColor: (context, isHovering, disabled) {\n            if (isSelected) {\n              return theme.fillColorScheme.themeSelect;\n            }\n            if (isHovering) {\n              return theme.fillColorScheme.contentHover;\n            }\n            return Colors.transparent;\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_database_modal.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/ai/service/ai_prompt_database_selector_cubit.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:equatable/equatable.dart';\nimport 'package:expandable/expandable.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nFuture<CustomPromptDatabaseConfig?> changeCustomPromptDatabaseConfig(\n  BuildContext context, {\n  CustomPromptDatabaseConfig? config,\n}) async {\n  return showDialog<CustomPromptDatabaseConfig?>(\n    context: context,\n    builder: (_) {\n      return MultiBlocProvider(\n        providers: [\n          BlocProvider.value(\n            value: context.read<UserWorkspaceBloc>(),\n          ),\n          BlocProvider(\n            create: (context) => AiPromptDatabaseSelectorCubit(\n              configuration: config,\n            ),\n          ),\n        ],\n        child: const AiPromptDatabaseModal(),\n      );\n    },\n  );\n}\n\nclass AiPromptDatabaseModal extends StatefulWidget {\n  const AiPromptDatabaseModal({\n    super.key,\n  });\n\n  @override\n  State<AiPromptDatabaseModal> createState() => _AiPromptDatabaseModalState();\n}\n\nclass _AiPromptDatabaseModalState extends State<AiPromptDatabaseModal> {\n  final expandableController = ExpandableController(initialExpanded: false);\n\n  @override\n  void dispose() {\n    expandableController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return BlocListener<AiPromptDatabaseSelectorCubit,\n        AiPromptDatabaseSelectorState>(\n      listener: (context, state) {\n        state.maybeMap(\n          invalidDatabase: (_) {\n            showSimpleAFDialog(\n              context: context,\n              title: LocaleKeys.ai_customPrompt_invalidDatabase.tr(),\n              content: LocaleKeys.ai_customPrompt_invalidDatabaseHelp.tr(),\n              primaryAction: (\n                LocaleKeys.button_ok.tr(),\n                (context) {},\n              ),\n            );\n          },\n          empty: (_) => expandableController.expanded = false,\n          selected: (_) => expandableController.expanded = true,\n          orElse: () {},\n        );\n      },\n      child: AFModal(\n        constraints: const BoxConstraints(\n          maxWidth: 450,\n          maxHeight: 400,\n        ),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.stretch,\n          children: [\n            AFModalHeader(\n              leading: Text(\n                LocaleKeys.ai_customPrompt_configureDatabase.tr(),\n                style: theme.textStyle.heading4.prominent(\n                  color: theme.textColorScheme.primary,\n                ),\n              ),\n              trailing: [\n                AFGhostButton.normal(\n                  onTap: () => Navigator.of(context).pop(),\n                  padding: EdgeInsets.all(theme.spacing.xs),\n                  builder: (context, isHovering, disabled) {\n                    return Center(\n                      child: FlowySvg(\n                        FlowySvgs.toast_close_s,\n                        size: Size.square(20),\n                      ),\n                    );\n                  },\n                ),\n              ],\n            ),\n            Flexible(\n              child: AFModalBody(\n                child: ExpandablePanel(\n                  controller: expandableController,\n                  theme: ExpandableThemeData(\n                    tapBodyToCollapse: false,\n                    hasIcon: false,\n                    tapBodyToExpand: false,\n                    tapHeaderToExpand: false,\n                  ),\n                  header: const _Header(),\n                  collapsed: const SizedBox.shrink(),\n                  expanded: const _Expanded(),\n                ),\n              ),\n            ),\n            AFModalFooter(\n              trailing: [\n                AFOutlinedButton.normal(\n                  onTap: () => Navigator.of(context).pop(),\n                  builder: (context, isHovering, disabled) {\n                    return Text(\n                      LocaleKeys.button_cancel.tr(),\n                      style: theme.textStyle.body.standard(\n                        color: theme.textColorScheme.primary,\n                      ),\n                    );\n                  },\n                ),\n                AFFilledButton.primary(\n                  onTap: () {\n                    final config = context\n                        .read<AiPromptDatabaseSelectorCubit>()\n                        .state\n                        .maybeMap(\n                          selected: (state) => state.config,\n                          orElse: () => null,\n                        );\n                    Navigator.of(context).pop(config);\n                  },\n                  builder: (context, isHovering, disabled) {\n                    return Text(\n                      LocaleKeys.button_done.tr(),\n                      style: theme.textStyle.body.enhanced(\n                        color: theme.textColorScheme.onFill,\n                      ),\n                    );\n                  },\n                ),\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _Header extends StatefulWidget {\n  const _Header();\n\n  @override\n  State<_Header> createState() => _HeaderState();\n}\n\nclass _HeaderState extends State<_Header> {\n  final popoverController = AFPopoverController();\n\n  @override\n  void dispose() {\n    popoverController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return BlocBuilder<AiPromptDatabaseSelectorCubit,\n        AiPromptDatabaseSelectorState>(\n      builder: (context, state) {\n        bool showNothing = false;\n        String? viewName;\n        state.maybeMap(\n          empty: (_) {\n            showNothing = false;\n            viewName = null;\n          },\n          selected: (selectedState) {\n            showNothing = false;\n            viewName = selectedState.config.view.nameOrDefault;\n          },\n          orElse: () {\n            showNothing = true;\n            viewName = null;\n          },\n        );\n\n        if (showNothing) {\n          return SizedBox.shrink();\n        }\n\n        return Padding(\n          padding: EdgeInsets.symmetric(\n            horizontal: theme.spacing.m,\n            vertical: theme.spacing.xl,\n          ),\n          child: Row(\n            spacing: theme.spacing.s,\n            children: [\n              Expanded(\n                child: Text(\n                  LocaleKeys.ai_customPrompt_selectDatabase.tr(),\n                  style: theme.textStyle.body.standard(\n                    color: theme.textColorScheme.secondary,\n                  ),\n                ),\n              ),\n              Expanded(\n                child: Center(\n                  child: ViewSelector(\n                    viewSelectorCubit: BlocProvider(\n                      create: (context) => ViewSelectorCubit(\n                        getIgnoreViewType: getIgnoreViewType,\n                      ),\n                    ),\n                    child: BlocSelector<SpaceBloc, SpaceState,\n                        (List<ViewPB>, ViewPB?)>(\n                      selector: (state) => (state.spaces, state.currentSpace),\n                      builder: (context, state) {\n                        return AFPopover(\n                          controller: popoverController,\n                          decoration: BoxDecoration(\n                            color: theme.surfaceColorScheme.primary,\n                            borderRadius:\n                                BorderRadius.circular(theme.borderRadius.l),\n                            border: Border.all(\n                              color: theme.borderColorScheme.primary,\n                            ),\n                            boxShadow: theme.shadow.medium,\n                          ),\n                          padding: EdgeInsets.zero,\n                          anchor: AFAnchor(\n                            childAlignment: Alignment.topCenter,\n                            overlayAlignment: Alignment.bottomCenter,\n                            offset: Offset(0, theme.spacing.xs),\n                          ),\n                          popover: (context) {\n                            return _PopoverContent(\n                              onSelectViewItem: (item) {\n                                context\n                                    .read<AiPromptDatabaseSelectorCubit>()\n                                    .selectDatabaseView(item.view.id);\n                                popoverController.hide();\n                              },\n                            );\n                          },\n                          child: AFOutlinedButton.normal(\n                            onTap: () {\n                              context\n                                  .read<ViewSelectorCubit>()\n                                  .refreshSources(state.$1, state.$2);\n                              popoverController.toggle();\n                            },\n                            builder: (context, isHovering, disabled) {\n                              return Row(\n                                mainAxisSize: MainAxisSize.min,\n                                spacing: theme.spacing.s,\n                                children: [\n                                  Flexible(\n                                    child: Text(\n                                      viewName ??\n                                          LocaleKeys\n                                              .ai_customPrompt_selectDatabase\n                                              .tr(),\n                                      style: theme.textStyle.body.enhanced(\n                                        color: theme.textColorScheme.primary,\n                                      ),\n                                      maxLines: 1,\n                                      overflow: TextOverflow.ellipsis,\n                                    ),\n                                  ),\n                                  FlowySvg(\n                                    FlowySvgs.toolbar_arrow_down_m,\n                                    color: theme.iconColorScheme.primary,\n                                    size: Size(12, 20),\n                                  ),\n                                ],\n                              );\n                            },\n                          ),\n                        );\n                      },\n                    ),\n                  ),\n                ),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  IgnoreViewType getIgnoreViewType(ViewSelectorItem item) {\n    final layout = item.view.layout;\n\n    if (layout.isDatabaseView) {\n      return IgnoreViewType.none;\n    }\n    if (layout.isDocumentView) {\n      return hasDatabaseDescendent(item)\n          ? IgnoreViewType.none\n          : IgnoreViewType.hide;\n    }\n    return IgnoreViewType.hide;\n  }\n\n  bool hasDatabaseDescendent(ViewSelectorItem viewSelectorItem) {\n    final layout = viewSelectorItem.view.layout;\n\n    if (layout == ViewLayoutPB.Chat) {\n      return false;\n    }\n\n    if (layout.isDatabaseView) {\n      return true;\n    }\n\n    // document may have children\n    return viewSelectorItem.children.any(\n      (child) => hasDatabaseDescendent(child),\n    );\n  }\n}\n\nclass _Expanded extends StatelessWidget {\n  const _Expanded();\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return BlocBuilder<AiPromptDatabaseSelectorCubit,\n        AiPromptDatabaseSelectorState>(\n      builder: (context, state) {\n        return state.maybeMap(\n          orElse: () => SizedBox.shrink(),\n          selected: (selectedState) {\n            return Padding(\n              padding: EdgeInsets.all(theme.spacing.m),\n              child: Column(\n                mainAxisSize: MainAxisSize.min,\n                crossAxisAlignment: CrossAxisAlignment.start,\n                spacing: theme.spacing.m,\n                children: [\n                  FieldSelector(\n                    title: LocaleKeys.ai_customPrompt_title.tr(),\n                    currentFieldId: selectedState.config.titleFieldId,\n                    isDisabled: true,\n                    fields: selectedState.fields,\n                    onSelect: (id) {},\n                  ),\n                  FieldSelector(\n                    title: LocaleKeys.ai_customPrompt_content.tr(),\n                    currentFieldId: selectedState.config.contentFieldId,\n                    fields: selectedState.fields\n                        .where((f) => f.fieldType == FieldType.RichText)\n                        .toList(),\n                    onSelect: (id) {\n                      if (id != null) {\n                        context\n                            .read<AiPromptDatabaseSelectorCubit>()\n                            .selectContentField(id);\n                      }\n                    },\n                  ),\n                  FieldSelector(\n                    title: LocaleKeys.ai_customPrompt_example.tr(),\n                    currentFieldId: selectedState.config.exampleFieldId,\n                    isOptional: true,\n                    fields: selectedState.fields\n                        .where((f) => f.fieldType == FieldType.RichText)\n                        .toList(),\n                    onSelect: (id) {\n                      context\n                          .read<AiPromptDatabaseSelectorCubit>()\n                          .selectExampleField(id);\n                    },\n                  ),\n                  FieldSelector(\n                    title: LocaleKeys.ai_customPrompt_category.tr(),\n                    currentFieldId: selectedState.config.categoryFieldId,\n                    isOptional: true,\n                    fields: selectedState.fields\n                        .where(\n                          (f) =>\n                              f.fieldType == FieldType.RichText ||\n                              f.fieldType == FieldType.SingleSelect ||\n                              f.fieldType == FieldType.MultiSelect,\n                        )\n                        .toList(),\n                    onSelect: (id) {\n                      context\n                          .read<AiPromptDatabaseSelectorCubit>()\n                          .selectCategoryField(id);\n                    },\n                  ),\n                ],\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass _PopoverContent extends StatefulWidget {\n  const _PopoverContent({\n    required this.onSelectViewItem,\n  });\n\n  final void Function(ViewSelectorItem item) onSelectViewItem;\n\n  @override\n  State<_PopoverContent> createState() => _PopoverContentState();\n}\n\nclass _PopoverContentState extends State<_PopoverContent> {\n  final focusNode = FocusNode();\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      focusNode.requestFocus();\n    });\n  }\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return ConstrainedBox(\n      constraints: const BoxConstraints.tightFor(\n        width: 300,\n        height: 400,\n      ),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          VSpace(\n            theme.spacing.m,\n          ),\n          Padding(\n            padding: EdgeInsets.symmetric(\n              horizontal: theme.spacing.m,\n            ),\n            child: AFTextField(\n              focusNode: focusNode,\n              size: AFTextFieldSize.m,\n              hintText: LocaleKeys.search_label.tr(),\n              controller:\n                  context.read<ViewSelectorCubit>().filterTextController,\n            ),\n          ),\n          VSpace(\n            theme.spacing.m,\n          ),\n          AFDivider(),\n          Expanded(\n            child: BlocBuilder<ViewSelectorCubit, ViewSelectorState>(\n              builder: (context, state) {\n                return ListView(\n                  shrinkWrap: true,\n                  padding: const EdgeInsets.fromLTRB(8, 4, 8, 12),\n                  children: _buildVisibleSources(context, state).toList(),\n                );\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Iterable<Widget> _buildVisibleSources(\n    BuildContext context,\n    ViewSelectorState state,\n  ) {\n    return state.visibleSources.map(\n      (e) => ViewSelectorTreeItem(\n        key: ValueKey(\n          'custom_prompt_database_tree_item_${e.view.id}',\n        ),\n        viewSelectorItem: e,\n        level: 0,\n        isDescendentOfSpace: e.view.isSpace,\n        isSelectedSection: false,\n        showCheckbox: false,\n        onSelected: (item) {\n          if (item.view.isDocument || item.view.isSpace) {\n            context.read<ViewSelectorCubit>().toggleIsExpanded(item, false);\n            return;\n          }\n          widget.onSelectViewItem(item);\n        },\n        height: 30.0,\n      ),\n    );\n  }\n}\n\nclass _FieldPBWrapper extends Equatable with AFDropDownMenuMixin {\n  const _FieldPBWrapper(this.field);\n\n  final FieldPB field;\n\n  @override\n  String get label => field.name;\n\n  @override\n  List<Object?> get props => [field.id];\n}\n\nclass FieldSelector extends StatelessWidget {\n  const FieldSelector({\n    super.key,\n    required this.title,\n    required this.currentFieldId,\n    this.isDisabled = false,\n    this.isOptional = false,\n    this.fields = const [],\n    required this.onSelect,\n  });\n\n  final String title;\n  final String? currentFieldId;\n  final bool isDisabled;\n  final bool isOptional;\n  final List<FieldPB> fields;\n  final void Function(String? id)? onSelect;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    final selectedField = fields.firstWhereOrNull(\n      (field) => field.id == currentFieldId,\n    );\n\n    return Row(\n      spacing: theme.spacing.s,\n      children: [\n        Expanded(\n          child: Text(\n            title,\n            style: theme.textStyle.body.standard(\n              color: theme.textColorScheme.secondary,\n            ),\n          ),\n        ),\n        Expanded(\n          child: AFDropDownMenu<_FieldPBWrapper>(\n            isDisabled: isDisabled,\n            items: fields.map((field) => _FieldPBWrapper(field)).toList(),\n            selectedItems: [\n              if (selectedField != null) _FieldPBWrapper(selectedField),\n            ],\n            clearIcon: selectedField == null ||\n                    !fields.contains(selectedField) ||\n                    !isOptional\n                ? null\n                : MouseRegion(\n                    cursor: SystemMouseCursors.click,\n                    child: GestureDetector(\n                      onTap: () {\n                        onSelect?.call(null);\n                      },\n                      child: FlowySvg(\n                        FlowySvgs.search_clear_m,\n                        size: Size.square(16),\n                        color: theme.iconColorScheme.tertiary,\n                      ),\n                    ),\n                  ),\n            onSelected: (value) {\n              if (value == null) {\n                return;\n              }\n              onSelect?.call(value.field.id);\n            },\n            dropdownIcon: FlowySvg(\n              FlowySvgs.toolbar_arrow_down_m,\n              color: theme.iconColorScheme.primary,\n              size: Size(12, 20),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_modal.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/user/prelude.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'ai_prompt_category_list.dart';\nimport 'ai_prompt_onboarding.dart';\nimport 'ai_prompt_preview.dart';\nimport 'ai_prompt_visible_list.dart';\n\nFuture<AiPrompt?> showAiPromptModal(\n  BuildContext context, {\n  required AiPromptSelectorCubit aiPromptSelectorCubit,\n}) async {\n  aiPromptSelectorCubit.loadCustomPrompts();\n\n  return showDialog<AiPrompt?>(\n    context: context,\n    builder: (_) {\n      return MultiBlocProvider(\n        providers: [\n          BlocProvider.value(\n            value: aiPromptSelectorCubit,\n          ),\n          BlocProvider.value(\n            value: context.read<UserWorkspaceBloc>(),\n          ),\n        ],\n        child: const AiPromptModal(),\n      );\n    },\n  );\n}\n\nclass AiPromptModal extends StatelessWidget {\n  const AiPromptModal({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return AFModal(\n      backgroundColor: theme.backgroundColorScheme.primary,\n      constraints: const BoxConstraints(\n        maxWidth: 1200,\n        maxHeight: 800,\n      ),\n      child: BlocListener<AiPromptSelectorCubit, AiPromptSelectorState>(\n        listener: (context, state) {\n          state.maybeMap(\n            invalidDatabase: (_) {\n              showLoadPromptFailedDialog(context);\n            },\n            orElse: () {},\n          );\n        },\n        child: Column(\n          children: [\n            AFModalHeader(\n              leading: Text(\n                LocaleKeys.ai_customPrompt_browsePrompts.tr(),\n                style: theme.textStyle.heading4.prominent(\n                  color: theme.textColorScheme.primary,\n                ),\n              ),\n              trailing: [\n                AFGhostButton.normal(\n                  onTap: () => Navigator.of(context).pop(),\n                  padding: EdgeInsets.all(theme.spacing.xs),\n                  builder: (context, isHovering, disabled) {\n                    return Center(\n                      child: FlowySvg(\n                        FlowySvgs.toast_close_s,\n                        size: Size.square(20),\n                      ),\n                    );\n                  },\n                ),\n              ],\n            ),\n            Expanded(\n              child: AFModalBody(\n                child:\n                    BlocBuilder<AiPromptSelectorCubit, AiPromptSelectorState>(\n                  builder: (context, state) {\n                    return state.maybeMap(\n                      loading: (_) {\n                        return const Center(\n                          child: CircularProgressIndicator(),\n                        );\n                      },\n                      ready: (readyState) {\n                        return Row(\n                          crossAxisAlignment: CrossAxisAlignment.stretch,\n                          children: [\n                            const Expanded(\n                              child: AiPromptCategoryList(),\n                            ),\n                            if (readyState.isCustomPromptSectionSelected &&\n                                readyState.databaseConfig == null)\n                              const Expanded(\n                                flex: 5,\n                                child: Center(\n                                  child: AiPromptOnboarding(),\n                                ),\n                              )\n                            else ...[\n                              const Expanded(\n                                flex: 2,\n                                child: AiPromptVisibleList(),\n                              ),\n                              Expanded(\n                                flex: 3,\n                                child: BlocBuilder<AiPromptSelectorCubit,\n                                    AiPromptSelectorState>(\n                                  builder: (context, state) {\n                                    final selectedPrompt = state.maybeMap(\n                                      ready: (state) {\n                                        return state.visiblePrompts\n                                            .firstWhereOrNull(\n                                          (prompt) =>\n                                              prompt.id ==\n                                              state.selectedPromptId,\n                                        );\n                                      },\n                                      orElse: () => null,\n                                    );\n                                    if (selectedPrompt == null) {\n                                      return const SizedBox.shrink();\n                                    }\n                                    return AiPromptPreview(\n                                      prompt: selectedPrompt,\n                                    );\n                                  },\n                                ),\n                              ),\n                            ],\n                          ],\n                        );\n                      },\n                      orElse: () => const SizedBox.shrink(),\n                    );\n                  },\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nvoid showLoadPromptFailedDialog(\n  BuildContext context,\n) {\n  showSimpleAFDialog(\n    context: context,\n    title: LocaleKeys.ai_customPrompt_invalidDatabase.tr(),\n    content: LocaleKeys.ai_customPrompt_invalidDatabaseHelp.tr(),\n    primaryAction: (\n      LocaleKeys.button_ok.tr(),\n      (context) {},\n    ),\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_onboarding.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'ai_prompt_database_modal.dart';\n\nclass AiPromptOnboarding extends StatelessWidget {\n  const AiPromptOnboarding({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        Text(\n          LocaleKeys.ai_customPrompt_customPrompt.tr(),\n          style: theme.textStyle.heading3.standard(\n            color: theme.textColorScheme.primary,\n          ),\n        ),\n        VSpace(\n          theme.spacing.s,\n        ),\n        Text(\n          LocaleKeys.ai_customPrompt_databasePrompts.tr(),\n          style: theme.textStyle.body.standard(\n            color: theme.textColorScheme.secondary,\n          ),\n        ),\n        VSpace(\n          theme.spacing.xxl,\n        ),\n        AFFilledButton.primary(\n          onTap: () async {\n            final config = await changeCustomPromptDatabaseConfig(context);\n\n            if (config != null && context.mounted) {\n              context\n                  .read<AiPromptSelectorCubit>()\n                  .updateCustomPromptDatabaseConfiguration(config);\n            }\n          },\n          builder: (context, isHovering, disabled) {\n            return Text(\n              LocaleKeys.ai_customPrompt_selectDatabase.tr(),\n              style: theme.textStyle.body.enhanced(\n                color: theme.textColorScheme.onFill,\n              ),\n            );\n          },\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_preview.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\n\nclass AiPromptPreview extends StatelessWidget {\n  const AiPromptPreview({\n    super.key,\n    required this.prompt,\n  });\n\n  final AiPrompt prompt;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return SelectionArea(\n      child: Column(\n        children: [\n          Padding(\n            padding: EdgeInsets.symmetric(\n              horizontal: theme.spacing.l,\n            ),\n            child: SelectionContainer.disabled(\n              child: Row(\n                children: [\n                  Expanded(\n                    child: Text(\n                      prompt.name,\n                      style: theme.textStyle.headline.standard(\n                        color: theme.textColorScheme.primary,\n                      ),\n                    ),\n                  ),\n                  HSpace(theme.spacing.s),\n                  AFFilledTextButton.primary(\n                    text: LocaleKeys.ai_customPrompt_usePrompt.tr(),\n                    onTap: () {\n                      Navigator.of(context).pop(prompt);\n                    },\n                  ),\n                ],\n              ),\n            ),\n          ),\n          VSpace(theme.spacing.xs),\n          Expanded(\n            child: ListView(\n              padding: EdgeInsets.all(\n                theme.spacing.l,\n              ),\n              children: [\n                SelectionContainer.disabled(\n                  child: Text(\n                    LocaleKeys.ai_customPrompt_prompt.tr(),\n                    style: theme.textStyle.heading4.standard(\n                      color: theme.textColorScheme.primary,\n                    ),\n                  ),\n                ),\n                VSpace(theme.spacing.xs),\n                _PromptContent(\n                  prompt: prompt,\n                ),\n                VSpace(theme.spacing.xl),\n                if (prompt.example.isNotEmpty) ...[\n                  SelectionContainer.disabled(\n                    child: Text(\n                      LocaleKeys.ai_customPrompt_promptExample.tr(),\n                      style: theme.textStyle.heading4.standard(\n                        color: theme.textColorScheme.primary,\n                      ),\n                    ),\n                  ),\n                  VSpace(theme.spacing.xs),\n                  _PromptExample(\n                    prompt: prompt,\n                  ),\n                  VSpace(theme.spacing.xl),\n                ],\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _PromptContent extends StatelessWidget {\n  const _PromptContent({\n    required this.prompt,\n  });\n\n  final AiPrompt prompt;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final textSpans = _buildTextSpans(context, prompt.content);\n\n    return Container(\n      padding: EdgeInsets.all(theme.spacing.l),\n      decoration: BoxDecoration(\n        color: theme.surfaceContainerColorScheme.layer01,\n        borderRadius: BorderRadius.circular(theme.borderRadius.m),\n      ),\n      child: Text.rich(\n        TextSpan(\n          style: theme.textStyle.body.standard(\n            color: theme.textColorScheme.primary,\n          ),\n          children: textSpans,\n        ),\n      ),\n    );\n  }\n\n  List<TextSpan> _buildTextSpans(BuildContext context, String text) {\n    final theme = AppFlowyTheme.of(context);\n    final spans = <TextSpan>[];\n\n    final parts = _splitPromptText(text);\n    for (final part in parts) {\n      if (part.startsWith('[') && part.endsWith(']')) {\n        spans.add(\n          TextSpan(\n            text: part,\n            style: TextStyle(color: theme.textColorScheme.featured),\n          ),\n        );\n      } else {\n        spans.add(TextSpan(text: part));\n      }\n    }\n\n    return spans;\n  }\n\n  List<String> _splitPromptText(String text) {\n    final regex = RegExp(r'(\\[[^\\[\\]]*?\\])');\n\n    final result = <String>[];\n\n    text.splitMapJoin(\n      regex,\n      onMatch: (match) {\n        result.add(match.group(0)!);\n        return '';\n      },\n      onNonMatch: (nonMatch) {\n        result.add(nonMatch);\n        return '';\n      },\n    );\n\n    return result;\n  }\n}\n\nclass _PromptExample extends StatelessWidget {\n  const _PromptExample({\n    required this.prompt,\n  });\n\n  final AiPrompt prompt;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return Container(\n      padding: EdgeInsets.all(theme.spacing.l),\n      decoration: BoxDecoration(\n        color: theme.surfaceContainerColorScheme.layer01,\n        borderRadius: BorderRadius.circular(theme.borderRadius.m),\n      ),\n      child: AIMarkdownText(\n        markdown: prompt.example,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_visible_list.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:diffutil_dart/diffutil.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'ai_prompt_database_modal.dart';\n\nconst Duration _listItemAnimationDuration = Duration(milliseconds: 150);\n\nclass AiPromptVisibleList extends StatefulWidget {\n  const AiPromptVisibleList({\n    super.key,\n  });\n\n  @override\n  State<AiPromptVisibleList> createState() => _AiPromptVisibleListState();\n}\n\nclass _AiPromptVisibleListState extends State<AiPromptVisibleList> {\n  final listKey = GlobalKey<AnimatedListState>();\n  final scrollController = ScrollController();\n  final List<AiPrompt> oldList = [];\n\n  late AiPromptSelectorCubit cubit;\n  late bool filterIsEmpty;\n\n  @override\n  void initState() {\n    super.initState();\n    cubit = context.read<AiPromptSelectorCubit>();\n    final textController = cubit.filterTextController;\n    filterIsEmpty = textController.text.isEmpty;\n    textController.addListener(handleFilterTextChanged);\n    final prompts = cubit.state.maybeMap(\n      ready: (value) => value.visiblePrompts,\n      orElse: () => <AiPrompt>[],\n    );\n    oldList.addAll(prompts);\n  }\n\n  @override\n  void dispose() {\n    cubit.filterTextController.removeListener(handleFilterTextChanged);\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return Column(\n      children: [\n        BlocConsumer<AiPromptSelectorCubit, AiPromptSelectorState>(\n          listener: (context, state) {\n            state.maybeMap(\n              ready: (state) {\n                handleVisiblePromptListChanged(state.visiblePrompts);\n              },\n              orElse: () {},\n            );\n          },\n          buildWhen: (p, c) {\n            return p.maybeMap(\n              ready: (pr) => c.maybeMap(\n                ready: (cr) =>\n                    pr.databaseConfig?.view.id != cr.databaseConfig?.view.id ||\n                    pr.isLoadingCustomPrompts != cr.isLoadingCustomPrompts ||\n                    pr.isCustomPromptSectionSelected !=\n                        cr.isCustomPromptSectionSelected,\n                orElse: () => false,\n              ),\n              orElse: () => true,\n            );\n          },\n          builder: (context, state) {\n            return state.maybeMap(\n              ready: (readyState) {\n                if (!readyState.isCustomPromptSectionSelected) {\n                  return const SizedBox.shrink();\n                }\n                return Container(\n                  margin: EdgeInsets.only(\n                    left: theme.spacing.l,\n                    right: theme.spacing.l,\n                    bottom: theme.spacing.l,\n                  ),\n                  decoration: BoxDecoration(\n                    borderRadius: BorderRadius.circular(theme.borderRadius.m),\n                    color: theme.surfaceContainerColorScheme.layer01,\n                  ),\n                  padding: EdgeInsets.all(theme.spacing.m),\n                  child: Row(\n                    children: [\n                      Expanded(\n                        child: Text.rich(\n                          TextSpan(\n                            children: [\n                              TextSpan(\n                                text:\n                                    \"${LocaleKeys.ai_customPrompt_promptDatabase.tr()}: \",\n                                style: TextStyle(fontWeight: FontWeight.w500),\n                              ),\n                              TextSpan(\n                                text: readyState\n                                        .databaseConfig?.view.nameOrDefault ??\n                                    \"\",\n                              ),\n                            ],\n                          ),\n                          style: theme.textStyle.body.standard(\n                            color: theme.textColorScheme.primary,\n                          ),\n                          maxLines: 1,\n                          overflow: TextOverflow.ellipsis,\n                        ),\n                      ),\n                      ConstrainedBox(\n                        constraints: const BoxConstraints(\n                          maxWidth: 150,\n                        ),\n                        child: AFOutlinedButton.normal(\n                          builder: (context, isHovering, disabled) {\n                            return Row(\n                              spacing: theme.spacing.s,\n                              mainAxisSize: MainAxisSize.min,\n                              children: [\n                                if (readyState.isLoadingCustomPrompts)\n                                  buildLoadingIndicator(theme),\n                                Flexible(\n                                  child: Text(\n                                    readyState.isLoadingCustomPrompts\n                                        ? LocaleKeys.ai_customPrompt_loading\n                                            .tr()\n                                        : LocaleKeys.button_change.tr(),\n                                    maxLines: 1,\n                                    style: theme.textStyle.body.enhanced(\n                                      color: theme.textColorScheme.primary,\n                                    ),\n                                    overflow: TextOverflow.ellipsis,\n                                  ),\n                                ),\n                              ],\n                            );\n                          },\n                          onTap: () async {\n                            final newConfig =\n                                await changeCustomPromptDatabaseConfig(\n                              context,\n                              config: readyState.databaseConfig,\n                            );\n                            if (newConfig != null && context.mounted) {\n                              context\n                                  .read<AiPromptSelectorCubit>()\n                                  .updateCustomPromptDatabaseConfiguration(\n                                    newConfig,\n                                  );\n                            }\n                          },\n                        ),\n                      ),\n                    ],\n                  ),\n                );\n              },\n              orElse: () => const SizedBox.shrink(),\n            );\n          },\n        ),\n        Padding(\n          padding: EdgeInsets.symmetric(horizontal: theme.spacing.l),\n          child: buildSearchField(context),\n        ),\n        Expanded(\n          child: TextFieldTapRegion(\n            groupId: \"ai_prompt_category_list\",\n            child: BlocBuilder<AiPromptSelectorCubit, AiPromptSelectorState>(\n              builder: (context, state) {\n                return state.maybeMap(\n                  ready: (readyState) {\n                    if (readyState.visiblePrompts.isEmpty) {\n                      return buildEmptyPrompts();\n                    }\n                    return buildPromptList();\n                  },\n                  orElse: () => const SizedBox.shrink(),\n                );\n              },\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget buildSearchField(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final iconSize = 20.0;\n\n    return AFTextField(\n      groupId: \"ai_prompt_category_list\",\n      hintText: \"Search\",\n      controller: context.read<AiPromptSelectorCubit>().filterTextController,\n      autoFocus: true,\n      suffixIconConstraints: BoxConstraints.tightFor(\n        width: iconSize + theme.spacing.m,\n        height: iconSize,\n      ),\n      suffixIconBuilder: filterIsEmpty\n          ? null\n          : (context, isObscured) => TextFieldTapRegion(\n                groupId: \"ai_prompt_category_list\",\n                child: Padding(\n                  padding: EdgeInsets.only(right: theme.spacing.m),\n                  child: GestureDetector(\n                    onTap: () => context\n                        .read<AiPromptSelectorCubit>()\n                        .filterTextController\n                        .clear(),\n                    child: MouseRegion(\n                      cursor: SystemMouseCursors.click,\n                      child: FlowySvg(\n                        FlowySvgs.search_clear_m,\n                        color: theme.iconColorScheme.tertiary,\n                        size: const Size.square(20),\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n    );\n  }\n\n  Widget buildEmptyPrompts() {\n    final theme = AppFlowyTheme.of(context);\n\n    return Center(\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          FlowySvg(\n            FlowySvgs.m_home_search_icon_m,\n            color: theme.iconColorScheme.secondary,\n            size: Size.square(24),\n          ),\n          VSpace(theme.spacing.m),\n          Text(\n            LocaleKeys.ai_customPrompt_noResults.tr(),\n            style: theme.textStyle.body\n                .standard(color: theme.textColorScheme.secondary),\n            maxLines: 1,\n            overflow: TextOverflow.ellipsis,\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget buildPromptList() {\n    final theme = AppFlowyTheme.of(context);\n\n    return AnimatedList(\n      controller: scrollController,\n      padding: EdgeInsets.all(theme.spacing.l),\n      key: listKey,\n      initialItemCount: oldList.length,\n      itemBuilder: (context, index, animation) {\n        return BlocBuilder<AiPromptSelectorCubit, AiPromptSelectorState>(\n          builder: (context, state) {\n            return state.maybeMap(\n              ready: (state) {\n                final prompt = state.visiblePrompts[index];\n\n                return Padding(\n                  padding: EdgeInsets.only(\n                    top: index == 0 ? 0 : theme.spacing.s,\n                    bottom: index == state.visiblePrompts.length - 1\n                        ? 0\n                        : theme.spacing.s,\n                  ),\n                  child: _AiPromptListItem(\n                    animation: animation,\n                    prompt: prompt,\n                    isSelected: state.selectedPromptId == prompt.id,\n                  ),\n                );\n              },\n              orElse: () => const SizedBox.shrink(),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  Widget buildLoadingIndicator(AppFlowyThemeData theme) {\n    return SizedBox.square(\n      dimension: 20,\n      child: Padding(\n        padding: EdgeInsets.all(2.5),\n        child: CircularProgressIndicator(\n          color: theme.iconColorScheme.tertiary,\n          strokeWidth: 2.0,\n        ),\n      ),\n    );\n  }\n\n  void handleVisiblePromptListChanged(\n    List<AiPrompt> newList,\n  ) {\n    final updates = calculateListDiff(oldList, newList).getUpdatesWithData();\n\n    for (final update in updates) {\n      update.when(\n        insert: (pos, data) {\n          listKey.currentState?.insertItem(\n            pos,\n            duration: _listItemAnimationDuration,\n          );\n        },\n        remove: (pos, data) {\n          listKey.currentState?.removeItem(\n            pos,\n            (context, animation) {\n              final isSelected =\n                  context.read<AiPromptSelectorCubit>().state.maybeMap(\n                        ready: (state) => state.selectedPromptId == data.id,\n                        orElse: () => false,\n                      );\n              return _AiPromptListItem(\n                animation: animation,\n                prompt: data,\n                isSelected: isSelected,\n              );\n            },\n            duration: _listItemAnimationDuration,\n          );\n        },\n        change: (pos, oldData, newData) {},\n        move: (from, to, data) {},\n      );\n    }\n    oldList\n      ..clear()\n      ..addAll(newList);\n  }\n\n  void handleFilterTextChanged() {\n    setState(() {\n      filterIsEmpty = cubit.filterTextController.text.isEmpty;\n    });\n  }\n}\n\nclass _AiPromptListItem extends StatefulWidget {\n  const _AiPromptListItem({\n    required this.animation,\n    required this.prompt,\n    required this.isSelected,\n  });\n\n  final Animation<double> animation;\n  final AiPrompt prompt;\n  final bool isSelected;\n\n  @override\n  State<_AiPromptListItem> createState() => _AiPromptListItemState();\n}\n\nclass _AiPromptListItemState extends State<_AiPromptListItem> {\n  bool isHovering = false;\n  Timer? timer;\n\n  @override\n  void dispose() {\n    timer?.cancel();\n    timer = null;\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final cubit = context.read<AiPromptSelectorCubit>();\n\n    final curvedAnimation = CurvedAnimation(\n      parent: widget.animation,\n      curve: Curves.easeIn,\n    );\n\n    final surfacePrimaryHover =\n        Theme.of(context).isLightMode ? Color(0xFFF8FAFF) : Color(0xFF3C3F4E);\n\n    return FadeTransition(\n      opacity: curvedAnimation,\n      child: SizeTransition(\n        sizeFactor: curvedAnimation,\n        child: MouseRegion(\n          onEnter: (_) {\n            setState(() {\n              isHovering = true;\n              timer = Timer(const Duration(milliseconds: 300), () {\n                if (mounted) {\n                  cubit.selectPrompt(widget.prompt.id);\n                }\n              });\n            });\n          },\n          onExit: (_) {\n            setState(() {\n              isHovering = false;\n              timer?.cancel();\n            });\n          },\n          child: GestureDetector(\n            onTap: () {\n              cubit.selectPrompt(widget.prompt.id);\n            },\n            child: Stack(\n              children: [\n                Container(\n                  padding: EdgeInsets.all(theme.spacing.m),\n                  decoration: BoxDecoration(\n                    borderRadius: BorderRadius.circular(theme.borderRadius.m),\n                    color: Colors.transparent,\n                    border: Border.all(\n                      color: widget.isSelected\n                          ? isHovering\n                              ? theme.borderColorScheme.themeThickHover\n                              : theme.borderColorScheme.themeThick\n                          : isHovering\n                              ? theme.borderColorScheme.primaryHover\n                              : theme.borderColorScheme.primary,\n                    ),\n                  ),\n                  child: Column(\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      Row(\n                        children: [\n                          Expanded(\n                            child: Text(\n                              widget.prompt.name,\n                              maxLines: 1,\n                              style: theme.textStyle.body.standard(\n                                color: theme.textColorScheme.primary,\n                              ),\n                              overflow: TextOverflow.ellipsis,\n                              softWrap: true,\n                            ),\n                          ),\n                        ],\n                      ),\n                      Text(\n                        widget.prompt.content,\n                        maxLines: 2,\n                        style: theme.textStyle.caption.standard(\n                          color: theme.textColorScheme.secondary,\n                        ),\n                        overflow: TextOverflow.ellipsis,\n                        softWrap: true,\n                      ),\n                    ],\n                  ),\n                ),\n                if (isHovering)\n                  Positioned(\n                    top: theme.spacing.s,\n                    right: theme.spacing.s,\n                    child: DecoratedBox(\n                      decoration: BoxDecoration(boxShadow: theme.shadow.small),\n                      child: AFBaseButton(\n                        onTap: () {\n                          Navigator.of(context).pop(widget.prompt);\n                        },\n                        builder: (context, isHovering, disabled) {\n                          return Text(\n                            LocaleKeys.ai_customPrompt_usePrompt.tr(),\n                            style: theme.textStyle.body.standard(\n                              color: theme.textColorScheme.primary,\n                            ),\n                          );\n                        },\n                        backgroundColor: (context, isHovering, disabled) {\n                          if (isHovering) {\n                            return surfacePrimaryHover;\n                          }\n                          return theme.surfaceColorScheme.primary;\n                        },\n                        padding: EdgeInsets.symmetric(\n                          vertical: theme.spacing.s,\n                          horizontal: theme.spacing.m,\n                        ),\n                        borderRadius: theme.borderRadius.m,\n                      ),\n                    ),\n                  ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/loading_indicator.dart",
    "content": "import 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_animate/flutter_animate.dart';\n\n/// An animated generating indicator for an AI response\nclass AILoadingIndicator extends StatelessWidget {\n  const AILoadingIndicator({\n    super.key,\n    this.text = \"\",\n    this.duration = const Duration(seconds: 1),\n  });\n\n  final String text;\n  final Duration duration;\n\n  @override\n  Widget build(BuildContext context) {\n    final slice = Duration(milliseconds: duration.inMilliseconds ~/ 5);\n    return SelectionContainer.disabled(\n      child: SizedBox(\n        height: 20,\n        child: SeparatedRow(\n          separatorBuilder: () => const HSpace(4),\n          children: [\n            Padding(\n              padding: const EdgeInsetsDirectional.only(end: 4.0),\n              child: FlowyText(\n                text,\n                color: Theme.of(context).hintColor,\n              ),\n            ),\n            buildDot(const Color(0xFF9327FF))\n                .animate(onPlay: (controller) => controller.repeat())\n                .slideY(duration: slice, begin: 0, end: -1)\n                .then()\n                .slideY(begin: -1, end: 1)\n                .then()\n                .slideY(begin: 1, end: 0)\n                .then()\n                .slideY(duration: slice * 2, begin: 0, end: 0),\n            buildDot(const Color(0xFFFB006D))\n                .animate(onPlay: (controller) => controller.repeat())\n                .slideY(duration: slice, begin: 0, end: 0)\n                .then()\n                .slideY(begin: 0, end: -1)\n                .then()\n                .slideY(begin: -1, end: 1)\n                .then()\n                .slideY(begin: 1, end: 0)\n                .then()\n                .slideY(begin: 0, end: 0),\n            buildDot(const Color(0xFFFFCE00))\n                .animate(onPlay: (controller) => controller.repeat())\n                .slideY(duration: slice * 2, begin: 0, end: 0)\n                .then()\n                .slideY(duration: slice, begin: 0, end: -1)\n                .then()\n                .slideY(begin: -1, end: 1)\n                .then()\n                .slideY(begin: 1, end: 0),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget buildDot(Color color) {\n    return SizedBox.square(\n      dimension: 4,\n      child: DecoratedBox(\n        decoration: BoxDecoration(\n          color: color,\n          borderRadius: BorderRadius.circular(2),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/prompt_input/action_buttons.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/icon_button.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\n\nimport 'layout_define.dart';\n\nclass PromptInputAttachmentButton extends StatelessWidget {\n  const PromptInputAttachmentButton({required this.onTap, super.key});\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      message: LocaleKeys.chat_uploadFile.tr(),\n      child: SizedBox.square(\n        dimension: DesktopAIPromptSizes.actionBarButtonSize,\n        child: FlowyIconButton(\n          hoverColor: AFThemeExtension.of(context).lightGreyHover,\n          radius: BorderRadius.circular(8),\n          icon: FlowySvg(\n            FlowySvgs.ai_attachment_s,\n            size: const Size.square(16),\n            color: Theme.of(context).iconTheme.color,\n          ),\n          onPressed: onTap,\n        ),\n      ),\n    );\n  }\n}\n\nclass PromptInputMentionButton extends StatelessWidget {\n  const PromptInputMentionButton({\n    super.key,\n    required this.buttonSize,\n    required this.iconSize,\n    required this.onTap,\n  });\n\n  final double buttonSize;\n  final double iconSize;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      message: LocaleKeys.chat_clickToMention.tr(),\n      preferBelow: false,\n      child: FlowyIconButton(\n        width: buttonSize,\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        radius: BorderRadius.circular(8),\n        icon: FlowySvg(\n          FlowySvgs.chat_at_s,\n          size: Size.square(iconSize),\n          color: Theme.of(context).iconTheme.color,\n        ),\n        onPressed: onTap,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/prompt_input/browse_prompts_button.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../ai_prompt_modal/ai_prompt_modal.dart';\n\nclass BrowsePromptsButton extends StatelessWidget {\n  const BrowsePromptsButton({\n    super.key,\n    required this.onSelectPrompt,\n  });\n\n  final void Function(AiPrompt) onSelectPrompt;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      message: LocaleKeys.ai_customPrompt_browsePrompts.tr(),\n      child: BlocProvider(\n        create: (context) => AiPromptSelectorCubit(),\n        child: Builder(\n          builder: (context) {\n            return GestureDetector(\n              onTap: () async {\n                final prompt = await showAiPromptModal(\n                  context,\n                  aiPromptSelectorCubit: context.read<AiPromptSelectorCubit>(),\n                );\n                if (context.mounted) {\n                  context.read<AiPromptSelectorCubit>().reset();\n                }\n                if (prompt != null && context.mounted) {\n                  onSelectPrompt(prompt);\n                }\n              },\n              behavior: HitTestBehavior.opaque,\n              child: SizedBox(\n                height: DesktopAIPromptSizes.actionBarButtonSize,\n                child: FlowyHover(\n                  style: const HoverStyle(\n                    borderRadius: BorderRadius.all(Radius.circular(8)),\n                  ),\n                  child: Padding(\n                    padding: const EdgeInsetsDirectional.all(4.0),\n                    child: Center(\n                      child: FlowyText(\n                        LocaleKeys.ai_customPrompt_browsePrompts.tr(),\n                        fontSize: 12,\n                        figmaLineHeight: 16,\n                        color: Theme.of(context).hintColor,\n                        overflow: TextOverflow.ellipsis,\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_input.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_user_cubit.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra/file_picker/file_picker_service.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'browse_prompts_button.dart';\n\ntypedef OnPromptInputSubmitted = void Function(\n  String input,\n  PredefinedFormat? predefinedFormat,\n  Map<String, dynamic> metadata,\n  String? promptId,\n);\n\nclass DesktopPromptInput extends StatefulWidget {\n  const DesktopPromptInput({\n    super.key,\n    required this.isStreaming,\n    required this.textController,\n    required this.onStopStreaming,\n    required this.onSubmitted,\n    required this.selectedSourcesNotifier,\n    required this.onUpdateSelectedSources,\n    this.hideDecoration = false,\n    this.hideFormats = false,\n    this.extraBottomActionButton,\n  });\n\n  final bool isStreaming;\n  final AiPromptInputTextEditingController textController;\n  final void Function() onStopStreaming;\n  final OnPromptInputSubmitted onSubmitted;\n  final ValueNotifier<List<String>> selectedSourcesNotifier;\n  final void Function(List<String>) onUpdateSelectedSources;\n  final bool hideDecoration;\n  final bool hideFormats;\n  final Widget? extraBottomActionButton;\n\n  @override\n  State<DesktopPromptInput> createState() => _DesktopPromptInputState();\n}\n\nclass _DesktopPromptInputState extends State<DesktopPromptInput> {\n  final textFieldKey = GlobalKey();\n  final layerLink = LayerLink();\n  final overlayController = OverlayPortalController();\n  final inputControlCubit = ChatInputControlCubit();\n  final chatUserCubit = ChatUserCubit();\n  final focusNode = FocusNode();\n\n  late SendButtonState sendButtonState;\n  bool isComposing = false;\n\n  @override\n  void initState() {\n    super.initState();\n\n    widget.textController.addListener(handleTextControllerChanged);\n    focusNode\n      ..addListener(\n        () {\n          if (!widget.hideDecoration) {\n            setState(() {}); // refresh border color\n          }\n          if (!focusNode.hasFocus) {\n            cancelMentionPage(); // hide menu when lost focus\n          }\n        },\n      )\n      ..onKeyEvent = handleKeyEvent;\n\n    updateSendButtonState();\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      focusNode.requestFocus();\n      checkForAskingAI();\n    });\n  }\n\n  @override\n  void didUpdateWidget(covariant oldWidget) {\n    updateSendButtonState();\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    widget.textController.removeListener(handleTextControllerChanged);\n    inputControlCubit.close();\n    chatUserCubit.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider.value(value: inputControlCubit),\n        BlocProvider.value(value: chatUserCubit),\n      ],\n      child: BlocListener<ChatInputControlCubit, ChatInputControlState>(\n        listener: (context, state) {\n          state.maybeWhen(\n            updateSelectedViews: (selectedViews) {\n              context\n                  .read<AIPromptInputBloc>()\n                  .add(AIPromptInputEvent.updateMentionedViews(selectedViews));\n            },\n            orElse: () {},\n          );\n        },\n        child: OverlayPortal(\n          controller: overlayController,\n          overlayChildBuilder: (context) {\n            return PromptInputMentionPageMenu(\n              anchor: PromptInputAnchor(textFieldKey, layerLink),\n              textController: widget.textController,\n              onPageSelected: handlePageSelected,\n            );\n          },\n          child: DecoratedBox(\n            decoration: decoration(context),\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                ConstrainedBox(\n                  constraints: BoxConstraints(\n                    maxHeight:\n                        DesktopAIPromptSizes.attachedFilesBarPadding.vertical +\n                            DesktopAIPromptSizes.attachedFilesPreviewHeight,\n                  ),\n                  child: TextFieldTapRegion(\n                    child: PromptInputFile(\n                      onDeleted: (file) => context\n                          .read<AIPromptInputBloc>()\n                          .add(AIPromptInputEvent.removeFile(file)),\n                    ),\n                  ),\n                ),\n                const VSpace(4.0),\n                BlocBuilder<AIPromptInputBloc, AIPromptInputState>(\n                  builder: (context, state) {\n                    return Stack(\n                      children: [\n                        ConstrainedBox(\n                          constraints: getTextFieldConstraints(\n                            state.showPredefinedFormats && !widget.hideFormats,\n                          ),\n                          child: inputTextField(),\n                        ),\n                        if (state.showPredefinedFormats && !widget.hideFormats)\n                          Positioned.fill(\n                            bottom: null,\n                            child: TextFieldTapRegion(\n                              child: Padding(\n                                padding: const EdgeInsetsDirectional.only(\n                                  start: 8.0,\n                                ),\n                                child: ChangeFormatBar(\n                                  showImageFormats:\n                                      state.modelState.type == AiType.cloud,\n                                  predefinedFormat: state.predefinedFormat,\n                                  spacing: 4.0,\n                                  onSelectPredefinedFormat: (format) =>\n                                      context.read<AIPromptInputBloc>().add(\n                                            AIPromptInputEvent\n                                                .updatePredefinedFormat(format),\n                                          ),\n                                ),\n                              ),\n                            ),\n                          ),\n                        Positioned.fill(\n                          top: null,\n                          child: TextFieldTapRegion(\n                            child: _PromptBottomActions(\n                              showPredefinedFormatBar:\n                                  state.showPredefinedFormats,\n                              showPredefinedFormatButton: !widget.hideFormats,\n                              onTogglePredefinedFormatSection: () =>\n                                  context.read<AIPromptInputBloc>().add(\n                                        AIPromptInputEvent\n                                            .toggleShowPredefinedFormat(),\n                                      ),\n                              onStartMention: startMentionPageFromButton,\n                              sendButtonState: sendButtonState,\n                              onSendPressed: handleSend,\n                              onStopStreaming: widget.onStopStreaming,\n                              selectedSourcesNotifier:\n                                  widget.selectedSourcesNotifier,\n                              onUpdateSelectedSources:\n                                  widget.onUpdateSelectedSources,\n                              onSelectPrompt: handleOnSelectPrompt,\n                              extraBottomActionButton:\n                                  widget.extraBottomActionButton,\n                            ),\n                          ),\n                        ),\n                      ],\n                    );\n                  },\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  BoxDecoration decoration(BuildContext context) {\n    if (widget.hideDecoration) {\n      return BoxDecoration();\n    }\n    return BoxDecoration(\n      color: Theme.of(context).colorScheme.surface,\n      border: Border.all(\n        color: focusNode.hasFocus\n            ? Theme.of(context).colorScheme.primary\n            : Theme.of(context).colorScheme.outline,\n        width: focusNode.hasFocus ? 1.5 : 1.0,\n      ),\n      borderRadius: const BorderRadius.all(Radius.circular(12.0)),\n    );\n  }\n\n  void checkForAskingAI() {\n    final paletteBloc = context.read<CommandPaletteBloc?>(),\n        paletteState = paletteBloc?.state;\n    if (paletteBloc == null || paletteState == null) return;\n    final isAskingAI = paletteState.askAI;\n    if (!isAskingAI) return;\n    paletteBloc.add(CommandPaletteEvent.askedAI());\n    final query = paletteState.query ?? '';\n    if (query.isEmpty) return;\n    final sources = (paletteState.askAISources ?? []).map((e) => e.id).toList();\n    final metadata =\n        context.read<AIPromptInputBloc?>()?.consumeMetadata() ?? {};\n    final promptBloc = context.read<AIPromptInputBloc?>();\n    final promptId = promptBloc?.promptId;\n    final promptState = promptBloc?.state;\n    final predefinedFormat = promptState?.predefinedFormat;\n    if (sources.isNotEmpty) {\n      widget.onUpdateSelectedSources(sources);\n    }\n    widget.onSubmitted.call(query, predefinedFormat, metadata, promptId ?? '');\n  }\n\n  void startMentionPageFromButton() {\n    if (overlayController.isShowing) {\n      return;\n    }\n    if (!focusNode.hasFocus) {\n      focusNode.requestFocus();\n    }\n    widget.textController.text += '@';\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      if (context.mounted) {\n        context\n            .read<ChatInputControlCubit>()\n            .startSearching(widget.textController.value);\n        overlayController.show();\n      }\n    });\n  }\n\n  void cancelMentionPage() {\n    if (overlayController.isShowing) {\n      inputControlCubit.reset();\n      overlayController.hide();\n    }\n  }\n\n  void updateSendButtonState() {\n    if (widget.isStreaming) {\n      sendButtonState = SendButtonState.streaming;\n    } else if (widget.textController.text.trim().isEmpty) {\n      sendButtonState = SendButtonState.disabled;\n    } else {\n      sendButtonState = SendButtonState.enabled;\n    }\n  }\n\n  void handleSend() {\n    if (widget.isStreaming) {\n      return;\n    }\n    String userInput = widget.textController.text.trim();\n    userInput = inputControlCubit.formatIntputText(userInput);\n    userInput = AiPromptInputTextEditingController.restore(userInput);\n\n    widget.textController.clear();\n    if (userInput.isEmpty) {\n      return;\n    }\n\n    // get the attached files and mentioned pages\n    final metadata = context.read<AIPromptInputBloc>().consumeMetadata();\n\n    final bloc = context.read<AIPromptInputBloc>();\n    final showPredefinedFormats = bloc.state.showPredefinedFormats;\n    final predefinedFormat = bloc.state.predefinedFormat;\n\n    widget.onSubmitted(\n      userInput,\n      showPredefinedFormats ? predefinedFormat : null,\n      metadata,\n      bloc.promptId,\n    );\n  }\n\n  void handleTextControllerChanged() {\n    setState(() {\n      // update whether send button is clickable\n      updateSendButtonState();\n      isComposing = !widget.textController.value.composing.isCollapsed;\n    });\n\n    if (isComposing) {\n      return;\n    }\n\n    // disable mention\n    return;\n\n    // handle text and selection changes ONLY when mentioning a page\n    // ignore: dead_code\n    if (!overlayController.isShowing ||\n        inputControlCubit.filterStartPosition == -1) {\n      return;\n    }\n\n    // handle cases where mention a page is cancelled\n    final textController = widget.textController;\n    final textSelection = textController.value.selection;\n    final isSelectingMultipleCharacters = !textSelection.isCollapsed;\n    final isCaretBeforeStartOfRange =\n        textSelection.baseOffset < inputControlCubit.filterStartPosition;\n    final isCaretAfterEndOfRange =\n        textSelection.baseOffset > inputControlCubit.filterEndPosition;\n    final isTextSame = inputControlCubit.inputText == textController.text;\n\n    if (isSelectingMultipleCharacters ||\n        isTextSame && (isCaretBeforeStartOfRange || isCaretAfterEndOfRange)) {\n      cancelMentionPage();\n      return;\n    }\n\n    final previousLength = inputControlCubit.inputText.characters.length;\n    final currentLength = textController.text.characters.length;\n\n    // delete \"@\"\n    if (previousLength != currentLength && isCaretBeforeStartOfRange) {\n      cancelMentionPage();\n      return;\n    }\n\n    // handle cases where mention the filter is updated\n    if (previousLength != currentLength) {\n      final diff = currentLength - previousLength;\n      final newEndPosition = inputControlCubit.filterEndPosition + diff;\n      final newFilter = textController.text.substring(\n        inputControlCubit.filterStartPosition,\n        newEndPosition,\n      );\n      inputControlCubit.updateFilter(\n        textController.text,\n        newFilter,\n        newEndPosition: newEndPosition,\n      );\n    } else if (!isTextSame) {\n      final newFilter = textController.text.substring(\n        inputControlCubit.filterStartPosition,\n        inputControlCubit.filterEndPosition,\n      );\n      inputControlCubit.updateFilter(textController.text, newFilter);\n    }\n  }\n\n  KeyEventResult handleKeyEvent(FocusNode node, KeyEvent event) {\n    // if (event.character == '@') {\n    //   WidgetsBinding.instance.addPostFrameCallback((_) {\n    //     inputControlCubit.startSearching(widget.textController.value);\n    //     overlayController.show();\n    //   });\n    // }\n    if (event is KeyDownEvent &&\n        event.logicalKey == LogicalKeyboardKey.escape) {\n      node.unfocus();\n      return KeyEventResult.handled;\n    }\n    return KeyEventResult.ignored;\n  }\n\n  void handlePageSelected(ViewPB view) {\n    final newText = widget.textController.text.replaceRange(\n      inputControlCubit.filterStartPosition,\n      inputControlCubit.filterEndPosition,\n      view.id,\n    );\n    widget.textController.value = TextEditingValue(\n      text: newText,\n      selection: TextSelection.collapsed(\n        offset: inputControlCubit.filterStartPosition + view.id.length,\n        affinity: TextAffinity.upstream,\n      ),\n    );\n\n    inputControlCubit.selectPage(view);\n    overlayController.hide();\n  }\n\n  Widget inputTextField() {\n    return Shortcuts(\n      shortcuts: buildShortcuts(),\n      child: Actions(\n        actions: buildActions(),\n        child: CompositedTransformTarget(\n          link: layerLink,\n          child: BlocBuilder<AIPromptInputBloc, AIPromptInputState>(\n            builder: (context, state) {\n              Widget textField = PromptInputTextField(\n                key: textFieldKey,\n                editable: state.modelState.isEditable,\n                cubit: inputControlCubit,\n                textController: widget.textController,\n                textFieldFocusNode: focusNode,\n                contentPadding:\n                    calculateContentPadding(state.showPredefinedFormats),\n                hintText: state.modelState.hintText,\n              );\n\n              if (state.modelState.tooltip != null) {\n                textField = FlowyTooltip(\n                  message: state.modelState.tooltip!,\n                  child: textField,\n                );\n              }\n\n              return textField;\n            },\n          ),\n        ),\n      ),\n    );\n  }\n\n  BoxConstraints getTextFieldConstraints(bool showPredefinedFormats) {\n    double minHeight = DesktopAIPromptSizes.textFieldMinHeight +\n        DesktopAIPromptSizes.actionBarSendButtonSize +\n        DesktopAIChatSizes.inputActionBarMargin.vertical;\n    double maxHeight = 300;\n    if (showPredefinedFormats) {\n      minHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight;\n      maxHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight;\n    }\n    return BoxConstraints(minHeight: minHeight, maxHeight: maxHeight);\n  }\n\n  EdgeInsetsGeometry calculateContentPadding(bool showPredefinedFormats) {\n    final top = showPredefinedFormats\n        ? DesktopAIPromptSizes.predefinedFormatButtonHeight\n        : 0.0;\n    final bottom = DesktopAIPromptSizes.actionBarSendButtonSize +\n        DesktopAIChatSizes.inputActionBarMargin.vertical;\n\n    return DesktopAIPromptSizes.textFieldContentPadding\n        .add(EdgeInsets.only(top: top, bottom: bottom));\n  }\n\n  Map<ShortcutActivator, Intent> buildShortcuts() {\n    if (isComposing) {\n      return const {};\n    }\n\n    return const {\n      SingleActivator(LogicalKeyboardKey.arrowUp): _FocusPreviousItemIntent(),\n      SingleActivator(LogicalKeyboardKey.arrowDown): _FocusNextItemIntent(),\n      SingleActivator(LogicalKeyboardKey.escape): _CancelMentionPageIntent(),\n      SingleActivator(LogicalKeyboardKey.enter): _SubmitOrMentionPageIntent(),\n    };\n  }\n\n  Map<Type, Action<Intent>> buildActions() {\n    return {\n      _FocusPreviousItemIntent: CallbackAction<_FocusPreviousItemIntent>(\n        onInvoke: (intent) {\n          inputControlCubit.updateSelectionUp();\n          return;\n        },\n      ),\n      _FocusNextItemIntent: CallbackAction<_FocusNextItemIntent>(\n        onInvoke: (intent) {\n          inputControlCubit.updateSelectionDown();\n          return;\n        },\n      ),\n      _CancelMentionPageIntent: CallbackAction<_CancelMentionPageIntent>(\n        onInvoke: (intent) {\n          cancelMentionPage();\n          return;\n        },\n      ),\n      _SubmitOrMentionPageIntent: CallbackAction<_SubmitOrMentionPageIntent>(\n        onInvoke: (intent) {\n          if (overlayController.isShowing) {\n            inputControlCubit.state.maybeWhen(\n              ready: (visibleViews, focusedViewIndex) {\n                if (focusedViewIndex != -1 &&\n                    focusedViewIndex < visibleViews.length) {\n                  handlePageSelected(visibleViews[focusedViewIndex]);\n                }\n              },\n              orElse: () {},\n            );\n          } else {\n            handleSend();\n          }\n          return;\n        },\n      ),\n    };\n  }\n\n  void handleOnSelectPrompt(AiPrompt prompt) {\n    final bloc = context.read<AIPromptInputBloc>();\n    bloc\n      ..add(AIPromptInputEvent.updateMentionedViews([]))\n      ..add(AIPromptInputEvent.updatePromptId(prompt.id));\n\n    final content = AiPromptInputTextEditingController.replace(prompt.content);\n\n    widget.textController.value = TextEditingValue(\n      text: content,\n      selection: TextSelection.collapsed(\n        offset: content.length,\n      ),\n    );\n\n    if (bloc.state.showPredefinedFormats) {\n      bloc.add(\n        AIPromptInputEvent.toggleShowPredefinedFormat(),\n      );\n    }\n  }\n}\n\nclass _SubmitOrMentionPageIntent extends Intent {\n  const _SubmitOrMentionPageIntent();\n}\n\nclass _CancelMentionPageIntent extends Intent {\n  const _CancelMentionPageIntent();\n}\n\nclass _FocusPreviousItemIntent extends Intent {\n  const _FocusPreviousItemIntent();\n}\n\nclass _FocusNextItemIntent extends Intent {\n  const _FocusNextItemIntent();\n}\n\nclass PromptInputTextField extends StatelessWidget {\n  const PromptInputTextField({\n    super.key,\n    required this.editable,\n    required this.cubit,\n    required this.textController,\n    required this.textFieldFocusNode,\n    required this.contentPadding,\n    this.hintText = \"\",\n  });\n\n  final ChatInputControlCubit cubit;\n  final TextEditingController textController;\n  final FocusNode textFieldFocusNode;\n  final EdgeInsetsGeometry contentPadding;\n  final bool editable;\n  final String hintText;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return TextField(\n      controller: textController,\n      focusNode: textFieldFocusNode,\n      readOnly: !editable,\n      enabled: editable,\n      decoration: InputDecoration(\n        border: InputBorder.none,\n        enabledBorder: InputBorder.none,\n        focusedBorder: InputBorder.none,\n        contentPadding: contentPadding,\n        hintText: hintText,\n        hintStyle: inputHintTextStyle(context),\n        isCollapsed: true,\n        isDense: true,\n      ),\n      keyboardType: TextInputType.multiline,\n      textCapitalization: TextCapitalization.sentences,\n      minLines: 1,\n      maxLines: null,\n      style: theme.textStyle.body.standard(\n        color: theme.textColorScheme.primary,\n      ),\n    );\n  }\n\n  TextStyle? inputHintTextStyle(BuildContext context) {\n    return AppFlowyTheme.of(context).textStyle.body.standard(\n          color: Theme.of(context).isLightMode\n              ? const Color(0xFFBDC2C8)\n              : const Color(0xFF3C3E51),\n        );\n  }\n}\n\nclass _PromptBottomActions extends StatelessWidget {\n  const _PromptBottomActions({\n    required this.sendButtonState,\n    required this.showPredefinedFormatBar,\n    required this.showPredefinedFormatButton,\n    required this.onTogglePredefinedFormatSection,\n    required this.onStartMention,\n    required this.onSendPressed,\n    required this.onStopStreaming,\n    required this.selectedSourcesNotifier,\n    required this.onUpdateSelectedSources,\n    required this.onSelectPrompt,\n    this.extraBottomActionButton,\n  });\n\n  final bool showPredefinedFormatBar;\n  final bool showPredefinedFormatButton;\n  final void Function() onTogglePredefinedFormatSection;\n  final void Function() onStartMention;\n  final SendButtonState sendButtonState;\n  final void Function() onSendPressed;\n  final void Function() onStopStreaming;\n  final ValueNotifier<List<String>> selectedSourcesNotifier;\n  final void Function(List<String>) onUpdateSelectedSources;\n  final void Function(AiPrompt) onSelectPrompt;\n  final Widget? extraBottomActionButton;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: DesktopAIPromptSizes.actionBarSendButtonSize,\n      margin: DesktopAIChatSizes.inputActionBarMargin,\n      child: BlocBuilder<AIPromptInputBloc, AIPromptInputState>(\n        builder: (context, state) {\n          return Row(\n            spacing: DesktopAIChatSizes.inputActionBarButtonSpacing,\n            children: [\n              if (showPredefinedFormatButton) _predefinedFormatButton(),\n              _selectModelButton(context),\n              _buildBrowsePromptsButton(),\n\n              const Spacer(),\n\n              if (context.read<ChatUserCubit>().supportSelectSource())\n                _selectSourcesButton(),\n\n              if (extraBottomActionButton != null) extraBottomActionButton!,\n              // _mentionButton(context),\n              if (state.supportChatWithFile) _attachmentButton(context),\n              _sendButton(),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _predefinedFormatButton() {\n    return PromptInputDesktopToggleFormatButton(\n      showFormatBar: showPredefinedFormatBar,\n      onTap: onTogglePredefinedFormatSection,\n    );\n  }\n\n  Widget _selectSourcesButton() {\n    return PromptInputDesktopSelectSourcesButton(\n      onUpdateSelectedSources: onUpdateSelectedSources,\n      selectedSourcesNotifier: selectedSourcesNotifier,\n    );\n  }\n\n  Widget _selectModelButton(BuildContext context) {\n    return SelectModelMenu(\n      aiModelStateNotifier:\n          context.read<AIPromptInputBloc>().aiModelStateNotifier,\n    );\n  }\n\n  Widget _buildBrowsePromptsButton() {\n    return BrowsePromptsButton(\n      onSelectPrompt: onSelectPrompt,\n    );\n  }\n\n  // Widget _mentionButton(BuildContext context) {\n  //   return PromptInputMentionButton(\n  //     iconSize: DesktopAIPromptSizes.actionBarIconSize,\n  //     buttonSize: DesktopAIPromptSizes.actionBarButtonSize,\n  //     onTap: onStartMention,\n  //   );\n  // }\n\n  Widget _attachmentButton(BuildContext context) {\n    return PromptInputAttachmentButton(\n      onTap: () async {\n        final path = await getIt<FilePickerService>().pickFiles(\n          dialogTitle: '',\n          type: FileType.custom,\n          allowedExtensions: [\"pdf\", \"txt\", \"md\"],\n        );\n\n        if (path == null) {\n          return;\n        }\n\n        for (final file in path.files) {\n          if (file.path != null && context.mounted) {\n            context\n                .read<AIPromptInputBloc>()\n                .add(AIPromptInputEvent.attachFile(file.path!, file.name));\n          }\n        }\n      },\n    );\n  }\n\n  Widget _sendButton() {\n    return PromptInputSendButton(\n      state: sendButtonState,\n      onSendPressed: onSendPressed,\n      onStopStreaming: onStopStreaming,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/prompt_input/file_attachment_list.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/ai/service/ai_prompt_input_bloc.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_input_file_bloc.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:styled_widget/styled_widget.dart';\n\nimport 'layout_define.dart';\n\nclass PromptInputFile extends StatelessWidget {\n  const PromptInputFile({\n    super.key,\n    required this.onDeleted,\n  });\n\n  final void Function(ChatFile) onDeleted;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocSelector<AIPromptInputBloc, AIPromptInputState, List<ChatFile>>(\n      selector: (state) => state.attachedFiles,\n      builder: (context, files) {\n        if (files.isEmpty) {\n          return const SizedBox.shrink();\n        }\n        return ListView.separated(\n          scrollDirection: Axis.horizontal,\n          padding: DesktopAIPromptSizes.attachedFilesBarPadding -\n              const EdgeInsets.only(top: 6),\n          separatorBuilder: (context, index) => const HSpace(\n            DesktopAIPromptSizes.attachedFilesPreviewSpacing - 6,\n          ),\n          itemCount: files.length,\n          itemBuilder: (context, index) => ChatFilePreview(\n            file: files[index],\n            onDeleted: () => onDeleted(files[index]),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass ChatFilePreview extends StatefulWidget {\n  const ChatFilePreview({\n    required this.file,\n    required this.onDeleted,\n    super.key,\n  });\n\n  final ChatFile file;\n  final VoidCallback onDeleted;\n\n  @override\n  State<ChatFilePreview> createState() => _ChatFilePreviewState();\n}\n\nclass _ChatFilePreviewState extends State<ChatFilePreview> {\n  bool isHover = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => ChatInputFileBloc(file: widget.file),\n      child: BlocBuilder<ChatInputFileBloc, ChatInputFileState>(\n        builder: (context, state) {\n          return MouseRegion(\n            onEnter: (_) => setHover(true),\n            onExit: (_) => setHover(false),\n            child: Stack(\n              children: [\n                Container(\n                  margin: const EdgeInsetsDirectional.only(top: 6, end: 6),\n                  constraints: const BoxConstraints(maxWidth: 240),\n                  decoration: BoxDecoration(\n                    border: Border.all(\n                      color: Theme.of(context).dividerColor,\n                    ),\n                    borderRadius: BorderRadius.circular(8),\n                  ),\n                  padding: const EdgeInsets.all(8.0),\n                  child: Row(\n                    mainAxisSize: MainAxisSize.min,\n                    children: [\n                      Container(\n                        decoration: BoxDecoration(\n                          color: AFThemeExtension.of(context).tint1,\n                          borderRadius: BorderRadius.circular(8),\n                        ),\n                        height: 32,\n                        width: 32,\n                        child: Center(\n                          child: FlowySvg(\n                            FlowySvgs.page_m,\n                            size: const Size.square(16),\n                            color: Theme.of(context).hintColor,\n                          ),\n                        ),\n                      ),\n                      const HSpace(8),\n                      Flexible(\n                        child: Column(\n                          crossAxisAlignment: CrossAxisAlignment.start,\n                          mainAxisAlignment: MainAxisAlignment.center,\n                          children: [\n                            FlowyText(\n                              widget.file.fileName,\n                              fontSize: 12.0,\n                            ),\n                            FlowyText(\n                              widget.file.fileType.name,\n                              color: Theme.of(context).hintColor,\n                              fontSize: 12.0,\n                            ),\n                          ],\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n                if (isHover)\n                  _CloseButton(\n                    onTap: widget.onDeleted,\n                  ).positioned(top: 0, right: 0),\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  void setHover(bool value) {\n    if (value != isHover) {\n      setState(() => isHover = value);\n    }\n  }\n}\n\nclass _CloseButton extends StatelessWidget {\n  const _CloseButton({required this.onTap});\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      cursor: SystemMouseCursors.click,\n      child: GestureDetector(\n        onTap: onTap,\n        child: FlowySvg(\n          FlowySvgs.ai_close_filled_s,\n          color: AFThemeExtension.of(context).greyHover,\n          size: const Size.square(16),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/prompt_input/layout_define.dart",
    "content": "import 'package:flutter/widgets.dart';\n\nclass DesktopAIPromptSizes {\n  const DesktopAIPromptSizes._();\n\n  static const attachedFilesBarPadding =\n      EdgeInsets.only(left: 8.0, top: 8.0, right: 8.0);\n  static const attachedFilesPreviewHeight = 48.0;\n  static const attachedFilesPreviewSpacing = 12.0;\n\n  static const predefinedFormatButtonHeight = 28.0;\n  static const predefinedFormatIconHeight = 16.0;\n\n  static const textFieldMinHeight = 36.0;\n  static const textFieldContentPadding =\n      EdgeInsetsDirectional.fromSTEB(14.0, 8.0, 14.0, 8.0);\n\n  static const actionBarButtonSize = 28.0;\n  static const actionBarIconSize = 16.0;\n  static const actionBarSendButtonSize = 32.0;\n  static const actionBarSendButtonIconSize = 24.0;\n}\n\nclass MobileAIPromptSizes {\n  const MobileAIPromptSizes._();\n\n  static const attachedFilesBarHeight = 68.0;\n  static const attachedFilesBarPadding =\n      EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0, bottom: 4.0);\n  static const attachedFilesPreviewHeight = 56.0;\n  static const attachedFilesPreviewSpacing = 8.0;\n\n  static const predefinedFormatButtonHeight = 32.0;\n  static const predefinedFormatIconHeight = 20.0;\n\n  static const textFieldMinHeight = 32.0;\n  static const textFieldContentPadding =\n      EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0);\n\n  static const mentionIconSize = 20.0;\n  static const sendButtonSize = 32.0;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/prompt_input/mention_page_bottom_sheet.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/flowy_search_text_field.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/plugins/base/drag_handler.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport 'mention_page_menu.dart';\n\nFuture<ViewPB?> showPageSelectorSheet(\n  BuildContext context, {\n  required bool Function(ViewPB view) filter,\n}) async {\n  return showMobileBottomSheet<ViewPB>(\n    context,\n    backgroundColor: Theme.of(context).colorScheme.surface,\n    maxChildSize: 0.98,\n    enableDraggableScrollable: true,\n    scrollableWidgetBuilder: (context, scrollController) {\n      return Expanded(\n        child: _MobilePageSelectorBody(\n          filter: filter,\n          scrollController: scrollController,\n        ),\n      );\n    },\n    builder: (context) => const SizedBox.shrink(),\n  );\n}\n\nclass _MobilePageSelectorBody extends StatefulWidget {\n  const _MobilePageSelectorBody({\n    this.filter,\n    this.scrollController,\n  });\n\n  final bool Function(ViewPB view)? filter;\n  final ScrollController? scrollController;\n\n  @override\n  State<_MobilePageSelectorBody> createState() =>\n      _MobilePageSelectorBodyState();\n}\n\nclass _MobilePageSelectorBodyState extends State<_MobilePageSelectorBody> {\n  final textController = TextEditingController();\n  late final Future<List<ViewPB>> _viewsFuture = _fetchViews();\n\n  @override\n  void dispose() {\n    textController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return CustomScrollView(\n      controller: widget.scrollController,\n      shrinkWrap: true,\n      slivers: [\n        SliverPersistentHeader(\n          pinned: true,\n          delegate: _Header(\n            child: ColoredBox(\n              color: Theme.of(context).cardColor,\n              child: Column(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  const DragHandle(),\n                  SizedBox(\n                    height: 44.0,\n                    child: Center(\n                      child: FlowyText.medium(\n                        LocaleKeys.document_mobilePageSelector_title.tr(),\n                        fontSize: 16.0,\n                      ),\n                    ),\n                  ),\n                  Padding(\n                    padding: const EdgeInsets.symmetric(\n                      horizontal: 16.0,\n                      vertical: 8.0,\n                    ),\n                    child: SizedBox(\n                      height: 44.0,\n                      child: FlowySearchTextField(\n                        controller: textController,\n                        onChanged: (_) => setState(() {}),\n                      ),\n                    ),\n                  ),\n                  const Divider(height: 0.5, thickness: 0.5),\n                ],\n              ),\n            ),\n          ),\n        ),\n        FutureBuilder(\n          future: _viewsFuture,\n          builder: (_, snapshot) {\n            if (snapshot.connectionState == ConnectionState.waiting) {\n              return const SliverToBoxAdapter(\n                child: CircularProgressIndicator.adaptive(),\n              );\n            }\n\n            if (snapshot.hasError || snapshot.data == null) {\n              return SliverToBoxAdapter(\n                child: FlowyText(\n                  LocaleKeys.document_mobilePageSelector_failedToLoad.tr(),\n                ),\n              );\n            }\n\n            final views = snapshot.data!\n                .where((v) => widget.filter?.call(v) ?? true)\n                .toList();\n\n            final filtered = views.where(\n              (v) =>\n                  textController.text.isEmpty ||\n                  v.name\n                      .toLowerCase()\n                      .contains(textController.text.toLowerCase()),\n            );\n\n            if (filtered.isEmpty) {\n              return SliverToBoxAdapter(\n                child: FlowyText(\n                  LocaleKeys.document_mobilePageSelector_noPagesFound.tr(),\n                ),\n              );\n            }\n\n            return SliverPadding(\n              padding:\n                  const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),\n              sliver: SliverList(\n                delegate: SliverChildBuilderDelegate(\n                  (context, index) {\n                    final view = filtered.elementAt(index);\n                    return InkWell(\n                      onTap: () => Navigator.of(context).pop(view),\n                      borderRadius: BorderRadius.circular(12),\n                      splashColor: Colors.transparent,\n                      child: Container(\n                        height: 44,\n                        padding: const EdgeInsets.all(4.0),\n                        child: Row(\n                          children: [\n                            MentionViewIcon(view: view),\n                            const HSpace(8),\n                            Expanded(\n                              child: MentionViewTitleAndAncestors(view: view),\n                            ),\n                          ],\n                        ),\n                      ),\n                    );\n                  },\n                  childCount: filtered.length,\n                ),\n              ),\n            );\n          },\n        ),\n      ],\n    );\n  }\n\n  Future<List<ViewPB>> _fetchViews() async =>\n      (await ViewBackendService.getAllViews()).toNullable()?.items ?? [];\n}\n\nclass _Header extends SliverPersistentHeaderDelegate {\n  const _Header({\n    required this.child,\n  });\n\n  final Widget child;\n\n  @override\n  Widget build(\n    BuildContext context,\n    double shrinkOffset,\n    bool overlapsContent,\n  ) {\n    return child;\n  }\n\n  @override\n  double get maxExtent => 120.5;\n\n  @override\n  double get minExtent => 120.5;\n\n  @override\n  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {\n    return false;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/prompt_input/mention_page_menu.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view_title/view_title_bar_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart' hide TextDirection;\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:scroll_to_index/scroll_to_index.dart';\n\nconst double _itemHeight = 44.0;\nconst double _noPageHeight = 20.0;\nconst double _fixedWidth = 360.0;\nconst double _maxHeight = 328.0;\n\nclass PromptInputAnchor {\n  PromptInputAnchor(this.anchorKey, this.layerLink);\n\n  final GlobalKey<State<StatefulWidget>> anchorKey;\n  final LayerLink layerLink;\n}\n\nclass PromptInputMentionPageMenu extends StatefulWidget {\n  const PromptInputMentionPageMenu({\n    super.key,\n    required this.anchor,\n    required this.textController,\n    required this.onPageSelected,\n  });\n\n  final PromptInputAnchor anchor;\n  final TextEditingController textController;\n  final void Function(ViewPB view) onPageSelected;\n\n  @override\n  State<PromptInputMentionPageMenu> createState() =>\n      _PromptInputMentionPageMenuState();\n}\n\nclass _PromptInputMentionPageMenuState\n    extends State<PromptInputMentionPageMenu> {\n  @override\n  void initState() {\n    super.initState();\n    Future.delayed(Duration.zero, () {\n      if (mounted) {\n        context.read<ChatInputControlCubit>().refreshViews();\n      }\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<ChatInputControlCubit, ChatInputControlState>(\n      builder: (context, state) {\n        return Stack(\n          children: [\n            CompositedTransformFollower(\n              link: widget.anchor.layerLink,\n              showWhenUnlinked: false,\n              offset: Offset(getPopupOffsetX(), 0.0),\n              followerAnchor: Alignment.bottomLeft,\n              child: Container(\n                constraints: const BoxConstraints(\n                  minWidth: _fixedWidth,\n                  maxWidth: _fixedWidth,\n                  maxHeight: _maxHeight,\n                ),\n                decoration: BoxDecoration(\n                  color: Theme.of(context).cardColor,\n                  borderRadius: BorderRadius.circular(6.0),\n                  boxShadow: const [\n                    BoxShadow(\n                      color: Color(0x0A1F2329),\n                      blurRadius: 24,\n                      offset: Offset(0, 8),\n                      spreadRadius: 8,\n                    ),\n                    BoxShadow(\n                      color: Color(0x0A1F2329),\n                      blurRadius: 12,\n                      offset: Offset(0, 6),\n                    ),\n                    BoxShadow(\n                      color: Color(0x0F1F2329),\n                      blurRadius: 8,\n                      offset: Offset(0, 4),\n                      spreadRadius: -8,\n                    ),\n                  ],\n                ),\n                child: TextFieldTapRegion(\n                  child: PromptInputMentionPageList(\n                    onPageSelected: widget.onPageSelected,\n                  ),\n                ),\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  double getPopupOffsetX() {\n    if (widget.anchor.anchorKey.currentContext == null) {\n      return 0.0;\n    }\n\n    final cubit = context.read<ChatInputControlCubit>();\n    if (cubit.filterStartPosition == -1) {\n      return 0.0;\n    }\n\n    final textPosition = TextPosition(offset: cubit.filterEndPosition);\n    final renderBox =\n        widget.anchor.anchorKey.currentContext?.findRenderObject() as RenderBox;\n\n    final textPainter = TextPainter(\n      text: TextSpan(text: cubit.formatIntputText(widget.textController.text)),\n      textDirection: TextDirection.ltr,\n    );\n    textPainter.layout(\n      minWidth: renderBox.size.width,\n      maxWidth: renderBox.size.width,\n    );\n\n    final caretOffset = textPainter.getOffsetForCaret(textPosition, Rect.zero);\n    final boxes = textPainter.getBoxesForSelection(\n      TextSelection(\n        baseOffset: textPosition.offset,\n        extentOffset: textPosition.offset,\n      ),\n    );\n\n    if (boxes.isNotEmpty) {\n      return boxes.last.right;\n    }\n\n    return caretOffset.dx;\n  }\n}\n\nclass PromptInputMentionPageList extends StatefulWidget {\n  const PromptInputMentionPageList({\n    super.key,\n    required this.onPageSelected,\n  });\n\n  final void Function(ViewPB view) onPageSelected;\n\n  @override\n  State<PromptInputMentionPageList> createState() =>\n      _PromptInputMentionPageListState();\n}\n\nclass _PromptInputMentionPageListState\n    extends State<PromptInputMentionPageList> {\n  final autoScrollController = SimpleAutoScrollController(\n    suggestedRowHeight: _itemHeight,\n    beginGetter: (rect) => rect.top + 8.0,\n    endGetter: (rect) => rect.bottom - 8.0,\n  );\n\n  @override\n  void dispose() {\n    autoScrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocConsumer<ChatInputControlCubit, ChatInputControlState>(\n      listenWhen: (previous, current) {\n        return previous.maybeWhen(\n          ready: (_, pFocusedViewIndex) => current.maybeWhen(\n            ready: (_, cFocusedViewIndex) =>\n                pFocusedViewIndex != cFocusedViewIndex,\n            orElse: () => false,\n          ),\n          orElse: () => false,\n        );\n      },\n      listener: (context, state) {\n        state.maybeWhen(\n          ready: (views, focusedViewIndex) {\n            if (focusedViewIndex == -1 || !autoScrollController.hasClients) {\n              return;\n            }\n            if (autoScrollController.isAutoScrolling) {\n              autoScrollController.position\n                  .jumpTo(autoScrollController.position.pixels);\n            }\n            autoScrollController.scrollToIndex(\n              focusedViewIndex,\n              duration: const Duration(milliseconds: 200),\n              preferPosition: AutoScrollPosition.begin,\n            );\n          },\n          orElse: () {},\n        );\n      },\n      builder: (context, state) {\n        return state.maybeWhen(\n          loading: () {\n            return const Padding(\n              padding: EdgeInsets.all(8.0),\n              child: SizedBox(\n                height: _noPageHeight,\n                child: Center(\n                  child: CircularProgressIndicator.adaptive(),\n                ),\n              ),\n            );\n          },\n          ready: (views, focusedViewIndex) {\n            if (views.isEmpty) {\n              return Padding(\n                padding: const EdgeInsets.all(8.0),\n                child: SizedBox(\n                  height: _noPageHeight,\n                  child: Center(\n                    child: FlowyText(\n                      LocaleKeys.chat_inputActionNoPages.tr(),\n                    ),\n                  ),\n                ),\n              );\n            }\n\n            return ListView.builder(\n              shrinkWrap: true,\n              controller: autoScrollController,\n              padding: const EdgeInsets.all(8.0),\n              itemCount: views.length,\n              itemBuilder: (context, index) {\n                final view = views[index];\n                return AutoScrollTag(\n                  key: ValueKey(\"chat_mention_page_item_${view.id}\"),\n                  index: index,\n                  controller: autoScrollController,\n                  child: _ChatMentionPageItem(\n                    view: view,\n                    onTap: () => widget.onPageSelected(view),\n                    isSelected: focusedViewIndex == index,\n                  ),\n                );\n              },\n            );\n          },\n          orElse: () => const SizedBox.shrink(),\n        );\n      },\n    );\n  }\n}\n\nclass _ChatMentionPageItem extends StatelessWidget {\n  const _ChatMentionPageItem({\n    required this.view,\n    required this.isSelected,\n    required this.onTap,\n  });\n\n  final ViewPB view;\n  final bool isSelected;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      message: view.name,\n      child: MouseRegion(\n        cursor: SystemMouseCursors.click,\n        child: GestureDetector(\n          onTap: onTap,\n          behavior: HitTestBehavior.opaque,\n          child: FlowyHover(\n            isSelected: () => isSelected,\n            child: Container(\n              height: _itemHeight,\n              padding: const EdgeInsets.all(4.0),\n              child: Row(\n                children: [\n                  MentionViewIcon(view: view),\n                  const HSpace(8.0),\n                  Expanded(child: MentionViewTitleAndAncestors(view: view)),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass MentionViewIcon extends StatelessWidget {\n  const MentionViewIcon({\n    super.key,\n    required this.view,\n  });\n\n  final ViewPB view;\n\n  @override\n  Widget build(BuildContext context) {\n    final spaceIcon = view.buildSpaceIconSvg(context);\n\n    if (view.icon.value.isNotEmpty) {\n      return SizedBox(\n        width: 16.0,\n        child: RawEmojiIconWidget(\n          emoji: view.icon.toEmojiIconData(),\n          emojiSize: 14,\n        ),\n      );\n    }\n\n    if (view.isSpace == true && spaceIcon != null) {\n      return SpaceIcon(\n        dimension: 16.0,\n        svgSize: 9.68,\n        space: view,\n        cornerRadius: 4,\n      );\n    }\n\n    return FlowySvg(\n      view.layout.icon,\n      size: const Size.square(16),\n      color: Theme.of(context).hintColor,\n    );\n  }\n}\n\nclass MentionViewTitleAndAncestors extends StatelessWidget {\n  const MentionViewTitleAndAncestors({\n    super.key,\n    required this.view,\n  });\n\n  final ViewPB view;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) => ViewTitleBarBloc(view: view),\n      child: BlocBuilder<ViewTitleBarBloc, ViewTitleBarState>(\n        builder: (context, state) {\n          final nonEmptyName = view.name.isEmpty\n              ? LocaleKeys.document_title_placeholder.tr()\n              : view.name;\n\n          final ancestorList = _getViewAncestorList(state.ancestors);\n\n          if (state.ancestors.isEmpty || ancestorList.trim().isEmpty) {\n            return FlowyText(\n              nonEmptyName,\n              fontSize: 14.0,\n              overflow: TextOverflow.ellipsis,\n            );\n          }\n\n          return Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              FlowyText(\n                nonEmptyName,\n                fontSize: 14.0,\n                figmaLineHeight: 20.0,\n                overflow: TextOverflow.ellipsis,\n              ),\n              FlowyText(\n                ancestorList,\n                fontSize: 12.0,\n                figmaLineHeight: 16.0,\n                color: Theme.of(context).hintColor,\n                overflow: TextOverflow.ellipsis,\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  /// see workspace/presentation/widgets/view_title_bar.dart, upon which this\n  /// function was based. This version doesn't include the current view in the\n  /// result, and returns a string rather than a list of widgets\n  String _getViewAncestorList(\n    List<ViewPB> views,\n  ) {\n    const lowerBound = 2;\n    final upperBound = views.length - 2;\n    bool hasAddedEllipsis = false;\n    String result = \"\";\n\n    if (views.length <= 1) {\n      return \"\";\n    }\n\n    // ignore the workspace name, use section name instead in the future\n    // skip the workspace view\n    for (var i = 1; i < views.length - 1; i++) {\n      final view = views[i];\n\n      if (i >= lowerBound && i < upperBound) {\n        if (!hasAddedEllipsis) {\n          hasAddedEllipsis = true;\n          result += \"… / \";\n        }\n        continue;\n      }\n\n      final nonEmptyName = view.name.isEmpty\n          ? LocaleKeys.document_title_placeholder.tr()\n          : view.name;\n\n      result += nonEmptyName;\n\n      if (i != views.length - 2) {\n        // if not the last one, add a divider\n        result += \" / \";\n      }\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/prompt_input/mentioned_page_text_span.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:extended_text_library/extended_text_library.dart';\nimport 'package:flutter/material.dart';\n\nclass PromptInputTextSpanBuilder extends SpecialTextSpanBuilder {\n  PromptInputTextSpanBuilder({\n    required this.inputControlCubit,\n    this.mentionedPageTextStyle,\n  });\n\n  final ChatInputControlCubit inputControlCubit;\n  final TextStyle? mentionedPageTextStyle;\n\n  @override\n  SpecialText? createSpecialText(\n    String flag, {\n    TextStyle? textStyle,\n    SpecialTextGestureTapCallback? onTap,\n    int? index,\n  }) {\n    if (flag == '') {\n      return null;\n    }\n\n    if (isStart(flag, MentionedPageText.flag)) {\n      return MentionedPageText(\n        inputControlCubit,\n        mentionedPageTextStyle ?? textStyle,\n        onTap,\n        // scrubbing over text is kinda funky\n        start: index! - (MentionedPageText.flag.length - 1),\n      );\n    }\n\n    return null;\n  }\n}\n\nclass MentionedPageText extends SpecialText {\n  MentionedPageText(\n    this.inputControlCubit,\n    TextStyle? textStyle,\n    SpecialTextGestureTapCallback? onTap, {\n    this.start,\n  }) : super(flag, '', textStyle, onTap: onTap);\n\n  static const String flag = '@';\n\n  final int? start;\n  final ChatInputControlCubit inputControlCubit;\n\n  @override\n  bool isEnd(String value) => inputControlCubit.selectedViewIds.contains(value);\n\n  @override\n  InlineSpan finishText() {\n    final String actualText = toString();\n\n    final viewName = inputControlCubit.allViews\n            .firstWhereOrNull((view) => view.id == actualText.substring(1))\n            ?.name ??\n        \"\";\n    final nonEmptyName = viewName.isEmpty\n        ? LocaleKeys.document_title_placeholder.tr()\n        : viewName;\n\n    return SpecialTextSpan(\n      text: \"@$nonEmptyName\",\n      actualText: actualText,\n      start: start!,\n      style: textStyle,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../../service/ai_entities.dart';\nimport 'layout_define.dart';\n\nclass PromptInputDesktopToggleFormatButton extends StatelessWidget {\n  const PromptInputDesktopToggleFormatButton({\n    super.key,\n    required this.showFormatBar,\n    required this.onTap,\n  });\n\n  final bool showFormatBar;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyIconButton(\n      tooltipText: showFormatBar\n          ? LocaleKeys.chat_changeFormat_defaultDescription.tr()\n          : LocaleKeys.chat_changeFormat_blankDescription.tr(),\n      width: 28.0,\n      onPressed: onTap,\n      icon: showFormatBar\n          ? const FlowySvg(\n              FlowySvgs.m_aa_text_s,\n              size: Size.square(16.0),\n              color: Color(0xFF666D76),\n            )\n          : const FlowySvg(\n              FlowySvgs.ai_text_image_s,\n              size: Size(21.0, 16.0),\n              color: Color(0xFF666D76),\n            ),\n    );\n  }\n}\n\nclass ChangeFormatBar extends StatelessWidget {\n  const ChangeFormatBar({\n    super.key,\n    required this.predefinedFormat,\n    required this.spacing,\n    required this.onSelectPredefinedFormat,\n    this.showImageFormats = true,\n  });\n\n  final PredefinedFormat? predefinedFormat;\n  final double spacing;\n  final void Function(PredefinedFormat) onSelectPredefinedFormat;\n  final bool showImageFormats;\n\n  @override\n  Widget build(BuildContext context) {\n    final showTextFormats = predefinedFormat?.imageFormat.hasText ?? true;\n    return SizedBox(\n      height: DesktopAIPromptSizes.predefinedFormatButtonHeight,\n      child: SeparatedRow(\n        mainAxisSize: MainAxisSize.min,\n        separatorBuilder: () => HSpace(spacing),\n        children: [\n          if (showImageFormats) ...[\n            _buildFormatButton(context, ImageFormat.text),\n            _buildFormatButton(context, ImageFormat.textAndImage),\n            _buildFormatButton(context, ImageFormat.image),\n          ],\n          if (showImageFormats && showTextFormats) _buildDivider(),\n          if (showTextFormats) ...[\n            _buildTextFormatButton(context, TextFormat.paragraph),\n            _buildTextFormatButton(context, TextFormat.bulletList),\n            _buildTextFormatButton(context, TextFormat.numberedList),\n            _buildTextFormatButton(context, TextFormat.table),\n          ],\n        ],\n      ),\n    );\n  }\n\n  Widget _buildFormatButton(BuildContext context, ImageFormat format) {\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: () {\n        if (predefinedFormat != null &&\n            format == predefinedFormat!.imageFormat) {\n          return;\n        }\n        if (format.hasText) {\n          final textFormat =\n              predefinedFormat?.textFormat ?? TextFormat.paragraph;\n          onSelectPredefinedFormat(\n            PredefinedFormat(imageFormat: format, textFormat: textFormat),\n          );\n        } else {\n          onSelectPredefinedFormat(\n            PredefinedFormat(imageFormat: format, textFormat: null),\n          );\n        }\n      },\n      child: FlowyTooltip(\n        message: format.i18n,\n        preferBelow: false,\n        child: SizedBox.square(\n          dimension: _buttonSize,\n          child: FlowyHover(\n            isSelected: () => format == predefinedFormat?.imageFormat,\n            child: Center(\n              child: FlowySvg(\n                format.icon,\n                size: format == ImageFormat.textAndImage\n                    ? Size(21.0 / 16.0 * _iconSize, _iconSize)\n                    : Size.square(_iconSize),\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildDivider() {\n    return VerticalDivider(\n      indent: 6.0,\n      endIndent: 6.0,\n      width: 1.0 + spacing * 2,\n    );\n  }\n\n  Widget _buildTextFormatButton(\n    BuildContext context,\n    TextFormat format,\n  ) {\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: () {\n        if (predefinedFormat != null &&\n            format == predefinedFormat!.textFormat) {\n          return;\n        }\n        onSelectPredefinedFormat(\n          PredefinedFormat(\n            imageFormat: predefinedFormat?.imageFormat ?? ImageFormat.text,\n            textFormat: format,\n          ),\n        );\n      },\n      child: FlowyTooltip(\n        message: format.i18n,\n        preferBelow: false,\n        child: SizedBox.square(\n          dimension: _buttonSize,\n          child: FlowyHover(\n            isSelected: () => format == predefinedFormat?.textFormat,\n            child: Center(\n              child: FlowySvg(\n                format.icon,\n                size: Size.square(_iconSize),\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  double get _buttonSize {\n    return UniversalPlatform.isMobile\n        ? MobileAIPromptSizes.predefinedFormatButtonHeight\n        : DesktopAIPromptSizes.predefinedFormatButtonHeight;\n  }\n\n  double get _iconSize {\n    return UniversalPlatform.isMobile\n        ? MobileAIPromptSizes.predefinedFormatIconHeight\n        : DesktopAIPromptSizes.predefinedFormatIconHeight;\n  }\n}\n\nclass PromptInputMobileToggleFormatButton extends StatelessWidget {\n  const PromptInputMobileToggleFormatButton({\n    super.key,\n    required this.showFormatBar,\n    required this.onTap,\n  });\n\n  final bool showFormatBar;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox.square(\n      dimension: 32.0,\n      child: FlowyButton(\n        radius: const BorderRadius.all(Radius.circular(8.0)),\n        margin: EdgeInsets.zero,\n        expandText: false,\n        text: showFormatBar\n            ? const FlowySvg(\n                FlowySvgs.m_aa_text_s,\n                size: Size.square(20.0),\n              )\n            : const FlowySvg(\n                FlowySvgs.ai_text_image_s,\n                size: Size(26.25, 20.0),\n              ),\n        onTap: onTap,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/prompt_input/prompt_input_text_controller.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/widgets.dart';\n\nfinal openingBracketReplacement = String.fromCharCode(0xFFFE);\nfinal closingBracketReplacement = String.fromCharCode(0xFFFD);\n\nclass AiPromptInputTextEditingController extends TextEditingController {\n  AiPromptInputTextEditingController();\n\n  static String replace(String text) {\n    return text\n        .replaceAll('[', openingBracketReplacement)\n        .replaceAll(']', closingBracketReplacement);\n  }\n\n  static String restore(String text) {\n    return text\n        .replaceAll(openingBracketReplacement, '[')\n        .replaceAll(closingBracketReplacement, ']');\n  }\n\n  void usePrompt(String content) {\n    value = TextEditingValue(\n      text: content,\n      selection: TextSelection.collapsed(\n        offset: content.length,\n      ),\n    );\n  }\n\n  @override\n  TextSpan buildTextSpan({\n    required BuildContext context,\n    TextStyle? style,\n    required bool withComposing,\n  }) {\n    return TextSpan(\n      style: style,\n      children: <InlineSpan>[...getTextSpans(context)],\n    );\n  }\n\n  Iterable<TextSpan> getTextSpans(BuildContext context) {\n    final open = openingBracketReplacement;\n    final close = closingBracketReplacement;\n    final regex = RegExp('($open[^$open$close]*?$close)');\n    final theme = AppFlowyTheme.of(context);\n\n    final result = <TextSpan>[];\n\n    text.splitMapJoin(\n      regex,\n      onMatch: (match) {\n        final string = match.group(0)!;\n        result.add(\n          TextSpan(\n            text: restore(string),\n            style: theme.textStyle.body.standard().copyWith(\n                  color: theme.textColorScheme.featured,\n                  backgroundColor:\n                      theme.fillColorScheme.featuredThick.withAlpha(51),\n                ),\n          ),\n        );\n        return '';\n      },\n      onNonMatch: (nonMatch) {\n        result.add(\n          TextSpan(\n            text: restore(nonMatch),\n          ),\n        );\n        return '';\n      },\n    );\n\n    return result;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SelectModelMenu extends StatefulWidget {\n  const SelectModelMenu({\n    super.key,\n    required this.aiModelStateNotifier,\n  });\n\n  final AIModelStateNotifier aiModelStateNotifier;\n\n  @override\n  State<SelectModelMenu> createState() => _SelectModelMenuState();\n}\n\nclass _SelectModelMenuState extends State<SelectModelMenu> {\n  final popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => SelectModelBloc(\n        aiModelStateNotifier: widget.aiModelStateNotifier,\n      ),\n      child: BlocBuilder<SelectModelBloc, SelectModelState>(\n        builder: (context, state) {\n          return AppFlowyPopover(\n            offset: Offset(-12.0, 0.0),\n            constraints: BoxConstraints(maxWidth: 250, maxHeight: 600),\n            direction: PopoverDirection.topWithLeftAligned,\n            margin: EdgeInsets.zero,\n            controller: popoverController,\n            popupBuilder: (popoverContext) {\n              return SelectModelPopoverContent(\n                models: state.models,\n                selectedModel: state.selectedModel,\n                onSelectModel: (model) {\n                  if (model != state.selectedModel) {\n                    context\n                        .read<SelectModelBloc>()\n                        .add(SelectModelEvent.selectModel(model));\n                  }\n                  popoverController.close();\n                },\n              );\n            },\n            child: _CurrentModelButton(\n              model: state.selectedModel,\n              onTap: () {\n                if (state.selectedModel != null) {\n                  popoverController.show();\n                }\n              },\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass SelectModelPopoverContent extends StatelessWidget {\n  const SelectModelPopoverContent({\n    super.key,\n    required this.models,\n    required this.selectedModel,\n    this.onSelectModel,\n  });\n\n  final List<AIModelPB> models;\n  final AIModelPB? selectedModel;\n  final void Function(AIModelPB)? onSelectModel;\n\n  @override\n  Widget build(BuildContext context) {\n    if (models.isEmpty) {\n      return const SizedBox.shrink();\n    }\n\n    // separate models into local and cloud models\n    final localModels = models.where((model) => model.isLocal).toList();\n    final cloudModels = models.where((model) => !model.isLocal).toList();\n\n    return Padding(\n      padding: const EdgeInsets.all(8.0),\n      child: SingleChildScrollView(\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            if (localModels.isNotEmpty) ...[\n              _ModelSectionHeader(\n                title: LocaleKeys.chat_switchModel_localModel.tr(),\n              ),\n              const VSpace(4.0),\n            ],\n            ...localModels.map(\n              (model) => _ModelItem(\n                model: model,\n                isSelected: model == selectedModel,\n                onTap: () => onSelectModel?.call(model),\n              ),\n            ),\n            if (cloudModels.isNotEmpty && localModels.isNotEmpty) ...[\n              const VSpace(8.0),\n              _ModelSectionHeader(\n                title: LocaleKeys.chat_switchModel_cloudModel.tr(),\n              ),\n              const VSpace(4.0),\n            ],\n            ...cloudModels.map(\n              (model) => _ModelItem(\n                model: model,\n                isSelected: model == selectedModel,\n                onTap: () => onSelectModel?.call(model),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _ModelSectionHeader extends StatelessWidget {\n  const _ModelSectionHeader({\n    required this.title,\n  });\n\n  final String title;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.only(top: 4, bottom: 2),\n      child: FlowyText(\n        title,\n        fontSize: 12,\n        figmaLineHeight: 16,\n        color: Theme.of(context).hintColor,\n        fontWeight: FontWeight.w500,\n      ),\n    );\n  }\n}\n\nclass _ModelItem extends StatelessWidget {\n  const _ModelItem({\n    required this.model,\n    required this.isSelected,\n    required this.onTap,\n  });\n\n  final AIModelPB model;\n  final bool isSelected;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return ConstrainedBox(\n      constraints: const BoxConstraints(minHeight: 32),\n      child: FlowyButton(\n        onTap: onTap,\n        margin: EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),\n        text: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            FlowyText(\n              model.i18n,\n              figmaLineHeight: 20,\n              overflow: TextOverflow.ellipsis,\n            ),\n            if (model.desc.isNotEmpty)\n              FlowyText(\n                model.desc,\n                fontSize: 12,\n                figmaLineHeight: 16,\n                color: Theme.of(context).hintColor,\n                overflow: TextOverflow.ellipsis,\n              ),\n          ],\n        ),\n        rightIcon: isSelected\n            ? FlowySvg(\n                FlowySvgs.check_s,\n                size: const Size.square(20),\n                color: Theme.of(context).colorScheme.primary,\n              )\n            : null,\n      ),\n    );\n  }\n}\n\nclass _CurrentModelButton extends StatelessWidget {\n  const _CurrentModelButton({\n    required this.model,\n    required this.onTap,\n  });\n\n  final AIModelPB? model;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      message: LocaleKeys.chat_switchModel_label.tr(),\n      child: GestureDetector(\n        onTap: onTap,\n        behavior: HitTestBehavior.opaque,\n        child: SizedBox(\n          height: DesktopAIPromptSizes.actionBarButtonSize,\n          child: FlowyHover(\n            style: const HoverStyle(\n              borderRadius: BorderRadius.all(Radius.circular(8)),\n            ),\n            child: Padding(\n              padding: const EdgeInsetsDirectional.all(4.0),\n              child: Row(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  Padding(\n                    // TODO: remove this after change icon to 20px\n                    padding: EdgeInsets.all(2),\n                    child: FlowySvg(\n                      FlowySvgs.ai_sparks_s,\n                      color: Theme.of(context).hintColor,\n                      size: Size.square(16),\n                    ),\n                  ),\n                  if (model != null && !model!.isDefault)\n                    Padding(\n                      padding: EdgeInsetsDirectional.only(end: 2.0),\n                      child: FlowyText(\n                        model!.i18n,\n                        fontSize: 12,\n                        figmaLineHeight: 16,\n                        color: Theme.of(context).hintColor,\n                        overflow: TextOverflow.ellipsis,\n                      ),\n                    ),\n                  FlowySvg(\n                    FlowySvgs.ai_source_drop_down_s,\n                    color: Theme.of(context).hintColor,\n                    size: const Size.square(8),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_bottom_sheet.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/ai/service/view_selector_cubit.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/flowy_search_text_field.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/plugins/base/drag_handler.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'select_sources_menu.dart';\n\nclass PromptInputMobileSelectSourcesButton extends StatefulWidget {\n  const PromptInputMobileSelectSourcesButton({\n    super.key,\n    required this.selectedSourcesNotifier,\n    required this.onUpdateSelectedSources,\n  });\n\n  final ValueNotifier<List<String>> selectedSourcesNotifier;\n  final void Function(List<String>) onUpdateSelectedSources;\n\n  @override\n  State<PromptInputMobileSelectSourcesButton> createState() =>\n      _PromptInputMobileSelectSourcesButtonState();\n}\n\nclass _PromptInputMobileSelectSourcesButtonState\n    extends State<PromptInputMobileSelectSourcesButton> {\n  late final cubit = ViewSelectorCubit(\n    maxSelectedParentPageCount: 3,\n    getIgnoreViewType: (item) {\n      if (item.view.isSpace) {\n        return IgnoreViewType.none;\n      }\n      if (item.view.layout != ViewLayoutPB.Document) {\n        return IgnoreViewType.hide;\n      }\n      return IgnoreViewType.none;\n    },\n  );\n\n  @override\n  void initState() {\n    super.initState();\n    widget.selectedSourcesNotifier.addListener(onSelectedSourcesChanged);\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      onSelectedSourcesChanged();\n    });\n  }\n\n  @override\n  void dispose() {\n    widget.selectedSourcesNotifier.removeListener(onSelectedSourcesChanged);\n    cubit.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(\n      builder: (context, state) {\n        final userProfile = context.read<UserWorkspaceBloc>().state.userProfile;\n        final workspaceId = state.currentWorkspace?.workspaceId ?? '';\n        return MultiBlocProvider(\n          providers: [\n            BlocProvider(\n              key: ValueKey(workspaceId),\n              create: (context) => SpaceBloc(\n                userProfile: userProfile,\n                workspaceId: workspaceId,\n              )..add(const SpaceEvent.initial(openFirstPage: false)),\n            ),\n            BlocProvider.value(\n              value: cubit,\n            ),\n          ],\n          child: BlocBuilder<SpaceBloc, SpaceState>(\n            builder: (context, state) {\n              return FlowyButton(\n                margin: const EdgeInsetsDirectional.fromSTEB(4, 6, 2, 6),\n                expandText: false,\n                text: Row(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    FlowySvg(\n                      FlowySvgs.ai_page_s,\n                      color: Theme.of(context).iconTheme.color,\n                      size: const Size.square(20.0),\n                    ),\n                    const HSpace(2.0),\n                    ValueListenableBuilder(\n                      valueListenable: widget.selectedSourcesNotifier,\n                      builder: (context, selectedSourceIds, _) {\n                        final documentId =\n                            context.read<DocumentBloc?>()?.documentId;\n                        final label = documentId != null &&\n                                selectedSourceIds.length == 1 &&\n                                selectedSourceIds[0] == documentId\n                            ? LocaleKeys.chat_currentPage.tr()\n                            : selectedSourceIds.length.toString();\n                        return FlowyText(\n                          label,\n                          fontSize: 14,\n                          figmaLineHeight: 20,\n                          color: Theme.of(context).hintColor,\n                        );\n                      },\n                    ),\n                    const HSpace(2.0),\n                    FlowySvg(\n                      FlowySvgs.ai_source_drop_down_s,\n                      color: Theme.of(context).hintColor,\n                      size: const Size.square(10),\n                    ),\n                  ],\n                ),\n                onTap: () async {\n                  unawaited(\n                    context\n                        .read<ViewSelectorCubit>()\n                        .refreshSources(state.spaces, state.currentSpace),\n                  );\n\n                  await showMobileBottomSheet<void>(\n                    context,\n                    backgroundColor: theme.surfaceColorScheme.primary,\n                    maxChildSize: 0.98,\n                    enableDraggableScrollable: true,\n                    scrollableWidgetBuilder: (_, scrollController) {\n                      return Expanded(\n                        child: BlocProvider.value(\n                          value: cubit,\n                          child: _MobileSelectSourcesSheetBody(\n                            scrollController: scrollController,\n                          ),\n                        ),\n                      );\n                    },\n                    builder: (context) => const SizedBox.shrink(),\n                  );\n                  if (context.mounted) {\n                    widget.onUpdateSelectedSources(cubit.selectedSourceIds);\n                  }\n                },\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n\n  void onSelectedSourcesChanged() {\n    cubit\n      ..updateSelectedSources(widget.selectedSourcesNotifier.value)\n      ..updateSelectedStatus();\n  }\n}\n\nclass _MobileSelectSourcesSheetBody extends StatelessWidget {\n  const _MobileSelectSourcesSheetBody({\n    required this.scrollController,\n  });\n\n  final ScrollController scrollController;\n\n  @override\n  Widget build(BuildContext context) {\n    return CustomScrollView(\n      controller: scrollController,\n      shrinkWrap: true,\n      slivers: [\n        SliverPersistentHeader(\n          pinned: true,\n          delegate: _Header(\n            child: ColoredBox(\n              color: AppFlowyTheme.of(context).surfaceColorScheme.primary,\n              child: Column(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  const DragHandle(),\n                  SizedBox(\n                    height: 44.0,\n                    child: Center(\n                      child: FlowyText.medium(\n                        LocaleKeys.chat_selectSources.tr(),\n                        fontSize: 16.0,\n                      ),\n                    ),\n                  ),\n                  Padding(\n                    padding: const EdgeInsets.symmetric(\n                      horizontal: 16.0,\n                      vertical: 8.0,\n                    ),\n                    child: SizedBox(\n                      height: 44.0,\n                      child: FlowySearchTextField(\n                        controller: context\n                            .read<ViewSelectorCubit>()\n                            .filterTextController,\n                      ),\n                    ),\n                  ),\n                  const Divider(height: 0.5, thickness: 0.5),\n                ],\n              ),\n            ),\n          ),\n        ),\n        BlocBuilder<ViewSelectorCubit, ViewSelectorState>(\n          builder: (context, state) {\n            return SliverList(\n              delegate: SliverChildBuilderDelegate(\n                childCount: state.selectedSources.length,\n                (context, index) {\n                  final source = state.selectedSources.elementAt(index);\n                  return ViewSelectorTreeItem(\n                    key: ValueKey(\n                      'selected_select_sources_tree_item_${source.view.id}',\n                    ),\n                    viewSelectorItem: source,\n                    level: 0,\n                    isDescendentOfSpace: source.view.isSpace,\n                    isSelectedSection: true,\n                    onSelected: (item) {\n                      context\n                          .read<ViewSelectorCubit>()\n                          .toggleSelectedStatus(item, true);\n                    },\n                    height: 40.0,\n                  );\n                },\n              ),\n            );\n          },\n        ),\n        BlocBuilder<ViewSelectorCubit, ViewSelectorState>(\n          builder: (context, state) {\n            if (state.selectedSources.isNotEmpty &&\n                state.visibleSources.isNotEmpty) {\n              return SliverToBoxAdapter(\n                child: Padding(\n                  padding: const EdgeInsets.symmetric(horizontal: 16.0),\n                  child: AFDivider(),\n                ),\n              );\n            }\n\n            return const SliverToBoxAdapter();\n          },\n        ),\n        BlocBuilder<ViewSelectorCubit, ViewSelectorState>(\n          builder: (context, state) {\n            return SliverList(\n              delegate: SliverChildBuilderDelegate(\n                childCount: state.visibleSources.length,\n                (context, index) {\n                  final source = state.visibleSources.elementAt(index);\n                  return ViewSelectorTreeItem(\n                    key: ValueKey(\n                      'visible_select_sources_tree_item_${source.view.id}',\n                    ),\n                    viewSelectorItem: source,\n                    level: 0,\n                    isDescendentOfSpace: source.view.isSpace,\n                    isSelectedSection: false,\n                    onSelected: (item) {\n                      context\n                          .read<ViewSelectorCubit>()\n                          .toggleSelectedStatus(item, false);\n                    },\n                    height: 40.0,\n                  );\n                },\n              ),\n            );\n          },\n        ),\n      ],\n    );\n  }\n}\n\nclass _Header extends SliverPersistentHeaderDelegate {\n  const _Header({\n    required this.child,\n  });\n\n  final Widget child;\n\n  @override\n  Widget build(\n    BuildContext context,\n    double shrinkOffset,\n    bool overlapsContent,\n  ) {\n    return child;\n  }\n\n  @override\n  double get maxExtent => 120.5;\n\n  @override\n  double get minExtent => 120.5;\n\n  @override\n  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {\n    return false;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_menu.dart",
    "content": "import 'dart:math';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../service/view_selector_cubit.dart';\nimport '../view_selector.dart';\nimport 'layout_define.dart';\nimport 'mention_page_menu.dart';\n\nclass PromptInputDesktopSelectSourcesButton extends StatefulWidget {\n  const PromptInputDesktopSelectSourcesButton({\n    super.key,\n    required this.selectedSourcesNotifier,\n    required this.onUpdateSelectedSources,\n  });\n\n  final ValueNotifier<List<String>> selectedSourcesNotifier;\n  final void Function(List<String>) onUpdateSelectedSources;\n\n  @override\n  State<PromptInputDesktopSelectSourcesButton> createState() =>\n      _PromptInputDesktopSelectSourcesButtonState();\n}\n\nclass _PromptInputDesktopSelectSourcesButtonState\n    extends State<PromptInputDesktopSelectSourcesButton> {\n  late final cubit = ViewSelectorCubit(\n    maxSelectedParentPageCount: 3,\n    getIgnoreViewType: (item) {\n      final view = item.view;\n\n      if (view.isSpace) {\n        return IgnoreViewType.none;\n      }\n      if (view.layout == ViewLayoutPB.Chat) {\n        return IgnoreViewType.hide;\n      }\n      if (view.layout != ViewLayoutPB.Document) {\n        return IgnoreViewType.disable;\n      }\n\n      return IgnoreViewType.none;\n    },\n  );\n  final popoverController = PopoverController();\n\n  @override\n  void initState() {\n    super.initState();\n    widget.selectedSourcesNotifier.addListener(onSelectedSourcesChanged);\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      onSelectedSourcesChanged();\n    });\n  }\n\n  @override\n  void dispose() {\n    widget.selectedSourcesNotifier.removeListener(onSelectedSourcesChanged);\n    cubit.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ViewSelector(\n      viewSelectorCubit: BlocProvider.value(\n        value: cubit,\n      ),\n      child: BlocBuilder<SpaceBloc, SpaceState>(\n        builder: (context, state) {\n          return AppFlowyPopover(\n            constraints: BoxConstraints.loose(const Size(320, 380)),\n            offset: const Offset(0.0, -10.0),\n            direction: PopoverDirection.topWithCenterAligned,\n            margin: EdgeInsets.zero,\n            controller: popoverController,\n            onOpen: () {\n              context\n                  .read<ViewSelectorCubit>()\n                  .refreshSources(state.spaces, state.currentSpace);\n            },\n            onClose: () {\n              widget.onUpdateSelectedSources(cubit.selectedSourceIds);\n              context\n                  .read<ViewSelectorCubit>()\n                  .refreshSources(state.spaces, state.currentSpace);\n            },\n            popupBuilder: (_) {\n              return BlocProvider.value(\n                value: context.read<ViewSelectorCubit>(),\n                child: const _PopoverContent(),\n              );\n            },\n            child: _IndicatorButton(\n              selectedSourcesNotifier: widget.selectedSourcesNotifier,\n              onTap: () => popoverController.show(),\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  void onSelectedSourcesChanged() {\n    cubit\n      ..updateSelectedSources(widget.selectedSourcesNotifier.value)\n      ..updateSelectedStatus();\n  }\n}\n\nclass _IndicatorButton extends StatelessWidget {\n  const _IndicatorButton({\n    required this.selectedSourcesNotifier,\n    required this.onTap,\n  });\n\n  final ValueNotifier<List<String>> selectedSourcesNotifier;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onTap: onTap,\n      behavior: HitTestBehavior.opaque,\n      child: SizedBox(\n        height: DesktopAIPromptSizes.actionBarButtonSize,\n        child: FlowyHover(\n          style: const HoverStyle(\n            borderRadius: BorderRadius.all(Radius.circular(8)),\n          ),\n          child: Padding(\n            padding: const EdgeInsetsDirectional.fromSTEB(6, 6, 4, 6),\n            child: Row(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                FlowySvg(\n                  FlowySvgs.ai_page_s,\n                  color: Theme.of(context).hintColor,\n                ),\n                const HSpace(2.0),\n                ValueListenableBuilder(\n                  valueListenable: selectedSourcesNotifier,\n                  builder: (context, selectedSourceIds, _) {\n                    final documentId =\n                        context.read<DocumentBloc?>()?.documentId;\n                    final label = documentId != null &&\n                            selectedSourceIds.length == 1 &&\n                            selectedSourceIds[0] == documentId\n                        ? LocaleKeys.chat_currentPage.tr()\n                        : selectedSourceIds.length.toString();\n                    return FlowyText(\n                      label,\n                      fontSize: 12,\n                      figmaLineHeight: 16,\n                      color: Theme.of(context).hintColor,\n                    );\n                  },\n                ),\n                const HSpace(2.0),\n                FlowySvg(\n                  FlowySvgs.ai_source_drop_down_s,\n                  color: Theme.of(context).hintColor,\n                  size: const Size.square(8),\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _PopoverContent extends StatelessWidget {\n  const _PopoverContent();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<ViewSelectorCubit, ViewSelectorState>(\n      builder: (context, state) {\n        final theme = AppFlowyTheme.of(context);\n\n        return Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            Padding(\n              padding: const EdgeInsets.fromLTRB(8, 12, 8, 8),\n              child: AFTextField(\n                size: AFTextFieldSize.m,\n                controller:\n                    context.read<ViewSelectorCubit>().filterTextController,\n                hintText: LocaleKeys.search_label.tr(),\n              ),\n            ),\n            AFDivider(\n              startIndent: theme.spacing.l,\n              endIndent: theme.spacing.l,\n            ),\n            Flexible(\n              child: ListView(\n                shrinkWrap: true,\n                padding: const EdgeInsets.fromLTRB(8, 4, 8, 12),\n                children: [\n                  ..._buildSelectedSources(context, state),\n                  if (state.selectedSources.isNotEmpty &&\n                      state.visibleSources.isNotEmpty)\n                    AFDivider(\n                      spacing: 4.0,\n                      startIndent: theme.spacing.l,\n                      endIndent: theme.spacing.l,\n                    ),\n                  ..._buildVisibleSources(context, state),\n                ],\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  Iterable<Widget> _buildSelectedSources(\n    BuildContext context,\n    ViewSelectorState state,\n  ) {\n    return state.selectedSources.map(\n      (e) => ViewSelectorTreeItem(\n        key: ValueKey(\n          'selected_select_sources_tree_item_${e.view.id}',\n        ),\n        viewSelectorItem: e,\n        level: 0,\n        isDescendentOfSpace: e.view.isSpace,\n        isSelectedSection: true,\n        onSelected: (item) {\n          context.read<ViewSelectorCubit>().toggleSelectedStatus(item, true);\n        },\n        height: 30.0,\n      ),\n    );\n  }\n\n  Iterable<Widget> _buildVisibleSources(\n    BuildContext context,\n    ViewSelectorState state,\n  ) {\n    return state.visibleSources.map(\n      (e) => ViewSelectorTreeItem(\n        key: ValueKey(\n          'visible_select_sources_tree_item_${e.view.id}',\n        ),\n        viewSelectorItem: e,\n        level: 0,\n        isDescendentOfSpace: e.view.isSpace,\n        isSelectedSection: false,\n        onSelected: (item) {\n          context.read<ViewSelectorCubit>().toggleSelectedStatus(item, false);\n        },\n        height: 30.0,\n      ),\n    );\n  }\n}\n\nclass ViewSelectorTreeItem extends StatefulWidget {\n  const ViewSelectorTreeItem({\n    super.key,\n    required this.viewSelectorItem,\n    required this.level,\n    required this.isDescendentOfSpace,\n    required this.isSelectedSection,\n    required this.onSelected,\n    this.onAdd,\n    required this.height,\n    this.showSaveButton = false,\n    this.showCheckbox = true,\n  });\n\n  final ViewSelectorItem viewSelectorItem;\n\n  /// nested level of the view item\n  final int level;\n\n  final bool isDescendentOfSpace;\n\n  final bool isSelectedSection;\n\n  final void Function(ViewSelectorItem viewSelectorItem) onSelected;\n\n  final void Function(ViewSelectorItem viewSelectorItem)? onAdd;\n\n  final bool showSaveButton;\n\n  final double height;\n\n  final bool showCheckbox;\n\n  @override\n  State<ViewSelectorTreeItem> createState() => _ViewSelectorTreeItemState();\n}\n\nclass _ViewSelectorTreeItemState extends State<ViewSelectorTreeItem> {\n  @override\n  Widget build(BuildContext context) {\n    final child = SizedBox(\n      height: widget.height,\n      child: ViewSelectorTreeItemInner(\n        viewSelectorItem: widget.viewSelectorItem,\n        level: widget.level,\n        isDescendentOfSpace: widget.isDescendentOfSpace,\n        isSelectedSection: widget.isSelectedSection,\n        showCheckbox: widget.showCheckbox,\n        showSaveButton: widget.showSaveButton,\n        onSelected: widget.onSelected,\n        onAdd: widget.onAdd,\n      ),\n    );\n\n    final disabledEnabledChild = widget.viewSelectorItem.isDisabled\n        ? FlowyTooltip(\n            message: widget.showCheckbox\n                ? switch (widget.viewSelectorItem.view.layout) {\n                    ViewLayoutPB.Document =>\n                      LocaleKeys.chat_sourcesLimitReached.tr(),\n                    _ => LocaleKeys.chat_sourceUnsupported.tr(),\n                  }\n                : \"\",\n            child: Opacity(\n              opacity: 0.5,\n              child: MouseRegion(\n                cursor: SystemMouseCursors.forbidden,\n                child: IgnorePointer(child: child),\n              ),\n            ),\n          )\n        : child;\n\n    return ValueListenableBuilder(\n      valueListenable: widget.viewSelectorItem.isExpandedNotifier,\n      builder: (context, isExpanded, child) {\n        // filter the child views that should be ignored\n        final childViews = widget.viewSelectorItem.children;\n\n        if (!isExpanded || childViews.isEmpty) {\n          return disabledEnabledChild;\n        }\n\n        return Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            disabledEnabledChild,\n            ...childViews.map(\n              (childSource) => ViewSelectorTreeItem(\n                key: ValueKey(\n                  'select_sources_tree_item_${childSource.view.id}',\n                ),\n                viewSelectorItem: childSource,\n                level: widget.level + 1,\n                isDescendentOfSpace: widget.isDescendentOfSpace,\n                isSelectedSection: widget.isSelectedSection,\n                onSelected: widget.onSelected,\n                height: widget.height,\n                showCheckbox: widget.showCheckbox,\n                showSaveButton: widget.showSaveButton,\n                onAdd: widget.onAdd,\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n\nclass ViewSelectorTreeItemInner extends StatelessWidget {\n  const ViewSelectorTreeItemInner({\n    super.key,\n    required this.viewSelectorItem,\n    required this.level,\n    required this.isDescendentOfSpace,\n    required this.isSelectedSection,\n    required this.showCheckbox,\n    required this.showSaveButton,\n    this.onSelected,\n    this.onAdd,\n  });\n\n  final ViewSelectorItem viewSelectorItem;\n  final int level;\n  final bool isDescendentOfSpace;\n  final bool isSelectedSection;\n  final bool showCheckbox;\n  final bool showSaveButton;\n  final void Function(ViewSelectorItem)? onSelected;\n  final void Function(ViewSelectorItem)? onAdd;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      behavior: HitTestBehavior.translucent,\n      onTap: () => onSelected?.call(viewSelectorItem),\n      child: FlowyHover(\n        style: HoverStyle(\n          hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        ),\n        builder: (context, onHover) {\n          final theme = AppFlowyTheme.of(context);\n\n          final isSaveButtonVisible =\n              showSaveButton && !viewSelectorItem.view.isSpace;\n          final isAddButtonVisible = onAdd != null;\n          return Row(\n            children: [\n              const HSpace(4.0),\n              HSpace(max(20.0 * level - (isDescendentOfSpace ? 2 : 0), 0)),\n              // builds the >, ^ or · button\n              ToggleIsExpandedButton(\n                viewSelectorItem: viewSelectorItem,\n                isSelectedSection: isSelectedSection,\n              ),\n              const HSpace(2.0),\n              // checkbox\n              if (!viewSelectorItem.view.isSpace && showCheckbox) ...[\n                SourceSelectedStatusCheckbox(\n                  viewSelectorItem: viewSelectorItem,\n                ),\n                const HSpace(4.0),\n              ],\n              // icon\n              MentionViewIcon(\n                view: viewSelectorItem.view,\n              ),\n              const HSpace(6.0),\n              // title\n              Expanded(\n                child: Text(\n                  viewSelectorItem.view.nameOrDefault,\n                  overflow: TextOverflow.ellipsis,\n                  style: theme.textStyle.body.standard(\n                    color: theme.textColorScheme.primary,\n                  ),\n                ),\n              ),\n              if (onHover && (isSaveButtonVisible || isAddButtonVisible)) ...[\n                const HSpace(4.0),\n                if (isSaveButtonVisible)\n                  FlowyIconButton(\n                    tooltipText: LocaleKeys.chat_addToPageButton.tr(),\n                    width: 24,\n                    icon: FlowySvg(\n                      FlowySvgs.ai_add_to_page_s,\n                      size: const Size.square(16),\n                      color: Theme.of(context).hintColor,\n                    ),\n                    onPressed: () => onSelected?.call(viewSelectorItem),\n                  ),\n                if (isSaveButtonVisible && isAddButtonVisible)\n                  const HSpace(4.0),\n                if (isAddButtonVisible)\n                  FlowyIconButton(\n                    tooltipText: LocaleKeys.chat_addToNewPage.tr(),\n                    width: 24,\n                    icon: FlowySvg(\n                      FlowySvgs.add_less_padding_s,\n                      size: const Size.square(16),\n                      color: Theme.of(context).hintColor,\n                    ),\n                    onPressed: () => onAdd?.call(viewSelectorItem),\n                  ),\n                const HSpace(4.0),\n              ],\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass ToggleIsExpandedButton extends StatelessWidget {\n  const ToggleIsExpandedButton({\n    super.key,\n    required this.viewSelectorItem,\n    required this.isSelectedSection,\n  });\n\n  final ViewSelectorItem viewSelectorItem;\n  final bool isSelectedSection;\n\n  @override\n  Widget build(BuildContext context) {\n    if (isReferencedDatabaseView(\n      viewSelectorItem.view,\n      viewSelectorItem.parentView,\n    )) {\n      return const _DotIconWidget();\n    }\n\n    if (viewSelectorItem.children.isEmpty) {\n      return const SizedBox.square(dimension: 16.0);\n    }\n\n    return FlowyHover(\n      child: GestureDetector(\n        child: ValueListenableBuilder(\n          valueListenable: viewSelectorItem.isExpandedNotifier,\n          builder: (context, value, _) => FlowySvg(\n            value\n                ? FlowySvgs.view_item_expand_s\n                : FlowySvgs.view_item_unexpand_s,\n            size: const Size.square(16.0),\n          ),\n        ),\n        onTap: () => context\n            .read<ViewSelectorCubit>()\n            .toggleIsExpanded(viewSelectorItem, isSelectedSection),\n      ),\n    );\n  }\n}\n\nclass _DotIconWidget extends StatelessWidget {\n  const _DotIconWidget();\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(6.0),\n      child: Container(\n        width: 4,\n        height: 4,\n        decoration: BoxDecoration(\n          color: Theme.of(context).iconTheme.color,\n          borderRadius: BorderRadius.circular(2),\n        ),\n      ),\n    );\n  }\n}\n\nclass SourceSelectedStatusCheckbox extends StatelessWidget {\n  const SourceSelectedStatusCheckbox({\n    super.key,\n    required this.viewSelectorItem,\n  });\n\n  final ViewSelectorItem viewSelectorItem;\n\n  @override\n  Widget build(BuildContext context) {\n    return ValueListenableBuilder(\n      valueListenable: viewSelectorItem.selectedStatusNotifier,\n      builder: (context, selectedStatus, _) => FlowySvg(\n        switch (selectedStatus) {\n          ViewSelectedStatus.unselected => FlowySvgs.uncheck_s,\n          ViewSelectedStatus.selected => FlowySvgs.check_filled_s,\n          ViewSelectedStatus.partiallySelected => FlowySvgs.check_partial_s,\n        },\n        size: const Size.square(18.0),\n        blendMode: null,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/prompt_input/send_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'layout_define.dart';\n\nenum SendButtonState { enabled, streaming, disabled }\n\nclass PromptInputSendButton extends StatelessWidget {\n  const PromptInputSendButton({\n    super.key,\n    required this.state,\n    required this.onSendPressed,\n    required this.onStopStreaming,\n  });\n\n  final SendButtonState state;\n  final VoidCallback onSendPressed;\n  final VoidCallback onStopStreaming;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyIconButton(\n      width: _buttonSize,\n      richTooltipText: switch (state) {\n        SendButtonState.streaming => TextSpan(\n            children: [\n              TextSpan(\n                text: '${LocaleKeys.chat_stopTooltip.tr()}  ',\n                style: context.tooltipTextStyle(),\n              ),\n              TextSpan(\n                text: 'ESC',\n                style: context\n                    .tooltipTextStyle()\n                    ?.copyWith(color: Theme.of(context).hintColor),\n              ),\n            ],\n          ),\n        _ => null,\n      },\n      icon: switch (state) {\n        SendButtonState.enabled => FlowySvg(\n            FlowySvgs.ai_send_filled_s,\n            size: Size.square(_iconSize),\n            color: Theme.of(context).colorScheme.primary,\n          ),\n        SendButtonState.disabled => FlowySvg(\n            FlowySvgs.ai_send_filled_s,\n            size: Size.square(_iconSize),\n            color: Theme.of(context).disabledColor,\n          ),\n        SendButtonState.streaming => FlowySvg(\n            FlowySvgs.ai_stop_filled_s,\n            size: Size.square(_iconSize),\n            color: Theme.of(context).colorScheme.primary,\n          ),\n      },\n      onPressed: () {\n        switch (state) {\n          case SendButtonState.enabled:\n            onSendPressed();\n            break;\n          case SendButtonState.streaming:\n            onStopStreaming();\n            break;\n          case SendButtonState.disabled:\n            break;\n        }\n      },\n      hoverColor: Colors.transparent,\n    );\n  }\n\n  double get _buttonSize {\n    return UniversalPlatform.isMobile\n        ? MobileAIPromptSizes.sendButtonSize\n        : DesktopAIPromptSizes.actionBarSendButtonSize;\n  }\n\n  double get _iconSize {\n    return UniversalPlatform.isMobile\n        ? MobileAIPromptSizes.sendButtonSize\n        : DesktopAIPromptSizes.actionBarSendButtonIconSize;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/ai/widgets/view_selector.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/user/prelude.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/single_child_widget.dart';\n\nclass ViewSelector extends StatelessWidget {\n  const ViewSelector({\n    super.key,\n    required this.viewSelectorCubit,\n    required this.child,\n  });\n\n  final SingleChildWidget viewSelectorCubit;\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    final userWorkspaceBloc = context.read<UserWorkspaceBloc>();\n    final userProfile = userWorkspaceBloc.state.userProfile;\n    final workspaceId =\n        userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';\n\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider(\n          create: (context) {\n            return SpaceBloc(\n              userProfile: userProfile,\n              workspaceId: workspaceId,\n            )..add(const SpaceEvent.initial(openFirstPage: false));\n          },\n        ),\n        viewSelectorCubit,\n      ],\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/core/config/kv.dart",
    "content": "import 'package:shared_preferences/shared_preferences.dart';\n\nabstract class KeyValueStorage {\n  Future<void> set(String key, String value);\n  Future<String?> get(String key);\n  Future<T?> getWithFormat<T>(\n    String key,\n    T Function(String value) formatter,\n  );\n  Future<void> remove(String key);\n  Future<void> clear();\n}\n\nclass DartKeyValue implements KeyValueStorage {\n  SharedPreferences? _sharedPreferences;\n  SharedPreferences get sharedPreferences => _sharedPreferences!;\n\n  @override\n  Future<String?> get(String key) async {\n    await _initSharedPreferencesIfNeeded();\n\n    final value = sharedPreferences.getString(key);\n    if (value != null) {\n      return value;\n    }\n    return null;\n  }\n\n  @override\n  Future<T?> getWithFormat<T>(\n    String key,\n    T Function(String value) formatter,\n  ) async {\n    final value = await get(key);\n    if (value == null) {\n      return null;\n    }\n    return formatter(value);\n  }\n\n  @override\n  Future<void> remove(String key) async {\n    await _initSharedPreferencesIfNeeded();\n\n    await sharedPreferences.remove(key);\n  }\n\n  @override\n  Future<void> set(String key, String value) async {\n    await _initSharedPreferencesIfNeeded();\n\n    await sharedPreferences.setString(key, value);\n  }\n\n  @override\n  Future<void> clear() async {\n    await _initSharedPreferencesIfNeeded();\n\n    await sharedPreferences.clear();\n  }\n\n  Future<void> _initSharedPreferencesIfNeeded() async {\n    _sharedPreferences ??= await SharedPreferences.getInstance();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/core/config/kv_keys.dart",
    "content": "class KVKeys {\n  const KVKeys._();\n\n  static const String prefix = 'io.appflowy.appflowy_flutter';\n\n  /// The key for the path location of the local data for the whole app.\n  static const String pathLocation = '$prefix.path_location';\n\n  /// The key for saving the window size\n  ///\n  /// The value is a json string with the following format:\n  ///   {'height': 600.0, 'width': 800.0}\n  static const String windowSize = 'windowSize';\n\n  /// The key for saving the window position\n  ///\n  /// The value is a json string with the following format:\n  ///   {'dx': 10.0, 'dy': 10.0}\n  static const String windowPosition = 'windowPosition';\n\n  /// The key for saving the window status\n  ///\n  /// The value is a json string with the following format:\n  ///  { 'windowMaximized': true }\n  ///\n  static const String windowMaximized = 'windowMaximized';\n\n  static const String kDocumentAppearanceFontSize =\n      'kDocumentAppearanceFontSize';\n  static const String kDocumentAppearanceFontFamily =\n      'kDocumentAppearanceFontFamily';\n  static const String kDocumentAppearanceDefaultTextDirection =\n      'kDocumentAppearanceDefaultTextDirection';\n  static const String kDocumentAppearanceCursorColor =\n      'kDocumentAppearanceCursorColor';\n  static const String kDocumentAppearanceSelectionColor =\n      'kDocumentAppearanceSelectionColor';\n  static const String kDocumentAppearanceWidth = 'kDocumentAppearanceWidth';\n\n  /// The key for saving the expanded views\n  ///\n  /// The value is a json string with the following format:\n  ///  {'viewId': true, 'viewId2': false}\n  static const String expandedViews = 'expandedViews';\n\n  /// The key for saving the expanded folder\n  ///\n  /// The value is a json string with the following format:\n  ///  {'SidebarFolderCategoryType.value': true}\n  static const String expandedFolders = 'expandedFolders';\n\n  /// @deprecated in version 0.7.6\n  /// The key for saving if showing the rename dialog when creating a new file\n  ///\n  /// The value is a boolean string.\n  static const String showRenameDialogWhenCreatingNewFile =\n      'showRenameDialogWhenCreatingNewFile';\n\n  static const String kCloudType = 'kCloudType';\n  static const String kAppflowyCloudBaseURL = 'kAppFlowyCloudBaseURL';\n  static const String kAppFlowyBaseShareDomain = 'kAppFlowyBaseShareDomain';\n  static const String kAppFlowyEnableSyncTrace = 'kAppFlowyEnableSyncTrace';\n\n  /// The key for saving the text scale factor.\n  ///\n  /// The value is a double string.\n  /// The value range is from 0.8 to 1.0. If it's greater than 1.0, it will cause\n  ///   the text to be too large and not aligned with the icon\n  static const String textScaleFactor = 'textScaleFactor';\n\n  /// The key for saving the feature flags\n  ///\n  /// The value is a json string with the following format:\n  /// {'feature_flag_1': true, 'feature_flag_2': false}\n  static const String featureFlag = 'featureFlag';\n\n  /// The key for saving show notification icon option\n  ///\n  /// The value is a boolean string\n  static const String showNotificationIcon = 'showNotificationIcon';\n\n  /// The key for saving the last opened workspace id\n  ///\n  /// The workspace id is a string.\n  @Deprecated('deprecated in version 0.5.5')\n  static const String lastOpenedWorkspaceId = 'lastOpenedWorkspaceId';\n\n  /// The key for saving the scale factor\n  ///\n  /// The value is a double string.\n  static const String scaleFactor = 'scaleFactor';\n\n  /// The key for saving the last opened tab (favorite, recent, space etc.)\n  ///\n  /// The value is a int string.\n  static const String lastOpenedSpace = 'lastOpenedSpace';\n\n  /// The key for saving the space tab order\n  ///\n  /// The value is a json string with the following format:\n  /// [0, 1, 2]\n  static const String spaceOrder = 'spaceOrder';\n\n  /// The key for saving the last opened space id (space A, space B)\n  ///\n  /// The value is a string.\n  static const String lastOpenedSpaceId = 'lastOpenedSpaceId';\n\n  /// The key for saving the upgrade space tag\n  ///\n  /// The value is a boolean string\n  static const String hasUpgradedSpace = 'hasUpgradedSpace060';\n\n  /// The key for saving the recent icons\n  ///\n  /// The value is a json string of [RecentIcons]\n  static const String recentIcons = 'kRecentIcons';\n\n  /// The key for saving compact mode ids for node or databse view\n  ///\n  /// The value is a json list of id\n  static const String compactModeIds = 'compactModeIds';\n\n  /// v0.9.4: has the user clicked the upgrade to pro button\n  /// The value is a boolean string\n  static const String hasClickedUpgradeToProButton =\n      'hasClickedUpgradeToProButton';\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/core/frameless_window.dart",
    "content": "import 'package:appflowy/startup/tasks/device_info_task.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass CocoaWindowChannel {\n  CocoaWindowChannel._();\n\n  final MethodChannel _channel = const MethodChannel(\"flutter/cocoaWindow\");\n\n  static final CocoaWindowChannel instance = CocoaWindowChannel._();\n\n  Future<void> setWindowPosition(Offset offset) async {\n    await _channel.invokeMethod(\"setWindowPosition\", [offset.dx, offset.dy]);\n  }\n\n  Future<List<double>> getWindowPosition() async {\n    final raw = await _channel.invokeMethod(\"getWindowPosition\");\n    final arr = raw as List<dynamic>;\n    final List<double> result = arr.map((s) => s as double).toList();\n    return result;\n  }\n\n  Future<void> zoom() async {\n    await _channel.invokeMethod(\"zoom\");\n  }\n}\n\nclass MoveWindowDetector extends StatefulWidget {\n  const MoveWindowDetector({\n    super.key,\n    this.child,\n  });\n\n  final Widget? child;\n\n  @override\n  MoveWindowDetectorState createState() => MoveWindowDetectorState();\n}\n\nclass MoveWindowDetectorState extends State<MoveWindowDetector> {\n  double winX = 0;\n  double winY = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    // the frameless window is only supported on macOS\n    if (!UniversalPlatform.isMacOS) {\n      return widget.child ?? const SizedBox.shrink();\n    }\n\n    // For the macOS version 15 or higher, we can control the window position by using system APIs\n    if (ApplicationInfo.macOSMajorVersion != null &&\n        ApplicationInfo.macOSMajorVersion! >= 15) {\n      return widget.child ?? const SizedBox.shrink();\n    }\n\n    return GestureDetector(\n      // https://stackoverflow.com/questions/52965799/flutter-gesturedetector-not-working-with-containers-in-stack\n      behavior: HitTestBehavior.translucent,\n      onDoubleTap: () async => CocoaWindowChannel.instance.zoom(),\n      onPanStart: (DragStartDetails details) {\n        winX = details.globalPosition.dx;\n        winY = details.globalPosition.dy;\n      },\n      onPanUpdate: (DragUpdateDetails details) async {\n        final windowPos = await CocoaWindowChannel.instance.getWindowPosition();\n        final double dx = windowPos[0];\n        final double dy = windowPos[1];\n        final deltaX = details.globalPosition.dx - winX;\n        final deltaY = details.globalPosition.dy - winY;\n        await CocoaWindowChannel.instance\n            .setWindowPosition(Offset(dx + deltaX, dy - deltaY));\n      },\n      child: widget.child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/core/helpers/helpers.dart",
    "content": "export 'target_platform.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/core/helpers/target_platform.dart",
    "content": "import 'package:flutter/foundation.dart' show TargetPlatform, kIsWeb;\n\nextension TargetPlatformHelper on TargetPlatform {\n  /// Convenience function to check if the app is running on a desktop computer.\n  ///\n  /// Easily check if on desktop by checking `defaultTargetPlatform.isDesktop`.\n  bool get isDesktop =>\n      !kIsWeb &&\n      (this == TargetPlatform.linux ||\n          this == TargetPlatform.macOS ||\n          this == TargetPlatform.windows);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:appflowy/workspace/presentation/home/toast.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:open_filex/open_filex.dart';\nimport 'package:string_validator/string_validator.dart';\nimport 'package:universal_platform/universal_platform.dart';\nimport 'package:url_launcher/url_launcher.dart' as launcher;\n\ntypedef OnFailureCallback = void Function(Uri uri);\n\n/// Launch the uri\n///\n/// If the uri is a local file path, it will be opened with the OpenFilex.\n/// Otherwise, it will be launched with the url_launcher.\nFuture<bool> afLaunchUri(\n  Uri uri, {\n  BuildContext? context,\n  OnFailureCallback? onFailure,\n  launcher.LaunchMode mode = launcher.LaunchMode.platformDefault,\n  String? webOnlyWindowName,\n  bool addingHttpSchemeWhenFailed = false,\n}) async {\n  final url = uri.toString();\n  final decodedUrl = Uri.decodeComponent(url);\n\n  // check if the uri is the local file path\n  if (localPathRegex.hasMatch(decodedUrl)) {\n    return _afLaunchLocalUri(\n      uri,\n      context: context,\n      onFailure: onFailure,\n    );\n  }\n\n  // on Linux or Android or Windows, add http scheme to the url if it is not present\n  if ((UniversalPlatform.isLinux ||\n          UniversalPlatform.isAndroid ||\n          UniversalPlatform.isWindows) &&\n      !isURL(url, {'require_protocol': true})) {\n    uri = Uri.parse('https://$url');\n  }\n\n  /// opening an incorrect link will cause a system error dialog to pop up on macOS\n  /// only use [canLaunchUrl] on macOS\n  /// and there is an known issue with url_launcher on Linux where it fails to launch\n  /// see https://github.com/flutter/flutter/issues/88463\n  bool result = true;\n  if (UniversalPlatform.isMacOS) {\n    result = await launcher.canLaunchUrl(uri);\n  }\n\n  if (result) {\n    try {\n      // try to launch the uri directly\n      result = await launcher.launchUrl(\n        uri,\n        mode: mode,\n        webOnlyWindowName: webOnlyWindowName,\n      );\n    } on PlatformException catch (e) {\n      Log.error('Failed to open uri: $e');\n      return false;\n    }\n  }\n\n  // if the uri is not a valid url, try to launch it with http scheme\n\n  if (addingHttpSchemeWhenFailed &&\n      !result &&\n      !isURL(url, {'require_protocol': true})) {\n    try {\n      final uriWithScheme = Uri.parse('http://$url');\n      result = await launcher.launchUrl(\n        uriWithScheme,\n        mode: mode,\n        webOnlyWindowName: webOnlyWindowName,\n      );\n    } on PlatformException catch (e) {\n      Log.error('Failed to open uri: $e');\n      if (context != null && context.mounted) {\n        _errorHandler(uri, context: context, onFailure: onFailure, e: e);\n      }\n    }\n  }\n\n  return result;\n}\n\n/// Launch the url string\n///\n/// See [afLaunchUri] for more details.\nFuture<bool> afLaunchUrlString(\n  String url, {\n  bool addingHttpSchemeWhenFailed = false,\n  BuildContext? context,\n  OnFailureCallback? onFailure,\n}) async {\n  final Uri uri;\n  try {\n    uri = Uri.parse(url);\n  } on FormatException catch (e) {\n    Log.error('Failed to parse url: $e');\n    return false;\n  }\n\n  // try to launch the uri directly\n  return afLaunchUri(\n    uri,\n    addingHttpSchemeWhenFailed: addingHttpSchemeWhenFailed,\n    context: context,\n    onFailure: onFailure,\n  );\n}\n\n/// Launch the local uri\n///\n/// See [afLaunchUri] for more details.\nFuture<bool> _afLaunchLocalUri(\n  Uri uri, {\n  BuildContext? context,\n  OnFailureCallback? onFailure,\n}) async {\n  final decodedUrl = Uri.decodeComponent(uri.toString());\n  // open the file with the OpenfileX\n  var result = await OpenFilex.open(decodedUrl);\n  if (result.type != ResultType.done) {\n    // For the file cant be opened, fallback to open the folder\n    final parentFolder = Directory(decodedUrl).parent.path;\n    result = await OpenFilex.open(parentFolder);\n  }\n  // show the toast if the file is not found\n  final message = switch (result.type) {\n    ResultType.done => LocaleKeys.openFileMessage_success.tr(),\n    ResultType.fileNotFound => LocaleKeys.openFileMessage_fileNotFound.tr(),\n    ResultType.noAppToOpen => LocaleKeys.openFileMessage_noAppToOpenFile.tr(),\n    ResultType.permissionDenied =>\n      LocaleKeys.openFileMessage_permissionDenied.tr(),\n    ResultType.error => LocaleKeys.failedToOpenUrl.tr(),\n  };\n  if (context != null && context.mounted) {\n    showToastNotification(\n      message: message,\n      type: result.type == ResultType.done\n          ? ToastificationType.success\n          : ToastificationType.error,\n    );\n  }\n  final openFileSuccess = result.type == ResultType.done;\n  if (!openFileSuccess && onFailure != null) {\n    onFailure(uri);\n    Log.error('Failed to open file: $result.message');\n  }\n  return openFileSuccess;\n}\n\nvoid _errorHandler(\n  Uri uri, {\n  BuildContext? context,\n  OnFailureCallback? onFailure,\n  PlatformException? e,\n}) {\n  Log.error('Failed to open uri: $e');\n\n  if (onFailure != null) {\n    onFailure(uri);\n  } else {\n    showMessageToast(\n      LocaleKeys.failedToOpenUrl.tr(args: [e?.message ?? \"PlatformException\"]),\n      context: context,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/core/network_monitor.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';\nimport 'package:connectivity_plus/connectivity_plus.dart';\nimport 'package:flutter/services.dart';\n\nclass NetworkListener {\n  NetworkListener() {\n    _connectivitySubscription =\n        _connectivity.onConnectivityChanged.listen(_updateConnectionStatus);\n  }\n\n  final Connectivity _connectivity = Connectivity();\n  late StreamSubscription<ConnectivityResult> _connectivitySubscription;\n\n  Future<void> start() async {\n    late ConnectivityResult result;\n    // Platform messages may fail, so we use a try/catch PlatformException.\n    try {\n      result = await _connectivity.checkConnectivity();\n    } on PlatformException catch (e) {\n      Log.error(\"Couldn't check connectivity status. $e\");\n      return;\n    }\n    return _updateConnectionStatus(result);\n  }\n\n  Future<void> stop() async {\n    await _connectivitySubscription.cancel();\n  }\n\n  Future<void> _updateConnectionStatus(ConnectivityResult result) async {\n    final networkType = () {\n      switch (result) {\n        case ConnectivityResult.wifi:\n          return NetworkTypePB.Wifi;\n        case ConnectivityResult.ethernet:\n          return NetworkTypePB.Ethernet;\n        case ConnectivityResult.mobile:\n          return NetworkTypePB.Cell;\n        case ConnectivityResult.bluetooth:\n          return NetworkTypePB.Bluetooth;\n        case ConnectivityResult.vpn:\n          return NetworkTypePB.VPN;\n        case ConnectivityResult.none:\n        case ConnectivityResult.other:\n          return NetworkTypePB.NetworkUnknown;\n      }\n    }();\n    final state = NetworkStatePB.create()..ty = networkType;\n    return UserEventUpdateNetworkState(state).send().then((result) {\n      result.fold((l) {}, (e) => Log.error(e));\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/core/notification/document_notification.dart",
    "content": "import 'package:appflowy/core/notification/notification_helper.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\n\n// This value should be the same as the DOCUMENT_OBSERVABLE_SOURCE value\nconst String _source = 'Document';\n\nclass DocumentNotificationParser\n    extends NotificationParser<DocumentNotification, FlowyError> {\n  DocumentNotificationParser({\n    super.id,\n    required super.callback,\n  }) : super(\n          tyParser: (ty, source) =>\n              source == _source ? DocumentNotification.valueOf(ty) : null,\n          errorParser: (bytes) => FlowyError.fromBuffer(bytes),\n        );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/core/notification/folder_notification.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nimport 'notification_helper.dart';\n\n// This value should be the same as the FOLDER_OBSERVABLE_SOURCE value\nconst String _source = 'Workspace';\n\nclass FolderNotificationParser\n    extends NotificationParser<FolderNotification, FlowyError> {\n  FolderNotificationParser({\n    super.id,\n    required super.callback,\n  }) : super(\n          tyParser: (ty, source) =>\n              source == _source ? FolderNotification.valueOf(ty) : null,\n          errorParser: (bytes) => FlowyError.fromBuffer(bytes),\n        );\n}\n\ntypedef FolderNotificationHandler = Function(\n  FolderNotification ty,\n  FlowyResult<Uint8List, FlowyError> result,\n);\n\nclass FolderNotificationListener {\n  FolderNotificationListener({\n    required String objectId,\n    required FolderNotificationHandler handler,\n  }) : _parser = FolderNotificationParser(\n          id: objectId,\n          callback: handler,\n        ) {\n    _subscription =\n        RustStreamReceiver.listen((observable) => _parser?.parse(observable));\n  }\n\n  FolderNotificationParser? _parser;\n  StreamSubscription<SubscribeObject>? _subscription;\n\n  Future<void> stop() async {\n    _parser = null;\n    await _subscription?.cancel();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/core/notification/grid_notification.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nimport 'notification_helper.dart';\n\n// This value should be the same as the DATABASE_OBSERVABLE_SOURCE value\nconst String _source = 'Database';\n\nclass DatabaseNotificationParser\n    extends NotificationParser<DatabaseNotification, FlowyError> {\n  DatabaseNotificationParser({\n    super.id,\n    required super.callback,\n  }) : super(\n          tyParser: (ty, source) =>\n              source == _source ? DatabaseNotification.valueOf(ty) : null,\n          errorParser: (bytes) => FlowyError.fromBuffer(bytes),\n        );\n}\n\ntypedef DatabaseNotificationHandler = Function(\n  DatabaseNotification ty,\n  FlowyResult<Uint8List, FlowyError> result,\n);\n\nclass DatabaseNotificationListener {\n  DatabaseNotificationListener({\n    required String objectId,\n    required DatabaseNotificationHandler handler,\n  }) : _parser = DatabaseNotificationParser(id: objectId, callback: handler) {\n    _subscription =\n        RustStreamReceiver.listen((observable) => _parser?.parse(observable));\n  }\n\n  DatabaseNotificationParser? _parser;\n  StreamSubscription<SubscribeObject>? _subscription;\n\n  Future<void> stop() async {\n    _parser = null;\n    await _subscription?.cancel();\n    _subscription = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/core/notification/notification_helper.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass NotificationParser<T, E extends Object> {\n  NotificationParser({\n    this.id,\n    required this.callback,\n    required this.errorParser,\n    required this.tyParser,\n  });\n\n  String? id;\n  void Function(T, FlowyResult<Uint8List, E>) callback;\n  E Function(Uint8List) errorParser;\n  T? Function(int, String) tyParser;\n\n  void parse(SubscribeObject subject) {\n    if (id != null) {\n      if (subject.id != id) {\n        return;\n      }\n    }\n\n    final ty = tyParser(subject.ty, subject.source);\n    if (ty == null) {\n      return;\n    }\n\n    if (subject.hasError()) {\n      final bytes = Uint8List.fromList(subject.error);\n      final error = errorParser(bytes);\n      callback(ty, FlowyResult.failure(error));\n    } else {\n      final bytes = Uint8List.fromList(subject.payload);\n      callback(ty, FlowyResult.success(bytes));\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/core/notification/search_notification.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/foundation.dart';\n\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-search/notification.pbenum.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nimport 'notification_helper.dart';\n\n// This value must be identical to the value in the backend (SEARCH_OBSERVABLE_SOURCE)\nconst _source = 'Search';\n\nclass SearchNotificationParser\n    extends NotificationParser<SearchNotification, FlowyError> {\n  SearchNotificationParser({\n    super.id,\n    required super.callback,\n    String? channel,\n  }) : super(\n          tyParser: (ty, source) => source == \"$_source$channel\"\n              ? SearchNotification.valueOf(ty)\n              : null,\n          errorParser: (bytes) => FlowyError.fromBuffer(bytes),\n        );\n}\n\ntypedef SearchNotificationHandler = Function(\n  SearchNotification ty,\n  FlowyResult<Uint8List, FlowyError> result,\n);\n\nclass SearchNotificationListener {\n  SearchNotificationListener({\n    required String objectId,\n    required SearchNotificationHandler handler,\n    String? channel,\n  }) : _parser = SearchNotificationParser(\n          id: objectId,\n          callback: handler,\n          channel: channel,\n        ) {\n    _subscription =\n        RustStreamReceiver.listen((observable) => _parser?.parse(observable));\n  }\n\n  StreamSubscription<SubscribeObject>? _subscription;\n  SearchNotificationParser? _parser;\n\n  Future<void> stop() async {\n    _parser = null;\n    await _subscription?.cancel();\n    _subscription = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/core/notification/user_notification.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\n\nimport 'notification_helper.dart';\n\n// This value should be the same as the USER_OBSERVABLE_SOURCE value\nconst String _source = 'User';\n\nclass UserNotificationParser\n    extends NotificationParser<UserNotification, FlowyError> {\n  UserNotificationParser({\n    required String super.id,\n    required super.callback,\n  }) : super(\n          tyParser: (ty, source) =>\n              source == _source ? UserNotification.valueOf(ty) : null,\n          errorParser: (bytes) => FlowyError.fromBuffer(bytes),\n        );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/date/date_service.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-date/entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass DateService {\n  static Future<FlowyResult<DateTime, FlowyError>> queryDate(\n    String search,\n  ) async {\n    final query = DateQueryPB.create()..query = search;\n    final result = await DateEventQueryDate(query).send();\n    return result.fold(\n      (s) {\n        final date = DateTime.tryParse(s.date);\n        if (date != null) {\n          return FlowyResult.success(date);\n        }\n        return FlowyResult.failure(\n          FlowyError(msg: 'Could not parse Date (NLP) from String'),\n        );\n      },\n      (e) => FlowyResult.failure(e),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/env/backend_env.dart",
    "content": "// ignore_for_file: non_constant_identifier_names\n\nimport 'package:appflowy/plugins/shared/share/constants.dart';\nimport 'package:json_annotation/json_annotation.dart';\n\npart 'backend_env.g.dart';\n\n@JsonSerializable()\nclass AppFlowyConfiguration {\n  AppFlowyConfiguration({\n    required this.root,\n    required this.app_version,\n    required this.custom_app_path,\n    required this.origin_app_path,\n    required this.device_id,\n    required this.platform,\n    required this.authenticator_type,\n    required this.appflowy_cloud_config,\n    required this.envs,\n  });\n\n  factory AppFlowyConfiguration.fromJson(Map<String, dynamic> json) =>\n      _$AppFlowyConfigurationFromJson(json);\n\n  final String root;\n  final String app_version;\n  final String custom_app_path;\n  final String origin_app_path;\n  final String device_id;\n  final String platform;\n  final int authenticator_type;\n  final AppFlowyCloudConfiguration appflowy_cloud_config;\n  final Map<String, String> envs;\n\n  Map<String, dynamic> toJson() => _$AppFlowyConfigurationToJson(this);\n}\n\n@JsonSerializable()\nclass AppFlowyCloudConfiguration {\n  AppFlowyCloudConfiguration({\n    required this.base_url,\n    required this.ws_base_url,\n    required this.gotrue_url,\n    required this.enable_sync_trace,\n    required this.base_web_domain,\n  });\n\n  factory AppFlowyCloudConfiguration.fromJson(Map<String, dynamic> json) =>\n      _$AppFlowyCloudConfigurationFromJson(json);\n\n  final String base_url;\n  final String ws_base_url;\n  final String gotrue_url;\n  final bool enable_sync_trace;\n\n  /// The base domain is used in\n  ///\n  /// - Share URL\n  /// - Publish URL\n  /// - Copy Link To Block\n  final String base_web_domain;\n\n  Map<String, dynamic> toJson() => _$AppFlowyCloudConfigurationToJson(this);\n\n  static AppFlowyCloudConfiguration defaultConfig() {\n    return AppFlowyCloudConfiguration(\n      base_url: '',\n      ws_base_url: '',\n      gotrue_url: '',\n      enable_sync_trace: false,\n      base_web_domain: ShareConstants.defaultBaseWebDomain,\n    );\n  }\n\n  bool get isValid {\n    return base_url.isNotEmpty &&\n        ws_base_url.isNotEmpty &&\n        gotrue_url.isNotEmpty;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/env/cloud_env.dart",
    "content": "import 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/env/backend_env.dart';\nimport 'package:appflowy/env/env.dart';\nimport 'package:appflowy/plugins/shared/share/constants.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/log.dart';\n\n/// Sets the cloud type for the application.\n///\n/// This method updates the cloud type setting in the key-value storage\n/// using the [KeyValueStorage] service. The cloud type is identified\n/// by the [AuthenticatorType] enum.\n///\n/// [ty] - The type of cloud to be set. It must be one of the values from\n/// [AuthenticatorType] enum. The corresponding integer value of the enum is stored:\n/// - `CloudType.local` is stored as \"0\".\n/// - `CloudType.appflowyCloud` is stored as \"2\".\n///\n/// The gap between [AuthenticatorType.local] and [AuthenticatorType.appflowyCloud] is\n/// due to previously supporting Supabase, this has been deprecated since and removed.\n/// To not cause conflicts with older clients, we keep the gap.\n///\nFuture<void> _setAuthenticatorType(AuthenticatorType ty) async {\n  switch (ty) {\n    case AuthenticatorType.local:\n      await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 0.toString());\n      break;\n    case AuthenticatorType.appflowyCloud:\n      await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 2.toString());\n      break;\n    case AuthenticatorType.appflowyCloudSelfHost:\n      await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 3.toString());\n      break;\n    case AuthenticatorType.appflowyCloudDevelop:\n      await getIt<KeyValueStorage>().set(KVKeys.kCloudType, 4.toString());\n      break;\n  }\n}\n\nconst String kAppflowyCloudUrl = \"https://beta.appflowy.cloud\";\n\n/// Retrieves the currently set cloud type.\n///\n/// This method fetches the cloud type setting from the key-value storage\n/// using the [KeyValueStorage] service and returns the corresponding\n/// [AuthenticatorType] enum value.\n///\n/// Returns:\n/// A Future that resolves to a [AuthenticatorType] enum value representing the\n/// currently set cloud type. The default return value is `CloudType.local`\n/// if no valid setting is found.\n///\nFuture<AuthenticatorType> getAuthenticatorType() async {\n  final value = await getIt<KeyValueStorage>().get(KVKeys.kCloudType);\n  if (value == null && !integrationMode().isUnitTest) {\n    // if the cloud type is not set, then set it to AppFlowy Cloud as default.\n    await useAppFlowyBetaCloudWithURL(\n      kAppflowyCloudUrl,\n      AuthenticatorType.appflowyCloud,\n    );\n    return AuthenticatorType.appflowyCloud;\n  }\n\n  switch (value ?? \"0\") {\n    case \"0\":\n      return AuthenticatorType.local;\n    case \"2\":\n      return AuthenticatorType.appflowyCloud;\n    case \"3\":\n      return AuthenticatorType.appflowyCloudSelfHost;\n    case \"4\":\n      return AuthenticatorType.appflowyCloudDevelop;\n    default:\n      await useAppFlowyBetaCloudWithURL(\n        kAppflowyCloudUrl,\n        AuthenticatorType.appflowyCloud,\n      );\n      return AuthenticatorType.appflowyCloud;\n  }\n}\n\n/// Determines whether authentication is enabled.\n///\n/// This getter evaluates if authentication should be enabled based on the\n/// current integration mode and cloud type settings.\n///\n/// Returns:\n/// A boolean value indicating whether authentication is enabled. It returns\n/// `true` if the application is in release or develop mode, and the cloud type\n/// is not set to `CloudType.local`. Additionally, it checks if either the\n/// AppFlowy Cloud configuration is valid.\n/// Returns `false` otherwise.\nbool get isAuthEnabled {\n  final env = getIt<AppFlowyCloudSharedEnv>();\n  if (env.authenticatorType.isAppFlowyCloudEnabled) {\n    return env.appflowyCloudConfig.isValid;\n  }\n\n  return false;\n}\n\nbool get isLocalAuthEnabled {\n  return currentCloudType().isLocal;\n}\n\n/// Determines if AppFlowy Cloud is enabled.\nbool get isAppFlowyCloudEnabled {\n  return currentCloudType().isAppFlowyCloudEnabled;\n}\n\nenum AuthenticatorType {\n  local,\n  appflowyCloud,\n  appflowyCloudSelfHost,\n  // The 'appflowyCloudDevelop' type is used for develop purposes only.\n  appflowyCloudDevelop;\n\n  bool get isLocal => this == AuthenticatorType.local;\n\n  bool get isAppFlowyCloudEnabled =>\n      this == AuthenticatorType.appflowyCloudSelfHost ||\n      this == AuthenticatorType.appflowyCloudDevelop ||\n      this == AuthenticatorType.appflowyCloud;\n\n  int get value {\n    switch (this) {\n      case AuthenticatorType.local:\n        return 0;\n      case AuthenticatorType.appflowyCloud:\n        return 2;\n      case AuthenticatorType.appflowyCloudSelfHost:\n        return 3;\n      case AuthenticatorType.appflowyCloudDevelop:\n        return 4;\n    }\n  }\n\n  static AuthenticatorType fromValue(int value) {\n    switch (value) {\n      case 0:\n        return AuthenticatorType.local;\n      case 2:\n        return AuthenticatorType.appflowyCloud;\n      case 3:\n        return AuthenticatorType.appflowyCloudSelfHost;\n      case 4:\n        return AuthenticatorType.appflowyCloudDevelop;\n      default:\n        return AuthenticatorType.local;\n    }\n  }\n}\n\nAuthenticatorType currentCloudType() {\n  return getIt<AppFlowyCloudSharedEnv>().authenticatorType;\n}\n\nFuture<void> _setAppFlowyCloudUrl(String? url) async {\n  await getIt<KeyValueStorage>().set(KVKeys.kAppflowyCloudBaseURL, url ?? '');\n}\n\nFuture<void> useBaseWebDomain(String? url) async {\n  await getIt<KeyValueStorage>().set(\n    KVKeys.kAppFlowyBaseShareDomain,\n    url ?? ShareConstants.defaultBaseWebDomain,\n  );\n}\n\nFuture<void> useSelfHostedAppFlowyCloud(String url) async {\n  await _setAuthenticatorType(AuthenticatorType.appflowyCloudSelfHost);\n  await _setAppFlowyCloudUrl(url);\n}\n\nFuture<void> useAppFlowyCloudDevelop(String url) async {\n  await _setAuthenticatorType(AuthenticatorType.appflowyCloudDevelop);\n  await _setAppFlowyCloudUrl(url);\n}\n\nFuture<void> useAppFlowyBetaCloudWithURL(\n  String url,\n  AuthenticatorType authenticatorType,\n) async {\n  await _setAuthenticatorType(authenticatorType);\n  await _setAppFlowyCloudUrl(url);\n}\n\nFuture<void> useLocalServer() async {\n  await _setAuthenticatorType(AuthenticatorType.local);\n}\n\n// Use getIt<AppFlowyCloudSharedEnv>() to get the shared environment.\nclass AppFlowyCloudSharedEnv {\n  AppFlowyCloudSharedEnv({\n    required AuthenticatorType authenticatorType,\n    required this.appflowyCloudConfig,\n  }) : _authenticatorType = authenticatorType;\n\n  final AuthenticatorType _authenticatorType;\n  final AppFlowyCloudConfiguration appflowyCloudConfig;\n\n  AuthenticatorType get authenticatorType => _authenticatorType;\n\n  static Future<AppFlowyCloudSharedEnv> fromEnv() async {\n    // If [Env.enableCustomCloud] is true, then use the custom cloud configuration.\n    if (Env.enableCustomCloud) {\n      // Use the custom cloud configuration.\n      var authenticatorType = await getAuthenticatorType();\n\n      final appflowyCloudConfig = authenticatorType.isAppFlowyCloudEnabled\n          ? await getAppFlowyCloudConfig(authenticatorType)\n          : AppFlowyCloudConfiguration.defaultConfig();\n\n      // In the backend, the value '2' represents the use of AppFlowy Cloud. However, in the frontend,\n      // we distinguish between [AuthenticatorType.appflowyCloudSelfHost] and [AuthenticatorType.appflowyCloud].\n      // When the cloud type is [AuthenticatorType.appflowyCloudSelfHost] in the frontend, it should be\n      // converted to [AuthenticatorType.appflowyCloud] to align with the backend representation,\n      // where both types are indicated by the value '2'.\n      if (authenticatorType.isAppFlowyCloudEnabled) {\n        authenticatorType = AuthenticatorType.appflowyCloud;\n      }\n      return AppFlowyCloudSharedEnv(\n        authenticatorType: authenticatorType,\n        appflowyCloudConfig: appflowyCloudConfig,\n      );\n    } else {\n      // Using the cloud settings from the .env file.\n      final appflowyCloudConfig = AppFlowyCloudConfiguration(\n        base_url: Env.afCloudUrl,\n        ws_base_url: await _getAppFlowyCloudWSUrl(Env.afCloudUrl),\n        gotrue_url: await _getAppFlowyCloudGotrueUrl(Env.afCloudUrl),\n        enable_sync_trace: false,\n        base_web_domain: Env.baseWebDomain,\n      );\n\n      return AppFlowyCloudSharedEnv(\n        authenticatorType: AuthenticatorType.fromValue(Env.authenticatorType),\n        appflowyCloudConfig: appflowyCloudConfig,\n      );\n    }\n  }\n\n  @override\n  String toString() {\n    return 'authenticator: $_authenticatorType\\n'\n        'appflowy: ${appflowyCloudConfig.toJson()}\\n';\n  }\n}\n\nFuture<AppFlowyCloudConfiguration> configurationFromUri(\n  Uri baseUri,\n  String baseUrl,\n  AuthenticatorType authenticatorType,\n  String baseShareDomain,\n) async {\n  // In development mode, the app is configured to access the AppFlowy cloud server directly through specific ports.\n  // This setup bypasses the need for Nginx, meaning that the AppFlowy cloud should be running without an Nginx server\n  // in the development environment.\n  // If you modify following code, please update the corresponding documentation in the appflowy billing.\n  if (authenticatorType == AuthenticatorType.appflowyCloudDevelop) {\n    return AppFlowyCloudConfiguration(\n      base_url: \"$baseUrl:8000\",\n      ws_base_url: \"ws://${baseUri.host}:8000/ws/v1\",\n      gotrue_url: \"$baseUrl:9999\",\n      enable_sync_trace: true,\n      base_web_domain: ShareConstants.testBaseWebDomain,\n    );\n  } else {\n    return AppFlowyCloudConfiguration(\n      base_url: baseUrl,\n      ws_base_url: await _getAppFlowyCloudWSUrl(baseUrl),\n      gotrue_url: await _getAppFlowyCloudGotrueUrl(baseUrl),\n      enable_sync_trace: await getSyncLogEnabled(),\n      base_web_domain: authenticatorType == AuthenticatorType.appflowyCloud\n          ? ShareConstants.defaultBaseWebDomain\n          : baseShareDomain,\n    );\n  }\n}\n\nFuture<AppFlowyCloudConfiguration> getAppFlowyCloudConfig(\n  AuthenticatorType authenticatorType,\n) async {\n  final baseURL = await getAppFlowyCloudUrl();\n  final baseShareDomain = await getAppFlowyShareDomain();\n\n  try {\n    final uri = Uri.parse(baseURL);\n    return await configurationFromUri(\n      uri,\n      baseURL,\n      authenticatorType,\n      baseShareDomain,\n    );\n  } catch (e) {\n    Log.error(\"Failed to parse AppFlowy Cloud URL: $e\");\n    return AppFlowyCloudConfiguration.defaultConfig();\n  }\n}\n\nFuture<String> getAppFlowyCloudUrl() async {\n  final result =\n      await getIt<KeyValueStorage>().get(KVKeys.kAppflowyCloudBaseURL);\n  return result ?? kAppflowyCloudUrl;\n}\n\nFuture<String> getAppFlowyShareDomain() async {\n  final result =\n      await getIt<KeyValueStorage>().get(KVKeys.kAppFlowyBaseShareDomain);\n  return result ?? ShareConstants.defaultBaseWebDomain;\n}\n\nFuture<bool> getSyncLogEnabled() async {\n  final result =\n      await getIt<KeyValueStorage>().get(KVKeys.kAppFlowyEnableSyncTrace);\n\n  if (result == null) {\n    return false;\n  }\n\n  return result.toLowerCase() == \"true\";\n}\n\nFuture<void> setSyncLogEnabled(bool enable) async {\n  await getIt<KeyValueStorage>().set(\n    KVKeys.kAppFlowyEnableSyncTrace,\n    enable.toString().toLowerCase(),\n  );\n}\n\nFuture<String> _getAppFlowyCloudWSUrl(String baseURL) async {\n  try {\n    final uri = Uri.parse(baseURL);\n\n    // Construct the WebSocket URL directly from the parsed URI.\n    final wsScheme = uri.isScheme('HTTPS') ? 'wss' : 'ws';\n    final wsUrl =\n        Uri(scheme: wsScheme, host: uri.host, port: uri.port, path: '/ws/v1');\n\n    return wsUrl.toString();\n  } catch (e) {\n    Log.error(\"Failed to get WebSocket URL: $e\");\n    return \"\";\n  }\n}\n\nFuture<String> _getAppFlowyCloudGotrueUrl(String baseURL) async {\n  return \"$baseURL/gotrue\";\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/env/cloud_env_test.dart",
    "content": "// lib/env/env.dart\n// ignore_for_file: prefer_const_declarations\n\nimport 'package:envied/envied.dart';\n\npart 'cloud_env_test.g.dart';\n\n/// Follow the guide on https://supabase.com/docs/guides/auth/social-login/auth-google to setup the auth provider.\n///\n@Envied(path: '.env.cloud.test')\nabstract class TestEnv {\n  /// AppFlowy Cloud Configuration\n  @EnviedField(\n    obfuscate: false,\n    varName: 'APPFLOWY_CLOUD_URL',\n    defaultValue: 'http://localhost',\n  )\n  static final String afCloudUrl = _TestEnv.afCloudUrl;\n\n  // Supabase Configuration:\n  @EnviedField(\n    obfuscate: false,\n    varName: 'SUPABASE_URL',\n    defaultValue: '',\n  )\n  static final String supabaseUrl = _TestEnv.supabaseUrl;\n  @EnviedField(\n    obfuscate: false,\n    varName: 'SUPABASE_ANON_KEY',\n    defaultValue: '',\n  )\n  static final String supabaseAnonKey = _TestEnv.supabaseAnonKey;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/env/env.dart",
    "content": "// lib/env/env.dart\nimport 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/plugins/shared/share/constants.dart';\nimport 'package:envied/envied.dart';\n\npart 'env.g.dart';\n\n@Envied(path: '.env')\nabstract class Env {\n  // 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.\n  static bool get enableCustomCloud {\n    return Env.authenticatorType ==\n            AuthenticatorType.appflowyCloudSelfHost.value ||\n        Env.authenticatorType == AuthenticatorType.appflowyCloud.value ||\n        Env.authenticatorType == AuthenticatorType.appflowyCloudDevelop.value &&\n            _Env.afCloudUrl.isEmpty;\n  }\n\n  @EnviedField(\n    obfuscate: false,\n    varName: 'AUTHENTICATOR_TYPE',\n    defaultValue: 2,\n  )\n  static const int authenticatorType = _Env.authenticatorType;\n\n  /// AppFlowy Cloud Configuration\n  @EnviedField(\n    obfuscate: false,\n    varName: 'APPFLOWY_CLOUD_URL',\n    defaultValue: '',\n  )\n  static const String afCloudUrl = _Env.afCloudUrl;\n\n  @EnviedField(\n    obfuscate: false,\n    varName: 'INTERNAL_BUILD',\n    defaultValue: '',\n  )\n  static const String internalBuild = _Env.internalBuild;\n\n  @EnviedField(\n    obfuscate: false,\n    varName: 'SENTRY_DSN',\n    defaultValue: '',\n  )\n  static const String sentryDsn = _Env.sentryDsn;\n\n  @EnviedField(\n    obfuscate: false,\n    varName: 'BASE_WEB_DOMAIN',\n    defaultValue: ShareConstants.defaultBaseWebDomain,\n  )\n  static const String baseWebDomain = _Env.baseWebDomain;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/page_access_level/data/repositories/page_access_level_repository.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\n/// Abstract repository for managing view lock status.\n///\n/// For example, we're using rust events now, but we can still use the http api\n/// for the future.\nabstract class PageAccessLevelRepository {\n  /// Gets the current view from the backend.\n  Future<FlowyResult<ViewPB, FlowyError>> getView(String pageId);\n\n  /// Locks the view.\n  Future<FlowyResult<void, FlowyError>> lockView(String pageId);\n\n  /// Unlocks the view.\n  Future<FlowyResult<void, FlowyError>> unlockView(String pageId);\n\n  /// Gets the access level of the current user.\n  Future<FlowyResult<ShareAccessLevel, FlowyError>> getAccessLevel(\n    String pageId,\n  );\n\n  /// Gets the section type of the shared section.\n  Future<FlowyResult<SharedSectionType, FlowyError>> getSectionType(\n    String pageId,\n  );\n\n  /// Get current workspace\n  Future<FlowyResult<UserWorkspacePB, FlowyError>> getCurrentWorkspace();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/page_access_level/data/repositories/rust_page_access_level_repository_impl.dart",
    "content": "import 'package:appflowy/features/page_access_level/data/repositories/page_access_level_repository.dart';\nimport 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/features/util/extensions.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'\n    hide AFRolePB;\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:collection/collection.dart';\n\nclass RustPageAccessLevelRepositoryImpl implements PageAccessLevelRepository {\n  @override\n  Future<FlowyResult<ViewPB, FlowyError>> getView(String pageId) async {\n    final result = await ViewBackendService.getView(pageId);\n    return result.fold(\n      (view) {\n        Log.debug('get view(${view.id}) success');\n        return FlowyResult.success(view);\n      },\n      (error) {\n        Log.error('failed to get view, error: $error');\n        return FlowyResult.failure(error);\n      },\n    );\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> lockView(String pageId) async {\n    final result = await ViewBackendService.lockView(pageId);\n    return result.fold(\n      (_) {\n        Log.debug('lock view($pageId) success');\n        return FlowyResult.success(null);\n      },\n      (error) {\n        Log.error('failed to lock view, error: $error');\n        return FlowyResult.failure(error);\n      },\n    );\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> unlockView(String pageId) async {\n    final result = await ViewBackendService.unlockView(pageId);\n    return result.fold(\n      (_) {\n        Log.debug('unlock view($pageId) success');\n        return FlowyResult.success(null);\n      },\n      (error) {\n        Log.error('failed to unlock view, error: $error');\n        return FlowyResult.failure(error);\n      },\n    );\n  }\n\n  /// 1. local users have full access\n  /// 2. local workspace users have full access\n  /// 3. page creator has full access\n  /// 4. owner and members in public page have full access\n  /// 5. check the shared users list\n  @override\n  Future<FlowyResult<ShareAccessLevel, FlowyError>> getAccessLevel(\n    String pageId,\n  ) async {\n    final userResult = await UserBackendService.getCurrentUserProfile();\n    final user = userResult.fold(\n      (s) => s,\n      (_) => null,\n    );\n\n    if (user == null) {\n      return FlowyResult.failure(\n        FlowyError(\n          code: ErrorCode.Internal,\n          msg: 'User not found',\n        ),\n      );\n    }\n\n    if (user.userAuthType == AuthTypePB.Local) {\n      // Local user can always have full access.\n      return FlowyResult.success(ShareAccessLevel.fullAccess);\n    }\n\n    if (user.workspaceType == WorkspaceTypePB.LocalW) {\n      // Local workspace can always have full access.\n      return FlowyResult.success(ShareAccessLevel.fullAccess);\n    }\n\n    // If the user is the creator of the page, they can always have full access.\n    final viewResult = await getView(pageId);\n    final view = viewResult.fold(\n      (s) => s,\n      (_) => null,\n    );\n    if (view?.createdBy == user.id) {\n      return FlowyResult.success(ShareAccessLevel.fullAccess);\n    }\n\n    // If the page is public, the user can always have full access.\n    final workspaceResult = await getCurrentWorkspace();\n    final workspace = workspaceResult.fold(\n      (s) => s,\n      (_) => null,\n    );\n    if (workspace == null) {\n      return FlowyResult.failure(\n        FlowyError(\n          code: ErrorCode.Internal,\n          msg: 'Current workspace not found',\n        ),\n      );\n    }\n\n    final sectionTypeResult = await getSectionType(pageId);\n    final sectionType = sectionTypeResult.fold(\n      (s) => s,\n      (_) => null,\n    );\n    if (sectionType == SharedSectionType.public &&\n        workspace.role != AFRolePB.Guest) {\n      return FlowyResult.success(ShareAccessLevel.fullAccess);\n    }\n\n    final email = user.email;\n\n    final request = GetSharedUsersPayloadPB(\n      viewId: pageId,\n    );\n    final result = await FolderEventGetSharedUsers(request).send();\n    return result.fold(\n      (success) {\n        final accessLevel = success.items\n                .firstWhereOrNull(\n                  (item) => item.email == email,\n                )\n                ?.accessLevel\n                .shareAccessLevel ??\n            ShareAccessLevel.readOnly;\n\n        Log.debug('current user access level: $accessLevel, in page: $pageId');\n\n        return FlowyResult.success(accessLevel);\n      },\n      (failure) {\n        Log.error(\n          'failed to get user access level: $failure, in page: $pageId',\n        );\n\n        // return the read access level if the user is not found\n        return FlowyResult.success(ShareAccessLevel.readOnly);\n      },\n    );\n  }\n\n  @override\n  Future<FlowyResult<SharedSectionType, FlowyError>> getSectionType(\n    String pageId,\n  ) async {\n    final request = ViewIdPB(value: pageId);\n    final result = await FolderEventGetSharedViewSection(request).send();\n    return result.fold(\n      (success) {\n        final sectionType = success.section.sharedSectionType;\n        Log.debug('shared section type: $sectionType, in page: $pageId');\n        return FlowyResult.success(sectionType);\n      },\n      (failure) {\n        Log.error(\n          'failed to get shared section type: $failure, in page: $pageId',\n        );\n\n        return FlowyResult.failure(failure);\n      },\n    );\n  }\n\n  @override\n  Future<FlowyResult<UserWorkspacePB, FlowyError>> getCurrentWorkspace() async {\n    final result = await UserBackendService.getCurrentWorkspace();\n    final currentWorkspaceId = result.fold(\n      (s) => s.id,\n      (_) => null,\n    );\n\n    if (currentWorkspaceId == null) {\n      return FlowyResult.failure(\n        FlowyError(\n          code: ErrorCode.Internal,\n          msg: 'Current workspace not found',\n        ),\n      );\n    }\n\n    final workspaceResult = await UserBackendService.getWorkspaceById(\n      currentWorkspaceId,\n    );\n    return workspaceResult;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/page_access_level/logic/page_access_level_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/features/page_access_level/data/repositories/page_access_level_repository.dart';\nimport 'package:appflowy/features/page_access_level/data/repositories/rust_page_access_level_repository_impl.dart';\nimport 'package:appflowy/features/page_access_level/logic/page_access_level_event.dart';\nimport 'package:appflowy/features/page_access_level/logic/page_access_level_state.dart';\nimport 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/workspace/application/view/view_listener.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:protobuf/protobuf.dart';\n\nexport 'page_access_level_event.dart';\nexport 'page_access_level_state.dart';\n\nclass PageAccessLevelBloc\n    extends Bloc<PageAccessLevelEvent, PageAccessLevelState> {\n  PageAccessLevelBloc({\n    required this.view,\n    this.ignorePageAccessLevel = false,\n    PageAccessLevelRepository? repository,\n  })  : repository = repository ?? RustPageAccessLevelRepositoryImpl(),\n        listener = ViewListener(viewId: view.id),\n        super(PageAccessLevelState.initial(view)) {\n    on<PageAccessLevelInitialEvent>(_onInitial);\n    on<PageAccessLevelLockEvent>(_onLock);\n    on<PageAccessLevelUnlockEvent>(_onUnlock);\n    on<PageAccessLevelUpdateLockStatusEvent>(_onUpdateLockStatus);\n    on<PageAccessLevelUpdateSectionTypeEvent>(_onUpdateSectionType);\n  }\n\n  final ViewPB view;\n\n  // The repository to manage view lock status.\n  // If you need to test this bloc, you can add your own repository implementation.\n  final PageAccessLevelRepository repository;\n\n  // Used to listen for view updates.\n  late final ViewListener listener;\n\n  // should ignore the page access level\n  // in the row details page, we don't need to check the page access level\n  final bool ignorePageAccessLevel;\n\n  @override\n  Future<void> close() async {\n    await listener.stop();\n    return super.close();\n  }\n\n  Future<void> _onInitial(\n    PageAccessLevelInitialEvent event,\n    Emitter<PageAccessLevelState> emit,\n  ) async {\n    // lock status\n    listener.start(\n      onViewUpdated: (view) async {\n        add(PageAccessLevelEvent.updateLockStatus(view.isLocked));\n      },\n    );\n\n    // section type\n    final sectionTypeResult = await repository.getSectionType(view.id);\n    final sectionType = sectionTypeResult.fold(\n      (sectionType) => sectionType,\n      (_) => SharedSectionType.public,\n    );\n\n    if (!FeatureFlag.sharedSection.isOn || ignorePageAccessLevel) {\n      emit(\n        state.copyWith(\n          view: view,\n          isLocked: view.isLocked,\n          isLoadingLockStatus: false,\n          accessLevel: ShareAccessLevel.fullAccess,\n          sectionType: sectionType,\n        ),\n      );\n      return;\n    }\n\n    final result = await repository.getView(view.id);\n    final accessLevel = await repository.getAccessLevel(view.id);\n    final latestView = result.fold(\n      (view) => view,\n      (_) => view,\n    );\n    emit(\n      state.copyWith(\n        view: latestView,\n        isLocked: latestView.isLocked,\n        isLoadingLockStatus: false,\n        accessLevel: accessLevel.fold(\n          (accessLevel) => accessLevel,\n          (_) => ShareAccessLevel.readOnly,\n        ),\n        sectionType: sectionType,\n      ),\n    );\n  }\n\n  Future<void> _onLock(\n    PageAccessLevelLockEvent event,\n    Emitter<PageAccessLevelState> emit,\n  ) async {\n    final result = await repository.lockView(view.id);\n    final isLocked = result.fold(\n      (_) => true,\n      (_) => false,\n    );\n    add(\n      PageAccessLevelEvent.updateLockStatus(\n        isLocked,\n      ),\n    );\n  }\n\n  Future<void> _onUnlock(\n    PageAccessLevelUnlockEvent event,\n    Emitter<PageAccessLevelState> emit,\n  ) async {\n    final result = await repository.unlockView(view.id);\n    final isLocked = result.fold(\n      (_) => false,\n      (_) => true,\n    );\n    add(\n      PageAccessLevelEvent.updateLockStatus(\n        isLocked,\n        lockCounter: state.lockCounter + 1,\n      ),\n    );\n  }\n\n  void _onUpdateLockStatus(\n    PageAccessLevelUpdateLockStatusEvent event,\n    Emitter<PageAccessLevelState> emit,\n  ) {\n    state.view.freeze();\n    final updatedView = state.view.rebuild(\n      (update) => update.isLocked = event.isLocked,\n    );\n    emit(\n      state.copyWith(\n        view: updatedView,\n        isLocked: event.isLocked,\n        lockCounter: event.lockCounter ?? state.lockCounter,\n      ),\n    );\n  }\n\n  void _onUpdateSectionType(\n    PageAccessLevelUpdateSectionTypeEvent event,\n    Emitter<PageAccessLevelState> emit,\n  ) {\n    emit(\n      state.copyWith(\n        sectionType: event.sectionType,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/page_access_level/logic/page_access_level_event.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\n\n/// Base class for all PageAccessLevel events\nsealed class PageAccessLevelEvent {\n  const PageAccessLevelEvent();\n\n  /// Initialize the view lock status, it will create a view listener to listen for view updates.\n  /// Also, it will fetch the current view lock status from the repository.\n  const factory PageAccessLevelEvent.initial() = PageAccessLevelInitialEvent;\n\n  /// Lock the view.\n  const factory PageAccessLevelEvent.lock() = PageAccessLevelLockEvent;\n\n  /// Unlock the view.\n  const factory PageAccessLevelEvent.unlock() = PageAccessLevelUnlockEvent;\n\n  /// Update the lock status in the state.\n  const factory PageAccessLevelEvent.updateLockStatus(\n    bool isLocked, {\n    int? lockCounter,\n  }) = PageAccessLevelUpdateLockStatusEvent;\n\n  /// Update the section type in the state.\n  const factory PageAccessLevelEvent.updateSectionType(\n    SharedSectionType sectionType,\n  ) = PageAccessLevelUpdateSectionTypeEvent;\n}\n\nclass PageAccessLevelInitialEvent extends PageAccessLevelEvent {\n  const PageAccessLevelInitialEvent();\n}\n\nclass PageAccessLevelLockEvent extends PageAccessLevelEvent {\n  const PageAccessLevelLockEvent();\n}\n\nclass PageAccessLevelUnlockEvent extends PageAccessLevelEvent {\n  const PageAccessLevelUnlockEvent();\n}\n\nclass PageAccessLevelUpdateLockStatusEvent extends PageAccessLevelEvent {\n  const PageAccessLevelUpdateLockStatusEvent(\n    this.isLocked, {\n    this.lockCounter,\n  });\n\n  final bool isLocked;\n  final int? lockCounter;\n}\n\nclass PageAccessLevelUpdateSectionTypeEvent extends PageAccessLevelEvent {\n  const PageAccessLevelUpdateSectionTypeEvent(this.sectionType);\n\n  final SharedSectionType sectionType;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/page_access_level/logic/page_access_level_state.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\n\nclass PageAccessLevelState {\n  factory PageAccessLevelState.initial(ViewPB view) => PageAccessLevelState(\n        view: view,\n        isLocked: false,\n        lockCounter: 0,\n        sectionType: SharedSectionType.public,\n        accessLevel: ShareAccessLevel.readOnly,\n      );\n\n  const PageAccessLevelState({\n    required this.view,\n    required this.isLocked,\n    required this.lockCounter,\n    required this.accessLevel,\n    required this.sectionType,\n    this.myRole,\n    this.isLoadingLockStatus = true,\n  });\n\n  final ViewPB view;\n  final bool isLocked;\n  final int lockCounter;\n  final bool isLoadingLockStatus;\n  final ShareAccessLevel accessLevel;\n  final SharedSectionType sectionType;\n  final ShareRole? myRole;\n\n  bool get isPublic => sectionType == SharedSectionType.public;\n  bool get isPrivate => sectionType == SharedSectionType.private;\n  bool get isShared => sectionType == SharedSectionType.shared;\n  bool get shouldHideSpace => myRole == ShareRole.guest;\n\n  bool get isEditable {\n    if (!FeatureFlag.sharedSection.isOn) {\n      return !isLocked;\n    }\n\n    return accessLevel != ShareAccessLevel.readOnly && !isLocked;\n  }\n\n  bool get isReadOnly => accessLevel == ShareAccessLevel.readOnly;\n\n  PageAccessLevelState copyWith({\n    ViewPB? view,\n    bool? isLocked,\n    int? lockCounter,\n    bool? isLoadingLockStatus,\n    ShareAccessLevel? accessLevel,\n    SharedSectionType? sectionType,\n    ShareRole? myRole,\n  }) {\n    return PageAccessLevelState(\n      view: view ?? this.view,\n      isLocked: isLocked ?? this.isLocked,\n      lockCounter: lockCounter ?? this.lockCounter,\n      isLoadingLockStatus: isLoadingLockStatus ?? this.isLoadingLockStatus,\n      accessLevel: accessLevel ?? this.accessLevel,\n      sectionType: sectionType ?? this.sectionType,\n      myRole: myRole ?? this.myRole,\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n    return other is PageAccessLevelState &&\n        other.view == view &&\n        other.isLocked == isLocked &&\n        other.lockCounter == lockCounter &&\n        other.isLoadingLockStatus == isLoadingLockStatus &&\n        other.accessLevel == accessLevel &&\n        other.sectionType == sectionType &&\n        other.myRole == myRole;\n  }\n\n  @override\n  int get hashCode {\n    return Object.hash(\n      view,\n      isLocked,\n      lockCounter,\n      isLoadingLockStatus,\n      accessLevel,\n      sectionType,\n      myRole,\n    );\n  }\n\n  @override\n  String toString() {\n    return 'PageAccessLevelState(view: $view, isLocked: $isLocked, lockCounter: $lockCounter, isLoadingLockStatus: $isLoadingLockStatus, accessLevel: $accessLevel, sectionType: $sectionType, myRole: $myRole)';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/settings/data/models/user_data_location.dart",
    "content": "import 'package:equatable/equatable.dart';\n\nclass UserDataLocation extends Equatable {\n  const UserDataLocation({\n    required this.path,\n    required this.isCustom,\n  });\n\n  final String path;\n  final bool isCustom;\n\n  @override\n  List<Object?> get props => [path, isCustom];\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/settings/data/repositories/rust_settings_repository_impl.dart",
    "content": "import 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/startup/tasks/rust_sdk.dart';\nimport 'package:appflowy/user/application/user_settings_service.dart';\nimport 'package:appflowy/workspace/application/settings/prelude.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nimport '../models/user_data_location.dart';\nimport 'settings_repository.dart';\n\nclass RustSettingsRepositoryImpl implements SettingsRepository {\n  const RustSettingsRepositoryImpl();\n\n  final _userBackendService = const UserSettingsBackendService();\n\n  @override\n  Future<FlowyResult<UserDataLocation, FlowyError>>\n      getUserDataLocation() async {\n    final defaultDirectory = (await appFlowyApplicationDataDirectory()).path;\n    final result = await _userBackendService.getUserSetting();\n\n    return result.map(\n      (settings) {\n        final userDirectory = settings.userFolder;\n        return UserDataLocation(\n          path: userDirectory,\n          isCustom: userDirectory.contains(defaultDirectory),\n        );\n      },\n    );\n  }\n\n  @override\n  Future<FlowyResult<UserDataLocation, FlowyError>>\n      resetUserDataLocation() async {\n    final directory = await appFlowyApplicationDataDirectory();\n    await getIt<ApplicationDataStorage>().setPath(directory.path);\n\n    return FlowyResult.success(\n      UserDataLocation(\n        path: directory.path,\n        isCustom: false,\n      ),\n    );\n  }\n\n  @override\n  Future<FlowyResult<UserDataLocation, FlowyError>> setCustomLocation(\n    String path,\n  ) async {\n    final defaultDirectory = (await appFlowyApplicationDataDirectory()).path;\n    await getIt<ApplicationDataStorage>().setCustomPath(path);\n\n    return FlowyResult.success(\n      UserDataLocation(\n        path: path,\n        isCustom: path.contains(defaultDirectory),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/settings/data/repositories/settings_repository.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nimport '../models/user_data_location.dart';\n\nabstract class SettingsRepository {\n  Future<FlowyResult<UserDataLocation, FlowyError>> getUserDataLocation();\n\n  Future<FlowyResult<UserDataLocation, FlowyError>> resetUserDataLocation();\n\n  Future<FlowyResult<UserDataLocation, FlowyError>> setCustomLocation(\n    String path,\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/settings/logic/data_location_bloc.dart",
    "content": "import 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../data/repositories/settings_repository.dart';\nimport 'data_location_event.dart';\nimport 'data_location_state.dart';\n\nclass DataLocationBloc extends Bloc<DataLocationEvent, DataLocationState> {\n  DataLocationBloc({\n    required SettingsRepository repository,\n  })  : _repository = repository,\n        super(DataLocationState.initial()) {\n    on<DataLocationInitial>(_onStarted);\n    on<DataLocationResetToDefault>(_onResetToDefault);\n    on<DataLocationSetCustomPath>(_onSetCustomPath);\n    on<DataLocationClearState>(_onClearState);\n  }\n\n  final SettingsRepository _repository;\n\n  Future<void> _onStarted(\n    DataLocationInitial event,\n    Emitter<DataLocationState> emit,\n  ) async {\n    final userDataLocation =\n        await _repository.getUserDataLocation().toNullable();\n\n    emit(\n      DataLocationState(\n        userDataLocation: userDataLocation,\n        didResetToDefault: false,\n      ),\n    );\n  }\n\n  Future<void> _onResetToDefault(\n    DataLocationResetToDefault event,\n    Emitter<DataLocationState> emit,\n  ) async {\n    final defaultLocation =\n        await _repository.resetUserDataLocation().toNullable();\n\n    emit(\n      DataLocationState(\n        userDataLocation: defaultLocation,\n        didResetToDefault: true,\n      ),\n    );\n  }\n\n  Future<void> _onClearState(\n    DataLocationClearState event,\n    Emitter<DataLocationState> emit,\n  ) async {\n    emit(\n      state.copyWith(\n        didResetToDefault: false,\n      ),\n    );\n  }\n\n  Future<void> _onSetCustomPath(\n    DataLocationSetCustomPath event,\n    Emitter<DataLocationState> emit,\n  ) async {\n    final userDataLocation =\n        await _repository.setCustomLocation(event.path).toNullable();\n\n    emit(\n      state.copyWith(\n        userDataLocation: userDataLocation,\n        didResetToDefault: false,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/settings/logic/data_location_event.dart",
    "content": "sealed class DataLocationEvent {\n  const DataLocationEvent();\n\n  factory DataLocationEvent.initial() = DataLocationInitial;\n\n  factory DataLocationEvent.resetToDefault() = DataLocationResetToDefault;\n\n  factory DataLocationEvent.setCustomPath(String path) =\n      DataLocationSetCustomPath;\n\n  factory DataLocationEvent.clearState() = DataLocationClearState;\n}\n\nclass DataLocationInitial extends DataLocationEvent {}\n\nclass DataLocationResetToDefault extends DataLocationEvent {}\n\nclass DataLocationSetCustomPath extends DataLocationEvent {\n  const DataLocationSetCustomPath(this.path);\n\n  final String path;\n}\n\nclass DataLocationClearState extends DataLocationEvent {}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/settings/logic/data_location_state.dart",
    "content": "import 'package:equatable/equatable.dart';\n\nimport '../data/models/user_data_location.dart';\n\nclass DataLocationState extends Equatable {\n  const DataLocationState({\n    required this.userDataLocation,\n    required this.didResetToDefault,\n  });\n\n  factory DataLocationState.initial() =>\n      const DataLocationState(userDataLocation: null, didResetToDefault: false);\n\n  final UserDataLocation? userDataLocation;\n  final bool didResetToDefault;\n\n  @override\n  List<Object?> get props => [userDataLocation, didResetToDefault];\n\n  DataLocationState copyWith({\n    UserDataLocation? userDataLocation,\n    bool? didResetToDefault,\n  }) {\n    return DataLocationState(\n      userDataLocation: userDataLocation ?? this.userDataLocation,\n      didResetToDefault: didResetToDefault ?? this.didResetToDefault,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/settings/settings.dart",
    "content": "export 'logic/data_location_bloc.dart';\nexport 'logic/data_location_event.dart';\nexport 'logic/data_location_state.dart';\nexport 'data/models/user_data_location.dart';\nexport 'data/repositories/settings_repository.dart';\nexport 'data/repositories/rust_settings_repository_impl.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/data/models/models.dart",
    "content": "export 'share_access_level.dart';\nexport 'share_popover_group_id.dart';\nexport 'share_role.dart';\nexport 'shared_user.dart';\nexport 'share_section_type.dart';"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/data/models/share_access_level.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\n/// The access level a user can have on a shared page.\nenum ShareAccessLevel {\n  /// Can view the page only.\n  readOnly,\n\n  /// Can read and comment on the page.\n  readAndComment,\n\n  /// Can read and write to the page.\n  readAndWrite,\n\n  /// Full access (edit, share, remove, etc.) and can add new users.\n  fullAccess;\n\n  String get title {\n    switch (this) {\n      case ShareAccessLevel.readOnly:\n        return LocaleKeys.shareTab_accessLevel_view.tr();\n      case ShareAccessLevel.readAndComment:\n        return LocaleKeys.shareTab_accessLevel_comment.tr();\n      case ShareAccessLevel.readAndWrite:\n        return LocaleKeys.shareTab_accessLevel_edit.tr();\n      case ShareAccessLevel.fullAccess:\n        return LocaleKeys.shareTab_accessLevel_fullAccess.tr();\n    }\n  }\n\n  String get subtitle {\n    switch (this) {\n      case ShareAccessLevel.readOnly:\n        return LocaleKeys.shareTab_cantMakeChanges.tr();\n      case ShareAccessLevel.readAndComment:\n        return LocaleKeys.shareTab_canMakeAnyChanges.tr();\n      case ShareAccessLevel.readAndWrite:\n        return LocaleKeys.shareTab_canMakeAnyChanges.tr();\n      case ShareAccessLevel.fullAccess:\n        return LocaleKeys.shareTab_canMakeAnyChanges.tr();\n    }\n  }\n\n  FlowySvgData get icon {\n    switch (this) {\n      case ShareAccessLevel.readOnly:\n        return FlowySvgs.access_level_view_m;\n      case ShareAccessLevel.readAndComment:\n        return FlowySvgs.access_level_edit_m;\n      case ShareAccessLevel.readAndWrite:\n        return FlowySvgs.access_level_edit_m;\n      case ShareAccessLevel.fullAccess:\n        return FlowySvgs.access_level_edit_m;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/data/models/share_popover_group_id.dart",
    "content": "class SharePopoverGroupId {}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/data/models/share_role.dart",
    "content": "enum ShareRole {\n  /// The owner of the shared page.\n  owner,\n\n  /// A guest on the shared page.\n  guest,\n\n  /// A member of the shared page.\n  member,\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/data/models/share_section_type.dart",
    "content": "/// The type of the shared section.\n///\n/// - public: the shared section is public, anyone in the workspace can view/edit it.\n/// - shared: the shared section is shared, anyone in the shared section can view/edit it.\n/// - private: the shared section is private, only the users in the shared section can view/edit it.\nenum SharedSectionType {\n  unknown,\n  public,\n  shared,\n  private;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/data/models/shared_group.dart",
    "content": "class SharedGroup {\n  SharedGroup({\n    required this.id,\n    required this.name,\n    required this.icon,\n  });\n\n  final String id;\n\n  final String name;\n\n  final String icon;\n\n  SharedGroup copyWith({\n    String? id,\n    String? name,\n    String? icon,\n  }) {\n    return SharedGroup(\n      id: id ?? this.id,\n      name: name ?? this.name,\n      icon: icon ?? this.icon,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/data/models/shared_user.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/share_access_level.dart';\nimport 'package:appflowy/features/share_tab/data/models/share_role.dart';\n\ntypedef SharedUsers = List<SharedUser>;\n\n/// Represents a user with a role on a shared page.\nclass SharedUser {\n  SharedUser({\n    required this.email,\n    required this.name,\n    required this.role,\n    required this.accessLevel,\n    this.avatarUrl,\n  });\n\n  final String email;\n\n  /// The name of the user.\n  final String name;\n\n  /// The role of the user.\n  final ShareRole role;\n\n  /// The access level of the user.\n  final ShareAccessLevel accessLevel;\n\n  /// The avatar URL of the user.\n  ///\n  /// if the avatar is not set, it will be the first letter of the name.\n  final String? avatarUrl;\n\n  SharedUser copyWith({\n    String? email,\n    String? name,\n    ShareRole? role,\n    ShareAccessLevel? accessLevel,\n    String? avatarUrl,\n  }) {\n    return SharedUser(\n      email: email ?? this.email,\n      name: name ?? this.name,\n      role: role ?? this.role,\n      accessLevel: accessLevel ?? this.accessLevel,\n      avatarUrl: avatarUrl ?? this.avatarUrl,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/data/repositories/local_share_with_user_repository_impl.dart",
    "content": "import 'dart:math';\n\nimport 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nimport 'share_with_user_repository.dart';\n\n// Move this file to test folder\nclass LocalShareWithUserRepositoryImpl extends ShareWithUserRepository {\n  LocalShareWithUserRepositoryImpl();\n\n  final SharedUsers _sharedUsers = [\n    // current user has full access\n    SharedUser(\n      email: 'lucas.xu@appflowy.io',\n      name: 'Lucas Xu - Long long long long long name',\n      accessLevel: ShareAccessLevel.readOnly,\n      role: ShareRole.guest,\n      avatarUrl: 'https://avatar.iran.liara.run/public',\n    ),\n    // member user has read and write access\n    SharedUser(\n      email: 'vivian@appflowy.io',\n      name: 'Vivian Wang',\n      accessLevel: ShareAccessLevel.readAndWrite,\n      role: ShareRole.member,\n      avatarUrl: 'https://avatar.iran.liara.run/public/girl',\n    ),\n    // member user has read access\n    SharedUser(\n      email: 'shuheng@appflowy.io',\n      name: 'Shuheng',\n      accessLevel: ShareAccessLevel.readOnly,\n      role: ShareRole.member,\n      avatarUrl: 'https://avatar.iran.liara.run/public/boy',\n    ),\n    // guest user has read access\n    SharedUser(\n      email: 'guest_user_1@appflowy.io',\n      name: 'Read Only Guest',\n      accessLevel: ShareAccessLevel.readOnly,\n      role: ShareRole.guest,\n      avatarUrl: 'https://avatar.iran.liara.run/public/boy/10',\n    ),\n    // guest user has read and write access\n    SharedUser(\n      email: 'guest_user_2@appflowy.io',\n      name: 'Read And Write Guest',\n      accessLevel: ShareAccessLevel.readAndWrite,\n      role: ShareRole.guest,\n      avatarUrl: 'https://avatar.iran.liara.run/public/boy/11',\n    ),\n    // Others\n    SharedUser(\n      email: 'member_user_1@appflowy.io',\n      name: 'Member User 1',\n      accessLevel: ShareAccessLevel.readAndWrite,\n      role: ShareRole.member,\n      avatarUrl: 'https://avatar.iran.liara.run/public/boy/12',\n    ),\n    SharedUser(\n      email: 'member_user_2@appflowy.io',\n      name: 'Member User 2',\n      accessLevel: ShareAccessLevel.readAndWrite,\n      role: ShareRole.member,\n      avatarUrl: 'https://avatar.iran.liara.run/public/boy/13',\n    ),\n  ];\n\n  final SharedUsers _availableSharedUsers = [\n    SharedUser(\n      email: 'guest_email@appflowy.io',\n      name: 'Guest',\n      accessLevel: ShareAccessLevel.readOnly,\n      role: ShareRole.guest,\n      avatarUrl: 'https://avatar.iran.liara.run/public/boy/28',\n    ),\n    SharedUser(\n      email: 'richard@appflowy.io',\n      name: 'Richard',\n      accessLevel: ShareAccessLevel.readAndWrite,\n      role: ShareRole.member,\n      avatarUrl: 'https://avatar.iran.liara.run/public/boy/28',\n    ),\n  ];\n\n  @override\n  Future<FlowyResult<SharedUsers, FlowyError>> getSharedUsersInPage({\n    required String pageId,\n  }) async {\n    return FlowySuccess(_sharedUsers);\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> removeSharedUserFromPage({\n    required String pageId,\n    required List<String> emails,\n  }) async {\n    for (final email in emails) {\n      _sharedUsers.removeWhere((user) => user.email == email);\n    }\n\n    return FlowySuccess(null);\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> sharePageWithUser({\n    required String pageId,\n    required ShareAccessLevel accessLevel,\n    required List<String> emails,\n  }) async {\n    for (final email in emails) {\n      final index = _sharedUsers.indexWhere((user) => user.email == email);\n      if (index != -1) {\n        // Update access level if user exists\n        final user = _sharedUsers[index];\n        _sharedUsers[index] = SharedUser(\n          name: user.name,\n          email: user.email,\n          accessLevel: accessLevel,\n          role: user.role,\n          avatarUrl: user.avatarUrl,\n        );\n      } else {\n        // Add new user\n        _sharedUsers.add(\n          SharedUser(\n            name: email.split('@').first,\n            email: email,\n            accessLevel: accessLevel,\n            role: ShareRole.guest,\n            avatarUrl:\n                'https://avatar.iran.liara.run/public/${Random().nextInt(100)}',\n          ),\n        );\n      }\n    }\n\n    return FlowySuccess(null);\n  }\n\n  @override\n  Future<FlowyResult<SharedUsers, FlowyError>> getAvailableSharedUsers({\n    required String pageId,\n  }) async {\n    return FlowySuccess([\n      ..._sharedUsers,\n      ..._availableSharedUsers,\n    ]);\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> changeRole({\n    required String workspaceId,\n    required String email,\n    required ShareRole role,\n  }) async {\n    final index = _sharedUsers.indexWhere((user) => user.email == email);\n    if (index != -1) {\n      _sharedUsers[index] = _sharedUsers[index].copyWith(role: role);\n    }\n\n    return FlowySuccess(null);\n  }\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> getCurrentUserProfile() async {\n    // Simulate fetching current user profile\n    return FlowySuccess(\n      UserProfilePB()\n        ..email = 'lucas.xu@appflowy.io'\n        ..name = 'Lucas Xu',\n    );\n  }\n\n  @override\n  Future<FlowyResult<SharedSectionType, FlowyError>> getCurrentPageSectionType({\n    required String pageId,\n  }) async {\n    return FlowySuccess(SharedSectionType.private);\n  }\n\n  @override\n  Future<bool> getUpgradeToProButtonClicked({\n    required String workspaceId,\n  }) async {\n    return false;\n  }\n\n  @override\n  Future<void> setUpgradeToProButtonClicked({\n    required String workspaceId,\n  }) async {\n    return;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/data/repositories/rust_share_with_user_repository_impl.dart",
    "content": "import 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/features/util/extensions.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:collection/collection.dart';\n\nimport 'share_with_user_repository.dart';\n\nclass RustShareWithUserRepositoryImpl extends ShareWithUserRepository {\n  RustShareWithUserRepositoryImpl();\n\n  @override\n  Future<FlowyResult<SharedUsers, FlowyError>> getSharedUsersInPage({\n    required String pageId,\n  }) async {\n    final request = GetSharedUsersPayloadPB(\n      viewId: pageId,\n    );\n    final result = await FolderEventGetSharedUsers(request).send();\n\n    return result.fold(\n      (success) {\n        Log.debug('get shared users success: $success');\n\n        return FlowySuccess(success.sharedUsers);\n      },\n      (failure) {\n        Log.error('get shared users failed: $failure');\n\n        return FlowyFailure(failure);\n      },\n    );\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> removeSharedUserFromPage({\n    required String pageId,\n    required List<String> emails,\n  }) async {\n    final request = RemoveUserFromSharedPagePayloadPB(\n      viewId: pageId,\n      emails: emails,\n    );\n    final result = await FolderEventRemoveUserFromSharedPage(request).send();\n\n    return result.fold(\n      (success) {\n        Log.debug('remove users($emails) from shared page($pageId)');\n\n        return FlowySuccess(success);\n      },\n      (failure) {\n        Log.error('remove users($emails) from shared page($pageId): $failure');\n\n        return FlowyFailure(failure);\n      },\n    );\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> sharePageWithUser({\n    required String pageId,\n    required ShareAccessLevel accessLevel,\n    required List<String> emails,\n  }) async {\n    final request = SharePageWithUserPayloadPB(\n      viewId: pageId,\n      emails: emails,\n      accessLevel: accessLevel.accessLevel,\n      autoConfirm: true,\n    );\n    final result = await FolderEventSharePageWithUser(request).send();\n\n    return result.fold(\n      (success) {\n        Log.debug(\n          'share page($pageId) with users($emails) with access level($accessLevel)',\n        );\n\n        return FlowySuccess(success);\n      },\n      (failure) {\n        Log.error(\n          'share page($pageId) with users($emails) with access level($accessLevel): $failure',\n        );\n\n        return FlowyFailure(failure);\n      },\n    );\n  }\n\n  @override\n  Future<FlowyResult<SharedUsers, FlowyError>> getAvailableSharedUsers({\n    required String pageId,\n  }) async {\n    return FlowySuccess([]);\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> changeRole({\n    required String workspaceId,\n    required String email,\n    required ShareRole role,\n  }) async {\n    final request = UpdateWorkspaceMemberPB(\n      workspaceId: workspaceId,\n      email: email,\n      role: role.userRole,\n    );\n    final result = await UserEventUpdateWorkspaceMember(request).send();\n    return result.fold(\n      (success) {\n        Log.debug(\n          'change role($role) for user($email) in workspaceId($workspaceId)',\n        );\n        return FlowySuccess(success);\n      },\n      (failure) {\n        Log.error(\n          'failed to change role($role) for user($email) in workspaceId($workspaceId)',\n          failure,\n        );\n        return FlowyFailure(failure);\n      },\n    );\n  }\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> getCurrentUserProfile() async {\n    final result = await UserEventGetUserProfile().send();\n    return result;\n  }\n\n  @override\n  Future<FlowyResult<SharedSectionType, FlowyError>> getCurrentPageSectionType({\n    required String pageId,\n  }) async {\n    final request = ViewIdPB.create()..value = pageId;\n    final result = await FolderEventGetViewAncestors(request).send();\n    final ancestors = result.fold(\n      (s) => s.items,\n      (f) => <ViewPB>[],\n    );\n    final space = ancestors.firstWhereOrNull((e) => e.isSpace);\n\n    if (space == null) {\n      return FlowySuccess(SharedSectionType.unknown);\n    }\n\n    final sectionType = switch (space.spacePermission) {\n      SpacePermission.publicToAll => SharedSectionType.public,\n      SpacePermission.private => SharedSectionType.private,\n    };\n\n    return FlowySuccess(sectionType);\n  }\n\n  @override\n  Future<bool> getUpgradeToProButtonClicked({\n    required String workspaceId,\n  }) async {\n    final result = await getIt<KeyValueStorage>().getWithFormat(\n      '${KVKeys.hasClickedUpgradeToProButton}_$workspaceId',\n      (value) => bool.parse(value),\n    );\n    if (result == null) {\n      return false;\n    }\n    return result;\n  }\n\n  @override\n  Future<void> setUpgradeToProButtonClicked({\n    required String workspaceId,\n  }) async {\n    await getIt<KeyValueStorage>().set(\n      '${KVKeys.hasClickedUpgradeToProButton}_$workspaceId',\n      'true',\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/data/repositories/share_with_user_repository.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\n/// Abstract repository for sharing with users.\n///\n/// For example, we're using rust events now, but we can still use the http api\n/// for the future.\nabstract class ShareWithUserRepository {\n  /// Gets the list of users and their roles for a shared page.\n  Future<FlowyResult<SharedUsers, FlowyError>> getSharedUsersInPage({\n    required String pageId,\n  });\n\n  /// Gets the list of users that are available to be shared with.\n  Future<FlowyResult<SharedUsers, FlowyError>> getAvailableSharedUsers({\n    required String pageId,\n  });\n\n  /// Removes a user from a shared page.\n  Future<FlowyResult<void, FlowyError>> removeSharedUserFromPage({\n    required String pageId,\n    required List<String> emails,\n  });\n\n  /// Shares a page with a user, assigning a role.\n  ///\n  /// If the user is already in the shared page, the access level will be updated.\n  Future<FlowyResult<void, FlowyError>> sharePageWithUser({\n    required String pageId,\n    required ShareAccessLevel accessLevel,\n    required List<String> emails,\n  });\n\n  /// Change the role of a user in a shared page.\n  Future<FlowyResult<void, FlowyError>> changeRole({\n    required String workspaceId,\n    required String email,\n    required ShareRole role,\n  });\n\n  /// Get current user profile.\n  Future<FlowyResult<UserProfilePB, FlowyError>> getCurrentUserProfile();\n\n  /// Get current page is in public section or private section.\n  Future<FlowyResult<SharedSectionType, FlowyError>> getCurrentPageSectionType({\n    required String pageId,\n  });\n\n  /// Get the upgrade to pro button has been clicked.\n  Future<bool> getUpgradeToProButtonClicked({\n    required String workspaceId,\n  });\n\n  /// Set the upgrade to pro button has been clicked.\n  Future<void> setUpgradeToProButtonClicked({\n    required String workspaceId,\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/logic/share_tab_bloc.dart",
    "content": "import 'package:appflowy/core/notification/folder_notification.dart';\nimport 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/features/share_tab/data/repositories/share_with_user_repository.dart';\nimport 'package:appflowy/features/share_tab/logic/share_tab_event.dart';\nimport 'package:appflowy/features/share_tab/logic/share_tab_state.dart';\nimport 'package:appflowy/features/util/extensions.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/shared/share/constants.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\n\nexport 'share_tab_event.dart';\nexport 'share_tab_state.dart';\n\nclass ShareTabBloc extends Bloc<ShareTabEvent, ShareTabState> {\n  ShareTabBloc({\n    required this.repository,\n    required this.pageId,\n    required this.workspaceId,\n  }) : super(ShareTabState.initial()) {\n    on<ShareTabEventInitialize>(_onInitial);\n    on<ShareTabEventLoadSharedUsers>(_onGetSharedUsers);\n    on<ShareTabEventInviteUsers>(_onShare);\n    on<ShareTabEventRemoveUsers>(_onRemove);\n    on<ShareTabEventUpdateUserAccessLevel>(_onUpdateAccessLevel);\n    on<ShareTabEventUpdateGeneralAccessLevel>(_onUpdateGeneralAccess);\n    on<ShareTabEventCopyShareLink>(_onCopyLink);\n    on<ShareTabEventSearchAvailableUsers>(_onSearchAvailableUsers);\n    on<ShareTabEventConvertToMember>(_onTurnIntoMember);\n    on<ShareTabEventClearState>(_onClearState);\n    on<ShareTabEventUpdateSharedUsers>(_onUpdateSharedUsers);\n    on<ShareTabEventUpgradeToProClicked>(_onUpgradeToProClicked);\n  }\n\n  final ShareWithUserRepository repository;\n  final String workspaceId;\n  final String pageId;\n\n  // Used to listen for shared view updates.\n  FolderNotificationListener? _folderNotificationListener;\n\n  @override\n  Future<void> close() async {\n      await _folderNotificationListener?.stop();\n    await super.close();\n  }\n\n  Future<void> _onInitial(\n    ShareTabEventInitialize event,\n    Emitter<ShareTabState> emit,\n  ) async {\n    if (!FeatureFlag.sharedSection.isOn) {\n      emit(\n        state.copyWith(\n          errorMessage: 'Sharing is currently disabled.',\n          users: [],\n          isLoading: false,\n        ),\n      );\n      return;\n    }\n\n    _initFolderNotificationListener();\n\n    final result = await repository.getCurrentUserProfile();\n    final currentUser = result.fold(\n      (user) => user,\n      (error) => null,\n    );\n\n    final sectionTypeResult = await repository.getCurrentPageSectionType(\n      pageId: pageId,\n    );\n    final sectionType = sectionTypeResult.fold(\n      (type) => type,\n      (error) => SharedSectionType.unknown,\n    );\n\n    final shareLink = ShareConstants.buildShareUrl(\n      workspaceId: workspaceId,\n      viewId: pageId,\n    );\n\n    final users = await _getSharedUsers();\n\n    final hasClickedUpgradeToPro =\n        await repository.getUpgradeToProButtonClicked(\n      workspaceId: workspaceId,\n    );\n\n    emit(\n      state.copyWith(\n        currentUser: currentUser,\n        shareLink: shareLink,\n        users: users,\n        sectionType: sectionType,\n        hasClickedUpgradeToPro: hasClickedUpgradeToPro,\n      ),\n    );\n  }\n\n  Future<void> _onGetSharedUsers(\n    ShareTabEventLoadSharedUsers event,\n    Emitter<ShareTabState> emit,\n  ) async {\n    if (!FeatureFlag.sharedSection.isOn) {\n      return;\n    }\n\n    emit(\n      state.copyWith(\n        errorMessage: '',\n      ),\n    );\n\n    final result = await repository.getSharedUsersInPage(\n      pageId: pageId,\n    );\n\n    result.fold(\n      (users) => emit(\n        state.copyWith(\n          users: users,\n          initialResult: FlowySuccess(null),\n        ),\n      ),\n      (error) => emit(\n        state.copyWith(\n          errorMessage: error.msg,\n          initialResult: FlowyFailure(error),\n        ),\n      ),\n    );\n  }\n\n  Future<void> _onShare(\n    ShareTabEventInviteUsers event,\n    Emitter<ShareTabState> emit,\n  ) async {\n    emit(\n      state.copyWith(\n        errorMessage: '',\n      ),\n    );\n\n    final result = await repository.sharePageWithUser(\n      pageId: pageId,\n      accessLevel: event.accessLevel,\n      emails: event.emails,\n    );\n\n    await result.fold(\n      (_) async {\n        final users = await _getSharedUsers();\n\n        emit(\n          state.copyWith(\n            shareResult: FlowySuccess(null),\n            users: users,\n          ),\n        );\n      },\n      (error) async {\n        emit(\n          state.copyWith(\n            errorMessage: error.msg,\n            shareResult: FlowyFailure(error),\n          ),\n        );\n      },\n    );\n  }\n\n  Future<void> _onRemove(\n    ShareTabEventRemoveUsers event,\n    Emitter<ShareTabState> emit,\n  ) async {\n    emit(\n      state.copyWith(\n        errorMessage: '',\n      ),\n    );\n\n    final result = await repository.removeSharedUserFromPage(\n      pageId: pageId,\n      emails: event.emails,\n    );\n\n    await result.fold(\n      (_) async {\n        final users = await _getSharedUsers();\n        emit(\n          state.copyWith(\n            removeResult: FlowySuccess(null),\n            users: users,\n          ),\n        );\n      },\n      (error) async {\n        emit(\n          state.copyWith(\n            isLoading: false,\n            removeResult: FlowyFailure(error),\n          ),\n        );\n      },\n    );\n  }\n\n  Future<void> _onUpdateAccessLevel(\n    ShareTabEventUpdateUserAccessLevel event,\n    Emitter<ShareTabState> emit,\n  ) async {\n    emit(\n      state.copyWith(),\n    );\n\n    final result = await repository.sharePageWithUser(\n      pageId: pageId,\n      accessLevel: event.accessLevel,\n      emails: [event.email],\n    );\n\n    await result.fold(\n      (_) async {\n        final users = await _getSharedUsers();\n        emit(\n          state.copyWith(\n            updateAccessLevelResult: FlowySuccess(null),\n            users: users,\n          ),\n        );\n      },\n      (error) async {\n        emit(\n          state.copyWith(\n            errorMessage: error.msg,\n            isLoading: false,\n          ),\n        );\n      },\n    );\n  }\n\n  void _onUpdateGeneralAccess(\n    ShareTabEventUpdateGeneralAccessLevel event,\n    Emitter<ShareTabState> emit,\n  ) {\n    emit(\n      state.copyWith(\n        generalAccessRole: event.accessLevel,\n      ),\n    );\n  }\n\n  void _onCopyLink(\n    ShareTabEventCopyShareLink event,\n    Emitter<ShareTabState> emit,\n  ) {\n    getIt<ClipboardService>().setData(\n      ClipboardServiceData(\n        plainText: event.link,\n      ),\n    );\n\n    emit(\n      state.copyWith(\n        linkCopied: true,\n      ),\n    );\n  }\n\n  Future<void> _onSearchAvailableUsers(\n    ShareTabEventSearchAvailableUsers event,\n    Emitter<ShareTabState> emit,\n  ) async {\n    emit(\n      state.copyWith(\n        errorMessage: '',\n      ),\n    );\n\n    final result = await repository.getAvailableSharedUsers(pageId: pageId);\n\n    result.fold(\n      (users) {\n        // filter by email and name\n        final availableUsers = users.where((user) {\n          final query = event.query.toLowerCase();\n          return user.name.toLowerCase().contains(query) ||\n              user.email.toLowerCase().contains(query);\n        }).toList();\n        emit(\n          state.copyWith(\n            availableUsers: availableUsers,\n          ),\n        );\n      },\n      (error) => emit(\n        state.copyWith(\n          errorMessage: error.msg,\n          availableUsers: [],\n        ),\n      ),\n    );\n  }\n\n  Future<void> _onTurnIntoMember(\n    ShareTabEventConvertToMember event,\n    Emitter<ShareTabState> emit,\n  ) async {\n    emit(\n      state.copyWith(\n        errorMessage: '',\n      ),\n    );\n\n    final result = await repository.changeRole(\n      workspaceId: workspaceId,\n      email: event.email,\n      role: ShareRole.member,\n    );\n\n    await result.fold(\n      (_) async {\n        final users = await _getSharedUsers();\n        emit(\n          state.copyWith(\n            turnIntoMemberResult: FlowySuccess(null),\n            users: users,\n          ),\n        );\n      },\n      (error) async {\n        emit(\n          state.copyWith(\n            errorMessage: error.msg,\n            turnIntoMemberResult: FlowyFailure(error),\n          ),\n        );\n      },\n    );\n  }\n\n  Future<SharedUsers> _getSharedUsers() async {\n    final shareResult = await repository.getSharedUsersInPage(\n      pageId: pageId,\n    );\n    return shareResult.fold(\n      (users) => users,\n      (error) => state.users,\n    );\n  }\n\n  void _onClearState(\n    ShareTabEventClearState event,\n    Emitter<ShareTabState> emit,\n  ) {\n    emit(\n      state.copyWith(\n        errorMessage: '',\n      ),\n    );\n  }\n\n  void _onUpdateSharedUsers(\n    ShareTabEventUpdateSharedUsers event,\n    Emitter<ShareTabState> emit,\n  ) {\n    emit(\n      state.copyWith(\n        users: event.users,\n      ),\n    );\n  }\n\n  Future<void> _onUpgradeToProClicked(\n    ShareTabEventUpgradeToProClicked event,\n    Emitter<ShareTabState> emit,\n  ) async {\n    await repository.setUpgradeToProButtonClicked(\n      workspaceId: workspaceId,\n    );\n    emit(\n      state.copyWith(\n        hasClickedUpgradeToPro: true,\n      ),\n    );\n  }\n\n  void _initFolderNotificationListener() {\n    _folderNotificationListener = FolderNotificationListener(\n      objectId: pageId,\n      handler: (notification, result) {\n        if (notification == FolderNotification.DidUpdateSharedUsers) {\n          final response = result.fold(\n            (payload) {\n              final repeatedSharedUsers =\n                  RepeatedSharedUserPB.fromBuffer(payload);\n              return repeatedSharedUsers;\n            },\n            (error) => null,\n          );\n          Log.debug('update shared users: $response');\n          if (response != null) {\n            add(\n              ShareTabEvent.updateSharedUsers(\n                users: response.sharedUsers.reversed.toList(),\n              ),\n            );\n          }\n        }\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/logic/share_tab_event.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\n\nsealed class ShareTabEvent {\n  const ShareTabEvent();\n\n  // Factory functions for creating events\n  factory ShareTabEvent.initialize() => const ShareTabEventInitialize();\n\n  factory ShareTabEvent.loadSharedUsers() =>\n      const ShareTabEventLoadSharedUsers();\n\n  factory ShareTabEvent.inviteUsers({\n    required List<String> emails,\n    required ShareAccessLevel accessLevel,\n  }) =>\n      ShareTabEventInviteUsers(emails: emails, accessLevel: accessLevel);\n\n  factory ShareTabEvent.removeUsers({\n    required List<String> emails,\n  }) =>\n      ShareTabEventRemoveUsers(emails: emails);\n\n  factory ShareTabEvent.updateUserAccessLevel({\n    required String email,\n    required ShareAccessLevel accessLevel,\n  }) =>\n      ShareTabEventUpdateUserAccessLevel(\n        email: email,\n        accessLevel: accessLevel,\n      );\n\n  factory ShareTabEvent.updateGeneralAccessLevel({\n    required ShareAccessLevel accessLevel,\n  }) =>\n      ShareTabEventUpdateGeneralAccessLevel(accessLevel: accessLevel);\n\n  factory ShareTabEvent.copyShareLink({\n    required String link,\n  }) =>\n      ShareTabEventCopyShareLink(link: link);\n\n  factory ShareTabEvent.searchAvailableUsers({\n    required String query,\n  }) =>\n      ShareTabEventSearchAvailableUsers(query: query);\n\n  factory ShareTabEvent.convertToMember({\n    required String email,\n  }) =>\n      ShareTabEventConvertToMember(email: email);\n\n  factory ShareTabEvent.clearState() => const ShareTabEventClearState();\n\n  factory ShareTabEvent.updateSharedUsers({\n    required SharedUsers users,\n  }) =>\n      ShareTabEventUpdateSharedUsers(users: users);\n\n  factory ShareTabEvent.upgradeToProClicked() =>\n      const ShareTabEventUpgradeToProClicked();\n}\n\n/// Initializes the share tab bloc.\nclass ShareTabEventInitialize extends ShareTabEvent {\n  const ShareTabEventInitialize();\n}\n\n/// Loads the shared users for the current page.\nclass ShareTabEventLoadSharedUsers extends ShareTabEvent {\n  const ShareTabEventLoadSharedUsers();\n}\n\n/// Invites users to the page with specified access level.\nclass ShareTabEventInviteUsers extends ShareTabEvent {\n  const ShareTabEventInviteUsers({\n    required this.emails,\n    required this.accessLevel,\n  });\n\n  final List<String> emails;\n  final ShareAccessLevel accessLevel;\n}\n\n/// Removes users from the shared page.\nclass ShareTabEventRemoveUsers extends ShareTabEvent {\n  const ShareTabEventRemoveUsers({\n    required this.emails,\n  });\n\n  final List<String> emails;\n}\n\n/// Updates the access level for a specific user.\nclass ShareTabEventUpdateUserAccessLevel extends ShareTabEvent {\n  const ShareTabEventUpdateUserAccessLevel({\n    required this.email,\n    required this.accessLevel,\n  });\n\n  final String email;\n  final ShareAccessLevel accessLevel;\n}\n\n/// Updates the general access level for all users.\nclass ShareTabEventUpdateGeneralAccessLevel extends ShareTabEvent {\n  const ShareTabEventUpdateGeneralAccessLevel({\n    required this.accessLevel,\n  });\n\n  final ShareAccessLevel accessLevel;\n}\n\n/// Copies the share link to the clipboard.\nclass ShareTabEventCopyShareLink extends ShareTabEvent {\n  const ShareTabEventCopyShareLink({\n    required this.link,\n  });\n\n  final String link;\n}\n\n/// Searches for available users by name or email.\nclass ShareTabEventSearchAvailableUsers extends ShareTabEvent {\n  const ShareTabEventSearchAvailableUsers({\n    required this.query,\n  });\n\n  final String query;\n}\n\n/// Converts a user into a member.\nclass ShareTabEventConvertToMember extends ShareTabEvent {\n  const ShareTabEventConvertToMember({\n    required this.email,\n  });\n\n  final String email;\n}\n\nclass ShareTabEventClearState extends ShareTabEvent {\n  const ShareTabEventClearState();\n}\n\nclass ShareTabEventUpdateSharedUsers extends ShareTabEvent {\n  const ShareTabEventUpdateSharedUsers({\n    required this.users,\n  });\n\n  final SharedUsers users;\n}\n\nclass ShareTabEventUpgradeToProClicked extends ShareTabEvent {\n  const ShareTabEventUpgradeToProClicked();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/logic/share_tab_state.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass ShareTabState {\n  factory ShareTabState.initial() => const ShareTabState();\n\n  const ShareTabState({\n    this.currentUser,\n    this.users = const [],\n    this.availableUsers = const [],\n    this.isLoading = false,\n    this.errorMessage = '',\n    this.shareLink = '',\n    this.generalAccessRole,\n    this.linkCopied = false,\n    this.sectionType = SharedSectionType.private,\n    this.initialResult,\n    this.shareResult,\n    this.removeResult,\n    this.updateAccessLevelResult,\n    this.turnIntoMemberResult,\n    this.hasClickedUpgradeToPro = false,\n  });\n\n  final UserProfilePB? currentUser;\n  final SharedUsers users;\n  final SharedUsers availableUsers;\n  final bool isLoading;\n  final String errorMessage;\n  final String shareLink;\n  final ShareAccessLevel? generalAccessRole;\n  final bool linkCopied;\n  final SharedSectionType sectionType;\n  final FlowyResult<void, FlowyError>? initialResult;\n  final FlowyResult<void, FlowyError>? shareResult;\n  final FlowyResult<void, FlowyError>? removeResult;\n  final FlowyResult<void, FlowyError>? updateAccessLevelResult;\n  final FlowyResult<void, FlowyError>? turnIntoMemberResult;\n  final bool hasClickedUpgradeToPro;\n\n  ShareTabState copyWith({\n    UserProfilePB? currentUser,\n    SharedUsers? users,\n    SharedUsers? availableUsers,\n    bool? isLoading,\n    String? errorMessage,\n    String? shareLink,\n    ShareAccessLevel? generalAccessRole,\n    bool? linkCopied,\n    SharedSectionType? sectionType,\n    FlowyResult<void, FlowyError>? initialResult,\n    FlowyResult<void, FlowyError>? shareResult,\n    FlowyResult<void, FlowyError>? removeResult,\n    FlowyResult<void, FlowyError>? updateAccessLevelResult,\n    FlowyResult<void, FlowyError>? turnIntoMemberResult,\n    bool? hasClickedUpgradeToPro,\n  }) {\n    return ShareTabState(\n      currentUser: currentUser ?? this.currentUser,\n      users: users ?? this.users,\n      availableUsers: availableUsers ?? this.availableUsers,\n      isLoading: isLoading ?? this.isLoading,\n      errorMessage: errorMessage ?? this.errorMessage,\n      shareLink: shareLink ?? this.shareLink,\n      generalAccessRole: generalAccessRole ?? this.generalAccessRole,\n      linkCopied: linkCopied ?? this.linkCopied,\n      sectionType: sectionType ?? this.sectionType,\n      initialResult: initialResult,\n      shareResult: shareResult,\n      removeResult: removeResult,\n      updateAccessLevelResult: updateAccessLevelResult,\n      turnIntoMemberResult: turnIntoMemberResult,\n      hasClickedUpgradeToPro:\n          hasClickedUpgradeToPro ?? this.hasClickedUpgradeToPro,\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n    return other is ShareTabState &&\n        other.currentUser == currentUser &&\n        other.users == users &&\n        other.availableUsers == availableUsers &&\n        other.isLoading == isLoading &&\n        other.errorMessage == errorMessage &&\n        other.shareLink == shareLink &&\n        other.generalAccessRole == generalAccessRole &&\n        other.linkCopied == linkCopied &&\n        other.sectionType == sectionType &&\n        other.initialResult == initialResult &&\n        other.shareResult == shareResult &&\n        other.removeResult == removeResult &&\n        other.updateAccessLevelResult == updateAccessLevelResult &&\n        other.turnIntoMemberResult == turnIntoMemberResult &&\n        other.hasClickedUpgradeToPro == hasClickedUpgradeToPro;\n  }\n\n  @override\n  int get hashCode {\n    return Object.hash(\n      currentUser,\n      users,\n      availableUsers,\n      isLoading,\n      errorMessage,\n      shareLink,\n      generalAccessRole,\n      linkCopied,\n      sectionType,\n      initialResult,\n      shareResult,\n      removeResult,\n      updateAccessLevelResult,\n      turnIntoMemberResult,\n      hasClickedUpgradeToPro,\n    );\n  }\n\n  @override\n  String toString() {\n    return 'ShareTabState(currentUser: $currentUser, users: $users, availableUsers: $availableUsers, isLoading: $isLoading, errorMessage: $errorMessage, shareLink: $shareLink, generalAccessRole: $generalAccessRole, shareSectionType: $SharedSectionType, linkCopied: $linkCopied, initialResult: $initialResult, shareResult: $shareResult, removeResult: $removeResult, updateAccessLevelResult: $updateAccessLevelResult, turnIntoMemberResult: $turnIntoMemberResult, hasClickedUpgradeToPro: $hasClickedUpgradeToPro)';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/presentation/share_tab.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/features/share_tab/data/models/shared_group.dart';\nimport 'package:appflowy/features/share_tab/logic/share_tab_bloc.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/copy_link_widget.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/general_access_section.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/people_with_access_section.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/share_with_user_widget.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/upgrade_to_pro_widget.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass ShareTab extends StatefulWidget {\n  const ShareTab({\n    super.key,\n    required this.workspaceId,\n    required this.pageId,\n    required this.workspaceName,\n    required this.workspaceIcon,\n    required this.isInProPlan,\n    required this.onUpgradeToPro,\n  });\n\n  final String workspaceId;\n  final String pageId;\n\n  // these 2 values should be provided by the share tab bloc\n  final String workspaceName;\n  final String workspaceIcon;\n\n  final bool isInProPlan;\n  final VoidCallback onUpgradeToPro;\n\n  @override\n  State<ShareTab> createState() => _ShareTabState();\n}\n\nclass _ShareTabState extends State<ShareTab> {\n  final TextEditingController controller = TextEditingController();\n  late final ShareTabBloc shareTabBloc;\n\n  @override\n  void initState() {\n    super.initState();\n\n    shareTabBloc = context.read<ShareTabBloc>();\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    shareTabBloc.add(ShareTabEvent.clearState());\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return BlocConsumer<ShareTabBloc, ShareTabState>(\n      listener: (context, state) {\n        _onListenShareWithUserState(context, state);\n      },\n      builder: (context, state) {\n        if (state.isLoading) {\n          return const SizedBox.shrink();\n        }\n\n        final currentUser = state.currentUser;\n        final accessLevel = state.users\n            .firstWhereOrNull(\n              (user) => user.email == currentUser?.email,\n            )\n            ?.accessLevel;\n        final isFullAccess = accessLevel == ShareAccessLevel.fullAccess;\n\n        return Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            // share page with user by email\n            // only user with full access can invite others\n\n            VSpace(theme.spacing.l),\n            ShareWithUserWidget(\n              controller: controller,\n              disabled: !isFullAccess,\n              onInvite: (emails) => _onSharePageWithUser(\n                context,\n                emails: emails,\n                accessLevel: ShareAccessLevel.readOnly,\n              ),\n            ),\n\n            if (!widget.isInProPlan && !state.hasClickedUpgradeToPro) ...[\n              UpgradeToProWidget(\n                onClose: () {\n                  context.read<ShareTabBloc>().add(\n                        ShareTabEvent.upgradeToProClicked(),\n                      );\n                },\n                onUpgrade: widget.onUpgradeToPro,\n              ),\n            ],\n\n            // shared users\n            if (state.users.isNotEmpty) ...[\n              VSpace(theme.spacing.l),\n              PeopleWithAccessSection(\n                isInPublicPage: state.sectionType == SharedSectionType.public,\n                currentUserEmail: state.currentUser?.email ?? '',\n                users: state.users,\n                callbacks: _buildPeopleWithAccessSectionCallbacks(context),\n              ),\n            ],\n\n            // general access\n            if (state.sectionType == SharedSectionType.public) ...[\n              VSpace(theme.spacing.m),\n              GeneralAccessSection(\n                group: SharedGroup(\n                  id: widget.workspaceId,\n                  name: widget.workspaceName,\n                  icon: widget.workspaceIcon,\n                ),\n              ),\n            ],\n\n            // copy link\n            VSpace(theme.spacing.xl),\n            CopyLinkWidget(shareLink: state.shareLink),\n            VSpace(theme.spacing.m),\n          ],\n        );\n      },\n    );\n  }\n\n  void _onSharePageWithUser(\n    BuildContext context, {\n    required List<String> emails,\n    required ShareAccessLevel accessLevel,\n  }) {\n    context.read<ShareTabBloc>().add(\n          ShareTabEvent.inviteUsers(emails: emails, accessLevel: accessLevel),\n        );\n  }\n\n  PeopleWithAccessSectionCallbacks _buildPeopleWithAccessSectionCallbacks(\n    BuildContext context,\n  ) {\n    return PeopleWithAccessSectionCallbacks(\n      onSelectAccessLevel: (user, accessLevel) {\n        context.read<ShareTabBloc>().add(\n              ShareTabEvent.updateUserAccessLevel(\n                email: user.email,\n                accessLevel: accessLevel,\n              ),\n            );\n      },\n      onTurnIntoMember: (user) {\n        context.read<ShareTabBloc>().add(\n              ShareTabEvent.convertToMember(email: user.email),\n            );\n      },\n      onRemoveAccess: (user) {\n        // show a dialog to confirm the action when removing self access\n        final theme = AppFlowyTheme.of(context);\n        final shareTabBloc = context.read<ShareTabBloc>();\n        final removingSelf =\n            user.email == shareTabBloc.state.currentUser?.email;\n        if (removingSelf) {\n          showConfirmDialog(\n            context: context,\n            title: 'Remove your own access',\n            titleStyle: theme.textStyle.body.standard(\n              color: theme.textColorScheme.primary,\n            ),\n            description: '',\n            style: ConfirmPopupStyle.cancelAndOk,\n            confirmLabel: 'Remove',\n            onConfirm: (_) {\n              shareTabBloc.add(\n                ShareTabEvent.removeUsers(emails: [user.email]),\n              );\n            },\n          );\n        } else {\n          shareTabBloc.add(\n            ShareTabEvent.removeUsers(emails: [user.email]),\n          );\n        }\n      },\n    );\n  }\n\n  void _onListenShareWithUserState(\n    BuildContext context,\n    ShareTabState state,\n  ) {\n    final shareResult = state.shareResult;\n    if (shareResult != null) {\n      shareResult.fold((success) {\n        // clear the controller to avoid showing the previous emails\n        controller.clear();\n\n        showToastNotification(\n          message: LocaleKeys.shareTab_invitationSent.tr(),\n        );\n      }, (error) {\n        String message;\n        switch (error.code) {\n          case ErrorCode.InvalidGuest:\n            message = LocaleKeys.shareTab_emailAlreadyInList.tr();\n            break;\n          case ErrorCode.FreePlanGuestLimitExceeded:\n            message = LocaleKeys.shareTab_upgradeToProToInviteGuests.tr();\n            break;\n          case ErrorCode.PaidPlanGuestLimitExceeded:\n            message = LocaleKeys.shareTab_maxGuestsReached.tr();\n            break;\n          default:\n            message = error.msg;\n        }\n        showToastNotification(\n          message: message,\n          type: ToastificationType.error,\n        );\n      });\n    }\n\n    final removeResult = state.removeResult;\n    if (removeResult != null) {\n      removeResult.fold((success) {\n        showToastNotification(\n          message: LocaleKeys.shareTab_removedGuestSuccessfully.tr(),\n        );\n      }, (error) {\n        showToastNotification(\n          message: error.msg,\n          type: ToastificationType.error,\n        );\n      });\n    }\n\n    final updateAccessLevelResult = state.updateAccessLevelResult;\n    if (updateAccessLevelResult != null) {\n      updateAccessLevelResult.fold((success) {\n        showToastNotification(\n          message: LocaleKeys.shareTab_updatedAccessLevelSuccessfully.tr(),\n        );\n      }, (error) {\n        showToastNotification(\n          message: error.msg,\n          type: ToastificationType.error,\n        );\n      });\n    }\n\n    final turnIntoMemberResult = state.turnIntoMemberResult;\n    if (turnIntoMemberResult != null) {\n      turnIntoMemberResult.fold((success) {\n        showToastNotification(\n          message: LocaleKeys.shareTab_turnedIntoMemberSuccessfully.tr(),\n        );\n      }, (error) {\n        showToastNotification(\n          message: error.msg,\n          type: ToastificationType.error,\n        );\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/access_level_list_widget.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nclass AccessLevelListCallbacks {\n  const AccessLevelListCallbacks({\n    required this.onSelectAccessLevel,\n    required this.onTurnIntoMember,\n    required this.onRemoveAccess,\n  });\n\n  factory AccessLevelListCallbacks.none() {\n    return AccessLevelListCallbacks(\n      onSelectAccessLevel: (_) {},\n      onTurnIntoMember: () {},\n      onRemoveAccess: () {},\n    );\n  }\n\n  /// Callback when an access level is selected\n  final void Function(ShareAccessLevel accessLevel) onSelectAccessLevel;\n\n  /// Callback when the \"Turn into Member\" option is selected\n  final VoidCallback onTurnIntoMember;\n\n  /// Callback when the \"Remove access\" option is selected\n  final VoidCallback onRemoveAccess;\n\n  /// Copy\n  AccessLevelListCallbacks copyWith({\n    VoidCallback? onRemoveAccess,\n    VoidCallback? onTurnIntoMember,\n    void Function(ShareAccessLevel accessLevel)? onSelectAccessLevel,\n  }) {\n    return AccessLevelListCallbacks(\n      onRemoveAccess: onRemoveAccess ?? this.onRemoveAccess,\n      onTurnIntoMember: onTurnIntoMember ?? this.onTurnIntoMember,\n      onSelectAccessLevel: onSelectAccessLevel ?? this.onSelectAccessLevel,\n    );\n  }\n}\n\nenum AdditionalUserManagementOptions {\n  turnIntoMember,\n  removeAccess,\n}\n\n/// A widget that displays a list of access levels for sharing.\n///\n/// This widget is used in a popover to allow users to select different access levels\n/// for shared content, as well as options to turn users into members or remove access.\nclass AccessLevelListWidget extends StatelessWidget {\n  const AccessLevelListWidget({\n    super.key,\n    required this.selectedAccessLevel,\n    required this.callbacks,\n    required this.supportedAccessLevels,\n    required this.additionalUserManagementOptions,\n  });\n\n  /// The currently selected access level\n  final ShareAccessLevel selectedAccessLevel;\n\n  /// Callbacks\n  final AccessLevelListCallbacks callbacks;\n\n  /// Supported access levels\n  final List<ShareAccessLevel> supportedAccessLevels;\n\n  /// Additional user management options\n  final List<AdditionalUserManagementOptions> additionalUserManagementOptions;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return AFMenu(\n      width: supportedAccessLevels.isNotEmpty ? 260 : 160,\n      children: [\n        // Display all available access level options\n        if (supportedAccessLevels.isNotEmpty) ...[\n          ...supportedAccessLevels.map(\n            (accessLevel) => _buildAccessLevelItem(\n              context,\n              accessLevel: accessLevel,\n              onTap: () => callbacks.onSelectAccessLevel(accessLevel),\n            ),\n          ),\n          AFDivider(spacing: theme.spacing.m),\n        ],\n\n        // Additional user management options\n        if (additionalUserManagementOptions\n            .contains(AdditionalUserManagementOptions.turnIntoMember))\n          AFTextMenuItem(\n            title: LocaleKeys.shareTab_turnIntoMember.tr(),\n            onTap: callbacks.onTurnIntoMember,\n          ),\n        if (additionalUserManagementOptions\n            .contains(AdditionalUserManagementOptions.removeAccess))\n          AFTextMenuItem(\n            title: LocaleKeys.shareTab_removeAccess.tr(),\n            titleColor: theme.textColorScheme.error,\n            onTap: callbacks.onRemoveAccess,\n          ),\n      ],\n    );\n  }\n\n  Widget _buildAccessLevelItem(\n    BuildContext context, {\n    required ShareAccessLevel accessLevel,\n    required VoidCallback onTap,\n  }) {\n    return AFTextMenuItem(\n      title: accessLevel.title,\n      subtitle: accessLevel.subtitle,\n\n      showSelectedBackground: false,\n      selected: selectedAccessLevel == accessLevel,\n      leading: FlowySvg(\n        accessLevel.icon,\n      ),\n      // Show a checkmark icon for the currently selected access level\n      trailing: selectedAccessLevel == accessLevel\n          ? FlowySvg(\n              FlowySvgs.m_blue_check_s,\n              blendMode: null,\n            )\n          : null,\n      onTap: onTap,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/copy_link_widget.dart",
    "content": "import 'package:appflowy/features/share_tab/logic/share_tab_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass CopyLinkWidget extends StatelessWidget {\n  const CopyLinkWidget({\n    super.key,\n    required this.shareLink,\n  });\n\n  final String shareLink;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Container(\n      padding: EdgeInsets.symmetric(\n        vertical: theme.spacing.m,\n        horizontal: theme.spacing.l,\n      ),\n      decoration: BoxDecoration(\n        color: theme.surfaceContainerColorScheme.layer01,\n        borderRadius: BorderRadius.circular(theme.spacing.m),\n        border: Border.all(\n          color: theme.borderColorScheme.primary,\n        ),\n      ),\n      child: Row(\n        children: [\n          FlowySvg(\n            FlowySvgs.toolbar_link_m,\n          ),\n          HSpace(theme.spacing.m),\n          Expanded(\n            child: Text(\n              LocaleKeys.shareTab_peopleAboveCanAccessWithTheLink.tr(),\n              style: theme.textStyle.caption.standard(\n                color: theme.textColorScheme.primary,\n              ),\n              overflow: TextOverflow.ellipsis,\n            ),\n          ),\n          AFOutlinedTextButton.normal(\n            text: LocaleKeys.shareTab_copyLink.tr(),\n            size: AFButtonSize.l,\n            padding: EdgeInsets.symmetric(\n              horizontal: theme.spacing.l,\n              vertical: theme.spacing.s,\n            ),\n            backgroundColor: (context, isHovering, disabled) {\n              final theme = AppFlowyTheme.of(context);\n              if (disabled) {\n                return theme.fillColorScheme.content;\n              }\n              if (isHovering) {\n                return theme.fillColorScheme.contentHover;\n              }\n              return theme.surfaceColorScheme.layer02;\n            },\n            onTap: () {\n              context.read<ShareTabBloc>().add(\n                    ShareTabEvent.copyShareLink(link: shareLink),\n                  );\n\n              if (FlowyRunner.currentMode.isUnitTest) {\n                return;\n              }\n\n              showToastNotification(\n                message: LocaleKeys.shareTab_copiedLinkToClipboard.tr(),\n              );\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/edit_access_level_widget.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/access_level_list_widget.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\n\nclass EditAccessLevelWidget extends StatefulWidget {\n  const EditAccessLevelWidget({\n    super.key,\n    required this.callbacks,\n    required this.selectedAccessLevel,\n    required this.supportedAccessLevels,\n    required this.additionalUserManagementOptions,\n    this.disabled = false,\n  });\n\n  /// Callbacks\n  final AccessLevelListCallbacks callbacks;\n\n  /// The currently selected access level\n  final ShareAccessLevel selectedAccessLevel;\n\n  /// Whether the widget is disabled\n  final bool disabled;\n\n  /// Supported access levels\n  final List<ShareAccessLevel> supportedAccessLevels;\n\n  /// Additional user management options\n  final List<AdditionalUserManagementOptions> additionalUserManagementOptions;\n\n  @override\n  State<EditAccessLevelWidget> createState() => _EditAccessLevelWidgetState();\n}\n\nclass _EditAccessLevelWidgetState extends State<EditAccessLevelWidget> {\n  final popoverController = AFPopoverController();\n\n  @override\n  void dispose() {\n    popoverController.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return AFPopover(\n      padding: EdgeInsets.zero,\n      decoration: BoxDecoration(), // the access level widget has a border\n      controller: popoverController,\n      popover: (_) {\n        return AccessLevelListWidget(\n          selectedAccessLevel: widget.selectedAccessLevel,\n          supportedAccessLevels: widget.supportedAccessLevels,\n          additionalUserManagementOptions:\n              widget.additionalUserManagementOptions,\n          callbacks: widget.callbacks.copyWith(\n            onSelectAccessLevel: (accessLevel) {\n              widget.callbacks.onSelectAccessLevel(accessLevel);\n\n              popoverController.hide();\n            },\n            onRemoveAccess: () {\n              widget.callbacks.onRemoveAccess();\n\n              popoverController.hide();\n            },\n          ),\n        );\n      },\n      child: AFGhostButton.normal(\n        disabled: widget.disabled,\n        onTap: () {\n          popoverController.show();\n        },\n        padding: EdgeInsets.symmetric(\n          vertical: theme.spacing.s,\n          horizontal: theme.spacing.l,\n        ),\n        builder: (context, isHovering, disabled) {\n          return Row(\n            children: [\n              Text(\n                widget.selectedAccessLevel.title,\n                style: theme.textStyle.body.standard(\n                  color: disabled\n                      ? theme.textColorScheme.secondary\n                      : theme.textColorScheme.primary,\n                ),\n              ),\n              HSpace(theme.spacing.xs),\n              FlowySvg(\n                FlowySvgs.arrow_down_s,\n                color: theme.textColorScheme.secondary,\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/general_access_section.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/shared_group.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/shared_group_widget.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nclass GeneralAccessSection extends StatelessWidget {\n  const GeneralAccessSection({\n    super.key,\n    required this.group,\n  });\n\n  final SharedGroup group;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return AFMenuSection(\n      title: LocaleKeys.shareTab_generalAccess.tr(),\n      padding: EdgeInsets.symmetric(\n        vertical: theme.spacing.xs,\n        horizontal: theme.spacing.m,\n      ),\n      children: [\n        SharedGroupWidget(\n          group: group,\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/guest_tag.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nclass GuestTag extends StatelessWidget {\n  const GuestTag({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Container(\n      padding: EdgeInsets.only(\n        left: theme.spacing.m,\n        right: theme.spacing.m,\n        bottom: 2,\n      ),\n      decoration: BoxDecoration(\n        color: theme.fillColorScheme.warningLight,\n        borderRadius: BorderRadius.circular(theme.spacing.s),\n      ),\n      child: Text(\n        LocaleKeys.shareTab_guest.tr(),\n        style: theme.textStyle.caption\n            .standard(\n              color: theme.textColorScheme.warning,\n            )\n            .copyWith(\n              height: 16.0 / 12.0,\n            ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/people_with_access_section.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/access_level_list_widget.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/shared_user_widget.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/material.dart';\n\nclass PeopleWithAccessSectionCallbacks {\n  const PeopleWithAccessSectionCallbacks({\n    required this.onRemoveAccess,\n    required this.onTurnIntoMember,\n    required this.onSelectAccessLevel,\n  });\n\n  factory PeopleWithAccessSectionCallbacks.none() {\n    return PeopleWithAccessSectionCallbacks(\n      onSelectAccessLevel: (_, __) {},\n      onTurnIntoMember: (_) {},\n      onRemoveAccess: (_) {},\n    );\n  }\n\n  /// Callback when an access level is selected\n  final void Function(SharedUser user, ShareAccessLevel accessLevel)\n      onSelectAccessLevel;\n\n  /// Callback when the \"Turn into Member\" option is selected\n  final void Function(SharedUser user) onTurnIntoMember;\n\n  /// Callback when the \"Remove access\" option is selected\n  final void Function(SharedUser user) onRemoveAccess;\n}\n\nclass PeopleWithAccessSection extends StatelessWidget {\n  const PeopleWithAccessSection({\n    super.key,\n    required this.currentUserEmail,\n    required this.users,\n    required this.isInPublicPage,\n    this.callbacks,\n  });\n\n  final String currentUserEmail;\n  final SharedUsers users;\n  final bool isInPublicPage;\n  final PeopleWithAccessSectionCallbacks? callbacks;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final currentUser = users.firstWhereOrNull(\n      (user) => user.email == currentUserEmail,\n    );\n\n    return AFMenuSection(\n      title: 'People with access',\n      constraints: BoxConstraints(\n        maxHeight: 240,\n      ),\n      padding: EdgeInsets.symmetric(\n        vertical: theme.spacing.xs,\n        horizontal: theme.spacing.m,\n      ),\n      children: users.map((user) {\n        if (currentUser == null) {\n          return const SizedBox.shrink();\n        }\n\n        return SharedUserWidget(\n          user: user,\n          currentUser: currentUser,\n          isInPublicPage: isInPublicPage,\n          callbacks: AccessLevelListCallbacks(\n            onRemoveAccess: () => callbacks?.onRemoveAccess.call(user),\n            onTurnIntoMember: () => callbacks?.onTurnIntoMember.call(user),\n            onSelectAccessLevel: (accessLevel) =>\n                callbacks?.onSelectAccessLevel.call(user, accessLevel),\n          ),\n        );\n      }).toList(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/share_with_user_widget.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:string_validator/string_validator.dart';\n\nclass ShareWithUserWidget extends StatefulWidget {\n  const ShareWithUserWidget({\n    super.key,\n    required this.onInvite,\n    this.controller,\n    this.disabled = false,\n    this.tooltip,\n  });\n\n  final TextEditingController? controller;\n  final void Function(List<String> emails) onInvite;\n  final bool disabled;\n  final String? tooltip;\n\n  @override\n  State<ShareWithUserWidget> createState() => _ShareWithUserWidgetState();\n}\n\nclass _ShareWithUserWidgetState extends State<ShareWithUserWidget> {\n  late final TextEditingController effectiveController;\n  bool isButtonEnabled = false;\n\n  @override\n  void initState() {\n    super.initState();\n\n    effectiveController = widget.controller ?? TextEditingController();\n    effectiveController.addListener(_onTextChanged);\n  }\n\n  @override\n  void dispose() {\n    if (widget.controller == null) {\n      effectiveController.dispose();\n    }\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    final Widget child = Row(\n      children: [\n        Expanded(\n          child: AFTextField(\n            controller: effectiveController,\n            size: AFTextFieldSize.m,\n            hintText: LocaleKeys.shareTab_inviteByEmail.tr(),\n          ),\n        ),\n        HSpace(theme.spacing.s),\n        AFFilledTextButton.primary(\n          text: LocaleKeys.shareTab_invite.tr(),\n          disabled: !isButtonEnabled,\n          onTap: () {\n            widget.onInvite(effectiveController.text.trim().split(','));\n          },\n        ),\n      ],\n    );\n\n    if (widget.disabled) {\n      return FlowyTooltip(\n        message:\n            widget.tooltip ?? LocaleKeys.shareTab_onlyFullAccessCanInvite.tr(),\n        child: IgnorePointer(\n          child: child,\n        ),\n      );\n    }\n\n    return child;\n  }\n\n  void _onTextChanged() {\n    setState(() {\n      final texts = effectiveController.text.trim().split(',');\n      isButtonEnabled = texts.isNotEmpty && texts.every(isEmail);\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/shared_group_widget.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/share_access_level.dart';\nimport 'package:appflowy/features/share_tab/data/models/shared_group.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/access_level_list_widget.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/edit_access_level_widget.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nclass SharedGroupWidget extends StatelessWidget {\n  const SharedGroupWidget({\n    super.key,\n    required this.group,\n  });\n\n  final SharedGroup group;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return AFMenuItem(\n      padding: EdgeInsets.symmetric(\n        vertical: theme.spacing.s,\n        horizontal: theme.spacing.m,\n      ),\n      leading: _buildLeading(context),\n      title: _buildTitle(context),\n      subtitle: _buildSubtitle(context),\n      trailing: _buildTrailing(context),\n      onTap: () {},\n    );\n  }\n\n  Widget _buildLeading(BuildContext context) {\n    return WorkspaceIcon(\n      isEditable: false,\n      workspaceIcon: group.icon,\n      workspaceName: group.name,\n      iconSize: 32.0,\n      emojiSize: 24.0,\n      fontSize: 16.0,\n      onSelected: (r) {},\n      borderRadius: 8.0,\n      showBorder: false,\n      figmaLineHeight: 24.0,\n    );\n  }\n\n  Widget _buildTitle(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Row(\n      children: [\n        Flexible(\n          child: Text(\n            LocaleKeys.shareTab_anyoneAtWorkspace.tr(\n              namedArgs: {\n                'workspace': group.name,\n              },\n            ),\n            style: theme.textStyle.body.standard(\n              color: theme.textColorScheme.primary,\n            ),\n            overflow: TextOverflow.ellipsis,\n          ),\n        ),\n        // HSpace(theme.spacing.xs),\n        // FlowySvg(\n        //   FlowySvgs.arrow_down_s,\n        //   color: theme.textColorScheme.secondary,\n        // ),\n      ],\n    );\n  }\n\n  Widget _buildSubtitle(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Text(\n      LocaleKeys.shareTab_anyoneInGroupWithLinkCanEdit.tr(),\n      textAlign: TextAlign.left,\n      style: theme.textStyle.caption.standard(\n        color: theme.textColorScheme.secondary,\n      ),\n    );\n  }\n\n  Widget _buildTrailing(BuildContext context) {\n    return EditAccessLevelWidget(\n      disabled: true,\n      supportedAccessLevels: ShareAccessLevel.values,\n      selectedAccessLevel: ShareAccessLevel.readAndWrite,\n      callbacks: AccessLevelListCallbacks.none(),\n      additionalUserManagementOptions: [],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/shared_user_widget.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/access_level_list_widget.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/edit_access_level_widget.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/guest_tag.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/turn_into_member_widget.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\n/// Widget to display a single shared user row as per the UI design, using AFMenuItem.\nclass SharedUserWidget extends StatelessWidget {\n  const SharedUserWidget({\n    super.key,\n    required this.user,\n    required this.currentUser,\n    required this.isInPublicPage,\n    this.callbacks,\n  });\n\n  final SharedUser user;\n  final SharedUser currentUser;\n  final AccessLevelListCallbacks? callbacks;\n  final bool isInPublicPage;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return AFMenuItem(\n      padding: EdgeInsets.symmetric(\n        vertical: theme.spacing.s,\n        horizontal: theme.spacing.m,\n      ),\n      leading: AFAvatar(\n        name: user.name,\n        url: user.avatarUrl,\n      ),\n      title: _buildTitle(context),\n      subtitle: _buildSubtitle(context),\n      trailing: _buildTrailing(context),\n      onTap: () {\n        // callbacks?.onSelectAccessLevel.call(user, user.accessLevel);\n      },\n    );\n  }\n\n  Widget _buildTitle(\n    BuildContext context,\n  ) {\n    final theme = AppFlowyTheme.of(context);\n    final isCurrentUser = user.email == currentUser.email;\n\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        Flexible(\n          child: Text(\n            user.name,\n            style: theme.textStyle.body.standard(\n              color: theme.textColorScheme.primary,\n            ),\n            overflow: TextOverflow.ellipsis,\n          ),\n        ),\n        // if the user is the current user, show '(You)'\n        if (isCurrentUser) ...[\n          HSpace(theme.spacing.xs),\n          Flexible(\n            child: Text(\n              LocaleKeys.shareTab_you.tr(),\n              style: theme.textStyle.caption.standard(\n                color: theme.textColorScheme.secondary,\n              ),\n              overflow: TextOverflow.ellipsis,\n            ),\n          ),\n        ],\n        // if the user is a guest, show 'Guest'\n        if (user.role == ShareRole.guest) ...[\n          HSpace(theme.spacing.m),\n          const GuestTag(),\n        ],\n      ],\n    );\n  }\n\n  Widget _buildSubtitle(\n    BuildContext context,\n  ) {\n    final theme = AppFlowyTheme.of(context);\n    return Text(\n      user.email,\n      style: theme.textStyle.caption.standard(\n        color: theme.textColorScheme.secondary,\n      ),\n    );\n  }\n\n  Widget _buildTrailing(BuildContext context) {\n    final isCurrentUser = user.email == currentUser.email;\n    final theme = AppFlowyTheme.of(context);\n    final currentAccessLevel = currentUser.accessLevel;\n\n    Widget disabledAccessButton() => AFGhostTextButton.disabled(\n          text: user.accessLevel.title,\n          textStyle: theme.textStyle.body.standard(\n            color: theme.textColorScheme.secondary,\n          ),\n        );\n\n    Widget editAccessWidget(List<ShareAccessLevel> supported) =>\n        EditAccessLevelWidget(\n          selectedAccessLevel: user.accessLevel,\n          supportedAccessLevels: supported,\n          additionalUserManagementOptions: [\n            AdditionalUserManagementOptions.removeAccess,\n          ],\n          callbacks: callbacks ?? AccessLevelListCallbacks.none(),\n        );\n\n    // In public page, member/owner permissions are fixed\n    if (isInPublicPage &&\n        (user.role == ShareRole.member || user.role == ShareRole.owner)) {\n      return disabledAccessButton();\n    }\n\n    // Full access user can turn a guest into a member\n    if (user.role == ShareRole.guest &&\n        currentAccessLevel == ShareAccessLevel.fullAccess) {\n      return Row(\n        children: [\n          TurnIntoMemberWidget(\n            onTap: () => callbacks?.onTurnIntoMember.call(),\n          ),\n          editAccessWidget([\n            ShareAccessLevel.readOnly,\n            ShareAccessLevel.readAndWrite,\n          ]),\n        ],\n      );\n    }\n\n    // Self-management\n    if (isCurrentUser) {\n      if (currentAccessLevel == ShareAccessLevel.readOnly ||\n          currentAccessLevel == ShareAccessLevel.readAndWrite) {\n        // Can only remove self\n        return editAccessWidget([]);\n      } else if (currentAccessLevel == ShareAccessLevel.fullAccess) {\n        // Full access user cannot change own access\n        return disabledAccessButton();\n      }\n    }\n\n    // Managing others\n    if (currentAccessLevel == ShareAccessLevel.readOnly ||\n        currentAccessLevel == ShareAccessLevel.readAndWrite) {\n      // Cannot change others' access\n      return disabledAccessButton();\n    } else {\n      // Full access user can manage others\n      final supportedAccessLevels = [\n        ShareAccessLevel.readOnly,\n        ShareAccessLevel.readAndWrite,\n      ];\n      return editAccessWidget(supportedAccessLevels);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/turn_into_member_widget.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flutter/material.dart';\n\nclass TurnIntoMemberWidget extends StatelessWidget {\n  const TurnIntoMemberWidget({\n    super.key,\n    required this.onTap,\n  });\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return FlowyTooltip(\n      message: LocaleKeys.shareTab_turnIntoMember.tr(),\n      child: AFGhostButton.normal(\n        onTap: onTap,\n        padding: EdgeInsets.all(theme.spacing.s),\n        builder: (context, isHovering, disabled) {\n          return FlowySvg(FlowySvgs.turn_into_member_m);\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/upgrade_to_pro_widget.dart",
    "content": "import 'package:appflowy/features/share_tab/logic/share_tab_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass UpgradeToProWidget extends StatelessWidget {\n  const UpgradeToProWidget({\n    super.key,\n    required this.onUpgrade,\n    required this.onClose,\n  });\n\n  final VoidCallback onClose;\n  final VoidCallback onUpgrade;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Container(\n      decoration: BoxDecoration(\n        color: Color(0x129327ff),\n        borderRadius: BorderRadius.circular(theme.borderRadius.m),\n      ),\n      padding: EdgeInsets.symmetric(\n        vertical: theme.spacing.m,\n        horizontal: theme.spacing.l,\n      ),\n      margin: EdgeInsets.only(\n        top: theme.spacing.l,\n      ),\n      child: Row(\n        children: [\n          FlowySvg(\n            FlowySvgs.upgrade_pro_crown_m,\n            blendMode: null,\n          ),\n          HSpace(\n            theme.spacing.m,\n          ),\n          RichText(\n            text: TextSpan(\n              children: [\n                TextSpan(\n                  text: LocaleKeys.shareTab_upgrade.tr(),\n                  style: theme.textStyle.caption.standard().copyWith(\n                        color: theme.textColorScheme.featured,\n                        decoration: TextDecoration.underline,\n                      ),\n                  recognizer: TapGestureRecognizer()\n                    ..onTap = () {\n                      onUpgrade();\n                    },\n                  mouseCursor: SystemMouseCursors.click,\n                ),\n                TextSpan(\n                  text: LocaleKeys.shareTab_toProPlanToInviteGuests.tr(),\n                  style: theme.textStyle.caption.standard().copyWith(\n                        color: theme.textColorScheme.featured,\n                      ),\n                ),\n              ],\n            ),\n          ),\n          const Spacer(),\n          AFGhostButton.normal(\n            size: AFButtonSize.s,\n            padding: EdgeInsets.all(theme.spacing.xs),\n            onTap: () {\n              context\n                  .read<ShareTabBloc>()\n                  .add(ShareTabEvent.upgradeToProClicked());\n              onClose();\n            },\n            builder: (context, isHovering, disabled) => FlowySvg(\n              FlowySvgs.upgrade_to_pro_close_m,\n              size: const Size.square(20),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/data/repositories/local_shared_pages_repository_impl.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/share_access_level.dart';\nimport 'package:appflowy/features/shared_section/data/repositories/shared_pages_repository.dart';\nimport 'package:appflowy/features/shared_section/models/shared_page.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\n// Move this file to test folder\nclass LocalSharedPagesRepositoryImpl implements SharedPagesRepository {\n  @override\n  Future<FlowyResult<SharedPages, FlowyError>> getSharedPages() async {\n    final pages = [\n      SharedPage(\n        view: ViewPB()\n          ..id = '1'\n          ..name = 'Welcome Page',\n        accessLevel: ShareAccessLevel.fullAccess,\n      ),\n      SharedPage(\n        view: ViewPB()\n          ..id = '2'\n          ..name = 'Project Plan',\n        accessLevel: ShareAccessLevel.readAndWrite,\n      ),\n      SharedPage(\n        view: ViewPB()\n          ..id = '3'\n          ..name = 'Readme',\n        accessLevel: ShareAccessLevel.readOnly,\n      ),\n    ];\n    return FlowyResult.success(pages);\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> leaveSharedPage(String pageId) async {\n    return FlowyResult.success(null);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/data/repositories/rust_shared_pages_repository_impl.dart",
    "content": "import 'package:appflowy/features/shared_section/data/repositories/shared_pages_repository.dart';\nimport 'package:appflowy/features/shared_section/models/shared_page.dart';\nimport 'package:appflowy/features/util/extensions.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass RustSharePagesRepositoryImpl implements SharedPagesRepository {\n  @override\n  Future<FlowyResult<SharedPages, FlowyError>> getSharedPages() async {\n    final result = await FolderEventGetSharedViews().send();\n    return result.fold(\n      (success) {\n        final sharedPages = success.sharedPages;\n\n        Log.debug('get shared pages success, len: ${sharedPages.length}');\n\n        return FlowyResult.success(sharedPages);\n      },\n      (error) {\n        Log.error('failed to get shared pages, error: $error');\n\n        return FlowyResult.failure(error);\n      },\n    );\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> leaveSharedPage(String pageId) async {\n    final user = await UserEventGetUserProfile().send();\n    final userEmail = user.fold(\n      (success) => success.email,\n      (error) => null,\n    );\n\n    if (userEmail == null) {\n      return FlowyResult.failure(FlowyError(msg: 'User email is null'));\n    }\n\n    final request = RemoveUserFromSharedPagePayloadPB(\n      viewId: pageId,\n      emails: [userEmail],\n    );\n    final result = await FolderEventRemoveUserFromSharedPage(request).send();\n\n    return result.fold(\n      (success) {\n        Log.debug('remove user($userEmail) from shared page($pageId)');\n\n        return FlowySuccess(success);\n      },\n      (failure) {\n        Log.error(\n          'remove user($userEmail) from shared page($pageId): $failure',\n        );\n\n        return FlowyFailure(failure);\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/data/repositories/shared_pages_repository.dart",
    "content": "import 'package:appflowy/features/shared_section/models/shared_page.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\n/// Abstract repository for sharing pages with users.\n///\n/// For example, we're using rust events now, but we can still use the http api\n/// for the future.\nabstract class SharedPagesRepository {\n  /// Gets the list of users and their roles for a shared page.\n  Future<FlowyResult<SharedPages, FlowyError>> getSharedPages();\n\n  /// Removes a shared page from the repository.\n  Future<FlowyResult<void, FlowyError>> leaveSharedPage(String pageId);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/logic/shared_section_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/core/notification/folder_notification.dart';\nimport 'package:appflowy/features/shared_section/data/repositories/shared_pages_repository.dart';\nimport 'package:appflowy/features/shared_section/logic/shared_section_event.dart';\nimport 'package:appflowy/features/shared_section/logic/shared_section_state.dart';\nimport 'package:appflowy/features/util/extensions.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nexport 'shared_section_event.dart';\nexport 'shared_section_state.dart';\n\nclass SharedSectionBloc extends Bloc<SharedSectionEvent, SharedSectionState> {\n  SharedSectionBloc({\n    required this.repository,\n    required this.workspaceId,\n    this.enablePolling = false,\n    this.pollingIntervalSeconds = 30,\n  }) : super(SharedSectionState.initial()) {\n    on<SharedSectionInitEvent>(_onInit);\n    on<SharedSectionRefreshEvent>(_onRefresh);\n    on<SharedSectionUpdateSharedPagesEvent>(_onUpdateSharedPages);\n    on<SharedSectionToggleExpandedEvent>(_onToggleExpanded);\n    on<SharedSectionLeaveSharedPageEvent>(_onLeaveSharedPage);\n  }\n\n  final String workspaceId;\n\n  // The repository to fetch the shared views.\n  // If you need to test this bloc, you can add your own repository implementation.\n  final SharedPagesRepository repository;\n\n  // Used to listen for shared view updates.\n  late final FolderNotificationListener _folderNotificationListener;\n\n  // Since the backend doesn't provide a way to listen for shared view updates (websocket with shared view updates is not implemented yet),\n  // we need to poll the shared views periodically.\n  final bool enablePolling;\n\n  // The interval of polling the shared views.\n  final int pollingIntervalSeconds;\n\n  Timer? _pollingTimer;\n\n  @override\n  Future<void> close() async {\n    await _folderNotificationListener.stop();\n    _pollingTimer?.cancel();\n    await super.close();\n  }\n\n  Future<void> _onInit(\n    SharedSectionInitEvent event,\n    Emitter<SharedSectionState> emit,\n  ) async {\n    _initFolderNotificationListener();\n    _startPollingIfNeeded();\n\n    emit(\n      state.copyWith(\n        isLoading: true,\n        errorMessage: '',\n      ),\n    );\n    final result = await repository.getSharedPages();\n    result.fold(\n      (pages) {\n        emit(\n          state.copyWith(\n            sharedPages: pages,\n            isLoading: false,\n          ),\n        );\n      },\n      (error) {\n        emit(\n          state.copyWith(\n            errorMessage: error.msg,\n            isLoading: false,\n          ),\n        );\n      },\n    );\n  }\n\n  Future<void> _onRefresh(\n    SharedSectionRefreshEvent event,\n    Emitter<SharedSectionState> emit,\n  ) async {\n    final result = await repository.getSharedPages();\n\n    result.fold(\n      (pages) {\n        emit(\n          state.copyWith(\n            sharedPages: pages,\n          ),\n        );\n      },\n      (error) {\n        emit(\n          state.copyWith(\n            errorMessage: error.msg,\n          ),\n        );\n      },\n    );\n  }\n\n  void _onUpdateSharedPages(\n    SharedSectionUpdateSharedPagesEvent event,\n    Emitter<SharedSectionState> emit,\n  ) {\n    emit(\n      state.copyWith(\n        sharedPages: event.sharedPages,\n      ),\n    );\n  }\n\n  void _onToggleExpanded(\n    SharedSectionToggleExpandedEvent event,\n    Emitter<SharedSectionState> emit,\n  ) {\n    emit(\n      state.copyWith(\n        isExpanded: !state.isExpanded,\n      ),\n    );\n  }\n\n  void _initFolderNotificationListener() {\n    _folderNotificationListener = FolderNotificationListener(\n      objectId: workspaceId,\n      handler: (notification, result) {\n        if (notification == FolderNotification.DidUpdateSharedViews) {\n          final response = result.fold(\n            (payload) {\n              final repeatedSharedViews =\n                  RepeatedSharedViewResponsePB.fromBuffer(payload);\n              return repeatedSharedViews;\n            },\n            (error) => null,\n          );\n          if (response != null) {\n            add(\n              SharedSectionEvent.updateSharedPages(\n                sharedPages: response.sharedPages,\n              ),\n            );\n          }\n        }\n      },\n    );\n  }\n\n  void _onLeaveSharedPage(\n    SharedSectionLeaveSharedPageEvent event,\n    Emitter<SharedSectionState> emit,\n  ) async {\n    final result = await repository.leaveSharedPage(event.pageId);\n    result.fold(\n      (success) {\n        add(\n          SharedSectionEvent.updateSharedPages(\n            sharedPages: state.sharedPages\n              ..removeWhere(\n                (page) => page.view.id == event.pageId,\n              ),\n          ),\n        );\n      },\n      (error) {\n        emit(state.copyWith(errorMessage: error.msg));\n      },\n    );\n  }\n\n  void _startPollingIfNeeded() {\n    _pollingTimer?.cancel();\n    if (enablePolling && pollingIntervalSeconds > 0) {\n      _pollingTimer = Timer.periodic(\n        Duration(seconds: pollingIntervalSeconds),\n        (_) {\n          add(const SharedSectionEvent.refresh());\n\n          Log.debug('Polling shared views');\n        },\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/logic/shared_section_event.dart",
    "content": "import 'package:appflowy/features/shared_section/models/shared_page.dart';\n\n/// Base class for all SharedSection events\nsealed class SharedSectionEvent {\n  const SharedSectionEvent();\n\n  /// Initialize, it will create a folder notification listener to listen for shared view updates.\n  /// Also, it will fetch the shared pages from the repository.\n  const factory SharedSectionEvent.init() = SharedSectionInitEvent;\n\n  /// Refresh, it will re-fetch the shared pages from the repository.\n  const factory SharedSectionEvent.refresh() = SharedSectionRefreshEvent;\n\n  /// Update the shared pages in the state.\n  const factory SharedSectionEvent.updateSharedPages({\n    required SharedPages sharedPages,\n  }) = SharedSectionUpdateSharedPagesEvent;\n\n  /// Toggle the expanded status of the shared section.\n  const factory SharedSectionEvent.toggleExpanded() =\n      SharedSectionToggleExpandedEvent;\n\n  /// Leave shared page.\n  const factory SharedSectionEvent.leaveSharedPage({\n    required String pageId,\n  }) = SharedSectionLeaveSharedPageEvent;\n}\n\nclass SharedSectionInitEvent extends SharedSectionEvent {\n  const SharedSectionInitEvent();\n}\n\nclass SharedSectionRefreshEvent extends SharedSectionEvent {\n  const SharedSectionRefreshEvent();\n}\n\nclass SharedSectionUpdateSharedPagesEvent extends SharedSectionEvent {\n  const SharedSectionUpdateSharedPagesEvent({\n    required this.sharedPages,\n  });\n\n  final SharedPages sharedPages;\n}\n\nclass SharedSectionToggleExpandedEvent extends SharedSectionEvent {\n  const SharedSectionToggleExpandedEvent();\n}\n\nclass SharedSectionLeaveSharedPageEvent extends SharedSectionEvent {\n  const SharedSectionLeaveSharedPageEvent({\n    required this.pageId,\n  });\n\n  final String pageId;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/logic/shared_section_state.dart",
    "content": "import 'package:appflowy/features/shared_section/models/shared_page.dart';\n\nclass SharedSectionState {\n  factory SharedSectionState.initial() => const SharedSectionState();\n\n  const SharedSectionState({\n    this.sharedPages = const [],\n    this.isLoading = false,\n    this.errorMessage = '',\n    this.isExpanded = true,\n  });\n\n  final SharedPages sharedPages;\n  final bool isLoading;\n  final String errorMessage;\n  final bool isExpanded;\n\n  SharedSectionState copyWith({\n    SharedPages? sharedPages,\n    bool? isLoading,\n    String? errorMessage,\n    bool? isExpanded,\n  }) {\n    return SharedSectionState(\n      sharedPages: sharedPages ?? this.sharedPages,\n      isLoading: isLoading ?? this.isLoading,\n      errorMessage: errorMessage ?? this.errorMessage,\n      isExpanded: isExpanded ?? this.isExpanded,\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n    return other is SharedSectionState &&\n        other.sharedPages == sharedPages &&\n        other.isLoading == isLoading &&\n        other.errorMessage == errorMessage &&\n        other.isExpanded == isExpanded;\n  }\n\n  @override\n  int get hashCode {\n    return Object.hash(\n      sharedPages,\n      isLoading,\n      errorMessage,\n      isExpanded,\n    );\n  }\n\n  @override\n  String toString() {\n    return 'SharedSectionState(sharedPages: $sharedPages, isLoading: $isLoading, errorMessage: $errorMessage, isExpanded: $isExpanded)';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/models/shared_page.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/share_access_level.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\n\ntypedef SharedPages = List<SharedPage>;\n\nclass SharedPage {\n  SharedPage({\n    required this.view,\n    required this.accessLevel,\n  });\n\n  final ViewPB view;\n  final ShareAccessLevel accessLevel;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/presentation/m_shared_section.dart",
    "content": "import 'package:appflowy/features/shared_section/data/repositories/rust_shared_pages_repository_impl.dart';\nimport 'package:appflowy/features/shared_section/logic/shared_section_bloc.dart';\nimport 'package:appflowy/features/shared_section/presentation/widgets/m_shared_page_list.dart';\nimport 'package:appflowy/features/shared_section/presentation/widgets/m_shared_section_header.dart';\nimport 'package:appflowy/features/shared_section/presentation/widgets/refresh_button.dart';\nimport 'package:appflowy/features/shared_section/presentation/widgets/shared_section_empty.dart';\nimport 'package:appflowy/features/shared_section/presentation/widgets/shared_section_error.dart';\nimport 'package:appflowy/features/shared_section/presentation/widgets/shared_section_loading.dart';\nimport 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MSharedSection extends StatelessWidget {\n  const MSharedSection({\n    super.key,\n    required this.workspaceId,\n  });\n\n  final String workspaceId;\n\n  @override\n  Widget build(BuildContext context) {\n    final repository = RustSharePagesRepositoryImpl();\n\n    return BlocProvider(\n      create: (_) => SharedSectionBloc(\n        workspaceId: workspaceId,\n        repository: repository,\n        enablePolling: true,\n      )..add(const SharedSectionInitEvent()),\n      child: BlocBuilder<SharedSectionBloc, SharedSectionState>(\n        builder: (context, state) {\n          if (state.isLoading) {\n            return const SharedSectionLoading();\n          }\n\n          if (state.errorMessage.isNotEmpty) {\n            return SharedSectionError(errorMessage: state.errorMessage);\n          }\n\n          // hide the shared section if there are no shared pages\n          if (state.sharedPages.isEmpty) {\n            return const SharedSectionEmpty();\n          }\n\n          return Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              const VSpace(HomeSpaceViewSizes.mVerticalPadding),\n\n              // Shared header\n              MSharedSectionHeader(),\n\n              Padding(\n                padding: const EdgeInsets.only(\n                  left: HomeSpaceViewSizes.mHorizontalPadding,\n                ),\n                child: MSharedPageList(\n                  sharedPages: state.sharedPages,\n                  onSelected: (view) {\n                    context.pushView(\n                      view,\n                      tabs: [\n                        PickerTabType.emoji,\n                        PickerTabType.icon,\n                        PickerTabType.custom,\n                      ].map((e) => e.name).toList(),\n                    );\n                  },\n                ),\n              ),\n\n              // Refresh button, for debugging only\n              if (kDebugMode)\n                RefreshSharedSectionButton(\n                  onTap: () {\n                    context.read<SharedSectionBloc>().add(\n                          const SharedSectionEvent.refresh(),\n                        );\n                  },\n                ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/presentation/shared_section.dart",
    "content": "import 'package:appflowy/features/shared_section/data/repositories/rust_shared_pages_repository_impl.dart';\nimport 'package:appflowy/features/shared_section/logic/shared_section_bloc.dart';\nimport 'package:appflowy/features/shared_section/presentation/widgets/refresh_button.dart';\nimport 'package:appflowy/features/shared_section/presentation/widgets/shared_page_list.dart';\nimport 'package:appflowy/features/shared_section/presentation/widgets/shared_section_error.dart';\nimport 'package:appflowy/features/shared_section/presentation/widgets/shared_section_header.dart';\nimport 'package:appflowy/features/shared_section/presentation/widgets/shared_section_loading.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SharedSection extends StatelessWidget {\n  const SharedSection({\n    super.key,\n    required this.workspaceId,\n  });\n\n  final String workspaceId;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final repository = RustSharePagesRepositoryImpl();\n\n    return BlocProvider(\n      create: (_) => SharedSectionBloc(\n        workspaceId: workspaceId,\n        repository: repository,\n        enablePolling: true,\n      )..add(const SharedSectionInitEvent()),\n      child: BlocBuilder<SharedSectionBloc, SharedSectionState>(\n        builder: (context, state) {\n          if (state.isLoading) {\n            return const SharedSectionLoading();\n          }\n\n          if (state.errorMessage.isNotEmpty) {\n            return SharedSectionError(errorMessage: state.errorMessage);\n          }\n\n          // hide the shared section if there are no shared pages\n          if (state.sharedPages.isEmpty) {\n            return const SizedBox.shrink();\n          }\n\n          return Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              // Shared header\n              SharedSectionHeader(\n                onTap: () {\n                  // expand or collapse the shared section\n                  context.read<SharedSectionBloc>().add(\n                        const SharedSectionToggleExpandedEvent(),\n                      );\n                },\n              ),\n\n              // Shared pages list\n              if (state.isExpanded)\n                SharedPageList(\n                  sharedPages: state.sharedPages,\n                  onSetEditing: (context, value) {\n                    context.read<ViewBloc>().add(ViewEvent.setIsEditing(value));\n                  },\n                  onAction: (action, view, data) async {\n                    switch (action) {\n                      case ViewMoreActionType.favorite:\n                      case ViewMoreActionType.unFavorite:\n                        context\n                            .read<FavoriteBloc>()\n                            .add(FavoriteEvent.toggle(view));\n                        break;\n                      case ViewMoreActionType.openInNewTab:\n                        context.read<TabsBloc>().openTab(view);\n                        break;\n                      case ViewMoreActionType.rename:\n                        await showAFTextFieldDialog(\n                          context: context,\n                          title: LocaleKeys.disclosureAction_rename.tr(),\n                          initialValue: view.nameOrDefault,\n                          maxLength: 256,\n                          onConfirm: (newValue) {\n                            // can not use bloc here because it has been disposed.\n                            ViewBackendService.updateView(\n                              viewId: view.id,\n                              name: newValue,\n                            );\n                          },\n                        );\n                        break;\n                      case ViewMoreActionType.leaveSharedPage:\n                        // show a dialog to confirm the action\n                        await showConfirmDialog(\n                          context: context,\n                          title: 'Remove your own access',\n                          titleStyle: theme.textStyle.body.standard(\n                            color: theme.textColorScheme.primary,\n                          ),\n                          description: '',\n                          style: ConfirmPopupStyle.cancelAndOk,\n                          confirmLabel: 'Remove',\n                          onConfirm: (_) {\n                            context.read<SharedSectionBloc>().add(\n                                  SharedSectionEvent.leaveSharedPage(\n                                    pageId: view.id,\n                                  ),\n                                );\n                          },\n                        );\n                        break;\n                      default:\n                        // Other actions are not allowed for read-only access\n                        break;\n                    }\n                  },\n                  onSelected: (context, view) {\n                    if (HardwareKeyboard.instance.isControlPressed) {\n                      context.read<TabsBloc>().openTab(view);\n                    }\n                    context.read<TabsBloc>().openPlugin(view);\n                  },\n                  onTertiarySelected: (context, view) {\n                    context.read<TabsBloc>().openTab(view);\n                  },\n                ),\n\n              // Refresh button, for debugging only\n              if (kDebugMode)\n                RefreshSharedSectionButton(\n                  onTap: () {\n                    context.read<SharedSectionBloc>().add(\n                          const SharedSectionEvent.refresh(),\n                        );\n                  },\n                ),\n\n              const VSpace(16.0),\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/m_shared_page_list.dart",
    "content": "import 'package:appflowy/features/shared_section/models/shared_page.dart';\nimport 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:flutter/material.dart';\n\n/// Shared pages on mobile\nclass MSharedPageList extends StatelessWidget {\n  const MSharedPageList({\n    super.key,\n    required this.sharedPages,\n    required this.onSelected,\n  });\n\n  final SharedPages sharedPages;\n  final ViewItemOnSelected onSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: sharedPages.map((sharedPage) {\n        final view = sharedPage.view;\n        return MobileViewItem(\n          key: ValueKey(view.id),\n          spaceType: FolderSpaceType.public,\n          isFirstChild: view.id == sharedPages.first.view.id,\n          view: view,\n          level: 0,\n          isDraggable: false, // disable draggable for shared pages\n          leftPadding: HomeSpaceViewSizes.leftPadding,\n          isFeedback: false,\n          onSelected: onSelected,\n        );\n      }).toList(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/m_shared_section_header.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\n\nclass MSharedSectionHeader extends StatelessWidget {\n  const MSharedSectionHeader({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return SizedBox(\n      height: 48,\n      child: Row(\n        children: [\n          const HSpace(HomeSpaceViewSizes.mHorizontalPadding),\n          FlowySvg(\n            FlowySvgs.shared_with_me_m,\n            color: theme.badgeColorScheme.color13Thick2,\n          ),\n          const HSpace(10.0),\n          FlowyText.medium(\n            LocaleKeys.shareSection_shared.tr(),\n            lineHeight: 1.15,\n            fontSize: 16.0,\n          ),\n          const HSpace(HomeSpaceViewSizes.mHorizontalPadding),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/refresh_button.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass RefreshSharedSectionButton extends StatelessWidget {\n  const RefreshSharedSectionButton({\n    super.key,\n    required this.onTap,\n  });\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return AFTextMenuItem(\n      leading: Icon(\n        Icons.refresh,\n        size: 20,\n        color: theme.iconColorScheme.secondary,\n      ),\n      title: 'Refresh',\n      onTap: onTap,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/shared_page_actions_button.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/share_access_level.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\ntypedef SharedPageActionsButtonCallback = void Function(\n  ViewMoreActionType type,\n  ViewPB view,\n  dynamic data,\n);\n\ntypedef SharedPageActionsButtonSetEditingCallback = void Function(\n  BuildContext context,\n  bool value,\n);\n\nclass SharedPageActionsButton extends StatefulWidget {\n  const SharedPageActionsButton({\n    super.key,\n    required this.view,\n    required this.accessLevel,\n    required this.onAction,\n    required this.buildChild,\n    required this.onSetEditing,\n    this.showAtCursor = false,\n  });\n\n  final ViewPB view;\n  final ShareAccessLevel accessLevel;\n  final SharedPageActionsButtonCallback onAction;\n  final SharedPageActionsButtonSetEditingCallback onSetEditing;\n  final bool showAtCursor;\n  final Widget Function(AFPopoverController) buildChild;\n\n  @override\n  State<SharedPageActionsButton> createState() =>\n      _SharedPageActionsButtonState();\n}\n\nclass _SharedPageActionsButtonState extends State<SharedPageActionsButton> {\n  AFPopoverController controller = AFPopoverController();\n\n  @override\n  void initState() {\n    super.initState();\n\n    controller.addListener(() {\n      if (!controller.isOpen) {\n        widget.onSetEditing(context, false);\n      }\n    });\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AFPopover(\n      controller: controller,\n      padding: EdgeInsets.zero,\n      decoration: BoxDecoration(), // the AFMenu has a border\n      anchor: const AFAnchorAuto(\n        offset: Offset(98, 8),\n      ),\n      popover: (context) => AFMenu(\n        width: 240,\n        backgroundColor:\n            Theme.of(context).cardColor, // to compatible with the old design\n        children: _buildMenuItems(context),\n      ),\n      child: widget.buildChild(controller),\n    );\n  }\n\n  List<Widget> _buildMenuItems(BuildContext context) {\n    final actionTypes = _buildActionTypes();\n    final menuItems = <Widget>[];\n\n    for (final actionType in actionTypes) {\n      if (actionType == ViewMoreActionType.divider) {\n        if (menuItems.isNotEmpty) {\n          menuItems.add(const AFDivider(spacing: 4));\n        }\n      } else {\n        menuItems.add(\n          AFTextMenuItem(\n            leading: FlowySvg(\n              actionType.leftIconSvg,\n              size: const Size.square(16),\n              color: actionType == ViewMoreActionType.delete\n                  ? Theme.of(context).colorScheme.error\n                  : null,\n            ),\n            title: actionType.name,\n            titleColor: actionType == ViewMoreActionType.delete\n                ? Theme.of(context).colorScheme.error\n                : null,\n            trailing: actionType.rightIcon,\n            onTap: () {\n              widget.onAction(actionType, widget.view, null);\n              controller.hide();\n            },\n          ),\n        );\n      }\n    }\n\n    return menuItems;\n  }\n\n  List<ViewMoreActionType> _buildActionTypes() {\n    final List<ViewMoreActionType> actionTypes = [];\n\n    // Always allow add to favorites and open in new tab\n    actionTypes.add(\n      widget.view.isFavorite\n          ? ViewMoreActionType.unFavorite\n          : ViewMoreActionType.favorite,\n    );\n\n    actionTypes.add(\n      ViewMoreActionType.leaveSharedPage,\n    );\n\n    // Only show editable actions if access level allows it\n    if (widget.accessLevel != ShareAccessLevel.readOnly) {\n      actionTypes.addAll([\n        ViewMoreActionType.divider,\n        ViewMoreActionType.rename,\n      ]);\n\n      // Chat doesn't change icon and duplicate\n      if (widget.view.layout != ViewLayoutPB.Chat) {\n        actionTypes.addAll([\n          ViewMoreActionType.changeIcon,\n        ]);\n      }\n\n      if (widget.accessLevel == ShareAccessLevel.fullAccess) {\n        actionTypes.addAll([\n          ViewMoreActionType.delete,\n        ]);\n      }\n    }\n\n    actionTypes.add(ViewMoreActionType.divider);\n    actionTypes.add(ViewMoreActionType.openInNewTab);\n\n    return actionTypes;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/shared_page_list.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/share_access_level.dart';\nimport 'package:appflowy/features/shared_section/models/shared_page.dart';\nimport 'package:appflowy/features/shared_section/presentation/widgets/shared_page_actions_button.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\n/// Shared pages on desktop\nclass SharedPageList extends StatelessWidget {\n  const SharedPageList({\n    super.key,\n    required this.sharedPages,\n    required this.onAction,\n    required this.onSelected,\n    required this.onTertiarySelected,\n    required this.onSetEditing,\n  });\n\n  final SharedPages sharedPages;\n  final ViewItemOnSelected onSelected;\n  final ViewItemOnSelected onTertiarySelected;\n  final SharedPageActionsButtonCallback onAction;\n  final SharedPageActionsButtonSetEditingCallback onSetEditing;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: sharedPages.map((sharedPage) {\n        final view = sharedPage.view;\n        final accessLevel = sharedPage.accessLevel;\n        return ViewItem(\n          key: ValueKey(view.id),\n          spaceType: FolderSpaceType.public,\n          isFirstChild: view.id == sharedPages.first.view.id,\n          view: view,\n          level: 0,\n          isDraggable: false, // disable draggable for shared pages\n          leftPadding: HomeSpaceViewSizes.leftPadding,\n          isFeedback: false,\n          onSelected: onSelected,\n          onTertiarySelected: onTertiarySelected,\n          rightIconsBuilder: (context, view) => [\n            IntrinsicWidth(\n              child: _buildSharedPageMoreActionButton(\n                context,\n                view,\n                accessLevel,\n              ),\n            ),\n            const SizedBox(width: 4.0),\n          ],\n        );\n      }).toList(),\n    );\n  }\n\n  Widget _buildSharedPageMoreActionButton(\n    BuildContext context,\n    ViewPB view,\n    ShareAccessLevel accessLevel,\n  ) {\n    return SharedPageActionsButton(\n      view: view,\n      accessLevel: accessLevel,\n      onAction: onAction,\n      onSetEditing: onSetEditing,\n      buildChild: (controller) => FlowyIconButton(\n        width: 24,\n        icon: const FlowySvg(FlowySvgs.workspace_three_dots_s),\n        onPressed: () {\n          onSetEditing(context, true);\n          controller.show();\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/shared_section_empty.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\n\nclass SharedSectionEmpty extends StatelessWidget {\n  const SharedSectionEmpty({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Center(\n      child: Column(\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          FlowySvg(\n            FlowySvgs.empty_shared_section_m,\n            color: theme.iconColorScheme.tertiary,\n          ),\n          const VSpace(12),\n          Text(\n            'Nothing shared with you',\n            style: theme.textStyle.heading3.enhanced(\n              color: theme.textColorScheme.secondary,\n            ),\n            textAlign: TextAlign.center,\n          ),\n          const VSpace(4),\n          Text(\n            'Pages shared with you will show here',\n            style: theme.textStyle.heading4.standard(\n              color: theme.textColorScheme.tertiary,\n            ),\n            textAlign: TextAlign.center,\n          ),\n          const VSpace(kBottomNavigationBarHeight + 60.0),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/shared_section_error.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass SharedSectionError extends StatelessWidget {\n  const SharedSectionError({\n    super.key,\n    required this.errorMessage,\n  });\n\n  final String errorMessage;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Padding(\n      padding: const EdgeInsets.all(16.0),\n      child: Text(\n        errorMessage,\n        style: theme.textStyle.body.enhanced(\n          color: theme.textColorScheme.warning,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/shared_section_header.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nclass SharedSectionHeader extends StatelessWidget {\n  const SharedSectionHeader({\n    super.key,\n    required this.onTap,\n  });\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return AFGhostIconTextButton.primary(\n      text: LocaleKeys.shareSection_shared.tr(),\n      mainAxisAlignment: MainAxisAlignment.start,\n      size: AFButtonSize.l,\n      onTap: onTap,\n      // todo: ask the designer to provide the token.\n      padding: EdgeInsets.symmetric(\n        horizontal: 4,\n        vertical: 6,\n      ),\n      borderRadius: theme.borderRadius.s,\n      iconBuilder: (context, isHover, disabled) => FlowySvg(\n        FlowySvgs.shared_with_me_m,\n        color: theme.badgeColorScheme.color13Thick2,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/shared_section/presentation/widgets/shared_section_loading.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass SharedSectionLoading extends StatelessWidget {\n  const SharedSectionLoading({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Center(\n      child: CircularProgressIndicator(\n        color: theme.iconColorScheme.primary,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/util/extensions.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/features/shared_section/models/shared_page.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart' as folder;\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pbenum.dart'\n    as user;\n\nextension RepeatedSharedViewResponsePBExtension\n    on RepeatedSharedViewResponsePB {\n  SharedPages get sharedPages {\n    return sharedViews.map((e) => e.sharedPage).toList();\n  }\n}\n\nextension SharedViewPBExtension on SharedViewPB {\n  SharedPage get sharedPage {\n    return SharedPage(\n      view: view,\n      accessLevel: accessLevel.shareAccessLevel,\n    );\n  }\n}\n\nextension RepeatedSharedUserPBExtension on RepeatedSharedUserPB {\n  SharedUsers get sharedUsers {\n    return items.map((e) => e.sharedUser).toList();\n  }\n}\n\nextension SharedUserPBExtension on SharedUserPB {\n  SharedUser get sharedUser {\n    return SharedUser(\n      email: email,\n      name: name,\n      accessLevel: accessLevel.shareAccessLevel,\n      role: role.shareRole,\n      avatarUrl: avatarUrl,\n    );\n  }\n}\n\nextension AFAccessLevelPBExtension on AFAccessLevelPB {\n  ShareAccessLevel get shareAccessLevel {\n    switch (this) {\n      case AFAccessLevelPB.ReadOnly:\n        return ShareAccessLevel.readOnly;\n      case AFAccessLevelPB.ReadAndComment:\n        return ShareAccessLevel.readAndComment;\n      case AFAccessLevelPB.ReadAndWrite:\n        return ShareAccessLevel.readAndWrite;\n      case AFAccessLevelPB.FullAccess:\n        return ShareAccessLevel.fullAccess;\n      default:\n        throw Exception('Unknown share role: $this');\n    }\n  }\n}\n\nextension ShareAccessLevelExtension on ShareAccessLevel {\n  AFAccessLevelPB get accessLevel {\n    switch (this) {\n      case ShareAccessLevel.readOnly:\n        return AFAccessLevelPB.ReadOnly;\n      case ShareAccessLevel.readAndComment:\n        return AFAccessLevelPB.ReadAndComment;\n      case ShareAccessLevel.readAndWrite:\n        return AFAccessLevelPB.ReadAndWrite;\n      case ShareAccessLevel.fullAccess:\n        return AFAccessLevelPB.FullAccess;\n    }\n  }\n}\n\nextension AFRolePBExtension on AFRolePB {\n  ShareRole get shareRole {\n    switch (this) {\n      case AFRolePB.Guest:\n        return ShareRole.guest;\n      case AFRolePB.Member:\n        return ShareRole.member;\n      case AFRolePB.Owner:\n        return ShareRole.owner;\n      default:\n        throw Exception('Unknown share role: $this');\n    }\n  }\n}\n\nextension ShareRoleExtension on ShareRole {\n  user.AFRolePB get userRole {\n    switch (this) {\n      case ShareRole.guest:\n        return user.AFRolePB.Guest;\n      case ShareRole.member:\n        return user.AFRolePB.Member;\n      case ShareRole.owner:\n        return user.AFRolePB.Owner;\n    }\n  }\n\n  folder.AFRolePB get folderRole {\n    switch (this) {\n      case ShareRole.guest:\n        return folder.AFRolePB.Guest;\n      case ShareRole.member:\n        return folder.AFRolePB.Member;\n      case ShareRole.owner:\n        return folder.AFRolePB.Owner;\n    }\n  }\n}\n\nextension SharedSectionTypeExtension on folder.SharedViewSectionPB {\n  SharedSectionType get sharedSectionType {\n    switch (this) {\n      case folder.SharedViewSectionPB.PublicSection:\n        return SharedSectionType.public;\n      case folder.SharedViewSectionPB.PrivateSection:\n        return SharedSectionType.private;\n      case folder.SharedViewSectionPB.SharedSection:\n        return SharedSectionType.shared;\n      default:\n        throw Exception('Unknown shared section type: $this');\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/view_management/README.md",
    "content": ""
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/view_management/logic/view_event.dart",
    "content": "\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/view_management/logic/view_management_bloc.dart",
    "content": "\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/view_management/logic/view_state.dart",
    "content": "\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/workspace/data/repositories/rust_workspace_repository_impl.dart",
    "content": "import 'package:appflowy/features/workspace/data/repositories/workspace_repository.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'\n    as billing_service;\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:fixnum/fixnum.dart';\n\n/// Implementation of WorkspaceRepository using UserBackendService.\nclass RustWorkspaceRepositoryImpl implements WorkspaceRepository {\n  RustWorkspaceRepositoryImpl({\n    required Int64 userId,\n  }) : _userService = UserBackendService(userId: userId);\n\n  final UserBackendService _userService;\n\n  @override\n  Future<FlowyResult<WorkspacePB, FlowyError>> getCurrentWorkspace() async {\n    return UserBackendService.getCurrentWorkspace();\n  }\n\n  @override\n  Future<FlowyResult<List<UserWorkspacePB>, FlowyError>> getWorkspaces() async {\n    return _userService.getWorkspaces();\n  }\n\n  @override\n  Future<FlowyResult<UserWorkspacePB, FlowyError>> createWorkspace({\n    required String name,\n    required WorkspaceTypePB workspaceType,\n  }) async {\n    return _userService.createUserWorkspace(name, workspaceType);\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> deleteWorkspace({\n    required String workspaceId,\n  }) async {\n    return _userService.deleteWorkspaceById(workspaceId);\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> openWorkspace({\n    required String workspaceId,\n    required WorkspaceTypePB workspaceType,\n  }) async {\n    return _userService.openWorkspace(workspaceId, workspaceType);\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> renameWorkspace({\n    required String workspaceId,\n    required String name,\n  }) async {\n    return _userService.renameWorkspace(workspaceId, name);\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> updateWorkspaceIcon({\n    required String workspaceId,\n    required String icon,\n  }) async {\n    return _userService.updateWorkspaceIcon(workspaceId, icon);\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> leaveWorkspace({\n    required String workspaceId,\n  }) async {\n    return _userService.leaveWorkspace(workspaceId);\n  }\n\n  @override\n  Future<FlowyResult<WorkspaceSubscriptionInfoPB, FlowyError>>\n      getWorkspaceSubscriptionInfo({\n    required String workspaceId,\n  }) async {\n    return UserBackendService.getWorkspaceSubscriptionInfo(workspaceId);\n  }\n\n  @override\n  Future<bool> isBillingEnabled() async {\n    return billing_service.isBillingEnabled();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/workspace/data/repositories/workspace_repository.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\n/// Abstract repository for workspace operations.\n///\n/// This abstracts the data source for workspace operations,\n/// allowing for different implementations (e.g., REST API, gRPC, local storage).\nabstract class WorkspaceRepository {\n  /// Gets the current workspace for the user.\n  Future<FlowyResult<WorkspacePB, FlowyError>> getCurrentWorkspace();\n\n  /// Gets the list of workspaces for the current user.\n  Future<FlowyResult<List<UserWorkspacePB>, FlowyError>> getWorkspaces();\n\n  /// Creates a new workspace.\n  Future<FlowyResult<UserWorkspacePB, FlowyError>> createWorkspace({\n    required String name,\n    required WorkspaceTypePB workspaceType,\n  });\n\n  /// Deletes a workspace by ID.\n  Future<FlowyResult<void, FlowyError>> deleteWorkspace({\n    required String workspaceId,\n  });\n\n  /// Opens a workspace.\n  Future<FlowyResult<void, FlowyError>> openWorkspace({\n    required String workspaceId,\n    required WorkspaceTypePB workspaceType,\n  });\n\n  /// Renames a workspace.\n  Future<FlowyResult<void, FlowyError>> renameWorkspace({\n    required String workspaceId,\n    required String name,\n  });\n\n  /// Updates workspace icon.\n  Future<FlowyResult<void, FlowyError>> updateWorkspaceIcon({\n    required String workspaceId,\n    required String icon,\n  });\n\n  /// Leaves a workspace.\n  Future<FlowyResult<void, FlowyError>> leaveWorkspace({\n    required String workspaceId,\n  });\n\n  /// Gets workspace subscription information.\n  Future<FlowyResult<WorkspaceSubscriptionInfoPB, FlowyError>>\n      getWorkspaceSubscriptionInfo({\n    required String workspaceId,\n  });\n\n  /// Is billing enabled.\n  Future<bool> isBillingEnabled();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/workspace/logic/workspace_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/features/workspace/data/repositories/workspace_repository.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_event.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_state.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/user/application/user_listener.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:protobuf/protobuf.dart';\n\nexport 'workspace_event.dart';\nexport 'workspace_state.dart';\n\nclass _WorkspaceFetchResult {\n  const _WorkspaceFetchResult({\n    this.currentWorkspace,\n    required this.workspaces,\n    required this.shouldOpenWorkspace,\n  });\n\n  final UserWorkspacePB? currentWorkspace;\n  final List<UserWorkspacePB> workspaces;\n  final bool shouldOpenWorkspace;\n}\n\nclass UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {\n  UserWorkspaceBloc({\n    required this.repository,\n    required this.userProfile,\n    this.initialWorkspaceId,\n  })  : _listener = UserListener(userProfile: userProfile),\n        super(UserWorkspaceState.initial(userProfile)) {\n    on<WorkspaceEventInitialize>(_onInitialize);\n    on<WorkspaceEventFetchWorkspaces>(_onFetchWorkspaces);\n    on<WorkspaceEventCreateWorkspace>(_onCreateWorkspace);\n    on<WorkspaceEventDeleteWorkspace>(_onDeleteWorkspace);\n    on<WorkspaceEventOpenWorkspace>(_onOpenWorkspace);\n    on<WorkspaceEventRenameWorkspace>(_onRenameWorkspace);\n    on<WorkspaceEventUpdateWorkspaceIcon>(_onUpdateWorkspaceIcon);\n    on<WorkspaceEventLeaveWorkspace>(_onLeaveWorkspace);\n    on<WorkspaceEventFetchWorkspaceSubscriptionInfo>(\n      _onFetchWorkspaceSubscriptionInfo,\n    );\n    on<WorkspaceEventUpdateWorkspaceSubscriptionInfo>(\n      _onUpdateWorkspaceSubscriptionInfo,\n    );\n    on<WorkspaceEventEmitWorkspaces>(_onEmitWorkspaces);\n    on<WorkspaceEventEmitUserProfile>(_onEmitUserProfile);\n    on<WorkspaceEventEmitCurrentWorkspace>(_onEmitCurrentWorkspace);\n  }\n\n  final String? initialWorkspaceId;\n  final WorkspaceRepository repository;\n  final UserProfilePB userProfile;\n  final UserListener _listener;\n\n  @override\n  Future<void> close() {\n    _listener.stop();\n    return super.close();\n  }\n\n  Future<void> _onInitialize(\n    WorkspaceEventInitialize event,\n    Emitter<UserWorkspaceState> emit,\n  ) async {\n    await _setupListeners();\n    await _initializeWorkspaces(emit);\n  }\n\n  Future<void> _onFetchWorkspaces(\n    WorkspaceEventFetchWorkspaces event,\n    Emitter<UserWorkspaceState> emit,\n  ) async {\n    final result = await _fetchWorkspaces(\n      initialWorkspaceId: event.initialWorkspaceId,\n    );\n\n    final currentWorkspace = result.currentWorkspace;\n    final workspaces = result.workspaces;\n    Log.info(\n      'fetch workspaces: current workspace: ${currentWorkspace?.workspaceId}, workspaces: ${workspaces.map((e) => e.workspaceId)}',\n    );\n\n    emit(\n      state.copyWith(\n        workspaces: workspaces,\n      ),\n    );\n\n    if (currentWorkspace != null &&\n        currentWorkspace.workspaceId != state.currentWorkspace?.workspaceId) {\n      Log.info(\n        'fetch workspaces: try to open workspace: ${currentWorkspace.workspaceId}',\n      );\n      add(\n        UserWorkspaceEvent.openWorkspace(\n          workspaceId: currentWorkspace.workspaceId,\n          workspaceType: currentWorkspace.workspaceType,\n        ),\n      );\n    }\n  }\n\n  Future<void> _onCreateWorkspace(\n    WorkspaceEventCreateWorkspace event,\n    Emitter<UserWorkspaceState> emit,\n  ) async {\n    emit(\n      state.copyWith(\n        actionResult: const WorkspaceActionResult(\n          actionType: WorkspaceActionType.create,\n          isLoading: true,\n          result: null,\n        ),\n      ),\n    );\n\n    final result = await repository.createWorkspace(\n      name: event.name,\n      workspaceType: event.workspaceType,\n    );\n\n    final workspaces = result.fold(\n      (s) => [...state.workspaces, s],\n      (e) => state.workspaces,\n    );\n\n    emit(\n      state.copyWith(\n        workspaces: _sortWorkspaces(workspaces),\n        actionResult: WorkspaceActionResult(\n          actionType: WorkspaceActionType.create,\n          isLoading: false,\n          result: result.map((_) {}),\n        ),\n      ),\n    );\n\n    result\n      ..onSuccess((s) {\n        Log.info('create workspace success: $s');\n        add(\n          UserWorkspaceEvent.openWorkspace(\n            workspaceId: s.workspaceId,\n            workspaceType: s.workspaceType,\n          ),\n        );\n      })\n      ..onFailure((f) {\n        Log.error('create workspace error: $f');\n      });\n  }\n\n  Future<void> _onDeleteWorkspace(\n    WorkspaceEventDeleteWorkspace event,\n    Emitter<UserWorkspaceState> emit,\n  ) async {\n    Log.info('try to delete workspace: ${event.workspaceId}');\n    emit(\n      state.copyWith(\n        actionResult: const WorkspaceActionResult(\n          actionType: WorkspaceActionType.delete,\n          isLoading: true,\n          result: null,\n        ),\n      ),\n    );\n\n    final remoteWorkspaces = await _fetchWorkspaces().then(\n      (value) => value.workspaces,\n    );\n\n    if (state.workspaces.length <= 1 || remoteWorkspaces.length <= 1) {\n      final result = FlowyResult.failure(\n        FlowyError(\n          code: ErrorCode.Internal,\n          msg: LocaleKeys.workspace_cannotDeleteTheOnlyWorkspace.tr(),\n        ),\n      );\n      return emit(\n        state.copyWith(\n          actionResult: WorkspaceActionResult(\n            actionType: WorkspaceActionType.delete,\n            result: result,\n            isLoading: false,\n          ),\n        ),\n      );\n    }\n\n    final result = await repository.deleteWorkspace(\n      workspaceId: event.workspaceId,\n    );\n    final workspacesResult = await _fetchWorkspaces();\n    final workspaces = workspacesResult.workspaces;\n    final containsDeletedWorkspace =\n        _findWorkspaceById(event.workspaceId, workspaces) != null;\n\n    result\n      ..onSuccess((_) {\n        Log.info('delete workspace success: ${event.workspaceId}');\n        final firstWorkspace = workspaces.firstOrNull;\n        assert(\n          firstWorkspace != null,\n          'the first workspace must not be null',\n        );\n        if (state.currentWorkspace?.workspaceId == event.workspaceId &&\n            firstWorkspace != null) {\n          Log.info(\n            'delete workspace: open the first workspace: ${firstWorkspace.workspaceId}',\n          );\n          add(\n            UserWorkspaceEvent.openWorkspace(\n              workspaceId: firstWorkspace.workspaceId,\n              workspaceType: firstWorkspace.workspaceType,\n            ),\n          );\n        }\n      })\n      ..onFailure((f) {\n        Log.error('delete workspace error: $f');\n        if (!containsDeletedWorkspace && workspaces.isNotEmpty) {\n          add(\n            UserWorkspaceEvent.openWorkspace(\n              workspaceId: workspaces.first.workspaceId,\n              workspaceType: workspaces.first.workspaceType,\n            ),\n          );\n        }\n      });\n\n    emit(\n      state.copyWith(\n        workspaces: workspaces,\n        actionResult: WorkspaceActionResult(\n          actionType: WorkspaceActionType.delete,\n          result: result,\n          isLoading: false,\n        ),\n      ),\n    );\n  }\n\n  Future<void> _onOpenWorkspace(\n    WorkspaceEventOpenWorkspace event,\n    Emitter<UserWorkspaceState> emit,\n  ) async {\n    emit(\n      state.copyWith(\n        actionResult: const WorkspaceActionResult(\n          actionType: WorkspaceActionType.open,\n          isLoading: true,\n          result: null,\n        ),\n      ),\n    );\n\n    final result = await repository.openWorkspace(\n      workspaceId: event.workspaceId,\n      workspaceType: event.workspaceType,\n    );\n\n    final currentWorkspace = result.fold(\n      (s) => _findWorkspaceById(event.workspaceId),\n      (e) => state.currentWorkspace,\n    );\n\n    result\n      ..onSuccess((s) {\n        add(\n          UserWorkspaceEvent.fetchWorkspaceSubscriptionInfo(\n            workspaceId: event.workspaceId,\n          ),\n        );\n\n        Log.info(\n          'open workspace success: ${event.workspaceId}, current workspace: ${currentWorkspace?.toProto3Json()}',\n        );\n      })\n      ..onFailure((f) {\n        Log.error('open workspace error: $f');\n      });\n\n    emit(\n      state.copyWith(\n        currentWorkspace: currentWorkspace,\n        actionResult: WorkspaceActionResult(\n          actionType: WorkspaceActionType.open,\n          isLoading: false,\n          result: result,\n        ),\n      ),\n    );\n\n    getIt<ReminderBloc>().add(\n      ReminderEvent.started(),\n    );\n  }\n\n  Future<void> _onRenameWorkspace(\n    WorkspaceEventRenameWorkspace event,\n    Emitter<UserWorkspaceState> emit,\n  ) async {\n    final result = await repository.renameWorkspace(\n      workspaceId: event.workspaceId,\n      name: event.name,\n    );\n\n    final workspaces = result.fold(\n      (s) => _updateWorkspaceInList(event.workspaceId, (workspace) {\n        workspace.freeze();\n        return workspace.rebuild((p0) {\n          p0.name = event.name;\n        });\n      }),\n      (f) => state.workspaces,\n    );\n\n    final currentWorkspace = _findWorkspaceById(\n      state.currentWorkspace?.workspaceId ?? '',\n      workspaces,\n    );\n\n    Log.info('rename workspace: ${event.workspaceId}, name: ${event.name}');\n\n    result.onFailure((f) {\n      Log.error('rename workspace error: $f');\n    });\n\n    emit(\n      state.copyWith(\n        workspaces: workspaces,\n        currentWorkspace: currentWorkspace,\n        actionResult: WorkspaceActionResult(\n          actionType: WorkspaceActionType.rename,\n          isLoading: false,\n          result: result,\n        ),\n      ),\n    );\n  }\n\n  Future<void> _onUpdateWorkspaceIcon(\n    WorkspaceEventUpdateWorkspaceIcon event,\n    Emitter<UserWorkspaceState> emit,\n  ) async {\n    final workspace = _findWorkspaceById(event.workspaceId);\n    if (workspace == null) {\n      Log.error('workspace not found: ${event.workspaceId}');\n      return;\n    }\n\n    if (event.icon == workspace.icon) {\n      Log.info('ignore same icon update');\n      return;\n    }\n\n    final result = await repository.updateWorkspaceIcon(\n      workspaceId: event.workspaceId,\n      icon: event.icon,\n    );\n\n    final workspaces = result.fold(\n      (s) => _updateWorkspaceInList(event.workspaceId, (workspace) {\n        workspace.freeze();\n        return workspace.rebuild((p0) {\n          p0.icon = event.icon;\n        });\n      }),\n      (f) => state.workspaces,\n    );\n\n    final currentWorkspace = _findWorkspaceById(\n      state.currentWorkspace?.workspaceId ?? '',\n      workspaces,\n    );\n\n    Log.info(\n      'update workspace icon: ${event.workspaceId}, icon: ${event.icon}',\n    );\n\n    result.onFailure((f) {\n      Log.error('update workspace icon error: $f');\n    });\n\n    emit(\n      state.copyWith(\n        workspaces: workspaces,\n        currentWorkspace: currentWorkspace,\n        actionResult: WorkspaceActionResult(\n          actionType: WorkspaceActionType.updateIcon,\n          isLoading: false,\n          result: result,\n        ),\n      ),\n    );\n  }\n\n  Future<void> _onLeaveWorkspace(\n    WorkspaceEventLeaveWorkspace event,\n    Emitter<UserWorkspaceState> emit,\n  ) async {\n    final result = await repository.leaveWorkspace(\n      workspaceId: event.workspaceId,\n    );\n\n    final workspaces = result.fold(\n      (s) => state.workspaces\n          .where((e) => e.workspaceId != event.workspaceId)\n          .toList(),\n      (e) => state.workspaces,\n    );\n\n    result\n      ..onSuccess((_) {\n        Log.info('leave workspace success: ${event.workspaceId}');\n        if (state.currentWorkspace?.workspaceId == event.workspaceId &&\n            workspaces.isNotEmpty) {\n          add(\n            UserWorkspaceEvent.openWorkspace(\n              workspaceId: workspaces.first.workspaceId,\n              workspaceType: workspaces.first.workspaceType,\n            ),\n          );\n        }\n      })\n      ..onFailure((f) {\n        Log.error('leave workspace error: $f');\n      });\n\n    emit(\n      state.copyWith(\n        workspaces: _sortWorkspaces(workspaces),\n        actionResult: WorkspaceActionResult(\n          actionType: WorkspaceActionType.leave,\n          isLoading: false,\n          result: result,\n        ),\n      ),\n    );\n  }\n\n  Future<void> _onFetchWorkspaceSubscriptionInfo(\n    WorkspaceEventFetchWorkspaceSubscriptionInfo event,\n    Emitter<UserWorkspaceState> emit,\n  ) async {\n    final enabled = await repository.isBillingEnabled();\n    // If billing is not enabled, we don't need to fetch the workspace subscription info\n    if (!enabled) {\n      return;\n    }\n\n    unawaited(\n      repository\n          .getWorkspaceSubscriptionInfo(\n        workspaceId: event.workspaceId,\n      )\n          .fold(\n        (workspaceSubscriptionInfo) {\n          if (isClosed) {\n            return;\n          }\n\n          if (state.currentWorkspace?.workspaceId != event.workspaceId) {\n            return;\n          }\n\n          Log.debug(\n            'fetch workspace subscription info: ${event.workspaceId}, $workspaceSubscriptionInfo',\n          );\n\n          add(\n            UserWorkspaceEvent.updateWorkspaceSubscriptionInfo(\n              workspaceId: event.workspaceId,\n              subscriptionInfo: workspaceSubscriptionInfo,\n            ),\n          );\n        },\n        (e) => Log.error('fetch workspace subscription info error: $e'),\n      ),\n    );\n  }\n\n  Future<void> _onUpdateWorkspaceSubscriptionInfo(\n    WorkspaceEventUpdateWorkspaceSubscriptionInfo event,\n    Emitter<UserWorkspaceState> emit,\n  ) async {\n    emit(\n      state.copyWith(workspaceSubscriptionInfo: event.subscriptionInfo),\n    );\n  }\n\n  Future<void> _onEmitWorkspaces(\n    WorkspaceEventEmitWorkspaces event,\n    Emitter<UserWorkspaceState> emit,\n  ) async {\n    emit(\n      state.copyWith(\n        workspaces: _sortWorkspaces(event.workspaces),\n      ),\n    );\n  }\n\n  Future<void> _onEmitUserProfile(\n    WorkspaceEventEmitUserProfile event,\n    Emitter<UserWorkspaceState> emit,\n  ) async {\n    emit(\n      state.copyWith(userProfile: event.userProfile),\n    );\n  }\n\n  Future<void> _onEmitCurrentWorkspace(\n    WorkspaceEventEmitCurrentWorkspace event,\n    Emitter<UserWorkspaceState> emit,\n  ) async {\n    emit(\n      state.copyWith(currentWorkspace: event.workspace),\n    );\n  }\n\n  Future<void> _setupListeners() async {\n    _listener.start(\n      onProfileUpdated: (result) {\n        if (!isClosed) {\n          result.fold(\n            (newProfile) => add(\n              UserWorkspaceEvent.emitUserProfile(userProfile: newProfile),\n            ),\n            (error) => Log.error(\"Failed to get user profile: $error\"),\n          );\n        }\n      },\n      onUserWorkspaceListUpdated: (workspaces) {\n        if (!isClosed) {\n          add(\n            UserWorkspaceEvent.emitWorkspaces(\n              workspaces: _sortWorkspaces(workspaces.items),\n            ),\n          );\n        }\n      },\n      onUserWorkspaceUpdated: (workspace) {\n        if (!isClosed) {\n          if (state.currentWorkspace?.workspaceId == workspace.workspaceId) {\n            add(UserWorkspaceEvent.emitCurrentWorkspace(workspace: workspace));\n          }\n        }\n      },\n    );\n  }\n\n  Future<void> _initializeWorkspaces(Emitter<UserWorkspaceState> emit) async {\n    final result = await _fetchWorkspaces(\n      initialWorkspaceId: initialWorkspaceId,\n    );\n    final currentWorkspace = result.currentWorkspace;\n    final workspaces = result.workspaces;\n    final isCollabWorkspaceOn =\n        state.userProfile.userAuthType == AuthTypePB.Server &&\n            FeatureFlag.collaborativeWorkspace.isOn;\n\n    Log.info(\n      'init workspace, current workspace: ${currentWorkspace?.workspaceId}, '\n      'workspaces: ${workspaces.map((e) => e.workspaceId)}, isCollabWorkspaceOn: $isCollabWorkspaceOn',\n    );\n\n    if (currentWorkspace != null) {\n      add(\n        UserWorkspaceEvent.fetchWorkspaceSubscriptionInfo(\n          workspaceId: currentWorkspace.workspaceId,\n        ),\n      );\n    }\n\n    if (currentWorkspace != null && result.shouldOpenWorkspace == true) {\n      Log.info('init open workspace: ${currentWorkspace.workspaceId}');\n      await repository.openWorkspace(\n        workspaceId: currentWorkspace.workspaceId,\n        workspaceType: currentWorkspace.workspaceType,\n      );\n    }\n\n    emit(\n      state.copyWith(\n        currentWorkspace: currentWorkspace,\n        workspaces: workspaces,\n        isCollabWorkspaceOn: isCollabWorkspaceOn,\n        actionResult: const WorkspaceActionResult(\n          actionType: WorkspaceActionType.none,\n          isLoading: false,\n          result: null,\n        ),\n      ),\n    );\n  }\n\n  // Helper methods\n  List<UserWorkspacePB> _sortWorkspaces(List<UserWorkspacePB> workspaces) {\n    final sorted = [...workspaces];\n    sorted.sort(\n      (a, b) => a.createdAtTimestamp.compareTo(b.createdAtTimestamp),\n    );\n    return sorted;\n  }\n\n  UserWorkspacePB? _findWorkspaceById(\n    String id, [\n    List<UserWorkspacePB>? workspacesList,\n  ]) {\n    final workspaces = workspacesList ?? state.workspaces;\n    return workspaces.firstWhereOrNull((e) => e.workspaceId == id);\n  }\n\n  List<UserWorkspacePB> _updateWorkspaceInList(\n    String workspaceId,\n    UserWorkspacePB Function(UserWorkspacePB workspace) updater,\n  ) {\n    final workspaces = [...state.workspaces];\n    final index = workspaces.indexWhere((e) => e.workspaceId == workspaceId);\n    if (index != -1) {\n      workspaces[index] = updater(workspaces[index]);\n    }\n    return workspaces;\n  }\n\n  Future<_WorkspaceFetchResult> _fetchWorkspaces({\n    String? initialWorkspaceId,\n  }) async {\n    try {\n      final currentWorkspaceResult = await repository.getCurrentWorkspace();\n      final currentWorkspace = currentWorkspaceResult.fold(\n        (s) => s,\n        (e) => null,\n      );\n      final currentWorkspaceId = initialWorkspaceId ?? currentWorkspace?.id;\n      final workspacesResult = await repository.getWorkspaces();\n      final workspaces = workspacesResult.getOrThrow();\n\n      if (workspaces.isEmpty && currentWorkspace != null) {\n        workspaces.add(\n          _convertWorkspacePBToUserWorkspace(currentWorkspace),\n        );\n      }\n\n      final currentWorkspaceInList = _findWorkspaceById(\n            currentWorkspaceId ?? '',\n            workspaces,\n          ) ??\n          workspaces.firstOrNull;\n\n      final sortedWorkspaces = _sortWorkspaces(workspaces);\n\n      Log.info(\n        'fetch workspaces: current workspace: ${currentWorkspaceInList?.workspaceId}, sorted workspaces: ${sortedWorkspaces.map((e) => '${e.name}: ${e.workspaceId}')}',\n      );\n\n      return _WorkspaceFetchResult(\n        currentWorkspace: currentWorkspaceInList,\n        workspaces: sortedWorkspaces,\n        shouldOpenWorkspace:\n            currentWorkspaceInList?.workspaceId != currentWorkspaceId,\n      );\n    } catch (e) {\n      Log.error('fetch workspace error: $e');\n      return _WorkspaceFetchResult(\n        currentWorkspace: state.currentWorkspace,\n        workspaces: state.workspaces,\n        shouldOpenWorkspace: false,\n      );\n    }\n  }\n\n  UserWorkspacePB _convertWorkspacePBToUserWorkspace(WorkspacePB workspace) {\n    return UserWorkspacePB.create()\n      ..workspaceId = workspace.id\n      ..name = workspace.name\n      ..createdAtTimestamp = workspace.createTime;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/workspace/logic/workspace_event.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\n\nsealed class UserWorkspaceEvent {\n  UserWorkspaceEvent();\n\n  // Factory functions for creating events\n  factory UserWorkspaceEvent.initialize() => WorkspaceEventInitialize();\n\n  factory UserWorkspaceEvent.fetchWorkspaces({\n    String? initialWorkspaceId,\n  }) =>\n      WorkspaceEventFetchWorkspaces(initialWorkspaceId: initialWorkspaceId);\n\n  factory UserWorkspaceEvent.createWorkspace({\n    required String name,\n    required WorkspaceTypePB workspaceType,\n  }) =>\n      WorkspaceEventCreateWorkspace(name: name, workspaceType: workspaceType);\n\n  factory UserWorkspaceEvent.deleteWorkspace({\n    required String workspaceId,\n  }) =>\n      WorkspaceEventDeleteWorkspace(workspaceId: workspaceId);\n\n  factory UserWorkspaceEvent.openWorkspace({\n    required String workspaceId,\n    required WorkspaceTypePB workspaceType,\n  }) =>\n      WorkspaceEventOpenWorkspace(\n        workspaceId: workspaceId,\n        workspaceType: workspaceType,\n      );\n\n  factory UserWorkspaceEvent.renameWorkspace({\n    required String workspaceId,\n    required String name,\n  }) =>\n      WorkspaceEventRenameWorkspace(workspaceId: workspaceId, name: name);\n\n  factory UserWorkspaceEvent.updateWorkspaceIcon({\n    required String workspaceId,\n    required String icon,\n  }) =>\n      WorkspaceEventUpdateWorkspaceIcon(\n        workspaceId: workspaceId,\n        icon: icon,\n      );\n\n  factory UserWorkspaceEvent.leaveWorkspace({\n    required String workspaceId,\n  }) =>\n      WorkspaceEventLeaveWorkspace(workspaceId: workspaceId);\n\n  factory UserWorkspaceEvent.fetchWorkspaceSubscriptionInfo({\n    required String workspaceId,\n  }) =>\n      WorkspaceEventFetchWorkspaceSubscriptionInfo(workspaceId: workspaceId);\n\n  factory UserWorkspaceEvent.updateWorkspaceSubscriptionInfo({\n    required String workspaceId,\n    required WorkspaceSubscriptionInfoPB subscriptionInfo,\n  }) =>\n      WorkspaceEventUpdateWorkspaceSubscriptionInfo(\n        workspaceId: workspaceId,\n        subscriptionInfo: subscriptionInfo,\n      );\n\n  factory UserWorkspaceEvent.emitWorkspaces({\n    required List<UserWorkspacePB> workspaces,\n  }) =>\n      WorkspaceEventEmitWorkspaces(workspaces: workspaces);\n\n  factory UserWorkspaceEvent.emitUserProfile({\n    required UserProfilePB userProfile,\n  }) =>\n      WorkspaceEventEmitUserProfile(userProfile: userProfile);\n\n  factory UserWorkspaceEvent.emitCurrentWorkspace({\n    required UserWorkspacePB workspace,\n  }) =>\n      WorkspaceEventEmitCurrentWorkspace(workspace: workspace);\n}\n\n/// Initializes the workspace bloc.\nclass WorkspaceEventInitialize extends UserWorkspaceEvent {\n  WorkspaceEventInitialize();\n}\n\n/// Fetches the list of workspaces for the current user.\nclass WorkspaceEventFetchWorkspaces extends UserWorkspaceEvent {\n  WorkspaceEventFetchWorkspaces({\n    this.initialWorkspaceId,\n  });\n\n  final String? initialWorkspaceId;\n}\n\n/// Creates a new workspace.\nclass WorkspaceEventCreateWorkspace extends UserWorkspaceEvent {\n  WorkspaceEventCreateWorkspace({\n    required this.name,\n    required this.workspaceType,\n  });\n\n  final String name;\n  final WorkspaceTypePB workspaceType;\n}\n\n/// Deletes a workspace.\nclass WorkspaceEventDeleteWorkspace extends UserWorkspaceEvent {\n  WorkspaceEventDeleteWorkspace({\n    required this.workspaceId,\n  });\n\n  final String workspaceId;\n}\n\n/// Opens a workspace.\nclass WorkspaceEventOpenWorkspace extends UserWorkspaceEvent {\n  WorkspaceEventOpenWorkspace({\n    required this.workspaceId,\n    required this.workspaceType,\n  });\n\n  final String workspaceId;\n  final WorkspaceTypePB workspaceType;\n}\n\n/// Renames a workspace.\nclass WorkspaceEventRenameWorkspace extends UserWorkspaceEvent {\n  WorkspaceEventRenameWorkspace({\n    required this.workspaceId,\n    required this.name,\n  });\n\n  final String workspaceId;\n  final String name;\n}\n\n/// Updates workspace icon.\nclass WorkspaceEventUpdateWorkspaceIcon extends UserWorkspaceEvent {\n  WorkspaceEventUpdateWorkspaceIcon({\n    required this.workspaceId,\n    required this.icon,\n  });\n\n  final String workspaceId;\n  final String icon;\n}\n\n/// Leaves a workspace.\nclass WorkspaceEventLeaveWorkspace extends UserWorkspaceEvent {\n  WorkspaceEventLeaveWorkspace({\n    required this.workspaceId,\n  });\n\n  final String workspaceId;\n}\n\n/// Fetches workspace subscription info.\nclass WorkspaceEventFetchWorkspaceSubscriptionInfo extends UserWorkspaceEvent {\n  WorkspaceEventFetchWorkspaceSubscriptionInfo({\n    required this.workspaceId,\n  });\n\n  final String workspaceId;\n}\n\n/// Updates workspace subscription info.\nclass WorkspaceEventUpdateWorkspaceSubscriptionInfo extends UserWorkspaceEvent {\n  WorkspaceEventUpdateWorkspaceSubscriptionInfo({\n    required this.workspaceId,\n    required this.subscriptionInfo,\n  });\n\n  final String workspaceId;\n  final WorkspaceSubscriptionInfoPB subscriptionInfo;\n}\n\nclass WorkspaceEventEmitWorkspaces extends UserWorkspaceEvent {\n  WorkspaceEventEmitWorkspaces({\n    required this.workspaces,\n  });\n\n  final List<UserWorkspacePB> workspaces;\n}\n\nclass WorkspaceEventEmitUserProfile extends UserWorkspaceEvent {\n  WorkspaceEventEmitUserProfile({\n    required this.userProfile,\n  });\n\n  final UserProfilePB userProfile;\n}\n\nclass WorkspaceEventEmitCurrentWorkspace extends UserWorkspaceEvent {\n  WorkspaceEventEmitCurrentWorkspace({\n    required this.workspace,\n  });\n\n  final UserWorkspacePB workspace;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/features/workspace/logic/workspace_state.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nenum WorkspaceActionType {\n  none,\n  create,\n  delete,\n  open,\n  rename,\n  updateIcon,\n  fetchWorkspaces,\n  leave,\n  fetchSubscriptionInfo,\n}\n\nclass WorkspaceActionResult {\n  const WorkspaceActionResult({\n    required this.actionType,\n    required this.isLoading,\n    required this.result,\n  });\n\n  final WorkspaceActionType actionType;\n  final bool isLoading;\n  final FlowyResult<void, FlowyError>? result;\n\n  @override\n  String toString() {\n    return 'WorkspaceActionResult(actionType: $actionType, isLoading: $isLoading, result: $result)';\n  }\n}\n\nclass UserWorkspaceState {\n  factory UserWorkspaceState.initial(UserProfilePB userProfile) =>\n      UserWorkspaceState(\n        userProfile: userProfile,\n      );\n\n  const UserWorkspaceState({\n    this.currentWorkspace,\n    this.workspaces = const [],\n    this.actionResult,\n    this.isCollabWorkspaceOn = false,\n    required this.userProfile,\n    this.workspaceSubscriptionInfo,\n  });\n\n  final UserWorkspacePB? currentWorkspace;\n  final List<UserWorkspacePB> workspaces;\n  final WorkspaceActionResult? actionResult;\n  final bool isCollabWorkspaceOn;\n  final UserProfilePB userProfile;\n  final WorkspaceSubscriptionInfoPB? workspaceSubscriptionInfo;\n\n  UserWorkspaceState copyWith({\n    UserWorkspacePB? currentWorkspace,\n    List<UserWorkspacePB>? workspaces,\n    WorkspaceActionResult? actionResult,\n    bool? isCollabWorkspaceOn,\n    UserProfilePB? userProfile,\n    WorkspaceSubscriptionInfoPB? workspaceSubscriptionInfo,\n  }) {\n    return UserWorkspaceState(\n      currentWorkspace: currentWorkspace ?? this.currentWorkspace,\n      workspaces: workspaces ?? this.workspaces,\n      actionResult: actionResult ?? this.actionResult,\n      isCollabWorkspaceOn: isCollabWorkspaceOn ?? this.isCollabWorkspaceOn,\n      userProfile: userProfile ?? this.userProfile,\n      workspaceSubscriptionInfo:\n          workspaceSubscriptionInfo ?? this.workspaceSubscriptionInfo,\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n    return other is UserWorkspaceState &&\n        other.currentWorkspace == currentWorkspace &&\n        other.workspaces == workspaces &&\n        other.actionResult == actionResult &&\n        other.isCollabWorkspaceOn == isCollabWorkspaceOn &&\n        other.userProfile == userProfile &&\n        other.workspaceSubscriptionInfo == workspaceSubscriptionInfo;\n  }\n\n  @override\n  int get hashCode {\n    return Object.hash(\n      currentWorkspace,\n      workspaces,\n      actionResult,\n      isCollabWorkspaceOn,\n      userProfile,\n      workspaceSubscriptionInfo,\n    );\n  }\n\n  @override\n  String toString() {\n    return 'WorkspaceState(currentWorkspace: $currentWorkspace, workspaces: $workspaces, actionResult: $actionResult, isCollabWorkspaceOn: $isCollabWorkspaceOn, userProfile: $userProfile, workspaceSubscriptionInfo: $workspaceSubscriptionInfo)';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart",
    "content": "// Copyright 2014 The Flutter Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style license that can be\n// found in the LICENSE file.\n\n// TODO(Mathias): Make a PR in Flutter repository that enables customizing\n//  the dropdown menu without having to copy the entire file.\n// This is a temporary solution!\n\nimport 'dart:math' as math;\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:flutter/services.dart';\n\nconst double _kMinimumWidth = 112.0;\n\nconst double _kDefaultHorizontalPadding = 12.0;\n\ntypedef CompareFunction<T> = bool Function(T? left, T? right);\n\n// Navigation shortcuts to move the selected menu items up or down.\nfinal Map<ShortcutActivator, Intent> _kMenuTraversalShortcuts =\n    <ShortcutActivator, Intent>{\n  LogicalKeySet(LogicalKeyboardKey.arrowUp): const _ArrowUpIntent(),\n  LogicalKeySet(LogicalKeyboardKey.arrowDown): const _ArrowDownIntent(),\n};\n\n/// A dropdown menu that can be opened from a [TextField]. The selected\n/// menu item is displayed in that field.\n///\n/// This widget is used to help people make a choice from a menu and put the\n/// selected item into the text input field. People can also filter the list based\n/// on the text input or search one item in the menu list.\n///\n/// The menu is composed of a list of [DropdownMenuEntry]s. People can provide information,\n/// such as: label, leading icon or trailing icon for each entry. The [TextField]\n/// will be updated based on the selection from the menu entries. The text field\n/// will stay empty if the selected entry is disabled.\n///\n/// The dropdown menu can be traversed by pressing the up or down key. During the\n/// process, the corresponding item will be highlighted and the text field will be updated.\n/// Disabled items will be skipped during traversal.\n///\n/// The menu can be scrollable if not all items in the list are displayed at once.\n///\n/// {@tool dartpad}\n/// This sample shows how to display outlined [AFDropdownMenu] and filled [AFDropdownMenu].\n///\n/// ** See code in examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart **\n/// {@end-tool}\n///\n/// See also:\n///\n/// * [MenuAnchor], which is a widget used to mark the \"anchor\" for a set of submenus.\n///   The [AFDropdownMenu] uses a [TextField] as the \"anchor\".\n/// * [TextField], which is a text input widget that uses an [InputDecoration].\n/// * [DropdownMenuEntry], which is used to build the [MenuItemButton] in the [AFDropdownMenu] list.\nclass AFDropdownMenu<T> extends StatefulWidget {\n  /// Creates a const [AFDropdownMenu].\n  ///\n  /// The leading and trailing icons in the text field can be customized by using\n  /// [leadingIcon], [trailingIcon] and [selectedTrailingIcon] properties. They are\n  /// passed down to the [InputDecoration] properties, and will override values\n  /// in the [InputDecoration.prefixIcon] and [InputDecoration.suffixIcon].\n  ///\n  /// Except leading and trailing icons, the text field can be configured by the\n  /// [InputDecorationTheme] property. The menu can be configured by the [menuStyle].\n  const AFDropdownMenu({\n    super.key,\n    this.enabled = true,\n    this.width,\n    this.menuHeight,\n    this.leadingIcon,\n    this.trailingIcon,\n    this.label,\n    this.hintText,\n    this.helperText,\n    this.errorText,\n    this.selectedTrailingIcon,\n    this.enableFilter = false,\n    this.enableSearch = true,\n    this.textStyle,\n    this.inputDecorationTheme,\n    this.menuStyle,\n    this.controller,\n    this.initialSelection,\n    this.onSelected,\n    this.requestFocusOnTap,\n    this.expandedInsets,\n    this.searchCallback,\n    this.selectOptionCompare,\n    required this.dropdownMenuEntries,\n  });\n\n  /// Determine if the [AFDropdownMenu] is enabled.\n  ///\n  /// Defaults to true.\n  final bool enabled;\n\n  /// Determine the width of the [AFDropdownMenu].\n  ///\n  /// If this is null, the width of the [AFDropdownMenu] will be the same as the width of the widest\n  /// menu item plus the width of the leading/trailing icon.\n  final double? width;\n\n  /// Determine the height of the menu.\n  ///\n  /// If this is null, the menu will display as many items as possible on the screen.\n  final double? menuHeight;\n\n  /// An optional Icon at the front of the text input field.\n  ///\n  /// Defaults to null. If this is not null, the menu items will have extra paddings to be aligned\n  /// with the text in the text field.\n  final Widget? leadingIcon;\n\n  /// An optional icon at the end of the text field.\n  ///\n  /// Defaults to an [Icon] with [Icons.arrow_drop_down].\n  final Widget? trailingIcon;\n\n  /// Optional widget that describes the input field.\n  ///\n  /// When the input field is empty and unfocused, the label is displayed on\n  /// top of the input field (i.e., at the same location on the screen where\n  /// text may be entered in the input field). When the input field receives\n  /// focus (or if the field is non-empty), the label moves above, either\n  /// vertically adjacent to, or to the center of the input field.\n  ///\n  /// Defaults to null.\n  final Widget? label;\n\n  /// Text that suggests what sort of input the field accepts.\n  ///\n  /// Defaults to null;\n  final String? hintText;\n\n  /// Text that provides context about the [AFDropdownMenu]'s value, such\n  /// as how the value will be used.\n  ///\n  /// If non-null, the text is displayed below the input field, in\n  /// the same location as [errorText]. If a non-null [errorText] value is\n  /// specified then the helper text is not shown.\n  ///\n  /// Defaults to null;\n  ///\n  /// See also:\n  ///\n  /// * [InputDecoration.helperText], which is the text that provides context about the [InputDecorator.child]'s value.\n  final String? helperText;\n\n  /// Text that appears below the input field and the border to show the error message.\n  ///\n  /// If non-null, the border's color animates to red and the [helperText] is not shown.\n  ///\n  /// Defaults to null;\n  ///\n  /// See also:\n  ///\n  /// * [InputDecoration.errorText], which is the text that appears below the [InputDecorator.child] and the border.\n  final String? errorText;\n\n  /// An optional icon at the end of the text field to indicate that the text\n  /// field is pressed.\n  ///\n  /// Defaults to an [Icon] with [Icons.arrow_drop_up].\n  final Widget? selectedTrailingIcon;\n\n  /// Determine if the menu list can be filtered by the text input.\n  ///\n  /// Defaults to false.\n  final bool enableFilter;\n\n  /// Determine if the first item that matches the text input can be highlighted.\n  ///\n  /// Defaults to true as the search function could be commonly used.\n  final bool enableSearch;\n\n  /// The text style for the [TextField] of the [AFDropdownMenu];\n  ///\n  /// Defaults to the overall theme's [TextTheme.bodyLarge]\n  /// if the dropdown menu theme's value is null.\n  final TextStyle? textStyle;\n\n  /// Defines the default appearance of [InputDecoration] to show around the text field.\n  ///\n  /// By default, shows a outlined text field.\n  final InputDecorationTheme? inputDecorationTheme;\n\n  /// The [MenuStyle] that defines the visual attributes of the menu.\n  ///\n  /// The default width of the menu is set to the width of the text field.\n  final MenuStyle? menuStyle;\n\n  /// Controls the text being edited or selected in the menu.\n  ///\n  /// If null, this widget will create its own [TextEditingController].\n  final TextEditingController? controller;\n\n  /// The value used to for an initial selection.\n  ///\n  /// Defaults to null.\n  final T? initialSelection;\n\n  /// The callback is called when a selection is made.\n  ///\n  /// Defaults to null. If null, only the text field is updated.\n  final ValueChanged<T?>? onSelected;\n\n  /// Determine if the dropdown button requests focus and the on-screen virtual\n  /// keyboard is shown in response to a touch event.\n  ///\n  /// By default, on mobile platforms, tapping on the text field and opening\n  /// the menu will not cause a focus request and the virtual keyboard will not\n  /// appear. The default behavior for desktop platforms is for the dropdown to\n  /// take the focus.\n  ///\n  /// Defaults to null. Setting this field to true or false, rather than allowing\n  /// the implementation to choose based on the platform, can be useful for\n  /// applications that want to override the default behavior.\n  final bool? requestFocusOnTap;\n\n  /// Descriptions of the menu items in the [AFDropdownMenu].\n  ///\n  /// This is a required parameter. It is recommended that at least one [DropdownMenuEntry]\n  /// is provided. If this is an empty list, the menu will be empty and only\n  /// contain space for padding.\n  final List<DropdownMenuEntry<T>> dropdownMenuEntries;\n\n  /// Defines the menu text field's width to be equal to its parent's width\n  /// plus the horizontal width of the specified insets.\n  ///\n  /// If this property is null, the width of the text field will be determined\n  /// by the width of menu items or [AFDropdownMenu.width]. If this property is not null,\n  /// the text field's width will match the parent's width plus the specified insets.\n  /// If the value of this property is [EdgeInsets.zero], the width of the text field will be the same\n  /// as its parent's width.\n  ///\n  /// The [expandedInsets]' top and bottom are ignored, only its left and right\n  /// properties are used.\n  ///\n  /// Defaults to null.\n  final EdgeInsets? expandedInsets;\n\n  /// When  [AFDropdownMenu.enableSearch] is true, this callback is used to compute\n  /// the index of the search result to be highlighted.\n  ///\n  /// {@tool snippet}\n  ///\n  /// In this example the `searchCallback` returns the index of the search result\n  /// that exactly matches the query.\n  ///\n  /// ```dart\n  /// DropdownMenu<Text>(\n  ///   searchCallback: (List<DropdownMenuEntry<Text>> entries, String query) {\n  ///     if (query.isEmpty) {\n  ///       return null;\n  ///     }\n  ///     final int index = entries.indexWhere((DropdownMenuEntry<Text> entry) => entry.label == query);\n  ///\n  ///     return index != -1 ? index : null;\n  ///   },\n  ///   dropdownMenuEntries: const <DropdownMenuEntry<Text>>[],\n  /// )\n  /// ```\n  /// {@end-tool}\n  ///\n  /// Defaults to null. If this is null and [AFDropdownMenu.enableSearch] is true,\n  /// the default function will return the index of the first matching result\n  /// which contains the contents of the text input field.\n  final SearchCallback<T>? searchCallback;\n\n  /// Defines the compare function for the menu items.\n  ///\n  /// Defaults to null. If this is null, the menu items will be sorted by the label.\n  final CompareFunction<T>? selectOptionCompare;\n\n  @override\n  State<AFDropdownMenu<T>> createState() => _AFDropdownMenuState<T>();\n}\n\nclass _AFDropdownMenuState<T> extends State<AFDropdownMenu<T>> {\n  final GlobalKey _anchorKey = GlobalKey();\n  final GlobalKey _leadingKey = GlobalKey();\n  late List<GlobalKey> buttonItemKeys;\n  final MenuController _controller = MenuController();\n  late bool _enableFilter;\n  late List<DropdownMenuEntry<T>> filteredEntries;\n  List<Widget>? _initialMenu;\n  int? currentHighlight;\n  double? leadingPadding;\n  bool _menuHasEnabledItem = false;\n  TextEditingController? _localTextEditingController;\n  TextEditingController get _textEditingController {\n    return widget.controller ??\n        (_localTextEditingController ??= TextEditingController());\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _enableFilter = widget.enableFilter;\n    filteredEntries = widget.dropdownMenuEntries;\n    buttonItemKeys = List<GlobalKey>.generate(\n      filteredEntries.length,\n      (int index) => GlobalKey(),\n    );\n    _menuHasEnabledItem =\n        filteredEntries.any((DropdownMenuEntry<T> entry) => entry.enabled);\n\n    final int index = filteredEntries.indexWhere(\n      (DropdownMenuEntry<T> entry) {\n        if (widget.selectOptionCompare != null) {\n          return widget.selectOptionCompare!(\n            entry.value,\n            widget.initialSelection,\n          );\n        } else {\n          return entry.value == widget.initialSelection;\n        }\n      },\n    );\n    if (index != -1) {\n      _textEditingController.value = TextEditingValue(\n        text: filteredEntries[index].label,\n        selection: TextSelection.collapsed(\n          offset: filteredEntries[index].label.length,\n        ),\n      );\n    }\n    refreshLeadingPadding();\n  }\n\n  @override\n  void dispose() {\n    _localTextEditingController?.dispose();\n    _localTextEditingController = null;\n    super.dispose();\n  }\n\n  @override\n  void didUpdateWidget(AFDropdownMenu<T> oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    if (oldWidget.controller != widget.controller) {\n      if (widget.controller != null) {\n        _localTextEditingController?.dispose();\n        _localTextEditingController = null;\n      }\n    }\n    if (oldWidget.enableSearch != widget.enableSearch) {\n      if (!widget.enableSearch) {\n        currentHighlight = null;\n      }\n    }\n    if (oldWidget.dropdownMenuEntries != widget.dropdownMenuEntries) {\n      currentHighlight = null;\n      filteredEntries = widget.dropdownMenuEntries;\n      buttonItemKeys = List<GlobalKey>.generate(\n        filteredEntries.length,\n        (int index) => GlobalKey(),\n      );\n      _menuHasEnabledItem =\n          filteredEntries.any((DropdownMenuEntry<T> entry) => entry.enabled);\n    }\n    if (oldWidget.leadingIcon != widget.leadingIcon) {\n      refreshLeadingPadding();\n    }\n    if (oldWidget.initialSelection != widget.initialSelection) {\n      final int index = filteredEntries.indexWhere(\n        (DropdownMenuEntry<T> entry) => entry.value == widget.initialSelection,\n      );\n      if (index != -1) {\n        _textEditingController.value = TextEditingValue(\n          text: filteredEntries[index].label,\n          selection: TextSelection.collapsed(\n            offset: filteredEntries[index].label.length,\n          ),\n        );\n      }\n    }\n  }\n\n  bool canRequestFocus() {\n    if (widget.requestFocusOnTap != null) {\n      return widget.requestFocusOnTap!;\n    }\n\n    switch (Theme.of(context).platform) {\n      case TargetPlatform.iOS:\n      case TargetPlatform.android:\n      case TargetPlatform.fuchsia:\n        return false;\n      case TargetPlatform.macOS:\n      case TargetPlatform.linux:\n      case TargetPlatform.windows:\n        return true;\n    }\n  }\n\n  void refreshLeadingPadding() {\n    WidgetsBinding.instance.addPostFrameCallback(\n      (_) {\n        setState(() {\n          leadingPadding = getWidth(_leadingKey);\n        });\n      },\n      debugLabel: 'DropdownMenu.refreshLeadingPadding',\n    );\n  }\n\n  // Remove the code here, it will throw a FlutterError\n  // Unless we upgrade to Flutter 3.24 https://github.com/flutter/flutter/issues/146764\n  void scrollToHighlight() {\n    // WidgetsBinding.instance.addPostFrameCallback(\n    //   (_) {\n    //     // try {\n    //   final BuildContext? highlightContext =\n    //       buttonItemKeys[currentHighlight!].currentContext;\n    //   if (highlightContext != null) {\n    //     Scrollable.ensureVisible(highlightContext);\n    //   }\n    // } catch (_) {\n    //   return;\n    // }\n    //   },\n    //   debugLabel: 'DropdownMenu.scrollToHighlight',\n    // );\n  }\n\n  double? getWidth(GlobalKey key) {\n    final BuildContext? context = key.currentContext;\n    if (context != null) {\n      final RenderBox box = context.findRenderObject()! as RenderBox;\n      return box.hasSize ? box.size.width : null;\n    }\n    return null;\n  }\n\n  List<DropdownMenuEntry<T>> filter(\n    List<DropdownMenuEntry<T>> entries,\n    TextEditingController textEditingController,\n  ) {\n    final String filterText = textEditingController.text.toLowerCase();\n    return entries\n        .where(\n          (DropdownMenuEntry<T> entry) =>\n              entry.label.toLowerCase().contains(filterText),\n        )\n        .toList();\n  }\n\n  int? search(\n    List<DropdownMenuEntry<T>> entries,\n    TextEditingController textEditingController,\n  ) {\n    final String searchText = textEditingController.value.text.toLowerCase();\n    if (searchText.isEmpty) {\n      return null;\n    }\n    final int index = entries.indexWhere(\n      (DropdownMenuEntry<T> entry) =>\n          entry.label.toLowerCase().contains(searchText),\n    );\n\n    return index != -1 ? index : null;\n  }\n\n  List<Widget> _buildButtons(\n    List<DropdownMenuEntry<T>> filteredEntries,\n    TextDirection textDirection, {\n    int? focusedIndex,\n    bool enableScrollToHighlight = true,\n  }) {\n    final List<Widget> result = <Widget>[];\n    for (int i = 0; i < filteredEntries.length; i++) {\n      final DropdownMenuEntry<T> entry = filteredEntries[i];\n\n      // By default, when the text field has a leading icon but a menu entry doesn't\n      // have one, the label of the entry should have extra padding to be aligned\n      // with the text in the text input field. When both the text field and the\n      // menu entry have leading icons, the menu entry should remove the extra\n      // paddings so its leading icon will be aligned with the leading icon of\n      // the text field.\n      final double padding = entry.leadingIcon == null\n          ? (leadingPadding ?? _kDefaultHorizontalPadding)\n          : _kDefaultHorizontalPadding;\n      final ButtonStyle defaultStyle;\n      switch (textDirection) {\n        case TextDirection.rtl:\n          defaultStyle = MenuItemButton.styleFrom(\n            padding: EdgeInsets.only(\n              left: _kDefaultHorizontalPadding,\n              right: padding,\n            ),\n          );\n        case TextDirection.ltr:\n          defaultStyle = MenuItemButton.styleFrom(\n            padding: EdgeInsets.only(\n              left: padding,\n              right: _kDefaultHorizontalPadding,\n            ),\n          );\n      }\n\n      ButtonStyle effectiveStyle = entry.style ?? defaultStyle;\n      final Color focusedBackgroundColor = effectiveStyle.foregroundColor\n              ?.resolve(<WidgetState>{WidgetState.focused}) ??\n          Theme.of(context).colorScheme.onSurface;\n\n      Widget label = entry.labelWidget ?? Text(entry.label);\n      if (widget.width != null) {\n        final double horizontalPadding = padding + _kDefaultHorizontalPadding;\n        label = ConstrainedBox(\n          constraints:\n              BoxConstraints(maxWidth: widget.width! - horizontalPadding),\n          child: label,\n        );\n      }\n\n      // Simulate the focused state because the text field should always be focused\n      // during traversal. If the menu item has a custom foreground color, the \"focused\"\n      // color will also change to foregroundColor.withValues(alpha: 0.12).\n      effectiveStyle = entry.enabled && i == focusedIndex\n          ? effectiveStyle.copyWith(\n              backgroundColor: WidgetStatePropertyAll<Color>(\n                focusedBackgroundColor.withValues(alpha: 0.12),\n              ),\n            )\n          : effectiveStyle;\n\n      final Widget menuItemButton = Padding(\n        padding: const EdgeInsets.only(bottom: 6),\n        child: MenuItemButton(\n          key: enableScrollToHighlight ? buttonItemKeys[i] : null,\n          style: effectiveStyle,\n          leadingIcon: entry.leadingIcon,\n          trailingIcon: entry.trailingIcon,\n          onPressed: entry.enabled\n              ? () {\n                  _textEditingController.value = TextEditingValue(\n                    text: entry.label,\n                    selection:\n                        TextSelection.collapsed(offset: entry.label.length),\n                  );\n                  currentHighlight = widget.enableSearch ? i : null;\n                  widget.onSelected?.call(entry.value);\n                }\n              : null,\n          requestFocusOnHover: false,\n          child: label,\n        ),\n      );\n      result.add(menuItemButton);\n    }\n\n    return result;\n  }\n\n  void handleUpKeyInvoke(_) {\n    setState(() {\n      if (!_menuHasEnabledItem || !_controller.isOpen) {\n        return;\n      }\n      _enableFilter = false;\n      currentHighlight ??= 0;\n      currentHighlight = (currentHighlight! - 1) % filteredEntries.length;\n      while (!filteredEntries[currentHighlight!].enabled) {\n        currentHighlight = (currentHighlight! - 1) % filteredEntries.length;\n      }\n      final String currentLabel = filteredEntries[currentHighlight!].label;\n      _textEditingController.value = TextEditingValue(\n        text: currentLabel,\n        selection: TextSelection.collapsed(offset: currentLabel.length),\n      );\n    });\n  }\n\n  void handleDownKeyInvoke(_) {\n    setState(() {\n      if (!_menuHasEnabledItem || !_controller.isOpen) {\n        return;\n      }\n      _enableFilter = false;\n      currentHighlight ??= -1;\n      currentHighlight = (currentHighlight! + 1) % filteredEntries.length;\n      while (!filteredEntries[currentHighlight!].enabled) {\n        currentHighlight = (currentHighlight! + 1) % filteredEntries.length;\n      }\n      final String currentLabel = filteredEntries[currentHighlight!].label;\n      _textEditingController.value = TextEditingValue(\n        text: currentLabel,\n        selection: TextSelection.collapsed(offset: currentLabel.length),\n      );\n    });\n  }\n\n  void handlePressed(MenuController controller) {\n    if (controller.isOpen) {\n      currentHighlight = null;\n      controller.close();\n    } else {\n      // close to open\n      if (_textEditingController.text.isNotEmpty) {\n        _enableFilter = false;\n      }\n      controller.open();\n    }\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final TextDirection textDirection = Directionality.of(context);\n    _initialMenu ??= _buildButtons(\n      widget.dropdownMenuEntries,\n      textDirection,\n      enableScrollToHighlight: false,\n    );\n    final DropdownMenuThemeData theme = DropdownMenuTheme.of(context);\n    final DropdownMenuThemeData defaults = _DropdownMenuDefaultsM3(context);\n\n    if (_enableFilter) {\n      filteredEntries =\n          filter(widget.dropdownMenuEntries, _textEditingController);\n    }\n\n    if (widget.enableSearch) {\n      if (widget.searchCallback != null) {\n        currentHighlight = widget.searchCallback!\n            .call(filteredEntries, _textEditingController.text);\n      } else {\n        currentHighlight = search(filteredEntries, _textEditingController);\n      }\n      if (currentHighlight != null) {\n        scrollToHighlight();\n      }\n    }\n\n    final List<Widget> menu = _buildButtons(\n      filteredEntries,\n      textDirection,\n      focusedIndex: currentHighlight,\n    );\n\n    final TextStyle? effectiveTextStyle =\n        widget.textStyle ?? theme.textStyle ?? defaults.textStyle;\n\n    MenuStyle? effectiveMenuStyle =\n        widget.menuStyle ?? theme.menuStyle ?? defaults.menuStyle!;\n\n    final double? anchorWidth = getWidth(_anchorKey);\n    if (widget.width != null) {\n      effectiveMenuStyle = effectiveMenuStyle.copyWith(\n        minimumSize: WidgetStatePropertyAll<Size?>(Size(widget.width!, 0.0)),\n      );\n    } else if (anchorWidth != null) {\n      effectiveMenuStyle = effectiveMenuStyle.copyWith(\n        minimumSize: WidgetStatePropertyAll<Size?>(Size(anchorWidth, 0.0)),\n      );\n    }\n\n    if (widget.menuHeight != null) {\n      effectiveMenuStyle = effectiveMenuStyle.copyWith(\n        maximumSize: WidgetStatePropertyAll<Size>(\n          Size(double.infinity, widget.menuHeight!),\n        ),\n      );\n    }\n    final InputDecorationTheme effectiveInputDecorationTheme =\n        widget.inputDecorationTheme ??\n            theme.inputDecorationTheme ??\n            defaults.inputDecorationTheme!;\n\n    final MouseCursor effectiveMouseCursor =\n        canRequestFocus() ? SystemMouseCursors.text : SystemMouseCursors.click;\n\n    Widget menuAnchor = MenuAnchor(\n      style: effectiveMenuStyle,\n      controller: _controller,\n      menuChildren: menu,\n      crossAxisUnconstrained: false,\n      builder: (\n        BuildContext context,\n        MenuController controller,\n        Widget? child,\n      ) {\n        assert(_initialMenu != null);\n        final Widget trailingButton = Padding(\n          padding: const EdgeInsets.all(4.0),\n          child: IconButton(\n            splashRadius: 1,\n            isSelected: controller.isOpen,\n            icon: widget.trailingIcon ?? const Icon(Icons.arrow_drop_down),\n            selectedIcon:\n                widget.selectedTrailingIcon ?? const Icon(Icons.arrow_drop_up),\n            onPressed: () {\n              handlePressed(controller);\n            },\n          ),\n        );\n\n        final Widget leadingButton = Padding(\n          padding: const EdgeInsets.all(8.0),\n          child: widget.leadingIcon ?? const SizedBox(),\n        );\n\n        final Widget textField = TextField(\n          key: _anchorKey,\n          mouseCursor: effectiveMouseCursor,\n          canRequestFocus: canRequestFocus(),\n          enableInteractiveSelection: canRequestFocus(),\n          textAlignVertical: TextAlignVertical.center,\n          style: effectiveTextStyle,\n          controller: _textEditingController,\n          onEditingComplete: () {\n            if (currentHighlight != null) {\n              final DropdownMenuEntry<T> entry =\n                  filteredEntries[currentHighlight!];\n              if (entry.enabled) {\n                _textEditingController.value = TextEditingValue(\n                  text: entry.label,\n                  selection:\n                      TextSelection.collapsed(offset: entry.label.length),\n                );\n                widget.onSelected?.call(entry.value);\n              }\n            } else {\n              widget.onSelected?.call(null);\n            }\n            if (!widget.enableSearch) {\n              currentHighlight = null;\n            }\n            controller.close();\n          },\n          onTap: () {\n            handlePressed(controller);\n          },\n          onChanged: (String text) {\n            controller.open();\n            setState(() {\n              filteredEntries = widget.dropdownMenuEntries;\n              _enableFilter = widget.enableFilter;\n            });\n          },\n          decoration: InputDecoration(\n            enabled: widget.enabled,\n            label: widget.label,\n            hintText: widget.hintText,\n            helperText: widget.helperText,\n            errorText: widget.errorText,\n            prefixIcon: widget.leadingIcon != null\n                ? Container(key: _leadingKey, child: widget.leadingIcon)\n                : null,\n            suffixIcon: trailingButton,\n          ).applyDefaults(effectiveInputDecorationTheme),\n        );\n\n        if (widget.expandedInsets != null) {\n          // If [expandedInsets] is not null, the width of the text field should depend\n          // on its parent width. So we don't need to use `_DropdownMenuBody` to\n          // calculate the children's width.\n          return textField;\n        }\n\n        return _DropdownMenuBody(\n          width: widget.width,\n          children: <Widget>[\n            textField,\n            for (final Widget item in _initialMenu!) item,\n            trailingButton,\n            leadingButton,\n          ],\n        );\n      },\n    );\n\n    if (widget.expandedInsets != null) {\n      menuAnchor = Container(\n        alignment: AlignmentDirectional.topStart,\n        padding: widget.expandedInsets?.copyWith(top: 0.0, bottom: 0.0),\n        child: menuAnchor,\n      );\n    }\n\n    return Shortcuts(\n      shortcuts: _kMenuTraversalShortcuts,\n      child: Actions(\n        actions: <Type, Action<Intent>>{\n          _ArrowUpIntent: CallbackAction<_ArrowUpIntent>(\n            onInvoke: handleUpKeyInvoke,\n          ),\n          _ArrowDownIntent: CallbackAction<_ArrowDownIntent>(\n            onInvoke: handleDownKeyInvoke,\n          ),\n        },\n        child: menuAnchor,\n      ),\n    );\n  }\n}\n\nclass _ArrowUpIntent extends Intent {\n  const _ArrowUpIntent();\n}\n\nclass _ArrowDownIntent extends Intent {\n  const _ArrowDownIntent();\n}\n\nclass _DropdownMenuBody extends MultiChildRenderObjectWidget {\n  const _DropdownMenuBody({\n    super.children,\n    this.width,\n  });\n\n  final double? width;\n\n  @override\n  _RenderDropdownMenuBody createRenderObject(BuildContext context) {\n    return _RenderDropdownMenuBody(\n      width: width,\n    );\n  }\n\n  @override\n  void updateRenderObject(\n    BuildContext context,\n    _RenderDropdownMenuBody renderObject,\n  ) {\n    renderObject.width = width;\n  }\n}\n\nclass _DropdownMenuBodyParentData extends ContainerBoxParentData<RenderBox> {}\n\nclass _RenderDropdownMenuBody extends RenderBox\n    with\n        ContainerRenderObjectMixin<RenderBox, _DropdownMenuBodyParentData>,\n        RenderBoxContainerDefaultsMixin<RenderBox,\n            _DropdownMenuBodyParentData> {\n  _RenderDropdownMenuBody({\n    double? width,\n  }) : _width = width;\n\n  double? get width => _width;\n  double? _width;\n  set width(double? value) {\n    if (_width == value) {\n      return;\n    }\n    _width = value;\n    markNeedsLayout();\n  }\n\n  @override\n  void setupParentData(RenderBox child) {\n    if (child.parentData is! _DropdownMenuBodyParentData) {\n      child.parentData = _DropdownMenuBodyParentData();\n    }\n  }\n\n  @override\n  void performLayout() {\n    final BoxConstraints constraints = this.constraints;\n    double maxWidth = 0.0;\n    double? maxHeight;\n    RenderBox? child = firstChild;\n\n    final BoxConstraints innerConstraints = BoxConstraints(\n      maxWidth: width ?? computeMaxIntrinsicWidth(constraints.maxWidth),\n      maxHeight: computeMaxIntrinsicHeight(constraints.maxHeight),\n    );\n    while (child != null) {\n      if (child == firstChild) {\n        child.layout(innerConstraints, parentUsesSize: true);\n        maxHeight ??= child.size.height;\n        final _DropdownMenuBodyParentData childParentData =\n            child.parentData! as _DropdownMenuBodyParentData;\n        assert(child.parentData == childParentData);\n        child = childParentData.nextSibling;\n        continue;\n      }\n      child.layout(innerConstraints, parentUsesSize: true);\n      final _DropdownMenuBodyParentData childParentData =\n          child.parentData! as _DropdownMenuBodyParentData;\n      childParentData.offset = Offset.zero;\n      maxWidth = math.max(maxWidth, child.size.width);\n      maxHeight ??= child.size.height;\n      assert(child.parentData == childParentData);\n      child = childParentData.nextSibling;\n    }\n\n    assert(maxHeight != null);\n    maxWidth = math.max(_kMinimumWidth, maxWidth);\n    size = constraints.constrain(Size(width ?? maxWidth, maxHeight!));\n  }\n\n  @override\n  void paint(PaintingContext context, Offset offset) {\n    final RenderBox? child = firstChild;\n    if (child != null) {\n      final _DropdownMenuBodyParentData childParentData =\n          child.parentData! as _DropdownMenuBodyParentData;\n      context.paintChild(child, offset + childParentData.offset);\n    }\n  }\n\n  @override\n  Size computeDryLayout(BoxConstraints constraints) {\n    final BoxConstraints constraints = this.constraints;\n    double maxWidth = 0.0;\n    double? maxHeight;\n    RenderBox? child = firstChild;\n    final BoxConstraints innerConstraints = BoxConstraints(\n      maxWidth: width ?? computeMaxIntrinsicWidth(constraints.maxWidth),\n      maxHeight: computeMaxIntrinsicHeight(constraints.maxHeight),\n    );\n\n    while (child != null) {\n      if (child == firstChild) {\n        final Size childSize = child.getDryLayout(innerConstraints);\n        maxHeight ??= childSize.height;\n        final _DropdownMenuBodyParentData childParentData =\n            child.parentData! as _DropdownMenuBodyParentData;\n        assert(child.parentData == childParentData);\n        child = childParentData.nextSibling;\n        continue;\n      }\n      final Size childSize = child.getDryLayout(innerConstraints);\n      final _DropdownMenuBodyParentData childParentData =\n          child.parentData! as _DropdownMenuBodyParentData;\n      childParentData.offset = Offset.zero;\n      maxWidth = math.max(maxWidth, childSize.width);\n      maxHeight ??= childSize.height;\n      assert(child.parentData == childParentData);\n      child = childParentData.nextSibling;\n    }\n\n    assert(maxHeight != null);\n    maxWidth = math.max(_kMinimumWidth, maxWidth);\n    return constraints.constrain(Size(width ?? maxWidth, maxHeight!));\n  }\n\n  @override\n  double computeMinIntrinsicWidth(double height) {\n    RenderBox? child = firstChild;\n    double width = 0;\n    while (child != null) {\n      if (child == firstChild) {\n        final _DropdownMenuBodyParentData childParentData =\n            child.parentData! as _DropdownMenuBodyParentData;\n        child = childParentData.nextSibling;\n        continue;\n      }\n      final double maxIntrinsicWidth = child.getMinIntrinsicWidth(height);\n      if (child == lastChild) {\n        width += maxIntrinsicWidth;\n      }\n      if (child == childBefore(lastChild!)) {\n        width += maxIntrinsicWidth;\n      }\n      width = math.max(width, maxIntrinsicWidth);\n      final _DropdownMenuBodyParentData childParentData =\n          child.parentData! as _DropdownMenuBodyParentData;\n      child = childParentData.nextSibling;\n    }\n\n    return math.max(width, _kMinimumWidth);\n  }\n\n  @override\n  double computeMaxIntrinsicWidth(double height) {\n    RenderBox? child = firstChild;\n    double width = 0;\n    while (child != null) {\n      if (child == firstChild) {\n        final _DropdownMenuBodyParentData childParentData =\n            child.parentData! as _DropdownMenuBodyParentData;\n        child = childParentData.nextSibling;\n        continue;\n      }\n      final double maxIntrinsicWidth = child.getMaxIntrinsicWidth(height);\n      // Add the width of leading Icon.\n      if (child == lastChild) {\n        width += maxIntrinsicWidth;\n      }\n      // Add the width of trailing Icon.\n      if (child == childBefore(lastChild!)) {\n        width += maxIntrinsicWidth;\n      }\n      width = math.max(width, maxIntrinsicWidth);\n      final _DropdownMenuBodyParentData childParentData =\n          child.parentData! as _DropdownMenuBodyParentData;\n      child = childParentData.nextSibling;\n    }\n\n    return math.max(width, _kMinimumWidth);\n  }\n\n  @override\n  double computeMinIntrinsicHeight(double height) {\n    final RenderBox? child = firstChild;\n    double width = 0;\n    if (child != null) {\n      width = math.max(width, child.getMinIntrinsicHeight(height));\n    }\n    return width;\n  }\n\n  @override\n  double computeMaxIntrinsicHeight(double height) {\n    final RenderBox? child = firstChild;\n    double width = 0;\n    if (child != null) {\n      width = math.max(width, child.getMaxIntrinsicHeight(height));\n    }\n    return width;\n  }\n\n  @override\n  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {\n    final RenderBox? child = firstChild;\n    if (child != null) {\n      final _DropdownMenuBodyParentData childParentData =\n          child.parentData! as _DropdownMenuBodyParentData;\n      final bool isHit = result.addWithPaintOffset(\n        offset: childParentData.offset,\n        position: position,\n        hitTest: (BoxHitTestResult result, Offset transformed) {\n          assert(transformed == position - childParentData.offset);\n          return child.hitTest(result, position: transformed);\n        },\n      );\n      if (isHit) {\n        return true;\n      }\n    }\n    return false;\n  }\n}\n\n// Hand coded defaults. These will be updated once we have tokens/spec.\nclass _DropdownMenuDefaultsM3 extends DropdownMenuThemeData {\n  _DropdownMenuDefaultsM3(this.context);\n\n  final BuildContext context;\n  late final ThemeData _theme = Theme.of(context);\n\n  @override\n  TextStyle? get textStyle => _theme.textTheme.bodyLarge;\n\n  @override\n  MenuStyle get menuStyle {\n    return const MenuStyle(\n      minimumSize: WidgetStatePropertyAll<Size>(Size(_kMinimumWidth, 0.0)),\n      maximumSize: WidgetStatePropertyAll<Size>(Size.infinite),\n      visualDensity: VisualDensity.standard,\n    );\n  }\n\n  @override\n  InputDecorationTheme get inputDecorationTheme {\n    return const InputDecorationTheme(border: OutlineInputBorder());\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/main.dart",
    "content": "import 'package:scaled_app/scaled_app.dart';\n\nimport 'startup/startup.dart';\n\nFuture<void> main() async {\n  ScaledWidgetsFlutterBinding.ensureInitialized(\n    scaleFactor: (_) => 1.0,\n  );\n\n  await runAppFlowy();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/application/base/mobile_view_page_bloc.dart",
    "content": "import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'mobile_view_page_bloc.freezed.dart';\n\nclass MobileViewPageBloc\n    extends Bloc<MobileViewPageEvent, MobileViewPageState> {\n  MobileViewPageBloc({\n    required this.viewId,\n  })  : _viewListener = ViewListener(viewId: viewId),\n        super(MobileViewPageState.initial()) {\n    on<MobileViewPageEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            _registerListeners();\n\n            final userProfilePB =\n                await UserBackendService.getCurrentUserProfile()\n                    .fold((s) => s, (f) => null);\n            final result = await ViewBackendService.getView(viewId);\n            final isImmersiveMode =\n                _isImmersiveMode(result.fold((s) => s, (f) => null));\n            emit(\n              state.copyWith(\n                isLoading: false,\n                result: result,\n                isImmersiveMode: isImmersiveMode,\n                userProfilePB: userProfilePB,\n              ),\n            );\n          },\n          updateImmersionMode: (isImmersiveMode) {\n            emit(\n              state.copyWith(\n                isImmersiveMode: isImmersiveMode,\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  final String viewId;\n  final ViewListener _viewListener;\n\n  @override\n  Future<void> close() {\n    _viewListener.stop();\n    return super.close();\n  }\n\n  void _registerListeners() {\n    _viewListener.start(\n      onViewUpdated: (view) {\n        final isImmersiveMode = _isImmersiveMode(view);\n        add(MobileViewPageEvent.updateImmersionMode(isImmersiveMode));\n      },\n    );\n  }\n\n  // only the document page supports immersive mode (version 0.5.6)\n  bool _isImmersiveMode(ViewPB? view) {\n    if (view == null) {\n      return false;\n    }\n\n    final cover = view.cover;\n    if (cover == null || cover.type == PageStyleCoverImageType.none) {\n      return false;\n    } else if (view.layout == ViewLayoutPB.Document && !cover.isPresets) {\n      // only support immersive mode for document layout\n      return true;\n    }\n\n    return false;\n  }\n}\n\n@freezed\nclass MobileViewPageEvent with _$MobileViewPageEvent {\n  const factory MobileViewPageEvent.initial() = Initial;\n  const factory MobileViewPageEvent.updateImmersionMode(bool isImmersiveMode) =\n      UpdateImmersionMode;\n}\n\n@freezed\nclass MobileViewPageState with _$MobileViewPageState {\n  const factory MobileViewPageState({\n    @Default(true) bool isLoading,\n    @Default(null) FlowyResult<ViewPB, FlowyError>? result,\n    @Default(false) bool isImmersiveMode,\n    @Default(null) UserProfilePB? userProfilePB,\n  }) = _MobileViewPageState;\n\n  factory MobileViewPageState.initial() => const MobileViewPageState();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:appflowy/mobile/presentation/chat/mobile_chat_screen.dart';\nimport 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart';\nimport 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';\nimport 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';\nimport 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/recent/cached_recent_service.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nextension MobileRouter on BuildContext {\n  Future<void> pushView(\n    ViewPB view, {\n    Map<String, dynamic>? arguments,\n    bool addInRecent = true,\n    bool showMoreButton = true,\n    String? fixedTitle,\n    String? blockId,\n    List<String>? tabs,\n  }) async {\n    // set the current view before pushing the new view\n    getIt<MenuSharedState>().latestOpenView = view;\n    unawaited(getIt<CachedRecentService>().updateRecentViews([view.id], true));\n    final queryParameters = view.queryParameters(arguments);\n\n    if (view.layout == ViewLayoutPB.Document) {\n      queryParameters[MobileDocumentScreen.viewShowMoreButton] =\n          showMoreButton.toString();\n      if (fixedTitle != null) {\n        queryParameters[MobileDocumentScreen.viewFixedTitle] = fixedTitle;\n      }\n      if (blockId != null) {\n        queryParameters[MobileDocumentScreen.viewBlockId] = blockId;\n      }\n    }\n    if (tabs != null) {\n      queryParameters[MobileDocumentScreen.viewSelectTabs] = tabs.join('-');\n    }\n\n    final uri = Uri(\n      path: view.routeName,\n      queryParameters: queryParameters,\n    ).toString();\n    await push(uri);\n  }\n}\n\nextension on ViewPB {\n  String get routeName {\n    switch (layout) {\n      case ViewLayoutPB.Document:\n        return MobileDocumentScreen.routeName;\n      case ViewLayoutPB.Grid:\n        return MobileGridScreen.routeName;\n      case ViewLayoutPB.Calendar:\n        return MobileCalendarScreen.routeName;\n      case ViewLayoutPB.Board:\n        return MobileBoardScreen.routeName;\n      case ViewLayoutPB.Chat:\n        return MobileChatScreen.routeName;\n\n      default:\n        throw UnimplementedError('routeName for $this is not implemented');\n    }\n  }\n\n  Map<String, dynamic> queryParameters([Map<String, dynamic>? arguments]) {\n    switch (layout) {\n      case ViewLayoutPB.Document:\n        return {\n          MobileDocumentScreen.viewId: id,\n          MobileDocumentScreen.viewTitle: name,\n        };\n      case ViewLayoutPB.Grid:\n        return {\n          MobileGridScreen.viewId: id,\n          MobileGridScreen.viewTitle: name,\n          MobileGridScreen.viewArgs: jsonEncode(arguments),\n        };\n      case ViewLayoutPB.Calendar:\n        return {\n          MobileCalendarScreen.viewId: id,\n          MobileCalendarScreen.viewTitle: name,\n        };\n      case ViewLayoutPB.Board:\n        return {\n          MobileBoardScreen.viewId: id,\n          MobileBoardScreen.viewTitle: name,\n        };\n      case ViewLayoutPB.Chat:\n        return {\n          MobileChatScreen.viewId: id,\n          MobileChatScreen.viewTitle: name,\n        };\n      default:\n        throw UnimplementedError(\n          'queryParameters for $this is not implemented',\n        );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/application/notification/notification_reminder_bloc.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';\nimport 'package:appflowy/plugins/document/application/document_service.dart';\nimport 'package:appflowy/user/application/reminder/reminder_extension.dart';\nimport 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';\nimport 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:time/time.dart';\n\npart 'notification_reminder_bloc.freezed.dart';\n\nclass NotificationReminderBloc\n    extends Bloc<NotificationReminderEvent, NotificationReminderState> {\n  NotificationReminderBloc() : super(NotificationReminderState.initial()) {\n    on<NotificationReminderEvent>((event, emit) async {\n      await event.when(\n        initial: (reminder, dateFormat, timeFormat) async {\n          this.reminder = reminder;\n          this.dateFormat = dateFormat;\n          this.timeFormat = timeFormat;\n\n          add(const NotificationReminderEvent.reset());\n        },\n        reset: () async {\n          final scheduledAt = await _getScheduledAt(\n            reminder,\n            dateFormat,\n            timeFormat,\n          );\n          final view = await _getView(reminder);\n\n          if (view == null) {\n            emit(\n              NotificationReminderState(\n                scheduledAt: scheduledAt,\n                pageTitle: '',\n                reminderContent: '',\n                isLocked: false,\n                status: NotificationReminderStatus.error,\n              ),\n            );\n            return;\n          }\n\n          final layout = view.layout;\n\n          if (layout.isDocumentView) {\n            final node = await _getContent(reminder);\n            if (node != null) {\n              emit(\n                NotificationReminderState(\n                  scheduledAt: scheduledAt,\n                  pageTitle: view.nameOrDefault,\n                  isLocked: view.isLocked,\n                  view: view,\n                  reminderContent: node.delta?.toPlainText() ?? '',\n                  nodes: [node],\n                  status: NotificationReminderStatus.loaded,\n                  blockId: reminder.meta[ReminderMetaKeys.blockId],\n                ),\n              );\n            }\n          } else if (layout.isDatabaseView) {\n            emit(\n              NotificationReminderState(\n                scheduledAt: scheduledAt,\n                pageTitle: view.nameOrDefault,\n                isLocked: view.isLocked,\n                view: view,\n                reminderContent: reminder.message,\n                status: NotificationReminderStatus.loaded,\n              ),\n            );\n          }\n        },\n      );\n    });\n  }\n\n  late final ReminderPB reminder;\n  late final UserDateFormatPB dateFormat;\n  late final UserTimeFormatPB timeFormat;\n\n  Future<String> _getScheduledAt(\n    ReminderPB reminder,\n    UserDateFormatPB dateFormat,\n    UserTimeFormatPB timeFormat,\n  ) async {\n    return _formatTimestamp(\n      reminder.scheduledAt.toInt() * 1000,\n      timeFormat: timeFormat,\n      dateFormate: dateFormat,\n    );\n  }\n\n  Future<ViewPB?> _getView(ReminderPB reminder) async {\n    return ViewBackendService.getView(reminder.objectId)\n        .fold((s) => s, (_) => null);\n  }\n\n  Future<Node?> _getContent(ReminderPB reminder) async {\n    final blockId = reminder.meta[ReminderMetaKeys.blockId];\n\n    if (blockId == null) {\n      return null;\n    }\n\n    final document = await DocumentService()\n        .openDocument(\n          documentId: reminder.objectId,\n        )\n        .fold((s) => s.toDocument(), (_) => null);\n\n    if (document == null) {\n      return null;\n    }\n\n    final node = _searchById(document.root, blockId);\n\n    if (node == null) {\n      return null;\n    }\n\n    return node;\n  }\n\n  Node? _searchById(Node current, String id) {\n    if (current.id == id) {\n      return current;\n    }\n\n    if (current.children.isNotEmpty) {\n      for (final child in current.children) {\n        final node = _searchById(child, id);\n\n        if (node != null) {\n          return node;\n        }\n      }\n    }\n\n    return null;\n  }\n\n  String _formatTimestamp(\n    int timestamp, {\n    required UserDateFormatPB dateFormate,\n    required UserTimeFormatPB timeFormat,\n  }) {\n    final now = DateTime.now();\n    final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);\n    final difference = now.difference(dateTime);\n    final String date;\n\n    if (difference.inMinutes < 1) {\n      date = LocaleKeys.sideBar_justNow.tr();\n    } else if (difference.inHours < 1 && dateTime.isToday) {\n      // Less than 1 hour\n      date = LocaleKeys.sideBar_minutesAgo\n          .tr(namedArgs: {'count': difference.inMinutes.toString()});\n    } else if (difference.inHours >= 1 && dateTime.isToday) {\n      // in same day\n      date = timeFormat.formatTime(dateTime);\n    } else {\n      date = dateFormate.formatDate(dateTime, false);\n    }\n\n    return date;\n  }\n}\n\n@freezed\nclass NotificationReminderEvent with _$NotificationReminderEvent {\n  const factory NotificationReminderEvent.initial(\n    ReminderPB reminder,\n    UserDateFormatPB dateFormat,\n    UserTimeFormatPB timeFormat,\n  ) = _Initial;\n\n  const factory NotificationReminderEvent.reset() = _Reset;\n}\n\nenum NotificationReminderStatus {\n  initial,\n  loading,\n  loaded,\n  error,\n}\n\n@freezed\nclass NotificationReminderState with _$NotificationReminderState {\n  const NotificationReminderState._();\n\n  const factory NotificationReminderState({\n    required String scheduledAt,\n    required String pageTitle,\n    required String reminderContent,\n    required bool isLocked,\n    @Default(NotificationReminderStatus.initial)\n    NotificationReminderStatus status,\n    @Default([]) List<Node> nodes,\n    String? blockId,\n    ViewPB? view,\n  }) = _NotificationReminderState;\n\n  factory NotificationReminderState.initial() =>\n      const NotificationReminderState(\n        scheduledAt: '',\n        pageTitle: '',\n        reminderContent: '',\n        isLocked: false,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:math';\n\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'document_page_style_bloc.freezed.dart';\n\nclass DocumentPageStyleBloc\n    extends Bloc<DocumentPageStyleEvent, DocumentPageStyleState> {\n  DocumentPageStyleBloc({\n    required this.view,\n  }) : super(DocumentPageStyleState.initial()) {\n    on<DocumentPageStyleEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            try {\n              if (view.id.isEmpty) {\n                return;\n              }\n              Map layoutObject = {};\n              final data = await ViewBackendService.getView(view.id);\n              data.onSuccess((s) {\n                if (s.extra.isNotEmpty) {\n                  layoutObject = jsonDecode(s.extra);\n                }\n              });\n              final fontLayout = _getSelectedFontLayout(layoutObject);\n              final lineHeightLayout = _getSelectedLineHeightLayout(\n                layoutObject,\n              );\n              final fontFamily = _getSelectedFontFamily(layoutObject);\n              final cover = _getSelectedCover(layoutObject);\n              final coverType = cover.$1;\n              final coverValue = cover.$2;\n              emit(\n                state.copyWith(\n                  fontLayout: fontLayout,\n                  fontFamily: fontFamily,\n                  lineHeightLayout: lineHeightLayout,\n                  coverImage: PageStyleCover(\n                    type: coverType,\n                    value: coverValue,\n                  ),\n                  iconPadding: calculateIconPadding(\n                    fontLayout,\n                    lineHeightLayout,\n                  ),\n                ),\n              );\n            } catch (e) {\n              Log.error('Failed to decode layout object: $e');\n            }\n          },\n          updateFont: (fontLayout) async {\n            emit(\n              state.copyWith(\n                fontLayout: fontLayout,\n                iconPadding: calculateIconPadding(\n                  fontLayout,\n                  state.lineHeightLayout,\n                ),\n              ),\n            );\n\n            unawaited(updateLayoutObject());\n          },\n          updateLineHeight: (lineHeightLayout) async {\n            emit(\n              state.copyWith(\n                lineHeightLayout: lineHeightLayout,\n                iconPadding: calculateIconPadding(\n                  state.fontLayout,\n                  lineHeightLayout,\n                ),\n              ),\n            );\n\n            unawaited(updateLayoutObject());\n          },\n          updateFontFamily: (fontFamily) async {\n            emit(\n              state.copyWith(\n                fontFamily: fontFamily,\n              ),\n            );\n\n            unawaited(updateLayoutObject());\n          },\n          updateCoverImage: (coverImage) async {\n            emit(\n              state.copyWith(\n                coverImage: coverImage,\n              ),\n            );\n\n            unawaited(updateLayoutObject());\n          },\n        );\n      },\n    );\n  }\n\n  final ViewPB view;\n  final ViewBackendService viewBackendService = ViewBackendService();\n\n  Future<void> updateLayoutObject() async {\n    final layoutObject = decodeLayoutObject();\n    if (layoutObject != null) {\n      await ViewBackendService.updateView(\n        viewId: view.id,\n        extra: layoutObject,\n      );\n    }\n  }\n\n  String? decodeLayoutObject() {\n    Map oldValue = {};\n    try {\n      final extra = view.extra;\n      oldValue = jsonDecode(extra);\n    } catch (e) {\n      Log.error('Failed to decode layout object: $e');\n    }\n    final newValue = {\n      ViewExtKeys.fontLayoutKey: state.fontLayout.toString(),\n      ViewExtKeys.lineHeightLayoutKey: state.lineHeightLayout.toString(),\n      ViewExtKeys.coverKey: {\n        ViewExtKeys.coverTypeKey: state.coverImage.type.toString(),\n        ViewExtKeys.coverValueKey: state.coverImage.value,\n      },\n      ViewExtKeys.fontKey: state.fontFamily,\n    };\n    final merged = mergeMaps(oldValue, newValue);\n    return jsonEncode(merged);\n  }\n\n  // because the line height can not be calculated accurately,\n  //  we need to adjust the icon padding manually.\n  double calculateIconPadding(\n    PageStyleFontLayout fontLayout,\n    PageStyleLineHeightLayout lineHeightLayout,\n  ) {\n    double padding = switch (fontLayout) {\n      PageStyleFontLayout.small => 1.0,\n      PageStyleFontLayout.normal => 1.0,\n      PageStyleFontLayout.large => 4.0,\n    };\n    switch (lineHeightLayout) {\n      case PageStyleLineHeightLayout.small:\n        padding -= 1.0;\n        break;\n      case PageStyleLineHeightLayout.normal:\n        break;\n      case PageStyleLineHeightLayout.large:\n        padding += 3.0;\n        break;\n    }\n    return max(0, padding);\n  }\n\n  double calculateIconScale(\n    PageStyleFontLayout fontLayout,\n  ) {\n    return switch (fontLayout) {\n      PageStyleFontLayout.small => 0.8,\n      PageStyleFontLayout.normal => 1.0,\n      PageStyleFontLayout.large => 1.2,\n    };\n  }\n\n  PageStyleFontLayout _getSelectedFontLayout(Map layoutObject) {\n    final fontLayout = layoutObject[ViewExtKeys.fontLayoutKey] ??\n        PageStyleFontLayout.normal.toString();\n    return PageStyleFontLayout.values.firstWhere(\n      (e) => e.toString() == fontLayout,\n    );\n  }\n\n  PageStyleLineHeightLayout _getSelectedLineHeightLayout(Map layoutObject) {\n    final lineHeightLayout = layoutObject[ViewExtKeys.lineHeightLayoutKey] ??\n        PageStyleLineHeightLayout.normal.toString();\n    return PageStyleLineHeightLayout.values.firstWhere(\n      (e) => e.toString() == lineHeightLayout,\n    );\n  }\n\n  String? _getSelectedFontFamily(Map layoutObject) {\n    return layoutObject[ViewExtKeys.fontKey];\n  }\n\n  (PageStyleCoverImageType, String colorValue) _getSelectedCover(\n    Map layoutObject,\n  ) {\n    final cover = layoutObject[ViewExtKeys.coverKey] ?? {};\n    final coverType = cover[ViewExtKeys.coverTypeKey] ??\n        PageStyleCoverImageType.none.toString();\n    final coverValue = cover[ViewExtKeys.coverValueKey] ?? '';\n    return (\n      PageStyleCoverImageType.values.firstWhere(\n        (e) => e.toString() == coverType,\n      ),\n      coverValue,\n    );\n  }\n}\n\n@freezed\nclass DocumentPageStyleEvent with _$DocumentPageStyleEvent {\n  const factory DocumentPageStyleEvent.initial() = Initial;\n  const factory DocumentPageStyleEvent.updateFont(\n    PageStyleFontLayout fontLayout,\n  ) = UpdateFontSize;\n  const factory DocumentPageStyleEvent.updateLineHeight(\n    PageStyleLineHeightLayout lineHeightLayout,\n  ) = UpdateLineHeight;\n  const factory DocumentPageStyleEvent.updateFontFamily(\n    String? fontFamily,\n  ) = UpdateFontFamily;\n  const factory DocumentPageStyleEvent.updateCoverImage(\n    PageStyleCover coverImage,\n  ) = UpdateCoverImage;\n}\n\n@freezed\nclass DocumentPageStyleState with _$DocumentPageStyleState {\n  const factory DocumentPageStyleState({\n    @Default(PageStyleFontLayout.normal) PageStyleFontLayout fontLayout,\n    @Default(PageStyleLineHeightLayout.normal)\n    PageStyleLineHeightLayout lineHeightLayout,\n    // the default font family is null, which means the system font\n    @Default(null) String? fontFamily,\n    @Default(2.0) double iconPadding,\n    required PageStyleCover coverImage,\n  }) = _DocumentPageStyleState;\n\n  factory DocumentPageStyleState.initial() => DocumentPageStyleState(\n        coverImage: PageStyleCover.none(),\n      );\n}\n\nenum PageStyleFontLayout {\n  small,\n  normal,\n  large;\n\n  @override\n  String toString() {\n    switch (this) {\n      case PageStyleFontLayout.small:\n        return 'small';\n      case PageStyleFontLayout.normal:\n        return 'normal';\n      case PageStyleFontLayout.large:\n        return 'large';\n    }\n  }\n\n  static PageStyleFontLayout fromString(String value) {\n    return PageStyleFontLayout.values.firstWhereOrNull(\n          (e) => e.toString() == value,\n        ) ??\n        PageStyleFontLayout.normal;\n  }\n\n  double get fontSize {\n    switch (this) {\n      case PageStyleFontLayout.small:\n        return 14.0;\n      case PageStyleFontLayout.normal:\n        return 16.0;\n      case PageStyleFontLayout.large:\n        return 18.0;\n    }\n  }\n\n  List<double> get headingFontSizes {\n    switch (this) {\n      case PageStyleFontLayout.small:\n        return [22.0, 18.0, 16.0, 16.0, 16.0, 16.0];\n      case PageStyleFontLayout.normal:\n        return [24.0, 20.0, 18.0, 18.0, 18.0, 18.0];\n      case PageStyleFontLayout.large:\n        return [26.0, 22.0, 20.0, 20.0, 20.0, 20.0];\n    }\n  }\n\n  double get factor {\n    switch (this) {\n      case PageStyleFontLayout.small:\n        return PageStyleFontLayout.small.fontSize /\n            PageStyleFontLayout.normal.fontSize;\n      case PageStyleFontLayout.normal:\n        return 1.0;\n      case PageStyleFontLayout.large:\n        return PageStyleFontLayout.large.fontSize /\n            PageStyleFontLayout.normal.fontSize;\n    }\n  }\n}\n\nenum PageStyleLineHeightLayout {\n  small,\n  normal,\n  large;\n\n  @override\n  String toString() {\n    switch (this) {\n      case PageStyleLineHeightLayout.small:\n        return 'small';\n      case PageStyleLineHeightLayout.normal:\n        return 'normal';\n      case PageStyleLineHeightLayout.large:\n        return 'large';\n    }\n  }\n\n  static PageStyleLineHeightLayout fromString(String value) {\n    return PageStyleLineHeightLayout.values.firstWhereOrNull(\n          (e) => e.toString() == value,\n        ) ??\n        PageStyleLineHeightLayout.normal;\n  }\n\n  double get lineHeight {\n    switch (this) {\n      case PageStyleLineHeightLayout.small:\n        return 1.4;\n      case PageStyleLineHeightLayout.normal:\n        return 1.5;\n      case PageStyleLineHeightLayout.large:\n        return 1.75;\n    }\n  }\n\n  double get padding {\n    switch (this) {\n      case PageStyleLineHeightLayout.small:\n        return 6.0;\n      case PageStyleLineHeightLayout.normal:\n        return 8.0;\n      case PageStyleLineHeightLayout.large:\n        return 8.0;\n    }\n  }\n\n  List<double> get headingPaddings {\n    switch (this) {\n      case PageStyleLineHeightLayout.small:\n        return [26.0, 22.0, 20.0, 20.0, 20.0, 20.0];\n      case PageStyleLineHeightLayout.normal:\n        return [30.0, 24.0, 22.0, 22.0, 22.0, 22.0];\n      case PageStyleLineHeightLayout.large:\n        return [34.0, 28.0, 26.0, 26.0, 26.0, 26.0];\n    }\n  }\n}\n\n// for the version above 0.5.5\nenum PageStyleCoverImageType {\n  none,\n  // normal color\n  pureColor,\n  // gradient color\n  gradientColor,\n  // built in images\n  builtInImage,\n  // custom images, uploaded by the user\n  customImage,\n  // local image\n  localImage,\n  // unsplash images\n  unsplashImage;\n\n  @override\n  String toString() {\n    switch (this) {\n      case PageStyleCoverImageType.none:\n        return 'none';\n      case PageStyleCoverImageType.pureColor:\n        return 'color';\n      case PageStyleCoverImageType.gradientColor:\n        return 'gradient';\n      case PageStyleCoverImageType.builtInImage:\n        return 'built_in';\n      case PageStyleCoverImageType.customImage:\n        return 'custom';\n      case PageStyleCoverImageType.localImage:\n        return 'local';\n      case PageStyleCoverImageType.unsplashImage:\n        return 'unsplash';\n    }\n  }\n\n  static PageStyleCoverImageType fromString(String value) {\n    return PageStyleCoverImageType.values.firstWhereOrNull(\n          (e) => e.toString() == value,\n        ) ??\n        PageStyleCoverImageType.none;\n  }\n\n  static String builtInImagePath(String value) {\n    return 'assets/images/built_in_cover_images/m_cover_image_$value.png';\n  }\n}\n\nclass PageStyleCover {\n  const PageStyleCover({\n    required this.type,\n    required this.value,\n  });\n\n  factory PageStyleCover.none() => const PageStyleCover(\n        type: PageStyleCoverImageType.none,\n        value: '',\n      );\n\n  final PageStyleCoverImageType type;\n\n  // there're 4 types of values:\n  // 1. pure color: enum value\n  // 2. gradient color: enum value\n  // 3. built-in image: the image name, read from the assets\n  // 4. custom image or unsplash image: the image url\n  final String value;\n\n  bool get isPresets => isPureColor || isGradient || isBuiltInImage;\n  bool get isPhoto => isCustomImage || isLocalImage;\n\n  bool get isNone => type == PageStyleCoverImageType.none;\n  bool get isPureColor => type == PageStyleCoverImageType.pureColor;\n  bool get isGradient => type == PageStyleCoverImageType.gradientColor;\n  bool get isBuiltInImage => type == PageStyleCoverImageType.builtInImage;\n  bool get isCustomImage => type == PageStyleCoverImageType.customImage;\n  bool get isUnsplashImage => type == PageStyleCoverImageType.unsplashImage;\n  bool get isLocalImage => type == PageStyleCoverImageType.localImage;\n\n  @override\n  bool operator ==(Object other) {\n    if (other is! PageStyleCover) {\n      return false;\n    }\n    return type == other.type && value == other.value;\n  }\n\n  @override\n  int get hashCode => Object.hash(type, value);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart",
    "content": "import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/plugins/document/application/document_listener.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport '../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\n\npart 'recent_view_bloc.freezed.dart';\n\nclass RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {\n  RecentViewBloc({\n    required this.view,\n  })  : _documentListener = DocumentListener(id: view.id),\n        _viewListener = ViewListener(viewId: view.id),\n        super(RecentViewState.initial()) {\n    on<RecentViewEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            _documentListener.start(\n              onDocEventUpdate: (docEvent) async {\n                if (state.coverTypeV2 != null) {\n                  return;\n                }\n                final (coverType, coverValue) = await getCoverV1();\n                add(\n                  RecentViewEvent.updateCover(\n                    coverType,\n                    null,\n                    coverValue,\n                  ),\n                );\n              },\n            );\n            _viewListener.start(\n              onViewUpdated: (view) {\n                add(\n                  RecentViewEvent.updateNameOrIcon(\n                    view.name,\n                    view.icon.toEmojiIconData(),\n                  ),\n                );\n\n                if (view.extra.isNotEmpty) {\n                  final cover = view.cover;\n                  add(\n                    RecentViewEvent.updateCover(\n                      CoverType.none,\n                      cover?.type,\n                      cover?.value,\n                    ),\n                  );\n                }\n              },\n            );\n\n            // only document supports the cover\n            if (view.layout != ViewLayoutPB.Document) {\n              emit(\n                state.copyWith(\n                  name: view.name,\n                  icon: view.icon.toEmojiIconData(),\n                ),\n              );\n            }\n\n            final cover = getCoverV2();\n\n            if (cover != null) {\n              emit(\n                state.copyWith(\n                  name: view.name,\n                  icon: view.icon.toEmojiIconData(),\n                  coverTypeV2: cover.type,\n                  coverValue: cover.value,\n                ),\n              );\n            } else {\n              final (coverTypeV1, coverValue) = await getCoverV1();\n              emit(\n                state.copyWith(\n                  name: view.name,\n                  icon: view.icon.toEmojiIconData(),\n                  coverTypeV1: coverTypeV1,\n                  coverValue: coverValue,\n                ),\n              );\n            }\n          },\n          updateNameOrIcon: (name, icon) {\n            emit(\n              state.copyWith(\n                name: name,\n                icon: icon,\n              ),\n            );\n          },\n          updateCover: (coverTypeV1, coverTypeV2, coverValue) {\n            emit(\n              state.copyWith(\n                coverTypeV1: coverTypeV1,\n                coverTypeV2: coverTypeV2,\n                coverValue: coverValue,\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  final ViewPB view;\n  final DocumentListener _documentListener;\n  final ViewListener _viewListener;\n\n  PageStyleCover? getCoverV2() {\n    return view.cover;\n  }\n\n  // for the version under 0.5.5\n  Future<(CoverType, String?)> getCoverV1() async {\n    return (CoverType.none, null);\n  }\n\n  @override\n  Future<void> close() async {\n    await _documentListener.stop();\n    await _viewListener.stop();\n    return super.close();\n  }\n}\n\n@freezed\nclass RecentViewEvent with _$RecentViewEvent {\n  const factory RecentViewEvent.initial() = Initial;\n\n  const factory RecentViewEvent.updateCover(\n    CoverType coverTypeV1,\n    // for the version under 0.5.5, including 0.5.5\n    PageStyleCoverImageType? coverTypeV2, // for the version above 0.5.5\n    String? coverValue,\n  ) = UpdateCover;\n\n  const factory RecentViewEvent.updateNameOrIcon(\n    String name,\n    EmojiIconData icon,\n  ) = UpdateNameOrIcon;\n}\n\n@freezed\nclass RecentViewState with _$RecentViewState {\n  const factory RecentViewState({\n    required String name,\n    required EmojiIconData icon,\n    @Default(CoverType.none) CoverType coverTypeV1,\n    PageStyleCoverImageType? coverTypeV2,\n    @Default(null) String? coverValue,\n  }) = _RecentViewState;\n\n  factory RecentViewState.initial() =>\n      RecentViewState(name: '', icon: EmojiIconData.none());\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/application/user_profile/user_profile_bloc.dart",
    "content": "import 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'user_profile_bloc.freezed.dart';\n\nclass UserProfileBloc extends Bloc<UserProfileEvent, UserProfileState> {\n  UserProfileBloc() : super(const _Initial()) {\n    on<UserProfileEvent>((event, emit) async {\n      await event.when(\n        started: () async => _initialize(emit),\n      );\n    });\n  }\n\n  Future<void> _initialize(Emitter<UserProfileState> emit) async {\n    emit(const UserProfileState.loading());\n    final latestOrFailure =\n        await FolderEventGetCurrentWorkspaceSetting().send();\n\n    final userOrFailure = await getIt<AuthService>().getUser();\n\n    final latest = latestOrFailure.fold(\n      (latestPB) => latestPB,\n      (error) => null,\n    );\n\n    final userProfile = userOrFailure.fold(\n      (userProfilePB) => userProfilePB,\n      (error) => null,\n    );\n\n    if (latest == null || userProfile == null) {\n      return emit(const UserProfileState.workspaceFailure());\n    }\n\n    emit(\n      UserProfileState.success(\n        workspaceSettings: latest,\n        userProfile: userProfile,\n      ),\n    );\n  }\n}\n\n@freezed\nclass UserProfileEvent with _$UserProfileEvent {\n  const factory UserProfileEvent.started() = _Started;\n}\n\n@freezed\nclass UserProfileState with _$UserProfileState {\n  const factory UserProfileState.initial() = _Initial;\n  const factory UserProfileState.loading() = _Loading;\n  const factory UserProfileState.workspaceFailure() = _WorkspaceFailure;\n  const factory UserProfileState.success({\n    required WorkspaceLatestPB workspaceSettings,\n    required UserProfilePB userProfile,\n  }) = _Success;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/base/animated_gesture.dart",
    "content": "import 'package:appflowy/shared/feedback_gesture_detector.dart';\nimport 'package:flutter/material.dart';\n\nclass AnimatedGestureDetector extends StatefulWidget {\n  const AnimatedGestureDetector({\n    super.key,\n    this.scaleFactor = 0.98,\n    this.feedback = true,\n    this.duration = const Duration(milliseconds: 100),\n    this.alignment = Alignment.center,\n    this.behavior = HitTestBehavior.opaque,\n    this.onTapUp,\n    required this.child,\n  });\n\n  final Widget child;\n  final double scaleFactor;\n  final Duration duration;\n  final Alignment alignment;\n  final bool feedback;\n  final HitTestBehavior behavior;\n  final VoidCallback? onTapUp;\n\n  @override\n  State<AnimatedGestureDetector> createState() =>\n      _AnimatedGestureDetectorState();\n}\n\nclass _AnimatedGestureDetectorState extends State<AnimatedGestureDetector> {\n  double scale = 1.0;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      behavior: widget.behavior,\n      onTapUp: (details) {\n        setState(() => scale = 1.0);\n\n        HapticFeedbackType.light.call();\n\n        widget.onTapUp?.call();\n      },\n      onTapDown: (details) {\n        setState(() => scale = widget.scaleFactor);\n      },\n      child: AnimatedScale(\n        scale: scale,\n        alignment: widget.alignment,\n        duration: widget.duration,\n        child: widget.child,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar.dart",
    "content": "import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nenum FlowyAppBarLeadingType {\n  back,\n  close,\n  cancel;\n\n  Widget getWidget(VoidCallback? onTap) {\n    switch (this) {\n      case FlowyAppBarLeadingType.back:\n        return AppBarImmersiveBackButton(onTap: onTap);\n      case FlowyAppBarLeadingType.close:\n        return AppBarCloseButton(onTap: onTap);\n      case FlowyAppBarLeadingType.cancel:\n        return AppBarCancelButton(onTap: onTap);\n    }\n  }\n\n  double? get width {\n    switch (this) {\n      case FlowyAppBarLeadingType.back:\n        return 40.0;\n      case FlowyAppBarLeadingType.close:\n        return 40.0;\n      case FlowyAppBarLeadingType.cancel:\n        return 120;\n    }\n  }\n}\n\nclass FlowyAppBar extends AppBar {\n  FlowyAppBar({\n    super.key,\n    super.actions,\n    Widget? title,\n    String? titleText,\n    FlowyAppBarLeadingType leadingType = FlowyAppBarLeadingType.back,\n    double? leadingWidth,\n    Widget? leading,\n    super.centerTitle,\n    VoidCallback? onTapLeading,\n    bool showDivider = true,\n    super.backgroundColor,\n  }) : super(\n          title: title ??\n              FlowyText(\n                titleText ?? '',\n                fontSize: 15.0,\n                fontWeight: FontWeight.w500,\n              ),\n          titleSpacing: 0,\n          elevation: 0,\n          leading: leading ?? leadingType.getWidget(onTapLeading),\n          leadingWidth: leadingWidth ?? leadingType.width,\n          toolbarHeight: 44.0,\n          bottom: showDivider\n              ? const PreferredSize(\n                  preferredSize: Size.fromHeight(0.5),\n                  child: Divider(\n                    height: 0.5,\n                  ),\n                )\n              : null,\n        );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar_actions.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass AppBarBackButton extends StatelessWidget {\n  const AppBarBackButton({\n    super.key,\n    this.onTap,\n    this.padding,\n  });\n\n  final VoidCallback? onTap;\n  final EdgeInsetsGeometry? padding;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppBarButton(\n      onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(),\n      padding: padding,\n      child: const FlowySvg(\n        FlowySvgs.m_app_bar_back_s,\n      ),\n    );\n  }\n}\n\nclass AppBarImmersiveBackButton extends StatelessWidget {\n  const AppBarImmersiveBackButton({\n    super.key,\n    this.onTap,\n  });\n\n  final VoidCallback? onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppBarButton(\n      onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(),\n      padding: const EdgeInsets.only(\n        left: 12.0,\n        top: 8.0,\n        bottom: 8.0,\n        right: 4.0,\n      ),\n      child: const FlowySvg(\n        FlowySvgs.m_app_bar_back_s,\n      ),\n    );\n  }\n}\n\nclass AppBarCloseButton extends StatelessWidget {\n  const AppBarCloseButton({\n    super.key,\n    this.onTap,\n  });\n\n  final VoidCallback? onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppBarButton(\n      onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(),\n      child: const FlowySvg(\n        FlowySvgs.m_app_bar_close_s,\n      ),\n    );\n  }\n}\n\nclass AppBarCancelButton extends StatelessWidget {\n  const AppBarCancelButton({\n    super.key,\n    this.onTap,\n  });\n\n  final VoidCallback? onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppBarButton(\n      onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(),\n      child: FlowyText(\n        LocaleKeys.button_cancel.tr(),\n        overflow: TextOverflow.ellipsis,\n      ),\n    );\n  }\n}\n\nclass AppBarDoneButton extends StatelessWidget {\n  const AppBarDoneButton({\n    super.key,\n    required this.onTap,\n  });\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppBarButton(\n      onTap: (_) => onTap(),\n      padding: const EdgeInsets.all(12),\n      child: FlowyText(\n        LocaleKeys.button_done.tr(),\n        color: Theme.of(context).colorScheme.primary,\n        fontWeight: FontWeight.w500,\n        textAlign: TextAlign.right,\n      ),\n    );\n  }\n}\n\nclass AppBarSaveButton extends StatelessWidget {\n  const AppBarSaveButton({\n    super.key,\n    required this.onTap,\n    this.enable = true,\n    this.padding = const EdgeInsets.all(12),\n  });\n\n  final VoidCallback onTap;\n  final bool enable;\n  final EdgeInsetsGeometry padding;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppBarButton(\n      onTap: (_) {\n        if (enable) {\n          onTap();\n        }\n      },\n      padding: padding,\n      child: FlowyText(\n        LocaleKeys.button_save.tr(),\n        color: enable\n            ? Theme.of(context).colorScheme.primary\n            : Theme.of(context).disabledColor,\n        fontWeight: FontWeight.w500,\n        textAlign: TextAlign.right,\n      ),\n    );\n  }\n}\n\nclass AppBarFilledDoneButton extends StatelessWidget {\n  const AppBarFilledDoneButton({super.key, required this.onTap});\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.fromLTRB(8, 4, 8, 8),\n      child: TextButton(\n        style: TextButton.styleFrom(\n          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),\n          shape: RoundedRectangleBorder(\n            borderRadius: BorderRadius.circular(10),\n          ),\n          elevation: 0,\n          visualDensity: VisualDensity.compact,\n          tapTargetSize: MaterialTapTargetSize.shrinkWrap,\n          enableFeedback: true,\n          backgroundColor: Theme.of(context).primaryColor,\n        ),\n        onPressed: onTap,\n        child: FlowyText.medium(\n          LocaleKeys.button_done.tr(),\n          fontSize: 16,\n          color: Theme.of(context).colorScheme.onPrimary,\n          overflow: TextOverflow.ellipsis,\n        ),\n      ),\n    );\n  }\n}\n\nclass AppBarMoreButton extends StatelessWidget {\n  const AppBarMoreButton({\n    super.key,\n    required this.onTap,\n  });\n\n  final void Function(BuildContext context) onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppBarButton(\n      padding: const EdgeInsets.all(12),\n      onTap: onTap,\n      child: const FlowySvg(FlowySvgs.three_dots_s),\n    );\n  }\n}\n\nclass AppBarButton extends StatelessWidget {\n  const AppBarButton({\n    super.key,\n    required this.onTap,\n    required this.child,\n    this.padding,\n  });\n\n  final void Function(BuildContext context) onTap;\n  final Widget child;\n  final EdgeInsetsGeometry? padding;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: () => onTap(context),\n      child: Padding(\n        padding: padding ?? const EdgeInsets.all(12),\n        child: child,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/base/flowy_search_text_field.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\n\nclass FlowySearchTextField extends StatelessWidget {\n  const FlowySearchTextField({\n    super.key,\n    this.hintText,\n    this.controller,\n    this.onChanged,\n    this.onSubmitted,\n  });\n\n  final String? hintText;\n  final TextEditingController? controller;\n  final ValueChanged<String>? onChanged;\n  final ValueChanged<String>? onSubmitted;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 44.0,\n      child: CupertinoSearchTextField(\n        controller: controller,\n        onChanged: onChanged,\n        onSubmitted: onSubmitted,\n        placeholder: hintText,\n        prefixIcon: const FlowySvg(FlowySvgs.m_search_m),\n        prefixInsets: const EdgeInsets.only(left: 16.0, right: 2.0),\n        suffixIcon: const Icon(Icons.close),\n        suffixInsets: const EdgeInsets.only(right: 16.0),\n        placeholderStyle: Theme.of(context).textTheme.titleSmall?.copyWith(\n              color: Theme.of(context).hintColor,\n              fontWeight: FontWeight.w400,\n              fontSize: 14.0,\n            ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart",
    "content": "import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/features/share_tab/data/models/share_access_level.dart';\nimport 'package:appflowy/features/workspace/data/repositories/rust_workspace_repository_impl.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';\nimport 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';\nimport 'package:appflowy/plugins/document/application/prelude.dart';\nimport 'package:appflowy/plugins/document/presentation/document_collaborators.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_notification.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MobileViewPage extends StatefulWidget {\n  const MobileViewPage({\n    super.key,\n    required this.id,\n    required this.viewLayout,\n    this.title,\n    this.arguments,\n    this.fixedTitle,\n    this.showMoreButton = true,\n    this.blockId,\n    this.bodyPaddingTop = 0.0,\n    this.tabs = const [PickerTabType.emoji, PickerTabType.icon],\n  });\n\n  /// view id\n  final String id;\n  final ViewLayoutPB viewLayout;\n  final String? title;\n  final Map<String, dynamic>? arguments;\n  final bool showMoreButton;\n  final String? blockId;\n  final double bodyPaddingTop;\n  final List<PickerTabType> tabs;\n\n  // only used in row page\n  final String? fixedTitle;\n\n  @override\n  State<MobileViewPage> createState() => _MobileViewPageState();\n}\n\nclass _MobileViewPageState extends State<MobileViewPage> {\n  // used to determine if the user has scrolled down and show the app bar in immersive mode\n  ScrollNotificationObserverState? _scrollNotificationObserver;\n\n  // control the app bar opacity when in immersive mode\n  final ValueNotifier<double> _appBarOpacity = ValueNotifier(1.0);\n\n  @override\n  void initState() {\n    super.initState();\n\n    getIt<ReminderBloc>().add(const ReminderEvent.started());\n  }\n\n  @override\n  void dispose() {\n    _appBarOpacity.dispose();\n\n    // there's no need to remove the listener, because the observer will be disposed when the widget is unmounted.\n    // inside the observer, the listener will be removed automatically.\n    // _scrollNotificationObserver?.removeListener(_onScrollNotification);\n    _scrollNotificationObserver = null;\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) => MobileViewPageBloc(viewId: widget.id)\n        ..add(const MobileViewPageEvent.initial()),\n      child: BlocBuilder<MobileViewPageBloc, MobileViewPageState>(\n        builder: (context, state) {\n          final view = state.result?.fold((s) => s, (f) => null);\n          final body = _buildBody(context, state);\n\n          if (view == null) {\n            return SizedBox.shrink();\n          }\n\n          return MultiBlocProvider(\n            providers: [\n              BlocProvider(\n                create: (_) =>\n                    FavoriteBloc()..add(const FavoriteEvent.initial()),\n              ),\n              BlocProvider(\n                create: (_) =>\n                    ViewBloc(view: view)..add(const ViewEvent.initial()),\n              ),\n              BlocProvider.value(\n                value: getIt<ReminderBloc>(),\n              ),\n              BlocProvider(\n                create: (_) =>\n                    ShareBloc(view: view)..add(const ShareEvent.initial()),\n              ),\n              if (state.userProfilePB != null)\n                BlocProvider(\n                  create: (_) => UserWorkspaceBloc(\n                    userProfile: state.userProfilePB!,\n                    repository: RustWorkspaceRepositoryImpl(\n                      userId: state.userProfilePB!.id,\n                    ),\n                  )..add(UserWorkspaceEvent.initialize()),\n                ),\n              if (view.layout.isDocumentView)\n                BlocProvider(\n                  create: (_) => DocumentPageStyleBloc(view: view)\n                    ..add(const DocumentPageStyleEvent.initial()),\n                ),\n              if (view.layout.isDocumentView || view.layout.isDatabaseView)\n                BlocProvider(\n                  create: (_) => PageAccessLevelBloc(view: view)\n                    ..add(const PageAccessLevelEvent.initial()),\n                ),\n            ],\n            child: Builder(\n              builder: (context) {\n                final view = context.watch<ViewBloc>().state.view;\n                return _buildApp(context, view, body);\n              },\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildApp(\n    BuildContext context,\n    ViewPB? view,\n    Widget child,\n  ) {\n    final isDocument = view?.layout.isDocumentView ?? false;\n    final title = _buildTitle(context, view);\n    final actions = _buildAppBarActions(context, view);\n    final appBar = isDocument\n        ? MobileViewPageImmersiveAppBar(\n            preferredSize: Size(\n              double.infinity,\n              AppBarTheme.of(context).toolbarHeight ?? kToolbarHeight,\n            ),\n            title: title,\n            appBarOpacity: _appBarOpacity,\n            actions: actions,\n            view: view,\n          )\n        : FlowyAppBar(title: title, actions: actions);\n    final body = isDocument\n        ? Builder(\n            builder: (context) {\n              _rebuildScrollNotificationObserver(context);\n              return child;\n            },\n          )\n        : SafeArea(child: child);\n    return Scaffold(\n      extendBodyBehindAppBar: isDocument,\n      appBar: appBar,\n      body: Padding(\n        padding: EdgeInsets.only(top: widget.bodyPaddingTop),\n        child: body,\n      ),\n    );\n  }\n\n  Widget _buildBody(BuildContext context, MobileViewPageState state) {\n    if (state.isLoading) {\n      return const Center(\n        child: CircularProgressIndicator(),\n      );\n    }\n\n    final result = state.result;\n    if (result == null) {\n      return FlowyMobileStateContainer.error(\n        emoji: '😔',\n        title: LocaleKeys.error_weAreSorry.tr(),\n        description: LocaleKeys.error_loadingViewError.tr(),\n        errorMsg: '',\n      );\n    }\n\n    return result.fold(\n      (view) {\n        final plugin = view.plugin(arguments: widget.arguments ?? const {})\n          ..init();\n        return plugin.widgetBuilder.buildWidget(\n          shrinkWrap: false,\n          context: PluginContext(userProfile: state.userProfilePB),\n          data: {\n            MobileDocumentScreen.viewFixedTitle: widget.fixedTitle,\n            MobileDocumentScreen.viewBlockId: widget.blockId,\n            MobileDocumentScreen.viewSelectTabs: widget.tabs,\n          },\n        );\n      },\n      (error) {\n        return FlowyMobileStateContainer.error(\n          emoji: '😔',\n          title: LocaleKeys.error_weAreSorry.tr(),\n          description: LocaleKeys.error_loadingViewError.tr(),\n          errorMsg: error.toString(),\n        );\n      },\n    );\n  }\n\n  // Document:\n  //  - [ collaborators, sync_indicator, layout_button, more_button]\n  // Database:\n  //  - [ sync_indicator, more_button]\n  List<Widget> _buildAppBarActions(BuildContext context, ViewPB? view) {\n    if (view == null) {\n      return [];\n    }\n\n    final isImmersiveMode =\n        context.read<MobileViewPageBloc>().state.isImmersiveMode;\n    final isLocked =\n        context.read<PageAccessLevelBloc?>()?.state.isLocked ?? false;\n    final accessLevel = context.read<PageAccessLevelBloc>().state.accessLevel;\n    final actions = <Widget>[];\n\n    if (FeatureFlag.syncDocument.isOn) {\n      // only document supports displaying collaborators.\n      if (view.layout.isDocumentView) {\n        actions.addAll([\n          DocumentCollaborators(\n            width: 60,\n            height: 44,\n            fontSize: 14,\n            padding: const EdgeInsets.symmetric(vertical: 8),\n            view: view,\n          ),\n          const HSpace(12.0),\n        ]);\n      }\n    }\n\n    if (view.layout.isDocumentView && !isLocked) {\n      actions.addAll([\n        MobileViewPageLayoutButton(\n          view: view,\n          isImmersiveMode: isImmersiveMode,\n          appBarOpacity: _appBarOpacity,\n          tabs: widget.tabs,\n        ),\n      ]);\n    }\n\n    if (widget.showMoreButton && accessLevel != ShareAccessLevel.readOnly) {\n      actions.addAll([\n        MobileViewPageMoreButton(\n          view: view,\n          isImmersiveMode: isImmersiveMode,\n          appBarOpacity: _appBarOpacity,\n        ),\n      ]);\n    } else {\n      actions.addAll([\n        const HSpace(18.0),\n      ]);\n    }\n\n    return actions;\n  }\n\n  Widget _buildTitle(BuildContext context, ViewPB? view) {\n    final icon = view?.icon;\n    return ValueListenableBuilder(\n      valueListenable: _appBarOpacity,\n      builder: (_, value, child) {\n        if (value < 0.99) {\n          return Padding(\n            padding: const EdgeInsets.only(left: 6.0),\n            child: _buildLockStatus(context, view),\n          );\n        }\n\n        final name =\n            widget.fixedTitle ?? view?.nameOrDefault ?? widget.title ?? '';\n\n        return Opacity(\n          opacity: value,\n          child: Row(\n            children: [\n              if (icon != null && icon.value.isNotEmpty) ...[\n                RawEmojiIconWidget(\n                  emoji: icon.toEmojiIconData(),\n                  emojiSize: 15,\n                ),\n                const HSpace(4),\n              ],\n              Flexible(\n                child: FlowyText.medium(\n                  name,\n                  fontSize: 15.0,\n                  overflow: TextOverflow.ellipsis,\n                  figmaLineHeight: 18.0,\n                ),\n              ),\n              const HSpace(4.0),\n              _buildLockStatusIcon(context, view),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildLockStatus(BuildContext context, ViewPB? view) {\n    if (view == null || view.layout == ViewLayoutPB.Chat) {\n      return const SizedBox.shrink();\n    }\n\n    return BlocConsumer<PageAccessLevelBloc, PageAccessLevelState>(\n      listenWhen: (previous, current) =>\n          previous.isLoadingLockStatus == current.isLoadingLockStatus &&\n          current.isLoadingLockStatus == false,\n      listener: (context, state) {\n        if (state.isLocked) {\n          showToastNotification(\n            message: LocaleKeys.lockPage_pageLockedToast.tr(),\n          );\n\n          EditorNotification.exitEditing().post();\n        }\n      },\n      builder: (context, state) {\n        if (state.isLocked) {\n          return LockedPageStatus();\n        } else if (!state.isLocked && state.lockCounter > 0) {\n          return ReLockedPageStatus();\n        }\n        return const SizedBox.shrink();\n      },\n    );\n  }\n\n  Widget _buildLockStatusIcon(BuildContext context, ViewPB? view) {\n    if (view == null || view.layout == ViewLayoutPB.Chat) {\n      return const SizedBox.shrink();\n    }\n\n    return BlocConsumer<PageAccessLevelBloc, PageAccessLevelState>(\n      listenWhen: (previous, current) =>\n          previous.isLoadingLockStatus == current.isLoadingLockStatus &&\n          current.isLoadingLockStatus == false,\n      listener: (context, state) {\n        if (state.isLocked) {\n          showToastNotification(\n            message: LocaleKeys.lockPage_pageLockedToast.tr(),\n          );\n        }\n      },\n      builder: (context, state) {\n        if (state.isLocked) {\n          return GestureDetector(\n            behavior: HitTestBehavior.opaque,\n            onTap: () {\n              context.read<PageAccessLevelBloc>().add(\n                    const PageAccessLevelEvent.unlock(),\n                  );\n            },\n            child: Padding(\n              padding: const EdgeInsets.only(\n                top: 4.0,\n                right: 8,\n                bottom: 4.0,\n              ),\n              child: FlowySvg(\n                FlowySvgs.lock_page_fill_s,\n                blendMode: null,\n              ),\n            ),\n          );\n        } else if (!state.isLocked && state.lockCounter > 0) {\n          return GestureDetector(\n            behavior: HitTestBehavior.opaque,\n            onTap: () {\n              context.read<PageAccessLevelBloc>().add(\n                    const PageAccessLevelEvent.lock(),\n                  );\n            },\n            child: Padding(\n              padding: const EdgeInsets.only(\n                top: 4.0,\n                right: 8,\n                bottom: 4.0,\n              ),\n              child: FlowySvg(\n                FlowySvgs.unlock_page_s,\n                color: Color(0xFF8F959E),\n                blendMode: null,\n              ),\n            ),\n          );\n        }\n        return const SizedBox.shrink();\n      },\n    );\n  }\n\n  void _rebuildScrollNotificationObserver(BuildContext context) {\n    _scrollNotificationObserver?.removeListener(_onScrollNotification);\n    _scrollNotificationObserver = ScrollNotificationObserver.maybeOf(context);\n    _scrollNotificationObserver?.addListener(_onScrollNotification);\n  }\n\n  // immersive mode related\n  // auto show or hide the app bar based on the scroll position\n  void _onScrollNotification(ScrollNotification notification) {\n    if (_scrollNotificationObserver == null) {\n      return;\n    }\n\n    if (notification is ScrollUpdateNotification &&\n        defaultScrollNotificationPredicate(notification)) {\n      final ScrollMetrics metrics = notification.metrics;\n      double height =\n          MediaQuery.of(context).padding.top + widget.bodyPaddingTop;\n      if (defaultTargetPlatform == TargetPlatform.android) {\n        height += AppBarTheme.of(context).toolbarHeight ?? kToolbarHeight;\n      }\n      final progress = (metrics.pixels / height).clamp(0.0, 1.0);\n      // reduce the sensitivity of the app bar opacity change\n      if ((progress - _appBarOpacity.value).abs() >= 0.1 ||\n          progress == 0 ||\n          progress == 1.0) {\n        _appBarOpacity.value = progress;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/base/option_color_list.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flutter/material.dart';\n\nclass OptionColorList extends StatelessWidget {\n  const OptionColorList({\n    super.key,\n    this.selectedColor,\n    required this.onSelectedColor,\n  });\n\n  final SelectOptionColorPB? selectedColor;\n  final void Function(SelectOptionColorPB color) onSelectedColor;\n\n  @override\n  Widget build(BuildContext context) {\n    return GridView.count(\n      crossAxisCount: 6,\n      shrinkWrap: true,\n      physics: const NeverScrollableScrollPhysics(),\n      padding: EdgeInsets.zero,\n      children: SelectOptionColorPB.values.map(\n        (colorPB) {\n          final color = colorPB.toColor(context);\n          final isSelected = selectedColor?.value == colorPB.value;\n          return GestureDetector(\n            onTap: () => onSelectedColor(colorPB),\n            child: Container(\n              margin: const EdgeInsets.all(\n                8.0,\n              ),\n              decoration: BoxDecoration(\n                color: color,\n                borderRadius: Corners.s12Border,\n                border: Border.all(\n                  width: isSelected ? 2.0 : 1.0,\n                  color: isSelected\n                      ? const Color(0xff00C6F1)\n                      : Theme.of(context).dividerColor,\n                ),\n              ),\n              alignment: Alignment.center,\n              child: isSelected\n                  ? const FlowySvg(\n                      FlowySvgs.m_blue_check_s,\n                      size: Size.square(28.0),\n                      blendMode: null,\n                    )\n                  : null,\n            ),\n          );\n        },\n      ).toList(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass TypeOptionMenuItemValue<T> {\n  const TypeOptionMenuItemValue({\n    required this.value,\n    required this.icon,\n    required this.text,\n    required this.backgroundColor,\n    required this.onTap,\n    this.iconPadding,\n  });\n\n  final T value;\n  final FlowySvgData icon;\n  final String text;\n  final Color backgroundColor;\n  final EdgeInsets? iconPadding;\n  final void Function(BuildContext context, T value) onTap;\n}\n\nclass TypeOptionMenu<T> extends StatelessWidget {\n  const TypeOptionMenu({\n    super.key,\n    required this.values,\n    this.width = 98,\n    this.iconWidth = 72,\n    this.scaleFactor = 1.0,\n    this.maxAxisSpacing = 18,\n    this.crossAxisCount = 3,\n  });\n\n  final List<TypeOptionMenuItemValue<T>> values;\n\n  final double iconWidth;\n  final double width;\n  final double scaleFactor;\n  final double maxAxisSpacing;\n  final int crossAxisCount;\n\n  @override\n  Widget build(BuildContext context) {\n    return TypeOptionGridView(\n      crossAxisCount: crossAxisCount,\n      mainAxisSpacing: maxAxisSpacing * scaleFactor,\n      itemWidth: width * scaleFactor,\n      children: values\n          .map(\n            (value) => TypeOptionMenuItem<T>(\n              value: value,\n              width: width,\n              iconWidth: iconWidth,\n              scaleFactor: scaleFactor,\n              iconPadding: value.iconPadding,\n            ),\n          )\n          .toList(),\n    );\n  }\n}\n\nclass TypeOptionMenuItem<T> extends StatelessWidget {\n  const TypeOptionMenuItem({\n    super.key,\n    required this.value,\n    this.width = 94,\n    this.iconWidth = 72,\n    this.scaleFactor = 1.0,\n    this.iconPadding,\n  });\n\n  final TypeOptionMenuItemValue<T> value;\n  final double iconWidth;\n  final double width;\n  final double scaleFactor;\n  final EdgeInsets? iconPadding;\n\n  double get scaledIconWidth => iconWidth * scaleFactor;\n  double get scaledWidth => width * scaleFactor;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onTap: () => value.onTap(context, value.value),\n      child: Column(\n        children: [\n          Container(\n            height: scaledIconWidth,\n            width: scaledIconWidth,\n            decoration: ShapeDecoration(\n              color: value.backgroundColor,\n              shape: RoundedRectangleBorder(\n                borderRadius: BorderRadius.circular(24 * scaleFactor),\n              ),\n            ),\n            padding: EdgeInsets.all(21 * scaleFactor) +\n                (iconPadding ?? EdgeInsets.zero),\n            child: FlowySvg(\n              value.icon,\n            ),\n          ),\n          const VSpace(6),\n          ConstrainedBox(\n            constraints: BoxConstraints(\n              maxWidth: scaledWidth,\n            ),\n            child: FlowyText(\n              value.text,\n              fontSize: 14.0,\n              maxLines: 2,\n              lineHeight: 1.0,\n              overflow: TextOverflow.ellipsis,\n              textAlign: TextAlign.center,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass TypeOptionGridView extends StatelessWidget {\n  const TypeOptionGridView({\n    super.key,\n    required this.children,\n    required this.crossAxisCount,\n    required this.mainAxisSpacing,\n    required this.itemWidth,\n  });\n\n  final List<Widget> children;\n  final int crossAxisCount;\n  final double mainAxisSpacing;\n  final double itemWidth;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        for (var i = 0; i < children.length; i += crossAxisCount)\n          Padding(\n            padding: EdgeInsets.only(bottom: mainAxisSpacing),\n            child: Row(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              mainAxisAlignment: MainAxisAlignment.spaceBetween,\n              children: [\n                for (var j = 0; j < crossAxisCount; j++)\n                  i + j < children.length\n                      ? SizedBox(\n                          width: itemWidth,\n                          child: children[i + j],\n                        )\n                      : HSpace(itemWidth),\n              ],\n            ),\n          ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart",
    "content": "import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';\nimport 'package:appflowy/mobile/presentation/base/view_page/more_bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_notification.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/page_style_bottom_sheet.dart';\nimport 'package:appflowy/plugins/shared/share/share_bloc.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nclass MobileViewPageImmersiveAppBar extends StatelessWidget\n    implements PreferredSizeWidget {\n  const MobileViewPageImmersiveAppBar({\n    super.key,\n    required this.preferredSize,\n    required this.appBarOpacity,\n    required this.title,\n    required this.actions,\n    required this.view,\n  });\n\n  final ValueListenable appBarOpacity;\n  final Widget title;\n  final List<Widget> actions;\n  final ViewPB? view;\n  @override\n  final Size preferredSize;\n\n  @override\n  Widget build(BuildContext context) {\n    return ValueListenableBuilder(\n      valueListenable: appBarOpacity,\n      builder: (_, opacity, __) => FlowyAppBar(\n        backgroundColor:\n            AppBarTheme.of(context).backgroundColor?.withValues(alpha: opacity),\n        showDivider: false,\n        title: _buildTitle(context, opacity: opacity),\n        leadingWidth: 44,\n        leading: Padding(\n          padding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 12.0),\n          child: _buildAppBarBackButton(context),\n        ),\n        actions: actions,\n      ),\n    );\n  }\n\n  Widget _buildTitle(\n    BuildContext context, {\n    required double opacity,\n  }) {\n    return title;\n  }\n\n  Widget _buildAppBarBackButton(BuildContext context) {\n    return AppBarButton(\n      padding: EdgeInsets.zero,\n      onTap: (context) => context.pop(),\n      child: _ImmersiveAppBarButton(\n        icon: FlowySvgs.m_app_bar_back_s,\n        dimension: 30.0,\n        iconPadding: 3.0,\n        isImmersiveMode:\n            context.read<MobileViewPageBloc>().state.isImmersiveMode,\n        appBarOpacity: appBarOpacity,\n      ),\n    );\n  }\n}\n\nclass MobileViewPageMoreButton extends StatelessWidget {\n  const MobileViewPageMoreButton({\n    super.key,\n    required this.view,\n    required this.isImmersiveMode,\n    required this.appBarOpacity,\n  });\n\n  final ViewPB view;\n  final bool isImmersiveMode;\n  final ValueListenable appBarOpacity;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppBarButton(\n      padding: const EdgeInsets.only(left: 8, right: 16),\n      onTap: (context) {\n        EditorNotification.exitEditing().post();\n\n        showMobileBottomSheet(\n          context,\n          showDragHandle: true,\n          showDivider: false,\n          backgroundColor: AFThemeExtension.of(context).background,\n          builder: (_) => MultiBlocProvider(\n            providers: [\n              BlocProvider.value(value: context.read<ViewBloc>()),\n              BlocProvider.value(value: context.read<FavoriteBloc>()),\n              BlocProvider.value(value: context.read<MobileViewPageBloc>()),\n              BlocProvider.value(value: context.read<ShareBloc>()),\n              BlocProvider.value(value: context.read<PageAccessLevelBloc>()),\n            ],\n            child: MobileViewPageMoreBottomSheet(view: view),\n          ),\n        );\n      },\n      child: _ImmersiveAppBarButton(\n        icon: FlowySvgs.m_app_bar_more_s,\n        dimension: 30.0,\n        iconPadding: 3.0,\n        isImmersiveMode: isImmersiveMode,\n        appBarOpacity: appBarOpacity,\n      ),\n    );\n  }\n}\n\nclass MobileViewPageLayoutButton extends StatelessWidget {\n  const MobileViewPageLayoutButton({\n    super.key,\n    required this.view,\n    required this.isImmersiveMode,\n    required this.appBarOpacity,\n    required this.tabs,\n  });\n\n  final ViewPB view;\n  final List<PickerTabType> tabs;\n  final bool isImmersiveMode;\n  final ValueListenable appBarOpacity;\n\n  @override\n  Widget build(BuildContext context) {\n    // only display the layout button if the view is a document\n    if (view.layout != ViewLayoutPB.Document) {\n      return const SizedBox.shrink();\n    }\n\n    return AppBarButton(\n      padding: const EdgeInsets.symmetric(vertical: 2.0),\n      onTap: (context) {\n        EditorNotification.exitEditing().post();\n\n        showMobileBottomSheet(\n          context,\n          showDragHandle: true,\n          showDivider: false,\n          showDoneButton: true,\n          showHeader: true,\n          title: LocaleKeys.pageStyle_title.tr(),\n          backgroundColor: AFThemeExtension.of(context).background,\n          builder: (_) => MultiBlocProvider(\n            providers: [\n              BlocProvider.value(value: context.read<DocumentPageStyleBloc>()),\n              BlocProvider.value(value: context.read<MobileViewPageBloc>()),\n            ],\n            child: PageStyleBottomSheet(\n              view: context.read<ViewBloc>().state.view,\n              tabs: tabs,\n            ),\n          ),\n        );\n      },\n      child: _ImmersiveAppBarButton(\n        icon: FlowySvgs.m_layout_s,\n        dimension: 30.0,\n        iconPadding: 3.0,\n        isImmersiveMode: isImmersiveMode,\n        appBarOpacity: appBarOpacity,\n      ),\n    );\n  }\n}\n\nclass _ImmersiveAppBarButton extends StatelessWidget {\n  const _ImmersiveAppBarButton({\n    required this.icon,\n    required this.dimension,\n    required this.iconPadding,\n    required this.isImmersiveMode,\n    required this.appBarOpacity,\n  });\n\n  final FlowySvgData icon;\n  final double dimension;\n  final double iconPadding;\n  final bool isImmersiveMode;\n  final ValueListenable appBarOpacity;\n\n  @override\n  Widget build(BuildContext context) {\n    assert(\n      dimension > 0.0 && dimension <= kToolbarHeight,\n      'dimension must be greater than 0, and less than or equal to kToolbarHeight',\n    );\n\n    // if the immersive mode is on, the icon should be white and add a black background\n    //  also, the icon opacity will change based on the app bar opacity\n    return UnconstrainedBox(\n      child: SizedBox.square(\n        dimension: dimension,\n        child: ValueListenableBuilder(\n          valueListenable: appBarOpacity,\n          builder: (context, appBarOpacity, child) {\n            Color? color;\n\n            // if there's no cover or the cover is not immersive,\n            //  make sure the app bar is always visible\n            if (!isImmersiveMode) {\n              color = null;\n            } else if (appBarOpacity < 0.99) {\n              color = Colors.white;\n            }\n\n            Widget child = Container(\n              margin: EdgeInsets.all(iconPadding),\n              child: FlowySvg(icon, color: color),\n            );\n\n            if (isImmersiveMode && appBarOpacity <= 0.99) {\n              child = DecoratedBox(\n                decoration: BoxDecoration(\n                  borderRadius: BorderRadius.circular(dimension / 2.0),\n                  color: Colors.black.withValues(alpha: 0.2),\n                ),\n                child: child,\n              );\n            }\n\n            return child;\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/home/workspaces/create_workspace_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_notification.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/shared/share/constants.dart';\nimport 'package:appflowy/plugins/shared/share/publish_name_generator.dart';\nimport 'package:appflowy/plugins/shared/share/share_bloc.dart';\nimport 'package:appflowy/shared/error_code/error_code_map.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nclass MobileViewPageMoreBottomSheet extends StatelessWidget {\n  const MobileViewPageMoreBottomSheet({super.key, required this.view});\n\n  final ViewPB view;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocListener<ShareBloc, ShareState>(\n      listener: (context, state) => _showToast(context, state),\n      child: BlocListener<ViewBloc, ViewState>(\n        listener: (context, state) {\n          if (state.successOrFailure.isSuccess && state.isDeleted) {\n            context.go('/home');\n          }\n        },\n        child: ViewPageBottomSheet(\n          view: view,\n          onAction: (action, {arguments}) async => _onAction(\n            context,\n            action,\n            arguments,\n          ),\n          onRename: (name) {\n            _onRename(\n              context,\n              name,\n            );\n            context.pop();\n          },\n        ),\n      ),\n    );\n  }\n\n  Future<void> _onAction(\n    BuildContext context,\n    MobileViewBottomSheetBodyAction action,\n    Map<String, dynamic>? arguments,\n  ) async {\n    switch (action) {\n      case MobileViewBottomSheetBodyAction.duplicate:\n        _duplicate(context);\n        break;\n      case MobileViewBottomSheetBodyAction.delete:\n        context.read<ViewBloc>().add(const ViewEvent.delete());\n        Navigator.of(context).pop();\n        break;\n      case MobileViewBottomSheetBodyAction.addToFavorites:\n        _addFavorite(context);\n        break;\n      case MobileViewBottomSheetBodyAction.removeFromFavorites:\n        _removeFavorite(context);\n        break;\n      case MobileViewBottomSheetBodyAction.undo:\n        EditorNotification.undo().post();\n        context.pop();\n        break;\n      case MobileViewBottomSheetBodyAction.redo:\n        EditorNotification.redo().post();\n        context.pop();\n        break;\n      case MobileViewBottomSheetBodyAction.helpCenter:\n        // unimplemented\n        context.pop();\n        break;\n      case MobileViewBottomSheetBodyAction.publish:\n        await _publish(context);\n        if (context.mounted) {\n          context.pop();\n        }\n        break;\n      case MobileViewBottomSheetBodyAction.unpublish:\n        _unpublish(context);\n        context.pop();\n        break;\n      case MobileViewBottomSheetBodyAction.copyPublishLink:\n        _copyPublishLink(context);\n        context.pop();\n        break;\n      case MobileViewBottomSheetBodyAction.visitSite:\n        _visitPublishedSite(context);\n        context.pop();\n        break;\n      case MobileViewBottomSheetBodyAction.copyShareLink:\n        _copyShareLink(context);\n        context.pop();\n        break;\n      case MobileViewBottomSheetBodyAction.updatePathName:\n        _updatePathName(context);\n      case MobileViewBottomSheetBodyAction.lockPage:\n        final isLocked =\n            arguments?[MobileViewBottomSheetBodyActionArguments.isLockedKey] ??\n                false;\n        await _lockPage(context, isLocked: isLocked);\n        // context.pop();\n        break;\n      case MobileViewBottomSheetBodyAction.rename:\n        // no need to implement, rename is handled by the onRename callback.\n        throw UnimplementedError();\n    }\n  }\n\n  Future<void> _lockPage(\n    BuildContext context, {\n    required bool isLocked,\n  }) async {\n    if (isLocked) {\n      context\n          .read<PageAccessLevelBloc>()\n          .add(const PageAccessLevelEvent.lock());\n    } else {\n      context\n          .read<PageAccessLevelBloc>()\n          .add(const PageAccessLevelEvent.unlock());\n    }\n  }\n\n  Future<void> _publish(BuildContext context) async {\n    final id = context.read<ShareBloc>().view.id;\n    final lastPublishName = context.read<ShareBloc>().state.pathName;\n    final publishName = lastPublishName.orDefault(\n      await generatePublishName(\n        id,\n        view.name,\n      ),\n    );\n    if (context.mounted) {\n      context.read<ShareBloc>().add(\n            ShareEvent.publish(\n              '',\n              publishName,\n              [view.id],\n            ),\n          );\n    }\n  }\n\n  void _duplicate(BuildContext context) {\n    context.read<ViewBloc>().add(const ViewEvent.duplicate());\n    context.pop();\n\n    showToastNotification(\n      message: LocaleKeys.button_duplicateSuccessfully.tr(),\n    );\n  }\n\n  void _addFavorite(BuildContext context) {\n    _toggleFavorite(context);\n\n    showToastNotification(\n      message: LocaleKeys.button_favoriteSuccessfully.tr(),\n    );\n  }\n\n  void _removeFavorite(BuildContext context) {\n    _toggleFavorite(context);\n\n    showToastNotification(\n      message: LocaleKeys.button_unfavoriteSuccessfully.tr(),\n    );\n  }\n\n  void _toggleFavorite(BuildContext context) {\n    context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));\n    context.pop();\n  }\n\n  void _unpublish(BuildContext context) {\n    context.read<ShareBloc>().add(const ShareEvent.unPublish());\n  }\n\n  void _copyPublishLink(BuildContext context) {\n    final url = context.read<ShareBloc>().state.url;\n    if (url.isNotEmpty) {\n      unawaited(\n        getIt<ClipboardService>().setData(\n          ClipboardServiceData(plainText: url),\n        ),\n      );\n      showToastNotification(\n        message: LocaleKeys.message_copy_success.tr(),\n      );\n    }\n  }\n\n  void _visitPublishedSite(BuildContext context) {\n    final url = context.read<ShareBloc>().state.url;\n    if (url.isNotEmpty) {\n      unawaited(\n        afLaunchUri(\n          Uri.parse(url),\n          mode: LaunchMode.externalApplication,\n        ),\n      );\n    }\n  }\n\n  void _copyShareLink(BuildContext context) {\n    final workspaceId = context.read<ShareBloc>().state.workspaceId;\n    final viewId = context.read<ShareBloc>().state.viewId;\n    final url = ShareConstants.buildShareUrl(\n      workspaceId: workspaceId,\n      viewId: viewId,\n    );\n    if (url.isNotEmpty) {\n      unawaited(\n        getIt<ClipboardService>().setData(\n          ClipboardServiceData(plainText: url),\n        ),\n      );\n      showToastNotification(\n        message: LocaleKeys.shareAction_copyLinkSuccess.tr(),\n      );\n    } else {\n      showToastNotification(\n        message: LocaleKeys.shareAction_copyLinkToBlockFailed.tr(),\n        type: ToastificationType.error,\n      );\n    }\n  }\n\n  void _onRename(BuildContext context, String name) {\n    if (name != view.name) {\n      context.read<ViewBloc>().add(ViewEvent.rename(name));\n    }\n  }\n\n  void _updatePathName(BuildContext context) async {\n    final shareBloc = context.read<ShareBloc>();\n    final pathName = shareBloc.state.pathName;\n    await showMobileBottomSheet(\n      context,\n      showHeader: true,\n      title: LocaleKeys.shareAction_updatePathName.tr(),\n      showCloseButton: true,\n      showDragHandle: true,\n      showDivider: false,\n      padding: const EdgeInsets.symmetric(horizontal: 16),\n      builder: (bottomSheetContext) {\n        FlowyResult<void, FlowyError>? previousUpdatePathNameResult;\n        return EditWorkspaceNameBottomSheet(\n          type: EditWorkspaceNameType.edit,\n          workspaceName: pathName,\n          hintText: '',\n          validator: (value) => null,\n          validatorBuilder: (context) {\n            return BlocProvider.value(\n              value: shareBloc,\n              child: BlocBuilder<ShareBloc, ShareState>(\n                builder: (context, state) {\n                  final updatePathNameResult = state.updatePathNameResult;\n\n                  if (updatePathNameResult == null &&\n                      previousUpdatePathNameResult == null) {\n                    return const SizedBox.shrink();\n                  }\n\n                  if (updatePathNameResult != null) {\n                    previousUpdatePathNameResult = updatePathNameResult;\n                  }\n\n                  final widget = previousUpdatePathNameResult?.fold(\n                        (value) => const SizedBox.shrink(),\n                        (error) => FlowyText(\n                          error.code.publishErrorMessage.orDefault(\n                            LocaleKeys.settings_sites_error_updatePathNameFailed\n                                .tr(),\n                          ),\n                          maxLines: 3,\n                          fontSize: 12,\n                          textAlign: TextAlign.left,\n                          overflow: TextOverflow.ellipsis,\n                          color: Theme.of(context).colorScheme.error,\n                        ),\n                      ) ??\n                      const SizedBox.shrink();\n\n                  return widget;\n                },\n              ),\n            );\n          },\n          onSubmitted: (name) {\n            // rename the path name\n            Log.info('rename the path name, from: $pathName, to: $name');\n\n            shareBloc.add(ShareEvent.updatePathName(name));\n          },\n        );\n      },\n    );\n    shareBloc.add(const ShareEvent.clearPathNameResult());\n  }\n\n  void _showToast(BuildContext context, ShareState state) {\n    if (state.publishResult != null) {\n      state.publishResult!.fold(\n        (value) => showToastNotification(\n          message: LocaleKeys.publish_publishSuccessfully.tr(),\n        ),\n        (error) => showToastNotification(\n          message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}',\n          type: ToastificationType.error,\n        ),\n      );\n    } else if (state.unpublishResult != null) {\n      state.unpublishResult!.fold(\n        (value) => showToastNotification(\n          message: LocaleKeys.publish_unpublishSuccessfully.tr(),\n        ),\n        (error) => showToastNotification(\n          message: LocaleKeys.publish_unpublishFailed.tr(),\n          description: error.msg,\n          type: ToastificationType.error,\n        ),\n      );\n    } else if (state.updatePathNameResult != null) {\n      state.updatePathNameResult!.onSuccess(\n        (value) {\n          showToastNotification(\n            message:\n                LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),\n          );\n\n          context.pop();\n        },\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet.dart",
    "content": "export 'bottom_sheet_action_widget.dart';\nexport 'bottom_sheet_add_new_page.dart';\nexport 'bottom_sheet_drag_handler.dart';\nexport 'bottom_sheet_rename_widget.dart';\nexport 'bottom_sheet_view_item.dart';\nexport 'bottom_sheet_view_item_body.dart';\nexport 'bottom_sheet_view_page.dart';\nexport 'default_mobile_action_pane.dart';\nexport 'show_mobile_bottom_sheet.dart';\nexport 'show_transition_bottom_sheet.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass BottomSheetActionWidget extends StatelessWidget {\n  const BottomSheetActionWidget({\n    super.key,\n    this.svg,\n    required this.text,\n    required this.onTap,\n    this.iconColor,\n  });\n\n  final FlowySvgData? svg;\n  final String text;\n  final VoidCallback onTap;\n  final Color? iconColor;\n\n  @override\n  Widget build(BuildContext context) {\n    final iconColor =\n        this.iconColor ?? AFThemeExtension.of(context).onBackground;\n\n    if (svg == null) {\n      return OutlinedButton(\n        style: Theme.of(context)\n            .outlinedButtonTheme\n            .style\n            ?.copyWith(alignment: Alignment.center),\n        onPressed: onTap,\n        child: FlowyText(\n          text,\n          textAlign: TextAlign.center,\n        ),\n      );\n    }\n\n    return OutlinedButton.icon(\n      icon: FlowySvg(\n        svg!,\n        size: const Size.square(22.0),\n        color: iconColor,\n      ),\n      label: FlowyText(\n        text,\n        overflow: TextOverflow.ellipsis,\n      ),\n      style: Theme.of(context)\n          .outlinedButtonTheme\n          .style\n          ?.copyWith(alignment: Alignment.centerLeft),\n      onPressed: onTap,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nclass AddNewPageWidgetBottomSheet extends StatelessWidget {\n  const AddNewPageWidgetBottomSheet({\n    super.key,\n    required this.view,\n    required this.onAction,\n  });\n\n  final ViewPB view;\n  final void Function(ViewLayoutPB layout) onAction;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        FlowyOptionTile.text(\n          text: LocaleKeys.document_menuName.tr(),\n          height: 52.0,\n          leftIcon: const FlowySvg(\n            FlowySvgs.icon_document_s,\n            size: Size.square(20),\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(ViewLayoutPB.Document),\n        ),\n        FlowyOptionTile.text(\n          text: LocaleKeys.grid_menuName.tr(),\n          height: 52.0,\n          leftIcon: const FlowySvg(\n            FlowySvgs.icon_grid_s,\n            size: Size.square(20),\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(ViewLayoutPB.Grid),\n        ),\n        FlowyOptionTile.text(\n          text: LocaleKeys.board_menuName.tr(),\n          height: 52.0,\n          leftIcon: const FlowySvg(\n            FlowySvgs.icon_board_s,\n            size: Size.square(20),\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(ViewLayoutPB.Board),\n        ),\n        FlowyOptionTile.text(\n          text: LocaleKeys.calendar_menuName.tr(),\n          height: 52.0,\n          leftIcon: const FlowySvg(\n            FlowySvgs.icon_calendar_s,\n            size: Size.square(20),\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(ViewLayoutPB.Calendar),\n        ),\n        FlowyOptionTile.text(\n          text: LocaleKeys.chat_newChat.tr(),\n          height: 52.0,\n          leftIcon: const FlowySvg(\n            FlowySvgs.chat_ai_page_s,\n            size: Size.square(20),\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(ViewLayoutPB.Chat),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nenum BlockActionBottomSheetType {\n  delete,\n  duplicate,\n  insertAbove,\n  insertBelow,\n}\n\n// Only works on mobile.\nclass BlockActionBottomSheet extends StatelessWidget {\n  const BlockActionBottomSheet({\n    super.key,\n    required this.onAction,\n    this.extendActionWidgets = const [],\n  });\n\n  final void Function(BlockActionBottomSheetType layout) onAction;\n  final List<Widget> extendActionWidgets;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        // insert above, insert below\n        FlowyOptionTile.text(\n          text: LocaleKeys.button_insertAbove.tr(),\n          leftIcon: const FlowySvg(\n            FlowySvgs.arrow_up_s,\n            size: Size.square(20),\n          ),\n          showTopBorder: false,\n          onTap: () => onAction(BlockActionBottomSheetType.insertAbove),\n        ),\n        FlowyOptionTile.text(\n          showTopBorder: false,\n          text: LocaleKeys.button_insertBelow.tr(),\n          leftIcon: const FlowySvg(\n            FlowySvgs.arrow_down_s,\n            size: Size.square(20),\n          ),\n          onTap: () => onAction(BlockActionBottomSheetType.insertBelow),\n        ),\n        // duplicate, delete\n        FlowyOptionTile.text(\n          showTopBorder: false,\n          text: LocaleKeys.button_duplicate.tr(),\n          leftIcon: const Padding(\n            padding: EdgeInsets.all(2),\n            child: FlowySvg(\n              FlowySvgs.copy_s,\n              size: Size.square(16),\n            ),\n          ),\n          onTap: () => onAction(BlockActionBottomSheetType.duplicate),\n        ),\n\n        ...extendActionWidgets,\n\n        FlowyOptionTile.text(\n          showTopBorder: false,\n          text: LocaleKeys.button_delete.tr(),\n          leftIcon: FlowySvg(\n            FlowySvgs.trash_s,\n            size: const Size.square(18),\n            color: Theme.of(context).colorScheme.error,\n          ),\n          textColor: Theme.of(context).colorScheme.error,\n          onTap: () => onAction(BlockActionBottomSheetType.delete),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass BottomSheetCloseButton extends StatelessWidget {\n  const BottomSheetCloseButton({\n    super.key,\n    this.onTap,\n  });\n\n  final VoidCallback? onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onTap: onTap ?? () => Navigator.pop(context),\n      child: const Padding(\n        padding: EdgeInsets.symmetric(horizontal: 16.0),\n        child: SizedBox(\n          width: 18,\n          height: 18,\n          child: FlowySvg(\n            FlowySvgs.m_bottom_sheet_close_m,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass BottomSheetDoneButton extends StatelessWidget {\n  const BottomSheetDoneButton({\n    super.key,\n    this.onDone,\n  });\n\n  final VoidCallback? onDone;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onTap: onDone ?? () => Navigator.pop(context),\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12.0),\n        child: FlowyText(\n          LocaleKeys.button_done.tr(),\n          color: Theme.of(context).colorScheme.primary,\n          fontWeight: FontWeight.w500,\n          textAlign: TextAlign.right,\n        ),\n      ),\n    );\n  }\n}\n\nclass BottomSheetRemoveButton extends StatelessWidget {\n  const BottomSheetRemoveButton({\n    super.key,\n    required this.onRemove,\n  });\n\n  final VoidCallback onRemove;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onTap: onRemove,\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12.0),\n        child: FlowyText(\n          LocaleKeys.button_remove.tr(),\n          color: Theme.of(context).colorScheme.primary,\n          fontWeight: FontWeight.w500,\n          textAlign: TextAlign.right,\n        ),\n      ),\n    );\n  }\n}\n\nclass BottomSheetBackButton extends StatelessWidget {\n  const BottomSheetBackButton({\n    super.key,\n    this.onTap,\n  });\n\n  final VoidCallback? onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onTap: onTap ?? () => Navigator.pop(context),\n      child: const Padding(\n        padding: EdgeInsets.symmetric(horizontal: 16.0),\n        child: SizedBox(\n          width: 18,\n          height: 18,\n          child: FlowySvg(\n            FlowySvgs.m_bottom_sheet_back_s,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_drag_handler.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass MobileBottomSheetDragHandler extends StatelessWidget {\n  const MobileBottomSheetDragHandler({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(vertical: 10.0),\n      child: Container(\n        width: 60,\n        height: 4,\n        decoration: BoxDecoration(\n          borderRadius: BorderRadius.circular(2.0),\n          color: Theme.of(context).hintColor,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_header.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_search_text_field.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart';\nimport 'package:appflowy/plugins/shared/share/constants.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\n// ignore: implementation_imports\nimport 'package:appflowy_editor/src/editor/util/link_util.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nclass MobileBottomSheetEditLinkWidget extends StatefulWidget {\n  const MobileBottomSheetEditLinkWidget({\n    super.key,\n    required this.linkInfo,\n    required this.onApply,\n    required this.onRemoveLink,\n    required this.currentViewId,\n    required this.onDispose,\n  });\n\n  final LinkInfo linkInfo;\n  final ValueChanged<LinkInfo> onApply;\n  final ValueChanged<LinkInfo> onRemoveLink;\n  final VoidCallback onDispose;\n  final String currentViewId;\n\n  @override\n  State<MobileBottomSheetEditLinkWidget> createState() =>\n      _MobileBottomSheetEditLinkWidgetState();\n}\n\nclass _MobileBottomSheetEditLinkWidgetState\n    extends State<MobileBottomSheetEditLinkWidget> {\n  ValueChanged<LinkInfo> get onApply => widget.onApply;\n\n  ValueChanged<LinkInfo> get onRemoveLink => widget.onRemoveLink;\n\n  late TextEditingController linkNameController =\n      TextEditingController(text: linkInfo.name);\n  final textFocusNode = FocusNode();\n  late LinkInfo linkInfo = widget.linkInfo;\n  late LinkSearchTextField searchTextField;\n  bool isShowingSearchResult = false;\n  ViewPB? currentView;\n  bool showErrorText = false;\n  bool showRemoveLink = false;\n  String title = LocaleKeys.editor_editLink.tr();\n\n  AppFlowyThemeData get theme => AppFlowyTheme.of(context);\n\n  @override\n  void initState() {\n    super.initState();\n    final isPageLink = linkInfo.isPage;\n    if (isPageLink) getPageView();\n    searchTextField = LinkSearchTextField(\n      initialSearchText: isPageLink ? '' : linkInfo.link,\n      initialViewId: linkInfo.viewId,\n      currentViewId: widget.currentViewId,\n      onEnter: () {},\n      onEscape: () {},\n      onDataRefresh: () {\n        if (mounted) setState(() {});\n      },\n    )..searchRecentViews();\n    if (linkInfo.link.isEmpty) {\n      isShowingSearchResult = true;\n      title = LocaleKeys.toolbar_addLink.tr();\n    } else {\n      showRemoveLink = true;\n      textFocusNode.requestFocus();\n    }\n    textFocusNode.addListener(() {\n      if (!mounted) return;\n      if (textFocusNode.hasFocus) {\n        setState(() {\n          isShowingSearchResult = false;\n        });\n      }\n    });\n  }\n\n  @override\n  void dispose() {\n    linkNameController.dispose();\n    textFocusNode.dispose();\n    searchTextField.dispose();\n    widget.onDispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: MediaQuery.of(context).size.height * 0.8,\n      child: SingleChildScrollView(\n        child: Column(\n          children: [\n            BottomSheetHeader(\n              title: title,\n              onClose: () => context.pop(),\n              confirmButton: FlowyTextButton(\n                LocaleKeys.button_done.tr(),\n                constraints:\n                    const BoxConstraints.tightFor(width: 62, height: 30),\n                padding: const EdgeInsets.only(left: 12),\n                fontColor: theme.textColorScheme.onFill,\n                fillColor: Theme.of(context).primaryColor,\n                onPressed: () {\n                  if (isShowingSearchResult) {\n                    onConfirm();\n                    return;\n                  }\n                  if (linkInfo.link.isEmpty || !isUri(linkInfo.link)) {\n                    setState(() {\n                      showErrorText = true;\n                    });\n                    return;\n                  }\n                  widget.onApply.call(linkInfo);\n                  context.pop();\n                },\n              ),\n            ),\n            const VSpace(20.0),\n            buildNameTextField(),\n            const VSpace(16.0),\n            buildLinkField(),\n            const VSpace(20.0),\n            buildRemoveLink(),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget buildNameTextField() {\n    return SizedBox(\n      height: 48,\n      child: TextFormField(\n        focusNode: textFocusNode,\n        textAlign: TextAlign.left,\n        controller: linkNameController,\n        style: TextStyle(\n          fontSize: 16,\n          height: 20 / 16,\n          fontWeight: FontWeight.w400,\n        ),\n        onChanged: (text) {\n          linkInfo = LinkInfo(\n            name: text,\n            link: linkInfo.link,\n            isPage: linkInfo.isPage,\n          );\n        },\n        decoration: LinkStyle.buildLinkTextFieldInputDecoration(\n          LocaleKeys.document_toolbar_linkNameHint.tr(),\n          contentPadding: EdgeInsets.all(14),\n          radius: 12,\n          context,\n        ),\n      ),\n    );\n  }\n\n  Widget buildLinkField() {\n    final width = MediaQuery.of(context).size.width;\n    final showPageView = linkInfo.isPage && !isShowingSearchResult;\n    Widget child;\n    if (showPageView) {\n      child = buildPageView();\n    } else if (!isShowingSearchResult) {\n      child = buildLinkView();\n    } else {\n      return Column(\n        mainAxisSize: MainAxisSize.min,\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          searchTextField.buildTextField(\n            autofocus: true,\n            context: context,\n            contentPadding: EdgeInsets.all(14),\n            textStyle: TextStyle(\n              fontSize: 16,\n              height: 20 / 16,\n              fontWeight: FontWeight.w400,\n            ),\n          ),\n          VSpace(6),\n          searchTextField.buildResultContainer(\n            context: context,\n            onPageLinkSelected: onPageSelected,\n            onLinkSelected: onLinkSelected,\n            width: width - 32,\n          ),\n        ],\n      );\n    }\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        child,\n        if (showErrorText)\n          Padding(\n            padding: const EdgeInsets.only(top: 4),\n            child: FlowyText.regular(\n              LocaleKeys.document_plugins_file_networkUrlInvalid.tr(),\n              color: theme.textColorScheme.error,\n              fontSize: 12,\n              figmaLineHeight: 16,\n            ),\n          ),\n      ],\n    );\n  }\n\n  Widget buildPageView() {\n    final height = 48.0;\n    late Widget child;\n    final view = currentView;\n    if (view == null) {\n      child = Center(\n        child: SizedBox.fromSize(\n          size: Size(10, 10),\n          child: CircularProgressIndicator(),\n        ),\n      );\n    } else {\n      final viewName = view.name;\n      final displayName = viewName.isEmpty\n          ? LocaleKeys.document_title_placeholder.tr()\n          : viewName;\n      child = GestureDetector(\n        onTap: showSearchResult,\n        child: Container(\n          height: height,\n          color: Colors.grey.withAlpha(1),\n          padding: EdgeInsets.all(14),\n          child: Row(\n            children: [\n              searchTextField.buildIcon(view),\n              HSpace(4),\n              Flexible(\n                child: FlowyText.regular(\n                  displayName,\n                  overflow: TextOverflow.ellipsis,\n                  figmaLineHeight: 20,\n                  fontSize: 14,\n                ),\n              ),\n            ],\n          ),\n        ),\n      );\n    }\n    return Container(\n      height: height,\n      decoration: buildBorderDecoration(),\n      child: child,\n    );\n  }\n\n  Widget buildLinkView() {\n    return Container(\n      height: 48,\n      decoration: buildBorderDecoration(),\n      child: GestureDetector(\n        behavior: HitTestBehavior.opaque,\n        onTap: showSearchResult,\n        child: Padding(\n          padding: EdgeInsets.all(12),\n          child: Row(\n            children: [\n              FlowySvg(FlowySvgs.toolbar_link_earth_m),\n              HSpace(8),\n              Flexible(\n                child: FlowyText.regular(\n                  linkInfo.link,\n                  overflow: TextOverflow.ellipsis,\n                  figmaLineHeight: 20,\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget buildRemoveLink() {\n    if (!showRemoveLink) return SizedBox.shrink();\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: () {\n        widget.onRemoveLink(linkInfo);\n        context.pop();\n      },\n      child: SizedBox(\n        height: 32,\n        child: Center(\n          child: Row(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              FlowySvg(\n                FlowySvgs.mobile_icon_remove_link_m,\n                color: theme.iconColorScheme.secondary,\n              ),\n              HSpace(8),\n              FlowyText.regular(\n                LocaleKeys.editor_removeLink.tr(),\n                overflow: TextOverflow.ellipsis,\n                figmaLineHeight: 20,\n                color: theme.textColorScheme.secondary,\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  void onConfirm() {\n    searchTextField.onSearchResult(\n      onLink: onLinkSelected,\n      onRecentViews: () => onPageSelected(searchTextField.currentRecentView),\n      onSearchViews: () => onPageSelected(searchTextField.currentSearchedView),\n      onEmpty: () {\n        searchTextField.unfocus();\n      },\n    );\n  }\n\n  Future<void> onPageSelected(ViewPB view) async {\n    currentView = view;\n    final link = ShareConstants.buildShareUrl(\n      workspaceId: await UserBackendService.getCurrentWorkspace().fold(\n        (s) => s.id,\n        (f) => '',\n      ),\n      viewId: view.id,\n    );\n    linkInfo = LinkInfo(\n      name: linkInfo.name,\n      link: link,\n      isPage: true,\n    );\n    searchTextField.updateText(linkInfo.link);\n    if (mounted) {\n      setState(() {\n        isShowingSearchResult = false;\n        searchTextField.unfocus();\n      });\n    }\n  }\n\n  void onLinkSelected() {\n    if (mounted) {\n      linkInfo = LinkInfo(\n        name: linkInfo.name,\n        link: searchTextField.searchText,\n      );\n      hideSearchResult();\n    }\n  }\n\n  void hideSearchResult() {\n    setState(() {\n      isShowingSearchResult = false;\n      searchTextField.unfocus();\n      textFocusNode.unfocus();\n    });\n  }\n\n  void showSearchResult() {\n    setState(() {\n      if (linkInfo.isPage) searchTextField.updateText('');\n      isShowingSearchResult = true;\n      searchTextField.requestFocus();\n    });\n  }\n\n  Future<void> getPageView() async {\n    if (!linkInfo.isPage) return;\n    final (view, isInTrash, isDeleted) =\n        await ViewBackendService.getMentionPageStatus(linkInfo.viewId);\n    if (mounted) {\n      setState(() {\n        currentView = view;\n      });\n    }\n  }\n\n  BoxDecoration buildCardDecoration() {\n    return BoxDecoration(\n      borderRadius: BorderRadius.circular(theme.borderRadius.l),\n      boxShadow: theme.shadow.medium,\n    );\n  }\n\n  BoxDecoration buildBorderDecoration() {\n    return BoxDecoration(\n      borderRadius: BorderRadius.circular(theme.borderRadius.l),\n      border: Border.all(color: theme.borderColorScheme.primary),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_header.dart",
    "content": "import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass BottomSheetHeader extends StatelessWidget {\n  const BottomSheetHeader({\n    super.key,\n    this.title,\n    this.onClose,\n    this.onDone,\n    this.confirmButton,\n  });\n\n  final String? title;\n  final VoidCallback? onClose;\n  final VoidCallback? onDone;\n  final Widget? confirmButton;\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      alignment: Alignment.center,\n      children: [\n        if (onClose != null)\n          Positioned(\n            left: 0,\n            child: Align(\n              alignment: Alignment.centerLeft,\n              child: BottomSheetCloseButton(\n                onTap: onClose,\n              ),\n            ),\n          ),\n        if (title != null)\n          Align(\n            child: FlowyText.medium(\n              title!,\n              fontSize: 16,\n            ),\n          ),\n        if (onDone != null || confirmButton != null)\n          Align(\n            alignment: Alignment.centerRight,\n            child: confirmButton ?? BottomSheetDoneButton(onDone: onDone),\n          ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_media_upload.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/file/mobile_file_upload_menu.dart';\nimport 'package:appflowy/util/xfile_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pbenum.dart';\nimport 'package:cross_file/cross_file.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:provider/provider.dart';\n\nclass MobileMediaUploadSheetContent extends StatelessWidget {\n  const MobileMediaUploadSheetContent({super.key, required this.dialogContext});\n\n  final BuildContext dialogContext;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      margin: const EdgeInsets.only(top: 12),\n      constraints: const BoxConstraints(\n        maxHeight: 340,\n        minHeight: 80,\n      ),\n      child: MobileFileUploadMenu(\n        onInsertLocalFile: (files) async {\n          dialogContext.pop();\n\n          await insertLocalFiles(\n            context,\n            files,\n            userProfile: context.read<MediaCellBloc>().state.userProfile,\n            documentId: context.read<MediaCellBloc>().rowId,\n            onUploadSuccess: (file, path, isLocalMode) {\n              final mediaCellBloc = context.read<MediaCellBloc>();\n              if (mediaCellBloc.isClosed) {\n                return;\n              }\n\n              mediaCellBloc.add(\n                MediaCellEvent.addFile(\n                  url: path,\n                  name: file.name,\n                  uploadType: isLocalMode\n                      ? FileUploadTypePB.LocalFile\n                      : FileUploadTypePB.CloudFile,\n                  fileType: file.fileType.toMediaFileTypePB(),\n                ),\n              );\n            },\n          );\n        },\n        onInsertNetworkFile: (url) async => _onInsertNetworkFile(\n          url,\n          dialogContext,\n          context,\n        ),\n      ),\n    );\n  }\n\n  Future<void> _onInsertNetworkFile(\n    String url,\n    BuildContext dialogContext,\n    BuildContext context,\n  ) async {\n    dialogContext.pop();\n\n    if (url.isEmpty) return;\n    final uri = Uri.tryParse(url);\n    if (uri == null) {\n      return;\n    }\n\n    final fakeFile = XFile(uri.path);\n    MediaFileTypePB fileType = fakeFile.fileType.toMediaFileTypePB();\n    fileType =\n        fileType == MediaFileTypePB.Other ? MediaFileTypePB.Link : fileType;\n\n    String name = uri.pathSegments.isNotEmpty ? uri.pathSegments.last : \"\";\n    if (name.isEmpty && uri.pathSegments.length > 1) {\n      name = uri.pathSegments[uri.pathSegments.length - 2];\n    } else if (name.isEmpty) {\n      name = uri.host;\n    }\n\n    context.read<MediaCellBloc>().add(\n          MediaCellEvent.addFile(\n            url: url,\n            name: name,\n            uploadType: FileUploadTypePB.NetworkFile,\n            fileType: fileType,\n          ),\n        );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileBottomSheetRenameWidget extends StatefulWidget {\n  const MobileBottomSheetRenameWidget({\n    super.key,\n    required this.name,\n    required this.onRename,\n    this.padding = const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),\n  });\n\n  final String name;\n  final void Function(String name) onRename;\n  final EdgeInsets padding;\n\n  @override\n  State<MobileBottomSheetRenameWidget> createState() =>\n      _MobileBottomSheetRenameWidgetState();\n}\n\nclass _MobileBottomSheetRenameWidgetState\n    extends State<MobileBottomSheetRenameWidget> {\n  late final TextEditingController controller;\n\n  @override\n  void initState() {\n    super.initState();\n    controller = TextEditingController(text: widget.name)\n      ..selection = TextSelection(\n        baseOffset: 0,\n        extentOffset: widget.name.length,\n      );\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: widget.padding,\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Expanded(\n            child: SizedBox(\n              height: 42.0,\n              child: FlowyTextField(\n                controller: controller,\n                textStyle: Theme.of(context).textTheme.bodyMedium,\n                keyboardType: TextInputType.text,\n                onSubmitted: (text) => widget.onRename(text),\n              ),\n            ),\n          ),\n          const HSpace(12.0),\n          FlowyTextButton(\n            LocaleKeys.button_edit.tr(),\n            constraints: const BoxConstraints.tightFor(height: 42),\n            padding: const EdgeInsets.symmetric(\n              horizontal: 16.0,\n            ),\n            fontColor: Colors.white,\n            fillColor: Theme.of(context).primaryColor,\n            onPressed: () {\n              widget.onRename(controller.text);\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';\nimport 'package:appflowy/startup/tasks/app_widget.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:fluttertoast/fluttertoast.dart';\n\nenum MobileBottomSheetType {\n  view,\n  rename,\n}\n\nclass MobileViewItemBottomSheet extends StatefulWidget {\n  const MobileViewItemBottomSheet({\n    super.key,\n    required this.view,\n    required this.actions,\n    this.defaultType = MobileBottomSheetType.view,\n  });\n\n  final ViewPB view;\n  final MobileBottomSheetType defaultType;\n  final List<MobileViewItemBottomSheetBodyAction> actions;\n\n  @override\n  State<MobileViewItemBottomSheet> createState() =>\n      _MobileViewItemBottomSheetState();\n}\n\nclass _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {\n  MobileBottomSheetType type = MobileBottomSheetType.view;\n  final fToast = FToast();\n\n  @override\n  void initState() {\n    super.initState();\n\n    type = widget.defaultType;\n    fToast.init(AppGlobals.context);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    switch (type) {\n      case MobileBottomSheetType.view:\n        return MobileViewItemBottomSheetBody(\n          actions: widget.actions,\n          isFavorite: widget.view.isFavorite,\n          onAction: (action) {\n            switch (action) {\n              case MobileViewItemBottomSheetBodyAction.rename:\n                setState(() {\n                  type = MobileBottomSheetType.rename;\n                });\n                break;\n              case MobileViewItemBottomSheetBodyAction.duplicate:\n                Navigator.pop(context);\n                context.read<ViewBloc>().add(const ViewEvent.duplicate());\n                showToastNotification(\n                  message: LocaleKeys.button_duplicateSuccessfully.tr(),\n                );\n                break;\n              case MobileViewItemBottomSheetBodyAction.share:\n                // unimplemented\n                Navigator.pop(context);\n                break;\n              case MobileViewItemBottomSheetBodyAction.delete:\n                Navigator.pop(context);\n                context.read<ViewBloc>().add(const ViewEvent.delete());\n                break;\n              case MobileViewItemBottomSheetBodyAction.addToFavorites:\n              case MobileViewItemBottomSheetBodyAction.removeFromFavorites:\n                Navigator.pop(context);\n                context\n                    .read<FavoriteBloc>()\n                    .add(FavoriteEvent.toggle(widget.view));\n                showToastNotification(\n                  message: !widget.view.isFavorite\n                      ? LocaleKeys.button_favoriteSuccessfully.tr()\n                      : LocaleKeys.button_unfavoriteSuccessfully.tr(),\n                );\n                break;\n              case MobileViewItemBottomSheetBodyAction.removeFromRecent:\n                _removeFromRecent(context);\n                break;\n              case MobileViewItemBottomSheetBodyAction.divider:\n                break;\n            }\n          },\n        );\n      case MobileBottomSheetType.rename:\n        return MobileBottomSheetRenameWidget(\n          name: widget.view.name,\n          onRename: (name) {\n            if (name != widget.view.name) {\n              context.read<ViewBloc>().add(ViewEvent.rename(name));\n            }\n            Navigator.pop(context);\n          },\n        );\n    }\n  }\n\n  Future<void> _removeFromRecent(BuildContext context) async {\n    final viewId = context.read<ViewBloc>().view.id;\n    final recentViewsBloc = context.read<RecentViewsBloc>();\n    Navigator.pop(context);\n\n    await _showConfirmDialog(\n      onDelete: () {\n        recentViewsBloc.add(RecentViewsEvent.removeRecentViews([viewId]));\n      },\n    );\n  }\n\n  Future<void> _showConfirmDialog({required VoidCallback onDelete}) async {\n    await showFlowyCupertinoConfirmDialog(\n      title: LocaleKeys.sideBar_removePageFromRecent.tr(),\n      leftButton: FlowyText(\n        LocaleKeys.button_cancel.tr(),\n        fontSize: 17.0,\n        figmaLineHeight: 24.0,\n        fontWeight: FontWeight.w500,\n        color: const Color(0xFF007AFF),\n      ),\n      rightButton: FlowyText(\n        LocaleKeys.button_delete.tr(),\n        fontSize: 17.0,\n        figmaLineHeight: 24.0,\n        fontWeight: FontWeight.w400,\n        color: const Color(0xFFFE0220),\n      ),\n      onRightButtonPressed: (context) {\n        onDelete();\n\n        Navigator.pop(context);\n\n        showToastNotification(\n          message: LocaleKeys.sideBar_removeSuccess.tr(),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart",
    "content": "import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nenum MobileViewItemBottomSheetBodyAction {\n  rename,\n  duplicate,\n  share,\n  delete,\n  addToFavorites,\n  removeFromFavorites,\n  divider,\n  removeFromRecent,\n}\n\nclass MobileViewItemBottomSheetBody extends StatelessWidget {\n  const MobileViewItemBottomSheetBody({\n    super.key,\n    this.isFavorite = false,\n    required this.onAction,\n    required this.actions,\n  });\n\n  final bool isFavorite;\n  final void Function(MobileViewItemBottomSheetBodyAction action) onAction;\n  final List<MobileViewItemBottomSheetBodyAction> actions;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.stretch,\n      children:\n          actions.map((action) => _buildActionButton(context, action)).toList(),\n    );\n  }\n\n  Widget _buildActionButton(\n    BuildContext context,\n    MobileViewItemBottomSheetBodyAction action,\n  ) {\n    final isLocked =\n        context.read<PageAccessLevelBloc?>()?.state.isLocked ?? false;\n    switch (action) {\n      case MobileViewItemBottomSheetBodyAction.rename:\n        return FlowyOptionTile.text(\n          text: LocaleKeys.button_rename.tr(),\n          height: 52.0,\n          leftIcon: const FlowySvg(\n            FlowySvgs.view_item_rename_s,\n            size: Size.square(18),\n          ),\n          enable: !isLocked,\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(\n            MobileViewItemBottomSheetBodyAction.rename,\n          ),\n        );\n      case MobileViewItemBottomSheetBodyAction.duplicate:\n        return FlowyOptionTile.text(\n          text: LocaleKeys.button_duplicate.tr(),\n          height: 52.0,\n          leftIcon: const FlowySvg(\n            FlowySvgs.duplicate_s,\n            size: Size.square(18),\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(\n            MobileViewItemBottomSheetBodyAction.duplicate,\n          ),\n        );\n\n      case MobileViewItemBottomSheetBodyAction.share:\n        return FlowyOptionTile.text(\n          text: LocaleKeys.button_share.tr(),\n          height: 52.0,\n          leftIcon: const FlowySvg(\n            FlowySvgs.share_s,\n            size: Size.square(18),\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(\n            MobileViewItemBottomSheetBodyAction.share,\n          ),\n        );\n      case MobileViewItemBottomSheetBodyAction.delete:\n        return FlowyOptionTile.text(\n          text: LocaleKeys.button_delete.tr(),\n          height: 52.0,\n          textColor: Theme.of(context).colorScheme.error,\n          leftIcon: FlowySvg(\n            FlowySvgs.trash_s,\n            size: const Size.square(18),\n            color: Theme.of(context).colorScheme.error,\n          ),\n          enable: !isLocked,\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(\n            MobileViewItemBottomSheetBodyAction.delete,\n          ),\n        );\n      case MobileViewItemBottomSheetBodyAction.addToFavorites:\n        return FlowyOptionTile.text(\n          height: 52.0,\n          text: LocaleKeys.button_addToFavorites.tr(),\n          leftIcon: const FlowySvg(\n            FlowySvgs.favorite_s,\n            size: Size.square(18),\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(\n            MobileViewItemBottomSheetBodyAction.addToFavorites,\n          ),\n        );\n      case MobileViewItemBottomSheetBodyAction.removeFromFavorites:\n        return FlowyOptionTile.text(\n          height: 52.0,\n          text: LocaleKeys.button_removeFromFavorites.tr(),\n          leftIcon: const FlowySvg(\n            FlowySvgs.favorite_section_remove_from_favorite_s,\n            size: Size.square(18),\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(\n            MobileViewItemBottomSheetBodyAction.removeFromFavorites,\n          ),\n        );\n      case MobileViewItemBottomSheetBodyAction.removeFromRecent:\n        return FlowyOptionTile.text(\n          height: 52.0,\n          text: LocaleKeys.button_removeFromRecent.tr(),\n          leftIcon: const FlowySvg(\n            FlowySvgs.remove_from_recent_s,\n            size: Size.square(18),\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(\n            MobileViewItemBottomSheetBodyAction.removeFromRecent,\n          ),\n        );\n\n      case MobileViewItemBottomSheetBodyAction.divider:\n        return const Padding(\n          padding: EdgeInsets.symmetric(horizontal: 12.0),\n          child: Divider(height: 0.5),\n        );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart",
    "content": "import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';\nimport 'package:appflowy/plugins/shared/share/share_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nenum MobileViewBottomSheetBodyAction {\n  undo,\n  redo,\n  rename,\n  duplicate,\n  delete,\n  addToFavorites,\n  removeFromFavorites,\n  helpCenter,\n  publish,\n  unpublish,\n  copyPublishLink,\n  visitSite,\n  copyShareLink,\n  updatePathName,\n  lockPage;\n\n  static const disableInLockedView = [\n    undo,\n    redo,\n    rename,\n    delete,\n  ];\n}\n\nclass MobileViewBottomSheetBodyActionArguments {\n  static const isLockedKey = 'is_locked';\n}\n\ntypedef MobileViewBottomSheetBodyActionCallback = void Function(\n  MobileViewBottomSheetBodyAction action,\n  // for the [MobileViewBottomSheetBodyAction.lockPage] action,\n  // it will pass the [isLocked] value to the callback.\n  {\n  Map<String, dynamic>? arguments,\n});\n\nclass ViewPageBottomSheet extends StatefulWidget {\n  const ViewPageBottomSheet({\n    super.key,\n    required this.view,\n    required this.onAction,\n    required this.onRename,\n  });\n\n  final ViewPB view;\n  final MobileViewBottomSheetBodyActionCallback onAction;\n  final void Function(String name) onRename;\n\n  @override\n  State<ViewPageBottomSheet> createState() => _ViewPageBottomSheetState();\n}\n\nclass _ViewPageBottomSheetState extends State<ViewPageBottomSheet> {\n  MobileBottomSheetType type = MobileBottomSheetType.view;\n\n  @override\n  Widget build(BuildContext context) {\n    switch (type) {\n      case MobileBottomSheetType.view:\n        return MobileViewBottomSheetBody(\n          view: widget.view,\n          onAction: (action, {arguments}) {\n            switch (action) {\n              case MobileViewBottomSheetBodyAction.rename:\n                setState(() {\n                  type = MobileBottomSheetType.rename;\n                });\n                break;\n              default:\n                widget.onAction(action, arguments: arguments);\n            }\n          },\n        );\n\n      case MobileBottomSheetType.rename:\n        return MobileBottomSheetRenameWidget(\n          name: widget.view.name,\n          onRename: (name) {\n            widget.onRename(name);\n          },\n        );\n    }\n  }\n}\n\nclass MobileViewBottomSheetBody extends StatelessWidget {\n  const MobileViewBottomSheetBody({\n    super.key,\n    required this.view,\n    required this.onAction,\n  });\n\n  final ViewPB view;\n  final MobileViewBottomSheetBodyActionCallback onAction;\n\n  @override\n  Widget build(BuildContext context) {\n    final isFavorite = view.isFavorite;\n    final isEditable =\n        context.watch<PageAccessLevelBloc?>()?.state.isEditable ?? false;\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.stretch,\n      children: [\n        MobileQuickActionButton(\n          text: LocaleKeys.button_rename.tr(),\n          icon: FlowySvgs.view_item_rename_s,\n          iconSize: const Size.square(18),\n          enable: isEditable,\n          onTap: () => onAction(\n            MobileViewBottomSheetBodyAction.rename,\n          ),\n        ),\n        _divider(),\n        MobileQuickActionButton(\n          text: isFavorite\n              ? LocaleKeys.button_removeFromFavorites.tr()\n              : LocaleKeys.button_addToFavorites.tr(),\n          icon: isFavorite ? FlowySvgs.unfavorite_s : FlowySvgs.favorite_s,\n          iconSize: const Size.square(18),\n          onTap: () => onAction(\n            isFavorite\n                ? MobileViewBottomSheetBodyAction.removeFromFavorites\n                : MobileViewBottomSheetBodyAction.addToFavorites,\n          ),\n        ),\n        _divider(),\n        if (view.layout.isDatabaseView || view.layout.isDocumentView) ...[\n          MobileQuickActionButton(\n            text: LocaleKeys.disclosureAction_lockPage.tr(),\n            icon: FlowySvgs.lock_page_s,\n            iconSize: const Size.square(18),\n            rightIconBuilder: (context) => _LockPageRightIconBuilder(\n              onAction: onAction,\n            ),\n            onTap: () {\n              final isLocked =\n                  context.read<PageAccessLevelBloc?>()?.state.isLocked ?? false;\n              onAction(\n                MobileViewBottomSheetBodyAction.lockPage,\n                arguments: {\n                  MobileViewBottomSheetBodyActionArguments.isLockedKey:\n                      isLocked,\n                },\n              );\n            },\n          ),\n          _divider(),\n        ],\n        MobileQuickActionButton(\n          text: LocaleKeys.button_duplicate.tr(),\n          icon: FlowySvgs.duplicate_s,\n          iconSize: const Size.square(18),\n          onTap: () => onAction(\n            MobileViewBottomSheetBodyAction.duplicate,\n          ),\n        ),\n        // copy link\n        _divider(),\n        MobileQuickActionButton(\n          text: LocaleKeys.shareAction_copyLink.tr(),\n          icon: FlowySvgs.m_copy_link_s,\n          iconSize: const Size.square(18),\n          onTap: () => onAction(\n            MobileViewBottomSheetBodyAction.copyShareLink,\n          ),\n        ),\n        _divider(),\n        ..._buildPublishActions(context),\n\n        MobileQuickActionButton(\n          text: LocaleKeys.button_delete.tr(),\n          textColor: Theme.of(context).colorScheme.error,\n          icon: FlowySvgs.trash_s,\n          iconColor: Theme.of(context).colorScheme.error,\n          iconSize: const Size.square(18),\n          enable: isEditable,\n          onTap: () => onAction(\n            MobileViewBottomSheetBodyAction.delete,\n          ),\n        ),\n        _divider(),\n      ],\n    );\n  }\n\n  List<Widget> _buildPublishActions(BuildContext context) {\n    final userProfile = context.read<MobileViewPageBloc>().state.userProfilePB;\n    // the publish feature is only available for AppFlowy Cloud\n    if (userProfile == null ||\n        userProfile.workspaceType != WorkspaceTypePB.ServerW) {\n      return [];\n    }\n\n    final isPublished = context.watch<ShareBloc>().state.isPublished;\n    if (isPublished) {\n      return [\n        MobileQuickActionButton(\n          text: LocaleKeys.shareAction_updatePathName.tr(),\n          icon: FlowySvgs.view_item_rename_s,\n          iconSize: const Size.square(18),\n          onTap: () => onAction(\n            MobileViewBottomSheetBodyAction.updatePathName,\n          ),\n        ),\n        _divider(),\n        MobileQuickActionButton(\n          text: LocaleKeys.shareAction_visitSite.tr(),\n          icon: FlowySvgs.m_visit_site_s,\n          iconSize: const Size.square(18),\n          onTap: () => onAction(\n            MobileViewBottomSheetBodyAction.visitSite,\n          ),\n        ),\n        _divider(),\n        MobileQuickActionButton(\n          text: LocaleKeys.shareAction_unPublish.tr(),\n          icon: FlowySvgs.m_unpublish_s,\n          iconSize: const Size.square(18),\n          onTap: () => onAction(\n            MobileViewBottomSheetBodyAction.unpublish,\n          ),\n        ),\n        _divider(),\n      ];\n    } else {\n      return [\n        MobileQuickActionButton(\n          text: LocaleKeys.shareAction_publish.tr(),\n          icon: FlowySvgs.m_publish_s,\n          onTap: () => onAction(\n            MobileViewBottomSheetBodyAction.publish,\n          ),\n        ),\n        _divider(),\n      ];\n    }\n  }\n\n  Widget _divider() => const MobileQuickActionDivider();\n}\n\nclass _LockPageRightIconBuilder extends StatelessWidget {\n  const _LockPageRightIconBuilder({\n    required this.onAction,\n  });\n\n  final MobileViewBottomSheetBodyActionCallback onAction;\n\n  @override\n  Widget build(BuildContext context) {\n    final isEditable =\n        context.watch<PageAccessLevelBloc?>()?.state.isEditable ?? false;\n    return SizedBox(\n      width: 46,\n      height: 30,\n      child: FittedBox(\n        fit: BoxFit.fill,\n        child: CupertinoSwitch(\n          value: isEditable,\n          activeTrackColor: Theme.of(context).colorScheme.primary,\n          onChanged: (value) {\n            onAction(\n              MobileViewBottomSheetBodyAction.lockPage,\n              arguments: {\n                MobileViewBottomSheetBodyActionArguments.isLockedKey: value,\n              },\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart",
    "content": "import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart';\nimport 'package:appflowy/mobile/presentation/page_item/mobile_slide_action_button.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_slidable/flutter_slidable.dart';\n\nenum MobilePaneActionType {\n  delete,\n  addToFavorites,\n  removeFromFavorites,\n  more,\n  add;\n\n  MobileSlideActionButton actionButton(\n    BuildContext context, {\n    MobilePageCardType? cardType,\n    FolderSpaceType? spaceType,\n  }) {\n    switch (this) {\n      case MobilePaneActionType.delete:\n        return MobileSlideActionButton(\n          backgroundColor: Colors.red,\n          svg: FlowySvgs.delete_s,\n          size: 30.0,\n          onPressed: (context) =>\n              context.read<ViewBloc>().add(const ViewEvent.delete()),\n        );\n      case MobilePaneActionType.removeFromFavorites:\n        return MobileSlideActionButton(\n          backgroundColor: const Color(0xFFFA217F),\n          svg: FlowySvgs.favorite_section_remove_from_favorite_s,\n          size: 24.0,\n          onPressed: (context) {\n            showToastNotification(\n              message: LocaleKeys.button_unfavoriteSuccessfully.tr(),\n            );\n\n            context\n                .read<FavoriteBloc>()\n                .add(FavoriteEvent.toggle(context.read<ViewBloc>().view));\n          },\n        );\n      case MobilePaneActionType.addToFavorites:\n        return MobileSlideActionButton(\n          backgroundColor: const Color(0xFF00C8FF),\n          svg: FlowySvgs.favorite_s,\n          size: 24.0,\n          onPressed: (context) {\n            showToastNotification(\n              message: LocaleKeys.button_favoriteSuccessfully.tr(),\n            );\n\n            context\n                .read<FavoriteBloc>()\n                .add(FavoriteEvent.toggle(context.read<ViewBloc>().view));\n          },\n        );\n      case MobilePaneActionType.add:\n        return MobileSlideActionButton(\n          backgroundColor: const Color(0xFF00C8FF),\n          svg: FlowySvgs.add_m,\n          size: 28.0,\n          onPressed: (context) {\n            final viewBloc = context.read<ViewBloc>();\n            final view = viewBloc.state.view;\n            final title = view.name;\n            showMobileBottomSheet(\n              context,\n              showHeader: true,\n              title: title,\n              showDragHandle: true,\n              showCloseButton: true,\n              useRootNavigator: true,\n              showDivider: false,\n              backgroundColor: Theme.of(context).colorScheme.surface,\n              builder: (sheetContext) {\n                return AddNewPageWidgetBottomSheet(\n                  view: view,\n                  onAction: (layout) {\n                    Navigator.of(sheetContext).pop();\n                    viewBloc.add(\n                      ViewEvent.createView(\n                        layout.defaultName,\n                        layout,\n                        section: spaceType!.toViewSectionPB,\n                      ),\n                    );\n                  },\n                );\n              },\n            );\n          },\n        );\n      case MobilePaneActionType.more:\n        return MobileSlideActionButton(\n          backgroundColor: const Color(0xE5515563),\n          svg: FlowySvgs.three_dots_s,\n          size: 24.0,\n          borderRadius: const BorderRadius.only(\n            topLeft: Radius.circular(10),\n            bottomLeft: Radius.circular(10),\n          ),\n          onPressed: (context) {\n            final viewBloc = context.read<ViewBloc>();\n            final favoriteBloc = context.read<FavoriteBloc>();\n            final recentViewsBloc = context.read<RecentViewsBloc?>();\n            showMobileBottomSheet(\n              context,\n              showDragHandle: true,\n              showDivider: false,\n              useRootNavigator: true,\n              backgroundColor: Theme.of(context).colorScheme.surface,\n              builder: (context) {\n                return MultiBlocProvider(\n                  providers: [\n                    BlocProvider.value(value: viewBloc),\n                    BlocProvider.value(value: favoriteBloc),\n                    if (recentViewsBloc != null)\n                      BlocProvider.value(value: recentViewsBloc),\n                    BlocProvider(\n                      create: (_) =>\n                          PageAccessLevelBloc(view: viewBloc.state.view)\n                            ..add(const PageAccessLevelEvent.initial()),\n                    ),\n                  ],\n                  child: BlocBuilder<ViewBloc, ViewState>(\n                    builder: (context, state) {\n                      return MobileViewItemBottomSheet(\n                        view: viewBloc.state.view,\n                        actions: _buildActions(state.view, cardType: cardType),\n                      );\n                    },\n                  ),\n                );\n              },\n            );\n          },\n        );\n    }\n  }\n\n  List<MobileViewItemBottomSheetBodyAction> _buildActions(\n    ViewPB view, {\n    MobilePageCardType? cardType,\n  }) {\n    final isFavorite = view.isFavorite;\n\n    if (cardType != null) {\n      switch (cardType) {\n        case MobilePageCardType.recent:\n          return [\n            isFavorite\n                ? MobileViewItemBottomSheetBodyAction.removeFromFavorites\n                : MobileViewItemBottomSheetBodyAction.addToFavorites,\n            MobileViewItemBottomSheetBodyAction.divider,\n            MobileViewItemBottomSheetBodyAction.divider,\n            MobileViewItemBottomSheetBodyAction.removeFromRecent,\n          ];\n        case MobilePageCardType.favorite:\n          return [\n            isFavorite\n                ? MobileViewItemBottomSheetBodyAction.removeFromFavorites\n                : MobileViewItemBottomSheetBodyAction.addToFavorites,\n            MobileViewItemBottomSheetBodyAction.divider,\n          ];\n      }\n    }\n\n    return [\n      isFavorite\n          ? MobileViewItemBottomSheetBodyAction.removeFromFavorites\n          : MobileViewItemBottomSheetBodyAction.addToFavorites,\n      MobileViewItemBottomSheetBodyAction.divider,\n      MobileViewItemBottomSheetBodyAction.rename,\n      if (view.layout != ViewLayoutPB.Chat)\n        MobileViewItemBottomSheetBodyAction.duplicate,\n      MobileViewItemBottomSheetBodyAction.divider,\n      MobileViewItemBottomSheetBodyAction.delete,\n    ];\n  }\n}\n\nActionPane buildEndActionPane(\n  BuildContext context,\n  List<MobilePaneActionType> actions, {\n  bool needSpace = true,\n  MobilePageCardType? cardType,\n  FolderSpaceType? spaceType,\n  required double spaceRatio,\n}) {\n  return ActionPane(\n    motion: const ScrollMotion(),\n    extentRatio: actions.length / spaceRatio,\n    children: [\n      if (needSpace) const HSpace(60),\n      ...actions.map(\n        (action) => action.actionButton(\n          context,\n          spaceType: spaceType,\n          cardType: cardType,\n        ),\n      ),\n    ],\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart",
    "content": "import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';\nimport 'package:appflowy/plugins/base/drag_handler.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nextension BottomSheetPaddingExtension on BuildContext {\n  /// Calculates the total amount of space that should be added to the bottom of\n  /// a bottom sheet\n  double bottomSheetPadding({\n    bool ignoreViewPadding = true,\n  }) {\n    final viewPadding = MediaQuery.viewPaddingOf(this);\n    final viewInsets = MediaQuery.viewInsetsOf(this);\n    double bottom = 0.0;\n    if (!ignoreViewPadding) {\n      bottom += viewPadding.bottom;\n    }\n    // for screens with 0 view padding, add some even more space\n    bottom += viewPadding.bottom == 0 ? 28.0 : 16.0;\n    bottom += viewInsets.bottom;\n    return bottom;\n  }\n}\n\nFuture<T?> showMobileBottomSheet<T>(\n  BuildContext context, {\n  required WidgetBuilder builder,\n  bool useSafeArea = true,\n  bool isDragEnabled = true,\n  bool showDragHandle = false,\n  bool showHeader = false,\n  // this field is only used if showHeader is true\n  bool showBackButton = false,\n  bool showCloseButton = false,\n  bool showRemoveButton = false,\n  VoidCallback? onRemove,\n  // this field is only used if showHeader is true\n  String title = '',\n  bool isScrollControlled = true,\n  bool showDivider = true,\n  bool useRootNavigator = false,\n  ShapeBorder? shape,\n  // the padding of the content, the padding of the header area is fixed\n  EdgeInsets padding = EdgeInsets.zero,\n  Color? backgroundColor,\n  BoxConstraints? constraints,\n  Color? barrierColor,\n  double? elevation,\n  bool showDoneButton = false,\n  void Function(BuildContext context)? onDone,\n  bool enableDraggableScrollable = false,\n  bool enableScrollable = false,\n  // this field is only used if showDragHandle is true\n  Widget Function(BuildContext, ScrollController)? scrollableWidgetBuilder,\n  // only used when enableDraggableScrollable is true\n  double minChildSize = 0.5,\n  double maxChildSize = 0.8,\n  double initialChildSize = 0.51,\n  double bottomSheetPadding = 0,\n  bool enablePadding = true,\n  WidgetBuilder? dragHandleBuilder,\n}) async {\n  assert(\n    showHeader ||\n        title.isEmpty && !showCloseButton && !showBackButton && !showDoneButton,\n  );\n  assert(!(showCloseButton && showBackButton));\n\n  shape ??= const RoundedRectangleBorder(\n    borderRadius: BorderRadius.vertical(\n      top: Radius.circular(16),\n    ),\n  );\n\n  backgroundColor ??= Theme.of(context).brightness == Brightness.light\n      ? const Color(0xFFF7F8FB)\n      : const Color(0xFF23262B);\n  barrierColor ??= Colors.black.withValues(alpha: 0.3);\n\n  return showModalBottomSheet<T>(\n    context: context,\n    isScrollControlled: isScrollControlled,\n    enableDrag: isDragEnabled,\n    useSafeArea: true,\n    clipBehavior: Clip.antiAlias,\n    constraints: constraints,\n    barrierColor: barrierColor,\n    elevation: elevation,\n    backgroundColor: backgroundColor,\n    shape: shape,\n    useRootNavigator: useRootNavigator,\n    builder: (context) {\n      final List<Widget> children = [];\n\n      final Widget child = builder(context);\n\n      // if the children is only one, we don't need to wrap it with a column\n      if (!showDragHandle && !showHeader && !showDivider) {\n        return child;\n      }\n\n      // ----- header area -----\n      if (showDragHandle) {\n        children.add(\n          dragHandleBuilder?.call(context) ?? const DragHandle(),\n        );\n      }\n\n      if (showHeader) {\n        children.add(\n          BottomSheetHeader(\n            showCloseButton: showCloseButton,\n            showBackButton: showBackButton,\n            showDoneButton: showDoneButton,\n            showRemoveButton: showRemoveButton,\n            title: title,\n            onRemove: onRemove,\n            onDone: onDone,\n          ),\n        );\n\n        if (showDivider) {\n          children.add(\n            const Divider(height: 0.5, thickness: 0.5),\n          );\n        }\n      }\n\n      // ----- header area -----\n\n      if (enableDraggableScrollable) {\n        final keyboardSize =\n            context.bottomSheetPadding() / MediaQuery.of(context).size.height;\n        return DraggableScrollableSheet(\n          expand: false,\n          snap: true,\n          initialChildSize: (initialChildSize + keyboardSize).clamp(0, 1),\n          minChildSize: (minChildSize + keyboardSize).clamp(0, 1.0),\n          maxChildSize: (maxChildSize + keyboardSize).clamp(0, 1.0),\n          builder: (context, scrollController) {\n            return Column(\n              children: [\n                ...children,\n                scrollableWidgetBuilder?.call(\n                      context,\n                      scrollController,\n                    ) ??\n                    Expanded(\n                      child: Scrollbar(\n                        controller: scrollController,\n                        child: SingleChildScrollView(\n                          controller: scrollController,\n                          child: child,\n                        ),\n                      ),\n                    ),\n              ],\n            );\n          },\n        );\n      } else if (enableScrollable) {\n        return Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            ...children,\n            Flexible(\n              child: SingleChildScrollView(\n                child: child,\n              ),\n            ),\n            VSpace(bottomSheetPadding),\n          ],\n        );\n      }\n\n      // ----- content area -----\n      if (enablePadding) {\n        // add content padding and extra bottom padding\n        children.add(\n          Padding(\n            padding:\n                padding + EdgeInsets.only(bottom: context.bottomSheetPadding()),\n            child: child,\n          ),\n        );\n      } else {\n        children.add(child);\n      }\n      // ----- content area -----\n\n      if (children.length == 1) {\n        return children.first;\n      }\n\n      return useSafeArea\n          ? SafeArea(\n              child: Column(\n                mainAxisSize: MainAxisSize.min,\n                children: children,\n              ),\n            )\n          : Column(\n              mainAxisSize: MainAxisSize.min,\n              children: children,\n            );\n    },\n  );\n}\n\nclass BottomSheetHeader extends StatelessWidget {\n  const BottomSheetHeader({\n    super.key,\n    required this.showBackButton,\n    required this.showCloseButton,\n    required this.showRemoveButton,\n    required this.title,\n    required this.showDoneButton,\n    this.onRemove,\n    this.onDone,\n    this.onBack,\n    this.onClose,\n  });\n\n  final String title;\n\n  final bool showBackButton;\n  final bool showCloseButton;\n  final bool showRemoveButton;\n  final bool showDoneButton;\n\n  final VoidCallback? onRemove;\n  final VoidCallback? onBack;\n  final VoidCallback? onClose;\n\n  final void Function(BuildContext context)? onDone;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Padding(\n      padding: const EdgeInsets.only(bottom: 4.0),\n      child: SizedBox(\n        height: 44.0, // the height of the header area is fixed\n        child: Stack(\n          children: [\n            if (showBackButton)\n              Align(\n                alignment: Alignment.centerLeft,\n                child: BottomSheetBackButton(\n                  onTap: onBack,\n                ),\n              ),\n            if (showCloseButton)\n              Align(\n                alignment: Alignment.centerLeft,\n                child: BottomSheetCloseButton(\n                  onTap: onClose,\n                ),\n              ),\n            if (showRemoveButton)\n              Align(\n                alignment: Alignment.centerLeft,\n                child: BottomSheetRemoveButton(\n                  onRemove: () => onRemove?.call(),\n                ),\n              ),\n            Align(\n              child: Container(\n                constraints: const BoxConstraints(maxWidth: 250),\n                child: Text(\n                  title,\n                  style: theme.textStyle.heading4.prominent(\n                    color: theme.textColorScheme.primary,\n                  ),\n                ),\n              ),\n            ),\n            if (showDoneButton)\n              Align(\n                alignment: Alignment.centerRight,\n                child: BottomSheetDoneButton(\n                  onDone: () {\n                    if (onDone != null) {\n                      onDone?.call(context);\n                    } else {\n                      Navigator.pop(context);\n                    }\n                  },\n                ),\n              ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:appflowy/plugins/base/drag_handler.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:sheet/route.dart';\nimport 'package:sheet/sheet.dart';\n\nimport 'show_mobile_bottom_sheet.dart';\n\nFuture<T?> showTransitionMobileBottomSheet<T>(\n  BuildContext context, {\n  required WidgetBuilder builder,\n  bool useRootNavigator = false,\n  EdgeInsets contentPadding = EdgeInsets.zero,\n  Color? backgroundColor,\n  // drag handle\n  bool showDragHandle = false,\n  // header\n  bool showHeader = false,\n  String title = '',\n  bool showBackButton = false,\n  bool showCloseButton = false,\n  bool showDoneButton = false,\n  bool showDivider = true,\n  // stops\n  double initialStop = 1.0,\n  List<double>? stops,\n}) {\n  assert(\n    showHeader ||\n        title.isEmpty &&\n            !showCloseButton &&\n            !showBackButton &&\n            !showDoneButton &&\n            !showDivider,\n  );\n  assert(!(showCloseButton && showBackButton));\n\n  backgroundColor ??= Theme.of(context).brightness == Brightness.light\n      ? const Color(0xFFF7F8FB)\n      : const Color(0xFF23262B);\n\n  return Navigator.of(\n    context,\n    rootNavigator: useRootNavigator,\n  ).push<T>(\n    TransitionSheetRoute<T>(\n      backgroundColor: backgroundColor,\n      initialStop: initialStop,\n      stops: stops,\n      builder: (context) {\n        final Widget child = builder(context);\n\n        // if the children is only one, we don't need to wrap it with a column\n        if (!showDragHandle && !showHeader) {\n          return child;\n        }\n\n        return Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            if (showDragHandle) const DragHandle(),\n            if (showHeader) ...[\n              BottomSheetHeader(\n                showCloseButton: showCloseButton,\n                showBackButton: showBackButton,\n                showDoneButton: showDoneButton,\n                showRemoveButton: false,\n                title: title,\n              ),\n              if (showDivider)\n                const Divider(\n                  height: 0.5,\n                  thickness: 0.5,\n                ),\n            ],\n            Expanded(\n              child: Padding(\n                padding: contentPadding,\n                child: child,\n              ),\n            ),\n          ],\n        );\n      },\n    ),\n  );\n}\n\n/// The top offset that will be displayed from the bottom route\nconst double _kPreviousRouteVisibleOffset = 10.0;\n\n/// Minimal distance from the top of the screen to the top of the previous route\n/// It will be used ff the top safe area is less than this value.\n/// In iPhones the top SafeArea is more or equal to this distance.\nconst double _kSheetMinimalOffset = 10;\n\nconst Curve _kCupertinoSheetCurve = Curves.easeOutExpo;\nconst Curve _kCupertinoTransitionCurve = Curves.linear;\n\n/// Wraps the child into a cupertino modal sheet appearance. This is used to\n/// create a [SheetRoute].\n///\n/// Clip the child widget to rectangle with top rounded corners and adds\n/// top padding and top safe area.\nclass _CupertinoSheetDecorationBuilder extends StatelessWidget {\n  const _CupertinoSheetDecorationBuilder({\n    required this.child,\n    required this.topRadius,\n    this.backgroundColor,\n  });\n\n  /// The child contained by the modal sheet\n  final Widget child;\n\n  /// The color to paint behind the child\n  final Color? backgroundColor;\n\n  /// The top corners of this modal sheet are rounded by this Radius\n  final Radius topRadius;\n\n  @override\n  Widget build(BuildContext context) {\n    return Builder(\n      builder: (BuildContext context) {\n        return Container(\n          clipBehavior: Clip.hardEdge,\n          decoration: BoxDecoration(\n            borderRadius: BorderRadius.vertical(top: topRadius),\n            color: backgroundColor,\n          ),\n          child: MediaQuery.removePadding(\n            context: context,\n            removeTop: true,\n            child: child,\n          ),\n        );\n      },\n    );\n  }\n}\n\n/// Customized CupertinoSheetRoute from the sheets package\n///\n/// A modal route that overlays a widget over the current route and animates\n/// it from the bottom with a cupertino modal sheet appearance\n///\n/// Clip the child widget to rectangle with top rounded corners and adds\n/// top padding and top safe area.\nclass TransitionSheetRoute<T> extends SheetRoute<T> {\n  TransitionSheetRoute({\n    required WidgetBuilder builder,\n    super.stops,\n    double initialStop = 1.0,\n    super.settings,\n    Color? backgroundColor,\n    super.maintainState = true,\n    super.fit,\n  }) : super(\n          builder: (BuildContext context) {\n            return _CupertinoSheetDecorationBuilder(\n              backgroundColor: backgroundColor,\n              topRadius: const Radius.circular(16),\n              child: Builder(builder: builder),\n            );\n          },\n          animationCurve: _kCupertinoSheetCurve,\n          initialExtent: initialStop,\n        );\n\n  @override\n  bool get draggable => true;\n\n  final SheetController _sheetController = SheetController();\n\n  @override\n  SheetController createSheetController() => _sheetController;\n\n  @override\n  Color? get barrierColor => Colors.transparent;\n\n  @override\n  bool get barrierDismissible => true;\n\n  @override\n  Widget buildSheet(BuildContext context, Widget child) {\n    final effectivePhysics = draggable\n        ? BouncingSheetPhysics(\n            parent: SnapSheetPhysics(\n              stops: stops ?? <double>[0, 1],\n              parent: physics,\n            ),\n          )\n        : const NeverDraggableSheetPhysics();\n    final MediaQueryData mediaQuery = MediaQuery.of(context);\n    final double topMargin =\n        math.max(_kSheetMinimalOffset, mediaQuery.padding.top) +\n            _kPreviousRouteVisibleOffset;\n    return Sheet.raw(\n      initialExtent: initialExtent,\n      decorationBuilder: decorationBuilder,\n      fit: fit,\n      maxExtent: mediaQuery.size.height - topMargin,\n      physics: effectivePhysics,\n      controller: sheetController,\n      child: child,\n    );\n  }\n\n  @override\n  Widget buildTransitions(\n    BuildContext context,\n    Animation<double> animation,\n    Animation<double> secondaryAnimation,\n    Widget child,\n  ) {\n    final double topPadding = MediaQuery.of(context).padding.top;\n    final double topOffset = math.max(_kSheetMinimalOffset, topPadding);\n    return AnimatedBuilder(\n      animation: secondaryAnimation,\n      child: child,\n      builder: (BuildContext context, Widget? child) {\n        final double progress = secondaryAnimation.value;\n        final double scale = 1 - progress / 10;\n        final double distanceWithScale =\n            (topOffset + _kPreviousRouteVisibleOffset) * 0.9;\n        final Offset offset =\n            Offset(0, progress * (topOffset - distanceWithScale));\n        return Transform.translate(\n          offset: offset,\n          child: Transform.scale(\n            scale: scale,\n            alignment: Alignment.topCenter,\n            child: child,\n          ),\n        );\n      },\n    );\n  }\n\n  @override\n  bool canDriveSecondaryTransitionForPreviousRoute(\n    Route<dynamic> previousRoute,\n  ) =>\n      true;\n\n  @override\n  Widget buildSecondaryTransitionForPreviousRoute(\n    BuildContext context,\n    Animation<double> secondaryAnimation,\n    Widget child,\n  ) {\n    final Animation<double> delayAnimation = CurvedAnimation(\n      parent: _sheetController.animation,\n      curve: Interval(\n        initialExtent == 1 ? 0 : initialExtent,\n        1,\n      ),\n    );\n\n    final Animation<double> secondaryAnimation = CurvedAnimation(\n      parent: _sheetController.animation,\n      curve: Interval(\n        0,\n        initialExtent,\n      ),\n    );\n\n    return CupertinoSheetBottomRouteTransition(\n      body: child,\n      sheetAnimation: delayAnimation,\n      secondaryAnimation: secondaryAnimation,\n    );\n  }\n}\n\n/// Animation for previous route when a [TransitionSheetRoute] enters/exits\n@visibleForTesting\nclass CupertinoSheetBottomRouteTransition extends StatelessWidget {\n  const CupertinoSheetBottomRouteTransition({\n    super.key,\n    required this.sheetAnimation,\n    required this.secondaryAnimation,\n    required this.body,\n  });\n\n  final Widget body;\n\n  final Animation<double> sheetAnimation;\n  final Animation<double> secondaryAnimation;\n\n  @override\n  Widget build(BuildContext context) {\n    final double topPadding = MediaQuery.of(context).padding.top;\n    final double topOffset = math.max(_kSheetMinimalOffset, topPadding);\n\n    final CurvedAnimation curvedAnimation = CurvedAnimation(\n      parent: sheetAnimation,\n      curve: _kCupertinoTransitionCurve,\n    );\n\n    return AnnotatedRegion<SystemUiOverlayStyle>(\n      value: SystemUiOverlayStyle.light,\n      child: AnimatedBuilder(\n        animation: secondaryAnimation,\n        child: body,\n        builder: (BuildContext context, Widget? child) {\n          final double progress = curvedAnimation.value;\n          final double scale = 1 - progress / 10;\n          return Stack(\n            children: <Widget>[\n              Container(color: Colors.black),\n              Transform.translate(\n                offset: Offset(0, progress * topOffset),\n                child: Transform.scale(\n                  scale: scale,\n                  alignment: Alignment.topCenter,\n                  child: ClipRRect(\n                    borderRadius: BorderRadius.vertical(\n                      top: Radius.lerp(\n                        Radius.zero,\n                        const Radius.circular(16.0),\n                        progress,\n                      )!,\n                    ),\n                    child: ColorFiltered(\n                      colorFilter: ColorFilter.mode(\n                        (Theme.of(context).brightness == Brightness.dark\n                                ? Colors.grey\n                                : Colors.black)\n                            .withValues(alpha: secondaryAnimation.value * 0.1),\n                        BlendMode.srcOver,\n                      ),\n                      child: child,\n                    ),\n                  ),\n                ),\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/chat/mobile_chat_screen.dart",
    "content": "import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileChatScreen extends StatelessWidget {\n  const MobileChatScreen({\n    super.key,\n    required this.id,\n    this.title,\n  });\n\n  /// view id\n  final String id;\n  final String? title;\n\n  static const routeName = '/chat';\n  static const viewId = 'id';\n  static const viewTitle = 'title';\n\n  @override\n  Widget build(BuildContext context) {\n    return MobileViewPage(\n      id: id,\n      title: title,\n      viewLayout: ViewLayoutPB.Chat,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/board/board.dart",
    "content": "export 'mobile_board_screen.dart';\nexport 'mobile_board_page.dart';\nexport 'widgets/mobile_hidden_groups_column.dart';\nexport 'widgets/mobile_board_trailing.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart",
    "content": "import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/database/board/board.dart';\nimport 'package:appflowy/mobile/presentation/database/board/widgets/group_card_header.dart';\nimport 'package:appflowy/mobile/presentation/database/card/card.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/board/application/board_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/card/card.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart';\nimport 'package:appflowy/shared/flowy_error_page.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_board/appflowy_board.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nclass MobileBoardPage extends StatefulWidget {\n  const MobileBoardPage({\n    super.key,\n    required this.view,\n    required this.databaseController,\n    this.onEditStateChanged,\n  });\n\n  final ViewPB view;\n\n  final DatabaseController databaseController;\n\n  /// Called when edit state changed\n  final VoidCallback? onEditStateChanged;\n\n  @override\n  State<MobileBoardPage> createState() => _MobileBoardPageState();\n}\n\nclass _MobileBoardPageState extends State<MobileBoardPage> {\n  late final ValueNotifier<DidCreateRowResult?> _didCreateRow;\n\n  @override\n  void initState() {\n    super.initState();\n    _didCreateRow = ValueNotifier(null)..addListener(_handleDidCreateRow);\n  }\n\n  @override\n  void dispose() {\n    _didCreateRow\n      ..removeListener(_handleDidCreateRow)\n      ..dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<BoardBloc>(\n      create: (_) => BoardBloc(\n        databaseController: widget.databaseController,\n        didCreateRow: _didCreateRow,\n      )..add(const BoardEvent.initial()),\n      child: BlocBuilder<BoardBloc, BoardState>(\n        builder: (context, state) => state.maybeMap(\n          loading: (_) => const Center(\n            child: CircularProgressIndicator.adaptive(),\n          ),\n          error: (err) => Center(\n            child: AppFlowyErrorPage(\n              error: err.error,\n            ),\n          ),\n          ready: (data) => const _BoardContent(),\n          orElse: () => const SizedBox.shrink(),\n        ),\n      ),\n    );\n  }\n\n  void _handleDidCreateRow() {\n    if (_didCreateRow.value != null) {\n      final result = _didCreateRow.value!;\n      switch (result.action) {\n        case DidCreateRowAction.openAsPage:\n          context.push(\n            MobileRowDetailPage.routeName,\n            extra: {\n              MobileRowDetailPage.argRowId: result.rowMeta.id,\n              MobileRowDetailPage.argDatabaseController:\n                  widget.databaseController,\n            },\n          );\n          break;\n        default:\n          break;\n      }\n    }\n  }\n}\n\nclass _BoardContent extends StatefulWidget {\n  const _BoardContent();\n\n  @override\n  State<_BoardContent> createState() => _BoardContentState();\n}\n\nclass _BoardContentState extends State<_BoardContent> {\n  late final ScrollController scrollController;\n\n  @override\n  void initState() {\n    super.initState();\n    scrollController = ScrollController();\n  }\n\n  @override\n  void dispose() {\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final screenWidth = MediaQuery.of(context).size.width;\n    final config = AppFlowyBoardConfig(\n      groupCornerRadius: 8,\n      groupBackgroundColor: Theme.of(context).colorScheme.secondary,\n      groupMargin: const EdgeInsets.fromLTRB(4, 0, 4, 12),\n      groupHeaderPadding: const EdgeInsets.all(8),\n      groupBodyPadding: const EdgeInsets.all(4),\n      groupFooterPadding: const EdgeInsets.all(8),\n      cardMargin: const EdgeInsets.all(4),\n    );\n\n    return BlocBuilder<BoardBloc, BoardState>(\n      builder: (context, state) {\n        return state.maybeMap(\n          orElse: () => const SizedBox.shrink(),\n          ready: (state) {\n            final isEditable =\n                context.watch<PageAccessLevelBloc?>()?.state.isEditable ??\n                    false;\n            final showCreateGroupButton = context\n                    .read<BoardBloc>()\n                    .groupingFieldType\n                    ?.canCreateNewGroup ??\n                false;\n            final showHiddenGroups = state.hiddenGroups.isNotEmpty;\n            return AppFlowyBoard(\n              scrollController: scrollController,\n              controller: context.read<BoardBloc>().boardController,\n              groupConstraints:\n                  BoxConstraints.tightFor(width: screenWidth * 0.7),\n              config: config,\n              leading: showHiddenGroups\n                  ? MobileHiddenGroupsColumn(\n                      padding: config.groupHeaderPadding,\n                    )\n                  : const HSpace(16),\n              trailing: showCreateGroupButton && isEditable\n                  ? const MobileBoardTrailing()\n                  : const HSpace(16),\n              headerBuilder: (_, groupData) {\n                return IgnorePointer(\n                  ignoring: !isEditable,\n                  child: GroupCardHeader(\n                    groupData: groupData,\n                  ),\n                );\n              },\n              footerBuilder: _buildFooter,\n              cardBuilder: (_, column, columnItem) => _buildCard(\n                context: context,\n                afGroupData: column,\n                afGroupItem: columnItem,\n                cardMargin: config.cardMargin,\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {\n    final isEditable =\n        context.read<PageAccessLevelBloc?>()?.state.isEditable ?? false;\n    final style = Theme.of(context);\n\n    return SizedBox(\n      height: 42,\n      width: double.infinity,\n      child: IgnorePointer(\n        ignoring: !isEditable,\n        child: TextButton.icon(\n          style: TextButton.styleFrom(\n            padding: const EdgeInsets.only(left: 8),\n            alignment: Alignment.centerLeft,\n          ),\n          icon: FlowySvg(\n            FlowySvgs.add_m,\n            color: style.colorScheme.onSurface,\n          ),\n          label: Text(\n            LocaleKeys.board_column_createNewCard.tr(),\n            style: style.textTheme.bodyMedium?.copyWith(\n              color: style.colorScheme.onSurface,\n            ),\n          ),\n          onPressed: () => context.read<BoardBloc>().add(\n                BoardEvent.createRow(\n                  columnData.id,\n                  OrderObjectPositionTypePB.End,\n                  null,\n                  null,\n                ),\n              ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildCard({\n    required BuildContext context,\n    required AppFlowyGroupData afGroupData,\n    required AppFlowyGroupItem afGroupItem,\n    required EdgeInsets cardMargin,\n  }) {\n    final boardBloc = context.read<BoardBloc>();\n    final groupItem = afGroupItem as GroupItem;\n    final groupData = afGroupData.customData as GroupData;\n    final rowMeta = groupItem.row;\n\n    final cellBuilder =\n        CardCellBuilder(databaseController: boardBloc.databaseController);\n\n    final groupItemId = groupItem.row.id + groupData.group.groupId;\n    final isLocked =\n        context.read<PageAccessLevelBloc?>()?.state.isLocked ?? false;\n\n    return Container(\n      key: ValueKey(groupItemId),\n      margin: cardMargin,\n      decoration: _makeBoxDecoration(context),\n      child: BlocProvider.value(\n        value: boardBloc,\n        child: IgnorePointer(\n          ignoring: isLocked,\n          child: RowCard(\n            fieldController: boardBloc.fieldController,\n            rowMeta: rowMeta,\n            viewId: boardBloc.viewId,\n            rowCache: boardBloc.rowCache,\n            groupingFieldId: groupItem.fieldInfo.id,\n            isEditing: false,\n            cellBuilder: cellBuilder,\n            onTap: (context) {\n              context.push(\n                MobileRowDetailPage.routeName,\n                extra: {\n                  MobileRowDetailPage.argRowId: rowMeta.id,\n                  MobileRowDetailPage.argDatabaseController:\n                      context.read<BoardBloc>().databaseController,\n                },\n              );\n            },\n            onStartEditing: () {},\n            onEndEditing: () {},\n            styleConfiguration: RowCardStyleConfiguration(\n              cellStyleMap: mobileBoardCardCellStyleMap(context),\n              showAccessory: false,\n            ),\n            userProfile: boardBloc.userProfile,\n          ),\n        ),\n      ),\n    );\n  }\n\n  BoxDecoration _makeBoxDecoration(BuildContext context) {\n    final themeMode = context.read<AppearanceSettingsCubit>().state.themeMode;\n    return BoxDecoration(\n      color: AFThemeExtension.of(context).background,\n      borderRadius: const BorderRadius.all(Radius.circular(8)),\n      border: themeMode == ThemeMode.light\n          ? Border.fromBorderSide(\n              BorderSide(\n                color: Theme.of(context)\n                    .colorScheme\n                    .outline\n                    .withValues(alpha: 0.5),\n              ),\n            )\n          : null,\n      boxShadow: themeMode == ThemeMode.light\n          ? [\n              BoxShadow(\n                color: Theme.of(context)\n                    .colorScheme\n                    .outline\n                    .withValues(alpha: 0.5),\n                blurRadius: 4,\n                offset: const Offset(0, 2),\n              ),\n            ]\n          : null,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_screen.dart",
    "content": "import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileBoardScreen extends StatelessWidget {\n  const MobileBoardScreen({\n    super.key,\n    required this.id,\n    this.title,\n  });\n\n  /// view id\n  final String id;\n  final String? title;\n\n  static const routeName = '/board';\n  static const viewId = 'id';\n  static const viewTitle = 'title';\n\n  @override\n  Widget build(BuildContext context) {\n    return MobileViewPage(\n      id: id,\n      title: title,\n      viewLayout: ViewLayoutPB.Document,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/group_card_header.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';\nimport 'package:appflowy/plugins/database/board/application/board_bloc.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_board/appflowy_board.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\n// similar to [BoardColumnHeader] in Desktop\nclass GroupCardHeader extends StatefulWidget {\n  const GroupCardHeader({\n    super.key,\n    required this.groupData,\n  });\n\n  final AppFlowyGroupData groupData;\n\n  @override\n  State<GroupCardHeader> createState() => _GroupCardHeaderState();\n}\n\nclass _GroupCardHeaderState extends State<GroupCardHeader> {\n  late final TextEditingController _controller =\n      TextEditingController.fromValue(\n    TextEditingValue(\n      selection: TextSelection.collapsed(\n        offset: widget.groupData.headerData.groupName.length,\n      ),\n      text: widget.groupData.headerData.groupName,\n    ),\n  );\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final boardCustomData = widget.groupData.customData as GroupData;\n    final titleTextStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(\n          fontWeight: FontWeight.w600,\n        );\n    return BlocBuilder<BoardBloc, BoardState>(\n      builder: (context, state) {\n        Widget title = Text(\n          widget.groupData.headerData.groupName,\n          style: titleTextStyle,\n          overflow: TextOverflow.ellipsis,\n        );\n\n        // header can be edited if it's not default group(no status) and the field type can be edited\n        if (!boardCustomData.group.isDefault &&\n            boardCustomData.fieldType.canEditHeader) {\n          title = GestureDetector(\n            onTap: () => context\n                .read<BoardBloc>()\n                .add(BoardEvent.startEditingHeader(widget.groupData.id)),\n            child: Text(\n              widget.groupData.headerData.groupName,\n              style: titleTextStyle,\n              overflow: TextOverflow.ellipsis,\n            ),\n          );\n        }\n\n        final isEditing = state.maybeMap(\n          ready: (value) => value.editingHeaderId == widget.groupData.id,\n          orElse: () => false,\n        );\n\n        if (isEditing) {\n          title = TextField(\n            controller: _controller,\n            autofocus: true,\n            onEditingComplete: () => context.read<BoardBloc>().add(\n                  BoardEvent.endEditingHeader(\n                    widget.groupData.id,\n                    _controller.text,\n                  ),\n                ),\n            style: titleTextStyle,\n            onTapOutside: (_) => context.read<BoardBloc>().add(\n                  // group header switch from TextField to Text\n                  // group name won't be changed\n                  BoardEvent.endEditingHeader(widget.groupData.id, null),\n                ),\n          );\n        }\n\n        return Padding(\n          padding: const EdgeInsets.only(left: 16),\n          child: SizedBox(\n            height: 42,\n            child: Row(\n              children: [\n                _buildHeaderIcon(boardCustomData),\n                Expanded(child: title),\n                IconButton(\n                  icon: Icon(\n                    Icons.more_horiz_rounded,\n                    color: Theme.of(context).colorScheme.onSurface,\n                  ),\n                  splashRadius: 5,\n                  onPressed: () => showMobileBottomSheet(\n                    context,\n                    showDragHandle: true,\n                    backgroundColor: Theme.of(context).colorScheme.surface,\n                    builder: (_) => Column(\n                      crossAxisAlignment: CrossAxisAlignment.stretch,\n                      children: [\n                        MobileQuickActionButton(\n                          text: LocaleKeys.board_column_renameColumn.tr(),\n                          icon: FlowySvgs.edit_s,\n                          onTap: () {\n                            context.read<BoardBloc>().add(\n                                  BoardEvent.startEditingHeader(\n                                    widget.groupData.id,\n                                  ),\n                                );\n                            context.pop();\n                          },\n                        ),\n                        const MobileQuickActionDivider(),\n                        MobileQuickActionButton(\n                          text: LocaleKeys.board_column_hideColumn.tr(),\n                          icon: FlowySvgs.hide_s,\n                          onTap: () {\n                            context.read<BoardBloc>().add(\n                                  BoardEvent.setGroupVisibility(\n                                    widget.groupData.customData.group\n                                        as GroupPB,\n                                    false,\n                                  ),\n                                );\n                            context.pop();\n                          },\n                        ),\n                      ],\n                    ),\n                  ),\n                ),\n                IconButton(\n                  icon: Icon(\n                    Icons.add,\n                    color: Theme.of(context).colorScheme.onSurface,\n                  ),\n                  splashRadius: 5,\n                  onPressed: () {\n                    context.read<BoardBloc>().add(\n                          BoardEvent.createRow(\n                            widget.groupData.id,\n                            OrderObjectPositionTypePB.Start,\n                            null,\n                            null,\n                          ),\n                        );\n                  },\n                ),\n              ],\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildHeaderIcon(GroupData customData) =>\n      switch (customData.fieldType) {\n        FieldType.Checkbox => FlowySvg(\n            customData.asCheckboxGroup()!.isCheck\n                ? FlowySvgs.check_filled_s\n                : FlowySvgs.uncheck_s,\n            blendMode: BlendMode.dst,\n          ),\n        _ => const SizedBox.shrink(),\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/board/application/board_bloc.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\n/// Add new group\nclass MobileBoardTrailing extends StatefulWidget {\n  const MobileBoardTrailing({super.key});\n\n  @override\n  State<MobileBoardTrailing> createState() => _MobileBoardTrailingState();\n}\n\nclass _MobileBoardTrailingState extends State<MobileBoardTrailing> {\n  final TextEditingController _textController = TextEditingController();\n\n  bool isEditing = false;\n\n  @override\n  void dispose() {\n    _textController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final screenSize = MediaQuery.of(context).size;\n    final style = Theme.of(context);\n\n    return Container(\n      margin: const EdgeInsets.symmetric(horizontal: 8),\n      child: SizedBox(\n        width: screenSize.width * 0.7,\n        child: isEditing\n            ? DecoratedBox(\n                decoration: BoxDecoration(\n                  color: style.colorScheme.secondary,\n                  borderRadius: BorderRadius.circular(8),\n                ),\n                child: Padding(\n                  padding:\n                      const EdgeInsets.symmetric(horizontal: 8, vertical: 4),\n                  child: Column(\n                    mainAxisSize: MainAxisSize.min,\n                    children: [\n                      TextField(\n                        controller: _textController,\n                        autofocus: true,\n                        onChanged: (_) => setState(() {}),\n                        decoration: InputDecoration(\n                          suffixIcon: AnimatedOpacity(\n                            duration: const Duration(milliseconds: 200),\n                            opacity: _textController.text.isNotEmpty ? 1 : 0,\n                            child: Material(\n                              color: Colors.transparent,\n                              shape: const CircleBorder(),\n                              clipBehavior: Clip.antiAlias,\n                              child: IconButton(\n                                icon: Icon(\n                                  Icons.close,\n                                  color: style.colorScheme.onSurface,\n                                ),\n                                onPressed: () =>\n                                    setState(() => _textController.clear()),\n                              ),\n                            ),\n                          ),\n                          isDense: true,\n                        ),\n                        onEditingComplete: () {\n                          context.read<BoardBloc>().add(\n                                BoardEvent.createGroup(\n                                  _textController.text,\n                                ),\n                              );\n                          _textController.clear();\n                          setState(() => isEditing = false);\n                        },\n                      ),\n                      Row(\n                        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                        children: [\n                          TextButton(\n                            child: Text(\n                              LocaleKeys.button_cancel.tr(),\n                              style: style.textTheme.titleSmall?.copyWith(\n                                color: style.colorScheme.onSurface,\n                              ),\n                            ),\n                            onPressed: () => setState(() => isEditing = false),\n                          ),\n                          TextButton(\n                            child: Text(\n                              LocaleKeys.button_add.tr(),\n                              style: style.textTheme.titleSmall?.copyWith(\n                                fontWeight: FontWeight.bold,\n                                color: style.colorScheme.onSurface,\n                              ),\n                            ),\n                            onPressed: () {\n                              context.read<BoardBloc>().add(\n                                    BoardEvent.createGroup(\n                                      _textController.text,\n                                    ),\n                                  );\n                              _textController.clear();\n                              setState(() => isEditing = false);\n                            },\n                          ),\n                        ],\n                      ),\n                    ],\n                  ),\n                ),\n              )\n            : ElevatedButton.icon(\n                style: ElevatedButton.styleFrom(\n                  foregroundColor: style.colorScheme.onSurface,\n                  backgroundColor: style.colorScheme.secondary,\n                  shape: RoundedRectangleBorder(\n                    borderRadius: BorderRadius.circular(8),\n                  ),\n                ).copyWith(\n                  overlayColor:\n                      WidgetStateProperty.all(Theme.of(context).hoverColor),\n                ),\n                icon: const Icon(Icons.add),\n                label: Text(\n                  LocaleKeys.board_column_newGroup.tr(),\n                  style: style.textTheme.bodyMedium!.copyWith(\n                    fontWeight: FontWeight.w600,\n                  ),\n                ),\n                onPressed: () => setState(() => isEditing = true),\n              ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/database/card/card.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/board/application/board_bloc.dart';\nimport 'package:appflowy/plugins/database/board/group_ext.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nclass MobileHiddenGroupsColumn extends StatelessWidget {\n  const MobileHiddenGroupsColumn({super.key, required this.padding});\n\n  final EdgeInsets padding;\n\n  @override\n  Widget build(BuildContext context) {\n    final databaseController = context.read<BoardBloc>().databaseController;\n    return BlocSelector<BoardBloc, BoardState, BoardLayoutSettingPB?>(\n      selector: (state) => state.maybeMap(\n        orElse: () => null,\n        ready: (value) => value.layoutSettings,\n      ),\n      builder: (context, layoutSettings) {\n        if (layoutSettings == null) {\n          return const SizedBox.shrink();\n        }\n        final isCollapsed = layoutSettings.collapseHiddenGroups;\n        return Container(\n          padding: padding,\n          child: AnimatedSize(\n            alignment: AlignmentDirectional.topStart,\n            curve: Curves.easeOut,\n            duration: const Duration(milliseconds: 150),\n            child: isCollapsed\n                ? SizedBox(\n                    height: 50,\n                    child: _collapseExpandIcon(context, isCollapsed),\n                  )\n                : SizedBox(\n                    width: 180,\n                    child: Column(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      children: [\n                        Row(\n                          children: [\n                            const Spacer(),\n                            _collapseExpandIcon(context, isCollapsed),\n                          ],\n                        ),\n                        Text(\n                          LocaleKeys.board_hiddenGroupSection_sectionTitle.tr(),\n                          style: Theme.of(context)\n                              .textTheme\n                              .bodyMedium\n                              ?.copyWith(\n                                color: Theme.of(context).colorScheme.tertiary,\n                              ),\n                        ),\n                        const VSpace(8),\n                        Expanded(\n                          child: MobileHiddenGroupList(\n                            databaseController: databaseController,\n                          ),\n                        ),\n                      ],\n                    ),\n                  ),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _collapseExpandIcon(BuildContext context, bool isCollapsed) {\n    return CircleAvatar(\n      radius: 20,\n      backgroundColor: Theme.of(context).colorScheme.secondary,\n      child: IconButton(\n        icon: FlowySvg(\n          isCollapsed\n              ? FlowySvgs.hamburger_s_s\n              : FlowySvgs.pull_left_outlined_s,\n          size: isCollapsed ? const Size.square(12) : const Size.square(40),\n        ),\n        onPressed: () => context\n            .read<BoardBloc>()\n            .add(BoardEvent.toggleHiddenSectionVisibility(!isCollapsed)),\n      ),\n    );\n  }\n}\n\nclass MobileHiddenGroupList extends StatelessWidget {\n  const MobileHiddenGroupList({\n    super.key,\n    required this.databaseController,\n  });\n\n  final DatabaseController databaseController;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<BoardBloc, BoardState>(\n      builder: (_, state) {\n        return state.maybeMap(\n          orElse: () => const SizedBox.shrink(),\n          ready: (state) {\n            return ReorderableListView.builder(\n              itemCount: state.hiddenGroups.length,\n              itemBuilder: (_, index) => MobileHiddenGroup(\n                key: ValueKey(state.hiddenGroups[index].groupId),\n                group: state.hiddenGroups[index],\n                index: index,\n              ),\n              proxyDecorator: (child, index, animation) => BlocProvider.value(\n                value: context.read<BoardBloc>(),\n                child: Material(color: Colors.transparent, child: child),\n              ),\n              physics: const ClampingScrollPhysics(),\n              onReorder: (oldIndex, newIndex) {\n                if (oldIndex < newIndex) {\n                  newIndex--;\n                }\n                final fromGroupId = state.hiddenGroups[oldIndex].groupId;\n                final toGroupId = state.hiddenGroups[newIndex].groupId;\n                context\n                    .read<BoardBloc>()\n                    .add(BoardEvent.reorderGroup(fromGroupId, toGroupId));\n              },\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass MobileHiddenGroup extends StatelessWidget {\n  const MobileHiddenGroup({\n    super.key,\n    required this.group,\n    required this.index,\n  });\n\n  final GroupPB group;\n  final int index;\n\n  @override\n  Widget build(BuildContext context) {\n    final databaseController = context.read<BoardBloc>().databaseController;\n    final primaryField = databaseController.fieldController.fieldInfos\n        .firstWhereOrNull((element) => element.isPrimary)!;\n\n    final cells = group.rows.map(\n      (item) {\n        final cellContext =\n            databaseController.rowCache.loadCells(item).firstWhere(\n                  (cellContext) => cellContext.fieldId == primaryField.id,\n                );\n\n        return TextButton(\n          style: TextButton.styleFrom(\n            textStyle: Theme.of(context).textTheme.bodyMedium,\n            foregroundColor: AFThemeExtension.of(context).onBackground,\n            visualDensity: VisualDensity.compact,\n          ),\n          child: CardCellBuilder(\n            databaseController: context.read<BoardBloc>().databaseController,\n          ).build(\n            cellContext: cellContext,\n            styleMap: {FieldType.RichText: _titleCellStyle(context)},\n            hasNotes: !item.isDocumentEmpty,\n          ),\n          onPressed: () {\n            context.push(\n              MobileRowDetailPage.routeName,\n              extra: {\n                MobileRowDetailPage.argRowId: item.id,\n                MobileRowDetailPage.argDatabaseController:\n                    context.read<BoardBloc>().databaseController,\n              },\n            );\n          },\n        );\n      },\n    ).toList();\n\n    return ExpansionTile(\n      tilePadding: EdgeInsets.zero,\n      childrenPadding: EdgeInsets.zero,\n      title: Row(\n        children: [\n          Expanded(\n            child: Text(\n              group.generateGroupName(databaseController),\n              style: Theme.of(context).textTheme.bodyMedium,\n              maxLines: 2,\n              overflow: TextOverflow.ellipsis,\n            ),\n          ),\n          GestureDetector(\n            child: const Padding(\n              padding: EdgeInsets.all(4),\n              child: FlowySvg(\n                FlowySvgs.hide_m,\n                size: Size.square(20),\n              ),\n            ),\n            onTap: () => showFlowyMobileConfirmDialog(\n              context,\n              title: FlowyText(LocaleKeys.board_mobile_showGroup.tr()),\n              content: FlowyText(\n                LocaleKeys.board_mobile_showGroupContent.tr(),\n              ),\n              actionButtonTitle: LocaleKeys.button_yes.tr(),\n              actionButtonColor: Theme.of(context).colorScheme.primary,\n              onActionButtonPressed: () => context\n                  .read<BoardBloc>()\n                  .add(BoardEvent.setGroupVisibility(group, true)),\n            ),\n          ),\n        ],\n      ),\n      children: cells,\n    );\n  }\n\n  TextCardCellStyle _titleCellStyle(BuildContext context) {\n    return TextCardCellStyle(\n      padding: EdgeInsets.zero,\n      textStyle: Theme.of(context).textTheme.bodyMedium!,\n      maxLines: 2,\n      titleTextStyle: Theme.of(context)\n          .textTheme\n          .bodyMedium!\n          .copyWith(fontSize: 11, overflow: TextOverflow.ellipsis),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/widgets.dart",
    "content": "export 'group_card_header.dart';\nexport 'mobile_board_trailing.dart';\nexport 'mobile_hidden_groups_column.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/card/card.dart",
    "content": "export 'card_detail/mobile_card_detail_screen.dart';\nexport 'mobile_card_content.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_banner_bloc.dart';\nimport 'package:appflowy/plugins/database/application/row/row_cache.dart';\nimport 'package:appflowy/plugins/database/application/row/row_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy/plugins/database/grid/application/row/mobile_row_detail_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_property.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';\nimport 'package:appflowy/shared/af_image.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:fluttertoast/fluttertoast.dart';\nimport 'package:go_router/go_router.dart';\n\nimport 'widgets/mobile_create_field_button.dart';\nimport 'widgets/mobile_row_property_list.dart';\n\nclass MobileRowDetailPage extends StatefulWidget {\n  const MobileRowDetailPage({\n    super.key,\n    required this.databaseController,\n    required this.rowId,\n  });\n\n  static const routeName = '/MobileRowDetailPage';\n  static const argDatabaseController = 'databaseController';\n  static const argRowId = 'rowId';\n\n  final DatabaseController databaseController;\n  final String rowId;\n\n  @override\n  State<MobileRowDetailPage> createState() => _MobileRowDetailPageState();\n}\n\nclass _MobileRowDetailPageState extends State<MobileRowDetailPage> {\n  late final MobileRowDetailBloc _bloc;\n  late final PageController _pageController;\n\n  String get viewId => widget.databaseController.viewId;\n\n  RowCache get rowCache => widget.databaseController.rowCache;\n\n  FieldController get fieldController =>\n      widget.databaseController.fieldController;\n\n  @override\n  void initState() {\n    super.initState();\n    _bloc = MobileRowDetailBloc(\n      databaseController: widget.databaseController,\n    )..add(MobileRowDetailEvent.initial(widget.rowId));\n    final initialPage = rowCache.rowInfos\n        .indexWhere((rowInfo) => rowInfo.rowId == widget.rowId);\n    _pageController =\n        PageController(initialPage: initialPage == -1 ? 0 : initialPage);\n  }\n\n  @override\n  void dispose() {\n    _bloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: _bloc,\n      child: Scaffold(\n        appBar: FlowyAppBar(\n          leadingType: FlowyAppBarLeadingType.close,\n          showDivider: false,\n          actions: [\n            AppBarMoreButton(\n              onTap: (_) => _showCardActions(context),\n            ),\n          ],\n        ),\n        body: BlocBuilder<MobileRowDetailBloc, MobileRowDetailState>(\n          buildWhen: (previous, current) =>\n              previous.rowInfos.length != current.rowInfos.length,\n          builder: (context, state) {\n            if (state.isLoading) {\n              return const SizedBox.shrink();\n            }\n            return PageView.builder(\n              controller: _pageController,\n              onPageChanged: (page) {\n                final rowId = _bloc.state.rowInfos[page].rowId;\n                _bloc.add(MobileRowDetailEvent.changeRowId(rowId));\n              },\n              itemCount: state.rowInfos.length,\n              itemBuilder: (context, index) {\n                if (state.rowInfos.isEmpty || state.currentRowId == null) {\n                  return const SizedBox.shrink();\n                }\n                return MobileRowDetailPageContent(\n                  databaseController: widget.databaseController,\n                  rowMeta: state.rowInfos[index].rowMeta,\n                );\n              },\n            );\n          },\n        ),\n        floatingActionButton: RowDetailFab(\n          onTapPrevious: () => _pageController.previousPage(\n            duration: const Duration(milliseconds: 300),\n            curve: Curves.ease,\n          ),\n          onTapNext: () => _pageController.nextPage(\n            duration: const Duration(milliseconds: 300),\n            curve: Curves.ease,\n          ),\n        ),\n      ),\n    );\n  }\n\n  void _showCardActions(BuildContext context) {\n    showMobileBottomSheet(\n      context,\n      backgroundColor: AFThemeExtension.of(context).background,\n      showDragHandle: true,\n      builder: (_) => Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          MobileQuickActionButton(\n            onTap: () =>\n                _performAction(viewId, _bloc.state.currentRowId, false),\n            icon: FlowySvgs.duplicate_s,\n            text: LocaleKeys.button_duplicate.tr(),\n          ),\n          const MobileQuickActionDivider(),\n          MobileQuickActionButton(\n            onTap: () => showMobileBottomSheet(\n              context,\n              title: LocaleKeys.grid_media_addFileMobile.tr(),\n              showHeader: true,\n              showCloseButton: true,\n              showDragHandle: true,\n              builder: (dialogContext) => Container(\n                margin: const EdgeInsets.only(top: 12),\n                constraints: const BoxConstraints(\n                  maxHeight: 340,\n                  minHeight: 80,\n                ),\n                child: FileUploadMenu(\n                  onInsertLocalFile: (files) async {\n                    context\n                      ..pop()\n                      ..pop();\n\n                    if (_bloc.state.currentRowId == null) {\n                      return;\n                    }\n\n                    await insertLocalFiles(\n                      context,\n                      files,\n                      userProfile: _bloc.userProfile,\n                      documentId: _bloc.state.currentRowId!,\n                      onUploadSuccess: (file, path, isLocalMode) {\n                        _bloc.add(\n                          MobileRowDetailEvent.addCover(\n                            RowCoverPB(\n                              data: path,\n                              uploadType: isLocalMode\n                                  ? FileUploadTypePB.LocalFile\n                                  : FileUploadTypePB.CloudFile,\n                              coverType: CoverTypePB.FileCover,\n                            ),\n                          ),\n                        );\n                      },\n                    );\n                  },\n                  onInsertNetworkFile: (url) async =>\n                      _onInsertNetworkFile(url, context),\n                ),\n              ),\n            ),\n            icon: FlowySvgs.add_cover_s,\n            text: 'Add cover',\n          ),\n          const MobileQuickActionDivider(),\n          MobileQuickActionButton(\n            onTap: () => _performAction(viewId, _bloc.state.currentRowId, true),\n            text: LocaleKeys.button_delete.tr(),\n            textColor: Theme.of(context).colorScheme.error,\n            icon: FlowySvgs.trash_s,\n            iconColor: Theme.of(context).colorScheme.error,\n          ),\n        ],\n      ),\n    );\n  }\n\n  void _performAction(String viewId, String? rowId, bool deleteRow) {\n    if (rowId == null) {\n      return;\n    }\n\n    deleteRow\n        ? RowBackendService.deleteRows(viewId, [rowId])\n        : RowBackendService.duplicateRow(viewId, rowId);\n\n    context\n      ..pop()\n      ..pop();\n    Fluttertoast.showToast(\n      msg: deleteRow\n          ? LocaleKeys.board_cardDeleted.tr()\n          : LocaleKeys.board_cardDuplicated.tr(),\n      gravity: ToastGravity.BOTTOM,\n    );\n  }\n\n  Future<void> _onInsertNetworkFile(\n    String url,\n    BuildContext context,\n  ) async {\n    context\n      ..pop()\n      ..pop();\n\n    if (url.isEmpty) return;\n    final uri = Uri.tryParse(url);\n    if (uri == null) {\n      return;\n    }\n\n    String name = uri.pathSegments.isNotEmpty ? uri.pathSegments.last : \"\";\n    if (name.isEmpty && uri.pathSegments.length > 1) {\n      name = uri.pathSegments[uri.pathSegments.length - 2];\n    } else if (name.isEmpty) {\n      name = uri.host;\n    }\n\n    _bloc.add(\n      MobileRowDetailEvent.addCover(\n        RowCoverPB(\n          data: url,\n          uploadType: FileUploadTypePB.NetworkFile,\n          coverType: CoverTypePB.FileCover,\n        ),\n      ),\n    );\n  }\n}\n\nclass RowDetailFab extends StatelessWidget {\n  const RowDetailFab({\n    super.key,\n    required this.onTapPrevious,\n    required this.onTapNext,\n  });\n\n  final VoidCallback onTapPrevious;\n  final VoidCallback onTapNext;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<MobileRowDetailBloc, MobileRowDetailState>(\n      builder: (context, state) {\n        final rowCount = state.rowInfos.length;\n        final rowIndex = state.rowInfos.indexWhere(\n          (rowInfo) => rowInfo.rowId == state.currentRowId,\n        );\n        if (rowIndex == -1 || rowCount == 0) {\n          return const SizedBox.shrink();\n        }\n\n        final previousDisabled = rowIndex == 0;\n        final nextDisabled = rowIndex == rowCount - 1;\n\n        return IntrinsicWidth(\n          child: Container(\n            height: 48,\n            decoration: BoxDecoration(\n              color: Theme.of(context).colorScheme.surface,\n              borderRadius: BorderRadius.circular(26),\n              boxShadow: const [\n                BoxShadow(\n                  offset: Offset(0, 8),\n                  blurRadius: 20,\n                  color: Color(0x191F2329),\n                ),\n              ],\n            ),\n            child: Row(\n              mainAxisSize: MainAxisSize.min,\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                SizedBox.square(\n                  dimension: 48,\n                  child: Material(\n                    color: Theme.of(context).colorScheme.surface,\n                    borderRadius: BorderRadius.circular(26),\n                    borderOnForeground: false,\n                    child: previousDisabled\n                        ? Icon(\n                            Icons.chevron_left_outlined,\n                            color: Theme.of(context).disabledColor,\n                          )\n                        : InkWell(\n                            borderRadius: BorderRadius.circular(26),\n                            onTap: onTapPrevious,\n                            child: const Icon(Icons.chevron_left_outlined),\n                          ),\n                  ),\n                ),\n                FlowyText.medium(\n                  \"${rowIndex + 1} / $rowCount\",\n                  fontSize: 14,\n                ),\n                SizedBox.square(\n                  dimension: 48,\n                  child: Material(\n                    color: Theme.of(context).colorScheme.surface,\n                    borderRadius: BorderRadius.circular(26),\n                    borderOnForeground: false,\n                    child: nextDisabled\n                        ? Icon(\n                            Icons.chevron_right_outlined,\n                            color: Theme.of(context).disabledColor,\n                          )\n                        : InkWell(\n                            borderRadius: BorderRadius.circular(26),\n                            onTap: onTapNext,\n                            child: const Icon(Icons.chevron_right_outlined),\n                          ),\n                  ),\n                ),\n              ],\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass MobileRowDetailPageContent extends StatefulWidget {\n  const MobileRowDetailPageContent({\n    super.key,\n    required this.databaseController,\n    required this.rowMeta,\n  });\n\n  final DatabaseController databaseController;\n  final RowMetaPB rowMeta;\n\n  @override\n  State<MobileRowDetailPageContent> createState() =>\n      MobileRowDetailPageContentState();\n}\n\nclass MobileRowDetailPageContentState\n    extends State<MobileRowDetailPageContent> {\n  late final RowController rowController;\n  late final EditableCellBuilder cellBuilder;\n\n  String get viewId => widget.databaseController.viewId;\n\n  RowCache get rowCache => widget.databaseController.rowCache;\n\n  FieldController get fieldController =>\n      widget.databaseController.fieldController;\n  ValueNotifier<String> primaryFieldId = ValueNotifier('');\n\n  @override\n  void initState() {\n    super.initState();\n\n    rowController = RowController(\n      rowMeta: widget.rowMeta,\n      viewId: viewId,\n      rowCache: rowCache,\n    );\n    rowController.initialize();\n\n    cellBuilder = EditableCellBuilder(\n      databaseController: widget.databaseController,\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<RowDetailBloc>(\n      create: (_) => RowDetailBloc(\n        fieldController: fieldController,\n        rowController: rowController,\n      ),\n      child: BlocBuilder<RowDetailBloc, RowDetailState>(\n        builder: (context, rowDetailState) => Column(\n          children: [\n            if (rowDetailState.rowMeta.cover.data.isNotEmpty) ...[\n              GestureDetector(\n                onTap: () => showMobileBottomSheet(\n                  context,\n                  backgroundColor: AFThemeExtension.of(context).background,\n                  showDragHandle: true,\n                  builder: (_) => Column(\n                    mainAxisSize: MainAxisSize.min,\n                    children: [\n                      MobileQuickActionButton(\n                        onTap: () {\n                          context\n                            ..pop()\n                            ..read<RowDetailBloc>()\n                                .add(const RowDetailEvent.removeCover());\n                        },\n                        text: LocaleKeys.button_delete.tr(),\n                        textColor: Theme.of(context).colorScheme.error,\n                        icon: FlowySvgs.trash_s,\n                        iconColor: Theme.of(context).colorScheme.error,\n                      ),\n                    ],\n                  ),\n                ),\n                child: SizedBox(\n                  height: 200,\n                  width: double.infinity,\n                  child: Container(\n                    clipBehavior: Clip.antiAlias,\n                    decoration: BoxDecoration(\n                      color: Theme.of(context).colorScheme.surface,\n                    ),\n                    child: AFImage(\n                      url: rowDetailState.rowMeta.cover.data,\n                      uploadType: widget.rowMeta.cover.uploadType,\n                      userProfile:\n                          context.read<MobileRowDetailBloc>().userProfile,\n                    ),\n                  ),\n                ),\n              ),\n            ],\n            BlocProvider<RowBannerBloc>(\n              create: (context) => RowBannerBloc(\n                viewId: viewId,\n                fieldController: fieldController,\n                rowMeta: rowController.rowMeta,\n              )..add(const RowBannerEvent.initial()),\n              child: BlocConsumer<RowBannerBloc, RowBannerState>(\n                listener: (context, state) {\n                  if (state.primaryField == null) {\n                    return;\n                  }\n                  primaryFieldId.value = state.primaryField!.id;\n                },\n                builder: (context, state) {\n                  if (state.primaryField == null) {\n                    return const SizedBox.shrink();\n                  }\n\n                  return Padding(\n                    padding: const EdgeInsets.symmetric(horizontal: 16),\n                    child: cellBuilder.buildCustom(\n                      CellContext(\n                        rowId: rowController.rowId,\n                        fieldId: state.primaryField!.id,\n                      ),\n                      skinMap: EditableCellSkinMap(textSkin: _TitleSkin()),\n                    ),\n                  );\n                },\n              ),\n            ),\n            Expanded(\n              child: ListView(\n                padding: const EdgeInsets.only(top: 9, bottom: 100),\n                children: [\n                  Padding(\n                    padding: const EdgeInsets.symmetric(horizontal: 16),\n                    child: MobileRowPropertyList(\n                      databaseController: widget.databaseController,\n                      cellBuilder: cellBuilder,\n                    ),\n                  ),\n                  Padding(\n                    padding: const EdgeInsets.fromLTRB(6, 6, 16, 0),\n                    child: Column(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      children: [\n                        if (rowDetailState.numHiddenFields != 0) ...[\n                          const ToggleHiddenFieldsVisibilityButton(),\n                        ],\n                        const VSpace(8.0),\n                        ValueListenableBuilder(\n                          valueListenable: primaryFieldId,\n                          builder: (context, primaryFieldId, child) {\n                            if (primaryFieldId.isEmpty) {\n                              return const SizedBox.shrink();\n                            }\n                            return OpenRowPageButton(\n                              databaseController: widget.databaseController,\n                              cellContext: CellContext(\n                                rowId: rowController.rowId,\n                                fieldId: primaryFieldId,\n                              ),\n                              documentId: rowController.rowMeta.documentId,\n                            );\n                          },\n                        ),\n                        MobileRowDetailCreateFieldButton(\n                          viewId: viewId,\n                          fieldController: fieldController,\n                        ),\n                      ],\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _TitleSkin extends IEditableTextCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TextCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return TextField(\n      controller: textEditingController,\n      focusNode: focusNode,\n      maxLines: null,\n      style: Theme.of(context).textTheme.bodyMedium?.copyWith(\n            fontSize: 23,\n            fontWeight: FontWeight.w500,\n          ),\n      onEditingComplete: () {\n        bloc.add(TextCellEvent.updateText(textEditingController.text));\n      },\n      decoration: InputDecoration(\n        contentPadding: const EdgeInsets.symmetric(vertical: 9),\n        border: InputBorder.none,\n        focusedBorder: InputBorder.none,\n        enabledBorder: InputBorder.none,\n        errorBorder: InputBorder.none,\n        disabledBorder: InputBorder.none,\n        hintText: LocaleKeys.grid_row_titlePlaceholder.tr(),\n        isDense: true,\n        isCollapsed: true,\n      ),\n      onTapOutside: (event) => focusNode.unfocus(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileRowDetailCreateFieldButton extends StatelessWidget {\n  const MobileRowDetailCreateFieldButton({\n    super.key,\n    required this.viewId,\n    required this.fieldController,\n  });\n\n  final String viewId;\n  final FieldController fieldController;\n\n  @override\n  Widget build(BuildContext context) {\n    return ConstrainedBox(\n      constraints: BoxConstraints(\n        minWidth: double.infinity,\n        maxHeight: GridSize.headerHeight,\n      ),\n      child: TextButton.icon(\n        style: Theme.of(context).textButtonTheme.style?.copyWith(\n              shape: WidgetStateProperty.all<RoundedRectangleBorder>(\n                RoundedRectangleBorder(\n                  borderRadius: BorderRadius.circular(12.0),\n                ),\n              ),\n              overlayColor: WidgetStateProperty.all<Color>(\n                Theme.of(context).hoverColor,\n              ),\n              alignment: AlignmentDirectional.centerStart,\n              splashFactory: NoSplash.splashFactory,\n              padding: const WidgetStatePropertyAll(\n                EdgeInsets.symmetric(horizontal: 6, vertical: 2),\n              ),\n            ),\n        label: FlowyText.medium(\n          LocaleKeys.grid_field_newProperty.tr(),\n          fontSize: 15,\n        ),\n        onPressed: () => mobileCreateFieldWorkflow(context, viewId),\n        icon: const FlowySvg(FlowySvgs.add_m),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MobileRowPropertyList extends StatelessWidget {\n  const MobileRowPropertyList({\n    super.key,\n    required this.databaseController,\n    required this.cellBuilder,\n  });\n\n  final DatabaseController databaseController;\n  final EditableCellBuilder cellBuilder;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<RowDetailBloc, RowDetailState>(\n      builder: (context, state) {\n        final List<CellContext> visibleCells =\n            state.visibleCells.where((cell) => !_isCellPrimary(cell)).toList();\n\n        return ListView.separated(\n          shrinkWrap: true,\n          physics: const NeverScrollableScrollPhysics(),\n          itemCount: visibleCells.length,\n          padding: EdgeInsets.zero,\n          itemBuilder: (context, index) => _PropertyCell(\n            key: ValueKey('row_detail_${visibleCells[index].fieldId}'),\n            cellContext: visibleCells[index],\n            fieldController: databaseController.fieldController,\n            cellBuilder: cellBuilder,\n          ),\n          separatorBuilder: (_, __) => const VSpace(22),\n        );\n      },\n    );\n  }\n\n  bool _isCellPrimary(CellContext cell) =>\n      databaseController.fieldController.getField(cell.fieldId)!.isPrimary;\n}\n\nclass _PropertyCell extends StatefulWidget {\n  const _PropertyCell({\n    super.key,\n    required this.cellContext,\n    required this.fieldController,\n    required this.cellBuilder,\n  });\n\n  final CellContext cellContext;\n  final FieldController fieldController;\n  final EditableCellBuilder cellBuilder;\n\n  @override\n  State<StatefulWidget> createState() => _PropertyCellState();\n}\n\nclass _PropertyCellState extends State<_PropertyCell> {\n  @override\n  Widget build(BuildContext context) {\n    final fieldInfo =\n        widget.fieldController.getField(widget.cellContext.fieldId)!;\n    final cell = widget.cellBuilder\n        .buildStyled(widget.cellContext, EditableCellStyle.mobileRowDetail);\n\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        Row(\n          children: [\n            FieldIcon(\n              fieldInfo: fieldInfo,\n            ),\n            const HSpace(6),\n            Expanded(\n              child: FlowyText.regular(\n                fieldInfo.name,\n                overflow: TextOverflow.ellipsis,\n                fontSize: 14,\n                figmaLineHeight: 16.0,\n                color: Theme.of(context).hintColor,\n              ),\n            ),\n          ],\n        ),\n        const VSpace(6),\n        cell,\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/option_text_field.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nclass OptionTextField extends StatelessWidget {\n  const OptionTextField({\n    super.key,\n    required this.controller,\n    this.autoFocus = false,\n    required this.isPrimary,\n    required this.fieldType,\n    required this.onTextChanged,\n    required this.onFieldTypeChanged,\n  });\n\n  final TextEditingController controller;\n  final bool autoFocus;\n  final bool isPrimary;\n  final FieldType fieldType;\n  final void Function(String value) onTextChanged;\n  final void Function(FieldType value) onFieldTypeChanged;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyOptionTile.textField(\n      controller: controller,\n      autofocus: autoFocus,\n      textFieldPadding: const EdgeInsets.symmetric(horizontal: 12.0),\n      onTextChanged: onTextChanged,\n      leftIcon: GestureDetector(\n        onTap: () async {\n          if (isPrimary) {\n            return;\n          }\n          final fieldType = await showFieldTypeGridBottomSheet(\n            context,\n            title: LocaleKeys.grid_field_editProperty.tr(),\n          );\n          if (fieldType != null) {\n            onFieldTypeChanged(fieldType);\n          }\n        },\n        child: Container(\n          height: 38,\n          width: 38,\n          decoration: BoxDecoration(\n            borderRadius: BorderRadius.circular(12),\n            color: Theme.of(context).brightness == Brightness.light\n                ? fieldType.mobileIconBackgroundColor\n                : fieldType.mobileIconBackgroundColorDark,\n          ),\n          child: Center(\n            child: FlowySvg(\n              fieldType.svgData,\n              size: const Size.square(22),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass OpenRowPageButton extends StatefulWidget {\n  const OpenRowPageButton({\n    super.key,\n    required this.documentId,\n    required this.databaseController,\n    required this.cellContext,\n  });\n\n  final String documentId;\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n\n  @override\n  State<OpenRowPageButton> createState() => _OpenRowPageButtonState();\n}\n\nclass _OpenRowPageButtonState extends State<OpenRowPageButton> {\n  late final cellBloc = TextCellBloc(\n    cellController: makeCellController(\n      widget.databaseController,\n      widget.cellContext,\n    ).as(),\n  );\n\n  ViewPB? view;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _preloadView(context, createDocumentIfMissed: true);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TextCellBloc, TextCellState>(\n      bloc: cellBloc,\n      builder: (context, state) {\n        return ConstrainedBox(\n          constraints: BoxConstraints(\n            minWidth: double.infinity,\n            maxHeight: GridSize.buttonHeight,\n          ),\n          child: TextButton.icon(\n            style: Theme.of(context).textButtonTheme.style?.copyWith(\n                  shape: WidgetStateProperty.all<RoundedRectangleBorder>(\n                    RoundedRectangleBorder(\n                      borderRadius: BorderRadius.circular(12.0),\n                    ),\n                  ),\n                  overlayColor: WidgetStateProperty.all<Color>(\n                    Theme.of(context).hoverColor,\n                  ),\n                  alignment: AlignmentDirectional.centerStart,\n                  splashFactory: NoSplash.splashFactory,\n                  padding: const WidgetStatePropertyAll(\n                    EdgeInsets.symmetric(horizontal: 6),\n                  ),\n                ),\n            label: FlowyText.medium(\n              LocaleKeys.grid_field_openRowDocument.tr(),\n              fontSize: 15,\n            ),\n            icon: const Padding(\n              padding: EdgeInsets.all(4.0),\n              child: FlowySvg(\n                FlowySvgs.full_view_s,\n                size: Size.square(16.0),\n              ),\n            ),\n            onPressed: () {\n              final name = state.content;\n              _openRowPage(context, name ?? \"\");\n            },\n          ),\n        );\n      },\n    );\n  }\n\n  Future<void> _openRowPage(BuildContext context, String fieldName) async {\n    Log.info('Open row page(${widget.documentId})');\n\n    if (view == null) {\n      showToastNotification(message: 'Failed to open row page');\n      // reload the view again\n      unawaited(_preloadView(context));\n      Log.error('Failed to open row page(${widget.documentId})');\n      return;\n    }\n\n    if (context.mounted) {\n      // the document in row is an orphan document, so we don't add it to recent\n      await context.pushView(\n        view!,\n        addInRecent: false,\n        showMoreButton: false,\n        fixedTitle: fieldName,\n        tabs: [PickerTabType.emoji.name],\n      );\n    }\n  }\n\n  // preload view to reduce the time to open the view\n  Future<void> _preloadView(\n    BuildContext context, {\n    bool createDocumentIfMissed = false,\n  }) async {\n    Log.info('Preload row page(${widget.documentId})');\n    final result = await ViewBackendService.getView(widget.documentId);\n    view = result.fold((s) => s, (f) => null);\n\n    if (view == null && createDocumentIfMissed) {\n      // create view if not exists\n      Log.info('Create row page(${widget.documentId})');\n      final result = await ViewBackendService.createOrphanView(\n        name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),\n        viewId: widget.documentId,\n        layoutType: ViewLayoutPB.Document,\n      );\n      view = result.fold((s) => s, (f) => null);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/widgets.dart",
    "content": "export 'mobile_create_field_button.dart';\nexport 'mobile_row_property_list.dart';\nexport 'option_text_field.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/card/mobile_card_content.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/plugins/database/widgets/card/card.dart';\nimport 'package:appflowy/plugins/database/widgets/card/card_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\n\nclass MobileCardContent extends StatelessWidget {\n  const MobileCardContent({\n    super.key,\n    required this.rowMeta,\n    required this.cellBuilder,\n    required this.cells,\n    required this.styleConfiguration,\n    required this.userProfile,\n  });\n\n  final RowMetaPB rowMeta;\n  final CardCellBuilder cellBuilder;\n  final List<CellMeta> cells;\n  final RowCardStyleConfiguration styleConfiguration;\n  final UserProfilePB? userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        if (rowMeta.cover.data.isNotEmpty) ...[\n          CardCover(cover: rowMeta.cover, userProfile: userProfile),\n        ],\n        Padding(\n          padding: styleConfiguration.cardPadding,\n          child: Column(\n            children: [\n              ...cells.map(\n                (cellMeta) => cellBuilder.build(\n                  cellContext: cellMeta.cellContext(),\n                  styleMap: mobileBoardCardCellStyleMap(context),\n                  hasNotes: !rowMeta.isDocumentEmpty,\n                ),\n              ),\n            ],\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/plugins/base/drag_handler.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/mobile_date_picker.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_header.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MobileDateCellEditScreen extends StatefulWidget {\n  const MobileDateCellEditScreen({\n    super.key,\n    required this.controller,\n    this.showAsFullScreen = true,\n  });\n\n  final DateCellController controller;\n  final bool showAsFullScreen;\n\n  static const routeName = '/edit_date_cell';\n\n  // the type is DateCellController\n  static const dateCellController = 'date_cell_controller';\n\n  // bool value, default is true\n  static const fullScreen = 'full_screen';\n\n  @override\n  State<MobileDateCellEditScreen> createState() =>\n      _MobileDateCellEditScreenState();\n}\n\nclass _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {\n  @override\n  Widget build(BuildContext context) =>\n      widget.showAsFullScreen ? _buildFullScreen() : _buildNotFullScreen();\n\n  Widget _buildFullScreen() {\n    return Scaffold(\n      appBar: FlowyAppBar(titleText: LocaleKeys.titleBar_date.tr()),\n      body: _buildDatePicker(),\n    );\n  }\n\n  Widget _buildNotFullScreen() {\n    return DraggableScrollableSheet(\n      expand: false,\n      snap: true,\n      initialChildSize: 0.7,\n      minChildSize: 0.4,\n      snapSizes: const [0.4, 0.7, 1.0],\n      builder: (_, controller) => Material(\n        color: Colors.transparent,\n        child: ListView(\n          controller: controller,\n          children: [\n            ColoredBox(\n              color: Theme.of(context).colorScheme.surface,\n              child: const Center(child: DragHandle()),\n            ),\n            const MobileDateHeader(),\n            _buildDatePicker(),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildDatePicker() {\n    return BlocProvider(\n      create: (_) => DateCellEditorBloc(\n        reminderBloc: getIt<ReminderBloc>(),\n        cellController: widget.controller,\n      ),\n      child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(\n        builder: (context, state) {\n          final dateCellBloc = context.read<DateCellEditorBloc>();\n          return MobileAppFlowyDatePicker(\n            dateTime: state.dateTime,\n            endDateTime: state.endDateTime,\n            isRange: state.isRange,\n            includeTime: state.includeTime,\n            dateFormat: state.dateTypeOptionPB.dateFormat,\n            timeFormat: state.dateTypeOptionPB.timeFormat,\n            reminderOption: state.reminderOption,\n            onDaySelected: (selectedDay) {\n              dateCellBloc.add(DateCellEditorEvent.updateDateTime(selectedDay));\n            },\n            onRangeSelected: (start, end) {\n              dateCellBloc.add(DateCellEditorEvent.updateDateRange(start, end));\n            },\n            onIsRangeChanged: (value, dateTime, endDateTime) {\n              dateCellBloc.add(\n                DateCellEditorEvent.setIsRange(value, dateTime, endDateTime),\n              );\n            },\n            onIncludeTimeChanged: (value, dateTime, endDateTime) {\n              dateCellBloc.add(\n                DateCellEditorEvent.setIncludeTime(\n                  value,\n                  dateTime,\n                  endDateTime,\n                ),\n              );\n            },\n            onClearDate: () {\n              dateCellBloc.add(const DateCellEditorEvent.clearDate());\n            },\n            onReminderSelected: (option) {\n              dateCellBloc.add(DateCellEditorEvent.setReminderOption(option));\n            },\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_create_field_screen.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/mobile/presentation/database/field/mobile_full_field_editor.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nclass MobileNewPropertyScreen extends StatefulWidget {\n  const MobileNewPropertyScreen({\n    super.key,\n    required this.viewId,\n    this.fieldType,\n  });\n\n  final String viewId;\n  final FieldType? fieldType;\n\n  static const routeName = '/new_property';\n  static const argViewId = 'view_id';\n  static const argFieldTypeId = 'field_type_id';\n\n  @override\n  State<MobileNewPropertyScreen> createState() =>\n      _MobileNewPropertyScreenState();\n}\n\nclass _MobileNewPropertyScreenState extends State<MobileNewPropertyScreen> {\n  late FieldOptionValues optionValues;\n\n  @override\n  void initState() {\n    super.initState();\n\n    final type = widget.fieldType ?? FieldType.RichText;\n    optionValues = FieldOptionValues(\n      type: type,\n      icon: \"\",\n      name: type.i18n,\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: FlowyAppBar(\n        centerTitle: true,\n        titleText: LocaleKeys.grid_field_newProperty.tr(),\n        leadingType: FlowyAppBarLeadingType.cancel,\n        actions: [\n          _SaveButton(\n            onSave: () {\n              context.pop(optionValues);\n            },\n          ),\n        ],\n      ),\n      body: MobileFieldEditor(\n        mode: FieldOptionMode.add,\n        defaultValues: optionValues,\n        onOptionValuesChanged: (optionValues) {\n          this.optionValues = optionValues;\n        },\n      ),\n    );\n  }\n}\n\nclass _SaveButton extends StatelessWidget {\n  const _SaveButton({\n    required this.onSave,\n  });\n\n  final VoidCallback onSave;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.only(right: 16.0),\n      child: Align(\n        child: GestureDetector(\n          onTap: onSave,\n          child: FlowyText.medium(\n            LocaleKeys.button_save.tr(),\n            color: const Color(0xFF00ADDC),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_edit_field_screen.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/mobile/presentation/database/field/mobile_full_field_editor.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/domain/field_backend_service.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nclass MobileEditPropertyScreen extends StatefulWidget {\n  const MobileEditPropertyScreen({\n    super.key,\n    required this.viewId,\n    required this.field,\n  });\n\n  final String viewId;\n  final FieldInfo field;\n\n  static const routeName = '/edit_property';\n  static const argViewId = 'view_id';\n  static const argField = 'field';\n\n  @override\n  State<MobileEditPropertyScreen> createState() =>\n      _MobileEditPropertyScreenState();\n}\n\nclass _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> {\n  late final FieldBackendService fieldService;\n  late FieldOptionValues _fieldOptionValues;\n\n  @override\n  void initState() {\n    super.initState();\n    _fieldOptionValues = FieldOptionValues.fromField(field: widget.field.field);\n    fieldService = FieldBackendService(\n      viewId: widget.viewId,\n      fieldId: widget.field.id,\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final viewId = widget.viewId;\n    final fieldId = widget.field.id;\n\n    return PopScope(\n      onPopInvokedWithResult: (didPop, _) {\n        if (!didPop) {\n          context.pop(_fieldOptionValues);\n        }\n      },\n      child: Scaffold(\n        appBar: FlowyAppBar(\n          titleText: LocaleKeys.grid_field_editProperty.tr(),\n          onTapLeading: () => context.pop(_fieldOptionValues),\n        ),\n        body: MobileFieldEditor(\n          mode: FieldOptionMode.edit,\n          isPrimary: widget.field.isPrimary,\n          defaultValues: FieldOptionValues.fromField(field: widget.field.field),\n          actions: [\n            widget.field.visibility?.isVisibleState() ?? true\n                ? FieldOptionAction.hide\n                : FieldOptionAction.show,\n            FieldOptionAction.duplicate,\n            FieldOptionAction.delete,\n          ],\n          onOptionValuesChanged: (fieldOptionValues) async {\n            await fieldService.updateField(name: fieldOptionValues.name);\n\n            await FieldBackendService.updateFieldType(\n              viewId: widget.viewId,\n              fieldId: widget.field.id,\n              fieldType: fieldOptionValues.type,\n            );\n\n            final data = fieldOptionValues.getTypeOptionData();\n            if (data != null) {\n              await FieldBackendService.updateFieldTypeOption(\n                viewId: widget.viewId,\n                fieldId: widget.field.id,\n                typeOptionData: data,\n              );\n            }\n            setState(() {\n              _fieldOptionValues = fieldOptionValues;\n            });\n          },\n          onAction: (action) {\n            final service = FieldServices(\n              viewId: viewId,\n              fieldId: fieldId,\n            );\n            switch (action) {\n              case FieldOptionAction.delete:\n                fieldService.delete();\n                context.pop();\n                return;\n              case FieldOptionAction.duplicate:\n                fieldService.duplicate();\n                break;\n              case FieldOptionAction.hide:\n                service.hide();\n                break;\n              case FieldOptionAction.show:\n                service.show();\n                break;\n            }\n            context.pop(_fieldOptionValues);\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:go_router/go_router.dart';\n\nimport 'mobile_create_field_screen.dart';\nimport 'mobile_edit_field_screen.dart';\nimport 'mobile_field_picker_list.dart';\nimport 'mobile_full_field_editor.dart';\nimport 'mobile_quick_field_editor.dart';\n\nconst mobileSupportedFieldTypes = [\n  FieldType.RichText,\n  FieldType.Number,\n  FieldType.SingleSelect,\n  FieldType.MultiSelect,\n  FieldType.DateTime,\n  FieldType.Media,\n  FieldType.URL,\n  FieldType.Checkbox,\n  FieldType.Checklist,\n  FieldType.LastEditedTime,\n  FieldType.CreatedTime,\n  // FieldType.Time,\n];\n\nFuture<FieldType?> showFieldTypeGridBottomSheet(\n  BuildContext context, {\n  required String title,\n}) {\n  return showMobileBottomSheet<FieldType>(\n    context,\n    showHeader: true,\n    showDragHandle: true,\n    showCloseButton: true,\n    elevation: 20,\n    title: title,\n    backgroundColor: AFThemeExtension.of(context).background,\n    enableDraggableScrollable: true,\n    builder: (context) {\n      final typeOptionMenuItemValue = mobileSupportedFieldTypes\n          .map(\n            (fieldType) => TypeOptionMenuItemValue(\n              value: fieldType,\n              backgroundColor: Theme.of(context).brightness == Brightness.light\n                  ? fieldType.mobileIconBackgroundColor\n                  : fieldType.mobileIconBackgroundColorDark,\n              text: fieldType.i18n,\n              icon: fieldType.svgData,\n              onTap: (context, fieldType) =>\n                  Navigator.of(context).pop(fieldType),\n            ),\n          )\n          .toList();\n      return Padding(\n        padding: EdgeInsets.all(16 * context.scale),\n        child: TypeOptionMenu<FieldType>(\n          values: typeOptionMenuItemValue,\n          scaleFactor: context.scale,\n        ),\n      );\n    },\n  );\n}\n\n/// Shows the field type grid and upon selection, allow users to edit the\n/// field's properties and saving it when the user clicks save.\nvoid mobileCreateFieldWorkflow(\n  BuildContext context,\n  String viewId, {\n  OrderObjectPositionPB? position,\n}) async {\n  final fieldType = await showFieldTypeGridBottomSheet(\n    context,\n    title: LocaleKeys.grid_field_newProperty.tr(),\n  );\n  if (fieldType == null || !context.mounted) {\n    return;\n  }\n  final optionValues = await context.push<FieldOptionValues>(\n    Uri(\n      path: MobileNewPropertyScreen.routeName,\n      queryParameters: {\n        MobileNewPropertyScreen.argViewId: viewId,\n        MobileNewPropertyScreen.argFieldTypeId: fieldType.value.toString(),\n      },\n    ).toString(),\n  );\n  if (optionValues != null) {\n    await optionValues.create(viewId: viewId, position: position);\n  }\n}\n\n/// Used to edit a field.\nFuture<FieldOptionValues?> showEditFieldScreen(\n  BuildContext context,\n  String viewId,\n  FieldInfo field,\n) {\n  return context.push<FieldOptionValues>(\n    MobileEditPropertyScreen.routeName,\n    extra: {\n      MobileEditPropertyScreen.argViewId: viewId,\n      MobileEditPropertyScreen.argField: field,\n    },\n  );\n}\n\n/// Shows some quick field options in a bottom sheet.\nvoid showQuickEditField(\n  BuildContext context,\n  String viewId,\n  FieldController fieldController,\n  FieldInfo fieldInfo,\n) {\n  showMobileBottomSheet(\n    context,\n    showDragHandle: true,\n    builder: (context) {\n      return SingleChildScrollView(\n        child: QuickEditField(\n          viewId: viewId,\n          fieldController: fieldController,\n          fieldInfo: fieldInfo,\n        ),\n      );\n    },\n  );\n}\n\n/// Display a list of fields in the current database that users can choose from.\nFuture<String?> showFieldPicker(\n  BuildContext context,\n  String title,\n  String? selectedFieldId,\n  FieldController fieldController,\n  bool Function(FieldInfo fieldInfo) filterBy,\n) {\n  return showMobileBottomSheet<String>(\n    context,\n    showDivider: false,\n    builder: (context) {\n      return MobileFieldPickerList(\n        title: title,\n        selectedFieldId: selectedFieldId,\n        fieldController: fieldController,\n        filterBy: filterBy,\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_picker_list.dart",
    "content": "import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';\nimport 'package:appflowy/plugins/base/drag_handler.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nclass MobileFieldPickerList extends StatefulWidget {\n  MobileFieldPickerList({\n    super.key,\n    required this.title,\n    required this.selectedFieldId,\n    required FieldController fieldController,\n    required bool Function(FieldInfo fieldInfo) filterBy,\n  }) : fields = fieldController.fieldInfos.where(filterBy).toList();\n\n  final String title;\n  final String? selectedFieldId;\n  final List<FieldInfo> fields;\n\n  @override\n  State<MobileFieldPickerList> createState() => _MobileFieldPickerListState();\n}\n\nclass _MobileFieldPickerListState extends State<MobileFieldPickerList> {\n  String? newFieldId;\n\n  @override\n  void initState() {\n    super.initState();\n    newFieldId = widget.selectedFieldId;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return DraggableScrollableSheet(\n      expand: false,\n      snap: true,\n      initialChildSize: 0.98,\n      minChildSize: 0.98,\n      maxChildSize: 0.98,\n      builder: (context, scrollController) {\n        return Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            const DragHandle(),\n            _Header(\n              title: widget.title,\n              onDone: (context) => context.pop(newFieldId),\n            ),\n            SingleChildScrollView(\n              controller: scrollController,\n              child: ListView.builder(\n                shrinkWrap: true,\n                itemCount: widget.fields.length,\n                itemBuilder: (context, index) => _FieldButton(\n                  field: widget.fields[index],\n                  showTopBorder: index == 0,\n                  isSelected: widget.fields[index].id == newFieldId,\n                  onSelect: (fieldId) => setState(() => newFieldId = fieldId),\n                ),\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n\n/// Same header as the one in showMobileBottomSheet, but allows popping the\n/// sheet with a value.\nclass _Header extends StatelessWidget {\n  const _Header({\n    required this.title,\n    required this.onDone,\n  });\n\n  final String title;\n  final void Function(BuildContext context) onDone;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.only(bottom: 4.0),\n      child: SizedBox(\n        height: 44.0,\n        child: Stack(\n          children: [\n            const Align(\n              alignment: Alignment.centerLeft,\n              child: AppBarBackButton(),\n            ),\n            Align(\n              child: FlowyText.medium(\n                title,\n                fontSize: 16.0,\n              ),\n            ),\n            Align(\n              alignment: Alignment.centerRight,\n              child: AppBarDoneButton(\n                onTap: () => onDone(context),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _FieldButton extends StatelessWidget {\n  const _FieldButton({\n    required this.field,\n    required this.isSelected,\n    required this.onSelect,\n    required this.showTopBorder,\n  });\n\n  final FieldInfo field;\n  final bool isSelected;\n  final void Function(String fieldId) onSelect;\n  final bool showTopBorder;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyOptionTile.checkbox(\n      text: field.name,\n      isSelected: isSelected,\n      leftIcon: FieldIcon(\n        fieldInfo: field,\n        dimension: 20,\n      ),\n      showTopBorder: showTopBorder,\n      onTap: () => onSelect(field.id),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart",
    "content": "import 'dart:math';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/flowy_search_text_field.dart';\nimport 'package:appflowy/mobile/presentation/base/option_color_list.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/widgets.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/plugins/base/drag_handler.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';\nimport 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:protobuf/protobuf.dart';\n\nimport 'mobile_field_bottom_sheets.dart';\n\nenum FieldOptionMode {\n  add,\n  edit,\n}\n\nclass FieldOptionValues {\n  FieldOptionValues({\n    required this.type,\n    required this.name,\n    required this.icon,\n    this.dateFormat,\n    this.timeFormat,\n    this.includeTime,\n    this.numberFormat,\n    this.selectOption = const [],\n  });\n\n  factory FieldOptionValues.fromField({required FieldPB field}) {\n    final fieldType = field.fieldType;\n    final buffer = field.typeOptionData;\n    return FieldOptionValues(\n      type: fieldType,\n      name: field.name,\n      icon: field.icon,\n      numberFormat: fieldType == FieldType.Number\n          ? NumberTypeOptionPB.fromBuffer(buffer).format\n          : null,\n      dateFormat: switch (fieldType) {\n        FieldType.DateTime => DateTypeOptionPB.fromBuffer(buffer).dateFormat,\n        FieldType.LastEditedTime ||\n        FieldType.CreatedTime =>\n          TimestampTypeOptionPB.fromBuffer(buffer).dateFormat,\n        _ => null\n      },\n      timeFormat: switch (fieldType) {\n        FieldType.DateTime => DateTypeOptionPB.fromBuffer(buffer).timeFormat,\n        FieldType.LastEditedTime ||\n        FieldType.CreatedTime =>\n          TimestampTypeOptionPB.fromBuffer(buffer).timeFormat,\n        _ => null\n      },\n      includeTime: switch (fieldType) {\n        FieldType.LastEditedTime ||\n        FieldType.CreatedTime =>\n          TimestampTypeOptionPB.fromBuffer(buffer).includeTime,\n        _ => null\n      },\n      selectOption: switch (fieldType) {\n        FieldType.SingleSelect =>\n          SingleSelectTypeOptionPB.fromBuffer(buffer).options,\n        FieldType.MultiSelect =>\n          MultiSelectTypeOptionPB.fromBuffer(buffer).options,\n        _ => [],\n      },\n    );\n  }\n\n  FieldType type;\n  String name;\n  String icon;\n\n  // FieldType.DateTime\n  // FieldType.LastEditedTime\n  // FieldType.CreatedTime\n  DateFormatPB? dateFormat;\n  TimeFormatPB? timeFormat;\n\n  // FieldType.LastEditedTime\n  // FieldType.CreatedTime\n  bool? includeTime;\n\n  // FieldType.Number\n  NumberFormatPB? numberFormat;\n\n  // FieldType.Select\n  // FieldType.MultiSelect\n  List<SelectOptionPB> selectOption;\n\n  Future<void> create({\n    required String viewId,\n    OrderObjectPositionPB? position,\n  }) async {\n    await FieldBackendService.createField(\n      viewId: viewId,\n      fieldType: type,\n      fieldName: name,\n      typeOptionData: getTypeOptionData(),\n      position: position,\n    );\n  }\n\n  Uint8List? getTypeOptionData() {\n    switch (type) {\n      case FieldType.RichText:\n      case FieldType.URL:\n      case FieldType.Checkbox:\n      case FieldType.Time:\n        return null;\n      case FieldType.Number:\n        return NumberTypeOptionPB(\n          format: numberFormat,\n        ).writeToBuffer();\n      case FieldType.DateTime:\n        return DateTypeOptionPB(\n          dateFormat: dateFormat,\n          timeFormat: timeFormat,\n        ).writeToBuffer();\n      case FieldType.SingleSelect:\n        return SingleSelectTypeOptionPB(\n          options: selectOption,\n        ).writeToBuffer();\n      case FieldType.MultiSelect:\n        return MultiSelectTypeOptionPB(\n          options: selectOption,\n        ).writeToBuffer();\n      case FieldType.Checklist:\n        return ChecklistTypeOptionPB().writeToBuffer();\n      case FieldType.LastEditedTime:\n      case FieldType.CreatedTime:\n        return TimestampTypeOptionPB(\n          dateFormat: dateFormat,\n          timeFormat: timeFormat,\n          includeTime: includeTime,\n        ).writeToBuffer();\n      case FieldType.Media:\n        return MediaTypeOptionPB().writeToBuffer();\n      default:\n        throw UnimplementedError();\n    }\n  }\n}\n\nenum FieldOptionAction {\n  hide,\n  show,\n  duplicate,\n  delete,\n}\n\nclass MobileFieldEditor extends StatefulWidget {\n  const MobileFieldEditor({\n    super.key,\n    required this.mode,\n    required this.defaultValues,\n    required this.onOptionValuesChanged,\n    this.actions = const [],\n    this.onAction,\n    this.isPrimary = false,\n  });\n\n  final FieldOptionMode mode;\n  final FieldOptionValues defaultValues;\n  final void Function(FieldOptionValues values) onOptionValuesChanged;\n\n  // only used in edit mode\n  final List<FieldOptionAction> actions;\n  final void Function(FieldOptionAction action)? onAction;\n\n  // the primary field can't be deleted, duplicated, and changed type\n  final bool isPrimary;\n\n  @override\n  State<MobileFieldEditor> createState() => _MobileFieldEditorState();\n}\n\nclass _MobileFieldEditorState extends State<MobileFieldEditor> {\n  final controller = TextEditingController();\n  bool isFieldNameChanged = false;\n\n  late FieldOptionValues values;\n\n  @override\n  void initState() {\n    super.initState();\n    values = widget.defaultValues;\n    controller.text = values.name;\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final option = _buildOption();\n    return Container(\n      color: Theme.of(context).brightness == Brightness.light\n          ? const Color(0xFFF7F8FB)\n          : const Color(0xFF23262B),\n      height: MediaQuery.of(context).size.height,\n      child: SingleChildScrollView(\n        child: Column(\n          children: [\n            const _Divider(),\n            OptionTextField(\n              controller: controller,\n              autoFocus: widget.mode == FieldOptionMode.add,\n              fieldType: values.type,\n              isPrimary: widget.isPrimary,\n              onTextChanged: (value) {\n                isFieldNameChanged = true;\n                _updateOptionValues(name: value);\n              },\n              onFieldTypeChanged: (type) {\n                setState(\n                  () {\n                    if (widget.mode == FieldOptionMode.add &&\n                        !isFieldNameChanged) {\n                      controller.text = type.i18n;\n                      _updateOptionValues(name: type.i18n);\n                    }\n                    _updateOptionValues(type: type);\n                  },\n                );\n              },\n            ),\n            const _Divider(),\n            if (!widget.isPrimary) ...[\n              _PropertyType(\n                type: values.type,\n                onSelected: (type) {\n                  setState(\n                    () {\n                      if (widget.mode == FieldOptionMode.add &&\n                          !isFieldNameChanged) {\n                        controller.text = type.i18n;\n                        _updateOptionValues(name: type.i18n);\n                      }\n                      _updateOptionValues(type: type);\n                    },\n                  );\n                },\n              ),\n              const _Divider(),\n              if (option.isNotEmpty) ...[\n                ...option,\n                const _Divider(),\n              ],\n            ],\n            ..._buildOptionActions(),\n            const _Divider(),\n            VSpace(MediaQuery.viewPaddingOf(context).bottom == 0 ? 28.0 : 16.0),\n          ],\n        ),\n      ),\n    );\n  }\n\n  List<Widget> _buildOption() {\n    switch (values.type) {\n      case FieldType.Number:\n        return [\n          _NumberOption(\n            selectedFormat: values.numberFormat ?? NumberFormatPB.Num,\n            onSelected: (format) => setState(\n              () => _updateOptionValues(\n                numberFormat: format,\n              ),\n            ),\n          ),\n        ];\n      case FieldType.DateTime:\n        return [\n          _DateOption(\n            selectedFormat: values.dateFormat ?? DateFormatPB.Local,\n            onSelected: (format) => _updateOptionValues(\n              dateFormat: format,\n            ),\n          ),\n          const _Divider(),\n          _TimeOption(\n            selectedFormat: values.timeFormat ?? TimeFormatPB.TwelveHour,\n            onSelected: (format) => _updateOptionValues(\n              timeFormat: format,\n            ),\n          ),\n        ];\n      case FieldType.LastEditedTime:\n      case FieldType.CreatedTime:\n        return [\n          _DateOption(\n            selectedFormat: values.dateFormat ?? DateFormatPB.Local,\n            onSelected: (format) => _updateOptionValues(\n              dateFormat: format,\n            ),\n          ),\n          const _Divider(),\n          _TimeOption(\n            selectedFormat: values.timeFormat ?? TimeFormatPB.TwelveHour,\n            onSelected: (format) => _updateOptionValues(\n              timeFormat: format,\n            ),\n          ),\n          const _Divider(),\n          _IncludeTimeOption(\n            includeTime: values.includeTime ?? true,\n            onToggle: (includeTime) => _updateOptionValues(\n              includeTime: includeTime,\n            ),\n          ),\n        ];\n      case FieldType.SingleSelect:\n      case FieldType.MultiSelect:\n        return [\n          _SelectOption(\n            mode: widget.mode,\n            selectOption: values.selectOption,\n            onAddOptions: (options) {\n              if (values.selectOption.lastOrNull?.name.isEmpty == true) {\n                // ignore the add action if the last one doesn't have a name\n                return;\n              }\n              setState(() {\n                _updateOptionValues(\n                  selectOption: values.selectOption + options,\n                );\n              });\n            },\n            onUpdateOptions: (options) {\n              _updateOptionValues(selectOption: options);\n            },\n          ),\n        ];\n      default:\n        return [];\n    }\n  }\n\n  List<Widget> _buildOptionActions() {\n    if (widget.mode == FieldOptionMode.add || widget.actions.isEmpty) {\n      return [];\n    }\n\n    return [\n      if (widget.actions.contains(FieldOptionAction.hide) && !widget.isPrimary)\n        FlowyOptionTile.text(\n          text: LocaleKeys.grid_field_hide.tr(),\n          leftIcon: const FlowySvg(FlowySvgs.m_field_hide_s),\n          onTap: () => widget.onAction?.call(FieldOptionAction.hide),\n        ),\n      if (widget.actions.contains(FieldOptionAction.show))\n        FlowyOptionTile.text(\n          text: LocaleKeys.grid_field_show.tr(),\n          leftIcon: const FlowySvg(FlowySvgs.show_m, size: Size.square(16)),\n          onTap: () => widget.onAction?.call(FieldOptionAction.show),\n        ),\n      if (widget.actions.contains(FieldOptionAction.duplicate) &&\n          !widget.isPrimary)\n        FlowyOptionTile.text(\n          showTopBorder: false,\n          text: LocaleKeys.button_duplicate.tr(),\n          leftIcon: const FlowySvg(FlowySvgs.m_field_copy_s),\n          onTap: () => widget.onAction?.call(FieldOptionAction.duplicate),\n        ),\n      if (widget.actions.contains(FieldOptionAction.delete) &&\n          !widget.isPrimary)\n        FlowyOptionTile.text(\n          showTopBorder: false,\n          text: LocaleKeys.button_delete.tr(),\n          textColor: Theme.of(context).colorScheme.error,\n          leftIcon: FlowySvg(\n            FlowySvgs.m_delete_s,\n            color: Theme.of(context).colorScheme.error,\n          ),\n          onTap: () => widget.onAction?.call(FieldOptionAction.delete),\n        ),\n    ];\n  }\n\n  void _updateOptionValues({\n    FieldType? type,\n    String? name,\n    DateFormatPB? dateFormat,\n    TimeFormatPB? timeFormat,\n    bool? includeTime,\n    NumberFormatPB? numberFormat,\n    List<SelectOptionPB>? selectOption,\n  }) {\n    if (type != null) {\n      values.type = type;\n    }\n    if (name != null) {\n      values.name = name;\n    }\n    if (dateFormat != null) {\n      values.dateFormat = dateFormat;\n    }\n    if (timeFormat != null) {\n      values.timeFormat = timeFormat;\n    }\n    if (includeTime != null) {\n      values.includeTime = includeTime;\n    }\n    if (numberFormat != null) {\n      values.numberFormat = numberFormat;\n    }\n    if (selectOption != null) {\n      values.selectOption = selectOption;\n    }\n\n    widget.onOptionValuesChanged(values);\n  }\n}\n\nclass _PropertyType extends StatelessWidget {\n  const _PropertyType({\n    required this.type,\n    required this.onSelected,\n  });\n\n  final FieldType type;\n  final void Function(FieldType type) onSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyOptionTile.text(\n      text: LocaleKeys.grid_field_propertyType.tr(),\n      trailing: Row(\n        children: [\n          FlowySvg(\n            type.svgData,\n            size: const Size.square(22),\n            color: Theme.of(context).hintColor,\n          ),\n          const HSpace(6.0),\n          FlowyText(\n            type.i18n,\n            color: Theme.of(context).hintColor,\n          ),\n          const HSpace(4.0),\n          FlowySvg(\n            FlowySvgs.arrow_right_s,\n            color: Theme.of(context).hintColor,\n            size: const Size.square(18.0),\n          ),\n        ],\n      ),\n      onTap: () async {\n        final fieldType = await showFieldTypeGridBottomSheet(\n          context,\n          title: LocaleKeys.grid_field_editProperty.tr(),\n        );\n        if (fieldType != null) {\n          onSelected(fieldType);\n        }\n      },\n    );\n  }\n}\n\nclass _Divider extends StatelessWidget {\n  const _Divider();\n\n  @override\n  Widget build(BuildContext context) {\n    return const VSpace(\n      24.0,\n    );\n  }\n}\n\nclass _DateOption extends StatefulWidget {\n  const _DateOption({\n    required this.selectedFormat,\n    required this.onSelected,\n  });\n\n  final DateFormatPB selectedFormat;\n  final Function(DateFormatPB format) onSelected;\n\n  @override\n  State<_DateOption> createState() => _DateOptionState();\n}\n\nclass _DateOptionState extends State<_DateOption> {\n  DateFormatPB selectedFormat = DateFormatPB.Local;\n\n  @override\n  void initState() {\n    super.initState();\n\n    selectedFormat = widget.selectedFormat;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Padding(\n          padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 4.0),\n          child: FlowyText(\n            LocaleKeys.grid_field_dateFormat.tr().toUpperCase(),\n            fontSize: 13,\n            color: Theme.of(context).hintColor,\n          ),\n        ),\n        ...DateFormatPB.values.mapIndexed((index, format) {\n          return FlowyOptionTile.checkbox(\n            text: format.title(),\n            isSelected: selectedFormat == format,\n            showTopBorder: index == 0,\n            onTap: () {\n              widget.onSelected(format);\n              setState(() {\n                selectedFormat = format;\n              });\n            },\n          );\n        }),\n      ],\n    );\n  }\n}\n\nclass _TimeOption extends StatefulWidget {\n  const _TimeOption({\n    required this.selectedFormat,\n    required this.onSelected,\n  });\n\n  final TimeFormatPB selectedFormat;\n  final Function(TimeFormatPB format) onSelected;\n\n  @override\n  State<_TimeOption> createState() => _TimeOptionState();\n}\n\nclass _TimeOptionState extends State<_TimeOption> {\n  TimeFormatPB selectedFormat = TimeFormatPB.TwelveHour;\n\n  @override\n  void initState() {\n    super.initState();\n\n    selectedFormat = widget.selectedFormat;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Padding(\n          padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 4.0),\n          child: FlowyText(\n            LocaleKeys.grid_field_timeFormat.tr().toUpperCase(),\n            fontSize: 13,\n            color: Theme.of(context).hintColor,\n          ),\n        ),\n        ...TimeFormatPB.values.mapIndexed((index, format) {\n          return FlowyOptionTile.checkbox(\n            text: format.title(),\n            isSelected: selectedFormat == format,\n            showTopBorder: index == 0,\n            onTap: () {\n              widget.onSelected(format);\n              setState(() {\n                selectedFormat = format;\n              });\n            },\n          );\n        }),\n      ],\n    );\n  }\n}\n\nclass _IncludeTimeOption extends StatefulWidget {\n  const _IncludeTimeOption({\n    required this.includeTime,\n    required this.onToggle,\n  });\n\n  final bool includeTime;\n  final void Function(bool includeTime) onToggle;\n\n  @override\n  State<_IncludeTimeOption> createState() => _IncludeTimeOptionState();\n}\n\nclass _IncludeTimeOptionState extends State<_IncludeTimeOption> {\n  late bool includeTime = widget.includeTime;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyOptionTile.toggle(\n      text: LocaleKeys.grid_field_includeTime.tr(),\n      isSelected: includeTime,\n      onValueChanged: (value) {\n        widget.onToggle(value);\n        setState(() {\n          includeTime = value;\n        });\n      },\n    );\n  }\n}\n\nclass _NumberOption extends StatelessWidget {\n  const _NumberOption({\n    required this.selectedFormat,\n    required this.onSelected,\n  });\n\n  final NumberFormatPB selectedFormat;\n  final void Function(NumberFormatPB format) onSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyOptionTile.text(\n      text: LocaleKeys.grid_field_numberFormat.tr(),\n      trailing: Row(\n        children: [\n          FlowyText(\n            selectedFormat.title(),\n            color: Theme.of(context).hintColor,\n          ),\n          const HSpace(4.0),\n          FlowySvg(\n            FlowySvgs.arrow_right_s,\n            color: Theme.of(context).hintColor,\n            size: const Size.square(18.0),\n          ),\n        ],\n      ),\n      onTap: () {\n        showMobileBottomSheet(\n          context,\n          backgroundColor: Theme.of(context).colorScheme.surface,\n          builder: (context) {\n            return DraggableScrollableSheet(\n              expand: false,\n              snap: true,\n              minChildSize: 0.5,\n              builder: (context, scrollController) => _NumberFormatList(\n                scrollController: scrollController,\n                selectedFormat: selectedFormat,\n                onSelected: (type) {\n                  onSelected(type);\n                  context.pop();\n                },\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass _NumberFormatList extends StatefulWidget {\n  const _NumberFormatList({\n    this.scrollController,\n    required this.selectedFormat,\n    required this.onSelected,\n  });\n\n  final NumberFormatPB selectedFormat;\n  final ScrollController? scrollController;\n  final void Function(NumberFormatPB format) onSelected;\n\n  @override\n  State<_NumberFormatList> createState() => _NumberFormatListState();\n}\n\nclass _NumberFormatListState extends State<_NumberFormatList> {\n  List<NumberFormatPB> formats = NumberFormatPB.values;\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      controller: widget.scrollController,\n      children: [\n        const Center(\n          child: DragHandle(),\n        ),\n        Container(\n          margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),\n          height: 44.0,\n          child: FlowySearchTextField(\n            onChanged: (String value) {\n              setState(() {\n                formats = NumberFormatPB.values\n                    .where(\n                      (element) => element\n                          .title()\n                          .toLowerCase()\n                          .contains(value.toLowerCase()),\n                    )\n                    .toList();\n              });\n            },\n          ),\n        ),\n        ...formats.mapIndexed(\n          (index, element) => FlowyOptionTile.checkbox(\n            text: element.title(),\n            content: Expanded(\n              child: Row(\n                children: [\n                  Padding(\n                    padding: const EdgeInsets.fromLTRB(\n                      4.0,\n                      16.0,\n                      12.0,\n                      16.0,\n                    ),\n                    child: FlowyText(\n                      element.title(),\n                      fontSize: 16,\n                    ),\n                  ),\n                  FlowyText(\n                    element.iconSymbol(),\n                    fontSize: 16,\n                    color: Theme.of(context).hintColor,\n                  ),\n                  widget.selectedFormat != element\n                      ? const HSpace(30.0)\n                      : const HSpace(6.0),\n                ],\n              ),\n            ),\n            isSelected: widget.selectedFormat == element,\n            showTopBorder: false,\n            onTap: () => widget.onSelected(element),\n          ),\n        ),\n      ],\n    );\n  }\n}\n\n// single select or multi select\nclass _SelectOption extends StatelessWidget {\n  _SelectOption({\n    required this.mode,\n    required this.selectOption,\n    required this.onAddOptions,\n    required this.onUpdateOptions,\n  });\n\n  final List<SelectOptionPB> selectOption;\n  final void Function(List<SelectOptionPB> options) onAddOptions;\n  final void Function(List<SelectOptionPB> options) onUpdateOptions;\n  final FieldOptionMode mode;\n\n  final random = Random();\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Padding(\n          padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 4.0),\n          child: FlowyText(\n            LocaleKeys.grid_field_optionTitle.tr().toUpperCase(),\n            fontSize: 13,\n            color: Theme.of(context).hintColor,\n          ),\n        ),\n        _SelectOptionList(\n          selectOptions: selectOption,\n          onUpdateOptions: onUpdateOptions,\n        ),\n        FlowyOptionTile.text(\n          text: LocaleKeys.grid_field_addOption.tr(),\n          leftIcon: const FlowySvg(\n            FlowySvgs.add_s,\n            size: Size.square(20),\n          ),\n          onTap: () {\n            onAddOptions([\n              SelectOptionPB(\n                id: uuid(),\n                name: '',\n                color: SelectOptionColorPB.valueOf(\n                  random.nextInt(SelectOptionColorPB.values.length),\n                ),\n              ),\n            ]);\n          },\n        ),\n      ],\n    );\n  }\n}\n\nclass _SelectOptionList extends StatefulWidget {\n  const _SelectOptionList({\n    required this.selectOptions,\n    required this.onUpdateOptions,\n  });\n\n  final List<SelectOptionPB> selectOptions;\n  final void Function(List<SelectOptionPB> options) onUpdateOptions;\n\n  @override\n  State<_SelectOptionList> createState() => _SelectOptionListState();\n}\n\nclass _SelectOptionListState extends State<_SelectOptionList> {\n  late List<SelectOptionPB> options;\n\n  @override\n  void initState() {\n    super.initState();\n\n    options = widget.selectOptions;\n  }\n\n  @override\n  void didUpdateWidget(covariant _SelectOptionList oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    options = widget.selectOptions;\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (widget.selectOptions.isEmpty) {\n      return const SizedBox.shrink();\n    }\n    return ListView(\n      shrinkWrap: true,\n      padding: EdgeInsets.zero,\n      // disable the inner scroll physics, so the outer ListView can scroll\n      physics: const NeverScrollableScrollPhysics(),\n      children: widget.selectOptions\n          .mapIndexed(\n            (index, option) => _SelectOptionTile(\n              option: option,\n              showTopBorder: index == 0,\n              showBottomBorder: index != widget.selectOptions.length - 1,\n              onUpdateOption: (option) {\n                _updateOption(index, option);\n              },\n            ),\n          )\n          .toList(),\n    );\n  }\n\n  void _updateOption(int index, SelectOptionPB option) {\n    final options = [...this.options];\n    options[index] = option;\n    this.options = options;\n    widget.onUpdateOptions(options);\n  }\n}\n\nclass _SelectOptionTile extends StatefulWidget {\n  const _SelectOptionTile({\n    required this.option,\n    required this.showTopBorder,\n    required this.showBottomBorder,\n    required this.onUpdateOption,\n  });\n\n  final SelectOptionPB option;\n  final bool showTopBorder;\n  final bool showBottomBorder;\n  final void Function(SelectOptionPB option) onUpdateOption;\n\n  @override\n  State<_SelectOptionTile> createState() => _SelectOptionTileState();\n}\n\nclass _SelectOptionTileState extends State<_SelectOptionTile> {\n  final TextEditingController controller = TextEditingController();\n  late SelectOptionPB option;\n\n  @override\n  void initState() {\n    super.initState();\n\n    controller.text = widget.option.name;\n    option = widget.option;\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyOptionTile.textField(\n      controller: controller,\n      textFieldHintText: LocaleKeys.grid_field_typeANewOption.tr(),\n      showTopBorder: widget.showTopBorder,\n      showBottomBorder: widget.showBottomBorder,\n      trailing: _SelectOptionColor(\n        color: option.color,\n        onChanged: (color) {\n          setState(() {\n            option.freeze();\n            option = option.rebuild((p0) => p0.color = color);\n            widget.onUpdateOption(option);\n          });\n          context.pop();\n        },\n      ),\n      onTextChanged: (name) {\n        setState(() {\n          option.freeze();\n          option = option.rebuild((p0) => p0.name = name);\n          widget.onUpdateOption(option);\n        });\n      },\n    );\n  }\n}\n\nclass _SelectOptionColor extends StatelessWidget {\n  const _SelectOptionColor({\n    required this.color,\n    required this.onChanged,\n  });\n\n  final SelectOptionColorPB color;\n  final void Function(SelectOptionColorPB) onChanged;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onTap: () {\n        showMobileBottomSheet(\n          context,\n          showHeader: true,\n          showCloseButton: true,\n          title: LocaleKeys.grid_selectOption_colorPanelTitle.tr(),\n          padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),\n          builder: (context) {\n            return OptionColorList(\n              selectedColor: color,\n              onSelectedColor: onChanged,\n            );\n          },\n        );\n      },\n      child: Container(\n        decoration: BoxDecoration(\n          color: color.toColor(context),\n          borderRadius: Corners.s10Border,\n        ),\n        width: 32,\n        height: 32,\n        alignment: Alignment.center,\n        child: const FlowySvg(\n          FlowySvgs.arrow_down_s,\n          size: Size.square(20),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_quick_field_editor.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/widgets.dart';\nimport 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/domain/field_backend_service.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nclass QuickEditField extends StatefulWidget {\n  const QuickEditField({\n    super.key,\n    required this.viewId,\n    required this.fieldController,\n    required this.fieldInfo,\n  });\n\n  final String viewId;\n  final FieldController fieldController;\n  final FieldInfo fieldInfo;\n\n  @override\n  State<QuickEditField> createState() => _QuickEditFieldState();\n}\n\nclass _QuickEditFieldState extends State<QuickEditField> {\n  final TextEditingController controller = TextEditingController();\n\n  late final FieldServices service = FieldServices(\n    viewId: widget.viewId,\n    fieldId: widget.fieldInfo.field.id,\n  );\n\n  late FieldVisibility fieldVisibility;\n\n  @override\n  void initState() {\n    super.initState();\n    fieldVisibility =\n        widget.fieldInfo.visibility ?? FieldVisibility.AlwaysShown;\n    controller.text = widget.fieldInfo.field.name;\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) => FieldEditorBloc(\n        viewId: widget.viewId,\n        fieldController: widget.fieldController,\n        fieldInfo: widget.fieldInfo,\n        isNew: false,\n      ),\n      child: BlocConsumer<FieldEditorBloc, FieldEditorState>(\n        listenWhen: (previous, current) =>\n            previous.field.name != current.field.name,\n        listener: (context, state) => controller.text = state.field.name,\n        builder: (context, state) {\n          return Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              const VSpace(16),\n              OptionTextField(\n                controller: controller,\n                isPrimary: state.field.isPrimary,\n                fieldType: state.field.fieldType,\n                onTextChanged: (text) {\n                  context\n                      .read<FieldEditorBloc>()\n                      .add(FieldEditorEvent.renameField(text));\n                },\n                onFieldTypeChanged: (fieldType) {\n                  context\n                      .read<FieldEditorBloc>()\n                      .add(FieldEditorEvent.switchFieldType(fieldType));\n                },\n              ),\n              const _Divider(),\n              FlowyOptionTile.text(\n                text: LocaleKeys.grid_field_editProperty.tr(),\n                leftIcon: const FlowySvg(FlowySvgs.m_field_edit_s),\n                onTap: () {\n                  showEditFieldScreen(\n                    context,\n                    widget.viewId,\n                    state.field,\n                  );\n                  context.pop();\n                },\n              ),\n              if (!widget.fieldInfo.isPrimary) ...[\n                FlowyOptionTile.text(\n                  showTopBorder: false,\n                  text: fieldVisibility.isVisibleState()\n                      ? LocaleKeys.grid_field_hide.tr()\n                      : LocaleKeys.grid_field_show.tr(),\n                  leftIcon: const FlowySvg(FlowySvgs.m_field_hide_s),\n                  onTap: () async {\n                    context.pop();\n                    if (fieldVisibility.isVisibleState()) {\n                      await service.hide();\n                    } else {\n                      await service.hide();\n                    }\n                  },\n                ),\n                FlowyOptionTile.text(\n                  showTopBorder: false,\n                  text: LocaleKeys.grid_field_insertLeft.tr(),\n                  leftIcon: const FlowySvg(FlowySvgs.m_filed_insert_left_s),\n                  onTap: () {\n                    context.pop();\n                    mobileCreateFieldWorkflow(\n                      context,\n                      widget.viewId,\n                      position: OrderObjectPositionPB(\n                        position: OrderObjectPositionTypePB.Before,\n                        objectId: widget.fieldInfo.id,\n                      ),\n                    );\n                  },\n                ),\n              ],\n              FlowyOptionTile.text(\n                showTopBorder: false,\n                text: LocaleKeys.grid_field_insertRight.tr(),\n                leftIcon: const FlowySvg(FlowySvgs.m_filed_insert_right_s),\n                onTap: () {\n                  context.pop();\n                  mobileCreateFieldWorkflow(\n                    context,\n                    widget.viewId,\n                    position: OrderObjectPositionPB(\n                      position: OrderObjectPositionTypePB.After,\n                      objectId: widget.fieldInfo.id,\n                    ),\n                  );\n                },\n              ),\n              if (!widget.fieldInfo.isPrimary) ...[\n                FlowyOptionTile.text(\n                  showTopBorder: false,\n                  text: LocaleKeys.button_duplicate.tr(),\n                  leftIcon: const FlowySvg(FlowySvgs.m_field_copy_s),\n                  onTap: () {\n                    context.pop();\n                    service.duplicate();\n                  },\n                ),\n                FlowyOptionTile.text(\n                  showTopBorder: false,\n                  text: LocaleKeys.button_delete.tr(),\n                  textColor: Theme.of(context).colorScheme.error,\n                  leftIcon: FlowySvg(\n                    FlowySvgs.m_field_delete_s,\n                    color: Theme.of(context).colorScheme.error,\n                  ),\n                  onTap: () {\n                    context.pop();\n                    service.delete();\n                  },\n                ),\n              ],\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _Divider extends StatelessWidget {\n  const _Divider();\n\n  @override\n  Widget build(BuildContext context) {\n    return const VSpace(20);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_calendar_events_empty.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\n\nclass MobileCalendarEventsEmpty extends StatelessWidget {\n  const MobileCalendarEventsEmpty({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Center(\n      child: Padding(\n        padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            FlowyText(\n              LocaleKeys.calendar_mobileEventScreen_emptyTitle.tr(),\n              fontWeight: FontWeight.w700,\n              fontSize: 14,\n            ),\n            const VSpace(8),\n            FlowyText.regular(\n              LocaleKeys.calendar_mobileEventScreen_emptyBody.tr(),\n              textAlign: TextAlign.center,\n              maxLines: 2,\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_calendar_events_screen.dart",
    "content": "import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/mobile/presentation/database/mobile_calendar_events_empty.dart';\nimport 'package:appflowy/plugins/database/application/row/row_cache.dart';\nimport 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart';\nimport 'package:appflowy/plugins/database/calendar/presentation/calendar_event_card.dart';\nimport 'package:calendar_view/calendar_view.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MobileCalendarEventsScreen extends StatefulWidget {\n  const MobileCalendarEventsScreen({\n    super.key,\n    required this.calendarBloc,\n    required this.date,\n    required this.events,\n    required this.rowCache,\n    required this.viewId,\n  });\n\n  final CalendarBloc calendarBloc;\n  final DateTime date;\n  final List<CalendarDayEvent> events;\n  final RowCache rowCache;\n  final String viewId;\n\n  static const routeName = '/calendar_events';\n\n  // GoRouter Arguments\n  static const calendarBlocKey = 'calendar_bloc';\n  static const calendarDateKey = 'date';\n  static const calendarEventsKey = 'events';\n  static const calendarRowCacheKey = 'row_cache';\n  static const calendarViewIdKey = 'view_id';\n\n  @override\n  State<MobileCalendarEventsScreen> createState() =>\n      _MobileCalendarEventsScreenState();\n}\n\nclass _MobileCalendarEventsScreenState\n    extends State<MobileCalendarEventsScreen> {\n  late final List<CalendarDayEvent> _events = widget.events;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      floatingActionButton: FloatingActionButton(\n        key: const Key('add_event_fab'),\n        elevation: 6,\n        backgroundColor: Theme.of(context).colorScheme.primary,\n        foregroundColor: Theme.of(context).colorScheme.onPrimary,\n        onPressed: () =>\n            widget.calendarBloc.add(CalendarEvent.createEvent(widget.date)),\n        child: const Text('+'),\n      ),\n      appBar: FlowyAppBar(\n        titleText: DateFormat.yMMMMd(context.locale.toLanguageTag())\n            .format(widget.date),\n      ),\n      body: BlocProvider<CalendarBloc>.value(\n        value: widget.calendarBloc,\n        child: BlocBuilder<CalendarBloc, CalendarState>(\n          buildWhen: (p, c) =>\n              p.newEvent != c.newEvent &&\n              c.newEvent?.date.withoutTime == widget.date,\n          builder: (context, state) {\n            if (state.newEvent?.event != null &&\n                _events\n                    .none((e) => e.eventId == state.newEvent!.event!.eventId) &&\n                state.newEvent!.date.withoutTime == widget.date) {\n              _events.add(state.newEvent!.event!);\n            }\n\n            if (_events.isEmpty) {\n              return const MobileCalendarEventsEmpty();\n            }\n\n            return SingleChildScrollView(\n              child: Column(\n                children: [\n                  const VSpace(10),\n                  ..._events.map((event) {\n                    return EventCard(\n                      databaseController:\n                          widget.calendarBloc.databaseController,\n                      event: event,\n                      constraints: const BoxConstraints.expand(),\n                      autoEdit: false,\n                      isDraggable: false,\n                      padding: const EdgeInsets.symmetric(\n                        horizontal: 12,\n                        vertical: 3,\n                      ),\n                    );\n                  }),\n                  const VSpace(24),\n                ],\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_calendar_screen.dart",
    "content": "import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileCalendarScreen extends StatelessWidget {\n  const MobileCalendarScreen({\n    super.key,\n    required this.id,\n    this.title,\n  });\n\n  /// view id\n  final String id;\n  final String? title;\n\n  static const routeName = '/calendar';\n  static const viewId = 'id';\n  static const viewTitle = 'title';\n\n  @override\n  Widget build(BuildContext context) {\n    return MobileViewPage(\n      id: id,\n      title: title,\n      viewLayout: ViewLayoutPB.Document,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/mobile_grid_screen.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/mobile/presentation/base/mobile_view_page.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\n\nclass MobileGridScreen extends StatelessWidget {\n  const MobileGridScreen({\n    super.key,\n    required this.id,\n    this.title,\n    this.arguments,\n  });\n\n  /// view id\n  final String id;\n  final String? title;\n  final Map<String, dynamic>? arguments;\n\n  static const routeName = '/grid';\n  static const viewId = 'id';\n  static const viewTitle = 'title';\n  static const viewArgs = 'arguments';\n\n  @override\n  Widget build(BuildContext context) {\n    return MobileViewPage(\n      id: id,\n      title: title,\n      viewLayout: ViewLayoutPB.Grid,\n      arguments: arguments,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_field_list.dart",
    "content": "import 'dart:ui';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/setting/property_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../field/mobile_field_bottom_sheets.dart';\n\nclass MobileDatabaseFieldList extends StatelessWidget {\n  const MobileDatabaseFieldList({\n    super.key,\n    required this.databaseController,\n    required this.canCreate,\n  });\n\n  final DatabaseController databaseController;\n  final bool canCreate;\n\n  @override\n  Widget build(BuildContext context) {\n    return _MobileDatabaseFieldListBody(\n      databaseController: databaseController,\n      viewId: context.read<ViewBloc>().state.view.id,\n      canCreate: canCreate,\n    );\n  }\n}\n\nclass _MobileDatabaseFieldListBody extends StatelessWidget {\n  const _MobileDatabaseFieldListBody({\n    required this.databaseController,\n    required this.viewId,\n    required this.canCreate,\n  });\n\n  final DatabaseController databaseController;\n  final String viewId;\n  final bool canCreate;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<DatabasePropertyBloc>(\n      create: (_) => DatabasePropertyBloc(\n        viewId: viewId,\n        fieldController: databaseController.fieldController,\n      )..add(const DatabasePropertyEvent.initial()),\n      child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>(\n        builder: (context, state) {\n          if (state.fieldContexts.isEmpty) {\n            return const SizedBox.shrink();\n          }\n\n          final fields = [...state.fieldContexts];\n          final firstField = fields.removeAt(0);\n          final firstCell = DatabaseFieldListTile(\n            key: ValueKey(firstField.id),\n            viewId: viewId,\n            fieldController: databaseController.fieldController,\n            fieldInfo: firstField,\n            showTopBorder: false,\n          );\n          final cells = fields\n              .mapIndexed(\n                (index, field) => DatabaseFieldListTile(\n                  key: ValueKey(field.id),\n                  viewId: viewId,\n                  fieldController: databaseController.fieldController,\n                  fieldInfo: field,\n                  showTopBorder: false,\n                ),\n              )\n              .toList();\n\n          return ReorderableListView.builder(\n            proxyDecorator: (_, index, anim) {\n              final field = fields[index];\n              return AnimatedBuilder(\n                animation: anim,\n                builder: (BuildContext context, Widget? child) {\n                  final double animValue =\n                      Curves.easeInOut.transform(anim.value);\n                  final double scale = lerpDouble(1, 1.05, animValue)!;\n                  return Transform.scale(\n                    scale: scale,\n                    child: Material(\n                      child: DatabaseFieldListTile(\n                        key: ValueKey(field.id),\n                        viewId: viewId,\n                        fieldController: databaseController.fieldController,\n                        fieldInfo: field,\n                        showTopBorder: true,\n                      ),\n                    ),\n                  );\n                },\n              );\n            },\n            shrinkWrap: true,\n            onReorder: (from, to) {\n              from++;\n              to++;\n              context\n                  .read<DatabasePropertyBloc>()\n                  .add(DatabasePropertyEvent.moveField(from, to));\n            },\n            header: firstCell,\n            footer: canCreate\n                ? Padding(\n                    padding: const EdgeInsets.only(top: 20),\n                    child: _NewDatabaseFieldTile(viewId: viewId),\n                  )\n                : null,\n            itemCount: cells.length,\n            itemBuilder: (context, index) => cells[index],\n            padding: EdgeInsets.only(\n              bottom: context.bottomSheetPadding(ignoreViewPadding: false),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass DatabaseFieldListTile extends StatelessWidget {\n  const DatabaseFieldListTile({\n    super.key,\n    required this.fieldInfo,\n    required this.viewId,\n    required this.fieldController,\n    required this.showTopBorder,\n  });\n\n  final FieldInfo fieldInfo;\n  final String viewId;\n  final FieldController fieldController;\n  final bool showTopBorder;\n\n  @override\n  Widget build(BuildContext context) {\n    if (fieldInfo.field.isPrimary) {\n      return FlowyOptionTile.text(\n        text: fieldInfo.name,\n        leftIcon: FieldIcon(\n          fieldInfo: fieldInfo,\n          dimension: 20,\n        ),\n        onTap: () => showEditFieldScreen(context, viewId, fieldInfo),\n        showTopBorder: showTopBorder,\n      );\n    } else {\n      return FlowyOptionTile.toggle(\n        isSelected: fieldInfo.visibility?.isVisibleState() ?? false,\n        text: fieldInfo.name,\n        leftIcon: FieldIcon(\n          fieldInfo: fieldInfo,\n          dimension: 20,\n        ),\n        showTopBorder: showTopBorder,\n        onTap: () => showEditFieldScreen(context, viewId, fieldInfo),\n        onValueChanged: (value) {\n          final newVisibility = fieldInfo.visibility!.toggle();\n          context.read<DatabasePropertyBloc>().add(\n                DatabasePropertyEvent.setFieldVisibility(\n                  fieldInfo.id,\n                  newVisibility,\n                ),\n              );\n        },\n      );\n    }\n  }\n}\n\nclass _NewDatabaseFieldTile extends StatelessWidget {\n  const _NewDatabaseFieldTile({required this.viewId});\n\n  final String viewId;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyOptionTile.text(\n      text: LocaleKeys.grid_field_newProperty.tr(),\n      leftIcon: FlowySvg(\n        FlowySvgs.add_s,\n        size: const Size.square(20),\n        color: Theme.of(context).hintColor,\n      ),\n      textColor: Theme.of(context).hintColor,\n      onTap: () => mobileCreateFieldWorkflow(context, viewId),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_filter_bottom_sheet.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';\nimport 'package:appflowy/mobile/presentation/database/view/database_filter_condition_list.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/select_option_loader.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart';\nimport 'package:appflowy/util/debounce.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_editor.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:fluttertoast/fluttertoast.dart';\nimport 'package:protobuf/protobuf.dart' hide FieldInfo;\nimport 'package:time/time.dart';\n\nimport 'database_filter_bottom_sheet_cubit.dart';\n\nclass MobileFilterEditor extends StatefulWidget {\n  const MobileFilterEditor({super.key});\n\n  @override\n  State<MobileFilterEditor> createState() => _MobileFilterEditorState();\n}\n\nclass _MobileFilterEditorState extends State<MobileFilterEditor> {\n  final pageController = PageController();\n  final scrollController = ScrollController();\n\n  @override\n  void dispose() {\n    pageController.dispose();\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => MobileFilterEditorCubit(\n        pageController: pageController,\n      ),\n      child: Column(\n        children: [\n          const _Header(),\n          SizedBox(\n            height: 400,\n            child: PageView.builder(\n              controller: pageController,\n              itemCount: 2,\n              physics: const NeverScrollableScrollPhysics(),\n              itemBuilder: (context, index) {\n                return switch (index) {\n                  0 => _ActiveFilters(scrollController: scrollController),\n                  1 => const _FilterDetail(),\n                  _ => const SizedBox.shrink(),\n                };\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _Header extends StatelessWidget {\n  const _Header();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<MobileFilterEditorCubit, MobileFilterEditorState>(\n      builder: (context, state) {\n        return SizedBox(\n          height: 44.0,\n          child: Stack(\n            children: [\n              if (_isBackButtonShown(state))\n                Align(\n                  alignment: Alignment.centerLeft,\n                  child: AppBarBackButton(\n                    padding: const EdgeInsets.symmetric(\n                      vertical: 12,\n                      horizontal: 16,\n                    ),\n                    onTap: () => context\n                        .read<MobileFilterEditorCubit>()\n                        .returnToOverview(),\n                  ),\n                ),\n              Align(\n                child: FlowyText.medium(\n                  LocaleKeys.grid_settings_filter.tr(),\n                  fontSize: 16.0,\n                ),\n              ),\n              if (_isSaveButtonShown(state))\n                Align(\n                  alignment: Alignment.centerRight,\n                  child: AppBarSaveButton(\n                    padding: const EdgeInsets.symmetric(\n                      vertical: 12,\n                      horizontal: 16,\n                    ),\n                    enable: _isSaveButtonEnabled(state),\n                    onTap: () => _saveOnTapHandler(context, state),\n                  ),\n                ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  bool _isBackButtonShown(MobileFilterEditorState state) {\n    return state.maybeWhen(\n      overview: (_) => false,\n      orElse: () => true,\n    );\n  }\n\n  bool _isSaveButtonShown(MobileFilterEditorState state) {\n    return state.maybeWhen(\n      editCondition: (filterId, newFilter, showSave) => showSave,\n      editContent: (_, __) => true,\n      orElse: () => false,\n    );\n  }\n\n  bool _isSaveButtonEnabled(MobileFilterEditorState state) {\n    return state.maybeWhen(\n      editCondition: (_, __, enableSave) => enableSave,\n      editContent: (_, __) => true,\n      orElse: () => false,\n    );\n  }\n\n  void _saveOnTapHandler(BuildContext context, MobileFilterEditorState state) {\n    state.maybeWhen(\n      editCondition: (filterId, newFilter, _) {\n        context\n            .read<FilterEditorBloc>()\n            .add(FilterEditorEvent.updateFilter(newFilter));\n      },\n      editContent: (filterId, newFilter) {\n        context\n            .read<FilterEditorBloc>()\n            .add(FilterEditorEvent.updateFilter(newFilter));\n      },\n      orElse: () {},\n    );\n    context.read<MobileFilterEditorCubit>().returnToOverview();\n  }\n}\n\nclass _ActiveFilters extends StatelessWidget {\n  const _ActiveFilters({\n    required this.scrollController,\n  });\n\n  final ScrollController scrollController;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterEditorBloc, FilterEditorState>(\n      builder: (context, state) {\n        return Padding(\n          padding: EdgeInsets.only(\n            bottom: MediaQuery.of(context).padding.bottom,\n          ),\n          child: Column(\n            children: [\n              Expanded(\n                child: state.filters.isEmpty\n                    ? _emptyBackground(context)\n                    : _filterList(context, state),\n              ),\n              const VSpace(12),\n              const _CreateFilterButton(),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _emptyBackground(BuildContext context) {\n    return Center(\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          FlowySvg(\n            FlowySvgs.filter_s,\n            size: const Size.square(60),\n            color: Theme.of(context).hintColor,\n          ),\n          FlowyText(\n            LocaleKeys.grid_filter_empty.tr(),\n            color: Theme.of(context).hintColor,\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _filterList(BuildContext context, FilterEditorState state) {\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      context.read<MobileFilterEditorCubit>().state.maybeWhen(\n            overview: (scrollToBottom) {\n              if (scrollToBottom && scrollController.hasClients) {\n                scrollController\n                    .jumpTo(scrollController.position.maxScrollExtent);\n                context.read<MobileFilterEditorCubit>().returnToOverview();\n              }\n            },\n            orElse: () {},\n          );\n    });\n\n    return ListView.separated(\n      controller: scrollController,\n      padding: const EdgeInsets.symmetric(\n        horizontal: 16,\n      ),\n      itemCount: state.filters.length,\n      itemBuilder: (context, index) {\n        final filter = state.filters[index];\n        final field = context\n            .read<FilterEditorBloc>()\n            .state\n            .fields\n            .firstWhereOrNull((field) => field.id == filter.fieldId);\n        return field == null\n            ? const SizedBox.shrink()\n            : _FilterItem(filter: filter, field: field);\n      },\n      separatorBuilder: (context, index) => const VSpace(12.0),\n    );\n  }\n}\n\nclass _CreateFilterButton extends StatelessWidget {\n  const _CreateFilterButton();\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: 44,\n      width: double.infinity,\n      margin: const EdgeInsets.symmetric(horizontal: 14),\n      decoration: BoxDecoration(\n        border: Border.fromBorderSide(\n          BorderSide(\n            width: 0.5,\n            color: Theme.of(context).dividerColor,\n          ),\n        ),\n        borderRadius: Corners.s10Border,\n      ),\n      child: InkWell(\n        onTap: () {\n          if (context.read<FilterEditorBloc>().state.fields.isEmpty) {\n            Fluttertoast.showToast(\n              msg: LocaleKeys.grid_filter_cannotFindCreatableField.tr(),\n              gravity: ToastGravity.BOTTOM,\n            );\n          } else {\n            context.read<MobileFilterEditorCubit>().startCreatingFilter();\n          }\n        },\n        borderRadius: Corners.s10Border,\n        child: Center(\n          child: Row(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              const FlowySvg(\n                FlowySvgs.add_s,\n                size: Size.square(16),\n              ),\n              const HSpace(6.0),\n              FlowyText(\n                LocaleKeys.grid_filter_addFilter.tr(),\n                fontSize: 15,\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _FilterItem extends StatelessWidget {\n  const _FilterItem({\n    required this.filter,\n    required this.field,\n  });\n\n  final DatabaseFilter filter;\n  final FieldInfo field;\n\n  @override\n  Widget build(BuildContext context) {\n    return DecoratedBox(\n      decoration: BoxDecoration(\n        color: Theme.of(context).hoverColor,\n        borderRadius: BorderRadius.circular(12),\n      ),\n      child: Stack(\n        children: [\n          Padding(\n            padding: const EdgeInsets.symmetric(\n              vertical: 14,\n              horizontal: 8,\n            ),\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Padding(\n                  padding: const EdgeInsets.symmetric(horizontal: 12.0),\n                  child: FlowyText.medium(\n                    LocaleKeys.grid_filter_where.tr(),\n                    fontSize: 15,\n                  ),\n                ),\n                const VSpace(10),\n                Row(\n                  children: [\n                    Expanded(\n                      child: FilterItemInnerButton(\n                        onTap: () => context\n                            .read<MobileFilterEditorCubit>()\n                            .startEditingFilterField(filter.filterId),\n                        icon: field.fieldType.svgData,\n                        child: FlowyText(\n                          field.name,\n                          overflow: TextOverflow.ellipsis,\n                        ),\n                      ),\n                    ),\n                    const HSpace(6),\n                    Expanded(\n                      child: FilterItemInnerButton(\n                        onTap: () => context\n                            .read<MobileFilterEditorCubit>()\n                            .startEditingFilterCondition(\n                              filter.filterId,\n                              filter,\n                              filter.fieldType == FieldType.DateTime,\n                            ),\n                        child: FlowyText(\n                          filter.conditionName,\n                          overflow: TextOverflow.ellipsis,\n                        ),\n                      ),\n                    ),\n                  ],\n                ),\n                if (filter.canAttachContent) ...[\n                  const VSpace(6),\n                  filter.getMobileDescription(\n                    field,\n                    onExpand: () => context\n                        .read<MobileFilterEditorCubit>()\n                        .startEditingFilterContent(filter.filterId, filter),\n                    onUpdate: (newFilter) => context\n                        .read<FilterEditorBloc>()\n                        .add(FilterEditorEvent.updateFilter(newFilter)),\n                  ),\n                ],\n              ],\n            ),\n          ),\n          Positioned(\n            right: 8,\n            top: 6,\n            child: _deleteButton(context),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _deleteButton(BuildContext context) {\n    return InkWell(\n      onTap: () => context\n          .read<FilterEditorBloc>()\n          .add(FilterEditorEvent.deleteFilter(filter.filterId)),\n      // steal from the container LongClickReorderWidget thing\n      onLongPress: () {},\n      borderRadius: BorderRadius.circular(10),\n      child: SizedBox.square(\n        dimension: 34,\n        child: Center(\n          child: FlowySvg(\n            FlowySvgs.trash_m,\n            size: const Size.square(18),\n            color: Theme.of(context).hintColor,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass FilterItemInnerButton extends StatelessWidget {\n  const FilterItemInnerButton({\n    super.key,\n    required this.onTap,\n    required this.child,\n    this.icon,\n  });\n\n  final VoidCallback onTap;\n  final FlowySvgData? icon;\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: onTap,\n      child: Container(\n        height: 44,\n        decoration: BoxDecoration(\n          border: Border.fromBorderSide(\n            BorderSide(\n              width: 0.5,\n              color: Theme.of(context).dividerColor,\n            ),\n          ),\n          borderRadius: Corners.s10Border,\n          color: Theme.of(context).colorScheme.surface,\n        ),\n        padding: const EdgeInsets.symmetric(horizontal: 12),\n        child: Center(\n          child: SeparatedRow(\n            separatorBuilder: () => const HSpace(6.0),\n            children: [\n              if (icon != null)\n                FlowySvg(\n                  icon!,\n                  size: const Size.square(16),\n                ),\n              Expanded(child: child),\n              FlowySvg(\n                FlowySvgs.icon_right_small_ccm_outlined_s,\n                size: const Size.square(14),\n                color: Theme.of(context).hintColor,\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass FilterItemInnerTextField extends StatefulWidget {\n  const FilterItemInnerTextField({\n    super.key,\n    required this.content,\n    required this.enabled,\n    required this.onSubmitted,\n  });\n\n  final String content;\n  final bool enabled;\n  final void Function(String) onSubmitted;\n\n  @override\n  State<FilterItemInnerTextField> createState() =>\n      _FilterItemInnerTextFieldState();\n}\n\nclass _FilterItemInnerTextFieldState extends State<FilterItemInnerTextField> {\n  late final TextEditingController textController =\n      TextEditingController(text: widget.content);\n  final FocusNode focusNode = FocusNode();\n  final Debounce debounce = Debounce(duration: 300.milliseconds);\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    textController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 44,\n      child: TextField(\n        enabled: widget.enabled,\n        focusNode: focusNode,\n        controller: textController,\n        onSubmitted: widget.onSubmitted,\n        onChanged: (value) => debounce.call(() => widget.onSubmitted(value)),\n        onTapOutside: (_) => focusNode.unfocus(),\n        decoration: InputDecoration(\n          filled: true,\n          fillColor: widget.enabled\n              ? Theme.of(context).colorScheme.surface\n              : Theme.of(context).disabledColor,\n          enabledBorder: _getBorder(Theme.of(context).dividerColor),\n          border: _getBorder(Theme.of(context).dividerColor),\n          focusedBorder: _getBorder(Theme.of(context).colorScheme.primary),\n          contentPadding: const EdgeInsets.symmetric(horizontal: 12),\n        ),\n      ),\n    );\n  }\n\n  InputBorder _getBorder(Color color) {\n    return OutlineInputBorder(\n      borderSide: BorderSide(\n        width: 0.5,\n        color: color,\n      ),\n      borderRadius: Corners.s10Border,\n    );\n  }\n}\n\nclass _FilterDetail extends StatelessWidget {\n  const _FilterDetail();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<MobileFilterEditorCubit, MobileFilterEditorState>(\n      builder: (context, state) {\n        return state.maybeWhen(\n          create: () {\n            return _FilterableFieldList(\n              onSelectField: (field) {\n                context\n                    .read<FilterEditorBloc>()\n                    .add(FilterEditorEvent.createFilter(field));\n                context.read<MobileFilterEditorCubit>().returnToOverview(\n                      scrollToBottom: true,\n                    );\n              },\n            );\n          },\n          editField: (filterId) {\n            return _FilterableFieldList(\n              onSelectField: (field) {\n                final filter = context\n                    .read<FilterEditorBloc>()\n                    .state\n                    .filters\n                    .firstWhereOrNull((filter) => filter.filterId == filterId);\n                if (filter != null && field.id != filter.fieldId) {\n                  context.read<FilterEditorBloc>().add(\n                        FilterEditorEvent.changeFilteringField(filterId, field),\n                      );\n                }\n                context.read<MobileFilterEditorCubit>().returnToOverview();\n              },\n            );\n          },\n          editCondition: (filterId, newFilter, showSave) {\n            return _FilterConditionList(\n              filterId: filterId,\n              onSelect: (newFilter) {\n                context\n                    .read<FilterEditorBloc>()\n                    .add(FilterEditorEvent.updateFilter(newFilter));\n                context.read<MobileFilterEditorCubit>().returnToOverview();\n              },\n            );\n          },\n          editContent: (filterId, filter) {\n            return _FilterContentEditor(\n              filter: filter,\n              onUpdateFilter: (newFilter) {\n                context.read<MobileFilterEditorCubit>().updateFilter(newFilter);\n              },\n            );\n          },\n          orElse: () => const SizedBox.shrink(),\n        );\n      },\n    );\n  }\n}\n\nclass _FilterableFieldList extends StatelessWidget {\n  const _FilterableFieldList({\n    required this.onSelectField,\n  });\n\n  final void Function(FieldInfo field) onSelectField;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        const VSpace(4.0),\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 16.0),\n          child: FlowyText(\n            LocaleKeys.grid_settings_filterBy.tr().toUpperCase(),\n            fontSize: 13,\n            color: Theme.of(context).hintColor,\n          ),\n        ),\n        const VSpace(4.0),\n        const Divider(\n          height: 0.5,\n          thickness: 0.5,\n        ),\n        Expanded(\n          child: BlocBuilder<FilterEditorBloc, FilterEditorState>(\n            builder: (context, blocState) {\n              return ListView.builder(\n                itemCount: blocState.fields.length,\n                itemBuilder: (context, index) {\n                  return FlowyOptionTile.text(\n                    text: blocState.fields[index].name,\n                    leftIcon: FieldIcon(\n                      fieldInfo: blocState.fields[index],\n                    ),\n                    showTopBorder: false,\n                    onTap: () => onSelectField(blocState.fields[index]),\n                  );\n                },\n              );\n            },\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass _FilterConditionList extends StatelessWidget {\n  const _FilterConditionList({\n    required this.filterId,\n    required this.onSelect,\n  });\n\n  final String filterId;\n  final void Function(DatabaseFilter filter) onSelect;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocSelector<FilterEditorBloc, FilterEditorState, DatabaseFilter?>(\n      selector: (state) => state.filters.firstWhereOrNull(\n        (filter) => filter.filterId == filterId,\n      ),\n      builder: (context, filter) {\n        if (filter == null) {\n          return const SizedBox.shrink();\n        }\n\n        if (filter is DateTimeFilter?) {\n          return _DateTimeFilterConditionList(\n            onSelect: (filter) {\n              if (filter.fieldType == FieldType.DateTime) {\n                context.read<MobileFilterEditorCubit>().updateFilter(filter);\n              } else {\n                onSelect(filter);\n              }\n            },\n          );\n        }\n\n        final conditions =\n            FilterCondition.fromFieldType(filter.fieldType).conditions;\n\n        return Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            const VSpace(4.0),\n            Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 16.0),\n              child: FlowyText(\n                LocaleKeys.grid_filter_conditon.tr().toUpperCase(),\n                fontSize: 13,\n                color: Theme.of(context).hintColor,\n              ),\n            ),\n            const VSpace(4.0),\n            const Divider(\n              height: 0.5,\n              thickness: 0.5,\n            ),\n            Expanded(\n              child: ListView.builder(\n                itemCount: conditions.length,\n                itemBuilder: (context, index) {\n                  return FlowyOptionTile.checkbox(\n                    text: conditions[index].$2,\n                    showTopBorder: false,\n                    isSelected: _isSelected(filter, conditions[index].$1),\n                    onTap: () {\n                      final newFilter =\n                          _updateCondition(filter, conditions[index].$1);\n                      onSelect(newFilter);\n                    },\n                  );\n                },\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  bool _isSelected(DatabaseFilter filter, ProtobufEnum condition) {\n    return switch (filter.fieldType) {\n      FieldType.RichText ||\n      FieldType.URL =>\n        (filter as TextFilter).condition == condition,\n      FieldType.Number => (filter as NumberFilter).condition == condition,\n      FieldType.SingleSelect ||\n      FieldType.MultiSelect =>\n        (filter as SelectOptionFilter).condition == condition,\n      FieldType.Checkbox => (filter as CheckboxFilter).condition == condition,\n      FieldType.Checklist => (filter as ChecklistFilter).condition == condition,\n      _ => false,\n    };\n  }\n\n  DatabaseFilter _updateCondition(\n    DatabaseFilter filter,\n    ProtobufEnum condition,\n  ) {\n    return switch (filter.fieldType) {\n      FieldType.RichText || FieldType.URL => (filter as TextFilter)\n          .copyWith(condition: condition as TextFilterConditionPB),\n      FieldType.Number => (filter as NumberFilter)\n          .copyWith(condition: condition as NumberFilterConditionPB),\n      FieldType.SingleSelect ||\n      FieldType.MultiSelect =>\n        (filter as SelectOptionFilter)\n            .copyWith(condition: condition as SelectOptionFilterConditionPB),\n      FieldType.Checkbox => (filter as CheckboxFilter)\n          .copyWith(condition: condition as CheckboxFilterConditionPB),\n      FieldType.Checklist => (filter as ChecklistFilter)\n          .copyWith(condition: condition as ChecklistFilterConditionPB),\n      _ => filter,\n    };\n  }\n}\n\nclass _DateTimeFilterConditionList extends StatelessWidget {\n  const _DateTimeFilterConditionList({\n    required this.onSelect,\n  });\n\n  final void Function(DatabaseFilter) onSelect;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<MobileFilterEditorCubit, MobileFilterEditorState>(\n      builder: (context, state) {\n        return state.maybeWhen(\n          orElse: () => const SizedBox.shrink(),\n          editCondition: (filterId, newFilter, _) {\n            final filter = newFilter as DateTimeFilter;\n            final conditions =\n                DateTimeFilterCondition.availableConditionsForFieldType(\n              filter.fieldType,\n            );\n            return Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                const VSpace(4.0),\n                if (filter.fieldType == FieldType.DateTime)\n                  _DateTimeFilterIsStartSelector(\n                    isStart: filter.condition.isStart,\n                    onSelect: (newValue) {\n                      final newFilter = filter.copyWithCondition(\n                        isStart: newValue,\n                        condition: filter.condition.toCondition(),\n                      );\n                      onSelect(newFilter);\n                    },\n                  ),\n                Padding(\n                  padding: const EdgeInsets.symmetric(horizontal: 16.0),\n                  child: FlowyText(\n                    LocaleKeys.grid_filter_conditon.tr().toUpperCase(),\n                    fontSize: 13,\n                    color: Theme.of(context).hintColor,\n                  ),\n                ),\n                const VSpace(4.0),\n                const Divider(\n                  height: 0.5,\n                  thickness: 0.5,\n                ),\n                Expanded(\n                  child: ListView.builder(\n                    itemCount: conditions.length,\n                    itemBuilder: (context, index) {\n                      return FlowyOptionTile.checkbox(\n                        text: conditions[index].filterName,\n                        showTopBorder: false,\n                        isSelected:\n                            filter.condition.toCondition() == conditions[index],\n                        onTap: () {\n                          final newFilter = filter.copyWithCondition(\n                            isStart: filter.condition.isStart,\n                            condition: conditions[index],\n                          );\n                          onSelect(newFilter);\n                        },\n                      );\n                    },\n                  ),\n                ),\n              ],\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass _DateTimeFilterIsStartSelector extends StatelessWidget {\n  const _DateTimeFilterIsStartSelector({\n    required this.isStart,\n    required this.onSelect,\n  });\n\n  final bool isStart;\n  final void Function(bool isStart) onSelect;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.fromLTRB(16, 0, 16, 20),\n      child: DefaultTabController(\n        length: 2,\n        initialIndex: isStart ? 0 : 1,\n        child: Container(\n          padding: const EdgeInsets.all(3.0),\n          decoration: BoxDecoration(\n            borderRadius: BorderRadius.circular(10),\n            color: Theme.of(context).hoverColor,\n          ),\n          child: TabBar(\n            indicatorSize: TabBarIndicatorSize.label,\n            labelPadding: EdgeInsets.zero,\n            padding: EdgeInsets.zero,\n            indicatorWeight: 0,\n            indicator: BoxDecoration(\n              borderRadius: BorderRadius.circular(10),\n              color: Theme.of(context).colorScheme.surface,\n            ),\n            splashFactory: NoSplash.splashFactory,\n            overlayColor: const WidgetStatePropertyAll(\n              Colors.transparent,\n            ),\n            onTap: (index) => onSelect(index == 0),\n            tabs: [\n              _tab(LocaleKeys.grid_dateFilter_startDate.tr()),\n              _tab(LocaleKeys.grid_dateFilter_endDate.tr()),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Tab _tab(String name) {\n    return Tab(\n      height: 34,\n      child: Center(\n        child: FlowyText(\n          name,\n          fontSize: 14,\n        ),\n      ),\n    );\n  }\n}\n\nclass _FilterContentEditor extends StatelessWidget {\n  const _FilterContentEditor({\n    required this.filter,\n    required this.onUpdateFilter,\n  });\n\n  final DatabaseFilter filter;\n  final void Function(DatabaseFilter) onUpdateFilter;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterEditorBloc, FilterEditorState>(\n      builder: (context, state) {\n        final field = state.fields\n            .firstWhereOrNull((field) => field.id == filter.fieldId);\n        if (field == null) return const SizedBox.shrink();\n        return switch (field.fieldType) {\n          FieldType.SingleSelect ||\n          FieldType.MultiSelect =>\n            _SelectOptionFilterContentEditor(\n              filter: filter as SelectOptionFilter,\n              field: field,\n            ),\n          FieldType.CreatedTime ||\n          FieldType.LastEditedTime ||\n          FieldType.DateTime =>\n            _DateTimeFilterContentEditor(filter: filter as DateTimeFilter),\n          _ => const SizedBox.shrink(),\n        };\n      },\n    );\n  }\n}\n\nclass _SelectOptionFilterContentEditor extends StatefulWidget {\n  _SelectOptionFilterContentEditor({\n    required this.filter,\n    required this.field,\n  }) : delegate = filter.makeDelegate(field);\n\n  final SelectOptionFilter filter;\n  final FieldInfo field;\n  final SelectOptionFilterDelegate delegate;\n\n  @override\n  State<_SelectOptionFilterContentEditor> createState() =>\n      _SelectOptionFilterContentEditorState();\n}\n\nclass _SelectOptionFilterContentEditorState\n    extends State<_SelectOptionFilterContentEditor> {\n  final TextEditingController textController = TextEditingController();\n  String filterText = \"\";\n  final List<SelectOptionPB> options = [];\n\n  @override\n  void initState() {\n    super.initState();\n    options.addAll(widget.delegate.getOptions(widget.field));\n  }\n\n  @override\n  void dispose() {\n    textController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        const Divider(\n          height: 0.5,\n          thickness: 0.5,\n        ),\n        Padding(\n          padding: const EdgeInsets.all(16.0),\n          child: FlowyMobileSearchTextField(\n            controller: textController,\n            onChanged: (text) {\n              if (textController.value.composing.isCollapsed) {\n                setState(() {\n                  filterText = text;\n                  filterOptions();\n                });\n              }\n            },\n            onSubmitted: (_) {},\n            hintText: LocaleKeys.grid_selectOption_searchOption.tr(),\n          ),\n        ),\n        Expanded(\n          child: ListView.separated(\n            padding: const EdgeInsets.symmetric(horizontal: 16.0),\n            separatorBuilder: (context, index) => const VSpace(20),\n            itemCount: options.length,\n            itemBuilder: (context, index) {\n              return MobileSelectOption(\n                option: options[index],\n                isSelected: widget.filter.optionIds.contains(options[index].id),\n                onTap: (isSelected) {\n                  _onTapHandler(\n                    context,\n                    options,\n                    options[index],\n                    isSelected,\n                  );\n                },\n                indicator: MobileSelectedOptionIndicator.multi,\n                showMoreOptionsButton: false,\n              );\n            },\n          ),\n        ),\n      ],\n    );\n  }\n\n  void filterOptions() {\n    options\n      ..clear()\n      ..addAll(widget.delegate.getOptions(widget.field));\n\n    if (filterText.isNotEmpty) {\n      options.retainWhere((option) {\n        final name = option.name.toLowerCase();\n        final lFilter = filterText.toLowerCase();\n        return name.contains(lFilter);\n      });\n    }\n  }\n\n  void _onTapHandler(\n    BuildContext context,\n    List<SelectOptionPB> options,\n    SelectOptionPB option,\n    bool isSelected,\n  ) {\n    final selectedOptionIds = Set<String>.from(widget.filter.optionIds);\n    if (isSelected) {\n      selectedOptionIds.remove(option.id);\n    } else {\n      selectedOptionIds.add(option.id);\n    }\n    _updateSelectOptions(context, options, selectedOptionIds);\n  }\n\n  void _updateSelectOptions(\n    BuildContext context,\n    List<SelectOptionPB> options,\n    Set<String> selectedOptionIds,\n  ) {\n    final optionIds =\n        options.map((e) => e.id).where(selectedOptionIds.contains).toList();\n    final newFilter = widget.filter.copyWith(optionIds: optionIds);\n    context.read<MobileFilterEditorCubit>().updateFilter(newFilter);\n  }\n}\n\nclass _DateTimeFilterContentEditor extends StatefulWidget {\n  const _DateTimeFilterContentEditor({\n    required this.filter,\n  });\n\n  final DateTimeFilter filter;\n\n  @override\n  State<_DateTimeFilterContentEditor> createState() =>\n      _DateTimeFilterContentEditorState();\n}\n\nclass _DateTimeFilterContentEditorState\n    extends State<_DateTimeFilterContentEditor> {\n  late DateTime focusedDay;\n\n  bool get isRange => widget.filter.condition.isRange;\n\n  @override\n  void initState() {\n    super.initState();\n    focusedDay = (isRange ? widget.filter.start : widget.filter.timestamp) ??\n        DateTime.now();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MobileDatePicker(\n      isRange: isRange,\n      selectedDay: isRange ? widget.filter.start : widget.filter.timestamp,\n      startDay: isRange ? widget.filter.start : null,\n      endDay: isRange ? widget.filter.end : null,\n      focusedDay: focusedDay,\n      onDaySelected: (selectedDay) {\n        final newFilter = isRange\n            ? widget.filter.copyWithRange(start: selectedDay, end: null)\n            : widget.filter.copyWithTimestamp(timestamp: selectedDay);\n        context.read<MobileFilterEditorCubit>().updateFilter(newFilter);\n      },\n      onRangeSelected: (start, end) {\n        final newFilter = widget.filter.copyWithRange(\n          start: start,\n          end: end,\n        );\n        context.read<MobileFilterEditorCubit>().updateFilter(newFilter);\n      },\n      onPageChanged: (focusedDay) {\n        setState(() => this.focusedDay = focusedDay);\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_filter_bottom_sheet_cubit.dart",
    "content": "import 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'database_filter_bottom_sheet_cubit.freezed.dart';\n\nclass MobileFilterEditorCubit extends Cubit<MobileFilterEditorState> {\n  MobileFilterEditorCubit({\n    required this.pageController,\n  }) : super(MobileFilterEditorState.overview());\n\n  final PageController pageController;\n\n  void returnToOverview({bool scrollToBottom = false}) {\n    _animateToPage(0);\n    emit(MobileFilterEditorState.overview(scrollToBottom: scrollToBottom));\n  }\n\n  void startCreatingFilter() {\n    _animateToPage(1);\n    emit(MobileFilterEditorState.create());\n  }\n\n  void startEditingFilterField(String filterId) {\n    _animateToPage(1);\n    emit(MobileFilterEditorState.editField(filterId: filterId));\n  }\n\n  void updateFilter(DatabaseFilter filter) {\n    emit(\n      state.maybeWhen(\n        editCondition: (filterId, newFilter, showSave) =>\n            MobileFilterEditorState.editCondition(\n          filterId: filterId,\n          newFilter: filter,\n          showSave: showSave,\n        ),\n        editContent: (filterId, _) => MobileFilterEditorState.editContent(\n          filterId: filterId,\n          newFilter: filter,\n        ),\n        orElse: () => state,\n      ),\n    );\n  }\n\n  void startEditingFilterCondition(\n    String filterId,\n    DatabaseFilter filter,\n    bool showSave,\n  ) {\n    _animateToPage(1);\n    emit(\n      MobileFilterEditorState.editCondition(\n        filterId: filterId,\n        newFilter: filter,\n        showSave: showSave,\n      ),\n    );\n  }\n\n  void startEditingFilterContent(String filterId, DatabaseFilter filter) {\n    _animateToPage(1);\n    emit(\n      MobileFilterEditorState.editContent(\n        filterId: filterId,\n        newFilter: filter,\n      ),\n    );\n  }\n\n  Future<void> _animateToPage(int page) async {\n    return pageController.animateToPage(\n      page,\n      duration: const Duration(milliseconds: 150),\n      curve: Curves.easeOut,\n    );\n  }\n}\n\n@freezed\nclass MobileFilterEditorState with _$MobileFilterEditorState {\n  factory MobileFilterEditorState.overview({\n    @Default(false) bool scrollToBottom,\n  }) = _OverviewState;\n\n  factory MobileFilterEditorState.create() = _CreateState;\n\n  factory MobileFilterEditorState.editField({\n    required String filterId,\n  }) = _EditFieldState;\n\n  factory MobileFilterEditorState.editCondition({\n    required String filterId,\n    required DatabaseFilter newFilter,\n    required bool showSave,\n  }) = _EditConditionState;\n\n  factory MobileFilterEditorState.editContent({\n    required String filterId,\n    required DatabaseFilter newFilter,\n  }) = _EditContentState;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_filter_condition_list.dart",
    "content": "import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checklist.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/number.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\n\nabstract class FilterCondition<C> {\n  static FilterCondition fromFieldType(FieldType fieldType) {\n    return switch (fieldType) {\n      FieldType.RichText || FieldType.URL => TextFilterCondition().as(),\n      FieldType.Number => NumberFilterCondition().as(),\n      FieldType.Checkbox => CheckboxFilterCondition().as(),\n      FieldType.Checklist => ChecklistFilterCondition().as(),\n      FieldType.SingleSelect => SingleSelectOptionFilterCondition().as(),\n      FieldType.MultiSelect => MultiSelectOptionFilterCondition().as(),\n      _ => MultiSelectOptionFilterCondition().as(),\n    };\n  }\n\n  List<(C, String)> get conditions;\n}\n\nmixin _GenericCastHelper {\n  FilterCondition<T> as<T>() => this as FilterCondition<T>;\n}\n\nfinal class TextFilterCondition\n    with _GenericCastHelper\n    implements FilterCondition<TextFilterConditionPB> {\n  @override\n  List<(TextFilterConditionPB, String)> get conditions {\n    return [\n      TextFilterConditionPB.TextContains,\n      TextFilterConditionPB.TextDoesNotContain,\n      TextFilterConditionPB.TextIs,\n      TextFilterConditionPB.TextIsNot,\n      TextFilterConditionPB.TextStartsWith,\n      TextFilterConditionPB.TextEndsWith,\n      TextFilterConditionPB.TextIsEmpty,\n      TextFilterConditionPB.TextIsNotEmpty,\n    ].map((e) => (e, e.filterName)).toList();\n  }\n}\n\nfinal class NumberFilterCondition\n    with _GenericCastHelper\n    implements FilterCondition<NumberFilterConditionPB> {\n  @override\n  List<(NumberFilterConditionPB, String)> get conditions {\n    return [\n      NumberFilterConditionPB.Equal,\n      NumberFilterConditionPB.NotEqual,\n      NumberFilterConditionPB.LessThan,\n      NumberFilterConditionPB.LessThanOrEqualTo,\n      NumberFilterConditionPB.GreaterThan,\n      NumberFilterConditionPB.GreaterThanOrEqualTo,\n      NumberFilterConditionPB.NumberIsEmpty,\n      NumberFilterConditionPB.NumberIsNotEmpty,\n    ].map((e) => (e, e.filterName)).toList();\n  }\n}\n\nfinal class CheckboxFilterCondition\n    with _GenericCastHelper\n    implements FilterCondition<CheckboxFilterConditionPB> {\n  @override\n  List<(CheckboxFilterConditionPB, String)> get conditions {\n    return [\n      CheckboxFilterConditionPB.IsChecked,\n      CheckboxFilterConditionPB.IsUnChecked,\n    ].map((e) => (e, e.filterName)).toList();\n  }\n}\n\nfinal class ChecklistFilterCondition\n    with _GenericCastHelper\n    implements FilterCondition<ChecklistFilterConditionPB> {\n  @override\n  List<(ChecklistFilterConditionPB, String)> get conditions {\n    return [\n      ChecklistFilterConditionPB.IsComplete,\n      ChecklistFilterConditionPB.IsIncomplete,\n    ].map((e) => (e, e.filterName)).toList();\n  }\n}\n\nfinal class SingleSelectOptionFilterCondition\n    with _GenericCastHelper\n    implements FilterCondition<SelectOptionFilterConditionPB> {\n  @override\n  List<(SelectOptionFilterConditionPB, String)> get conditions {\n    return [\n      SelectOptionFilterConditionPB.OptionIs,\n      SelectOptionFilterConditionPB.OptionIsNot,\n      SelectOptionFilterConditionPB.OptionIsEmpty,\n      SelectOptionFilterConditionPB.OptionIsNotEmpty,\n    ].map((e) => (e, e.i18n)).toList();\n  }\n}\n\nfinal class MultiSelectOptionFilterCondition\n    with _GenericCastHelper\n    implements FilterCondition<SelectOptionFilterConditionPB> {\n  @override\n  List<(SelectOptionFilterConditionPB, String)> get conditions {\n    return [\n      SelectOptionFilterConditionPB.OptionContains,\n      SelectOptionFilterConditionPB.OptionDoesNotContain,\n      SelectOptionFilterConditionPB.OptionIsEmpty,\n      SelectOptionFilterConditionPB.OptionIsNotEmpty,\n    ].map((e) => (e, e.i18n)).toList();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/sort_entities.dart';\nimport 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:fluttertoast/fluttertoast.dart';\n\nimport 'database_sort_bottom_sheet_cubit.dart';\n\nclass MobileSortEditor extends StatefulWidget {\n  const MobileSortEditor({\n    super.key,\n  });\n\n  @override\n  State<MobileSortEditor> createState() => _MobileSortEditorState();\n}\n\nclass _MobileSortEditorState extends State<MobileSortEditor> {\n  final PageController _pageController = PageController();\n\n  @override\n  void dispose() {\n    _pageController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => MobileSortEditorCubit(\n        pageController: _pageController,\n      ),\n      child: Column(\n        children: [\n          const _Header(),\n          SizedBox(\n            height: 400, //314,\n            child: PageView.builder(\n              controller: _pageController,\n              itemCount: 2,\n              physics: const NeverScrollableScrollPhysics(),\n              itemBuilder: (context, index) {\n                return index == 0\n                    ? Padding(\n                        padding: EdgeInsets.only(\n                          bottom: MediaQuery.of(context).padding.bottom,\n                        ),\n                        child: const _Overview(),\n                      )\n                    : const _SortDetail();\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _Header extends StatelessWidget {\n  const _Header();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<MobileSortEditorCubit, MobileSortEditorState>(\n      builder: (context, state) {\n        return SizedBox(\n          height: 44.0,\n          child: Stack(\n            children: [\n              if (state.showBackButton)\n                Align(\n                  alignment: Alignment.centerLeft,\n                  child: AppBarBackButton(\n                    padding: const EdgeInsets.symmetric(\n                      vertical: 12,\n                      horizontal: 16,\n                    ),\n                    onTap: () => context\n                        .read<MobileSortEditorCubit>()\n                        .returnToOverview(),\n                  ),\n                ),\n              Align(\n                child: FlowyText.medium(\n                  LocaleKeys.grid_settings_sort.tr(),\n                  fontSize: 16.0,\n                ),\n              ),\n              if (state.isCreatingNewSort)\n                Align(\n                  alignment: Alignment.centerRight,\n                  child: AppBarSaveButton(\n                    padding: const EdgeInsets.symmetric(\n                      vertical: 12,\n                      horizontal: 16,\n                    ),\n                    enable: state.newSortFieldId != null,\n                    onTap: () {\n                      _tryCreateSort(context, state);\n                      context.read<MobileSortEditorCubit>().returnToOverview();\n                    },\n                  ),\n                ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  void _tryCreateSort(BuildContext context, MobileSortEditorState state) {\n    if (state.newSortFieldId != null && state.newSortCondition != null) {\n      context.read<SortEditorBloc>().add(\n            SortEditorEvent.createSort(\n              fieldId: state.newSortFieldId!,\n              condition: state.newSortCondition!,\n            ),\n          );\n    }\n  }\n}\n\nclass _Overview extends StatelessWidget {\n  const _Overview();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SortEditorBloc, SortEditorState>(\n      builder: (context, state) {\n        return Column(\n          children: [\n            Expanded(\n              child: state.sorts.isEmpty\n                  ? Center(\n                      child: Column(\n                        mainAxisSize: MainAxisSize.min,\n                        children: [\n                          FlowySvg(\n                            FlowySvgs.sort_descending_s,\n                            size: const Size.square(60),\n                            color: Theme.of(context).hintColor,\n                          ),\n                          FlowyText(\n                            LocaleKeys.grid_sort_empty.tr(),\n                            color: Theme.of(context).hintColor,\n                          ),\n                        ],\n                      ),\n                    )\n                  : ReorderableListView.builder(\n                      padding: const EdgeInsets.symmetric(\n                        horizontal: 16,\n                      ),\n                      proxyDecorator: (child, index, animation) => Material(\n                        color: Colors.transparent,\n                        child: child,\n                      ),\n                      onReorder: (oldIndex, newIndex) => context\n                          .read<SortEditorBloc>()\n                          .add(SortEditorEvent.reorderSort(oldIndex, newIndex)),\n                      itemCount: state.sorts.length,\n                      itemBuilder: (context, index) => _SortItem(\n                        key: ValueKey(\"sort_item_$index\"),\n                        sort: state.sorts[index],\n                      ),\n                    ),\n            ),\n            Container(\n              height: 44,\n              width: double.infinity,\n              margin: const EdgeInsets.symmetric(horizontal: 14),\n              decoration: BoxDecoration(\n                border: Border.fromBorderSide(\n                  BorderSide(\n                    width: 0.5,\n                    color: Theme.of(context).dividerColor,\n                  ),\n                ),\n                borderRadius: Corners.s10Border,\n              ),\n              child: InkWell(\n                onTap: () {\n                  final firstField = context\n                      .read<SortEditorBloc>()\n                      .state\n                      .creatableFields\n                      .firstOrNull;\n                  if (firstField == null) {\n                    Fluttertoast.showToast(\n                      msg: LocaleKeys.grid_sort_cannotFindCreatableField.tr(),\n                      gravity: ToastGravity.BOTTOM,\n                    );\n                  } else {\n                    context.read<MobileSortEditorCubit>().startCreatingSort();\n                  }\n                },\n                borderRadius: Corners.s10Border,\n                child: Center(\n                  child: Row(\n                    mainAxisSize: MainAxisSize.min,\n                    children: [\n                      const FlowySvg(\n                        FlowySvgs.add_s,\n                        size: Size.square(16),\n                      ),\n                      const HSpace(6.0),\n                      FlowyText(\n                        LocaleKeys.grid_sort_addSort.tr(),\n                        fontSize: 15,\n                      ),\n                    ],\n                  ),\n                ),\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n\nclass _SortItem extends StatelessWidget {\n  const _SortItem({super.key, required this.sort});\n\n  final DatabaseSort sort;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      margin: const EdgeInsets.symmetric(\n        vertical: 4.0,\n      ),\n      decoration: BoxDecoration(\n        color: Theme.of(context).hoverColor,\n        borderRadius: BorderRadius.circular(12),\n      ),\n      child: Stack(\n        children: [\n          GestureDetector(\n            behavior: HitTestBehavior.opaque,\n            onTap: () => context\n                .read<MobileSortEditorCubit>()\n                .startEditingSort(sort.sortId),\n            child: Padding(\n              padding: const EdgeInsets.symmetric(\n                vertical: 14,\n                horizontal: 8,\n              ),\n              child: Column(\n                mainAxisSize: MainAxisSize.min,\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  Padding(\n                    padding: const EdgeInsets.symmetric(horizontal: 12.0),\n                    child: FlowyText.medium(\n                      LocaleKeys.grid_sort_by.tr(),\n                      fontSize: 15,\n                    ),\n                  ),\n                  const VSpace(10),\n                  Row(\n                    children: [\n                      Expanded(\n                        child: Container(\n                          height: 44,\n                          decoration: BoxDecoration(\n                            border: Border.fromBorderSide(\n                              BorderSide(\n                                width: 0.5,\n                                color: Theme.of(context).dividerColor,\n                              ),\n                            ),\n                            borderRadius: Corners.s10Border,\n                            color: Theme.of(context).colorScheme.surface,\n                          ),\n                          padding: const EdgeInsets.symmetric(horizontal: 12),\n                          child: Center(\n                            child: Row(\n                              children: [\n                                Expanded(\n                                  child: BlocSelector<SortEditorBloc,\n                                      SortEditorState, FieldInfo?>(\n                                    selector: (state) =>\n                                        state.allFields.firstWhereOrNull(\n                                      (field) => field.id == sort.fieldId,\n                                    ),\n                                    builder: (context, field) {\n                                      return FlowyText(\n                                        field?.name ?? \"\",\n                                        overflow: TextOverflow.ellipsis,\n                                      );\n                                    },\n                                  ),\n                                ),\n                                const HSpace(6.0),\n                                FlowySvg(\n                                  FlowySvgs.icon_right_small_ccm_outlined_s,\n                                  size: const Size.square(14),\n                                  color: Theme.of(context).hintColor,\n                                ),\n                              ],\n                            ),\n                          ),\n                        ),\n                      ),\n                      const HSpace(6),\n                      Expanded(\n                        child: Container(\n                          height: 44,\n                          decoration: BoxDecoration(\n                            border: Border.fromBorderSide(\n                              BorderSide(\n                                width: 0.5,\n                                color: Theme.of(context).dividerColor,\n                              ),\n                            ),\n                            borderRadius: Corners.s10Border,\n                            color: Theme.of(context).colorScheme.surface,\n                          ),\n                          padding: const EdgeInsetsDirectional.only(\n                            start: 12,\n                            end: 10,\n                          ),\n                          child: Center(\n                            child: Row(\n                              children: [\n                                Expanded(\n                                  child: FlowyText(\n                                    sort.condition.name,\n                                  ),\n                                ),\n                                const HSpace(6.0),\n                                FlowySvg(\n                                  FlowySvgs.icon_right_small_ccm_outlined_s,\n                                  size: const Size.square(14),\n                                  color: Theme.of(context).hintColor,\n                                ),\n                              ],\n                            ),\n                          ),\n                        ),\n                      ),\n                    ],\n                  ),\n                ],\n              ),\n            ),\n          ),\n          Positioned(\n            right: 8,\n            top: 6,\n            child: InkWell(\n              onTap: () => context\n                  .read<SortEditorBloc>()\n                  .add(SortEditorEvent.deleteSort(sort.sortId)),\n              // steal from the container LongClickReorderWidget thing\n              onLongPress: () {},\n              borderRadius: BorderRadius.circular(10),\n              child: SizedBox.square(\n                dimension: 34,\n                child: Center(\n                  child: FlowySvg(\n                    FlowySvgs.trash_m,\n                    size: const Size.square(18),\n                    color: Theme.of(context).hintColor,\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _SortDetail extends StatelessWidget {\n  const _SortDetail();\n\n  @override\n  Widget build(BuildContext context) {\n    final isCreatingNewSort =\n        context.read<MobileSortEditorCubit>().state.isCreatingNewSort;\n\n    return isCreatingNewSort\n        ? const _SortDetailContent()\n        : BlocSelector<SortEditorBloc, SortEditorState, DatabaseSort>(\n            selector: (state) => state.sorts.firstWhere(\n              (sort) =>\n                  sort.sortId ==\n                  context.read<MobileSortEditorCubit>().state.editingSortId,\n            ),\n            builder: (context, sort) {\n              return _SortDetailContent(sort: sort);\n            },\n          );\n  }\n}\n\nclass _SortDetailContent extends StatelessWidget {\n  const _SortDetailContent({\n    this.sort,\n  });\n\n  final DatabaseSort? sort;\n\n  bool get isCreatingNewSort => sort == null;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        const VSpace(4),\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 16),\n          child: DefaultTabController(\n            length: 2,\n            initialIndex: isCreatingNewSort\n                ? 0\n                : sort!.condition == SortConditionPB.Ascending\n                    ? 0\n                    : 1,\n            child: Container(\n              padding: const EdgeInsets.all(3.0),\n              decoration: BoxDecoration(\n                borderRadius: BorderRadius.circular(10),\n                color: Theme.of(context).hoverColor,\n              ),\n              child: TabBar(\n                indicatorSize: TabBarIndicatorSize.label,\n                labelPadding: EdgeInsets.zero,\n                padding: EdgeInsets.zero,\n                indicatorWeight: 0,\n                indicator: BoxDecoration(\n                  borderRadius: BorderRadius.circular(10),\n                  color: Theme.of(context).colorScheme.surface,\n                ),\n                splashFactory: NoSplash.splashFactory,\n                overlayColor: const WidgetStatePropertyAll(\n                  Colors.transparent,\n                ),\n                onTap: (index) {\n                  final newCondition = index == 0\n                      ? SortConditionPB.Ascending\n                      : SortConditionPB.Descending;\n                  _changeCondition(context, newCondition);\n                },\n                tabs: [\n                  Tab(\n                    height: 34,\n                    child: Center(\n                      child: FlowyText(\n                        LocaleKeys.grid_sort_ascending.tr(),\n                        fontSize: 14,\n                      ),\n                    ),\n                  ),\n                  Tab(\n                    height: 34,\n                    child: Center(\n                      child: FlowyText(\n                        LocaleKeys.grid_sort_descending.tr(),\n                        fontSize: 14,\n                      ),\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n        const VSpace(20),\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 16.0),\n          child: FlowyText(\n            LocaleKeys.grid_settings_sortBy.tr().toUpperCase(),\n            fontSize: 13,\n            color: Theme.of(context).hintColor,\n          ),\n        ),\n        const VSpace(4.0),\n        const Divider(\n          height: 0.5,\n          thickness: 0.5,\n        ),\n        Expanded(\n          child: BlocBuilder<SortEditorBloc, SortEditorState>(\n            builder: (context, state) {\n              final fields = state.allFields\n                  .where((field) => field.fieldType.canCreateSort)\n                  .toList();\n              return ListView.builder(\n                itemCount: fields.length,\n                itemBuilder: (context, index) {\n                  final fieldInfo = fields[index];\n                  final isSelected = isCreatingNewSort\n                      ? context\n                              .watch<MobileSortEditorCubit>()\n                              .state\n                              .newSortFieldId ==\n                          fieldInfo.id\n                      : sort!.fieldId == fieldInfo.id;\n\n                  final canSort =\n                      fieldInfo.fieldType.canCreateSort && !fieldInfo.hasSort;\n                  final beingEdited =\n                      !isCreatingNewSort && sort!.fieldId == fieldInfo.id;\n                  final enabled = canSort || beingEdited;\n\n                  return FlowyOptionTile.checkbox(\n                    text: fieldInfo.field.name,\n                    leftIcon: FieldIcon(\n                      fieldInfo: fieldInfo,\n                    ),\n                    isSelected: isSelected,\n                    textColor: enabled ? null : Theme.of(context).disabledColor,\n                    showTopBorder: false,\n                    onTap: () {\n                      if (isSelected) {\n                        return;\n                      }\n                      if (enabled) {\n                        _changeFieldId(context, fieldInfo.id);\n                      } else {\n                        Fluttertoast.showToast(\n                          msg: LocaleKeys.grid_sort_fieldInUse.tr(),\n                          gravity: ToastGravity.BOTTOM,\n                        );\n                      }\n                    },\n                  );\n                },\n              );\n            },\n          ),\n        ),\n      ],\n    );\n  }\n\n  void _changeCondition(BuildContext context, SortConditionPB newCondition) {\n    if (isCreatingNewSort) {\n      context.read<MobileSortEditorCubit>().changeSortCondition(newCondition);\n    } else {\n      context.read<SortEditorBloc>().add(\n            SortEditorEvent.editSort(\n              sortId: sort!.sortId,\n              condition: newCondition,\n            ),\n          );\n    }\n  }\n\n  void _changeFieldId(BuildContext context, String newFieldId) {\n    if (isCreatingNewSort) {\n      context.read<MobileSortEditorCubit>().changeFieldId(newFieldId);\n    } else {\n      context.read<SortEditorBloc>().add(\n            SortEditorEvent.editSort(\n              sortId: sort!.sortId,\n              fieldId: newFieldId,\n            ),\n          );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet_cubit.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'database_sort_bottom_sheet_cubit.freezed.dart';\n\nclass MobileSortEditorCubit extends Cubit<MobileSortEditorState> {\n  MobileSortEditorCubit({\n    required this.pageController,\n  }) : super(MobileSortEditorState.initial());\n\n  final PageController pageController;\n\n  void returnToOverview() {\n    _animateToPage(0);\n    emit(MobileSortEditorState.initial());\n  }\n\n  void startCreatingSort() {\n    _animateToPage(1);\n    emit(\n      state.copyWith(\n        showBackButton: true,\n        isCreatingNewSort: true,\n        newSortCondition: SortConditionPB.Ascending,\n      ),\n    );\n  }\n\n  void startEditingSort(String sortId) {\n    _animateToPage(1);\n    emit(\n      state.copyWith(\n        showBackButton: true,\n        editingSortId: sortId,\n      ),\n    );\n  }\n\n  /// only used when creating a new sort\n  void changeFieldId(String fieldId) {\n    emit(state.copyWith(newSortFieldId: fieldId));\n  }\n\n  /// only used when creating a new sort\n  void changeSortCondition(SortConditionPB condition) {\n    emit(state.copyWith(newSortCondition: condition));\n  }\n\n  Future<void> _animateToPage(int page) async {\n    return pageController.animateToPage(\n      page,\n      duration: const Duration(milliseconds: 150),\n      curve: Curves.easeOut,\n    );\n  }\n}\n\n@freezed\nclass MobileSortEditorState with _$MobileSortEditorState {\n  factory MobileSortEditorState({\n    required bool showBackButton,\n    required String? editingSortId,\n    required bool isCreatingNewSort,\n    required String? newSortFieldId,\n    required SortConditionPB? newSortCondition,\n  }) = _MobileSortEditorState;\n\n  factory MobileSortEditorState.initial() => MobileSortEditorState(\n        showBackButton: false,\n        editingSortId: null,\n        isCreatingNewSort: false,\n        newSortFieldId: null,\n        newSortCondition: null,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_layout.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/calendar/application/calendar_setting_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../field/mobile_field_bottom_sheets.dart';\n\n/// [DatabaseViewLayoutPicker] is seen when changing the layout type of a\n/// database view or creating a new database view.\nclass DatabaseViewLayoutPicker extends StatelessWidget {\n  const DatabaseViewLayoutPicker({\n    super.key,\n    required this.selectedLayout,\n    required this.onSelect,\n  });\n\n  final DatabaseLayoutPB selectedLayout;\n  final void Function(DatabaseLayoutPB layout) onSelect;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        _buildButton(DatabaseLayoutPB.Grid, true),\n        _buildButton(DatabaseLayoutPB.Board, false),\n        _buildButton(DatabaseLayoutPB.Calendar, false),\n      ],\n    );\n  }\n\n  Widget _buildButton(DatabaseLayoutPB layout, bool showTopBorder) {\n    return FlowyOptionTile.checkbox(\n      text: layout.layoutName,\n      leftIcon: FlowySvg(layout.icon, size: const Size.square(20)),\n      isSelected: selectedLayout == layout,\n      showTopBorder: showTopBorder,\n      onTap: () {\n        onSelect(layout);\n      },\n    );\n  }\n}\n\n/// [MobileCalendarViewLayoutSettings] is used when the database layout is\n/// calendar. It allows changing the field being used to layout the events,\n/// and which day of the week the calendar starts on.\nclass MobileCalendarViewLayoutSettings extends StatelessWidget {\n  const MobileCalendarViewLayoutSettings({\n    super.key,\n    required this.databaseController,\n  });\n\n  final DatabaseController databaseController;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<CalendarSettingBloc>(\n      create: (context) {\n        return CalendarSettingBloc(\n          databaseController: databaseController,\n        )..add(const CalendarSettingEvent.initial());\n      },\n      child: BlocBuilder<CalendarSettingBloc, CalendarSettingState>(\n        builder: (context, state) {\n          if (state.layoutSetting == null) {\n            return const SizedBox.shrink();\n          }\n          return Column(\n            mainAxisSize: MainAxisSize.min,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              _CalendarLayoutField(\n                context: context,\n                databaseController: databaseController,\n                selectedFieldId: state.layoutSetting?.fieldId,\n              ),\n              _divider(),\n              ..._startWeek(context, state.layoutSetting?.firstDayOfWeek),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  List<Widget> _startWeek(BuildContext context, int? firstDayOfWeek) {\n    final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;\n    return [\n      Padding(\n        padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 4.0),\n        child: FlowyText(\n          LocaleKeys.calendar_settings_firstDayOfWeek.tr().toUpperCase(),\n          fontSize: 13,\n          color: Theme.of(context).hintColor,\n        ),\n      ),\n      FlowyOptionTile.checkbox(\n        text: symbols.WEEKDAYS[0],\n        isSelected: firstDayOfWeek! == 0,\n        onTap: () {\n          context.read<CalendarSettingBloc>().add(\n                const CalendarSettingEvent.updateLayoutSetting(\n                  firstDayOfWeek: 0,\n                ),\n              );\n        },\n      ),\n      FlowyOptionTile.checkbox(\n        text: symbols.WEEKDAYS[1],\n        isSelected: firstDayOfWeek == 1,\n        showTopBorder: false,\n        onTap: () {\n          context.read<CalendarSettingBloc>().add(\n                const CalendarSettingEvent.updateLayoutSetting(\n                  firstDayOfWeek: 1,\n                ),\n              );\n        },\n      ),\n    ];\n  }\n\n  Widget _divider() => const VSpace(20);\n}\n\nclass _CalendarLayoutField extends StatelessWidget {\n  const _CalendarLayoutField({\n    required this.context,\n    required this.databaseController,\n    required this.selectedFieldId,\n  });\n\n  final BuildContext context;\n  final DatabaseController databaseController;\n  final String? selectedFieldId;\n\n  @override\n  Widget build(BuildContext context) {\n    FieldInfo? selectedField;\n    if (selectedFieldId != null) {\n      selectedField =\n          databaseController.fieldController.getField(selectedFieldId!);\n    }\n    return FlowyOptionTile.text(\n      text: LocaleKeys.calendar_settings_layoutDateField.tr(),\n      trailing: selectedFieldId == null\n          ? null\n          : Row(\n              children: [\n                FlowyText(\n                  selectedField!.name,\n                  color: Theme.of(context).hintColor,\n                ),\n                const HSpace(8),\n                const FlowySvg(FlowySvgs.arrow_right_s),\n              ],\n            ),\n      onTap: () async {\n        final newFieldId = await showFieldPicker(\n          context,\n          LocaleKeys.calendar_settings_changeLayoutDateField.tr(),\n          selectedFieldId,\n          databaseController.fieldController,\n          (field) => field.fieldType == FieldType.DateTime,\n        );\n        if (context.mounted &&\n            newFieldId != null &&\n            newFieldId != selectedFieldId) {\n          context.read<CalendarSettingBloc>().add(\n                CalendarSettingEvent.updateLayoutSetting(\n                  layoutFieldId: newFieldId,\n                ),\n              );\n        }\n      },\n    );\n  }\n}\n\nclass MobileBoardViewLayoutSettings extends StatelessWidget {\n  const MobileBoardViewLayoutSettings({\n    super.key,\n    required this.databaseController,\n  });\n\n  final DatabaseController databaseController;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyOptionTile.text(text: LocaleKeys.board_groupBy.tr());\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nimport 'database_view_layout.dart';\nimport 'database_view_quick_actions.dart';\n\n/// [MobileDatabaseViewList] shows a list of all the views in the database and\n/// adds a button to create a new database view.\nclass MobileDatabaseViewList extends StatelessWidget {\n  const MobileDatabaseViewList({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<ViewBloc, ViewState>(\n      builder: (context, state) {\n        final views = [state.view, ...state.view.childViews];\n\n        return Column(\n          children: [\n            _Header(\n              title: LocaleKeys.grid_settings_viewList.plural(\n                context.watch<DatabaseTabBarBloc>().state.tabBars.length,\n                namedArgs: {\n                  'count':\n                      '${context.watch<DatabaseTabBarBloc>().state.tabBars.length}',\n                },\n              ),\n              showBackButton: false,\n              useFilledDoneButton: false,\n              onDone: (context) => Navigator.pop(context),\n            ),\n            Expanded(\n              child: ListView(\n                shrinkWrap: true,\n                padding: EdgeInsets.zero,\n                children: [\n                  ...views.mapIndexed(\n                    (index, view) => MobileDatabaseViewListButton(\n                      view: view,\n                      showTopBorder: index == 0,\n                    ),\n                  ),\n                  const VSpace(20),\n                  const MobileNewDatabaseViewButton(),\n                  VSpace(\n                    context.bottomSheetPadding(ignoreViewPadding: false),\n                  ),\n                ],\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n\n/// Same header as the one in showMobileBottomSheet, but allows popping the\n/// sheet with a value.\nclass _Header extends StatelessWidget {\n  const _Header({\n    required this.title,\n    required this.showBackButton,\n    required this.useFilledDoneButton,\n    required this.onDone,\n  });\n\n  final String title;\n  final bool showBackButton;\n  final bool useFilledDoneButton;\n  final void Function(BuildContext context) onDone;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.only(bottom: 4.0),\n      child: SizedBox(\n        height: 44.0,\n        child: Stack(\n          children: [\n            if (showBackButton)\n              const Align(\n                alignment: Alignment.centerLeft,\n                child: AppBarBackButton(),\n              ),\n            Align(\n              child: FlowyText.medium(\n                title,\n                fontSize: 16.0,\n              ),\n            ),\n            useFilledDoneButton\n                ? Align(\n                    alignment: Alignment.centerRight,\n                    child: AppBarFilledDoneButton(\n                      onTap: () => onDone(context),\n                    ),\n                  )\n                : Align(\n                    alignment: Alignment.centerRight,\n                    child: AppBarDoneButton(\n                      onTap: () => onDone(context),\n                    ),\n                  ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\n@visibleForTesting\nclass MobileDatabaseViewListButton extends StatelessWidget {\n  const MobileDatabaseViewListButton({\n    super.key,\n    required this.view,\n    required this.showTopBorder,\n  });\n\n  final ViewPB view;\n  final bool showTopBorder;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(\n      builder: (context, state) {\n        final index =\n            state.tabBars.indexWhere((tabBar) => tabBar.viewId == view.id);\n        final isSelected = index == state.selectedIndex;\n        return FlowyOptionTile.text(\n          text: view.name,\n          onTap: () {\n            context\n                .read<DatabaseTabBarBloc>()\n                .add(DatabaseTabBarEvent.selectView(view.id));\n          },\n          leftIcon: _buildViewIconButton(context, view),\n          trailing: _trailing(\n            context,\n            state.tabBarControllerByViewId[view.id]!.controller,\n            isSelected,\n          ),\n          showTopBorder: showTopBorder,\n        );\n      },\n    );\n  }\n\n  Widget _buildViewIconButton(BuildContext context, ViewPB view) {\n    final iconData = view.icon.toEmojiIconData();\n    Widget icon;\n    if (iconData.isEmpty || iconData.type != FlowyIconType.icon) {\n      icon = view.defaultIcon();\n    } else {\n      icon = RawEmojiIconWidget(\n        emoji: iconData,\n        emojiSize: 14.0,\n        enableColor: false,\n      );\n    }\n    return SizedBox.square(\n      dimension: 20.0,\n      child: icon,\n    );\n  }\n\n  Widget _trailing(\n    BuildContext context,\n    DatabaseController databaseController,\n    bool isSelected,\n  ) {\n    final more = FlowyIconButton(\n      icon: FlowySvg(\n        FlowySvgs.three_dots_s,\n        size: const Size.square(20),\n        color: Theme.of(context).hintColor,\n      ),\n      onPressed: () {\n        showMobileBottomSheet(\n          context,\n          showDragHandle: true,\n          backgroundColor: AFThemeExtension.of(context).background,\n          builder: (_) {\n            return BlocProvider<ViewBloc>(\n              create: (_) =>\n                  ViewBloc(view: view)..add(const ViewEvent.initial()),\n              child: MobileDatabaseViewQuickActions(\n                view: view,\n                databaseController: databaseController,\n              ),\n            );\n          },\n        );\n      },\n    );\n    if (isSelected) {\n      return Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          const FlowySvg(\n            FlowySvgs.m_blue_check_s,\n            size: Size.square(20),\n            blendMode: BlendMode.dst,\n          ),\n          const HSpace(8),\n          more,\n        ],\n      );\n    } else {\n      return more;\n    }\n  }\n}\n\nclass MobileNewDatabaseViewButton extends StatelessWidget {\n  const MobileNewDatabaseViewButton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyOptionTile.text(\n      text: LocaleKeys.grid_settings_createView.tr(),\n      textColor: Theme.of(context).hintColor,\n      leftIcon: FlowySvg(\n        FlowySvgs.add_s,\n        size: const Size.square(20),\n        color: Theme.of(context).hintColor,\n      ),\n      onTap: () async {\n        final result = await showMobileBottomSheet<(DatabaseLayoutPB, String)>(\n          context,\n          showDragHandle: true,\n          builder: (_) {\n            return const MobileCreateDatabaseView();\n          },\n        );\n        if (context.mounted && result != null) {\n          context\n              .read<DatabaseTabBarBloc>()\n              .add(DatabaseTabBarEvent.createView(result.$1, result.$2));\n        }\n      },\n    );\n  }\n}\n\nclass MobileCreateDatabaseView extends StatefulWidget {\n  const MobileCreateDatabaseView({super.key});\n\n  @override\n  State<MobileCreateDatabaseView> createState() =>\n      _MobileCreateDatabaseViewState();\n}\n\nclass _MobileCreateDatabaseViewState extends State<MobileCreateDatabaseView> {\n  late final TextEditingController controller;\n  DatabaseLayoutPB layoutType = DatabaseLayoutPB.Grid;\n\n  @override\n  void initState() {\n    super.initState();\n    controller = TextEditingController(\n      text: LocaleKeys.grid_title_placeholder.tr(),\n    );\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        _Header(\n          title: LocaleKeys.grid_settings_createView.tr(),\n          showBackButton: true,\n          useFilledDoneButton: true,\n          onDone: (context) =>\n              context.pop((layoutType, controller.text.trim())),\n        ),\n        FlowyOptionTile.textField(\n          autofocus: true,\n          controller: controller,\n        ),\n        const VSpace(20),\n        DatabaseViewLayoutPicker(\n          selectedLayout: layoutType,\n          onSelect: (layout) {\n            setState(() => layoutType = layout);\n          },\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nimport 'edit_database_view_screen.dart';\n\n/// [MobileDatabaseViewQuickActions] is gives users to quickly edit a database\n/// view from the [MobileDatabaseViewList]\nclass MobileDatabaseViewQuickActions extends StatelessWidget {\n  const MobileDatabaseViewQuickActions({\n    super.key,\n    required this.view,\n    required this.databaseController,\n  });\n\n  final ViewPB view;\n  final DatabaseController databaseController;\n\n  @override\n  Widget build(BuildContext context) {\n    final isInline = view.childViews.isNotEmpty;\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        _actionButton(context, _Action.edit, () async {\n          final bloc = context.read<ViewBloc>();\n          await showTransitionMobileBottomSheet(\n            context,\n            showHeader: true,\n            showDoneButton: true,\n            title: LocaleKeys.grid_settings_editView.tr(),\n            builder: (_) => BlocProvider.value(\n              value: bloc,\n              child: MobileEditDatabaseViewScreen(\n                databaseController: databaseController,\n              ),\n            ),\n          );\n          if (context.mounted) {\n            context.pop();\n          }\n        }),\n        const MobileQuickActionDivider(),\n        _actionButton(\n          context,\n          _Action.changeIcon,\n          () {\n            showMobileBottomSheet(\n              context,\n              showDragHandle: true,\n              showDivider: false,\n              showHeader: true,\n              title: LocaleKeys.titleBar_pageIcon.tr(),\n              backgroundColor: AFThemeExtension.of(context).background,\n              enableDraggableScrollable: true,\n              minChildSize: 0.6,\n              initialChildSize: 0.61,\n              scrollableWidgetBuilder: (_, controller) {\n                return Expanded(\n                  child: FlowyIconEmojiPicker(\n                    tabs: const [PickerTabType.icon],\n                    enableBackgroundColorSelection: false,\n                    onSelectedEmoji: (r) {\n                      ViewBackendService.updateViewIcon(\n                        view: view,\n                        viewIcon: r.data,\n                      );\n                      Navigator.pop(context);\n                    },\n                  ),\n                );\n              },\n              builder: (_) => const SizedBox.shrink(),\n            ).then((_) {\n              if (context.mounted) {\n                Navigator.pop(context);\n              }\n            });\n          },\n          !isInline,\n        ),\n        const MobileQuickActionDivider(),\n        _actionButton(\n          context,\n          _Action.duplicate,\n          () {\n            context.read<ViewBloc>().add(const ViewEvent.duplicate());\n            context.pop();\n          },\n          !isInline,\n        ),\n        const MobileQuickActionDivider(),\n        _actionButton(\n          context,\n          _Action.delete,\n          () {\n            context.read<ViewBloc>().add(const ViewEvent.delete());\n            context.pop();\n          },\n          !isInline,\n        ),\n      ],\n    );\n  }\n\n  Widget _actionButton(\n    BuildContext context,\n    _Action action,\n    VoidCallback onTap, [\n    bool enable = true,\n  ]) {\n    return MobileQuickActionButton(\n      icon: action.icon,\n      text: action.label,\n      textColor: action.color(context),\n      iconColor: action.color(context),\n      onTap: onTap,\n      enable: enable,\n    );\n  }\n}\n\nenum _Action {\n  edit,\n  changeIcon,\n  delete,\n  duplicate;\n\n  String get label {\n    return switch (this) {\n      edit => LocaleKeys.grid_settings_editView.tr(),\n      duplicate => LocaleKeys.button_duplicate.tr(),\n      delete => LocaleKeys.button_delete.tr(),\n      changeIcon => LocaleKeys.disclosureAction_changeIcon.tr(),\n    };\n  }\n\n  FlowySvgData get icon {\n    return switch (this) {\n      edit => FlowySvgs.view_item_rename_s,\n      duplicate => FlowySvgs.duplicate_s,\n      delete => FlowySvgs.trash_s,\n      changeIcon => FlowySvgs.change_icon_s,\n    };\n  }\n\n  Color? color(BuildContext context) {\n    return switch (this) {\n      delete => Theme.of(context).colorScheme.error,\n      _ => null,\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/domain/database_view_service.dart';\nimport 'package:appflowy/plugins/database/domain/layout_service.dart';\nimport 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nimport 'database_field_list.dart';\nimport 'database_view_layout.dart';\n\n/// [MobileEditDatabaseViewScreen] is the main widget used to edit a database\n/// view. It contains multiple sub-pages, and the current page is managed by\n/// [MobileEditDatabaseViewCubit]\nclass MobileEditDatabaseViewScreen extends StatelessWidget {\n  const MobileEditDatabaseViewScreen({\n    super.key,\n    required this.databaseController,\n  });\n\n  final DatabaseController databaseController;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<ViewBloc, ViewState>(\n      builder: (context, state) {\n        return Column(\n          children: [\n            _NameAndIcon(view: state.view),\n            _divider(),\n            DatabaseViewSettingTile(\n              setting: DatabaseViewSettings.layout,\n              databaseController: databaseController,\n              view: state.view,\n              showTopBorder: true,\n            ),\n            if (databaseController.databaseLayout == DatabaseLayoutPB.Calendar)\n              DatabaseViewSettingTile(\n                setting: DatabaseViewSettings.calendar,\n                databaseController: databaseController,\n                view: state.view,\n              ),\n            DatabaseViewSettingTile(\n              setting: DatabaseViewSettings.fields,\n              databaseController: databaseController,\n              view: state.view,\n            ),\n            _divider(),\n          ],\n        );\n      },\n    );\n  }\n\n  Widget _divider() => const VSpace(20);\n}\n\nclass _NameAndIcon extends StatefulWidget {\n  const _NameAndIcon({required this.view});\n\n  final ViewPB view;\n\n  @override\n  State<_NameAndIcon> createState() => _NameAndIconState();\n}\n\nclass _NameAndIconState extends State<_NameAndIcon> {\n  final TextEditingController textEditingController = TextEditingController();\n\n  @override\n  void initState() {\n    super.initState();\n    textEditingController.text = widget.view.name;\n  }\n\n  @override\n  void dispose() {\n    textEditingController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      child: FlowyOptionTile.textField(\n        autofocus: true,\n        showTopBorder: false,\n        controller: textEditingController,\n        onTextChanged: (text) {\n          context.read<ViewBloc>().add(ViewEvent.rename(text));\n        },\n      ),\n    );\n  }\n}\n\nenum DatabaseViewSettings {\n  layout,\n  fields,\n  filter,\n  sort,\n  board,\n  calendar,\n  duplicate,\n  delete;\n\n  String get label {\n    return switch (this) {\n      layout => LocaleKeys.grid_settings_databaseLayout.tr(),\n      fields => LocaleKeys.grid_settings_properties.tr(),\n      filter => LocaleKeys.grid_settings_filter.tr(),\n      sort => LocaleKeys.grid_settings_sort.tr(),\n      board => LocaleKeys.grid_settings_boardSettings.tr(),\n      calendar => LocaleKeys.grid_settings_calendarSettings.tr(),\n      duplicate => LocaleKeys.grid_settings_duplicateView.tr(),\n      delete => LocaleKeys.grid_settings_deleteView.tr(),\n    };\n  }\n\n  FlowySvgData get icon {\n    return switch (this) {\n      layout => FlowySvgs.card_view_s,\n      fields => FlowySvgs.disorder_list_s,\n      filter => FlowySvgs.filter_s,\n      sort => FlowySvgs.sort_ascending_s,\n      board => FlowySvgs.board_s,\n      calendar => FlowySvgs.calendar_s,\n      duplicate => FlowySvgs.copy_s,\n      delete => FlowySvgs.delete_s,\n    };\n  }\n}\n\nclass DatabaseViewSettingTile extends StatelessWidget {\n  const DatabaseViewSettingTile({\n    super.key,\n    required this.setting,\n    required this.databaseController,\n    required this.view,\n    this.showTopBorder = false,\n  });\n\n  final DatabaseViewSettings setting;\n  final DatabaseController databaseController;\n  final ViewPB view;\n  final bool showTopBorder;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyOptionTile.text(\n      text: setting.label,\n      leftIcon: FlowySvg(setting.icon, size: const Size.square(20)),\n      trailing: _trailing(context, setting, view, databaseController),\n      showTopBorder: showTopBorder,\n      onTap: () => _onTap(context),\n    );\n  }\n\n  Widget _trailing(\n    BuildContext context,\n    DatabaseViewSettings setting,\n    ViewPB view,\n    DatabaseController databaseController,\n  ) {\n    switch (setting) {\n      case DatabaseViewSettings.layout:\n        return Row(\n          children: [\n            FlowyText(\n              lineHeight: 1.0,\n              databaseLayoutFromViewLayout(view.layout).layoutName,\n              color: Theme.of(context).hintColor,\n            ),\n            const HSpace(8),\n            const FlowySvg(FlowySvgs.arrow_right_s),\n          ],\n        );\n      case DatabaseViewSettings.fields:\n        final numVisible = databaseController.fieldController.fieldInfos\n            .where((field) => field.visibility != FieldVisibility.AlwaysHidden)\n            .length;\n        return Row(\n          children: [\n            FlowyText(\n              LocaleKeys.grid_settings_numberOfVisibleFields\n                  .tr(args: [numVisible.toString()]),\n              color: Theme.of(context).hintColor,\n            ),\n            const HSpace(8),\n            const FlowySvg(FlowySvgs.arrow_right_s),\n          ],\n        );\n      default:\n        return const SizedBox.shrink();\n    }\n  }\n\n  void _onTap(BuildContext context) async {\n    if (setting == DatabaseViewSettings.layout) {\n      final databaseLayout = databaseLayoutFromViewLayout(view.layout);\n      final newLayout = await showMobileBottomSheet<DatabaseLayoutPB>(\n        context,\n        showDragHandle: true,\n        showHeader: true,\n        showDivider: false,\n        title: LocaleKeys.grid_settings_layout.tr(),\n        builder: (context) {\n          return DatabaseViewLayoutPicker(\n            selectedLayout: databaseLayout,\n            onSelect: (layout) => Navigator.of(context).pop(layout),\n          );\n        },\n      );\n      if (newLayout != null && newLayout != databaseLayout) {\n        await DatabaseViewBackendService.updateLayout(\n          viewId: databaseController.viewId,\n          layout: newLayout,\n        );\n      }\n      return;\n    }\n\n    if (setting == DatabaseViewSettings.fields) {\n      await showTransitionMobileBottomSheet(\n        context,\n        showHeader: true,\n        showBackButton: true,\n        title: LocaleKeys.grid_settings_properties.tr(),\n        builder: (_) {\n          return BlocProvider.value(\n            value: context.read<ViewBloc>(),\n            child: MobileDatabaseFieldList(\n              databaseController: databaseController,\n              canCreate: true,\n            ),\n          );\n        },\n      );\n      return;\n    }\n\n    if (setting == DatabaseViewSettings.board) {\n      await showMobileBottomSheet<DatabaseLayoutPB>(\n        context,\n        builder: (context) {\n          return Padding(\n            padding: const EdgeInsets.only(top: 24, bottom: 46),\n            child: MobileBoardViewLayoutSettings(\n              databaseController: databaseController,\n            ),\n          );\n        },\n      );\n      return;\n    }\n\n    if (setting == DatabaseViewSettings.calendar) {\n      await showMobileBottomSheet<DatabaseLayoutPB>(\n        context,\n        showDragHandle: true,\n        showHeader: true,\n        showDivider: false,\n        title: LocaleKeys.calendar_settings_name.tr(),\n        builder: (context) {\n          return MobileCalendarViewLayoutSettings(\n            databaseController: databaseController,\n          );\n        },\n      );\n      return;\n    }\n\n    if (setting == DatabaseViewSettings.delete) {\n      context.read<ViewBloc>().add(const ViewEvent.delete());\n      context.pop(true);\n      return;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart",
    "content": "import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileDocumentScreen extends StatelessWidget {\n  const MobileDocumentScreen({\n    super.key,\n    required this.id,\n    this.title,\n    this.showMoreButton = true,\n    this.fixedTitle,\n    this.blockId,\n    this.tabs = const [PickerTabType.emoji, PickerTabType.icon],\n  });\n\n  /// view id\n  final String id;\n  final String? title;\n  final bool showMoreButton;\n  final String? fixedTitle;\n  final String? blockId;\n  final List<PickerTabType> tabs;\n\n  static const routeName = '/docs';\n  static const viewId = 'id';\n  static const viewTitle = 'title';\n  static const viewShowMoreButton = 'show_more_button';\n  static const viewFixedTitle = 'fixed_title';\n  static const viewBlockId = 'block_id';\n  static const viewSelectTabs = 'select_tabs';\n\n  @override\n  Widget build(BuildContext context) {\n    return MobileViewPage(\n      id: id,\n      title: title,\n      viewLayout: ViewLayoutPB.Document,\n      showMoreButton: showMoreButton,\n      fixedTitle: fixedTitle,\n      blockId: blockId,\n      tabs: tabs,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_slidable/flutter_slidable.dart';\n\nclass MobileFavoritePageFolder extends StatelessWidget {\n  const MobileFavoritePageFolder({\n    super.key,\n    required this.userProfile,\n  });\n\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    final workspaceId =\n        context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??\n            '';\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider(\n          create: (_) => SidebarSectionsBloc()\n            ..add(SidebarSectionsEvent.initial(userProfile, workspaceId)),\n        ),\n        BlocProvider(\n          create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),\n        ),\n      ],\n      child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(\n        listener: (context, state) =>\n            context.read<FavoriteBloc>().add(const FavoriteEvent.initial()),\n        child: MultiBlocListener(\n          listeners: [\n            BlocListener<SidebarSectionsBloc, SidebarSectionsState>(\n              listenWhen: (p, c) =>\n                  p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,\n              listener: (context, state) =>\n                  context.pushView(state.lastCreatedRootView!),\n            ),\n          ],\n          child: Builder(\n            builder: (context) {\n              final favoriteState = context.watch<FavoriteBloc>().state;\n              if (favoriteState.views.isEmpty) {\n                return FlowyMobileStateContainer.info(\n                  emoji: '😁',\n                  title: LocaleKeys.favorite_noFavorite.tr(),\n                  description: LocaleKeys.favorite_noFavoriteHintText.tr(),\n                );\n              }\n              return Scrollbar(\n                child: SingleChildScrollView(\n                  child: Padding(\n                    padding: const EdgeInsets.all(8.0),\n                    child: SlidableAutoCloseBehavior(\n                      child: Column(\n                        children: [\n                          MobileFavoriteFolder(\n                            showHeader: false,\n                            forceExpanded: true,\n                            views:\n                                favoriteState.views.map((e) => e.item).toList(),\n                          ),\n                          const VSpace(100.0),\n                        ],\n                      ),\n                    ),\n                  ),\n                ),\n              );\n            },\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/features/workspace/data/repositories/rust_workspace_repository_impl.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/mobile/presentation/favorite/mobile_favorite_folder.dart';\nimport 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MobileFavoriteScreen extends StatelessWidget {\n  const MobileFavoriteScreen({\n    super.key,\n  });\n\n  static const routeName = '/favorite';\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: Future.wait([\n        FolderEventGetCurrentWorkspaceSetting().send(),\n        getIt<AuthService>().getUser(),\n      ]),\n      builder: (context, snapshots) {\n        if (!snapshots.hasData) {\n          return const Center(child: CircularProgressIndicator.adaptive());\n        }\n\n        final latest = snapshots.data?[0].fold(\n          (latest) {\n            return latest as WorkspaceLatestPB?;\n          },\n          (error) => null,\n        );\n        final userProfile = snapshots.data?[1].fold(\n          (userProfilePB) {\n            return userProfilePB as UserProfilePB?;\n          },\n          (error) => null,\n        );\n\n        // In the unlikely case either of the above is null, eg.\n        // when a workspace is already open this can happen.\n        if (latest == null || userProfile == null) {\n          return const WorkspaceFailedScreen();\n        }\n\n        return Scaffold(\n          body: SafeArea(\n            child: BlocProvider(\n              create: (_) => UserWorkspaceBloc(\n                userProfile: userProfile,\n                repository: RustWorkspaceRepositoryImpl(\n                  userId: userProfile.id,\n                ),\n              )..add(\n                  UserWorkspaceEvent.initialize(),\n                ),\n              child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(\n                buildWhen: (previous, current) =>\n                    previous.currentWorkspace?.workspaceId !=\n                    current.currentWorkspace?.workspaceId,\n                builder: (context, state) {\n                  return MobileFavoritePage(\n                    userProfile: userProfile,\n                  );\n                },\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass MobileFavoritePage extends StatelessWidget {\n  const MobileFavoritePage({\n    super.key,\n    required this.userProfile,\n  });\n\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        // Header\n        Padding(\n          padding: EdgeInsets.only(\n            left: 16,\n            right: 16,\n            top: Platform.isAndroid ? 8.0 : 0.0,\n          ),\n          child: MobileHomePageHeader(\n            userProfile: userProfile,\n          ),\n        ),\n        const Divider(),\n\n        // Folder\n        Expanded(\n          child: MobileFavoritePageFolder(\n            userProfile: userProfile,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart';\nimport 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MobileFavoriteSpace extends StatefulWidget {\n  const MobileFavoriteSpace({\n    super.key,\n    required this.userProfile,\n  });\n\n  final UserProfilePB userProfile;\n\n  @override\n  State<MobileFavoriteSpace> createState() => _MobileFavoriteSpaceState();\n}\n\nclass _MobileFavoriteSpaceState extends State<MobileFavoriteSpace>\n    with AutomaticKeepAliveClientMixin {\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    final workspaceId =\n        context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??\n            '';\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider(\n          create: (_) => SidebarSectionsBloc()\n            ..add(\n              SidebarSectionsEvent.initial(widget.userProfile, workspaceId),\n            ),\n        ),\n        BlocProvider(\n          create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),\n        ),\n      ],\n      child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(\n        listener: (context, state) =>\n            context.read<FavoriteBloc>().add(const FavoriteEvent.initial()),\n        child: MultiBlocListener(\n          listeners: [\n            BlocListener<SidebarSectionsBloc, SidebarSectionsState>(\n              listenWhen: (p, c) =>\n                  p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,\n              listener: (context, state) =>\n                  context.pushView(state.lastCreatedRootView!),\n            ),\n          ],\n          child: Builder(\n            builder: (context) {\n              final favoriteState = context.watch<FavoriteBloc>().state;\n\n              if (favoriteState.isLoading) {\n                return const SizedBox.shrink();\n              }\n\n              if (favoriteState.views.isEmpty) {\n                return const EmptySpacePlaceholder(\n                  type: MobilePageCardType.favorite,\n                );\n              }\n\n              return _FavoriteViews(\n                favoriteViews: favoriteState.views.reversed.toList(),\n              );\n            },\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _FavoriteViews extends StatelessWidget {\n  const _FavoriteViews({\n    required this.favoriteViews,\n  });\n\n  final List<SectionViewPB> favoriteViews;\n\n  @override\n  Widget build(BuildContext context) {\n    final borderColor = Theme.of(context).isLightMode\n        ? const Color(0xFFE9E9EC)\n        : const Color(0x1AFFFFFF);\n    return ListView.separated(\n      key: const PageStorageKey('favorite_views_page_storage_key'),\n      padding: EdgeInsets.only(\n        bottom: HomeSpaceViewSizes.mVerticalPadding +\n            MediaQuery.of(context).padding.bottom,\n      ),\n      itemBuilder: (context, index) {\n        final view = favoriteViews[index];\n        return Container(\n          padding: const EdgeInsets.symmetric(vertical: 24.0),\n          decoration: BoxDecoration(\n            border: Border(\n              bottom: BorderSide(\n                color: borderColor,\n                width: 0.5,\n              ),\n            ),\n          ),\n          child: MobileViewPage(\n            key: ValueKey(view.item.id),\n            view: view.item,\n            timestamp: view.timestamp,\n            type: MobilePageCardType.favorite,\n          ),\n        );\n      },\n      separatorBuilder: (context, index) => const HSpace(8),\n      itemCount: favoriteViews.length,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart",
    "content": "import 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart';\nimport 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder_header.dart';\nimport 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MobileFavoriteFolder extends StatelessWidget {\n  const MobileFavoriteFolder({\n    super.key,\n    required this.views,\n    this.showHeader = true,\n    this.forceExpanded = false,\n  });\n\n  final bool showHeader;\n  final bool forceExpanded;\n  final List<ViewPB> views;\n\n  @override\n  Widget build(BuildContext context) {\n    if (views.isEmpty) {\n      return const SizedBox.shrink();\n    }\n\n    return BlocProvider<FolderBloc>(\n      create: (context) => FolderBloc(type: FolderSpaceType.favorite)\n        ..add(\n          const FolderEvent.initial(),\n        ),\n      child: BlocBuilder<FolderBloc, FolderState>(\n        builder: (context, state) {\n          return Column(\n            children: [\n              if (showHeader) ...[\n                MobileFavoriteFolderHeader(\n                  isExpanded: context.read<FolderBloc>().state.isExpanded,\n                  onPressed: () => context\n                      .read<FolderBloc>()\n                      .add(const FolderEvent.expandOrUnExpand()),\n                  onAdded: () => context.read<FolderBloc>().add(\n                        const FolderEvent.expandOrUnExpand(isExpanded: true),\n                      ),\n                ),\n                const VSpace(8.0),\n                const Divider(\n                  height: 1,\n                ),\n              ],\n              if (forceExpanded || state.isExpanded)\n                ...views.map(\n                  (view) => MobileViewItem(\n                    key: ValueKey(\n                      '${FolderSpaceType.favorite.name} ${view.id}',\n                    ),\n                    spaceType: FolderSpaceType.favorite,\n                    isDraggable: false,\n                    isFirstChild: view.id == views.first.id,\n                    isFeedback: false,\n                    view: view,\n                    level: 0,\n                    onSelected: context.pushView,\n                    endActionPane: (context) => buildEndActionPane(\n                      context,\n                      [\n                        view.isFavorite\n                            ? MobilePaneActionType.removeFromFavorites\n                            : MobilePaneActionType.addToFavorites,\n                        MobilePaneActionType.more,\n                      ],\n                      spaceType: FolderSpaceType.favorite,\n                      spaceRatio: 5,\n                    ),\n                  ),\n                ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder_header.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileFavoriteFolderHeader extends StatefulWidget {\n  const MobileFavoriteFolderHeader({\n    super.key,\n    required this.onPressed,\n    required this.onAdded,\n    required this.isExpanded,\n  });\n\n  final VoidCallback onPressed;\n  final VoidCallback onAdded;\n  final bool isExpanded;\n\n  @override\n  State<MobileFavoriteFolderHeader> createState() =>\n      _MobileFavoriteFolderHeaderState();\n}\n\nclass _MobileFavoriteFolderHeaderState\n    extends State<MobileFavoriteFolderHeader> {\n  double _turns = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        Expanded(\n          child: FlowyButton(\n            text: FlowyText.semibold(\n              LocaleKeys.sideBar_favorites.tr(),\n              fontSize: 20.0,\n            ),\n            margin: const EdgeInsets.symmetric(vertical: 8),\n            expandText: false,\n            mainAxisAlignment: MainAxisAlignment.start,\n            rightIcon: AnimatedRotation(\n              duration: const Duration(milliseconds: 200),\n              turns: _turns,\n              child: const Icon(\n                Icons.keyboard_arrow_down_rounded,\n                color: Colors.grey,\n              ),\n            ),\n            onTap: () {\n              setState(() {\n                _turns = widget.isExpanded ? -0.25 : 0;\n              });\n              widget.onPressed();\n            },\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/home.dart",
    "content": "export 'mobile_home_page.dart';\nexport 'mobile_home_setting_page.dart';\nexport 'mobile_home_trash_page.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/mobile/presentation/home/mobile_folders.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MobileHomeSpace extends StatefulWidget {\n  const MobileHomeSpace({super.key, required this.userProfile});\n\n  final UserProfilePB userProfile;\n\n  @override\n  State<MobileHomeSpace> createState() => _MobileHomeSpaceState();\n}\n\nclass _MobileHomeSpaceState extends State<MobileHomeSpace>\n    with AutomaticKeepAliveClientMixin {\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    final workspaceId =\n        context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??\n            '';\n    return SingleChildScrollView(\n      child: Padding(\n        padding: EdgeInsets.only(\n          top: HomeSpaceViewSizes.mVerticalPadding,\n          bottom: HomeSpaceViewSizes.mVerticalPadding +\n              MediaQuery.of(context).padding.bottom,\n        ),\n        child: MobileFolders(\n          user: widget.userProfile,\n          workspaceId: workspaceId,\n          showFavorite: false,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder.dart';\nimport 'package:appflowy/mobile/presentation/home/space/mobile_space.dart';\nimport 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_slidable/flutter_slidable.dart';\n\n// Contains Public And Private Sections\nclass MobileFolders extends StatelessWidget {\n  const MobileFolders({\n    super.key,\n    required this.user,\n    required this.workspaceId,\n    required this.showFavorite,\n  });\n\n  final UserProfilePB user;\n  final String workspaceId;\n  final bool showFavorite;\n\n  @override\n  Widget build(BuildContext context) {\n    final workspaceId =\n        context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??\n            '';\n    return BlocListener<UserWorkspaceBloc, UserWorkspaceState>(\n      listenWhen: (previous, current) =>\n          previous.currentWorkspace?.workspaceId !=\n          current.currentWorkspace?.workspaceId,\n      listener: (context, state) {\n        context.read<SidebarSectionsBloc>().add(\n              SidebarSectionsEvent.initial(\n                user,\n                state.currentWorkspace?.workspaceId ?? workspaceId,\n              ),\n            );\n        context.read<SpaceBloc>().add(\n              SpaceEvent.reset(\n                user,\n                state.currentWorkspace?.workspaceId ?? workspaceId,\n                false,\n              ),\n            );\n      },\n      child: const _MobileFolder(),\n    );\n  }\n}\n\nclass _MobileFolder extends StatefulWidget {\n  const _MobileFolder();\n\n  @override\n  State<_MobileFolder> createState() => _MobileFolderState();\n}\n\nclass _MobileFolderState extends State<_MobileFolder> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(\n      builder: (context, state) {\n        return SlidableAutoCloseBehavior(\n          child: Column(\n            children: [\n              ..._buildSpaceOrSection(context, state),\n              const VSpace(80.0),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  List<Widget> _buildSpaceOrSection(\n    BuildContext context,\n    SidebarSectionsState state,\n  ) {\n    if (context.watch<SpaceBloc>().state.spaces.isNotEmpty) {\n      return [\n        const MobileSpace(),\n      ];\n    }\n\n    if (context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn) {\n      return [\n        MobileSectionFolder(\n          title: LocaleKeys.sideBar_workspace.tr(),\n          spaceType: FolderSpaceType.public,\n          views: state.section.publicViews,\n        ),\n        const VSpace(8.0),\n        MobileSectionFolder(\n          title: LocaleKeys.sideBar_private.tr(),\n          spaceType: FolderSpaceType.private,\n          views: state.section.privateViews,\n        ),\n      ];\n    }\n\n    return [\n      MobileSectionFolder(\n        title: LocaleKeys.sideBar_personal.tr(),\n        spaceType: FolderSpaceType.public,\n        views: state.section.publicViews,\n      ),\n    ];\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart",
    "content": "import 'package:appflowy/features/workspace/data/repositories/rust_workspace_repository_impl.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';\nimport 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';\nimport 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/shared/loading.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';\nimport 'package:appflowy/workspace/application/recent/cached_recent_service.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\n\nclass MobileHomeScreen extends StatelessWidget {\n  const MobileHomeScreen({super.key});\n\n  static const routeName = '/home';\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: Future.wait([\n        FolderEventGetCurrentWorkspaceSetting().send(),\n        getIt<AuthService>().getUser(),\n      ]),\n      builder: (context, snapshots) {\n        if (!snapshots.hasData) {\n          return const Center(child: CircularProgressIndicator.adaptive());\n        }\n\n        final workspaceLatest = snapshots.data?[0].fold(\n          (workspaceLatestPB) {\n            return workspaceLatestPB as WorkspaceLatestPB?;\n          },\n          (error) => null,\n        );\n        final userProfile = snapshots.data?[1].fold(\n          (userProfilePB) {\n            return userProfilePB as UserProfilePB?;\n          },\n          (error) => null,\n        );\n\n        // In the unlikely case either of the above is null, eg.\n        // when a workspace is already open this can happen.\n        if (workspaceLatest == null || userProfile == null) {\n          return const WorkspaceFailedScreen();\n        }\n\n        return Scaffold(\n          body: SafeArea(\n            bottom: false,\n            child: Provider.value(\n              value: userProfile,\n              child: MobileHomePage(\n                userProfile: userProfile,\n                workspaceLatest: workspaceLatest,\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nfinal PropertyValueNotifier<UserWorkspacePB?> mCurrentWorkspace =\n    PropertyValueNotifier<UserWorkspacePB?>(null);\n\nclass MobileHomePage extends StatefulWidget {\n  const MobileHomePage({\n    super.key,\n    required this.userProfile,\n    required this.workspaceLatest,\n  });\n\n  final UserProfilePB userProfile;\n  final WorkspaceLatestPB workspaceLatest;\n\n  @override\n  State<MobileHomePage> createState() => _MobileHomePageState();\n}\n\nclass _MobileHomePageState extends State<MobileHomePage> {\n  Loading? loadingIndicator;\n\n  @override\n  void initState() {\n    super.initState();\n\n    getIt<MenuSharedState>().addLatestViewListener(_onLatestViewChange);\n    getIt<ReminderBloc>().add(const ReminderEvent.started());\n  }\n\n  @override\n  void dispose() {\n    getIt<MenuSharedState>().removeLatestViewListener(_onLatestViewChange);\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider(\n          create: (_) => UserWorkspaceBloc(\n            userProfile: widget.userProfile,\n            repository: RustWorkspaceRepositoryImpl(\n              userId: widget.userProfile.id,\n            ),\n          )..add(UserWorkspaceEvent.initialize()),\n        ),\n        BlocProvider(\n          create: (context) =>\n              FavoriteBloc()..add(const FavoriteEvent.initial()),\n        ),\n        BlocProvider.value(\n          value: getIt<ReminderBloc>()..add(const ReminderEvent.started()),\n        ),\n      ],\n      child: _HomePage(userProfile: widget.userProfile),\n    );\n  }\n\n  void _onLatestViewChange() async {\n    final id = getIt<MenuSharedState>().latestOpenView?.id;\n    if (id == null || id.isEmpty) {\n      return;\n    }\n    await FolderEventSetLatestView(ViewIdPB(value: id)).send();\n  }\n}\n\nclass _HomePage extends StatefulWidget {\n  const _HomePage({required this.userProfile});\n\n  final UserProfilePB userProfile;\n\n  @override\n  State<_HomePage> createState() => _HomePageState();\n}\n\nclass _HomePageState extends State<_HomePage> {\n  Loading? loadingIndicator;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(\n      buildWhen: (previous, current) =>\n          previous.currentWorkspace?.workspaceId !=\n          current.currentWorkspace?.workspaceId,\n      listener: (context, state) {\n        getIt<CachedRecentService>().reset();\n        mCurrentWorkspace.value = state.currentWorkspace;\n        if (FeatureFlag.search.isOn) {\n          // Notify command palette that workspace has changed\n          context.read<CommandPaletteBloc>().add(\n                CommandPaletteEvent.workspaceChanged(\n                  workspaceId: state.currentWorkspace?.workspaceId,\n                ),\n              );\n        }\n        Debounce.debounce(\n          'workspace_action_result',\n          const Duration(milliseconds: 150),\n          () {\n            _showResultDialog(context, state);\n          },\n        );\n      },\n      builder: (context, state) {\n        if (state.currentWorkspace == null) {\n          return const SizedBox.shrink();\n        }\n\n        final workspaceId = state.currentWorkspace!.workspaceId;\n\n        return Column(\n          key: ValueKey('mobile_home_page_$workspaceId'),\n          children: [\n            // Header\n            Padding(\n              padding: const EdgeInsets.only(\n                left: HomeSpaceViewSizes.mHorizontalPadding,\n                right: 8.0,\n              ),\n              child: MobileHomePageHeader(\n                userProfile: widget.userProfile,\n              ),\n            ),\n\n            Expanded(\n              child: MultiBlocProvider(\n                providers: [\n                  BlocProvider(\n                    create: (_) =>\n                        SpaceOrderBloc()..add(const SpaceOrderEvent.initial()),\n                  ),\n                  BlocProvider(\n                    create: (_) => SidebarSectionsBloc()\n                      ..add(\n                        SidebarSectionsEvent.initial(\n                          widget.userProfile,\n                          workspaceId,\n                        ),\n                      ),\n                  ),\n                  BlocProvider(\n                    create: (_) =>\n                        FavoriteBloc()..add(const FavoriteEvent.initial()),\n                  ),\n                  BlocProvider(\n                    create: (_) => SpaceBloc(\n                      userProfile: widget.userProfile,\n                      workspaceId: workspaceId,\n                    )..add(\n                        const SpaceEvent.initial(\n                          openFirstPage: false,\n                        ),\n                      ),\n                  ),\n                ],\n                child: MobileHomePageTab(\n                  userProfile: widget.userProfile,\n                ),\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  void _showResultDialog(BuildContext context, UserWorkspaceState state) {\n    final actionResult = state.actionResult;\n    if (actionResult == null) {\n      return;\n    }\n\n    Log.info('workspace action result: $actionResult');\n\n    final actionType = actionResult.actionType;\n    final result = actionResult.result;\n    final isLoading = actionResult.isLoading;\n\n    if (isLoading) {\n      loadingIndicator ??= Loading(context)..start();\n      return;\n    } else {\n      loadingIndicator?.stop();\n      loadingIndicator = null;\n    }\n\n    if (result == null) {\n      return;\n    }\n\n    result.onFailure((f) {\n      Log.error(\n        '[Workspace] Failed to perform ${actionType.toString()} action: $f',\n      );\n    });\n\n    final String? message;\n    ToastificationType toastType = ToastificationType.success;\n    switch (actionType) {\n      case WorkspaceActionType.open:\n        message = result.onFailure((e) {\n          toastType = ToastificationType.error;\n          return '${LocaleKeys.workspace_openFailed.tr()}: ${e.msg}';\n        });\n        break;\n      case WorkspaceActionType.delete:\n        message = result.fold(\n          (s) {\n            toastType = ToastificationType.success;\n            return LocaleKeys.workspace_deleteSuccess.tr();\n          },\n          (e) {\n            toastType = ToastificationType.error;\n            return '${LocaleKeys.workspace_deleteFailed.tr()}: ${e.msg}';\n          },\n        );\n        break;\n      case WorkspaceActionType.leave:\n        message = result.fold(\n          (s) {\n            toastType = ToastificationType.success;\n            return LocaleKeys\n                .settings_workspacePage_leaveWorkspacePrompt_success\n                .tr();\n          },\n          (e) {\n            toastType = ToastificationType.error;\n            return '${LocaleKeys.settings_workspacePage_leaveWorkspacePrompt_fail.tr()}: ${e.msg}';\n          },\n        );\n        break;\n      case WorkspaceActionType.rename:\n        message = result.fold(\n          (s) {\n            toastType = ToastificationType.success;\n            return LocaleKeys.workspace_renameSuccess.tr();\n          },\n          (e) {\n            toastType = ToastificationType.error;\n            return '${LocaleKeys.workspace_renameFailed.tr()}: ${e.msg}';\n          },\n        );\n        break;\n      default:\n        message = null;\n        toastType = ToastificationType.error;\n        break;\n    }\n\n    if (message != null) {\n      showToastNotification(message: message, type: toastType);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/animated_gesture.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart';\nimport 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/built_in_svgs.dart';\nimport 'package:appflowy/workspace/application/user/settings_user_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nimport 'setting/settings_popup_menu.dart';\n\nclass MobileHomePageHeader extends StatelessWidget {\n  const MobileHomePageHeader({\n    super.key,\n    required this.userProfile,\n  });\n\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => getIt<SettingsUserViewBloc>(param1: userProfile)\n        ..add(const SettingsUserEvent.initial()),\n      child: BlocBuilder<SettingsUserViewBloc, SettingsUserState>(\n        builder: (context, state) {\n          final isCollaborativeWorkspace =\n              context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn;\n          return ConstrainedBox(\n            constraints: const BoxConstraints(minHeight: 56),\n            child: Row(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                Expanded(\n                  child: isCollaborativeWorkspace\n                      ? _MobileWorkspace(userProfile: userProfile)\n                      : _MobileUser(userProfile: userProfile),\n                ),\n                HomePageSettingsPopupMenu(\n                  userProfile: userProfile,\n                ),\n                const HSpace(8.0),\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _MobileUser extends StatelessWidget {\n  const _MobileUser({\n    required this.userProfile,\n  });\n\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    final userIcon = userProfile.iconUrl;\n    return Row(\n      children: [\n        _UserIcon(userIcon: userIcon),\n        const HSpace(12),\n        Expanded(\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              const FlowyText.medium('AppFlowy', fontSize: 18),\n              const VSpace(4),\n              FlowyText.regular(\n                userProfile.email.isNotEmpty\n                    ? userProfile.email\n                    : userProfile.name,\n                fontSize: 12,\n                color: Theme.of(context).colorScheme.onSurface,\n                overflow: TextOverflow.ellipsis,\n              ),\n            ],\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass _MobileWorkspace extends StatelessWidget {\n  const _MobileWorkspace({\n    required this.userProfile,\n  });\n\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(\n      builder: (context, state) {\n        final currentWorkspace = state.currentWorkspace;\n        if (currentWorkspace == null) {\n          return const SizedBox.shrink();\n        }\n        return AnimatedGestureDetector(\n          scaleFactor: 0.99,\n          alignment: Alignment.centerLeft,\n          onTapUp: () {\n            context.read<UserWorkspaceBloc>().add(\n                  UserWorkspaceEvent.fetchWorkspaces(),\n                );\n            _showSwitchWorkspacesBottomSheet(context);\n          },\n          child: Row(\n            children: [\n              WorkspaceIcon(\n                workspaceIcon: currentWorkspace.icon,\n                workspaceName: currentWorkspace.name,\n                iconSize: 36,\n                fontSize: 18.0,\n                isEditable: true,\n                figmaLineHeight: 26.0,\n                emojiSize: 24.0,\n                borderRadius: 12.0,\n                onSelected: (result) => context.read<UserWorkspaceBloc>().add(\n                      UserWorkspaceEvent.updateWorkspaceIcon(\n                        workspaceId: currentWorkspace.workspaceId,\n                        icon: result.emoji,\n                      ),\n                    ),\n              ),\n              currentWorkspace.icon.isNotEmpty\n                  ? const HSpace(2)\n                  : const HSpace(8),\n              Flexible(\n                child: FlowyText.semibold(\n                  currentWorkspace.name,\n                  fontSize: 20.0,\n                  overflow: TextOverflow.ellipsis,\n                ),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  void _showSwitchWorkspacesBottomSheet(\n    BuildContext context,\n  ) {\n    showMobileBottomSheet(\n      context,\n      showDivider: false,\n      showHeader: true,\n      showDragHandle: true,\n      showCloseButton: true,\n      useRootNavigator: true,\n      enableScrollable: true,\n      bottomSheetPadding: context.bottomSheetPadding(),\n      title: LocaleKeys.workspace_menuTitle.tr(),\n      backgroundColor: Theme.of(context).colorScheme.surface,\n      builder: (sheetContext) {\n        return BlocProvider.value(\n          value: context.read<UserWorkspaceBloc>(),\n          child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(\n            builder: (context, state) {\n              final currentWorkspace = state.currentWorkspace;\n              final workspaces = state.workspaces;\n              if (currentWorkspace == null || workspaces.isEmpty) {\n                return const SizedBox.shrink();\n              }\n              return MobileWorkspaceMenu(\n                userProfile: userProfile,\n                currentWorkspace: currentWorkspace,\n                workspaces: workspaces,\n                onWorkspaceSelected: (workspace) {\n                  Navigator.of(sheetContext).pop();\n\n                  if (workspace == currentWorkspace) {\n                    return;\n                  }\n\n                  context.read<UserWorkspaceBloc>().add(\n                        UserWorkspaceEvent.openWorkspace(\n                          workspaceId: workspace.workspaceId,\n                          workspaceType: workspace.workspaceType,\n                        ),\n                      );\n                },\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass _UserIcon extends StatelessWidget {\n  const _UserIcon({\n    required this.userIcon,\n  });\n\n  final String userIcon;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      useIntrinsicWidth: true,\n      text: builtInSVGIcons.contains(userIcon)\n          // to be compatible with old user icon\n          ? FlowySvg(\n              FlowySvgData('emoji/$userIcon'),\n              size: const Size.square(32),\n              blendMode: null,\n            )\n          : FlowyText(\n              userIcon.isNotEmpty ? userIcon : '🐻',\n              fontSize: 26,\n            ),\n      onTap: () async {\n        final icon = await context.push<EmojiIconData>(\n          Uri(\n            path: MobileEmojiPickerScreen.routeName,\n            queryParameters: {\n              MobileEmojiPickerScreen.pageTitle:\n                  LocaleKeys.titleBar_userIcon.tr(),\n              MobileEmojiPickerScreen.selectTabs: [PickerTabType.emoji.name],\n            },\n          ).toString(),\n        );\n        if (icon != null) {\n          if (context.mounted) {\n            context.read<SettingsUserViewBloc>().add(\n                  SettingsUserEvent.updateUserIcon(\n                    iconUrl: icon.emoji,\n                  ),\n                );\n          }\n        }\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/env/env.dart';\nimport 'package:appflowy/features/workspace/data/repositories/rust_workspace_repository_impl.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:appflowy/mobile/presentation/setting/ai/ai_settings_group.dart';\nimport 'package:appflowy/mobile/presentation/setting/cloud/cloud_setting_group.dart';\nimport 'package:appflowy/mobile/presentation/setting/user_session_setting_group.dart';\nimport 'package:appflowy/mobile/presentation/setting/workspace/workspace_setting_group.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MobileHomeSettingPage extends StatefulWidget {\n  const MobileHomeSettingPage({\n    super.key,\n  });\n\n  static const routeName = '/settings';\n\n  @override\n  State<MobileHomeSettingPage> createState() => _MobileHomeSettingPageState();\n}\n\nclass _MobileHomeSettingPageState extends State<MobileHomeSettingPage> {\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: getIt<AuthService>().getUser(),\n      builder: (context, snapshot) {\n        String? errorMsg;\n        if (!snapshot.hasData) {\n          return const Center(child: CircularProgressIndicator.adaptive());\n        }\n\n        final userProfile = snapshot.data?.fold(\n          (userProfile) {\n            return userProfile;\n          },\n          (error) {\n            errorMsg = error.msg;\n            return null;\n          },\n        );\n\n        return Scaffold(\n          appBar: FlowyAppBar(\n            titleText: LocaleKeys.settings_title.tr(),\n          ),\n          body: userProfile == null\n              ? _buildErrorWidget(errorMsg)\n              : _buildSettingsWidget(userProfile),\n        );\n      },\n    );\n  }\n\n  Widget _buildErrorWidget(String? errorMsg) {\n    return FlowyMobileStateContainer.error(\n      emoji: '🛸',\n      title: LocaleKeys.settings_mobile_userprofileError.tr(),\n      description: LocaleKeys.settings_mobile_userprofileErrorDescription.tr(),\n      errorMsg: errorMsg,\n    );\n  }\n\n  Widget _buildSettingsWidget(UserProfilePB userProfile) {\n    return BlocProvider(\n      create: (context) => UserWorkspaceBloc(\n        userProfile: userProfile,\n        repository: RustWorkspaceRepositoryImpl(\n          userId: userProfile.id,\n        ),\n      )..add(UserWorkspaceEvent.initialize()),\n      child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(\n        builder: (context, state) {\n          final currentWorkspaceId = state.currentWorkspace?.workspaceId ?? '';\n          return SingleChildScrollView(\n            child: Padding(\n              padding: const EdgeInsets.all(16),\n              child: Column(\n                children: [\n                  PersonalInfoSettingGroup(\n                    userProfile: userProfile,\n                  ),\n                  if (state.userProfile.userAuthType == AuthTypePB.Server)\n                    const WorkspaceSettingGroup(),\n                  const AppearanceSettingGroup(),\n                  const LanguageSettingGroup(),\n                  if (Env.enableCustomCloud) const CloudSettingGroup(),\n                  if (isAuthEnabled)\n                    AiSettingsGroup(\n                      key: ValueKey(currentWorkspaceId),\n                      userProfile: userProfile,\n                      workspaceId: currentWorkspaceId,\n                    ),\n                  const SupportSettingGroup(),\n                  const AboutSettingGroup(),\n                  UserSessionSettingGroup(\n                    userProfile: userProfile,\n                    showThirdPartyLogin: false,\n                  ),\n                  const VSpace(20),\n                ],\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/plugins/trash/application/prelude.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:fluttertoast/fluttertoast.dart';\nimport 'package:go_router/go_router.dart';\n\nclass MobileHomeTrashPage extends StatelessWidget {\n  const MobileHomeTrashPage({super.key});\n\n  static const routeName = '/trash';\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => getIt<TrashBloc>()..add(const TrashEvent.initial()),\n      child: BlocBuilder<TrashBloc, TrashState>(\n        builder: (context, state) {\n          return Scaffold(\n            appBar: AppBar(\n              title: Text(LocaleKeys.trash_text.tr()),\n              actions: [\n                state.objects.isEmpty\n                    ? const SizedBox.shrink()\n                    : IconButton(\n                        splashRadius: 20,\n                        icon: const Icon(Icons.more_horiz),\n                        onPressed: () {\n                          final trashBloc = context.read<TrashBloc>();\n                          showMobileBottomSheet(\n                            context,\n                            showHeader: true,\n                            showCloseButton: true,\n                            showDragHandle: true,\n                            padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),\n                            title: LocaleKeys.trash_mobile_actions.tr(),\n                            builder: (_) => Row(\n                              children: [\n                                Expanded(\n                                  child: _TrashActionAllButton(\n                                    trashBloc: trashBloc,\n                                  ),\n                                ),\n                                const SizedBox(\n                                  width: 16,\n                                ),\n                                Expanded(\n                                  child: _TrashActionAllButton(\n                                    trashBloc: trashBloc,\n                                    type: _TrashActionType.restoreAll,\n                                  ),\n                                ),\n                              ],\n                            ),\n                          );\n                        },\n                      ),\n              ],\n            ),\n            body: state.objects.isEmpty\n                ? const _EmptyTrashBin()\n                : _DeletedFilesListView(state),\n          );\n        },\n      ),\n    );\n  }\n}\n\nenum _TrashActionType {\n  restoreAll,\n  deleteAll,\n}\n\nclass _EmptyTrashBin extends StatelessWidget {\n  const _EmptyTrashBin();\n\n  @override\n  Widget build(BuildContext context) {\n    return Center(\n      child: Column(\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          const FlowySvg(\n            FlowySvgs.m_empty_trash_xl,\n            size: Size.square(46),\n          ),\n          const VSpace(16.0),\n          FlowyText.medium(\n            LocaleKeys.trash_mobile_empty.tr(),\n            fontSize: 18.0,\n            textAlign: TextAlign.center,\n          ),\n          const VSpace(8.0),\n          FlowyText.regular(\n            LocaleKeys.trash_mobile_emptyDescription.tr(),\n            fontSize: 17.0,\n            maxLines: 10,\n            textAlign: TextAlign.center,\n            lineHeight: 1.3,\n            color: Theme.of(context).hintColor,\n          ),\n          const VSpace(kBottomNavigationBarHeight + 36.0),\n        ],\n      ),\n    );\n  }\n}\n\nclass _TrashActionAllButton extends StatelessWidget {\n  /// Switch between 'delete all' and 'restore all' feature\n  const _TrashActionAllButton({\n    this.type = _TrashActionType.deleteAll,\n    required this.trashBloc,\n  });\n  final _TrashActionType type;\n  final TrashBloc trashBloc;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = Theme.of(context);\n    final isDeleteAll = type == _TrashActionType.deleteAll;\n    return BlocProvider.value(\n      value: trashBloc,\n      child: BottomSheetActionWidget(\n        svg: isDeleteAll ? FlowySvgs.m_delete_m : FlowySvgs.m_restore_m,\n        text: isDeleteAll\n            ? LocaleKeys.trash_deleteAll.tr()\n            : LocaleKeys.trash_restoreAll.tr(),\n        onTap: () {\n          final trashList = trashBloc.state.objects;\n          if (trashList.isNotEmpty) {\n            context.pop();\n            showFlowyMobileConfirmDialog(\n              context,\n              title: FlowyText(\n                isDeleteAll\n                    ? LocaleKeys.trash_confirmDeleteAll_title.tr()\n                    : LocaleKeys.trash_restoreAll.tr(),\n              ),\n              content: FlowyText(\n                isDeleteAll\n                    ? LocaleKeys.trash_confirmDeleteAll_caption.tr()\n                    : LocaleKeys.trash_confirmRestoreAll_caption.tr(),\n              ),\n              actionButtonTitle: isDeleteAll\n                  ? LocaleKeys.trash_deleteAll.tr()\n                  : LocaleKeys.trash_restoreAll.tr(),\n              actionButtonColor: isDeleteAll\n                  ? theme.colorScheme.error\n                  : theme.colorScheme.primary,\n              onActionButtonPressed: () {\n                if (isDeleteAll) {\n                  trashBloc.add(\n                    const TrashEvent.deleteAll(),\n                  );\n                } else {\n                  trashBloc.add(\n                    const TrashEvent.restoreAll(),\n                  );\n                }\n              },\n              cancelButtonTitle: LocaleKeys.button_cancel.tr(),\n            );\n          } else {\n            // when there is no deleted files\n            // show toast\n            Fluttertoast.showToast(\n              msg: LocaleKeys.trash_mobile_empty.tr(),\n              gravity: ToastGravity.CENTER,\n            );\n          }\n        },\n      ),\n    );\n  }\n}\n\nclass _DeletedFilesListView extends StatelessWidget {\n  const _DeletedFilesListView(\n    this.state,\n  );\n\n  final TrashState state;\n  @override\n  Widget build(BuildContext context) {\n    final theme = Theme.of(context);\n    return Padding(\n      padding: const EdgeInsets.symmetric(vertical: 8),\n      child: ListView.builder(\n        itemBuilder: (context, index) {\n          final deletedFile = state.objects[index];\n\n          return Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),\n            child: ListTile(\n              // TODO: show different file type icon, implement this feature after TrashPB has file type field\n              leading: FlowySvg(\n                FlowySvgs.document_s,\n                size: const Size.square(24),\n                color: theme.colorScheme.onSurface,\n              ),\n              title: Text(\n                deletedFile.name,\n                style: theme.textTheme.labelMedium\n                    ?.copyWith(color: theme.colorScheme.onSurface),\n              ),\n              horizontalTitleGap: 0,\n              tileColor: theme.colorScheme.onSurface.withValues(alpha: 0.1),\n              shape: RoundedRectangleBorder(\n                borderRadius: BorderRadius.circular(8),\n              ),\n              trailing: Row(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  IconButton(\n                    splashRadius: 20,\n                    icon: FlowySvg(\n                      FlowySvgs.m_restore_m,\n                      size: const Size.square(24),\n                      color: theme.colorScheme.onSurface,\n                    ),\n                    onPressed: () {\n                      context\n                          .read<TrashBloc>()\n                          .add(TrashEvent.putback(deletedFile.id));\n                      Fluttertoast.showToast(\n                        msg:\n                            '${deletedFile.name} ${LocaleKeys.trash_mobile_isRestored.tr()}',\n                        gravity: ToastGravity.BOTTOM,\n                      );\n                    },\n                  ),\n                  IconButton(\n                    splashRadius: 20,\n                    icon: FlowySvg(\n                      FlowySvgs.m_delete_m,\n                      size: const Size.square(24),\n                      color: theme.colorScheme.onSurface,\n                    ),\n                    onPressed: () {\n                      context\n                          .read<TrashBloc>()\n                          .add(TrashEvent.delete(deletedFile));\n                      Fluttertoast.showToast(\n                        msg:\n                            '${deletedFile.name} ${LocaleKeys.trash_mobile_isDeleted.tr()}',\n                        gravity: ToastGravity.BOTTOM,\n                      );\n                    },\n                  ),\n                ],\n              ),\n            ),\n          );\n        },\n        itemCount: state.objects.length,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/home/recent_folder/mobile_recent_view.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/workspace/application/recent/prelude.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nclass MobileRecentFolder extends StatefulWidget {\n  const MobileRecentFolder({super.key});\n\n  @override\n  State<MobileRecentFolder> createState() => _MobileRecentFolderState();\n}\n\nclass _MobileRecentFolderState extends State<MobileRecentFolder> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) =>\n          RecentViewsBloc()..add(const RecentViewsEvent.initial()),\n      child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(\n        listenWhen: (previous, current) =>\n            current.currentWorkspace != null &&\n            previous.currentWorkspace?.workspaceId !=\n                current.currentWorkspace!.workspaceId,\n        listener: (context, state) => context\n            .read<RecentViewsBloc>()\n            .add(const RecentViewsEvent.resetRecentViews()),\n        child: BlocBuilder<RecentViewsBloc, RecentViewsState>(\n          builder: (context, state) {\n            final ids = <String>{};\n\n            List<ViewPB> recentViews = state.views.map((e) => e.item).toList();\n            recentViews.retainWhere((element) => ids.add(element.id));\n\n            // only keep the first 20 items.\n            recentViews = recentViews.take(20).toList();\n\n            if (recentViews.isEmpty) {\n              return const SizedBox.shrink();\n            }\n\n            return Column(\n              children: [\n                _RecentViews(\n                  key: ValueKey(recentViews),\n                  // the recent views are in reverse order\n                  recentViews: recentViews,\n                ),\n                const VSpace(12.0),\n              ],\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass _RecentViews extends StatelessWidget {\n  const _RecentViews({\n    super.key,\n    required this.recentViews,\n  });\n\n  final List<ViewPB> recentViews;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 24),\n          child: GestureDetector(\n            child: FlowyText.semibold(\n              LocaleKeys.sideBar_recent.tr(),\n              fontSize: 20.0,\n            ),\n            onTap: () {\n              showMobileBottomSheet(\n                context,\n                showDivider: false,\n                showDragHandle: true,\n                backgroundColor: AFThemeExtension.of(context).background,\n                builder: (_) {\n                  return Column(\n                    children: [\n                      FlowyOptionTile.text(\n                        text: LocaleKeys.button_clear.tr(),\n                        leftIcon: FlowySvg(\n                          FlowySvgs.m_delete_s,\n                          color: Theme.of(context).colorScheme.error,\n                        ),\n                        textColor: Theme.of(context).colorScheme.error,\n                        onTap: () {\n                          context.read<RecentViewsBloc>().add(\n                                RecentViewsEvent.removeRecentViews(\n                                  recentViews.map((e) => e.id).toList(),\n                                ),\n                              );\n                          context.pop();\n                        },\n                      ),\n                    ],\n                  );\n                },\n              );\n            },\n          ),\n        ),\n        SizedBox(\n          height: 148,\n          child: ListView.separated(\n            key: const PageStorageKey('recent_views_page_storage_key'),\n            padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),\n            scrollDirection: Axis.horizontal,\n            itemBuilder: (context, index) {\n              final view = recentViews[index];\n              return SizedBox.square(\n                dimension: 148,\n                child: MobileRecentView(view: view),\n              );\n            },\n            separatorBuilder: (context, index) => const HSpace(8),\n            itemCount: recentViews.length,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/mobile/application/recent/recent_view_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy/shared/flowy_gradient_colors.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\nimport 'package:string_validator/string_validator.dart';\n\nclass MobileRecentView extends StatelessWidget {\n  const MobileRecentView({\n    super.key,\n    required this.view,\n  });\n\n  final ViewPB view;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = Theme.of(context);\n\n    return BlocProvider<RecentViewBloc>(\n      create: (context) => RecentViewBloc(view: view)\n        ..add(\n          const RecentViewEvent.initial(),\n        ),\n      child: BlocBuilder<RecentViewBloc, RecentViewState>(\n        builder: (context, state) {\n          return GestureDetector(\n            onTap: () => context.pushView(view),\n            child: Stack(\n              children: [\n                DecoratedBox(\n                  decoration: BoxDecoration(\n                    borderRadius: BorderRadius.circular(8),\n                    border: Border.all(color: theme.colorScheme.outline),\n                  ),\n                  child: Column(\n                    mainAxisSize: MainAxisSize.min,\n                    crossAxisAlignment: CrossAxisAlignment.stretch,\n                    children: [\n                      Expanded(child: _buildCover(context, state)),\n                      Expanded(child: _buildTitle(context, state)),\n                    ],\n                  ),\n                ),\n                Align(\n                  alignment: Alignment.centerLeft,\n                  child: _buildIcon(context, state),\n                ),\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildCover(BuildContext context, RecentViewState state) {\n    return Padding(\n      padding: const EdgeInsets.only(top: 1.0, left: 1.0, right: 1.0),\n      child: ClipRRect(\n        borderRadius: const BorderRadius.only(\n          topLeft: Radius.circular(8),\n          topRight: Radius.circular(8),\n        ),\n        child: _RecentCover(\n          coverTypeV1: state.coverTypeV1,\n          coverTypeV2: state.coverTypeV2,\n          value: state.coverValue,\n        ),\n      ),\n    );\n  }\n\n  Widget _buildTitle(BuildContext context, RecentViewState state) {\n    return Padding(\n      padding: const EdgeInsets.fromLTRB(8, 18, 8, 2),\n      // hack: minLines currently not supported in Text widget.\n      // https://github.com/flutter/flutter/issues/31134\n      child: Stack(\n        children: [\n          FlowyText.medium(\n            view.name,\n            fontSize: 16.0,\n            maxLines: 2,\n            overflow: TextOverflow.ellipsis,\n          ),\n          const FlowyText(\n            \"\\n\\n\",\n            maxLines: 2,\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildIcon(BuildContext context, RecentViewState state) {\n    return Padding(\n      padding: const EdgeInsets.only(left: 8.0),\n      child: state.icon.isNotEmpty\n          ? RawEmojiIconWidget(emoji: state.icon, emojiSize: 30)\n          : SizedBox.square(\n              dimension: 32.0,\n              child: view.defaultIcon(),\n            ),\n    );\n  }\n}\n\nclass _RecentCover extends StatelessWidget {\n  const _RecentCover({\n    required this.coverTypeV1,\n    this.coverTypeV2,\n    this.value,\n  });\n\n  final CoverType coverTypeV1;\n  final PageStyleCoverImageType? coverTypeV2;\n  final String? value;\n\n  @override\n  Widget build(BuildContext context) {\n    final placeholder = Container(\n      // random color, update it once we have a better placeholder\n      color:\n          Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.2),\n    );\n    final value = this.value;\n    if (value == null) {\n      return placeholder;\n    }\n    if (coverTypeV2 != null) {\n      return _buildCoverV2(context, value, placeholder);\n    }\n    return _buildCoverV1(context, value, placeholder);\n  }\n\n  Widget _buildCoverV2(BuildContext context, String value, Widget placeholder) {\n    final type = coverTypeV2;\n    if (type == null) {\n      return placeholder;\n    }\n    if (type == PageStyleCoverImageType.customImage ||\n        type == PageStyleCoverImageType.unsplashImage) {\n      final userProfilePB = Provider.of<UserProfilePB?>(context);\n      return FlowyNetworkImage(\n        url: value,\n        userProfilePB: userProfilePB,\n      );\n    }\n\n    if (type == PageStyleCoverImageType.builtInImage) {\n      return Image.asset(\n        PageStyleCoverImageType.builtInImagePath(value),\n        fit: BoxFit.cover,\n      );\n    }\n\n    if (type == PageStyleCoverImageType.pureColor) {\n      final color = value.coverColor(context);\n      if (color != null) {\n        return ColoredBox(\n          color: color,\n        );\n      }\n    }\n\n    if (type == PageStyleCoverImageType.gradientColor) {\n      return Container(\n        decoration: BoxDecoration(\n          gradient: FlowyGradientColor.fromId(value).linear,\n        ),\n      );\n    }\n\n    if (type == PageStyleCoverImageType.localImage) {\n      return Image.file(\n        File(value),\n        fit: BoxFit.cover,\n      );\n    }\n\n    return placeholder;\n  }\n\n  Widget _buildCoverV1(BuildContext context, String value, Widget placeholder) {\n    switch (coverTypeV1) {\n      case CoverType.file:\n        if (isURL(value)) {\n          final userProfilePB = Provider.of<UserProfilePB?>(context);\n          return FlowyNetworkImage(\n            url: value,\n            userProfilePB: userProfilePB,\n          );\n        }\n        final imageFile = File(value);\n        if (!imageFile.existsSync()) {\n          return placeholder;\n        }\n        return Image.file(\n          imageFile,\n        );\n      case CoverType.asset:\n        return Image.asset(\n          value,\n          fit: BoxFit.cover,\n        );\n      case CoverType.color:\n        final color = value.tryToColor() ?? Colors.white;\n        return Container(\n          color: color,\n        );\n      case CoverType.none:\n        return placeholder;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart",
    "content": "import 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart';\nimport 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/recent/prelude.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_slidable/flutter_slidable.dart';\n\nclass MobileRecentSpace extends StatefulWidget {\n  const MobileRecentSpace({super.key});\n\n  @override\n  State<MobileRecentSpace> createState() => _MobileRecentSpaceState();\n}\n\nclass _MobileRecentSpaceState extends State<MobileRecentSpace>\n    with AutomaticKeepAliveClientMixin {\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    return BlocProvider(\n      create: (context) =>\n          RecentViewsBloc()..add(const RecentViewsEvent.initial()),\n      child: BlocBuilder<RecentViewsBloc, RecentViewsState>(\n        builder: (context, state) {\n          if (state.isLoading) {\n            return const SizedBox.shrink();\n          }\n\n          final recentViews = _filterRecentViews(state.views);\n\n          if (recentViews.isEmpty) {\n            return const Center(\n              child: EmptySpacePlaceholder(type: MobilePageCardType.recent),\n            );\n          }\n\n          return _RecentViews(recentViews: recentViews);\n        },\n      ),\n    );\n  }\n\n  List<SectionViewPB> _filterRecentViews(List<SectionViewPB> recentViews) {\n    final ids = <String>{};\n    final filteredRecentViews = recentViews.toList();\n    filteredRecentViews.retainWhere((e) => ids.add(e.item.id));\n    return filteredRecentViews;\n  }\n}\n\nclass _RecentViews extends StatelessWidget {\n  const _RecentViews({\n    required this.recentViews,\n  });\n\n  final List<SectionViewPB> recentViews;\n\n  @override\n  Widget build(BuildContext context) {\n    final borderColor = Theme.of(context).isLightMode\n        ? const Color(0xFFE9E9EC)\n        : const Color(0x1AFFFFFF);\n    return SlidableAutoCloseBehavior(\n      child: ListView.separated(\n        key: const PageStorageKey('recent_views_page_storage_key'),\n        padding: EdgeInsets.only(\n          bottom: HomeSpaceViewSizes.mVerticalPadding +\n              MediaQuery.of(context).padding.bottom,\n        ),\n        itemBuilder: (context, index) {\n          final sectionView = recentViews[index];\n          return Container(\n            padding: const EdgeInsets.symmetric(vertical: 24.0),\n            decoration: BoxDecoration(\n              border: Border(\n                bottom: BorderSide(\n                  color: borderColor,\n                  width: 0.5,\n                ),\n              ),\n            ),\n            child: MobileViewPage(\n              key: ValueKey(sectionView.item.id),\n              view: sectionView.item,\n              timestamp: sectionView.timestamp,\n              type: MobilePageCardType.recent,\n            ),\n          );\n        },\n        separatorBuilder: (context, index) => const HSpace(8),\n        itemCount: recentViews.length,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart';\nimport 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MobileSectionFolder extends StatelessWidget {\n  const MobileSectionFolder({\n    super.key,\n    required this.title,\n    required this.views,\n    required this.spaceType,\n  });\n\n  final String title;\n  final List<ViewPB> views;\n  final FolderSpaceType spaceType;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<FolderBloc>(\n      create: (context) => FolderBloc(type: spaceType)\n        ..add(\n          const FolderEvent.initial(),\n        ),\n      child: BlocBuilder<FolderBloc, FolderState>(\n        builder: (context, state) {\n          return Column(\n            children: [\n              SizedBox(\n                height: HomeSpaceViewSizes.mViewHeight,\n                child: MobileSectionFolderHeader(\n                  title: title,\n                  isExpanded: context.read<FolderBloc>().state.isExpanded,\n                  onPressed: () => context\n                      .read<FolderBloc>()\n                      .add(const FolderEvent.expandOrUnExpand()),\n                  onAdded: () => _createNewPage(context),\n                ),\n              ),\n              if (state.isExpanded)\n                Padding(\n                  padding: const EdgeInsets.only(\n                    left: HomeSpaceViewSizes.leftPadding,\n                  ),\n                  child: _Pages(\n                    views: views,\n                    spaceType: spaceType,\n                  ),\n                ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  void _createNewPage(BuildContext context) {\n    context.read<SidebarSectionsBloc>().add(\n          SidebarSectionsEvent.createRootViewInSection(\n            name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),\n            index: 0,\n            viewSection: spaceType.toViewSectionPB,\n          ),\n        );\n    context.read<FolderBloc>().add(\n          const FolderEvent.expandOrUnExpand(isExpanded: true),\n        );\n  }\n}\n\nclass _Pages extends StatelessWidget {\n  const _Pages({\n    required this.views,\n    required this.spaceType,\n  });\n\n  final List<ViewPB> views;\n  final FolderSpaceType spaceType;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: views\n          .map(\n            (view) => MobileViewItem(\n              key: ValueKey(\n                '${FolderSpaceType.private.name} ${view.id}',\n              ),\n              spaceType: spaceType,\n              isFirstChild: view.id == views.first.id,\n              view: view,\n              level: 0,\n              leftPadding: HomeSpaceViewSizes.leftPadding,\n              isFeedback: false,\n              onSelected: (v) => context.pushView(\n                v,\n                tabs: [\n                  PickerTabType.emoji,\n                  PickerTabType.icon,\n                  PickerTabType.custom,\n                ].map((e) => e.name).toList(),\n              ),\n              endActionPane: (context) {\n                final view = context.read<ViewBloc>().state.view;\n                return buildEndActionPane(\n                  context,\n                  [\n                    MobilePaneActionType.more,\n                    if (view.layout == ViewLayoutPB.Document)\n                      MobilePaneActionType.add,\n                  ],\n                  spaceType: spaceType,\n                  needSpace: false,\n                  spaceRatio: 5,\n                );\n              },\n            ),\n          )\n          .toList(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\n@visibleForTesting\nconst Key mobileCreateNewPageButtonKey = Key('mobileCreateNewPageButtonKey');\n\nclass MobileSectionFolderHeader extends StatefulWidget {\n  const MobileSectionFolderHeader({\n    super.key,\n    required this.title,\n    required this.onPressed,\n    required this.onAdded,\n    required this.isExpanded,\n  });\n\n  final String title;\n  final VoidCallback onPressed;\n  final VoidCallback onAdded;\n  final bool isExpanded;\n\n  @override\n  State<MobileSectionFolderHeader> createState() =>\n      _MobileSectionFolderHeaderState();\n}\n\nclass _MobileSectionFolderHeaderState extends State<MobileSectionFolderHeader> {\n  double _turns = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        const HSpace(HomeSpaceViewSizes.mHorizontalPadding),\n        Expanded(\n          child: FlowyButton(\n            text: FlowyText.medium(\n              widget.title,\n              fontSize: 16.0,\n            ),\n            margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 2.0),\n            expandText: false,\n            iconPadding: 2,\n            mainAxisAlignment: MainAxisAlignment.start,\n            rightIcon: AnimatedRotation(\n              duration: const Duration(milliseconds: 200),\n              turns: _turns,\n              child: const FlowySvg(\n                FlowySvgs.m_spaces_expand_s,\n              ),\n            ),\n            onTap: () {\n              setState(() {\n                _turns = widget.isExpanded ? -0.25 : 0;\n              });\n              widget.onPressed();\n            },\n          ),\n        ),\n        GestureDetector(\n          behavior: HitTestBehavior.translucent,\n          onTap: widget.onAdded,\n          child: Container(\n            // expand the touch area\n            margin: const EdgeInsets.symmetric(\n              horizontal: HomeSpaceViewSizes.mHorizontalPadding,\n              vertical: 8.0,\n            ),\n            child: const FlowySvg(\n              FlowySvgs.m_space_add_s,\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:appflowy/mobile/presentation/setting/workspace/invite_members_screen.dart';\nimport 'package:appflowy/shared/popup_menu/appflowy_popup_menu.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart'\n    hide PopupMenuButton, PopupMenuDivider, PopupMenuItem, PopupMenuEntry;\nimport 'package:go_router/go_router.dart';\n\nenum _MobileSettingsPopupMenuItem {\n  settings,\n  members,\n  trash,\n  help,\n  helpAndDocumentation,\n}\n\nclass HomePageSettingsPopupMenu extends StatelessWidget {\n  const HomePageSettingsPopupMenu({\n    super.key,\n    required this.userProfile,\n  });\n\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    return PopupMenuButton<_MobileSettingsPopupMenuItem>(\n      offset: const Offset(0, 36),\n      padding: EdgeInsets.zero,\n      shape: const RoundedRectangleBorder(\n        borderRadius: BorderRadius.all(\n          Radius.circular(12.0),\n        ),\n      ),\n      shadowColor: const Color(0x68000000),\n      elevation: 10,\n      color: context.popupMenuBackgroundColor,\n      itemBuilder: (BuildContext context) =>\n          <PopupMenuEntry<_MobileSettingsPopupMenuItem>>[\n        _buildItem(\n          value: _MobileSettingsPopupMenuItem.settings,\n          svg: FlowySvgs.m_notification_settings_s,\n          text: LocaleKeys.settings_popupMenuItem_settings.tr(),\n        ),\n        // only show the member items in cloud mode\n        if (userProfile.workspaceType == WorkspaceTypePB.ServerW) ...[\n          const PopupMenuDivider(height: 0.5),\n          _buildItem(\n            value: _MobileSettingsPopupMenuItem.members,\n            svg: FlowySvgs.m_settings_member_s,\n            text: LocaleKeys.settings_popupMenuItem_members.tr(),\n          ),\n        ],\n        const PopupMenuDivider(height: 0.5),\n        _buildItem(\n          value: _MobileSettingsPopupMenuItem.trash,\n          svg: FlowySvgs.trash_s,\n          text: LocaleKeys.settings_popupMenuItem_trash.tr(),\n        ),\n        const PopupMenuDivider(height: 0.5),\n        _buildItem(\n          value: _MobileSettingsPopupMenuItem.helpAndDocumentation,\n          svg: FlowySvgs.help_and_documentation_s,\n          text: LocaleKeys.settings_popupMenuItem_helpAndDocumentation.tr(),\n        ),\n        const PopupMenuDivider(height: 0.5),\n        _buildItem(\n          value: _MobileSettingsPopupMenuItem.help,\n          svg: FlowySvgs.message_support_s,\n          text: LocaleKeys.settings_popupMenuItem_getSupport.tr(),\n        ),\n      ],\n      onSelected: (_MobileSettingsPopupMenuItem value) {\n        switch (value) {\n          case _MobileSettingsPopupMenuItem.members:\n            _openMembersPage(context);\n            break;\n          case _MobileSettingsPopupMenuItem.trash:\n            _openTrashPage(context);\n            break;\n          case _MobileSettingsPopupMenuItem.settings:\n            _openSettingsPage(context);\n            break;\n          case _MobileSettingsPopupMenuItem.help:\n            _openHelpPage(context);\n            break;\n          case _MobileSettingsPopupMenuItem.helpAndDocumentation:\n            _openHelpAndDocumentationPage(context);\n            break;\n        }\n      },\n      child: const Padding(\n        padding: EdgeInsets.all(8.0),\n        child: FlowySvg(\n          FlowySvgs.m_settings_more_s,\n        ),\n      ),\n    );\n  }\n\n  PopupMenuItem<T> _buildItem<T>({\n    required T value,\n    required FlowySvgData svg,\n    required String text,\n  }) {\n    return PopupMenuItem<T>(\n      value: value,\n      padding: EdgeInsets.zero,\n      child: _PopupButton(\n        svg: svg,\n        text: text,\n      ),\n    );\n  }\n\n  void _openMembersPage(BuildContext context) {\n    context.push(InviteMembersScreen.routeName);\n  }\n\n  void _openTrashPage(BuildContext context) {\n    context.push(MobileHomeTrashPage.routeName);\n  }\n\n  void _openHelpPage(BuildContext context) {\n    afLaunchUrlString('https://discord.com/invite/9Q2xaN37tV');\n  }\n\n  void _openSettingsPage(BuildContext context) {\n    context.push(MobileHomeSettingPage.routeName);\n  }\n\n  void _openHelpAndDocumentationPage(BuildContext context) {\n    afLaunchUrlString('https://appflowy.com/guide');\n  }\n}\n\nclass _PopupButton extends StatelessWidget {\n  const _PopupButton({\n    required this.svg,\n    required this.text,\n  });\n\n  final FlowySvgData svg;\n  final String text;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: 44,\n      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),\n      child: Row(\n        children: [\n          FlowySvg(svg, size: const Size.square(20)),\n          const HSpace(12),\n          FlowyText.regular(\n            text,\n            fontSize: 16,\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/shared/empty_placeholder.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/home/shared/mobile_page_card.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass EmptySpacePlaceholder extends StatelessWidget {\n  const EmptySpacePlaceholder({\n    super.key,\n    required this.type,\n  });\n\n  final MobilePageCardType type;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 48.0),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          const FlowySvg(\n            FlowySvgs.m_empty_page_xl,\n          ),\n          const VSpace(16.0),\n          FlowyText.medium(\n            _emptyPageText,\n            fontSize: 18.0,\n            textAlign: TextAlign.center,\n          ),\n          const VSpace(8.0),\n          FlowyText.regular(\n            _emptyPageSubText,\n            fontSize: 17.0,\n            maxLines: 10,\n            textAlign: TextAlign.center,\n            lineHeight: 1.3,\n            color: Theme.of(context).hintColor,\n          ),\n          const VSpace(kBottomNavigationBarHeight + 36.0),\n        ],\n      ),\n    );\n  }\n\n  String get _emptyPageText => switch (type) {\n        MobilePageCardType.recent => LocaleKeys.sideBar_emptyRecent.tr(),\n        MobilePageCardType.favorite => LocaleKeys.sideBar_emptyFavorite.tr(),\n      };\n\n  String get _emptyPageSubText => switch (type) {\n        MobilePageCardType.recent =>\n          LocaleKeys.sideBar_emptyRecentDescription.tr(),\n        MobilePageCardType.favorite =>\n          LocaleKeys.sideBar_emptyFavoriteDescription.tr(),\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/mobile/application/recent/recent_view_bloc.dart';\nimport 'package:appflowy/mobile/presentation/base/animated_gesture.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy/shared/flowy_gradient_colors.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';\nimport 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_slidable/flutter_slidable.dart';\nimport 'package:provider/provider.dart';\nimport 'package:string_validator/string_validator.dart';\nimport 'package:time/time.dart';\n\nenum MobilePageCardType {\n  recent,\n  favorite;\n\n  String get lastOperationHintText => switch (this) {\n        MobilePageCardType.recent => LocaleKeys.sideBar_lastViewed.tr(),\n        MobilePageCardType.favorite => LocaleKeys.sideBar_favoriteAt.tr(),\n      };\n}\n\nclass MobileViewPage extends StatelessWidget {\n  const MobileViewPage({\n    super.key,\n    required this.view,\n    this.timestamp,\n    required this.type,\n  });\n\n  final ViewPB view;\n  final Int64? timestamp;\n  final MobilePageCardType type;\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider<ViewBloc>(\n          create: (context) => ViewBloc(view: view, shouldLoadChildViews: false)\n            ..add(const ViewEvent.initial()),\n        ),\n        BlocProvider(\n          create: (context) =>\n              RecentViewBloc(view: view)..add(const RecentViewEvent.initial()),\n        ),\n      ],\n      child: BlocBuilder<RecentViewBloc, RecentViewState>(\n        builder: (context, state) {\n          return Slidable(\n            endActionPane: buildEndActionPane(\n              context,\n              [\n                MobilePaneActionType.more,\n                context.watch<ViewBloc>().state.view.isFavorite\n                    ? MobilePaneActionType.removeFromFavorites\n                    : MobilePaneActionType.addToFavorites,\n              ],\n              cardType: type,\n              spaceRatio: 4,\n            ),\n            child: AnimatedGestureDetector(\n              onTapUp: () => context.pushView(\n                view,\n                tabs: [\n                  PickerTabType.emoji,\n                  PickerTabType.icon,\n                  PickerTabType.custom,\n                ].map((e) => e.name).toList(),\n              ),\n              child: Row(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  const HSpace(HomeSpaceViewSizes.mHorizontalPadding),\n                  Expanded(child: _buildDescription(context, state)),\n                  const HSpace(20.0),\n                  SizedBox(\n                    width: 84,\n                    height: 60,\n                    child: _buildCover(context, state),\n                  ),\n                  const HSpace(HomeSpaceViewSizes.mHorizontalPadding),\n                ],\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildDescription(BuildContext context, RecentViewState state) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        // page icon & page title\n        _buildTitle(context, state),\n        const VSpace(12.0),\n        // author & last viewed\n        _buildNameAndLastViewed(context, state),\n      ],\n    );\n  }\n\n  Widget _buildNameAndLastViewed(BuildContext context, RecentViewState state) {\n    final supportAvatar = isURL(state.icon.emoji);\n    if (!supportAvatar) {\n      return _buildLastViewed(context);\n    }\n    return Row(\n      children: [\n        _buildAvatar(context, state),\n        Flexible(child: _buildAuthor(context, state)),\n        const Padding(\n          padding: EdgeInsets.symmetric(horizontal: 3.0),\n          child: FlowySvg(FlowySvgs.dot_s),\n        ),\n        _buildLastViewed(context),\n      ],\n    );\n  }\n\n  Widget _buildAvatar(BuildContext context, RecentViewState state) {\n    final userProfile = Provider.of<UserProfilePB?>(context);\n    final iconUrl = userProfile?.iconUrl;\n    if (iconUrl == null ||\n        iconUrl.isEmpty ||\n        view.createdBy != userProfile?.id ||\n        !isURL(iconUrl)) {\n      return const SizedBox.shrink();\n    }\n\n    return Padding(\n      padding: const EdgeInsets.only(top: 2, bottom: 2, right: 8),\n      child: ClipRRect(\n        borderRadius: BorderRadius.circular(8.0),\n        child: SizedBox.square(\n          dimension: 16.0,\n          child: FlowyNetworkImage(\n            url: iconUrl,\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildCover(BuildContext context, RecentViewState state) {\n    return ClipRRect(\n      borderRadius: BorderRadius.circular(8),\n      child: _ViewCover(\n        layout: view.layout,\n        coverTypeV1: state.coverTypeV1,\n        coverTypeV2: state.coverTypeV2,\n        value: state.coverValue,\n      ),\n    );\n  }\n\n  Widget _buildTitle(BuildContext context, RecentViewState state) {\n    final name = state.name;\n    final icon = state.icon;\n    return RichText(\n      maxLines: 3,\n      overflow: TextOverflow.ellipsis,\n      text: TextSpan(\n        children: [\n          if (icon.isNotEmpty) ...[\n            WidgetSpan(\n              child: SizedBox(\n                width: 20,\n                child: RawEmojiIconWidget(\n                  emoji: icon,\n                  emojiSize: 18.0,\n                ),\n              ),\n            ),\n            const WidgetSpan(child: HSpace(8.0)),\n          ],\n          TextSpan(\n            text: name.orDefault(\n              LocaleKeys.menuAppHeader_defaultNewPageName.tr(),\n            ),\n            style: Theme.of(context).textTheme.bodyMedium!.copyWith(\n                  fontSize: 16.0,\n                  fontWeight: FontWeight.w600,\n                  height: 1.3,\n                ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildAuthor(BuildContext context, RecentViewState state) {\n    return FlowyText.regular(\n      // view.createdBy.toString(),\n      '',\n      fontSize: 12.0,\n      color: Theme.of(context).hintColor,\n      overflow: TextOverflow.ellipsis,\n    );\n  }\n\n  Widget _buildLastViewed(BuildContext context) {\n    final textColor = Theme.of(context).isLightMode\n        ? const Color(0x7F171717)\n        : Colors.white.withValues(alpha: 0.45);\n    if (timestamp == null) {\n      return const SizedBox.shrink();\n    }\n    final date = _formatTimestamp(\n      context,\n      timestamp!.toInt() * 1000,\n    );\n    return FlowyText.regular(\n      date,\n      fontSize: 13.0,\n      color: textColor,\n    );\n  }\n\n  String _formatTimestamp(BuildContext context, int timestamp) {\n    final now = DateTime.now();\n    final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);\n    final difference = now.difference(dateTime);\n    final String date;\n\n    final dateFormate =\n        context.read<AppearanceSettingsCubit>().state.dateFormat;\n    final timeFormate =\n        context.read<AppearanceSettingsCubit>().state.timeFormat;\n\n    if (difference.inMinutes < 1) {\n      date = LocaleKeys.sideBar_justNow.tr();\n    } else if (difference.inHours < 1 && dateTime.isToday) {\n      // Less than 1 hour\n      date = LocaleKeys.sideBar_minutesAgo\n          .tr(namedArgs: {'count': difference.inMinutes.toString()});\n    } else if (difference.inHours >= 1 && dateTime.isToday) {\n      // in same day\n      date = timeFormate.formatTime(dateTime);\n    } else {\n      date = dateFormate.formatDate(dateTime, false);\n    }\n\n    if (difference.inHours >= 1) {\n      return '${type.lastOperationHintText} $date';\n    }\n\n    return date;\n  }\n}\n\nclass _ViewCover extends StatelessWidget {\n  const _ViewCover({\n    required this.layout,\n    required this.coverTypeV1,\n    this.coverTypeV2,\n    this.value,\n  });\n\n  final ViewLayoutPB layout;\n  final CoverType coverTypeV1;\n  final PageStyleCoverImageType? coverTypeV2;\n  final String? value;\n\n  @override\n  Widget build(BuildContext context) {\n    final placeholder = _buildPlaceholder(context);\n    final value = this.value;\n    if (value == null) {\n      return placeholder;\n    }\n    if (coverTypeV2 != null) {\n      return _buildCoverV2(context, value, placeholder);\n    }\n    return _buildCoverV1(context, value, placeholder);\n  }\n\n  Widget _buildPlaceholder(BuildContext context) {\n    final isLightMode = Theme.of(context).isLightMode;\n    final (svg, color) = switch (layout) {\n      ViewLayoutPB.Document => (\n          FlowySvgs.m_document_thumbnail_m,\n          isLightMode ? const Color(0xCCEDFBFF) : const Color(0x33658B90)\n        ),\n      ViewLayoutPB.Grid => (\n          FlowySvgs.m_grid_thumbnail_m,\n          isLightMode ? const Color(0xFFF5F4FF) : const Color(0x338B80AD)\n        ),\n      ViewLayoutPB.Board => (\n          FlowySvgs.m_board_thumbnail_m,\n          isLightMode ? const Color(0x7FE0FDD9) : const Color(0x3372936B),\n        ),\n      ViewLayoutPB.Calendar => (\n          FlowySvgs.m_calendar_thumbnail_m,\n          isLightMode ? const Color(0xFFFFF7F0) : const Color(0x33A68B77)\n        ),\n      ViewLayoutPB.Chat => (\n          FlowySvgs.m_chat_thumbnail_m,\n          isLightMode ? const Color(0x66FFE6FD) : const Color(0x33987195)\n        ),\n      _ => (\n          FlowySvgs.m_document_thumbnail_m,\n          isLightMode ? Colors.black : Colors.white\n        )\n    };\n    return ColoredBox(\n      color: color,\n      child: Center(\n        child: FlowySvg(\n          svg,\n          blendMode: null,\n        ),\n      ),\n    );\n  }\n\n  Widget _buildCoverV2(BuildContext context, String value, Widget placeholder) {\n    final type = coverTypeV2;\n    if (type == null) {\n      return placeholder;\n    }\n    if (type == PageStyleCoverImageType.customImage ||\n        type == PageStyleCoverImageType.unsplashImage) {\n      final userProfilePB = Provider.of<UserProfilePB?>(context);\n      return FlowyNetworkImage(\n        url: value,\n        userProfilePB: userProfilePB,\n      );\n    }\n\n    if (type == PageStyleCoverImageType.builtInImage) {\n      return Image.asset(\n        PageStyleCoverImageType.builtInImagePath(value),\n        fit: BoxFit.cover,\n      );\n    }\n\n    if (type == PageStyleCoverImageType.pureColor) {\n      final color = value.coverColor(context);\n      if (color != null) {\n        return ColoredBox(\n          color: color,\n        );\n      }\n    }\n\n    if (type == PageStyleCoverImageType.gradientColor) {\n      return Container(\n        decoration: BoxDecoration(\n          gradient: FlowyGradientColor.fromId(value).linear,\n        ),\n      );\n    }\n\n    if (type == PageStyleCoverImageType.localImage) {\n      return Image.file(\n        File(value),\n        fit: BoxFit.cover,\n      );\n    }\n\n    return placeholder;\n  }\n\n  Widget _buildCoverV1(BuildContext context, String value, Widget placeholder) {\n    switch (coverTypeV1) {\n      case CoverType.file:\n        if (isURL(value)) {\n          final userProfilePB = Provider.of<UserProfilePB?>(context);\n          return FlowyNetworkImage(\n            url: value,\n            userProfilePB: userProfilePB,\n          );\n        }\n        final imageFile = File(value);\n        if (!imageFile.existsSync()) {\n          return placeholder;\n        }\n        return Image.file(\n          imageFile,\n        );\n      case CoverType.asset:\n        return Image.asset(\n          value,\n          fit: BoxFit.cover,\n        );\n      case CoverType.color:\n        final color = value.tryToColor() ?? Colors.white;\n        return Container(\n          color: color,\n        );\n      case CoverType.none:\n        return placeholder;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/space/constants.dart",
    "content": "class SpaceUIConstants {\n  static const itemHeight = 52.0;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/space/manage_space_widget.dart",
    "content": "import 'package:appflowy/shared/icon_emoji_picker/icon.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart' hide Icon;\n\nimport 'widgets.dart';\n\nenum ManageSpaceType {\n  create,\n  edit,\n}\n\nclass ManageSpaceWidget extends StatelessWidget {\n  const ManageSpaceWidget({\n    super.key,\n    required this.controller,\n    required this.permission,\n    required this.selectedColor,\n    required this.selectedIcon,\n    required this.type,\n  });\n\n  final TextEditingController controller;\n  final ValueNotifier<SpacePermission> permission;\n  final ValueNotifier<String> selectedColor;\n  final ValueNotifier<Icon?> selectedIcon;\n  final ManageSpaceType type;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        ManageSpaceNameOption(\n          controller: controller,\n          type: type,\n        ),\n        ManageSpacePermissionOption(permission: permission),\n        ConstrainedBox(\n          constraints: const BoxConstraints(\n            maxHeight: 560,\n          ),\n          child: ManageSpaceIconOption(\n            selectedColor: selectedColor,\n            selectedIcon: selectedIcon,\n          ),\n        ),\n        const VSpace(60),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/home/space/mobile_space_header.dart';\nimport 'package:appflowy/mobile/presentation/home/space/mobile_space_menu.dart';\nimport 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/shared/list_extension.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MobileSpace extends StatelessWidget {\n  const MobileSpace({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SpaceBloc, SpaceState>(\n      builder: (context, state) {\n        if (state.spaces.isEmpty) {\n          return const SizedBox.shrink();\n        }\n\n        final currentSpace = state.currentSpace ?? state.spaces.first;\n\n        return Column(\n          children: [\n            MobileSpaceHeader(\n              isExpanded: state.isExpanded,\n              space: currentSpace,\n              onAdded: () => _showCreatePageMenu(context, currentSpace),\n              onPressed: () => _showSpaceMenu(context),\n            ),\n            Padding(\n              padding: const EdgeInsets.only(\n                left: HomeSpaceViewSizes.mHorizontalPadding,\n              ),\n              child: _Pages(\n                key: ValueKey(currentSpace.id),\n                space: currentSpace,\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  void _showSpaceMenu(BuildContext context) {\n    showMobileBottomSheet(\n      context,\n      showDivider: false,\n      showHeader: true,\n      showDragHandle: true,\n      showCloseButton: true,\n      showDoneButton: true,\n      useRootNavigator: true,\n      title: LocaleKeys.space_title.tr(),\n      backgroundColor: Theme.of(context).colorScheme.surface,\n      enableScrollable: true,\n      bottomSheetPadding: context.bottomSheetPadding(),\n      builder: (_) {\n        return BlocProvider.value(\n          value: context.read<SpaceBloc>(),\n          child: const Padding(\n            padding: EdgeInsets.symmetric(horizontal: 8.0),\n            child: MobileSpaceMenu(),\n          ),\n        );\n      },\n    );\n  }\n\n  void _showCreatePageMenu(BuildContext context, ViewPB space) {\n    final title = space.name;\n    showMobileBottomSheet(\n      context,\n      showHeader: true,\n      title: title,\n      showDragHandle: true,\n      showCloseButton: true,\n      useRootNavigator: true,\n      showDivider: false,\n      backgroundColor: Theme.of(context).colorScheme.surface,\n      builder: (sheetContext) {\n        return AddNewPageWidgetBottomSheet(\n          view: space,\n          onAction: (layout) {\n            Navigator.of(sheetContext).pop();\n            context.read<SpaceBloc>().add(\n                  SpaceEvent.createPage(\n                    name: '',\n                    layout: layout,\n                    index: 0,\n                    openAfterCreate: true,\n                  ),\n                );\n            context.read<SpaceBloc>().add(\n                  SpaceEvent.expand(space, true),\n                );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass _Pages extends StatelessWidget {\n  const _Pages({\n    super.key,\n    required this.space,\n  });\n\n  final ViewPB space;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) =>\n          ViewBloc(view: space)..add(const ViewEvent.initial()),\n      child: BlocBuilder<ViewBloc, ViewState>(\n        builder: (context, state) {\n          final spaceType = space.spacePermission == SpacePermission.publicToAll\n              ? FolderSpaceType.public\n              : FolderSpaceType.private;\n          final childViews = state.view.childViews.unique((view) => view.id);\n          if (childViews.length != state.view.childViews.length) {\n            final duplicatedViews = state.view.childViews\n                .where((view) => childViews.contains(view))\n                .toList();\n            Log.error('some view id are duplicated: $duplicatedViews');\n          }\n          return Column(\n            children: childViews\n                .map(\n                  (view) => MobileViewItem(\n                    key: ValueKey(\n                      '${space.id} ${view.id}',\n                    ),\n                    spaceType: spaceType,\n                    isFirstChild: view.id == state.view.childViews.first.id,\n                    view: view,\n                    level: 0,\n                    leftPadding: HomeSpaceViewSizes.leftPadding,\n                    isFeedback: false,\n                    onSelected: (v) => context.pushView(\n                      v,\n                      tabs: [\n                        PickerTabType.emoji,\n                        PickerTabType.icon,\n                        PickerTabType.custom,\n                      ].map((e) => e.name).toList(),\n                    ),\n                    endActionPane: (context) {\n                      final view = context.read<ViewBloc>().state.view;\n                      final actions = [\n                        MobilePaneActionType.more,\n                        if (view.layout == ViewLayoutPB.Document)\n                          MobilePaneActionType.add,\n                      ];\n                      return buildEndActionPane(\n                        context,\n                        actions,\n                        spaceType: spaceType,\n                        spaceRatio: actions.length == 1 ? 3 : 4,\n                      );\n                    },\n                  ),\n                )\n                .toList(),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_header.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\n@visibleForTesting\nconst Key mobileCreateNewPageButtonKey = Key('mobileCreateNewPageButtonKey');\n\nclass MobileSpaceHeader extends StatelessWidget {\n  const MobileSpaceHeader({\n    super.key,\n    required this.space,\n    required this.onPressed,\n    required this.onAdded,\n    required this.isExpanded,\n  });\n\n  final ViewPB space;\n  final VoidCallback onPressed;\n  final VoidCallback onAdded;\n  final bool isExpanded;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      behavior: HitTestBehavior.translucent,\n      onTap: onPressed,\n      child: SizedBox(\n        height: 48,\n        child: Row(\n          children: [\n            const HSpace(HomeSpaceViewSizes.mHorizontalPadding),\n            SpaceIcon(\n              dimension: 24,\n              space: space,\n              svgSize: 14,\n              textDimension: 18.0,\n              cornerRadius: 6.0,\n            ),\n            const HSpace(8),\n            FlowyText.medium(\n              space.name,\n              lineHeight: 1.15,\n              fontSize: 16.0,\n            ),\n            const HSpace(4.0),\n            const FlowySvg(\n              FlowySvgs.workspace_drop_down_menu_show_s,\n            ),\n            const Spacer(),\n            GestureDetector(\n              behavior: HitTestBehavior.translucent,\n              onTap: onAdded,\n              child: Container(\n                // expand the touch area\n                margin: const EdgeInsets.symmetric(\n                  horizontal: HomeSpaceViewSizes.mHorizontalPadding,\n                  vertical: 8.0,\n                ),\n                child: const FlowySvg(\n                  FlowySvgs.m_space_add_s,\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  // Future<void> _onAction(SpaceMoreActionType type, dynamic data) async {\n  //   switch (type) {\n  //     case SpaceMoreActionType.rename:\n  //       await _showRenameDialog();\n  //       break;\n  //     case SpaceMoreActionType.changeIcon:\n  //       final (String icon, String iconColor) = data;\n  //       context.read<SpaceBloc>().add(SpaceEvent.changeIcon(icon, iconColor));\n  //       break;\n  //     case SpaceMoreActionType.manage:\n  //       _showManageSpaceDialog(context);\n  //       break;\n  //     case SpaceMoreActionType.addNewSpace:\n  //       break;\n  //     case SpaceMoreActionType.collapseAllPages:\n  //       break;\n  //     case SpaceMoreActionType.delete:\n  //       _showDeleteSpaceDialog(context);\n  //       break;\n  //     case SpaceMoreActionType.divider:\n  //       break;\n  //   }\n  // }\n\n  // Future<void> _showRenameDialog() async {\n  //   await NavigatorTextFieldDialog(\n  //     title: LocaleKeys.space_rename.tr(),\n  //     value: space.name,\n  //     autoSelectAllText: true,\n  //     onConfirm: (name, _) {\n  //       context.read<SpaceBloc>().add(SpaceEvent.rename(space, name));\n  //     },\n  //   ).show(context);\n  // }\n\n  // void _showManageSpaceDialog(BuildContext context) {\n  //   final spaceBloc = context.read<SpaceBloc>();\n  //   showDialog(\n  //     context: context,\n  //     builder: (_) {\n  //       return Dialog(\n  //         shape: RoundedRectangleBorder(\n  //           borderRadius: BorderRadius.circular(12.0),\n  //         ),\n  //         child: BlocProvider.value(\n  //           value: spaceBloc,\n  //           child: const ManageSpacePopup(),\n  //         ),\n  //       );\n  //     },\n  //   );\n  // }\n\n  // void _showDeleteSpaceDialog(BuildContext context) {\n  //   final spaceBloc = context.read<SpaceBloc>();\n  //   showDialog(\n  //     context: context,\n  //     builder: (_) {\n  //       return Dialog(\n  //         shape: RoundedRectangleBorder(\n  //           borderRadius: BorderRadius.circular(12.0),\n  //         ),\n  //         child: BlocProvider.value(\n  //           value: spaceBloc,\n  //           child: const SizedBox(width: 440, child: DeleteSpacePopup()),\n  //         ),\n  //       );\n  //     },\n  //   );\n  // }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_menu.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/animated_gesture.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/home/space/space_menu_bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/home/workspaces/create_workspace_menu.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/util/navigator_context_extension.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart' hide Icon;\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'constants.dart';\nimport 'manage_space_widget.dart';\n\nclass MobileSpaceMenu extends StatelessWidget {\n  const MobileSpaceMenu({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SpaceBloc, SpaceState>(\n      builder: (context, state) {\n        return SingleChildScrollView(\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              const VSpace(4.0),\n              for (final space in state.spaces)\n                SizedBox(\n                  height: SpaceUIConstants.itemHeight,\n                  child: MobileSpaceMenuItem(\n                    space: space,\n                    isSelected: state.currentSpace?.id == space.id,\n                  ),\n                ),\n              const Padding(\n                padding: EdgeInsets.symmetric(vertical: 8.0),\n                child: Divider(\n                  height: 0.5,\n                ),\n              ),\n              const SizedBox(\n                height: SpaceUIConstants.itemHeight,\n                child: _CreateSpaceButton(),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass MobileSpaceMenuItem extends StatelessWidget {\n  const MobileSpaceMenuItem({\n    super.key,\n    required this.space,\n    required this.isSelected,\n  });\n\n  final ViewPB space;\n  final bool isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      text: Row(\n        children: [\n          FlowyText.medium(\n            space.name,\n            fontSize: 16.0,\n          ),\n          const HSpace(6.0),\n          if (space.spacePermission == SpacePermission.private)\n            const FlowySvg(\n              FlowySvgs.space_lock_s,\n              size: Size.square(12),\n            ),\n        ],\n      ),\n      margin: const EdgeInsets.symmetric(horizontal: 12.0),\n      iconPadding: 10,\n      leftIcon: SpaceIcon(\n        dimension: 24,\n        space: space,\n        svgSize: 14,\n        textDimension: 18.0,\n        cornerRadius: 6.0,\n      ),\n      leftIconSize: const Size.square(24),\n      rightIcon: SpaceMenuItemTrailing(\n        key: ValueKey('${space.id}_space_menu_item_trailing'),\n        space: space,\n        currentSpace: context.read<SpaceBloc>().state.currentSpace,\n      ),\n      onTap: () {\n        context.read<SpaceBloc>().add(SpaceEvent.open(space: space));\n        Navigator.of(context).pop();\n      },\n    );\n  }\n}\n\nclass _CreateSpaceButton extends StatefulWidget {\n  const _CreateSpaceButton();\n\n  @override\n  State<_CreateSpaceButton> createState() => _CreateSpaceButtonState();\n}\n\nclass _CreateSpaceButtonState extends State<_CreateSpaceButton> {\n  final controller = TextEditingController();\n  final permission = ValueNotifier<SpacePermission>(\n    SpacePermission.publicToAll,\n  );\n  final selectedColor = ValueNotifier<String>(\n    builtInSpaceColors.first,\n  );\n  final selectedIcon = ValueNotifier<Icon?>(\n    kIconGroups?.first.icons.first,\n  );\n\n  @override\n  void dispose() {\n    controller.dispose();\n    permission.dispose();\n    selectedColor.dispose();\n    selectedIcon.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      text: FlowyText.regular(LocaleKeys.space_createNewSpace.tr()),\n      iconPadding: 10,\n      leftIcon: const Padding(\n        padding: EdgeInsets.all(2.0),\n        child: FlowySvg(\n          FlowySvgs.space_add_s,\n        ),\n      ),\n      margin: const EdgeInsets.symmetric(horizontal: 12.0),\n      leftIconSize: const Size.square(24),\n      onTap: () => _showCreateSpaceDialog(context),\n    );\n  }\n\n  Future<void> _showCreateSpaceDialog(BuildContext context) async {\n    await showMobileBottomSheet(\n      context,\n      showHeader: true,\n      title: LocaleKeys.space_createSpace.tr(),\n      showCloseButton: true,\n      showDivider: false,\n      showDoneButton: true,\n      enableScrollable: true,\n      showDragHandle: true,\n      bottomSheetPadding: context.bottomSheetPadding(),\n      onDone: (bottomSheetContext) {\n        final iconPath = selectedIcon.value?.iconPath ?? '';\n        context.read<SpaceBloc>().add(\n              SpaceEvent.create(\n                name: controller.text.orDefault(\n                  LocaleKeys.space_defaultSpaceName.tr(),\n                ),\n                permission: permission.value,\n                iconColor: selectedColor.value,\n                icon: iconPath,\n                createNewPageByDefault: true,\n                openAfterCreate: false,\n              ),\n            );\n        Navigator.pop(bottomSheetContext);\n        Navigator.pop(context);\n\n        Log.info(\n          'create space on mobile, name: ${controller.text}, permission: ${permission.value}, color: ${selectedColor.value}, icon: $iconPath',\n        );\n      },\n      padding: const EdgeInsets.symmetric(horizontal: 16),\n      builder: (bottomSheetContext) => ManageSpaceWidget(\n        controller: controller,\n        permission: permission,\n        selectedColor: selectedColor,\n        selectedIcon: selectedIcon,\n        type: ManageSpaceType.create,\n      ),\n    );\n\n    _resetState();\n  }\n\n  void _resetState() {\n    controller.clear();\n    permission.value = SpacePermission.publicToAll;\n    selectedColor.value = builtInSpaceColors.first;\n    selectedIcon.value = kIconGroups?.first.icons.first;\n  }\n}\n\nclass SpaceMenuItemTrailing extends StatefulWidget {\n  const SpaceMenuItemTrailing({\n    super.key,\n    required this.space,\n    this.currentSpace,\n  });\n\n  final ViewPB space;\n  final ViewPB? currentSpace;\n\n  @override\n  State<SpaceMenuItemTrailing> createState() => _SpaceMenuItemTrailingState();\n}\n\nclass _SpaceMenuItemTrailingState extends State<SpaceMenuItemTrailing> {\n  final controller = TextEditingController();\n  final permission = ValueNotifier<SpacePermission>(\n    SpacePermission.publicToAll,\n  );\n  final selectedColor = ValueNotifier<String>(\n    builtInSpaceColors.first,\n  );\n  final selectedIcon = ValueNotifier<Icon?>(\n    kIconGroups?.first.icons.first,\n  );\n\n  @override\n  void dispose() {\n    controller.dispose();\n    permission.dispose();\n    selectedColor.dispose();\n    selectedIcon.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    const iconSize = Size.square(20);\n    return Row(\n      children: [\n        const HSpace(12.0),\n        // show the check icon if the space is the current space\n        if (widget.space.id == widget.currentSpace?.id)\n          const FlowySvg(\n            FlowySvgs.m_blue_check_s,\n            size: iconSize,\n            blendMode: null,\n          ),\n        const HSpace(8.0),\n        // more options button\n        AnimatedGestureDetector(\n          onTapUp: () => _showMoreOptions(context),\n          child: const Padding(\n            padding: EdgeInsets.all(8.0),\n            child: FlowySvg(\n              FlowySvgs.workspace_three_dots_s,\n              size: iconSize,\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  void _showMoreOptions(BuildContext context) {\n    final actions = [\n      SpaceMoreActionType.rename,\n      SpaceMoreActionType.duplicate,\n      SpaceMoreActionType.manage,\n      SpaceMoreActionType.delete,\n    ];\n\n    showMobileBottomSheet(\n      context,\n      showDragHandle: true,\n      showDivider: false,\n      useRootNavigator: true,\n      backgroundColor: Theme.of(context).colorScheme.surface,\n      builder: (bottomSheetContext) {\n        return SpaceMenuMoreOptions(\n          actions: actions,\n          onAction: (action) => _onActions(\n            context,\n            bottomSheetContext,\n            action,\n          ),\n        );\n      },\n    );\n  }\n\n  void _onActions(\n    BuildContext context,\n    BuildContext bottomSheetContext,\n    SpaceMoreActionType action,\n  ) {\n    Log.info('execute action in space menu bottom sheet: $action');\n\n    switch (action) {\n      case SpaceMoreActionType.rename:\n        _showRenameSpaceBottomSheet(context);\n        break;\n      case SpaceMoreActionType.duplicate:\n        _duplicateSpace(context, bottomSheetContext);\n        break;\n      case SpaceMoreActionType.manage:\n        _showManageSpaceBottomSheet(context);\n        break;\n      case SpaceMoreActionType.delete:\n        _deleteSpace(context, bottomSheetContext);\n        break;\n      default:\n        assert(false, 'Unsupported action: $action');\n        break;\n    }\n  }\n\n  void _duplicateSpace(BuildContext context, BuildContext bottomSheetContext) {\n    Log.info('duplicate the space: ${widget.space.name}');\n\n    context.read<SpaceBloc>().add(const SpaceEvent.duplicate());\n\n    showToastNotification(\n      message: LocaleKeys.space_success_duplicateSpace.tr(),\n    );\n\n    Navigator.of(bottomSheetContext).pop();\n    Navigator.of(context).pop();\n  }\n\n  void _showRenameSpaceBottomSheet(BuildContext context) {\n    Navigator.of(context).pop();\n\n    showMobileBottomSheet(\n      context,\n      showHeader: true,\n      title: LocaleKeys.space_renameSpace.tr(),\n      showCloseButton: true,\n      showDragHandle: true,\n      showDivider: false,\n      padding: const EdgeInsets.symmetric(horizontal: 16),\n      builder: (bottomSheetContext) {\n        return EditWorkspaceNameBottomSheet(\n          type: EditWorkspaceNameType.edit,\n          workspaceName: widget.space.name,\n          hintText: LocaleKeys.space_spaceNamePlaceholder.tr(),\n          validator: (value) => null,\n          onSubmitted: (name) {\n            // rename the workspace\n            Log.info('rename the space, from: ${widget.space.name}, to: $name');\n            bottomSheetContext.popToHome();\n\n            context\n                .read<SpaceBloc>()\n                .add(SpaceEvent.rename(space: widget.space, name: name));\n\n            showToastNotification(\n              message: LocaleKeys.space_success_renameSpace.tr(),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  Future<void> _showManageSpaceBottomSheet(BuildContext context) async {\n    controller.text = widget.space.name;\n    permission.value = widget.space.spacePermission;\n    selectedColor.value =\n        widget.space.spaceIconColor ?? builtInSpaceColors.first;\n    selectedIcon.value = widget.space.spaceIcon?.icon;\n\n    await showMobileBottomSheet(\n      context,\n      showHeader: true,\n      title: LocaleKeys.space_manageSpace.tr(),\n      showCloseButton: true,\n      showDivider: false,\n      showDoneButton: true,\n      enableScrollable: true,\n      showDragHandle: true,\n      bottomSheetPadding: context.bottomSheetPadding(),\n      onDone: (bottomSheetContext) {\n        String iconName = '';\n        final icon = selectedIcon.value;\n        final iconGroup = icon?.iconGroup;\n        final iconId = icon?.name;\n        if (icon != null && iconGroup != null) {\n          iconName = '${iconGroup.name}/$iconId';\n        }\n        Log.info(\n          'update space on mobile, name: ${controller.text}, permission: ${permission.value}, color: ${selectedColor.value}, icon: $iconName',\n        );\n        context.read<SpaceBloc>().add(\n              SpaceEvent.update(\n                space: widget.space,\n                name: controller.text.orDefault(\n                  LocaleKeys.space_defaultSpaceName.tr(),\n                ),\n                permission: permission.value,\n                iconColor: selectedColor.value,\n                icon: iconName,\n              ),\n            );\n\n        showToastNotification(\n          message: LocaleKeys.space_success_updateSpace.tr(),\n        );\n\n        Navigator.pop(bottomSheetContext);\n        Navigator.pop(context);\n      },\n      padding: const EdgeInsets.symmetric(horizontal: 16),\n      builder: (bottomSheetContext) => ManageSpaceWidget(\n        controller: controller,\n        permission: permission,\n        selectedColor: selectedColor,\n        selectedIcon: selectedIcon,\n        type: ManageSpaceType.edit,\n      ),\n    );\n  }\n\n  void _deleteSpace(\n    BuildContext context,\n    BuildContext bottomSheetContext,\n  ) {\n    Navigator.of(bottomSheetContext).pop();\n\n    _showConfirmDialog(\n      context,\n      '${LocaleKeys.space_delete.tr()}: ${widget.space.name}',\n      LocaleKeys.space_deleteConfirmationDescription.tr(),\n      LocaleKeys.button_delete.tr(),\n      (_) async {\n        context.read<SpaceBloc>().add(SpaceEvent.delete(widget.space));\n\n        showToastNotification(\n          message: LocaleKeys.space_success_deleteSpace.tr(),\n        );\n\n        Navigator.pop(context);\n      },\n    );\n  }\n\n  void _showConfirmDialog(\n    BuildContext context,\n    String title,\n    String content,\n    String rightButtonText,\n    void Function(BuildContext context)? onRightButtonPressed,\n  ) {\n    showFlowyCupertinoConfirmDialog(\n      title: title,\n      content: FlowyText(\n        content,\n        fontSize: 14,\n        maxLines: 10,\n      ),\n      leftButton: FlowyText(\n        LocaleKeys.button_cancel.tr(),\n        fontSize: 17.0,\n        figmaLineHeight: 24.0,\n        fontWeight: FontWeight.w500,\n        color: const Color(0xFF007AFF),\n      ),\n      rightButton: FlowyText(\n        rightButtonText,\n        fontSize: 17.0,\n        figmaLineHeight: 24.0,\n        fontWeight: FontWeight.w400,\n        color: const Color(0xFFFE0220),\n      ),\n      onRightButtonPressed: onRightButtonPressed,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/space/space_menu_bottom_sheet.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nimport 'constants.dart';\n\nclass SpaceMenuMoreOptions extends StatelessWidget {\n  const SpaceMenuMoreOptions({\n    super.key,\n    required this.onAction,\n    required this.actions,\n  });\n\n  final void Function(SpaceMoreActionType action) onAction;\n  final List<SpaceMoreActionType> actions;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.stretch,\n      children: actions\n          .map(\n            (action) => _buildActionButton(context, action),\n          )\n          .toList(),\n    );\n  }\n\n  Widget _buildActionButton(\n    BuildContext context,\n    SpaceMoreActionType action,\n  ) {\n    switch (action) {\n      case SpaceMoreActionType.rename:\n        return FlowyOptionTile.text(\n          text: LocaleKeys.button_rename.tr(),\n          height: SpaceUIConstants.itemHeight,\n          leftIcon: const FlowySvg(\n            FlowySvgs.view_item_rename_s,\n            size: Size.square(18),\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(\n            SpaceMoreActionType.rename,\n          ),\n        );\n      case SpaceMoreActionType.delete:\n        return FlowyOptionTile.text(\n          text: LocaleKeys.button_delete.tr(),\n          height: SpaceUIConstants.itemHeight,\n          textColor: Theme.of(context).colorScheme.error,\n          leftIcon: FlowySvg(\n            FlowySvgs.trash_s,\n            size: const Size.square(18),\n            color: Theme.of(context).colorScheme.error,\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(\n            SpaceMoreActionType.delete,\n          ),\n        );\n      case SpaceMoreActionType.manage:\n        return FlowyOptionTile.text(\n          text: LocaleKeys.space_manage.tr(),\n          height: SpaceUIConstants.itemHeight,\n          leftIcon: const FlowySvg(\n            FlowySvgs.settings_s,\n            size: Size.square(18),\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(\n            SpaceMoreActionType.manage,\n          ),\n        );\n      case SpaceMoreActionType.duplicate:\n        return FlowyOptionTile.text(\n          text: SpaceMoreActionType.duplicate.name,\n          height: SpaceUIConstants.itemHeight,\n          leftIcon: const FlowySvg(\n            FlowySvgs.duplicate_s,\n            size: Size.square(18),\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(\n            SpaceMoreActionType.duplicate,\n          ),\n        );\n      default:\n        return const SizedBox.shrink();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/space/space_permission_bottom_sheet.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nclass SpacePermissionBottomSheet extends StatelessWidget {\n  const SpacePermissionBottomSheet({\n    super.key,\n    required this.onAction,\n    required this.permission,\n  });\n\n  final SpacePermission permission;\n  final void Function(SpacePermission action) onAction;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        FlowyOptionTile.text(\n          text: LocaleKeys.space_publicPermission.tr(),\n          leftIcon: const FlowySvg(\n            FlowySvgs.space_permission_public_s,\n          ),\n          trailing: permission == SpacePermission.publicToAll\n              ? const FlowySvg(\n                  FlowySvgs.m_blue_check_s,\n                  blendMode: null,\n                )\n              : null,\n          onTap: () => onAction(SpacePermission.publicToAll),\n        ),\n        FlowyOptionTile.text(\n          text: LocaleKeys.space_privatePermission.tr(),\n          showTopBorder: false,\n          leftIcon: const FlowySvg(\n            FlowySvgs.space_permission_private_s,\n          ),\n          trailing: permission == SpacePermission.private\n              ? const FlowySvg(\n                  FlowySvgs.m_blue_check_s,\n                  blendMode: null,\n                )\n              : null,\n          onTap: () => onAction(SpacePermission.private),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/space/widgets.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/animated_gesture.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/colors.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart' hide Icon;\n\nimport 'constants.dart';\nimport 'manage_space_widget.dart';\nimport 'space_permission_bottom_sheet.dart';\n\nclass ManageSpaceNameOption extends StatelessWidget {\n  const ManageSpaceNameOption({\n    super.key,\n    required this.controller,\n    required this.type,\n  });\n\n  final TextEditingController controller;\n  final ManageSpaceType type;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Padding(\n          padding: const EdgeInsets.only(left: 16, bottom: 4),\n          child: FlowyText(\n            LocaleKeys.space_spaceName.tr(),\n            fontSize: 14,\n            figmaLineHeight: 20.0,\n            fontWeight: FontWeight.w400,\n            color: Theme.of(context).hintColor,\n          ),\n        ),\n        FlowyOptionTile.textField(\n          controller: controller,\n          autofocus: type == ManageSpaceType.create ? true : false,\n          textFieldHintText: LocaleKeys.space_spaceNamePlaceholder.tr(),\n        ),\n        const VSpace(16),\n      ],\n    );\n  }\n}\n\nclass ManageSpacePermissionOption extends StatelessWidget {\n  const ManageSpacePermissionOption({\n    super.key,\n    required this.permission,\n  });\n\n  final ValueNotifier<SpacePermission> permission;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Padding(\n          padding: const EdgeInsets.only(left: 16, bottom: 4),\n          child: FlowyText(\n            LocaleKeys.space_permission.tr(),\n            fontSize: 14,\n            figmaLineHeight: 20.0,\n            fontWeight: FontWeight.w400,\n            color: Theme.of(context).hintColor,\n          ),\n        ),\n        ValueListenableBuilder(\n          valueListenable: permission,\n          builder: (context, value, child) => FlowyOptionTile.text(\n            height: SpaceUIConstants.itemHeight,\n            text: value.i18n,\n            leftIcon: FlowySvg(value.icon),\n            trailing: const FlowySvg(\n              FlowySvgs.arrow_right_s,\n            ),\n            onTap: () {\n              showMobileBottomSheet(\n                context,\n                showHeader: true,\n                title: LocaleKeys.space_permission.tr(),\n                showCloseButton: true,\n                showDivider: false,\n                showDragHandle: true,\n                builder: (context) => SpacePermissionBottomSheet(\n                  permission: value,\n                  onAction: (value) {\n                    permission.value = value;\n                    Navigator.pop(context);\n                  },\n                ),\n              );\n            },\n          ),\n        ),\n        const VSpace(16),\n      ],\n    );\n  }\n}\n\nclass ManageSpaceIconOption extends StatefulWidget {\n  const ManageSpaceIconOption({\n    super.key,\n    required this.selectedColor,\n    required this.selectedIcon,\n  });\n\n  final ValueNotifier<String> selectedColor;\n  final ValueNotifier<Icon?> selectedIcon;\n\n  @override\n  State<ManageSpaceIconOption> createState() => _ManageSpaceIconOptionState();\n}\n\nclass _ManageSpaceIconOptionState extends State<ManageSpaceIconOption> {\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        ..._buildColorOption(context),\n        ..._buildSpaceIconOption(context),\n      ],\n    );\n  }\n\n  List<Widget> _buildColorOption(BuildContext context) {\n    return [\n      Padding(\n        padding: const EdgeInsets.only(left: 16, bottom: 4),\n        child: FlowyText(\n          LocaleKeys.space_mSpaceIconColor.tr(),\n          fontSize: 14,\n          figmaLineHeight: 20.0,\n          fontWeight: FontWeight.w400,\n          color: Theme.of(context).hintColor,\n        ),\n      ),\n      ValueListenableBuilder(\n        valueListenable: widget.selectedColor,\n        builder: (context, selectedColor, child) {\n          return FlowyOptionDecorateBox(\n            child: Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),\n              child: SingleChildScrollView(\n                scrollDirection: Axis.horizontal,\n                child: Row(\n                  children: builtInSpaceColors.map((color) {\n                    return SpaceColorItem(\n                      color: color,\n                      selectedColor: selectedColor,\n                      onSelected: (color) => widget.selectedColor.value = color,\n                    );\n                  }).toList(),\n                ),\n              ),\n            ),\n          );\n        },\n      ),\n      const VSpace(16),\n    ];\n  }\n\n  List<Widget> _buildSpaceIconOption(BuildContext context) {\n    return [\n      Padding(\n        padding: const EdgeInsets.only(left: 16, bottom: 4),\n        child: FlowyText(\n          LocaleKeys.space_mSpaceIcon.tr(),\n          fontSize: 14,\n          figmaLineHeight: 20.0,\n          fontWeight: FontWeight.w400,\n          color: Theme.of(context).hintColor,\n        ),\n      ),\n      Expanded(\n        child: SizedBox(\n          width: double.infinity,\n          child: ValueListenableBuilder(\n            valueListenable: widget.selectedColor,\n            builder: (context, selectedColor, child) {\n              return ValueListenableBuilder(\n                valueListenable: widget.selectedIcon,\n                builder: (context, selectedIcon, child) {\n                  return FlowyOptionDecorateBox(\n                    child: Padding(\n                      padding: const EdgeInsets.symmetric(\n                        horizontal: 16,\n                      ),\n                      child: _buildIconGroups(\n                        context,\n                        selectedColor,\n                        selectedIcon,\n                      ),\n                    ),\n                  );\n                },\n              );\n            },\n          ),\n        ),\n      ),\n      const VSpace(16),\n    ];\n  }\n\n  Widget _buildIconGroups(\n    BuildContext context,\n    String selectedColor,\n    Icon? selectedIcon,\n  ) {\n    final iconGroups = kIconGroups;\n    if (iconGroups == null) {\n      return const SizedBox.shrink();\n    }\n\n    return ListView.builder(\n      itemCount: iconGroups.length,\n      itemBuilder: (context, index) {\n        final iconGroup = iconGroups[index];\n        return Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            const VSpace(12.0),\n            FlowyText(\n              iconGroup.displayName.capitalize(),\n              fontSize: 12,\n              figmaLineHeight: 18.0,\n              color: context.pickerTextColor,\n            ),\n            const VSpace(4.0),\n            Center(\n              child: Wrap(\n                spacing: 10.0,\n                runSpacing: 8.0,\n                children: iconGroup.icons.map((icon) {\n                  return SpaceIconItem(\n                    icon: icon,\n                    isSelected: selectedIcon?.name == icon.name,\n                    selectedColor: selectedColor,\n                    onSelectedIcon: (icon) => widget.selectedIcon.value = icon,\n                  );\n                }).toList(),\n              ),\n            ),\n            const VSpace(12.0),\n            if (index == iconGroups.length - 1) ...[\n              const StreamlinePermit(),\n            ],\n          ],\n        );\n      },\n    );\n  }\n}\n\nclass SpaceIconItem extends StatelessWidget {\n  const SpaceIconItem({\n    super.key,\n    required this.icon,\n    required this.onSelectedIcon,\n    required this.isSelected,\n    required this.selectedColor,\n  });\n\n  final Icon icon;\n  final void Function(Icon icon) onSelectedIcon;\n  final bool isSelected;\n  final String selectedColor;\n\n  @override\n  Widget build(BuildContext context) {\n    return AnimatedGestureDetector(\n      onTapUp: () => onSelectedIcon(icon),\n      child: Container(\n        width: 36,\n        height: 36,\n        decoration: isSelected\n            ? BoxDecoration(\n                color: Color(int.parse(selectedColor)),\n                borderRadius: BorderRadius.circular(8.0),\n              )\n            : ShapeDecoration(\n                color: Colors.transparent,\n                shape: RoundedRectangleBorder(\n                  side: const BorderSide(\n                    width: 0.5,\n                    color: Color(0x661F2329),\n                  ),\n                  borderRadius: BorderRadius.circular(8),\n                ),\n              ),\n        child: Center(\n          child: FlowySvg.string(\n            icon.content,\n            size: const Size.square(18),\n            color: isSelected\n                ? Theme.of(context).colorScheme.surface\n                : context.pickerIconColor,\n            opacity: isSelected ? 1.0 : 0.7,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass SpaceColorItem extends StatelessWidget {\n  const SpaceColorItem({\n    super.key,\n    required this.color,\n    required this.selectedColor,\n    required this.onSelected,\n  });\n\n  final String color;\n  final String selectedColor;\n  final void Function(String color) onSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    final child = Center(\n      child: Container(\n        width: 28,\n        height: 28,\n        decoration: BoxDecoration(\n          color: Color(int.parse(color)),\n          borderRadius: BorderRadius.circular(14),\n        ),\n      ),\n    );\n\n    final decoration = color != selectedColor\n        ? null\n        : ShapeDecoration(\n            color: Colors.transparent,\n            shape: RoundedRectangleBorder(\n              side: BorderSide(\n                width: 1.50,\n                color: Theme.of(context).colorScheme.primary,\n              ),\n              borderRadius: BorderRadius.circular(21),\n            ),\n          );\n\n    return AnimatedGestureDetector(\n      onTapUp: () => onSelected(color),\n      child: Container(\n        width: 36,\n        height: 36,\n        decoration: decoration,\n        child: child,\n      ),\n    );\n  }\n}\n\nextension on SpacePermission {\n  String get i18n {\n    switch (this) {\n      case SpacePermission.publicToAll:\n        return LocaleKeys.space_publicPermission.tr();\n      case SpacePermission.private:\n        return LocaleKeys.space_privatePermission.tr();\n    }\n  }\n\n  FlowySvgData get icon {\n    switch (this) {\n      case SpacePermission.publicToAll:\n        return FlowySvgs.space_permission_public_s;\n      case SpacePermission.private:\n        return FlowySvgs.space_permission_private_s;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_round_underline_tab_indicator.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass RoundUnderlineTabIndicator extends Decoration {\n  const RoundUnderlineTabIndicator({\n    this.borderRadius,\n    this.borderSide = const BorderSide(width: 2.0, color: Colors.white),\n    this.insets = EdgeInsets.zero,\n    required this.width,\n  });\n\n  final BorderRadius? borderRadius;\n  final BorderSide borderSide;\n  final EdgeInsetsGeometry insets;\n  final double width;\n\n  @override\n  Decoration? lerpFrom(Decoration? a, double t) {\n    if (a is UnderlineTabIndicator) {\n      return UnderlineTabIndicator(\n        borderSide: BorderSide.lerp(a.borderSide, borderSide, t),\n        insets: EdgeInsetsGeometry.lerp(a.insets, insets, t)!,\n      );\n    }\n    return super.lerpFrom(a, t);\n  }\n\n  @override\n  Decoration? lerpTo(Decoration? b, double t) {\n    if (b is UnderlineTabIndicator) {\n      return UnderlineTabIndicator(\n        borderSide: BorderSide.lerp(borderSide, b.borderSide, t),\n        insets: EdgeInsetsGeometry.lerp(insets, b.insets, t)!,\n      );\n    }\n    return super.lerpTo(b, t);\n  }\n\n  @override\n  BoxPainter createBoxPainter([VoidCallback? onChanged]) {\n    return _UnderlinePainter(this, borderRadius, onChanged);\n  }\n\n  Rect _indicatorRectFor(Rect rect, TextDirection textDirection) {\n    final Rect indicator = insets.resolve(textDirection).deflateRect(rect);\n    final center = indicator.center.dx;\n    return Rect.fromLTWH(\n      center - width / 2.0,\n      indicator.bottom - borderSide.width,\n      width,\n      borderSide.width,\n    );\n  }\n\n  @override\n  Path getClipPath(Rect rect, TextDirection textDirection) {\n    if (borderRadius != null) {\n      return Path()\n        ..addRRect(\n          borderRadius!.toRRect(_indicatorRectFor(rect, textDirection)),\n        );\n    }\n    return Path()..addRect(_indicatorRectFor(rect, textDirection));\n  }\n}\n\nclass _UnderlinePainter extends BoxPainter {\n  _UnderlinePainter(\n    this.decoration,\n    this.borderRadius,\n    super.onChanged,\n  );\n\n  final RoundUnderlineTabIndicator decoration;\n  final BorderRadius? borderRadius;\n\n  @override\n  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {\n    assert(configuration.size != null);\n    final Rect rect = offset & configuration.size!;\n    final TextDirection textDirection = configuration.textDirection!;\n    final Paint paint;\n    if (borderRadius != null) {\n      paint = Paint()..color = decoration.borderSide.color;\n      final Rect indicator = decoration._indicatorRectFor(rect, textDirection);\n      final RRect rrect = RRect.fromRectAndCorners(\n        indicator,\n        topLeft: borderRadius!.topLeft,\n        topRight: borderRadius!.topRight,\n        bottomRight: borderRadius!.bottomRight,\n        bottomLeft: borderRadius!.bottomLeft,\n      );\n      canvas.drawRRect(rrect, paint);\n    } else {\n      paint = decoration.borderSide.toPaint()..strokeCap = StrokeCap.round;\n      final Rect indicator = decoration\n          ._indicatorRectFor(rect, textDirection)\n          .deflate(decoration.borderSide.width / 2.0);\n      canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, paint);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart",
    "content": "import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart';\nimport 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';\nimport 'package:flutter/material.dart';\nimport 'package:reorderable_tabbar/reorderable_tabbar.dart';\n\nclass MobileSpaceTabBar extends StatelessWidget {\n  const MobileSpaceTabBar({\n    super.key,\n    this.height = 38.0,\n    required this.tabController,\n    required this.tabs,\n    required this.onReorder,\n  });\n\n  final double height;\n  final List<MobileSpaceTabType> tabs;\n  final TabController tabController;\n  final OnReorder onReorder;\n\n  @override\n  Widget build(BuildContext context) {\n    final baseStyle = Theme.of(context).textTheme.bodyMedium;\n    final labelStyle = baseStyle?.copyWith(\n      fontWeight: FontWeight.w500,\n      fontSize: 16.0,\n      height: 22.0 / 16.0,\n    );\n    final unselectedLabelStyle = baseStyle?.copyWith(\n      fontWeight: FontWeight.w400,\n      fontSize: 15.0,\n      height: 22.0 / 15.0,\n    );\n\n    return Container(\n      height: height,\n      padding: const EdgeInsets.only(left: 8.0),\n      child: ReorderableTabBar(\n        controller: tabController,\n        tabs: tabs.map((e) => Tab(text: e.tr)).toList(),\n        indicatorSize: TabBarIndicatorSize.label,\n        indicatorColor: Theme.of(context).primaryColor,\n        isScrollable: true,\n        labelStyle: labelStyle,\n        labelColor: baseStyle?.color,\n        labelPadding: const EdgeInsets.symmetric(horizontal: 12.0),\n        unselectedLabelStyle: unselectedLabelStyle,\n        overlayColor: WidgetStateProperty.all(Colors.transparent),\n        indicator: RoundUnderlineTabIndicator(\n          width: 28.0,\n          borderSide: BorderSide(\n            color: Theme.of(context).primaryColor,\n            width: 3,\n          ),\n        ),\n        onReorder: onReorder,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/animated_gesture.dart';\nimport 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass FloatingAIEntry extends StatelessWidget {\n  const FloatingAIEntry({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return AnimatedGestureDetector(\n      scaleFactor: 0.99,\n      onTapUp: () => mobileCreateNewAIChatNotifier.value =\n          mobileCreateNewAIChatNotifier.value + 1,\n      child: Hero(\n        tag: \"ai_chat_prompt\",\n        child: DecoratedBox(\n          decoration: _buildShadowDecoration(context),\n          child: Container(\n            decoration: _buildWrapperDecoration(context),\n            height: 48,\n            alignment: Alignment.centerLeft,\n            child: Padding(\n              padding: const EdgeInsets.only(left: 18),\n              child: _buildHintText(context),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  BoxDecoration _buildShadowDecoration(BuildContext context) {\n    return BoxDecoration(\n      borderRadius: BorderRadius.circular(30),\n      boxShadow: [\n        BoxShadow(\n          blurRadius: 20,\n          spreadRadius: 1,\n          offset: const Offset(0, 4),\n          color: Colors.black.withValues(alpha: 0.05),\n        ),\n      ],\n    );\n  }\n\n  BoxDecoration _buildWrapperDecoration(BuildContext context) {\n    final outlineColor = Theme.of(context).colorScheme.outline;\n    final borderColor = Theme.of(context).isLightMode\n        ? outlineColor.withValues(alpha: 0.7)\n        : outlineColor.withValues(alpha: 0.3);\n    return BoxDecoration(\n      borderRadius: BorderRadius.circular(30),\n      color: Theme.of(context).colorScheme.surface,\n      border: Border.fromBorderSide(\n        BorderSide(\n          color: borderColor,\n        ),\n      ),\n    );\n  }\n\n  Widget _buildHintText(BuildContext context) {\n    return Row(\n      children: [\n        FlowySvg(\n          FlowySvgs.toolbar_item_ai_s,\n          size: const Size.square(16.0),\n          color: Theme.of(context).hintColor,\n          opacity: 0.7,\n        ),\n        const HSpace(8),\n        FlowyText(\n          LocaleKeys.chat_inputMessageHint.tr(),\n          color: Theme.of(context).hintColor,\n        ),\n      ],\n    );\n  }\n}\n\nclass FloatingAIEntryV2 extends StatelessWidget {\n  const FloatingAIEntryV2({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return GestureDetector(\n      onTap: () {\n        mobileCreateNewAIChatNotifier.value =\n            mobileCreateNewAIChatNotifier.value + 1;\n      },\n      child: Container(\n        width: 56,\n        height: 56,\n        decoration: BoxDecoration(\n          shape: BoxShape.circle,\n          color: theme.surfaceColorScheme.primary,\n          boxShadow: theme.shadow.small,\n          border: Border.all(color: theme.borderColorScheme.primary),\n        ),\n        child: Center(\n          child: FlowySvg(\n            FlowySvgs.m_home_ai_chat_icon_m,\n            blendMode: null,\n            size: Size(24, 24),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart",
    "content": "import 'package:appflowy/features/shared_section/presentation/m_shared_section.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/mobile/presentation/home/favorite_folder/favorite_space.dart';\nimport 'package:appflowy/mobile/presentation/home/home_space/home_space.dart';\nimport 'package:appflowy/mobile/presentation/home/recent_folder/recent_space.dart';\nimport 'package:appflowy/mobile/presentation/home/tab/_tab_bar.dart';\nimport 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';\nimport 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:appflowy/mobile/presentation/setting/workspace/invite_members_screen.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\n\nimport 'ai_bubble_button.dart';\n\nfinal ValueNotifier<int> mobileCreateNewAIChatNotifier = ValueNotifier(0);\n\nclass MobileHomePageTab extends StatefulWidget {\n  const MobileHomePageTab({\n    super.key,\n    required this.userProfile,\n  });\n\n  final UserProfilePB userProfile;\n\n  @override\n  State<MobileHomePageTab> createState() => _MobileHomePageTabState();\n}\n\nclass _MobileHomePageTabState extends State<MobileHomePageTab>\n    with SingleTickerProviderStateMixin {\n  TabController? tabController;\n\n  @override\n  void initState() {\n    super.initState();\n\n    mobileCreateNewPageNotifier.addListener(_createNewDocument);\n    mobileCreateNewAIChatNotifier.addListener(_createNewAIChat);\n    mobileLeaveWorkspaceNotifier.addListener(_leaveWorkspace);\n  }\n\n  @override\n  void dispose() {\n    tabController?.removeListener(_onTabChange);\n    tabController?.dispose();\n\n    mobileCreateNewPageNotifier.removeListener(_createNewDocument);\n    mobileCreateNewAIChatNotifier.removeListener(_createNewAIChat);\n    mobileLeaveWorkspaceNotifier.removeListener(_leaveWorkspace);\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Provider.value(\n      value: widget.userProfile,\n      child: MultiBlocListener(\n        listeners: [\n          BlocListener<SpaceBloc, SpaceState>(\n            listenWhen: (p, c) =>\n                p.lastCreatedPage?.id != c.lastCreatedPage?.id,\n            listener: (context, state) {\n              final lastCreatedPage = state.lastCreatedPage;\n              if (lastCreatedPage != null) {\n                context.pushView(\n                  lastCreatedPage,\n                  tabs: [\n                    PickerTabType.emoji,\n                    PickerTabType.icon,\n                    PickerTabType.custom,\n                  ].map((e) => e.name).toList(),\n                );\n              }\n            },\n          ),\n          BlocListener<SidebarSectionsBloc, SidebarSectionsState>(\n            listenWhen: (p, c) =>\n                p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,\n            listener: (context, state) {\n              final lastCreatedPage = state.lastCreatedRootView;\n              if (lastCreatedPage != null) {\n                context.pushView(\n                  lastCreatedPage,\n                  tabs: [\n                    PickerTabType.emoji,\n                    PickerTabType.icon,\n                    PickerTabType.custom,\n                  ].map((e) => e.name).toList(),\n                );\n              }\n            },\n          ),\n        ],\n        child: BlocBuilder<SpaceOrderBloc, SpaceOrderState>(\n          builder: (context, state) {\n            if (state.isLoading) {\n              return const SizedBox.shrink();\n            }\n\n            _initTabController(state);\n\n            return Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                MobileSpaceTabBar(\n                  tabController: tabController!,\n                  tabs: state.tabsOrder,\n                  onReorder: (from, to) {\n                    context.read<SpaceOrderBloc>().add(\n                          SpaceOrderEvent.reorder(from, to),\n                        );\n                  },\n                ),\n                const HSpace(12.0),\n                Expanded(\n                  child: TabBarView(\n                    controller: tabController,\n                    children: _buildTabs(state),\n                  ),\n                ),\n              ],\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  void _initTabController(SpaceOrderState state) {\n    if (tabController != null) {\n      return;\n    }\n    tabController = TabController(\n      length: state.tabsOrder.length,\n      vsync: this,\n      initialIndex: state.tabsOrder.indexOf(state.defaultTab),\n    );\n    tabController?.addListener(_onTabChange);\n  }\n\n  void _onTabChange() {\n    if (tabController == null) {\n      return;\n    }\n    context\n        .read<SpaceOrderBloc>()\n        .add(SpaceOrderEvent.open(tabController!.index));\n  }\n\n  List<Widget> _buildTabs(SpaceOrderState state) {\n    return state.tabsOrder.map((tab) {\n      switch (tab) {\n        case MobileSpaceTabType.recent:\n          return const MobileRecentSpace();\n        case MobileSpaceTabType.spaces:\n          final showAIFloatingButton =\n              widget.userProfile.workspaceType == WorkspaceTypePB.ServerW;\n          return Stack(\n            children: [\n              MobileHomeSpace(userProfile: widget.userProfile),\n              if (showAIFloatingButton)\n                Positioned(\n                  right: 20,\n                  bottom: MediaQuery.of(context).padding.bottom + 16,\n                  child: FloatingAIEntryV2(),\n                ),\n            ],\n          );\n        case MobileSpaceTabType.favorites:\n          return MobileFavoriteSpace(userProfile: widget.userProfile);\n        case MobileSpaceTabType.shared:\n          final workspaceId = context\n              .read<UserWorkspaceBloc>()\n              .state\n              .currentWorkspace\n              ?.workspaceId;\n          if (workspaceId == null) {\n            return const SizedBox.shrink();\n          }\n          return MSharedSection(\n            workspaceId: workspaceId,\n          );\n      }\n    }).toList();\n  }\n\n  // quick create new page when clicking the add button in navigation bar\n  void _createNewDocument() => _createNewPage(ViewLayoutPB.Document);\n\n  void _createNewAIChat() => _createNewPage(ViewLayoutPB.Chat);\n\n  void _createNewPage(ViewLayoutPB layout) {\n    if (context.read<SpaceBloc>().state.spaces.isNotEmpty) {\n      context.read<SpaceBloc>().add(\n            SpaceEvent.createPage(\n              name: '',\n              layout: layout,\n              openAfterCreate: true,\n            ),\n          );\n    } else if (layout == ViewLayoutPB.Document) {\n      // only support create document in section\n      context.read<SidebarSectionsBloc>().add(\n            SidebarSectionsEvent.createRootViewInSection(\n              name: '',\n              index: 0,\n              viewSection: FolderSpaceType.public.toViewSectionPB,\n            ),\n          );\n    }\n  }\n\n  void _leaveWorkspace() {\n    final workspaceId =\n        context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId;\n    if (workspaceId == null) {\n      return Log.error('Workspace ID is null');\n    }\n    context\n        .read<UserWorkspaceBloc>()\n        .add(UserWorkspaceEvent.leaveWorkspace(workspaceId: workspaceId));\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/tab/space_order_bloc.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'space_order_bloc.freezed.dart';\n\nenum MobileSpaceTabType {\n  // DO NOT CHANGE THE ORDER\n  spaces,\n  recent,\n  favorites,\n  shared;\n\n  String get tr {\n    switch (this) {\n      case MobileSpaceTabType.recent:\n        return LocaleKeys.sideBar_RecentSpace.tr();\n      case MobileSpaceTabType.spaces:\n        return LocaleKeys.sideBar_Spaces.tr();\n      case MobileSpaceTabType.favorites:\n        return LocaleKeys.sideBar_favoriteSpace.tr();\n      case MobileSpaceTabType.shared:\n        return 'Shared';\n    }\n  }\n}\n\nclass SpaceOrderBloc extends Bloc<SpaceOrderEvent, SpaceOrderState> {\n  SpaceOrderBloc() : super(const SpaceOrderState()) {\n    on<SpaceOrderEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            final tabsOrder = await _getTabsOrder();\n            final defaultTab = await _getDefaultTab();\n            emit(\n              state.copyWith(\n                tabsOrder: tabsOrder,\n                defaultTab: defaultTab,\n                isLoading: false,\n              ),\n            );\n          },\n          open: (index) async {\n            final tab = state.tabsOrder[index];\n            await _setDefaultTab(tab);\n          },\n          reorder: (from, to) async {\n            final tabsOrder = List.of(state.tabsOrder);\n            tabsOrder.insert(to, tabsOrder.removeAt(from));\n            await _setTabsOrder(tabsOrder);\n            emit(state.copyWith(tabsOrder: tabsOrder));\n          },\n        );\n      },\n    );\n  }\n\n  final _storage = getIt<KeyValueStorage>();\n\n  Future<MobileSpaceTabType> _getDefaultTab() async {\n    try {\n      return await _storage.getWithFormat<MobileSpaceTabType>(\n              KVKeys.lastOpenedSpace, (value) {\n            return MobileSpaceTabType.values[int.parse(value)];\n          }) ??\n          MobileSpaceTabType.spaces;\n    } catch (e) {\n      return MobileSpaceTabType.spaces;\n    }\n  }\n\n  Future<void> _setDefaultTab(MobileSpaceTabType tab) async {\n    await _storage.set(\n      KVKeys.lastOpenedSpace,\n      tab.index.toString(),\n    );\n  }\n\n  Future<List<MobileSpaceTabType>> _getTabsOrder() async {\n    try {\n      return await _storage.getWithFormat<List<MobileSpaceTabType>>(\n              KVKeys.spaceOrder, (value) {\n            final order = jsonDecode(value).cast<int>();\n            if (order.isEmpty) {\n              return MobileSpaceTabType.values;\n            }\n            if (!order.contains(MobileSpaceTabType.shared.index)) {\n              order.add(MobileSpaceTabType.shared.index);\n            }\n            return order\n                .map((e) => MobileSpaceTabType.values[e])\n                .cast<MobileSpaceTabType>()\n                .toList();\n          }) ??\n          MobileSpaceTabType.values;\n    } catch (e) {\n      return MobileSpaceTabType.values;\n    }\n  }\n\n  Future<void> _setTabsOrder(List<MobileSpaceTabType> tabsOrder) async {\n    await _storage.set(\n      KVKeys.spaceOrder,\n      jsonEncode(tabsOrder.map((e) => e.index).toList()),\n    );\n  }\n}\n\n@freezed\nclass SpaceOrderEvent with _$SpaceOrderEvent {\n  const factory SpaceOrderEvent.initial() = Initial;\n  const factory SpaceOrderEvent.open(int index) = Open;\n  const factory SpaceOrderEvent.reorder(int from, int to) = Reorder;\n}\n\n@freezed\nclass SpaceOrderState with _$SpaceOrderState {\n  const factory SpaceOrderState({\n    @Default(MobileSpaceTabType.spaces) MobileSpaceTabType defaultTab,\n    @Default(MobileSpaceTabType.values) List<MobileSpaceTabType> tabsOrder,\n    @Default(true) bool isLoading,\n  }) = _SpaceOrderState;\n\n  factory SpaceOrderState.initial() => const SpaceOrderState();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/create_workspace_menu.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nenum EditWorkspaceNameType {\n  create,\n  edit;\n\n  String get title {\n    switch (this) {\n      case EditWorkspaceNameType.create:\n        return LocaleKeys.workspace_create.tr();\n      case EditWorkspaceNameType.edit:\n        return LocaleKeys.workspace_renameWorkspace.tr();\n    }\n  }\n\n  String get actionTitle {\n    switch (this) {\n      case EditWorkspaceNameType.create:\n        return LocaleKeys.workspace_create.tr();\n      case EditWorkspaceNameType.edit:\n        return LocaleKeys.button_confirm.tr();\n    }\n  }\n}\n\nclass EditWorkspaceNameBottomSheet extends StatefulWidget {\n  const EditWorkspaceNameBottomSheet({\n    super.key,\n    required this.type,\n    required this.onSubmitted,\n    required this.workspaceName,\n    this.hintText,\n    this.validator,\n    this.validatorBuilder,\n  });\n\n  final EditWorkspaceNameType type;\n  final void Function(String) onSubmitted;\n\n  // if the workspace name is not empty, it will be used as the initial value of the text field.\n  final String? workspaceName;\n\n  final String? hintText;\n\n  final String? Function(String?)? validator;\n\n  final WidgetBuilder? validatorBuilder;\n\n  @override\n  State<EditWorkspaceNameBottomSheet> createState() =>\n      _EditWorkspaceNameBottomSheetState();\n}\n\nclass _EditWorkspaceNameBottomSheetState\n    extends State<EditWorkspaceNameBottomSheet> {\n  late final TextEditingController _textFieldController;\n\n  final _formKey = GlobalKey<FormState>();\n\n  @override\n  void initState() {\n    super.initState();\n    _textFieldController = TextEditingController(\n      text: widget.workspaceName,\n    );\n  }\n\n  @override\n  void dispose() {\n    _textFieldController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      mainAxisSize: MainAxisSize.min,\n      children: <Widget>[\n        Form(\n          key: _formKey,\n          child: TextFormField(\n            autofocus: true,\n            controller: _textFieldController,\n            keyboardType: TextInputType.text,\n            decoration: InputDecoration(\n              hintText:\n                  widget.hintText ?? LocaleKeys.workspace_defaultName.tr(),\n            ),\n            validator: widget.validator ??\n                (value) {\n                  if (value == null || value.isEmpty) {\n                    return LocaleKeys.workspace_workspaceNameCannotBeEmpty.tr();\n                  }\n                  return null;\n                },\n            onEditingComplete: _onSubmit,\n          ),\n        ),\n        if (widget.validatorBuilder != null) ...[\n          const VSpace(4),\n          widget.validatorBuilder!(context),\n          const VSpace(4),\n        ],\n        const VSpace(16),\n        SizedBox(\n          width: double.infinity,\n          child: PrimaryRoundedButton(\n            text: widget.type.actionTitle,\n            fontSize: 16,\n            margin: const EdgeInsets.symmetric(\n              vertical: 16,\n            ),\n            onTap: _onSubmit,\n          ),\n        ),\n      ],\n    );\n  }\n\n  void _onSubmit() {\n    if (_formKey.currentState!.validate()) {\n      final value = _textFieldController.text;\n      widget.onSubmitted.call(value);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/animated_gesture.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/util/navigator_context_extension.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'create_workspace_menu.dart';\nimport 'workspace_more_options.dart';\n\n// Only works on mobile.\nclass MobileWorkspaceMenu extends StatelessWidget {\n  const MobileWorkspaceMenu({\n    super.key,\n    required this.userProfile,\n    required this.currentWorkspace,\n    required this.workspaces,\n    required this.onWorkspaceSelected,\n  });\n\n  final UserProfilePB userProfile;\n  final UserWorkspacePB currentWorkspace;\n  final List<UserWorkspacePB> workspaces;\n  final void Function(UserWorkspacePB workspace) onWorkspaceSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    // user profile\n    final List<Widget> children = [\n      _WorkspaceUserItem(userProfile: userProfile),\n      _buildDivider(),\n    ];\n\n    // workspace list\n    for (var i = 0; i < workspaces.length; i++) {\n      final workspace = workspaces[i];\n      children.add(\n        _WorkspaceMenuItem(\n          key: ValueKey(workspace.workspaceId),\n          userProfile: userProfile,\n          workspace: workspace,\n          showTopBorder: false,\n          currentWorkspace: currentWorkspace,\n          onWorkspaceSelected: onWorkspaceSelected,\n        ),\n      );\n    }\n\n    // create workspace button\n    children.addAll([\n      _buildDivider(),\n      const _CreateWorkspaceButton(),\n    ]);\n\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: children,\n    );\n  }\n\n  Widget _buildDivider() {\n    return const Padding(\n      padding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),\n      child: Divider(height: 0.5),\n    );\n  }\n}\n\nclass _CreateWorkspaceButton extends StatelessWidget {\n  const _CreateWorkspaceButton();\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 4.0),\n      child: FlowyOptionTile.text(\n        height: 60,\n        showTopBorder: false,\n        showBottomBorder: false,\n        leftIcon: _buildLeftIcon(context),\n        onTap: () => _showCreateWorkspaceBottomSheet(context),\n        content: Expanded(\n          child: Padding(\n            padding: const EdgeInsets.only(left: 16.0),\n            child: FlowyText.medium(\n              LocaleKeys.workspace_create.tr(),\n              fontSize: 14,\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  void _showCreateWorkspaceBottomSheet(BuildContext context) {\n    showMobileBottomSheet(\n      context,\n      showHeader: true,\n      title: LocaleKeys.workspace_create.tr(),\n      showCloseButton: true,\n      showDragHandle: true,\n      showDivider: false,\n      padding: const EdgeInsets.symmetric(horizontal: 16),\n      builder: (bottomSheetContext) {\n        return EditWorkspaceNameBottomSheet(\n          type: EditWorkspaceNameType.create,\n          workspaceName: LocaleKeys.workspace_defaultName.tr(),\n          onSubmitted: (name) {\n            // create a new workspace\n            Log.info('create a new workspace: $name');\n            bottomSheetContext.popToHome();\n\n            context.read<UserWorkspaceBloc>().add(\n                  UserWorkspaceEvent.createWorkspace(\n                    name: name,\n                    workspaceType: WorkspaceTypePB.ServerW,\n                  ),\n                );\n          },\n        );\n      },\n    );\n  }\n\n  Widget _buildLeftIcon(BuildContext context) {\n    return Container(\n      width: 36.0,\n      height: 36.0,\n      padding: const EdgeInsets.all(7.0),\n      decoration: BoxDecoration(\n        borderRadius: BorderRadius.circular(12),\n        border: Border.all(\n          color: const Color(0x01717171).withValues(alpha: 0.12),\n          width: 0.8,\n        ),\n      ),\n      child: const FlowySvg(FlowySvgs.add_workspace_s),\n    );\n  }\n}\n\nclass _WorkspaceUserItem extends StatelessWidget {\n  const _WorkspaceUserItem({required this.userProfile});\n\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    final color = Theme.of(context).isLightMode\n        ? const Color(0x99333333)\n        : const Color(0x99CCCCCC);\n    return FlowyOptionTile.text(\n      height: 32,\n      showTopBorder: false,\n      showBottomBorder: false,\n      content: Expanded(\n        child: Padding(\n          padding: const EdgeInsets.only(),\n          child: FlowyText(\n            userProfile.email,\n            fontSize: 14,\n            color: color,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _WorkspaceMenuItem extends StatelessWidget {\n  const _WorkspaceMenuItem({\n    super.key,\n    required this.userProfile,\n    required this.workspace,\n    required this.showTopBorder,\n    required this.currentWorkspace,\n    required this.onWorkspaceSelected,\n  });\n\n  final UserProfilePB userProfile;\n  final UserWorkspacePB workspace;\n  final bool showTopBorder;\n  final UserWorkspacePB currentWorkspace;\n  final void Function(UserWorkspacePB workspace) onWorkspaceSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) => WorkspaceMemberBloc(\n        userProfile: userProfile,\n        workspace: workspace,\n      )..add(const WorkspaceMemberEvent.initial()),\n      child: BlocBuilder<WorkspaceMemberBloc, WorkspaceMemberState>(\n        builder: (context, state) {\n          return FlowyOptionTile.text(\n            height: 60,\n            showTopBorder: showTopBorder,\n            showBottomBorder: false,\n            leftIcon: _WorkspaceMenuItemIcon(workspace: workspace),\n            trailing: _WorkspaceMenuItemTrailing(\n              workspace: workspace,\n              currentWorkspace: currentWorkspace,\n            ),\n            onTap: () => onWorkspaceSelected(workspace),\n            content: Expanded(\n              child: _WorkspaceMenuItemContent(\n                workspace: workspace,\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\n// - Workspace name\n// - Workspace member count\nclass _WorkspaceMenuItemContent extends StatelessWidget {\n  const _WorkspaceMenuItemContent({\n    required this.workspace,\n  });\n\n  final UserWorkspacePB workspace;\n\n  @override\n  Widget build(BuildContext context) {\n    final memberCount = workspace.memberCount.toInt();\n    return Padding(\n      padding: const EdgeInsets.only(left: 12),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          FlowyText(\n            workspace.name,\n            fontSize: 14,\n            fontWeight: FontWeight.w500,\n            overflow: TextOverflow.ellipsis,\n          ),\n          FlowyText(\n            memberCount == 0\n                ? ''\n                : LocaleKeys.settings_appearance_members_membersCount.plural(\n                    memberCount,\n                  ),\n            fontSize: 10.0,\n            color: Theme.of(context).hintColor,\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _WorkspaceMenuItemIcon extends StatelessWidget {\n  const _WorkspaceMenuItemIcon({\n    required this.workspace,\n  });\n\n  final UserWorkspacePB workspace;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 4.0),\n      child: WorkspaceIcon(\n        workspaceName: workspace.name,\n        workspaceIcon: workspace.icon,\n        isEditable: false,\n        iconSize: 36,\n        emojiSize: 24.0,\n        fontSize: 18.0,\n        figmaLineHeight: 26.0,\n        borderRadius: 12.0,\n        onSelected: (result) => context.read<UserWorkspaceBloc>().add(\n              UserWorkspaceEvent.updateWorkspaceIcon(\n                workspaceId: workspace.workspaceId,\n                icon: result.emoji,\n              ),\n            ),\n      ),\n    );\n  }\n}\n\nclass _WorkspaceMenuItemTrailing extends StatelessWidget {\n  const _WorkspaceMenuItemTrailing({\n    required this.workspace,\n    required this.currentWorkspace,\n  });\n\n  final UserWorkspacePB workspace;\n  final UserWorkspacePB currentWorkspace;\n\n  @override\n  Widget build(BuildContext context) {\n    const iconSize = Size.square(20);\n    return Row(\n      children: [\n        const HSpace(12.0),\n        // show the check icon if the workspace is the current workspace\n        if (workspace.workspaceId == currentWorkspace.workspaceId)\n          const FlowySvg(\n            FlowySvgs.m_blue_check_s,\n            size: iconSize,\n            blendMode: null,\n          ),\n        const HSpace(8.0),\n        // more options button\n        AnimatedGestureDetector(\n          onTapUp: () => _showMoreOptions(context),\n          child: const Padding(\n            padding: EdgeInsets.all(8.0),\n            child: FlowySvg(\n              FlowySvgs.workspace_three_dots_s,\n              size: iconSize,\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  void _showMoreOptions(BuildContext context) {\n    final actions =\n        context.read<WorkspaceMemberBloc>().state.myRole == AFRolePB.Owner\n            ? [\n                // only the owner can update workspace properties\n                WorkspaceMenuMoreOption.rename,\n                WorkspaceMenuMoreOption.delete,\n              ]\n            : [\n                WorkspaceMenuMoreOption.leave,\n              ];\n\n    showMobileBottomSheet(\n      context,\n      showDragHandle: true,\n      showDivider: false,\n      useRootNavigator: true,\n      backgroundColor: Theme.of(context).colorScheme.surface,\n      builder: (bottomSheetContext) {\n        return WorkspaceMenuMoreOptions(\n          actions: actions,\n          onAction: (action) => _onActions(context, bottomSheetContext, action),\n        );\n      },\n    );\n  }\n\n  void _onActions(\n    BuildContext context,\n    BuildContext bottomSheetContext,\n    WorkspaceMenuMoreOption action,\n  ) {\n    Log.info('execute action in workspace menu bottom sheet: $action');\n\n    switch (action) {\n      case WorkspaceMenuMoreOption.rename:\n        _showRenameWorkspaceBottomSheet(context);\n        break;\n      case WorkspaceMenuMoreOption.invite:\n        _pushToInviteMembersPage(context);\n        break;\n      case WorkspaceMenuMoreOption.delete:\n        _deleteWorkspace(context, bottomSheetContext);\n        break;\n      case WorkspaceMenuMoreOption.leave:\n        _leaveWorkspace(context, bottomSheetContext);\n        break;\n    }\n  }\n\n  void _pushToInviteMembersPage(BuildContext context) {\n    // empty implementation\n    // we don't support invite members in workspace menu\n  }\n\n  void _showRenameWorkspaceBottomSheet(BuildContext context) {\n    showMobileBottomSheet(\n      context,\n      showHeader: true,\n      title: LocaleKeys.workspace_renameWorkspace.tr(),\n      showCloseButton: true,\n      showDragHandle: true,\n      showDivider: false,\n      padding: const EdgeInsets.symmetric(horizontal: 16),\n      builder: (bottomSheetContext) {\n        return EditWorkspaceNameBottomSheet(\n          type: EditWorkspaceNameType.edit,\n          workspaceName: workspace.name,\n          onSubmitted: (name) {\n            // rename the workspace\n            Log.info('rename the workspace: $name');\n            bottomSheetContext.popToHome();\n\n            context.read<UserWorkspaceBloc>().add(\n                  UserWorkspaceEvent.renameWorkspace(\n                    workspaceId: workspace.workspaceId,\n                    name: name,\n                  ),\n                );\n          },\n        );\n      },\n    );\n  }\n\n  void _deleteWorkspace(BuildContext context, BuildContext bottomSheetContext) {\n    Navigator.of(bottomSheetContext).pop();\n\n    _showConfirmDialog(\n      context,\n      '${LocaleKeys.space_delete.tr()}: ${workspace.name}',\n      LocaleKeys.workspace_deleteWorkspaceHintText.tr(),\n      LocaleKeys.button_delete.tr(),\n      (_) async {\n        context.read<UserWorkspaceBloc>().add(\n              UserWorkspaceEvent.deleteWorkspace(\n                workspaceId: workspace.workspaceId,\n              ),\n            );\n        context.popToHome();\n      },\n    );\n  }\n\n  void _leaveWorkspace(BuildContext context, BuildContext bottomSheetContext) {\n    Navigator.of(bottomSheetContext).pop();\n\n    _showConfirmDialog(\n      context,\n      '${LocaleKeys.settings_workspacePage_leaveWorkspacePrompt_title.tr()}: ${workspace.name}',\n      LocaleKeys.settings_workspacePage_leaveWorkspacePrompt_content.tr(),\n      LocaleKeys.button_confirm.tr(),\n      (_) async {\n        context.read<UserWorkspaceBloc>().add(\n              UserWorkspaceEvent.leaveWorkspace(\n                workspaceId: workspace.workspaceId,\n              ),\n            );\n        context.popToHome();\n      },\n    );\n  }\n\n  void _showConfirmDialog(\n    BuildContext context,\n    String title,\n    String content,\n    String rightButtonText,\n    void Function(BuildContext context)? onRightButtonPressed,\n  ) {\n    showFlowyCupertinoConfirmDialog(\n      title: title,\n      content: FlowyText(\n        content,\n        fontSize: 14,\n        color: Theme.of(context).hintColor,\n        maxLines: 10,\n      ),\n      leftButton: FlowyText(\n        LocaleKeys.button_cancel.tr(),\n        fontSize: 17.0,\n        figmaLineHeight: 24.0,\n        fontWeight: FontWeight.w500,\n        color: const Color(0xFF007AFF),\n      ),\n      rightButton: FlowyText(\n        rightButtonText,\n        fontSize: 17.0,\n        figmaLineHeight: 24.0,\n        fontWeight: FontWeight.w400,\n        color: const Color(0xFFFE0220),\n      ),\n      onRightButtonPressed: onRightButtonPressed,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_more_options.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nenum WorkspaceMenuMoreOption {\n  rename,\n  invite,\n  delete,\n  leave,\n}\n\nclass WorkspaceMenuMoreOptions extends StatelessWidget {\n  const WorkspaceMenuMoreOptions({\n    super.key,\n    this.isFavorite = false,\n    required this.onAction,\n    required this.actions,\n  });\n\n  final bool isFavorite;\n  final void Function(WorkspaceMenuMoreOption action) onAction;\n  final List<WorkspaceMenuMoreOption> actions;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.stretch,\n      children: actions\n          .map(\n            (action) => _buildActionButton(context, action),\n          )\n          .toList(),\n    );\n  }\n\n  Widget _buildActionButton(\n    BuildContext context,\n    WorkspaceMenuMoreOption action,\n  ) {\n    switch (action) {\n      case WorkspaceMenuMoreOption.rename:\n        return FlowyOptionTile.text(\n          text: LocaleKeys.button_rename.tr(),\n          height: 52.0,\n          leftIcon: const FlowySvg(\n            FlowySvgs.view_item_rename_s,\n            size: Size.square(18),\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(\n            WorkspaceMenuMoreOption.rename,\n          ),\n        );\n      case WorkspaceMenuMoreOption.delete:\n        return FlowyOptionTile.text(\n          text: LocaleKeys.button_delete.tr(),\n          height: 52.0,\n          textColor: Theme.of(context).colorScheme.error,\n          leftIcon: FlowySvg(\n            FlowySvgs.trash_s,\n            size: const Size.square(18),\n            color: Theme.of(context).colorScheme.error,\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(\n            WorkspaceMenuMoreOption.delete,\n          ),\n        );\n      case WorkspaceMenuMoreOption.invite:\n        return FlowyOptionTile.text(\n          // i18n\n          text: 'Invite',\n          height: 52.0,\n          leftIcon: const FlowySvg(\n            FlowySvgs.workspace_add_member_s,\n            size: Size.square(18),\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(\n            WorkspaceMenuMoreOption.invite,\n          ),\n        );\n      case WorkspaceMenuMoreOption.leave:\n        return FlowyOptionTile.text(\n          text: LocaleKeys.workspace_leaveCurrentWorkspace.tr(),\n          height: 52.0,\n          textColor: Theme.of(context).colorScheme.error,\n          leftIcon: FlowySvg(\n            FlowySvgs.leave_workspace_s,\n            size: const Size.square(18),\n            color: Theme.of(context).colorScheme.error,\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => onAction(\n            WorkspaceMenuMoreOption.leave,\n          ),\n        );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/inline_actions/mobile_inline_actions_handler.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\n\nimport 'mobile_inline_actions_menu_group.dart';\n\nextension _StartWithsSort on List<InlineActionsResult> {\n  void sortByStartsWithKeyword(String search) => sort(\n        (a, b) {\n          final aCount = a.startsWithKeywords\n                  ?.where(\n                    (key) => search.toLowerCase().startsWith(key),\n                  )\n                  .length ??\n              0;\n\n          final bCount = b.startsWithKeywords\n                  ?.where(\n                    (key) => search.toLowerCase().startsWith(key),\n                  )\n                  .length ??\n              0;\n\n          if (aCount > bCount) {\n            return -1;\n          } else if (bCount > aCount) {\n            return 1;\n          }\n\n          return 0;\n        },\n      );\n}\n\nconst _invalidSearchesAmount = 10;\n\nclass MobileInlineActionsHandler extends StatefulWidget {\n  const MobileInlineActionsHandler({\n    super.key,\n    required this.results,\n    required this.editorState,\n    required this.menuService,\n    required this.onDismiss,\n    required this.style,\n    required this.service,\n    this.startCharAmount = 1,\n    this.startOffset = 0,\n    this.cancelBySpaceHandler,\n  });\n\n  final List<InlineActionsResult> results;\n  final EditorState editorState;\n  final InlineActionsMenuService menuService;\n  final VoidCallback onDismiss;\n  final InlineActionsMenuStyle style;\n  final int startCharAmount;\n  final InlineActionsService service;\n  final bool Function()? cancelBySpaceHandler;\n  final int startOffset;\n\n  @override\n  State<MobileInlineActionsHandler> createState() =>\n      _MobileInlineActionsHandlerState();\n}\n\nclass _MobileInlineActionsHandlerState\n    extends State<MobileInlineActionsHandler> {\n  final _focusNode =\n      FocusNode(debugLabel: 'mobile_inline_actions_menu_handler');\n\n  late List<InlineActionsResult> results = widget.results;\n  int invalidCounter = 0;\n  late int startOffset;\n\n  String _search = '';\n\n  set search(String search) {\n    _search = search;\n    _doSearch();\n  }\n\n  Future<void> _doSearch() async {\n    final List<InlineActionsResult> newResults = [];\n    for (final handler in widget.service.handlers) {\n      final group = await handler.search(_search);\n\n      if (group.results.isNotEmpty) {\n        newResults.add(group);\n      }\n    }\n\n    invalidCounter = results.every((group) => group.results.isEmpty)\n        ? invalidCounter + 1\n        : 0;\n\n    if (invalidCounter >= _invalidSearchesAmount) {\n      widget.onDismiss();\n\n      // Workaround to bring focus back to editor\n      await editorState.updateSelectionWithReason(editorState.selection);\n\n      return;\n    }\n\n    _resetSelection();\n\n    newResults.sortByStartsWithKeyword(_search);\n    setState(() => results = newResults);\n  }\n\n  void _resetSelection() {\n    _selectedGroup = 0;\n    _selectedIndex = 0;\n  }\n\n  int _selectedGroup = 0;\n  int _selectedIndex = 0;\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addPostFrameCallback(\n      (_) => _focusNode.requestFocus(),\n    );\n\n    startOffset = editorState.selection?.endIndex ?? 0;\n    keepEditorFocusNotifier.increase();\n    editorState.selectionNotifier.addListener(onSelectionChanged);\n  }\n\n  @override\n  void dispose() {\n    editorState.selectionNotifier.removeListener(onSelectionChanged);\n    _focusNode.dispose();\n    keepEditorFocusNotifier.decrease();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final width = editorState.renderBox!.size.width - 24 * 2;\n    return Focus(\n      focusNode: _focusNode,\n      child: Container(\n        constraints: BoxConstraints(\n          maxHeight: 192,\n          minWidth: width,\n          maxWidth: width,\n        ),\n        margin: EdgeInsets.symmetric(horizontal: 24.0),\n        decoration: BoxDecoration(\n          color: widget.style.backgroundColor,\n          borderRadius: BorderRadius.circular(6.0),\n          boxShadow: [\n            BoxShadow(\n              blurRadius: 5,\n              spreadRadius: 1,\n              color: Colors.black.withValues(alpha: 0.1),\n            ),\n          ],\n        ),\n        child: noResults\n            ? SizedBox(\n                width: 150,\n                child: FlowyText.regular(\n                  LocaleKeys.inlineActions_noResults.tr(),\n                ),\n              )\n            : SingleChildScrollView(\n                physics: const ClampingScrollPhysics(),\n                child: Material(\n                  color: Colors.transparent,\n                  child: Padding(\n                    padding: EdgeInsets.all(6.0),\n                    child: Column(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      children: results\n                          .where((g) => g.results.isNotEmpty)\n                          .mapIndexed(\n                            (index, group) => MobileInlineActionsGroup(\n                              result: group,\n                              editorState: editorState,\n                              menuService: menuService,\n                              style: widget.style,\n                              onSelected: widget.onDismiss,\n                              startOffset: startOffset - widget.startCharAmount,\n                              endOffset:\n                                  _search.length + widget.startCharAmount,\n                              isLastGroup: index == results.length - 1,\n                              isGroupSelected: _selectedGroup == index,\n                              selectedIndex: _selectedIndex,\n                              onPreSelect: (int value) {\n                                setState(() {\n                                  _selectedGroup = index;\n                                  _selectedIndex = value;\n                                });\n                              },\n                            ),\n                          )\n                          .toList(),\n                    ),\n                  ),\n                ),\n              ),\n      ),\n    );\n  }\n\n  bool get noResults =>\n      results.isEmpty || results.every((e) => e.results.isEmpty);\n\n  int get groupLength => results.length;\n\n  int lengthOfGroup(int index) =>\n      results.length > index ? results[index].results.length : -1;\n\n  InlineActionsMenuItem handlerOf(int groupIndex, int handlerIndex) =>\n      results[groupIndex].results[handlerIndex];\n\n  EditorState get editorState => widget.editorState;\n\n  InlineActionsMenuService get menuService => widget.menuService;\n\n  void onSelectionChanged() {\n    final selection = editorState.selection;\n    if (selection == null) {\n      menuService.dismiss();\n      return;\n    }\n    if (!selection.isCollapsed) {\n      menuService.dismiss();\n      return;\n    }\n    final startOffset = widget.startOffset;\n    final endOffset = selection.end.offset;\n    if (endOffset < startOffset) {\n      menuService.dismiss();\n      return;\n    }\n    final node = editorState.getNodeAtPath(selection.start.path);\n    final text = node?.delta?.toPlainText() ?? '';\n    final search = text.substring(startOffset, endOffset);\n    this.search = search;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nimport 'mobile_inline_actions_handler.dart';\n\nclass MobileInlineActionsMenu extends InlineActionsMenuService {\n  MobileInlineActionsMenu({\n    required this.context,\n    required this.editorState,\n    required this.initialResults,\n    required this.style,\n    required this.service,\n    this.startCharAmount = 1,\n    this.cancelBySpaceHandler,\n  });\n\n  final BuildContext context;\n  final EditorState editorState;\n  final List<InlineActionsResult> initialResults;\n  final bool Function()? cancelBySpaceHandler;\n  final InlineActionsService service;\n\n  @override\n  final InlineActionsMenuStyle style;\n\n  final int startCharAmount;\n\n  OverlayEntry? _menuEntry;\n\n  @override\n  void dismiss() {\n    if (_menuEntry != null) {\n      editorState.service.keyboardService?.enable();\n      editorState.service.scrollService?.enable();\n    }\n\n    _menuEntry?.remove();\n    _menuEntry = null;\n  }\n\n  @override\n  Future<void> show() {\n    final completer = Completer<void>();\n    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n      _show();\n      completer.complete();\n    });\n    return completer.future;\n  }\n\n  void _show() {\n    final selectionRects = editorState.selectionRects();\n    if (selectionRects.isEmpty) {\n      return;\n    }\n\n    const double menuHeight = 192.0;\n    const Offset menuOffset = Offset(0, 10);\n    final Offset editorOffset =\n        editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;\n    final Size editorSize = editorState.renderBox!.size;\n\n    // Default to opening the overlay below\n    Alignment alignment = Alignment.topLeft;\n\n    final firstRect = selectionRects.first;\n    Offset offset = firstRect.bottomRight + menuOffset;\n\n    // Show above\n    if (offset.dy + menuHeight >= editorOffset.dy + editorSize.height) {\n      offset = firstRect.topRight - menuOffset;\n      alignment = Alignment.bottomLeft;\n\n      offset = Offset(\n        offset.dx,\n        MediaQuery.of(context).size.height - offset.dy,\n      );\n    }\n\n    final (left, top, right, bottom) = _getPosition(alignment, offset);\n\n    _menuEntry = OverlayEntry(\n      builder: (context) => SizedBox(\n        width: editorSize.width,\n        height: editorSize.height,\n        child: GestureDetector(\n          behavior: HitTestBehavior.opaque,\n          onTap: dismiss,\n          child: Stack(\n            children: [\n              Positioned(\n                top: top,\n                bottom: bottom,\n                left: left,\n                right: right,\n                child: MobileInlineActionsHandler(\n                  service: service,\n                  results: initialResults,\n                  editorState: editorState,\n                  menuService: this,\n                  onDismiss: dismiss,\n                  style: style,\n                  startCharAmount: startCharAmount,\n                  cancelBySpaceHandler: cancelBySpaceHandler,\n                  startOffset: editorState.selection?.start.offset ?? 0,\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n\n    Overlay.of(context).insert(_menuEntry!);\n\n    editorState.service.keyboardService?.disable(showCursor: true);\n    editorState.service.scrollService?.disable();\n  }\n\n  (double? left, double? top, double? right, double? bottom) _getPosition(\n    Alignment alignment,\n    Offset offset,\n  ) {\n    double? left, top, right, bottom;\n    switch (alignment) {\n      case Alignment.topLeft:\n        left = 0;\n        top = offset.dy;\n        break;\n      case Alignment.bottomLeft:\n        left = 0;\n        bottom = offset.dy;\n        break;\n      case Alignment.topRight:\n        right = offset.dx;\n        top = offset.dy;\n        break;\n      case Alignment.bottomRight:\n        right = offset.dx;\n        bottom = offset.dy;\n        break;\n    }\n\n    return (left, top, right, bottom);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart",
    "content": "import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileInlineActionsGroup extends StatelessWidget {\n  const MobileInlineActionsGroup({\n    super.key,\n    required this.result,\n    required this.editorState,\n    required this.menuService,\n    required this.style,\n    required this.onSelected,\n    required this.startOffset,\n    required this.endOffset,\n    required this.onPreSelect,\n    this.isLastGroup = false,\n    this.isGroupSelected = false,\n    this.selectedIndex = 0,\n  });\n\n  final InlineActionsResult result;\n  final EditorState editorState;\n  final InlineActionsMenuService menuService;\n  final InlineActionsMenuStyle style;\n  final VoidCallback onSelected;\n  final ValueChanged<int> onPreSelect;\n  final int startOffset;\n  final int endOffset;\n\n  final bool isLastGroup;\n  final bool isGroupSelected;\n  final int selectedIndex;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        if (result.title != null) ...[\n          SizedBox(\n            height: 36,\n            child: Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 12),\n              child: Align(\n                alignment: Alignment.centerLeft,\n                child: FlowyText.medium(\n                  result.title!,\n                  color: style.groupTextColor,\n                  fontSize: 12,\n                ),\n              ),\n            ),\n          ),\n        ],\n        ...result.results.mapIndexed(\n          (index, item) => GestureDetector(\n            onTapDown: (e) {\n              onPreSelect.call(index);\n            },\n            child: MobileInlineActionsWidget(\n              item: item,\n              editorState: editorState,\n              menuService: menuService,\n              isSelected: isGroupSelected && index == selectedIndex,\n              style: style,\n              onSelected: onSelected,\n              startOffset: startOffset,\n              endOffset: endOffset,\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass MobileInlineActionsWidget extends StatelessWidget {\n  const MobileInlineActionsWidget({\n    super.key,\n    required this.item,\n    required this.editorState,\n    required this.menuService,\n    required this.isSelected,\n    required this.style,\n    required this.onSelected,\n    required this.startOffset,\n    required this.endOffset,\n  });\n\n  final InlineActionsMenuItem item;\n  final EditorState editorState;\n  final InlineActionsMenuService menuService;\n  final bool isSelected;\n  final InlineActionsMenuStyle style;\n  final VoidCallback onSelected;\n  final int startOffset;\n  final int endOffset;\n\n  @override\n  Widget build(BuildContext context) {\n    final hasIcon = item.iconBuilder != null;\n    return Container(\n      height: 36,\n      decoration: BoxDecoration(\n        color: isSelected ? style.menuItemSelectedColor : null,\n        borderRadius: BorderRadius.circular(6.0),\n      ),\n      child: FlowyButton(\n        expand: true,\n        isSelected: isSelected,\n        text: Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 12),\n          child: Align(\n            alignment: Alignment.centerLeft,\n            child: Row(\n              children: [\n                if (hasIcon) ...[\n                  item.iconBuilder!.call(isSelected),\n                  SizedBox(width: 12),\n                ],\n                Flexible(\n                  child: FlowyText.regular(\n                    item.label,\n                    figmaLineHeight: 18,\n                    overflow: TextOverflow.ellipsis,\n                    fontSize: 16,\n                    color: style.menuItemSelectedTextColor,\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ),\n        onTap: () => _onPressed(context),\n      ),\n    );\n  }\n\n  void _onPressed(BuildContext context) {\n    onSelected();\n    item.onSelected?.call(\n      context,\n      editorState,\n      menuService,\n      (startOffset, endOffset),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart",
    "content": "import 'dart:io';\nimport 'dart:ui';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';\nimport 'package:appflowy/mobile/presentation/widgets/navigation_bar_button.dart';\nimport 'package:appflowy/shared/popup_menu/appflowy_popup_menu.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/presentation/notifications/number_red_dot.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nimport 'home/mobile_home_page.dart';\nimport 'search/mobile_search_page.dart';\n\nenum BottomNavigationBarActionType {\n  home,\n  notificationMultiSelect,\n}\n\nfinal PropertyValueNotifier<ViewLayoutPB?> mobileCreateNewPageNotifier =\n    PropertyValueNotifier(null);\nfinal ValueNotifier<BottomNavigationBarActionType> bottomNavigationBarType =\n    ValueNotifier(BottomNavigationBarActionType.home);\nfinal ValueNotifier<String?> bottomNavigationBarItemType =\n    ValueNotifier(BottomNavigationBarItemType.home.label);\n\nenum BottomNavigationBarItemType {\n  home,\n  search,\n  add,\n  notification;\n\n  String get label => name;\n  String? get routeName {\n    return switch (this) {\n      home => MobileHomeScreen.routeName,\n      search => MobileSearchScreen.routeName,\n      notification => MobileNotificationsScreenV2.routeName,\n      add => null,\n    };\n  }\n\n  ValueKey get valueKey {\n    return ValueKey(label);\n  }\n\n  Widget get iconWidget {\n    return switch (this) {\n      home => const FlowySvg(FlowySvgs.m_home_unselected_m),\n      search => const FlowySvg(FlowySvgs.m_home_search_icon_m),\n      add => const FlowySvg(FlowySvgs.m_home_add_m),\n      notification => const _NotificationNavigationBarItemIcon(),\n    };\n  }\n\n  Widget? get activeIcon {\n    return switch (this) {\n      home => const FlowySvg(FlowySvgs.m_home_selected_m, blendMode: null),\n      search =>\n        const FlowySvg(FlowySvgs.m_home_search_icon_active_m, blendMode: null),\n      add => null,\n      notification => const _NotificationNavigationBarItemIcon(isActive: true),\n    };\n  }\n\n  BottomNavigationBarItem get navigationItem {\n    return BottomNavigationBarItem(\n      key: valueKey,\n      label: label,\n      icon: iconWidget,\n      activeIcon: activeIcon,\n    );\n  }\n}\n\nfinal _items =\n    BottomNavigationBarItemType.values.map((e) => e.navigationItem).toList();\n\n/// Builds the \"shell\" for the app by building a Scaffold with a\n/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.\nclass MobileBottomNavigationBar extends StatefulWidget {\n  /// Constructs an [MobileBottomNavigationBar].\n  const MobileBottomNavigationBar({\n    required this.navigationShell,\n    super.key,\n  });\n\n  /// The navigation shell and container for the branch Navigators.\n  final StatefulNavigationShell navigationShell;\n\n  @override\n  State<MobileBottomNavigationBar> createState() =>\n      _MobileBottomNavigationBarState();\n}\n\nclass _MobileBottomNavigationBarState extends State<MobileBottomNavigationBar> {\n  Widget? _bottomNavigationBar;\n\n  @override\n  void initState() {\n    super.initState();\n\n    bottomNavigationBarType.addListener(_animate);\n  }\n\n  @override\n  void dispose() {\n    bottomNavigationBarType.removeListener(_animate);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    _bottomNavigationBar = switch (bottomNavigationBarType.value) {\n      BottomNavigationBarActionType.home =>\n        _buildHomePageNavigationBar(context),\n      BottomNavigationBarActionType.notificationMultiSelect =>\n        _buildNotificationNavigationBar(context),\n    };\n\n    return Scaffold(\n      body: widget.navigationShell,\n      extendBody: true,\n      bottomNavigationBar: AnimatedSwitcher(\n        duration: const Duration(milliseconds: 250),\n        switchInCurve: Curves.easeInOut,\n        switchOutCurve: Curves.easeInOut,\n        transitionBuilder: _transitionBuilder,\n        child: _bottomNavigationBar,\n      ),\n    );\n  }\n\n  Widget _buildHomePageNavigationBar(BuildContext context) {\n    return _HomePageNavigationBar(\n      navigationShell: widget.navigationShell,\n    );\n  }\n\n  Widget _buildNotificationNavigationBar(BuildContext context) {\n    return const _NotificationNavigationBar();\n  }\n\n  // widget A going down, widget B going up\n  Widget _transitionBuilder(\n    Widget child,\n    Animation<double> animation,\n  ) {\n    return SlideTransition(\n      position: Tween<Offset>(\n        begin: const Offset(0, 1),\n        end: Offset.zero,\n      ).animate(animation),\n      child: child,\n    );\n  }\n\n  void _animate() {\n    setState(() {});\n  }\n}\n\nclass _NotificationNavigationBarItemIcon extends StatelessWidget {\n  const _NotificationNavigationBarItemIcon({\n    this.isActive = false,\n  });\n\n  final bool isActive;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: getIt<ReminderBloc>(),\n      child: BlocBuilder<ReminderBloc, ReminderState>(\n        builder: (context, state) {\n          final hasUnreads = state.reminders.any(\n            (reminder) => !reminder.isRead,\n          );\n          return SizedBox(\n            width: 40,\n            height: 40,\n            child: Stack(\n              children: [\n                Center(\n                  child: isActive\n                      ? const FlowySvg(\n                          FlowySvgs.m_home_active_notification_m,\n                          blendMode: null,\n                        )\n                      : const FlowySvg(\n                          FlowySvgs.m_home_notification_m,\n                        ),\n                ),\n                if (hasUnreads)\n                  const Align(\n                    alignment: Alignment.topRight,\n                    child: NumberedRedDot.mobile(),\n                  ),\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _HomePageNavigationBar extends StatelessWidget {\n  const _HomePageNavigationBar({\n    required this.navigationShell,\n  });\n\n  final StatefulNavigationShell navigationShell;\n\n  @override\n  Widget build(BuildContext context) {\n    return ClipRRect(\n      child: BackdropFilter(\n        filter: ImageFilter.blur(\n          sigmaX: 3,\n          sigmaY: 3,\n        ),\n        child: DecoratedBox(\n          decoration: BoxDecoration(\n            border: context.border,\n            color: context.backgroundColor,\n          ),\n          child: Theme(\n            data: _getThemeData(context),\n            child: BottomNavigationBar(\n              showSelectedLabels: false,\n              showUnselectedLabels: false,\n              enableFeedback: false,\n              type: BottomNavigationBarType.fixed,\n              elevation: 0,\n              items: _items,\n              backgroundColor: Colors.transparent,\n              currentIndex: navigationShell.currentIndex,\n              onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  ThemeData _getThemeData(BuildContext context) {\n    if (Platform.isAndroid) {\n      return Theme.of(context);\n    }\n\n    // hide the splash effect for iOS\n    return Theme.of(context).copyWith(\n      splashFactory: NoSplash.splashFactory,\n      splashColor: Colors.transparent,\n      highlightColor: Colors.transparent,\n    );\n  }\n\n  /// Navigate to the current location of the branch at the provided index when\n  /// tapping an item in the BottomNavigationBar.\n  void _onTap(BuildContext context, int bottomBarIndex) {\n    // close the popup menu\n    closePopupMenu();\n\n    final label = _items[bottomBarIndex].label;\n    if (label == BottomNavigationBarItemType.add.label) {\n      // show an add dialog\n      mobileCreateNewPageNotifier.value = ViewLayoutPB.Document;\n      return;\n    } else if (label == BottomNavigationBarItemType.notification.label) {\n      getIt<ReminderBloc>().add(const ReminderEvent.refresh());\n    }\n    bottomNavigationBarItemType.value = label;\n    // When navigating to a new branch, it's recommended to use the goBranch\n    // method, as doing so makes sure the last navigation state of the\n    // Navigator for the branch is restored.\n    navigationShell.goBranch(\n      bottomBarIndex,\n      // A common pattern when using bottom navigation bars is to support\n      // navigating to the initial location when tapping the item that is\n      // already active. This example demonstrates how to support this behavior,\n      // using the initialLocation parameter of goBranch.\n      initialLocation: bottomBarIndex == navigationShell.currentIndex,\n    );\n  }\n}\n\nclass _NotificationNavigationBar extends StatelessWidget {\n  const _NotificationNavigationBar();\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      // todo: use real height here.\n      height: 90,\n      decoration: BoxDecoration(\n        border: context.border,\n        color: context.backgroundColor,\n      ),\n      padding: const EdgeInsets.only(bottom: 20),\n      child: ValueListenableBuilder(\n        valueListenable: mSelectedNotificationIds,\n        builder: (context, value, child) {\n          if (value.isEmpty) {\n            // not editable\n            return IgnorePointer(\n              child: Opacity(\n                opacity: 0.3,\n                child: child,\n              ),\n            );\n          }\n\n          return child!;\n        },\n        child: Row(\n          children: [\n            const HSpace(20),\n            Expanded(\n              child: NavigationBarButton(\n                icon: FlowySvgs.m_notification_action_mark_as_read_s,\n                text: LocaleKeys.settings_notifications_action_markAsRead.tr(),\n                onTap: () => _onMarkAsRead(context),\n              ),\n            ),\n            const HSpace(16),\n            Expanded(\n              child: NavigationBarButton(\n                icon: FlowySvgs.m_notification_action_archive_s,\n                text: LocaleKeys.settings_notifications_action_archive.tr(),\n                onTap: () => _onArchive(context),\n              ),\n            ),\n            const HSpace(20),\n          ],\n        ),\n      ),\n    );\n  }\n\n  void _onMarkAsRead(BuildContext context) {\n    if (mSelectedNotificationIds.value.isEmpty) {\n      return;\n    }\n\n    showToastNotification(\n      message: LocaleKeys\n          .settings_notifications_markAsReadNotifications_allSuccess\n          .tr(),\n    );\n\n    getIt<ReminderBloc>()\n        .add(ReminderEvent.markAsRead(mSelectedNotificationIds.value));\n\n    mSelectedNotificationIds.value = [];\n  }\n\n  void _onArchive(BuildContext context) {\n    if (mSelectedNotificationIds.value.isEmpty) {\n      return;\n    }\n\n    showToastNotification(\n      message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess\n          .tr(),\n    );\n\n    getIt<ReminderBloc>()\n        .add(ReminderEvent.archive(mSelectedNotificationIds.value));\n\n    mSelectedNotificationIds.value = [];\n  }\n}\n\nextension on BuildContext {\n  Color get backgroundColor {\n    return Theme.of(this).isLightMode\n        ? Colors.white.withValues(alpha: 0.95)\n        : const Color(0xFF23262B).withValues(alpha: 0.95);\n  }\n\n  Color get borderColor {\n    return Theme.of(this).isLightMode\n        ? const Color(0x141F2329)\n        : const Color(0xFF23262B).withValues(alpha: 0.5);\n  }\n\n  Border? get border {\n    return Theme.of(this).isLightMode\n        ? Border(top: BorderSide(color: borderColor))\n        : null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_multiple_select_page.dart",
    "content": "import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';\nimport 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MobileNotificationsMultiSelectScreen extends StatelessWidget {\n  const MobileNotificationsMultiSelectScreen({super.key});\n\n  static const routeName = '/notifications_multi_select';\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<ReminderBloc>.value(\n      value: getIt<ReminderBloc>(),\n      child: const MobileNotificationMultiSelect(),\n    );\n  }\n}\n\nclass MobileNotificationMultiSelect extends StatefulWidget {\n  const MobileNotificationMultiSelect({\n    super.key,\n  });\n\n  @override\n  State<MobileNotificationMultiSelect> createState() =>\n      _MobileNotificationMultiSelectState();\n}\n\nclass _MobileNotificationMultiSelectState\n    extends State<MobileNotificationMultiSelect> {\n  @override\n  void dispose() {\n    mSelectedNotificationIds.value.clear();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return const Scaffold(\n      body: SafeArea(\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            MobileNotificationMultiSelectPageHeader(),\n            VSpace(12.0),\n            Expanded(\n              child: MultiSelectNotificationTab(),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/user_profile/user_profile_bloc.dart';\nimport 'package:appflowy/mobile/presentation/notifications/widgets/mobile_notification_tab_bar.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';\nimport 'package:appflowy/workspace/presentation/notifications/reminder_extension.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/inbox_action_bar.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/notification_view.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MobileNotificationsScreen extends StatefulWidget {\n  const MobileNotificationsScreen({super.key});\n\n  static const routeName = '/notifications';\n\n  @override\n  State<MobileNotificationsScreen> createState() =>\n      _MobileNotificationsScreenState();\n}\n\nclass _MobileNotificationsScreenState extends State<MobileNotificationsScreen>\n    with SingleTickerProviderStateMixin {\n  final ReminderBloc reminderBloc = getIt<ReminderBloc>();\n  late final TabController controller = TabController(length: 2, vsync: this);\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider<UserProfileBloc>(\n          create: (context) =>\n              UserProfileBloc()..add(const UserProfileEvent.started()),\n        ),\n        BlocProvider<ReminderBloc>.value(value: reminderBloc),\n        BlocProvider<NotificationFilterBloc>(\n          create: (_) => NotificationFilterBloc(),\n        ),\n      ],\n      child: BlocBuilder<UserProfileBloc, UserProfileState>(\n        builder: (context, state) {\n          return state.maybeWhen(\n            orElse: () =>\n                const Center(child: CircularProgressIndicator.adaptive()),\n            workspaceFailure: () => const WorkspaceFailedScreen(),\n            success: (workspaceLatest, userProfile) =>\n                _NotificationScreenContent(\n              workspaceLatest: workspaceLatest,\n              userProfile: userProfile,\n              controller: controller,\n              reminderBloc: reminderBloc,\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _NotificationScreenContent extends StatelessWidget {\n  const _NotificationScreenContent({\n    required this.workspaceLatest,\n    required this.userProfile,\n    required this.controller,\n    required this.reminderBloc,\n  });\n\n  final WorkspaceLatestPB workspaceLatest;\n  final UserProfilePB userProfile;\n  final TabController controller;\n  final ReminderBloc reminderBloc;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) => SidebarSectionsBloc()\n        ..add(\n          SidebarSectionsEvent.initial(\n            userProfile,\n            workspaceLatest.workspaceId,\n          ),\n        ),\n      child: BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(\n        builder: (context, sectionState) =>\n            BlocBuilder<NotificationFilterBloc, NotificationFilterState>(\n          builder: (context, filterState) =>\n              BlocBuilder<ReminderBloc, ReminderState>(\n            builder: (context, state) {\n              // Workaround for rebuilding the Blocks by brightness\n              Theme.of(context).brightness;\n\n              final List<ReminderPB> pastReminders = state.pastReminders\n                  .where(\n                    (r) => filterState.showUnreadsOnly ? !r.isRead : true,\n                  )\n                  .sortByScheduledAt();\n\n              final List<ReminderPB> upcomingReminders =\n                  state.upcomingReminders.sortByScheduledAt();\n\n              return Scaffold(\n                appBar: AppBar(\n                  automaticallyImplyLeading: false,\n                  elevation: 0,\n                  title: Text(LocaleKeys.notificationHub_mobile_title.tr()),\n                ),\n                body: SafeArea(\n                  child: Column(\n                    children: [\n                      MobileNotificationTabBar(controller: controller),\n                      Expanded(\n                        child: TabBarView(\n                          controller: controller,\n                          children: [\n                            NotificationsView(\n                              shownReminders: pastReminders,\n                              reminderBloc: reminderBloc,\n                              views: sectionState.section.publicViews,\n                              onAction: _onAction,\n                              onReadChanged: _onReadChanged,\n                              actionBar: InboxActionBar(\n                                showUnreadsOnly: filterState.showUnreadsOnly,\n                              ),\n                            ),\n                            NotificationsView(\n                              shownReminders: upcomingReminders,\n                              reminderBloc: reminderBloc,\n                              views: sectionState.section.publicViews,\n                              isUpcoming: true,\n                              onAction: _onAction,\n                            ),\n                          ],\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n              );\n            },\n          ),\n        ),\n      ),\n    );\n  }\n\n  void _onAction(ReminderPB reminder, int? path, ViewPB? view) =>\n      reminderBloc.add(\n        ReminderEvent.pressReminder(\n          reminderId: reminder.id,\n          path: path,\n          view: view,\n        ),\n      );\n\n  void _onReadChanged(ReminderPB reminder, bool isRead) => reminderBloc.add(\n        ReminderEvent.update(ReminderUpdate(id: reminder.id, isRead: isRead)),\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_screen.dart",
    "content": "import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_multiple_select_page.dart';\nimport 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';\nimport 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'widgets/tab_bar.dart';\n\nfinal PropertyValueNotifier<List<String>> mSelectedNotificationIds =\n    PropertyValueNotifier([]);\n\nclass MobileNotificationsScreenV2 extends StatefulWidget {\n  const MobileNotificationsScreenV2({super.key});\n\n  static const routeName = '/notifications';\n\n  @override\n  State<MobileNotificationsScreenV2> createState() =>\n      _MobileNotificationsScreenV2State();\n}\n\nclass _MobileNotificationsScreenV2State\n    extends State<MobileNotificationsScreenV2>\n    with AutomaticKeepAliveClientMixin {\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  void initState() {\n    super.initState();\n\n    mCurrentWorkspace.addListener(_onRefresh);\n  }\n\n  @override\n  void dispose() {\n    mCurrentWorkspace.removeListener(_onRefresh);\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n\n    return BlocProvider<ReminderBloc>.value(\n      value: getIt<ReminderBloc>(),\n      child: ValueListenableBuilder(\n        valueListenable: bottomNavigationBarType,\n        builder: (_, value, __) {\n          switch (value) {\n            case BottomNavigationBarActionType.home:\n              return const MobileNotificationsTab();\n            case BottomNavigationBarActionType.notificationMultiSelect:\n              return const MobileNotificationMultiSelect();\n          }\n        },\n      ),\n    );\n  }\n\n  void _onRefresh() => getIt<ReminderBloc>().add(const ReminderEvent.refresh());\n}\n\nclass MobileNotificationsTab extends StatefulWidget {\n  const MobileNotificationsTab({super.key});\n\n  @override\n  State<MobileNotificationsTab> createState() => _MobileNotificationsTabState();\n}\n\nclass _MobileNotificationsTabState extends State<MobileNotificationsTab>\n    with SingleTickerProviderStateMixin {\n  late TabController tabController;\n\n  final tabs = [\n    NotificationTabType.inbox,\n    NotificationTabType.unread,\n    NotificationTabType.archive,\n  ];\n\n  @override\n  void initState() {\n    super.initState();\n    tabController = TabController(length: 3, vsync: this);\n  }\n\n  @override\n  void dispose() {\n    tabController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: SafeArea(\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            const MobileNotificationPageHeader(),\n            MobileNotificationTabBar(\n              tabController: tabController,\n              tabs: tabs,\n            ),\n            const VSpace(12.0),\n            Expanded(\n              child: TabBarView(\n                controller: tabController,\n                children: tabs.map((e) => NotificationTab(tabType: e)).toList(),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/color.dart",
    "content": "import 'package:appflowy/util/theme_extension.dart';\nimport 'package:flutter/material.dart';\n\nextension NotificationItemColors on BuildContext {\n  Color get notificationItemTextColor {\n    if (Theme.of(this).isLightMode) {\n      return const Color(0xFF171717);\n    }\n    return const Color(0xFFffffff).withValues(alpha: 0.8);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/empty.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass EmptyNotification extends StatelessWidget {\n  const EmptyNotification({\n    super.key,\n    required this.type,\n  });\n\n  final NotificationTabType type;\n\n  @override\n  Widget build(BuildContext context) {\n    final title = switch (type) {\n      NotificationTabType.inbox =>\n        LocaleKeys.settings_notifications_emptyInbox_title.tr(),\n      NotificationTabType.archive =>\n        LocaleKeys.settings_notifications_emptyArchived_title.tr(),\n      NotificationTabType.unread =>\n        LocaleKeys.settings_notifications_emptyUnread_title.tr(),\n    };\n    final desc = switch (type) {\n      NotificationTabType.inbox =>\n        LocaleKeys.settings_notifications_emptyInbox_description.tr(),\n      NotificationTabType.archive =>\n        LocaleKeys.settings_notifications_emptyArchived_description.tr(),\n      NotificationTabType.unread =>\n        LocaleKeys.settings_notifications_emptyUnread_description.tr(),\n    };\n    return Column(\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        const FlowySvg(FlowySvgs.m_empty_notification_xl),\n        const VSpace(12.0),\n        FlowyText(\n          title,\n          fontSize: 16.0,\n          figmaLineHeight: 24.0,\n          fontWeight: FontWeight.w500,\n        ),\n        const VSpace(4.0),\n        Opacity(\n          opacity: 0.45,\n          child: FlowyText(\n            desc,\n            fontSize: 15.0,\n            figmaLineHeight: 22.0,\n            fontWeight: FontWeight.w400,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/header.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';\nimport 'package:appflowy/mobile/presentation/notifications/widgets/settings_popup_menu.dart';\nimport 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileNotificationPageHeader extends StatelessWidget {\n  const MobileNotificationPageHeader({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return ConstrainedBox(\n      constraints: const BoxConstraints(minHeight: 56),\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          const HSpace(18.0),\n          FlowyText(\n            LocaleKeys.settings_notifications_titles_notifications.tr(),\n            fontSize: 20,\n            fontWeight: FontWeight.w600,\n          ),\n          const Spacer(),\n          const NotificationSettingsPopupMenu(),\n          const HSpace(16.0),\n        ],\n      ),\n    );\n  }\n}\n\nclass MobileNotificationMultiSelectPageHeader extends StatelessWidget {\n  const MobileNotificationMultiSelectPageHeader({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return ConstrainedBox(\n      constraints: const BoxConstraints(minHeight: 56),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: [\n          _buildCancelButton(\n            isOpaque: false,\n            padding: const EdgeInsets.symmetric(horizontal: 16),\n            onTap: () => bottomNavigationBarType.value =\n                BottomNavigationBarActionType.home,\n          ),\n          ValueListenableBuilder(\n            valueListenable: mSelectedNotificationIds,\n            builder: (_, value, __) {\n              return FlowyText(\n                // todo: i18n\n                '${value.length} Selected',\n                fontSize: 17.0,\n                figmaLineHeight: 24.0,\n                fontWeight: FontWeight.w500,\n              );\n            },\n          ),\n          // this button is used to align the text to the center\n          _buildCancelButton(\n            isOpaque: true,\n            padding: const EdgeInsets.symmetric(horizontal: 16),\n          ),\n        ],\n      ),\n    );\n  }\n\n  //\n  Widget _buildCancelButton({\n    required bool isOpaque,\n    required EdgeInsets padding,\n    VoidCallback? onTap,\n  }) {\n    return GestureDetector(\n      onTap: onTap,\n      child: Padding(\n        padding: padding,\n        child: FlowyText(\n          LocaleKeys.button_cancel.tr(),\n          fontSize: 17.0,\n          figmaLineHeight: 24.0,\n          fontWeight: FontWeight.w400,\n          color: isOpaque ? Colors.transparent : null,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/mobile_notification_tab_bar.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileNotificationTabBar extends StatefulWidget {\n  const MobileNotificationTabBar({super.key, required this.controller});\n\n  final TabController controller;\n\n  @override\n  State<MobileNotificationTabBar> createState() =>\n      _MobileNotificationTabBarState();\n}\n\nclass _MobileNotificationTabBarState extends State<MobileNotificationTabBar> {\n  @override\n  void initState() {\n    super.initState();\n    widget.controller.addListener(_updateState);\n  }\n\n  @override\n  void dispose() {\n    widget.controller.removeListener(_updateState);\n    super.dispose();\n  }\n\n  void _updateState() => setState(() {});\n\n  @override\n  Widget build(BuildContext context) {\n    final borderSide = BorderSide(\n      color: AFThemeExtension.of(context).calloutBGColor,\n    );\n\n    return DecoratedBox(\n      decoration: BoxDecoration(\n        border: Border(\n          bottom: borderSide,\n          top: borderSide,\n        ),\n      ),\n      child: Row(\n        children: [\n          Expanded(\n            child: TabBar(\n              controller: widget.controller,\n              padding: const EdgeInsets.symmetric(horizontal: 8),\n              labelPadding: EdgeInsets.zero,\n              indicatorSize: TabBarIndicatorSize.label,\n              indicator: UnderlineTabIndicator(\n                borderSide: BorderSide(\n                  color: Theme.of(context).colorScheme.primary,\n                ),\n              ),\n              isScrollable: true,\n              tabs: [\n                FlowyTabItem(\n                  label: LocaleKeys.notificationHub_tabs_inbox.tr(),\n                  isSelected: widget.controller.index == 0,\n                ),\n                FlowyTabItem(\n                  label: LocaleKeys.notificationHub_tabs_upcoming.tr(),\n                  isSelected: widget.controller.index == 1,\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/multi_select_notification_item.dart",
    "content": "import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';\nimport 'package:appflowy/mobile/presentation/base/animated_gesture.dart';\nimport 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';\nimport 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MultiSelectNotificationItem extends StatelessWidget {\n  const MultiSelectNotificationItem({\n    super.key,\n    required this.reminder,\n  });\n\n  final ReminderPB reminder;\n\n  @override\n  Widget build(BuildContext context) {\n    final settings = context.read<AppearanceSettingsCubit>().state;\n    final dateFormate = settings.dateFormat;\n    final timeFormate = settings.timeFormat;\n    return BlocProvider<NotificationReminderBloc>(\n      create: (context) => NotificationReminderBloc()\n        ..add(\n          NotificationReminderEvent.initial(\n            reminder,\n            dateFormate,\n            timeFormate,\n          ),\n        ),\n      child: BlocBuilder<NotificationReminderBloc, NotificationReminderState>(\n        builder: (context, state) {\n          if (state.status == NotificationReminderStatus.loading ||\n              state.status == NotificationReminderStatus.initial) {\n            return const SizedBox.shrink();\n          }\n\n          if (state.status == NotificationReminderStatus.error) {\n            // error handle.\n            return const SizedBox.shrink();\n          }\n\n          final child = ValueListenableBuilder(\n            valueListenable: mSelectedNotificationIds,\n            builder: (_, selectedIds, child) {\n              return Container(\n                margin: const EdgeInsets.symmetric(horizontal: 12),\n                decoration: selectedIds.contains(reminder.id)\n                    ? ShapeDecoration(\n                        color: const Color(0x1900BCF0),\n                        shape: RoundedRectangleBorder(\n                          borderRadius: BorderRadius.circular(10),\n                        ),\n                      )\n                    : null,\n                child: child,\n              );\n            },\n            child: Padding(\n              padding: const EdgeInsets.symmetric(vertical: 8),\n              child: _InnerNotificationItem(\n                reminder: reminder,\n              ),\n            ),\n          );\n\n          return AnimatedGestureDetector(\n            scaleFactor: 0.99,\n            onTapUp: () {\n              if (mSelectedNotificationIds.value.contains(reminder.id)) {\n                mSelectedNotificationIds.value = mSelectedNotificationIds.value\n                  ..remove(reminder.id);\n              } else {\n                mSelectedNotificationIds.value = mSelectedNotificationIds.value\n                  ..add(reminder.id);\n              }\n            },\n            child: child,\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _InnerNotificationItem extends StatelessWidget {\n  const _InnerNotificationItem({\n    required this.reminder,\n  });\n\n  final ReminderPB reminder;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        const HSpace(10.0),\n        NotificationCheckIcon(\n          isSelected: mSelectedNotificationIds.value.contains(reminder.id),\n        ),\n        const HSpace(12.0),\n        NotificationIcon(reminder: reminder),\n        const HSpace(12.0),\n        Expanded(\n          child: NotificationContent(reminder: reminder),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/notification_item.dart",
    "content": "import 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';\nimport 'package:appflowy/mobile/presentation/base/animated_gesture.dart';\nimport 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_slidable/flutter_slidable.dart';\n\nclass NotificationItem extends StatelessWidget {\n  const NotificationItem({\n    super.key,\n    required this.tabType,\n    required this.reminder,\n  });\n\n  final NotificationTabType tabType;\n  final ReminderPB reminder;\n\n  @override\n  Widget build(BuildContext context) {\n    final settings = context.read<AppearanceSettingsCubit>().state;\n    final dateFormate = settings.dateFormat;\n    final timeFormate = settings.timeFormat;\n    return BlocProvider<NotificationReminderBloc>(\n      create: (context) => NotificationReminderBloc()\n        ..add(\n          NotificationReminderEvent.initial(\n            reminder,\n            dateFormate,\n            timeFormate,\n          ),\n        ),\n      child: BlocBuilder<NotificationReminderBloc, NotificationReminderState>(\n        builder: (context, state) {\n          if (state.status == NotificationReminderStatus.loading ||\n              state.status == NotificationReminderStatus.initial) {\n            return const SizedBox.shrink();\n          }\n\n          if (state.status == NotificationReminderStatus.error) {\n            // error handle.\n            return const SizedBox.shrink();\n          }\n\n          final child = GestureDetector(\n            onLongPress: () {\n              context.onMoreAction();\n            },\n            child: Padding(\n              padding: const EdgeInsets.all(8),\n              child: _SlidableNotificationItem(\n                tabType: tabType,\n                reminder: reminder,\n                child: _InnerNotificationItem(\n                  tabType: tabType,\n                  reminder: reminder,\n                ),\n              ),\n            ),\n          );\n\n          return AnimatedGestureDetector(\n            scaleFactor: 0.99,\n            child: child,\n            onTapUp: () async {\n              final view = state.view;\n              final blockId = state.blockId;\n              if (view == null) {\n                return;\n              }\n\n              await context.pushView(\n                view,\n                blockId: blockId,\n              );\n\n              if (!reminder.isRead && context.mounted) {\n                context.read<ReminderBloc>().add(\n                      ReminderEvent.markAsRead([reminder.id]),\n                    );\n              }\n            },\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _InnerNotificationItem extends StatelessWidget {\n  const _InnerNotificationItem({\n    required this.reminder,\n    required this.tabType,\n  });\n\n  final NotificationTabType tabType;\n  final ReminderPB reminder;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        const HSpace(10.0),\n        NotificationIcon(reminder: reminder),\n        const HSpace(12.0),\n        Expanded(child: NotificationContent(reminder: reminder)),\n        const HSpace(6.0),\n      ],\n    );\n  }\n}\n\nclass _SlidableNotificationItem extends StatelessWidget {\n  const _SlidableNotificationItem({\n    required this.tabType,\n    required this.reminder,\n    required this.child,\n  });\n\n  final NotificationTabType tabType;\n  final ReminderPB reminder;\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    final List<NotificationPaneActionType> actions = switch (tabType) {\n      NotificationTabType.inbox => [\n          NotificationPaneActionType.more,\n          if (!reminder.isRead) NotificationPaneActionType.markAsRead,\n        ],\n      NotificationTabType.unread => [\n          NotificationPaneActionType.more,\n          NotificationPaneActionType.markAsRead,\n        ],\n      NotificationTabType.archive => [\n          if (kDebugMode) NotificationPaneActionType.unArchive,\n        ],\n    };\n\n    if (actions.isEmpty) {\n      return child;\n    }\n\n    final children = actions\n        .map(\n          (action) => action.actionButton(\n            context,\n            tabType: tabType,\n          ),\n        )\n        .toList();\n\n    final extentRatio = actions.length == 1 ? 1 / 5 : 1 / 3;\n\n    return Slidable(\n      endActionPane: ActionPane(\n        motion: const ScrollMotion(),\n        extentRatio: extentRatio,\n        children: children,\n      ),\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/settings_popup_menu.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:appflowy/shared/popup_menu/appflowy_popup_menu.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart'\n    hide PopupMenuButton, PopupMenuDivider, PopupMenuItem, PopupMenuEntry;\nimport 'package:go_router/go_router.dart';\nimport 'package:provider/provider.dart';\n\nenum _NotificationSettingsPopupMenuItem {\n  settings,\n  markAllAsRead,\n  archiveAll,\n  // only visible in debug mode\n  unarchiveAll;\n}\n\nclass NotificationSettingsPopupMenu extends StatelessWidget {\n  const NotificationSettingsPopupMenu({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return PopupMenuButton<_NotificationSettingsPopupMenuItem>(\n      offset: const Offset(0, 36),\n      padding: EdgeInsets.zero,\n      shape: const RoundedRectangleBorder(\n        borderRadius: BorderRadius.all(\n          Radius.circular(12.0),\n        ),\n      ),\n      // todo: replace it with shadows\n      shadowColor: const Color(0x68000000),\n      elevation: 10,\n      color: context.popupMenuBackgroundColor,\n      itemBuilder: (BuildContext context) =>\n          <PopupMenuEntry<_NotificationSettingsPopupMenuItem>>[\n        _buildItem(\n          value: _NotificationSettingsPopupMenuItem.settings,\n          svg: FlowySvgs.m_notification_settings_s,\n          text: LocaleKeys.settings_notifications_settings_settings.tr(),\n        ),\n        const PopupMenuDivider(height: 0.5),\n        _buildItem(\n          value: _NotificationSettingsPopupMenuItem.markAllAsRead,\n          svg: FlowySvgs.m_notification_mark_as_read_s,\n          text: LocaleKeys.settings_notifications_settings_markAllAsRead.tr(),\n        ),\n        const PopupMenuDivider(height: 0.5),\n        _buildItem(\n          value: _NotificationSettingsPopupMenuItem.archiveAll,\n          svg: FlowySvgs.m_notification_archived_s,\n          text: LocaleKeys.settings_notifications_settings_archiveAll.tr(),\n        ),\n        // only visible in debug mode\n        if (kDebugMode) ...[\n          const PopupMenuDivider(height: 0.5),\n          _buildItem(\n            value: _NotificationSettingsPopupMenuItem.unarchiveAll,\n            svg: FlowySvgs.m_notification_archived_s,\n            text: 'Unarchive all (Debug Mode)',\n          ),\n        ],\n      ],\n      onSelected: (_NotificationSettingsPopupMenuItem value) {\n        switch (value) {\n          case _NotificationSettingsPopupMenuItem.markAllAsRead:\n            _onMarkAllAsRead(context);\n            break;\n          case _NotificationSettingsPopupMenuItem.archiveAll:\n            _onArchiveAll(context);\n            break;\n          case _NotificationSettingsPopupMenuItem.settings:\n            context.push(MobileHomeSettingPage.routeName);\n            break;\n          case _NotificationSettingsPopupMenuItem.unarchiveAll:\n            _onUnarchiveAll(context);\n            break;\n        }\n      },\n      child: const Padding(\n        padding: EdgeInsets.all(8.0),\n        child: FlowySvg(\n          FlowySvgs.m_settings_more_s,\n        ),\n      ),\n    );\n  }\n\n  PopupMenuItem<T> _buildItem<T>({\n    required T value,\n    required FlowySvgData svg,\n    required String text,\n  }) {\n    return PopupMenuItem<T>(\n      value: value,\n      padding: EdgeInsets.zero,\n      child: _PopupButton(\n        svg: svg,\n        text: text,\n      ),\n    );\n  }\n\n  void _onMarkAllAsRead(BuildContext context) {\n    showToastNotification(\n      message: LocaleKeys\n          .settings_notifications_markAsReadNotifications_allSuccess\n          .tr(),\n    );\n\n    context.read<ReminderBloc>().add(const ReminderEvent.markAllRead());\n  }\n\n  void _onArchiveAll(BuildContext context) {\n    showToastNotification(\n      message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess\n          .tr(),\n    );\n\n    context.read<ReminderBloc>().add(const ReminderEvent.archiveAll());\n  }\n\n  void _onUnarchiveAll(BuildContext context) {\n    if (!kDebugMode) {\n      return;\n    }\n\n    showToastNotification(\n      message: 'Unarchive all success (Debug Mode)',\n    );\n\n    context.read<ReminderBloc>().add(const ReminderEvent.unarchiveAll());\n  }\n}\n\nclass _PopupButton extends StatelessWidget {\n  const _PopupButton({\n    required this.svg,\n    required this.text,\n  });\n\n  final FlowySvgData svg;\n  final String text;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: 44,\n      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),\n      child: Row(\n        children: [\n          FlowySvg(svg),\n          const HSpace(12),\n          FlowyText.regular(\n            text,\n            fontSize: 16,\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/shared.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/mobile/presentation/notifications/widgets/color.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_configuration.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/user/application/reminder/reminder_extension.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nconst _kNotificationIconHeight = 36.0;\n\nclass NotificationIcon extends StatelessWidget {\n  const NotificationIcon({\n    super.key,\n    required this.reminder,\n    this.atSize = 12,\n  });\n\n  final ReminderPB reminder;\n  final double atSize;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return SizedBox(\n      width: 42,\n      height: 36,\n      child: Stack(\n        children: [\n          const FlowySvg(\n            FlowySvgs.m_notification_reminder_s,\n            size: Size.square(32),\n            blendMode: null,\n          ),\n          Align(\n            alignment: Alignment.bottomRight,\n            child: Container(\n              width: 20,\n              height: 20,\n              decoration: BoxDecoration(\n                shape: BoxShape.circle,\n                color: theme.fillColorScheme.primary,\n              ),\n              child: Center(\n                child: FlowySvg(\n                  FlowySvgs.notification_icon_at_s,\n                  size: Size.square(atSize),\n                  color: theme.iconColorScheme.primary,\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass NotificationCheckIcon extends StatelessWidget {\n  const NotificationCheckIcon({super.key, required this.isSelected});\n\n  final bool isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: _kNotificationIconHeight,\n      child: Center(\n        child: FlowySvg(\n          isSelected\n              ? FlowySvgs.m_notification_multi_select_s\n              : FlowySvgs.m_notification_multi_unselect_s,\n          blendMode: isSelected ? null : BlendMode.srcIn,\n        ),\n      ),\n    );\n  }\n}\n\nclass UnreadRedDot extends StatelessWidget {\n  const UnreadRedDot({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return SizedBox(\n      height: _kNotificationIconHeight,\n      child: Center(\n        child: SizedBox.square(\n          dimension: 7.0,\n          child: DecoratedBox(\n            decoration: ShapeDecoration(\n              color: theme.borderColorScheme.errorThick,\n              shape: OvalBorder(),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass NotificationContent extends StatefulWidget {\n  const NotificationContent({\n    super.key,\n    required this.reminder,\n  });\n\n  final ReminderPB reminder;\n\n  @override\n  State<NotificationContent> createState() => _NotificationContentState();\n}\n\nclass _NotificationContentState extends State<NotificationContent> {\n  AppFlowyThemeData get theme => AppFlowyTheme.of(context);\n\n  @override\n  void didUpdateWidget(covariant NotificationContent oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    context.read<NotificationReminderBloc>().add(\n          const NotificationReminderEvent.reset(),\n        );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<NotificationReminderBloc, NotificationReminderState>(\n      builder: (context, state) {\n        final view = state.view;\n        if (view == null) {\n          return const SizedBox.shrink();\n        }\n\n        return Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            // title & time\n            _buildHeader(state.scheduledAt, !widget.reminder.isRead),\n            // page name\n            _buildPageName(context, state.isLocked, state.pageTitle),\n            // content\n            _buildContent(view, nodes: state.nodes),\n          ],\n        );\n      },\n    );\n  }\n\n  Widget _buildContent(ViewPB view, {List<Node>? nodes}) {\n    if (view.layout.isDocumentView && nodes != null) {\n      return IntrinsicHeight(\n        child: BlocProvider(\n          create: (context) => DocumentPageStyleBloc(view: view),\n          child: NotificationDocumentContent(\n            reminder: widget.reminder,\n            nodes: nodes,\n          ),\n        ),\n      );\n    } else if (view.layout.isDatabaseView) {\n      final opacity = widget.reminder.type == ReminderType.past ? 0.3 : 1.0;\n      return Opacity(\n        opacity: opacity,\n        child: FlowyText(\n          widget.reminder.message,\n          fontSize: 14,\n          figmaLineHeight: 22,\n          color: context.notificationItemTextColor,\n          maxLines: 3,\n          overflow: TextOverflow.ellipsis,\n        ),\n      );\n    }\n\n    return const SizedBox.shrink();\n  }\n\n  Widget _buildHeader(String createAt, bool unread) {\n    return SizedBox(\n      height: 22,\n      child: Row(\n        children: [\n          FlowyText.semibold(\n            LocaleKeys.settings_notifications_titles_reminder.tr(),\n            fontSize: 14,\n            figmaLineHeight: 20,\n            color: theme.textColorScheme.primary,\n          ),\n          Spacer(),\n          if (createAt.isNotEmpty)\n            FlowyText.regular(\n              createAt,\n              fontSize: 12,\n              figmaLineHeight: 18,\n              color: theme.textColorScheme.secondary,\n            ),\n          if (unread) ...[\n            HSpace(4),\n            const UnreadRedDot(),\n          ],\n        ],\n      ),\n    );\n  }\n\n  Widget _buildPageName(\n    BuildContext context,\n    bool isLocked,\n    String pageTitle,\n  ) {\n    return Opacity(\n      opacity: 0.5,\n      child: SizedBox(\n        height: 18,\n        child: Row(\n          children: [\n            /// TODO: need to be replaced after reminder support more types\n            FlowyText.regular(\n              LocaleKeys.notificationHub_mentionedYou.tr(),\n              fontSize: 12,\n              figmaLineHeight: 18,\n              color: theme.textColorScheme.secondary,\n            ),\n            const NotificationEllipse(),\n            if (isLocked)\n              Padding(\n                padding: EdgeInsets.only(right: 5),\n                child: FlowySvg(\n                  FlowySvgs.notification_lock_s,\n                  color: theme.iconColorScheme.secondary,\n                ),\n              ),\n            Flexible(\n              child: FlowyText.regular(\n                pageTitle,\n                fontSize: 12,\n                figmaLineHeight: 18,\n                color: theme.textColorScheme.secondary,\n                overflow: TextOverflow.ellipsis,\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass NotificationEllipse extends StatelessWidget {\n  const NotificationEllipse({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      width: 2.50,\n      height: 2.50,\n      margin: const EdgeInsets.symmetric(horizontal: 6.0),\n      decoration: ShapeDecoration(\n        color: context.notificationItemTextColor,\n        shape: const OvalBorder(),\n      ),\n    );\n  }\n}\n\nclass NotificationDocumentContent extends StatelessWidget {\n  const NotificationDocumentContent({\n    super.key,\n    required this.reminder,\n    required this.nodes,\n  });\n\n  final ReminderPB reminder;\n  final List<Node> nodes;\n\n  @override\n  Widget build(BuildContext context) {\n    final editorState = EditorState(\n      document: Document(\n        root: pageNode(children: nodes),\n      ),\n    );\n\n    final styleCustomizer = EditorStyleCustomizer(\n      context: context,\n      padding: EdgeInsets.zero,\n    );\n\n    final editorStyle = styleCustomizer.style().copyWith(\n          // hide the cursor\n          cursorColor: Colors.transparent,\n          cursorWidth: 0,\n          textStyleConfiguration: TextStyleConfiguration(\n            lineHeight: 22 / 14,\n            applyHeightToFirstAscent: true,\n            applyHeightToLastDescent: true,\n            text: TextStyle(\n              fontSize: 14,\n              color: context.notificationItemTextColor,\n              height: 22 / 14,\n              fontWeight: FontWeight.w400,\n              leadingDistribution: TextLeadingDistribution.even,\n            ),\n          ),\n        );\n\n    final blockBuilders = buildBlockComponentBuilders(\n      context: context,\n      editorState: editorState,\n      styleCustomizer: styleCustomizer,\n      // the editor is not editable in the chat\n      editable: false,\n      customPadding: (node) => EdgeInsets.zero,\n    );\n\n    final headingBuilder = blockBuilders[HeadingBlockKeys.type];\n    if (headingBuilder != null &&\n        headingBuilder is HeadingBlockComponentBuilder) {\n      final newHeadingBuilder = HeadingBlockComponentBuilder(\n        configuration: headingBuilder.configuration,\n        textStyleBuilder: (v) {\n          final fontFamily = context\n              .read<DocumentAppearanceCubit>()\n              .state\n              .fontFamily\n              .orDefault(\n                context.read<AppearanceSettingsCubit>().state.font,\n              );\n          return styleCustomizer.baseTextStyle(fontFamily);\n        },\n      );\n      blockBuilders[HeadingBlockKeys.type] = newHeadingBuilder;\n    }\n\n    return IgnorePointer(\n      child: Opacity(\n        opacity: reminder.type == ReminderType.past ? 0.3 : 1,\n        child: AppFlowyEditor(\n          editorState: editorState,\n          editorStyle: editorStyle,\n          disableSelectionService: true,\n          disableKeyboardService: true,\n          disableScrollService: true,\n          editable: false,\n          shrinkWrap: true,\n          blockComponentBuilders: blockBuilders,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/slide_actions.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/page_item/mobile_slide_action_button.dart';\nimport 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/user/application/reminder/reminder_extension.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nenum NotificationPaneActionType {\n  more,\n  markAsRead,\n  // only used in the debug mode.\n  unArchive;\n\n  MobileSlideActionButton actionButton(\n    BuildContext context, {\n    required NotificationTabType tabType,\n  }) {\n    switch (this) {\n      case NotificationPaneActionType.markAsRead:\n        return MobileSlideActionButton(\n          backgroundColor: const Color(0xFF00C8FF),\n          svg: FlowySvgs.m_notification_action_mark_as_read_s,\n          size: 24.0,\n          onPressed: (context) {\n            showToastNotification(\n              message: LocaleKeys\n                  .settings_notifications_markAsReadNotifications_success\n                  .tr(),\n            );\n\n            context.read<ReminderBloc>().add(\n                  ReminderEvent.update(\n                    ReminderUpdate(\n                      id: context.read<NotificationReminderBloc>().reminder.id,\n                      isRead: true,\n                    ),\n                  ),\n                );\n          },\n        );\n      // this action is only used in the debug mode.\n      case NotificationPaneActionType.unArchive:\n        return MobileSlideActionButton(\n          backgroundColor: const Color(0xFF00C8FF),\n          svg: FlowySvgs.m_notification_action_mark_as_read_s,\n          size: 24.0,\n          onPressed: (context) {\n            showToastNotification(\n              message: 'Unarchive notification success',\n            );\n\n            context.read<ReminderBloc>().add(\n                  ReminderEvent.update(\n                    ReminderUpdate(\n                      id: context.read<NotificationReminderBloc>().reminder.id,\n                      isArchived: false,\n                    ),\n                  ),\n                );\n          },\n        );\n      case NotificationPaneActionType.more:\n        return MobileSlideActionButton(\n          backgroundColor: const Color(0xE5515563),\n          svg: FlowySvgs.three_dots_s,\n          size: 24.0,\n          borderRadius: const BorderRadius.only(\n            topLeft: Radius.circular(10),\n            bottomLeft: Radius.circular(10),\n          ),\n          onPressed: (context) => context.onMoreAction(),\n        );\n    }\n  }\n}\n\nextension NotificationPaneActionExtension on BuildContext {\n  void onMoreAction() {\n    final reminderBloc = read<ReminderBloc>();\n    final notificationReminderBloc = read<NotificationReminderBloc>();\n\n    showMobileBottomSheet(\n      this,\n      showDragHandle: true,\n      showDivider: false,\n      useRootNavigator: true,\n      backgroundColor: Theme.of(this).colorScheme.surface,\n      builder: (_) {\n        return MultiBlocProvider(\n          providers: [\n            BlocProvider.value(value: reminderBloc),\n            BlocProvider.value(value: notificationReminderBloc),\n          ],\n          child: _NotificationMoreActions(\n            onClickMultipleChoice: () {\n              Future.delayed(const Duration(milliseconds: 250), () {\n                bottomNavigationBarType.value =\n                    BottomNavigationBarActionType.notificationMultiSelect;\n              });\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass _NotificationMoreActions extends StatelessWidget {\n  const _NotificationMoreActions({\n    required this.onClickMultipleChoice,\n  });\n\n  final VoidCallback onClickMultipleChoice;\n\n  @override\n  Widget build(BuildContext context) {\n    final reminder = context.read<NotificationReminderBloc>().reminder;\n    return Column(\n      children: [\n        if (!reminder.isRead)\n          FlowyOptionTile.text(\n            height: 52.0,\n            text: LocaleKeys.settings_notifications_action_markAsRead.tr(),\n            leftIcon: const FlowySvg(\n              FlowySvgs.m_notification_action_mark_as_read_s,\n              size: Size.square(20),\n            ),\n            showTopBorder: false,\n            showBottomBorder: false,\n            onTap: () => _onMarkAsRead(context),\n          ),\n        FlowyOptionTile.text(\n          height: 52.0,\n          text: LocaleKeys.settings_notifications_action_multipleChoice.tr(),\n          leftIcon: const FlowySvg(\n            FlowySvgs.m_notification_action_multiple_choice_s,\n            size: Size.square(20),\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () => _onMultipleChoice(context),\n        ),\n        if (!reminder.isArchived)\n          FlowyOptionTile.text(\n            height: 52.0,\n            text: LocaleKeys.settings_notifications_action_archive.tr(),\n            leftIcon: const FlowySvg(\n              FlowySvgs.m_notification_action_archive_s,\n              size: Size.square(20),\n            ),\n            showTopBorder: false,\n            showBottomBorder: false,\n            onTap: () => _onArchive(context),\n          ),\n      ],\n    );\n  }\n\n  void _onMarkAsRead(BuildContext context) {\n    Navigator.of(context).pop();\n\n    showToastNotification(\n      message: LocaleKeys.settings_notifications_markAsReadNotifications_success\n          .tr(),\n    );\n\n    context.read<ReminderBloc>().add(\n          ReminderEvent.update(\n            ReminderUpdate(\n              id: context.read<NotificationReminderBloc>().reminder.id,\n              isRead: true,\n            ),\n          ),\n        );\n  }\n\n  void _onMultipleChoice(BuildContext context) {\n    Navigator.of(context).pop();\n\n    onClickMultipleChoice();\n  }\n\n  void _onArchive(BuildContext context) {\n    showToastNotification(\n      message: LocaleKeys.settings_notifications_archiveNotifications_success\n          .tr()\n          .tr(),\n    );\n\n    context.read<ReminderBloc>().add(\n          ReminderEvent.update(\n            ReminderUpdate(\n              id: context.read<NotificationReminderBloc>().reminder.id,\n              isRead: true,\n              isArchived: true,\n            ),\n          ),\n        );\n\n    Navigator.of(context).pop();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';\nimport 'package:appflowy/shared/list_extension.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/user/application/reminder/reminder_extension.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/appflowy_backend.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass NotificationTab extends StatefulWidget {\n  const NotificationTab({\n    super.key,\n    required this.tabType,\n  });\n\n  final NotificationTabType tabType;\n\n  @override\n  State<NotificationTab> createState() => _NotificationTabState();\n}\n\nclass _NotificationTabState extends State<NotificationTab>\n    with AutomaticKeepAliveClientMixin {\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n\n    return BlocBuilder<ReminderBloc, ReminderState>(\n      builder: (context, state) {\n        final reminders = _filterReminders(state.reminders);\n\n        if (reminders.isEmpty) {\n          // add refresh indicator to the empty notification.\n          return EmptyNotification(\n            type: widget.tabType,\n          );\n        }\n\n        final child = ListView.separated(\n          itemCount: reminders.length,\n          separatorBuilder: (context, index) => const VSpace(8.0),\n          itemBuilder: (context, index) {\n            final reminder = reminders[index];\n            return NotificationItem(\n              key: ValueKey('${widget.tabType}_${reminder.id}'),\n              tabType: widget.tabType,\n              reminder: reminder,\n            );\n          },\n        );\n\n        return RefreshIndicator.adaptive(\n          onRefresh: () async => _onRefresh(context),\n          child: child,\n        );\n      },\n    );\n  }\n\n  Future<void> _onRefresh(BuildContext context) async {\n    context.read<ReminderBloc>().add(const ReminderEvent.refresh());\n\n    // at least 0.5 seconds to dismiss the refresh indicator.\n    // otherwise, it will be dismissed immediately.\n    await context.read<ReminderBloc>().stream.firstOrNull;\n    await Future.delayed(const Duration(milliseconds: 500));\n\n    if (context.mounted) {\n      showToastNotification(\n        message: LocaleKeys.settings_notifications_refreshSuccess.tr(),\n      );\n    }\n  }\n\n  List<ReminderPB> _filterReminders(List<ReminderPB> reminders) {\n    switch (widget.tabType) {\n      case NotificationTabType.inbox:\n        return reminders.reversed\n            .where((reminder) => !reminder.isArchived)\n            .toList()\n            .unique((reminder) => reminder.id);\n      case NotificationTabType.archive:\n        return reminders.reversed\n            .where((reminder) => reminder.isArchived)\n            .toList()\n            .unique((reminder) => reminder.id);\n      case NotificationTabType.unread:\n        return reminders.reversed\n            .where((reminder) => !reminder.isRead)\n            .toList()\n            .unique((reminder) => reminder.id);\n    }\n  }\n}\n\nclass MultiSelectNotificationTab extends StatelessWidget {\n  const MultiSelectNotificationTab({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<ReminderBloc, ReminderState>(\n      builder: (context, state) {\n        // find the reminders that are not archived or read.\n        final reminders = state.reminders.reversed\n            .where((reminder) => !reminder.isArchived || !reminder.isRead)\n            .toList();\n\n        if (reminders.isEmpty) {\n          // add refresh indicator to the empty notification.\n          return const SizedBox.shrink();\n        }\n\n        return ListView.separated(\n          itemCount: reminders.length,\n          separatorBuilder: (context, index) => const VSpace(8.0),\n          itemBuilder: (context, index) {\n            final reminder = reminders[index];\n            return MultiSelectNotificationItem(\n              key: ValueKey(reminder.id),\n              reminder: reminder,\n            );\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab_bar.dart",
    "content": "import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';\nimport 'package:flutter/material.dart';\nimport 'package:reorderable_tabbar/reorderable_tabbar.dart';\n\nclass MobileNotificationTabBar extends StatelessWidget {\n  const MobileNotificationTabBar({\n    super.key,\n    this.height = 38.0,\n    required this.tabController,\n    required this.tabs,\n  });\n\n  final double height;\n  final List<NotificationTabType> tabs;\n  final TabController tabController;\n\n  @override\n  Widget build(BuildContext context) {\n    final baseStyle = Theme.of(context).textTheme.bodyMedium;\n    final labelStyle = baseStyle?.copyWith(\n      fontWeight: FontWeight.w500,\n      fontSize: 16.0,\n      height: 22.0 / 16.0,\n    );\n    final unselectedLabelStyle = baseStyle?.copyWith(\n      fontWeight: FontWeight.w400,\n      fontSize: 15.0,\n      height: 22.0 / 15.0,\n    );\n\n    return Container(\n      height: height,\n      padding: const EdgeInsets.only(left: 8.0),\n      child: ReorderableTabBar(\n        controller: tabController,\n        tabs: tabs.map((e) => Tab(text: e.tr)).toList(),\n        indicatorSize: TabBarIndicatorSize.label,\n        isScrollable: true,\n        labelStyle: labelStyle,\n        labelColor: baseStyle?.color,\n        labelPadding: const EdgeInsets.symmetric(horizontal: 12.0),\n        unselectedLabelStyle: unselectedLabelStyle,\n        overlayColor: WidgetStateProperty.all(Colors.transparent),\n        indicator: const RoundUnderlineTabIndicator(\n          width: 28.0,\n          borderSide: BorderSide(\n            color: Color(0xFF00C8FF),\n            width: 3,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/widgets.dart",
    "content": "export 'empty.dart';\nexport 'header.dart';\nexport 'multi_select_notification_item.dart';\nexport 'notification_item.dart';\nexport 'settings_popup_menu.dart';\nexport 'shared.dart';\nexport 'slide_actions.dart';\nexport 'tab.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_slide_action_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_slidable/flutter_slidable.dart';\n\nclass MobileSlideActionButton extends StatelessWidget {\n  const MobileSlideActionButton({\n    super.key,\n    required this.svg,\n    this.size = 32.0,\n    this.backgroundColor = Colors.transparent,\n    this.borderRadius = BorderRadius.zero,\n    required this.onPressed,\n  });\n\n  final FlowySvgData svg;\n  final double size;\n  final Color backgroundColor;\n  final SlidableActionCallback onPressed;\n  final BorderRadius borderRadius;\n\n  @override\n  Widget build(BuildContext context) {\n    return CustomSlidableAction(\n      borderRadius: borderRadius,\n      backgroundColor: backgroundColor,\n      onPressed: (context) {\n        HapticFeedback.mediumImpact();\n        onPressed(context);\n      },\n      padding: EdgeInsets.zero,\n      child: FlowySvg(\n        svg,\n        size: Size.square(size),\n        color: Colors.white,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_slidable/flutter_slidable.dart';\n\ntypedef ViewItemOnSelected = void Function(ViewPB);\ntypedef ActionPaneBuilder = ActionPane Function(BuildContext context);\n\nclass MobileViewItem extends StatelessWidget {\n  const MobileViewItem({\n    super.key,\n    required this.view,\n    this.parentView,\n    required this.spaceType,\n    required this.level,\n    this.leftPadding = 10,\n    required this.onSelected,\n    this.isFirstChild = false,\n    this.isDraggable = true,\n    required this.isFeedback,\n    this.startActionPane,\n    this.endActionPane,\n  });\n\n  final ViewPB view;\n  final ViewPB? parentView;\n\n  final FolderSpaceType spaceType;\n\n  // indicate the level of the view item\n  // used to calculate the left padding\n  final int level;\n\n  // the left padding of the view item for each level\n  // the left padding of the each level = level * leftPadding\n  final double leftPadding;\n\n  // Selected by normal conventions\n  final ViewItemOnSelected onSelected;\n\n  // used for indicating the first child of the parent view, so that we can\n  // add top border to the first child\n  final bool isFirstChild;\n\n  // it should be false when it's rendered as feedback widget inside DraggableItem\n  final bool isDraggable;\n\n  // identify if the view item is rendered as feedback widget inside DraggableItem\n  final bool isFeedback;\n\n  // the actions of the view item, such as favorite, rename, delete, etc.\n  final ActionPaneBuilder? startActionPane;\n  final ActionPaneBuilder? endActionPane;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()),\n      child: BlocConsumer<ViewBloc, ViewState>(\n        listenWhen: (p, c) =>\n            c.lastCreatedView != null &&\n            p.lastCreatedView?.id != c.lastCreatedView!.id,\n        listener: (context, state) => context.pushView(state.lastCreatedView!),\n        builder: (context, state) {\n          return InnerMobileViewItem(\n            view: state.view,\n            parentView: parentView,\n            childViews: state.view.childViews,\n            spaceType: spaceType,\n            level: level,\n            leftPadding: leftPadding,\n            showActions: true,\n            isExpanded: state.isExpanded,\n            onSelected: onSelected,\n            isFirstChild: isFirstChild,\n            isDraggable: isDraggable,\n            isFeedback: isFeedback,\n            startActionPane: startActionPane,\n            endActionPane: endActionPane,\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass InnerMobileViewItem extends StatelessWidget {\n  const InnerMobileViewItem({\n    super.key,\n    required this.view,\n    required this.parentView,\n    required this.childViews,\n    required this.spaceType,\n    this.isDraggable = true,\n    this.isExpanded = true,\n    required this.level,\n    required this.leftPadding,\n    required this.showActions,\n    required this.onSelected,\n    this.isFirstChild = false,\n    required this.isFeedback,\n    this.startActionPane,\n    this.endActionPane,\n  });\n\n  final ViewPB view;\n  final ViewPB? parentView;\n  final List<ViewPB> childViews;\n  final FolderSpaceType spaceType;\n\n  final bool isDraggable;\n  final bool isExpanded;\n  final bool isFirstChild;\n\n  // identify if the view item is rendered as feedback widget inside DraggableItem\n  final bool isFeedback;\n\n  final int level;\n  final double leftPadding;\n\n  final bool showActions;\n  final ViewItemOnSelected onSelected;\n\n  final ActionPaneBuilder? startActionPane;\n  final ActionPaneBuilder? endActionPane;\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child = SingleMobileInnerViewItem(\n      view: view,\n      parentView: parentView,\n      level: level,\n      showActions: showActions,\n      spaceType: spaceType,\n      onSelected: onSelected,\n      isExpanded: isExpanded,\n      isDraggable: isDraggable,\n      leftPadding: leftPadding,\n      isFeedback: isFeedback,\n      startActionPane: startActionPane,\n      endActionPane: endActionPane,\n    );\n\n    // if the view is expanded and has child views, render its child views\n    if (isExpanded) {\n      if (childViews.isNotEmpty) {\n        final children = childViews.map((childView) {\n          return MobileViewItem(\n            key: ValueKey('${spaceType.name} ${childView.id}'),\n            parentView: view,\n            spaceType: spaceType,\n            isFirstChild: childView.id == childViews.first.id,\n            view: childView,\n            level: level + 1,\n            onSelected: onSelected,\n            isDraggable: isDraggable,\n            leftPadding: leftPadding,\n            isFeedback: isFeedback,\n            startActionPane: startActionPane,\n            endActionPane: endActionPane,\n          );\n        }).toList();\n\n        child = Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            child,\n            ...children,\n          ],\n        );\n      }\n    }\n\n    // wrap the child with DraggableItem if isDraggable is true\n    if (isDraggable && !isReferencedDatabaseView(view, parentView)) {\n      child = DraggableViewItem(\n        isFirstChild: isFirstChild,\n        view: view,\n        centerHighlightColor: Colors.blue.shade200,\n        topHighlightColor: Colors.blue.shade200,\n        bottomHighlightColor: Colors.blue.shade200,\n        feedback: (context) {\n          return MobileViewItem(\n            view: view,\n            parentView: parentView,\n            spaceType: spaceType,\n            level: level,\n            onSelected: onSelected,\n            isDraggable: false,\n            leftPadding: leftPadding,\n            isFeedback: true,\n            startActionPane: startActionPane,\n            endActionPane: endActionPane,\n          );\n        },\n        child: child,\n      );\n    }\n\n    return child;\n  }\n}\n\nclass SingleMobileInnerViewItem extends StatefulWidget {\n  const SingleMobileInnerViewItem({\n    super.key,\n    required this.view,\n    required this.parentView,\n    required this.isExpanded,\n    required this.level,\n    required this.leftPadding,\n    this.isDraggable = true,\n    required this.spaceType,\n    required this.showActions,\n    required this.onSelected,\n    required this.isFeedback,\n    this.startActionPane,\n    this.endActionPane,\n  });\n\n  final ViewPB view;\n  final ViewPB? parentView;\n  final bool isExpanded;\n\n  // identify if the view item is rendered as feedback widget inside DraggableItem\n  final bool isFeedback;\n\n  final int level;\n  final double leftPadding;\n\n  final bool isDraggable;\n  final bool showActions;\n  final ViewItemOnSelected onSelected;\n  final FolderSpaceType spaceType;\n  final ActionPaneBuilder? startActionPane;\n  final ActionPaneBuilder? endActionPane;\n\n  @override\n  State<SingleMobileInnerViewItem> createState() =>\n      _SingleMobileInnerViewItemState();\n}\n\nclass _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {\n  @override\n  Widget build(BuildContext context) {\n    final children = [\n      // expand icon\n      _buildLeftIcon(),\n      // icon\n      _buildViewIcon(),\n      const HSpace(8),\n      // title\n      Expanded(\n        child: FlowyText.regular(\n          widget.view.nameOrDefault,\n          fontSize: 16.0,\n          figmaLineHeight: 20.0,\n          overflow: TextOverflow.ellipsis,\n        ),\n      ),\n    ];\n\n    Widget child = InkWell(\n      borderRadius: BorderRadius.circular(4.0),\n      onTap: () => widget.onSelected(widget.view),\n      child: SizedBox(\n        height: HomeSpaceViewSizes.mViewHeight,\n        child: Padding(\n          padding: EdgeInsets.only(left: widget.level * widget.leftPadding),\n          child: Row(\n            children: children,\n          ),\n        ),\n      ),\n    );\n\n    if (widget.startActionPane != null || widget.endActionPane != null) {\n      child = Slidable(\n        // Specify a key if the Slidable is dismissible.\n        key: ValueKey(widget.view.hashCode),\n        startActionPane: widget.startActionPane?.call(context),\n        endActionPane: widget.endActionPane?.call(context),\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  Widget _buildViewIcon() {\n    final iconData = widget.view.icon.toEmojiIconData();\n    final icon = iconData.isNotEmpty\n        ? EmojiIconWidget(\n            emoji: widget.view.icon.toEmojiIconData(),\n            emojiSize: Platform.isAndroid ? 16.0 : 18.0,\n          )\n        : Opacity(\n            opacity: 0.7,\n            child: widget.view.defaultIcon(size: const Size.square(18)),\n          );\n    return SizedBox(\n      width: 18.0,\n      child: icon,\n    );\n  }\n\n  // > button or · button\n  // show > if the view is expandable.\n  // show · if the view can't contain child views.\n  Widget _buildLeftIcon() {\n    const rightPadding = 6.0;\n    if (context.read<ViewBloc>().state.view.childViews.isEmpty) {\n      return HSpace(widget.leftPadding + rightPadding);\n    }\n\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      child: Padding(\n        padding:\n            const EdgeInsets.only(right: rightPadding, top: 6.0, bottom: 6.0),\n        child: FlowySvg(\n          widget.isExpanded ? FlowySvgs.m_expand_s : FlowySvgs.m_collapse_s,\n          blendMode: null,\n        ),\n      ),\n      onTap: () {\n        context\n            .read<ViewBloc>()\n            .add(ViewEvent.setIsExpanded(!widget.isExpanded));\n      },\n    );\n  }\n}\n\n// 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.\nbool isReferencedDatabaseView(ViewPB view, ViewPB? parentView) {\n  if (parentView == null) {\n    return false;\n  }\n  return view.layout.isDatabaseView && parentView.layout.isDatabaseView;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item_add_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileViewAddButton extends StatelessWidget {\n  const MobileViewAddButton({\n    super.key,\n    required this.onPressed,\n  });\n\n  final VoidCallback onPressed;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyIconButton(\n      width: HomeSpaceViewSizes.mViewButtonDimension,\n      height: HomeSpaceViewSizes.mViewButtonDimension,\n      icon: const FlowySvg(\n        FlowySvgs.m_space_add_s,\n      ),\n      onPressed: onPressed,\n    );\n  }\n}\n\nclass MobileViewMoreButton extends StatelessWidget {\n  const MobileViewMoreButton({\n    super.key,\n    required this.onPressed,\n  });\n\n  final VoidCallback onPressed;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyIconButton(\n      width: HomeSpaceViewSizes.mViewButtonDimension,\n      height: HomeSpaceViewSizes.mViewButtonDimension,\n      icon: const FlowySvg(\n        FlowySvgs.m_space_more_s,\n      ),\n      onPressed: onPressed,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/presentation.dart",
    "content": "export 'editor/mobile_editor_screen.dart';\nexport 'home/home.dart';\nexport 'mobile_bottom_navigation_bar.dart';\nexport 'root_placeholder_page.dart';\nexport 'setting/setting.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/root_placeholder_page.dart",
    "content": "import 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\n/// Widget for the root/initial pages in the bottom navigation bar.\nclass RootPlaceholderScreen extends StatelessWidget {\n  /// Creates a RootScreen\n  const RootPlaceholderScreen({\n    required this.label,\n    required this.detailsPath,\n    this.secondDetailsPath,\n    super.key,\n  });\n\n  /// The label\n  final String label;\n\n  /// The path to the detail page\n  final String detailsPath;\n\n  /// The path to another detail page\n  final String? secondDetailsPath;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        centerTitle: true,\n        title: FlowyText.medium(label),\n      ),\n      body: const SizedBox.shrink(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_ask_ai_entrance.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';\nimport 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'mobile_search_summary_cell.dart';\n\nclass MobileSearchAskAiEntrance extends StatelessWidget {\n  const MobileSearchAskAiEntrance({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final bloc = context.read<CommandPaletteBloc?>(), state = bloc?.state;\n    if (bloc == null || state == null) return _AskAIFor();\n\n    final generatingAIOverview = state.generatingAIOverview;\n    if (generatingAIOverview) return _AISearching();\n\n    final hasMockSummary = _mockSummary?.isNotEmpty ?? false,\n        hasSummaries = state.resultSummaries.isNotEmpty;\n    if (hasMockSummary || hasSummaries) return _AIOverview();\n\n    return _AskAIFor();\n  }\n}\n\nclass _AskAIFor extends StatelessWidget {\n  const _AskAIFor();\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context),\n        spaceM = theme.spacing.m,\n        spaceL = theme.spacing.l;\n    return GestureDetector(\n      onTap: () {\n        context\n            .read<CommandPaletteBloc?>()\n            ?.add(CommandPaletteEvent.goingToAskAI());\n        mobileCreateNewAIChatNotifier.value =\n            mobileCreateNewAIChatNotifier.value + 1;\n      },\n      behavior: HitTestBehavior.opaque,\n      child: Container(\n        margin: EdgeInsets.only(top: spaceM),\n        padding: EdgeInsets.all(spaceL),\n        child: Row(\n          children: [\n            SizedBox.square(\n              dimension: 24,\n              child: Center(\n                child: FlowySvg(\n                  FlowySvgs.m_home_ai_chat_icon_m,\n                  size: Size.square(20),\n                  blendMode: null,\n                ),\n              ),\n            ),\n            HSpace(8),\n            buildText(context),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget buildText(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final bloc = context.read<CommandPaletteBloc?>();\n    final queryText = bloc?.state.query ?? '';\n    if (queryText.isEmpty) {\n      return Text(\n        LocaleKeys.search_askAIAnything.tr(),\n        style: theme.textStyle.heading4\n            .standard(color: theme.textColorScheme.primary),\n      );\n    }\n    return Flexible(\n      child: RichText(\n        maxLines: 1,\n        overflow: TextOverflow.ellipsis,\n        text: TextSpan(\n          children: [\n            TextSpan(\n              text: LocaleKeys.search_askAIFor.tr(),\n              style: theme.textStyle.heading4\n                  .standard(color: theme.textColorScheme.primary),\n            ),\n            TextSpan(\n              text: ' \"$queryText\"',\n              style: theme.textStyle.heading4\n                  .enhanced(color: theme.textColorScheme.primary),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _AISearching extends StatelessWidget {\n  const _AISearching();\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context),\n        spaceM = theme.spacing.m,\n        spaceL = theme.spacing.l;\n    return Container(\n      margin: EdgeInsets.only(top: spaceM),\n      padding: EdgeInsets.all(spaceL),\n      child: SizedBox(\n        height: 24,\n        child: Row(\n          children: [\n            SizedBox.square(\n              dimension: 24,\n              child: Center(\n                child: FlowySvg(\n                  FlowySvgs.m_home_ai_chat_icon_m,\n                  size: Size.square(20),\n                  blendMode: null,\n                ),\n              ),\n            ),\n            HSpace(8),\n            Text(\n              LocaleKeys.search_searching.tr(),\n              style: theme.textStyle.heading4\n                  .standard(color: theme.textColorScheme.secondary),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _AIOverview extends StatelessWidget {\n  const _AIOverview();\n\n  @override\n  Widget build(BuildContext context) {\n    final bloc = context.read<CommandPaletteBloc?>(), state = bloc?.state;\n    final summaries = _mockSummary ?? state?.resultSummaries ?? [];\n    if (summaries.isEmpty) {\n      return const SizedBox.shrink();\n    }\n    final theme = AppFlowyTheme.of(context),\n        spaceM = theme.spacing.m,\n        spaceL = theme.spacing.l;\n    return Container(\n      margin: EdgeInsets.only(top: spaceM),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          VSpace(spaceM),\n          buildHeader(context),\n          VSpace(spaceL),\n          LayoutBuilder(\n            builder: (context, constrains) {\n              final summary = summaries.first;\n              return MobileSearchSummaryCell(\n                key: ValueKey(summary.content.trim()),\n                summary: summary,\n                maxWidth: constrains.maxWidth,\n                theme: AppFlowyTheme.of(context),\n                textStyle: theme.textStyle.heading4\n                    .standard(color: theme.textColorScheme.primary)\n                    .copyWith(height: 22 / 16),\n              );\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget buildHeader(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Row(\n      children: [\n        FlowySvg(\n          FlowySvgs.ai_searching_icon_m,\n          size: Size.square(20),\n          blendMode: null,\n        ),\n        HSpace(8),\n        Text(\n          LocaleKeys.commandPalette_aiOverview.tr(),\n          style: theme.textStyle.heading4\n              .enhanced(color: theme.textColorScheme.primary),\n        ),\n      ],\n    );\n  }\n}\n\nList<SearchSummaryPB>? _mockSummary;\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_cell.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/search_icon.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'mobile_view_ancestors.dart';\n\nclass MobileSearchResultCell extends StatelessWidget {\n  const MobileSearchResultCell({\n    super.key,\n    required this.item,\n    this.query,\n    this.view,\n  });\n  final SearchResultItem item;\n  final ViewPB? view;\n  final String? query;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final commandPaletteState = context.read<CommandPaletteBloc>().state;\n    final displayName = item.displayName.isEmpty\n        ? LocaleKeys.menuAppHeader_defaultNewPageName.tr()\n        : item.displayName;\n    final titleStyle = theme.textStyle.heading4\n        .standard(color: theme.textColorScheme.primary)\n        .copyWith(height: 24 / 16);\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),\n      child: Row(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          SizedBox.square(\n            dimension: 24,\n            child: Center(\n              child: (view?.toSearchResultItem().icon ?? item.icon)\n                  .buildIcon(context),\n            ),\n          ),\n          HSpace(12),\n          Flexible(\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                RichText(\n                  maxLines: 1,\n                  overflow: TextOverflow.ellipsis,\n                  text: buildHighLightSpan(\n                    content: displayName,\n                    normal: titleStyle,\n                    highlight: titleStyle.copyWith(\n                      backgroundColor: theme.fillColorScheme.themeSelect,\n                    ),\n                  ),\n                ),\n                buildPath(commandPaletteState, theme),\n                ...buildSummary(theme),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget buildPath(CommandPaletteState state, AppFlowyThemeData theme) {\n    return BlocProvider(\n      key: ValueKey(item.id),\n      create: (context) => ViewAncestorBloc(item.id),\n      child: BlocBuilder<ViewAncestorBloc, ViewAncestorState>(\n        builder: (context, state) {\n          final ancestors = state.ancestor.ancestors;\n          if(ancestors.isEmpty) return const SizedBox.shrink();\n          List<String> displayPath = ancestors.map((e) => e.name).toList();\n          if (ancestors.length > 2) {\n            displayPath = [ancestors.first.name, '...', ancestors.last.name];\n          }\n          return Text(\n            displayPath.join(' / '),\n            maxLines: 1,\n            overflow: TextOverflow.ellipsis,\n            style: theme.textStyle.body\n                .standard(color: theme.textColorScheme.tertiary),\n          );\n        },\n      ),\n    );\n  }\n\n  List<Widget> buildSummary(AppFlowyThemeData theme) {\n    if (item.content.isEmpty) return [];\n    return [\n      VSpace(theme.spacing.m),\n      RichText(\n        maxLines: 3,\n        overflow: TextOverflow.ellipsis,\n        text: buildHighLightSpan(\n          content: item.content,\n          normal: theme.textStyle.body\n              .standard(color: theme.textColorScheme.secondary),\n          highlight: theme.textStyle.body\n              .standard(color: theme.textColorScheme.primary)\n              .copyWith(\n                backgroundColor: theme.fillColorScheme.themeSelect,\n              ),\n        ),\n      ),\n    ];\n  }\n\n  TextSpan buildHighLightSpan({\n    required String content,\n    required TextStyle normal,\n    required TextStyle highlight,\n  }) {\n    final queryText = (query ?? '').trim();\n    if (queryText.isEmpty) {\n      return TextSpan(text: content, style: normal);\n    }\n    final contents = content.splitIncludeSeparator(queryText);\n    return TextSpan(\n      children: List.generate(contents.length, (index) {\n        final content = contents[index];\n        final isHighlight = content.toLowerCase() == queryText.toLowerCase();\n        return TextSpan(\n          text: content,\n          style: isHighlight ? highlight : normal,\n        );\n      }),\n    );\n  }\n}\n\nextension ViewPBToSearchResultItem on ViewPB {\n  SearchResultItem toSearchResultItem() {\n    final hasIcon = icon.value.isNotEmpty;\n    return SearchResultItem(\n      id: id,\n      displayName: nameOrDefault,\n      icon: ResultIconPB(\n        ty: hasIcon\n            ? ResultIconTypePB.valueOf(icon.ty.value)\n            : ResultIconTypePB.Icon,\n        value: hasIcon ? icon.value : '${layout.value}',\n      ),\n      content: '',\n    );\n  }\n}\n\nextension StringSplitExtension on String {\n  List<String> splitIncludeSeparator(String separator) {\n    final splits =\n        split(RegExp(RegExp.escape(separator), caseSensitive: false));\n    final List<String> contents = [];\n    int charIndex = 0;\n    final seperatorLength = separator.length;\n    for (int i = 0; i < splits.length; i++) {\n      contents.add(splits[i]);\n      charIndex += splits[i].length;\n      if (i != splits.length - 1) {\n        contents.add(substring(charIndex, charIndex + seperatorLength));\n        charIndex += seperatorLength;\n      }\n    }\n    return contents;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_icon.dart",
    "content": "import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nextension MobileSearchIconItemExtension on ResultIconPB {\n  Widget? buildIcon(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    if (ty == ResultIconTypePB.Emoji) {\n      return SizedBox(\n        width: 20,\n        child: getIcon(size: 20) ?? SizedBox.shrink(),\n      );\n    } else {\n      return getIcon(\n            size: 20,\n            iconColor: theme.iconColorScheme.secondary,\n          ) ??\n          SizedBox.shrink();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_page.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\n\nimport 'mobile_search_ask_ai_entrance.dart';\nimport 'mobile_search_result.dart';\nimport 'mobile_search_textfield.dart';\n\nclass MobileSearchScreen extends StatelessWidget {\n  const MobileSearchScreen({\n    super.key,\n  });\n\n  static const routeName = '/search';\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: Future.wait([\n        FolderEventGetCurrentWorkspaceSetting().send(),\n        getIt<AuthService>().getUser(),\n      ]),\n      builder: (context, snapshots) {\n        if (!snapshots.hasData) {\n          return const Center(child: CircularProgressIndicator.adaptive());\n        }\n\n        final latest = snapshots.data?[0].fold(\n          (latest) {\n            return latest as WorkspaceLatestPB?;\n          },\n          (error) => null,\n        );\n        final userProfile = snapshots.data?[1].fold(\n          (userProfilePB) {\n            return userProfilePB as UserProfilePB?;\n          },\n          (error) => null,\n        );\n\n        // In the unlikely case either of the above is null, eg.\n        // when a workspace is already open this can happen.\n        if (latest == null || userProfile == null) {\n          return const WorkspaceFailedScreen();\n        }\n\n        return Provider.value(\n          value: userProfile,\n          child: MobileSearchPage(\n            userProfile: userProfile,\n            workspaceLatestPB: latest,\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass MobileSearchPage extends StatefulWidget {\n  const MobileSearchPage({\n    super.key,\n    required this.userProfile,\n    required this.workspaceLatestPB,\n  });\n\n  final UserProfilePB userProfile;\n  final WorkspaceLatestPB workspaceLatestPB;\n\n  @override\n  State<MobileSearchPage> createState() => _MobileSearchPageState();\n}\n\nclass _MobileSearchPageState extends State<MobileSearchPage> {\n  bool get enableShowAISearch =>\n      widget.userProfile.workspaceType == WorkspaceTypePB.ServerW;\n\n  final focusNode = FocusNode();\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<CommandPaletteBloc, CommandPaletteState>(\n      builder: (context, state) {\n        return SafeArea(\n          child: Scaffold(\n            body: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                MobileSearchTextfield(\n                  focusNode: focusNode,\n                  hintText: enableShowAISearch\n                      ? LocaleKeys.search_searchOrAskAI.tr()\n                      : LocaleKeys.search_label.tr(),\n                  query: state.query ?? '',\n                  onChanged: (value) => context.read<CommandPaletteBloc>().add(\n                        CommandPaletteEvent.searchChanged(search: value),\n                      ),\n                ),\n                Flexible(\n                  child: NotificationListener(\n                    onNotification: (t) {\n                      if (t is ScrollUpdateNotification) {\n                        if (focusNode.hasFocus) {\n                          focusNode.unfocus();\n                        }\n                      }\n                      return true;\n                    },\n                    child: SingleChildScrollView(\n                      child: Padding(\n                        padding: const EdgeInsets.symmetric(horizontal: 16),\n                        child: Column(\n                          children: [\n                            if (enableShowAISearch) MobileSearchAskAiEntrance(),\n                            MobileSearchResult(),\n                          ],\n                        ),\n                      ),\n                    ),\n                  ),\n                ),\n              ],\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_reference_bottom_sheet.dart",
    "content": "import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/startup/tasks/app_widget.dart';\nimport 'package:appflowy/workspace/application/command_palette/search_result_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';\nimport 'package:flutter/material.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart' hide TextDirection;\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\n\nclass SearchSourceReferenceBottomSheet extends StatelessWidget {\n  const SearchSourceReferenceBottomSheet(this.sources, {super.key});\n\n  final List<SearchSourcePB> sources;\n  @override\n  Widget build(BuildContext context) {\n    return PageReferenceList(\n      sources: sources,\n      onTap: (id) async {\n        final theme = AppFlowyTheme.of(context);\n        final view = (await ViewBackendService.getView(id)).toNullable();\n        if (view == null) {\n          showToastNotification(\n            message: LocaleKeys.search_somethingWentWrong.tr(),\n            type: ToastificationType.error,\n          );\n          return;\n        }\n        await showMobileBottomSheet(\n          AppGlobals.rootNavKey.currentContext ?? context,\n          showDragHandle: true,\n          showDivider: false,\n          enableDraggableScrollable: true,\n          maxChildSize: 0.95,\n          minChildSize: 0.95,\n          initialChildSize: 0.95,\n          backgroundColor: theme.surfaceColorScheme.primary,\n          dragHandleBuilder: (_) => const _DragHandler(),\n          builder: (_) => SizedBox(\n            height: MediaQuery.of(context).size.height,\n            child: MobileViewPage(\n              id: id,\n              viewLayout: view.layout,\n              title: view.nameOrDefault,\n              tabs: PickerTabType.values,\n              bodyPaddingTop:\n                  AppBarTheme.of(context).toolbarHeight ?? kToolbarHeight,\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass PageReferenceList extends StatelessWidget {\n  const PageReferenceList({\n    super.key,\n    required this.sources,\n    required this.onTap,\n  });\n\n  final List<SearchSourcePB> sources;\n  final ValueChanged<String> onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Padding(\n          padding: const EdgeInsets.only(left: 16, top: 8),\n          child: Text(\n            LocaleKeys.commandPalette_aiOverviewSource.tr(),\n            style: theme.textStyle.body.enhanced(\n              color: theme.textColorScheme.secondary,\n            ),\n          ),\n        ),\n        const VSpace(6),\n        ListView.builder(\n          shrinkWrap: true,\n          padding: EdgeInsets.fromLTRB(16, 0, 16, 8),\n          physics: const NeverScrollableScrollPhysics(),\n          itemBuilder: (context, index) {\n            final source = sources[index];\n            final displayName = source.displayName.isEmpty\n                ? LocaleKeys.menuAppHeader_defaultNewPageName.tr()\n                : source.displayName;\n            final sapceM = theme.spacing.m, spaceL = theme.spacing.l;\n            return FlowyButton(\n              onTap: () => onTap.call(source.id),\n              margin: EdgeInsets.zero,\n              text: Column(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  if (index != 0) AFDivider(),\n                  Padding(\n                    padding: EdgeInsets.symmetric(\n                      vertical: spaceL,\n                      horizontal: sapceM,\n                    ),\n                    child: Row(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      children: [\n                        SizedBox.square(\n                          dimension: 20,\n                          child: Center(child: buildIcon(source.icon, theme)),\n                        ),\n                        HSpace(12),\n                        Flexible(\n                          child: Column(\n                            crossAxisAlignment: CrossAxisAlignment.start,\n                            mainAxisSize: MainAxisSize.min,\n                            children: [\n                              Text(\n                                displayName,\n                                maxLines: 1,\n                                overflow: TextOverflow.ellipsis,\n                                style: theme.textStyle.heading4.standard(\n                                  color: theme.textColorScheme.primary,\n                                ),\n                              ),\n                            ],\n                          ),\n                        ),\n                      ],\n                    ),\n                  ),\n                ],\n              ),\n            );\n          },\n          itemCount: sources.length,\n        ),\n      ],\n    );\n  }\n\n  Widget buildIcon(ResultIconPB icon, AppFlowyThemeData theme) {\n    if (icon.ty == ResultIconTypePB.Emoji) {\n      return icon.getIcon(size: 16, lineHeight: 20 / 16) ?? SizedBox.shrink();\n    } else {\n      return icon.getIcon(size: 20, iconColor: theme.iconColorScheme.primary) ??\n          SizedBox.shrink();\n    }\n  }\n}\n\nclass _DragHandler extends StatelessWidget {\n  const _DragHandler();\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: 4,\n      width: 40,\n      margin: const EdgeInsets.symmetric(vertical: 12),\n      decoration: BoxDecoration(\n        color: Colors.grey.shade400,\n        borderRadius: BorderRadius.circular(2),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_result.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';\nimport 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'mobile_search_cell.dart';\n\nclass MobileSearchResult extends StatelessWidget {\n  const MobileSearchResult({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final state = context.read<CommandPaletteBloc>().state;\n    final query = (state.query ?? '').trim();\n    if (query.isEmpty) {\n      return const MobileSearchRecentList();\n    }\n    return MobileSearchResultList();\n  }\n}\n\nclass MobileSearchRecentList extends StatelessWidget {\n  const MobileSearchRecentList({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final commandPaletteState = context.read<CommandPaletteBloc>().state;\n    final theme = AppFlowyTheme.of(context);\n    final trashIdSet = commandPaletteState.trash.map((e) => e.id).toSet();\n    return BlocProvider(\n      create: (context) =>\n          RecentViewsBloc()..add(const RecentViewsEvent.initial()),\n      child: BlocBuilder<RecentViewsBloc, RecentViewsState>(\n        builder: (context, state) {\n          final List<ViewPB> recentViews = state.views\n              .map((e) => e.item)\n              .where((e) => !trashIdSet.contains(e.id))\n              .toList();\n          return Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              const VSpace(16),\n              Text(\n                LocaleKeys.sideBar_recent.tr(),\n                style: theme.textStyle.heading4\n                    .enhanced(color: theme.textColorScheme.secondary)\n                    .copyWith(\n                      letterSpacing: 0.2,\n                      height: 24 / 16,\n                    ),\n              ),\n              const VSpace(4),\n              Column(\n                mainAxisSize: MainAxisSize.min,\n                children: List.generate(recentViews.length, (index) {\n                  final view = recentViews[index];\n                  return GestureDetector(\n                    behavior: HitTestBehavior.opaque,\n                    onTap: () => _goToView(context, view),\n                    child: MobileSearchResultCell(\n                      item: view.toSearchResultItem(),\n                    ),\n                  );\n                }),\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass MobileSearchResultList extends StatelessWidget {\n  const MobileSearchResultList({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final state = context.read<CommandPaletteBloc>().state,\n        theme = AppFlowyTheme.of(context);\n    final isSearching = state.searching || state.generatingAIOverview,\n        cachedViews = state.cachedViews;\n    List<SearchResultItem> displayItems =\n        state.combinedResponseItems.values.toList();\n    if (cachedViews.isNotEmpty) {\n      displayItems =\n          displayItems.where((item) => cachedViews[item.id] != null).toList();\n    }\n    final hasData = displayItems.isNotEmpty;\n\n    if (isSearching && !hasData) {\n      return SizedBox(\n        height: 400,\n        child: Center(child: CircularProgressIndicator.adaptive()),\n      );\n    } else if (!hasData) {\n      return _NoResult();\n    }\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        const VSpace(16),\n        Text(\n          LocaleKeys.commandPalette_bestMatches.tr(),\n          style: theme.textStyle.heading4\n              .enhanced(color: theme.textColorScheme.secondary)\n              .copyWith(\n                letterSpacing: 0.2,\n                height: 24 / 16,\n              ),\n        ),\n        const VSpace(4),\n        ListView.separated(\n          shrinkWrap: true,\n          physics: const NeverScrollableScrollPhysics(),\n          itemBuilder: (context, index) {\n            final item = displayItems[index];\n            final view = state.cachedViews[item.id];\n            return GestureDetector(\n              behavior: HitTestBehavior.opaque,\n              onTap: () async {\n                if (view != null && context.mounted) {\n                  await _goToView(context, view);\n                } else {\n                  showToastNotification(\n                    message: LocaleKeys.search_pageNotExist.tr(),\n                    type: ToastificationType.error,\n                  );\n                  Log.error(\n                    'tapping search result, view not found: ${item.id}',\n                  );\n                }\n              },\n              child: MobileSearchResultCell(\n                item: item,\n                view: view,\n                query: state.query,\n              ),\n            );\n          },\n          separatorBuilder: (context, index) => AFDivider(),\n          itemCount: displayItems.length,\n        ),\n      ],\n    );\n  }\n}\n\nclass _NoResult extends StatelessWidget {\n  const _NoResult();\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final textColor = theme.textColorScheme.secondary;\n    return Align(\n      alignment: Alignment.topCenter,\n      child: Column(\n        children: [\n          const VSpace(48),\n          FlowySvg(\n            FlowySvgs.m_home_search_icon_m,\n            color: theme.iconColorScheme.secondary,\n            size: Size.square(24),\n          ),\n          const VSpace(12),\n          Text(\n            LocaleKeys.search_noResultForSearching.tr(),\n            style: theme.textStyle.heading4.enhanced(color: textColor),\n            maxLines: 1,\n            overflow: TextOverflow.ellipsis,\n          ),\n          Text(\n            textAlign: TextAlign.center,\n            LocaleKeys.search_noResultForSearchingHint.tr(),\n            style: theme.textStyle.body.standard(color: textColor),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nFuture<void> _goToView(BuildContext context, ViewPB view) async {\n  await context.pushView(\n    view,\n    tabs: [\n      PickerTabType.emoji,\n      PickerTabType.icon,\n      PickerTabType.custom,\n    ].map((e) => e.name).toList(),\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_summary_cell.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';\nimport 'package:appflowy/mobile/presentation/search/mobile_search_cell.dart';\nimport 'package:appflowy/startup/tasks/app_widget.dart';\nimport 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart' hide TextDirection;\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nimport 'mobile_search_reference_bottom_sheet.dart';\n\nclass MobileSearchSummaryCell extends StatefulWidget {\n  const MobileSearchSummaryCell({\n    super.key,\n    required this.summary,\n    required this.maxWidth,\n    required this.theme,\n    required this.textStyle,\n  });\n\n  final SearchSummaryPB summary;\n  final double maxWidth;\n  final AppFlowyThemeData theme;\n  final TextStyle textStyle;\n\n  @override\n  State<MobileSearchSummaryCell> createState() =>\n      _MobileSearchSummaryCellState();\n}\n\nclass _MobileSearchSummaryCellState extends State<MobileSearchSummaryCell> {\n  late TextPainter _painter;\n  late _TextInfo _textInfo = _TextInfo.normal(summary.content);\n  bool tappedShowMore = false;\n  final maxLines = 4;\n\n  SearchSummaryPB get summary => widget.summary;\n  double get maxWidth => widget.maxWidth;\n  AppFlowyThemeData get theme => widget.theme;\n\n  TextStyle get textStyle => widget.textStyle;\n\n  TextStyle get showMoreStyle =>\n      theme.textStyle.heading4.standard(color: theme.textColorScheme.secondary);\n\n  @override\n  void initState() {\n    super.initState();\n    refreshTextPainter();\n  }\n\n  @override\n  void didUpdateWidget(MobileSearchSummaryCell oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    if (oldWidget.maxWidth != maxWidth) {\n      refreshTextPainter();\n    }\n  }\n\n  @override\n  void dispose() {\n    _painter.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final showReference = summary.sources.isNotEmpty;\n    final query = summary.highlights.isEmpty\n        ? context.read<CommandPaletteBloc?>()?.state.query\n        : summary.highlights;\n    return _textInfo.build(\n      context: context,\n      normal: textStyle,\n      more: showMoreStyle,\n      summury: summary,\n      query: query ?? '',\n      showMore: () {\n        setState(() {\n          tappedShowMore = true;\n          _textInfo = _TextInfo.normal(summary.content);\n        });\n      },\n      normalWidgetSpan: showReference\n          ? WidgetSpan(\n              alignment: PlaceholderAlignment.middle,\n              child: GestureDetector(\n                behavior: HitTestBehavior.opaque,\n                onTap: () => showPageReferences(context),\n                child: Padding(\n                  padding: const EdgeInsets.fromLTRB(4, 4, 15, 4),\n                  child: Container(\n                    width: 21,\n                    height: 15,\n                    decoration: BoxDecoration(\n                      color: Theme.of(context).colorScheme.secondary,\n                      borderRadius: BorderRadius.circular(6),\n                    ),\n                    child: FlowySvg(\n                      FlowySvgs.toolbar_link_m,\n                      color: theme.iconColorScheme.primary,\n                      size: Size.square(10),\n                    ),\n                  ),\n                ),\n              ),\n            )\n          : null,\n      overflowFadeCover: buildFadeCover(),\n    );\n  }\n\n  Widget buildFadeCover() {\n    final fillColor = Theme.of(context).scaffoldBackgroundColor;\n    return Container(\n      width: maxWidth,\n      height: 40,\n      decoration: BoxDecoration(\n        gradient: LinearGradient(\n          colors: [\n            fillColor.withValues(alpha: 0),\n            fillColor.withValues(alpha: 0.6 * 255),\n            fillColor,\n          ],\n          begin: Alignment.topCenter,\n          end: Alignment.bottomCenter,\n        ),\n      ),\n    );\n  }\n\n  void refreshTextPainter() {\n    final content = summary.content;\n    if (!tappedShowMore) {\n      _painter = TextPainter(\n        text: TextSpan(text: content, style: textStyle),\n        textDirection: TextDirection.ltr,\n        maxLines: maxLines,\n      );\n      _painter.layout(maxWidth: maxWidth);\n      if (_painter.didExceedMaxLines) {\n        final lines = _painter.computeLineMetrics();\n        final lastLine = lines.last;\n        final offset = Offset(\n          lastLine.left + lastLine.width,\n          lines.map((e) => e.height).reduce((a, b) => a + b),\n        );\n        final range = _painter.getPositionForOffset(offset);\n        final text = content.substring(0, range.offset);\n        _textInfo = _TextInfo.overflow(text);\n      } else {\n        _textInfo = _TextInfo.normal(content);\n      }\n    }\n  }\n\n  void showPageReferences(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    showMobileBottomSheet(\n      AppGlobals.rootNavKey.currentContext ?? context,\n      showDragHandle: true,\n      showDivider: false,\n      enableDraggableScrollable: true,\n      backgroundColor: theme.surfaceColorScheme.primary,\n      builder: (_) => SearchSourceReferenceBottomSheet(summary.sources),\n    );\n  }\n}\n\nclass _TextInfo {\n  _TextInfo({required this.text, required this.isOverflow});\n\n  _TextInfo.normal(this.text) : isOverflow = false;\n\n  _TextInfo.overflow(this.text) : isOverflow = true;\n\n  final String text;\n  final bool isOverflow;\n\n  Widget build({\n    required BuildContext context,\n    required TextStyle normal,\n    required TextStyle more,\n    required VoidCallback showMore,\n    required String query,\n    required SearchSummaryPB summury,\n    WidgetSpan? normalWidgetSpan,\n    Widget? overflowFadeCover,\n  }) {\n    final theme = AppFlowyTheme.of(context);\n    if (isOverflow) {\n      return GestureDetector(\n        behavior: HitTestBehavior.opaque,\n        onTap: showMore,\n        child: IgnorePointer(\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Stack(\n                children: [\n                  SelectionArea(\n                    child: Text.rich(\n                      _buildHighLightSpan(\n                        content: text,\n                        normal: normal,\n                        query: query,\n                        highlight: normal.copyWith(\n                          backgroundColor: theme.fillColorScheme.themeSelect,\n                        ),\n                      ),\n                    ),\n                  ),\n                  if (overflowFadeCover != null)\n                    Positioned(\n                      bottom: 0,\n                      left: 0,\n                      child: overflowFadeCover,\n                    ),\n                ],\n              ),\n              SizedBox(\n                height: 34,\n                child: Row(\n                  children: [\n                    Padding(\n                      padding: const EdgeInsets.symmetric(vertical: 7),\n                      child: FlowySvg(\n                        FlowySvgs.arrow_down_s,\n                        size: Size.square(20),\n                        color: theme.iconColorScheme.secondary,\n                      ),\n                    ),\n                    HSpace(8),\n                    Text(LocaleKeys.search_showMore.tr(), style: more),\n                  ],\n                ),\n              ),\n              VSpace(16),\n            ],\n          ),\n        ),\n      );\n    } else {\n      return Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          SelectionArea(\n            child: Text.rich(\n              TextSpan(\n                children: [\n                  _buildHighLightSpan(\n                    content: text,\n                    normal: normal,\n                    query: query,\n                    highlight: normal.copyWith(\n                      backgroundColor: theme.fillColorScheme.themeSelect,\n                    ),\n                  ),\n                  if (normalWidgetSpan != null) normalWidgetSpan,\n                ],\n              ),\n            ),\n          ),\n          VSpace(16),\n          SizedBox(\n            width: 156,\n            height: 42,\n            child: AFOutlinedButton.normal(\n              borderRadius: 21,\n              padding: EdgeInsets.zero,\n              builder: (context, hovering, disabled) {\n                return Center(\n                  child: SizedBox(\n                    height: 22,\n                    child: Row(\n                      mainAxisSize: MainAxisSize.min,\n                      children: [\n                        FlowySvg(\n                          FlowySvgs.chat_ai_page_s,\n                          size: Size.square(20),\n                          color: theme.iconColorScheme.secondary,\n                        ),\n                        HSpace(8),\n                        Text(\n                          LocaleKeys.commandPalette_aiAskFollowUp.tr(),\n                          style: theme.textStyle.heading4.standard(\n                            color: theme.textColorScheme.primary,\n                          ),\n                        ),\n                      ],\n                    ),\n                  ),\n                );\n              },\n              onTap: () {\n                context.read<CommandPaletteBloc?>()?.add(\n                      CommandPaletteEvent.goingToAskAI(\n                        sources: summury.sources,\n                      ),\n                    );\n                mobileCreateNewAIChatNotifier.value =\n                    mobileCreateNewAIChatNotifier.value + 1;\n              },\n            ),\n          ),\n          VSpace(16),\n        ],\n      );\n    }\n  }\n\n  TextSpan _buildHighLightSpan({\n    required String content,\n    required TextStyle normal,\n    required TextStyle highlight,\n    String? query,\n  }) {\n    final queryText = (query ?? '').trim();\n    if (queryText.isEmpty) {\n      return TextSpan(text: content, style: normal);\n    }\n    final contents = content.splitIncludeSeparator(queryText);\n    return TextSpan(\n      children: List.generate(contents.length, (index) {\n        final content = contents[index];\n        final isHighlight = content.toLowerCase() == queryText.toLowerCase();\n        return TextSpan(\n          text: content,\n          style: isHighlight ? highlight : normal,\n        );\n      }),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_textfield.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';\nimport 'package:appflowy/shared/popup_menu/appflowy_popup_menu.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/startup/tasks/af_navigator_observer.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nimport 'mobile_search_page.dart';\n\nclass MobileSearchTextfield extends StatefulWidget {\n  const MobileSearchTextfield({\n    super.key,\n    this.onChanged,\n    required this.hintText,\n    required this.query,\n    required this.focusNode,\n  });\n\n  final String hintText;\n  final String query;\n  final ValueChanged<String>? onChanged;\n  final FocusNode focusNode;\n\n  @override\n  State<MobileSearchTextfield> createState() => _MobileSearchTextfieldState();\n}\n\nclass _MobileSearchTextfieldState extends State<MobileSearchTextfield>\n    with RouteAware {\n  late final TextEditingController controller;\n\n  FocusNode get focusNode => widget.focusNode;\n  late String lastPage = bottomNavigationBarItemType.value ?? '';\n  String lastText = '';\n\n  @override\n  void initState() {\n    super.initState();\n    controller = TextEditingController(text: widget.query);\n    controller.addListener(() {\n      if (!mounted) return;\n      if (lastText != controller.text) {\n        widget.onChanged?.call(controller.text);\n        lastText = controller.text;\n      }\n    });\n    bottomNavigationBarItemType.addListener(onBackOrLeave);\n    makeSureHasFocus();\n    getIt<AFNavigatorObserver>().addListener(onRoute);\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    bottomNavigationBarItemType.removeListener(onBackOrLeave);\n    getIt<AFNavigatorObserver>().removeListener(onRoute);\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Container(\n      height: 42,\n      margin: EdgeInsets.symmetric(vertical: 8),\n      padding: EdgeInsets.only(left: 4, right: 16),\n      child: ValueListenableBuilder(\n        valueListenable: controller,\n        builder: (context, _, __) {\n          return Row(\n            children: [\n              GestureDetector(\n                onTap: () {\n                  if (lastPage.isEmpty) return;\n                  // close the popup menu\n                  closePopupMenu();\n                  try {\n                    BottomNavigationBarItemType label =\n                        BottomNavigationBarItemType.values.byName(lastPage);\n                    if (label == BottomNavigationBarItemType.search) {\n                      label = BottomNavigationBarItemType.home;\n                    }\n                    if (label == BottomNavigationBarItemType.notification) {\n                      getIt<ReminderBloc>().add(const ReminderEvent.refresh());\n                    }\n                    bottomNavigationBarItemType.value = label.label;\n                    final routeName = label.routeName;\n                    if (routeName != null) GoRouter.of(context).go(routeName);\n                  } on ArgumentError {\n                    Log.error(\n                      'lastPage: [$lastPage] cannot be converted to BottomNavigationBarItemType',\n                    );\n                  }\n                },\n                child: SizedBox.square(\n                  dimension: 40,\n                  child: Center(\n                    child: FlowySvg(\n                      FlowySvgs.search_page_arrow_left_m,\n                      size: Size.square(20),\n                      color: theme.iconColorScheme.primary,\n                    ),\n                  ),\n                ),\n              ),\n              Expanded(\n                child: TextFormField(\n                  autovalidateMode: AutovalidateMode.onUserInteraction,\n                  focusNode: focusNode,\n                  textAlign: TextAlign.left,\n                  controller: controller,\n                  style: theme.textStyle.heading4.standard(\n                    color: theme.textColorScheme.primary,\n                  ),\n                  decoration: buildInputDecoration(context),\n                ),\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  InputDecoration buildInputDecoration(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final showCancelIcon = controller.text.isNotEmpty;\n    final border = OutlineInputBorder(\n      borderRadius: BorderRadius.all(Radius.circular(10.0)),\n      borderSide: BorderSide(color: theme.borderColorScheme.primary),\n    );\n    final enableBorder = border.copyWith(\n      borderSide: BorderSide(color: theme.borderColorScheme.themeThick),\n    );\n    final hintStyle = theme.textStyle.heading4.standard(\n      color: theme.textColorScheme.tertiary,\n    );\n    return InputDecoration(\n      hintText: widget.hintText,\n      hintStyle: hintStyle,\n      contentPadding: const EdgeInsets.fromLTRB(8, 10, 8, 10),\n      isDense: true,\n      border: border,\n      enabledBorder: border,\n      focusedBorder: enableBorder,\n      prefixIconConstraints: BoxConstraints.loose(Size(38, 40)),\n      prefixIcon: Padding(\n        padding: const EdgeInsets.fromLTRB(8, 10, 8, 10),\n        child: FlowySvg(\n          FlowySvgs.m_home_search_icon_m,\n          color: theme.iconColorScheme.secondary,\n          size: Size.square(20),\n        ),\n      ),\n      suffixIconConstraints:\n          showCancelIcon ? BoxConstraints.loose(Size(34, 40)) : null,\n      suffixIcon: showCancelIcon\n          ? GestureDetector(\n              onTap: () {\n                controller.clear();\n              },\n              child: Padding(\n                padding: const EdgeInsets.fromLTRB(4, 10, 8, 10),\n                child: FlowySvg(\n                  FlowySvgs.search_clear_m,\n                  color: theme.iconColorScheme.tertiary,\n                  size: const Size.square(20),\n                ),\n              ),\n            )\n          : null,\n    );\n  }\n\n  Future<void> makeSureHasFocus() async {\n    if (!mounted || focusNode.hasFocus) return;\n    focusNode.requestFocus();\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      makeSureHasFocus();\n    });\n  }\n\n  void onBackOrLeave() {\n    final label = bottomNavigationBarItemType.value;\n    if (label == BottomNavigationBarItemType.search.label) {\n      focusNode.requestFocus();\n    } else {\n      focusNode.unfocus();\n      controller.clear();\n      lastPage = label ?? '';\n    }\n  }\n\n  void onRoute(RouteInfo routeInfo) {\n    final oldName = routeInfo.oldRoute?.settings.name;\n    if (oldName != MobileSearchScreen.routeName) return;\n    if (routeInfo is PushRouterInfo) {\n      focusNode.unfocus();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_view_ancestors.dart",
    "content": "import 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport 'view_ancestor_cache.dart';\npart 'mobile_view_ancestors.freezed.dart';\n\nclass ViewAncestorBloc extends Bloc<ViewAncestorEvent, ViewAncestorState> {\n  ViewAncestorBloc(String viewId) : super(ViewAncestorState.initial(viewId)) {\n    _cache = getIt<ViewAncestorCache>();\n    _dispatch();\n  }\n\n  late final ViewAncestorCache _cache;\n\n  void _dispatch() {\n    on<ViewAncestorEvent>(\n      (event, emit) async {\n        await event.map(\n          initial: (e) async {\n            emit(state.copyWith(isLoading: true));\n\n            final ancester = await _cache.getAncestor(\n              state.viewId,\n              onRefresh: (ancestor) {\n                if (!emit.isDone) {\n                  emit(state.copyWith(ancestor: ancestor, isLoading: false));\n                }\n              },\n            );\n            if (ancester != null) {\n              emit(state.copyWith(ancestor: ancester, isLoading: false));\n            }\n          },\n        );\n      },\n    );\n    add(const ViewAncestorEvent.initial());\n  }\n}\n\n@freezed\nclass ViewAncestorEvent with _$ViewAncestorEvent {\n  const factory ViewAncestorEvent.initial() = Initial;\n}\n\n@freezed\nclass ViewAncestorState with _$ViewAncestorState {\n  const factory ViewAncestorState({\n    required ViewAncestor ancestor,\n    required String viewId,\n    @Default(true) bool isLoading,\n  }) = _ViewAncestorState;\n\n  factory ViewAncestorState.initial(String viewId) => ViewAncestorState(\n        viewId: viewId,\n        ancestor: ViewAncestor.empty(),\n      );\n}\n\nextension ViewAncestorTextExtension on ViewAncestorState {\n  Widget buildPath(BuildContext context, {TextStyle? style}) {\n    final theme = AppFlowyTheme.of(context);\n    final ancestors = ancestor.ancestors;\n    final textStyle = style ??\n        theme.textStyle.caption.standard(color: theme.textColorScheme.tertiary);\n    final textHeight = (textStyle.fontSize ?? 0.0) * (textStyle.height ?? 1.0);\n    if (isLoading) return VSpace(textHeight);\n    return LayoutBuilder(\n      builder: (context, constrains) {\n        final List<String> displayPath = ancestors.map((e) => e.name).toList();\n        if (displayPath.isEmpty) return const SizedBox.shrink();\n        TextPainter textPainter =\n            _buildTextPainter(displayPath.join(' / '), textStyle);\n        textPainter.layout(maxWidth: constrains.maxWidth);\n        if (textPainter.didExceedMaxLines && displayPath.length > 2) {\n          displayPath.removeAt(displayPath.length - 2);\n          displayPath.insert(displayPath.length - 1, '...');\n        }\n        textPainter = _buildTextPainter(displayPath.join(' / '), textStyle);\n        textPainter.layout(maxWidth: constrains.maxWidth);\n        while (textPainter.didExceedMaxLines && displayPath.length > 3) {\n          displayPath.removeAt(displayPath.length - 2);\n          textPainter = _buildTextPainter(displayPath.join(' / '), textStyle);\n          textPainter.layout(maxWidth: constrains.maxWidth);\n        }\n        return Text(\n          displayPath.join(' / '),\n          style: textStyle,\n          maxLines: 2,\n          overflow: TextOverflow.ellipsis,\n        );\n      },\n    );\n  }\n\n  TextPainter _buildTextPainter(String text, TextStyle style) => TextPainter(\n        text: TextSpan(text: text, style: style),\n        maxLines: 1,\n        textDirection: TextDirection.ltr,\n      );\n\n  Widget buildOnelinePath(BuildContext context) {\n    final ancestors = ancestor.ancestors;\n    List<String> displayPath = ancestors.map((e) => e.name).toList();\n    if (ancestors.length > 2) {\n      displayPath = [ancestors.first.name, '...', ancestors.last.name];\n    }\n    final theme = AppFlowyTheme.of(context);\n    final style = theme.textStyle.caption\n        .standard(color: theme.textColorScheme.tertiary)\n        .copyWith(letterSpacing: 0.1);\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        HSpace(8),\n        Text(\n          '-',\n          style: style.copyWith(\n            color: theme.borderColorScheme.primaryHover,\n          ),\n        ),\n        HSpace(8),\n        Flexible(\n          child: Text(\n            displayPath.join(' / '),\n            style: style,\n            maxLines: 1,\n            overflow: TextOverflow.ellipsis,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/search/view_ancestor_cache.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter/material.dart';\n\nclass ViewAncestorCache {\n  ViewAncestorCache();\n\n  final Map<String, ViewAncestor> _ancestors = {};\n\n  Future<ViewAncestor?> getAncestor(\n    String viewId, {\n    ValueChanged<ViewAncestor>? onRefresh,\n  }) async {\n    final cachedAncestor = _ancestors[viewId];\n    if (cachedAncestor != null) {\n      unawaited(_getAncestor(viewId, onRefresh: onRefresh));\n      return cachedAncestor;\n    }\n    return _getAncestor(viewId);\n  }\n\n  Future<ViewAncestor?> _getAncestor(\n    String viewId, {\n    ValueChanged<ViewAncestor>? onRefresh,\n  }) async {\n    final List<ViewPB>? ancestors =\n        await ViewBackendService.getViewAncestors(viewId).fold(\n      (s) => s.items\n          .where((e) => e.parentViewId.isNotEmpty && e.id != viewId)\n          .toList(),\n      (f) => null,\n    );\n    if (ancestors != null) {\n      final newAncestors = ViewAncestor(\n        ancestors: ancestors.map((e) => ViewParent.fromViewPB(e)).toList(),\n      );\n      _ancestors[viewId] = newAncestors;\n      onRefresh?.call(newAncestors);\n      return newAncestors;\n    }\n    return null;\n  }\n}\n\nclass ViewAncestor {\n  const ViewAncestor({required this.ancestors});\n\n  const ViewAncestor.empty() : ancestors = const [];\n\n  final List<ViewParent> ancestors;\n}\n\nclass ViewParent {\n  ViewParent({required this.id, required this.name});\n\n  final String id;\n  final String name;\n\n  static ViewParent fromViewPB(ViewPB view) =>\n      ViewParent(id: view.id, name: view.nameOrDefault);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nimport 'mobile_selection_menu_item_widget.dart';\nimport 'mobile_selection_menu_widget.dart';\n\nclass MobileSelectionMenu extends SelectionMenuService {\n  MobileSelectionMenu({\n    required this.context,\n    required this.editorState,\n    required this.selectionMenuItems,\n    this.deleteSlashByDefault = false,\n    this.deleteKeywordsByDefault = false,\n    this.style = MobileSelectionMenuStyle.light,\n    this.itemCountFilter = 0,\n    this.startOffset = 0,\n    this.singleColumn = false,\n  });\n\n  final BuildContext context;\n  final EditorState editorState;\n  final List<SelectionMenuItem> selectionMenuItems;\n  final bool deleteSlashByDefault;\n  final bool deleteKeywordsByDefault;\n  final bool singleColumn;\n\n  @override\n  final MobileSelectionMenuStyle style;\n\n  OverlayEntry? _selectionMenuEntry;\n  Offset _offset = Offset.zero;\n  Alignment _alignment = Alignment.topLeft;\n  final int itemCountFilter;\n  final int startOffset;\n  ValueNotifier<_Position> _positionNotifier = ValueNotifier(_Position.zero);\n\n  @override\n  void dismiss() {\n    if (_selectionMenuEntry != null) {\n      editorState.service.keyboardService?.enable();\n      editorState.service.scrollService?.enable();\n      editorState\n          .removeScrollViewScrolledListener(_checkPositionAfterScrolling);\n      _positionNotifier.dispose();\n    }\n\n    _selectionMenuEntry?.remove();\n    _selectionMenuEntry = null;\n  }\n\n  @override\n  Future<void> show() async {\n    final completer = Completer<void>();\n    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n      _show();\n      editorState.addScrollViewScrolledListener(_checkPositionAfterScrolling);\n      completer.complete();\n    });\n    return completer.future;\n  }\n\n  void _show() {\n    final position = _getCurrentPosition();\n    if (position == null) return;\n\n    final editorHeight = editorState.renderBox!.size.height;\n    final editorWidth = editorState.renderBox!.size.width;\n\n    _positionNotifier = ValueNotifier(position);\n    final showAtTop = position.top != null;\n    _selectionMenuEntry = OverlayEntry(\n      builder: (context) {\n        return SizedBox(\n          width: editorWidth,\n          height: editorHeight,\n          child: GestureDetector(\n            behavior: HitTestBehavior.opaque,\n            onTap: dismiss,\n            child: Stack(\n              children: [\n                ValueListenableBuilder(\n                  valueListenable: _positionNotifier,\n                  builder: (context, value, _) {\n                    return Positioned(\n                      top: value.top,\n                      bottom: value.bottom,\n                      left: value.left,\n                      right: value.right,\n                      child: SingleChildScrollView(\n                        scrollDirection: Axis.horizontal,\n                        child: MobileSelectionMenuWidget(\n                          selectionMenuStyle: style,\n                          singleColumn: singleColumn,\n                          showAtTop: showAtTop,\n                          items: selectionMenuItems\n                            ..forEach((element) {\n                              if (element is MobileSelectionMenuItem) {\n                                element.deleteSlash = false;\n                                element.deleteKeywords =\n                                    deleteKeywordsByDefault;\n                                for (final e in element.children) {\n                                  e.deleteSlash = deleteSlashByDefault;\n                                  e.deleteKeywords = deleteKeywordsByDefault;\n                                  e.onSelected = () {\n                                    dismiss();\n                                  };\n                                }\n                              } else {\n                                element.deleteSlash = deleteSlashByDefault;\n                                element.deleteKeywords =\n                                    deleteKeywordsByDefault;\n                                element.onSelected = () {\n                                  dismiss();\n                                };\n                              }\n                            }),\n                          maxItemInRow: 5,\n                          editorState: editorState,\n                          itemCountFilter: itemCountFilter,\n                          startOffset: startOffset,\n                          menuService: this,\n                          onExit: () {\n                            dismiss();\n                          },\n                          deleteSlashByDefault: deleteSlashByDefault,\n                        ),\n                      ),\n                    );\n                  },\n                ),\n              ],\n            ),\n          ),\n        );\n      },\n    );\n\n    Overlay.of(context, rootOverlay: true).insert(_selectionMenuEntry!);\n\n    editorState.service.keyboardService?.disable(showCursor: true);\n    editorState.service.scrollService?.disable();\n  }\n\n  /// the workaround for: editor auto scrolling that will cause wrong position\n  /// of slash menu\n  void _checkPositionAfterScrolling() {\n    final position = _getCurrentPosition();\n    if (position == null) return;\n    if (position == _positionNotifier.value) {\n      Future.delayed(const Duration(milliseconds: 100)).then((_) {\n        final position = _getCurrentPosition();\n        if (position == null) return;\n        if (position != _positionNotifier.value) {\n          _positionNotifier.value = position;\n        }\n      });\n    } else {\n      _positionNotifier.value = position;\n    }\n  }\n\n  _Position? _getCurrentPosition() {\n    final selectionRects = editorState.selectionRects();\n    if (selectionRects.isEmpty) {\n      return null;\n    }\n    final screenSize = MediaQuery.of(context).size;\n    calculateSelectionMenuOffset(selectionRects.first, screenSize);\n    final (left, top, right, bottom) = getPosition();\n    return _Position(left, top, right, bottom);\n  }\n\n  @override\n  Alignment get alignment {\n    return _alignment;\n  }\n\n  @override\n  Offset get offset {\n    return _offset;\n  }\n\n  @override\n  (double? left, double? top, double? right, double? bottom) getPosition() {\n    double? left, top, right, bottom;\n    switch (alignment) {\n      case Alignment.topLeft:\n        left = offset.dx;\n        top = offset.dy;\n        break;\n      case Alignment.bottomLeft:\n        left = offset.dx;\n        bottom = offset.dy;\n        break;\n      case Alignment.topRight:\n        right = offset.dx;\n        top = offset.dy;\n        break;\n      case Alignment.bottomRight:\n        right = offset.dx;\n        bottom = offset.dy;\n        break;\n    }\n    return (left, top, right, bottom);\n  }\n\n  void calculateSelectionMenuOffset(Rect rect, Size screenSize) {\n    // Workaround: We can customize the padding through the [EditorStyle],\n    // but the coordinates of overlay are not properly converted currently.\n    // Just subtract the padding here as a result.\n    const menuHeight = 192.0, menuWidth = 240.0;\n    final editorOffset =\n        editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;\n    final editorHeight = editorState.renderBox!.size.height;\n    final screenHeight = screenSize.height;\n    final editorWidth = editorState.renderBox!.size.width;\n    final rectHeight = rect.height;\n\n    // show below default\n    _alignment = Alignment.bottomRight;\n    final bottomRight = rect.topLeft;\n    final offset = bottomRight;\n    final limitX = editorWidth + editorOffset.dx - menuWidth,\n        limitY = screenHeight -\n            editorHeight +\n            editorOffset.dy -\n            menuHeight -\n            rectHeight;\n    _offset = Offset(\n      editorWidth - offset.dx - menuWidth,\n      screenHeight - offset.dy - menuHeight - rectHeight,\n    );\n\n    if (offset.dy + menuHeight >= editorOffset.dy + editorHeight) {\n      /// show above\n      if (offset.dy > menuHeight) {\n        _offset = Offset(\n          _offset.dx,\n          offset.dy - menuHeight,\n        );\n        _alignment = Alignment.topRight;\n      } else {\n        _offset = Offset(\n          _offset.dx,\n          limitY,\n        );\n      }\n    }\n\n    if (offset.dx + menuWidth >= editorOffset.dx + editorWidth) {\n      /// show left\n      if (offset.dx > menuWidth) {\n        _alignment = _alignment == Alignment.bottomRight\n            ? Alignment.bottomLeft\n            : Alignment.topLeft;\n        _offset = Offset(\n          offset.dx - menuWidth,\n          _offset.dy,\n        );\n      } else {\n        _offset = Offset(\n          limitX,\n          _offset.dy,\n        );\n      }\n    }\n  }\n}\n\nclass _Position {\n  const _Position(this.left, this.top, this.right, this.bottom);\n\n  final double? left;\n  final double? top;\n  final double? right;\n  final double? bottom;\n\n  static const _Position zero = _Position(0, 0, 0, 0);\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is _Position &&\n          runtimeType == other.runtimeType &&\n          left == other.left &&\n          top == other.top &&\n          right == other.right &&\n          bottom == other.bottom;\n\n  @override\n  int get hashCode =>\n      left.hashCode ^ top.hashCode ^ right.hashCode ^ bottom.hashCode;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\n\nclass MobileSelectionMenuItem extends SelectionMenuItem {\n  MobileSelectionMenuItem({\n    required super.getName,\n    required super.icon,\n    super.keywords = const [],\n    required super.handler,\n    this.children = const [],\n    super.nameBuilder,\n    super.deleteKeywords,\n    super.deleteSlash,\n  });\n\n  final List<SelectionMenuItem> children;\n\n  bool get isNotEmpty => children.isNotEmpty;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nimport 'mobile_selection_menu_item.dart';\n\nclass MobileSelectionMenuItemWidget extends StatelessWidget {\n  const MobileSelectionMenuItemWidget({\n    super.key,\n    required this.editorState,\n    required this.menuService,\n    required this.item,\n    required this.isSelected,\n    required this.selectionMenuStyle,\n    required this.onTap,\n  });\n\n  final EditorState editorState;\n  final SelectionMenuService menuService;\n  final SelectionMenuItem item;\n  final bool isSelected;\n  final MobileSelectionMenuStyle selectionMenuStyle;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    final style = selectionMenuStyle;\n    final showRightArrow = item is MobileSelectionMenuItem &&\n        (item as MobileSelectionMenuItem).isNotEmpty;\n    return Container(\n      padding: const EdgeInsets.symmetric(horizontal: 6),\n      child: TextButton.icon(\n        icon: item.icon(\n          editorState,\n          false,\n          selectionMenuStyle,\n        ),\n        style: ButtonStyle(\n          alignment: Alignment.centerLeft,\n          overlayColor: WidgetStateProperty.all(Colors.transparent),\n          backgroundColor: isSelected\n              ? WidgetStateProperty.all(\n                  style.selectionMenuItemSelectedColor,\n                )\n              : WidgetStateProperty.all(Colors.transparent),\n        ),\n        label: Row(\n          children: [\n            item.nameBuilder?.call(item.name, style, false) ??\n                Text(\n                  item.name,\n                  textAlign: TextAlign.left,\n                  style: TextStyle(\n                    color: style.selectionMenuItemTextColor,\n                    fontSize: 16.0,\n                  ),\n                ),\n            if (showRightArrow) ...[\n              Spacer(),\n              Icon(\n                Icons.keyboard_arrow_right_rounded,\n                color: style.selectionMenuItemRightIconColor,\n              ),\n            ],\n          ],\n        ),\n        onPressed: () {\n          onTap.call();\n          item.handler(\n            editorState,\n            menuService,\n            context,\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass MobileSelectionMenuStyle extends SelectionMenuStyle {\n  const MobileSelectionMenuStyle({\n    required super.selectionMenuBackgroundColor,\n    required super.selectionMenuItemTextColor,\n    required super.selectionMenuItemIconColor,\n    required super.selectionMenuItemSelectedTextColor,\n    required super.selectionMenuItemSelectedIconColor,\n    required super.selectionMenuItemSelectedColor,\n    required super.selectionMenuUnselectedLabelColor,\n    required super.selectionMenuDividerColor,\n    required super.selectionMenuLinkBorderColor,\n    required super.selectionMenuInvalidLinkColor,\n    required super.selectionMenuButtonColor,\n    required super.selectionMenuButtonTextColor,\n    required super.selectionMenuButtonIconColor,\n    required super.selectionMenuButtonBorderColor,\n    required super.selectionMenuTabIndicatorColor,\n    required this.selectionMenuItemRightIconColor,\n  });\n\n  final Color selectionMenuItemRightIconColor;\n\n  static const MobileSelectionMenuStyle light = MobileSelectionMenuStyle(\n    selectionMenuBackgroundColor: Color(0xFFFFFFFF),\n    selectionMenuItemTextColor: Color(0xFF1F2225),\n    selectionMenuItemIconColor: Color(0xFF333333),\n    selectionMenuItemSelectedColor: Color(0xFFF2F5F7),\n    selectionMenuItemRightIconColor: Color(0xB31E2022),\n    selectionMenuItemSelectedTextColor: Color.fromARGB(255, 56, 91, 247),\n    selectionMenuItemSelectedIconColor: Color.fromARGB(255, 56, 91, 247),\n    selectionMenuUnselectedLabelColor: Color(0xFF333333),\n    selectionMenuDividerColor: Color(0xFF00BCF0),\n    selectionMenuLinkBorderColor: Color(0xFF00BCF0),\n    selectionMenuInvalidLinkColor: Color(0xFFE53935),\n    selectionMenuButtonColor: Color(0xFF00BCF0),\n    selectionMenuButtonTextColor: Color(0xFF333333),\n    selectionMenuButtonIconColor: Color(0xFF333333),\n    selectionMenuButtonBorderColor: Color(0xFF00BCF0),\n    selectionMenuTabIndicatorColor: Color(0xFF00BCF0),\n  );\n\n  static const MobileSelectionMenuStyle dark = MobileSelectionMenuStyle(\n    selectionMenuBackgroundColor: Color(0xFF424242),\n    selectionMenuItemTextColor: Color(0xFFFFFFFF),\n    selectionMenuItemIconColor: Color(0xFFFFFFFF),\n    selectionMenuItemSelectedColor: Color(0xFF666666),\n    selectionMenuItemRightIconColor: Color(0xB3FFFFFF),\n    selectionMenuItemSelectedTextColor: Color(0xFF131720),\n    selectionMenuItemSelectedIconColor: Color(0xFF131720),\n    selectionMenuUnselectedLabelColor: Color(0xFFBBC3CD),\n    selectionMenuDividerColor: Color(0xFF3A3F44),\n    selectionMenuLinkBorderColor: Color(0xFF3A3F44),\n    selectionMenuInvalidLinkColor: Color(0xFFE53935),\n    selectionMenuButtonColor: Color(0xFF00BCF0),\n    selectionMenuButtonTextColor: Color(0xFFFFFFFF),\n    selectionMenuButtonIconColor: Color(0xFFFFFFFF),\n    selectionMenuButtonBorderColor: Color(0xFF00BCF0),\n    selectionMenuTabIndicatorColor: Color(0xFF00BCF0),\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart",
    "content": "import 'dart:math';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nimport 'mobile_selection_menu_item.dart';\nimport 'mobile_selection_menu_item_widget.dart';\nimport 'slash_keyboard_service_interceptor.dart';\n\nclass MobileSelectionMenuWidget extends StatefulWidget {\n  const MobileSelectionMenuWidget({\n    super.key,\n    required this.items,\n    required this.itemCountFilter,\n    required this.maxItemInRow,\n    required this.menuService,\n    required this.editorState,\n    required this.onExit,\n    required this.selectionMenuStyle,\n    required this.deleteSlashByDefault,\n    required this.singleColumn,\n    required this.startOffset,\n    required this.showAtTop,\n    this.nameBuilder,\n  });\n\n  final List<SelectionMenuItem> items;\n  final int itemCountFilter;\n  final int maxItemInRow;\n\n  final SelectionMenuService menuService;\n  final EditorState editorState;\n\n  final VoidCallback onExit;\n\n  final MobileSelectionMenuStyle selectionMenuStyle;\n\n  final bool deleteSlashByDefault;\n  final bool singleColumn;\n  final bool showAtTop;\n  final int startOffset;\n\n  final SelectionMenuItemNameBuilder? nameBuilder;\n\n  @override\n  State<MobileSelectionMenuWidget> createState() =>\n      _MobileSelectionMenuWidgetState();\n}\n\nclass _MobileSelectionMenuWidgetState extends State<MobileSelectionMenuWidget> {\n  final _focusNode = FocusNode(debugLabel: 'popup_list_widget');\n\n  List<SelectionMenuItem> _showingItems = [];\n\n  int _searchCounter = 0;\n\n  EditorState get editorState => widget.editorState;\n\n  SelectionMenuService get menuService => widget.menuService;\n\n  String _keyword = '';\n\n  String get keyword => _keyword;\n\n  int selectedIndex = 0;\n\n  late AppFlowyKeyboardServiceInterceptor keyboardInterceptor;\n\n  List<SelectionMenuItem> get filterItems {\n    final List<SelectionMenuItem> items = [];\n    for (final item in widget.items) {\n      if (item is MobileSelectionMenuItem) {\n        for (final childItem in item.children) {\n          items.add(childItem);\n        }\n      } else {\n        items.add(item);\n      }\n    }\n    return items;\n  }\n\n  set keyword(String newKeyword) {\n    _keyword = newKeyword;\n\n    // Search items according to the keyword, and calculate the length of\n    //  the longest keyword, which is used to dismiss the selection_service.\n    var maxKeywordLength = 0;\n\n    final items = newKeyword.isEmpty\n        ? widget.items\n        : filterItems\n            .where(\n              (item) => item.allKeywords.any((keyword) {\n                final value = keyword.contains(newKeyword.toLowerCase());\n                if (value) {\n                  maxKeywordLength = max(maxKeywordLength, keyword.length);\n                }\n                return value;\n              }),\n            )\n            .toList(growable: false);\n\n    AppFlowyEditorLog.ui.debug('$items');\n\n    if (keyword.length >= maxKeywordLength + 2 &&\n        !(widget.deleteSlashByDefault && _searchCounter < 2)) {\n      return widget.onExit();\n    }\n\n    _showingItems = items;\n    refreshSelectedIndex();\n\n    if (_showingItems.isEmpty) {\n      _searchCounter++;\n    } else {\n      _searchCounter = 0;\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _showingItems = buildInitialItems();\n\n    keepEditorFocusNotifier.increase();\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      _focusNode.requestFocus();\n    });\n\n    keyboardInterceptor = SlashKeyboardServiceInterceptor(\n      onDelete: () async {\n        if (!mounted) return false;\n        final hasItemsChanged = !isInitialItems();\n        if (keyword.isEmpty && hasItemsChanged) {\n          _showingItems = buildInitialItems();\n          refreshSelectedIndex();\n          return true;\n        }\n        return false;\n      },\n      onEnter: () {\n        if (!mounted) return;\n        if (_showingItems.isEmpty) return;\n        final item = _showingItems[selectedIndex];\n        if (item is MobileSelectionMenuItem) {\n          selectedIndex = 0;\n          item.onSelected?.call();\n        } else {\n          item.handler(\n            editorState,\n            menuService,\n            context,\n          );\n        }\n      },\n    );\n    editorState.service.keyboardService\n        ?.registerInterceptor(keyboardInterceptor);\n    editorState.selectionNotifier.addListener(onSelectionChanged);\n  }\n\n  @override\n  void dispose() {\n    editorState.service.keyboardService\n        ?.unregisterInterceptor(keyboardInterceptor);\n    editorState.selectionNotifier.removeListener(onSelectionChanged);\n    _focusNode.dispose();\n    keepEditorFocusNotifier.decrease();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 192,\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          if (widget.showAtTop) Spacer(),\n          Focus(\n            focusNode: _focusNode,\n            child: DecoratedBox(\n              decoration: BoxDecoration(\n                color: widget.selectionMenuStyle.selectionMenuBackgroundColor,\n                boxShadow: [\n                  BoxShadow(\n                    blurRadius: 5,\n                    spreadRadius: 1,\n                    color: Colors.black.withValues(alpha: 0.1),\n                  ),\n                ],\n                borderRadius: BorderRadius.circular(6.0),\n              ),\n              child: _showingItems.isEmpty\n                  ? _buildNoResultsWidget(context)\n                  : _buildResultsWidget(\n                      context,\n                      _showingItems,\n                      widget.itemCountFilter,\n                    ),\n            ),\n          ),\n          if (!widget.showAtTop) Spacer(),\n        ],\n      ),\n    );\n  }\n\n  void onSelectionChanged() {\n    final selection = editorState.selection;\n    if (selection == null) {\n      widget.onExit();\n      return;\n    }\n    if (!selection.isCollapsed) {\n      widget.onExit();\n      return;\n    }\n    final startOffset = widget.startOffset;\n    final endOffset = selection.end.offset;\n    if (endOffset < startOffset) {\n      widget.onExit();\n      return;\n    }\n    final node = editorState.getNodeAtPath(selection.start.path);\n    final text = node?.delta?.toPlainText() ?? '';\n    final search = text.substring(startOffset, endOffset);\n    keyword = search;\n  }\n\n  Widget _buildResultsWidget(\n    BuildContext buildContext,\n    List<SelectionMenuItem> items,\n    int itemCountFilter,\n  ) {\n    if (widget.singleColumn) {\n      final List<Widget> itemWidgets = [];\n      for (var i = 0; i < items.length; i++) {\n        final item = items[i];\n        itemWidgets.add(\n          GestureDetector(\n            onTapDown: (e) {\n              setState(() {\n                selectedIndex = i;\n              });\n            },\n            child: MobileSelectionMenuItemWidget(\n              item: item,\n              isSelected: i == selectedIndex,\n              editorState: editorState,\n              menuService: menuService,\n              selectionMenuStyle: widget.selectionMenuStyle,\n              onTap: () {\n                if (item is MobileSelectionMenuItem) refreshSelectedIndex();\n              },\n            ),\n          ),\n        );\n      }\n      return ConstrainedBox(\n        constraints: const BoxConstraints(\n          maxHeight: 192,\n          minWidth: 240,\n          maxWidth: 240,\n        ),\n        child: ListView(\n          shrinkWrap: true,\n          padding: EdgeInsets.zero,\n          children: itemWidgets,\n        ),\n      );\n    } else {\n      final List<Widget> columns = [];\n      List<Widget> itemWidgets = [];\n      // apply item count filter\n      if (itemCountFilter > 0) {\n        items = items.take(itemCountFilter).toList();\n      }\n\n      for (var i = 0; i < items.length; i++) {\n        final item = items[i];\n        if (i != 0 && i % (widget.maxItemInRow) == 0) {\n          columns.add(\n            Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: itemWidgets,\n            ),\n          );\n          itemWidgets = [];\n        }\n        itemWidgets.add(\n          MobileSelectionMenuItemWidget(\n            item: item,\n            isSelected: false,\n            editorState: editorState,\n            menuService: menuService,\n            selectionMenuStyle: widget.selectionMenuStyle,\n            onTap: () {\n              if (item is MobileSelectionMenuItem) refreshSelectedIndex();\n            },\n          ),\n        );\n      }\n      if (itemWidgets.isNotEmpty) {\n        columns.add(\n          Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: itemWidgets,\n          ),\n        );\n        itemWidgets = [];\n      }\n      return Row(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: columns,\n      );\n    }\n  }\n\n  void refreshSelectedIndex() {\n    if (!mounted) return;\n    setState(() {\n      selectedIndex = 0;\n    });\n  }\n\n  Widget _buildNoResultsWidget(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return DecoratedBox(\n      decoration: BoxDecoration(\n        color: Theme.of(context).cardColor,\n        boxShadow: [\n          BoxShadow(\n            blurRadius: 5,\n            spreadRadius: 1,\n            color: Colors.black.withValues(alpha: 0.1),\n          ),\n        ],\n        borderRadius: BorderRadius.circular(12.0),\n      ),\n      child: SizedBox(\n        width: 240,\n        height: 48,\n        child: Padding(\n          padding: const EdgeInsets.all(6.0),\n          child: Material(\n            color: Colors.transparent,\n            child: Center(\n              child: Text(\n                LocaleKeys.inlineActions_noResults.tr(),\n                style: TextStyle(\n                  fontSize: 18.0,\n                  color: theme.textColorScheme.primary,\n                ),\n                textAlign: TextAlign.center,\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  List<SelectionMenuItem> buildInitialItems() {\n    final List<SelectionMenuItem> items = [];\n    for (final item in widget.items) {\n      if (item is MobileSelectionMenuItem) {\n        item.onSelected = () {\n          if (mounted) {\n            setState(() {\n              _showingItems = item.children\n                  .map((e) => e..onSelected = widget.onExit)\n                  .toList();\n            });\n          }\n        };\n      }\n      items.add(item);\n    }\n    return items;\n  }\n\n  bool isInitialItems() {\n    if (_showingItems.length != widget.items.length) return false;\n    int i = 0;\n    for (final item in _showingItems) {\n      final widgetItem = widget.items[i];\n      if (widgetItem.name != item.name) return false;\n      i++;\n    }\n    return true;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/slash_keyboard_service_interceptor.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/keyboard_interceptor/keyboard_interceptor.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\n\nclass SlashKeyboardServiceInterceptor extends EditorKeyboardInterceptor {\n  SlashKeyboardServiceInterceptor({\n    required this.onDelete,\n    required this.onEnter,\n  });\n\n  final AsyncValueGetter<bool> onDelete;\n  final VoidCallback onEnter;\n\n  @override\n  Future<bool> interceptDelete(\n    TextEditingDeltaDeletion deletion,\n    EditorState editorState,\n  ) async {\n    final intercept = await onDelete.call();\n    if (intercept) {\n      return true;\n    } else {\n      return super.interceptDelete(deletion, editorState);\n    }\n  }\n\n  @override\n  Future<bool> interceptInsert(\n    TextEditingDeltaInsertion insertion,\n    EditorState editorState,\n    List<CharacterShortcutEvent> characterShortcutEvents,\n  ) async {\n    final text = insertion.textInserted;\n    if (text.contains('\\n')) {\n      onEnter.call();\n      return true;\n    }\n    return super\n        .interceptInsert(insertion, editorState, characterShortcutEvents);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about.dart",
    "content": "export 'about_setting_group.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';\nimport 'package:appflowy/startup/tasks/device_info_task.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nimport '../widgets/widgets.dart';\n\nclass AboutSettingGroup extends StatelessWidget {\n  const AboutSettingGroup({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MobileSettingGroup(\n      groupTitle: LocaleKeys.settings_mobile_about.tr(),\n      settingItemList: [\n        MobileSettingItem(\n          name: LocaleKeys.settings_mobile_privacyPolicy.tr(),\n          trailing: MobileSettingTrailing(\n            text: '',\n          ),\n          onTap: () => afLaunchUrlString('https://appflowy.com/privacy'),\n        ),\n        MobileSettingItem(\n          name: LocaleKeys.settings_mobile_termsAndConditions.tr(),\n          trailing: MobileSettingTrailing(\n            text: '',\n          ),\n          onTap: () => afLaunchUrlString('https://appflowy.com/terms'),\n        ),\n        if (kDebugMode)\n          MobileSettingItem(\n            name: 'Feature Flags',\n            trailing: MobileSettingTrailing(\n              text: '',\n            ),\n            onTap: () {\n              context.push(FeatureFlagScreen.routeName);\n            },\n          ),\n        MobileSettingItem(\n          name: LocaleKeys.settings_mobile_version.tr(),\n          trailing: MobileSettingTrailing(\n            text:\n                '${ApplicationInfo.applicationVersion} (${ApplicationInfo.buildNumber})',\n            showArrow: false,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/ai/ai_settings_group.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_group_widget.dart';\nimport 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart';\nimport 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';\nimport 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nclass AiSettingsGroup extends StatelessWidget {\n  const AiSettingsGroup({\n    super.key,\n    required this.userProfile,\n    required this.workspaceId,\n  });\n\n  final UserProfilePB userProfile;\n  final String workspaceId;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => SettingsAIBloc(\n        userProfile,\n        workspaceId,\n      )..add(const SettingsAIEvent.started()),\n      child: BlocBuilder<SettingsAIBloc, SettingsAIState>(\n        builder: (context, state) {\n          return MobileSettingGroup(\n            groupTitle: LocaleKeys.settings_aiPage_title.tr(),\n            settingItemList: [\n              MobileSettingItem(\n                name: LocaleKeys.settings_aiPage_keys_llmModelType.tr(),\n                trailing: MobileSettingTrailing(\n                  text: state.availableModels?.selectedModel.name ?? \"\",\n                ),\n                onTap: () => _onLLMModelTypeTap(context, state),\n              ),\n              // enable AI search if needed\n              // MobileSettingItem(\n              //   name: LocaleKeys.settings_aiPage_keys_enableAISearchTitle.tr(),\n              //   trailing: const Icon(\n              //     Icons.chevron_right,\n              //   ),\n              //   onTap: () => context.push(AppFlowyCloudPage.routeName),\n              // ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  void _onLLMModelTypeTap(BuildContext context, SettingsAIState state) {\n    final availableModels = state.availableModels;\n    showMobileBottomSheet(\n      context,\n      showHeader: true,\n      showDragHandle: true,\n      showDivider: false,\n      title: LocaleKeys.settings_aiPage_keys_llmModelType.tr(),\n      builder: (_) {\n        return Column(\n          children: (availableModels?.models ?? [])\n              .asMap()\n              .entries\n              .map(\n                (entry) => FlowyOptionTile.checkbox(\n                  text: entry.value.name,\n                  showTopBorder: entry.key == 0,\n                  isSelected:\n                      availableModels?.selectedModel.name == entry.value.name,\n                  onTap: () {\n                    context\n                        .read<SettingsAIBloc>()\n                        .add(SettingsAIEvent.selectModel(entry.value));\n                    context.pop();\n                  },\n                ),\n              )\n              .toList(),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/appearance_setting_group.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/setting/appearance/rtl_setting.dart';\nimport 'package:appflowy/mobile/presentation/setting/appearance/text_scale_setting.dart';\nimport 'package:appflowy/mobile/presentation/setting/appearance/theme_setting.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nimport '../setting.dart';\n\nclass AppearanceSettingGroup extends StatelessWidget {\n  const AppearanceSettingGroup({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MobileSettingGroup(\n      groupTitle: LocaleKeys.settings_menu_appearance.tr(),\n      settingItemList: const [\n        ThemeSetting(),\n        FontSetting(),\n        DisplaySizeSetting(),\n        RTLSetting(),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/rtl_setting.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../setting.dart';\n\nclass RTLSetting extends StatelessWidget {\n  const RTLSetting({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final textDirection =\n        context.watch<AppearanceSettingsCubit>().state.textDirection;\n    return MobileSettingItem(\n      name: LocaleKeys.settings_appearance_textDirection_label.tr(),\n      trailing: MobileSettingTrailing(\n        text: _textDirectionLabelText(textDirection),\n      ),\n      onTap: () {\n        showMobileBottomSheet(\n          context,\n          showHeader: true,\n          showDragHandle: true,\n          showDivider: false,\n          title: LocaleKeys.settings_appearance_textDirection_label.tr(),\n          builder: (context) {\n            return Column(\n              children: [\n                FlowyOptionTile.checkbox(\n                  text: LocaleKeys.settings_appearance_textDirection_ltr.tr(),\n                  isSelected: textDirection == AppFlowyTextDirection.ltr,\n                  onTap: () => applyTextDirectionAndPop(\n                    context,\n                    AppFlowyTextDirection.ltr,\n                  ),\n                ),\n                FlowyOptionTile.checkbox(\n                  showTopBorder: false,\n                  text: LocaleKeys.settings_appearance_textDirection_rtl.tr(),\n                  isSelected: textDirection == AppFlowyTextDirection.rtl,\n                  onTap: () => applyTextDirectionAndPop(\n                    context,\n                    AppFlowyTextDirection.rtl,\n                  ),\n                ),\n                FlowyOptionTile.checkbox(\n                  showTopBorder: false,\n                  text: LocaleKeys.settings_appearance_textDirection_auto.tr(),\n                  isSelected: textDirection == AppFlowyTextDirection.auto,\n                  onTap: () => applyTextDirectionAndPop(\n                    context,\n                    AppFlowyTextDirection.auto,\n                  ),\n                ),\n              ],\n            );\n          },\n        );\n      },\n    );\n  }\n\n  String _textDirectionLabelText(AppFlowyTextDirection textDirection) {\n    switch (textDirection) {\n      case AppFlowyTextDirection.auto:\n        return LocaleKeys.settings_appearance_textDirection_auto.tr();\n      case AppFlowyTextDirection.rtl:\n        return LocaleKeys.settings_appearance_textDirection_rtl.tr();\n      case AppFlowyTextDirection.ltr:\n        return LocaleKeys.settings_appearance_textDirection_ltr.tr();\n    }\n  }\n\n  void applyTextDirectionAndPop(\n    BuildContext context,\n    AppFlowyTextDirection textDirection,\n  ) {\n    context.read<AppearanceSettingsCubit>().setTextDirection(textDirection);\n    context\n        .read<DocumentAppearanceCubit>()\n        .syncDefaultTextDirection(textDirection.name);\n    Navigator.pop(context);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/text_scale_setting.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/startup/tasks/app_window_size_manager.dart';\nimport 'package:appflowy/workspace/presentation/home/hotkeys.dart';\nimport 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:scaled_app/scaled_app.dart';\n\nimport '../setting.dart';\n\nconst int _divisions = 4;\nconst double _minMobileScaleFactor = 0.8;\nconst double _maxMobileScaleFactor = 1.2;\n\nclass DisplaySizeSetting extends StatefulWidget {\n  const DisplaySizeSetting({\n    super.key,\n  });\n\n  @override\n  State<DisplaySizeSetting> createState() => _DisplaySizeSettingState();\n}\n\nclass _DisplaySizeSettingState extends State<DisplaySizeSetting> {\n  double scaleFactor = 1.0;\n  final windowSizeManager = WindowSizeManager();\n\n  @override\n  void initState() {\n    super.initState();\n    windowSizeManager.getScaleFactor().then((v) {\n      if (v != scaleFactor && mounted) {\n        setState(() {\n          scaleFactor = v;\n        });\n      }\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MobileSettingItem(\n      name: LocaleKeys.settings_appearance_displaySize.tr(),\n      trailing: MobileSettingTrailing(\n        text: scaleFactor.toStringAsFixed(1),\n      ),\n      onTap: () {\n        showMobileBottomSheet(\n          context,\n          showHeader: true,\n          showDragHandle: true,\n          showDivider: false,\n          title: LocaleKeys.settings_appearance_displaySize.tr(),\n          builder: (context) {\n            return FontSizeStepper(\n              value: scaleFactor,\n              minimumValue: _minMobileScaleFactor,\n              maximumValue: _maxMobileScaleFactor,\n              divisions: _divisions,\n              onChanged: (newScaleFactor) async {\n                await _setScale(newScaleFactor);\n              },\n            );\n          },\n        );\n      },\n    );\n  }\n\n  Future<void> _setScale(double value) async {\n    if (FlowyRunner.currentMode == IntegrationMode.integrationTest) {\n      // The integration test will fail if we check the scale factor in the test.\n      // #0      ScaledWidgetsFlutterBinding.Eval ()\n      // #1      ScaledWidgetsFlutterBinding.instance (package:scaled_app/scaled_app.dart:66:62)\n      // ignore: invalid_use_of_visible_for_testing_member\n      appflowyScaleFactor = value;\n    } else {\n      ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => value;\n    }\n    if (mounted) {\n      setState(() {\n        scaleFactor = value;\n      });\n    }\n    await windowSizeManager.setScaleFactor(value);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/theme_setting.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/util/theme_mode_extension.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../setting.dart';\n\nclass ThemeSetting extends StatelessWidget {\n  const ThemeSetting({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final themeMode = context.watch<AppearanceSettingsCubit>().state.themeMode;\n    return MobileSettingItem(\n      name: LocaleKeys.settings_appearance_themeMode_label.tr(),\n      trailing: MobileSettingTrailing(\n        text: themeMode.labelText,\n      ),\n      onTap: () {\n        showMobileBottomSheet(\n          context,\n          showHeader: true,\n          showDragHandle: true,\n          showDivider: false,\n          title: LocaleKeys.settings_appearance_themeMode_label.tr(),\n          builder: (context) {\n            final themeMode =\n                context.read<AppearanceSettingsCubit>().state.themeMode;\n            return Column(\n              children: [\n                FlowyOptionTile.checkbox(\n                  text: LocaleKeys.settings_appearance_themeMode_system.tr(),\n                  leftIcon: const FlowySvg(\n                    FlowySvgs.m_theme_mode_system_s,\n                  ),\n                  isSelected: themeMode == ThemeMode.system,\n                  onTap: () {\n                    context\n                        .read<AppearanceSettingsCubit>()\n                        .setThemeMode(ThemeMode.system);\n                    Navigator.pop(context);\n                  },\n                ),\n                FlowyOptionTile.checkbox(\n                  showTopBorder: false,\n                  text: LocaleKeys.settings_appearance_themeMode_light.tr(),\n                  leftIcon: const FlowySvg(\n                    FlowySvgs.m_theme_mode_light_s,\n                  ),\n                  isSelected: themeMode == ThemeMode.light,\n                  onTap: () {\n                    context\n                        .read<AppearanceSettingsCubit>()\n                        .setThemeMode(ThemeMode.light);\n                    Navigator.pop(context);\n                  },\n                ),\n                FlowyOptionTile.checkbox(\n                  showTopBorder: false,\n                  text: LocaleKeys.settings_appearance_themeMode_dark.tr(),\n                  leftIcon: const FlowySvg(\n                    FlowySvgs.m_theme_mode_dark_s,\n                  ),\n                  isSelected: themeMode == ThemeMode.dark,\n                  onTap: () {\n                    context\n                        .read<AppearanceSettingsCubit>()\n                        .setThemeMode(ThemeMode.dark);\n                    Navigator.pop(context);\n                  },\n                ),\n              ],\n            );\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/setting_cloud.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nclass AppFlowyCloudPage extends StatelessWidget {\n  const AppFlowyCloudPage({super.key});\n\n  static const routeName = '/AppFlowyCloudPage';\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: FlowyAppBar(\n        titleText: LocaleKeys.settings_menu_cloudSettings.tr(),\n      ),\n      body: SettingCloud(\n        restartAppFlowy: () async {\n          await getIt<AuthService>().signOut();\n          await runAppFlowy();\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/cloud_setting_group.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/mobile/presentation/setting/cloud/appflowy_cloud_page.dart';\nimport 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_group_widget.dart';\nimport 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart';\nimport 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/setting_cloud.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nclass CloudSettingGroup extends StatelessWidget {\n  const CloudSettingGroup({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: getAuthenticatorType(),\n      builder: (context, snapshot) {\n        final cloudType = snapshot.data ?? AuthenticatorType.appflowyCloud;\n        final name = titleFromCloudType(cloudType);\n        return MobileSettingGroup(\n          groupTitle: 'Cloud settings',\n          settingItemList: [\n            MobileSettingItem(\n              name: 'Cloud server',\n              trailing: MobileSettingTrailing(\n                text: name,\n              ),\n              onTap: () => context.push(AppFlowyCloudPage.routeName),\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/shared/google_fonts_extension.dart';\nimport 'package:appflowy/util/font_family_extension.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:google_fonts/google_fonts.dart';\n\nfinal List<String> _availableFonts = [\n  defaultFontFamily,\n  ...GoogleFonts.asMap().keys,\n];\n\nclass FontPickerScreen extends StatelessWidget {\n  const FontPickerScreen({super.key});\n\n  static const routeName = '/font_picker';\n\n  @override\n  Widget build(BuildContext context) {\n    return const LanguagePickerPage();\n  }\n}\n\nclass LanguagePickerPage extends StatefulWidget {\n  const LanguagePickerPage({super.key});\n\n  @override\n  State<LanguagePickerPage> createState() => _LanguagePickerPageState();\n}\n\nclass _LanguagePickerPageState extends State<LanguagePickerPage> {\n  late List<String> availableFonts;\n\n  @override\n  void initState() {\n    super.initState();\n    availableFonts = _availableFonts;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final selectedFontFamilyName =\n        context.watch<AppearanceSettingsCubit>().state.font;\n    return Scaffold(\n      appBar: FlowyAppBar(\n        titleText: LocaleKeys.titleBar_font.tr(),\n      ),\n      body: SafeArea(\n        child: Scrollbar(\n          child: FontSelector(\n            selectedFontFamilyName: selectedFontFamilyName,\n            onFontFamilySelected: (fontFamilyName) =>\n                context.pop(fontFamilyName),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass FontSelector extends StatefulWidget {\n  const FontSelector({\n    super.key,\n    this.scrollController,\n    required this.selectedFontFamilyName,\n    required this.onFontFamilySelected,\n  });\n\n  final ScrollController? scrollController;\n  final String selectedFontFamilyName;\n  final void Function(String fontFamilyName) onFontFamilySelected;\n\n  @override\n  State<FontSelector> createState() => _FontSelectorState();\n}\n\nclass _FontSelectorState extends State<FontSelector> {\n  late List<String> availableFonts;\n\n  @override\n  void initState() {\n    super.initState();\n    availableFonts = _availableFonts;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView.builder(\n      controller: widget.scrollController,\n      itemCount: availableFonts.length + 1, // with search bar\n      itemBuilder: (context, index) {\n        if (index == 0) {\n          // search bar\n          return _buildSearchBar(context);\n        }\n\n        final fontFamilyName = availableFonts[index - 1];\n        final usingDefaultFontFamily = fontFamilyName == defaultFontFamily;\n        final fontFamily = !usingDefaultFontFamily\n            ? getGoogleFontSafely(fontFamilyName).fontFamily\n            : defaultFontFamily;\n        return FlowyOptionTile.checkbox(\n          text: fontFamilyName.fontFamilyDisplayName,\n          isSelected: widget.selectedFontFamilyName == fontFamilyName,\n          showTopBorder: false,\n          onTap: () => widget.onFontFamilySelected(fontFamilyName),\n          fontFamily: fontFamily,\n          backgroundColor: Colors.transparent,\n        );\n      },\n    );\n  }\n\n  Widget _buildSearchBar(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(\n        vertical: 8.0,\n        horizontal: 12.0,\n      ),\n      child: FlowyMobileSearchTextField(\n        onChanged: (keyword) {\n          setState(() {\n            availableFonts = _availableFonts\n                .where(\n                  (font) =>\n                      font.isEmpty || // keep the default one always\n                      font\n                          .parseFontFamilyName()\n                          .toLowerCase()\n                          .contains(keyword.toLowerCase()),\n                )\n                .toList();\n          });\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_setting.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart';\nimport 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/util/font_family_extension.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nimport '../setting.dart';\n\nclass FontSetting extends StatelessWidget {\n  const FontSetting({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final selectedFont = context.watch<AppearanceSettingsCubit>().state.font;\n    final name = selectedFont.fontFamilyDisplayName;\n    return MobileSettingItem(\n      name: LocaleKeys.settings_appearance_fontFamily_label.tr(),\n      trailing: MobileSettingTrailing(\n        text: name,\n      ),\n      onTap: () async {\n        final newFont = await context.push<String>(FontPickerScreen.routeName);\n        if (newFont != null && newFont != selectedFont) {\n          if (context.mounted) {\n            context.read<AppearanceSettingsCubit>().setFontFamily(newFont);\n            unawaited(\n              context.read<DocumentAppearanceCubit>().syncFontFamily(newFont),\n            );\n          }\n        }\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/language/language_picker_screen.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/language.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nclass LanguagePickerScreen extends StatelessWidget {\n  const LanguagePickerScreen({super.key});\n\n  static const routeName = '/language_picker';\n\n  @override\n  Widget build(BuildContext context) => const LanguagePickerPage();\n}\n\nclass LanguagePickerPage extends StatefulWidget {\n  const LanguagePickerPage({\n    super.key,\n  });\n\n  @override\n  State<LanguagePickerPage> createState() => _LanguagePickerPageState();\n}\n\nclass _LanguagePickerPageState extends State<LanguagePickerPage> {\n  @override\n  Widget build(BuildContext context) {\n    final supportedLocales = EasyLocalization.of(context)!.supportedLocales;\n    return Scaffold(\n      appBar: FlowyAppBar(\n        titleText: LocaleKeys.titleBar_language.tr(),\n      ),\n      body: SafeArea(\n        child: ListView.builder(\n          itemBuilder: (context, index) {\n            final locale = supportedLocales[index];\n            return FlowyOptionTile.checkbox(\n              text: languageFromLocale(locale),\n              isSelected: EasyLocalization.of(context)!.locale == locale,\n              showTopBorder: false,\n              onTap: () => context.pop(locale),\n              backgroundColor: Colors.transparent,\n            );\n          },\n          itemCount: supportedLocales.length,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/language_setting_group.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/setting/language/language_picker_screen.dart';\nimport 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/language.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nimport 'setting.dart';\n\nclass LanguageSettingGroup extends StatefulWidget {\n  const LanguageSettingGroup({\n    super.key,\n  });\n\n  @override\n  State<LanguageSettingGroup> createState() => _LanguageSettingGroupState();\n}\n\nclass _LanguageSettingGroupState extends State<LanguageSettingGroup> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocSelector<AppearanceSettingsCubit, AppearanceSettingsState,\n        Locale>(\n      selector: (state) {\n        return state.locale;\n      },\n      builder: (context, locale) {\n        return MobileSettingGroup(\n          groupTitle: LocaleKeys.settings_menu_language.tr(),\n          settingItemList: [\n            MobileSettingItem(\n              name: LocaleKeys.settings_menu_language.tr(),\n              trailing: MobileSettingTrailing(\n                text: languageFromLocale(locale),\n              ),\n              onTap: () async {\n                final newLocale =\n                    await context.push<Locale>(LanguagePickerScreen.routeName);\n                if (newLocale != null && newLocale != locale) {\n                  if (context.mounted) {\n                    context\n                        .read<AppearanceSettingsCubit>()\n                        .setLocale(context, newLocale);\n                  }\n                }\n              },\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/launch_settings_page.dart",
    "content": "import 'package:appflowy/env/env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:appflowy/mobile/presentation/setting/self_host_setting_group.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MobileLaunchSettingsPage extends StatelessWidget {\n  const MobileLaunchSettingsPage({\n    super.key,\n  });\n\n  static const routeName = '/launch_settings';\n\n  @override\n  Widget build(BuildContext context) {\n    context.watch<AppearanceSettingsCubit>();\n    return Scaffold(\n      appBar: FlowyAppBar(\n        titleText: LocaleKeys.settings_title.tr(),\n      ),\n      body: SingleChildScrollView(\n        child: Padding(\n          padding: const EdgeInsets.all(16),\n          child: Column(\n            children: [\n              const LanguageSettingGroup(),\n              if (Env.enableCustomCloud) const SelfHostSettingGroup(),\n              const SupportSettingGroup(),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/notifications_setting_group.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'widgets/widgets.dart';\n\nclass NotificationsSettingGroup extends StatefulWidget {\n  const NotificationsSettingGroup({super.key});\n\n  @override\n  State<NotificationsSettingGroup> createState() =>\n      _NotificationsSettingGroupState();\n}\n\nclass _NotificationsSettingGroupState extends State<NotificationsSettingGroup> {\n  bool isPushNotificationOn = false;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = Theme.of(context);\n    return MobileSettingGroup(\n      groupTitle: LocaleKeys.notificationHub_title.tr(),\n      settingItemList: [\n        MobileSettingItem(\n          name: LocaleKeys.settings_mobile_pushNotifications.tr(),\n          trailing: Switch.adaptive(\n            activeColor: theme.colorScheme.primary,\n            value: isPushNotificationOn,\n            onChanged: (bool value) {\n              setState(() {\n                isPushNotificationOn = value;\n              });\n            },\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/edit_username_bottom_sheet.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nclass EditUsernameBottomSheet extends StatefulWidget {\n  const EditUsernameBottomSheet(\n    this.context, {\n    this.userName,\n    required this.onSubmitted,\n    super.key,\n  });\n  final BuildContext context;\n  final String? userName;\n  final void Function(String) onSubmitted;\n  @override\n  State<EditUsernameBottomSheet> createState() =>\n      _EditUsernameBottomSheetState();\n}\n\nclass _EditUsernameBottomSheetState extends State<EditUsernameBottomSheet> {\n  late TextEditingController _textFieldController;\n\n  final _formKey = GlobalKey<FormState>();\n\n  @override\n  void initState() {\n    super.initState();\n    _textFieldController = TextEditingController(text: widget.userName);\n  }\n\n  @override\n  void dispose() {\n    _textFieldController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    void submitUserName() {\n      if (_formKey.currentState!.validate()) {\n        final value = _textFieldController.text;\n        widget.onSubmitted.call(value);\n        widget.context.pop();\n      }\n    }\n\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: <Widget>[\n        Form(\n          key: _formKey,\n          child: TextFormField(\n            controller: _textFieldController,\n            keyboardType: TextInputType.text,\n            validator: (value) {\n              if (value == null || value.isEmpty) {\n                return LocaleKeys.settings_mobile_usernameEmptyError.tr();\n              }\n              return null;\n            },\n            onEditingComplete: submitUserName,\n          ),\n        ),\n        const VSpace(16),\n        AFFilledTextButton.primary(\n          text: LocaleKeys.button_update.tr(),\n          onTap: submitUserName,\n          size: AFButtonSize.l,\n          alignment: Alignment.center,\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info.dart",
    "content": "export 'edit_username_bottom_sheet.dart';\nexport 'personal_info_setting_group.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/application/password/password_bloc.dart';\nimport 'package:appflowy/workspace/application/user/prelude.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/password/change_password.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/password/setup_password.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../widgets/widgets.dart';\nimport 'personal_info.dart';\n\nclass PersonalInfoSettingGroup extends StatelessWidget {\n  const PersonalInfoSettingGroup({\n    super.key,\n    required this.userProfile,\n  });\n\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider<SettingsUserViewBloc>(\n          create: (context) => getIt<SettingsUserViewBloc>(\n            param1: userProfile,\n          )..add(const SettingsUserEvent.initial()),\n        ),\n        BlocProvider(\n          create: (context) => PasswordBloc(userProfile)\n            ..add(PasswordEvent.init())\n            ..add(PasswordEvent.checkHasPassword()),\n        ),\n      ],\n      child: BlocSelector<SettingsUserViewBloc, SettingsUserState, String>(\n        selector: (state) => state.userProfile.name,\n        builder: (context, userName) {\n          return MobileSettingGroup(\n            groupTitle: LocaleKeys.settings_accountPage_title.tr(),\n            settingItemList: [\n              MobileSettingItem(\n                name: 'User name',\n                trailing: MobileSettingTrailing(\n                  text: userName,\n                ),\n                onTap: () {\n                  showMobileBottomSheet(\n                    context,\n                    showHeader: true,\n                    title: LocaleKeys.settings_mobile_username.tr(),\n                    showCloseButton: true,\n                    showDragHandle: true,\n                    showDivider: false,\n                    padding: const EdgeInsets.symmetric(horizontal: 16),\n                    builder: (_) {\n                      return EditUsernameBottomSheet(\n                        context,\n                        userName: userName,\n                        onSubmitted: (value) => context\n                            .read<SettingsUserViewBloc>()\n                            .add(SettingsUserEvent.updateUserName(name: value)),\n                      );\n                    },\n                  );\n                },\n              ),\n              ...userProfile.userAuthType == AuthTypePB.Server\n                  ? [\n                      _buildEmailItem(context, userProfile),\n                      _buildPasswordItem(context, userProfile),\n                    ]\n                  : [\n                      _buildLoginItem(context, userProfile),\n                    ],\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildEmailItem(BuildContext context, UserProfilePB userProfile) {\n    final theme = AppFlowyTheme.of(context);\n    return MobileSettingItem(\n      name: LocaleKeys.settings_accountPage_email_title.tr(),\n      trailing: Text(\n        userProfile.email,\n        style: theme.textStyle.heading4.standard(\n          color: theme.textColorScheme.secondary,\n        ),\n      ),\n    );\n  }\n\n  Widget _buildPasswordItem(BuildContext context, UserProfilePB userProfile) {\n    return BlocBuilder<PasswordBloc, PasswordState>(\n      builder: (context, state) {\n        final hasPassword = state.hasPassword;\n        final title = hasPassword\n            ? LocaleKeys.newSettings_myAccount_password_changePassword.tr()\n            : LocaleKeys.newSettings_myAccount_password_setupPassword.tr();\n        final passwordBloc = context.read<PasswordBloc>();\n        return MobileSettingItem(\n          name: LocaleKeys.newSettings_myAccount_password_title.tr(),\n          trailing: MobileSettingTrailing(\n            text: '',\n          ),\n          onTap: () {\n            showMobileBottomSheet(\n              context,\n              showHeader: true,\n              title: title,\n              showCloseButton: true,\n              showDragHandle: true,\n              showDivider: false,\n              padding: const EdgeInsets.symmetric(horizontal: 16),\n              builder: (_) {\n                Widget child;\n                if (hasPassword) {\n                  child = ChangePasswordDialogContent(\n                    userProfile: userProfile,\n                    showTitle: false,\n                    showCloseAndSaveButton: false,\n                    showSaveButton: true,\n                    padding: EdgeInsets.zero,\n                  );\n                } else {\n                  child = SetupPasswordDialogContent(\n                    userProfile: userProfile,\n                    showTitle: false,\n                    showCloseAndSaveButton: false,\n                    showSaveButton: true,\n                    padding: EdgeInsets.zero,\n                  );\n                }\n                return BlocProvider.value(\n                  value: passwordBloc,\n                  child: child,\n                );\n              },\n            );\n          },\n        );\n      },\n    );\n  }\n\n  Widget _buildLoginItem(BuildContext context, UserProfilePB userProfile) {\n    final theme = AppFlowyTheme.of(context);\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Text(\n          LocaleKeys.signIn_youAreInLocalMode.tr(),\n          style: theme.textStyle.body.standard(\n            color: theme.textColorScheme.secondary,\n          ),\n        ),\n        VSpace(theme.spacing.m),\n        AFOutlinedTextButton.normal(\n          text: LocaleKeys.signIn_loginToAppFlowyCloud.tr(),\n          size: AFButtonSize.l,\n          alignment: Alignment.center,\n          onTap: () async {\n            // logout and restart the app\n            await getIt<AuthService>().signOut();\n            await runAppFlowy();\n          },\n        ),\n        VSpace(theme.spacing.m),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/settings/appflowy_cloud_urls_bloc.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nenum SelfHostUrlBottomSheetType {\n  shareDomain,\n  cloudURL,\n}\n\nclass SelfHostUrlBottomSheet extends StatefulWidget {\n  const SelfHostUrlBottomSheet({\n    super.key,\n    required this.url,\n    required this.type,\n  });\n\n  final String url;\n  final SelfHostUrlBottomSheetType type;\n\n  @override\n  State<SelfHostUrlBottomSheet> createState() => _SelfHostUrlBottomSheetState();\n}\n\nclass _SelfHostUrlBottomSheetState extends State<SelfHostUrlBottomSheet> {\n  final TextEditingController _textFieldController = TextEditingController();\n\n  final _formKey = GlobalKey<FormState>();\n  @override\n  void initState() {\n    super.initState();\n\n    _textFieldController.text = widget.url;\n  }\n\n  @override\n  void dispose() {\n    _textFieldController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: <Widget>[\n        Form(\n          key: _formKey,\n          child: TextFormField(\n            controller: _textFieldController,\n            keyboardType: TextInputType.text,\n            validator: (value) {\n              if (value == null ||\n                  value.isEmpty ||\n                  validateUrl(value).isFailure) {\n                return LocaleKeys.settings_menu_invalidCloudURLScheme.tr();\n              }\n              return null;\n            },\n            onEditingComplete: _saveSelfHostUrl,\n          ),\n        ),\n        const SizedBox(\n          height: 16,\n        ),\n        SizedBox(\n          width: double.infinity,\n          child: ElevatedButton(\n            onPressed: _saveSelfHostUrl,\n            child: Text(LocaleKeys.settings_menu_restartApp.tr()),\n          ),\n        ),\n      ],\n    );\n  }\n\n  void _saveSelfHostUrl() {\n    if (_formKey.currentState!.validate()) {\n      final value = _textFieldController.text;\n      if (value.isNotEmpty) {\n        validateUrl(value).fold(\n          (url) async {\n            switch (widget.type) {\n              case SelfHostUrlBottomSheetType.shareDomain:\n                await useBaseWebDomain(url);\n              case SelfHostUrlBottomSheetType.cloudURL:\n                await useSelfHostedAppFlowyCloud(url);\n            }\n            await runAppFlowy();\n          },\n          (err) => Log.error(err),\n        );\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host_setting_group.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\n\nimport 'setting.dart';\n\nclass SelfHostSettingGroup extends StatefulWidget {\n  const SelfHostSettingGroup({\n    super.key,\n  });\n\n  @override\n  State<SelfHostSettingGroup> createState() => _SelfHostSettingGroupState();\n}\n\nclass _SelfHostSettingGroupState extends State<SelfHostSettingGroup> {\n  final future = Future.wait([\n    getAppFlowyCloudUrl(),\n    getAppFlowyShareDomain(),\n  ]);\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: future,\n      builder: (context, snapshot) {\n        final data = snapshot.data;\n        if (!snapshot.hasData || data == null || data.length != 2) {\n          return const SizedBox.shrink();\n        }\n        final url = data[0];\n        final shareDomain = data[1];\n        return MobileSettingGroup(\n          groupTitle: LocaleKeys.settings_menu_cloudAppFlowy.tr(),\n          settingItemList: [\n            _buildSelfHostField(url),\n            _buildShareDomainField(shareDomain),\n          ],\n        );\n      },\n    );\n  }\n\n  Widget _buildSelfHostField(String url) {\n    return MobileSettingItem(\n      title: Padding(\n        padding: const EdgeInsets.only(bottom: 4.0),\n        child: FlowyText(\n          LocaleKeys.settings_menu_cloudURL.tr(),\n          fontSize: 12.0,\n          color: Theme.of(context).hintColor,\n        ),\n      ),\n      subtitle: FlowyText(\n        url,\n      ),\n      trailing: const Icon(\n        Icons.chevron_right,\n      ),\n      onTap: () {\n        showMobileBottomSheet(\n          context,\n          showHeader: true,\n          title: LocaleKeys.editor_urlHint.tr(),\n          showCloseButton: true,\n          showDivider: false,\n          padding: const EdgeInsets.symmetric(\n            horizontal: 16.0,\n            vertical: 8.0,\n          ),\n          builder: (_) {\n            return SelfHostUrlBottomSheet(\n              url: url,\n              type: SelfHostUrlBottomSheetType.cloudURL,\n            );\n          },\n        );\n      },\n    );\n  }\n\n  Widget _buildShareDomainField(String shareDomain) {\n    return MobileSettingItem(\n      title: Padding(\n        padding: const EdgeInsets.only(bottom: 4.0),\n        child: FlowyText(\n          LocaleKeys.settings_menu_webURL.tr(),\n          fontSize: 12.0,\n          color: Theme.of(context).hintColor,\n        ),\n      ),\n      subtitle: FlowyText(\n        shareDomain,\n      ),\n      trailing: const Icon(\n        Icons.chevron_right,\n      ),\n      onTap: () {\n        showMobileBottomSheet(\n          context,\n          showHeader: true,\n          title: LocaleKeys.editor_urlHint.tr(),\n          showCloseButton: true,\n          showDivider: false,\n          padding: const EdgeInsets.symmetric(\n            horizontal: 16.0,\n            vertical: 8.0,\n          ),\n          builder: (_) {\n            return SelfHostUrlBottomSheet(\n              url: shareDomain,\n              type: SelfHostUrlBottomSheetType.shareDomain,\n            );\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/setting.dart",
    "content": "export 'about/about.dart';\nexport 'appearance/appearance_setting_group.dart';\nexport 'font/font_setting.dart';\nexport 'language_setting_group.dart';\nexport 'notifications_setting_group.dart';\nexport 'personal_info/personal_info.dart';\nexport 'support_setting_group.dart';\nexport 'widgets/widgets.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/shared/appflowy_cache_manager.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/share_log_files.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/fix_data_widget.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:package_info_plus/package_info_plus.dart';\n\nimport 'widgets/widgets.dart';\n\nclass SupportSettingGroup extends StatelessWidget {\n  const SupportSettingGroup({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: PackageInfo.fromPlatform(),\n      builder: (context, snapshot) => MobileSettingGroup(\n        groupTitle: LocaleKeys.settings_mobile_support.tr(),\n        settingItemList: [\n          MobileSettingItem(\n            name: LocaleKeys.settings_mobile_joinDiscord.tr(),\n            trailing: MobileSettingTrailing(\n              text: '',\n            ),\n            onTap: () => afLaunchUrlString('https://discord.gg/JucBXeU2FE'),\n          ),\n          MobileSettingItem(\n            name: LocaleKeys.workspace_errorActions_reportIssue.tr(),\n            trailing: MobileSettingTrailing(\n              text: '',\n            ),\n            onTap: () {\n              showMobileBottomSheet(\n                context,\n                showDragHandle: true,\n                showHeader: true,\n                title: LocaleKeys.workspace_errorActions_reportIssue.tr(),\n                backgroundColor: Theme.of(context).colorScheme.surface,\n                builder: (context) {\n                  return _ReportIssuesWidget(\n                    version: snapshot.data?.version ?? '',\n                  );\n                },\n              );\n            },\n          ),\n          MobileSettingItem(\n            name: LocaleKeys.settings_files_clearCache.tr(),\n            trailing: MobileSettingTrailing(\n              text: '',\n            ),\n            onTap: () async {\n              await showFlowyMobileConfirmDialog(\n                context,\n                title: FlowyText(\n                  LocaleKeys.settings_files_areYouSureToClearCache.tr(),\n                  maxLines: 2,\n                ),\n                content: FlowyText(\n                  LocaleKeys.settings_files_clearCacheDesc.tr(),\n                  fontSize: 12,\n                  maxLines: 4,\n                ),\n                actionButtonTitle: LocaleKeys.button_yes.tr(),\n                onActionButtonPressed: () async {\n                  await getIt<FlowyCacheManager>().clearAllCache();\n                  // check the workspace and space health\n                  await WorkspaceDataManager.checkViewHealth(\n                    dryRun: false,\n                  );\n                  if (context.mounted) {\n                    showToastNotification(\n                      message: LocaleKeys.settings_files_clearCacheSuccess.tr(),\n                    );\n                  }\n                },\n              );\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _ReportIssuesWidget extends StatelessWidget {\n  const _ReportIssuesWidget({\n    required this.version,\n  });\n\n  final String version;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        FlowyOptionTile.text(\n          showTopBorder: false,\n          text: LocaleKeys.workspace_errorActions_reportIssueOnGithub.tr(),\n          onTap: () {\n            final String os = Platform.operatingSystem;\n            afLaunchUrlString(\n              'https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&projects=&template=bug_report.yaml&title=[Bug]%20Mobile:%20&version=$version&os=$os',\n            );\n          },\n        ),\n        FlowyOptionTile.text(\n          showTopBorder: false,\n          text: LocaleKeys.workspace_errorActions_exportLogFiles.tr(),\n          onTap: () => shareLogFiles(context),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/application/sign_in_bloc.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/account_deletion.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass UserSessionSettingGroup extends StatelessWidget {\n  const UserSessionSettingGroup({\n    super.key,\n    required this.userProfile,\n    required this.showThirdPartyLogin,\n  });\n\n  final UserProfilePB userProfile;\n  final bool showThirdPartyLogin;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Column(\n      children: [\n        // third party sign in buttons\n        if (showThirdPartyLogin) _buildThirdPartySignInButtons(context),\n        VSpace(theme.spacing.xxl),\n\n        // logout button\n        MobileLogoutButton(\n          text: LocaleKeys.settings_menu_logout.tr(),\n          onPressed: () async => _showLogoutDialog(),\n        ),\n\n        // delete account button\n        // only show the delete account button in cloud mode\n        if (userProfile.userAuthType == AuthTypePB.Server) ...[\n          VSpace(theme.spacing.xxl),\n          AFOutlinedTextButton.destructive(\n            alignment: Alignment.center,\n            text: LocaleKeys.button_deleteAccount.tr(),\n            textStyle: theme.textStyle.body.standard(\n              color: theme.textColorScheme.error,\n            ),\n            onTap: () => _showDeleteAccountDialog(context),\n            size: AFButtonSize.l,\n          ),\n        ],\n\n        VSpace(theme.spacing.xxl),\n      ],\n    );\n  }\n\n  Widget _buildThirdPartySignInButtons(BuildContext context) {\n    return BlocProvider(\n      create: (context) => getIt<SignInBloc>(),\n      child: BlocConsumer<SignInBloc, SignInState>(\n        listener: (context, state) {\n          state.successOrFail?.fold(\n            (result) => runAppFlowy(),\n            (e) => Log.error(e),\n          );\n        },\n        builder: (context, state) {\n          return Column(\n            children: [\n              const ContinueWithEmailAndPassword(),\n              const VSpace(12.0),\n              const ThirdPartySignInButtons(\n                expanded: true,\n              ),\n              const VSpace(16.0),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  Future<void> _showDeleteAccountDialog(BuildContext context) async {\n    return showMobileBottomSheet(\n      context,\n      useRootNavigator: true,\n      backgroundColor: Theme.of(context).colorScheme.surface,\n      builder: (_) => const _DeleteAccountBottomSheet(),\n    );\n  }\n\n  Future<void> _showLogoutDialog() async {\n    return showFlowyCupertinoConfirmDialog(\n      title: LocaleKeys.settings_menu_logoutPrompt.tr(),\n      leftButton: FlowyText(\n        LocaleKeys.button_cancel.tr(),\n        fontSize: 17.0,\n        figmaLineHeight: 24.0,\n        fontWeight: FontWeight.w500,\n        color: const Color(0xFF007AFF),\n      ),\n      rightButton: FlowyText(\n        LocaleKeys.button_logout.tr(),\n        fontSize: 17.0,\n        figmaLineHeight: 24.0,\n        fontWeight: FontWeight.w400,\n        color: const Color(0xFFFE0220),\n      ),\n      onRightButtonPressed: (context) async {\n        Navigator.of(context).pop();\n        await getIt<AuthService>().signOut();\n        await runAppFlowy();\n      },\n    );\n  }\n}\n\nclass _DeleteAccountBottomSheet extends StatefulWidget {\n  const _DeleteAccountBottomSheet();\n\n  @override\n  State<_DeleteAccountBottomSheet> createState() =>\n      _DeleteAccountBottomSheetState();\n}\n\nclass _DeleteAccountBottomSheetState extends State<_DeleteAccountBottomSheet> {\n  final controller = TextEditingController();\n  final isChecked = ValueNotifier(false);\n\n  @override\n  void dispose() {\n    controller.dispose();\n    isChecked.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 24.0),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          const VSpace(18.0),\n          const FlowySvg(\n            FlowySvgs.icon_warning_xl,\n            blendMode: null,\n          ),\n          const VSpace(12.0),\n          FlowyText(\n            LocaleKeys.newSettings_myAccount_deleteAccount_title.tr(),\n            fontSize: 20.0,\n            fontWeight: FontWeight.w500,\n          ),\n          const VSpace(12.0),\n          FlowyText(\n            LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint1.tr(),\n            fontSize: 14.0,\n            fontWeight: FontWeight.w400,\n            maxLines: 10,\n          ),\n          const VSpace(18.0),\n          SizedBox(\n            height: 36.0,\n            child: FlowyTextField(\n              controller: controller,\n              textStyle: const TextStyle(fontSize: 14.0),\n              hintStyle: const TextStyle(fontSize: 14.0),\n              hintText: LocaleKeys\n                  .newSettings_myAccount_deleteAccount_confirmHint3\n                  .tr(),\n            ),\n          ),\n          const VSpace(18.0),\n          _buildCheckbox(),\n          const VSpace(18.0),\n          MobileLogoutButton(\n            text: LocaleKeys.button_deleteAccount.tr(),\n            textColor: Theme.of(context).colorScheme.error,\n            onPressed: () => deleteMyAccount(\n              context,\n              controller.text.trim(),\n              isChecked.value,\n            ),\n          ),\n          const VSpace(12.0),\n          MobileLogoutButton(\n            text: LocaleKeys.button_cancel.tr(),\n            onPressed: () => Navigator.of(context).pop(),\n          ),\n          const VSpace(36.0),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildCheckbox() {\n    return Row(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        GestureDetector(\n          onTap: () => isChecked.value = !isChecked.value,\n          child: ValueListenableBuilder<bool>(\n            valueListenable: isChecked,\n            builder: (context, isChecked, _) {\n              return Padding(\n                padding: const EdgeInsets.all(1.0),\n                child: FlowySvg(\n                  isChecked ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,\n                  size: const Size.square(16.0),\n                  blendMode: isChecked ? null : BlendMode.srcIn,\n                ),\n              );\n            },\n          ),\n        ),\n        const HSpace(6.0),\n        Expanded(\n          child: FlowyText.regular(\n            LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint2.tr(),\n            fontSize: 14.0,\n            figmaLineHeight: 18.0,\n            maxLines: 3,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_group_widget.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileSettingGroup extends StatelessWidget {\n  const MobileSettingGroup({\n    required this.groupTitle,\n    required this.settingItemList,\n    this.showDivider = true,\n    super.key,\n  });\n\n  final String groupTitle;\n  final List<Widget> settingItemList;\n  final bool showDivider;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        VSpace(theme.spacing.s),\n        Text(\n          groupTitle,\n          style: theme.textStyle.heading4.enhanced(\n            color: theme.textColorScheme.primary,\n          ),\n        ),\n        VSpace(theme.spacing.s),\n        ...settingItemList,\n        showDivider\n            ? AFDivider(spacing: theme.spacing.m)\n            : const SizedBox.shrink(),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileSettingItem extends StatelessWidget {\n  const MobileSettingItem({\n    super.key,\n    this.name,\n    this.padding = const EdgeInsets.only(bottom: 4),\n    this.trailing,\n    this.leadingIcon,\n    this.title,\n    this.subtitle,\n    this.onTap,\n  });\n\n  final String? name;\n  final EdgeInsets padding;\n  final Widget? trailing;\n  final Widget? leadingIcon;\n  final Widget? subtitle;\n  final VoidCallback? onTap;\n  final Widget? title;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: padding,\n      child: ListTile(\n        title: title ?? _buildDefaultTitle(context, name),\n        subtitle: subtitle,\n        trailing: trailing,\n        onTap: onTap,\n        visualDensity: VisualDensity.compact,\n        contentPadding: EdgeInsets.zero,\n      ),\n    );\n  }\n\n  Widget _buildDefaultTitle(BuildContext context, String? name) {\n    final theme = AppFlowyTheme.of(context);\n    return Row(\n      children: [\n        if (leadingIcon != null) ...[\n          leadingIcon!,\n          const HSpace(8),\n        ],\n        Expanded(\n          child: Text(\n            name ?? '',\n            style: theme.textStyle.heading4.standard(\n              color: theme.textColorScheme.primary,\n            ),\n            overflow: TextOverflow.ellipsis,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_trailing.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileSettingTrailing extends StatelessWidget {\n  const MobileSettingTrailing({\n    super.key,\n    required this.text,\n    this.showArrow = true,\n  });\n\n  final String text;\n  final bool showArrow;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        Flexible(\n          child: Text(\n            text,\n            style: theme.textStyle.heading4.standard(\n              color: theme.textColorScheme.secondary,\n            ),\n            overflow: TextOverflow.ellipsis,\n          ),\n        ),\n        if (showArrow) ...[\n          const HSpace(8),\n          FlowySvg(\n            FlowySvgs.toolbar_arrow_right_m,\n            size: Size.square(24),\n            color: theme.iconColorScheme.tertiary,\n          ),\n        ],\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/widgets.dart",
    "content": "export 'mobile_setting_group_widget.dart';\nexport 'mobile_setting_item_widget.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/add_members_screen.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/shared/af_role_pb_extension.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/invitation/m_invite_member_by_email.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'member_list.dart';\n\nclass AddMembersScreen extends StatelessWidget {\n  const AddMembersScreen({\n    super.key,\n  });\n\n  static const routeName = '/add_member';\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: FlowyAppBar(\n        titleText: 'Add members',\n      ),\n      body: const _InviteMemberPage(),\n      resizeToAvoidBottomInset: false,\n    );\n  }\n}\n\nclass _InviteMemberPage extends StatefulWidget {\n  const _InviteMemberPage();\n\n  @override\n  State<_InviteMemberPage> createState() => _InviteMemberPageState();\n}\n\nclass _InviteMemberPageState extends State<_InviteMemberPage> {\n  final emailController = TextEditingController();\n  late final Future<UserProfilePB?> userProfile;\n  bool exceededLimit = false;\n\n  @override\n  void initState() {\n    super.initState();\n    userProfile = UserBackendService.getCurrentUserProfile().fold(\n      (s) => s,\n      (f) => null,\n    );\n  }\n\n  @override\n  void dispose() {\n    emailController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return FutureBuilder(\n      future: userProfile,\n      builder: (context, snapshot) {\n        if (snapshot.connectionState == ConnectionState.waiting) {\n          return const SizedBox.shrink();\n        }\n        if (snapshot.hasError || snapshot.data == null) {\n          return _buildError(context);\n        }\n\n        final userProfile = snapshot.data!;\n\n        return BlocProvider<WorkspaceMemberBloc>(\n          create: (context) => WorkspaceMemberBloc(userProfile: userProfile)\n            ..add(const WorkspaceMemberEvent.initial())\n            ..add(const WorkspaceMemberEvent.getInviteCode()),\n          child: BlocConsumer<WorkspaceMemberBloc, WorkspaceMemberState>(\n            listener: _onListener,\n            builder: (context, state) {\n              return Column(\n                children: [\n                  if (state.myRole.isOwner) ...[\n                    Container(\n                      width: double.infinity,\n                      padding: EdgeInsets.all(theme.spacing.xl),\n                      child: const MInviteMemberByEmail(),\n                    ),\n                    VSpace(theme.spacing.m),\n                  ],\n                  if (state.members.isNotEmpty) ...[\n                    const AFDivider(),\n                    VSpace(theme.spacing.xl),\n                    MobileMemberList(\n                      members: state.members,\n                      userProfile: userProfile,\n                      myRole: state.myRole,\n                    ),\n                  ],\n                  if (state.myRole.isMember) ...[\n                    Spacer(),\n                    const _LeaveWorkspaceButton(),\n                  ],\n                  const VSpace(48),\n                ],\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildError(BuildContext context) {\n    return Center(\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 48.0),\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            FlowyText.medium(\n              LocaleKeys.settings_appearance_members_workspaceMembersError.tr(),\n              fontSize: 18.0,\n              textAlign: TextAlign.center,\n            ),\n            const VSpace(8.0),\n            FlowyText.regular(\n              LocaleKeys\n                  .settings_appearance_members_workspaceMembersErrorDescription\n                  .tr(),\n              fontSize: 17.0,\n              maxLines: 10,\n              textAlign: TextAlign.center,\n              lineHeight: 1.3,\n              color: Theme.of(context).hintColor,\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  void _onListener(BuildContext context, WorkspaceMemberState state) {\n    final actionResult = state.actionResult;\n    if (actionResult == null) {\n      return;\n    }\n\n    final actionType = actionResult.actionType;\n    final result = actionResult.result;\n\n    // only show the result dialog when the action is WorkspaceMemberActionType.add\n    if (actionType == WorkspaceMemberActionType.addByEmail) {\n      result.fold(\n        (s) {\n          showToastNotification(\n            message:\n                LocaleKeys.settings_appearance_members_addMemberSuccess.tr(),\n          );\n        },\n        (f) {\n          Log.error('add workspace member failed: $f');\n          final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded\n              ? LocaleKeys\n                  .settings_appearance_members_inviteFailedMemberLimitMobile\n                  .tr()\n              : LocaleKeys.settings_appearance_members_failedToAddMember.tr();\n          setState(() {\n            exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;\n          });\n          showToastNotification(\n            type: ToastificationType.error,\n            message: message,\n          );\n        },\n      );\n    } else if (actionType == WorkspaceMemberActionType.inviteByEmail) {\n      result.fold(\n        (s) {\n          showToastNotification(\n            message:\n                LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(),\n          );\n        },\n        (f) {\n          Log.error('invite workspace member failed: $f');\n          final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded\n              ? LocaleKeys\n                  .settings_appearance_members_inviteFailedMemberLimitMobile\n                  .tr()\n              : LocaleKeys.settings_appearance_members_failedToInviteMember\n                  .tr();\n          setState(() {\n            exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;\n          });\n          showToastNotification(\n            type: ToastificationType.error,\n            message: message,\n          );\n        },\n      );\n    } else if (actionType == WorkspaceMemberActionType.removeByEmail) {\n      result.fold(\n        (s) {\n          showToastNotification(\n            message: LocaleKeys\n                .settings_appearance_members_removeFromWorkspaceSuccess\n                .tr(),\n          );\n        },\n        (f) {\n          showToastNotification(\n            type: ToastificationType.error,\n            message: LocaleKeys\n                .settings_appearance_members_removeFromWorkspaceFailed\n                .tr(),\n          );\n        },\n      );\n    } else if (actionType == WorkspaceMemberActionType.generateInviteLink) {\n      result.fold(\n        (s) async {\n          showToastNotification(\n            message: LocaleKeys\n                .settings_appearance_members_generatedLinkSuccessfully\n                .tr(),\n          );\n\n          // copy the invite link to the clipboard\n          final inviteLink = state.inviteLink;\n          if (inviteLink != null) {\n            await getIt<ClipboardService>().setPlainText(inviteLink);\n            showToastNotification(\n              message: LocaleKeys.shareAction_copyLinkSuccess.tr(),\n            );\n          }\n        },\n        (f) {\n          Log.error('generate invite link failed: $f');\n          showToastNotification(\n            type: ToastificationType.error,\n            message:\n                LocaleKeys.settings_appearance_members_generatedLinkFailed.tr(),\n          );\n        },\n      );\n    } else if (actionType == WorkspaceMemberActionType.resetInviteLink) {\n      result.fold(\n        (s) async {\n          showToastNotification(\n            message: LocaleKeys\n                .settings_appearance_members_resetLinkSuccessfully\n                .tr(),\n          );\n\n          // copy the invite link to the clipboard\n          final inviteLink = state.inviteLink;\n          if (inviteLink != null) {\n            await getIt<ClipboardService>().setPlainText(inviteLink);\n            showToastNotification(\n              message: LocaleKeys.shareAction_copyLinkSuccess.tr(),\n            );\n          }\n        },\n        (f) {\n          Log.error('generate invite link failed: $f');\n          showToastNotification(\n            type: ToastificationType.error,\n            message:\n                LocaleKeys.settings_appearance_members_resetLinkFailed.tr(),\n          );\n        },\n      );\n    }\n  }\n}\n\nclass _LeaveWorkspaceButton extends StatelessWidget {\n  const _LeaveWorkspaceButton();\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 16),\n      child: AFOutlinedTextButton.destructive(\n        alignment: Alignment.center,\n        text: LocaleKeys.workspace_leaveCurrentWorkspace.tr(),\n        onTap: () => _leaveWorkspace(context),\n        size: AFButtonSize.l,\n      ),\n    );\n  }\n\n  void _leaveWorkspace(BuildContext context) {\n    showFlowyCupertinoConfirmDialog(\n      title: LocaleKeys.workspace_leaveCurrentWorkspacePrompt.tr(),\n      leftButton: FlowyText(\n        LocaleKeys.button_cancel.tr(),\n        fontSize: 17.0,\n        figmaLineHeight: 24.0,\n        fontWeight: FontWeight.w500,\n        color: const Color(0xFF007AFF),\n      ),\n      rightButton: FlowyText(\n        LocaleKeys.button_confirm.tr(),\n        fontSize: 17.0,\n        figmaLineHeight: 24.0,\n        fontWeight: FontWeight.w400,\n        color: const Color(0xFFFE0220),\n      ),\n      onRightButtonPressed: (buttonContext) async {},\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_member_by_link.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';\nimport 'package:appflowy/shared/af_role_pb_extension.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:string_validator/string_validator.dart';\n\nimport 'member_list.dart';\n\nValueNotifier<int> mobileLeaveWorkspaceNotifier = ValueNotifier(0);\n\nclass InviteMembersScreen extends StatelessWidget {\n  const InviteMembersScreen({\n    super.key,\n  });\n\n  static const routeName = '/invite_member';\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: FlowyAppBar(\n        titleText: LocaleKeys.settings_appearance_members_label.tr(),\n      ),\n      body: const _InviteMemberPage(),\n      resizeToAvoidBottomInset: false,\n    );\n  }\n}\n\nclass _InviteMemberPage extends StatefulWidget {\n  const _InviteMemberPage();\n\n  @override\n  State<_InviteMemberPage> createState() => _InviteMemberPageState();\n}\n\nclass _InviteMemberPageState extends State<_InviteMemberPage> {\n  final emailController = TextEditingController();\n  late final Future<UserProfilePB?> userProfile;\n  bool exceededLimit = false;\n\n  @override\n  void initState() {\n    super.initState();\n    userProfile = UserBackendService.getCurrentUserProfile().fold(\n      (s) => s,\n      (f) => null,\n    );\n  }\n\n  @override\n  void dispose() {\n    emailController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return FutureBuilder(\n      future: userProfile,\n      builder: (context, snapshot) {\n        if (snapshot.connectionState == ConnectionState.waiting) {\n          return const SizedBox.shrink();\n        }\n        if (snapshot.hasError || snapshot.data == null) {\n          return _buildError(context);\n        }\n\n        final userProfile = snapshot.data!;\n\n        return BlocProvider<WorkspaceMemberBloc>(\n          create: (context) => WorkspaceMemberBloc(userProfile: userProfile)\n            ..add(const WorkspaceMemberEvent.initial())\n            ..add(const WorkspaceMemberEvent.getInviteCode()),\n          child: BlocConsumer<WorkspaceMemberBloc, WorkspaceMemberState>(\n            listener: _onListener,\n            builder: (context, state) {\n              return Column(\n                children: [\n                  Expanded(\n                    child: Column(\n                      mainAxisSize: MainAxisSize.min,\n                      children: [\n                        if (state.myRole.isOwner) ...[\n                          Padding(\n                            padding: EdgeInsets.all(theme.spacing.xl),\n                            child: _buildInviteMemberArea(context),\n                          ),\n                          const VSpace(16),\n                        ],\n                        if (state.members.isNotEmpty) ...[\n                          const AFDivider(),\n                          VSpace(theme.spacing.xl),\n                          MobileMemberList(\n                            members: state.members,\n                            userProfile: userProfile,\n                            myRole: state.myRole,\n                          ),\n                        ],\n                      ],\n                    ),\n                  ),\n                  if (state.myRole.isMember) const _LeaveWorkspaceButton(),\n                  const VSpace(48),\n                ],\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildInviteMemberArea(BuildContext context) {\n    return Column(\n      children: [\n        TextFormField(\n          autofocus: true,\n          controller: emailController,\n          keyboardType: TextInputType.text,\n          decoration: InputDecoration(\n            hintText: LocaleKeys.settings_appearance_members_inviteHint.tr(),\n          ),\n        ),\n        const VSpace(16),\n        if (exceededLimit) ...[\n          FlowyText.regular(\n            LocaleKeys.settings_appearance_members_inviteFailedMemberLimitMobile\n                .tr(),\n            fontSize: 14.0,\n            maxLines: 3,\n            color: Theme.of(context).colorScheme.error,\n          ),\n          const VSpace(16),\n        ],\n        SizedBox(\n          width: double.infinity,\n          child: ElevatedButton(\n            onPressed: () => _inviteMember(context),\n            child: Text(\n              LocaleKeys.settings_appearance_members_sendInvite.tr(),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildError(BuildContext context) {\n    return Center(\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 48.0),\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            FlowyText.medium(\n              LocaleKeys.settings_appearance_members_workspaceMembersError.tr(),\n              fontSize: 18.0,\n              textAlign: TextAlign.center,\n            ),\n            const VSpace(8.0),\n            FlowyText.regular(\n              LocaleKeys\n                  .settings_appearance_members_workspaceMembersErrorDescription\n                  .tr(),\n              fontSize: 17.0,\n              maxLines: 10,\n              textAlign: TextAlign.center,\n              lineHeight: 1.3,\n              color: Theme.of(context).hintColor,\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  void _onListener(BuildContext context, WorkspaceMemberState state) {\n    final actionResult = state.actionResult;\n    if (actionResult == null) {\n      return;\n    }\n\n    final actionType = actionResult.actionType;\n    final result = actionResult.result;\n\n    // get keyboard height\n    final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;\n\n    // only show the result dialog when the action is WorkspaceMemberActionType.add\n    if (actionType == WorkspaceMemberActionType.addByEmail) {\n      result.fold(\n        (s) {\n          showToastNotification(\n            message:\n                LocaleKeys.settings_appearance_members_addMemberSuccess.tr(),\n            bottomPadding: keyboardHeight,\n          );\n        },\n        (f) {\n          Log.error('add workspace member failed: $f');\n          final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded\n              ? LocaleKeys\n                  .settings_appearance_members_inviteFailedMemberLimitMobile\n                  .tr()\n              : LocaleKeys.settings_appearance_members_failedToAddMember.tr();\n          setState(() {\n            exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;\n          });\n          showToastNotification(\n            type: ToastificationType.error,\n            bottomPadding: keyboardHeight,\n            message: message,\n          );\n        },\n      );\n    } else if (actionType == WorkspaceMemberActionType.inviteByEmail) {\n      result.fold(\n        (s) {\n          showToastNotification(\n            message:\n                LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(),\n            bottomPadding: keyboardHeight,\n          );\n        },\n        (f) {\n          Log.error('invite workspace member failed: $f');\n          final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded\n              ? LocaleKeys\n                  .settings_appearance_members_inviteFailedMemberLimitMobile\n                  .tr()\n              : LocaleKeys.settings_appearance_members_failedToInviteMember\n                  .tr();\n          setState(() {\n            exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;\n          });\n          showToastNotification(\n            type: ToastificationType.error,\n            message: message,\n            bottomPadding: keyboardHeight,\n          );\n        },\n      );\n    } else if (actionType == WorkspaceMemberActionType.removeByEmail) {\n      result.fold(\n        (s) {\n          showToastNotification(\n            message: LocaleKeys\n                .settings_appearance_members_removeFromWorkspaceSuccess\n                .tr(),\n            bottomPadding: keyboardHeight,\n          );\n        },\n        (f) {\n          showToastNotification(\n            type: ToastificationType.error,\n            message: LocaleKeys\n                .settings_appearance_members_removeFromWorkspaceFailed\n                .tr(),\n            bottomPadding: keyboardHeight,\n          );\n        },\n      );\n    }\n  }\n\n  void _inviteMember(BuildContext context) {\n    final email = emailController.text;\n    if (!isEmail(email)) {\n      showToastNotification(\n        type: ToastificationType.error,\n        message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(),\n      );\n      return;\n    }\n    context\n        .read<WorkspaceMemberBloc>()\n        .add(WorkspaceMemberEvent.inviteWorkspaceMemberByEmail(email));\n    // clear the email field after inviting\n    emailController.clear();\n  }\n}\n\nclass _LeaveWorkspaceButton extends StatelessWidget {\n  const _LeaveWorkspaceButton();\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      width: double.infinity,\n      margin: const EdgeInsets.symmetric(horizontal: 16),\n      child: ElevatedButton(\n        style: ElevatedButton.styleFrom(\n          backgroundColor: Colors.transparent,\n          foregroundColor: Theme.of(context).colorScheme.error,\n          shape: RoundedRectangleBorder(\n            borderRadius: BorderRadius.circular(4),\n            side: BorderSide(\n              color: Theme.of(context).colorScheme.error,\n              width: 0.5,\n            ),\n          ),\n        ),\n        onPressed: () => _leaveWorkspace(context),\n        child: FlowyText(\n          LocaleKeys.workspace_leaveCurrentWorkspace.tr(),\n          fontSize: 14.0,\n          color: Theme.of(context).colorScheme.error,\n          fontWeight: FontWeight.w500,\n        ),\n      ),\n    );\n  }\n\n  void _leaveWorkspace(BuildContext context) {\n    showFlowyCupertinoConfirmDialog(\n      title: LocaleKeys.workspace_leaveCurrentWorkspacePrompt.tr(),\n      leftButton: FlowyText(\n        LocaleKeys.button_cancel.tr(),\n        fontSize: 17.0,\n        figmaLineHeight: 24.0,\n        fontWeight: FontWeight.w500,\n        color: const Color(0xFF007AFF),\n      ),\n      rightButton: FlowyText(\n        LocaleKeys.button_confirm.tr(),\n        fontSize: 17.0,\n        figmaLineHeight: 24.0,\n        fontWeight: FontWeight.w400,\n        color: const Color(0xFFFE0220),\n      ),\n      onRightButtonPressed: (buttonContext) async {\n        // try to use popUntil with a specific route name but failed\n        // so use pop twice as a workaround\n        Navigator.of(buttonContext).pop();\n        Navigator.of(context).pop();\n        Navigator.of(context).pop();\n\n        mobileLeaveWorkspaceNotifier.value =\n            mobileLeaveWorkspaceNotifier.value + 1;\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/mobile/presentation/setting/workspace/add_members_screen.dart';\nimport 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/shared/af_role_pb_extension.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/invitation/m_invite_member_by_link.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nimport 'member_list.dart';\n\nValueNotifier<int> mobileLeaveWorkspaceNotifier = ValueNotifier(0);\n\nclass InviteMembersScreen extends StatelessWidget {\n  const InviteMembersScreen({\n    super.key,\n  });\n\n  static const routeName = '/invite_member';\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: FlowyAppBar(\n        titleText: LocaleKeys.settings_appearance_members_label.tr(),\n        actions: [\n          _buildAddMemberButton(context),\n        ],\n      ),\n      body: const _InviteMemberPage(),\n      resizeToAvoidBottomInset: false,\n    );\n  }\n\n  Widget _buildAddMemberButton(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.only(right: 20),\n      child: GestureDetector(\n        onTap: () {\n          context.push(AddMembersScreen.routeName);\n        },\n        child: FlowySvg(FlowySvgs.add_thin_s),\n      ),\n    );\n  }\n}\n\nclass _InviteMemberPage extends StatefulWidget {\n  const _InviteMemberPage();\n\n  @override\n  State<_InviteMemberPage> createState() => _InviteMemberPageState();\n}\n\nclass _InviteMemberPageState extends State<_InviteMemberPage> {\n  final emailController = TextEditingController();\n  late final Future<UserProfilePB?> userProfile;\n  bool exceededLimit = false;\n\n  @override\n  void initState() {\n    super.initState();\n    userProfile = UserBackendService.getCurrentUserProfile().fold(\n      (s) => s,\n      (f) => null,\n    );\n  }\n\n  @override\n  void dispose() {\n    emailController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return FutureBuilder(\n      future: userProfile,\n      builder: (context, snapshot) {\n        if (snapshot.connectionState == ConnectionState.waiting) {\n          return const SizedBox.shrink();\n        }\n        if (snapshot.hasError || snapshot.data == null) {\n          return _buildError(context);\n        }\n\n        final userProfile = snapshot.data!;\n\n        return BlocProvider<WorkspaceMemberBloc>(\n          create: (context) => WorkspaceMemberBloc(userProfile: userProfile)\n            ..add(const WorkspaceMemberEvent.initial())\n            ..add(const WorkspaceMemberEvent.getInviteCode()),\n          child: BlocConsumer<WorkspaceMemberBloc, WorkspaceMemberState>(\n            listener: _onListener,\n            builder: (context, state) {\n              return SingleChildScrollView(\n                child: Column(\n                  children: [\n                    if (state.myRole.isOwner) ...[\n                      Container(\n                        width: double.infinity,\n                        padding: EdgeInsets.all(theme.spacing.xl),\n                        child: const MInviteMemberByLink(),\n                      ),\n                      VSpace(theme.spacing.m),\n                    ],\n                    if (state.members.isNotEmpty) ...[\n                      const AFDivider(),\n                      VSpace(theme.spacing.xl),\n                      MobileMemberList(\n                        members: state.members,\n                        userProfile: userProfile,\n                        myRole: state.myRole,\n                      ),\n                    ],\n                    if (state.myRole.isMember) ...[\n                      Spacer(),\n                      const _LeaveWorkspaceButton(),\n                    ],\n                    const VSpace(48),\n                  ],\n                ),\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildError(BuildContext context) {\n    return Center(\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 48.0),\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            FlowyText.medium(\n              LocaleKeys.settings_appearance_members_workspaceMembersError.tr(),\n              fontSize: 18.0,\n              textAlign: TextAlign.center,\n            ),\n            const VSpace(8.0),\n            FlowyText.regular(\n              LocaleKeys\n                  .settings_appearance_members_workspaceMembersErrorDescription\n                  .tr(),\n              fontSize: 17.0,\n              maxLines: 10,\n              textAlign: TextAlign.center,\n              lineHeight: 1.3,\n              color: Theme.of(context).hintColor,\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  void _onListener(BuildContext context, WorkspaceMemberState state) {\n    final actionResult = state.actionResult;\n    if (actionResult == null) {\n      return;\n    }\n\n    final actionType = actionResult.actionType;\n    final result = actionResult.result;\n\n    // only show the result dialog when the action is WorkspaceMemberActionType.add\n    if (actionType == WorkspaceMemberActionType.addByEmail) {\n      result.fold(\n        (s) {\n          showToastNotification(\n            message:\n                LocaleKeys.settings_appearance_members_addMemberSuccess.tr(),\n          );\n        },\n        (f) {\n          Log.error('add workspace member failed: $f');\n          final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded\n              ? LocaleKeys\n                  .settings_appearance_members_inviteFailedMemberLimitMobile\n                  .tr()\n              : LocaleKeys.settings_appearance_members_failedToAddMember.tr();\n          setState(() {\n            exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;\n          });\n          showToastNotification(\n            type: ToastificationType.error,\n            message: message,\n          );\n        },\n      );\n    } else if (actionType == WorkspaceMemberActionType.inviteByEmail) {\n      result.fold(\n        (s) {\n          showToastNotification(\n            message:\n                LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(),\n          );\n        },\n        (f) {\n          Log.error('invite workspace member failed: $f');\n          final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded\n              ? LocaleKeys\n                  .settings_appearance_members_inviteFailedMemberLimitMobile\n                  .tr()\n              : LocaleKeys.settings_appearance_members_failedToInviteMember\n                  .tr();\n          setState(() {\n            exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded;\n          });\n\n          showToastNotification(\n            type: ToastificationType.error,\n            message: message,\n          );\n        },\n      );\n    } else if (actionType == WorkspaceMemberActionType.removeByEmail) {\n      result.fold(\n        (s) {\n          showToastNotification(\n            message: LocaleKeys\n                .settings_appearance_members_removeFromWorkspaceSuccess\n                .tr(),\n          );\n        },\n        (f) {\n          showToastNotification(\n            type: ToastificationType.error,\n            message: LocaleKeys\n                .settings_appearance_members_removeFromWorkspaceFailed\n                .tr(),\n          );\n        },\n      );\n    } else if (actionType == WorkspaceMemberActionType.generateInviteLink) {\n      result.fold(\n        (s) {\n          showToastNotification(\n            message: LocaleKeys\n                .settings_appearance_members_generatedLinkSuccessfully\n                .tr(),\n          );\n\n          // copy the invite link to the clipboard\n          final inviteLink = state.inviteLink;\n          if (inviteLink != null) {\n            getIt<ClipboardService>().setPlainText(inviteLink);\n            showToastNotification(\n              message: LocaleKeys.shareAction_copyLinkSuccess.tr(),\n            );\n          }\n        },\n        (f) {\n          Log.error('generate invite link failed: $f');\n          showToastNotification(\n            type: ToastificationType.error,\n            message:\n                LocaleKeys.settings_appearance_members_generatedLinkFailed.tr(),\n          );\n        },\n      );\n    } else if (actionType == WorkspaceMemberActionType.resetInviteLink) {\n      result.fold(\n        (s) {\n          showToastNotification(\n            message: LocaleKeys\n                .settings_appearance_members_resetLinkSuccessfully\n                .tr(),\n          );\n\n          // copy the invite link to the clipboard\n          final inviteLink = state.inviteLink;\n          if (inviteLink != null) {\n            getIt<ClipboardService>().setPlainText(inviteLink);\n            showToastNotification(\n              message: LocaleKeys.shareAction_copyLinkSuccess.tr(),\n            );\n          }\n        },\n        (f) {\n          Log.error('generate invite link failed: $f');\n          showToastNotification(\n            type: ToastificationType.error,\n            message:\n                LocaleKeys.settings_appearance_members_resetLinkFailed.tr(),\n          );\n        },\n      );\n    }\n  }\n}\n\nclass _LeaveWorkspaceButton extends StatelessWidget {\n  const _LeaveWorkspaceButton();\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 16),\n      child: AFOutlinedTextButton.destructive(\n        alignment: Alignment.center,\n        text: LocaleKeys.workspace_leaveCurrentWorkspace.tr(),\n        onTap: () => _leaveWorkspace(context),\n        size: AFButtonSize.l,\n      ),\n    );\n  }\n\n  void _leaveWorkspace(BuildContext context) {\n    showFlowyCupertinoConfirmDialog(\n      title: LocaleKeys.workspace_leaveCurrentWorkspacePrompt.tr(),\n      leftButton: FlowyText(\n        LocaleKeys.button_cancel.tr(),\n        fontSize: 17.0,\n        figmaLineHeight: 24.0,\n        fontWeight: FontWeight.w500,\n        color: const Color(0xFF007AFF),\n      ),\n      rightButton: FlowyText(\n        LocaleKeys.button_confirm.tr(),\n        fontSize: 17.0,\n        figmaLineHeight: 24.0,\n        fontWeight: FontWeight.w400,\n        color: const Color(0xFFFE0220),\n      ),\n      onRightButtonPressed: (buttonContext) async {\n        // try to use popUntil with a specific route name but failed\n        // so use pop twice as a workaround\n        Navigator.of(buttonContext).pop();\n        Navigator.of(context).pop();\n        Navigator.of(context).pop();\n\n        mobileLeaveWorkspaceNotifier.value =\n            mobileLeaveWorkspaceNotifier.value + 1;\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/member_list.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/shared/af_role_pb_extension.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_slidable/flutter_slidable.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass MobileMemberList extends StatelessWidget {\n  const MobileMemberList({\n    super.key,\n    required this.members,\n    required this.myRole,\n    required this.userProfile,\n  });\n\n  final List<WorkspaceMemberPB> members;\n  final AFRolePB myRole;\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return SingleChildScrollView(\n      child: SlidableAutoCloseBehavior(\n        child: SeparatedColumn(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          separatorBuilder: () => SizedBox.shrink(),\n          children: [\n            Padding(\n              padding: const EdgeInsets.symmetric(\n                horizontal: 16.0,\n                vertical: 8.0,\n              ),\n              child: Text(\n                'Joined',\n                style: theme.textStyle.heading4.enhanced(\n                  color: theme.textColorScheme.primary,\n                ),\n              ),\n            ),\n            ...members.map(\n              (member) => _MemberItem(\n                member: member,\n                myRole: myRole,\n                userProfile: userProfile,\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _MemberItem extends StatelessWidget {\n  const _MemberItem({\n    required this.member,\n    required this.myRole,\n    required this.userProfile,\n  });\n\n  final WorkspaceMemberPB member;\n  final AFRolePB myRole;\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final canDelete = myRole.canDelete && member.email != userProfile.email;\n\n    Widget child;\n\n    if (UniversalPlatform.isDesktop) {\n      child = Row(\n        children: [\n          Expanded(\n            child: Text(\n              member.name,\n              style: theme.textStyle.heading4.standard(\n                color: theme.textColorScheme.primary,\n              ),\n            ),\n          ),\n          Expanded(\n            child: Text(\n              member.role.description,\n              style: theme.textStyle.heading4.standard(\n                color: theme.textColorScheme.secondary,\n              ),\n              textAlign: TextAlign.end,\n            ),\n          ),\n        ],\n      );\n    } else {\n      child = Row(\n        children: [\n          Expanded(\n            child: Text(\n              member.name,\n              style: theme.textStyle.heading4.standard(\n                color: theme.textColorScheme.primary,\n              ),\n              overflow: TextOverflow.ellipsis,\n            ),\n          ),\n          Text(\n            member.role.description,\n            style: theme.textStyle.heading4.standard(\n              color: theme.textColorScheme.secondary,\n            ),\n            textAlign: TextAlign.end,\n          ),\n        ],\n      );\n    }\n\n    child = Container(\n      padding: EdgeInsets.symmetric(\n        horizontal: theme.spacing.xl,\n        vertical: theme.spacing.l,\n      ),\n      child: child,\n    );\n\n    if (canDelete) {\n      child = Slidable(\n        key: ValueKey(member.email),\n        endActionPane: ActionPane(\n          extentRatio: 1 / 6.0,\n          motion: const ScrollMotion(),\n          children: [\n            CustomSlidableAction(\n              backgroundColor: const Color(0xE5515563),\n              borderRadius: const BorderRadius.only(\n                topLeft: Radius.circular(10),\n                bottomLeft: Radius.circular(10),\n              ),\n              onPressed: (context) {\n                HapticFeedback.mediumImpact();\n                _showDeleteMenu(context);\n              },\n              padding: EdgeInsets.zero,\n              child: const FlowySvg(\n                FlowySvgs.three_dots_s,\n                size: Size.square(24),\n                color: Colors.white,\n              ),\n            ),\n          ],\n        ),\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  void _showDeleteMenu(BuildContext context) {\n    final workspaceMemberBloc = context.read<WorkspaceMemberBloc>();\n    showMobileBottomSheet(\n      context,\n      showDragHandle: true,\n      showDivider: false,\n      useRootNavigator: true,\n      backgroundColor: Theme.of(context).colorScheme.surface,\n      builder: (context) {\n        return FlowyOptionTile.text(\n          text: LocaleKeys.settings_appearance_members_removeFromWorkspace.tr(),\n          height: 52.0,\n          textColor: Theme.of(context).colorScheme.error,\n          leftIcon: FlowySvg(\n            FlowySvgs.trash_s,\n            size: const Size.square(18),\n            color: Theme.of(context).colorScheme.error,\n          ),\n          showTopBorder: false,\n          showBottomBorder: false,\n          onTap: () {\n            workspaceMemberBloc.add(\n              WorkspaceMemberEvent.removeWorkspaceMemberByEmail(\n                member.email,\n              ),\n            );\n            Navigator.of(context).pop();\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/workspace_setting_group.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_trailing.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nimport '../widgets/widgets.dart';\nimport 'invite_members_screen.dart';\n\nclass WorkspaceSettingGroup extends StatelessWidget {\n  const WorkspaceSettingGroup({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(\n      builder: (context, state) {\n        final currentWorkspace = state.workspaces.firstWhereOrNull(\n          (e) => e.workspaceId == state.currentWorkspace?.workspaceId,\n        );\n        final memberCount = currentWorkspace?.memberCount;\n        String memberCountText = '';\n        // if the member count is greater than 0, show the member count\n        if (memberCount != null && memberCount > 0) {\n          memberCountText = memberCount.toString();\n        }\n        return MobileSettingGroup(\n          groupTitle: LocaleKeys.settings_appearance_members_label.tr(),\n          settingItemList: [\n            MobileSettingItem(\n              name: LocaleKeys.settings_appearance_members_label.tr(),\n              trailing: MobileSettingTrailing(\n                text: memberCountText,\n              ),\n              onTap: () {\n                context.push(InviteMembersScreen.routeName);\n              },\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_option_decorate_box.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass FlowyOptionDecorateBox extends StatelessWidget {\n  const FlowyOptionDecorateBox({\n    super.key,\n    this.showTopBorder = true,\n    this.showBottomBorder = true,\n    this.color,\n    required this.child,\n  });\n\n  final bool showTopBorder;\n  final bool showBottomBorder;\n  final Widget child;\n  final Color? color;\n\n  @override\n  Widget build(BuildContext context) {\n    return DecoratedBox(\n      decoration: BoxDecoration(\n        color: color ?? Theme.of(context).colorScheme.surface,\n        border: Border(\n          top: showTopBorder\n              ? BorderSide(\n                  color: Theme.of(context).dividerColor,\n                  width: 0.5,\n                )\n              : BorderSide.none,\n          bottom: showBottomBorder\n              ? BorderSide(\n                  color: Theme.of(context).dividerColor,\n                  width: 0.5,\n                )\n              : BorderSide.none,\n        ),\n      ),\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileQuickActionButton extends StatelessWidget {\n  const MobileQuickActionButton({\n    super.key,\n    required this.onTap,\n    required this.icon,\n    required this.text,\n    this.textColor,\n    this.iconColor,\n    this.iconSize,\n    this.enable = true,\n    this.rightIconBuilder,\n  });\n\n  final VoidCallback onTap;\n  final FlowySvgData icon;\n  final String text;\n  final Color? textColor;\n  final Color? iconColor;\n  final Size? iconSize;\n  final bool enable;\n  final WidgetBuilder? rightIconBuilder;\n\n  @override\n  Widget build(BuildContext context) {\n    final iconSize = this.iconSize ?? const Size.square(18);\n    return Opacity(\n      opacity: enable ? 1.0 : 0.5,\n      child: InkWell(\n        onTap: enable ? onTap : null,\n        overlayColor:\n            enable ? null : const WidgetStatePropertyAll(Colors.transparent),\n        splashColor: Colors.transparent,\n        child: Container(\n          height: 52,\n          padding: const EdgeInsets.symmetric(horizontal: 16),\n          child: Row(\n            children: [\n              FlowySvg(\n                icon,\n                size: iconSize,\n                color: iconColor,\n              ),\n              HSpace(30 - iconSize.width),\n              Expanded(\n                child: FlowyText.regular(\n                  text,\n                  fontSize: 16,\n                  color: textColor,\n                ),\n              ),\n              if (rightIconBuilder != null) rightIconBuilder!(context),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass MobileQuickActionDivider extends StatelessWidget {\n  const MobileQuickActionDivider({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return const Divider(height: 0.5, thickness: 0.5);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_search_text_field.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\n\nclass FlowyMobileSearchTextField extends StatelessWidget {\n  const FlowyMobileSearchTextField({\n    super.key,\n    this.hintText,\n    this.controller,\n    this.onChanged,\n    this.onSubmitted,\n  });\n\n  final String? hintText;\n  final TextEditingController? controller;\n  final ValueChanged<String>? onChanged;\n  final ValueChanged<String>? onSubmitted;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 44.0,\n      child: CupertinoSearchTextField(\n        controller: controller,\n        onChanged: onChanged,\n        onSubmitted: onSubmitted,\n        placeholder: hintText,\n        prefixIcon: const FlowySvg(FlowySvgs.m_search_m),\n        prefixInsets: const EdgeInsets.only(left: 16.0, right: 2.0),\n        suffixIcon: const Icon(Icons.close),\n        suffixInsets: const EdgeInsets.only(right: 16.0),\n        placeholderStyle: Theme.of(context).textTheme.titleSmall?.copyWith(\n              color: Theme.of(context).hintColor,\n              fontWeight: FontWeight.w400,\n              fontSize: 14.0,\n            ),\n        style: Theme.of(context).textTheme.titleSmall?.copyWith(\n              color: Theme.of(context).textTheme.bodyMedium?.color,\n              fontWeight: FontWeight.w400,\n              fontSize: 14.0,\n            ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_state_container.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:package_info_plus/package_info_plus.dart';\n\nenum _FlowyMobileStateContainerType {\n  info,\n  error,\n}\n\n/// Used to display info(like empty state) or error state\n/// error state has two buttons to report issue with error message or reach out on discord\nclass FlowyMobileStateContainer extends StatelessWidget {\n  const FlowyMobileStateContainer.error({\n    this.emoji,\n    required this.title,\n    this.description,\n    required this.errorMsg,\n    super.key,\n  }) : _stateType = _FlowyMobileStateContainerType.error;\n\n  const FlowyMobileStateContainer.info({\n    this.emoji,\n    required this.title,\n    this.description,\n    super.key,\n  })  : errorMsg = null,\n        _stateType = _FlowyMobileStateContainerType.info;\n\n  final String? emoji;\n  final String title;\n  final String? description;\n  final String? errorMsg;\n  final _FlowyMobileStateContainerType _stateType;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = Theme.of(context);\n\n    return SizedBox.expand(\n      child: Padding(\n        padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 32),\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            Text(\n              emoji ??\n                  (_stateType == _FlowyMobileStateContainerType.error\n                      ? '🛸'\n                      : ''),\n              style: const TextStyle(fontSize: 40),\n              textAlign: TextAlign.center,\n            ),\n            const SizedBox(height: 8),\n            Text(\n              title,\n              style: theme.textTheme.labelLarge,\n              textAlign: TextAlign.center,\n            ),\n            const SizedBox(height: 4),\n            Text(\n              description ?? '',\n              style: theme.textTheme.bodyMedium?.copyWith(\n                color: theme.hintColor,\n              ),\n              textAlign: TextAlign.center,\n            ),\n            if (_stateType == _FlowyMobileStateContainerType.error) ...[\n              const SizedBox(height: 8),\n              FutureBuilder(\n                future: PackageInfo.fromPlatform(),\n                builder: (context, snapshot) {\n                  return Column(\n                    crossAxisAlignment: CrossAxisAlignment.stretch,\n                    children: [\n                      OutlinedButton(\n                        onPressed: () {\n                          final String? version = snapshot.data?.version;\n                          final String os = Platform.operatingSystem;\n                          afLaunchUrlString(\n                            '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',\n                          );\n                        },\n                        child: Text(\n                          LocaleKeys.workspace_errorActions_reportIssue.tr(),\n                        ),\n                      ),\n                      OutlinedButton(\n                        onPressed: () =>\n                            afLaunchUrlString('https://discord.gg/JucBXeU2FE'),\n                        child: Text(\n                          LocaleKeys.workspace_errorActions_reachOut.tr(),\n                        ),\n                      ),\n                    ],\n                  );\n                },\n              ),\n            ],\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\n\nenum FlowyOptionTileType {\n  text,\n  textField,\n  checkbox,\n  toggle,\n}\n\nclass FlowyOptionTile extends StatelessWidget {\n  const FlowyOptionTile._({\n    super.key,\n    required this.type,\n    this.showTopBorder = true,\n    this.showBottomBorder = true,\n    this.text,\n    this.textColor,\n    this.controller,\n    this.leading,\n    this.onTap,\n    this.trailing,\n    this.textFieldPadding = const EdgeInsets.symmetric(\n      horizontal: 12.0,\n      vertical: 2.0,\n    ),\n    this.isSelected = false,\n    this.onValueChanged,\n    this.textFieldHintText,\n    this.onTextChanged,\n    this.onTextSubmitted,\n    this.autofocus,\n    this.content,\n    this.backgroundColor,\n    this.fontFamily,\n    this.height,\n    this.enable = true,\n  });\n\n  factory FlowyOptionTile.text({\n    String? text,\n    Widget? content,\n    Color? textColor,\n    bool showTopBorder = true,\n    bool showBottomBorder = true,\n    Widget? leftIcon,\n    Widget? trailing,\n    VoidCallback? onTap,\n    double? height,\n    bool enable = true,\n  }) {\n    return FlowyOptionTile._(\n      type: FlowyOptionTileType.text,\n      text: text,\n      content: content,\n      textColor: textColor,\n      onTap: onTap,\n      showTopBorder: showTopBorder,\n      showBottomBorder: showBottomBorder,\n      leading: leftIcon,\n      trailing: trailing,\n      height: height,\n      enable: enable,\n    );\n  }\n\n  factory FlowyOptionTile.textField({\n    required TextEditingController controller,\n    void Function(String value)? onTextChanged,\n    void Function(String value)? onTextSubmitted,\n    EdgeInsets textFieldPadding = const EdgeInsets.symmetric(\n      vertical: 16.0,\n    ),\n    bool showTopBorder = true,\n    bool showBottomBorder = true,\n    Widget? leftIcon,\n    Widget? trailing,\n    String? textFieldHintText,\n    bool autofocus = false,\n    bool enable = true,\n  }) {\n    return FlowyOptionTile._(\n      type: FlowyOptionTileType.textField,\n      controller: controller,\n      textFieldPadding: textFieldPadding,\n      showTopBorder: showTopBorder,\n      showBottomBorder: showBottomBorder,\n      leading: leftIcon,\n      trailing: trailing,\n      textFieldHintText: textFieldHintText,\n      onTextChanged: onTextChanged,\n      onTextSubmitted: onTextSubmitted,\n      autofocus: autofocus,\n      enable: enable,\n    );\n  }\n\n  factory FlowyOptionTile.checkbox({\n    Key? key,\n    required String text,\n    required bool isSelected,\n    required VoidCallback? onTap,\n    Color? textColor,\n    Widget? leftIcon,\n    Widget? content,\n    bool showTopBorder = true,\n    bool showBottomBorder = true,\n    String? fontFamily,\n    Color? backgroundColor,\n    bool enable = true,\n  }) {\n    return FlowyOptionTile._(\n      key: key,\n      type: FlowyOptionTileType.checkbox,\n      isSelected: isSelected,\n      text: text,\n      textColor: textColor,\n      content: content,\n      onTap: onTap,\n      fontFamily: fontFamily,\n      backgroundColor: backgroundColor,\n      showTopBorder: showTopBorder,\n      showBottomBorder: showBottomBorder,\n      leading: leftIcon,\n      enable: enable,\n      trailing: isSelected\n          ? const FlowySvg(\n              FlowySvgs.m_blue_check_s,\n              blendMode: null,\n            )\n          : null,\n    );\n  }\n\n  factory FlowyOptionTile.toggle({\n    required String text,\n    required bool isSelected,\n    required void Function(bool value) onValueChanged,\n    void Function()? onTap,\n    bool showTopBorder = true,\n    bool showBottomBorder = true,\n    Widget? leftIcon,\n    bool enable = true,\n  }) {\n    return FlowyOptionTile._(\n      type: FlowyOptionTileType.toggle,\n      text: text,\n      onTap: onTap ?? () => onValueChanged(!isSelected),\n      onValueChanged: onValueChanged,\n      showTopBorder: showTopBorder,\n      showBottomBorder: showBottomBorder,\n      leading: leftIcon,\n      trailing: _Toggle(value: isSelected, onChanged: onValueChanged),\n      enable: enable,\n    );\n  }\n\n  final bool showTopBorder;\n  final bool showBottomBorder;\n  final String? text;\n  final Color? textColor;\n  final TextEditingController? controller;\n  final EdgeInsets textFieldPadding;\n  final void Function()? onTap;\n  final Widget? leading;\n  final Widget? trailing;\n\n  // customize the content widget\n  final Widget? content;\n\n  // only used in checkbox or switcher\n  final bool isSelected;\n\n  // only used in switcher\n  final void Function(bool value)? onValueChanged;\n\n  // only used in textfield\n  final String? textFieldHintText;\n  final void Function(String value)? onTextChanged;\n  final void Function(String value)? onTextSubmitted;\n  final bool? autofocus;\n\n  final FlowyOptionTileType type;\n\n  final Color? backgroundColor;\n  final String? fontFamily;\n\n  final double? height;\n\n  final bool enable;\n\n  @override\n  Widget build(BuildContext context) {\n    final leadingWidget = _buildLeading();\n\n    Widget child = FlowyOptionDecorateBox(\n      color: backgroundColor,\n      showTopBorder: showTopBorder,\n      showBottomBorder: showBottomBorder,\n      child: SizedBox(\n        height: height,\n        child: Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 16.0),\n          child: Row(\n            children: [\n              if (leadingWidget != null) leadingWidget,\n              if (content != null) content!,\n              if (content == null) _buildText(),\n              if (content == null) _buildTextField(),\n              if (trailing != null) trailing!,\n            ],\n          ),\n        ),\n      ),\n    );\n\n    if (type == FlowyOptionTileType.checkbox ||\n        type == FlowyOptionTileType.toggle ||\n        type == FlowyOptionTileType.text) {\n      child = GestureDetector(\n        onTap: onTap,\n        child: child,\n      );\n    }\n\n    if (!enable) {\n      child = Opacity(\n        opacity: 0.5,\n        child: IgnorePointer(\n          child: child,\n        ),\n      );\n    }\n\n    return child;\n  }\n\n  Widget? _buildLeading() {\n    if (leading != null) {\n      return Center(child: leading);\n    } else {\n      return null;\n    }\n  }\n\n  Widget _buildText() {\n    if (text == null || type == FlowyOptionTileType.textField) {\n      return const SizedBox.shrink();\n    }\n\n    final padding = EdgeInsets.symmetric(\n      horizontal: leading == null ? 0.0 : 12.0,\n      vertical: 14.0,\n    );\n\n    return Expanded(\n      child: Padding(\n        padding: padding,\n        child: FlowyText(\n          text!,\n          fontSize: 16,\n          color: textColor,\n          fontFamily: fontFamily,\n        ),\n      ),\n    );\n  }\n\n  Widget _buildTextField() {\n    if (controller == null) {\n      return const SizedBox.shrink();\n    }\n\n    return Expanded(\n      child: Container(\n        constraints: const BoxConstraints.tightFor(\n          height: 54.0,\n        ),\n        alignment: Alignment.center,\n        child: TextField(\n          controller: controller,\n          autofocus: autofocus ?? false,\n          textInputAction: TextInputAction.done,\n          decoration: InputDecoration(\n            border: InputBorder.none,\n            enabledBorder: InputBorder.none,\n            focusedBorder: InputBorder.none,\n            contentPadding: textFieldPadding,\n            hintText: textFieldHintText,\n          ),\n          onChanged: onTextChanged,\n          onSubmitted: onTextSubmitted,\n        ),\n      ),\n    );\n  }\n}\n\nclass _Toggle extends StatelessWidget {\n  const _Toggle({\n    required this.value,\n    required this.onChanged,\n  });\n\n  final bool value;\n  final void Function(bool value) onChanged;\n\n  @override\n  Widget build(BuildContext context) {\n    // CupertinoSwitch adds a 8px margin all around. The original size of the\n    // switch is 38 x 22.\n    return SizedBox(\n      width: 46,\n      height: 30,\n      child: FittedBox(\n        fit: BoxFit.fill,\n        child: CupertinoSwitch(\n          value: value,\n          activeTrackColor: Theme.of(context).colorScheme.primary,\n          onChanged: onChanged,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/widgets/navigation_bar_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass NavigationBarButton extends StatelessWidget {\n  const NavigationBarButton({\n    super.key,\n    required this.text,\n    required this.icon,\n    required this.onTap,\n    this.enable = true,\n  });\n\n  final String text;\n  final FlowySvgData icon;\n  final VoidCallback onTap;\n  final bool enable;\n\n  @override\n  Widget build(BuildContext context) {\n    return Opacity(\n      opacity: enable ? 1.0 : 0.3,\n      child: Container(\n        height: 40,\n        decoration: ShapeDecoration(\n          shape: RoundedRectangleBorder(\n            side: const BorderSide(color: Color(0x3F1F2329)),\n            borderRadius: BorderRadius.circular(10),\n          ),\n        ),\n        child: FlowyButton(\n          useIntrinsicWidth: true,\n          expandText: false,\n          iconPadding: 8,\n          leftIcon: FlowySvg(icon),\n          onTap: enable ? onTap : null,\n          text: FlowyText(\n            text,\n            fontSize: 15.0,\n            figmaLineHeight: 18.0,\n            fontWeight: FontWeight.w400,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/tasks/app_widget.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\n\nenum ConfirmDialogActionAlignment {\n  // The action buttons are aligned vertically\n  // ---------------------\n  // |  Action Button    |\n  // |  Cancel Button    |\n  vertical,\n  // The action buttons are aligned horizontally\n  // ---------------------\n  // |  Action Button    |  Cancel Button    |\n  horizontal,\n}\n\n/// show the dialog to confirm one single action\n/// [onActionButtonPressed] and [onCancelButtonPressed] end with close the dialog\nFuture<T?> showFlowyMobileConfirmDialog<T>(\n  BuildContext context, {\n  Widget? title,\n  Widget? content,\n  ConfirmDialogActionAlignment actionAlignment =\n      ConfirmDialogActionAlignment.horizontal,\n  required String actionButtonTitle,\n  required VoidCallback? onActionButtonPressed,\n  Color? actionButtonColor,\n  String? cancelButtonTitle,\n  Color? cancelButtonColor,\n  VoidCallback? onCancelButtonPressed,\n}) async {\n  return showDialog(\n    context: context,\n    builder: (dialogContext) {\n      final foregroundColor = Theme.of(context).colorScheme.onSurface;\n      final actionButton = TextButton(\n        child: FlowyText(\n          actionButtonTitle,\n          color: actionButtonColor ?? foregroundColor,\n        ),\n        onPressed: () {\n          onActionButtonPressed?.call();\n          // we cannot use dialogContext.pop() here because this is no GoRouter in dialogContext. Use Navigator instead to close the dialog.\n          Navigator.of(dialogContext).pop();\n        },\n      );\n      final cancelButton = TextButton(\n        child: FlowyText(\n          cancelButtonTitle ?? LocaleKeys.button_cancel.tr(),\n          color: cancelButtonColor ?? foregroundColor,\n        ),\n        onPressed: () {\n          onCancelButtonPressed?.call();\n          Navigator.of(dialogContext).pop();\n        },\n      );\n\n      final actions = switch (actionAlignment) {\n        ConfirmDialogActionAlignment.horizontal => [\n            actionButton,\n            cancelButton,\n          ],\n        ConfirmDialogActionAlignment.vertical => [\n            Column(\n              children: [\n                actionButton,\n                const Divider(height: 1, color: Colors.grey),\n                cancelButton,\n              ],\n            ),\n          ],\n      };\n\n      return AlertDialog.adaptive(\n        title: title,\n        content: content,\n        contentPadding: const EdgeInsets.symmetric(\n          horizontal: 24.0,\n          vertical: 4.0,\n        ),\n        actionsAlignment: MainAxisAlignment.center,\n        actions: actions,\n      );\n    },\n  );\n}\n\nFuture<T?> showFlowyCupertinoConfirmDialog<T>({\n  BuildContext? context,\n  required String title,\n  Widget? content,\n  required Widget leftButton,\n  required Widget rightButton,\n  void Function(BuildContext context)? onLeftButtonPressed,\n  void Function(BuildContext context)? onRightButtonPressed,\n}) {\n  return showDialog(\n    context: context ?? AppGlobals.context,\n    barrierColor: Colors.black.withValues(alpha: 0.25),\n    builder: (context) => CupertinoAlertDialog(\n      title: FlowyText.medium(\n        title,\n        fontSize: 16,\n        maxLines: 10,\n        figmaLineHeight: 22.0,\n      ),\n      content: content,\n      actions: [\n        CupertinoDialogAction(\n          onPressed: () {\n            if (onLeftButtonPressed != null) {\n              onLeftButtonPressed(context);\n            } else {\n              Navigator.of(context).pop();\n            }\n          },\n          child: leftButton,\n        ),\n        CupertinoDialogAction(\n          onPressed: () {\n            if (onRightButtonPressed != null) {\n              onRightButtonPressed(context);\n            } else {\n              Navigator.of(context).pop();\n            }\n          },\n          child: rightButton,\n        ),\n      ],\n    ),\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/mobile/presentation/widgets/widgets.dart",
    "content": "export 'flowy_mobile_option_decorate_box.dart';\nexport 'flowy_mobile_state_container.dart';\nexport 'flowy_option_tile.dart';\nexport 'show_flowy_mobile_confirm_dialog.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/ai_chat_prelude.dart",
    "content": "export 'ai_model_switch_listener.dart';\nexport 'chat_ai_message_bloc.dart';\nexport 'chat_bloc.dart';\nexport 'chat_edit_document_service.dart';\nexport 'chat_entity.dart';\nexport 'chat_input_control_cubit.dart';\nexport 'chat_input_file_bloc.dart';\nexport 'chat_member_bloc.dart';\nexport 'chat_message_listener.dart';\nexport 'chat_message_service.dart';\nexport 'chat_message_stream.dart';\nexport 'chat_notification.dart';\nexport 'chat_select_message_bloc.dart';\nexport 'chat_user_message_bloc.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/ai_model_switch_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/plugins/ai_chat/application/chat_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\ntypedef OnUpdateSelectedModel = void Function(AIModelPB model);\n\nclass AIModelSwitchListener {\n  AIModelSwitchListener({required this.objectId}) {\n    _parser = ChatNotificationParser(id: objectId, callback: _callback);\n    _subscription = RustStreamReceiver.listen(\n      (observable) => _parser?.parse(observable),\n    );\n  }\n\n  final String objectId;\n  StreamSubscription<SubscribeObject>? _subscription;\n  ChatNotificationParser? _parser;\n\n  void start({\n    OnUpdateSelectedModel? onUpdateSelectedModel,\n  }) {\n    this.onUpdateSelectedModel = onUpdateSelectedModel;\n  }\n\n  OnUpdateSelectedModel? onUpdateSelectedModel;\n\n  void _callback(\n    ChatNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    result.map((r) {\n      switch (ty) {\n        case ChatNotification.DidUpdateSelectedModel:\n          onUpdateSelectedModel?.call(AIModelPB.fromBuffer(r));\n          break;\n        default:\n          break;\n      }\n    });\n  }\n\n  Future<void> stop() async {\n    await _subscription?.cancel();\n    _subscription = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart",
    "content": "import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport 'chat_message_service.dart';\n\npart 'chat_ai_message_bloc.freezed.dart';\n\nclass ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {\n  ChatAIMessageBloc({\n    dynamic message,\n    String? refSourceJsonString,\n    required this.chatId,\n    required this.questionId,\n  }) : super(\n          ChatAIMessageState.initial(\n            message,\n            parseMetadata(refSourceJsonString),\n          ),\n        ) {\n    _registerEventHandlers();\n    _initializeStreamListener();\n    _checkInitialStreamState();\n  }\n\n  final String chatId;\n  final Int64? questionId;\n\n  void _registerEventHandlers() {\n    on<_UpdateText>((event, emit) {\n      emit(\n        state.copyWith(\n          text: event.text,\n          messageState: const MessageState.ready(),\n        ),\n      );\n    });\n\n    on<_ReceiveError>((event, emit) {\n      emit(state.copyWith(messageState: MessageState.onError(event.error)));\n    });\n\n    on<_Retry>((event, emit) async {\n      if (questionId == null) {\n        Log.error(\"Question id is not valid: $questionId\");\n        return;\n      }\n      emit(state.copyWith(messageState: const MessageState.loading()));\n      final payload = ChatMessageIdPB(\n        chatId: chatId,\n        messageId: questionId,\n      );\n      final result = await AIEventGetAnswerForQuestion(payload).send();\n      if (!isClosed) {\n        result.fold(\n          (answer) => add(ChatAIMessageEvent.retryResult(answer.content)),\n          (err) {\n            Log.error(\"Failed to get answer: $err\");\n            add(ChatAIMessageEvent.receiveError(err.toString()));\n          },\n        );\n      }\n    });\n\n    on<_RetryResult>((event, emit) {\n      emit(\n        state.copyWith(\n          text: event.text,\n          messageState: const MessageState.ready(),\n        ),\n      );\n    });\n\n    on<_OnAIResponseLimit>((event, emit) {\n      emit(\n        state.copyWith(\n          messageState: const MessageState.onAIResponseLimit(),\n        ),\n      );\n    });\n\n    on<_OnAIImageResponseLimit>((event, emit) {\n      emit(\n        state.copyWith(\n          messageState: const MessageState.onAIImageResponseLimit(),\n        ),\n      );\n    });\n\n    on<_OnAIMaxRquired>((event, emit) {\n      emit(\n        state.copyWith(\n          messageState: MessageState.onAIMaxRequired(event.message),\n        ),\n      );\n    });\n\n    on<_OnLocalAIInitializing>((event, emit) {\n      emit(\n        state.copyWith(\n          messageState: const MessageState.onInitializingLocalAI(),\n        ),\n      );\n    });\n\n    on<_ReceiveMetadata>((event, emit) {\n      Log.debug(\"AI Steps: ${event.metadata.progress?.step}\");\n      emit(\n        state.copyWith(\n          sources: event.metadata.sources,\n          progress: event.metadata.progress,\n        ),\n      );\n    });\n\n    on<_OnAIFollowUp>((event, emit) {\n      emit(\n        state.copyWith(\n          messageState: MessageState.aiFollowUp(event.followUpData),\n        ),\n      );\n    });\n  }\n\n  void _initializeStreamListener() {\n    if (state.stream != null) {\n      state.stream!.listen(\n        onData: (text) => _safeAdd(ChatAIMessageEvent.updateText(text)),\n        onError: (error) =>\n            _safeAdd(ChatAIMessageEvent.receiveError(error.toString())),\n        onAIResponseLimit: () =>\n            _safeAdd(const ChatAIMessageEvent.onAIResponseLimit()),\n        onAIImageResponseLimit: () =>\n            _safeAdd(const ChatAIMessageEvent.onAIImageResponseLimit()),\n        onMetadata: (metadata) =>\n            _safeAdd(ChatAIMessageEvent.receiveMetadata(metadata)),\n        onAIMaxRequired: (message) {\n          Log.info(message);\n          _safeAdd(ChatAIMessageEvent.onAIMaxRequired(message));\n        },\n        onLocalAIInitializing: () =>\n            _safeAdd(const ChatAIMessageEvent.onLocalAIInitializing()),\n        onAIFollowUp: (data) {\n          _safeAdd(ChatAIMessageEvent.onAIFollowUp(data));\n        },\n      );\n    }\n  }\n\n  void _checkInitialStreamState() {\n    if (state.stream != null) {\n      if (state.stream!.aiLimitReached) {\n        add(const ChatAIMessageEvent.onAIResponseLimit());\n      } else if (state.stream!.error != null) {\n        add(ChatAIMessageEvent.receiveError(state.stream!.error!));\n      }\n    }\n  }\n\n  void _safeAdd(ChatAIMessageEvent event) {\n    if (!isClosed) {\n      add(event);\n    }\n  }\n}\n\n@freezed\nclass ChatAIMessageEvent with _$ChatAIMessageEvent {\n  const factory ChatAIMessageEvent.updateText(String text) = _UpdateText;\n  const factory ChatAIMessageEvent.receiveError(String error) = _ReceiveError;\n  const factory ChatAIMessageEvent.retry() = _Retry;\n  const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult;\n  const factory ChatAIMessageEvent.onAIResponseLimit() = _OnAIResponseLimit;\n  const factory ChatAIMessageEvent.onAIImageResponseLimit() =\n      _OnAIImageResponseLimit;\n  const factory ChatAIMessageEvent.onAIMaxRequired(String message) =\n      _OnAIMaxRquired;\n  const factory ChatAIMessageEvent.onLocalAIInitializing() =\n      _OnLocalAIInitializing;\n  const factory ChatAIMessageEvent.receiveMetadata(\n    MetadataCollection metadata,\n  ) = _ReceiveMetadata;\n  const factory ChatAIMessageEvent.onAIFollowUp(\n    AIFollowUpData followUpData,\n  ) = _OnAIFollowUp;\n}\n\n@freezed\nclass ChatAIMessageState with _$ChatAIMessageState {\n  const factory ChatAIMessageState({\n    AnswerStream? stream,\n    required String text,\n    required MessageState messageState,\n    required List<ChatMessageRefSource> sources,\n    required AIChatProgress? progress,\n  }) = _ChatAIMessageState;\n\n  factory ChatAIMessageState.initial(\n    dynamic text,\n    MetadataCollection metadata,\n  ) {\n    return ChatAIMessageState(\n      text: text is String ? text : \"\",\n      stream: text is AnswerStream ? text : null,\n      messageState: const MessageState.ready(),\n      sources: metadata.sources,\n      progress: metadata.progress,\n    );\n  }\n}\n\n@freezed\nclass MessageState with _$MessageState {\n  const factory MessageState.onError(String error) = _Error;\n  const factory MessageState.onAIResponseLimit() = _AIResponseLimit;\n  const factory MessageState.onAIImageResponseLimit() = _AIImageResponseLimit;\n  const factory MessageState.onAIMaxRequired(String message) = _AIMaxRequired;\n  const factory MessageState.onInitializingLocalAI() = _LocalAIInitializing;\n  const factory MessageState.ready() = _Ready;\n  const factory MessageState.loading() = _Loading;\n  const factory MessageState.aiFollowUp(AIFollowUpData followUpData) =\n      _AIFollowUp;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:collection/collection.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport 'chat_entity.dart';\nimport 'chat_message_handler.dart';\nimport 'chat_message_listener.dart';\nimport 'chat_message_stream.dart';\nimport 'chat_settings_manager.dart';\nimport 'chat_stream_manager.dart';\n\npart 'chat_bloc.freezed.dart';\n\n/// Returns current Unix timestamp (seconds since epoch)\nint timestamp() {\n  return DateTime.now().millisecondsSinceEpoch ~/ 1000;\n}\n\nclass ChatBloc extends Bloc<ChatEvent, ChatState> {\n  ChatBloc({\n    required this.chatId,\n    required this.userId,\n  })  : chatController = InMemoryChatController(),\n        listener = ChatMessageListener(chatId: chatId),\n        super(ChatState.initial()) {\n    // Initialize managers\n    _messageHandler = ChatMessageHandler(\n      chatId: chatId,\n      userId: userId,\n      chatController: chatController,\n    );\n\n    _streamManager = ChatStreamManager(chatId);\n    _settingsManager = ChatSettingsManager(chatId: chatId);\n\n    _startListening();\n    _dispatch();\n    _loadMessages();\n    _loadSettings();\n  }\n\n  final String chatId;\n  final String userId;\n  final ChatMessageListener listener;\n  final ChatController chatController;\n\n  // Managers\n  late final ChatMessageHandler _messageHandler;\n  late final ChatStreamManager _streamManager;\n  late final ChatSettingsManager _settingsManager;\n\n  ChatMessagePB? lastSentMessage;\n\n  bool isLoadingPreviousMessages = false;\n  bool hasMorePreviousMessages = true;\n  bool isFetchingRelatedQuestions = false;\n  bool shouldFetchRelatedQuestions = false;\n\n  // Accessor for selected sources\n  ValueNotifier<List<String>> get selectedSourcesNotifier =>\n      _settingsManager.selectedSourcesNotifier;\n\n  @override\n  Future<void> close() async {\n    // Safely dispose all resources\n    await _streamManager.dispose();\n    await listener.stop();\n\n    final request = ViewIdPB(value: chatId);\n    unawaited(FolderEventCloseView(request).send());\n\n    _settingsManager.dispose();\n    chatController.dispose();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<ChatEvent>((event, emit) async {\n      await event.when(\n        // Chat settings\n        didReceiveChatSettings: (settings) async =>\n            _handleChatSettings(settings),\n        updateSelectedSources: (selectedSourcesIds) async =>\n            _handleUpdateSources(selectedSourcesIds),\n\n        // Message loading\n        didLoadLatestMessages: (messages) async =>\n            _handleLatestMessages(messages, emit),\n        loadPreviousMessages: () async => _loadPreviousMessagesIfNeeded(),\n        didLoadPreviousMessages: (messages, hasMore) async =>\n            _handlePreviousMessages(messages, hasMore),\n\n        // Message handling\n        receiveMessage: (message) async => _handleReceiveMessage(message),\n\n        // Sending messages\n        sendMessage: (message, format, metadata, promptId) async =>\n            _handleSendMessage(message, format, metadata, promptId, emit),\n        finishSending: () async => emit(\n          state.copyWith(\n            promptResponseState: PromptResponseState.streamingAnswer,\n          ),\n        ),\n\n        // Stream control\n        stopStream: () async => _handleStopStream(emit),\n        failedSending: () async => _handleFailedSending(emit),\n\n        // Answer regeneration\n        regenerateAnswer: (id, format, model) async =>\n            _handleRegenerateAnswer(id, format, model, emit),\n\n        // Streaming completion\n        didFinishAnswerStream: () async => emit(\n          state.copyWith(\n            promptResponseState: PromptResponseState.ready,\n          ),\n        ),\n\n        // Related questions\n        didReceiveRelatedQuestions: (questions) async =>\n            _handleRelatedQuestions(\n          questions,\n          emit,\n        ),\n\n        // Message management\n        deleteMessage: (message) async => chatController.remove(message),\n\n        // AI follow-up\n        onAIFollowUp: (followUpData) async {\n          shouldFetchRelatedQuestions =\n              followUpData.shouldGenerateRelatedQuestion;\n        },\n      );\n    });\n  }\n\n  // Chat settings handlers\n  void _handleChatSettings(ChatSettingsPB settings) {\n    _settingsManager.selectedSourcesNotifier.value = settings.ragIds;\n  }\n\n  Future<void> _handleUpdateSources(List<String> selectedSourcesIds) async {\n    await _settingsManager.updateSelectedSources(selectedSourcesIds);\n  }\n\n  // Message loading handlers\n  Future<void> _handleLatestMessages(\n    List<Message> messages,\n    Emitter<ChatState> emit,\n  ) async {\n    for (final message in messages) {\n      await chatController.insert(message, index: 0);\n    }\n\n    // Check if emit is still valid after async operations\n    if (emit.isDone) {\n      return;\n    }\n\n    switch (state.loadingState) {\n      case LoadChatMessageStatus.loading when chatController.messages.isEmpty:\n        emit(state.copyWith(loadingState: LoadChatMessageStatus.loadingRemote));\n        break;\n      case LoadChatMessageStatus.loading:\n      case LoadChatMessageStatus.loadingRemote:\n        emit(state.copyWith(loadingState: LoadChatMessageStatus.ready));\n        break;\n      default:\n        break;\n    }\n  }\n\n  void _handlePreviousMessages(List<Message> messages, bool hasMore) {\n    for (final message in messages) {\n      chatController.insert(message, index: 0);\n    }\n\n    isLoadingPreviousMessages = false;\n    hasMorePreviousMessages = hasMore;\n  }\n\n  // Message handling\n  void _handleReceiveMessage(Message message) {\n    final oldMessage =\n        chatController.messages.firstWhereOrNull((m) => m.id == message.id);\n    if (oldMessage == null) {\n      chatController.insert(message);\n    } else {\n      chatController.update(oldMessage, message);\n    }\n  }\n\n  // Message sending handlers\n  void _handleSendMessage(\n    String message,\n    PredefinedFormat? format,\n    Map<String, dynamic>? metadata,\n    String? promptId,\n    Emitter<ChatState> emit,\n  ) {\n    _messageHandler.clearErrorMessages();\n    emit(state.copyWith(clearErrorMessages: !state.clearErrorMessages));\n\n    _messageHandler.clearRelatedQuestions();\n    _startStreamingMessage(message, format, metadata, promptId);\n    lastSentMessage = null;\n\n    isFetchingRelatedQuestions = false;\n    shouldFetchRelatedQuestions = format == null || format.imageFormat.hasText;\n\n    emit(\n      state.copyWith(\n        promptResponseState: PromptResponseState.sendingQuestion,\n      ),\n    );\n  }\n\n  // Stream control handlers\n  Future<void> _handleStopStream(Emitter<ChatState> emit) async {\n    await _streamManager.stopStream();\n\n    // Allow user input\n    emit(state.copyWith(promptResponseState: PromptResponseState.ready));\n\n    // No need to remove old message if stream has started already\n    if (_streamManager.hasAnswerStreamStarted) {\n      return;\n    }\n\n    // Remove the non-started message from the list\n    final message = chatController.messages.lastWhereOrNull(\n      (e) => e.id == _messageHandler.answerStreamMessageId,\n    );\n    if (message != null) {\n      await chatController.remove(message);\n    }\n\n    await _streamManager.disposeAnswerStream();\n  }\n\n  void _handleFailedSending(Emitter<ChatState> emit) {\n    final lastMessage = chatController.messages.lastOrNull;\n    if (lastMessage != null) {\n      chatController.remove(lastMessage);\n    }\n    emit(state.copyWith(promptResponseState: PromptResponseState.ready));\n  }\n\n  // Answer regeneration handler\n  void _handleRegenerateAnswer(\n    String id,\n    PredefinedFormat? format,\n    AIModelPB? model,\n    Emitter<ChatState> emit,\n  ) {\n    _messageHandler.clearRelatedQuestions();\n    _regenerateAnswer(id, format, model);\n    lastSentMessage = null;\n\n    isFetchingRelatedQuestions = false;\n    shouldFetchRelatedQuestions = false;\n\n    emit(\n      state.copyWith(\n        promptResponseState: PromptResponseState.sendingQuestion,\n      ),\n    );\n  }\n\n  // Related questions handler\n  void _handleRelatedQuestions(\n    List<String> questions,\n    Emitter<ChatState> emit,\n  ) {\n    if (questions.isEmpty) {\n      return;\n    }\n\n    final metadata = {\n      onetimeShotType: OnetimeShotType.relatedQuestion,\n      'questions': questions,\n    };\n\n    final createdAt = DateTime.now();\n    final message = TextMessage(\n      id: \"related_question_$createdAt\",\n      text: '',\n      metadata: metadata,\n      author: const User(id: systemUserId),\n      createdAt: createdAt,\n    );\n\n    chatController.insert(message);\n\n    emit(\n      state.copyWith(\n        promptResponseState: PromptResponseState.relatedQuestionsReady,\n      ),\n    );\n  }\n\n  void _startListening() {\n    listener.start(\n      chatMessageCallback: (pb) {\n        if (isClosed) {\n          return;\n        }\n\n        _messageHandler.processReceivedMessage(pb);\n        final message = _messageHandler.createTextMessage(pb);\n        add(ChatEvent.receiveMessage(message));\n      },\n      chatErrorMessageCallback: (err) {\n        if (!isClosed) {\n          Log.error(\"chat error: ${err.errorMessage}\");\n          add(const ChatEvent.didFinishAnswerStream());\n        }\n      },\n      latestMessageCallback: (list) {\n        if (!isClosed) {\n          final messages =\n              list.messages.map(_messageHandler.createTextMessage).toList();\n          add(ChatEvent.didLoadLatestMessages(messages));\n        }\n      },\n      prevMessageCallback: (list) {\n        if (!isClosed) {\n          final messages =\n              list.messages.map(_messageHandler.createTextMessage).toList();\n          add(ChatEvent.didLoadPreviousMessages(messages, list.hasMore));\n        }\n      },\n      finishStreamingCallback: () async {\n        if (isClosed) {\n          return;\n        }\n\n        add(const ChatEvent.didFinishAnswerStream());\n        unawaited(_fetchRelatedQuestionsIfNeeded());\n      },\n    );\n  }\n\n  // Split method to handle related questions\n  Future<void> _fetchRelatedQuestionsIfNeeded() async {\n    // Don't fetch related questions if conditions aren't met\n    if (_streamManager.answerStream == null ||\n        lastSentMessage == null ||\n        !shouldFetchRelatedQuestions) {\n      return;\n    }\n\n    final payload = ChatMessageIdPB(\n      chatId: chatId,\n      messageId: lastSentMessage!.messageId,\n    );\n\n    isFetchingRelatedQuestions = true;\n    await AIEventGetRelatedQuestion(payload).send().fold(\n      (list) {\n        // while fetching related questions, the user might enter a new\n        // question or regenerate a previous response. In such cases, don't\n        // display the relatedQuestions\n        if (!isClosed && isFetchingRelatedQuestions) {\n          add(\n            ChatEvent.didReceiveRelatedQuestions(\n              list.items.map((e) => e.content).toList(),\n            ),\n          );\n          isFetchingRelatedQuestions = false;\n        }\n      },\n      (err) => Log.error(\"Failed to get related questions: $err\"),\n    );\n  }\n\n  void _loadSettings() async {\n    final getChatSettingsPayload =\n        AIEventGetChatSettings(ChatId(value: chatId));\n\n    await getChatSettingsPayload.send().fold(\n      (settings) {\n        if (!isClosed) {\n          add(ChatEvent.didReceiveChatSettings(settings: settings));\n        }\n      },\n      (err) => Log.error(\"Failed to load chat settings: $err\"),\n    );\n  }\n\n  void _loadMessages() async {\n    final loadMessagesPayload = LoadNextChatMessagePB(\n      chatId: chatId,\n      limit: Int64(10),\n    );\n\n    await AIEventLoadNextMessage(loadMessagesPayload).send().fold(\n      (list) {\n        if (!isClosed) {\n          final messages =\n              list.messages.map(_messageHandler.createTextMessage).toList();\n          add(ChatEvent.didLoadLatestMessages(messages));\n        }\n      },\n      (err) => Log.error(\"Failed to load messages: $err\"),\n    );\n  }\n\n  void _loadPreviousMessagesIfNeeded() {\n    if (isLoadingPreviousMessages) {\n      return;\n    }\n\n    final oldestMessage = _messageHandler.getOldestMessage();\n\n    if (oldestMessage != null) {\n      final oldestMessageId = Int64.tryParseInt(oldestMessage.id);\n      if (oldestMessageId == null) {\n        Log.error(\"Failed to parse message_id: ${oldestMessage.id}\");\n        return;\n      }\n      isLoadingPreviousMessages = true;\n      _loadPreviousMessages(oldestMessageId);\n    }\n  }\n\n  void _loadPreviousMessages(Int64? beforeMessageId) {\n    final payload = LoadPrevChatMessagePB(\n      chatId: chatId,\n      limit: Int64(10),\n      beforeMessageId: beforeMessageId,\n    );\n    AIEventLoadPrevMessage(payload).send();\n  }\n\n  Future<void> _startStreamingMessage(\n    String message,\n    PredefinedFormat? format,\n    Map<String, dynamic>? metadata,\n    String? promptId,\n  ) async {\n    // Prepare streams\n    await _streamManager.prepareStreams();\n\n    // Create and add question message\n    final questionStreamMessage = _messageHandler.createQuestionStreamMessage(\n      _streamManager.questionStream!,\n      metadata,\n    );\n    add(ChatEvent.receiveMessage(questionStreamMessage));\n\n    // Send stream request\n    await _streamManager.sendStreamRequest(message, format, promptId).fold(\n      (question) {\n        if (!isClosed) {\n          // Create and add answer stream message\n          final streamAnswer = _messageHandler.createAnswerStreamMessage(\n            stream: _streamManager.answerStream!,\n            questionMessageId: question.messageId,\n            fakeQuestionMessageId: questionStreamMessage.id,\n          );\n\n          lastSentMessage = question;\n          add(const ChatEvent.finishSending());\n          add(ChatEvent.receiveMessage(streamAnswer));\n        }\n      },\n      (err) {\n        if (!isClosed) {\n          Log.error(\"Failed to send message: ${err.msg}\");\n\n          final metadata = {\n            onetimeShotType: OnetimeShotType.error,\n            if (err.code != ErrorCode.Internal) errorMessageTextKey: err.msg,\n          };\n\n          final error = TextMessage(\n            text: '',\n            metadata: metadata,\n            author: const User(id: systemUserId),\n            id: systemUserId,\n            createdAt: DateTime.now(),\n          );\n\n          add(const ChatEvent.failedSending());\n          add(ChatEvent.receiveMessage(error));\n        }\n      },\n    );\n  }\n\n  // Refactored method to handle answer regeneration\n  void _regenerateAnswer(\n    String answerMessageIdString,\n    PredefinedFormat? format,\n    AIModelPB? model,\n  ) async {\n    final id = _messageHandler.getEffectiveMessageId(answerMessageIdString);\n    final answerMessageId = Int64.tryParseInt(id);\n    if (answerMessageId == null) {\n      return;\n    }\n\n    await _streamManager.prepareStreams();\n    await _streamManager\n        .sendRegenerateRequest(\n      answerMessageId,\n      format,\n      model,\n    )\n        .fold(\n      (_) {\n        if (!isClosed) {\n          final streamAnswer = _messageHandler\n              .createAnswerStreamMessage(\n                stream: _streamManager.answerStream!,\n                questionMessageId: answerMessageId - 1,\n              )\n              .copyWith(id: answerMessageIdString);\n\n          add(ChatEvent.receiveMessage(streamAnswer));\n          add(const ChatEvent.finishSending());\n        }\n      },\n      (err) => Log.error(\"Failed to regenerate answer: ${err.msg}\"),\n    );\n  }\n}\n\n@freezed\nclass ChatEvent with _$ChatEvent {\n  // chat settings\n  const factory ChatEvent.didReceiveChatSettings({\n    required ChatSettingsPB settings,\n  }) = _DidReceiveChatSettings;\n  const factory ChatEvent.updateSelectedSources({\n    required List<String> selectedSourcesIds,\n  }) = _UpdateSelectedSources;\n\n  // send message\n  const factory ChatEvent.sendMessage({\n    required String message,\n    PredefinedFormat? format,\n    Map<String, dynamic>? metadata,\n    String? promptId,\n  }) = _SendMessage;\n  const factory ChatEvent.finishSending() = _FinishSendMessage;\n  const factory ChatEvent.failedSending() = _FailSendMessage;\n\n  // regenerate\n  const factory ChatEvent.regenerateAnswer(\n    String id,\n    PredefinedFormat? format,\n    AIModelPB? model,\n  ) = _RegenerateAnswer;\n\n  // streaming answer\n  const factory ChatEvent.stopStream() = _StopStream;\n  const factory ChatEvent.didFinishAnswerStream() = _DidFinishAnswerStream;\n\n  // receive message\n  const factory ChatEvent.receiveMessage(Message message) = _ReceiveMessage;\n\n  // loading messages\n  const factory ChatEvent.didLoadLatestMessages(List<Message> messages) =\n      _DidLoadMessages;\n  const factory ChatEvent.loadPreviousMessages() = _LoadPreviousMessages;\n  const factory ChatEvent.didLoadPreviousMessages(\n    List<Message> messages,\n    bool hasMore,\n  ) = _DidLoadPreviousMessages;\n\n  // related questions\n  const factory ChatEvent.didReceiveRelatedQuestions(\n    List<String> questions,\n  ) = _DidReceiveRelatedQueston;\n\n  const factory ChatEvent.deleteMessage(Message message) = _DeleteMessage;\n\n  const factory ChatEvent.onAIFollowUp(AIFollowUpData followUpData) =\n      _OnAIFollowUp;\n}\n\n@freezed\nclass ChatState with _$ChatState {\n  const factory ChatState({\n    required LoadChatMessageStatus loadingState,\n    required PromptResponseState promptResponseState,\n    required bool clearErrorMessages,\n  }) = _ChatState;\n\n  factory ChatState.initial() => const ChatState(\n        loadingState: LoadChatMessageStatus.loading,\n        promptResponseState: PromptResponseState.ready,\n        clearErrorMessages: false,\n      );\n}\n\nbool isOtherUserMessage(Message message) {\n  return message.author.id != aiResponseUserId &&\n      message.author.id != systemUserId &&\n      !message.author.id.startsWith(\"streamId:\");\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_edit_document_service.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';\nimport 'package:appflowy/plugins/document/application/prelude.dart';\nimport 'package:appflowy/shared/markdown_to_document.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\n\nclass ChatEditDocumentService {\n  const ChatEditDocumentService._();\n\n  static Future<ViewPB?> saveMessagesToNewPage(\n    String chatPageName,\n    String parentViewId,\n    List<TextMessage> messages,\n  ) async {\n    if (messages.isEmpty) {\n      return null;\n    }\n\n    // Convert messages to markdown and trim the last empty newline.\n    final completeMessage = messages.map((m) => m.text).join('\\n').trimRight();\n    if (completeMessage.isEmpty) {\n      return null;\n    }\n\n    final document = customMarkdownToDocument(\n      completeMessage,\n      tableWidth: 250.0,\n    );\n    final initialBytes =\n        DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer();\n    if (initialBytes == null) {\n      Log.error('Failed to convert messages to document');\n      return null;\n    }\n\n    return ViewBackendService.createView(\n      name: LocaleKeys.chat_addToNewPageName.tr(args: [chatPageName]),\n      layoutType: ViewLayoutPB.Document,\n      parentViewId: parentViewId,\n      initialDataBytes: initialBytes,\n    ).toNullable();\n  }\n\n  static Future<void> addMessagesToPage(\n    String documentId,\n    List<TextMessage> messages,\n  ) async {\n    // Convert messages to markdown and trim the last empty newline.\n    final completeMessage = messages.map((m) => m.text).join('\\n').trimRight();\n    if (completeMessage.isEmpty) {\n      return;\n    }\n\n    final bloc = DocumentBloc(\n      documentId: documentId,\n      saveToBlocMap: false,\n    )..add(const DocumentEvent.initial());\n\n    if (bloc.state.editorState == null) {\n      await bloc.stream.firstWhere((state) => state.editorState != null);\n    }\n\n    final editorState = bloc.state.editorState;\n    if (editorState == null) {\n      Log.error(\"Can't get EditorState of document\");\n      return;\n    }\n\n    final messageDocument = customMarkdownToDocument(\n      completeMessage,\n      tableWidth: 250.0,\n    );\n    if (messageDocument.isEmpty) {\n      Log.error('Failed to convert message to document');\n      return;\n    }\n\n    final lastNodeOrNull = editorState.document.root.children.lastOrNull;\n\n    final rootIsEmpty = lastNodeOrNull == null;\n    final isLastLineEmpty = lastNodeOrNull?.children.isNotEmpty == false &&\n        lastNodeOrNull?.delta?.isNotEmpty == false;\n\n    final nodes = [\n      if (rootIsEmpty || !isLastLineEmpty) paragraphNode(),\n      ...messageDocument.root.children,\n      paragraphNode(),\n    ];\n    final insertPath = rootIsEmpty ||\n            listEquals(lastNodeOrNull.path, const [0]) && isLastLineEmpty\n        ? const [0]\n        : lastNodeOrNull.path.next;\n\n    final transaction = editorState.transaction..insertNodes(insertPath, nodes);\n    await editorState.apply(transaction);\n    await bloc.close();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_entity.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:equatable/equatable.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:path/path.dart' as path;\n\npart 'chat_entity.g.dart';\npart 'chat_entity.freezed.dart';\n\nconst errorMessageTextKey = \"errorMessageText\";\nconst systemUserId = \"system\";\nconst aiResponseUserId = \"0\";\n\n/// `messageRefSourceJsonStringKey` is the key used for metadata that contains the reference source of a message.\n/// Each message may include this information.\n/// - When used in a sent message, it indicates that the message includes an attachment.\n/// - When used in a received message, it indicates the AI reference sources used to answer a question.\nconst messageRefSourceJsonStringKey = \"ref_source_json_string\";\nconst messageChatFileListKey = \"chat_files\";\nconst messageQuestionIdKey = \"question_id\";\n\n@JsonSerializable()\nclass ChatMessageRefSource {\n  ChatMessageRefSource({\n    required this.id,\n    required this.name,\n    required this.source,\n  });\n\n  factory ChatMessageRefSource.fromJson(Map<String, dynamic> json) =>\n      _$ChatMessageRefSourceFromJson(json);\n\n  final String id;\n  final String name;\n  final String source;\n\n  Map<String, dynamic> toJson() => _$ChatMessageRefSourceToJson(this);\n}\n\n@JsonSerializable()\nclass AIChatProgress {\n  AIChatProgress({\n    required this.step,\n  });\n\n  factory AIChatProgress.fromJson(Map<String, dynamic> json) =>\n      _$AIChatProgressFromJson(json);\n\n  final String step;\n\n  Map<String, dynamic> toJson() => _$AIChatProgressToJson(this);\n}\n\nenum PromptResponseState {\n  ready,\n  sendingQuestion,\n  streamingAnswer,\n  relatedQuestionsReady;\n\n  bool get isReady => this == ready || this == relatedQuestionsReady;\n}\n\nclass ChatFile extends Equatable {\n  const ChatFile({\n    required this.filePath,\n    required this.fileName,\n    required this.fileType,\n  });\n\n  static ChatFile? fromFilePath(String filePath) {\n    final file = File(filePath);\n    if (!file.existsSync()) {\n      return null;\n    }\n\n    final fileName = path.basename(filePath);\n    final extension = path.extension(filePath).toLowerCase();\n\n    ContextLoaderTypePB fileType;\n    switch (extension) {\n      case '.pdf':\n        fileType = ContextLoaderTypePB.PDF;\n        break;\n      case '.txt':\n        fileType = ContextLoaderTypePB.Txt;\n        break;\n      case '.md':\n        fileType = ContextLoaderTypePB.Markdown;\n        break;\n      default:\n        fileType = ContextLoaderTypePB.UnknownLoaderType;\n    }\n\n    return ChatFile(\n      filePath: filePath,\n      fileName: fileName,\n      fileType: fileType,\n    );\n  }\n\n  final String filePath;\n  final String fileName;\n  final ContextLoaderTypePB fileType;\n\n  @override\n  List<Object?> get props => [filePath];\n}\n\ntypedef ChatFileMap = Map<String, ChatFile>;\ntypedef ChatMentionedPageMap = Map<String, ViewPB>;\n\n@freezed\nclass ChatLoadingState with _$ChatLoadingState {\n  const factory ChatLoadingState.loading() = _Loading;\n  const factory ChatLoadingState.finish({FlowyError? error}) = _Finish;\n}\n\nextension ChatLoadingStateExtension on ChatLoadingState {\n  bool get isLoading => this is _Loading;\n  bool get isFinish => this is _Finish;\n}\n\nenum OnetimeShotType {\n  sendingMessage,\n  relatedQuestion,\n  error,\n}\n\nconst onetimeShotType = \"OnetimeShotType\";\n\nOnetimeShotType? onetimeMessageTypeFromMeta(Map<String, dynamic>? metadata) {\n  return metadata?[onetimeShotType];\n}\n\nenum LoadChatMessageStatus {\n  loading,\n  loadingRemote,\n  ready,\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_control_cubit.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'chat_input_control_cubit.freezed.dart';\n\nclass ChatInputControlCubit extends Cubit<ChatInputControlState> {\n  ChatInputControlCubit() : super(const ChatInputControlState.loading());\n\n  final List<ViewPB> allViews = [];\n  final List<String> selectedViewIds = [];\n\n  /// used when mentioning a page\n  ///\n  /// the text position after the @ character\n  int _filterStartPosition = -1;\n\n  /// used when mentioning a page\n  ///\n  /// the text position after the @ character, at the end of the filter\n  int _filterEndPosition = -1;\n\n  /// used when mentioning a page\n  ///\n  /// the entire string input in the prompt\n  String _inputText = \"\";\n\n  /// used when mentioning a page\n  ///\n  /// the current filtering text, after the @ characater\n  String _filter = \"\";\n\n  String get inputText => _inputText;\n  int get filterStartPosition => _filterStartPosition;\n  int get filterEndPosition => _filterEndPosition;\n\n  void refreshViews() async {\n    final newViews = await ViewBackendService.getAllViews().fold(\n      (result) {\n        return result.items\n            .where(\n              (v) =>\n                  !v.isSpace &&\n                  v.layout.isDocumentView &&\n                  v.parentViewId != v.id,\n            )\n            .toList();\n      },\n      (err) {\n        Log.error(err);\n        return <ViewPB>[];\n      },\n    );\n    allViews\n      ..clear()\n      ..addAll(newViews);\n\n    // update visible views\n    newViews.retainWhere((v) => !selectedViewIds.contains(v.id));\n    if (_filter.isNotEmpty) {\n      newViews.retainWhere(\n        (v) {\n          final nonEmptyName = v.name.isEmpty\n              ? LocaleKeys.document_title_placeholder.tr()\n              : v.name;\n          return nonEmptyName.toLowerCase().contains(_filter);\n        },\n      );\n    }\n    final focusedViewIndex = newViews.isEmpty ? -1 : 0;\n    emit(\n      ChatInputControlState.ready(\n        visibleViews: newViews,\n        focusedViewIndex: focusedViewIndex,\n      ),\n    );\n  }\n\n  void startSearching(TextEditingValue textEditingValue) {\n    _filterStartPosition =\n        _filterEndPosition = textEditingValue.selection.baseOffset;\n    _filter = \"\";\n    _inputText = textEditingValue.text;\n    state.maybeMap(\n      ready: (readyState) {\n        emit(\n          readyState.copyWith(\n            visibleViews: allViews,\n            focusedViewIndex: allViews.isEmpty ? -1 : 0,\n          ),\n        );\n      },\n      orElse: () {},\n    );\n  }\n\n  void reset() {\n    _filterStartPosition = _filterEndPosition = -1;\n    _filter = _inputText = \"\";\n    state.maybeMap(\n      ready: (readyState) {\n        emit(\n          readyState.copyWith(\n            visibleViews: allViews,\n            focusedViewIndex: allViews.isEmpty ? -1 : 0,\n          ),\n        );\n      },\n      orElse: () {},\n    );\n  }\n\n  void updateFilter(\n    String newInputText,\n    String newFilter, {\n    int? newEndPosition,\n  }) {\n    updateInputText(newInputText);\n\n    // filter the views\n    _filter = newFilter.toLowerCase();\n    if (newEndPosition != null) {\n      _filterEndPosition = newEndPosition;\n    }\n\n    final newVisibleViews =\n        allViews.where((v) => !selectedViewIds.contains(v.id)).toList();\n\n    if (_filter.isNotEmpty) {\n      newVisibleViews.retainWhere(\n        (v) {\n          final nonEmptyName = v.name.isEmpty\n              ? LocaleKeys.document_title_placeholder.tr()\n              : v.name;\n          return nonEmptyName.toLowerCase().contains(_filter);\n        },\n      );\n    }\n\n    state.maybeWhen(\n      ready: (_, oldFocusedIndex) {\n        final newFocusedViewIndex = oldFocusedIndex < newVisibleViews.length\n            ? oldFocusedIndex\n            : (newVisibleViews.isEmpty ? -1 : 0);\n        emit(\n          ChatInputControlState.ready(\n            visibleViews: newVisibleViews,\n            focusedViewIndex: newFocusedViewIndex,\n          ),\n        );\n      },\n      orElse: () {},\n    );\n  }\n\n  void updateInputText(String newInputText) {\n    _inputText = newInputText;\n\n    // input text is changed, see if there are any deletions\n    selectedViewIds.retainWhere(_inputText.contains);\n    _notifyUpdateSelectedViews();\n  }\n\n  void updateSelectionUp() {\n    state.maybeMap(\n      ready: (readyState) {\n        final newIndex = readyState.visibleViews.isEmpty\n            ? -1\n            : (readyState.focusedViewIndex - 1) %\n                readyState.visibleViews.length;\n        emit(\n          readyState.copyWith(focusedViewIndex: newIndex),\n        );\n      },\n      orElse: () {},\n    );\n  }\n\n  void updateSelectionDown() {\n    state.maybeMap(\n      ready: (readyState) {\n        final newIndex = readyState.visibleViews.isEmpty\n            ? -1\n            : (readyState.focusedViewIndex + 1) %\n                readyState.visibleViews.length;\n        emit(\n          readyState.copyWith(focusedViewIndex: newIndex),\n        );\n      },\n      orElse: () {},\n    );\n  }\n\n  void selectPage(ViewPB view) {\n    selectedViewIds.add(view.id);\n    _notifyUpdateSelectedViews();\n    reset();\n  }\n\n  String formatIntputText(final String input) {\n    String result = input;\n    for (final viewId in selectedViewIds) {\n      if (!result.contains(viewId)) {\n        continue;\n      }\n      final view = allViews.firstWhereOrNull((view) => view.id == viewId);\n      if (view != null) {\n        final nonEmptyName = view.name.isEmpty\n            ? LocaleKeys.document_title_placeholder.tr()\n            : view.name;\n        result = result.replaceAll(RegExp(viewId), nonEmptyName);\n      }\n    }\n    return result;\n  }\n\n  void _notifyUpdateSelectedViews() {\n    final stateCopy = state;\n    final selectedViews =\n        allViews.where((view) => selectedViewIds.contains(view.id)).toList();\n    emit(ChatInputControlState.updateSelectedViews(selectedViews));\n    emit(stateCopy);\n  }\n}\n\n@freezed\nclass ChatInputControlState with _$ChatInputControlState {\n  const factory ChatInputControlState.loading() = _Loading;\n\n  const factory ChatInputControlState.ready({\n    required List<ViewPB> visibleViews,\n    required int focusedViewIndex,\n  }) = _Ready;\n\n  const factory ChatInputControlState.updateSelectedViews(\n    List<ViewPB> selectedViews,\n  ) = _UpdateOneShot;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_file_bloc.dart",
    "content": "import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'chat_input_file_bloc.freezed.dart';\n\nclass ChatInputFileBloc extends Bloc<ChatInputFileEvent, ChatInputFileState> {\n  ChatInputFileBloc({\n    required this.file,\n  }) : super(const ChatInputFileState()) {\n    on<ChatInputFileEvent>(\n      (event, emit) async {\n        event.when(\n          updateUploadState: (UploadFileIndicator indicator) {\n            emit(state.copyWith(uploadFileIndicator: indicator));\n          },\n        );\n      },\n    );\n  }\n\n  final ChatFile file;\n}\n\n@freezed\nclass ChatInputFileEvent with _$ChatInputFileEvent {\n  const factory ChatInputFileEvent.updateUploadState(\n    UploadFileIndicator indicator,\n  ) = _UpdateUploadState;\n}\n\n@freezed\nclass ChatInputFileState with _$ChatInputFileState {\n  const factory ChatInputFileState({\n    UploadFileIndicator? uploadFileIndicator,\n  }) = _ChatInputFileState;\n}\n\n@freezed\nclass UploadFileIndicator with _$UploadFileIndicator {\n  const factory UploadFileIndicator.finish() = _Finish;\n  const factory UploadFileIndicator.uploading() = _Uploading;\n  const factory UploadFileIndicator.error(String error) = _Error;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_member_bloc.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:equatable/equatable.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'chat_member_bloc.freezed.dart';\n\nclass ChatMemberBloc extends Bloc<ChatMemberEvent, ChatMemberState> {\n  ChatMemberBloc() : super(const ChatMemberState()) {\n    on<ChatMemberEvent>(\n      (event, emit) async {\n        await event.when(\n          receiveMemberInfo: (String id, WorkspaceMemberPB memberInfo) {\n            final members = Map<String, ChatMember>.from(state.members);\n            members[id] = ChatMember(info: memberInfo);\n            emit(state.copyWith(members: members));\n          },\n          getMemberInfo: (String userId) async {\n            if (state.members.containsKey(userId)) {\n              // Member info already exists. Debouncing refresh member info from backend would be better.\n              return;\n            }\n\n            final payload = WorkspaceMemberIdPB(\n              uid: Int64.parseInt(userId),\n            );\n\n            await UserEventGetMemberInfo(payload).send().then((result) {\n              result.fold(\n                (member) {\n                  if (!isClosed) {\n                    add(ChatMemberEvent.receiveMemberInfo(userId, member));\n                  }\n                },\n                (err) => Log.error(\"Error getting member info: $err\"),\n              );\n            });\n          },\n        );\n      },\n    );\n  }\n}\n\n@freezed\nclass ChatMemberEvent with _$ChatMemberEvent {\n  const factory ChatMemberEvent.getMemberInfo(\n    String userId,\n  ) = _GetMemberInfo;\n  const factory ChatMemberEvent.receiveMemberInfo(\n    String id,\n    WorkspaceMemberPB memberInfo,\n  ) = _ReceiveMemberInfo;\n}\n\n@freezed\nclass ChatMemberState with _$ChatMemberState {\n  const factory ChatMemberState({\n    @Default({}) Map<String, ChatMember> members,\n  }) = _ChatMemberState;\n}\n\nclass ChatMember extends Equatable {\n  ChatMember({\n    required this.info,\n  });\n  final DateTime _date = DateTime.now();\n  final WorkspaceMemberPB info;\n\n  @override\n  List<Object?> get props => [_date, info];\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_handler.dart",
    "content": "import 'dart:collection';\n\nimport 'package:appflowy/util/int64_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';\nimport 'package:collection/collection.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\nimport 'package:nanoid/nanoid.dart';\n\nimport 'chat_entity.dart';\nimport 'chat_message_stream.dart';\n\n/// Returns current Unix timestamp (seconds since epoch)\nint timestamp() {\n  return DateTime.now().millisecondsSinceEpoch ~/ 1000;\n}\n\n/// Handles message creation and manipulation for the chat system\nclass ChatMessageHandler {\n  ChatMessageHandler({\n    required this.chatId,\n    required this.userId,\n    required this.chatController,\n  });\n  final String chatId;\n  final String userId;\n  final ChatController chatController;\n\n  /// Maps real message IDs to temporary streaming message IDs\n  final HashMap<String, String> _temporaryMessageIDMap = HashMap();\n\n  /// Gets the effective message ID from the temporary map\n  String getEffectiveMessageId(String messageId) {\n    return _temporaryMessageIDMap.entries\n            .firstWhereOrNull((entry) => entry.value == messageId)\n            ?.key ??\n        messageId;\n  }\n\n  String answerStreamMessageId = '';\n  String questionStreamMessageId = '';\n\n  /// Create a message from ChatMessagePB object\n  Message createTextMessage(ChatMessagePB message) {\n    String messageId = message.messageId.toString();\n\n    /// If the message id is in the temporary map, we will use the previous fake message id\n    if (_temporaryMessageIDMap.containsKey(messageId)) {\n      messageId = _temporaryMessageIDMap[messageId]!;\n    }\n    final metadata = message.metadata == 'null' ? '[]' : message.metadata;\n\n    return TextMessage(\n      author: User(id: message.authorId),\n      id: messageId,\n      text: message.content,\n      createdAt: message.createdAt.toDateTime(),\n      metadata: {\n        messageRefSourceJsonStringKey: metadata,\n      },\n    );\n  }\n\n  /// Create a streaming answer message\n  Message createAnswerStreamMessage({\n    required AnswerStream stream,\n    required Int64 questionMessageId,\n    String? fakeQuestionMessageId,\n  }) {\n    answerStreamMessageId = fakeQuestionMessageId == null\n        ? (questionMessageId + 1).toString()\n        : \"${fakeQuestionMessageId}_ans\";\n\n    return TextMessage(\n      id: answerStreamMessageId,\n      text: '',\n      author: User(id: \"streamId:${nanoid()}\"),\n      metadata: {\n        \"$AnswerStream\": stream,\n        messageQuestionIdKey: questionMessageId,\n        \"chatId\": chatId,\n      },\n      createdAt: DateTime.now(),\n    );\n  }\n\n  /// Create a streaming question message\n  Message createQuestionStreamMessage(\n    QuestionStream stream,\n    Map<String, dynamic>? sentMetadata,\n  ) {\n    final now = DateTime.now();\n    questionStreamMessageId = timestamp().toString();\n\n    return TextMessage(\n      author: User(id: userId),\n      metadata: {\n        \"$QuestionStream\": stream,\n        \"chatId\": chatId,\n        if (sentMetadata != null)\n          messageChatFileListKey: sentMetadata[messageChatFileListKey],\n      },\n      id: questionStreamMessageId,\n      createdAt: now,\n      text: '',\n    );\n  }\n\n  /// Clear error messages from the chat\n  void clearErrorMessages() {\n    final errorMessages = chatController.messages\n        .where(\n          (message) =>\n              onetimeMessageTypeFromMeta(message.metadata) ==\n              OnetimeShotType.error,\n        )\n        .toList();\n\n    for (final message in errorMessages) {\n      chatController.remove(message);\n    }\n  }\n\n  /// Clear related questions from the chat\n  void clearRelatedQuestions() {\n    final relatedQuestionMessages = chatController.messages\n        .where(\n          (message) =>\n              onetimeMessageTypeFromMeta(message.metadata) ==\n              OnetimeShotType.relatedQuestion,\n        )\n        .toList();\n\n    for (final message in relatedQuestionMessages) {\n      chatController.remove(message);\n    }\n  }\n\n  /// Checks if a message is a one-time message\n  bool isOneTimeMessage(Message message) {\n    return message.metadata != null &&\n        message.metadata!.containsKey(onetimeShotType);\n  }\n\n  /// Get the oldest message that is not a one-time message\n  Message? getOldestMessage() {\n    return chatController.messages\n        .firstWhereOrNull((message) => !isOneTimeMessage(message));\n  }\n\n  /// Add a message to the temporary ID map when receiving from server\n  void processReceivedMessage(ChatMessagePB pb) {\n    // 3 means message response from AI\n    if (pb.authorType == 3 && answerStreamMessageId.isNotEmpty) {\n      _temporaryMessageIDMap.putIfAbsent(\n        pb.messageId.toString(),\n        () => answerStreamMessageId,\n      );\n      answerStreamMessageId = '';\n    }\n\n    // 1 means message response from User\n    if (pb.authorType == 1 && questionStreamMessageId.isNotEmpty) {\n      _temporaryMessageIDMap.putIfAbsent(\n        pb.messageId.toString(),\n        () => questionStreamMessageId,\n      );\n      questionStreamMessageId = '';\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_height_manager.dart",
    "content": "import 'dart:math';\n\nimport 'package:universal_platform/universal_platform.dart';\n\nclass MessageHeightConstants {\n  static const String answerSuffix = '_ans';\n\n  static const String withoutMinHeightSuffix = '_without_min_height';\n\n  // This offset comes from the chat input box height + navigation bar height\n  // It's used to calculate the minimum height for answer messages\n  //\n  //  navigation bar height + last user message height\n  //    + last AI message height + chat input box height = screen height\n  static const double defaultDesktopScreenOffset = 220.0;\n  static const double defaultMobileScreenOffset = 304.0;\n\n  static const double relatedQuestionOffset = 72.0;\n}\n\nclass ChatMessageHeightManager {\n  factory ChatMessageHeightManager() => _instance;\n\n  ChatMessageHeightManager._();\n\n  static final ChatMessageHeightManager _instance =\n      ChatMessageHeightManager._();\n\n  final Map<String, double> _heightCache = <String, double>{};\n\n  double get defaultScreenOffset {\n    if (UniversalPlatform.isMobile) {\n      return MessageHeightConstants.defaultMobileScreenOffset;\n    }\n    return MessageHeightConstants.defaultDesktopScreenOffset;\n  }\n\n  /// Cache a message height\n  void cacheHeight({\n    required String messageId,\n    required double height,\n  }) {\n    if (messageId.isEmpty || height <= 0) {\n      assert(false, 'messageId or height is invalid');\n      return;\n    }\n\n    _heightCache[messageId] = height;\n  }\n\n  void cacheWithoutMinHeight({\n    required String messageId,\n    required double height,\n  }) {\n    if (messageId.isEmpty || height <= 0) {\n      assert(false, 'messageId or height is invalid');\n      return;\n    }\n\n    _heightCache[messageId + MessageHeightConstants.withoutMinHeightSuffix] =\n        height;\n  }\n\n  double? getCachedHeight({\n    required String messageId,\n  }) {\n    if (messageId.isEmpty) return null;\n\n    final height = _heightCache[messageId];\n    return height;\n  }\n\n  double? getCachedWithoutMinHeight({\n    required String messageId,\n  }) {\n    if (messageId.isEmpty) return null;\n    final height =\n        _heightCache[messageId + MessageHeightConstants.withoutMinHeightSuffix];\n    return height;\n  }\n\n  /// Calculate minimum height for AI answer messages\n  ///\n  /// For the user message, we don't need to calculate the minimum height\n  double calculateMinHeight({\n    required String messageId,\n    required double screenHeight,\n  }) {\n    if (!isAnswerMessage(messageId)) return 0.0;\n\n    final originalMessageId = getOriginalMessageId(\n      messageId: messageId,\n    );\n    final cachedHeight = getCachedHeight(\n      messageId: originalMessageId,\n    );\n\n    if (cachedHeight == null) {\n      return 0.0;\n    }\n\n    final calculatedHeight = screenHeight - cachedHeight - defaultScreenOffset;\n    return max(calculatedHeight, 0.0);\n  }\n\n  /// Calculate minimum height for related question messages\n  ///\n  /// For the user message, we don't need to calculate the minimum height\n  double calculateRelatedQuestionMinHeight({\n    required String messageId,\n  }) {\n    final cacheHeight = getCachedHeight(\n      messageId: messageId,\n    );\n    final cacheHeightWithoutMinHeight = getCachedWithoutMinHeight(\n      messageId: messageId,\n    );\n    double minHeight = 0;\n    if (cacheHeight != null && cacheHeightWithoutMinHeight != null) {\n      minHeight = cacheHeight -\n          cacheHeightWithoutMinHeight -\n          MessageHeightConstants.relatedQuestionOffset;\n    }\n    minHeight = max(minHeight, 0);\n    return minHeight;\n  }\n\n  bool isAnswerMessage(String messageId) {\n    return messageId.endsWith(MessageHeightConstants.answerSuffix);\n  }\n\n  /// Get the original message ID from an answer message ID\n  ///\n  /// Answer message ID is like: \"message_id_ans\"\n  /// Original message ID is like: \"message_id\"\n  String getOriginalMessageId({\n    required String messageId,\n  }) {\n    if (!isAnswerMessage(messageId)) {\n      return messageId;\n    }\n\n    return messageId.replaceAll(MessageHeightConstants.answerSuffix, '');\n  }\n\n  void removeFromCache({\n    required String messageId,\n  }) {\n    if (messageId.isEmpty) return;\n\n    _heightCache.remove(messageId);\n\n    final answerMessageId = messageId + MessageHeightConstants.answerSuffix;\n    _heightCache.remove(answerMessageId);\n  }\n\n  void clearCache() {\n    _heightCache.clear();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nimport 'chat_notification.dart';\n\ntypedef ChatMessageCallback = void Function(ChatMessagePB message);\ntypedef ChatErrorMessageCallback = void Function(ChatMessageErrorPB message);\ntypedef LatestMessageCallback = void Function(ChatMessageListPB list);\ntypedef PrevMessageCallback = void Function(ChatMessageListPB list);\n\nclass ChatMessageListener {\n  ChatMessageListener({required this.chatId}) {\n    _parser = ChatNotificationParser(id: chatId, callback: _callback);\n    _subscription = RustStreamReceiver.listen(\n      (observable) => _parser?.parse(observable),\n    );\n  }\n\n  final String chatId;\n  StreamSubscription<SubscribeObject>? _subscription;\n  ChatNotificationParser? _parser;\n\n  ChatMessageCallback? chatMessageCallback;\n  ChatErrorMessageCallback? chatErrorMessageCallback;\n  LatestMessageCallback? latestMessageCallback;\n  PrevMessageCallback? prevMessageCallback;\n  void Function()? finishStreamingCallback;\n\n  void start({\n    ChatMessageCallback? chatMessageCallback,\n    ChatErrorMessageCallback? chatErrorMessageCallback,\n    LatestMessageCallback? latestMessageCallback,\n    PrevMessageCallback? prevMessageCallback,\n    void Function()? finishStreamingCallback,\n  }) {\n    this.chatMessageCallback = chatMessageCallback;\n    this.chatErrorMessageCallback = chatErrorMessageCallback;\n    this.latestMessageCallback = latestMessageCallback;\n    this.prevMessageCallback = prevMessageCallback;\n    this.finishStreamingCallback = finishStreamingCallback;\n  }\n\n  void _callback(\n    ChatNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    result.map((r) {\n      switch (ty) {\n        case ChatNotification.DidReceiveChatMessage:\n          chatMessageCallback?.call(ChatMessagePB.fromBuffer(r));\n          break;\n        case ChatNotification.StreamChatMessageError:\n          chatErrorMessageCallback?.call(ChatMessageErrorPB.fromBuffer(r));\n          break;\n        case ChatNotification.DidLoadLatestChatMessage:\n          latestMessageCallback?.call(ChatMessageListPB.fromBuffer(r));\n          break;\n        case ChatNotification.DidLoadPrevChatMessage:\n          prevMessageCallback?.call(ChatMessageListPB.fromBuffer(r));\n          break;\n        case ChatNotification.FinishStreaming:\n          finishStreamingCallback?.call();\n          break;\n        default:\n          break;\n      }\n    });\n  }\n\n  Future<void> stop() async {\n    await _subscription?.cancel();\n    _subscription = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_service.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:nanoid/nanoid.dart';\n\n/// Indicate file source from appflowy document\nconst appflowySource = \"appflowy\";\n\nList<ChatFile> fileListFromMessageMetadata(\n  Map<String, dynamic>? map,\n) {\n  final List<ChatFile> metadata = [];\n  if (map != null) {\n    for (final entry in map.entries) {\n      if (entry.value is ChatFile) {\n        metadata.add(entry.value);\n      }\n    }\n  }\n\n  return metadata;\n}\n\nList<ChatFile> chatFilesFromMetadataString(String? s) {\n  if (s == null || s.isEmpty || s == \"null\") {\n    return [];\n  }\n\n  final metadataJson = jsonDecode(s);\n  if (metadataJson is Map<String, dynamic>) {\n    final file = chatFileFromMap(metadataJson);\n    if (file != null) {\n      return [file];\n    } else {\n      return [];\n    }\n  } else if (metadataJson is List) {\n    return metadataJson\n        .map((e) => e as Map<String, dynamic>)\n        .map(chatFileFromMap)\n        .where((file) => file != null)\n        .cast<ChatFile>()\n        .toList();\n  } else {\n    Log.error(\"Invalid metadata: $metadataJson\");\n    return [];\n  }\n}\n\nChatFile? chatFileFromMap(Map<String, dynamic>? map) {\n  if (map == null) return null;\n\n  final filePath = map['source'] as String?;\n  final fileName = map['name'] as String?;\n\n  if (filePath == null || fileName == null) {\n    return null;\n  }\n  return ChatFile.fromFilePath(filePath);\n}\n\nclass MetadataCollection {\n  MetadataCollection({\n    required this.sources,\n    this.progress,\n  });\n  final List<ChatMessageRefSource> sources;\n  final AIChatProgress? progress;\n}\n\nMetadataCollection parseMetadata(String? s) {\n  if (s == null || s.trim().isEmpty || s.toLowerCase() == \"null\") {\n    return MetadataCollection(sources: []);\n  }\n\n  final List<ChatMessageRefSource> metadata = [];\n  AIChatProgress? progress;\n\n  try {\n    final dynamic decodedJson = jsonDecode(s);\n    if (decodedJson == null) {\n      return MetadataCollection(sources: []);\n    }\n\n    void processMap(Map<String, dynamic> map) {\n      if (map.containsKey(\"step\") && map[\"step\"] != null) {\n        progress = AIChatProgress.fromJson(map);\n      } else if (map.containsKey(\"id\") && map[\"id\"] != null) {\n        metadata.add(ChatMessageRefSource.fromJson(map));\n      } else {\n        Log.info(\"Unsupported metadata format: $map\");\n      }\n    }\n\n    if (decodedJson is Map<String, dynamic>) {\n      processMap(decodedJson);\n    } else if (decodedJson is List) {\n      for (final element in decodedJson) {\n        if (element is Map<String, dynamic>) {\n          processMap(element);\n        } else {\n          Log.error(\"Invalid metadata element: $element\");\n        }\n      }\n    } else {\n      Log.error(\"Invalid metadata format: $decodedJson\");\n    }\n  } catch (e, stacktrace) {\n    Log.error(\"Failed to parse metadata: $e, input: $s\");\n    Log.debug(stacktrace.toString());\n  }\n\n  return MetadataCollection(sources: metadata, progress: progress);\n}\n\nFuture<List<ChatMessageMetaPB>> metadataPBFromMetadata(\n  Map<String, dynamic>? map,\n) async {\n  if (map == null) return [];\n\n  final List<ChatMessageMetaPB> metadata = [];\n\n  for (final value in map.values) {\n    switch (value) {\n      case ViewPB _ when value.layout.isDocumentView:\n        final payload = OpenDocumentPayloadPB(documentId: value.id);\n        await DocumentEventGetDocumentText(payload).send().fold(\n          (pb) {\n            metadata.add(\n              ChatMessageMetaPB(\n                id: value.id,\n                name: value.name,\n                data: pb.text,\n                loaderType: ContextLoaderTypePB.Txt,\n                source: appflowySource,\n              ),\n            );\n          },\n          (err) => Log.error('Failed to get document text: $err'),\n        );\n        break;\n      case ChatFile(\n          filePath: final filePath,\n          fileName: final fileName,\n          fileType: final fileType,\n        ):\n        metadata.add(\n          ChatMessageMetaPB(\n            id: nanoid(8),\n            name: fileName,\n            data: filePath,\n            loaderType: fileType,\n            source: filePath,\n          ),\n        );\n        break;\n    }\n  }\n\n  return metadata;\n}\n\nList<ChatFile> chatFilesFromMessageMetadata(\n  Map<String, dynamic>? map,\n) {\n  final List<ChatFile> metadata = [];\n  if (map != null) {\n    for (final entry in map.entries) {\n      if (entry.value is ChatFile) {\n        metadata.add(entry.value);\n      }\n    }\n  }\n\n  return metadata;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi';\nimport 'dart:isolate';\n\nimport 'package:appflowy/ai/service/ai_entities.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_message_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:json_annotation/json_annotation.dart';\n\npart 'chat_message_stream.g.dart';\n\n/// A stream that receives answer events from an isolate or external process.\n/// It caches events that might occur before a listener is attached.\nclass AnswerStream {\n  AnswerStream() {\n    _port.handler = _controller.add;\n    _subscription = _controller.stream.listen(\n      _handleEvent,\n      onDone: _onDoneCallback,\n      onError: _handleError,\n    );\n  }\n\n  final RawReceivePort _port = RawReceivePort();\n  final StreamController<String> _controller = StreamController.broadcast();\n  late StreamSubscription<String> _subscription;\n\n  bool _hasStarted = false;\n  bool _aiLimitReached = false;\n  bool _aiImageLimitReached = false;\n  String? _error;\n  String _text = \"\";\n\n  // Callbacks\n  void Function(String text)? _onData;\n  void Function()? _onStart;\n  void Function()? _onEnd;\n  void Function(String error)? _onError;\n  void Function()? _onLocalAIInitializing;\n  void Function()? _onAIResponseLimit;\n  void Function()? _onAIImageResponseLimit;\n  void Function(String message)? _onAIMaxRequired;\n  void Function(MetadataCollection metadata)? _onMetadata;\n  void Function(AIFollowUpData)? _onAIFollowUp;\n  // Caches for events that occur before listen() is called.\n  final List<String> _pendingAIMaxRequiredEvents = [];\n  bool _pendingLocalAINotReady = false;\n\n  int get nativePort => _port.sendPort.nativePort;\n  bool get hasStarted => _hasStarted;\n  bool get aiLimitReached => _aiLimitReached;\n  bool get aiImageLimitReached => _aiImageLimitReached;\n  String? get error => _error;\n  String get text => _text;\n\n  /// Releases the resources used by the AnswerStream.\n  Future<void> dispose() async {\n    await _controller.close();\n    await _subscription.cancel();\n    _port.close();\n  }\n\n  /// Handles incoming events from the underlying stream.\n  void _handleEvent(String event) {\n    if (event.startsWith(AIStreamEventPrefix.data)) {\n      _hasStarted = true;\n      final newText = event.substring(AIStreamEventPrefix.data.length);\n      _text += newText;\n      _onData?.call(_text);\n    } else if (event.startsWith(AIStreamEventPrefix.error)) {\n      _error = event.substring(AIStreamEventPrefix.error.length);\n      _onError?.call(_error!);\n    } else if (event.startsWith(AIStreamEventPrefix.metadata)) {\n      final s = event.substring(AIStreamEventPrefix.metadata.length);\n      _onMetadata?.call(parseMetadata(s));\n    } else if (event == AIStreamEventPrefix.aiResponseLimit) {\n      _aiLimitReached = true;\n      _onAIResponseLimit?.call();\n    } else if (event == AIStreamEventPrefix.aiImageResponseLimit) {\n      _aiImageLimitReached = true;\n      _onAIImageResponseLimit?.call();\n    } else if (event.startsWith(AIStreamEventPrefix.aiMaxRequired)) {\n      final msg = event.substring(AIStreamEventPrefix.aiMaxRequired.length);\n      if (_onAIMaxRequired != null) {\n        _onAIMaxRequired!(msg);\n      } else {\n        _pendingAIMaxRequiredEvents.add(msg);\n      }\n    } else if (event.startsWith(AIStreamEventPrefix.localAINotReady)) {\n      if (_onLocalAIInitializing != null) {\n        _onLocalAIInitializing!();\n      } else {\n        _pendingLocalAINotReady = true;\n      }\n    } else if (event.startsWith(AIStreamEventPrefix.aiFollowUp)) {\n      final s = event.substring(AIStreamEventPrefix.aiFollowUp.length);\n      try {\n        final dynamic jsonData = jsonDecode(s);\n        final data = AIFollowUpData.fromJson(jsonData);\n        _onAIFollowUp?.call(data);\n      } catch (e) {\n        Log.error('Error deserializing AIFollowUp data: $e\\nRaw JSON: $s');\n      }\n    }\n  }\n\n  void _onDoneCallback() {\n    _onEnd?.call();\n  }\n\n  void _handleError(dynamic error) {\n    _error = error.toString();\n    _onError?.call(_error!);\n  }\n\n  /// Registers listeners for various events.\n  ///\n  /// If certain events have already occurred (e.g. AI_MAX_REQUIRED or LOCAL_AI_NOT_READY),\n  /// they will be flushed immediately.\n  void listen({\n    void Function(String text)? onData,\n    void Function()? onStart,\n    void Function()? onEnd,\n    void Function(String error)? onError,\n    void Function()? onAIResponseLimit,\n    void Function()? onAIImageResponseLimit,\n    void Function(String message)? onAIMaxRequired,\n    void Function(MetadataCollection metadata)? onMetadata,\n    void Function()? onLocalAIInitializing,\n    void Function(AIFollowUpData)? onAIFollowUp,\n  }) {\n    _onData = onData;\n    _onStart = onStart;\n    _onEnd = onEnd;\n    _onError = onError;\n    _onAIResponseLimit = onAIResponseLimit;\n    _onAIImageResponseLimit = onAIImageResponseLimit;\n    _onAIMaxRequired = onAIMaxRequired;\n    _onMetadata = onMetadata;\n    _onLocalAIInitializing = onLocalAIInitializing;\n    _onAIFollowUp = onAIFollowUp;\n    // Flush pending AI_MAX_REQUIRED events.\n    if (_onAIMaxRequired != null && _pendingAIMaxRequiredEvents.isNotEmpty) {\n      for (final msg in _pendingAIMaxRequiredEvents) {\n        _onAIMaxRequired!(msg);\n      }\n      _pendingAIMaxRequiredEvents.clear();\n    }\n\n    // Flush pending LOCAL_AI_NOT_READY event.\n    if (_pendingLocalAINotReady && _onLocalAIInitializing != null) {\n      _onLocalAIInitializing!();\n      _pendingLocalAINotReady = false;\n    }\n\n    _onStart?.call();\n  }\n}\n\nclass QuestionStream {\n  QuestionStream() {\n    _port.handler = _controller.add;\n    _subscription = _controller.stream.listen(\n      (event) {\n        if (event.startsWith(\"data:\")) {\n          _hasStarted = true;\n          final newText = event.substring(5);\n          _text += newText;\n          if (_onData != null) {\n            _onData!(_text);\n          }\n        } else if (event.startsWith(\"message_id:\")) {\n          final messageId = event.substring(11);\n          _onMessageId?.call(messageId);\n        } else if (event.startsWith(\"start_index_file:\")) {\n          final indexName = event.substring(17);\n          _onFileIndexStart?.call(indexName);\n        } else if (event.startsWith(\"end_index_file:\")) {\n          final indexName = event.substring(10);\n          _onFileIndexEnd?.call(indexName);\n        } else if (event.startsWith(\"index_file_error:\")) {\n          final indexName = event.substring(16);\n          _onFileIndexError?.call(indexName);\n        } else if (event.startsWith(\"index_start:\")) {\n          _onIndexStart?.call();\n        } else if (event.startsWith(\"index_end:\")) {\n          _onIndexEnd?.call();\n        } else if (event.startsWith(\"done:\")) {\n          _onDone?.call();\n        } else if (event.startsWith(\"error:\")) {\n          _error = event.substring(5);\n          if (_onError != null) {\n            _onError!(_error!);\n          }\n        }\n      },\n      onError: (error) {\n        if (_onError != null) {\n          _onError!(error.toString());\n        }\n      },\n    );\n  }\n\n  final RawReceivePort _port = RawReceivePort();\n  final StreamController<String> _controller = StreamController.broadcast();\n  late StreamSubscription<String> _subscription;\n  bool _hasStarted = false;\n  String? _error;\n  String _text = \"\";\n\n  // Callbacks\n  void Function(String text)? _onData;\n  void Function(String error)? _onError;\n  void Function(String messageId)? _onMessageId;\n  void Function(String indexName)? _onFileIndexStart;\n  void Function(String indexName)? _onFileIndexEnd;\n  void Function(String indexName)? _onFileIndexError;\n  void Function()? _onIndexStart;\n  void Function()? _onIndexEnd;\n  void Function()? _onDone;\n  int get nativePort => _port.sendPort.nativePort;\n  bool get hasStarted => _hasStarted;\n  String? get error => _error;\n  String get text => _text;\n\n  Future<void> dispose() async {\n    await _controller.close();\n    await _subscription.cancel();\n    _port.close();\n  }\n\n  void listen({\n    void Function(String text)? onData,\n    void Function(String error)? onError,\n    void Function(String messageId)? onMessageId,\n    void Function(String indexName)? onFileIndexStart,\n    void Function(String indexName)? onFileIndexEnd,\n    void Function(String indexName)? onFileIndexFail,\n    void Function()? onIndexStart,\n    void Function()? onIndexEnd,\n    void Function()? onDone,\n  }) {\n    _onData = onData;\n    _onError = onError;\n    _onMessageId = onMessageId;\n\n    _onFileIndexStart = onFileIndexStart;\n    _onFileIndexEnd = onFileIndexEnd;\n    _onFileIndexError = onFileIndexFail;\n\n    _onIndexStart = onIndexStart;\n    _onIndexEnd = onIndexEnd;\n    _onDone = onDone;\n  }\n}\n\n@JsonSerializable()\nclass AIFollowUpData {\n  AIFollowUpData({\n    required this.shouldGenerateRelatedQuestion,\n  });\n\n  factory AIFollowUpData.fromJson(Map<String, dynamic> json) =>\n      _$AIFollowUpDataFromJson(json);\n\n  @JsonKey(name: 'should_generate_related_question')\n  final bool shouldGenerateRelatedQuestion;\n\n  Map<String, dynamic> toJson() => _$AIFollowUpDataToJson(this);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_notification.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/notification_helper.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass ChatNotificationParser\n    extends NotificationParser<ChatNotification, FlowyError> {\n  ChatNotificationParser({\n    super.id,\n    required super.callback,\n  }) : super(\n          tyParser: (ty, source) =>\n              source == \"Chat\" ? ChatNotification.valueOf(ty) : null,\n          errorParser: (bytes) => FlowyError.fromBuffer(bytes),\n        );\n}\n\ntypedef ChatNotificationHandler = Function(\n  ChatNotification ty,\n  FlowyResult<Uint8List, FlowyError> result,\n);\n\nclass ChatNotificationListener {\n  ChatNotificationListener({\n    required String objectId,\n    required ChatNotificationHandler handler,\n  }) : _parser = ChatNotificationParser(id: objectId, callback: handler) {\n    _subscription =\n        RustStreamReceiver.listen((observable) => _parser?.parse(observable));\n  }\n\n  ChatNotificationParser? _parser;\n  StreamSubscription<SubscribeObject>? _subscription;\n\n  Future<void> stop() async {\n    _parser = null;\n    await _subscription?.cancel();\n    _subscription = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_message_bloc.dart",
    "content": "import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';\nimport 'package:appflowy/plugins/util.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'chat_select_message_bloc.freezed.dart';\n\nclass ChatSelectMessageBloc\n    extends Bloc<ChatSelectMessageEvent, ChatSelectMessageState> {\n  ChatSelectMessageBloc({required this.viewNotifier})\n      : super(ChatSelectMessageState.initial()) {\n    _dispatch();\n  }\n\n  final ViewPluginNotifier viewNotifier;\n\n  void _dispatch() {\n    on<ChatSelectMessageEvent>(\n      (event, emit) {\n        event.when(\n          enableStartSelectingMessages: () {\n            emit(state.copyWith(enabled: true));\n          },\n          toggleSelectingMessages: () {\n            if (state.isSelectingMessages) {\n              emit(\n                state.copyWith(\n                  isSelectingMessages: false,\n                  selectedMessages: [],\n                ),\n              );\n            } else {\n              emit(state.copyWith(isSelectingMessages: true));\n            }\n          },\n          toggleSelectMessage: (Message message) {\n            if (state.selectedMessages.contains(message)) {\n              emit(\n                state.copyWith(\n                  selectedMessages: state.selectedMessages\n                      .where((m) => m != message)\n                      .toList(),\n                ),\n              );\n            } else {\n              emit(\n                state.copyWith(\n                  selectedMessages: [...state.selectedMessages, message],\n                ),\n              );\n            }\n          },\n          selectAllMessages: (List<Message> messages) {\n            final filtered = messages.where(isAIMessage).toList();\n            emit(state.copyWith(selectedMessages: filtered));\n          },\n          unselectAllMessages: () {\n            emit(state.copyWith(selectedMessages: const []));\n          },\n          reset: () {\n            emit(\n              state.copyWith(\n                isSelectingMessages: false,\n                selectedMessages: [],\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  bool isMessageSelected(String messageId) =>\n      state.selectedMessages.any((m) => m.id == messageId);\n\n  bool isAIMessage(Message message) {\n    return message.author.id == aiResponseUserId ||\n        message.author.id == systemUserId ||\n        message.author.id.startsWith(\"streamId:\");\n  }\n}\n\n@freezed\nclass ChatSelectMessageEvent with _$ChatSelectMessageEvent {\n  const factory ChatSelectMessageEvent.enableStartSelectingMessages() =\n      _EnableStartSelectingMessages;\n  const factory ChatSelectMessageEvent.toggleSelectingMessages() =\n      _ToggleSelectingMessages;\n  const factory ChatSelectMessageEvent.toggleSelectMessage(Message message) =\n      _ToggleSelectMessage;\n  const factory ChatSelectMessageEvent.selectAllMessages(\n    List<Message> messages,\n  ) = _SelectAllMessages;\n  const factory ChatSelectMessageEvent.unselectAllMessages() =\n      _UnselectAllMessages;\n  const factory ChatSelectMessageEvent.reset() = _Reset;\n}\n\n@freezed\nclass ChatSelectMessageState with _$ChatSelectMessageState {\n  const factory ChatSelectMessageState({\n    required bool isSelectingMessages,\n    required List<Message> selectedMessages,\n    required bool enabled,\n  }) = _ChatSelectMessageState;\n\n  factory ChatSelectMessageState.initial() => const ChatSelectMessageState(\n        enabled: false,\n        isSelectingMessages: false,\n        selectedMessages: [],\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_settings_manager.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter/foundation.dart';\n\n/// Manages settings for a chat\nclass ChatSettingsManager {\n  ChatSettingsManager({\n    required this.chatId,\n  }) : selectedSourcesNotifier = ValueNotifier([]);\n  final String chatId;\n\n  /// Notifies listeners when selected sources change\n  final ValueNotifier<List<String>> selectedSourcesNotifier;\n\n  /// Load settings from backend\n  Future<void> loadSettings() async {\n    final getChatSettingsPayload =\n        AIEventGetChatSettings(ChatId(value: chatId));\n\n    await getChatSettingsPayload.send().then((result) {\n      result.fold(\n        (settings) {\n          selectedSourcesNotifier.value = settings.ragIds;\n        },\n        (err) => Log.error(\"Failed to load chat settings: $err\"),\n      );\n    });\n  }\n\n  /// Update selected sources\n  Future<void> updateSelectedSources(List<String> selectedSourcesIds) async {\n    selectedSourcesNotifier.value = [...selectedSourcesIds];\n\n    final payload = UpdateChatSettingsPB(\n      chatId: ChatId(value: chatId),\n      ragIds: selectedSourcesIds,\n    );\n\n    await AIEventUpdateChatSettings(payload).send().onFailure(Log.error);\n  }\n\n  /// Clean up resources\n  void dispose() {\n    selectedSourcesNotifier.dispose();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_stream_manager.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:fixnum/fixnum.dart';\n\nimport 'chat_message_stream.dart';\n\n/// Manages chat streaming operations\nclass ChatStreamManager {\n  ChatStreamManager(this.chatId);\n  final String chatId;\n\n  AnswerStream? answerStream;\n  QuestionStream? questionStream;\n\n  /// Dispose of all streams\n  Future<void> dispose() async {\n    await answerStream?.dispose();\n    answerStream = null;\n\n    await questionStream?.dispose();\n    questionStream = null;\n  }\n\n  /// Prepare streams for a new message\n  Future<void> prepareStreams() async {\n    await dispose();\n    answerStream = AnswerStream();\n    questionStream = QuestionStream();\n  }\n\n  /// Build the payload for a streaming message\n  StreamChatPayloadPB buildStreamPayload(\n    String message,\n    PredefinedFormat? format,\n    String? promptId,\n  ) {\n    final payload = StreamChatPayloadPB(\n      chatId: chatId,\n      message: message,\n      messageType: ChatMessageTypePB.User,\n      questionStreamPort: Int64(questionStream!.nativePort),\n      answerStreamPort: Int64(answerStream!.nativePort),\n    );\n\n    if (format != null) {\n      payload.format = format.toPB();\n    }\n\n    if (promptId != null) {\n      payload.promptId = promptId;\n    }\n\n    return payload;\n  }\n\n  /// Send a streaming message request to the server\n  Future<FlowyResult<ChatMessagePB, FlowyError>> sendStreamRequest(\n    String message,\n    PredefinedFormat? format,\n    String? promptId,\n  ) async {\n    final payload = buildStreamPayload(message, format, promptId);\n    return AIEventStreamMessage(payload).send();\n  }\n\n  /// Build the payload for regenerating a response\n  RegenerateResponsePB buildRegeneratePayload(\n    Int64 answerMessageId,\n    PredefinedFormat? format,\n    AIModelPB? model,\n  ) {\n    final payload = RegenerateResponsePB(\n      chatId: chatId,\n      answerMessageId: answerMessageId,\n      answerStreamPort: Int64(answerStream!.nativePort),\n    );\n\n    if (format != null) {\n      payload.format = format.toPB();\n    }\n\n    if (model != null) {\n      payload.model = model;\n    }\n\n    return payload;\n  }\n\n  /// Send a request to regenerate a response\n  Future<FlowyResult<dynamic, FlowyError>> sendRegenerateRequest(\n    Int64 answerMessageId,\n    PredefinedFormat? format,\n    AIModelPB? model,\n  ) async {\n    final payload = buildRegeneratePayload(answerMessageId, format, model);\n    return AIEventRegenerateResponse(payload).send();\n  }\n\n  /// Stop the current streaming message\n  Future<void> stopStream() async {\n    if (answerStream == null) {\n      return;\n    }\n\n    final payload = StopStreamPB(chatId: chatId);\n    await AIEventStopStream(payload).send();\n  }\n\n  /// Check if the answer stream has started\n  bool get hasAnswerStreamStarted =>\n      answerStream != null && answerStream!.hasStarted;\n\n  Future<void> disposeAnswerStream() async {\n    if (answerStream == null) {\n      return;\n    }\n\n    await answerStream!.dispose();\n    answerStream = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_cubit.dart",
    "content": "import 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:equatable/equatable.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\n/// ChatUserCubit is responsible for fetching and storing the user profile\nclass ChatUserCubit extends Cubit<ChatUserState> {\n  ChatUserCubit() : super(ChatUserLoadingState()) {\n    fetchUserProfile();\n  }\n\n  /// Fetches the user profile from the AuthService\n  Future<void> fetchUserProfile() async {\n    emit(ChatUserLoadingState());\n    final userOrFailure = await getIt<AuthService>().getUser();\n\n    userOrFailure.fold(\n      (userProfile) => emit(ChatUserSuccessState(userProfile)),\n      (error) => emit(ChatUserFailureState(error)),\n    );\n  }\n\n  bool supportSelectSource() {\n    if (state is ChatUserSuccessState) {\n      final userProfile = (state as ChatUserSuccessState).userProfile;\n      if (userProfile.userAuthType == AuthTypePB.Server) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  bool isValueWorkspace() {\n    if (state is ChatUserSuccessState) {\n      final userProfile = (state as ChatUserSuccessState).userProfile;\n      return userProfile.workspaceType == WorkspaceTypePB.LocalW &&\n          userProfile.userAuthType != AuthTypePB.Local;\n    }\n    return false;\n  }\n\n  /// Refreshes the user profile data\n  Future<void> refresh() async {\n    await fetchUserProfile();\n  }\n}\n\n/// Base state class for ChatUserCubit\nabstract class ChatUserState extends Equatable {\n  const ChatUserState();\n\n  @override\n  List<Object?> get props => [];\n}\n\n/// Loading state when fetching user profile\nclass ChatUserLoadingState extends ChatUserState {}\n\n/// Success state when user profile is fetched successfully\nclass ChatUserSuccessState extends ChatUserState {\n  const ChatUserSuccessState(this.userProfile);\n  final UserProfilePB userProfile;\n\n  @override\n  List<Object?> get props => [userProfile];\n}\n\n/// Failure state when fetching user profile fails\nclass ChatUserFailureState extends ChatUserState {\n  const ChatUserFailureState(this.error);\n  final FlowyError error;\n\n  @override\n  List<Object?> get props => [error];\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart",
    "content": "import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'chat_user_message_bloc.freezed.dart';\n\nclass ChatUserMessageBloc\n    extends Bloc<ChatUserMessageEvent, ChatUserMessageState> {\n  ChatUserMessageBloc({\n    required this.questionStream,\n    required String text,\n  }) : super(ChatUserMessageState.initial(text)) {\n    _dispatch();\n    _startListening();\n  }\n\n  final QuestionStream? questionStream;\n\n  void _dispatch() {\n    on<ChatUserMessageEvent>(\n      (event, emit) {\n        event.when(\n          updateText: (String text) {\n            emit(state.copyWith(text: text));\n          },\n          updateMessageId: (String messageId) {\n            emit(state.copyWith(messageId: messageId));\n          },\n          receiveError: (String error) {},\n          updateQuestionState: (QuestionMessageState newState) {\n            emit(state.copyWith(messageState: newState));\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    questionStream?.listen(\n      onData: (text) {\n        if (!isClosed) {\n          add(ChatUserMessageEvent.updateText(text));\n        }\n      },\n      onMessageId: (messageId) {\n        if (!isClosed) {\n          add(ChatUserMessageEvent.updateMessageId(messageId));\n        }\n      },\n      onError: (error) {\n        if (!isClosed) {\n          add(ChatUserMessageEvent.receiveError(error.toString()));\n        }\n      },\n      onFileIndexStart: (indexName) {\n        Log.debug(\"index start: $indexName\");\n      },\n      onFileIndexEnd: (indexName) {\n        Log.info(\"index end: $indexName\");\n      },\n      onFileIndexFail: (indexName) {\n        Log.debug(\"index fail: $indexName\");\n      },\n      onIndexStart: () {\n        if (!isClosed) {\n          add(\n            const ChatUserMessageEvent.updateQuestionState(\n              QuestionMessageState.indexStart(),\n            ),\n          );\n        }\n      },\n      onIndexEnd: () {\n        if (!isClosed) {\n          add(\n            const ChatUserMessageEvent.updateQuestionState(\n              QuestionMessageState.indexEnd(),\n            ),\n          );\n        }\n      },\n      onDone: () {\n        if (!isClosed) {\n          add(\n            const ChatUserMessageEvent.updateQuestionState(\n              QuestionMessageState.finish(),\n            ),\n          );\n        }\n      },\n    );\n  }\n}\n\n@freezed\nclass ChatUserMessageEvent with _$ChatUserMessageEvent {\n  const factory ChatUserMessageEvent.updateText(String text) = _UpdateText;\n  const factory ChatUserMessageEvent.updateQuestionState(\n    QuestionMessageState newState,\n  ) = _UpdateQuestionState;\n  const factory ChatUserMessageEvent.updateMessageId(String messageId) =\n      _UpdateMessageId;\n  const factory ChatUserMessageEvent.receiveError(String error) = _ReceiveError;\n}\n\n@freezed\nclass ChatUserMessageState with _$ChatUserMessageState {\n  const factory ChatUserMessageState({\n    required String text,\n    required String? messageId,\n    required QuestionMessageState messageState,\n  }) = _ChatUserMessageState;\n\n  factory ChatUserMessageState.initial(String message) => ChatUserMessageState(\n        text: message,\n        messageId: null,\n        messageState: const QuestionMessageState.finish(),\n      );\n}\n\n@freezed\nclass QuestionMessageState with _$QuestionMessageState {\n  const factory QuestionMessageState.indexFileStart(String fileName) =\n      _IndexFileStart;\n  const factory QuestionMessageState.indexFileEnd(String fileName) =\n      _IndexFileEnd;\n  const factory QuestionMessageState.indexFileFail(String fileName) =\n      _IndexFileFail;\n\n  const factory QuestionMessageState.indexStart() = _IndexStart;\n  const factory QuestionMessageState.indexEnd() = _IndexEnd;\n  const factory QuestionMessageState.finish() = _Finish;\n}\n\nextension QuestionMessageStateX on QuestionMessageState {\n  bool get isFinish => this is _Finish;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart",
    "content": "import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_select_message_bloc.dart';\nimport 'package:appflowy/plugins/ai_chat/chat_page.dart';\nimport 'package:appflowy/plugins/util.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_stack.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';\nimport 'package:appflowy/workspace/presentation/widgets/favorite_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart';\nimport 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart';\nimport 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';\nimport 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass AIChatPluginBuilder extends PluginBuilder {\n  @override\n  Plugin build(dynamic data) {\n    if (data is ViewPB) {\n      return AIChatPagePlugin(view: data);\n    }\n\n    throw FlowyPluginException.invalidData;\n  }\n\n  @override\n  String get menuName => \"AI Chat\";\n\n  @override\n  FlowySvgData get icon => FlowySvgs.chat_ai_page_s;\n\n  @override\n  PluginType get pluginType => PluginType.chat;\n\n  @override\n  ViewLayoutPB get layoutType => ViewLayoutPB.Chat;\n}\n\nclass AIChatPluginConfig implements PluginConfig {\n  @override\n  bool get creatable => true;\n}\n\nclass AIChatPagePlugin extends Plugin {\n  AIChatPagePlugin({\n    required ViewPB view,\n  }) : notifier = ViewPluginNotifier(view: view);\n\n  late final ViewInfoBloc _viewInfoBloc;\n  late final PageAccessLevelBloc _pageAccessLevelBloc;\n  late final _chatMessageSelectorBloc =\n      ChatSelectMessageBloc(viewNotifier: notifier);\n\n  @override\n  final ViewPluginNotifier notifier;\n\n  @override\n  PluginWidgetBuilder get widgetBuilder => AIChatPagePluginWidgetBuilder(\n        viewInfoBloc: _viewInfoBloc,\n        pageAccessLevelBloc: _pageAccessLevelBloc,\n        chatMessageSelectorBloc: _chatMessageSelectorBloc,\n        notifier: notifier,\n      );\n\n  @override\n  PluginId get id => notifier.view.id;\n\n  @override\n  PluginType get pluginType => PluginType.chat;\n\n  @override\n  void init() {\n    _viewInfoBloc = ViewInfoBloc(view: notifier.view)\n      ..add(const ViewInfoEvent.started());\n    _pageAccessLevelBloc = PageAccessLevelBloc(view: notifier.view)\n      ..add(const PageAccessLevelEvent.initial());\n  }\n\n  @override\n  void dispose() {\n    _viewInfoBloc.close();\n    _pageAccessLevelBloc.close();\n    _chatMessageSelectorBloc.close();\n    notifier.dispose();\n  }\n}\n\nclass AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder\n    with NavigationItem {\n  AIChatPagePluginWidgetBuilder({\n    required this.viewInfoBloc,\n    required this.pageAccessLevelBloc,\n    required this.chatMessageSelectorBloc,\n    required this.notifier,\n  });\n\n  final ViewInfoBloc viewInfoBloc;\n  final PageAccessLevelBloc pageAccessLevelBloc;\n  final ChatSelectMessageBloc chatMessageSelectorBloc;\n  final ViewPluginNotifier notifier;\n  int? deletedViewIndex;\n\n  @override\n  String? get viewName => notifier.view.nameOrDefault;\n\n  @override\n  Widget get leftBarItem {\n    return BlocProvider.value(\n      value: pageAccessLevelBloc,\n      child: ViewTitleBar(key: ValueKey(notifier.view.id), view: notifier.view),\n    );\n  }\n\n  @override\n  Widget tabBarItem(String pluginId, [bool shortForm = false]) =>\n      ViewTabBarItem(view: notifier.view, shortForm: shortForm);\n\n  @override\n  Widget buildWidget({\n    required PluginContext context,\n    required bool shrinkWrap,\n    Map<String, dynamic>? data,\n  }) {\n    notifier.isDeleted.addListener(_onDeleted);\n\n    if (context.userProfile == null) {\n      Log.error(\"User profile is null when opening AI Chat plugin\");\n      return const SizedBox();\n    }\n\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider.value(value: chatMessageSelectorBloc),\n        BlocProvider.value(value: viewInfoBloc),\n        BlocProvider.value(value: pageAccessLevelBloc),\n      ],\n      child: AIChatPage(\n        userProfile: context.userProfile!,\n        key: ValueKey(notifier.view.id),\n        view: notifier.view,\n        onDeleted: () =>\n            context.onDeleted?.call(notifier.view, deletedViewIndex),\n      ),\n    );\n  }\n\n  void _onDeleted() {\n    final deletedView = notifier.isDeleted.value;\n    if (deletedView != null && deletedView.hasIndex()) {\n      deletedViewIndex = deletedView.index;\n    }\n  }\n\n  @override\n  List<NavigationItem> get navigationItems => [this];\n\n  @override\n  EdgeInsets get contentPadding => EdgeInsets.zero;\n\n  @override\n  Widget? get rightBarItem => MultiBlocProvider(\n        providers: [\n          BlocProvider.value(value: viewInfoBloc),\n          BlocProvider.value(value: chatMessageSelectorBloc),\n        ],\n        child: BlocBuilder<ChatSelectMessageBloc, ChatSelectMessageState>(\n          builder: (context, state) {\n            if (state.isSelectingMessages) {\n              return const SizedBox.shrink();\n            }\n\n            return Row(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                ViewFavoriteButton(\n                  key: ValueKey('favorite_button_${notifier.view.id}'),\n                  view: notifier.view,\n                ),\n                const HSpace(4),\n                MoreViewActions(\n                  key: ValueKey(notifier.view.id),\n                  view: notifier.view,\n                  customActions: [\n                    CustomViewAction(\n                      view: notifier.view,\n                      disabled: !state.enabled,\n                      leftIcon: FlowySvgs.ai_add_to_page_s,\n                      label: LocaleKeys.moreAction_saveAsNewPage.tr(),\n                      tooltipMessage: state.enabled\n                          ? null\n                          : LocaleKeys.moreAction_saveAsNewPageDisabled.tr(),\n                      onTap: () {\n                        chatMessageSelectorBloc.add(\n                          const ChatSelectMessageEvent\n                              .toggleSelectingMessages(),\n                        );\n                      },\n                    ),\n                    ViewAction(\n                      type: ViewMoreActionType.divider,\n                      view: notifier.view,\n                    ),\n                  ],\n                ),\n              ],\n            );\n          },\n        ),\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/chat_page/chat_content_page.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:desktop_drop/desktop_drop.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'application/chat_bloc.dart';\nimport 'application/chat_member_bloc.dart';\n\nclass AIChatPage extends StatelessWidget {\n  const AIChatPage({\n    super.key,\n    required this.view,\n    required this.onDeleted,\n    required this.userProfile,\n  });\n\n  final ViewPB view;\n  final VoidCallback onDeleted;\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        /// [ChatBloc] is used to handle chat messages including send/receive message\n        BlocProvider(\n          create: (_) => ChatBloc(\n            chatId: view.id,\n            userId: userProfile.id.toString(),\n          ),\n        ),\n\n        /// [AIPromptInputBloc] is used to handle the user prompt\n        BlocProvider(\n          create: (_) => AIPromptInputBloc(\n            objectId: view.id,\n            predefinedFormat: PredefinedFormat(\n              imageFormat: ImageFormat.text,\n              textFormat: TextFormat.bulletList,\n            ),\n          ),\n        ),\n        BlocProvider(create: (_) => ChatMemberBloc()),\n      ],\n      child: Builder(\n        builder: (context) {\n          return DropTarget(\n            onDragDone: (DropDoneDetails detail) async {\n              if (context.read<AIPromptInputBloc>().state.supportChatWithFile) {\n                for (final file in detail.files) {\n                  context\n                      .read<AIPromptInputBloc>()\n                      .add(AIPromptInputEvent.attachFile(file.path, file.name));\n                }\n              }\n            },\n            child: FocusScope(\n              onKeyEvent: (focusNode, event) {\n                if (event is! KeyUpEvent) {\n                  return KeyEventResult.ignored;\n                }\n\n                if (event.logicalKey == LogicalKeyboardKey.escape ||\n                    event.logicalKey == LogicalKeyboardKey.keyC &&\n                        HardwareKeyboard.instance.isControlPressed) {\n                  final chatBloc = context.read<ChatBloc>();\n                  if (!chatBloc.state.promptResponseState.isReady) {\n                    chatBloc.add(ChatEvent.stopStream());\n                    return KeyEventResult.handled;\n                  }\n                }\n\n                return KeyEventResult.ignored;\n              },\n              child: ChatContentPage(\n                view: view,\n                userProfile: userProfile,\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/animated_chat_list.dart",
    "content": "// ignore_for_file: implementation_imports\n\nimport 'dart:async';\n\nimport 'package:appflowy/util/debounce.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:diffutil_dart/diffutil.dart' as diffutil;\nimport 'package:flutter/material.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\nimport 'package:flutter_chat_ui/src/scroll_to_bottom.dart';\nimport 'package:flutter_chat_ui/src/utils/message_list_diff.dart';\nimport 'package:provider/provider.dart';\nimport 'package:scrollable_positioned_list/scrollable_positioned_list.dart';\n\nimport '../application/chat_message_height_manager.dart';\nimport 'widgets/message_height_calculator.dart';\n\nclass ChatAnimatedList extends StatefulWidget {\n  const ChatAnimatedList({\n    super.key,\n    required this.scrollController,\n    required this.itemBuilder,\n    this.insertAnimationDuration = const Duration(milliseconds: 250),\n    this.removeAnimationDuration = const Duration(milliseconds: 250),\n    this.scrollToEndAnimationDuration = const Duration(milliseconds: 250),\n    this.scrollToBottomAppearanceDelay = const Duration(milliseconds: 250),\n    this.bottomPadding = 8,\n    this.onLoadPreviousMessages,\n    this.scrollBottomPadding = 440,\n  });\n\n  final ScrollController scrollController;\n  final ChatItem itemBuilder;\n  final Duration insertAnimationDuration;\n  final Duration removeAnimationDuration;\n  final Duration scrollToEndAnimationDuration;\n  final Duration scrollToBottomAppearanceDelay;\n  final double? bottomPadding;\n  final VoidCallback? onLoadPreviousMessages;\n  final double scrollBottomPadding;\n\n  @override\n  State<ChatAnimatedList> createState() => _ChatAnimatedListState();\n}\n\nclass _ChatAnimatedListState extends State<ChatAnimatedList>\n    with SingleTickerProviderStateMixin {\n  late final ChatController chatController = Provider.of<ChatController>(\n    context,\n    listen: false,\n  );\n  late List<Message> oldList;\n  late StreamSubscription<ChatOperation> operationsSubscription;\n\n  late final AnimationController scrollToBottomController;\n  late final Animation<double> scrollToBottomAnimation;\n  Timer? scrollToBottomShowTimer;\n\n  final ScrollOffsetController scrollOffsetController =\n      ScrollOffsetController();\n\n  final ItemScrollController itemScrollController = ItemScrollController();\n\n  final ScrollOffsetListener scrollOffsetListener =\n      ScrollOffsetListener.create();\n\n  final ItemPositionsListener itemPositionsListener =\n      ItemPositionsListener.create();\n\n  int lastUserMessageIndex = 0;\n  bool isScrollingToBottom = false;\n\n  final loadPreviousMessagesDebounce = Debounce(\n    duration: const Duration(milliseconds: 200),\n  );\n\n  int initialScrollIndex = 0;\n  double initialAlignment = 1.0;\n  List<Message> messages = [];\n\n  final ChatMessageHeightManager heightManager = ChatMessageHeightManager();\n\n  @override\n  void initState() {\n    super.initState();\n\n    // TODO: Add assert for messages having same id\n    oldList = List.from(chatController.messages);\n    operationsSubscription = chatController.operationsStream.listen((event) {\n      setState(() {\n        messages = chatController.messages;\n      });\n      switch (event.type) {\n        case ChatOperationType.insert:\n          assert(\n            event.index != null,\n            'Index must be provided when inserting a message.',\n          );\n          assert(\n            event.message != null,\n            'Message must be provided when inserting a message.',\n          );\n\n          _onInserted(event.index!, event.message!);\n          oldList = List.from(chatController.messages);\n          break;\n        case ChatOperationType.remove:\n          assert(\n            event.index != null,\n            'Index must be provided when removing a message.',\n          );\n          assert(\n            event.message != null,\n            'Message must be provided when removing a message.',\n          );\n\n          _onRemoved(event.index!, event.message!);\n          oldList = List.from(chatController.messages);\n          break;\n        case ChatOperationType.set:\n          final newList = chatController.messages;\n\n          final updates = diffutil\n              .calculateDiff<Message>(\n                MessageListDiff(oldList, newList),\n              )\n              .getUpdatesWithData();\n\n          for (var i = updates.length - 1; i >= 0; i--) {\n            _onDiffUpdate(updates.elementAt(i));\n          }\n\n          oldList = List.from(newList);\n          break;\n        default:\n          break;\n      }\n    });\n\n    messages = chatController.messages;\n\n    scrollToBottomController = AnimationController(\n      vsync: this,\n      duration: const Duration(milliseconds: 300),\n    );\n\n    scrollToBottomAnimation = CurvedAnimation(\n      parent: scrollToBottomController,\n      curve: Curves.easeInOut,\n    );\n\n    itemPositionsListener.itemPositions.addListener(() {\n      _handleToggleScrollToBottom();\n    });\n\n    itemPositionsListener.itemPositions.addListener(() {\n      _handleLoadPreviousMessages();\n    });\n  }\n\n  @override\n  void dispose() {\n    scrollToBottomShowTimer?.cancel();\n    scrollToBottomController.dispose();\n    operationsSubscription.cancel();\n\n    _clearMessageHeightCache();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final builders = context.watch<Builders>();\n\n    // A trick to avoid the first message being scrolled to the top\n    initialScrollIndex = messages.length;\n    initialAlignment = 1.0;\n    if (messages.length <= 2) {\n      initialScrollIndex = 0;\n      initialAlignment = 0.0;\n    }\n\n    final Widget child = Stack(\n      children: [\n        ScrollablePositionedList.builder(\n          scrollOffsetController: scrollOffsetController,\n          itemScrollController: itemScrollController,\n          initialScrollIndex: initialScrollIndex,\n          initialAlignment: initialAlignment,\n          scrollOffsetListener: scrollOffsetListener,\n          itemPositionsListener: itemPositionsListener,\n          physics: ClampingScrollPhysics(),\n          shrinkWrap: true,\n          // the extra item is a vertical padding.\n          itemCount: messages.length + 1,\n          itemBuilder: (context, index) {\n            if (index < 0 || index > messages.length) {\n              Log.error('[chat animation list] index out of range: $index');\n              return const SizedBox.shrink();\n            }\n\n            if (index == messages.length) {\n              return const SizedBox.shrink();\n            }\n\n            final message = messages[index];\n            return MessageHeightCalculator(\n              messageId: message.id,\n              onHeightMeasured: _cacheMessageHeight,\n              child: widget.itemBuilder(\n                context,\n                Tween<double>(begin: 1, end: 1).animate(\n                  CurvedAnimation(\n                    parent: scrollToBottomController,\n                    curve: Curves.easeInOut,\n                  ),\n                ),\n                message,\n              ),\n            );\n          },\n        ),\n        builders.scrollToBottomBuilder?.call(\n              context,\n              scrollToBottomAnimation,\n              _handleScrollToBottom,\n            ) ??\n            ScrollToBottom(\n              animation: scrollToBottomAnimation,\n              onPressed: _handleScrollToBottom,\n            ),\n      ],\n    );\n\n    return child;\n  }\n\n  Future<void> _scrollLastUserMessageToTop() async {\n    final user = Provider.of<User>(context, listen: false);\n    final lastUserMessageIndex = messages.lastIndexWhere(\n      (message) => message.author.id == user.id,\n    );\n\n    // waiting for the ai answer message to be inserted\n    if (lastUserMessageIndex == -1 ||\n        lastUserMessageIndex + 1 >= messages.length) {\n      return;\n    }\n\n    if (this.lastUserMessageIndex != lastUserMessageIndex) {\n      // scroll the current message to the top\n      await itemScrollController.scrollTo(\n        index: lastUserMessageIndex,\n        duration: const Duration(milliseconds: 300),\n        curve: Curves.easeInOut,\n      );\n    }\n\n    this.lastUserMessageIndex = lastUserMessageIndex;\n  }\n\n  Future<void> _handleScrollToBottom() async {\n    isScrollingToBottom = true;\n\n    scrollToBottomShowTimer?.cancel();\n\n    await scrollToBottomController.reverse();\n\n    await itemScrollController.scrollTo(\n      index: messages.length + 1,\n      alignment: 1.0,\n      duration: const Duration(milliseconds: 300),\n      curve: Curves.easeInOut,\n    );\n\n    isScrollingToBottom = false;\n  }\n\n  void _handleToggleScrollToBottom() {\n    if (isScrollingToBottom) {\n      return;\n    }\n\n    // get the max item\n    final sortedItems = itemPositionsListener.itemPositions.value.toList()\n      ..sort((a, b) => a.index.compareTo(b.index));\n    final maxItem = sortedItems.lastOrNull;\n\n    if (maxItem == null) {\n      return;\n    }\n\n    if (maxItem.index > messages.length - 1 ||\n        (maxItem.index == messages.length - 1 &&\n            maxItem.itemTrailingEdge <= 1.01)) {\n      scrollToBottomShowTimer?.cancel();\n      scrollToBottomController.reverse();\n      return;\n    }\n\n    scrollToBottomShowTimer?.cancel();\n    scrollToBottomShowTimer = Timer(widget.scrollToBottomAppearanceDelay, () {\n      if (mounted) {\n        scrollToBottomController.forward();\n      }\n    });\n  }\n\n  void _handleLoadPreviousMessages() {\n    final sortedItems = itemPositionsListener.itemPositions.value.toList()\n      ..sort((a, b) => a.index.compareTo(b.index));\n    final minItem = sortedItems.firstOrNull;\n\n    if (minItem == null || minItem.index > 0 || minItem.itemLeadingEdge < 0) {\n      return;\n    }\n\n    loadPreviousMessagesDebounce.call(\n      () {\n        widget.onLoadPreviousMessages?.call();\n      },\n    );\n  }\n\n  void _cacheMessageHeight(String messageId, double height) {\n    heightManager.cacheHeight(messageId: messageId, height: height);\n  }\n\n  void _clearMessageHeightCache() {\n    heightManager.clearCache();\n  }\n\n  Future<void> _onInserted(final int position, final Message data) async {\n    // scroll the last user message to the top if it's the last message\n    if (position == oldList.length) {\n      await _scrollLastUserMessageToTop();\n    }\n  }\n\n  void _onRemoved(final int position, final Message data) {\n    // Clean up cached height for removed message\n    heightManager.removeFromCache(messageId: data.id);\n  }\n\n  void _onDiffUpdate(diffutil.DataDiffUpdate<Message> update) {\n    // do nothing\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/animated_chat_list_reversed.dart",
    "content": "// ignore_for_file: implementation_imports\n\nimport 'dart:async';\nimport 'dart:math';\n\nimport 'package:diffutil_dart/diffutil.dart' as diffutil;\nimport 'package:flutter/material.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\nimport 'package:flutter_chat_ui/src/scroll_to_bottom.dart';\nimport 'package:flutter_chat_ui/src/utils/message_list_diff.dart';\nimport 'package:provider/provider.dart';\n\nclass ChatAnimatedListReversed extends StatefulWidget {\n  const ChatAnimatedListReversed({\n    super.key,\n    required this.scrollController,\n    required this.itemBuilder,\n    this.insertAnimationDuration = const Duration(milliseconds: 250),\n    this.removeAnimationDuration = const Duration(milliseconds: 250),\n    this.scrollToEndAnimationDuration = const Duration(milliseconds: 250),\n    this.scrollToBottomAppearanceDelay = const Duration(milliseconds: 250),\n    this.bottomPadding = 8,\n    this.onLoadPreviousMessages,\n  });\n\n  final ScrollController scrollController;\n  final ChatItem itemBuilder;\n  final Duration insertAnimationDuration;\n  final Duration removeAnimationDuration;\n  final Duration scrollToEndAnimationDuration;\n  final Duration scrollToBottomAppearanceDelay;\n  final double? bottomPadding;\n  final VoidCallback? onLoadPreviousMessages;\n\n  @override\n  ChatAnimatedListReversedState createState() =>\n      ChatAnimatedListReversedState();\n}\n\nclass ChatAnimatedListReversedState extends State<ChatAnimatedListReversed>\n    with SingleTickerProviderStateMixin {\n  final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey();\n  late final ChatController _chatController = Provider.of<ChatController>(\n    context,\n    listen: false,\n  );\n  late List<Message> _oldList;\n  late StreamSubscription<ChatOperation> _operationsSubscription;\n\n  late final AnimationController _scrollToBottomController;\n  late final Animation<double> _scrollToBottomAnimation;\n  Timer? _scrollToBottomShowTimer;\n\n  bool _userHasScrolled = false;\n  bool _isScrollingToBottom = false;\n  String _lastInsertedMessageId = '';\n\n  @override\n  void initState() {\n    super.initState();\n\n    // TODO: Add assert for messages having same id\n    _oldList = List.from(_chatController.messages);\n    _operationsSubscription = _chatController.operationsStream.listen((event) {\n      switch (event.type) {\n        case ChatOperationType.insert:\n          assert(\n            event.index != null,\n            'Index must be provided when inserting a message.',\n          );\n          assert(\n            event.message != null,\n            'Message must be provided when inserting a message.',\n          );\n          _onInserted(event.index!, event.message!);\n          _oldList = List.from(_chatController.messages);\n          break;\n        case ChatOperationType.remove:\n          assert(\n            event.index != null,\n            'Index must be provided when removing a message.',\n          );\n          assert(\n            event.message != null,\n            'Message must be provided when removing a message.',\n          );\n          _onRemoved(event.index!, event.message!);\n          _oldList = List.from(_chatController.messages);\n          break;\n        case ChatOperationType.set:\n          final newList = _chatController.messages;\n\n          final updates = diffutil\n              .calculateDiff<Message>(\n                MessageListDiff(_oldList, newList),\n              )\n              .getUpdatesWithData();\n\n          for (var i = updates.length - 1; i >= 0; i--) {\n            _onDiffUpdate(updates.elementAt(i));\n          }\n\n          _oldList = List.from(newList);\n          break;\n        default:\n          break;\n      }\n    });\n\n    _scrollToBottomController = AnimationController(\n      vsync: this,\n      duration: const Duration(milliseconds: 300),\n    );\n\n    _scrollToBottomAnimation = CurvedAnimation(\n      parent: _scrollToBottomController,\n      curve: Curves.easeInOut,\n    );\n\n    widget.scrollController.addListener(_handleLoadPreviousMessages);\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      _handleLoadPreviousMessages();\n    });\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n    _scrollToBottomShowTimer?.cancel();\n    _scrollToBottomController.dispose();\n    _operationsSubscription.cancel();\n    widget.scrollController.removeListener(_handleLoadPreviousMessages);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final builders = context.watch<Builders>();\n\n    return NotificationListener<Notification>(\n      onNotification: (notification) {\n        if (notification is UserScrollNotification) {\n          // When user scrolls up, save it to `_userHasScrolled`\n          if (notification.direction == ScrollDirection.reverse) {\n            _userHasScrolled = true;\n          } else {\n            // When user overscolls to the bottom or stays idle at the bottom, set `_userHasScrolled` to false\n            if (notification.metrics.pixels ==\n                notification.metrics.minScrollExtent) {\n              _userHasScrolled = false;\n            }\n          }\n        }\n\n        if (notification is ScrollUpdateNotification) {\n          _handleToggleScrollToBottom();\n        }\n\n        // Allow other listeners to get the notification\n        return false;\n      },\n      child: Stack(\n        children: [\n          CustomScrollView(\n            reverse: true,\n            controller: widget.scrollController,\n            slivers: <Widget>[\n              SliverPadding(\n                padding: EdgeInsets.only(\n                  top: widget.bottomPadding ?? 0,\n                ),\n              ),\n              SliverAnimatedList(\n                key: _listKey,\n                initialItemCount: _chatController.messages.length,\n                itemBuilder: (\n                  BuildContext context,\n                  int index,\n                  Animation<double> animation,\n                ) {\n                  final message = _chatController.messages[\n                      max(_chatController.messages.length - 1 - index, 0)];\n                  return widget.itemBuilder(\n                    context,\n                    animation,\n                    message,\n                  );\n                },\n              ),\n            ],\n          ),\n          builders.scrollToBottomBuilder?.call(\n                context,\n                _scrollToBottomAnimation,\n                _handleScrollToBottom,\n              ) ??\n              ScrollToBottom(\n                animation: _scrollToBottomAnimation,\n                onPressed: _handleScrollToBottom,\n              ),\n        ],\n      ),\n    );\n  }\n\n  void _subsequentScrollToEnd(Message data) async {\n    final user = Provider.of<User>(context, listen: false);\n\n    // We only want to scroll to the bottom if user has not scrolled up\n    // or if the message is sent by the current user.\n    if (data.id == _lastInsertedMessageId &&\n        widget.scrollController.offset >\n            widget.scrollController.position.minScrollExtent &&\n        (user.id == data.author.id && _userHasScrolled)) {\n      if (widget.scrollToEndAnimationDuration == Duration.zero) {\n        widget.scrollController\n            .jumpTo(widget.scrollController.position.minScrollExtent);\n      } else {\n        await widget.scrollController.animateTo(\n          widget.scrollController.position.minScrollExtent,\n          duration: widget.scrollToEndAnimationDuration,\n          curve: Curves.linearToEaseOut,\n        );\n      }\n\n      if (!widget.scrollController.hasClients || !mounted) return;\n\n      // Because of the issue I have opened here https://github.com/flutter/flutter/issues/129768\n      // we need an additional jump to the end. Sometimes Flutter\n      // will not scroll to the very end. Sometimes it will not scroll to the\n      // very end even with this, so this is something that needs to be\n      // addressed by the Flutter team.\n      //\n      // Additionally here we have a check for the message id, because\n      // if new message arrives in the meantime it will trigger another\n      // scroll to the end animation, making this logic redundant.\n      if (data.id == _lastInsertedMessageId &&\n          widget.scrollController.offset >\n              widget.scrollController.position.minScrollExtent &&\n          (user.id == data.author.id && _userHasScrolled)) {\n        widget.scrollController\n            .jumpTo(widget.scrollController.position.minScrollExtent);\n      }\n    }\n  }\n\n  void _scrollToEnd(Message data) {\n    WidgetsBinding.instance.addPostFrameCallback(\n      (_) {\n        if (!widget.scrollController.hasClients || !mounted) return;\n\n        _subsequentScrollToEnd(data);\n      },\n    );\n  }\n\n  void _handleScrollToBottom() {\n    _isScrollingToBottom = true;\n    _scrollToBottomController.reverse();\n\n    WidgetsBinding.instance.addPostFrameCallback((_) async {\n      if (!widget.scrollController.hasClients || !mounted) return;\n\n      if (widget.scrollToEndAnimationDuration == Duration.zero) {\n        widget.scrollController\n            .jumpTo(widget.scrollController.position.minScrollExtent);\n      } else {\n        await widget.scrollController.animateTo(\n          widget.scrollController.position.minScrollExtent,\n          duration: widget.scrollToEndAnimationDuration,\n          curve: Curves.linearToEaseOut,\n        );\n      }\n\n      if (!widget.scrollController.hasClients || !mounted) return;\n\n      if (widget.scrollController.offset <\n          widget.scrollController.position.minScrollExtent) {\n        widget.scrollController.jumpTo(\n          widget.scrollController.position.minScrollExtent,\n        );\n      }\n\n      _isScrollingToBottom = false;\n    });\n  }\n\n  void _handleToggleScrollToBottom() {\n    if (_isScrollingToBottom) {\n      return;\n    }\n\n    _scrollToBottomShowTimer?.cancel();\n    if (widget.scrollController.offset >\n        widget.scrollController.position.minScrollExtent) {\n      _scrollToBottomShowTimer =\n          Timer(widget.scrollToBottomAppearanceDelay, () {\n        if (mounted) {\n          _scrollToBottomController.forward();\n        }\n      });\n    } else {\n      if (_scrollToBottomController.status != AnimationStatus.completed) {\n        _scrollToBottomController.stop();\n      }\n      _scrollToBottomController.reverse();\n    }\n  }\n\n  void _onInserted(final int position, final Message data) {\n    // There is a scroll notification listener the controls the\n    // `_userHasScrolled` variable.\n    //\n    // If for some reason `_userHasScrolled` is true and the user is not at the\n    // bottom of the list, set `_userHasScrolled` to false so that the scroll\n    // animation is triggered.\n    if (position == 0 &&\n        _userHasScrolled &&\n        widget.scrollController.offset >\n            widget.scrollController.position.minScrollExtent) {\n      _userHasScrolled = false;\n    }\n\n    _listKey.currentState!.insertItem(\n      0,\n      duration: widget.insertAnimationDuration,\n    );\n\n    // Used later to trigger scroll to end only for the last inserted message.\n    _lastInsertedMessageId = data.id;\n\n    if (position == _oldList.length) {\n      _scrollToEnd(data);\n    }\n  }\n\n  void _onRemoved(final int position, final Message data) {\n    final visualPosition = max(_oldList.length - position - 1, 0);\n    _listKey.currentState!.removeItem(\n      visualPosition,\n      (context, animation) => widget.itemBuilder(\n        context,\n        animation,\n        data,\n        isRemoved: true,\n      ),\n      duration: widget.removeAnimationDuration,\n    );\n  }\n\n  void _onChanged(int position, Message oldData, Message newData) {\n    _onRemoved(position, oldData);\n    _listKey.currentState!.insertItem(\n      max(_oldList.length - position - 1, 0),\n      duration: widget.insertAnimationDuration,\n    );\n  }\n\n  void _onDiffUpdate(diffutil.DataDiffUpdate<Message> update) {\n    update.when<void>(\n      insert: (pos, data) => _onInserted(max(_oldList.length - pos, 0), data),\n      remove: (pos, data) => _onRemoved(pos, data),\n      change: (pos, oldData, newData) => _onChanged(pos, oldData, newData),\n      move: (_, __, ___) => throw UnimplementedError('unused'),\n    );\n  }\n\n  void _handleLoadPreviousMessages() {\n    if (widget.scrollController.offset >=\n        widget.scrollController.position.maxScrollExtent) {\n      widget.onLoadPreviousMessages?.call();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_avatar.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/util/built_in_svgs.dart';\nimport 'package:appflowy/util/color_generator/color_generator.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:string_validator/string_validator.dart';\n\nimport 'layout_define.dart';\n\nclass ChatAIAvatar extends StatelessWidget {\n  const ChatAIAvatar({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      width: DesktopAIChatSizes.avatarSize,\n      height: DesktopAIChatSizes.avatarSize,\n      clipBehavior: Clip.hardEdge,\n      decoration: const BoxDecoration(shape: BoxShape.circle),\n      foregroundDecoration: ShapeDecoration(\n        shape: CircleBorder(\n          side: BorderSide(color: Theme.of(context).colorScheme.outline),\n        ),\n      ),\n      child: const CircleAvatar(\n        backgroundColor: Colors.transparent,\n        child: FlowySvg(\n          FlowySvgs.app_logo_s,\n          size: Size.square(16),\n          blendMode: null,\n        ),\n      ),\n    );\n  }\n}\n\nclass ChatUserAvatar extends StatelessWidget {\n  const ChatUserAvatar({\n    super.key,\n    required this.iconUrl,\n    required this.name,\n    this.defaultName,\n  });\n\n  final String iconUrl;\n  final String name;\n  final String? defaultName;\n\n  @override\n  Widget build(BuildContext context) {\n    late final Widget child;\n    if (iconUrl.isEmpty) {\n      child = _buildEmptyAvatar(context);\n    } else if (isURL(iconUrl)) {\n      child = _buildUrlAvatar(context);\n    } else {\n      child = _buildEmojiAvatar(context);\n    }\n    return Container(\n      width: DesktopAIChatSizes.avatarSize,\n      height: DesktopAIChatSizes.avatarSize,\n      clipBehavior: Clip.hardEdge,\n      decoration: const BoxDecoration(shape: BoxShape.circle),\n      foregroundDecoration: ShapeDecoration(\n        shape: CircleBorder(\n          side: BorderSide(color: Theme.of(context).colorScheme.outline),\n        ),\n      ),\n      child: child,\n    );\n  }\n\n  Widget _buildEmptyAvatar(BuildContext context) {\n    final String nameOrDefault = _userName(name, defaultName);\n\n    final Color color = ColorGenerator(name).toColor();\n    const initialsCount = 2;\n\n    // Taking the first letters of the name components and limiting to 2 elements\n    final nameInitials = nameOrDefault\n        .split(' ')\n        .where((element) => element.isNotEmpty)\n        .take(initialsCount)\n        .map((element) => element[0].toUpperCase())\n        .join();\n\n    return ColoredBox(\n      color: color,\n      child: Center(\n        child: FlowyText.regular(\n          nameInitials,\n          color: Colors.black,\n        ),\n      ),\n    );\n  }\n\n  Widget _buildUrlAvatar(BuildContext context) {\n    return CircleAvatar(\n      backgroundColor: Colors.transparent,\n      radius: DesktopAIChatSizes.avatarSize / 2,\n      child: Image.network(\n        iconUrl,\n        fit: BoxFit.cover,\n        errorBuilder: (context, error, stackTrace) =>\n            _buildEmptyAvatar(context),\n      ),\n    );\n  }\n\n  Widget _buildEmojiAvatar(BuildContext context) {\n    return CircleAvatar(\n      backgroundColor: Colors.transparent,\n      radius: DesktopAIChatSizes.avatarSize / 2,\n      child: builtInSVGIcons.contains(iconUrl)\n          ? FlowySvg(\n              FlowySvgData('emoji/$iconUrl'),\n              blendMode: null,\n            )\n          : FlowyText.emoji(\n              iconUrl,\n              fontSize: 24, // cannot reduce\n              optimizeEmojiAlign: true,\n            ),\n    );\n  }\n\n  /// Return the user name.\n  ///\n  /// If the user name is empty, return the default user name.\n  String _userName(String name, String? defaultName) =>\n      name.isEmpty ? (defaultName ?? LocaleKeys.defaultUsername.tr()) : name;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_editor_style.dart",
    "content": "// ref appflowy_flutter/lib/plugins/document/presentation/editor_style.dart\n\n// diff:\n// - text style\n// - heading text style and padding builders\n// - don't listen to document appearance cubit\n//\n\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/workspace/application/appearance_defaults.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:google_fonts/google_fonts.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass ChatEditorStyleCustomizer extends EditorStyleCustomizer {\n  ChatEditorStyleCustomizer({\n    required super.context,\n    required super.padding,\n    super.width,\n  });\n\n  @override\n  EditorStyle desktop() {\n    final theme = Theme.of(context);\n    final afThemeExtension = AFThemeExtension.of(context);\n    final appearanceFont = context.read<AppearanceSettingsCubit>().state.font;\n    final appearance = context.read<DocumentAppearanceCubit>().state;\n    const fontSize = 14.0;\n    String fontFamily = appearance.fontFamily;\n    if (fontFamily.isEmpty && appearanceFont.isNotEmpty) {\n      fontFamily = appearanceFont;\n    }\n\n    return EditorStyle.desktop(\n      padding: padding,\n      maxWidth: width,\n      cursorColor: appearance.cursorColor ??\n          DefaultAppearanceSettings.getDefaultCursorColor(context),\n      selectionColor: appearance.selectionColor ??\n          DefaultAppearanceSettings.getDefaultSelectionColor(context),\n      defaultTextDirection: appearance.defaultTextDirection,\n      textStyleConfiguration: TextStyleConfiguration(\n        lineHeight: 20 / 14,\n        applyHeightToFirstAscent: true,\n        applyHeightToLastDescent: true,\n        text: baseTextStyle(fontFamily).copyWith(\n          fontSize: fontSize,\n          color: afThemeExtension.onBackground,\n        ),\n        bold: baseTextStyle(fontFamily, fontWeight: FontWeight.bold).copyWith(\n          fontWeight: FontWeight.w600,\n        ),\n        italic: baseTextStyle(fontFamily).copyWith(fontStyle: FontStyle.italic),\n        underline: baseTextStyle(fontFamily).copyWith(\n          decoration: TextDecoration.underline,\n        ),\n        strikethrough: baseTextStyle(fontFamily).copyWith(\n          decoration: TextDecoration.lineThrough,\n        ),\n        href: baseTextStyle(fontFamily).copyWith(\n          color: theme.colorScheme.primary,\n          decoration: TextDecoration.underline,\n        ),\n        code: GoogleFonts.robotoMono(\n          textStyle: baseTextStyle(fontFamily).copyWith(\n            fontSize: fontSize,\n            fontWeight: FontWeight.normal,\n            color: Colors.red,\n            backgroundColor:\n                theme.colorScheme.inverseSurface.withValues(alpha: 0.8),\n          ),\n        ),\n      ),\n      textSpanDecorator: customizeAttributeDecorator,\n      textScaleFactor:\n          context.watch<AppearanceSettingsCubit>().state.textScaleFactor,\n    );\n  }\n\n  @override\n  TextStyle headingStyleBuilder(int level) {\n    final String? fontFamily;\n    final List<double> fontSizes;\n    const fontSize = 14.0;\n\n    fontFamily = context.read<DocumentAppearanceCubit>().state.fontFamily;\n    fontSizes = [\n      fontSize + 12,\n      fontSize + 10,\n      fontSize + 6,\n      fontSize + 2,\n      fontSize,\n    ];\n    return baseTextStyle(fontFamily, fontWeight: FontWeight.w600).copyWith(\n      fontSize: fontSizes.elementAtOrNull(level - 1) ?? fontSize,\n    );\n  }\n\n  @override\n  CodeBlockStyle codeBlockStyleBuilder() {\n    final fontFamily =\n        context.read<DocumentAppearanceCubit>().state.codeFontFamily;\n\n    return CodeBlockStyle(\n      textStyle: baseTextStyle(fontFamily).copyWith(\n        height: 1.4,\n        color: AFThemeExtension.of(context).onBackground,\n      ),\n      backgroundColor: AFThemeExtension.of(context).calloutBGColor,\n      foregroundColor: AFThemeExtension.of(context).textColor.withAlpha(155),\n      wrapLines: true,\n    );\n  }\n\n  @override\n  TextStyle calloutBlockStyleBuilder() {\n    if (UniversalPlatform.isMobile) {\n      final afThemeExtension = AFThemeExtension.of(context);\n      final pageStyle = context.read<DocumentPageStyleBloc>().state;\n      final fontFamily = pageStyle.fontFamily ?? defaultFontFamily;\n      final baseTextStyle = this.baseTextStyle(fontFamily);\n      return baseTextStyle.copyWith(\n        color: afThemeExtension.onBackground,\n      );\n    } else {\n      final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;\n      return baseTextStyle(null).copyWith(\n        fontSize: fontSize,\n        height: 1.5,\n      );\n    }\n  }\n\n  @override\n  TextStyle outlineBlockPlaceholderStyleBuilder() {\n    return TextStyle(\n      fontFamily: defaultFontFamily,\n      height: 1.5,\n      color: AFThemeExtension.of(context).onBackground.withValues(alpha: 0.6),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/ai/widgets/prompt_input/mentioned_page_text_span.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:extended_text_field/extended_text_field.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass MobileChatInput extends StatefulWidget {\n  const MobileChatInput({\n    super.key,\n    required this.isStreaming,\n    required this.onStopStreaming,\n    required this.onSubmitted,\n    required this.selectedSourcesNotifier,\n    required this.onUpdateSelectedSources,\n  });\n\n  final bool isStreaming;\n  final void Function() onStopStreaming;\n  final ValueNotifier<List<String>> selectedSourcesNotifier;\n  final void Function(String, PredefinedFormat?, Map<String, dynamic>)\n      onSubmitted;\n  final void Function(List<String>) onUpdateSelectedSources;\n\n  @override\n  State<MobileChatInput> createState() => _MobileChatInputState();\n}\n\nclass _MobileChatInputState extends State<MobileChatInput> {\n  final inputControlCubit = ChatInputControlCubit();\n  final focusNode = FocusNode();\n  final textController = TextEditingController();\n\n  late SendButtonState sendButtonState;\n\n  @override\n  void initState() {\n    super.initState();\n\n    textController.addListener(handleTextControllerChanged);\n    // focusNode.onKeyEvent = handleKeyEvent;\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      focusNode.requestFocus();\n      checkForAskingAI();\n    });\n\n    updateSendButtonState();\n  }\n\n  @override\n  void didUpdateWidget(covariant oldWidget) {\n    updateSendButtonState();\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    textController.dispose();\n    inputControlCubit.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Hero(\n      tag: \"ai_chat_prompt\",\n      child: BlocProvider.value(\n        value: inputControlCubit,\n        child: BlocListener<ChatInputControlCubit, ChatInputControlState>(\n          listener: (context, state) {\n            state.maybeWhen(\n              updateSelectedViews: (selectedViews) {\n                context.read<AIPromptInputBloc>().add(\n                      AIPromptInputEvent.updateMentionedViews(selectedViews),\n                    );\n              },\n              orElse: () {},\n            );\n          },\n          child: DecoratedBox(\n            decoration: BoxDecoration(\n              border: Border(\n                top: BorderSide(color: Theme.of(context).colorScheme.outline),\n              ),\n              color: Theme.of(context).colorScheme.surface,\n              boxShadow: const [\n                BoxShadow(\n                  blurRadius: 4.0,\n                  offset: Offset(0, -2),\n                  color: Color.fromRGBO(0, 0, 0, 0.05),\n                ),\n              ],\n              borderRadius:\n                  const BorderRadius.vertical(top: Radius.circular(8.0)),\n            ),\n            child: BlocBuilder<AIPromptInputBloc, AIPromptInputState>(\n              builder: (context, state) {\n                return Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    ConstrainedBox(\n                      constraints: BoxConstraints(\n                        maxHeight: MobileAIPromptSizes\n                                .attachedFilesBarPadding.vertical +\n                            MobileAIPromptSizes.attachedFilesPreviewHeight,\n                      ),\n                      child: PromptInputFile(\n                        onDeleted: (file) => context\n                            .read<AIPromptInputBloc>()\n                            .add(AIPromptInputEvent.removeFile(file)),\n                      ),\n                    ),\n                    if (state.showPredefinedFormats)\n                      TextFieldTapRegion(\n                        child: Padding(\n                          padding: const EdgeInsets.all(8.0),\n                          child: ChangeFormatBar(\n                            predefinedFormat: state.predefinedFormat,\n                            spacing: 8.0,\n                            onSelectPredefinedFormat: (format) =>\n                                context.read<AIPromptInputBloc>().add(\n                                      AIPromptInputEvent.updatePredefinedFormat(\n                                        format,\n                                      ),\n                                    ),\n                          ),\n                        ),\n                      )\n                    else\n                      const VSpace(8.0),\n                    inputTextField(context),\n                    TextFieldTapRegion(\n                      child: Container(\n                        padding: const EdgeInsets.symmetric(vertical: 8.0),\n                        child: Row(\n                          children: [\n                            const HSpace(8.0),\n                            leadingButtons(\n                              context,\n                              state.showPredefinedFormats,\n                            ),\n                            const Spacer(),\n                            sendButton(),\n                            const HSpace(12.0),\n                          ],\n                        ),\n                      ),\n                    ),\n                  ],\n                );\n              },\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  void updateSendButtonState() {\n    if (widget.isStreaming) {\n      sendButtonState = SendButtonState.streaming;\n    } else if (textController.text.trim().isEmpty) {\n      sendButtonState = SendButtonState.disabled;\n    } else {\n      sendButtonState = SendButtonState.enabled;\n    }\n  }\n\n  void handleSendPressed() {\n    if (widget.isStreaming) {\n      return;\n    }\n    final trimmedText = inputControlCubit.formatIntputText(\n      textController.text.trim(),\n    );\n    textController.clear();\n    if (trimmedText.isEmpty) {\n      return;\n    }\n\n    onSubmitText(trimmedText);\n  }\n\n  void onSubmitText(String text) {\n    // get the attached files and mentioned pages\n    final metadata = context.read<AIPromptInputBloc>().consumeMetadata();\n\n    final bloc = context.read<AIPromptInputBloc>();\n    final showPredefinedFormats = bloc.state.showPredefinedFormats;\n    final predefinedFormat = bloc.state.predefinedFormat;\n\n    widget.onSubmitted(\n      text,\n      showPredefinedFormats ? predefinedFormat : null,\n      metadata,\n    );\n  }\n\n  void checkForAskingAI() {\n    if (!UniversalPlatform.isMobile) return;\n    final paletteBloc = context.read<CommandPaletteBloc?>(),\n        paletteState = paletteBloc?.state;\n    if (paletteBloc == null || paletteState == null) return;\n    final isAskingAI = paletteState.askAI;\n    if (!isAskingAI) return;\n    paletteBloc.add(CommandPaletteEvent.askedAI());\n    final query = paletteState.query ?? '';\n    if (query.isEmpty) return;\n    final sources = (paletteState.askAISources ?? []).map((e) => e.id).toList();\n    final metadata =\n        context.read<AIPromptInputBloc?>()?.consumeMetadata() ?? {};\n    final promptState = context.read<AIPromptInputBloc?>()?.state;\n    final predefinedFormat = promptState?.predefinedFormat;\n    if (sources.isNotEmpty) {\n      widget.onUpdateSelectedSources(sources);\n    }\n    widget.onSubmitted.call(query, predefinedFormat, metadata);\n  }\n\n  void handleTextControllerChanged() {\n    if (textController.value.isComposingRangeValid) {\n      return;\n    }\n    // inputControlCubit.updateInputText(textController.text);\n    setState(() => updateSendButtonState());\n  }\n\n  // KeyEventResult handleKeyEvent(FocusNode node, KeyEvent event) {\n  //   if (event.character == '@') {\n  //     WidgetsBinding.instance.addPostFrameCallback((_) {\n  //       mentionPage(context);\n  //     });\n  //   }\n  //   return KeyEventResult.ignored;\n  // }\n\n  Future<void> mentionPage(BuildContext context) async {\n    // if the focus node is on focus, unfocus it for better animation\n    // otherwise, the page sheet animation will be blocked by the keyboard\n    inputControlCubit.refreshViews();\n    inputControlCubit.startSearching(textController.value);\n    if (focusNode.hasFocus) {\n      focusNode.unfocus();\n      await Future.delayed(const Duration(milliseconds: 100));\n    }\n\n    if (context.mounted) {\n      final selectedView = await showPageSelectorSheet(\n        context,\n        filter: (view) =>\n            !view.isSpace &&\n            view.layout.isDocumentView &&\n            view.parentViewId != view.id &&\n            !inputControlCubit.selectedViewIds.contains(view.id),\n      );\n      if (selectedView != null) {\n        final newText = textController.text.replaceRange(\n          inputControlCubit.filterStartPosition,\n          inputControlCubit.filterStartPosition,\n          selectedView.id,\n        );\n        textController.value = TextEditingValue(\n          text: newText,\n          selection: TextSelection.collapsed(\n            offset:\n                textController.selection.baseOffset + selectedView.id.length,\n            affinity: TextAffinity.upstream,\n          ),\n        );\n\n        inputControlCubit.selectPage(selectedView);\n      }\n      focusNode.requestFocus();\n      inputControlCubit.reset();\n    }\n  }\n\n  Widget inputTextField(BuildContext context) {\n    return BlocBuilder<AIPromptInputBloc, AIPromptInputState>(\n      builder: (context, state) {\n        return ExtendedTextField(\n          controller: textController,\n          focusNode: focusNode,\n          textAlignVertical: TextAlignVertical.center,\n          decoration: InputDecoration(\n            border: InputBorder.none,\n            enabledBorder: InputBorder.none,\n            focusedBorder: InputBorder.none,\n            contentPadding: MobileAIPromptSizes.textFieldContentPadding,\n            hintText: state.modelState.hintText,\n            hintStyle: inputHintTextStyle(context),\n            isCollapsed: true,\n            isDense: true,\n          ),\n          keyboardType: TextInputType.multiline,\n          textCapitalization: TextCapitalization.sentences,\n          minLines: 1,\n          maxLines: null,\n          style:\n              Theme.of(context).textTheme.bodyMedium?.copyWith(height: 20 / 14),\n          specialTextSpanBuilder: PromptInputTextSpanBuilder(\n            inputControlCubit: inputControlCubit,\n            mentionedPageTextStyle:\n                Theme.of(context).textTheme.bodyMedium?.copyWith(\n                      color: Theme.of(context).colorScheme.primary,\n                      fontWeight: FontWeight.w600,\n                    ),\n          ),\n          onTapOutside: (_) => focusNode.unfocus(),\n        );\n      },\n    );\n  }\n\n  TextStyle? inputHintTextStyle(BuildContext context) {\n    return Theme.of(context).textTheme.bodyMedium?.copyWith(\n          color: Theme.of(context).isLightMode\n              ? const Color(0xFFBDC2C8)\n              : const Color(0xFF3C3E51),\n        );\n  }\n\n  Widget leadingButtons(BuildContext context, bool showPredefinedFormats) {\n    return _LeadingActions(\n      // onMention: () {\n      //   textController.text += '@';\n      //   if (!focusNode.hasFocus) {\n      //     focusNode.requestFocus();\n      //   }\n      //   WidgetsBinding.instance.addPostFrameCallback((_) {\n      //     mentionPage(context);\n      //   });\n      // },\n      showPredefinedFormats: showPredefinedFormats,\n      onTogglePredefinedFormatSection: () {\n        context\n            .read<AIPromptInputBloc>()\n            .add(AIPromptInputEvent.toggleShowPredefinedFormat());\n      },\n      selectedSourcesNotifier: widget.selectedSourcesNotifier,\n      onUpdateSelectedSources: widget.onUpdateSelectedSources,\n    );\n  }\n\n  Widget sendButton() {\n    return PromptInputSendButton(\n      state: sendButtonState,\n      onSendPressed: handleSendPressed,\n      onStopStreaming: widget.onStopStreaming,\n    );\n  }\n}\n\nclass _LeadingActions extends StatelessWidget {\n  const _LeadingActions({\n    required this.showPredefinedFormats,\n    required this.onTogglePredefinedFormatSection,\n    required this.selectedSourcesNotifier,\n    required this.onUpdateSelectedSources,\n  });\n\n  final bool showPredefinedFormats;\n  final void Function() onTogglePredefinedFormatSection;\n  final ValueNotifier<List<String>> selectedSourcesNotifier;\n  final void Function(List<String>) onUpdateSelectedSources;\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      color: Theme.of(context).colorScheme.surface,\n      child: SeparatedRow(\n        mainAxisSize: MainAxisSize.min,\n        separatorBuilder: () => const HSpace(4.0),\n        children: [\n          PromptInputMobileSelectSourcesButton(\n            selectedSourcesNotifier: selectedSourcesNotifier,\n            onUpdateSelectedSources: onUpdateSelectedSources,\n          ),\n          PromptInputMobileToggleFormatButton(\n            showFormatBar: showPredefinedFormats,\n            onTap: onTogglePredefinedFormatSection,\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_message_selector_banner.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_edit_document_service.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_select_message_bloc.dart';\nimport 'package:appflowy/plugins/document/application/prelude.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\n\nimport 'message/ai_message_action_bar.dart';\nimport 'message/message_util.dart';\n\nclass ChatMessageSelectorBanner extends StatelessWidget {\n  const ChatMessageSelectorBanner({\n    super.key,\n    required this.view,\n    this.allMessages = const [],\n  });\n\n  final ViewPB view;\n  final List<Message> allMessages;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<ChatSelectMessageBloc, ChatSelectMessageState>(\n      builder: (context, state) {\n        if (!state.isSelectingMessages) {\n          return const SizedBox.shrink();\n        }\n\n        final selectedAmount = state.selectedMessages.length;\n        final totalAmount = allMessages.length;\n        final allSelected = selectedAmount == totalAmount;\n\n        return Container(\n          height: 48,\n          color: const Color(0xFF00BCF0),\n          padding: const EdgeInsets.symmetric(horizontal: 16),\n          child: Row(\n            children: [\n              GestureDetector(\n                onTap: () {\n                  if (selectedAmount > 0) {\n                    _unselectAllMessages(context);\n                  } else {\n                    _selectAllMessages(context);\n                  }\n                },\n                child: FlowySvg(\n                  allSelected\n                      ? FlowySvgs.checkbox_ai_selected_s\n                      : selectedAmount > 0\n                          ? FlowySvgs.checkbox_ai_minus_s\n                          : FlowySvgs.checkbox_ai_empty_s,\n                  blendMode: BlendMode.dstIn,\n                  size: const Size.square(18),\n                ),\n              ),\n              const HSpace(8),\n              Expanded(\n                child: FlowyText.semibold(\n                  allSelected\n                      ? LocaleKeys.chat_selectBanner_allSelected.tr()\n                      : selectedAmount > 0\n                          ? LocaleKeys.chat_selectBanner_nSelected\n                              .tr(args: [selectedAmount.toString()])\n                          : LocaleKeys.chat_selectBanner_selectMessages.tr(),\n                  figmaLineHeight: 16,\n                  color: Colors.white,\n                ),\n              ),\n              SaveToPageButton(\n                view: view,\n              ),\n              const HSpace(8),\n              MouseRegion(\n                cursor: SystemMouseCursors.click,\n                child: GestureDetector(\n                  onTap: () => context.read<ChatSelectMessageBloc>().add(\n                        const ChatSelectMessageEvent.toggleSelectingMessages(),\n                      ),\n                  child: const FlowySvg(\n                    FlowySvgs.close_m,\n                    color: Colors.white,\n                    size: Size.square(24),\n                  ),\n                ),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  void _selectAllMessages(BuildContext context) => context\n      .read<ChatSelectMessageBloc>()\n      .add(ChatSelectMessageEvent.selectAllMessages(allMessages));\n\n  void _unselectAllMessages(BuildContext context) => context\n      .read<ChatSelectMessageBloc>()\n      .add(const ChatSelectMessageEvent.unselectAllMessages());\n}\n\nclass SaveToPageButton extends StatefulWidget {\n  const SaveToPageButton({\n    super.key,\n    required this.view,\n  });\n\n  final ViewPB view;\n\n  @override\n  State<SaveToPageButton> createState() => _SaveToPageButtonState();\n}\n\nclass _SaveToPageButtonState extends State<SaveToPageButton> {\n  final popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return ViewSelector(\n      viewSelectorCubit: BlocProvider(\n        create: (context) => ViewSelectorCubit(\n          getIgnoreViewType: (item) {\n            final view = item.view;\n\n            if (view.isSpace) {\n              return IgnoreViewType.none;\n            }\n            if (view.layout != ViewLayoutPB.Document) {\n              return IgnoreViewType.hide;\n            }\n            return IgnoreViewType.none;\n          },\n        ),\n      ),\n      child: BlocSelector<SpaceBloc, SpaceState, ViewPB?>(\n        selector: (state) => state.currentSpace,\n        builder: (context, spaceView) {\n          return AppFlowyPopover(\n            controller: popoverController,\n            triggerActions: PopoverTriggerFlags.none,\n            margin: EdgeInsets.zero,\n            offset: const Offset(0, 18),\n            direction: PopoverDirection.bottomWithRightAligned,\n            constraints: const BoxConstraints.tightFor(width: 300, height: 400),\n            child: buildButton(context, spaceView),\n            popupBuilder: (_) => buildPopover(context),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget buildButton(BuildContext context, ViewPB? spaceView) {\n    return BlocBuilder<ChatSelectMessageBloc, ChatSelectMessageState>(\n      builder: (context, state) {\n        final selectedAmount = state.selectedMessages.length;\n\n        return Opacity(\n          opacity: selectedAmount == 0 ? 0.5 : 1,\n          child: FlowyTextButton(\n            LocaleKeys.chat_selectBanner_saveButton.tr(),\n            onPressed: selectedAmount == 0\n                ? null\n                : () async {\n                    final documentId = getOpenedDocumentId();\n                    if (documentId != null) {\n                      await onAddToExistingPage(context, documentId);\n                      await forceReload(documentId);\n                      await Future.delayed(const Duration(milliseconds: 500));\n                      await updateSelection(documentId);\n                    } else {\n                      if (spaceView != null) {\n                        unawaited(\n                          context\n                              .read<ViewSelectorCubit>()\n                              .refreshSources([spaceView], spaceView),\n                        );\n                      }\n                      popoverController.show();\n                    }\n                  },\n            fontColor: Colors.white,\n            borderColor: Colors.white,\n            fillColor: Colors.transparent,\n            padding: const EdgeInsets.symmetric(\n              horizontal: 12.0,\n              vertical: 6.0,\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget buildPopover(BuildContext context) {\n    return BlocProvider.value(\n      value: context.read<ViewSelectorCubit>(),\n      child: SaveToPagePopoverContent(\n        onAddToNewPage: (parentViewId) async {\n          await addMessageToNewPage(context, parentViewId);\n          popoverController.close();\n        },\n        onAddToExistingPage: (documentId) async {\n          final view = await onAddToExistingPage(context, documentId);\n\n          if (context.mounted) {\n            openPageFromMessage(context, view);\n          }\n          await Future.delayed(const Duration(milliseconds: 500));\n          await updateSelection(documentId);\n          popoverController.close();\n        },\n      ),\n    );\n  }\n\n  Future<ViewPB?> onAddToExistingPage(\n    BuildContext context,\n    String documentId,\n  ) async {\n    final bloc = context.read<ChatSelectMessageBloc>();\n\n    final selectedMessages = [\n      ...bloc.state.selectedMessages.whereType<TextMessage>(),\n    ]..sort((a, b) => a.createdAt.compareTo(b.createdAt));\n\n    await ChatEditDocumentService.addMessagesToPage(\n      documentId,\n      selectedMessages,\n    );\n    await Future.delayed(const Duration(milliseconds: 500));\n    final view = await ViewBackendService.getView(documentId).toNullable();\n    if (context.mounted) {\n      showSaveMessageSuccessToast(context, view);\n    }\n\n    bloc.add(const ChatSelectMessageEvent.reset());\n\n    return view;\n  }\n\n  Future<void> addMessageToNewPage(\n    BuildContext context,\n    String parentViewId,\n  ) async {\n    final bloc = context.read<ChatSelectMessageBloc>();\n\n    final selectedMessages = [\n      ...bloc.state.selectedMessages.whereType<TextMessage>(),\n    ]..sort((a, b) => a.createdAt.compareTo(b.createdAt));\n\n    final newView = await ChatEditDocumentService.saveMessagesToNewPage(\n      widget.view.nameOrDefault,\n      parentViewId,\n      selectedMessages,\n    );\n\n    if (context.mounted) {\n      showSaveMessageSuccessToast(context, newView);\n      openPageFromMessage(context, newView);\n    }\n    bloc.add(const ChatSelectMessageEvent.reset());\n  }\n\n  Future<void> forceReload(String documentId) async {\n    final bloc = DocumentBloc.findOpen(documentId);\n    if (bloc == null) {\n      return;\n    }\n    await bloc.forceReloadDocumentState();\n  }\n\n  Future<void> updateSelection(String documentId) async {\n    final bloc = DocumentBloc.findOpen(documentId);\n    if (bloc == null) {\n      return;\n    }\n    await bloc.forceReloadDocumentState();\n    final editorState = bloc.state.editorState;\n    final lastNodePath = editorState?.getLastSelectable()?.$1.path;\n    if (editorState == null || lastNodePath == null) {\n      return;\n    }\n    unawaited(\n      editorState.updateSelectionWithReason(\n        Selection.collapsed(Position(path: lastNodePath)),\n      ),\n    );\n  }\n\n  String? getOpenedDocumentId() {\n    final pageManager = getIt<TabsBloc>().state.currentPageManager;\n    if (!pageManager.showSecondaryPluginNotifier.value) {\n      return null;\n    }\n    return pageManager.secondaryNotifier.plugin.id;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_page/chat_animation_list_widget.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/plugins/ai_chat/application/ai_chat_prelude.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/animated_chat_list.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/animated_chat_list_reversed.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/chat_welcome_page.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\n\n@visibleForTesting\nbool skipAIChatWelcomePage = false;\n\nclass ChatAnimationListWidget extends StatefulWidget {\n  const ChatAnimationListWidget({\n    super.key,\n    required this.userProfile,\n    required this.scrollController,\n    required this.itemBuilder,\n    this.enableReversedList = false,\n  });\n\n  final UserProfilePB userProfile;\n  final ScrollController scrollController;\n  final ChatItem itemBuilder;\n  final bool enableReversedList;\n\n  @override\n  State<ChatAnimationListWidget> createState() =>\n      _ChatAnimationListWidgetState();\n}\n\nclass _ChatAnimationListWidgetState extends State<ChatAnimationListWidget> {\n  @override\n  Widget build(BuildContext context) {\n    final bloc = context.read<ChatBloc>();\n\n    // this logic is quite weird, why don't we just get the message from the state?\n    if (bloc.chatController.messages.isEmpty && !skipAIChatWelcomePage) {\n      return ChatWelcomePage(\n        userProfile: widget.userProfile,\n        onSelectedQuestion: (question) {\n          final aiPromptInputBloc = context.read<AIPromptInputBloc>();\n          final showPredefinedFormats =\n              aiPromptInputBloc.state.showPredefinedFormats;\n          final predefinedFormat = aiPromptInputBloc.state.predefinedFormat;\n          bloc.add(\n            ChatEvent.sendMessage(\n              message: question,\n              format: showPredefinedFormats ? predefinedFormat : null,\n            ),\n          );\n        },\n      );\n    }\n\n    // don't call this in the build method\n    context\n        .read<ChatSelectMessageBloc>()\n        .add(ChatSelectMessageEvent.enableStartSelectingMessages());\n\n    // final bool reversed = false;\n\n    return BlocSelector<ChatSelectMessageBloc, ChatSelectMessageState, bool>(\n      selector: (state) => state.isSelectingMessages,\n      builder: (context, isSelectingMessages) {\n        return widget.enableReversedList\n            ? ChatAnimatedListReversed(\n                scrollController: widget.scrollController,\n                itemBuilder: widget.itemBuilder,\n                bottomPadding: isSelectingMessages\n                    ? 48.0 + DesktopAIChatSizes.messageActionBarIconSize\n                    : 8.0,\n                onLoadPreviousMessages: () {\n                  if (bloc.isClosed) {\n                    return;\n                  }\n                  bloc.add(const ChatEvent.loadPreviousMessages());\n                },\n              )\n            : ChatAnimatedList(\n                scrollController: widget.scrollController,\n                itemBuilder: widget.itemBuilder,\n                bottomPadding: isSelectingMessages\n                    ? 48.0 + DesktopAIChatSizes.messageActionBarIconSize\n                    : 8.0,\n                onLoadPreviousMessages: () {\n                  if (bloc.isClosed) {\n                    return;\n                  }\n                  bloc.add(const ChatEvent.loadPreviousMessages());\n                },\n              );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_page/chat_content_page.dart",
    "content": "import 'package:appflowy/plugins/ai_chat/application/ai_chat_prelude.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/chat_page/load_chat_message_status_ready.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass ChatContentPage extends StatelessWidget {\n  const ChatContentPage({\n    super.key,\n    required this.view,\n    required this.userProfile,\n  });\n\n  final UserProfilePB userProfile;\n  final ViewPB view;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<ChatBloc, ChatState>(\n      builder: (context, state) {\n        return switch (state.loadingState) {\n          LoadChatMessageStatus.ready => LoadChatMessageStatusReady(\n              view: view,\n              userProfile: userProfile,\n              chatController: context.read<ChatBloc>().chatController,\n            ),\n          _ => const Center(child: CircularProgressIndicator.adaptive()),\n        };\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_page/chat_footer.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/plugins/ai_chat/application/ai_chat_prelude.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart';\nimport 'package:appflowy/workspace/presentation/home/home_stack.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass ChatFooter extends StatefulWidget {\n  const ChatFooter({\n    super.key,\n    required this.view,\n  });\n\n  final ViewPB view;\n\n  @override\n  State<ChatFooter> createState() => _ChatFooterState();\n}\n\nclass _ChatFooterState extends State<ChatFooter> {\n  final textController = AiPromptInputTextEditingController();\n\n  @override\n  void dispose() {\n    textController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocSelector<ChatSelectMessageBloc, ChatSelectMessageState, bool>(\n      selector: (state) => state.isSelectingMessages,\n      builder: (context, isSelectingMessages) {\n        return AnimatedSwitcher(\n          duration: const Duration(milliseconds: 150),\n          transitionBuilder: (child, animation) {\n            return NonClippingSizeTransition(\n              sizeFactor: animation,\n              axisAlignment: -1,\n              child: child,\n            );\n          },\n          child: isSelectingMessages\n              ? const SizedBox.shrink()\n              : Padding(\n                  padding: AIChatUILayout.safeAreaInsets(context),\n                  child: BlocSelector<ChatBloc, ChatState, bool>(\n                    selector: (state) {\n                      return state.promptResponseState.isReady;\n                    },\n                    builder: (context, canSendMessage) {\n                      final chatBloc = context.read<ChatBloc>();\n\n                      return UniversalPlatform.isDesktop\n                          ? _buildDesktopInput(\n                              context,\n                              chatBloc,\n                              canSendMessage,\n                            )\n                          : _buildMobileInput(\n                              context,\n                              chatBloc,\n                              canSendMessage,\n                            );\n                    },\n                  ),\n                ),\n        );\n      },\n    );\n  }\n\n  Widget _buildDesktopInput(\n    BuildContext context,\n    ChatBloc chatBloc,\n    bool canSendMessage,\n  ) {\n    return DesktopPromptInput(\n      isStreaming: !canSendMessage,\n      textController: textController,\n      onStopStreaming: () {\n        chatBloc.add(const ChatEvent.stopStream());\n      },\n      onSubmitted: (text, format, metadata, promptId) {\n        chatBloc.add(\n          ChatEvent.sendMessage(\n            message: text,\n            format: format,\n            metadata: metadata,\n            promptId: promptId,\n          ),\n        );\n      },\n      selectedSourcesNotifier: chatBloc.selectedSourcesNotifier,\n      onUpdateSelectedSources: (ids) {\n        chatBloc.add(\n          ChatEvent.updateSelectedSources(\n            selectedSourcesIds: ids,\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildMobileInput(\n    BuildContext context,\n    ChatBloc chatBloc,\n    bool canSendMessage,\n  ) {\n    return MobileChatInput(\n      isStreaming: !canSendMessage,\n      onStopStreaming: () {\n        chatBloc.add(const ChatEvent.stopStream());\n      },\n      onSubmitted: (text, format, metadata) {\n        chatBloc.add(\n          ChatEvent.sendMessage(\n            message: text,\n            format: format,\n            metadata: metadata,\n          ),\n        );\n      },\n      selectedSourcesNotifier: chatBloc.selectedSourcesNotifier,\n      onUpdateSelectedSources: (ids) {\n        chatBloc.add(\n          ChatEvent.updateSelectedSources(\n            selectedSourcesIds: ids,\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_page/chat_message_widget.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\nimport 'package:provider/provider.dart';\n\nclass ChatMessage extends StatelessWidget {\n  const ChatMessage({\n    super.key,\n    required this.message,\n    required this.child,\n    this.sentMessageAlignment = AlignmentDirectional.centerEnd,\n    this.receivedMessageAlignment = AlignmentDirectional.centerStart,\n    this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 1),\n  });\n\n  final Message message;\n  final Widget child;\n  final AlignmentGeometry sentMessageAlignment;\n  final AlignmentGeometry receivedMessageAlignment;\n  final EdgeInsetsGeometry padding;\n\n  @override\n  Widget build(BuildContext context) {\n    final isSentByMe = context.watch<User>().id == message.author.id;\n\n    return Align(\n      alignment: isSentByMe ? sentMessageAlignment : receivedMessageAlignment,\n      child: Padding(\n        padding: padding,\n        child: child,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_page/load_chat_message_status_ready.dart",
    "content": "import 'package:appflowy/plugins/ai_chat/presentation/chat_message_selector_banner.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/chat_page/chat_animation_list_widget.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/chat_page/chat_footer.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/chat_page/chat_message_widget.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/chat_page/text_message_widget.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/scroll_to_bottom.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\nimport 'package:flutter_chat_ui/flutter_chat_ui.dart' hide ChatMessage;\nimport 'package:universal_platform/universal_platform.dart';\n\nclass LoadChatMessageStatusReady extends StatelessWidget {\n  const LoadChatMessageStatusReady({\n    super.key,\n    required this.view,\n    required this.userProfile,\n    required this.chatController,\n  });\n\n  final ViewPB view;\n  final UserProfilePB userProfile;\n  final ChatController chatController;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        // Chat header, banner\n        _buildHeader(context),\n        // Chat body, a list of messages\n        _buildBody(context),\n        // Chat footer, a text input field with toolbar, send button, etc.\n        _buildFooter(context),\n      ],\n    );\n  }\n\n  Widget _buildHeader(BuildContext context) {\n    return ChatMessageSelectorBanner(\n      view: view,\n      allMessages: chatController.messages,\n    );\n  }\n\n  Widget _buildBody(BuildContext context) {\n    final bool enableAnimation = true;\n    return Expanded(\n      child: Align(\n        alignment: Alignment.topCenter,\n        child: _wrapConstraints(\n          SelectionArea(\n            child: ScrollConfiguration(\n              behavior: ScrollConfiguration.of(context).copyWith(\n                scrollbars: false,\n              ),\n              child: Chat(\n                chatController: chatController,\n                user: User(id: userProfile.id.toString()),\n                darkTheme: ChatTheme.fromThemeData(Theme.of(context)),\n                theme: ChatTheme.fromThemeData(Theme.of(context)),\n                builders: Builders(\n                  // we have a custom input builder, so we don't need the default one\n                  inputBuilder: (_) => const SizedBox.shrink(),\n                  textMessageBuilder: (\n                    context,\n                    message,\n                  ) =>\n                      TextMessageWidget(\n                    message: message,\n                    userProfile: userProfile,\n                    view: view,\n                    enableAnimation: enableAnimation,\n                  ),\n                  chatMessageBuilder: (\n                    context,\n                    message,\n                    animation,\n                    child,\n                  ) =>\n                      ChatMessage(\n                    message: message,\n                    padding: const EdgeInsets.symmetric(vertical: 18.0),\n                    child: child,\n                  ),\n                  scrollToBottomBuilder: (\n                    context,\n                    animation,\n                    onPressed,\n                  ) =>\n                      CustomScrollToBottom(\n                    animation: animation,\n                    onPressed: onPressed,\n                  ),\n                  chatAnimatedListBuilder: (\n                    context,\n                    scrollController,\n                    itemBuilder,\n                  ) =>\n                      ChatAnimationListWidget(\n                    userProfile: userProfile,\n                    scrollController: scrollController,\n                    itemBuilder: itemBuilder,\n                    enableReversedList: !enableAnimation,\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildFooter(BuildContext context) {\n    return _wrapConstraints(\n      ChatFooter(view: view),\n    );\n  }\n\n  Widget _wrapConstraints(Widget child) {\n    return Container(\n      constraints: const BoxConstraints(maxWidth: 784),\n      margin: UniversalPlatform.isDesktop\n          ? const EdgeInsets.symmetric(horizontal: 60.0)\n          : null,\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_page/text_message_widget.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/plugins/ai_chat/application/ai_chat_prelude.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_message_height_manager.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/chat_related_question.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/message/ai_text_message.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/message/error_text_message.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/message/message_util.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/message/user_text_message.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\nimport 'package:string_validator/string_validator.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nclass TextMessageWidget extends StatelessWidget {\n  const TextMessageWidget({\n    super.key,\n    required this.message,\n    required this.userProfile,\n    required this.view,\n    this.enableAnimation = true,\n  });\n\n  final TextMessage message;\n  final UserProfilePB userProfile;\n  final ViewPB view;\n  final bool enableAnimation;\n\n  @override\n  Widget build(BuildContext context) {\n    final messageType = onetimeMessageTypeFromMeta(\n      message.metadata,\n    );\n\n    if (messageType == OnetimeShotType.error) {\n      return ChatErrorMessageWidget(\n        errorMessage: message.metadata?[errorMessageTextKey] ?? \"\",\n      );\n    }\n\n    if (messageType == OnetimeShotType.relatedQuestion) {\n      final messages = context.read<ChatBloc>().chatController.messages;\n      final lastAIMessage = messages.lastWhere(\n        (e) =>\n            onetimeMessageTypeFromMeta(e.metadata) == null &&\n            (e.author.id == aiResponseUserId || e.author.id == systemUserId),\n      );\n      final minHeight =\n          ChatMessageHeightManager().calculateRelatedQuestionMinHeight(\n        messageId: lastAIMessage.id,\n      );\n      return Container(\n        constraints: BoxConstraints(\n          minHeight: minHeight,\n        ),\n        child: RelatedQuestionList(\n          relatedQuestions: message.metadata!['questions'],\n          onQuestionSelected: (question) {\n            final bloc = context.read<AIPromptInputBloc>();\n            final showPredefinedFormats = bloc.state.showPredefinedFormats;\n            final predefinedFormat = bloc.state.predefinedFormat;\n\n            context.read<ChatBloc>().add(\n                  ChatEvent.sendMessage(\n                    message: question,\n                    format: showPredefinedFormats ? predefinedFormat : null,\n                  ),\n                );\n          },\n        ),\n      );\n    }\n\n    if (message.author.id == userProfile.id.toString() ||\n        isOtherUserMessage(message)) {\n      return ChatUserMessageWidget(\n        user: message.author,\n        message: message,\n      );\n    }\n\n    final stream = message.metadata?[\"$AnswerStream\"];\n    final questionId = message.metadata?[messageQuestionIdKey];\n    final refSourceJsonString =\n        message.metadata?[messageRefSourceJsonStringKey] as String?;\n\n    return BlocSelector<ChatSelectMessageBloc, ChatSelectMessageState, bool>(\n      selector: (state) => state.isSelectingMessages,\n      builder: (context, isSelectingMessages) {\n        return BlocBuilder<ChatBloc, ChatState>(\n          buildWhen: (previous, current) =>\n              previous.promptResponseState != current.promptResponseState,\n          builder: (context, state) {\n            final chatController = context.read<ChatBloc>().chatController;\n            final messages = chatController.messages\n                .where((e) => onetimeMessageTypeFromMeta(e.metadata) == null);\n            final isLastMessage =\n                messages.isEmpty ? false : messages.last.id == message.id;\n            final hasRelatedQuestions = state.promptResponseState ==\n                PromptResponseState.relatedQuestionsReady;\n            return ChatAIMessageWidget(\n              user: message.author,\n              messageUserId: message.id,\n              message: message,\n              stream: stream is AnswerStream ? stream : null,\n              questionId: questionId,\n              chatId: view.id,\n              refSourceJsonString: refSourceJsonString,\n              isStreaming: !state.promptResponseState.isReady,\n              isLastMessage: isLastMessage,\n              hasRelatedQuestions: hasRelatedQuestions,\n              isSelectingMessages: isSelectingMessages,\n              enableAnimation: enableAnimation,\n              onSelectedMetadata: (metadata) =>\n                  _onSelectMetadata(context, metadata),\n              onRegenerate: () => context\n                  .read<ChatBloc>()\n                  .add(ChatEvent.regenerateAnswer(message.id, null, null)),\n              onChangeFormat: (format) => context\n                  .read<ChatBloc>()\n                  .add(ChatEvent.regenerateAnswer(message.id, format, null)),\n              onChangeModel: (model) => context\n                  .read<ChatBloc>()\n                  .add(ChatEvent.regenerateAnswer(message.id, null, model)),\n              onStopStream: () => context.read<ChatBloc>().add(\n                    const ChatEvent.stopStream(),\n                  ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  void _onSelectMetadata(\n    BuildContext context,\n    ChatMessageRefSource metadata,\n  ) async {\n    // When the source of metatdata is appflowy, which means it is a appflowy page\n    if (metadata.source == \"appflowy\") {\n      final sidebarView =\n          await ViewBackendService.getView(metadata.id).toNullable();\n      if (context.mounted) {\n        openPageFromMessage(context, sidebarView);\n      }\n      return;\n    }\n\n    if (metadata.source == \"web\") {\n      if (isURL(metadata.name)) {\n        late Uri uri;\n        try {\n          uri = Uri.parse(metadata.name);\n          // `Uri` identifies `localhost` as a scheme\n          if (!uri.hasScheme || uri.scheme == 'localhost') {\n            uri = Uri.parse(\"http://${metadata.name}\");\n            await InternetAddress.lookup(uri.host);\n          }\n          await launchUrl(uri);\n        } catch (err) {\n          Log.error(\"failed to open url $err\");\n        }\n      }\n      return;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'layout_define.dart';\n\nclass RelatedQuestionList extends StatelessWidget {\n  const RelatedQuestionList({\n    super.key,\n    required this.onQuestionSelected,\n    required this.relatedQuestions,\n  });\n\n  final void Function(String) onQuestionSelected;\n  final List<String> relatedQuestions;\n\n  @override\n  Widget build(BuildContext context) {\n    return SelectionContainer.disabled(\n      child: ListView.separated(\n        shrinkWrap: true,\n        physics: const NeverScrollableScrollPhysics(),\n        itemCount: relatedQuestions.length + 1,\n        padding: const EdgeInsets.only(left: 4.0, bottom: 8.0) +\n            AIChatUILayout.messageMargin,\n        separatorBuilder: (context, index) => const VSpace(4.0),\n        itemBuilder: (context, index) {\n          if (index == 0) {\n            return Padding(\n              padding: const EdgeInsets.only(bottom: 4.0),\n              child: FlowyText(\n                LocaleKeys.chat_relatedQuestion.tr(),\n                color: Theme.of(context).hintColor,\n                fontWeight: FontWeight.w600,\n              ),\n            );\n          } else {\n            return Align(\n              alignment: AlignmentDirectional.centerStart,\n              child: RelatedQuestionItem(\n                question: relatedQuestions[index - 1],\n                onQuestionSelected: onQuestionSelected,\n              ),\n            );\n          }\n        },\n      ),\n    );\n  }\n}\n\nclass RelatedQuestionItem extends StatelessWidget {\n  const RelatedQuestionItem({\n    required this.question,\n    required this.onQuestionSelected,\n    super.key,\n  });\n\n  final String question;\n  final Function(String) onQuestionSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      mainAxisAlignment: MainAxisAlignment.start,\n      text: Flexible(\n        child: FlowyText(\n          question,\n          lineHeight: 1.4,\n          maxLines: 2,\n          overflow: TextOverflow.ellipsis,\n        ),\n      ),\n      expandText: false,\n      margin: UniversalPlatform.isMobile\n          ? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0)\n          : const EdgeInsets.all(8.0),\n      leftIcon: FlowySvg(\n        FlowySvgs.ai_chat_outlined_s,\n        color: Theme.of(context).colorScheme.primary,\n        size: const Size.square(16.0),\n      ),\n      onTap: () => onQuestionSelected(question),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass ChatWelcomePage extends StatelessWidget {\n  const ChatWelcomePage({\n    required this.userProfile,\n    required this.onSelectedQuestion,\n    super.key,\n  });\n\n  final void Function(String) onSelectedQuestion;\n  final UserProfilePB userProfile;\n\n  static final List<String> desktopItems = [\n    LocaleKeys.chat_question1.tr(),\n    LocaleKeys.chat_question2.tr(),\n    LocaleKeys.chat_question3.tr(),\n    LocaleKeys.chat_question4.tr(),\n  ];\n\n  static final List<List<String>> mobileItems = [\n    [\n      LocaleKeys.chat_question1.tr(),\n      LocaleKeys.chat_question2.tr(),\n    ],\n    [\n      LocaleKeys.chat_question3.tr(),\n      LocaleKeys.chat_question4.tr(),\n    ],\n    [\n      LocaleKeys.chat_question5.tr(),\n      LocaleKeys.chat_question6.tr(),\n    ],\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        const FlowySvg(\n          FlowySvgs.app_logo_xl,\n          size: Size.square(32),\n          blendMode: null,\n        ),\n        const VSpace(16),\n        FlowyText(\n          fontSize: 15,\n          LocaleKeys.chat_questionDetail.tr(args: [userProfile.name]),\n        ),\n        UniversalPlatform.isDesktop ? const VSpace(32 - 16) : const VSpace(24),\n        ...UniversalPlatform.isDesktop\n            ? buildDesktopSampleQuestions(context)\n            : buildMobileSampleQuestions(context),\n      ],\n    );\n  }\n\n  Iterable<Widget> buildDesktopSampleQuestions(BuildContext context) {\n    return desktopItems.map(\n      (question) => Padding(\n        padding: const EdgeInsets.only(top: 16.0),\n        child: WelcomeSampleQuestion(\n          question: question,\n          onSelected: onSelectedQuestion,\n        ),\n      ),\n    );\n  }\n\n  Iterable<Widget> buildMobileSampleQuestions(BuildContext context) {\n    return [\n      _AutoScrollingSampleQuestions(\n        key: const ValueKey('inf_scroll_1'),\n        onSelected: onSelectedQuestion,\n        questions: mobileItems[0],\n        offset: 60.0,\n      ),\n      const VSpace(8),\n      _AutoScrollingSampleQuestions(\n        key: const ValueKey('inf_scroll_2'),\n        onSelected: onSelectedQuestion,\n        questions: mobileItems[1],\n        offset: -50.0,\n        reverse: true,\n      ),\n      const VSpace(8),\n      _AutoScrollingSampleQuestions(\n        key: const ValueKey('inf_scroll_3'),\n        onSelected: onSelectedQuestion,\n        questions: mobileItems[2],\n        offset: 120.0,\n      ),\n    ];\n  }\n}\n\nclass WelcomeSampleQuestion extends StatelessWidget {\n  const WelcomeSampleQuestion({\n    required this.question,\n    required this.onSelected,\n    super.key,\n  });\n\n  final void Function(String) onSelected;\n  final String question;\n\n  @override\n  Widget build(BuildContext context) {\n    final isLightMode = Theme.of(context).isLightMode;\n    return DecoratedBox(\n      decoration: BoxDecoration(\n        borderRadius: BorderRadius.circular(16),\n        boxShadow: [\n          BoxShadow(\n            offset: const Offset(0, 1),\n            blurRadius: 2,\n            spreadRadius: -2,\n            color: isLightMode\n                ? const Color(0x051F2329)\n                : Theme.of(context).shadowColor.withValues(alpha: 0.02),\n          ),\n          BoxShadow(\n            offset: const Offset(0, 2),\n            blurRadius: 4,\n            color: isLightMode\n                ? const Color(0x051F2329)\n                : Theme.of(context).shadowColor.withValues(alpha: 0.02),\n          ),\n          BoxShadow(\n            offset: const Offset(0, 2),\n            blurRadius: 8,\n            spreadRadius: 2,\n            color: isLightMode\n                ? const Color(0x051F2329)\n                : Theme.of(context).shadowColor.withValues(alpha: 0.02),\n          ),\n        ],\n      ),\n      child: TextButton(\n        onPressed: () => onSelected(question),\n        style: ButtonStyle(\n          padding: WidgetStatePropertyAll(\n            EdgeInsets.symmetric(\n              horizontal: 16,\n              vertical: UniversalPlatform.isDesktop ? 8 : 0,\n            ),\n          ),\n          backgroundColor: WidgetStateProperty.resolveWith<Color?>((state) {\n            if (state.contains(WidgetState.hovered)) {\n              return isLightMode\n                  ? const Color(0xFFF9FAFD)\n                  : AFThemeExtension.of(context).lightGreyHover;\n            }\n            return Theme.of(context).colorScheme.surface;\n          }),\n          overlayColor: WidgetStateColor.transparent,\n          shape: WidgetStatePropertyAll(\n            RoundedRectangleBorder(\n              borderRadius: BorderRadius.circular(16),\n              side: BorderSide(color: Theme.of(context).dividerColor),\n            ),\n          ),\n        ),\n        child: FlowyText(\n          question,\n          color: isLightMode\n              ? Theme.of(context).hintColor\n              : const Color(0xFF666D76),\n        ),\n      ),\n    );\n  }\n}\n\nclass _AutoScrollingSampleQuestions extends StatefulWidget {\n  const _AutoScrollingSampleQuestions({\n    super.key,\n    required this.questions,\n    this.offset = 0.0,\n    this.reverse = false,\n    required this.onSelected,\n  });\n\n  final List<String> questions;\n  final void Function(String) onSelected;\n  final double offset;\n  final bool reverse;\n\n  @override\n  State<_AutoScrollingSampleQuestions> createState() =>\n      _AutoScrollingSampleQuestionsState();\n}\n\nclass _AutoScrollingSampleQuestionsState\n    extends State<_AutoScrollingSampleQuestions> {\n  late final scrollController = ScrollController(\n    initialScrollOffset: widget.offset,\n  );\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 36,\n      child: InfiniteScrollView(\n        scrollController: scrollController,\n        centerKey: UniqueKey(),\n        itemCount: widget.questions.length,\n        itemBuilder: (context, index) {\n          return WelcomeSampleQuestion(\n            question: widget.questions[index],\n            onSelected: widget.onSelected,\n          );\n        },\n        separatorBuilder: (context, index) => const HSpace(8),\n      ),\n    );\n  }\n}\n\nclass InfiniteScrollView extends StatelessWidget {\n  const InfiniteScrollView({\n    super.key,\n    required this.itemCount,\n    required this.centerKey,\n    required this.itemBuilder,\n    required this.separatorBuilder,\n    this.scrollController,\n  });\n\n  final int itemCount;\n  final Widget Function(BuildContext context, int index) itemBuilder;\n  final Widget Function(BuildContext context, int index) separatorBuilder;\n  final Key centerKey;\n\n  final ScrollController? scrollController;\n\n  @override\n  Widget build(BuildContext context) {\n    return CustomScrollView(\n      scrollDirection: Axis.horizontal,\n      controller: scrollController,\n      center: centerKey,\n      anchor: 0.5,\n      slivers: [\n        _buildList(isForward: false),\n        SliverToBoxAdapter(\n          child: separatorBuilder.call(context, 0),\n        ),\n        SliverToBoxAdapter(\n          key: centerKey,\n          child: itemBuilder.call(context, 0),\n        ),\n        SliverToBoxAdapter(\n          child: separatorBuilder.call(context, 0),\n        ),\n        _buildList(isForward: true),\n      ],\n    );\n  }\n\n  Widget _buildList({required bool isForward}) {\n    return SliverList.separated(\n      itemBuilder: (context, index) {\n        index = (index + 1) % itemCount;\n        return itemBuilder(context, index);\n      },\n      separatorBuilder: (context, index) {\n        index = (index + 1) % itemCount;\n        return separatorBuilder(context, index);\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/layout_define.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass AIChatUILayout {\n  const AIChatUILayout._();\n\n  static EdgeInsets safeAreaInsets(BuildContext context) {\n    final query = MediaQuery.of(context);\n    return UniversalPlatform.isMobile\n        ? EdgeInsets.fromLTRB(\n            query.padding.left,\n            0,\n            query.padding.right,\n            query.viewInsets.bottom + query.padding.bottom,\n          )\n        : const EdgeInsets.only(bottom: 24);\n  }\n\n  static EdgeInsets get messageMargin => UniversalPlatform.isMobile\n      ? const EdgeInsets.symmetric(horizontal: 16)\n      : EdgeInsets.zero;\n}\n\nclass DesktopAIChatSizes {\n  const DesktopAIChatSizes._();\n\n  static const avatarSize = 32.0;\n  static const avatarAndChatBubbleSpacing = 12.0;\n\n  static const messageActionBarIconSize = 28.0;\n  static const messageHoverActionBarPadding = EdgeInsets.all(2.0);\n  static const messageHoverActionBarRadius =\n      BorderRadius.all(Radius.circular(8.0));\n  static const messageHoverActionBarIconRadius =\n      BorderRadius.all(Radius.circular(6.0));\n  static const messageActionBarIconRadius =\n      BorderRadius.all(Radius.circular(8.0));\n\n  static const inputActionBarMargin =\n      EdgeInsetsDirectional.fromSTEB(8, 0, 8, 4);\n  static const inputActionBarButtonSpacing = 4.0;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nFuture<PredefinedFormat?> showChangeFormatBottomSheet(\n  BuildContext context,\n) {\n  return showMobileBottomSheet<PredefinedFormat?>(\n    context,\n    showDragHandle: true,\n    builder: (context) => const _ChangeFormatBottomSheetContent(),\n  );\n}\n\nclass _ChangeFormatBottomSheetContent extends StatefulWidget {\n  const _ChangeFormatBottomSheetContent();\n\n  @override\n  State<_ChangeFormatBottomSheetContent> createState() =>\n      _ChangeFormatBottomSheetContentState();\n}\n\nclass _ChangeFormatBottomSheetContentState\n    extends State<_ChangeFormatBottomSheetContent> {\n  PredefinedFormat? predefinedFormat;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        _Header(\n          onCancel: () => Navigator.of(context).pop(),\n          onDone: () => Navigator.of(context).pop(predefinedFormat),\n        ),\n        const VSpace(4.0),\n        _Body(\n          predefinedFormat: predefinedFormat,\n          onSelectPredefinedFormat: (format) {\n            setState(() => predefinedFormat = format);\n          },\n        ),\n        const VSpace(16.0),\n      ],\n    );\n  }\n}\n\nclass _Header extends StatelessWidget {\n  const _Header({\n    required this.onCancel,\n    required this.onDone,\n  });\n\n  final VoidCallback onCancel;\n  final VoidCallback onDone;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 44.0,\n      child: Stack(\n        children: [\n          Align(\n            alignment: Alignment.centerLeft,\n            child: AppBarBackButton(\n              padding: const EdgeInsets.symmetric(\n                vertical: 12,\n                horizontal: 16,\n              ),\n              onTap: onCancel,\n            ),\n          ),\n          Align(\n            child: Container(\n              constraints: const BoxConstraints(maxWidth: 250),\n              child: FlowyText(\n                LocaleKeys.chat_changeFormat_actionButton.tr(),\n                fontSize: 17.0,\n                fontWeight: FontWeight.w500,\n                overflow: TextOverflow.ellipsis,\n              ),\n            ),\n          ),\n          Align(\n            alignment: Alignment.centerRight,\n            child: AppBarDoneButton(\n              onTap: onDone,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _Body extends StatelessWidget {\n  const _Body({\n    required this.predefinedFormat,\n    required this.onSelectPredefinedFormat,\n  });\n\n  final PredefinedFormat? predefinedFormat;\n  final void Function(PredefinedFormat) onSelectPredefinedFormat;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        _buildFormatButton(ImageFormat.text, true),\n        _buildFormatButton(ImageFormat.textAndImage),\n        _buildFormatButton(ImageFormat.image),\n        const VSpace(32.0),\n        Opacity(\n          opacity: predefinedFormat?.imageFormat.hasText ?? true ? 1 : 0,\n          child: Column(\n            children: [\n              _buildTextFormatButton(TextFormat.paragraph, true),\n              _buildTextFormatButton(TextFormat.bulletList),\n              _buildTextFormatButton(TextFormat.numberedList),\n              _buildTextFormatButton(TextFormat.table),\n            ],\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildFormatButton(\n    ImageFormat format, [\n    bool isFirst = false,\n  ]) {\n    return FlowyOptionTile.checkbox(\n      text: format.i18n,\n      isSelected: format == predefinedFormat?.imageFormat,\n      showTopBorder: isFirst,\n      leftIcon: FlowySvg(\n        format.icon,\n        size: format == ImageFormat.textAndImage\n            ? const Size(21.0 / 16.0 * 20, 20)\n            : const Size.square(20),\n      ),\n      onTap: () {\n        if (predefinedFormat != null &&\n            format == predefinedFormat!.imageFormat) {\n          return;\n        }\n        if (format.hasText) {\n          final textFormat =\n              predefinedFormat?.textFormat ?? TextFormat.paragraph;\n          onSelectPredefinedFormat(\n            PredefinedFormat(imageFormat: format, textFormat: textFormat),\n          );\n        } else {\n          onSelectPredefinedFormat(\n            PredefinedFormat(imageFormat: format, textFormat: null),\n          );\n        }\n      },\n    );\n  }\n\n  Widget _buildTextFormatButton(\n    TextFormat format, [\n    bool isFirst = false,\n  ]) {\n    return FlowyOptionTile.checkbox(\n      text: format.i18n,\n      isSelected: format == predefinedFormat?.textFormat,\n      showTopBorder: isFirst,\n      leftIcon: FlowySvg(\n        format.icon,\n        size: const Size.square(20),\n      ),\n      onTap: () {\n        if (predefinedFormat != null &&\n            format == predefinedFormat!.textFormat) {\n          return;\n        }\n        onSelectPredefinedFormat(\n          PredefinedFormat(\n            imageFormat: predefinedFormat?.imageFormat ?? ImageFormat.text,\n            textFormat: format,\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_model_bottom_sheet.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nFuture<AIModelPB?> showChangeModelBottomSheet(\n  BuildContext context,\n  List<AIModelPB> models,\n) {\n  return showMobileBottomSheet<AIModelPB?>(\n    context,\n    showDragHandle: true,\n    builder: (context) => _ChangeModelBottomSheetContent(models: models),\n  );\n}\n\nclass _ChangeModelBottomSheetContent extends StatefulWidget {\n  const _ChangeModelBottomSheetContent({\n    required this.models,\n  });\n\n  final List<AIModelPB> models;\n\n  @override\n  State<_ChangeModelBottomSheetContent> createState() =>\n      _ChangeModelBottomSheetContentState();\n}\n\nclass _ChangeModelBottomSheetContentState\n    extends State<_ChangeModelBottomSheetContent> {\n  AIModelPB? model;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        _Header(\n          onCancel: () => Navigator.of(context).pop(),\n          onDone: () => Navigator.of(context).pop(model),\n        ),\n        const VSpace(4.0),\n        _Body(\n          models: widget.models,\n          selectedModel: model,\n          onSelectModel: (format) {\n            setState(() => model = format);\n          },\n        ),\n        const VSpace(16.0),\n      ],\n    );\n  }\n}\n\nclass _Header extends StatelessWidget {\n  const _Header({\n    required this.onCancel,\n    required this.onDone,\n  });\n\n  final VoidCallback onCancel;\n  final VoidCallback onDone;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 44.0,\n      child: Stack(\n        children: [\n          Align(\n            alignment: Alignment.centerLeft,\n            child: AppBarBackButton(\n              padding: const EdgeInsets.symmetric(\n                vertical: 12,\n                horizontal: 16,\n              ),\n              onTap: onCancel,\n            ),\n          ),\n          Align(\n            child: Container(\n              constraints: const BoxConstraints(maxWidth: 250),\n              child: FlowyText(\n                LocaleKeys.chat_switchModel_label.tr(),\n                fontSize: 17.0,\n                fontWeight: FontWeight.w500,\n                overflow: TextOverflow.ellipsis,\n              ),\n            ),\n          ),\n          Align(\n            alignment: Alignment.centerRight,\n            child: AppBarDoneButton(\n              onTap: onDone,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _Body extends StatelessWidget {\n  const _Body({\n    required this.models,\n    required this.selectedModel,\n    required this.onSelectModel,\n  });\n\n  final List<AIModelPB> models;\n  final AIModelPB? selectedModel;\n  final void Function(AIModelPB) onSelectModel;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: models\n          .mapIndexed(\n            (index, model) => _buildModelButton(model, index == 0),\n          )\n          .toList(),\n    );\n  }\n\n  Widget _buildModelButton(\n    AIModelPB model, [\n    bool isFirst = false,\n  ]) {\n    return FlowyOptionTile.checkbox(\n      text: model.name,\n      isSelected: model == selectedModel,\n      showTopBorder: isFirst,\n      onTap: () {\n        onSelectModel(model);\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_configuration.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/shared/markdown_to_document.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../chat_editor_style.dart';\n\n// Wrap the appflowy_editor as a chat text message widget\nclass AIMarkdownText extends StatelessWidget {\n  const AIMarkdownText({\n    super.key,\n    required this.markdown,\n    this.withAnimation = false,\n  });\n\n  final String markdown;\n  final bool withAnimation;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => DocumentPageStyleBloc(view: ViewPB())\n        ..add(const DocumentPageStyleEvent.initial()),\n      child: _AppFlowyEditorMarkdown(\n        markdown: markdown,\n        withAnimation: withAnimation,\n      ),\n    );\n  }\n}\n\nclass _AppFlowyEditorMarkdown extends StatefulWidget {\n  const _AppFlowyEditorMarkdown({\n    required this.markdown,\n    this.withAnimation = false,\n  });\n\n  // the text should be the markdown format\n  final String markdown;\n\n  /// Whether to animate the text.\n  final bool withAnimation;\n\n  @override\n  State<_AppFlowyEditorMarkdown> createState() =>\n      _AppFlowyEditorMarkdownState();\n}\n\nclass _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown>\n    with TickerProviderStateMixin {\n  late EditorState editorState;\n  late EditorScrollController scrollController;\n  late Timer markdownOutputTimer;\n  int offset = 0;\n\n  final Map<String, (AnimationController, Animation<double>)> _animations = {};\n\n  @override\n  void initState() {\n    super.initState();\n\n    editorState = _parseMarkdown(widget.markdown.trim());\n    scrollController = EditorScrollController(\n      editorState: editorState,\n      shrinkWrap: true,\n    );\n\n    if (widget.withAnimation) {\n      markdownOutputTimer =\n          Timer.periodic(const Duration(milliseconds: 60), (timer) {\n        if (offset >= widget.markdown.length || !widget.withAnimation) {\n          return;\n        }\n\n        final markdown = widget.markdown.substring(0, offset);\n        offset += 30;\n\n        final editorState = _parseMarkdown(\n          markdown,\n          previousDocument: this.editorState.document,\n        );\n        final lastCurrentNode = editorState.document.last;\n        final lastPreviousNode = this.editorState.document.last;\n        if (lastCurrentNode?.id != lastPreviousNode?.id ||\n            lastCurrentNode?.type != lastPreviousNode?.type ||\n            lastCurrentNode?.delta?.toPlainText() !=\n                lastPreviousNode?.delta?.toPlainText()) {\n          setState(() {\n            this.editorState.dispose();\n            this.editorState = editorState;\n            scrollController.dispose();\n            scrollController = EditorScrollController(\n              editorState: editorState,\n              shrinkWrap: true,\n            );\n          });\n        }\n      });\n    }\n  }\n\n  @override\n  void didUpdateWidget(covariant _AppFlowyEditorMarkdown oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    if (oldWidget.markdown != widget.markdown && !widget.withAnimation) {\n      final editorState = _parseMarkdown(\n        widget.markdown.trim(),\n        previousDocument: this.editorState.document,\n      );\n      this.editorState.dispose();\n      this.editorState = editorState;\n      scrollController.dispose();\n      scrollController = EditorScrollController(\n        editorState: editorState,\n        shrinkWrap: true,\n      );\n    }\n  }\n\n  @override\n  void dispose() {\n    scrollController.dispose();\n    editorState.dispose();\n\n    if (widget.withAnimation) {\n      markdownOutputTimer.cancel();\n      for (final controller in _animations.values.map((e) => e.$1)) {\n        controller.dispose();\n      }\n    }\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    // don't lazy load the styleCustomizer and blockBuilders,\n    // it needs the context to get the theme.\n    final styleCustomizer = ChatEditorStyleCustomizer(\n      context: context,\n      padding: EdgeInsets.zero,\n    );\n    final editorStyle = styleCustomizer.style().copyWith(\n          // hide the cursor\n          cursorColor: Colors.transparent,\n          cursorWidth: 0,\n        );\n    final blockBuilders = buildBlockComponentBuilders(\n      context: context,\n      editorState: editorState,\n      styleCustomizer: styleCustomizer,\n      // the editor is not editable in the chat\n      editable: false,\n      alwaysDistributeSimpleTableColumnWidths: UniversalPlatform.isDesktop,\n      customPadding: (node) => EdgeInsets.zero,\n    );\n    return IntrinsicHeight(\n      child: AppFlowyEditor(\n        shrinkWrap: true,\n        // the editor is not editable in the chat\n        editable: false,\n        disableKeyboardService: UniversalPlatform.isMobile,\n        disableSelectionService: UniversalPlatform.isMobile,\n        editorStyle: editorStyle,\n        editorScrollController: scrollController,\n        blockComponentBuilders: blockBuilders,\n        commandShortcutEvents: [customCopyCommand],\n        disableAutoScroll: true,\n        editorState: editorState,\n        blockWrapper: (\n          context, {\n          required Node node,\n          required Widget child,\n        }) {\n          if (!widget.withAnimation) {\n            return child;\n          }\n\n          if (!_animations.containsKey(node.id)) {\n            final duration = UniversalPlatform.isMobile\n                ? const Duration(milliseconds: 800)\n                : const Duration(milliseconds: 1600);\n            final controller = AnimationController(\n              vsync: this,\n              duration: duration,\n            );\n            final fade = Tween<double>(\n              begin: 0,\n              end: 1,\n            ).animate(controller);\n            _animations[node.id] = (controller, fade);\n            controller.forward();\n          }\n          final (controller, fade) = _animations[node.id]!;\n          return _AnimatedWrapper(\n            fade: fade,\n            child: child,\n          );\n        },\n        contextMenuItems: [\n          [\n            ContextMenuItem(\n              getName: LocaleKeys.document_plugins_contextMenu_copy.tr,\n              onPressed: (editorState) =>\n                  customCopyCommand.execute(editorState),\n            ),\n          ]\n        ],\n      ),\n    );\n  }\n\n  EditorState _parseMarkdown(\n    String markdown, {\n    Document? previousDocument,\n  }) {\n    // merge the nodes from the previous document with the new document to keep the same node ids\n    final document = customMarkdownToDocument(markdown);\n    final documentIterator = NodeIterator(\n      document: document,\n      startNode: document.root,\n    );\n    if (previousDocument != null) {\n      final previousDocumentIterator = NodeIterator(\n        document: previousDocument,\n        startNode: previousDocument.root,\n      );\n      while (\n          documentIterator.moveNext() && previousDocumentIterator.moveNext()) {\n        final currentNode = documentIterator.current;\n        final previousNode = previousDocumentIterator.current;\n        if (currentNode.path.equals(previousNode.path)) {\n          currentNode.id = previousNode.id;\n        }\n      }\n    }\n    final editorState = EditorState(document: document);\n    return editorState;\n  }\n}\n\nclass _AnimatedWrapper extends StatelessWidget {\n  const _AnimatedWrapper({\n    required this.fade,\n    required this.child,\n  });\n\n  final Animation<double> fade;\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return AnimatedBuilder(\n      animation: fade,\n      builder: (context, childWidget) {\n        return ShaderMask(\n          shaderCallback: (Rect bounds) {\n            return LinearGradient(\n              stops: [fade.value, fade.value],\n              colors: const [\n                Colors.white,\n                Colors.transparent,\n              ],\n            ).createShader(bounds);\n          },\n          blendMode: BlendMode.dstIn,\n          child: Opacity(\n            opacity: fade.value,\n            child: childWidget,\n          ),\n        );\n      },\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_edit_document_service.dart';\nimport 'package:appflowy/plugins/document/application/prelude.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/shared/markdown_to_document.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\n\nimport '../layout_define.dart';\nimport 'message_util.dart';\n\nclass AIMessageActionBar extends StatefulWidget {\n  const AIMessageActionBar({\n    super.key,\n    required this.message,\n    required this.showDecoration,\n    this.onRegenerate,\n    this.onChangeFormat,\n    this.onChangeModel,\n    this.onOverrideVisibility,\n  });\n\n  final Message message;\n  final bool showDecoration;\n  final void Function()? onRegenerate;\n  final void Function(PredefinedFormat)? onChangeFormat;\n  final void Function(AIModelPB)? onChangeModel;\n  final void Function(bool)? onOverrideVisibility;\n\n  @override\n  State<AIMessageActionBar> createState() => _AIMessageActionBarState();\n}\n\nclass _AIMessageActionBarState extends State<AIMessageActionBar> {\n  final popoverMutex = PopoverMutex();\n\n  @override\n  Widget build(BuildContext context) {\n    final isLightMode = Theme.of(context).isLightMode;\n\n    final child = SeparatedRow(\n      mainAxisSize: MainAxisSize.min,\n      separatorBuilder: () => const HSpace(8.0),\n      children: _buildChildren(),\n    );\n\n    return widget.showDecoration\n        ? Container(\n            padding: DesktopAIChatSizes.messageHoverActionBarPadding,\n            decoration: BoxDecoration(\n              borderRadius: DesktopAIChatSizes.messageHoverActionBarRadius,\n              border: Border.all(\n                color: isLightMode\n                    ? const Color(0x1F1F2329)\n                    : Theme.of(context).dividerColor,\n                strokeAlign: BorderSide.strokeAlignOutside,\n              ),\n              color: Theme.of(context).cardColor,\n              boxShadow: [\n                BoxShadow(\n                  offset: const Offset(0, 1),\n                  blurRadius: 2,\n                  spreadRadius: -2,\n                  color: isLightMode\n                      ? const Color(0x051F2329)\n                      : Theme.of(context).shadowColor.withValues(alpha: 0.02),\n                ),\n                BoxShadow(\n                  offset: const Offset(0, 2),\n                  blurRadius: 4,\n                  color: isLightMode\n                      ? const Color(0x051F2329)\n                      : Theme.of(context).shadowColor.withValues(alpha: 0.02),\n                ),\n                BoxShadow(\n                  offset: const Offset(0, 2),\n                  blurRadius: 8,\n                  spreadRadius: 2,\n                  color: isLightMode\n                      ? const Color(0x051F2329)\n                      : Theme.of(context).shadowColor.withValues(alpha: 0.02),\n                ),\n              ],\n            ),\n            child: child,\n          )\n        : child;\n  }\n\n  List<Widget> _buildChildren() {\n    return [\n      CopyButton(\n        isInHoverBar: widget.showDecoration,\n        textMessage: widget.message as TextMessage,\n      ),\n      RegenerateButton(\n        isInHoverBar: widget.showDecoration,\n        onTap: () => widget.onRegenerate?.call(),\n      ),\n      ChangeFormatButton(\n        isInHoverBar: widget.showDecoration,\n        onRegenerate: widget.onChangeFormat,\n        popoverMutex: popoverMutex,\n        onOverrideVisibility: widget.onOverrideVisibility,\n      ),\n      ChangeModelButton(\n        isInHoverBar: widget.showDecoration,\n        onRegenerate: widget.onChangeModel,\n        popoverMutex: popoverMutex,\n        onOverrideVisibility: widget.onOverrideVisibility,\n      ),\n      SaveToPageButton(\n        textMessage: widget.message as TextMessage,\n        isInHoverBar: widget.showDecoration,\n        popoverMutex: popoverMutex,\n        onOverrideVisibility: widget.onOverrideVisibility,\n      ),\n    ];\n  }\n}\n\nclass CopyButton extends StatelessWidget {\n  const CopyButton({\n    super.key,\n    required this.isInHoverBar,\n    required this.textMessage,\n  });\n\n  final bool isInHoverBar;\n  final TextMessage textMessage;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      message: LocaleKeys.settings_menu_clickToCopy.tr(),\n      child: FlowyIconButton(\n        width: DesktopAIChatSizes.messageActionBarIconSize,\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        radius: isInHoverBar\n            ? DesktopAIChatSizes.messageHoverActionBarIconRadius\n            : DesktopAIChatSizes.messageActionBarIconRadius,\n        icon: FlowySvg(\n          FlowySvgs.copy_s,\n          color: Theme.of(context).hintColor,\n          size: const Size.square(16),\n        ),\n        onPressed: () async {\n          final messageText = textMessage.text.trim();\n          final document = customMarkdownToDocument(\n            messageText,\n            tableWidth: 250.0,\n          );\n          await getIt<ClipboardService>().setData(\n            ClipboardServiceData(\n              plainText: _stripMarkdownIfNecessary(messageText),\n              inAppJson: jsonEncode(document.toJson()),\n            ),\n          );\n          if (context.mounted) {\n            showToastNotification(\n              message: LocaleKeys.message_copy_success.tr(),\n            );\n          }\n        },\n      ),\n    );\n  }\n\n  String _stripMarkdownIfNecessary(String plainText) {\n    // match and capture inner url as group\n    final matches = singleLineMarkdownImageRegex.allMatches(plainText);\n\n    if (matches.length != 1) {\n      return plainText;\n    }\n\n    return matches.first[1] ?? plainText;\n  }\n}\n\nclass RegenerateButton extends StatelessWidget {\n  const RegenerateButton({\n    super.key,\n    required this.isInHoverBar,\n    required this.onTap,\n  });\n\n  final bool isInHoverBar;\n  final void Function() onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      message: LocaleKeys.chat_regenerate.tr(),\n      child: FlowyIconButton(\n        width: DesktopAIChatSizes.messageActionBarIconSize,\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        radius: isInHoverBar\n            ? DesktopAIChatSizes.messageHoverActionBarIconRadius\n            : DesktopAIChatSizes.messageActionBarIconRadius,\n        icon: FlowySvg(\n          FlowySvgs.ai_try_again_s,\n          color: Theme.of(context).hintColor,\n          size: const Size.square(16),\n        ),\n        onPressed: onTap,\n      ),\n    );\n  }\n}\n\nclass ChangeFormatButton extends StatefulWidget {\n  const ChangeFormatButton({\n    super.key,\n    required this.isInHoverBar,\n    this.popoverMutex,\n    this.onRegenerate,\n    this.onOverrideVisibility,\n  });\n\n  final bool isInHoverBar;\n  final PopoverMutex? popoverMutex;\n  final void Function(PredefinedFormat)? onRegenerate;\n  final void Function(bool)? onOverrideVisibility;\n\n  @override\n  State<ChangeFormatButton> createState() => _ChangeFormatButtonState();\n}\n\nclass _ChangeFormatButtonState extends State<ChangeFormatButton> {\n  final popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      controller: popoverController,\n      mutex: widget.popoverMutex,\n      triggerActions: PopoverTriggerFlags.none,\n      margin: EdgeInsets.zero,\n      offset: Offset(0, widget.isInHoverBar ? 8 : 4),\n      direction: PopoverDirection.bottomWithLeftAligned,\n      constraints: const BoxConstraints(),\n      onClose: () => widget.onOverrideVisibility?.call(false),\n      child: buildButton(context),\n      popupBuilder: (_) => BlocProvider.value(\n        value: context.read<AIPromptInputBloc>(),\n        child: _ChangeFormatPopoverContent(\n          onRegenerate: widget.onRegenerate,\n        ),\n      ),\n    );\n  }\n\n  Widget buildButton(BuildContext context) {\n    return FlowyTooltip(\n      message: LocaleKeys.chat_changeFormat_actionButton.tr(),\n      child: FlowyIconButton(\n        width: 32.0,\n        height: DesktopAIChatSizes.messageActionBarIconSize,\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        radius: widget.isInHoverBar\n            ? DesktopAIChatSizes.messageHoverActionBarIconRadius\n            : DesktopAIChatSizes.messageActionBarIconRadius,\n        icon: Row(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            FlowySvg(\n              FlowySvgs.ai_retry_font_s,\n              color: Theme.of(context).hintColor,\n              size: const Size.square(16),\n            ),\n            FlowySvg(\n              FlowySvgs.ai_source_drop_down_s,\n              color: Theme.of(context).hintColor,\n              size: const Size.square(8),\n            ),\n          ],\n        ),\n        onPressed: () {\n          widget.onOverrideVisibility?.call(true);\n          popoverController.show();\n        },\n      ),\n    );\n  }\n}\n\nclass _ChangeFormatPopoverContent extends StatefulWidget {\n  const _ChangeFormatPopoverContent({\n    this.onRegenerate,\n  });\n\n  final void Function(PredefinedFormat)? onRegenerate;\n\n  @override\n  State<_ChangeFormatPopoverContent> createState() =>\n      _ChangeFormatPopoverContentState();\n}\n\nclass _ChangeFormatPopoverContentState\n    extends State<_ChangeFormatPopoverContent> {\n  PredefinedFormat? predefinedFormat;\n\n  @override\n  Widget build(BuildContext context) {\n    final isLightMode = Theme.of(context).isLightMode;\n    return Container(\n      padding: const EdgeInsets.all(2.0),\n      decoration: BoxDecoration(\n        borderRadius: DesktopAIChatSizes.messageHoverActionBarRadius,\n        border: Border.all(\n          color: isLightMode\n              ? const Color(0x1F1F2329)\n              : Theme.of(context).dividerColor,\n          strokeAlign: BorderSide.strokeAlignOutside,\n        ),\n        color: Theme.of(context).cardColor,\n        boxShadow: [\n          BoxShadow(\n            offset: const Offset(0, 1),\n            blurRadius: 2,\n            spreadRadius: -2,\n            color: isLightMode\n                ? const Color(0x051F2329)\n                : Theme.of(context).shadowColor.withValues(alpha: 0.02),\n          ),\n          BoxShadow(\n            offset: const Offset(0, 2),\n            blurRadius: 4,\n            color: isLightMode\n                ? const Color(0x051F2329)\n                : Theme.of(context).shadowColor.withValues(alpha: 0.02),\n          ),\n          BoxShadow(\n            offset: const Offset(0, 2),\n            blurRadius: 8,\n            spreadRadius: 2,\n            color: isLightMode\n                ? const Color(0x051F2329)\n                : Theme.of(context).shadowColor.withValues(alpha: 0.02),\n          ),\n        ],\n      ),\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          BlocBuilder<AIPromptInputBloc, AIPromptInputState>(\n            builder: (context, state) {\n              return ChangeFormatBar(\n                spacing: 2.0,\n                showImageFormats: state.modelState.type.isCloud,\n                predefinedFormat: predefinedFormat,\n                onSelectPredefinedFormat: (format) {\n                  setState(() => predefinedFormat = format);\n                },\n              );\n            },\n          ),\n          const HSpace(4.0),\n          FlowyTooltip(\n            message: LocaleKeys.chat_changeFormat_confirmButton.tr(),\n            child: MouseRegion(\n              cursor: SystemMouseCursors.click,\n              child: GestureDetector(\n                behavior: HitTestBehavior.opaque,\n                onTap: () {\n                  if (predefinedFormat != null) {\n                    widget.onRegenerate?.call(predefinedFormat!);\n                  }\n                },\n                child: SizedBox.square(\n                  dimension: DesktopAIPromptSizes.predefinedFormatButtonHeight,\n                  child: Center(\n                    child: FlowySvg(\n                      FlowySvgs.ai_retry_filled_s,\n                      color: Theme.of(context).colorScheme.primary,\n                      size: const Size.square(20),\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass ChangeModelButton extends StatefulWidget {\n  const ChangeModelButton({\n    super.key,\n    required this.isInHoverBar,\n    this.popoverMutex,\n    this.onRegenerate,\n    this.onOverrideVisibility,\n  });\n\n  final bool isInHoverBar;\n  final PopoverMutex? popoverMutex;\n  final void Function(AIModelPB)? onRegenerate;\n  final void Function(bool)? onOverrideVisibility;\n\n  @override\n  State<ChangeModelButton> createState() => _ChangeModelButtonState();\n}\n\nclass _ChangeModelButtonState extends State<ChangeModelButton> {\n  final popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      controller: popoverController,\n      mutex: widget.popoverMutex,\n      triggerActions: PopoverTriggerFlags.none,\n      margin: EdgeInsets.zero,\n      offset: Offset(8, 0),\n      direction: PopoverDirection.rightWithBottomAligned,\n      constraints: BoxConstraints(maxWidth: 250, maxHeight: 600),\n      onClose: () => widget.onOverrideVisibility?.call(false),\n      child: buildButton(context),\n      popupBuilder: (_) {\n        final bloc = context.read<AIPromptInputBloc>();\n        final (models, _) = bloc.aiModelStateNotifier.getModelSelection();\n        return SelectModelPopoverContent(\n          models: models,\n          selectedModel: null,\n          onSelectModel: widget.onRegenerate,\n        );\n      },\n    );\n  }\n\n  Widget buildButton(BuildContext context) {\n    return FlowyTooltip(\n      message: LocaleKeys.chat_switchModel_label.tr(),\n      child: FlowyIconButton(\n        width: 32.0,\n        height: DesktopAIChatSizes.messageActionBarIconSize,\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        radius: widget.isInHoverBar\n            ? DesktopAIChatSizes.messageHoverActionBarIconRadius\n            : DesktopAIChatSizes.messageActionBarIconRadius,\n        icon: Row(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            FlowySvg(\n              FlowySvgs.ai_sparks_s,\n              color: Theme.of(context).hintColor,\n              size: const Size.square(16),\n            ),\n            FlowySvg(\n              FlowySvgs.ai_source_drop_down_s,\n              color: Theme.of(context).hintColor,\n              size: const Size.square(8),\n            ),\n          ],\n        ),\n        onPressed: () {\n          widget.onOverrideVisibility?.call(true);\n          popoverController.show();\n        },\n      ),\n    );\n  }\n}\n\nclass SaveToPageButton extends StatefulWidget {\n  const SaveToPageButton({\n    super.key,\n    required this.textMessage,\n    required this.isInHoverBar,\n    this.popoverMutex,\n    this.onOverrideVisibility,\n  });\n\n  final TextMessage textMessage;\n  final bool isInHoverBar;\n  final PopoverMutex? popoverMutex;\n  final void Function(bool)? onOverrideVisibility;\n\n  @override\n  State<SaveToPageButton> createState() => _SaveToPageButtonState();\n}\n\nclass _SaveToPageButtonState extends State<SaveToPageButton> {\n  final popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return ViewSelector(\n      viewSelectorCubit: BlocProvider(\n        create: (context) => ViewSelectorCubit(\n          getIgnoreViewType: (item) {\n            final view = item.view;\n\n            if (view.isSpace) {\n              return IgnoreViewType.none;\n            }\n            if (view.layout != ViewLayoutPB.Document) {\n              return IgnoreViewType.hide;\n            }\n\n            return IgnoreViewType.none;\n          },\n        ),\n      ),\n      child: BlocSelector<SpaceBloc, SpaceState, ViewPB?>(\n        selector: (state) => state.currentSpace,\n        builder: (context, spaceView) {\n          return AppFlowyPopover(\n            controller: popoverController,\n            triggerActions: PopoverTriggerFlags.none,\n            margin: EdgeInsets.zero,\n            mutex: widget.popoverMutex,\n            offset: const Offset(8, 0),\n            direction: PopoverDirection.rightWithBottomAligned,\n            constraints: const BoxConstraints.tightFor(width: 300, height: 400),\n            onClose: () {\n              if (spaceView != null) {\n                context\n                    .read<ViewSelectorCubit>()\n                    .refreshSources([spaceView], spaceView);\n              }\n              widget.onOverrideVisibility?.call(false);\n            },\n            child: buildButton(context, spaceView),\n            popupBuilder: (_) => buildPopover(context),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget buildButton(BuildContext context, ViewPB? spaceView) {\n    return FlowyTooltip(\n      message: LocaleKeys.chat_addToPageButton.tr(),\n      child: FlowyIconButton(\n        width: DesktopAIChatSizes.messageActionBarIconSize,\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        radius: widget.isInHoverBar\n            ? DesktopAIChatSizes.messageHoverActionBarIconRadius\n            : DesktopAIChatSizes.messageActionBarIconRadius,\n        icon: FlowySvg(\n          FlowySvgs.ai_add_to_page_s,\n          color: Theme.of(context).hintColor,\n          size: const Size.square(16),\n        ),\n        onPressed: () async {\n          final documentId = getOpenedDocumentId();\n          if (documentId != null) {\n            await onAddToExistingPage(context, documentId);\n            await forceReload(documentId);\n            await Future.delayed(const Duration(milliseconds: 500));\n            await updateSelection(documentId);\n          } else {\n            widget.onOverrideVisibility?.call(true);\n            if (spaceView != null) {\n              unawaited(\n                context\n                    .read<ViewSelectorCubit>()\n                    .refreshSources([spaceView], spaceView),\n              );\n            }\n            popoverController.show();\n          }\n        },\n      ),\n    );\n  }\n\n  Widget buildPopover(BuildContext context) {\n    return BlocProvider.value(\n      value: context.read<ViewSelectorCubit>(),\n      child: SaveToPagePopoverContent(\n        onAddToNewPage: (parentViewId) {\n          addMessageToNewPage(context, parentViewId);\n          popoverController.close();\n        },\n        onAddToExistingPage: (documentId) async {\n          popoverController.close();\n          final view = await onAddToExistingPage(context, documentId);\n\n          if (context.mounted) {\n            openPageFromMessage(context, view);\n          }\n          await Future.delayed(const Duration(milliseconds: 500));\n          await updateSelection(documentId);\n        },\n      ),\n    );\n  }\n\n  Future<ViewPB?> onAddToExistingPage(\n    BuildContext context,\n    String documentId,\n  ) async {\n    await ChatEditDocumentService.addMessagesToPage(\n      documentId,\n      [widget.textMessage],\n    );\n    await Future.delayed(const Duration(milliseconds: 500));\n    final view = await ViewBackendService.getView(documentId).toNullable();\n    if (context.mounted) {\n      showSaveMessageSuccessToast(context, view);\n    }\n    return view;\n  }\n\n  void addMessageToNewPage(BuildContext context, String parentViewId) async {\n    final chatView = await ViewBackendService.getView(\n      context.read<ChatAIMessageBloc>().chatId,\n    ).toNullable();\n    if (chatView != null) {\n      final newView = await ChatEditDocumentService.saveMessagesToNewPage(\n        chatView.nameOrDefault,\n        parentViewId,\n        [widget.textMessage],\n      );\n\n      if (context.mounted) {\n        showSaveMessageSuccessToast(context, newView);\n        openPageFromMessage(context, newView);\n      }\n    }\n  }\n\n  Future<void> forceReload(String documentId) async {\n    final bloc = DocumentBloc.findOpen(documentId);\n    if (bloc == null) {\n      return;\n    }\n    await bloc.forceReloadDocumentState();\n  }\n\n  Future<void> updateSelection(String documentId) async {\n    final bloc = DocumentBloc.findOpen(documentId);\n    if (bloc == null) {\n      return;\n    }\n    await bloc.forceReloadDocumentState();\n    final editorState = bloc.state.editorState;\n    final lastNodePath = editorState?.getLastSelectable()?.$1.path;\n    if (editorState == null || lastNodePath == null) {\n      return;\n    }\n    unawaited(\n      editorState.updateSelectionWithReason(\n        Selection.collapsed(Position(path: lastNodePath)),\n      ),\n    );\n  }\n\n  String? getOpenedDocumentId() {\n    final pageManager = getIt<TabsBloc>().state.currentPageManager;\n    if (!pageManager.showSecondaryPluginNotifier.value) {\n      return null;\n    }\n    return pageManager.secondaryNotifier.plugin.id;\n  }\n}\n\nclass SaveToPagePopoverContent extends StatelessWidget {\n  const SaveToPagePopoverContent({\n    super.key,\n    required this.onAddToNewPage,\n    required this.onAddToExistingPage,\n  });\n\n  final void Function(String) onAddToNewPage;\n  final void Function(String) onAddToExistingPage;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<ViewSelectorCubit, ViewSelectorState>(\n      builder: (context, state) {\n        final theme = AppFlowyTheme.of(context);\n\n        return Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            Container(\n              height: 24,\n              margin: const EdgeInsets.fromLTRB(12, 8, 12, 4),\n              child: Align(\n                alignment: AlignmentDirectional.centerStart,\n                child: Text(\n                  LocaleKeys.chat_addToPageTitle.tr(),\n                  style: theme.textStyle.caption\n                      .standard(color: theme.textColorScheme.secondary),\n                ),\n              ),\n            ),\n            Padding(\n              padding: const EdgeInsets.only(left: 12, right: 12, bottom: 8),\n              child: AFTextField(\n                controller:\n                    context.read<ViewSelectorCubit>().filterTextController,\n                hintText: LocaleKeys.search_label.tr(),\n                size: AFTextFieldSize.m,\n              ),\n            ),\n            AFDivider(\n              startIndent: theme.spacing.l,\n              endIndent: theme.spacing.l,\n            ),\n            Expanded(\n              child: ListView(\n                shrinkWrap: true,\n                padding: const EdgeInsets.fromLTRB(8, 4, 8, 12),\n                children: _buildVisibleSources(context, state).toList(),\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  Iterable<Widget> _buildVisibleSources(\n    BuildContext context,\n    ViewSelectorState state,\n  ) {\n    return state.visibleSources.map(\n      (e) => ViewSelectorTreeItem(\n        key: ValueKey(\n          'save_to_page_tree_item_${e.view.id}',\n        ),\n        viewSelectorItem: e,\n        level: 0,\n        isDescendentOfSpace: e.view.isSpace,\n        isSelectedSection: false,\n        showCheckbox: false,\n        showSaveButton: true,\n        onSelected: (source) {\n          if (source.view.isSpace) {\n            onAddToNewPage(source.view.id);\n          } else {\n            onAddToExistingPage(source.view.id);\n          }\n        },\n        onAdd: (source) {\n          onAddToNewPage(source.view.id);\n        },\n        height: 30.0,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_edit_document_service.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_select_message_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/shared/markdown_to_document.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../layout_define.dart';\nimport 'ai_change_format_bottom_sheet.dart';\nimport 'ai_change_model_bottom_sheet.dart';\nimport 'ai_message_action_bar.dart';\nimport 'message_util.dart';\n\n/// Wraps an AI response message with the avatar and actions. On desktop,\n/// the actions will be displayed below the response if the response is the\n/// last message in the chat. For the others, the actions will be shown on hover\n/// On mobile, the actions will be displayed in a bottom sheet on long press.\nclass ChatAIMessageBubble extends StatelessWidget {\n  const ChatAIMessageBubble({\n    super.key,\n    required this.message,\n    required this.child,\n    required this.showActions,\n    this.isLastMessage = false,\n    this.isSelectingMessages = false,\n    this.onRegenerate,\n    this.onChangeFormat,\n    this.onChangeModel,\n  });\n\n  final Message message;\n  final Widget child;\n  final bool showActions;\n  final bool isLastMessage;\n  final bool isSelectingMessages;\n  final void Function()? onRegenerate;\n  final void Function(PredefinedFormat)? onChangeFormat;\n  final void Function(AIModelPB)? onChangeModel;\n\n  @override\n  Widget build(BuildContext context) {\n    final messageWidget = _WrapIsSelectingMessage(\n      isSelectingMessages: isSelectingMessages,\n      message: message,\n      child: child,\n    );\n\n    return !isSelectingMessages && showActions\n        ? UniversalPlatform.isMobile\n            ? _wrapPopMenu(messageWidget)\n            : isLastMessage\n                ? _wrapBottomActions(messageWidget)\n                : _wrapHover(messageWidget)\n        : messageWidget;\n  }\n\n  Widget _wrapBottomActions(Widget child) {\n    return ChatAIBottomInlineActions(\n      message: message,\n      onRegenerate: onRegenerate,\n      onChangeFormat: onChangeFormat,\n      onChangeModel: onChangeModel,\n      child: child,\n    );\n  }\n\n  Widget _wrapHover(Widget child) {\n    return ChatAIMessageHover(\n      message: message,\n      onRegenerate: onRegenerate,\n      onChangeFormat: onChangeFormat,\n      onChangeModel: onChangeModel,\n      child: child,\n    );\n  }\n\n  Widget _wrapPopMenu(Widget child) {\n    return ChatAIMessagePopup(\n      message: message,\n      onRegenerate: onRegenerate,\n      onChangeFormat: onChangeFormat,\n      onChangeModel: onChangeModel,\n      child: child,\n    );\n  }\n}\n\nclass ChatAIBottomInlineActions extends StatelessWidget {\n  const ChatAIBottomInlineActions({\n    super.key,\n    required this.child,\n    required this.message,\n    this.onRegenerate,\n    this.onChangeFormat,\n    this.onChangeModel,\n  });\n\n  final Widget child;\n  final Message message;\n  final void Function()? onRegenerate;\n  final void Function(PredefinedFormat)? onChangeFormat;\n  final void Function(AIModelPB)? onChangeModel;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        child,\n        const VSpace(16.0),\n        AIMessageActionBar(\n          message: message,\n          showDecoration: false,\n          onRegenerate: onRegenerate,\n          onChangeFormat: onChangeFormat,\n          onChangeModel: onChangeModel,\n        ),\n        const VSpace(16.0),\n      ],\n    );\n  }\n}\n\nclass ChatAIMessageHover extends StatefulWidget {\n  const ChatAIMessageHover({\n    super.key,\n    required this.child,\n    required this.message,\n    this.onRegenerate,\n    this.onChangeFormat,\n    this.onChangeModel,\n  });\n\n  final Widget child;\n  final Message message;\n  final void Function()? onRegenerate;\n  final void Function(PredefinedFormat)? onChangeFormat;\n  final void Function(AIModelPB)? onChangeModel;\n\n  @override\n  State<ChatAIMessageHover> createState() => _ChatAIMessageHoverState();\n}\n\nclass _ChatAIMessageHoverState extends State<ChatAIMessageHover> {\n  final controller = OverlayPortalController();\n  final layerLink = LayerLink();\n\n  bool hoverBubble = false;\n  bool hoverActionBar = false;\n  bool overrideVisibility = false;\n\n  ScrollPosition? scrollPosition;\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      addScrollListener();\n      controller.show();\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      opaque: false,\n      onEnter: (_) {\n        if (!hoverBubble && isBottomOfWidgetVisible(context)) {\n          setState(() => hoverBubble = true);\n        }\n      },\n      onHover: (_) {\n        if (!hoverBubble && isBottomOfWidgetVisible(context)) {\n          setState(() => hoverBubble = true);\n        }\n      },\n      onExit: (_) {\n        if (hoverBubble) {\n          setState(() => hoverBubble = false);\n        }\n      },\n      child: OverlayPortal(\n        controller: controller,\n        overlayChildBuilder: (_) {\n          return CompositedTransformFollower(\n            showWhenUnlinked: false,\n            link: layerLink,\n            targetAnchor: Alignment.bottomLeft,\n            offset: const Offset(4.0, 0.0),\n            child: Align(\n              alignment: Alignment.topLeft,\n              child: MouseRegion(\n                opaque: false,\n                onEnter: (_) {\n                  if (!hoverActionBar && isBottomOfWidgetVisible(context)) {\n                    setState(() => hoverActionBar = true);\n                  }\n                },\n                onExit: (_) {\n                  if (hoverActionBar) {\n                    setState(() => hoverActionBar = false);\n                  }\n                },\n                child: SizedBox(\n                  width: 784,\n                  height: DesktopAIChatSizes.messageActionBarIconSize +\n                      DesktopAIChatSizes.messageHoverActionBarPadding.vertical,\n                  child: hoverBubble || hoverActionBar || overrideVisibility\n                      ? Align(\n                          alignment: AlignmentDirectional.centerStart,\n                          child: AIMessageActionBar(\n                            message: widget.message,\n                            showDecoration: true,\n                            onRegenerate: widget.onRegenerate,\n                            onChangeFormat: widget.onChangeFormat,\n                            onChangeModel: widget.onChangeModel,\n                            onOverrideVisibility: (visibility) {\n                              overrideVisibility = visibility;\n                            },\n                          ),\n                        )\n                      : null,\n                ),\n              ),\n            ),\n          );\n        },\n        child: CompositedTransformTarget(\n          link: layerLink,\n          child: widget.child,\n        ),\n      ),\n    );\n  }\n\n  void addScrollListener() {\n    if (!mounted) {\n      return;\n    }\n    scrollPosition = Scrollable.maybeOf(context)?.position;\n    scrollPosition?.addListener(handleScroll);\n  }\n\n  void handleScroll() {\n    if (!mounted) {\n      return;\n    }\n    if ((hoverActionBar || hoverBubble) && !isBottomOfWidgetVisible(context)) {\n      setState(() {\n        hoverBubble = false;\n        hoverActionBar = false;\n      });\n    }\n  }\n\n  bool isBottomOfWidgetVisible(BuildContext context) {\n    if (Scrollable.maybeOf(context) == null) {\n      return false;\n    }\n    final scrollableRenderBox =\n        Scrollable.of(context).context.findRenderObject() as RenderBox;\n    final scrollableHeight = scrollableRenderBox.size.height;\n    final scrollableOffset = scrollableRenderBox.localToGlobal(Offset.zero);\n\n    final messageRenderBox = context.findRenderObject() as RenderBox;\n    final messageOffset = messageRenderBox.localToGlobal(Offset.zero);\n    final messageHeight = messageRenderBox.size.height;\n\n    return messageOffset.dy +\n            messageHeight +\n            DesktopAIChatSizes.messageActionBarIconSize +\n            DesktopAIChatSizes.messageHoverActionBarPadding.vertical <=\n        scrollableOffset.dy + scrollableHeight;\n  }\n\n  @override\n  void dispose() {\n    scrollPosition?.isScrollingNotifier.removeListener(handleScroll);\n    super.dispose();\n  }\n}\n\nclass ChatAIMessagePopup extends StatelessWidget {\n  const ChatAIMessagePopup({\n    super.key,\n    required this.child,\n    required this.message,\n    this.onRegenerate,\n    this.onChangeFormat,\n    this.onChangeModel,\n  });\n\n  final Widget child;\n  final Message message;\n  final void Function()? onRegenerate;\n  final void Function(PredefinedFormat)? onChangeFormat;\n  final void Function(AIModelPB)? onChangeModel;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onLongPress: () {\n        showMobileBottomSheet(\n          context,\n          showDragHandle: true,\n          backgroundColor: AFThemeExtension.of(context).background,\n          builder: (bottomSheetContext) {\n            return Column(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                _copyButton(context, bottomSheetContext),\n                _divider(),\n                _regenerateButton(context),\n                _divider(),\n                _changeFormatButton(context),\n                _divider(),\n                _changeModelButton(context),\n                _divider(),\n                _saveToPageButton(context),\n              ],\n            );\n          },\n        );\n      },\n      child: child,\n    );\n  }\n\n  Widget _divider() => const MobileQuickActionDivider();\n\n  Widget _copyButton(BuildContext context, BuildContext bottomSheetContext) {\n    return MobileQuickActionButton(\n      onTap: () async {\n        if (message is! TextMessage) {\n          return;\n        }\n        final textMessage = message as TextMessage;\n        final document = customMarkdownToDocument(textMessage.text);\n        await getIt<ClipboardService>().setData(\n          ClipboardServiceData(\n            plainText: textMessage.text,\n            inAppJson: jsonEncode(document.toJson()),\n          ),\n        );\n        if (bottomSheetContext.mounted) {\n          Navigator.of(bottomSheetContext).pop();\n        }\n        if (context.mounted) {\n          showToastNotification(\n            message: LocaleKeys.message_copy_success.tr(),\n          );\n        }\n      },\n      icon: FlowySvgs.copy_s,\n      iconSize: const Size.square(20),\n      text: LocaleKeys.button_copy.tr(),\n    );\n  }\n\n  Widget _regenerateButton(BuildContext context) {\n    return MobileQuickActionButton(\n      onTap: () {\n        onRegenerate?.call();\n        Navigator.of(context).pop();\n      },\n      icon: FlowySvgs.ai_try_again_s,\n      iconSize: const Size.square(20),\n      text: LocaleKeys.chat_regenerate.tr(),\n    );\n  }\n\n  Widget _changeFormatButton(BuildContext context) {\n    return MobileQuickActionButton(\n      onTap: () async {\n        final result = await showChangeFormatBottomSheet(context);\n        if (result != null) {\n          onChangeFormat?.call(result);\n          if (context.mounted) {\n            Navigator.of(context).pop();\n          }\n        }\n      },\n      icon: FlowySvgs.ai_retry_font_s,\n      iconSize: const Size.square(20),\n      text: LocaleKeys.chat_changeFormat_actionButton.tr(),\n    );\n  }\n\n  Widget _changeModelButton(BuildContext context) {\n    return MobileQuickActionButton(\n      onTap: () async {\n        final bloc = context.read<AIPromptInputBloc>();\n        final (models, _) = bloc.aiModelStateNotifier.getModelSelection();\n        final result = await showChangeModelBottomSheet(context, models);\n        if (result != null) {\n          onChangeModel?.call(result);\n          if (context.mounted) {\n            Navigator.of(context).pop();\n          }\n        }\n      },\n      icon: FlowySvgs.ai_sparks_s,\n      iconSize: const Size.square(20),\n      text: LocaleKeys.chat_switchModel_label.tr(),\n    );\n  }\n\n  Widget _saveToPageButton(BuildContext context) {\n    return MobileQuickActionButton(\n      onTap: () async {\n        final selectedView = await showPageSelectorSheet(\n          context,\n          filter: (view) =>\n              !view.isSpace &&\n              view.layout.isDocumentView &&\n              view.parentViewId != view.id,\n        );\n        if (selectedView == null) {\n          return;\n        }\n\n        await ChatEditDocumentService.addMessagesToPage(\n          selectedView.id,\n          [message as TextMessage],\n        );\n\n        if (context.mounted) {\n          context.pop();\n          openPageFromMessage(context, selectedView);\n        }\n      },\n      icon: FlowySvgs.ai_add_to_page_s,\n      iconSize: const Size.square(20),\n      text: LocaleKeys.chat_addToPageButton.tr(),\n    );\n  }\n}\n\nclass _WrapIsSelectingMessage extends StatelessWidget {\n  const _WrapIsSelectingMessage({\n    required this.message,\n    required this.child,\n    this.isSelectingMessages = false,\n  });\n\n  final Message message;\n  final Widget child;\n  final bool isSelectingMessages;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<ChatSelectMessageBloc, ChatSelectMessageState>(\n      builder: (context, state) {\n        final isSelected =\n            context.read<ChatSelectMessageBloc>().isMessageSelected(message.id);\n        return GestureDetector(\n          onTap: () {\n            if (isSelectingMessages) {\n              context\n                  .read<ChatSelectMessageBloc>()\n                  .add(ChatSelectMessageEvent.toggleSelectMessage(message));\n            }\n          },\n          behavior: isSelectingMessages ? HitTestBehavior.opaque : null,\n          child: DecoratedBox(\n            decoration: BoxDecoration(\n              color: isSelected\n                  ? Theme.of(context).colorScheme.tertiaryContainer\n                  : null,\n              borderRadius: const BorderRadius.all(Radius.circular(8.0)),\n            ),\n            child: Row(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                if (isSelectingMessages) ...[\n                  ChatSelectMessageIndicator(isSelected: isSelected),\n                  const HSpace(DesktopAIChatSizes.avatarAndChatBubbleSpacing),\n                ],\n                Expanded(\n                  child: IgnorePointer(\n                    ignoring: isSelectingMessages,\n                    child: child,\n                  ),\n                ),\n              ],\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass ChatSelectMessageIndicator extends StatelessWidget {\n  const ChatSelectMessageIndicator({\n    super.key,\n    required this.isSelected,\n  });\n\n  final bool isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      cursor: SystemMouseCursors.click,\n      child: SizedBox.square(\n        dimension: 30.0,\n        child: Center(\n          child: FlowySvg(\n            isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,\n            blendMode: BlendMode.dst,\n            size: const Size.square(20),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:string_validator/string_validator.dart';\nimport 'package:time/time.dart';\n\nclass AIMessageMetadata extends StatefulWidget {\n  const AIMessageMetadata({\n    required this.sources,\n    required this.onSelectedMetadata,\n    super.key,\n  });\n\n  final List<ChatMessageRefSource> sources;\n  final void Function(ChatMessageRefSource metadata)? onSelectedMetadata;\n\n  @override\n  State<AIMessageMetadata> createState() => _AIMessageMetadataState();\n}\n\nclass _AIMessageMetadataState extends State<AIMessageMetadata> {\n  bool isExpanded = true;\n\n  @override\n  Widget build(BuildContext context) {\n    return AnimatedSize(\n      duration: 150.milliseconds,\n      alignment: AlignmentDirectional.topStart,\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          const VSpace(8.0),\n          ConstrainedBox(\n            constraints: const BoxConstraints(\n              maxHeight: 24,\n              maxWidth: 240,\n            ),\n            child: FlowyButton(\n              margin: const EdgeInsets.all(4.0),\n              useIntrinsicWidth: true,\n              hoverColor: Colors.transparent,\n              radius: BorderRadius.circular(8.0),\n              text: FlowyText(\n                LocaleKeys.chat_referenceSource.plural(\n                  widget.sources.length,\n                  namedArgs: {'count': '${widget.sources.length}'},\n                ),\n                fontSize: 12,\n                color: Theme.of(context).hintColor,\n              ),\n              rightIcon: FlowySvg(\n                isExpanded ? FlowySvgs.arrow_up_s : FlowySvgs.arrow_down_s,\n                size: const Size.square(10),\n              ),\n              onTap: () {\n                setState(() => isExpanded = !isExpanded);\n              },\n            ),\n          ),\n          if (isExpanded) ...[\n            const VSpace(4.0),\n            Wrap(\n              spacing: 8.0,\n              runSpacing: 4.0,\n              children: widget.sources.map(\n                (m) {\n                  if (isURL(m.id)) {\n                    return _MetadataButton(\n                      name: m.id,\n                      onTap: () => widget.onSelectedMetadata?.call(m),\n                    );\n                  } else if (isUUID(m.id)) {\n                    return FutureBuilder<ViewPB?>(\n                      future: ViewBackendService.getView(m.id)\n                          .then((f) => f.toNullable()),\n                      builder: (context, snapshot) {\n                        final data = snapshot.data;\n                        if (!snapshot.hasData ||\n                            snapshot.connectionState != ConnectionState.done ||\n                            data == null) {\n                          return _MetadataButton(\n                            name: m.name,\n                            onTap: () => widget.onSelectedMetadata?.call(m),\n                          );\n                        }\n                        return BlocProvider(\n                          create: (_) => ViewBloc(view: data),\n                          child: BlocBuilder<ViewBloc, ViewState>(\n                            builder: (context, state) {\n                              return _MetadataButton(\n                                name: state.view.nameOrDefault,\n                                onTap: () => widget.onSelectedMetadata?.call(m),\n                              );\n                            },\n                          ),\n                        );\n                      },\n                    );\n                  } else {\n                    return _MetadataButton(\n                      name: m.name,\n                      onTap: () => widget.onSelectedMetadata?.call(m),\n                    );\n                  }\n                },\n              ).toList(),\n            ),\n          ],\n        ],\n      ),\n    );\n  }\n}\n\nclass _MetadataButton extends StatelessWidget {\n  const _MetadataButton({\n    this.name = \"\",\n    this.onTap,\n  });\n\n  final String name;\n  final void Function()? onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return ConstrainedBox(\n      constraints: const BoxConstraints(\n        maxHeight: 24,\n        maxWidth: 240,\n      ),\n      child: FlowyButton(\n        margin: const EdgeInsets.all(4.0),\n        useIntrinsicWidth: true,\n        radius: BorderRadius.circular(8.0),\n        text: FlowyText(\n          name,\n          fontSize: 12,\n          overflow: TextOverflow.ellipsis,\n        ),\n        leftIcon: FlowySvg(\n          FlowySvgs.icon_document_s,\n          size: const Size.square(16),\n          color: Theme.of(context).hintColor,\n        ),\n        onTap: onTap,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_message_height_manager.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/widgets/message_height_calculator.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\n\nimport '../layout_define.dart';\nimport 'ai_markdown_text.dart';\nimport 'ai_message_bubble.dart';\nimport 'ai_metadata.dart';\nimport 'error_text_message.dart';\n\n/// [ChatAIMessageWidget] includes both the text of the AI response as well as\n/// the avatar, decorations and hover effects that are also rendered. This is\n/// different from [ChatUserMessageWidget] which only contains the message and\n/// has to be separately wrapped with a bubble since the hover effects need to\n/// know the current streaming status of the message.\nclass ChatAIMessageWidget extends StatelessWidget {\n  const ChatAIMessageWidget({\n    super.key,\n    required this.user,\n    required this.messageUserId,\n    required this.message,\n    required this.stream,\n    required this.questionId,\n    required this.chatId,\n    required this.refSourceJsonString,\n    required this.onStopStream,\n    this.onSelectedMetadata,\n    this.onRegenerate,\n    this.onChangeFormat,\n    this.onChangeModel,\n    this.isLastMessage = false,\n    this.isStreaming = false,\n    this.isSelectingMessages = false,\n    this.enableAnimation = true,\n    this.hasRelatedQuestions = false,\n  });\n\n  final User user;\n  final String messageUserId;\n\n  final Message message;\n  final AnswerStream? stream;\n  final Int64? questionId;\n  final String chatId;\n  final String? refSourceJsonString;\n  final void Function(ChatMessageRefSource metadata)? onSelectedMetadata;\n  final void Function()? onRegenerate;\n  final void Function() onStopStream;\n  final void Function(PredefinedFormat)? onChangeFormat;\n  final void Function(AIModelPB)? onChangeModel;\n  final bool isStreaming;\n  final bool isLastMessage;\n  final bool isSelectingMessages;\n  final bool enableAnimation;\n  final bool hasRelatedQuestions;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => ChatAIMessageBloc(\n        message: stream ?? (message as TextMessage).text,\n        refSourceJsonString: refSourceJsonString,\n        chatId: chatId,\n        questionId: questionId,\n      ),\n      child: BlocConsumer<ChatAIMessageBloc, ChatAIMessageState>(\n        listenWhen: (previous, current) =>\n            previous.messageState != current.messageState,\n        listener: (context, state) => _handleMessageState(state, context),\n        builder: (context, blocState) {\n          final loadingText = blocState.progress?.step ??\n              LocaleKeys.chat_generatingResponse.tr();\n\n          // Calculate minimum height only for the last AI answer message\n          double minHeight = 0;\n          if (isLastMessage && !hasRelatedQuestions) {\n            final screenHeight = MediaQuery.of(context).size.height;\n            minHeight = ChatMessageHeightManager().calculateMinHeight(\n              messageId: message.id,\n              screenHeight: screenHeight,\n            );\n          }\n\n          return Container(\n            alignment: Alignment.topLeft,\n            constraints: BoxConstraints(\n              minHeight: minHeight,\n            ),\n            padding: AIChatUILayout.messageMargin,\n            child: MessageHeightCalculator(\n              messageId: message.id,\n              onHeightMeasured: (messageId, height) {\n                ChatMessageHeightManager().cacheWithoutMinHeight(\n                  messageId: messageId,\n                  height: height,\n                );\n              },\n              child: blocState.messageState.when(\n                loading: () => ChatAIMessageBubble(\n                  message: message,\n                  showActions: false,\n                  child: Padding(\n                    padding: const EdgeInsets.only(top: 8.0),\n                    child: AILoadingIndicator(text: loadingText),\n                  ),\n                ),\n                ready: () {\n                  return blocState.text.isEmpty\n                      ? _LoadingMessage(\n                          message: message,\n                          loadingText: loadingText,\n                        )\n                      : _NonEmptyMessage(\n                          user: user,\n                          messageUserId: messageUserId,\n                          message: message,\n                          stream: stream,\n                          questionId: questionId,\n                          chatId: chatId,\n                          refSourceJsonString: refSourceJsonString,\n                          onStopStream: onStopStream,\n                          onSelectedMetadata: onSelectedMetadata,\n                          onRegenerate: onRegenerate,\n                          onChangeFormat: onChangeFormat,\n                          onChangeModel: onChangeModel,\n                          isLastMessage: isLastMessage,\n                          isStreaming: isStreaming,\n                          isSelectingMessages: isSelectingMessages,\n                          enableAnimation: enableAnimation,\n                        );\n                },\n                onError: (error) {\n                  return ChatErrorMessageWidget(\n                    errorMessage: LocaleKeys.chat_aiServerUnavailable.tr(),\n                  );\n                },\n                onAIResponseLimit: () {\n                  return ChatErrorMessageWidget(\n                    errorMessage:\n                        LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr(),\n                  );\n                },\n                onAIImageResponseLimit: () {\n                  return ChatErrorMessageWidget(\n                    errorMessage: LocaleKeys.sideBar_purchaseAIMax.tr(),\n                  );\n                },\n                onAIMaxRequired: (message) {\n                  return ChatErrorMessageWidget(\n                    errorMessage: message,\n                  );\n                },\n                onInitializingLocalAI: () {\n                  onStopStream();\n\n                  return ChatErrorMessageWidget(\n                    errorMessage: LocaleKeys\n                        .settings_aiPage_keys_localAIInitializing\n                        .tr(),\n                  );\n                },\n                aiFollowUp: (followUpData) {\n                  return const SizedBox.shrink();\n                },\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  void _handleMessageState(ChatAIMessageState state, BuildContext context) {\n    if (state.stream?.error?.isEmpty != false) {\n      state.messageState.maybeMap(\n        aiFollowUp: (messageState) {\n          context\n              .read<ChatBloc>()\n              .add(ChatEvent.onAIFollowUp(messageState.followUpData));\n        },\n        orElse: () {\n          // do nothing\n        },\n      );\n\n      return;\n    }\n    context.read<ChatBloc>().add(ChatEvent.deleteMessage(message));\n  }\n}\n\nclass _LoadingMessage extends StatelessWidget {\n  const _LoadingMessage({\n    required this.message,\n    required this.loadingText,\n  });\n\n  final Message message;\n  final String loadingText;\n\n  @override\n  Widget build(BuildContext context) {\n    return ChatAIMessageBubble(\n      message: message,\n      showActions: false,\n      child: Padding(\n        padding: EdgeInsetsDirectional.only(start: 4.0, top: 8.0),\n        child: AILoadingIndicator(text: loadingText),\n      ),\n    );\n  }\n}\n\nclass _NonEmptyMessage extends StatelessWidget {\n  const _NonEmptyMessage({\n    required this.user,\n    required this.messageUserId,\n    required this.message,\n    required this.stream,\n    required this.questionId,\n    required this.chatId,\n    required this.refSourceJsonString,\n    required this.onStopStream,\n    this.onSelectedMetadata,\n    this.onRegenerate,\n    this.onChangeFormat,\n    this.onChangeModel,\n    this.isLastMessage = false,\n    this.isStreaming = false,\n    this.isSelectingMessages = false,\n    this.enableAnimation = true,\n  });\n\n  final User user;\n  final String messageUserId;\n\n  final Message message;\n  final AnswerStream? stream;\n  final Int64? questionId;\n  final String chatId;\n  final String? refSourceJsonString;\n  final ValueChanged<ChatMessageRefSource>? onSelectedMetadata;\n  final VoidCallback? onRegenerate;\n  final VoidCallback onStopStream;\n  final ValueChanged<PredefinedFormat>? onChangeFormat;\n  final ValueChanged<AIModelPB>? onChangeModel;\n  final bool isStreaming;\n  final bool isLastMessage;\n  final bool isSelectingMessages;\n  final bool enableAnimation;\n\n  @override\n  Widget build(BuildContext context) {\n    final state = context.read<ChatAIMessageBloc>().state;\n    final showActions = stream == null && state.text.isNotEmpty && !isStreaming;\n    return ChatAIMessageBubble(\n      message: message,\n      isLastMessage: isLastMessage,\n      showActions: showActions,\n      isSelectingMessages: isSelectingMessages,\n      onRegenerate: onRegenerate,\n      onChangeFormat: onChangeFormat,\n      onChangeModel: onChangeModel,\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Padding(\n            padding: EdgeInsetsDirectional.only(start: 4.0),\n            child: AIMarkdownText(\n              markdown: state.text,\n              withAnimation: enableAnimation && stream != null,\n            ),\n          ),\n          if (state.sources.isNotEmpty)\n            SelectionContainer.disabled(\n              child: AIMessageMetadata(\n                sources: state.sources,\n                onSelectedMetadata: onSelectedMetadata,\n              ),\n            ),\n          if (state.sources.isNotEmpty && !isLastMessage) const VSpace(8.0),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/error_text_message.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass ChatErrorMessageWidget extends StatefulWidget {\n  const ChatErrorMessageWidget({\n    super.key,\n    required this.errorMessage,\n    this.onRetry,\n  });\n\n  final String errorMessage;\n  final VoidCallback? onRetry;\n\n  @override\n  State<ChatErrorMessageWidget> createState() => _ChatErrorMessageWidgetState();\n}\n\nclass _ChatErrorMessageWidgetState extends State<ChatErrorMessageWidget> {\n  late final TapGestureRecognizer recognizer;\n\n  @override\n  void initState() {\n    super.initState();\n    recognizer = TapGestureRecognizer()..onTap = widget.onRetry;\n  }\n\n  @override\n  void dispose() {\n    recognizer.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Center(\n      child: Container(\n        margin: const EdgeInsets.only(top: 16.0, bottom: 24.0) +\n            (UniversalPlatform.isMobile\n                ? const EdgeInsets.symmetric(horizontal: 16)\n                : EdgeInsets.zero),\n        padding: const EdgeInsets.all(8.0),\n        decoration: BoxDecoration(\n          color: Theme.of(context).isLightMode\n              ? const Color(0x80FFE7EE)\n              : const Color(0x80591734),\n          borderRadius: const BorderRadius.all(Radius.circular(8.0)),\n        ),\n        constraints: UniversalPlatform.isDesktop\n            ? const BoxConstraints(maxWidth: 480)\n            : null,\n        child: Row(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            const FlowySvg(\n              FlowySvgs.toast_error_filled_s,\n              blendMode: null,\n            ),\n            const HSpace(8.0),\n            Flexible(\n              child: _buildText(),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildText() {\n    final errorMessage = widget.errorMessage;\n\n    return widget.onRetry != null\n        ? RichText(\n            text: TextSpan(\n              children: [\n                TextSpan(\n                  text: errorMessage,\n                  style: Theme.of(context).textTheme.bodyMedium,\n                ),\n                TextSpan(\n                  text: ' ',\n                  style: Theme.of(context).textTheme.bodyMedium,\n                ),\n                TextSpan(\n                  text: LocaleKeys.chat_retry.tr(),\n                  recognizer: recognizer,\n                  mouseCursor: SystemMouseCursors.click,\n                  style: Theme.of(context).textTheme.bodyMedium?.copyWith(\n                        fontWeight: FontWeight.w600,\n                        decoration: TextDecoration.underline,\n                      ),\n                ),\n              ],\n            ),\n          )\n        : FlowyText(\n            errorMessage,\n            lineHeight: 1.4,\n            maxLines: null,\n          );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\n/// Opens a message in the right hand sidebar on desktop, and push the page\n/// on mobile\nvoid openPageFromMessage(BuildContext context, ViewPB? view) {\n  if (view == null) {\n    showToastNotification(\n      message: LocaleKeys.chat_openPagePreviewFailedToast.tr(),\n      type: ToastificationType.error,\n    );\n    return;\n  }\n  if (UniversalPlatform.isDesktop) {\n    getIt<TabsBloc>().add(\n      TabsEvent.openSecondaryPlugin(\n        plugin: view.plugin(),\n      ),\n    );\n  } else {\n    context.pushView(view);\n  }\n}\n\nvoid showSaveMessageSuccessToast(BuildContext context, ViewPB? view) {\n  if (view == null) {\n    return;\n  }\n  showToastNotification(\n    richMessage: TextSpan(\n      children: [\n        TextSpan(\n          text: LocaleKeys.chat_addToNewPageSuccessToast.tr(),\n          style: Theme.of(context).textTheme.bodyMedium?.copyWith(\n                color: const Color(0xFFFFFFFF),\n              ),\n        ),\n        const TextSpan(\n          text: ' ',\n        ),\n        TextSpan(\n          text: view.nameOrDefault,\n          style: Theme.of(context).textTheme.bodyMedium?.copyWith(\n                color: const Color(0xFFFFFFFF),\n                fontWeight: FontWeight.w700,\n              ),\n        ),\n      ],\n    ),\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_member_bloc.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\n\nimport '../chat_avatar.dart';\nimport '../layout_define.dart';\n\nclass ChatUserMessageBubble extends StatelessWidget {\n  const ChatUserMessageBubble({\n    super.key,\n    required this.message,\n    required this.child,\n    this.files = const [],\n  });\n\n  final Message message;\n  final Widget child;\n  final List<ChatFile> files;\n\n  @override\n  Widget build(BuildContext context) {\n    context\n        .read<ChatMemberBloc>()\n        .add(ChatMemberEvent.getMemberInfo(message.author.id));\n\n    return Padding(\n      padding: AIChatUILayout.messageMargin,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        crossAxisAlignment: CrossAxisAlignment.end,\n        children: [\n          if (files.isNotEmpty) ...[\n            Padding(\n              padding: const EdgeInsets.only(right: 32),\n              child: _MessageFileList(files: files),\n            ),\n            const VSpace(6),\n          ],\n          Row(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            mainAxisAlignment: MainAxisAlignment.end,\n            children: [\n              const Spacer(),\n              _buildBubble(context),\n              const HSpace(DesktopAIChatSizes.avatarAndChatBubbleSpacing),\n              _buildAvatar(),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildAvatar() {\n    return BlocBuilder<ChatMemberBloc, ChatMemberState>(\n      builder: (context, state) {\n        final member = state.members[message.author.id];\n        return SelectionContainer.disabled(\n          child: ChatUserAvatar(\n            iconUrl: member?.info.avatarUrl ?? \"\",\n            name: member?.info.name ?? \"\",\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildBubble(BuildContext context) {\n    return Flexible(\n      flex: 5,\n      child: Container(\n        decoration: BoxDecoration(\n          borderRadius: const BorderRadius.all(Radius.circular(16.0)),\n          color: Theme.of(context).colorScheme.surfaceContainerHighest,\n        ),\n        padding: const EdgeInsets.symmetric(\n          horizontal: 16.0,\n          vertical: 8.0,\n        ),\n        child: child,\n      ),\n    );\n  }\n}\n\nclass _MessageFileList extends StatelessWidget {\n  const _MessageFileList({required this.files});\n\n  final List<ChatFile> files;\n\n  @override\n  Widget build(BuildContext context) {\n    final List<Widget> children = files\n        .map(\n          (file) => _MessageFile(\n            file: file,\n          ),\n        )\n        .toList();\n\n    return Wrap(\n      direction: Axis.vertical,\n      crossAxisAlignment: WrapCrossAlignment.end,\n      spacing: 6,\n      runSpacing: 6,\n      children: children,\n    );\n  }\n}\n\nclass _MessageFile extends StatelessWidget {\n  const _MessageFile({required this.file});\n\n  final ChatFile file;\n\n  @override\n  Widget build(BuildContext context) {\n    return DecoratedBox(\n      decoration: BoxDecoration(\n        color: Colors.transparent,\n        borderRadius: BorderRadius.circular(10),\n        border: Border.all(\n          color: Theme.of(context).colorScheme.secondary,\n        ),\n      ),\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 16),\n        child: Row(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            FlowySvg(\n              FlowySvgs.page_m,\n              size: const Size.square(16),\n              color: Theme.of(context).hintColor,\n            ),\n            const HSpace(6),\n            Flexible(\n              child: ConstrainedBox(\n                constraints: const BoxConstraints(maxWidth: 400),\n                child: FlowyText(\n                  file.fileName,\n                  fontSize: 12,\n                  maxLines: 6,\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart",
    "content": "import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_message_service.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';\nimport 'package:appflowy/plugins/ai_chat/application/chat_user_message_bloc.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_chat_core/flutter_chat_core.dart';\n\nimport 'user_message_bubble.dart';\n\nclass ChatUserMessageWidget extends StatelessWidget {\n  const ChatUserMessageWidget({\n    super.key,\n    required this.user,\n    required this.message,\n  });\n\n  final User user;\n  final TextMessage message;\n\n  @override\n  Widget build(BuildContext context) {\n    final stream = message.metadata?[\"$QuestionStream\"];\n    final messageText = stream is QuestionStream ? stream.text : message.text;\n\n    return BlocProvider(\n      create: (context) => ChatUserMessageBloc(\n        text: messageText,\n        questionStream: stream,\n      ),\n      child: ChatUserMessageBubble(\n        message: message,\n        files: _getFiles(),\n        child: BlocBuilder<ChatUserMessageBloc, ChatUserMessageState>(\n          builder: (context, state) {\n            return Opacity(\n              opacity: state.messageState.isFinish ? 1.0 : 0.8,\n              child: TextMessageText(\n                text: state.text,\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  List<ChatFile> _getFiles() {\n    if (message.metadata == null) {\n      return const [];\n    }\n\n    final refSourceMetadata =\n        message.metadata?[messageRefSourceJsonStringKey] as String?;\n    if (refSourceMetadata != null) {\n      return chatFilesFromMetadataString(refSourceMetadata);\n    }\n\n    final chatFileList =\n        message.metadata![messageChatFileListKey] as List<ChatFile>?;\n    return chatFileList ?? [];\n  }\n}\n\n/// Widget to reuse the markdown capabilities, e.g., for previews.\nclass TextMessageText extends StatelessWidget {\n  const TextMessageText({\n    super.key,\n    required this.text,\n  });\n\n  /// Text that is shown as markdown.\n  final String text;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyText(\n      text,\n      lineHeight: 1.4,\n      maxLines: null,\n      color: AFThemeExtension.of(context).textColor,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/scroll_to_bottom.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\n\nconst BorderRadius _borderRadius = BorderRadius.all(Radius.circular(16));\n\nclass CustomScrollToBottom extends StatelessWidget {\n  const CustomScrollToBottom({\n    super.key,\n    required this.animation,\n    required this.onPressed,\n  });\n\n  final Animation<double> animation;\n  final VoidCallback onPressed;\n\n  @override\n  Widget build(BuildContext context) {\n    final isLightMode = Theme.of(context).isLightMode;\n\n    return Positioned(\n      bottom: 24,\n      left: 0,\n      right: 0,\n      child: Center(\n        child: ScaleTransition(\n          scale: animation,\n          child: DecoratedBox(\n            decoration: BoxDecoration(\n              color: Theme.of(context).colorScheme.surface,\n              border: Border.all(\n                color: Theme.of(context).dividerColor,\n                strokeAlign: BorderSide.strokeAlignOutside,\n              ),\n              borderRadius: _borderRadius,\n              boxShadow: [\n                BoxShadow(\n                  offset: const Offset(0, 8),\n                  blurRadius: 16,\n                  spreadRadius: 8,\n                  color: isLightMode\n                      ? const Color(0x0F1F2329)\n                      : Theme.of(context).shadowColor.withValues(alpha: 0.06),\n                ),\n                BoxShadow(\n                  offset: const Offset(0, 4),\n                  blurRadius: 8,\n                  color: isLightMode\n                      ? const Color(0x141F2329)\n                      : Theme.of(context).shadowColor.withValues(alpha: 0.08),\n                ),\n                BoxShadow(\n                  offset: const Offset(0, 2),\n                  blurRadius: 4,\n                  color: isLightMode\n                      ? const Color(0x1F1F2329)\n                      : Theme.of(context).shadowColor.withValues(alpha: 0.12),\n                ),\n              ],\n            ),\n            child: Material(\n              borderRadius: _borderRadius,\n              color: Colors.transparent,\n              borderOnForeground: false,\n              child: InkWell(\n                overlayColor: WidgetStateProperty.all(\n                  AFThemeExtension.of(context).lightGreyHover,\n                ),\n                borderRadius: _borderRadius,\n                onTap: onPressed,\n                child: const SizedBox.square(\n                  dimension: 32,\n                  child: Center(\n                    child: FlowySvg(\n                      FlowySvgs.ai_scroll_to_bottom_s,\n                      size: Size.square(20),\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/widgets/message_height_calculator.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/scheduler.dart';\n\n/// Callback type for height measurement\ntypedef HeightMeasuredCallback = void Function(String messageId, double height);\n\n/// Widget that measures and caches message heights with proper lifecycle management\nclass MessageHeightCalculator extends StatefulWidget {\n  const MessageHeightCalculator({\n    super.key,\n    required this.messageId,\n    required this.child,\n    this.onHeightMeasured,\n  });\n\n  final String messageId;\n  final Widget child;\n  final HeightMeasuredCallback? onHeightMeasured;\n\n  @override\n  State<MessageHeightCalculator> createState() =>\n      _MessageHeightCalculatorState();\n}\n\nclass _MessageHeightCalculatorState extends State<MessageHeightCalculator>\n    with WidgetsBindingObserver {\n  final GlobalKey measureKey = GlobalKey();\n\n  double? lastMeasuredHeight;\n  bool isMeasuring = false;\n  int measurementAttempts = 0;\n  static const int maxMeasurementAttempts = 3;\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addObserver(this);\n    _scheduleMeasurement();\n  }\n\n  @override\n  void didUpdateWidget(MessageHeightCalculator oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    if (oldWidget.messageId != widget.messageId) {\n      _resetMeasurement();\n      _scheduleMeasurement();\n    }\n  }\n\n  @override\n  void dispose() {\n    WidgetsBinding.instance.removeObserver(this);\n    super.dispose();\n  }\n\n  @override\n  void didChangeMetrics() {\n    if (mounted) {\n      _scheduleMeasurement();\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return KeyedSubtree(\n      key: measureKey,\n      child: widget.child,\n    );\n  }\n\n  void _resetMeasurement() {\n    lastMeasuredHeight = null;\n    isMeasuring = false;\n    measurementAttempts = 0;\n  }\n\n  void _scheduleMeasurement() {\n    if (isMeasuring || !mounted) return;\n\n    isMeasuring = true;\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      _measureHeight();\n    });\n  }\n\n  void _measureHeight() {\n    if (!mounted || measurementAttempts >= maxMeasurementAttempts) {\n      isMeasuring = false;\n      return;\n    }\n\n    measurementAttempts++;\n\n    try {\n      final renderBox =\n          measureKey.currentContext?.findRenderObject() as RenderBox?;\n\n      if (renderBox == null || !renderBox.hasSize) {\n        // Retry measurement in next frame if render box is not ready\n        if (measurementAttempts < maxMeasurementAttempts) {\n          SchedulerBinding.instance.addPostFrameCallback((_) {\n            if (mounted) _measureHeight();\n          });\n        } else {\n          isMeasuring = false;\n        }\n        return;\n      }\n\n      final height = renderBox.size.height;\n\n      if (lastMeasuredHeight == null ||\n          (height - (lastMeasuredHeight ?? 0)).abs() > 1.0) {\n        lastMeasuredHeight = height;\n\n        widget.onHeightMeasured?.call(widget.messageId, height);\n      }\n\n      isMeasuring = false;\n    } catch (e) {\n      isMeasuring = false;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/base/color/color_picker.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass FlowyMobileColorPicker extends StatelessWidget {\n  const FlowyMobileColorPicker({\n    super.key,\n    required this.onSelectedColor,\n  });\n\n  final void Function(FlowyColorOption? option) onSelectedColor;\n\n  @override\n  Widget build(BuildContext context) {\n    const defaultColor = Colors.transparent;\n    final colors = [\n      // reset to default background color\n      FlowyColorOption(\n        color: defaultColor,\n        i18n: LocaleKeys.document_plugins_optionAction_defaultColor.tr(),\n        id: optionActionColorDefaultColor,\n      ),\n      ...FlowyTint.values.map(\n        (e) => FlowyColorOption(\n          color: e.color(context),\n          i18n: e.tintName(AppFlowyEditorL10n.current),\n          id: e.id,\n        ),\n      ),\n    ];\n    return ListView.separated(\n      itemBuilder: (context, index) {\n        final color = colors[index];\n        return SizedBox(\n          height: 56,\n          child: FlowyButton(\n            useIntrinsicWidth: true,\n            text: FlowyText(\n              color.i18n,\n            ),\n            leftIcon: _ColorIcon(\n              color: color.color,\n            ),\n            leftIconSize: const Size.square(36.0),\n            iconPadding: 12.0,\n            margin: const EdgeInsets.symmetric(\n              horizontal: 12.0,\n              vertical: 16.0,\n            ),\n            onTap: () => onSelectedColor(color),\n          ),\n        );\n      },\n      separatorBuilder: (_, __) => const Divider(\n        height: 1,\n      ),\n      itemCount: colors.length,\n    );\n  }\n}\n\nclass _ColorIcon extends StatelessWidget {\n  const _ColorIcon({required this.color});\n\n  final Color color;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox.square(\n      dimension: 24,\n      child: DecoratedBox(\n        decoration: BoxDecoration(\n          color: color,\n          shape: BoxShape.circle,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/base/color/color_picker_screen.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/plugins/base/color/color_picker.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nclass MobileColorPickerScreen extends StatelessWidget {\n  const MobileColorPickerScreen({super.key, this.title});\n\n  final String? title;\n\n  static const routeName = '/color_picker';\n  static const pageTitle = 'title';\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: FlowyAppBar(\n        titleText: title ?? LocaleKeys.titleBar_pageIcon.tr(),\n      ),\n      body: SafeArea(\n        child: FlowyMobileColorPicker(\n          onSelectedColor: (option) => context.pop(option),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/base/drag_handler.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass DragHandle extends StatelessWidget {\n  const DragHandle({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: 4,\n      width: 40,\n      margin: const EdgeInsets.symmetric(vertical: 6),\n      decoration: BoxDecoration(\n        color: Colors.grey.shade400,\n        borderRadius: BorderRadius.circular(2),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart",
    "content": "import 'dart:math';\n\nimport 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/emoji_search_bar.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_emoji_mart/flutter_emoji_mart.dart';\n\n// use a global value to store the selected emoji to prevent reloading every time.\nEmojiData? kCachedEmojiData;\nconst _kRecentEmojiCategoryId = 'Recent';\n\nclass EmojiPickerResult {\n  EmojiPickerResult({\n    required this.emojiId,\n    required this.emoji,\n    this.isRandom = false,\n  });\n\n  final String emojiId;\n  final String emoji;\n  final bool isRandom;\n}\n\nclass FlowyEmojiPicker extends StatefulWidget {\n  const FlowyEmojiPicker({\n    super.key,\n    required this.onEmojiSelected,\n    this.emojiPerLine = 9,\n    this.ensureFocus = false,\n  });\n\n  final ValueChanged<EmojiPickerResult> onEmojiSelected;\n  final int emojiPerLine;\n  final bool ensureFocus;\n\n  @override\n  State<FlowyEmojiPicker> createState() => _FlowyEmojiPickerState();\n}\n\nclass _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {\n  late EmojiData emojiData;\n  bool loaded = false;\n\n  @override\n  void initState() {\n    super.initState();\n\n    // load the emoji data from cache if it's available\n    if (kCachedEmojiData != null) {\n      loadEmojis(kCachedEmojiData!);\n    } else {\n      EmojiData.builtIn().then(\n        (value) {\n          kCachedEmojiData = value;\n          loadEmojis(value);\n        },\n      );\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (!loaded) {\n      return const Center(\n        child: SizedBox.square(\n          dimension: 24.0,\n          child: CircularProgressIndicator(\n            strokeWidth: 2.0,\n          ),\n        ),\n      );\n    }\n\n    return EmojiPicker(\n      emojiData: emojiData,\n      configuration: EmojiPickerConfiguration(\n        showTabs: false,\n        defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none,\n      ),\n      onEmojiSelected: (id, emoji) {\n        widget.onEmojiSelected.call(\n          EmojiPickerResult(emojiId: id, emoji: emoji),\n        );\n        RecentIcons.putEmoji(id);\n      },\n      padding: const EdgeInsets.symmetric(horizontal: 16.0),\n      headerBuilder: (_, category) => FlowyEmojiHeader(category: category),\n      itemBuilder: (context, emojiId, emoji, callback) {\n        final name = emojiData.emojis[emojiId]?.name ?? '';\n        return SizedBox.square(\n          dimension: 36.0,\n          child: FlowyButton(\n            margin: EdgeInsets.zero,\n            radius: Corners.s8Border,\n            text: FlowyTooltip(\n              message: name,\n              preferBelow: false,\n              child: FlowyText.emoji(\n                emoji,\n                fontSize: 24.0,\n              ),\n            ),\n            onTap: () => callback(emojiId, emoji),\n          ),\n        );\n      },\n      searchBarBuilder: (context, keyword, skinTone) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 16.0),\n          child: FlowyEmojiSearchBar(\n            emojiData: emojiData,\n            ensureFocus: widget.ensureFocus,\n            onKeywordChanged: (value) {\n              keyword.value = value;\n            },\n            onSkinToneChanged: (value) {\n              skinTone.value = value;\n            },\n            onRandomEmojiSelected: (id, emoji) {\n              widget.onEmojiSelected.call(\n                EmojiPickerResult(emojiId: id, emoji: emoji, isRandom: true),\n              );\n              RecentIcons.putEmoji(id);\n            },\n          ),\n        );\n      },\n    );\n  }\n\n  void loadEmojis(EmojiData data) {\n    RecentIcons.getEmojiIds().then((v) {\n      if (v.isEmpty) {\n        emojiData = data;\n        if (mounted) setState(() => loaded = true);\n        return;\n      }\n      final categories = List.of(data.categories);\n      categories.insert(\n        0,\n        Category(\n          id: _kRecentEmojiCategoryId,\n          emojiIds: v.sublist(0, min(widget.emojiPerLine, v.length)),\n        ),\n      );\n      emojiData = EmojiData(categories: categories, emojis: data.emojis);\n      if (mounted) setState(() => loaded = true);\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_emoji_mart/flutter_emoji_mart.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass FlowyEmojiHeader extends StatelessWidget {\n  const FlowyEmojiHeader({\n    super.key,\n    required this.category,\n  });\n\n  final Category category;\n\n  @override\n  Widget build(BuildContext context) {\n    if (UniversalPlatform.isDesktop) {\n      return Container(\n        height: 22,\n        color: Theme.of(context).cardColor,\n        child: Padding(\n          padding: const EdgeInsets.only(bottom: 4.0),\n          child: FlowyText.regular(\n            category.id.capitalize(),\n            color: Theme.of(context).hintColor,\n          ),\n        ),\n      );\n    } else {\n      return Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Container(\n            height: 40,\n            width: double.infinity,\n            padding: const EdgeInsets.symmetric(horizontal: 8.0),\n            color: Theme.of(context).cardColor,\n            child: Padding(\n              padding: const EdgeInsets.only(\n                top: 14.0,\n                bottom: 4.0,\n              ),\n              child: FlowyText.regular(category.id),\n            ),\n          ),\n          const Divider(\n            height: 1,\n            thickness: 1,\n          ),\n        ],\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart",
    "content": "import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nimport '../../../generated/locale_keys.g.dart';\nimport '../../../mobile/presentation/base/app_bar/app_bar.dart';\nimport '../../../shared/icon_emoji_picker/tab.dart';\n\nclass MobileEmojiPickerScreen extends StatelessWidget {\n  const MobileEmojiPickerScreen({\n    super.key,\n    this.title,\n    this.selectedType,\n    this.documentId,\n    this.tabs = const [PickerTabType.emoji, PickerTabType.icon],\n  });\n\n  final PickerTabType? selectedType;\n  final String? title;\n  final String? documentId;\n  final List<PickerTabType> tabs;\n\n  static const routeName = '/emoji_picker';\n  static const pageTitle = 'title';\n  static const iconSelectedType = 'iconSelected_type';\n  static const selectTabs = 'tabs';\n  static const uploadDocumentId = 'document_id';\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: FlowyAppBar(\n        titleText: title ?? LocaleKeys.titleBar_pageIcon.tr(),\n      ),\n      body: SafeArea(\n        child: FlowyIconEmojiPicker(\n          tabs: tabs,\n          documentId: documentId,\n          initialType: selectedType,\n          onSelectedEmoji: (r) {\n            context.pop<EmojiIconData>(r.data);\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart",
    "content": "import 'dart:io';\n\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:google_fonts/google_fonts.dart';\n\n// used to prevent loading font from google fonts every time\nList<String>? _cachedFallbackFontFamily;\n\n// Some emojis are not supported by the default font on Android or Linux, fallback to noto color emoji\nclass EmojiText extends StatelessWidget {\n  const EmojiText({\n    super.key,\n    required this.emoji,\n    required this.fontSize,\n    this.textAlign,\n    this.lineHeight,\n  });\n\n  final String emoji;\n  final double fontSize;\n  final TextAlign? textAlign;\n  final double? lineHeight;\n\n  @override\n  Widget build(BuildContext context) {\n    _loadFallbackFontFamily();\n    return FlowyText(\n      emoji,\n      fontSize: fontSize,\n      textAlign: textAlign,\n      strutStyle: const StrutStyle(forceStrutHeight: true),\n      fallbackFontFamily: _cachedFallbackFontFamily,\n      lineHeight: lineHeight,\n      isEmoji: true,\n    );\n  }\n\n  void _loadFallbackFontFamily() {\n    if (Platform.isLinux) {\n      final notoColorEmoji = GoogleFonts.notoColorEmoji().fontFamily;\n      if (notoColorEmoji != null) {\n        _cachedFallbackFontFamily = [notoColorEmoji];\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/base/icon/icon_widget.dart",
    "content": "import 'package:appflowy/plugins/base/emoji/emoji_text.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:flutter/material.dart';\n\nimport '../../../generated/flowy_svgs.g.dart';\n\nclass IconWidget extends StatelessWidget {\n  const IconWidget({super.key, required this.size, required this.iconsData});\n\n  final IconsData iconsData;\n  final double size;\n\n  @override\n  Widget build(BuildContext context) {\n    final colorValue = int.tryParse(iconsData.color ?? '');\n    Color? color;\n    if (colorValue != null) {\n      color = Color(colorValue);\n    }\n    final svgString = iconsData.svgString;\n    if (svgString == null) {\n      return EmojiText(\n        emoji: '❓',\n        fontSize: size,\n        textAlign: TextAlign.center,\n      );\n    }\n    return FlowySvg.string(\n      svgString,\n      size: Size.square(size),\n      color: color,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/blank/blank.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/workspace/presentation/home/home_stack.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\n\nclass BlankPluginBuilder extends PluginBuilder {\n  @override\n  Plugin build(dynamic data) {\n    return BlankPagePlugin();\n  }\n\n  @override\n  String get menuName => \"Blank\";\n\n  @override\n  FlowySvgData get icon => const FlowySvgData('');\n\n  @override\n  PluginType get pluginType => PluginType.blank;\n\n  @override\n  ViewLayoutPB get layoutType => ViewLayoutPB.Document;\n}\n\nclass BlankPluginConfig implements PluginConfig {\n  @override\n  bool get creatable => false;\n}\n\nclass BlankPagePlugin extends Plugin {\n  @override\n  PluginWidgetBuilder get widgetBuilder => BlankPagePluginWidgetBuilder();\n\n  @override\n  PluginId get id => \"\";\n\n  @override\n  PluginType get pluginType => PluginType.blank;\n}\n\nclass BlankPagePluginWidgetBuilder extends PluginWidgetBuilder\n    with NavigationItem {\n  @override\n  String? get viewName => LocaleKeys.blankPageTitle.tr();\n\n  @override\n  Widget get leftBarItem => FlowyText.medium(LocaleKeys.blankPageTitle.tr());\n\n  @override\n  Widget tabBarItem(String pluginId, [bool shortForm = false]) => leftBarItem;\n\n  @override\n  Widget buildWidget({\n    required PluginContext context,\n    required bool shrinkWrap,\n    Map<String, dynamic>? data,\n  }) =>\n      const BlankPage();\n\n  @override\n  List<NavigationItem> get navigationItems => [this];\n}\n\nclass BlankPage extends StatefulWidget {\n  const BlankPage({super.key});\n\n  @override\n  State<BlankPage> createState() => _BlankPageState();\n}\n\nclass _BlankPageState extends State<BlankPage> {\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox.expand(\n      child: Container(\n        color: Theme.of(context).colorScheme.surface,\n        child: const Padding(\n          padding: EdgeInsets.all(10),\n          child: SizedBox.shrink(),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculation_type_ext.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nextension CalcTypeLabel on CalculationType {\n  String get label => switch (this) {\n        CalculationType.Average =>\n          LocaleKeys.grid_calculationTypeLabel_average.tr(),\n        CalculationType.Max => LocaleKeys.grid_calculationTypeLabel_max.tr(),\n        CalculationType.Median =>\n          LocaleKeys.grid_calculationTypeLabel_median.tr(),\n        CalculationType.Min => LocaleKeys.grid_calculationTypeLabel_min.tr(),\n        CalculationType.Sum => LocaleKeys.grid_calculationTypeLabel_sum.tr(),\n        CalculationType.Count =>\n          LocaleKeys.grid_calculationTypeLabel_count.tr(),\n        CalculationType.CountEmpty =>\n          LocaleKeys.grid_calculationTypeLabel_countEmpty.tr(),\n        CalculationType.CountNonEmpty =>\n          LocaleKeys.grid_calculationTypeLabel_countNonEmpty.tr(),\n        _ => throw UnimplementedError(\n            'Label for $this has not been implemented',\n          ),\n      };\n\n  String get shortLabel => switch (this) {\n        CalculationType.CountEmpty =>\n          LocaleKeys.grid_calculationTypeLabel_countEmptyShort.tr(),\n        CalculationType.CountNonEmpty =>\n          LocaleKeys.grid_calculationTypeLabel_countNonEmptyShort.tr(),\n        _ => label,\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculations_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/grid_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flowy_infra/notifier.dart';\n\ntypedef UpdateCalculationValue\n    = FlowyResult<CalculationChangesetNotificationPB, FlowyError>;\n\nclass CalculationsListener {\n  CalculationsListener({required this.viewId});\n\n  final String viewId;\n\n  PublishNotifier<UpdateCalculationValue>? _calculationNotifier =\n      PublishNotifier();\n  DatabaseNotificationListener? _listener;\n\n  void start({\n    required void Function(UpdateCalculationValue) onCalculationChanged,\n  }) {\n    _calculationNotifier?.addPublishListener(onCalculationChanged);\n    _listener = DatabaseNotificationListener(\n      objectId: viewId,\n      handler: _handler,\n    );\n  }\n\n  void _handler(\n    DatabaseNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case DatabaseNotification.DidUpdateCalculation:\n        _calculationNotifier?.value = result.fold(\n          (payload) => FlowyResult.success(\n            CalculationChangesetNotificationPB.fromBuffer(payload),\n          ),\n          (err) => FlowyResult.failure(err),\n        );\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _listener?.stop();\n    _calculationNotifier?.dispose();\n    _calculationNotifier = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculations_service.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass CalculationsBackendService {\n  const CalculationsBackendService({required this.viewId});\n\n  final String viewId;\n\n  // Get Calculations (initial fetch)\n\n  Future<FlowyResult<RepeatedCalculationsPB, FlowyError>>\n      getCalculations() async {\n    final payload = DatabaseViewIdPB()..value = viewId;\n\n    return DatabaseEventGetAllCalculations(payload).send();\n  }\n\n  Future<void> updateCalculation(\n    String fieldId,\n    CalculationType type, {\n    String? calculationId,\n  }) async {\n    final payload = UpdateCalculationChangesetPB()\n      ..viewId = viewId\n      ..fieldId = fieldId\n      ..calculationType = type;\n\n    if (calculationId != null) {\n      payload.calculationId = calculationId;\n    }\n\n    await DatabaseEventUpdateCalculation(payload).send();\n  }\n\n  Future<void> removeCalculation(\n    String fieldId,\n    String calculationId,\n  ) async {\n    final payload = RemoveCalculationChangesetPB()\n      ..viewId = viewId\n      ..fieldId = fieldId\n      ..calculationId = calculationId;\n\n    await DatabaseEventRemoveCalculation(payload).send();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'checkbox_cell_bloc.freezed.dart';\n\nclass CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {\n  CheckboxCellBloc({\n    required this.cellController,\n  }) : super(CheckboxCellState.initial(cellController)) {\n    _dispatch();\n  }\n\n  final CheckboxCellController cellController;\n  void Function()? _onCellChangedFn;\n\n  @override\n  Future<void> close() async {\n    if (_onCellChangedFn != null) {\n      cellController.removeListener(\n        onCellChanged: _onCellChangedFn!,\n        onFieldChanged: _onFieldChangedListener,\n      );\n    }\n    await cellController.dispose();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<CheckboxCellEvent>(\n      (event, emit) {\n        event.when(\n          initial: () => _startListening(),\n          didUpdateCell: (isSelected) {\n            emit(state.copyWith(isSelected: isSelected));\n          },\n          didUpdateField: (fieldName) {\n            emit(state.copyWith(fieldName: fieldName));\n          },\n          select: () {\n            cellController.saveCellData(state.isSelected ? \"No\" : \"Yes\");\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _onCellChangedFn = cellController.addListener(\n      onCellChanged: (cellData) {\n        if (!isClosed) {\n          add(CheckboxCellEvent.didUpdateCell(_isSelected(cellData)));\n        }\n      },\n      onFieldChanged: _onFieldChangedListener,\n    );\n  }\n\n  void _onFieldChangedListener(FieldInfo fieldInfo) {\n    if (!isClosed) {\n      add(CheckboxCellEvent.didUpdateField(fieldInfo.name));\n    }\n  }\n}\n\n@freezed\nclass CheckboxCellEvent with _$CheckboxCellEvent {\n  const factory CheckboxCellEvent.initial() = _Initial;\n  const factory CheckboxCellEvent.select() = _Selected;\n  const factory CheckboxCellEvent.didUpdateCell(bool isSelected) =\n      _DidUpdateCell;\n  const factory CheckboxCellEvent.didUpdateField(String fieldName) =\n      _DidUpdateField;\n}\n\n@freezed\nclass CheckboxCellState with _$CheckboxCellState {\n  const factory CheckboxCellState({\n    required bool isSelected,\n    required String fieldName,\n  }) = _CheckboxCellState;\n\n  factory CheckboxCellState.initial(CheckboxCellController cellController) {\n    return CheckboxCellState(\n      isSelected: _isSelected(cellController.getCellData()),\n      fieldName: cellController.fieldInfo.field.name,\n    );\n  }\n}\n\nbool _isSelected(CheckboxCellDataPB? cellData) {\n  return cellData != null && cellData.isChecked;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checklist_cell_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/domain/checklist_cell_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'checklist_cell_bloc.freezed.dart';\n\nclass ChecklistSelectOption {\n  ChecklistSelectOption({required this.isSelected, required this.data});\n\n  final bool isSelected;\n  final SelectOptionPB data;\n}\n\nclass ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {\n  ChecklistCellBloc({required this.cellController})\n      : _checklistCellService = ChecklistCellBackendService(\n          viewId: cellController.viewId,\n          fieldId: cellController.fieldId,\n          rowId: cellController.rowId,\n        ),\n        super(ChecklistCellState.initial(cellController)) {\n    _dispatch();\n    _startListening();\n  }\n\n  final ChecklistCellController cellController;\n  final ChecklistCellBackendService _checklistCellService;\n  void Function()? _onCellChangedFn;\n\n  int? nextPhantomIndex;\n\n  @override\n  Future<void> close() async {\n    if (_onCellChangedFn != null) {\n      cellController.removeListener(onCellChanged: _onCellChangedFn!);\n    }\n    await cellController.dispose();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<ChecklistCellEvent>(\n      (event, emit) async {\n        await event.when(\n          didUpdateCell: (data) {\n            if (data == null) {\n              emit(\n                const ChecklistCellState(\n                  tasks: [],\n                  percent: 0,\n                  showIncompleteOnly: false,\n                  phantomIndex: null,\n                ),\n              );\n              return;\n            }\n            final phantomIndex = state.phantomIndex != null\n                ? nextPhantomIndex ?? state.phantomIndex\n                : null;\n            emit(\n              state.copyWith(\n                tasks: _makeChecklistSelectOptions(data),\n                percent: data.percentage,\n                phantomIndex: phantomIndex,\n              ),\n            );\n            nextPhantomIndex = null;\n          },\n          updateTaskName: (option, name) {\n            _updateOption(option, name);\n          },\n          selectTask: (id) async {\n            await _checklistCellService.select(optionId: id);\n          },\n          createNewTask: (name, index) async {\n            await _createTask(name, index);\n          },\n          deleteTask: (id) async {\n            await _deleteOption([id]);\n          },\n          reorderTask: (fromIndex, toIndex) async {\n            await _reorderTask(fromIndex, toIndex, emit);\n          },\n          toggleShowIncompleteOnly: () {\n            emit(state.copyWith(showIncompleteOnly: !state.showIncompleteOnly));\n          },\n          updatePhantomIndex: (index) {\n            emit(\n              ChecklistCellState(\n                tasks: state.tasks,\n                percent: state.percent,\n                showIncompleteOnly: state.showIncompleteOnly,\n                phantomIndex: index,\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _onCellChangedFn = cellController.addListener(\n      onCellChanged: (data) {\n        if (!isClosed) {\n          add(ChecklistCellEvent.didUpdateCell(data));\n        }\n      },\n    );\n  }\n\n  Future<void> _createTask(String name, int? index) async {\n    nextPhantomIndex = index == null ? state.tasks.length + 1 : index + 1;\n\n    int? actualIndex = index;\n    if (index != null && state.showIncompleteOnly) {\n      int notSelectedTaskCount = 0;\n      for (int i = 0; i < state.tasks.length; i++) {\n        if (!state.tasks[i].isSelected) {\n          notSelectedTaskCount++;\n        }\n\n        if (notSelectedTaskCount == index) {\n          actualIndex = i + 1;\n          break;\n        }\n      }\n    }\n\n    final result = await _checklistCellService.create(\n      name: name,\n      index: actualIndex,\n    );\n    result.fold((l) {}, (err) => Log.error(err));\n  }\n\n  void _updateOption(SelectOptionPB option, String name) async {\n    final result =\n        await _checklistCellService.updateName(option: option, name: name);\n    result.fold((l) => null, (err) => Log.error(err));\n  }\n\n  Future<void> _deleteOption(List<String> options) async {\n    final result = await _checklistCellService.delete(optionIds: options);\n    result.fold((l) => null, (err) => Log.error(err));\n  }\n\n  Future<void> _reorderTask(\n    int fromIndex,\n    int toIndex,\n    Emitter<ChecklistCellState> emit,\n  ) async {\n    if (fromIndex < toIndex) {\n      toIndex--;\n    }\n\n    final tasks = state.showIncompleteOnly\n        ? state.tasks.where((task) => !task.isSelected).toList()\n        : state.tasks;\n\n    final fromId = tasks[fromIndex].data.id;\n    final toId = tasks[toIndex].data.id;\n\n    final newTasks = [...state.tasks];\n    newTasks.insert(toIndex, newTasks.removeAt(fromIndex));\n    emit(state.copyWith(tasks: newTasks));\n    final result = await _checklistCellService.reorder(\n      fromTaskId: fromId,\n      toTaskId: toId,\n    );\n    result.fold((l) => null, (err) => Log.error(err));\n  }\n}\n\n@freezed\nclass ChecklistCellEvent with _$ChecklistCellEvent {\n  const factory ChecklistCellEvent.didUpdateCell(\n    ChecklistCellDataPB? data,\n  ) = _DidUpdateCell;\n  const factory ChecklistCellEvent.updateTaskName(\n    SelectOptionPB option,\n    String name,\n  ) = _UpdateTaskName;\n  const factory ChecklistCellEvent.selectTask(String taskId) = _SelectTask;\n  const factory ChecklistCellEvent.createNewTask(\n    String description, {\n    int? index,\n  }) = _CreateNewTask;\n  const factory ChecklistCellEvent.deleteTask(String taskId) = _DeleteTask;\n  const factory ChecklistCellEvent.reorderTask(int fromIndex, int toIndex) =\n      _ReorderTask;\n\n  const factory ChecklistCellEvent.toggleShowIncompleteOnly() = _IncompleteOnly;\n  const factory ChecklistCellEvent.updatePhantomIndex(int? index) =\n      _UpdatePhantomIndex;\n}\n\n@freezed\nclass ChecklistCellState with _$ChecklistCellState {\n  const factory ChecklistCellState({\n    required List<ChecklistSelectOption> tasks,\n    required double percent,\n    required bool showIncompleteOnly,\n    required int? phantomIndex,\n  }) = _ChecklistCellState;\n\n  factory ChecklistCellState.initial(ChecklistCellController cellController) {\n    final cellData = cellController.getCellData(loadIfNotExist: true);\n\n    return ChecklistCellState(\n      tasks: _makeChecklistSelectOptions(cellData),\n      percent: cellData?.percentage ?? 0,\n      showIncompleteOnly: false,\n      phantomIndex: null,\n    );\n  }\n}\n\nList<ChecklistSelectOption> _makeChecklistSelectOptions(\n  ChecklistCellDataPB? data,\n) {\n  if (data == null) {\n    return [];\n  }\n  return data.options\n      .map(\n        (option) => ChecklistSelectOption(\n          isSelected: data.selectedOptions.any(\n            (selected) => selected.id == option.id,\n          ),\n          data: option,\n        ),\n      )\n      .toList();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_bloc.dart",
    "content": "import 'dart:async';\nimport 'dart:ui';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport 'date_cell_editor_bloc.dart';\n\npart 'date_cell_bloc.freezed.dart';\n\nclass DateCellBloc extends Bloc<DateCellEvent, DateCellState> {\n  DateCellBloc({required this.cellController})\n      : super(DateCellState.initial(cellController)) {\n    _dispatch();\n    _startListening();\n  }\n\n  final DateCellController cellController;\n  VoidCallback? _onCellChangedFn;\n\n  @override\n  Future<void> close() async {\n    if (_onCellChangedFn != null) {\n      cellController.removeListener(\n        onCellChanged: _onCellChangedFn!,\n        onFieldChanged: _onFieldChangedListener,\n      );\n    }\n    await cellController.dispose();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<DateCellEvent>(\n      (event, emit) async {\n        event.when(\n          didReceiveCellUpdate: (DateCellDataPB? cellData) {\n            final dateCellData = DateCellData.fromPB(cellData);\n            emit(\n              state.copyWith(\n                cellData: dateCellData,\n              ),\n            );\n          },\n          didUpdateField: (fieldInfo) {\n            emit(\n              state.copyWith(\n                fieldInfo: fieldInfo,\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _onCellChangedFn = cellController.addListener(\n      onCellChanged: (data) {\n        if (!isClosed) {\n          add(DateCellEvent.didReceiveCellUpdate(data));\n        }\n      },\n      onFieldChanged: _onFieldChangedListener,\n    );\n  }\n\n  void _onFieldChangedListener(FieldInfo fieldInfo) {\n    if (!isClosed) {\n      add(DateCellEvent.didUpdateField(fieldInfo));\n    }\n  }\n}\n\n@freezed\nclass DateCellEvent with _$DateCellEvent {\n  const factory DateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) =\n      _DidReceiveCellUpdate;\n  const factory DateCellEvent.didUpdateField(FieldInfo fieldInfo) =\n      _DidUpdateField;\n}\n\n@freezed\nclass DateCellState with _$DateCellState {\n  const factory DateCellState({\n    required FieldInfo fieldInfo,\n    required DateCellData cellData,\n  }) = _DateCellState;\n\n  factory DateCellState.initial(DateCellController cellController) {\n    final cellData = DateCellData.fromPB(cellController.getCellData());\n\n    return DateCellState(\n      fieldInfo: cellController.fieldInfo,\n      cellData: cellData,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';\nimport 'package:appflowy/plugins/database/domain/date_cell_service.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/user/application/reminder/reminder_extension.dart';\nimport 'package:appflowy/util/int64_extension.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:calendar_view/calendar_view.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart'\n    show StringTranslateExtension;\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:nanoid/non_secure.dart';\nimport 'package:protobuf/protobuf.dart' hide FieldInfo;\n\npart 'date_cell_editor_bloc.freezed.dart';\n\nclass DateCellEditorBloc\n    extends Bloc<DateCellEditorEvent, DateCellEditorState> {\n  DateCellEditorBloc({\n    required this.cellController,\n    required ReminderBloc reminderBloc,\n  })  : _reminderBloc = reminderBloc,\n        _dateCellBackendService = DateCellBackendService(\n          viewId: cellController.viewId,\n          fieldId: cellController.fieldId,\n          rowId: cellController.rowId,\n        ),\n        super(DateCellEditorState.initial(cellController, reminderBloc)) {\n    _dispatch();\n    _startListening();\n  }\n\n  final DateCellBackendService _dateCellBackendService;\n  final DateCellController cellController;\n  final ReminderBloc _reminderBloc;\n\n  void Function()? _onCellChangedFn;\n\n  void _dispatch() {\n    on<DateCellEditorEvent>(\n      (event, emit) async {\n        await event.when(\n          didReceiveCellUpdate: (DateCellDataPB? cellData) {\n            final dateCellData = DateCellData.fromPB(cellData);\n\n            ReminderOption reminderOption = state.reminderOption;\n\n            if (dateCellData.reminderId.isNotEmpty &&\n                dateCellData.dateTime != null &&\n                reminderOption != ReminderOption.none) {\n              final reminder = _reminderBloc.state.reminders\n                  .firstWhereOrNull((r) => r.id == dateCellData.reminderId);\n              if (reminder != null) {\n                reminderOption = ReminderOption.fromDateDifference(\n                  dateCellData.dateTime!,\n                  reminder.scheduledAt.toDateTime(),\n                );\n              }\n            }\n\n            emit(\n              state.copyWith(\n                dateTime: dateCellData.dateTime,\n                endDateTime: dateCellData.endDateTime,\n                includeTime: dateCellData.includeTime,\n                isRange: dateCellData.isRange,\n                reminderId: dateCellData.reminderId,\n                reminderOption: reminderOption,\n              ),\n            );\n          },\n          didUpdateField: (field) {\n            final typeOption = DateTypeOptionDataParser()\n                .fromBuffer(field.field.typeOptionData);\n            emit(state.copyWith(dateTypeOptionPB: typeOption));\n          },\n          updateDateTime: (date) async {\n            if (state.isRange) {\n              return;\n            }\n            await _updateDateData(date: date);\n          },\n          updateDateRange: (DateTime start, DateTime end) async {\n            if (!state.isRange) {\n              return;\n            }\n            await _updateDateData(date: start, endDate: end);\n          },\n          setIncludeTime: (includeTime, dateTime, endDateTime) async {\n            await _updateIncludeTime(includeTime, dateTime, endDateTime);\n          },\n          setIsRange: (isRange, dateTime, endDateTime) async {\n            await _updateIsRange(isRange, dateTime, endDateTime);\n          },\n          setDateFormat: (DateFormatPB dateFormat) async {\n            await _updateTypeOption(emit, dateFormat: dateFormat);\n          },\n          setTimeFormat: (TimeFormatPB timeFormat) async {\n            await _updateTypeOption(emit, timeFormat: timeFormat);\n          },\n          clearDate: () async {\n            // Remove reminder if neccessary\n            if (state.reminderId.isNotEmpty) {\n              _reminderBloc.add(\n                ReminderEvent.removeReminder(reminderId: state.reminderId),\n              );\n            }\n\n            await _clearDate();\n          },\n          setReminderOption: (ReminderOption option) async {\n            emit(state.copyWith(reminderOption: option));\n            await _setReminderOption(option);\n          },\n        );\n      },\n    );\n  }\n\n  Future<FlowyResult<void, FlowyError>> _updateDateData({\n    DateTime? date,\n    DateTime? endDate,\n    bool updateReminderIfNecessary = true,\n  }) async {\n    final result = await _dateCellBackendService.update(\n      date: date,\n      endDate: endDate,\n    );\n    if (updateReminderIfNecessary) {\n      result.onSuccess((_) => _updateReminderIfNecessary(date));\n    }\n    return result;\n  }\n\n  Future<void> _updateIsRange(\n    bool isRange,\n    DateTime? dateTime,\n    DateTime? endDateTime,\n  ) {\n    return _dateCellBackendService\n        .update(\n          date: dateTime,\n          endDate: endDateTime,\n          isRange: isRange,\n        )\n        .fold((s) => _updateReminderIfNecessary(dateTime), Log.error);\n  }\n\n  Future<void> _updateIncludeTime(\n    bool includeTime,\n    DateTime? dateTime,\n    DateTime? endDateTime,\n  ) {\n    return _dateCellBackendService\n        .update(\n          date: dateTime,\n          endDate: endDateTime,\n          includeTime: includeTime,\n        )\n        .fold((s) => _updateReminderIfNecessary(dateTime), Log.error);\n  }\n\n  Future<void> _clearDate() async {\n    final result = await _dateCellBackendService.clear();\n    result.onFailure(Log.error);\n  }\n\n  Future<void> _setReminderOption(ReminderOption option) async {\n    if (state.reminderId.isEmpty) {\n      if (option == ReminderOption.none) {\n        // do nothing\n        return;\n      }\n      // if no date, fill it first\n      final fillerDateTime =\n          state.includeTime ? DateTime.now() : DateTime.now().withoutTime;\n      if (state.dateTime == null) {\n        final result = await _updateDateData(\n          date: fillerDateTime,\n          endDate: state.isRange ? fillerDateTime : null,\n          updateReminderIfNecessary: false,\n        );\n        // return if filling date is unsuccessful\n        if (result.isFailure) {\n          return;\n        }\n      }\n\n      // create a reminder\n      final reminderId = nanoid();\n      await _updateCellReminderId(reminderId);\n      final dateTime = state.dateTime ?? fillerDateTime;\n      _reminderBloc.add(\n        ReminderEvent.addById(\n          reminderId: reminderId,\n          objectId: cellController.viewId,\n          meta: {\n            ReminderMetaKeys.includeTime: state.includeTime.toString(),\n            ReminderMetaKeys.rowId: cellController.rowId,\n          },\n          scheduledAt: Int64(\n            option.getNotificationDateTime(dateTime).millisecondsSinceEpoch ~/\n                1000,\n          ),\n        ),\n      );\n    } else {\n      if (option == ReminderOption.none) {\n        // remove reminder from reminder bloc and cell data\n        _reminderBloc\n            .add(ReminderEvent.removeReminder(reminderId: state.reminderId));\n        await _updateCellReminderId(\"\");\n      } else {\n        // Update reminder\n        final scheduledAt = option.getNotificationDateTime(state.dateTime!);\n        _reminderBloc.add(\n          ReminderEvent.update(\n            ReminderUpdate(\n              id: state.reminderId,\n              scheduledAt: scheduledAt,\n              includeTime: state.includeTime,\n            ),\n          ),\n        );\n      }\n    }\n  }\n\n  Future<void> _updateCellReminderId(\n    String reminderId,\n  ) async {\n    final result = await _dateCellBackendService.update(\n      reminderId: reminderId,\n    );\n    result.onFailure(Log.error);\n  }\n\n  void _updateReminderIfNecessary(\n    DateTime? dateTime,\n  ) {\n    if (state.reminderId.isEmpty || dateTime == null) {\n      return;\n    }\n\n    final scheduledAt = state.reminderOption.getNotificationDateTime(dateTime);\n\n    // Update Reminder\n    _reminderBloc.add(\n      ReminderEvent.update(\n        ReminderUpdate(\n          id: state.reminderId,\n          scheduledAt: scheduledAt,\n          includeTime: state.includeTime,\n        ),\n      ),\n    );\n  }\n\n  String timeFormatPrompt(FlowyError error) {\n    return switch (state.dateTypeOptionPB.timeFormat) {\n      TimeFormatPB.TwelveHour =>\n        \"${LocaleKeys.grid_field_invalidTimeFormat.tr()}. e.g. 01:00 PM\",\n      TimeFormatPB.TwentyFourHour =>\n        \"${LocaleKeys.grid_field_invalidTimeFormat.tr()}. e.g. 13:00\",\n      _ => \"\",\n    };\n  }\n\n  @override\n  Future<void> close() async {\n    if (_onCellChangedFn != null) {\n      cellController.removeListener(\n        onCellChanged: _onCellChangedFn!,\n        onFieldChanged: _onFieldChangedListener,\n      );\n    }\n    return super.close();\n  }\n\n  void _startListening() {\n    _onCellChangedFn = cellController.addListener(\n      onCellChanged: (cell) {\n        if (!isClosed) {\n          add(DateCellEditorEvent.didReceiveCellUpdate(cell));\n        }\n      },\n      onFieldChanged: _onFieldChangedListener,\n    );\n  }\n\n  void _onFieldChangedListener(FieldInfo fieldInfo) {\n    if (!isClosed) {\n      add(DateCellEditorEvent.didUpdateField(fieldInfo));\n    }\n  }\n\n  Future<void> _updateTypeOption(\n    Emitter<DateCellEditorState> emit, {\n    DateFormatPB? dateFormat,\n    TimeFormatPB? timeFormat,\n  }) async {\n    state.dateTypeOptionPB.freeze();\n    final newDateTypeOption = state.dateTypeOptionPB.rebuild((typeOption) {\n      if (dateFormat != null) {\n        typeOption.dateFormat = dateFormat;\n      }\n\n      if (timeFormat != null) {\n        typeOption.timeFormat = timeFormat;\n      }\n    });\n\n    final result = await FieldBackendService.updateFieldTypeOption(\n      viewId: cellController.viewId,\n      fieldId: cellController.fieldInfo.id,\n      typeOptionData: newDateTypeOption.writeToBuffer(),\n    );\n\n    result.onFailure(Log.error);\n  }\n}\n\n@freezed\nclass DateCellEditorEvent with _$DateCellEditorEvent {\n  const factory DateCellEditorEvent.didUpdateField(\n    FieldInfo fieldInfo,\n  ) = _DidUpdateField;\n\n  // notification that cell is updated in the backend\n  const factory DateCellEditorEvent.didReceiveCellUpdate(\n    DateCellDataPB? data,\n  ) = _DidReceiveCellUpdate;\n\n  const factory DateCellEditorEvent.updateDateTime(DateTime day) =\n      _UpdateDateTime;\n\n  const factory DateCellEditorEvent.updateDateRange(\n    DateTime start,\n    DateTime end,\n  ) = _UpdateDateRange;\n\n  const factory DateCellEditorEvent.setIncludeTime(\n    bool includeTime,\n    DateTime? dateTime,\n    DateTime? endDateTime,\n  ) = _IncludeTime;\n\n  const factory DateCellEditorEvent.setIsRange(\n    bool isRange,\n    DateTime? dateTime,\n    DateTime? endDateTime,\n  ) = _SetIsRange;\n\n  const factory DateCellEditorEvent.setReminderOption(ReminderOption option) =\n      _SetReminderOption;\n\n  // date field type options are modified\n  const factory DateCellEditorEvent.setTimeFormat(TimeFormatPB timeFormat) =\n      _SetTimeFormat;\n\n  const factory DateCellEditorEvent.setDateFormat(DateFormatPB dateFormat) =\n      _SetDateFormat;\n\n  const factory DateCellEditorEvent.clearDate() = _ClearDate;\n}\n\n@freezed\nclass DateCellEditorState with _$DateCellEditorState {\n  const factory DateCellEditorState({\n    // the date field's type option\n    required DateTypeOptionPB dateTypeOptionPB,\n\n    // cell data from the backend\n    required DateTime? dateTime,\n    required DateTime? endDateTime,\n    required bool includeTime,\n    required bool isRange,\n    required String reminderId,\n    @Default(ReminderOption.none) ReminderOption reminderOption,\n  }) = _DateCellEditorState;\n\n  factory DateCellEditorState.initial(\n    DateCellController controller,\n    ReminderBloc reminderBloc,\n  ) {\n    final typeOption = controller.getTypeOption(DateTypeOptionDataParser());\n    final cellData = controller.getCellData();\n    final dateCellData = DateCellData.fromPB(cellData);\n\n    ReminderOption reminderOption = ReminderOption.none;\n\n    if (dateCellData.reminderId.isNotEmpty && dateCellData.dateTime != null) {\n      final reminder = reminderBloc.state.allReminders\n          .firstWhereOrNull((r) => r.id == dateCellData.reminderId);\n      if (reminder != null) {\n        final eventDate = dateCellData.includeTime\n            ? dateCellData.dateTime!\n            : dateCellData.dateTime!.withoutTime;\n        reminderOption = ReminderOption.fromDateDifference(\n          eventDate,\n          reminder.scheduledAt.toDateTime(),\n        );\n      }\n    }\n\n    return DateCellEditorState(\n      dateTypeOptionPB: typeOption,\n      dateTime: dateCellData.dateTime,\n      endDateTime: dateCellData.endDateTime,\n      includeTime: dateCellData.includeTime,\n      isRange: dateCellData.isRange,\n      reminderId: dateCellData.reminderId,\n      reminderOption: reminderOption,\n    );\n  }\n}\n\n/// Helper class to parse ProtoBuf payloads into DateCellEditorState\nclass DateCellData {\n  const DateCellData({\n    required this.dateTime,\n    required this.endDateTime,\n    required this.includeTime,\n    required this.isRange,\n    required this.reminderId,\n  });\n\n  const DateCellData.empty()\n      : dateTime = null,\n        endDateTime = null,\n        includeTime = false,\n        isRange = false,\n        reminderId = \"\";\n\n  factory DateCellData.fromPB(DateCellDataPB? cellData) {\n    // a null DateCellDataPB may be returned, indicating that all the fields are\n    // their default values: empty strings and false booleans\n    if (cellData == null) {\n      return const DateCellData.empty();\n    }\n\n    final dateTime =\n        cellData.hasTimestamp() ? cellData.timestamp.toDateTime() : null;\n    final endDateTime = dateTime == null || !cellData.isRange\n        ? null\n        : cellData.hasEndTimestamp()\n            ? cellData.endTimestamp.toDateTime()\n            : null;\n\n    return DateCellData(\n      dateTime: dateTime,\n      endDateTime: endDateTime,\n      includeTime: cellData.includeTime,\n      isRange: cellData.isRange,\n      reminderId: cellData.reminderId,\n    );\n  }\n\n  final DateTime? dateTime;\n  final DateTime? endDateTime;\n  final bool includeTime;\n  final bool isRange;\n  final String reminderId;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/media_cell_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'media_cell_bloc.freezed.dart';\n\nclass MediaCellBloc extends Bloc<MediaCellEvent, MediaCellState> {\n  MediaCellBloc({\n    required this.cellController,\n  }) : super(MediaCellState.initial(cellController)) {\n    _dispatch();\n    _startListening();\n  }\n\n  late final RowBackendService _rowService =\n      RowBackendService(viewId: cellController.viewId);\n  final MediaCellController cellController;\n\n  void Function()? _onCellChangedFn;\n\n  String get databaseId => cellController.viewId;\n  String get rowId => cellController.rowId;\n  bool get wrapContent => cellController.fieldInfo.wrapCellContent ?? false;\n\n  @override\n  Future<void> close() async {\n    if (_onCellChangedFn != null) {\n      cellController.removeListener(\n        onCellChanged: _onCellChangedFn!,\n        onFieldChanged: _onFieldChangedListener,\n      );\n    }\n    await cellController.dispose();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<MediaCellEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            // Fetch user profile\n            final userProfileResult =\n                await UserBackendService.getCurrentUserProfile();\n            userProfileResult.fold(\n              (userProfile) => emit(state.copyWith(userProfile: userProfile)),\n              (l) => Log.error(l),\n            );\n          },\n          didUpdateCell: (files) {\n            emit(state.copyWith(files: files));\n          },\n          didUpdateField: (fieldName) {\n            final typeOption =\n                cellController.getTypeOption(MediaTypeOptionDataParser());\n\n            emit(\n              state.copyWith(\n                fieldName: fieldName,\n                hideFileNames: typeOption.hideFileNames,\n              ),\n            );\n          },\n          addFile: (url, name, uploadType, fileType) async {\n            final newFile = MediaFilePB(\n              id: uuid(),\n              url: url,\n              name: name,\n              uploadType: uploadType,\n              fileType: fileType,\n            );\n\n            final payload = MediaCellChangesetPB(\n              viewId: cellController.viewId,\n              cellId: CellIdPB(\n                viewId: cellController.viewId,\n                fieldId: cellController.fieldId,\n                rowId: cellController.rowId,\n              ),\n              insertedFiles: [newFile],\n              removedIds: [],\n            );\n\n            final result = await DatabaseEventUpdateMediaCell(payload).send();\n            result.fold((l) => null, (err) => Log.error(err));\n          },\n          removeFile: (id) async {\n            final payload = MediaCellChangesetPB(\n              viewId: cellController.viewId,\n              cellId: CellIdPB(\n                viewId: cellController.viewId,\n                fieldId: cellController.fieldId,\n                rowId: cellController.rowId,\n              ),\n              insertedFiles: [],\n              removedIds: [id],\n            );\n\n            final result = await DatabaseEventUpdateMediaCell(payload).send();\n            result.fold((l) => null, (err) => Log.error(err));\n          },\n          reorderFiles: (from, to) async {\n            final files = List<MediaFilePB>.from(state.files);\n            files.insert(to, files.removeAt(from));\n\n            // We emit the new state first to update the UI\n            emit(state.copyWith(files: files));\n\n            final payload = MediaCellChangesetPB(\n              viewId: cellController.viewId,\n              cellId: CellIdPB(\n                viewId: cellController.viewId,\n                fieldId: cellController.fieldId,\n                rowId: cellController.rowId,\n              ),\n              insertedFiles: files,\n              // In the backend we remove all files by id before we do inserts.\n              // So this will effectively reorder the files.\n              removedIds: files.map((file) => file.id).toList(),\n            );\n\n            final result = await DatabaseEventUpdateMediaCell(payload).send();\n            result.fold((l) => null, (err) => Log.error(err));\n          },\n          renameFile: (fileId, name) async {\n            final payload = RenameMediaChangesetPB(\n              viewId: cellController.viewId,\n              cellId: CellIdPB(\n                viewId: cellController.viewId,\n                fieldId: cellController.fieldId,\n                rowId: cellController.rowId,\n              ),\n              fileId: fileId,\n              name: name,\n            );\n\n            final result = await DatabaseEventRenameMediaFile(payload).send();\n            result.fold((l) => null, (err) => Log.error(err));\n          },\n          toggleShowAllFiles: () {\n            emit(state.copyWith(showAllFiles: !state.showAllFiles));\n          },\n          setCover: (cover) => _rowService.updateMeta(\n            rowId: cellController.rowId,\n            cover: cover,\n          ),\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _onCellChangedFn = cellController.addListener(\n      onCellChanged: (cellData) {\n        if (!isClosed) {\n          add(MediaCellEvent.didUpdateCell(cellData?.files ?? const []));\n        }\n      },\n      onFieldChanged: _onFieldChangedListener,\n    );\n  }\n\n  void _onFieldChangedListener(FieldInfo fieldInfo) {\n    if (!isClosed) {\n      add(MediaCellEvent.didUpdateField(fieldInfo.name));\n    }\n  }\n\n  void renameFile(String fileId, String name) =>\n      add(MediaCellEvent.renameFile(fileId: fileId, name: name));\n\n  void deleteFile(String fileId) =>\n      add(MediaCellEvent.removeFile(fileId: fileId));\n}\n\n@freezed\nclass MediaCellEvent with _$MediaCellEvent {\n  const factory MediaCellEvent.initial() = _Initial;\n\n  const factory MediaCellEvent.didUpdateCell(List<MediaFilePB> files) =\n      _DidUpdateCell;\n\n  const factory MediaCellEvent.didUpdateField(String fieldName) =\n      _DidUpdateField;\n\n  const factory MediaCellEvent.addFile({\n    required String url,\n    required String name,\n    required FileUploadTypePB uploadType,\n    required MediaFileTypePB fileType,\n  }) = _AddFile;\n\n  const factory MediaCellEvent.removeFile({\n    required String fileId,\n  }) = _RemoveFile;\n\n  const factory MediaCellEvent.reorderFiles({\n    required int from,\n    required int to,\n  }) = _ReorderFiles;\n\n  const factory MediaCellEvent.renameFile({\n    required String fileId,\n    required String name,\n  }) = _RenameFile;\n\n  const factory MediaCellEvent.toggleShowAllFiles() = _ToggleShowAllFiles;\n\n  const factory MediaCellEvent.setCover(RowCoverPB cover) = _SetCover;\n}\n\n@freezed\nclass MediaCellState with _$MediaCellState {\n  const factory MediaCellState({\n    UserProfilePB? userProfile,\n    required String fieldName,\n    @Default([]) List<MediaFilePB> files,\n    @Default(false) showAllFiles,\n    @Default(true) hideFileNames,\n  }) = _MediaCellState;\n\n  factory MediaCellState.initial(MediaCellController cellController) {\n    final cellData = cellController.getCellData();\n    final typeOption =\n        cellController.getTypeOption(MediaTypeOptionDataParser());\n\n    return MediaCellState(\n      fieldName: cellController.fieldInfo.field.name,\n      files: cellData?.files ?? const [],\n      hideFileNames: typeOption.hideFileNames,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/number_cell_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'number_cell_bloc.freezed.dart';\n\nclass NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {\n  NumberCellBloc({\n    required this.cellController,\n  }) : super(NumberCellState.initial(cellController)) {\n    _dispatch();\n    _startListening();\n  }\n\n  final NumberCellController cellController;\n  void Function()? _onCellChangedFn;\n\n  @override\n  Future<void> close() async {\n    if (_onCellChangedFn != null) {\n      cellController.removeListener(\n        onCellChanged: _onCellChangedFn!,\n        onFieldChanged: _onFieldChangedListener,\n      );\n    }\n    await cellController.dispose();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<NumberCellEvent>(\n      (event, emit) async {\n        await event.when(\n          didReceiveCellUpdate: (cellData) {\n            emit(state.copyWith(content: cellData ?? \"\"));\n          },\n          didUpdateField: (fieldInfo) {\n            final wrap = fieldInfo.wrapCellContent;\n            if (wrap != null) {\n              emit(state.copyWith(wrap: wrap));\n            }\n          },\n          updateCell: (text) async {\n            if (state.content != text) {\n              emit(state.copyWith(content: text));\n              await cellController.saveCellData(text);\n            }\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _onCellChangedFn = cellController.addListener(\n      onCellChanged: (cellContent) {\n        if (!isClosed) {\n          add(NumberCellEvent.didReceiveCellUpdate(cellContent));\n        }\n      },\n      onFieldChanged: _onFieldChangedListener,\n    );\n  }\n\n  void _onFieldChangedListener(FieldInfo fieldInfo) {\n    if (!isClosed) {\n      add(NumberCellEvent.didUpdateField(fieldInfo));\n    }\n  }\n}\n\n@freezed\nclass NumberCellEvent with _$NumberCellEvent {\n  const factory NumberCellEvent.didReceiveCellUpdate(String? cellContent) =\n      _DidReceiveCellUpdate;\n  const factory NumberCellEvent.didUpdateField(FieldInfo fieldInfo) =\n      _DidUpdateField;\n  const factory NumberCellEvent.updateCell(String text) = _UpdateCell;\n}\n\n@freezed\nclass NumberCellState with _$NumberCellState {\n  const factory NumberCellState({\n    required String content,\n    required bool wrap,\n  }) = _NumberCellState;\n\n  factory NumberCellState.initial(TextCellController cellController) {\n    final wrap = cellController.fieldInfo.wrapCellContent;\n    return NumberCellState(\n      content: cellController.getCellData() ?? \"\",\n      wrap: wrap ?? true,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/relation_type_option_cubit.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'relation_cell_bloc.freezed.dart';\n\nclass RelationCellBloc extends Bloc<RelationCellEvent, RelationCellState> {\n  RelationCellBloc({required this.cellController})\n      : super(RelationCellState.initial(cellController)) {\n    _dispatch();\n    _startListening();\n    _init();\n  }\n\n  final RelationCellController cellController;\n  void Function()? _onCellChangedFn;\n\n  @override\n  Future<void> close() async {\n    if (_onCellChangedFn != null) {\n      cellController.removeListener(\n        onCellChanged: _onCellChangedFn!,\n        onFieldChanged: _onFieldChangedListener,\n      );\n    }\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<RelationCellEvent>(\n      (event, emit) async {\n        await event.when(\n          didUpdateCell: (cellData) async {\n            if (cellData == null ||\n                cellData.rowIds.isEmpty ||\n                state.relatedDatabaseMeta == null) {\n              emit(state.copyWith(rows: const []));\n              return;\n            }\n            final payload = GetRelatedRowDataPB(\n              databaseId: state.relatedDatabaseMeta!.databaseId,\n              rowIds: cellData.rowIds,\n            );\n            final result =\n                await DatabaseEventGetRelatedRowDatas(payload).send();\n            final rows = result.fold(\n              (data) => data.rows,\n              (err) {\n                Log.error(err);\n                return const <RelatedRowDataPB>[];\n              },\n            );\n            emit(state.copyWith(rows: rows));\n          },\n          didUpdateField: (FieldInfo fieldInfo) async {\n            final wrap = fieldInfo.wrapCellContent;\n            if (wrap != null) {\n              emit(state.copyWith(wrap: wrap));\n            }\n            final RelationTypeOptionPB typeOption =\n                cellController.getTypeOption(RelationTypeOptionDataParser());\n            if (typeOption.databaseId.isEmpty) {\n              return;\n            }\n            final meta = await _loadDatabaseMeta(typeOption.databaseId);\n            emit(state.copyWith(relatedDatabaseMeta: meta));\n            _loadCellData();\n          },\n          selectDatabaseId: (databaseId) async {\n            await _updateTypeOption(databaseId);\n          },\n          selectRow: (rowId) async {\n            await _handleSelectRow(rowId);\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _onCellChangedFn = cellController.addListener(\n      onCellChanged: (data) {\n        if (!isClosed) {\n          add(RelationCellEvent.didUpdateCell(data));\n        }\n      },\n      onFieldChanged: _onFieldChangedListener,\n    );\n  }\n\n  void _onFieldChangedListener(FieldInfo fieldInfo) {\n    if (!isClosed) {\n      add(RelationCellEvent.didUpdateField(fieldInfo));\n    }\n  }\n\n  void _init() {\n    add(RelationCellEvent.didUpdateField(cellController.fieldInfo));\n  }\n\n  void _loadCellData() {\n    final cellData = cellController.getCellData();\n    if (!isClosed && cellData != null) {\n      add(RelationCellEvent.didUpdateCell(cellData));\n    }\n  }\n\n  Future<void> _handleSelectRow(String rowId) async {\n    final payload = RelationCellChangesetPB(\n      viewId: cellController.viewId,\n      cellId: CellIdPB(\n        viewId: cellController.viewId,\n        fieldId: cellController.fieldId,\n        rowId: cellController.rowId,\n      ),\n    );\n    if (state.rows.any((row) => row.rowId == rowId)) {\n      payload.removedRowIds.add(rowId);\n    } else {\n      payload.insertedRowIds.add(rowId);\n    }\n    final result = await DatabaseEventUpdateRelationCell(payload).send();\n    result.fold((l) => null, (err) => Log.error(err));\n  }\n\n  Future<DatabaseMeta?> _loadDatabaseMeta(String databaseId) async {\n    final getDatabaseResult = await DatabaseEventGetDatabases().send();\n    final databaseMeta = getDatabaseResult.fold<DatabaseMetaPB?>(\n      (s) => s.items.firstWhereOrNull(\n        (metaPB) => metaPB.databaseId == databaseId,\n      ),\n      (f) => null,\n    );\n    if (databaseMeta != null) {\n      final result = await ViewBackendService.getView(databaseMeta.viewId);\n      return result.fold(\n        (s) => DatabaseMeta(\n          databaseId: databaseId,\n          viewId: databaseMeta.viewId,\n          databaseName: s.name,\n        ),\n        (f) => null,\n      );\n    }\n    return null;\n  }\n\n  Future<void> _updateTypeOption(String databaseId) async {\n    final newDateTypeOption = RelationTypeOptionPB(\n      databaseId: databaseId,\n    );\n\n    final result = await FieldBackendService.updateFieldTypeOption(\n      viewId: cellController.viewId,\n      fieldId: cellController.fieldInfo.id,\n      typeOptionData: newDateTypeOption.writeToBuffer(),\n    );\n    result.fold((s) => null, (err) => Log.error(err));\n  }\n}\n\n@freezed\nclass RelationCellEvent with _$RelationCellEvent {\n  const factory RelationCellEvent.didUpdateCell(RelationCellDataPB? data) =\n      _DidUpdateCell;\n  const factory RelationCellEvent.didUpdateField(FieldInfo fieldInfo) =\n      _DidUpdateField;\n  const factory RelationCellEvent.selectDatabaseId(\n    String databaseId,\n  ) = _SelectDatabaseId;\n  const factory RelationCellEvent.selectRow(String rowId) = _SelectRowId;\n}\n\n@freezed\nclass RelationCellState with _$RelationCellState {\n  const factory RelationCellState({\n    required DatabaseMeta? relatedDatabaseMeta,\n    required List<RelatedRowDataPB> rows,\n    required bool wrap,\n  }) = _RelationCellState;\n\n  factory RelationCellState.initial(RelationCellController cellController) {\n    final wrap = cellController.fieldInfo.wrapCellContent;\n    return RelationCellState(\n      relatedDatabaseMeta: null,\n      rows: [],\n      wrap: wrap ?? true,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_row_search_bloc.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'relation_row_search_bloc.freezed.dart';\n\nclass RelationRowSearchBloc\n    extends Bloc<RelationRowSearchEvent, RelationRowSearchState> {\n  RelationRowSearchBloc({\n    required this.databaseId,\n  }) : super(RelationRowSearchState.initial()) {\n    _dispatch();\n    _init();\n  }\n\n  final String databaseId;\n  final List<RelatedRowDataPB> allRows = [];\n\n  void _dispatch() {\n    on<RelationRowSearchEvent>(\n      (event, emit) {\n        event.when(\n          didUpdateRowList: (List<RelatedRowDataPB> rowList) {\n            allRows\n              ..clear()\n              ..addAll(rowList);\n            emit(\n              state.copyWith(\n                filteredRows: allRows,\n                focusedRowId: state.focusedRowId ?? allRows.firstOrNull?.rowId,\n              ),\n            );\n          },\n          updateFilter: (String filter) => _updateFilter(filter, emit),\n          updateFocusedOption: (String rowId) {\n            emit(state.copyWith(focusedRowId: rowId));\n          },\n          focusPreviousOption: () => _focusOption(true, emit),\n          focusNextOption: () => _focusOption(false, emit),\n        );\n      },\n    );\n  }\n\n  Future<void> _init() async {\n    final payload = DatabaseIdPB(value: databaseId);\n    final result = await DatabaseEventGetRelatedDatabaseRows(payload).send();\n    result.fold(\n      (data) => add(RelationRowSearchEvent.didUpdateRowList(data.rows)),\n      (err) => Log.error(err),\n    );\n  }\n\n  void _updateFilter(String filter, Emitter<RelationRowSearchState> emit) {\n    final rows = [...allRows];\n\n    if (filter.isNotEmpty) {\n      rows.retainWhere(\n        (row) =>\n            row.name.toLowerCase().contains(filter.toLowerCase()) ||\n            (row.name.isEmpty &&\n                LocaleKeys.grid_row_titlePlaceholder\n                    .tr()\n                    .toLowerCase()\n                    .contains(filter.toLowerCase())),\n      );\n    }\n\n    final focusedRowId = rows.isEmpty\n        ? null\n        : rows.any((row) => row.rowId == state.focusedRowId)\n            ? state.focusedRowId\n            : rows.first.rowId;\n\n    emit(\n      state.copyWith(\n        filteredRows: rows,\n        focusedRowId: focusedRowId,\n      ),\n    );\n  }\n\n  void _focusOption(bool previous, Emitter<RelationRowSearchState> emit) {\n    if (state.filteredRows.isEmpty) {\n      return;\n    }\n\n    final rowIds = state.filteredRows.map((e) => e.rowId).toList();\n    final currentIndex = state.focusedRowId == null\n        ? -1\n        : rowIds.indexWhere((id) => id == state.focusedRowId);\n\n    // If the current index is -1, it means that the focused row is not in the list of row ids.\n    // In this case, we set the new index to the last index if previous is true, otherwise to 0.\n    final newIndex = currentIndex == -1\n        ? (previous ? rowIds.length - 1 : 0)\n        : (currentIndex + (previous ? -1 : 1)) % rowIds.length;\n\n    emit(state.copyWith(focusedRowId: rowIds[newIndex]));\n  }\n}\n\n@freezed\nclass RelationRowSearchEvent with _$RelationRowSearchEvent {\n  const factory RelationRowSearchEvent.didUpdateRowList(\n    List<RelatedRowDataPB> rowList,\n  ) = _DidUpdateRowList;\n  const factory RelationRowSearchEvent.updateFilter(String filter) =\n      _UpdateFilter;\n  const factory RelationRowSearchEvent.updateFocusedOption(\n    String rowId,\n  ) = _UpdateFocusedOption;\n  const factory RelationRowSearchEvent.focusPreviousOption() =\n      _FocusPreviousOption;\n  const factory RelationRowSearchEvent.focusNextOption() = _FocusNextOption;\n}\n\n@freezed\nclass RelationRowSearchState with _$RelationRowSearchState {\n  const factory RelationRowSearchState({\n    required List<RelatedRowDataPB> filteredRows,\n    required String? focusedRowId,\n  }) = _RelationRowSearchState;\n\n  factory RelationRowSearchState.initial() => const RelationRowSearchState(\n        filteredRows: [],\n        focusedRowId: null,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'select_option_cell_bloc.freezed.dart';\n\nclass SelectOptionCellBloc\n    extends Bloc<SelectOptionCellEvent, SelectOptionCellState> {\n  SelectOptionCellBloc({\n    required this.cellController,\n  }) : super(SelectOptionCellState.initial(cellController)) {\n    _dispatch();\n    _startListening();\n  }\n\n  final SelectOptionCellController cellController;\n  void Function()? _onCellChangedFn;\n\n  @override\n  Future<void> close() async {\n    if (_onCellChangedFn != null) {\n      cellController.removeListener(\n        onCellChanged: _onCellChangedFn!,\n        onFieldChanged: _onFieldChangedListener,\n      );\n    }\n    await cellController.dispose();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<SelectOptionCellEvent>(\n      (event, emit) {\n        event.when(\n          didReceiveOptions: (List<SelectOptionPB> selectedOptions) {\n            emit(\n              state.copyWith(\n                selectedOptions: selectedOptions,\n              ),\n            );\n          },\n          didUpdateField: (fieldInfo) {\n            final wrap = fieldInfo.wrapCellContent;\n            if (wrap != null) {\n              emit(state.copyWith(wrap: wrap));\n            }\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _onCellChangedFn = cellController.addListener(\n      onCellChanged: (selectOptionCellData) {\n        if (!isClosed) {\n          add(\n            SelectOptionCellEvent.didReceiveOptions(\n              selectOptionCellData?.selectOptions ?? [],\n            ),\n          );\n        }\n      },\n      onFieldChanged: _onFieldChangedListener,\n    );\n  }\n\n  void _onFieldChangedListener(FieldInfo fieldInfo) {\n    if (!isClosed) {\n      add(SelectOptionCellEvent.didUpdateField(fieldInfo));\n    }\n  }\n}\n\n@freezed\nclass SelectOptionCellEvent with _$SelectOptionCellEvent {\n  const factory SelectOptionCellEvent.didReceiveOptions(\n    List<SelectOptionPB> selectedOptions,\n  ) = _DidReceiveOptions;\n  const factory SelectOptionCellEvent.didUpdateField(FieldInfo fieldInfo) =\n      _DidUpdateField;\n}\n\n@freezed\nclass SelectOptionCellState with _$SelectOptionCellState {\n  const factory SelectOptionCellState({\n    required List<SelectOptionPB> selectedOptions,\n    required bool wrap,\n  }) = _SelectOptionCellState;\n\n  factory SelectOptionCellState.initial(\n    SelectOptionCellController cellController,\n  ) {\n    final data = cellController.getCellData();\n    final wrap = cellController.fieldInfo.wrapCellContent;\n    return SelectOptionCellState(\n      selectedOptions: data?.selectOptions ?? [],\n      wrap: wrap ?? true,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/widgets.dart';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/plugins/database/domain/select_option_cell_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'select_option_cell_editor_bloc.freezed.dart';\n\nconst String createSelectOptionSuggestionId =\n    \"create_select_option_suggestion_id\";\n\nclass SelectOptionCellEditorBloc\n    extends Bloc<SelectOptionCellEditorEvent, SelectOptionCellEditorState> {\n  SelectOptionCellEditorBloc({\n    required this.cellController,\n  })  : _selectOptionService = SelectOptionCellBackendService(\n          viewId: cellController.viewId,\n          fieldId: cellController.fieldId,\n          rowId: cellController.rowId,\n        ),\n        _typeOptionAction = cellController.fieldType == FieldType.SingleSelect\n            ? SingleSelectAction(\n                viewId: cellController.viewId,\n                fieldId: cellController.fieldId,\n                onTypeOptionUpdated: (typeOptionData) =>\n                    FieldBackendService.updateFieldTypeOption(\n                  viewId: cellController.viewId,\n                  fieldId: cellController.fieldId,\n                  typeOptionData: typeOptionData,\n                ),\n              )\n            : MultiSelectAction(\n                viewId: cellController.viewId,\n                fieldId: cellController.fieldId,\n                onTypeOptionUpdated: (typeOptionData) =>\n                    FieldBackendService.updateFieldTypeOption(\n                  viewId: cellController.viewId,\n                  fieldId: cellController.fieldId,\n                  typeOptionData: typeOptionData,\n                ),\n              ),\n        super(SelectOptionCellEditorState.initial(cellController)) {\n    _dispatch();\n    _startListening();\n    final loadedOptions = _loadAllOptions(cellController);\n    add(SelectOptionCellEditorEvent.didUpdateOptions(loadedOptions));\n  }\n\n  final SelectOptionCellBackendService _selectOptionService;\n  final ISelectOptionAction _typeOptionAction;\n  final SelectOptionCellController cellController;\n\n  VoidCallback? _onCellChangedFn;\n\n  final List<SelectOptionPB> allOptions = [];\n  String filter = \"\";\n\n  void _dispatch() {\n    on<SelectOptionCellEditorEvent>(\n      (event, emit) async {\n        await event.when(\n          didUpdateCell: (selectedOptions) {\n            emit(state.copyWith(selectedOptions: selectedOptions));\n          },\n          didUpdateOptions: (options) {\n            allOptions\n              ..clear()\n              ..addAll(options);\n            final result = _getVisibleOptions(options);\n            emit(\n              state.copyWith(\n                options: result.options,\n                createSelectOptionSuggestion:\n                    result.createSelectOptionSuggestion,\n              ),\n            );\n          },\n          createOption: () async {\n            if (state.createSelectOptionSuggestion == null) {\n              return;\n            }\n            filter = \"\";\n            await _createOption(\n              name: state.createSelectOptionSuggestion!.name,\n              color: state.createSelectOptionSuggestion!.color,\n            );\n            emit(state.copyWith(clearFilter: true));\n          },\n          deleteOption: (option) async {\n            await _deleteOption([option]);\n          },\n          deleteAllOptions: () async {\n            if (allOptions.isNotEmpty) {\n              await _deleteOption(allOptions);\n            }\n          },\n          updateOption: (option) async {\n            await _updateOption(option);\n          },\n          selectOption: (optionId) async {\n            await _selectOptionService.select(optionIds: [optionId]);\n          },\n          unselectOption: (optionId) async {\n            await _selectOptionService.unselect(optionIds: [optionId]);\n          },\n          unselectLastOption: () async {\n            if (state.selectedOptions.isEmpty) {\n              return;\n            }\n            final lastSelectedOptionId = state.selectedOptions.last.id;\n            await _selectOptionService\n                .unselect(optionIds: [lastSelectedOptionId]);\n          },\n          submitTextField: () {\n            _submitTextFieldValue(emit);\n          },\n          selectMultipleOptions: (optionNames, remainder) {\n            if (optionNames.isNotEmpty) {\n              _selectMultipleOptions(optionNames);\n            }\n            _filterOption(remainder, emit);\n          },\n          reorderOption: (fromOptionId, toOptionId) {\n            final options = _typeOptionAction.reorderOption(\n              allOptions,\n              fromOptionId,\n              toOptionId,\n            );\n            allOptions\n              ..clear()\n              ..addAll(options);\n            final result = _getVisibleOptions(options);\n            emit(state.copyWith(options: result.options));\n          },\n          filterOption: (filterText) {\n            _filterOption(filterText, emit);\n          },\n          focusPreviousOption: () {\n            _focusOption(true, emit);\n          },\n          focusNextOption: () {\n            _focusOption(false, emit);\n          },\n          updateFocusedOption: (optionId) {\n            emit(state.copyWith(focusedOptionId: optionId));\n          },\n          resetClearFilterFlag: () {\n            emit(state.copyWith(clearFilter: false));\n          },\n        );\n      },\n    );\n  }\n\n  @override\n  Future<void> close() async {\n    if (_onCellChangedFn != null) {\n      cellController.removeListener(\n        onCellChanged: _onCellChangedFn!,\n        onFieldChanged: _onFieldChangedListener,\n      );\n    }\n    return super.close();\n  }\n\n  void _startListening() {\n    _onCellChangedFn = cellController.addListener(\n      onCellChanged: (cellData) {\n        if (!isClosed) {\n          add(\n            SelectOptionCellEditorEvent.didUpdateCell(\n              cellData == null ? [] : cellData.selectOptions,\n            ),\n          );\n        }\n      },\n      onFieldChanged: _onFieldChangedListener,\n    );\n  }\n\n  void _onFieldChangedListener(FieldInfo fieldInfo) {\n    if (!isClosed) {\n      final loadedOptions = _loadAllOptions(cellController);\n      add(SelectOptionCellEditorEvent.didUpdateOptions(loadedOptions));\n    }\n  }\n\n  Future<void> _createOption({\n    required String name,\n    required SelectOptionColorPB color,\n  }) async {\n    final result = await _selectOptionService.create(\n      name: name,\n      color: color,\n    );\n    result.fold((l) => {}, (err) => Log.error(err));\n  }\n\n  Future<void> _deleteOption(List<SelectOptionPB> options) async {\n    final result = await _selectOptionService.delete(options: options);\n    result.fold((l) => null, (err) => Log.error(err));\n  }\n\n  Future<void> _updateOption(SelectOptionPB option) async {\n    final result = await _selectOptionService.update(\n      option: option,\n    );\n\n    result.fold((l) => null, (err) => Log.error(err));\n  }\n\n  void _submitTextFieldValue(Emitter<SelectOptionCellEditorState> emit) {\n    if (state.focusedOptionId == null) {\n      return;\n    }\n\n    final focusedOptionId = state.focusedOptionId!;\n\n    if (focusedOptionId == createSelectOptionSuggestionId) {\n      filter = \"\";\n      _createOption(\n        name: state.createSelectOptionSuggestion!.name,\n        color: state.createSelectOptionSuggestion!.color,\n      );\n      emit(\n        state.copyWith(\n          createSelectOptionSuggestion: null,\n          clearFilter: true,\n        ),\n      );\n    } else if (!state.selectedOptions\n        .any((option) => option.id == focusedOptionId)) {\n      _selectOptionService.select(optionIds: [focusedOptionId]);\n      emit(\n        state.copyWith(\n          clearFilter: true,\n        ),\n      );\n    }\n  }\n\n  void _selectMultipleOptions(List<String> optionNames) {\n    final optionIds = optionNames\n        .map(\n          (name) => allOptions.firstWhereOrNull(\n            (option) => option.name.toLowerCase() == name.toLowerCase(),\n          ),\n        )\n        .nonNulls\n        .map((option) => option.id)\n        .toList();\n\n    _selectOptionService.select(optionIds: optionIds);\n  }\n\n  void _filterOption(\n    String filterText,\n    Emitter<SelectOptionCellEditorState> emit,\n  ) {\n    filter = filterText;\n    final _MakeOptionResult result = _getVisibleOptions(\n      allOptions,\n    );\n    final focusedOptionId = result.options.isEmpty\n        ? result.createSelectOptionSuggestion == null\n            ? null\n            : createSelectOptionSuggestionId\n        : result.options.any((option) => option.id == state.focusedOptionId)\n            ? state.focusedOptionId\n            : result.options.first.id;\n    emit(\n      state.copyWith(\n        options: result.options,\n        createSelectOptionSuggestion: result.createSelectOptionSuggestion,\n        focusedOptionId: focusedOptionId,\n      ),\n    );\n  }\n\n  _MakeOptionResult _getVisibleOptions(\n    List<SelectOptionPB> allOptions,\n  ) {\n    final List<SelectOptionPB> options = List.from(allOptions);\n    String newOptionName = filter;\n\n    if (filter.isNotEmpty) {\n      options.retainWhere((option) {\n        final name = option.name.toLowerCase();\n        final lFilter = filter.toLowerCase();\n\n        if (name == lFilter) {\n          newOptionName = \"\";\n        }\n\n        return name.contains(lFilter);\n      });\n    }\n\n    return _MakeOptionResult(\n      options: options,\n      createSelectOptionSuggestion: newOptionName.isEmpty\n          ? null\n          : CreateSelectOptionSuggestion(\n              name: newOptionName,\n              color: newSelectOptionColor(allOptions),\n            ),\n    );\n  }\n\n  void _focusOption(bool previous, Emitter<SelectOptionCellEditorState> emit) {\n    if (state.options.isEmpty && state.createSelectOptionSuggestion == null) {\n      return;\n    }\n\n    final optionIds = [\n      ...state.options.map((e) => e.id),\n      if (state.createSelectOptionSuggestion != null)\n        createSelectOptionSuggestionId,\n    ];\n\n    if (state.focusedOptionId == null) {\n      emit(\n        state.copyWith(\n          focusedOptionId: previous ? optionIds.last : optionIds.first,\n        ),\n      );\n      return;\n    }\n\n    final currentIndex =\n        optionIds.indexWhere((id) => id == state.focusedOptionId);\n\n    final newIndex = currentIndex == -1\n        ? 0\n        : (currentIndex + (previous ? -1 : 1)) % optionIds.length;\n\n    emit(state.copyWith(focusedOptionId: optionIds[newIndex]));\n  }\n}\n\n@freezed\nclass SelectOptionCellEditorEvent with _$SelectOptionCellEditorEvent {\n  const factory SelectOptionCellEditorEvent.didUpdateCell(\n    List<SelectOptionPB> selectedOptions,\n  ) = _DidUpdateCell;\n  const factory SelectOptionCellEditorEvent.didUpdateOptions(\n    List<SelectOptionPB> options,\n  ) = _DidUpdateOptions;\n  const factory SelectOptionCellEditorEvent.createOption() = _CreateOption;\n  const factory SelectOptionCellEditorEvent.selectOption(String optionId) =\n      _SelectOption;\n  const factory SelectOptionCellEditorEvent.unselectOption(String optionId) =\n      _UnselectOption;\n  const factory SelectOptionCellEditorEvent.unselectLastOption() =\n      _UnselectLastOption;\n  const factory SelectOptionCellEditorEvent.updateOption(\n    SelectOptionPB option,\n  ) = _UpdateOption;\n  const factory SelectOptionCellEditorEvent.deleteOption(\n    SelectOptionPB option,\n  ) = _DeleteOption;\n  const factory SelectOptionCellEditorEvent.deleteAllOptions() =\n      _DeleteAllOptions;\n  const factory SelectOptionCellEditorEvent.reorderOption(\n    String fromOptionId,\n    String toOptionId,\n  ) = _ReorderOption;\n  const factory SelectOptionCellEditorEvent.filterOption(String filterText) =\n      _SelectOptionFilter;\n  const factory SelectOptionCellEditorEvent.submitTextField() =\n      _SubmitTextField;\n  const factory SelectOptionCellEditorEvent.selectMultipleOptions(\n    List<String> optionNames,\n    String remainder,\n  ) = _SelectMultipleOptions;\n  const factory SelectOptionCellEditorEvent.focusPreviousOption() =\n      _FocusPreviousOption;\n  const factory SelectOptionCellEditorEvent.focusNextOption() =\n      _FocusNextOption;\n  const factory SelectOptionCellEditorEvent.updateFocusedOption(\n    String? optionId,\n  ) = _UpdateFocusedOption;\n  const factory SelectOptionCellEditorEvent.resetClearFilterFlag() =\n      _ResetClearFilterFlag;\n}\n\n@freezed\nclass SelectOptionCellEditorState with _$SelectOptionCellEditorState {\n  const factory SelectOptionCellEditorState({\n    required List<SelectOptionPB> options,\n    required List<SelectOptionPB> selectedOptions,\n    required CreateSelectOptionSuggestion? createSelectOptionSuggestion,\n    required String? focusedOptionId,\n    required bool clearFilter,\n  }) = _SelectOptionEditorState;\n\n  factory SelectOptionCellEditorState.initial(\n    SelectOptionCellController cellController,\n  ) {\n    final allOptions = _loadAllOptions(cellController);\n    final data = cellController.getCellData();\n    return SelectOptionCellEditorState(\n      options: allOptions,\n      selectedOptions: data?.selectOptions ?? [],\n      createSelectOptionSuggestion: null,\n      focusedOptionId: null,\n      clearFilter: false,\n    );\n  }\n}\n\nclass _MakeOptionResult {\n  _MakeOptionResult({\n    required this.options,\n    required this.createSelectOptionSuggestion,\n  });\n\n  List<SelectOptionPB> options;\n  CreateSelectOptionSuggestion? createSelectOptionSuggestion;\n}\n\nclass CreateSelectOptionSuggestion {\n  CreateSelectOptionSuggestion({\n    required this.name,\n    required this.color,\n  });\n\n  final String name;\n  final SelectOptionColorPB color;\n}\n\nList<SelectOptionPB> _loadAllOptions(\n  SelectOptionCellController cellController,\n) {\n  if (cellController.fieldType == FieldType.SingleSelect) {\n    return cellController\n        .getTypeOption<SingleSelectTypeOptionPB>(\n          SingleSelectTypeOptionDataParser(),\n        )\n        .options;\n  } else {\n    return cellController\n        .getTypeOption<MultiSelectTypeOptionPB>(\n          MultiSelectTypeOptionDataParser(),\n        )\n        .options;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/summary_cell_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'summary_cell_bloc.freezed.dart';\n\nclass SummaryCellBloc extends Bloc<SummaryCellEvent, SummaryCellState> {\n  SummaryCellBloc({\n    required this.cellController,\n  }) : super(SummaryCellState.initial(cellController)) {\n    _dispatch();\n    _startListening();\n  }\n\n  final SummaryCellController cellController;\n  void Function()? _onCellChangedFn;\n\n  @override\n  Future<void> close() async {\n    if (_onCellChangedFn != null) {\n      cellController.removeListener(\n        onCellChanged: _onCellChangedFn!,\n        onFieldChanged: _onFieldChangedListener,\n      );\n    }\n    await cellController.dispose();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<SummaryCellEvent>(\n      (event, emit) async {\n        await event.when(\n          didReceiveCellUpdate: (cellData) {\n            emit(\n              state.copyWith(content: cellData ?? \"\"),\n            );\n          },\n          didUpdateField: (fieldInfo) {\n            final wrap = fieldInfo.wrapCellContent;\n            if (wrap != null) {\n              emit(state.copyWith(wrap: wrap));\n            }\n          },\n          updateCell: (text) async {\n            if (state.content != text) {\n              emit(state.copyWith(content: text));\n              await cellController.saveCellData(text);\n\n              // If the input content is \"abc\" that can't parsered as number then the data stored in the backend will be an empty string.\n              // So for every cell data that will be formatted in the backend.\n              // It needs to get the formatted data after saving.\n              add(\n                SummaryCellEvent.didReceiveCellUpdate(\n                  cellController.getCellData() ?? \"\",\n                ),\n              );\n            }\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _onCellChangedFn = cellController.addListener(\n      onCellChanged: (cellContent) {\n        if (!isClosed) {\n          add(\n            SummaryCellEvent.didReceiveCellUpdate(cellContent ?? \"\"),\n          );\n        }\n      },\n      onFieldChanged: _onFieldChangedListener,\n    );\n  }\n\n  void _onFieldChangedListener(FieldInfo fieldInfo) {\n    if (!isClosed) {\n      add(SummaryCellEvent.didUpdateField(fieldInfo));\n    }\n  }\n}\n\n@freezed\nclass SummaryCellEvent with _$SummaryCellEvent {\n  const factory SummaryCellEvent.didReceiveCellUpdate(String? cellContent) =\n      _DidReceiveCellUpdate;\n  const factory SummaryCellEvent.didUpdateField(FieldInfo fieldInfo) =\n      _DidUpdateField;\n  const factory SummaryCellEvent.updateCell(String text) = _UpdateCell;\n}\n\n@freezed\nclass SummaryCellState with _$SummaryCellState {\n  const factory SummaryCellState({\n    required String content,\n    required bool wrap,\n  }) = _SummaryCellState;\n\n  factory SummaryCellState.initial(SummaryCellController cellController) {\n    final wrap = cellController.fieldInfo.wrapCellContent;\n    return SummaryCellState(\n      content: cellController.getCellData() ?? \"\",\n      wrap: wrap ?? true,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/summary_row_bloc.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'summary_row_bloc.freezed.dart';\n\nclass SummaryRowBloc extends Bloc<SummaryRowEvent, SummaryRowState> {\n  SummaryRowBloc({\n    required this.viewId,\n    required this.rowId,\n    required this.fieldId,\n  }) : super(SummaryRowState.initial()) {\n    _dispatch();\n  }\n\n  final String viewId;\n  final String rowId;\n  final String fieldId;\n\n  void _dispatch() {\n    on<SummaryRowEvent>(\n      (event, emit) async {\n        event.when(\n          startSummary: () {\n            final params = SummaryRowPB(\n              viewId: viewId,\n              rowId: rowId,\n              fieldId: fieldId,\n            );\n            emit(\n              state.copyWith(\n                loadingState: const LoadingState.loading(),\n                error: null,\n              ),\n            );\n\n            DatabaseEventSummarizeRow(params).send().then(\n                  (result) => {\n                    if (!isClosed) add(SummaryRowEvent.finishSummary(result)),\n                  },\n                );\n          },\n          finishSummary: (result) {\n            result.fold(\n              (s) => {\n                emit(\n                  state.copyWith(\n                    loadingState: const LoadingState.finish(),\n                    error: null,\n                  ),\n                ),\n              },\n              (err) => {\n                emit(\n                  state.copyWith(\n                    loadingState: const LoadingState.finish(),\n                    error: err,\n                  ),\n                ),\n              },\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\n@freezed\nclass SummaryRowEvent with _$SummaryRowEvent {\n  const factory SummaryRowEvent.startSummary() = _DidStartSummary;\n  const factory SummaryRowEvent.finishSummary(\n    FlowyResult<void, FlowyError> result,\n  ) = _DidFinishSummary;\n}\n\n@freezed\nclass SummaryRowState with _$SummaryRowState {\n  const factory SummaryRowState({\n    required LoadingState loadingState,\n    required FlowyError? error,\n  }) = _SummaryRowState;\n\n  factory SummaryRowState.initial() {\n    return const SummaryRowState(\n      loadingState: LoadingState.finish(),\n      error: null,\n    );\n  }\n}\n\n@freezed\nclass LoadingState with _$LoadingState {\n  const factory LoadingState.loading() = _Loading;\n  const factory LoadingState.finish() = _Finish;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/text_cell_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'text_cell_bloc.freezed.dart';\n\nclass TextCellBloc extends Bloc<TextCellEvent, TextCellState> {\n  TextCellBloc({required this.cellController})\n      : super(TextCellState.initial(cellController)) {\n    _dispatch();\n    _startListening();\n  }\n\n  final TextCellController cellController;\n  void Function()? _onCellChangedFn;\n\n  @override\n  Future<void> close() async {\n    if (_onCellChangedFn != null) {\n      cellController.removeListener(\n        onCellChanged: _onCellChangedFn!,\n        onFieldChanged: _onFieldChangedListener,\n      );\n    }\n    await cellController.dispose();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<TextCellEvent>(\n      (event, emit) {\n        event.when(\n          didReceiveCellUpdate: (content) {\n            emit(state.copyWith(content: content));\n          },\n          didUpdateField: (fieldInfo) {\n            final wrap = fieldInfo.wrapCellContent;\n            if (wrap != null) {\n              emit(state.copyWith(wrap: wrap));\n            }\n          },\n          updateText: (String text) {\n            // If the content is null, it indicates that either the cell is empty (no data)\n            // or the cell data is still being fetched from the backend and is not yet available.\n            if (state.content != null && state.content != text) {\n              cellController.saveCellData(text, debounce: true);\n            }\n          },\n          enableEdit: (bool enabled) {\n            emit(state.copyWith(enableEdit: enabled));\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _onCellChangedFn = cellController.addListener(\n      onCellChanged: (cellContent) {\n        if (!isClosed) {\n          add(TextCellEvent.didReceiveCellUpdate(cellContent));\n        }\n      },\n      onFieldChanged: _onFieldChangedListener,\n    );\n  }\n\n  void _onFieldChangedListener(FieldInfo fieldInfo) {\n    if (!isClosed) {\n      add(TextCellEvent.didUpdateField(fieldInfo));\n    }\n  }\n}\n\n@freezed\nclass TextCellEvent with _$TextCellEvent {\n  const factory TextCellEvent.didReceiveCellUpdate(String? cellContent) =\n      _DidReceiveCellUpdate;\n  const factory TextCellEvent.didUpdateField(FieldInfo fieldInfo) =\n      _DidUpdateField;\n  const factory TextCellEvent.updateText(String text) = _UpdateText;\n  const factory TextCellEvent.enableEdit(bool enabled) = _EnableEdit;\n}\n\n@freezed\nclass TextCellState with _$TextCellState {\n  const factory TextCellState({\n    required String? content,\n    required ValueNotifier<String>? emoji,\n    required ValueNotifier<bool>? hasDocument,\n    required bool enableEdit,\n    required bool wrap,\n  }) = _TextCellState;\n\n  factory TextCellState.initial(TextCellController cellController) {\n    final cellData = cellController.getCellData();\n    final wrap = cellController.fieldInfo.wrapCellContent ?? true;\n    ValueNotifier<String>? emoji;\n    ValueNotifier<bool>? hasDocument;\n    if (cellController.fieldInfo.isPrimary) {\n      emoji = cellController.icon;\n      hasDocument = cellController.hasDocument;\n    }\n\n    return TextCellState(\n      content: cellData,\n      emoji: emoji,\n      enableEdit: false,\n      hasDocument: hasDocument,\n      wrap: wrap,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/time_cell_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/util/time.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\n\npart 'time_cell_bloc.freezed.dart';\n\nclass TimeCellBloc extends Bloc<TimeCellEvent, TimeCellState> {\n  TimeCellBloc({\n    required this.cellController,\n  }) : super(TimeCellState.initial(cellController)) {\n    _dispatch();\n    _startListening();\n  }\n\n  final TimeCellController cellController;\n  void Function()? _onCellChangedFn;\n\n  @override\n  Future<void> close() async {\n    if (_onCellChangedFn != null) {\n      cellController.removeListener(\n        onCellChanged: _onCellChangedFn!,\n        onFieldChanged: _onFieldChangedListener,\n      );\n    }\n    await cellController.dispose();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<TimeCellEvent>(\n      (event, emit) async {\n        await event.when(\n          didReceiveCellUpdate: (content) {\n            emit(\n              state.copyWith(\n                content:\n                    content != null ? formatTime(content.time.toInt()) : \"\",\n              ),\n            );\n          },\n          didUpdateField: (fieldInfo) {\n            final wrap = fieldInfo.wrapCellContent;\n            if (wrap != null) {\n              emit(state.copyWith(wrap: wrap));\n            }\n          },\n          updateCell: (text) async {\n            text = parseTime(text)?.toString() ?? text;\n            if (state.content != text) {\n              emit(state.copyWith(content: text));\n              await cellController.saveCellData(text);\n\n              // If the input content is \"abc\" that can't parsered as number\n              // then the data stored in the backend will be an empty string.\n              // So for every cell data that will be formatted in the backend.\n              // It needs to get the formatted data after saving.\n              add(\n                TimeCellEvent.didReceiveCellUpdate(\n                  cellController.getCellData(),\n                ),\n              );\n            }\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _onCellChangedFn = cellController.addListener(\n      onCellChanged: (cellContent) {\n        if (!isClosed) {\n          add(TimeCellEvent.didReceiveCellUpdate(cellContent));\n        }\n      },\n      onFieldChanged: _onFieldChangedListener,\n    );\n  }\n\n  void _onFieldChangedListener(FieldInfo fieldInfo) {\n    if (!isClosed) {\n      add(TimeCellEvent.didUpdateField(fieldInfo));\n    }\n  }\n}\n\n@freezed\nclass TimeCellEvent with _$TimeCellEvent {\n  const factory TimeCellEvent.didReceiveCellUpdate(TimeCellDataPB? cell) =\n      _DidReceiveCellUpdate;\n  const factory TimeCellEvent.didUpdateField(FieldInfo fieldInfo) =\n      _DidUpdateField;\n  const factory TimeCellEvent.updateCell(String text) = _UpdateCell;\n}\n\n@freezed\nclass TimeCellState with _$TimeCellState {\n  const factory TimeCellState({\n    required String content,\n    required bool wrap,\n  }) = _TimeCellState;\n\n  factory TimeCellState.initial(TimeCellController cellController) {\n    final wrap = cellController.fieldInfo.wrapCellContent;\n    final cellData = cellController.getCellData();\n    return TimeCellState(\n      content: cellData != null ? formatTime(cellData.time.toInt()) : \"\",\n      wrap: wrap ?? true,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'timestamp_cell_bloc.freezed.dart';\n\nclass TimestampCellBloc extends Bloc<TimestampCellEvent, TimestampCellState> {\n  TimestampCellBloc({\n    required this.cellController,\n  }) : super(TimestampCellState.initial(cellController)) {\n    _dispatch();\n    _startListening();\n  }\n\n  final TimestampCellController cellController;\n  void Function()? _onCellChangedFn;\n\n  @override\n  Future<void> close() async {\n    if (_onCellChangedFn != null) {\n      cellController.removeListener(\n        onCellChanged: _onCellChangedFn!,\n        onFieldChanged: _onFieldChangedListener,\n      );\n    }\n    await cellController.dispose();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<TimestampCellEvent>(\n      (event, emit) async {\n        event.when(\n          didReceiveCellUpdate: (TimestampCellDataPB? cellData) {\n            emit(\n              state.copyWith(\n                data: cellData,\n                dateStr: cellData?.dateTime ?? \"\",\n              ),\n            );\n          },\n          didUpdateField: (fieldInfo) {\n            final wrap = fieldInfo.wrapCellContent;\n            if (wrap != null) {\n              emit(state.copyWith(wrap: wrap));\n            }\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _onCellChangedFn = cellController.addListener(\n      onCellChanged: (data) {\n        if (!isClosed) {\n          add(TimestampCellEvent.didReceiveCellUpdate(data));\n        }\n      },\n      onFieldChanged: _onFieldChangedListener,\n    );\n  }\n\n  void _onFieldChangedListener(FieldInfo fieldInfo) {\n    if (!isClosed) {\n      add(TimestampCellEvent.didUpdateField(fieldInfo));\n    }\n  }\n}\n\n@freezed\nclass TimestampCellEvent with _$TimestampCellEvent {\n  const factory TimestampCellEvent.didReceiveCellUpdate(\n    TimestampCellDataPB? data,\n  ) = _DidReceiveCellUpdate;\n  const factory TimestampCellEvent.didUpdateField(FieldInfo fieldInfo) =\n      _DidUpdateField;\n}\n\n@freezed\nclass TimestampCellState with _$TimestampCellState {\n  const factory TimestampCellState({\n    required TimestampCellDataPB? data,\n    required String dateStr,\n    required FieldInfo fieldInfo,\n    required bool wrap,\n  }) = _TimestampCellState;\n\n  factory TimestampCellState.initial(TimestampCellController cellController) {\n    final cellData = cellController.getCellData();\n    final wrap = cellController.fieldInfo.wrapCellContent;\n\n    return TimestampCellState(\n      fieldInfo: cellController.fieldInfo,\n      data: cellData,\n      dateStr: cellData?.dateTime ?? \"\",\n      wrap: wrap ?? true,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/translate_cell_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'translate_cell_bloc.freezed.dart';\n\nclass TranslateCellBloc extends Bloc<TranslateCellEvent, TranslateCellState> {\n  TranslateCellBloc({\n    required this.cellController,\n  }) : super(TranslateCellState.initial(cellController)) {\n    _dispatch();\n    _startListening();\n  }\n\n  final TranslateCellController cellController;\n  void Function()? _onCellChangedFn;\n\n  @override\n  Future<void> close() async {\n    if (_onCellChangedFn != null) {\n      cellController.removeListener(\n        onCellChanged: _onCellChangedFn!,\n        onFieldChanged: _onFieldChangedListener,\n      );\n    }\n    await cellController.dispose();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<TranslateCellEvent>(\n      (event, emit) async {\n        await event.when(\n          didReceiveCellUpdate: (cellData) {\n            emit(\n              state.copyWith(content: cellData ?? \"\"),\n            );\n          },\n          didUpdateField: (fieldInfo) {\n            final wrap = fieldInfo.wrapCellContent;\n            if (wrap != null) {\n              emit(state.copyWith(wrap: wrap));\n            }\n          },\n          updateCell: (text) async {\n            if (state.content != text) {\n              emit(state.copyWith(content: text));\n              await cellController.saveCellData(text);\n\n              // If the input content is \"abc\" that can't parsered as number then the data stored in the backend will be an empty string.\n              // So for every cell data that will be formatted in the backend.\n              // It needs to get the formatted data after saving.\n              add(\n                TranslateCellEvent.didReceiveCellUpdate(\n                  cellController.getCellData() ?? \"\",\n                ),\n              );\n            }\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _onCellChangedFn = cellController.addListener(\n      onCellChanged: (cellContent) {\n        if (!isClosed) {\n          add(\n            TranslateCellEvent.didReceiveCellUpdate(cellContent ?? \"\"),\n          );\n        }\n      },\n      onFieldChanged: _onFieldChangedListener,\n    );\n  }\n\n  void _onFieldChangedListener(FieldInfo fieldInfo) {\n    if (!isClosed) {\n      add(TranslateCellEvent.didUpdateField(fieldInfo));\n    }\n  }\n}\n\n@freezed\nclass TranslateCellEvent with _$TranslateCellEvent {\n  const factory TranslateCellEvent.didReceiveCellUpdate(String? cellContent) =\n      _DidReceiveCellUpdate;\n  const factory TranslateCellEvent.didUpdateField(FieldInfo fieldInfo) =\n      _DidUpdateField;\n  const factory TranslateCellEvent.updateCell(String text) = _UpdateCell;\n}\n\n@freezed\nclass TranslateCellState with _$TranslateCellState {\n  const factory TranslateCellState({\n    required String content,\n    required bool wrap,\n  }) = _TranslateCellState;\n\n  factory TranslateCellState.initial(TranslateCellController cellController) {\n    final wrap = cellController.fieldInfo.wrapCellContent;\n    return TranslateCellState(\n      content: cellController.getCellData() ?? \"\",\n      wrap: wrap ?? true,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/translate_row_bloc.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'translate_row_bloc.freezed.dart';\n\nclass TranslateRowBloc extends Bloc<TranslateRowEvent, TranslateRowState> {\n  TranslateRowBloc({\n    required this.viewId,\n    required this.rowId,\n    required this.fieldId,\n  }) : super(TranslateRowState.initial()) {\n    _dispatch();\n  }\n\n  final String viewId;\n  final String rowId;\n  final String fieldId;\n\n  void _dispatch() {\n    on<TranslateRowEvent>(\n      (event, emit) async {\n        event.when(\n          startTranslate: () {\n            final params = TranslateRowPB(\n              viewId: viewId,\n              rowId: rowId,\n              fieldId: fieldId,\n            );\n            emit(\n              state.copyWith(\n                loadingState: const LoadingState.loading(),\n                error: null,\n              ),\n            );\n\n            DatabaseEventTranslateRow(params).send().then(\n                  (result) => {\n                    if (!isClosed)\n                      add(TranslateRowEvent.finishTranslate(result)),\n                  },\n                );\n          },\n          finishTranslate: (result) {\n            result.fold(\n              (s) => {\n                emit(\n                  state.copyWith(\n                    loadingState: const LoadingState.finish(),\n                    error: null,\n                  ),\n                ),\n              },\n              (err) => {\n                emit(\n                  state.copyWith(\n                    loadingState: const LoadingState.finish(),\n                    error: err,\n                  ),\n                ),\n              },\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\n@freezed\nclass TranslateRowEvent with _$TranslateRowEvent {\n  const factory TranslateRowEvent.startTranslate() = _DidStartTranslate;\n  const factory TranslateRowEvent.finishTranslate(\n    FlowyResult<void, FlowyError> result,\n  ) = _DidFinishTranslate;\n}\n\n@freezed\nclass TranslateRowState with _$TranslateRowState {\n  const factory TranslateRowState({\n    required LoadingState loadingState,\n    required FlowyError? error,\n  }) = _TranslateRowState;\n\n  factory TranslateRowState.initial() {\n    return const TranslateRowState(\n      loadingState: LoadingState.finish(),\n      error: null,\n    );\n  }\n}\n\n@freezed\nclass LoadingState with _$LoadingState {\n  const factory LoadingState.loading() = _Loading;\n  const factory LoadingState.finish() = _Finish;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/url_cell_bloc.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'url_cell_bloc.freezed.dart';\n\nclass URLCellBloc extends Bloc<URLCellEvent, URLCellState> {\n  URLCellBloc({\n    required this.cellController,\n  }) : super(URLCellState.initial(cellController)) {\n    _dispatch();\n    _startListening();\n  }\n\n  final URLCellController cellController;\n  void Function()? _onCellChangedFn;\n\n  @override\n  Future<void> close() async {\n    if (_onCellChangedFn != null) {\n      cellController.removeListener(\n        onCellChanged: _onCellChangedFn!,\n        onFieldChanged: _onFieldChangedListener,\n      );\n    }\n    await cellController.dispose();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<URLCellEvent>(\n      (event, emit) async {\n        await event.when(\n          didUpdateCell: (cellData) async {\n            final content = cellData?.content ?? \"\";\n            final isValid = await _isUrlValid(content);\n            emit(\n              state.copyWith(\n                content: content,\n                isValid: isValid,\n              ),\n            );\n          },\n          didUpdateField: (fieldInfo) {\n            final wrap = fieldInfo.wrapCellContent;\n            if (wrap != null) {\n              emit(state.copyWith(wrap: wrap));\n            }\n          },\n          updateURL: (String url) {\n            cellController.saveCellData(url, debounce: true);\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _onCellChangedFn = cellController.addListener(\n      onCellChanged: (cellData) {\n        if (!isClosed) {\n          add(URLCellEvent.didUpdateCell(cellData));\n        }\n      },\n      onFieldChanged: _onFieldChangedListener,\n    );\n  }\n\n  void _onFieldChangedListener(FieldInfo fieldInfo) {\n    if (!isClosed) {\n      add(URLCellEvent.didUpdateField(fieldInfo));\n    }\n  }\n\n  Future<bool> _isUrlValid(String content) async {\n    if (content.isEmpty) {\n      return true;\n    }\n\n    try {\n      // check protocol is provided\n      const linkPrefix = [\n        'http://',\n        'https://',\n      ];\n      final shouldAddScheme =\n          !linkPrefix.any((pattern) => content.startsWith(pattern));\n      final url = shouldAddScheme ? 'http://$content' : content;\n\n      // get hostname and check validity\n      final uri = Uri.parse(url);\n      final hostName = uri.host;\n      await InternetAddress.lookup(hostName);\n    } catch (_) {\n      return false;\n    }\n    return true;\n  }\n}\n\n@freezed\nclass URLCellEvent with _$URLCellEvent {\n  const factory URLCellEvent.updateURL(String url) = _UpdateURL;\n  const factory URLCellEvent.didUpdateCell(URLCellDataPB? cell) =\n      _DidUpdateCell;\n  const factory URLCellEvent.didUpdateField(FieldInfo fieldInfo) =\n      _DidUpdateField;\n}\n\n@freezed\nclass URLCellState with _$URLCellState {\n  const factory URLCellState({\n    required String content,\n    required bool isValid,\n    required bool wrap,\n  }) = _URLCellState;\n\n  factory URLCellState.initial(URLCellController cellController) {\n    final cellData = cellController.getCellData();\n    final wrap = cellController.fieldInfo.wrapCellContent;\n    return URLCellState(\n      content: cellData?.content ?? \"\",\n      isValid: true,\n      wrap: wrap ?? true,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_cache.dart",
    "content": "import 'package:appflowy/plugins/database/application/row/row_service.dart';\n\nimport 'cell_controller.dart';\n\n/// CellMemCache is used to cache cell data of each block.\n/// We use CellContext to index the cell in the cache.\n/// Read https://docs.appflowy.io/docs/documentation/software-contributions/architecture/frontend/frontend/grid\n/// for more information\nclass CellMemCache {\n  CellMemCache();\n\n  /// fieldId: {rowId: cellData}\n  final Map<String, Map<RowId, dynamic>> _cellByFieldId = {};\n\n  void removeCellWithFieldId(String fieldId) {\n    _cellByFieldId.remove(fieldId);\n  }\n\n  void remove(CellContext context) {\n    _cellByFieldId[context.fieldId]?.remove(context.rowId);\n  }\n\n  void insert<T>(CellContext context, T data) {\n    _cellByFieldId.putIfAbsent(context.fieldId, () => {});\n    _cellByFieldId[context.fieldId]![context.rowId] = data;\n  }\n\n  T? get<T>(CellContext context) {\n    final value = _cellByFieldId[context.fieldId]?[context.rowId];\n    return value is T ? value : null;\n  }\n\n  void dispose() {\n    _cellByFieldId.clear();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/domain/cell_listener.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';\nimport 'package:appflowy/plugins/database/application/row/row_cache.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport 'cell_cache.dart';\nimport 'cell_data_loader.dart';\nimport 'cell_data_persistence.dart';\n\npart 'cell_controller.freezed.dart';\n\n@freezed\nclass CellContext with _$CellContext {\n  const factory CellContext({\n    required String fieldId,\n    required RowId rowId,\n  }) = _DatabaseCellContext;\n}\n\n/// [CellController] is used to manipulate the cell and receive notifications.\n/// The cell data is stored in the [RowCache]'s [CellMemCache].\n///\n/// * Read/write cell data\n/// * Listen on field/cell notifications.\n///\n/// T represents the type of the cell data.\n/// D represents the type of data that will be saved to the disk.\nclass CellController<T, D> {\n  CellController({\n    required this.viewId,\n    required FieldController fieldController,\n    required CellContext cellContext,\n    required RowCache rowCache,\n    required CellDataLoader<T> cellDataLoader,\n    required CellDataPersistence<D> cellDataPersistence,\n  })  : _fieldController = fieldController,\n        _cellContext = cellContext,\n        _rowCache = rowCache,\n        _cellDataLoader = cellDataLoader,\n        _cellDataPersistence = cellDataPersistence,\n        _cellDataNotifier =\n            CellDataNotifier(value: rowCache.cellCache.get(cellContext)) {\n    _startListening();\n  }\n\n  final String viewId;\n  final FieldController _fieldController;\n  final CellContext _cellContext;\n  final RowCache _rowCache;\n  final CellDataLoader<T> _cellDataLoader;\n  final CellDataPersistence<D> _cellDataPersistence;\n\n  CellListener? _cellListener;\n  CellDataNotifier<T?>? _cellDataNotifier;\n\n  Timer? _loadDataOperation;\n  Timer? _saveDataOperation;\n\n  Completer? _completer;\n\n  RowId get rowId => _cellContext.rowId;\n  String get fieldId => _cellContext.fieldId;\n  FieldInfo get fieldInfo => _fieldController.getField(_cellContext.fieldId)!;\n  FieldType get fieldType =>\n      _fieldController.getField(_cellContext.fieldId)!.fieldType;\n  ValueNotifier<String>? get icon => _rowCache.getRow(rowId)?.rowIconNotifier;\n  ValueNotifier<bool>? get hasDocument =>\n      _rowCache.getRow(rowId)?.rowDocumentNotifier;\n  CellMemCache get _cellCache => _rowCache.cellCache;\n\n  /// casting method for painless type coersion\n  CellController<A, B> as<A, B>() => this as CellController<A, B>;\n\n  /// Start listening to backend changes\n  void _startListening() {\n    _cellListener = CellListener(\n      rowId: _cellContext.rowId,\n      fieldId: _cellContext.fieldId,\n    );\n\n    // 1. Listen on user edit event and load the new cell data if needed.\n    // For example:\n    //  user input: 12\n    //  cell display: $12\n    _cellListener?.start(\n      onCellChanged: (result) {\n        result.fold(\n          (_) => _loadData(),\n          (err) => Log.error(err),\n        );\n      },\n    );\n\n    // 2. Listen on the field event and load the cell data if needed.\n    _fieldController.addSingleFieldListener(\n      fieldId,\n      onFieldChanged: _onFieldChangedListener,\n    );\n  }\n\n  /// Add a new listener\n  VoidCallback? addListener({\n    required void Function(T?) onCellChanged,\n    void Function(FieldInfo fieldInfo)? onFieldChanged,\n  }) {\n    /// an adaptor for the onCellChanged listener\n    void onCellChangedFn() => onCellChanged(_cellDataNotifier?.value);\n    _cellDataNotifier?.addListener(onCellChangedFn);\n\n    if (onFieldChanged != null) {\n      _fieldController.addSingleFieldListener(\n        fieldId,\n        onFieldChanged: onFieldChanged,\n      );\n    }\n\n    // Return the function pointer that can be used when calling removeListener.\n    return onCellChangedFn;\n  }\n\n  void removeListener({\n    required VoidCallback onCellChanged,\n    void Function(FieldInfo fieldInfo)? onFieldChanged,\n    VoidCallback? onRowMetaChanged,\n  }) {\n    _cellDataNotifier?.removeListener(onCellChanged);\n\n    if (onFieldChanged != null) {\n      _fieldController.removeSingleFieldListener(\n        fieldId: fieldId,\n        onFieldChanged: onFieldChanged,\n      );\n    }\n  }\n\n  void _onFieldChangedListener(FieldInfo fieldInfo) {\n    // reloadOnFieldChanged should be true if you want to reload the cell\n    // data when the corresponding field is changed.\n    // For example:\n    //   ￥12 -> $12\n    if (_cellDataLoader.reloadOnFieldChange) {\n      _loadData();\n    }\n  }\n\n  /// Get the cell data. The cell data will be read from the cache first,\n  /// and load from disk if it doesn't exist. You can set [loadIfNotExist] to\n  /// false to disable this behavior.\n  T? getCellData({bool loadIfNotExist = true}) {\n    final T? data = _cellCache.get(_cellContext);\n    if (data == null && loadIfNotExist) {\n      _loadData();\n    }\n    return data;\n  }\n\n  /// Return the TypeOptionPB that can be parsed into corresponding class using the [parser].\n  /// [PD] is the type that the parser return.\n  PD getTypeOption<PD>(TypeOptionParser parser) {\n    return parser.fromBuffer(fieldInfo.field.typeOptionData);\n  }\n\n  /// Saves the cell data to disk. You can set [debounce] to reduce the amount\n  /// of save operations, which is useful when editing a [TextField].\n  Future<void> saveCellData(\n    D data, {\n    bool debounce = false,\n    void Function(FlowyError?)? onFinish,\n  }) async {\n    _loadDataOperation?.cancel();\n    if (debounce) {\n      _saveDataOperation?.cancel();\n      _completer = Completer();\n      _saveDataOperation = Timer(const Duration(milliseconds: 300), () async {\n        final result = await _cellDataPersistence.save(\n          viewId: viewId,\n          cellContext: _cellContext,\n          data: data,\n        );\n        onFinish?.call(result);\n        _completer?.complete();\n      });\n    } else {\n      final result = await _cellDataPersistence.save(\n        viewId: viewId,\n        cellContext: _cellContext,\n        data: data,\n      );\n      onFinish?.call(result);\n    }\n  }\n\n  void _loadData() {\n    _saveDataOperation?.cancel();\n    _loadDataOperation?.cancel();\n\n    _loadDataOperation = Timer(const Duration(milliseconds: 10), () {\n      _cellDataLoader\n          .loadData(viewId: viewId, cellContext: _cellContext)\n          .then((data) {\n        if (data != null) {\n          _cellCache.insert(_cellContext, data);\n        } else {\n          _cellCache.remove(_cellContext);\n        }\n        _cellDataNotifier?.value = data;\n      });\n    });\n  }\n\n  Future<void> dispose() async {\n    await _cellListener?.stop();\n    _cellListener = null;\n\n    _fieldController.removeSingleFieldListener(\n      fieldId: fieldId,\n      onFieldChanged: _onFieldChangedListener,\n    );\n\n    _loadDataOperation?.cancel();\n    await _completer?.future;\n    _saveDataOperation?.cancel();\n    _cellDataNotifier?.dispose();\n    _cellDataNotifier = null;\n  }\n}\n\nclass CellDataNotifier<T> extends ChangeNotifier {\n  CellDataNotifier({required T value, this.listenWhen}) : _value = value;\n\n  T _value;\n  bool Function(T? oldValue, T? newValue)? listenWhen;\n\n  set value(T newValue) {\n    if (listenWhen != null && !listenWhen!.call(_value, newValue)) {\n      return;\n    }\n    _value = newValue;\n    notifyListeners();\n  }\n\n  T get value => _value;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart",
    "content": "import 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\n\nimport 'cell_controller.dart';\nimport 'cell_data_loader.dart';\nimport 'cell_data_persistence.dart';\n\ntypedef TextCellController = CellController<String, String>;\ntypedef CheckboxCellController = CellController<CheckboxCellDataPB, String>;\ntypedef NumberCellController = CellController<String, String>;\ntypedef SelectOptionCellController\n    = CellController<SelectOptionCellDataPB, String>;\ntypedef ChecklistCellController = CellController<ChecklistCellDataPB, String>;\ntypedef DateCellController = CellController<DateCellDataPB, String>;\ntypedef TimestampCellController = CellController<TimestampCellDataPB, String>;\ntypedef URLCellController = CellController<URLCellDataPB, String>;\ntypedef RelationCellController = CellController<RelationCellDataPB, String>;\ntypedef SummaryCellController = CellController<String, String>;\ntypedef TimeCellController = CellController<TimeCellDataPB, String>;\ntypedef TranslateCellController = CellController<String, String>;\ntypedef MediaCellController = CellController<MediaCellDataPB, String>;\n\nCellController makeCellController(\n  DatabaseController databaseController,\n  CellContext cellContext,\n) {\n  final DatabaseController(:viewId, :rowCache, :fieldController) =\n      databaseController;\n  final fieldType = fieldController.getField(cellContext.fieldId)!.fieldType;\n  switch (fieldType) {\n    case FieldType.Checkbox:\n      return CheckboxCellController(\n        viewId: viewId,\n        fieldController: fieldController,\n        cellContext: cellContext,\n        rowCache: rowCache,\n        cellDataLoader: CellDataLoader(\n          parser: CheckboxCellDataParser(),\n        ),\n        cellDataPersistence: TextCellDataPersistence(),\n      );\n    case FieldType.DateTime:\n      return DateCellController(\n        viewId: viewId,\n        fieldController: fieldController,\n        cellContext: cellContext,\n        rowCache: rowCache,\n        cellDataLoader: CellDataLoader(\n          parser: DateCellDataParser(),\n          reloadOnFieldChange: true,\n        ),\n        cellDataPersistence: TextCellDataPersistence(),\n      );\n    case FieldType.LastEditedTime:\n    case FieldType.CreatedTime:\n      return TimestampCellController(\n        viewId: viewId,\n        fieldController: fieldController,\n        cellContext: cellContext,\n        rowCache: rowCache,\n        cellDataLoader: CellDataLoader(\n          parser: TimestampCellDataParser(),\n          reloadOnFieldChange: true,\n        ),\n        cellDataPersistence: TextCellDataPersistence(),\n      );\n    case FieldType.Number:\n      return NumberCellController(\n        viewId: viewId,\n        fieldController: fieldController,\n        cellContext: cellContext,\n        rowCache: rowCache,\n        cellDataLoader: CellDataLoader(\n          parser: NumberCellDataParser(),\n          reloadOnFieldChange: true,\n        ),\n        cellDataPersistence: TextCellDataPersistence(),\n      );\n    case FieldType.RichText:\n      return TextCellController(\n        viewId: viewId,\n        fieldController: fieldController,\n        cellContext: cellContext,\n        rowCache: rowCache,\n        cellDataLoader: CellDataLoader(\n          parser: StringCellDataParser(),\n        ),\n        cellDataPersistence: TextCellDataPersistence(),\n      );\n    case FieldType.MultiSelect:\n    case FieldType.SingleSelect:\n      return SelectOptionCellController(\n        viewId: viewId,\n        fieldController: fieldController,\n        cellContext: cellContext,\n        rowCache: rowCache,\n        cellDataLoader: CellDataLoader(\n          parser: SelectOptionCellDataParser(),\n          reloadOnFieldChange: true,\n        ),\n        cellDataPersistence: TextCellDataPersistence(),\n      );\n    case FieldType.Checklist:\n      return ChecklistCellController(\n        viewId: viewId,\n        fieldController: fieldController,\n        cellContext: cellContext,\n        rowCache: rowCache,\n        cellDataLoader: CellDataLoader(\n          parser: ChecklistCellDataParser(),\n          reloadOnFieldChange: true,\n        ),\n        cellDataPersistence: TextCellDataPersistence(),\n      );\n    case FieldType.URL:\n      return URLCellController(\n        viewId: viewId,\n        fieldController: fieldController,\n        cellContext: cellContext,\n        rowCache: rowCache,\n        cellDataLoader: CellDataLoader(\n          parser: URLCellDataParser(),\n        ),\n        cellDataPersistence: TextCellDataPersistence(),\n      );\n    case FieldType.Relation:\n      return RelationCellController(\n        viewId: viewId,\n        fieldController: fieldController,\n        cellContext: cellContext,\n        rowCache: rowCache,\n        cellDataLoader: CellDataLoader(\n          parser: RelationCellDataParser(),\n          reloadOnFieldChange: true,\n        ),\n        cellDataPersistence: TextCellDataPersistence(),\n      );\n    case FieldType.Summary:\n      return SummaryCellController(\n        viewId: viewId,\n        fieldController: fieldController,\n        cellContext: cellContext,\n        rowCache: rowCache,\n        cellDataLoader: CellDataLoader(\n          parser: StringCellDataParser(),\n          reloadOnFieldChange: true,\n        ),\n        cellDataPersistence: TextCellDataPersistence(),\n      );\n    case FieldType.Time:\n      return TimeCellController(\n        viewId: viewId,\n        fieldController: fieldController,\n        cellContext: cellContext,\n        rowCache: rowCache,\n        cellDataLoader: CellDataLoader(\n          parser: TimeCellDataParser(),\n          reloadOnFieldChange: true,\n        ),\n        cellDataPersistence: TextCellDataPersistence(),\n      );\n    case FieldType.Translate:\n      return TranslateCellController(\n        viewId: viewId,\n        fieldController: fieldController,\n        cellContext: cellContext,\n        rowCache: rowCache,\n        cellDataLoader: CellDataLoader(\n          parser: StringCellDataParser(),\n          reloadOnFieldChange: true,\n        ),\n        cellDataPersistence: TextCellDataPersistence(),\n      );\n    case FieldType.Media:\n      return MediaCellController(\n        viewId: viewId,\n        fieldController: fieldController,\n        cellContext: cellContext,\n        rowCache: rowCache,\n        cellDataLoader: CellDataLoader(\n          parser: MediaCellDataParser(),\n          reloadOnFieldChange: true,\n        ),\n        cellDataPersistence: TextCellDataPersistence(),\n      );\n  }\n  throw UnimplementedError;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/plugins/database/domain/cell_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\n\nimport 'cell_controller.dart';\n\nabstract class CellDataParser<T> {\n  T? parserData(List<int> data);\n}\n\nclass CellDataLoader<T> {\n  CellDataLoader({\n    required this.parser,\n    this.reloadOnFieldChange = false,\n  });\n\n  final CellDataParser<T> parser;\n\n  /// Reload the cell data if the field is changed.\n  final bool reloadOnFieldChange;\n\n  Future<T?> loadData({\n    required String viewId,\n    required CellContext cellContext,\n  }) {\n    return CellBackendService.getCell(\n      viewId: viewId,\n      cellContext: cellContext,\n    ).then(\n      (result) => result.fold(\n        (CellPB cell) {\n          try {\n            return parser.parserData(cell.data);\n          } catch (e, s) {\n            Log.error('$parser parser cellData failed, $e');\n            Log.error('Stack trace \\n $s');\n            return null;\n          }\n        },\n        (err) {\n          Log.error(err);\n          return null;\n        },\n      ),\n    );\n  }\n}\n\nclass StringCellDataParser implements CellDataParser<String> {\n  @override\n  String? parserData(List<int> data) {\n    try {\n      final s = utf8.decode(data);\n      return s;\n    } catch (e) {\n      Log.error(\"Failed to parse string data: $e\");\n      return null;\n    }\n  }\n}\n\nclass CheckboxCellDataParser implements CellDataParser<CheckboxCellDataPB> {\n  @override\n  CheckboxCellDataPB? parserData(List<int> data) {\n    if (data.isEmpty) {\n      return null;\n    }\n\n    try {\n      return CheckboxCellDataPB.fromBuffer(data);\n    } catch (e) {\n      Log.error(\"Failed to parse checkbox data: $e\");\n      return null;\n    }\n  }\n}\n\nclass NumberCellDataParser implements CellDataParser<String> {\n  @override\n  String? parserData(List<int> data) {\n    try {\n      return utf8.decode(data);\n    } catch (e) {\n      Log.error(\"Failed to parse number data: $e\");\n      return null;\n    }\n  }\n}\n\nclass DateCellDataParser implements CellDataParser<DateCellDataPB> {\n  @override\n  DateCellDataPB? parserData(List<int> data) {\n    if (data.isEmpty) {\n      return null;\n    }\n    try {\n      return DateCellDataPB.fromBuffer(data);\n    } catch (e) {\n      Log.error(\"Failed to parse date data: $e\");\n      return null;\n    }\n  }\n}\n\nclass TimestampCellDataParser implements CellDataParser<TimestampCellDataPB> {\n  @override\n  TimestampCellDataPB? parserData(List<int> data) {\n    if (data.isEmpty) {\n      return null;\n    }\n    try {\n      return TimestampCellDataPB.fromBuffer(data);\n    } catch (e) {\n      Log.error(\"Failed to parse timestamp data: $e\");\n      return null;\n    }\n  }\n}\n\nclass SelectOptionCellDataParser\n    implements CellDataParser<SelectOptionCellDataPB> {\n  @override\n  SelectOptionCellDataPB? parserData(List<int> data) {\n    if (data.isEmpty) {\n      return null;\n    }\n    try {\n      return SelectOptionCellDataPB.fromBuffer(data);\n    } catch (e) {\n      Log.error(\"Failed to parse select option data: $e\");\n      return null;\n    }\n  }\n}\n\nclass ChecklistCellDataParser implements CellDataParser<ChecklistCellDataPB> {\n  @override\n  ChecklistCellDataPB? parserData(List<int> data) {\n    if (data.isEmpty) {\n      return null;\n    }\n\n    try {\n      return ChecklistCellDataPB.fromBuffer(data);\n    } catch (e) {\n      Log.error(\"Failed to parse checklist data: $e\");\n      return null;\n    }\n  }\n}\n\nclass URLCellDataParser implements CellDataParser<URLCellDataPB> {\n  @override\n  URLCellDataPB? parserData(List<int> data) {\n    if (data.isEmpty) {\n      return null;\n    }\n    try {\n      return URLCellDataPB.fromBuffer(data);\n    } catch (e) {\n      Log.error(\"Failed to parse url data: $e\");\n      return null;\n    }\n  }\n}\n\nclass RelationCellDataParser implements CellDataParser<RelationCellDataPB> {\n  @override\n  RelationCellDataPB? parserData(List<int> data) {\n    if (data.isEmpty) {\n      return null;\n    }\n\n    try {\n      return RelationCellDataPB.fromBuffer(data);\n    } catch (e) {\n      Log.error(\"Failed to parse relation data: $e\");\n      return null;\n    }\n  }\n}\n\nclass TimeCellDataParser implements CellDataParser<TimeCellDataPB> {\n  @override\n  TimeCellDataPB? parserData(List<int> data) {\n    if (data.isEmpty) {\n      return null;\n    }\n    try {\n      return TimeCellDataPB.fromBuffer(data);\n    } catch (e) {\n      Log.error(\"Failed to parse timer data: $e\");\n      return null;\n    }\n  }\n}\n\nclass MediaCellDataParser implements CellDataParser<MediaCellDataPB> {\n  @override\n  MediaCellDataPB? parserData(List<int> data) {\n    if (data.isEmpty) {\n      return null;\n    }\n\n    try {\n      return MediaCellDataPB.fromBuffer(data);\n    } catch (e) {\n      Log.error(\"Failed to parse media cell data: $e\");\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_persistence.dart",
    "content": "import 'package:appflowy/plugins/database/domain/cell_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';\n\nimport 'cell_controller.dart';\n\n/// Save the cell data to disk\n/// You can extend this class to do custom operations.\nabstract class CellDataPersistence<D> {\n  Future<FlowyError?> save({\n    required String viewId,\n    required CellContext cellContext,\n    required D data,\n  });\n}\n\nclass TextCellDataPersistence implements CellDataPersistence<String> {\n  TextCellDataPersistence();\n\n  @override\n  Future<FlowyError?> save({\n    required String viewId,\n    required CellContext cellContext,\n    required String data,\n  }) async {\n    final fut = CellBackendService.updateCell(\n      viewId: viewId,\n      cellContext: cellContext,\n      data: data,\n    );\n    return fut.then((result) {\n      return result.fold(\n        (l) => null,\n        (err) => err,\n      );\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/database_controller.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/view/view_cache.dart';\nimport 'package:appflowy/plugins/database/domain/database_view_service.dart';\nimport 'package:appflowy/plugins/database/domain/group_listener.dart';\nimport 'package:appflowy/plugins/database/domain/layout_service.dart';\nimport 'package:appflowy/plugins/database/domain/layout_setting_listener.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/material.dart';\n\nimport 'defines.dart';\nimport 'row/row_cache.dart';\n\ntypedef OnGroupConfigurationChanged = void Function(List<GroupSettingPB>);\ntypedef OnGroupByField = void Function(List<GroupPB>);\ntypedef OnUpdateGroup = void Function(List<GroupPB>);\ntypedef OnDeleteGroup = void Function(List<String>);\ntypedef OnInsertGroup = void Function(InsertedGroupPB);\n\nclass GroupCallbacks {\n  GroupCallbacks({\n    this.onGroupConfigurationChanged,\n    this.onGroupByField,\n    this.onUpdateGroup,\n    this.onDeleteGroup,\n    this.onInsertGroup,\n  });\n\n  final OnGroupConfigurationChanged? onGroupConfigurationChanged;\n  final OnGroupByField? onGroupByField;\n  final OnUpdateGroup? onUpdateGroup;\n  final OnDeleteGroup? onDeleteGroup;\n  final OnInsertGroup? onInsertGroup;\n}\n\nclass DatabaseLayoutSettingCallbacks {\n  DatabaseLayoutSettingCallbacks({\n    required this.onLayoutSettingsChanged,\n  });\n\n  final void Function(DatabaseLayoutSettingPB) onLayoutSettingsChanged;\n}\n\nclass DatabaseCallbacks {\n  DatabaseCallbacks({\n    this.onDatabaseChanged,\n    this.onNumOfRowsChanged,\n    this.onFieldsChanged,\n    this.onFiltersChanged,\n    this.onSortsChanged,\n    this.onRowsUpdated,\n    this.onRowsDeleted,\n    this.onRowsCreated,\n  });\n\n  OnDatabaseChanged? onDatabaseChanged;\n  OnFieldsChanged? onFieldsChanged;\n  OnFiltersChanged? onFiltersChanged;\n  OnSortsChanged? onSortsChanged;\n  OnNumOfRowsChanged? onNumOfRowsChanged;\n  OnRowsDeleted? onRowsDeleted;\n  OnRowsUpdated? onRowsUpdated;\n  OnRowsCreated? onRowsCreated;\n}\n\nclass DatabaseController {\n  DatabaseController({required this.view})\n      : _databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),\n        fieldController = FieldController(viewId: view.id),\n        _groupListener = DatabaseGroupListener(view.id),\n        databaseLayout = databaseLayoutFromViewLayout(view.layout),\n        _layoutListener = DatabaseLayoutSettingListener(view.id) {\n    _viewCache = DatabaseViewCache(\n      viewId: viewId,\n      fieldController: fieldController,\n    );\n\n    _listenOnRowsChanged();\n    _listenOnFieldsChanged();\n    _listenOnGroupChanged();\n    _listenOnLayoutChanged();\n  }\n\n  final ViewPB view;\n  final DatabaseViewBackendService _databaseViewBackendSvc;\n  final FieldController fieldController;\n  DatabaseLayoutPB databaseLayout;\n  DatabaseLayoutSettingPB? databaseLayoutSetting;\n  late DatabaseViewCache _viewCache;\n\n  // Callbacks\n  final List<DatabaseCallbacks> _databaseCallbacks = [];\n  final List<GroupCallbacks> _groupCallbacks = [];\n  final List<DatabaseLayoutSettingCallbacks> _layoutCallbacks = [];\n  final Set<ValueChanged<bool>> _compactModeCallbacks = {};\n\n  // Getters\n  RowCache get rowCache => _viewCache.rowCache;\n\n  String get viewId => view.id;\n\n  // Listener\n  final DatabaseGroupListener _groupListener;\n  final DatabaseLayoutSettingListener _layoutListener;\n\n  final ValueNotifier<bool> _isLoading = ValueNotifier(true);\n  final ValueNotifier<bool> _compactMode = ValueNotifier(true);\n\n  void setIsLoading(bool isLoading) => _isLoading.value = isLoading;\n\n  ValueNotifier<bool> get isLoading => _isLoading;\n\n  void setCompactMode(bool compactMode) {\n    _compactMode.value = compactMode;\n    for (final callback in Set.of(_compactModeCallbacks)) {\n      callback.call(compactMode);\n    }\n  }\n\n  ValueNotifier<bool> get compactModeNotifier => _compactMode;\n\n  void addListener({\n    DatabaseCallbacks? onDatabaseChanged,\n    DatabaseLayoutSettingCallbacks? onLayoutSettingsChanged,\n    GroupCallbacks? onGroupChanged,\n    ValueChanged<bool>? onCompactModeChanged,\n  }) {\n    if (onLayoutSettingsChanged != null) {\n      _layoutCallbacks.add(onLayoutSettingsChanged);\n    }\n\n    if (onDatabaseChanged != null) {\n      _databaseCallbacks.add(onDatabaseChanged);\n    }\n\n    if (onGroupChanged != null) {\n      _groupCallbacks.add(onGroupChanged);\n    }\n\n    if (onCompactModeChanged != null) {\n      _compactModeCallbacks.add(onCompactModeChanged);\n    }\n  }\n\n  void removeListener({\n    DatabaseCallbacks? onDatabaseChanged,\n    DatabaseLayoutSettingCallbacks? onLayoutSettingsChanged,\n    GroupCallbacks? onGroupChanged,\n    ValueChanged<bool>? onCompactModeChanged,\n  }) {\n    if (onDatabaseChanged != null) {\n      _databaseCallbacks.remove(onDatabaseChanged);\n    }\n\n    if (onLayoutSettingsChanged != null) {\n      _layoutCallbacks.remove(onLayoutSettingsChanged);\n    }\n\n    if (onGroupChanged != null) {\n      _groupCallbacks.remove(onGroupChanged);\n    }\n\n    if (onCompactModeChanged != null) {\n      _compactModeCallbacks.remove(onCompactModeChanged);\n    }\n  }\n\n  Future<FlowyResult<void, FlowyError>> open() async {\n    return _databaseViewBackendSvc.openDatabase().then((result) {\n      return result.fold(\n        (DatabasePB database) async {\n          databaseLayout = database.layoutType;\n\n          // Load the actual database field data.\n          final fieldsOrFail = await fieldController.loadFields(\n            fieldIds: database.fields,\n          );\n          return fieldsOrFail.fold(\n            (fields) {\n              // Notify the database is changed after the fields are loaded.\n              // The database won't can't be used until the fields are loaded.\n              for (final callback in _databaseCallbacks) {\n                callback.onDatabaseChanged?.call(database);\n              }\n              _viewCache.rowCache.setInitialRows(database.rows);\n              return Future(() async {\n                await _loadGroups();\n                await _loadLayoutSetting();\n                return FlowyResult.success(fields);\n              });\n            },\n            (err) {\n              Log.error(err);\n              return FlowyResult.failure(err);\n            },\n          );\n        },\n        (err) => FlowyResult.failure(err),\n      );\n    });\n  }\n\n  Future<FlowyResult<void, FlowyError>> moveGroupRow({\n    required RowMetaPB fromRow,\n    required String fromGroupId,\n    required String toGroupId,\n    RowMetaPB? toRow,\n  }) {\n    return _databaseViewBackendSvc.moveGroupRow(\n      fromRowId: fromRow.id,\n      fromGroupId: fromGroupId,\n      toGroupId: toGroupId,\n      toRowId: toRow?.id,\n    );\n  }\n\n  Future<FlowyResult<void, FlowyError>> moveRow({\n    required String fromRowId,\n    required String toRowId,\n  }) {\n    return _databaseViewBackendSvc.moveRow(\n      fromRowId: fromRowId,\n      toRowId: toRowId,\n    );\n  }\n\n  Future<FlowyResult<void, FlowyError>> moveGroup({\n    required String fromGroupId,\n    required String toGroupId,\n  }) {\n    return _databaseViewBackendSvc.moveGroup(\n      fromGroupId: fromGroupId,\n      toGroupId: toGroupId,\n    );\n  }\n\n  Future<void> updateLayoutSetting({\n    BoardLayoutSettingPB? boardLayoutSetting,\n    CalendarLayoutSettingPB? calendarLayoutSetting,\n  }) async {\n    await _databaseViewBackendSvc\n        .updateLayoutSetting(\n      boardLayoutSetting: boardLayoutSetting,\n      calendarLayoutSetting: calendarLayoutSetting,\n      layoutType: databaseLayout,\n    )\n        .then((result) {\n      result.fold((l) => null, (r) => Log.error(r));\n    });\n  }\n\n  Future<void> dispose() async {\n    await _databaseViewBackendSvc.closeView();\n    await fieldController.dispose();\n    await _groupListener.stop();\n    await _viewCache.dispose();\n    _databaseCallbacks.clear();\n    _groupCallbacks.clear();\n    _layoutCallbacks.clear();\n    _compactModeCallbacks.clear();\n    _isLoading.dispose();\n  }\n\n  Future<void> _loadGroups() async {\n    final groupsResult = await _databaseViewBackendSvc.loadGroups();\n    groupsResult.fold(\n      (groups) {\n        for (final callback in _groupCallbacks) {\n          callback.onGroupByField?.call(groups.items);\n        }\n      },\n      (err) => Log.error(err),\n    );\n  }\n\n  Future<void> _loadLayoutSetting() {\n    return _databaseViewBackendSvc\n        .getLayoutSetting(databaseLayout)\n        .then((result) {\n      result.fold(\n        (newDatabaseLayoutSetting) {\n          databaseLayoutSetting = newDatabaseLayoutSetting;\n\n          for (final callback in _layoutCallbacks) {\n            callback.onLayoutSettingsChanged(newDatabaseLayoutSetting);\n          }\n        },\n        (r) => Log.error(r),\n      );\n    });\n  }\n\n  void _listenOnRowsChanged() {\n    final callbacks = DatabaseViewCallbacks(\n      onNumOfRowsChanged: (rows, rowByRowId, reason) {\n        for (final callback in _databaseCallbacks) {\n          callback.onNumOfRowsChanged?.call(rows, rowByRowId, reason);\n        }\n      },\n      onRowsDeleted: (ids) {\n        for (final callback in _databaseCallbacks) {\n          callback.onRowsDeleted?.call(ids);\n        }\n      },\n      onRowsUpdated: (ids, reason) {\n        for (final callback in _databaseCallbacks) {\n          callback.onRowsUpdated?.call(ids, reason);\n        }\n      },\n      onRowsCreated: (ids) {\n        for (final callback in _databaseCallbacks) {\n          callback.onRowsCreated?.call(ids);\n        }\n      },\n    );\n    _viewCache.addListener(callbacks);\n  }\n\n  void _listenOnFieldsChanged() {\n    fieldController.addListener(\n      onReceiveFields: (fields) {\n        for (final callback in _databaseCallbacks) {\n          callback.onFieldsChanged?.call(UnmodifiableListView(fields));\n        }\n      },\n      onSorts: (sorts) {\n        for (final callback in _databaseCallbacks) {\n          callback.onSortsChanged?.call(sorts);\n        }\n      },\n      onFilters: (filters) {\n        for (final callback in _databaseCallbacks) {\n          callback.onFiltersChanged?.call(filters);\n        }\n      },\n    );\n  }\n\n  void _listenOnGroupChanged() {\n    _groupListener.start(\n      onNumOfGroupsChanged: (result) {\n        result.fold(\n          (changeset) {\n            if (changeset.updateGroups.isNotEmpty) {\n              for (final callback in _groupCallbacks) {\n                callback.onUpdateGroup?.call(changeset.updateGroups);\n              }\n            }\n\n            if (changeset.deletedGroups.isNotEmpty) {\n              for (final callback in _groupCallbacks) {\n                callback.onDeleteGroup?.call(changeset.deletedGroups);\n              }\n            }\n\n            for (final insertedGroup in changeset.insertedGroups) {\n              for (final callback in _groupCallbacks) {\n                callback.onInsertGroup?.call(insertedGroup);\n              }\n            }\n          },\n          (r) => Log.error(r),\n        );\n      },\n      onGroupByNewField: (result) {\n        result.fold(\n          (groups) {\n            for (final callback in _groupCallbacks) {\n              callback.onGroupByField?.call(groups);\n            }\n          },\n          (r) => Log.error(r),\n        );\n      },\n    );\n  }\n\n  void _listenOnLayoutChanged() {\n    _layoutListener.start(\n      onLayoutChanged: (result) {\n        result.fold(\n          (newLayout) {\n            databaseLayoutSetting = newLayout;\n            databaseLayoutSetting?.freeze();\n\n            for (final callback in _layoutCallbacks) {\n              callback.onLayoutSettingsChanged(newLayout);\n            }\n          },\n          (r) => Log.error(r),\n        );\n      },\n    );\n  }\n\n  void initCompactMode(bool enableCompactMode) {\n    if (_compactMode.value != enableCompactMode) {\n      _compactMode.value = enableCompactMode;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/defines.dart",
    "content": "import 'dart:collection';\n\nimport 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport 'field/field_info.dart';\nimport 'field/sort_entities.dart';\nimport 'row/row_cache.dart';\nimport 'row/row_service.dart';\n\npart 'defines.freezed.dart';\n\ntypedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);\ntypedef OnFiltersChanged = void Function(List<DatabaseFilter>);\ntypedef OnSortsChanged = void Function(List<DatabaseSort>);\ntypedef OnDatabaseChanged = void Function(DatabasePB);\n\ntypedef OnRowsCreated = void Function(List<InsertedRowPB> rows);\ntypedef OnRowsUpdated = void Function(\n  List<RowId> rowIds,\n  ChangedReason reason,\n);\ntypedef OnRowsDeleted = void Function(List<RowId> rowIds);\ntypedef OnNumOfRowsChanged = void Function(\n  UnmodifiableListView<RowInfo> rows,\n  UnmodifiableMapView<RowId, RowInfo> rowById,\n  ChangedReason reason,\n);\ntypedef OnRowsVisibilityChanged = void Function(\n  List<(RowId, bool)> rowVisibilityChanges,\n);\n\n@freezed\nclass LoadingState with _$LoadingState {\n  const factory LoadingState.idle() = _Idle;\n  const factory LoadingState.loading() = _Loading;\n  const factory LoadingState.finish(\n    FlowyResult<void, FlowyError> successOrFail,\n  ) = _Finish;\n\n  const LoadingState._();\n  bool isLoading() => this is _Loading;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/field/field_cell_bloc.dart",
    "content": "import 'dart:math';\n\nimport 'package:appflowy/plugins/database/domain/field_settings_service.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport 'field_info.dart';\n\npart 'field_cell_bloc.freezed.dart';\n\nclass FieldCellBloc extends Bloc<FieldCellEvent, FieldCellState> {\n  FieldCellBloc({required String viewId, required FieldInfo fieldInfo})\n      : _fieldSettingsService = FieldSettingsBackendService(viewId: viewId),\n        super(FieldCellState.initial(fieldInfo)) {\n    _dispatch();\n  }\n\n  final FieldSettingsBackendService _fieldSettingsService;\n\n  void _dispatch() {\n    on<FieldCellEvent>(\n      (event, emit) async {\n        event.when(\n          onFieldChanged: (newFieldInfo) =>\n              emit(FieldCellState.initial(newFieldInfo)),\n          onResizeStart: () =>\n              emit(state.copyWith(isResizing: true, resizeStart: state.width)),\n          startUpdateWidth: (offset) {\n            final width = max(offset + state.resizeStart, 50).toDouble();\n            emit(state.copyWith(width: width));\n          },\n          endUpdateWidth: () {\n            if (state.width != state.fieldInfo.width) {\n              _fieldSettingsService.updateFieldSettings(\n                fieldId: state.fieldInfo.id,\n                width: state.width,\n              );\n            }\n            emit(state.copyWith(isResizing: false, resizeStart: 0));\n          },\n        );\n      },\n    );\n  }\n}\n\n@freezed\nclass FieldCellEvent with _$FieldCellEvent {\n  const factory FieldCellEvent.onFieldChanged(FieldInfo newFieldInfo) =\n      _OnFieldChanged;\n  const factory FieldCellEvent.onResizeStart() = _OnResizeStart;\n  const factory FieldCellEvent.startUpdateWidth(double offset) =\n      _StartUpdateWidth;\n  const factory FieldCellEvent.endUpdateWidth() = _EndUpdateWidth;\n}\n\n@freezed\nclass FieldCellState with _$FieldCellState {\n  factory FieldCellState.initial(FieldInfo fieldInfo) => FieldCellState(\n        fieldInfo: fieldInfo,\n        isResizing: false,\n        width: fieldInfo.width!.toDouble(),\n        resizeStart: 0,\n      );\n\n  const factory FieldCellState({\n    required FieldInfo fieldInfo,\n    required double width,\n    required bool isResizing,\n    required double resizeStart,\n  }) = _FieldCellState;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart",
    "content": "import 'dart:collection';\n\nimport 'package:appflowy/plugins/database/application/row/row_cache.dart';\nimport 'package:appflowy/plugins/database/application/setting/setting_listener.dart';\nimport 'package:appflowy/plugins/database/domain/database_view_service.dart';\nimport 'package:appflowy/plugins/database/domain/field_listener.dart';\nimport 'package:appflowy/plugins/database/domain/field_settings_listener.dart';\nimport 'package:appflowy/plugins/database/domain/field_settings_service.dart';\nimport 'package:appflowy/plugins/database/domain/filter_listener.dart';\nimport 'package:appflowy/plugins/database/domain/filter_service.dart';\nimport 'package:appflowy/plugins/database/domain/sort_listener.dart';\nimport 'package:appflowy/plugins/database/domain/sort_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/foundation.dart';\n\nimport '../setting/setting_service.dart';\nimport 'field_info.dart';\nimport 'filter_entities.dart';\nimport 'sort_entities.dart';\n\nclass _GridFieldNotifier extends ChangeNotifier {\n  List<FieldInfo> _fieldInfos = [];\n\n  set fieldInfos(List<FieldInfo> fieldInfos) {\n    _fieldInfos = fieldInfos;\n    notifyListeners();\n  }\n\n  void notify() {\n    notifyListeners();\n  }\n\n  UnmodifiableListView<FieldInfo> get fieldInfos =>\n      UnmodifiableListView(_fieldInfos);\n}\n\nclass _GridFilterNotifier extends ChangeNotifier {\n  List<DatabaseFilter> _filters = [];\n\n  set filters(List<DatabaseFilter> filters) {\n    _filters = filters;\n    notifyListeners();\n  }\n\n  void notify() {\n    notifyListeners();\n  }\n\n  List<DatabaseFilter> get filters => _filters;\n}\n\nclass _GridSortNotifier extends ChangeNotifier {\n  List<DatabaseSort> _sorts = [];\n\n  set sorts(List<DatabaseSort> sorts) {\n    _sorts = sorts;\n    notifyListeners();\n  }\n\n  void notify() {\n    notifyListeners();\n  }\n\n  List<DatabaseSort> get sorts => _sorts;\n}\n\ntypedef OnReceiveUpdateFields = void Function(List<FieldInfo>);\ntypedef OnReceiveField = void Function(FieldInfo);\ntypedef OnReceiveFields = void Function(List<FieldInfo>);\ntypedef OnReceiveFilters = void Function(List<DatabaseFilter>);\ntypedef OnReceiveSorts = void Function(List<DatabaseSort>);\n\nclass FieldController {\n  FieldController({required this.viewId})\n      : _fieldListener = FieldsListener(viewId: viewId),\n        _settingListener = DatabaseSettingListener(viewId: viewId),\n        _filterBackendSvc = FilterBackendService(viewId: viewId),\n        _filtersListener = FiltersListener(viewId: viewId),\n        _databaseViewBackendSvc = DatabaseViewBackendService(viewId: viewId),\n        _sortBackendSvc = SortBackendService(viewId: viewId),\n        _sortsListener = SortsListener(viewId: viewId),\n        _fieldSettingsListener = FieldSettingsListener(viewId: viewId),\n        _fieldSettingsBackendSvc = FieldSettingsBackendService(viewId: viewId) {\n    // Start listeners\n    _listenOnFieldChanges();\n    _listenOnSettingChanges();\n    _listenOnFilterChanges();\n    _listenOnSortChanged();\n    _listenOnFieldSettingsChanged();\n  }\n\n  final String viewId;\n\n  // Listeners\n  final FieldsListener _fieldListener;\n  final DatabaseSettingListener _settingListener;\n  final FiltersListener _filtersListener;\n  final SortsListener _sortsListener;\n  final FieldSettingsListener _fieldSettingsListener;\n\n  // FFI services\n  final DatabaseViewBackendService _databaseViewBackendSvc;\n  final FilterBackendService _filterBackendSvc;\n  final SortBackendService _sortBackendSvc;\n  final FieldSettingsBackendService _fieldSettingsBackendSvc;\n\n  bool _isDisposed = false;\n\n  // Field callbacks\n  final Map<OnReceiveFields, VoidCallback> _fieldCallbacks = {};\n  final _GridFieldNotifier _fieldNotifier = _GridFieldNotifier();\n\n  // Field updated callbacks\n  final Map<OnReceiveUpdateFields, void Function(List<FieldInfo>)>\n      _updatedFieldCallbacks = {};\n\n  // Filter callbacks\n  final Map<OnReceiveFilters, VoidCallback> _filterCallbacks = {};\n  _GridFilterNotifier? _filterNotifier = _GridFilterNotifier();\n\n  // Sort callbacks\n  final Map<OnReceiveSorts, VoidCallback> _sortCallbacks = {};\n  _GridSortNotifier? _sortNotifier = _GridSortNotifier();\n\n  // Database settings temporary storage\n  final Map<String, GroupSettingPB> _groupConfigurationByFieldId = {};\n  final List<FieldSettingsPB> _fieldSettings = [];\n\n  // Getters\n  List<FieldInfo> get fieldInfos => [..._fieldNotifier.fieldInfos];\n  List<DatabaseFilter> get filters => [..._filterNotifier?.filters ?? []];\n  List<DatabaseSort> get sorts => [..._sortNotifier?.sorts ?? []];\n  List<GroupSettingPB> get groupSettings =>\n      _groupConfigurationByFieldId.entries.map((e) => e.value).toList();\n\n  FieldInfo? getField(String fieldId) {\n    return _fieldNotifier.fieldInfos\n        .firstWhereOrNull((element) => element.id == fieldId);\n  }\n\n  DatabaseFilter? getFilterByFilterId(String filterId) {\n    return _filterNotifier?.filters\n        .firstWhereOrNull((element) => element.filterId == filterId);\n  }\n\n  DatabaseFilter? getFilterByFieldId(String fieldId) {\n    return _filterNotifier?.filters\n        .firstWhereOrNull((element) => element.fieldId == fieldId);\n  }\n\n  DatabaseSort? getSortBySortId(String sortId) {\n    return _sortNotifier?.sorts\n        .firstWhereOrNull((element) => element.sortId == sortId);\n  }\n\n  DatabaseSort? getSortByFieldId(String fieldId) {\n    return _sortNotifier?.sorts\n        .firstWhereOrNull((element) => element.fieldId == fieldId);\n  }\n\n  /// Listen for filter changes in the backend.\n  void _listenOnFilterChanges() {\n    _filtersListener.start(\n      onFilterChanged: (result) {\n        if (_isDisposed) {\n          return;\n        }\n\n        result.fold(\n          (FilterChangesetNotificationPB changeset) {\n            _filterNotifier?.filters =\n                _filterListFromPBs(changeset.filters.items);\n            _fieldNotifier.fieldInfos =\n                _updateFieldInfos(_fieldNotifier.fieldInfos);\n          },\n          (err) => Log.error(err),\n        );\n      },\n    );\n  }\n\n  /// Listen for sort changes in the backend.\n  void _listenOnSortChanged() {\n    void deleteSortFromChangeset(\n      List<DatabaseSort> newDatabaseSorts,\n      SortChangesetNotificationPB changeset,\n    ) {\n      final deleteSortIds = changeset.deleteSorts.map((e) => e.id).toList();\n      if (deleteSortIds.isNotEmpty) {\n        newDatabaseSorts.retainWhere(\n          (element) => !deleteSortIds.contains(element.sortId),\n        );\n      }\n    }\n\n    void insertSortFromChangeset(\n      List<DatabaseSort> newDatabaseSorts,\n      SortChangesetNotificationPB changeset,\n    ) {\n      for (final newSortPB in changeset.insertSorts) {\n        final sortIndex = newDatabaseSorts\n            .indexWhere((element) => element.sortId == newSortPB.sort.id);\n        if (sortIndex == -1) {\n          newDatabaseSorts.insert(\n            newSortPB.index,\n            DatabaseSort.fromPB(newSortPB.sort),\n          );\n        }\n      }\n    }\n\n    void updateSortFromChangeset(\n      List<DatabaseSort> newDatabaseSorts,\n      SortChangesetNotificationPB changeset,\n    ) {\n      for (final updatedSort in changeset.updateSorts) {\n        final newDatabaseSort = DatabaseSort.fromPB(updatedSort);\n\n        final sortIndex = newDatabaseSorts.indexWhere(\n          (element) => element.sortId == updatedSort.id,\n        );\n\n        if (sortIndex != -1) {\n          newDatabaseSorts.removeAt(sortIndex);\n          newDatabaseSorts.insert(sortIndex, newDatabaseSort);\n        } else {\n          newDatabaseSorts.add(newDatabaseSort);\n        }\n      }\n    }\n\n    void updateFieldInfos(\n      List<DatabaseSort> newDatabaseSorts,\n      SortChangesetNotificationPB changeset,\n    ) {\n      final changedFieldIds = HashSet<String>.from([\n        ...changeset.insertSorts.map((sort) => sort.sort.fieldId),\n        ...changeset.updateSorts.map((sort) => sort.fieldId),\n        ...changeset.deleteSorts.map((sort) => sort.fieldId),\n        ...?_sortNotifier?.sorts.map((sort) => sort.fieldId),\n      ]);\n\n      final newFieldInfos = [...fieldInfos];\n\n      for (final fieldId in changedFieldIds) {\n        final index =\n            newFieldInfos.indexWhere((fieldInfo) => fieldInfo.id == fieldId);\n        if (index == -1) {\n          continue;\n        }\n        newFieldInfos[index] = newFieldInfos[index].copyWith(\n          hasSort: newDatabaseSorts.any((sort) => sort.fieldId == fieldId),\n        );\n      }\n\n      _fieldNotifier.fieldInfos = newFieldInfos;\n    }\n\n    _sortsListener.start(\n      onSortChanged: (result) {\n        if (_isDisposed) {\n          return;\n        }\n        result.fold(\n          (SortChangesetNotificationPB changeset) {\n            final List<DatabaseSort> newDatabaseSorts = sorts;\n            deleteSortFromChangeset(newDatabaseSorts, changeset);\n            insertSortFromChangeset(newDatabaseSorts, changeset);\n            updateSortFromChangeset(newDatabaseSorts, changeset);\n\n            updateFieldInfos(newDatabaseSorts, changeset);\n            _sortNotifier?.sorts = newDatabaseSorts;\n          },\n          (err) => Log.error(err),\n        );\n      },\n    );\n  }\n\n  /// Listen for database setting changes in the backend.\n  void _listenOnSettingChanges() {\n    _settingListener.start(\n      onSettingUpdated: (result) {\n        if (_isDisposed) {\n          return;\n        }\n\n        result.fold(\n          (setting) => _updateSetting(setting),\n          (r) => Log.error(r),\n        );\n      },\n    );\n  }\n\n  /// Listen for field changes in the backend.\n  void _listenOnFieldChanges() {\n    Future<FieldInfo> attachFieldSettings(FieldInfo fieldInfo) async {\n      return _fieldSettingsBackendSvc\n          .getFieldSettings(fieldInfo.id)\n          .then((result) {\n        final fieldSettings = result.fold(\n          (fieldSettings) => fieldSettings,\n          (err) => null,\n        );\n        if (fieldSettings == null) {\n          return fieldInfo;\n        }\n        final updatedFieldInfo =\n            fieldInfo.copyWith(fieldSettings: fieldSettings);\n\n        final index = _fieldSettings\n            .indexWhere((element) => element.fieldId == fieldInfo.id);\n        if (index != -1) {\n          _fieldSettings.removeAt(index);\n        }\n        _fieldSettings.add(fieldSettings);\n\n        return updatedFieldInfo;\n      });\n    }\n\n    List<FieldInfo> deleteFields(List<FieldIdPB> deletedFields) {\n      if (deletedFields.isEmpty) {\n        return fieldInfos;\n      }\n      final List<FieldInfo> newFields = fieldInfos;\n      final Map<String, FieldIdPB> deletedFieldMap = {\n        for (final fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder,\n      };\n\n      newFields.retainWhere((field) => deletedFieldMap[field.id] == null);\n      return newFields;\n    }\n\n    Future<List<FieldInfo>> insertFields(\n      List<IndexFieldPB> insertedFields,\n      List<FieldInfo> fieldInfos,\n    ) async {\n      if (insertedFields.isEmpty) {\n        return fieldInfos;\n      }\n      final List<FieldInfo> newFieldInfos = fieldInfos;\n      for (final indexField in insertedFields) {\n        final initial = FieldInfo.initial(indexField.field_1);\n        final fieldInfo = await attachFieldSettings(initial);\n        if (newFieldInfos.length > indexField.index) {\n          newFieldInfos.insert(indexField.index, fieldInfo);\n        } else {\n          newFieldInfos.add(fieldInfo);\n        }\n      }\n      return newFieldInfos;\n    }\n\n    Future<(List<FieldInfo>, List<FieldInfo>)> updateFields(\n      List<FieldPB> updatedFieldPBs,\n      List<FieldInfo> fieldInfos,\n    ) async {\n      if (updatedFieldPBs.isEmpty) {\n        return (<FieldInfo>[], fieldInfos);\n      }\n\n      final List<FieldInfo> newFieldInfo = fieldInfos;\n      final List<FieldInfo> updatedFields = [];\n      for (final updatedFieldPB in updatedFieldPBs) {\n        final index =\n            newFieldInfo.indexWhere((field) => field.id == updatedFieldPB.id);\n        if (index != -1) {\n          newFieldInfo.removeAt(index);\n          final initial = FieldInfo.initial(updatedFieldPB);\n          final fieldInfo = await attachFieldSettings(initial);\n          newFieldInfo.insert(index, fieldInfo);\n          updatedFields.add(fieldInfo);\n        }\n      }\n\n      return (updatedFields, newFieldInfo);\n    }\n\n    // Listen on field's changes\n    _fieldListener.start(\n      onFieldsChanged: (result) async {\n        result.fold(\n          (changeset) async {\n            if (_isDisposed) {\n              return;\n            }\n            List<FieldInfo> updatedFields;\n            List<FieldInfo> fieldInfos = deleteFields(changeset.deletedFields);\n            fieldInfos =\n                await insertFields(changeset.insertedFields, fieldInfos);\n            (updatedFields, fieldInfos) =\n                await updateFields(changeset.updatedFields, fieldInfos);\n\n            _fieldNotifier.fieldInfos = _updateFieldInfos(fieldInfos);\n            for (final listener in _updatedFieldCallbacks.values) {\n              listener(updatedFields);\n            }\n          },\n          (err) => Log.error(err),\n        );\n      },\n    );\n  }\n\n  /// Listen for field setting changes in the backend.\n  void _listenOnFieldSettingsChanged() {\n    FieldInfo? updateFieldSettings(FieldSettingsPB updatedFieldSettings) {\n      final newFields = [...fieldInfos];\n\n      if (newFields.isEmpty) {\n        return null;\n      }\n\n      final index = newFields\n          .indexWhere((field) => field.id == updatedFieldSettings.fieldId);\n\n      if (index != -1) {\n        newFields[index] =\n            newFields[index].copyWith(fieldSettings: updatedFieldSettings);\n        _fieldNotifier.fieldInfos = newFields;\n        _fieldSettings\n          ..removeWhere(\n            (field) => field.fieldId == updatedFieldSettings.fieldId,\n          )\n          ..add(updatedFieldSettings);\n        return newFields[index];\n      }\n\n      return null;\n    }\n\n    _fieldSettingsListener.start(\n      onFieldSettingsChanged: (result) {\n        if (_isDisposed) {\n          return;\n        }\n        result.fold(\n          (fieldSettings) {\n            final updatedFieldInfo = updateFieldSettings(fieldSettings);\n            if (updatedFieldInfo == null) {\n              return;\n            }\n\n            for (final listener in _updatedFieldCallbacks.values) {\n              listener([updatedFieldInfo]);\n            }\n          },\n          (err) => Log.error(err),\n        );\n      },\n    );\n  }\n\n  /// Updates sort, filter, group and field info from `DatabaseViewSettingPB`\n  void _updateSetting(DatabaseViewSettingPB setting) {\n    _groupConfigurationByFieldId.clear();\n    for (final configuration in setting.groupSettings.items) {\n      _groupConfigurationByFieldId[configuration.fieldId] = configuration;\n    }\n\n    _filterNotifier?.filters = _filterListFromPBs(setting.filters.items);\n\n    _sortNotifier?.sorts = _sortListFromPBs(setting.sorts.items);\n\n    _fieldSettings.clear();\n    _fieldSettings.addAll(setting.fieldSettings.items);\n\n    _fieldNotifier.fieldInfos = _updateFieldInfos(_fieldNotifier.fieldInfos);\n  }\n\n  /// Attach sort, filter, group information and field settings to `FieldInfo`\n  List<FieldInfo> _updateFieldInfos(List<FieldInfo> fieldInfos) {\n    return fieldInfos\n        .map(\n          (field) => field.copyWith(\n            fieldSettings: _fieldSettings\n                .firstWhereOrNull((setting) => setting.fieldId == field.id),\n            isGroupField: _groupConfigurationByFieldId[field.id] != null,\n            hasFilter: getFilterByFieldId(field.id) != null,\n            hasSort: getSortByFieldId(field.id) != null,\n          ),\n        )\n        .toList();\n  }\n\n  /// Load all of the fields. This is required when opening the database\n  Future<FlowyResult<void, FlowyError>> loadFields({\n    required List<FieldIdPB> fieldIds,\n  }) async {\n    final result = await _databaseViewBackendSvc.getFields(fieldIds: fieldIds);\n    return Future(\n      () => result.fold(\n        (newFields) async {\n          if (_isDisposed) {\n            return FlowyResult.success(null);\n          }\n\n          _fieldNotifier.fieldInfos =\n              newFields.map((field) => FieldInfo.initial(field)).toList();\n          await Future.wait([\n            _loadFilters(),\n            _loadSorts(),\n            _loadAllFieldSettings(),\n            _loadSettings(),\n          ]);\n          _fieldNotifier.fieldInfos =\n              _updateFieldInfos(_fieldNotifier.fieldInfos);\n\n          return FlowyResult.success(null);\n        },\n        (err) => FlowyResult.failure(err),\n      ),\n    );\n  }\n\n  /// Load all the filters from the backend. Required by `loadFields`\n  Future<FlowyResult<void, FlowyError>> _loadFilters() async {\n    return _filterBackendSvc.getAllFilters().then((result) {\n      return result.fold(\n        (filterPBs) {\n          _filterNotifier?.filters = _filterListFromPBs(filterPBs);\n          return FlowyResult.success(null);\n        },\n        (err) => FlowyResult.failure(err),\n      );\n    });\n  }\n\n  /// Load all the sorts from the backend. Required by `loadFields`\n  Future<FlowyResult<void, FlowyError>> _loadSorts() async {\n    return _sortBackendSvc.getAllSorts().then((result) {\n      return result.fold(\n        (sortPBs) {\n          _sortNotifier?.sorts = _sortListFromPBs(sortPBs);\n          return FlowyResult.success(null);\n        },\n        (err) => FlowyResult.failure(err),\n      );\n    });\n  }\n\n  /// Load all the field settings from the backend. Required by `loadFields`\n  Future<FlowyResult<void, FlowyError>> _loadAllFieldSettings() async {\n    return _fieldSettingsBackendSvc.getAllFieldSettings().then((result) {\n      return result.fold(\n        (fieldSettingsList) {\n          _fieldSettings.clear();\n          _fieldSettings.addAll(fieldSettingsList);\n          return FlowyResult.success(null);\n        },\n        (err) => FlowyResult.failure(err),\n      );\n    });\n  }\n\n  Future<FlowyResult<void, FlowyError>> _loadSettings() async {\n    return SettingBackendService(viewId: viewId).getSetting().then(\n          (result) => result.fold(\n            (setting) {\n              _groupConfigurationByFieldId.clear();\n              for (final configuration in setting.groupSettings.items) {\n                _groupConfigurationByFieldId[configuration.fieldId] =\n                    configuration;\n              }\n              return FlowyResult.success(null);\n            },\n            (err) => FlowyResult.failure(err),\n          ),\n        );\n  }\n\n  /// Attach corresponding `FieldInfo`s to the `FilterPB`s\n  List<DatabaseFilter> _filterListFromPBs(List<FilterPB> filterPBs) {\n    return filterPBs.map(DatabaseFilter.fromPB).toList();\n  }\n\n  /// Attach corresponding `FieldInfo`s to the `SortPB`s\n  List<DatabaseSort> _sortListFromPBs(List<SortPB> sortPBs) {\n    return sortPBs.map(DatabaseSort.fromPB).toList();\n  }\n\n  void addListener({\n    OnReceiveFields? onReceiveFields,\n    OnReceiveUpdateFields? onFieldsChanged,\n    OnReceiveFilters? onFilters,\n    OnReceiveSorts? onSorts,\n    bool Function()? listenWhen,\n  }) {\n    if (onFieldsChanged != null) {\n      void callback(List<FieldInfo> updateFields) {\n        if (listenWhen != null && listenWhen() == false) {\n          return;\n        }\n        onFieldsChanged(updateFields);\n      }\n\n      _updatedFieldCallbacks[onFieldsChanged] = callback;\n    }\n\n    if (onReceiveFields != null) {\n      void callback() {\n        if (listenWhen != null && listenWhen() == false) {\n          return;\n        }\n        onReceiveFields(fieldInfos);\n      }\n\n      _fieldCallbacks[onReceiveFields] = callback;\n      _fieldNotifier.addListener(callback);\n    }\n\n    if (onFilters != null) {\n      void callback() {\n        if (listenWhen != null && listenWhen() == false) {\n          return;\n        }\n        onFilters(filters);\n      }\n\n      _filterCallbacks[onFilters] = callback;\n      _filterNotifier?.addListener(callback);\n    }\n\n    if (onSorts != null) {\n      void callback() {\n        if (listenWhen != null && listenWhen() == false) {\n          return;\n        }\n        onSorts(sorts);\n      }\n\n      _sortCallbacks[onSorts] = callback;\n      _sortNotifier?.addListener(callback);\n    }\n  }\n\n  void addSingleFieldListener(\n    String fieldId, {\n    required OnReceiveField onFieldChanged,\n    bool Function()? listenWhen,\n  }) {\n    void key(List<FieldInfo> fieldInfos) {\n      final fieldInfo = fieldInfos.firstWhereOrNull(\n        (fieldInfo) => fieldInfo.id == fieldId,\n      );\n      if (fieldInfo != null) {\n        onFieldChanged(fieldInfo);\n      }\n    }\n\n    void callback() {\n      if (listenWhen != null && listenWhen() == false) {\n        return;\n      }\n      key(fieldInfos);\n    }\n\n    _fieldCallbacks[key] = callback;\n    _fieldNotifier.addListener(callback);\n  }\n\n  void removeListener({\n    OnReceiveFields? onFieldsListener,\n    OnReceiveSorts? onSortsListener,\n    OnReceiveFilters? onFiltersListener,\n    OnReceiveUpdateFields? onChangesetListener,\n  }) {\n    if (onFieldsListener != null) {\n      final callback = _fieldCallbacks.remove(onFieldsListener);\n      if (callback != null) {\n        _fieldNotifier.removeListener(callback);\n      }\n    }\n    if (onFiltersListener != null) {\n      final callback = _filterCallbacks.remove(onFiltersListener);\n      if (callback != null) {\n        _filterNotifier?.removeListener(callback);\n      }\n    }\n\n    if (onSortsListener != null) {\n      final callback = _sortCallbacks.remove(onSortsListener);\n      if (callback != null) {\n        _sortNotifier?.removeListener(callback);\n      }\n    }\n  }\n\n  void removeSingleFieldListener({\n    required String fieldId,\n    required OnReceiveField onFieldChanged,\n  }) {\n    void key(List<FieldInfo> fieldInfos) {\n      final fieldInfo = fieldInfos.firstWhereOrNull(\n        (fieldInfo) => fieldInfo.id == fieldId,\n      );\n      if (fieldInfo != null) {\n        onFieldChanged(fieldInfo);\n      }\n    }\n\n    final callback = _fieldCallbacks.remove(key);\n    if (callback != null) {\n      _fieldNotifier.removeListener(callback);\n    }\n  }\n\n  /// Stop listeners, dispose notifiers and clear listener callbacks\n  Future<void> dispose() async {\n    if (_isDisposed) {\n      Log.warn('FieldController is already disposed');\n      return;\n    }\n    _isDisposed = true;\n    await _fieldListener.stop();\n    await _filtersListener.stop();\n    await _settingListener.stop();\n    await _sortsListener.stop();\n    await _fieldSettingsListener.stop();\n\n    for (final callback in _fieldCallbacks.values) {\n      _fieldNotifier.removeListener(callback);\n    }\n    _fieldNotifier.dispose();\n\n    for (final callback in _filterCallbacks.values) {\n      _filterNotifier?.removeListener(callback);\n    }\n    _filterNotifier?.dispose();\n    _filterNotifier = null;\n\n    for (final callback in _sortCallbacks.values) {\n      _sortNotifier?.removeListener(callback);\n    }\n    _sortNotifier?.dispose();\n    _sortNotifier = null;\n  }\n}\n\nclass RowCacheDependenciesImpl extends RowFieldsDelegate with RowLifeCycle {\n  RowCacheDependenciesImpl(FieldController cache) : _fieldController = cache;\n\n  final FieldController _fieldController;\n  OnReceiveFields? _onFieldFn;\n\n  @override\n  UnmodifiableListView<FieldInfo> get fieldInfos =>\n      UnmodifiableListView(_fieldController.fieldInfos);\n\n  @override\n  void onFieldsChanged(void Function(List<FieldInfo>) callback) {\n    if (_onFieldFn != null) {\n      _fieldController.removeListener(onFieldsListener: _onFieldFn!);\n    }\n\n    _onFieldFn = (fieldInfos) => callback(fieldInfos);\n    _fieldController.addListener(onReceiveFields: _onFieldFn);\n  }\n\n  @override\n  void onRowDisposed() {\n    if (_onFieldFn != null) {\n      _fieldController.removeListener(onFieldsListener: _onFieldFn!);\n      _onFieldFn = null;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/field/field_editor_bloc.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/plugins/database/domain/field_settings_service.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport 'field_controller.dart';\nimport 'field_info.dart';\n\npart 'field_editor_bloc.freezed.dart';\n\nclass FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {\n  FieldEditorBloc({\n    required this.viewId,\n    required this.fieldInfo,\n    required this.fieldController,\n    this.onFieldInserted,\n    required this.isNew,\n  })  : _fieldService = FieldBackendService(\n          viewId: viewId,\n          fieldId: fieldInfo.id,\n        ),\n        fieldSettingsService = FieldSettingsBackendService(viewId: viewId),\n        super(FieldEditorState(field: fieldInfo)) {\n    _dispatch();\n    _startListening();\n    _init();\n  }\n\n  final String viewId;\n  final FieldInfo fieldInfo;\n  final bool isNew;\n  final FieldController fieldController;\n  final FieldBackendService _fieldService;\n  final FieldSettingsBackendService fieldSettingsService;\n  final void Function(String newFieldId)? onFieldInserted;\n\n  late final OnReceiveField _listener;\n\n  String get fieldId => fieldInfo.id;\n\n  @override\n  Future<void> close() {\n    fieldController.removeSingleFieldListener(\n      fieldId: fieldId,\n      onFieldChanged: _listener,\n    );\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<FieldEditorEvent>(\n      (event, emit) async {\n        await event.when(\n          didUpdateField: (fieldInfo) {\n            emit(state.copyWith(field: fieldInfo));\n          },\n          switchFieldType: (fieldType) async {\n            String? fieldName;\n            if (!state.wasRenameManually && isNew) {\n              fieldName = fieldType.i18n;\n            }\n\n            await _fieldService.updateType(\n              fieldType: fieldType,\n              fieldName: fieldName,\n            );\n          },\n          renameField: (newName) async {\n            final result = await _fieldService.updateField(name: newName);\n            _logIfError(result);\n            emit(state.copyWith(wasRenameManually: true));\n          },\n          updateIcon: (icon) async {\n            final result = await _fieldService.updateField(icon: icon);\n            _logIfError(result);\n          },\n          updateTypeOption: (typeOptionData) async {\n            final result = await FieldBackendService.updateFieldTypeOption(\n              viewId: viewId,\n              fieldId: fieldId,\n              typeOptionData: typeOptionData,\n            );\n            _logIfError(result);\n          },\n          insertLeft: () async {\n            final result = await _fieldService.createBefore();\n            result.fold(\n              (newField) => onFieldInserted?.call(newField.id),\n              (err) => Log.error(\"Failed creating field $err\"),\n            );\n          },\n          insertRight: () async {\n            final result = await _fieldService.createAfter();\n            result.fold(\n              (newField) => onFieldInserted?.call(newField.id),\n              (err) => Log.error(\"Failed creating field $err\"),\n            );\n          },\n          toggleFieldVisibility: () async {\n            final currentVisibility =\n                state.field.visibility ?? FieldVisibility.AlwaysShown;\n            final newVisibility =\n                currentVisibility == FieldVisibility.AlwaysHidden\n                    ? FieldVisibility.AlwaysShown\n                    : FieldVisibility.AlwaysHidden;\n            final result = await fieldSettingsService.updateFieldSettings(\n              fieldId: fieldId,\n              fieldVisibility: newVisibility,\n            );\n            _logIfError(result);\n          },\n          toggleWrapCellContent: () async {\n            final currentWrap = state.field.wrapCellContent ?? false;\n            final result = await fieldSettingsService.updateFieldSettings(\n              fieldId: state.field.id,\n              wrapCellContent: !currentWrap,\n            );\n            _logIfError(result);\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _listener = (field) {\n      if (!isClosed) {\n        add(FieldEditorEvent.didUpdateField(field));\n      }\n    };\n    fieldController.addSingleFieldListener(\n      fieldId,\n      onFieldChanged: _listener,\n    );\n  }\n\n  void _init() async {\n    await Future.delayed(const Duration(milliseconds: 50));\n    if (!isClosed) {\n      final field = fieldController.getField(fieldId);\n      if (field != null) {\n        add(FieldEditorEvent.didUpdateField(field));\n      }\n    }\n  }\n\n  void _logIfError(FlowyResult<void, FlowyError> result) {\n    result.fold(\n      (l) => null,\n      (err) => Log.error(err),\n    );\n  }\n}\n\n@freezed\nclass FieldEditorEvent with _$FieldEditorEvent {\n  const factory FieldEditorEvent.didUpdateField(final FieldInfo fieldInfo) =\n      _DidUpdateField;\n  const factory FieldEditorEvent.switchFieldType(final FieldType fieldType) =\n      _SwitchFieldType;\n  const factory FieldEditorEvent.updateTypeOption(\n    final Uint8List typeOptionData,\n  ) = _UpdateTypeOption;\n  const factory FieldEditorEvent.renameField(final String name) = _RenameField;\n  const factory FieldEditorEvent.updateIcon(String icon) = _UpdateIcon;\n  const factory FieldEditorEvent.insertLeft() = _InsertLeft;\n  const factory FieldEditorEvent.insertRight() = _InsertRight;\n  const factory FieldEditorEvent.toggleFieldVisibility() =\n      _ToggleFieldVisiblity;\n  const factory FieldEditorEvent.toggleWrapCellContent() =\n      _ToggleWrapCellContent;\n}\n\n@freezed\nclass FieldEditorState with _$FieldEditorState {\n  const factory FieldEditorState({\n    required final FieldInfo field,\n    @Default(false) bool wasRenameManually,\n  }) = _FieldEditorState;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'field_info.freezed.dart';\n\n@freezed\nclass FieldInfo with _$FieldInfo {\n  const FieldInfo._();\n\n  factory FieldInfo.initial(FieldPB field) => FieldInfo(\n        field: field,\n        fieldSettings: null,\n        hasFilter: false,\n        hasSort: false,\n        isGroupField: false,\n      );\n\n  const factory FieldInfo({\n    required FieldPB field,\n    required FieldSettingsPB? fieldSettings,\n    required bool isGroupField,\n    required bool hasFilter,\n    required bool hasSort,\n  }) = _FieldInfo;\n\n  String get id => field.id;\n\n  FieldType get fieldType => field.fieldType;\n\n  String get name => field.name;\n\n  String get icon => field.icon;\n\n  bool get isPrimary => field.isPrimary;\n\n  double? get width => fieldSettings?.width.toDouble();\n\n  FieldVisibility? get visibility => fieldSettings?.visibility;\n\n  bool? get wrapCellContent => fieldSettings?.wrapCellContent;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/field/filter_entities.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/database/view/database_filter_bottom_sheet.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/select_option_loader.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checklist.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/number.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';\nimport 'package:appflowy/util/int64_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:equatable/equatable.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/widgets.dart';\n\nabstract class DatabaseFilter extends Equatable {\n  const DatabaseFilter({\n    required this.filterId,\n    required this.fieldId,\n    required this.fieldType,\n  });\n\n  factory DatabaseFilter.fromPB(FilterPB filterPB) {\n    final FilterDataPB(:fieldId, :fieldType) = filterPB.data;\n    switch (fieldType) {\n      case FieldType.RichText:\n      case FieldType.URL:\n        final data = TextFilterPB.fromBuffer(filterPB.data.data);\n        return TextFilter(\n          filterId: filterPB.id,\n          fieldId: fieldId,\n          fieldType: fieldType,\n          condition: data.condition,\n          content: data.content,\n        );\n      case FieldType.Number:\n        final data = NumberFilterPB.fromBuffer(filterPB.data.data);\n        return NumberFilter(\n          filterId: filterPB.id,\n          fieldId: fieldId,\n          fieldType: fieldType,\n          condition: data.condition,\n          content: data.content,\n        );\n      case FieldType.Checkbox:\n        final data = CheckboxFilterPB.fromBuffer(filterPB.data.data);\n        return CheckboxFilter(\n          filterId: filterPB.id,\n          fieldId: fieldId,\n          fieldType: fieldType,\n          condition: data.condition,\n        );\n      case FieldType.Checklist:\n        final data = ChecklistFilterPB.fromBuffer(filterPB.data.data);\n        return ChecklistFilter(\n          filterId: filterPB.id,\n          fieldId: fieldId,\n          fieldType: fieldType,\n          condition: data.condition,\n        );\n      case FieldType.SingleSelect:\n      case FieldType.MultiSelect:\n        final data = SelectOptionFilterPB.fromBuffer(filterPB.data.data);\n        return SelectOptionFilter(\n          filterId: filterPB.id,\n          fieldId: fieldId,\n          fieldType: fieldType,\n          condition: data.condition,\n          optionIds: data.optionIds,\n        );\n      case FieldType.LastEditedTime:\n      case FieldType.CreatedTime:\n      case FieldType.DateTime:\n        final data = DateFilterPB.fromBuffer(filterPB.data.data);\n        return DateTimeFilter(\n          filterId: filterPB.id,\n          fieldId: fieldId,\n          fieldType: fieldType,\n          condition: data.condition,\n          timestamp: data.hasTimestamp() ? data.timestamp.toDateTime() : null,\n          start: data.hasStart() ? data.start.toDateTime() : null,\n          end: data.hasEnd() ? data.end.toDateTime() : null,\n        );\n      default:\n        throw ArgumentError();\n    }\n  }\n\n  final String filterId;\n  final String fieldId;\n  final FieldType fieldType;\n\n  String get conditionName;\n\n  bool get canAttachContent;\n\n  String getContentDescription(FieldInfo field);\n\n  Widget getMobileDescription(\n    FieldInfo field, {\n    required VoidCallback onExpand,\n    required void Function(DatabaseFilter filter) onUpdate,\n  }) =>\n      const SizedBox.shrink();\n\n  Uint8List writeToBuffer();\n}\n\nfinal class TextFilter extends DatabaseFilter {\n  TextFilter({\n    required super.filterId,\n    required super.fieldId,\n    required super.fieldType,\n    required this.condition,\n    required String content,\n  }) {\n    this.content = canAttachContent ? content : \"\";\n  }\n\n  final TextFilterConditionPB condition;\n  late final String content;\n\n  @override\n  String get conditionName => condition.filterName;\n\n  @override\n  bool get canAttachContent =>\n      condition != TextFilterConditionPB.TextIsEmpty &&\n      condition != TextFilterConditionPB.TextIsNotEmpty;\n\n  @override\n  String getContentDescription(FieldInfo field) {\n    final filterDesc = condition.choicechipPrefix;\n\n    if (condition == TextFilterConditionPB.TextIsEmpty ||\n        condition == TextFilterConditionPB.TextIsNotEmpty) {\n      return filterDesc;\n    }\n\n    return content.isEmpty ? filterDesc : \"$filterDesc $content\";\n  }\n\n  @override\n  Widget getMobileDescription(\n    FieldInfo field, {\n    required VoidCallback onExpand,\n    required void Function(DatabaseFilter filter) onUpdate,\n  }) {\n    return FilterItemInnerTextField(\n      content: content,\n      enabled: canAttachContent,\n      onSubmitted: (content) {\n        final newFilter = copyWith(content: content);\n        onUpdate(newFilter);\n      },\n    );\n  }\n\n  @override\n  Uint8List writeToBuffer() {\n    final filterPB = TextFilterPB()..condition = condition;\n\n    if (condition != TextFilterConditionPB.TextIsEmpty &&\n        condition != TextFilterConditionPB.TextIsNotEmpty) {\n      filterPB.content = content;\n    }\n    return filterPB.writeToBuffer();\n  }\n\n  TextFilter copyWith({\n    TextFilterConditionPB? condition,\n    String? content,\n  }) {\n    return TextFilter(\n      filterId: filterId,\n      fieldId: fieldId,\n      fieldType: fieldType,\n      condition: condition ?? this.condition,\n      content: content ?? this.content,\n    );\n  }\n\n  @override\n  List<Object?> get props => [filterId, fieldId, condition, content];\n}\n\nfinal class NumberFilter extends DatabaseFilter {\n  NumberFilter({\n    required super.filterId,\n    required super.fieldId,\n    required super.fieldType,\n    required this.condition,\n    required String content,\n  }) {\n    this.content = canAttachContent ? content : \"\";\n  }\n\n  final NumberFilterConditionPB condition;\n  late final String content;\n\n  @override\n  String get conditionName => condition.filterName;\n\n  @override\n  bool get canAttachContent =>\n      condition != NumberFilterConditionPB.NumberIsEmpty &&\n      condition != NumberFilterConditionPB.NumberIsNotEmpty;\n\n  @override\n  String getContentDescription(FieldInfo field) {\n    if (condition == NumberFilterConditionPB.NumberIsEmpty ||\n        condition == NumberFilterConditionPB.NumberIsNotEmpty) {\n      return condition.shortName;\n    }\n\n    return \"${condition.shortName} $content\";\n  }\n\n  @override\n  Widget getMobileDescription(\n    FieldInfo field, {\n    required VoidCallback onExpand,\n    required void Function(DatabaseFilter filter) onUpdate,\n  }) {\n    return FilterItemInnerTextField(\n      content: content,\n      enabled: canAttachContent,\n      onSubmitted: (content) {\n        final newFilter = copyWith(content: content);\n        onUpdate(newFilter);\n      },\n    );\n  }\n\n  @override\n  Uint8List writeToBuffer() {\n    final filterPB = NumberFilterPB()..condition = condition;\n\n    if (condition != NumberFilterConditionPB.NumberIsEmpty &&\n        condition != NumberFilterConditionPB.NumberIsNotEmpty) {\n      filterPB.content = content;\n    }\n    return filterPB.writeToBuffer();\n  }\n\n  NumberFilter copyWith({\n    NumberFilterConditionPB? condition,\n    String? content,\n  }) {\n    return NumberFilter(\n      filterId: filterId,\n      fieldId: fieldId,\n      fieldType: fieldType,\n      condition: condition ?? this.condition,\n      content: content ?? this.content,\n    );\n  }\n\n  @override\n  List<Object?> get props => [filterId, fieldId, condition, content];\n}\n\nfinal class CheckboxFilter extends DatabaseFilter {\n  const CheckboxFilter({\n    required super.filterId,\n    required super.fieldId,\n    required super.fieldType,\n    required this.condition,\n  });\n\n  final CheckboxFilterConditionPB condition;\n\n  @override\n  String get conditionName => condition.filterName;\n\n  @override\n  bool get canAttachContent => false;\n\n  @override\n  String getContentDescription(FieldInfo field) => condition.filterName;\n\n  @override\n  Uint8List writeToBuffer() {\n    return (CheckboxFilterPB()..condition = condition).writeToBuffer();\n  }\n\n  CheckboxFilter copyWith({\n    CheckboxFilterConditionPB? condition,\n  }) {\n    return CheckboxFilter(\n      filterId: filterId,\n      fieldId: fieldId,\n      fieldType: fieldType,\n      condition: condition ?? this.condition,\n    );\n  }\n\n  @override\n  List<Object?> get props => [filterId, fieldId, condition];\n}\n\nfinal class ChecklistFilter extends DatabaseFilter {\n  const ChecklistFilter({\n    required super.filterId,\n    required super.fieldId,\n    required super.fieldType,\n    required this.condition,\n  });\n\n  final ChecklistFilterConditionPB condition;\n\n  @override\n  String get conditionName => condition.filterName;\n\n  @override\n  bool get canAttachContent => false;\n\n  @override\n  String getContentDescription(FieldInfo field) => condition.filterName;\n\n  @override\n  Uint8List writeToBuffer() {\n    return (ChecklistFilterPB()..condition = condition).writeToBuffer();\n  }\n\n  ChecklistFilter copyWith({\n    ChecklistFilterConditionPB? condition,\n  }) {\n    return ChecklistFilter(\n      filterId: filterId,\n      fieldId: fieldId,\n      fieldType: fieldType,\n      condition: condition ?? this.condition,\n    );\n  }\n\n  @override\n  List<Object?> get props => [filterId, fieldId, condition];\n}\n\nfinal class SelectOptionFilter extends DatabaseFilter {\n  SelectOptionFilter({\n    required super.filterId,\n    required super.fieldId,\n    required super.fieldType,\n    required this.condition,\n    required List<String> optionIds,\n  }) {\n    if (canAttachContent) {\n      this.optionIds.addAll(optionIds);\n    }\n  }\n\n  final SelectOptionFilterConditionPB condition;\n  final List<String> optionIds = [];\n\n  @override\n  String get conditionName => condition.i18n;\n\n  @override\n  bool get canAttachContent =>\n      condition != SelectOptionFilterConditionPB.OptionIsEmpty &&\n      condition != SelectOptionFilterConditionPB.OptionIsNotEmpty;\n\n  @override\n  String getContentDescription(FieldInfo field) {\n    if (!canAttachContent || optionIds.isEmpty) {\n      return condition.i18n;\n    }\n\n    final delegate = makeDelegate(field);\n    final options = delegate.getOptions(field);\n\n    final optionNames = options\n        .where((option) => optionIds.contains(option.id))\n        .map((option) => option.name)\n        .join(', ');\n    return \"${condition.i18n} $optionNames\";\n  }\n\n  @override\n  Widget getMobileDescription(\n    FieldInfo field, {\n    required VoidCallback onExpand,\n    required void Function(DatabaseFilter filter) onUpdate,\n  }) {\n    final delegate = makeDelegate(field);\n    final options = delegate\n        .getOptions(field)\n        .where((option) => optionIds.contains(option.id))\n        .toList();\n\n    return FilterItemInnerButton(\n      onTap: onExpand,\n      child: ListView.separated(\n        physics: const NeverScrollableScrollPhysics(),\n        scrollDirection: Axis.horizontal,\n        separatorBuilder: (context, index) => const HSpace(8),\n        itemCount: options.length,\n        itemBuilder: (context, index) => SelectOptionTag(\n          option: options[index],\n          fontSize: 14,\n          borderRadius: BorderRadius.circular(9),\n          padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),\n        ),\n        padding: const EdgeInsets.symmetric(vertical: 8),\n      ),\n    );\n  }\n\n  @override\n  Uint8List writeToBuffer() {\n    final filterPB = SelectOptionFilterPB()..condition = condition;\n\n    if (canAttachContent) {\n      filterPB.optionIds.addAll(optionIds);\n    }\n\n    return filterPB.writeToBuffer();\n  }\n\n  SelectOptionFilter copyWith({\n    SelectOptionFilterConditionPB? condition,\n    List<String>? optionIds,\n  }) {\n    return SelectOptionFilter(\n      filterId: filterId,\n      fieldId: fieldId,\n      fieldType: fieldType,\n      condition: condition ?? this.condition,\n      optionIds: optionIds ?? this.optionIds,\n    );\n  }\n\n  SelectOptionFilterDelegate makeDelegate(FieldInfo field) =>\n      field.fieldType == FieldType.SingleSelect\n          ? const SingleSelectOptionFilterDelegateImpl()\n          : const MultiSelectOptionFilterDelegateImpl();\n\n  @override\n  List<Object?> get props => [filterId, fieldId, condition, optionIds];\n}\n\nenum DateTimeFilterCondition {\n  on,\n  before,\n  after,\n  onOrBefore,\n  onOrAfter,\n  between,\n  isEmpty,\n  isNotEmpty;\n\n  DateFilterConditionPB toPB(bool isStart) {\n    return isStart\n        ? switch (this) {\n            on => DateFilterConditionPB.DateStartsOn,\n            before => DateFilterConditionPB.DateStartsBefore,\n            after => DateFilterConditionPB.DateStartsAfter,\n            onOrBefore => DateFilterConditionPB.DateStartsOnOrBefore,\n            onOrAfter => DateFilterConditionPB.DateStartsOnOrAfter,\n            between => DateFilterConditionPB.DateStartsBetween,\n            isEmpty => DateFilterConditionPB.DateStartIsEmpty,\n            isNotEmpty => DateFilterConditionPB.DateStartIsNotEmpty,\n          }\n        : switch (this) {\n            on => DateFilterConditionPB.DateEndsOn,\n            before => DateFilterConditionPB.DateEndsBefore,\n            after => DateFilterConditionPB.DateEndsAfter,\n            onOrBefore => DateFilterConditionPB.DateEndsOnOrBefore,\n            onOrAfter => DateFilterConditionPB.DateEndsOnOrAfter,\n            between => DateFilterConditionPB.DateEndsBetween,\n            isEmpty => DateFilterConditionPB.DateEndIsEmpty,\n            isNotEmpty => DateFilterConditionPB.DateEndIsNotEmpty,\n          };\n  }\n\n  String get choiceChipPrefix {\n    return switch (this) {\n      on => \"\",\n      before => LocaleKeys.grid_dateFilter_choicechipPrefix_before.tr(),\n      after => LocaleKeys.grid_dateFilter_choicechipPrefix_after.tr(),\n      onOrBefore => LocaleKeys.grid_dateFilter_choicechipPrefix_onOrBefore.tr(),\n      onOrAfter => LocaleKeys.grid_dateFilter_choicechipPrefix_onOrAfter.tr(),\n      between => LocaleKeys.grid_dateFilter_choicechipPrefix_between.tr(),\n      isEmpty => LocaleKeys.grid_dateFilter_choicechipPrefix_isEmpty.tr(),\n      isNotEmpty => LocaleKeys.grid_dateFilter_choicechipPrefix_isNotEmpty.tr(),\n    };\n  }\n\n  String get filterName {\n    return switch (this) {\n      on => LocaleKeys.grid_dateFilter_is.tr(),\n      before => LocaleKeys.grid_dateFilter_before.tr(),\n      after => LocaleKeys.grid_dateFilter_after.tr(),\n      onOrBefore => LocaleKeys.grid_dateFilter_onOrBefore.tr(),\n      onOrAfter => LocaleKeys.grid_dateFilter_onOrAfter.tr(),\n      between => LocaleKeys.grid_dateFilter_between.tr(),\n      isEmpty => LocaleKeys.grid_dateFilter_empty.tr(),\n      isNotEmpty => LocaleKeys.grid_dateFilter_notEmpty.tr(),\n    };\n  }\n\n  static List<DateTimeFilterCondition> availableConditionsForFieldType(\n    FieldType fieldType,\n  ) {\n    final result = [...values];\n    if (fieldType == FieldType.CreatedTime ||\n        fieldType == FieldType.LastEditedTime) {\n      result.remove(isEmpty);\n      result.remove(isNotEmpty);\n    }\n\n    return result;\n  }\n}\n\nfinal class DateTimeFilter extends DatabaseFilter {\n  DateTimeFilter({\n    required super.filterId,\n    required super.fieldId,\n    required super.fieldType,\n    required this.condition,\n    DateTime? timestamp,\n    DateTime? start,\n    DateTime? end,\n  }) {\n    if (canAttachContent) {\n      if (condition == DateFilterConditionPB.DateStartsBetween ||\n          condition == DateFilterConditionPB.DateEndsBetween) {\n        this.start = start;\n        this.end = end;\n        this.timestamp = null;\n      } else {\n        this.timestamp = timestamp;\n        this.start = null;\n        this.end = null;\n      }\n    } else {\n      this.timestamp = null;\n      this.start = null;\n      this.end = null;\n    }\n  }\n\n  final DateFilterConditionPB condition;\n  late final DateTime? timestamp;\n  late final DateTime? start;\n  late final DateTime? end;\n\n  @override\n  String get conditionName => condition.toCondition().filterName;\n\n  @override\n  bool get canAttachContent => ![\n        DateFilterConditionPB.DateStartIsEmpty,\n        DateFilterConditionPB.DateStartIsNotEmpty,\n        DateFilterConditionPB.DateEndIsEmpty,\n        DateFilterConditionPB.DateEndIsNotEmpty,\n      ].contains(condition);\n\n  @override\n  String getContentDescription(FieldInfo field) {\n    return switch (condition) {\n      DateFilterConditionPB.DateStartIsEmpty ||\n      DateFilterConditionPB.DateStartIsNotEmpty ||\n      DateFilterConditionPB.DateEndIsEmpty ||\n      DateFilterConditionPB.DateEndIsNotEmpty =>\n        condition.toCondition().choiceChipPrefix,\n      DateFilterConditionPB.DateStartsOn ||\n      DateFilterConditionPB.DateEndsOn =>\n        timestamp?.defaultFormat ?? \"\",\n      DateFilterConditionPB.DateStartsBetween ||\n      DateFilterConditionPB.DateEndsBetween =>\n        \"${condition.toCondition().choiceChipPrefix} ${start?.defaultFormat ?? \"\"} - ${end?.defaultFormat ?? \"\"}\",\n      _ =>\n        \"${condition.toCondition().choiceChipPrefix} ${timestamp?.defaultFormat ?? \"\"}\"\n    };\n  }\n\n  @override\n  Widget getMobileDescription(\n    FieldInfo field, {\n    required VoidCallback onExpand,\n    required void Function(DatabaseFilter filter) onUpdate,\n  }) {\n    String? text;\n\n    if (condition.isRange) {\n      text = \"${start?.defaultFormat ?? \"\"} - ${end?.defaultFormat ?? \"\"}\";\n      text = text == \" - \" ? null : text;\n    } else {\n      text = timestamp.defaultFormat;\n    }\n    return FilterItemInnerButton(\n      onTap: onExpand,\n      child: FlowyText(\n        text ?? \"\",\n        overflow: TextOverflow.ellipsis,\n      ),\n    );\n  }\n\n  @override\n  Uint8List writeToBuffer() {\n    final filterPB = DateFilterPB()..condition = condition;\n\n    Int64 dateTimeToInt(DateTime dateTime) {\n      return Int64(dateTime.millisecondsSinceEpoch ~/ 1000);\n    }\n\n    switch (condition) {\n      case DateFilterConditionPB.DateStartsOn:\n      case DateFilterConditionPB.DateStartsBefore:\n      case DateFilterConditionPB.DateStartsOnOrBefore:\n      case DateFilterConditionPB.DateStartsAfter:\n      case DateFilterConditionPB.DateStartsOnOrAfter:\n      case DateFilterConditionPB.DateEndsOn:\n      case DateFilterConditionPB.DateEndsBefore:\n      case DateFilterConditionPB.DateEndsOnOrBefore:\n      case DateFilterConditionPB.DateEndsAfter:\n      case DateFilterConditionPB.DateEndsOnOrAfter:\n        if (timestamp != null) {\n          filterPB.timestamp = dateTimeToInt(timestamp!);\n        }\n        break;\n      case DateFilterConditionPB.DateStartsBetween:\n      case DateFilterConditionPB.DateEndsBetween:\n        if (start != null) {\n          filterPB.start = dateTimeToInt(start!);\n        }\n        if (end != null) {\n          filterPB.end = dateTimeToInt(end!);\n        }\n        break;\n      default:\n        break;\n    }\n\n    return filterPB.writeToBuffer();\n  }\n\n  DateTimeFilter copyWithCondition({\n    required bool isStart,\n    required DateTimeFilterCondition condition,\n  }) {\n    return DateTimeFilter(\n      filterId: filterId,\n      fieldId: fieldId,\n      fieldType: fieldType,\n      condition: condition.toPB(isStart),\n      start: start,\n      end: end,\n      timestamp: timestamp,\n    );\n  }\n\n  DateTimeFilter copyWithTimestamp({\n    required DateTime timestamp,\n  }) {\n    return DateTimeFilter(\n      filterId: filterId,\n      fieldId: fieldId,\n      fieldType: fieldType,\n      condition: condition,\n      start: start,\n      end: end,\n      timestamp: timestamp,\n    );\n  }\n\n  DateTimeFilter copyWithRange({\n    required DateTime? start,\n    required DateTime? end,\n  }) {\n    return DateTimeFilter(\n      filterId: filterId,\n      fieldId: fieldId,\n      fieldType: fieldType,\n      condition: condition,\n      start: start,\n      end: end,\n      timestamp: timestamp,\n    );\n  }\n\n  @override\n  List<Object?> get props =>\n      [filterId, fieldId, condition, timestamp, start, end];\n}\n\nfinal class TimeFilter extends DatabaseFilter {\n  const TimeFilter({\n    required super.filterId,\n    required super.fieldId,\n    required super.fieldType,\n    required this.condition,\n    required this.content,\n  });\n\n  final NumberFilterConditionPB condition;\n  final String content;\n\n  @override\n  String get conditionName => condition.filterName;\n\n  @override\n  bool get canAttachContent =>\n      condition != NumberFilterConditionPB.NumberIsEmpty &&\n      condition != NumberFilterConditionPB.NumberIsNotEmpty;\n\n  @override\n  String getContentDescription(FieldInfo field) {\n    if (condition == NumberFilterConditionPB.NumberIsEmpty ||\n        condition == NumberFilterConditionPB.NumberIsNotEmpty) {\n      return condition.shortName;\n    }\n\n    return \"${condition.shortName} $content\";\n  }\n\n  @override\n  Uint8List writeToBuffer() {\n    return (NumberFilterPB()\n          ..condition = condition\n          ..content = content)\n        .writeToBuffer();\n  }\n\n  TimeFilter copyWith({NumberFilterConditionPB? condition, String? content}) {\n    return TimeFilter(\n      filterId: filterId,\n      fieldId: fieldId,\n      fieldType: fieldType,\n      condition: condition ?? this.condition,\n      content: content ?? this.content,\n    );\n  }\n\n  @override\n  List<Object?> get props => [filterId, fieldId, condition, content];\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/field/sort_entities.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:equatable/equatable.dart';\n\nfinal class DatabaseSort extends Equatable {\n  const DatabaseSort({\n    required this.sortId,\n    required this.fieldId,\n    required this.condition,\n  });\n\n  DatabaseSort.fromPB(SortPB sort)\n      : sortId = sort.id,\n        fieldId = sort.fieldId,\n        condition = sort.condition;\n\n  final String sortId;\n  final String fieldId;\n  final SortConditionPB condition;\n\n  @override\n  List<Object> get props => [sortId, fieldId, condition];\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/edit_select_option_bloc.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:protobuf/protobuf.dart';\n\npart 'edit_select_option_bloc.freezed.dart';\n\nclass EditSelectOptionBloc\n    extends Bloc<EditSelectOptionEvent, EditSelectOptionState> {\n  EditSelectOptionBloc({required SelectOptionPB option})\n      : super(EditSelectOptionState.initial(option)) {\n    on<EditSelectOptionEvent>(\n      (event, emit) async {\n        event.when(\n          updateName: (name) {\n            emit(state.copyWith(option: _updateName(name)));\n          },\n          updateColor: (color) {\n            emit(state.copyWith(option: _updateColor(color)));\n          },\n          delete: () {\n            emit(state.copyWith(deleted: true));\n          },\n        );\n      },\n    );\n  }\n\n  SelectOptionPB _updateColor(SelectOptionColorPB color) {\n    state.option.freeze();\n    return state.option.rebuild((option) {\n      option.color = color;\n    });\n  }\n\n  SelectOptionPB _updateName(String name) {\n    state.option.freeze();\n    return state.option.rebuild((option) {\n      option.name = name;\n    });\n  }\n}\n\n@freezed\nclass EditSelectOptionEvent with _$EditSelectOptionEvent {\n  const factory EditSelectOptionEvent.updateName(String name) = _UpdateName;\n  const factory EditSelectOptionEvent.updateColor(SelectOptionColorPB color) =\n      _UpdateColor;\n  const factory EditSelectOptionEvent.delete() = _Delete;\n}\n\n@freezed\nclass EditSelectOptionState with _$EditSelectOptionState {\n  const factory EditSelectOptionState({\n    required SelectOptionPB option,\n    required bool deleted,\n  }) = _EditSelectOptionState;\n\n  factory EditSelectOptionState.initial(SelectOptionPB option) =>\n      EditSelectOptionState(\n        option: option,\n        deleted: false,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/number_format_bloc.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pbenum.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'number_format_bloc.freezed.dart';\n\nclass NumberFormatBloc extends Bloc<NumberFormatEvent, NumberFormatState> {\n  NumberFormatBloc() : super(NumberFormatState.initial()) {\n    on<NumberFormatEvent>(\n      (event, emit) async {\n        event.map(\n          setFilter: (_SetFilter value) {\n            final List<NumberFormatPB> formats =\n                List.from(NumberFormatPB.values);\n            if (value.filter.isNotEmpty) {\n              formats.retainWhere(\n                (element) => element\n                    .title()\n                    .toLowerCase()\n                    .contains(value.filter.toLowerCase()),\n              );\n            }\n            emit(state.copyWith(formats: formats, filter: value.filter));\n          },\n        );\n      },\n    );\n  }\n}\n\n@freezed\nclass NumberFormatEvent with _$NumberFormatEvent {\n  const factory NumberFormatEvent.setFilter(String filter) = _SetFilter;\n}\n\n@freezed\nclass NumberFormatState with _$NumberFormatState {\n  const factory NumberFormatState({\n    required List<NumberFormatPB> formats,\n    required String filter,\n  }) = _NumberFormatState;\n\n  factory NumberFormatState.initial() {\n    return const NumberFormatState(\n      formats: NumberFormatPB.values,\n      filter: \"\",\n    );\n  }\n}\n\nextension NumberFormatExtension on NumberFormatPB {\n  String title() {\n    switch (this) {\n      case NumberFormatPB.ArgentinePeso:\n        return \"Argentine peso\";\n      case NumberFormatPB.Baht:\n        return \"Baht\";\n      case NumberFormatPB.CanadianDollar:\n        return \"Canadian dollar\";\n      case NumberFormatPB.ChileanPeso:\n        return \"Chilean peso\";\n      case NumberFormatPB.ColombianPeso:\n        return \"Colombian peso\";\n      case NumberFormatPB.DanishKrone:\n        return \"Danish crown\";\n      case NumberFormatPB.Dirham:\n        return \"Dirham\";\n      case NumberFormatPB.EUR:\n        return \"Euro\";\n      case NumberFormatPB.Forint:\n        return \"Forint\";\n      case NumberFormatPB.Franc:\n        return \"Franc\";\n      case NumberFormatPB.HongKongDollar:\n        return \"Hone Kong dollar\";\n      case NumberFormatPB.Koruna:\n        return \"Koruna\";\n      case NumberFormatPB.Krona:\n        return \"Krona\";\n      case NumberFormatPB.Leu:\n        return \"Leu\";\n      case NumberFormatPB.Lira:\n        return \"Lira\";\n      case NumberFormatPB.MexicanPeso:\n        return \"Mexican peso\";\n      case NumberFormatPB.NewTaiwanDollar:\n        return \"New Taiwan dollar\";\n      case NumberFormatPB.NewZealandDollar:\n        return \"New Zealand dollar\";\n      case NumberFormatPB.NorwegianKrone:\n        return \"Norwegian krone\";\n      case NumberFormatPB.Num:\n        return \"Number\";\n      case NumberFormatPB.Percent:\n        return \"Percent\";\n      case NumberFormatPB.PhilippinePeso:\n        return \"Philippine peso\";\n      case NumberFormatPB.Pound:\n        return \"Pound\";\n      case NumberFormatPB.Rand:\n        return \"Rand\";\n      case NumberFormatPB.Real:\n        return \"Real\";\n      case NumberFormatPB.Ringgit:\n        return \"Ringgit\";\n      case NumberFormatPB.Riyal:\n        return \"Riyal\";\n      case NumberFormatPB.Ruble:\n        return \"Ruble\";\n      case NumberFormatPB.Rupee:\n        return \"Rupee\";\n      case NumberFormatPB.Rupiah:\n        return \"Rupiah\";\n      case NumberFormatPB.Shekel:\n        return \"Skekel\";\n      case NumberFormatPB.USD:\n        return \"US dollar\";\n      case NumberFormatPB.UruguayanPeso:\n        return \"Uruguayan peso\";\n      case NumberFormatPB.Won:\n        return \"Won\";\n      case NumberFormatPB.Yen:\n        return \"Yen\";\n      case NumberFormatPB.Yuan:\n        return \"Yuan\";\n      default:\n        throw UnimplementedError;\n    }\n  }\n\n  String iconSymbol([bool defaultPrefixInc = true]) {\n    switch (this) {\n      case NumberFormatPB.ArgentinePeso:\n        return \"\\$\";\n      case NumberFormatPB.Baht:\n        return \"฿\";\n      case NumberFormatPB.CanadianDollar:\n        return \"C\\$\";\n      case NumberFormatPB.ChileanPeso:\n        return \"\\$\";\n      case NumberFormatPB.ColombianPeso:\n        return \"\\$\";\n      case NumberFormatPB.DanishKrone:\n        return \"kr\";\n      case NumberFormatPB.Dirham:\n        return \"د.إ\";\n      case NumberFormatPB.EUR:\n        return \"€\";\n      case NumberFormatPB.Forint:\n        return \"Ft\";\n      case NumberFormatPB.Franc:\n        return \"Fr\";\n      case NumberFormatPB.HongKongDollar:\n        return \"HK\\$\";\n      case NumberFormatPB.Koruna:\n        return \"Kč\";\n      case NumberFormatPB.Krona:\n        return \"kr\";\n      case NumberFormatPB.Leu:\n        return \"lei\";\n      case NumberFormatPB.Lira:\n        return \"₺\";\n      case NumberFormatPB.MexicanPeso:\n        return \"\\$\";\n      case NumberFormatPB.NewTaiwanDollar:\n        return \"NT\\$\";\n      case NumberFormatPB.NewZealandDollar:\n        return \"NZ\\$\";\n      case NumberFormatPB.NorwegianKrone:\n        return \"kr\";\n      case NumberFormatPB.Num:\n        return defaultPrefixInc ? \"#\" : \"\";\n      case NumberFormatPB.Percent:\n        return \"%\";\n      case NumberFormatPB.PhilippinePeso:\n        return \"₱\";\n      case NumberFormatPB.Pound:\n        return \"£\";\n      case NumberFormatPB.Rand:\n        return \"R\";\n      case NumberFormatPB.Real:\n        return \"R\\$\";\n      case NumberFormatPB.Ringgit:\n        return \"RM\";\n      case NumberFormatPB.Riyal:\n        return \"ر.س\";\n      case NumberFormatPB.Ruble:\n        return \"₽\";\n      case NumberFormatPB.Rupee:\n        return \"₹\";\n      case NumberFormatPB.Rupiah:\n        return \"Rp\";\n      case NumberFormatPB.Shekel:\n        return \"₪\";\n      case NumberFormatPB.USD:\n        return \"\\$\";\n      case NumberFormatPB.UruguayanPeso:\n        return \"\\$U\";\n      case NumberFormatPB.Won:\n        return \"₩\";\n      case NumberFormatPB.Yen:\n        return \"JPY ¥\";\n      case NumberFormatPB.Yuan:\n        return \"¥\";\n      default:\n        throw UnimplementedError;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart",
    "content": "import 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'relation_type_option_cubit.freezed.dart';\n\nclass RelationDatabaseListCubit extends Cubit<RelationDatabaseListState> {\n  RelationDatabaseListCubit() : super(RelationDatabaseListState.initial()) {\n    _loadDatabaseMetas();\n  }\n\n  void _loadDatabaseMetas() async {\n    final metaPBs = await DatabaseEventGetDatabases()\n        .send()\n        .fold<List<DatabaseMetaPB>>((s) => s.items, (f) => []);\n    final futures = metaPBs.map((meta) {\n      return ViewBackendService.getView(meta.viewId).then(\n        (result) => result.fold(\n          (s) => DatabaseMeta(\n            databaseId: meta.databaseId,\n            viewId: meta.viewId,\n            databaseName: s.name,\n          ),\n          (f) => null,\n        ),\n      );\n    });\n    final databaseMetas = await Future.wait(futures);\n    emit(\n      RelationDatabaseListState(\n        databaseMetas: databaseMetas.nonNulls.toList(),\n      ),\n    );\n  }\n}\n\n@freezed\nclass DatabaseMeta with _$DatabaseMeta {\n  factory DatabaseMeta({\n    /// id of the database\n    required String databaseId,\n\n    /// id of the view\n    required String viewId,\n\n    /// name of the database\n    required String databaseName,\n  }) = _DatabaseMeta;\n}\n\n@freezed\nclass RelationDatabaseListState with _$RelationDatabaseListState {\n  factory RelationDatabaseListState({\n    required List<DatabaseMeta> databaseMetas,\n  }) = _RelationDatabaseListState;\n\n  factory RelationDatabaseListState.initial() =>\n      RelationDatabaseListState(databaseMetas: []);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_option_type_option_bloc.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport 'select_type_option_actions.dart';\n\npart 'select_option_type_option_bloc.freezed.dart';\n\nclass SelectOptionTypeOptionBloc\n    extends Bloc<SelectOptionTypeOptionEvent, SelectOptionTypeOptionState> {\n  SelectOptionTypeOptionBloc({\n    required List<SelectOptionPB> options,\n    required this.typeOptionAction,\n  }) : super(SelectOptionTypeOptionState.initial(options)) {\n    _dispatch();\n  }\n\n  final ISelectOptionAction typeOptionAction;\n\n  void _dispatch() {\n    on<SelectOptionTypeOptionEvent>(\n      (event, emit) async {\n        event.when(\n          createOption: (optionName) {\n            final List<SelectOptionPB> options =\n                typeOptionAction.insertOption(state.options, optionName);\n            emit(state.copyWith(options: options));\n          },\n          addingOption: () {\n            emit(state.copyWith(isEditingOption: true, newOptionName: null));\n          },\n          endAddingOption: () {\n            emit(state.copyWith(isEditingOption: false, newOptionName: null));\n          },\n          updateOption: (option) {\n            final options =\n                typeOptionAction.updateOption(state.options, option);\n            emit(state.copyWith(options: options));\n          },\n          deleteOption: (option) {\n            final options =\n                typeOptionAction.deleteOption(state.options, option);\n            emit(state.copyWith(options: options));\n          },\n          reorderOption: (fromOptionId, toOptionId) {\n            final options = typeOptionAction.reorderOption(\n              state.options,\n              fromOptionId,\n              toOptionId,\n            );\n            emit(state.copyWith(options: options));\n          },\n        );\n      },\n    );\n  }\n}\n\n@freezed\nclass SelectOptionTypeOptionEvent with _$SelectOptionTypeOptionEvent {\n  const factory SelectOptionTypeOptionEvent.createOption(String optionName) =\n      _CreateOption;\n  const factory SelectOptionTypeOptionEvent.addingOption() = _AddingOption;\n  const factory SelectOptionTypeOptionEvent.endAddingOption() =\n      _EndAddingOption;\n  const factory SelectOptionTypeOptionEvent.updateOption(\n    SelectOptionPB option,\n  ) = _UpdateOption;\n  const factory SelectOptionTypeOptionEvent.deleteOption(\n    SelectOptionPB option,\n  ) = _DeleteOption;\n  const factory SelectOptionTypeOptionEvent.reorderOption(\n    String fromOptionId,\n    String toOptionId,\n  ) = _ReorderOption;\n}\n\n@freezed\nclass SelectOptionTypeOptionState with _$SelectOptionTypeOptionState {\n  const factory SelectOptionTypeOptionState({\n    required List<SelectOptionPB> options,\n    required bool isEditingOption,\n    required String? newOptionName,\n  }) = _SelectOptionTypeOptionState;\n\n  factory SelectOptionTypeOptionState.initial(List<SelectOptionPB> options) =>\n      SelectOptionTypeOptionState(\n        options: options,\n        isEditingOption: false,\n        newOptionName: null,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_type_option_actions.dart",
    "content": "import 'package:appflowy/plugins/database/domain/type_option_service.dart';\nimport 'package:appflowy/plugins/database/widgets/field/type_option_editor/builder.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';\nimport 'package:nanoid/nanoid.dart';\n\nabstract class ISelectOptionAction {\n  ISelectOptionAction({\n    required this.onTypeOptionUpdated,\n    required String viewId,\n    required String fieldId,\n  }) : service = TypeOptionBackendService(viewId: viewId, fieldId: fieldId);\n\n  final TypeOptionBackendService service;\n  final TypeOptionDataCallback onTypeOptionUpdated;\n\n  void updateTypeOption(List<SelectOptionPB> options) {\n    final newTypeOption = MultiSelectTypeOptionPB()..options.addAll(options);\n    onTypeOptionUpdated(newTypeOption.writeToBuffer());\n  }\n\n  List<SelectOptionPB> insertOption(\n    List<SelectOptionPB> options,\n    String optionName,\n  ) {\n    if (options.any((element) => element.name == optionName)) {\n      return options;\n    }\n\n    final newOptions = List<SelectOptionPB>.from(options);\n\n    final newSelectOption = SelectOptionPB()\n      ..id = nanoid(4)\n      ..color = newSelectOptionColor(options)\n      ..name = optionName;\n\n    newOptions.insert(0, newSelectOption);\n\n    updateTypeOption(newOptions);\n    return newOptions;\n  }\n\n  List<SelectOptionPB> deleteOption(\n    List<SelectOptionPB> options,\n    SelectOptionPB deletedOption,\n  ) {\n    final newOptions = List<SelectOptionPB>.from(options);\n    final index =\n        newOptions.indexWhere((option) => option.id == deletedOption.id);\n    if (index != -1) {\n      newOptions.removeAt(index);\n    }\n\n    updateTypeOption(newOptions);\n    return newOptions;\n  }\n\n  List<SelectOptionPB> updateOption(\n    List<SelectOptionPB> options,\n    SelectOptionPB option,\n  ) {\n    final newOptions = List<SelectOptionPB>.from(options);\n    final index = newOptions.indexWhere((element) => element.id == option.id);\n    if (index != -1) {\n      newOptions[index] = option;\n    }\n\n    updateTypeOption(newOptions);\n    return newOptions;\n  }\n\n  List<SelectOptionPB> reorderOption(\n    List<SelectOptionPB> options,\n    String fromOptionId,\n    String toOptionId,\n  ) {\n    final newOptions = List<SelectOptionPB>.from(options);\n    final fromIndex =\n        newOptions.indexWhere((element) => element.id == fromOptionId);\n    final toIndex =\n        newOptions.indexWhere((element) => element.id == toOptionId);\n\n    if (fromIndex != -1 && toIndex != -1) {\n      newOptions.insert(toIndex, newOptions.removeAt(fromIndex));\n    }\n\n    updateTypeOption(newOptions);\n    return newOptions;\n  }\n}\n\nclass MultiSelectAction extends ISelectOptionAction {\n  MultiSelectAction({\n    required super.viewId,\n    required super.fieldId,\n    required super.onTypeOptionUpdated,\n  });\n\n  @override\n  void updateTypeOption(List<SelectOptionPB> options) {\n    final newTypeOption = MultiSelectTypeOptionPB()..options.addAll(options);\n    onTypeOptionUpdated(newTypeOption.writeToBuffer());\n  }\n}\n\nclass SingleSelectAction extends ISelectOptionAction {\n  SingleSelectAction({\n    required super.viewId,\n    required super.fieldId,\n    required super.onTypeOptionUpdated,\n  });\n\n  @override\n  void updateTypeOption(List<SelectOptionPB> options) {\n    final newTypeOption = SingleSelectTypeOptionPB()..options.addAll(options);\n    onTypeOptionUpdated(newTypeOption.writeToBuffer());\n  }\n}\n\nSelectOptionColorPB newSelectOptionColor(List<SelectOptionPB> options) {\n  final colorFrequency = List.filled(SelectOptionColorPB.values.length, 0);\n\n  for (final option in options) {\n    colorFrequency[option.color.value]++;\n  }\n\n  final minIndex = colorFrequency\n      .asMap()\n      .entries\n      .reduce((a, b) => a.value <= b.value ? a : b)\n      .key;\n\n  return SelectOptionColorPB.valueOf(minIndex) ?? SelectOptionColorPB.Purple;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/translate_type_option_bloc.dart",
    "content": "import 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/translate_entities.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:protobuf/protobuf.dart';\n\npart 'translate_type_option_bloc.freezed.dart';\n\nclass TranslateTypeOptionBloc\n    extends Bloc<TranslateTypeOptionEvent, TranslateTypeOptionState> {\n  TranslateTypeOptionBloc({required TranslateTypeOptionPB option})\n      : super(TranslateTypeOptionState.initial(option)) {\n    on<TranslateTypeOptionEvent>(\n      (event, emit) async {\n        event.when(\n          selectLanguage: (languageType) {\n            emit(\n              state.copyWith(\n                option: _updateLanguage(languageType),\n                language: languageTypeToLanguage(languageType),\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  TranslateTypeOptionPB _updateLanguage(TranslateLanguagePB languageType) {\n    state.option.freeze();\n    return state.option.rebuild((option) {\n      option.language = languageType;\n    });\n  }\n}\n\n@freezed\nclass TranslateTypeOptionEvent with _$TranslateTypeOptionEvent {\n  const factory TranslateTypeOptionEvent.selectLanguage(\n    TranslateLanguagePB languageType,\n  ) = _SelectLanguage;\n}\n\n@freezed\nclass TranslateTypeOptionState with _$TranslateTypeOptionState {\n  const factory TranslateTypeOptionState({\n    required TranslateTypeOptionPB option,\n    required String language,\n  }) = _TranslateTypeOptionState;\n\n  factory TranslateTypeOptionState.initial(TranslateTypeOptionPB option) =>\n      TranslateTypeOptionState(\n        option: option,\n        language: languageTypeToLanguage(option.language),\n      );\n}\n\nString languageTypeToLanguage(TranslateLanguagePB langaugeType) {\n  switch (langaugeType) {\n    case TranslateLanguagePB.SimplifiedChinese:\n      return 'Simplified Chinese';\n    case TranslateLanguagePB.TraditionalChinese:\n      return 'Traditional Chinese';\n    case TranslateLanguagePB.English:\n      return 'English';\n    case TranslateLanguagePB.French:\n      return 'French';\n    case TranslateLanguagePB.German:\n      return 'German';\n    case TranslateLanguagePB.Spanish:\n      return 'Spanish';\n    case TranslateLanguagePB.Hindi:\n      return 'Hindi';\n    case TranslateLanguagePB.Portuguese:\n      return 'Portuguese';\n    case TranslateLanguagePB.StandardArabic:\n      return 'Standard Arabic';\n    default:\n      Log.error('Unknown language type: $langaugeType');\n      return 'English';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/type_option_data_parser.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\n\nabstract class TypeOptionParser<T> {\n  T fromBuffer(List<int> buffer);\n}\n\nclass NumberTypeOptionDataParser extends TypeOptionParser<NumberTypeOptionPB> {\n  @override\n  NumberTypeOptionPB fromBuffer(List<int> buffer) {\n    return NumberTypeOptionPB.fromBuffer(buffer);\n  }\n}\n\nclass DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {\n  @override\n  DateTypeOptionPB fromBuffer(List<int> buffer) {\n    return DateTypeOptionPB.fromBuffer(buffer);\n  }\n}\n\nclass TimestampTypeOptionDataParser\n    extends TypeOptionParser<TimestampTypeOptionPB> {\n  @override\n  TimestampTypeOptionPB fromBuffer(List<int> buffer) {\n    return TimestampTypeOptionPB.fromBuffer(buffer);\n  }\n}\n\nclass SingleSelectTypeOptionDataParser\n    extends TypeOptionParser<SingleSelectTypeOptionPB> {\n  @override\n  SingleSelectTypeOptionPB fromBuffer(List<int> buffer) {\n    return SingleSelectTypeOptionPB.fromBuffer(buffer);\n  }\n}\n\nclass MultiSelectTypeOptionDataParser\n    extends TypeOptionParser<MultiSelectTypeOptionPB> {\n  @override\n  MultiSelectTypeOptionPB fromBuffer(List<int> buffer) {\n    return MultiSelectTypeOptionPB.fromBuffer(buffer);\n  }\n}\n\nclass RelationTypeOptionDataParser\n    extends TypeOptionParser<RelationTypeOptionPB> {\n  @override\n  RelationTypeOptionPB fromBuffer(List<int> buffer) {\n    return RelationTypeOptionPB.fromBuffer(buffer);\n  }\n}\n\nclass TranslateTypeOptionDataParser\n    extends TypeOptionParser<TranslateTypeOptionPB> {\n  @override\n  TranslateTypeOptionPB fromBuffer(List<int> buffer) {\n    return TranslateTypeOptionPB.fromBuffer(buffer);\n  }\n}\n\nclass MediaTypeOptionDataParser extends TypeOptionParser<MediaTypeOptionPB> {\n  @override\n  MediaTypeOptionPB fromBuffer(List<int> buffer) {\n    return MediaTypeOptionPB.fromBuffer(buffer);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/layout/layout_bloc.dart",
    "content": "import 'package:appflowy/plugins/database/domain/database_view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'layout_bloc.freezed.dart';\n\nclass DatabaseLayoutBloc\n    extends Bloc<DatabaseLayoutEvent, DatabaseLayoutState> {\n  DatabaseLayoutBloc({\n    required String viewId,\n    required DatabaseLayoutPB databaseLayout,\n  }) : super(DatabaseLayoutState.initial(viewId, databaseLayout)) {\n    on<DatabaseLayoutEvent>(\n      (event, emit) async {\n        event.when(\n          initial: () {},\n          updateLayout: (DatabaseLayoutPB layout) {\n            DatabaseViewBackendService.updateLayout(\n              viewId: viewId,\n              layout: layout,\n            );\n            emit(state.copyWith(databaseLayout: layout));\n          },\n        );\n      },\n    );\n  }\n}\n\n@freezed\nclass DatabaseLayoutEvent with _$DatabaseLayoutEvent {\n  const factory DatabaseLayoutEvent.initial() = _Initial;\n\n  const factory DatabaseLayoutEvent.updateLayout(DatabaseLayoutPB layout) =\n      _UpdateLayout;\n}\n\n@freezed\nclass DatabaseLayoutState with _$DatabaseLayoutState {\n  const factory DatabaseLayoutState({\n    required String viewId,\n    required DatabaseLayoutPB databaseLayout,\n  }) = _DatabaseLayoutState;\n\n  factory DatabaseLayoutState.initial(\n    String viewId,\n    DatabaseLayoutPB databaseLayout,\n  ) =>\n      DatabaseLayoutState(\n        viewId: viewId,\n        databaseLayout: databaseLayout,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/row/related_row_detail_bloc.dart",
    "content": "import 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport '../database_controller.dart';\n\nimport 'row_controller.dart';\n\npart 'related_row_detail_bloc.freezed.dart';\n\nclass RelatedRowDetailPageBloc\n    extends Bloc<RelatedRowDetailPageEvent, RelatedRowDetailPageState> {\n  RelatedRowDetailPageBloc({\n    required String databaseId,\n    required String initialRowId,\n  }) : super(const RelatedRowDetailPageState.loading()) {\n    _dispatch();\n    _init(databaseId, initialRowId);\n  }\n\n  UserProfilePB? _userProfile;\n  UserProfilePB? get userProfile => _userProfile;\n\n  @override\n  Future<void> close() {\n    state.whenOrNull(\n      ready: (databaseController, rowController) async {\n        await rowController.dispose();\n        await databaseController.dispose();\n      },\n    );\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<RelatedRowDetailPageEvent>((event, emit) async {\n      await event.when(\n        didInitialize: (databaseController, rowController) async {\n          final response = await UserEventGetUserProfile().send();\n          response.fold(\n            (userProfile) => _userProfile = userProfile,\n            (err) => Log.error(err),\n          );\n\n          await rowController.initialize();\n\n          await state.maybeWhen(\n            ready: (_, oldRowController) async {\n              await oldRowController.dispose();\n              emit(\n                RelatedRowDetailPageState.ready(\n                  databaseController: databaseController,\n                  rowController: rowController,\n                ),\n              );\n            },\n            orElse: () {\n              emit(\n                RelatedRowDetailPageState.ready(\n                  databaseController: databaseController,\n                  rowController: rowController,\n                ),\n              );\n            },\n          );\n        },\n      );\n    });\n  }\n\n  void _init(String databaseId, String initialRowId) async {\n    final viewId = await DatabaseEventGetDefaultDatabaseViewId(\n      DatabaseIdPB(value: databaseId),\n    ).send().fold(\n          (pb) => pb.value,\n          (error) => null,\n        );\n\n    if (viewId == null) {\n      return;\n    }\n\n    final databaseView = await ViewBackendService.getView(viewId)\n        .fold((viewPB) => viewPB, (f) => null);\n    if (databaseView == null) {\n      return;\n    }\n    final databaseController = DatabaseController(view: databaseView);\n    await databaseController.open().fold(\n          (s) => databaseController.setIsLoading(false),\n          (f) => null,\n        );\n    final rowInfo = databaseController.rowCache.getRow(initialRowId);\n    if (rowInfo == null) {\n      return;\n    }\n    final rowController = RowController(\n      rowMeta: rowInfo.rowMeta,\n      viewId: databaseView.id,\n      rowCache: databaseController.rowCache,\n    );\n\n    add(\n      RelatedRowDetailPageEvent.didInitialize(\n        databaseController,\n        rowController,\n      ),\n    );\n  }\n}\n\n@freezed\nclass RelatedRowDetailPageEvent with _$RelatedRowDetailPageEvent {\n  const factory RelatedRowDetailPageEvent.didInitialize(\n    DatabaseController databaseController,\n    RowController rowController,\n  ) = _DidInitialize;\n}\n\n@freezed\nclass RelatedRowDetailPageState with _$RelatedRowDetailPageState {\n  const factory RelatedRowDetailPageState.loading() = _LoadingState;\n  const factory RelatedRowDetailPageState.ready({\n    required DatabaseController databaseController,\n    required RowController rowController,\n  }) = _ReadyState;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/row/row_banner_bloc.dart",
    "content": "import 'package:flutter/foundation.dart';\n\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:equatable/equatable.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport '../../domain/row_meta_listener.dart';\n\npart 'row_banner_bloc.freezed.dart';\n\nclass RowBannerBloc extends Bloc<RowBannerEvent, RowBannerState> {\n  RowBannerBloc({\n    required this.viewId,\n    required this.fieldController,\n    required RowMetaPB rowMeta,\n  })  : _rowBackendSvc = RowBackendService(viewId: viewId),\n        _metaListener = RowMetaListener(rowMeta.id),\n        super(RowBannerState.initial(rowMeta)) {\n    _dispatch();\n  }\n\n  final String viewId;\n  final FieldController fieldController;\n  final RowBackendService _rowBackendSvc;\n  final RowMetaListener _metaListener;\n\n  UserProfilePB? _userProfile;\n  UserProfilePB? get userProfile => _userProfile;\n\n  bool get hasCover => state.rowMeta.cover.data.isNotEmpty;\n\n  @override\n  Future<void> close() async {\n    await _metaListener.stop();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<RowBannerEvent>(\n      (event, emit) {\n        event.when(\n          initial: () async {\n            await _loadPrimaryField();\n            _listenRowMetaChanged();\n            final result = await UserEventGetUserProfile().send();\n            result.fold(\n              (userProfile) => _userProfile = userProfile,\n              (error) => Log.error(error),\n            );\n          },\n          didReceiveRowMeta: (RowMetaPB rowMeta) {\n            emit(state.copyWith(rowMeta: rowMeta));\n          },\n          setCover: (RowCoverPB cover) => _updateMeta(cover: cover),\n          setIcon: (String iconURL) => _updateMeta(iconURL: iconURL),\n          removeCover: () => _removeCover(),\n          didReceiveFieldUpdate: (updatedField) {\n            emit(\n              state.copyWith(\n                primaryField: updatedField,\n                loadingState: const LoadingState.finish(),\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  Future<void> _loadPrimaryField() async {\n    final fieldOrError =\n        await FieldBackendService.getPrimaryField(viewId: viewId);\n    fieldOrError.fold(\n      (primaryField) {\n        if (!isClosed) {\n          fieldController.addSingleFieldListener(\n            primaryField.id,\n            onFieldChanged: (updatedField) {\n              if (!isClosed) {\n                add(RowBannerEvent.didReceiveFieldUpdate(updatedField.field));\n              }\n            },\n          );\n          add(RowBannerEvent.didReceiveFieldUpdate(primaryField));\n        }\n      },\n      (r) => Log.error(r),\n    );\n  }\n\n  /// Listen the changes of the row meta and then update the banner\n  void _listenRowMetaChanged() {\n    _metaListener.start(\n      callback: (rowMeta) {\n        if (!isClosed) {\n          add(RowBannerEvent.didReceiveRowMeta(rowMeta));\n        }\n      },\n    );\n  }\n\n  /// Update the meta of the row and the view\n  Future<void> _updateMeta({String? iconURL, RowCoverPB? cover}) async {\n    final result = await _rowBackendSvc.updateMeta(\n      iconURL: iconURL,\n      cover: cover,\n      rowId: state.rowMeta.id,\n    );\n    result.fold((l) => null, (err) => Log.error(err));\n  }\n\n  Future<void> _removeCover() async {\n    final result = await _rowBackendSvc.removeCover(state.rowMeta.id);\n    result.fold((l) => null, (err) => Log.error(err));\n  }\n}\n\n@freezed\nclass RowBannerEvent with _$RowBannerEvent {\n  const factory RowBannerEvent.initial() = _Initial;\n  const factory RowBannerEvent.didReceiveRowMeta(RowMetaPB rowMeta) =\n      _DidReceiveRowMeta;\n  const factory RowBannerEvent.didReceiveFieldUpdate(FieldPB field) =\n      _DidReceiveFieldUpdate;\n  const factory RowBannerEvent.setIcon(String iconURL) = _SetIcon;\n  const factory RowBannerEvent.setCover(RowCoverPB cover) = _SetCover;\n  const factory RowBannerEvent.removeCover() = _RemoveCover;\n}\n\n@freezed\nclass RowBannerState extends Equatable with _$RowBannerState {\n  const RowBannerState._();\n\n  const factory RowBannerState({\n    required FieldPB? primaryField,\n    required RowMetaPB rowMeta,\n    required LoadingState loadingState,\n  }) = _RowBannerState;\n\n  factory RowBannerState.initial(RowMetaPB rowMetaPB) => RowBannerState(\n        primaryField: null,\n        rowMeta: rowMetaPB,\n        loadingState: const LoadingState.loading(),\n      );\n\n  @override\n  List<Object?> get props => [\n        rowMeta.cover.data,\n        rowMeta.icon,\n        primaryField,\n        loadingState,\n      ];\n}\n\n@freezed\nclass LoadingState with _$LoadingState {\n  const factory LoadingState.loading() = _Loading;\n  const factory LoadingState.finish() = _Finish;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/row/row_cache.dart",
    "content": "import 'dart:collection';\n\nimport 'package:equatable/equatable.dart';\nimport 'package:flutter/foundation.dart';\n\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport '../cell/cell_cache.dart';\nimport '../cell/cell_controller.dart';\n\nimport 'row_list.dart';\nimport 'row_service.dart';\n\npart 'row_cache.freezed.dart';\n\ntypedef RowUpdateCallback = void Function();\n\n/// A delegate that provides the fields of the row.\nabstract class RowFieldsDelegate {\n  UnmodifiableListView<FieldInfo> get fieldInfos;\n  void onFieldsChanged(void Function(List<FieldInfo>) callback);\n}\n\nabstract mixin class RowLifeCycle {\n  void onRowDisposed();\n}\n\n/// Read https://docs.appflowy.io/docs/documentation/software-contributions/architecture/frontend/frontend/grid for more information.\n\nclass RowCache {\n  RowCache({\n    required this.viewId,\n    required RowFieldsDelegate fieldsDelegate,\n    required RowLifeCycle rowLifeCycle,\n  })  : _cellMemCache = CellMemCache(),\n        _changedNotifier = RowChangesetNotifier(),\n        _rowLifeCycle = rowLifeCycle,\n        _fieldDelegate = fieldsDelegate {\n    // Listen to field changes. If a field is deleted, we can safely remove the\n    // cells corresponding to that field from our cache.\n    fieldsDelegate.onFieldsChanged((fieldInfos) {\n      for (final fieldInfo in fieldInfos) {\n        _cellMemCache.removeCellWithFieldId(fieldInfo.id);\n      }\n\n      _changedNotifier?.receive(const ChangedReason.fieldDidChange());\n    });\n  }\n\n  final String viewId;\n  final RowList _rowList = RowList();\n  final CellMemCache _cellMemCache;\n  final RowLifeCycle _rowLifeCycle;\n  final RowFieldsDelegate _fieldDelegate;\n  RowChangesetNotifier? _changedNotifier;\n  bool _isInitialRows = false;\n  final List<RowsVisibilityChangePB> _pendingVisibilityChanges = [];\n\n  /// Returns a unmodifiable list of RowInfo\n  UnmodifiableListView<RowInfo> get rowInfos {\n    final visibleRows = [..._rowList.rows];\n    return UnmodifiableListView(visibleRows);\n  }\n\n  /// Returns a unmodifiable map of RowInfo\n  UnmodifiableMapView<RowId, RowInfo> get rowByRowId {\n    return UnmodifiableMapView(_rowList.rowInfoByRowId);\n  }\n\n  CellMemCache get cellCache => _cellMemCache;\n  ChangedReason get changeReason =>\n      _changedNotifier?.reason ?? const InitialListState();\n\n  RowInfo? getRow(RowId rowId) {\n    return _rowList.get(rowId);\n  }\n\n  void setInitialRows(List<RowMetaPB> rows) {\n    for (final row in rows) {\n      final rowInfo = buildGridRow(row);\n      _rowList.add(rowInfo);\n    }\n    _isInitialRows = true;\n    _changedNotifier?.receive(const ChangedReason.setInitialRows());\n\n    for (final changeset in _pendingVisibilityChanges) {\n      applyRowsVisibility(changeset);\n    }\n    _pendingVisibilityChanges.clear();\n  }\n\n  void setRowMeta(RowMetaPB rowMeta) {\n    final rowInfo = _rowList.get(rowMeta.id);\n    if (rowInfo != null) {\n      rowInfo.updateRowMeta(rowMeta);\n    }\n\n    _changedNotifier?.receive(const ChangedReason.didFetchRow());\n  }\n\n  void dispose() {\n    _rowList.dispose();\n    _rowLifeCycle.onRowDisposed();\n    _changedNotifier?.dispose();\n    _changedNotifier = null;\n    _cellMemCache.dispose();\n  }\n\n  void applyRowsChanged(RowsChangePB changeset) {\n    _deleteRows(changeset.deletedRows);\n    _insertRows(changeset.insertedRows);\n    _updateRows(changeset.updatedRows);\n  }\n\n  void applyRowsVisibility(RowsVisibilityChangePB changeset) {\n    if (_isInitialRows) {\n      _hideRows(changeset.invisibleRows);\n      _showRows(changeset.visibleRows);\n      _changedNotifier?.receive(\n        ChangedReason.updateRowsVisibility(changeset),\n      );\n    } else {\n      _pendingVisibilityChanges.add(changeset);\n    }\n  }\n\n  void reorderAllRows(List<String> rowIds) {\n    _rowList.reorderWithRowIds(rowIds);\n    _changedNotifier?.receive(const ChangedReason.reorderRows());\n  }\n\n  void reorderSingleRow(ReorderSingleRowPB reorderRow) {\n    final rowInfo = _rowList.get(reorderRow.rowId);\n    if (rowInfo != null) {\n      _rowList.moveRow(\n        reorderRow.rowId,\n        reorderRow.oldIndex,\n        reorderRow.newIndex,\n      );\n      _changedNotifier?.receive(\n        ChangedReason.reorderSingleRow(\n          reorderRow,\n          rowInfo,\n        ),\n      );\n    }\n  }\n\n  void _deleteRows(List<RowId> deletedRowIds) {\n    for (final rowId in deletedRowIds) {\n      final deletedRow = _rowList.remove(rowId);\n      if (deletedRow != null) {\n        _changedNotifier?.receive(ChangedReason.delete(deletedRow));\n      }\n    }\n  }\n\n  void _insertRows(List<InsertedRowPB> insertRows) {\n    final InsertedIndexs insertedIndices = [];\n    for (final insertedRow in insertRows) {\n      if (insertedRow.hasIndex()) {\n        final index = _rowList.insert(\n          insertedRow.index,\n          buildGridRow(insertedRow.rowMeta),\n        );\n        if (index != null) {\n          insertedIndices.add(index);\n        }\n      }\n    }\n    _changedNotifier?.receive(ChangedReason.insert(insertedIndices));\n  }\n\n  void _updateRows(List<UpdatedRowPB> updatedRows) {\n    if (updatedRows.isEmpty) return;\n    final List<RowMetaPB> updatedList = [];\n    for (final updatedRow in updatedRows) {\n      for (final fieldId in updatedRow.fieldIds) {\n        final key = CellContext(\n          fieldId: fieldId,\n          rowId: updatedRow.rowId,\n        );\n        _cellMemCache.remove(key);\n      }\n      if (updatedRow.hasRowMeta()) {\n        updatedList.add(updatedRow.rowMeta);\n      }\n    }\n\n    final updatedIndexs = _rowList.updateRows(\n      rowMetas: updatedList,\n      builder: (rowId) => buildGridRow(rowId),\n    );\n\n    if (updatedIndexs.isNotEmpty) {\n      _changedNotifier?.receive(ChangedReason.update(updatedIndexs));\n    }\n  }\n\n  void _hideRows(List<RowId> invisibleRows) {\n    for (final rowId in invisibleRows) {\n      final deletedRow = _rowList.remove(rowId);\n      if (deletedRow != null) {\n        _changedNotifier?.receive(ChangedReason.delete(deletedRow));\n      }\n    }\n  }\n\n  void _showRows(List<InsertedRowPB> visibleRows) {\n    for (final insertedRow in visibleRows) {\n      final insertedIndex =\n          _rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta));\n      if (insertedIndex != null) {\n        _changedNotifier?.receive(ChangedReason.insert([insertedIndex]));\n      }\n    }\n  }\n\n  void onRowsChanged(void Function(ChangedReason) onRowChanged) {\n    _changedNotifier?.addListener(() {\n      if (_changedNotifier != null) {\n        onRowChanged(_changedNotifier!.reason);\n      }\n    });\n  }\n\n  RowUpdateCallback addListener({\n    required RowId rowId,\n    void Function(List<CellContext>, ChangedReason)? onRowChanged,\n  }) {\n    void listenerHandler() async {\n      if (onRowChanged != null) {\n        final rowInfo = _rowList.get(rowId);\n        if (rowInfo != null) {\n          final cellDataMap = _makeCells(rowInfo.rowMeta);\n          if (_changedNotifier != null) {\n            onRowChanged(cellDataMap, _changedNotifier!.reason);\n          }\n        }\n      }\n    }\n\n    _changedNotifier?.addListener(listenerHandler);\n    return listenerHandler;\n  }\n\n  void removeRowListener(VoidCallback callback) {\n    _changedNotifier?.removeListener(callback);\n  }\n\n  List<CellContext> loadCells(RowMetaPB rowMeta) {\n    final rowInfo = _rowList.get(rowMeta.id);\n    if (rowInfo == null) {\n      _loadRow(rowMeta.id);\n    }\n    final cells = _makeCells(rowMeta);\n    return cells;\n  }\n\n  Future<void> _loadRow(RowId rowId) async {\n    final result = await RowBackendService.getRow(viewId: viewId, rowId: rowId);\n    result.fold(\n      (rowMetaPB) {\n        final rowInfo = _rowList.get(rowMetaPB.id);\n        final rowIndex = _rowList.indexOfRow(rowMetaPB.id);\n        if (rowInfo != null && rowIndex != null) {\n          rowInfo.rowMetaNotifier.value = rowMetaPB;\n\n          final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();\n          updatedIndexs[rowMetaPB.id] = UpdatedIndex(\n            index: rowIndex,\n            rowId: rowMetaPB.id,\n          );\n\n          _changedNotifier?.receive(ChangedReason.update(updatedIndexs));\n        }\n      },\n      (err) => Log.error(err),\n    );\n  }\n\n  List<CellContext> _makeCells(RowMetaPB rowMeta) {\n    return _fieldDelegate.fieldInfos\n        .map(\n          (fieldInfo) => CellContext(\n            rowId: rowMeta.id,\n            fieldId: fieldInfo.id,\n          ),\n        )\n        .toList();\n  }\n\n  RowInfo buildGridRow(RowMetaPB rowMetaPB) {\n    return RowInfo(\n      fields: _fieldDelegate.fieldInfos,\n      rowMeta: rowMetaPB,\n    );\n  }\n}\n\nclass RowChangesetNotifier extends ChangeNotifier {\n  RowChangesetNotifier();\n\n  ChangedReason reason = const InitialListState();\n\n  void receive(ChangedReason newReason) {\n    reason = newReason;\n    reason.map(\n      insert: (_) => notifyListeners(),\n      delete: (_) => notifyListeners(),\n      update: (_) => notifyListeners(),\n      fieldDidChange: (_) => notifyListeners(),\n      initial: (_) {},\n      reorderRows: (_) => notifyListeners(),\n      reorderSingleRow: (_) => notifyListeners(),\n      updateRowsVisibility: (_) => notifyListeners(),\n      setInitialRows: (_) => notifyListeners(),\n      didFetchRow: (_) => notifyListeners(),\n    );\n  }\n}\n\nclass RowInfo extends Equatable {\n  RowInfo({\n    required this.fields,\n    required RowMetaPB rowMeta,\n  })  : rowMetaNotifier = ValueNotifier<RowMetaPB>(rowMeta),\n        rowIconNotifier = ValueNotifier<String>(rowMeta.icon),\n        rowDocumentNotifier = ValueNotifier<bool>(\n          !(rowMeta.hasIsDocumentEmpty() ? rowMeta.isDocumentEmpty : true),\n        );\n\n  final UnmodifiableListView<FieldInfo> fields;\n  final ValueNotifier<RowMetaPB> rowMetaNotifier;\n  final ValueNotifier<String> rowIconNotifier;\n  final ValueNotifier<bool> rowDocumentNotifier;\n\n  String get rowId => rowMetaNotifier.value.id;\n\n  RowMetaPB get rowMeta => rowMetaNotifier.value;\n\n  /// Updates the RowMeta and automatically updates the related notifiers.\n  void updateRowMeta(RowMetaPB newMeta) {\n    rowMetaNotifier.value = newMeta;\n    rowIconNotifier.value = newMeta.icon;\n    rowDocumentNotifier.value = !newMeta.isDocumentEmpty;\n  }\n\n  /// Dispose of the notifiers when they are no longer needed.\n  void dispose() {\n    rowMetaNotifier.dispose();\n    rowIconNotifier.dispose();\n    rowDocumentNotifier.dispose();\n  }\n\n  @override\n  List<Object> get props => [rowMeta];\n}\n\ntypedef InsertedIndexs = List<InsertedIndex>;\ntypedef DeletedIndexs = List<DeletedIndex>;\n// key: id of the row\n// value: UpdatedIndex\ntypedef UpdatedIndexMap = LinkedHashMap<RowId, UpdatedIndex>;\n\n@freezed\nclass ChangedReason with _$ChangedReason {\n  const factory ChangedReason.insert(InsertedIndexs items) = _Insert;\n  const factory ChangedReason.delete(DeletedIndex item) = _Delete;\n  const factory ChangedReason.update(UpdatedIndexMap indexs) = _Update;\n  const factory ChangedReason.fieldDidChange() = _FieldDidChange;\n  const factory ChangedReason.initial() = InitialListState;\n  const factory ChangedReason.didFetchRow() = _DidFetchRow;\n  const factory ChangedReason.reorderRows() = _ReorderRows;\n  const factory ChangedReason.reorderSingleRow(\n    ReorderSingleRowPB reorderRow,\n    RowInfo rowInfo,\n  ) = _ReorderSingleRow;\n  const factory ChangedReason.updateRowsVisibility(\n    RowsVisibilityChangePB changeset,\n  ) = _UpdateRowsVisibility;\n  const factory ChangedReason.setInitialRows() = _SetInitialRows;\n}\n\nclass InsertedIndex {\n  InsertedIndex({\n    required this.index,\n    required this.rowId,\n  });\n\n  final int index;\n  final RowId rowId;\n}\n\nclass DeletedIndex {\n  DeletedIndex({\n    required this.index,\n    required this.rowInfo,\n  });\n\n  final int index;\n  final RowInfo rowInfo;\n}\n\nclass UpdatedIndex {\n  UpdatedIndex({\n    required this.index,\n    required this.rowId,\n  });\n\n  final int index;\n  final RowId rowId;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/row/row_controller.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy/plugins/database/domain/row_listener.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter/material.dart';\n\nimport '../cell/cell_cache.dart';\nimport '../cell/cell_controller.dart';\nimport 'row_cache.dart';\n\ntypedef OnRowChanged = void Function(List<CellContext>, ChangedReason);\n\nclass RowController {\n  RowController({\n    required RowMetaPB rowMeta,\n    required this.viewId,\n    required RowCache rowCache,\n    this.groupId,\n  })  : _rowMeta = rowMeta,\n        _rowCache = rowCache,\n        _rowBackendSvc = RowBackendService(viewId: viewId),\n        _rowListener = RowListener(rowMeta.id);\n\n  RowMetaPB _rowMeta;\n  final String? groupId;\n  VoidCallback? _onRowMetaChanged;\n  final String viewId;\n  final List<VoidCallback> _onRowChangedListeners = [];\n  final RowCache _rowCache;\n  final RowListener _rowListener;\n  final RowBackendService _rowBackendSvc;\n  bool _isDisposed = false;\n\n  String get rowId => _rowMeta.id;\n  RowMetaPB get rowMeta => _rowMeta;\n  CellMemCache get cellCache => _rowCache.cellCache;\n\n  List<CellContext> loadCells() => _rowCache.loadCells(rowMeta);\n\n  /// This method must be called to initialize the row controller; otherwise, the row will not sync between devices.\n  /// When creating a row controller, calling [initialize] immediately may not be necessary.\n  /// Only call [initialize] when the row becomes visible. This approach helps reduce unnecessary sync operations.\n  Future<void> initialize() async {\n    await _rowBackendSvc.initRow(rowMeta.id);\n    unawaited(\n      _rowBackendSvc.getRowMeta(rowId).then(\n        (result) {\n          if (_isDisposed) {\n            return;\n          }\n\n          result.fold(\n            (rowMeta) {\n              _rowMeta = rowMeta;\n              _rowCache.setRowMeta(rowMeta);\n              _onRowMetaChanged?.call();\n            },\n            (error) => debugPrint(error.toString()),\n          );\n        },\n      ),\n    );\n\n    _rowListener.start(\n      onRowFetched: (DidFetchRowPB row) {\n        _rowCache.setRowMeta(row.meta);\n      },\n      onMetaChanged: (newRowMeta) {\n        if (_isDisposed) {\n          return;\n        }\n        _rowMeta = newRowMeta;\n        _rowCache.setRowMeta(newRowMeta);\n        _onRowMetaChanged?.call();\n      },\n    );\n  }\n\n  void addListener({\n    OnRowChanged? onRowChanged,\n    VoidCallback? onMetaChanged,\n  }) {\n    final fn = _rowCache.addListener(\n      rowId: rowMeta.id,\n      onRowChanged: (context, reasons) {\n        if (_isDisposed) {\n          return;\n        }\n        onRowChanged?.call(context, reasons);\n      },\n    );\n\n    // Add the listener to the list so that we can remove it later.\n    _onRowChangedListeners.add(fn);\n    _onRowMetaChanged = onMetaChanged;\n  }\n\n  Future<void> dispose() async {\n    _isDisposed = true;\n    await _rowListener.stop();\n    for (final fn in _onRowChangedListeners) {\n      _rowCache.removeRowListener(fn);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/row/row_list.dart",
    "content": "import 'dart:collection';\nimport 'dart:math';\n\nimport 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';\n\nimport 'row_cache.dart';\nimport 'row_service.dart';\n\nclass RowList {\n  /// Use List to reverse the order of the row.\n  List<RowInfo> _rowInfos = [];\n\n  List<RowInfo> get rows => List.from(_rowInfos);\n\n  /// Use Map for faster access the raw row data.\n  final HashMap<RowId, RowInfo> rowInfoByRowId = HashMap();\n\n  RowInfo? get(RowId rowId) {\n    return rowInfoByRowId[rowId];\n  }\n\n  int? indexOfRow(RowId rowId) {\n    final rowInfo = rowInfoByRowId[rowId];\n    if (rowInfo != null) {\n      return _rowInfos.indexOf(rowInfo);\n    }\n    return null;\n  }\n\n  void add(RowInfo rowInfo) {\n    final rowId = rowInfo.rowId;\n    if (contains(rowId)) {\n      final index = _rowInfos.indexWhere((element) => element.rowId == rowId);\n      _rowInfos.removeAt(index);\n      _rowInfos.insert(index, rowInfo);\n    } else {\n      _rowInfos.add(rowInfo);\n    }\n    rowInfoByRowId[rowId] = rowInfo;\n  }\n\n  InsertedIndex? insert(int index, RowInfo rowInfo) {\n    final rowId = rowInfo.rowId;\n    var insertedIndex = index;\n    if (_rowInfos.length <= insertedIndex) {\n      insertedIndex = _rowInfos.length;\n    }\n\n    final oldRowInfo = get(rowId);\n    if (oldRowInfo != null) {\n      _rowInfos.insert(insertedIndex, rowInfo);\n      _rowInfos.remove(oldRowInfo);\n      rowInfoByRowId[rowId] = rowInfo;\n      return null;\n    } else {\n      _rowInfos.insert(insertedIndex, rowInfo);\n      rowInfoByRowId[rowId] = rowInfo;\n      return InsertedIndex(index: insertedIndex, rowId: rowId);\n    }\n  }\n\n  DeletedIndex? remove(RowId rowId) {\n    final rowInfo = rowInfoByRowId[rowId];\n    if (rowInfo != null) {\n      final index = _rowInfos.indexOf(rowInfo);\n      if (index != -1) {\n        rowInfoByRowId.remove(rowInfo.rowId);\n        _rowInfos.remove(rowInfo);\n      }\n      return DeletedIndex(index: index, rowInfo: rowInfo);\n    } else {\n      return null;\n    }\n  }\n\n  InsertedIndexs insertRows(\n    List<InsertedRowPB> insertedRows,\n    RowInfo Function(RowMetaPB) builder,\n  ) {\n    final InsertedIndexs insertIndexs = [];\n    for (final insertRow in insertedRows) {\n      final isContains = contains(insertRow.rowMeta.id);\n\n      var index = insertRow.index;\n      if (_rowInfos.length < index) {\n        index = _rowInfos.length;\n      }\n      insert(index, builder(insertRow.rowMeta));\n\n      if (!isContains) {\n        insertIndexs.add(\n          InsertedIndex(\n            index: index,\n            rowId: insertRow.rowMeta.id,\n          ),\n        );\n      }\n    }\n    return insertIndexs;\n  }\n\n  DeletedIndexs removeRows(List<String> rowIds) {\n    final List<RowInfo> newRows = [];\n    final DeletedIndexs deletedIndex = [];\n    final Map<String, String> deletedRowByRowId = {\n      for (final rowId in rowIds) rowId: rowId,\n    };\n\n    _rowInfos.asMap().forEach((index, RowInfo rowInfo) {\n      if (deletedRowByRowId[rowInfo.rowId] == null) {\n        newRows.add(rowInfo);\n      } else {\n        rowInfoByRowId.remove(rowInfo.rowId);\n        deletedIndex.add(DeletedIndex(index: index, rowInfo: rowInfo));\n      }\n    });\n    _rowInfos = newRows;\n    return deletedIndex;\n  }\n\n  UpdatedIndexMap updateRows({\n    required List<RowMetaPB> rowMetas,\n    required RowInfo Function(RowMetaPB) builder,\n  }) {\n    final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();\n    for (final rowMeta in rowMetas) {\n      final index = _rowInfos.indexWhere(\n        (rowInfo) => rowInfo.rowId == rowMeta.id,\n      );\n      if (index != -1) {\n        rowInfoByRowId[rowMeta.id]?.updateRowMeta(rowMeta);\n      } else {\n        final insertIndex = max(index, _rowInfos.length);\n        final rowInfo = builder(rowMeta);\n        insert(insertIndex, rowInfo);\n        updatedIndexs[rowMeta.id] = UpdatedIndex(\n          index: insertIndex,\n          rowId: rowMeta.id,\n        );\n      }\n    }\n    return updatedIndexs;\n  }\n\n  void reorderWithRowIds(List<String> rowIds) {\n    _rowInfos.clear();\n\n    for (final rowId in rowIds) {\n      final rowInfo = rowInfoByRowId[rowId];\n      if (rowInfo != null) {\n        _rowInfos.add(rowInfo);\n      }\n    }\n  }\n\n  void moveRow(RowId rowId, int oldIndex, int newIndex) {\n    final index = _rowInfos.indexWhere(\n      (rowInfo) => rowInfo.rowId == rowId,\n    );\n    if (index != -1) {\n      final rowInfo = remove(rowId)!.rowInfo;\n      insert(newIndex, rowInfo);\n    }\n  }\n\n  bool contains(RowId rowId) {\n    return rowInfoByRowId[rowId] != null;\n  }\n\n  void dispose() {\n    for (final rowInfo in _rowInfos) {\n      rowInfo.dispose();\n    }\n    _rowInfos.clear();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nimport '../field/field_info.dart';\n\ntypedef RowId = String;\n\nclass RowBackendService {\n  RowBackendService({required this.viewId});\n\n  final String viewId;\n\n  static Future<FlowyResult<RowMetaPB, FlowyError>> createRow({\n    required String viewId,\n    String? groupId,\n    void Function(RowDataBuilder builder)? withCells,\n    OrderObjectPositionTypePB? position,\n    String? targetRowId,\n  }) {\n    final payload = CreateRowPayloadPB(\n      viewId: viewId,\n      groupId: groupId,\n      rowPosition: OrderObjectPositionPB(\n        position: position,\n        objectId: targetRowId,\n      ),\n    );\n\n    if (withCells != null) {\n      final rowBuilder = RowDataBuilder();\n      withCells(rowBuilder);\n      payload.data.addAll(rowBuilder.build());\n    }\n\n    return DatabaseEventCreateRow(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> initRow(RowId rowId) async {\n    final payload = DatabaseViewRowIdPB()\n      ..viewId = viewId\n      ..rowId = rowId;\n\n    return DatabaseEventInitRow(payload).send();\n  }\n\n  Future<FlowyResult<RowMetaPB, FlowyError>> createRowBefore(RowId rowId) {\n    return createRow(\n      viewId: viewId,\n      position: OrderObjectPositionTypePB.Before,\n      targetRowId: rowId,\n    );\n  }\n\n  Future<FlowyResult<RowMetaPB, FlowyError>> createRowAfter(RowId rowId) {\n    return createRow(\n      viewId: viewId,\n      position: OrderObjectPositionTypePB.After,\n      targetRowId: rowId,\n    );\n  }\n\n  static Future<FlowyResult<RowMetaPB, FlowyError>> getRow({\n    required String viewId,\n    required String rowId,\n  }) {\n    final payload = DatabaseViewRowIdPB()\n      ..viewId = viewId\n      ..rowId = rowId;\n\n    return DatabaseEventGetRowMeta(payload).send();\n  }\n\n  Future<FlowyResult<RowMetaPB, FlowyError>> getRowMeta(RowId rowId) {\n    final payload = DatabaseViewRowIdPB.create()\n      ..viewId = viewId\n      ..rowId = rowId;\n\n    return DatabaseEventGetRowMeta(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> updateMeta({\n    required String rowId,\n    String? iconURL,\n    RowCoverPB? cover,\n    bool? isDocumentEmpty,\n  }) {\n    final payload = UpdateRowMetaChangesetPB.create()\n      ..viewId = viewId\n      ..id = rowId;\n\n    if (iconURL != null) {\n      payload.iconUrl = iconURL;\n    }\n    if (cover != null) {\n      payload.cover = cover;\n    }\n\n    if (isDocumentEmpty != null) {\n      payload.isDocumentEmpty = isDocumentEmpty;\n    }\n\n    return DatabaseEventUpdateRowMeta(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> removeCover(String rowId) async {\n    final payload = RemoveCoverPayloadPB.create()\n      ..viewId = viewId\n      ..rowId = rowId;\n\n    return DatabaseEventRemoveCover(payload).send();\n  }\n\n  static Future<FlowyResult<void, FlowyError>> deleteRows(\n    String viewId,\n    List<RowId> rowIds,\n  ) {\n    final payload = RepeatedRowIdPB.create()\n      ..viewId = viewId\n      ..rowIds.addAll(rowIds);\n\n    return DatabaseEventDeleteRows(payload).send();\n  }\n\n  static Future<FlowyResult<void, FlowyError>> duplicateRow(\n    String viewId,\n    RowId rowId,\n  ) {\n    final payload = DatabaseViewRowIdPB(\n      viewId: viewId,\n      rowId: rowId,\n    );\n\n    return DatabaseEventDuplicateRow(payload).send();\n  }\n}\n\nclass RowDataBuilder {\n  final _cellDataByFieldId = <String, String>{};\n\n  void insertText(FieldInfo fieldInfo, String text) {\n    assert(fieldInfo.fieldType == FieldType.RichText);\n    _cellDataByFieldId[fieldInfo.field.id] = text;\n  }\n\n  void insertNumber(FieldInfo fieldInfo, int num) {\n    assert(fieldInfo.fieldType == FieldType.Number);\n    _cellDataByFieldId[fieldInfo.field.id] = num.toString();\n  }\n\n  void insertDate(FieldInfo fieldInfo, DateTime date) {\n    assert(fieldInfo.fieldType == FieldType.DateTime);\n    final timestamp = date.millisecondsSinceEpoch ~/ 1000;\n    _cellDataByFieldId[fieldInfo.field.id] = timestamp.toString();\n  }\n\n  Map<String, String> build() {\n    return _cellDataByFieldId;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/setting/group_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/domain/group_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/board_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'group_bloc.freezed.dart';\n\nclass DatabaseGroupBloc extends Bloc<DatabaseGroupEvent, DatabaseGroupState> {\n  DatabaseGroupBloc({\n    required String viewId,\n    required DatabaseController databaseController,\n  })  : _databaseController = databaseController,\n        _groupBackendSvc = GroupBackendService(viewId),\n        super(\n          DatabaseGroupState.initial(\n            viewId,\n            databaseController.fieldController.fieldInfos,\n            databaseController.databaseLayoutSetting!.board,\n            databaseController.fieldController.groupSettings,\n          ),\n        ) {\n    _dispatch();\n  }\n\n  final DatabaseController _databaseController;\n  final GroupBackendService _groupBackendSvc;\n  Function(List<FieldInfo>)? _onFieldsFn;\n  DatabaseLayoutSettingCallbacks? _layoutSettingCallbacks;\n\n  @override\n  Future<void> close() async {\n    if (_onFieldsFn != null) {\n      _databaseController.fieldController\n          .removeListener(onFieldsListener: _onFieldsFn!);\n      _onFieldsFn = null;\n    }\n    _layoutSettingCallbacks = null;\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<DatabaseGroupEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async => _startListening(),\n          didReceiveFieldUpdate: (fieldInfos) {\n            emit(\n              state.copyWith(\n                fieldInfos: fieldInfos,\n                groupSettings:\n                    _databaseController.fieldController.groupSettings,\n              ),\n            );\n          },\n          setGroupByField: (\n            String fieldId,\n            FieldType fieldType, [\n            List<int>? settingContent,\n          ]) async {\n            final result = await _groupBackendSvc.groupByField(\n              fieldId: fieldId,\n              settingContent: settingContent ?? [],\n            );\n            result.fold((l) => null, (err) => Log.error(err));\n          },\n          didUpdateLayoutSettings: (layoutSettings) {\n            emit(state.copyWith(layoutSettings: layoutSettings));\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _onFieldsFn = (fieldInfos) =>\n        add(DatabaseGroupEvent.didReceiveFieldUpdate(fieldInfos));\n    _databaseController.fieldController.addListener(\n      onReceiveFields: _onFieldsFn,\n      listenWhen: () => !isClosed,\n    );\n\n    _layoutSettingCallbacks = DatabaseLayoutSettingCallbacks(\n      onLayoutSettingsChanged: (layoutSettings) {\n        if (isClosed || !layoutSettings.hasBoard()) {\n          return;\n        }\n        add(\n          DatabaseGroupEvent.didUpdateLayoutSettings(layoutSettings.board),\n        );\n      },\n    );\n    _databaseController.addListener(\n      onLayoutSettingsChanged: _layoutSettingCallbacks,\n    );\n  }\n}\n\n@freezed\nclass DatabaseGroupEvent with _$DatabaseGroupEvent {\n  const factory DatabaseGroupEvent.initial() = _Initial;\n  const factory DatabaseGroupEvent.setGroupByField(\n    String fieldId,\n    FieldType fieldType, [\n    @Default([]) List<int> settingContent,\n  ]) = _DatabaseGroupEvent;\n  const factory DatabaseGroupEvent.didReceiveFieldUpdate(\n    List<FieldInfo> fields,\n  ) = _DidReceiveFieldUpdate;\n  const factory DatabaseGroupEvent.didUpdateLayoutSettings(\n    BoardLayoutSettingPB layoutSettings,\n  ) = _DidUpdateLayoutSettings;\n}\n\n@freezed\nclass DatabaseGroupState with _$DatabaseGroupState {\n  const factory DatabaseGroupState({\n    required String viewId,\n    required List<FieldInfo> fieldInfos,\n    required BoardLayoutSettingPB layoutSettings,\n    required List<GroupSettingPB> groupSettings,\n  }) = _DatabaseGroupState;\n\n  factory DatabaseGroupState.initial(\n    String viewId,\n    List<FieldInfo> fieldInfos,\n    BoardLayoutSettingPB layoutSettings,\n    List<GroupSettingPB> groupSettings,\n  ) =>\n      DatabaseGroupState(\n        viewId: viewId,\n        fieldInfos: fieldInfos,\n        layoutSettings: layoutSettings,\n        groupSettings: groupSettings,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/setting/property_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/plugins/database/domain/field_settings_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'property_bloc.freezed.dart';\n\nclass DatabasePropertyBloc\n    extends Bloc<DatabasePropertyEvent, DatabasePropertyState> {\n  DatabasePropertyBloc({\n    required String viewId,\n    required FieldController fieldController,\n  })  : _fieldController = fieldController,\n        super(\n          DatabasePropertyState.initial(\n            viewId,\n            fieldController.fieldInfos,\n          ),\n        ) {\n    _dispatch();\n  }\n\n  final FieldController _fieldController;\n  Function(List<FieldInfo>)? _onFieldsFn;\n\n  @override\n  Future<void> close() async {\n    if (_onFieldsFn != null) {\n      _fieldController.removeListener(onFieldsListener: _onFieldsFn!);\n      _onFieldsFn = null;\n    }\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<DatabasePropertyEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () {\n            _startListening();\n          },\n          setFieldVisibility: (fieldId, visibility) async {\n            final fieldSettingsSvc =\n                FieldSettingsBackendService(viewId: state.viewId);\n\n            final result = await fieldSettingsSvc.updateFieldSettings(\n              fieldId: fieldId,\n              fieldVisibility: visibility,\n            );\n\n            result.fold((l) => null, (err) => Log.error(err));\n          },\n          didReceiveFieldUpdate: (fields) {\n            emit(state.copyWith(fieldContexts: fields));\n          },\n          moveField: (fromIndex, toIndex) async {\n            if (fromIndex < toIndex) {\n              toIndex--;\n            }\n            final fromId = state.fieldContexts[fromIndex].field.id;\n            final toId = state.fieldContexts[toIndex].field.id;\n\n            final fieldContexts = List<FieldInfo>.from(state.fieldContexts);\n            fieldContexts.insert(toIndex, fieldContexts.removeAt(fromIndex));\n            emit(state.copyWith(fieldContexts: fieldContexts));\n\n            final result = await FieldBackendService.moveField(\n              viewId: state.viewId,\n              fromFieldId: fromId,\n              toFieldId: toId,\n            );\n\n            result.fold((l) => null, (r) => Log.error(r));\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _onFieldsFn =\n        (fields) => add(DatabasePropertyEvent.didReceiveFieldUpdate(fields));\n    _fieldController.addListener(\n      onReceiveFields: _onFieldsFn,\n      listenWhen: () => !isClosed,\n    );\n  }\n}\n\n@freezed\nclass DatabasePropertyEvent with _$DatabasePropertyEvent {\n  const factory DatabasePropertyEvent.initial() = _Initial;\n  const factory DatabasePropertyEvent.setFieldVisibility(\n    String fieldId,\n    FieldVisibility visibility,\n  ) = _SetFieldVisibility;\n  const factory DatabasePropertyEvent.didReceiveFieldUpdate(\n    List<FieldInfo> fields,\n  ) = _DidReceiveFieldUpdate;\n  const factory DatabasePropertyEvent.moveField(int fromIndex, int toIndex) =\n      _MoveField;\n}\n\n@freezed\nclass DatabasePropertyState with _$DatabasePropertyState {\n  const factory DatabasePropertyState({\n    required String viewId,\n    required List<FieldInfo> fieldContexts,\n  }) = _GridPropertyState;\n\n  factory DatabasePropertyState.initial(\n    String viewId,\n    List<FieldInfo> fieldContexts,\n  ) =>\n      DatabasePropertyState(\n        viewId: viewId,\n        fieldContexts: fieldContexts,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/setting/setting_listener.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/grid_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flowy_infra/notifier.dart';\n\ntypedef UpdateSettingNotifiedValue\n    = FlowyResult<DatabaseViewSettingPB, FlowyError>;\n\nclass DatabaseSettingListener {\n  DatabaseSettingListener({required this.viewId});\n\n  final String viewId;\n\n  DatabaseNotificationListener? _listener;\n  PublishNotifier<UpdateSettingNotifiedValue>? _updateSettingNotifier =\n      PublishNotifier();\n\n  void start({\n    required void Function(UpdateSettingNotifiedValue) onSettingUpdated,\n  }) {\n    _updateSettingNotifier?.addPublishListener(onSettingUpdated);\n    _listener =\n        DatabaseNotificationListener(objectId: viewId, handler: _handler);\n  }\n\n  void _handler(\n    DatabaseNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case DatabaseNotification.DidUpdateSettings:\n        result.fold(\n          (payload) => _updateSettingNotifier?.value = FlowyResult.success(\n            DatabaseViewSettingPB.fromBuffer(payload),\n          ),\n          (error) => _updateSettingNotifier?.value = FlowyResult.failure(error),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _listener?.stop();\n    _updateSettingNotifier?.dispose();\n    _updateSettingNotifier = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/setting/setting_service.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass SettingBackendService {\n  const SettingBackendService({required this.viewId});\n\n  final String viewId;\n\n  Future<FlowyResult<DatabaseViewSettingPB, FlowyError>> getSetting() {\n    final payload = DatabaseViewIdPB.create()..value = viewId;\n    return DatabaseEventGetDatabaseSetting(payload).send();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/share_bloc.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/workspace/application/settings/share/export_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'share_bloc.freezed.dart';\n\nclass DatabaseShareBloc extends Bloc<DatabaseShareEvent, DatabaseShareState> {\n  DatabaseShareBloc({\n    required this.view,\n  }) : super(const DatabaseShareState.initial()) {\n    on<ShareCSV>(_onShareCSV);\n  }\n\n  final ViewPB view;\n\n  Future<void> _onShareCSV(\n    ShareCSV event,\n    Emitter<DatabaseShareState> emit,\n  ) async {\n    emit(const DatabaseShareState.loading());\n\n    final result = await BackendExportService.exportDatabaseAsCSV(view.id);\n    result.fold(\n      (l) => _saveCSVToPath(l.data, event.path),\n      (r) => Log.error(r),\n    );\n\n    emit(\n      DatabaseShareState.finish(\n        result.fold(\n          (l) {\n            _saveCSVToPath(l.data, event.path);\n            return FlowyResult.success(null);\n          },\n          (r) => FlowyResult.failure(r),\n        ),\n      ),\n    );\n  }\n\n  ExportDataPB _saveCSVToPath(String markdown, String path) {\n    File(path).writeAsStringSync(markdown);\n    return ExportDataPB()\n      ..data = markdown\n      ..exportType = ExportType.Markdown;\n  }\n}\n\n@freezed\nclass DatabaseShareEvent with _$DatabaseShareEvent {\n  const factory DatabaseShareEvent.shareCSV(String path) = ShareCSV;\n}\n\n@freezed\nclass DatabaseShareState with _$DatabaseShareState {\n  const factory DatabaseShareState.initial() = _Initial;\n  const factory DatabaseShareState.loading() = _Loading;\n  const factory DatabaseShareState.finish(\n    FlowyResult<void, FlowyError> successOrFail,\n  ) = _Finish;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/sync/database_sync_state_listener.dart';\nimport 'package:appflowy/plugins/database/domain/database_view_service.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:connectivity_plus/connectivity_plus.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'database_sync_bloc.freezed.dart';\n\nclass DatabaseSyncBloc extends Bloc<DatabaseSyncEvent, DatabaseSyncBlocState> {\n  DatabaseSyncBloc({\n    required this.view,\n  }) : super(DatabaseSyncBlocState.initial()) {\n    on<DatabaseSyncEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            final userProfile = await getIt<AuthService>().getUser().then(\n                  (value) => value.fold((s) => s, (f) => null),\n                );\n            final databaseId = await DatabaseViewBackendService(viewId: view.id)\n                .getDatabaseId()\n                .then((value) => value.fold((s) => s, (f) => null));\n            emit(\n              state.copyWith(\n                shouldShowIndicator:\n                    userProfile?.workspaceType == WorkspaceTypePB.ServerW &&\n                        databaseId != null,\n              ),\n            );\n            if (databaseId != null) {\n              _syncStateListener =\n                  DatabaseSyncStateListener(databaseId: databaseId)\n                    ..start(\n                      didReceiveSyncState: (syncState) {\n                        Log.info(\n                          'database sync state changed, from ${state.syncState} to $syncState',\n                        );\n                        add(DatabaseSyncEvent.syncStateChanged(syncState));\n                      },\n                    );\n            }\n\n            final isNetworkConnected = await _connectivity\n                .checkConnectivity()\n                .then((value) => value != ConnectivityResult.none);\n            emit(state.copyWith(isNetworkConnected: isNetworkConnected));\n\n            connectivityStream =\n                _connectivity.onConnectivityChanged.listen((result) {\n              add(DatabaseSyncEvent.networkStateChanged(result));\n            });\n          },\n          syncStateChanged: (syncState) {\n            emit(state.copyWith(syncState: syncState.value));\n          },\n          networkStateChanged: (result) {\n            emit(\n              state.copyWith(\n                isNetworkConnected: result != ConnectivityResult.none,\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  final ViewPB view;\n  final _connectivity = Connectivity();\n\n  StreamSubscription? connectivityStream;\n  DatabaseSyncStateListener? _syncStateListener;\n\n  @override\n  Future<void> close() async {\n    await connectivityStream?.cancel();\n    await _syncStateListener?.stop();\n    return super.close();\n  }\n}\n\n@freezed\nclass DatabaseSyncEvent with _$DatabaseSyncEvent {\n  const factory DatabaseSyncEvent.initial() = Initial;\n  const factory DatabaseSyncEvent.syncStateChanged(\n    DatabaseSyncStatePB syncState,\n  ) = syncStateChanged;\n  const factory DatabaseSyncEvent.networkStateChanged(\n    ConnectivityResult result,\n  ) = NetworkStateChanged;\n}\n\n@freezed\nclass DatabaseSyncBlocState with _$DatabaseSyncBlocState {\n  const factory DatabaseSyncBlocState({\n    required DatabaseSyncState syncState,\n    @Default(true) bool isNetworkConnected,\n    @Default(false) bool shouldShowIndicator,\n  }) = _DatabaseSyncState;\n\n  factory DatabaseSyncBlocState.initial() => const DatabaseSyncBlocState(\n        syncState: DatabaseSyncState.Syncing,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_state_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/grid_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\ntypedef DatabaseSyncStateCallback = void Function(\n  DatabaseSyncStatePB syncState,\n);\n\nclass DatabaseSyncStateListener {\n  DatabaseSyncStateListener({\n    // NOTE: NOT the view id.\n    required this.databaseId,\n  });\n\n  final String databaseId;\n  StreamSubscription<SubscribeObject>? _subscription;\n  DatabaseNotificationParser? _parser;\n\n  DatabaseSyncStateCallback? didReceiveSyncState;\n\n  void start({\n    DatabaseSyncStateCallback? didReceiveSyncState,\n  }) {\n    this.didReceiveSyncState = didReceiveSyncState;\n\n    _parser = DatabaseNotificationParser(\n      id: databaseId,\n      callback: _callback,\n    );\n    _subscription = RustStreamReceiver.listen(\n      (observable) => _parser?.parse(observable),\n    );\n  }\n\n  void _callback(\n    DatabaseNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case DatabaseNotification.DidUpdateDatabaseSyncUpdate:\n        result.map(\n          (r) {\n            final value = DatabaseSyncStatePB.fromBuffer(r);\n            didReceiveSyncState?.call(value);\n          },\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _subscription?.cancel();\n    _subscription = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart",
    "content": "import 'package:appflowy/plugins/database/domain/database_view_service.dart';\nimport 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';\nimport 'package:appflowy/plugins/document/presentation/compact_mode_event.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:equatable/equatable.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'database_controller.dart';\n\npart 'tab_bar_bloc.freezed.dart';\n\nclass DatabaseTabBarBloc\n    extends Bloc<DatabaseTabBarEvent, DatabaseTabBarState> {\n  DatabaseTabBarBloc({\n    required ViewPB view,\n    required String compactModeId,\n    required bool enableCompactMode,\n  }) : super(\n          DatabaseTabBarState.initial(\n            view,\n            compactModeId,\n            enableCompactMode,\n          ),\n        ) {\n    on<DatabaseTabBarEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () {\n            _listenInlineViewChanged();\n            _loadChildView();\n          },\n          didLoadChildViews: (List<ViewPB> childViews) {\n            emit(\n              state.copyWith(\n                tabBars: [\n                  ...state.tabBars,\n                  ...childViews.map(\n                    (newChildView) => DatabaseTabBar(view: newChildView),\n                  ),\n                ],\n                tabBarControllerByViewId: _extendsTabBarController(childViews),\n              ),\n            );\n          },\n          selectView: (String viewId) {\n            final index =\n                state.tabBars.indexWhere((element) => element.viewId == viewId);\n            if (index != -1) {\n              emit(\n                state.copyWith(selectedIndex: index),\n              );\n            }\n          },\n          createView: (layout, name) {\n            _createLinkedView(layout.layoutType, name ?? layout.layoutName);\n          },\n          deleteView: (String viewId) async {\n            final result = await ViewBackendService.deleteView(viewId: viewId);\n            result.fold(\n              (l) {},\n              (r) => Log.error(r),\n            );\n          },\n          renameView: (String viewId, String newName) {\n            ViewBackendService.updateView(viewId: viewId, name: newName);\n          },\n          didUpdateChildViews: (updatePB) async {\n            if (updatePB.createChildViews.isNotEmpty) {\n              final allTabBars = [\n                ...state.tabBars,\n                ...updatePB.createChildViews\n                    .map((e) => DatabaseTabBar(view: e)),\n              ];\n              emit(\n                state.copyWith(\n                  tabBars: allTabBars,\n                  selectedIndex: state.tabBars.length,\n                  tabBarControllerByViewId:\n                      _extendsTabBarController(updatePB.createChildViews),\n                ),\n              );\n            }\n\n            if (updatePB.deleteChildViews.isNotEmpty) {\n              final allTabBars = [...state.tabBars];\n              final tabBarControllerByViewId = {\n                ...state.tabBarControllerByViewId,\n              };\n              var newSelectedIndex = state.selectedIndex;\n              for (final viewId in updatePB.deleteChildViews) {\n                final index = allTabBars.indexWhere(\n                  (element) => element.viewId == viewId,\n                );\n                if (index != -1) {\n                  final tabBar = allTabBars.removeAt(index);\n                  // Dispose the controller when the tab is removed.\n                  final controller =\n                      tabBarControllerByViewId.remove(tabBar.viewId);\n                  await controller?.dispose();\n                }\n\n                if (index == state.selectedIndex) {\n                  if (index > 0 && allTabBars.isNotEmpty) {\n                    newSelectedIndex = index - 1;\n                  }\n                }\n              }\n              emit(\n                state.copyWith(\n                  tabBars: allTabBars,\n                  selectedIndex: newSelectedIndex,\n                  tabBarControllerByViewId: tabBarControllerByViewId,\n                ),\n              );\n            }\n          },\n          viewDidUpdate: (ViewPB updatedView) {\n            final index = state.tabBars.indexWhere(\n              (element) => element.viewId == updatedView.id,\n            );\n            if (index != -1) {\n              final allTabBars = [...state.tabBars];\n              final updatedTabBar = DatabaseTabBar(view: updatedView);\n              allTabBars[index] = updatedTabBar;\n              emit(state.copyWith(tabBars: allTabBars));\n            }\n          },\n        );\n      },\n    );\n  }\n\n  @override\n  Future<void> close() async {\n    for (final tabBar in state.tabBars) {\n      await state.tabBarControllerByViewId[tabBar.viewId]?.dispose();\n      tabBar.dispose();\n    }\n    return super.close();\n  }\n\n  void _listenInlineViewChanged() {\n    final controller = state.tabBarControllerByViewId[state.parentView.id];\n    controller?.onViewUpdated = (newView) {\n      add(DatabaseTabBarEvent.viewDidUpdate(newView));\n    };\n\n    // Only listen the child view changes when the parent view is inline.\n    controller?.onViewChildViewChanged = (update) {\n      add(DatabaseTabBarEvent.didUpdateChildViews(update));\n    };\n  }\n\n  /// Create tab bar controllers for the new views and return the updated map.\n  Map<String, DatabaseTabBarController> _extendsTabBarController(\n    List<ViewPB> newViews,\n  ) {\n    final tabBarControllerByViewId = {...state.tabBarControllerByViewId};\n    for (final view in newViews) {\n      final controller = DatabaseTabBarController(\n        view: view,\n        compactModeId: state.compactModeId,\n        enableCompactMode: state.enableCompactMode,\n      )..onViewUpdated = (newView) {\n          add(DatabaseTabBarEvent.viewDidUpdate(newView));\n        };\n\n      tabBarControllerByViewId[view.id] = controller;\n    }\n    return tabBarControllerByViewId;\n  }\n\n  Future<void> _createLinkedView(ViewLayoutPB layoutType, String name) async {\n    final viewId = state.parentView.id;\n    final databaseIdOrError =\n        await DatabaseViewBackendService(viewId: viewId).getDatabaseId();\n    databaseIdOrError.fold(\n      (databaseId) async {\n        final linkedViewOrError =\n            await ViewBackendService.createDatabaseLinkedView(\n          parentViewId: viewId,\n          databaseId: databaseId,\n          layoutType: layoutType,\n          name: name,\n        );\n\n        linkedViewOrError.fold(\n          (linkedView) {},\n          (err) => Log.error(err),\n        );\n      },\n      (r) => Log.error(r),\n    );\n  }\n\n  void _loadChildView() async {\n    final viewsOrFail =\n        await ViewBackendService.getChildViews(viewId: state.parentView.id);\n\n    viewsOrFail.fold(\n      (views) {\n        if (!isClosed) {\n          add(DatabaseTabBarEvent.didLoadChildViews(views));\n        }\n      },\n      (err) => Log.error(err),\n    );\n  }\n}\n\n@freezed\nclass DatabaseTabBarEvent with _$DatabaseTabBarEvent {\n  const factory DatabaseTabBarEvent.initial() = _Initial;\n\n  const factory DatabaseTabBarEvent.didLoadChildViews(\n    List<ViewPB> childViews,\n  ) = _DidLoadChildViews;\n\n  const factory DatabaseTabBarEvent.selectView(String viewId) = _DidSelectView;\n\n  const factory DatabaseTabBarEvent.createView(\n    DatabaseLayoutPB layout,\n    String? name,\n  ) = _CreateView;\n\n  const factory DatabaseTabBarEvent.renameView(String viewId, String newName) =\n      _RenameView;\n\n  const factory DatabaseTabBarEvent.deleteView(String viewId) = _DeleteView;\n\n  const factory DatabaseTabBarEvent.didUpdateChildViews(\n    ChildViewUpdatePB updatePB,\n  ) = _DidUpdateChildViews;\n\n  const factory DatabaseTabBarEvent.viewDidUpdate(ViewPB view) = _ViewDidUpdate;\n}\n\n@freezed\nclass DatabaseTabBarState with _$DatabaseTabBarState {\n  const factory DatabaseTabBarState({\n    required ViewPB parentView,\n    required int selectedIndex,\n    required String compactModeId,\n    required bool enableCompactMode,\n    required List<DatabaseTabBar> tabBars,\n    required Map<String, DatabaseTabBarController> tabBarControllerByViewId,\n  }) = _DatabaseTabBarState;\n\n  factory DatabaseTabBarState.initial(\n    ViewPB view,\n    String compactModeId,\n    bool enableCompactMode,\n  ) {\n    final tabBar = DatabaseTabBar(view: view);\n    return DatabaseTabBarState(\n      parentView: view,\n      selectedIndex: 0,\n      compactModeId: compactModeId,\n      enableCompactMode: enableCompactMode,\n      tabBars: [tabBar],\n      tabBarControllerByViewId: {\n        view.id: DatabaseTabBarController(\n          view: view,\n          compactModeId: compactModeId,\n          enableCompactMode: enableCompactMode,\n        ),\n      },\n    );\n  }\n}\n\nclass DatabaseTabBar extends Equatable {\n  DatabaseTabBar({\n    required this.view,\n  }) : _builder = UniversalPlatform.isMobile\n            ? view.mobileTabBarItem()\n            : view.tabBarItem();\n\n  final ViewPB view;\n  final DatabaseTabBarItemBuilder _builder;\n\n  String get viewId => view.id;\n\n  DatabaseTabBarItemBuilder get builder => _builder;\n\n  ViewLayoutPB get layout => view.layout;\n\n  @override\n  List<Object?> get props => [view.hashCode];\n\n  void dispose() {\n    _builder.dispose();\n  }\n}\n\ntypedef OnViewUpdated = void Function(ViewPB newView);\ntypedef OnViewChildViewChanged = void Function(\n  ChildViewUpdatePB childViewUpdate,\n);\n\nclass DatabaseTabBarController {\n  DatabaseTabBarController({\n    required this.view,\n    required String compactModeId,\n    required bool enableCompactMode,\n  })  : controller = DatabaseController(view: view)\n          ..initCompactMode(enableCompactMode)\n          ..addListener(\n            onCompactModeChanged: (v) async {\n              compactModeEventBus\n                  .fire(CompactModeEvent(id: compactModeId, enable: v));\n            },\n          ),\n        viewListener = ViewListener(viewId: view.id) {\n    viewListener.start(\n      onViewChildViewsUpdated: (update) => onViewChildViewChanged?.call(update),\n      onViewUpdated: (newView) {\n        view = newView;\n        onViewUpdated?.call(newView);\n      },\n    );\n  }\n\n  ViewPB view;\n  final DatabaseController controller;\n  final ViewListener viewListener;\n  OnViewUpdated? onViewUpdated;\n  OnViewChildViewChanged? onViewChildViewChanged;\n\n  Future<void> dispose() async {\n    await Future.wait([viewListener.stop(), controller.dispose()]);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/view/view_cache.dart",
    "content": "import 'dart:async';\nimport 'dart:collection';\n\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy_backend/log.dart';\n\nimport '../defines.dart';\nimport '../field/field_controller.dart';\nimport '../row/row_cache.dart';\n\nimport 'view_listener.dart';\n\nclass DatabaseViewCallbacks {\n  const DatabaseViewCallbacks({\n    this.onNumOfRowsChanged,\n    this.onRowsCreated,\n    this.onRowsUpdated,\n    this.onRowsDeleted,\n  });\n\n  /// Will get called when number of rows were changed that includes\n  /// update/delete/insert rows. The [onNumOfRowsChanged] will return all\n  /// the rows of the current database\n  final OnNumOfRowsChanged? onNumOfRowsChanged;\n\n  // Will get called when creating new rows\n  final OnRowsCreated? onRowsCreated;\n\n  /// Will get called when rows were updated\n  final OnRowsUpdated? onRowsUpdated;\n\n  /// Will get called when number of rows were deleted\n  final OnRowsDeleted? onRowsDeleted;\n}\n\n/// Read https://docs.appflowy.io/docs/documentation/software-contributions/architecture/frontend/frontend/grid for more information\nclass DatabaseViewCache {\n  DatabaseViewCache({\n    required this.viewId,\n    required FieldController fieldController,\n  }) : _databaseViewListener = DatabaseViewListener(viewId: viewId) {\n    final depsImpl = RowCacheDependenciesImpl(fieldController);\n    _rowCache = RowCache(\n      viewId: viewId,\n      fieldsDelegate: depsImpl,\n      rowLifeCycle: depsImpl,\n    );\n\n    _databaseViewListener.start(\n      onRowsChanged: (result) => result.fold(\n        (changeset) {\n          // Update the cache\n          _rowCache.applyRowsChanged(changeset);\n\n          if (changeset.deletedRows.isNotEmpty) {\n            for (final callback in _callbacks) {\n              callback.onRowsDeleted?.call(changeset.deletedRows);\n            }\n          }\n\n          if (changeset.updatedRows.isNotEmpty) {\n            for (final callback in _callbacks) {\n              callback.onRowsUpdated?.call(\n                changeset.updatedRows.map((e) => e.rowId).toList(),\n                _rowCache.changeReason,\n              );\n            }\n          }\n\n          if (changeset.insertedRows.isNotEmpty) {\n            for (final callback in _callbacks) {\n              callback.onRowsCreated?.call(changeset.insertedRows);\n            }\n          }\n        },\n        (err) => Log.error(err),\n      ),\n      onRowsVisibilityChanged: (result) => result.fold(\n        (changeset) => _rowCache.applyRowsVisibility(changeset),\n        (err) => Log.error(err),\n      ),\n      onReorderAllRows: (result) => result.fold(\n        (rowIds) => _rowCache.reorderAllRows(rowIds),\n        (err) => Log.error(err),\n      ),\n      onReorderSingleRow: (result) => result.fold(\n        (reorderRow) => _rowCache.reorderSingleRow(reorderRow),\n        (err) => Log.error(err),\n      ),\n    );\n\n    _rowCache.onRowsChanged(\n      (reason) {\n        for (final callback in _callbacks) {\n          callback.onNumOfRowsChanged\n              ?.call(rowInfos, _rowCache.rowByRowId, reason);\n        }\n      },\n    );\n  }\n\n  final String viewId;\n  late RowCache _rowCache;\n  final DatabaseViewListener _databaseViewListener;\n  final List<DatabaseViewCallbacks> _callbacks = [];\n\n  UnmodifiableListView<RowInfo> get rowInfos => _rowCache.rowInfos;\n  RowCache get rowCache => _rowCache;\n\n  RowInfo? getRow(RowId rowId) => _rowCache.getRow(rowId);\n\n  Future<void> dispose() async {\n    await _databaseViewListener.stop();\n    _rowCache.dispose();\n    _callbacks.clear();\n  }\n\n  void addListener(DatabaseViewCallbacks callbacks) {\n    _callbacks.add(callbacks);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/application/view/view_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/grid_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/view_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\ntypedef RowsVisibilityCallback = void Function(\n  FlowyResult<RowsVisibilityChangePB, FlowyError>,\n);\ntypedef NumberOfRowsCallback = void Function(\n  FlowyResult<RowsChangePB, FlowyError>,\n);\ntypedef ReorderAllRowsCallback = void Function(\n  FlowyResult<List<String>, FlowyError>,\n);\ntypedef SingleRowCallback = void Function(\n  FlowyResult<ReorderSingleRowPB, FlowyError>,\n);\n\nclass DatabaseViewListener {\n  DatabaseViewListener({required this.viewId});\n\n  final String viewId;\n  DatabaseNotificationListener? _listener;\n\n  void start({\n    required NumberOfRowsCallback onRowsChanged,\n    required ReorderAllRowsCallback onReorderAllRows,\n    required SingleRowCallback onReorderSingleRow,\n    required RowsVisibilityCallback onRowsVisibilityChanged,\n  }) {\n    // Stop any existing listener\n    _listener?.stop();\n\n    // Initialize the notification listener\n    _listener = DatabaseNotificationListener(\n      objectId: viewId,\n      handler: (ty, result) => _handler(\n        ty,\n        result,\n        onRowsChanged,\n        onReorderAllRows,\n        onReorderSingleRow,\n        onRowsVisibilityChanged,\n      ),\n    );\n  }\n\n  void _handler(\n    DatabaseNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n    NumberOfRowsCallback onRowsChanged,\n    ReorderAllRowsCallback onReorderAllRows,\n    SingleRowCallback onReorderSingleRow,\n    RowsVisibilityCallback onRowsVisibilityChanged,\n  ) {\n    switch (ty) {\n      case DatabaseNotification.DidUpdateViewRowsVisibility:\n        result.fold(\n          (payload) => onRowsVisibilityChanged(\n            FlowyResult.success(RowsVisibilityChangePB.fromBuffer(payload)),\n          ),\n          (error) => onRowsVisibilityChanged(FlowyResult.failure(error)),\n        );\n        break;\n      case DatabaseNotification.DidUpdateRow:\n        result.fold(\n          (payload) => onRowsChanged(\n            FlowyResult.success(RowsChangePB.fromBuffer(payload)),\n          ),\n          (error) => onRowsChanged(FlowyResult.failure(error)),\n        );\n        break;\n      case DatabaseNotification.DidReorderRows:\n        result.fold(\n          (payload) => onReorderAllRows(\n            FlowyResult.success(ReorderAllRowsPB.fromBuffer(payload).rowOrders),\n          ),\n          (error) => onReorderAllRows(FlowyResult.failure(error)),\n        );\n        break;\n      case DatabaseNotification.DidReorderSingleRow:\n        result.fold(\n          (payload) => onReorderSingleRow(\n            FlowyResult.success(ReorderSingleRowPB.fromBuffer(payload)),\n          ),\n          (error) => onReorderSingleRow(FlowyResult.failure(error)),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _listener?.stop();\n    _listener = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/board/application/board_actions_bloc.dart",
    "content": "import 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/board/application/board_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'board_actions_bloc.freezed.dart';\n\nclass BoardActionsCubit extends Cubit<BoardActionsState> {\n  BoardActionsCubit({\n    required this.databaseController,\n  }) : super(const BoardActionsState.initial());\n\n  final DatabaseController databaseController;\n\n  void startEditingRow(GroupedRowId groupedRowId) {\n    emit(BoardActionsState.startEditingRow(groupedRowId: groupedRowId));\n    emit(const BoardActionsState.initial());\n  }\n\n  void endEditing(GroupedRowId groupedRowId) {\n    emit(const BoardActionsState.endEditingRow());\n    emit(BoardActionsState.setFocus(groupedRowIds: [groupedRowId]));\n    emit(const BoardActionsState.initial());\n  }\n\n  void openCard(RowMetaPB rowMeta) {\n    emit(BoardActionsState.openCard(rowMeta: rowMeta));\n    emit(const BoardActionsState.initial());\n  }\n\n  void openCardWithRowId(rowId) {\n    final rowMeta = databaseController.rowCache.getRow(rowId)!.rowMeta;\n    openCard(rowMeta);\n  }\n\n  void setFocus(List<GroupedRowId> groupedRowIds) {\n    emit(BoardActionsState.setFocus(groupedRowIds: groupedRowIds));\n    emit(const BoardActionsState.initial());\n  }\n\n  void startCreateBottomRow(String groupId) {\n    emit(const BoardActionsState.setFocus(groupedRowIds: []));\n    emit(BoardActionsState.startCreateBottomRow(groupId: groupId));\n    emit(const BoardActionsState.initial());\n  }\n\n  void createRow(\n    GroupedRowId? groupedRowId,\n    CreateBoardCardRelativePosition relativePosition,\n  ) {\n    emit(\n      BoardActionsState.createRow(\n        groupedRowId: groupedRowId,\n        position: relativePosition,\n      ),\n    );\n    emit(const BoardActionsState.initial());\n  }\n}\n\n@freezed\nclass BoardActionsState with _$BoardActionsState {\n  const factory BoardActionsState.initial() = _BoardActionsInitialState;\n\n  const factory BoardActionsState.openCard({\n    required RowMetaPB rowMeta,\n  }) = _BoardActionsOpenCardState;\n\n  const factory BoardActionsState.startEditingRow({\n    required GroupedRowId groupedRowId,\n  }) = _BoardActionsStartEditingRowState;\n\n  const factory BoardActionsState.endEditingRow() =\n      _BoardActionsEndEditingRowState;\n\n  const factory BoardActionsState.setFocus({\n    required List<GroupedRowId> groupedRowIds,\n  }) = _BoardActionsSetFocusState;\n\n  const factory BoardActionsState.startCreateBottomRow({\n    required String groupId,\n  }) = _BoardActionsStartCreateBottomRowState;\n\n  const factory BoardActionsState.createRow({\n    required GroupedRowId? groupedRowId,\n    required CreateBoardCardRelativePosition position,\n  }) = _BoardActionCreateRowState;\n}\n\nenum CreateBoardCardRelativePosition {\n  before,\n  after,\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart",
    "content": "import 'dart:async';\nimport 'dart:collection';\n\nimport 'package:appflowy/plugins/database/application/defines.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy/plugins/database/board/group_ext.dart';\nimport 'package:appflowy/plugins/database/domain/group_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_board/appflowy_board.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:collection/collection.dart';\nimport 'package:equatable/equatable.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:protobuf/protobuf.dart' hide FieldInfo;\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../../application/database_controller.dart';\nimport '../../application/field/field_controller.dart';\nimport '../../application/row/row_cache.dart';\nimport 'group_controller.dart';\n\npart 'board_bloc.freezed.dart';\n\nclass BoardBloc extends Bloc<BoardEvent, BoardState> {\n  BoardBloc({\n    required this.databaseController,\n    this.didCreateRow,\n    AppFlowyBoardController? boardController,\n  }) : super(const BoardState.loading()) {\n    groupBackendSvc = GroupBackendService(viewId);\n    _initBoardController(boardController);\n    _dispatch();\n  }\n\n  final DatabaseController databaseController;\n  late final AppFlowyBoardController boardController;\n  final LinkedHashMap<String, GroupController> groupControllers =\n      LinkedHashMap();\n  final List<GroupPB> groupList = [];\n\n  final ValueNotifier<DidCreateRowResult?>? didCreateRow;\n\n  late final GroupBackendService groupBackendSvc;\n\n  UserProfilePB? _userProfile;\n  UserProfilePB? get userProfile => _userProfile;\n\n  FieldController get fieldController => databaseController.fieldController;\n  String get viewId => databaseController.viewId;\n\n  DatabaseCallbacks? _databaseCallbacks;\n  DatabaseLayoutSettingCallbacks? _layoutSettingsCallback;\n  GroupCallbacks? _groupCallbacks;\n\n  void _initBoardController(AppFlowyBoardController? controller) {\n    boardController = controller ??\n        AppFlowyBoardController(\n          onMoveGroup: (fromGroupId, fromIndex, toGroupId, toIndex) =>\n              databaseController.moveGroup(\n            fromGroupId: fromGroupId,\n            toGroupId: toGroupId,\n          ),\n          onMoveGroupItem: (groupId, fromIndex, toIndex) {\n            final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex);\n            final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);\n            if (fromRow != null) {\n              databaseController.moveGroupRow(\n                fromRow: fromRow,\n                toRow: toRow,\n                fromGroupId: groupId,\n                toGroupId: groupId,\n              );\n            }\n          },\n          onMoveGroupItemToGroup: (fromGroupId, fromIndex, toGroupId, toIndex) {\n            final fromRow =\n                groupControllers[fromGroupId]?.rowAtIndex(fromIndex);\n            final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);\n            if (fromRow != null) {\n              databaseController.moveGroupRow(\n                fromRow: fromRow,\n                toRow: toRow,\n                fromGroupId: fromGroupId,\n                toGroupId: toGroupId,\n              );\n            }\n          },\n        );\n  }\n\n  void _dispatch() {\n    on<BoardEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            emit(BoardState.initial(viewId));\n            _startListening();\n            await _openDatabase(emit);\n\n            final result = await UserEventGetUserProfile().send();\n            result.fold(\n              (profile) => _userProfile = profile,\n              (err) => Log.error('Failed to fetch user profile: ${err.msg}'),\n            );\n          },\n          createRow: (groupId, position, title, targetRowId) async {\n            final primaryField = databaseController.fieldController.fieldInfos\n                .firstWhereOrNull((element) => element.isPrimary)!;\n            final void Function(RowDataBuilder)? cellBuilder = title == null\n                ? null\n                : (builder) => builder.insertText(primaryField, title);\n\n            final result = await RowBackendService.createRow(\n              viewId: databaseController.viewId,\n              groupId: groupId,\n              position: position,\n              targetRowId: targetRowId,\n              withCells: cellBuilder,\n            );\n\n            final startEditing = position != OrderObjectPositionTypePB.End;\n            final action = UniversalPlatform.isMobile\n                ? DidCreateRowAction.openAsPage\n                : startEditing\n                    ? DidCreateRowAction.startEditing\n                    : DidCreateRowAction.none;\n\n            result.fold(\n              (rowMeta) {\n                state.maybeMap(\n                  ready: (value) {\n                    didCreateRow?.value = DidCreateRowResult(\n                      action: action,\n                      rowMeta: rowMeta,\n                      groupId: groupId,\n                    );\n                  },\n                  orElse: () {},\n                );\n              },\n              (err) => Log.error(err),\n            );\n          },\n          createGroup: (name) async {\n            final result = await groupBackendSvc.createGroup(name: name);\n            result.onFailure(Log.error);\n          },\n          deleteGroup: (groupId) async {\n            final result = await groupBackendSvc.deleteGroup(groupId: groupId);\n            result.onFailure(Log.error);\n          },\n          renameGroup: (groupId, name) async {\n            final result = await groupBackendSvc.updateGroup(\n              groupId: groupId,\n              name: name,\n            );\n            result.onFailure(Log.error);\n          },\n          didReceiveError: (error) {\n            emit(BoardState.error(error: error));\n          },\n          didReceiveGroups: (List<GroupPB> groups) {\n            state.maybeMap(\n              ready: (state) {\n                emit(\n                  state.copyWith(\n                    hiddenGroups: _filterHiddenGroups(hideUngrouped, groups),\n                    groupIds: groups.map((group) => group.groupId).toList(),\n                  ),\n                );\n              },\n              orElse: () {},\n            );\n          },\n          didUpdateLayoutSettings: (layoutSettings) {\n            state.maybeMap(\n              ready: (state) {\n                emit(\n                  state.copyWith(\n                    layoutSettings: layoutSettings,\n                    hiddenGroups: _filterHiddenGroups(hideUngrouped, groupList),\n                  ),\n                );\n              },\n              orElse: () {},\n            );\n          },\n          setGroupVisibility: (GroupPB group, bool isVisible) async {\n            await _setGroupVisibility(group, isVisible);\n          },\n          toggleHiddenSectionVisibility: (isVisible) async {\n            await state.maybeMap(\n              ready: (state) async {\n                final newLayoutSettings = state.layoutSettings!;\n                newLayoutSettings.freeze();\n\n                final newLayoutSetting = newLayoutSettings.rebuild(\n                  (message) => message.collapseHiddenGroups = isVisible,\n                );\n\n                await databaseController.updateLayoutSetting(\n                  boardLayoutSetting: newLayoutSetting,\n                );\n              },\n              orElse: () {},\n            );\n          },\n          reorderGroup: (fromGroupId, toGroupId) async {\n            _reorderGroup(fromGroupId, toGroupId, emit);\n          },\n          startEditingHeader: (String groupId) {\n            state.maybeMap(\n              ready: (state) => emit(state.copyWith(editingHeaderId: groupId)),\n              orElse: () {},\n            );\n          },\n          endEditingHeader: (String groupId, String? groupName) async {\n            final group = groupControllers[groupId]?.group;\n            if (group != null) {\n              final currentName = group.generateGroupName(databaseController);\n              if (currentName != groupName) {\n                await groupBackendSvc.updateGroup(\n                  groupId: groupId,\n                  name: groupName,\n                );\n              }\n            }\n\n            state.maybeMap(\n              ready: (state) => emit(state.copyWith(editingHeaderId: null)),\n              orElse: () {},\n            );\n          },\n          deleteCards: (groupedRowIds) async {\n            final rowIds = groupedRowIds.map((e) => e.rowId).toList();\n            await RowBackendService.deleteRows(viewId, rowIds);\n          },\n          moveGroupToAdjacentGroup: (groupedRowId, toPrevious) async {\n            final fromRow =\n                databaseController.rowCache.getRow(groupedRowId.rowId)?.rowMeta;\n            final currentGroupIndex =\n                boardController.groupIds.indexOf(groupedRowId.groupId);\n            final toGroupIndex =\n                toPrevious ? currentGroupIndex - 1 : currentGroupIndex + 1;\n            if (fromRow != null &&\n                toGroupIndex > -1 &&\n                toGroupIndex < boardController.groupIds.length) {\n              final toGroupId = boardController.groupDatas[toGroupIndex].id;\n              final result = await databaseController.moveGroupRow(\n                fromRow: fromRow,\n                fromGroupId: groupedRowId.groupId,\n                toGroupId: toGroupId,\n              );\n              result.fold(\n                (s) {\n                  final previousState = state;\n                  emit(\n                    BoardState.setFocus(\n                      groupedRowIds: [\n                        GroupedRowId(\n                          groupId: toGroupId,\n                          rowId: groupedRowId.rowId,\n                        ),\n                      ],\n                    ),\n                  );\n                  emit(previousState);\n                },\n                (f) {},\n              );\n            }\n          },\n          openRowDetail: (rowMeta) {\n            final copyState = state;\n            emit(BoardState.openRowDetail(rowMeta: rowMeta));\n            emit(copyState);\n          },\n        );\n      },\n    );\n  }\n\n  Future<void> _setGroupVisibility(GroupPB group, bool isVisible) async {\n    if (group.isDefault) {\n      await state.maybeMap(\n        ready: (state) async {\n          final newLayoutSettings = state.layoutSettings!;\n          newLayoutSettings.freeze();\n\n          final newLayoutSetting = newLayoutSettings.rebuild(\n            (message) => message.hideUngroupedColumn = !isVisible,\n          );\n\n          await databaseController.updateLayoutSetting(\n            boardLayoutSetting: newLayoutSetting,\n          );\n        },\n        orElse: () {},\n      );\n    } else {\n      await groupBackendSvc.updateGroup(\n        groupId: group.groupId,\n        visible: isVisible,\n      );\n    }\n  }\n\n  void _reorderGroup(\n    String fromGroupId,\n    String toGroupId,\n    Emitter<BoardState> emit,\n  ) async {\n    final fromIndex = groupList.indexWhere((g) => g.groupId == fromGroupId);\n    final toIndex = groupList.indexWhere((g) => g.groupId == toGroupId);\n    final group = groupList.removeAt(fromIndex);\n    groupList.insert(toIndex, group);\n    add(BoardEvent.didReceiveGroups(groupList));\n    final result = await databaseController.moveGroup(\n      fromGroupId: fromGroupId,\n      toGroupId: toGroupId,\n    );\n    result.fold((l) => {}, (err) => Log.error(err));\n  }\n\n  @override\n  Future<void> close() async {\n    for (final controller in groupControllers.values) {\n      await controller.dispose();\n    }\n\n    databaseController.removeListener(\n      onDatabaseChanged: _databaseCallbacks,\n      onLayoutSettingsChanged: _layoutSettingsCallback,\n      onGroupChanged: _groupCallbacks,\n    );\n\n    _databaseCallbacks = null;\n    _layoutSettingsCallback = null;\n    _groupCallbacks = null;\n\n    boardController.dispose();\n    return super.close();\n  }\n\n  bool get hideUngrouped =>\n      databaseController.databaseLayoutSetting?.board.hideUngroupedColumn ??\n      false;\n\n  FieldType? get groupingFieldType {\n    if (groupList.isEmpty) {\n      return null;\n    }\n    return databaseController.fieldController\n        .getField(groupList.first.fieldId)\n        ?.fieldType;\n  }\n\n  void initializeGroups(List<GroupPB> groups) {\n    for (final controller in groupControllers.values) {\n      controller.dispose();\n    }\n\n    groupControllers.clear();\n    boardController.clear();\n    groupList.clear();\n    groupList.addAll(groups);\n\n    boardController.addGroups(\n      groups\n          .where((group) {\n            final field = fieldController.getField(group.fieldId);\n            return field != null &&\n                (!group.isDefault && group.isVisible ||\n                    group.isDefault &&\n                        !hideUngrouped &&\n                        field.fieldType != FieldType.Checkbox);\n          })\n          .map((group) => _initializeGroupData(group))\n          .toList(),\n    );\n\n    for (final group in groups) {\n      final controller = _initializeGroupController(group);\n      groupControllers[controller.group.groupId] = controller;\n    }\n  }\n\n  RowCache get rowCache => databaseController.rowCache;\n\n  void _startListening() {\n    _layoutSettingsCallback = DatabaseLayoutSettingCallbacks(\n      onLayoutSettingsChanged: (layoutSettings) {\n        if (isClosed) {\n          return;\n        }\n        final index = groupList.indexWhere((element) => element.isDefault);\n        if (index != -1) {\n          if (layoutSettings.board.hideUngroupedColumn) {\n            boardController.removeGroup(groupList[index].fieldId);\n          } else {\n            final newGroup = _initializeGroupData(groupList[index]);\n            final visibleGroups = [...groupList]\n              ..retainWhere((g) => g.isVisible || g.isDefault);\n            final indexInVisibleGroups =\n                visibleGroups.indexWhere((g) => g.isDefault);\n            if (indexInVisibleGroups != -1) {\n              boardController.insertGroup(indexInVisibleGroups, newGroup);\n            }\n          }\n        }\n        add(BoardEvent.didUpdateLayoutSettings(layoutSettings.board));\n      },\n    );\n    _groupCallbacks = GroupCallbacks(\n      onGroupByField: (groups) {\n        if (isClosed) {\n          return;\n        }\n\n        initializeGroups(groups);\n        add(BoardEvent.didReceiveGroups(groups));\n      },\n      onDeleteGroup: (groupIds) {\n        if (isClosed) {\n          return;\n        }\n\n        boardController.removeGroups(groupIds);\n        groupList.removeWhere((group) => groupIds.contains(group.groupId));\n        add(BoardEvent.didReceiveGroups(groupList));\n      },\n      onInsertGroup: (insertGroups) {\n        if (isClosed) {\n          return;\n        }\n\n        final group = insertGroups.group;\n        final newGroup = _initializeGroupData(group);\n        final controller = _initializeGroupController(group);\n        groupControllers[controller.group.groupId] = controller;\n        boardController.addGroup(newGroup);\n        groupList.insert(insertGroups.index, group);\n        add(BoardEvent.didReceiveGroups(groupList));\n      },\n      onUpdateGroup: (updatedGroups) async {\n        if (isClosed) {\n          return;\n        }\n\n        // workaround: update group most of the time gets called before fields in\n        // field controller are updated. For single and multi-select group\n        // renames, this is required before generating the new group name.\n        await Future.delayed(const Duration(milliseconds: 50));\n\n        for (final group in updatedGroups) {\n          // see if the column is already in the board\n          final index = groupList.indexWhere((g) => g.groupId == group.groupId);\n          if (index == -1) {\n            continue;\n          }\n\n          final columnController =\n              boardController.getGroupController(group.groupId);\n          if (columnController != null) {\n            // remove the group or update its name\n            columnController.updateGroupName(\n              group.generateGroupName(databaseController),\n            );\n            if (!group.isVisible) {\n              boardController.removeGroup(group.groupId);\n            }\n          } else {\n            final newGroup = _initializeGroupData(group);\n            final visibleGroups = [...groupList]..retainWhere(\n                (g) =>\n                    (g.isVisible && !g.isDefault) ||\n                    g.isDefault && !hideUngrouped ||\n                    g.groupId == group.groupId,\n              );\n            final indexInVisibleGroups =\n                visibleGroups.indexWhere((g) => g.groupId == group.groupId);\n            if (indexInVisibleGroups != -1) {\n              boardController.insertGroup(indexInVisibleGroups, newGroup);\n            }\n          }\n\n          groupList.removeAt(index);\n          groupList.insert(index, group);\n        }\n        add(BoardEvent.didReceiveGroups(groupList));\n      },\n    );\n    _databaseCallbacks = DatabaseCallbacks(\n      onRowsCreated: (rows) {\n        for (final row in rows) {\n          if (!isClosed && row.isHiddenInView) {\n            add(BoardEvent.openRowDetail(row.rowMeta));\n          }\n        }\n      },\n    );\n\n    databaseController.addListener(\n      onDatabaseChanged: _databaseCallbacks,\n      onLayoutSettingsChanged: _layoutSettingsCallback,\n      onGroupChanged: _groupCallbacks,\n    );\n  }\n\n  List<AppFlowyGroupItem> _buildGroupItems(GroupPB group) {\n    final items = group.rows.map((row) {\n      final fieldInfo = fieldController.getField(group.fieldId);\n      return GroupItem(\n        row: row,\n        fieldInfo: fieldInfo!,\n      );\n    }).toList();\n\n    return <AppFlowyGroupItem>[...items];\n  }\n\n  Future<void> _openDatabase(Emitter<BoardState> emit) {\n    return databaseController.open().fold(\n          (datbasePB) => databaseController.setIsLoading(false),\n          (err) => emit(BoardState.error(error: err)),\n        );\n  }\n\n  GroupController _initializeGroupController(GroupPB group) {\n    group.freeze();\n\n    final delegate = GroupControllerDelegateImpl(\n      controller: boardController,\n      fieldController: fieldController,\n      onNewColumnItem: (groupId, row, index) {},\n    );\n\n    final controller = GroupController(\n      group: group,\n      delegate: delegate,\n      onGroupChanged: (newGroup) {\n        if (isClosed) return;\n\n        final index =\n            groupList.indexWhere((g) => g.groupId == newGroup.groupId);\n        if (index != -1) {\n          groupList.removeAt(index);\n          groupList.insert(index, newGroup);\n          add(BoardEvent.didReceiveGroups(groupList));\n        }\n      },\n    );\n\n    return controller..startListening();\n  }\n\n  AppFlowyGroupData _initializeGroupData(GroupPB group) {\n    return AppFlowyGroupData(\n      id: group.groupId,\n      name: group.generateGroupName(databaseController),\n      items: _buildGroupItems(group),\n      customData: GroupData(\n        group: group,\n        fieldInfo: fieldController.getField(group.fieldId)!,\n      ),\n    );\n  }\n}\n\n@freezed\nclass BoardEvent with _$BoardEvent {\n  const factory BoardEvent.initial() = _InitialBoard;\n  const factory BoardEvent.createRow(\n    String groupId,\n    OrderObjectPositionTypePB position,\n    String? title,\n    String? targetRowId,\n  ) = _CreateRow;\n  const factory BoardEvent.createGroup(String name) = _CreateGroup;\n  const factory BoardEvent.startEditingHeader(String groupId) =\n      _StartEditingHeader;\n  const factory BoardEvent.endEditingHeader(String groupId, String? groupName) =\n      _EndEditingHeader;\n  const factory BoardEvent.setGroupVisibility(\n    GroupPB group,\n    bool isVisible,\n  ) = _SetGroupVisibility;\n  const factory BoardEvent.toggleHiddenSectionVisibility(bool isVisible) =\n      _ToggleHiddenSectionVisibility;\n  const factory BoardEvent.renameGroup(String groupId, String name) =\n      _RenameGroup;\n  const factory BoardEvent.deleteGroup(String groupId) = _DeleteGroup;\n  const factory BoardEvent.reorderGroup(String fromGroupId, String toGroupId) =\n      _ReorderGroup;\n  const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError;\n  const factory BoardEvent.didReceiveGroups(List<GroupPB> groups) =\n      _DidReceiveGroups;\n  const factory BoardEvent.didUpdateLayoutSettings(\n    BoardLayoutSettingPB layoutSettings,\n  ) = _DidUpdateLayoutSettings;\n  const factory BoardEvent.deleteCards(List<GroupedRowId> groupedRowIds) =\n      _DeleteCards;\n  const factory BoardEvent.moveGroupToAdjacentGroup(\n    GroupedRowId groupedRowId,\n    bool toPrevious,\n  ) = _MoveGroupToAdjacentGroup;\n  const factory BoardEvent.openRowDetail(RowMetaPB rowMeta) = _OpenRowDetail;\n}\n\n@freezed\nclass BoardState with _$BoardState {\n  const BoardState._();\n\n  const factory BoardState.loading() = _BoardLoadingState;\n\n  const factory BoardState.error({\n    required FlowyError error,\n  }) = _BoardErrorState;\n\n  const factory BoardState.ready({\n    required String viewId,\n    required List<String> groupIds,\n    required LoadingState loadingState,\n    required FlowyError? noneOrError,\n    required BoardLayoutSettingPB? layoutSettings,\n    required List<GroupPB> hiddenGroups,\n    String? editingHeaderId,\n  }) = _BoardReadyState;\n\n  const factory BoardState.setFocus({\n    required List<GroupedRowId> groupedRowIds,\n  }) = _BoardSetFocusState;\n\n  const factory BoardState.openRowDetail({\n    required RowMetaPB rowMeta,\n  }) = _BoardOpenRowDetailState;\n\n  factory BoardState.initial(String viewId) => BoardState.ready(\n        viewId: viewId,\n        groupIds: [],\n        noneOrError: null,\n        loadingState: const LoadingState.loading(),\n        layoutSettings: null,\n        hiddenGroups: [],\n      );\n\n  bool get isLoading => maybeMap(loading: (_) => true, orElse: () => false);\n  bool get isError => maybeMap(error: (_) => true, orElse: () => false);\n  bool get isReady => maybeMap(ready: (_) => true, orElse: () => false);\n  bool get isSetFocus => maybeMap(setFocus: (_) => true, orElse: () => false);\n}\n\nList<GroupPB> _filterHiddenGroups(bool hideUngrouped, List<GroupPB> groups) {\n  return [...groups]..retainWhere(\n      (group) => !group.isVisible || group.isDefault && hideUngrouped,\n    );\n}\n\nclass GroupItem extends AppFlowyGroupItem {\n  GroupItem({\n    required this.row,\n    required this.fieldInfo,\n  });\n\n  final RowMetaPB row;\n  final FieldInfo fieldInfo;\n\n  @override\n  String get id => row.id.toString();\n}\n\n/// Identifies a card in a database view that has grouping. To support cases\n/// in which a card can belong to more than one group at the same time (e.g.\n/// FieldType.Multiselect), we include the card's group id as well.\n///\nclass GroupedRowId extends Equatable {\n  const GroupedRowId({\n    required this.rowId,\n    required this.groupId,\n  });\n\n  final String rowId;\n  final String groupId;\n\n  @override\n  List<Object?> get props => [rowId, groupId];\n}\n\nclass GroupControllerDelegateImpl extends GroupControllerDelegate {\n  GroupControllerDelegateImpl({\n    required this.controller,\n    required this.fieldController,\n    required this.onNewColumnItem,\n  });\n\n  final FieldController fieldController;\n  final AppFlowyBoardController controller;\n  final void Function(String, RowMetaPB, int?) onNewColumnItem;\n\n  @override\n  bool hasGroup(String groupId) => controller.groupIds.contains(groupId);\n\n  @override\n  void insertRow(GroupPB group, RowMetaPB row, int? index) {\n    final fieldInfo = fieldController.getField(group.fieldId);\n    if (fieldInfo == null) {\n      return Log.warn(\"fieldInfo should not be null\");\n    }\n\n    if (index != null) {\n      final item = GroupItem(\n        row: row,\n        fieldInfo: fieldInfo,\n      );\n      controller.insertGroupItem(group.groupId, index, item);\n    } else {\n      final item = GroupItem(\n        row: row,\n        fieldInfo: fieldInfo,\n      );\n      controller.addGroupItem(group.groupId, item);\n    }\n  }\n\n  @override\n  void removeRow(GroupPB group, RowId rowId) =>\n      controller.removeGroupItem(group.groupId, rowId.toString());\n\n  @override\n  void updateRow(GroupPB group, RowMetaPB row) {\n    final fieldInfo = fieldController.getField(group.fieldId);\n    if (fieldInfo == null) {\n      return Log.warn(\"fieldInfo should not be null\");\n    }\n\n    controller.updateGroupItem(\n      group.groupId,\n      GroupItem(\n        row: row,\n        fieldInfo: fieldInfo,\n      ),\n    );\n  }\n\n  @override\n  void addNewRow(GroupPB group, RowMetaPB row, int? index) {\n    final fieldInfo = fieldController.getField(group.fieldId);\n    if (fieldInfo == null) {\n      return Log.warn(\"fieldInfo should not be null\");\n    }\n\n    final item = GroupItem(row: row, fieldInfo: fieldInfo);\n\n    if (index != null) {\n      controller.insertGroupItem(group.groupId, index, item);\n    } else {\n      controller.addGroupItem(group.groupId, item);\n    }\n\n    onNewColumnItem(group.groupId, row, index);\n  }\n}\n\nclass GroupData {\n  const GroupData({\n    required this.group,\n    required this.fieldInfo,\n  });\n\n  final GroupPB group;\n  final FieldInfo fieldInfo;\n\n  CheckboxGroup? asCheckboxGroup() =>\n      fieldType == FieldType.Checkbox ? CheckboxGroup(group) : null;\n\n  FieldType get fieldType => fieldInfo.fieldType;\n}\n\nclass CheckboxGroup {\n  const CheckboxGroup(this.group);\n\n  final GroupPB group;\n\n  // Hardcode value: \"Yes\" that equal to the value defined in Rust\n  // pub const CHECK: &str = \"Yes\";\n  bool get isCheck => group.groupId == \"Yes\";\n}\n\nenum DidCreateRowAction {\n  none,\n  openAsPage,\n  startEditing,\n}\n\nclass DidCreateRowResult {\n  DidCreateRowResult({\n    required this.action,\n    required this.rowMeta,\n    required this.groupId,\n  });\n\n  final DidCreateRowAction action;\n  final RowMetaPB rowMeta;\n  final String groupId;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/grid_notification.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flowy_infra/notifier.dart';\nimport 'package:protobuf/protobuf.dart';\n\nabstract class GroupControllerDelegate {\n  bool hasGroup(String groupId);\n  void removeRow(GroupPB group, RowId rowId);\n  void insertRow(GroupPB group, RowMetaPB row, int? index);\n  void updateRow(GroupPB group, RowMetaPB row);\n  void addNewRow(GroupPB group, RowMetaPB row, int? index);\n}\n\nclass GroupController {\n  GroupController({\n    required this.group,\n    required this.delegate,\n    required this.onGroupChanged,\n  }) : _listener = SingleGroupListener(group);\n\n  GroupPB group;\n  final SingleGroupListener _listener;\n  final GroupControllerDelegate delegate;\n  final void Function(GroupPB group) onGroupChanged;\n\n  RowMetaPB? rowAtIndex(int index) => group.rows.elementAtOrNull(index);\n\n  RowMetaPB? firstRow() => group.rows.firstOrNull;\n\n  RowMetaPB? lastRow() => group.rows.lastOrNull;\n\n  void startListening() {\n    _listener.start(\n      onGroupChanged: (result) {\n        result.fold(\n          (GroupRowsNotificationPB changeset) {\n            final newItems = [...group.rows];\n            final isGroupExist = delegate.hasGroup(group.groupId);\n            for (final deletedRow in changeset.deletedRows) {\n              newItems.removeWhere((rowPB) => rowPB.id == deletedRow);\n              if (isGroupExist) {\n                delegate.removeRow(group, deletedRow);\n              }\n            }\n\n            for (final insertedRow in changeset.insertedRows) {\n              if (newItems.any((rowPB) => rowPB.id == insertedRow.rowMeta.id)) {\n                continue;\n              }\n\n              final index = insertedRow.hasIndex() ? insertedRow.index : null;\n              if (insertedRow.hasIndex() &&\n                  newItems.length > insertedRow.index) {\n                newItems.insert(insertedRow.index, insertedRow.rowMeta);\n              } else {\n                newItems.add(insertedRow.rowMeta);\n              }\n\n              if (isGroupExist) {\n                if (insertedRow.isNew) {\n                  delegate.addNewRow(group, insertedRow.rowMeta, index);\n                } else {\n                  delegate.insertRow(group, insertedRow.rowMeta, index);\n                }\n              }\n            }\n\n            for (final updatedRow in changeset.updatedRows) {\n              final index = newItems.indexWhere(\n                (rowPB) => rowPB.id == updatedRow.id,\n              );\n\n              if (index != -1) {\n                newItems[index] = updatedRow;\n                if (isGroupExist) {\n                  delegate.updateRow(group, updatedRow);\n                }\n              }\n            }\n\n            group = group.rebuild((group) {\n              group.rows.clear();\n              group.rows.addAll(newItems);\n            });\n            group.freeze();\n            Log.debug(\n              \"Build GroupPB:${group.groupId}: items: ${group.rows.length}\",\n            );\n            onGroupChanged(group);\n          },\n          (err) => Log.error(err),\n        );\n      },\n    );\n  }\n\n  Future<void> dispose() async {\n    await _listener.stop();\n  }\n}\n\ntypedef UpdateGroupNotifiedValue\n    = FlowyResult<GroupRowsNotificationPB, FlowyError>;\n\nclass SingleGroupListener {\n  SingleGroupListener(this.group);\n\n  final GroupPB group;\n\n  PublishNotifier<UpdateGroupNotifiedValue>? _groupNotifier = PublishNotifier();\n  DatabaseNotificationListener? _listener;\n\n  void start({\n    required void Function(UpdateGroupNotifiedValue) onGroupChanged,\n  }) {\n    _groupNotifier?.addPublishListener(onGroupChanged);\n    _listener = DatabaseNotificationListener(\n      objectId: group.groupId,\n      handler: _handler,\n    );\n  }\n\n  void _handler(\n    DatabaseNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case DatabaseNotification.DidUpdateGroupRow:\n        result.fold(\n          (payload) => _groupNotifier?.value =\n              FlowyResult.success(GroupRowsNotificationPB.fromBuffer(payload)),\n          (error) => _groupNotifier?.value = FlowyResult.failure(error),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _listener?.stop();\n    _groupNotifier?.dispose();\n    _groupNotifier = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/board/board.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nclass BoardPluginBuilder implements PluginBuilder {\n  @override\n  Plugin build(dynamic data) {\n    if (data is ViewPB) {\n      return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data);\n    } else {\n      throw FlowyPluginException.invalidData;\n    }\n  }\n\n  @override\n  String get menuName => LocaleKeys.board_menuName.tr();\n\n  @override\n  FlowySvgData get icon => FlowySvgs.icon_board_s;\n\n  @override\n  PluginType get pluginType => PluginType.board;\n\n  @override\n  ViewLayoutPB get layoutType => ViewLayoutPB.Board;\n}\n\nclass BoardPluginConfig implements PluginConfig {\n  @override\n  bool get creatable => true;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/board/group_ext.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:calendar_view/calendar_view.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nextension GroupName on GroupPB {\n  String generateGroupName(DatabaseController databaseController) {\n    final fieldController = databaseController.fieldController;\n    final field = fieldController.getField(fieldId);\n    if (field == null) {\n      return \"\";\n    }\n\n    // if the group is the default group, then\n    if (isDefault) {\n      return \"No ${field.name}\";\n    }\n\n    final groupSettings = databaseController.fieldController.groupSettings\n        .firstWhereOrNull((gs) => gs.fieldId == field.id);\n\n    switch (field.fieldType) {\n      case FieldType.SingleSelect:\n        final options =\n            SingleSelectTypeOptionPB.fromBuffer(field.field.typeOptionData)\n                .options;\n        final option =\n            options.firstWhereOrNull((option) => option.id == groupId);\n        return option == null ? \"\" : option.name;\n      case FieldType.MultiSelect:\n        final options =\n            MultiSelectTypeOptionPB.fromBuffer(field.field.typeOptionData)\n                .options;\n        final option =\n            options.firstWhereOrNull((option) => option.id == groupId);\n        return option == null ? \"\" : option.name;\n      case FieldType.Checkbox:\n        return groupId;\n      case FieldType.URL:\n        return groupId;\n      case FieldType.DateTime:\n        final config = groupSettings?.content != null\n            ? DateGroupConfigurationPB.fromBuffer(groupSettings!.content)\n            : DateGroupConfigurationPB();\n        final dateFormat = DateFormat(\"y/MM/dd\");\n        try {\n          final targetDateTime = dateFormat.parseLoose(groupId);\n          switch (config.condition) {\n            case DateConditionPB.Day:\n              return DateFormat(\"MMM dd, y\").format(targetDateTime);\n            case DateConditionPB.Week:\n              final beginningOfWeek = targetDateTime\n                  .subtract(Duration(days: targetDateTime.weekday - 1));\n              final endOfWeek = targetDateTime.add(\n                Duration(days: DateTime.daysPerWeek - targetDateTime.weekday),\n              );\n\n              final beginningOfWeekFormat =\n                  beginningOfWeek.year != endOfWeek.year\n                      ? \"MMM dd y\"\n                      : \"MMM dd\";\n              final endOfWeekFormat = beginningOfWeek.month != endOfWeek.month\n                  ? \"MMM dd y\"\n                  : \"dd y\";\n\n              return LocaleKeys.board_dateCondition_weekOf.tr(\n                args: [\n                  DateFormat(beginningOfWeekFormat).format(beginningOfWeek),\n                  DateFormat(endOfWeekFormat).format(endOfWeek),\n                ],\n              );\n            case DateConditionPB.Month:\n              return DateFormat(\"MMM y\").format(targetDateTime);\n            case DateConditionPB.Year:\n              return DateFormat(\"y\").format(targetDateTime);\n            case DateConditionPB.Relative:\n              final targetDateTimeDay = DateTime(\n                targetDateTime.year,\n                targetDateTime.month,\n                targetDateTime.day,\n              );\n              final nowDay = DateTime.now().withoutTime;\n              final diff = targetDateTimeDay.difference(nowDay).inDays;\n              return switch (diff) {\n                0 => LocaleKeys.board_dateCondition_today.tr(),\n                -1 => LocaleKeys.board_dateCondition_yesterday.tr(),\n                1 => LocaleKeys.board_dateCondition_tomorrow.tr(),\n                -7 => LocaleKeys.board_dateCondition_lastSevenDays.tr(),\n                2 => LocaleKeys.board_dateCondition_nextSevenDays.tr(),\n                -30 => LocaleKeys.board_dateCondition_lastThirtyDays.tr(),\n                8 => LocaleKeys.board_dateCondition_nextThirtyDays.tr(),\n                _ => DateFormat(\"MMM y\").format(targetDateTimeDay)\n              };\n            default:\n              return \"\";\n          }\n        } on FormatException {\n          return \"\";\n        }\n      default:\n        return \"\";\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/database/board/mobile_board_page.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_controller.dart';\nimport 'package:appflowy/plugins/database/board/application/board_actions_bloc.dart';\nimport 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';\nimport 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';\nimport 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:appflowy/plugins/database/widgets/card/card_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_detail.dart';\nimport 'package:appflowy/shared/conditional_listenable_builder.dart';\nimport 'package:appflowy/shared/flowy_error_page.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_board/appflowy_board.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart' hide Card;\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../../widgets/card/card.dart';\nimport '../../widgets/cell/card_cell_builder.dart';\nimport '../application/board_bloc.dart';\nimport 'toolbar/board_setting_bar.dart';\nimport 'widgets/board_focus_scope.dart';\nimport 'widgets/board_hidden_groups.dart';\nimport 'widgets/board_shortcut_container.dart';\n\nclass BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {\n  final _toggleExtension = ToggleExtensionNotifier();\n\n  @override\n  Widget content(\n    BuildContext context,\n    ViewPB view,\n    DatabaseController controller,\n    bool shrinkWrap,\n    String? initialRowId,\n  ) =>\n      UniversalPlatform.isDesktop\n          ? DesktopBoardPage(\n              key: _makeValueKey(controller),\n              view: view,\n              databaseController: controller,\n              shrinkWrap: shrinkWrap,\n            )\n          : MobileBoardPage(\n              key: _makeValueKey(controller),\n              view: view,\n              databaseController: controller,\n            );\n\n  @override\n  Widget settingBar(BuildContext context, DatabaseController controller) =>\n      BoardSettingBar(\n        key: _makeValueKey(controller),\n        databaseController: controller,\n        toggleExtension: _toggleExtension,\n      );\n\n  @override\n  Widget settingBarExtension(\n    BuildContext context,\n    DatabaseController controller,\n  ) {\n    return DatabaseViewSettingExtension(\n      key: _makeValueKey(controller),\n      viewId: controller.viewId,\n      databaseController: controller,\n      toggleExtension: _toggleExtension,\n    );\n  }\n\n  @override\n  void dispose() {\n    _toggleExtension.dispose();\n    super.dispose();\n  }\n\n  ValueKey _makeValueKey(DatabaseController controller) =>\n      ValueKey(controller.viewId);\n}\n\nclass DesktopBoardPage extends StatefulWidget {\n  const DesktopBoardPage({\n    super.key,\n    required this.view,\n    required this.databaseController,\n    this.onEditStateChanged,\n    this.shrinkWrap = false,\n  });\n\n  final ViewPB view;\n\n  final DatabaseController databaseController;\n\n  /// Called when edit state changed\n  final VoidCallback? onEditStateChanged;\n\n  /// If true, the board will shrink wrap its content\n  final bool shrinkWrap;\n\n  @override\n  State<DesktopBoardPage> createState() => _DesktopBoardPageState();\n}\n\nclass _DesktopBoardPageState extends State<DesktopBoardPage> {\n  late final AppFlowyBoardController _boardController = AppFlowyBoardController(\n    onMoveGroup: (fromGroupId, fromIndex, toGroupId, toIndex) =>\n        widget.databaseController.moveGroup(\n      fromGroupId: fromGroupId,\n      toGroupId: toGroupId,\n    ),\n    onMoveGroupItem: (groupId, fromIndex, toIndex) {\n      final groupControllers = _boardBloc.groupControllers;\n      final fromRow = groupControllers[groupId]?.rowAtIndex(fromIndex);\n      final toRow = groupControllers[groupId]?.rowAtIndex(toIndex);\n      if (fromRow != null) {\n        widget.databaseController.moveGroupRow(\n          fromRow: fromRow,\n          toRow: toRow,\n          fromGroupId: groupId,\n          toGroupId: groupId,\n        );\n      }\n    },\n    onMoveGroupItemToGroup: (fromGroupId, fromIndex, toGroupId, toIndex) {\n      final groupControllers = _boardBloc.groupControllers;\n      final fromRow = groupControllers[fromGroupId]?.rowAtIndex(fromIndex);\n      final toRow = groupControllers[toGroupId]?.rowAtIndex(toIndex);\n      if (fromRow != null) {\n        widget.databaseController.moveGroupRow(\n          fromRow: fromRow,\n          toRow: toRow,\n          fromGroupId: fromGroupId,\n          toGroupId: toGroupId,\n        );\n      }\n    },\n    onStartDraggingCard: (groupId, index) {\n      final groupControllers = _boardBloc.groupControllers;\n      final toRow = groupControllers[groupId]?.rowAtIndex(index);\n      if (toRow != null) {\n        _focusScope.clear();\n      }\n    },\n  );\n\n  late final _focusScope = BoardFocusScope(\n    boardController: _boardController,\n  );\n  late final BoardBloc _boardBloc;\n  late final BoardActionsCubit _boardActionsCubit;\n  late final ValueNotifier<DidCreateRowResult?> _didCreateRow;\n\n  @override\n  void initState() {\n    super.initState();\n    _didCreateRow = ValueNotifier(null)..addListener(_handleDidCreateRow);\n    _boardBloc = BoardBloc(\n      databaseController: widget.databaseController,\n      didCreateRow: _didCreateRow,\n      boardController: _boardController,\n    )..add(const BoardEvent.initial());\n    _boardActionsCubit = BoardActionsCubit(\n      databaseController: widget.databaseController,\n    );\n  }\n\n  @override\n  void dispose() {\n    _focusScope.dispose();\n    _boardBloc.close();\n    _boardActionsCubit.close();\n    _didCreateRow.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider<BoardBloc>.value(value: _boardBloc),\n        BlocProvider.value(value: _boardActionsCubit),\n      ],\n      child: BlocBuilder<BoardBloc, BoardState>(\n        builder: (context, state) => state.maybeMap(\n          loading: (_) => const Center(\n            child: CircularProgressIndicator.adaptive(),\n          ),\n          error: (err) => Center(child: AppFlowyErrorPage(error: err.error)),\n          orElse: () => _BoardContent(\n            shrinkWrap: widget.shrinkWrap,\n            onEditStateChanged: widget.onEditStateChanged,\n            focusScope: _focusScope,\n            boardController: _boardController,\n            view: widget.view,\n          ),\n        ),\n      ),\n    );\n  }\n\n  void _handleDidCreateRow() async {\n    // work around: wait for the new card to be inserted into the board before enabling edit\n    await Future.delayed(const Duration(milliseconds: 50));\n    if (_didCreateRow.value != null) {\n      final result = _didCreateRow.value!;\n      switch (result.action) {\n        case DidCreateRowAction.openAsPage:\n          _boardActionsCubit.openCard(result.rowMeta);\n          break;\n        case DidCreateRowAction.startEditing:\n          _boardActionsCubit.startEditingRow(\n            GroupedRowId(\n              groupId: result.groupId,\n              rowId: result.rowMeta.id,\n            ),\n          );\n          break;\n        default:\n          break;\n      }\n    }\n  }\n}\n\nclass _BoardContent extends StatefulWidget {\n  const _BoardContent({\n    required this.boardController,\n    required this.focusScope,\n    required this.view,\n    this.onEditStateChanged,\n    this.shrinkWrap = false,\n  });\n\n  final AppFlowyBoardController boardController;\n  final BoardFocusScope focusScope;\n  final VoidCallback? onEditStateChanged;\n  final bool shrinkWrap;\n  final ViewPB view;\n\n  @override\n  State<_BoardContent> createState() => _BoardContentState();\n}\n\nclass _BoardContentState extends State<_BoardContent> {\n  final ScrollController scrollController = ScrollController();\n  final AppFlowyBoardScrollController scrollManager =\n      AppFlowyBoardScrollController();\n\n  final config = const AppFlowyBoardConfig(\n    groupMargin: EdgeInsets.symmetric(horizontal: 4),\n    groupBodyPadding: EdgeInsets.symmetric(horizontal: 4),\n    groupFooterPadding: EdgeInsets.fromLTRB(8, 14, 8, 4),\n    groupHeaderPadding: EdgeInsets.symmetric(horizontal: 8),\n    cardMargin: EdgeInsets.symmetric(horizontal: 4, vertical: 3),\n    stretchGroupHeight: false,\n  );\n\n  late final cellBuilder = CardCellBuilder(\n    databaseController: databaseController,\n  );\n\n  DatabaseController get databaseController =>\n      context.read<BoardBloc>().databaseController;\n\n  @override\n  void dispose() {\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final horizontalPadding =\n        context.read<DatabasePluginWidgetBuilderSize?>()?.horizontalPadding ??\n            0.0;\n    return MultiBlocListener(\n      listeners: [\n        BlocListener<BoardBloc, BoardState>(\n          listener: (context, state) {\n            state.maybeMap(\n              ready: (value) {\n                widget.onEditStateChanged?.call();\n              },\n              openRowDetail: (value) {\n                _openCard(\n                  context: context,\n                  databaseController:\n                      context.read<BoardBloc>().databaseController,\n                  rowMeta: value.rowMeta,\n                );\n              },\n              orElse: () {},\n            );\n          },\n        ),\n        BlocListener<BoardActionsCubit, BoardActionsState>(\n          listener: (context, state) {\n            state.maybeMap(\n              openCard: (value) {\n                _openCard(\n                  context: context,\n                  databaseController:\n                      context.read<BoardBloc>().databaseController,\n                  rowMeta: value.rowMeta,\n                );\n              },\n              setFocus: (value) {\n                widget.focusScope.focusedGroupedRows = value.groupedRowIds;\n              },\n              startEditingRow: (value) {\n                widget.boardController.enableGroupDragging(false);\n                widget.focusScope.clear();\n              },\n              endEditingRow: (value) {\n                widget.boardController.enableGroupDragging(true);\n              },\n              orElse: () {},\n            );\n          },\n        ),\n      ],\n      child: FocusScope(\n        autofocus: true,\n        child: BoardShortcutContainer(\n          focusScope: widget.focusScope,\n          child: Padding(\n            padding: const EdgeInsets.only(top: 8.0),\n            child: ValueListenableBuilder(\n              valueListenable: databaseController.compactModeNotifier,\n              builder: (context, compactMode, _) {\n                return AppFlowyBoard(\n                  boardScrollController: scrollManager,\n                  scrollController: scrollController,\n                  shrinkWrap: widget.shrinkWrap,\n                  controller: context.read<BoardBloc>().boardController,\n                  groupConstraints:\n                      BoxConstraints.tightFor(width: compactMode ? 196 : 256),\n                  config: config,\n                  leading: HiddenGroupsColumn(\n                    shrinkWrap: widget.shrinkWrap,\n                    margin: config.groupHeaderPadding +\n                        EdgeInsets.only(\n                          left: widget.shrinkWrap ? horizontalPadding : 0.0,\n                        ),\n                  ),\n                  trailing: context\n                              .read<BoardBloc>()\n                              .groupingFieldType\n                              ?.canCreateNewGroup ??\n                          false\n                      ? BoardTrailing(scrollController: scrollController)\n                      : const HSpace(40),\n                  headerBuilder: (_, groupData) => BlocProvider.value(\n                    value: context.read<BoardBloc>(),\n                    child: BoardColumnHeader(\n                      databaseController: databaseController,\n                      groupData: groupData,\n                      margin: config.groupHeaderPadding,\n                    ),\n                  ),\n                  footerBuilder: (_, groupData) => MultiBlocProvider(\n                    providers: [\n                      BlocProvider.value(value: context.read<BoardBloc>()),\n                      BlocProvider.value(\n                        value: context.read<BoardActionsCubit>(),\n                      ),\n                    ],\n                    child: BoardColumnFooter(\n                      columnData: groupData,\n                      boardConfig: config,\n                      scrollManager: scrollManager,\n                    ),\n                  ),\n                  cardBuilder: (cardContext, column, columnItem) =>\n                      MultiBlocProvider(\n                    key: ValueKey(\"board_card_${column.id}_${columnItem.id}\"),\n                    providers: [\n                      BlocProvider<BoardBloc>.value(\n                        value: cardContext.read<BoardBloc>(),\n                      ),\n                      BlocProvider.value(\n                        value: cardContext.read<BoardActionsCubit>(),\n                      ),\n                      BlocProvider(\n                        create: (_) => PageAccessLevelBloc(\n                          view: widget.view,\n                          ignorePageAccessLevel: true,\n                        )..add(PageAccessLevelEvent.initial()),\n                      ),\n                    ],\n                    child:\n                        BlocBuilder<PageAccessLevelBloc, PageAccessLevelState>(\n                      builder: (lockStatusContext, state) {\n                        return IgnorePointer(\n                          ignoring: !state.isEditable,\n                          child: _BoardCard(\n                            afGroupData: column,\n                            groupItem: columnItem as GroupItem,\n                            boardConfig: config,\n                            notifier: widget.focusScope,\n                            cellBuilder: cellBuilder,\n                            compactMode: compactMode,\n                            onOpenCard: (rowMeta) => _openCard(\n                              context: context,\n                              databaseController: lockStatusContext\n                                  .read<BoardBloc>()\n                                  .databaseController,\n                              rowMeta: rowMeta,\n                            ),\n                          ),\n                        );\n                      },\n                    ),\n                  ),\n                );\n              },\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\n@visibleForTesting\nclass BoardColumnFooter extends StatefulWidget {\n  const BoardColumnFooter({\n    super.key,\n    required this.columnData,\n    required this.boardConfig,\n    required this.scrollManager,\n  });\n\n  final AppFlowyGroupData columnData;\n  final AppFlowyBoardConfig boardConfig;\n  final AppFlowyBoardScrollController scrollManager;\n\n  @override\n  State<BoardColumnFooter> createState() => _BoardColumnFooterState();\n}\n\nclass _BoardColumnFooterState extends State<BoardColumnFooter> {\n  final TextEditingController _textController = TextEditingController();\n  late final FocusNode _focusNode;\n  bool _isCreating = false;\n\n  @override\n  void initState() {\n    super.initState();\n    _focusNode = FocusNode(\n      onKeyEvent: (node, event) {\n        if (_focusNode.hasFocus &&\n            event.logicalKey == LogicalKeyboardKey.escape) {\n          _focusNode.unfocus();\n          return KeyEventResult.handled;\n        }\n        return KeyEventResult.ignored;\n      },\n    )..addListener(() {\n        if (!_focusNode.hasFocus) {\n          setState(() => _isCreating = false);\n        }\n      });\n  }\n\n  @override\n  void dispose() {\n    _textController.dispose();\n    _focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      if (_isCreating) {\n        _focusNode.requestFocus();\n      }\n    });\n    return Padding(\n      padding: widget.boardConfig.groupFooterPadding,\n      child: AnimatedSwitcher(\n        duration: const Duration(milliseconds: 150),\n        child:\n            _isCreating ? _createCardsTextField() : _startCreatingCardsButton(),\n      ),\n    );\n  }\n\n  Widget _createCardsTextField() {\n    const nada = DoNothingAndStopPropagationIntent();\n    return Shortcuts(\n      shortcuts: {\n        const SingleActivator(LogicalKeyboardKey.arrowUp): nada,\n        const SingleActivator(LogicalKeyboardKey.arrowDown): nada,\n        const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): nada,\n        const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): nada,\n        const SingleActivator(LogicalKeyboardKey.keyE): nada,\n        const SingleActivator(LogicalKeyboardKey.keyN): nada,\n        const SingleActivator(LogicalKeyboardKey.delete): nada,\n        // const SingleActivator(LogicalKeyboardKey.backspace): nada,\n        const SingleActivator(LogicalKeyboardKey.enter): nada,\n        const SingleActivator(LogicalKeyboardKey.numpadEnter): nada,\n        const SingleActivator(LogicalKeyboardKey.comma): nada,\n        const SingleActivator(LogicalKeyboardKey.period): nada,\n        SingleActivator(\n          LogicalKeyboardKey.arrowUp,\n          shift: true,\n          meta: Platform.isMacOS,\n          control: !Platform.isMacOS,\n        ): nada,\n      },\n      child: FlowyTextField(\n        hintTextConstraints: const BoxConstraints(maxHeight: 36),\n        controller: _textController,\n        focusNode: _focusNode,\n        onSubmitted: (name) {\n          context.read<BoardBloc>().add(\n                BoardEvent.createRow(\n                  widget.columnData.id,\n                  OrderObjectPositionTypePB.End,\n                  name,\n                  null,\n                ),\n              );\n          widget.scrollManager.scrollToBottom(widget.columnData.id);\n          _textController.clear();\n          _focusNode.requestFocus();\n        },\n      ),\n    );\n  }\n\n  Widget _startCreatingCardsButton() {\n    return BlocListener<BoardActionsCubit, BoardActionsState>(\n      listener: (context, state) {\n        state.maybeWhen(\n          startCreateBottomRow: (groupId) {\n            if (groupId == widget.columnData.id) {\n              setState(() => _isCreating = true);\n            }\n          },\n          orElse: () {},\n        );\n      },\n      child: FlowyTooltip(\n        message: LocaleKeys.board_column_addToColumnBottomTooltip.tr(),\n        child: SizedBox(\n          height: 36,\n          child: FlowyButton(\n            leftIcon: FlowySvg(\n              FlowySvgs.add_s,\n              color: Theme.of(context).hintColor,\n            ),\n            text: FlowyText(\n              LocaleKeys.board_column_createNewCard.tr(),\n              color: Theme.of(context).hintColor,\n            ),\n            onTap: () {\n              context\n                  .read<BoardActionsCubit>()\n                  .startCreateBottomRow(widget.columnData.id);\n            },\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _BoardCard extends StatefulWidget {\n  const _BoardCard({\n    required this.afGroupData,\n    required this.groupItem,\n    required this.boardConfig,\n    required this.cellBuilder,\n    required this.notifier,\n    required this.compactMode,\n    required this.onOpenCard,\n  });\n\n  final AppFlowyGroupData afGroupData;\n  final GroupItem groupItem;\n  final AppFlowyBoardConfig boardConfig;\n  final CardCellBuilder cellBuilder;\n  final BoardFocusScope notifier;\n  final bool compactMode;\n  final void Function(RowMetaPB) onOpenCard;\n\n  @override\n  State<_BoardCard> createState() => _BoardCardState();\n}\n\nclass _BoardCardState extends State<_BoardCard> {\n  bool _isEditing = false;\n\n  @override\n  Widget build(BuildContext context) {\n    final boardBloc = context.read<BoardBloc>();\n    final groupData = widget.afGroupData.customData as GroupData;\n    final rowCache = boardBloc.rowCache;\n    final databaseController = boardBloc.databaseController;\n    final rowMeta =\n        rowCache.getRow(widget.groupItem.id)?.rowMeta ?? widget.groupItem.row;\n\n    const nada = DoNothingAndStopPropagationIntent();\n\n    return BlocListener<BoardActionsCubit, BoardActionsState>(\n      listener: (context, state) {\n        state.maybeMap(\n          startEditingRow: (value) {\n            if (value.groupedRowId.rowId == widget.groupItem.id &&\n                value.groupedRowId.groupId == groupData.group.groupId) {\n              setState(() => _isEditing = true);\n            }\n          },\n          endEditingRow: (_) {\n            if (_isEditing) {\n              setState(() => _isEditing = false);\n            }\n          },\n          createRow: (value) {\n            if ((_isEditing && value.groupedRowId == null) ||\n                (value.groupedRowId?.rowId == widget.groupItem.id &&\n                    value.groupedRowId?.groupId == groupData.group.groupId)) {\n              context.read<BoardBloc>().add(\n                    BoardEvent.createRow(\n                      groupData.group.groupId,\n                      value.position == CreateBoardCardRelativePosition.before\n                          ? OrderObjectPositionTypePB.Before\n                          : OrderObjectPositionTypePB.After,\n                      null,\n                      widget.groupItem.row.id,\n                    ),\n                  );\n            }\n          },\n          orElse: () {},\n        );\n      },\n      child: Shortcuts(\n        shortcuts: {\n          const SingleActivator(LogicalKeyboardKey.arrowUp): nada,\n          const SingleActivator(LogicalKeyboardKey.arrowDown): nada,\n          const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): nada,\n          const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true):\n              nada,\n          const SingleActivator(LogicalKeyboardKey.keyE): nada,\n          const SingleActivator(LogicalKeyboardKey.keyN): nada,\n          const SingleActivator(LogicalKeyboardKey.delete): nada,\n          // const SingleActivator(LogicalKeyboardKey.backspace): nada,\n          const SingleActivator(LogicalKeyboardKey.enter): nada,\n          const SingleActivator(LogicalKeyboardKey.numpadEnter): nada,\n          const SingleActivator(LogicalKeyboardKey.comma): nada,\n          const SingleActivator(LogicalKeyboardKey.period): nada,\n          SingleActivator(\n            LogicalKeyboardKey.arrowUp,\n            shift: true,\n            meta: Platform.isMacOS,\n            control: !Platform.isMacOS,\n          ): nada,\n        },\n        child: ConditionalListenableBuilder<List<GroupedRowId>>(\n          valueListenable: widget.notifier,\n          buildWhen: (previous, current) {\n            final focusItem = GroupedRowId(\n              groupId: groupData.group.groupId,\n              rowId: rowMeta.id,\n            );\n            final previousContainsFocus = previous.contains(focusItem);\n            final currentContainsFocus = current.contains(focusItem);\n\n            return previousContainsFocus != currentContainsFocus;\n          },\n          builder: (context, focusedItems, child) {\n            final cardMargin = widget.boardConfig.cardMargin;\n            final margin = widget.compactMode\n                ? cardMargin - EdgeInsets.symmetric(horizontal: 2)\n                : cardMargin;\n            return Container(\n              margin: margin,\n              decoration: _makeBoxDecoration(\n                context,\n                groupData.group.groupId,\n                widget.groupItem.id,\n              ),\n              child: child,\n            );\n          },\n          child: RowCard(\n            fieldController: databaseController.fieldController,\n            rowMeta: rowMeta,\n            viewId: boardBloc.viewId,\n            rowCache: rowCache,\n            groupingFieldId: widget.groupItem.fieldInfo.id,\n            isEditing: _isEditing,\n            cellBuilder: widget.cellBuilder,\n            onTap: (context) => widget.onOpenCard(\n              context.read<CardBloc>().rowController.rowMeta,\n            ),\n            onShiftTap: (_) {\n              Focus.of(context).requestFocus();\n              widget.notifier.toggle(\n                GroupedRowId(\n                  rowId: widget.groupItem.row.id,\n                  groupId: groupData.group.groupId,\n                ),\n              );\n            },\n            styleConfiguration: RowCardStyleConfiguration(\n              cellStyleMap: desktopBoardCardCellStyleMap(context),\n              hoverStyle: HoverStyle(\n                hoverColor: Theme.of(context).brightness == Brightness.light\n                    ? const Color(0x0F1F2329)\n                    : const Color(0x0FEFF4FB),\n                foregroundColorOnHover:\n                    AFThemeExtension.of(context).onBackground,\n              ),\n            ),\n            onStartEditing: () =>\n                context.read<BoardActionsCubit>().startEditingRow(\n                      GroupedRowId(\n                        groupId: groupData.group.groupId,\n                        rowId: rowMeta.id,\n                      ),\n                    ),\n            onEndEditing: () => context.read<BoardActionsCubit>().endEditing(\n                  GroupedRowId(\n                    groupId: groupData.group.groupId,\n                    rowId: rowMeta.id,\n                  ),\n                ),\n            userProfile: context.read<BoardBloc>().userProfile,\n          ),\n        ),\n      ),\n    );\n  }\n\n  BoxDecoration _makeBoxDecoration(\n    BuildContext context,\n    String groupId,\n    String rowId,\n  ) {\n    return BoxDecoration(\n      color: Theme.of(context).colorScheme.surface,\n      borderRadius: const BorderRadius.all(Radius.circular(6)),\n      border: Border.fromBorderSide(\n        BorderSide(\n          color: widget.notifier\n                  .isFocused(GroupedRowId(rowId: rowId, groupId: groupId))\n              ? Theme.of(context).colorScheme.primary\n              : Theme.of(context).brightness == Brightness.light\n                  ? const Color(0xFF1F2329).withValues(alpha: 0.12)\n                  : const Color(0xFF59647A),\n        ),\n      ),\n      boxShadow: [\n        BoxShadow(\n          blurRadius: 4,\n          color: const Color(0xFF1F2329).withValues(alpha: 0.02),\n        ),\n        BoxShadow(\n          blurRadius: 4,\n          spreadRadius: -2,\n          color: const Color(0xFF1F2329).withValues(alpha: 0.02),\n        ),\n      ],\n    );\n  }\n}\n\nclass BoardTrailing extends StatefulWidget {\n  const BoardTrailing({super.key, required this.scrollController});\n\n  final ScrollController scrollController;\n\n  @override\n  State<BoardTrailing> createState() => _BoardTrailingState();\n}\n\nclass _BoardTrailingState extends State<BoardTrailing> {\n  final TextEditingController _textController = TextEditingController();\n  late final FocusNode _focusNode;\n\n  bool isEditing = false;\n\n  void _cancelAddNewGroup() {\n    _textController.clear();\n    setState(() => isEditing = false);\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _focusNode = FocusNode(\n      onKeyEvent: (node, event) {\n        if (_focusNode.hasFocus &&\n            event.logicalKey == LogicalKeyboardKey.escape) {\n          _cancelAddNewGroup();\n          return KeyEventResult.handled;\n        }\n        return KeyEventResult.ignored;\n      },\n    )..addListener(_onFocusChanged);\n  }\n\n  @override\n  void dispose() {\n    _focusNode.removeListener(_onFocusChanged);\n    _focusNode.dispose();\n    _textController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    // call after every setState\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      if (isEditing) {\n        _focusNode.requestFocus();\n        widget.scrollController.jumpTo(\n          widget.scrollController.position.maxScrollExtent,\n        );\n      }\n    });\n\n    return Container(\n      padding: const EdgeInsets.only(left: 8.0, top: 12, right: 40),\n      alignment: AlignmentDirectional.topStart,\n      child: AnimatedSwitcher(\n        duration: const Duration(milliseconds: 300),\n        child: isEditing\n            ? SizedBox(\n                width: 256,\n                child: Padding(\n                  padding: const EdgeInsets.all(8.0),\n                  child: TextField(\n                    controller: _textController,\n                    focusNode: _focusNode,\n                    decoration: InputDecoration(\n                      suffixIcon: Padding(\n                        padding: const EdgeInsets.only(left: 4, bottom: 8.0),\n                        child: FlowyIconButton(\n                          icon: const FlowySvg(FlowySvgs.close_filled_s),\n                          hoverColor: Colors.transparent,\n                          onPressed: () => _textController.clear(),\n                        ),\n                      ),\n                      suffixIconConstraints:\n                          BoxConstraints.loose(const Size(20, 24)),\n                      border: const UnderlineInputBorder(),\n                      contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 8),\n                      isDense: true,\n                    ),\n                    style: Theme.of(context).textTheme.bodySmall,\n                    onSubmitted: (groupName) => context\n                        .read<BoardBloc>()\n                        .add(BoardEvent.createGroup(groupName)),\n                  ),\n                ),\n              )\n            : FlowyTooltip(\n                message: LocaleKeys.board_column_createNewColumn.tr(),\n                child: FlowyIconButton(\n                  width: 26,\n                  icon: const FlowySvg(FlowySvgs.add_s),\n                  iconColorOnHover: Theme.of(context).colorScheme.onSurface,\n                  onPressed: () => setState(() => isEditing = true),\n                ),\n              ),\n      ),\n    );\n  }\n\n  void _onFocusChanged() {\n    if (!_focusNode.hasFocus) {\n      _cancelAddNewGroup();\n    }\n  }\n}\n\nvoid _openCard({\n  required BuildContext context,\n  required DatabaseController databaseController,\n  required RowMetaPB rowMeta,\n}) {\n  final rowController = RowController(\n    rowMeta: rowMeta,\n    viewId: databaseController.viewId,\n    rowCache: databaseController.rowCache,\n  );\n\n  FlowyOverlay.show(\n    context: context,\n    builder: (_) => BlocProvider.value(\n      value: context.read<UserWorkspaceBloc>(),\n      child: RowDetailPage(\n        databaseController: databaseController,\n        rowController: rowController,\n        userProfile: context.read<BoardBloc>().userProfile,\n      ),\n    ),\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart",
    "content": "import 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/view_database_button.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/setting_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\n\nclass BoardSettingBar extends StatelessWidget {\n  const BoardSettingBar({\n    super.key,\n    required this.databaseController,\n    required this.toggleExtension,\n  });\n\n  final DatabaseController databaseController;\n  final ToggleExtensionNotifier toggleExtension;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => FilterEditorBloc(\n        viewId: databaseController.viewId,\n        fieldController: databaseController.fieldController,\n      ),\n      child: ValueListenableBuilder<bool>(\n        valueListenable: databaseController.isLoading,\n        builder: (context, value, child) {\n          if (value) {\n            return const SizedBox.shrink();\n          }\n          final isReference =\n              Provider.of<ReferenceState?>(context)?.isReference ?? false;\n          return SizedBox(\n            height: 20,\n            child: Row(\n              mainAxisAlignment: MainAxisAlignment.end,\n              children: [\n                FilterButton(\n                  toggleExtension: toggleExtension,\n                ),\n                if (isReference) ...[\n                  const HSpace(2),\n                  ViewDatabaseButton(view: databaseController.view),\n                ],\n                const HSpace(2),\n                SettingButton(\n                  databaseController: databaseController,\n                ),\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_checkbox_column_header.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/board/application/board_bloc.dart';\nimport 'package:appflowy/plugins/database/board/group_ext.dart';\nimport 'package:appflowy_board/appflowy_board.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport 'board_column_header.dart';\n\nclass CheckboxColumnHeader extends StatelessWidget {\n  const CheckboxColumnHeader({\n    super.key,\n    required this.databaseController,\n    required this.groupData,\n  });\n\n  final DatabaseController databaseController;\n  final AppFlowyGroupData groupData;\n\n  @override\n  Widget build(BuildContext context) {\n    final customData = groupData.customData as GroupData;\n    final groupName = customData.group.generateGroupName(databaseController);\n    return Row(\n      children: [\n        FlowySvg(\n          customData.asCheckboxGroup()!.isCheck\n              ? FlowySvgs.check_filled_s\n              : FlowySvgs.uncheck_s,\n          blendMode: BlendMode.dst,\n          size: const Size.square(18),\n        ),\n        const HSpace(6),\n        Expanded(\n          child: Align(\n            alignment: AlignmentDirectional.centerStart,\n            child: FlowyTooltip(\n              message: groupName,\n              child: FlowyText.medium(\n                groupName,\n                overflow: TextOverflow.ellipsis,\n              ),\n            ),\n          ),\n        ),\n        const HSpace(6),\n        GroupOptionsButton(\n          groupData: groupData,\n        ),\n        const HSpace(4),\n        CreateCardFromTopButton(\n          groupId: groupData.id,\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_column_header.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/board/application/board_bloc.dart';\nimport 'package:appflowy/plugins/database/board/group_ext.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_board/appflowy_board.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'board_checkbox_column_header.dart';\nimport 'board_editable_column_header.dart';\n\nclass BoardColumnHeader extends StatefulWidget {\n  const BoardColumnHeader({\n    super.key,\n    required this.databaseController,\n    required this.groupData,\n    required this.margin,\n  });\n\n  final DatabaseController databaseController;\n  final AppFlowyGroupData groupData;\n  final EdgeInsets margin;\n\n  @override\n  State<BoardColumnHeader> createState() => _BoardColumnHeaderState();\n}\n\nclass _BoardColumnHeaderState extends State<BoardColumnHeader> {\n  final ValueNotifier<bool> isEditing = ValueNotifier(false);\n\n  GroupData get customData => widget.groupData.customData;\n\n  @override\n  void dispose() {\n    isEditing.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final Widget child = switch (customData.fieldType) {\n      FieldType.MultiSelect ||\n      FieldType.SingleSelect when !customData.group.isDefault =>\n        EditableColumnHeader(\n          databaseController: widget.databaseController,\n          groupData: widget.groupData,\n          isEditing: isEditing,\n          onSubmitted: (columnName) {\n            context\n                .read<BoardBloc>()\n                .add(BoardEvent.renameGroup(widget.groupData.id, columnName));\n          },\n        ),\n      FieldType.Checkbox => CheckboxColumnHeader(\n          databaseController: widget.databaseController,\n          groupData: widget.groupData,\n        ),\n      _ => _DefaultColumnHeaderContent(\n          databaseController: widget.databaseController,\n          groupData: widget.groupData,\n        ),\n    };\n\n    return Container(\n      padding: widget.margin,\n      height: 50,\n      child: child,\n    );\n  }\n}\n\nclass GroupOptionsButton extends StatelessWidget {\n  const GroupOptionsButton({\n    super.key,\n    required this.groupData,\n    this.isEditing,\n  });\n\n  final AppFlowyGroupData groupData;\n  final ValueNotifier<bool>? isEditing;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      clickHandler: PopoverClickHandler.gestureDetector,\n      margin: const EdgeInsets.all(8),\n      constraints: BoxConstraints.loose(const Size(168, 300)),\n      direction: PopoverDirection.bottomWithLeftAligned,\n      child: FlowyIconButton(\n        width: 20,\n        icon: const FlowySvg(FlowySvgs.details_horizontal_s),\n        iconColorOnHover: Theme.of(context).colorScheme.onSurface,\n      ),\n      popupBuilder: (popoverContext) {\n        final customGroupData = groupData.customData as GroupData;\n        final isDefault = customGroupData.group.isDefault;\n        final menuItems = GroupOption.values.toList();\n        if (!customGroupData.fieldType.canEditHeader || isDefault) {\n          menuItems.remove(GroupOption.rename);\n        }\n        if (!customGroupData.fieldType.canDeleteGroup || isDefault) {\n          menuItems.remove(GroupOption.delete);\n        }\n        return SeparatedColumn(\n          mainAxisSize: MainAxisSize.min,\n          separatorBuilder: () => const VSpace(4),\n          children: [\n            ...menuItems.map(\n              (action) => SizedBox(\n                height: GridSize.popoverItemHeight,\n                child: FlowyButton(\n                  leftIcon: FlowySvg(action.icon),\n                  text: FlowyText(\n                    action.text,\n                    lineHeight: 1.0,\n                    overflow: TextOverflow.ellipsis,\n                  ),\n                  onTap: () {\n                    run(context, action, customGroupData.group);\n                    PopoverContainer.of(popoverContext).close();\n                  },\n                ),\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  void run(BuildContext context, GroupOption option, GroupPB group) {\n    switch (option) {\n      case GroupOption.rename:\n        isEditing?.value = true;\n        break;\n      case GroupOption.hide:\n        context\n            .read<BoardBloc>()\n            .add(BoardEvent.setGroupVisibility(group, false));\n        break;\n      case GroupOption.delete:\n        showConfirmDeletionDialog(\n          context: context,\n          name: LocaleKeys.board_column_label.tr(),\n          description: LocaleKeys.board_column_deleteColumnConfirmation.tr(),\n          onConfirm: () {\n            context\n                .read<BoardBloc>()\n                .add(BoardEvent.deleteGroup(group.groupId));\n          },\n        );\n        break;\n    }\n  }\n}\n\nclass CreateCardFromTopButton extends StatelessWidget {\n  const CreateCardFromTopButton({\n    super.key,\n    required this.groupId,\n  });\n\n  final String groupId;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      message: LocaleKeys.board_column_addToColumnTopTooltip.tr(),\n      preferBelow: false,\n      child: FlowyIconButton(\n        width: 20,\n        icon: const FlowySvg(FlowySvgs.add_s),\n        iconColorOnHover: Theme.of(context).colorScheme.onSurface,\n        onPressed: () => context.read<BoardBloc>().add(\n              BoardEvent.createRow(\n                groupId,\n                OrderObjectPositionTypePB.Start,\n                null,\n                null,\n              ),\n            ),\n      ),\n    );\n  }\n}\n\nclass _DefaultColumnHeaderContent extends StatelessWidget {\n  const _DefaultColumnHeaderContent({\n    required this.databaseController,\n    required this.groupData,\n  });\n\n  final DatabaseController databaseController;\n  final AppFlowyGroupData groupData;\n\n  @override\n  Widget build(BuildContext context) {\n    final customData = groupData.customData as GroupData;\n    final groupName = customData.group.generateGroupName(databaseController);\n    return Row(\n      children: [\n        Expanded(\n          child: Align(\n            alignment: AlignmentDirectional.centerStart,\n            child: FlowyTooltip(\n              message: groupName,\n              child: FlowyText.medium(\n                groupName,\n                overflow: TextOverflow.ellipsis,\n              ),\n            ),\n          ),\n        ),\n        const HSpace(6),\n        GroupOptionsButton(\n          groupData: groupData,\n        ),\n        const HSpace(4),\n        CreateCardFromTopButton(\n          groupId: groupData.id,\n        ),\n      ],\n    );\n  }\n}\n\nenum GroupOption {\n  rename,\n  hide,\n  delete;\n\n  FlowySvgData get icon => switch (this) {\n        rename => FlowySvgs.edit_s,\n        hide => FlowySvgs.hide_s,\n        delete => FlowySvgs.delete_s,\n      };\n\n  String get text => switch (this) {\n        rename => LocaleKeys.board_column_renameColumn.tr(),\n        hide => LocaleKeys.board_column_hideColumn.tr(),\n        delete => LocaleKeys.board_column_deleteColumn.tr(),\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_editable_column_header.dart",
    "content": "import 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';\nimport 'package:appflowy/plugins/database/board/application/board_bloc.dart';\nimport 'package:appflowy/plugins/database/board/group_ext.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_board/appflowy_board.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nimport 'board_column_header.dart';\n\n/// This column header is used for the MultiSelect and SingleSelect field types.\nclass EditableColumnHeader extends StatefulWidget {\n  const EditableColumnHeader({\n    super.key,\n    required this.databaseController,\n    required this.groupData,\n    required this.isEditing,\n    required this.onSubmitted,\n  });\n\n  final DatabaseController databaseController;\n  final AppFlowyGroupData groupData;\n  final ValueNotifier<bool> isEditing;\n  final void Function(String columnName) onSubmitted;\n\n  @override\n  State<EditableColumnHeader> createState() => _EditableColumnHeaderState();\n}\n\nclass _EditableColumnHeaderState extends State<EditableColumnHeader> {\n  late final FocusNode focusNode;\n  late final TextEditingController textController = TextEditingController(\n    text: _generateGroupName(),\n  );\n\n  GroupData get customData => widget.groupData.customData;\n\n  @override\n  void initState() {\n    super.initState();\n    focusNode = FocusNode(\n      onKeyEvent: (node, event) {\n        if (event.logicalKey == LogicalKeyboardKey.escape &&\n            event is KeyUpEvent) {\n          focusNode.unfocus();\n          return KeyEventResult.handled;\n        }\n        return KeyEventResult.ignored;\n      },\n    )..addListener(onFocusChanged);\n  }\n\n  @override\n  void didUpdateWidget(covariant oldWidget) {\n    if (oldWidget.groupData.customData != widget.groupData.customData) {\n      textController.text = _generateGroupName();\n    }\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  void dispose() {\n    focusNode\n      ..removeListener(onFocusChanged)\n      ..dispose();\n    textController.dispose();\n    super.dispose();\n  }\n\n  void onFocusChanged() {\n    if (!focusNode.hasFocus) {\n      widget.isEditing.value = false;\n      widget.onSubmitted(textController.text);\n    } else {\n      textController.selection = TextSelection(\n        baseOffset: 0,\n        extentOffset: textController.text.length,\n      );\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        Expanded(\n          child: ValueListenableBuilder(\n            valueListenable: widget.isEditing,\n            builder: (context, isEditing, _) {\n              if (isEditing) {\n                focusNode.requestFocus();\n              }\n              return isEditing ? _buildTextField() : _buildTitle();\n            },\n          ),\n        ),\n        const HSpace(6),\n        GroupOptionsButton(\n          groupData: widget.groupData,\n          isEditing: widget.isEditing,\n        ),\n        const HSpace(4),\n        CreateCardFromTopButton(\n          groupId: widget.groupData.id,\n        ),\n      ],\n    );\n  }\n\n  Widget _buildTitle() {\n    final (backgroundColor, dotColor) = _generateGroupColor();\n    final groupName = _generateGroupName();\n    return MouseRegion(\n      cursor: SystemMouseCursors.click,\n      child: GestureDetector(\n        behavior: HitTestBehavior.opaque,\n        onTap: () {\n          widget.isEditing.value = true;\n        },\n        child: Align(\n          alignment: AlignmentDirectional.centerStart,\n          child: FlowyTooltip(\n            message: groupName,\n            child: Container(\n              height: 20,\n              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),\n              decoration: BoxDecoration(\n                color: backgroundColor,\n                borderRadius: BorderRadius.circular(6),\n              ),\n              child: Row(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  Center(\n                    child: Container(\n                      height: 6,\n                      width: 6,\n                      decoration: BoxDecoration(\n                        color: dotColor,\n                        borderRadius: BorderRadius.circular(3),\n                      ),\n                    ),\n                  ),\n                  const HSpace(4.0),\n                  Flexible(\n                    child: FlowyText.medium(\n                      groupName,\n                      overflow: TextOverflow.ellipsis,\n                      lineHeight: 1.0,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildTextField() {\n    return TextField(\n      controller: textController,\n      focusNode: focusNode,\n      onEditingComplete: () {\n        widget.isEditing.value = false;\n      },\n      onSubmitted: widget.onSubmitted,\n      style: Theme.of(context).textTheme.bodyMedium,\n      decoration: InputDecoration(\n        filled: true,\n        fillColor: Theme.of(context).colorScheme.surface,\n        hoverColor: Colors.transparent,\n        contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),\n        focusedBorder: OutlineInputBorder(\n          borderSide: BorderSide(\n            color: Theme.of(context).colorScheme.primary,\n          ),\n        ),\n        enabledBorder: OutlineInputBorder(\n          borderSide: BorderSide(\n            color: Theme.of(context).colorScheme.primary,\n          ),\n        ),\n        border: OutlineInputBorder(\n          borderSide: BorderSide(\n            color: Theme.of(context).colorScheme.primary,\n          ),\n        ),\n        isDense: true,\n      ),\n    );\n  }\n\n  String _generateGroupName() {\n    return customData.group.generateGroupName(widget.databaseController);\n  }\n\n  (Color? backgroundColor, Color? dotColor) _generateGroupColor() {\n    Color? backgroundColor;\n    Color? dotColor;\n\n    final groupId = widget.groupData.id;\n    final fieldId = customData.fieldInfo.id;\n    final field = widget.databaseController.fieldController.getField(fieldId);\n    if (field != null) {\n      final selectOptions = switch (field.fieldType) {\n        FieldType.MultiSelect => MultiSelectTypeOptionDataParser()\n            .fromBuffer(field.field.typeOptionData)\n            .options,\n        FieldType.SingleSelect => SingleSelectTypeOptionDataParser()\n            .fromBuffer(field.field.typeOptionData)\n            .options,\n        _ => <SelectOptionPB>[],\n      };\n\n      final colorPB =\n          selectOptions.firstWhereOrNull((e) => e.id == groupId)?.color;\n\n      if (colorPB != null) {\n        backgroundColor = colorPB.toColor(context);\n        dotColor = getColorOfDot(colorPB);\n      }\n    }\n\n    return (backgroundColor, dotColor);\n  }\n\n  // move to theme file and allow theme customization once palette is finalized\n  Color getColorOfDot(SelectOptionColorPB color) {\n    return switch (Theme.of(context).brightness) {\n      Brightness.light => switch (color) {\n          SelectOptionColorPB.Purple => const Color(0xFFAB8DFF),\n          SelectOptionColorPB.Pink => const Color(0xFFFF8EF5),\n          SelectOptionColorPB.LightPink => const Color(0xFFFF85A9),\n          SelectOptionColorPB.Orange => const Color(0xFFFFBC7E),\n          SelectOptionColorPB.Yellow => const Color(0xFFFCD86F),\n          SelectOptionColorPB.Lime => const Color(0xFFC6EC41),\n          SelectOptionColorPB.Green => const Color(0xFF74F37D),\n          SelectOptionColorPB.Aqua => const Color(0xFF40F0D1),\n          SelectOptionColorPB.Blue => const Color(0xFF00C8FF),\n          _ => throw ArgumentError,\n        },\n      Brightness.dark => switch (color) {\n          SelectOptionColorPB.Purple => const Color(0xFF502FD6),\n          SelectOptionColorPB.Pink => const Color(0xFFBF1CC0),\n          SelectOptionColorPB.LightPink => const Color(0xFFC42A53),\n          SelectOptionColorPB.Orange => const Color(0xFFD77922),\n          SelectOptionColorPB.Yellow => const Color(0xFFC59A1A),\n          SelectOptionColorPB.Lime => const Color(0xFFA4C824),\n          SelectOptionColorPB.Green => const Color(0xFF23CA2E),\n          SelectOptionColorPB.Aqua => const Color(0xFF19CCAC),\n          SelectOptionColorPB.Blue => const Color(0xFF04A9D7),\n          _ => throw ArgumentError,\n        }\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_focus_scope.dart",
    "content": "import 'package:appflowy/plugins/database/board/application/board_bloc.dart';\nimport 'package:appflowy_board/appflowy_board.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/foundation.dart';\n\nclass BoardFocusScope extends ChangeNotifier\n    implements ValueListenable<List<GroupedRowId>> {\n  BoardFocusScope({\n    required this.boardController,\n  });\n\n  final AppFlowyBoardController boardController;\n  List<GroupedRowId> _focusedCards = [];\n\n  @override\n  List<GroupedRowId> get value => _focusedCards;\n\n  UnmodifiableListView<GroupedRowId> get focusedGroupedRows =>\n      UnmodifiableListView(_focusedCards);\n\n  set focusedGroupedRows(List<GroupedRowId> focusedGroupedRows) {\n    _deepCopy();\n    _focusedCards\n      ..clear()\n      ..addAll(focusedGroupedRows);\n    notifyListeners();\n  }\n\n  bool isFocused(GroupedRowId groupedRowId) =>\n      _focusedCards.contains(groupedRowId);\n\n  void toggle(GroupedRowId groupedRowId) {\n    _deepCopy();\n    if (_focusedCards.contains(groupedRowId)) {\n      _focusedCards.remove(groupedRowId);\n    } else {\n      _focusedCards.add(groupedRowId);\n    }\n    notifyListeners();\n  }\n\n  bool focusNext() {\n    _deepCopy();\n\n    // if no card is focused, focus on the first card in the board\n    if (_focusedCards.isEmpty) {\n      _focusFirstCard();\n      notifyListeners();\n      return true;\n    }\n\n    final lastFocusedCard = _focusedCards.last;\n    final groupController = boardController.controller(lastFocusedCard.groupId);\n    final iterable = groupController?.items\n        .skipWhile((item) => item.id != lastFocusedCard.rowId);\n\n    // 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\n    if (iterable == null || iterable.isEmpty) {\n      _focusFirstCard();\n      notifyListeners();\n      return true;\n    }\n\n    if (iterable.length == 1) {\n      // focus on the first card in the next group\n      final group = boardController.groupDatas\n          .skipWhile((item) => item.id != lastFocusedCard.groupId)\n          .skip(1)\n          .firstWhereOrNull((groupData) => groupData.items.isNotEmpty);\n      if (group != null) {\n        _focusedCards\n          ..clear()\n          ..add(\n            GroupedRowId(\n              rowId: group.items.first.id,\n              groupId: group.id,\n            ),\n          );\n      }\n    } else {\n      // focus on the next card in the same group\n      _focusedCards\n        ..clear()\n        ..add(\n          GroupedRowId(\n            rowId: iterable.elementAt(1).id,\n            groupId: lastFocusedCard.groupId,\n          ),\n        );\n    }\n\n    notifyListeners();\n\n    return true;\n  }\n\n  bool focusPrevious() {\n    _deepCopy();\n\n    // if no card is focused, focus on the last card in the board\n    if (_focusedCards.isEmpty) {\n      _focusLastCard();\n      notifyListeners();\n      return true;\n    }\n\n    final lastFocusedCard = _focusedCards.last;\n    final groupController = boardController.controller(lastFocusedCard.groupId);\n    final iterable = groupController?.items.reversed\n        .skipWhile((item) => item.id != lastFocusedCard.rowId);\n\n    // 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\n    if (iterable == null || iterable.isEmpty) {\n      _focusLastCard();\n      notifyListeners();\n      return true;\n    }\n\n    if (iterable.length == 1) {\n      // focus on the last card in the previous group\n      final group = boardController.groupDatas.reversed\n          .skipWhile((item) => item.id != lastFocusedCard.groupId)\n          .skip(1)\n          .firstWhereOrNull((groupData) => groupData.items.isNotEmpty);\n      if (group != null) {\n        _focusedCards\n          ..clear()\n          ..add(\n            GroupedRowId(\n              rowId: group.items.last.id,\n              groupId: group.id,\n            ),\n          );\n      }\n    } else {\n      // focus on the next card in the same group\n      _focusedCards\n        ..clear()\n        ..add(\n          GroupedRowId(\n            rowId: iterable.elementAt(1).id,\n            groupId: lastFocusedCard.groupId,\n          ),\n        );\n    }\n\n    notifyListeners();\n\n    return true;\n  }\n\n  bool adjustRangeDown() {\n    _deepCopy();\n\n    // if no card is focused, focus on the first card in the board\n    if (_focusedCards.isEmpty) {\n      _focusFirstCard();\n      notifyListeners();\n      return true;\n    }\n\n    final firstFocusedCard = _focusedCards.first;\n    final lastFocusedCard = _focusedCards.last;\n\n    // determine whether to shrink or expand the selection\n    bool isExpand = false;\n    if (_focusedCards.length == 1) {\n      isExpand = true;\n    } else {\n      final firstGroupIndex = boardController.groupDatas\n          .indexWhere((element) => element.id == firstFocusedCard.groupId);\n      final lastGroupIndex = boardController.groupDatas\n          .indexWhere((element) => element.id == lastFocusedCard.groupId);\n\n      if (firstGroupIndex == -1 || lastGroupIndex == -1) {\n        _focusFirstCard();\n        notifyListeners();\n        return true;\n      }\n\n      if (firstGroupIndex < lastGroupIndex) {\n        isExpand = true;\n      } else if (firstGroupIndex > lastGroupIndex) {\n        isExpand = false;\n      } else {\n        final groupItems =\n            boardController.groupDatas.elementAt(firstGroupIndex).items;\n        final firstCardIndex =\n            groupItems.indexWhere((item) => item.id == firstFocusedCard.rowId);\n        final lastCardIndex =\n            groupItems.indexWhere((item) => item.id == lastFocusedCard.rowId);\n\n        if (firstCardIndex == -1 || lastCardIndex == -1) {\n          _focusFirstCard();\n          notifyListeners();\n          return true;\n        }\n\n        isExpand = firstCardIndex < lastCardIndex;\n      }\n    }\n\n    if (isExpand) {\n      final groupController =\n          boardController.controller(lastFocusedCard.groupId);\n\n      if (groupController == null) {\n        _focusFirstCard();\n        notifyListeners();\n        return true;\n      }\n\n      final iterable = groupController.items\n          .skipWhile((item) => item.id != lastFocusedCard.rowId);\n\n      if (iterable.length == 1) {\n        // focus on the first card in the next group\n        final group = boardController.groupDatas\n            .skipWhile((item) => item.id != lastFocusedCard.groupId)\n            .skip(1)\n            .firstWhereOrNull((groupData) => groupData.items.isNotEmpty);\n        if (group != null) {\n          _focusedCards.add(\n            GroupedRowId(\n              rowId: group.items.first.id,\n              groupId: group.id,\n            ),\n          );\n        }\n      } else {\n        _focusedCards.add(\n          GroupedRowId(\n            rowId: iterable.elementAt(1).id,\n            groupId: lastFocusedCard.groupId,\n          ),\n        );\n      }\n    } else {\n      _focusedCards.removeLast();\n    }\n\n    notifyListeners();\n    return true;\n  }\n\n  bool adjustRangeUp() {\n    _deepCopy();\n\n    // if no card is focused, focus on the first card in the board\n    if (_focusedCards.isEmpty) {\n      _focusLastCard();\n      notifyListeners();\n      return true;\n    }\n\n    final firstFocusedCard = _focusedCards.first;\n    final lastFocusedCard = _focusedCards.last;\n\n    // determine whether to shrink or expand the selection\n    bool isExpand = false;\n    if (_focusedCards.length == 1) {\n      isExpand = true;\n    } else {\n      final firstGroupIndex = boardController.groupDatas\n          .indexWhere((element) => element.id == firstFocusedCard.groupId);\n      final lastGroupIndex = boardController.groupDatas\n          .indexWhere((element) => element.id == lastFocusedCard.groupId);\n\n      if (firstGroupIndex == -1 || lastGroupIndex == -1) {\n        _focusLastCard();\n        notifyListeners();\n        return true;\n      }\n\n      if (firstGroupIndex < lastGroupIndex) {\n        isExpand = false;\n      } else if (firstGroupIndex > lastGroupIndex) {\n        isExpand = true;\n      } else {\n        final groupItems =\n            boardController.groupDatas.elementAt(firstGroupIndex).items;\n        final firstCardIndex =\n            groupItems.indexWhere((item) => item.id == firstFocusedCard.rowId);\n        final lastCardIndex =\n            groupItems.indexWhere((item) => item.id == lastFocusedCard.rowId);\n\n        if (firstCardIndex == -1 || lastCardIndex == -1) {\n          _focusLastCard();\n          notifyListeners();\n          return true;\n        }\n\n        isExpand = firstCardIndex > lastCardIndex;\n      }\n    }\n\n    if (isExpand) {\n      final groupController =\n          boardController.controller(lastFocusedCard.groupId);\n\n      if (groupController == null) {\n        _focusLastCard();\n        notifyListeners();\n        return true;\n      }\n\n      final iterable = groupController.items.reversed\n          .skipWhile((item) => item.id != lastFocusedCard.rowId);\n\n      if (iterable.length == 1) {\n        // focus on the last card in the previous group\n        final group = boardController.groupDatas.reversed\n            .skipWhile((item) => item.id != lastFocusedCard.groupId)\n            .skip(1)\n            .firstWhereOrNull((groupData) => groupData.items.isNotEmpty);\n        if (group != null) {\n          _focusedCards.add(\n            GroupedRowId(\n              rowId: group.items.last.id,\n              groupId: group.id,\n            ),\n          );\n        }\n      } else {\n        _focusedCards.add(\n          GroupedRowId(\n            rowId: iterable.elementAt(1).id,\n            groupId: lastFocusedCard.groupId,\n          ),\n        );\n      }\n    } else {\n      _focusedCards.removeLast();\n    }\n\n    notifyListeners();\n\n    return true;\n  }\n\n  bool clear() {\n    _deepCopy();\n    _focusedCards.clear();\n    notifyListeners();\n    return true;\n  }\n\n  void _focusFirstCard() {\n    _focusedCards.clear();\n    final firstGroup = boardController.groupDatas\n        .firstWhereOrNull((group) => group.items.isNotEmpty);\n    final firstCard = firstGroup?.items.firstOrNull;\n    if (firstCard != null) {\n      _focusedCards\n          .add(GroupedRowId(rowId: firstCard.id, groupId: firstGroup!.id));\n    }\n  }\n\n  void _focusLastCard() {\n    _focusedCards.clear();\n    final lastGroup = boardController.groupDatas\n        .lastWhereOrNull((group) => group.items.isNotEmpty);\n    final lastCard = lastGroup?.items.lastOrNull;\n    if (lastCard != null) {\n      _focusedCards\n          .add(GroupedRowId(rowId: lastCard.id, groupId: lastGroup!.id));\n    }\n  }\n\n  void _deepCopy() {\n    _focusedCards = [..._focusedCards];\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_cache.dart';\nimport 'package:appflowy/plugins/database/application/row/row_controller.dart';\nimport 'package:appflowy/plugins/database/board/application/board_bloc.dart';\nimport 'package:appflowy/plugins/database/board/group_ext.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_detail.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass HiddenGroupsColumn extends StatelessWidget {\n  const HiddenGroupsColumn({\n    super.key,\n    required this.margin,\n    required this.shrinkWrap,\n  });\n\n  final EdgeInsets margin;\n  final bool shrinkWrap;\n\n  @override\n  Widget build(BuildContext context) {\n    final databaseController = context.read<BoardBloc>().databaseController;\n    return BlocSelector<BoardBloc, BoardState, BoardLayoutSettingPB?>(\n      selector: (state) => state.maybeMap(\n        orElse: () => null,\n        ready: (value) => value.layoutSettings,\n      ),\n      builder: (context, layoutSettings) {\n        if (layoutSettings == null) {\n          return const SizedBox.shrink();\n        }\n        final isCollapsed = layoutSettings.collapseHiddenGroups;\n        final leftPadding = margin.left +\n            context.read<DatabasePluginWidgetBuilderSize>().paddingLeft;\n        final leftPaddingWithMaxDocWidth = context\n            .read<DatabasePluginWidgetBuilderSize>()\n            .paddingLeftWithMaxDocumentWidth;\n        return AnimatedSize(\n          alignment: AlignmentDirectional.topStart,\n          curve: Curves.easeOut,\n          duration: const Duration(milliseconds: 150),\n          child: isCollapsed\n              ? SizedBox(\n                  height: 50,\n                  child: Padding(\n                    padding: EdgeInsets.only(\n                      left: 80 + leftPaddingWithMaxDocWidth,\n                      right: 8,\n                    ),\n                    child: Center(\n                      child: _collapseExpandIcon(context, isCollapsed),\n                    ),\n                  ),\n                )\n              : Container(\n                  width: 274 + leftPaddingWithMaxDocWidth,\n                  padding: EdgeInsets.only(\n                    left: leftPadding,\n                    right: margin.right + 4,\n                  ),\n                  child: Column(\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      SizedBox(\n                        height: 50,\n                        child: Row(\n                          children: [\n                            Expanded(\n                              child: FlowyText.medium(\n                                LocaleKeys.board_hiddenGroupSection_sectionTitle\n                                    .tr(),\n                                overflow: TextOverflow.ellipsis,\n                                color: Theme.of(context).hintColor,\n                              ),\n                            ),\n                            _collapseExpandIcon(context, isCollapsed),\n                          ],\n                        ),\n                      ),\n                      _hiddenGroupList(databaseController),\n                    ],\n                  ),\n                ),\n        );\n      },\n    );\n  }\n\n  Widget _hiddenGroupList(DatabaseController databaseController) {\n    final hiddenGroupList = HiddenGroupList(\n      shrinkWrap: shrinkWrap,\n      databaseController: databaseController,\n    );\n    return shrinkWrap ? hiddenGroupList : Expanded(child: hiddenGroupList);\n  }\n\n  Widget _collapseExpandIcon(BuildContext context, bool isCollapsed) {\n    return FlowyTooltip(\n      message: isCollapsed\n          ? LocaleKeys.board_hiddenGroupSection_expandTooltip.tr()\n          : LocaleKeys.board_hiddenGroupSection_collapseTooltip.tr(),\n      preferBelow: false,\n      child: FlowyIconButton(\n        width: 20,\n        height: 20,\n        iconColorOnHover: Theme.of(context).colorScheme.onSurface,\n        onPressed: () => context\n            .read<BoardBloc>()\n            .add(BoardEvent.toggleHiddenSectionVisibility(!isCollapsed)),\n        icon: FlowySvg(\n          isCollapsed\n              ? FlowySvgs.hamburger_s_s\n              : FlowySvgs.pull_left_outlined_s,\n        ),\n      ),\n    );\n  }\n}\n\nclass HiddenGroupList extends StatelessWidget {\n  const HiddenGroupList({\n    super.key,\n    required this.databaseController,\n    required this.shrinkWrap,\n  });\n\n  final DatabaseController databaseController;\n  final bool shrinkWrap;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<BoardBloc, BoardState>(\n      builder: (context, state) {\n        return state.maybeMap(\n          orElse: () => const SizedBox.shrink(),\n          ready: (state) => ReorderableListView.builder(\n            proxyDecorator: (child, index, animation) => Material(\n              color: Colors.transparent,\n              child: Stack(\n                children: [\n                  child,\n                  MouseRegion(\n                    cursor: Platform.isWindows\n                        ? SystemMouseCursors.click\n                        : SystemMouseCursors.grabbing,\n                    child: const SizedBox.expand(),\n                  ),\n                ],\n              ),\n            ),\n            shrinkWrap: shrinkWrap,\n            buildDefaultDragHandles: false,\n            itemCount: state.hiddenGroups.length,\n            itemBuilder: (_, index) => Padding(\n              padding: const EdgeInsets.only(bottom: 4),\n              key: ValueKey(\"hiddenGroup${state.hiddenGroups[index].groupId}\"),\n              child: HiddenGroupCard(\n                group: state.hiddenGroups[index],\n                index: index,\n                bloc: context.read<BoardBloc>(),\n              ),\n            ),\n            onReorder: (oldIndex, newIndex) {\n              if (oldIndex < newIndex) {\n                newIndex--;\n              }\n              final fromGroupId = state.hiddenGroups[oldIndex].groupId;\n              final toGroupId = state.hiddenGroups[newIndex].groupId;\n              context\n                  .read<BoardBloc>()\n                  .add(BoardEvent.reorderGroup(fromGroupId, toGroupId));\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass HiddenGroupCard extends StatefulWidget {\n  const HiddenGroupCard({\n    super.key,\n    required this.group,\n    required this.index,\n    required this.bloc,\n  });\n\n  final GroupPB group;\n  final BoardBloc bloc;\n  final int index;\n\n  @override\n  State<HiddenGroupCard> createState() => _HiddenGroupCardState();\n}\n\nclass _HiddenGroupCardState extends State<HiddenGroupCard> {\n  final PopoverController _popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    final databaseController = widget.bloc.databaseController;\n    final primaryField = databaseController.fieldController.fieldInfos\n        .firstWhereOrNull((element) => element.isPrimary)!;\n    return AppFlowyPopover(\n      controller: _popoverController,\n      direction: PopoverDirection.bottomWithCenterAligned,\n      triggerActions: PopoverTriggerFlags.none,\n      constraints: const BoxConstraints(maxWidth: 234, maxHeight: 300),\n      popupBuilder: (popoverContext) {\n        return BlocProvider.value(\n          value: context.read<BoardBloc>(),\n          child: HiddenGroupPopupItemList(\n            viewId: databaseController.viewId,\n            groupId: widget.group.groupId,\n            primaryFieldId: primaryField.id,\n            rowCache: databaseController.rowCache,\n          ),\n        );\n      },\n      child: HiddenGroupButtonContent(\n        popoverController: _popoverController,\n        groupId: widget.group.groupId,\n        index: widget.index,\n        bloc: widget.bloc,\n      ),\n    );\n  }\n}\n\nclass HiddenGroupButtonContent extends StatelessWidget {\n  const HiddenGroupButtonContent({\n    super.key,\n    required this.popoverController,\n    required this.groupId,\n    required this.index,\n    required this.bloc,\n  });\n\n  final PopoverController popoverController;\n  final String groupId;\n  final int index;\n  final BoardBloc bloc;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.only(right: 8.0),\n      child: GestureDetector(\n        behavior: HitTestBehavior.opaque,\n        onTap: popoverController.show,\n        child: FlowyHover(\n          builder: (context, isHovering) {\n            return BlocProvider<BoardBloc>.value(\n              value: bloc,\n              child: BlocBuilder<BoardBloc, BoardState>(\n                builder: (context, state) {\n                  return state.maybeMap(\n                    orElse: () => const SizedBox.shrink(),\n                    ready: (state) {\n                      final group = state.hiddenGroups.firstWhereOrNull(\n                        (g) => g.groupId == groupId,\n                      );\n                      if (group == null) {\n                        return const SizedBox.shrink();\n                      }\n\n                      return SizedBox(\n                        height: 32,\n                        child: Padding(\n                          padding: const EdgeInsets.symmetric(\n                            horizontal: 4,\n                            vertical: 3,\n                          ),\n                          child: Row(\n                            children: [\n                              HiddenGroupCardActions(\n                                isVisible: isHovering,\n                                index: index,\n                              ),\n                              const HSpace(4),\n                              Expanded(\n                                child: Row(\n                                  children: [\n                                    Flexible(\n                                      child: FlowyText(\n                                        group.generateGroupName(\n                                          bloc.databaseController,\n                                        ),\n                                        overflow: TextOverflow.ellipsis,\n                                      ),\n                                    ),\n                                    const HSpace(6),\n                                    FlowyText(\n                                      group.rows.length.toString(),\n                                      overflow: TextOverflow.ellipsis,\n                                      color: Theme.of(context).hintColor,\n                                    ),\n                                  ],\n                                ),\n                              ),\n                              if (isHovering) ...[\n                                const HSpace(6),\n                                FlowyIconButton(\n                                  width: 20,\n                                  icon: const FlowySvg(\n                                    FlowySvgs.show_m,\n                                    size: Size.square(16),\n                                  ),\n                                  onPressed: () =>\n                                      context.read<BoardBloc>().add(\n                                            BoardEvent.setGroupVisibility(\n                                              group,\n                                              true,\n                                            ),\n                                          ),\n                                ),\n                              ],\n                            ],\n                          ),\n                        ),\n                      );\n                    },\n                  );\n                },\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass HiddenGroupCardActions extends StatelessWidget {\n  const HiddenGroupCardActions({\n    super.key,\n    required this.isVisible,\n    required this.index,\n  });\n\n  final bool isVisible;\n  final int index;\n\n  @override\n  Widget build(BuildContext context) {\n    return ReorderableDragStartListener(\n      index: index,\n      enabled: isVisible,\n      child: MouseRegion(\n        cursor: SystemMouseCursors.grab,\n        child: SizedBox(\n          height: 14,\n          width: 14,\n          child: isVisible\n              ? FlowySvg(\n                  FlowySvgs.drag_element_s,\n                  color: Theme.of(context).hintColor,\n                )\n              : const SizedBox.shrink(),\n        ),\n      ),\n    );\n  }\n}\n\nclass HiddenGroupPopupItemList extends StatelessWidget {\n  const HiddenGroupPopupItemList({\n    super.key,\n    required this.groupId,\n    required this.viewId,\n    required this.primaryFieldId,\n    required this.rowCache,\n  });\n\n  final String groupId;\n  final String viewId;\n  final String primaryFieldId;\n  final RowCache rowCache;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<BoardBloc, BoardState>(\n      builder: (context, state) {\n        return state.maybeMap(\n          orElse: () => const SizedBox.shrink(),\n          ready: (state) {\n            final group = state.hiddenGroups.firstWhereOrNull(\n              (g) => g.groupId == groupId,\n            );\n            if (group == null) {\n              return const SizedBox.shrink();\n            }\n            final bloc = context.read<BoardBloc>();\n            final cells = <Widget>[\n              Padding(\n                padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),\n                child: FlowyText(\n                  group.generateGroupName(bloc.databaseController),\n                  fontSize: 10,\n                  color: Theme.of(context).hintColor,\n                  overflow: TextOverflow.ellipsis,\n                ),\n              ),\n              ...group.rows.map(\n                (item) {\n                  final rowController = RowController(\n                    rowMeta: item,\n                    viewId: viewId,\n                    rowCache: rowCache,\n                  );\n                  rowController.initialize();\n\n                  final databaseController =\n                      context.read<BoardBloc>().databaseController;\n\n                  return HiddenGroupPopupItem(\n                    cellContext: rowCache.loadCells(item).firstWhere(\n                          (cellContext) =>\n                              cellContext.fieldId == primaryFieldId,\n                        ),\n                    rowController: rowController,\n                    rowMeta: item,\n                    cellBuilder: CardCellBuilder(\n                      databaseController: databaseController,\n                    ),\n                    onPressed: () {\n                      FlowyOverlay.show(\n                        context: context,\n                        builder: (_) => BlocProvider.value(\n                          value: context.read<UserWorkspaceBloc>(),\n                          child: RowDetailPage(\n                            databaseController: databaseController,\n                            rowController: rowController,\n                            userProfile: context.read<BoardBloc>().userProfile,\n                          ),\n                        ),\n                      );\n                      PopoverContainer.of(context).close();\n                    },\n                  );\n                },\n              ),\n            ];\n\n            return ListView.separated(\n              itemBuilder: (context, index) => cells[index],\n              itemCount: cells.length,\n              separatorBuilder: (context, index) =>\n                  VSpace(GridSize.typeOptionSeparatorHeight),\n              shrinkWrap: true,\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass HiddenGroupPopupItem extends StatelessWidget {\n  const HiddenGroupPopupItem({\n    super.key,\n    required this.rowMeta,\n    required this.cellContext,\n    required this.onPressed,\n    required this.cellBuilder,\n    required this.rowController,\n  });\n\n  final RowMetaPB rowMeta;\n  final CellContext cellContext;\n  final RowController rowController;\n  final CardCellBuilder cellBuilder;\n  final VoidCallback onPressed;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 26,\n      child: FlowyButton(\n        margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),\n        text: cellBuilder.build(\n          cellContext: cellContext,\n          styleMap: {FieldType.RichText: _titleCellStyle(context)},\n          hasNotes: false,\n        ),\n        onTap: onPressed,\n      ),\n    );\n  }\n\n  TextCardCellStyle _titleCellStyle(BuildContext context) {\n    return TextCardCellStyle(\n      padding: EdgeInsets.zero,\n      textStyle: Theme.of(context).textTheme.bodyMedium!,\n      titleTextStyle: Theme.of(context)\n          .textTheme\n          .bodyMedium!\n          .copyWith(fontSize: 11, overflow: TextOverflow.ellipsis),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_shortcut_container.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/board/application/board_actions_bloc.dart';\nimport 'package:appflowy/plugins/database/board/application/board_bloc.dart';\nimport 'package:appflowy/plugins/shared/callback_shortcuts.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'board_focus_scope.dart';\n\nclass BoardShortcutContainer extends StatelessWidget {\n  const BoardShortcutContainer({\n    super.key,\n    required this.focusScope,\n    required this.child,\n  });\n\n  final BoardFocusScope focusScope;\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return AFCallbackShortcuts(\n      bindings: _shortcutBindings(context),\n      child: FocusScope(\n        child: Focus(\n          child: Builder(\n            builder: (context) {\n              return GestureDetector(\n                behavior: HitTestBehavior.opaque,\n                onTap: () {\n                  final focusNode = Focus.of(context);\n                  focusNode.requestFocus();\n                  focusScope.clear();\n                },\n                child: child,\n              );\n            },\n          ),\n        ),\n      ),\n    );\n  }\n\n  Map<ShortcutActivator, AFBindingCallback> _shortcutBindings(\n    BuildContext context,\n  ) {\n    return {\n      const SingleActivator(LogicalKeyboardKey.arrowUp):\n          focusScope.focusPrevious,\n      const SingleActivator(LogicalKeyboardKey.arrowDown): focusScope.focusNext,\n      const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true):\n          focusScope.adjustRangeUp,\n      const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true):\n          focusScope.adjustRangeDown,\n      const SingleActivator(LogicalKeyboardKey.escape): focusScope.clear,\n      const SingleActivator(LogicalKeyboardKey.delete): () =>\n          _removeHandler(context),\n      const SingleActivator(LogicalKeyboardKey.backspace): () =>\n          _removeHandler(context),\n      SingleActivator(\n        LogicalKeyboardKey.arrowUp,\n        shift: true,\n        meta: Platform.isMacOS,\n        control: !Platform.isMacOS,\n      ): () => _shiftCmdUpHandler(context),\n      const SingleActivator(LogicalKeyboardKey.enter): () =>\n          _enterHandler(context),\n      const SingleActivator(LogicalKeyboardKey.numpadEnter): () =>\n          _enterHandler(context),\n      const SingleActivator(LogicalKeyboardKey.enter, shift: true): () =>\n          _shiftEnterHandler(context),\n      const SingleActivator(LogicalKeyboardKey.comma): () =>\n          _moveGroupToAdjacentGroup(context, true),\n      const SingleActivator(LogicalKeyboardKey.period): () =>\n          _moveGroupToAdjacentGroup(context, false),\n      const SingleActivator(LogicalKeyboardKey.keyE): () =>\n          _keyEHandler(context),\n      const SingleActivator(LogicalKeyboardKey.keyN): () =>\n          _keyNHandler(context),\n    };\n  }\n\n  bool _keyEHandler(BuildContext context) {\n    if (focusScope.value.length != 1) {\n      return false;\n    }\n    context.read<BoardActionsCubit>().startEditingRow(focusScope.value.first);\n    return true;\n  }\n\n  bool _keyNHandler(BuildContext context) {\n    if (focusScope.value.length != 1) {\n      return false;\n    }\n    context\n        .read<BoardActionsCubit>()\n        .startCreateBottomRow(focusScope.value.first.groupId);\n    focusScope.clear();\n    return true;\n  }\n\n  bool _enterHandler(BuildContext context) {\n    if (focusScope.value.length != 1) {\n      return false;\n    }\n    context\n        .read<BoardActionsCubit>()\n        .openCardWithRowId(focusScope.value.first.rowId);\n    return true;\n  }\n\n  bool _shiftEnterHandler(BuildContext context) {\n    if (focusScope.value.isEmpty) {\n      context\n          .read<BoardActionsCubit>()\n          .createRow(null, CreateBoardCardRelativePosition.after);\n    } else if (focusScope.value.length == 1) {\n      context.read<BoardActionsCubit>().createRow(\n            focusScope.value.first,\n            CreateBoardCardRelativePosition.after,\n          );\n    } else {\n      return false;\n    }\n    return true;\n  }\n\n  bool _shiftCmdUpHandler(BuildContext context) {\n    if (focusScope.value.isEmpty) {\n      context\n          .read<BoardActionsCubit>()\n          .createRow(null, CreateBoardCardRelativePosition.before);\n    } else if (focusScope.value.length == 1) {\n      context.read<BoardActionsCubit>().createRow(\n            focusScope.value.first,\n            CreateBoardCardRelativePosition.before,\n          );\n    } else {\n      return false;\n    }\n    return true;\n  }\n\n  bool _removeHandler(BuildContext context) {\n    if (focusScope.value.length != 1) {\n      return false;\n    }\n\n    NavigatorOkCancelDialog(\n      message: LocaleKeys.grid_row_deleteCardPrompt.tr(),\n      onOkPressed: () {\n        context.read<BoardBloc>().add(BoardEvent.deleteCards(focusScope.value));\n      },\n    ).show(context);\n\n    return true;\n  }\n\n  bool _moveGroupToAdjacentGroup(BuildContext context, bool toPrevious) {\n    if (focusScope.value.length != 1) {\n      return false;\n    }\n    context.read<BoardBloc>().add(\n          BoardEvent.moveGroupToAdjacentGroup(\n            focusScope.value.first,\n            toPrevious,\n          ),\n        );\n    focusScope.clear();\n    return true;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/board/tests/integrate_test/card_test.dart",
    "content": "// import 'package:flutter_test/flutter_test.dart';\n// import 'package:integration_test/integration_test.dart';\n// import 'package:appflowy/main.dart' as app;\n\n// void main() {\n//   IntegrationTestWidgetsFlutterBinding.ensureInitialized();\n\n//   group('end-to-end test', () {\n//     testWidgets('tap on the floating action button, verify counter',\n//         (tester) async {\n//       app.main();\n\n//       await tester.pumpAndSettle();\n//     });\n//   });\n// }\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/cell_cache.dart';\nimport 'package:appflowy/plugins/database/application/defines.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:calendar_view/calendar_view.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport '../../application/database_controller.dart';\nimport '../../application/row/row_cache.dart';\n\npart 'calendar_bloc.freezed.dart';\n\nclass CalendarBloc extends Bloc<CalendarEvent, CalendarState> {\n  CalendarBloc({required this.databaseController})\n      : super(CalendarState.initial()) {\n    _dispatch();\n  }\n\n  final DatabaseController databaseController;\n  Map<String, FieldInfo> fieldInfoByFieldId = {};\n\n  // Getters\n  String get viewId => databaseController.viewId;\n  FieldController get fieldController => databaseController.fieldController;\n  CellMemCache get cellCache => databaseController.rowCache.cellCache;\n  RowCache get rowCache => databaseController.rowCache;\n\n  UserProfilePB? _userProfile;\n  UserProfilePB? get userProfile => _userProfile;\n\n  DatabaseCallbacks? _databaseCallbacks;\n  DatabaseLayoutSettingCallbacks? _layoutSettingCallbacks;\n\n  @override\n  Future<void> close() async {\n    databaseController.removeListener(\n      onDatabaseChanged: _databaseCallbacks,\n      onLayoutSettingsChanged: _layoutSettingCallbacks,\n    );\n    _databaseCallbacks = null;\n    _layoutSettingCallbacks = null;\n    await super.close();\n  }\n\n  void _dispatch() {\n    on<CalendarEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            final result = await UserEventGetUserProfile().send();\n            result.fold(\n              (profile) => _userProfile = profile,\n              (err) => Log.error('Failed to get user profile: $err'),\n            );\n\n            _startListening();\n            await _openDatabase(emit);\n            _loadAllEvents();\n          },\n          didReceiveCalendarSettings: (CalendarLayoutSettingPB settings) {\n            // If the field id changed, reload all events\n            if (state.settings?.fieldId != settings.fieldId) {\n              _loadAllEvents();\n            }\n            emit(state.copyWith(settings: settings));\n          },\n          didReceiveDatabaseUpdate: (DatabasePB database) {\n            emit(state.copyWith(database: database));\n          },\n          didLoadAllEvents: (events) {\n            final calenderEvents = _calendarEventDataFromEventPBs(events);\n            emit(\n              state.copyWith(\n                initialEvents: calenderEvents,\n                allEvents: calenderEvents,\n              ),\n            );\n          },\n          createEvent: (DateTime date) async {\n            await _createEvent(date);\n          },\n          duplicateEvent: (String viewId, String rowId) async {\n            final result = await RowBackendService.duplicateRow(viewId, rowId);\n            result.fold(\n              (_) => null,\n              (e) => Log.error('Failed to duplicate event: $e', e),\n            );\n          },\n          deleteEvent: (String viewId, String rowId) async {\n            final result = await RowBackendService.deleteRows(viewId, [rowId]);\n            result.fold(\n              (_) => null,\n              (e) => Log.error('Failed to delete event: $e', e),\n            );\n          },\n          newEventPopupDisplayed: () {\n            emit(state.copyWith(editingEvent: null));\n          },\n          moveEvent: (CalendarDayEvent event, DateTime date) async {\n            await _moveEvent(event, date);\n          },\n          didCreateEvent: (CalendarEventData<CalendarDayEvent> event) {\n            emit(state.copyWith(editingEvent: event));\n          },\n          updateCalendarLayoutSetting:\n              (CalendarLayoutSettingPB layoutSetting) async {\n            await _updateCalendarLayoutSetting(layoutSetting);\n          },\n          didUpdateEvent: (CalendarEventData<CalendarDayEvent> eventData) {\n            final allEvents = [...state.allEvents];\n            final index = allEvents.indexWhere(\n              (element) => element.event!.eventId == eventData.event!.eventId,\n            );\n            if (index != -1) {\n              allEvents[index] = eventData;\n            }\n            emit(state.copyWith(allEvents: allEvents, updateEvent: eventData));\n          },\n          didDeleteEvents: (List<RowId> deletedRowIds) {\n            final events = [...state.allEvents];\n            events.retainWhere(\n              (element) => !deletedRowIds.contains(element.event!.eventId),\n            );\n            emit(\n              state.copyWith(\n                allEvents: events,\n                deleteEventIds: deletedRowIds,\n              ),\n            );\n            emit(state.copyWith(deleteEventIds: const []));\n          },\n          didReceiveEvent: (CalendarEventData<CalendarDayEvent> event) {\n            emit(\n              state.copyWith(\n                allEvents: [...state.allEvents, event],\n                newEvent: event,\n              ),\n            );\n            emit(state.copyWith(newEvent: null));\n          },\n          openRowDetail: (row) {\n            emit(state.copyWith(openRow: row));\n            emit(state.copyWith(openRow: null));\n          },\n        );\n      },\n    );\n  }\n\n  FieldInfo? _getCalendarFieldInfo(String fieldId) {\n    final fieldInfos = databaseController.fieldController.fieldInfos;\n    final index = fieldInfos.indexWhere(\n      (element) => element.field.id == fieldId,\n    );\n    if (index != -1) {\n      return fieldInfos[index];\n    } else {\n      return null;\n    }\n  }\n\n  Future<void> _openDatabase(Emitter<CalendarState> emit) async {\n    final result = await databaseController.open();\n    result.fold(\n      (database) {\n        databaseController.setIsLoading(false);\n        emit(\n          state.copyWith(\n            loadingState: LoadingState.finish(FlowyResult.success(null)),\n          ),\n        );\n      },\n      (err) => emit(\n        state.copyWith(\n          loadingState: LoadingState.finish(FlowyResult.failure(err)),\n        ),\n      ),\n    );\n  }\n\n  Future<void> _createEvent(DateTime date) async {\n    final settings = state.settings;\n    if (settings == null) {\n      Log.warn('Calendar settings not found');\n      return;\n    }\n    final dateField = _getCalendarFieldInfo(settings.fieldId);\n    if (dateField != null) {\n      final newRow = await RowBackendService.createRow(\n        viewId: viewId,\n        withCells: (builder) => builder.insertDate(dateField, date),\n      ).then(\n        (result) => result.fold(\n          (newRow) => newRow,\n          (err) {\n            Log.error(err);\n            return null;\n          },\n        ),\n      );\n\n      if (newRow != null) {\n        final event = await _loadEvent(newRow.id);\n        if (event != null && !isClosed) {\n          add(CalendarEvent.didCreateEvent(event));\n        }\n      }\n    }\n  }\n\n  Future<void> _moveEvent(CalendarDayEvent event, DateTime date) async {\n    final timestamp = _eventTimestamp(event, date);\n    final payload = MoveCalendarEventPB(\n      cellPath: CellIdPB(\n        viewId: viewId,\n        rowId: event.eventId,\n        fieldId: event.dateFieldId,\n      ),\n      timestamp: timestamp,\n    );\n    return DatabaseEventMoveCalendarEvent(payload).send().then((result) {\n      return result.fold(\n        (_) async {\n          final modifiedEvent = await _loadEvent(event.eventId);\n          add(CalendarEvent.didUpdateEvent(modifiedEvent!));\n        },\n        (err) {\n          Log.error(err);\n          return null;\n        },\n      );\n    });\n  }\n\n  Future<void> _updateCalendarLayoutSetting(\n    CalendarLayoutSettingPB layoutSetting,\n  ) async {\n    return databaseController.updateLayoutSetting(\n      calendarLayoutSetting: layoutSetting,\n    );\n  }\n\n  Future<CalendarEventData<CalendarDayEvent>?> _loadEvent(RowId rowId) async {\n    final payload = DatabaseViewRowIdPB(viewId: viewId, rowId: rowId);\n    return DatabaseEventGetCalendarEvent(payload).send().fold(\n      (eventPB) => _calendarEventDataFromEventPB(eventPB),\n      (r) {\n        Log.error(r);\n        return null;\n      },\n    );\n  }\n\n  void _loadAllEvents() async {\n    final payload = CalendarEventRequestPB.create()..viewId = viewId;\n    final result = await DatabaseEventGetAllCalendarEvents(payload).send();\n    result.fold(\n      (events) {\n        if (!isClosed) {\n          add(CalendarEvent.didLoadAllEvents(events.items));\n        }\n      },\n      (r) => Log.error(r),\n    );\n  }\n\n  List<CalendarEventData<CalendarDayEvent>> _calendarEventDataFromEventPBs(\n    List<CalendarEventPB> eventPBs,\n  ) {\n    final calendarEvents = <CalendarEventData<CalendarDayEvent>>[];\n    for (final eventPB in eventPBs) {\n      final event = _calendarEventDataFromEventPB(eventPB);\n      if (event != null) {\n        calendarEvents.add(event);\n      }\n    }\n    return calendarEvents;\n  }\n\n  CalendarEventData<CalendarDayEvent>? _calendarEventDataFromEventPB(\n    CalendarEventPB eventPB,\n  ) {\n    final fieldInfo = fieldInfoByFieldId[eventPB.dateFieldId];\n    if (fieldInfo == null) {\n      return null;\n    }\n\n    // timestamp is stored as seconds, but constructor requires milliseconds\n    final date = DateTime.fromMillisecondsSinceEpoch(\n      eventPB.timestamp.toInt() * 1000,\n    );\n\n    final eventData = CalendarDayEvent(\n      event: eventPB,\n      eventId: eventPB.rowMeta.id,\n      dateFieldId: eventPB.dateFieldId,\n      date: date,\n    );\n\n    return CalendarEventData(\n      title: eventPB.title,\n      date: date,\n      event: eventData,\n    );\n  }\n\n  void _startListening() {\n    _databaseCallbacks = DatabaseCallbacks(\n      onDatabaseChanged: (database) {\n        if (isClosed) return;\n      },\n      onFieldsChanged: (fieldInfos) {\n        if (isClosed) {\n          return;\n        }\n        fieldInfoByFieldId = {\n          for (final fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo,\n        };\n      },\n      onRowsCreated: (rows) async {\n        if (isClosed) {\n          return;\n        }\n        for (final row in rows) {\n          if (row.isHiddenInView) {\n            add(CalendarEvent.openRowDetail(row.rowMeta));\n          } else {\n            final event = await _loadEvent(row.rowMeta.id);\n            if (event != null) {\n              add(CalendarEvent.didReceiveEvent(event));\n            }\n          }\n        }\n      },\n      onRowsDeleted: (rowIds) {\n        if (isClosed) {\n          return;\n        }\n        add(CalendarEvent.didDeleteEvents(rowIds));\n      },\n      onRowsUpdated: (rowIds, reason) async {\n        if (isClosed) {\n          return;\n        }\n        for (final id in rowIds) {\n          final event = await _loadEvent(id);\n          if (event != null) {\n            if (isEventDayChanged(event)) {\n              add(CalendarEvent.didDeleteEvents([id]));\n              add(CalendarEvent.didReceiveEvent(event));\n            } else {\n              add(CalendarEvent.didUpdateEvent(event));\n            }\n          }\n        }\n      },\n      onNumOfRowsChanged: (rows, rowById, reason) {\n        reason.maybeWhen(\n          updateRowsVisibility: (changeset) async {\n            if (isClosed) {\n              return;\n            }\n            for (final id in changeset.invisibleRows) {\n              if (_containsEvent(id)) {\n                add(CalendarEvent.didDeleteEvents([id]));\n              }\n            }\n            for (final row in changeset.visibleRows) {\n              final id = row.rowMeta.id;\n              if (!_containsEvent(id)) {\n                final event = await _loadEvent(id);\n                if (event != null) {\n                  add(CalendarEvent.didReceiveEvent(event));\n                }\n              }\n            }\n          },\n          orElse: () {},\n        );\n      },\n    );\n\n    _layoutSettingCallbacks = DatabaseLayoutSettingCallbacks(\n      onLayoutSettingsChanged: _didReceiveLayoutSetting,\n    );\n\n    databaseController.addListener(\n      onDatabaseChanged: _databaseCallbacks,\n      onLayoutSettingsChanged: _layoutSettingCallbacks,\n    );\n  }\n\n  void _didReceiveLayoutSetting(DatabaseLayoutSettingPB layoutSetting) {\n    if (layoutSetting.hasCalendar()) {\n      if (isClosed) {\n        return;\n      }\n      add(CalendarEvent.didReceiveCalendarSettings(layoutSetting.calendar));\n    }\n  }\n\n  bool isEventDayChanged(CalendarEventData<CalendarDayEvent> event) {\n    final index = state.allEvents.indexWhere(\n      (element) => element.event!.eventId == event.event!.eventId,\n    );\n    if (index == -1) {\n      return false;\n    }\n    return state.allEvents[index].date.day != event.date.day;\n  }\n\n  bool _containsEvent(String rowId) {\n    return state.allEvents.any((element) => element.event!.eventId == rowId);\n  }\n\n  Int64 _eventTimestamp(CalendarDayEvent event, DateTime date) {\n    final time =\n        event.date.hour * 3600 + event.date.minute * 60 + event.date.second;\n    return Int64(date.millisecondsSinceEpoch ~/ 1000 + time);\n  }\n}\n\ntypedef Events = List<CalendarEventData<CalendarDayEvent>>;\n\n@freezed\nclass CalendarEvent with _$CalendarEvent {\n  const factory CalendarEvent.initial() = _InitialCalendar;\n\n  // Called after loading the calendar layout setting from the backend\n  const factory CalendarEvent.didReceiveCalendarSettings(\n    CalendarLayoutSettingPB settings,\n  ) = _ReceiveCalendarSettings;\n\n  // Called after loading all the current evnets\n  const factory CalendarEvent.didLoadAllEvents(List<CalendarEventPB> events) =\n      _ReceiveCalendarEvents;\n\n  // Called when specific event was updated\n  const factory CalendarEvent.didUpdateEvent(\n    CalendarEventData<CalendarDayEvent> event,\n  ) = _DidUpdateEvent;\n\n  // Called after creating a new event\n  const factory CalendarEvent.didCreateEvent(\n    CalendarEventData<CalendarDayEvent> event,\n  ) = _DidReceiveNewEvent;\n\n  // Called after creating a new event\n  const factory CalendarEvent.newEventPopupDisplayed() =\n      _NewEventPopupDisplayed;\n\n  // Called when receive a new event\n  const factory CalendarEvent.didReceiveEvent(\n    CalendarEventData<CalendarDayEvent> event,\n  ) = _DidReceiveEvent;\n\n  // Called when deleting events\n  const factory CalendarEvent.didDeleteEvents(List<RowId> rowIds) =\n      _DidDeleteEvents;\n\n  // Called when creating a new event\n  const factory CalendarEvent.createEvent(DateTime date) = _CreateEvent;\n\n  // Called when moving an event\n  const factory CalendarEvent.moveEvent(CalendarDayEvent event, DateTime date) =\n      _MoveEvent;\n\n  // Called when updating the calendar's layout settings\n  const factory CalendarEvent.updateCalendarLayoutSetting(\n    CalendarLayoutSettingPB layoutSetting,\n  ) = _UpdateCalendarLayoutSetting;\n\n  const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) =\n      _ReceiveDatabaseUpdate;\n\n  const factory CalendarEvent.duplicateEvent(String viewId, String rowId) =\n      _DuplicateEvent;\n\n  const factory CalendarEvent.deleteEvent(String viewId, String rowId) =\n      _DeleteEvent;\n\n  const factory CalendarEvent.openRowDetail(RowMetaPB row) = _OpenRowDetail;\n}\n\n@freezed\nclass CalendarState with _$CalendarState {\n  const factory CalendarState({\n    required DatabasePB? database,\n    // events by row id\n    required Events allEvents,\n    required Events initialEvents,\n    CalendarEventData<CalendarDayEvent>? editingEvent,\n    CalendarEventData<CalendarDayEvent>? newEvent,\n    CalendarEventData<CalendarDayEvent>? updateEvent,\n    required List<String> deleteEventIds,\n    required CalendarLayoutSettingPB? settings,\n    required RowMetaPB? openRow,\n    required LoadingState loadingState,\n    required FlowyError? noneOrError,\n  }) = _CalendarState;\n\n  factory CalendarState.initial() => const CalendarState(\n        database: null,\n        allEvents: [],\n        initialEvents: [],\n        deleteEventIds: [],\n        settings: null,\n        openRow: null,\n        noneOrError: null,\n        loadingState: LoadingState.loading(),\n      );\n}\n\n@freezed\nclass CalendarDayEvent with _$CalendarDayEvent {\n  const factory CalendarDayEvent({\n    required CalendarEventPB event,\n    required String dateFieldId,\n    required String eventId,\n    required DateTime date,\n  }) = _CalendarDayEvent;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_event_editor_bloc.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'calendar_event_editor_bloc.freezed.dart';\n\nclass CalendarEventEditorBloc\n    extends Bloc<CalendarEventEditorEvent, CalendarEventEditorState> {\n  CalendarEventEditorBloc({\n    required this.fieldController,\n    required this.rowController,\n    required this.layoutSettings,\n  }) : super(CalendarEventEditorState.initial()) {\n    _dispatch();\n  }\n\n  final FieldController fieldController;\n  final RowController rowController;\n  final CalendarLayoutSettingPB layoutSettings;\n\n  void _dispatch() {\n    on<CalendarEventEditorEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () {\n            rowController.initialize();\n\n            _startListening();\n            final primaryFieldId = fieldController.fieldInfos\n                .firstWhere((fieldInfo) => fieldInfo.isPrimary)\n                .id;\n            final cells = rowController\n                .loadCells()\n                .where(\n                  (cellContext) =>\n                      _filterCellContext(cellContext, primaryFieldId),\n                )\n                .toList();\n            add(CalendarEventEditorEvent.didReceiveCellDatas(cells));\n          },\n          didReceiveCellDatas: (cells) {\n            emit(state.copyWith(cells: cells));\n          },\n          delete: () async {\n            final result = await RowBackendService.deleteRows(\n              rowController.viewId,\n              [rowController.rowId],\n            );\n            result.fold((l) => null, (err) => Log.error(err));\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    rowController.addListener(\n      onRowChanged: (cells, reason) {\n        if (isClosed) {\n          return;\n        }\n        final primaryFieldId = fieldController.fieldInfos\n            .firstWhere((fieldInfo) => fieldInfo.isPrimary)\n            .id;\n        final cellData = cells\n            .where(\n              (cellContext) => _filterCellContext(cellContext, primaryFieldId),\n            )\n            .toList();\n        add(CalendarEventEditorEvent.didReceiveCellDatas(cellData));\n      },\n    );\n  }\n\n  bool _filterCellContext(CellContext cellContext, String primaryFieldId) {\n    return fieldController\n            .getField(cellContext.fieldId)!\n            .fieldSettings!\n            .visibility\n            .isVisibleState() ||\n        cellContext.fieldId == layoutSettings.fieldId ||\n        cellContext.fieldId == primaryFieldId;\n  }\n\n  @override\n  Future<void> close() async {\n    await rowController.dispose();\n    return super.close();\n  }\n}\n\n@freezed\nclass CalendarEventEditorEvent with _$CalendarEventEditorEvent {\n  const factory CalendarEventEditorEvent.initial() = _Initial;\n  const factory CalendarEventEditorEvent.didReceiveCellDatas(\n    List<CellContext> cells,\n  ) = _DidReceiveCellDatas;\n  const factory CalendarEventEditorEvent.delete() = _Delete;\n}\n\n@freezed\nclass CalendarEventEditorState with _$CalendarEventEditorState {\n  const factory CalendarEventEditorState({\n    required List<CellContext> cells,\n  }) = _CalendarEventEditorState;\n\n  factory CalendarEventEditorState.initial() =>\n      const CalendarEventEditorState(cells: []);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_setting_bloc.dart",
    "content": "import 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/domain/layout_setting_listener.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:protobuf/protobuf.dart';\n\npart 'calendar_setting_bloc.freezed.dart';\n\nclass CalendarSettingBloc\n    extends Bloc<CalendarSettingEvent, CalendarSettingState> {\n  CalendarSettingBloc({required DatabaseController databaseController})\n      : _databaseController = databaseController,\n        _listener = DatabaseLayoutSettingListener(databaseController.viewId),\n        super(\n          CalendarSettingState.initial(\n            databaseController.databaseLayoutSetting?.calendar,\n          ),\n        ) {\n    _dispatch();\n  }\n\n  final DatabaseController _databaseController;\n  final DatabaseLayoutSettingListener _listener;\n\n  @override\n  Future<void> close() async {\n    await _listener.stop();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<CalendarSettingEvent>((event, emit) {\n      event.when(\n        initial: () {\n          _startListening();\n        },\n        didUpdateLayoutSetting: (CalendarLayoutSettingPB setting) {\n          emit(state.copyWith(layoutSetting: layoutSetting));\n        },\n        updateLayoutSetting: (\n          bool? showWeekends,\n          bool? showWeekNumbers,\n          int? firstDayOfWeek,\n          String? layoutFieldId,\n        ) {\n          _updateLayoutSettings(\n            showWeekends,\n            showWeekNumbers,\n            firstDayOfWeek,\n            layoutFieldId,\n            emit,\n          );\n        },\n      );\n    });\n  }\n\n  void _updateLayoutSettings(\n    bool? showWeekends,\n    bool? showWeekNumbers,\n    int? firstDayOfWeek,\n    String? layoutFieldId,\n    Emitter<CalendarSettingState> emit,\n  ) {\n    final currentSetting = state.layoutSetting;\n    if (currentSetting == null) {\n      return;\n    }\n    currentSetting.freeze();\n    final newSetting = currentSetting.rebuild((setting) {\n      if (showWeekends != null) {\n        setting.showWeekends = !showWeekends;\n      }\n\n      if (showWeekNumbers != null) {\n        setting.showWeekNumbers = !showWeekNumbers;\n      }\n\n      if (firstDayOfWeek != null) {\n        setting.firstDayOfWeek = firstDayOfWeek;\n      }\n\n      if (layoutFieldId != null) {\n        setting.fieldId = layoutFieldId;\n      }\n    });\n\n    _databaseController.updateLayoutSetting(\n      calendarLayoutSetting: newSetting,\n    );\n    emit(state.copyWith(layoutSetting: newSetting));\n  }\n\n  CalendarLayoutSettingPB? get layoutSetting =>\n      _databaseController.databaseLayoutSetting?.calendar;\n\n  void _startListening() {\n    _listener.start(\n      onLayoutChanged: (result) {\n        if (isClosed) {\n          return;\n        }\n\n        result.fold(\n          (setting) => add(\n            CalendarSettingEvent.didUpdateLayoutSetting(setting.calendar),\n          ),\n          (r) => Log.error(r),\n        );\n      },\n    );\n  }\n}\n\n@freezed\nclass CalendarSettingState with _$CalendarSettingState {\n  const factory CalendarSettingState({\n    required CalendarLayoutSettingPB? layoutSetting,\n  }) = _CalendarSettingState;\n\n  factory CalendarSettingState.initial(\n    CalendarLayoutSettingPB? layoutSettings,\n  ) {\n    return CalendarSettingState(layoutSetting: layoutSettings);\n  }\n}\n\n@freezed\nclass CalendarSettingEvent with _$CalendarSettingEvent {\n  const factory CalendarSettingEvent.initial() = _Initial;\n  const factory CalendarSettingEvent.didUpdateLayoutSetting(\n    CalendarLayoutSettingPB setting,\n  ) = _DidUpdateLayoutSetting;\n  const factory CalendarSettingEvent.updateLayoutSetting({\n    bool? showWeekends,\n    bool? showWeekNumbers,\n    int? firstDayOfWeek,\n    String? layoutFieldId,\n  }) = _UpdateLayoutSetting;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/calendar/application/unschedule_event_bloc.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/cell_cache.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport '../../application/database_controller.dart';\nimport '../../application/row/row_cache.dart';\n\npart 'unschedule_event_bloc.freezed.dart';\n\nclass UnscheduleEventsBloc\n    extends Bloc<UnscheduleEventsEvent, UnscheduleEventsState> {\n  UnscheduleEventsBloc({required this.databaseController})\n      : super(UnscheduleEventsState.initial()) {\n    _dispatch();\n  }\n\n  final DatabaseController databaseController;\n  Map<String, FieldInfo> fieldInfoByFieldId = {};\n\n  // Getters\n  String get viewId => databaseController.viewId;\n  FieldController get fieldController => databaseController.fieldController;\n  CellMemCache get cellCache => databaseController.rowCache.cellCache;\n  RowCache get rowCache => databaseController.rowCache;\n\n  DatabaseCallbacks? _databaseCallbacks;\n\n  @override\n  Future<void> close() async {\n    databaseController.removeListener(onDatabaseChanged: _databaseCallbacks);\n    _databaseCallbacks = null;\n    await super.close();\n  }\n\n  void _dispatch() {\n    on<UnscheduleEventsEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            _startListening();\n            _loadAllEvents();\n          },\n          didLoadAllEvents: (events) {\n            emit(\n              state.copyWith(\n                allEvents: events,\n                unscheduleEvents:\n                    events.where((element) => !element.hasTimestamp()).toList(),\n              ),\n            );\n          },\n          didDeleteEvents: (List<RowId> deletedRowIds) {\n            final events = [...state.allEvents];\n            events.retainWhere(\n              (element) => !deletedRowIds.contains(element.rowMeta.id),\n            );\n            emit(\n              state.copyWith(\n                allEvents: events,\n                unscheduleEvents:\n                    events.where((element) => !element.hasTimestamp()).toList(),\n              ),\n            );\n          },\n          didReceiveEvent: (CalendarEventPB event) {\n            final events = [...state.allEvents, event];\n            emit(\n              state.copyWith(\n                allEvents: events,\n                unscheduleEvents:\n                    events.where((element) => !element.hasTimestamp()).toList(),\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  Future<CalendarEventPB?> _loadEvent(\n    RowId rowId,\n  ) async {\n    final payload = DatabaseViewRowIdPB(viewId: viewId, rowId: rowId);\n    return DatabaseEventGetCalendarEvent(payload).send().then(\n          (result) => result.fold(\n            (eventPB) => eventPB,\n            (r) {\n              Log.error(r);\n              return null;\n            },\n          ),\n        );\n  }\n\n  void _loadAllEvents() async {\n    final payload = CalendarEventRequestPB.create()..viewId = viewId;\n    final result = await DatabaseEventGetAllCalendarEvents(payload).send();\n    result.fold(\n      (events) {\n        if (!isClosed) {\n          add(UnscheduleEventsEvent.didLoadAllEvents(events.items));\n        }\n      },\n      (r) => Log.error(r),\n    );\n  }\n\n  void _startListening() {\n    _databaseCallbacks = DatabaseCallbacks(\n      onRowsCreated: (rows) async {\n        if (isClosed) {\n          return;\n        }\n        for (final row in rows) {\n          final event = await _loadEvent(row.rowMeta.id);\n          if (event != null && !isClosed) {\n            add(UnscheduleEventsEvent.didReceiveEvent(event));\n          }\n        }\n      },\n      onRowsDeleted: (rowIds) {\n        if (isClosed) {\n          return;\n        }\n        add(UnscheduleEventsEvent.didDeleteEvents(rowIds));\n      },\n      onRowsUpdated: (rowIds, reason) async {\n        if (isClosed) {\n          return;\n        }\n        for (final id in rowIds) {\n          final event = await _loadEvent(id);\n          if (event != null) {\n            add(UnscheduleEventsEvent.didDeleteEvents([id]));\n            add(UnscheduleEventsEvent.didReceiveEvent(event));\n          }\n        }\n      },\n    );\n\n    databaseController.addListener(onDatabaseChanged: _databaseCallbacks);\n  }\n}\n\n@freezed\nclass UnscheduleEventsEvent with _$UnscheduleEventsEvent {\n  const factory UnscheduleEventsEvent.initial() = _InitialCalendar;\n\n  // Called after loading all the current evnets\n  const factory UnscheduleEventsEvent.didLoadAllEvents(\n    List<CalendarEventPB> events,\n  ) = _ReceiveUnscheduleEventsEvents;\n\n  const factory UnscheduleEventsEvent.didDeleteEvents(List<RowId> rowIds) =\n      _DidDeleteEvents;\n\n  const factory UnscheduleEventsEvent.didReceiveEvent(\n    CalendarEventPB event,\n  ) = _DidReceiveEvent;\n}\n\n@freezed\nclass UnscheduleEventsState with _$UnscheduleEventsState {\n  const factory UnscheduleEventsState({\n    required DatabasePB? database,\n    required List<CalendarEventPB> allEvents,\n    required List<CalendarEventPB> unscheduleEvents,\n  }) = _UnscheduleEventsState;\n\n  factory UnscheduleEventsState.initial() => const UnscheduleEventsState(\n        database: null,\n        allEvents: [],\n        unscheduleEvents: [],\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/calendar/calendar.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nclass CalendarPluginBuilder extends PluginBuilder {\n  @override\n  Plugin build(dynamic data) {\n    if (data is ViewPB) {\n      return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data);\n    } else {\n      throw FlowyPluginException.invalidData;\n    }\n  }\n\n  @override\n  String get menuName => LocaleKeys.calendar_menuName.tr();\n\n  @override\n  FlowySvgData get icon => FlowySvgs.icon_calendar_s;\n\n  @override\n  PluginType get pluginType => PluginType.calendar;\n\n  @override\n  ViewLayoutPB get layoutType => ViewLayoutPB.Calendar;\n}\n\nclass CalendarPluginConfig implements PluginConfig {\n  @override\n  bool get creatable => true;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/database/mobile_calendar_events_screen.dart';\nimport 'package:appflowy/plugins/database/application/row/row_cache.dart';\nimport 'package:calendar_view/calendar_view.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra/time/duration.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../../grid/presentation/layout/sizes.dart';\nimport '../application/calendar_bloc.dart';\nimport 'calendar_event_card.dart';\n\nclass CalendarDayCard extends StatelessWidget {\n  const CalendarDayCard({\n    super.key,\n    required this.viewId,\n    required this.isToday,\n    required this.isInMonth,\n    required this.date,\n    required this.rowCache,\n    required this.events,\n    required this.onCreateEvent,\n    required this.position,\n  });\n\n  final String viewId;\n  final bool isToday;\n  final bool isInMonth;\n  final DateTime date;\n  final RowCache rowCache;\n  final List<CalendarDayEvent> events;\n  final void Function(DateTime) onCreateEvent;\n  final CellPosition position;\n\n  @override\n  Widget build(BuildContext context) {\n    final hoverBackgroundColor =\n        Theme.of(context).brightness == Brightness.light\n            ? Theme.of(context).colorScheme.secondaryContainer\n            : Colors.transparent;\n\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        return ChangeNotifierProvider(\n          create: (_) => _CardEnterNotifier(),\n          builder: (context, child) {\n            final child = Column(\n              mainAxisSize: MainAxisSize.min,\n              crossAxisAlignment: CrossAxisAlignment.end,\n              children: [\n                _Header(\n                  date: date,\n                  isInMonth: isInMonth,\n                  isToday: isToday,\n                ),\n\n                // Add a separator between the header and the content.\n                const VSpace(6.0),\n\n                // List of cards or empty space\n                if (events.isNotEmpty && !UniversalPlatform.isMobile) ...[\n                  _EventList(\n                    events: events,\n                    viewId: viewId,\n                    rowCache: rowCache,\n                    constraints: constraints,\n                  ),\n                ] else if (events.isNotEmpty && UniversalPlatform.isMobile) ...[\n                  const _EventIndicator(),\n                ],\n              ],\n            );\n\n            return Stack(\n              children: [\n                GestureDetector(\n                  onDoubleTap: () => onCreateEvent(date),\n                  onTap: UniversalPlatform.isMobile\n                      ? () => _mobileOnTap(context)\n                      : null,\n                  child: Container(\n                    decoration: BoxDecoration(\n                      color: date.isWeekend\n                          ? AFThemeExtension.of(context).calendarWeekendBGColor\n                          : Colors.transparent,\n                      border: _borderFromPosition(context, position),\n                    ),\n                  ),\n                ),\n                DragTarget<CalendarDayEvent>(\n                  builder: (context, candidate, __) {\n                    return Stack(\n                      children: [\n                        Container(\n                          width: double.infinity,\n                          height: double.infinity,\n                          color:\n                              candidate.isEmpty ? null : hoverBackgroundColor,\n                          padding: const EdgeInsets.only(top: 5.0),\n                          child: child,\n                        ),\n                        if (candidate.isEmpty && !UniversalPlatform.isMobile)\n                          NewEventButton(\n                            onCreate: () => onCreateEvent(date),\n                          ),\n                      ],\n                    );\n                  },\n                  onAcceptWithDetails: (details) {\n                    final event = details.data;\n                    if (event.date != date) {\n                      context\n                          .read<CalendarBloc>()\n                          .add(CalendarEvent.moveEvent(event, date));\n                    }\n                  },\n                ),\n                MouseRegion(\n                  onEnter: (p) => notifyEnter(context, true),\n                  onExit: (p) => notifyEnter(context, false),\n                  opaque: false,\n                  hitTestBehavior: HitTestBehavior.translucent,\n                ),\n              ],\n            );\n          },\n        );\n      },\n    );\n  }\n\n  void _mobileOnTap(BuildContext context) {\n    context.push(\n      MobileCalendarEventsScreen.routeName,\n      extra: {\n        MobileCalendarEventsScreen.calendarBlocKey:\n            context.read<CalendarBloc>(),\n        MobileCalendarEventsScreen.calendarDateKey: date,\n        MobileCalendarEventsScreen.calendarEventsKey: events,\n        MobileCalendarEventsScreen.calendarRowCacheKey: rowCache,\n        MobileCalendarEventsScreen.calendarViewIdKey: viewId,\n      },\n    );\n  }\n\n  bool notifyEnter(BuildContext context, bool isEnter) =>\n      Provider.of<_CardEnterNotifier>(context, listen: false).onEnter = isEnter;\n\n  Border _borderFromPosition(BuildContext context, CellPosition position) {\n    final BorderSide borderSide =\n        BorderSide(color: Theme.of(context).dividerColor);\n\n    return Border(\n      top: borderSide,\n      left: borderSide,\n      bottom: [\n        CellPosition.bottom,\n        CellPosition.bottomLeft,\n        CellPosition.bottomRight,\n      ].contains(position)\n          ? borderSide\n          : BorderSide.none,\n      right: [\n        CellPosition.topRight,\n        CellPosition.bottomRight,\n        CellPosition.right,\n      ].contains(position)\n          ? borderSide\n          : BorderSide.none,\n    );\n  }\n}\n\nclass _EventIndicator extends StatelessWidget {\n  const _EventIndicator();\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        Container(\n          width: 7,\n          height: 7,\n          decoration: BoxDecoration(\n            shape: BoxShape.circle,\n            color: Theme.of(context).hintColor,\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass _Header extends StatelessWidget {\n  const _Header({\n    required this.isToday,\n    required this.isInMonth,\n    required this.date,\n  });\n\n  final bool isToday;\n  final bool isInMonth;\n  final DateTime date;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 6.0),\n      child: _DayBadge(isToday: isToday, isInMonth: isInMonth, date: date),\n    );\n  }\n}\n\n@visibleForTesting\nclass NewEventButton extends StatelessWidget {\n  const NewEventButton({super.key, required this.onCreate});\n\n  final VoidCallback onCreate;\n\n  @override\n  Widget build(BuildContext context) {\n    return Consumer<_CardEnterNotifier>(\n      builder: (context, notifier, _) {\n        if (!notifier.onEnter) {\n          return const SizedBox.shrink();\n        }\n\n        return Padding(\n          padding: const EdgeInsets.all(4.0),\n          child: FlowyIconButton(\n            onPressed: onCreate,\n            icon: const FlowySvg(FlowySvgs.add_s),\n            fillColor: Theme.of(context).colorScheme.surface,\n            hoverColor: AFThemeExtension.of(context).lightGreyHover,\n            width: 22,\n            tooltipText: LocaleKeys.calendar_newEventButtonTooltip.tr(),\n            radius: Corners.s6Border,\n            decoration: BoxDecoration(\n              border: Border.fromBorderSide(\n                BorderSide(\n                  color: Theme.of(context).brightness == Brightness.light\n                      ? const Color(0xffd0d3d6)\n                      : const Color(0xff59647a),\n                  width: 0.5,\n                ),\n              ),\n              borderRadius: Corners.s6Border,\n              boxShadow: [\n                BoxShadow(\n                  spreadRadius: -2,\n                  color: const Color(0xFF1F2329).withValues(alpha: 0.02),\n                  blurRadius: 2,\n                ),\n                BoxShadow(\n                  color: const Color(0xFF1F2329).withValues(alpha: 0.02),\n                  blurRadius: 4,\n                ),\n                BoxShadow(\n                  spreadRadius: 2,\n                  color: const Color(0xFF1F2329).withValues(alpha: 0.02),\n                  blurRadius: 8,\n                ),\n              ],\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass _DayBadge extends StatelessWidget {\n  const _DayBadge({\n    required this.isToday,\n    required this.isInMonth,\n    required this.date,\n  });\n\n  final bool isToday;\n  final bool isInMonth;\n  final DateTime date;\n\n  @override\n  Widget build(BuildContext context) {\n    Color dayTextColor = AFThemeExtension.of(context).onBackground;\n    Color monthTextColor = AFThemeExtension.of(context).onBackground;\n    final String monthString =\n        DateFormat(\"MMM \", context.locale.toLanguageTag()).format(date);\n    final String dayString = date.day.toString();\n\n    if (!isInMonth) {\n      dayTextColor = Theme.of(context).disabledColor;\n      monthTextColor = Theme.of(context).disabledColor;\n    }\n    if (isToday) {\n      dayTextColor = Theme.of(context).colorScheme.onPrimary;\n    }\n\n    final double size = UniversalPlatform.isMobile ? 20 : 18;\n\n    return SizedBox(\n      height: size,\n      child: Row(\n        mainAxisAlignment: UniversalPlatform.isMobile\n            ? MainAxisAlignment.center\n            : MainAxisAlignment.end,\n        children: [\n          if (date.day == 1 && !UniversalPlatform.isMobile)\n            FlowyText.medium(\n              monthString,\n              fontSize: 11,\n              color: monthTextColor,\n            ),\n          Container(\n            decoration: BoxDecoration(\n              color: isToday ? Theme.of(context).colorScheme.primary : null,\n              borderRadius: BorderRadius.circular(10),\n            ),\n            width: isToday ? size : null,\n            height: isToday ? size : null,\n            child: Center(\n              child: FlowyText(\n                dayString,\n                fontSize: UniversalPlatform.isMobile ? 12 : 11,\n                color: dayTextColor,\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _EventList extends StatelessWidget {\n  const _EventList({\n    required this.events,\n    required this.viewId,\n    required this.rowCache,\n    required this.constraints,\n  });\n\n  final List<CalendarDayEvent> events;\n  final String viewId;\n  final RowCache rowCache;\n  final BoxConstraints constraints;\n\n  @override\n  Widget build(BuildContext context) {\n    final editingEvent = context.watch<CalendarBloc>().state.editingEvent;\n\n    return Flexible(\n      child: ScrollConfiguration(\n        behavior: ScrollConfiguration.of(context).copyWith(scrollbars: true),\n        child: ListView.separated(\n          itemBuilder: (BuildContext context, int index) {\n            final autoEdit =\n                editingEvent?.event?.eventId == events[index].eventId;\n            return EventCard(\n              databaseController:\n                  context.read<CalendarBloc>().databaseController,\n              event: events[index],\n              constraints: constraints,\n              autoEdit: autoEdit,\n            );\n          },\n          itemCount: events.length,\n          padding: const EdgeInsets.fromLTRB(4.0, 0, 4.0, 4.0),\n          separatorBuilder: (_, __) =>\n              VSpace(GridSize.typeOptionSeparatorHeight),\n          shrinkWrap: true,\n        ),\n      ),\n    );\n  }\n}\n\nclass _CardEnterNotifier extends ChangeNotifier {\n  _CardEnterNotifier();\n\n  bool _onEnter = false;\n\n  set onEnter(bool value) {\n    if (_onEnter != value) {\n      _onEnter = value;\n      notifyListeners();\n    }\n  }\n\n  bool get onEnter => _onEnter;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_cache.dart';\nimport 'package:appflowy/plugins/database/application/row/row_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/card/card.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_detail.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../application/calendar_bloc.dart';\nimport 'calendar_event_editor.dart';\n\nclass EventCard extends StatefulWidget {\n  const EventCard({\n    super.key,\n    required this.databaseController,\n    required this.event,\n    required this.constraints,\n    required this.autoEdit,\n    this.isDraggable = true,\n    this.padding = EdgeInsets.zero,\n  });\n\n  final DatabaseController databaseController;\n  final CalendarDayEvent event;\n  final BoxConstraints constraints;\n  final bool autoEdit;\n  final bool isDraggable;\n  final EdgeInsets padding;\n\n  @override\n  State<EventCard> createState() => _EventCardState();\n}\n\nclass _EventCardState extends State<EventCard> {\n  final PopoverController _popoverController = PopoverController();\n\n  String get viewId => widget.databaseController.viewId;\n  RowCache get rowCache => widget.databaseController.rowCache;\n\n  @override\n  void initState() {\n    super.initState();\n    if (widget.autoEdit) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        _popoverController.show();\n        context\n            .read<CalendarBloc>()\n            .add(const CalendarEvent.newEventPopupDisplayed());\n      });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final rowInfo = rowCache.getRow(widget.event.eventId);\n    if (rowInfo == null) {\n      return const SizedBox.shrink();\n    }\n\n    final cellBuilder = CardCellBuilder(\n      databaseController: widget.databaseController,\n    );\n\n    Widget card = RowCard(\n      // Add the key here to make sure the card is rebuilt when the cells\n      // in this row are updated.\n      key: ValueKey(widget.event.eventId),\n      fieldController: widget.databaseController.fieldController,\n      rowMeta: rowInfo.rowMeta,\n      viewId: viewId,\n      rowCache: rowCache,\n      isEditing: false,\n      cellBuilder: cellBuilder,\n      isCompact: true,\n      onTap: (context) {\n        if (UniversalPlatform.isMobile) {\n          context.push(\n            MobileRowDetailPage.routeName,\n            extra: {\n              MobileRowDetailPage.argRowId: rowInfo.rowId,\n              MobileRowDetailPage.argDatabaseController:\n                  widget.databaseController,\n            },\n          );\n        } else {\n          _popoverController.show();\n        }\n      },\n      styleConfiguration: RowCardStyleConfiguration(\n        cellStyleMap: desktopCalendarCardCellStyleMap(context),\n        showAccessory: false,\n        cardPadding: const EdgeInsets.all(6),\n        hoverStyle: HoverStyle(\n          hoverColor: Theme.of(context).brightness == Brightness.light\n              ? const Color(0x0F1F2329)\n              : const Color(0x0FEFF4FB),\n          foregroundColorOnHover: AFThemeExtension.of(context).onBackground,\n        ),\n      ),\n      onStartEditing: () {},\n      onEndEditing: () {},\n      userProfile: context.read<CalendarBloc>().userProfile,\n    );\n\n    final decoration = BoxDecoration(\n      color: Theme.of(context).colorScheme.surface,\n      border: Border.fromBorderSide(\n        BorderSide(\n          color: Theme.of(context).brightness == Brightness.light\n              ? const Color(0xffd0d3d6)\n              : const Color(0xff59647a),\n          width: 0.5,\n        ),\n      ),\n      borderRadius: Corners.s6Border,\n      boxShadow: [\n        BoxShadow(\n          spreadRadius: -2,\n          color: const Color(0xFF1F2329).withValues(alpha: 0.02),\n          blurRadius: 2,\n        ),\n        BoxShadow(\n          color: const Color(0xFF1F2329).withValues(alpha: 0.02),\n          blurRadius: 4,\n        ),\n        BoxShadow(\n          spreadRadius: 2,\n          color: const Color(0xFF1F2329).withValues(alpha: 0.02),\n          blurRadius: 8,\n        ),\n      ],\n    );\n\n    card = AppFlowyPopover(\n      triggerActions: PopoverTriggerFlags.none,\n      direction: PopoverDirection.rightWithCenterAligned,\n      controller: _popoverController,\n      constraints: const BoxConstraints(maxWidth: 360, maxHeight: 348),\n      asBarrier: true,\n      margin: EdgeInsets.zero,\n      offset: const Offset(10.0, 0),\n      popupBuilder: (_) {\n        final settings = context.watch<CalendarBloc>().state.settings;\n        if (settings == null) {\n          return const SizedBox.shrink();\n        }\n        return MultiBlocProvider(\n          providers: [\n            BlocProvider.value(\n              value: context.read<CalendarBloc>(),\n            ),\n            BlocProvider.value(\n              value: context.read<ViewBloc>(),\n            ),\n          ],\n          child: CalendarEventEditor(\n            databaseController: widget.databaseController,\n            rowMeta: widget.event.event.rowMeta,\n            layoutSettings: settings,\n            onExpand: () {\n              final rowController = RowController(\n                rowMeta: widget.event.event.rowMeta,\n                viewId: widget.databaseController.viewId,\n                rowCache: widget.databaseController.rowCache,\n              );\n\n              FlowyOverlay.show(\n                context: context,\n                builder: (_) => BlocProvider.value(\n                  value: context.read<UserWorkspaceBloc>(),\n                  child: RowDetailPage(\n                    databaseController: widget.databaseController,\n                    rowController: rowController,\n                    userProfile: context.read<CalendarBloc>().userProfile,\n                  ),\n                ),\n              );\n            },\n          ),\n        );\n      },\n      child: Padding(\n        padding: widget.padding,\n        child: Material(\n          color: Colors.transparent,\n          child: DecoratedBox(\n            decoration: decoration,\n            child: card,\n          ),\n        ),\n      ),\n    );\n\n    if (widget.isDraggable) {\n      return Draggable<CalendarDayEvent>(\n        data: widget.event,\n        feedback: Container(\n          constraints: BoxConstraints(\n            maxWidth: widget.constraints.maxWidth - 8.0,\n          ),\n          decoration: decoration,\n          child: Opacity(\n            opacity: 0.6,\n            child: card,\n          ),\n        ),\n        child: card,\n      );\n    }\n\n    return card;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_controller.dart';\nimport 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart';\nimport 'package:appflowy/plugins/database/calendar/application/calendar_event_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';\nimport 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass CalendarEventEditor extends StatelessWidget {\n  CalendarEventEditor({\n    super.key,\n    required RowMetaPB rowMeta,\n    required this.layoutSettings,\n    required this.databaseController,\n    required this.onExpand,\n  })  : rowController = RowController(\n          rowMeta: rowMeta,\n          viewId: databaseController.viewId,\n          rowCache: databaseController.rowCache,\n        ),\n        cellBuilder =\n            EditableCellBuilder(databaseController: databaseController);\n\n  final CalendarLayoutSettingPB layoutSettings;\n  final DatabaseController databaseController;\n  final RowController rowController;\n  final EditableCellBuilder cellBuilder;\n  final VoidCallback onExpand;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<CalendarEventEditorBloc>(\n      create: (context) => CalendarEventEditorBloc(\n        fieldController: databaseController.fieldController,\n        rowController: rowController,\n        layoutSettings: layoutSettings,\n      )..add(const CalendarEventEditorEvent.initial()),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          EventEditorControls(\n            rowController: rowController,\n            databaseController: databaseController,\n            onExpand: onExpand,\n          ),\n          Flexible(\n            child: EventPropertyList(\n              fieldController: databaseController.fieldController,\n              dateFieldId: layoutSettings.fieldId,\n              cellBuilder: cellBuilder,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass EventEditorControls extends StatelessWidget {\n  const EventEditorControls({\n    super.key,\n    required this.rowController,\n    required this.databaseController,\n    required this.onExpand,\n  });\n\n  final RowController rowController;\n  final DatabaseController databaseController;\n  final VoidCallback onExpand;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 0),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.end,\n        children: [\n          FlowyTooltip(\n            message: LocaleKeys.calendar_duplicateEvent.tr(),\n            child: FlowyIconButton(\n              width: 20,\n              icon: FlowySvg(\n                FlowySvgs.m_duplicate_s,\n                size: const Size.square(16),\n                color: Theme.of(context).iconTheme.color,\n              ),\n              onPressed: () {\n                context.read<CalendarBloc>().add(\n                      CalendarEvent.duplicateEvent(\n                        rowController.viewId,\n                        rowController.rowId,\n                      ),\n                    );\n                PopoverContainer.of(context).close();\n              },\n            ),\n          ),\n          const HSpace(8.0),\n          FlowyIconButton(\n            width: 20,\n            icon: FlowySvg(\n              FlowySvgs.delete_s,\n              size: const Size.square(16),\n              color: Theme.of(context).iconTheme.color,\n            ),\n            onPressed: () {\n              showConfirmDeletionDialog(\n                context: context,\n                name: LocaleKeys.grid_row_label.tr(),\n                description: LocaleKeys.grid_row_deleteRowPrompt.tr(),\n                onConfirm: () {\n                  context.read<CalendarBloc>().add(\n                        CalendarEvent.deleteEvent(\n                          rowController.viewId,\n                          rowController.rowId,\n                        ),\n                      );\n                  PopoverContainer.of(context).close();\n                },\n              );\n            },\n          ),\n          const HSpace(8.0),\n          FlowyIconButton(\n            width: 20,\n            icon: FlowySvg(\n              FlowySvgs.full_view_s,\n              size: const Size.square(16),\n              color: Theme.of(context).iconTheme.color,\n            ),\n            onPressed: () {\n              PopoverContainer.of(context).close();\n              onExpand.call();\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass EventPropertyList extends StatelessWidget {\n  const EventPropertyList({\n    super.key,\n    required this.fieldController,\n    required this.dateFieldId,\n    required this.cellBuilder,\n  });\n\n  final FieldController fieldController;\n  final String dateFieldId;\n  final EditableCellBuilder cellBuilder;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<CalendarEventEditorBloc, CalendarEventEditorState>(\n      builder: (context, state) {\n        final primaryFieldId = fieldController.fieldInfos\n            .firstWhereOrNull((fieldInfo) => fieldInfo.isPrimary)!\n            .id;\n        final reorderedList = List<CellContext>.from(state.cells)\n          ..retainWhere((cell) => cell.fieldId != primaryFieldId);\n\n        final primaryCellContext = state.cells\n            .firstWhereOrNull((cell) => cell.fieldId == primaryFieldId);\n        final dateFieldIndex =\n            reorderedList.indexWhere((cell) => cell.fieldId == dateFieldId);\n\n        if (primaryCellContext == null || dateFieldIndex == -1) {\n          return const SizedBox.shrink();\n        }\n\n        reorderedList.insert(0, reorderedList.removeAt(dateFieldIndex));\n\n        final children = [\n          Padding(\n            padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),\n            child: cellBuilder.buildCustom(\n              primaryCellContext,\n              skinMap: EditableCellSkinMap(textSkin: _TitleTextCellSkin()),\n            ),\n          ),\n          ...reorderedList.map(\n            (cellContext) => PropertyCell(\n              fieldController: fieldController,\n              cellContext: cellContext,\n              cellBuilder: cellBuilder,\n            ),\n          ),\n        ];\n\n        return ListView(\n          shrinkWrap: true,\n          padding: const EdgeInsets.only(bottom: 16.0),\n          children: children,\n        );\n      },\n    );\n  }\n}\n\nclass PropertyCell extends StatefulWidget {\n  const PropertyCell({\n    super.key,\n    required this.fieldController,\n    required this.cellContext,\n    required this.cellBuilder,\n  });\n\n  final FieldController fieldController;\n  final CellContext cellContext;\n  final EditableCellBuilder cellBuilder;\n\n  @override\n  State<StatefulWidget> createState() => _PropertyCellState();\n}\n\nclass _PropertyCellState extends State<PropertyCell> {\n  @override\n  Widget build(BuildContext context) {\n    final fieldInfo =\n        widget.fieldController.getField(widget.cellContext.fieldId)!;\n    final cell = widget.cellBuilder\n        .buildStyled(widget.cellContext, EditableCellStyle.desktopRowDetail);\n\n    final gesture = GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: () => cell.requestFocus.notify(),\n      child: AccessoryHover(\n        fieldType: fieldInfo.fieldType,\n        child: cell,\n      ),\n    );\n\n    return Container(\n      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),\n      constraints: const BoxConstraints(minHeight: 28),\n      child: Row(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          SizedBox(\n            width: 88,\n            height: 28,\n            child: Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 6),\n              child: Row(\n                children: [\n                  FieldIcon(\n                    fieldInfo: fieldInfo,\n                    dimension: 14,\n                  ),\n                  const HSpace(4.0),\n                  Expanded(\n                    child: FlowyText.regular(\n                      fieldInfo.name,\n                      color: Theme.of(context).hintColor,\n                      overflow: TextOverflow.ellipsis,\n                      fontSize: 11,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n          const HSpace(8),\n          Expanded(child: gesture),\n        ],\n      ),\n    );\n  }\n}\n\nclass _TitleTextCellSkin extends IEditableTextCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TextCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return FlowyTextField(\n      controller: textEditingController,\n      textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 14),\n      focusNode: focusNode,\n      hintText: LocaleKeys.calendar_defaultNewCalendarTitle.tr(),\n      onEditingComplete: () {\n        bloc.add(TextCellEvent.updateText(textEditingController.text));\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart",
    "content": "import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/database/card/card.dart';\nimport 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart';\nimport 'package:appflowy/plugins/database/calendar/application/unschedule_event_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';\nimport 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:calendar_view/calendar_view.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../../application/row/row_controller.dart';\nimport '../../widgets/row/row_detail.dart';\nimport 'calendar_day.dart';\nimport 'layout/sizes.dart';\nimport 'toolbar/calendar_setting_bar.dart';\n\nclass CalendarPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {\n  final _toggleExtension = ToggleExtensionNotifier();\n\n  @override\n  Widget content(\n    BuildContext context,\n    ViewPB view,\n    DatabaseController controller,\n    bool shrinkWrap,\n    String? initialRowId,\n  ) {\n    return CalendarPage(\n      key: _makeValueKey(controller),\n      view: view,\n      databaseController: controller,\n      shrinkWrap: shrinkWrap,\n    );\n  }\n\n  @override\n  Widget settingBar(BuildContext context, DatabaseController controller) {\n    return CalendarSettingBar(\n      key: _makeValueKey(controller),\n      databaseController: controller,\n      toggleExtension: _toggleExtension,\n    );\n  }\n\n  @override\n  Widget settingBarExtension(\n    BuildContext context,\n    DatabaseController controller,\n  ) {\n    return DatabaseViewSettingExtension(\n      key: _makeValueKey(controller),\n      viewId: controller.viewId,\n      databaseController: controller,\n      toggleExtension: _toggleExtension,\n    );\n  }\n\n  @override\n  void dispose() {\n    _toggleExtension.dispose();\n    super.dispose();\n  }\n\n  ValueKey _makeValueKey(DatabaseController controller) {\n    return ValueKey(controller.viewId);\n  }\n}\n\nclass CalendarPage extends StatefulWidget {\n  const CalendarPage({\n    super.key,\n    required this.view,\n    required this.databaseController,\n    this.shrinkWrap = false,\n  });\n\n  final ViewPB view;\n  final DatabaseController databaseController;\n  final bool shrinkWrap;\n\n  @override\n  State<CalendarPage> createState() => _CalendarPageState();\n}\n\nclass _CalendarPageState extends State<CalendarPage> {\n  final _eventController = EventController<CalendarDayEvent>();\n  late final CalendarBloc _calendarBloc;\n  GlobalKey<MonthViewState>? _calendarState;\n\n  @override\n  void initState() {\n    super.initState();\n    _calendarState = GlobalKey<MonthViewState>();\n    _calendarBloc = CalendarBloc(\n      databaseController: widget.databaseController,\n    )..add(const CalendarEvent.initial());\n  }\n\n  @override\n  void dispose() {\n    _calendarBloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return CalendarControllerProvider(\n      controller: _eventController,\n      child: MultiBlocProvider(\n        providers: [\n          BlocProvider<CalendarBloc>.value(\n            value: _calendarBloc,\n          ),\n          BlocProvider(\n            create: (context) => PageAccessLevelBloc(view: widget.view)\n              ..add(\n                PageAccessLevelEvent.initial(),\n              ),\n          ),\n        ],\n        child: MultiBlocListener(\n          listeners: [\n            BlocListener<CalendarBloc, CalendarState>(\n              listenWhen: (p, c) => p.initialEvents != c.initialEvents,\n              listener: (context, state) {\n                _eventController.removeWhere((_) => true);\n                _eventController.addAll(state.initialEvents);\n              },\n            ),\n            BlocListener<CalendarBloc, CalendarState>(\n              listenWhen: (p, c) => p.deleteEventIds != c.deleteEventIds,\n              listener: (context, state) {\n                _eventController.removeWhere(\n                  (element) =>\n                      state.deleteEventIds.contains(element.event!.eventId),\n                );\n              },\n            ),\n            BlocListener<CalendarBloc, CalendarState>(\n              // Event create by click the + button or double click on the\n              // calendar\n              listenWhen: (p, c) => p.newEvent != c.newEvent,\n              listener: (context, state) {\n                if (state.newEvent != null) {\n                  _eventController.add(state.newEvent!);\n                }\n              },\n            ),\n            BlocListener<CalendarBloc, CalendarState>(\n              // When an event is rescheduled\n              listenWhen: (p, c) => p.updateEvent != c.updateEvent,\n              listener: (context, state) {\n                if (state.updateEvent != null) {\n                  _eventController.removeWhere(\n                    (element) =>\n                        element.event!.eventId ==\n                        state.updateEvent!.event!.eventId,\n                  );\n                  _eventController.add(state.updateEvent!);\n                }\n              },\n            ),\n            BlocListener<CalendarBloc, CalendarState>(\n              listenWhen: (p, c) => p.openRow != c.openRow,\n              listener: (context, state) {\n                if (state.openRow != null) {\n                  showEventDetails(\n                    context: context,\n                    databaseController: _calendarBloc.databaseController,\n                    rowMeta: state.openRow!,\n                  );\n                }\n              },\n            ),\n          ],\n          child: BlocBuilder<CalendarBloc, CalendarState>(\n            builder: (context, state) {\n              return ValueListenableBuilder<bool>(\n                valueListenable: widget.databaseController.isLoading,\n                builder: (_, value, ___) {\n                  if (value) {\n                    return const Center(\n                      child: CircularProgressIndicator.adaptive(),\n                    );\n                  }\n                  return _buildCalendar(\n                    context,\n                    _eventController,\n                    state.settings?.firstDayOfWeek ?? 0,\n                  );\n                },\n              );\n            },\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildCalendar(\n    BuildContext context,\n    EventController eventController,\n    int firstDayOfWeek,\n  ) {\n    return LayoutBuilder(\n      // must specify MonthView width for useAvailableVerticalSpace to work properly\n      builder: (context, constraints) {\n        final paddingLeft =\n            context.read<DatabasePluginWidgetBuilderSize>().paddingLeft;\n        EdgeInsets padding = UniversalPlatform.isMobile\n            ? CalendarSize.contentInsetsMobile\n            : CalendarSize.contentInsets +\n                const EdgeInsets.symmetric(horizontal: 40);\n        final double horizontalPadding =\n            context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding;\n        if (horizontalPadding == 0) {\n          padding = padding.copyWith(left: 0, right: 0);\n        }\n        padding = padding.copyWith(left: paddingLeft + padding.left);\n        return Padding(\n          padding: padding,\n          child: ScrollConfiguration(\n            behavior:\n                ScrollConfiguration.of(context).copyWith(scrollbars: false),\n            child: MonthView(\n              key: _calendarState,\n              controller: _eventController,\n              width: constraints.maxWidth,\n              cellAspectRatio: UniversalPlatform.isMobile ? 0.9 : 0.6,\n              startDay: _weekdayFromInt(firstDayOfWeek),\n              showBorder: false,\n              headerBuilder: _headerNavigatorBuilder,\n              weekDayBuilder: _headerWeekDayBuilder,\n              cellBuilder: (\n                date,\n                calenderEvents,\n                isToday,\n                isInMonth,\n                position,\n              ) =>\n                  _calendarDayBuilder(\n                context,\n                date,\n                calenderEvents,\n                isToday,\n                isInMonth,\n                position,\n              ),\n              useAvailableVerticalSpace: widget.shrinkWrap,\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _headerNavigatorBuilder(DateTime currentMonth) {\n    return SizedBox(\n      height: 24,\n      child: Row(\n        children: [\n          GestureDetector(\n            onTap: UniversalPlatform.isMobile\n                ? () => showMobileBottomSheet(\n                      context,\n                      title: LocaleKeys.calendar_quickJumpYear.tr(),\n                      showHeader: true,\n                      showCloseButton: true,\n                      builder: (_) => SizedBox(\n                        height: 200,\n                        child: YearPicker(\n                          firstDate: CalendarConstants.epochDate.withoutTime,\n                          lastDate: CalendarConstants.maxDate.withoutTime,\n                          selectedDate: currentMonth,\n                          currentDate: DateTime.now(),\n                          onChanged: (newDate) {\n                            _calendarState?.currentState?.jumpToMonth(newDate);\n                            context.pop();\n                          },\n                        ),\n                      ),\n                    )\n                : null,\n            child: Row(\n              children: [\n                FlowyText.medium(\n                  DateFormat('MMMM y', context.locale.toLanguageTag())\n                      .format(currentMonth),\n                ),\n                if (UniversalPlatform.isMobile) ...[\n                  const HSpace(6),\n                  const FlowySvg(FlowySvgs.arrow_down_s),\n                ],\n              ],\n            ),\n          ),\n          const Spacer(),\n          FlowyIconButton(\n            width: CalendarSize.navigatorButtonWidth,\n            height: CalendarSize.navigatorButtonHeight,\n            icon: const FlowySvg(FlowySvgs.arrow_left_s),\n            tooltipText: LocaleKeys.calendar_navigation_previousMonth.tr(),\n            hoverColor: AFThemeExtension.of(context).lightGreyHover,\n            onPressed: () => _calendarState?.currentState?.previousPage(),\n          ),\n          FlowyTextButton(\n            LocaleKeys.calendar_navigation_today.tr(),\n            fillColor: Colors.transparent,\n            fontWeight: FontWeight.w400,\n            fontSize: 10,\n            fontColor: AFThemeExtension.of(context).textColor,\n            tooltip: LocaleKeys.calendar_navigation_jumpToday.tr(),\n            padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),\n            hoverColor: AFThemeExtension.of(context).lightGreyHover,\n            onPressed: () =>\n                _calendarState?.currentState?.animateToMonth(DateTime.now()),\n          ),\n          FlowyIconButton(\n            width: CalendarSize.navigatorButtonWidth,\n            height: CalendarSize.navigatorButtonHeight,\n            icon: const FlowySvg(FlowySvgs.arrow_right_s),\n            tooltipText: LocaleKeys.calendar_navigation_nextMonth.tr(),\n            hoverColor: AFThemeExtension.of(context).lightGreyHover,\n            onPressed: () => _calendarState?.currentState?.nextPage(),\n          ),\n          const HSpace(6.0),\n          UnscheduledEventsButton(\n            databaseController: widget.databaseController,\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _headerWeekDayBuilder(day) {\n    // incoming day starts from Monday, the symbols start from Sunday\n    final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;\n    String weekDayString = symbols.WEEKDAYS[(day + 1) % 7];\n\n    if (UniversalPlatform.isMobile) {\n      weekDayString = weekDayString.substring(0, 3);\n    }\n\n    return Center(\n      child: Padding(\n        padding: CalendarSize.daysOfWeekInsets,\n        child: FlowyText.regular(\n          weekDayString,\n          fontSize: 9,\n          color: Theme.of(context).hintColor,\n        ),\n      ),\n    );\n  }\n\n  Widget _calendarDayBuilder(\n    BuildContext context,\n    DateTime date,\n    List<CalendarEventData<CalendarDayEvent>> calenderEvents,\n    isToday,\n    isInMonth,\n    position,\n  ) {\n    // Sort the events by timestamp. Because the database view is not\n    // reserving the order of the events. Reserving the order of the rows/events\n    // is implemnted in the develop branch(WIP). Will be replaced with that.\n    final events = calenderEvents.map((value) => value.event!).toList()\n      ..sort((a, b) => a.event.timestamp.compareTo(b.event.timestamp));\n    final isEditable =\n        context.watch<PageAccessLevelBloc?>()?.state.isEditable ?? false;\n    return IgnorePointer(\n      ignoring: !isEditable,\n      child: CalendarDayCard(\n        viewId: widget.view.id,\n        isToday: isToday,\n        isInMonth: isInMonth,\n        events: events,\n        date: date,\n        rowCache: _calendarBloc.rowCache,\n        onCreateEvent: (date) =>\n            _calendarBloc.add(CalendarEvent.createEvent(date)),\n        position: position,\n      ),\n    );\n  }\n\n  WeekDays _weekdayFromInt(int dayOfWeek) {\n    // dayOfWeek starts from Sunday, WeekDays starts from Monday\n    return WeekDays.values[(dayOfWeek - 1) % 7];\n  }\n}\n\nvoid showEventDetails({\n  required BuildContext context,\n  required DatabaseController databaseController,\n  required RowMetaPB rowMeta,\n}) {\n  final rowController = RowController(\n    rowMeta: rowMeta,\n    viewId: databaseController.viewId,\n    rowCache: databaseController.rowCache,\n  );\n\n  FlowyOverlay.show(\n    context: context,\n    builder: (BuildContext overlayContext) {\n      return BlocProvider.value(\n        value: context.read<UserWorkspaceBloc>(),\n        child: RowDetailPage(\n          rowController: rowController,\n          databaseController: databaseController,\n          userProfile: context.read<CalendarBloc>().userProfile,\n        ),\n      );\n    },\n  );\n}\n\nclass UnscheduledEventsButton extends StatefulWidget {\n  const UnscheduledEventsButton({super.key, required this.databaseController});\n\n  final DatabaseController databaseController;\n\n  @override\n  State<UnscheduledEventsButton> createState() =>\n      _UnscheduledEventsButtonState();\n}\n\nclass _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {\n  final PopoverController _popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<UnscheduleEventsBloc>(\n      create: (_) =>\n          UnscheduleEventsBloc(databaseController: widget.databaseController)\n            ..add(const UnscheduleEventsEvent.initial()),\n      child: BlocBuilder<UnscheduleEventsBloc, UnscheduleEventsState>(\n        builder: (context, state) {\n          return AppFlowyPopover(\n            direction: PopoverDirection.bottomWithCenterAligned,\n            triggerActions: PopoverTriggerFlags.none,\n            controller: _popoverController,\n            offset: const Offset(0, 8),\n            constraints: const BoxConstraints(maxWidth: 282, maxHeight: 600),\n            child: OutlinedButton(\n              style: OutlinedButton.styleFrom(\n                shape: RoundedRectangleBorder(\n                  side: BorderSide(color: Theme.of(context).dividerColor),\n                  borderRadius: Corners.s6Border,\n                ),\n                side: BorderSide(color: Theme.of(context).dividerColor),\n                padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),\n              ),\n              onPressed: () {\n                if (state.unscheduleEvents.isNotEmpty) {\n                  if (UniversalPlatform.isMobile) {\n                    _showUnscheduledEventsMobile(state.unscheduleEvents);\n                  } else {\n                    _popoverController.show();\n                  }\n                }\n              },\n              child: FlowyTooltip(\n                message: LocaleKeys.calendar_settings_noDateHint.plural(\n                  state.unscheduleEvents.length,\n                  namedArgs: {'count': '${state.unscheduleEvents.length}'},\n                ),\n                child: FlowyText.regular(\n                  \"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})\",\n                  fontSize: 10,\n                ),\n              ),\n            ),\n            popupBuilder: (_) => MultiBlocProvider(\n              providers: [\n                BlocProvider.value(\n                  value: context.read<CalendarBloc>(),\n                ),\n                BlocProvider.value(\n                  value: context.read<UserWorkspaceBloc>(),\n                ),\n              ],\n              child: UnscheduleEventsList(\n                databaseController: widget.databaseController,\n                unscheduleEvents: state.unscheduleEvents,\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  void _showUnscheduledEventsMobile(List<CalendarEventPB> events) =>\n      showMobileBottomSheet(\n        context,\n        builder: (_) {\n          return Column(\n            children: [\n              FlowyText(\n                LocaleKeys.calendar_settings_unscheduledEventsTitle.tr(),\n              ),\n              UnscheduleEventsList(\n                databaseController: widget.databaseController,\n                unscheduleEvents: events,\n              ),\n            ],\n          );\n        },\n      );\n}\n\nclass UnscheduleEventsList extends StatelessWidget {\n  const UnscheduleEventsList({\n    super.key,\n    required this.unscheduleEvents,\n    required this.databaseController,\n  });\n\n  final List<CalendarEventPB> unscheduleEvents;\n  final DatabaseController databaseController;\n\n  @override\n  Widget build(BuildContext context) {\n    final cells = [\n      if (!UniversalPlatform.isMobile)\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),\n          child: FlowyText(\n            LocaleKeys.calendar_settings_clickToAdd.tr(),\n            fontSize: 10,\n            color: Theme.of(context).hintColor,\n            overflow: TextOverflow.ellipsis,\n          ),\n        ),\n      ...unscheduleEvents.map(\n        (event) => UnscheduledEventCell(\n          event: event,\n          onPressed: () {\n            if (UniversalPlatform.isMobile) {\n              context.push(\n                MobileRowDetailPage.routeName,\n                extra: {\n                  MobileRowDetailPage.argRowId: event.rowMeta.id,\n                  MobileRowDetailPage.argDatabaseController: databaseController,\n                },\n              );\n              context.pop();\n            } else {\n              showEventDetails(\n                context: context,\n                rowMeta: event.rowMeta,\n                databaseController: databaseController,\n              );\n              PopoverContainer.of(context).close();\n            }\n          },\n        ),\n      ),\n    ];\n\n    final child = ListView.separated(\n      itemBuilder: (context, index) => cells[index],\n      itemCount: cells.length,\n      separatorBuilder: (context, index) =>\n          VSpace(GridSize.typeOptionSeparatorHeight),\n      shrinkWrap: true,\n    );\n\n    if (UniversalPlatform.isMobile) {\n      return Flexible(child: child);\n    }\n\n    return child;\n  }\n}\n\nclass UnscheduledEventCell extends StatelessWidget {\n  const UnscheduledEventCell({\n    super.key,\n    required this.event,\n    required this.onPressed,\n  });\n\n  final CalendarEventPB event;\n  final VoidCallback onPressed;\n\n  @override\n  Widget build(BuildContext context) {\n    return UniversalPlatform.isMobile\n        ? MobileUnscheduledEventTile(event: event, onPressed: onPressed)\n        : DesktopUnscheduledEventTile(event: event, onPressed: onPressed);\n  }\n}\n\nclass DesktopUnscheduledEventTile extends StatelessWidget {\n  const DesktopUnscheduledEventTile({\n    super.key,\n    required this.event,\n    required this.onPressed,\n  });\n\n  final CalendarEventPB event;\n  final VoidCallback onPressed;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 26,\n      child: FlowyButton(\n        margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),\n        text: FlowyText(\n          event.title.isEmpty\n              ? LocaleKeys.calendar_defaultNewCalendarTitle.tr()\n              : event.title,\n          fontSize: 11,\n        ),\n        onTap: onPressed,\n      ),\n    );\n  }\n}\n\nclass MobileUnscheduledEventTile extends StatelessWidget {\n  const MobileUnscheduledEventTile({\n    super.key,\n    required this.event,\n    required this.onPressed,\n  });\n\n  final CalendarEventPB event;\n  final VoidCallback onPressed;\n\n  @override\n  Widget build(BuildContext context) {\n    return MobileSettingItem(\n      name: event.title.isEmpty\n          ? LocaleKeys.calendar_defaultNewCalendarTitle.tr()\n          : event.title,\n      onTap: onPressed,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/layout/sizes.dart",
    "content": "import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:flutter/widgets.dart';\n\nclass CalendarSize {\n  static double scale = 1;\n\n  static double get headerContainerPadding => 12 * scale;\n\n  static EdgeInsets get contentInsets => EdgeInsets.fromLTRB(\n        GridSize.horizontalHeaderPadding,\n        CalendarSize.headerContainerPadding,\n        GridSize.horizontalHeaderPadding,\n        CalendarSize.headerContainerPadding,\n      );\n\n  static EdgeInsets get contentInsetsMobile => EdgeInsets.fromLTRB(\n        GridSize.horizontalHeaderPadding / 2,\n        0,\n        GridSize.horizontalHeaderPadding / 2,\n        0,\n      );\n\n  static double get scrollBarSize => 8 * scale;\n  static double get navigatorButtonWidth => 20 * scale;\n  static double get navigatorButtonHeight => 24 * scale;\n  static EdgeInsets get daysOfWeekInsets =>\n      EdgeInsets.only(top: 12.0 * scale, bottom: 5.0 * scale);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/setting/property_bloc.dart';\nimport 'package:appflowy/plugins/database/calendar/application/calendar_setting_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\n/// Widget that displays a list of settings that alters the appearance of the\n/// calendar\nclass CalendarLayoutSetting extends StatefulWidget {\n  const CalendarLayoutSetting({\n    super.key,\n    required this.databaseController,\n  });\n\n  final DatabaseController databaseController;\n\n  @override\n  State<CalendarLayoutSetting> createState() => _CalendarLayoutSettingState();\n}\n\nclass _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {\n  final PopoverMutex popoverMutex = PopoverMutex();\n\n  @override\n  void dispose() {\n    popoverMutex.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) {\n        return CalendarSettingBloc(\n          databaseController: widget.databaseController,\n        )..add(const CalendarSettingEvent.initial());\n      },\n      child: BlocBuilder<CalendarSettingBloc, CalendarSettingState>(\n        builder: (context, state) {\n          final CalendarLayoutSettingPB? settings = state.layoutSetting;\n\n          if (settings == null) {\n            return const CircularProgressIndicator();\n          }\n          final availableSettings = _availableCalendarSettings(settings);\n          final bloc = context.read<CalendarSettingBloc>();\n          final items = availableSettings.map((setting) {\n            switch (setting) {\n              case CalendarLayoutSettingAction.showWeekNumber:\n                return ShowWeekNumber(\n                  showWeekNumbers: settings.showWeekNumbers,\n                  onUpdated: (showWeekNumbers) => bloc.add(\n                    CalendarSettingEvent.updateLayoutSetting(\n                      showWeekNumbers: showWeekNumbers,\n                    ),\n                  ),\n                );\n              case CalendarLayoutSettingAction.showWeekends:\n                return ShowWeekends(\n                  showWeekends: settings.showWeekends,\n                  onUpdated: (showWeekends) => bloc.add(\n                    CalendarSettingEvent.updateLayoutSetting(\n                      showWeekends: showWeekends,\n                    ),\n                  ),\n                );\n              case CalendarLayoutSettingAction.firstDayOfWeek:\n                return FirstDayOfWeek(\n                  firstDayOfWeek: settings.firstDayOfWeek,\n                  popoverMutex: popoverMutex,\n                  onUpdated: (firstDayOfWeek) => bloc.add(\n                    CalendarSettingEvent.updateLayoutSetting(\n                      firstDayOfWeek: firstDayOfWeek,\n                    ),\n                  ),\n                );\n              case CalendarLayoutSettingAction.layoutField:\n                return LayoutDateField(\n                  databaseController: widget.databaseController,\n                  fieldId: settings.fieldId,\n                  popoverMutex: popoverMutex,\n                  onUpdated: (fieldId) => bloc.add(\n                    CalendarSettingEvent.updateLayoutSetting(\n                      layoutFieldId: fieldId,\n                    ),\n                  ),\n                );\n              default:\n                return const SizedBox.shrink();\n            }\n          }).toList();\n\n          return SizedBox(\n            width: 200,\n            child: ListView.separated(\n              shrinkWrap: true,\n              itemCount: items.length,\n              separatorBuilder: (_, __) =>\n                  VSpace(GridSize.typeOptionSeparatorHeight),\n              physics: StyledScrollPhysics(),\n              itemBuilder: (_, int index) => items[index],\n              padding: const EdgeInsets.all(6.0),\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  List<CalendarLayoutSettingAction> _availableCalendarSettings(\n    CalendarLayoutSettingPB layoutSettings,\n  ) {\n    final List<CalendarLayoutSettingAction> settings = [\n      CalendarLayoutSettingAction.layoutField,\n    ];\n\n    switch (layoutSettings.layoutTy) {\n      case CalendarLayoutPB.DayLayout:\n        break;\n      case CalendarLayoutPB.MonthLayout:\n      case CalendarLayoutPB.WeekLayout:\n        settings.add(CalendarLayoutSettingAction.firstDayOfWeek);\n        break;\n    }\n\n    return settings;\n  }\n}\n\nclass LayoutDateField extends StatelessWidget {\n  const LayoutDateField({\n    super.key,\n    required this.databaseController,\n    required this.fieldId,\n    required this.popoverMutex,\n    required this.onUpdated,\n  });\n\n  final DatabaseController databaseController;\n  final String fieldId;\n  final PopoverMutex popoverMutex;\n  final Function(String fieldId) onUpdated;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      direction: PopoverDirection.leftWithTopAligned,\n      triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,\n      constraints: BoxConstraints.loose(const Size(300, 400)),\n      mutex: popoverMutex,\n      offset: const Offset(-14, 0),\n      popupBuilder: (context) {\n        return BlocProvider(\n          create: (context) => DatabasePropertyBloc(\n            viewId: databaseController.viewId,\n            fieldController: databaseController.fieldController,\n          )..add(const DatabasePropertyEvent.initial()),\n          child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>(\n            builder: (context, state) {\n              final items = state.fieldContexts\n                  .where((field) => field.fieldType == FieldType.DateTime)\n                  .map(\n                (fieldInfo) {\n                  return SizedBox(\n                    height: GridSize.popoverItemHeight,\n                    child: FlowyButton(\n                      text: FlowyText(\n                        fieldInfo.name,\n                        lineHeight: 1.0,\n                      ),\n                      onTap: () {\n                        onUpdated(fieldInfo.id);\n                        popoverMutex.close();\n                      },\n                      leftIcon: const FlowySvg(FlowySvgs.date_s),\n                      rightIcon: fieldInfo.id == fieldId\n                          ? const FlowySvg(FlowySvgs.check_s)\n                          : null,\n                    ),\n                  );\n                },\n              ).toList();\n\n              return SizedBox(\n                width: 200,\n                child: ListView.separated(\n                  shrinkWrap: true,\n                  itemBuilder: (_, index) => items[index],\n                  separatorBuilder: (_, __) =>\n                      VSpace(GridSize.typeOptionSeparatorHeight),\n                  itemCount: items.length,\n                ),\n              );\n            },\n          ),\n        );\n      },\n      child: SizedBox(\n        height: GridSize.popoverItemHeight,\n        child: FlowyButton(\n          margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),\n          text: FlowyText(\n            lineHeight: 1.0,\n            LocaleKeys.calendar_settings_layoutDateField.tr(),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass ShowWeekNumber extends StatelessWidget {\n  const ShowWeekNumber({\n    super.key,\n    required this.showWeekNumbers,\n    required this.onUpdated,\n  });\n\n  final bool showWeekNumbers;\n  final Function(bool showWeekNumbers) onUpdated;\n\n  @override\n  Widget build(BuildContext context) {\n    return _toggleItem(\n      onToggle: (showWeekNumbers) => onUpdated(!showWeekNumbers),\n      value: showWeekNumbers,\n      text: LocaleKeys.calendar_settings_showWeekNumbers.tr(),\n    );\n  }\n}\n\nclass ShowWeekends extends StatelessWidget {\n  const ShowWeekends({\n    super.key,\n    required this.showWeekends,\n    required this.onUpdated,\n  });\n\n  final bool showWeekends;\n  final Function(bool showWeekends) onUpdated;\n\n  @override\n  Widget build(BuildContext context) {\n    return _toggleItem(\n      onToggle: (showWeekends) => onUpdated(!showWeekends),\n      value: showWeekends,\n      text: LocaleKeys.calendar_settings_showWeekends.tr(),\n    );\n  }\n}\n\nclass FirstDayOfWeek extends StatelessWidget {\n  const FirstDayOfWeek({\n    super.key,\n    required this.firstDayOfWeek,\n    required this.popoverMutex,\n    required this.onUpdated,\n  });\n\n  final int firstDayOfWeek;\n  final PopoverMutex popoverMutex;\n  final Function(int firstDayOfWeek) onUpdated;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      direction: PopoverDirection.leftWithTopAligned,\n      constraints: BoxConstraints.loose(const Size(300, 400)),\n      triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,\n      mutex: popoverMutex,\n      offset: const Offset(-14, 0),\n      popupBuilder: (context) {\n        final symbols =\n            DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;\n        // starts from sunday\n        const len = 2;\n        final items = symbols.WEEKDAYS.take(len).indexed.map((entry) {\n          return StartFromButton(\n            title: entry.$2,\n            dayIndex: entry.$1,\n            isSelected: firstDayOfWeek == entry.$1,\n            onTap: (index) {\n              onUpdated(index);\n              popoverMutex.close();\n            },\n          );\n        }).toList();\n\n        return SizedBox(\n          width: 100,\n          child: ListView.separated(\n            shrinkWrap: true,\n            itemBuilder: (_, index) => items[index],\n            separatorBuilder: (_, __) =>\n                VSpace(GridSize.typeOptionSeparatorHeight),\n            itemCount: len,\n          ),\n        );\n      },\n      child: SizedBox(\n        height: GridSize.popoverItemHeight,\n        child: FlowyButton(\n          margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),\n          text: FlowyText(\n            lineHeight: 1.0,\n            LocaleKeys.calendar_settings_firstDayOfWeek.tr(),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nWidget _toggleItem({\n  required String text,\n  required bool value,\n  required void Function(bool) onToggle,\n}) {\n  return SizedBox(\n    height: GridSize.popoverItemHeight,\n    child: Padding(\n      padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),\n      child: Row(\n        children: [\n          FlowyText(text),\n          const Spacer(),\n          Toggle(\n            value: value,\n            onChanged: (value) => onToggle(value),\n            padding: EdgeInsets.zero,\n          ),\n        ],\n      ),\n    ),\n  );\n}\n\nenum CalendarLayoutSettingAction {\n  layoutField,\n  layoutType,\n  showWeekends,\n  firstDayOfWeek,\n  showWeekNumber,\n  showTimeLine,\n}\n\nclass StartFromButton extends StatelessWidget {\n  const StartFromButton({\n    super.key,\n    required this.title,\n    required this.dayIndex,\n    required this.onTap,\n    required this.isSelected,\n  });\n\n  final String title;\n  final int dayIndex;\n  final void Function(int) onTap;\n  final bool isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        text: FlowyText(\n          title,\n          lineHeight: 1.0,\n        ),\n        onTap: () => onTap(dayIndex),\n        rightIcon: isSelected ? const FlowySvg(FlowySvgs.check_s) : null,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_setting_bar.dart",
    "content": "import 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/view_database_button.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/setting_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\n\nclass CalendarSettingBar extends StatelessWidget {\n  const CalendarSettingBar({\n    super.key,\n    required this.databaseController,\n    required this.toggleExtension,\n  });\n\n  final DatabaseController databaseController;\n  final ToggleExtensionNotifier toggleExtension;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => FilterEditorBloc(\n        viewId: databaseController.viewId,\n        fieldController: databaseController.fieldController,\n      ),\n      child: ValueListenableBuilder<bool>(\n        valueListenable: databaseController.isLoading,\n        builder: (context, value, child) {\n          if (value) {\n            return const SizedBox.shrink();\n          }\n          final isReference =\n              Provider.of<ReferenceState?>(context)?.isReference ?? false;\n          return SizedBox(\n            height: 20,\n            child: Row(\n              mainAxisAlignment: MainAxisAlignment.end,\n              children: [\n                FilterButton(\n                  toggleExtension: toggleExtension,\n                ),\n                if (isReference) ...[\n                  const HSpace(2),\n                  ViewDatabaseButton(view: databaseController.view),\n                ],\n                const HSpace(2),\n                SettingButton(\n                  databaseController: databaseController,\n                ),\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/cell_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/grid_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flowy_infra/notifier.dart';\n\nimport '../application/row/row_service.dart';\n\ntypedef UpdateFieldNotifiedValue = FlowyResult<void, FlowyError>;\n\nclass CellListener {\n  CellListener({required this.rowId, required this.fieldId});\n\n  final RowId rowId;\n  final String fieldId;\n\n  PublishNotifier<UpdateFieldNotifiedValue>? _updateCellNotifier =\n      PublishNotifier();\n  DatabaseNotificationListener? _listener;\n\n  void start({required void Function(UpdateFieldNotifiedValue) onCellChanged}) {\n    _updateCellNotifier?.addPublishListener(onCellChanged);\n    _listener = DatabaseNotificationListener(\n      objectId: \"$rowId:$fieldId\",\n      handler: _handler,\n    );\n  }\n\n  void _handler(\n    DatabaseNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case DatabaseNotification.DidUpdateCell:\n        result.fold(\n          (payload) => _updateCellNotifier?.value = FlowyResult.success(null),\n          (error) => _updateCellNotifier?.value = FlowyResult.failure(error),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _listener?.stop();\n    _updateCellNotifier?.dispose();\n    _updateCellNotifier = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/cell_service.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nimport '../application/cell/cell_controller.dart';\n\nclass CellBackendService {\n  CellBackendService();\n\n  static Future<FlowyResult<void, FlowyError>> updateCell({\n    required String viewId,\n    required CellContext cellContext,\n    required String data,\n  }) {\n    final payload = CellChangesetPB()\n      ..viewId = viewId\n      ..fieldId = cellContext.fieldId\n      ..rowId = cellContext.rowId\n      ..cellChangeset = data;\n    return DatabaseEventUpdateCell(payload).send();\n  }\n\n  static Future<FlowyResult<CellPB, FlowyError>> getCell({\n    required String viewId,\n    required CellContext cellContext,\n  }) {\n    final payload = CellIdPB()\n      ..viewId = viewId\n      ..fieldId = cellContext.fieldId\n      ..rowId = cellContext.rowId;\n    return DatabaseEventGetCell(payload).send();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/checklist_cell_service.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:protobuf/protobuf.dart';\n\nclass ChecklistCellBackendService {\n  ChecklistCellBackendService({\n    required this.viewId,\n    required this.fieldId,\n    required this.rowId,\n  });\n\n  final String viewId;\n  final String fieldId;\n  final String rowId;\n\n  Future<FlowyResult<void, FlowyError>> create({\n    required String name,\n    int? index,\n  }) {\n    final insert = ChecklistCellInsertPB()..name = name;\n    if (index != null) {\n      insert.index = index;\n    }\n\n    final payload = ChecklistCellDataChangesetPB()\n      ..cellId = _makdeCellId()\n      ..insertTask.add(insert);\n\n    return DatabaseEventUpdateChecklistCell(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> delete({\n    required List<String> optionIds,\n  }) {\n    final payload = ChecklistCellDataChangesetPB()\n      ..cellId = _makdeCellId()\n      ..deleteTasks.addAll(optionIds);\n\n    return DatabaseEventUpdateChecklistCell(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> select({\n    required String optionId,\n  }) {\n    final payload = ChecklistCellDataChangesetPB()\n      ..cellId = _makdeCellId()\n      ..completedTasks.add(optionId);\n\n    return DatabaseEventUpdateChecklistCell(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> updateName({\n    required SelectOptionPB option,\n    required name,\n  }) {\n    option.freeze();\n    final newOption = option.rebuild((option) {\n      option.name = name;\n    });\n    final payload = ChecklistCellDataChangesetPB()\n      ..cellId = _makdeCellId()\n      ..updateTasks.add(newOption);\n\n    return DatabaseEventUpdateChecklistCell(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> reorder({\n    required fromTaskId,\n    required toTaskId,\n  }) {\n    final payload = ChecklistCellDataChangesetPB()\n      ..cellId = _makdeCellId()\n      ..reorder = \"$fromTaskId $toTaskId\";\n\n    return DatabaseEventUpdateChecklistCell(payload).send();\n  }\n\n  CellIdPB _makdeCellId() {\n    return CellIdPB()\n      ..viewId = viewId\n      ..fieldId = fieldId\n      ..rowId = rowId;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/database_view_service.dart",
    "content": "import 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nimport 'layout_service.dart';\n\nclass DatabaseViewBackendService {\n  DatabaseViewBackendService({required this.viewId});\n\n  final String viewId;\n\n  /// Returns the database id associated with the view.\n  Future<FlowyResult<String, FlowyError>> getDatabaseId() async {\n    final payload = DatabaseViewIdPB(value: viewId);\n    return DatabaseEventGetDatabaseId(payload)\n        .send()\n        .then((value) => value.map((l) => l.value));\n  }\n\n  static Future<FlowyResult<ViewPB, FlowyError>> updateLayout({\n    required String viewId,\n    required DatabaseLayoutPB layout,\n  }) {\n    final payload = UpdateViewPayloadPB.create()\n      ..viewId = viewId\n      ..layout = viewLayoutFromDatabaseLayout(layout);\n\n    return FolderEventUpdateView(payload).send();\n  }\n\n  Future<FlowyResult<DatabasePB, FlowyError>> openDatabase() async {\n    final payload = DatabaseViewIdPB(value: viewId);\n    return DatabaseEventGetDatabase(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> moveGroupRow({\n    required RowId fromRowId,\n    required String fromGroupId,\n    required String toGroupId,\n    RowId? toRowId,\n  }) {\n    final payload = MoveGroupRowPayloadPB.create()\n      ..viewId = viewId\n      ..fromRowId = fromRowId\n      ..fromGroupId = fromGroupId\n      ..toGroupId = toGroupId;\n\n    if (toRowId != null) {\n      payload.toRowId = toRowId;\n    }\n\n    return DatabaseEventMoveGroupRow(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> moveRow({\n    required String fromRowId,\n    required String toRowId,\n  }) {\n    final payload = MoveRowPayloadPB.create()\n      ..viewId = viewId\n      ..fromRowId = fromRowId\n      ..toRowId = toRowId;\n\n    return DatabaseEventMoveRow(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> moveGroup({\n    required String fromGroupId,\n    required String toGroupId,\n  }) {\n    final payload = MoveGroupPayloadPB.create()\n      ..viewId = viewId\n      ..fromGroupId = fromGroupId\n      ..toGroupId = toGroupId;\n\n    return DatabaseEventMoveGroup(payload).send();\n  }\n\n  Future<FlowyResult<List<FieldPB>, FlowyError>> getFields({\n    List<FieldIdPB>? fieldIds,\n  }) {\n    final payload = GetFieldPayloadPB.create()..viewId = viewId;\n\n    if (fieldIds != null) {\n      payload.fieldIds = RepeatedFieldIdPB(items: fieldIds);\n    }\n    return DatabaseEventGetFields(payload).send().then((result) {\n      return result.fold(\n        (l) => FlowyResult.success(l.items),\n        (r) => FlowyResult.failure(r),\n      );\n    });\n  }\n\n  Future<FlowyResult<DatabaseLayoutSettingPB, FlowyError>> getLayoutSetting(\n    DatabaseLayoutPB layoutType,\n  ) {\n    final payload = DatabaseLayoutMetaPB.create()\n      ..viewId = viewId\n      ..layout = layoutType;\n    return DatabaseEventGetLayoutSetting(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> updateLayoutSetting({\n    required DatabaseLayoutPB layoutType,\n    BoardLayoutSettingPB? boardLayoutSetting,\n    CalendarLayoutSettingPB? calendarLayoutSetting,\n  }) {\n    final payload = LayoutSettingChangesetPB.create()\n      ..viewId = viewId\n      ..layoutType = layoutType;\n\n    if (boardLayoutSetting != null) {\n      payload.board = boardLayoutSetting;\n    }\n\n    if (calendarLayoutSetting != null) {\n      payload.calendar = calendarLayoutSetting;\n    }\n\n    return DatabaseEventSetLayoutSetting(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> closeView() {\n    final request = ViewIdPB(value: viewId);\n    return FolderEventCloseView(request).send();\n  }\n\n  Future<FlowyResult<RepeatedGroupPB, FlowyError>> loadGroups() {\n    final payload = DatabaseViewIdPB(value: viewId);\n    return DatabaseEventGetGroups(payload).send();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/date_cell_service.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:fixnum/fixnum.dart';\n\nfinal class DateCellBackendService {\n  DateCellBackendService({\n    required String viewId,\n    required String fieldId,\n    required String rowId,\n  }) : cellId = CellIdPB()\n          ..viewId = viewId\n          ..fieldId = fieldId\n          ..rowId = rowId;\n\n  final CellIdPB cellId;\n\n  Future<FlowyResult<void, FlowyError>> update({\n    bool? includeTime,\n    bool? isRange,\n    DateTime? date,\n    DateTime? endDate,\n    String? reminderId,\n  }) {\n    final payload = DateCellChangesetPB()..cellId = cellId;\n\n    if (includeTime != null) {\n      payload.includeTime = includeTime;\n    }\n    if (isRange != null) {\n      payload.isRange = isRange;\n    }\n    if (date != null) {\n      final dateTimestamp = date.millisecondsSinceEpoch ~/ 1000;\n      payload.timestamp = Int64(dateTimestamp);\n    }\n    if (endDate != null) {\n      final dateTimestamp = endDate.millisecondsSinceEpoch ~/ 1000;\n      payload.endTimestamp = Int64(dateTimestamp);\n    }\n    if (reminderId != null) {\n      payload.reminderId = reminderId;\n    }\n\n    return DatabaseEventUpdateDateCell(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> clear() {\n    final payload = DateCellChangesetPB()\n      ..cellId = cellId\n      ..clearFlag = true;\n\n    return DatabaseEventUpdateDateCell(payload).send();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/field_backend_service.dart",
    "content": "import 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/plugins/database/domain/field_settings_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\n\n// This class is used for combining the\n// 1. FieldBackendService\n// 2. FieldSettingsBackendService\n// 3. TypeOptionBackendService\n//\n// including,\n// hide, delete, duplicated,\n// insertLeft, insertRight,\n// updateName\nclass FieldServices {\n  FieldServices({\n    required this.viewId,\n    required this.fieldId,\n  })  : fieldBackendService = FieldBackendService(\n          viewId: viewId,\n          fieldId: fieldId,\n        ),\n        fieldSettingsService = FieldSettingsBackendService(\n          viewId: viewId,\n        );\n\n  final String viewId;\n  final String fieldId;\n\n  final FieldBackendService fieldBackendService;\n  final FieldSettingsBackendService fieldSettingsService;\n\n  Future<void> hide() async {\n    await fieldSettingsService.updateFieldSettings(\n      fieldId: fieldId,\n      fieldVisibility: FieldVisibility.AlwaysHidden,\n    );\n  }\n\n  Future<void> show() async {\n    await fieldSettingsService.updateFieldSettings(\n      fieldId: fieldId,\n      fieldVisibility: FieldVisibility.AlwaysShown,\n    );\n  }\n\n  Future<void> delete() async {\n    await fieldBackendService.delete();\n  }\n\n  Future<void> duplicate() async {\n    await fieldBackendService.duplicate();\n  }\n\n  Future<void> insertLeft() async {\n    await FieldBackendService.createField(\n      viewId: viewId,\n      position: OrderObjectPositionPB(\n        position: OrderObjectPositionTypePB.Before,\n        objectId: fieldId,\n      ),\n    );\n  }\n\n  Future<void> insertRight() async {\n    await FieldBackendService.createField(\n      viewId: viewId,\n      position: OrderObjectPositionPB(\n        position: OrderObjectPositionTypePB.After,\n        objectId: fieldId,\n      ),\n    );\n  }\n\n  Future<void> updateName(String name) async {\n    await fieldBackendService.updateField(\n      name: name,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/field_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/grid_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flowy_infra/notifier.dart';\n\ntypedef UpdateFieldsNotifiedValue\n    = FlowyResult<DatabaseFieldChangesetPB, FlowyError>;\n\nclass FieldsListener {\n  FieldsListener({required this.viewId});\n\n  final String viewId;\n\n  PublishNotifier<UpdateFieldsNotifiedValue>? updateFieldsNotifier =\n      PublishNotifier();\n  DatabaseNotificationListener? _listener;\n\n  void start({\n    required void Function(UpdateFieldsNotifiedValue) onFieldsChanged,\n  }) {\n    updateFieldsNotifier?.addPublishListener(onFieldsChanged);\n    _listener = DatabaseNotificationListener(\n      objectId: viewId,\n      handler: _handler,\n    );\n  }\n\n  void _handler(\n    DatabaseNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case DatabaseNotification.DidUpdateFields:\n        result.fold(\n          (payload) => updateFieldsNotifier?.value =\n              FlowyResult.success(DatabaseFieldChangesetPB.fromBuffer(payload)),\n          (error) => updateFieldsNotifier?.value = FlowyResult.failure(error),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _listener?.stop();\n    updateFieldsNotifier?.dispose();\n    updateFieldsNotifier = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\n/// FieldService provides many field-related interfaces event functions. Check out\n/// `rust-lib/flowy-database/event_map.rs` for a list of events and their\n/// implementations.\nclass FieldBackendService {\n  FieldBackendService({required this.viewId, required this.fieldId});\n\n  final String viewId;\n  final String fieldId;\n\n  /// Create a field in a database view. The position will only be applicable\n  /// in this view; for other views it will be appended to the end\n  static Future<FlowyResult<FieldPB, FlowyError>> createField({\n    required String viewId,\n    FieldType fieldType = FieldType.RichText,\n    String? fieldName,\n    String? icon,\n    Uint8List? typeOptionData,\n    OrderObjectPositionPB? position,\n  }) {\n    final payload = CreateFieldPayloadPB(\n      viewId: viewId,\n      fieldType: fieldType,\n      fieldName: fieldName,\n      typeOptionData: typeOptionData,\n      fieldPosition: position,\n    );\n\n    return DatabaseEventCreateField(payload).send();\n  }\n\n  /// Reorder a field within a database view\n  static Future<FlowyResult<void, FlowyError>> moveField({\n    required String viewId,\n    required String fromFieldId,\n    required String toFieldId,\n  }) {\n    final payload = MoveFieldPayloadPB(\n      viewId: viewId,\n      fromFieldId: fromFieldId,\n      toFieldId: toFieldId,\n    );\n\n    return DatabaseEventMoveField(payload).send();\n  }\n\n  /// Delete a field\n  static Future<FlowyResult<void, FlowyError>> deleteField({\n    required String viewId,\n    required String fieldId,\n  }) {\n    final payload = DeleteFieldPayloadPB(\n      viewId: viewId,\n      fieldId: fieldId,\n    );\n\n    return DatabaseEventDeleteField(payload).send();\n  }\n\n  // Clear all data of all cells in a Field\n  static Future<FlowyResult<void, FlowyError>> clearField({\n    required String viewId,\n    required String fieldId,\n  }) {\n    final payload = ClearFieldPayloadPB(\n      viewId: viewId,\n      fieldId: fieldId,\n    );\n\n    return DatabaseEventClearField(payload).send();\n  }\n\n  /// Duplicate a field\n  static Future<FlowyResult<void, FlowyError>> duplicateField({\n    required String viewId,\n    required String fieldId,\n  }) {\n    final payload = DuplicateFieldPayloadPB(viewId: viewId, fieldId: fieldId);\n\n    return DatabaseEventDuplicateField(payload).send();\n  }\n\n  /// Update a field's properties\n  Future<FlowyResult<void, FlowyError>> updateField({\n    String? name,\n    String? icon,\n    bool? frozen,\n  }) {\n    final payload = FieldChangesetPB.create()\n      ..viewId = viewId\n      ..fieldId = fieldId;\n\n    if (name != null) {\n      payload.name = name;\n    }\n\n    if (icon != null) {\n      payload.icon = icon;\n    }\n\n    if (frozen != null) {\n      payload.frozen = frozen;\n    }\n\n    return DatabaseEventUpdateField(payload).send();\n  }\n\n  /// Change a field's type\n  static Future<FlowyResult<void, FlowyError>> updateFieldType({\n    required String viewId,\n    required String fieldId,\n    required FieldType fieldType,\n    String? fieldName,\n  }) {\n    final payload = UpdateFieldTypePayloadPB()\n      ..viewId = viewId\n      ..fieldId = fieldId\n      ..fieldType = fieldType;\n\n    // Only set if fieldName is not null\n    if (fieldName != null) {\n      payload.fieldName = fieldName;\n    }\n\n    return DatabaseEventUpdateFieldType(payload).send();\n  }\n\n  /// Update a field's type option data\n  static Future<FlowyResult<void, FlowyError>> updateFieldTypeOption({\n    required String viewId,\n    required String fieldId,\n    required List<int> typeOptionData,\n  }) {\n    final payload = TypeOptionChangesetPB.create()\n      ..viewId = viewId\n      ..fieldId = fieldId\n      ..typeOptionData = typeOptionData;\n\n    return DatabaseEventUpdateFieldTypeOption(payload).send();\n  }\n\n  static Future<FlowyResult<List<FieldPB>, FlowyError>> getFields({\n    required String viewId,\n  }) {\n    final payload = GetFieldPayloadPB.create()..viewId = viewId;\n\n    return DatabaseEventGetFields(payload).send().fold(\n          (repeated) => FlowySuccess(repeated.items),\n          (error) => FlowyFailure(error),\n        );\n  }\n\n  /// Returns the primary field of the view.\n  static Future<FlowyResult<FieldPB, FlowyError>> getPrimaryField({\n    required String viewId,\n  }) {\n    final payload = DatabaseViewIdPB.create()..value = viewId;\n    return DatabaseEventGetPrimaryField(payload).send();\n  }\n\n  Future<FlowyResult<FieldPB, FlowyError>> createBefore({\n    FieldType fieldType = FieldType.RichText,\n    String? fieldName,\n    Uint8List? typeOptionData,\n  }) {\n    return createField(\n      viewId: viewId,\n      fieldType: fieldType,\n      fieldName: fieldName,\n      typeOptionData: typeOptionData,\n      position: OrderObjectPositionPB(\n        position: OrderObjectPositionTypePB.Before,\n        objectId: fieldId,\n      ),\n    );\n  }\n\n  Future<FlowyResult<FieldPB, FlowyError>> createAfter({\n    FieldType fieldType = FieldType.RichText,\n    String? fieldName,\n    Uint8List? typeOptionData,\n  }) {\n    return createField(\n      viewId: viewId,\n      fieldType: fieldType,\n      fieldName: fieldName,\n      typeOptionData: typeOptionData,\n      position: OrderObjectPositionPB(\n        position: OrderObjectPositionTypePB.After,\n        objectId: fieldId,\n      ),\n    );\n  }\n\n  Future<FlowyResult<void, FlowyError>> updateType({\n    required FieldType fieldType,\n    String? fieldName,\n  }) =>\n      updateFieldType(\n        viewId: viewId,\n        fieldId: fieldId,\n        fieldType: fieldType,\n        fieldName: fieldName,\n      );\n\n  Future<FlowyResult<void, FlowyError>> delete() =>\n      deleteField(viewId: viewId, fieldId: fieldId);\n\n  Future<FlowyResult<void, FlowyError>> duplicate() =>\n      duplicateField(viewId: viewId, fieldId: fieldId);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/field_settings_listener.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/grid_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flowy_infra/notifier.dart';\n\ntypedef FieldSettingsValue = FlowyResult<FieldSettingsPB, FlowyError>;\n\nclass FieldSettingsListener {\n  FieldSettingsListener({required this.viewId});\n\n  final String viewId;\n\n  PublishNotifier<FieldSettingsValue>? _fieldSettingsNotifier =\n      PublishNotifier();\n  DatabaseNotificationListener? _listener;\n\n  void start({\n    required void Function(FieldSettingsValue) onFieldSettingsChanged,\n  }) {\n    _fieldSettingsNotifier?.addPublishListener(onFieldSettingsChanged);\n    _listener = DatabaseNotificationListener(\n      objectId: viewId,\n      handler: _handler,\n    );\n  }\n\n  void _handler(\n    DatabaseNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case DatabaseNotification.DidUpdateFieldSettings:\n        result.fold(\n          (payload) => _fieldSettingsNotifier?.value =\n              FlowyResult.success(FieldSettingsPB.fromBuffer(payload)),\n          (error) => _fieldSettingsNotifier?.value = FlowyResult.failure(error),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _listener?.stop();\n    _fieldSettingsNotifier?.dispose();\n    _fieldSettingsNotifier = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/field_settings_service.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass FieldSettingsBackendService {\n  FieldSettingsBackendService({required this.viewId});\n\n  final String viewId;\n\n  Future<FlowyResult<FieldSettingsPB, FlowyError>> getFieldSettings(\n    String fieldId,\n  ) {\n    final id = FieldIdPB(fieldId: fieldId);\n    final ids = RepeatedFieldIdPB()..items.add(id);\n    final payload = FieldIdsPB()\n      ..viewId = viewId\n      ..fieldIds = ids;\n\n    return DatabaseEventGetFieldSettings(payload).send().then((result) {\n      return result.fold(\n        (repeatedFieldSettings) {\n          final fieldSetting = repeatedFieldSettings.items.first;\n          if (!fieldSetting.hasVisibility()) {\n            fieldSetting.visibility = FieldVisibility.AlwaysShown;\n          }\n\n          return FlowyResult.success(fieldSetting);\n        },\n        (r) => FlowyResult.failure(r),\n      );\n    });\n  }\n\n  Future<FlowyResult<List<FieldSettingsPB>, FlowyError>> getAllFieldSettings() {\n    final payload = DatabaseViewIdPB()..value = viewId;\n\n    return DatabaseEventGetAllFieldSettings(payload).send().then((result) {\n      return result.fold(\n        (repeatedFieldSettings) {\n          final fieldSettings = <FieldSettingsPB>[];\n\n          for (final fieldSetting in repeatedFieldSettings.items) {\n            if (!fieldSetting.hasVisibility()) {\n              fieldSetting.visibility = FieldVisibility.AlwaysShown;\n            }\n            fieldSettings.add(fieldSetting);\n          }\n\n          return FlowyResult.success(fieldSettings);\n        },\n        (r) => FlowyResult.failure(r),\n      );\n    });\n  }\n\n  Future<FlowyResult<void, FlowyError>> updateFieldSettings({\n    required String fieldId,\n    FieldVisibility? fieldVisibility,\n    double? width,\n    bool? wrapCellContent,\n  }) {\n    final FieldSettingsChangesetPB payload = FieldSettingsChangesetPB.create()\n      ..viewId = viewId\n      ..fieldId = fieldId;\n\n    if (fieldVisibility != null) {\n      payload.visibility = fieldVisibility;\n    }\n\n    if (width != null) {\n      payload.width = width.round();\n    }\n\n    if (wrapCellContent != null) {\n      payload.wrapCellContent = wrapCellContent;\n    }\n\n    return DatabaseEventUpdateFieldSettings(payload).send();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/filter_listener.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/grid_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/filter_changeset.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flowy_infra/notifier.dart';\n\ntypedef UpdateFilterNotifiedValue\n    = FlowyResult<FilterChangesetNotificationPB, FlowyError>;\n\nclass FiltersListener {\n  FiltersListener({required this.viewId});\n\n  final String viewId;\n\n  PublishNotifier<UpdateFilterNotifiedValue>? _filterNotifier =\n      PublishNotifier();\n  DatabaseNotificationListener? _listener;\n\n  void start({\n    required void Function(UpdateFilterNotifiedValue) onFilterChanged,\n  }) {\n    _filterNotifier?.addPublishListener(onFilterChanged);\n    _listener = DatabaseNotificationListener(\n      objectId: viewId,\n      handler: _handler,\n    );\n  }\n\n  void _handler(\n    DatabaseNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case DatabaseNotification.DidUpdateFilter:\n        result.fold(\n          (payload) => _filterNotifier?.value = FlowyResult.success(\n            FilterChangesetNotificationPB.fromBuffer(payload),\n          ),\n          (error) => _filterNotifier?.value = FlowyResult.failure(error),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _listener?.stop();\n    _filterNotifier?.dispose();\n    _filterNotifier = null;\n  }\n}\n\nclass FilterListener {\n  FilterListener({required this.viewId, required this.filterId});\n\n  final String viewId;\n  final String filterId;\n\n  PublishNotifier<FilterPB>? _onUpdateNotifier = PublishNotifier();\n\n  DatabaseNotificationListener? _listener;\n\n  void start({void Function(FilterPB)? onUpdated}) {\n    _onUpdateNotifier?.addPublishListener((filter) {\n      onUpdated?.call(filter);\n    });\n\n    _listener = DatabaseNotificationListener(\n      objectId: viewId,\n      handler: _handler,\n    );\n  }\n\n  void handleChangeset(FilterChangesetNotificationPB changeset) {\n    final filters = changeset.filters.items;\n    final updatedIndex = filters.indexWhere(\n      (filter) => filter.id == filterId,\n    );\n    if (updatedIndex != -1) {\n      _onUpdateNotifier?.value = filters[updatedIndex];\n    }\n  }\n\n  void _handler(\n    DatabaseNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case DatabaseNotification.DidUpdateFilter:\n        result.fold(\n          (payload) => handleChangeset(\n            FilterChangesetNotificationPB.fromBuffer(payload),\n          ),\n          (error) {},\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _listener?.stop();\n    _onUpdateNotifier?.dispose();\n    _onUpdateNotifier = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:fixnum/fixnum.dart' as $fixnum;\n\nclass FilterBackendService {\n  const FilterBackendService({required this.viewId});\n\n  final String viewId;\n\n  Future<FlowyResult<List<FilterPB>, FlowyError>> getAllFilters() {\n    final payload = DatabaseViewIdPB()..value = viewId;\n\n    return DatabaseEventGetAllFilters(payload).send().then((result) {\n      return result.fold(\n        (repeated) => FlowyResult.success(repeated.items),\n        (r) => FlowyResult.failure(r),\n      );\n    });\n  }\n\n  Future<FlowyResult<void, FlowyError>> insertTextFilter({\n    required String fieldId,\n    String? filterId,\n    required TextFilterConditionPB condition,\n    required String content,\n  }) {\n    final filter = TextFilterPB()\n      ..condition = condition\n      ..content = content;\n\n    return filterId == null\n        ? insertFilter(\n            fieldId: fieldId,\n            fieldType: FieldType.RichText,\n            data: filter.writeToBuffer(),\n          )\n        : updateFilter(\n            filterId: filterId,\n            fieldId: fieldId,\n            fieldType: FieldType.RichText,\n            data: filter.writeToBuffer(),\n          );\n  }\n\n  Future<FlowyResult<void, FlowyError>> insertCheckboxFilter({\n    required String fieldId,\n    String? filterId,\n    required CheckboxFilterConditionPB condition,\n  }) {\n    final filter = CheckboxFilterPB()..condition = condition;\n\n    return filterId == null\n        ? insertFilter(\n            fieldId: fieldId,\n            fieldType: FieldType.Checkbox,\n            data: filter.writeToBuffer(),\n          )\n        : updateFilter(\n            filterId: filterId,\n            fieldId: fieldId,\n            fieldType: FieldType.Checkbox,\n            data: filter.writeToBuffer(),\n          );\n  }\n\n  Future<FlowyResult<void, FlowyError>> insertNumberFilter({\n    required String fieldId,\n    String? filterId,\n    required NumberFilterConditionPB condition,\n    String content = \"\",\n  }) {\n    final filter = NumberFilterPB()\n      ..condition = condition\n      ..content = content;\n\n    return filterId == null\n        ? insertFilter(\n            fieldId: fieldId,\n            fieldType: FieldType.Number,\n            data: filter.writeToBuffer(),\n          )\n        : updateFilter(\n            filterId: filterId,\n            fieldId: fieldId,\n            fieldType: FieldType.Number,\n            data: filter.writeToBuffer(),\n          );\n  }\n\n  Future<FlowyResult<void, FlowyError>> insertDateFilter({\n    required String fieldId,\n    required FieldType fieldType,\n    String? filterId,\n    required DateFilterConditionPB condition,\n    int? start,\n    int? end,\n    int? timestamp,\n  }) {\n    final filter = DateFilterPB()..condition = condition;\n\n    if (timestamp != null) {\n      filter.timestamp = $fixnum.Int64(timestamp);\n    }\n    if (start != null) {\n      filter.start = $fixnum.Int64(start);\n    }\n    if (end != null) {\n      filter.end = $fixnum.Int64(end);\n    }\n\n    return filterId == null\n        ? insertFilter(\n            fieldId: fieldId,\n            fieldType: fieldType,\n            data: filter.writeToBuffer(),\n          )\n        : updateFilter(\n            filterId: filterId,\n            fieldId: fieldId,\n            fieldType: fieldType,\n            data: filter.writeToBuffer(),\n          );\n  }\n\n  Future<FlowyResult<void, FlowyError>> insertURLFilter({\n    required String fieldId,\n    String? filterId,\n    required TextFilterConditionPB condition,\n    String content = \"\",\n  }) {\n    final filter = TextFilterPB()\n      ..condition = condition\n      ..content = content;\n\n    return filterId == null\n        ? insertFilter(\n            fieldId: fieldId,\n            fieldType: FieldType.URL,\n            data: filter.writeToBuffer(),\n          )\n        : updateFilter(\n            filterId: filterId,\n            fieldId: fieldId,\n            fieldType: FieldType.URL,\n            data: filter.writeToBuffer(),\n          );\n  }\n\n  Future<FlowyResult<void, FlowyError>> insertSelectOptionFilter({\n    required String fieldId,\n    required FieldType fieldType,\n    required SelectOptionFilterConditionPB condition,\n    String? filterId,\n    List<String> optionIds = const [],\n  }) {\n    final filter = SelectOptionFilterPB()\n      ..condition = condition\n      ..optionIds.addAll(optionIds);\n\n    return filterId == null\n        ? insertFilter(\n            fieldId: fieldId,\n            fieldType: fieldType,\n            data: filter.writeToBuffer(),\n          )\n        : updateFilter(\n            filterId: filterId,\n            fieldId: fieldId,\n            fieldType: fieldType,\n            data: filter.writeToBuffer(),\n          );\n  }\n\n  Future<FlowyResult<void, FlowyError>> insertChecklistFilter({\n    required String fieldId,\n    required ChecklistFilterConditionPB condition,\n    String? filterId,\n    List<String> optionIds = const [],\n  }) {\n    final filter = ChecklistFilterPB()..condition = condition;\n\n    return filterId == null\n        ? insertFilter(\n            fieldId: fieldId,\n            fieldType: FieldType.Checklist,\n            data: filter.writeToBuffer(),\n          )\n        : updateFilter(\n            filterId: filterId,\n            fieldId: fieldId,\n            fieldType: FieldType.Checklist,\n            data: filter.writeToBuffer(),\n          );\n  }\n\n  Future<FlowyResult<void, FlowyError>> insertTimeFilter({\n    required String fieldId,\n    String? filterId,\n    required NumberFilterConditionPB condition,\n    String content = \"\",\n  }) {\n    final filter = TimeFilterPB()\n      ..condition = condition\n      ..content = content;\n\n    return filterId == null\n        ? insertFilter(\n            fieldId: fieldId,\n            fieldType: FieldType.Time,\n            data: filter.writeToBuffer(),\n          )\n        : updateFilter(\n            filterId: filterId,\n            fieldId: fieldId,\n            fieldType: FieldType.Time,\n            data: filter.writeToBuffer(),\n          );\n  }\n\n  Future<FlowyResult<void, FlowyError>> insertFilter({\n    required String fieldId,\n    required FieldType fieldType,\n    required List<int> data,\n  }) async {\n    final filterData = FilterDataPB()\n      ..fieldId = fieldId\n      ..fieldType = fieldType\n      ..data = data;\n\n    final insertFilterPayload = InsertFilterPB()..data = filterData;\n\n    final payload = DatabaseSettingChangesetPB()\n      ..viewId = viewId\n      ..insertFilter = insertFilterPayload;\n\n    final result = await DatabaseEventUpdateDatabaseSetting(payload).send();\n    return result.fold(\n      (l) => FlowyResult.success(l),\n      (err) {\n        Log.error(err);\n        return FlowyResult.failure(err);\n      },\n    );\n  }\n\n  Future<FlowyResult<void, FlowyError>> updateFilter({\n    required String filterId,\n    required String fieldId,\n    required FieldType fieldType,\n    required List<int> data,\n  }) async {\n    final filterData = FilterDataPB()\n      ..fieldId = fieldId\n      ..fieldType = fieldType\n      ..data = data;\n\n    final updateFilterPayload = UpdateFilterDataPB()\n      ..filterId = filterId\n      ..data = filterData;\n\n    final payload = DatabaseSettingChangesetPB()\n      ..viewId = viewId\n      ..updateFilterData = updateFilterPayload;\n\n    final result = await DatabaseEventUpdateDatabaseSetting(payload).send();\n    return result.fold(\n      (l) => FlowyResult.success(l),\n      (err) {\n        Log.error(err);\n        return FlowyResult.failure(err);\n      },\n    );\n  }\n\n  Future<FlowyResult<void, FlowyError>> insertMediaFilter({\n    required String fieldId,\n    String? filterId,\n    required MediaFilterConditionPB condition,\n    String content = \"\",\n  }) {\n    final filter = MediaFilterPB()\n      ..condition = condition\n      ..content = content;\n\n    return filterId == null\n        ? insertFilter(\n            fieldId: fieldId,\n            fieldType: FieldType.Media,\n            data: filter.writeToBuffer(),\n          )\n        : updateFilter(\n            filterId: filterId,\n            fieldId: fieldId,\n            fieldType: FieldType.Media,\n            data: filter.writeToBuffer(),\n          );\n  }\n\n  Future<FlowyResult<void, FlowyError>> deleteFilter({\n    required String filterId,\n  }) async {\n    final deleteFilterPayload = DeleteFilterPB()..filterId = filterId;\n\n    final payload = DatabaseSettingChangesetPB()\n      ..viewId = viewId\n      ..deleteFilter = deleteFilterPayload;\n\n    final result = await DatabaseEventUpdateDatabaseSetting(payload).send();\n    return result.fold(\n      (l) => FlowyResult.success(l),\n      (err) {\n        Log.error(err);\n        return FlowyResult.failure(err);\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/group_listener.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/grid_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/group_changeset.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flowy_infra/notifier.dart';\n\ntypedef GroupUpdateValue = FlowyResult<GroupChangesPB, FlowyError>;\ntypedef GroupByNewFieldValue = FlowyResult<List<GroupPB>, FlowyError>;\n\nclass DatabaseGroupListener {\n  DatabaseGroupListener(this.viewId);\n\n  final String viewId;\n\n  PublishNotifier<GroupUpdateValue>? _numOfGroupsNotifier = PublishNotifier();\n  PublishNotifier<GroupByNewFieldValue>? _groupByFieldNotifier =\n      PublishNotifier();\n  DatabaseNotificationListener? _listener;\n\n  void start({\n    required void Function(GroupUpdateValue) onNumOfGroupsChanged,\n    required void Function(GroupByNewFieldValue) onGroupByNewField,\n  }) {\n    _numOfGroupsNotifier?.addPublishListener(onNumOfGroupsChanged);\n    _groupByFieldNotifier?.addPublishListener(onGroupByNewField);\n    _listener = DatabaseNotificationListener(\n      objectId: viewId,\n      handler: _handler,\n    );\n  }\n\n  void _handler(\n    DatabaseNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case DatabaseNotification.DidUpdateNumOfGroups:\n        result.fold(\n          (payload) => _numOfGroupsNotifier?.value =\n              FlowyResult.success(GroupChangesPB.fromBuffer(payload)),\n          (error) => _numOfGroupsNotifier?.value = FlowyResult.failure(error),\n        );\n        break;\n      case DatabaseNotification.DidGroupByField:\n        result.fold(\n          (payload) => _groupByFieldNotifier?.value = FlowyResult.success(\n            GroupChangesPB.fromBuffer(payload).initialGroups,\n          ),\n          (error) => _groupByFieldNotifier?.value = FlowyResult.failure(error),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _listener?.stop();\n    _numOfGroupsNotifier?.dispose();\n    _numOfGroupsNotifier = null;\n\n    _groupByFieldNotifier?.dispose();\n    _groupByFieldNotifier = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/group_service.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass GroupBackendService {\n  GroupBackendService(this.viewId);\n\n  final String viewId;\n\n  Future<FlowyResult<void, FlowyError>> groupByField({\n    required String fieldId,\n    required List<int> settingContent,\n  }) {\n    final payload = GroupByFieldPayloadPB.create()\n      ..viewId = viewId\n      ..fieldId = fieldId\n      ..settingContent = settingContent;\n\n    return DatabaseEventSetGroupByField(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> updateGroup({\n    required String groupId,\n    String? name,\n    bool? visible,\n  }) {\n    final payload = UpdateGroupPB.create()\n      ..viewId = viewId\n      ..groupId = groupId;\n\n    if (name != null) {\n      payload.name = name;\n    }\n    if (visible != null) {\n      payload.visible = visible;\n    }\n    return DatabaseEventUpdateGroup(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> createGroup({\n    required String name,\n    String groupConfigId = \"\",\n  }) {\n    final payload = CreateGroupPayloadPB.create()\n      ..viewId = viewId\n      ..name = name;\n\n    return DatabaseEventCreateGroup(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> deleteGroup({\n    required String groupId,\n  }) {\n    final payload = DeleteGroupPayloadPB.create()\n      ..viewId = viewId\n      ..groupId = groupId;\n\n    return DatabaseEventDeleteGroup(payload).send();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/layout_service.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\n\nViewLayoutPB viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {\n  switch (databaseLayout) {\n    case DatabaseLayoutPB.Board:\n      return ViewLayoutPB.Board;\n    case DatabaseLayoutPB.Calendar:\n      return ViewLayoutPB.Calendar;\n    case DatabaseLayoutPB.Grid:\n      return ViewLayoutPB.Grid;\n    default:\n      throw UnimplementedError;\n  }\n}\n\nDatabaseLayoutPB databaseLayoutFromViewLayout(ViewLayoutPB viewLayout) {\n  switch (viewLayout) {\n    case ViewLayoutPB.Board:\n      return DatabaseLayoutPB.Board;\n    case ViewLayoutPB.Calendar:\n      return DatabaseLayoutPB.Calendar;\n    case ViewLayoutPB.Grid:\n      return DatabaseLayoutPB.Grid;\n    default:\n      throw UnimplementedError;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/layout_setting_listener.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/grid_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flowy_infra/notifier.dart';\n\ntypedef LayoutSettingsValue<T> = FlowyResult<T, FlowyError>;\n\nclass DatabaseLayoutSettingListener {\n  DatabaseLayoutSettingListener(this.viewId);\n\n  final String viewId;\n\n  PublishNotifier<LayoutSettingsValue<DatabaseLayoutSettingPB>>?\n      _settingNotifier = PublishNotifier();\n  DatabaseNotificationListener? _listener;\n\n  void start({\n    required void Function(LayoutSettingsValue<DatabaseLayoutSettingPB>)\n        onLayoutChanged,\n  }) {\n    _settingNotifier?.addPublishListener(onLayoutChanged);\n    _listener = DatabaseNotificationListener(\n      objectId: viewId,\n      handler: _handler,\n    );\n  }\n\n  void _handler(\n    DatabaseNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case DatabaseNotification.DidUpdateLayoutSettings:\n        result.fold(\n          (payload) => _settingNotifier?.value =\n              FlowyResult.success(DatabaseLayoutSettingPB.fromBuffer(payload)),\n          (error) => _settingNotifier?.value = FlowyResult.failure(error),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _listener?.stop();\n    _settingNotifier?.dispose();\n    _settingNotifier = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/row_listener.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/grid_notification.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\ntypedef DidFetchRowCallback = void Function(DidFetchRowPB);\ntypedef RowMetaCallback = void Function(RowMetaPB);\n\nclass RowListener {\n  RowListener(this.rowId);\n\n  final String rowId;\n\n  DidFetchRowCallback? _onRowFetchedCallback;\n  RowMetaCallback? _onMetaChangedCallback;\n  DatabaseNotificationListener? _listener;\n\n  /// OnMetaChanged will be called when the row meta is changed.\n  /// OnRowFetched will be called when the row is fetched from remote storage\n  void start({\n    RowMetaCallback? onMetaChanged,\n    DidFetchRowCallback? onRowFetched,\n  }) {\n    _onMetaChangedCallback = onMetaChanged;\n    _onRowFetchedCallback = onRowFetched;\n    _listener = DatabaseNotificationListener(\n      objectId: rowId,\n      handler: _handler,\n    );\n  }\n\n  void _handler(\n    DatabaseNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case DatabaseNotification.DidUpdateRowMeta:\n        result.fold(\n          (payload) {\n            if (_onMetaChangedCallback != null) {\n              _onMetaChangedCallback!(RowMetaPB.fromBuffer(payload));\n            }\n          },\n          (error) => Log.error(error),\n        );\n        break;\n      case DatabaseNotification.DidFetchRow:\n        result.fold(\n          (payload) {\n            if (_onRowFetchedCallback != null) {\n              _onRowFetchedCallback!(DidFetchRowPB.fromBuffer(payload));\n            }\n          },\n          (error) => Log.error(error),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _listener?.stop();\n    _onMetaChangedCallback = null;\n    _onRowFetchedCallback = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/row_meta_listener.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/grid_notification.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\ntypedef RowMetaCallback = void Function(RowMetaPB);\n\nclass RowMetaListener {\n  RowMetaListener(this.rowId);\n\n  final String rowId;\n\n  RowMetaCallback? _callback;\n  DatabaseNotificationListener? _listener;\n\n  void start({required RowMetaCallback callback}) {\n    _callback = callback;\n    _listener = DatabaseNotificationListener(\n      objectId: rowId,\n      handler: _handler,\n    );\n  }\n\n  void _handler(\n    DatabaseNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case DatabaseNotification.DidUpdateRowMeta:\n        result.fold(\n          (payload) {\n            if (_callback != null) {\n              _callback!(RowMetaPB.fromBuffer(payload));\n            }\n          },\n          (error) => Log.error(error),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _listener?.stop();\n    _callback = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/select_option_cell_service.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:nanoid/nanoid.dart';\n\nclass SelectOptionCellBackendService {\n  SelectOptionCellBackendService({\n    required this.viewId,\n    required this.fieldId,\n    required this.rowId,\n  });\n\n  final String viewId;\n  final String fieldId;\n  final String rowId;\n\n  Future<FlowyResult<void, FlowyError>> create({\n    required String name,\n    SelectOptionColorPB? color,\n    bool isSelected = true,\n  }) {\n    final option = SelectOptionPB()\n      ..id = nanoid(4)\n      ..name = name;\n    if (color != null) {\n      option.color = color;\n    }\n\n    final payload = RepeatedSelectOptionPayload()\n      ..viewId = viewId\n      ..fieldId = fieldId\n      ..rowId = rowId\n      ..items.add(option);\n\n    return DatabaseEventInsertOrUpdateSelectOption(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> update({\n    required SelectOptionPB option,\n  }) {\n    final payload = RepeatedSelectOptionPayload()\n      ..items.add(option)\n      ..viewId = viewId\n      ..fieldId = fieldId\n      ..rowId = rowId;\n\n    return DatabaseEventInsertOrUpdateSelectOption(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> delete({\n    required Iterable<SelectOptionPB> options,\n  }) {\n    final payload = RepeatedSelectOptionPayload()\n      ..items.addAll(options)\n      ..viewId = viewId\n      ..fieldId = fieldId\n      ..rowId = rowId;\n\n    return DatabaseEventDeleteSelectOption(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> select({\n    required Iterable<String> optionIds,\n  }) {\n    final payload = SelectOptionCellChangesetPB()\n      ..cellIdentifier = _cellIdentifier()\n      ..insertOptionIds.addAll(optionIds);\n\n    return DatabaseEventUpdateSelectOptionCell(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> unselect({\n    required Iterable<String> optionIds,\n  }) {\n    final payload = SelectOptionCellChangesetPB()\n      ..cellIdentifier = _cellIdentifier()\n      ..deleteOptionIds.addAll(optionIds);\n\n    return DatabaseEventUpdateSelectOptionCell(payload).send();\n  }\n\n  CellIdPB _cellIdentifier() {\n    return CellIdPB()\n      ..viewId = viewId\n      ..fieldId = fieldId\n      ..rowId = rowId;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/sort_listener.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/grid_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flowy_infra/notifier.dart';\n\ntypedef SortNotifiedValue\n    = FlowyResult<SortChangesetNotificationPB, FlowyError>;\n\nclass SortsListener {\n  SortsListener({required this.viewId});\n\n  final String viewId;\n\n  PublishNotifier<SortNotifiedValue>? _notifier = PublishNotifier();\n  DatabaseNotificationListener? _listener;\n\n  void start({\n    required void Function(SortNotifiedValue) onSortChanged,\n  }) {\n    _notifier?.addPublishListener(onSortChanged);\n    _listener = DatabaseNotificationListener(\n      objectId: viewId,\n      handler: _handler,\n    );\n  }\n\n  void _handler(\n    DatabaseNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case DatabaseNotification.DidUpdateSort:\n        result.fold(\n          (payload) => _notifier?.value = FlowyResult.success(\n            SortChangesetNotificationPB.fromBuffer(payload),\n          ),\n          (error) => _notifier?.value = FlowyResult.failure(error),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _listener?.stop();\n    _notifier?.dispose();\n    _notifier = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/sort_service.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass SortBackendService {\n  SortBackendService({required this.viewId});\n\n  final String viewId;\n\n  Future<FlowyResult<List<SortPB>, FlowyError>> getAllSorts() {\n    final payload = DatabaseViewIdPB()..value = viewId;\n\n    return DatabaseEventGetAllSorts(payload).send().then((result) {\n      return result.fold(\n        (repeated) => FlowyResult.success(repeated.items),\n        (r) => FlowyResult.failure(r),\n      );\n    });\n  }\n\n  Future<FlowyResult<void, FlowyError>> updateSort({\n    required String sortId,\n    required String fieldId,\n    required SortConditionPB condition,\n  }) {\n    final insertSortPayload = UpdateSortPayloadPB.create()\n      ..viewId = viewId\n      ..sortId = sortId\n      ..fieldId = fieldId\n      ..condition = condition;\n\n    final payload = DatabaseSettingChangesetPB.create()\n      ..viewId = viewId\n      ..updateSort = insertSortPayload;\n    return DatabaseEventUpdateDatabaseSetting(payload).send().then((result) {\n      return result.fold(\n        (l) => FlowyResult.success(l),\n        (err) {\n          Log.error(err);\n          return FlowyResult.failure(err);\n        },\n      );\n    });\n  }\n\n  Future<FlowyResult<void, FlowyError>> insertSort({\n    required String fieldId,\n    required SortConditionPB condition,\n  }) {\n    final insertSortPayload = UpdateSortPayloadPB.create()\n      ..fieldId = fieldId\n      ..viewId = viewId\n      ..condition = condition;\n\n    final payload = DatabaseSettingChangesetPB.create()\n      ..viewId = viewId\n      ..updateSort = insertSortPayload;\n    return DatabaseEventUpdateDatabaseSetting(payload).send().then((result) {\n      return result.fold(\n        (l) => FlowyResult.success(l),\n        (err) {\n          Log.error(err);\n          return FlowyResult.failure(err);\n        },\n      );\n    });\n  }\n\n  Future<FlowyResult<void, FlowyError>> reorderSort({\n    required String fromSortId,\n    required String toSortId,\n  }) {\n    final payload = DatabaseSettingChangesetPB()\n      ..viewId = viewId\n      ..reorderSort = (ReorderSortPayloadPB()\n        ..viewId = viewId\n        ..fromSortId = fromSortId\n        ..toSortId = toSortId);\n\n    return DatabaseEventUpdateDatabaseSetting(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> deleteSort({\n    required String sortId,\n  }) {\n    final deleteSortPayload = DeleteSortPayloadPB.create()\n      ..sortId = sortId\n      ..viewId = viewId;\n\n    final payload = DatabaseSettingChangesetPB.create()\n      ..viewId = viewId\n      ..deleteSort = deleteSortPayload;\n\n    return DatabaseEventUpdateDatabaseSetting(payload).send().then((result) {\n      return result.fold(\n        (l) => FlowyResult.success(l),\n        (err) {\n          Log.error(err);\n          return FlowyResult.failure(err);\n        },\n      );\n    });\n  }\n\n  Future<FlowyResult<void, FlowyError>> deleteAllSorts() {\n    final payload = DatabaseViewIdPB(value: viewId);\n    return DatabaseEventDeleteAllSorts(payload).send().then((result) {\n      return result.fold(\n        (l) => FlowyResult.success(l),\n        (err) {\n          Log.error(err);\n          return FlowyResult.failure(err);\n        },\n      );\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/domain/type_option_service.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass TypeOptionBackendService {\n  TypeOptionBackendService({\n    required this.viewId,\n    required this.fieldId,\n  });\n\n  final String viewId;\n  final String fieldId;\n\n  Future<FlowyResult<SelectOptionPB, FlowyError>> newOption({\n    required String name,\n  }) {\n    final payload = CreateSelectOptionPayloadPB.create()\n      ..optionName = name\n      ..viewId = viewId\n      ..fieldId = fieldId;\n\n    return DatabaseEventCreateSelectOption(payload).send();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart",
    "content": "import 'package:appflowy/plugins/database/application/calculations/calculations_listener.dart';\nimport 'package:appflowy/plugins/database/application/calculations/calculations_service.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'calculations_bloc.freezed.dart';\n\nclass CalculationsBloc extends Bloc<CalculationsEvent, CalculationsState> {\n  CalculationsBloc({\n    required this.viewId,\n    required FieldController fieldController,\n  })  : _fieldController = fieldController,\n        _calculationsListener = CalculationsListener(viewId: viewId),\n        _calculationsService = CalculationsBackendService(viewId: viewId),\n        super(CalculationsState.initial()) {\n    _dispatch();\n  }\n\n  final String viewId;\n  final FieldController _fieldController;\n  final CalculationsListener _calculationsListener;\n  late final CalculationsBackendService _calculationsService;\n\n  @override\n  Future<void> close() async {\n    _fieldController.removeListener(onFieldsListener: _onReceiveFields);\n    await _calculationsListener.stop();\n    await super.close();\n  }\n\n  void _dispatch() {\n    on<CalculationsEvent>((event, emit) async {\n      await event.when(\n        started: () async {\n          _startListening();\n          await _getAllCalculations();\n\n          if (!isClosed) {\n            add(\n              CalculationsEvent.didReceiveFieldUpdate(\n                _fieldController.fieldInfos,\n              ),\n            );\n          }\n        },\n        didReceiveFieldUpdate: (fields) async {\n          emit(\n            state.copyWith(\n              fields: fields\n                  .where(\n                    (e) =>\n                        e.visibility != null &&\n                        e.visibility != FieldVisibility.AlwaysHidden,\n                  )\n                  .toList(),\n            ),\n          );\n        },\n        didReceiveCalculationsUpdate: (calculationsMap) async {\n          emit(\n            state.copyWith(\n              calculationsByFieldId: calculationsMap,\n            ),\n          );\n        },\n        updateCalculationType: (fieldId, type, calculationId) async {\n          await _calculationsService.updateCalculation(\n            fieldId,\n            type,\n            calculationId: calculationId,\n          );\n        },\n        removeCalculation: (fieldId, calculationId) async {\n          await _calculationsService.removeCalculation(fieldId, calculationId);\n        },\n      );\n    });\n  }\n\n  void _startListening() {\n    _fieldController.addListener(\n      listenWhen: () => !isClosed,\n      onReceiveFields: _onReceiveFields,\n    );\n\n    _calculationsListener.start(\n      onCalculationChanged: (changesetOrFailure) {\n        if (isClosed) {\n          return;\n        }\n\n        changesetOrFailure.fold(\n          (changeset) {\n            final calculationsMap = {...state.calculationsByFieldId};\n            if (changeset.insertCalculations.isNotEmpty) {\n              for (final insert in changeset.insertCalculations) {\n                calculationsMap[insert.fieldId] = insert;\n              }\n            }\n\n            if (changeset.updateCalculations.isNotEmpty) {\n              for (final update in changeset.updateCalculations) {\n                calculationsMap.removeWhere((key, _) => key == update.fieldId);\n                calculationsMap.addAll({update.fieldId: update});\n              }\n            }\n\n            if (changeset.deleteCalculations.isNotEmpty) {\n              for (final delete in changeset.deleteCalculations) {\n                calculationsMap.removeWhere((key, _) => key == delete.fieldId);\n              }\n            }\n\n            add(\n              CalculationsEvent.didReceiveCalculationsUpdate(\n                calculationsMap,\n              ),\n            );\n          },\n          (_) => null,\n        );\n      },\n    );\n  }\n\n  void _onReceiveFields(List<FieldInfo> fields) =>\n      add(CalculationsEvent.didReceiveFieldUpdate(fields));\n\n  Future<void> _getAllCalculations() async {\n    final calculationsOrFailure = await _calculationsService.getCalculations();\n\n    if (isClosed) {\n      return;\n    }\n\n    final RepeatedCalculationsPB? calculations =\n        calculationsOrFailure.fold((s) => s, (e) => null);\n    if (calculations != null) {\n      final calculationMap = <String, CalculationPB>{};\n      for (final calculation in calculations.items) {\n        calculationMap[calculation.fieldId] = calculation;\n      }\n\n      add(CalculationsEvent.didReceiveCalculationsUpdate(calculationMap));\n    }\n  }\n}\n\n@freezed\nclass CalculationsEvent with _$CalculationsEvent {\n  const factory CalculationsEvent.started() = _Started;\n\n  const factory CalculationsEvent.didReceiveFieldUpdate(\n    List<FieldInfo> fields,\n  ) = _DidReceiveFieldUpdate;\n\n  const factory CalculationsEvent.didReceiveCalculationsUpdate(\n    Map<String, CalculationPB> calculationsByFieldId,\n  ) = _DidReceiveCalculationsUpdate;\n\n  const factory CalculationsEvent.updateCalculationType(\n    String fieldId,\n    CalculationType type, {\n    @Default(null) String? calculationId,\n  }) = _UpdateCalculationType;\n\n  const factory CalculationsEvent.removeCalculation(\n    String fieldId,\n    String calculationId,\n  ) = _RemoveCalculation;\n}\n\n@freezed\nclass CalculationsState with _$CalculationsState {\n  const factory CalculationsState({\n    required List<FieldInfo> fields,\n    required Map<String, CalculationPB> calculationsByFieldId,\n  }) = _CalculationsState;\n\n  factory CalculationsState.initial() => const CalculationsState(\n        fields: [],\n        calculationsByFieldId: {},\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/field_type_calc_ext.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\n\nextension AvailableCalculations on FieldType {\n  List<CalculationType> calculationsForFieldType() {\n    final calculationTypes = [\n      CalculationType.Count,\n    ];\n\n    // These FieldTypes cannot be empty, or might hold secondary\n    // data causing them to be seen as not empty when in fact they\n    // are empty.\n    if (![\n      FieldType.URL,\n      FieldType.Checkbox,\n      FieldType.LastEditedTime,\n      FieldType.CreatedTime,\n    ].contains(this)) {\n      calculationTypes.addAll([\n        CalculationType.CountEmpty,\n        CalculationType.CountNonEmpty,\n      ]);\n    }\n\n    switch (this) {\n      case FieldType.Number:\n        calculationTypes.addAll([\n          CalculationType.Sum,\n          CalculationType.Average,\n          CalculationType.Min,\n          CalculationType.Max,\n          CalculationType.Median,\n        ]);\n        break;\n      default:\n        break;\n    }\n\n    return calculationTypes;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_editor_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy/plugins/database/domain/filter_service.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'filter_editor_bloc.freezed.dart';\n\nclass FilterEditorBloc extends Bloc<FilterEditorEvent, FilterEditorState> {\n  FilterEditorBloc({required this.viewId, required this.fieldController})\n      : _filterBackendSvc = FilterBackendService(viewId: viewId),\n        super(\n          FilterEditorState.initial(\n            viewId,\n            fieldController.filters,\n            _getCreatableFilter(fieldController.fieldInfos),\n          ),\n        ) {\n    _dispatch();\n    _startListening();\n  }\n\n  final String viewId;\n  final FieldController fieldController;\n  final FilterBackendService _filterBackendSvc;\n\n  void Function(List<DatabaseFilter>)? _onFilterFn;\n  void Function(List<FieldInfo>)? _onFieldFn;\n\n  void _dispatch() {\n    on<FilterEditorEvent>(\n      (event, emit) async {\n        await event.when(\n          didReceiveFilters: (filters) {\n            emit(state.copyWith(filters: filters));\n          },\n          didReceiveFields: (List<FieldInfo> fields) {\n            emit(\n              state.copyWith(\n                fields: _getCreatableFilter(fields),\n              ),\n            );\n          },\n          createFilter: (field) {\n            return _createDefaultFilter(null, field);\n          },\n          changeFilteringField: (filterId, field) {\n            return _createDefaultFilter(filterId, field);\n          },\n          updateFilter: (filter) {\n            return _filterBackendSvc.updateFilter(\n              filterId: filter.filterId,\n              fieldId: filter.fieldId,\n              fieldType: filter.fieldType,\n              data: filter.writeToBuffer(),\n            );\n          },\n          deleteFilter: (filterId) async {\n            return _filterBackendSvc.deleteFilter(filterId: filterId);\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _onFilterFn = (filters) {\n      add(FilterEditorEvent.didReceiveFilters(filters));\n    };\n\n    _onFieldFn = (fields) {\n      add(FilterEditorEvent.didReceiveFields(fields));\n    };\n\n    fieldController.addListener(\n      onFilters: _onFilterFn,\n      onReceiveFields: _onFieldFn,\n    );\n  }\n\n  @override\n  Future<void> close() async {\n    if (_onFilterFn != null) {\n      fieldController.removeListener(onFiltersListener: _onFilterFn!);\n      _onFilterFn = null;\n    }\n    if (_onFieldFn != null) {\n      fieldController.removeListener(onFieldsListener: _onFieldFn!);\n      _onFieldFn = null;\n    }\n    return super.close();\n  }\n\n  Future<FlowyResult<void, FlowyError>> _createDefaultFilter(\n    String? filterId,\n    FieldInfo field,\n  ) async {\n    final fieldId = field.id;\n    switch (field.fieldType) {\n      case FieldType.Checkbox:\n        return _filterBackendSvc.insertCheckboxFilter(\n          filterId: filterId,\n          fieldId: fieldId,\n          condition: CheckboxFilterConditionPB.IsChecked,\n        );\n      case FieldType.DateTime:\n      case FieldType.LastEditedTime:\n      case FieldType.CreatedTime:\n        final now = DateTime.now();\n        final timestamp =\n            DateTime(now.year, now.month, now.day).millisecondsSinceEpoch ~/\n                1000;\n        return _filterBackendSvc.insertDateFilter(\n          filterId: filterId,\n          fieldId: fieldId,\n          fieldType: field.fieldType,\n          condition: DateFilterConditionPB.DateStartsOn,\n          timestamp: timestamp,\n        );\n      case FieldType.MultiSelect:\n        return _filterBackendSvc.insertSelectOptionFilter(\n          filterId: filterId,\n          fieldId: fieldId,\n          condition: SelectOptionFilterConditionPB.OptionContains,\n          fieldType: FieldType.MultiSelect,\n        );\n      case FieldType.Checklist:\n        return _filterBackendSvc.insertChecklistFilter(\n          filterId: filterId,\n          fieldId: fieldId,\n          condition: ChecklistFilterConditionPB.IsIncomplete,\n        );\n      case FieldType.Number:\n        return _filterBackendSvc.insertNumberFilter(\n          filterId: filterId,\n          fieldId: fieldId,\n          condition: NumberFilterConditionPB.Equal,\n        );\n      case FieldType.Time:\n        return _filterBackendSvc.insertTimeFilter(\n          filterId: filterId,\n          fieldId: fieldId,\n          condition: NumberFilterConditionPB.Equal,\n        );\n      case FieldType.RichText:\n        return _filterBackendSvc.insertTextFilter(\n          filterId: filterId,\n          fieldId: fieldId,\n          condition: TextFilterConditionPB.TextContains,\n          content: '',\n        );\n      case FieldType.SingleSelect:\n        return _filterBackendSvc.insertSelectOptionFilter(\n          filterId: filterId,\n          fieldId: fieldId,\n          condition: SelectOptionFilterConditionPB.OptionIs,\n          fieldType: FieldType.SingleSelect,\n        );\n      case FieldType.URL:\n        return _filterBackendSvc.insertURLFilter(\n          filterId: filterId,\n          fieldId: fieldId,\n          condition: TextFilterConditionPB.TextContains,\n        );\n      case FieldType.Media:\n        return _filterBackendSvc.insertMediaFilter(\n          filterId: filterId,\n          fieldId: fieldId,\n          condition: MediaFilterConditionPB.MediaIsNotEmpty,\n        );\n      default:\n        throw UnimplementedError();\n    }\n  }\n}\n\n@freezed\nclass FilterEditorEvent with _$FilterEditorEvent {\n  const factory FilterEditorEvent.didReceiveFilters(\n    List<DatabaseFilter> filters,\n  ) = _DidReceiveFilters;\n  const factory FilterEditorEvent.didReceiveFields(List<FieldInfo> fields) =\n      _DidReceiveFields;\n  const factory FilterEditorEvent.createFilter(FieldInfo field) = _CreateFilter;\n  const factory FilterEditorEvent.updateFilter(DatabaseFilter filter) =\n      _UpdateFilter;\n  const factory FilterEditorEvent.changeFilteringField(\n    String filterId,\n    FieldInfo field,\n  ) = _ChangeFilteringField;\n  const factory FilterEditorEvent.deleteFilter(String filterId) = _DeleteFilter;\n}\n\n@freezed\nclass FilterEditorState with _$FilterEditorState {\n  const factory FilterEditorState({\n    required String viewId,\n    required List<DatabaseFilter> filters,\n    required List<FieldInfo> fields,\n  }) = _FilterEditorState;\n\n  factory FilterEditorState.initial(\n    String viewId,\n    List<DatabaseFilter> filterInfos,\n    List<FieldInfo> fields,\n  ) =>\n      FilterEditorState(\n        viewId: viewId,\n        filters: filterInfos,\n        fields: fields,\n      );\n}\n\nList<FieldInfo> _getCreatableFilter(List<FieldInfo> fieldInfos) {\n  final List<FieldInfo> creatableFields = List.from(fieldInfos);\n  creatableFields.retainWhere(\n    (field) => field.fieldType.canCreateFilter && !field.isGroupField,\n  );\n  return creatableFields;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_loader.dart",
    "content": "import 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\n\nabstract class SelectOptionFilterDelegate {\n  const SelectOptionFilterDelegate();\n\n  List<SelectOptionPB> getOptions(FieldInfo fieldInfo);\n}\n\nclass SingleSelectOptionFilterDelegateImpl\n    implements SelectOptionFilterDelegate {\n  const SingleSelectOptionFilterDelegateImpl();\n\n  @override\n  List<SelectOptionPB> getOptions(FieldInfo fieldInfo) {\n    final parser = SingleSelectTypeOptionDataParser();\n    return parser.fromBuffer(fieldInfo.field.typeOptionData).options;\n  }\n}\n\nclass MultiSelectOptionFilterDelegateImpl\n    implements SelectOptionFilterDelegate {\n  const MultiSelectOptionFilterDelegateImpl();\n\n  @override\n  List<SelectOptionPB> getOptions(FieldInfo fieldInfo) {\n    return MultiSelectTypeOptionDataParser()\n        .fromBuffer(fieldInfo.field.typeOptionData)\n        .options;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/application/grid_accessory_bloc.dart",
    "content": "import 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'grid_accessory_bloc.freezed.dart';\n\nclass DatabaseViewSettingExtensionBloc extends Bloc<\n    DatabaseViewSettingExtensionEvent, DatabaseViewSettingExtensionState> {\n  DatabaseViewSettingExtensionBloc({required this.viewId})\n      : super(DatabaseViewSettingExtensionState.initial(viewId)) {\n    on<DatabaseViewSettingExtensionEvent>(\n      (event, emit) async {\n        event.when(\n          initial: () {},\n          toggleMenu: () {\n            emit(state.copyWith(isVisible: !state.isVisible));\n          },\n        );\n      },\n    );\n  }\n\n  final String viewId;\n}\n\n@freezed\nclass DatabaseViewSettingExtensionEvent\n    with _$DatabaseViewSettingExtensionEvent {\n  const factory DatabaseViewSettingExtensionEvent.initial() = _Initial;\n  const factory DatabaseViewSettingExtensionEvent.toggleMenu() =\n      _MenuVisibleChange;\n}\n\n@freezed\nclass DatabaseViewSettingExtensionState\n    with _$DatabaseViewSettingExtensionState {\n  const factory DatabaseViewSettingExtensionState({\n    required String viewId,\n    required bool isVisible,\n  }) = _DatabaseViewSettingExtensionState;\n\n  factory DatabaseViewSettingExtensionState.initial(\n    String viewId,\n  ) =>\n      DatabaseViewSettingExtensionState(\n        viewId: viewId,\n        isVisible: false,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/application/grid_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/defines.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy/plugins/database/application/field/sort_entities.dart';\nimport 'package:appflowy/plugins/database/application/row/row_cache.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport '../../application/database_controller.dart';\n\npart 'grid_bloc.freezed.dart';\n\nclass GridBloc extends Bloc<GridEvent, GridState> {\n  GridBloc({\n    required ViewPB view,\n    required this.databaseController,\n    this.shrinkWrapped = false,\n  }) : super(GridState.initial(view.id)) {\n    _dispatch();\n  }\n\n  final DatabaseController databaseController;\n\n  /// When true will emit the count of visible rows to show\n  ///\n  final bool shrinkWrapped;\n\n  String get viewId => databaseController.viewId;\n\n  UserProfilePB? _userProfile;\n  UserProfilePB? get userProfile => _userProfile;\n\n  DatabaseCallbacks? _databaseCallbacks;\n\n  @override\n  Future<void> close() async {\n    databaseController.removeListener(onDatabaseChanged: _databaseCallbacks);\n    _databaseCallbacks = null;\n    await super.close();\n  }\n\n  void _dispatch() {\n    on<GridEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            final response = await UserEventGetUserProfile().send();\n            response.fold(\n              (userProfile) => _userProfile = userProfile,\n              (err) => Log.error(err),\n            );\n\n            _startListening();\n            await _openGrid(emit);\n          },\n          openRowDetail: (row) {\n            emit(\n              state.copyWith(\n                createdRow: row,\n                openRowDetail: true,\n              ),\n            );\n          },\n          createRow: (openRowDetail) async {\n            final lastVisibleRowId =\n                shrinkWrapped ? state.lastVisibleRow?.rowId : null;\n\n            final result = await RowBackendService.createRow(\n              viewId: viewId,\n              position: lastVisibleRowId != null\n                  ? OrderObjectPositionTypePB.After\n                  : null,\n              targetRowId: lastVisibleRowId,\n            );\n            result.fold(\n              (createdRow) => emit(\n                state.copyWith(\n                  createdRow: createdRow,\n                  openRowDetail: openRowDetail ?? false,\n                  visibleRows: state.visibleRows + 1,\n                ),\n              ),\n              (err) => Log.error(err),\n            );\n          },\n          resetCreatedRow: () {\n            emit(state.copyWith(createdRow: null, openRowDetail: false));\n          },\n          deleteRow: (rowInfo) async {\n            await RowBackendService.deleteRows(viewId, [rowInfo.rowId]);\n          },\n          moveRow: (int from, int to) {\n            final List<RowInfo> rows = [...state.rowInfos];\n\n            final fromRow = rows[from].rowId;\n            final toRow = rows[to].rowId;\n\n            rows.insert(to, rows.removeAt(from));\n            emit(state.copyWith(rowInfos: rows));\n\n            databaseController.moveRow(fromRowId: fromRow, toRowId: toRow);\n          },\n          didReceiveFieldUpdate: (fields) {\n            emit(state.copyWith(fields: fields));\n          },\n          didLoadRows: (newRowInfos, reason) {\n            emit(\n              state.copyWith(\n                rowInfos: newRowInfos,\n                rowCount: newRowInfos.length,\n                reason: reason,\n              ),\n            );\n          },\n          didReceveFilters: (filters) {\n            emit(state.copyWith(filters: filters));\n          },\n          didReceveSorts: (sorts) {\n            emit(state.copyWith(reorderable: sorts.isEmpty, sorts: sorts));\n          },\n          loadMoreRows: () {\n            emit(state.copyWith(visibleRows: state.visibleRows + 25));\n          },\n        );\n      },\n    );\n  }\n\n  RowCache get rowCache => databaseController.rowCache;\n\n  void _startListening() {\n    _databaseCallbacks = DatabaseCallbacks(\n      onNumOfRowsChanged: (rowInfos, _, reason) {\n        if (!isClosed) {\n          add(GridEvent.didLoadRows(rowInfos, reason));\n        }\n      },\n      onRowsCreated: (rows) {\n        for (final row in rows) {\n          if (!isClosed && row.isHiddenInView) {\n            add(GridEvent.openRowDetail(row.rowMeta));\n          }\n        }\n      },\n      onRowsUpdated: (rows, reason) {\n        // TODO(nathan): separate different reasons\n        if (!isClosed) {\n          add(\n            GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason),\n          );\n        }\n      },\n      onFieldsChanged: (fields) {\n        if (!isClosed) {\n          add(GridEvent.didReceiveFieldUpdate(fields));\n        }\n      },\n      onFiltersChanged: (filters) {\n        if (!isClosed) {\n          add(GridEvent.didReceveFilters(filters));\n        }\n      },\n      onSortsChanged: (sorts) {\n        if (!isClosed) {\n          add(GridEvent.didReceveSorts(sorts));\n        }\n      },\n    );\n    databaseController.addListener(onDatabaseChanged: _databaseCallbacks);\n  }\n\n  Future<void> _openGrid(Emitter<GridState> emit) async {\n    final result = await databaseController.open();\n    result.fold(\n      (grid) {\n        databaseController.setIsLoading(false);\n        emit(\n          state.copyWith(\n            loadingState: LoadingState.finish(FlowyResult.success(null)),\n          ),\n        );\n      },\n      (err) => emit(\n        state.copyWith(\n          loadingState: LoadingState.finish(FlowyResult.failure(err)),\n        ),\n      ),\n    );\n  }\n}\n\n@freezed\nclass GridEvent with _$GridEvent {\n  const factory GridEvent.initial() = InitialGrid;\n  const factory GridEvent.openRowDetail(RowMetaPB row) = _OpenRowDetail;\n  const factory GridEvent.createRow({bool? openRowDetail}) = _CreateRow;\n  const factory GridEvent.resetCreatedRow() = _ResetCreatedRow;\n  const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow;\n  const factory GridEvent.moveRow(int from, int to) = _MoveRow;\n  const factory GridEvent.didLoadRows(\n    List<RowInfo> rows,\n    ChangedReason reason,\n  ) = _DidReceiveRowUpdate;\n  const factory GridEvent.didReceiveFieldUpdate(\n    List<FieldInfo> fields,\n  ) = _DidReceiveFieldUpdate;\n  const factory GridEvent.didReceveFilters(List<DatabaseFilter> filters) =\n      _DidReceiveFilters;\n  const factory GridEvent.didReceveSorts(List<DatabaseSort> sorts) =\n      _DidReceiveSorts;\n  const factory GridEvent.loadMoreRows() = _LoadMoreRows;\n}\n\n@freezed\nclass GridState with _$GridState {\n  const factory GridState({\n    required String viewId,\n    required List<FieldInfo> fields,\n    required List<RowInfo> rowInfos,\n    required int rowCount,\n    required RowMetaPB? createdRow,\n    required LoadingState loadingState,\n    required bool reorderable,\n    required ChangedReason reason,\n    required List<DatabaseSort> sorts,\n    required List<DatabaseFilter> filters,\n    required bool openRowDetail,\n    @Default(0) int visibleRows,\n  }) = _GridState;\n\n  factory GridState.initial(String viewId) => GridState(\n        fields: [],\n        rowInfos: [],\n        rowCount: 0,\n        createdRow: null,\n        viewId: viewId,\n        reorderable: true,\n        loadingState: const LoadingState.loading(),\n        reason: const InitialListState(),\n        filters: [],\n        sorts: [],\n        openRowDetail: false,\n        visibleRows: 25,\n      );\n}\n\nextension _LastVisibleRow on GridState {\n  /// Returns the last visible [RowInfo] in the list of [rowInfos].\n  /// Only returns if the visibleRows is less than the rowCount, otherwise returns null.\n  ///\n  RowInfo? get lastVisibleRow {\n    if (visibleRows < rowCount) {\n      return rowInfos[visibleRows - 1];\n    }\n\n    return null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/application/grid_header_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport '../../domain/field_service.dart';\n\npart 'grid_header_bloc.freezed.dart';\n\nclass GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {\n  GridHeaderBloc({required this.viewId, required this.fieldController})\n      : super(GridHeaderState.initial()) {\n    _dispatch();\n  }\n\n  final String viewId;\n  final FieldController fieldController;\n\n  @override\n  Future<void> close() async {\n    fieldController.removeListener(onFieldsListener: _onReceiveFields);\n    await super.close();\n  }\n\n  void _dispatch() {\n    on<GridHeaderEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () {\n            _startListening();\n            add(\n              GridHeaderEvent.didReceiveFieldUpdate(fieldController.fieldInfos),\n            );\n          },\n          didReceiveFieldUpdate: (List<FieldInfo> fields) {\n            emit(\n              state.copyWith(\n                fields: fields\n                    .where(\n                      (element) =>\n                          element.visibility != null &&\n                          element.visibility != FieldVisibility.AlwaysHidden,\n                    )\n                    .toList(),\n              ),\n            );\n          },\n          startEditingField: (fieldId) {\n            emit(state.copyWith(editingFieldId: fieldId));\n          },\n          startEditingNewField: (fieldId) {\n            emit(state.copyWith(editingFieldId: fieldId, newFieldId: fieldId));\n          },\n          endEditingField: () {\n            emit(state.copyWith(editingFieldId: null, newFieldId: null));\n          },\n          moveField: (fromIndex, toIndex) async {\n            await _moveField(fromIndex, toIndex, emit);\n          },\n        );\n      },\n    );\n  }\n\n  Future<void> _moveField(\n    int fromIndex,\n    int toIndex,\n    Emitter<GridHeaderState> emit,\n  ) async {\n    final fromId = state.fields[fromIndex].id;\n    final toId = state.fields[toIndex].id;\n\n    final fields = List<FieldInfo>.from(state.fields);\n    fields.insert(toIndex, fields.removeAt(fromIndex));\n    emit(state.copyWith(fields: fields));\n\n    final result = await FieldBackendService.moveField(\n      viewId: viewId,\n      fromFieldId: fromId,\n      toFieldId: toId,\n    );\n    result.fold((l) {}, (err) => Log.error(err));\n  }\n\n  void _startListening() {\n    fieldController.addListener(\n      onReceiveFields: _onReceiveFields,\n      listenWhen: () => !isClosed,\n    );\n  }\n\n  void _onReceiveFields(List<FieldInfo> fields) =>\n      add(GridHeaderEvent.didReceiveFieldUpdate(fields));\n}\n\n@freezed\nclass GridHeaderEvent with _$GridHeaderEvent {\n  const factory GridHeaderEvent.initial() = _InitialHeader;\n  const factory GridHeaderEvent.didReceiveFieldUpdate(List<FieldInfo> fields) =\n      _DidReceiveFieldUpdate;\n  const factory GridHeaderEvent.startEditingField(String fieldId) =\n      _StartEditingField;\n  const factory GridHeaderEvent.startEditingNewField(String fieldId) =\n      _StartEditingNewField;\n  const factory GridHeaderEvent.endEditingField() = _EndEditingField;\n  const factory GridHeaderEvent.moveField(\n    int fromIndex,\n    int toIndex,\n  ) = _MoveField;\n}\n\n@freezed\nclass GridHeaderState with _$GridHeaderState {\n  const factory GridHeaderState({\n    required List<FieldInfo> fields,\n    required String? editingFieldId,\n    required String? newFieldId,\n  }) = _GridHeaderState;\n\n  factory GridHeaderState.initial() =>\n      const GridHeaderState(fields: [], editingFieldId: null, newFieldId: null);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/application/row/mobile_row_detail_bloc.dart",
    "content": "import 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_cache.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'mobile_row_detail_bloc.freezed.dart';\n\nclass MobileRowDetailBloc\n    extends Bloc<MobileRowDetailEvent, MobileRowDetailState> {\n  MobileRowDetailBloc({required this.databaseController})\n      : super(MobileRowDetailState.initial()) {\n    rowBackendService = RowBackendService(viewId: databaseController.viewId);\n    _dispatch();\n  }\n\n  final DatabaseController databaseController;\n  late final RowBackendService rowBackendService;\n\n  UserProfilePB? _userProfile;\n  UserProfilePB? get userProfile => _userProfile;\n\n  DatabaseCallbacks? _databaseCallbacks;\n\n  @override\n  Future<void> close() async {\n    databaseController.removeListener(onDatabaseChanged: _databaseCallbacks);\n    _databaseCallbacks = null;\n    await super.close();\n  }\n\n  void _dispatch() {\n    on<MobileRowDetailEvent>(\n      (event, emit) {\n        event.when(\n          initial: (rowId) async {\n            _startListening();\n\n            emit(\n              state.copyWith(\n                isLoading: false,\n                currentRowId: rowId,\n                rowInfos: databaseController.rowCache.rowInfos,\n              ),\n            );\n\n            final result = await UserEventGetUserProfile().send();\n            result.fold(\n              (profile) => _userProfile = profile,\n              (error) => Log.error(error),\n            );\n          },\n          didLoadRows: (rows) {\n            emit(state.copyWith(rowInfos: rows));\n          },\n          changeRowId: (rowId) {\n            emit(state.copyWith(currentRowId: rowId));\n          },\n          addCover: (rowCover) async {\n            if (state.currentRowId == null) {\n              return;\n            }\n\n            await rowBackendService.updateMeta(\n              rowId: state.currentRowId!,\n              cover: rowCover,\n            );\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _databaseCallbacks = DatabaseCallbacks(\n      onNumOfRowsChanged: (rowInfos, _, reason) {\n        if (!isClosed) {\n          add(MobileRowDetailEvent.didLoadRows(rowInfos));\n        }\n      },\n      onRowsUpdated: (rows, reason) {\n        if (!isClosed) {\n          add(\n            MobileRowDetailEvent.didLoadRows(\n              databaseController.rowCache.rowInfos,\n            ),\n          );\n        }\n      },\n    );\n    databaseController.addListener(onDatabaseChanged: _databaseCallbacks);\n  }\n}\n\n@freezed\nclass MobileRowDetailEvent with _$MobileRowDetailEvent {\n  const factory MobileRowDetailEvent.initial(String rowId) = _Initial;\n  const factory MobileRowDetailEvent.didLoadRows(List<RowInfo> rows) =\n      _DidLoadRows;\n  const factory MobileRowDetailEvent.changeRowId(String rowId) = _ChangeRowId;\n  const factory MobileRowDetailEvent.addCover(RowCoverPB cover) = _AddCover;\n}\n\n@freezed\nclass MobileRowDetailState with _$MobileRowDetailState {\n  const factory MobileRowDetailState({\n    required bool isLoading,\n    required String? currentRowId,\n    required List<RowInfo> rowInfos,\n  }) = _MobileRowDetailState;\n\n  factory MobileRowDetailState.initial() {\n    return const MobileRowDetailState(\n      isLoading: true,\n      rowInfos: [],\n      currentRowId: null,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/foundation.dart';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport '../../../application/row/row_cache.dart';\nimport '../../../application/row/row_controller.dart';\nimport '../../../application/row/row_service.dart';\n\npart 'row_bloc.freezed.dart';\n\nclass RowBloc extends Bloc<RowEvent, RowState> {\n  RowBloc({\n    required this.fieldController,\n    required this.rowId,\n    required this.viewId,\n    required RowController rowController,\n  })  : _rowBackendSvc = RowBackendService(viewId: viewId),\n        _rowController = rowController,\n        super(RowState.initial()) {\n    _dispatch();\n    _startListening();\n    _init();\n    rowController.initialize();\n  }\n\n  final FieldController fieldController;\n  final RowBackendService _rowBackendSvc;\n  final RowController _rowController;\n  final String viewId;\n  final String rowId;\n\n  @override\n  Future<void> close() async {\n    await _rowController.dispose();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<RowEvent>(\n      (event, emit) async {\n        event.when(\n          createRow: () {\n            _rowBackendSvc.createRowAfter(rowId);\n          },\n          didReceiveCells: (List<CellContext> cellContexts, reason) {\n            final visibleCellContexts = cellContexts\n                .where(\n                  (cellContext) => fieldController\n                      .getField(cellContext.fieldId)!\n                      .fieldSettings!\n                      .visibility\n                      .isVisibleState(),\n                )\n                .toList();\n            emit(\n              state.copyWith(\n                cellContexts: visibleCellContexts,\n                changeReason: reason,\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() =>\n      _rowController.addListener(onRowChanged: _onRowChanged);\n\n  void _onRowChanged(List<CellContext> cells, ChangedReason reason) {\n    if (!isClosed) {\n      add(RowEvent.didReceiveCells(cells, reason));\n    }\n  }\n\n  void _init() {\n    add(\n      RowEvent.didReceiveCells(\n        _rowController.loadCells(),\n        const ChangedReason.setInitialRows(),\n      ),\n    );\n  }\n}\n\n@freezed\nclass RowEvent with _$RowEvent {\n  const factory RowEvent.createRow() = _CreateRow;\n  const factory RowEvent.didReceiveCells(\n    List<CellContext> cellsByFieldId,\n    ChangedReason reason,\n  ) = _DidReceiveCells;\n}\n\n@freezed\nclass RowState with _$RowState {\n  const factory RowState({\n    required List<CellContext> cellContexts,\n    ChangedReason? changeReason,\n  }) = _RowState;\n\n  factory RowState.initial() => const RowState(cellContexts: []);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_detail_bloc.dart",
    "content": "import 'package:flutter/foundation.dart';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/row/row_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/plugins/database/domain/field_settings_service.dart';\nimport 'package:appflowy/plugins/database/domain/row_meta_listener.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'row_detail_bloc.freezed.dart';\n\nclass RowDetailBloc extends Bloc<RowDetailEvent, RowDetailState> {\n  RowDetailBloc({\n    required this.fieldController,\n    required this.rowController,\n  })  : _metaListener = RowMetaListener(rowController.rowId),\n        _rowService = RowBackendService(viewId: rowController.viewId),\n        super(RowDetailState.initial(rowController.rowMeta)) {\n    _dispatch();\n    _startListening();\n    _init();\n\n    rowController.initialize();\n  }\n\n  final FieldController fieldController;\n  final RowController rowController;\n  final RowMetaListener _metaListener;\n  final RowBackendService _rowService;\n  final List<CellContext> allCells = [];\n\n  @override\n  Future<void> close() async {\n    await rowController.dispose();\n    await _metaListener.stop();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<RowDetailEvent>(\n      (event, emit) async {\n        await event.when(\n          didReceiveCellDatas: (visibleCells, numHiddenFields) {\n            emit(\n              state.copyWith(\n                visibleCells: visibleCells,\n                numHiddenFields: numHiddenFields,\n              ),\n            );\n          },\n          didUpdateFields: (fields) {\n            emit(state.copyWith(fields: fields));\n          },\n          deleteField: (fieldId) async {\n            final result = await FieldBackendService.deleteField(\n              viewId: rowController.viewId,\n              fieldId: fieldId,\n            );\n            result.fold((l) {}, (err) => Log.error(err));\n          },\n          toggleFieldVisibility: (fieldId) async {\n            await _toggleFieldVisibility(fieldId, emit);\n          },\n          reorderField: (fromIndex, toIndex) async {\n            await _reorderField(fromIndex, toIndex, emit);\n          },\n          toggleHiddenFieldVisibility: () {\n            final showHiddenFields = !state.showHiddenFields;\n            final visibleCells = List<CellContext>.from(\n              allCells.where((cellContext) {\n                final fieldInfo = fieldController.getField(cellContext.fieldId);\n                return fieldInfo != null &&\n                    !fieldInfo.isPrimary &&\n                    (fieldInfo.visibility!.isVisibleState() ||\n                        showHiddenFields);\n              }),\n            );\n\n            emit(\n              state.copyWith(\n                showHiddenFields: showHiddenFields,\n                visibleCells: visibleCells,\n              ),\n            );\n          },\n          startEditingField: (fieldId) {\n            emit(state.copyWith(editingFieldId: fieldId));\n          },\n          startEditingNewField: (fieldId) {\n            emit(state.copyWith(editingFieldId: fieldId, newFieldId: fieldId));\n          },\n          endEditingField: () {\n            emit(state.copyWith(editingFieldId: \"\", newFieldId: \"\"));\n          },\n          removeCover: () => _rowService.removeCover(rowController.rowId),\n          setCover: (cover) =>\n              _rowService.updateMeta(rowId: rowController.rowId, cover: cover),\n          didReceiveRowMeta: (rowMeta) {\n            emit(state.copyWith(rowMeta: rowMeta));\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _metaListener.start(\n      callback: (rowMeta) {\n        if (!isClosed) {\n          add(RowDetailEvent.didReceiveRowMeta(rowMeta));\n        }\n      },\n    );\n\n    rowController.addListener(\n      onRowChanged: (cellMap, reason) {\n        if (isClosed) {\n          return;\n        }\n        allCells.clear();\n        allCells.addAll(cellMap);\n        int numHiddenFields = 0;\n        final visibleCells = <CellContext>[];\n\n        for (final cellContext in allCells) {\n          final fieldInfo = fieldController.getField(cellContext.fieldId);\n          if (fieldInfo == null || fieldInfo.isPrimary) {\n            continue;\n          }\n          final isHidden = !fieldInfo.visibility!.isVisibleState();\n          if (!isHidden || state.showHiddenFields) {\n            visibleCells.add(cellContext);\n          }\n          if (isHidden) {\n            numHiddenFields++;\n          }\n        }\n\n        add(\n          RowDetailEvent.didReceiveCellDatas(\n            visibleCells,\n            numHiddenFields,\n          ),\n        );\n      },\n    );\n    fieldController.addListener(\n      onReceiveFields: (fields) => add(RowDetailEvent.didUpdateFields(fields)),\n      listenWhen: () => !isClosed,\n    );\n  }\n\n  void _init() {\n    allCells.addAll(rowController.loadCells());\n    int numHiddenFields = 0;\n    final visibleCells = <CellContext>[];\n    for (final cell in allCells) {\n      final fieldInfo = fieldController.getField(cell.fieldId);\n      if (fieldInfo == null || fieldInfo.isPrimary) {\n        continue;\n      }\n      final isHidden = !fieldInfo.visibility!.isVisibleState();\n      if (!isHidden) {\n        visibleCells.add(cell);\n      } else {\n        numHiddenFields++;\n      }\n    }\n    add(\n      RowDetailEvent.didReceiveCellDatas(\n        visibleCells,\n        numHiddenFields,\n      ),\n    );\n    add(RowDetailEvent.didUpdateFields(fieldController.fieldInfos));\n  }\n\n  Future<void> _toggleFieldVisibility(\n    String fieldId,\n    Emitter<RowDetailState> emit,\n  ) async {\n    final fieldInfo = fieldController.getField(fieldId)!;\n    final fieldVisibility = fieldInfo.visibility == FieldVisibility.AlwaysShown\n        ? FieldVisibility.AlwaysHidden\n        : FieldVisibility.AlwaysShown;\n    final result =\n        await FieldSettingsBackendService(viewId: rowController.viewId)\n            .updateFieldSettings(\n      fieldId: fieldId,\n      fieldVisibility: fieldVisibility,\n    );\n    result.fold((l) {}, (err) => Log.error(err));\n  }\n\n  Future<void> _reorderField(\n    int fromIndex,\n    int toIndex,\n    Emitter<RowDetailState> emit,\n  ) async {\n    if (fromIndex < toIndex) {\n      toIndex--;\n    }\n    final fromId = state.visibleCells[fromIndex].fieldId;\n    final toId = state.visibleCells[toIndex].fieldId;\n\n    final cells = List<CellContext>.from(state.visibleCells);\n    cells.insert(toIndex, cells.removeAt(fromIndex));\n    emit(state.copyWith(visibleCells: cells));\n\n    final result = await FieldBackendService.moveField(\n      viewId: rowController.viewId,\n      fromFieldId: fromId,\n      toFieldId: toId,\n    );\n    result.fold((l) {}, (err) => Log.error(err));\n  }\n}\n\n@freezed\nclass RowDetailEvent with _$RowDetailEvent {\n  const factory RowDetailEvent.didUpdateFields(List<FieldInfo> fields) =\n      _DidUpdateFields;\n\n  /// Triggered by listeners to update row data\n  const factory RowDetailEvent.didReceiveCellDatas(\n    List<CellContext> visibleCells,\n    int numHiddenFields,\n  ) = _DidReceiveCellDatas;\n\n  /// Used to delete a field\n  const factory RowDetailEvent.deleteField(String fieldId) = _DeleteField;\n\n  /// Used to show/hide a field\n  const factory RowDetailEvent.toggleFieldVisibility(String fieldId) =\n      _ToggleFieldVisibility;\n\n  /// Used to reorder a field\n  const factory RowDetailEvent.reorderField(\n    int fromIndex,\n    int toIndex,\n  ) = _ReorderField;\n\n  /// Used to hide/show the hidden fields in the row detail page\n  const factory RowDetailEvent.toggleHiddenFieldVisibility() =\n      _ToggleHiddenFieldVisibility;\n\n  /// Begin editing an event;\n  const factory RowDetailEvent.startEditingField(String fieldId) =\n      _StartEditingField;\n\n  const factory RowDetailEvent.startEditingNewField(String fieldId) =\n      _StartEditingNewField;\n\n  /// End editing an event\n  const factory RowDetailEvent.endEditingField() = _EndEditingField;\n\n  const factory RowDetailEvent.removeCover() = _RemoveCover;\n\n  const factory RowDetailEvent.setCover(RowCoverPB cover) = _SetCover;\n\n  const factory RowDetailEvent.didReceiveRowMeta(RowMetaPB rowMeta) =\n      _DidReceiveRowMeta;\n}\n\n@freezed\nclass RowDetailState with _$RowDetailState {\n  const factory RowDetailState({\n    required List<FieldInfo> fields,\n    required List<CellContext> visibleCells,\n    required bool showHiddenFields,\n    required int numHiddenFields,\n    required String editingFieldId,\n    required String newFieldId,\n    required RowMetaPB rowMeta,\n  }) = _RowDetailState;\n\n  factory RowDetailState.initial(RowMetaPB rowMeta) => RowDetailState(\n        fields: [],\n        visibleCells: [],\n        showHiddenFields: false,\n        numHiddenFields: 0,\n        editingFieldId: \"\",\n        newFieldId: \"\",\n        rowMeta: rowMeta,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_document_bloc.dart",
    "content": "import 'package:flutter/foundation.dart';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport '../../../application/row/row_service.dart';\n\npart 'row_document_bloc.freezed.dart';\n\nclass RowDocumentBloc extends Bloc<RowDocumentEvent, RowDocumentState> {\n  RowDocumentBloc({\n    required this.rowId,\n    required String viewId,\n  })  : _rowBackendSvc = RowBackendService(viewId: viewId),\n        super(RowDocumentState.initial()) {\n    _dispatch();\n  }\n\n  final String rowId;\n  final RowBackendService _rowBackendSvc;\n\n  void _dispatch() {\n    on<RowDocumentEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () {\n            _getRowDocumentView();\n          },\n          didReceiveRowDocument: (view) {\n            emit(\n              state.copyWith(\n                viewPB: view,\n                loadingState: const LoadingState.finish(),\n              ),\n            );\n          },\n          didReceiveError: (FlowyError error) {\n            emit(\n              state.copyWith(\n                loadingState: LoadingState.error(error),\n              ),\n            );\n          },\n          updateIsEmpty: (isEmpty) async {\n            final unitOrFailure = await _rowBackendSvc.updateMeta(\n              rowId: rowId,\n              isDocumentEmpty: isEmpty,\n            );\n\n            unitOrFailure.fold((l) => null, (err) => Log.error(err));\n          },\n        );\n      },\n    );\n  }\n\n  Future<void> _getRowDocumentView() async {\n    final rowDetailOrError = await _rowBackendSvc.getRowMeta(rowId);\n    rowDetailOrError.fold(\n      (RowMetaPB rowMeta) async {\n        final viewsOrError =\n            await ViewBackendService.getView(rowMeta.documentId);\n\n        if (isClosed) {\n          return;\n        }\n\n        viewsOrError.fold(\n          (view) => add(RowDocumentEvent.didReceiveRowDocument(view)),\n          (error) async {\n            if (error.code == ErrorCode.RecordNotFound) {\n              // By default, the document of the row is not exist. So creating a\n              // new document for the given document id of the row.\n              final documentView =\n                  await _createRowDocumentView(rowMeta.documentId);\n              if (documentView != null && !isClosed) {\n                add(RowDocumentEvent.didReceiveRowDocument(documentView));\n              }\n            } else {\n              add(RowDocumentEvent.didReceiveError(error));\n            }\n          },\n        );\n      },\n      (err) => Log.error('Failed to get row detail: $err'),\n    );\n  }\n\n  Future<ViewPB?> _createRowDocumentView(String viewId) async {\n    final result = await ViewBackendService.createOrphanView(\n      viewId: viewId,\n      name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),\n      desc: '',\n      layoutType: ViewLayoutPB.Document,\n    );\n    return result.fold(\n      (view) => view,\n      (error) {\n        Log.error(error);\n        return null;\n      },\n    );\n  }\n}\n\n@freezed\nclass RowDocumentEvent with _$RowDocumentEvent {\n  const factory RowDocumentEvent.initial() = _InitialRow;\n  const factory RowDocumentEvent.didReceiveRowDocument(ViewPB view) =\n      _DidReceiveRowDocument;\n  const factory RowDocumentEvent.didReceiveError(FlowyError error) =\n      _DidReceiveError;\n  const factory RowDocumentEvent.updateIsEmpty(bool isDocumentEmpty) =\n      _UpdateIsEmpty;\n}\n\n@freezed\nclass RowDocumentState with _$RowDocumentState {\n  const factory RowDocumentState({\n    ViewPB? viewPB,\n    required LoadingState loadingState,\n  }) = _RowDocumentState;\n\n  factory RowDocumentState.initial() => const RowDocumentState(\n        loadingState: LoadingState.loading(),\n      );\n}\n\n@freezed\nclass LoadingState with _$LoadingState {\n  const factory LoadingState.loading() = _Loading;\n  const factory LoadingState.error(FlowyError error) = _Error;\n  const factory LoadingState.finish() = _Finish;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/application/simple_text_filter_bloc.dart",
    "content": "import 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'simple_text_filter_bloc.freezed.dart';\n\nclass SimpleTextFilterBloc<T>\n    extends Bloc<SimpleTextFilterEvent<T>, SimpleTextFilterState<T>> {\n  SimpleTextFilterBloc({\n    required this.values,\n    required this.comparator,\n    this.filterText = \"\",\n  }) : super(SimpleTextFilterState(values: values)) {\n    _dispatch();\n  }\n\n  final String Function(T) comparator;\n\n  final List<T> values;\n  String filterText;\n\n  void _dispatch() {\n    on<SimpleTextFilterEvent<T>>((event, emit) async {\n      event.when(\n        updateFilter: (String filter) {\n          filterText = filter.toLowerCase();\n          _filter(emit);\n        },\n        receiveNewValues: (List<T> newValues) {\n          values\n            ..clear()\n            ..addAll(newValues);\n          _filter(emit);\n        },\n      );\n    });\n  }\n\n  void _filter(Emitter<SimpleTextFilterState<T>> emit) {\n    final List<T> result = [...values];\n\n    result.retainWhere((value) {\n      if (filterText.isNotEmpty) {\n        return comparator(value).toLowerCase().contains(filterText);\n      }\n      return true;\n    });\n\n    emit(SimpleTextFilterState(values: result));\n  }\n}\n\n@freezed\nclass SimpleTextFilterEvent<T> with _$SimpleTextFilterEvent<T> {\n  const factory SimpleTextFilterEvent.updateFilter(String filter) =\n      _UpdateFilter;\n  const factory SimpleTextFilterEvent.receiveNewValues(List<T> newValues) =\n      _ReceiveNewValues<T>;\n}\n\n@freezed\nclass SimpleTextFilterState<T> with _$SimpleTextFilterState<T> {\n  const factory SimpleTextFilterState({\n    required List<T> values,\n  }) = _SimpleTextFilterState<T>;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/application/sort/sort_editor_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/sort_entities.dart';\nimport 'package:appflowy/plugins/database/domain/sort_service.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'sort_editor_bloc.freezed.dart';\n\nclass SortEditorBloc extends Bloc<SortEditorEvent, SortEditorState> {\n  SortEditorBloc({\n    required this.viewId,\n    required this.fieldController,\n  })  : _sortBackendSvc = SortBackendService(viewId: viewId),\n        super(\n          SortEditorState.initial(\n            fieldController.sorts,\n            fieldController.fieldInfos,\n          ),\n        ) {\n    _dispatch();\n    _startListening();\n  }\n\n  final String viewId;\n  final SortBackendService _sortBackendSvc;\n  final FieldController fieldController;\n\n  void Function(List<FieldInfo>)? _onFieldFn;\n  void Function(List<DatabaseSort>)? _onSortsFn;\n\n  void _dispatch() {\n    on<SortEditorEvent>(\n      (event, emit) async {\n        await event.when(\n          didReceiveFields: (List<FieldInfo> fields) {\n            emit(\n              state.copyWith(\n                allFields: fields,\n                creatableFields: _getCreatableSorts(fields),\n              ),\n            );\n          },\n          createSort: (\n            String fieldId,\n            SortConditionPB? condition,\n          ) async {\n            final result = await _sortBackendSvc.insertSort(\n              fieldId: fieldId,\n              condition: condition ?? SortConditionPB.Ascending,\n            );\n            result.fold((l) => {}, (err) => Log.error(err));\n          },\n          editSort: (\n            String sortId,\n            String? fieldId,\n            SortConditionPB? condition,\n          ) async {\n            final sort = state.sorts\n                .firstWhereOrNull((element) => element.sortId == sortId);\n            if (sort == null) {\n              return;\n            }\n\n            final result = await _sortBackendSvc.updateSort(\n              sortId: sortId,\n              fieldId: fieldId ?? sort.fieldId,\n              condition: condition ?? sort.condition,\n            );\n            result.fold((l) => {}, (err) => Log.error(err));\n          },\n          deleteAllSorts: () async {\n            final result = await _sortBackendSvc.deleteAllSorts();\n            result.fold((l) => {}, (err) => Log.error(err));\n          },\n          didReceiveSorts: (sorts) {\n            emit(state.copyWith(sorts: sorts));\n          },\n          deleteSort: (sortId) async {\n            final result = await _sortBackendSvc.deleteSort(\n              sortId: sortId,\n            );\n            result.fold((l) => null, (err) => Log.error(err));\n          },\n          reorderSort: (fromIndex, toIndex) async {\n            if (fromIndex < toIndex) {\n              toIndex--;\n            }\n\n            final fromId = state.sorts[fromIndex].sortId;\n            final toId = state.sorts[toIndex].sortId;\n\n            final newSorts = [...state.sorts];\n            newSorts.insert(toIndex, newSorts.removeAt(fromIndex));\n            emit(state.copyWith(sorts: newSorts));\n            final result = await _sortBackendSvc.reorderSort(\n              fromSortId: fromId,\n              toSortId: toId,\n            );\n            result.fold((l) => null, (err) => Log.error(err));\n          },\n        );\n      },\n    );\n  }\n\n  void _startListening() {\n    _onFieldFn = (fields) {\n      add(SortEditorEvent.didReceiveFields(List.from(fields)));\n    };\n    _onSortsFn = (sorts) {\n      add(SortEditorEvent.didReceiveSorts(sorts));\n    };\n\n    fieldController.addListener(\n      listenWhen: () => !isClosed,\n      onReceiveFields: _onFieldFn,\n      onSorts: _onSortsFn,\n    );\n  }\n\n  @override\n  Future<void> close() async {\n    fieldController.removeListener(\n      onFieldsListener: _onFieldFn,\n      onSortsListener: _onSortsFn,\n    );\n    _onFieldFn = null;\n    _onSortsFn = null;\n    return super.close();\n  }\n}\n\n@freezed\nclass SortEditorEvent with _$SortEditorEvent {\n  const factory SortEditorEvent.didReceiveFields(List<FieldInfo> fieldInfos) =\n      _DidReceiveFields;\n  const factory SortEditorEvent.didReceiveSorts(List<DatabaseSort> sorts) =\n      _DidReceiveSorts;\n  const factory SortEditorEvent.createSort({\n    required String fieldId,\n    SortConditionPB? condition,\n  }) = _CreateSort;\n  const factory SortEditorEvent.editSort({\n    required String sortId,\n    String? fieldId,\n    SortConditionPB? condition,\n  }) = _EditSort;\n  const factory SortEditorEvent.reorderSort(int oldIndex, int newIndex) =\n      _ReorderSort;\n  const factory SortEditorEvent.deleteSort(String sortId) = _DeleteSort;\n  const factory SortEditorEvent.deleteAllSorts() = _DeleteAllSorts;\n}\n\n@freezed\nclass SortEditorState with _$SortEditorState {\n  const factory SortEditorState({\n    required List<DatabaseSort> sorts,\n    required List<FieldInfo> allFields,\n    required List<FieldInfo> creatableFields,\n  }) = _SortEditorState;\n\n  factory SortEditorState.initial(\n    List<DatabaseSort> sorts,\n    List<FieldInfo> fields,\n  ) {\n    return SortEditorState(\n      sorts: sorts,\n      allFields: fields,\n      creatableFields: _getCreatableSorts(fields),\n    );\n  }\n}\n\nList<FieldInfo> _getCreatableSorts(List<FieldInfo> fieldInfos) {\n  final List<FieldInfo> creatableFields = List.from(fieldInfos);\n  creatableFields.retainWhere(\n    (field) => field.fieldType.canCreateSort && !field.hasSort,\n  );\n  return creatableFields;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/grid.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nclass GridPluginBuilder implements PluginBuilder {\n  @override\n  Plugin build(dynamic data) {\n    if (data is ViewPB) {\n      return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data);\n    } else {\n      throw FlowyPluginException.invalidData;\n    }\n  }\n\n  @override\n  String get menuName => LocaleKeys.grid_menuName.tr();\n\n  @override\n  FlowySvgData get icon => FlowySvgs.icon_grid_s;\n\n  @override\n  PluginType get pluginType => PluginType.grid;\n\n  @override\n  ViewLayoutPB get layoutType => ViewLayoutPB.Grid;\n}\n\nclass GridPluginConfig implements PluginConfig {\n  @override\n  bool get creatable => true;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';\nimport 'package:appflowy/plugins/database/domain/sort_service.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart';\nimport 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/shared/flowy_error_page.dart';\nimport 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';\nimport 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:linked_scroll_controller/linked_scroll_controller.dart';\nimport 'package:provider/provider.dart';\n\nimport '../../application/database_controller.dart';\nimport '../../application/row/row_controller.dart';\nimport '../../tab_bar/tab_bar_view.dart';\nimport '../../widgets/row/row_detail.dart';\nimport '../application/grid_bloc.dart';\nimport 'grid_scroll.dart';\nimport 'layout/layout.dart';\nimport 'layout/sizes.dart';\nimport 'widgets/footer/grid_footer.dart';\nimport 'widgets/header/grid_header.dart';\nimport 'widgets/row/row.dart';\nimport 'widgets/shortcuts.dart';\n\nclass ToggleExtensionNotifier extends ChangeNotifier {\n  bool _isToggled = false;\n\n  bool get isToggled => _isToggled;\n\n  void toggle() {\n    _isToggled = !_isToggled;\n    notifyListeners();\n  }\n}\n\nclass DesktopGridTabBarBuilderImpl extends DatabaseTabBarItemBuilder {\n  final _toggleExtension = ToggleExtensionNotifier();\n\n  @override\n  Widget content(\n    BuildContext context,\n    ViewPB view,\n    DatabaseController controller,\n    bool shrinkWrap,\n    String? initialRowId,\n  ) {\n    return GridPage(\n      key: _makeValueKey(controller),\n      view: view,\n      databaseController: controller,\n      initialRowId: initialRowId,\n      shrinkWrap: shrinkWrap,\n    );\n  }\n\n  @override\n  Widget settingBar(BuildContext context, DatabaseController controller) {\n    return GridSettingBar(\n      key: _makeValueKey(controller),\n      controller: controller,\n      toggleExtension: _toggleExtension,\n    );\n  }\n\n  @override\n  Widget settingBarExtension(\n    BuildContext context,\n    DatabaseController controller,\n  ) {\n    return DatabaseViewSettingExtension(\n      key: _makeValueKey(controller),\n      viewId: controller.viewId,\n      databaseController: controller,\n      toggleExtension: _toggleExtension,\n    );\n  }\n\n  @override\n  void dispose() {\n    _toggleExtension.dispose();\n    super.dispose();\n  }\n\n  ValueKey _makeValueKey(DatabaseController controller) {\n    return ValueKey(controller.viewId);\n  }\n}\n\nclass GridPage extends StatefulWidget {\n  const GridPage({\n    super.key,\n    required this.view,\n    required this.databaseController,\n    this.onDeleted,\n    this.initialRowId,\n    this.shrinkWrap = false,\n  });\n\n  final ViewPB view;\n  final DatabaseController databaseController;\n  final VoidCallback? onDeleted;\n  final String? initialRowId;\n  final bool shrinkWrap;\n\n  @override\n  State<GridPage> createState() => _GridPageState();\n}\n\nclass _GridPageState extends State<GridPage> {\n  bool _didOpenInitialRow = false;\n\n  late final GridBloc gridBloc = GridBloc(\n    view: widget.view,\n    databaseController: widget.databaseController,\n    shrinkWrapped: widget.shrinkWrap,\n  )..add(const GridEvent.initial());\n\n  @override\n  void dispose() {\n    gridBloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider<GridBloc>(\n          create: (_) => gridBloc,\n        ),\n        BlocProvider(\n          create: (context) => PageAccessLevelBloc(view: widget.view)\n            ..add(PageAccessLevelEvent.initial()),\n        ),\n      ],\n      child: BlocListener<ActionNavigationBloc, ActionNavigationState>(\n        listener: (context, state) {\n          final action = state.action;\n          if (action?.type == ActionType.openRow &&\n              action?.objectId == widget.view.id) {\n            final rowId = action!.arguments?[ActionArgumentKeys.rowId];\n            if (rowId != null) {\n              // If Reminder in existing database is pressed\n              // then open the row\n              _openRow(context, rowId);\n            }\n          }\n        },\n        child: BlocConsumer<GridBloc, GridState>(\n          listener: listener,\n          builder: (context, state) => state.loadingState.map(\n            idle: (_) => const SizedBox.shrink(),\n            loading: (_) => const Center(\n              child: CircularProgressIndicator.adaptive(),\n            ),\n            finish: (result) => result.successOrFail.fold(\n              (_) => GridShortcuts(\n                child: GridPageContent(\n                  key: ValueKey(widget.view.id),\n                  view: widget.view,\n                  shrinkWrap: widget.shrinkWrap,\n                ),\n              ),\n              (err) => Center(child: AppFlowyErrorPage(error: err)),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  void _openRow(BuildContext context, String rowId) {\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      final gridBloc = context.read<GridBloc>();\n      final rowCache = gridBloc.rowCache;\n      final rowMeta = rowCache.getRow(rowId)?.rowMeta;\n      if (rowMeta == null) {\n        return;\n      }\n\n      final rowController = RowController(\n        viewId: widget.view.id,\n        rowMeta: rowMeta,\n        rowCache: rowCache,\n      );\n\n      FlowyOverlay.show(\n        context: context,\n        builder: (_) => BlocProvider.value(\n          value: context.read<UserWorkspaceBloc>(),\n          child: RowDetailPage(\n            databaseController: context.read<GridBloc>().databaseController,\n            rowController: rowController,\n            userProfile: context.read<GridBloc>().userProfile,\n          ),\n        ),\n      );\n    });\n  }\n\n  void listener(BuildContext context, GridState state) {\n    state.loadingState.whenOrNull(\n      // If initial row id is defined, open row details overlay\n      finish: (_) async {\n        if (widget.initialRowId != null && !_didOpenInitialRow) {\n          _didOpenInitialRow = true;\n\n          _openRow(context, widget.initialRowId!);\n          return;\n        }\n\n        final bloc = context.read<DatabaseTabBarBloc>();\n        final isCurrentView =\n            bloc.state.tabBars[bloc.state.selectedIndex].viewId ==\n                widget.view.id;\n\n        if (state.openRowDetail && state.createdRow != null && isCurrentView) {\n          final rowController = RowController(\n            viewId: widget.view.id,\n            rowMeta: state.createdRow!,\n            rowCache: context.read<GridBloc>().rowCache,\n          );\n          unawaited(\n            FlowyOverlay.show(\n              context: context,\n              builder: (_) => BlocProvider.value(\n                value: context.read<UserWorkspaceBloc>(),\n                child: RowDetailPage(\n                  databaseController:\n                      context.read<GridBloc>().databaseController,\n                  rowController: rowController,\n                  userProfile: context.read<GridBloc>().userProfile,\n                ),\n              ),\n            ),\n          );\n          context.read<GridBloc>().add(const GridEvent.resetCreatedRow());\n        }\n      },\n    );\n  }\n}\n\nclass GridPageContent extends StatefulWidget {\n  const GridPageContent({\n    super.key,\n    required this.view,\n    this.shrinkWrap = false,\n  });\n\n  final ViewPB view;\n  final bool shrinkWrap;\n\n  @override\n  State<GridPageContent> createState() => _GridPageContentState();\n}\n\nclass _GridPageContentState extends State<GridPageContent> {\n  final _scrollController = GridScrollController(\n    scrollGroupController: LinkedScrollControllerGroup(),\n  );\n  late final ScrollController headerScrollController;\n\n  @override\n  void initState() {\n    super.initState();\n    headerScrollController = _scrollController.linkHorizontalController();\n  }\n\n  @override\n  void dispose() {\n    _scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        _GridHeader(\n          headerScrollController: headerScrollController,\n          editable: context.read<PageAccessLevelBloc>().state.isEditable,\n          shrinkWrap: widget.shrinkWrap,\n        ),\n        _GridRows(\n          viewId: widget.view.id,\n          scrollController: _scrollController,\n          shrinkWrap: widget.shrinkWrap,\n        ),\n      ],\n    );\n  }\n}\n\nclass _GridHeader extends StatelessWidget {\n  const _GridHeader({\n    required this.headerScrollController,\n    required this.editable,\n    required this.shrinkWrap,\n  });\n\n  final ScrollController headerScrollController;\n  final bool editable;\n  final bool shrinkWrap;\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child = BlocBuilder<GridBloc, GridState>(\n      builder: (_, state) => GridHeaderSliverAdaptor(\n        viewId: state.viewId,\n        anchorScrollController: headerScrollController,\n        shrinkWrap: shrinkWrap,\n      ),\n    );\n\n    if (!editable) {\n      child = IgnorePointer(\n        child: child,\n      );\n    }\n\n    return child;\n  }\n}\n\nclass _GridRows extends StatefulWidget {\n  const _GridRows({\n    required this.viewId,\n    required this.scrollController,\n    this.shrinkWrap = false,\n  });\n\n  final String viewId;\n  final GridScrollController scrollController;\n\n  /// When [shrinkWrap] is active, the Grid will show items according to\n  /// GridState.visibleRows and will not have a vertical scroll area.\n  ///\n  final bool shrinkWrap;\n\n  @override\n  State<_GridRows> createState() => _GridRowsState();\n}\n\nclass _GridRowsState extends State<_GridRows> {\n  bool showFloatingCalculations = false;\n  bool isAtBottom = false;\n\n  @override\n  void initState() {\n    super.initState();\n    if (!widget.shrinkWrap) {\n      _evaluateFloatingCalculations();\n      widget.scrollController.verticalController.addListener(_onScrollChanged);\n    }\n  }\n\n  void _onScrollChanged() {\n    final controller = widget.scrollController.verticalController;\n    final isAtBottom = controller.position.atEdge && controller.offset > 0 ||\n        controller.offset >= controller.position.maxScrollExtent - 1;\n    if (isAtBottom != this.isAtBottom) {\n      setState(() => this.isAtBottom = isAtBottom);\n    }\n  }\n\n  @override\n  void dispose() {\n    if (!widget.shrinkWrap) {\n      widget.scrollController.verticalController\n          .removeListener(_onScrollChanged);\n    }\n    super.dispose();\n  }\n\n  void _evaluateFloatingCalculations() {\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      if (mounted && !widget.shrinkWrap) {\n        setState(() {\n          final verticalController = widget.scrollController.verticalController;\n          // maxScrollExtent is 0.0 if scrolling is not possible\n          showFloatingCalculations =\n              verticalController.position.maxScrollExtent > 0;\n\n          isAtBottom = verticalController.position.atEdge &&\n              verticalController.offset > 0;\n        });\n      }\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final paddingLeft = context\n            .read<DatabasePluginWidgetBuilderSize?>()\n            ?.paddingLeftWithMaxDocumentWidth ??\n        0.0;\n    Widget child;\n    if (widget.shrinkWrap) {\n      child = Scrollbar(\n        controller: widget.scrollController.horizontalController,\n        child: SingleChildScrollView(\n          scrollDirection: Axis.horizontal,\n          controller: widget.scrollController.horizontalController,\n          child: ConstrainedBox(\n            constraints: BoxConstraints(\n              maxWidth: GridLayout.headerWidth(\n                context\n                            .read<DatabasePluginWidgetBuilderSize>()\n                            .horizontalPadding *\n                        3 +\n                    paddingLeft,\n                context.read<GridBloc>().state.fields,\n              ),\n            ),\n            child: _shrinkWrapRenderList(context),\n          ),\n        ),\n      );\n    } else {\n      child = _WrapScrollView(\n        scrollController: widget.scrollController,\n        contentWidth: GridLayout.headerWidth(\n          context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding,\n          context.read<GridBloc>().state.fields,\n        ),\n        child: BlocListener<GridBloc, GridState>(\n          listenWhen: (previous, current) =>\n              previous.rowCount != current.rowCount,\n          listener: (context, state) => _evaluateFloatingCalculations(),\n          child: ScrollConfiguration(\n            behavior:\n                ScrollConfiguration.of(context).copyWith(scrollbars: false),\n            child: _renderList(context),\n          ),\n        ),\n      );\n    }\n\n    if (widget.shrinkWrap) {\n      return child;\n    }\n\n    return Flexible(child: child);\n  }\n\n  Widget _shrinkWrapRenderList(BuildContext context) {\n    final state = context.read<GridBloc>().state;\n    final databaseSize = context.read<DatabasePluginWidgetBuilderSize?>();\n    return ListView(\n      shrinkWrap: true,\n      physics: const NeverScrollableScrollPhysics(),\n      padding: EdgeInsets.fromLTRB(\n        databaseSize?.paddingLeft ?? 0.0,\n        0,\n        databaseSize?.horizontalPadding ?? 0.0,\n        0,\n      ),\n      children: [\n        widget.shrinkWrap\n            ? _reorderableListView(state)\n            : Expanded(child: _reorderableListView(state)),\n        if (showFloatingCalculations && !widget.shrinkWrap) ...[\n          _PositionedCalculationsRow(\n            viewId: widget.viewId,\n            isAtBottom: isAtBottom,\n          ),\n        ],\n      ],\n    );\n  }\n\n  Widget _renderList(BuildContext context) {\n    final state = context.read<GridBloc>().state;\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        widget.shrinkWrap\n            ? _reorderableListView(state)\n            : Expanded(child: _reorderableListView(state)),\n        if (showFloatingCalculations && !widget.shrinkWrap) ...[\n          _PositionedCalculationsRow(\n            viewId: widget.viewId,\n            isAtBottom: isAtBottom,\n          ),\n        ],\n      ],\n    );\n  }\n\n  Widget _reorderableListView(GridState state) {\n    final List<Widget> footer = [\n      const GridRowBottomBar(),\n      if (widget.shrinkWrap && state.visibleRows < state.rowInfos.length)\n        const GridRowLoadMoreButton(),\n      if (!showFloatingCalculations) GridCalculationsRow(viewId: widget.viewId),\n    ];\n\n    // If we are using shrinkWrap, we need to show at most\n    // state.visibleRows + 1 items. The visibleRows can be larger\n    // than the actual rowInfos length.\n    final itemCount = widget.shrinkWrap\n        ? (state.visibleRows + 1).clamp(0, state.rowInfos.length + 1)\n        : state.rowInfos.length + 1;\n\n    return ReorderableListView.builder(\n      cacheExtent: 500,\n      scrollController: widget.scrollController.verticalController,\n      physics: const ClampingScrollPhysics(),\n      buildDefaultDragHandles: false,\n      shrinkWrap: widget.shrinkWrap,\n      proxyDecorator: (child, _, __) => Provider.value(\n        value: context.read<DatabasePluginWidgetBuilderSize>(),\n        child: Material(\n          color: Colors.white.withValues(alpha: .1),\n          child: Opacity(opacity: .5, child: child),\n        ),\n      ),\n      onReorder: (fromIndex, newIndex) {\n        final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;\n\n        if (state.sorts.isNotEmpty) {\n          showCancelAndDeleteDialog(\n            context: context,\n            title: LocaleKeys.grid_sort_sortsActive.tr(\n              namedArgs: {\n                'intention': LocaleKeys.grid_row_reorderRowDescription.tr(),\n              },\n            ),\n            description: LocaleKeys.grid_sort_removeSorting.tr(),\n            confirmLabel: LocaleKeys.button_remove.tr(),\n            closeOnAction: true,\n            onDelete: () {\n              SortBackendService(viewId: widget.viewId).deleteAllSorts();\n              moveRow(fromIndex, toIndex);\n            },\n          );\n        } else {\n          moveRow(fromIndex, toIndex);\n        }\n      },\n      itemCount: itemCount,\n      itemBuilder: (context, index) {\n        if (index == itemCount - 1) {\n          final child = Column(\n            key: const Key('grid_footer'),\n            mainAxisSize: MainAxisSize.min,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: footer,\n          );\n\n          if (!context.read<PageAccessLevelBloc>().state.isEditable) {\n            return IgnorePointer(\n              key: const Key('grid_footer'),\n              child: child,\n            );\n          }\n\n          return child;\n        }\n\n        return _renderRow(\n          context,\n          state.rowInfos[index].rowId,\n          index: index,\n        );\n      },\n    );\n  }\n\n  Widget _renderRow(\n    BuildContext context,\n    RowId rowId, {\n    required int index,\n    Animation<double>? animation,\n  }) {\n    final databaseController = context.read<GridBloc>().databaseController;\n    final DatabaseController(:viewId, :rowCache) = databaseController;\n    final rowMeta = rowCache.getRow(rowId)?.rowMeta;\n\n    /// Return placeholder widget if the rowMeta is null.\n    if (rowMeta == null) {\n      Log.warn('RowMeta is null for rowId: $rowId');\n      return const SizedBox.shrink();\n    }\n\n    final child = GridRow(\n      key: ValueKey(\"grid_row_$rowId\"),\n      shrinkWrap: widget.shrinkWrap,\n      fieldController: databaseController.fieldController,\n      rowId: rowId,\n      viewId: viewId,\n      index: index,\n      editable: context.watch<PageAccessLevelBloc>().state.isEditable,\n      rowController: RowController(\n        viewId: viewId,\n        rowMeta: rowMeta,\n        rowCache: rowCache,\n      ),\n      cellBuilder: EditableCellBuilder(databaseController: databaseController),\n      openDetailPage: (rowDetailContext) => FlowyOverlay.show(\n        context: rowDetailContext,\n        builder: (_) {\n          final rowMeta = rowCache.getRow(rowId)?.rowMeta;\n          if (rowMeta == null) {\n            return const SizedBox.shrink();\n          }\n\n          return BlocProvider.value(\n            value: context.read<UserWorkspaceBloc>(),\n            child: RowDetailPage(\n              rowController: RowController(\n                viewId: viewId,\n                rowMeta: rowMeta,\n                rowCache: rowCache,\n              ),\n              databaseController: databaseController,\n              userProfile: context.read<GridBloc>().userProfile,\n            ),\n          );\n        },\n      ),\n    );\n\n    if (animation != null) {\n      return SizeTransition(sizeFactor: animation, child: child);\n    }\n\n    return child;\n  }\n\n  void moveRow(int from, int to) {\n    if (from != to) {\n      context.read<GridBloc>().add(GridEvent.moveRow(from, to));\n    }\n  }\n}\n\nclass _WrapScrollView extends StatelessWidget {\n  const _WrapScrollView({\n    required this.contentWidth,\n    required this.scrollController,\n    required this.child,\n  });\n\n  final GridScrollController scrollController;\n  final double contentWidth;\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return ScrollbarListStack(\n      includeInsets: false,\n      axis: Axis.vertical,\n      controller: scrollController.verticalController,\n      barSize: GridSize.scrollBarSize,\n      autoHideScrollbar: false,\n      child: StyledSingleChildScrollView(\n        autoHideScrollbar: false,\n        includeInsets: false,\n        controller: scrollController.horizontalController,\n        axis: Axis.horizontal,\n        child: SizedBox(\n          width: contentWidth,\n          child: child,\n        ),\n      ),\n    );\n  }\n}\n\n/// This Widget is used to show the Calculations Row at the bottom of the Grids ScrollView\n/// when the ScrollView is scrollable.\n///\nclass _PositionedCalculationsRow extends StatefulWidget {\n  const _PositionedCalculationsRow({\n    required this.viewId,\n    this.isAtBottom = false,\n  });\n\n  final String viewId;\n\n  /// We don't need to show the top border if the scroll offset\n  /// is at the bottom of the ScrollView.\n  ///\n  final bool isAtBottom;\n\n  @override\n  State<_PositionedCalculationsRow> createState() =>\n      _PositionedCalculationsRowState();\n}\n\nclass _PositionedCalculationsRowState\n    extends State<_PositionedCalculationsRow> {\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      margin: EdgeInsets.only(\n        left: context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding,\n      ),\n      padding: const EdgeInsets.only(bottom: 10),\n      decoration: BoxDecoration(\n        color: Theme.of(context).canvasColor,\n        border: widget.isAtBottom\n            ? null\n            : Border(\n                top: BorderSide(\n                  color: AFThemeExtension.of(context).borderColor,\n                ),\n              ),\n      ),\n      child: SizedBox(\n        height: 36,\n        width: double.infinity,\n        child: GridCalculationsRow(\n          key: const Key('floating_grid_calculations'),\n          viewId: widget.viewId,\n          includeDefaultInsets: false,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_scroll.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:linked_scroll_controller/linked_scroll_controller.dart';\n\nclass GridScrollController {\n  GridScrollController({\n    required LinkedScrollControllerGroup scrollGroupController,\n  })  : _scrollGroupController = scrollGroupController,\n        verticalController = ScrollController(),\n        horizontalController = scrollGroupController.addAndGet();\n\n  final LinkedScrollControllerGroup _scrollGroupController;\n  final ScrollController verticalController;\n  final ScrollController horizontalController;\n\n  final List<ScrollController> _linkHorizontalControllers = [];\n\n  ScrollController linkHorizontalController() {\n    final controller = _scrollGroupController.addAndGet();\n    _linkHorizontalControllers.add(controller);\n    return controller;\n  }\n\n  void dispose() {\n    for (final controller in _linkHorizontalControllers) {\n      controller.dispose();\n    }\n    verticalController.dispose();\n    horizontalController.dispose();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/layout.dart",
    "content": "import 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart';\n\nimport 'sizes.dart';\n\nclass GridLayout {\n  static double headerWidth(double padding, List<FieldInfo> fields) {\n    if (fields.isEmpty) return 0;\n\n    final fieldsWidth = fields\n        .where(\n          (element) =>\n              element.visibility != null &&\n              element.visibility != FieldVisibility.AlwaysHidden,\n        )\n        .map((fieldInfo) => fieldInfo.width!.toDouble())\n        .reduce((value, element) => value + element);\n\n    return fieldsWidth + padding + GridSize.newPropertyButtonWidth;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart",
    "content": "import 'package:flutter/widgets.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass GridSize {\n  static double scale = 1;\n\n  static double get scrollBarSize => 8 * scale;\n\n  static double get headerHeight => 36 * scale;\n\n  static double get buttonHeight => 38 * scale;\n\n  static double get footerHeight => 36 * scale;\n\n  static double get horizontalHeaderPadding =>\n      UniversalPlatform.isDesktop ? 40 * scale : 16 * scale;\n\n  static double get cellHPadding => 10 * scale;\n\n  static double get cellVPadding => 8 * scale;\n\n  static double get popoverItemHeight => 26 * scale;\n\n  static double get typeOptionSeparatorHeight => 4 * scale;\n\n  static double get newPropertyButtonWidth => 140 * scale;\n\n  static double get mobileNewPropertyButtonWidth => 200 * scale;\n\n  static EdgeInsets get cellContentInsets => EdgeInsets.symmetric(\n        horizontal: GridSize.cellHPadding,\n        vertical: GridSize.cellVPadding,\n      );\n\n  static EdgeInsets get compactCellContentInsets =>\n      cellContentInsets - EdgeInsets.symmetric(vertical: 2);\n\n  static EdgeInsets get typeOptionContentInsets => const EdgeInsets.all(4);\n\n  static EdgeInsets get toolbarSettingButtonInsets =>\n      const EdgeInsets.symmetric(horizontal: 6, vertical: 2);\n\n  static EdgeInsets get footerContentInsets => EdgeInsets.fromLTRB(\n        GridSize.horizontalHeaderPadding,\n        0,\n        UniversalPlatform.isMobile ? GridSize.horizontalHeaderPadding : 0,\n        UniversalPlatform.isMobile ? 100 : 0,\n      );\n\n  static EdgeInsets get contentInsets => EdgeInsets.symmetric(\n        horizontal: GridSize.horizontalHeaderPadding,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart",
    "content": "import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';\nimport 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:appflowy/shared/flowy_error_page.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:linked_scroll_controller/linked_scroll_controller.dart';\n\nimport 'grid_scroll.dart';\nimport 'layout/sizes.dart';\nimport 'widgets/header/mobile_grid_header.dart';\nimport 'widgets/mobile_fab.dart';\nimport 'widgets/row/mobile_row.dart';\n\nclass MobileGridTabBarBuilderImpl extends DatabaseTabBarItemBuilder {\n  @override\n  Widget content(\n    BuildContext context,\n    ViewPB view,\n    DatabaseController controller,\n    bool shrinkWrap,\n    String? initialRowId,\n  ) {\n    return MobileGridPage(\n      key: _makeValueKey(controller),\n      view: view,\n      databaseController: controller,\n      initialRowId: initialRowId,\n      shrinkWrap: shrinkWrap,\n    );\n  }\n\n  @override\n  Widget settingBar(BuildContext context, DatabaseController controller) =>\n      const SizedBox.shrink();\n\n  @override\n  Widget settingBarExtension(\n    BuildContext context,\n    DatabaseController controller,\n  ) =>\n      const SizedBox.shrink();\n\n  ValueKey _makeValueKey(DatabaseController controller) {\n    return ValueKey(controller.viewId);\n  }\n}\n\nclass MobileGridPage extends StatefulWidget {\n  const MobileGridPage({\n    super.key,\n    required this.view,\n    required this.databaseController,\n    this.onDeleted,\n    this.initialRowId,\n    this.shrinkWrap = false,\n  });\n\n  final ViewPB view;\n  final DatabaseController databaseController;\n  final VoidCallback? onDeleted;\n  final String? initialRowId;\n  final bool shrinkWrap;\n\n  @override\n  State<MobileGridPage> createState() => _MobileGridPageState();\n}\n\nclass _MobileGridPageState extends State<MobileGridPage> {\n  bool _didOpenInitialRow = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider<ActionNavigationBloc>.value(\n          value: getIt<ActionNavigationBloc>(),\n        ),\n        BlocProvider<GridBloc>(\n          create: (context) => GridBloc(\n            view: widget.view,\n            databaseController: widget.databaseController,\n          )..add(const GridEvent.initial()),\n        ),\n      ],\n      child: BlocBuilder<GridBloc, GridState>(\n        builder: (context, state) {\n          return state.loadingState.map(\n            loading: (_) =>\n                const Center(child: CircularProgressIndicator.adaptive()),\n            finish: (result) {\n              _openRow(context, widget.initialRowId, true);\n              return result.successOrFail.fold(\n                (_) => GridPageContent(\n                  view: widget.view,\n                  shrinkWrap: widget.shrinkWrap,\n                ),\n                (err) => Center(\n                  child: AppFlowyErrorPage(\n                    error: err,\n                  ),\n                ),\n              );\n            },\n            idle: (_) => const SizedBox.shrink(),\n          );\n        },\n      ),\n    );\n  }\n\n  void _openRow(\n    BuildContext context,\n    String? rowId, [\n    bool initialRow = false,\n  ]) {\n    if (rowId != null && (!initialRow || (initialRow && !_didOpenInitialRow))) {\n      _didOpenInitialRow = initialRow;\n\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        context.push(\n          MobileRowDetailPage.routeName,\n          extra: {\n            MobileRowDetailPage.argRowId: rowId,\n            MobileRowDetailPage.argDatabaseController:\n                widget.databaseController,\n          },\n        );\n      });\n    }\n  }\n}\n\nclass GridPageContent extends StatefulWidget {\n  const GridPageContent({\n    super.key,\n    required this.view,\n    this.shrinkWrap = false,\n  });\n\n  final ViewPB view;\n  final bool shrinkWrap;\n\n  @override\n  State<GridPageContent> createState() => _GridPageContentState();\n}\n\nclass _GridPageContentState extends State<GridPageContent> {\n  final _scrollController = GridScrollController(\n    scrollGroupController: LinkedScrollControllerGroup(),\n  );\n  late final ScrollController contentScrollController;\n  late final ScrollController reorderableController;\n\n  @override\n  void initState() {\n    super.initState();\n    contentScrollController = _scrollController.linkHorizontalController();\n    reorderableController = _scrollController.linkHorizontalController();\n  }\n\n  @override\n  void dispose() {\n    _scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final isEditable =\n        context.read<PageAccessLevelBloc?>()?.state.isEditable ?? false;\n    return BlocListener<GridBloc, GridState>(\n      listenWhen: (previous, current) =>\n          previous.createdRow != current.createdRow,\n      listener: (context, state) {\n        if (state.createdRow == null || !state.openRowDetail) {\n          return;\n        }\n        final bloc = context.read<GridBloc>();\n        context.push(\n          MobileRowDetailPage.routeName,\n          extra: {\n            MobileRowDetailPage.argRowId: state.createdRow!.id,\n            MobileRowDetailPage.argDatabaseController: bloc.databaseController,\n          },\n        );\n        bloc.add(const GridEvent.resetCreatedRow());\n      },\n      child: Stack(\n        children: [\n          Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              _GridHeader(\n                contentScrollController: contentScrollController,\n                reorderableController: reorderableController,\n              ),\n              _GridRows(\n                viewId: widget.view.id,\n                scrollController: _scrollController,\n              ),\n            ],\n          ),\n          if (!widget.shrinkWrap && isEditable)\n            Positioned(\n              bottom: 16,\n              right: 16,\n              child: getGridFabs(context),\n            ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _GridHeader extends StatelessWidget {\n  const _GridHeader({\n    required this.contentScrollController,\n    required this.reorderableController,\n  });\n\n  final ScrollController contentScrollController;\n  final ScrollController reorderableController;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<GridBloc, GridState>(\n      builder: (context, state) {\n        return MobileGridHeader(\n          viewId: state.viewId,\n          contentScrollController: contentScrollController,\n          reorderableController: reorderableController,\n        );\n      },\n    );\n  }\n}\n\nclass _GridRows extends StatelessWidget {\n  const _GridRows({\n    required this.viewId,\n    required this.scrollController,\n  });\n\n  final String viewId;\n  final GridScrollController scrollController;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<GridBloc, GridState>(\n      buildWhen: (previous, current) => previous.fields != current.fields,\n      builder: (context, state) {\n        final double contentWidth = getMobileGridContentWidth(state.fields);\n        return Flexible(\n          child: _WrapScrollView(\n            scrollController: scrollController,\n            contentWidth: contentWidth,\n            child: BlocBuilder<GridBloc, GridState>(\n              buildWhen: (previous, current) => current.reason.maybeWhen(\n                reorderRows: () => true,\n                reorderSingleRow: (reorderRow, rowInfo) => true,\n                delete: (item) => true,\n                insert: (item) => true,\n                orElse: () => false,\n              ),\n              builder: (context, state) {\n                final behavior = ScrollConfiguration.of(context).copyWith(\n                  scrollbars: false,\n                  physics: const ClampingScrollPhysics(),\n                );\n                return ScrollConfiguration(\n                  behavior: behavior,\n                  child: _renderList(context, state),\n                );\n              },\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _renderList(\n    BuildContext context,\n    GridState state,\n  ) {\n    final children = state.rowInfos.mapIndexed((index, rowInfo) {\n      return ReorderableDelayedDragStartListener(\n        key: ValueKey(rowInfo.rowMeta.id),\n        index: index,\n        child: _renderRow(\n          context,\n          rowInfo.rowId,\n          isDraggable: state.reorderable,\n          index: index,\n        ),\n      );\n    }).toList();\n\n    return ReorderableListView.builder(\n      scrollController: scrollController.verticalController,\n      buildDefaultDragHandles: false,\n      shrinkWrap: true,\n      proxyDecorator: (child, index, animation) => Material(\n        color: Colors.transparent,\n        child: child,\n      ),\n      onReorder: (fromIndex, newIndex) {\n        final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;\n        if (fromIndex == toIndex) {\n          return;\n        }\n        context.read<GridBloc>().add(GridEvent.moveRow(fromIndex, toIndex));\n      },\n      itemCount: state.rowInfos.length,\n      itemBuilder: (context, index) => children[index],\n      footer: Padding(\n        padding: GridSize.footerContentInsets,\n        child: _AddRowButton(),\n      ),\n    );\n  }\n\n  Widget _renderRow(\n    BuildContext context,\n    RowId rowId, {\n    int? index,\n    required bool isDraggable,\n    Animation<double>? animation,\n  }) {\n    final rowMeta = context\n        .read<GridBloc>()\n        .databaseController\n        .rowCache\n        .getRow(rowId)\n        ?.rowMeta;\n\n    if (rowMeta == null) {\n      Log.warn('RowMeta is null for rowId: $rowId');\n      return const SizedBox.shrink();\n    }\n\n    final databaseController = context.read<GridBloc>().databaseController;\n\n    Widget child = MobileGridRow(\n      key: ValueKey(rowMeta.id),\n      rowId: rowId,\n      isDraggable: isDraggable,\n      databaseController: databaseController,\n      openDetailPage: (context) {\n        context.push(\n          MobileRowDetailPage.routeName,\n          extra: {\n            MobileRowDetailPage.argRowId: rowId,\n            MobileRowDetailPage.argDatabaseController: databaseController,\n          },\n        );\n      },\n    );\n\n    if (animation != null) {\n      child = SizeTransition(\n        sizeFactor: animation,\n        child: child,\n      );\n    }\n\n    final isEditable =\n        context.read<PageAccessLevelBloc?>()?.state.isEditable ?? false;\n    if (!isEditable) {\n      child = IgnorePointer(\n        child: child,\n      );\n    }\n\n    return child;\n  }\n}\n\nclass _WrapScrollView extends StatelessWidget {\n  const _WrapScrollView({\n    required this.contentWidth,\n    required this.scrollController,\n    required this.child,\n  });\n\n  final GridScrollController scrollController;\n  final double contentWidth;\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleChildScrollView(\n      controller: scrollController.horizontalController,\n      scrollDirection: Axis.horizontal,\n      child: SizedBox(\n        width: contentWidth,\n        child: child,\n      ),\n    );\n  }\n}\n\nclass _AddRowButton extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    final borderSide = BorderSide(\n      color: Theme.of(context).dividerColor,\n    );\n    const radius = BorderRadius.only(\n      bottomLeft: Radius.circular(24),\n      bottomRight: Radius.circular(24),\n    );\n    final decoration = BoxDecoration(\n      borderRadius: radius,\n      border: BorderDirectional(\n        start: borderSide,\n        end: borderSide,\n        bottom: borderSide,\n      ),\n    );\n    return Container(\n      height: 54,\n      decoration: decoration,\n      child: FlowyButton(\n        text: FlowyText(\n          LocaleKeys.grid_row_newRow.tr(),\n          fontSize: 15,\n          color: Theme.of(context).hintColor,\n        ),\n        margin: const EdgeInsets.symmetric(horizontal: 20.0),\n        radius: radius,\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        onTap: () => context.read<GridBloc>().add(const GridEvent.createRow()),\n        leftIcon: FlowySvg(\n          FlowySvgs.add_s,\n          color: Theme.of(context).hintColor,\n          size: const Size.square(18),\n        ),\n        leftIconSize: const Size.square(18),\n      ),\n    );\n  }\n}\n\ndouble getMobileGridContentWidth(List<FieldInfo> fields) {\n  final visibleFields = fields.where(\n    (field) => field.visibility != FieldVisibility.AlwaysHidden,\n  );\n  return (visibleFields.length + 1) * 200 +\n      GridSize.horizontalHeaderPadding * 2;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/application/calculations/calculations_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/application/calculations/field_type_calc_ext.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_selector.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass CalculateCell extends StatefulWidget {\n  const CalculateCell({\n    super.key,\n    required this.fieldInfo,\n    required this.width,\n    this.calculation,\n  });\n\n  final FieldInfo fieldInfo;\n  final double width;\n  final CalculationPB? calculation;\n\n  @override\n  State<CalculateCell> createState() => _CalculateCellState();\n}\n\nclass _CalculateCellState extends State<CalculateCell> {\n  final _cellScrollController = ScrollController();\n  bool isSelected = false;\n  bool isScrollable = false;\n\n  @override\n  void initState() {\n    super.initState();\n    _checkScrollable();\n  }\n\n  @override\n  void didUpdateWidget(covariant CalculateCell oldWidget) {\n    _checkScrollable();\n    super.didUpdateWidget(oldWidget);\n  }\n\n  void _checkScrollable() {\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      if (_cellScrollController.hasClients) {\n        setState(\n          () =>\n              isScrollable = _cellScrollController.position.maxScrollExtent > 0,\n        );\n      }\n    });\n  }\n\n  @override\n  void dispose() {\n    _cellScrollController.dispose();\n    super.dispose();\n  }\n\n  void setIsSelected(bool selected) => setState(() => isSelected = selected);\n\n  @override\n  Widget build(BuildContext context) {\n    final prefix = _prefixFromFieldType(widget.fieldInfo.fieldType);\n\n    return SizedBox(\n      height: 35,\n      width: widget.width,\n      child: AppFlowyPopover(\n        constraints: BoxConstraints.loose(const Size(150, 200)),\n        direction: PopoverDirection.bottomWithCenterAligned,\n        onClose: () => setIsSelected(false),\n        popupBuilder: (_) {\n          WidgetsBinding.instance.addPostFrameCallback((_) {\n            if (mounted) {\n              setIsSelected(true);\n            }\n          });\n\n          return SingleChildScrollView(\n            child: Column(\n              children: [\n                if (widget.calculation != null)\n                  RemoveCalculationButton(\n                    onTap: () => context.read<CalculationsBloc>().add(\n                          CalculationsEvent.removeCalculation(\n                            widget.fieldInfo.id,\n                            widget.calculation!.id,\n                          ),\n                        ),\n                  ),\n                ...widget.fieldInfo.fieldType.calculationsForFieldType().map(\n                      (type) => CalculationTypeItem(\n                        type: type,\n                        onTap: () {\n                          if (type != widget.calculation?.calculationType) {\n                            context.read<CalculationsBloc>().add(\n                                  CalculationsEvent.updateCalculationType(\n                                    widget.fieldInfo.id,\n                                    type,\n                                    calculationId: widget.calculation?.id,\n                                  ),\n                                );\n                          }\n                        },\n                      ),\n                    ),\n              ],\n            ),\n          );\n        },\n        child: widget.calculation != null\n            ? _showCalculateValue(context, prefix)\n            : CalculationSelector(isSelected: isSelected),\n      ),\n    );\n  }\n\n  Widget _showCalculateValue(BuildContext context, String? prefix) {\n    prefix = prefix != null ? '$prefix ' : '';\n    final calculateValue =\n        '$prefix${_withoutTrailingZeros(widget.calculation!.value)}';\n\n    return FlowyTooltip(\n      message: !isScrollable ? \"\" : null,\n      richMessage: !isScrollable\n          ? null\n          : TextSpan(\n              children: [\n                TextSpan(\n                  text: widget.calculation!.calculationType.shortLabel\n                      .toUpperCase(),\n                  style: context.tooltipTextStyle(),\n                ),\n                const TextSpan(text: ' '),\n                TextSpan(\n                  text: calculateValue,\n                  style: context\n                      .tooltipTextStyle()\n                      ?.copyWith(fontWeight: FontWeight.w500),\n                ),\n              ],\n            ),\n      child: FlowyButton(\n        radius: BorderRadius.zero,\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        text: Row(\n          children: [\n            Expanded(\n              child: SingleChildScrollView(\n                controller: _cellScrollController,\n                key: ValueKey(widget.calculation!.id),\n                reverse: true,\n                physics: const NeverScrollableScrollPhysics(),\n                scrollDirection: Axis.horizontal,\n                child: Row(\n                  mainAxisSize: MainAxisSize.min,\n                  mainAxisAlignment: MainAxisAlignment.end,\n                  children: [\n                    FlowyText(\n                      lineHeight: 1.0,\n                      widget.calculation!.calculationType.shortLabel\n                          .toUpperCase(),\n                      color: Theme.of(context).hintColor,\n                      fontSize: 10,\n                    ),\n                    if (widget.calculation!.value.isNotEmpty) ...[\n                      const HSpace(8),\n                      FlowyText(\n                        lineHeight: 1.0,\n                        calculateValue,\n                        color: AFThemeExtension.of(context).textColor,\n                        overflow: TextOverflow.ellipsis,\n                      ),\n                    ],\n                  ],\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  String _withoutTrailingZeros(String value) {\n    if (trailingZerosRegex.hasMatch(value)) {\n      final match = trailingZerosRegex.firstMatch(value)!;\n      return match.group(1)!;\n    }\n\n    return value;\n  }\n\n  String? _prefixFromFieldType(FieldType fieldType) => switch (fieldType) {\n        FieldType.Number =>\n          NumberTypeOptionPB.fromBuffer(widget.fieldInfo.field.typeOptionData)\n              .format\n              .iconSymbol(false),\n        _ => null,\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_selector.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\n\nclass CalculationSelector extends StatefulWidget {\n  const CalculationSelector({\n    super.key,\n    required this.isSelected,\n  });\n\n  final bool isSelected;\n\n  @override\n  State<CalculationSelector> createState() => _CalculationSelectorState();\n}\n\nclass _CalculationSelectorState extends State<CalculationSelector> {\n  bool _isHovering = false;\n\n  void _setHovering(bool isHovering) =>\n      setState(() => _isHovering = isHovering);\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      onEnter: (_) => _setHovering(true),\n      onExit: (_) => _setHovering(false),\n      child: AnimatedOpacity(\n        duration: const Duration(milliseconds: 100),\n        opacity: widget.isSelected || _isHovering ? 1 : 0,\n        child: FlowyButton(\n          radius: BorderRadius.zero,\n          hoverColor: AFThemeExtension.of(context).lightGreyHover,\n          text: Row(\n            mainAxisAlignment: MainAxisAlignment.end,\n            children: [\n              Flexible(\n                child: FlowyText(\n                  LocaleKeys.grid_calculate.tr(),\n                  color: Theme.of(context).hintColor,\n                  overflow: TextOverflow.ellipsis,\n                ),\n              ),\n              const HSpace(8),\n              FlowySvg(\n                FlowySvgs.arrow_down_s,\n                color: Theme.of(context).hintColor,\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pbenum.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\n\nclass CalculationTypeItem extends StatelessWidget {\n  const CalculationTypeItem({\n    super.key,\n    required this.type,\n    required this.onTap,\n  });\n\n  final CalculationType type;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        text: FlowyText(\n          type.label,\n          overflow: TextOverflow.ellipsis,\n          lineHeight: 1.0,\n        ),\n        onTap: () {\n          onTap();\n          PopoverContainer.of(context).close();\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart",
    "content": "import 'package:appflowy/plugins/database/grid/application/calculations/calculations_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart';\nimport 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass GridCalculationsRow extends StatelessWidget {\n  const GridCalculationsRow({\n    super.key,\n    required this.viewId,\n    this.includeDefaultInsets = true,\n  });\n\n  final String viewId;\n  final bool includeDefaultInsets;\n\n  @override\n  Widget build(BuildContext context) {\n    final gridBloc = context.read<GridBloc>();\n\n    return BlocProvider(\n      create: (context) => CalculationsBloc(\n        viewId: gridBloc.databaseController.viewId,\n        fieldController: gridBloc.databaseController.fieldController,\n      )..add(const CalculationsEvent.started()),\n      child: BlocBuilder<CalculationsBloc, CalculationsState>(\n        builder: (context, state) {\n          final padding =\n              context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding;\n          return Padding(\n            padding: includeDefaultInsets\n                ? EdgeInsets.symmetric(horizontal: padding)\n                : EdgeInsets.zero,\n            child: Row(\n              children: [\n                ...state.fields.map(\n                  (field) => CalculateCell(\n                    key: Key(\n                      '${field.id}-${state.calculationsByFieldId[field.id]?.id}',\n                    ),\n                    width: field.width!.toDouble(),\n                    fieldInfo: field,\n                    calculation: state.calculationsByFieldId[field.id],\n                  ),\n                ),\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\n\nclass RemoveCalculationButton extends StatelessWidget {\n  const RemoveCalculationButton({\n    super.key,\n    required this.onTap,\n  });\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        text: FlowyText(\n          LocaleKeys.grid_calculationTypeLabel_none.tr(),\n          overflow: TextOverflow.ellipsis,\n        ),\n        onTap: () {\n          onTap();\n          PopoverContainer.of(context).close();\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/common/type_option_separator.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass TypeOptionSeparator extends StatelessWidget {\n  const TypeOptionSeparator({this.spacing = 6.0, super.key});\n\n  final double spacing;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: EdgeInsets.symmetric(vertical: spacing),\n      child: Container(\n        color: Theme.of(context).dividerColor,\n        height: 1.0,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../condition_button.dart';\nimport '../disclosure_button.dart';\n\nimport 'choicechip.dart';\n\nclass CheckboxFilterChoicechip extends StatelessWidget {\n  const CheckboxFilterChoicechip({\n    super.key,\n    required this.filterId,\n  });\n\n  final String filterId;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      constraints: BoxConstraints.loose(const Size(200, 76)),\n      direction: PopoverDirection.bottomWithCenterAligned,\n      popupBuilder: (_) {\n        return BlocProvider.value(\n          value: context.read<FilterEditorBloc>(),\n          child: CheckboxFilterEditor(\n            filterId: filterId,\n          ),\n        );\n      },\n      child: SingleFilterBlocSelector<CheckboxFilter>(\n        filterId: filterId,\n        builder: (context, filter, field) {\n          return ChoiceChipButton(\n            fieldInfo: field,\n            filterDesc: filter.condition.filterName,\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass CheckboxFilterEditor extends StatefulWidget {\n  const CheckboxFilterEditor({\n    super.key,\n    required this.filterId,\n  });\n\n  final String filterId;\n\n  @override\n  State<CheckboxFilterEditor> createState() => _CheckboxFilterEditorState();\n}\n\nclass _CheckboxFilterEditorState extends State<CheckboxFilterEditor> {\n  final popoverMutex = PopoverMutex();\n\n  @override\n  void dispose() {\n    popoverMutex.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleFilterBlocSelector<CheckboxFilter>(\n      filterId: widget.filterId,\n      builder: (context, filter, field) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),\n          child: IntrinsicHeight(\n            child: Row(\n              children: [\n                Expanded(\n                  child: FlowyText(\n                    field.name,\n                    overflow: TextOverflow.ellipsis,\n                  ),\n                ),\n                const HSpace(4),\n                CheckboxFilterConditionList(\n                  filter: filter,\n                  popoverMutex: popoverMutex,\n                  onCondition: (condition) {\n                    final newFilter = filter.copyWith(condition: condition);\n                    context\n                        .read<FilterEditorBloc>()\n                        .add(FilterEditorEvent.updateFilter(newFilter));\n                  },\n                ),\n                DisclosureButton(\n                  popoverMutex: popoverMutex,\n                  onAction: (action) {\n                    switch (action) {\n                      case FilterDisclosureAction.delete:\n                        context.read<FilterEditorBloc>().add(\n                              FilterEditorEvent.deleteFilter(\n                                filter.filterId,\n                              ),\n                            );\n                        break;\n                    }\n                  },\n                ),\n              ],\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass CheckboxFilterConditionList extends StatelessWidget {\n  const CheckboxFilterConditionList({\n    super.key,\n    required this.filter,\n    required this.popoverMutex,\n    required this.onCondition,\n  });\n\n  final CheckboxFilter filter;\n  final PopoverMutex popoverMutex;\n  final void Function(CheckboxFilterConditionPB) onCondition;\n\n  @override\n  Widget build(BuildContext context) {\n    return PopoverActionList<ConditionWrapper>(\n      asBarrier: true,\n      mutex: popoverMutex,\n      direction: PopoverDirection.bottomWithCenterAligned,\n      actions: CheckboxFilterConditionPB.values\n          .map(\n            (action) => ConditionWrapper(\n              action,\n              filter.condition == action,\n            ),\n          )\n          .toList(),\n      buildChild: (controller) {\n        return ConditionButton(\n          conditionName: filter.conditionName,\n          onTap: () => controller.show(),\n        );\n      },\n      onSelected: (action, controller) {\n        onCondition(action.inner);\n        controller.close();\n      },\n    );\n  }\n}\n\nclass ConditionWrapper extends ActionCell {\n  ConditionWrapper(this.inner, this.isSelected);\n\n  final CheckboxFilterConditionPB inner;\n  final bool isSelected;\n\n  @override\n  Widget? rightIcon(Color iconColor) =>\n      isSelected ? const FlowySvg(FlowySvgs.check_s) : null;\n\n  @override\n  String get name => inner.filterName;\n}\n\nextension TextFilterConditionPBExtension on CheckboxFilterConditionPB {\n  String get filterName {\n    switch (this) {\n      case CheckboxFilterConditionPB.IsChecked:\n        return LocaleKeys.grid_checkboxFilter_isChecked.tr();\n      case CheckboxFilterConditionPB.IsUnChecked:\n        return LocaleKeys.grid_checkboxFilter_isUnchecked.tr();\n      default:\n        return \"\";\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/checklist.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pbenum.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../condition_button.dart';\nimport '../disclosure_button.dart';\nimport 'choicechip.dart';\n\nclass ChecklistFilterChoicechip extends StatelessWidget {\n  const ChecklistFilterChoicechip({\n    super.key,\n    required this.filterId,\n  });\n\n  final String filterId;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      controller: PopoverController(),\n      constraints: BoxConstraints.loose(const Size(200, 160)),\n      direction: PopoverDirection.bottomWithCenterAligned,\n      popupBuilder: (_) {\n        return BlocProvider.value(\n          value: context.read<FilterEditorBloc>(),\n          child: ChecklistFilterEditor(filterId: filterId),\n        );\n      },\n      child: SingleFilterBlocSelector<ChecklistFilter>(\n        filterId: filterId,\n        builder: (context, filter, field) {\n          return ChoiceChipButton(\n            fieldInfo: field,\n            filterDesc: filter.getContentDescription(field),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass ChecklistFilterEditor extends StatefulWidget {\n  const ChecklistFilterEditor({\n    super.key,\n    required this.filterId,\n  });\n\n  final String filterId;\n\n  @override\n  ChecklistState createState() => ChecklistState();\n}\n\nclass ChecklistState extends State<ChecklistFilterEditor> {\n  final PopoverMutex popoverMutex = PopoverMutex();\n\n  @override\n  void dispose() {\n    popoverMutex.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleFilterBlocSelector<ChecklistFilter>(\n      filterId: widget.filterId,\n      builder: (context, filter, field) {\n        return SizedBox(\n          height: 20,\n          child: Row(\n            children: [\n              Expanded(\n                child: FlowyText(\n                  field.name,\n                  overflow: TextOverflow.ellipsis,\n                ),\n              ),\n              const HSpace(4),\n              ChecklistFilterConditionList(\n                filter: filter,\n                popoverMutex: popoverMutex,\n                onCondition: (condition) {\n                  final newFilter = filter.copyWith(condition: condition);\n                  context\n                      .read<FilterEditorBloc>()\n                      .add(FilterEditorEvent.updateFilter(newFilter));\n                },\n              ),\n              DisclosureButton(\n                popoverMutex: popoverMutex,\n                onAction: (action) {\n                  switch (action) {\n                    case FilterDisclosureAction.delete:\n                      context\n                          .read<FilterEditorBloc>()\n                          .add(FilterEditorEvent.deleteFilter(filter.filterId));\n                      break;\n                  }\n                },\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass ChecklistFilterConditionList extends StatelessWidget {\n  const ChecklistFilterConditionList({\n    super.key,\n    required this.filter,\n    required this.popoverMutex,\n    required this.onCondition,\n  });\n\n  final ChecklistFilter filter;\n  final PopoverMutex popoverMutex;\n  final void Function(ChecklistFilterConditionPB) onCondition;\n\n  @override\n  Widget build(BuildContext context) {\n    return PopoverActionList<ConditionWrapper>(\n      asBarrier: true,\n      direction: PopoverDirection.bottomWithCenterAligned,\n      mutex: popoverMutex,\n      actions: ChecklistFilterConditionPB.values\n          .map((action) => ConditionWrapper(action))\n          .toList(),\n      buildChild: (controller) {\n        return ConditionButton(\n          conditionName: filter.condition.filterName,\n          onTap: () => controller.show(),\n        );\n      },\n      onSelected: (action, controller) {\n        onCondition(action.inner);\n        controller.close();\n      },\n    );\n  }\n}\n\nclass ConditionWrapper extends ActionCell {\n  ConditionWrapper(this.inner);\n\n  final ChecklistFilterConditionPB inner;\n\n  @override\n  String get name => inner.filterName;\n}\n\nextension ChecklistFilterConditionPBExtension on ChecklistFilterConditionPB {\n  String get filterName {\n    switch (this) {\n      case ChecklistFilterConditionPB.IsComplete:\n        return LocaleKeys.grid_checklistFilter_isComplete.tr();\n      case ChecklistFilterConditionPB.IsIncomplete:\n        return LocaleKeys.grid_checklistFilter_isIncomplted.tr();\n      default:\n        return \"\";\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass ChoiceChipButton extends StatelessWidget {\n  const ChoiceChipButton({\n    super.key,\n    required this.fieldInfo,\n    this.filterDesc = '',\n    this.onTap,\n  });\n\n  final FieldInfo fieldInfo;\n  final String filterDesc;\n  final VoidCallback? onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    final buttonText =\n        filterDesc.isEmpty ? fieldInfo.name : \"${fieldInfo.name}: $filterDesc\";\n\n    return SizedBox(\n      height: 28,\n      child: FlowyButton(\n        decoration: BoxDecoration(\n          color: Colors.transparent,\n          border: Border.fromBorderSide(\n            BorderSide(\n              color: AFThemeExtension.of(context).toggleOffFill,\n            ),\n          ),\n          borderRadius: const BorderRadius.all(Radius.circular(14)),\n        ),\n        useIntrinsicWidth: true,\n        text: FlowyText(\n          buttonText,\n          lineHeight: 1.0,\n          color: AFThemeExtension.of(context).textColor,\n        ),\n        margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),\n        radius: const BorderRadius.all(Radius.circular(14)),\n        leftIcon: FieldIcon(\n          fieldInfo: fieldInfo,\n        ),\n        rightIcon: const _ChoicechipDownArrow(),\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        onTap: onTap,\n      ),\n    );\n  }\n}\n\nclass _ChoicechipDownArrow extends StatelessWidget {\n  const _ChoicechipDownArrow();\n\n  @override\n  Widget build(BuildContext context) {\n    return Transform.rotate(\n      angle: -math.pi / 2,\n      child: FlowySvg(\n        FlowySvgs.arrow_left_s,\n        color: AFThemeExtension.of(context).textColor,\n      ),\n    );\n  }\n}\n\nclass SingleFilterBlocSelector<T extends DatabaseFilter>\n    extends StatelessWidget {\n  const SingleFilterBlocSelector({\n    super.key,\n    required this.filterId,\n    required this.builder,\n  });\n\n  final String filterId;\n  final Widget Function(BuildContext, T, FieldInfo) builder;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocSelector<FilterEditorBloc, FilterEditorState, (T, FieldInfo)?>(\n      selector: (state) {\n        final filter = state.filters\n            .firstWhereOrNull((filter) => filter.filterId == filterId) as T?;\n        if (filter == null) {\n          return null;\n        }\n        final field = state.fields\n            .firstWhereOrNull((field) => field.id == filter.fieldId);\n        if (field == null) {\n          return null;\n        }\n        return (filter, field);\n      },\n      builder: (context, selection) {\n        if (selection == null) {\n          return const SizedBox.shrink();\n        }\n        return builder(context, selection.$1, selection.$2);\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart",
    "content": "import 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../condition_button.dart';\nimport '../disclosure_button.dart';\n\nimport 'choicechip.dart';\n\nclass DateFilterChoicechip extends StatelessWidget {\n  const DateFilterChoicechip({\n    super.key,\n    required this.filterId,\n  });\n\n  final String filterId;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      constraints: BoxConstraints.loose(const Size(275, 120)),\n      direction: PopoverDirection.bottomWithLeftAligned,\n      popupBuilder: (_) {\n        return BlocProvider.value(\n          value: context.read<FilterEditorBloc>(),\n          child: DateFilterEditor(filterId: filterId),\n        );\n      },\n      child: SingleFilterBlocSelector<DateTimeFilter>(\n        filterId: filterId,\n        builder: (context, filter, field) {\n          return ChoiceChipButton(\n            fieldInfo: field,\n            filterDesc: filter.getContentDescription(field),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass DateFilterEditor extends StatefulWidget {\n  const DateFilterEditor({\n    super.key,\n    required this.filterId,\n  });\n\n  final String filterId;\n\n  @override\n  State<DateFilterEditor> createState() => _DateFilterEditorState();\n}\n\nclass _DateFilterEditorState extends State<DateFilterEditor> {\n  final popoverMutex = PopoverMutex();\n  final popooverController = PopoverController();\n\n  @override\n  void dispose() {\n    popoverMutex.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleFilterBlocSelector<DateTimeFilter>(\n      filterId: widget.filterId,\n      builder: (context, filter, field) {\n        final List<Widget> children = [\n          _buildFilterPanel(filter, field),\n          if (![\n            DateFilterConditionPB.DateStartIsEmpty,\n            DateFilterConditionPB.DateStartIsNotEmpty,\n            DateFilterConditionPB.DateEndIsEmpty,\n            DateFilterConditionPB.DateStartIsNotEmpty,\n          ].contains(filter.condition)) ...[\n            const VSpace(4),\n            _buildFilterContentField(filter),\n          ],\n        ];\n\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),\n          child: IntrinsicHeight(child: Column(children: children)),\n        );\n      },\n    );\n  }\n\n  Widget _buildFilterPanel(\n    DateTimeFilter filter,\n    FieldInfo field,\n  ) {\n    return SizedBox(\n      height: 20,\n      child: Row(\n        children: [\n          Expanded(\n            child: field.fieldType == FieldType.DateTime\n                ? DateFilterIsStartList(\n                    filter: filter,\n                    popoverMutex: popoverMutex,\n                    onChangeIsStart: (isStart) {\n                      final newFilter = filter.copyWithCondition(\n                        isStart: isStart,\n                        condition: filter.condition.toCondition(),\n                      );\n                      context\n                          .read<FilterEditorBloc>()\n                          .add(FilterEditorEvent.updateFilter(newFilter));\n                    },\n                  )\n                : FlowyText(\n                    field.name,\n                    overflow: TextOverflow.ellipsis,\n                  ),\n          ),\n          const HSpace(4),\n          Expanded(\n            child: DateFilterConditionList(\n              filter: filter,\n              popoverMutex: popoverMutex,\n              onCondition: (condition) {\n                final newFilter = filter.copyWithCondition(\n                  isStart: filter.condition.isStart,\n                  condition: condition,\n                );\n                context\n                    .read<FilterEditorBloc>()\n                    .add(FilterEditorEvent.updateFilter(newFilter));\n              },\n            ),\n          ),\n          const HSpace(4),\n          DisclosureButton(\n            popoverMutex: popoverMutex,\n            onAction: (action) {\n              switch (action) {\n                case FilterDisclosureAction.delete:\n                  context\n                      .read<FilterEditorBloc>()\n                      .add(FilterEditorEvent.deleteFilter(filter.filterId));\n                  break;\n              }\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildFilterContentField(DateTimeFilter filter) {\n    final isRange = filter.condition.isRange;\n    String? text;\n\n    if (isRange) {\n      text =\n          \"${filter.start?.defaultFormat ?? \"\"} - ${filter.end?.defaultFormat ?? \"\"}\";\n      text = text == \" - \" ? null : text;\n    } else {\n      text = filter.timestamp.defaultFormat;\n    }\n\n    return AppFlowyPopover(\n      controller: popooverController,\n      triggerActions: PopoverTriggerFlags.none,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      constraints: BoxConstraints.loose(const Size(260, 620)),\n      offset: const Offset(0, 4),\n      margin: EdgeInsets.zero,\n      mutex: popoverMutex,\n      child: FlowyButton(\n        decoration: BoxDecoration(\n          border: Border.fromBorderSide(\n            BorderSide(color: Theme.of(context).colorScheme.outline),\n          ),\n          borderRadius: Corners.s6Border,\n        ),\n        onTap: popooverController.show,\n        text: FlowyText(\n          text ?? \"\",\n          overflow: TextOverflow.ellipsis,\n        ),\n      ),\n      popupBuilder: (_) {\n        return BlocProvider.value(\n          value: context.read<FilterEditorBloc>(),\n          child: SingleFilterBlocSelector<DateTimeFilter>(\n            filterId: widget.filterId,\n            builder: (context, filter, field) {\n              return DesktopAppFlowyDatePicker(\n                isRange: isRange,\n                includeTime: false,\n                dateFormat: DateFormatPB.Friendly,\n                timeFormat: TimeFormatPB.TwentyFourHour,\n                dateTime: isRange ? filter.start : filter.timestamp,\n                endDateTime: isRange ? filter.end : null,\n                onDaySelected: (selectedDay) {\n                  final newFilter = isRange\n                      ? filter.copyWithRange(start: selectedDay, end: null)\n                      : filter.copyWithTimestamp(timestamp: selectedDay);\n                  context\n                      .read<FilterEditorBloc>()\n                      .add(FilterEditorEvent.updateFilter(newFilter));\n                  if (isRange) {\n                    popooverController.close();\n                  }\n                },\n                onRangeSelected: (start, end) {\n                  final newFilter = filter.copyWithRange(\n                    start: start,\n                    end: end,\n                  );\n                  context\n                      .read<FilterEditorBloc>()\n                      .add(FilterEditorEvent.updateFilter(newFilter));\n                },\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass DateFilterIsStartList extends StatelessWidget {\n  const DateFilterIsStartList({\n    super.key,\n    required this.filter,\n    required this.popoverMutex,\n    required this.onChangeIsStart,\n  });\n\n  final DateTimeFilter filter;\n  final PopoverMutex popoverMutex;\n  final Function(bool isStart) onChangeIsStart;\n\n  @override\n  Widget build(BuildContext context) {\n    return PopoverActionList<_IsStartWrapper>(\n      asBarrier: true,\n      mutex: popoverMutex,\n      direction: PopoverDirection.bottomWithCenterAligned,\n      actions: [\n        _IsStartWrapper(\n          true,\n          filter.condition.isStart,\n        ),\n        _IsStartWrapper(\n          false,\n          !filter.condition.isStart,\n        ),\n      ],\n      buildChild: (controller) {\n        return ConditionButton(\n          conditionName: filter.condition.isStart\n              ? LocaleKeys.grid_dateFilter_startDate.tr()\n              : LocaleKeys.grid_dateFilter_endDate.tr(),\n          onTap: () => controller.show(),\n        );\n      },\n      onSelected: (action, controller) {\n        onChangeIsStart(action.inner);\n        controller.close();\n      },\n    );\n  }\n}\n\nclass _IsStartWrapper extends ActionCell {\n  _IsStartWrapper(this.inner, this.isSelected);\n\n  final bool inner;\n  final bool isSelected;\n\n  @override\n  Widget? rightIcon(Color iconColor) =>\n      isSelected ? const FlowySvg(FlowySvgs.check_s) : null;\n\n  @override\n  String get name => inner\n      ? LocaleKeys.grid_dateFilter_startDate.tr()\n      : LocaleKeys.grid_dateFilter_endDate.tr();\n}\n\nclass DateFilterConditionList extends StatelessWidget {\n  const DateFilterConditionList({\n    super.key,\n    required this.filter,\n    required this.popoverMutex,\n    required this.onCondition,\n  });\n\n  final DateTimeFilter filter;\n  final PopoverMutex popoverMutex;\n  final Function(DateTimeFilterCondition) onCondition;\n\n  @override\n  Widget build(BuildContext context) {\n    final conditions = DateTimeFilterCondition.availableConditionsForFieldType(\n      filter.fieldType,\n    );\n    return PopoverActionList<ConditionWrapper>(\n      asBarrier: true,\n      mutex: popoverMutex,\n      direction: PopoverDirection.bottomWithCenterAligned,\n      actions: conditions\n          .map(\n            (action) => ConditionWrapper(\n              action,\n              filter.condition.toCondition() == action,\n            ),\n          )\n          .toList(),\n      buildChild: (controller) {\n        return ConditionButton(\n          conditionName: filter.condition.toCondition().filterName,\n          onTap: () => controller.show(),\n        );\n      },\n      onSelected: (action, controller) {\n        onCondition(action.inner);\n        controller.close();\n      },\n    );\n  }\n}\n\nclass ConditionWrapper extends ActionCell {\n  ConditionWrapper(this.inner, this.isSelected);\n\n  final DateTimeFilterCondition inner;\n  final bool isSelected;\n\n  @override\n  Widget? rightIcon(Color iconColor) =>\n      isSelected ? const FlowySvg(FlowySvgs.check_s) : null;\n\n  @override\n  String get name => inner.filterName;\n}\n\nextension DateFilterConditionPBExtension on DateFilterConditionPB {\n  bool get isStart {\n    return switch (this) {\n      DateFilterConditionPB.DateStartsOn ||\n      DateFilterConditionPB.DateStartsBefore ||\n      DateFilterConditionPB.DateStartsAfter ||\n      DateFilterConditionPB.DateStartsOnOrBefore ||\n      DateFilterConditionPB.DateStartsOnOrAfter ||\n      DateFilterConditionPB.DateStartsBetween ||\n      DateFilterConditionPB.DateStartIsEmpty ||\n      DateFilterConditionPB.DateStartIsNotEmpty =>\n        true,\n      _ => false\n    };\n  }\n\n  bool get isRange {\n    return switch (this) {\n      DateFilterConditionPB.DateStartsBetween ||\n      DateFilterConditionPB.DateEndsBetween =>\n        true,\n      _ => false,\n    };\n  }\n\n  DateTimeFilterCondition toCondition() {\n    return switch (this) {\n      DateFilterConditionPB.DateStartsOn ||\n      DateFilterConditionPB.DateEndsOn =>\n        DateTimeFilterCondition.on,\n      DateFilterConditionPB.DateStartsBefore ||\n      DateFilterConditionPB.DateEndsBefore =>\n        DateTimeFilterCondition.before,\n      DateFilterConditionPB.DateStartsAfter ||\n      DateFilterConditionPB.DateEndsAfter =>\n        DateTimeFilterCondition.after,\n      DateFilterConditionPB.DateStartsOnOrBefore ||\n      DateFilterConditionPB.DateEndsOnOrBefore =>\n        DateTimeFilterCondition.onOrBefore,\n      DateFilterConditionPB.DateStartsOnOrAfter ||\n      DateFilterConditionPB.DateEndsOnOrAfter =>\n        DateTimeFilterCondition.onOrAfter,\n      DateFilterConditionPB.DateStartsBetween ||\n      DateFilterConditionPB.DateEndsBetween =>\n        DateTimeFilterCondition.between,\n      DateFilterConditionPB.DateStartIsEmpty ||\n      DateFilterConditionPB.DateEndIsEmpty =>\n        DateTimeFilterCondition.isEmpty,\n      DateFilterConditionPB.DateStartIsNotEmpty ||\n      DateFilterConditionPB.DateEndIsNotEmpty =>\n        DateTimeFilterCondition.isNotEmpty,\n      _ => throw ArgumentError(),\n    };\n  }\n}\n\nextension DateTimeChoicechipExtension on DateTime {\n  DateTime get considerLocal {\n    return DateTime(year, month, day);\n  }\n}\n\nextension DateTimeDefaultFormatExtension on DateTime? {\n  String? get defaultFormat {\n    return this != null ? DateFormat('dd/MM/yyyy').format(this!) : null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/number.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../condition_button.dart';\nimport '../disclosure_button.dart';\n\nimport 'choicechip.dart';\n\nclass NumberFilterChoiceChip extends StatelessWidget {\n  const NumberFilterChoiceChip({\n    super.key,\n    required this.filterId,\n  });\n\n  final String filterId;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      constraints: BoxConstraints.loose(const Size(200, 100)),\n      direction: PopoverDirection.bottomWithCenterAligned,\n      popupBuilder: (_) {\n        return BlocProvider.value(\n          value: context.read<FilterEditorBloc>(),\n          child: NumberFilterEditor(filterId: filterId),\n        );\n      },\n      child: SingleFilterBlocSelector<NumberFilter>(\n        filterId: filterId,\n        builder: (context, filter, field) {\n          return ChoiceChipButton(\n            fieldInfo: field,\n            filterDesc: filter.getContentDescription(field),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass NumberFilterEditor extends StatefulWidget {\n  const NumberFilterEditor({\n    super.key,\n    required this.filterId,\n  });\n\n  final String filterId;\n\n  @override\n  State<NumberFilterEditor> createState() => _NumberFilterEditorState();\n}\n\nclass _NumberFilterEditorState extends State<NumberFilterEditor> {\n  final popoverMutex = PopoverMutex();\n\n  @override\n  void dispose() {\n    popoverMutex.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleFilterBlocSelector<NumberFilter>(\n      filterId: widget.filterId,\n      builder: (context, filter, field) {\n        final List<Widget> children = [\n          _buildFilterPanel(filter, field),\n          if (filter.condition != NumberFilterConditionPB.NumberIsEmpty &&\n              filter.condition != NumberFilterConditionPB.NumberIsNotEmpty) ...[\n            const VSpace(4),\n            _buildFilterNumberField(filter),\n          ],\n        ];\n\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),\n          child: IntrinsicHeight(child: Column(children: children)),\n        );\n      },\n    );\n  }\n\n  Widget _buildFilterPanel(\n    NumberFilter filter,\n    FieldInfo field,\n  ) {\n    return SizedBox(\n      height: 20,\n      child: Row(\n        children: [\n          Expanded(\n            child: FlowyText(\n              field.name,\n              overflow: TextOverflow.ellipsis,\n            ),\n          ),\n          const HSpace(4),\n          Expanded(\n            child: NumberFilterConditionList(\n              filter: filter,\n              popoverMutex: popoverMutex,\n              onCondition: (condition) {\n                final newFilter = filter.copyWith(condition: condition);\n                context\n                    .read<FilterEditorBloc>()\n                    .add(FilterEditorEvent.updateFilter(newFilter));\n              },\n            ),\n          ),\n          const HSpace(4),\n          DisclosureButton(\n            popoverMutex: popoverMutex,\n            onAction: (action) {\n              switch (action) {\n                case FilterDisclosureAction.delete:\n                  context.read<FilterEditorBloc>().add(\n                        FilterEditorEvent.deleteFilter(\n                          filter.filterId,\n                        ),\n                      );\n                  break;\n              }\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildFilterNumberField(\n    NumberFilter filter,\n  ) {\n    return FlowyTextField(\n      text: filter.content,\n      hintText: LocaleKeys.grid_settings_typeAValue.tr(),\n      debounceDuration: const Duration(milliseconds: 300),\n      autoFocus: false,\n      onChanged: (text) {\n        final newFilter = filter.copyWith(content: text);\n        context\n            .read<FilterEditorBloc>()\n            .add(FilterEditorEvent.updateFilter(newFilter));\n      },\n    );\n  }\n}\n\nclass NumberFilterConditionList extends StatelessWidget {\n  const NumberFilterConditionList({\n    super.key,\n    required this.filter,\n    required this.popoverMutex,\n    required this.onCondition,\n  });\n\n  final NumberFilter filter;\n  final PopoverMutex popoverMutex;\n  final void Function(NumberFilterConditionPB) onCondition;\n\n  @override\n  Widget build(BuildContext context) {\n    return PopoverActionList<ConditionWrapper>(\n      asBarrier: true,\n      mutex: popoverMutex,\n      direction: PopoverDirection.bottomWithCenterAligned,\n      actions: NumberFilterConditionPB.values\n          .map(\n            (action) => ConditionWrapper(\n              action,\n              filter.condition == action,\n            ),\n          )\n          .toList(),\n      buildChild: (controller) {\n        return ConditionButton(\n          conditionName: filter.condition.filterName,\n          onTap: () => controller.show(),\n        );\n      },\n      onSelected: (action, controller) {\n        onCondition(action.inner);\n        controller.close();\n      },\n    );\n  }\n}\n\nclass ConditionWrapper extends ActionCell {\n  ConditionWrapper(this.inner, this.isSelected);\n\n  final NumberFilterConditionPB inner;\n  final bool isSelected;\n\n  @override\n  Widget? rightIcon(Color iconColor) =>\n      isSelected ? const FlowySvg(FlowySvgs.check_s) : null;\n\n  @override\n  String get name => inner.filterName;\n}\n\nextension NumberFilterConditionPBExtension on NumberFilterConditionPB {\n  String get shortName {\n    return switch (this) {\n      NumberFilterConditionPB.Equal => \"=\",\n      NumberFilterConditionPB.NotEqual => \"≠\",\n      NumberFilterConditionPB.LessThan => \"<\",\n      NumberFilterConditionPB.LessThanOrEqualTo => \"≤\",\n      NumberFilterConditionPB.GreaterThan => \">\",\n      NumberFilterConditionPB.GreaterThanOrEqualTo => \"≥\",\n      NumberFilterConditionPB.NumberIsEmpty =>\n        LocaleKeys.grid_numberFilter_isEmpty.tr(),\n      NumberFilterConditionPB.NumberIsNotEmpty =>\n        LocaleKeys.grid_numberFilter_isNotEmpty.tr(),\n      _ => \"\",\n    };\n  }\n\n  String get filterName {\n    return switch (this) {\n      NumberFilterConditionPB.Equal => LocaleKeys.grid_numberFilter_equal.tr(),\n      NumberFilterConditionPB.NotEqual =>\n        LocaleKeys.grid_numberFilter_notEqual.tr(),\n      NumberFilterConditionPB.LessThan =>\n        LocaleKeys.grid_numberFilter_lessThan.tr(),\n      NumberFilterConditionPB.LessThanOrEqualTo =>\n        LocaleKeys.grid_numberFilter_lessThanOrEqualTo.tr(),\n      NumberFilterConditionPB.GreaterThan =>\n        LocaleKeys.grid_numberFilter_greaterThan.tr(),\n      NumberFilterConditionPB.GreaterThanOrEqualTo =>\n        LocaleKeys.grid_numberFilter_greaterThanOrEqualTo.tr(),\n      NumberFilterConditionPB.NumberIsEmpty =>\n        LocaleKeys.grid_numberFilter_isEmpty.tr(),\n      NumberFilterConditionPB.NumberIsNotEmpty =>\n        LocaleKeys.grid_numberFilter_isNotEmpty.tr(),\n      _ => \"\",\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/database/view/database_filter_condition_list.dart';\nimport 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/condition_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/widgets.dart';\n\nclass SelectOptionFilterConditionList extends StatelessWidget {\n  const SelectOptionFilterConditionList({\n    super.key,\n    required this.filter,\n    required this.fieldType,\n    required this.popoverMutex,\n    required this.onCondition,\n  });\n\n  final SelectOptionFilter filter;\n  final FieldType fieldType;\n  final PopoverMutex popoverMutex;\n  final void Function(SelectOptionFilterConditionPB) onCondition;\n\n  @override\n  Widget build(BuildContext context) {\n    final conditions = (fieldType == FieldType.SingleSelect\n        ? SingleSelectOptionFilterCondition().conditions\n        : MultiSelectOptionFilterCondition().conditions);\n    return PopoverActionList<ConditionWrapper>(\n      asBarrier: true,\n      mutex: popoverMutex,\n      direction: PopoverDirection.bottomWithCenterAligned,\n      actions: conditions\n          .map(\n            (action) => ConditionWrapper(\n              action.$1,\n              filter.condition == action.$1,\n            ),\n          )\n          .toList(),\n      buildChild: (controller) {\n        return ConditionButton(\n          conditionName: filter.condition.i18n,\n          onTap: () => controller.show(),\n        );\n      },\n      onSelected: (action, controller) async {\n        onCondition(action.inner);\n        controller.close();\n      },\n    );\n  }\n}\n\nclass ConditionWrapper extends ActionCell {\n  ConditionWrapper(this.inner, this.isSelected);\n\n  final SelectOptionFilterConditionPB inner;\n  final bool isSelected;\n\n  @override\n  Widget? rightIcon(Color iconColor) {\n    return isSelected ? const FlowySvg(FlowySvgs.check_s) : null;\n  }\n\n  @override\n  String get name => inner.i18n;\n}\n\nextension SelectOptionFilterConditionPBExtension\n    on SelectOptionFilterConditionPB {\n  String get i18n {\n    return switch (this) {\n      SelectOptionFilterConditionPB.OptionIs =>\n        LocaleKeys.grid_selectOptionFilter_is.tr(),\n      SelectOptionFilterConditionPB.OptionIsNot =>\n        LocaleKeys.grid_selectOptionFilter_isNot.tr(),\n      SelectOptionFilterConditionPB.OptionContains =>\n        LocaleKeys.grid_selectOptionFilter_contains.tr(),\n      SelectOptionFilterConditionPB.OptionDoesNotContain =>\n        LocaleKeys.grid_selectOptionFilter_doesNotContain.tr(),\n      SelectOptionFilterConditionPB.OptionIsEmpty =>\n        LocaleKeys.grid_selectOptionFilter_isEmpty.tr(),\n      SelectOptionFilterConditionPB.OptionIsNotEmpty =>\n        LocaleKeys.grid_selectOptionFilter_isNotEmpty.tr(),\n      _ => \"\",\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SelectOptionFilterList extends StatelessWidget {\n  const SelectOptionFilterList({\n    super.key,\n    required this.filter,\n    required this.field,\n    required this.options,\n    required this.onTap,\n  });\n\n  final SelectOptionFilter filter;\n  final FieldInfo field;\n  final List<SelectOptionPB> options;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView.separated(\n      physics: const NeverScrollableScrollPhysics(),\n      shrinkWrap: true,\n      itemCount: options.length,\n      separatorBuilder: (context, index) =>\n          VSpace(GridSize.typeOptionSeparatorHeight),\n      itemBuilder: (context, index) {\n        final option = options[index];\n        final isSelected = filter.optionIds.contains(option.id);\n        return SelectOptionFilterCell(\n          option: option,\n          isSelected: isSelected,\n          onTap: () => _onTapHandler(context, option, isSelected),\n        );\n      },\n    );\n  }\n\n  void _onTapHandler(\n    BuildContext context,\n    SelectOptionPB option,\n    bool isSelected,\n  ) {\n    final selectedOptionIds = Set<String>.from(filter.optionIds);\n    if (isSelected) {\n      selectedOptionIds.remove(option.id);\n    } else {\n      selectedOptionIds.add(option.id);\n    }\n    _updateSelectOptions(context, filter, selectedOptionIds);\n    onTap();\n  }\n\n  void _updateSelectOptions(\n    BuildContext context,\n    SelectOptionFilter filter,\n    Set<String> selectedOptionIds,\n  ) {\n    final optionIds =\n        options.map((e) => e.id).where(selectedOptionIds.contains).toList();\n    final newFilter = filter.copyWith(optionIds: optionIds);\n    context\n        .read<FilterEditorBloc>()\n        .add(FilterEditorEvent.updateFilter(newFilter));\n  }\n}\n\nclass SelectOptionFilterCell extends StatelessWidget {\n  const SelectOptionFilterCell({\n    super.key,\n    required this.option,\n    required this.isSelected,\n    required this.onTap,\n  });\n\n  final SelectOptionPB option;\n  final bool isSelected;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyHover(\n        resetHoverOnRebuild: false,\n        style: HoverStyle(\n          hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        ),\n        child: SelectOptionTagCell(\n          option: option,\n          onSelected: onTap,\n          children: [\n            if (isSelected)\n              const Padding(\n                padding: EdgeInsets.only(right: 6),\n                child: FlowySvg(FlowySvgs.check_s),\n              ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart",
    "content": "import 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../disclosure_button.dart';\nimport '../choicechip.dart';\n\nimport 'condition_list.dart';\nimport 'option_list.dart';\n\nclass SelectOptionFilterChoicechip extends StatelessWidget {\n  const SelectOptionFilterChoicechip({\n    super.key,\n    required this.filterId,\n  });\n\n  final String filterId;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      constraints: BoxConstraints.loose(const Size(240, 160)),\n      direction: PopoverDirection.bottomWithCenterAligned,\n      popupBuilder: (_) {\n        return BlocProvider.value(\n          value: context.read<FilterEditorBloc>(),\n          child: SelectOptionFilterEditor(filterId: filterId),\n        );\n      },\n      child: SingleFilterBlocSelector<SelectOptionFilter>(\n        filterId: filterId,\n        builder: (context, filter, field) {\n          return ChoiceChipButton(\n            fieldInfo: field,\n            filterDesc: filter.getContentDescription(field),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass SelectOptionFilterEditor extends StatefulWidget {\n  const SelectOptionFilterEditor({\n    super.key,\n    required this.filterId,\n  });\n\n  final String filterId;\n\n  @override\n  State<SelectOptionFilterEditor> createState() =>\n      _SelectOptionFilterEditorState();\n}\n\nclass _SelectOptionFilterEditorState extends State<SelectOptionFilterEditor> {\n  final popoverMutex = PopoverMutex();\n\n  @override\n  void dispose() {\n    popoverMutex.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleFilterBlocSelector<SelectOptionFilter>(\n      filterId: widget.filterId,\n      builder: (context, filter, field) {\n        final List<Widget> slivers = [\n          SliverToBoxAdapter(child: _buildFilterPanel(filter, field)),\n        ];\n\n        if (filter.canAttachContent) {\n          slivers\n            ..add(const SliverToBoxAdapter(child: VSpace(4)))\n            ..add(\n              SliverToBoxAdapter(\n                child: SelectOptionFilterList(\n                  filter: filter,\n                  field: field,\n                  options: filter.makeDelegate(field).getOptions(field),\n                  onTap: () => popoverMutex.close(),\n                ),\n              ),\n            );\n        }\n\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),\n          child: CustomScrollView(\n            shrinkWrap: true,\n            slivers: slivers,\n            physics: StyledScrollPhysics(),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildFilterPanel(\n    SelectOptionFilter filter,\n    FieldInfo field,\n  ) {\n    return SizedBox(\n      height: 20,\n      child: Row(\n        children: [\n          Expanded(\n            child: FlowyText(\n              field.field.name,\n              overflow: TextOverflow.ellipsis,\n            ),\n          ),\n          const HSpace(4),\n          SelectOptionFilterConditionList(\n            filter: filter,\n            fieldType: field.fieldType,\n            popoverMutex: popoverMutex,\n            onCondition: (condition) {\n              final newFilter = filter.copyWith(condition: condition);\n              context\n                  .read<FilterEditorBloc>()\n                  .add(FilterEditorEvent.updateFilter(newFilter));\n            },\n          ),\n          DisclosureButton(\n            popoverMutex: popoverMutex,\n            onAction: (action) {\n              switch (action) {\n                case FilterDisclosureAction.delete:\n                  context\n                      .read<FilterEditorBloc>()\n                      .add(FilterEditorEvent.deleteFilter(filter.filterId));\n                  break;\n              }\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../condition_button.dart';\nimport '../disclosure_button.dart';\n\nimport 'choicechip.dart';\n\nclass TextFilterChoicechip extends StatelessWidget {\n  const TextFilterChoicechip({\n    super.key,\n    required this.filterId,\n  });\n\n  final String filterId;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      constraints: BoxConstraints.loose(const Size(200, 76)),\n      direction: PopoverDirection.bottomWithCenterAligned,\n      popupBuilder: (_) {\n        return BlocProvider.value(\n          value: context.read<FilterEditorBloc>(),\n          child: TextFilterEditor(filterId: filterId),\n        );\n      },\n      child: SingleFilterBlocSelector<TextFilter>(\n        filterId: filterId,\n        builder: (context, filter, field) {\n          return ChoiceChipButton(\n            fieldInfo: field,\n            filterDesc: filter.getContentDescription(field),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass TextFilterEditor extends StatefulWidget {\n  const TextFilterEditor({\n    super.key,\n    required this.filterId,\n  });\n\n  final String filterId;\n\n  @override\n  State<TextFilterEditor> createState() => _TextFilterEditorState();\n}\n\nclass _TextFilterEditorState extends State<TextFilterEditor> {\n  final popoverMutex = PopoverMutex();\n\n  @override\n  void dispose() {\n    popoverMutex.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleFilterBlocSelector<TextFilter>(\n      filterId: widget.filterId,\n      builder: (context, filter, field) {\n        final List<Widget> children = [\n          _buildFilterPanel(filter, field),\n        ];\n\n        if (filter.condition != TextFilterConditionPB.TextIsEmpty &&\n            filter.condition != TextFilterConditionPB.TextIsNotEmpty) {\n          children.add(const VSpace(4));\n          children.add(_buildFilterTextField(filter, field));\n        }\n\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),\n          child: IntrinsicHeight(child: Column(children: children)),\n        );\n      },\n    );\n  }\n\n  Widget _buildFilterPanel(TextFilter filter, FieldInfo field) {\n    return SizedBox(\n      height: 20,\n      child: Row(\n        children: [\n          Expanded(\n            child: FlowyText(\n              field.name,\n              overflow: TextOverflow.ellipsis,\n            ),\n          ),\n          const HSpace(4),\n          Expanded(\n            child: TextFilterConditionList(\n              filter: filter,\n              popoverMutex: popoverMutex,\n              onCondition: (condition) {\n                final newFilter = filter.copyWith(condition: condition);\n                context\n                    .read<FilterEditorBloc>()\n                    .add(FilterEditorEvent.updateFilter(newFilter));\n              },\n            ),\n          ),\n          const HSpace(4),\n          DisclosureButton(\n            popoverMutex: popoverMutex,\n            onAction: (action) {\n              switch (action) {\n                case FilterDisclosureAction.delete:\n                  context\n                      .read<FilterEditorBloc>()\n                      .add(FilterEditorEvent.deleteFilter(filter.filterId));\n                  break;\n              }\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildFilterTextField(TextFilter filter, FieldInfo field) {\n    return FlowyTextField(\n      text: filter.content,\n      hintText: LocaleKeys.grid_settings_typeAValue.tr(),\n      debounceDuration: const Duration(milliseconds: 300),\n      autoFocus: false,\n      onChanged: (text) {\n        final newFilter = filter.copyWith(content: text);\n        context\n            .read<FilterEditorBloc>()\n            .add(FilterEditorEvent.updateFilter(newFilter));\n      },\n    );\n  }\n}\n\nclass TextFilterConditionList extends StatelessWidget {\n  const TextFilterConditionList({\n    super.key,\n    required this.filter,\n    required this.popoverMutex,\n    required this.onCondition,\n  });\n\n  final TextFilter filter;\n  final PopoverMutex popoverMutex;\n  final void Function(TextFilterConditionPB) onCondition;\n\n  @override\n  Widget build(BuildContext context) {\n    return PopoverActionList<ConditionWrapper>(\n      asBarrier: true,\n      mutex: popoverMutex,\n      direction: PopoverDirection.bottomWithCenterAligned,\n      actions: TextFilterConditionPB.values\n          .map(\n            (action) => ConditionWrapper(\n              action,\n              filter.condition == action,\n            ),\n          )\n          .toList(),\n      buildChild: (controller) {\n        return ConditionButton(\n          conditionName: filter.condition.filterName,\n          onTap: () => controller.show(),\n        );\n      },\n      onSelected: (action, controller) async {\n        onCondition(action.inner);\n        controller.close();\n      },\n    );\n  }\n}\n\nclass ConditionWrapper extends ActionCell {\n  ConditionWrapper(this.inner, this.isSelected);\n\n  final TextFilterConditionPB inner;\n  final bool isSelected;\n\n  @override\n  Widget? rightIcon(Color iconColor) {\n    if (isSelected) {\n      return const FlowySvg(FlowySvgs.check_s);\n    } else {\n      return null;\n    }\n  }\n\n  @override\n  String get name => inner.filterName;\n}\n\nextension TextFilterConditionPBExtension on TextFilterConditionPB {\n  String get filterName {\n    switch (this) {\n      case TextFilterConditionPB.TextContains:\n        return LocaleKeys.grid_textFilter_contains.tr();\n      case TextFilterConditionPB.TextDoesNotContain:\n        return LocaleKeys.grid_textFilter_doesNotContain.tr();\n      case TextFilterConditionPB.TextEndsWith:\n        return LocaleKeys.grid_textFilter_endsWith.tr();\n      case TextFilterConditionPB.TextIs:\n        return LocaleKeys.grid_textFilter_is.tr();\n      case TextFilterConditionPB.TextIsNot:\n        return LocaleKeys.grid_textFilter_isNot.tr();\n      case TextFilterConditionPB.TextStartsWith:\n        return LocaleKeys.grid_textFilter_startWith.tr();\n      case TextFilterConditionPB.TextIsEmpty:\n        return LocaleKeys.grid_textFilter_isEmpty.tr();\n      case TextFilterConditionPB.TextIsNotEmpty:\n        return LocaleKeys.grid_textFilter_isNotEmpty.tr();\n      default:\n        return \"\";\n    }\n  }\n\n  String get choicechipPrefix {\n    switch (this) {\n      case TextFilterConditionPB.TextDoesNotContain:\n        return LocaleKeys.grid_textFilter_choicechipPrefix_isNot.tr();\n      case TextFilterConditionPB.TextEndsWith:\n        return LocaleKeys.grid_textFilter_choicechipPrefix_endWith.tr();\n      case TextFilterConditionPB.TextIsNot:\n        return LocaleKeys.grid_textFilter_choicechipPrefix_isNot.tr();\n      case TextFilterConditionPB.TextStartsWith:\n        return LocaleKeys.grid_textFilter_choicechipPrefix_startWith.tr();\n      case TextFilterConditionPB.TextIsEmpty:\n        return LocaleKeys.grid_textFilter_choicechipPrefix_isEmpty.tr();\n      case TextFilterConditionPB.TextIsNotEmpty:\n        return LocaleKeys.grid_textFilter_choicechipPrefix_isNotEmpty.tr();\n      default:\n        return \"\";\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/time.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../condition_button.dart';\nimport '../disclosure_button.dart';\n\nimport 'choicechip.dart';\n\nclass TimeFilterChoiceChip extends StatelessWidget {\n  const TimeFilterChoiceChip({\n    super.key,\n    required this.filterId,\n  });\n\n  final String filterId;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      constraints: BoxConstraints.loose(const Size(200, 100)),\n      direction: PopoverDirection.bottomWithCenterAligned,\n      popupBuilder: (_) {\n        return BlocProvider.value(\n          value: context.read<FilterEditorBloc>(),\n          child: TimeFilterEditor(filterId: filterId),\n        );\n      },\n      child: SingleFilterBlocSelector<TimeFilter>(\n        filterId: filterId,\n        builder: (context, filter, field) {\n          return ChoiceChipButton(\n            fieldInfo: field,\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass TimeFilterEditor extends StatefulWidget {\n  const TimeFilterEditor({\n    super.key,\n    required this.filterId,\n  });\n\n  final String filterId;\n  @override\n  State<TimeFilterEditor> createState() => _TimeFilterEditorState();\n}\n\nclass _TimeFilterEditorState extends State<TimeFilterEditor> {\n  final popoverMutex = PopoverMutex();\n\n  @override\n  void dispose() {\n    popoverMutex.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleFilterBlocSelector<TimeFilter>(\n      filterId: widget.filterId,\n      builder: (context, filter, field) {\n        final List<Widget> children = [\n          _buildFilterPanel(filter, field),\n          if (filter.condition != NumberFilterConditionPB.NumberIsEmpty &&\n              filter.condition != NumberFilterConditionPB.NumberIsNotEmpty) ...[\n            const VSpace(4),\n            _buildFilterTimeField(filter, field),\n          ],\n        ];\n\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),\n          child: IntrinsicHeight(child: Column(children: children)),\n        );\n      },\n    );\n  }\n\n  Widget _buildFilterPanel(\n    TimeFilter filter,\n    FieldInfo field,\n  ) {\n    return SizedBox(\n      height: 20,\n      child: Row(\n        children: [\n          Expanded(\n            child: FlowyText(\n              field.name,\n              overflow: TextOverflow.ellipsis,\n            ),\n          ),\n          const HSpace(4),\n          Expanded(\n            child: TimeFilterConditionList(\n              filter: filter,\n              popoverMutex: popoverMutex,\n              onCondition: (condition) {\n                final newFilter = filter.copyWith(condition: condition);\n                context\n                    .read<FilterEditorBloc>()\n                    .add(FilterEditorEvent.updateFilter(newFilter));\n              },\n            ),\n          ),\n          const HSpace(4),\n          DisclosureButton(\n            popoverMutex: popoverMutex,\n            onAction: (action) {\n              switch (action) {\n                case FilterDisclosureAction.delete:\n                  context\n                      .read<FilterEditorBloc>()\n                      .add(FilterEditorEvent.deleteFilter(filter.filterId));\n                  break;\n              }\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildFilterTimeField(\n    TimeFilter filter,\n    FieldInfo field,\n  ) {\n    return FlowyTextField(\n      text: filter.content,\n      hintText: LocaleKeys.grid_settings_typeAValue.tr(),\n      debounceDuration: const Duration(milliseconds: 300),\n      autoFocus: false,\n      onChanged: (text) {\n        final newFilter = filter.copyWith(content: text);\n        context\n            .read<FilterEditorBloc>()\n            .add(FilterEditorEvent.updateFilter(newFilter));\n      },\n    );\n  }\n}\n\nclass TimeFilterConditionList extends StatelessWidget {\n  const TimeFilterConditionList({\n    super.key,\n    required this.filter,\n    required this.popoverMutex,\n    required this.onCondition,\n  });\n\n  final TimeFilter filter;\n  final PopoverMutex popoverMutex;\n  final void Function(NumberFilterConditionPB) onCondition;\n\n  @override\n  Widget build(BuildContext context) {\n    return PopoverActionList<ConditionWrapper>(\n      asBarrier: true,\n      mutex: popoverMutex,\n      direction: PopoverDirection.bottomWithCenterAligned,\n      actions: NumberFilterConditionPB.values\n          .map(\n            (action) => ConditionWrapper(\n              action,\n              filter.condition == action,\n            ),\n          )\n          .toList(),\n      buildChild: (controller) {\n        return ConditionButton(\n          conditionName: filter.condition.filterName,\n          onTap: () => controller.show(),\n        );\n      },\n      onSelected: (action, controller) {\n        onCondition(action.inner);\n        controller.close();\n      },\n    );\n  }\n}\n\nclass ConditionWrapper extends ActionCell {\n  ConditionWrapper(this.inner, this.isSelected);\n\n  final NumberFilterConditionPB inner;\n  final bool isSelected;\n\n  @override\n  Widget? rightIcon(Color iconColor) =>\n      isSelected ? const FlowySvg(FlowySvgs.check_s) : null;\n\n  @override\n  String get name => inner.filterName;\n}\n\nextension TimeFilterConditionPBExtension on NumberFilterConditionPB {\n  String get filterName {\n    return switch (this) {\n      NumberFilterConditionPB.Equal => LocaleKeys.grid_numberFilter_equal.tr(),\n      NumberFilterConditionPB.NotEqual =>\n        LocaleKeys.grid_numberFilter_notEqual.tr(),\n      NumberFilterConditionPB.LessThan =>\n        LocaleKeys.grid_numberFilter_lessThan.tr(),\n      NumberFilterConditionPB.LessThanOrEqualTo =>\n        LocaleKeys.grid_numberFilter_lessThanOrEqualTo.tr(),\n      NumberFilterConditionPB.GreaterThan =>\n        LocaleKeys.grid_numberFilter_greaterThan.tr(),\n      NumberFilterConditionPB.GreaterThanOrEqualTo =>\n        LocaleKeys.grid_numberFilter_greaterThanOrEqualTo.tr(),\n      NumberFilterConditionPB.NumberIsEmpty =>\n        LocaleKeys.grid_numberFilter_isEmpty.tr(),\n      NumberFilterConditionPB.NumberIsNotEmpty =>\n        LocaleKeys.grid_numberFilter_isNotEmpty.tr(),\n      _ => \"\",\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/url.dart",
    "content": "import 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'choicechip.dart';\n\nclass URLFilterChoicechip extends StatelessWidget {\n  const URLFilterChoicechip({\n    super.key,\n    required this.filterId,\n  });\n\n  final String filterId;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      constraints: BoxConstraints.loose(const Size(200, 76)),\n      direction: PopoverDirection.bottomWithCenterAligned,\n      popupBuilder: (_) {\n        return BlocProvider.value(\n          value: context.read<FilterEditorBloc>(),\n          child: TextFilterEditor(filterId: filterId),\n        );\n      },\n      child: SingleFilterBlocSelector<TextFilter>(\n        filterId: filterId,\n        builder: (context, filter, field) {\n          return ChoiceChipButton(\n            fieldInfo: field,\n            filterDesc: filter.getContentDescription(field),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/condition_button.dart",
    "content": "import 'dart:math' as math;\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\n\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\n\nclass ConditionButton extends StatelessWidget {\n  const ConditionButton({\n    super.key,\n    required this.conditionName,\n    required this.onTap,\n  });\n\n  final String conditionName;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    final arrow = Transform.rotate(\n      angle: -math.pi / 2,\n      child: FlowySvg(\n        FlowySvgs.arrow_left_s,\n        color: AFThemeExtension.of(context).textColor,\n      ),\n    );\n\n    return SizedBox(\n      height: 20,\n      child: FlowyButton(\n        useIntrinsicWidth: true,\n        text: FlowyText(\n          lineHeight: 1.0,\n          conditionName,\n          fontSize: 10,\n          color: AFThemeExtension.of(context).textColor,\n          overflow: TextOverflow.ellipsis,\n        ),\n        margin: const EdgeInsets.symmetric(horizontal: 4),\n        radius: Corners.s6Border,\n        rightIcon: arrow,\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        onTap: onTap,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/application/simple_text_filter_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/style_widget/text_field.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass CreateDatabaseViewFilterList extends StatelessWidget {\n  const CreateDatabaseViewFilterList({\n    super.key,\n    this.onTap,\n  });\n\n  final VoidCallback? onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    final filterBloc = context.read<FilterEditorBloc>();\n    return BlocProvider(\n      create: (_) => SimpleTextFilterBloc<FieldInfo>(\n        values: List.from(filterBloc.state.fields),\n        comparator: (val) => val.name,\n      ),\n      child: BlocListener<FilterEditorBloc, FilterEditorState>(\n        listenWhen: (previous, current) => previous.fields != current.fields,\n        listener: (context, state) {\n          context\n              .read<SimpleTextFilterBloc<FieldInfo>>()\n              .add(SimpleTextFilterEvent.receiveNewValues(state.fields));\n        },\n        child: BlocBuilder<SimpleTextFilterBloc<FieldInfo>,\n            SimpleTextFilterState<FieldInfo>>(\n          builder: (context, state) {\n            final cells = state.values.map((fieldInfo) {\n              return SizedBox(\n                height: GridSize.popoverItemHeight,\n                child: FilterableFieldButton(\n                  fieldInfo: fieldInfo,\n                  onTap: () {\n                    context\n                        .read<FilterEditorBloc>()\n                        .add(FilterEditorEvent.createFilter(fieldInfo));\n                    onTap?.call();\n                  },\n                ),\n              );\n            }).toList();\n\n            final List<Widget> slivers = [\n              SliverPersistentHeader(\n                pinned: true,\n                delegate: _FilterTextFieldDelegate(),\n              ),\n              SliverToBoxAdapter(\n                child: ListView.separated(\n                  shrinkWrap: true,\n                  itemCount: cells.length,\n                  itemBuilder: (_, int index) => cells[index],\n                  separatorBuilder: (_, __) =>\n                      VSpace(GridSize.typeOptionSeparatorHeight),\n                ),\n              ),\n            ];\n            return CustomScrollView(\n              shrinkWrap: true,\n              slivers: slivers,\n              physics: StyledScrollPhysics(),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {\n  _FilterTextFieldDelegate();\n\n  double fixHeight = 36;\n\n  @override\n  Widget build(\n    BuildContext context,\n    double shrinkOffset,\n    bool overlapsContent,\n  ) {\n    return Container(\n      padding: const EdgeInsets.only(bottom: 4),\n      color: Theme.of(context).cardColor,\n      height: fixHeight,\n      child: FlowyTextField(\n        hintText: LocaleKeys.grid_settings_filterBy.tr(),\n        onChanged: (text) {\n          context\n              .read<SimpleTextFilterBloc<FieldInfo>>()\n              .add(SimpleTextFilterEvent.updateFilter(text));\n        },\n      ),\n    );\n  }\n\n  @override\n  double get maxExtent => fixHeight;\n\n  @override\n  double get minExtent => fixHeight;\n\n  @override\n  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {\n    return false;\n  }\n}\n\nclass FilterableFieldButton extends StatelessWidget {\n  const FilterableFieldButton({\n    super.key,\n    required this.fieldInfo,\n    required this.onTap,\n  });\n\n  final FieldInfo fieldInfo;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      hoverColor: AFThemeExtension.of(context).lightGreyHover,\n      text: FlowyText(\n        lineHeight: 1.0,\n        fieldInfo.field.name,\n        color: AFThemeExtension.of(context).textColor,\n      ),\n      onTap: onTap,\n      leftIcon: FieldIcon(\n        fieldInfo: fieldInfo,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/disclosure_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/icon_button.dart';\nimport 'package:flutter/material.dart';\n\nclass DisclosureButton extends StatefulWidget {\n  const DisclosureButton({\n    super.key,\n    required this.popoverMutex,\n    required this.onAction,\n  });\n\n  final PopoverMutex popoverMutex;\n  final Function(FilterDisclosureAction) onAction;\n\n  @override\n  State<DisclosureButton> createState() => _DisclosureButtonState();\n}\n\nclass _DisclosureButtonState extends State<DisclosureButton> {\n  @override\n  Widget build(BuildContext context) {\n    return PopoverActionList<FilterDisclosureActionWrapper>(\n      asBarrier: true,\n      mutex: widget.popoverMutex,\n      actions: FilterDisclosureAction.values\n          .map((action) => FilterDisclosureActionWrapper(action))\n          .toList(),\n      buildChild: (controller) {\n        return FlowyIconButton(\n          hoverColor: AFThemeExtension.of(context).lightGreyHover,\n          width: 20,\n          icon: FlowySvg(\n            FlowySvgs.details_s,\n            color: Theme.of(context).iconTheme.color,\n          ),\n          onPressed: () => controller.show(),\n        );\n      },\n      onSelected: (action, controller) async {\n        widget.onAction(action.inner);\n        controller.close();\n      },\n    );\n  }\n}\n\nenum FilterDisclosureAction {\n  delete,\n}\n\nclass FilterDisclosureActionWrapper extends ActionCell {\n  FilterDisclosureActionWrapper(this.inner);\n\n  final FilterDisclosureAction inner;\n\n  @override\n  Widget? leftIcon(Color iconColor) => null;\n\n  @override\n  String get name => inner.name;\n}\n\nextension FilterDisclosureActionExtension on FilterDisclosureAction {\n  String get name {\n    switch (this) {\n      case FilterDisclosureAction.delete:\n        return LocaleKeys.grid_settings_deleteFilter.tr();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'create_filter_list.dart';\nimport 'filter_menu_item.dart';\n\nclass FilterMenu extends StatelessWidget {\n  const FilterMenu({\n    super.key,\n    required this.fieldController,\n  });\n\n  final FieldController fieldController;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => FilterEditorBloc(\n        viewId: fieldController.viewId,\n        fieldController: fieldController,\n      ),\n      child: BlocBuilder<FilterEditorBloc, FilterEditorState>(\n        buildWhen: (previous, current) {\n          final previousIds = previous.filters.map((e) => e.filterId).toList();\n          final currentIds = current.filters.map((e) => e.filterId).toList();\n          return !listEquals(previousIds, currentIds);\n        },\n        builder: (context, state) {\n          final List<Widget> children = [];\n          children.addAll(\n            state.filters\n                .map(\n                  (filter) => FilterMenuItem(\n                    key: ValueKey(filter.filterId),\n                    filterId: filter.filterId,\n                    fieldType: state.fields\n                        .firstWhere(\n                          (element) => element.id == filter.fieldId,\n                        )\n                        .fieldType,\n                  ),\n                )\n                .toList(),\n          );\n\n          if (state.fields.isNotEmpty) {\n            children.add(\n              AddFilterButton(\n                viewId: state.viewId,\n              ),\n            );\n          }\n\n          return Wrap(\n            spacing: 6,\n            runSpacing: 4,\n            children: children,\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass AddFilterButton extends StatefulWidget {\n  const AddFilterButton({required this.viewId, super.key});\n\n  final String viewId;\n\n  @override\n  State<AddFilterButton> createState() => _AddFilterButtonState();\n}\n\nclass _AddFilterButtonState extends State<AddFilterButton> {\n  final PopoverController popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return wrapPopover(\n      SizedBox(\n        height: 28,\n        child: FlowyButton(\n          text: FlowyText(\n            lineHeight: 1.0,\n            LocaleKeys.grid_settings_addFilter.tr(),\n            color: AFThemeExtension.of(context).textColor,\n          ),\n          useIntrinsicWidth: true,\n          hoverColor: AFThemeExtension.of(context).lightGreyHover,\n          leftIcon: FlowySvg(\n            FlowySvgs.add_s,\n            color: Theme.of(context).iconTheme.color,\n          ),\n          onTap: () => popoverController.show(),\n        ),\n      ),\n    );\n  }\n\n  Widget wrapPopover(Widget child) {\n    return AppFlowyPopover(\n      controller: popoverController,\n      constraints: BoxConstraints.loose(const Size(200, 300)),\n      triggerActions: PopoverTriggerFlags.none,\n      child: child,\n      popupBuilder: (_) {\n        return BlocProvider.value(\n          value: context.read<FilterEditorBloc>(),\n          child: CreateDatabaseViewFilterList(\n            onTap: () => popoverController.close(),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter/material.dart';\n\nimport 'choicechip/checkbox.dart';\nimport 'choicechip/checklist.dart';\nimport 'choicechip/date.dart';\nimport 'choicechip/number.dart';\nimport 'choicechip/select_option/select_option.dart';\nimport 'choicechip/text.dart';\nimport 'choicechip/url.dart';\n\nclass FilterMenuItem extends StatelessWidget {\n  const FilterMenuItem({\n    super.key,\n    required this.fieldType,\n    required this.filterId,\n  });\n\n  final FieldType fieldType;\n  final String filterId;\n\n  @override\n  Widget build(BuildContext context) {\n    return switch (fieldType) {\n      FieldType.RichText => TextFilterChoicechip(filterId: filterId),\n      FieldType.Number => NumberFilterChoiceChip(filterId: filterId),\n      FieldType.URL => URLFilterChoicechip(filterId: filterId),\n      FieldType.Checkbox => CheckboxFilterChoicechip(filterId: filterId),\n      FieldType.Checklist => ChecklistFilterChoicechip(filterId: filterId),\n      FieldType.DateTime ||\n      FieldType.LastEditedTime ||\n      FieldType.CreatedTime =>\n        DateFilterChoicechip(filterId: filterId),\n      FieldType.SingleSelect ||\n      FieldType.MultiSelect =>\n        SelectOptionFilterChoicechip(filterId: filterId),\n      // FieldType.Time =>\n      //   TimeFilterChoiceChip(filterInfo: filterInfo),\n      _ => const SizedBox.shrink(),\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass GridAddRowButton extends StatelessWidget {\n  const GridAddRowButton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final color = Theme.of(context).brightness == Brightness.light\n        ? const Color(0xFF171717).withValues(alpha: 0.4)\n        : const Color(0xFFFFFFFF).withValues(alpha: 0.4);\n    return FlowyButton(\n      radius: BorderRadius.zero,\n      decoration: BoxDecoration(\n        border: Border(\n          bottom: BorderSide(color: AFThemeExtension.of(context).borderColor),\n        ),\n      ),\n      text: FlowyText(\n        lineHeight: 1.0,\n        LocaleKeys.grid_row_newRow.tr(),\n        color: color,\n      ),\n      margin: const EdgeInsets.symmetric(horizontal: 12),\n      hoverColor: AFThemeExtension.of(context).lightGreyHover,\n      onTap: () => context.read<GridBloc>().add(const GridEvent.createRow()),\n      leftIcon: FlowySvg(\n        FlowySvgs.add_less_padding_s,\n        color: color,\n      ),\n    );\n  }\n}\n\nclass GridRowBottomBar extends StatelessWidget {\n  const GridRowBottomBar({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final padding =\n        context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding;\n    return Container(\n      padding: GridSize.footerContentInsets.copyWith(left: 0) +\n          EdgeInsets.only(left: padding),\n      height: GridSize.footerHeight,\n      child: const GridAddRowButton(),\n    );\n  }\n}\n\nclass GridRowLoadMoreButton extends StatelessWidget {\n  const GridRowLoadMoreButton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final padding =\n        context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding;\n    final color = Theme.of(context).brightness == Brightness.light\n        ? const Color(0xFF171717).withValues(alpha: 0.4)\n        : const Color(0xFFFFFFFF).withValues(alpha: 0.4);\n\n    return Container(\n      padding: GridSize.footerContentInsets.copyWith(left: 0) +\n          EdgeInsets.only(left: padding),\n      height: GridSize.footerHeight,\n      child: FlowyButton(\n        radius: BorderRadius.zero,\n        decoration: BoxDecoration(\n          border: Border(\n            bottom: BorderSide(color: AFThemeExtension.of(context).borderColor),\n          ),\n        ),\n        text: FlowyText(\n          lineHeight: 1.0,\n          LocaleKeys.grid_row_loadMore.tr(),\n          color: color,\n        ),\n        margin: const EdgeInsets.symmetric(horizontal: 12),\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        onTap: () => context.read<GridBloc>().add(\n              const GridEvent.loadMoreRows(),\n            ),\n        leftIcon: FlowySvg(\n          FlowySvgs.load_more_s,\n          color: color,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/application/field/field_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/widgets/field/field_editor.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../layout/sizes.dart';\n\nclass GridFieldCell extends StatefulWidget {\n  const GridFieldCell({\n    super.key,\n    required this.viewId,\n    required this.fieldController,\n    required this.fieldInfo,\n    required this.onTap,\n    required this.onEditorOpened,\n    required this.onFieldInsertedOnEitherSide,\n    required this.isEditing,\n    required this.isNew,\n  });\n\n  final String viewId;\n  final FieldController fieldController;\n  final FieldInfo fieldInfo;\n  final VoidCallback onTap;\n  final VoidCallback onEditorOpened;\n  final void Function(String fieldId) onFieldInsertedOnEitherSide;\n  final bool isEditing;\n  final bool isNew;\n\n  @override\n  State<GridFieldCell> createState() => _GridFieldCellState();\n}\n\nclass _GridFieldCellState extends State<GridFieldCell> {\n  final PopoverController popoverController = PopoverController();\n  late final FieldCellBloc _bloc;\n\n  @override\n  void initState() {\n    super.initState();\n    _bloc = FieldCellBloc(viewId: widget.viewId, fieldInfo: widget.fieldInfo);\n    if (widget.isEditing) {\n      WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n        popoverController.show();\n      });\n    }\n  }\n\n  @override\n  void didUpdateWidget(covariant oldWidget) {\n    if (widget.fieldInfo != oldWidget.fieldInfo && !_bloc.isClosed) {\n      _bloc.add(FieldCellEvent.onFieldChanged(widget.fieldInfo));\n    }\n    if (widget.isEditing) {\n      WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n        popoverController.show();\n      });\n    }\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: _bloc,\n      child: BlocBuilder<FieldCellBloc, FieldCellState>(\n        builder: (context, state) {\n          final button = AppFlowyPopover(\n            triggerActions: PopoverTriggerFlags.none,\n            constraints: const BoxConstraints(),\n            margin: EdgeInsets.zero,\n            direction: PopoverDirection.bottomWithLeftAligned,\n            controller: popoverController,\n            popupBuilder: (BuildContext context) {\n              widget.onEditorOpened();\n              return FieldEditor(\n                viewId: widget.viewId,\n                fieldController: widget.fieldController,\n                fieldInfo: widget.fieldInfo,\n                isNewField: widget.isNew,\n                initialPage: widget.isNew\n                    ? FieldEditorPage.details\n                    : FieldEditorPage.general,\n                onFieldInserted: widget.onFieldInsertedOnEitherSide,\n              );\n            },\n            child: FlowyTooltip(\n              message: widget.fieldInfo.name,\n              preferBelow: false,\n              child: SizedBox(\n                height: GridSize.headerHeight,\n                child: FieldCellButton(\n                  field: widget.fieldInfo.field,\n                  onTap: widget.onTap,\n                  margin: const EdgeInsetsDirectional.fromSTEB(12, 9, 10, 9),\n                ),\n              ),\n            ),\n          );\n\n          const line = Positioned(\n            top: 0,\n            bottom: 0,\n            right: 0,\n            child: DragToExpandLine(),\n          );\n\n          return _GridHeaderCellContainer(\n            width: state.width,\n            child: Stack(\n              alignment: Alignment.centerRight,\n              children: [button, line],\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    _bloc.close();\n    super.dispose();\n  }\n}\n\nclass _GridHeaderCellContainer extends StatelessWidget {\n  const _GridHeaderCellContainer({\n    required this.child,\n    required this.width,\n  });\n\n  final Widget child;\n  final double width;\n\n  @override\n  Widget build(BuildContext context) {\n    final borderSide =\n        BorderSide(color: AFThemeExtension.of(context).borderColor);\n    final decoration = BoxDecoration(\n      border: Border(\n        right: borderSide,\n        bottom: borderSide,\n      ),\n    );\n\n    return Container(\n      width: width,\n      decoration: decoration,\n      child: child,\n    );\n  }\n}\n\n@visibleForTesting\nclass DragToExpandLine extends StatelessWidget {\n  const DragToExpandLine({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return InkWell(\n      onTap: () {},\n      child: GestureDetector(\n        behavior: HitTestBehavior.opaque,\n        onHorizontalDragStart: (details) {\n          context\n              .read<FieldCellBloc>()\n              .add(const FieldCellEvent.onResizeStart());\n        },\n        onHorizontalDragUpdate: (value) {\n          context\n              .read<FieldCellBloc>()\n              .add(FieldCellEvent.startUpdateWidth(value.localPosition.dx));\n        },\n        onHorizontalDragEnd: (end) {\n          context\n              .read<FieldCellBloc>()\n              .add(const FieldCellEvent.endUpdateWidth());\n        },\n        child: FlowyHover(\n          cursor: SystemMouseCursors.resizeLeftRight,\n          style: HoverStyle(\n            hoverColor: Theme.of(context).colorScheme.primary,\n            borderRadius: BorderRadius.zero,\n            contentMargin: const EdgeInsets.only(left: 6),\n          ),\n          child: const SizedBox(width: 4),\n        ),\n      ),\n    );\n  }\n}\n\nclass FieldCellButton extends StatelessWidget {\n  const FieldCellButton({\n    super.key,\n    required this.field,\n    required this.onTap,\n    this.maxLines = 1,\n    this.radius = BorderRadius.zero,\n    this.margin,\n  });\n\n  final FieldPB field;\n  final VoidCallback onTap;\n  final int? maxLines;\n  final BorderRadius? radius;\n  final EdgeInsetsGeometry? margin;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      hoverColor: AFThemeExtension.of(context).lightGreyHover,\n      onTap: onTap,\n      leftIcon: FieldIcon(\n        fieldInfo: FieldInfo.initial(field),\n      ),\n      rightIcon: field.fieldType.rightIcon != null\n          ? FlowySvg(\n              field.fieldType.rightIcon!,\n              blendMode: null,\n              size: const Size.square(18),\n            )\n          : null,\n      radius: radius,\n      text: FlowyText(\n        field.name,\n        lineHeight: 1.0,\n        maxLines: maxLines,\n        overflow: TextOverflow.ellipsis,\n        color: AFThemeExtension.of(context).textColor,\n      ),\n      margin: margin ?? GridSize.cellContentInsets,\n    );\n  }\n}\n\nclass FieldIcon extends StatelessWidget {\n  const FieldIcon({\n    super.key,\n    required this.fieldInfo,\n    this.dimension = 16.0,\n  });\n\n  final FieldInfo fieldInfo;\n  final double dimension;\n\n  @override\n  Widget build(BuildContext context) {\n    final svgContent = kIconGroups?.findSvgContent(\n      fieldInfo.icon,\n    );\n    final color =\n        Theme.of(context).isLightMode ? const Color(0xFF171717) : Colors.white;\n    return svgContent == null\n        ? FlowySvg(\n            fieldInfo.fieldType.svgData,\n            color: color.withValues(alpha: 0.6),\n            size: Size.square(dimension),\n          )\n        : SizedBox.square(\n            dimension: dimension,\n            child: Center(\n              child: FlowySvg.string(\n                svgContent,\n                color: color.withValues(alpha: 0.45),\n                size: Size.square(dimension - 2),\n              ),\n            ),\n          );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_type_extension.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';\n\nextension RowDetailAccessoryExtension on FieldType {\n  bool get showRowDetailAccessory => switch (this) {\n        FieldType.Media => false,\n        _ => true,\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/application/grid_header_bloc.dart';\nimport 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:reorderables/reorderables.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../../layout/sizes.dart';\nimport 'desktop_field_cell.dart';\n\nclass GridHeaderSliverAdaptor extends StatefulWidget {\n  const GridHeaderSliverAdaptor({\n    super.key,\n    required this.viewId,\n    required this.shrinkWrap,\n    required this.anchorScrollController,\n  });\n\n  final String viewId;\n  final ScrollController anchorScrollController;\n  final bool shrinkWrap;\n\n  @override\n  State<GridHeaderSliverAdaptor> createState() =>\n      _GridHeaderSliverAdaptorState();\n}\n\nclass _GridHeaderSliverAdaptorState extends State<GridHeaderSliverAdaptor> {\n  @override\n  Widget build(BuildContext context) {\n    final fieldController =\n        context.read<GridBloc>().databaseController.fieldController;\n    final databaseSize = context.read<DatabasePluginWidgetBuilderSize?>();\n    final horizontalPadding = databaseSize?.horizontalPadding ?? 0.0;\n    final paddingLeft = databaseSize?.paddingLeftWithMaxDocumentWidth ?? 0.0;\n    return BlocProvider(\n      create: (context) {\n        return GridHeaderBloc(\n          viewId: widget.viewId,\n          fieldController: fieldController,\n        )..add(const GridHeaderEvent.initial());\n      },\n      child: SingleChildScrollView(\n        scrollDirection: Axis.horizontal,\n        controller: widget.anchorScrollController,\n        child: Padding(\n          padding: widget.shrinkWrap\n              ? EdgeInsets.fromLTRB(\n                  horizontalPadding + paddingLeft,\n                  0,\n                  horizontalPadding,\n                  0,\n                )\n              : EdgeInsets.zero,\n          child: _GridHeader(\n            viewId: widget.viewId,\n            fieldController: fieldController,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _GridHeader extends StatefulWidget {\n  const _GridHeader({required this.viewId, required this.fieldController});\n\n  final String viewId;\n  final FieldController fieldController;\n\n  @override\n  State<_GridHeader> createState() => _GridHeaderState();\n}\n\nclass _GridHeaderState extends State<_GridHeader> {\n  final Map<String, ValueKey<String>> _gridMap = {};\n  final _scrollController = ScrollController();\n\n  @override\n  void dispose() {\n    _scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<GridHeaderBloc, GridHeaderState>(\n      builder: (context, state) {\n        final cells = state.fields\n            .map(\n              (fieldInfo) => GridFieldCell(\n                key: _getKeyById(fieldInfo.id),\n                viewId: widget.viewId,\n                fieldInfo: fieldInfo,\n                fieldController: widget.fieldController,\n                onTap: () => context\n                    .read<GridHeaderBloc>()\n                    .add(GridHeaderEvent.startEditingField(fieldInfo.id)),\n                onFieldInsertedOnEitherSide: (fieldId) => context\n                    .read<GridHeaderBloc>()\n                    .add(GridHeaderEvent.startEditingNewField(fieldId)),\n                onEditorOpened: () => context\n                    .read<GridHeaderBloc>()\n                    .add(const GridHeaderEvent.endEditingField()),\n                isEditing: state.editingFieldId == fieldInfo.id,\n                isNew: state.newFieldId == fieldInfo.id,\n              ),\n            )\n            .toList();\n\n        return RepaintBoundary(\n          child: ReorderableRow(\n            scrollController: _scrollController,\n            buildDraggableFeedback: (context, constraints, child) => Material(\n              color: Colors.transparent,\n              child: child,\n            ),\n            draggingWidgetOpacity: 0,\n            header: _cellLeading(),\n            needsLongPressDraggable: UniversalPlatform.isMobile,\n            footer: _CellTrailing(viewId: widget.viewId),\n            onReorder: (int oldIndex, int newIndex) {\n              context\n                  .read<GridHeaderBloc>()\n                  .add(GridHeaderEvent.moveField(oldIndex, newIndex));\n            },\n            children: cells,\n          ),\n        );\n      },\n    );\n  }\n\n  /// This is a workaround for [ReorderableRow].\n  /// [ReorderableRow] warps the child's key with a [GlobalKey].\n  /// It will trigger the child's widget's to recreate.\n  /// The state will lose.\n  ValueKey<String>? _getKeyById(String id) {\n    if (_gridMap.containsKey(id)) {\n      return _gridMap[id];\n    }\n    final newKey = ValueKey(id);\n    _gridMap[id] = newKey;\n    return newKey;\n  }\n\n  Widget _cellLeading() {\n    return SizedBox(\n      width: context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding,\n    );\n  }\n}\n\nclass _CellTrailing extends StatelessWidget {\n  const _CellTrailing({required this.viewId});\n\n  final String viewId;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      width: GridSize.newPropertyButtonWidth,\n      height: GridSize.headerHeight,\n      decoration: BoxDecoration(\n        border: Border(\n          bottom: BorderSide(color: AFThemeExtension.of(context).borderColor),\n        ),\n      ),\n      child: CreateFieldButton(\n        viewId: viewId,\n        onFieldCreated: (fieldId) => context\n            .read<GridHeaderBloc>()\n            .add(GridHeaderEvent.startEditingNewField(fieldId)),\n      ),\n    );\n  }\n}\n\nclass CreateFieldButton extends StatelessWidget {\n  const CreateFieldButton({\n    super.key,\n    required this.viewId,\n    required this.onFieldCreated,\n  });\n\n  final String viewId;\n  final void Function(String fieldId) onFieldCreated;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      margin: GridSize.cellContentInsets,\n      radius: BorderRadius.zero,\n      text: FlowyText(\n        lineHeight: 1.0,\n        LocaleKeys.grid_field_newProperty.tr(),\n        overflow: TextOverflow.ellipsis,\n      ),\n      hoverColor: AFThemeExtension.of(context).greyHover,\n      onTap: () async {\n        final result = await FieldBackendService.createField(\n          viewId: viewId,\n        );\n        result.fold(\n          (field) => onFieldCreated(field.id),\n          (err) => Log.error(\"Failed to create field type option: $err\"),\n        );\n      },\n      leftIcon: const FlowySvg(\n        FlowySvgs.add_less_padding_s,\n        size: Size.square(16),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_field_button.dart",
    "content": "import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileFieldButton extends StatelessWidget {\n  const MobileFieldButton.first({\n    super.key,\n    required this.viewId,\n    required this.fieldController,\n    required this.fieldInfo,\n  })  : radius = const BorderRadius.only(topLeft: Radius.circular(24)),\n        margin = const EdgeInsets.symmetric(vertical: 14, horizontal: 18),\n        index = null;\n\n  const MobileFieldButton({\n    super.key,\n    required this.viewId,\n    required this.fieldController,\n    required this.fieldInfo,\n    required this.index,\n  })  : radius = BorderRadius.zero,\n        margin = const EdgeInsets.symmetric(vertical: 14, horizontal: 12);\n\n  final String viewId;\n  final int? index;\n  final FieldController fieldController;\n  final FieldInfo fieldInfo;\n  final BorderRadius? radius;\n  final EdgeInsets? margin;\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child = Container(\n      width: 200,\n      decoration: _getDecoration(context),\n      child: FlowyButton(\n        onTap: () =>\n            showQuickEditField(context, viewId, fieldController, fieldInfo),\n        radius: radius,\n        margin: margin,\n        leftIconSize: const Size.square(18),\n        leftIcon: FieldIcon(\n          fieldInfo: fieldInfo,\n          dimension: 18,\n        ),\n        text: FlowyText(\n          fieldInfo.name,\n          fontSize: 15,\n          overflow: TextOverflow.ellipsis,\n        ),\n      ),\n    );\n\n    if (index != null) {\n      child = ReorderableDelayedDragStartListener(index: index!, child: child);\n    }\n\n    return child;\n  }\n\n  BoxDecoration? _getDecoration(BuildContext context) {\n    final borderSide = BorderSide(\n      color: Theme.of(context).dividerColor,\n    );\n\n    if (index == null) {\n      return BoxDecoration(\n        borderRadius: const BorderRadiusDirectional.only(\n          topStart: Radius.circular(24),\n        ),\n        border: BorderDirectional(\n          top: borderSide,\n          start: borderSide,\n        ),\n      );\n    } else {\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_grid_header.dart",
    "content": "import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/application/grid_header_bloc.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../layout/sizes.dart';\nimport '../../mobile_grid_page.dart';\nimport 'mobile_field_button.dart';\n\nconst double _kGridHeaderHeight = 50.0;\n\nclass MobileGridHeader extends StatefulWidget {\n  const MobileGridHeader({\n    super.key,\n    required this.viewId,\n    required this.contentScrollController,\n    required this.reorderableController,\n  });\n\n  final String viewId;\n  final ScrollController contentScrollController;\n  final ScrollController reorderableController;\n\n  @override\n  State<MobileGridHeader> createState() => _MobileGridHeaderState();\n}\n\nclass _MobileGridHeaderState extends State<MobileGridHeader> {\n  @override\n  Widget build(BuildContext context) {\n    final fieldController =\n        context.read<GridBloc>().databaseController.fieldController;\n    final isEditable =\n        context.read<PageAccessLevelBloc?>()?.state.isEditable ?? false;\n    return BlocProvider(\n      create: (context) {\n        return GridHeaderBloc(\n          viewId: widget.viewId,\n          fieldController: fieldController,\n        )..add(const GridHeaderEvent.initial());\n      },\n      child: Stack(\n        children: [\n          BlocBuilder<GridHeaderBloc, GridHeaderState>(\n            builder: (context, state) {\n              return SingleChildScrollView(\n                scrollDirection: Axis.horizontal,\n                controller: widget.contentScrollController,\n                child: Stack(\n                  children: [\n                    Positioned(\n                      top: 0,\n                      left: GridSize.horizontalHeaderPadding + 24,\n                      right: GridSize.horizontalHeaderPadding + 24,\n                      child: _divider(),\n                    ),\n                    Positioned(\n                      bottom: 0,\n                      left: GridSize.horizontalHeaderPadding,\n                      right: GridSize.horizontalHeaderPadding,\n                      child: _divider(),\n                    ),\n                    SizedBox(\n                      height: _kGridHeaderHeight,\n                      width: getMobileGridContentWidth(state.fields),\n                    ),\n                  ],\n                ),\n              );\n            },\n          ),\n          IgnorePointer(\n            ignoring: !isEditable,\n            child: SizedBox(\n              height: _kGridHeaderHeight,\n              child: _GridHeader(\n                viewId: widget.viewId,\n                fieldController: fieldController,\n                scrollController: widget.reorderableController,\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _divider() {\n    return Divider(\n      height: 1,\n      thickness: 1,\n      color: Theme.of(context).dividerColor,\n    );\n  }\n}\n\nclass _GridHeader extends StatefulWidget {\n  const _GridHeader({\n    required this.viewId,\n    required this.fieldController,\n    required this.scrollController,\n  });\n\n  final String viewId;\n  final FieldController fieldController;\n  final ScrollController scrollController;\n\n  @override\n  State<_GridHeader> createState() => _GridHeaderState();\n}\n\nclass _GridHeaderState extends State<_GridHeader> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<GridHeaderBloc, GridHeaderState>(\n      builder: (context, state) {\n        final fields = [...state.fields];\n        FieldInfo? firstField;\n        if (fields.isNotEmpty) {\n          firstField = fields.removeAt(0);\n        }\n\n        final cells = fields\n            .mapIndexed(\n              (index, fieldInfo) => MobileFieldButton(\n                key: ValueKey(fieldInfo.id),\n                index: index,\n                viewId: widget.viewId,\n                fieldController: widget.fieldController,\n                fieldInfo: fieldInfo,\n              ),\n            )\n            .toList();\n\n        return ReorderableListView.builder(\n          scrollController: widget.scrollController,\n          shrinkWrap: true,\n          scrollDirection: Axis.horizontal,\n          proxyDecorator: (child, index, anim) => Material(\n            color: Colors.transparent,\n            child: child,\n          ),\n          padding: EdgeInsets.symmetric(\n            horizontal: GridSize.horizontalHeaderPadding,\n          ),\n          header: firstField != null\n              ? MobileFieldButton.first(\n                  viewId: widget.viewId,\n                  fieldController: widget.fieldController,\n                  fieldInfo: firstField,\n                )\n              : null,\n          footer: CreateFieldButton(\n            viewId: widget.viewId,\n            onFieldCreated: (fieldId) => context\n                .read<GridHeaderBloc>()\n                .add(GridHeaderEvent.startEditingNewField(fieldId)),\n          ),\n          onReorder: (int oldIndex, int newIndex) {\n            if (oldIndex < newIndex) {\n              newIndex--;\n            }\n            oldIndex++;\n            newIndex++;\n            context\n                .read<GridHeaderBloc>()\n                .add(GridHeaderEvent.moveField(oldIndex, newIndex));\n          },\n          itemCount: cells.length,\n          itemBuilder: (context, index) => cells[index],\n        );\n      },\n    );\n  }\n}\n\nclass CreateFieldButton extends StatelessWidget {\n  const CreateFieldButton({\n    super.key,\n    required this.viewId,\n    required this.onFieldCreated,\n  });\n\n  final String viewId;\n  final void Function(String fieldId) onFieldCreated;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      constraints: BoxConstraints(\n        maxWidth: GridSize.mobileNewPropertyButtonWidth,\n        minHeight: GridSize.headerHeight,\n      ),\n      decoration: _getDecoration(context),\n      child: FlowyButton(\n        margin: const EdgeInsets.symmetric(vertical: 14, horizontal: 12),\n        radius: const BorderRadius.only(topRight: Radius.circular(24)),\n        text: FlowyText(\n          LocaleKeys.grid_field_newProperty.tr(),\n          fontSize: 15,\n          overflow: TextOverflow.ellipsis,\n          color: Theme.of(context).hintColor,\n        ),\n        hoverColor: AFThemeExtension.of(context).greyHover,\n        onTap: () => mobileCreateFieldWorkflow(context, viewId),\n        leftIconSize: const Size.square(18),\n        leftIcon: FlowySvg(\n          FlowySvgs.add_s,\n          size: const Size.square(18),\n          color: Theme.of(context).hintColor,\n        ),\n      ),\n    );\n  }\n\n  BoxDecoration? _getDecoration(BuildContext context) {\n    final borderSide = BorderSide(\n      color: Theme.of(context).dividerColor,\n    );\n\n    return BoxDecoration(\n      borderRadius: const BorderRadiusDirectional.only(\n        topEnd: Radius.circular(24),\n      ),\n      border: BorderDirectional(\n        top: borderSide,\n        end: borderSide,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/mobile_fab.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart';\nimport 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:provider/provider.dart';\n\nWidget getGridFabs(BuildContext context) {\n  return Row(\n    mainAxisSize: MainAxisSize.min,\n    children: [\n      MobileGridFab(\n        backgroundColor: Theme.of(context).colorScheme.surface,\n        foregroundColor: Theme.of(context).primaryColor,\n        onTap: () {\n          final bloc = context.read<GridBloc>();\n          if (bloc.state.rowInfos.isNotEmpty) {\n            context.push(\n              MobileRowDetailPage.routeName,\n              extra: {\n                MobileRowDetailPage.argRowId: bloc.state.rowInfos.first.rowId,\n                MobileRowDetailPage.argDatabaseController:\n                    bloc.databaseController,\n              },\n            );\n          }\n        },\n        boxShadow: const BoxShadow(\n          offset: Offset(0, 8),\n          color: Color(0x145D7D8B),\n          blurRadius: 20,\n        ),\n        icon: FlowySvgs.properties_s,\n        iconSize: const Size.square(24),\n      ),\n      const HSpace(16),\n      MobileGridFab(\n        backgroundColor: Theme.of(context).primaryColor,\n        foregroundColor: Colors.white,\n        onTap: () {\n          context\n              .read<GridBloc>()\n              .add(const GridEvent.createRow(openRowDetail: true));\n        },\n        overlayColor: const WidgetStatePropertyAll<Color>(Color(0xFF009FD1)),\n        boxShadow: const BoxShadow(\n          offset: Offset(0, 8),\n          color: Color(0x6612BFEF),\n          blurRadius: 18,\n          spreadRadius: -5,\n        ),\n        icon: FlowySvgs.add_s,\n        iconSize: const Size.square(24),\n      ),\n    ],\n  );\n}\n\nclass MobileGridFab extends StatelessWidget {\n  const MobileGridFab({\n    super.key,\n    required this.backgroundColor,\n    required this.foregroundColor,\n    required this.boxShadow,\n    required this.onTap,\n    required this.icon,\n    required this.iconSize,\n    this.overlayColor,\n  });\n\n  final Color backgroundColor;\n  final Color foregroundColor;\n  final BoxShadow boxShadow;\n  final VoidCallback onTap;\n  final FlowySvgData icon;\n  final Size iconSize;\n  final WidgetStateProperty<Color?>? overlayColor;\n\n  @override\n  Widget build(BuildContext context) {\n    final radius = BorderRadius.circular(20);\n    return DecoratedBox(\n      decoration: BoxDecoration(\n        color: backgroundColor,\n        border: const Border.fromBorderSide(\n          BorderSide(width: 0.5, color: Color(0xFFE4EDF0)),\n        ),\n        borderRadius: radius,\n        boxShadow: [boxShadow],\n      ),\n      child: Material(\n        borderOnForeground: false,\n        color: Colors.transparent,\n        borderRadius: radius,\n        child: InkWell(\n          borderRadius: radius,\n          overlayColor: overlayColor,\n          onTap: onTap,\n          child: SizedBox.square(\n            dimension: 56,\n            child: Center(\n              child: FlowySvg(\n                icon,\n                color: foregroundColor,\n                size: iconSize,\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/action.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy/plugins/database/domain/sort_service.dart';\nimport 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass RowActionMenu extends StatelessWidget {\n  const RowActionMenu({\n    super.key,\n    required this.viewId,\n    required this.rowId,\n    this.actions = RowAction.values,\n    this.groupId,\n  });\n\n  const RowActionMenu.board({\n    super.key,\n    required this.viewId,\n    required this.rowId,\n    required this.groupId,\n  }) : actions = const [RowAction.duplicate, RowAction.delete];\n\n  final String viewId;\n  final RowId rowId;\n  final List<RowAction> actions;\n  final String? groupId;\n\n  @override\n  Widget build(BuildContext context) {\n    final cells =\n        actions.map((action) => _actionCell(context, action)).toList();\n\n    return SeparatedColumn(\n      crossAxisAlignment: CrossAxisAlignment.stretch,\n      mainAxisSize: MainAxisSize.min,\n      separatorBuilder: () => VSpace(GridSize.typeOptionSeparatorHeight),\n      children: cells,\n    );\n  }\n\n  Widget _actionCell(BuildContext context, RowAction action) {\n    Widget icon = FlowySvg(action.icon);\n    if (action == RowAction.insertAbove) {\n      icon = RotatedBox(quarterTurns: 1, child: icon);\n    }\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        text: FlowyText(\n          action.text,\n          overflow: TextOverflow.ellipsis,\n          lineHeight: 1.0,\n        ),\n        onTap: () {\n          action.performAction(context, viewId, rowId);\n          PopoverContainer.of(context).close();\n        },\n        leftIcon: icon,\n      ),\n    );\n  }\n}\n\nenum RowAction {\n  insertAbove,\n  insertBelow,\n  duplicate,\n  delete;\n\n  FlowySvgData get icon {\n    return switch (this) {\n      insertAbove => FlowySvgs.arrow_s,\n      insertBelow => FlowySvgs.add_s,\n      duplicate => FlowySvgs.duplicate_s,\n      delete => FlowySvgs.delete_s,\n    };\n  }\n\n  String get text {\n    return switch (this) {\n      insertAbove => LocaleKeys.grid_row_insertRecordAbove.tr(),\n      insertBelow => LocaleKeys.grid_row_insertRecordBelow.tr(),\n      duplicate => LocaleKeys.grid_row_duplicate.tr(),\n      delete => LocaleKeys.grid_row_delete.tr(),\n    };\n  }\n\n  void performAction(BuildContext context, String viewId, String rowId) {\n    switch (this) {\n      case insertAbove:\n      case insertBelow:\n        final position = this == insertAbove\n            ? OrderObjectPositionTypePB.Before\n            : OrderObjectPositionTypePB.After;\n        final intention = this == insertAbove\n            ? LocaleKeys.grid_row_createRowAboveDescription.tr()\n            : LocaleKeys.grid_row_createRowBelowDescription.tr();\n        if (context.read<GridBloc>().state.sorts.isNotEmpty) {\n          showCancelAndDeleteDialog(\n            context: context,\n            title: LocaleKeys.grid_sort_sortsActive.tr(\n              namedArgs: {'intention': intention},\n            ),\n            description: LocaleKeys.grid_sort_removeSorting.tr(),\n            confirmLabel: LocaleKeys.button_remove.tr(),\n            closeOnAction: true,\n            onDelete: () {\n              SortBackendService(viewId: viewId).deleteAllSorts();\n              RowBackendService.createRow(\n                viewId: viewId,\n                position: position,\n                targetRowId: rowId,\n              );\n            },\n          );\n        } else {\n          RowBackendService.createRow(\n            viewId: viewId,\n            position: position,\n            targetRowId: rowId,\n          );\n        }\n        break;\n      case duplicate:\n        RowBackendService.duplicateRow(viewId, rowId);\n        break;\n      case delete:\n        showConfirmDeletionDialog(\n          context: context,\n          name: LocaleKeys.grid_row_label.tr(),\n          description: LocaleKeys.grid_row_deleteRowPrompt.tr(),\n          onConfirm: () => RowBackendService.deleteRows(viewId, [rowId]),\n        );\n        break;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_cache.dart';\nimport 'package:appflowy/plugins/database/application/row/row_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy/plugins/database/grid/application/row/row_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/mobile_cell_container.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../layout/sizes.dart';\n\nclass MobileGridRow extends StatefulWidget {\n  const MobileGridRow({\n    super.key,\n    required this.rowId,\n    required this.databaseController,\n    required this.openDetailPage,\n    this.isDraggable = false,\n  });\n\n  final RowId rowId;\n  final DatabaseController databaseController;\n  final void Function(BuildContext context) openDetailPage;\n  final bool isDraggable;\n\n  @override\n  State<MobileGridRow> createState() => _MobileGridRowState();\n}\n\nclass _MobileGridRowState extends State<MobileGridRow> {\n  late final RowController _rowController;\n  late final EditableCellBuilder _cellBuilder;\n\n  String get viewId => widget.databaseController.viewId;\n  RowCache get rowCache => widget.databaseController.rowCache;\n\n  @override\n  void initState() {\n    super.initState();\n    _rowController = RowController(\n      rowMeta: rowCache.getRow(widget.rowId)!.rowMeta,\n      viewId: viewId,\n      rowCache: rowCache,\n    );\n    _cellBuilder = EditableCellBuilder(\n      databaseController: widget.databaseController,\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => RowBloc(\n        fieldController: widget.databaseController.fieldController,\n        rowId: widget.rowId,\n        rowController: _rowController,\n        viewId: viewId,\n      ),\n      child: BlocBuilder<RowBloc, RowState>(\n        builder: (context, state) {\n          return Row(\n            children: [\n              SizedBox(width: GridSize.horizontalHeaderPadding),\n              Expanded(\n                child: RowContent(\n                  fieldController: widget.databaseController.fieldController,\n                  builder: _cellBuilder,\n                  onExpand: () => widget.openDetailPage(context),\n                ),\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  @override\n  Future<void> dispose() async {\n    super.dispose();\n    await _rowController.dispose();\n  }\n}\n\nclass RowContent extends StatelessWidget {\n  const RowContent({\n    super.key,\n    required this.fieldController,\n    required this.onExpand,\n    required this.builder,\n  });\n\n  final FieldController fieldController;\n  final VoidCallback onExpand;\n  final EditableCellBuilder builder;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<RowBloc, RowState>(\n      builder: (context, state) {\n        return SizedBox(\n          height: 52,\n          child: Row(\n            crossAxisAlignment: CrossAxisAlignment.stretch,\n            children: [\n              ..._makeCells(context, state.cellContexts),\n              _finalCellDecoration(context),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  List<Widget> _makeCells(\n    BuildContext context,\n    List<CellContext> cellContexts,\n  ) {\n    return cellContexts.map(\n      (cellContext) {\n        final fieldInfo = fieldController.getField(cellContext.fieldId)!;\n        final EditableCellWidget child = builder.buildStyled(\n          cellContext,\n          EditableCellStyle.mobileGrid,\n        );\n        return MobileCellContainer(\n          isPrimary: fieldInfo.field.isPrimary,\n          onPrimaryFieldCellTap: onExpand,\n          child: child,\n        );\n      },\n    ).toList();\n  }\n\n  Widget _finalCellDecoration(BuildContext context) {\n    return Container(\n      width: 200,\n      constraints: const BoxConstraints(minHeight: 46),\n      decoration: BoxDecoration(\n        border: Border(\n          bottom: BorderSide(color: Theme.of(context).dividerColor),\n          right: BorderSide(color: Theme.of(context).dividerColor),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport \"package:appflowy/generated/locale_keys.g.dart\";\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy/plugins/database/domain/sort_service.dart';\nimport 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/application/row/row_bloc.dart';\nimport 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\n\nimport '../../../../widgets/row/accessory/cell_accessory.dart';\nimport '../../../../widgets/row/cells/cell_container.dart';\nimport '../../layout/sizes.dart';\nimport 'action.dart';\n\nclass GridRow extends StatelessWidget {\n  const GridRow({\n    super.key,\n    required this.fieldController,\n    required this.viewId,\n    required this.rowId,\n    required this.rowController,\n    required this.cellBuilder,\n    required this.openDetailPage,\n    required this.index,\n    this.shrinkWrap = false,\n    required this.editable,\n  });\n\n  final FieldController fieldController;\n  final String viewId;\n  final RowId rowId;\n  final RowController rowController;\n  final EditableCellBuilder cellBuilder;\n  final void Function(BuildContext context) openDetailPage;\n  final int index;\n  final bool shrinkWrap;\n  final bool editable;\n\n  @override\n  Widget build(BuildContext context) {\n    Widget rowContent = RowContent(\n      fieldController: fieldController,\n      cellBuilder: cellBuilder,\n      onExpand: () => openDetailPage(context),\n    );\n\n    if (!shrinkWrap) {\n      rowContent = Expanded(child: rowContent);\n    }\n\n    rowContent = BlocProvider(\n      create: (_) => RowBloc(\n        fieldController: fieldController,\n        rowId: rowId,\n        rowController: rowController,\n        viewId: viewId,\n      ),\n      child: _RowEnterRegion(\n        child: Row(\n          children: [\n            _RowLeading(viewId: viewId, index: index),\n            rowContent,\n          ],\n        ),\n      ),\n    );\n\n    if (!editable) {\n      rowContent = IgnorePointer(\n        child: rowContent,\n      );\n    }\n\n    return rowContent;\n  }\n}\n\nclass _RowLeading extends StatefulWidget {\n  const _RowLeading({\n    required this.viewId,\n    required this.index,\n  });\n\n  final String viewId;\n  final int index;\n\n  @override\n  State<_RowLeading> createState() => _RowLeadingState();\n}\n\nclass _RowLeadingState extends State<_RowLeading> {\n  final PopoverController popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      controller: popoverController,\n      triggerActions: PopoverTriggerFlags.none,\n      constraints: BoxConstraints.loose(const Size(200, 200)),\n      direction: PopoverDirection.rightWithCenterAligned,\n      margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),\n      popupBuilder: (_) {\n        final bloc = context.read<RowBloc>();\n        return BlocProvider.value(\n          value: context.read<GridBloc>(),\n          child: RowActionMenu(\n            viewId: bloc.viewId,\n            rowId: bloc.rowId,\n          ),\n        );\n      },\n      child: Consumer<RegionStateNotifier>(\n        builder: (context, state, _) {\n          return SizedBox(\n            width: context\n                .read<DatabasePluginWidgetBuilderSize>()\n                .horizontalPadding,\n            child: state.onEnter ? _activeWidget() : null,\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _activeWidget() {\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.end,\n      children: [\n        InsertRowButton(viewId: widget.viewId),\n        ReorderableDragStartListener(\n          index: widget.index,\n          child: RowMenuButton(\n            openMenu: popoverController.show,\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass InsertRowButton extends StatelessWidget {\n  const InsertRowButton({\n    super.key,\n    required this.viewId,\n  });\n\n  final String viewId;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyIconButton(\n      tooltipText: LocaleKeys.tooltip_addNewRow.tr(),\n      hoverColor: AFThemeExtension.of(context).lightGreyHover,\n      width: 20,\n      height: 30,\n      onPressed: () {\n        final rowBloc = context.read<RowBloc>();\n        if (context.read<GridBloc>().state.sorts.isNotEmpty) {\n          showCancelAndDeleteDialog(\n            context: context,\n            title: LocaleKeys.grid_sort_sortsActive.tr(\n              namedArgs: {\n                'intention': LocaleKeys.grid_row_createRowBelowDescription.tr(),\n              },\n            ),\n            description: LocaleKeys.grid_sort_removeSorting.tr(),\n            confirmLabel: LocaleKeys.button_remove.tr(),\n            closeOnAction: true,\n            onDelete: () {\n              SortBackendService(viewId: viewId).deleteAllSorts();\n              rowBloc.add(const RowEvent.createRow());\n            },\n          );\n        } else {\n          rowBloc.add(const RowEvent.createRow());\n        }\n      },\n      iconPadding: const EdgeInsets.all(3),\n      icon: FlowySvg(\n        FlowySvgs.add_s,\n        color: Theme.of(context).colorScheme.tertiary,\n      ),\n    );\n  }\n}\n\nclass RowMenuButton extends StatefulWidget {\n  const RowMenuButton({\n    super.key,\n    required this.openMenu,\n  });\n\n  final VoidCallback openMenu;\n\n  @override\n  State<RowMenuButton> createState() => _RowMenuButtonState();\n}\n\nclass _RowMenuButtonState extends State<RowMenuButton> {\n  @override\n  Widget build(BuildContext context) {\n    return FlowyIconButton(\n      richTooltipText: TextSpan(\n        children: [\n          TextSpan(\n            text: '${LocaleKeys.tooltip_dragRow.tr()}\\n',\n            style: context.tooltipTextStyle(),\n          ),\n          TextSpan(\n            text: LocaleKeys.tooltip_openMenu.tr(),\n            style: context.tooltipTextStyle(),\n          ),\n        ],\n      ),\n      hoverColor: AFThemeExtension.of(context).lightGreyHover,\n      width: 20,\n      height: 30,\n      onPressed: () => widget.openMenu(),\n      iconPadding: const EdgeInsets.all(3),\n      icon: FlowySvg(\n        FlowySvgs.drag_element_s,\n        color: Theme.of(context).colorScheme.tertiary,\n      ),\n    );\n  }\n}\n\nclass RowContent extends StatelessWidget {\n  const RowContent({\n    super.key,\n    required this.fieldController,\n    required this.cellBuilder,\n    required this.onExpand,\n  });\n\n  final FieldController fieldController;\n  final VoidCallback onExpand;\n  final EditableCellBuilder cellBuilder;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<RowBloc, RowState>(\n      builder: (context, state) {\n        return IntrinsicHeight(\n          child: Row(\n            crossAxisAlignment: CrossAxisAlignment.stretch,\n            children: [\n              ..._makeCells(context, state.cellContexts),\n              _finalCellDecoration(context),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  List<Widget> _makeCells(\n    BuildContext context,\n    List<CellContext> cellContexts,\n  ) {\n    return cellContexts.map(\n      (cellContext) {\n        final fieldInfo = fieldController.getField(cellContext.fieldId)!;\n        final EditableCellWidget child = cellBuilder.buildStyled(\n          cellContext,\n          EditableCellStyle.desktopGrid,\n        );\n        return CellContainer(\n          width: fieldInfo.width!.toDouble(),\n          isPrimary: fieldInfo.field.isPrimary,\n          accessoryBuilder: (buildContext) {\n            final builder = child.accessoryBuilder;\n            final List<GridCellAccessoryBuilder> accessories = [];\n            if (fieldInfo.field.isPrimary) {\n              accessories.add(\n                GridCellAccessoryBuilder(\n                  builder: (key) => PrimaryCellAccessory(\n                    key: key,\n                    onTap: onExpand,\n                    isCellEditing: buildContext.isCellEditing,\n                  ),\n                ),\n              );\n            }\n\n            if (builder != null) {\n              accessories.addAll(builder(buildContext));\n            }\n\n            return accessories;\n          },\n          child: child,\n        );\n      },\n    ).toList();\n  }\n\n  Widget _finalCellDecoration(BuildContext context) {\n    return MouseRegion(\n      cursor: SystemMouseCursors.basic,\n      child: ValueListenableBuilder(\n        valueListenable: cellBuilder.databaseController.compactModeNotifier,\n        builder: (context, compactMode, _) {\n          return Container(\n            width: GridSize.newPropertyButtonWidth,\n            constraints: BoxConstraints(minHeight: compactMode ? 32 : 36),\n            decoration: BoxDecoration(\n              border: Border(\n                bottom:\n                    BorderSide(color: AFThemeExtension.of(context).borderColor),\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass RegionStateNotifier extends ChangeNotifier {\n  bool _onEnter = false;\n\n  set onEnter(bool value) {\n    if (_onEnter != value) {\n      _onEnter = value;\n      notifyListeners();\n    }\n  }\n\n  bool get onEnter => _onEnter;\n}\n\nclass _RowEnterRegion extends StatefulWidget {\n  const _RowEnterRegion({required this.child});\n\n  final Widget child;\n\n  @override\n  State<_RowEnterRegion> createState() => _RowEnterRegionState();\n}\n\nclass _RowEnterRegionState extends State<_RowEnterRegion> {\n  late final RegionStateNotifier _rowStateNotifier;\n\n  @override\n  void initState() {\n    super.initState();\n    _rowStateNotifier = RegionStateNotifier();\n  }\n\n  @override\n  Future<void> dispose() async {\n    _rowStateNotifier.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ChangeNotifierProvider.value(\n      value: _rowStateNotifier,\n      child: MouseRegion(\n        cursor: SystemMouseCursors.click,\n        onEnter: (p) => _rowStateNotifier.onEnter = true,\n        onExit: (p) => _rowStateNotifier.onEnter = false,\n        child: widget.child,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/shortcuts.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nclass GridShortcuts extends StatelessWidget {\n  const GridShortcuts({required this.child, super.key});\n\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return Shortcuts(\n      shortcuts: bindKeys([]),\n      child: Actions(\n        dispatcher: LoggingActionDispatcher(),\n        actions: const {},\n        child: child,\n      ),\n    );\n  }\n}\n\nMap<ShortcutActivator, Intent> bindKeys(List<LogicalKeyboardKey> keys) {\n  return {for (final key in keys) LogicalKeySet(key): KeyboardKeyIdent(key)};\n}\n\nclass KeyboardKeyIdent extends Intent {\n  const KeyboardKeyIdent(this.key);\n\n  final KeyboardKey key;\n}\n\nclass LoggingActionDispatcher extends ActionDispatcher {\n  @override\n  Object? invokeAction(\n    covariant Action<Intent> action,\n    covariant Intent intent, [\n    BuildContext? context,\n  ]) {\n    // print('Action invoked: $action($intent) from $context');\n    super.invokeAction(action, intent, context);\n\n    return null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/grid/application/simple_text_filter_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/style_widget/text_field.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass CreateDatabaseViewSortList extends StatelessWidget {\n  const CreateDatabaseViewSortList({\n    super.key,\n    required this.onTap,\n  });\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    final sortBloc = context.read<SortEditorBloc>();\n    return BlocProvider(\n      create: (_) => SimpleTextFilterBloc<FieldInfo>(\n        values: List.from(sortBloc.state.creatableFields),\n        comparator: (val) => val.name,\n      ),\n      child: BlocListener<SortEditorBloc, SortEditorState>(\n        listenWhen: (previous, current) =>\n            previous.creatableFields != current.creatableFields,\n        listener: (context, state) {\n          context.read<SimpleTextFilterBloc<FieldInfo>>().add(\n                SimpleTextFilterEvent.receiveNewValues(state.creatableFields),\n              );\n        },\n        child: BlocBuilder<SimpleTextFilterBloc<FieldInfo>,\n            SimpleTextFilterState<FieldInfo>>(\n          builder: (context, state) {\n            final cells = state.values.map((fieldInfo) {\n              return GridSortPropertyCell(\n                fieldInfo: fieldInfo,\n                onTap: () {\n                  context\n                      .read<SortEditorBloc>()\n                      .add(SortEditorEvent.createSort(fieldId: fieldInfo.id));\n                  onTap.call();\n                },\n              );\n            }).toList();\n\n            final List<Widget> slivers = [\n              SliverPersistentHeader(\n                pinned: true,\n                delegate: _SortTextFieldDelegate(),\n              ),\n              SliverToBoxAdapter(\n                child: ListView.separated(\n                  shrinkWrap: true,\n                  itemCount: cells.length,\n                  itemBuilder: (_, index) => cells[index],\n                  separatorBuilder: (_, __) =>\n                      VSpace(GridSize.typeOptionSeparatorHeight),\n                ),\n              ),\n            ];\n            return CustomScrollView(\n              shrinkWrap: true,\n              slivers: slivers,\n              physics: StyledScrollPhysics(),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass _SortTextFieldDelegate extends SliverPersistentHeaderDelegate {\n  _SortTextFieldDelegate();\n\n  double fixHeight = 36;\n\n  @override\n  Widget build(\n    BuildContext context,\n    double shrinkOffset,\n    bool overlapsContent,\n  ) {\n    return Container(\n      padding: const EdgeInsets.only(bottom: 4),\n      color: Theme.of(context).cardColor,\n      height: fixHeight,\n      child: FlowyTextField(\n        hintText: LocaleKeys.grid_settings_sortBy.tr(),\n        onChanged: (text) {\n          context\n              .read<SimpleTextFilterBloc<FieldInfo>>()\n              .add(SimpleTextFilterEvent.updateFilter(text));\n        },\n      ),\n    );\n  }\n\n  @override\n  double get maxExtent => fixHeight;\n\n  @override\n  double get minExtent => fixHeight;\n\n  @override\n  bool shouldRebuild(covariant oldDelegate) => false;\n}\n\nclass GridSortPropertyCell extends StatelessWidget {\n  const GridSortPropertyCell({\n    super.key,\n    required this.fieldInfo,\n    required this.onTap,\n  });\n\n  final FieldInfo fieldInfo;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        text: FlowyText(\n          fieldInfo.name,\n          lineHeight: 1.0,\n          color: AFThemeExtension.of(context).textColor,\n        ),\n        onTap: onTap,\n        leftIcon: FieldIcon(\n          fieldInfo: fieldInfo,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/order_panel.dart",
    "content": "import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_editor.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\n\nclass OrderPanel extends StatelessWidget {\n  const OrderPanel({required this.onCondition, super.key});\n\n  final Function(SortConditionPB) onCondition;\n\n  @override\n  Widget build(BuildContext context) {\n    final List<Widget> children = SortConditionPB.values.map((condition) {\n      return OrderPanelItem(\n        condition: condition,\n        onCondition: onCondition,\n      );\n    }).toList();\n\n    return ConstrainedBox(\n      constraints: const BoxConstraints(minWidth: 160),\n      child: IntrinsicWidth(\n        child: IntrinsicHeight(\n          child: Column(\n            children: children,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass OrderPanelItem extends StatelessWidget {\n  const OrderPanelItem({\n    super.key,\n    required this.condition,\n    required this.onCondition,\n  });\n\n  final SortConditionPB condition;\n  final Function(SortConditionPB) onCondition;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        text: FlowyText(condition.title),\n        onTap: () => onCondition(condition),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_choice_button.dart",
    "content": "import 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\n\nclass SortChoiceButton extends StatelessWidget {\n  const SortChoiceButton({\n    super.key,\n    required this.text,\n    this.onTap,\n    this.radius = const Radius.circular(14),\n    this.leftIcon,\n    this.rightIcon,\n    this.editable = true,\n  });\n\n  final String text;\n  final VoidCallback? onTap;\n  final Radius radius;\n  final Widget? leftIcon;\n  final Widget? rightIcon;\n  final bool editable;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      decoration: BoxDecoration(\n        color: Colors.transparent,\n        border: Border.fromBorderSide(\n          BorderSide(color: Theme.of(context).dividerColor),\n        ),\n        borderRadius: BorderRadius.all(radius),\n      ),\n      useIntrinsicWidth: true,\n      text: FlowyText(\n        text,\n        lineHeight: 1.0,\n        color: AFThemeExtension.of(context).textColor,\n        overflow: TextOverflow.ellipsis,\n      ),\n      margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),\n      radius: BorderRadius.all(radius),\n      leftIcon: leftIcon,\n      rightIcon: rightIcon,\n      hoverColor: AFThemeExtension.of(context).lightGreyHover,\n      onTap: onTap,\n      disable: !editable,\n      disableOpacity: 1.0,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_editor.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/sort_entities.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'create_sort_list.dart';\nimport 'order_panel.dart';\nimport 'sort_choice_button.dart';\n\nclass SortEditor extends StatefulWidget {\n  const SortEditor({super.key});\n\n  @override\n  State<SortEditor> createState() => _SortEditorState();\n}\n\nclass _SortEditorState extends State<SortEditor> {\n  final popoverMutex = PopoverMutex();\n\n  @override\n  void dispose() {\n    popoverMutex.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SortEditorBloc, SortEditorState>(\n      builder: (context, state) {\n        return ReorderableListView.builder(\n          onReorder: (oldIndex, newIndex) => context\n              .read<SortEditorBloc>()\n              .add(SortEditorEvent.reorderSort(oldIndex, newIndex)),\n          itemCount: state.sorts.length,\n          itemBuilder: (context, index) => DatabaseSortItem(\n            key: ValueKey(state.sorts[index].sortId),\n            index: index,\n            sort: state.sorts[index],\n            popoverMutex: popoverMutex,\n          ),\n          proxyDecorator: (child, index, animation) => Material(\n            color: Colors.transparent,\n            child: Stack(\n              children: [\n                BlocProvider.value(\n                  value: context.read<SortEditorBloc>(),\n                  child: child,\n                ),\n                MouseRegion(\n                  cursor: Platform.isWindows\n                      ? SystemMouseCursors.click\n                      : SystemMouseCursors.grabbing,\n                  child: const SizedBox.expand(),\n                ),\n              ],\n            ),\n          ),\n          shrinkWrap: true,\n          buildDefaultDragHandles: false,\n          footer: Row(\n            children: [\n              Flexible(\n                child: DatabaseAddSortButton(\n                  disable: state.creatableFields.isEmpty,\n                  popoverMutex: popoverMutex,\n                ),\n              ),\n              const HSpace(6),\n              Flexible(\n                child: DeleteAllSortsButton(\n                  popoverMutex: popoverMutex,\n                ),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass DatabaseSortItem extends StatelessWidget {\n  const DatabaseSortItem({\n    super.key,\n    required this.index,\n    required this.popoverMutex,\n    required this.sort,\n  });\n\n  final int index;\n  final PopoverMutex popoverMutex;\n  final DatabaseSort sort;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.symmetric(vertical: 6),\n      color: Theme.of(context).cardColor,\n      child: Row(\n        children: [\n          ReorderableDragStartListener(\n            index: index,\n            child: MouseRegion(\n              cursor: Platform.isWindows\n                  ? SystemMouseCursors.click\n                  : SystemMouseCursors.grab,\n              child: SizedBox(\n                width: 14 + 12,\n                height: 14,\n                child: FlowySvg(\n                  FlowySvgs.drag_element_s,\n                  size: const Size.square(14),\n                  color: Theme.of(context).iconTheme.color,\n                ),\n              ),\n            ),\n          ),\n          Flexible(\n            fit: FlexFit.tight,\n            child: SizedBox(\n              height: 26,\n              child: BlocSelector<SortEditorBloc, SortEditorState, FieldInfo?>(\n                selector: (state) => state.allFields.firstWhereOrNull(\n                  (field) => field.id == sort.fieldId,\n                ),\n                builder: (context, field) {\n                  return SortChoiceButton(\n                    text: field?.name ?? \"\",\n                    editable: false,\n                  );\n                },\n              ),\n            ),\n          ),\n          const HSpace(6),\n          Flexible(\n            fit: FlexFit.tight,\n            child: SizedBox(\n              height: 26,\n              child: SortConditionButton(\n                sort: sort,\n                popoverMutex: popoverMutex,\n              ),\n            ),\n          ),\n          const HSpace(6),\n          FlowyIconButton(\n            width: 26,\n            onPressed: () {\n              context\n                  .read<SortEditorBloc>()\n                  .add(SortEditorEvent.deleteSort(sort.sortId));\n              PopoverContainer.of(context).close();\n            },\n            hoverColor: AFThemeExtension.of(context).lightGreyHover,\n            icon: FlowySvg(\n              FlowySvgs.trash_m,\n              color: Theme.of(context).iconTheme.color,\n              size: const Size.square(16),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nextension SortConditionExtension on SortConditionPB {\n  String get title {\n    return switch (this) {\n      SortConditionPB.Ascending => LocaleKeys.grid_sort_ascending.tr(),\n      SortConditionPB.Descending => LocaleKeys.grid_sort_descending.tr(),\n      _ => throw UnimplementedError(),\n    };\n  }\n}\n\nclass DatabaseAddSortButton extends StatefulWidget {\n  const DatabaseAddSortButton({\n    super.key,\n    required this.disable,\n    required this.popoverMutex,\n  });\n\n  final bool disable;\n  final PopoverMutex popoverMutex;\n\n  @override\n  State<DatabaseAddSortButton> createState() => _DatabaseAddSortButtonState();\n}\n\nclass _DatabaseAddSortButtonState extends State<DatabaseAddSortButton> {\n  final _popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      controller: _popoverController,\n      mutex: widget.popoverMutex,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      constraints: BoxConstraints.loose(const Size(200, 300)),\n      offset: const Offset(-6, 8),\n      triggerActions: PopoverTriggerFlags.none,\n      asBarrier: true,\n      popupBuilder: (popoverContext) {\n        return BlocProvider.value(\n          value: context.read<SortEditorBloc>(),\n          child: CreateDatabaseViewSortList(\n            onTap: () => _popoverController.close(),\n          ),\n        );\n      },\n      child: SizedBox(\n        height: GridSize.popoverItemHeight,\n        child: FlowyButton(\n          hoverColor: AFThemeExtension.of(context).greyHover,\n          disable: widget.disable,\n          text: FlowyText(LocaleKeys.grid_sort_addSort.tr()),\n          onTap: () => _popoverController.show(),\n          leftIcon: const FlowySvg(FlowySvgs.add_s),\n        ),\n      ),\n    );\n  }\n}\n\nclass DeleteAllSortsButton extends StatelessWidget {\n  const DeleteAllSortsButton({super.key, required this.popoverMutex});\n\n  final PopoverMutex popoverMutex;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SortEditorBloc, SortEditorState>(\n      builder: (context, state) {\n        return SizedBox(\n          height: GridSize.popoverItemHeight,\n          child: FlowyButton(\n            text: FlowyText(LocaleKeys.grid_sort_deleteAllSorts.tr()),\n            onTap: () {\n              context\n                  .read<SortEditorBloc>()\n                  .add(const SortEditorEvent.deleteAllSorts());\n              PopoverContainer.of(context).close();\n            },\n            leftIcon: const FlowySvg(FlowySvgs.delete_s),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass SortConditionButton extends StatefulWidget {\n  const SortConditionButton({\n    super.key,\n    required this.popoverMutex,\n    required this.sort,\n  });\n\n  final PopoverMutex popoverMutex;\n  final DatabaseSort sort;\n\n  @override\n  State<SortConditionButton> createState() => _SortConditionButtonState();\n}\n\nclass _SortConditionButtonState extends State<SortConditionButton> {\n  final PopoverController popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      controller: popoverController,\n      mutex: widget.popoverMutex,\n      constraints: BoxConstraints.loose(const Size(340, 200)),\n      direction: PopoverDirection.bottomWithLeftAligned,\n      triggerActions: PopoverTriggerFlags.none,\n      popupBuilder: (BuildContext popoverContext) {\n        return OrderPanel(\n          onCondition: (condition) {\n            context.read<SortEditorBloc>().add(\n                  SortEditorEvent.editSort(\n                    sortId: widget.sort.sortId,\n                    condition: condition,\n                  ),\n                );\n            popoverController.close();\n          },\n        );\n      },\n      child: SortChoiceButton(\n        text: widget.sort.condition.title,\n        rightIcon: FlowySvg(\n          FlowySvgs.arrow_down_s,\n          color: Theme.of(context).iconTheme.color,\n        ),\n        onTap: () => popoverController.show(),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_menu.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/sort_entities.dart';\nimport 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'sort_choice_button.dart';\nimport 'sort_editor.dart';\n\nclass SortMenu extends StatelessWidget {\n  const SortMenu({\n    super.key,\n    required this.fieldController,\n  });\n\n  final FieldController fieldController;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => SortEditorBloc(\n        viewId: fieldController.viewId,\n        fieldController: fieldController,\n      ),\n      child: BlocBuilder<SortEditorBloc, SortEditorState>(\n        builder: (context, state) {\n          if (state.sorts.isEmpty) {\n            return const SizedBox.shrink();\n          }\n\n          return AppFlowyPopover(\n            controller: PopoverController(),\n            constraints: BoxConstraints.loose(const Size(320, 200)),\n            direction: PopoverDirection.bottomWithLeftAligned,\n            offset: const Offset(0, 5),\n            margin: const EdgeInsets.fromLTRB(6.0, 0.0, 6.0, 6.0),\n            popupBuilder: (BuildContext popoverContext) {\n              return BlocProvider.value(\n                value: context.read<SortEditorBloc>(),\n                child: const SortEditor(),\n              );\n            },\n            child: SortChoiceChip(sorts: state.sorts),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass SortChoiceChip extends StatelessWidget {\n  const SortChoiceChip({\n    super.key,\n    required this.sorts,\n    this.onTap,\n  });\n\n  final List<DatabaseSort> sorts;\n  final VoidCallback? onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    final arrow = Transform.rotate(\n      angle: -math.pi / 2,\n      child: FlowySvg(\n        FlowySvgs.arrow_left_s,\n        color: Theme.of(context).iconTheme.color,\n      ),\n    );\n\n    final text = LocaleKeys.grid_settings_sort.tr();\n    final leftIcon = FlowySvg(\n      FlowySvgs.sort_ascending_s,\n      color: Theme.of(context).iconTheme.color,\n    );\n\n    return SizedBox(\n      height: 28,\n      child: SortChoiceButton(\n        text: text,\n        leftIcon: leftIcon,\n        rightIcon: arrow,\n        onTap: onTap,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../filter/create_filter_list.dart';\n\nclass FilterButton extends StatefulWidget {\n  const FilterButton({\n    super.key,\n    required this.toggleExtension,\n  });\n\n  final ToggleExtensionNotifier toggleExtension;\n\n  @override\n  State<FilterButton> createState() => _FilterButtonState();\n}\n\nclass _FilterButtonState extends State<FilterButton> {\n  final _popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FilterEditorBloc, FilterEditorState>(\n      builder: (context, state) {\n        return _wrapPopover(\n          MouseRegion(\n            cursor: SystemMouseCursors.click,\n            child: FlowyIconButton(\n              tooltipText: LocaleKeys.grid_settings_filter.tr(),\n              width: 24,\n              height: 24,\n              iconPadding: const EdgeInsets.all(3),\n              hoverColor: AFThemeExtension.of(context).lightGreyHover,\n              icon: const FlowySvg(FlowySvgs.database_filter_s),\n              onPressed: () {\n                final bloc = context.read<FilterEditorBloc>();\n                if (bloc.state.filters.isEmpty) {\n                  _popoverController.show();\n                } else {\n                  widget.toggleExtension.toggle();\n                }\n              },\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _wrapPopover(Widget child) {\n    return AppFlowyPopover(\n      controller: _popoverController,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      constraints: BoxConstraints.loose(const Size(200, 300)),\n      offset: const Offset(0, 8),\n      triggerActions: PopoverTriggerFlags.none,\n      child: child,\n      popupBuilder: (_) {\n        return BlocProvider.value(\n          value: context.read<FilterEditorBloc>(),\n          child: CreateDatabaseViewFilterList(\n            onTap: () {\n              if (!widget.toggleExtension.isToggled) {\n                widget.toggleExtension.toggle();\n              }\n              _popoverController.close();\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart",
    "content": "import 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/setting_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\n\nimport 'filter_button.dart';\nimport 'sort_button.dart';\nimport 'view_database_button.dart';\n\nclass GridSettingBar extends StatelessWidget {\n  const GridSettingBar({\n    super.key,\n    required this.controller,\n    required this.toggleExtension,\n  });\n\n  final DatabaseController controller;\n  final ToggleExtensionNotifier toggleExtension;\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider(\n          create: (context) => FilterEditorBloc(\n            viewId: controller.viewId,\n            fieldController: controller.fieldController,\n          ),\n        ),\n        BlocProvider(\n          create: (context) => SortEditorBloc(\n            viewId: controller.viewId,\n            fieldController: controller.fieldController,\n          ),\n        ),\n      ],\n      child: ValueListenableBuilder<bool>(\n        valueListenable: controller.isLoading,\n        builder: (context, isLoading, child) {\n          if (isLoading) {\n            return const SizedBox.shrink();\n          }\n          final isReference =\n              Provider.of<ReferenceState?>(context)?.isReference ?? false;\n          return SizedBox(\n            height: 20,\n            child: Row(\n              mainAxisAlignment: MainAxisAlignment.end,\n              children: [\n                FilterButton(toggleExtension: toggleExtension),\n                const HSpace(2),\n                SortButton(toggleExtension: toggleExtension),\n                if (isReference) ...[\n                  const HSpace(2),\n                  ViewDatabaseButton(view: controller.view),\n                ],\n                const HSpace(2),\n                SettingButton(databaseController: controller),\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../sort/create_sort_list.dart';\n\nclass SortButton extends StatefulWidget {\n  const SortButton({super.key, required this.toggleExtension});\n\n  final ToggleExtensionNotifier toggleExtension;\n\n  @override\n  State<SortButton> createState() => _SortButtonState();\n}\n\nclass _SortButtonState extends State<SortButton> {\n  final _popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SortEditorBloc, SortEditorState>(\n      builder: (context, state) {\n        return wrapPopover(\n          MouseRegion(\n            cursor: SystemMouseCursors.click,\n            child: FlowyIconButton(\n              tooltipText: LocaleKeys.grid_settings_sort.tr(),\n              width: 24,\n              height: 24,\n              iconPadding: const EdgeInsets.all(3),\n              hoverColor: AFThemeExtension.of(context).lightGreyHover,\n              icon: const FlowySvg(FlowySvgs.database_sort_s),\n              onPressed: () {\n                if (state.sorts.isEmpty) {\n                  _popoverController.show();\n                } else {\n                  widget.toggleExtension.toggle();\n                }\n              },\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget wrapPopover(Widget child) {\n    return AppFlowyPopover(\n      controller: _popoverController,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      constraints: BoxConstraints.loose(const Size(200, 300)),\n      offset: const Offset(0, 8),\n      triggerActions: PopoverTriggerFlags.none,\n      popupBuilder: (popoverContext) {\n        return BlocProvider.value(\n          value: context.read<SortEditorBloc>(),\n          child: CreateDatabaseViewSortList(\n            onTap: () {\n              if (!widget.toggleExtension.isToggled) {\n                widget.toggleExtension.toggle();\n              }\n              _popoverController.close();\n            },\n          ),\n        );\n      },\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/view_database_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/icon_button.dart';\nimport 'package:flutter/material.dart';\n\nclass ViewDatabaseButton extends StatelessWidget {\n  const ViewDatabaseButton({super.key, required this.view});\n\n  final ViewPB view;\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      cursor: SystemMouseCursors.click,\n      child: FlowyIconButton(\n        tooltipText: LocaleKeys.grid_rowPage_viewDatabase.tr(),\n        width: 24,\n        height: 24,\n        iconPadding: const EdgeInsets.all(3),\n        icon: const FlowySvg(FlowySvgs.database_fullscreen_s),\n        onPressed: () {\n          getIt<TabsBloc>().add(\n            TabsEvent.openPlugin(\n              plugin: view.plugin(),\n              view: view,\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/setting_menu.dart",
    "content": "import 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/grid/application/grid_accessory_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_menu.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_menu.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\n\nclass DatabaseViewSettingExtension extends StatelessWidget {\n  const DatabaseViewSettingExtension({\n    super.key,\n    required this.viewId,\n    required this.databaseController,\n    required this.toggleExtension,\n  });\n\n  final String viewId;\n  final DatabaseController databaseController;\n  final ToggleExtensionNotifier toggleExtension;\n\n  @override\n  Widget build(BuildContext context) {\n    return ChangeNotifierProvider.value(\n      value: toggleExtension,\n      child: Consumer<ToggleExtensionNotifier>(\n        builder: (context, value, child) {\n          if (value.isToggled) {\n            return BlocProvider(\n              create: (context) =>\n                  DatabaseViewSettingExtensionBloc(viewId: viewId),\n              child: _DatabaseViewSettingContent(\n                fieldController: databaseController.fieldController,\n              ),\n            );\n          } else {\n            return const SizedBox.shrink();\n          }\n        },\n      ),\n    );\n  }\n}\n\nclass _DatabaseViewSettingContent extends StatelessWidget {\n  const _DatabaseViewSettingContent({required this.fieldController});\n\n  final FieldController fieldController;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<DatabaseViewSettingExtensionBloc,\n        DatabaseViewSettingExtensionState>(\n      builder: (context, state) {\n        return DecoratedBox(\n          decoration: BoxDecoration(\n            border: Border(\n              bottom: BorderSide(\n                color: Theme.of(context).dividerColor,\n              ),\n            ),\n          ),\n          child: Padding(\n            padding: const EdgeInsets.symmetric(vertical: 8.0),\n            child: Row(\n              children: [\n                SortMenu(fieldController: fieldController),\n                const HSpace(6),\n                Expanded(\n                  child: FilterMenu(fieldController: fieldController),\n                ),\n              ],\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_add_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/extension.dart';\nimport 'package:flutter/material.dart';\n\nclass AddDatabaseViewButton extends StatefulWidget {\n  const AddDatabaseViewButton({super.key, required this.onTap});\n\n  final Function(DatabaseLayoutPB) onTap;\n\n  @override\n  State<AddDatabaseViewButton> createState() => _AddDatabaseViewButtonState();\n}\n\nclass _AddDatabaseViewButtonState extends State<AddDatabaseViewButton> {\n  final popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      controller: popoverController,\n      constraints: BoxConstraints.loose(const Size(200, 400)),\n      direction: PopoverDirection.bottomWithLeftAligned,\n      offset: const Offset(0, 8),\n      margin: EdgeInsets.zero,\n      triggerActions: PopoverTriggerFlags.none,\n      child: Padding(\n        padding: const EdgeInsetsDirectional.only(\n          top: 2.0,\n          bottom: 7.0,\n          start: 6.0,\n        ),\n        child: FlowyIconButton(\n          width: 26,\n          hoverColor: AFThemeExtension.of(context).greyHover,\n          onPressed: () => popoverController.show(),\n          radius: Corners.s4Border,\n          icon: FlowySvg(\n            FlowySvgs.add_s,\n            color: Theme.of(context).hintColor,\n          ),\n          iconColorOnHover: Theme.of(context).colorScheme.onSurface,\n        ),\n      ),\n      popupBuilder: (BuildContext context) {\n        return TabBarAddButtonAction(\n          onTap: (action) {\n            popoverController.close();\n            widget.onTap(action);\n          },\n        );\n      },\n    );\n  }\n}\n\nclass TabBarAddButtonAction extends StatelessWidget {\n  const TabBarAddButtonAction({super.key, required this.onTap});\n\n  final Function(DatabaseLayoutPB) onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    final cells = DatabaseLayoutPB.values.map((layout) {\n      return TabBarAddButtonActionCell(\n        action: layout,\n        onTap: onTap,\n      );\n    }).toList();\n\n    return ListView.separated(\n      shrinkWrap: true,\n      itemCount: cells.length,\n      itemBuilder: (BuildContext context, int index) => cells[index],\n      separatorBuilder: (BuildContext context, int index) =>\n          VSpace(GridSize.typeOptionSeparatorHeight),\n      padding: const EdgeInsets.symmetric(vertical: 4.0),\n    );\n  }\n}\n\nclass TabBarAddButtonActionCell extends StatelessWidget {\n  const TabBarAddButtonActionCell({\n    super.key,\n    required this.action,\n    required this.onTap,\n  });\n\n  final DatabaseLayoutPB action;\n  final void Function(DatabaseLayoutPB) onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        text: FlowyText(\n          '${LocaleKeys.grid_createView.tr()} ${action.layoutName}',\n          color: AFThemeExtension.of(context).textColor,\n        ),\n        leftIcon: FlowySvg(\n          action.icon,\n          color: Theme.of(context).iconTheme.color,\n        ),\n        onTap: () => onTap(action),\n      ).padding(horizontal: 6.0),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\n\nimport 'tab_bar_add_button.dart';\n\nclass TabBarHeader extends StatelessWidget {\n  const TabBarHeader({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 35,\n      child: Stack(\n        children: [\n          Positioned(\n            bottom: 0,\n            left: 0,\n            right: 0,\n            child: Divider(\n              color: AFThemeExtension.of(context).borderColor,\n              height: 1,\n              thickness: 1,\n            ),\n          ),\n          Row(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              const Expanded(\n                child: DatabaseTabBar(),\n              ),\n              Flexible(\n                child: BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(\n                  builder: (context, state) {\n                    return Padding(\n                      padding: const EdgeInsets.only(top: 6.0),\n                      child: pageSettingBarFromState(context, state),\n                    );\n                  },\n                ),\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget pageSettingBarFromState(\n    BuildContext context,\n    DatabaseTabBarState state,\n  ) {\n    if (state.tabBars.length < state.selectedIndex) {\n      return const SizedBox.shrink();\n    }\n    final tabBar = state.tabBars[state.selectedIndex];\n    final controller =\n        state.tabBarControllerByViewId[tabBar.viewId]!.controller;\n    return tabBar.builder.settingBar(context, controller);\n  }\n}\n\nclass DatabaseTabBar extends StatefulWidget {\n  const DatabaseTabBar({super.key});\n\n  @override\n  State<DatabaseTabBar> createState() => _DatabaseTabBarState();\n}\n\nclass _DatabaseTabBarState extends State<DatabaseTabBar> {\n  final _scrollController = ScrollController();\n\n  @override\n  void dispose() {\n    _scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(\n      builder: (context, state) {\n        return ListView.separated(\n          controller: _scrollController,\n          scrollDirection: Axis.horizontal,\n          shrinkWrap: true,\n          itemCount: state.tabBars.length + 1,\n          itemBuilder: (context, index) => index == state.tabBars.length\n              ? AddDatabaseViewButton(\n                  onTap: (layoutType) {\n                    context\n                        .read<DatabaseTabBarBloc>()\n                        .add(DatabaseTabBarEvent.createView(layoutType, null));\n                  },\n                )\n              : DatabaseTabBarItem(\n                  key: ValueKey(state.tabBars[index].viewId),\n                  view: state.tabBars[index].view,\n                  isSelected: state.selectedIndex == index,\n                  onTap: (selectedView) {\n                    context\n                        .read<DatabaseTabBarBloc>()\n                        .add(DatabaseTabBarEvent.selectView(selectedView.id));\n                  },\n                ),\n          separatorBuilder: (context, index) => VerticalDivider(\n            width: 1.0,\n            thickness: 1.0,\n            indent: 8,\n            endIndent: 13,\n            color: Theme.of(context).dividerColor,\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass DatabaseTabBarItem extends StatelessWidget {\n  const DatabaseTabBarItem({\n    super.key,\n    required this.view,\n    required this.isSelected,\n    required this.onTap,\n  });\n\n  final ViewPB view;\n  final bool isSelected;\n  final Function(ViewPB) onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return ConstrainedBox(\n      constraints: const BoxConstraints(maxWidth: 160),\n      child: Stack(\n        children: [\n          Padding(\n            padding: const EdgeInsets.symmetric(vertical: 2),\n            child: SizedBox(\n              height: 26,\n              child: TabBarItemButton(\n                view: view,\n                isSelected: isSelected,\n                onTap: () => onTap(view),\n              ),\n            ),\n          ),\n          if (isSelected)\n            Positioned(\n              bottom: 0,\n              left: 0,\n              right: 0,\n              child: Divider(\n                height: 2,\n                thickness: 2,\n                color: Theme.of(context).colorScheme.primary,\n              ),\n            ),\n        ],\n      ),\n    );\n  }\n}\n\nclass TabBarItemButton extends StatefulWidget {\n  const TabBarItemButton({\n    super.key,\n    required this.view,\n    required this.isSelected,\n    required this.onTap,\n  });\n\n  final ViewPB view;\n  final bool isSelected;\n  final VoidCallback onTap;\n\n  @override\n  State<TabBarItemButton> createState() => _TabBarItemButtonState();\n}\n\nclass _TabBarItemButtonState extends State<TabBarItemButton> {\n  final menuController = PopoverController();\n  final iconController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    Color? color;\n    if (!widget.isSelected) {\n      color = Theme.of(context).hintColor;\n    }\n    if (Theme.of(context).brightness == Brightness.dark) {\n      color = null;\n    }\n    return AppFlowyPopover(\n      controller: menuController,\n      constraints: const BoxConstraints(\n        minWidth: 120,\n        maxWidth: 460,\n        maxHeight: 300,\n      ),\n      direction: PopoverDirection.bottomWithCenterAligned,\n      clickHandler: PopoverClickHandler.gestureDetector,\n      popupBuilder: (_) {\n        return IntrinsicHeight(\n          child: IntrinsicWidth(\n            child: Column(\n              children: [\n                ActionCellWidget(\n                  action: TabBarViewAction.rename,\n                  itemHeight: ActionListSizes.itemHeight,\n                  onSelected: (action) {\n                    showAFTextFieldDialog(\n                      context: context,\n                      title: LocaleKeys.menuAppHeader_renameDialog.tr(),\n                      initialValue: widget.view.nameOrDefault,\n                      onConfirm: (newValue) {\n                        context.read<DatabaseTabBarBloc>().add(\n                              DatabaseTabBarEvent.renameView(\n                                widget.view.id,\n                                newValue,\n                              ),\n                            );\n                      },\n                    );\n                    menuController.close();\n                  },\n                ),\n                AppFlowyPopover(\n                  controller: iconController,\n                  direction: PopoverDirection.rightWithCenterAligned,\n                  constraints: BoxConstraints.loose(const Size(364, 356)),\n                  margin: const EdgeInsets.all(0),\n                  child: ActionCellWidget(\n                    action: TabBarViewAction.changeIcon,\n                    itemHeight: ActionListSizes.itemHeight,\n                    onSelected: (action) {\n                      iconController.show();\n                    },\n                  ),\n                  popupBuilder: (context) {\n                    return FlowyIconEmojiPicker(\n                      tabs: const [PickerTabType.icon],\n                      enableBackgroundColorSelection: false,\n                      onSelectedEmoji: (r) {\n                        ViewBackendService.updateViewIcon(\n                          view: widget.view,\n                          viewIcon: r.data,\n                        );\n                        if (!r.keepOpen) {\n                          iconController.close();\n                          menuController.close();\n                        }\n                      },\n                    );\n                  },\n                ),\n                ActionCellWidget(\n                  action: TabBarViewAction.delete,\n                  itemHeight: ActionListSizes.itemHeight,\n                  onSelected: (action) {\n                    NavigatorAlertDialog(\n                      title: LocaleKeys.grid_deleteView.tr(),\n                      confirm: () {\n                        context.read<DatabaseTabBarBloc>().add(\n                              DatabaseTabBarEvent.deleteView(widget.view.id),\n                            );\n                      },\n                    ).show(context);\n                    menuController.close();\n                  },\n                ),\n              ],\n            ),\n          ),\n        );\n      },\n      child: IntrinsicWidth(\n        child: FlowyTooltip(\n          message: widget.view.nameOrDefault,\n          preferBelow: false,\n          child: FlowyButton(\n            radius: Corners.s6Border,\n            hoverColor: AFThemeExtension.of(context).greyHover,\n            onTap: () {\n              if (widget.isSelected) menuController.show();\n              widget.onTap.call();\n            },\n            margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),\n            onSecondaryTap: () {\n              menuController.show();\n            },\n            leftIcon: _buildViewIcon(),\n            text: FlowyText(\n              widget.view.nameOrDefault,\n              lineHeight: 1.0,\n              textAlign: TextAlign.center,\n              overflow: TextOverflow.ellipsis,\n              color: color,\n              fontWeight: widget.isSelected ? FontWeight.w500 : FontWeight.w400,\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildViewIcon() {\n    final iconData = widget.view.icon.toEmojiIconData();\n    Widget icon;\n    if (iconData.isEmpty || iconData.type != FlowyIconType.icon) {\n      icon = widget.view.defaultIcon();\n    } else {\n      icon = RawEmojiIconWidget(\n        emoji: iconData,\n        emojiSize: 14.0,\n        enableColor: false,\n      );\n    }\n    final isReference =\n        Provider.of<ReferenceState?>(context)?.isReference ?? false;\n    final iconWidget = Opacity(opacity: 0.6, child: icon);\n    return isReference\n        ? Stack(\n            children: [\n              iconWidget,\n              const Positioned(\n                right: 0,\n                bottom: 0,\n                child: FlowySvg(\n                  FlowySvgs.referenced_page_s,\n                  blendMode: BlendMode.dstIn,\n                ),\n              ),\n            ],\n          )\n        : iconWidget;\n  }\n}\n\nenum TabBarViewAction implements ActionCell {\n  rename,\n  changeIcon,\n  delete;\n\n  @override\n  String get name {\n    switch (this) {\n      case TabBarViewAction.rename:\n        return LocaleKeys.disclosureAction_rename.tr();\n      case TabBarViewAction.changeIcon:\n        return LocaleKeys.disclosureAction_changeIcon.tr();\n      case TabBarViewAction.delete:\n        return LocaleKeys.disclosureAction_delete.tr();\n    }\n  }\n\n  Widget icon(Color iconColor) {\n    switch (this) {\n      case TabBarViewAction.rename:\n        return const FlowySvg(FlowySvgs.edit_s);\n      case TabBarViewAction.changeIcon:\n        return const FlowySvg(FlowySvgs.change_icon_s);\n      case TabBarViewAction.delete:\n        return const FlowySvg(FlowySvgs.delete_s);\n    }\n  }\n\n  @override\n  Widget? leftIcon(Color iconColor) => icon(iconColor);\n\n  @override\n  Widget? rightIcon(Color iconColor) => null;\n\n  @override\n  Color? textColor(BuildContext context) {\n    return null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/database/view/database_view_list.dart';\nimport 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/mobile_database_controls.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MobileTabBarHeader extends StatelessWidget {\n  const MobileTabBarHeader({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: EdgeInsets.only(\n        left: GridSize.horizontalHeaderPadding,\n        top: 14.0,\n        right: GridSize.horizontalHeaderPadding - 5.0,\n      ),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: [\n          const _DatabaseViewSelectorButton(),\n          const Spacer(),\n          BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(\n            builder: (context, state) {\n              final currentView = state.tabBars.firstWhereIndexedOrNull(\n                (index, tabBar) => index == state.selectedIndex,\n              );\n\n              if (currentView == null) {\n                return const SizedBox.shrink();\n              }\n\n              return MobileDatabaseControls(\n                controller: state\n                    .tabBarControllerByViewId[currentView.viewId]!.controller,\n                features: switch (currentView.layout) {\n                  ViewLayoutPB.Board || ViewLayoutPB.Calendar => [\n                      MobileDatabaseControlFeatures.filter,\n                    ],\n                  ViewLayoutPB.Grid => [\n                      MobileDatabaseControlFeatures.sort,\n                      MobileDatabaseControlFeatures.filter,\n                    ],\n                  _ => [],\n                },\n              );\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _DatabaseViewSelectorButton extends StatelessWidget {\n  const _DatabaseViewSelectorButton();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(\n      builder: (context, state) {\n        final tabBar = state.tabBars.firstWhereIndexedOrNull(\n          (index, tabBar) => index == state.selectedIndex,\n        );\n\n        if (tabBar == null) {\n          return const SizedBox.shrink();\n        }\n\n        return TextButton(\n          style: ButtonStyle(\n            padding: const WidgetStatePropertyAll(\n              EdgeInsets.fromLTRB(12, 8, 8, 8),\n            ),\n            maximumSize: const WidgetStatePropertyAll(Size(200, 48)),\n            minimumSize: const WidgetStatePropertyAll(Size(48, 0)),\n            shape: const WidgetStatePropertyAll(\n              RoundedRectangleBorder(\n                borderRadius: BorderRadius.all(Radius.circular(12)),\n              ),\n            ),\n            backgroundColor: WidgetStatePropertyAll(\n              Theme.of(context).brightness == Brightness.light\n                  ? const Color(0x0F212729)\n                  : const Color(0x0FFFFFFF),\n            ),\n            overlayColor: WidgetStatePropertyAll(\n              Theme.of(context).colorScheme.secondary,\n            ),\n          ),\n          child: Row(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              _buildViewIconButton(context, tabBar.view),\n              const HSpace(6),\n              Flexible(\n                child: FlowyText.medium(\n                  tabBar.view.nameOrDefault,\n                  fontSize: 14,\n                  overflow: TextOverflow.ellipsis,\n                ),\n              ),\n              const HSpace(8),\n              const FlowySvg(\n                FlowySvgs.arrow_tight_s,\n                size: Size.square(10),\n              ),\n            ],\n          ),\n          onPressed: () {\n            showTransitionMobileBottomSheet(\n              context,\n              showDivider: false,\n              builder: (_) {\n                return MultiBlocProvider(\n                  providers: [\n                    BlocProvider<ViewBloc>.value(\n                      value: context.read<ViewBloc>(),\n                    ),\n                    BlocProvider<DatabaseTabBarBloc>.value(\n                      value: context.read<DatabaseTabBarBloc>(),\n                    ),\n                  ],\n                  child: const MobileDatabaseViewList(),\n                );\n              },\n            );\n          },\n        );\n      },\n    );\n  }\n\n  Widget _buildViewIconButton(BuildContext context, ViewPB view) {\n    final iconData = view.icon.toEmojiIconData();\n    if (iconData.isEmpty || iconData.type != FlowyIconType.icon) {\n      return SizedBox.square(\n        dimension: 16.0,\n        child: view.defaultIcon(),\n      );\n    }\n    return RawEmojiIconWidget(\n      emoji: iconData,\n      emojiSize: 16,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:math';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/document/presentation/compact_mode_event.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart';\nimport 'package:appflowy/plugins/shared/share/share_button.dart';\nimport 'package:appflowy/plugins/util.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_stack.dart';\nimport 'package:appflowy/workspace/presentation/widgets/favorite_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart';\nimport 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';\nimport 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'desktop/tab_bar_header.dart';\nimport 'mobile/mobile_tab_bar_header.dart';\n\nabstract class DatabaseTabBarItemBuilder {\n  const DatabaseTabBarItemBuilder();\n\n  /// Returns the content of the tab bar item. The content is shown when the tab\n  /// bar item is selected. It can be any kind of database view.\n  Widget content(\n    BuildContext context,\n    ViewPB view,\n    DatabaseController controller,\n    bool shrinkWrap,\n    String? initialRowId,\n  );\n\n  /// Returns the setting bar of the tab bar item. The setting bar is shown on the\n  /// top right conner when the tab bar item is selected.\n  Widget settingBar(\n    BuildContext context,\n    DatabaseController controller,\n  );\n\n  Widget settingBarExtension(\n    BuildContext context,\n    DatabaseController controller,\n  );\n\n  /// Should be called in case a builder has resources it\n  /// needs to dispose of.\n  ///\n  // If we add any logic in this method, add @mustCallSuper !\n  void dispose() {}\n}\n\nclass DatabaseTabBarView extends StatefulWidget {\n  const DatabaseTabBarView({\n    super.key,\n    required this.view,\n    required this.shrinkWrap,\n    required this.showActions,\n    this.initialRowId,\n    this.actionBuilder,\n    this.node,\n  });\n\n  final ViewPB view;\n  final bool shrinkWrap;\n  final BlockComponentActionBuilder? actionBuilder;\n  final bool showActions;\n  final Node? node;\n\n  /// Used to open a Row on plugin load\n  ///\n  final String? initialRowId;\n\n  @override\n  State<DatabaseTabBarView> createState() => _DatabaseTabBarViewState();\n}\n\nclass _DatabaseTabBarViewState extends State<DatabaseTabBarView> {\n  bool enableCompactMode = false;\n  bool initialed = false;\n  StreamSubscription<CompactModeEvent>? compactModeSubscription;\n\n  String get compactModeId => widget.node?.id ?? widget.view.id;\n\n  @override\n  void initState() {\n    super.initState();\n    if (widget.node != null) {\n      enableCompactMode =\n          widget.node!.attributes[DatabaseBlockKeys.enableCompactMode] ?? false;\n      setState(() {\n        initialed = true;\n      });\n    } else {\n      fetchLocalCompactMode(compactModeId).then((v) {\n        if (mounted) {\n          setState(() {\n            enableCompactMode = v;\n            initialed = true;\n          });\n        }\n      });\n      compactModeSubscription =\n          compactModeEventBus.on<CompactModeEvent>().listen((event) {\n        if (event.id != widget.view.id) return;\n        updateLocalCompactMode(event.enable);\n      });\n    }\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n    compactModeSubscription?.cancel();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (!initialed) return Center(child: CircularProgressIndicator());\n    return LayoutBuilder(\n      builder: (context, constraints) {\n        final maxWidth = constraints.maxWidth;\n        final editorState = context.read<EditorState?>();\n        final maxDocWidth = editorState?.editorStyle.maxWidth ?? maxWidth;\n        final paddingLeft = max(0, maxWidth - maxDocWidth) / 2;\n        return MultiBlocProvider(\n          providers: [\n            BlocProvider<DatabaseTabBarBloc>(\n              create: (_) => DatabaseTabBarBloc(\n                view: widget.view,\n                compactModeId: compactModeId,\n                enableCompactMode: enableCompactMode,\n              )..add(const DatabaseTabBarEvent.initial()),\n            ),\n            BlocProvider<ViewBloc>(\n              create: (_) => ViewBloc(view: widget.view)\n                ..add(\n                  const ViewEvent.initial(),\n                ),\n            ),\n          ],\n          child: BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(\n            builder: (innerContext, state) {\n              final layout = state.tabBars[state.selectedIndex].layout;\n              final isCalendar = layout == ViewLayoutPB.Calendar;\n              final databseBuilderSize =\n                  context.read<DatabasePluginWidgetBuilderSize>();\n              final horizontalPadding = databseBuilderSize.horizontalPadding;\n              final showActionWrapper = widget.showActions &&\n                  widget.actionBuilder != null &&\n                  widget.node != null;\n              final Widget child = Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  if (UniversalPlatform.isMobile) const VSpace(12),\n                  ValueListenableBuilder<bool>(\n                    valueListenable: state\n                        .tabBarControllerByViewId[state.parentView.id]!\n                        .controller\n                        .isLoading,\n                    builder: (_, value, ___) {\n                      if (value) {\n                        return const SizedBox.shrink();\n                      }\n\n                      Widget child = UniversalPlatform.isDesktop\n                          ? const TabBarHeader()\n                          : const MobileTabBarHeader();\n\n                      if (innerContext.watch<ViewBloc>().state.view.isLocked) {\n                        child = IgnorePointer(\n                          child: child,\n                        );\n                      }\n\n                      if (showActionWrapper) {\n                        child = BlockComponentActionWrapper(\n                          node: widget.node!,\n                          actionBuilder: widget.actionBuilder!,\n                          child: Padding(\n                            padding: EdgeInsets.only(right: horizontalPadding),\n                            child: child,\n                          ),\n                        );\n                      }\n\n                      if (UniversalPlatform.isDesktop) {\n                        child = Container(\n                          padding: EdgeInsets.fromLTRB(\n                            horizontalPadding + paddingLeft,\n                            0,\n                            horizontalPadding,\n                            0,\n                          ),\n                          child: child,\n                        );\n                      }\n\n                      return child;\n                    },\n                  ),\n                  pageSettingBarExtensionFromState(context, state),\n                  wrapContent(\n                    layout: layout,\n                    child: Padding(\n                      padding:\n                          (isCalendar && widget.shrinkWrap || showActionWrapper)\n                              ? EdgeInsets.only(left: 42 - horizontalPadding)\n                              : EdgeInsets.zero,\n                      child: Provider(\n                        create: (_) => DatabasePluginWidgetBuilderSize(\n                          horizontalPadding: horizontalPadding,\n                          paddingLeftWithMaxDocumentWidth: paddingLeft,\n                          verticalPadding: databseBuilderSize.verticalPadding,\n                        ),\n                        child: pageContentFromState(context, state),\n                      ),\n                    ),\n                  ),\n                ],\n              );\n\n              return child;\n            },\n          ),\n        );\n      },\n    );\n  }\n\n  Future<bool> fetchLocalCompactMode(String compactModeId) async {\n    Set<String> compactModeIds = {};\n    try {\n      final localIds = await getIt<KeyValueStorage>().get(\n        KVKeys.compactModeIds,\n      );\n      final List<dynamic> decodedList = jsonDecode(localIds ?? '');\n      compactModeIds = Set.from(decodedList.map((item) => item as String));\n    } catch (e) {\n      Log.warn('fetch local compact mode from id :$compactModeId failed', e);\n    }\n    return compactModeIds.contains(compactModeId);\n  }\n\n  Future<void> updateLocalCompactMode(bool enableCompactMode) async {\n    Set<String> compactModeIds = {};\n    try {\n      final localIds = await getIt<KeyValueStorage>().get(\n        KVKeys.compactModeIds,\n      );\n      final List<dynamic> decodedList = jsonDecode(localIds ?? '');\n      compactModeIds = Set.from(decodedList.map((item) => item as String));\n    } catch (e) {\n      Log.warn('get compact mode ids failed', e);\n    }\n    if (enableCompactMode) {\n      compactModeIds.add(compactModeId);\n    } else {\n      compactModeIds.remove(compactModeId);\n    }\n    await getIt<KeyValueStorage>().set(\n      KVKeys.compactModeIds,\n      jsonEncode(compactModeIds.toList()),\n    );\n  }\n\n  Widget wrapContent({required ViewLayoutPB layout, required Widget child}) {\n    if (widget.shrinkWrap) {\n      if (layout.shrinkWrappable) {\n        return child;\n      }\n\n      return SizedBox(\n        height: layout.pluginHeight,\n        child: child,\n      );\n    }\n\n    return Expanded(child: child);\n  }\n\n  Widget pageContentFromState(BuildContext context, DatabaseTabBarState state) {\n    final tab = state.tabBars[state.selectedIndex];\n    final controller = state.tabBarControllerByViewId[tab.viewId]!.controller;\n\n    return tab.builder.content(\n      context,\n      tab.view,\n      controller,\n      widget.shrinkWrap,\n      widget.initialRowId,\n    );\n  }\n\n  Widget pageSettingBarExtensionFromState(\n    BuildContext context,\n    DatabaseTabBarState state,\n  ) {\n    if (state.tabBars.length < state.selectedIndex) {\n      return const SizedBox.shrink();\n    }\n    final tabBar = state.tabBars[state.selectedIndex];\n    final controller =\n        state.tabBarControllerByViewId[tabBar.viewId]!.controller;\n    return Padding(\n      padding: EdgeInsets.symmetric(\n        horizontal:\n            context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding,\n      ),\n      child: tabBar.builder.settingBarExtension(\n        context,\n        controller,\n      ),\n    );\n  }\n}\n\nclass DatabaseTabBarViewPlugin extends Plugin {\n  DatabaseTabBarViewPlugin({\n    required ViewPB view,\n    required PluginType pluginType,\n    this.initialRowId,\n  })  : _pluginType = pluginType,\n        notifier = ViewPluginNotifier(view: view);\n\n  @override\n  final ViewPluginNotifier notifier;\n\n  final PluginType _pluginType;\n  late final ViewInfoBloc _viewInfoBloc;\n  late final PageAccessLevelBloc _pageAccessLevelBloc;\n\n  /// Used to open a Row on plugin load\n  ///\n  final String? initialRowId;\n\n  @override\n  PluginWidgetBuilder get widgetBuilder => DatabasePluginWidgetBuilder(\n        bloc: _viewInfoBloc,\n        pageAccessLevelBloc: _pageAccessLevelBloc,\n        notifier: notifier,\n        initialRowId: initialRowId,\n      );\n\n  @override\n  PluginId get id => notifier.view.id;\n\n  @override\n  PluginType get pluginType => _pluginType;\n\n  @override\n  void init() {\n    _viewInfoBloc = ViewInfoBloc(view: notifier.view)\n      ..add(const ViewInfoEvent.started());\n    _pageAccessLevelBloc = PageAccessLevelBloc(view: notifier.view)\n      ..add(const PageAccessLevelEvent.initial());\n  }\n\n  @override\n  void dispose() {\n    _viewInfoBloc.close();\n    _pageAccessLevelBloc.close();\n    notifier.dispose();\n  }\n}\n\nconst kDatabasePluginWidgetBuilderHorizontalPadding = 'horizontal_padding';\nconst kDatabasePluginWidgetBuilderShowActions = 'show_actions';\nconst kDatabasePluginWidgetBuilderActionBuilder = 'action_builder';\nconst kDatabasePluginWidgetBuilderNode = 'node';\n\nclass DatabasePluginWidgetBuilderSize {\n  const DatabasePluginWidgetBuilderSize({\n    required this.horizontalPadding,\n    this.verticalPadding = 16.0,\n    this.paddingLeftWithMaxDocumentWidth = 0.0,\n  });\n\n  final double horizontalPadding;\n  final double verticalPadding;\n  final double paddingLeftWithMaxDocumentWidth;\n\n  double get paddingLeft => paddingLeftWithMaxDocumentWidth + horizontalPadding;\n}\n\nclass DatabasePluginWidgetBuilder extends PluginWidgetBuilder {\n  DatabasePluginWidgetBuilder({\n    required this.bloc,\n    required this.pageAccessLevelBloc,\n    required this.notifier,\n    this.initialRowId,\n  });\n\n  final ViewInfoBloc bloc;\n  final PageAccessLevelBloc pageAccessLevelBloc;\n  final ViewPluginNotifier notifier;\n\n  /// Used to open a Row on plugin load\n  ///\n  final String? initialRowId;\n\n  @override\n  String? get viewName => notifier.view.nameOrDefault;\n\n  @override\n  Widget get leftBarItem {\n    return BlocProvider.value(\n      value: pageAccessLevelBloc,\n      child: ViewTitleBar(key: ValueKey(notifier.view.id), view: notifier.view),\n    );\n  }\n\n  @override\n  Widget tabBarItem(String pluginId, [bool shortForm = false]) =>\n      ViewTabBarItem(view: notifier.view, shortForm: shortForm);\n\n  @override\n  Widget buildWidget({\n    required PluginContext context,\n    required bool shrinkWrap,\n    Map<String, dynamic>? data,\n  }) {\n    notifier.isDeleted.addListener(() {\n      final deletedView = notifier.isDeleted.value;\n      if (deletedView != null && deletedView.hasIndex()) {\n        context.onDeleted?.call(notifier.view, deletedView.index);\n      }\n    });\n\n    final horizontalPadding =\n        data?[kDatabasePluginWidgetBuilderHorizontalPadding] as double? ??\n            GridSize.horizontalHeaderPadding + 40;\n    final BlockComponentActionBuilder? actionBuilder =\n        data?[kDatabasePluginWidgetBuilderActionBuilder];\n    final bool showActions =\n        data?[kDatabasePluginWidgetBuilderShowActions] ?? false;\n    final Node? node = data?[kDatabasePluginWidgetBuilderNode];\n\n    return Provider(\n      create: (context) => DatabasePluginWidgetBuilderSize(\n        horizontalPadding: horizontalPadding,\n      ),\n      child: DatabaseTabBarView(\n        key: ValueKey(notifier.view.id),\n        view: notifier.view,\n        shrinkWrap: shrinkWrap,\n        initialRowId: initialRowId,\n        actionBuilder: actionBuilder,\n        showActions: showActions,\n        node: node,\n      ),\n    );\n  }\n\n  @override\n  List<NavigationItem> get navigationItems => [this];\n\n  @override\n  Widget? get rightBarItem {\n    final view = notifier.view;\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider<ViewInfoBloc>.value(\n          value: bloc,\n        ),\n        BlocProvider<PageAccessLevelBloc>.value(\n          value: pageAccessLevelBloc,\n        ),\n      ],\n      child: Row(\n        children: [\n          ShareButton(key: ValueKey(view.id), view: view),\n          const HSpace(10),\n          ViewFavoriteButton(view: view),\n          const HSpace(4),\n          MoreViewActions(view: view),\n        ],\n      ),\n    );\n  }\n\n  @override\n  EdgeInsets get contentPadding => const EdgeInsets.only(top: 28);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/mobile/presentation/database/card/card.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_cache.dart';\nimport 'package:appflowy/plugins/database/application/row/row_controller.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/row/action.dart';\nimport 'package:appflowy/shared/af_image.dart';\nimport 'package:appflowy/shared/flowy_gradient_colors.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../cell/card_cell_builder.dart';\nimport '../cell/card_cell_skeleton/card_cell.dart';\nimport 'card_bloc.dart';\nimport 'container/accessory.dart';\nimport 'container/card_container.dart';\n\n/// Edit a database row with card style widget\nclass RowCard extends StatefulWidget {\n  const RowCard({\n    super.key,\n    required this.fieldController,\n    required this.rowMeta,\n    required this.viewId,\n    required this.isEditing,\n    required this.rowCache,\n    required this.cellBuilder,\n    required this.onTap,\n    required this.onStartEditing,\n    required this.onEndEditing,\n    required this.styleConfiguration,\n    this.onShiftTap,\n    this.groupingFieldId,\n    this.groupId,\n    required this.userProfile,\n    this.isCompact = false,\n  });\n\n  final FieldController fieldController;\n  final RowMetaPB rowMeta;\n  final String viewId;\n  final String? groupingFieldId;\n  final String? groupId;\n\n  final bool isEditing;\n  final RowCache rowCache;\n\n  /// The [CardCellBuilder] is used to build the card cells.\n  final CardCellBuilder cellBuilder;\n\n  /// Called when the user taps on the card.\n  final void Function(BuildContext context) onTap;\n\n  final void Function(BuildContext context)? onShiftTap;\n\n  /// Called when the user starts editing the card.\n  final VoidCallback onStartEditing;\n\n  /// Called when the user ends editing the card.\n  final VoidCallback onEndEditing;\n\n  final RowCardStyleConfiguration styleConfiguration;\n\n  /// Specifically the token is used to handle requests to retrieve images\n  /// from cloud storage, such as the card cover.\n  final UserProfilePB? userProfile;\n\n  /// Whether the card is in a narrow space.\n  /// This is used to determine eg. the Cover height.\n  final bool isCompact;\n\n  @override\n  State<RowCard> createState() => _RowCardState();\n}\n\nclass _RowCardState extends State<RowCard> {\n  final popoverController = PopoverController();\n  late final CardBloc _cardBloc;\n\n  @override\n  void initState() {\n    super.initState();\n    final rowController = RowController(\n      viewId: widget.viewId,\n      rowMeta: widget.rowMeta,\n      rowCache: widget.rowCache,\n    );\n\n    _cardBloc = CardBloc(\n      fieldController: widget.fieldController,\n      viewId: widget.viewId,\n      groupFieldId: widget.groupingFieldId,\n      isEditing: widget.isEditing,\n      rowController: rowController,\n    )..add(const CardEvent.initial());\n  }\n\n  @override\n  void didUpdateWidget(covariant oldWidget) {\n    if (widget.isEditing != _cardBloc.state.isEditing) {\n      _cardBloc.add(CardEvent.setIsEditing(widget.isEditing));\n    }\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  void dispose() {\n    _cardBloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: _cardBloc,\n      child: BlocListener<CardBloc, CardState>(\n        listenWhen: (previous, current) =>\n            previous.isEditing != current.isEditing,\n        listener: (context, state) {\n          if (!state.isEditing) {\n            widget.onEndEditing();\n          }\n        },\n        child: UniversalPlatform.isMobile ? _mobile() : _desktop(),\n      ),\n    );\n  }\n\n  Widget _mobile() {\n    return BlocBuilder<CardBloc, CardState>(\n      builder: (context, state) {\n        return GestureDetector(\n          onTap: () => widget.onTap(context),\n          behavior: HitTestBehavior.opaque,\n          child: MobileCardContent(\n            userProfile: widget.userProfile,\n            rowMeta: state.rowMeta,\n            cellBuilder: widget.cellBuilder,\n            styleConfiguration: widget.styleConfiguration,\n            cells: state.cells,\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _desktop() {\n    final accessories = widget.styleConfiguration.showAccessory\n        ? const <CardAccessory>[\n            EditCardAccessory(),\n            MoreCardOptionsAccessory(),\n          ]\n        : null;\n    return AppFlowyPopover(\n      controller: popoverController,\n      triggerActions: PopoverTriggerFlags.none,\n      constraints: BoxConstraints.loose(const Size(140, 200)),\n      direction: PopoverDirection.rightWithCenterAligned,\n      popupBuilder: (_) => RowActionMenu.board(\n        viewId: _cardBloc.viewId,\n        rowId: _cardBloc.rowController.rowId,\n        groupId: widget.groupId,\n      ),\n      child: Builder(\n        builder: (context) {\n          return RowCardContainer(\n            buildAccessoryWhen: () =>\n                !context.watch<CardBloc>().state.isEditing,\n            accessories: accessories ?? [],\n            openAccessory: _handleOpenAccessory,\n            onTap: widget.onTap,\n            onShiftTap: widget.onShiftTap,\n            child: BlocBuilder<CardBloc, CardState>(\n              builder: (context, state) {\n                return _CardContent(\n                  rowMeta: state.rowMeta,\n                  cellBuilder: widget.cellBuilder,\n                  styleConfiguration: widget.styleConfiguration,\n                  cells: state.cells,\n                  userProfile: widget.userProfile,\n                  isCompact: widget.isCompact,\n                );\n              },\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  void _handleOpenAccessory(AccessoryType newAccessoryType) {\n    switch (newAccessoryType) {\n      case AccessoryType.edit:\n        widget.onStartEditing();\n        break;\n      case AccessoryType.more:\n        popoverController.show();\n        break;\n    }\n  }\n}\n\nclass _CardContent extends StatelessWidget {\n  const _CardContent({\n    required this.rowMeta,\n    required this.cellBuilder,\n    required this.cells,\n    required this.styleConfiguration,\n    this.userProfile,\n    this.isCompact = false,\n  });\n\n  final RowMetaPB rowMeta;\n  final CardCellBuilder cellBuilder;\n  final List<CellMeta> cells;\n  final RowCardStyleConfiguration styleConfiguration;\n  final UserProfilePB? userProfile;\n  final bool isCompact;\n\n  @override\n  Widget build(BuildContext context) {\n    final child = Column(\n      mainAxisAlignment: MainAxisAlignment.center,\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        CardCover(\n          cover: rowMeta.cover,\n          userProfile: userProfile,\n          isCompact: isCompact,\n        ),\n        Padding(\n          padding: styleConfiguration.cardPadding,\n          child: Column(\n            children: _makeCells(context, rowMeta, cells),\n          ),\n        ),\n      ],\n    );\n    return styleConfiguration.hoverStyle == null\n        ? child\n        : FlowyHover(\n            style: styleConfiguration.hoverStyle,\n            buildWhenOnHover: () => !context.read<CardBloc>().state.isEditing,\n            child: child,\n          );\n  }\n\n  List<Widget> _makeCells(\n    BuildContext context,\n    RowMetaPB rowMeta,\n    List<CellMeta> cells,\n  ) {\n    return cells\n        .mapIndexed(\n          (int index, CellMeta cellMeta) => _CardContentCell(\n            cellBuilder: cellBuilder,\n            cellMeta: cellMeta,\n            rowMeta: rowMeta,\n            isTitle: index == 0,\n            styleMap: styleConfiguration.cellStyleMap,\n          ),\n        )\n        .toList();\n  }\n}\n\nclass _CardContentCell extends StatefulWidget {\n  const _CardContentCell({\n    required this.cellBuilder,\n    required this.cellMeta,\n    required this.rowMeta,\n    required this.isTitle,\n    required this.styleMap,\n  });\n\n  final CellMeta cellMeta;\n  final RowMetaPB rowMeta;\n  final CardCellBuilder cellBuilder;\n  final CardCellStyleMap styleMap;\n  final bool isTitle;\n\n  @override\n  State<_CardContentCell> createState() => _CardContentCellState();\n}\n\nclass _CardContentCellState extends State<_CardContentCell> {\n  late final EditableCardNotifier? cellNotifier;\n\n  @override\n  void initState() {\n    super.initState();\n    cellNotifier = widget.isTitle ? EditableCardNotifier() : null;\n    cellNotifier?.isCellEditing.addListener(listener);\n  }\n\n  void listener() {\n    final isEditing = cellNotifier!.isCellEditing.value;\n    context.read<CardBloc>().add(CardEvent.setIsEditing(isEditing));\n  }\n\n  @override\n  void dispose() {\n    cellNotifier?.isCellEditing.removeListener(listener);\n    cellNotifier?.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocListener<CardBloc, CardState>(\n      listenWhen: (previous, current) =>\n          previous.isEditing != current.isEditing,\n      listener: (context, state) {\n        cellNotifier?.isCellEditing.value = state.isEditing;\n      },\n      child: widget.cellBuilder.build(\n        cellContext: widget.cellMeta.cellContext(),\n        styleMap: widget.styleMap,\n        cellNotifier: cellNotifier,\n        hasNotes: !widget.rowMeta.isDocumentEmpty,\n      ),\n    );\n  }\n}\n\nclass CardCover extends StatelessWidget {\n  const CardCover({\n    super.key,\n    this.cover,\n    this.userProfile,\n    this.isCompact = false,\n  });\n\n  final RowCoverPB? cover;\n  final UserProfilePB? userProfile;\n  final bool isCompact;\n\n  @override\n  Widget build(BuildContext context) {\n    if (cover == null ||\n        cover!.data.isEmpty ||\n        cover!.uploadType == FileUploadTypePB.CloudFile &&\n            userProfile == null) {\n      return const SizedBox.shrink();\n    }\n\n    return Container(\n      clipBehavior: Clip.antiAlias,\n      decoration: BoxDecoration(\n        borderRadius: const BorderRadius.only(\n          topLeft: Radius.circular(4),\n          topRight: Radius.circular(4),\n        ),\n        color: Theme.of(context).cardColor,\n      ),\n      child: Row(\n        children: [\n          Expanded(child: _renderCover(context, cover!)),\n        ],\n      ),\n    );\n  }\n\n  Widget _renderCover(BuildContext context, RowCoverPB cover) {\n    final height = isCompact ? 50.0 : 100.0;\n\n    if (cover.coverType == CoverTypePB.FileCover) {\n      return SizedBox(\n        height: height,\n        width: double.infinity,\n        child: AFImage(\n          url: cover.data,\n          uploadType: cover.uploadType,\n          userProfile: userProfile,\n        ),\n      );\n    }\n\n    if (cover.coverType == CoverTypePB.AssetCover) {\n      return SizedBox(\n        height: height,\n        width: double.infinity,\n        child: Image.asset(\n          PageStyleCoverImageType.builtInImagePath(cover.data),\n          fit: BoxFit.cover,\n        ),\n      );\n    }\n\n    if (cover.coverType == CoverTypePB.ColorCover) {\n      final color = FlowyTint.fromId(cover.data)?.color(context) ??\n          cover.data.tryToColor();\n      return Container(\n        height: height,\n        width: double.infinity,\n        color: color,\n      );\n    }\n\n    if (cover.coverType == CoverTypePB.GradientCover) {\n      return Container(\n        height: height,\n        width: double.infinity,\n        decoration: BoxDecoration(\n          gradient: FlowyGradientColor.fromId(cover.data).linear,\n        ),\n      );\n    }\n\n    return const SizedBox.shrink();\n  }\n}\n\nclass EditCardAccessory extends StatelessWidget with CardAccessory {\n  const EditCardAccessory({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(3.0),\n      child: FlowySvg(\n        FlowySvgs.edit_s,\n        color: Theme.of(context).hintColor,\n      ),\n    );\n  }\n\n  @override\n  AccessoryType get type => AccessoryType.edit;\n}\n\nclass MoreCardOptionsAccessory extends StatelessWidget with CardAccessory {\n  const MoreCardOptionsAccessory({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(3.0),\n      child: FlowySvg(\n        FlowySvgs.three_dots_s,\n        color: Theme.of(context).hintColor,\n      ),\n    );\n  }\n\n  @override\n  AccessoryType get type => AccessoryType.more;\n}\n\nclass RowCardStyleConfiguration {\n  const RowCardStyleConfiguration({\n    required this.cellStyleMap,\n    this.showAccessory = true,\n    this.cardPadding = const EdgeInsets.all(4),\n    this.hoverStyle,\n  });\n\n  final CardCellStyleMap cellStyleMap;\n  final bool showAccessory;\n  final EdgeInsets cardPadding;\n  final HoverStyle? hoverStyle;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/row/row_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter/foundation.dart';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_cache.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'card_bloc.freezed.dart';\n\nclass CardBloc extends Bloc<CardEvent, CardState> {\n  CardBloc({\n    required this.fieldController,\n    required this.groupFieldId,\n    required this.viewId,\n    required bool isEditing,\n    required this.rowController,\n  }) : super(\n          CardState.initial(\n            _makeCells(\n              fieldController,\n              groupFieldId,\n              rowController,\n            ),\n            isEditing,\n            rowController.rowMeta,\n          ),\n        ) {\n    rowController.initialize();\n    _dispatch();\n  }\n\n  final FieldController fieldController;\n  final String? groupFieldId;\n  final String viewId;\n  final RowController rowController;\n\n  VoidCallback? _rowCallback;\n\n  @override\n  Future<void> close() async {\n    if (_rowCallback != null) {\n      _rowCallback = null;\n    }\n    await rowController.dispose();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<CardEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            await _startListening();\n          },\n          didReceiveCells: (cells, reason) async {\n            emit(\n              state.copyWith(\n                cells: cells,\n                changeReason: reason,\n              ),\n            );\n          },\n          setIsEditing: (bool isEditing) {\n            if (isEditing != state.isEditing) {\n              emit(state.copyWith(isEditing: isEditing));\n            }\n          },\n          didUpdateRowMeta: (rowMeta) {\n            emit(state.copyWith(rowMeta: rowMeta));\n          },\n        );\n      },\n    );\n  }\n\n  Future<void> _startListening() async {\n    rowController.addListener(\n      onRowChanged: (cellMap, reason) {\n        if (!isClosed) {\n          final cells =\n              _makeCells(fieldController, groupFieldId, rowController);\n          add(CardEvent.didReceiveCells(cells, reason));\n        }\n      },\n      onMetaChanged: () {\n        if (!isClosed) {\n          add(CardEvent.didUpdateRowMeta(rowController.rowMeta));\n        }\n      },\n    );\n  }\n}\n\nList<CellMeta> _makeCells(\n  FieldController fieldController,\n  String? groupFieldId,\n  RowController rowController,\n) {\n  // Only show the non-hidden cells and cells that aren't of the grouping field\n  final cellContext = rowController.loadCells();\n\n  cellContext.removeWhere((cellContext) {\n    final fieldInfo = fieldController.getField(cellContext.fieldId);\n    return fieldInfo == null ||\n        !(fieldInfo.visibility?.isVisibleState() ?? false) ||\n        (groupFieldId != null && cellContext.fieldId == groupFieldId);\n  });\n  return cellContext\n      .map(\n        (cellCtx) => CellMeta(\n          fieldId: cellCtx.fieldId,\n          rowId: cellCtx.rowId,\n          fieldType: fieldController.getField(cellCtx.fieldId)!.fieldType,\n        ),\n      )\n      .toList();\n}\n\n@freezed\nclass CardEvent with _$CardEvent {\n  const factory CardEvent.initial() = _InitialRow;\n  const factory CardEvent.setIsEditing(bool isEditing) = _IsEditing;\n  const factory CardEvent.didReceiveCells(\n    List<CellMeta> cells,\n    ChangedReason reason,\n  ) = _DidReceiveCells;\n  const factory CardEvent.didUpdateRowMeta(RowMetaPB rowMeta) =\n      _DidUpdateRowMeta;\n}\n\n@freezed\nclass CellMeta with _$CellMeta {\n  const CellMeta._();\n\n  const factory CellMeta({\n    required String fieldId,\n    required RowId rowId,\n    required FieldType fieldType,\n  }) = _DatabaseCellMeta;\n\n  CellContext cellContext() => CellContext(fieldId: fieldId, rowId: rowId);\n}\n\n@freezed\nclass CardState with _$CardState {\n  const factory CardState({\n    required List<CellMeta> cells,\n    required bool isEditing,\n    required RowMetaPB rowMeta,\n    ChangedReason? changeReason,\n  }) = _RowCardState;\n\n  factory CardState.initial(\n    List<CellMeta> cells,\n    bool isEditing,\n    RowMetaPB rowMeta,\n  ) =>\n      CardState(\n        cells: cells,\n        isEditing: isEditing,\n        rowMeta: rowMeta,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart",
    "content": "import 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\n\nenum AccessoryType {\n  edit,\n  more,\n}\n\nabstract mixin class CardAccessory implements Widget {\n  AccessoryType get type;\n  void onTap(BuildContext context) {}\n}\n\nclass CardAccessoryContainer extends StatelessWidget {\n  const CardAccessoryContainer({\n    super.key,\n    required this.accessories,\n    required this.onTapAccessory,\n  });\n\n  final List<CardAccessory> accessories;\n  final void Function(AccessoryType) onTapAccessory;\n\n  @override\n  Widget build(BuildContext context) {\n    if (accessories.isEmpty) {\n      return const SizedBox.shrink();\n    }\n\n    final children = accessories.map<Widget>((accessory) {\n      return GestureDetector(\n        behavior: HitTestBehavior.opaque,\n        onTap: () {\n          accessory.onTap(context);\n          onTapAccessory(accessory.type);\n        },\n        child: _wrapHover(context, accessory),\n      );\n    }).toList();\n\n    children.insert(\n      1,\n      VerticalDivider(\n        width: 1,\n        thickness: 1,\n        color: Theme.of(context).brightness == Brightness.light\n            ? const Color(0xFF1F2329).withValues(alpha: 0.12)\n            : const Color(0xff59647a),\n      ),\n    );\n\n    return _wrapDecoration(\n      context,\n      IntrinsicHeight(child: Row(children: children)),\n    );\n  }\n\n  Widget _wrapHover(BuildContext context, CardAccessory accessory) {\n    return SizedBox(\n      width: 24,\n      height: 22,\n      child: FlowyHover(\n        style: HoverStyle(\n          backgroundColor: Theme.of(context).colorScheme.surface,\n          borderRadius: BorderRadius.zero,\n        ),\n        child: accessory,\n      ),\n    );\n  }\n\n  Widget _wrapDecoration(BuildContext context, Widget child) {\n    final decoration = BoxDecoration(\n      color: Theme.of(context).colorScheme.surface,\n      borderRadius: const BorderRadius.all(Radius.circular(4)),\n      border: Border.fromBorderSide(\n        BorderSide(\n          color: Theme.of(context).brightness == Brightness.light\n              ? const Color(0xFF1F2329).withValues(alpha: 0.12)\n              : const Color(0xff59647a),\n        ),\n      ),\n      boxShadow: [\n        BoxShadow(\n          blurRadius: 4,\n          color: const Color(0xFF1F2329).withValues(alpha: 0.02),\n        ),\n        BoxShadow(\n          blurRadius: 4,\n          spreadRadius: -2,\n          color: const Color(0xFF1F2329).withValues(alpha: 0.02),\n        ),\n      ],\n    );\n    return Container(\n      clipBehavior: Clip.hardEdge,\n      decoration: decoration,\n      child: ClipRRect(\n        borderRadius: const BorderRadius.all(Radius.circular(4)),\n        child: child,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/card_container.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:provider/provider.dart';\n\nimport 'accessory.dart';\n\nclass RowCardContainer extends StatelessWidget {\n  const RowCardContainer({\n    super.key,\n    required this.child,\n    required this.onTap,\n    required this.openAccessory,\n    required this.accessories,\n    this.buildAccessoryWhen,\n    this.onShiftTap,\n  });\n\n  final Widget child;\n  final void Function(BuildContext) onTap;\n  final void Function(BuildContext)? onShiftTap;\n  final void Function(AccessoryType) openAccessory;\n  final List<CardAccessory> accessories;\n  final bool Function()? buildAccessoryWhen;\n\n  @override\n  Widget build(BuildContext context) {\n    return ChangeNotifierProvider(\n      create: (_) => _CardContainerNotifier(),\n      child: Consumer<_CardContainerNotifier>(\n        builder: (context, notifier, _) {\n          final shouldBuildAccessory = buildAccessoryWhen?.call() ?? true;\n\n          return GestureDetector(\n            behavior: HitTestBehavior.opaque,\n            onTap: () {\n              if (HardwareKeyboard.instance.isShiftPressed) {\n                onShiftTap?.call(context);\n              } else {\n                onTap(context);\n              }\n            },\n            child: ConstrainedBox(\n              constraints: const BoxConstraints(minHeight: 36),\n              child: _CardEnterRegion(\n                shouldBuildAccessory: shouldBuildAccessory,\n                accessories: accessories,\n                onTapAccessory: openAccessory,\n                child: child,\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _CardEnterRegion extends StatelessWidget {\n  const _CardEnterRegion({\n    required this.shouldBuildAccessory,\n    required this.child,\n    required this.accessories,\n    required this.onTapAccessory,\n  });\n\n  final bool shouldBuildAccessory;\n  final Widget child;\n  final List<CardAccessory> accessories;\n  final void Function(AccessoryType) onTapAccessory;\n\n  @override\n  Widget build(BuildContext context) {\n    return Selector<_CardContainerNotifier, bool>(\n      selector: (context, notifier) => notifier.onEnter,\n      builder: (context, onEnter, _) {\n        final List<Widget> children = [\n          child,\n          if (onEnter && shouldBuildAccessory)\n            Positioned(\n              top: 7.0,\n              right: 7.0,\n              child: CardAccessoryContainer(\n                accessories: accessories,\n                onTapAccessory: onTapAccessory,\n              ),\n            ),\n        ];\n\n        return MouseRegion(\n          cursor: SystemMouseCursors.click,\n          onEnter: (p) =>\n              Provider.of<_CardContainerNotifier>(context, listen: false)\n                  .onEnter = true,\n          onExit: (p) =>\n              Provider.of<_CardContainerNotifier>(context, listen: false)\n                  .onEnter = false,\n          child: IntrinsicHeight(\n            child: Stack(\n              alignment: AlignmentDirectional.topEnd,\n              fit: StackFit.expand,\n              children: children,\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass _CardContainerNotifier extends ChangeNotifier {\n  _CardContainerNotifier();\n\n  bool _onEnter = false;\n\n  set onEnter(bool value) {\n    if (_onEnter != value) {\n      _onEnter = value;\n      notifyListeners();\n    }\n  }\n\n  bool get onEnter => _onEnter;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart",
    "content": "import 'package:flutter/widgets.dart';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';\n\nimport 'card_cell_skeleton/card_cell.dart';\nimport 'card_cell_skeleton/checkbox_card_cell.dart';\nimport 'card_cell_skeleton/checklist_card_cell.dart';\nimport 'card_cell_skeleton/date_card_cell.dart';\nimport 'card_cell_skeleton/media_card_cell.dart';\nimport 'card_cell_skeleton/number_card_cell.dart';\nimport 'card_cell_skeleton/relation_card_cell.dart';\nimport 'card_cell_skeleton/select_option_card_cell.dart';\nimport 'card_cell_skeleton/summary_card_cell.dart';\nimport 'card_cell_skeleton/text_card_cell.dart';\nimport 'card_cell_skeleton/time_card_cell.dart';\nimport 'card_cell_skeleton/timestamp_card_cell.dart';\nimport 'card_cell_skeleton/translate_card_cell.dart';\nimport 'card_cell_skeleton/url_card_cell.dart';\n\ntypedef CardCellStyleMap = Map<FieldType, CardCellStyle>;\n\nclass CardCellBuilder {\n  CardCellBuilder({required this.databaseController});\n\n  final DatabaseController databaseController;\n\n  Widget build({\n    required CellContext cellContext,\n    required CardCellStyleMap styleMap,\n    EditableCardNotifier? cellNotifier,\n    required bool hasNotes,\n  }) {\n    final fieldType = databaseController.fieldController\n        .getField(cellContext.fieldId)!\n        .fieldType;\n    final key = ValueKey(\n      \"${databaseController.viewId}${cellContext.fieldId}${cellContext.rowId}\",\n    );\n    final style = styleMap[fieldType];\n    return switch (fieldType) {\n      FieldType.Checkbox => CheckboxCardCell(\n          key: key,\n          style: isStyleOrNull(style),\n          databaseController: databaseController,\n          cellContext: cellContext,\n        ),\n      FieldType.Checklist => ChecklistCardCell(\n          key: key,\n          style: isStyleOrNull(style),\n          databaseController: databaseController,\n          cellContext: cellContext,\n        ),\n      FieldType.DateTime => DateCardCell(\n          key: key,\n          style: isStyleOrNull(style),\n          databaseController: databaseController,\n          cellContext: cellContext,\n        ),\n      FieldType.LastEditedTime || FieldType.CreatedTime => TimestampCardCell(\n          key: key,\n          style: isStyleOrNull(style),\n          databaseController: databaseController,\n          cellContext: cellContext,\n        ),\n      FieldType.SingleSelect || FieldType.MultiSelect => SelectOptionCardCell(\n          key: key,\n          style: isStyleOrNull(style),\n          databaseController: databaseController,\n          cellContext: cellContext,\n        ),\n      FieldType.Number => NumberCardCell(\n          style: isStyleOrNull(style),\n          databaseController: databaseController,\n          cellContext: cellContext,\n          key: key,\n        ),\n      FieldType.RichText => TextCardCell(\n          key: key,\n          style: isStyleOrNull(style),\n          databaseController: databaseController,\n          cellContext: cellContext,\n          editableNotifier: cellNotifier,\n          showNotes: hasNotes,\n        ),\n      FieldType.URL => URLCardCell(\n          key: key,\n          style: isStyleOrNull(style),\n          databaseController: databaseController,\n          cellContext: cellContext,\n        ),\n      FieldType.Relation => RelationCardCell(\n          key: key,\n          style: isStyleOrNull(style),\n          databaseController: databaseController,\n          cellContext: cellContext,\n        ),\n      FieldType.Summary => SummaryCardCell(\n          key: key,\n          style: isStyleOrNull(style),\n          databaseController: databaseController,\n          cellContext: cellContext,\n        ),\n      FieldType.Time => TimeCardCell(\n          key: key,\n          style: isStyleOrNull(style),\n          databaseController: databaseController,\n          cellContext: cellContext,\n        ),\n      FieldType.Translate => TranslateCardCell(\n          key: key,\n          style: isStyleOrNull(style),\n          databaseController: databaseController,\n          cellContext: cellContext,\n        ),\n      FieldType.Media => MediaCardCell(\n          key: key,\n          style: isStyleOrNull(style),\n          databaseController: databaseController,\n          cellContext: cellContext,\n        ),\n      _ => throw UnimplementedError,\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/card_cell.dart",
    "content": "import 'package:flutter/material.dart';\n\nabstract class CardCell<T extends CardCellStyle> extends StatefulWidget {\n  const CardCell({super.key, required this.style});\n\n  final T style;\n}\n\nabstract class CardCellStyle {\n  const CardCellStyle({required this.padding});\n\n  final EdgeInsetsGeometry padding;\n}\n\nS? isStyleOrNull<S>(CardCellStyle? style) {\n  if (style is S) {\n    return style as S;\n  } else {\n    return null;\n  }\n}\n\nclass EditableCardNotifier {\n  EditableCardNotifier({bool isEditing = false})\n      : isCellEditing = ValueNotifier(isEditing);\n\n  final ValueNotifier<bool> isCellEditing;\n\n  void dispose() {\n    isCellEditing.dispose();\n  }\n}\n\nabstract mixin class EditableCell {\n  // Each cell notifier will be bind to the [EditableRowNotifier], which enable\n  // the row notifier receive its cells event. For example: begin editing the\n  // cell or end editing the cell.\n  //\n  EditableCardNotifier? get editableNotifier;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/checkbox_card_cell.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart';\nimport 'package:flowy_infra_ui/style_widget/icon_button.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'card_cell.dart';\n\nclass CheckboxCardCellStyle extends CardCellStyle {\n  CheckboxCardCellStyle({\n    required super.padding,\n    required this.iconSize,\n    required this.showFieldName,\n    this.textStyle,\n  }) : assert(!showFieldName || showFieldName && textStyle != null);\n\n  final Size iconSize;\n  final bool showFieldName;\n  final TextStyle? textStyle;\n}\n\nclass CheckboxCardCell extends CardCell<CheckboxCardCellStyle> {\n  const CheckboxCardCell({\n    super.key,\n    required super.style,\n    required this.databaseController,\n    required this.cellContext,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n\n  @override\n  State<CheckboxCardCell> createState() => _CheckboxCellState();\n}\n\nclass _CheckboxCellState extends State<CheckboxCardCell> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) {\n        return CheckboxCellBloc(\n          cellController: makeCellController(\n            widget.databaseController,\n            widget.cellContext,\n          ).as(),\n        )..add(const CheckboxCellEvent.initial());\n      },\n      child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(\n        builder: (context, state) {\n          return Container(\n            alignment: AlignmentDirectional.centerStart,\n            padding: widget.style.padding,\n            child: Row(\n              children: [\n                FlowyIconButton(\n                  icon: FlowySvg(\n                    state.isSelected\n                        ? FlowySvgs.check_filled_s\n                        : FlowySvgs.uncheck_s,\n                    blendMode: BlendMode.dst,\n                    size: widget.style.iconSize,\n                  ),\n                  width: 20,\n                  onPressed: () => context\n                      .read<CheckboxCellBloc>()\n                      .add(const CheckboxCellEvent.select()),\n                ),\n                if (widget.style.showFieldName) ...[\n                  const HSpace(6.0),\n                  Text(\n                    state.fieldName,\n                    style: widget.style.textStyle,\n                  ),\n                ],\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/checklist_card_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'card_cell.dart';\n\nclass ChecklistCardCellStyle extends CardCellStyle {\n  ChecklistCardCellStyle({\n    required super.padding,\n    required this.textStyle,\n  });\n\n  final TextStyle textStyle;\n}\n\nclass ChecklistCardCell extends CardCell<ChecklistCardCellStyle> {\n  const ChecklistCardCell({\n    super.key,\n    required super.style,\n    required this.databaseController,\n    required this.cellContext,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n\n  @override\n  State<ChecklistCardCell> createState() => _ChecklistCellState();\n}\n\nclass _ChecklistCellState extends State<ChecklistCardCell> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) {\n        return ChecklistCellBloc(\n          cellController: makeCellController(\n            widget.databaseController,\n            widget.cellContext,\n          ).as(),\n        );\n      },\n      child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(\n        builder: (context, state) {\n          if (state.tasks.isEmpty) {\n            return const SizedBox.shrink();\n          }\n          return Padding(\n            padding: widget.style.padding,\n            child: ChecklistProgressBar(\n              tasks: state.tasks,\n              percent: state.percent,\n              textStyle: widget.style.textStyle,\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/date_card_cell.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_skeleton/date.dart';\nimport 'card_cell.dart';\n\nclass DateCardCellStyle extends CardCellStyle {\n  DateCardCellStyle({\n    required super.padding,\n    required this.textStyle,\n  });\n\n  final TextStyle textStyle;\n}\n\nclass DateCardCell extends CardCell<DateCardCellStyle> {\n  const DateCardCell({\n    super.key,\n    required super.style,\n    required this.databaseController,\n    required this.cellContext,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n\n  @override\n  State<DateCardCell> createState() => _DateCellState();\n}\n\nclass _DateCellState extends State<DateCardCell> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) {\n        return DateCellBloc(\n          cellController: makeCellController(\n            widget.databaseController,\n            widget.cellContext,\n          ).as(),\n        );\n      },\n      child: BlocBuilder<DateCellBloc, DateCellState>(\n        builder: (context, state) {\n          final dateStr = getDateCellStrFromCellData(\n            state.fieldInfo,\n            state.cellData,\n          );\n\n          if (dateStr.isEmpty) {\n            return const SizedBox.shrink();\n          }\n\n          return Container(\n            alignment: Alignment.centerLeft,\n            padding: widget.style.padding,\n            child: Row(\n              children: [\n                Flexible(\n                  child: Text(\n                    dateStr,\n                    style: widget.style.textStyle,\n                    overflow: TextOverflow.ellipsis,\n                  ),\n                ),\n                if (state.cellData.reminderId.isNotEmpty) ...[\n                  const HSpace(4),\n                  const FlowySvg(FlowySvgs.clock_alarm_s),\n                ],\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/media_card_cell.dart",
    "content": "import 'package:flutter/widgets.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/card_cell.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MediaCardCellStyle extends CardCellStyle {\n  const MediaCardCellStyle({\n    required super.padding,\n    required this.textStyle,\n  });\n\n  final TextStyle textStyle;\n}\n\nclass MediaCardCell extends CardCell<MediaCardCellStyle> {\n  const MediaCardCell({\n    super.key,\n    required super.style,\n    required this.databaseController,\n    required this.cellContext,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n\n  @override\n  State<MediaCardCell> createState() => _MediaCellState();\n}\n\nclass _MediaCellState extends State<MediaCardCell> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<MediaCellBloc>(\n      create: (_) => MediaCellBloc(\n        cellController: makeCellController(\n          widget.databaseController,\n          widget.cellContext,\n        ).as(),\n      )..add(const MediaCellEvent.initial()),\n      child: BlocBuilder<MediaCellBloc, MediaCellState>(\n        builder: (context, state) {\n          if (state.files.isEmpty) {\n            return const SizedBox.shrink();\n          }\n\n          return Padding(\n            padding: const EdgeInsets.only(left: 4),\n            child: Row(\n              children: [\n                const FlowySvg(\n                  FlowySvgs.media_s,\n                  size: Size.square(12),\n                ),\n                const HSpace(6),\n                Flexible(\n                  child: FlowyText.regular(\n                    LocaleKeys.grid_media_attachmentsHint\n                        .tr(args: ['${state.files.length}']),\n                    fontSize: 12,\n                    color: AFThemeExtension.of(context).secondaryTextColor,\n                    overflow: TextOverflow.ellipsis,\n                  ),\n                ),\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/number_card_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'card_cell.dart';\n\nclass NumberCardCellStyle extends CardCellStyle {\n  const NumberCardCellStyle({\n    required super.padding,\n    required this.textStyle,\n  });\n\n  final TextStyle textStyle;\n}\n\nclass NumberCardCell extends CardCell<NumberCardCellStyle> {\n  const NumberCardCell({\n    super.key,\n    required super.style,\n    required this.databaseController,\n    required this.cellContext,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n\n  @override\n  State<NumberCardCell> createState() => _NumberCellState();\n}\n\nclass _NumberCellState extends State<NumberCardCell> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) {\n        return NumberCellBloc(\n          cellController: makeCellController(\n            widget.databaseController,\n            widget.cellContext,\n          ).as(),\n        );\n      },\n      child: BlocBuilder<NumberCellBloc, NumberCellState>(\n        buildWhen: (previous, current) => previous.content != current.content,\n        builder: (context, state) {\n          if (state.content.isEmpty) {\n            return const SizedBox.shrink();\n          }\n\n          return Container(\n            alignment: AlignmentDirectional.centerStart,\n            padding: widget.style.padding,\n            child: Text(state.content, style: widget.style.textStyle),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'card_cell.dart';\n\nclass RelationCardCellStyle extends CardCellStyle {\n  RelationCardCellStyle({\n    required super.padding,\n    required this.textStyle,\n    required this.wrap,\n  });\n\n  final TextStyle textStyle;\n  final bool wrap;\n}\n\nclass RelationCardCell extends CardCell<RelationCardCellStyle> {\n  const RelationCardCell({\n    super.key,\n    required super.style,\n    required this.databaseController,\n    required this.cellContext,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n\n  @override\n  State<RelationCardCell> createState() => _RelationCellState();\n}\n\nclass _RelationCellState extends State<RelationCardCell> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) {\n        return RelationCellBloc(\n          cellController: makeCellController(\n            widget.databaseController,\n            widget.cellContext,\n          ).as(),\n        );\n      },\n      child: BlocBuilder<RelationCellBloc, RelationCellState>(\n        builder: (context, state) {\n          if (state.rows.isEmpty) {\n            return const SizedBox.shrink();\n          }\n\n          final children = state.rows.map(\n            (row) {\n              final isEmpty = row.name.isEmpty;\n              return Text(\n                isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name,\n                style: widget.style.textStyle.copyWith(\n                  color: isEmpty ? Theme.of(context).hintColor : null,\n                  decoration: TextDecoration.underline,\n                ),\n                overflow: TextOverflow.ellipsis,\n              );\n            },\n          ).toList();\n\n          return Container(\n            alignment: AlignmentDirectional.topStart,\n            padding: widget.style.padding,\n            child: widget.style.wrap\n                ? Wrap(spacing: 4, runSpacing: 4, children: children)\n                : SingleChildScrollView(\n                    scrollDirection: Axis.horizontal,\n                    child: Row(\n                      mainAxisSize: MainAxisSize.min,\n                      children: children,\n                    ),\n                  ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/select_option_card_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'card_cell.dart';\n\nclass SelectOptionCardCellStyle extends CardCellStyle {\n  SelectOptionCardCellStyle({\n    required super.padding,\n    required this.tagFontSize,\n    required this.wrap,\n    required this.tagPadding,\n  });\n\n  final double tagFontSize;\n  final bool wrap;\n  final EdgeInsets tagPadding;\n}\n\nclass SelectOptionCardCell extends CardCell<SelectOptionCardCellStyle> {\n  const SelectOptionCardCell({\n    super.key,\n    required super.style,\n    required this.databaseController,\n    required this.cellContext,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n\n  @override\n  State<SelectOptionCardCell> createState() => _SelectOptionCellState();\n}\n\nclass _SelectOptionCellState extends State<SelectOptionCardCell> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) {\n        return SelectOptionCellBloc(\n          cellController: makeCellController(\n            widget.databaseController,\n            widget.cellContext,\n          ).as(),\n        );\n      },\n      child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(\n        buildWhen: (previous, current) {\n          return previous.selectedOptions != current.selectedOptions;\n        },\n        builder: (context, state) {\n          if (state.selectedOptions.isEmpty) {\n            return const SizedBox.shrink();\n          }\n\n          final children = state.selectedOptions\n              .map(\n                (option) => SelectOptionTag(\n                  option: option,\n                  fontSize: widget.style.tagFontSize,\n                  padding: widget.style.tagPadding,\n                ),\n              )\n              .toList();\n\n          return Container(\n            alignment: AlignmentDirectional.topStart,\n            padding: widget.style.padding,\n            child: widget.style.wrap\n                ? Wrap(spacing: 4, runSpacing: 4, children: children)\n                : SingleChildScrollView(\n                    scrollDirection: Axis.horizontal,\n                    child: Row(\n                      mainAxisSize: MainAxisSize.min,\n                      children: children,\n                    ),\n                  ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'card_cell.dart';\n\nclass SummaryCardCellStyle extends CardCellStyle {\n  const SummaryCardCellStyle({\n    required super.padding,\n    required this.textStyle,\n  });\n\n  final TextStyle textStyle;\n}\n\nclass SummaryCardCell extends CardCell<SummaryCardCellStyle> {\n  const SummaryCardCell({\n    super.key,\n    required super.style,\n    required this.databaseController,\n    required this.cellContext,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n\n  @override\n  State<SummaryCardCell> createState() => _SummaryCellState();\n}\n\nclass _SummaryCellState extends State<SummaryCardCell> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) {\n        return SummaryCellBloc(\n          cellController: makeCellController(\n            widget.databaseController,\n            widget.cellContext,\n          ).as(),\n        );\n      },\n      child: BlocBuilder<SummaryCellBloc, SummaryCellState>(\n        buildWhen: (previous, current) => previous.content != current.content,\n        builder: (context, state) {\n          if (state.content.isEmpty) {\n            return const SizedBox.shrink();\n          }\n\n          return Container(\n            alignment: AlignmentDirectional.centerStart,\n            padding: widget.style.padding,\n            child: Text(state.content, style: widget.style.textStyle),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart",
    "content": "import 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_builder.dart';\nimport 'card_cell.dart';\n\nclass TextCardCellStyle extends CardCellStyle {\n  TextCardCellStyle({\n    required super.padding,\n    required this.textStyle,\n    required this.titleTextStyle,\n    this.maxLines = 1,\n  });\n\n  final TextStyle textStyle;\n  final TextStyle titleTextStyle;\n  final int? maxLines;\n}\n\nclass TextCardCell extends CardCell<TextCardCellStyle> with EditableCell {\n  const TextCardCell({\n    super.key,\n    required super.style,\n    required this.databaseController,\n    required this.cellContext,\n    this.showNotes = false,\n    this.editableNotifier,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n  final bool showNotes;\n\n  @override\n  final EditableCardNotifier? editableNotifier;\n\n  @override\n  State<TextCardCell> createState() => _TextCellState();\n}\n\nclass _TextCellState extends State<TextCardCell> {\n  late final cellBloc = TextCellBloc(\n    cellController: makeCellController(\n      widget.databaseController,\n      widget.cellContext,\n    ).as(),\n  );\n  late final TextEditingController _textEditingController;\n  final focusNode = SingleListenerFocusNode();\n\n  @override\n  void initState() {\n    super.initState();\n    _textEditingController =\n        TextEditingController(text: cellBloc.state.content);\n\n    if (widget.editableNotifier?.isCellEditing.value ?? false) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        focusNode.requestFocus();\n        cellBloc.add(const TextCellEvent.enableEdit(true));\n      });\n    }\n\n    // If the focusNode lost its focus, the widget's editableNotifier will\n    // set to false, which will cause the [EditableRowNotifier] to receive\n    // end edit event.\n    focusNode.addListener(_onFocusChanged);\n    _bindEditableNotifier();\n  }\n\n  void _onFocusChanged() {\n    if (!focusNode.hasFocus) {\n      widget.editableNotifier?.isCellEditing.value = false;\n      cellBloc.add(const TextCellEvent.enableEdit(false));\n      cellBloc.add(TextCellEvent.updateText(_textEditingController.text));\n    }\n  }\n\n  void _bindEditableNotifier() {\n    widget.editableNotifier?.isCellEditing.addListener(() {\n      if (!mounted) {\n        return;\n      }\n\n      final isEditing = widget.editableNotifier?.isCellEditing.value ?? false;\n      if (isEditing) {\n        WidgetsBinding.instance\n            .addPostFrameCallback((_) => focusNode.requestFocus());\n      }\n      cellBloc.add(TextCellEvent.enableEdit(isEditing));\n    });\n  }\n\n  @override\n  void didUpdateWidget(covariant oldWidget) {\n    if (oldWidget.editableNotifier != widget.editableNotifier) {\n      _bindEditableNotifier();\n    }\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final isTitle = cellBloc.cellController.fieldInfo.isPrimary;\n    return BlocProvider.value(\n      value: cellBloc,\n      child: BlocListener<TextCellBloc, TextCellState>(\n        listenWhen: (previous, current) => previous.content != current.content,\n        listener: (context, state) {\n          _textEditingController.text = state.content ?? \"\";\n        },\n        child: isTitle ? _buildTitle() : _buildText(),\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    _textEditingController.dispose();\n    widget.editableNotifier?.isCellEditing\n        .removeListener(_bindEditableNotifier);\n    focusNode.dispose();\n    cellBloc.close();\n    super.dispose();\n  }\n\n  Widget? _buildIcon(TextCellState state) {\n    if (state.emoji?.value.isNotEmpty ?? false) {\n      return FlowyText.emoji(\n        optimizeEmojiAlign: true,\n        state.emoji?.value ?? '',\n      );\n    }\n\n    if (widget.showNotes) {\n      return FlowyTooltip(\n        message: LocaleKeys.board_notesTooltip.tr(),\n        child: Padding(\n          padding: const EdgeInsets.all(1.0),\n          child: FlowySvg(\n            FlowySvgs.notes_s,\n            color: Theme.of(context).hintColor,\n          ),\n        ),\n      );\n    }\n    return null;\n  }\n\n  Widget _buildText() {\n    return BlocBuilder<TextCellBloc, TextCellState>(\n      builder: (context, state) {\n        final content = state.content ?? \"\";\n\n        return content.isEmpty\n            ? const SizedBox.shrink()\n            : Container(\n                padding: widget.style.padding,\n                alignment: AlignmentDirectional.centerStart,\n                child: Text(\n                  content,\n                  style: widget.style.textStyle,\n                  maxLines: widget.style.maxLines,\n                ),\n              );\n      },\n    );\n  }\n\n  Widget _buildTitle() {\n    final textField = _buildTextField();\n    return BlocBuilder<TextCellBloc, TextCellState>(\n      builder: (context, state) {\n        final icon = _buildIcon(state);\n        if (icon == null) {\n          return textField;\n        }\n        final resolved =\n            widget.style.padding.resolve(Directionality.of(context));\n        final padding = EdgeInsetsDirectional.only(\n          start: resolved.left,\n          top: resolved.top,\n          bottom: resolved.bottom,\n        );\n        return Row(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Container(\n              padding: padding,\n              child: icon,\n            ),\n            Expanded(child: textField),\n          ],\n        );\n      },\n    );\n  }\n\n  Widget _buildTextField() {\n    return BlocSelector<TextCellBloc, TextCellState, bool>(\n      selector: (state) => state.enableEdit,\n      builder: (context, isEditing) {\n        return IgnorePointer(\n          ignoring: !isEditing,\n          child: CallbackShortcuts(\n            bindings: {\n              const SingleActivator(LogicalKeyboardKey.escape): () =>\n                  focusNode.unfocus(),\n              const SimpleActivator(LogicalKeyboardKey.enter): () =>\n                  focusNode.unfocus(),\n            },\n            child: TextField(\n              controller: _textEditingController,\n              focusNode: focusNode,\n              onEditingComplete: () => focusNode.unfocus(),\n              onSubmitted: (_) => focusNode.unfocus(),\n              maxLines: null,\n              minLines: 1,\n              textInputAction: TextInputAction.done,\n              readOnly: !isEditing,\n              enableInteractiveSelection: isEditing,\n              style: widget.style.titleTextStyle,\n              decoration: InputDecoration(\n                contentPadding: widget.style.padding,\n                border: InputBorder.none,\n                enabledBorder: InputBorder.none,\n                isDense: true,\n                isCollapsed: true,\n                hintText: LocaleKeys.grid_row_titlePlaceholder.tr(),\n                hintStyle: widget.style.titleTextStyle.copyWith(\n                  color: Theme.of(context).hintColor,\n                ),\n              ),\n              onTapOutside: (_) {},\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass SimpleActivator with Diagnosticable implements ShortcutActivator {\n  const SimpleActivator(\n    this.trigger, {\n    this.includeRepeats = true,\n  });\n\n  final LogicalKeyboardKey trigger;\n  final bool includeRepeats;\n\n  @override\n  bool accepts(KeyEvent event, HardwareKeyboard state) {\n    return (event is KeyDownEvent ||\n            (includeRepeats && event is KeyRepeatEvent)) &&\n        trigger == event.logicalKey;\n  }\n\n  @override\n  String debugDescribeKeys() =>\n      kDebugMode ? trigger.debugName ?? trigger.toStringShort() : '';\n\n  @override\n  Iterable<LogicalKeyboardKey>? get triggers => <LogicalKeyboardKey>[trigger];\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/time_card_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';\n\nimport 'card_cell.dart';\n\nclass TimeCardCellStyle extends CardCellStyle {\n  const TimeCardCellStyle({\n    required super.padding,\n    required this.textStyle,\n  });\n\n  final TextStyle textStyle;\n}\n\nclass TimeCardCell extends CardCell<TimeCardCellStyle> {\n  const TimeCardCell({\n    super.key,\n    required super.style,\n    required this.databaseController,\n    required this.cellContext,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n\n  @override\n  State<TimeCardCell> createState() => _TimeCellState();\n}\n\nclass _TimeCellState extends State<TimeCardCell> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) {\n        return TimeCellBloc(\n          cellController: makeCellController(\n            widget.databaseController,\n            widget.cellContext,\n          ).as(),\n        );\n      },\n      child: BlocBuilder<TimeCellBloc, TimeCellState>(\n        buildWhen: (previous, current) => previous.content != current.content,\n        builder: (context, state) {\n          if (state.content.isEmpty) {\n            return const SizedBox.shrink();\n          }\n\n          return Container(\n            alignment: AlignmentDirectional.centerStart,\n            padding: widget.style.padding,\n            child: Text(state.content, style: widget.style.textStyle),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/timestamp_card_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'card_cell.dart';\n\nclass TimestampCardCellStyle extends CardCellStyle {\n  TimestampCardCellStyle({\n    required super.padding,\n    required this.textStyle,\n  });\n\n  final TextStyle textStyle;\n}\n\nclass TimestampCardCell extends CardCell<TimestampCardCellStyle> {\n  const TimestampCardCell({\n    super.key,\n    required super.style,\n    required this.databaseController,\n    required this.cellContext,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n\n  @override\n  State<TimestampCardCell> createState() => _TimestampCellState();\n}\n\nclass _TimestampCellState extends State<TimestampCardCell> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) {\n        return TimestampCellBloc(\n          cellController: makeCellController(\n            widget.databaseController,\n            widget.cellContext,\n          ).as(),\n        );\n      },\n      child: BlocBuilder<TimestampCellBloc, TimestampCellState>(\n        buildWhen: (previous, current) => previous.dateStr != current.dateStr,\n        builder: (context, state) {\n          if (state.dateStr.isEmpty) {\n            return const SizedBox.shrink();\n          }\n\n          return Container(\n            alignment: AlignmentDirectional.centerStart,\n            padding: widget.style.padding,\n            child: Text(\n              state.dateStr,\n              style: widget.style.textStyle,\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'card_cell.dart';\n\nclass TranslateCardCellStyle extends CardCellStyle {\n  const TranslateCardCellStyle({\n    required super.padding,\n    required this.textStyle,\n  });\n\n  final TextStyle textStyle;\n}\n\nclass TranslateCardCell extends CardCell<TranslateCardCellStyle> {\n  const TranslateCardCell({\n    super.key,\n    required super.style,\n    required this.databaseController,\n    required this.cellContext,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n\n  @override\n  State<TranslateCardCell> createState() => _TranslateCellState();\n}\n\nclass _TranslateCellState extends State<TranslateCardCell> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) {\n        return TranslateCellBloc(\n          cellController: makeCellController(\n            widget.databaseController,\n            widget.cellContext,\n          ).as(),\n        );\n      },\n      child: BlocBuilder<TranslateCellBloc, TranslateCellState>(\n        buildWhen: (previous, current) => previous.content != current.content,\n        builder: (context, state) {\n          if (state.content.isEmpty) {\n            return const SizedBox.shrink();\n          }\n\n          return Container(\n            alignment: AlignmentDirectional.centerStart,\n            padding: widget.style.padding,\n            child: Text(state.content, style: widget.style.textStyle),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/url_card_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'card_cell.dart';\n\nclass URLCardCellStyle extends CardCellStyle {\n  URLCardCellStyle({\n    required super.padding,\n    required this.textStyle,\n  });\n\n  final TextStyle textStyle;\n}\n\nclass URLCardCell extends CardCell<URLCardCellStyle> {\n  const URLCardCell({\n    super.key,\n    required super.style,\n    required this.databaseController,\n    required this.cellContext,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n\n  @override\n  State<URLCardCell> createState() => _URLCellState();\n}\n\nclass _URLCellState extends State<URLCardCell> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) {\n        return URLCellBloc(\n          cellController: makeCellController(\n            widget.databaseController,\n            widget.cellContext,\n          ).as(),\n        );\n      },\n      child: BlocBuilder<URLCellBloc, URLCellState>(\n        buildWhen: (previous, current) => previous.content != current.content,\n        builder: (context, state) {\n          if (state.content.isEmpty) {\n            return const SizedBox.shrink();\n          }\n          return Container(\n            alignment: AlignmentDirectional.centerStart,\n            padding: widget.style.padding,\n            child: Text(\n              state.content,\n              style: widget.style.textStyle,\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\n\nimport '../card_cell_builder.dart';\nimport '../card_cell_skeleton/checkbox_card_cell.dart';\nimport '../card_cell_skeleton/checklist_card_cell.dart';\nimport '../card_cell_skeleton/date_card_cell.dart';\nimport '../card_cell_skeleton/media_card_cell.dart';\nimport '../card_cell_skeleton/number_card_cell.dart';\nimport '../card_cell_skeleton/relation_card_cell.dart';\nimport '../card_cell_skeleton/select_option_card_cell.dart';\nimport '../card_cell_skeleton/summary_card_cell.dart';\nimport '../card_cell_skeleton/text_card_cell.dart';\nimport '../card_cell_skeleton/timestamp_card_cell.dart';\nimport '../card_cell_skeleton/translate_card_cell.dart';\nimport '../card_cell_skeleton/url_card_cell.dart';\n\nCardCellStyleMap desktopCalendarCardCellStyleMap(BuildContext context) {\n  const EdgeInsetsGeometry padding = EdgeInsets.symmetric(vertical: 2);\n  final TextStyle textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(\n        fontSize: 10,\n        overflow: TextOverflow.ellipsis,\n        fontWeight: FontWeight.w400,\n      );\n\n  return {\n    FieldType.Checkbox: CheckboxCardCellStyle(\n      padding: padding,\n      iconSize: const Size.square(16),\n      showFieldName: true,\n      textStyle: textStyle,\n    ),\n    FieldType.Checklist: ChecklistCardCellStyle(\n      padding: padding,\n      textStyle: textStyle.copyWith(color: Theme.of(context).hintColor),\n    ),\n    FieldType.CreatedTime: TimestampCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.DateTime: DateCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.LastEditedTime: TimestampCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.MultiSelect: SelectOptionCardCellStyle(\n      padding: padding,\n      tagFontSize: 9,\n      wrap: true,\n      tagPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),\n    ),\n    FieldType.Number: NumberCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.RichText: TextCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n      titleTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(\n            fontSize: 11,\n            overflow: TextOverflow.ellipsis,\n          ),\n    ),\n    FieldType.SingleSelect: SelectOptionCardCellStyle(\n      padding: padding,\n      tagFontSize: 9,\n      wrap: true,\n      tagPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),\n    ),\n    FieldType.URL: URLCardCellStyle(\n      padding: padding,\n      textStyle: textStyle.copyWith(\n        color: Theme.of(context).colorScheme.primary,\n        decoration: TextDecoration.underline,\n      ),\n    ),\n    FieldType.Relation: RelationCardCellStyle(\n      padding: padding,\n      wrap: true,\n      textStyle: textStyle,\n    ),\n    FieldType.Summary: SummaryCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.Translate: TranslateCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.Media: MediaCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n  };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\n\nimport '../card_cell_builder.dart';\nimport '../card_cell_skeleton/checkbox_card_cell.dart';\nimport '../card_cell_skeleton/checklist_card_cell.dart';\nimport '../card_cell_skeleton/date_card_cell.dart';\nimport '../card_cell_skeleton/media_card_cell.dart';\nimport '../card_cell_skeleton/number_card_cell.dart';\nimport '../card_cell_skeleton/relation_card_cell.dart';\nimport '../card_cell_skeleton/select_option_card_cell.dart';\nimport '../card_cell_skeleton/summary_card_cell.dart';\nimport '../card_cell_skeleton/text_card_cell.dart';\nimport '../card_cell_skeleton/time_card_cell.dart';\nimport '../card_cell_skeleton/timestamp_card_cell.dart';\nimport '../card_cell_skeleton/translate_card_cell.dart';\nimport '../card_cell_skeleton/url_card_cell.dart';\n\nCardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) {\n  const EdgeInsetsGeometry padding = EdgeInsets.all(4);\n  final TextStyle textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(\n        fontSize: 11,\n        overflow: TextOverflow.ellipsis,\n      );\n\n  return {\n    FieldType.Checkbox: CheckboxCardCellStyle(\n      padding: padding,\n      iconSize: const Size.square(16),\n      showFieldName: true,\n      textStyle: textStyle,\n    ),\n    FieldType.Checklist: ChecklistCardCellStyle(\n      padding: padding,\n      textStyle: textStyle.copyWith(color: Theme.of(context).hintColor),\n    ),\n    FieldType.CreatedTime: TimestampCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.DateTime: DateCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.LastEditedTime: TimestampCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.MultiSelect: SelectOptionCardCellStyle(\n      padding: padding,\n      tagFontSize: 11,\n      wrap: true,\n      tagPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),\n    ),\n    FieldType.Number: NumberCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.RichText: TextCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n      maxLines: 2,\n      titleTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(\n            overflow: TextOverflow.ellipsis,\n          ),\n    ),\n    FieldType.SingleSelect: SelectOptionCardCellStyle(\n      padding: padding,\n      tagFontSize: 11,\n      wrap: true,\n      tagPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),\n    ),\n    FieldType.URL: URLCardCellStyle(\n      padding: padding,\n      textStyle: textStyle.copyWith(\n        color: Theme.of(context).colorScheme.primary,\n        decoration: TextDecoration.underline,\n      ),\n    ),\n    FieldType.Relation: RelationCardCellStyle(\n      padding: padding,\n      wrap: true,\n      textStyle: textStyle,\n    ),\n    FieldType.Summary: SummaryCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.Time: TimeCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.Translate: TranslateCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.Media: MediaCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n  };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/media_card_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\n\nimport '../card_cell_builder.dart';\nimport '../card_cell_skeleton/checkbox_card_cell.dart';\nimport '../card_cell_skeleton/checklist_card_cell.dart';\nimport '../card_cell_skeleton/date_card_cell.dart';\nimport '../card_cell_skeleton/number_card_cell.dart';\nimport '../card_cell_skeleton/relation_card_cell.dart';\nimport '../card_cell_skeleton/select_option_card_cell.dart';\nimport '../card_cell_skeleton/text_card_cell.dart';\nimport '../card_cell_skeleton/time_card_cell.dart';\nimport '../card_cell_skeleton/timestamp_card_cell.dart';\nimport '../card_cell_skeleton/url_card_cell.dart';\n\nCardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) {\n  const EdgeInsetsGeometry padding = EdgeInsets.all(4);\n  final TextStyle textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(\n        fontSize: 14,\n        overflow: TextOverflow.ellipsis,\n        fontWeight: FontWeight.w400,\n      );\n\n  return {\n    FieldType.Checkbox: CheckboxCardCellStyle(\n      padding: padding,\n      iconSize: const Size.square(24),\n      showFieldName: true,\n      textStyle: textStyle,\n    ),\n    FieldType.Checklist: ChecklistCardCellStyle(\n      padding: padding,\n      textStyle: textStyle.copyWith(color: Theme.of(context).hintColor),\n    ),\n    FieldType.CreatedTime: TimestampCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.DateTime: DateCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.LastEditedTime: TimestampCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.MultiSelect: SelectOptionCardCellStyle(\n      padding: padding,\n      tagFontSize: 12,\n      wrap: true,\n      tagPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),\n    ),\n    FieldType.Number: NumberCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.RichText: TextCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n      titleTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(\n            overflow: TextOverflow.ellipsis,\n          ),\n    ),\n    FieldType.SingleSelect: SelectOptionCardCellStyle(\n      padding: padding,\n      tagFontSize: 12,\n      wrap: true,\n      tagPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),\n    ),\n    FieldType.URL: URLCardCellStyle(\n      padding: padding,\n      textStyle: textStyle.copyWith(\n        color: Theme.of(context).colorScheme.primary,\n        decoration: TextDecoration.underline,\n      ),\n    ),\n    FieldType.Relation: RelationCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n      wrap: true,\n    ),\n    FieldType.Summary: SummaryCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.Time: TimeCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.Translate: TranslateCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n    FieldType.Media: MediaCardCellStyle(\n      padding: padding,\n      textStyle: textStyle,\n    ),\n  };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checkbox_cell.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport '../editable_cell_skeleton/checkbox.dart';\n\nclass DesktopGridCheckboxCellSkin extends IEditableCheckboxCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    CheckboxCellBloc bloc,\n    CheckboxCellState state,\n  ) {\n    return ValueListenableBuilder(\n      valueListenable: compactModeNotifier,\n      builder: (context, compactMode, _) {\n        final padding = compactMode\n            ? GridSize.compactCellContentInsets\n            : GridSize.cellContentInsets;\n        return Container(\n          alignment: AlignmentDirectional.centerStart,\n          padding: padding,\n          child: FlowyIconButton(\n            hoverColor: Colors.transparent,\n            onPressed: () => bloc.add(const CheckboxCellEvent.select()),\n            icon: FlowySvg(\n              state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,\n              blendMode: BlendMode.dst,\n              size: const Size.square(20),\n            ),\n            width: 20,\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checklist_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_skeleton/checklist.dart';\n\nclass DesktopGridChecklistCellSkin extends IEditableChecklistCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    ChecklistCellBloc bloc,\n    PopoverController popoverController,\n  ) {\n    return AppFlowyPopover(\n      margin: EdgeInsets.zero,\n      controller: popoverController,\n      constraints: BoxConstraints.loose(const Size(360, 400)),\n      direction: PopoverDirection.bottomWithLeftAligned,\n      triggerActions: PopoverTriggerFlags.none,\n      skipTraversal: true,\n      popupBuilder: (popoverContext) {\n        WidgetsBinding.instance.addPostFrameCallback((_) {\n          cellContainerNotifier.isFocus = true;\n        });\n        return BlocProvider.value(\n          value: bloc,\n          child: ChecklistCellEditor(\n            cellController: bloc.cellController,\n          ),\n        );\n      },\n      onClose: () => cellContainerNotifier.isFocus = false,\n      child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(\n        builder: (context, state) {\n          return ValueListenableBuilder(\n            valueListenable: compactModeNotifier,\n            builder: (context, compactMode, _) {\n              final padding = compactMode\n                  ? GridSize.compactCellContentInsets\n                  : GridSize.cellContentInsets;\n\n              return Container(\n                alignment: AlignmentDirectional.centerStart,\n                padding: padding,\n                child: state.tasks.isEmpty\n                    ? const SizedBox.shrink()\n                    : ChecklistProgressBar(\n                        tasks: state.tasks,\n                        percent: state.percent,\n                      ),\n              );\n            },\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_date_cell.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/date_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/widgets.dart';\n\nimport '../editable_cell_skeleton/date.dart';\n\nclass DesktopGridDateCellSkin extends IEditableDateCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    DateCellBloc bloc,\n    DateCellState state,\n    PopoverController popoverController,\n  ) {\n    return AppFlowyPopover(\n      controller: popoverController,\n      triggerActions: PopoverTriggerFlags.none,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      constraints: BoxConstraints.loose(const Size(260, 620)),\n      margin: EdgeInsets.zero,\n      child: Align(\n        alignment: AlignmentDirectional.centerStart,\n        child: state.fieldInfo.wrapCellContent ?? false\n            ? _buildCellContent(state, compactModeNotifier)\n            : SingleChildScrollView(\n                physics: const NeverScrollableScrollPhysics(),\n                scrollDirection: Axis.horizontal,\n                child: _buildCellContent(state, compactModeNotifier),\n              ),\n      ),\n      popupBuilder: (BuildContext popoverContent) {\n        return DateCellEditor(\n          cellController: bloc.cellController,\n          onDismissed: () => cellContainerNotifier.isFocus = false,\n        );\n      },\n      onClose: () {\n        cellContainerNotifier.isFocus = false;\n      },\n    );\n  }\n\n  Widget _buildCellContent(\n    DateCellState state,\n    ValueNotifier<bool> compactModeNotifier,\n  ) {\n    final wrap = state.fieldInfo.wrapCellContent ?? false;\n    final dateStr = getDateCellStrFromCellData(\n      state.fieldInfo,\n      state.cellData,\n    );\n    return ValueListenableBuilder(\n      valueListenable: compactModeNotifier,\n      builder: (context, compactMode, _) {\n        final padding = compactMode\n            ? GridSize.compactCellContentInsets\n            : GridSize.cellContentInsets;\n        return Padding(\n          padding: padding,\n          child: Row(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              Flexible(\n                child: FlowyText(\n                  dateStr,\n                  overflow: wrap ? null : TextOverflow.ellipsis,\n                  maxLines: wrap ? null : 1,\n                ),\n              ),\n              if (state.cellData.reminderId.isNotEmpty) ...[\n                const HSpace(4),\n                FlowyTooltip(\n                  message: LocaleKeys.grid_field_reminderOnDateTooltip.tr(),\n                  child: const FlowySvg(FlowySvgs.clock_alarm_s),\n                ),\n              ],\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_media_cell.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_media_upload.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/media.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/mobile_media_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/media_file_type_ext.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/shared/af_image.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass GridMediaCellSkin extends IEditableMediaCellSkin {\n  const GridMediaCellSkin({this.isMobileRowDetail = false});\n\n  final bool isMobileRowDetail;\n\n  @override\n  void dispose() {}\n\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    PopoverController popoverController,\n    MediaCellBloc bloc,\n  ) {\n    final isMobile = UniversalPlatform.isMobile;\n\n    Widget child = BlocBuilder<MediaCellBloc, MediaCellState>(\n      builder: (context, state) {\n        final wrapContent = context.read<MediaCellBloc>().wrapContent;\n        final List<Widget> children = state.files\n            .map<Widget>(\n              (file) => GestureDetector(\n                onTap: () => _openOrExpandFile(context, file, state.files),\n                child: Padding(\n                  padding: wrapContent\n                      ? const EdgeInsets.only(right: 4)\n                      : EdgeInsets.zero,\n                  child: _FilePreviewRender(file: file),\n                ),\n              ),\n            )\n            .toList();\n\n        if (isMobileRowDetail && state.files.isEmpty) {\n          children.add(\n            Padding(\n              padding: const EdgeInsets.symmetric(vertical: 4),\n              child: Text(\n                LocaleKeys.grid_row_textPlaceholder.tr(),\n                style: Theme.of(context).textTheme.bodyMedium?.copyWith(\n                      fontSize: 16,\n                      color: Theme.of(context).hintColor,\n                    ),\n              ),\n            ),\n          );\n        }\n\n        if (!isMobile && wrapContent) {\n          return Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 4),\n            child: SizedBox(\n              width: double.infinity,\n              child: Wrap(\n                runSpacing: 4,\n                children: children,\n              ),\n            ),\n          );\n        }\n\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 4),\n          child: SizedBox(\n            width: double.infinity,\n            child: SingleChildScrollView(\n              scrollDirection: Axis.horizontal,\n              child: SeparatedRow(\n                separatorBuilder: () => const HSpace(4),\n                children: children..add(const SizedBox(width: 16)),\n              ),\n            ),\n          ),\n        );\n      },\n    );\n\n    if (!isMobile) {\n      child = AppFlowyPopover(\n        controller: popoverController,\n        constraints: const BoxConstraints(\n          minWidth: 250,\n          maxWidth: 250,\n          maxHeight: 400,\n        ),\n        margin: EdgeInsets.zero,\n        triggerActions: PopoverTriggerFlags.none,\n        direction: PopoverDirection.bottomWithCenterAligned,\n        popupBuilder: (_) => BlocProvider.value(\n          value: context.read<MediaCellBloc>(),\n          child: const MediaCellEditor(),\n        ),\n        onClose: () => cellContainerNotifier.isFocus = false,\n        child: child,\n      );\n    } else {\n      child = Align(\n        alignment: AlignmentDirectional.centerStart,\n        child: child,\n      );\n\n      if (isMobileRowDetail) {\n        child = Container(\n          decoration: BoxDecoration(\n            border: Border.fromBorderSide(\n              BorderSide(color: Theme.of(context).colorScheme.outline),\n            ),\n            borderRadius: const BorderRadius.all(Radius.circular(14)),\n          ),\n          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),\n          alignment: AlignmentDirectional.centerStart,\n          child: child,\n        );\n      }\n\n      child = InkWell(\n        borderRadius:\n            isMobileRowDetail ? BorderRadius.circular(12) : BorderRadius.zero,\n        onTap: () => _tapCellMobile(context),\n        hoverColor: Colors.transparent,\n        child: child,\n      );\n    }\n\n    return BlocProvider.value(\n      value: bloc,\n      child: Builder(builder: (context) => child),\n    );\n  }\n\n  void _openOrExpandFile(\n    BuildContext context,\n    MediaFilePB file,\n    List<MediaFilePB> files,\n  ) {\n    if (file.fileType != MediaFileTypePB.Image) {\n      afLaunchUrlString(file.url, context: context);\n      return;\n    }\n\n    final images =\n        files.where((f) => f.fileType == MediaFileTypePB.Image).toList();\n    final index = images.indexOf(file);\n\n    showDialog(\n      context: context,\n      builder: (_) => InteractiveImageViewer(\n        userProfile: context.read<MediaCellBloc>().state.userProfile,\n        imageProvider: AFBlockImageProvider(\n          initialIndex: index,\n          images: images\n              .map(\n                (e) => ImageBlockData(\n                  url: e.url,\n                  type: e.uploadType.toCustomImageType(),\n                ),\n              )\n              .toList(),\n          onDeleteImage: (index) {\n            final deleteFile = images[index];\n            context.read<MediaCellBloc>().deleteFile(deleteFile.id);\n          },\n        ),\n      ),\n    );\n  }\n\n  void _tapCellMobile(BuildContext context) {\n    final files = context.read<MediaCellBloc>().state.files;\n\n    if (files.isEmpty) {\n      showMobileBottomSheet(\n        context,\n        title: LocaleKeys.grid_media_addFileMobile.tr(),\n        showHeader: true,\n        showCloseButton: true,\n        showDragHandle: true,\n        builder: (dContext) => BlocProvider.value(\n          value: context.read<MediaCellBloc>(),\n          child: MobileMediaUploadSheetContent(\n            dialogContext: dContext,\n          ),\n        ),\n      );\n      return;\n    }\n\n    showMobileBottomSheet(\n      context,\n      builder: (_) => BlocProvider.value(\n        value: context.read<MediaCellBloc>(),\n        child: const MobileMediaCellEditor(),\n      ),\n    );\n  }\n}\n\nclass _FilePreviewRender extends StatelessWidget {\n  const _FilePreviewRender({required this.file});\n\n  final MediaFilePB file;\n\n  @override\n  Widget build(BuildContext context) {\n    if (file.fileType != MediaFileTypePB.Image) {\n      return FlowyTooltip(\n        message: file.name,\n        child: Container(\n          height: 28,\n          width: 28,\n          clipBehavior: Clip.antiAlias,\n          padding: const EdgeInsets.symmetric(horizontal: 8),\n          decoration: BoxDecoration(\n            color: AFThemeExtension.of(context).greyHover,\n            borderRadius: BorderRadius.circular(4),\n          ),\n          child: FlowySvg(\n            file.fileType.icon,\n            size: const Size.square(12),\n            color: AFThemeExtension.of(context).textColor,\n          ),\n        ),\n      );\n    }\n\n    return FlowyTooltip(\n      message: file.name,\n      child: Container(\n        height: 28,\n        width: 28,\n        clipBehavior: Clip.antiAlias,\n        decoration: BoxDecoration(borderRadius: BorderRadius.circular(4)),\n        child: AFImage(\n          url: file.url,\n          uploadType: file.uploadType,\n          userProfile: context.read<MediaCellBloc>().state.userProfile,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_number_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_skeleton/number.dart';\n\nclass DesktopGridNumberCellSkin extends IEditableNumberCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    NumberCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return ValueListenableBuilder(\n      valueListenable: compactModeNotifier,\n      builder: (context, compactMode, _) {\n        final padding = compactMode\n            ? GridSize.compactCellContentInsets\n            : GridSize.cellContentInsets;\n\n        return TextField(\n          controller: textEditingController,\n          focusNode: focusNode,\n          onEditingComplete: () => focusNode.unfocus(),\n          onSubmitted: (_) => focusNode.unfocus(),\n          maxLines: context.watch<NumberCellBloc>().state.wrap ? null : 1,\n          style: Theme.of(context).textTheme.bodyMedium,\n          textInputAction: TextInputAction.done,\n          decoration: InputDecoration(\n            contentPadding: padding,\n            border: InputBorder.none,\n            focusedBorder: InputBorder.none,\n            enabledBorder: InputBorder.none,\n            errorBorder: InputBorder.none,\n            disabledBorder: InputBorder.none,\n            isDense: true,\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/relation_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_skeleton/relation.dart';\n\nclass DesktopGridRelationCellSkin extends IEditableRelationCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    RelationCellBloc bloc,\n    RelationCellState state,\n    PopoverController popoverController,\n  ) {\n    final userWorkspaceBloc = context.read<UserWorkspaceBloc>();\n    return AppFlowyPopover(\n      controller: popoverController,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      constraints: const BoxConstraints(maxWidth: 400, maxHeight: 400),\n      margin: EdgeInsets.zero,\n      onClose: () => cellContainerNotifier.isFocus = false,\n      popupBuilder: (context) {\n        return MultiBlocProvider(\n          providers: [\n            BlocProvider.value(value: userWorkspaceBloc),\n            BlocProvider.value(value: bloc),\n          ],\n          child: const RelationCellEditor(),\n        );\n      },\n      child: Align(\n        alignment: AlignmentDirectional.centerStart,\n        child: ValueListenableBuilder(\n          valueListenable: compactModeNotifier,\n          builder: (context, compactMode, _) {\n            return state.wrap\n                ? _buildWrapRows(context, state.rows, compactMode)\n                : _buildNoWrapRows(context, state.rows, compactMode);\n          },\n        ),\n      ),\n    );\n  }\n\n  Widget _buildWrapRows(\n    BuildContext context,\n    List<RelatedRowDataPB> rows,\n    bool compactMode,\n  ) {\n    return Padding(\n      padding: compactMode\n          ? GridSize.compactCellContentInsets\n          : GridSize.cellContentInsets,\n      child: Wrap(\n        runSpacing: 4,\n        spacing: 4.0,\n        children: rows.map(\n          (row) {\n            final isEmpty = row.name.isEmpty;\n            return FlowyText(\n              isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name,\n              color: isEmpty ? Theme.of(context).hintColor : null,\n              decoration: TextDecoration.underline,\n              overflow: TextOverflow.ellipsis,\n            );\n          },\n        ).toList(),\n      ),\n    );\n  }\n\n  Widget _buildNoWrapRows(\n    BuildContext context,\n    List<RelatedRowDataPB> rows,\n    bool compactMode,\n  ) {\n    return SingleChildScrollView(\n      physics: const NeverScrollableScrollPhysics(),\n      scrollDirection: Axis.horizontal,\n      child: Padding(\n        padding: GridSize.cellContentInsets,\n        child: SeparatedRow(\n          separatorBuilder: () => const HSpace(4.0),\n          mainAxisSize: MainAxisSize.min,\n          children: rows.map(\n            (row) {\n              final isEmpty = row.name.isEmpty;\n              return FlowyText(\n                isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name,\n                color: isEmpty ? Theme.of(context).hintColor : null,\n                decoration: TextDecoration.underline,\n                overflow: TextOverflow.ellipsis,\n              );\n            },\n          ).toList(),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_select_option_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_skeleton/select_option.dart';\n\nclass DesktopGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    SelectOptionCellBloc bloc,\n    PopoverController popoverController,\n  ) {\n    return AppFlowyPopover(\n      controller: popoverController,\n      constraints: const BoxConstraints.tightFor(width: 300),\n      margin: EdgeInsets.zero,\n      triggerActions: PopoverTriggerFlags.none,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      popupBuilder: (BuildContext popoverContext) {\n        return SelectOptionCellEditor(\n          cellController: bloc.cellController,\n        );\n      },\n      onClose: () => cellContainerNotifier.isFocus = false,\n      child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(\n        builder: (context, state) {\n          return Align(\n            alignment: AlignmentDirectional.centerStart,\n            child: state.wrap\n                ? _buildWrapOptions(\n                    context,\n                    state.selectedOptions,\n                    compactModeNotifier,\n                  )\n                : _buildNoWrapOptions(\n                    context,\n                    state.selectedOptions,\n                    compactModeNotifier,\n                  ),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildWrapOptions(\n    BuildContext context,\n    List<SelectOptionPB> options,\n    ValueNotifier<bool> compactModeNotifier,\n  ) {\n    return ValueListenableBuilder(\n      valueListenable: compactModeNotifier,\n      builder: (context, compactMode, _) {\n        final padding = compactMode\n            ? GridSize.compactCellContentInsets\n            : GridSize.cellContentInsets;\n        return Padding(\n          padding: padding,\n          child: Wrap(\n            runSpacing: 4,\n            children: options.map(\n              (option) {\n                return Padding(\n                  padding: const EdgeInsets.only(right: 4),\n                  child: SelectOptionTag(\n                    option: option,\n                    padding: EdgeInsets.symmetric(\n                      vertical: compactMode ? 2 : 4,\n                      horizontal: 8,\n                    ),\n                  ),\n                );\n              },\n            ).toList(),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildNoWrapOptions(\n    BuildContext context,\n    List<SelectOptionPB> options,\n    ValueNotifier<bool> compactModeNotifier,\n  ) {\n    return SingleChildScrollView(\n      physics: const NeverScrollableScrollPhysics(),\n      scrollDirection: Axis.horizontal,\n      child: ValueListenableBuilder(\n        valueListenable: compactModeNotifier,\n        builder: (context, compactMode, _) {\n          final padding = compactMode\n              ? GridSize.compactCellContentInsets\n              : GridSize.cellContentInsets;\n          return Padding(\n            padding: padding,\n            child: Row(\n              mainAxisSize: MainAxisSize.min,\n              children: options.map(\n                (option) {\n                  return Padding(\n                    padding: const EdgeInsets.only(right: 4),\n                    child: SelectOptionTag(\n                      option: option,\n                      padding: const EdgeInsets.symmetric(\n                        vertical: 1,\n                        horizontal: 8,\n                      ),\n                    ),\n                  );\n                },\n              ).toList(),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\nimport 'package:styled_widget/styled_widget.dart';\n\nclass DesktopGridSummaryCellSkin extends IEditableSummaryCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    SummaryCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return ChangeNotifierProvider(\n      create: (_) => SummaryMouseNotifier(),\n      builder: (context, child) {\n        return MouseRegion(\n          cursor: SystemMouseCursors.click,\n          opaque: false,\n          onEnter: (p) =>\n              Provider.of<SummaryMouseNotifier>(context, listen: false)\n                  .onEnter = true,\n          onExit: (p) =>\n              Provider.of<SummaryMouseNotifier>(context, listen: false)\n                  .onEnter = false,\n          child: ValueListenableBuilder(\n            valueListenable: compactModeNotifier,\n            builder: (context, compactMode, _) {\n              final padding = compactMode\n                  ? GridSize.compactCellContentInsets\n                  : GridSize.cellContentInsets;\n\n              return ConstrainedBox(\n                constraints: BoxConstraints(\n                  minHeight: compactMode\n                      ? GridSize.headerHeight - 4\n                      : GridSize.headerHeight,\n                ),\n                child: Stack(\n                  fit: StackFit.expand,\n                  children: [\n                    Center(\n                      child: TextField(\n                        controller: textEditingController,\n                        enabled: false,\n                        focusNode: focusNode,\n                        onEditingComplete: () => focusNode.unfocus(),\n                        onSubmitted: (_) => focusNode.unfocus(),\n                        maxLines: null,\n                        style: Theme.of(context).textTheme.bodyMedium,\n                        textInputAction: TextInputAction.done,\n                        decoration: InputDecoration(\n                          contentPadding: padding,\n                          border: InputBorder.none,\n                          focusedBorder: InputBorder.none,\n                          enabledBorder: InputBorder.none,\n                          errorBorder: InputBorder.none,\n                          disabledBorder: InputBorder.none,\n                          isDense: true,\n                        ),\n                      ),\n                    ),\n                    Padding(\n                      padding: EdgeInsets.symmetric(\n                        horizontal: GridSize.cellVPadding,\n                      ),\n                      child: Consumer<SummaryMouseNotifier>(\n                        builder: (\n                          BuildContext context,\n                          SummaryMouseNotifier notifier,\n                          Widget? child,\n                        ) {\n                          if (notifier.onEnter) {\n                            return SummaryCellAccessory(\n                              viewId: bloc.cellController.viewId,\n                              fieldId: bloc.cellController.fieldId,\n                              rowId: bloc.cellController.rowId,\n                            );\n                          } else {\n                            return const SizedBox.shrink();\n                          }\n                        },\n                      ),\n                    ).positioned(right: 0, bottom: compactMode ? 4 : 8),\n                  ],\n                ),\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass SummaryMouseNotifier extends ChangeNotifier {\n  SummaryMouseNotifier();\n\n  bool _onEnter = false;\n\n  set onEnter(bool value) {\n    if (_onEnter != value) {\n      _onEnter = value;\n      notifyListeners();\n    }\n  }\n\n  bool get onEnter => _onEnter;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_text_cell.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_skeleton/text.dart';\n\nclass DesktopGridTextCellSkin extends IEditableTextCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TextCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return ValueListenableBuilder(\n      valueListenable: compactModeNotifier,\n      builder: (context, compactMode, data) {\n        final padding = compactMode\n            ? GridSize.compactCellContentInsets\n            : GridSize.cellContentInsets;\n        return Padding(\n          padding: padding,\n          child: Row(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              const _IconOrEmoji(),\n              Expanded(\n                child: TextField(\n                  controller: textEditingController,\n                  focusNode: focusNode,\n                  maxLines: context.watch<TextCellBloc>().state.wrap ? null : 1,\n                  style: Theme.of(context).textTheme.bodyMedium?.copyWith(\n                        fontWeight: context\n                                .read<TextCellBloc>()\n                                .cellController\n                                .fieldInfo\n                                .isPrimary\n                            ? FontWeight.w500\n                            : null,\n                      ),\n                  decoration: const InputDecoration(\n                    border: InputBorder.none,\n                    focusedBorder: InputBorder.none,\n                    enabledBorder: InputBorder.none,\n                    errorBorder: InputBorder.none,\n                    disabledBorder: InputBorder.none,\n                    isDense: true,\n                    isCollapsed: true,\n                  ),\n                ),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass _IconOrEmoji extends StatelessWidget {\n  const _IconOrEmoji();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TextCellBloc, TextCellState>(\n      builder: (context, state) {\n        // if not a title cell, return empty widget\n        if (state.emoji == null || state.hasDocument == null) {\n          return const SizedBox.shrink();\n        }\n\n        return ValueListenableBuilder<String>(\n          valueListenable: state.emoji!,\n          builder: (context, emoji, _) {\n            return emoji.isNotEmpty\n                ? Padding(\n                    padding: const EdgeInsetsDirectional.only(end: 6.0),\n                    child: FlowyText.emoji(\n                      optimizeEmojiAlign: true,\n                      emoji,\n                    ),\n                  )\n                : ValueListenableBuilder<bool>(\n                    valueListenable: state.hasDocument!,\n                    builder: (context, hasDocument, _) {\n                      return hasDocument\n                          ? Padding(\n                              padding:\n                                  const EdgeInsetsDirectional.only(end: 6.0)\n                                      .add(const EdgeInsets.all(1)),\n                              child: FlowySvg(\n                                FlowySvgs.notes_s,\n                                color: Theme.of(context).hintColor,\n                              ),\n                            )\n                          : const SizedBox.shrink();\n                    },\n                  );\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_time_cell.dart",
    "content": "import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_skeleton/time.dart';\n\nclass DesktopGridTimeCellSkin extends IEditableTimeCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    TimeCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return TextField(\n      controller: textEditingController,\n      focusNode: focusNode,\n      onEditingComplete: () => focusNode.unfocus(),\n      onSubmitted: (_) => focusNode.unfocus(),\n      maxLines: context.watch<TimeCellBloc>().state.wrap ? null : 1,\n      style: Theme.of(context).textTheme.bodyMedium,\n      textInputAction: TextInputAction.done,\n      decoration: InputDecoration(\n        contentPadding: GridSize.cellContentInsets,\n        border: InputBorder.none,\n        focusedBorder: InputBorder.none,\n        enabledBorder: InputBorder.none,\n        errorBorder: InputBorder.none,\n        disabledBorder: InputBorder.none,\n        isDense: true,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_timestamp_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/widgets.dart';\n\nimport '../editable_cell_skeleton/timestamp.dart';\n\nclass DesktopGridTimestampCellSkin extends IEditableTimestampCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TimestampCellBloc bloc,\n    TimestampCellState state,\n  ) {\n    return Container(\n      alignment: AlignmentDirectional.centerStart,\n      child: state.wrap\n          ? _buildCellContent(state, compactModeNotifier)\n          : SingleChildScrollView(\n              physics: const NeverScrollableScrollPhysics(),\n              scrollDirection: Axis.horizontal,\n              child: _buildCellContent(state, compactModeNotifier),\n            ),\n    );\n  }\n\n  Widget _buildCellContent(\n    TimestampCellState state,\n    ValueNotifier<bool> compactModeNotifier,\n  ) {\n    return ValueListenableBuilder(\n      valueListenable: compactModeNotifier,\n      builder: (context, compactMode, _) {\n        final padding = compactMode\n            ? GridSize.compactCellContentInsets\n            : GridSize.cellContentInsets;\n        return Padding(\n          padding: padding,\n          child: FlowyText(\n            state.dateStr,\n            overflow: state.wrap ? null : TextOverflow.ellipsis,\n            maxLines: state.wrap ? null : 1,\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\nimport 'package:styled_widget/styled_widget.dart';\n\nclass DesktopGridTranslateCellSkin extends IEditableTranslateCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TranslateCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return ChangeNotifierProvider(\n      create: (_) => TranslateMouseNotifier(),\n      builder: (context, child) {\n        return ValueListenableBuilder(\n          valueListenable: compactModeNotifier,\n          builder: (context, compactMode, _) {\n            final padding = compactMode\n                ? GridSize.compactCellContentInsets\n                : GridSize.cellContentInsets;\n\n            return MouseRegion(\n              cursor: SystemMouseCursors.click,\n              opaque: false,\n              onEnter: (p) =>\n                  Provider.of<TranslateMouseNotifier>(context, listen: false)\n                      .onEnter = true,\n              onExit: (p) =>\n                  Provider.of<TranslateMouseNotifier>(context, listen: false)\n                      .onEnter = false,\n              child: ConstrainedBox(\n                constraints: BoxConstraints(\n                  minHeight: compactMode\n                      ? GridSize.headerHeight - 4\n                      : GridSize.headerHeight,\n                ),\n                child: Stack(\n                  fit: StackFit.expand,\n                  children: [\n                    Center(\n                      child: TextField(\n                        controller: textEditingController,\n                        readOnly: true,\n                        focusNode: focusNode,\n                        onEditingComplete: () => focusNode.unfocus(),\n                        onSubmitted: (_) => focusNode.unfocus(),\n                        maxLines: null,\n                        style: Theme.of(context).textTheme.bodyMedium,\n                        textInputAction: TextInputAction.done,\n                        decoration: InputDecoration(\n                          contentPadding: padding,\n                          border: InputBorder.none,\n                          focusedBorder: InputBorder.none,\n                          enabledBorder: InputBorder.none,\n                          errorBorder: InputBorder.none,\n                          disabledBorder: InputBorder.none,\n                          isDense: true,\n                        ),\n                      ),\n                    ),\n                    Padding(\n                      padding: EdgeInsets.symmetric(\n                        horizontal: GridSize.cellVPadding,\n                      ),\n                      child: Consumer<TranslateMouseNotifier>(\n                        builder: (\n                          BuildContext context,\n                          TranslateMouseNotifier notifier,\n                          Widget? child,\n                        ) {\n                          if (notifier.onEnter) {\n                            return TranslateCellAccessory(\n                              viewId: bloc.cellController.viewId,\n                              fieldId: bloc.cellController.fieldId,\n                              rowId: bloc.cellController.rowId,\n                            );\n                          } else {\n                            return const SizedBox.shrink();\n                          }\n                        },\n                      ),\n                    ).positioned(right: 0, bottom: compactMode ? 4 : 8),\n                  ],\n                ),\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass TranslateMouseNotifier extends ChangeNotifier {\n  TranslateMouseNotifier();\n\n  bool _onEnter = false;\n\n  set onEnter(bool value) {\n    if (_onEnter != value) {\n      _onEnter = value;\n      notifyListeners();\n    }\n  }\n\n  bool get onEnter => _onEnter;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy/workspace/presentation/home/toast.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_skeleton/url.dart';\n\nclass DesktopGridURLSkin extends IEditableURLCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    URLCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n    URLCellDataNotifier cellDataNotifier,\n  ) {\n    return BlocSelector<URLCellBloc, URLCellState, bool>(\n      selector: (state) => state.wrap,\n      builder: (context, wrap) => ValueListenableBuilder(\n        valueListenable: compactModeNotifier,\n        builder: (context, compactMode, _) {\n          final padding = compactMode\n              ? GridSize.compactCellContentInsets\n              : GridSize.cellContentInsets;\n          return TextField(\n            controller: textEditingController,\n            focusNode: focusNode,\n            maxLines: wrap ? null : 1,\n            style: Theme.of(context).textTheme.bodyMedium?.copyWith(\n                  color: Theme.of(context).colorScheme.primary,\n                  decoration: TextDecoration.underline,\n                ),\n            decoration: InputDecoration(\n              contentPadding: padding,\n              border: InputBorder.none,\n              focusedBorder: InputBorder.none,\n              enabledBorder: InputBorder.none,\n              errorBorder: InputBorder.none,\n              disabledBorder: InputBorder.none,\n              hintStyle: Theme.of(context)\n                  .textTheme\n                  .bodyMedium\n                  ?.copyWith(color: Theme.of(context).hintColor),\n              isDense: true,\n            ),\n            onTapOutside: (_) => focusNode.unfocus(),\n          );\n        },\n      ),\n    );\n  }\n\n  @override\n  List<GridCellAccessoryBuilder> accessoryBuilder(\n    GridCellAccessoryBuildContext context,\n    URLCellDataNotifier cellDataNotifier,\n  ) {\n    return [\n      accessoryFromType(\n        GridURLCellAccessoryType.visitURL,\n        cellDataNotifier,\n      ),\n      accessoryFromType(\n        GridURLCellAccessoryType.copyURL,\n        cellDataNotifier,\n      ),\n    ];\n  }\n}\n\nGridCellAccessoryBuilder accessoryFromType(\n  GridURLCellAccessoryType ty,\n  URLCellDataNotifier cellDataNotifier,\n) {\n  switch (ty) {\n    case GridURLCellAccessoryType.visitURL:\n      return VisitURLCellAccessoryBuilder(\n        builder: (Key key) => _VisitURLAccessory(\n          key: key,\n          cellDataNotifier: cellDataNotifier,\n        ),\n      );\n    case GridURLCellAccessoryType.copyURL:\n      return CopyURLCellAccessoryBuilder(\n        builder: (Key key) => _CopyURLAccessory(\n          key: key,\n          cellDataNotifier: cellDataNotifier,\n        ),\n      );\n  }\n}\n\nenum GridURLCellAccessoryType {\n  copyURL,\n  visitURL,\n}\n\ntypedef CopyURLCellAccessoryBuilder\n    = GridCellAccessoryBuilder<State<_CopyURLAccessory>>;\n\nclass _CopyURLAccessory extends StatefulWidget {\n  const _CopyURLAccessory({\n    super.key,\n    required this.cellDataNotifier,\n  });\n\n  final URLCellDataNotifier cellDataNotifier;\n\n  @override\n  State<_CopyURLAccessory> createState() => _CopyURLAccessoryState();\n}\n\nclass _CopyURLAccessoryState extends State<_CopyURLAccessory>\n    with GridCellAccessoryState {\n  @override\n  Widget build(BuildContext context) {\n    if (widget.cellDataNotifier.value.isNotEmpty) {\n      return FlowyTooltip(\n        message: LocaleKeys.grid_url_copy.tr(),\n        preferBelow: false,\n        child: _URLAccessoryIconContainer(\n          child: FlowySvg(\n            FlowySvgs.copy_s,\n            color: AFThemeExtension.of(context).textColor,\n          ),\n        ),\n      );\n    } else {\n      return const SizedBox.shrink();\n    }\n  }\n\n  @override\n  void onTap() {\n    final content = widget.cellDataNotifier.value;\n    if (content.isEmpty) {\n      return;\n    }\n    Clipboard.setData(ClipboardData(text: content));\n    showMessageToast(LocaleKeys.grid_row_copyProperty.tr());\n  }\n}\n\ntypedef VisitURLCellAccessoryBuilder\n    = GridCellAccessoryBuilder<State<_VisitURLAccessory>>;\n\nclass _VisitURLAccessory extends StatefulWidget {\n  const _VisitURLAccessory({\n    super.key,\n    required this.cellDataNotifier,\n  });\n\n  final URLCellDataNotifier cellDataNotifier;\n\n  @override\n  State<_VisitURLAccessory> createState() => _VisitURLAccessoryState();\n}\n\nclass _VisitURLAccessoryState extends State<_VisitURLAccessory>\n    with GridCellAccessoryState {\n  @override\n  Widget build(BuildContext context) {\n    if (widget.cellDataNotifier.value.isNotEmpty) {\n      return FlowyTooltip(\n        message: LocaleKeys.grid_url_launch.tr(),\n        preferBelow: false,\n        child: _URLAccessoryIconContainer(\n          child: FlowySvg(\n            FlowySvgs.url_s,\n            color: AFThemeExtension.of(context).textColor,\n          ),\n        ),\n      );\n    } else {\n      return const SizedBox.shrink();\n    }\n  }\n\n  @override\n  bool enable() => widget.cellDataNotifier.value.isNotEmpty;\n\n  @override\n  void onTap() => openUrlCellLink(widget.cellDataNotifier.value);\n}\n\nclass _URLAccessoryIconContainer extends StatelessWidget {\n  const _URLAccessoryIconContainer({required this.child});\n\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      width: 26,\n      height: 26,\n      decoration: BoxDecoration(\n        border: Border.fromBorderSide(\n          BorderSide(color: Theme.of(context).dividerColor),\n        ),\n        borderRadius: Corners.s6Border,\n      ),\n      child: FlowyHover(\n        style: HoverStyle(\n          backgroundColor: AFThemeExtension.of(context).background,\n          hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        ),\n        child: Center(\n          child: child,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checkbox_cell.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport '../editable_cell_skeleton/checkbox.dart';\n\nclass DesktopRowDetailCheckboxCellSkin extends IEditableCheckboxCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    CheckboxCellBloc bloc,\n    CheckboxCellState state,\n  ) {\n    return Container(\n      alignment: AlignmentDirectional.centerStart,\n      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),\n      child: FlowyIconButton(\n        hoverColor: Colors.transparent,\n        onPressed: () => bloc.add(const CheckboxCellEvent.select()),\n        icon: FlowySvg(\n          state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,\n          blendMode: BlendMode.dst,\n          size: const Size.square(20),\n        ),\n        width: 20,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_textfield.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../editable_cell_skeleton/checklist.dart';\n\nclass DesktopRowDetailChecklistCellSkin extends IEditableChecklistCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    ChecklistCellBloc bloc,\n    PopoverController popoverController,\n  ) {\n    return ChecklistRowDetailCell(\n      context: context,\n      cellContainerNotifier: cellContainerNotifier,\n      bloc: bloc,\n      popoverController: popoverController,\n    );\n  }\n}\n\nclass ChecklistRowDetailCell extends StatefulWidget {\n  const ChecklistRowDetailCell({\n    super.key,\n    required this.context,\n    required this.cellContainerNotifier,\n    required this.bloc,\n    required this.popoverController,\n  });\n\n  final BuildContext context;\n  final CellContainerNotifier cellContainerNotifier;\n  final ChecklistCellBloc bloc;\n  final PopoverController popoverController;\n\n  @override\n  State<ChecklistRowDetailCell> createState() => _ChecklistRowDetailCellState();\n}\n\nclass _ChecklistRowDetailCellState extends State<ChecklistRowDetailCell> {\n  final phantomTextController = TextEditingController();\n\n  @override\n  void dispose() {\n    phantomTextController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Align(\n      alignment: AlignmentDirectional.centerStart,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          ProgressAndHideCompleteButton(\n            onToggleHideComplete: () => context\n                .read<ChecklistCellBloc>()\n                .add(const ChecklistCellEvent.toggleShowIncompleteOnly()),\n          ),\n          const VSpace(2.0),\n          _ChecklistItems(\n            phantomTextController: phantomTextController,\n            onStartCreatingTaskAfter: (index) {\n              context\n                  .read<ChecklistCellBloc>()\n                  .add(ChecklistCellEvent.updatePhantomIndex(index + 1));\n            },\n          ),\n          ChecklistItemControl(\n            cellNotifer: widget.cellContainerNotifier,\n            onTap: () {\n              final bloc = context.read<ChecklistCellBloc>();\n              if (bloc.state.phantomIndex == null) {\n                bloc.add(\n                  ChecklistCellEvent.updatePhantomIndex(\n                    bloc.state.showIncompleteOnly\n                        ? bloc.state.tasks\n                            .where((task) => !task.isSelected)\n                            .length\n                        : bloc.state.tasks.length,\n                  ),\n                );\n              } else {\n                bloc.add(\n                  ChecklistCellEvent.createNewTask(\n                    phantomTextController.text,\n                    index: bloc.state.phantomIndex,\n                  ),\n                );\n              }\n              phantomTextController.clear();\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n\n@visibleForTesting\nclass ProgressAndHideCompleteButton extends StatelessWidget {\n  const ProgressAndHideCompleteButton({\n    super.key,\n    required this.onToggleHideComplete,\n  });\n\n  final VoidCallback onToggleHideComplete;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<ChecklistCellBloc, ChecklistCellState>(\n      buildWhen: (previous, current) =>\n          previous.showIncompleteOnly != current.showIncompleteOnly,\n      builder: (context, state) {\n        return Padding(\n          padding: const EdgeInsets.only(left: 8.0),\n          child: Row(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              Flexible(\n                child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(\n                  builder: (context, state) {\n                    return ChecklistProgressBar(\n                      tasks: state.tasks,\n                      percent: state.percent,\n                    );\n                  },\n                ),\n              ),\n              const HSpace(6.0),\n              FlowyIconButton(\n                tooltipText: state.showIncompleteOnly\n                    ? LocaleKeys.grid_checklist_showComplete.tr()\n                    : LocaleKeys.grid_checklist_hideComplete.tr(),\n                width: 32,\n                iconColorOnHover: Theme.of(context).colorScheme.onSurface,\n                icon: FlowySvg(\n                  state.showIncompleteOnly\n                      ? FlowySvgs.show_m\n                      : FlowySvgs.hide_m,\n                  size: const Size.square(16),\n                ),\n                onPressed: onToggleHideComplete,\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass _ChecklistItems extends StatelessWidget {\n  const _ChecklistItems({\n    required this.phantomTextController,\n    required this.onStartCreatingTaskAfter,\n  });\n\n  final TextEditingController phantomTextController;\n  final void Function(int index) onStartCreatingTaskAfter;\n\n  @override\n  Widget build(BuildContext context) {\n    return Actions(\n      actions: {\n        _CancelCreatingFromPhantomIntent:\n            CallbackAction<_CancelCreatingFromPhantomIntent>(\n          onInvoke: (_CancelCreatingFromPhantomIntent intent) {\n            phantomTextController.clear();\n            context\n                .read<ChecklistCellBloc>()\n                .add(const ChecklistCellEvent.updatePhantomIndex(null));\n            return;\n          },\n        ),\n      },\n      child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(\n        builder: (context, state) {\n          final children = _makeChildren(context, state);\n          return ReorderableListView.builder(\n            shrinkWrap: true,\n            physics: const NeverScrollableScrollPhysics(),\n            proxyDecorator: (child, index, _) => Material(\n              color: Colors.transparent,\n              child: MouseRegion(\n                cursor: UniversalPlatform.isWindows\n                    ? SystemMouseCursors.click\n                    : SystemMouseCursors.grabbing,\n                child: IgnorePointer(\n                  child: BlocProvider.value(\n                    value: context.read<ChecklistCellBloc>(),\n                    child: child,\n                  ),\n                ),\n              ),\n            ),\n            buildDefaultDragHandles: false,\n            itemCount: children.length,\n            itemBuilder: (_, index) => children[index],\n            onReorder: (from, to) {\n              context\n                  .read<ChecklistCellBloc>()\n                  .add(ChecklistCellEvent.reorderTask(from, to));\n            },\n          );\n        },\n      ),\n    );\n  }\n\n  List<Widget> _makeChildren(BuildContext context, ChecklistCellState state) {\n    final children = <Widget>[];\n\n    final tasks = [...state.tasks];\n\n    if (state.showIncompleteOnly) {\n      tasks.removeWhere((task) => task.isSelected);\n    }\n\n    children.addAll(\n      tasks.mapIndexed(\n        (index, task) => Padding(\n          key: ValueKey('checklist_row_detail_cell_task_${task.data.id}'),\n          padding: const EdgeInsets.symmetric(vertical: 2.0),\n          child: ChecklistItem(\n            task: task,\n            index: index,\n            onSubmitted: () {\n              onStartCreatingTaskAfter(index);\n            },\n          ),\n        ),\n      ),\n    );\n\n    if (state.phantomIndex != null) {\n      children.insert(\n        state.phantomIndex!,\n        Padding(\n          key: const ValueKey('new_checklist_cell_task'),\n          padding: const EdgeInsets.symmetric(vertical: 2.0),\n          child: PhantomChecklistItem(\n            index: state.phantomIndex!,\n            textController: phantomTextController,\n          ),\n        ),\n      );\n    }\n\n    return children;\n  }\n}\n\nclass _CancelCreatingFromPhantomIntent extends Intent {\n  const _CancelCreatingFromPhantomIntent();\n}\n\nclass _SubmitPhantomTaskIntent extends Intent {\n  const _SubmitPhantomTaskIntent({\n    required this.taskDescription,\n    required this.index,\n  });\n\n  final String taskDescription;\n  final int index;\n}\n\n@visibleForTesting\nclass PhantomChecklistItem extends StatefulWidget {\n  const PhantomChecklistItem({\n    super.key,\n    required this.index,\n    required this.textController,\n  });\n\n  final int index;\n  final TextEditingController textController;\n\n  @override\n  State<PhantomChecklistItem> createState() => _PhantomChecklistItemState();\n}\n\nclass _PhantomChecklistItemState extends State<PhantomChecklistItem> {\n  final focusNode = FocusNode();\n\n  bool isComposing = false;\n\n  @override\n  void initState() {\n    super.initState();\n    widget.textController.addListener(_onTextChanged);\n    focusNode.addListener(_onFocusChanged);\n    WidgetsBinding.instance\n        .addPostFrameCallback((_) => focusNode.requestFocus());\n  }\n\n  void _onTextChanged() => setState(\n        () => isComposing = !widget.textController.value.composing.isCollapsed,\n      );\n\n  void _onFocusChanged() {\n    if (!focusNode.hasFocus) {\n      widget.textController.clear();\n      Actions.maybeInvoke(\n        context,\n        const _CancelCreatingFromPhantomIntent(),\n      );\n    }\n  }\n\n  @override\n  void dispose() {\n    widget.textController.removeListener(_onTextChanged);\n    focusNode.removeListener(_onFocusChanged);\n    focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Actions(\n      actions: {\n        _SubmitPhantomTaskIntent: CallbackAction<_SubmitPhantomTaskIntent>(\n          onInvoke: (_SubmitPhantomTaskIntent intent) {\n            context.read<ChecklistCellBloc>().add(\n                  ChecklistCellEvent.createNewTask(\n                    intent.taskDescription,\n                    index: intent.index,\n                  ),\n                );\n            widget.textController.clear();\n            return;\n          },\n        ),\n      },\n      child: Shortcuts(\n        shortcuts: _buildShortcuts(),\n        child: Container(\n          constraints: const BoxConstraints(minHeight: 32),\n          decoration: BoxDecoration(\n            color: AFThemeExtension.of(context).lightGreyHover,\n            borderRadius: Corners.s6Border,\n          ),\n          child: Center(\n            child: ChecklistCellTextfield(\n              textController: widget.textController,\n              focusNode: focusNode,\n              contentPadding: const EdgeInsets.symmetric(\n                horizontal: 10,\n                vertical: 8,\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Map<ShortcutActivator, Intent> _buildShortcuts() {\n    return isComposing\n        ? const {}\n        : {\n            const SingleActivator(LogicalKeyboardKey.enter):\n                _SubmitPhantomTaskIntent(\n              taskDescription: widget.textController.text,\n              index: widget.index,\n            ),\n            const SingleActivator(LogicalKeyboardKey.escape):\n                const _CancelCreatingFromPhantomIntent(),\n          };\n  }\n}\n\n@visibleForTesting\nclass ChecklistItemControl extends StatelessWidget {\n  const ChecklistItemControl({\n    super.key,\n    required this.cellNotifer,\n    required this.onTap,\n  });\n\n  final CellContainerNotifier cellNotifer;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return ChangeNotifierProvider.value(\n      value: cellNotifer,\n      child: Consumer<CellContainerNotifier>(\n        builder: (buildContext, notifier, _) => TextFieldTapRegion(\n          child: GestureDetector(\n            behavior: HitTestBehavior.opaque,\n            onTap: onTap,\n            child: Container(\n              margin: const EdgeInsets.fromLTRB(8.0, 2.0, 8.0, 0),\n              height: 12,\n              child: AnimatedSwitcher(\n                duration: const Duration(milliseconds: 150),\n                child: notifier.isHover\n                    ? FlowyTooltip(\n                        message: LocaleKeys.grid_checklist_addNew.tr(),\n                        child: Row(\n                          children: [\n                            const Flexible(child: Center(child: Divider())),\n                            const HSpace(12.0),\n                            FilledButton(\n                              style: FilledButton.styleFrom(\n                                minimumSize: const Size.square(12),\n                                maximumSize: const Size.square(12),\n                                padding: EdgeInsets.zero,\n                              ),\n                              onPressed: onTap,\n                              child: FlowySvg(\n                                FlowySvgs.add_s,\n                                color: Theme.of(context).colorScheme.onPrimary,\n                              ),\n                            ),\n                            const HSpace(12.0),\n                            const Flexible(child: Center(child: Divider())),\n                          ],\n                        ),\n                      )\n                    : const SizedBox.expand(),\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_date_cell.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/date_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass DesktopRowDetailDateCellSkin extends IEditableDateCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    DateCellBloc bloc,\n    DateCellState state,\n    PopoverController popoverController,\n  ) {\n    final dateStr = getDateCellStrFromCellData(\n      state.fieldInfo,\n      state.cellData,\n    );\n    final text =\n        dateStr.isEmpty ? LocaleKeys.grid_row_textPlaceholder.tr() : dateStr;\n    final color = dateStr.isEmpty ? Theme.of(context).hintColor : null;\n\n    return AppFlowyPopover(\n      controller: popoverController,\n      triggerActions: PopoverTriggerFlags.none,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      constraints: BoxConstraints.loose(const Size(260, 620)),\n      margin: EdgeInsets.zero,\n      asBarrier: true,\n      child: Container(\n        alignment: AlignmentDirectional.centerStart,\n        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),\n        child: Row(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            Flexible(\n              child: FlowyText(\n                text,\n                color: color,\n                overflow: TextOverflow.ellipsis,\n              ),\n            ),\n            if (state.cellData.reminderId.isNotEmpty) ...[\n              const HSpace(4),\n              FlowyTooltip(\n                message: LocaleKeys.grid_field_reminderOnDateTooltip.tr(),\n                child: const FlowySvg(FlowySvgs.clock_alarm_s),\n              ),\n            ],\n          ],\n        ),\n      ),\n      popupBuilder: (BuildContext popoverContent) {\n        return DateCellEditor(\n          cellController: bloc.cellController,\n          onDismissed: () => cellContainerNotifier.isFocus = false,\n        );\n      },\n      onClose: () {\n        cellContainerNotifier.isFocus = false;\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_media_cell.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/media.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/media_file_type_ext.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_block_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/shared/af_image.dart';\nimport 'package:appflowy/util/xfile_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:cross_file/cross_file.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:reorderables/reorderables.dart';\n\nconst _dropFileKey = 'files_media';\nconst _itemWidth = 86.4;\n\nclass DekstopRowDetailMediaCellSkin extends IEditableMediaCellSkin {\n  final mutex = PopoverMutex();\n\n  @override\n  void dispose() {\n    mutex.dispose();\n  }\n\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    PopoverController popoverController,\n    MediaCellBloc bloc,\n  ) {\n    return BlocProvider.value(\n      value: bloc,\n      child: BlocBuilder<MediaCellBloc, MediaCellState>(\n        builder: (context, state) => LayoutBuilder(\n          builder: (context, constraints) {\n            if (state.files.isEmpty) {\n              return _AddFileButton(\n                controller: popoverController,\n                direction: PopoverDirection.bottomWithLeftAligned,\n                mutex: mutex,\n                child: Padding(\n                  padding: const EdgeInsets.symmetric(\n                    horizontal: 8,\n                    vertical: 6,\n                  ),\n                  child: FlowyText(\n                    LocaleKeys.grid_row_textPlaceholder.tr(),\n                    color: Theme.of(context).hintColor,\n                    overflow: TextOverflow.ellipsis,\n                  ),\n                ),\n              );\n            }\n\n            int itemsToShow = state.showAllFiles ? state.files.length : 0;\n            if (!state.showAllFiles) {\n              // The row width is surrounded by 8px padding on each side\n              final rowWidth = constraints.maxWidth - 16;\n\n              // Each item needs 94.4 px to render, 86.4px width + 8px runSpacing\n              final itemsPerRow = rowWidth ~/ (_itemWidth + 8);\n\n              // We show at most 2 rows\n              itemsToShow = itemsPerRow * 2;\n            }\n\n            final filesToDisplay =\n                state.showAllFiles || itemsToShow >= state.files.length\n                    ? state.files\n                    : state.files.take(itemsToShow - 1).toList();\n            final extraCount = state.files.length - itemsToShow;\n            final images = state.files\n                .where((f) => f.fileType == MediaFileTypePB.Image)\n                .toList();\n\n            final size = constraints.maxWidth / 2 - 6;\n            return _AddFileButton(\n              controller: popoverController,\n              mutex: mutex,\n              child: Column(\n                mainAxisSize: MainAxisSize.min,\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  Padding(\n                    padding: const EdgeInsets.all(8.0),\n                    child: ReorderableWrap(\n                      needsLongPressDraggable: false,\n                      runSpacing: 8,\n                      spacing: 8,\n                      onReorder: (from, to) => context\n                          .read<MediaCellBloc>()\n                          .add(MediaCellEvent.reorderFiles(from: from, to: to)),\n                      footer: extraCount > 0 && !state.showAllFiles\n                          ? GestureDetector(\n                              behavior: HitTestBehavior.opaque,\n                              onTap: () => _toggleShowAllFiles(context),\n                              child: _FilePreviewRender(\n                                key: ValueKey(state.files[itemsToShow - 1].id),\n                                file: state.files[itemsToShow - 1],\n                                index: 9,\n                                images: images,\n                                size: size,\n                                mutex: mutex,\n                                hideFileNames: state.hideFileNames,\n                                foregroundText: LocaleKeys.grid_media_extraCount\n                                    .tr(args: [extraCount.toString()]),\n                              ),\n                            )\n                          : null,\n                      buildDraggableFeedback: (_, __, child) =>\n                          BlocProvider.value(\n                        value: context.read<MediaCellBloc>(),\n                        child: _FilePreviewFeedback(child: child),\n                      ),\n                      children: filesToDisplay\n                          .mapIndexed(\n                            (index, file) => _FilePreviewRender(\n                              key: ValueKey(file.id),\n                              file: file,\n                              index: index,\n                              images: images,\n                              size: size,\n                              mutex: mutex,\n                              hideFileNames: state.hideFileNames,\n                            ),\n                          )\n                          .toList(),\n                    ),\n                  ),\n                  Padding(\n                    padding: const EdgeInsets.all(4),\n                    child: Row(\n                      mainAxisSize: MainAxisSize.min,\n                      children: [\n                        FlowySvg(\n                          FlowySvgs.add_thin_s,\n                          size: const Size.square(12),\n                          color: Theme.of(context).hintColor,\n                        ),\n                        const HSpace(6),\n                        FlowyText.medium(\n                          LocaleKeys.grid_media_addFileOrImage.tr(),\n                          fontSize: 12,\n                          color: Theme.of(context).hintColor,\n                          figmaLineHeight: 18,\n                        ),\n                      ],\n                    ),\n                  ),\n                ],\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  void _toggleShowAllFiles(BuildContext context) {\n    context\n        .read<MediaCellBloc>()\n        .add(const MediaCellEvent.toggleShowAllFiles());\n  }\n}\n\nclass _FilePreviewFeedback extends StatelessWidget {\n  const _FilePreviewFeedback({required this.child});\n\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return DecoratedBox(\n      position: DecorationPosition.foreground,\n      decoration: BoxDecoration(\n        borderRadius: BorderRadius.circular(6),\n        border: Border.all(\n          width: 2,\n          color: const Color(0xFF00BCF0),\n        ),\n      ),\n      child: DecoratedBox(\n        decoration: BoxDecoration(\n          boxShadow: [\n            BoxShadow(\n              color: const Color(0xFF1F2329).withValues(alpha: .2),\n              blurRadius: 6,\n              offset: const Offset(0, 3),\n            ),\n          ],\n        ),\n        child: BlocProvider.value(\n          value: context.read<MediaCellBloc>(),\n          child: Material(\n            type: MaterialType.transparency,\n            child: child,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nconst _menuWidth = 350.0;\n\nclass _AddFileButton extends StatefulWidget {\n  const _AddFileButton({\n    this.mutex,\n    required this.controller,\n    this.direction = PopoverDirection.bottomWithCenterAligned,\n    required this.child,\n  });\n\n  final PopoverController controller;\n  final PopoverMutex? mutex;\n  final PopoverDirection direction;\n  final Widget child;\n\n  @override\n  State<_AddFileButton> createState() => _AddFileButtonState();\n}\n\nclass _AddFileButtonState extends State<_AddFileButton> {\n  Offset? position;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      triggerActions: PopoverTriggerFlags.none,\n      controller: widget.controller,\n      mutex: widget.mutex,\n      offset: const Offset(0, 10),\n      direction: widget.direction,\n      constraints: const BoxConstraints(maxWidth: _menuWidth),\n      margin: EdgeInsets.zero,\n      asBarrier: true,\n      onClose: () =>\n          context.read<EditorDropManagerState>().remove(_dropFileKey),\n      popupBuilder: (_) {\n        WidgetsBinding.instance.addPostFrameCallback((_) {\n          context.read<EditorDropManagerState>().add(_dropFileKey);\n        });\n\n        return FileUploadMenu(\n          allowMultipleFiles: true,\n          onInsertLocalFile: (files) => insertLocalFiles(\n            context,\n            files,\n            userProfile: context.read<MediaCellBloc>().state.userProfile,\n            documentId: context.read<MediaCellBloc>().rowId,\n            onUploadSuccess: (file, path, isLocalMode) {\n              final mediaCellBloc = context.read<MediaCellBloc>();\n              if (mediaCellBloc.isClosed) {\n                return;\n              }\n\n              mediaCellBloc.add(\n                MediaCellEvent.addFile(\n                  url: path,\n                  name: file.name,\n                  uploadType: isLocalMode\n                      ? FileUploadTypePB.LocalFile\n                      : FileUploadTypePB.CloudFile,\n                  fileType: file.fileType.toMediaFileTypePB(),\n                ),\n              );\n\n              widget.controller.close();\n            },\n          ),\n          onInsertNetworkFile: (url) {\n            if (url.isEmpty) return;\n            final uri = Uri.tryParse(url);\n            if (uri == null) {\n              return;\n            }\n\n            final fakeFile = XFile(uri.path);\n            MediaFileTypePB fileType = fakeFile.fileType.toMediaFileTypePB();\n            fileType = fileType == MediaFileTypePB.Other\n                ? MediaFileTypePB.Link\n                : fileType;\n\n            String name =\n                uri.pathSegments.isNotEmpty ? uri.pathSegments.last : \"\";\n            if (name.isEmpty && uri.pathSegments.length > 1) {\n              name = uri.pathSegments[uri.pathSegments.length - 2];\n            } else if (name.isEmpty) {\n              name = uri.host;\n            }\n\n            context.read<MediaCellBloc>().add(\n                  MediaCellEvent.addFile(\n                    url: url,\n                    name: name,\n                    uploadType: FileUploadTypePB.NetworkFile,\n                    fileType: fileType,\n                  ),\n                );\n\n            widget.controller.close();\n          },\n        );\n      },\n      child: MouseRegion(\n        onEnter: (event) => position = event.position,\n        onExit: (_) => position = null,\n        onHover: (event) => position = event.position,\n        child: GestureDetector(\n          onTap: () {\n            if (position != null) {\n              widget.controller.showAt(\n                position! - const Offset(_menuWidth / 2, 0),\n              );\n            }\n          },\n          behavior: HitTestBehavior.translucent,\n          child: FlowyHover(resetHoverOnRebuild: false, child: widget.child),\n        ),\n      ),\n    );\n  }\n}\n\nclass _FilePreviewRender extends StatefulWidget {\n  const _FilePreviewRender({\n    super.key,\n    required this.file,\n    required this.images,\n    required this.index,\n    required this.size,\n    required this.mutex,\n    this.hideFileNames = false,\n    this.foregroundText,\n  });\n\n  final MediaFilePB file;\n  final List<MediaFilePB> images;\n  final int index;\n  final double size;\n  final PopoverMutex mutex;\n  final bool hideFileNames;\n  final String? foregroundText;\n\n  @override\n  State<_FilePreviewRender> createState() => _FilePreviewRenderState();\n}\n\nclass _FilePreviewRenderState extends State<_FilePreviewRender> {\n  final nameController = TextEditingController();\n  final controller = PopoverController();\n  bool isHovering = false;\n  bool isSelected = false;\n\n  late int thisIndex;\n\n  MediaFilePB get file => widget.file;\n\n  @override\n  void initState() {\n    super.initState();\n    thisIndex = widget.images.indexOf(file);\n  }\n\n  @override\n  void dispose() {\n    nameController.dispose();\n    controller.close();\n    super.dispose();\n  }\n\n  @override\n  void didUpdateWidget(covariant _FilePreviewRender oldWidget) {\n    thisIndex = widget.images.indexOf(file);\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child;\n    if (file.fileType == MediaFileTypePB.Image) {\n      child = AFImage(\n        url: file.url,\n        uploadType: file.uploadType,\n        userProfile: context.read<MediaCellBloc>().state.userProfile,\n        width: _itemWidth,\n        borderRadius: BorderRadius.only(\n          topLeft: Corners.s5Radius,\n          topRight: Corners.s5Radius,\n          bottomLeft: widget.hideFileNames ? Corners.s5Radius : Radius.zero,\n          bottomRight: widget.hideFileNames ? Corners.s5Radius : Radius.zero,\n        ),\n      );\n    } else {\n      child = DecoratedBox(\n        decoration: BoxDecoration(color: file.fileType.color),\n        child: Center(\n          child: Padding(\n            padding: const EdgeInsets.all(8),\n            child: FlowySvg(\n              file.fileType.icon,\n              color: const Color(0xFF666D76),\n            ),\n          ),\n        ),\n      );\n    }\n\n    if (widget.foregroundText != null) {\n      child = Stack(\n        children: [\n          Positioned.fill(\n            child: DecoratedBox(\n              position: DecorationPosition.foreground,\n              decoration:\n                  BoxDecoration(color: Colors.black.withValues(alpha: 0.5)),\n              child: child,\n            ),\n          ),\n          Positioned.fill(\n            child: Center(\n              child: FlowyText.semibold(\n                widget.foregroundText!,\n                color: Colors.white,\n                fontSize: 14,\n              ),\n            ),\n          ),\n        ],\n      );\n    }\n\n    return MouseRegion(\n      onEnter: (_) => setState(() => isHovering = true),\n      onExit: (_) => setState(() => isHovering = false),\n      child: FlowyTooltip(\n        message: file.name,\n        child: AppFlowyPopover(\n          controller: controller,\n          constraints: const BoxConstraints(maxWidth: 240),\n          offset: const Offset(0, 5),\n          triggerActions: PopoverTriggerFlags.none,\n          onClose: () => setState(() => isSelected = false),\n          asBarrier: true,\n          popupBuilder: (popoverContext) => MultiBlocProvider(\n            providers: [\n              BlocProvider.value(value: context.read<RowDetailBloc>()),\n              BlocProvider.value(value: context.read<MediaCellBloc>()),\n            ],\n            child: _FileMenu(\n              parentContext: context,\n              index: thisIndex,\n              file: file,\n              images: widget.images,\n              controller: controller,\n              nameController: nameController,\n            ),\n          ),\n          child: GestureDetector(\n            behavior: HitTestBehavior.translucent,\n            onTap: widget.foregroundText != null\n                ? null\n                : () {\n                    if (file.uploadType == FileUploadTypePB.LocalFile) {\n                      afLaunchUrlString(file.url);\n                      return;\n                    }\n\n                    if (file.fileType != MediaFileTypePB.Image) {\n                      afLaunchUrlString(widget.file.url);\n                      return;\n                    }\n\n                    openInteractiveViewerFromFiles(\n                      context,\n                      widget.images,\n                      userProfile:\n                          context.read<MediaCellBloc>().state.userProfile,\n                      initialIndex: thisIndex,\n                      onDeleteImage: (index) {\n                        final deleteFile = widget.images[index];\n                        context.read<MediaCellBloc>().deleteFile(deleteFile.id);\n                      },\n                    );\n                  },\n            child: Container(\n              width: _itemWidth,\n              clipBehavior: Clip.antiAlias,\n              decoration: BoxDecoration(\n                borderRadius: const BorderRadius.all(Corners.s6Radius),\n                border: Border.all(color: Theme.of(context).dividerColor),\n                color: Theme.of(context).cardColor,\n              ),\n              child: Stack(\n                children: [\n                  Column(\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      SizedBox(height: 68, child: child),\n                      if (!widget.hideFileNames)\n                        Row(\n                          children: [\n                            Expanded(\n                              child: Padding(\n                                padding: const EdgeInsets.all(4),\n                                child: FlowyText(\n                                  file.name,\n                                  fontSize: 10,\n                                  overflow: TextOverflow.ellipsis,\n                                  figmaLineHeight: 16,\n                                ),\n                              ),\n                            ),\n                          ],\n                        ),\n                    ],\n                  ),\n                  if (widget.foregroundText == null &&\n                      (isHovering || isSelected))\n                    Positioned(\n                      top: 3,\n                      right: 3,\n                      child: FlowyIconButton(\n                        onPressed: () {\n                          setState(() => isSelected = true);\n                          controller.show();\n                        },\n                        fillColor: Colors.black.withValues(alpha: 0.4),\n                        width: 18,\n                        radius: BorderRadius.circular(4),\n                        icon: const FlowySvg(\n                          FlowySvgs.three_dots_s,\n                          color: Colors.white,\n                          size: Size.square(16),\n                        ),\n                      ),\n                    ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _FileMenu extends StatefulWidget {\n  const _FileMenu({\n    required this.parentContext,\n    required this.index,\n    required this.file,\n    required this.images,\n    required this.controller,\n    required this.nameController,\n  });\n\n  /// Parent [BuildContext] used to retrieve the [MediaCellBloc]\n  final BuildContext parentContext;\n\n  /// Index of this file in [widget.images]\n  final int index;\n\n  /// The current [MediaFilePB] being previewed\n  final MediaFilePB file;\n\n  /// All images in the field, excluding non-image files-\n  final List<MediaFilePB> images;\n\n  /// The [PopoverController] to close the popover\n  final PopoverController controller;\n\n  /// The [TextEditingController] for renaming the file\n  final TextEditingController nameController;\n\n  @override\n  State<_FileMenu> createState() => _FileMenuState();\n}\n\nclass _FileMenuState extends State<_FileMenu> {\n  final errorMessage = ValueNotifier<String?>(null);\n\n  @override\n  void dispose() {\n    errorMessage.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SeparatedColumn(\n      separatorBuilder: () => const VSpace(8),\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        if (widget.file.fileType == MediaFileTypePB.Image) ...[\n          MediaMenuItem(\n            onTap: () {\n              widget.controller.close();\n              _showInteractiveViewer(context);\n            },\n            icon: FlowySvgs.full_view_s,\n            label: LocaleKeys.grid_media_expand.tr(),\n          ),\n          MediaMenuItem(\n            onTap: () {\n              widget.controller.close();\n              _setCover(context);\n            },\n            icon: FlowySvgs.cover_s,\n            label: LocaleKeys.grid_media_setAsCover.tr(),\n          ),\n        ],\n        MediaMenuItem(\n          onTap: () {\n            widget.controller.close();\n            afLaunchUrlString(widget.file.url);\n          },\n          icon: FlowySvgs.open_in_browser_s,\n          label: LocaleKeys.grid_media_openInBrowser.tr(),\n        ),\n        MediaMenuItem(\n          onTap: () {\n            widget.controller.close();\n            widget.nameController.text = widget.file.name;\n            widget.nameController.selection = TextSelection(\n              baseOffset: 0,\n              extentOffset: widget.nameController.text.length,\n            );\n\n            _showRenameConfirmDialog();\n          },\n          icon: FlowySvgs.rename_s,\n          label: LocaleKeys.grid_media_rename.tr(),\n        ),\n        if (widget.file.uploadType == FileUploadTypePB.CloudFile) ...[\n          MediaMenuItem(\n            onTap: () async => downloadMediaFile(\n              context,\n              widget.file,\n              userProfile: context.read<MediaCellBloc>().state.userProfile,\n            ),\n            icon: FlowySvgs.save_as_s,\n            label: LocaleKeys.button_download.tr(),\n          ),\n        ],\n        MediaMenuItem(\n          onTap: () {\n            widget.controller.close();\n            showConfirmDeletionDialog(\n              context: context,\n              name: widget.file.name,\n              description: LocaleKeys.grid_media_deleteFileDescription.tr(),\n              onConfirm: () => widget.parentContext\n                  .read<MediaCellBloc>()\n                  .add(MediaCellEvent.removeFile(fileId: widget.file.id)),\n            );\n          },\n          icon: FlowySvgs.trash_s,\n          label: LocaleKeys.button_delete.tr(),\n        ),\n      ],\n    );\n  }\n\n  void _saveName(BuildContext context) {\n    final newName = widget.nameController.text.trim();\n    if (newName.isEmpty) {\n      return;\n    }\n\n    context\n        .read<MediaCellBloc>()\n        .add(MediaCellEvent.renameFile(fileId: widget.file.id, name: newName));\n    Navigator.of(context).pop();\n  }\n\n  void _showRenameConfirmDialog() {\n    showCustomConfirmDialog(\n      context: widget.parentContext,\n      title: LocaleKeys.document_plugins_file_renameFile_title.tr(),\n      description: LocaleKeys.document_plugins_file_renameFile_description.tr(),\n      closeOnConfirm: false,\n      builder: (builderContext) => FileRenameTextField(\n        nameController: widget.nameController,\n        errorMessage: errorMessage,\n        onSubmitted: () => _saveName(widget.parentContext),\n        disposeController: false,\n      ),\n      style: ConfirmPopupStyle.cancelAndOk,\n      confirmLabel: LocaleKeys.button_save.tr(),\n      onConfirm: () => _saveName(widget.parentContext),\n      onCancel: Navigator.of(widget.parentContext).pop,\n    );\n  }\n\n  void _setCover(BuildContext context) => context.read<RowDetailBloc>().add(\n        RowDetailEvent.setCover(\n          RowCoverPB(\n            data: widget.file.url,\n            uploadType: widget.file.uploadType,\n            coverType: CoverTypePB.FileCover,\n          ),\n        ),\n      );\n\n  void _showInteractiveViewer(BuildContext context) => showDialog(\n        context: context,\n        builder: (_) => InteractiveImageViewer(\n          userProfile:\n              widget.parentContext.read<MediaCellBloc>().state.userProfile,\n          imageProvider: AFBlockImageProvider(\n            initialIndex: widget.index,\n            images: widget.images\n                .map(\n                  (e) => ImageBlockData(\n                    url: e.url,\n                    type: e.uploadType.toCustomImageType(),\n                  ),\n                )\n                .toList(),\n            onDeleteImage: (index) {\n              final deleteFile = widget.images[index];\n              widget.parentContext\n                  .read<MediaCellBloc>()\n                  .deleteFile(deleteFile.id);\n            },\n          ),\n        ),\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_number_cell.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nimport '../editable_cell_skeleton/number.dart';\n\nclass DesktopRowDetailNumberCellSkin extends IEditableNumberCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    NumberCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return TextField(\n      controller: textEditingController,\n      focusNode: focusNode,\n      onEditingComplete: () => focusNode.unfocus(),\n      onSubmitted: (_) => focusNode.unfocus(),\n      style: Theme.of(context).textTheme.bodyMedium,\n      textInputAction: TextInputAction.done,\n      decoration: InputDecoration(\n        contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 9),\n        border: InputBorder.none,\n        focusedBorder: InputBorder.none,\n        enabledBorder: InputBorder.none,\n        errorBorder: InputBorder.none,\n        disabledBorder: InputBorder.none,\n        hintText: LocaleKeys.grid_row_textPlaceholder.tr(),\n        hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(\n              color: Theme.of(context).hintColor,\n            ),\n        isDense: true,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/relation_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_skeleton/relation.dart';\n\nclass DesktopRowDetailRelationCellSkin extends IEditableRelationCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    RelationCellBloc bloc,\n    RelationCellState state,\n    PopoverController popoverController,\n  ) {\n    final userWorkspaceBloc = context.read<UserWorkspaceBloc>();\n    return AppFlowyPopover(\n      controller: popoverController,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      constraints: const BoxConstraints(maxWidth: 400, maxHeight: 400),\n      margin: EdgeInsets.zero,\n      asBarrier: true,\n      onClose: () => cellContainerNotifier.isFocus = false,\n      popupBuilder: (context) {\n        return MultiBlocProvider(\n          providers: [\n            BlocProvider.value(value: userWorkspaceBloc),\n            BlocProvider.value(value: bloc),\n          ],\n          child: const RelationCellEditor(),\n        );\n      },\n      child: Container(\n        alignment: AlignmentDirectional.centerStart,\n        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),\n        child: state.rows.isEmpty\n            ? _buildPlaceholder(context)\n            : _buildRows(context, state.rows),\n      ),\n    );\n  }\n\n  Widget _buildPlaceholder(BuildContext context) {\n    return FlowyText(\n      LocaleKeys.grid_row_textPlaceholder.tr(),\n      color: Theme.of(context).hintColor,\n    );\n  }\n\n  Widget _buildRows(BuildContext context, List<RelatedRowDataPB> rows) {\n    return Wrap(\n      runSpacing: 4.0,\n      spacing: 4.0,\n      children: rows.map(\n        (row) {\n          final isEmpty = row.name.isEmpty;\n          return FlowyText(\n            isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name,\n            color: isEmpty ? Theme.of(context).hintColor : null,\n            decoration: TextDecoration.underline,\n            overflow: TextOverflow.ellipsis,\n          );\n        },\n      ).toList(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_select_option_cell.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_skeleton/select_option.dart';\n\nclass DesktopRowDetailSelectOptionCellSkin\n    extends IEditableSelectOptionCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    SelectOptionCellBloc bloc,\n    PopoverController popoverController,\n  ) {\n    return AppFlowyPopover(\n      controller: popoverController,\n      constraints: const BoxConstraints.tightFor(width: 300),\n      margin: EdgeInsets.zero,\n      asBarrier: true,\n      triggerActions: PopoverTriggerFlags.none,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      onClose: () => cellContainerNotifier.isFocus = false,\n      onOpen: () => cellContainerNotifier.isFocus = true,\n      popupBuilder: (_) => SelectOptionCellEditor(\n        cellController: bloc.cellController,\n      ),\n      child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(\n        builder: (context, state) {\n          return Container(\n            alignment: AlignmentDirectional.centerStart,\n            padding: state.selectedOptions.isEmpty\n                ? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0)\n                : const EdgeInsets.symmetric(horizontal: 8.0, vertical: 5.0),\n            child: state.selectedOptions.isEmpty\n                ? _buildPlaceholder(context)\n                : _buildOptions(context, state.selectedOptions),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildPlaceholder(BuildContext context) {\n    return FlowyText(\n      LocaleKeys.grid_row_textPlaceholder.tr(),\n      color: Theme.of(context).hintColor,\n    );\n  }\n\n  Widget _buildOptions(BuildContext context, List<SelectOptionPB> options) {\n    return Wrap(\n      runSpacing: 4,\n      spacing: 4,\n      children: options.map(\n        (option) {\n          return SelectOptionTag(\n            option: option,\n            padding: const EdgeInsets.symmetric(\n              vertical: 4,\n              horizontal: 8,\n            ),\n          );\n        },\n      ).toList(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass DesktopRowDetailSummaryCellSkin extends IEditableSummaryCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    SummaryCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return Padding(\n      padding: const EdgeInsets.all(8.0),\n      child: Stack(\n        children: [\n          TextField(\n            controller: textEditingController,\n            readOnly: true,\n            focusNode: focusNode,\n            onEditingComplete: () => focusNode.unfocus(),\n            onSubmitted: (_) => focusNode.unfocus(),\n            style: Theme.of(context).textTheme.bodyMedium,\n            textInputAction: TextInputAction.done,\n            maxLines: null,\n            minLines: 1,\n            decoration: InputDecoration(\n              contentPadding: GridSize.cellContentInsets,\n              border: InputBorder.none,\n              focusedBorder: InputBorder.none,\n              enabledBorder: InputBorder.none,\n              errorBorder: InputBorder.none,\n              disabledBorder: InputBorder.none,\n              isDense: true,\n            ),\n          ),\n          ChangeNotifierProvider.value(\n            value: cellContainerNotifier,\n            child: Selector<CellContainerNotifier, bool>(\n              selector: (_, notifier) => notifier.isHover,\n              builder: (context, isHover, child) {\n                return Visibility(\n                  visible: isHover,\n                  child: Row(\n                    children: [\n                      const Spacer(),\n                      SummaryCellAccessory(\n                        viewId: bloc.cellController.viewId,\n                        fieldId: bloc.cellController.fieldId,\n                        rowId: bloc.cellController.rowId,\n                      ),\n                    ],\n                  ),\n                );\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_text_cell.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nimport '../editable_cell_skeleton/text.dart';\n\nclass DesktopRowDetailTextCellSkin extends IEditableTextCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TextCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return TextField(\n      controller: textEditingController,\n      focusNode: focusNode,\n      maxLines: null,\n      style: Theme.of(context).textTheme.bodyMedium,\n      decoration: InputDecoration(\n        contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 9),\n        border: InputBorder.none,\n        focusedBorder: InputBorder.none,\n        enabledBorder: InputBorder.none,\n        errorBorder: InputBorder.none,\n        disabledBorder: InputBorder.none,\n        hintText: LocaleKeys.grid_row_textPlaceholder.tr(),\n        hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(\n              color: Theme.of(context).hintColor,\n            ),\n        isDense: true,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_time_cell.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nimport '../editable_cell_skeleton/time.dart';\n\nclass DesktopRowDetailTimeCellSkin extends IEditableTimeCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    TimeCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return TextField(\n      controller: textEditingController,\n      focusNode: focusNode,\n      onEditingComplete: () => focusNode.unfocus(),\n      onSubmitted: (_) => focusNode.unfocus(),\n      style: Theme.of(context).textTheme.bodyMedium,\n      textInputAction: TextInputAction.done,\n      decoration: InputDecoration(\n        contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 9),\n        border: InputBorder.none,\n        focusedBorder: InputBorder.none,\n        enabledBorder: InputBorder.none,\n        errorBorder: InputBorder.none,\n        disabledBorder: InputBorder.none,\n        hintText: LocaleKeys.grid_row_textPlaceholder.tr(),\n        hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(\n              color: Theme.of(context).hintColor,\n            ),\n        isDense: true,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_timestamp_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/widgets.dart';\n\nimport '../editable_cell_skeleton/timestamp.dart';\n\nclass DesktopRowDetailTimestampCellSkin extends IEditableTimestampCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TimestampCellBloc bloc,\n    TimestampCellState state,\n  ) {\n    return Container(\n      alignment: AlignmentDirectional.centerStart,\n      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6.0),\n      child: FlowyText(\n        state.dateStr,\n        maxLines: null,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_url_cell.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nimport '../editable_cell_skeleton/url.dart';\n\nclass DesktopRowDetailURLSkin extends IEditableURLCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    URLCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n    URLCellDataNotifier cellDataNotifier,\n  ) {\n    return LinkTextField(\n      controller: textEditingController,\n      focusNode: focusNode,\n    );\n  }\n\n  @override\n  List<GridCellAccessoryBuilder> accessoryBuilder(\n    GridCellAccessoryBuildContext context,\n    URLCellDataNotifier cellDataNotifier,\n  ) {\n    return [\n      accessoryFromType(\n        GridURLCellAccessoryType.visitURL,\n        cellDataNotifier,\n      ),\n    ];\n  }\n}\n\nclass LinkTextField extends StatefulWidget {\n  const LinkTextField({\n    super.key,\n    required this.controller,\n    required this.focusNode,\n  });\n\n  final TextEditingController controller;\n  final FocusNode focusNode;\n\n  @override\n  State<LinkTextField> createState() => _LinkTextFieldState();\n}\n\nclass _LinkTextFieldState extends State<LinkTextField> {\n  bool isLinkClickable = false;\n\n  @override\n  void initState() {\n    super.initState();\n    HardwareKeyboard.instance.addHandler(_handleGlobalKeyEvent);\n  }\n\n  @override\n  void dispose() {\n    HardwareKeyboard.instance.removeHandler(_handleGlobalKeyEvent);\n    super.dispose();\n  }\n\n  bool _handleGlobalKeyEvent(KeyEvent event) {\n    final keyboard = HardwareKeyboard.instance;\n    final canOpenLink = event is KeyDownEvent &&\n        (keyboard.isControlPressed || keyboard.isMetaPressed);\n    if (canOpenLink != isLinkClickable) {\n      setState(() => isLinkClickable = canOpenLink);\n    }\n\n    return false;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return TextField(\n      mouseCursor:\n          isLinkClickable ? SystemMouseCursors.click : SystemMouseCursors.text,\n      controller: widget.controller,\n      focusNode: widget.focusNode,\n      style: Theme.of(context).textTheme.bodyMedium?.copyWith(\n            color: Theme.of(context).colorScheme.primary,\n            decoration: TextDecoration.underline,\n          ),\n      onTap: () {\n        if (isLinkClickable) {\n          openUrlCellLink(widget.controller.text);\n        }\n      },\n      decoration: InputDecoration(\n        contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 9),\n        border: InputBorder.none,\n        focusedBorder: InputBorder.none,\n        enabledBorder: InputBorder.none,\n        errorBorder: InputBorder.none,\n        disabledBorder: InputBorder.none,\n        hintText: LocaleKeys.grid_row_textPlaceholder.tr(),\n        hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(\n              color: Theme.of(context).hintColor,\n            ),\n        isDense: true,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass DesktopRowDetailTranslateCellSkin extends IEditableTranslateCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TranslateCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return Padding(\n      padding: const EdgeInsets.all(8.0),\n      child: Stack(\n        children: [\n          TextField(\n            controller: textEditingController,\n            focusNode: focusNode,\n            readOnly: true,\n            onEditingComplete: () => focusNode.unfocus(),\n            onSubmitted: (_) => focusNode.unfocus(),\n            style: Theme.of(context).textTheme.bodyMedium,\n            textInputAction: TextInputAction.done,\n            maxLines: null,\n            minLines: 1,\n            decoration: InputDecoration(\n              contentPadding: GridSize.cellContentInsets,\n              border: InputBorder.none,\n              focusedBorder: InputBorder.none,\n              enabledBorder: InputBorder.none,\n              errorBorder: InputBorder.none,\n              disabledBorder: InputBorder.none,\n              isDense: true,\n            ),\n          ),\n          ChangeNotifierProvider.value(\n            value: cellContainerNotifier,\n            child: Selector<CellContainerNotifier, bool>(\n              selector: (_, notifier) => notifier.isHover,\n              builder: (context, isHover, child) {\n                return Visibility(\n                  visible: isHover,\n                  child: Row(\n                    children: [\n                      const Spacer(),\n                      TranslateCellAccessory(\n                        viewId: bloc.cellController.viewId,\n                        fieldId: bloc.cellController.fieldId,\n                        rowId: bloc.cellController.rowId,\n                      ),\n                    ],\n                  ),\n                );\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/widgets.dart';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/media.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\n\nimport '../row/accessory/cell_accessory.dart';\nimport '../row/accessory/cell_shortcuts.dart';\nimport '../row/cells/cell_container.dart';\n\nimport 'editable_cell_skeleton/checkbox.dart';\nimport 'editable_cell_skeleton/checklist.dart';\nimport 'editable_cell_skeleton/date.dart';\nimport 'editable_cell_skeleton/number.dart';\nimport 'editable_cell_skeleton/relation.dart';\nimport 'editable_cell_skeleton/select_option.dart';\nimport 'editable_cell_skeleton/summary.dart';\nimport 'editable_cell_skeleton/text.dart';\nimport 'editable_cell_skeleton/time.dart';\nimport 'editable_cell_skeleton/timestamp.dart';\nimport 'editable_cell_skeleton/url.dart';\n\nenum EditableCellStyle {\n  desktopGrid,\n  desktopRowDetail,\n  mobileGrid,\n  mobileRowDetail,\n}\n\n/// Build an editable cell widget\nclass EditableCellBuilder {\n  EditableCellBuilder({required this.databaseController});\n\n  final DatabaseController databaseController;\n\n  EditableCellWidget buildStyled(\n    CellContext cellContext,\n    EditableCellStyle style,\n  ) {\n    final fieldType = databaseController.fieldController\n        .getField(cellContext.fieldId)!\n        .fieldType;\n    final key = ValueKey(\n      \"${databaseController.viewId}${cellContext.fieldId}${cellContext.rowId}\",\n    );\n    return switch (fieldType) {\n      FieldType.Checkbox => EditableCheckboxCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: IEditableCheckboxCellSkin.fromStyle(style),\n          key: key,\n        ),\n      FieldType.Checklist => EditableChecklistCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: IEditableChecklistCellSkin.fromStyle(style),\n          key: key,\n        ),\n      FieldType.CreatedTime => EditableTimestampCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: IEditableTimestampCellSkin.fromStyle(style),\n          key: key,\n          fieldType: FieldType.CreatedTime,\n        ),\n      FieldType.DateTime => EditableDateCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: IEditableDateCellSkin.fromStyle(style),\n          key: key,\n        ),\n      FieldType.LastEditedTime => EditableTimestampCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: IEditableTimestampCellSkin.fromStyle(style),\n          key: key,\n          fieldType: FieldType.LastEditedTime,\n        ),\n      FieldType.MultiSelect => EditableSelectOptionCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: IEditableSelectOptionCellSkin.fromStyle(style),\n          key: key,\n          fieldType: FieldType.MultiSelect,\n        ),\n      FieldType.Number => EditableNumberCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: IEditableNumberCellSkin.fromStyle(style),\n          key: key,\n        ),\n      FieldType.RichText => EditableTextCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: IEditableTextCellSkin.fromStyle(style),\n          key: key,\n        ),\n      FieldType.SingleSelect => EditableSelectOptionCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: IEditableSelectOptionCellSkin.fromStyle(style),\n          key: key,\n          fieldType: FieldType.SingleSelect,\n        ),\n      FieldType.URL => EditableURLCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: IEditableURLCellSkin.fromStyle(style),\n          key: key,\n        ),\n      FieldType.Relation => EditableRelationCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: IEditableRelationCellSkin.fromStyle(style),\n          key: key,\n        ),\n      FieldType.Summary => EditableSummaryCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: IEditableSummaryCellSkin.fromStyle(style),\n          key: key,\n        ),\n      FieldType.Time => EditableTimeCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: IEditableTimeCellSkin.fromStyle(style),\n          key: key,\n        ),\n      FieldType.Translate => EditableTranslateCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: IEditableTranslateCellSkin.fromStyle(style),\n          key: key,\n        ),\n      FieldType.Media => EditableMediaCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: IEditableMediaCellSkin.fromStyle(style),\n          style: style,\n          key: key,\n        ),\n      _ => throw UnimplementedError(),\n    };\n  }\n\n  EditableCellWidget buildCustom(\n    CellContext cellContext, {\n    required EditableCellSkinMap skinMap,\n  }) {\n    final DatabaseController(:fieldController) = databaseController;\n    final fieldType = fieldController.getField(cellContext.fieldId)!.fieldType;\n\n    final key = ValueKey(\n      \"${databaseController.viewId}${cellContext.fieldId}${cellContext.rowId}\",\n    );\n    assert(skinMap.has(fieldType));\n    return switch (fieldType) {\n      FieldType.Checkbox => EditableCheckboxCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: skinMap.checkboxSkin!,\n          key: key,\n        ),\n      FieldType.Checklist => EditableChecklistCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: skinMap.checklistSkin!,\n          key: key,\n        ),\n      FieldType.CreatedTime => EditableTimestampCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: skinMap.timestampSkin!,\n          key: key,\n          fieldType: FieldType.CreatedTime,\n        ),\n      FieldType.DateTime => EditableDateCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: skinMap.dateSkin!,\n          key: key,\n        ),\n      FieldType.LastEditedTime => EditableTimestampCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: skinMap.timestampSkin!,\n          key: key,\n          fieldType: FieldType.LastEditedTime,\n        ),\n      FieldType.MultiSelect => EditableSelectOptionCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: skinMap.selectOptionSkin!,\n          key: key,\n          fieldType: FieldType.MultiSelect,\n        ),\n      FieldType.Number => EditableNumberCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: skinMap.numberSkin!,\n          key: key,\n        ),\n      FieldType.RichText => EditableTextCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: skinMap.textSkin!,\n          key: key,\n        ),\n      FieldType.SingleSelect => EditableSelectOptionCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: skinMap.selectOptionSkin!,\n          key: key,\n          fieldType: FieldType.SingleSelect,\n        ),\n      FieldType.URL => EditableURLCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: skinMap.urlSkin!,\n          key: key,\n        ),\n      FieldType.Relation => EditableRelationCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: skinMap.relationSkin!,\n          key: key,\n        ),\n      FieldType.Time => EditableTimeCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: skinMap.timeSkin!,\n          key: key,\n        ),\n      FieldType.Media => EditableMediaCell(\n          databaseController: databaseController,\n          cellContext: cellContext,\n          skin: skinMap.mediaSkin!,\n          style: EditableCellStyle.desktopGrid,\n          key: key,\n        ),\n      _ => throw UnimplementedError(),\n    };\n  }\n}\n\nabstract class CellEditable {\n  SingleListenerChangeNotifier get requestFocus;\n\n  CellContainerNotifier get cellContainerNotifier;\n}\n\ntypedef AccessoryBuilder = List<GridCellAccessoryBuilder> Function(\n  GridCellAccessoryBuildContext buildContext,\n);\n\nabstract class CellAccessory extends Widget {\n  const CellAccessory({super.key});\n\n  AccessoryBuilder? get accessoryBuilder;\n}\n\nabstract class EditableCellWidget extends StatefulWidget\n    implements CellAccessory, CellEditable, CellShortcuts {\n  EditableCellWidget({super.key});\n\n  @override\n  final CellContainerNotifier cellContainerNotifier = CellContainerNotifier();\n\n  @override\n  AccessoryBuilder? get accessoryBuilder => null;\n\n  @override\n  final requestFocus = SingleListenerChangeNotifier();\n\n  @override\n  final Map<CellKeyboardKey, CellKeyboardAction> shortcutHandlers = {};\n}\n\nabstract class GridCellState<T extends EditableCellWidget> extends State<T> {\n  @override\n  void initState() {\n    super.initState();\n    widget.requestFocus.addListener(onRequestFocus);\n  }\n\n  @override\n  void didUpdateWidget(covariant T oldWidget) {\n    if (oldWidget != this) {\n      oldWidget.requestFocus.removeListener(onRequestFocus);\n      widget.requestFocus.addListener(onRequestFocus);\n    }\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  void dispose() {\n    widget.requestFocus.removeListener(onRequestFocus);\n    widget.requestFocus.dispose();\n    super.dispose();\n  }\n\n  /// Subclass can override this method to request focus.\n  void onRequestFocus();\n\n  String? onCopy() => null;\n}\n\nabstract class GridEditableTextCell<T extends EditableCellWidget>\n    extends GridCellState<T> {\n  SingleListenerFocusNode get focusNode;\n\n  @override\n  void initState() {\n    super.initState();\n    widget.shortcutHandlers[CellKeyboardKey.onEnter] =\n        () => focusNode.unfocus();\n    _listenOnFocusNodeChanged();\n  }\n\n  @override\n  void dispose() {\n    widget.shortcutHandlers.clear();\n    focusNode.removeAllListener();\n    focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  void onRequestFocus() {\n    if (!focusNode.hasFocus && focusNode.canRequestFocus) {\n      FocusScope.of(context).requestFocus(focusNode);\n    }\n  }\n\n  void _listenOnFocusNodeChanged() {\n    widget.cellContainerNotifier.isFocus = focusNode.hasFocus;\n    focusNode.setListener(() {\n      widget.cellContainerNotifier.isFocus = focusNode.hasFocus;\n      focusChanged();\n    });\n  }\n\n  Future<void> focusChanged() async {}\n}\n\nclass SingleListenerChangeNotifier extends ChangeNotifier {\n  VoidCallback? _listener;\n\n  @override\n  void addListener(VoidCallback listener) {\n    if (_listener != null) {\n      removeListener(_listener!);\n    }\n    _listener = listener;\n    super.addListener(listener);\n  }\n\n  @override\n  void dispose() {\n    _listener = null;\n    super.dispose();\n  }\n\n  void notify() => notifyListeners();\n}\n\nclass SingleListenerFocusNode extends FocusNode {\n  VoidCallback? _listener;\n\n  void setListener(VoidCallback listener) {\n    if (_listener != null) {\n      removeListener(_listener!);\n    }\n\n    _listener = listener;\n    super.addListener(listener);\n  }\n\n  void removeAllListener() {\n    if (_listener != null) {\n      removeListener(_listener!);\n    }\n  }\n\n  @override\n  void dispose() {\n    removeAllListener();\n    super.dispose();\n  }\n}\n\nclass EditableCellSkinMap {\n  EditableCellSkinMap({\n    this.checkboxSkin,\n    this.checklistSkin,\n    this.timestampSkin,\n    this.dateSkin,\n    this.selectOptionSkin,\n    this.numberSkin,\n    this.textSkin,\n    this.urlSkin,\n    this.relationSkin,\n    this.timeSkin,\n    this.mediaSkin,\n  });\n\n  final IEditableCheckboxCellSkin? checkboxSkin;\n  final IEditableChecklistCellSkin? checklistSkin;\n  final IEditableTimestampCellSkin? timestampSkin;\n  final IEditableDateCellSkin? dateSkin;\n  final IEditableSelectOptionCellSkin? selectOptionSkin;\n  final IEditableNumberCellSkin? numberSkin;\n  final IEditableTextCellSkin? textSkin;\n  final IEditableURLCellSkin? urlSkin;\n  final IEditableRelationCellSkin? relationSkin;\n  final IEditableTimeCellSkin? timeSkin;\n  final IEditableMediaCellSkin? mediaSkin;\n\n  bool has(FieldType fieldType) {\n    return switch (fieldType) {\n      FieldType.Checkbox => checkboxSkin != null,\n      FieldType.Checklist => checklistSkin != null,\n      FieldType.CreatedTime ||\n      FieldType.LastEditedTime =>\n        timestampSkin != null,\n      FieldType.DateTime => dateSkin != null,\n      FieldType.MultiSelect ||\n      FieldType.SingleSelect =>\n        selectOptionSkin != null,\n      FieldType.Number => numberSkin != null,\n      FieldType.RichText => textSkin != null,\n      FieldType.URL => urlSkin != null,\n      FieldType.Time => timeSkin != null,\n      FieldType.Media => mediaSkin != null,\n      _ => throw UnimplementedError(),\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../desktop_grid/desktop_grid_checkbox_cell.dart';\nimport '../desktop_row_detail/desktop_row_detail_checkbox_cell.dart';\nimport '../mobile_grid/mobile_grid_checkbox_cell.dart';\nimport '../mobile_row_detail/mobile_row_detail_checkbox_cell.dart';\n\nabstract class IEditableCheckboxCellSkin {\n  const IEditableCheckboxCellSkin();\n\n  factory IEditableCheckboxCellSkin.fromStyle(EditableCellStyle style) {\n    return switch (style) {\n      EditableCellStyle.desktopGrid => DesktopGridCheckboxCellSkin(),\n      EditableCellStyle.desktopRowDetail => DesktopRowDetailCheckboxCellSkin(),\n      EditableCellStyle.mobileGrid => MobileGridCheckboxCellSkin(),\n      EditableCellStyle.mobileRowDetail => MobileRowDetailCheckboxCellSkin(),\n    };\n  }\n\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    CheckboxCellBloc bloc,\n    CheckboxCellState state,\n  );\n}\n\nclass EditableCheckboxCell extends EditableCellWidget {\n  EditableCheckboxCell({\n    super.key,\n    required this.databaseController,\n    required this.cellContext,\n    required this.skin,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n  final IEditableCheckboxCellSkin skin;\n\n  @override\n  GridCellState<EditableCheckboxCell> createState() => _CheckboxCellState();\n}\n\nclass _CheckboxCellState extends GridCellState<EditableCheckboxCell> {\n  late final cellBloc = CheckboxCellBloc(\n    cellController: makeCellController(\n      widget.databaseController,\n      widget.cellContext,\n    ).as(),\n  )..add(const CheckboxCellEvent.initial());\n\n  @override\n  void dispose() {\n    cellBloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: cellBloc,\n      child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(\n        builder: (context, state) {\n          return widget.skin.build(\n            context,\n            widget.cellContainerNotifier,\n            widget.databaseController.compactModeNotifier,\n            cellBloc,\n            state,\n          );\n        },\n      ),\n    );\n  }\n\n  @override\n  void onRequestFocus() => cellBloc.add(const CheckboxCellEvent.select());\n\n  @override\n  String? onCopy() {\n    if (cellBloc.state.isSelected) {\n      return \"Yes\";\n    } else {\n      return \"No\";\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../desktop_grid/desktop_grid_checklist_cell.dart';\nimport '../desktop_row_detail/desktop_row_detail_checklist_cell.dart';\nimport '../mobile_grid/mobile_grid_checklist_cell.dart';\nimport '../mobile_row_detail/mobile_row_detail_checklist_cell.dart';\n\nabstract class IEditableChecklistCellSkin {\n  const IEditableChecklistCellSkin();\n\n  factory IEditableChecklistCellSkin.fromStyle(EditableCellStyle style) {\n    return switch (style) {\n      EditableCellStyle.desktopGrid => DesktopGridChecklistCellSkin(),\n      EditableCellStyle.desktopRowDetail => DesktopRowDetailChecklistCellSkin(),\n      EditableCellStyle.mobileGrid => MobileGridChecklistCellSkin(),\n      EditableCellStyle.mobileRowDetail => MobileRowDetailChecklistCellSkin(),\n    };\n  }\n\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    ChecklistCellBloc bloc,\n    PopoverController popoverController,\n  );\n}\n\nclass EditableChecklistCell extends EditableCellWidget {\n  EditableChecklistCell({\n    super.key,\n    required this.databaseController,\n    required this.cellContext,\n    required this.skin,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n  final IEditableChecklistCellSkin skin;\n\n  @override\n  GridCellState<EditableChecklistCell> createState() =>\n      GridChecklistCellState();\n}\n\nclass GridChecklistCellState extends GridCellState<EditableChecklistCell> {\n  final PopoverController _popover = PopoverController();\n  late final cellBloc = ChecklistCellBloc(\n    cellController: makeCellController(\n      widget.databaseController,\n      widget.cellContext,\n    ).as(),\n  );\n\n  @override\n  void dispose() {\n    cellBloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: cellBloc,\n      child: widget.skin.build(\n        context,\n        widget.cellContainerNotifier,\n        widget.databaseController.compactModeNotifier,\n        cellBloc,\n        _popover,\n      ),\n    );\n  }\n\n  @override\n  void onRequestFocus() {\n    if (widget.skin is DesktopGridChecklistCellSkin) {\n      _popover.show();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/date.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:intl/intl.dart';\n\nimport '../desktop_grid/desktop_grid_date_cell.dart';\nimport '../desktop_row_detail/desktop_row_detail_date_cell.dart';\nimport '../mobile_grid/mobile_grid_date_cell.dart';\nimport '../mobile_row_detail/mobile_row_detail_date_cell.dart';\n\nabstract class IEditableDateCellSkin {\n  const IEditableDateCellSkin();\n\n  factory IEditableDateCellSkin.fromStyle(EditableCellStyle style) {\n    return switch (style) {\n      EditableCellStyle.desktopGrid => DesktopGridDateCellSkin(),\n      EditableCellStyle.desktopRowDetail => DesktopRowDetailDateCellSkin(),\n      EditableCellStyle.mobileGrid => MobileGridDateCellSkin(),\n      EditableCellStyle.mobileRowDetail => MobileRowDetailDateCellSkin(),\n    };\n  }\n\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    DateCellBloc bloc,\n    DateCellState state,\n    PopoverController popoverController,\n  );\n}\n\nclass EditableDateCell extends EditableCellWidget {\n  EditableDateCell({\n    super.key,\n    required this.databaseController,\n    required this.cellContext,\n    required this.skin,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n  final IEditableDateCellSkin skin;\n\n  @override\n  GridCellState<EditableDateCell> createState() => _DateCellState();\n}\n\nclass _DateCellState extends GridCellState<EditableDateCell> {\n  final PopoverController _popover = PopoverController();\n  late final cellBloc = DateCellBloc(\n    cellController: makeCellController(\n      widget.databaseController,\n      widget.cellContext,\n    ).as(),\n  );\n\n  @override\n  void dispose() {\n    cellBloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: cellBloc,\n      child: BlocBuilder<DateCellBloc, DateCellState>(\n        builder: (context, state) {\n          return widget.skin.build(\n            context,\n            widget.cellContainerNotifier,\n            widget.databaseController.compactModeNotifier,\n            cellBloc,\n            state,\n            _popover,\n          );\n        },\n      ),\n    );\n  }\n\n  @override\n  void onRequestFocus() {\n    _popover.show();\n    widget.cellContainerNotifier.isFocus = true;\n  }\n\n  @override\n  String? onCopy() => getDateCellStrFromCellData(\n        cellBloc.state.fieldInfo,\n        cellBloc.state.cellData,\n      );\n}\n\nString getDateCellStrFromCellData(FieldInfo field, DateCellData cellData) {\n  if (cellData.dateTime == null) {\n    return \"\";\n  }\n\n  final DateTypeOptionPB(:dateFormat, :timeFormat) =\n      DateTypeOptionDataParser().fromBuffer(field.field.typeOptionData);\n\n  final format = cellData.includeTime\n      ? DateFormat(\"${dateFormat.pattern} ${timeFormat.pattern}\")\n      : DateFormat(dateFormat.pattern);\n\n  if (cellData.isRange) {\n    return \"${format.format(cellData.dateTime!)} → ${format.format(cellData.endDateTime!)}\";\n  } else {\n    return format.format(cellData.dateTime!);\n  }\n}\n\nextension GetDateFormatExtension on DateFormatPB {\n  String get pattern => switch (this) {\n        DateFormatPB.Local => 'MM/dd/y',\n        DateFormatPB.US => 'y/MM/dd',\n        DateFormatPB.ISO => 'y-MM-dd',\n        DateFormatPB.Friendly => 'MMM dd, y',\n        DateFormatPB.DayMonthYear => 'dd/MM/y',\n        _ => 'MMM dd, y',\n      };\n}\n\nextension GetTimeFormatExtension on TimeFormatPB {\n  String get pattern => switch (this) {\n        TimeFormatPB.TwelveHour => 'hh:mm a',\n        _ => 'HH:mm',\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/media.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_media_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_media_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../../application/cell/cell_controller_builder.dart';\n\nabstract class IEditableMediaCellSkin {\n  const IEditableMediaCellSkin();\n\n  factory IEditableMediaCellSkin.fromStyle(EditableCellStyle style) {\n    return switch (style) {\n      EditableCellStyle.desktopGrid => const GridMediaCellSkin(),\n      EditableCellStyle.desktopRowDetail => DekstopRowDetailMediaCellSkin(),\n      EditableCellStyle.mobileGrid => const GridMediaCellSkin(),\n      EditableCellStyle.mobileRowDetail =>\n        const GridMediaCellSkin(isMobileRowDetail: true),\n    };\n  }\n\n  bool autoShowPopover(EditableCellStyle style) => switch (style) {\n        EditableCellStyle.desktopGrid => true,\n        EditableCellStyle.desktopRowDetail => false,\n        EditableCellStyle.mobileGrid => false,\n        EditableCellStyle.mobileRowDetail => false,\n      };\n\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    PopoverController popoverController,\n    MediaCellBloc bloc,\n  );\n\n  void dispose();\n}\n\nclass EditableMediaCell extends EditableCellWidget {\n  EditableMediaCell({\n    super.key,\n    required this.databaseController,\n    required this.cellContext,\n    required this.skin,\n    required this.style,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n  final IEditableMediaCellSkin skin;\n  final EditableCellStyle style;\n\n  @override\n  GridEditableTextCell<EditableMediaCell> createState() =>\n      _EditableMediaCellState();\n}\n\nclass _EditableMediaCellState extends GridEditableTextCell<EditableMediaCell> {\n  final PopoverController popoverController = PopoverController();\n\n  late final cellBloc = MediaCellBloc(\n    cellController: makeCellController(\n      widget.databaseController,\n      widget.cellContext,\n    ).as(),\n  );\n\n  @override\n  void dispose() {\n    cellBloc.close();\n    widget.skin.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: cellBloc..add(const MediaCellEvent.initial()),\n      child: Builder(\n        builder: (context) => widget.skin.build(\n          context,\n          widget.cellContainerNotifier,\n          popoverController,\n          cellBloc,\n        ),\n      ),\n    );\n  }\n\n  @override\n  SingleListenerFocusNode focusNode = SingleListenerFocusNode();\n\n  @override\n  void onRequestFocus() => widget.skin.autoShowPopover(widget.style)\n      ? popoverController.show()\n      : null;\n\n  @override\n  String? onCopy() => null;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/number.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../desktop_grid/desktop_grid_number_cell.dart';\nimport '../desktop_row_detail/desktop_row_detail_number_cell.dart';\nimport '../mobile_grid/mobile_grid_number_cell.dart';\nimport '../mobile_row_detail/mobile_row_detail_number_cell.dart';\n\nabstract class IEditableNumberCellSkin {\n  const IEditableNumberCellSkin();\n\n  factory IEditableNumberCellSkin.fromStyle(EditableCellStyle style) {\n    return switch (style) {\n      EditableCellStyle.desktopGrid => DesktopGridNumberCellSkin(),\n      EditableCellStyle.desktopRowDetail => DesktopRowDetailNumberCellSkin(),\n      EditableCellStyle.mobileGrid => MobileGridNumberCellSkin(),\n      EditableCellStyle.mobileRowDetail => MobileRowDetailNumberCellSkin(),\n    };\n  }\n\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    NumberCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  );\n}\n\nclass EditableNumberCell extends EditableCellWidget {\n  EditableNumberCell({\n    super.key,\n    required this.databaseController,\n    required this.cellContext,\n    required this.skin,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n  final IEditableNumberCellSkin skin;\n\n  @override\n  GridEditableTextCell<EditableNumberCell> createState() => _NumberCellState();\n}\n\nclass _NumberCellState extends GridEditableTextCell<EditableNumberCell> {\n  late final TextEditingController _textEditingController;\n  late final cellBloc = NumberCellBloc(\n    cellController: makeCellController(\n      widget.databaseController,\n      widget.cellContext,\n    ).as(),\n  );\n\n  @override\n  void initState() {\n    super.initState();\n    _textEditingController =\n        TextEditingController(text: cellBloc.state.content);\n  }\n\n  @override\n  void dispose() {\n    _textEditingController.dispose();\n    cellBloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: cellBloc,\n      child: BlocListener<NumberCellBloc, NumberCellState>(\n        listener: (context, state) {\n          if (!focusNode.hasFocus) {\n            _textEditingController.text = state.content;\n          }\n        },\n        child: Builder(\n          builder: (context) {\n            return widget.skin.build(\n              context,\n              widget.cellContainerNotifier,\n              widget.databaseController.compactModeNotifier,\n              cellBloc,\n              focusNode,\n              _textEditingController,\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  @override\n  SingleListenerFocusNode focusNode = SingleListenerFocusNode();\n\n  @override\n  void onRequestFocus() {\n    focusNode.requestFocus();\n  }\n\n  @override\n  String? onCopy() => cellBloc.state.content;\n\n  @override\n  Future<void> focusChanged() async {\n    if (mounted &&\n        !cellBloc.isClosed &&\n        cellBloc.state.content != _textEditingController.text.trim()) {\n      cellBloc\n          .add(NumberCellEvent.updateCell(_textEditingController.text.trim()));\n    }\n    return super.focusChanged();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/relation.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../desktop_grid/desktop_grid_relation_cell.dart';\nimport '../desktop_row_detail/desktop_row_detail_relation_cell.dart';\nimport '../mobile_grid/mobile_grid_relation_cell.dart';\nimport '../mobile_row_detail/mobile_row_detail_relation_cell.dart';\n\nabstract class IEditableRelationCellSkin {\n  factory IEditableRelationCellSkin.fromStyle(EditableCellStyle style) {\n    return switch (style) {\n      EditableCellStyle.desktopGrid => DesktopGridRelationCellSkin(),\n      EditableCellStyle.desktopRowDetail => DesktopRowDetailRelationCellSkin(),\n      EditableCellStyle.mobileGrid => MobileGridRelationCellSkin(),\n      EditableCellStyle.mobileRowDetail => MobileRowDetailRelationCellSkin(),\n    };\n  }\n\n  const IEditableRelationCellSkin();\n\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    RelationCellBloc bloc,\n    RelationCellState state,\n    PopoverController popoverController,\n  );\n}\n\nclass EditableRelationCell extends EditableCellWidget {\n  EditableRelationCell({\n    super.key,\n    required this.databaseController,\n    required this.cellContext,\n    required this.skin,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n  final IEditableRelationCellSkin skin;\n\n  @override\n  GridCellState<EditableRelationCell> createState() => _RelationCellState();\n}\n\nclass _RelationCellState extends GridCellState<EditableRelationCell> {\n  final PopoverController _popover = PopoverController();\n  late final cellBloc = RelationCellBloc(\n    cellController: makeCellController(\n      widget.databaseController,\n      widget.cellContext,\n    ).as(),\n  );\n\n  @override\n  void dispose() {\n    cellBloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: cellBloc,\n      child: BlocBuilder<RelationCellBloc, RelationCellState>(\n        builder: (context, state) {\n          return widget.skin.build(\n            context,\n            widget.cellContainerNotifier,\n            widget.databaseController.compactModeNotifier,\n            cellBloc,\n            state,\n            _popover,\n          );\n        },\n      ),\n    );\n  }\n\n  @override\n  void onRequestFocus() {\n    _popover.show();\n    widget.cellContainerNotifier.isFocus = true;\n  }\n\n  @override\n  String? onCopy() => \"\";\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../desktop_grid/desktop_grid_select_option_cell.dart';\nimport '../desktop_row_detail/desktop_row_detail_select_option_cell.dart';\nimport '../mobile_grid/mobile_grid_select_option_cell.dart';\nimport '../mobile_row_detail/mobile_row_detail_select_cell_option.dart';\n\nabstract class IEditableSelectOptionCellSkin {\n  const IEditableSelectOptionCellSkin();\n\n  factory IEditableSelectOptionCellSkin.fromStyle(EditableCellStyle style) {\n    return switch (style) {\n      EditableCellStyle.desktopGrid => DesktopGridSelectOptionCellSkin(),\n      EditableCellStyle.desktopRowDetail =>\n        DesktopRowDetailSelectOptionCellSkin(),\n      EditableCellStyle.mobileGrid => MobileGridSelectOptionCellSkin(),\n      EditableCellStyle.mobileRowDetail =>\n        MobileRowDetailSelectOptionCellSkin(),\n    };\n  }\n\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    SelectOptionCellBloc bloc,\n    PopoverController popoverController,\n  );\n}\n\nclass EditableSelectOptionCell extends EditableCellWidget {\n  EditableSelectOptionCell({\n    super.key,\n    required this.databaseController,\n    required this.cellContext,\n    required this.skin,\n    required this.fieldType,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n  final IEditableSelectOptionCellSkin skin;\n\n  final FieldType fieldType;\n\n  @override\n  GridCellState<EditableSelectOptionCell> createState() =>\n      _SelectOptionCellState();\n}\n\nclass _SelectOptionCellState extends GridCellState<EditableSelectOptionCell> {\n  final PopoverController _popover = PopoverController();\n\n  late final cellBloc = SelectOptionCellBloc(\n    cellController: makeCellController(\n      widget.databaseController,\n      widget.cellContext,\n    ).as(),\n  );\n\n  @override\n  void dispose() {\n    cellBloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: cellBloc,\n      child: widget.skin.build(\n        context,\n        widget.cellContainerNotifier,\n        widget.databaseController.compactModeNotifier,\n        cellBloc,\n        _popover,\n      ),\n    );\n  }\n\n  @override\n  void onRequestFocus() => _popover.show();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/summary_row_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy/workspace/presentation/home/toast.dart';\nimport 'package:appflowy_backend/dispatch/error.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/icon_button.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nabstract class IEditableSummaryCellSkin {\n  const IEditableSummaryCellSkin();\n\n  factory IEditableSummaryCellSkin.fromStyle(EditableCellStyle style) {\n    return switch (style) {\n      EditableCellStyle.desktopGrid => DesktopGridSummaryCellSkin(),\n      EditableCellStyle.desktopRowDetail => DesktopRowDetailSummaryCellSkin(),\n      EditableCellStyle.mobileGrid => MobileGridSummaryCellSkin(),\n      EditableCellStyle.mobileRowDetail => MobileRowDetailSummaryCellSkin(),\n    };\n  }\n\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    SummaryCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  );\n}\n\nclass EditableSummaryCell extends EditableCellWidget {\n  EditableSummaryCell({\n    super.key,\n    required this.databaseController,\n    required this.cellContext,\n    required this.skin,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n  final IEditableSummaryCellSkin skin;\n\n  @override\n  GridEditableTextCell<EditableSummaryCell> createState() =>\n      _SummaryCellState();\n}\n\nclass _SummaryCellState extends GridEditableTextCell<EditableSummaryCell> {\n  late final TextEditingController _textEditingController;\n  late final cellBloc = SummaryCellBloc(\n    cellController: makeCellController(\n      widget.databaseController,\n      widget.cellContext,\n    ).as(),\n  );\n\n  @override\n  void initState() {\n    super.initState();\n    _textEditingController =\n        TextEditingController(text: cellBloc.state.content);\n  }\n\n  @override\n  void dispose() {\n    _textEditingController.dispose();\n    cellBloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: cellBloc,\n      child: BlocListener<SummaryCellBloc, SummaryCellState>(\n        listener: (context, state) {\n          _textEditingController.text = state.content;\n        },\n        child: Builder(\n          builder: (context) {\n            return widget.skin.build(\n              context,\n              widget.cellContainerNotifier,\n              widget.databaseController.compactModeNotifier,\n              cellBloc,\n              focusNode,\n              _textEditingController,\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  @override\n  SingleListenerFocusNode focusNode = SingleListenerFocusNode();\n\n  @override\n  void onRequestFocus() {\n    focusNode.requestFocus();\n  }\n\n  @override\n  String? onCopy() => cellBloc.state.content;\n\n  @override\n  Future<void> focusChanged() {\n    if (mounted &&\n        !cellBloc.isClosed &&\n        cellBloc.state.content != _textEditingController.text.trim()) {\n      cellBloc\n          .add(SummaryCellEvent.updateCell(_textEditingController.text.trim()));\n    }\n    return super.focusChanged();\n  }\n}\n\nclass SummaryCellAccessory extends StatelessWidget {\n  const SummaryCellAccessory({\n    required this.viewId,\n    required this.rowId,\n    required this.fieldId,\n    super.key,\n  });\n\n  final String viewId;\n  final String rowId;\n  final String fieldId;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => SummaryRowBloc(\n        viewId: viewId,\n        rowId: rowId,\n        fieldId: fieldId,\n      ),\n      child: BlocConsumer<SummaryRowBloc, SummaryRowState>(\n        listenWhen: (previous, current) {\n          return previous.error != current.error;\n        },\n        listener: (context, state) {\n          if (state.error != null) {\n            if (state.error!.isAIResponseLimitExceeded) {\n              showSnackBarMessage(\n                context,\n                LocaleKeys.sideBar_aiResponseLimitDialogTitle.tr(),\n              );\n            } else {\n              showSnackBarMessage(context, state.error!.msg);\n            }\n          }\n        },\n        builder: (context, state) {\n          return const Row(\n            children: [SummaryButton(), HSpace(6), CopyButton()],\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass SummaryButton extends StatelessWidget {\n  const SummaryButton({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SummaryRowBloc, SummaryRowState>(\n      builder: (context, state) {\n        return state.loadingState.when(\n          loading: () {\n            return const Center(\n              child: CircularProgressIndicator.adaptive(),\n            );\n          },\n          finish: () {\n            return FlowyTooltip(\n              message: LocaleKeys.tooltip_aiGenerate.tr(),\n              child: Container(\n                width: 26,\n                height: 26,\n                decoration: BoxDecoration(\n                  border: Border.fromBorderSide(\n                    BorderSide(color: Theme.of(context).dividerColor),\n                  ),\n                  borderRadius: Corners.s6Border,\n                ),\n                child: FlowyIconButton(\n                  hoverColor: AFThemeExtension.of(context).lightGreyHover,\n                  fillColor: Theme.of(context).cardColor,\n                  icon: FlowySvg(\n                    FlowySvgs.ai_summary_generate_s,\n                    color: Theme.of(context).colorScheme.primary,\n                  ),\n                  onPressed: () {\n                    context\n                        .read<SummaryRowBloc>()\n                        .add(const SummaryRowEvent.startSummary());\n                  },\n                ),\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass CopyButton extends StatelessWidget {\n  const CopyButton({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SummaryCellBloc, SummaryCellState>(\n      builder: (blocContext, state) {\n        return FlowyTooltip(\n          message: LocaleKeys.settings_menu_clickToCopy.tr(),\n          child: Container(\n            width: 26,\n            height: 26,\n            decoration: BoxDecoration(\n              border: Border.fromBorderSide(\n                BorderSide(color: Theme.of(context).dividerColor),\n              ),\n              borderRadius: Corners.s6Border,\n            ),\n            child: FlowyIconButton(\n              hoverColor: AFThemeExtension.of(context).lightGreyHover,\n              fillColor: Theme.of(context).cardColor,\n              icon: FlowySvg(\n                FlowySvgs.ai_copy_s,\n                color: Theme.of(context).colorScheme.primary,\n              ),\n              onPressed: () {\n                Clipboard.setData(ClipboardData(text: state.content));\n                showMessageToast(LocaleKeys.grid_row_copyProperty.tr());\n              },\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/text.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../desktop_grid/desktop_grid_text_cell.dart';\nimport '../desktop_row_detail/desktop_row_detail_text_cell.dart';\nimport '../mobile_grid/mobile_grid_text_cell.dart';\nimport '../mobile_row_detail/mobile_row_detail_text_cell.dart';\n\nabstract class IEditableTextCellSkin {\n  const IEditableTextCellSkin();\n\n  factory IEditableTextCellSkin.fromStyle(EditableCellStyle style) {\n    return switch (style) {\n      EditableCellStyle.desktopGrid => DesktopGridTextCellSkin(),\n      EditableCellStyle.desktopRowDetail => DesktopRowDetailTextCellSkin(),\n      EditableCellStyle.mobileGrid => MobileGridTextCellSkin(),\n      EditableCellStyle.mobileRowDetail => MobileRowDetailTextCellSkin(),\n    };\n  }\n\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TextCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  );\n}\n\nclass EditableTextCell extends EditableCellWidget {\n  EditableTextCell({\n    super.key,\n    required this.databaseController,\n    required this.cellContext,\n    required this.skin,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n  final IEditableTextCellSkin skin;\n\n  @override\n  GridEditableTextCell<EditableTextCell> createState() => _TextCellState();\n}\n\nclass _TextCellState extends GridEditableTextCell<EditableTextCell> {\n  late final TextEditingController _textEditingController;\n  late final cellBloc = TextCellBloc(\n    cellController: makeCellController(\n      widget.databaseController,\n      widget.cellContext,\n    ).as(),\n  );\n\n  @override\n  void initState() {\n    super.initState();\n    _textEditingController =\n        TextEditingController(text: cellBloc.state.content);\n  }\n\n  @override\n  void dispose() {\n    _textEditingController.dispose();\n    cellBloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: cellBloc,\n      child: BlocListener<TextCellBloc, TextCellState>(\n        listenWhen: (previous, current) => previous.content != current.content,\n        listener: (context, state) {\n          // It's essential to set the new content to the textEditingController.\n          // If you don't, the old value in textEditingController will persist and\n          // overwrite the correct value, leading to inconsistencies between the\n          // displayed text and the actual data.\n          _textEditingController.text = state.content ?? \"\";\n        },\n        child: Builder(\n          builder: (context) {\n            return widget.skin.build(\n              context,\n              widget.cellContainerNotifier,\n              widget.databaseController.compactModeNotifier,\n              cellBloc,\n              focusNode,\n              _textEditingController,\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  @override\n  SingleListenerFocusNode focusNode = SingleListenerFocusNode();\n\n  @override\n  void onRequestFocus() {\n    focusNode.requestFocus();\n  }\n\n  @override\n  String? onCopy() => cellBloc.state.content;\n\n  @override\n  Future<void> focusChanged() {\n    if (mounted &&\n        !cellBloc.isClosed &&\n        cellBloc.state.content != _textEditingController.text.trim()) {\n      cellBloc\n          .add(TextCellEvent.updateText(_textEditingController.text.trim()));\n    }\n    return super.focusChanged();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/time.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../desktop_grid/desktop_grid_time_cell.dart';\nimport '../desktop_row_detail/desktop_row_detail_time_cell.dart';\nimport '../mobile_grid/mobile_grid_time_cell.dart';\nimport '../mobile_row_detail/mobile_row_detail_time_cell.dart';\n\nabstract class IEditableTimeCellSkin {\n  const IEditableTimeCellSkin();\n\n  factory IEditableTimeCellSkin.fromStyle(EditableCellStyle style) {\n    return switch (style) {\n      EditableCellStyle.desktopGrid => DesktopGridTimeCellSkin(),\n      EditableCellStyle.desktopRowDetail => DesktopRowDetailTimeCellSkin(),\n      EditableCellStyle.mobileGrid => MobileGridTimeCellSkin(),\n      EditableCellStyle.mobileRowDetail => MobileRowDetailTimeCellSkin(),\n    };\n  }\n\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    TimeCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  );\n}\n\nclass EditableTimeCell extends EditableCellWidget {\n  EditableTimeCell({\n    super.key,\n    required this.databaseController,\n    required this.cellContext,\n    required this.skin,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n  final IEditableTimeCellSkin skin;\n\n  @override\n  GridEditableTextCell<EditableTimeCell> createState() => _TimeCellState();\n}\n\nclass _TimeCellState extends GridEditableTextCell<EditableTimeCell> {\n  late final TextEditingController _textEditingController;\n  late final cellBloc = TimeCellBloc(\n    cellController: makeCellController(\n      widget.databaseController,\n      widget.cellContext,\n    ).as(),\n  );\n\n  @override\n  void initState() {\n    super.initState();\n    _textEditingController =\n        TextEditingController(text: cellBloc.state.content);\n  }\n\n  @override\n  void dispose() {\n    _textEditingController.dispose();\n    cellBloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: cellBloc,\n      child: BlocListener<TimeCellBloc, TimeCellState>(\n        listener: (context, state) =>\n            _textEditingController.text = state.content,\n        child: Builder(\n          builder: (context) {\n            return widget.skin.build(\n              context,\n              widget.cellContainerNotifier,\n              cellBloc,\n              focusNode,\n              _textEditingController,\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  @override\n  SingleListenerFocusNode focusNode = SingleListenerFocusNode();\n\n  @override\n  void onRequestFocus() {\n    focusNode.requestFocus();\n  }\n\n  @override\n  String? onCopy() => cellBloc.state.content;\n\n  @override\n  Future<void> focusChanged() async {\n    if (mounted &&\n        !cellBloc.isClosed &&\n        cellBloc.state.content != _textEditingController.text.trim()) {\n      cellBloc\n          .add(TimeCellEvent.updateCell(_textEditingController.text.trim()));\n    }\n    return super.focusChanged();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../desktop_grid/desktop_grid_timestamp_cell.dart';\nimport '../desktop_row_detail/desktop_row_detail_timestamp_cell.dart';\nimport '../mobile_grid/mobile_grid_timestamp_cell.dart';\nimport '../mobile_row_detail/mobile_row_detail_timestamp_cell.dart';\n\nabstract class IEditableTimestampCellSkin {\n  const IEditableTimestampCellSkin();\n\n  factory IEditableTimestampCellSkin.fromStyle(EditableCellStyle style) {\n    return switch (style) {\n      EditableCellStyle.desktopGrid => DesktopGridTimestampCellSkin(),\n      EditableCellStyle.desktopRowDetail => DesktopRowDetailTimestampCellSkin(),\n      EditableCellStyle.mobileGrid => MobileGridTimestampCellSkin(),\n      EditableCellStyle.mobileRowDetail => MobileRowDetailTimestampCellSkin(),\n    };\n  }\n\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TimestampCellBloc bloc,\n    TimestampCellState state,\n  );\n}\n\nclass EditableTimestampCell extends EditableCellWidget {\n  EditableTimestampCell({\n    super.key,\n    required this.databaseController,\n    required this.cellContext,\n    required this.skin,\n    required this.fieldType,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n  final IEditableTimestampCellSkin skin;\n  final FieldType fieldType;\n\n  @override\n  GridCellState<EditableTimestampCell> createState() => _TimestampCellState();\n}\n\nclass _TimestampCellState extends GridCellState<EditableTimestampCell> {\n  late final cellBloc = TimestampCellBloc(\n    cellController: makeCellController(\n      widget.databaseController,\n      widget.cellContext,\n    ).as(),\n  );\n\n  @override\n  void dispose() {\n    cellBloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: cellBloc,\n      child: BlocBuilder<TimestampCellBloc, TimestampCellState>(\n        builder: (context, state) {\n          return widget.skin.build(\n            context,\n            widget.cellContainerNotifier,\n            widget.databaseController.compactModeNotifier,\n            cellBloc,\n            state,\n          );\n        },\n      ),\n    );\n  }\n\n  @override\n  void onRequestFocus() {\n    widget.cellContainerNotifier.isFocus = true;\n  }\n\n  @override\n  String? onCopy() => cellBloc.state.dateStr;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/translate_row_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy/workspace/presentation/home/toast.dart';\nimport 'package:appflowy_backend/dispatch/error.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/icon_button.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nabstract class IEditableTranslateCellSkin {\n  const IEditableTranslateCellSkin();\n\n  factory IEditableTranslateCellSkin.fromStyle(EditableCellStyle style) {\n    return switch (style) {\n      EditableCellStyle.desktopGrid => DesktopGridTranslateCellSkin(),\n      EditableCellStyle.desktopRowDetail => DesktopRowDetailTranslateCellSkin(),\n      EditableCellStyle.mobileGrid => MobileGridTranslateCellSkin(),\n      EditableCellStyle.mobileRowDetail => MobileRowDetailTranslateCellSkin(),\n    };\n  }\n\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TranslateCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  );\n}\n\nclass EditableTranslateCell extends EditableCellWidget {\n  EditableTranslateCell({\n    super.key,\n    required this.databaseController,\n    required this.cellContext,\n    required this.skin,\n  });\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n  final IEditableTranslateCellSkin skin;\n\n  @override\n  GridEditableTextCell<EditableTranslateCell> createState() =>\n      _TranslateCellState();\n}\n\nclass _TranslateCellState extends GridEditableTextCell<EditableTranslateCell> {\n  late final TextEditingController _textEditingController;\n  late final cellBloc = TranslateCellBloc(\n    cellController: makeCellController(\n      widget.databaseController,\n      widget.cellContext,\n    ).as(),\n  );\n\n  @override\n  void initState() {\n    super.initState();\n    _textEditingController =\n        TextEditingController(text: cellBloc.state.content);\n  }\n\n  @override\n  void dispose() {\n    _textEditingController.dispose();\n    cellBloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: cellBloc,\n      child: BlocListener<TranslateCellBloc, TranslateCellState>(\n        listener: (context, state) {\n          _textEditingController.text = state.content;\n        },\n        child: Builder(\n          builder: (context) {\n            return widget.skin.build(\n              context,\n              widget.cellContainerNotifier,\n              widget.databaseController.compactModeNotifier,\n              cellBloc,\n              focusNode,\n              _textEditingController,\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  @override\n  SingleListenerFocusNode focusNode = SingleListenerFocusNode();\n\n  @override\n  void onRequestFocus() {\n    focusNode.requestFocus();\n  }\n\n  @override\n  String? onCopy() => cellBloc.state.content;\n\n  @override\n  Future<void> focusChanged() {\n    if (mounted &&\n        !cellBloc.isClosed &&\n        cellBloc.state.content != _textEditingController.text.trim()) {\n      cellBloc.add(\n        TranslateCellEvent.updateCell(_textEditingController.text.trim()),\n      );\n    }\n    return super.focusChanged();\n  }\n}\n\nclass TranslateCellAccessory extends StatelessWidget {\n  const TranslateCellAccessory({\n    required this.viewId,\n    required this.rowId,\n    required this.fieldId,\n    super.key,\n  });\n\n  final String viewId;\n  final String rowId;\n  final String fieldId;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => TranslateRowBloc(\n        viewId: viewId,\n        rowId: rowId,\n        fieldId: fieldId,\n      ),\n      child: BlocConsumer<TranslateRowBloc, TranslateRowState>(\n        listenWhen: (previous, current) {\n          return previous.error != current.error;\n        },\n        listener: (context, state) {\n          if (state.error != null) {\n            if (state.error!.isAIResponseLimitExceeded) {\n              showSnackBarMessage(\n                context,\n                LocaleKeys.sideBar_aiResponseLimitDialogTitle.tr(),\n              );\n            } else {\n              showSnackBarMessage(context, state.error!.msg);\n            }\n          }\n        },\n        builder: (context, state) {\n          return const Row(\n            children: [TranslateButton(), HSpace(6), CopyButton()],\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass TranslateButton extends StatelessWidget {\n  const TranslateButton({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TranslateRowBloc, TranslateRowState>(\n      builder: (context, state) {\n        return state.loadingState.map(\n          loading: (_) {\n            return const Center(\n              child: CircularProgressIndicator.adaptive(),\n            );\n          },\n          finish: (_) {\n            return FlowyTooltip(\n              message: LocaleKeys.tooltip_aiGenerate.tr(),\n              child: Container(\n                width: 26,\n                height: 26,\n                decoration: BoxDecoration(\n                  border: Border.fromBorderSide(\n                    BorderSide(color: Theme.of(context).dividerColor),\n                  ),\n                  borderRadius: Corners.s6Border,\n                ),\n                child: FlowyIconButton(\n                  hoverColor: AFThemeExtension.of(context).lightGreyHover,\n                  fillColor: Theme.of(context).cardColor,\n                  icon: FlowySvg(\n                    FlowySvgs.ai_summary_generate_s,\n                    color: Theme.of(context).colorScheme.primary,\n                  ),\n                  onPressed: () {\n                    context\n                        .read<TranslateRowBloc>()\n                        .add(const TranslateRowEvent.startTranslate());\n                  },\n                ),\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass CopyButton extends StatelessWidget {\n  const CopyButton({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<TranslateCellBloc, TranslateCellState>(\n      builder: (blocContext, state) {\n        return FlowyTooltip(\n          message: LocaleKeys.settings_menu_clickToCopy.tr(),\n          child: Container(\n            width: 26,\n            height: 26,\n            decoration: BoxDecoration(\n              border: Border.fromBorderSide(\n                BorderSide(color: Theme.of(context).dividerColor),\n              ),\n              borderRadius: Corners.s6Border,\n            ),\n            child: FlowyIconButton(\n              hoverColor: AFThemeExtension.of(context).lightGreyHover,\n              fillColor: Theme.of(context).cardColor,\n              icon: FlowySvg(\n                FlowySvgs.ai_copy_s,\n                color: Theme.of(context).colorScheme.primary,\n              ),\n              onPressed: () {\n                Clipboard.setData(ClipboardData(text: state.content));\n                showMessageToast(LocaleKeys.grid_row_copyProperty.tr());\n              },\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:fluttertoast/fluttertoast.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nimport '../desktop_grid/desktop_grid_url_cell.dart';\nimport '../desktop_row_detail/desktop_row_detail_url_cell.dart';\nimport '../mobile_grid/mobile_grid_url_cell.dart';\nimport '../mobile_row_detail/mobile_row_detail_url_cell.dart';\n\nabstract class IEditableURLCellSkin {\n  const IEditableURLCellSkin();\n\n  factory IEditableURLCellSkin.fromStyle(EditableCellStyle style) {\n    return switch (style) {\n      EditableCellStyle.desktopGrid => DesktopGridURLSkin(),\n      EditableCellStyle.desktopRowDetail => DesktopRowDetailURLSkin(),\n      EditableCellStyle.mobileGrid => MobileGridURLCellSkin(),\n      EditableCellStyle.mobileRowDetail => MobileRowDetailURLCellSkin(),\n    };\n  }\n\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    URLCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n    URLCellDataNotifier cellDataNotifier,\n  );\n\n  List<GridCellAccessoryBuilder> accessoryBuilder(\n    GridCellAccessoryBuildContext context,\n    URLCellDataNotifier cellDataNotifier,\n  );\n}\n\ntypedef URLCellDataNotifier = CellDataNotifier<String>;\n\nclass EditableURLCell extends EditableCellWidget {\n  EditableURLCell({\n    super.key,\n    required this.databaseController,\n    required this.cellContext,\n    required this.skin,\n  }) : _cellDataNotifier = CellDataNotifier(value: '');\n\n  final DatabaseController databaseController;\n  final CellContext cellContext;\n  final IEditableURLCellSkin skin;\n  final URLCellDataNotifier _cellDataNotifier;\n\n  @override\n  List<GridCellAccessoryBuilder> Function(\n    GridCellAccessoryBuildContext buildContext,\n  ) get accessoryBuilder => (context) {\n        return skin.accessoryBuilder(context, _cellDataNotifier);\n      };\n\n  @override\n  GridCellState<EditableURLCell> createState() => _GridURLCellState();\n}\n\nclass _GridURLCellState extends GridEditableTextCell<EditableURLCell> {\n  late final TextEditingController _textEditingController;\n  late final cellBloc = URLCellBloc(\n    cellController: makeCellController(\n      widget.databaseController,\n      widget.cellContext,\n    ).as(),\n  );\n\n  @override\n  SingleListenerFocusNode focusNode = SingleListenerFocusNode();\n\n  @override\n  void initState() {\n    super.initState();\n    _textEditingController =\n        TextEditingController(text: cellBloc.state.content);\n  }\n\n  @override\n  void dispose() {\n    widget._cellDataNotifier.dispose();\n    _textEditingController.dispose();\n    cellBloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: cellBloc,\n      child: BlocListener<URLCellBloc, URLCellState>(\n        listenWhen: (previous, current) => previous.content != current.content,\n        listener: (context, state) {\n          if (!focusNode.hasFocus) {\n            _textEditingController.value =\n                _textEditingController.value.copyWith(text: state.content);\n          }\n          widget._cellDataNotifier.value = state.content;\n        },\n        child: widget.skin.build(\n          context,\n          widget.cellContainerNotifier,\n          widget.databaseController.compactModeNotifier,\n          cellBloc,\n          focusNode,\n          _textEditingController,\n          widget._cellDataNotifier,\n        ),\n      ),\n    );\n  }\n\n  @override\n  Future<void> focusChanged() async {\n    if (mounted &&\n        !cellBloc.isClosed &&\n        cellBloc.state.content != _textEditingController.text) {\n      cellBloc.add(URLCellEvent.updateURL(_textEditingController.text));\n    }\n    return super.focusChanged();\n  }\n\n  @override\n  String? onCopy() => cellBloc.state.content;\n}\n\nclass MobileURLEditor extends StatelessWidget {\n  const MobileURLEditor({\n    super.key,\n    required this.textEditingController,\n  });\n\n  final TextEditingController textEditingController;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        const VSpace(4.0),\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 16),\n          child: FlowyTextField(\n            controller: textEditingController,\n            hintStyle: Theme.of(context)\n                .textTheme\n                .bodyMedium\n                ?.copyWith(color: Theme.of(context).hintColor),\n            hintText: LocaleKeys.grid_url_textFieldHint.tr(),\n            textStyle: Theme.of(context).textTheme.bodyMedium,\n            keyboardType: TextInputType.url,\n            hintTextConstraints: const BoxConstraints(maxHeight: 52),\n            error: context.watch<URLCellBloc>().state.isValid\n                ? null\n                : const SizedBox.shrink(),\n            onChanged: (_) {\n              if (textEditingController.value.composing.isCollapsed) {\n                context\n                    .read<URLCellBloc>()\n                    .add(URLCellEvent.updateURL(textEditingController.text));\n              }\n            },\n            onSubmitted: (text) =>\n                context.read<URLCellBloc>().add(URLCellEvent.updateURL(text)),\n          ),\n        ),\n        const VSpace(8.0),\n        MobileQuickActionButton(\n          enable: context.watch<URLCellBloc>().state.content.isNotEmpty,\n          onTap: () {\n            openUrlCellLink(textEditingController.text);\n            context.pop();\n          },\n          icon: FlowySvgs.url_s,\n          text: LocaleKeys.grid_url_launch.tr(),\n        ),\n        const MobileQuickActionDivider(),\n        MobileQuickActionButton(\n          enable: context.watch<URLCellBloc>().state.content.isNotEmpty,\n          onTap: () {\n            Clipboard.setData(\n              ClipboardData(text: textEditingController.text),\n            );\n            Fluttertoast.showToast(\n              msg: LocaleKeys.message_copy_success.tr(),\n              gravity: ToastGravity.BOTTOM,\n            );\n            context.pop();\n          },\n          icon: FlowySvgs.copy_s,\n          text: LocaleKeys.grid_url_copy.tr(),\n        ),\n      ],\n    );\n  }\n}\n\nvoid openUrlCellLink(String content) async {\n  late Uri uri;\n\n  try {\n    uri = Uri.parse(content);\n    // `Uri` identifies `localhost` as a scheme\n    if (!uri.hasScheme || uri.scheme == 'localhost') {\n      uri = Uri.parse(\"http://$content\");\n      await InternetAddress.lookup(uri.host);\n    }\n  } catch (_) {\n    uri = Uri.parse(\n      \"https://www.google.com/search?q=${Uri.encodeComponent(content)}\",\n    );\n  } finally {\n    await launchUrl(uri);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checkbox_cell.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flutter/material.dart';\n\nimport '../editable_cell_skeleton/checkbox.dart';\n\nclass MobileGridCheckboxCellSkin extends IEditableCheckboxCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    CheckboxCellBloc bloc,\n    CheckboxCellState state,\n  ) {\n    return Align(\n      alignment: Alignment.centerLeft,\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),\n        child: FlowySvg(\n          state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,\n          blendMode: BlendMode.dst,\n          size: const Size.square(24),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checklist_cell.dart",
    "content": "import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_skeleton/checklist.dart';\n\nclass MobileGridChecklistCellSkin extends IEditableChecklistCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    ChecklistCellBloc bloc,\n    PopoverController popoverController,\n  ) {\n    return BlocBuilder<ChecklistCellBloc, ChecklistCellState>(\n      builder: (context, state) {\n        return FlowyButton(\n          radius: BorderRadius.zero,\n          hoverColor: Colors.transparent,\n          text: Container(\n            alignment: Alignment.centerLeft,\n            padding: GridSize.cellContentInsets,\n            child: state.tasks.isEmpty\n                ? const SizedBox.shrink()\n                : ChecklistProgressBar(\n                    tasks: state.tasks,\n                    percent: state.percent,\n                    textStyle: Theme.of(context)\n                        .textTheme\n                        .bodyMedium\n                        ?.copyWith(fontSize: 15),\n                  ),\n          ),\n          onTap: () => showMobileBottomSheet(\n            context,\n            builder: (context) {\n              return BlocProvider.value(\n                value: bloc,\n                child: const MobileChecklistCellEditScreen(),\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileGridDateCellSkin extends IEditableDateCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    DateCellBloc bloc,\n    DateCellState state,\n    PopoverController popoverController,\n  ) {\n    final dateStr = getDateCellStrFromCellData(\n      state.fieldInfo,\n      state.cellData,\n    );\n    return FlowyButton(\n      radius: BorderRadius.zero,\n      hoverColor: Colors.transparent,\n      margin: EdgeInsets.zero,\n      text: Align(\n        alignment: AlignmentDirectional.centerStart,\n        child: SingleChildScrollView(\n          scrollDirection: Axis.horizontal,\n          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),\n          child: Row(\n            children: [\n              if (state.cellData.reminderId.isNotEmpty) ...[\n                const FlowySvg(FlowySvgs.clock_alarm_s),\n                const HSpace(6),\n              ],\n              FlowyText(\n                dateStr,\n                fontSize: 15,\n              ),\n            ],\n          ),\n        ),\n      ),\n      onTap: () {\n        showMobileBottomSheet(\n          context,\n          builder: (context) {\n            return MobileDateCellEditScreen(\n              controller: bloc.cellController,\n              showAsFullScreen: false,\n            );\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_number_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flutter/material.dart';\n\nimport '../editable_cell_skeleton/number.dart';\n\nclass MobileGridNumberCellSkin extends IEditableNumberCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    NumberCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return TextField(\n      controller: textEditingController,\n      focusNode: focusNode,\n      style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 15),\n      decoration: const InputDecoration(\n        enabledBorder: InputBorder.none,\n        focusedBorder: InputBorder.none,\n        contentPadding: EdgeInsets.symmetric(horizontal: 14, vertical: 12),\n        isCollapsed: true,\n      ),\n      onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart",
    "content": "import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport '../editable_cell_skeleton/relation.dart';\n\nclass MobileGridRelationCellSkin extends IEditableRelationCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    RelationCellBloc bloc,\n    RelationCellState state,\n    PopoverController popoverController,\n  ) {\n    return FlowyButton(\n      radius: BorderRadius.zero,\n      hoverColor: Colors.transparent,\n      margin: EdgeInsets.zero,\n      text: Align(\n        alignment: AlignmentDirectional.centerStart,\n        child: SingleChildScrollView(\n          scrollDirection: Axis.horizontal,\n          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),\n          child: Row(\n            mainAxisSize: MainAxisSize.min,\n            children: state.rows\n                .map(\n                  (row) => FlowyText(\n                    row.name,\n                    fontSize: 15,\n                    decoration: TextDecoration.underline,\n                  ),\n                )\n                .toList(),\n          ),\n        ),\n      ),\n      onTap: () {\n        showMobileBottomSheet(\n          context,\n          backgroundColor: Theme.of(context).colorScheme.secondaryContainer,\n          builder: (context) {\n            return const FlowyText(\"Coming soon\");\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_select_option_cell.dart",
    "content": "import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_skeleton/select_option.dart';\n\nclass MobileGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    SelectOptionCellBloc bloc,\n    PopoverController popoverController,\n  ) {\n    return BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(\n      builder: (context, state) {\n        return FlowyButton(\n          hoverColor: Colors.transparent,\n          radius: BorderRadius.zero,\n          margin: EdgeInsets.zero,\n          text: Align(\n            alignment: AlignmentDirectional.centerStart,\n            child: state.selectedOptions.isEmpty\n                ? const SizedBox.shrink()\n                : _buildOptions(context, state.selectedOptions),\n          ),\n          onTap: () {\n            showMobileBottomSheet(\n              context,\n              builder: (context) {\n                return MobileSelectOptionEditor(\n                  cellController: bloc.cellController,\n                );\n              },\n            );\n          },\n        );\n      },\n    );\n  }\n\n  Widget _buildOptions(BuildContext context, List<SelectOptionPB> options) {\n    final children = options\n        .mapIndexed(\n          (index, option) => SelectOptionTag(\n            option: option,\n            fontSize: 14,\n            padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),\n          ),\n        )\n        .toList();\n\n    return ListView.separated(\n      scrollDirection: Axis.horizontal,\n      separatorBuilder: (context, index) => const HSpace(8),\n      itemCount: children.length,\n      itemBuilder: (context, index) => children[index],\n      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\nimport 'package:styled_widget/styled_widget.dart';\n\nclass MobileGridSummaryCellSkin extends IEditableSummaryCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    SummaryCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return ChangeNotifierProvider(\n      create: (_) => SummaryMouseNotifier(),\n      builder: (context, child) {\n        return MouseRegion(\n          cursor: SystemMouseCursors.click,\n          opaque: false,\n          onEnter: (p) =>\n              Provider.of<SummaryMouseNotifier>(context, listen: false)\n                  .onEnter = true,\n          onExit: (p) =>\n              Provider.of<SummaryMouseNotifier>(context, listen: false)\n                  .onEnter = false,\n          child: Stack(\n            children: [\n              TextField(\n                controller: textEditingController,\n                readOnly: true,\n                focusNode: focusNode,\n                onEditingComplete: () => focusNode.unfocus(),\n                onSubmitted: (_) => focusNode.unfocus(),\n                style: Theme.of(context).textTheme.bodyMedium,\n                textInputAction: TextInputAction.done,\n                decoration: InputDecoration(\n                  contentPadding: GridSize.cellContentInsets,\n                  border: InputBorder.none,\n                  focusedBorder: InputBorder.none,\n                  enabledBorder: InputBorder.none,\n                  errorBorder: InputBorder.none,\n                  disabledBorder: InputBorder.none,\n                  isDense: true,\n                ),\n              ),\n              Padding(\n                padding: EdgeInsets.symmetric(\n                  horizontal: GridSize.cellVPadding,\n                ),\n                child: Consumer<SummaryMouseNotifier>(\n                  builder: (\n                    BuildContext context,\n                    SummaryMouseNotifier notifier,\n                    Widget? child,\n                  ) {\n                    if (notifier.onEnter) {\n                      return SummaryCellAccessory(\n                        viewId: bloc.cellController.viewId,\n                        fieldId: bloc.cellController.fieldId,\n                        rowId: bloc.cellController.rowId,\n                      );\n                    } else {\n                      return const SizedBox.shrink();\n                    }\n                  },\n                ),\n              ).positioned(right: 0, bottom: 0),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_text_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_skeleton/text.dart';\n\nclass MobileGridTextCellSkin extends IEditableTextCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TextCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return Row(\n      children: [\n        const HSpace(10),\n        BlocBuilder<TextCellBloc, TextCellState>(\n          buildWhen: (p, c) => p.emoji != c.emoji,\n          builder: (context, state) => Center(\n            child: FlowyText.emoji(\n              state.emoji?.value ?? \"\",\n              fontSize: 15,\n              optimizeEmojiAlign: true,\n            ),\n          ),\n        ),\n        Expanded(\n          child: TextField(\n            controller: textEditingController,\n            focusNode: focusNode,\n            style: Theme.of(context).textTheme.bodyMedium?.copyWith(\n                  fontSize: 15,\n                ),\n            decoration: const InputDecoration(\n              enabledBorder: InputBorder.none,\n              focusedBorder: InputBorder.none,\n              contentPadding: EdgeInsets.symmetric(horizontal: 4),\n              isCollapsed: true,\n            ),\n            onTapOutside: (event) => focusNode.unfocus(),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_time_cell.dart",
    "content": "import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';\nimport 'package:flutter/material.dart';\n\nimport '../editable_cell_skeleton/time.dart';\n\nclass MobileGridTimeCellSkin extends IEditableTimeCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    TimeCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return TextField(\n      controller: textEditingController,\n      focusNode: focusNode,\n      style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 15),\n      decoration: const InputDecoration(\n        enabledBorder: InputBorder.none,\n        focusedBorder: InputBorder.none,\n        contentPadding: EdgeInsets.symmetric(horizontal: 14, vertical: 12),\n        isCollapsed: true,\n      ),\n      onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_timestamp_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport '../editable_cell_skeleton/timestamp.dart';\n\nclass MobileGridTimestampCellSkin extends IEditableTimestampCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TimestampCellBloc bloc,\n    TimestampCellState state,\n  ) {\n    return Container(\n      alignment: Alignment.centerLeft,\n      padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),\n      child: FlowyText(\n        state.dateStr,\n        fontSize: 15,\n        overflow: TextOverflow.ellipsis,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\nimport 'package:styled_widget/styled_widget.dart';\n\nclass MobileGridTranslateCellSkin extends IEditableTranslateCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TranslateCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return ChangeNotifierProvider(\n      create: (_) => TranslateMouseNotifier(),\n      builder: (context, child) {\n        return MouseRegion(\n          cursor: SystemMouseCursors.click,\n          opaque: false,\n          onEnter: (p) =>\n              Provider.of<TranslateMouseNotifier>(context, listen: false)\n                  .onEnter = true,\n          onExit: (p) =>\n              Provider.of<TranslateMouseNotifier>(context, listen: false)\n                  .onEnter = false,\n          child: Stack(\n            children: [\n              TextField(\n                controller: textEditingController,\n                readOnly: true,\n                focusNode: focusNode,\n                onEditingComplete: () => focusNode.unfocus(),\n                onSubmitted: (_) => focusNode.unfocus(),\n                style: Theme.of(context).textTheme.bodyMedium,\n                textInputAction: TextInputAction.done,\n                decoration: InputDecoration(\n                  contentPadding: GridSize.cellContentInsets,\n                  border: InputBorder.none,\n                  focusedBorder: InputBorder.none,\n                  enabledBorder: InputBorder.none,\n                  errorBorder: InputBorder.none,\n                  disabledBorder: InputBorder.none,\n                  isDense: true,\n                ),\n              ),\n              Padding(\n                padding: EdgeInsets.symmetric(\n                  horizontal: GridSize.cellVPadding,\n                ),\n                child: Consumer<TranslateMouseNotifier>(\n                  builder: (\n                    BuildContext context,\n                    TranslateMouseNotifier notifier,\n                    Widget? child,\n                  ) {\n                    if (notifier.onEnter) {\n                      return TranslateCellAccessory(\n                        viewId: bloc.cellController.viewId,\n                        fieldId: bloc.cellController.fieldId,\n                        rowId: bloc.cellController.rowId,\n                      );\n                    } else {\n                      return const SizedBox.shrink();\n                    }\n                  },\n                ),\n              ).positioned(right: 0, bottom: 0),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart",
    "content": "import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_skeleton/url.dart';\n\nclass MobileGridURLCellSkin extends IEditableURLCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    URLCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n    URLCellDataNotifier cellDataNotifier,\n  ) {\n    return BlocSelector<URLCellBloc, URLCellState, String>(\n      selector: (state) => state.content,\n      builder: (context, content) {\n        return GestureDetector(\n          onTap: () => _showURLEditor(context, bloc, textEditingController),\n          behavior: HitTestBehavior.opaque,\n          child: Container(\n            alignment: AlignmentDirectional.centerStart,\n            padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),\n            child: SingleChildScrollView(\n              scrollDirection: Axis.horizontal,\n              child: Text(\n                content,\n                maxLines: 1,\n                style: Theme.of(context).textTheme.titleMedium?.copyWith(\n                      decoration: TextDecoration.underline,\n                      color: Theme.of(context).colorScheme.primary,\n                    ),\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  void _showURLEditor(\n    BuildContext context,\n    URLCellBloc bloc,\n    TextEditingController textEditingController,\n  ) {\n    showMobileBottomSheet(\n      context,\n      showDragHandle: true,\n      backgroundColor: AFThemeExtension.of(context).background,\n      builder: (context) => BlocProvider.value(\n        value: bloc,\n        child: MobileURLEditor(\n          textEditingController: textEditingController,\n        ),\n      ),\n    );\n  }\n\n  @override\n  List<GridCellAccessoryBuilder<State<StatefulWidget>>> accessoryBuilder(\n    GridCellAccessoryBuildContext context,\n    URLCellDataNotifier cellDataNotifier,\n  ) =>\n      const [];\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\n\nimport '../editable_cell_skeleton/checkbox.dart';\n\nclass MobileRowDetailCheckboxCellSkin extends IEditableCheckboxCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    CheckboxCellBloc bloc,\n    CheckboxCellState state,\n  ) {\n    return InkWell(\n      onTap: () => bloc.add(const CheckboxCellEvent.select()),\n      borderRadius: const BorderRadius.all(Radius.circular(14)),\n      child: Container(\n        constraints: const BoxConstraints(\n          minHeight: 48,\n          minWidth: double.infinity,\n        ),\n        decoration: BoxDecoration(\n          border: Border.fromBorderSide(\n            BorderSide(color: Theme.of(context).colorScheme.outline),\n          ),\n          borderRadius: const BorderRadius.all(Radius.circular(14)),\n        ),\n        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),\n        alignment: AlignmentDirectional.centerStart,\n        child: FlowySvg(\n          state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,\n          color: AFThemeExtension.of(context).onBackground,\n          blendMode: BlendMode.dst,\n          size: const Size.square(24),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_skeleton/checklist.dart';\n\nclass MobileRowDetailChecklistCellSkin extends IEditableChecklistCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    ChecklistCellBloc bloc,\n    PopoverController popoverController,\n  ) {\n    return BlocBuilder<ChecklistCellBloc, ChecklistCellState>(\n      builder: (context, state) {\n        return InkWell(\n          borderRadius: const BorderRadius.all(Radius.circular(14)),\n          onTap: () => showMobileBottomSheet(\n            context,\n            backgroundColor: AFThemeExtension.of(context).background,\n            builder: (context) {\n              return BlocProvider.value(\n                value: bloc,\n                child: const MobileChecklistCellEditScreen(),\n              );\n            },\n          ),\n          child: Container(\n            constraints: const BoxConstraints(\n              minHeight: 48,\n              minWidth: double.infinity,\n            ),\n            decoration: BoxDecoration(\n              border: Border.fromBorderSide(\n                BorderSide(color: Theme.of(context).colorScheme.outline),\n              ),\n              borderRadius: const BorderRadius.all(Radius.circular(14)),\n            ),\n            padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),\n            alignment: AlignmentDirectional.centerStart,\n            child: state.tasks.isEmpty\n                ? FlowyText(\n                    LocaleKeys.grid_row_textPlaceholder.tr(),\n                    fontSize: 15,\n                    color: Theme.of(context).hintColor,\n                  )\n                : ChecklistProgressBar(\n                    tasks: state.tasks,\n                    percent: state.percent,\n                    textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(\n                          fontSize: 15,\n                          color: Theme.of(context).hintColor,\n                        ),\n                  ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_date_cell.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileRowDetailDateCellSkin extends IEditableDateCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    DateCellBloc bloc,\n    DateCellState state,\n    PopoverController popoverController,\n  ) {\n    final dateStr = getDateCellStrFromCellData(\n      state.fieldInfo,\n      state.cellData,\n    );\n    final text =\n        dateStr.isEmpty ? LocaleKeys.grid_row_textPlaceholder.tr() : dateStr;\n    final color = dateStr.isEmpty ? Theme.of(context).hintColor : null;\n\n    return InkWell(\n      borderRadius: const BorderRadius.all(Radius.circular(14)),\n      onTap: () => showMobileBottomSheet(\n        context,\n        builder: (context) {\n          return MobileDateCellEditScreen(\n            controller: bloc.cellController,\n            showAsFullScreen: false,\n          );\n        },\n      ),\n      child: Container(\n        constraints: const BoxConstraints(\n          minHeight: 48,\n          minWidth: double.infinity,\n        ),\n        decoration: BoxDecoration(\n          border: Border.fromBorderSide(\n            BorderSide(color: Theme.of(context).colorScheme.outline),\n          ),\n          borderRadius: const BorderRadius.all(Radius.circular(14)),\n        ),\n        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),\n        child: Row(\n          children: [\n            if (state.cellData.reminderId.isNotEmpty) ...[\n              const FlowySvg(FlowySvgs.clock_alarm_s),\n              const HSpace(6),\n            ],\n            FlowyText.regular(\n              text,\n              fontSize: 16,\n              color: color,\n              maxLines: null,\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_number_cell.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nimport '../editable_cell_skeleton/number.dart';\n\nclass MobileRowDetailNumberCellSkin extends IEditableNumberCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    NumberCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return TextField(\n      controller: textEditingController,\n      keyboardType: const TextInputType.numberWithOptions(\n        signed: true,\n        decimal: true,\n      ),\n      focusNode: focusNode,\n      style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 16),\n      decoration: InputDecoration(\n        enabledBorder:\n            _getInputBorder(color: Theme.of(context).colorScheme.outline),\n        focusedBorder:\n            _getInputBorder(color: Theme.of(context).colorScheme.primary),\n        hintText: LocaleKeys.grid_row_textPlaceholder.tr(),\n        contentPadding:\n            const EdgeInsets.symmetric(horizontal: 12, vertical: 13),\n        isCollapsed: true,\n        isDense: true,\n        constraints: const BoxConstraints(),\n      ),\n      // close keyboard when tapping outside of the text field\n      onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n    );\n  }\n\n  InputBorder _getInputBorder({Color? color}) {\n    return OutlineInputBorder(\n      borderSide: BorderSide(color: color!),\n      borderRadius: const BorderRadius.all(Radius.circular(14)),\n      gapPadding: 0,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart",
    "content": "import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/relation.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileRowDetailRelationCellSkin extends IEditableRelationCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    RelationCellBloc bloc,\n    RelationCellState state,\n    PopoverController popoverController,\n  ) {\n    return InkWell(\n      borderRadius: const BorderRadius.all(Radius.circular(14)),\n      onTap: () => showMobileBottomSheet(\n        context,\n        builder: (context) {\n          return const FlowyText(\"Coming soon\");\n        },\n      ),\n      child: Container(\n        constraints: const BoxConstraints(\n          minHeight: 48,\n          minWidth: double.infinity,\n        ),\n        decoration: BoxDecoration(\n          border: Border.fromBorderSide(\n            BorderSide(color: Theme.of(context).colorScheme.outline),\n          ),\n          borderRadius: const BorderRadius.all(Radius.circular(14)),\n        ),\n        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),\n        child: Wrap(\n          runSpacing: 4.0,\n          spacing: 4.0,\n          children: state.rows\n              .map(\n                (row) => FlowyText(\n                  row.name,\n                  fontSize: 16,\n                  decoration: TextDecoration.underline,\n                  overflow: TextOverflow.ellipsis,\n                ),\n              )\n              .toList(),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_select_cell_option.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_skeleton/select_option.dart';\n\nclass MobileRowDetailSelectOptionCellSkin\n    extends IEditableSelectOptionCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    SelectOptionCellBloc bloc,\n    PopoverController popoverController,\n  ) {\n    return BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(\n      builder: (context, state) {\n        return InkWell(\n          borderRadius: const BorderRadius.all(Radius.circular(14)),\n          onTap: () => showMobileBottomSheet(\n            context,\n            builder: (context) {\n              return MobileSelectOptionEditor(\n                cellController: bloc.cellController,\n              );\n            },\n          ),\n          child: Container(\n            constraints: const BoxConstraints(\n              minHeight: 48,\n              minWidth: double.infinity,\n            ),\n            padding: EdgeInsets.symmetric(\n              horizontal: 12,\n              vertical: state.selectedOptions.isEmpty ? 13 : 10,\n            ),\n            decoration: BoxDecoration(\n              border: Border.fromBorderSide(\n                BorderSide(color: Theme.of(context).colorScheme.outline),\n              ),\n              borderRadius: const BorderRadius.all(Radius.circular(14)),\n            ),\n            child: Row(\n              children: [\n                Expanded(\n                  child: state.selectedOptions.isEmpty\n                      ? _buildPlaceholder(context)\n                      : _buildOptions(context, state.selectedOptions),\n                ),\n                const HSpace(6),\n                RotatedBox(\n                  quarterTurns: 3,\n                  child: Icon(\n                    Icons.chevron_left,\n                    color: Theme.of(context).hintColor,\n                  ),\n                ),\n                const HSpace(2),\n              ],\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildPlaceholder(BuildContext context) {\n    return Container(\n      alignment: Alignment.centerLeft,\n      padding: const EdgeInsets.symmetric(vertical: 1),\n      child: FlowyText(\n        LocaleKeys.grid_row_textPlaceholder.tr(),\n        color: Theme.of(context).hintColor,\n      ),\n    );\n  }\n\n  Widget _buildOptions(BuildContext context, List<SelectOptionPB> options) {\n    final children = options.mapIndexed(\n      (index, option) {\n        return Padding(\n          padding: EdgeInsets.only(left: index == 0 ? 0 : 4),\n          child: SelectOptionTag(\n            option: option,\n            fontSize: 14,\n            padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 5),\n          ),\n        );\n      },\n    ).toList();\n\n    return Align(\n      alignment: AlignmentDirectional.centerStart,\n      child: Wrap(\n        runSpacing: 4,\n        children: children,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileRowDetailSummaryCellSkin extends IEditableSummaryCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    SummaryCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return Container(\n      decoration: BoxDecoration(\n        border: Border.fromBorderSide(\n          BorderSide(color: Theme.of(context).colorScheme.outline),\n        ),\n        borderRadius: const BorderRadius.all(Radius.circular(14)),\n      ),\n      padding: const EdgeInsets.symmetric(\n        horizontal: 4,\n        vertical: 2,\n      ),\n      child: Column(\n        children: [\n          TextField(\n            controller: textEditingController,\n            readOnly: true,\n            focusNode: focusNode,\n            onEditingComplete: () => focusNode.unfocus(),\n            onSubmitted: (_) => focusNode.unfocus(),\n            style: Theme.of(context).textTheme.bodyMedium,\n            textInputAction: TextInputAction.done,\n            maxLines: null,\n            minLines: 1,\n            decoration: InputDecoration(\n              contentPadding: GridSize.cellContentInsets,\n              border: InputBorder.none,\n              focusedBorder: InputBorder.none,\n              enabledBorder: InputBorder.none,\n              errorBorder: InputBorder.none,\n              disabledBorder: InputBorder.none,\n              isDense: true,\n            ),\n          ),\n          Row(\n            children: [\n              const Spacer(),\n              Padding(\n                padding: const EdgeInsets.all(8.0),\n                child: SummaryCellAccessory(\n                  viewId: bloc.cellController.viewId,\n                  fieldId: bloc.cellController.fieldId,\n                  rowId: bloc.cellController.rowId,\n                ),\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_text_cell.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nimport '../editable_cell_skeleton/text.dart';\n\nclass MobileRowDetailTextCellSkin extends IEditableTextCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TextCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return TextField(\n      controller: textEditingController,\n      focusNode: focusNode,\n      maxLines: null,\n      decoration: InputDecoration(\n        enabledBorder:\n            _getInputBorder(color: Theme.of(context).colorScheme.outline),\n        focusedBorder:\n            _getInputBorder(color: Theme.of(context).colorScheme.primary),\n        hintText: LocaleKeys.grid_row_textPlaceholder.tr(),\n        contentPadding:\n            const EdgeInsets.symmetric(horizontal: 12, vertical: 13),\n        isCollapsed: true,\n        isDense: true,\n        constraints: const BoxConstraints(minHeight: 48),\n        hintStyle: Theme.of(context)\n            .textTheme\n            .bodyMedium\n            ?.copyWith(color: Theme.of(context).hintColor),\n      ),\n      onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n    );\n  }\n\n  InputBorder _getInputBorder({Color? color}) {\n    return OutlineInputBorder(\n      borderSide: BorderSide(color: color!),\n      borderRadius: const BorderRadius.all(Radius.circular(14)),\n      gapPadding: 0,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_time_cell.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nimport '../editable_cell_skeleton/time.dart';\n\nclass MobileRowDetailTimeCellSkin extends IEditableTimeCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    TimeCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return TextField(\n      controller: textEditingController,\n      focusNode: focusNode,\n      style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 16),\n      decoration: InputDecoration(\n        enabledBorder:\n            _getInputBorder(color: Theme.of(context).colorScheme.outline),\n        focusedBorder:\n            _getInputBorder(color: Theme.of(context).colorScheme.primary),\n        hintText: LocaleKeys.grid_row_textPlaceholder.tr(),\n        contentPadding:\n            const EdgeInsets.symmetric(horizontal: 12, vertical: 13),\n        isCollapsed: true,\n        isDense: true,\n        constraints: const BoxConstraints(),\n      ),\n      // close keyboard when tapping outside of the text field\n      onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),\n    );\n  }\n\n  InputBorder _getInputBorder({Color? color}) {\n    return OutlineInputBorder(\n      borderSide: BorderSide(color: color!),\n      borderRadius: const BorderRadius.all(Radius.circular(14)),\n      gapPadding: 0,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_timestamp_cell.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport '../editable_cell_skeleton/timestamp.dart';\n\nclass MobileRowDetailTimestampCellSkin extends IEditableTimestampCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TimestampCellBloc bloc,\n    TimestampCellState state,\n  ) {\n    return Container(\n      constraints: const BoxConstraints(\n        minHeight: 48,\n        minWidth: double.infinity,\n      ),\n      decoration: BoxDecoration(\n        border: Border.fromBorderSide(\n          BorderSide(color: Theme.of(context).colorScheme.outline),\n        ),\n        borderRadius: const BorderRadius.all(Radius.circular(14)),\n      ),\n      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),\n      child: FlowyText(\n        state.dateStr.isEmpty\n            ? LocaleKeys.grid_row_textPlaceholder.tr()\n            : state.dateStr,\n        fontSize: 16,\n        color: state.dateStr.isEmpty ? Theme.of(context).hintColor : null,\n        maxLines: null,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileRowDetailTranslateCellSkin extends IEditableTranslateCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TranslateCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return Container(\n      decoration: BoxDecoration(\n        border: Border.fromBorderSide(\n          BorderSide(color: Theme.of(context).colorScheme.outline),\n        ),\n        borderRadius: const BorderRadius.all(Radius.circular(14)),\n      ),\n      padding: const EdgeInsets.symmetric(\n        horizontal: 4,\n        vertical: 2,\n      ),\n      child: Column(\n        children: [\n          TextField(\n            readOnly: true,\n            controller: textEditingController,\n            focusNode: focusNode,\n            onEditingComplete: () => focusNode.unfocus(),\n            onSubmitted: (_) => focusNode.unfocus(),\n            style: Theme.of(context).textTheme.bodyMedium,\n            textInputAction: TextInputAction.done,\n            maxLines: null,\n            minLines: 1,\n            decoration: InputDecoration(\n              contentPadding: GridSize.cellContentInsets,\n              border: InputBorder.none,\n              focusedBorder: InputBorder.none,\n              enabledBorder: InputBorder.none,\n              errorBorder: InputBorder.none,\n              disabledBorder: InputBorder.none,\n              isDense: true,\n            ),\n          ),\n          Row(\n            children: [\n              const Spacer(),\n              Padding(\n                padding: const EdgeInsets.all(8.0),\n                child: TranslateCellAccessory(\n                  viewId: bloc.cellController.viewId,\n                  fieldId: bloc.cellController.fieldId,\n                  rowId: bloc.cellController.rowId,\n                ),\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../editable_cell_skeleton/url.dart';\n\nclass MobileRowDetailURLCellSkin extends IEditableURLCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    URLCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n    URLCellDataNotifier cellDataNotifier,\n  ) {\n    return BlocSelector<URLCellBloc, URLCellState, String>(\n      selector: (state) => state.content,\n      builder: (context, content) {\n        return InkWell(\n          borderRadius: const BorderRadius.all(Radius.circular(14)),\n          onTap: () => showMobileBottomSheet(\n            context,\n            showDragHandle: true,\n            backgroundColor: AFThemeExtension.of(context).background,\n            builder: (_) {\n              return BlocProvider.value(\n                value: bloc,\n                child: MobileURLEditor(\n                  textEditingController: textEditingController,\n                ),\n              );\n            },\n          ),\n          child: Container(\n            constraints: const BoxConstraints(\n              minHeight: 48,\n              minWidth: double.infinity,\n            ),\n            decoration: BoxDecoration(\n              border: Border.fromBorderSide(\n                BorderSide(color: Theme.of(context).colorScheme.outline),\n              ),\n              borderRadius: const BorderRadius.all(Radius.circular(14)),\n            ),\n            child: Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),\n              child: Text(\n                content.isEmpty\n                    ? LocaleKeys.grid_row_textPlaceholder.tr()\n                    : content,\n                style: Theme.of(context).textTheme.bodyMedium?.copyWith(\n                      fontSize: 16,\n                      decoration:\n                          content.isEmpty ? null : TextDecoration.underline,\n                      color: content.isEmpty\n                          ? Theme.of(context).hintColor\n                          : Theme.of(context).colorScheme.primary,\n                    ),\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  @override\n  List<GridCellAccessoryBuilder<State<StatefulWidget>>> accessoryBuilder(\n    GridCellAccessoryBuildContext context,\n    URLCellDataNotifier cellDataNotifier,\n  ) =>\n      const [];\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';\nimport 'package:appflowy/util/debounce.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../../application/cell/bloc/checklist_cell_bloc.dart';\nimport 'checklist_cell_textfield.dart';\nimport 'checklist_progress_bar.dart';\n\nclass ChecklistCellEditor extends StatefulWidget {\n  const ChecklistCellEditor({required this.cellController, super.key});\n\n  final ChecklistCellController cellController;\n\n  @override\n  State<ChecklistCellEditor> createState() => _ChecklistCellEditorState();\n}\n\nclass _ChecklistCellEditorState extends State<ChecklistCellEditor> {\n  /// Focus node for the new task text field\n  late final FocusNode newTaskFocusNode;\n\n  @override\n  void initState() {\n    super.initState();\n    newTaskFocusNode = FocusNode(\n      onKeyEvent: (node, event) {\n        if (event.logicalKey == LogicalKeyboardKey.escape) {\n          node.unfocus();\n          return KeyEventResult.handled;\n        }\n        return KeyEventResult.ignored;\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocConsumer<ChecklistCellBloc, ChecklistCellState>(\n      listener: (context, state) {\n        if (state.tasks.isEmpty) {\n          newTaskFocusNode.requestFocus();\n        }\n      },\n      builder: (context, state) {\n        return Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            if (state.tasks.isNotEmpty)\n              Padding(\n                padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),\n                child: ChecklistProgressBar(\n                  tasks: state.tasks,\n                  percent: state.percent,\n                ),\n              ),\n            ChecklistItemList(\n              options: state.tasks,\n              onUpdateTask: () => newTaskFocusNode.requestFocus(),\n            ),\n            if (state.tasks.isNotEmpty) const TypeOptionSeparator(spacing: 0.0),\n            Padding(\n              padding: const EdgeInsets.symmetric(vertical: 8),\n              child: NewTaskItem(focusNode: newTaskFocusNode),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    newTaskFocusNode.dispose();\n    super.dispose();\n  }\n}\n\n/// Displays the a list of all the existing tasks and an input field to create\n/// a new task if `isAddingNewTask` is true\nclass ChecklistItemList extends StatelessWidget {\n  const ChecklistItemList({\n    super.key,\n    required this.options,\n    required this.onUpdateTask,\n  });\n\n  final List<ChecklistSelectOption> options;\n  final VoidCallback onUpdateTask;\n\n  @override\n  Widget build(BuildContext context) {\n    if (options.isEmpty) {\n      return const SizedBox.shrink();\n    }\n\n    final itemList = options\n        .mapIndexed(\n          (index, option) => Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0),\n            key: ValueKey(option.data.id),\n            child: ChecklistItem(\n              task: option,\n              index: index,\n              onSubmitted: index == options.length - 1 ? onUpdateTask : null,\n            ),\n          ),\n        )\n        .toList();\n\n    return Flexible(\n      child: ReorderableListView.builder(\n        shrinkWrap: true,\n        proxyDecorator: (child, index, _) => Material(\n          color: Colors.transparent,\n          child: MouseRegion(\n            cursor: UniversalPlatform.isWindows\n                ? SystemMouseCursors.click\n                : SystemMouseCursors.grabbing,\n            child: IgnorePointer(\n              child: BlocProvider.value(\n                value: context.read<ChecklistCellBloc>(),\n                child: child,\n              ),\n            ),\n          ),\n        ),\n        buildDefaultDragHandles: false,\n        itemBuilder: (context, index) => itemList[index],\n        itemCount: itemList.length,\n        padding: const EdgeInsets.symmetric(vertical: 6.0),\n        onReorder: (from, to) {\n          context\n              .read<ChecklistCellBloc>()\n              .add(ChecklistCellEvent.reorderTask(from, to));\n        },\n      ),\n    );\n  }\n}\n\nclass _SelectTaskIntent extends Intent {\n  const _SelectTaskIntent();\n}\n\nclass _EndEditingTaskIntent extends Intent {\n  const _EndEditingTaskIntent();\n}\n\nclass _UpdateTaskDescriptionIntent extends Intent {\n  const _UpdateTaskDescriptionIntent();\n}\n\nclass ChecklistItem extends StatefulWidget {\n  const ChecklistItem({\n    super.key,\n    required this.task,\n    required this.index,\n    this.onSubmitted,\n    this.autofocus = false,\n  });\n\n  final ChecklistSelectOption task;\n  final int index;\n  final VoidCallback? onSubmitted;\n  final bool autofocus;\n\n  @override\n  State<ChecklistItem> createState() => _ChecklistItemState();\n}\n\nclass _ChecklistItemState extends State<ChecklistItem> {\n  TextEditingController textController = TextEditingController();\n  final textFieldFocusNode = FocusNode();\n  final focusNode = FocusNode(skipTraversal: true);\n\n  bool isHovered = false;\n  bool isFocused = false;\n  bool isComposing = false;\n\n  final _debounceOnChanged = Debounce(\n    duration: const Duration(milliseconds: 300),\n  );\n\n  @override\n  void initState() {\n    super.initState();\n    textController.text = widget.task.data.name;\n    textController.addListener(_onTextChanged);\n    if (widget.autofocus) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        focusNode.requestFocus();\n        textFieldFocusNode.requestFocus();\n      });\n    }\n  }\n\n  void _onTextChanged() =>\n      setState(() => isComposing = !textController.value.composing.isCollapsed);\n\n  @override\n  void didUpdateWidget(covariant oldWidget) {\n    if (!focusNode.hasFocus &&\n        oldWidget.task.data.name != widget.task.data.name) {\n      textController.text = widget.task.data.name;\n    }\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  void dispose() {\n    _debounceOnChanged.dispose();\n\n    textController.removeListener(_onTextChanged);\n    textController.dispose();\n    focusNode.dispose();\n    textFieldFocusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final isFocusedOrHovered = isHovered || isFocused;\n    final color = isFocusedOrHovered || textFieldFocusNode.hasFocus\n        ? AFThemeExtension.of(context).lightGreyHover\n        : Colors.transparent;\n    return FocusableActionDetector(\n      focusNode: focusNode,\n      onShowHoverHighlight: (value) => setState(() => isHovered = value),\n      onFocusChange: (value) => setState(() => isFocused = value),\n      actions: _buildActions(),\n      shortcuts: _buildShortcuts(),\n      child: Container(\n        constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight),\n        decoration: BoxDecoration(color: color, borderRadius: Corners.s6Border),\n        child: _buildChild(isFocusedOrHovered && !textFieldFocusNode.hasFocus),\n      ),\n    );\n  }\n\n  Widget _buildChild(bool showTrash) {\n    return Row(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        ReorderableDragStartListener(\n          index: widget.index,\n          child: MouseRegion(\n            cursor: Platform.isWindows\n                ? SystemMouseCursors.click\n                : SystemMouseCursors.grab,\n            child: SizedBox(\n              width: 20,\n              height: 32,\n              child: Align(\n                alignment: AlignmentDirectional.centerEnd,\n                child: FlowySvg(\n                  FlowySvgs.drag_element_s,\n                  size: const Size.square(14),\n                  color: AFThemeExtension.of(context).onBackground,\n                ),\n              ),\n            ),\n          ),\n        ),\n        ChecklistCellCheckIcon(task: widget.task),\n        Expanded(\n          child: ChecklistCellTextfield(\n            textController: textController,\n            focusNode: textFieldFocusNode,\n            lineHeight: Platform.isWindows ? 1.2 : 1.1,\n            onChanged: () {\n              _debounceOnChanged.call(() {\n                if (!isComposing) {\n                  _submitUpdateTaskDescription(textController.text);\n                }\n              });\n            },\n            onSubmitted: () {\n              _submitUpdateTaskDescription(textController.text);\n\n              if (widget.onSubmitted != null) {\n                widget.onSubmitted?.call();\n              } else {\n                Actions.invoke(context, const NextFocusIntent());\n              }\n            },\n          ),\n        ),\n        if (showTrash)\n          ChecklistCellDeleteButton(\n            onPressed: () => context\n                .read<ChecklistCellBloc>()\n                .add(ChecklistCellEvent.deleteTask(widget.task.data.id)),\n          ),\n      ],\n    );\n  }\n\n  Map<ShortcutActivator, Intent> _buildShortcuts() {\n    return {\n      SingleActivator(\n        LogicalKeyboardKey.enter,\n        meta: Platform.isMacOS,\n        control: !Platform.isMacOS,\n      ): const _SelectTaskIntent(),\n      if (!isComposing)\n        const SingleActivator(LogicalKeyboardKey.enter):\n            const _UpdateTaskDescriptionIntent(),\n      if (!isComposing)\n        const SingleActivator(LogicalKeyboardKey.escape):\n            const _EndEditingTaskIntent(),\n    };\n  }\n\n  Map<Type, Action<Intent>> _buildActions() {\n    return {\n      _SelectTaskIntent: CallbackAction<_SelectTaskIntent>(\n        onInvoke: (_SelectTaskIntent intent) {\n          context\n              .read<ChecklistCellBloc>()\n              .add(ChecklistCellEvent.selectTask(widget.task.data.id));\n          return;\n        },\n      ),\n      _UpdateTaskDescriptionIntent:\n          CallbackAction<_UpdateTaskDescriptionIntent>(\n        onInvoke: (_UpdateTaskDescriptionIntent intent) {\n          textFieldFocusNode.unfocus();\n          widget.onSubmitted?.call();\n          return;\n        },\n      ),\n      _EndEditingTaskIntent: CallbackAction<_EndEditingTaskIntent>(\n        onInvoke: (_EndEditingTaskIntent intent) {\n          textFieldFocusNode.unfocus();\n          return;\n        },\n      ),\n    };\n  }\n\n  void _submitUpdateTaskDescription(String description) => context\n      .read<ChecklistCellBloc>()\n      .add(ChecklistCellEvent.updateTaskName(widget.task.data, description));\n}\n\n/// Creates a new task after entering the description and pressing enter.\n/// This can be cancelled by pressing escape\n@visibleForTesting\nclass NewTaskItem extends StatefulWidget {\n  const NewTaskItem({super.key, required this.focusNode});\n\n  final FocusNode focusNode;\n\n  @override\n  State<NewTaskItem> createState() => _NewTaskItemState();\n}\n\nclass _NewTaskItemState extends State<NewTaskItem> {\n  final textController = TextEditingController();\n\n  bool isCreateButtonEnabled = false;\n  bool isComposing = false;\n\n  @override\n  void initState() {\n    super.initState();\n    textController.addListener(_onTextChanged);\n    if (widget.focusNode.canRequestFocus) {\n      widget.focusNode.requestFocus();\n    }\n  }\n\n  void _onTextChanged() =>\n      setState(() => isComposing = !textController.value.composing.isCollapsed);\n\n  @override\n  void dispose() {\n    textController.removeListener(_onTextChanged);\n    textController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.symmetric(horizontal: 8),\n      constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight),\n      child: Row(\n        children: [\n          const HSpace(8),\n          Expanded(\n            child: CallbackShortcuts(\n              bindings: isComposing\n                  ? const {}\n                  : {\n                      const SingleActivator(LogicalKeyboardKey.enter): () =>\n                          _createNewTask(context),\n                    },\n              child: TextField(\n                focusNode: widget.focusNode,\n                controller: textController,\n                style: Theme.of(context).textTheme.bodyMedium,\n                maxLines: null,\n                decoration: InputDecoration(\n                  border: InputBorder.none,\n                  isCollapsed: true,\n                  contentPadding: const EdgeInsets.symmetric(\n                    vertical: 6.0,\n                    horizontal: 2.0,\n                  ),\n                  hintText: LocaleKeys.grid_checklist_addNew.tr(),\n                ),\n                onSubmitted: (_) => _createNewTask(context),\n                onChanged: (_) => setState(\n                  () => isCreateButtonEnabled = textController.text.isNotEmpty,\n                ),\n              ),\n            ),\n          ),\n          FlowyTextButton(\n            LocaleKeys.grid_checklist_submitNewTask.tr(),\n            fontSize: 11,\n            fillColor: isCreateButtonEnabled\n                ? Theme.of(context).colorScheme.primary\n                : Theme.of(context).disabledColor,\n            hoverColor: isCreateButtonEnabled\n                ? Theme.of(context).colorScheme.primaryContainer\n                : Theme.of(context).disabledColor,\n            fontColor: Theme.of(context).colorScheme.onPrimary,\n            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),\n            onPressed: isCreateButtonEnabled\n                ? () {\n                    context.read<ChecklistCellBloc>().add(\n                          ChecklistCellEvent.createNewTask(textController.text),\n                        );\n                    widget.focusNode.requestFocus();\n                    textController.clear();\n                  }\n                : null,\n          ),\n        ],\n      ),\n    );\n  }\n\n  void _createNewTask(BuildContext context) {\n    final taskDescription = textController.text;\n    if (taskDescription.isNotEmpty) {\n      context\n          .read<ChecklistCellBloc>()\n          .add(ChecklistCellEvent.createNewTask(taskDescription));\n      textController.clear();\n    }\n    widget.focusNode.requestFocus();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_textfield.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../application/cell/bloc/checklist_cell_bloc.dart';\n\nclass ChecklistCellCheckIcon extends StatelessWidget {\n  const ChecklistCellCheckIcon({\n    super.key,\n    required this.task,\n  });\n\n  final ChecklistSelectOption task;\n\n  @override\n  Widget build(BuildContext context) {\n    return ExcludeFocus(\n      child: FlowyIconButton(\n        width: 32,\n        icon: FlowySvg(\n          task.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,\n          blendMode: BlendMode.dst,\n        ),\n        hoverColor: Colors.transparent,\n        onPressed: () => context.read<ChecklistCellBloc>().add(\n              ChecklistCellEvent.selectTask(task.data.id),\n            ),\n      ),\n    );\n  }\n}\n\nclass ChecklistCellTextfield extends StatelessWidget {\n  const ChecklistCellTextfield({\n    super.key,\n    required this.textController,\n    required this.focusNode,\n    this.onChanged,\n    this.contentPadding = const EdgeInsets.symmetric(\n      vertical: 8,\n      horizontal: 2,\n    ),\n    this.onSubmitted,\n    this.lineHeight,\n  });\n\n  final TextEditingController textController;\n  final FocusNode focusNode;\n  final EdgeInsetsGeometry contentPadding;\n  final VoidCallback? onSubmitted;\n  final VoidCallback? onChanged;\n  final double? lineHeight;\n\n  @override\n  Widget build(BuildContext context) {\n    final textStyle = Theme.of(context).textTheme.bodyMedium;\n    return TextField(\n      controller: textController,\n      focusNode: focusNode,\n      style: textStyle?.copyWith(\n        height: lineHeight,\n      ),\n      maxLines: null,\n      decoration: InputDecoration(\n        border: InputBorder.none,\n        isCollapsed: true,\n        isDense: true,\n        contentPadding: contentPadding,\n        hintText: LocaleKeys.grid_checklist_taskHint.tr(),\n      ),\n      textInputAction: onSubmitted == null ? TextInputAction.next : null,\n      onChanged: (_) => onChanged?.call(),\n      onSubmitted: (_) => onSubmitted?.call(),\n    );\n  }\n}\n\nclass ChecklistCellDeleteButton extends StatefulWidget {\n  const ChecklistCellDeleteButton({\n    super.key,\n    required this.onPressed,\n  });\n\n  final VoidCallback onPressed;\n\n  @override\n  State<ChecklistCellDeleteButton> createState() =>\n      _ChecklistCellDeleteButtonState();\n}\n\nclass _ChecklistCellDeleteButtonState extends State<ChecklistCellDeleteButton> {\n  final _materialStatesController = WidgetStatesController();\n\n  @override\n  void dispose() {\n    _materialStatesController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return TextButton(\n      onPressed: widget.onPressed,\n      onHover: (_) => setState(() {}),\n      onFocusChange: (_) => setState(() {}),\n      style: ButtonStyle(\n        fixedSize: const WidgetStatePropertyAll(Size.square(32)),\n        minimumSize: const WidgetStatePropertyAll(Size.square(32)),\n        maximumSize: const WidgetStatePropertyAll(Size.square(32)),\n        overlayColor: WidgetStateProperty.resolveWith((state) {\n          if (state.contains(WidgetState.focused)) {\n            return AFThemeExtension.of(context).greyHover;\n          }\n          return Colors.transparent;\n        }),\n        shape: const WidgetStatePropertyAll(\n          RoundedRectangleBorder(borderRadius: Corners.s6Border),\n        ),\n      ),\n      statesController: _materialStatesController,\n      child: FlowySvg(\n        FlowySvgs.delete_s,\n        color: _materialStatesController.value.contains(WidgetState.hovered) ||\n                _materialStatesController.value.contains(WidgetState.focused)\n            ? Theme.of(context).colorScheme.error\n            : null,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_progress_bar.dart",
    "content": "import 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\nimport 'package:percent_indicator/percent_indicator.dart';\n\nimport '../../application/cell/bloc/checklist_cell_bloc.dart';\n\nclass ChecklistProgressBar extends StatefulWidget {\n  const ChecklistProgressBar({\n    super.key,\n    required this.tasks,\n    required this.percent,\n    this.textStyle,\n  });\n\n  final List<ChecklistSelectOption> tasks;\n  final double percent;\n  final TextStyle? textStyle;\n  final int segmentLimit = 5;\n\n  @override\n  State<ChecklistProgressBar> createState() => _ChecklistProgressBarState();\n}\n\nclass _ChecklistProgressBarState extends State<ChecklistProgressBar> {\n  @override\n  Widget build(BuildContext context) {\n    final numFinishedTasks = widget.tasks.where((e) => e.isSelected).length;\n    final completedTaskColor = numFinishedTasks == widget.tasks.length\n        ? AFThemeExtension.of(context).success\n        : Theme.of(context).colorScheme.primary;\n\n    return Row(\n      children: [\n        Expanded(\n          child: widget.tasks.isNotEmpty &&\n                  widget.tasks.length <= widget.segmentLimit\n              ? Row(\n                  children: [\n                    ...List<Widget>.generate(\n                      widget.tasks.length,\n                      (index) => Flexible(\n                        child: Container(\n                          decoration: BoxDecoration(\n                            borderRadius:\n                                const BorderRadius.all(Radius.circular(2)),\n                            color: index < numFinishedTasks\n                                ? completedTaskColor\n                                : AFThemeExtension.of(context)\n                                    .progressBarBGColor,\n                          ),\n                          margin: const EdgeInsets.symmetric(horizontal: 1),\n                          height: 4.0,\n                        ),\n                      ),\n                    ),\n                  ],\n                )\n              : LinearPercentIndicator(\n                  lineHeight: 4.0,\n                  percent: widget.percent,\n                  padding: EdgeInsets.zero,\n                  progressColor: completedTaskColor,\n                  backgroundColor:\n                      AFThemeExtension.of(context).progressBarBGColor,\n                  barRadius: const Radius.circular(2),\n                ),\n        ),\n        SizedBox(\n          width: 45,\n          child: Align(\n            alignment: AlignmentDirectional.centerEnd,\n            child: Text(\n              \"${(widget.percent * 100).round()}%\",\n              style: widget.textStyle,\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/date_cell_editor.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../application/cell/bloc/date_cell_editor_bloc.dart';\n\nclass DateCellEditor extends StatefulWidget {\n  const DateCellEditor({\n    super.key,\n    required this.onDismissed,\n    required this.cellController,\n  });\n\n  final VoidCallback onDismissed;\n  final DateCellController cellController;\n\n  @override\n  State<StatefulWidget> createState() => _DateCellEditor();\n}\n\nclass _DateCellEditor extends State<DateCellEditor> {\n  final PopoverMutex popoverMutex = PopoverMutex();\n\n  @override\n  void dispose() {\n    popoverMutex.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<DateCellEditorBloc>(\n      create: (context) => DateCellEditorBloc(\n        reminderBloc: getIt<ReminderBloc>(),\n        cellController: widget.cellController,\n      ),\n      child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(\n        builder: (context, state) {\n          final dateCellBloc = context.read<DateCellEditorBloc>();\n          return DesktopAppFlowyDatePicker(\n            dateTime: state.dateTime,\n            endDateTime: state.endDateTime,\n            dateFormat: state.dateTypeOptionPB.dateFormat,\n            timeFormat: state.dateTypeOptionPB.timeFormat,\n            includeTime: state.includeTime,\n            isRange: state.isRange,\n            reminderOption: state.reminderOption,\n            popoverMutex: popoverMutex,\n            options: [\n              OptionGroup(\n                options: [\n                  DateTypeOptionButton(\n                    popoverMutex: popoverMutex,\n                    dateFormat: state.dateTypeOptionPB.dateFormat,\n                    timeFormat: state.dateTypeOptionPB.timeFormat,\n                    onDateFormatChanged: (format) {\n                      dateCellBloc\n                          .add(DateCellEditorEvent.setDateFormat(format));\n                    },\n                    onTimeFormatChanged: (format) {\n                      dateCellBloc\n                          .add(DateCellEditorEvent.setTimeFormat(format));\n                    },\n                  ),\n                  ClearDateButton(\n                    onClearDate: () {\n                      dateCellBloc.add(const DateCellEditorEvent.clearDate());\n                    },\n                  ),\n                ],\n              ),\n            ],\n            onIncludeTimeChanged: (value, dateTime, endDateTime) {\n              dateCellBloc.add(\n                DateCellEditorEvent.setIncludeTime(\n                  value,\n                  dateTime,\n                  endDateTime,\n                ),\n              );\n            },\n            onIsRangeChanged: (value, dateTime, endDateTime) {\n              dateCellBloc.add(\n                DateCellEditorEvent.setIsRange(value, dateTime, endDateTime),\n              );\n            },\n            onDaySelected: (selectedDay) {\n              dateCellBloc.add(DateCellEditorEvent.updateDateTime(selectedDay));\n            },\n            onRangeSelected: (start, end) {\n              dateCellBloc.add(DateCellEditorEvent.updateDateRange(start, end));\n            },\n            onReminderSelected: (option) {\n              dateCellBloc.add(DateCellEditorEvent.setReminderOption(option));\n            },\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/extension.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nextension SelectOptionColorExtension on SelectOptionColorPB {\n  Color toColor(BuildContext context) {\n    switch (this) {\n      case SelectOptionColorPB.Purple:\n        return AFThemeExtension.of(context).tint1;\n      case SelectOptionColorPB.Pink:\n        return AFThemeExtension.of(context).tint2;\n      case SelectOptionColorPB.LightPink:\n        return AFThemeExtension.of(context).tint3;\n      case SelectOptionColorPB.Orange:\n        return AFThemeExtension.of(context).tint4;\n      case SelectOptionColorPB.Yellow:\n        return AFThemeExtension.of(context).tint5;\n      case SelectOptionColorPB.Lime:\n        return AFThemeExtension.of(context).tint6;\n      case SelectOptionColorPB.Green:\n        return AFThemeExtension.of(context).tint7;\n      case SelectOptionColorPB.Aqua:\n        return AFThemeExtension.of(context).tint8;\n      case SelectOptionColorPB.Blue:\n        return AFThemeExtension.of(context).tint9;\n      default:\n        throw ArgumentError;\n    }\n  }\n\n  String colorName() {\n    switch (this) {\n      case SelectOptionColorPB.Purple:\n        return LocaleKeys.grid_selectOption_purpleColor.tr();\n      case SelectOptionColorPB.Pink:\n        return LocaleKeys.grid_selectOption_pinkColor.tr();\n      case SelectOptionColorPB.LightPink:\n        return LocaleKeys.grid_selectOption_lightPinkColor.tr();\n      case SelectOptionColorPB.Orange:\n        return LocaleKeys.grid_selectOption_orangeColor.tr();\n      case SelectOptionColorPB.Yellow:\n        return LocaleKeys.grid_selectOption_yellowColor.tr();\n      case SelectOptionColorPB.Lime:\n        return LocaleKeys.grid_selectOption_limeColor.tr();\n      case SelectOptionColorPB.Green:\n        return LocaleKeys.grid_selectOption_greenColor.tr();\n      case SelectOptionColorPB.Aqua:\n        return LocaleKeys.grid_selectOption_aquaColor.tr();\n      case SelectOptionColorPB.Blue:\n        return LocaleKeys.grid_selectOption_blueColor.tr();\n      default:\n        throw ArgumentError;\n    }\n  }\n}\n\nclass SelectOptionTag extends StatelessWidget {\n  const SelectOptionTag({\n    super.key,\n    this.option,\n    this.name,\n    this.fontSize,\n    this.color,\n    this.textStyle,\n    this.onRemove,\n    this.textAlign,\n    this.isExpanded = false,\n    this.borderRadius,\n    required this.padding,\n  }) : assert(option != null || name != null && color != null);\n\n  final SelectOptionPB? option;\n  final String? name;\n  final double? fontSize;\n  final Color? color;\n  final TextStyle? textStyle;\n  final void Function(String)? onRemove;\n  final EdgeInsets padding;\n  final BorderRadius? borderRadius;\n  final TextAlign? textAlign;\n  final bool isExpanded;\n\n  @override\n  Widget build(BuildContext context) {\n    final optionName = option?.name ?? name!;\n    final optionColor = option?.color.toColor(context) ?? color!;\n    final text = FlowyText(\n      optionName,\n      fontSize: fontSize,\n      overflow: TextOverflow.ellipsis,\n      color: AFThemeExtension.of(context).textColor,\n      textAlign: textAlign,\n    );\n\n    return Container(\n      padding: onRemove == null ? padding : padding.copyWith(right: 2.0),\n      decoration: BoxDecoration(\n        color: optionColor,\n        borderRadius: borderRadius ??\n            BorderRadius.circular(UniversalPlatform.isDesktopOrWeb ? 6 : 11),\n      ),\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          isExpanded ? Expanded(child: text) : Flexible(child: text),\n          if (onRemove != null) ...[\n            const HSpace(4),\n            FlowyIconButton(\n              width: 16.0,\n              onPressed: () => onRemove?.call(optionName),\n              hoverColor: Colors.transparent,\n              icon: const FlowySvg(FlowySvgs.close_s),\n            ),\n          ],\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/media_cell_editor.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';\nimport 'package:flutter/material.dart';\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/media_file_type_ext.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_block_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/shared/af_image.dart';\nimport 'package:appflowy/util/xfile_ext.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';\nimport 'package:cross_file/cross_file.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MediaCellEditor extends StatefulWidget {\n  const MediaCellEditor({super.key});\n\n  @override\n  State<MediaCellEditor> createState() => _MediaCellEditorState();\n}\n\nclass _MediaCellEditorState extends State<MediaCellEditor> {\n  final addFilePopoverController = PopoverController();\n  final itemMutex = PopoverMutex();\n\n  @override\n  void dispose() {\n    addFilePopoverController.close();\n    itemMutex.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<MediaCellBloc, MediaCellState>(\n      builder: (context, state) {\n        final images = state.files\n            .where((file) => file.fileType == MediaFileTypePB.Image)\n            .toList();\n\n        return Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            if (state.files.isNotEmpty) ...[\n              Flexible(\n                child: ReorderableListView.builder(\n                  padding: const EdgeInsets.all(6),\n                  physics: const ClampingScrollPhysics(),\n                  shrinkWrap: true,\n                  buildDefaultDragHandles: false,\n                  itemBuilder: (_, index) => BlocProvider.value(\n                    key: Key(state.files[index].id),\n                    value: context.read<MediaCellBloc>(),\n                    child: RenderMedia(\n                      file: state.files[index],\n                      images: images,\n                      index: index,\n                      enableReordering: state.files.length > 1,\n                      mutex: itemMutex,\n                    ),\n                  ),\n                  itemCount: state.files.length,\n                  onReorder: (from, to) {\n                    if (from < to) {\n                      to--;\n                    }\n\n                    context\n                        .read<MediaCellBloc>()\n                        .add(MediaCellEvent.reorderFiles(from: from, to: to));\n                  },\n                  proxyDecorator: (child, index, animation) => Material(\n                    color: Colors.transparent,\n                    child: SizeTransition(\n                      sizeFactor: animation,\n                      child: child,\n                    ),\n                  ),\n                ),\n              ),\n            ],\n            _AddButton(addFilePopoverController: addFilePopoverController),\n          ],\n        );\n      },\n    );\n  }\n}\n\nclass _AddButton extends StatelessWidget {\n  const _AddButton({required this.addFilePopoverController});\n\n  final PopoverController addFilePopoverController;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      controller: addFilePopoverController,\n      direction: PopoverDirection.bottomWithCenterAligned,\n      offset: const Offset(0, 10),\n      constraints: const BoxConstraints(maxWidth: 350),\n      triggerActions: PopoverTriggerFlags.none,\n      popupBuilder: (popoverContext) => FileUploadMenu(\n        allowMultipleFiles: true,\n        onInsertLocalFile: (files) async => insertLocalFiles(\n          context,\n          files,\n          userProfile: context.read<MediaCellBloc>().state.userProfile,\n          documentId: context.read<MediaCellBloc>().rowId,\n          onUploadSuccess: (file, path, isLocalMode) {\n            final mediaCellBloc = context.read<MediaCellBloc>();\n            if (mediaCellBloc.isClosed) {\n              return;\n            }\n\n            mediaCellBloc.add(\n              MediaCellEvent.addFile(\n                url: path,\n                name: file.name,\n                uploadType: isLocalMode\n                    ? FileUploadTypePB.LocalFile\n                    : FileUploadTypePB.CloudFile,\n                fileType: file.fileType.toMediaFileTypePB(),\n              ),\n            );\n\n            addFilePopoverController.close();\n          },\n        ),\n        onInsertNetworkFile: (url) {\n          if (url.isEmpty) return;\n\n          final uri = Uri.tryParse(url);\n          if (uri == null) {\n            return;\n          }\n\n          final fakeFile = XFile(uri.path);\n          MediaFileTypePB fileType = fakeFile.fileType.toMediaFileTypePB();\n          fileType = fileType == MediaFileTypePB.Other\n              ? MediaFileTypePB.Link\n              : fileType;\n\n          String name =\n              uri.pathSegments.isNotEmpty ? uri.pathSegments.last : \"\";\n          if (name.isEmpty && uri.pathSegments.length > 1) {\n            name = uri.pathSegments[uri.pathSegments.length - 2];\n          } else if (name.isEmpty) {\n            name = uri.host;\n          }\n\n          context.read<MediaCellBloc>().add(\n                MediaCellEvent.addFile(\n                  url: url,\n                  name: name,\n                  uploadType: FileUploadTypePB.NetworkFile,\n                  fileType: fileType,\n                ),\n              );\n\n          addFilePopoverController.close();\n        },\n      ),\n      child: DecoratedBox(\n        decoration: BoxDecoration(\n          border: Border(\n            top: BorderSide(\n              color: Theme.of(context).dividerColor,\n            ),\n          ),\n        ),\n        child: Padding(\n          padding: const EdgeInsets.all(8.0),\n          child: GestureDetector(\n            behavior: HitTestBehavior.translucent,\n            onTap: addFilePopoverController.show,\n            child: FlowyHover(\n              resetHoverOnRebuild: false,\n              child: Padding(\n                padding: const EdgeInsets.all(4.0),\n                child: Row(\n                  children: [\n                    FlowySvg(\n                      FlowySvgs.add_thin_s,\n                      size: const Size.square(14),\n                      color: AFThemeExtension.of(context).lightIconColor,\n                    ),\n                    const HSpace(8),\n                    FlowyText.regular(\n                      LocaleKeys.grid_media_addFileOrImage.tr(),\n                      figmaLineHeight: 20,\n                      fontSize: 14,\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nextension ToCustomImageType on FileUploadTypePB {\n  CustomImageType toCustomImageType() => switch (this) {\n        FileUploadTypePB.NetworkFile => CustomImageType.external,\n        FileUploadTypePB.CloudFile => CustomImageType.internal,\n        _ => CustomImageType.local,\n      };\n}\n\n@visibleForTesting\nclass RenderMedia extends StatefulWidget {\n  const RenderMedia({\n    super.key,\n    required this.index,\n    required this.file,\n    required this.images,\n    required this.enableReordering,\n    required this.mutex,\n  });\n\n  final int index;\n  final MediaFilePB file;\n  final List<MediaFilePB> images;\n  final bool enableReordering;\n  final PopoverMutex mutex;\n\n  @override\n  State<RenderMedia> createState() => _RenderMediaState();\n}\n\nclass _RenderMediaState extends State<RenderMedia> {\n  bool isHovering = false;\n  int? imageIndex;\n\n  MediaFilePB get file => widget.file;\n\n  late final controller = PopoverController();\n\n  @override\n  void initState() {\n    super.initState();\n    imageIndex = widget.images.indexOf(file);\n  }\n\n  @override\n  void didUpdateWidget(covariant RenderMedia oldWidget) {\n    imageIndex = widget.images.indexOf(file);\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.only(bottom: 4),\n      child: MouseRegion(\n        onEnter: (_) => setState(() => isHovering = true),\n        onExit: (_) => setState(() => isHovering = false),\n        cursor: SystemMouseCursors.click,\n        child: DecoratedBox(\n          decoration: BoxDecoration(\n            borderRadius: BorderRadius.circular(4),\n            color: isHovering\n                ? AFThemeExtension.of(context).greyHover\n                : Colors.transparent,\n          ),\n          child: Row(\n            crossAxisAlignment: widget.file.fileType == MediaFileTypePB.Image\n                ? CrossAxisAlignment.start\n                : CrossAxisAlignment.center,\n            children: [\n              ReorderableDragStartListener(\n                index: widget.index,\n                enabled: widget.enableReordering,\n                child: Padding(\n                  padding: const EdgeInsets.all(4),\n                  child: FlowySvg(\n                    FlowySvgs.drag_element_s,\n                    color: AFThemeExtension.of(context).lightIconColor,\n                  ),\n                ),\n              ),\n              const HSpace(4),\n              if (widget.file.fileType == MediaFileTypePB.Image) ...[\n                Flexible(\n                  child: Padding(\n                    padding: const EdgeInsets.symmetric(vertical: 4),\n                    child: _openInteractiveViewer(\n                      context,\n                      files: widget.images,\n                      index: imageIndex!,\n                      child: AFImage(\n                        url: widget.file.url,\n                        uploadType: widget.file.uploadType,\n                        userProfile:\n                            context.read<MediaCellBloc>().state.userProfile,\n                      ),\n                    ),\n                  ),\n                ),\n              ] else ...[\n                Expanded(\n                  child: GestureDetector(\n                    onTap: () => afLaunchUrlString(file.url),\n                    child: Row(\n                      children: [\n                        Padding(\n                          padding: const EdgeInsets.symmetric(vertical: 4),\n                          child: FlowySvg(\n                            file.fileType.icon,\n                            color: AFThemeExtension.of(context).strongText,\n                            size: const Size.square(12),\n                          ),\n                        ),\n                        const HSpace(8),\n                        Flexible(\n                          child: Padding(\n                            padding: const EdgeInsets.only(bottom: 1),\n                            child: FlowyText(\n                              file.name,\n                              overflow: TextOverflow.ellipsis,\n                              fontSize: 14,\n                            ),\n                          ),\n                        ),\n                      ],\n                    ),\n                  ),\n                ),\n              ],\n              const HSpace(4),\n              AppFlowyPopover(\n                controller: controller,\n                mutex: widget.mutex,\n                asBarrier: true,\n                offset: const Offset(0, 4),\n                constraints: const BoxConstraints(maxWidth: 240),\n                direction: PopoverDirection.bottomWithLeftAligned,\n                popupBuilder: (popoverContext) => BlocProvider.value(\n                  value: context.read<MediaCellBloc>(),\n                  child: MediaItemMenu(\n                    file: file,\n                    images: widget.images,\n                    index: imageIndex ?? -1,\n                    closeContext: popoverContext,\n                    onAction: () => controller.close(),\n                  ),\n                ),\n                child: FlowyIconButton(\n                  hoverColor: Colors.transparent,\n                  width: 24,\n                  icon: FlowySvg(\n                    FlowySvgs.three_dots_s,\n                    size: const Size.square(16),\n                    color: AFThemeExtension.of(context).lightIconColor,\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _openInteractiveViewer(\n    BuildContext context, {\n    required List<MediaFilePB> files,\n    required int index,\n    required Widget child,\n  }) =>\n      GestureDetector(\n        behavior: HitTestBehavior.translucent,\n        onTap: () => openInteractiveViewerFromFiles(\n          context,\n          files,\n          onDeleteImage: (index) {\n            final deleteFile = files[index];\n            context.read<MediaCellBloc>().deleteFile(deleteFile.id);\n          },\n          userProfile: context.read<MediaCellBloc>().state.userProfile,\n          initialIndex: index,\n        ),\n        child: child,\n      );\n}\n\nclass MediaItemMenu extends StatefulWidget {\n  const MediaItemMenu({\n    super.key,\n    required this.file,\n    required this.images,\n    required this.index,\n    this.closeContext,\n    this.onAction,\n  });\n\n  /// The [MediaFilePB] this menu concerns\n  final MediaFilePB file;\n\n  /// The list of [MediaFilePB] which are images\n  /// This is used to show the [InteractiveImageViewer]\n  final List<MediaFilePB> images;\n\n  /// The index of the [MediaFilePB] in the [images] list\n  final int index;\n\n  /// The [BuildContext] used to show the [InteractiveImageViewer]\n  final BuildContext? closeContext;\n\n  /// Callback to be called when an action is performed\n  final VoidCallback? onAction;\n\n  @override\n  State<MediaItemMenu> createState() => _MediaItemMenuState();\n}\n\nclass _MediaItemMenuState extends State<MediaItemMenu> {\n  late final nameController = TextEditingController(text: widget.file.name);\n  final errorMessage = ValueNotifier<String?>(null);\n\n  BuildContext? renameContext;\n\n  @override\n  void dispose() {\n    nameController.dispose();\n    errorMessage.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SeparatedColumn(\n      separatorBuilder: () => const VSpace(8),\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        if (widget.file.fileType == MediaFileTypePB.Image) ...[\n          MediaMenuItem(\n            onTap: () {\n              widget.onAction?.call();\n              _showInteractiveViewer();\n            },\n            icon: FlowySvgs.full_view_s,\n            label: LocaleKeys.grid_media_expand.tr(),\n          ),\n          MediaMenuItem(\n            onTap: () {\n              context.read<MediaCellBloc>().add(\n                    MediaCellEvent.setCover(\n                      RowCoverPB(\n                        data: widget.file.url,\n                        uploadType: widget.file.uploadType,\n                        coverType: CoverTypePB.FileCover,\n                      ),\n                    ),\n                  );\n              widget.onAction?.call();\n            },\n            icon: FlowySvgs.cover_s,\n            label: LocaleKeys.grid_media_setAsCover.tr(),\n          ),\n        ],\n        MediaMenuItem(\n          onTap: () {\n            widget.onAction?.call();\n            afLaunchUrlString(widget.file.url);\n          },\n          icon: FlowySvgs.open_in_browser_s,\n          label: LocaleKeys.grid_media_openInBrowser.tr(),\n        ),\n        MediaMenuItem(\n          onTap: () async {\n            await _showRenameDialog();\n            widget.onAction?.call();\n          },\n          icon: FlowySvgs.rename_s,\n          label: LocaleKeys.grid_media_rename.tr(),\n        ),\n        if (widget.file.uploadType == FileUploadTypePB.CloudFile) ...[\n          MediaMenuItem(\n            onTap: () async {\n              await downloadMediaFile(\n                context,\n                widget.file,\n                userProfile: context.read<MediaCellBloc>().state.userProfile,\n              );\n              widget.onAction?.call();\n            },\n            icon: FlowySvgs.save_as_s,\n            label: LocaleKeys.button_download.tr(),\n          ),\n        ],\n        MediaMenuItem(\n          onTap: () async {\n            await showConfirmDeletionDialog(\n              context: context,\n              name: widget.file.name,\n              description: LocaleKeys.grid_media_deleteFileDescription.tr(),\n              onConfirm: () => context\n                  .read<MediaCellBloc>()\n                  .add(MediaCellEvent.removeFile(fileId: widget.file.id)),\n            );\n            widget.onAction?.call();\n          },\n          icon: FlowySvgs.trash_s,\n          label: LocaleKeys.button_delete.tr(),\n        ),\n      ],\n    );\n  }\n\n  Future<void> _showRenameDialog() async {\n    nameController.selection = TextSelection(\n      baseOffset: 0,\n      extentOffset: nameController.text.length,\n    );\n\n    await showCustomConfirmDialog(\n      context: context,\n      title: LocaleKeys.document_plugins_file_renameFile_title.tr(),\n      description: LocaleKeys.document_plugins_file_renameFile_description.tr(),\n      closeOnConfirm: false,\n      builder: (dialogContext) {\n        renameContext = dialogContext;\n        return FileRenameTextField(\n          nameController: nameController,\n          errorMessage: errorMessage,\n          onSubmitted: () => _saveName(context),\n          disposeController: false,\n        );\n      },\n      confirmLabel: LocaleKeys.button_save.tr(),\n      onConfirm: () => _saveName(context),\n    );\n  }\n\n  void _saveName(BuildContext context) {\n    if (nameController.text.isEmpty) {\n      errorMessage.value =\n          LocaleKeys.document_plugins_file_renameFile_nameEmptyError.tr();\n      return;\n    }\n\n    context.read<MediaCellBloc>().add(\n          MediaCellEvent.renameFile(\n            fileId: widget.file.id,\n            name: nameController.text,\n          ),\n        );\n\n    if (renameContext != null) {\n      Navigator.of(renameContext!).pop();\n    }\n  }\n\n  void _showInteractiveViewer() {\n    showDialog(\n      context: widget.closeContext ?? context,\n      builder: (_) => InteractiveImageViewer(\n        userProfile: context.read<MediaCellBloc>().state.userProfile,\n        imageProvider: AFBlockImageProvider(\n          initialIndex: widget.index,\n          images: widget.images\n              .map(\n                (e) => ImageBlockData(\n                  url: e.url,\n                  type: e.uploadType.toCustomImageType(),\n                ),\n              )\n              .toList(),\n          onDeleteImage: (index) {\n            final deleteFile = widget.images[index];\n            context.read<MediaCellBloc>().deleteFile(deleteFile.id);\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass MediaMenuItem extends StatelessWidget {\n  const MediaMenuItem({\n    super.key,\n    required this.onTap,\n    required this.icon,\n    required this.label,\n  });\n\n  final VoidCallback onTap;\n  final FlowySvgData icon;\n  final String label;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      onTap: onTap,\n      leftIcon: FlowySvg(icon),\n      text: Padding(\n        padding: const EdgeInsets.only(left: 4, top: 1, bottom: 1),\n        child: FlowyText.regular(\n          label,\n          figmaLineHeight: 20,\n        ),\n      ),\n      hoverColor: AFThemeExtension.of(context).lightGreyHover,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/plugins/base/drag_handler.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nclass MobileChecklistCellEditScreen extends StatefulWidget {\n  const MobileChecklistCellEditScreen({super.key});\n\n  @override\n  State<MobileChecklistCellEditScreen> createState() =>\n      _MobileChecklistCellEditScreenState();\n}\n\nclass _MobileChecklistCellEditScreenState\n    extends State<MobileChecklistCellEditScreen> {\n  @override\n  Widget build(BuildContext context) {\n    return ConstrainedBox(\n      constraints: const BoxConstraints.tightFor(height: 420),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          const DragHandle(),\n          Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 8.0),\n            child: _buildHeader(context),\n          ),\n          const Divider(),\n          const Expanded(child: _TaskList()),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildHeader(BuildContext context) {\n    return Stack(\n      children: [\n        SizedBox(\n          height: 44.0,\n          child: Align(\n            child: FlowyText.medium(\n              LocaleKeys.grid_field_checklistFieldName.tr(),\n              fontSize: 18,\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass _TaskList extends StatelessWidget {\n  const _TaskList();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<ChecklistCellBloc, ChecklistCellState>(\n      builder: (context, state) {\n        final cells = <Widget>[];\n        cells.addAll(\n          state.tasks\n              .mapIndexed(\n                (index, task) => _ChecklistItem(\n                  key: ValueKey('mobile_checklist_task_${task.data.id}'),\n                  task: task,\n                  index: index,\n                  autofocus: state.phantomIndex != null &&\n                      index == state.tasks.length - 1,\n                  onAutofocus: () {\n                    context\n                        .read<ChecklistCellBloc>()\n                        .add(const ChecklistCellEvent.updatePhantomIndex(null));\n                  },\n                ),\n              )\n              .toList(),\n        );\n        cells.add(\n          const _NewTaskButton(key: ValueKey('mobile_checklist_new_task')),\n        );\n\n        return ReorderableListView.builder(\n          shrinkWrap: true,\n          proxyDecorator: (child, index, _) => Material(\n            color: Colors.transparent,\n            child: BlocProvider.value(\n              value: context.read<ChecklistCellBloc>(),\n              child: child,\n            ),\n          ),\n          buildDefaultDragHandles: false,\n          itemCount: cells.length,\n          itemBuilder: (_, index) => cells[index],\n          padding: const EdgeInsets.only(bottom: 12.0),\n          onReorder: (from, to) {\n            context\n                .read<ChecklistCellBloc>()\n                .add(ChecklistCellEvent.reorderTask(from, to));\n          },\n        );\n      },\n    );\n  }\n}\n\nclass _ChecklistItem extends StatefulWidget {\n  const _ChecklistItem({\n    super.key,\n    required this.task,\n    required this.index,\n    required this.autofocus,\n    this.onAutofocus,\n  });\n\n  final ChecklistSelectOption task;\n  final int index;\n  final bool autofocus;\n  final VoidCallback? onAutofocus;\n\n  @override\n  State<_ChecklistItem> createState() => _ChecklistItemState();\n}\n\nclass _ChecklistItemState extends State<_ChecklistItem> {\n  late final TextEditingController textController;\n  final FocusNode focusNode = FocusNode();\n  Timer? _debounceOnChanged;\n\n  @override\n  void initState() {\n    super.initState();\n    textController = TextEditingController(text: widget.task.data.name);\n    if (widget.autofocus) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        focusNode.requestFocus();\n        widget.onAutofocus?.call();\n      });\n    }\n  }\n\n  @override\n  void didUpdateWidget(covariant oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    if (widget.task.data.name != oldWidget.task.data.name &&\n        !focusNode.hasFocus) {\n      textController.text = widget.task.data.name;\n    }\n  }\n\n  @override\n  void dispose() {\n    textController.dispose();\n    focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 4),\n      constraints: const BoxConstraints(minHeight: 44),\n      child: Row(\n        children: [\n          ReorderableDelayedDragStartListener(\n            index: widget.index,\n            child: InkWell(\n              borderRadius: BorderRadius.circular(22),\n              onTap: () => context\n                  .read<ChecklistCellBloc>()\n                  .add(ChecklistCellEvent.selectTask(widget.task.data.id)),\n              child: SizedBox.square(\n                dimension: 44,\n                child: Center(\n                  child: FlowySvg(\n                    widget.task.isSelected\n                        ? FlowySvgs.check_filled_s\n                        : FlowySvgs.uncheck_s,\n                    size: const Size.square(20.0),\n                    blendMode: BlendMode.dst,\n                  ),\n                ),\n              ),\n            ),\n          ),\n          Expanded(\n            child: TextField(\n              controller: textController,\n              focusNode: focusNode,\n              style: Theme.of(context).textTheme.bodyMedium,\n              keyboardType: TextInputType.multiline,\n              maxLines: null,\n              decoration: InputDecoration(\n                border: InputBorder.none,\n                enabledBorder: InputBorder.none,\n                focusedBorder: InputBorder.none,\n                isCollapsed: true,\n                isDense: true,\n                contentPadding: const EdgeInsets.symmetric(vertical: 12),\n                hintText: LocaleKeys.grid_checklist_taskHint.tr(),\n              ),\n              onChanged: _debounceOnChangedText,\n              onSubmitted: (description) {\n                _submitUpdateTaskDescription(description);\n              },\n            ),\n          ),\n          InkWell(\n            borderRadius: BorderRadius.circular(22),\n            onTap: _showDeleteTaskBottomSheet,\n            child: SizedBox.square(\n              dimension: 44,\n              child: Center(\n                child: FlowySvg(\n                  FlowySvgs.three_dots_s,\n                  color: Theme.of(context).hintColor,\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  void _debounceOnChangedText(String text) {\n    _debounceOnChanged?.cancel();\n    _debounceOnChanged = Timer(const Duration(milliseconds: 300), () {\n      _submitUpdateTaskDescription(text);\n    });\n  }\n\n  void _submitUpdateTaskDescription(String description) {\n    context.read<ChecklistCellBloc>().add(\n          ChecklistCellEvent.updateTaskName(\n            widget.task.data,\n            description.trim(),\n          ),\n        );\n  }\n\n  void _showDeleteTaskBottomSheet() {\n    showMobileBottomSheet(\n      context,\n      showDragHandle: true,\n      builder: (_) => Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 8.0),\n            child: InkWell(\n              onTap: () {\n                context.read<ChecklistCellBloc>().add(\n                      ChecklistCellEvent.deleteTask(widget.task.data.id),\n                    );\n                context.pop();\n              },\n              borderRadius: BorderRadius.circular(12),\n              child: Container(\n                height: 44,\n                padding: const EdgeInsets.symmetric(horizontal: 8),\n                child: Row(\n                  children: [\n                    FlowySvg(\n                      FlowySvgs.m_delete_m,\n                      size: const Size.square(20),\n                      color: Theme.of(context).colorScheme.error,\n                    ),\n                    const HSpace(8),\n                    FlowyText(\n                      LocaleKeys.button_delete.tr(),\n                      fontSize: 15,\n                      color: Theme.of(context).colorScheme.error,\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n          const Divider(height: 9),\n        ],\n      ),\n    );\n  }\n}\n\nclass _NewTaskButton extends StatelessWidget {\n  const _NewTaskButton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 8.0),\n      child: InkWell(\n        borderRadius: BorderRadius.circular(12),\n        onTap: () {\n          context\n              .read<ChecklistCellBloc>()\n              .add(const ChecklistCellEvent.updatePhantomIndex(-1));\n          context\n              .read<ChecklistCellBloc>()\n              .add(const ChecklistCellEvent.createNewTask(\"\"));\n        },\n        child: Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 13),\n          child: Row(\n            children: [\n              const FlowySvg(FlowySvgs.add_s, size: Size.square(20)),\n              const HSpace(11),\n              FlowyText(LocaleKeys.grid_checklist_addNew.tr(), fontSize: 15),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_media_cell_editor.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_media_upload.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';\nimport 'package:appflowy/plugins/base/drag_handler.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/media_file_type_ext.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/image_render.dart';\nimport 'package:appflowy/shared/loading.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nclass MobileMediaCellEditor extends StatelessWidget {\n  const MobileMediaCellEditor({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      constraints: const BoxConstraints.tightFor(height: 420),\n      child: BlocProvider.value(\n        value: context.read<MediaCellBloc>(),\n        child: BlocBuilder<MediaCellBloc, MediaCellState>(\n          builder: (context, state) => Column(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              const DragHandle(),\n              SizedBox(\n                height: 46.0,\n                child: Stack(\n                  children: [\n                    Align(\n                      child: FlowyText.medium(\n                        LocaleKeys.grid_field_mediaFieldName.tr(),\n                        fontSize: 18,\n                      ),\n                    ),\n                    Positioned(\n                      top: 8,\n                      right: 18,\n                      child: GestureDetector(\n                        onTap: () => showMobileBottomSheet(\n                          context,\n                          title: LocaleKeys.grid_media_addFileMobile.tr(),\n                          showHeader: true,\n                          showCloseButton: true,\n                          showDragHandle: true,\n                          builder: (dContext) => BlocProvider.value(\n                            value: context.read<MediaCellBloc>(),\n                            child: MobileMediaUploadSheetContent(\n                              dialogContext: dContext,\n                            ),\n                          ),\n                        ),\n                        child: const FlowySvg(\n                          FlowySvgs.add_m,\n                          size: Size.square(28),\n                        ),\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n              const Divider(height: 0.5),\n              Expanded(\n                child: SingleChildScrollView(\n                  child: Column(\n                    children: [\n                      if (state.files.isNotEmpty) const Divider(height: .5),\n                      ...state.files.map(\n                        (file) => Padding(\n                          padding: const EdgeInsets.only(bottom: 2),\n                          child: _FileItem(key: Key(file.id), file: file),\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _FileItem extends StatelessWidget {\n  const _FileItem({super.key, required this.file});\n\n  final MediaFilePB file;\n\n  @override\n  Widget build(BuildContext context) {\n    return DecoratedBox(\n      decoration: BoxDecoration(\n        color: Theme.of(context).scaffoldBackgroundColor,\n      ),\n      child: ListTile(\n        contentPadding: const EdgeInsets.symmetric(\n          vertical: 12,\n          horizontal: 16,\n        ),\n        title: Row(\n          crossAxisAlignment: file.fileType == MediaFileTypePB.Image\n              ? CrossAxisAlignment.start\n              : CrossAxisAlignment.center,\n          children: [\n            if (file.fileType != MediaFileTypePB.Image) ...[\n              Flexible(\n                child: GestureDetector(\n                  onTap: () => afLaunchUrlString(file.url),\n                  child: Row(\n                    children: [\n                      FlowySvg(file.fileType.icon, size: const Size.square(24)),\n                      const HSpace(12),\n                      Expanded(\n                        child: FlowyText(\n                          file.name,\n                          overflow: TextOverflow.ellipsis,\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n              ),\n            ] else ...[\n              Expanded(\n                child: Container(\n                  alignment: Alignment.centerLeft,\n                  constraints: const BoxConstraints(maxHeight: 96),\n                  child: GestureDetector(\n                    onTap: () => openInteractiveViewer(context),\n                    child: ImageRender(\n                      userProfile:\n                          context.read<MediaCellBloc>().state.userProfile,\n                      fit: BoxFit.fitHeight,\n                      borderRadius: BorderRadius.zero,\n                      image: ImageBlockData(\n                        url: file.url,\n                        type: file.uploadType.toCustomImageType(),\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n            ],\n            FlowyIconButton(\n              width: 20,\n              icon: const FlowySvg(\n                FlowySvgs.three_dots_s,\n                size: Size.square(20),\n              ),\n              onPressed: () => showMobileBottomSheet(\n                context,\n                backgroundColor: Theme.of(context).colorScheme.surface,\n                showDragHandle: true,\n                builder: (_) => BlocProvider.value(\n                  value: context.read<MediaCellBloc>(),\n                  child: _EditFileSheet(file: file),\n                ),\n              ),\n            ),\n            const HSpace(6),\n          ],\n        ),\n      ),\n    );\n  }\n\n  void openInteractiveViewer(BuildContext context) =>\n      openInteractiveViewerFromFile(\n        context,\n        file,\n        onDeleteImage: (_) => context.read<MediaCellBloc>().deleteFile(file.id),\n        userProfile: context.read<MediaCellBloc>().state.userProfile,\n      );\n}\n\nclass _EditFileSheet extends StatefulWidget {\n  const _EditFileSheet({required this.file});\n\n  final MediaFilePB file;\n\n  @override\n  State<_EditFileSheet> createState() => _EditFileSheetState();\n}\n\nclass _EditFileSheetState extends State<_EditFileSheet> {\n  late final controller = TextEditingController(text: widget.file.name);\n  Loading? loader;\n\n  MediaFilePB get file => widget.file;\n\n  @override\n  void dispose() {\n    controller.dispose();\n    loader?.stop();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleChildScrollView(\n      child: Column(\n        children: [\n          const VSpace(16),\n          if (file.fileType == MediaFileTypePB.Image) ...[\n            FlowyOptionTile.text(\n              showTopBorder: false,\n              text: LocaleKeys.grid_media_expand.tr(),\n              leftIcon: const FlowySvg(\n                FlowySvgs.full_view_s,\n                size: Size.square(20),\n              ),\n              onTap: () => openInteractiveViewer(context),\n            ),\n            FlowyOptionTile.text(\n              showTopBorder: false,\n              text: LocaleKeys.grid_media_setAsCover.tr(),\n              leftIcon: const FlowySvg(\n                FlowySvgs.cover_s,\n                size: Size.square(20),\n              ),\n              onTap: () => context.read<MediaCellBloc>().add(\n                    MediaCellEvent.setCover(\n                      RowCoverPB(\n                        data: file.url,\n                        uploadType: file.uploadType,\n                        coverType: CoverTypePB.FileCover,\n                      ),\n                    ),\n                  ),\n            ),\n          ],\n          FlowyOptionTile.text(\n            showTopBorder: file.fileType == MediaFileTypePB.Image,\n            text: LocaleKeys.grid_media_openInBrowser.tr(),\n            leftIcon: const FlowySvg(\n              FlowySvgs.open_in_browser_s,\n              size: Size.square(20),\n            ),\n            onTap: () => afLaunchUrlString(file.url),\n          ),\n          // TODO(Mathias): Rename interaction need design\n          // FlowyOptionTile.text(\n          //   text: LocaleKeys.grid_media_rename.tr(),\n          //   leftIcon: const FlowySvg(\n          //     FlowySvgs.rename_s,\n          //     size: Size.square(20),\n          //   ),\n          //   onTap: () {},\n          // ),\n          if (widget.file.uploadType == FileUploadTypePB.CloudFile) ...[\n            FlowyOptionTile.text(\n              onTap: () async => downloadMediaFile(\n                context,\n                file,\n                userProfile: context.read<MediaCellBloc>().state.userProfile,\n                onDownloadBegin: () {\n                  loader?.stop();\n                  loader = Loading(context);\n                  loader?.start();\n                },\n                onDownloadEnd: () => loader?.stop(),\n              ),\n              text: LocaleKeys.button_download.tr(),\n              leftIcon: const FlowySvg(\n                FlowySvgs.save_as_s,\n                size: Size.square(20),\n              ),\n            ),\n          ],\n          FlowyOptionTile.text(\n            text: LocaleKeys.grid_media_delete.tr(),\n            textColor: Theme.of(context).colorScheme.error,\n            leftIcon: FlowySvg(\n              FlowySvgs.trash_s,\n              size: const Size.square(20),\n              color: Theme.of(context).colorScheme.error,\n            ),\n            onTap: () {\n              context.pop();\n              context.read<MediaCellBloc>().deleteFile(file.id);\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  void openInteractiveViewer(BuildContext context) =>\n      openInteractiveViewerFromFile(\n        context,\n        file,\n        onDeleteImage: (_) => context.read<MediaCellBloc>().deleteFile(file.id),\n        userProfile: context.read<MediaCellBloc>().state.userProfile,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';\nimport 'package:appflowy/mobile/presentation/base/option_color_list.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/plugins/base/drag_handler.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:protobuf/protobuf.dart';\n\n// include single select and multiple select\nclass MobileSelectOptionEditor extends StatefulWidget {\n  const MobileSelectOptionEditor({\n    super.key,\n    required this.cellController,\n  });\n\n  final SelectOptionCellController cellController;\n\n  @override\n  State<MobileSelectOptionEditor> createState() =>\n      _MobileSelectOptionEditorState();\n}\n\nclass _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {\n  final searchController = TextEditingController();\n  final renameController = TextEditingController();\n\n  String typingOption = '';\n  FieldType get fieldType => widget.cellController.fieldType;\n\n  bool showMoreOptions = false;\n  SelectOptionPB? option;\n\n  @override\n  void dispose() {\n    searchController.dispose();\n    renameController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ConstrainedBox(\n      constraints: const BoxConstraints.tightFor(height: 420),\n      child: BlocProvider(\n        create: (context) => SelectOptionCellEditorBloc(\n          cellController: widget.cellController,\n        ),\n        child: BlocBuilder<SelectOptionCellEditorBloc,\n            SelectOptionCellEditorState>(\n          builder: (context, state) {\n            return Column(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                const DragHandle(),\n                _buildHeader(context),\n                const Divider(height: 0.5),\n                Expanded(\n                  child: Padding(\n                    padding: EdgeInsets.symmetric(\n                      horizontal: showMoreOptions ? 0.0 : 16.0,\n                    ),\n                    child: _buildBody(context),\n                  ),\n                ),\n              ],\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  Widget _buildHeader(BuildContext context) {\n    const height = 44.0;\n    return Stack(\n      children: [\n        if (showMoreOptions)\n          Align(\n            alignment: Alignment.centerLeft,\n            child: AppBarBackButton(onTap: _popOrBack),\n          ),\n        SizedBox(\n          height: 44.0,\n          child: Align(\n            child: FlowyText.medium(\n              _headerTitle(),\n              fontSize: 18,\n            ),\n          ),\n        ),\n      ].map((e) => SizedBox(height: height, child: e)).toList(),\n    );\n  }\n\n  Widget _buildBody(BuildContext context) {\n    if (showMoreOptions && option != null) {\n      return _MoreOptions(\n        initialOption: option!,\n        controller: renameController,\n        onDelete: () {\n          context\n              .read<SelectOptionCellEditorBloc>()\n              .add(SelectOptionCellEditorEvent.deleteOption(option!));\n          _popOrBack();\n        },\n        onUpdate: (name, color) {\n          final option = this.option;\n          if (option == null) {\n            return;\n          }\n          option.freeze();\n          context.read<SelectOptionCellEditorBloc>().add(\n            SelectOptionCellEditorEvent.updateOption(\n              option.rebuild((p0) {\n                if (name != null) {\n                  p0.name = name;\n                }\n                if (color != null) {\n                  p0.color = color;\n                }\n              }),\n            ),\n          );\n        },\n      );\n    }\n\n    return SingleChildScrollView(\n      child: Column(\n        children: [\n          const VSpace(16),\n          _SearchField(\n            controller: searchController,\n            hintText: LocaleKeys.grid_selectOption_searchOrCreateOption.tr(),\n            onSubmitted: (_) {\n              context\n                  .read<SelectOptionCellEditorBloc>()\n                  .add(const SelectOptionCellEditorEvent.submitTextField());\n              searchController.clear();\n            },\n            onChanged: (value) {\n              typingOption = value;\n              context.read<SelectOptionCellEditorBloc>().add(\n                    SelectOptionCellEditorEvent.selectMultipleOptions(\n                      [],\n                      value,\n                    ),\n                  );\n            },\n          ),\n          const VSpace(22),\n          _OptionList(\n            fieldType: widget.cellController.fieldType,\n            onCreateOption: (optionName) {\n              context\n                  .read<SelectOptionCellEditorBloc>()\n                  .add(const SelectOptionCellEditorEvent.createOption());\n              searchController.clear();\n            },\n            onCheck: (option, isSelected) {\n              if (isSelected) {\n                context\n                    .read<SelectOptionCellEditorBloc>()\n                    .add(SelectOptionCellEditorEvent.unselectOption(option.id));\n              } else {\n                context\n                    .read<SelectOptionCellEditorBloc>()\n                    .add(SelectOptionCellEditorEvent.selectOption(option.id));\n              }\n            },\n            onMoreOptions: (option) {\n              setState(() {\n                this.option = option;\n                renameController.text = option.name;\n                showMoreOptions = true;\n              });\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  String _headerTitle() {\n    switch (fieldType) {\n      case FieldType.SingleSelect:\n        return LocaleKeys.grid_field_singleSelectFieldName.tr();\n      case FieldType.MultiSelect:\n        return LocaleKeys.grid_field_multiSelectFieldName.tr();\n      default:\n        throw UnimplementedError();\n    }\n  }\n\n  void _popOrBack() {\n    if (showMoreOptions) {\n      setState(() {\n        showMoreOptions = false;\n        option = null;\n      });\n    } else {\n      context.pop();\n    }\n  }\n}\n\nclass _SearchField extends StatelessWidget {\n  const _SearchField({\n    this.hintText,\n    required this.onChanged,\n    required this.onSubmitted,\n    required this.controller,\n  });\n\n  final String? hintText;\n  final void Function(String value) onChanged;\n  final void Function(String value) onSubmitted;\n  final TextEditingController controller;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyMobileSearchTextField(\n      controller: controller,\n      onChanged: onChanged,\n      onSubmitted: onSubmitted,\n      hintText: hintText,\n    );\n  }\n}\n\nclass _OptionList extends StatelessWidget {\n  const _OptionList({\n    required this.fieldType,\n    required this.onCreateOption,\n    required this.onCheck,\n    required this.onMoreOptions,\n  });\n\n  final FieldType fieldType;\n  final void Function(String optionName) onCreateOption;\n  final void Function(SelectOptionPB option, bool value) onCheck;\n  final void Function(SelectOptionPB option) onMoreOptions;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionCellEditorState>(\n      builder: (context, state) {\n        // existing options\n        final List<Widget> cells = [];\n\n        // create an option cell\n        if (state.createSelectOptionSuggestion != null) {\n          cells.add(\n            _CreateOptionCell(\n              name: state.createSelectOptionSuggestion!.name,\n              color: state.createSelectOptionSuggestion!.color,\n              onTap: () => onCreateOption(\n                state.createSelectOptionSuggestion!.name,\n              ),\n            ),\n          );\n        }\n\n        cells.addAll(\n          state.options.map(\n            (option) => MobileSelectOption(\n              indicator: fieldType == FieldType.MultiSelect\n                  ? MobileSelectedOptionIndicator.multi\n                  : MobileSelectedOptionIndicator.single,\n              option: option,\n              isSelected: state.selectedOptions.contains(option),\n              onTap: (value) => onCheck(option, value),\n              onMoreOptions: () => onMoreOptions(option),\n            ),\n          ),\n        );\n\n        return ListView.separated(\n          shrinkWrap: true,\n          itemCount: cells.length,\n          separatorBuilder: (_, __) => const VSpace(20),\n          physics: const NeverScrollableScrollPhysics(),\n          itemBuilder: (_, int index) => cells[index],\n          padding: const EdgeInsets.only(bottom: 12.0),\n        );\n      },\n    );\n  }\n}\n\nclass MobileSelectOption extends StatelessWidget {\n  const MobileSelectOption({\n    super.key,\n    required this.indicator,\n    required this.option,\n    required this.isSelected,\n    required this.onTap,\n    this.showMoreOptionsButton = true,\n    this.onMoreOptions,\n  });\n\n  final MobileSelectedOptionIndicator indicator;\n  final SelectOptionPB option;\n  final bool isSelected;\n  final void Function(bool value) onTap;\n  final bool showMoreOptionsButton;\n  final VoidCallback? onMoreOptions;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 40,\n      child: GestureDetector(\n        // no need to add click effect, so using gesture detector\n        behavior: HitTestBehavior.translucent,\n        onTap: () => onTap(isSelected),\n        child: Row(\n          children: [\n            // checked or selected icon\n            SizedBox(\n              height: 20,\n              width: 20,\n              child: _IsSelectedIndicator(\n                indicator: indicator,\n                isSelected: isSelected,\n              ),\n            ),\n            // padding\n            const HSpace(12),\n            // option tag\n            Expanded(\n              child: Align(\n                alignment: AlignmentDirectional.centerStart,\n                child: SelectOptionTag(\n                  option: option,\n                  padding: const EdgeInsets.symmetric(\n                    vertical: 10,\n                    horizontal: 14,\n                  ),\n                  textAlign: TextAlign.center,\n                  fontSize: 15.0,\n                ),\n              ),\n            ),\n            if (showMoreOptionsButton) ...[\n              const HSpace(24),\n              // more options\n              FlowyIconButton(\n                icon: const FlowySvg(\n                  FlowySvgs.m_field_more_s,\n                ),\n                onPressed: onMoreOptions,\n              ),\n            ],\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _CreateOptionCell extends StatelessWidget {\n  const _CreateOptionCell({\n    required this.name,\n    required this.color,\n    required this.onTap,\n  });\n\n  final String name;\n  final SelectOptionColorPB color;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 44,\n      child: GestureDetector(\n        behavior: HitTestBehavior.translucent,\n        onTap: onTap,\n        child: Row(\n          children: [\n            FlowyText(\n              LocaleKeys.grid_selectOption_create.tr(),\n              color: Theme.of(context).hintColor,\n            ),\n            const HSpace(8),\n            Expanded(\n              child: Align(\n                alignment: AlignmentDirectional.centerStart,\n                child: SelectOptionTag(\n                  name: name,\n                  color: color.toColor(context),\n                  textAlign: TextAlign.center,\n                  padding: const EdgeInsets.symmetric(\n                    vertical: 10,\n                    horizontal: 14,\n                  ),\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _MoreOptions extends StatefulWidget {\n  const _MoreOptions({\n    required this.initialOption,\n    required this.onDelete,\n    required this.onUpdate,\n    required this.controller,\n  });\n\n  final SelectOptionPB initialOption;\n  final VoidCallback onDelete;\n  final void Function(String? name, SelectOptionColorPB? color) onUpdate;\n  final TextEditingController controller;\n\n  @override\n  State<_MoreOptions> createState() => _MoreOptionsState();\n}\n\nclass _MoreOptionsState extends State<_MoreOptions> {\n  late SelectOptionPB option = widget.initialOption;\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleChildScrollView(\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          _buildRenameTextField(context),\n          const VSpace(16.0),\n          _buildDeleteButton(context),\n          const VSpace(16.0),\n          Padding(\n            padding: const EdgeInsets.only(left: 12.0),\n            child: FlowyText(\n              LocaleKeys.grid_selectOption_colorPanelTitle.tr().toUpperCase(),\n              color: Theme.of(context).hintColor,\n              fontSize: 13,\n            ),\n          ),\n          const VSpace(4.0),\n          FlowyOptionDecorateBox(\n            child: Padding(\n              padding: const EdgeInsets.symmetric(\n                vertical: 12.0,\n                horizontal: 6.0,\n              ),\n              child: OptionColorList(\n                selectedColor: option.color,\n                onSelectedColor: (color) {\n                  widget.onUpdate(null, color);\n                  setState(() {\n                    option.freeze();\n                    option = option.rebuild((option) => option.color = color);\n                  });\n                },\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildRenameTextField(BuildContext context) {\n    return ConstrainedBox(\n      constraints: const BoxConstraints.tightFor(height: 52.0),\n      child: FlowyOptionTile.textField(\n        showTopBorder: false,\n        onTextChanged: (name) => widget.onUpdate(name, null),\n        controller: widget.controller,\n      ),\n    );\n  }\n\n  Widget _buildDeleteButton(BuildContext context) {\n    return FlowyOptionTile.text(\n      text: LocaleKeys.button_delete.tr(),\n      textColor: Theme.of(context).colorScheme.error,\n      leftIcon: FlowySvg(\n        FlowySvgs.m_delete_s,\n        color: Theme.of(context).colorScheme.error,\n      ),\n      onTap: widget.onDelete,\n    );\n  }\n}\n\nenum MobileSelectedOptionIndicator { single, multi }\n\nclass _IsSelectedIndicator extends StatelessWidget {\n  const _IsSelectedIndicator({\n    required this.indicator,\n    required this.isSelected,\n  });\n\n  final MobileSelectedOptionIndicator indicator;\n  final bool isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return isSelected\n        ? DecoratedBox(\n            decoration: BoxDecoration(\n              shape: BoxShape.circle,\n              color: Theme.of(context).colorScheme.primary,\n            ),\n            child: Center(\n              child: indicator == MobileSelectedOptionIndicator.multi\n                  ? FlowySvg(\n                      FlowySvgs.checkmark_tiny_s,\n                      color: Theme.of(context).colorScheme.onPrimary,\n                    )\n                  : Container(\n                      width: 7.5,\n                      height: 7.5,\n                      decoration: BoxDecoration(\n                        shape: BoxShape.circle,\n                        color: Theme.of(context).colorScheme.onPrimary,\n                      ),\n                    ),\n            ),\n          )\n        : DecoratedBox(\n            decoration: BoxDecoration(\n              shape: BoxShape.circle,\n              border: Border.fromBorderSide(\n                BorderSide(\n                  color: Theme.of(context).dividerColor,\n                ),\n              ),\n            ),\n          );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/relation_type_option_cubit.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';\nimport 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:appflowy/plugins/database/widgets/row/relation_row_detail.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../application/cell/bloc/relation_cell_bloc.dart';\nimport '../../application/cell/bloc/relation_row_search_bloc.dart';\n\nclass RelationCellEditor extends StatelessWidget {\n  const RelationCellEditor({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<RelationCellBloc, RelationCellState>(\n      builder: (context, cellState) {\n        return cellState.relatedDatabaseMeta == null\n            ? const _RelationCellEditorDatabasePicker()\n            : _RelationCellEditorContent(\n                relatedDatabaseMeta: cellState.relatedDatabaseMeta!,\n                selectedRowIds: cellState.rows.map((e) => e.rowId).toList(),\n              );\n      },\n    );\n  }\n}\n\nclass _RelationCellEditorContent extends StatefulWidget {\n  const _RelationCellEditorContent({\n    required this.relatedDatabaseMeta,\n    required this.selectedRowIds,\n  });\n\n  final DatabaseMeta relatedDatabaseMeta;\n  final List<String> selectedRowIds;\n\n  @override\n  State<_RelationCellEditorContent> createState() =>\n      _RelationCellEditorContentState();\n}\n\nclass _RelationCellEditorContentState\n    extends State<_RelationCellEditorContent> {\n  final textEditingController = TextEditingController();\n  late final FocusNode focusNode;\n  late final bloc = RelationRowSearchBloc(\n    databaseId: widget.relatedDatabaseMeta.databaseId,\n  );\n\n  @override\n  void initState() {\n    super.initState();\n    focusNode = FocusNode(\n      onKeyEvent: (node, event) {\n        switch (event.logicalKey) {\n          case LogicalKeyboardKey.arrowUp when event is! KeyUpEvent:\n            if (textEditingController.value.composing.isCollapsed) {\n              bloc.add(const RelationRowSearchEvent.focusPreviousOption());\n              return KeyEventResult.handled;\n            }\n            break;\n          case LogicalKeyboardKey.arrowDown when event is! KeyUpEvent:\n            if (textEditingController.value.composing.isCollapsed) {\n              bloc.add(const RelationRowSearchEvent.focusNextOption());\n              return KeyEventResult.handled;\n            }\n            break;\n          case LogicalKeyboardKey.escape when event is! KeyUpEvent:\n            if (!textEditingController.value.composing.isCollapsed) {\n              final end = textEditingController.value.composing.end;\n              final text = textEditingController.text;\n\n              textEditingController.value = TextEditingValue(\n                text: text,\n                selection: TextSelection.collapsed(offset: end),\n              );\n              return KeyEventResult.handled;\n            }\n            break;\n        }\n        return KeyEventResult.ignored;\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    textEditingController.dispose();\n    focusNode.dispose();\n    bloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider.value(value: bloc),\n        BlocProvider.value(value: context.read<UserWorkspaceBloc>()),\n      ],\n      child: BlocBuilder<RelationRowSearchBloc, RelationRowSearchState>(\n        buildWhen: (previous, current) =>\n            !listEquals(previous.filteredRows, current.filteredRows),\n        builder: (context, state) {\n          final selected = <RelatedRowDataPB>[];\n          final unselected = <RelatedRowDataPB>[];\n          for (final row in state.filteredRows) {\n            if (widget.selectedRowIds.contains(row.rowId)) {\n              selected.add(row);\n            } else {\n              unselected.add(row);\n            }\n          }\n          return TextFieldTapRegion(\n            child: CustomScrollView(\n              shrinkWrap: true,\n              slivers: [\n                _CellEditorTitle(\n                  databaseMeta: widget.relatedDatabaseMeta,\n                ),\n                _SearchField(\n                  focusNode: focusNode,\n                  textEditingController: textEditingController,\n                ),\n                const SliverToBoxAdapter(\n                  child: TypeOptionSeparator(spacing: 0.0),\n                ),\n                if (state.filteredRows.isEmpty)\n                  SliverToBoxAdapter(\n                    child: Padding(\n                      padding: const EdgeInsets.all(6.0) +\n                          GridSize.typeOptionContentInsets,\n                      child: FlowyText.regular(\n                        LocaleKeys.grid_relation_emptySearchResult.tr(),\n                        color: Theme.of(context).hintColor,\n                      ),\n                    ),\n                  ),\n                if (selected.isNotEmpty) ...[\n                  SliverToBoxAdapter(\n                    child: Padding(\n                      padding: const EdgeInsets.symmetric(horizontal: 6.0) +\n                          GridSize.typeOptionContentInsets,\n                      child: FlowyText.regular(\n                        LocaleKeys.grid_relation_linkedRowListLabel.plural(\n                          selected.length,\n                          namedArgs: {'count': '${selected.length}'},\n                        ),\n                        fontSize: 11,\n                        overflow: TextOverflow.ellipsis,\n                        color: Theme.of(context).hintColor,\n                      ),\n                    ),\n                  ),\n                  _RowList(\n                    databaseId: widget.relatedDatabaseMeta.databaseId,\n                    rows: selected,\n                    isSelected: true,\n                  ),\n                  const SliverToBoxAdapter(\n                    child: VSpace(4.0),\n                  ),\n                ],\n                if (unselected.isNotEmpty) ...[\n                  SliverToBoxAdapter(\n                    child: Padding(\n                      padding: const EdgeInsets.symmetric(horizontal: 6.0) +\n                          GridSize.typeOptionContentInsets,\n                      child: FlowyText.regular(\n                        LocaleKeys.grid_relation_unlinkedRowListLabel.tr(),\n                        fontSize: 11,\n                        overflow: TextOverflow.ellipsis,\n                        color: Theme.of(context).hintColor,\n                      ),\n                    ),\n                  ),\n                  _RowList(\n                    databaseId: widget.relatedDatabaseMeta.databaseId,\n                    rows: unselected,\n                    isSelected: false,\n                  ),\n                  const SliverToBoxAdapter(\n                    child: VSpace(4.0),\n                  ),\n                ],\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _CellEditorTitle extends StatelessWidget {\n  const _CellEditorTitle({\n    required this.databaseMeta,\n  });\n\n  final DatabaseMeta databaseMeta;\n\n  @override\n  Widget build(BuildContext context) {\n    return SliverToBoxAdapter(\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 6.0) +\n            GridSize.typeOptionContentInsets,\n        child: Row(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            FlowyText.regular(\n              LocaleKeys.grid_relation_inRelatedDatabase.tr(),\n              fontSize: 11,\n              color: Theme.of(context).hintColor,\n            ),\n            MouseRegion(\n              cursor: SystemMouseCursors.click,\n              child: GestureDetector(\n                onTap: () => _openRelatedDatbase(context),\n                child: Padding(\n                  padding:\n                      const EdgeInsets.symmetric(horizontal: 4, vertical: 2),\n                  child: FlowyText.regular(\n                    databaseMeta.databaseName,\n                    fontSize: 11,\n                    overflow: TextOverflow.ellipsis,\n                    decoration: TextDecoration.underline,\n                  ),\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  void _openRelatedDatbase(BuildContext context) {\n    FolderEventGetView(ViewIdPB(value: databaseMeta.viewId))\n        .send()\n        .then((result) {\n      result.fold(\n        (view) {\n          PopoverContainer.of(context).closeAll();\n          Navigator.of(context).maybePop();\n          getIt<TabsBloc>().add(\n            TabsEvent.openPlugin(\n              plugin: DatabaseTabBarViewPlugin(\n                view: view,\n                pluginType: view.pluginType,\n              ),\n            ),\n          );\n        },\n        (err) => Log.error(err),\n      );\n    });\n  }\n}\n\nclass _SearchField extends StatelessWidget {\n  const _SearchField({\n    required this.focusNode,\n    required this.textEditingController,\n  });\n\n  final FocusNode focusNode;\n  final TextEditingController textEditingController;\n\n  @override\n  Widget build(BuildContext context) {\n    return SliverToBoxAdapter(\n      child: Padding(\n        padding: const EdgeInsets.only(left: 6.0, bottom: 6.0, right: 6.0),\n        child: FlowyTextField(\n          focusNode: focusNode,\n          controller: textEditingController,\n          hintText: LocaleKeys.grid_relation_rowSearchTextFieldPlaceholder.tr(),\n          hintStyle: Theme.of(context)\n              .textTheme\n              .bodySmall\n              ?.copyWith(color: Theme.of(context).hintColor),\n          onChanged: (text) {\n            if (textEditingController.value.composing.isCollapsed) {\n              context\n                  .read<RelationRowSearchBloc>()\n                  .add(RelationRowSearchEvent.updateFilter(text));\n            }\n          },\n          onSubmitted: (_) {\n            final focusedRowId =\n                context.read<RelationRowSearchBloc>().state.focusedRowId;\n            if (focusedRowId != null) {\n              final row = context\n                  .read<RelationCellBloc>()\n                  .state\n                  .rows\n                  .firstWhereOrNull((e) => e.rowId == focusedRowId);\n              if (row != null) {\n                FlowyOverlay.show(\n                  context: context,\n                  builder: (BuildContext overlayContext) {\n                    return BlocProvider.value(\n                      value: context.read<UserWorkspaceBloc>(),\n                      child: RelatedRowDetailPage(\n                        databaseId: context\n                            .read<RelationCellBloc>()\n                            .state\n                            .relatedDatabaseMeta!\n                            .databaseId,\n                        rowId: row.rowId,\n                      ),\n                    );\n                  },\n                );\n                PopoverContainer.of(context).close();\n              } else {\n                context\n                    .read<RelationCellBloc>()\n                    .add(RelationCellEvent.selectRow(focusedRowId));\n              }\n            }\n            focusNode.requestFocus();\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass _RowList extends StatelessWidget {\n  const _RowList({\n    required this.databaseId,\n    required this.rows,\n    required this.isSelected,\n  });\n\n  final String databaseId;\n  final List<RelatedRowDataPB> rows;\n  final bool isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return SliverList(\n      delegate: SliverChildBuilderDelegate(\n        (context, index) => _RowListItem(\n          row: rows[index],\n          databaseId: databaseId,\n          isSelected: isSelected,\n        ),\n        childCount: rows.length,\n      ),\n    );\n  }\n}\n\nclass _RowListItem extends StatelessWidget {\n  const _RowListItem({\n    required this.row,\n    required this.isSelected,\n    required this.databaseId,\n  });\n\n  final RelatedRowDataPB row;\n  final String databaseId;\n  final bool isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    final isHovered =\n        context.watch<RelationRowSearchBloc>().state.focusedRowId == row.rowId;\n    return Container(\n      height: 28,\n      margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 2.0),\n      decoration: BoxDecoration(\n        color: isHovered ? AFThemeExtension.of(context).lightGreyHover : null,\n        borderRadius: const BorderRadius.all(Radius.circular(6)),\n      ),\n      child: GestureDetector(\n        onTap: () {\n          final userWorkspaceBloc = context.read<UserWorkspaceBloc>();\n          if (isSelected) {\n            FlowyOverlay.show(\n              context: context,\n              builder: (BuildContext overlayContext) {\n                return BlocProvider.value(\n                  value: userWorkspaceBloc,\n                  child: RelatedRowDetailPage(\n                    databaseId: databaseId,\n                    rowId: row.rowId,\n                  ),\n                );\n              },\n            );\n            PopoverContainer.of(context).close();\n          } else {\n            context\n                .read<RelationCellBloc>()\n                .add(RelationCellEvent.selectRow(row.rowId));\n          }\n        },\n        child: MouseRegion(\n          cursor: SystemMouseCursors.click,\n          onHover: (_) => context\n              .read<RelationRowSearchBloc>()\n              .add(RelationRowSearchEvent.updateFocusedOption(row.rowId)),\n          child: Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 4.0),\n            child: Row(\n              children: [\n                Expanded(\n                  child: FlowyText(\n                    row.name.trim().isEmpty\n                        ? LocaleKeys.grid_title_placeholder.tr()\n                        : row.name,\n                    color: row.name.trim().isEmpty\n                        ? Theme.of(context).hintColor\n                        : null,\n                    overflow: TextOverflow.ellipsis,\n                  ),\n                ),\n                if (isSelected && isHovered)\n                  _UnselectRowButton(\n                    onPressed: () => context\n                        .read<RelationCellBloc>()\n                        .add(RelationCellEvent.selectRow(row.rowId)),\n                  ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _UnselectRowButton extends StatefulWidget {\n  const _UnselectRowButton({\n    required this.onPressed,\n  });\n\n  final VoidCallback onPressed;\n\n  @override\n  State<_UnselectRowButton> createState() => _UnselectRowButtonState();\n}\n\nclass _UnselectRowButtonState extends State<_UnselectRowButton> {\n  final _materialStatesController = WidgetStatesController();\n\n  @override\n  void dispose() {\n    _materialStatesController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return TextButton(\n      onPressed: widget.onPressed,\n      onHover: (_) => setState(() {}),\n      onFocusChange: (_) => setState(() {}),\n      style: ButtonStyle(\n        fixedSize: const WidgetStatePropertyAll(Size.square(32)),\n        minimumSize: const WidgetStatePropertyAll(Size.square(32)),\n        maximumSize: const WidgetStatePropertyAll(Size.square(32)),\n        overlayColor: WidgetStateProperty.resolveWith((state) {\n          if (state.contains(WidgetState.focused)) {\n            return AFThemeExtension.of(context).greyHover;\n          }\n          return Colors.transparent;\n        }),\n        shape: const WidgetStatePropertyAll(\n          RoundedRectangleBorder(borderRadius: Corners.s6Border),\n        ),\n      ),\n      statesController: _materialStatesController,\n      child: Container(\n        color: _materialStatesController.value.contains(WidgetState.hovered) ||\n                _materialStatesController.value.contains(WidgetState.focused)\n            ? Theme.of(context).colorScheme.primary\n            : AFThemeExtension.of(context).onBackground,\n        width: 12,\n        height: 1,\n      ),\n    );\n  }\n}\n\nclass _RelationCellEditorDatabasePicker extends StatelessWidget {\n  const _RelationCellEditorDatabasePicker();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) => RelationDatabaseListCubit(),\n      child: BlocBuilder<RelationDatabaseListCubit, RelationDatabaseListState>(\n        builder: (context, state) {\n          return Column(\n            mainAxisSize: MainAxisSize.min,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Padding(\n                padding: const EdgeInsets.fromLTRB(6, 6, 6, 0),\n                child: FlowyText(\n                  LocaleKeys.grid_relation_noDatabaseSelected.tr(),\n                  maxLines: null,\n                  fontSize: 10,\n                  color: Theme.of(context).hintColor,\n                ),\n              ),\n              Flexible(\n                child: ListView.separated(\n                  padding: const EdgeInsets.all(6),\n                  separatorBuilder: (context, index) =>\n                      VSpace(GridSize.typeOptionSeparatorHeight),\n                  itemCount: state.databaseMetas.length,\n                  shrinkWrap: true,\n                  itemBuilder: (context, index) {\n                    final databaseMeta = state.databaseMetas[index];\n                    return SizedBox(\n                      height: GridSize.popoverItemHeight,\n                      child: FlowyButton(\n                        onTap: () => context.read<RelationCellBloc>().add(\n                              RelationCellEvent.selectDatabaseId(\n                                databaseMeta.databaseId,\n                              ),\n                            ),\n                        text: FlowyText(\n                          databaseMeta.databaseName,\n                          overflow: TextOverflow.ellipsis,\n                        ),\n                      ),\n                    );\n                  },\n                ),\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart",
    "content": "import 'dart:collection';\nimport 'dart:io';\n\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../grid/presentation/widgets/common/type_option_separator.dart';\nimport '../field/type_option_editor/select/select_option_editor.dart';\n\nimport 'extension.dart';\nimport 'select_option_text_field.dart';\n\nconst double _editorPanelWidth = 300;\n\nclass SelectOptionCellEditor extends StatefulWidget {\n  const SelectOptionCellEditor({super.key, required this.cellController});\n\n  final SelectOptionCellController cellController;\n\n  @override\n  State<SelectOptionCellEditor> createState() => _SelectOptionCellEditorState();\n}\n\nclass _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {\n  final textEditingController = TextEditingController();\n  final scrollController = ScrollController();\n  final popoverMutex = PopoverMutex();\n  late final bloc = SelectOptionCellEditorBloc(\n    cellController: widget.cellController,\n  );\n  late final FocusNode focusNode;\n\n  @override\n  void initState() {\n    super.initState();\n    focusNode = FocusNode(\n      onKeyEvent: (node, event) {\n        switch (event.logicalKey) {\n          case LogicalKeyboardKey.arrowUp when event is! KeyUpEvent:\n            if (textEditingController.value.composing.isCollapsed) {\n              bloc.add(const SelectOptionCellEditorEvent.focusPreviousOption());\n              return KeyEventResult.handled;\n            }\n            break;\n          case LogicalKeyboardKey.arrowDown when event is! KeyUpEvent:\n            if (textEditingController.value.composing.isCollapsed) {\n              bloc.add(const SelectOptionCellEditorEvent.focusNextOption());\n              return KeyEventResult.handled;\n            }\n            break;\n          case LogicalKeyboardKey.escape when event is! KeyUpEvent:\n            if (!textEditingController.value.composing.isCollapsed) {\n              final end = textEditingController.value.composing.end;\n              final text = textEditingController.text;\n\n              textEditingController.value = TextEditingValue(\n                text: text,\n                selection: TextSelection.collapsed(offset: end),\n              );\n              return KeyEventResult.handled;\n            }\n            break;\n          case LogicalKeyboardKey.backspace when event is KeyUpEvent:\n            if (!textEditingController.text.isNotEmpty) {\n              bloc.add(const SelectOptionCellEditorEvent.unselectLastOption());\n              return KeyEventResult.handled;\n            }\n            break;\n        }\n        return KeyEventResult.ignored;\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    popoverMutex.dispose();\n    textEditingController.dispose();\n    scrollController.dispose();\n    bloc.close();\n    focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: bloc,\n      child: TextFieldTapRegion(\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            _TextField(\n              textEditingController: textEditingController,\n              scrollController: scrollController,\n              focusNode: focusNode,\n              popoverMutex: popoverMutex,\n            ),\n            const TypeOptionSeparator(spacing: 0.0),\n            Flexible(\n              child: Focus(\n                descendantsAreFocusable: false,\n                child: _OptionList(\n                  textEditingController: textEditingController,\n                  popoverMutex: popoverMutex,\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _OptionList extends StatelessWidget {\n  const _OptionList({\n    required this.textEditingController,\n    required this.popoverMutex,\n  });\n\n  final TextEditingController textEditingController;\n  final PopoverMutex popoverMutex;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocConsumer<SelectOptionCellEditorBloc,\n        SelectOptionCellEditorState>(\n      listenWhen: (prev, curr) => prev.clearFilter != curr.clearFilter,\n      listener: (context, state) {\n        if (state.clearFilter) {\n          textEditingController.clear();\n          context\n              .read<SelectOptionCellEditorBloc>()\n              .add(const SelectOptionCellEditorEvent.resetClearFilterFlag());\n        }\n      },\n      buildWhen: (previous, current) =>\n          !listEquals(previous.options, current.options) ||\n          previous.createSelectOptionSuggestion !=\n              current.createSelectOptionSuggestion,\n      builder: (context, state) => ReorderableListView.builder(\n        shrinkWrap: true,\n        proxyDecorator: (child, index, _) => Material(\n          color: Colors.transparent,\n          child: Stack(\n            children: [\n              BlocProvider.value(\n                value: context.read<SelectOptionCellEditorBloc>(),\n                child: child,\n              ),\n              MouseRegion(\n                cursor: Platform.isWindows\n                    ? SystemMouseCursors.click\n                    : SystemMouseCursors.grabbing,\n                child: const SizedBox.expand(),\n              ),\n            ],\n          ),\n        ),\n        buildDefaultDragHandles: false,\n        itemCount: state.options.length,\n        onReorderStart: (_) => popoverMutex.close(),\n        itemBuilder: (_, int index) {\n          final option = state.options[index];\n          return _SelectOptionCell(\n            key: ValueKey(\"select_cell_option_list_${option.id}\"),\n            index: index,\n            option: option,\n            popoverMutex: popoverMutex,\n          );\n        },\n        onReorder: (oldIndex, newIndex) {\n          if (oldIndex < newIndex) {\n            newIndex--;\n          }\n          final fromOptionId = state.options[oldIndex].id;\n          final toOptionId = state.options[newIndex].id;\n          context.read<SelectOptionCellEditorBloc>().add(\n                SelectOptionCellEditorEvent.reorderOption(\n                  fromOptionId,\n                  toOptionId,\n                ),\n              );\n        },\n        header: Padding(\n          padding: EdgeInsets.only(\n            bottom: state.createSelectOptionSuggestion != null ||\n                    state.options.isNotEmpty\n                ? 12\n                : 0,\n          ),\n          child: const _Title(),\n        ),\n        footer: state.createSelectOptionSuggestion != null\n            ? _CreateOptionCell(\n                suggestion: state.createSelectOptionSuggestion!,\n              )\n            : null,\n        padding: const EdgeInsets.symmetric(vertical: 8),\n      ),\n    );\n  }\n}\n\nclass _TextField extends StatelessWidget {\n  const _TextField({\n    required this.textEditingController,\n    required this.scrollController,\n    required this.focusNode,\n    required this.popoverMutex,\n  });\n\n  final TextEditingController textEditingController;\n  final ScrollController scrollController;\n  final FocusNode focusNode;\n  final PopoverMutex popoverMutex;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionCellEditorState>(\n      builder: (context, state) {\n        final optionMap = LinkedHashMap<String, SelectOptionPB>.fromIterable(\n          state.selectedOptions,\n          key: (option) => option.name,\n          value: (option) => option,\n        );\n\n        return Material(\n          color: Colors.transparent,\n          child: Padding(\n            padding: const EdgeInsets.all(12.0),\n            child: SelectOptionTextField(\n              options: state.options,\n              focusNode: focusNode,\n              selectedOptionMap: optionMap,\n              distanceToText: _editorPanelWidth * 0.7,\n              textController: textEditingController,\n              scrollController: scrollController,\n              textSeparators: const [','],\n              onClick: () => popoverMutex.close(),\n              newText: (text) => context\n                  .read<SelectOptionCellEditorBloc>()\n                  .add(SelectOptionCellEditorEvent.filterOption(text)),\n              onSubmitted: () {\n                context\n                    .read<SelectOptionCellEditorBloc>()\n                    .add(const SelectOptionCellEditorEvent.submitTextField());\n                focusNode.requestFocus();\n              },\n              onPaste: (tagNames, remainder) {\n                context.read<SelectOptionCellEditorBloc>().add(\n                      SelectOptionCellEditorEvent.selectMultipleOptions(\n                        tagNames,\n                        remainder,\n                      ),\n                    );\n              },\n              onRemove: (name) =>\n                  context.read<SelectOptionCellEditorBloc>().add(\n                        SelectOptionCellEditorEvent.unselectOption(\n                          optionMap[name]!.id,\n                        ),\n                      ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass _Title extends StatelessWidget {\n  const _Title();\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 16.0),\n      child: FlowyText.regular(\n        LocaleKeys.grid_selectOption_panelTitle.tr(),\n        color: Theme.of(context).hintColor,\n      ),\n    );\n  }\n}\n\nclass _SelectOptionCell extends StatefulWidget {\n  const _SelectOptionCell({\n    super.key,\n    required this.option,\n    required this.index,\n    required this.popoverMutex,\n  });\n\n  final SelectOptionPB option;\n  final int index;\n  final PopoverMutex popoverMutex;\n\n  @override\n  State<_SelectOptionCell> createState() => _SelectOptionCellState();\n}\n\nclass _SelectOptionCellState extends State<_SelectOptionCell> {\n  final _popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      controller: _popoverController,\n      offset: const Offset(8, 0),\n      margin: EdgeInsets.zero,\n      asBarrier: true,\n      constraints: BoxConstraints.loose(const Size(200, 470)),\n      mutex: widget.popoverMutex,\n      clickHandler: PopoverClickHandler.gestureDetector,\n      popupBuilder: (popoverContext) => SelectOptionEditor(\n        key: ValueKey(widget.option.id),\n        option: widget.option,\n        onDeleted: () {\n          context\n              .read<SelectOptionCellEditorBloc>()\n              .add(SelectOptionCellEditorEvent.deleteOption(widget.option));\n          PopoverContainer.of(popoverContext).close();\n        },\n        onUpdated: (updatedOption) => context\n            .read<SelectOptionCellEditorBloc>()\n            .add(SelectOptionCellEditorEvent.updateOption(updatedOption)),\n      ),\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0),\n        child: MouseRegion(\n          onEnter: (_) => context.read<SelectOptionCellEditorBloc>().add(\n                SelectOptionCellEditorEvent.updateFocusedOption(\n                  widget.option.id,\n                ),\n              ),\n          child: Container(\n            height: 28,\n            decoration: BoxDecoration(\n              color: context\n                          .watch<SelectOptionCellEditorBloc>()\n                          .state\n                          .focusedOptionId ==\n                      widget.option.id\n                  ? AFThemeExtension.of(context).lightGreyHover\n                  : null,\n              borderRadius: const BorderRadius.all(Radius.circular(6)),\n            ),\n            child: SelectOptionTagCell(\n              option: widget.option,\n              index: widget.index,\n              onSelected: _onTap,\n              children: [\n                if (context\n                    .watch<SelectOptionCellEditorBloc>()\n                    .state\n                    .selectedOptions\n                    .contains(widget.option))\n                  FlowyIconButton(\n                    width: 20,\n                    hoverColor: Colors.transparent,\n                    onPressed: _onTap,\n                    icon: FlowySvg(\n                      FlowySvgs.check_s,\n                      color: Theme.of(context).iconTheme.color,\n                    ),\n                  ),\n                FlowyIconButton(\n                  onPressed: () => _popoverController.show(),\n                  iconPadding: const EdgeInsets.symmetric(horizontal: 6.0),\n                  hoverColor: Colors.transparent,\n                  icon: FlowySvg(\n                    FlowySvgs.three_dots_s,\n                    size: const Size.square(16),\n                    color: AFThemeExtension.of(context).onBackground,\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  void _onTap() {\n    widget.popoverMutex.close();\n    final bloc = context.read<SelectOptionCellEditorBloc>();\n    if (bloc.state.selectedOptions.contains(widget.option)) {\n      bloc.add(SelectOptionCellEditorEvent.unselectOption(widget.option.id));\n    } else {\n      bloc.add(SelectOptionCellEditorEvent.selectOption(widget.option.id));\n    }\n  }\n}\n\nclass SelectOptionTagCell extends StatelessWidget {\n  const SelectOptionTagCell({\n    super.key,\n    required this.option,\n    required this.onSelected,\n    this.children = const [],\n    this.index,\n  });\n\n  final SelectOptionPB option;\n  final VoidCallback onSelected;\n  final List<Widget> children;\n  final int? index;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      crossAxisAlignment: CrossAxisAlignment.stretch,\n      children: [\n        if (index != null)\n          ReorderableDragStartListener(\n            index: index!,\n            child: MouseRegion(\n              cursor: Platform.isWindows\n                  ? SystemMouseCursors.click\n                  : SystemMouseCursors.grab,\n              child: GestureDetector(\n                onTap: onSelected,\n                child: SizedBox(\n                  width: 26,\n                  child: Center(\n                    child: FlowySvg(\n                      FlowySvgs.drag_element_s,\n                      size: const Size.square(14),\n                      color: AFThemeExtension.of(context).onBackground,\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ),\n        Expanded(\n          child: GestureDetector(\n            behavior: HitTestBehavior.opaque,\n            onTap: onSelected,\n            child: MouseRegion(\n              cursor: SystemMouseCursors.click,\n              child: Container(\n                alignment: AlignmentDirectional.centerStart,\n                padding: const EdgeInsets.symmetric(horizontal: 6.0),\n                child: SelectOptionTag(\n                  fontSize: 14,\n                  option: option,\n                  padding: const EdgeInsets.symmetric(\n                    horizontal: 8,\n                    vertical: 2,\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ),\n        ...children,\n      ],\n    );\n  }\n}\n\nclass _CreateOptionCell extends StatelessWidget {\n  const _CreateOptionCell({required this.suggestion});\n\n  final CreateSelectOptionSuggestion suggestion;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: 32,\n      margin: const EdgeInsets.symmetric(horizontal: 8.0),\n      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),\n      decoration: BoxDecoration(\n        color:\n            context.watch<SelectOptionCellEditorBloc>().state.focusedOptionId ==\n                    createSelectOptionSuggestionId\n                ? AFThemeExtension.of(context).lightGreyHover\n                : null,\n        borderRadius: const BorderRadius.all(Radius.circular(6)),\n      ),\n      child: GestureDetector(\n        onTap: () => context\n            .read<SelectOptionCellEditorBloc>()\n            .add(const SelectOptionCellEditorEvent.createOption()),\n        child: MouseRegion(\n          onEnter: (_) {\n            context.read<SelectOptionCellEditorBloc>().add(\n                  const SelectOptionCellEditorEvent.updateFocusedOption(\n                    createSelectOptionSuggestionId,\n                  ),\n                );\n          },\n          child: Row(\n            children: [\n              FlowyText(\n                LocaleKeys.grid_selectOption_create.tr(),\n                color: Theme.of(context).hintColor,\n              ),\n              const HSpace(10),\n              Expanded(\n                child: Align(\n                  alignment: Alignment.centerLeft,\n                  child: SelectOptionTag(\n                    name: suggestion.name,\n                    color: suggestion.color.toColor(context),\n                    fontSize: 14,\n                    padding: const EdgeInsets.symmetric(\n                      horizontal: 8,\n                      vertical: 2,\n                    ),\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_text_field.dart",
    "content": "import 'dart:collection';\n\nimport 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\n\nimport 'extension.dart';\n\nclass SelectOptionTextField extends StatefulWidget {\n  const SelectOptionTextField({\n    super.key,\n    required this.options,\n    required this.selectedOptionMap,\n    required this.distanceToText,\n    required this.textSeparators,\n    required this.textController,\n    required this.focusNode,\n    required this.onSubmitted,\n    required this.newText,\n    required this.onPaste,\n    required this.onRemove,\n    this.scrollController,\n    this.onClick,\n  });\n\n  final List<SelectOptionPB> options;\n  final LinkedHashMap<String, SelectOptionPB> selectedOptionMap;\n  final double distanceToText;\n  final List<String> textSeparators;\n  final TextEditingController textController;\n  final ScrollController? scrollController;\n  final FocusNode focusNode;\n\n  final Function() onSubmitted;\n  final Function(String) newText;\n  final Function(List<String>, String) onPaste;\n  final Function(String) onRemove;\n  final VoidCallback? onClick;\n\n  @override\n  State<SelectOptionTextField> createState() => _SelectOptionTextFieldState();\n}\n\nclass _SelectOptionTextFieldState extends State<SelectOptionTextField> {\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      widget.focusNode.requestFocus();\n      _scrollToEnd();\n    });\n    widget.textController.addListener(_onChanged);\n  }\n\n  @override\n  void didUpdateWidget(covariant oldWidget) {\n    if (oldWidget.selectedOptionMap.length < widget.selectedOptionMap.length) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        _scrollToEnd();\n      });\n    }\n\n    if (oldWidget.textController != widget.textController) {\n      oldWidget.textController.removeListener(_onChanged);\n      widget.textController.addListener(_onChanged);\n    }\n\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  void dispose() {\n    widget.textController.removeListener(_onChanged);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return TextField(\n      controller: widget.textController,\n      focusNode: widget.focusNode,\n      onTap: widget.onClick,\n      onSubmitted: (_) => widget.onSubmitted(),\n      style: Theme.of(context).textTheme.bodyMedium,\n      decoration: InputDecoration(\n        enabledBorder: OutlineInputBorder(\n          borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),\n          borderRadius: Corners.s10Border,\n        ),\n        isDense: true,\n        prefixIcon: _renderTags(context),\n        prefixIconConstraints: BoxConstraints(maxWidth: widget.distanceToText),\n        focusedBorder: OutlineInputBorder(\n          borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),\n          borderRadius: Corners.s10Border,\n        ),\n      ),\n    );\n  }\n\n  void _scrollToEnd() {\n    if (widget.scrollController?.hasClients ?? false) {\n      widget.scrollController?.animateTo(\n        widget.scrollController!.position.maxScrollExtent,\n        duration: const Duration(milliseconds: 150),\n        curve: Curves.easeOut,\n      );\n    }\n  }\n\n  void _onChanged() {\n    if (!widget.textController.value.composing.isCollapsed) {\n      return;\n    }\n\n    // split input\n    final (submitted, remainder) = splitInput(\n      widget.textController.text.trimLeft(),\n      widget.textSeparators,\n    );\n\n    if (submitted.isNotEmpty) {\n      widget.textController.text = remainder;\n      widget.textController.selection =\n          TextSelection.collapsed(offset: widget.textController.text.length);\n    }\n    widget.onPaste(submitted, remainder);\n  }\n\n  Widget? _renderTags(BuildContext context) {\n    if (widget.selectedOptionMap.isEmpty) {\n      return null;\n    }\n\n    final children = widget.selectedOptionMap.values\n        .map(\n          (option) => SelectOptionTag(\n            option: option,\n            onRemove: (option) => widget.onRemove(option),\n            padding: const EdgeInsets.symmetric(\n              horizontal: 8,\n              vertical: 4,\n            ),\n          ),\n        )\n        .toList();\n\n    return Focus(\n      descendantsAreFocusable: false,\n      child: MouseRegion(\n        cursor: SystemMouseCursors.basic,\n        child: Padding(\n          padding: const EdgeInsets.all(8.0),\n          child: ScrollConfiguration(\n            behavior: ScrollConfiguration.of(context).copyWith(\n              dragDevices: {\n                PointerDeviceKind.mouse,\n                PointerDeviceKind.touch,\n                PointerDeviceKind.trackpad,\n                PointerDeviceKind.stylus,\n                PointerDeviceKind.invertedStylus,\n              },\n            ),\n            child: SingleChildScrollView(\n              scrollDirection: Axis.horizontal,\n              controller: widget.scrollController,\n              child: Wrap(spacing: 4, children: children),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\n@visibleForTesting\n(List<String>, String) splitInput(String input, List<String> textSeparators) {\n  final List<String> splits = [];\n  String currentString = '';\n\n  // split the string into tokens\n  for (final char in input.split('')) {\n    if (textSeparators.contains(char)) {\n      if (currentString.trim().isNotEmpty) {\n        splits.add(currentString.trim());\n      }\n      currentString = '';\n      continue;\n    }\n    currentString += char;\n  }\n  // add the remainder (might be '')\n  splits.add(currentString);\n\n  final submittedOptions = splits.sublist(0, splits.length - 1).toList();\n  final remainder = splits.elementAt(splits.length - 1).trimLeft();\n\n  return (submittedOptions, remainder);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/database_layout_ext.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nextension DatabaseLayoutExtension on DatabaseLayoutPB {\n  String get layoutName {\n    return switch (this) {\n      DatabaseLayoutPB.Board => LocaleKeys.board_menuName.tr(),\n      DatabaseLayoutPB.Calendar => LocaleKeys.calendar_menuName.tr(),\n      DatabaseLayoutPB.Grid => LocaleKeys.grid_menuName.tr(),\n      _ => \"\",\n    };\n  }\n\n  ViewLayoutPB get layoutType {\n    return switch (this) {\n      DatabaseLayoutPB.Board => ViewLayoutPB.Board,\n      DatabaseLayoutPB.Calendar => ViewLayoutPB.Calendar,\n      DatabaseLayoutPB.Grid => ViewLayoutPB.Grid,\n      _ => throw UnimplementedError(),\n    };\n  }\n\n  FlowySvgData get icon {\n    return switch (this) {\n      DatabaseLayoutPB.Board => FlowySvgs.board_s,\n      DatabaseLayoutPB.Calendar => FlowySvgs.calendar_s,\n      DatabaseLayoutPB.Grid => FlowySvgs.grid_s,\n      _ => throw UnimplementedError(),\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/database_view_widget.dart",
    "content": "import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_listener.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass DatabaseViewWidget extends StatefulWidget {\n  const DatabaseViewWidget({\n    super.key,\n    required this.view,\n    this.shrinkWrap = true,\n    required this.showActions,\n    required this.node,\n    this.actionBuilder,\n  });\n\n  final ViewPB view;\n  final bool shrinkWrap;\n  final BlockComponentActionBuilder? actionBuilder;\n  final bool showActions;\n  final Node node;\n\n  @override\n  State<DatabaseViewWidget> createState() => _DatabaseViewWidgetState();\n}\n\nclass _DatabaseViewWidgetState extends State<DatabaseViewWidget> {\n  /// Listens to the view updates.\n  late final ViewListener _listener;\n\n  /// Notifies the view layout type changes. When the layout type changes,\n  /// the widget of the view will be updated.\n  late final ValueNotifier<ViewLayoutPB> _layoutTypeChangeNotifier;\n\n  /// The view will be updated by the [ViewListener].\n  late ViewPB view;\n\n  late Plugin viewPlugin;\n\n  @override\n  void initState() {\n    super.initState();\n    view = widget.view;\n    viewPlugin = view.plugin()..init();\n    _listenOnViewUpdated();\n  }\n\n  @override\n  void dispose() {\n    _layoutTypeChangeNotifier.dispose();\n    _listener.stop();\n    viewPlugin.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    double? horizontalPadding = 0.0;\n    final databasePluginWidgetBuilderSize =\n        Provider.of<DatabasePluginWidgetBuilderSize?>(context);\n    if (view.layout == ViewLayoutPB.Grid || view.layout == ViewLayoutPB.Board) {\n      horizontalPadding = 40.0;\n    }\n    if (databasePluginWidgetBuilderSize != null) {\n      horizontalPadding = databasePluginWidgetBuilderSize.horizontalPadding;\n    }\n\n    return ValueListenableBuilder<ViewLayoutPB>(\n      valueListenable: _layoutTypeChangeNotifier,\n      builder: (_, __, ___) => viewPlugin.widgetBuilder.buildWidget(\n        shrinkWrap: widget.shrinkWrap,\n        context: PluginContext(),\n        data: {\n          kDatabasePluginWidgetBuilderHorizontalPadding: horizontalPadding,\n          kDatabasePluginWidgetBuilderActionBuilder: widget.actionBuilder,\n          kDatabasePluginWidgetBuilderShowActions: widget.showActions,\n          kDatabasePluginWidgetBuilderNode: widget.node,\n        },\n      ),\n    );\n  }\n\n  void _listenOnViewUpdated() {\n    _listener = ViewListener(viewId: widget.view.id)\n      ..start(\n        onViewUpdated: (updatedView) {\n          if (mounted) {\n            view = updatedView;\n            _layoutTypeChangeNotifier.value = view.layout;\n          }\n        },\n      );\n\n    _layoutTypeChangeNotifier = ValueNotifier(widget.view.layout);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart",
    "content": "import 'dart:convert';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../../../shared/icon_emoji_picker/icon_picker.dart';\nimport 'field_type_list.dart';\nimport 'type_option_editor/builder.dart';\n\nenum FieldEditorPage {\n  general,\n  details,\n}\n\nclass FieldEditor extends StatefulWidget {\n  const FieldEditor({\n    super.key,\n    required this.viewId,\n    required this.fieldInfo,\n    required this.fieldController,\n    required this.isNewField,\n    this.initialPage = FieldEditorPage.details,\n    this.onFieldInserted,\n  });\n\n  final String viewId;\n  final FieldInfo fieldInfo;\n  final FieldController fieldController;\n  final FieldEditorPage initialPage;\n  final void Function(String fieldId)? onFieldInserted;\n  final bool isNewField;\n\n  @override\n  State<StatefulWidget> createState() => _FieldEditorState();\n}\n\nclass _FieldEditorState extends State<FieldEditor> {\n  final PopoverMutex popoverMutex = PopoverMutex();\n  late FieldEditorPage _currentPage;\n  late final TextEditingController textController =\n      TextEditingController(text: widget.fieldInfo.name);\n\n  @override\n  void initState() {\n    super.initState();\n    _currentPage = widget.initialPage;\n  }\n\n  @override\n  void dispose() {\n    popoverMutex.dispose();\n    textController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) => FieldEditorBloc(\n        viewId: widget.viewId,\n        fieldInfo: widget.fieldInfo,\n        fieldController: widget.fieldController,\n        onFieldInserted: widget.onFieldInserted,\n        isNew: widget.isNewField,\n      ),\n      child: _currentPage == FieldEditorPage.general\n          ? _fieldGeneral()\n          : _fieldDetails(),\n    );\n  }\n\n  Widget _fieldGeneral() {\n    return SizedBox(\n      width: 240,\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          _NameAndIcon(\n            popoverMutex: popoverMutex,\n            textController: textController,\n            padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),\n          ),\n          VSpace(GridSize.typeOptionSeparatorHeight),\n          _EditFieldButton(\n            padding: const EdgeInsets.symmetric(horizontal: 8.0),\n            onTap: () {\n              setState(() => _currentPage = FieldEditorPage.details);\n            },\n          ),\n          VSpace(GridSize.typeOptionSeparatorHeight),\n          _actionCell(FieldAction.insertLeft),\n          VSpace(GridSize.typeOptionSeparatorHeight),\n          _actionCell(FieldAction.insertRight),\n          VSpace(GridSize.typeOptionSeparatorHeight),\n          _actionCell(FieldAction.toggleVisibility),\n          VSpace(GridSize.typeOptionSeparatorHeight),\n          _actionCell(FieldAction.duplicate),\n          VSpace(GridSize.typeOptionSeparatorHeight),\n          _actionCell(FieldAction.clearData),\n          VSpace(GridSize.typeOptionSeparatorHeight),\n          _actionCell(FieldAction.delete),\n          const TypeOptionSeparator(spacing: 8.0),\n          _actionCell(FieldAction.wrap),\n          const VSpace(8.0),\n        ],\n      ),\n    );\n  }\n\n  Widget _actionCell(FieldAction action) {\n    return BlocBuilder<FieldEditorBloc, FieldEditorState>(\n      builder: (context, state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: FieldActionCell(\n            viewId: widget.viewId,\n            fieldInfo: state.field,\n            action: action,\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _fieldDetails() {\n    return SizedBox(\n      width: 260,\n      child: FieldDetailsEditor(\n        viewId: widget.viewId,\n        textEditingController: textController,\n      ),\n    );\n  }\n}\n\nclass _EditFieldButton extends StatelessWidget {\n  const _EditFieldButton({\n    required this.padding,\n    this.onTap,\n  });\n\n  final EdgeInsetsGeometry padding;\n  final void Function()? onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: GridSize.popoverItemHeight,\n      padding: padding,\n      child: FlowyButton(\n        leftIcon: const FlowySvg(FlowySvgs.edit_s),\n        text: FlowyText(\n          lineHeight: 1.0,\n          LocaleKeys.grid_field_editProperty.tr(),\n        ),\n        onTap: onTap,\n      ),\n    );\n  }\n}\n\nclass FieldActionCell extends StatelessWidget {\n  const FieldActionCell({\n    super.key,\n    required this.viewId,\n    required this.fieldInfo,\n    required this.action,\n    this.popoverMutex,\n  });\n\n  final String viewId;\n  final FieldInfo fieldInfo;\n  final FieldAction action;\n  final PopoverMutex? popoverMutex;\n\n  @override\n  Widget build(BuildContext context) {\n    bool enable = true;\n    // If the field is primary, delete and duplicate are disabled.\n    if (fieldInfo.isPrimary &&\n        (action == FieldAction.duplicate || action == FieldAction.delete)) {\n      enable = false;\n    }\n    return FlowyIconTextButton(\n      resetHoverOnRebuild: false,\n      disable: !enable,\n      onHover: (_) => popoverMutex?.close(),\n      onTap: () => action.run(context, viewId, fieldInfo),\n      // show the error color when delete is hovered\n      textBuilder: (onHover) => FlowyText(\n        action.title(fieldInfo),\n        lineHeight: 1.0,\n        color: enable\n            ? action == FieldAction.delete && onHover\n                ? Theme.of(context).colorScheme.error\n                : null\n            : Theme.of(context).disabledColor,\n      ),\n      leftIconBuilder: (onHover) => action.leading(\n        fieldInfo,\n        enable\n            ? action == FieldAction.delete && onHover\n                ? Theme.of(context).colorScheme.error\n                : null\n            : Theme.of(context).disabledColor,\n      ),\n      rightIconBuilder: (_) => action.trailing(context, fieldInfo),\n    );\n  }\n}\n\nenum FieldAction {\n  insertLeft,\n  insertRight,\n  toggleVisibility,\n  duplicate,\n  clearData,\n  delete,\n  wrap;\n\n  Widget? leading(FieldInfo fieldInfo, Color? color) {\n    FlowySvgData? svgData;\n    switch (this) {\n      case FieldAction.insertLeft:\n        svgData = FlowySvgs.arrow_s;\n      case FieldAction.insertRight:\n        svgData = FlowySvgs.arrow_s;\n      case FieldAction.toggleVisibility:\n        if (fieldInfo.visibility != null &&\n            fieldInfo.visibility == FieldVisibility.AlwaysHidden) {\n          svgData = FlowySvgs.show_m;\n        } else {\n          svgData = FlowySvgs.hide_s;\n        }\n      case FieldAction.duplicate:\n        svgData = FlowySvgs.copy_s;\n      case FieldAction.clearData:\n        svgData = FlowySvgs.reload_s;\n      case FieldAction.delete:\n        svgData = FlowySvgs.delete_s;\n      default:\n    }\n\n    if (svgData == null) {\n      return null;\n    }\n    final icon = FlowySvg(\n      svgData,\n      size: const Size.square(16),\n      color: color,\n    );\n    return this == FieldAction.insertRight\n        ? Transform.flip(flipX: true, child: icon)\n        : icon;\n  }\n\n  Widget? trailing(BuildContext context, FieldInfo fieldInfo) {\n    if (this == FieldAction.wrap) {\n      return Toggle(\n        value: fieldInfo.wrapCellContent ?? false,\n        onChanged: (_) => context\n            .read<FieldEditorBloc>()\n            .add(const FieldEditorEvent.toggleWrapCellContent()),\n        padding: EdgeInsets.zero,\n      );\n    }\n\n    return null;\n  }\n\n  String title(FieldInfo fieldInfo) {\n    switch (this) {\n      case FieldAction.insertLeft:\n        return LocaleKeys.grid_field_insertLeft.tr();\n      case FieldAction.insertRight:\n        return LocaleKeys.grid_field_insertRight.tr();\n      case FieldAction.toggleVisibility:\n        if (fieldInfo.visibility != null &&\n            fieldInfo.visibility == FieldVisibility.AlwaysHidden) {\n          return LocaleKeys.grid_field_show.tr();\n        } else {\n          return LocaleKeys.grid_field_hide.tr();\n        }\n      case FieldAction.duplicate:\n        return LocaleKeys.grid_field_duplicate.tr();\n      case FieldAction.clearData:\n        return LocaleKeys.grid_field_clear.tr();\n      case FieldAction.delete:\n        return LocaleKeys.grid_field_delete.tr();\n      case FieldAction.wrap:\n        return LocaleKeys.grid_field_wrapCellContent.tr();\n    }\n  }\n\n  void run(BuildContext context, String viewId, FieldInfo fieldInfo) {\n    switch (this) {\n      case FieldAction.insertLeft:\n        PopoverContainer.of(context).close();\n        context\n            .read<FieldEditorBloc>()\n            .add(const FieldEditorEvent.insertLeft());\n        break;\n      case FieldAction.insertRight:\n        PopoverContainer.of(context).close();\n        context\n            .read<FieldEditorBloc>()\n            .add(const FieldEditorEvent.insertRight());\n        break;\n      case FieldAction.toggleVisibility:\n        PopoverContainer.of(context).close();\n        context\n            .read<FieldEditorBloc>()\n            .add(const FieldEditorEvent.toggleFieldVisibility());\n        break;\n      case FieldAction.duplicate:\n        PopoverContainer.of(context).close();\n        FieldBackendService.duplicateField(\n          viewId: viewId,\n          fieldId: fieldInfo.id,\n        );\n        break;\n      case FieldAction.clearData:\n        PopoverContainer.of(context).closeAll();\n        showCancelAndConfirmDialog(\n          context: context,\n          title: LocaleKeys.grid_field_label.tr(),\n          description: LocaleKeys.grid_field_clearFieldPromptMessage.tr(),\n          confirmLabel: LocaleKeys.button_confirm.tr(),\n          onConfirm: (_) {\n            FieldBackendService.clearField(\n              viewId: viewId,\n              fieldId: fieldInfo.id,\n            );\n          },\n        );\n        break;\n      case FieldAction.delete:\n        PopoverContainer.of(context).closeAll();\n        showConfirmDeletionDialog(\n          context: context,\n          name: LocaleKeys.grid_field_label.tr(),\n          description: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),\n          onConfirm: () {\n            FieldBackendService.deleteField(\n              viewId: viewId,\n              fieldId: fieldInfo.id,\n            );\n          },\n        );\n        break;\n      case FieldAction.wrap:\n        context\n            .read<FieldEditorBloc>()\n            .add(const FieldEditorEvent.toggleWrapCellContent());\n        break;\n    }\n  }\n}\n\nclass FieldDetailsEditor extends StatefulWidget {\n  const FieldDetailsEditor({\n    super.key,\n    required this.viewId,\n    required this.textEditingController,\n    this.onAction,\n  });\n\n  final String viewId;\n  final TextEditingController textEditingController;\n  final Function()? onAction;\n\n  @override\n  State<StatefulWidget> createState() => _FieldDetailsEditorState();\n}\n\nclass _FieldDetailsEditorState extends State<FieldDetailsEditor> {\n  final PopoverMutex popoverMutex = PopoverMutex();\n\n  @override\n  void dispose() {\n    popoverMutex.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final List<Widget> children = [\n      _NameAndIcon(\n        popoverMutex: popoverMutex,\n        textController: widget.textEditingController,\n        padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),\n      ),\n      const VSpace(8.0),\n      SwitchFieldButton(popoverMutex: popoverMutex),\n      const TypeOptionSeparator(spacing: 8.0),\n      Flexible(\n        child: FieldTypeOptionEditor(\n          viewId: widget.viewId,\n          popoverMutex: popoverMutex,\n        ),\n      ),\n      _addFieldVisibilityToggleButton(),\n      _addDuplicateFieldButton(),\n      _addDeleteFieldButton(),\n    ];\n\n    return Padding(\n      padding: const EdgeInsets.symmetric(vertical: 8.0),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: children,\n      ),\n    );\n  }\n\n  Widget _addFieldVisibilityToggleButton() {\n    return BlocBuilder<FieldEditorBloc, FieldEditorState>(\n      builder: (context, state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          child: FieldActionCell(\n            viewId: widget.viewId,\n            fieldInfo: state.field,\n            action: FieldAction.toggleVisibility,\n            popoverMutex: popoverMutex,\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _addDeleteFieldButton() {\n    return BlocBuilder<FieldEditorBloc, FieldEditorState>(\n      builder: (context, state) {\n        return Padding(\n          padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0),\n          child: FieldActionCell(\n            viewId: widget.viewId,\n            fieldInfo: state.field,\n            action: FieldAction.delete,\n            popoverMutex: popoverMutex,\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _addDuplicateFieldButton() {\n    return BlocBuilder<FieldEditorBloc, FieldEditorState>(\n      builder: (context, state) {\n        return Padding(\n          padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0),\n          child: FieldActionCell(\n            viewId: widget.viewId,\n            fieldInfo: state.field,\n            action: FieldAction.duplicate,\n            popoverMutex: popoverMutex,\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass FieldTypeOptionEditor extends StatelessWidget {\n  const FieldTypeOptionEditor({\n    super.key,\n    required this.viewId,\n    required this.popoverMutex,\n  });\n\n  final String viewId;\n  final PopoverMutex popoverMutex;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FieldEditorBloc, FieldEditorState>(\n      builder: (context, state) {\n        if (state.field.isPrimary) {\n          return const SizedBox.shrink();\n        }\n        final typeOptionEditor = makeTypeOptionEditor(\n          context: context,\n          viewId: viewId,\n          field: state.field.field,\n          popoverMutex: popoverMutex,\n          onTypeOptionUpdated: (Uint8List typeOptionData) {\n            context\n                .read<FieldEditorBloc>()\n                .add(FieldEditorEvent.updateTypeOption(typeOptionData));\n          },\n        );\n\n        if (typeOptionEditor == null) {\n          return const SizedBox.shrink();\n        }\n\n        return Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            Flexible(child: typeOptionEditor),\n            const TypeOptionSeparator(spacing: 8.0),\n          ],\n        );\n      },\n    );\n  }\n}\n\nclass _NameAndIcon extends StatelessWidget {\n  const _NameAndIcon({\n    required this.textController,\n    this.padding = EdgeInsets.zero,\n    this.popoverMutex,\n  });\n\n  final TextEditingController textController;\n  final PopoverMutex? popoverMutex;\n  final EdgeInsetsGeometry padding;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: padding,\n      child: Row(\n        children: [\n          FieldEditIconButton(\n            popoverMutex: popoverMutex,\n          ),\n          const HSpace(6),\n          Expanded(\n            child: FieldNameTextField(\n              textController: textController,\n              popoverMutex: popoverMutex,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass FieldEditIconButton extends StatefulWidget {\n  const FieldEditIconButton({\n    super.key,\n    this.popoverMutex,\n  });\n\n  final PopoverMutex? popoverMutex;\n\n  @override\n  State<FieldEditIconButton> createState() => _FieldEditIconButtonState();\n}\n\nclass _FieldEditIconButtonState extends State<FieldEditIconButton> {\n  final PopoverController popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      offset: const Offset(0, 4),\n      constraints: BoxConstraints.loose(const Size(360, 432)),\n      margin: EdgeInsets.zero,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      controller: popoverController,\n      mutex: widget.popoverMutex,\n      child: FlowyIconButton(\n        decoration: BoxDecoration(\n          border: Border.fromBorderSide(\n            BorderSide(\n              color: Theme.of(context).colorScheme.outline,\n            ),\n          ),\n          borderRadius: Corners.s8Border,\n        ),\n        icon: BlocBuilder<FieldEditorBloc, FieldEditorState>(\n          builder: (context, state) {\n            return FieldIcon(fieldInfo: state.field);\n          },\n        ),\n        width: 32,\n        onPressed: () => popoverController.show(),\n      ),\n      popupBuilder: (popoverContext) {\n        return FlowyIconEmojiPicker(\n          enableBackgroundColorSelection: false,\n          tabs: const [PickerTabType.icon],\n          onSelectedEmoji: (r) {\n            if (r.type == FlowyIconType.icon) {\n              try {\n                final iconsData = IconsData.fromJson(jsonDecode(r.emoji));\n                context.read<FieldEditorBloc>().add(\n                      FieldEditorEvent.updateIcon(\n                        '${iconsData.groupName}/${iconsData.iconName}',\n                      ),\n                    );\n              } on FormatException catch (e) {\n                Log.warn('FieldEditIconButton onSelectedEmoji error:$e');\n                context\n                    .read<FieldEditorBloc>()\n                    .add(const FieldEditorEvent.updateIcon(''));\n              }\n            }\n            PopoverContainer.of(popoverContext).close();\n          },\n        );\n      },\n    );\n  }\n}\n\nclass FieldNameTextField extends StatefulWidget {\n  const FieldNameTextField({\n    super.key,\n    required this.textController,\n    this.popoverMutex,\n  });\n\n  final TextEditingController textController;\n  final PopoverMutex? popoverMutex;\n\n  @override\n  State<FieldNameTextField> createState() => _FieldNameTextFieldState();\n}\n\nclass _FieldNameTextFieldState extends State<FieldNameTextField> {\n  final focusNode = FocusNode();\n\n  @override\n  void initState() {\n    super.initState();\n\n    focusNode.addListener(_onFocusChanged);\n    widget.popoverMutex?.addPopoverListener(_onPopoverChanged);\n  }\n\n  @override\n  void dispose() {\n    widget.popoverMutex?.removePopoverListener(_onPopoverChanged);\n    focusNode.removeListener(_onFocusChanged);\n    focusNode.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTextField(\n      focusNode: focusNode,\n      controller: widget.textController,\n      onSubmitted: (_) => PopoverContainer.of(context).close(),\n      onChanged: (newName) {\n        context\n            .read<FieldEditorBloc>()\n            .add(FieldEditorEvent.renameField(newName));\n      },\n    );\n  }\n\n  void _onFocusChanged() {\n    if (focusNode.hasFocus) {\n      widget.popoverMutex?.close();\n    }\n  }\n\n  void _onPopoverChanged() {\n    if (focusNode.hasFocus) {\n      focusNode.unfocus();\n    }\n  }\n}\n\nclass SwitchFieldButton extends StatefulWidget {\n  const SwitchFieldButton({\n    super.key,\n    required this.popoverMutex,\n  });\n\n  final PopoverMutex popoverMutex;\n\n  @override\n  State<SwitchFieldButton> createState() => _SwitchFieldButtonState();\n}\n\nclass _SwitchFieldButtonState extends State<SwitchFieldButton> {\n  final PopoverController _popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FieldEditorBloc, FieldEditorState>(\n      builder: (context, state) {\n        if (state.field.isPrimary) {\n          return SizedBox(\n            height: GridSize.popoverItemHeight,\n            child: Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 8.0),\n              child: FlowyTooltip(\n                message: LocaleKeys.grid_field_switchPrimaryFieldTooltip.tr(),\n                child: FlowyButton(\n                  text: FlowyText(\n                    state.field.fieldType.i18n,\n                    lineHeight: 1.0,\n                    color: Theme.of(context).disabledColor,\n                  ),\n                  leftIcon: FlowySvg(\n                    state.field.fieldType.svgData,\n                    color: Theme.of(context).disabledColor,\n                  ),\n                  rightIcon: FlowySvg(\n                    FlowySvgs.more_s,\n                    color: Theme.of(context).disabledColor,\n                  ),\n                ),\n              ),\n            ),\n          );\n        }\n        return SizedBox(\n          height: GridSize.popoverItemHeight,\n          child: AppFlowyPopover(\n            constraints: BoxConstraints.loose(const Size(460, 540)),\n            triggerActions: PopoverTriggerFlags.hover,\n            mutex: widget.popoverMutex,\n            controller: _popoverController,\n            offset: const Offset(8, 0),\n            margin: const EdgeInsets.all(8),\n            popupBuilder: (BuildContext popoverContext) {\n              return FieldTypeList(\n                onSelectField: (newFieldType) {\n                  context\n                      .read<FieldEditorBloc>()\n                      .add(FieldEditorEvent.switchFieldType(newFieldType));\n                },\n              );\n            },\n            child: Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 8.0),\n              child: FlowyButton(\n                onTap: () => _popoverController.show(),\n                text: FlowyText(\n                  state.field.fieldType.i18n,\n                  lineHeight: 1.0,\n                ),\n                leftIcon: FlowySvg(\n                  state.field.fieldType.svgData,\n                ),\n                rightIcon: const FlowySvg(\n                  FlowySvgs.more_s,\n                ),\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\n\ntypedef SelectFieldCallback = void Function(FieldType);\n\nconst List<FieldType> _supportedFieldTypes = [\n  FieldType.RichText,\n  FieldType.Number,\n  FieldType.SingleSelect,\n  FieldType.MultiSelect,\n  FieldType.DateTime,\n  FieldType.Media,\n  FieldType.URL,\n  FieldType.Checkbox,\n  FieldType.Checklist,\n  FieldType.LastEditedTime,\n  FieldType.CreatedTime,\n  FieldType.Relation,\n  FieldType.Summary,\n  FieldType.Translate,\n  // FieldType.Time,\n];\n\nclass FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {\n  const FieldTypeList({required this.onSelectField, super.key});\n\n  final SelectFieldCallback onSelectField;\n\n  @override\n  Widget build(BuildContext context) {\n    final cells = _supportedFieldTypes.map((fieldType) {\n      return FieldTypeCell(\n        fieldType: fieldType,\n        onSelectField: (fieldType) {\n          onSelectField(fieldType);\n          PopoverContainer.of(context).closeAll();\n        },\n      );\n    }).toList();\n\n    return SizedBox(\n      width: 140,\n      child: ListView.separated(\n        shrinkWrap: true,\n        itemCount: cells.length,\n        separatorBuilder: (context, index) {\n          return VSpace(GridSize.typeOptionSeparatorHeight);\n        },\n        physics: StyledScrollPhysics(),\n        itemBuilder: (BuildContext context, int index) {\n          return cells[index];\n        },\n      ),\n    );\n  }\n}\n\nclass FieldTypeCell extends StatelessWidget {\n  const FieldTypeCell({\n    super.key,\n    required this.fieldType,\n    required this.onSelectField,\n  });\n\n  final FieldType fieldType;\n  final SelectFieldCallback onSelectField;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        text: FlowyText(fieldType.i18n, lineHeight: 1.0),\n        onTap: () => onSelectField(fieldType),\n        leftIcon: FlowySvg(\n          fieldType.svgData,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:flutter/material.dart';\n\nimport 'package:appflowy/plugins/database/widgets/field/type_option_editor/media.dart';\nimport 'package:appflowy/plugins/database/widgets/field/type_option_editor/translate.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\n\nimport 'checkbox.dart';\nimport 'checklist.dart';\nimport 'date.dart';\nimport 'multi_select.dart';\nimport 'number.dart';\nimport 'relation.dart';\nimport 'rich_text.dart';\nimport 'single_select.dart';\nimport 'summary.dart';\nimport 'time.dart';\nimport 'timestamp.dart';\nimport 'url.dart';\n\ntypedef TypeOptionDataCallback = void Function(Uint8List typeOptionData);\n\nabstract class TypeOptionEditorFactory {\n  factory TypeOptionEditorFactory.makeBuilder(FieldType fieldType) {\n    return switch (fieldType) {\n      FieldType.RichText => const RichTextTypeOptionEditorFactory(),\n      FieldType.Number => const NumberTypeOptionEditorFactory(),\n      FieldType.URL => const URLTypeOptionEditorFactory(),\n      FieldType.DateTime => const DateTypeOptionEditorFactory(),\n      FieldType.LastEditedTime => const TimestampTypeOptionEditorFactory(),\n      FieldType.CreatedTime => const TimestampTypeOptionEditorFactory(),\n      FieldType.SingleSelect => const SingleSelectTypeOptionEditorFactory(),\n      FieldType.MultiSelect => const MultiSelectTypeOptionEditorFactory(),\n      FieldType.Checkbox => const CheckboxTypeOptionEditorFactory(),\n      FieldType.Checklist => const ChecklistTypeOptionEditorFactory(),\n      FieldType.Relation => const RelationTypeOptionEditorFactory(),\n      FieldType.Summary => const SummaryTypeOptionEditorFactory(),\n      FieldType.Time => const TimeTypeOptionEditorFactory(),\n      FieldType.Translate => const TranslateTypeOptionEditorFactory(),\n      FieldType.Media => const MediaTypeOptionEditorFactory(),\n      _ => throw UnimplementedError(),\n    };\n  }\n\n  Widget? build({\n    required BuildContext context,\n    required String viewId,\n    required FieldPB field,\n    required PopoverMutex popoverMutex,\n    required TypeOptionDataCallback onTypeOptionUpdated,\n  });\n}\n\nWidget? makeTypeOptionEditor({\n  required BuildContext context,\n  required String viewId,\n  required FieldPB field,\n  required PopoverMutex popoverMutex,\n  required TypeOptionDataCallback onTypeOptionUpdated,\n}) {\n  final editorBuilder = TypeOptionEditorFactory.makeBuilder(field.fieldType);\n  return editorBuilder.build(\n    context: context,\n    viewId: viewId,\n    field: field,\n    onTypeOptionUpdated: onTypeOptionUpdated,\n    popoverMutex: popoverMutex,\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/checkbox.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\n\nimport 'builder.dart';\n\nclass CheckboxTypeOptionEditorFactory implements TypeOptionEditorFactory {\n  const CheckboxTypeOptionEditorFactory();\n\n  @override\n  Widget? build({\n    required BuildContext context,\n    required String viewId,\n    required FieldPB field,\n    required PopoverMutex popoverMutex,\n    required TypeOptionDataCallback onTypeOptionUpdated,\n  }) =>\n      null;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/checklist.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\n\nimport 'builder.dart';\n\nclass ChecklistTypeOptionEditorFactory implements TypeOptionEditorFactory {\n  const ChecklistTypeOptionEditorFactory();\n\n  @override\n  Widget? build({\n    required BuildContext context,\n    required String viewId,\n    required FieldPB field,\n    required PopoverMutex popoverMutex,\n    required TypeOptionDataCallback onTypeOptionUpdated,\n  }) =>\n      null;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\n\nclass DateFormatButton extends StatelessWidget {\n  const DateFormatButton({\n    super.key,\n    this.onTap,\n    this.onHover,\n  });\n\n  final VoidCallback? onTap;\n  final void Function(bool)? onHover;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        text: FlowyText(\n          LocaleKeys.grid_field_dateFormat.tr(),\n          lineHeight: 1.0,\n        ),\n        onTap: onTap,\n        onHover: onHover,\n        rightIcon: const FlowySvg(FlowySvgs.more_s),\n      ),\n    );\n  }\n}\n\nclass TimeFormatButton extends StatelessWidget {\n  const TimeFormatButton({\n    super.key,\n    this.onTap,\n    this.onHover,\n  });\n\n  final VoidCallback? onTap;\n  final void Function(bool)? onHover;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        text: FlowyText(\n          LocaleKeys.grid_field_timeFormat.tr(),\n          lineHeight: 1.0,\n        ),\n        onTap: onTap,\n        onHover: onHover,\n        rightIcon: const FlowySvg(FlowySvgs.more_s),\n      ),\n    );\n  }\n}\n\nclass DateFormatList extends StatelessWidget {\n  const DateFormatList({\n    super.key,\n    required this.selectedFormat,\n    required this.onSelected,\n  });\n\n  final DateFormatPB selectedFormat;\n  final Function(DateFormatPB format) onSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    final cells = DateFormatPB.values\n        .where((value) => value != DateFormatPB.FriendlyFull)\n        .map((format) {\n      return DateFormatCell(\n        dateFormat: format,\n        onSelected: onSelected,\n        isSelected: selectedFormat == format,\n      );\n    }).toList();\n\n    return SizedBox(\n      width: 180,\n      child: ListView.separated(\n        shrinkWrap: true,\n        separatorBuilder: (context, index) {\n          return VSpace(GridSize.typeOptionSeparatorHeight);\n        },\n        itemCount: cells.length,\n        itemBuilder: (BuildContext context, int index) {\n          return cells[index];\n        },\n      ),\n    );\n  }\n}\n\nclass DateFormatCell extends StatelessWidget {\n  const DateFormatCell({\n    super.key,\n    required this.dateFormat,\n    required this.onSelected,\n    required this.isSelected,\n  });\n\n  final DateFormatPB dateFormat;\n  final Function(DateFormatPB format) onSelected;\n  final bool isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    Widget? checkmark;\n    if (isSelected) {\n      checkmark = const FlowySvg(FlowySvgs.check_s);\n    }\n\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        text: FlowyText(\n          dateFormat.title(),\n          lineHeight: 1.0,\n        ),\n        rightIcon: checkmark,\n        onTap: () => onSelected(dateFormat),\n      ),\n    );\n  }\n}\n\nextension DateFormatExtension on DateFormatPB {\n  String title() {\n    switch (this) {\n      case DateFormatPB.Friendly:\n        return LocaleKeys.grid_field_dateFormatFriendly.tr();\n      case DateFormatPB.ISO:\n        return LocaleKeys.grid_field_dateFormatISO.tr();\n      case DateFormatPB.Local:\n        return LocaleKeys.grid_field_dateFormatLocal.tr();\n      case DateFormatPB.US:\n        return LocaleKeys.grid_field_dateFormatUS.tr();\n      case DateFormatPB.DayMonthYear:\n        return LocaleKeys.grid_field_dateFormatDayMonthYear.tr();\n      case DateFormatPB.FriendlyFull:\n        return LocaleKeys.grid_field_dateFormatFriendly.tr();\n      default:\n        throw UnimplementedError;\n    }\n  }\n}\n\nclass TimeFormatList extends StatelessWidget {\n  const TimeFormatList({\n    super.key,\n    required this.selectedFormat,\n    required this.onSelected,\n  });\n\n  final TimeFormatPB selectedFormat;\n  final Function(TimeFormatPB format) onSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    final cells = TimeFormatPB.values.map((format) {\n      return TimeFormatCell(\n        isSelected: format == selectedFormat,\n        timeFormat: format,\n        onSelected: onSelected,\n      );\n    }).toList();\n\n    return SizedBox(\n      width: 120,\n      child: ListView.separated(\n        shrinkWrap: true,\n        separatorBuilder: (context, index) {\n          return VSpace(GridSize.typeOptionSeparatorHeight);\n        },\n        itemCount: cells.length,\n        itemBuilder: (BuildContext context, int index) {\n          return cells[index];\n        },\n      ),\n    );\n  }\n}\n\nclass TimeFormatCell extends StatelessWidget {\n  const TimeFormatCell({\n    super.key,\n    required this.timeFormat,\n    required this.onSelected,\n    required this.isSelected,\n  });\n\n  final TimeFormatPB timeFormat;\n  final bool isSelected;\n  final Function(TimeFormatPB format) onSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    Widget? checkmark;\n    if (isSelected) {\n      checkmark = const FlowySvg(FlowySvgs.check_s);\n    }\n\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        text: FlowyText(\n          timeFormat.title(),\n          lineHeight: 1.0,\n        ),\n        rightIcon: checkmark,\n        onTap: () => onSelected(timeFormat),\n      ),\n    );\n  }\n}\n\nextension TimeFormatExtension on TimeFormatPB {\n  String title() {\n    switch (this) {\n      case TimeFormatPB.TwelveHour:\n        return LocaleKeys.grid_field_timeFormatTwelveHour.tr();\n      case TimeFormatPB.TwentyFourHour:\n        return LocaleKeys.grid_field_timeFormatTwentyFourHour.tr();\n      default:\n        throw UnimplementedError;\n    }\n  }\n}\n\nclass IncludeTimeButton extends StatelessWidget {\n  const IncludeTimeButton({\n    super.key,\n    required this.onChanged,\n    required this.includeTime,\n    this.showIcon = true,\n  });\n\n  final Function(bool value) onChanged;\n  final bool includeTime;\n  final bool showIcon;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: Padding(\n        padding: GridSize.typeOptionContentInsets,\n        child: Row(\n          children: [\n            if (showIcon) ...[\n              FlowySvg(\n                FlowySvgs.clock_alarm_s,\n                color: Theme.of(context).iconTheme.color,\n              ),\n              const HSpace(4),\n            ],\n            const HSpace(2),\n            FlowyText(LocaleKeys.grid_field_includeTime.tr()),\n            const Spacer(),\n            Toggle(\n              value: includeTime,\n              onChanged: onChanged,\n              padding: EdgeInsets.zero,\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date.dart",
    "content": "import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:protobuf/protobuf.dart';\n\nimport '../../../grid/presentation/layout/sizes.dart';\nimport 'builder.dart';\nimport 'date/date_time_format.dart';\n\nclass DateTypeOptionEditorFactory implements TypeOptionEditorFactory {\n  const DateTypeOptionEditorFactory();\n\n  @override\n  Widget? build({\n    required BuildContext context,\n    required String viewId,\n    required FieldPB field,\n    required PopoverMutex popoverMutex,\n    required TypeOptionDataCallback onTypeOptionUpdated,\n  }) {\n    final typeOption = _parseTypeOptionData(field.typeOptionData);\n\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        _renderDateFormatButton(\n          typeOption,\n          popoverMutex,\n          onTypeOptionUpdated,\n        ),\n        VSpace(GridSize.typeOptionSeparatorHeight),\n        _renderTimeFormatButton(\n          typeOption,\n          popoverMutex,\n          onTypeOptionUpdated,\n        ),\n      ],\n    );\n  }\n\n  Widget _renderDateFormatButton(\n    DateTypeOptionPB typeOption,\n    PopoverMutex popoverMutex,\n    TypeOptionDataCallback onTypeOptionUpdated,\n  ) {\n    return AppFlowyPopover(\n      mutex: popoverMutex,\n      asBarrier: true,\n      triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,\n      offset: const Offset(8, 0),\n      constraints: BoxConstraints.loose(const Size(460, 440)),\n      popupBuilder: (popoverContext) {\n        return DateFormatList(\n          selectedFormat: typeOption.dateFormat,\n          onSelected: (format) {\n            final newTypeOption =\n                _updateTypeOption(typeOption: typeOption, dateFormat: format);\n            onTypeOptionUpdated(newTypeOption.writeToBuffer());\n            PopoverContainer.of(popoverContext).close();\n          },\n        );\n      },\n      child: const Padding(\n        padding: EdgeInsets.symmetric(horizontal: 12.0),\n        child: DateFormatButton(),\n      ),\n    );\n  }\n\n  Widget _renderTimeFormatButton(\n    DateTypeOptionPB typeOption,\n    PopoverMutex popoverMutex,\n    TypeOptionDataCallback onTypeOptionUpdated,\n  ) {\n    return AppFlowyPopover(\n      mutex: popoverMutex,\n      asBarrier: true,\n      triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,\n      offset: const Offset(8, 0),\n      constraints: BoxConstraints.loose(const Size(460, 440)),\n      popupBuilder: (BuildContext popoverContext) {\n        return TimeFormatList(\n          selectedFormat: typeOption.timeFormat,\n          onSelected: (format) {\n            final newTypeOption =\n                _updateTypeOption(typeOption: typeOption, timeFormat: format);\n            onTypeOptionUpdated(newTypeOption.writeToBuffer());\n            PopoverContainer.of(popoverContext).close();\n          },\n        );\n      },\n      child: const Padding(\n        padding: EdgeInsets.symmetric(horizontal: 12.0),\n        child: TimeFormatButton(),\n      ),\n    );\n  }\n\n  DateTypeOptionPB _parseTypeOptionData(List<int> data) {\n    return DateTypeOptionDataParser().fromBuffer(data);\n  }\n\n  DateTypeOptionPB _updateTypeOption({\n    required DateTypeOptionPB typeOption,\n    DateFormatPB? dateFormat,\n    TimeFormatPB? timeFormat,\n  }) {\n    typeOption.freeze();\n    return typeOption.rebuild((typeOption) {\n      if (dateFormat != null) {\n        typeOption.dateFormat = dateFormat;\n      }\n\n      if (timeFormat != null) {\n        typeOption.timeFormat = timeFormat;\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/media.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/field/type_option_editor/builder.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:protobuf/protobuf.dart';\n\nclass MediaTypeOptionEditorFactory implements TypeOptionEditorFactory {\n  const MediaTypeOptionEditorFactory();\n\n  @override\n  Widget? build({\n    required BuildContext context,\n    required String viewId,\n    required FieldPB field,\n    required PopoverMutex popoverMutex,\n    required TypeOptionDataCallback onTypeOptionUpdated,\n  }) {\n    final typeOption = _parseTypeOptionData(field.typeOptionData);\n\n    return Container(\n      padding: const EdgeInsets.symmetric(horizontal: 8),\n      height: GridSize.popoverItemHeight,\n      alignment: Alignment.centerLeft,\n      child: FlowyButton(\n        resetHoverOnRebuild: false,\n        text: FlowyText(\n          LocaleKeys.grid_media_showFileNames.tr(),\n          lineHeight: 1.0,\n        ),\n        onHover: (_) => popoverMutex.close(),\n        rightIcon: Toggle(\n          value: !typeOption.hideFileNames,\n          onChanged: (val) => onTypeOptionUpdated(\n            _toggleHideFiles(typeOption, !val).writeToBuffer(),\n          ),\n          padding: EdgeInsets.zero,\n        ),\n      ),\n    );\n  }\n\n  MediaTypeOptionPB _parseTypeOptionData(List<int> data) {\n    return MediaTypeOptionDataParser().fromBuffer(data);\n  }\n\n  MediaTypeOptionPB _toggleHideFiles(\n    MediaTypeOptionPB typeOption,\n    bool hideFileNames,\n  ) {\n    typeOption.freeze();\n    return typeOption.rebuild((to) => to.hideFileNames = hideFileNames);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/multi_select.dart",
    "content": "import 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter/material.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\n\nimport 'builder.dart';\nimport 'select/select_option.dart';\n\nclass MultiSelectTypeOptionEditorFactory implements TypeOptionEditorFactory {\n  const MultiSelectTypeOptionEditorFactory();\n\n  @override\n  Widget? build({\n    required BuildContext context,\n    required String viewId,\n    required FieldPB field,\n    required PopoverMutex popoverMutex,\n    required TypeOptionDataCallback onTypeOptionUpdated,\n  }) {\n    final typeOption = _parseTypeOptionData(field.typeOptionData);\n\n    return SelectOptionTypeOptionWidget(\n      options: typeOption.options,\n      beginEdit: () => PopoverContainer.of(context).closeAll(),\n      popoverMutex: popoverMutex,\n      typeOptionAction: MultiSelectAction(\n        viewId: viewId,\n        fieldId: field.id,\n        onTypeOptionUpdated: onTypeOptionUpdated,\n      ),\n    );\n  }\n\n  MultiSelectTypeOptionPB _parseTypeOptionData(List<int> data) {\n    return MultiSelectTypeOptionDataParser().fromBuffer(data);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/number.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:protobuf/protobuf.dart';\n\nimport '../../../grid/presentation/layout/sizes.dart';\nimport '../../../grid/presentation/widgets/common/type_option_separator.dart';\nimport 'builder.dart';\n\nclass NumberTypeOptionEditorFactory implements TypeOptionEditorFactory {\n  const NumberTypeOptionEditorFactory();\n\n  @override\n  Widget? build({\n    required BuildContext context,\n    required String viewId,\n    required FieldPB field,\n    required PopoverMutex popoverMutex,\n    required TypeOptionDataCallback onTypeOptionUpdated,\n  }) {\n    final typeOption = _parseTypeOptionData(field.typeOptionData);\n\n    final selectNumUnitButton = SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        rightIcon: const FlowySvg(FlowySvgs.more_s),\n        text: FlowyText(\n          lineHeight: 1.0,\n          typeOption.format.title(),\n        ),\n      ),\n    );\n\n    final numFormatTitle = Container(\n      padding: const EdgeInsets.only(left: 6),\n      height: GridSize.popoverItemHeight,\n      alignment: Alignment.centerLeft,\n      child: FlowyText.regular(\n        LocaleKeys.grid_field_numberFormat.tr(),\n        color: Theme.of(context).hintColor,\n        fontSize: 11,\n      ),\n    );\n\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 8),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          numFormatTitle,\n          AppFlowyPopover(\n            mutex: popoverMutex,\n            triggerActions:\n                PopoverTriggerFlags.hover | PopoverTriggerFlags.click,\n            offset: const Offset(16, 0),\n            constraints: BoxConstraints.loose(const Size(460, 440)),\n            margin: EdgeInsets.zero,\n            child: selectNumUnitButton,\n            popupBuilder: (BuildContext popoverContext) {\n              return NumberFormatList(\n                selectedFormat: typeOption.format,\n                onSelected: (format) {\n                  final newTypeOption = _updateNumberFormat(typeOption, format);\n                  onTypeOptionUpdated(newTypeOption.writeToBuffer());\n                  PopoverContainer.of(popoverContext).close();\n                },\n              );\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  NumberTypeOptionPB _parseTypeOptionData(List<int> data) {\n    return NumberTypeOptionDataParser().fromBuffer(data);\n  }\n\n  NumberTypeOptionPB _updateNumberFormat(\n    NumberTypeOptionPB typeOption,\n    NumberFormatPB format,\n  ) {\n    typeOption.freeze();\n    return typeOption.rebuild((typeOption) => typeOption.format = format);\n  }\n}\n\ntypedef SelectNumberFormatCallback = void Function(NumberFormatPB format);\n\nclass NumberFormatList extends StatelessWidget {\n  const NumberFormatList({\n    super.key,\n    required this.selectedFormat,\n    required this.onSelected,\n  });\n\n  final NumberFormatPB selectedFormat;\n  final SelectNumberFormatCallback onSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => NumberFormatBloc(),\n      child: SizedBox(\n        width: 180,\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            const _FilterTextField(),\n            const TypeOptionSeparator(spacing: 0.0),\n            BlocBuilder<NumberFormatBloc, NumberFormatState>(\n              builder: (context, state) {\n                final cells = state.formats.map((format) {\n                  return NumberFormatCell(\n                    isSelected: format == selectedFormat,\n                    format: format,\n                    onSelected: (format) {\n                      onSelected(format);\n                    },\n                  );\n                }).toList();\n\n                final list = ListView.separated(\n                  shrinkWrap: true,\n                  separatorBuilder: (context, index) {\n                    return VSpace(GridSize.typeOptionSeparatorHeight);\n                  },\n                  itemCount: cells.length,\n                  itemBuilder: (BuildContext context, int index) {\n                    return cells[index];\n                  },\n                  padding: const EdgeInsets.all(6.0),\n                );\n                return Flexible(child: list);\n              },\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass NumberFormatCell extends StatelessWidget {\n  const NumberFormatCell({\n    super.key,\n    required this.format,\n    required this.isSelected,\n    required this.onSelected,\n  });\n\n  final NumberFormatPB format;\n  final bool isSelected;\n  final SelectNumberFormatCallback onSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    final checkmark = isSelected ? const FlowySvg(FlowySvgs.check_s) : null;\n\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        text: FlowyText(\n          format.title(),\n          lineHeight: 1.0,\n        ),\n        onTap: () => onSelected(format),\n        rightIcon: checkmark,\n      ),\n    );\n  }\n}\n\nclass _FilterTextField extends StatelessWidget {\n  const _FilterTextField();\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(6.0),\n      child: FlowyTextField(\n        onChanged: (text) => context\n            .read<NumberFormatBloc>()\n            .add(NumberFormatEvent.setFilter(text)),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/relation.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/relation_type_option_cubit.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:protobuf/protobuf.dart';\n\nimport 'builder.dart';\n\nclass RelationTypeOptionEditorFactory implements TypeOptionEditorFactory {\n  const RelationTypeOptionEditorFactory();\n\n  @override\n  Widget? build({\n    required BuildContext context,\n    required String viewId,\n    required FieldPB field,\n    required PopoverMutex popoverMutex,\n    required TypeOptionDataCallback onTypeOptionUpdated,\n  }) {\n    final typeOption = _parseTypeOptionData(field.typeOptionData);\n\n    return BlocProvider(\n      create: (_) => RelationDatabaseListCubit(),\n      child: Builder(\n        builder: (context) {\n          return Column(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              Container(\n                padding: const EdgeInsets.only(left: 14, right: 8),\n                height: GridSize.popoverItemHeight,\n                alignment: Alignment.centerLeft,\n                child: FlowyText.regular(\n                  LocaleKeys.grid_relation_relatedDatabasePlaceLabel.tr(),\n                  color: Theme.of(context).hintColor,\n                  fontSize: 11,\n                ),\n              ),\n              AppFlowyPopover(\n                mutex: popoverMutex,\n                triggerActions:\n                    PopoverTriggerFlags.hover | PopoverTriggerFlags.click,\n                offset: const Offset(6, 0),\n                child: Container(\n                  padding: const EdgeInsets.symmetric(horizontal: 8),\n                  height: GridSize.popoverItemHeight,\n                  child: FlowyButton(\n                    text: BlocBuilder<RelationDatabaseListCubit,\n                        RelationDatabaseListState>(\n                      builder: (context, state) {\n                        final databaseMeta =\n                            state.databaseMetas.firstWhereOrNull(\n                          (meta) => meta.databaseId == typeOption.databaseId,\n                        );\n                        return FlowyText(\n                          lineHeight: 1.0,\n                          databaseMeta == null\n                              ? LocaleKeys\n                                  .grid_relation_relatedDatabasePlaceholder\n                                  .tr()\n                              : databaseMeta.databaseName,\n                          color: databaseMeta == null\n                              ? Theme.of(context).hintColor\n                              : null,\n                          overflow: TextOverflow.ellipsis,\n                        );\n                      },\n                    ),\n                    rightIcon: const FlowySvg(FlowySvgs.more_s),\n                  ),\n                ),\n                popupBuilder: (popoverContext) {\n                  return BlocProvider.value(\n                    value: context.read<RelationDatabaseListCubit>(),\n                    child: _DatabaseList(\n                      onSelectDatabase: (newDatabaseId) {\n                        final newTypeOption = _updateTypeOption(\n                          typeOption: typeOption,\n                          databaseId: newDatabaseId,\n                        );\n                        onTypeOptionUpdated(newTypeOption.writeToBuffer());\n                        PopoverContainer.of(context).close();\n                      },\n                      currentDatabaseId: typeOption.databaseId,\n                    ),\n                  );\n                },\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  RelationTypeOptionPB _parseTypeOptionData(List<int> data) {\n    return RelationTypeOptionDataParser().fromBuffer(data);\n  }\n\n  RelationTypeOptionPB _updateTypeOption({\n    required RelationTypeOptionPB typeOption,\n    required String databaseId,\n  }) {\n    typeOption.freeze();\n    return typeOption.rebuild((typeOption) {\n      typeOption.databaseId = databaseId;\n    });\n  }\n}\n\nclass _DatabaseList extends StatelessWidget {\n  const _DatabaseList({\n    required this.onSelectDatabase,\n    required this.currentDatabaseId,\n  });\n\n  final String currentDatabaseId;\n  final void Function(String databaseId) onSelectDatabase;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<RelationDatabaseListCubit, RelationDatabaseListState>(\n      builder: (context, state) {\n        final children = state.databaseMetas.map((meta) {\n          return SizedBox(\n            height: GridSize.popoverItemHeight,\n            child: FlowyButton(\n              onTap: () => onSelectDatabase(meta.databaseId),\n              text: FlowyText(\n                lineHeight: 1.0,\n                meta.databaseName,\n                overflow: TextOverflow.ellipsis,\n              ),\n              rightIcon: meta.databaseId == currentDatabaseId\n                  ? const FlowySvg(\n                      FlowySvgs.check_s,\n                    )\n                  : null,\n            ),\n          );\n        }).toList();\n\n        return ListView.separated(\n          shrinkWrap: true,\n          padding: EdgeInsets.zero,\n          separatorBuilder: (_, __) =>\n              VSpace(GridSize.typeOptionSeparatorHeight),\n          itemCount: children.length,\n          itemBuilder: (context, index) => children[index],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/rich_text.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\nimport 'builder.dart';\n\nclass RichTextTypeOptionEditorFactory implements TypeOptionEditorFactory {\n  const RichTextTypeOptionEditorFactory();\n\n  @override\n  Widget? build({\n    required BuildContext context,\n    required String viewId,\n    required FieldPB field,\n    required PopoverMutex popoverMutex,\n    required TypeOptionDataCallback onTypeOptionUpdated,\n  }) =>\n      null;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/select_option_type_option_bloc.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'select_option_editor.dart';\n\nclass SelectOptionTypeOptionWidget extends StatelessWidget {\n  const SelectOptionTypeOptionWidget({\n    super.key,\n    required this.options,\n    required this.beginEdit,\n    required this.typeOptionAction,\n    this.popoverMutex,\n  });\n\n  final List<SelectOptionPB> options;\n  final VoidCallback beginEdit;\n  final ISelectOptionAction typeOptionAction;\n  final PopoverMutex? popoverMutex;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<SelectOptionTypeOptionBloc>(\n      create: (context) => SelectOptionTypeOptionBloc(\n        options: options,\n        typeOptionAction: typeOptionAction,\n      ),\n      child:\n          BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>(\n        builder: (context, state) {\n          final List<Widget> children = [\n            const _OptionTitle(),\n            const VSpace(4),\n            if (state.isEditingOption) ...[\n              CreateOptionTextField(popoverMutex: popoverMutex),\n              const VSpace(4),\n            ] else\n              const _AddOptionButton(),\n            const VSpace(4),\n            Flexible(\n              child: _OptionList(\n                popoverMutex: popoverMutex,\n              ),\n            ),\n          ];\n\n          return Column(\n            mainAxisSize: MainAxisSize.min,\n            children: children,\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _OptionTitle extends StatelessWidget {\n  const _OptionTitle();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>(\n      builder: (context, state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 16),\n          child: Align(\n            alignment: AlignmentDirectional.centerStart,\n            child: FlowyText.regular(\n              LocaleKeys.grid_field_optionTitle.tr(),\n              fontSize: 11,\n              color: Theme.of(context).hintColor,\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass _OptionCell extends StatefulWidget {\n  const _OptionCell({\n    super.key,\n    required this.option,\n    required this.index,\n    this.popoverMutex,\n  });\n\n  final SelectOptionPB option;\n  final int index;\n  final PopoverMutex? popoverMutex;\n\n  @override\n  State<_OptionCell> createState() => _OptionCellState();\n}\n\nclass _OptionCellState extends State<_OptionCell> {\n  final PopoverController _popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    final child = SizedBox(\n      height: 28,\n      child: SelectOptionTagCell(\n        option: widget.option,\n        index: widget.index,\n        onSelected: () => _popoverController.show(),\n        children: [\n          FlowyIconButton(\n            onPressed: () => _popoverController.show(),\n            iconPadding: const EdgeInsets.symmetric(horizontal: 6.0),\n            hoverColor: Colors.transparent,\n            icon: FlowySvg(\n              FlowySvgs.three_dots_s,\n              color: Theme.of(context).iconTheme.color,\n              size: const Size.square(16),\n            ),\n          ),\n        ],\n      ),\n    );\n    return AppFlowyPopover(\n      controller: _popoverController,\n      mutex: widget.popoverMutex,\n      offset: const Offset(8, 0),\n      margin: EdgeInsets.zero,\n      asBarrier: true,\n      constraints: BoxConstraints.loose(const Size(460, 470)),\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 8.0),\n        child: FlowyHover(\n          resetHoverOnRebuild: false,\n          style: HoverStyle(\n            hoverColor: AFThemeExtension.of(context).lightGreyHover,\n          ),\n          child: child,\n        ),\n      ),\n      popupBuilder: (BuildContext popoverContext) {\n        return SelectOptionEditor(\n          option: widget.option,\n          onDeleted: () {\n            context\n                .read<SelectOptionTypeOptionBloc>()\n                .add(SelectOptionTypeOptionEvent.deleteOption(widget.option));\n            PopoverContainer.of(popoverContext).close();\n          },\n          onUpdated: (updatedOption) {\n            context\n                .read<SelectOptionTypeOptionBloc>()\n                .add(SelectOptionTypeOptionEvent.updateOption(updatedOption));\n            PopoverContainer.of(popoverContext).close();\n          },\n          key: ValueKey(widget.option.id),\n        );\n      },\n    );\n  }\n}\n\nclass _AddOptionButton extends StatelessWidget {\n  const _AddOptionButton();\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 8.0),\n      child: SizedBox(\n        height: GridSize.popoverItemHeight,\n        child: FlowyButton(\n          text: FlowyText(\n            lineHeight: 1.0,\n            LocaleKeys.grid_field_addSelectOption.tr(),\n          ),\n          onTap: () {\n            context\n                .read<SelectOptionTypeOptionBloc>()\n                .add(const SelectOptionTypeOptionEvent.addingOption());\n          },\n          leftIcon: const FlowySvg(FlowySvgs.add_s),\n        ),\n      ),\n    );\n  }\n}\n\nclass CreateOptionTextField extends StatefulWidget {\n  const CreateOptionTextField({super.key, this.popoverMutex});\n\n  final PopoverMutex? popoverMutex;\n\n  @override\n  State<CreateOptionTextField> createState() => _CreateOptionTextFieldState();\n}\n\nclass _CreateOptionTextFieldState extends State<CreateOptionTextField> {\n  final focusNode = FocusNode();\n\n  @override\n  void initState() {\n    super.initState();\n\n    focusNode.addListener(_onFocusChanged);\n    widget.popoverMutex?.addPopoverListener(_onPopoverChanged);\n  }\n\n  @override\n  void dispose() {\n    widget.popoverMutex?.removePopoverListener(_onPopoverChanged);\n    focusNode.removeListener(_onFocusChanged);\n    focusNode.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>(\n      builder: (context, state) {\n        final text = state.newOptionName ?? '';\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 14.0),\n          child: FlowyTextField(\n            autoClearWhenDone: true,\n            text: text,\n            focusNode: focusNode,\n            onCanceled: () {\n              context\n                  .read<SelectOptionTypeOptionBloc>()\n                  .add(const SelectOptionTypeOptionEvent.endAddingOption());\n            },\n            onEditingComplete: () {},\n            onSubmitted: (optionName) {\n              context\n                  .read<SelectOptionTypeOptionBloc>()\n                  .add(SelectOptionTypeOptionEvent.createOption(optionName));\n            },\n          ),\n        );\n      },\n    );\n  }\n\n  void _onFocusChanged() {\n    if (focusNode.hasFocus) {\n      widget.popoverMutex?.close();\n    }\n  }\n\n  void _onPopoverChanged() {\n    if (focusNode.hasFocus) {\n      focusNode.unfocus();\n    }\n  }\n}\n\nclass _OptionList extends StatelessWidget {\n  const _OptionList({\n    this.popoverMutex,\n  });\n\n  final PopoverMutex? popoverMutex;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>(\n      builder: (context, state) {\n        return ReorderableListView.builder(\n          shrinkWrap: true,\n          onReorderStart: (_) => popoverMutex?.close(),\n          proxyDecorator: (child, index, _) => Material(\n            color: Colors.transparent,\n            child: Stack(\n              children: [\n                BlocProvider.value(\n                  value: context.read<SelectOptionTypeOptionBloc>(),\n                  child: child,\n                ),\n                MouseRegion(\n                  cursor: Platform.isWindows\n                      ? SystemMouseCursors.click\n                      : SystemMouseCursors.grabbing,\n                  child: const SizedBox.expand(),\n                ),\n              ],\n            ),\n          ),\n          buildDefaultDragHandles: false,\n          itemBuilder: (context, index) => _OptionCell(\n            key: ValueKey(\"select_type_option_list_${state.options[index].id}\"),\n            index: index,\n            option: state.options[index],\n            popoverMutex: popoverMutex,\n          ),\n          itemCount: state.options.length,\n          onReorder: (oldIndex, newIndex) {\n            if (oldIndex < newIndex) {\n              newIndex--;\n            }\n            final fromOptionId = state.options[oldIndex].id;\n            final toOptionId = state.options[newIndex].id;\n            context.read<SelectOptionTypeOptionBloc>().add(\n                  SelectOptionTypeOptionEvent.reorderOption(\n                    fromOptionId,\n                    toOptionId,\n                  ),\n                );\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option_editor.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/edit_select_option_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/style_widget/text_field.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../../../grid/presentation/layout/sizes.dart';\nimport '../../../../grid/presentation/widgets/common/type_option_separator.dart';\n\nclass SelectOptionEditor extends StatelessWidget {\n  const SelectOptionEditor({\n    super.key,\n    required this.option,\n    required this.onDeleted,\n    required this.onUpdated,\n    this.showOptions = true,\n    this.autoFocus = true,\n  });\n\n  final SelectOptionPB option;\n  final VoidCallback onDeleted;\n  final Function(SelectOptionPB) onUpdated;\n  final bool showOptions;\n  final bool autoFocus;\n\n  static String get identifier => (SelectOptionEditor).toString();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => EditSelectOptionBloc(option: option),\n      child: MultiBlocListener(\n        listeners: [\n          BlocListener<EditSelectOptionBloc, EditSelectOptionState>(\n            listenWhen: (p, c) => p.deleted != c.deleted,\n            listener: (context, state) {\n              if (state.deleted) {\n                onDeleted();\n              }\n            },\n          ),\n          BlocListener<EditSelectOptionBloc, EditSelectOptionState>(\n            listenWhen: (p, c) => p.option != c.option,\n            listener: (context, state) {\n              onUpdated(state.option);\n            },\n          ),\n        ],\n        child: BlocBuilder<EditSelectOptionBloc, EditSelectOptionState>(\n          builder: (context, state) {\n            final List<Widget> cells = [\n              _OptionNameTextField(\n                name: state.option.name,\n                autoFocus: autoFocus,\n              ),\n              const VSpace(10),\n              const _DeleteTag(),\n              const TypeOptionSeparator(),\n              SelectOptionColorList(\n                selectedColor: state.option.color,\n                onSelectedColor: (color) => context\n                    .read<EditSelectOptionBloc>()\n                    .add(EditSelectOptionEvent.updateColor(color)),\n              ),\n            ];\n            return SizedBox(\n              width: 180,\n              child: ListView.builder(\n                shrinkWrap: true,\n                physics: StyledScrollPhysics(),\n                itemCount: cells.length,\n                itemBuilder: (context, index) {\n                  if (cells[index] is TypeOptionSeparator) {\n                    return cells[index];\n                  } else {\n                    return Padding(\n                      padding: const EdgeInsets.symmetric(horizontal: 6.0),\n                      child: cells[index],\n                    );\n                  }\n                },\n                padding: const EdgeInsets.symmetric(vertical: 6.0),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass _DeleteTag extends StatelessWidget {\n  const _DeleteTag();\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        text: FlowyText(\n          lineHeight: 1.0,\n          LocaleKeys.grid_selectOption_deleteTag.tr(),\n        ),\n        leftIcon: const FlowySvg(FlowySvgs.delete_s),\n        onTap: () {\n          context\n              .read<EditSelectOptionBloc>()\n              .add(const EditSelectOptionEvent.delete());\n        },\n      ),\n    );\n  }\n}\n\nclass _OptionNameTextField extends StatelessWidget {\n  const _OptionNameTextField({\n    required this.name,\n    required this.autoFocus,\n  });\n\n  final String name;\n  final bool autoFocus;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTextField(\n      autoFocus: autoFocus,\n      text: name,\n      submitOnLeave: true,\n      onSubmitted: (newName) {\n        if (name != newName) {\n          context\n              .read<EditSelectOptionBloc>()\n              .add(EditSelectOptionEvent.updateName(newName));\n        }\n      },\n    );\n  }\n}\n\nclass SelectOptionColorList extends StatelessWidget {\n  const SelectOptionColorList({\n    super.key,\n    this.selectedColor,\n    required this.onSelectedColor,\n  });\n\n  final SelectOptionColorPB? selectedColor;\n  final void Function(SelectOptionColorPB color) onSelectedColor;\n\n  @override\n  Widget build(BuildContext context) {\n    final cells = SelectOptionColorPB.values.map((color) {\n      return _SelectOptionColorCell(\n        color: color,\n        isSelected: selectedColor != null ? selectedColor == color : false,\n        onSelectedColor: onSelectedColor,\n      );\n    }).toList();\n\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Padding(\n          padding: GridSize.typeOptionContentInsets,\n          child: SizedBox(\n            height: GridSize.popoverItemHeight,\n            child: FlowyText(\n              LocaleKeys.grid_selectOption_colorPanelTitle.tr(),\n              textAlign: TextAlign.left,\n              color: Theme.of(context).hintColor,\n            ),\n          ),\n        ),\n        ListView.separated(\n          shrinkWrap: true,\n          separatorBuilder: (context, index) {\n            return VSpace(GridSize.typeOptionSeparatorHeight);\n          },\n          itemCount: cells.length,\n          physics: StyledScrollPhysics(),\n          itemBuilder: (BuildContext context, int index) {\n            return cells[index];\n          },\n        ),\n      ],\n    );\n  }\n}\n\nclass _SelectOptionColorCell extends StatelessWidget {\n  const _SelectOptionColorCell({\n    required this.color,\n    required this.isSelected,\n    required this.onSelectedColor,\n  });\n\n  final SelectOptionColorPB color;\n  final bool isSelected;\n  final void Function(SelectOptionColorPB color) onSelectedColor;\n\n  @override\n  Widget build(BuildContext context) {\n    Widget? checkmark;\n    if (isSelected) {\n      checkmark = const FlowySvg(FlowySvgs.check_s);\n    }\n\n    final colorIcon = SizedBox.square(\n      dimension: 16,\n      child: DecoratedBox(\n        decoration: BoxDecoration(\n          color: color.toColor(context),\n          shape: BoxShape.circle,\n        ),\n      ),\n    );\n\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        text: FlowyText(\n          lineHeight: 1.0,\n          color.colorName(),\n          color: AFThemeExtension.of(context).textColor,\n        ),\n        leftIcon: colorIcon,\n        rightIcon: checkmark,\n        onTap: () => onSelectedColor(color),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/single_select.dart",
    "content": "import 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter/material.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\n\nimport 'builder.dart';\nimport 'select/select_option.dart';\n\nclass SingleSelectTypeOptionEditorFactory implements TypeOptionEditorFactory {\n  const SingleSelectTypeOptionEditorFactory();\n\n  @override\n  Widget? build({\n    required BuildContext context,\n    required String viewId,\n    required FieldPB field,\n    required PopoverMutex popoverMutex,\n    required TypeOptionDataCallback onTypeOptionUpdated,\n  }) {\n    final typeOption = _parseTypeOptionData(field.typeOptionData);\n\n    return SelectOptionTypeOptionWidget(\n      options: typeOption.options,\n      beginEdit: () => PopoverContainer.of(context).closeAll(),\n      popoverMutex: popoverMutex,\n      typeOptionAction: SingleSelectAction(\n        viewId: viewId,\n        fieldId: field.id,\n        onTypeOptionUpdated: onTypeOptionUpdated,\n      ),\n    );\n  }\n\n  SingleSelectTypeOptionPB _parseTypeOptionData(List<int> data) {\n    return SingleSelectTypeOptionDataParser().fromBuffer(data);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/summary.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\n\nimport 'builder.dart';\n\nclass SummaryTypeOptionEditorFactory implements TypeOptionEditorFactory {\n  const SummaryTypeOptionEditorFactory();\n\n  @override\n  Widget? build({\n    required BuildContext context,\n    required String viewId,\n    required FieldPB field,\n    required PopoverMutex popoverMutex,\n    required TypeOptionDataCallback onTypeOptionUpdated,\n  }) =>\n      null;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/time.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\n\nimport 'builder.dart';\n\nclass TimeTypeOptionEditorFactory implements TypeOptionEditorFactory {\n  const TimeTypeOptionEditorFactory();\n\n  @override\n  Widget? build({\n    required BuildContext context,\n    required String viewId,\n    required FieldPB field,\n    required PopoverMutex popoverMutex,\n    required TypeOptionDataCallback onTypeOptionUpdated,\n  }) =>\n      null;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/timestamp.dart",
    "content": "import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:protobuf/protobuf.dart';\n\nimport 'builder.dart';\nimport 'date/date_time_format.dart';\n\nclass TimestampTypeOptionEditorFactory implements TypeOptionEditorFactory {\n  const TimestampTypeOptionEditorFactory();\n\n  @override\n  Widget? build({\n    required BuildContext context,\n    required String viewId,\n    required FieldPB field,\n    required PopoverMutex popoverMutex,\n    required TypeOptionDataCallback onTypeOptionUpdated,\n  }) {\n    final typeOption = _parseTypeOptionData(field.typeOptionData);\n\n    return SeparatedColumn(\n      mainAxisSize: MainAxisSize.min,\n      separatorBuilder: () => VSpace(GridSize.typeOptionSeparatorHeight),\n      children: [\n        _renderDateFormatButton(typeOption, popoverMutex, onTypeOptionUpdated),\n        _renderTimeFormatButton(typeOption, popoverMutex, onTypeOptionUpdated),\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 12.0),\n          child: IncludeTimeButton(\n            includeTime: typeOption.includeTime,\n            showIcon: false,\n            onChanged: (value) {\n              final newTypeOption = _updateTypeOption(\n                typeOption: typeOption,\n                includeTime: value,\n              );\n              onTypeOptionUpdated(newTypeOption.writeToBuffer());\n            },\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget _renderDateFormatButton(\n    TimestampTypeOptionPB typeOption,\n    PopoverMutex popoverMutex,\n    TypeOptionDataCallback onTypeOptionUpdated,\n  ) {\n    return AppFlowyPopover(\n      mutex: popoverMutex,\n      asBarrier: true,\n      triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,\n      offset: const Offset(8, 0),\n      constraints: BoxConstraints.loose(const Size(460, 440)),\n      popupBuilder: (popoverContext) {\n        return DateFormatList(\n          selectedFormat: typeOption.dateFormat,\n          onSelected: (format) {\n            final newTypeOption =\n                _updateTypeOption(typeOption: typeOption, dateFormat: format);\n            onTypeOptionUpdated(newTypeOption.writeToBuffer());\n            PopoverContainer.of(popoverContext).close();\n          },\n        );\n      },\n      child: const Padding(\n        padding: EdgeInsets.symmetric(horizontal: 12.0),\n        child: DateFormatButton(),\n      ),\n    );\n  }\n\n  Widget _renderTimeFormatButton(\n    TimestampTypeOptionPB typeOption,\n    PopoverMutex popoverMutex,\n    TypeOptionDataCallback onTypeOptionUpdated,\n  ) {\n    return AppFlowyPopover(\n      mutex: popoverMutex,\n      asBarrier: true,\n      triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,\n      offset: const Offset(8, 0),\n      constraints: BoxConstraints.loose(const Size(460, 440)),\n      popupBuilder: (BuildContext popoverContext) {\n        return TimeFormatList(\n          selectedFormat: typeOption.timeFormat,\n          onSelected: (format) {\n            final newTypeOption =\n                _updateTypeOption(typeOption: typeOption, timeFormat: format);\n            onTypeOptionUpdated(newTypeOption.writeToBuffer());\n            PopoverContainer.of(popoverContext).close();\n          },\n        );\n      },\n      child: const Padding(\n        padding: EdgeInsets.symmetric(horizontal: 12.0),\n        child: TimeFormatButton(),\n      ),\n    );\n  }\n\n  TimestampTypeOptionPB _parseTypeOptionData(List<int> data) {\n    return TimestampTypeOptionDataParser().fromBuffer(data);\n  }\n\n  TimestampTypeOptionPB _updateTypeOption({\n    required TimestampTypeOptionPB typeOption,\n    DateFormatPB? dateFormat,\n    TimeFormatPB? timeFormat,\n    bool? includeTime,\n  }) {\n    typeOption.freeze();\n    return typeOption.rebuild((typeOption) {\n      if (dateFormat != null) {\n        typeOption.dateFormat = dateFormat;\n      }\n\n      if (timeFormat != null) {\n        typeOption.timeFormat = timeFormat;\n      }\n\n      if (includeTime != null) {\n        typeOption.includeTime = includeTime;\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/translate.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/translate_type_option_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport './builder.dart';\n\nclass TranslateTypeOptionEditorFactory implements TypeOptionEditorFactory {\n  const TranslateTypeOptionEditorFactory();\n\n  @override\n  Widget? build({\n    required BuildContext context,\n    required String viewId,\n    required FieldPB field,\n    required PopoverMutex popoverMutex,\n    required TypeOptionDataCallback onTypeOptionUpdated,\n  }) {\n    final typeOption = TranslateTypeOptionPB.fromBuffer(field.typeOptionData);\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 12.0),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          FlowyText(\n            LocaleKeys.grid_field_translateTo.tr(),\n          ),\n          const HSpace(6),\n          Padding(\n            padding: const EdgeInsets.symmetric(vertical: 8),\n            child: BlocProvider(\n              create: (context) => TranslateTypeOptionBloc(option: typeOption),\n              child: BlocConsumer<TranslateTypeOptionBloc,\n                  TranslateTypeOptionState>(\n                listenWhen: (previous, current) =>\n                    previous.option != current.option,\n                listener: (context, state) {\n                  onTypeOptionUpdated(state.option.writeToBuffer());\n                },\n                builder: (context, state) {\n                  return _wrapLanguageListPopover(\n                    context,\n                    state,\n                    popoverMutex,\n                    SelectLanguageButton(\n                      language: state.language,\n                    ),\n                  );\n                },\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _wrapLanguageListPopover(\n    BuildContext blocContext,\n    TranslateTypeOptionState state,\n    PopoverMutex popoverMutex,\n    Widget child,\n  ) {\n    return AppFlowyPopover(\n      mutex: popoverMutex,\n      asBarrier: true,\n      triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,\n      offset: const Offset(8, 0),\n      constraints: BoxConstraints.loose(const Size(460, 440)),\n      popupBuilder: (popoverContext) {\n        return LanguageList(\n          onSelected: (language) {\n            blocContext\n                .read<TranslateTypeOptionBloc>()\n                .add(TranslateTypeOptionEvent.selectLanguage(language));\n            PopoverContainer.of(popoverContext).close();\n          },\n          selectedLanguage: state.option.language,\n        );\n      },\n      child: child,\n    );\n  }\n}\n\nclass SelectLanguageButton extends StatelessWidget {\n  const SelectLanguageButton({required this.language, super.key});\n  final String language;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 30,\n      child: FlowyButton(\n        text: FlowyText(\n          language,\n          lineHeight: 1.0,\n        ),\n      ),\n    );\n  }\n}\n\nclass LanguageList extends StatelessWidget {\n  const LanguageList({\n    super.key,\n    required this.onSelected,\n    required this.selectedLanguage,\n  });\n\n  final Function(TranslateLanguagePB) onSelected;\n  final TranslateLanguagePB selectedLanguage;\n\n  @override\n  Widget build(BuildContext context) {\n    final cells = TranslateLanguagePB.values.map((languageType) {\n      return LanguageCell(\n        languageType: languageType,\n        onSelected: onSelected,\n        isSelected: languageType == selectedLanguage,\n      );\n    }).toList();\n\n    return SizedBox(\n      width: 180,\n      child: ListView.separated(\n        shrinkWrap: true,\n        separatorBuilder: (context, index) {\n          return VSpace(GridSize.typeOptionSeparatorHeight);\n        },\n        itemCount: cells.length,\n        itemBuilder: (BuildContext context, int index) {\n          return cells[index];\n        },\n      ),\n    );\n  }\n}\n\nclass LanguageCell extends StatelessWidget {\n  const LanguageCell({\n    required this.languageType,\n    required this.onSelected,\n    required this.isSelected,\n    super.key,\n  });\n  final Function(TranslateLanguagePB) onSelected;\n  final TranslateLanguagePB languageType;\n  final bool isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    Widget? checkmark;\n    if (isSelected) {\n      checkmark = const FlowySvg(FlowySvgs.check_s);\n    }\n\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        text: FlowyText(\n          languageTypeToLanguage(languageType),\n          lineHeight: 1.0,\n        ),\n        rightIcon: checkmark,\n        onTap: () => onSelected(languageType),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/url.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\nimport 'builder.dart';\n\nclass URLTypeOptionEditorFactory implements TypeOptionEditorFactory {\n  const URLTypeOptionEditorFactory();\n\n  @override\n  Widget? build({\n    required BuildContext context,\n    required String viewId,\n    required FieldPB field,\n    required PopoverMutex popoverMutex,\n    required TypeOptionDataCallback onTypeOptionUpdated,\n  }) =>\n      null;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/setting/group_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';\nimport 'package:appflowy/util/field_type_extension.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:protobuf/protobuf.dart' hide FieldInfo;\n\nclass DatabaseGroupList extends StatelessWidget {\n  const DatabaseGroupList({\n    super.key,\n    required this.viewId,\n    required this.databaseController,\n    required this.onDismissed,\n  });\n\n  final String viewId;\n  final DatabaseController databaseController;\n  final VoidCallback onDismissed;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => DatabaseGroupBloc(\n        viewId: viewId,\n        databaseController: databaseController,\n      )..add(const DatabaseGroupEvent.initial()),\n      child: BlocBuilder<DatabaseGroupBloc, DatabaseGroupState>(\n        builder: (context, state) {\n          final field = state.fieldInfos.firstWhereOrNull(\n            (field) => field.fieldType.canBeGroup && field.isGroupField,\n          );\n          final showHideUngroupedToggle =\n              field?.fieldType != FieldType.Checkbox;\n\n          DateGroupConfigurationPB? config;\n          if (field != null) {\n            final gs = state.groupSettings\n                .firstWhereOrNull((gs) => gs.fieldId == field.id);\n            config = gs != null\n                ? DateGroupConfigurationPB.fromBuffer(gs.content)\n                : null;\n          }\n\n          final children = [\n            if (showHideUngroupedToggle) ...[\n              Padding(\n                padding: const EdgeInsets.symmetric(horizontal: 6),\n                child: SizedBox(\n                  height: GridSize.popoverItemHeight,\n                  child: FlowyButton(\n                    resetHoverOnRebuild: false,\n                    text: FlowyText(\n                      LocaleKeys.board_showUngrouped.tr(),\n                      lineHeight: 1.0,\n                    ),\n                    onTap: () {\n                      _updateLayoutSettings(\n                        state.layoutSettings,\n                        !state.layoutSettings.hideUngroupedColumn,\n                      );\n                    },\n                    rightIcon: Toggle(\n                      value: !state.layoutSettings.hideUngroupedColumn,\n                      onChanged: (value) =>\n                          _updateLayoutSettings(state.layoutSettings, !value),\n                      padding: EdgeInsets.zero,\n                    ),\n                  ),\n                ),\n              ),\n              const TypeOptionSeparator(spacing: 0),\n            ],\n            SizedBox(\n              height: GridSize.popoverItemHeight,\n              child: Padding(\n                padding:\n                    const EdgeInsets.symmetric(horizontal: 12, vertical: 4),\n                child: FlowyText(\n                  LocaleKeys.board_groupBy.tr(),\n                  textAlign: TextAlign.left,\n                  color: Theme.of(context).hintColor,\n                ),\n              ),\n            ),\n            ...state.fieldInfos\n                .where((fieldInfo) => fieldInfo.fieldType.canBeGroup)\n                .map(\n                  (fieldInfo) => _GridGroupCell(\n                    fieldInfo: fieldInfo,\n                    name: fieldInfo.name,\n                    checked: fieldInfo.isGroupField,\n                    onSelected: onDismissed,\n                    key: ValueKey(fieldInfo.id),\n                  ),\n                ),\n            if (field?.fieldType.groupConditions.isNotEmpty ?? false) ...[\n              const TypeOptionSeparator(spacing: 0),\n              SizedBox(\n                height: GridSize.popoverItemHeight,\n                child: Padding(\n                  padding:\n                      const EdgeInsets.symmetric(horizontal: 12, vertical: 4),\n                  child: FlowyText(\n                    LocaleKeys.board_groupCondition.tr(),\n                    textAlign: TextAlign.left,\n                    color: Theme.of(context).hintColor,\n                  ),\n                ),\n              ),\n              ...field!.fieldType.groupConditions.map(\n                (condition) => _GridGroupCell(\n                  fieldInfo: field,\n                  name: condition.name,\n                  condition: condition.value,\n                  onSelected: onDismissed,\n                  checked: config?.condition == condition,\n                ),\n              ),\n            ],\n          ];\n\n          return ListView.separated(\n            shrinkWrap: true,\n            itemCount: children.length,\n            itemBuilder: (BuildContext context, int index) => children[index],\n            separatorBuilder: (BuildContext context, int index) =>\n                VSpace(GridSize.typeOptionSeparatorHeight),\n            padding: const EdgeInsets.symmetric(vertical: 6.0),\n          );\n        },\n      ),\n    );\n  }\n\n  Future<void> _updateLayoutSettings(\n    BoardLayoutSettingPB layoutSettings,\n    bool hideUngrouped,\n  ) {\n    layoutSettings.freeze();\n    final newLayoutSetting = layoutSettings.rebuild((message) {\n      message.hideUngroupedColumn = hideUngrouped;\n    });\n    return databaseController.updateLayoutSetting(\n      boardLayoutSetting: newLayoutSetting,\n    );\n  }\n}\n\nclass _GridGroupCell extends StatelessWidget {\n  const _GridGroupCell({\n    super.key,\n    required this.fieldInfo,\n    required this.onSelected,\n    required this.checked,\n    required this.name,\n    this.condition = 0,\n  });\n\n  final FieldInfo fieldInfo;\n  final VoidCallback onSelected;\n  final bool checked;\n  final int condition;\n  final String name;\n\n  @override\n  Widget build(BuildContext context) {\n    Widget? rightIcon;\n    if (checked) {\n      rightIcon = const Padding(\n        padding: EdgeInsets.all(2.0),\n        child: FlowySvg(FlowySvgs.check_s),\n      );\n    }\n\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 6.0),\n        child: FlowyButton(\n          hoverColor: AFThemeExtension.of(context).lightGreyHover,\n          text: FlowyText(\n            name,\n            color: AFThemeExtension.of(context).textColor,\n            lineHeight: 1.0,\n          ),\n          leftIcon: FieldIcon(fieldInfo: fieldInfo),\n          rightIcon: rightIcon,\n          onTap: () {\n            List<int> settingContent = [];\n            switch (fieldInfo.fieldType) {\n              case FieldType.DateTime:\n                final config = DateGroupConfigurationPB()\n                  ..condition = DateConditionPB.values[condition];\n                settingContent = config.writeToBuffer();\n                break;\n              default:\n            }\n            context.read<DatabaseGroupBloc>().add(\n                  DatabaseGroupEvent.setGroupByField(\n                    fieldInfo.id,\n                    fieldInfo.fieldType,\n                    settingContent,\n                  ),\n                );\n            onSelected();\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/media_file_type_ext.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';\n\nextension FileTypeDisplay on MediaFileTypePB {\n  FlowySvgData get icon => switch (this) {\n        MediaFileTypePB.Image => FlowySvgs.image_s,\n        MediaFileTypePB.Link => FlowySvgs.ft_link_s,\n        MediaFileTypePB.Document => FlowySvgs.icon_document_s,\n        MediaFileTypePB.Archive => FlowySvgs.ft_archive_s,\n        MediaFileTypePB.Video => FlowySvgs.ft_video_s,\n        MediaFileTypePB.Audio => FlowySvgs.ft_audio_s,\n        MediaFileTypePB.Text => FlowySvgs.ft_text_s,\n        _ => FlowySvgs.icon_document_s,\n      };\n\n  Color get color => switch (this) {\n        MediaFileTypePB.Image => const Color(0xFF5465A1),\n        MediaFileTypePB.Link => const Color(0xFFEBE4FF),\n        MediaFileTypePB.Audio => const Color(0xFFE4FFDE),\n        MediaFileTypePB.Video => const Color(0xFFE0F8FF),\n        MediaFileTypePB.Archive => const Color(0xFFFFE7EE),\n        MediaFileTypePB.Text ||\n        MediaFileTypePB.Document ||\n        MediaFileTypePB.Other =>\n          const Color(0xFFF5FFDC),\n        _ => const Color(0xFF87B3A8),\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_accessory.dart",
    "content": "import 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:styled_widget/styled_widget.dart';\n\nimport '../../cell/editable_cell_builder.dart';\n\nclass GridCellAccessoryBuildContext {\n  GridCellAccessoryBuildContext({\n    required this.anchorContext,\n    required this.isCellEditing,\n  });\n\n  final BuildContext anchorContext;\n  final bool isCellEditing;\n}\n\nclass GridCellAccessoryBuilder<T extends State<StatefulWidget>> {\n  GridCellAccessoryBuilder({required Widget Function(Key key) builder})\n      : _builder = builder;\n\n  final GlobalKey<T> _key = GlobalKey();\n\n  final Widget Function(Key key) _builder;\n\n  Widget build() => _builder(_key);\n\n  void onTap() {\n    (_key.currentState as GridCellAccessoryState).onTap();\n  }\n\n  bool enable() {\n    if (_key.currentState == null) {\n      return true;\n    }\n    return (_key.currentState as GridCellAccessoryState).enable();\n  }\n}\n\nabstract mixin class GridCellAccessoryState {\n  void onTap();\n\n  // The accessory will be hidden if enable() return false;\n  bool enable() => true;\n}\n\nclass PrimaryCellAccessory extends StatefulWidget {\n  const PrimaryCellAccessory({\n    super.key,\n    required this.onTap,\n    required this.isCellEditing,\n  });\n\n  final VoidCallback onTap;\n  final bool isCellEditing;\n\n  @override\n  State<StatefulWidget> createState() => _PrimaryCellAccessoryState();\n}\n\nclass _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>\n    with GridCellAccessoryState {\n  @override\n  Widget build(BuildContext context) {\n    return FlowyHover(\n      style: HoverStyle(\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        backgroundColor: Theme.of(context).cardColor,\n      ),\n      builder: (_, onHover) {\n        return FlowyTooltip(\n          message: LocaleKeys.tooltip_openAsPage.tr(),\n          child: Container(\n            width: 26,\n            height: 26,\n            decoration: BoxDecoration(\n              border: Border.fromBorderSide(\n                BorderSide(color: Theme.of(context).dividerColor),\n              ),\n              borderRadius: Corners.s6Border,\n            ),\n            child: Center(\n              child: FlowySvg(\n                FlowySvgs.full_view_s,\n                color: Theme.of(context).colorScheme.primary,\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  @override\n  void onTap() => widget.onTap();\n\n  @override\n  bool enable() => !widget.isCellEditing;\n}\n\nclass AccessoryHover extends StatefulWidget {\n  const AccessoryHover({\n    super.key,\n    required this.child,\n    required this.fieldType,\n  });\n\n  final CellAccessory child;\n  final FieldType fieldType;\n\n  @override\n  State<AccessoryHover> createState() => _AccessoryHoverState();\n}\n\nclass _AccessoryHoverState extends State<AccessoryHover> {\n  bool _isHover = false;\n\n  @override\n  Widget build(BuildContext context) {\n    // Some FieldType has built-in handling for more gestures\n    // and granular control, so we don't need to show the accessory.\n    if (!widget.fieldType.showRowDetailAccessory) {\n      return widget.child;\n    }\n\n    final List<Widget> children = [\n      DecoratedBox(\n        decoration: BoxDecoration(\n          color: _isHover && widget.fieldType != FieldType.Checklist\n              ? AFThemeExtension.of(context).lightGreyHover\n              : Colors.transparent,\n          borderRadius: Corners.s6Border,\n        ),\n        child: widget.child,\n      ),\n    ];\n\n    final accessoryBuilder = widget.child.accessoryBuilder;\n    if (accessoryBuilder != null && _isHover) {\n      final accessories = accessoryBuilder(\n        GridCellAccessoryBuildContext(\n          anchorContext: context,\n          isCellEditing: false,\n        ),\n      );\n      children.add(\n        Padding(\n          padding: const EdgeInsets.only(right: 6),\n          child: CellAccessoryContainer(accessories: accessories),\n        ).positioned(right: 0),\n      );\n    }\n\n    return MouseRegion(\n      cursor: SystemMouseCursors.click,\n      opaque: false,\n      onEnter: (p) => setState(() => _isHover = true),\n      onExit: (p) => setState(() => _isHover = false),\n      child: Stack(\n        alignment: AlignmentDirectional.center,\n        children: children,\n      ),\n    );\n  }\n}\n\nclass CellAccessoryContainer extends StatelessWidget {\n  const CellAccessoryContainer({required this.accessories, super.key});\n\n  final List<GridCellAccessoryBuilder> accessories;\n\n  @override\n  Widget build(BuildContext context) {\n    final children =\n        accessories.where((accessory) => accessory.enable()).map((accessory) {\n      return GestureDetector(\n        behavior: HitTestBehavior.opaque,\n        onTap: () => accessory.onTap(),\n        child: accessory.build(),\n      );\n    }).toList();\n\n    return SeparatedRow(\n      separatorBuilder: () => const HSpace(6),\n      children: children,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_shortcuts.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\ntypedef CellKeyboardAction = dynamic Function();\n\nenum CellKeyboardKey {\n  onEnter,\n  onCopy,\n  onInsert,\n}\n\nabstract class CellShortcuts extends Widget {\n  const CellShortcuts({super.key});\n\n  Map<CellKeyboardKey, CellKeyboardAction> get shortcutHandlers;\n}\n\nclass GridCellShortcuts extends StatelessWidget {\n  const GridCellShortcuts({required this.child, super.key});\n\n  final CellShortcuts child;\n\n  @override\n  Widget build(BuildContext context) {\n    return Shortcuts(\n      shortcuts: shortcuts,\n      child: Actions(\n        actions: actions,\n        child: child,\n      ),\n    );\n  }\n\n  Map<ShortcutActivator, Intent> get shortcuts => {\n        if (shouldAddKeyboardKey(CellKeyboardKey.onEnter))\n          LogicalKeySet(LogicalKeyboardKey.enter): const GridCellEnterIdent(),\n        if (shouldAddKeyboardKey(CellKeyboardKey.onCopy))\n          LogicalKeySet(\n            Platform.isMacOS\n                ? LogicalKeyboardKey.meta\n                : LogicalKeyboardKey.control,\n            LogicalKeyboardKey.keyC,\n          ): const GridCellCopyIntent(),\n      };\n\n  Map<Type, Action<Intent>> get actions => {\n        if (shouldAddKeyboardKey(CellKeyboardKey.onEnter))\n          GridCellEnterIdent: GridCellEnterAction(child: child),\n        if (shouldAddKeyboardKey(CellKeyboardKey.onCopy))\n          GridCellCopyIntent: GridCellCopyAction(child: child),\n      };\n\n  bool shouldAddKeyboardKey(CellKeyboardKey key) =>\n      child.shortcutHandlers.containsKey(key);\n}\n\nclass GridCellEnterIdent extends Intent {\n  const GridCellEnterIdent();\n}\n\nclass GridCellEnterAction extends Action<GridCellEnterIdent> {\n  GridCellEnterAction({required this.child});\n\n  final CellShortcuts child;\n\n  @override\n  void invoke(covariant GridCellEnterIdent intent) {\n    final callback = child.shortcutHandlers[CellKeyboardKey.onEnter];\n    if (callback != null) {\n      callback();\n    }\n  }\n}\n\nclass GridCellCopyIntent extends Intent {\n  const GridCellCopyIntent();\n}\n\nclass GridCellCopyAction extends Action<GridCellCopyIntent> {\n  GridCellCopyAction({required this.child});\n\n  final CellShortcuts child;\n\n  @override\n  void invoke(covariant GridCellCopyIntent intent) {\n    final callback = child.shortcutHandlers[CellKeyboardKey.onCopy];\n    if (callback == null) {\n      return;\n    }\n\n    final s = callback();\n    if (s is String) {\n      Clipboard.setData(ClipboardData(text: s));\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/cell_container.dart",
    "content": "import 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\nimport 'package:styled_widget/styled_widget.dart';\n\nimport '../../../grid/presentation/layout/sizes.dart';\nimport '../../../grid/presentation/widgets/row/row.dart';\nimport '../../cell/editable_cell_builder.dart';\nimport '../accessory/cell_accessory.dart';\nimport '../accessory/cell_shortcuts.dart';\n\nclass CellContainer extends StatelessWidget {\n  const CellContainer({\n    super.key,\n    required this.child,\n    required this.width,\n    required this.isPrimary,\n    this.accessoryBuilder,\n  });\n\n  final EditableCellWidget child;\n  final AccessoryBuilder? accessoryBuilder;\n  final double width;\n  final bool isPrimary;\n\n  @override\n  Widget build(BuildContext context) {\n    return ChangeNotifierProvider.value(\n      value: child.cellContainerNotifier,\n      child: Selector<CellContainerNotifier, bool>(\n        selector: (context, notifier) => notifier.isFocus,\n        builder: (providerContext, isFocus, _) {\n          Widget container = Center(child: GridCellShortcuts(child: child));\n\n          if (accessoryBuilder != null) {\n            final accessories = accessoryBuilder!.call(\n              GridCellAccessoryBuildContext(\n                anchorContext: context,\n                isCellEditing: isFocus,\n              ),\n            );\n\n            if (accessories.isNotEmpty) {\n              container = _GridCellEnterRegion(\n                accessories: accessories,\n                isPrimary: isPrimary,\n                child: container,\n              );\n            }\n          }\n\n          return GestureDetector(\n            behavior: HitTestBehavior.opaque,\n            onTap: () {\n              if (!isFocus) {\n                child.requestFocus.notify();\n              }\n            },\n            child: Container(\n              constraints: BoxConstraints(maxWidth: width, minHeight: 32),\n              decoration: _makeBoxDecoration(context, isFocus),\n              child: container,\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) {\n    if (isFocus) {\n      final borderSide = BorderSide(\n        color: Theme.of(context).colorScheme.primary,\n      );\n\n      return BoxDecoration(border: Border.fromBorderSide(borderSide));\n    }\n\n    final borderSide =\n        BorderSide(color: AFThemeExtension.of(context).borderColor);\n    return BoxDecoration(\n      border: Border(right: borderSide, bottom: borderSide),\n    );\n  }\n}\n\nclass _GridCellEnterRegion extends StatelessWidget {\n  const _GridCellEnterRegion({\n    required this.child,\n    required this.accessories,\n    required this.isPrimary,\n  });\n\n  final Widget child;\n  final List<GridCellAccessoryBuilder> accessories;\n  final bool isPrimary;\n\n  @override\n  Widget build(BuildContext context) {\n    return Selector2<RegionStateNotifier, CellContainerNotifier, bool>(\n      selector: (context, regionNotifier, cellNotifier) =>\n          !cellNotifier.isFocus &&\n          (cellNotifier.isHover || regionNotifier.onEnter && isPrimary),\n      builder: (context, showAccessory, _) {\n        final List<Widget> children = [child];\n\n        if (showAccessory) {\n          children.add(\n            CellAccessoryContainer(accessories: accessories).positioned(\n              right: GridSize.cellContentInsets.right,\n            ),\n          );\n        }\n\n        return MouseRegion(\n          cursor: SystemMouseCursors.click,\n          onEnter: (p) =>\n              CellContainerNotifier.of(context, listen: false).isHover = true,\n          onExit: (p) =>\n              CellContainerNotifier.of(context, listen: false).isHover = false,\n          child: Stack(\n            alignment: Alignment.center,\n            fit: StackFit.expand,\n            children: children,\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass CellContainerNotifier extends ChangeNotifier {\n  bool _isFocus = false;\n  bool _onEnter = false;\n\n  set isFocus(bool value) {\n    if (_isFocus != value) {\n      _isFocus = value;\n      notifyListeners();\n    }\n  }\n\n  set isHover(bool value) {\n    if (_onEnter != value) {\n      _onEnter = value;\n      notifyListeners();\n    }\n  }\n\n  bool get isFocus => _isFocus;\n\n  bool get isHover => _onEnter;\n\n  static CellContainerNotifier of(BuildContext context, {bool listen = true}) {\n    return Provider.of<CellContainerNotifier>(context, listen: listen);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/mobile_cell_container.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nimport '../../cell/editable_cell_builder.dart';\nimport 'cell_container.dart';\n\nclass MobileCellContainer extends StatelessWidget {\n  const MobileCellContainer({\n    super.key,\n    required this.child,\n    required this.isPrimary,\n    this.onPrimaryFieldCellTap,\n  });\n\n  final EditableCellWidget child;\n  final bool isPrimary;\n  final VoidCallback? onPrimaryFieldCellTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return ChangeNotifierProvider.value(\n      value: child.cellContainerNotifier,\n      child: Selector<CellContainerNotifier, bool>(\n        selector: (context, notifier) => notifier.isFocus,\n        builder: (providerContext, isFocus, _) {\n          Widget container = Center(child: child);\n\n          if (isPrimary) {\n            container = IgnorePointer(child: container);\n          }\n\n          return GestureDetector(\n            behavior: HitTestBehavior.opaque,\n            onTap: () {\n              if (isPrimary) {\n                onPrimaryFieldCellTap?.call();\n                return;\n              }\n              if (!isFocus) {\n                child.requestFocus.notify();\n              }\n            },\n            child: Container(\n              constraints: const BoxConstraints(maxWidth: 200, minHeight: 46),\n              decoration: _makeBoxDecoration(context, isPrimary, isFocus),\n              child: container,\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  BoxDecoration _makeBoxDecoration(\n    BuildContext context,\n    bool isPrimary,\n    bool isFocus,\n  ) {\n    if (isFocus) {\n      return BoxDecoration(\n        border: Border.fromBorderSide(\n          BorderSide(\n            color: Theme.of(context).colorScheme.primary,\n          ),\n        ),\n      );\n    }\n\n    final borderSide = BorderSide(color: Theme.of(context).dividerColor);\n    return BoxDecoration(\n      border: Border(\n        left: isPrimary ? borderSide : BorderSide.none,\n        right: borderSide,\n        bottom: borderSide,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/plugins/database/application/row/related_row_detail_bloc.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'row_detail.dart';\n\nclass RelatedRowDetailPage extends StatelessWidget {\n  const RelatedRowDetailPage({\n    super.key,\n    required this.databaseId,\n    required this.rowId,\n  });\n\n  final String databaseId;\n  final String rowId;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) => RelatedRowDetailPageBloc(\n        databaseId: databaseId,\n        initialRowId: rowId,\n      ),\n      child: BlocBuilder<RelatedRowDetailPageBloc, RelatedRowDetailPageState>(\n        builder: (_, state) {\n          return state.when(\n            loading: () => const SizedBox.shrink(),\n            ready: (databaseController, rowController) {\n              return BlocProvider.value(\n                value: context.read<UserWorkspaceBloc>(),\n                child: RowDetailPage(\n                  databaseController: databaseController,\n                  rowController: rowController,\n                  allowOpenAsFullPage: false,\n                ),\n              );\n            },\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_action.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/row/row_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass RowActionList extends StatelessWidget {\n  const RowActionList({super.key, required this.rowController});\n\n  final RowController rowController;\n\n  @override\n  Widget build(BuildContext context) {\n    return IntrinsicWidth(\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          RowDetailPageDuplicateButton(\n            viewId: rowController.viewId,\n            rowId: rowController.rowId,\n          ),\n          const VSpace(4.0),\n          RowDetailPageDeleteButton(\n            viewId: rowController.viewId,\n            rowId: rowController.rowId,\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass RowDetailPageDeleteButton extends StatelessWidget {\n  const RowDetailPageDeleteButton({\n    super.key,\n    required this.viewId,\n    required this.rowId,\n  });\n\n  final String viewId;\n  final String rowId;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        text: FlowyText.regular(\n          LocaleKeys.grid_row_delete.tr(),\n          lineHeight: 1.0,\n        ),\n        leftIcon: const FlowySvg(FlowySvgs.trash_m),\n        onTap: () {\n          RowBackendService.deleteRows(viewId, [rowId]);\n          FlowyOverlay.pop(context);\n        },\n      ),\n    );\n  }\n}\n\nclass RowDetailPageDuplicateButton extends StatelessWidget {\n  const RowDetailPageDuplicateButton({\n    super.key,\n    required this.viewId,\n    required this.rowId,\n  });\n\n  final String viewId;\n  final String rowId;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: GridSize.popoverItemHeight,\n      child: FlowyButton(\n        text: FlowyText.regular(\n          LocaleKeys.grid_row_duplicate.tr(),\n          lineHeight: 1.0,\n        ),\n        leftIcon: const FlowySvg(FlowySvgs.copy_s),\n        onTap: () {\n          RowBackendService.duplicateRow(viewId, rowId);\n          FlowyOverlay.pop(context);\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_banner_bloc.dart';\nimport 'package:appflowy/plugins/database/application/row/row_controller.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_action.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';\nimport 'package:appflowy/plugins/shared/cover_type_ext.dart';\nimport 'package:appflowy/shared/af_image.dart';\nimport 'package:appflowy/shared/flowy_gradient_colors.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu;\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/widget/rounded_button.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:string_validator/string_validator.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../../../../shared/icon_emoji_picker/tab.dart';\nimport '../../../document/presentation/editor_plugins/plugins.dart';\n\n/// We have the cover height as public as it is used in the row_detail.dart file\n/// Used to determine the position of the row actions depending on if there is a cover or not.\n///\nconst rowCoverHeight = 250.0;\n\nconst _iconHeight = 60.0;\nconst _toolbarHeight = 40.0;\n\nclass RowBanner extends StatefulWidget {\n  const RowBanner({\n    super.key,\n    required this.databaseController,\n    required this.rowController,\n    required this.cellBuilder,\n    this.allowOpenAsFullPage = true,\n    this.userProfile,\n  });\n\n  final DatabaseController databaseController;\n  final RowController rowController;\n  final EditableCellBuilder cellBuilder;\n  final bool allowOpenAsFullPage;\n  final UserProfilePB? userProfile;\n\n  @override\n  State<RowBanner> createState() => _RowBannerState();\n}\n\nclass _RowBannerState extends State<RowBanner> {\n  final _isHovering = ValueNotifier(false);\n  late final isLocalMode =\n      (widget.userProfile?.workspaceType ?? WorkspaceTypePB.LocalW) ==\n          WorkspaceTypePB.LocalW;\n\n  @override\n  void dispose() {\n    _isHovering.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<RowBannerBloc>(\n      create: (context) => RowBannerBloc(\n        viewId: widget.rowController.viewId,\n        fieldController: widget.databaseController.fieldController,\n        rowMeta: widget.rowController.rowMeta,\n      )..add(const RowBannerEvent.initial()),\n      child: BlocBuilder<RowBannerBloc, RowBannerState>(\n        builder: (context, state) {\n          final hasCover = state.rowMeta.cover.data.isNotEmpty;\n          final hasIcon = state.rowMeta.icon.isNotEmpty;\n\n          return Column(\n            children: [\n              LayoutBuilder(\n                builder: (context, constraints) {\n                  return Stack(\n                    children: [\n                      SizedBox(\n                        height: _calculateOverallHeight(hasIcon, hasCover),\n                        width: constraints.maxWidth,\n                        child: RowHeaderToolbar(\n                          offset: GridSize.horizontalHeaderPadding + 20,\n                          hasIcon: hasIcon,\n                          hasCover: hasCover,\n                          onIconChanged: (icon) {\n                            if (icon != null) {\n                              context\n                                  .read<RowBannerBloc>()\n                                  .add(RowBannerEvent.setIcon(icon));\n                            }\n                          },\n                          onCoverChanged: (cover) {\n                            if (cover != null) {\n                              context\n                                  .read<RowBannerBloc>()\n                                  .add(RowBannerEvent.setCover(cover));\n                            }\n                          },\n                        ),\n                      ),\n                      if (hasCover)\n                        RowCover(\n                          rowId: widget.rowController.rowId,\n                          cover: state.rowMeta.cover,\n                          userProfile: widget.userProfile,\n                          onCoverChanged: (type, details, uploadType) {\n                            if (details != null) {\n                              context.read<RowBannerBloc>().add(\n                                    RowBannerEvent.setCover(\n                                      RowCoverPB(\n                                        data: details,\n                                        uploadType: uploadType,\n                                        coverType: type.into(),\n                                      ),\n                                    ),\n                                  );\n                            } else {\n                              context\n                                  .read<RowBannerBloc>()\n                                  .add(const RowBannerEvent.removeCover());\n                            }\n                          },\n                          isLocalMode: isLocalMode,\n                        ),\n                      if (hasIcon)\n                        Positioned(\n                          left: GridSize.horizontalHeaderPadding + 20,\n                          bottom: hasCover\n                              ? _toolbarHeight - _iconHeight / 2\n                              : _toolbarHeight,\n                          child: RowIcon(\n                            ///TODO: avoid hardcoding for [FlowyIconType]\n                            icon: EmojiIconData(\n                              FlowyIconType.emoji,\n                              state.rowMeta.icon,\n                            ),\n                            onIconChanged: (icon) {\n                              if (icon == null || icon.isEmpty) {\n                                context\n                                    .read<RowBannerBloc>()\n                                    .add(const RowBannerEvent.setIcon(\"\"));\n                              } else {\n                                context\n                                    .read<RowBannerBloc>()\n                                    .add(RowBannerEvent.setIcon(icon));\n                              }\n                            },\n                          ),\n                        ),\n                    ],\n                  );\n                },\n              ),\n              const VSpace(8),\n              _BannerTitle(\n                cellBuilder: widget.cellBuilder,\n                rowController: widget.rowController,\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  double _calculateOverallHeight(bool hasIcon, bool hasCover) {\n    switch ((hasIcon, hasCover)) {\n      case (true, true):\n        return rowCoverHeight + _toolbarHeight;\n      case (true, false):\n        return 50 + _iconHeight + _toolbarHeight;\n      case (false, true):\n        return rowCoverHeight + _toolbarHeight;\n      case (false, false):\n        return _toolbarHeight;\n    }\n  }\n}\n\nclass RowCover extends StatefulWidget {\n  const RowCover({\n    super.key,\n    required this.rowId,\n    required this.cover,\n    this.userProfile,\n    required this.onCoverChanged,\n    this.isLocalMode = true,\n  });\n\n  final String rowId;\n  final RowCoverPB cover;\n  final UserProfilePB? userProfile;\n  final void Function(\n    CoverType type,\n    String? details,\n    FileUploadTypePB? uploadType,\n  ) onCoverChanged;\n  final bool isLocalMode;\n\n  @override\n  State<RowCover> createState() => _RowCoverState();\n}\n\nclass _RowCoverState extends State<RowCover> {\n  final popoverController = PopoverController();\n  bool isOverlayButtonsHidden = true;\n  bool isPopoverOpen = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: rowCoverHeight,\n      child: MouseRegion(\n        onEnter: (_) => setState(() => isOverlayButtonsHidden = false),\n        onExit: (_) => setState(() => isOverlayButtonsHidden = true),\n        child: Stack(\n          children: [\n            SizedBox(\n              width: double.infinity,\n              child: DesktopRowCover(\n                cover: widget.cover,\n                userProfile: widget.userProfile,\n              ),\n            ),\n            if (!isOverlayButtonsHidden || isPopoverOpen)\n              _buildCoverOverlayButtons(context),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildCoverOverlayButtons(BuildContext context) {\n    return Positioned(\n      bottom: 20,\n      right: 50,\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          AppFlowyPopover(\n            controller: popoverController,\n            triggerActions: PopoverTriggerFlags.none,\n            offset: const Offset(0, 8),\n            direction: PopoverDirection.bottomWithCenterAligned,\n            constraints: const BoxConstraints(\n              maxWidth: 540,\n              maxHeight: 360,\n              minHeight: 80,\n            ),\n            margin: EdgeInsets.zero,\n            onClose: () => setState(() => isPopoverOpen = false),\n            child: IntrinsicWidth(\n              child: RoundedTextButton(\n                height: 28.0,\n                onPressed: () => popoverController.show(),\n                hoverColor: Theme.of(context).colorScheme.surface,\n                textColor: Theme.of(context).colorScheme.tertiary,\n                fillColor: Theme.of(context)\n                    .colorScheme\n                    .surface\n                    .withValues(alpha: 0.5),\n                title: LocaleKeys.document_plugins_cover_changeCover.tr(),\n              ),\n            ),\n            popupBuilder: (BuildContext popoverContext) {\n              isPopoverOpen = true;\n\n              return UploadImageMenu(\n                limitMaximumImageSize: !widget.isLocalMode,\n                supportTypes: const [\n                  UploadImageType.color,\n                  UploadImageType.local,\n                  UploadImageType.url,\n                  UploadImageType.unsplash,\n                ],\n                onSelectedAIImage: (_) => throw UnimplementedError(),\n                onSelectedLocalImages: (files) {\n                  popoverController.close();\n                  if (files.isEmpty) {\n                    return;\n                  }\n\n                  final item = files.map((file) => file.path).first;\n                  onCoverChanged(\n                    CoverType.file,\n                    item,\n                    widget.isLocalMode\n                        ? FileUploadTypePB.LocalFile\n                        : FileUploadTypePB.CloudFile,\n                  );\n                },\n                onSelectedNetworkImage: (url) {\n                  popoverController.close();\n                  onCoverChanged(\n                    CoverType.file,\n                    url,\n                    FileUploadTypePB.NetworkFile,\n                  );\n                },\n                onSelectedColor: (color) {\n                  popoverController.close();\n                  onCoverChanged(\n                    CoverType.color,\n                    color,\n                    FileUploadTypePB.LocalFile,\n                  );\n                },\n              );\n            },\n          ),\n          const HSpace(10),\n          DeleteCoverButton(\n            onTap: () => widget.onCoverChanged(CoverType.none, null, null),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Future<void> onCoverChanged(\n    CoverType type,\n    String? details,\n    FileUploadTypePB? uploadType,\n  ) async {\n    if (type == CoverType.file && details != null && !isURL(details)) {\n      if (widget.isLocalMode) {\n        details = await saveImageToLocalStorage(details);\n      } else {\n        // else we should save the image to cloud storage\n        (details, _) = await saveImageToCloudStorage(details, widget.rowId);\n      }\n    }\n    widget.onCoverChanged(type, details, uploadType);\n  }\n}\n\nclass DesktopRowCover extends StatefulWidget {\n  const DesktopRowCover({super.key, required this.cover, this.userProfile});\n\n  final RowCoverPB cover;\n  final UserProfilePB? userProfile;\n\n  @override\n  State<DesktopRowCover> createState() => _DesktopRowCoverState();\n}\n\nclass _DesktopRowCoverState extends State<DesktopRowCover> {\n  RowCoverPB get cover => widget.cover;\n\n  @override\n  Widget build(BuildContext context) {\n    if (cover.coverType == CoverTypePB.FileCover) {\n      return SizedBox(\n        height: rowCoverHeight,\n        width: double.infinity,\n        child: AFImage(\n          url: cover.data,\n          uploadType: cover.uploadType,\n          userProfile: widget.userProfile,\n        ),\n      );\n    }\n\n    if (cover.coverType == CoverTypePB.AssetCover) {\n      return SizedBox(\n        height: rowCoverHeight,\n        width: double.infinity,\n        child: Image.asset(\n          PageStyleCoverImageType.builtInImagePath(cover.data),\n          fit: BoxFit.cover,\n        ),\n      );\n    }\n\n    if (cover.coverType == CoverTypePB.ColorCover) {\n      final color = FlowyTint.fromId(cover.data)?.color(context) ??\n          cover.data.tryToColor();\n      return Container(\n        height: rowCoverHeight,\n        width: double.infinity,\n        color: color,\n      );\n    }\n\n    if (cover.coverType == CoverTypePB.GradientCover) {\n      return Container(\n        height: rowCoverHeight,\n        width: double.infinity,\n        decoration: BoxDecoration(\n          gradient: FlowyGradientColor.fromId(cover.data).linear,\n        ),\n      );\n    }\n\n    return const SizedBox.shrink();\n  }\n}\n\nclass RowHeaderToolbar extends StatefulWidget {\n  const RowHeaderToolbar({\n    super.key,\n    required this.offset,\n    required this.hasIcon,\n    required this.hasCover,\n    required this.onIconChanged,\n    required this.onCoverChanged,\n  });\n\n  final double offset;\n  final bool hasIcon;\n  final bool hasCover;\n\n  /// Returns null if the icon is removed.\n  ///\n  final void Function(String? icon) onIconChanged;\n\n  /// Returns null if the cover is removed.\n  ///\n  final void Function(RowCoverPB? cover) onCoverChanged;\n\n  @override\n  State<RowHeaderToolbar> createState() => _RowHeaderToolbarState();\n}\n\nclass _RowHeaderToolbarState extends State<RowHeaderToolbar> {\n  final popoverController = PopoverController();\n  final bool isDesktop = UniversalPlatform.isDesktopOrWeb;\n\n  bool isHidden = UniversalPlatform.isDesktopOrWeb;\n  bool isPopoverOpen = false;\n\n  @override\n  Widget build(BuildContext context) {\n    if (!isDesktop) {\n      return const SizedBox.shrink();\n    }\n\n    return MouseRegion(\n      opaque: false,\n      onEnter: (_) => setState(() => isHidden = false),\n      onExit: isPopoverOpen ? null : (_) => setState(() => isHidden = true),\n      child: Container(\n        alignment: Alignment.bottomLeft,\n        width: double.infinity,\n        padding: EdgeInsets.symmetric(horizontal: widget.offset),\n        child: SizedBox(\n          height: 28,\n          child: Visibility(\n            visible: !isHidden || isPopoverOpen,\n            child: Row(\n              crossAxisAlignment: CrossAxisAlignment.stretch,\n              children: [\n                if (!widget.hasCover)\n                  FlowyButton(\n                    resetHoverOnRebuild: false,\n                    useIntrinsicWidth: true,\n                    leftIconSize: const Size.square(18),\n                    leftIcon: const FlowySvg(FlowySvgs.add_cover_s),\n                    text: FlowyText.small(\n                      LocaleKeys.document_plugins_cover_addCover.tr(),\n                    ),\n                    onTap: () => widget.onCoverChanged(\n                      RowCoverPB(\n                        data: isDesktop ? '1' : '0xffe8e0ff',\n                        uploadType: FileUploadTypePB.LocalFile,\n                        coverType: isDesktop\n                            ? CoverTypePB.AssetCover\n                            : CoverTypePB.ColorCover,\n                      ),\n                    ),\n                  ),\n                if (!widget.hasIcon)\n                  AppFlowyPopover(\n                    controller: popoverController,\n                    onClose: () => setState(() => isPopoverOpen = false),\n                    offset: const Offset(0, 8),\n                    direction: PopoverDirection.bottomWithCenterAligned,\n                    constraints: BoxConstraints.loose(const Size(360, 380)),\n                    margin: EdgeInsets.zero,\n                    triggerActions: PopoverTriggerFlags.none,\n                    popupBuilder: (_) {\n                      isPopoverOpen = true;\n                      return FlowyIconEmojiPicker(\n                        tabs: const [PickerTabType.emoji],\n                        onSelectedEmoji: (result) {\n                          widget.onIconChanged(result.emoji);\n                          popoverController.close();\n                        },\n                      );\n                    },\n                    child: FlowyButton(\n                      useIntrinsicWidth: true,\n                      leftIconSize: const Size.square(18),\n                      leftIcon: const FlowySvg(FlowySvgs.add_icon_s),\n                      text: FlowyText.small(\n                        widget.hasIcon\n                            ? LocaleKeys.document_plugins_cover_removeIcon.tr()\n                            : LocaleKeys.document_plugins_cover_addIcon.tr(),\n                      ),\n                      onTap: () async {\n                        if (!isDesktop) {\n                          final result = await context.push<EmojiIconData>(\n                            MobileEmojiPickerScreen.routeName,\n                          );\n\n                          if (result != null) {\n                            widget.onIconChanged(result.emoji);\n                          }\n                        } else {\n                          popoverController.show();\n                        }\n                      },\n                    ),\n                  ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass RowIcon extends StatefulWidget {\n  const RowIcon({\n    super.key,\n    required this.icon,\n    required this.onIconChanged,\n  });\n\n  final EmojiIconData icon;\n  final void Function(String?) onIconChanged;\n\n  @override\n  State<RowIcon> createState() => _RowIconState();\n}\n\nclass _RowIconState extends State<RowIcon> {\n  final controller = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    if (widget.icon.isEmpty) {\n      return const SizedBox.shrink();\n    }\n\n    return AppFlowyPopover(\n      controller: controller,\n      offset: const Offset(0, 8),\n      direction: PopoverDirection.bottomWithCenterAligned,\n      constraints: BoxConstraints.loose(const Size(360, 380)),\n      margin: EdgeInsets.zero,\n      popupBuilder: (_) => FlowyIconEmojiPicker(\n        tabs: const [PickerTabType.emoji],\n        onSelectedEmoji: (result) {\n          controller.close();\n          widget.onIconChanged(result.emoji);\n        },\n      ),\n      child: EmojiIconWidget(emoji: widget.icon),\n    );\n  }\n}\n\nclass _BannerTitle extends StatelessWidget {\n  const _BannerTitle({\n    required this.cellBuilder,\n    required this.rowController,\n  });\n\n  final EditableCellBuilder cellBuilder;\n  final RowController rowController;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<RowBannerBloc, RowBannerState>(\n      builder: (context, state) {\n        final children = [\n          if (state.primaryField != null)\n            Expanded(\n              child: cellBuilder.buildCustom(\n                CellContext(\n                  fieldId: state.primaryField!.id,\n                  rowId: rowController.rowId,\n                ),\n                skinMap: EditableCellSkinMap(textSkin: _TitleSkin()),\n              ),\n            ),\n        ];\n\n        return Padding(\n          padding: const EdgeInsets.only(left: 60),\n          child: Row(children: children),\n        );\n      },\n    );\n  }\n}\n\nclass _TitleSkin extends IEditableTextCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TextCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return CallbackShortcuts(\n      bindings: {\n        const SingleActivator(LogicalKeyboardKey.escape): () =>\n            focusNode.unfocus(),\n        const SimpleActivator(LogicalKeyboardKey.enter): () =>\n            focusNode.unfocus(),\n      },\n      child: TextField(\n        controller: textEditingController,\n        focusNode: focusNode,\n        autofocus: true,\n        style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 28),\n        maxLines: null,\n        decoration: InputDecoration(\n          contentPadding: EdgeInsets.zero,\n          border: InputBorder.none,\n          focusedBorder: InputBorder.none,\n          enabledBorder: InputBorder.none,\n          errorBorder: InputBorder.none,\n          disabledBorder: InputBorder.none,\n          hintText: LocaleKeys.grid_row_titlePlaceholder.tr(),\n          isDense: true,\n          isCollapsed: true,\n        ),\n        onEditingComplete: () {\n          bloc.add(TextCellEvent.updateText(textEditingController.text));\n        },\n      ),\n    );\n  }\n}\n\nclass RowActionButton extends StatelessWidget {\n  const RowActionButton({super.key, required this.rowController});\n\n  final RowController rowController;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      direction: PopoverDirection.bottomWithLeftAligned,\n      popupBuilder: (context) => RowActionList(rowController: rowController),\n      child: FlowyTooltip(\n        message: LocaleKeys.grid_rowPage_moreRowActions.tr(),\n        child: FlowyIconButton(\n          width: 20,\n          height: 20,\n          icon: const FlowySvg(FlowySvgs.details_horizontal_s),\n          iconColorOnHover: Theme.of(context).colorScheme.onSurface,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_controller.dart';\nimport 'package:appflowy/plugins/database/domain/database_view_service.dart';\nimport 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_document.dart';\nimport 'package:appflowy/plugins/database_document/database_document_plugin.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\n\nimport '../cell/editable_cell_builder.dart';\nimport 'row_banner.dart';\nimport 'row_property.dart';\n\nclass RowDetailPage extends StatefulWidget with FlowyOverlayDelegate {\n  const RowDetailPage({\n    super.key,\n    required this.rowController,\n    required this.databaseController,\n    this.allowOpenAsFullPage = true,\n    this.userProfile,\n  });\n\n  final RowController rowController;\n  final DatabaseController databaseController;\n  final bool allowOpenAsFullPage;\n  final UserProfilePB? userProfile;\n\n  @override\n  State<RowDetailPage> createState() => _RowDetailPageState();\n}\n\nclass _RowDetailPageState extends State<RowDetailPage> {\n  // To allow blocking drop target in RowDocument from Field dialogs\n  final dropManagerState = EditorDropManagerState();\n\n  late final cellBuilder = EditableCellBuilder(\n    databaseController: widget.databaseController,\n  );\n  late final ScrollController scrollController;\n\n  double scrollOffset = 0;\n\n  @override\n  void initState() {\n    super.initState();\n    scrollController =\n        ScrollController(onAttach: (_) => attachScrollListener());\n  }\n\n  void attachScrollListener() => scrollController.addListener(onScrollChanged);\n\n  @override\n  void dispose() {\n    scrollController.removeListener(onScrollChanged);\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyDialog(\n      child: ChangeNotifierProvider.value(\n        value: dropManagerState,\n        child: MultiBlocProvider(\n          providers: [\n            BlocProvider(\n              create: (_) => RowDetailBloc(\n                fieldController: widget.databaseController.fieldController,\n                rowController: widget.rowController,\n              ),\n            ),\n            BlocProvider.value(value: getIt<ReminderBloc>()),\n          ],\n          child: BlocBuilder<RowDetailBloc, RowDetailState>(\n            builder: (context, state) => Stack(\n              fit: StackFit.expand,\n              children: [\n                Positioned.fill(\n                  child: NestedScrollView(\n                    controller: scrollController,\n                    headerSliverBuilder:\n                        (BuildContext context, bool innerBoxIsScrolled) {\n                      return <Widget>[\n                        SliverToBoxAdapter(\n                          child: Column(\n                            children: [\n                              RowBanner(\n                                databaseController: widget.databaseController,\n                                rowController: widget.rowController,\n                                cellBuilder: cellBuilder,\n                                allowOpenAsFullPage: widget.allowOpenAsFullPage,\n                                userProfile: widget.userProfile,\n                              ),\n                              const VSpace(16),\n                              Padding(\n                                padding:\n                                    const EdgeInsets.only(left: 40, right: 60),\n                                child: RowPropertyList(\n                                  cellBuilder: cellBuilder,\n                                  viewId: widget.databaseController.viewId,\n                                  fieldController:\n                                      widget.databaseController.fieldController,\n                                ),\n                              ),\n                              const VSpace(20),\n                              const Padding(\n                                padding: EdgeInsets.symmetric(horizontal: 60),\n                                child: Divider(height: 1.0),\n                              ),\n                              const VSpace(20),\n                            ],\n                          ),\n                        ),\n                      ];\n                    },\n                    body: RowDocument(\n                      viewId: widget.rowController.viewId,\n                      rowId: widget.rowController.rowId,\n                    ),\n                  ),\n                ),\n                Positioned(\n                  top: calculateActionsOffset(\n                    state.rowMeta.cover.data.isNotEmpty,\n                  ),\n                  right: 12,\n                  child: Row(children: actions(context)),\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  void onScrollChanged() {\n    if (scrollOffset != scrollController.offset) {\n      setState(() => scrollOffset = scrollController.offset);\n    }\n  }\n\n  double calculateActionsOffset(bool hasCover) {\n    if (!hasCover) {\n      return 12;\n    }\n\n    final offsetByScroll = clampDouble(\n      rowCoverHeight - scrollOffset,\n      0,\n      rowCoverHeight,\n    );\n    return 12 + offsetByScroll;\n  }\n\n  List<Widget> actions(BuildContext context) {\n    return [\n      if (widget.allowOpenAsFullPage) ...[\n        FlowyTooltip(\n          message: LocaleKeys.grid_rowPage_openAsFullPage.tr(),\n          child: FlowyIconButton(\n            width: 20,\n            height: 20,\n            icon: const FlowySvg(FlowySvgs.full_view_s),\n            iconColorOnHover: Theme.of(context).colorScheme.onSurface,\n            onPressed: () async {\n              Navigator.of(context).pop();\n              final databaseId = await DatabaseViewBackendService(\n                viewId: widget.databaseController.viewId,\n              )\n                  .getDatabaseId()\n                  .then((value) => value.fold((s) => s, (f) => null));\n              final documentId = widget.rowController.rowMeta.documentId;\n              if (databaseId != null) {\n                getIt<TabsBloc>().add(\n                  TabsEvent.openPlugin(\n                    plugin: DatabaseDocumentPlugin(\n                      data: DatabaseDocumentContext(\n                        view: widget.databaseController.view,\n                        databaseId: databaseId,\n                        rowId: widget.rowController.rowId,\n                        documentId: documentId,\n                      ),\n                      pluginType: PluginType.databaseDocument,\n                    ),\n                    setLatest: false,\n                  ),\n                );\n              }\n            },\n          ),\n        ),\n        const HSpace(4),\n      ],\n      RowActionButton(rowController: widget.rowController),\n    ];\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/grid/application/row/row_document_bloc.dart';\nimport 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_drop_handler.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_page.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/shared/flowy_error_page.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\n\nclass RowDocument extends StatelessWidget {\n  const RowDocument({\n    super.key,\n    required this.viewId,\n    required this.rowId,\n  });\n\n  final String viewId;\n  final String rowId;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<RowDocumentBloc>(\n      create: (context) => RowDocumentBloc(viewId: viewId, rowId: rowId)\n        ..add(const RowDocumentEvent.initial()),\n      child: BlocConsumer<RowDocumentBloc, RowDocumentState>(\n        listener: (_, state) => state.loadingState.maybeWhen(\n          error: (error) => Log.error('RowDocument error: $error'),\n          orElse: () => null,\n        ),\n        builder: (context, state) {\n          return state.loadingState.when(\n            loading: () => const Center(\n              child: CircularProgressIndicator.adaptive(),\n            ),\n            error: (error) => Center(\n              child: AppFlowyErrorPage(\n                error: error,\n              ),\n            ),\n            finish: () => _RowEditor(\n              view: state.viewPB!,\n              onIsEmptyChanged: (isEmpty) => context\n                  .read<RowDocumentBloc>()\n                  .add(RowDocumentEvent.updateIsEmpty(isEmpty)),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _RowEditor extends StatelessWidget {\n  const _RowEditor({\n    required this.view,\n    this.onIsEmptyChanged,\n  });\n\n  final ViewPB view;\n  final void Function(bool)? onIsEmptyChanged;\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider(\n          create: (_) => DocumentBloc(documentId: view.id)\n            ..add(const DocumentEvent.initial()),\n        ),\n        BlocProvider(\n          create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()),\n        ),\n      ],\n      child: BlocConsumer<DocumentBloc, DocumentState>(\n        listenWhen: (previous, current) =>\n            previous.isDocumentEmpty != current.isDocumentEmpty,\n        listener: (_, state) {\n          if (state.isDocumentEmpty != null) {\n            onIsEmptyChanged?.call(state.isDocumentEmpty!);\n          }\n          if (state.error != null) {\n            Log.error('RowEditor error: ${state.error}');\n          }\n          if (state.editorState == null) {\n            Log.error('RowEditor unable to get editorState');\n          }\n        },\n        builder: (context, state) {\n          if (state.isLoading) {\n            return const Center(child: CircularProgressIndicator.adaptive());\n          }\n\n          final editorState = state.editorState;\n          final error = state.error;\n          if (error != null || editorState == null) {\n            return Center(\n              child: AppFlowyErrorPage(error: error),\n            );\n          }\n\n          return BlocProvider<ViewInfoBloc>(\n            create: (context) => ViewInfoBloc(view: view),\n            child: Container(\n              constraints: const BoxConstraints(minHeight: 300),\n              child: Provider(\n                create: (_) {\n                  final context = SharedEditorContext();\n                  context.isInDatabaseRowPage = true;\n                  return context;\n                },\n                dispose: (_, editorContext) => editorContext.dispose(),\n                child: AiWriterScrollWrapper(\n                  viewId: view.id,\n                  editorState: editorState,\n                  child: EditorDropHandler(\n                    viewId: view.id,\n                    editorState: editorState,\n                    isLocalMode: context.read<DocumentBloc>().isLocalMode,\n                    dropManagerState: context.read<EditorDropManagerState>(),\n                    child: EditorTransactionService(\n                      viewId: view.id,\n                      editorState: editorState,\n                      child: Provider(\n                        create: (context) => DatabasePluginWidgetBuilderSize(\n                          horizontalPadding: 0,\n                        ),\n                        child: AppFlowyEditorPage(\n                          shrinkWrap: true,\n                          autoFocus: false,\n                          editorState: editorState,\n                          styleCustomizer: EditorStyleCustomizer(\n                            context: context,\n                            padding: const EdgeInsets.only(left: 16, right: 54),\n                          ),\n                          showParagraphPlaceholder: (editorState, _) =>\n                              editorState.document.isEmpty,\n                          placeholderText: (_) =>\n                              LocaleKeys.cardDetails_notesPlaceholder.tr(),\n                        ),\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/field/field_editor.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../cell/editable_cell_builder.dart';\nimport 'accessory/cell_accessory.dart';\n\n/// Display the row properties in a list. Only used in [RowDetailPage].\nclass RowPropertyList extends StatelessWidget {\n  const RowPropertyList({\n    super.key,\n    required this.viewId,\n    required this.fieldController,\n    required this.cellBuilder,\n  });\n\n  final String viewId;\n  final FieldController fieldController;\n  final EditableCellBuilder cellBuilder;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<RowDetailBloc, RowDetailState>(\n      buildWhen: (previous, current) =>\n          previous.showHiddenFields != current.showHiddenFields ||\n          !listEquals(previous.visibleCells, current.visibleCells),\n      builder: (context, state) {\n        final children = state.visibleCells\n            .mapIndexed(\n              (index, cell) => _PropertyCell(\n                key: ValueKey('row_detail_${cell.fieldId}'),\n                cellContext: cell,\n                cellBuilder: cellBuilder,\n                fieldController: fieldController,\n                index: index,\n              ),\n            )\n            .toList();\n\n        return ReorderableListView(\n          shrinkWrap: true,\n          physics: const NeverScrollableScrollPhysics(),\n          onReorder: (from, to) => context\n              .read<RowDetailBloc>()\n              .add(RowDetailEvent.reorderField(from, to)),\n          buildDefaultDragHandles: false,\n          proxyDecorator: (child, index, animation) => Material(\n            color: Colors.transparent,\n            child: Stack(\n              children: [\n                BlocProvider.value(\n                  value: context.read<RowDetailBloc>(),\n                  child: child,\n                ),\n                MouseRegion(\n                  cursor: Platform.isWindows\n                      ? SystemMouseCursors.click\n                      : SystemMouseCursors.grabbing,\n                  child: const SizedBox(\n                    width: 16,\n                    height: 30,\n                    child: FlowySvg(FlowySvgs.drag_element_s),\n                  ),\n                ),\n              ],\n            ),\n          ),\n          footer: Padding(\n            padding: const EdgeInsets.only(left: 20),\n            child: Column(\n              children: [\n                if (context.watch<RowDetailBloc>().state.numHiddenFields != 0)\n                  const Padding(\n                    padding: EdgeInsets.only(bottom: 4.0),\n                    child: ToggleHiddenFieldsVisibilityButton(),\n                  ),\n                CreateRowFieldButton(\n                  viewId: viewId,\n                  fieldController: fieldController,\n                ),\n              ],\n            ),\n          ),\n          children: children,\n        );\n      },\n    );\n  }\n}\n\nclass _PropertyCell extends StatefulWidget {\n  const _PropertyCell({\n    super.key,\n    required this.cellContext,\n    required this.cellBuilder,\n    required this.fieldController,\n    required this.index,\n  });\n\n  final CellContext cellContext;\n  final EditableCellBuilder cellBuilder;\n  final FieldController fieldController;\n  final int index;\n\n  @override\n  State<StatefulWidget> createState() => _PropertyCellState();\n}\n\nclass _PropertyCellState extends State<_PropertyCell> {\n  final PopoverController _popoverController = PopoverController();\n\n  final ValueNotifier<bool> _isFieldHover = ValueNotifier(false);\n\n  @override\n  Widget build(BuildContext context) {\n    final cell = widget.cellBuilder.buildStyled(\n      widget.cellContext,\n      EditableCellStyle.desktopRowDetail,\n    );\n    final gesture = GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: () => cell.requestFocus.notify(),\n      child: AccessoryHover(\n        fieldType: widget.fieldController\n            .getField(widget.cellContext.fieldId)!\n            .fieldType,\n        child: cell,\n      ),\n    );\n\n    return Container(\n      margin: const EdgeInsets.only(bottom: 8),\n      constraints: const BoxConstraints(minHeight: 30),\n      child: MouseRegion(\n        onEnter: (event) {\n          _isFieldHover.value = true;\n          cell.cellContainerNotifier.isHover = true;\n        },\n        onExit: (event) {\n          _isFieldHover.value = false;\n          cell.cellContainerNotifier.isHover = false;\n        },\n        child: Row(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            ValueListenableBuilder(\n              valueListenable: _isFieldHover,\n              builder: (context, value, _) {\n                return ReorderableDragStartListener(\n                  index: widget.index,\n                  enabled: value,\n                  child: _buildDragHandle(context),\n                );\n              },\n            ),\n            const HSpace(4),\n            _buildFieldButton(context),\n            const HSpace(8),\n            Expanded(child: gesture),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildDragHandle(BuildContext context) {\n    return MouseRegion(\n      cursor: Platform.isWindows\n          ? SystemMouseCursors.click\n          : SystemMouseCursors.grab,\n      child: SizedBox(\n        width: 16,\n        height: 30,\n        child: BlocListener<RowDetailBloc, RowDetailState>(\n          listenWhen: (previous, current) =>\n              previous.editingFieldId != current.editingFieldId,\n          listener: (context, state) {\n            if (state.editingFieldId == widget.cellContext.fieldId) {\n              WidgetsBinding.instance.addPostFrameCallback((_) {\n                _popoverController.show();\n              });\n            }\n          },\n          child: ValueListenableBuilder(\n            valueListenable: _isFieldHover,\n            builder: (_, isHovering, child) =>\n                isHovering ? child! : const SizedBox.shrink(),\n            child: BlockActionButton(\n              onTap: () => context.read<RowDetailBloc>().add(\n                    RowDetailEvent.startEditingField(\n                      widget.cellContext.fieldId,\n                    ),\n                  ),\n              svg: FlowySvgs.drag_element_s,\n              richMessage: TextSpan(\n                text: LocaleKeys.grid_rowPage_fieldDragElementTooltip.tr(),\n                style: context.tooltipTextStyle(),\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildFieldButton(BuildContext context) {\n    return BlocSelector<RowDetailBloc, RowDetailState, FieldInfo?>(\n      selector: (state) => state.fields.firstWhereOrNull(\n        (fieldInfo) => fieldInfo.field.id == widget.cellContext.fieldId,\n      ),\n      builder: (context, fieldInfo) {\n        if (fieldInfo == null) {\n          return const SizedBox.shrink();\n        }\n        return AppFlowyPopover(\n          controller: _popoverController,\n          constraints: BoxConstraints.loose(const Size(240, 600)),\n          margin: EdgeInsets.zero,\n          triggerActions: PopoverTriggerFlags.none,\n          direction: PopoverDirection.bottomWithLeftAligned,\n          onClose: () => context\n              .read<RowDetailBloc>()\n              .add(const RowDetailEvent.endEditingField()),\n          popupBuilder: (popoverContext) => FieldEditor(\n            viewId: widget.fieldController.viewId,\n            fieldInfo: fieldInfo,\n            fieldController: widget.fieldController,\n            isNewField: context.watch<RowDetailBloc>().state.newFieldId ==\n                widget.cellContext.fieldId,\n          ),\n          child: SizedBox(\n            width: 160,\n            height: 30,\n            child: Tooltip(\n              waitDuration: const Duration(seconds: 1),\n              preferBelow: false,\n              verticalOffset: 15,\n              message: fieldInfo.name,\n              child: FieldCellButton(\n                field: fieldInfo.field,\n                onTap: () => context.read<RowDetailBloc>().add(\n                      RowDetailEvent.startEditingField(\n                        widget.cellContext.fieldId,\n                      ),\n                    ),\n                radius: BorderRadius.circular(6),\n                margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass ToggleHiddenFieldsVisibilityButton extends StatelessWidget {\n  const ToggleHiddenFieldsVisibilityButton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<RowDetailBloc, RowDetailState>(\n      buildWhen: (previous, current) =>\n          previous.showHiddenFields != current.showHiddenFields ||\n          previous.numHiddenFields != current.numHiddenFields,\n      builder: (context, state) {\n        final text = state.showHiddenFields\n            ? LocaleKeys.grid_rowPage_hideHiddenFields.plural(\n                state.numHiddenFields,\n                namedArgs: {'count': '${state.numHiddenFields}'},\n              )\n            : LocaleKeys.grid_rowPage_showHiddenFields.plural(\n                state.numHiddenFields,\n                namedArgs: {'count': '${state.numHiddenFields}'},\n              );\n        final quarterTurns = state.showHiddenFields ? 1 : 3;\n        return UniversalPlatform.isDesktopOrWeb\n            ? _desktop(context, text, quarterTurns)\n            : _mobile(context, text, quarterTurns);\n      },\n    );\n  }\n\n  Widget _desktop(BuildContext context, String text, int quarterTurns) {\n    return SizedBox(\n      height: 30,\n      child: FlowyButton(\n        text: FlowyText(\n          text,\n          lineHeight: 1.0,\n          color: Theme.of(context).hintColor,\n        ),\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        leftIcon: RotatedBox(\n          quarterTurns: quarterTurns,\n          child: FlowySvg(\n            FlowySvgs.arrow_left_s,\n            color: Theme.of(context).hintColor,\n          ),\n        ),\n        margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),\n        onTap: () => context.read<RowDetailBloc>().add(\n              const RowDetailEvent.toggleHiddenFieldVisibility(),\n            ),\n      ),\n    );\n  }\n\n  Widget _mobile(BuildContext context, String text, int quarterTurns) {\n    return ConstrainedBox(\n      constraints: const BoxConstraints(minWidth: double.infinity),\n      child: TextButton.icon(\n        style: Theme.of(context).textButtonTheme.style?.copyWith(\n              shape: WidgetStateProperty.all<RoundedRectangleBorder>(\n                RoundedRectangleBorder(\n                  borderRadius: BorderRadius.circular(12.0),\n                ),\n              ),\n              overlayColor: WidgetStateProperty.all<Color>(\n                Theme.of(context).hoverColor,\n              ),\n              alignment: AlignmentDirectional.centerStart,\n              splashFactory: NoSplash.splashFactory,\n              padding: const WidgetStatePropertyAll(\n                EdgeInsets.symmetric(vertical: 14, horizontal: 6),\n              ),\n            ),\n        label: FlowyText(\n          text,\n          fontSize: 15,\n          color: Theme.of(context).hintColor,\n        ),\n        onPressed: () => context\n            .read<RowDetailBloc>()\n            .add(const RowDetailEvent.toggleHiddenFieldVisibility()),\n        icon: RotatedBox(\n          quarterTurns: quarterTurns,\n          child: FlowySvg(\n            FlowySvgs.arrow_left_s,\n            color: Theme.of(context).hintColor,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass CreateRowFieldButton extends StatelessWidget {\n  const CreateRowFieldButton({\n    super.key,\n    required this.viewId,\n    required this.fieldController,\n  });\n\n  final String viewId;\n  final FieldController fieldController;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 30,\n      child: FlowyButton(\n        margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),\n        text: FlowyText(\n          lineHeight: 1.0,\n          LocaleKeys.grid_field_newProperty.tr(),\n          color: Theme.of(context).hintColor,\n        ),\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        onTap: () async {\n          final result = await FieldBackendService.createField(\n            viewId: viewId,\n          );\n          await Future.delayed(const Duration(milliseconds: 50));\n          result.fold(\n            (field) => context\n                .read<RowDetailBloc>()\n                .add(RowDetailEvent.startEditingNewField(field.id)),\n            (err) => Log.error(\"Failed to create field type option: $err\"),\n          );\n        },\n        leftIcon: FlowySvg(\n          FlowySvgs.add_m,\n          color: Theme.of(context).hintColor,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/layout/layout_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass DatabaseLayoutSelector extends StatelessWidget {\n  const DatabaseLayoutSelector({\n    super.key,\n    required this.viewId,\n    required this.databaseController,\n  });\n\n  final String viewId;\n  final DatabaseController databaseController;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => DatabaseLayoutBloc(\n        viewId: viewId,\n        databaseLayout: databaseController.databaseLayout,\n      )..add(const DatabaseLayoutEvent.initial()),\n      child: BlocBuilder<DatabaseLayoutBloc, DatabaseLayoutState>(\n        builder: (context, state) {\n          final cells = DatabaseLayoutPB.values\n              .map(\n                (layout) => DatabaseViewLayoutCell(\n                  databaseLayout: layout,\n                  isSelected: state.databaseLayout == layout,\n                  onTap: (selectedLayout) => context\n                      .read<DatabaseLayoutBloc>()\n                      .add(DatabaseLayoutEvent.updateLayout(selectedLayout)),\n                ),\n              )\n              .toList();\n          return Padding(\n            padding: const EdgeInsets.symmetric(vertical: 6.0),\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                ListView.separated(\n                  shrinkWrap: true,\n                  itemCount: cells.length,\n                  padding: EdgeInsets.zero,\n                  itemBuilder: (_, int index) => cells[index],\n                  separatorBuilder: (_, __) =>\n                      VSpace(GridSize.typeOptionSeparatorHeight),\n                ),\n                Container(\n                  height: 1,\n                  margin: EdgeInsets.fromLTRB(8, 4, 8, 0),\n                  color: AFThemeExtension.of(context).borderColor,\n                ),\n                Padding(\n                  padding: const EdgeInsets.fromLTRB(8, 4, 8, 2),\n                  child: SizedBox(\n                    height: 30,\n                    child: FlowyButton(\n                      resetHoverOnRebuild: false,\n                      text: FlowyText(\n                        LocaleKeys.grid_settings_compactMode.tr(),\n                        lineHeight: 1.0,\n                      ),\n                      onTap: () {\n                        databaseController.setCompactMode(\n                          !databaseController.compactModeNotifier.value,\n                        );\n                      },\n                      rightIcon: ValueListenableBuilder(\n                        valueListenable: databaseController.compactModeNotifier,\n                        builder: (context, compactMode, child) {\n                          return Toggle(\n                            value: compactMode,\n                            duration: Duration.zero,\n                            onChanged: (value) =>\n                                databaseController.setCompactMode(value),\n                            padding: EdgeInsets.zero,\n                          );\n                        },\n                      ),\n                    ),\n                  ),\n                ),\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass DatabaseViewLayoutCell extends StatelessWidget {\n  const DatabaseViewLayoutCell({\n    super.key,\n    required this.isSelected,\n    required this.databaseLayout,\n    required this.onTap,\n  });\n\n  final bool isSelected;\n  final DatabaseLayoutPB databaseLayout;\n  final void Function(DatabaseLayoutPB) onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 6),\n      child: SizedBox(\n        height: 30,\n        child: FlowyButton(\n          hoverColor: AFThemeExtension.of(context).lightGreyHover,\n          text: FlowyText(\n            lineHeight: 1.0,\n            databaseLayout.layoutName,\n            color: AFThemeExtension.of(context).textColor,\n          ),\n          leftIcon: FlowySvg(\n            databaseLayout.icon,\n            color: Theme.of(context).iconTheme.color,\n          ),\n          rightIcon: isSelected ? const FlowySvg(FlowySvgs.check_s) : null,\n          onTap: () => onTap(databaseLayout),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/group/database_group.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/database_layout_selector.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/setting_property_list.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nenum DatabaseSettingAction {\n  showProperties,\n  showLayout,\n  showGroup,\n  showCalendarLayout,\n}\n\nextension DatabaseSettingActionExtension on DatabaseSettingAction {\n  FlowySvgData iconData() {\n    switch (this) {\n      case DatabaseSettingAction.showProperties:\n        return FlowySvgs.multiselect_s;\n      case DatabaseSettingAction.showLayout:\n        return FlowySvgs.database_layout_s;\n      case DatabaseSettingAction.showGroup:\n        return FlowySvgs.group_s;\n      case DatabaseSettingAction.showCalendarLayout:\n        return FlowySvgs.calendar_layout_s;\n    }\n  }\n\n  String title() {\n    switch (this) {\n      case DatabaseSettingAction.showProperties:\n        return LocaleKeys.grid_settings_properties.tr();\n      case DatabaseSettingAction.showLayout:\n        return LocaleKeys.grid_settings_databaseLayout.tr();\n      case DatabaseSettingAction.showGroup:\n        return LocaleKeys.grid_settings_group.tr();\n      case DatabaseSettingAction.showCalendarLayout:\n        return LocaleKeys.calendar_settings_name.tr();\n    }\n  }\n\n  Widget build(\n    BuildContext context,\n    DatabaseController databaseController,\n    PopoverMutex popoverMutex,\n  ) {\n    final popover = switch (this) {\n      DatabaseSettingAction.showLayout => DatabaseLayoutSelector(\n          viewId: databaseController.viewId,\n          databaseController: databaseController,\n        ),\n      DatabaseSettingAction.showGroup => DatabaseGroupList(\n          viewId: databaseController.viewId,\n          databaseController: databaseController,\n          onDismissed: () {},\n        ),\n      DatabaseSettingAction.showProperties => DatabasePropertyList(\n          viewId: databaseController.viewId,\n          fieldController: databaseController.fieldController,\n        ),\n      DatabaseSettingAction.showCalendarLayout => CalendarLayoutSetting(\n          databaseController: databaseController,\n        ),\n    };\n\n    return AppFlowyPopover(\n      triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,\n      direction: PopoverDirection.leftWithTopAligned,\n      mutex: popoverMutex,\n      margin: EdgeInsets.zero,\n      offset: const Offset(-14, 0),\n      child: SizedBox(\n        height: GridSize.popoverItemHeight,\n        child: FlowyButton(\n          hoverColor: AFThemeExtension.of(context).lightGreyHover,\n          text: FlowyText(\n            title(),\n            lineHeight: 1.0,\n            color: AFThemeExtension.of(context).textColor,\n          ),\n          leftIcon: FlowySvg(\n            iconData(),\n            color: Theme.of(context).iconTheme.color,\n          ),\n          rightIcon: FlowySvg(FlowySvgs.database_settings_arrow_right_s),\n        ),\n      ),\n      popupBuilder: (context) => popover,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_settings_list.dart",
    "content": "import 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/database_setting_action.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass DatabaseSettingsList extends StatefulWidget {\n  const DatabaseSettingsList({\n    super.key,\n    required this.databaseController,\n  });\n\n  final DatabaseController databaseController;\n\n  @override\n  State<StatefulWidget> createState() => _DatabaseSettingsListState();\n}\n\nclass _DatabaseSettingsListState extends State<DatabaseSettingsList> {\n  final PopoverMutex popoverMutex = PopoverMutex();\n\n  @override\n  void dispose() {\n    popoverMutex.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final cells =\n        actionsForDatabaseLayout(widget.databaseController.databaseLayout)\n            .map(\n              (action) => action.build(\n                context,\n                widget.databaseController,\n                popoverMutex,\n              ),\n            )\n            .toList();\n\n    return ListView.separated(\n      shrinkWrap: true,\n      padding: EdgeInsets.zero,\n      itemCount: cells.length,\n      separatorBuilder: (context, index) =>\n          VSpace(GridSize.typeOptionSeparatorHeight),\n      physics: StyledScrollPhysics(),\n      itemBuilder: (BuildContext context, int index) => cells[index],\n    );\n  }\n}\n\n/// Returns the list of actions that should be shown for the given database layout.\nList<DatabaseSettingAction> actionsForDatabaseLayout(DatabaseLayoutPB? layout) {\n  switch (layout) {\n    case DatabaseLayoutPB.Board:\n      return [\n        DatabaseSettingAction.showProperties,\n        DatabaseSettingAction.showLayout,\n        if (!UniversalPlatform.isMobile) DatabaseSettingAction.showGroup,\n      ];\n    case DatabaseLayoutPB.Calendar:\n      return [\n        DatabaseSettingAction.showProperties,\n        DatabaseSettingAction.showLayout,\n        DatabaseSettingAction.showCalendarLayout,\n      ];\n    case DatabaseLayoutPB.Grid:\n      return [\n        DatabaseSettingAction.showProperties,\n        DatabaseSettingAction.showLayout,\n      ];\n    default:\n      return [];\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/setting/field_visibility_extension.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart';\n\nextension ToggleVisibility on FieldVisibility {\n  FieldVisibility toggle() => switch (this) {\n        FieldVisibility.AlwaysShown => FieldVisibility.AlwaysHidden,\n        FieldVisibility.AlwaysHidden => FieldVisibility.AlwaysShown,\n        _ => FieldVisibility.AlwaysHidden,\n      };\n\n  bool isVisibleState() => switch (this) {\n        FieldVisibility.AlwaysShown => true,\n        FieldVisibility.HideWhenEmpty => true,\n        FieldVisibility.AlwaysHidden => false,\n        _ => false,\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/database/view/database_field_list.dart';\nimport 'package:appflowy/mobile/presentation/database/view/database_filter_bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/database/view/database_sort_bottom_sheet.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nenum MobileDatabaseControlFeatures { sort, filter }\n\nclass MobileDatabaseControls extends StatelessWidget {\n  const MobileDatabaseControls({\n    super.key,\n    required this.controller,\n    required this.features,\n  });\n\n  final DatabaseController controller;\n  final List<MobileDatabaseControlFeatures> features;\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider(\n          create: (context) => FilterEditorBloc(\n            viewId: controller.viewId,\n            fieldController: controller.fieldController,\n          ),\n        ),\n        BlocProvider<SortEditorBloc>(\n          create: (context) => SortEditorBloc(\n            viewId: controller.viewId,\n            fieldController: controller.fieldController,\n          ),\n        ),\n      ],\n      child: ValueListenableBuilder<bool>(\n        valueListenable: controller.isLoading,\n        builder: (context, isLoading, child) {\n          if (isLoading) {\n            return const SizedBox.shrink();\n          }\n\n          return SeparatedRow(\n            separatorBuilder: () => const HSpace(8.0),\n            children: [\n              if (features.contains(MobileDatabaseControlFeatures.sort))\n                _DatabaseControlButton(\n                  icon: FlowySvgs.sort_ascending_s,\n                  count: context.watch<SortEditorBloc>().state.sorts.length,\n                  onTap: () => _showEditSortPanelFromToolbar(\n                    context,\n                    controller,\n                  ),\n                ),\n              if (features.contains(MobileDatabaseControlFeatures.filter))\n                _DatabaseControlButton(\n                  icon: FlowySvgs.filter_s,\n                  count: context.watch<FilterEditorBloc>().state.filters.length,\n                  onTap: () => _showEditFilterPanelFromToolbar(\n                    context,\n                    controller,\n                  ),\n                ),\n              _DatabaseControlButton(\n                icon: FlowySvgs.m_field_hide_s,\n                onTap: () => _showDatabaseFieldListFromToolbar(\n                  context,\n                  controller,\n                ),\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _DatabaseControlButton extends StatelessWidget {\n  const _DatabaseControlButton({\n    required this.onTap,\n    required this.icon,\n    this.count = 0,\n  });\n\n  final VoidCallback onTap;\n  final FlowySvgData icon;\n  final int count;\n\n  @override\n  Widget build(BuildContext context) {\n    return InkWell(\n      onTap: onTap,\n      borderRadius: BorderRadius.circular(10),\n      child: Padding(\n        padding: const EdgeInsets.all(5.0),\n        child: count == 0\n            ? FlowySvg(\n                icon,\n                size: const Size.square(20),\n              )\n            : Row(\n                children: [\n                  FlowySvg(\n                    icon,\n                    size: const Size.square(20),\n                    color: Theme.of(context).colorScheme.primary,\n                  ),\n                  const HSpace(2.0),\n                  FlowyText.medium(\n                    count.toString(),\n                    color: Theme.of(context).colorScheme.primary,\n                  ),\n                ],\n              ),\n      ),\n    );\n  }\n}\n\nvoid _showDatabaseFieldListFromToolbar(\n  BuildContext context,\n  DatabaseController databaseController,\n) {\n  showTransitionMobileBottomSheet(\n    context,\n    showHeader: true,\n    showBackButton: true,\n    title: LocaleKeys.grid_settings_properties.tr(),\n    builder: (_) {\n      return BlocProvider.value(\n        value: context.read<ViewBloc>(),\n        child: MobileDatabaseFieldList(\n          databaseController: databaseController,\n          canCreate: false,\n        ),\n      );\n    },\n  );\n}\n\nvoid _showEditSortPanelFromToolbar(\n  BuildContext context,\n  DatabaseController databaseController,\n) {\n  showMobileBottomSheet(\n    context,\n    showDragHandle: true,\n    showDivider: false,\n    useSafeArea: false,\n    backgroundColor: AFThemeExtension.of(context).background,\n    builder: (_) {\n      return BlocProvider.value(\n        value: context.read<SortEditorBloc>(),\n        child: const MobileSortEditor(),\n      );\n    },\n  );\n}\n\nvoid _showEditFilterPanelFromToolbar(\n  BuildContext context,\n  DatabaseController databaseController,\n) {\n  showMobileBottomSheet(\n    context,\n    showDragHandle: true,\n    showDivider: false,\n    useSafeArea: false,\n    backgroundColor: AFThemeExtension.of(context).background,\n    builder: (_) {\n      return BlocProvider.value(\n        value: context.read<FilterEditorBloc>(),\n        child: const MobileFilterEditor(),\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/database_settings_list.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass SettingButton extends StatefulWidget {\n  const SettingButton({super.key, required this.databaseController});\n\n  final DatabaseController databaseController;\n\n  @override\n  State<SettingButton> createState() => _SettingButtonState();\n}\n\nclass _SettingButtonState extends State<SettingButton> {\n  final PopoverController _popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      controller: _popoverController,\n      constraints: BoxConstraints.loose(const Size(200, 400)),\n      direction: PopoverDirection.bottomWithCenterAligned,\n      offset: const Offset(0, 8),\n      triggerActions: PopoverTriggerFlags.none,\n      child: MouseRegion(\n        cursor: SystemMouseCursors.click,\n        child: FlowyIconButton(\n          tooltipText: LocaleKeys.settings_title.tr(),\n          width: 24,\n          height: 24,\n          iconPadding: const EdgeInsets.all(3),\n          hoverColor: AFThemeExtension.of(context).lightGreyHover,\n          icon: const FlowySvg(FlowySvgs.settings_s),\n          onPressed: _popoverController.show,\n        ),\n      ),\n      popupBuilder: (_) =>\n          DatabaseSettingsList(databaseController: widget.databaseController),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/setting/property_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';\nimport 'package:appflowy/plugins/database/widgets/field/field_editor.dart';\nimport 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass DatabasePropertyList extends StatefulWidget {\n  const DatabasePropertyList({\n    super.key,\n    required this.viewId,\n    required this.fieldController,\n  });\n\n  final String viewId;\n  final FieldController fieldController;\n\n  @override\n  State<StatefulWidget> createState() => _DatabasePropertyListState();\n}\n\nclass _DatabasePropertyListState extends State<DatabasePropertyList> {\n  final PopoverMutex _popoverMutex = PopoverMutex();\n  late final DatabasePropertyBloc _bloc;\n\n  @override\n  void initState() {\n    super.initState();\n    _bloc = DatabasePropertyBloc(\n      viewId: widget.viewId,\n      fieldController: widget.fieldController,\n    )..add(const DatabasePropertyEvent.initial());\n  }\n\n  @override\n  void dispose() {\n    _popoverMutex.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<DatabasePropertyBloc>.value(\n      value: _bloc,\n      child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>(\n        builder: (context, state) {\n          final cells = state.fieldContexts\n              .mapIndexed(\n                (index, field) => DatabasePropertyCell(\n                  key: ValueKey(field.id),\n                  viewId: widget.viewId,\n                  fieldController: widget.fieldController,\n                  fieldInfo: field,\n                  popoverMutex: _popoverMutex,\n                  index: index,\n                ),\n              )\n              .toList();\n\n          return ReorderableListView(\n            proxyDecorator: (child, index, _) => Material(\n              color: Colors.transparent,\n              child: Stack(\n                children: [\n                  child,\n                  MouseRegion(\n                    cursor: Platform.isWindows\n                        ? SystemMouseCursors.click\n                        : SystemMouseCursors.grabbing,\n                    child: const SizedBox.expand(),\n                  ),\n                ],\n              ),\n            ),\n            buildDefaultDragHandles: false,\n            shrinkWrap: true,\n            onReorder: (from, to) {\n              context\n                  .read<DatabasePropertyBloc>()\n                  .add(DatabasePropertyEvent.moveField(from, to));\n            },\n            onReorderStart: (_) => _popoverMutex.close(),\n            padding: const EdgeInsets.symmetric(vertical: 4.0),\n            children: cells,\n          );\n        },\n      ),\n    );\n  }\n}\n\n@visibleForTesting\nclass DatabasePropertyCell extends StatefulWidget {\n  const DatabasePropertyCell({\n    super.key,\n    required this.fieldInfo,\n    required this.viewId,\n    required this.popoverMutex,\n    required this.index,\n    required this.fieldController,\n  });\n\n  final FieldInfo fieldInfo;\n  final String viewId;\n  final PopoverMutex popoverMutex;\n  final int index;\n  final FieldController fieldController;\n\n  @override\n  State<DatabasePropertyCell> createState() => _DatabasePropertyCellState();\n}\n\nclass _DatabasePropertyCellState extends State<DatabasePropertyCell> {\n  final PopoverController _popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    final visiblity = widget.fieldInfo.visibility;\n    final visibleIcon = FlowySvg(\n      visiblity != null && visiblity != FieldVisibility.AlwaysHidden\n          ? FlowySvgs.show_m\n          : FlowySvgs.hide_m,\n      size: const Size.square(16),\n      color: Theme.of(context).iconTheme.color,\n    );\n\n    return AppFlowyPopover(\n      mutex: widget.popoverMutex,\n      controller: _popoverController,\n      offset: const Offset(-8, 0),\n      direction: PopoverDirection.leftWithTopAligned,\n      constraints: BoxConstraints.loose(const Size(240, 400)),\n      triggerActions: PopoverTriggerFlags.none,\n      margin: EdgeInsets.zero,\n      child: Container(\n        height: GridSize.popoverItemHeight,\n        margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 6),\n        child: FlowyButton(\n          hoverColor: AFThemeExtension.of(context).lightGreyHover,\n          text: FlowyText(\n            lineHeight: 1.0,\n            widget.fieldInfo.name,\n            color: AFThemeExtension.of(context).textColor,\n          ),\n          leftIconSize: const Size(36, 18),\n          leftIcon: Row(\n            children: [\n              ReorderableDragStartListener(\n                index: widget.index,\n                child: MouseRegion(\n                  cursor: Platform.isWindows\n                      ? SystemMouseCursors.click\n                      : SystemMouseCursors.grab,\n                  child: SizedBox(\n                    width: 14,\n                    height: 14,\n                    child: FlowySvg(\n                      FlowySvgs.drag_element_s,\n                      color: Theme.of(context).iconTheme.color,\n                    ),\n                  ),\n                ),\n              ),\n              const HSpace(6.0),\n              FieldIcon(\n                fieldInfo: widget.fieldInfo,\n              ),\n            ],\n          ),\n          rightIcon: FlowyIconButton(\n            hoverColor: Colors.transparent,\n            onPressed: () {\n              if (widget.fieldInfo.fieldSettings == null) {\n                return;\n              }\n\n              final newVisiblity = widget.fieldInfo.visibility!.toggle();\n              context.read<DatabasePropertyBloc>().add(\n                    DatabasePropertyEvent.setFieldVisibility(\n                      widget.fieldInfo.id,\n                      newVisiblity,\n                    ),\n                  );\n            },\n            icon: visibleIcon,\n          ),\n          onTap: () => _popoverController.show(),\n        ),\n      ),\n      popupBuilder: (BuildContext context) {\n        return FieldEditor(\n          viewId: widget.viewId,\n          fieldInfo: widget.fieldInfo,\n          fieldController: widget.fieldController,\n          isNewField: false,\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database/widgets/share_button.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/share_bloc.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/workspace/application/view/view_listener.dart';\nimport 'package:appflowy/workspace/presentation/home/toast.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/file_picker/file_picker_service.dart';\nimport 'package:flowy_infra_ui/widget/rounded_button.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass DatabaseShareButton extends StatelessWidget {\n  const DatabaseShareButton({\n    super.key,\n    required this.view,\n  });\n\n  final ViewPB view;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => DatabaseShareBloc(view: view),\n      child: BlocListener<DatabaseShareBloc, DatabaseShareState>(\n        listener: (context, state) {\n          state.mapOrNull(\n            finish: (state) {\n              state.successOrFail.fold(\n                (data) => _handleExportData(context),\n                _handleExportError,\n              );\n            },\n          );\n        },\n        child: BlocBuilder<DatabaseShareBloc, DatabaseShareState>(\n          builder: (context, state) => IntrinsicWidth(\n            child: DatabaseShareActionList(view: view),\n          ),\n        ),\n      ),\n    );\n  }\n\n  void _handleExportData(BuildContext context) {\n    showSnackBarMessage(\n      context,\n      LocaleKeys.settings_files_exportFileSuccess.tr(),\n    );\n  }\n\n  void _handleExportError(FlowyError error) {\n    showMessageToast(error.msg);\n  }\n}\n\nclass DatabaseShareActionList extends StatefulWidget {\n  const DatabaseShareActionList({\n    super.key,\n    required this.view,\n  });\n\n  final ViewPB view;\n\n  @override\n  State<DatabaseShareActionList> createState() =>\n      DatabaseShareActionListState();\n}\n\n@visibleForTesting\nclass DatabaseShareActionListState extends State<DatabaseShareActionList> {\n  late String name;\n  late final ViewListener viewListener = ViewListener(viewId: widget.view.id);\n\n  @override\n  void initState() {\n    super.initState();\n    listenOnViewUpdated();\n  }\n\n  @override\n  void dispose() {\n    viewListener.stop();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final databaseShareBloc = context.read<DatabaseShareBloc>();\n    return PopoverActionList<ShareActionWrapper>(\n      direction: PopoverDirection.bottomWithCenterAligned,\n      offset: const Offset(0, 8),\n      actions: ShareAction.values\n          .map((action) => ShareActionWrapper(action))\n          .toList(),\n      buildChild: (controller) => Listener(\n        onPointerDown: (_) => controller.show(),\n        child: RoundedTextButton(\n          title: LocaleKeys.shareAction_buttonText.tr(),\n          padding: const EdgeInsets.symmetric(horizontal: 12.0),\n          fontSize: 14.0,\n          textColor: Theme.of(context).colorScheme.onPrimary,\n          onPressed: () {},\n        ),\n      ),\n      onSelected: (action, controller) async {\n        switch (action.inner) {\n          case ShareAction.csv:\n            final exportPath = await getIt<FilePickerService>().saveFile(\n              dialogTitle: '',\n              fileName: '${name.toFileName()}.csv',\n            );\n            if (exportPath != null) {\n              databaseShareBloc.add(DatabaseShareEvent.shareCSV(exportPath));\n            }\n            break;\n        }\n        controller.close();\n      },\n    );\n  }\n\n  void listenOnViewUpdated() {\n    name = widget.view.name;\n    viewListener.start(\n      onViewUpdated: (view) {\n        name = view.name;\n      },\n    );\n  }\n}\n\nenum ShareAction {\n  csv,\n}\n\nclass ShareActionWrapper extends ActionCell {\n  ShareActionWrapper(this.inner);\n\n  final ShareAction inner;\n\n  Widget? icon(Color iconColor) => null;\n\n  @override\n  String get name {\n    switch (inner) {\n      case ShareAction.csv:\n        return LocaleKeys.shareAction_csv.tr();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/row/related_row_detail_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_banner.dart';\nimport 'package:appflowy/plugins/database/widgets/row/row_property.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/banner.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_drop_handler.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_page.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/shared/flowy_error_page.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';\nimport 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\n\nimport '../../workspace/application/view/view_bloc.dart';\n\n// 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.\n\nclass DatabaseDocumentPage extends StatefulWidget {\n  const DatabaseDocumentPage({\n    super.key,\n    required this.view,\n    required this.databaseId,\n    required this.rowId,\n    required this.documentId,\n    this.initialSelection,\n  });\n\n  final ViewPB view;\n  final String databaseId;\n  final String rowId;\n  final String documentId;\n  final Selection? initialSelection;\n\n  @override\n  State<DatabaseDocumentPage> createState() => _DatabaseDocumentPageState();\n}\n\nclass _DatabaseDocumentPageState extends State<DatabaseDocumentPage> {\n  EditorState? editorState;\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider.value(\n          value: getIt<ActionNavigationBloc>(),\n        ),\n        BlocProvider(\n          create: (_) => DocumentBloc(\n            databaseViewId: widget.databaseId,\n            rowId: widget.rowId,\n            documentId: widget.documentId,\n          )..add(const DocumentEvent.initial()),\n        ),\n        BlocProvider(\n          create: (_) =>\n              ViewBloc(view: widget.view)..add(const ViewEvent.initial()),\n        ),\n      ],\n      child: BlocBuilder<DocumentBloc, DocumentState>(\n        builder: (context, state) {\n          if (state.isLoading) {\n            return const Center(child: CircularProgressIndicator.adaptive());\n          }\n\n          final editorState = state.editorState;\n          this.editorState = editorState;\n          final error = state.error;\n          if (error != null || editorState == null) {\n            Log.error(error);\n            return Center(\n              child: AppFlowyErrorPage(\n                error: error,\n              ),\n            );\n          }\n\n          if (state.forceClose) {\n            return const SizedBox.shrink();\n          }\n\n          return BlocListener<ActionNavigationBloc, ActionNavigationState>(\n            listener: _onNotificationAction,\n            listenWhen: (_, curr) => curr.action != null,\n            child: AiWriterScrollWrapper(\n              viewId: widget.view.id,\n              editorState: editorState,\n              child: _buildEditorPage(context, state),\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildEditorPage(BuildContext context, DocumentState state) {\n    final appflowyEditorPage = EditorDropHandler(\n      viewId: widget.view.id,\n      editorState: state.editorState!,\n      isLocalMode: context.read<DocumentBloc>().isLocalMode,\n      child: AppFlowyEditorPage(\n        editorState: state.editorState!,\n        styleCustomizer: EditorStyleCustomizer(\n          context: context,\n          padding: EditorStyleCustomizer.documentPadding,\n          editorState: state.editorState!,\n        ),\n        header: _buildDatabaseDataContent(context, state.editorState!),\n        initialSelection: widget.initialSelection,\n        useViewInfoBloc: false,\n        placeholderText: (node) =>\n            node.type == ParagraphBlockKeys.type && !node.isInTable\n                ? LocaleKeys.editor_slashPlaceHolder.tr()\n                : '',\n      ),\n    );\n\n    return Provider(\n      create: (_) {\n        final context = SharedEditorContext();\n        context.isInDatabaseRowPage = true;\n        return context;\n      },\n      dispose: (_, editorContext) => editorContext.dispose(),\n      child: EditorTransactionService(\n        viewId: widget.view.id,\n        editorState: state.editorState!,\n        child: Column(\n          children: [\n            if (state.isDeleted) _buildBanner(context),\n            Expanded(child: appflowyEditorPage),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildDatabaseDataContent(\n    BuildContext context,\n    EditorState editorState,\n  ) {\n    return BlocProvider(\n      create: (context) => RelatedRowDetailPageBloc(\n        databaseId: widget.databaseId,\n        initialRowId: widget.rowId,\n      ),\n      child: BlocBuilder<RelatedRowDetailPageBloc, RelatedRowDetailPageState>(\n        builder: (context, state) {\n          return state.when(\n            loading: () => const SizedBox.shrink(),\n            ready: (databaseController, rowController) {\n              final padding = EditorStyleCustomizer.documentPadding;\n              return BlocProvider(\n                create: (context) => RowDetailBloc(\n                  fieldController: databaseController.fieldController,\n                  rowController: rowController,\n                ),\n                child: Column(\n                  children: [\n                    RowBanner(\n                      databaseController: databaseController,\n                      rowController: rowController,\n                      cellBuilder: EditableCellBuilder(\n                        databaseController: databaseController,\n                      ),\n                      userProfile:\n                          context.read<RelatedRowDetailPageBloc>().userProfile,\n                    ),\n                    Padding(\n                      padding: EdgeInsets.only(\n                        top: 24,\n                        left: padding.left,\n                        right: padding.right,\n                      ),\n                      child: RowPropertyList(\n                        viewId: databaseController.viewId,\n                        fieldController: databaseController.fieldController,\n                        cellBuilder: EditableCellBuilder(\n                          databaseController: databaseController,\n                        ),\n                      ),\n                    ),\n                    const TypeOptionSeparator(spacing: 24.0),\n                  ],\n                ),\n              );\n            },\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildBanner(BuildContext context) {\n    return DocumentBanner(\n      viewName: widget.view.name,\n      onRestore: () => context.read<DocumentBloc>().add(\n            const DocumentEvent.restorePage(),\n          ),\n      onDelete: () => context.read<DocumentBloc>().add(\n            const DocumentEvent.deletePermanently(),\n          ),\n    );\n  }\n\n  void _onNotificationAction(\n    BuildContext context,\n    ActionNavigationState state,\n  ) {\n    if (state.action != null && state.action!.type == ActionType.jumpToBlock) {\n      final path = state.action?.arguments?[ActionArgumentKeys.nodePath];\n\n      final editorState = context.read<DocumentBloc>().state.editorState;\n      if (editorState != null && widget.documentId == state.action?.objectId) {\n        editorState.updateSelectionWithReason(\n          Selection.collapsed(Position(path: [path])),\n        );\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database_document/database_document_plugin.dart",
    "content": "library;\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/home_stack.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'database_document_page.dart';\nimport 'presentation/database_document_title.dart';\n\n// 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.\n\nclass DatabaseDocumentContext {\n  DatabaseDocumentContext({\n    required this.view,\n    required this.databaseId,\n    required this.rowId,\n    required this.documentId,\n  });\n\n  final ViewPB view;\n  final String databaseId;\n  final String rowId;\n  final String documentId;\n}\n\nclass DatabaseDocumentPluginBuilder extends PluginBuilder {\n  @override\n  Plugin build(dynamic data) {\n    if (data is DatabaseDocumentContext) {\n      return DatabaseDocumentPlugin(pluginType: pluginType, data: data);\n    }\n\n    throw FlowyPluginException.invalidData;\n  }\n\n  @override\n  String get menuName => LocaleKeys.document_menuName.tr();\n\n  @override\n  FlowySvgData get icon => FlowySvgs.icon_document_s;\n\n  @override\n  PluginType get pluginType => PluginType.databaseDocument;\n\n  @override\n  ViewLayoutPB get layoutType => ViewLayoutPB.Document;\n}\n\nclass DatabaseDocumentPlugin extends Plugin {\n  DatabaseDocumentPlugin({\n    required this.data,\n    required PluginType pluginType,\n    this.initialSelection,\n  }) : _pluginType = pluginType;\n\n  final DatabaseDocumentContext data;\n  final PluginType _pluginType;\n\n  final Selection? initialSelection;\n\n  @override\n  PluginWidgetBuilder get widgetBuilder => DatabaseDocumentPluginWidgetBuilder(\n        view: data.view,\n        databaseId: data.databaseId,\n        rowId: data.rowId,\n        documentId: data.documentId,\n        initialSelection: initialSelection,\n      );\n\n  @override\n  PluginType get pluginType => _pluginType;\n\n  @override\n  PluginId get id => data.rowId;\n}\n\nclass DatabaseDocumentPluginWidgetBuilder extends PluginWidgetBuilder\n    with NavigationItem {\n  DatabaseDocumentPluginWidgetBuilder({\n    required this.view,\n    required this.databaseId,\n    required this.rowId,\n    required this.documentId,\n    this.initialSelection,\n  });\n\n  final ViewPB view;\n  final String databaseId;\n  final String rowId;\n  final String documentId;\n  final Selection? initialSelection;\n\n  @override\n  String? get viewName => view.nameOrDefault;\n\n  @override\n  EdgeInsets get contentPadding => EdgeInsets.zero;\n\n  @override\n  Widget buildWidget({\n    required PluginContext context,\n    required bool shrinkWrap,\n    Map<String, dynamic>? data,\n  }) {\n    return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(\n      builder: (_, state) => DatabaseDocumentPage(\n        key: ValueKey(documentId),\n        view: view,\n        databaseId: databaseId,\n        documentId: documentId,\n        rowId: rowId,\n        initialSelection: initialSelection,\n      ),\n    );\n  }\n\n  @override\n  Widget get leftBarItem =>\n      ViewTitleBarWithRow(view: view, databaseId: databaseId, rowId: rowId);\n\n  @override\n  Widget tabBarItem(String pluginId, [bool shortForm = false]) =>\n      const SizedBox.shrink();\n\n  @override\n  Widget? get rightBarItem => const SizedBox.shrink();\n\n  @override\n  List<NavigationItem> get navigationItems => [this];\n}\n\nclass DatabaseDocumentPluginConfig implements PluginConfig {\n  @override\n  bool get creatable => false;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';\nimport 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'database_document_title_bloc.dart';\n\n// 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.\n\n// workspaces / ... / database view name / row name\nclass ViewTitleBarWithRow extends StatelessWidget {\n  const ViewTitleBarWithRow({\n    super.key,\n    required this.view,\n    required this.databaseId,\n    required this.rowId,\n  });\n\n  final ViewPB view;\n  final String databaseId;\n  final String rowId;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => DatabaseDocumentTitleBloc(\n        view: view,\n        rowId: rowId,\n      ),\n      child: BlocBuilder<DatabaseDocumentTitleBloc, DatabaseDocumentTitleState>(\n        builder: (context, state) {\n          if (state.ancestors.isEmpty) {\n            return const SizedBox.shrink();\n          }\n          return SingleChildScrollView(\n            scrollDirection: Axis.horizontal,\n            child: SizedBox(\n              height: 24,\n              child: Row(\n                // refresh the view title bar when the ancestors changed\n                key: ValueKey(state.ancestors.hashCode),\n                children: _buildViewTitles(state.ancestors),\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  List<Widget> _buildViewTitles(List<ViewPB> views) {\n    // if the level is too deep, only show the root view, the database view and the row\n    return views.length > 2\n        ? [\n            _buildViewButton(views[1]),\n            const FlowySvg(FlowySvgs.title_bar_divider_s),\n            const FlowyText.regular(' ... '),\n            const FlowySvg(FlowySvgs.title_bar_divider_s),\n            _buildViewButton(views.last),\n            const FlowySvg(FlowySvgs.title_bar_divider_s),\n            _buildRowName(),\n          ]\n        : [\n            ...views\n                .map(\n                  (e) => [\n                    _buildViewButton(e),\n                    const FlowySvg(FlowySvgs.title_bar_divider_s),\n                  ],\n                )\n                .flattened,\n            _buildRowName(),\n          ];\n  }\n\n  Widget _buildViewButton(ViewPB view) {\n    return FlowyTooltip(\n      message: view.name,\n      child: ViewTitle(\n        view: view,\n        behavior: ViewTitleBehavior.uneditable,\n        onUpdated: () {},\n      ),\n    );\n  }\n\n  Widget _buildRowName() {\n    return _RowName(\n      rowId: rowId,\n    );\n  }\n}\n\nclass _RowName extends StatelessWidget {\n  const _RowName({\n    required this.rowId,\n  });\n\n  final String rowId;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<DatabaseDocumentTitleBloc, DatabaseDocumentTitleState>(\n      builder: (context, state) {\n        if (state.databaseController == null) {\n          return const SizedBox.shrink();\n        }\n\n        final cellBuilder = EditableCellBuilder(\n          databaseController: state.databaseController!,\n        );\n\n        return cellBuilder.buildCustom(\n          CellContext(\n            fieldId: state.fieldId!,\n            rowId: rowId,\n          ),\n          skinMap: EditableCellSkinMap(textSkin: _TitleSkin()),\n        );\n      },\n    );\n  }\n}\n\nclass _TitleSkin extends IEditableTextCellSkin {\n  @override\n  Widget build(\n    BuildContext context,\n    CellContainerNotifier cellContainerNotifier,\n    ValueNotifier<bool> compactModeNotifier,\n    TextCellBloc bloc,\n    FocusNode focusNode,\n    TextEditingController textEditingController,\n  ) {\n    return BlocSelector<TextCellBloc, TextCellState, String>(\n      selector: (state) => state.content ?? \"\",\n      builder: (context, content) {\n        final name = content.isEmpty\n            ? LocaleKeys.grid_row_titlePlaceholder.tr()\n            : content;\n        return BlocBuilder<DatabaseDocumentTitleBloc,\n            DatabaseDocumentTitleState>(\n          builder: (context, state) {\n            return FlowyTooltip(\n              message: name,\n              child: AppFlowyPopover(\n                constraints: const BoxConstraints(\n                  maxWidth: 300,\n                  maxHeight: 44,\n                ),\n                direction: PopoverDirection.bottomWithLeftAligned,\n                offset: const Offset(0, 18),\n                popupBuilder: (_) {\n                  return RenameRowPopover(\n                    textController: textEditingController,\n                    icon: state.icon ?? EmojiIconData.none(),\n                    onUpdateIcon: (icon) {\n                      context\n                          .read<DatabaseDocumentTitleBloc>()\n                          .add(DatabaseDocumentTitleEvent.updateIcon(icon));\n                    },\n                    onUpdateName: (text) =>\n                        bloc.add(TextCellEvent.updateText(text)),\n                    tabs: const [PickerTabType.emoji],\n                  );\n                },\n                child: FlowyButton(\n                  useIntrinsicWidth: true,\n                  onTap: () {},\n                  margin: const EdgeInsets.symmetric(horizontal: 6),\n                  text: Row(\n                    children: [\n                      if (state.icon != null) ...[\n                        RawEmojiIconWidget(emoji: state.icon!, emojiSize: 14),\n                        const HSpace(4.0),\n                      ],\n                      ConstrainedBox(\n                        constraints: const BoxConstraints(maxWidth: 180),\n                        child: FlowyText.regular(\n                          name,\n                          overflow: TextOverflow.ellipsis,\n                          fontSize: 14.0,\n                          figmaLineHeight: 18.0,\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass RenameRowPopover extends StatefulWidget {\n  const RenameRowPopover({\n    super.key,\n    required this.textController,\n    required this.onUpdateName,\n    required this.onUpdateIcon,\n    required this.icon,\n    this.tabs = const [PickerTabType.emoji, PickerTabType.icon],\n  });\n\n  final TextEditingController textController;\n  final EmojiIconData icon;\n\n  final ValueChanged<String> onUpdateName;\n  final ValueChanged<EmojiIconData> onUpdateIcon;\n  final List<PickerTabType> tabs;\n\n  @override\n  State<RenameRowPopover> createState() => _RenameRowPopoverState();\n}\n\nclass _RenameRowPopoverState extends State<RenameRowPopover> {\n  @override\n  void initState() {\n    super.initState();\n    widget.textController.selection = TextSelection(\n      baseOffset: 0,\n      extentOffset: widget.textController.value.text.characters.length,\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        EmojiPickerButton(\n          emoji: widget.icon,\n          direction: PopoverDirection.bottomWithCenterAligned,\n          offset: const Offset(0, 18),\n          defaultIcon: const FlowySvg(FlowySvgs.document_s),\n          onSubmitted: (r, _) {\n            widget.onUpdateIcon(r.data);\n            if (!r.keepOpen) PopoverContainer.of(context).close();\n          },\n          tabs: widget.tabs,\n        ),\n        const HSpace(6),\n        SizedBox(\n          height: 36.0,\n          width: 220,\n          child: FlowyTextField(\n            controller: widget.textController,\n            maxLength: 256,\n            onSubmitted: (text) {\n              widget.onUpdateName(text);\n              PopoverContainer.of(context).close();\n            },\n            onCanceled: () => widget.onUpdateName(widget.textController.text),\n            showCounter: false,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_controller.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/plugins/database/domain/row_meta_listener.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport '../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\n\npart 'database_document_title_bloc.freezed.dart';\n\nclass DatabaseDocumentTitleBloc\n    extends Bloc<DatabaseDocumentTitleEvent, DatabaseDocumentTitleState> {\n  DatabaseDocumentTitleBloc({\n    required this.view,\n    required this.rowId,\n  })  : _metaListener = RowMetaListener(rowId),\n        super(DatabaseDocumentTitleState.initial()) {\n    _dispatch();\n    _startListening();\n    _init();\n  }\n\n  final ViewPB view;\n  final String rowId;\n  final RowMetaListener _metaListener;\n\n  void _dispatch() {\n    on<DatabaseDocumentTitleEvent>((event, emit) async {\n      event.when(\n        didUpdateAncestors: (ancestors) {\n          emit(\n            state.copyWith(\n              ancestors: ancestors,\n            ),\n          );\n        },\n        didUpdateRowTitleInfo: (databaseController, rowController, fieldId) {\n          emit(\n            state.copyWith(\n              databaseController: databaseController,\n              rowController: rowController,\n              fieldId: fieldId,\n            ),\n          );\n        },\n        didUpdateRowIcon: (icon) {\n          emit(\n            state.copyWith(\n              icon: icon,\n            ),\n          );\n        },\n        updateIcon: (icon) {\n          _updateMeta(icon.emoji);\n        },\n      );\n    });\n  }\n\n  void _startListening() {\n    _metaListener.start(\n      callback: (rowMeta) {\n        if (!isClosed) {\n          add(\n            DatabaseDocumentTitleEvent.didUpdateRowIcon(\n              EmojiIconData.emoji(rowMeta.icon),\n            ),\n          );\n        }\n      },\n    );\n  }\n\n  void _init() async {\n    // get the database controller, row controller and primary field id\n    final databaseController = DatabaseController(view: view);\n    await databaseController.open().fold(\n          (s) => databaseController.setIsLoading(false),\n          (f) => null,\n        );\n    final rowInfo = databaseController.rowCache.getRow(rowId);\n    if (rowInfo == null) {\n      return;\n    }\n    final rowController = RowController(\n      rowMeta: rowInfo.rowMeta,\n      viewId: view.id,\n      rowCache: databaseController.rowCache,\n    );\n    unawaited(rowController.initialize());\n\n    final primaryFieldId =\n        await FieldBackendService.getPrimaryField(viewId: view.id).fold(\n      (primaryField) => primaryField.id,\n      (r) {\n        Log.error(r);\n        return null;\n      },\n    );\n    if (primaryFieldId != null) {\n      add(\n        DatabaseDocumentTitleEvent.didUpdateRowTitleInfo(\n          databaseController,\n          rowController,\n          primaryFieldId,\n        ),\n      );\n    }\n\n    // load ancestors\n    final ancestors = await ViewBackendService.getViewAncestors(view.id)\n        .fold((s) => s.items, (f) => <ViewPB>[]);\n    add(DatabaseDocumentTitleEvent.didUpdateAncestors(ancestors));\n\n    // initialize icon\n    if (rowInfo.rowMeta.icon.isNotEmpty) {\n      add(\n        DatabaseDocumentTitleEvent.didUpdateRowIcon(\n          EmojiIconData.emoji(rowInfo.rowMeta.icon),\n        ),\n      );\n    }\n  }\n\n  /// Update the meta of the row and the view\n  void _updateMeta(String iconURL) {\n    RowBackendService(viewId: view.id)\n        .updateMeta(\n          iconURL: iconURL,\n          rowId: rowId,\n        )\n        .fold((l) => null, (err) => Log.error(err));\n  }\n}\n\n@freezed\nclass DatabaseDocumentTitleEvent with _$DatabaseDocumentTitleEvent {\n  const factory DatabaseDocumentTitleEvent.didUpdateAncestors(\n    List<ViewPB> ancestors,\n  ) = _DidUpdateAncestors;\n\n  const factory DatabaseDocumentTitleEvent.didUpdateRowTitleInfo(\n    DatabaseController databaseController,\n    RowController rowController,\n    String fieldId,\n  ) = _DidUpdateRowTitleInfo;\n\n  const factory DatabaseDocumentTitleEvent.didUpdateRowIcon(\n    EmojiIconData icon,\n  ) = _DidUpdateRowIcon;\n\n  const factory DatabaseDocumentTitleEvent.updateIcon(\n    EmojiIconData icon,\n  ) = _UpdateIcon;\n}\n\n@freezed\nclass DatabaseDocumentTitleState with _$DatabaseDocumentTitleState {\n  const factory DatabaseDocumentTitleState({\n    required List<ViewPB> ancestors,\n    required DatabaseController? databaseController,\n    required RowController? rowController,\n    required String? fieldId,\n    required EmojiIconData? icon,\n  }) = _DatabaseDocumentTitleState;\n\n  factory DatabaseDocumentTitleState.initial() =>\n      const DatabaseDocumentTitleState(\n        ancestors: [],\n        databaseController: null,\n        rowController: null,\n        fieldId: null,\n        icon: null,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/application/doc_sync_state_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/document_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\ntypedef DocumentSyncStateCallback = void Function(\n  DocumentSyncStatePB syncState,\n);\n\nclass DocumentSyncStateListener {\n  DocumentSyncStateListener({\n    required this.id,\n  });\n\n  final String id;\n  StreamSubscription<SubscribeObject>? _subscription;\n  DocumentNotificationParser? _parser;\n  DocumentSyncStateCallback? didReceiveSyncState;\n\n  void start({\n    DocumentSyncStateCallback? didReceiveSyncState,\n  }) {\n    this.didReceiveSyncState = didReceiveSyncState;\n\n    _parser = DocumentNotificationParser(\n      id: id,\n      callback: _callback,\n    );\n    _subscription = RustStreamReceiver.listen(\n      (observable) => _parser?.parse(observable),\n    );\n  }\n\n  void _callback(\n    DocumentNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case DocumentNotification.DidUpdateDocumentSyncState:\n        result.map(\n          (r) {\n            final value = DocumentSyncStatePB.fromBuffer(r);\n            didReceiveSyncState?.call(value);\n          },\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _subscription?.cancel();\n    _subscription = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/application/document_appearance_cubit.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/util/color_to_hex_string.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass DocumentAppearance {\n  const DocumentAppearance({\n    required this.fontSize,\n    required this.fontFamily,\n    required this.codeFontFamily,\n    required this.width,\n    this.cursorColor,\n    this.selectionColor,\n    this.defaultTextDirection,\n  });\n\n  final double fontSize;\n  final String fontFamily;\n  final String codeFontFamily;\n  final Color? cursorColor;\n  final Color? selectionColor;\n  final String? defaultTextDirection;\n  final double width;\n\n  /// For nullable fields (like `cursorColor`),\n  /// use the corresponding `isNull` flag (like `cursorColorIsNull`) to explicitly set the field to `null`.\n  ///\n  /// This is necessary because simply passing `null` as the value does not distinguish between wanting to\n  /// set the field to `null` and not wanting to update the field at all.\n  DocumentAppearance copyWith({\n    double? fontSize,\n    String? fontFamily,\n    String? codeFontFamily,\n    Color? cursorColor,\n    Color? selectionColor,\n    String? defaultTextDirection,\n    bool cursorColorIsNull = false,\n    bool selectionColorIsNull = false,\n    bool textDirectionIsNull = false,\n    double? width,\n  }) {\n    return DocumentAppearance(\n      fontSize: fontSize ?? this.fontSize,\n      fontFamily: fontFamily ?? this.fontFamily,\n      codeFontFamily: codeFontFamily ?? this.codeFontFamily,\n      cursorColor: cursorColorIsNull ? null : cursorColor ?? this.cursorColor,\n      selectionColor:\n          selectionColorIsNull ? null : selectionColor ?? this.selectionColor,\n      defaultTextDirection: textDirectionIsNull\n          ? null\n          : defaultTextDirection ?? this.defaultTextDirection,\n      width: width ?? this.width,\n    );\n  }\n}\n\nclass DocumentAppearanceCubit extends Cubit<DocumentAppearance> {\n  DocumentAppearanceCubit()\n      : super(\n          DocumentAppearance(\n            fontSize: 16.0,\n            fontFamily: defaultFontFamily,\n            codeFontFamily: builtInCodeFontFamily,\n            width: UniversalPlatform.isMobile\n                ? double.infinity\n                : EditorStyleCustomizer.maxDocumentWidth,\n          ),\n        );\n\n  Future<void> fetch() async {\n    final prefs = await SharedPreferences.getInstance();\n    final fontSize =\n        prefs.getDouble(KVKeys.kDocumentAppearanceFontSize) ?? 16.0;\n    final fontFamily = prefs.getString(KVKeys.kDocumentAppearanceFontFamily) ??\n        defaultFontFamily;\n    final defaultTextDirection =\n        prefs.getString(KVKeys.kDocumentAppearanceDefaultTextDirection);\n\n    final cursorColorString =\n        prefs.getString(KVKeys.kDocumentAppearanceCursorColor);\n    final selectionColorString =\n        prefs.getString(KVKeys.kDocumentAppearanceSelectionColor);\n    final cursorColor =\n        cursorColorString != null ? Color(int.parse(cursorColorString)) : null;\n    final selectionColor = selectionColorString != null\n        ? Color(int.parse(selectionColorString))\n        : null;\n    final double? width = prefs.getDouble(KVKeys.kDocumentAppearanceWidth);\n\n    final textScaleFactor =\n        double.parse(prefs.getString(KVKeys.textScaleFactor) ?? '1.0');\n\n    if (isClosed) {\n      return;\n    }\n\n    emit(\n      state.copyWith(\n        fontSize: fontSize * textScaleFactor,\n        fontFamily: fontFamily,\n        cursorColor: cursorColor,\n        selectionColor: selectionColor,\n        defaultTextDirection: defaultTextDirection,\n        cursorColorIsNull: cursorColor == null,\n        selectionColorIsNull: selectionColor == null,\n        textDirectionIsNull: defaultTextDirection == null,\n        width: width,\n      ),\n    );\n  }\n\n  Future<void> syncFontSize(double fontSize) async {\n    final prefs = await SharedPreferences.getInstance();\n    await prefs.setDouble(KVKeys.kDocumentAppearanceFontSize, fontSize);\n\n    if (!isClosed) {\n      emit(state.copyWith(fontSize: fontSize));\n    }\n  }\n\n  Future<void> syncFontFamily(String fontFamily) async {\n    final prefs = await SharedPreferences.getInstance();\n    await prefs.setString(KVKeys.kDocumentAppearanceFontFamily, fontFamily);\n\n    if (!isClosed) {\n      emit(state.copyWith(fontFamily: fontFamily));\n    }\n  }\n\n  Future<void> syncDefaultTextDirection(String? direction) async {\n    final prefs = await SharedPreferences.getInstance();\n    if (direction == null) {\n      await prefs.remove(KVKeys.kDocumentAppearanceDefaultTextDirection);\n    } else {\n      await prefs.setString(\n        KVKeys.kDocumentAppearanceDefaultTextDirection,\n        direction,\n      );\n    }\n\n    if (!isClosed) {\n      emit(\n        state.copyWith(\n          defaultTextDirection: direction,\n          textDirectionIsNull: direction == null,\n        ),\n      );\n    }\n  }\n\n  Future<void> syncCursorColor(Color? cursorColor) async {\n    final prefs = await SharedPreferences.getInstance();\n\n    if (cursorColor == null) {\n      await prefs.remove(KVKeys.kDocumentAppearanceCursorColor);\n    } else {\n      await prefs.setString(\n        KVKeys.kDocumentAppearanceCursorColor,\n        cursorColor.toHexString(),\n      );\n    }\n\n    if (!isClosed) {\n      emit(\n        state.copyWith(\n          cursorColor: cursorColor,\n          cursorColorIsNull: cursorColor == null,\n        ),\n      );\n    }\n  }\n\n  Future<void> syncSelectionColor(Color? selectionColor) async {\n    final prefs = await SharedPreferences.getInstance();\n\n    if (selectionColor == null) {\n      await prefs.remove(KVKeys.kDocumentAppearanceSelectionColor);\n    } else {\n      await prefs.setString(\n        KVKeys.kDocumentAppearanceSelectionColor,\n        selectionColor.toHexString(),\n      );\n    }\n\n    if (!isClosed) {\n      emit(\n        state.copyWith(\n          selectionColor: selectionColor,\n          selectionColorIsNull: selectionColor == null,\n        ),\n      );\n    }\n  }\n\n  Future<void> syncWidth(double? width) async {\n    final prefs = await SharedPreferences.getInstance();\n\n    width ??= UniversalPlatform.isMobile\n        ? double.infinity\n        : EditorStyleCustomizer.maxDocumentWidth;\n    width = width.clamp(\n      EditorStyleCustomizer.minDocumentWidth,\n      EditorStyleCustomizer.maxDocumentWidth,\n    );\n    await prefs.setDouble(KVKeys.kDocumentAppearanceWidth, width);\n\n    if (!isClosed) {\n      emit(state.copyWith(width: width));\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/application/document_awareness_metadata.dart",
    "content": "// This file is \"main.dart\"\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'document_awareness_metadata.freezed.dart';\npart 'document_awareness_metadata.g.dart';\n\n@freezed\nclass DocumentAwarenessMetadata with _$DocumentAwarenessMetadata {\n  const factory DocumentAwarenessMetadata({\n    // ignore: invalid_annotation_target\n    @JsonKey(name: 'cursor_color') required String cursorColor,\n    // ignore: invalid_annotation_target\n    @JsonKey(name: 'selection_color') required String selectionColor,\n    // ignore: invalid_annotation_target\n    @JsonKey(name: 'user_name') required String userName,\n    // ignore: invalid_annotation_target\n    @JsonKey(name: 'user_avatar') required String userAvatar,\n  }) = _DocumentAwarenessMetadata;\n\n  factory DocumentAwarenessMetadata.fromJson(Map<String, Object?> json) =>\n      _$DocumentAwarenessMetadataFromJson(json);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:appflowy/plugins/document/application/doc_sync_state_listener.dart';\nimport 'package:appflowy/plugins/document/application/document_awareness_metadata.dart';\nimport 'package:appflowy/plugins/document/application/document_collab_adapter.dart';\nimport 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';\nimport 'package:appflowy/plugins/document/application/document_listener.dart';\nimport 'package:appflowy/plugins/document/application/document_rules.dart';\nimport 'package:appflowy/plugins/document/application/document_service.dart';\nimport 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';\nimport 'package:appflowy/plugins/trash/application/trash_service.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/startup/tasks/app_widget.dart';\nimport 'package:appflowy/startup/tasks/device_info_task.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/util/color_generator/color_generator.dart';\nimport 'package:appflowy/util/color_to_hex_string.dart';\nimport 'package:appflowy/util/debounce.dart';\nimport 'package:appflowy/util/throttle.dart';\nimport 'package:appflowy/workspace/application/view/view_listener.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart'\n    show AppFlowyEditorLogLevel, EditorState, TransactionTime;\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'document_bloc.freezed.dart';\n\n/// Enable this flag to enable the internal log for\n/// - document diff\n/// - document integrity check\n/// - document sync state\n/// - document awareness states\nbool enableDocumentInternalLog = false;\n\nfinal Map<String, DocumentBloc> _documentBlocMap = {};\n\nclass DocumentBloc extends Bloc<DocumentEvent, DocumentState> {\n  DocumentBloc({\n    required this.documentId,\n    this.databaseViewId,\n    this.rowId,\n    bool saveToBlocMap = true,\n  })  : _saveToBlocMap = saveToBlocMap,\n        _documentListener = DocumentListener(id: documentId),\n        _syncStateListener = DocumentSyncStateListener(id: documentId),\n        super(DocumentState.initial()) {\n    _viewListener = databaseViewId == null && rowId == null\n        ? ViewListener(viewId: documentId)\n        : null;\n    on<DocumentEvent>(_onDocumentEvent);\n  }\n\n  static DocumentBloc? findOpen(String documentId) =>\n      _documentBlocMap[documentId];\n\n  /// For a normal document, the document id is the same as the view id\n  final String documentId;\n\n  final String? databaseViewId;\n  final String? rowId;\n\n  final bool _saveToBlocMap;\n\n  final DocumentListener _documentListener;\n  final DocumentSyncStateListener _syncStateListener;\n  late final ViewListener? _viewListener;\n\n  final DocumentService _documentService = DocumentService();\n  final TrashService _trashService = TrashService();\n\n  late DocumentCollabAdapter _documentCollabAdapter;\n\n  late final TransactionAdapter _transactionAdapter = TransactionAdapter(\n    documentId: documentId,\n    documentService: _documentService,\n  );\n\n  late final DocumentRules _documentRules;\n\n  StreamSubscription? _transactionSubscription;\n\n  bool isClosing = false;\n\n  static const _syncDuration = Duration(milliseconds: 250);\n  final _updateSelectionDebounce = Debounce(duration: _syncDuration);\n  final _syncThrottle = Throttler(duration: _syncDuration);\n\n  // The conflict handle logic is not fully implemented yet\n  // use the syncTimer to force to reload the document state when the conflict happens.\n  Timer? _syncTimer;\n\n  bool get isLocalMode {\n    final userProfilePB = state.userProfilePB;\n    final type = userProfilePB?.workspaceType ?? WorkspaceTypePB.LocalW;\n    return type == WorkspaceTypePB.LocalW;\n  }\n\n  @override\n  Future<void> close() async {\n    isClosing = true;\n    if (_saveToBlocMap) {\n      _documentBlocMap.remove(documentId);\n    }\n    await checkDocumentIntegrity();\n    await _cancelSubscriptions();\n    _clearEditorState();\n    return super.close();\n  }\n\n  Future<void> _cancelSubscriptions() async {\n    await _documentService.syncAwarenessStates(documentId: documentId);\n    await _documentListener.stop();\n    await _syncStateListener.stop();\n    await _viewListener?.stop();\n    await _transactionSubscription?.cancel();\n    await _documentService.closeDocument(viewId: documentId);\n  }\n\n  void _clearEditorState() {\n    _updateSelectionDebounce.dispose();\n    _syncThrottle.dispose();\n\n    _syncTimer?.cancel();\n    _syncTimer = null;\n    state.editorState?.selectionNotifier\n        .removeListener(_debounceOnSelectionUpdate);\n    state.editorState?.service.keyboardService?.closeKeyboard();\n    state.editorState?.dispose();\n  }\n\n  Future<void> _onDocumentEvent(\n    DocumentEvent event,\n    Emitter<DocumentState> emit,\n  ) async {\n    await event.when(\n      initial: () async {\n        if (_saveToBlocMap) {\n          _documentBlocMap[documentId] = this;\n        }\n        final result = await _fetchDocumentState();\n        _onViewChanged();\n        _onDocumentChanged();\n        final newState = await result.fold(\n          (s) async {\n            final userProfilePB =\n                await getIt<AuthService>().getUser().toNullable();\n            return state.copyWith(\n              error: null,\n              editorState: s,\n              isLoading: false,\n              userProfilePB: userProfilePB,\n            );\n          },\n          (f) async => state.copyWith(\n            error: f,\n            editorState: null,\n            isLoading: false,\n          ),\n        );\n        emit(newState);\n        if (newState.userProfilePB != null) {\n          await _updateCollaborator();\n        }\n      },\n      moveToTrash: () async {\n        emit(state.copyWith(isDeleted: true));\n      },\n      restore: () async {\n        emit(state.copyWith(isDeleted: false));\n      },\n      deletePermanently: () async {\n        if (databaseViewId == null && rowId == null) {\n          final result = await _trashService.deleteViews([documentId]);\n          final forceClose = result.fold((l) => true, (r) => false);\n          emit(state.copyWith(forceClose: forceClose));\n        }\n      },\n      restorePage: () async {\n        if (databaseViewId == null && rowId == null) {\n          final result = await TrashService.putback(documentId);\n          final isDeleted = result.fold((l) => false, (r) => true);\n          emit(state.copyWith(isDeleted: isDeleted));\n        }\n      },\n      syncStateChanged: (syncState) {\n        emit(state.copyWith(syncState: syncState.value));\n      },\n      clearAwarenessStates: () async {\n        // sync a null selection and a null meta to clear the awareness states\n        await _documentService.syncAwarenessStates(\n          documentId: documentId,\n        );\n      },\n      syncAwarenessStates: () async {\n        await _updateCollaborator();\n      },\n    );\n  }\n\n  /// subscribe to the view(document page) change\n  void _onViewChanged() {\n    _viewListener?.start(\n      onViewMoveToTrash: (r) {\n        r.map((r) => add(const DocumentEvent.moveToTrash()));\n      },\n      onViewDeleted: (r) {\n        r.map((r) => add(const DocumentEvent.moveToTrash()));\n      },\n      onViewRestored: (r) => r.map((r) => add(const DocumentEvent.restore())),\n    );\n  }\n\n  /// subscribe to the document content change\n  void _onDocumentChanged() {\n    _documentListener.start(\n      onDocEventUpdate: _throttleSyncDoc,\n      onDocAwarenessUpdate: _onAwarenessStatesUpdate,\n    );\n\n    _syncStateListener.start(\n      didReceiveSyncState: (syncState) {\n        if (!isClosed) {\n          add(DocumentEvent.syncStateChanged(syncState));\n        }\n      },\n    );\n  }\n\n  /// Fetch document\n  Future<FlowyResult<EditorState?, FlowyError>> _fetchDocumentState() async {\n    final result = await _documentService.openDocument(documentId: documentId);\n    return result.fold(\n      (s) async => FlowyResult.success(await _initAppFlowyEditorState(s)),\n      (e) => FlowyResult.failure(e),\n    );\n  }\n\n  Future<EditorState?> _initAppFlowyEditorState(DocumentDataPB data) async {\n    if (enableDocumentInternalLog) {\n      Log.info('document data: ${data.toProto3Json()}');\n    }\n\n    final document = data.toDocument();\n    if (document == null) {\n      assert(false, 'document is null');\n      return null;\n    }\n\n    final editorState = EditorState(document: document);\n\n    _documentCollabAdapter = DocumentCollabAdapter(editorState, documentId);\n    _documentRules = DocumentRules(editorState: editorState);\n\n    // subscribe to the document change from the editor\n    _transactionSubscription = editorState.transactionStream.listen(\n      (value) async {\n        final time = value.$1;\n        final transaction = value.$2;\n        final options = value.$3;\n        if (time != TransactionTime.before) {\n          return;\n        }\n\n        if (options.inMemoryUpdate) {\n          if (enableDocumentInternalLog) {\n            Log.trace('skip transaction for in-memory update');\n          }\n          return;\n        }\n\n        if (enableDocumentInternalLog) {\n          Log.trace(\n            '[TransactionAdapter] 1. transaction before apply: ${transaction.hashCode}',\n          );\n        }\n\n        // apply transaction to backend\n        await _transactionAdapter.apply(transaction, editorState);\n\n        // check if the document is empty.\n        await _documentRules.applyRules(value: value);\n\n        if (enableDocumentInternalLog) {\n          Log.trace(\n            '[TransactionAdapter] 4. transaction after apply: ${transaction.hashCode}',\n          );\n        }\n\n        if (!isClosed) {\n          // ignore: invalid_use_of_visible_for_testing_member\n          emit(state.copyWith(isDocumentEmpty: editorState.document.isEmpty));\n        }\n      },\n    );\n\n    editorState.selectionNotifier.addListener(_debounceOnSelectionUpdate);\n\n    // output the log from the editor when debug mode\n    if (kDebugMode) {\n      editorState.logConfiguration\n        ..level = AppFlowyEditorLogLevel.all\n        ..handler = (log) {\n          if (enableDocumentInternalLog) {\n            // Log.info(log);\n          }\n        };\n    }\n\n    return editorState;\n  }\n\n  Future<void> _onDocumentStateUpdate(DocEventPB docEvent) async {\n    if (!docEvent.isRemote || !FeatureFlag.syncDocument.isOn) {\n      return;\n    }\n\n    unawaited(_documentCollabAdapter.syncV3(docEvent: docEvent));\n  }\n\n  Future<void> _onAwarenessStatesUpdate(\n    DocumentAwarenessStatesPB awarenessStates,\n  ) async {\n    if (!FeatureFlag.syncDocument.isOn) {\n      return;\n    }\n\n    final userId = state.userProfilePB?.id;\n    if (userId != null) {\n      await _documentCollabAdapter.updateRemoteSelection(\n        userId.toString(),\n        awarenessStates,\n      );\n    }\n  }\n\n  void _debounceOnSelectionUpdate() {\n    _updateSelectionDebounce.call(_onSelectionUpdate);\n  }\n\n  void _throttleSyncDoc(DocEventPB docEvent) {\n    if (enableDocumentInternalLog) {\n      Log.info('[DocumentBloc] throttle sync doc: ${docEvent.toProto3Json()}');\n    }\n    _syncThrottle.call(() {\n      _onDocumentStateUpdate(docEvent);\n    });\n  }\n\n  Future<void> _onSelectionUpdate() async {\n    if (isClosing) {\n      return;\n    }\n    final user = state.userProfilePB;\n    final deviceId = ApplicationInfo.deviceId;\n    if (!FeatureFlag.syncDocument.isOn || user == null) {\n      return;\n    }\n\n    final editorState = state.editorState;\n    if (editorState == null) {\n      return;\n    }\n    final selection = editorState.selection;\n\n    // sync the selection\n    final id = user.id.toString() + deviceId;\n    final basicColor = ColorGenerator(id.toString()).toColor();\n    final metadata = DocumentAwarenessMetadata(\n      cursorColor: basicColor.toHexString(),\n      selectionColor: basicColor.withValues(alpha: 0.6).toHexString(),\n      userName: user.name,\n      userAvatar: user.iconUrl,\n    );\n    await _documentService.syncAwarenessStates(\n      documentId: documentId,\n      selection: selection,\n      metadata: jsonEncode(metadata.toJson()),\n    );\n  }\n\n  Future<void> _updateCollaborator() async {\n    final user = state.userProfilePB;\n    final deviceId = ApplicationInfo.deviceId;\n    if (!FeatureFlag.syncDocument.isOn || user == null) {\n      return;\n    }\n\n    // sync the selection\n    final id = user.id.toString() + deviceId;\n    final basicColor = ColorGenerator(id.toString()).toColor();\n    final metadata = DocumentAwarenessMetadata(\n      cursorColor: basicColor.toHexString(),\n      selectionColor: basicColor.withValues(alpha: 0.6).toHexString(),\n      userName: user.name,\n      userAvatar: user.iconUrl,\n    );\n    await _documentService.syncAwarenessStates(\n      documentId: documentId,\n      metadata: jsonEncode(metadata.toJson()),\n    );\n  }\n\n  Future<void> forceReloadDocumentState() {\n    return _documentCollabAdapter.syncV3();\n  }\n\n  // this is only used for debug mode\n  Future<void> checkDocumentIntegrity() async {\n    if (!enableDocumentInternalLog) {\n      return;\n    }\n\n    final cloudDocResult =\n        await _documentService.getDocument(documentId: documentId);\n    final cloudDoc = cloudDocResult.fold((s) => s, (f) => null)?.toDocument();\n    final localDoc = state.editorState?.document;\n    if (cloudDoc == null || localDoc == null) {\n      return;\n    }\n    final cloudJson = cloudDoc.toJson();\n    final localJson = localDoc.toJson();\n    final deepEqual = const DeepCollectionEquality().equals(\n      cloudJson,\n      localJson,\n    );\n    if (!deepEqual) {\n      Log.error('document integrity check failed');\n      // Enable it to debug the document integrity check failed\n      Log.error('cloud doc: $cloudJson');\n      Log.error('local doc: $localJson');\n\n      final context = AppGlobals.rootNavKey.currentContext;\n      if (context != null && context.mounted) {\n        showToastNotification(\n          message: 'document integrity check failed',\n          type: ToastificationType.error,\n        );\n      }\n    }\n  }\n}\n\n@freezed\nclass DocumentEvent with _$DocumentEvent {\n  const factory DocumentEvent.initial() = Initial;\n  const factory DocumentEvent.moveToTrash() = MoveToTrash;\n  const factory DocumentEvent.restore() = Restore;\n  const factory DocumentEvent.restorePage() = RestorePage;\n  const factory DocumentEvent.deletePermanently() = DeletePermanently;\n  const factory DocumentEvent.syncStateChanged(\n    final DocumentSyncStatePB syncState,\n  ) = syncStateChanged;\n  const factory DocumentEvent.syncAwarenessStates() = SyncAwarenessStates;\n  const factory DocumentEvent.clearAwarenessStates() = ClearAwarenessStates;\n}\n\n@freezed\nclass DocumentState with _$DocumentState {\n  const factory DocumentState({\n    required final bool isDeleted,\n    required final bool forceClose,\n    required final bool isLoading,\n    required final DocumentSyncState syncState,\n    bool? isDocumentEmpty,\n    UserProfilePB? userProfilePB,\n    EditorState? editorState,\n    FlowyError? error,\n    @Default(null) DocumentAwarenessStatesPB? awarenessStates,\n  }) = _DocumentState;\n\n  factory DocumentState.initial() => const DocumentState(\n        isDeleted: false,\n        forceClose: false,\n        isLoading: true,\n        syncState: DocumentSyncState.Syncing,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/plugins/document/application/document_awareness_metadata.dart';\nimport 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';\nimport 'package:appflowy/plugins/document/application/document_diff.dart';\nimport 'package:appflowy/plugins/document/application/prelude.dart';\nimport 'package:appflowy/shared/list_extension.dart';\nimport 'package:appflowy/startup/tasks/device_info_task.dart';\nimport 'package:appflowy/util/color_generator/color_generator.dart';\nimport 'package:appflowy/util/json_print.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass DocumentCollabAdapter {\n  DocumentCollabAdapter(\n    this.editorState,\n    this.docId,\n  );\n\n  final EditorState editorState;\n  final String docId;\n  final DocumentDiff diff = const DocumentDiff();\n\n  final _service = DocumentService();\n\n  /// Sync version 1\n  ///\n  /// Force to reload the document\n  ///\n  /// Only use in development\n  Future<EditorState?> syncV1() async {\n    final result = await _service.getDocument(documentId: docId);\n    final document = result.fold((s) => s.toDocument(), (f) => null);\n    if (document == null) {\n      return null;\n    }\n    return EditorState(document: document);\n  }\n\n  /// Sync version 2\n  ///\n  /// Translate the [docEvent] from yrs to [Operation]s and apply it to the [editorState]\n  ///\n  /// Not fully implemented yet\n  Future<void> syncV2(DocEventPB docEvent) async {\n    prettyPrintJson(docEvent.toProto3Json());\n\n    final transaction = editorState.transaction;\n\n    for (final event in docEvent.events) {\n      for (final blockEvent in event.event) {\n        switch (blockEvent.command) {\n          case DeltaTypePB.Inserted:\n            break;\n          case DeltaTypePB.Updated:\n            await _syncUpdated(blockEvent, transaction);\n            break;\n          case DeltaTypePB.Removed:\n            break;\n          default:\n        }\n      }\n    }\n\n    await editorState.apply(transaction, isRemote: true);\n  }\n\n  /// Sync version 3\n  ///\n  /// Diff the local document with the remote document and apply the changes\n  Future<void> syncV3({DocEventPB? docEvent}) async {\n    final result = await _service.getDocument(documentId: docId);\n    final document = result.fold((s) => s.toDocument(), (f) => null);\n    if (document == null) {\n      return;\n    }\n\n    final ops = diff.diffDocument(editorState.document, document);\n    if (ops.isEmpty) {\n      return;\n    }\n\n    if (enableDocumentInternalLog) {\n      prettyPrintJson(ops.map((op) => op.toJson()).toList());\n    }\n\n    final transaction = editorState.transaction;\n    for (final op in ops) {\n      transaction.add(op);\n    }\n    await editorState.apply(transaction, isRemote: true);\n\n    if (enableDocumentInternalLog) {\n      assert(() {\n        final local = editorState.document.root.toJson();\n        final remote = document.root.toJson();\n        if (!const DeepCollectionEquality().equals(local, remote)) {\n          Log.error('Invalid diff status');\n          Log.error('Local: $local');\n          Log.error('Remote: $remote');\n          return false;\n        }\n        return true;\n      }());\n    }\n  }\n\n  Future<void> forceReload() async {\n    final result = await _service.getDocument(documentId: docId);\n    final document = result.fold((s) => s.toDocument(), (f) => null);\n    if (document == null) {\n      return;\n    }\n\n    final beforeSelection = editorState.selection;\n\n    final clear = editorState.transaction;\n    clear.deleteNodes(editorState.document.root.children);\n    await editorState.apply(clear, isRemote: true);\n\n    final insert = editorState.transaction;\n    insert.insertNodes([0], document.root.children);\n    await editorState.apply(insert, isRemote: true);\n\n    editorState.selection = beforeSelection;\n  }\n\n  Future<void> _syncUpdated(\n    BlockEventPayloadPB payload,\n    Transaction transaction,\n  ) async {\n    assert(payload.command == DeltaTypePB.Updated);\n\n    final path = payload.path;\n    final id = payload.id;\n    final value = jsonDecode(payload.value);\n\n    final nodes = NodeIterator(\n      document: editorState.document,\n      startNode: editorState.document.root,\n    ).toList();\n\n    // 1. meta -> text_map = text delta change\n    if (path.isTextDeltaChangeset) {\n      // find the 'text' block and apply the delta\n      // ⚠️ not completed yet.\n      final target = nodes.singleWhereOrNull((n) => n.id == id);\n      if (target != null) {\n        try {\n          final delta = Delta.fromJson(jsonDecode(value));\n          transaction.insertTextDelta(target, 0, delta);\n        } catch (e) {\n          Log.error('Failed to apply delta: $value, error: $e');\n        }\n      }\n    } else if (path.isBlockChangeset) {\n      final target = nodes.singleWhereOrNull((n) => n.id == id);\n      if (target != null) {\n        try {\n          final delta = jsonDecode(value['data'])['delta'];\n          transaction.updateNode(target, {\n            'delta': Delta.fromJson(delta).toJson(),\n          });\n        } catch (e) {\n          Log.error('Failed to update $value, error: $e');\n        }\n      }\n    }\n  }\n\n  Future<void> updateRemoteSelection(\n    String userId,\n    DocumentAwarenessStatesPB states,\n  ) async {\n    final List<RemoteSelection> remoteSelections = [];\n    final deviceId = ApplicationInfo.deviceId;\n    // the values may be duplicated, sort by the timestamp and then filter the duplicated values\n    final values = states.value.values\n        .sorted(\n          (a, b) => b.timestamp.compareTo(a.timestamp),\n        ) // in descending order\n        .unique(\n          (e) => Object.hashAll([e.user.uid, e.user.deviceId]),\n        );\n    for (final state in values) {\n      // the following code is only for version 1\n      if (state.version != 1 || state.metadata.isEmpty) {\n        continue;\n      }\n      final uid = state.user.uid.toString();\n      final did = state.user.deviceId;\n\n      DocumentAwarenessMetadata metadata;\n      try {\n        metadata = DocumentAwarenessMetadata.fromJson(\n          jsonDecode(state.metadata),\n        );\n      } catch (e) {\n        Log.error('Failed to parse metadata: $e, ${state.metadata}');\n        continue;\n      }\n      final selectionColor = metadata.selectionColor.tryToColor();\n      final cursorColor = metadata.cursorColor.tryToColor();\n      if ((uid == userId && did == deviceId) ||\n          (cursorColor == null || selectionColor == null)) {\n        continue;\n      }\n      final start = state.selection.start;\n      final end = state.selection.end;\n      final selection = Selection(\n        start: Position(\n          path: start.path.toIntList(),\n          offset: start.offset.toInt(),\n        ),\n        end: Position(\n          path: end.path.toIntList(),\n          offset: end.offset.toInt(),\n        ),\n      );\n      final color = ColorGenerator(uid + did).toColor();\n      final remoteSelection = RemoteSelection(\n        id: uid,\n        selection: selection,\n        selectionColor: selectionColor,\n        cursorColor: cursorColor,\n        builder: (_, __, rect) {\n          return Positioned(\n            top: rect.top - 14,\n            left: selection.isCollapsed ? rect.right : rect.left,\n            child: ColoredBox(\n              color: color,\n              child: Padding(\n                padding: const EdgeInsets.symmetric(\n                  horizontal: 2.0,\n                  vertical: 1.0,\n                ),\n                child: FlowyText(\n                  metadata.userName,\n                  color: Colors.black,\n                  fontSize: 12.0,\n                ),\n              ),\n            ),\n          );\n        },\n      );\n      remoteSelections.add(remoteSelection);\n    }\n\n    editorState.remoteSelections.value = remoteSelections;\n  }\n}\n\nextension on List<Int64> {\n  List<int> toIntList() {\n    return map((e) => e.toInt()).toList();\n  }\n}\n\nextension on List<String> {\n  bool get isTextDeltaChangeset {\n    return length == 3 && this[0] == 'meta' && this[1] == 'text_map';\n  }\n\n  bool get isBlockChangeset {\n    return length == 2 && this[0] == 'blocks';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:appflowy/plugins/document/application/document_awareness_metadata.dart';\nimport 'package:appflowy/plugins/document/application/document_listener.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/startup/tasks/device_info_task.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'document_collaborators_bloc.freezed.dart';\n\nbool _filterCurrentUser = false;\n\nclass DocumentCollaboratorsBloc\n    extends Bloc<DocumentCollaboratorsEvent, DocumentCollaboratorsState> {\n  DocumentCollaboratorsBloc({\n    required this.view,\n  })  : _listener = DocumentListener(id: view.id),\n        super(DocumentCollaboratorsState.initial()) {\n    on<DocumentCollaboratorsEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            final result = await getIt<AuthService>().getUser();\n            final userProfile = result.fold((s) => s, (f) => null);\n            emit(\n              state.copyWith(\n                shouldShowIndicator:\n                    userProfile?.workspaceType == WorkspaceTypePB.ServerW,\n              ),\n            );\n            final deviceId = ApplicationInfo.deviceId;\n            if (userProfile != null) {\n              _listener.start(\n                onDocAwarenessUpdate: (states) {\n                  if (isClosed) {\n                    return;\n                  }\n\n                  add(\n                    DocumentCollaboratorsEvent.update(\n                      userProfile,\n                      deviceId,\n                      states,\n                    ),\n                  );\n                },\n              );\n            }\n          },\n          update: (userProfile, deviceId, states) {\n            final collaborators = _buildCollaborators(\n              userProfile,\n              deviceId,\n              states,\n            );\n            emit(state.copyWith(collaborators: collaborators));\n          },\n        );\n      },\n    );\n  }\n\n  final ViewPB view;\n  final DocumentListener _listener;\n\n  @override\n  Future<void> close() async {\n    await _listener.stop();\n    return super.close();\n  }\n\n  List<DocumentAwarenessMetadata> _buildCollaborators(\n    UserProfilePB userProfile,\n    String deviceId,\n    DocumentAwarenessStatesPB states,\n  ) {\n    final result = <DocumentAwarenessMetadata>[];\n    final ids = <dynamic>{};\n    final sorted = states.value.values.toList()\n      ..sort((a, b) => b.timestamp.compareTo(a.timestamp))\n      // filter the duplicate users\n      ..retainWhere((e) => ids.add(e.user.uid.toString() + e.user.deviceId))\n      // only keep version 1 and metadata is not empty\n      ..retainWhere((e) => e.version == 1)\n      ..retainWhere((e) => e.metadata.isNotEmpty);\n    for (final state in sorted) {\n      if (state.version != 1) {\n        continue;\n      }\n      // filter current user\n      if (_filterCurrentUser &&\n          userProfile.id == state.user.uid &&\n          deviceId == state.user.deviceId) {\n        continue;\n      }\n      try {\n        final metadata = DocumentAwarenessMetadata.fromJson(\n          jsonDecode(state.metadata),\n        );\n        result.add(metadata);\n      } catch (e) {\n        Log.error('Failed to parse metadata: $e');\n      }\n    }\n    return result;\n  }\n}\n\n@freezed\nclass DocumentCollaboratorsEvent with _$DocumentCollaboratorsEvent {\n  const factory DocumentCollaboratorsEvent.initial() = Initial;\n  const factory DocumentCollaboratorsEvent.update(\n    UserProfilePB userProfile,\n    String deviceId,\n    DocumentAwarenessStatesPB states,\n  ) = Update;\n}\n\n@freezed\nclass DocumentCollaboratorsState with _$DocumentCollaboratorsState {\n  const factory DocumentCollaboratorsState({\n    @Default([]) List<DocumentAwarenessMetadata> collaborators,\n    @Default(false) bool shouldShowIndicator,\n  }) = _DocumentCollaboratorsState;\n\n  factory DocumentCollaboratorsState.initial() =>\n      const DocumentCollaboratorsState();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart'\n    show\n        Document,\n        Node,\n        Attributes,\n        Delta,\n        ParagraphBlockKeys,\n        NodeIterator,\n        NodeExternalValues,\n        HeadingBlockKeys,\n        NumberedListBlockKeys,\n        BulletedListBlockKeys,\n        blockComponentDelta;\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:nanoid/nanoid.dart';\n\nclass ExternalValues extends NodeExternalValues {\n  const ExternalValues({\n    required this.externalId,\n    required this.externalType,\n  });\n\n  final String externalId;\n  final String externalType;\n}\n\nextension DocumentDataPBFromTo on DocumentDataPB {\n  static DocumentDataPB? fromDocument(Document document) {\n    final startNode = document.first;\n    final endNode = document.last;\n    if (startNode == null || endNode == null) {\n      return null;\n    }\n    final pageId = document.root.id;\n\n    // generate the block\n    final blocks = <String, BlockPB>{};\n    final nodes = NodeIterator(\n      document: document,\n      startNode: startNode,\n      endNode: endNode,\n    ).toList();\n    for (final node in nodes) {\n      if (blocks.containsKey(node.id)) {\n        assert(false, 'duplicate node id: ${node.id}');\n      }\n      final parentId = node.parent?.id;\n      final childrenId = nanoid(10);\n      blocks[node.id] = node.toBlock(\n        parentId: parentId,\n        childrenId: childrenId,\n      );\n    }\n    // root\n    blocks[pageId] = document.root.toBlock(\n      parentId: '',\n      childrenId: pageId,\n    );\n\n    // generate the meta\n    final childrenMap = <String, ChildrenPB>{};\n    blocks.values.where((e) => e.parentId.isNotEmpty).forEach((value) {\n      final childrenId = blocks[value.parentId]?.childrenId;\n      if (childrenId != null) {\n        childrenMap[childrenId] ??= ChildrenPB.create();\n        childrenMap[childrenId]!.children.add(value.id);\n      }\n    });\n    final meta = MetaPB(childrenMap: childrenMap);\n\n    return DocumentDataPB(\n      blocks: blocks,\n      pageId: pageId,\n      meta: meta,\n    );\n  }\n\n  Document? toDocument() {\n    final rootId = pageId;\n    try {\n      final root = buildNode(rootId);\n\n      if (root != null) {\n        return Document(root: root);\n      }\n\n      return null;\n    } catch (e) {\n      Log.error('create document error: $e');\n      return null;\n    }\n  }\n\n  Node? buildNode(String id) {\n    final block = blocks[id];\n    final childrenId = block?.childrenId;\n    final childrenIds = meta.childrenMap[childrenId]?.children;\n\n    final children = <Node>[];\n    if (childrenIds != null && childrenIds.isNotEmpty) {\n      children.addAll(childrenIds.map((e) => buildNode(e)).nonNulls);\n    }\n\n    final node = block?.toNode(\n      children: children,\n      meta: meta,\n    );\n\n    for (final element in children) {\n      element.parent = node;\n    }\n\n    return node;\n  }\n}\n\nextension BlockToNode on BlockPB {\n  Node toNode({\n    Iterable<Node>? children,\n    required MetaPB meta,\n  }) {\n    final node = Node(\n      id: id,\n      type: ty,\n      attributes: _dataAdapter(ty, data, meta),\n      children: children ?? [],\n    );\n    node.externalValues = ExternalValues(\n      externalId: externalId,\n      externalType: externalType,\n    );\n    return node;\n  }\n\n  Attributes _dataAdapter(String ty, String data, MetaPB meta) {\n    final map = Attributes.from(jsonDecode(data));\n\n    // it used in the delta case now.\n    final externalType = this.externalType;\n    final externalId = this.externalId;\n    if (externalType.isNotEmpty && externalId.isNotEmpty) {\n      // the 'text' type is the only type that is supported now.\n      if (externalType == 'text') {\n        final deltaString = meta.textMap[externalId];\n        if (deltaString != null) {\n          final delta = jsonDecode(deltaString);\n          map[blockComponentDelta] = delta;\n        }\n      }\n    }\n\n    Attributes adapterCallback(Attributes map) => map\n      ..putIfAbsent(\n        blockComponentDelta,\n        () => Delta().toJson(),\n      );\n\n    final adapter = {\n      ParagraphBlockKeys.type: adapterCallback,\n      HeadingBlockKeys.type: adapterCallback,\n      CodeBlockKeys.type: adapterCallback,\n      QuoteBlockKeys.type: adapterCallback,\n      NumberedListBlockKeys.type: adapterCallback,\n      BulletedListBlockKeys.type: adapterCallback,\n      ToggleListBlockKeys.type: adapterCallback,\n    };\n    return adapter[ty]?.call(map) ?? map;\n  }\n}\n\nextension NodeToBlock on Node {\n  BlockPB toBlock({\n    String? parentId,\n    String? childrenId,\n    Attributes? attributes,\n    String? externalId,\n    String? externalType,\n  }) {\n    assert(id.isNotEmpty);\n    final block = BlockPB.create()\n      ..id = id\n      ..ty = type\n      ..data = _dataAdapter(type, attributes ?? this.attributes);\n    if (childrenId != null && childrenId.isNotEmpty) {\n      block.childrenId = childrenId;\n    }\n    if (parentId != null && parentId.isNotEmpty) {\n      block.parentId = parentId;\n    }\n    if (externalId != null && externalId.isNotEmpty) {\n      block.externalId = externalId;\n    }\n    if (externalType != null && externalType.isNotEmpty) {\n      block.externalType = externalType;\n    }\n    return block;\n  }\n\n  String _dataAdapter(String type, Attributes attributes) {\n    try {\n      return jsonEncode(\n        attributes,\n        toEncodable: (value) {\n          if (value is Map) {\n            return jsonEncode(value);\n          }\n          return value;\n        },\n      );\n    } catch (e) {\n      Log.error('encode attributes error: $e');\n      return '{}';\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/application/document_diff.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/foundation.dart';\n\n/// DocumentDiff compares two document states and generates operations needed\n/// to transform one state into another.\nclass DocumentDiff {\n  const DocumentDiff({\n    this.enableDebugLog = false,\n  });\n\n  final bool enableDebugLog;\n\n  // Using DeepCollectionEquality for deep comparison of collections\n  static const _equality = DeepCollectionEquality();\n\n  /// Generates operations needed to transform oldDocument into newDocument.\n  /// Returns a list of operations (Insert, Delete, Update) that can be applied sequentially.\n  List<Operation> diffDocument(Document oldDocument, Document newDocument) {\n    return diffNode(oldDocument.root, newDocument.root);\n  }\n\n  /// Compares two nodes and their children recursively to generate transformation operations.\n  /// Returns a list of operations that will transform oldNode into newNode.\n  List<Operation> diffNode(Node oldNode, Node newNode) {\n    final operations = <Operation>[];\n\n    // Compare and update node attributes if they're different.\n    //  Using DeepCollectionEquality instead of == for deep comparison of collections\n    if (!_equality.equals(oldNode.attributes, newNode.attributes)) {\n      operations.add(\n        UpdateOperation(oldNode.path, newNode.attributes, oldNode.attributes),\n      );\n    }\n\n    final oldChildrenById = Map<String, Node>.fromEntries(\n      oldNode.children.map((child) => MapEntry(child.id, child)),\n    );\n    final newChildrenById = Map<String, Node>.fromEntries(\n      newNode.children.map((child) => MapEntry(child.id, child)),\n    );\n\n    // Insertion or Update\n    for (final newChild in newNode.children) {\n      final oldChild = oldChildrenById[newChild.id];\n      if (oldChild == null) {\n        // If the node doesn't exist in the old document, it's a new node.\n        operations.add(InsertOperation(newChild.path, [newChild]));\n      } else {\n        // If the node exists in the old document, recursively compare its children\n        operations.addAll(diffNode(oldChild, newChild));\n      }\n    }\n\n    // Deletion\n    for (final id in oldChildrenById.keys) {\n      // If the node doesn't exist in the new document, it's a deletion.\n      if (!newChildrenById.containsKey(id)) {\n        final oldChild = oldChildrenById[id]!;\n        operations.add(DeleteOperation(oldChild.path, [oldChild]));\n      }\n    }\n\n    // Optimize operations by merging consecutive inserts and deletes\n    return _optimizeOperations(operations);\n  }\n\n  /// Optimizes the list of operations by merging consecutive operations where possible.\n  /// This reduces the total number of operations that need to be applied.\n  List<Operation> _optimizeOperations(List<Operation> operations) {\n    // Optimize the insert operations first, then the delete operations\n    final optimizedOps = mergeDeleteOperations(\n      mergeInsertOperations(\n        operations,\n      ),\n    );\n    return optimizedOps;\n  }\n\n  /// Merges consecutive insert operations to reduce the number of operations.\n  /// Operations are merged if they target consecutive paths in the document.\n  List<Operation> mergeInsertOperations(List<Operation> operations) {\n    if (enableDebugLog) {\n      _logOperations('mergeInsertOperations[before]', operations);\n    }\n\n    final copy = [...operations];\n    final insertOperations = operations\n        .whereType<InsertOperation>()\n        .sorted(_descendingCompareTo)\n        .toList();\n\n    _mergeConsecutiveOperations<InsertOperation>(\n      insertOperations,\n      (prev, current) => InsertOperation(\n        prev.path,\n        [...prev.nodes, ...current.nodes],\n      ),\n    );\n\n    if (insertOperations.isNotEmpty) {\n      copy\n        ..removeWhere((op) => op is InsertOperation)\n        ..insertAll(0, insertOperations); // Insert ops must be at the start\n    }\n\n    if (enableDebugLog) {\n      _logOperations('mergeInsertOperations[after]', copy);\n    }\n\n    return copy;\n  }\n\n  /// Merges consecutive delete operations to reduce the number of operations.\n  /// Operations are merged if they target consecutive paths in the document.\n  List<Operation> mergeDeleteOperations(List<Operation> operations) {\n    if (enableDebugLog) {\n      _logOperations('mergeDeleteOperations[before]', operations);\n    }\n\n    final copy = [...operations];\n    final deleteOperations = operations\n        .whereType<DeleteOperation>()\n        .sorted(_descendingCompareTo)\n        .toList();\n\n    _mergeConsecutiveOperations<DeleteOperation>(\n      deleteOperations,\n      (prev, current) => DeleteOperation(\n        prev.path,\n        [...prev.nodes, ...current.nodes],\n      ),\n    );\n\n    if (deleteOperations.isNotEmpty) {\n      copy\n        ..removeWhere((op) => op is DeleteOperation)\n        ..addAll(deleteOperations); // Delete ops must be at the end\n    }\n\n    if (enableDebugLog) {\n      _logOperations('mergeDeleteOperations[after]', copy);\n    }\n\n    return copy;\n  }\n\n  /// Merge consecutive operations of the same type\n  void _mergeConsecutiveOperations<T extends Operation>(\n    List<T> operations,\n    T Function(T prev, T current) merge,\n  ) {\n    for (var i = operations.length - 1; i > 0; i--) {\n      final op = operations[i];\n      final previousOp = operations[i - 1];\n\n      if (op.path.equals(previousOp.path.next)) {\n        operations\n          ..removeAt(i)\n          ..[i - 1] = merge(previousOp, op);\n      }\n    }\n  }\n\n  void _logOperations(String prefix, List<Operation> operations) {\n    debugPrint('$prefix: ${operations.map((op) => op.toJson()).toList()}');\n  }\n\n  int _descendingCompareTo(Operation a, Operation b) {\n    return a.path > b.path ? 1 : -1;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/application/document_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/document_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\ntypedef OnDocumentEventUpdate = void Function(DocEventPB docEvent);\ntypedef OnDocumentAwarenessStateUpdate = void Function(\n  DocumentAwarenessStatesPB awarenessStates,\n);\n\nclass DocumentListener {\n  DocumentListener({\n    required this.id,\n  });\n\n  final String id;\n\n  StreamSubscription<SubscribeObject>? _subscription;\n  DocumentNotificationParser? _parser;\n\n  OnDocumentEventUpdate? _onDocEventUpdate;\n  OnDocumentAwarenessStateUpdate? _onDocAwarenessUpdate;\n\n  void start({\n    OnDocumentEventUpdate? onDocEventUpdate,\n    OnDocumentAwarenessStateUpdate? onDocAwarenessUpdate,\n  }) {\n    _onDocEventUpdate = onDocEventUpdate;\n    _onDocAwarenessUpdate = onDocAwarenessUpdate;\n\n    _parser = DocumentNotificationParser(\n      id: id,\n      callback: _callback,\n    );\n    _subscription = RustStreamReceiver.listen(\n      (observable) => _parser?.parse(observable),\n    );\n  }\n\n  void _callback(\n    DocumentNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case DocumentNotification.DidReceiveUpdate:\n        result.map(\n          (s) => _onDocEventUpdate?.call(DocEventPB.fromBuffer(s)),\n        );\n        break;\n      case DocumentNotification.DidUpdateDocumentAwarenessState:\n        result.map(\n          (s) => _onDocAwarenessUpdate?.call(\n            DocumentAwarenessStatesPB.fromBuffer(s),\n          ),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    _onDocAwarenessUpdate = null;\n    _onDocEventUpdate = null;\n    await _subscription?.cancel();\n    _subscription = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/application/document_rules.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\n/// Apply rules to the document\n///\n/// 1. ensure there is at least one paragraph in the document, otherwise the user will be blocked from typing\n/// 2. remove columns block if its children are empty\nclass DocumentRules {\n  DocumentRules({\n    required this.editorState,\n  });\n\n  final EditorState editorState;\n\n  Future<void> applyRules({\n    required EditorTransactionValue value,\n  }) async {\n    await Future.wait([\n      _ensureAtLeastOneParagraphExists(value: value),\n      _removeColumnIfItIsEmpty(value: value),\n    ]);\n  }\n\n  Future<void> _ensureAtLeastOneParagraphExists({\n    required EditorTransactionValue value,\n  }) async {\n    final document = editorState.document;\n    if (document.root.children.isEmpty) {\n      final transaction = editorState.transaction;\n      transaction\n        ..insertNode([0], paragraphNode())\n        ..afterSelection = Selection.collapsed(\n          Position(path: [0]),\n        );\n      await editorState.apply(transaction);\n    }\n  }\n\n  Future<void> _removeColumnIfItIsEmpty({\n    required EditorTransactionValue value,\n  }) async {\n    final transaction = value.$2;\n    final options = value.$3;\n\n    if (options.inMemoryUpdate) {\n      return;\n    }\n\n    for (final operation in transaction.operations) {\n      final deleteColumnsTransaction = editorState.transaction;\n      if (operation is DeleteOperation) {\n        final path = operation.path;\n        final column = editorState.document.nodeAtPath(path.parent);\n        if (column != null && column.type == SimpleColumnBlockKeys.type) {\n          // check if the column is empty\n          final children = column.children;\n          if (children.isEmpty) {\n            // delete the column or the columns\n            final columns = column.parent;\n\n            if (columns != null &&\n                columns.type == SimpleColumnsBlockKeys.type) {\n              final nonEmptyColumnCount = columns.children.fold(\n                0,\n                (p, c) => c.children.isEmpty ? p : p + 1,\n              );\n\n              // Example:\n              // columns\n              //  - column 1\n              //    - paragraph 1-1\n              //    - paragraph 1-2\n              //  - column 2\n              //    - paragraph 2\n              //  - column 3\n              //    - paragraph 3\n              //\n              // case 1: delete the paragraph 3 from column 3.\n              // because there is only one child in column 3, we should delete the column 3 as well.\n              // the result should be:\n              // columns\n              //  - column 1\n              //    - paragraph 1-1\n              //    - paragraph 1-2\n              //  - column 2\n              //    - paragraph 2\n              //\n              // case 2: delete the paragraph 3 from column 3 and delete the paragraph 2 from column 2.\n              // in this case, there will be only one column left, so we should delete the columns block and flatten the children.\n              // the result should be:\n              // paragraph 1-1\n              // paragraph 1-2\n\n              // if there is only one empty column left, delete the columns block and flatten the children\n              if (nonEmptyColumnCount <= 1) {\n                // move the children in columns out of the column\n                final children = columns.children\n                    .map((e) => e.children)\n                    .expand((e) => e)\n                    .map((e) => e.deepCopy())\n                    .toList();\n                deleteColumnsTransaction.insertNodes(columns.path, children);\n                deleteColumnsTransaction.deleteNode(columns);\n              } else {\n                // otherwise, delete the column\n                deleteColumnsTransaction.deleteNode(column);\n\n                final deletedColumnRatio =\n                    column.attributes[SimpleColumnBlockKeys.ratio];\n                if (deletedColumnRatio != null) {\n                  // update the ratio of the columns\n                  final columnsNode = column.columnsParent;\n                  if (columnsNode != null) {\n                    final length = columnsNode.children.length;\n                    for (final columnNode in columnsNode.children) {\n                      final ratio =\n                          columnNode.attributes[SimpleColumnBlockKeys.ratio] ??\n                              1.0 / length;\n                      if (ratio != null) {\n                        deleteColumnsTransaction.updateNode(columnNode, {\n                          ...columnNode.attributes,\n                          SimpleColumnBlockKeys.ratio:\n                              ratio + deletedColumnRatio / (length - 1),\n                        });\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n\n      if (deleteColumnsTransaction.operations.isNotEmpty) {\n        await editorState.apply(deleteColumnsTransaction);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/application/document_service.dart",
    "content": "import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:fixnum/fixnum.dart';\n\nclass DocumentService {\n  // unused now.\n  Future<FlowyResult<void, FlowyError>> createDocument({\n    required ViewPB view,\n  }) async {\n    final canOpen = await openDocument(documentId: view.id);\n    if (canOpen.isSuccess) {\n      return FlowyResult.success(null);\n    }\n    final payload = CreateDocumentPayloadPB()..documentId = view.id;\n    final result = await DocumentEventCreateDocument(payload).send();\n    return result;\n  }\n\n  Future<FlowyResult<DocumentDataPB, FlowyError>> openDocument({\n    required String documentId,\n  }) async {\n    final payload = OpenDocumentPayloadPB()..documentId = documentId;\n    final result = await DocumentEventOpenDocument(payload).send();\n    return result;\n  }\n\n  Future<FlowyResult<DocumentDataPB, FlowyError>> getDocument({\n    required String documentId,\n  }) async {\n    final payload = OpenDocumentPayloadPB()..documentId = documentId;\n    final result = await DocumentEventGetDocumentData(payload).send();\n    return result;\n  }\n\n  Future<FlowyResult<(DocumentDataPB, BlockPB, Node), FlowyError>>\n      getDocumentNode({\n    required String documentId,\n    required String blockId,\n  }) async {\n    final documentResult = await getDocument(documentId: documentId);\n    final document = documentResult.fold((l) => l, (f) => null);\n    if (document == null) {\n      Log.error('unable to get the document for page $documentId');\n      return FlowyResult.failure(FlowyError(msg: 'Document not found'));\n    }\n\n    final blockResult = await getBlockFromDocument(\n      document: document,\n      blockId: blockId,\n    );\n    final block = blockResult.fold((l) => l, (f) => null);\n    if (block == null) {\n      Log.error(\n        'unable to get the block $blockId from the document $documentId',\n      );\n      return FlowyResult.failure(FlowyError(msg: 'Block not found'));\n    }\n\n    final node = document.buildNode(blockId);\n    if (node == null) {\n      Log.error(\n        'unable to get the node for block $blockId in document $documentId',\n      );\n      return FlowyResult.failure(FlowyError(msg: 'Node not found'));\n    }\n\n    return FlowyResult.success((document, block, node));\n  }\n\n  Future<FlowyResult<BlockPB, FlowyError>> getBlockFromDocument({\n    required DocumentDataPB document,\n    required String blockId,\n  }) async {\n    final block = document.blocks[blockId];\n\n    if (block != null) {\n      return FlowyResult.success(block);\n    }\n\n    return FlowyResult.failure(\n      FlowyError(\n        msg: 'Block($blockId) not found in Document(${document.pageId})',\n      ),\n    );\n  }\n\n  Future<FlowyResult<void, FlowyError>> closeDocument({\n    required String viewId,\n  }) async {\n    final payload = ViewIdPB()..value = viewId;\n    final result = await FolderEventCloseView(payload).send();\n    return result;\n  }\n\n  Future<FlowyResult<void, FlowyError>> applyAction({\n    required String documentId,\n    required Iterable<BlockActionPB> actions,\n  }) async {\n    final payload = ApplyActionPayloadPB(\n      documentId: documentId,\n      actions: actions,\n    );\n    final result = await DocumentEventApplyAction(payload).send();\n    return result;\n  }\n\n  /// Creates a new external text.\n  ///\n  /// Normally, it's used to the block that needs sync long text.\n  ///\n  /// the delta parameter is the json representation of the delta.\n  Future<FlowyResult<void, FlowyError>> createExternalText({\n    required String documentId,\n    required String textId,\n    String? delta,\n  }) async {\n    final payload = TextDeltaPayloadPB(\n      documentId: documentId,\n      textId: textId,\n      delta: delta,\n    );\n    final result = await DocumentEventCreateText(payload).send();\n    return result;\n  }\n\n  /// Updates the external text.\n  ///\n  /// this function is compatible with the [createExternalText] function.\n  ///\n  /// the delta parameter is the json representation of the delta too.\n  Future<FlowyResult<void, FlowyError>> updateExternalText({\n    required String documentId,\n    required String textId,\n    String? delta,\n  }) async {\n    final payload = TextDeltaPayloadPB(\n      documentId: documentId,\n      textId: textId,\n      delta: delta,\n    );\n    final result = await DocumentEventApplyTextDeltaEvent(payload).send();\n    return result;\n  }\n\n  /// Upload a file to the cloud storage.\n  Future<FlowyResult<UploadedFilePB, FlowyError>> uploadFile({\n    required String localFilePath,\n    required String documentId,\n  }) async {\n    final workspace = await FolderEventReadCurrentWorkspace().send();\n    return workspace.fold(\n      (l) async {\n        final payload = UploadFileParamsPB(\n          workspaceId: l.id,\n          localFilePath: localFilePath,\n          documentId: documentId,\n        );\n        return DocumentEventUploadFile(payload).send();\n      },\n      (r) async {\n        return FlowyResult.failure(FlowyError(msg: 'Workspace not found'));\n      },\n    );\n  }\n\n  /// Download a file from the cloud storage.\n  Future<FlowyResult<void, FlowyError>> downloadFile({\n    required String url,\n  }) async {\n    final workspace = await FolderEventReadCurrentWorkspace().send();\n    return workspace.fold((l) async {\n      final payload = DownloadFilePB(\n        url: url,\n      );\n      final result = await DocumentEventDownloadFile(payload).send();\n      return result;\n    }, (r) async {\n      return FlowyResult.failure(FlowyError(msg: 'Workspace not found'));\n    });\n  }\n\n  /// Sync the awareness states\n  /// For example, the cursor position, selection, who is viewing the document.\n  Future<FlowyResult<void, FlowyError>> syncAwarenessStates({\n    required String documentId,\n    Selection? selection,\n    String? metadata,\n  }) async {\n    final payload = UpdateDocumentAwarenessStatePB(\n      documentId: documentId,\n      selection: convertSelectionToAwarenessSelection(selection),\n      metadata: metadata,\n    );\n\n    final result = await DocumentEventSetAwarenessState(payload).send();\n    return result;\n  }\n\n  DocumentAwarenessSelectionPB? convertSelectionToAwarenessSelection(\n    Selection? selection,\n  ) {\n    if (selection == null) {\n      return null;\n    }\n    return DocumentAwarenessSelectionPB(\n      start: DocumentAwarenessPositionPB(\n        offset: Int64(selection.startIndex),\n        path: selection.start.path.map((e) => Int64(e)),\n      ),\n      end: DocumentAwarenessPositionPB(\n        offset: Int64(selection.endIndex),\n        path: selection.end.path.map((e) => Int64(e)),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/document/application/doc_sync_state_listener.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:connectivity_plus/connectivity_plus.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'document_sync_bloc.freezed.dart';\n\nclass DocumentSyncBloc extends Bloc<DocumentSyncEvent, DocumentSyncBlocState> {\n  DocumentSyncBloc({\n    required this.view,\n  })  : _syncStateListener = DocumentSyncStateListener(id: view.id),\n        super(DocumentSyncBlocState.initial()) {\n    on<DocumentSyncEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            final userProfile = await getIt<AuthService>().getUser().then(\n                  (result) => result.fold(\n                    (l) => l,\n                    (r) => null,\n                  ),\n                );\n            emit(\n              state.copyWith(\n                shouldShowIndicator:\n                    userProfile?.workspaceType == WorkspaceTypePB.ServerW,\n              ),\n            );\n            _syncStateListener.start(\n              didReceiveSyncState: (syncState) {\n                add(DocumentSyncEvent.syncStateChanged(syncState));\n              },\n            );\n\n            final isNetworkConnected = await _connectivity\n                .checkConnectivity()\n                .then((value) => value != ConnectivityResult.none);\n            emit(state.copyWith(isNetworkConnected: isNetworkConnected));\n\n            connectivityStream =\n                _connectivity.onConnectivityChanged.listen((result) {\n              add(DocumentSyncEvent.networkStateChanged(result));\n            });\n          },\n          syncStateChanged: (syncState) {\n            emit(state.copyWith(syncState: syncState.value));\n          },\n          networkStateChanged: (result) {\n            emit(\n              state.copyWith(\n                isNetworkConnected: result != ConnectivityResult.none,\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  final ViewPB view;\n  final DocumentSyncStateListener _syncStateListener;\n  final _connectivity = Connectivity();\n\n  StreamSubscription? connectivityStream;\n\n  @override\n  Future<void> close() async {\n    await connectivityStream?.cancel();\n    await _syncStateListener.stop();\n    return super.close();\n  }\n}\n\n@freezed\nclass DocumentSyncEvent with _$DocumentSyncEvent {\n  const factory DocumentSyncEvent.initial() = Initial;\n  const factory DocumentSyncEvent.syncStateChanged(\n    DocumentSyncStatePB syncState,\n  ) = syncStateChanged;\n  const factory DocumentSyncEvent.networkStateChanged(\n    ConnectivityResult result,\n  ) = NetworkStateChanged;\n}\n\n@freezed\nclass DocumentSyncBlocState with _$DocumentSyncBlocState {\n  const factory DocumentSyncBlocState({\n    required DocumentSyncState syncState,\n    @Default(true) bool isNetworkConnected,\n    @Default(false) bool shouldShowIndicator,\n  }) = _DocumentSyncState;\n\n  factory DocumentSyncBlocState.initial() => const DocumentSyncBlocState(\n        syncState: DocumentSyncState.Syncing,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/application/document_validator.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\n\nclass DocumentValidator {\n  const DocumentValidator({\n    required this.editorState,\n    required this.rules,\n  });\n\n  final EditorState editorState;\n  final List<DocumentRule> rules;\n\n  Future<bool> validate(Transaction transaction) async {\n    // deep copy the document\n    final root = this.editorState.document.root.deepCopy();\n    final dummyDocument = Document(root: root);\n    if (dummyDocument.isEmpty) {\n      return true;\n    }\n\n    final editorState = EditorState(document: dummyDocument);\n    await editorState.apply(transaction);\n\n    final iterator = NodeIterator(\n      document: editorState.document,\n      startNode: editorState.document.root,\n    );\n\n    for (final rule in rules) {\n      while (iterator.moveNext()) {\n        if (!rule.validate(iterator.current)) {\n          return false;\n        }\n      }\n    }\n\n    return true;\n  }\n\n  Future<bool> applyTransactionInDummyDocument(Transaction transaction) async {\n    // deep copy the document\n    final root = this.editorState.document.root.deepCopy();\n    final dummyDocument = Document(root: root);\n    if (dummyDocument.isEmpty) {\n      return true;\n    }\n\n    final editorState = EditorState(document: dummyDocument);\n    await editorState.apply(transaction);\n\n    final iterator = NodeIterator(\n      document: editorState.document,\n      startNode: editorState.document.root,\n    );\n\n    for (final rule in rules) {\n      while (iterator.moveNext()) {\n        if (!rule.validate(iterator.current)) {\n          return false;\n        }\n      }\n    }\n\n    return true;\n  }\n}\n\nclass DocumentRule {\n  const DocumentRule({\n    required this.type,\n  });\n\n  final String type;\n\n  bool validate(Node node) {\n    return true;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';\nimport 'package:appflowy/plugins/document/application/document_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:nanoid/nanoid.dart';\n\nconst kExternalTextType = 'text';\n\n/// Uses to adjust the data structure between the editor and the backend.\n///\n/// The editor uses a tree structure to represent the document, while the backend uses a flat structure.\n/// This adapter is used to convert the editor's transaction to the backend's transaction.\nclass TransactionAdapter {\n  TransactionAdapter({\n    required this.documentId,\n    required this.documentService,\n  });\n\n  final DocumentService documentService;\n  final String documentId;\n\n  Future<void> apply(Transaction transaction, EditorState editorState) async {\n    if (enableDocumentInternalLog) {\n      Log.info(\n        '[TransactionAdapter] 2. apply transaction begin ${transaction.hashCode} in $hashCode',\n      );\n    }\n\n    await _applyInternal(transaction, editorState);\n\n    if (enableDocumentInternalLog) {\n      Log.info(\n        '[TransactionAdapter] 3. apply transaction end ${transaction.hashCode} in $hashCode',\n      );\n    }\n  }\n\n  Future<void> _applyInternal(\n    Transaction transaction,\n    EditorState editorState,\n  ) async {\n    final stopwatch = Stopwatch()..start();\n    if (enableDocumentInternalLog) {\n      Log.info('transaction => ${transaction.toJson()}');\n    }\n\n    final actions = transactionToBlockActions(transaction, editorState);\n    final textActions = filterTextDeltaActions(actions);\n\n    final actionCostTime = stopwatch.elapsedMilliseconds;\n    for (final textAction in textActions) {\n      final payload = textAction.textDeltaPayloadPB!;\n      final type = textAction.textDeltaType;\n      if (type == TextDeltaType.create) {\n        await documentService.createExternalText(\n          documentId: payload.documentId,\n          textId: payload.textId,\n          delta: payload.delta,\n        );\n        if (enableDocumentInternalLog) {\n          Log.info(\n            '[editor_transaction_adapter] create external text: id: ${payload.textId} delta: ${payload.delta}',\n          );\n        }\n      } else if (type == TextDeltaType.update) {\n        await documentService.updateExternalText(\n          documentId: payload.documentId,\n          textId: payload.textId,\n          delta: payload.delta,\n        );\n        if (enableDocumentInternalLog) {\n          Log.info(\n            '[editor_transaction_adapter] update external text: id: ${payload.textId} delta: ${payload.delta}',\n          );\n        }\n      }\n    }\n\n    final blockActions = filterBlockActions(actions);\n\n    for (final action in blockActions) {\n      if (enableDocumentInternalLog) {\n        Log.info(\n          '[editor_transaction_adapter] action => ${action.toProto3Json()}',\n        );\n      }\n    }\n\n    await documentService.applyAction(\n      documentId: documentId,\n      actions: blockActions,\n    );\n\n    final elapsed = stopwatch.elapsedMilliseconds;\n    stopwatch.stop();\n    if (enableDocumentInternalLog) {\n      Log.info(\n        '[editor_transaction_adapter] apply transaction cost: total $elapsed ms, converter action $actionCostTime ms, apply action ${elapsed - actionCostTime} ms',\n      );\n    }\n  }\n\n  List<BlockActionWrapper> transactionToBlockActions(\n    Transaction transaction,\n    EditorState editorState,\n  ) {\n    return transaction.operations\n        .map((op) => op.toBlockAction(editorState, documentId))\n        .nonNulls\n        .expand((element) => element)\n        .toList(growable: false); // avoid lazy evaluation\n  }\n\n  List<BlockActionWrapper> filterTextDeltaActions(\n    List<BlockActionWrapper> actions,\n  ) {\n    return actions\n        .where(\n          (e) =>\n              e.textDeltaType != TextDeltaType.none &&\n              e.textDeltaPayloadPB != null,\n        )\n        .toList(growable: false);\n  }\n\n  List<BlockActionPB> filterBlockActions(\n    List<BlockActionWrapper> actions,\n  ) {\n    return actions.map((e) => e.blockActionPB).toList(growable: false);\n  }\n}\n\nextension BlockAction on Operation {\n  List<BlockActionWrapper> toBlockAction(\n    EditorState editorState,\n    String documentId,\n  ) {\n    final op = this;\n    if (op is InsertOperation) {\n      return op.toBlockAction(editorState, documentId);\n    } else if (op is UpdateOperation) {\n      return op.toBlockAction(editorState, documentId);\n    } else if (op is DeleteOperation) {\n      return op.toBlockAction(editorState);\n    }\n    throw UnimplementedError();\n  }\n}\n\nextension on InsertOperation {\n  List<BlockActionWrapper> toBlockAction(\n    EditorState editorState,\n    String documentId, {\n    Node? previousNode,\n  }) {\n    Path currentPath = path;\n    final List<BlockActionWrapper> actions = [];\n    for (final node in nodes) {\n      if (node.type == AiWriterBlockKeys.type) {\n        continue;\n      }\n\n      final parentId = node.parent?.id ??\n          editorState.getNodeAtPath(currentPath.parent)?.id ??\n          '';\n      assert(parentId.isNotEmpty);\n\n      String prevId = '';\n      // if the node is the first child of the parent, then its prevId should be empty.\n      final isFirstChild = currentPath.previous.equals(currentPath);\n\n      if (!isFirstChild) {\n        prevId = previousNode?.id ??\n            editorState.getNodeAtPath(currentPath.previous)?.id ??\n            '';\n        assert(prevId.isNotEmpty && prevId != node.id);\n      }\n\n      // create the external text if the node contains the delta in its data.\n      final delta = node.delta;\n      TextDeltaPayloadPB? textDeltaPayloadPB;\n      String? textId;\n      if (delta != null) {\n        textId = nanoid(6);\n\n        textDeltaPayloadPB = TextDeltaPayloadPB(\n          documentId: documentId,\n          textId: textId,\n          delta: jsonEncode(node.delta!.toJson()),\n        );\n\n        // sync the text id to the node\n        node.externalValues = ExternalValues(\n          externalId: textId,\n          externalType: kExternalTextType,\n        );\n      }\n\n      // remove the delta from the data when the incremental update is stable.\n      final payload = BlockActionPayloadPB()\n        ..block = node.toBlock(\n          childrenId: nanoid(6),\n          externalId: textId,\n          externalType: textId != null ? kExternalTextType : null,\n          attributes: {...node.attributes}..remove(blockComponentDelta),\n        )\n        ..parentId = parentId\n        ..prevId = prevId;\n\n      // pass the external text id to the payload.\n      if (textDeltaPayloadPB != null) {\n        payload.textId = textDeltaPayloadPB.textId;\n      }\n\n      assert(payload.block.childrenId.isNotEmpty);\n      final blockActionPB = BlockActionPB()\n        ..action = BlockActionTypePB.Insert\n        ..payload = payload;\n\n      actions.add(\n        BlockActionWrapper(\n          blockActionPB: blockActionPB,\n          textDeltaPayloadPB: textDeltaPayloadPB,\n          textDeltaType: TextDeltaType.create,\n        ),\n      );\n      if (node.children.isNotEmpty) {\n        Node? prevChild;\n        for (final child in node.children) {\n          actions.addAll(\n            InsertOperation(currentPath + child.path, [child]).toBlockAction(\n              editorState,\n              documentId,\n              previousNode: prevChild,\n            ),\n          );\n          prevChild = child;\n        }\n      }\n      previousNode = node;\n      currentPath = currentPath.next;\n    }\n    return actions;\n  }\n}\n\nextension on UpdateOperation {\n  List<BlockActionWrapper> toBlockAction(\n    EditorState editorState,\n    String documentId,\n  ) {\n    final List<BlockActionWrapper> actions = [];\n\n    // if the attributes are both empty, we don't need to update\n    if (const DeepCollectionEquality().equals(attributes, oldAttributes)) {\n      return actions;\n    }\n    final node = editorState.getNodeAtPath(path);\n    if (node == null) {\n      assert(false, 'node not found at path: $path');\n      return actions;\n    }\n    final parentId =\n        node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';\n    assert(parentId.isNotEmpty);\n\n    // create the external text if the node contains the delta in its data.\n    final prevDelta = oldAttributes[blockComponentDelta];\n    final delta = attributes[blockComponentDelta];\n\n    final composedAttributes = composeAttributes(oldAttributes, attributes);\n    final composedDelta = composedAttributes?[blockComponentDelta];\n    composedAttributes?.remove(blockComponentDelta);\n\n    final payload = BlockActionPayloadPB()\n      ..block = node.toBlock(\n        parentId: parentId,\n        attributes: composedAttributes,\n      )\n      ..parentId = parentId;\n    final blockActionPB = BlockActionPB()\n      ..action = BlockActionTypePB.Update\n      ..payload = payload;\n\n    final textId = (node.externalValues as ExternalValues?)?.externalId;\n    if (textId == null || textId.isEmpty) {\n      // to be compatible with the old version, we create a new text id if the text id is empty.\n      final textId = nanoid(6);\n      final textDelta = composedDelta ?? delta ?? prevDelta;\n      final correctedTextDelta =\n          textDelta != null ? _correctAttributes(textDelta) : null;\n\n      final textDeltaPayloadPB = correctedTextDelta == null\n          ? null\n          : TextDeltaPayloadPB(\n              documentId: documentId,\n              textId: textId,\n              delta: jsonEncode(correctedTextDelta),\n            );\n\n      node.externalValues = ExternalValues(\n        externalId: textId,\n        externalType: kExternalTextType,\n      );\n\n      if (enableDocumentInternalLog) {\n        Log.info('create text delta: $textDeltaPayloadPB');\n      }\n\n      // update the external text id and external type to the block\n      blockActionPB.payload.block\n        ..externalId = textId\n        ..externalType = kExternalTextType;\n\n      actions.add(\n        BlockActionWrapper(\n          blockActionPB: blockActionPB,\n          textDeltaPayloadPB: textDeltaPayloadPB,\n          textDeltaType: TextDeltaType.create,\n        ),\n      );\n    } else {\n      final diff = prevDelta != null && delta != null\n          ? Delta.fromJson(prevDelta).diff(\n              Delta.fromJson(delta),\n            )\n          : null;\n\n      final correctedDiff = diff != null ? _correctDelta(diff) : null;\n\n      final textDeltaPayloadPB = correctedDiff == null\n          ? null\n          : TextDeltaPayloadPB(\n              documentId: documentId,\n              textId: textId,\n              delta: jsonEncode(correctedDiff),\n            );\n\n      if (enableDocumentInternalLog) {\n        Log.info('update text delta: $textDeltaPayloadPB');\n      }\n\n      // update the external text id and external type to the block\n      blockActionPB.payload.block\n        ..externalId = textId\n        ..externalType = kExternalTextType;\n\n      actions.add(\n        BlockActionWrapper(\n          blockActionPB: blockActionPB,\n          textDeltaPayloadPB: textDeltaPayloadPB,\n          textDeltaType: TextDeltaType.update,\n        ),\n      );\n    }\n\n    return actions;\n  }\n\n  // if the value in Delta's attributes is false, we should set the value to null instead.\n  // because on Yjs, canceling format must use the null value. If we use false, the update will be rejected.\n  List<TextOperation>? _correctDelta(Delta delta) {\n    // if the value in diff's attributes is false, we should set the value to null instead.\n    // because on Yjs, canceling format must use the null value. If we use false, the update will be rejected.\n    final correctedOps = delta.map((op) {\n      final attributes = op.attributes?.map(\n        (key, value) => MapEntry(\n          key,\n          // if the value is false, we should set the value to null instead.\n          value == false ? null : value,\n        ),\n      );\n\n      if (attributes != null) {\n        if (op is TextRetain) {\n          return TextRetain(op.length, attributes: attributes);\n        } else if (op is TextInsert) {\n          return TextInsert(op.text, attributes: attributes);\n        }\n        // ignore the other operations that do not contain attributes.\n      }\n\n      return op;\n    });\n\n    return correctedOps.toList(growable: false);\n  }\n\n  // Refer to [_correctDelta] for more details.\n  List<Map<String, dynamic>> _correctAttributes(\n    List<Map<String, dynamic>> attributes,\n  ) {\n    final correctedAttributes = attributes.map((attribute) {\n      return attribute.map((key, value) {\n        if (value is bool) {\n          return MapEntry(key, value == false ? null : value);\n        } else if (value is Map<String, dynamic>) {\n          return MapEntry(\n            key,\n            value.map((key, value) {\n              return MapEntry(key, value == false ? null : value);\n            }),\n          );\n        }\n        return MapEntry(key, value);\n      });\n    }).toList(growable: false);\n\n    return correctedAttributes;\n  }\n}\n\nextension on DeleteOperation {\n  List<BlockActionWrapper> toBlockAction(EditorState editorState) {\n    final List<BlockActionPB> actions = [];\n    for (final node in nodes) {\n      final parentId =\n          node.parent?.id ?? editorState.getNodeAtPath(path.parent)?.id ?? '';\n      final payload = BlockActionPayloadPB()\n        ..block = node.toBlock(\n          parentId: parentId,\n        )\n        ..parentId = parentId;\n      assert(parentId.isNotEmpty);\n      actions.add(\n        BlockActionPB()\n          ..action = BlockActionTypePB.Delete\n          ..payload = payload,\n      );\n    }\n    return actions\n        .map((e) => BlockActionWrapper(blockActionPB: e))\n        .toList(growable: false);\n  }\n}\n\nenum TextDeltaType {\n  none,\n  create,\n  update,\n}\n\nclass BlockActionWrapper {\n  BlockActionWrapper({\n    required this.blockActionPB,\n    this.textDeltaType = TextDeltaType.none,\n    this.textDeltaPayloadPB,\n  });\n\n  final BlockActionPB blockActionPB;\n  final TextDeltaPayloadPB? textDeltaPayloadPB;\n  final TextDeltaType textDeltaType;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/application/prelude.dart",
    "content": "export '../../shared/share/share_bloc.dart';\nexport 'document_bloc.dart';\nexport 'document_service.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/document.dart",
    "content": "library;\n\nimport 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/plugins/document/document_page.dart';\nimport 'package:appflowy/plugins/document/presentation/document_collaborators.dart';\nimport 'package:appflowy/plugins/shared/share/share_button.dart';\nimport 'package:appflowy/plugins/util.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_stack.dart';\nimport 'package:appflowy/workspace/presentation/widgets/favorite_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart';\nimport 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';\nimport 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass DocumentPluginBuilder extends PluginBuilder {\n  @override\n  Plugin build(dynamic data) {\n    if (data is ViewPB) {\n      return DocumentPlugin(pluginType: pluginType, view: data);\n    }\n\n    throw FlowyPluginException.invalidData;\n  }\n\n  @override\n  String get menuName => LocaleKeys.document_menuName.tr();\n\n  @override\n  FlowySvgData get icon => FlowySvgs.icon_document_s;\n\n  @override\n  PluginType get pluginType => PluginType.document;\n\n  @override\n  ViewLayoutPB get layoutType => ViewLayoutPB.Document;\n}\n\nclass DocumentPlugin extends Plugin {\n  DocumentPlugin({\n    required ViewPB view,\n    required PluginType pluginType,\n    this.initialSelection,\n    this.initialBlockId,\n  }) : notifier = ViewPluginNotifier(view: view) {\n    _pluginType = pluginType;\n  }\n\n  late PluginType _pluginType;\n  late final ViewInfoBloc _viewInfoBloc;\n  late final PageAccessLevelBloc _pageAccessLevelBloc;\n\n  @override\n  final ViewPluginNotifier notifier;\n\n  // the initial selection of the document\n  final Selection? initialSelection;\n\n  // the initial block id of the document\n  final String? initialBlockId;\n\n  @override\n  PluginWidgetBuilder get widgetBuilder => DocumentPluginWidgetBuilder(\n        bloc: _viewInfoBloc,\n        pageAccessLevelBloc: _pageAccessLevelBloc,\n        notifier: notifier,\n        initialSelection: initialSelection,\n        initialBlockId: initialBlockId,\n      );\n\n  @override\n  PluginType get pluginType => _pluginType;\n\n  @override\n  PluginId get id => notifier.view.id;\n\n  @override\n  void init() {\n    _viewInfoBloc = ViewInfoBloc(view: notifier.view)\n      ..add(const ViewInfoEvent.started());\n    _pageAccessLevelBloc = PageAccessLevelBloc(view: notifier.view)\n      ..add(const PageAccessLevelEvent.initial());\n  }\n\n  @override\n  void dispose() {\n    _viewInfoBloc.close();\n    _pageAccessLevelBloc.close();\n    notifier.dispose();\n  }\n}\n\nclass DocumentPluginWidgetBuilder extends PluginWidgetBuilder\n    with NavigationItem {\n  DocumentPluginWidgetBuilder({\n    required this.bloc,\n    required this.notifier,\n    this.initialSelection,\n    this.initialBlockId,\n    required this.pageAccessLevelBloc,\n  });\n\n  final ViewInfoBloc bloc;\n  final ViewPluginNotifier notifier;\n  final PageAccessLevelBloc pageAccessLevelBloc;\n\n  ViewPB get view => notifier.view;\n  int? deletedViewIndex;\n  final Selection? initialSelection;\n  final String? initialBlockId;\n\n  @override\n  EdgeInsets get contentPadding => EdgeInsets.zero;\n\n  @override\n  Widget buildWidget({\n    required PluginContext context,\n    required bool shrinkWrap,\n    Map<String, dynamic>? data,\n  }) {\n    notifier.isDeleted.addListener(() {\n      final deletedView = notifier.isDeleted.value;\n      if (deletedView != null && deletedView.hasIndex()) {\n        deletedViewIndex = deletedView.index;\n      }\n    });\n\n    final fixedTitle = data?[MobileDocumentScreen.viewFixedTitle];\n    final blockId = initialBlockId ?? data?[MobileDocumentScreen.viewBlockId];\n    final tabs = data?[MobileDocumentScreen.viewSelectTabs] ??\n        const [\n          PickerTabType.emoji,\n          PickerTabType.icon,\n          PickerTabType.custom,\n        ];\n\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider<ViewInfoBloc>.value(\n          value: bloc,\n        ),\n        BlocProvider<PageAccessLevelBloc>.value(\n          value: pageAccessLevelBloc,\n        ),\n      ],\n      child: BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(\n        builder: (_, state) => DocumentPage(\n          key: ValueKey(view.id),\n          view: view,\n          onDeleted: () => context.onDeleted?.call(view, deletedViewIndex),\n          initialSelection: initialSelection,\n          initialBlockId: blockId,\n          fixedTitle: fixedTitle,\n          tabs: tabs,\n        ),\n      ),\n    );\n  }\n\n  @override\n  String? get viewName => notifier.view.nameOrDefault;\n\n  @override\n  Widget get leftBarItem {\n    return BlocProvider.value(\n      value: pageAccessLevelBloc,\n      child: ViewTitleBar(key: ValueKey(view.id), view: view),\n    );\n  }\n\n  @override\n  Widget tabBarItem(String pluginId, [bool shortForm = false]) =>\n      ViewTabBarItem(view: notifier.view, shortForm: shortForm);\n\n  @override\n  Widget? get rightBarItem {\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider<ViewInfoBloc>.value(\n          value: bloc,\n        ),\n        BlocProvider<PageAccessLevelBloc>.value(\n          value: pageAccessLevelBloc,\n        ),\n      ],\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          ...FeatureFlag.syncDocument.isOn\n              ? [\n                  DocumentCollaborators(\n                    key: ValueKey('collaborators_${view.id}'),\n                    width: 120,\n                    height: 32,\n                    view: view,\n                  ),\n                  const HSpace(16),\n                ]\n              : [const HSpace(8)],\n          ShareButton(\n            key: ValueKey('share_button_${view.id}'),\n            view: view,\n          ),\n          const HSpace(10),\n          ViewFavoriteButton(\n            key: ValueKey('favorite_button_${view.id}'),\n            view: view,\n          ),\n          const HSpace(4),\n          MoreViewActions(view: view),\n        ],\n      ),\n    );\n  }\n\n  @override\n  List<NavigationItem> get navigationItems => [this];\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/document_page.dart",
    "content": "import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/banner.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_drop_handler.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_page.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/shared/flowy_error_page.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';\nimport 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass DocumentPage extends StatefulWidget {\n  const DocumentPage({\n    super.key,\n    required this.view,\n    required this.onDeleted,\n    required this.tabs,\n    this.initialSelection,\n    this.initialBlockId,\n    this.fixedTitle,\n  });\n\n  final ViewPB view;\n  final VoidCallback onDeleted;\n  final Selection? initialSelection;\n  final String? initialBlockId;\n  final String? fixedTitle;\n  final List<PickerTabType> tabs;\n\n  @override\n  State<DocumentPage> createState() => _DocumentPageState();\n}\n\nclass _DocumentPageState extends State<DocumentPage>\n    with WidgetsBindingObserver {\n  EditorState? editorState;\n  Selection? initialSelection;\n  late final documentBloc = DocumentBloc(documentId: widget.view.id)\n    ..add(const DocumentEvent.initial());\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addObserver(this);\n  }\n\n  @override\n  void dispose() {\n    WidgetsBinding.instance.removeObserver(this);\n    documentBloc.close();\n\n    super.dispose();\n  }\n\n  @override\n  void didChangeAppLifecycleState(AppLifecycleState state) {\n    if (state == AppLifecycleState.paused ||\n        state == AppLifecycleState.detached) {\n      documentBloc.add(const DocumentEvent.clearAwarenessStates());\n    } else if (state == AppLifecycleState.resumed) {\n      documentBloc.add(const DocumentEvent.syncAwarenessStates());\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider.value(value: getIt<ActionNavigationBloc>()),\n        BlocProvider.value(value: documentBloc),\n        BlocProvider(\n          create: (context) =>\n              ViewBloc(view: widget.view)..add(const ViewEvent.initial()),\n          lazy: false,\n        ),\n      ],\n      child: BlocConsumer<PageAccessLevelBloc, PageAccessLevelState>(\n        listenWhen: (prev, curr) =>\n            curr.isLocked != prev.isLocked ||\n            curr.accessLevel != prev.accessLevel ||\n            curr.isLoadingLockStatus != prev.isLoadingLockStatus,\n        listener: (context, pageAccessLevelState) {\n          if (pageAccessLevelState.isLoadingLockStatus) {\n            return;\n          }\n\n          editorState?.editable = pageAccessLevelState.isEditable;\n        },\n        builder: (context, pageAccessLevelState) {\n          return BlocBuilder<DocumentBloc, DocumentState>(\n            buildWhen: shouldRebuildDocument,\n            builder: (context, state) {\n              if (state.isLoading) {\n                return const Center(\n                  child: CircularProgressIndicator.adaptive(),\n                );\n              }\n\n              final editorState = state.editorState;\n              this.editorState = editorState;\n              final error = state.error;\n              if (error != null || editorState == null) {\n                Log.error(error);\n                return Center(child: AppFlowyErrorPage(error: error));\n              }\n\n              if (state.forceClose) {\n                widget.onDeleted();\n                return const SizedBox.shrink();\n              }\n\n              return MultiBlocListener(\n                listeners: [\n                  BlocListener<PageAccessLevelBloc, PageAccessLevelState>(\n                    listener: (context, state) {\n                      editorState.editable = state.isEditable;\n                    },\n                  ),\n                  BlocListener<ActionNavigationBloc, ActionNavigationState>(\n                    listenWhen: (_, curr) => curr.action != null,\n                    listener: onNotificationAction,\n                  ),\n                ],\n                child: AiWriterScrollWrapper(\n                  viewId: widget.view.id,\n                  editorState: editorState,\n                  child: buildEditorPage(context, state),\n                ),\n              );\n            },\n          );\n        },\n      ),\n    );\n  }\n\n  Widget buildEditorPage(\n    BuildContext context,\n    DocumentState state,\n  ) {\n    final editorState = state.editorState;\n    if (editorState == null) {\n      return const SizedBox.shrink();\n    }\n\n    final width = context.read<DocumentAppearanceCubit>().state.width;\n\n    // avoid the initial selection calculation change when the editorState is not changed\n    initialSelection ??= _calculateInitialSelection(editorState);\n\n    final Widget child;\n    if (UniversalPlatform.isMobile) {\n      child = BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(\n        builder: (context, styleState) => AppFlowyEditorPage(\n          editorState: editorState,\n          // if the view's name is empty, focus on the title\n          autoFocus: widget.view.name.isEmpty ? false : null,\n          styleCustomizer: EditorStyleCustomizer(\n            context: context,\n            width: width,\n            padding: EditorStyleCustomizer.documentPadding,\n            editorState: editorState,\n          ),\n          header: buildCoverAndIcon(context, state),\n          initialSelection: initialSelection,\n        ),\n      );\n    } else {\n      child = EditorDropHandler(\n        viewId: widget.view.id,\n        editorState: editorState,\n        isLocalMode: context.read<DocumentBloc>().isLocalMode,\n        child: AppFlowyEditorPage(\n          editorState: editorState,\n          // if the view's name is empty, focus on the title\n          autoFocus: widget.view.name.isEmpty ? false : null,\n          styleCustomizer: EditorStyleCustomizer(\n            context: context,\n            width: width,\n            padding: EditorStyleCustomizer.documentPadding,\n            editorState: editorState,\n          ),\n          header: buildCoverAndIcon(context, state),\n          initialSelection: initialSelection,\n          placeholderText: (node) =>\n              node.type == ParagraphBlockKeys.type && !node.isInTable\n                  ? LocaleKeys.editor_slashPlaceHolder.tr()\n                  : '',\n        ),\n      );\n    }\n\n    return Provider(\n      create: (_) {\n        final context = SharedEditorContext();\n        final children = editorState.document.root.children;\n        final firstDelta = children.firstOrNull?.delta;\n        final isEmptyDocument =\n            children.length == 1 && (firstDelta == null || firstDelta.isEmpty);\n        if (widget.view.name.isEmpty && isEmptyDocument) {\n          context.requestCoverTitleFocus = true;\n        }\n        return context;\n      },\n      dispose: (buildContext, editorContext) => editorContext.dispose(),\n      child: EditorTransactionService(\n        viewId: widget.view.id,\n        editorState: state.editorState!,\n        child: Column(\n          children: [\n            // the banner only shows on desktop\n            if (state.isDeleted && UniversalPlatform.isDesktop)\n              buildBanner(context),\n            Expanded(child: child),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget buildBanner(BuildContext context) {\n    return DocumentBanner(\n      viewName: widget.view.nameOrDefault,\n      onRestore: () =>\n          context.read<DocumentBloc>().add(const DocumentEvent.restorePage()),\n      onDelete: () => context\n          .read<DocumentBloc>()\n          .add(const DocumentEvent.deletePermanently()),\n    );\n  }\n\n  Widget buildCoverAndIcon(BuildContext context, DocumentState state) {\n    final editorState = state.editorState;\n    final userProfilePB = state.userProfilePB;\n    if (editorState == null || userProfilePB == null) {\n      return const SizedBox.shrink();\n    }\n\n    if (UniversalPlatform.isMobile) {\n      return DocumentImmersiveCover(\n        fixedTitle: widget.fixedTitle,\n        view: widget.view,\n        tabs: widget.tabs,\n        userProfilePB: userProfilePB,\n      );\n    }\n\n    final page = editorState.document.root;\n    return DocumentCoverWidget(\n      node: page,\n      tabs: widget.tabs,\n      editorState: editorState,\n      view: widget.view,\n      onIconChanged: (icon) async => ViewBackendService.updateViewIcon(\n        view: widget.view,\n        viewIcon: icon,\n      ),\n    );\n  }\n\n  void onNotificationAction(\n    BuildContext context,\n    ActionNavigationState state,\n  ) {\n    final action = state.action;\n    if (action == null ||\n        action.type != ActionType.jumpToBlock ||\n        action.objectId != widget.view.id) {\n      return;\n    }\n\n    final editorState = context.read<DocumentBloc>().state.editorState;\n    if (editorState == null) {\n      return;\n    }\n\n    final Path? path = _getPathFromAction(action, editorState);\n    if (path != null) {\n      editorState.updateSelectionWithReason(\n        Selection.collapsed(Position(path: path)),\n      );\n    }\n  }\n\n  Path? _getPathFromAction(NavigationAction action, EditorState editorState) {\n    final path = action.arguments?[ActionArgumentKeys.nodePath];\n    if (path is int) {\n      return [path];\n    } else if (path is List<int>?) {\n      if (path == null || path.isEmpty) {\n        final blockId = action.arguments?[ActionArgumentKeys.blockId];\n        if (blockId != null) {\n          return _findNodePathByBlockId(editorState, blockId);\n        }\n      }\n    }\n    return path;\n  }\n\n  Path? _findNodePathByBlockId(EditorState editorState, String blockId) {\n    final document = editorState.document;\n    final startNode = document.root.children.firstOrNull;\n    if (startNode == null) {\n      return null;\n    }\n\n    final nodeIterator = NodeIterator(document: document, startNode: startNode);\n    while (nodeIterator.moveNext()) {\n      final node = nodeIterator.current;\n      if (node.id == blockId) {\n        return node.path;\n      }\n    }\n\n    return null;\n  }\n\n  bool shouldRebuildDocument(DocumentState previous, DocumentState current) {\n    // only rebuild the document page when the below fields are changed\n    // this is to prevent unnecessary rebuilds\n    //\n    // If you confirm the newly added fields should be rebuilt, please update\n    // this function.\n    if (previous.editorState != current.editorState) {\n      return true;\n    }\n\n    if (previous.forceClose != current.forceClose ||\n        previous.isDeleted != current.isDeleted) {\n      return true;\n    }\n\n    if (previous.userProfilePB != current.userProfilePB) {\n      return true;\n    }\n\n    if (previous.isLoading != current.isLoading ||\n        previous.error != current.error) {\n      return true;\n    }\n\n    return false;\n  }\n\n  Selection? _calculateInitialSelection(EditorState editorState) {\n    if (widget.initialSelection != null) {\n      return widget.initialSelection;\n    }\n\n    if (widget.initialBlockId != null) {\n      final path = _findNodePathByBlockId(editorState, widget.initialBlockId!);\n      if (path != null) {\n        editorState.selectionType = SelectionType.block;\n        editorState.selectionExtraInfo = {\n          selectionExtraInfoDoNotAttachTextService: true,\n        };\n        return Selection.collapsed(\n          Position(\n            path: path,\n          ),\n        );\n      }\n    }\n\n    return null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/banner.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/buttons/base_styled_button.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\n\nclass DocumentBanner extends StatelessWidget {\n  const DocumentBanner({\n    super.key,\n    required this.viewName,\n    required this.onRestore,\n    required this.onDelete,\n  });\n\n  final String viewName;\n  final void Function() onRestore;\n  final void Function() onDelete;\n\n  @override\n  Widget build(BuildContext context) {\n    final colorScheme = Theme.of(context).colorScheme;\n    return ConstrainedBox(\n      constraints: const BoxConstraints(minHeight: 60),\n      child: Container(\n        width: double.infinity,\n        color: colorScheme.surfaceContainerHighest,\n        child: FittedBox(\n          fit: BoxFit.scaleDown,\n          child: Row(\n            children: [\n              FlowyText.medium(\n                LocaleKeys.deletePagePrompt_text.tr(),\n                color: colorScheme.tertiary,\n                fontSize: 14,\n              ),\n              const HSpace(20),\n              BaseStyledButton(\n                minWidth: 160,\n                minHeight: 40,\n                contentPadding: EdgeInsets.zero,\n                bgColor: Colors.transparent,\n                highlightColor: Theme.of(context).colorScheme.onErrorContainer,\n                outlineColor: colorScheme.tertiaryContainer,\n                borderRadius: Corners.s8Border,\n                onPressed: onRestore,\n                child: FlowyText.medium(\n                  LocaleKeys.deletePagePrompt_restore.tr(),\n                  color: colorScheme.tertiary,\n                  fontSize: 13,\n                ),\n              ),\n              const HSpace(20),\n              BaseStyledButton(\n                minWidth: 220,\n                minHeight: 40,\n                contentPadding: EdgeInsets.zero,\n                bgColor: Colors.transparent,\n                highlightColor: Theme.of(context).colorScheme.error,\n                outlineColor: colorScheme.tertiaryContainer,\n                borderRadius: Corners.s8Border,\n                onPressed: () => showConfirmDeletionDialog(\n                  context: context,\n                  name: viewName.trim().isEmpty\n                      ? LocaleKeys.menuAppHeader_defaultNewPageName.tr()\n                      : viewName,\n                  description: LocaleKeys\n                      .deletePagePrompt_deletePermanentDescription\n                      .tr(),\n                  onConfirm: onDelete,\n                ),\n                child: FlowyText.medium(\n                  LocaleKeys.deletePagePrompt_deletePermanent.tr(),\n                  color: colorScheme.tertiary,\n                  fontSize: 13,\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/collaborator_avatar_stack.dart",
    "content": "import 'package:avatar_stack/avatar_stack.dart';\nimport 'package:avatar_stack/positions.dart';\nimport 'package:flutter/material.dart';\n\nclass CollaboratorAvatarStack extends StatelessWidget {\n  const CollaboratorAvatarStack({\n    super.key,\n    required this.avatars,\n    this.settings,\n    this.infoWidgetBuilder,\n    this.width,\n    this.height,\n    this.borderWidth,\n    this.borderColor,\n    this.backgroundColor,\n    required this.plusWidgetBuilder,\n  });\n\n  final List<Widget> avatars;\n  final Positions? settings;\n  final InfoWidgetBuilder? infoWidgetBuilder;\n  final double? width;\n  final double? height;\n  final double? borderWidth;\n  final Color? borderColor;\n  final Color? backgroundColor;\n  final Widget Function(int value, BorderSide border) plusWidgetBuilder;\n\n  @override\n  Widget build(BuildContext context) {\n    final settings = this.settings ??\n        RestrictedPositions(\n          maxCoverage: 0.4,\n          minCoverage: 0.3,\n          align: StackAlign.right,\n          laying: StackLaying.first,\n        );\n\n    final border = BorderSide(\n      color: borderColor ?? Theme.of(context).dividerColor,\n      width: borderWidth ?? 2.0,\n    );\n\n    return SizedBox(\n      height: height,\n      width: width,\n      child: WidgetStack(\n        positions: settings,\n        buildInfoWidget: (value, _) => plusWidgetBuilder(value, border),\n        stackedWidgets: avatars\n            .map(\n              (avatar) => CircleAvatar(\n                backgroundColor: border.color,\n                child: Padding(\n                  padding: EdgeInsets.all(border.width),\n                  child: avatar,\n                ),\n              ),\n            )\n            .toList(),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/compact_mode_event.dart",
    "content": "import 'package:event_bus/event_bus.dart';\n\nEventBus compactModeEventBus = EventBus();\n\nclass CompactModeEvent {\n  CompactModeEvent({\n    required this.id,\n    required this.enable,\n  });\n\n  final String id;\n  final bool enable;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/document_collaborators.dart",
    "content": "import 'package:appflowy/plugins/document/application/document_awareness_metadata.dart';\nimport 'package:appflowy/plugins/document/application/document_collaborators_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/collaborator_avatar_stack.dart';\nimport 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:avatar_stack/avatar_stack.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass DocumentCollaborators extends StatelessWidget {\n  const DocumentCollaborators({\n    super.key,\n    required this.height,\n    required this.width,\n    required this.view,\n    this.padding,\n    this.fontSize,\n  });\n\n  final ViewPB view;\n  final double height;\n  final double width;\n  final EdgeInsets? padding;\n  final double? fontSize;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => DocumentCollaboratorsBloc(view: view)\n        ..add(const DocumentCollaboratorsEvent.initial()),\n      child: BlocBuilder<DocumentCollaboratorsBloc, DocumentCollaboratorsState>(\n        builder: (context, state) {\n          final collaborators = state.collaborators;\n          if (!state.shouldShowIndicator || collaborators.isEmpty) {\n            return const SizedBox.shrink();\n          }\n\n          return Padding(\n            padding: padding ?? EdgeInsets.zero,\n            child: CollaboratorAvatarStack(\n              height: height,\n              width: width,\n              borderWidth: 1.0,\n              plusWidgetBuilder: (value, border) {\n                final lastXCollaborators = collaborators.sublist(\n                  collaborators.length - value,\n                );\n                return BorderedCircleAvatar(\n                  border: border,\n                  backgroundColor: Theme.of(context).hoverColor,\n                  child: FittedBox(\n                    child: Padding(\n                      padding: const EdgeInsets.all(8.0),\n                      child: FlowyTooltip(\n                        message: lastXCollaborators\n                            .map((e) => e.userName)\n                            .join('\\n'),\n                        child: FlowyText(\n                          '+$value',\n                          fontSize: fontSize,\n                          color: Colors.black,\n                        ),\n                      ),\n                    ),\n                  ),\n                );\n              },\n              avatars: [\n                ...collaborators.map(\n                  (c) => _UserAvatar(fontSize: fontSize, user: c, width: width),\n                ),\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _UserAvatar extends StatelessWidget {\n  const _UserAvatar({\n    this.fontSize,\n    required this.user,\n    required this.width,\n  });\n\n  final DocumentAwarenessMetadata user;\n  final double? fontSize;\n  final double width;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      message: user.userName,\n      child: IgnorePointer(\n        child: UserAvatar(\n          iconUrl: user.userAvatar,\n          name: user.userName,\n          size: AFAvatarSize.m,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_page.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart'\n    hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys;\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:easy_localization/easy_localization.dart' hide TextDirection;\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'editor_plugins/link_preview/custom_link_preview_block_component.dart';\nimport 'editor_plugins/page_block/custom_page_block_component.dart';\n\n/// A global configuration for the editor.\nclass EditorGlobalConfiguration {\n  /// Whether to enable the drag menu in the editor.\n  ///\n  /// Case 1, resizing the columns block in the desktop, then the drag menu will be disabled.\n  static ValueNotifier<bool> enableDragMenu = ValueNotifier(true);\n}\n\n/// The node types that support slash menu.\nfinal Set<String> supportSlashMenuNodeTypes = {\n  ParagraphBlockKeys.type,\n  HeadingBlockKeys.type,\n\n  // Lists\n  TodoListBlockKeys.type,\n  BulletedListBlockKeys.type,\n  NumberedListBlockKeys.type,\n  QuoteBlockKeys.type,\n  ToggleListBlockKeys.type,\n  CalloutBlockKeys.type,\n\n  // Simple table\n  SimpleTableBlockKeys.type,\n  SimpleTableRowBlockKeys.type,\n  SimpleTableCellBlockKeys.type,\n\n  // Columns\n  SimpleColumnsBlockKeys.type,\n  SimpleColumnBlockKeys.type,\n};\n\n/// Build the block component builders.\n///\n/// Every block type should have a corresponding builder in the map.\n/// Otherwise, the errorBlockComponentBuilder will be rendered.\n///\n/// Additional, you can define the block render options in the builder\n/// - customize the block option actions. (... button and + button)\n/// - customize the block component configuration. (padding, placeholder, etc.)\n/// - customize the block icon. (bulleted list, numbered list, todo list)\n/// - customize the hover menu. (show the menu at the top-right corner of the block)\nMap<String, BlockComponentBuilder> buildBlockComponentBuilders({\n  required BuildContext context,\n  required EditorState editorState,\n  required EditorStyleCustomizer styleCustomizer,\n  SlashMenuItemsBuilder? slashMenuItemsBuilder,\n  bool editable = true,\n  ShowPlaceholder? showParagraphPlaceholder,\n  String Function(Node)? placeholderText,\n  EdgeInsets Function(Node node)? customPadding,\n  bool alwaysDistributeSimpleTableColumnWidths = false,\n}) {\n  final configuration = _buildDefaultConfiguration(\n    context,\n    padding: customPadding,\n  );\n  final builders = _buildBlockComponentBuilderMap(\n    context,\n    configuration: configuration,\n    editorState: editorState,\n    styleCustomizer: styleCustomizer,\n    showParagraphPlaceholder: showParagraphPlaceholder,\n    placeholderText: placeholderText,\n    alwaysDistributeSimpleTableColumnWidths:\n        alwaysDistributeSimpleTableColumnWidths,\n    customHeadingPadding: customPadding,\n  );\n\n  // customize the action builder. actually, we can customize them in their own builder. Put them here just for convenience.\n  if (editable) {\n    _customBlockOptionActions(\n      context,\n      builders: builders,\n      editorState: editorState,\n      styleCustomizer: styleCustomizer,\n      slashMenuItemsBuilder: slashMenuItemsBuilder,\n    );\n  }\n\n  return builders;\n}\n\nBlockComponentConfiguration _buildDefaultConfiguration(\n  BuildContext context, {\n  EdgeInsets Function(Node node)? padding,\n}) {\n  final configuration = BlockComponentConfiguration(\n    padding: (node) {\n      if (UniversalPlatform.isMobile) {\n        final pageStyle = context.read<DocumentPageStyleBloc>().state;\n        final factor = pageStyle.fontLayout.factor;\n        final top = pageStyle.lineHeightLayout.padding * factor;\n        EdgeInsets edgeInsets = EdgeInsets.only(top: top);\n        // only add padding for the top level node, otherwise the nested node will have extra padding\n        if (node.path.length == 1) {\n          if (node.type != SimpleTableBlockKeys.type) {\n            // do not add padding for the simple table to allow it overflow\n            edgeInsets = edgeInsets.copyWith(\n              left: EditorStyleCustomizer.nodeHorizontalPadding,\n            );\n          }\n          edgeInsets = edgeInsets.copyWith(\n            right: EditorStyleCustomizer.nodeHorizontalPadding,\n          );\n        }\n        return padding?.call(node) ?? edgeInsets;\n      }\n\n      return const EdgeInsets.symmetric(vertical: 5.0);\n    },\n    indentPadding: (node, textDirection) {\n      double padding = 26.0;\n\n      // only add indent padding for the top level node to align the children\n      if (UniversalPlatform.isMobile && node.level == 1) {\n        padding += EditorStyleCustomizer.nodeHorizontalPadding - 4;\n      }\n\n      // in the quote block, we reduce the indent padding for the first level block.\n      //  So we have to add more padding for the second level to avoid the drag menu overlay the quote icon.\n      if (node.parent?.type == QuoteBlockKeys.type &&\n          UniversalPlatform.isDesktop) {\n        padding += 22;\n      }\n\n      return textDirection == TextDirection.ltr\n          ? EdgeInsets.only(left: padding)\n          : EdgeInsets.only(right: padding);\n    },\n  );\n  return configuration;\n}\n\n/// Build the option actions for the block component.\n///\n/// Notes: different block type may have different option actions.\n/// All the block types have the delete and duplicate options.\nList<OptionAction> _buildOptionActions(BuildContext context, String type) {\n  final standardActions = [\n    OptionAction.delete,\n    OptionAction.duplicate,\n  ];\n\n  // filter out the copy link to block option if in local mode\n  if (context.read<DocumentBloc?>()?.isLocalMode != true) {\n    standardActions.add(OptionAction.copyLinkToBlock);\n  }\n\n  standardActions.add(OptionAction.turnInto);\n\n  if (SimpleTableBlockKeys.type == type) {\n    standardActions.addAll([\n      OptionAction.divider,\n      OptionAction.setToPageWidth,\n      OptionAction.distributeColumnsEvenly,\n    ]);\n  }\n\n  if (EditorOptionActionType.color.supportTypes.contains(type)) {\n    standardActions.addAll([OptionAction.divider, OptionAction.color]);\n  }\n\n  if (EditorOptionActionType.align.supportTypes.contains(type)) {\n    standardActions.addAll([OptionAction.divider, OptionAction.align]);\n  }\n\n  if (EditorOptionActionType.depth.supportTypes.contains(type)) {\n    standardActions.addAll([OptionAction.divider, OptionAction.depth]);\n  }\n\n  return standardActions;\n}\n\nvoid _customBlockOptionActions(\n  BuildContext context, {\n  required Map<String, BlockComponentBuilder> builders,\n  required EditorState editorState,\n  required EditorStyleCustomizer styleCustomizer,\n  SlashMenuItemsBuilder? slashMenuItemsBuilder,\n}) {\n  for (final entry in builders.entries) {\n    if (entry.key == PageBlockKeys.type) {\n      continue;\n    }\n    final builder = entry.value;\n    final actions = _buildOptionActions(context, entry.key);\n\n    if (UniversalPlatform.isDesktop) {\n      builder.showActions = (node) {\n        final parentTableNode = node.parentTableNode;\n        // disable the option action button in table cell to avoid the misalignment issue\n        if (node.type != SimpleTableBlockKeys.type && parentTableNode != null) {\n          return false;\n        }\n        return true;\n      };\n\n      builder.configuration = builder.configuration.copyWith(\n        blockSelectionAreaMargin: (_) => const EdgeInsets.symmetric(\n          vertical: 1,\n        ),\n      );\n\n      builder.actionTrailingBuilder = (context, state) {\n        if (context.node.parent?.type == QuoteBlockKeys.type) {\n          return const SizedBox(\n            width: 24,\n            height: 24,\n          );\n        }\n        return const SizedBox.shrink();\n      };\n\n      builder.actionBuilder = (context, state) {\n        double top = builder.configuration.padding(context.node).top;\n        final type = context.node.type;\n        final level = context.node.attributes[HeadingBlockKeys.level] ?? 0;\n        if ((type == HeadingBlockKeys.type ||\n                type == ToggleListBlockKeys.type) &&\n            level > 0) {\n          final offset = [13.0, 11.0, 8.0, 6.0, 4.0, 2.0];\n          top += offset[level - 1];\n        } else if (type == SimpleTableBlockKeys.type) {\n          top += 8.0;\n        } else {\n          top += 2.0;\n        }\n        if (overflowTypes.contains(type)) {\n          top = top / 2;\n        }\n        return ValueListenableBuilder(\n          valueListenable: EditorGlobalConfiguration.enableDragMenu,\n          builder: (_, enableDragMenu, child) {\n            return ValueListenableBuilder(\n              valueListenable: editorState.editableNotifier,\n              builder: (_, editable, child) {\n                return IgnorePointer(\n                  ignoring: !editable,\n                  child: Opacity(\n                    opacity: editable && enableDragMenu ? 1.0 : 0.0,\n                    child: Padding(\n                      padding: EdgeInsets.only(top: top),\n                      child: BlockActionList(\n                        blockComponentContext: context,\n                        blockComponentState: state,\n                        editorState: editorState,\n                        blockComponentBuilder: builders,\n                        actions: actions,\n                        showSlashMenu: slashMenuItemsBuilder != null\n                            ? () => customAppFlowySlashCommand(\n                                  itemsBuilder: slashMenuItemsBuilder,\n                                  shouldInsertSlash: false,\n                                  deleteKeywordsByDefault: true,\n                                  style: styleCustomizer\n                                      .selectionMenuStyleBuilder(),\n                                  supportSlashMenuNodeTypes:\n                                      supportSlashMenuNodeTypes,\n                                ).handler.call(editorState)\n                            : () {},\n                      ),\n                    ),\n                  ),\n                );\n              },\n            );\n          },\n        );\n      };\n    }\n  }\n}\n\nMap<String, BlockComponentBuilder> _buildBlockComponentBuilderMap(\n  BuildContext context, {\n  required BlockComponentConfiguration configuration,\n  required EditorState editorState,\n  required EditorStyleCustomizer styleCustomizer,\n  ShowPlaceholder? showParagraphPlaceholder,\n  String Function(Node)? placeholderText,\n  EdgeInsets Function(Node)? customHeadingPadding,\n  bool alwaysDistributeSimpleTableColumnWidths = false,\n}) {\n  final customBlockComponentBuilderMap = {\n    PageBlockKeys.type: CustomPageBlockComponentBuilder(),\n    ParagraphBlockKeys.type: _buildParagraphBlockComponentBuilder(\n      context,\n      configuration,\n      showParagraphPlaceholder,\n      placeholderText,\n    ),\n    TodoListBlockKeys.type: _buildTodoListBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    BulletedListBlockKeys.type: _buildBulletedListBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    NumberedListBlockKeys.type: _buildNumberedListBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    QuoteBlockKeys.type: _buildQuoteBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    HeadingBlockKeys.type: _buildHeadingBlockComponentBuilder(\n      context,\n      configuration,\n      styleCustomizer,\n      customHeadingPadding,\n    ),\n    ImageBlockKeys.type: _buildCustomImageBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    MultiImageBlockKeys.type: _buildMultiImageBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    TableBlockKeys.type: _buildTableBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    TableCellBlockKeys.type: _buildTableCellBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    DatabaseBlockKeys.gridType: _buildDatabaseViewBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    DatabaseBlockKeys.boardType: _buildDatabaseViewBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    DatabaseBlockKeys.calendarType: _buildDatabaseViewBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    CalloutBlockKeys.type: _buildCalloutBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    DividerBlockKeys.type: _buildDividerBlockComponentBuilder(\n      context,\n      configuration,\n      editorState,\n    ),\n    MathEquationBlockKeys.type: _buildMathEquationBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    CodeBlockKeys.type: _buildCodeBlockComponentBuilder(\n      context,\n      configuration,\n      styleCustomizer,\n    ),\n    AiWriterBlockKeys.type: _buildAIWriterBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    ToggleListBlockKeys.type: _buildToggleListBlockComponentBuilder(\n      context,\n      configuration,\n      styleCustomizer,\n      customHeadingPadding,\n    ),\n    OutlineBlockKeys.type: _buildOutlineBlockComponentBuilder(\n      context,\n      configuration,\n      styleCustomizer,\n    ),\n    LinkPreviewBlockKeys.type: _buildLinkPreviewBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    // Flutter doesn't support the video widget, so we forward the video block to the link preview block\n    VideoBlockKeys.type: _buildLinkPreviewBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    FileBlockKeys.type: _buildFileBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    SubPageBlockKeys.type: _buildSubPageBlockComponentBuilder(\n      context,\n      configuration,\n      styleCustomizer: styleCustomizer,\n    ),\n    errorBlockComponentBuilderKey: ErrorBlockComponentBuilder(\n      configuration: configuration,\n    ),\n    SimpleTableBlockKeys.type: _buildSimpleTableBlockComponentBuilder(\n      context,\n      configuration,\n      alwaysDistributeColumnWidths: alwaysDistributeSimpleTableColumnWidths,\n    ),\n    SimpleTableRowBlockKeys.type: _buildSimpleTableRowBlockComponentBuilder(\n      context,\n      configuration,\n      alwaysDistributeColumnWidths: alwaysDistributeSimpleTableColumnWidths,\n    ),\n    SimpleTableCellBlockKeys.type: _buildSimpleTableCellBlockComponentBuilder(\n      context,\n      configuration,\n      alwaysDistributeColumnWidths: alwaysDistributeSimpleTableColumnWidths,\n    ),\n    SimpleColumnsBlockKeys.type: _buildSimpleColumnsBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n    SimpleColumnBlockKeys.type: _buildSimpleColumnBlockComponentBuilder(\n      context,\n      configuration,\n    ),\n  };\n\n  final builders = {\n    ...standardBlockComponentBuilderMap,\n    ...customBlockComponentBuilderMap,\n  };\n\n  return builders;\n}\n\nSimpleTableBlockComponentBuilder _buildSimpleTableBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration, {\n  bool alwaysDistributeColumnWidths = false,\n}) {\n  final copiedConfiguration = configuration.copyWith(\n    padding: (node) {\n      final padding = configuration.padding(node);\n      if (UniversalPlatform.isDesktop) {\n        return padding;\n      } else {\n        return padding;\n      }\n    },\n  );\n  return SimpleTableBlockComponentBuilder(\n    configuration: copiedConfiguration,\n    alwaysDistributeColumnWidths: alwaysDistributeColumnWidths,\n  );\n}\n\nSimpleTableRowBlockComponentBuilder _buildSimpleTableRowBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration, {\n  bool alwaysDistributeColumnWidths = false,\n}) {\n  return SimpleTableRowBlockComponentBuilder(\n    configuration: configuration,\n    alwaysDistributeColumnWidths: alwaysDistributeColumnWidths,\n  );\n}\n\nSimpleTableCellBlockComponentBuilder _buildSimpleTableCellBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration, {\n  bool alwaysDistributeColumnWidths = false,\n}) {\n  return SimpleTableCellBlockComponentBuilder(\n    configuration: configuration,\n    alwaysDistributeColumnWidths: alwaysDistributeColumnWidths,\n  );\n}\n\nParagraphBlockComponentBuilder _buildParagraphBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n  ShowPlaceholder? showParagraphPlaceholder,\n  String Function(Node)? placeholderText,\n) {\n  return ParagraphBlockComponentBuilder(\n    configuration: configuration.copyWith(\n      placeholderText: placeholderText,\n      textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(\n        context,\n        node: node,\n        configuration: configuration,\n        textSpan: textSpan,\n      ),\n      textAlign: (node) => _buildTextAlignInTableCell(\n        context,\n        node: node,\n        configuration: configuration,\n      ),\n    ),\n    showPlaceholder: showParagraphPlaceholder,\n  );\n}\n\nTodoListBlockComponentBuilder _buildTodoListBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n) {\n  return TodoListBlockComponentBuilder(\n    configuration: configuration.copyWith(\n      placeholderText: (_) => LocaleKeys.blockPlaceholders_todoList.tr(),\n      textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(\n        context,\n        node: node,\n        configuration: configuration,\n        textSpan: textSpan,\n      ),\n      textAlign: (node) => _buildTextAlignInTableCell(\n        context,\n        node: node,\n        configuration: configuration,\n      ),\n    ),\n    iconBuilder: (_, node, onCheck) => TodoListIcon(\n      node: node,\n      onCheck: onCheck,\n    ),\n    toggleChildrenTriggers: [\n      LogicalKeyboardKey.shift,\n      LogicalKeyboardKey.shiftLeft,\n      LogicalKeyboardKey.shiftRight,\n    ],\n  );\n}\n\nBulletedListBlockComponentBuilder _buildBulletedListBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n) {\n  return BulletedListBlockComponentBuilder(\n    configuration: configuration.copyWith(\n      placeholderText: (_) => LocaleKeys.blockPlaceholders_bulletList.tr(),\n      textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(\n        context,\n        node: node,\n        configuration: configuration,\n        textSpan: textSpan,\n      ),\n      textAlign: (node) => _buildTextAlignInTableCell(\n        context,\n        node: node,\n        configuration: configuration,\n      ),\n    ),\n    iconBuilder: (_, node) => BulletedListIcon(node: node),\n  );\n}\n\nNumberedListBlockComponentBuilder _buildNumberedListBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n) {\n  return NumberedListBlockComponentBuilder(\n    configuration: configuration.copyWith(\n      placeholderText: (_) => LocaleKeys.blockPlaceholders_numberList.tr(),\n      textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(\n        context,\n        node: node,\n        configuration: configuration,\n        textSpan: textSpan,\n      ),\n      textAlign: (node) => _buildTextAlignInTableCell(\n        context,\n        node: node,\n        configuration: configuration,\n      ),\n    ),\n    iconBuilder: (_, node, textDirection) {\n      TextStyle? textStyle;\n      if (node.isInHeaderColumn || node.isInHeaderRow) {\n        textStyle = configuration.textStyle(node).copyWith(\n              fontWeight: FontWeight.bold,\n            );\n      }\n      return NumberedListIcon(\n        node: node,\n        textDirection: textDirection,\n        textStyle: textStyle,\n      );\n    },\n  );\n}\n\nQuoteBlockComponentBuilder _buildQuoteBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n) {\n  return QuoteBlockComponentBuilder(\n    configuration: configuration.copyWith(\n      placeholderText: (_) => LocaleKeys.blockPlaceholders_quote.tr(),\n      textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(\n        context,\n        node: node,\n        configuration: configuration,\n        textSpan: textSpan,\n      ),\n      textAlign: (node) => _buildTextAlignInTableCell(\n        context,\n        node: node,\n        configuration: configuration,\n      ),\n      indentPadding: (node, textDirection) {\n        if (UniversalPlatform.isMobile) {\n          return configuration.indentPadding(node, textDirection);\n        }\n\n        if (node.isInTable) {\n          return textDirection == TextDirection.ltr\n              ? EdgeInsets.only(left: 24)\n              : EdgeInsets.only(right: 24);\n        }\n\n        return EdgeInsets.zero;\n      },\n    ),\n  );\n}\n\nHeadingBlockComponentBuilder _buildHeadingBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n  EditorStyleCustomizer styleCustomizer,\n  EdgeInsets Function(Node)? customHeadingPadding,\n) {\n  return HeadingBlockComponentBuilder(\n    configuration: configuration.copyWith(\n      textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(\n        context,\n        node: node,\n        configuration: configuration,\n        textSpan: textSpan,\n      ),\n      padding: (node) {\n        if (customHeadingPadding != null) {\n          return customHeadingPadding.call(node);\n        }\n\n        if (UniversalPlatform.isMobile) {\n          final pageStyle = context.read<DocumentPageStyleBloc>().state;\n          final factor = pageStyle.fontLayout.factor;\n          final headingPaddings =\n              pageStyle.lineHeightLayout.headingPaddings.map((e) => e * factor);\n          final level =\n              (node.attributes[HeadingBlockKeys.level] ?? 6).clamp(1, 6);\n          final top = headingPaddings.elementAt(level - 1);\n          EdgeInsets edgeInsets = EdgeInsets.only(top: top);\n          if (node.path.length == 1) {\n            edgeInsets = edgeInsets.copyWith(\n              left: EditorStyleCustomizer.nodeHorizontalPadding,\n              right: EditorStyleCustomizer.nodeHorizontalPadding,\n            );\n          }\n          return edgeInsets;\n        }\n\n        return const EdgeInsets.only(top: 12.0, bottom: 4.0);\n      },\n      placeholderText: (node) {\n        int level = node.attributes[HeadingBlockKeys.level] ?? 6;\n        level = level.clamp(1, 6);\n        return LocaleKeys.blockPlaceholders_heading.tr(\n          args: [level.toString()],\n        );\n      },\n      textAlign: (node) => _buildTextAlignInTableCell(\n        context,\n        node: node,\n        configuration: configuration,\n      ),\n    ),\n    textStyleBuilder: (level) {\n      return styleCustomizer.headingStyleBuilder(level);\n    },\n  );\n}\n\nCustomImageBlockComponentBuilder _buildCustomImageBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n) {\n  return CustomImageBlockComponentBuilder(\n    configuration: configuration,\n    showMenu: true,\n    menuBuilder: (node, state, imageStateNotifier) => Positioned(\n      top: 10,\n      right: 10,\n      child: ImageMenu(\n        node: node,\n        state: state,\n        imageStateNotifier: imageStateNotifier,\n      ),\n    ),\n  );\n}\n\nMultiImageBlockComponentBuilder _buildMultiImageBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n) {\n  return MultiImageBlockComponentBuilder(\n    configuration: configuration,\n    showMenu: true,\n    menuBuilder: (\n      Node node,\n      MultiImageBlockComponentState state,\n      ValueNotifier<int> indexNotifier,\n      VoidCallback onImageDeleted,\n    ) =>\n        Positioned(\n      top: 10,\n      right: 10,\n      child: MultiImageMenu(\n        node: node,\n        state: state,\n        indexNotifier: indexNotifier,\n        isLocalMode: context.read<DocumentBloc>().isLocalMode,\n        onImageDeleted: onImageDeleted,\n      ),\n    ),\n  );\n}\n\nTableBlockComponentBuilder _buildTableBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n) {\n  return TableBlockComponentBuilder(\n    menuBuilder: (\n      node,\n      editorState,\n      position,\n      dir,\n      onBuild,\n      onClose,\n    ) =>\n        TableMenu(\n      node: node,\n      editorState: editorState,\n      position: position,\n      dir: dir,\n      onBuild: onBuild,\n      onClose: onClose,\n    ),\n  );\n}\n\nTableCellBlockComponentBuilder _buildTableCellBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n) {\n  return TableCellBlockComponentBuilder(\n    colorBuilder: (context, node) {\n      final String colorString =\n          node.attributes[TableCellBlockKeys.colBackgroundColor] ??\n              node.attributes[TableCellBlockKeys.rowBackgroundColor] ??\n              '';\n      if (colorString.isEmpty) {\n        return null;\n      }\n      return buildEditorCustomizedColor(context, node, colorString);\n    },\n    menuBuilder: (\n      node,\n      editorState,\n      position,\n      dir,\n      onBuild,\n      onClose,\n    ) =>\n        TableMenu(\n      node: node,\n      editorState: editorState,\n      position: position,\n      dir: dir,\n      onBuild: onBuild,\n      onClose: onClose,\n    ),\n  );\n}\n\nDatabaseViewBlockComponentBuilder _buildDatabaseViewBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n) {\n  return DatabaseViewBlockComponentBuilder(\n    configuration: configuration.copyWith(\n      padding: (node) {\n        if (UniversalPlatform.isMobile) {\n          return configuration.padding(node);\n        }\n        return const EdgeInsets.symmetric(vertical: 10);\n      },\n    ),\n  );\n}\n\nCalloutBlockComponentBuilder _buildCalloutBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n) {\n  final calloutBGColor = AFThemeExtension.of(context).calloutBGColor;\n  return CalloutBlockComponentBuilder(\n    configuration: configuration.copyWith(\n      padding: (node) {\n        if (UniversalPlatform.isMobile) {\n          return configuration.padding(node);\n        }\n        return const EdgeInsets.symmetric(vertical: 10);\n      },\n      textAlign: (node) => _buildTextAlignInTableCell(\n        context,\n        node: node,\n        configuration: configuration,\n      ),\n      textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell(\n        context,\n        node: node,\n        configuration: configuration,\n        textSpan: textSpan,\n      ),\n      indentPadding: (node, _) => EdgeInsets.only(left: 42),\n    ),\n    inlinePadding: (node) {\n      if (node.children.isEmpty) {\n        return const EdgeInsets.symmetric(vertical: 8.0);\n      }\n      return EdgeInsets.only(top: 8.0, bottom: 2.0);\n    },\n    defaultColor: calloutBGColor,\n  );\n}\n\nDividerBlockComponentBuilder _buildDividerBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n  EditorState editorState,\n) {\n  return DividerBlockComponentBuilder(\n    configuration: configuration,\n    height: 28.0,\n    wrapper: (_, node, child) => MobileBlockActionButtons(\n      showThreeDots: false,\n      node: node,\n      editorState: editorState,\n      child: child,\n    ),\n  );\n}\n\nMathEquationBlockComponentBuilder _buildMathEquationBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n) {\n  return MathEquationBlockComponentBuilder(\n    configuration: configuration,\n  );\n}\n\nCodeBlockComponentBuilder _buildCodeBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n  EditorStyleCustomizer styleCustomizer,\n) {\n  return CodeBlockComponentBuilder(\n    styleBuilder: styleCustomizer.codeBlockStyleBuilder,\n    configuration: configuration,\n    padding: const EdgeInsets.only(left: 20, right: 30, bottom: 34),\n    languagePickerBuilder: codeBlockLanguagePickerBuilder,\n    copyButtonBuilder: codeBlockCopyBuilder,\n  );\n}\n\nAIWriterBlockComponentBuilder _buildAIWriterBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n) {\n  return AIWriterBlockComponentBuilder();\n}\n\nToggleListBlockComponentBuilder _buildToggleListBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n  EditorStyleCustomizer styleCustomizer,\n  EdgeInsets Function(Node)? customHeadingPadding,\n) {\n  return ToggleListBlockComponentBuilder(\n    configuration: configuration.copyWith(\n      padding: (node) {\n        if (customHeadingPadding != null) {\n          return customHeadingPadding(node);\n        }\n\n        if (UniversalPlatform.isMobile) {\n          final pageStyle = context.read<DocumentPageStyleBloc>().state;\n          final factor = pageStyle.fontLayout.factor;\n          final headingPaddings =\n              pageStyle.lineHeightLayout.headingPaddings.map((e) => e * factor);\n          final level =\n              (node.attributes[HeadingBlockKeys.level] ?? 6).clamp(1, 6);\n          final top = headingPaddings.elementAt(level - 1);\n          return configuration.padding(node).copyWith(top: top);\n        }\n\n        return const EdgeInsets.only(top: 12.0, bottom: 4.0);\n      },\n      textStyle: (node, {TextSpan? textSpan}) {\n        final textStyle = _buildTextStyleInTableCell(\n          context,\n          node: node,\n          configuration: configuration,\n          textSpan: textSpan,\n        );\n        final level = node.attributes[ToggleListBlockKeys.level] as int?;\n        if (level == null) {\n          return textStyle;\n        }\n        return textStyle.merge(styleCustomizer.headingStyleBuilder(level));\n      },\n      textAlign: (node) => _buildTextAlignInTableCell(\n        context,\n        node: node,\n        configuration: configuration,\n      ),\n      placeholderText: (node) {\n        int? level = node.attributes[ToggleListBlockKeys.level];\n        if (level == null) {\n          return configuration.placeholderText(node);\n        }\n        level = level.clamp(1, 6);\n        return LocaleKeys.blockPlaceholders_heading.tr(\n          args: [level.toString()],\n        );\n      },\n    ),\n    textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),\n  );\n}\n\nOutlineBlockComponentBuilder _buildOutlineBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n  EditorStyleCustomizer styleCustomizer,\n) {\n  return OutlineBlockComponentBuilder(\n    configuration: configuration.copyWith(\n      placeholderTextStyle: (node, {TextSpan? textSpan}) =>\n          styleCustomizer.outlineBlockPlaceholderStyleBuilder(),\n      padding: (node) {\n        if (UniversalPlatform.isMobile) {\n          return configuration.padding(node);\n        }\n        return const EdgeInsets.only(top: 12.0, bottom: 4.0);\n      },\n    ),\n  );\n}\n\nCustomLinkPreviewBlockComponentBuilder _buildLinkPreviewBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n) {\n  return CustomLinkPreviewBlockComponentBuilder(\n    configuration: configuration.copyWith(\n      padding: (node) {\n        if (UniversalPlatform.isMobile) {\n          return configuration.padding(node);\n        }\n        return const EdgeInsets.symmetric(vertical: 10);\n      },\n    ),\n  );\n}\n\nFileBlockComponentBuilder _buildFileBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n) {\n  return FileBlockComponentBuilder(\n    configuration: configuration,\n  );\n}\n\nSubPageBlockComponentBuilder _buildSubPageBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration, {\n  required EditorStyleCustomizer styleCustomizer,\n}) {\n  return SubPageBlockComponentBuilder(\n    configuration: configuration.copyWith(\n      textStyle: (node, {TextSpan? textSpan}) =>\n          styleCustomizer.subPageBlockTextStyleBuilder(),\n      padding: (node) {\n        if (UniversalPlatform.isMobile) {\n          return const EdgeInsets.symmetric(horizontal: 18);\n        }\n        return configuration.padding(node);\n      },\n    ),\n  );\n}\n\nSimpleColumnsBlockComponentBuilder _buildSimpleColumnsBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n) {\n  return SimpleColumnsBlockComponentBuilder(\n    configuration: configuration.copyWith(\n      padding: (node) {\n        if (UniversalPlatform.isMobile) {\n          return configuration.padding(node);\n        }\n\n        return EdgeInsets.zero;\n      },\n    ),\n  );\n}\n\nSimpleColumnBlockComponentBuilder _buildSimpleColumnBlockComponentBuilder(\n  BuildContext context,\n  BlockComponentConfiguration configuration,\n) {\n  return SimpleColumnBlockComponentBuilder(\n    configuration: configuration.copyWith(\n      padding: (_) => EdgeInsets.zero,\n    ),\n  );\n}\n\nTextStyle _buildTextStyleInTableCell(\n  BuildContext context, {\n  required Node node,\n  required BlockComponentConfiguration configuration,\n  required TextSpan? textSpan,\n}) {\n  TextStyle textStyle = configuration.textStyle(node, textSpan: textSpan);\n\n  textStyle = textStyle.copyWith(\n    fontFamily: textSpan?.style?.fontFamily,\n    fontSize: textSpan?.style?.fontSize,\n  );\n\n  if (node.isInHeaderColumn ||\n      node.isInHeaderRow ||\n      node.isInBoldColumn ||\n      node.isInBoldRow) {\n    textStyle = textStyle.copyWith(\n      fontWeight: FontWeight.bold,\n    );\n  }\n\n  final cellTextColor = node.textColorInColumn ?? node.textColorInRow;\n\n  // enable it if we need to support the text color of the text span\n  // final isTextSpanColorNull = textSpan?.style?.color == null;\n  // final isTextSpanChildrenColorNull =\n  //     textSpan?.children?.every((e) => e.style?.color == null) ?? true;\n\n  if (cellTextColor != null) {\n    textStyle = textStyle.copyWith(\n      color: buildEditorCustomizedColor(\n        context,\n        node,\n        cellTextColor,\n      ),\n    );\n  }\n\n  return textStyle;\n}\n\nTextAlign _buildTextAlignInTableCell(\n  BuildContext context, {\n  required Node node,\n  required BlockComponentConfiguration configuration,\n}) {\n  final isInTable = node.isInTable;\n  if (!isInTable) {\n    return configuration.textAlign(node);\n  }\n\n  return node.tableAlign.textAlign;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_drop_handler.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_file.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/shared/patterns/file_type_patterns.dart';\nimport 'package:appflowy/workspace/presentation/widgets/draggable_item/draggable_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:desktop_drop/desktop_drop.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nconst _excludeFromDropTarget = [\n  ImageBlockKeys.type,\n  CustomImageBlockKeys.type,\n  MultiImageBlockKeys.type,\n  FileBlockKeys.type,\n  SimpleTableBlockKeys.type,\n  SimpleTableCellBlockKeys.type,\n  SimpleTableRowBlockKeys.type,\n];\n\nclass EditorDropHandler extends StatelessWidget {\n  const EditorDropHandler({\n    super.key,\n    required this.viewId,\n    required this.editorState,\n    required this.isLocalMode,\n    required this.child,\n    this.dropManagerState,\n  });\n\n  final String viewId;\n  final EditorState editorState;\n  final bool isLocalMode;\n  final Widget child;\n  final EditorDropManagerState? dropManagerState;\n\n  @override\n  Widget build(BuildContext context) {\n    final childWidget = Consumer<EditorDropManagerState>(\n      builder: (context, dropState, _) => DragTarget<ViewPB>(\n        onLeave: (_) {\n          editorState.selectionService.removeDropTarget();\n          disableAutoScrollWhenDragging = false;\n        },\n        onMove: (details) {\n          disableAutoScrollWhenDragging = true;\n\n          if (details.data.id == viewId) {\n            return;\n          }\n\n          _onDragUpdated(details.offset);\n        },\n        onWillAcceptWithDetails: (details) {\n          if (!dropState.isDropEnabled) {\n            return false;\n          }\n\n          if (details.data.id == viewId) {\n            return false;\n          }\n\n          return true;\n        },\n        onAcceptWithDetails: _onDragViewDone,\n        builder: (context, _, __) => ValueListenableBuilder(\n          valueListenable: enableDocumentDragNotifier,\n          builder: (context, value, _) {\n            final enableDocumentDrag = value;\n            return DropTarget(\n              enable: dropState.isDropEnabled && enableDocumentDrag,\n              onDragExited: (_) =>\n                  editorState.selectionService.removeDropTarget(),\n              onDragUpdated: (details) =>\n                  _onDragUpdated(details.globalPosition),\n              onDragDone: _onDragDone,\n              child: child,\n            );\n          },\n        ),\n      ),\n    );\n\n    // Due to how DropTarget works, there is no way to differentiate if an overlay is\n    // blocking the target visibly, so when we have an overlay with a drop target,\n    // we should disable the drop target for the Editor, until it is closed.\n    //\n    // See FileBlockComponent for sample use.\n    //\n    // Relates to:\n    // - https://github.com/MixinNetwork/flutter-plugins/issues/2\n    // - https://github.com/MixinNetwork/flutter-plugins/issues/331\n    if (dropManagerState != null) {\n      return ChangeNotifierProvider.value(\n        value: dropManagerState!,\n        child: childWidget,\n      );\n    }\n\n    return ChangeNotifierProvider(\n      create: (_) => EditorDropManagerState(),\n      child: childWidget,\n    );\n  }\n\n  void _onDragUpdated(Offset position) {\n    final data = editorState.selectionService.getDropTargetRenderData(position);\n\n    if (dropManagerState?.isDropEnabled == false) {\n      return editorState.selectionService.removeDropTarget();\n    }\n\n    if (data != null &&\n        data.dropPath != null &&\n\n        // We implement custom Drop logic for image blocks, this is\n        // how we can exclude them from the Drop Target\n        !_excludeFromDropTarget.contains(data.cursorNode?.type)) {\n      // Render the drop target\n      editorState.selectionService.renderDropTargetForOffset(position);\n    } else {\n      editorState.selectionService.removeDropTarget();\n    }\n  }\n\n  Future<void> _onDragDone(DropDoneDetails details) async {\n    editorState.selectionService.removeDropTarget();\n\n    final data = editorState.selectionService\n        .getDropTargetRenderData(details.globalPosition);\n\n    if (data != null) {\n      final cursorNode = data.cursorNode;\n      final dropPath = data.dropPath;\n\n      if (cursorNode != null && dropPath != null) {\n        if (_excludeFromDropTarget.contains(cursorNode.type)) {\n          return;\n        }\n\n        for (final file in details.files) {\n          final fileName = file.name.toLowerCase();\n          if (file.mimeType?.startsWith('image/') ??\n              false || imgExtensionRegex.hasMatch(fileName)) {\n            await editorState.dropImages(dropPath, [file], viewId, isLocalMode);\n          } else {\n            await editorState.dropFiles(dropPath, [file], viewId, isLocalMode);\n          }\n        }\n      }\n    }\n  }\n\n  void _onDragViewDone(DragTargetDetails<ViewPB> details) {\n    editorState.selectionService.removeDropTarget();\n\n    final data =\n        editorState.selectionService.getDropTargetRenderData(details.offset);\n    if (data != null) {\n      final cursorNode = data.cursorNode;\n      final dropPath = data.dropPath;\n\n      if (cursorNode != null && dropPath != null) {\n        if (_excludeFromDropTarget.contains(cursorNode.type)) {\n          return;\n        }\n\n        final view = details.data;\n        final node = pageMentionNode(view.id);\n        final t = editorState.transaction..insertNode(dropPath, node);\n        editorState.apply(t);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_drop_manager.dart",
    "content": "import 'package:flutter/widgets.dart';\n\nclass EditorDropManagerState extends ChangeNotifier {\n  final Set<String> _draggedTypes = {};\n\n  void add(String type) {\n    _draggedTypes.add(type);\n    notifyListeners();\n  }\n\n  void remove(String type) {\n    _draggedTypes.remove(type);\n    notifyListeners();\n  }\n\n  bool get isDropEnabled => _draggedTypes.isEmpty;\n\n  bool contains(String type) => _draggedTypes.contains(type);\n}\n\nfinal enableDocumentDragNotifier = ValueNotifier(true);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_notification.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nenum EditorNotificationType {\n  none,\n  undo,\n  redo,\n  exitEditing,\n  paste,\n  dragStart,\n  dragEnd,\n  turnInto,\n}\n\nclass EditorNotification {\n  const EditorNotification({required this.type});\n\n  EditorNotification.undo() : type = EditorNotificationType.undo;\n  EditorNotification.redo() : type = EditorNotificationType.redo;\n  EditorNotification.exitEditing() : type = EditorNotificationType.exitEditing;\n  EditorNotification.paste() : type = EditorNotificationType.paste;\n  EditorNotification.dragStart() : type = EditorNotificationType.dragStart;\n  EditorNotification.dragEnd() : type = EditorNotificationType.dragEnd;\n  EditorNotification.turnInto() : type = EditorNotificationType.turnInto;\n\n  static final PropertyValueNotifier<EditorNotificationType> _notifier =\n      PropertyValueNotifier(EditorNotificationType.none);\n\n  final EditorNotificationType type;\n\n  void post() => _notifier.value = type;\n\n  static void addListener(ValueChanged<EditorNotificationType> listener) {\n    _notifier.addListener(() => listener(_notifier.value));\n  }\n\n  static void removeListener(ValueChanged<EditorNotificationType> listener) {\n    _notifier.removeListener(() => listener(_notifier.value));\n  }\n\n  static void dispose() => _notifier.dispose();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart",
    "content": "import 'dart:ui' as ui;\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_configuration.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/plugins/inline_actions/handlers/child_page.dart';\nimport 'package:appflowy/plugins/inline_actions/handlers/date_reference.dart';\nimport 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart';\nimport 'package:appflowy/plugins/inline_actions/handlers/reminder_reference.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/af_focus_manager.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart' hide QuoteBlockKeys;\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart';\nimport 'editor_plugins/toolbar_item/custom_format_toolbar_items.dart';\nimport 'editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart';\nimport 'editor_plugins/toolbar_item/custom_link_toolbar_item.dart';\nimport 'editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart';\nimport 'editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart';\nimport 'editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart';\nimport 'editor_plugins/toolbar_item/more_option_toolbar_item.dart';\nimport 'editor_plugins/toolbar_item/text_heading_toolbar_item.dart';\nimport 'editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart';\n\n/// Wrapper for the appflowy editor.\nclass AppFlowyEditorPage extends StatefulWidget {\n  const AppFlowyEditorPage({\n    super.key,\n    required this.editorState,\n    this.header,\n    this.shrinkWrap = false,\n    this.scrollController,\n    this.autoFocus,\n    required this.styleCustomizer,\n    this.showParagraphPlaceholder,\n    this.placeholderText,\n    this.initialSelection,\n    this.useViewInfoBloc = true,\n  });\n\n  final Widget? header;\n  final EditorState editorState;\n  final ScrollController? scrollController;\n  final bool shrinkWrap;\n  final bool? autoFocus;\n  final EditorStyleCustomizer styleCustomizer;\n  final ShowPlaceholder? showParagraphPlaceholder;\n  final String Function(Node)? placeholderText;\n\n  /// Used to provide an initial selection on Page-load\n  final Selection? initialSelection;\n\n  final bool useViewInfoBloc;\n\n  @override\n  State<AppFlowyEditorPage> createState() => _AppFlowyEditorPageState();\n}\n\nclass _AppFlowyEditorPageState extends State<AppFlowyEditorPage>\n    with WidgetsBindingObserver {\n  late final ScrollController effectiveScrollController;\n\n  late final InlineActionsService inlineActionsService = InlineActionsService(\n    context: context,\n    handlers: [\n      if (FeatureFlag.inlineSubPageMention.isOn)\n        InlineChildPageService(currentViewId: documentBloc.documentId),\n      InlinePageReferenceService(currentViewId: documentBloc.documentId),\n      DateReferenceService(context),\n      ReminderReferenceService(context),\n    ],\n  );\n\n  late final List<CommandShortcutEvent> commandShortcuts = [\n    ...commandShortcutEvents,\n    ..._buildFindAndReplaceCommands(),\n  ];\n\n  final List<ToolbarItem> toolbarItems = [\n    improveWritingItem,\n    group0PaddingItem,\n    aiWriterItem,\n    customTextHeadingItem,\n    buildPaddingPlaceholderItem(\n      1,\n      isActive: onlyShowInSingleTextTypeSelectionAndExcludeTable,\n    ),\n    ...customMarkdownFormatItems,\n    group1PaddingItem,\n    customTextColorItem,\n    group1PaddingItem,\n    customHighlightColorItem,\n    customInlineCodeItem,\n    suggestionsItem,\n    customLinkItem,\n    group4PaddingItem,\n    customTextAlignItem,\n    moreOptionItem,\n  ];\n\n  List<CharacterShortcutEvent> get characterShortcutEvents {\n    return buildCharacterShortcutEvents(\n      context,\n      documentBloc,\n      styleCustomizer,\n      inlineActionsService,\n      (editorState, node) => _customSlashMenuItems(\n        editorState: editorState,\n        node: node,\n      ),\n    );\n  }\n\n  EditorStyleCustomizer get styleCustomizer => widget.styleCustomizer;\n\n  DocumentBloc get documentBloc => context.read<DocumentBloc>();\n\n  late final EditorScrollController editorScrollController;\n\n  late final ViewInfoBloc viewInfoBloc = context.read<ViewInfoBloc>();\n\n  final editorKeyboardInterceptor = EditorKeyboardInterceptor();\n\n  Future<bool> showSlashMenu(editorState) async => customSlashCommand(\n        _customSlashMenuItems(),\n        shouldInsertSlash: false,\n        style: styleCustomizer.selectionMenuStyleBuilder(),\n        supportSlashMenuNodeTypes: supportSlashMenuNodeTypes,\n      ).handler(editorState);\n\n  AFFocusManager? focusManager;\n\n  AppLifecycleState? lifecycleState = WidgetsBinding.instance.lifecycleState;\n  List<Selection?> previousSelections = [];\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addObserver(this);\n\n    if (widget.useViewInfoBloc) {\n      viewInfoBloc.add(\n        ViewInfoEvent.registerEditorState(editorState: widget.editorState),\n      );\n    }\n\n    _initEditorL10n();\n    _initializeShortcuts();\n\n    AppFlowyRichTextKeys.partialSliced.addAll([\n      MentionBlockKeys.mention,\n      InlineMathEquationKeys.formula,\n    ]);\n\n    indentableBlockTypes.addAll([\n      ToggleListBlockKeys.type,\n      CalloutBlockKeys.type,\n      QuoteBlockKeys.type,\n    ]);\n    convertibleBlockTypes.addAll([\n      ToggleListBlockKeys.type,\n      CalloutBlockKeys.type,\n      QuoteBlockKeys.type,\n    ]);\n\n    editorLaunchUrl = (url) {\n      if (url != null) {\n        afLaunchUrlString(url, addingHttpSchemeWhenFailed: true);\n      }\n\n      return Future.value(true);\n    };\n\n    effectiveScrollController = widget.scrollController ?? ScrollController();\n    // disable the color parse in the HTML decoder.\n    DocumentHTMLDecoder.enableColorParse = false;\n\n    editorScrollController = EditorScrollController(\n      editorState: widget.editorState,\n      shrinkWrap: widget.shrinkWrap,\n      scrollController: effectiveScrollController,\n    );\n\n    toolbarItemWhiteList.addAll([\n      ToggleListBlockKeys.type,\n      CalloutBlockKeys.type,\n      TableBlockKeys.type,\n      SimpleTableBlockKeys.type,\n      SimpleTableCellBlockKeys.type,\n      SimpleTableRowBlockKeys.type,\n    ]);\n    AppFlowyRichTextKeys.supportSliced.add(AppFlowyRichTextKeys.fontFamily);\n\n    // customize the dynamic theme color\n    _customizeBlockComponentBackgroundColorDecorator();\n\n    widget.editorState.selectionNotifier.addListener(onSelectionChanged);\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      if (!mounted) {\n        return;\n      }\n\n      focusManager = AFFocusManager.maybeOf(context);\n      focusManager?.loseFocusNotifier.addListener(_loseFocus);\n\n      _scrollToSelectionIfNeeded();\n\n      widget.editorState.service.keyboardService?.registerInterceptor(\n        editorKeyboardInterceptor,\n      );\n    });\n  }\n\n  void _scrollToSelectionIfNeeded() {\n    final initialSelection = widget.initialSelection;\n    final path = initialSelection?.start.path;\n    if (path == null) {\n      return;\n    }\n\n    // on desktop, using jumpTo to scroll to the selection.\n    // on mobile, using scrollTo to scroll to the selection, because using jumpTo will break the scroll notification metrics.\n    if (UniversalPlatform.isDesktop) {\n      editorScrollController.itemScrollController.jumpTo(\n        index: path.first,\n        alignment: 0.5,\n      );\n      widget.editorState.updateSelectionWithReason(\n        initialSelection,\n      );\n    } else {\n      const delayDuration = Duration(milliseconds: 250);\n      const animationDuration = Duration(milliseconds: 400);\n      Future.delayed(delayDuration, () {\n        editorScrollController.itemScrollController.scrollTo(\n          index: path.first,\n          duration: animationDuration,\n          curve: Curves.easeInOut,\n        );\n        widget.editorState.updateSelectionWithReason(\n          initialSelection,\n          extraInfo: {\n            selectionExtraInfoDoNotAttachTextService: true,\n            selectionExtraInfoDisableMobileToolbarKey: true,\n          },\n        );\n      }).then((_) {\n        Future.delayed(animationDuration, () {\n          widget.editorState.selectionType = SelectionType.inline;\n          widget.editorState.selectionExtraInfo = null;\n        });\n      });\n    }\n  }\n\n  void onSelectionChanged() {\n    if (widget.editorState.isDisposed) {\n      return;\n    }\n\n    previousSelections.add(widget.editorState.selection);\n\n    if (previousSelections.length > 2) {\n      previousSelections.removeAt(0);\n    }\n  }\n\n  @override\n  void didChangeAppLifecycleState(AppLifecycleState state) {\n    super.didChangeAppLifecycleState(state);\n    lifecycleState = state;\n\n    if (widget.editorState.isDisposed) {\n      return;\n    }\n\n    if (previousSelections.length == 2 &&\n        state == AppLifecycleState.resumed &&\n        widget.editorState.selection == null) {\n      widget.editorState.selection = previousSelections.first;\n    }\n  }\n\n  @override\n  void didChangeDependencies() {\n    final currFocusManager = AFFocusManager.maybeOf(context);\n    if (focusManager != currFocusManager) {\n      focusManager?.loseFocusNotifier.removeListener(_loseFocus);\n      focusManager = currFocusManager;\n      focusManager?.loseFocusNotifier.addListener(_loseFocus);\n    }\n\n    super.didChangeDependencies();\n  }\n\n  @override\n  void dispose() {\n    widget.editorState.selectionNotifier.removeListener(onSelectionChanged);\n    widget.editorState.service.keyboardService?.unregisterInterceptor(\n      editorKeyboardInterceptor,\n    );\n    focusManager?.loseFocusNotifier.removeListener(_loseFocus);\n\n    if (widget.useViewInfoBloc && !viewInfoBloc.isClosed) {\n      viewInfoBloc.add(const ViewInfoEvent.unregisterEditorState());\n    }\n\n    SystemChannels.textInput.invokeMethod('TextInput.hide');\n\n    if (widget.scrollController == null) {\n      effectiveScrollController.dispose();\n    }\n    inlineActionsService.dispose();\n    editorScrollController.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final (bool autoFocus, Selection? selection) =\n        _computeAutoFocusParameters();\n\n    final isRTL =\n        context.read<AppearanceSettingsCubit>().state.layoutDirection ==\n            LayoutDirection.rtlLayout;\n    final textDirection = isRTL ? ui.TextDirection.rtl : ui.TextDirection.ltr;\n\n    _setRTLToolbarItems(\n      context.read<AppearanceSettingsCubit>().state.enableRtlToolbarItems,\n    );\n\n    final isViewDeleted = context.read<DocumentBloc>().state.isDeleted;\n    final isEditable =\n        context.read<PageAccessLevelBloc?>()?.state.isEditable ?? true;\n\n    final editor = Directionality(\n      textDirection: textDirection,\n      child: AppFlowyEditor(\n        editorState: widget.editorState,\n        editable: !isViewDeleted && isEditable,\n        disableSelectionService: UniversalPlatform.isMobile && !isEditable,\n        disableKeyboardService: UniversalPlatform.isMobile && !isEditable,\n        editorScrollController: editorScrollController,\n        // setup the auto focus parameters\n        autoFocus: widget.autoFocus ?? autoFocus,\n        focusedSelection: selection,\n        // setup the theme\n        editorStyle: styleCustomizer.style(),\n        // customize the block builders\n        blockComponentBuilders: buildBlockComponentBuilders(\n          slashMenuItemsBuilder: (editorState, node) => _customSlashMenuItems(\n            editorState: editorState,\n            node: node,\n          ),\n          context: context,\n          editorState: widget.editorState,\n          styleCustomizer: widget.styleCustomizer,\n          showParagraphPlaceholder: widget.showParagraphPlaceholder,\n          placeholderText: widget.placeholderText,\n        ),\n        // customize the shortcuts\n        characterShortcutEvents: characterShortcutEvents,\n        commandShortcutEvents: commandShortcuts,\n        // customize the context menu items\n        contextMenuItems: customContextMenuItems,\n        // customize the header and footer.\n        header: widget.header,\n        autoScrollEdgeOffset: UniversalPlatform.isDesktopOrWeb\n            ? 250\n            : appFlowyEditorAutoScrollEdgeOffset,\n        footer: GestureDetector(\n          behavior: HitTestBehavior.translucent,\n          onTap: () async {\n            // if the last one isn't a empty node, insert a new empty node.\n            await _focusOnLastEmptyParagraph();\n          },\n          child: SizedBox(\n            width: double.infinity,\n            height: UniversalPlatform.isDesktopOrWeb ? 600 : 400,\n          ),\n        ),\n        dropTargetStyle: AppFlowyDropTargetStyle(\n          color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.8),\n          margin: const EdgeInsets.only(left: 44),\n        ),\n      ),\n    );\n\n    if (isViewDeleted) {\n      return editor;\n    }\n\n    final editorState = widget.editorState;\n\n    if (UniversalPlatform.isMobile) {\n      return AppFlowyMobileToolbar(\n        toolbarHeight: 42.0,\n        editorState: editorState,\n        toolbarItemsBuilder: (sel) => buildMobileToolbarItems(editorState, sel),\n        child: MobileFloatingToolbar(\n          editorState: editorState,\n          editorScrollController: editorScrollController,\n          toolbarBuilder: (_, anchor, closeToolbar) =>\n              CustomMobileFloatingToolbar(\n            editorState: editorState,\n            anchor: anchor,\n            closeToolbar: closeToolbar,\n          ),\n          floatingToolbarHeight: 32,\n          child: editor,\n        ),\n      );\n    }\n    final appTheme = AppFlowyTheme.of(context);\n    return Center(\n      child: BlocProvider.value(\n        value: context.read<DocumentBloc>(),\n        child: FloatingToolbar(\n          floatingToolbarHeight: 40,\n          padding: EdgeInsets.symmetric(horizontal: 6),\n          style: FloatingToolbarStyle(\n            backgroundColor: Theme.of(context).cardColor,\n            toolbarActiveColor: Color(0xffe0f8fd),\n          ),\n          items: toolbarItems,\n          decoration: BoxDecoration(\n            borderRadius: BorderRadius.circular(appTheme.borderRadius.l),\n            color: appTheme.surfaceColorScheme.primary,\n            boxShadow: appTheme.shadow.small,\n          ),\n          toolbarBuilder: (_, child, onDismiss, isMetricsChanged) =>\n              BlocProvider.value(\n            value: context.read<DocumentBloc>(),\n            child: DesktopFloatingToolbar(\n              editorState: editorState,\n              onDismiss: onDismiss,\n              enableAnimation: !isMetricsChanged,\n              child: child,\n            ),\n          ),\n          placeHolderBuilder: (_) => customPlaceholderItem,\n          editorState: editorState,\n          editorScrollController: editorScrollController,\n          textDirection: textDirection,\n          tooltipBuilder: (context, id, message, child) =>\n              widget.styleCustomizer.buildToolbarItemTooltip(\n            context,\n            id,\n            message,\n            child,\n          ),\n          child: editor,\n        ),\n      ),\n    );\n  }\n\n  List<SelectionMenuItem> _customSlashMenuItems({\n    EditorState? editorState,\n    Node? node,\n  }) {\n    final documentBloc = context.read<DocumentBloc>();\n    final isLocalMode = documentBloc.isLocalMode;\n    final view = context.read<ViewBloc>().state.view;\n    return slashMenuItemsBuilder(\n      editorState: editorState,\n      node: node,\n      isLocalMode: isLocalMode,\n      documentBloc: documentBloc,\n      view: view,\n    );\n  }\n\n  (bool, Selection?) _computeAutoFocusParameters() {\n    if (widget.editorState.document.isEmpty) {\n      return (true, Selection.collapsed(Position(path: [0])));\n    }\n    return const (false, null);\n  }\n\n  Future<void> _initializeShortcuts() async {\n    defaultCommandShortcutEvents;\n    final settingsShortcutService = SettingsShortcutService();\n    final customizeShortcuts =\n        await settingsShortcutService.getCustomizeShortcuts();\n    await settingsShortcutService.updateCommandShortcuts(\n      commandShortcuts,\n      customizeShortcuts,\n    );\n  }\n\n  void _setRTLToolbarItems(bool enableRtlToolbarItems) {\n    final textDirectionItemIds = textDirectionItems.map((e) => e.id);\n    // clear all the text direction items\n    toolbarItems.removeWhere((item) => textDirectionItemIds.contains(item.id));\n    // only show the rtl item when the layout direction is ltr.\n    if (enableRtlToolbarItems) {\n      toolbarItems.addAll(textDirectionItems);\n    }\n  }\n\n  List<CommandShortcutEvent> _buildFindAndReplaceCommands() {\n    return findAndReplaceCommands(\n      context: context,\n      style: FindReplaceStyle(\n        findMenuBuilder: (\n          context,\n          editorState,\n          localizations,\n          style,\n          showReplaceMenu,\n          onDismiss,\n        ) =>\n            Material(\n          child: DecoratedBox(\n            decoration: BoxDecoration(\n              color: Theme.of(context).colorScheme.surfaceContainerHighest,\n              borderRadius: BorderRadius.circular(4),\n            ),\n            child: FindAndReplaceMenuWidget(\n              showReplaceMenu: showReplaceMenu,\n              editorState: editorState,\n              onDismiss: onDismiss,\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  void _customizeBlockComponentBackgroundColorDecorator() {\n    blockComponentBackgroundColorDecorator = (Node node, String colorString) {\n      if (mounted && context.mounted) {\n        return buildEditorCustomizedColor(context, node, colorString);\n      }\n      return null;\n    };\n  }\n\n  void _initEditorL10n() => AppFlowyEditorL10n.current = EditorI18n();\n\n  Future<void> _focusOnLastEmptyParagraph() async {\n    final editorState = widget.editorState;\n    final root = editorState.document.root;\n    final lastNode = root.children.lastOrNull;\n    final transaction = editorState.transaction;\n    if (lastNode == null ||\n        lastNode.delta?.isEmpty == false ||\n        lastNode.type != ParagraphBlockKeys.type) {\n      transaction.insertNode([root.children.length], paragraphNode());\n      transaction.afterSelection = Selection.collapsed(\n        Position(path: [root.children.length]),\n      );\n    } else {\n      transaction.afterSelection = Selection.collapsed(\n        Position(path: lastNode.path),\n      );\n    }\n\n    transaction.customSelectionType = SelectionType.inline;\n    transaction.reason = SelectionUpdateReason.uiEvent;\n\n    await editorState.apply(transaction);\n  }\n\n  void _loseFocus() {\n    if (!widget.editorState.isDisposed) {\n      widget.editorState.selection = null;\n    }\n  }\n}\n\nColor? buildEditorCustomizedColor(\n  BuildContext context,\n  Node node,\n  String colorString,\n) {\n  if (!context.mounted) {\n    return null;\n  }\n\n  // the color string is from FlowyTint.\n  final tintColor = FlowyTint.values.firstWhereOrNull(\n    (e) => e.id == colorString,\n  );\n  if (tintColor != null) {\n    return tintColor.color(context);\n  }\n\n  final themeColor = themeBackgroundColors[colorString];\n  if (themeColor != null) {\n    return themeColor.color(context);\n  }\n\n  if (colorString == optionActionColorDefaultColor) {\n    final defaultColor = node.type == CalloutBlockKeys.type\n        ? AFThemeExtension.of(context).calloutBGColor\n        : Colors.transparent;\n    return defaultColor;\n  }\n\n  if (colorString == tableCellDefaultColor) {\n    return AFThemeExtension.of(context).tableCellBGColor;\n  }\n\n  try {\n    return colorString.tryToColor();\n  } catch (e) {\n    return null;\n  }\n}\n\nbool showInAnyTextType(EditorState editorState) {\n  final selection = editorState.selection;\n  if (selection == null) {\n    return false;\n  }\n\n  final nodes = editorState.getNodesInSelection(selection);\n  return nodes.any((node) => toolbarItemWhiteList.contains(node.type));\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nclass BlockAddButton extends StatelessWidget {\n  const BlockAddButton({\n    super.key,\n    required this.blockComponentContext,\n    required this.blockComponentState,\n    required this.editorState,\n    required this.showSlashMenu,\n  });\n\n  final BlockComponentContext blockComponentContext;\n  final BlockComponentActionState blockComponentState;\n\n  final EditorState editorState;\n  final VoidCallback showSlashMenu;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlockActionButton(\n      svg: FlowySvgs.add_s,\n      richMessage: TextSpan(\n        children: [\n          TextSpan(\n            text: LocaleKeys.blockActions_addBelowTooltip.tr(),\n            style: context.tooltipTextStyle(),\n          ),\n          const TextSpan(text: '\\n'),\n          TextSpan(\n            text: Platform.isMacOS\n                ? LocaleKeys.blockActions_addAboveMacCmd.tr()\n                : LocaleKeys.blockActions_addAboveCmd.tr(),\n            style: context.tooltipTextStyle(),\n          ),\n          const TextSpan(text: ' '),\n          TextSpan(\n            text: LocaleKeys.blockActions_addAboveTooltip.tr(),\n            style: context.tooltipTextStyle(),\n          ),\n        ],\n      ),\n      onTap: () {\n        final isAltPressed = HardwareKeyboard.instance.isAltPressed;\n\n        final transaction = editorState.transaction;\n\n        // If the current block is not an empty paragraph block,\n        // then insert a new block above/below the current block.\n        final node = blockComponentContext.node;\n        if (node.type != ParagraphBlockKeys.type ||\n            (node.delta?.isNotEmpty ?? true)) {\n          final path = isAltPressed ? node.path : node.path.next;\n\n          transaction.insertNode(path, paragraphNode());\n          transaction.afterSelection = Selection.collapsed(\n            Position(path: path),\n          );\n        } else {\n          transaction.afterSelection = Selection.collapsed(\n            Position(path: node.path),\n          );\n        }\n\n        // show the slash menu.\n        editorState.apply(transaction).then(\n              (_) => WidgetsBinding.instance.addPostFrameCallback(\n                (_) => showSlashMenu(),\n              ),\n            );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass BlockActionButton extends StatelessWidget {\n  const BlockActionButton({\n    super.key,\n    required this.svg,\n    required this.richMessage,\n    required this.onTap,\n    this.showTooltip = true,\n    this.onPointerDown,\n  });\n\n  final FlowySvgData svg;\n  final bool showTooltip;\n  final InlineSpan richMessage;\n  final VoidCallback onTap;\n  final VoidCallback? onPointerDown;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      richMessage: showTooltip ? richMessage : null,\n      child: FlowyIconButton(\n        width: 18.0,\n        hoverColor: Colors.transparent,\n        iconColorOnHover: Theme.of(context).iconTheme.color,\n        onPressed: onTap,\n        icon: MouseRegion(\n          cursor: Platform.isWindows\n              ? SystemMouseCursors.click\n              : SystemMouseCursors.grab,\n          child: FlowySvg(\n            svg,\n            size: const Size.square(18.0),\n            color: Theme.of(context).iconTheme.color,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_list.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass BlockActionList extends StatelessWidget {\n  const BlockActionList({\n    super.key,\n    required this.blockComponentContext,\n    required this.blockComponentState,\n    required this.editorState,\n    required this.actions,\n    required this.showSlashMenu,\n    required this.blockComponentBuilder,\n  });\n\n  final BlockComponentContext blockComponentContext;\n  final BlockComponentActionState blockComponentState;\n  final List<OptionAction> actions;\n  final VoidCallback showSlashMenu;\n  final EditorState editorState;\n  final Map<String, BlockComponentBuilder> blockComponentBuilder;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.end,\n      children: [\n        BlockAddButton(\n          blockComponentContext: blockComponentContext,\n          blockComponentState: blockComponentState,\n          editorState: editorState,\n          showSlashMenu: showSlashMenu,\n        ),\n        const HSpace(2.0),\n        BlockOptionButton(\n          blockComponentContext: blockComponentContext,\n          blockComponentState: blockComponentState,\n          actions: actions,\n          editorState: editorState,\n          blockComponentBuilder: blockComponentBuilder,\n        ),\n        const HSpace(5.0),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'drag_to_reorder/draggable_option_button.dart';\n\nclass BlockOptionButton extends StatefulWidget {\n  const BlockOptionButton({\n    super.key,\n    required this.blockComponentContext,\n    required this.blockComponentState,\n    required this.actions,\n    required this.editorState,\n    required this.blockComponentBuilder,\n  });\n\n  final BlockComponentContext blockComponentContext;\n  final BlockComponentActionState blockComponentState;\n  final List<OptionAction> actions;\n  final EditorState editorState;\n  final Map<String, BlockComponentBuilder> blockComponentBuilder;\n\n  @override\n  State<BlockOptionButton> createState() => _BlockOptionButtonState();\n}\n\nclass _BlockOptionButtonState extends State<BlockOptionButton> {\n  // the mutex is used to ensure that only one popover is open at a time\n  // for example, when the user is selecting the color, the turn into option\n  // should not be shown.\n  final mutex = PopoverMutex();\n\n  @override\n  Widget build(BuildContext context) {\n    final direction =\n        context.read<AppearanceSettingsCubit>().state.layoutDirection ==\n                LayoutDirection.rtlLayout\n            ? PopoverDirection.rightWithCenterAligned\n            : PopoverDirection.leftWithCenterAligned;\n    return BlocProvider(\n      create: (context) => BlockActionOptionCubit(\n        editorState: widget.editorState,\n        blockComponentBuilder: widget.blockComponentBuilder,\n      ),\n      child: BlocBuilder<BlockActionOptionCubit, BlockActionOptionState>(\n        builder: (context, _) => PopoverActionList<PopoverAction>(\n          actions: _buildPopoverActions(context),\n          animationDuration: Durations.short3,\n          slideDistance: 5,\n          beginScaleFactor: 1.0,\n          beginOpacity: 0.8,\n          direction: direction,\n          onPopupBuilder: _onPopoverBuilder,\n          onClosed: () => _onPopoverClosed(context),\n          onSelected: (action, controller) => _onActionSelected(\n            context,\n            action,\n            controller,\n          ),\n          buildChild: (controller) => DraggableOptionButton(\n            controller: controller,\n            editorState: widget.editorState,\n            blockComponentContext: widget.blockComponentContext,\n            blockComponentBuilder: widget.blockComponentBuilder,\n          ),\n        ),\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    mutex.dispose();\n\n    super.dispose();\n  }\n\n  List<PopoverAction> _buildPopoverActions(BuildContext context) {\n    return widget.actions.map((e) {\n      switch (e) {\n        case OptionAction.divider:\n          return DividerOptionAction();\n        case OptionAction.color:\n          return ColorOptionAction(\n            editorState: widget.editorState,\n            mutex: mutex,\n          );\n        case OptionAction.align:\n          return AlignOptionAction(editorState: widget.editorState);\n        case OptionAction.depth:\n          return DepthOptionAction(editorState: widget.editorState);\n        case OptionAction.turnInto:\n          return TurnIntoOptionAction(\n            editorState: widget.editorState,\n            blockComponentBuilder: widget.blockComponentBuilder,\n            mutex: mutex,\n          );\n        default:\n          return OptionActionWrapper(e);\n      }\n    }).toList();\n  }\n\n  void _onPopoverBuilder() {\n    keepEditorFocusNotifier.increase();\n    widget.blockComponentState.alwaysShowActions = true;\n  }\n\n  void _onPopoverClosed(BuildContext context) {\n    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n      widget.editorState.selectionType = null;\n      widget.editorState.selection = null;\n      widget.blockComponentState.alwaysShowActions = false;\n    });\n\n    PopoverContainer.maybeOf(context)?.closeAll();\n  }\n\n  void _onActionSelected(\n    BuildContext context,\n    PopoverAction action,\n    PopoverController controller,\n  ) {\n    if (action is! OptionActionWrapper) {\n      return;\n    }\n\n    context.read<BlockActionOptionCubit>().handleAction(\n          action.inner,\n          widget.blockComponentContext.node,\n        );\n    controller.close();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart",
    "content": "import 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_notification.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/shared/share/constants.dart';\nimport 'package:appflowy/plugins/trash/application/prelude.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart'\n    hide QuoteBlockKeys, quoteNode;\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass BlockActionOptionState {}\n\nclass BlockActionOptionCubit extends Cubit<BlockActionOptionState> {\n  BlockActionOptionCubit({\n    required this.editorState,\n    required this.blockComponentBuilder,\n  }) : super(BlockActionOptionState());\n\n  final EditorState editorState;\n  final Map<String, BlockComponentBuilder> blockComponentBuilder;\n\n  Future<void> handleAction(OptionAction action, Node node) async {\n    final transaction = editorState.transaction;\n    switch (action) {\n      case OptionAction.delete:\n        _deleteBlocks(transaction, node);\n        break;\n      case OptionAction.duplicate:\n        await _duplicateBlock(transaction, node);\n        EditorNotification.paste().post();\n        break;\n      case OptionAction.moveUp:\n        transaction.moveNode(node.path.previous, node);\n        break;\n      case OptionAction.moveDown:\n        transaction.moveNode(node.path.next.next, node);\n        break;\n      case OptionAction.copyLinkToBlock:\n        await _copyLinkToBlock(node);\n        break;\n      case OptionAction.setToPageWidth:\n        await _setToPageWidth(node);\n        break;\n      case OptionAction.distributeColumnsEvenly:\n        await _distributeColumnsEvenly(node);\n        break;\n      case OptionAction.align:\n      case OptionAction.color:\n      case OptionAction.divider:\n      case OptionAction.depth:\n      case OptionAction.turnInto:\n        throw UnimplementedError();\n    }\n\n    await editorState.apply(transaction);\n  }\n\n  /// If the selection is a block selection, delete the selected blocks.\n  /// Otherwise, delete the selected block.\n  void _deleteBlocks(Transaction transaction, Node selectedNode) {\n    final selection = editorState.selection;\n    final selectionType = editorState.selectionType;\n    if (selectionType == SelectionType.block && selection != null) {\n      final nodes = editorState.getNodesInSelection(selection.normalized);\n      transaction.deleteNodes(nodes);\n    } else {\n      transaction.deleteNode(selectedNode);\n    }\n  }\n\n  Future<void> _duplicateBlock(Transaction transaction, Node node) async {\n    final selection = editorState.selection;\n    final selectionType = editorState.selectionType;\n    if (selectionType == SelectionType.block && selection != null) {\n      final nodes = editorState.getNodesInSelection(selection.normalized);\n      for (final node in nodes) {\n        _validateNode(node);\n      }\n      transaction.insertNodes(\n        selection.normalized.end.path.next,\n        nodes.map((e) => _copyBlock(e)).toList(),\n      );\n    } else {\n      _validateNode(node);\n      transaction.insertNode(node.path.next, _copyBlock(node));\n    }\n  }\n\n  void _validateNode(Node node) {\n    final type = node.type;\n    final builder = blockComponentBuilder[type];\n\n    if (builder == null) {\n      Log.error('Block type $type is not supported');\n      return;\n    }\n\n    final valid = builder.validate(node);\n    if (!valid) {\n      Log.error('Block type $type is not valid');\n    }\n  }\n\n  Node _copyBlock(Node node) {\n    Node copiedNode = node.deepCopy();\n\n    final type = node.type;\n    final builder = blockComponentBuilder[type];\n\n    if (builder == null) {\n      Log.error('Block type $type is not supported');\n    } else {\n      final valid = builder.validate(node);\n      if (!valid) {\n        Log.error('Block type $type is not valid');\n        if (node.type == TableBlockKeys.type) {\n          copiedNode = _fixTableBlock(node);\n          copiedNode = _convertTableToSimpleTable(copiedNode);\n        }\n      } else {\n        if (node.type == TableBlockKeys.type) {\n          copiedNode = _convertTableToSimpleTable(node);\n        }\n      }\n    }\n\n    return copiedNode;\n  }\n\n  Node _fixTableBlock(Node node) {\n    if (node.type != TableBlockKeys.type) {\n      return node;\n    }\n\n    // the table node should contains colsLen and rowsLen\n    final colsLen = node.attributes[TableBlockKeys.colsLen];\n    final rowsLen = node.attributes[TableBlockKeys.rowsLen];\n    if (colsLen == null || rowsLen == null) {\n      return node;\n    }\n\n    final newChildren = <Node>[];\n    final children = node.children;\n\n    // based on the colsLen and rowsLen, iterate the children and fix the data\n    for (var i = 0; i < rowsLen; i++) {\n      for (var j = 0; j < colsLen; j++) {\n        final cell = children\n            .where(\n              (n) =>\n                  n.attributes[TableCellBlockKeys.rowPosition] == i &&\n                  n.attributes[TableCellBlockKeys.colPosition] == j,\n            )\n            .firstOrNull;\n        if (cell != null) {\n          newChildren.add(cell.deepCopy());\n        } else {\n          newChildren.add(\n            tableCellNode('', i, j),\n          );\n        }\n      }\n    }\n\n    return node.copyWith(\n      children: newChildren,\n      attributes: {\n        ...node.attributes,\n        TableBlockKeys.colsLen: colsLen,\n        TableBlockKeys.rowsLen: rowsLen,\n      },\n    );\n  }\n\n  Node _convertTableToSimpleTable(Node node) {\n    if (node.type != TableBlockKeys.type) {\n      return node;\n    }\n\n    // the table node should contains colsLen and rowsLen\n    final colsLen = node.attributes[TableBlockKeys.colsLen];\n    final rowsLen = node.attributes[TableBlockKeys.rowsLen];\n    if (colsLen == null || rowsLen == null) {\n      return node;\n    }\n\n    final rows = <List<Node>>[];\n    final children = node.children;\n    for (var i = 0; i < rowsLen; i++) {\n      final row = <Node>[];\n      for (var j = 0; j < colsLen; j++) {\n        final cell = children\n            .where(\n              (n) =>\n                  n.attributes[TableCellBlockKeys.rowPosition] == i &&\n                  n.attributes[TableCellBlockKeys.colPosition] == j,\n            )\n            .firstOrNull;\n        row.add(\n          simpleTableCellBlockNode(\n            children: [cell?.children.first.deepCopy() ?? paragraphNode()],\n          ),\n        );\n      }\n      rows.add(row);\n    }\n\n    return simpleTableBlockNode(\n      children: rows.map((e) => simpleTableRowBlockNode(children: e)).toList(),\n    );\n  }\n\n  Future<void> _copyLinkToBlock(Node node) async {\n    List<Node> nodes = [node];\n\n    final selection = editorState.selection;\n    final selectionType = editorState.selectionType;\n    if (selectionType == SelectionType.block && selection != null) {\n      nodes = editorState.getNodesInSelection(selection.normalized);\n    }\n\n    final context = editorState.document.root.context;\n    final viewId = context?.read<DocumentBloc>().documentId;\n    if (viewId == null) {\n      return;\n    }\n\n    final workspace = await FolderEventReadCurrentWorkspace().send();\n    final workspaceId = workspace.fold(\n      (l) => l.id,\n      (r) => '',\n    );\n\n    if (workspaceId.isEmpty || viewId.isEmpty) {\n      Log.error('Failed to get workspace id: $workspaceId or view id: $viewId');\n      emit(BlockActionOptionState()); // Emit a new state to trigger UI update\n      return;\n    }\n\n    final blockIds = nodes.map((e) => e.id);\n    final links = blockIds.map(\n      (e) => ShareConstants.buildShareUrl(\n        workspaceId: workspaceId,\n        viewId: viewId,\n        blockId: e,\n      ),\n    );\n\n    await getIt<ClipboardService>().setData(\n      ClipboardServiceData(plainText: links.join('\\n')),\n    );\n\n    emit(BlockActionOptionState()); // Emit a new state to trigger UI update\n  }\n\n  static Future<bool> turnIntoBlock(\n    String type,\n    Node node,\n    EditorState editorState, {\n    int? level,\n    String? currentViewId,\n    bool keepSelection = false,\n  }) async {\n    final selection = editorState.selection;\n    if (selection == null) {\n      return false;\n    }\n\n    // Notify the transaction service that the next apply is from turn into action\n    EditorNotification.turnInto().post();\n\n    final toType = type;\n\n    // only handle the node in the same depth\n    final selectedNodes = editorState\n        .getNodesInSelection(selection.normalized)\n        .where((e) => e.path.length == node.path.length)\n        .toList();\n    Log.info('turnIntoBlock selectedNodes $selectedNodes');\n\n    // try to turn into a single toggle heading block\n    if (await turnIntoSingleToggleHeading(\n      type: toType,\n      selectedNodes: selectedNodes,\n      level: level,\n      editorState: editorState,\n      afterSelection: keepSelection ? selection : null,\n    )) {\n      return true;\n    }\n\n    // try to turn into a page block\n    if (currentViewId != null &&\n        await turnIntoPage(\n          type: toType,\n          selectedNodes: selectedNodes,\n          selection: selection,\n          currentViewId: currentViewId,\n          editorState: editorState,\n        )) {\n      return true;\n    }\n\n    final insertedNode = <Node>[];\n    for (final node in selectedNodes) {\n      Log.info('Turn into block: from ${node.type} to $type');\n\n      Node afterNode = node.copyWith(\n        type: type,\n        attributes: {\n          if (toType == HeadingBlockKeys.type) HeadingBlockKeys.level: level,\n          if (toType == ToggleListBlockKeys.type)\n            ToggleListBlockKeys.level: level,\n          if (toType == TodoListBlockKeys.type)\n            TodoListBlockKeys.checked: false,\n          blockComponentBackgroundColor:\n              node.attributes[blockComponentBackgroundColor],\n          blockComponentTextDirection:\n              node.attributes[blockComponentTextDirection],\n          blockComponentDelta: (node.delta ?? Delta()).toJson(),\n        },\n      );\n\n      // heading block should not have children\n      if ([HeadingBlockKeys.type].contains(toType)) {\n        afterNode = afterNode.copyWith(children: []);\n        afterNode = await _handleSubPageNode(afterNode, node);\n        insertedNode.add(afterNode);\n        insertedNode.addAll(node.children.map((e) => e.deepCopy()));\n      } else if (!EditorOptionActionType.turnInto.supportTypes\n          .contains(node.type)) {\n        afterNode = node.deepCopy();\n        insertedNode.add(afterNode);\n      } else {\n        afterNode = await _handleSubPageNode(afterNode, node);\n        insertedNode.add(afterNode);\n      }\n    }\n\n    final transaction = editorState.transaction;\n    transaction.insertNodes(\n      node.path,\n      insertedNode,\n    );\n    transaction.deleteNodes(selectedNodes);\n    if (keepSelection) transaction.afterSelection = selection;\n    await editorState.apply(transaction);\n\n    return true;\n  }\n\n  /// Takes the new [Node] and the Node which is a SubPageBlock.\n  ///\n  /// Returns the altered [Node] with the delta as the Views' name.\n  ///\n  static Future<Node> _handleSubPageNode(Node node, Node subPageNode) async {\n    if (subPageNode.type != SubPageBlockKeys.type) {\n      return node;\n    }\n\n    final delta = await _deltaFromSubPageNode(subPageNode);\n    return node.copyWith(\n      attributes: {\n        ...node.attributes,\n        blockComponentDelta: (delta ?? Delta()).toJson(),\n      },\n    );\n  }\n\n  /// Returns the [Delta] from a SubPage [Node], where the\n  /// [Delta] is the views' name.\n  ///\n  static Future<Delta?> _deltaFromSubPageNode(Node node) async {\n    if (node.type != SubPageBlockKeys.type) {\n      return null;\n    }\n\n    final viewId = node.attributes[SubPageBlockKeys.viewId];\n    final viewOrFailure = await ViewBackendService.getView(viewId);\n    final view = viewOrFailure.toNullable();\n    if (view != null) {\n      return Delta(operations: [TextInsert(view.name)]);\n    }\n\n    Log.error(\"Failed to get view by id($viewId)\");\n    return null;\n  }\n\n  // turn a single node into toggle heading block\n  // 1. find the sibling nodes after the selected node until\n  //  meet the first node that contains level and its value is greater or equal to the level\n  // 2. move the found nodes in the selected node\n  //\n  // example:\n  // Toggle Heading 1 <- selected node\n  // - bulleted item 1\n  // - bulleted item 2\n  // - bulleted item 3\n  // Heading 1\n  // - paragraph 1\n  // - paragraph 2\n  // when turning \"Toggle Heading 1\" into toggle heading, the bulleted items will be moved into the toggle heading\n  static Future<bool> turnIntoSingleToggleHeading({\n    required String type,\n    required List<Node> selectedNodes,\n    required EditorState editorState,\n    int? level,\n    Delta? delta,\n    Selection? afterSelection,\n  }) async {\n    // only support turn a single node into toggle heading block\n    if (type != ToggleListBlockKeys.type ||\n        selectedNodes.length != 1 ||\n        level == null) {\n      return false;\n    }\n\n    // find the sibling nodes after the selected node until\n    final insertedNodes = <Node>[];\n    final node = selectedNodes.first;\n    Path path = node.path.next;\n    Node? nextNode = editorState.getNodeAtPath(path);\n    while (nextNode != null) {\n      if (nextNode.type == HeadingBlockKeys.type &&\n          nextNode.attributes[HeadingBlockKeys.level] != null &&\n          nextNode.attributes[HeadingBlockKeys.level]! <= level) {\n        break;\n      }\n\n      if (nextNode.type == ToggleListBlockKeys.type &&\n          nextNode.attributes[ToggleListBlockKeys.level] != null &&\n          nextNode.attributes[ToggleListBlockKeys.level]! <= level) {\n        break;\n      }\n\n      insertedNodes.add(nextNode);\n\n      path = path.next;\n      nextNode = editorState.getNodeAtPath(path);\n    }\n\n    Log.info('insertedNodes $insertedNodes');\n\n    Log.info(\n      'Turn into block: from ${node.type} to $type',\n    );\n\n    Delta newDelta = delta ?? (node.delta ?? Delta());\n    if (delta == null && node.type == SubPageBlockKeys.type) {\n      newDelta = await _deltaFromSubPageNode(node) ?? Delta();\n    }\n\n    final afterNode = node.copyWith(\n      type: type,\n      attributes: {\n        ToggleListBlockKeys.level: level,\n        ToggleListBlockKeys.collapsed:\n            node.attributes[ToggleListBlockKeys.collapsed] ?? false,\n        blockComponentBackgroundColor:\n            node.attributes[blockComponentBackgroundColor],\n        blockComponentTextDirection:\n            node.attributes[blockComponentTextDirection],\n        blockComponentDelta: newDelta.toJson(),\n      },\n      children: [\n        ...node.children.map((e) => e.deepCopy()),\n        ...insertedNodes.map((e) => e.deepCopy()),\n      ],\n    );\n\n    final transaction = editorState.transaction;\n    transaction.insertNode(\n      node.path,\n      afterNode,\n    );\n    transaction.deleteNodes([\n      node,\n      ...insertedNodes,\n    ]);\n    if (afterSelection != null) {\n      transaction.afterSelection = afterSelection;\n    } else if (insertedNodes.isNotEmpty) {\n      // select the blocks\n      transaction.afterSelection = Selection(\n        start: Position(path: node.path.child(0)),\n        end: Position(path: node.path.child(insertedNodes.length - 1)),\n      );\n    } else {\n      transaction.afterSelection = transaction.beforeSelection;\n    }\n    await editorState.apply(transaction);\n\n    return true;\n  }\n\n  static Future<bool> turnIntoPage({\n    required String type,\n    required List<Node> selectedNodes,\n    required Selection selection,\n    required String currentViewId,\n    required EditorState editorState,\n  }) async {\n    if (type != SubPageBlockKeys.type || selectedNodes.isEmpty) {\n      return false;\n    }\n\n    if (selectedNodes.length == 1 &&\n        selectedNodes.first.type == SubPageBlockKeys.type) {\n      return true;\n    }\n\n    Log.info('Turn into page');\n\n    final insertedNodes = selectedNodes.map((n) => n.deepCopy()).toList();\n    final document = Document.blank()..insert([0], insertedNodes);\n    final name = await _extractNameFromNodes(selectedNodes);\n\n    final viewResult = await ViewBackendService.createView(\n      layoutType: ViewLayoutPB.Document,\n      name: name,\n      parentViewId: currentViewId,\n      initialDataBytes:\n          DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer(),\n    );\n\n    await viewResult.fold(\n      (view) async {\n        final node = subPageNode(viewId: view.id);\n        final transaction = editorState.transaction;\n        transaction\n          ..insertNode(selection.normalized.start.path.next, node)\n          ..deleteNodes(selectedNodes)\n          ..afterSelection = Selection.collapsed(selection.normalized.start);\n        editorState.selectionType = SelectionType.inline;\n\n        await editorState.apply(transaction);\n\n        // We move views after applying transaction to avoid performing side-effects on the views\n        final viewIdsToMove = _extractChildViewIds(selectedNodes);\n        for (final viewId in viewIdsToMove) {\n          // Attempt to put back from trash if necessary\n          await TrashService.putback(viewId);\n\n          await ViewBackendService.moveViewV2(\n            viewId: viewId,\n            newParentId: view.id,\n            prevViewId: null,\n          );\n        }\n      },\n      (err) async => Log.error(err),\n    );\n\n    return true;\n  }\n\n  static Future<String> _extractNameFromNodes(List<Node>? nodes) async {\n    if (nodes == null || nodes.isEmpty) {\n      return '';\n    }\n\n    String name = '';\n    for (final node in nodes) {\n      if (name.length > 30) {\n        return name.substring(0, name.length > 30 ? 30 : name.length);\n      }\n\n      if (node.delta != null) {\n        // \"ABC [Hello world]\" -> ABC Hello world\n        final textInserts = node.delta!.whereType<TextInsert>();\n        for (final ti in textInserts) {\n          if (ti.attributes?[MentionBlockKeys.mention] != null) {\n            // fetch the view name\n            final pageId = ti.attributes![MentionBlockKeys.mention]\n                [MentionBlockKeys.pageId];\n            final viewOrFailure = await ViewBackendService.getView(pageId);\n\n            final view = viewOrFailure.toNullable();\n            if (view == null) {\n              Log.error('Failed to fetch view with id: $pageId');\n              continue;\n            }\n\n            name += view.name;\n          } else {\n            name += ti.data!.toString();\n          }\n        }\n\n        if (name.isNotEmpty) {\n          break;\n        }\n      }\n\n      if (node.children.isNotEmpty) {\n        final n = await _extractNameFromNodes(node.children);\n        if (n.isNotEmpty) {\n          name = n;\n          break;\n        }\n      }\n    }\n\n    return name.substring(0, name.length > 30 ? 30 : name.length);\n  }\n\n  static List<String> _extractChildViewIds(List<Node> nodes) {\n    final List<String> viewIds = [];\n    for (final node in nodes) {\n      if (node.type == SubPageBlockKeys.type) {\n        final viewId = node.attributes[SubPageBlockKeys.viewId];\n        viewIds.add(viewId);\n      }\n\n      if (node.children.isNotEmpty) {\n        viewIds.addAll(_extractChildViewIds(node.children));\n      }\n\n      if (node.delta == null || node.delta!.isEmpty) {\n        continue;\n      }\n\n      final textInserts = node.delta!.whereType<TextInsert>();\n      for (final ti in textInserts) {\n        final Map<String, dynamic>? mention =\n            ti.attributes?[MentionBlockKeys.mention];\n        if (mention != null &&\n            mention[MentionBlockKeys.type] == MentionType.childPage.name) {\n          final String? viewId = mention[MentionBlockKeys.pageId];\n          if (viewId != null) {\n            viewIds.add(viewId);\n          }\n        }\n      }\n    }\n\n    return viewIds;\n  }\n\n  Selection? calculateTurnIntoSelection(\n    Node selectedNode,\n    Selection? beforeSelection,\n  ) {\n    final path = selectedNode.path;\n    final selection = Selection.collapsed(Position(path: path));\n\n    // if the previous selection is null or the start path is not in the same level as the current block path,\n    // then update the selection with the current block path\n    // for example,'|' means the selection,\n    // case 1: collapsed selection\n    // - bulleted item 1\n    // - bulleted |item 2\n    // when clicking the bulleted item 1, the bulleted item 1 path should be selected\n    // case 2: not collapsed selection\n    // - bulleted item 1\n    // - bulleted |item 2\n    // - bulleted |item 3\n    // when clicking the bulleted item 1, the bulleted item 1 path should be selected\n    if (beforeSelection == null ||\n        beforeSelection.start.path.length != path.length ||\n        !path.inSelection(beforeSelection)) {\n      return selection;\n    }\n    // if the beforeSelection start with the current block,\n    //  then updating the selection with the beforeSelection that may contains multiple blocks\n    return beforeSelection;\n  }\n\n  Future<void> _setToPageWidth(Node node) async {\n    if (node.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    await editorState.setColumnWidthToPageWidth(tableNode: node);\n  }\n\n  Future<void> _distributeColumnsEvenly(Node node) async {\n    if (node.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    await editorState.distributeColumnWidthToPageWidth(tableNode: node);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_notification.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\n\nimport 'draggable_option_button_feedback.dart';\nimport 'option_button.dart';\n\n// this flag is used to disable the tooltip of the block when it is dragged\nValueNotifier<bool> isDraggingAppFlowyEditorBlock = ValueNotifier(false);\n\nclass DraggableOptionButton extends StatefulWidget {\n  const DraggableOptionButton({\n    super.key,\n    required this.controller,\n    required this.editorState,\n    required this.blockComponentContext,\n    required this.blockComponentBuilder,\n  });\n\n  final PopoverController controller;\n  final EditorState editorState;\n  final BlockComponentContext blockComponentContext;\n  final Map<String, BlockComponentBuilder> blockComponentBuilder;\n\n  @override\n  State<DraggableOptionButton> createState() => _DraggableOptionButtonState();\n}\n\nclass _DraggableOptionButtonState extends State<DraggableOptionButton> {\n  late Node node;\n  late BlockComponentContext blockComponentContext;\n\n  Offset? globalPosition;\n\n  @override\n  void initState() {\n    super.initState();\n\n    // copy the node to avoid the node in document being updated\n    node = widget.blockComponentContext.node.deepCopy();\n  }\n\n  @override\n  void dispose() {\n    node.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Draggable<Node>(\n      data: node,\n      onDragStarted: _onDragStart,\n      onDragUpdate: _onDragUpdate,\n      onDragEnd: _onDragEnd,\n      feedback: DraggleOptionButtonFeedback(\n        controller: widget.controller,\n        editorState: widget.editorState,\n        blockComponentContext: widget.blockComponentContext,\n        blockComponentBuilder: widget.blockComponentBuilder,\n      ),\n      child: OptionButton(\n        isDragging: isDraggingAppFlowyEditorBlock,\n        controller: widget.controller,\n        editorState: widget.editorState,\n        blockComponentContext: widget.blockComponentContext,\n      ),\n    );\n  }\n\n  void _onDragStart() {\n    EditorNotification.dragStart().post();\n    isDraggingAppFlowyEditorBlock.value = true;\n    widget.editorState.selectionService.removeDropTarget();\n  }\n\n  void _onDragUpdate(DragUpdateDetails details) {\n    isDraggingAppFlowyEditorBlock.value = true;\n\n    final offset = details.globalPosition;\n\n    widget.editorState.selectionService.renderDropTargetForOffset(\n      offset,\n      interceptor: (context, targetNode) {\n        // if the cursor node is in a columns block or a column block,\n        //  we will return the node's parent instead to support dragging a node to the inside of a columns block or a column block.\n        final parentColumnNode = targetNode.columnParent;\n        if (parentColumnNode != null) {\n          final position = getDragAreaPosition(\n            context,\n            targetNode,\n            offset,\n          );\n\n          if (position != null && position.$2 == HorizontalPosition.right) {\n            return parentColumnNode;\n          }\n\n          if (position != null &&\n              position.$2 == HorizontalPosition.left &&\n              position.$1 == VerticalPosition.middle) {\n            return parentColumnNode;\n          }\n        }\n\n        // return simple table block if the target node is in a simple table block\n        final parentSimpleTableNode = targetNode.parentTableNode;\n        if (parentSimpleTableNode != null) {\n          return parentSimpleTableNode;\n        }\n\n        return targetNode;\n      },\n      builder: (context, data) {\n        return VisualDragArea(\n          editorState: widget.editorState,\n          data: data,\n          dragNode: widget.blockComponentContext.node,\n        );\n      },\n    );\n\n    globalPosition = details.globalPosition;\n\n    // auto scroll the page when the drag position is at the edge of the screen\n    widget.editorState.scrollService?.startAutoScroll(\n      details.localPosition,\n    );\n  }\n\n  void _onDragEnd(DraggableDetails details) {\n    isDraggingAppFlowyEditorBlock.value = false;\n\n    widget.editorState.selectionService.removeDropTarget();\n\n    if (globalPosition == null) {\n      return;\n    }\n\n    final data = widget.editorState.selectionService.getDropTargetRenderData(\n      globalPosition!,\n      interceptor: (context, targetNode) {\n        // if the cursor node is in a columns block or a column block,\n        //  we will return the node's parent instead to support dragging a node to the inside of a columns block or a column block.\n        final parentColumnNode = targetNode.columnParent;\n        if (parentColumnNode != null) {\n          final position = getDragAreaPosition(\n            context,\n            targetNode,\n            globalPosition!,\n          );\n\n          if (position != null && position.$2 == HorizontalPosition.right) {\n            return parentColumnNode;\n          }\n\n          if (position != null &&\n              position.$2 == HorizontalPosition.left &&\n              position.$1 == VerticalPosition.middle) {\n            return parentColumnNode;\n          }\n        }\n\n        // return simple table block if the target node is in a simple table block\n        final parentSimpleTableNode = targetNode.parentTableNode;\n        if (parentSimpleTableNode != null) {\n          return parentSimpleTableNode;\n        }\n\n        return targetNode;\n      },\n    );\n\n    dragToMoveNode(\n      context,\n      node: widget.blockComponentContext.node,\n      acceptedPath: data?.cursorNode?.path,\n      dragOffset: globalPosition!,\n    ).then((_) {\n      EditorNotification.dragEnd().post();\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button_feedback.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass DraggleOptionButtonFeedback extends StatefulWidget {\n  const DraggleOptionButtonFeedback({\n    super.key,\n    required this.controller,\n    required this.editorState,\n    required this.blockComponentContext,\n    required this.blockComponentBuilder,\n  });\n\n  final PopoverController controller;\n  final EditorState editorState;\n  final BlockComponentContext blockComponentContext;\n  final Map<String, BlockComponentBuilder> blockComponentBuilder;\n\n  @override\n  State<DraggleOptionButtonFeedback> createState() =>\n      _DraggleOptionButtonFeedbackState();\n}\n\nclass _DraggleOptionButtonFeedbackState\n    extends State<DraggleOptionButtonFeedback> {\n  late Node node;\n  late BlockComponentContext blockComponentContext;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _setupLockComponentContext();\n    widget.blockComponentContext.node.addListener(_updateBlockComponentContext);\n  }\n\n  @override\n  void dispose() {\n    widget.blockComponentContext.node\n        .removeListener(_updateBlockComponentContext);\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final maxWidth = (widget.editorState.renderBox?.size.width ??\n            MediaQuery.of(context).size.width) *\n        0.8;\n\n    return Opacity(\n      opacity: 0.7,\n      child: Material(\n        color: Colors.transparent,\n        child: Container(\n          constraints: BoxConstraints(\n            maxWidth: maxWidth,\n          ),\n          child: IntrinsicHeight(\n            child: Provider.value(\n              value: widget.editorState,\n              child: _buildBlock(),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildBlock() {\n    final node = widget.blockComponentContext.node;\n    final builder = widget.blockComponentBuilder[node.type];\n    if (builder == null) {\n      return const SizedBox.shrink();\n    }\n\n    const unsupportedRenderBlockTypes = [\n      TableBlockKeys.type,\n      CustomImageBlockKeys.type,\n      MultiImageBlockKeys.type,\n      FileBlockKeys.type,\n      DatabaseBlockKeys.boardType,\n      DatabaseBlockKeys.calendarType,\n      DatabaseBlockKeys.gridType,\n    ];\n\n    if (unsupportedRenderBlockTypes.contains(node.type)) {\n      // unable to render table block without provider/context\n      // render a placeholder instead\n      return Container(\n        padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),\n        decoration: BoxDecoration(\n          color: Theme.of(context).cardColor,\n          borderRadius: BorderRadius.circular(8),\n        ),\n        child: FlowyText(node.type.replaceAll('_', ' ').capitalize()),\n      );\n    }\n\n    return IntrinsicHeight(\n      child: MultiProvider(\n        providers: [\n          Provider.value(value: widget.editorState),\n          Provider.value(value: getIt<ReminderBloc>()),\n        ],\n        child: builder.build(blockComponentContext),\n      ),\n    );\n  }\n\n  void _updateBlockComponentContext() {\n    setState(() => _setupLockComponentContext());\n  }\n\n  void _setupLockComponentContext() {\n    node = widget.blockComponentContext.node.deepCopy();\n    blockComponentContext = BlockComponentContext(\n      widget.blockComponentContext.buildContext,\n      node,\n    );\n  }\n}\n\nclass _OptionButton extends StatefulWidget {\n  const _OptionButton({\n    required this.controller,\n    required this.editorState,\n    required this.blockComponentContext,\n    required this.isDragging,\n  });\n\n  final PopoverController controller;\n  final EditorState editorState;\n  final BlockComponentContext blockComponentContext;\n  final ValueNotifier<bool> isDragging;\n\n  @override\n  State<_OptionButton> createState() => _OptionButtonState();\n}\n\nconst _interceptorKey = 'document_option_button_interceptor';\n\nclass _OptionButtonState extends State<_OptionButton> {\n  late final gestureInterceptor = SelectionGestureInterceptor(\n    key: _interceptorKey,\n    canTap: (details) => !_isTapInBounds(details.globalPosition),\n  );\n\n  // the selection will be cleared when tap the option button\n  // so we need to restore the selection after tap the option button\n  Selection? beforeSelection;\n  RenderBox? get renderBox => context.findRenderObject() as RenderBox?;\n\n  @override\n  void initState() {\n    super.initState();\n\n    widget.editorState.service.selectionService.registerGestureInterceptor(\n      gestureInterceptor,\n    );\n  }\n\n  @override\n  void dispose() {\n    widget.editorState.service.selectionService.unregisterGestureInterceptor(\n      _interceptorKey,\n    );\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ValueListenableBuilder(\n      valueListenable: widget.isDragging,\n      builder: (context, isDragging, child) {\n        return BlockActionButton(\n          svg: FlowySvgs.drag_element_s,\n          showTooltip: !isDragging,\n          richMessage: TextSpan(\n            children: [\n              TextSpan(\n                text: LocaleKeys.document_plugins_optionAction_drag.tr(),\n                style: context.tooltipTextStyle(),\n              ),\n              TextSpan(\n                text: LocaleKeys.document_plugins_optionAction_toMove.tr(),\n                style: context.tooltipTextStyle(),\n              ),\n              const TextSpan(text: '\\n'),\n              TextSpan(\n                text: LocaleKeys.document_plugins_optionAction_click.tr(),\n                style: context.tooltipTextStyle(),\n              ),\n              TextSpan(\n                text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(),\n                style: context.tooltipTextStyle(),\n              ),\n            ],\n          ),\n          onPointerDown: () {\n            if (widget.editorState.selection != null) {\n              beforeSelection = widget.editorState.selection;\n            }\n          },\n          onTap: () {\n            if (widget.editorState.selection != null) {\n              beforeSelection = widget.editorState.selection;\n            }\n\n            widget.controller.show();\n\n            // update selection\n            _updateBlockSelection();\n          },\n        );\n      },\n    );\n  }\n\n  void _updateBlockSelection() {\n    if (beforeSelection == null) {\n      final path = widget.blockComponentContext.node.path;\n      final selection = Selection.collapsed(\n        Position(path: path),\n      );\n      widget.editorState.updateSelectionWithReason(\n        selection,\n        customSelectionType: SelectionType.block,\n      );\n    } else {\n      widget.editorState.updateSelectionWithReason(\n        beforeSelection!,\n        customSelectionType: SelectionType.block,\n      );\n    }\n  }\n\n  bool _isTapInBounds(Offset offset) {\n    if (renderBox == null) {\n      return false;\n    }\n\n    final localPosition = renderBox!.globalToLocal(offset);\n    final result = renderBox!.paintBounds.contains(localPosition);\n    if (result) {\n      beforeSelection = widget.editorState.selection;\n    } else {\n      beforeSelection = null;\n    }\n\n    return result;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/option_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nconst _interceptorKey = 'document_option_button_interceptor';\n\nclass OptionButton extends StatefulWidget {\n  const OptionButton({\n    super.key,\n    required this.controller,\n    required this.editorState,\n    required this.blockComponentContext,\n    required this.isDragging,\n  });\n\n  final PopoverController controller;\n  final EditorState editorState;\n  final BlockComponentContext blockComponentContext;\n  final ValueNotifier<bool> isDragging;\n\n  @override\n  State<OptionButton> createState() => _OptionButtonState();\n}\n\nclass _OptionButtonState extends State<OptionButton> {\n  late final registerKey =\n      _interceptorKey + widget.blockComponentContext.node.id;\n  late final gestureInterceptor = SelectionGestureInterceptor(\n    key: registerKey,\n    canTap: (details) => !_isTapInBounds(details.globalPosition),\n  );\n\n  // the selection will be cleared when tap the option button\n  // so we need to restore the selection after tap the option button\n  Selection? beforeSelection;\n  RenderBox? get renderBox => context.findRenderObject() as RenderBox?;\n\n  @override\n  void initState() {\n    super.initState();\n\n    widget.editorState.service.selectionService.registerGestureInterceptor(\n      gestureInterceptor,\n    );\n  }\n\n  @override\n  void dispose() {\n    widget.editorState.service.selectionService.unregisterGestureInterceptor(\n      registerKey,\n    );\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ValueListenableBuilder(\n      valueListenable: widget.isDragging,\n      builder: (context, isDragging, child) {\n        return BlockActionButton(\n          svg: FlowySvgs.drag_element_s,\n          showTooltip: !isDragging,\n          richMessage: TextSpan(\n            children: [\n              TextSpan(\n                text: LocaleKeys.document_plugins_optionAction_drag.tr(),\n                style: context.tooltipTextStyle(),\n              ),\n              TextSpan(\n                text: LocaleKeys.document_plugins_optionAction_toMove.tr(),\n                style: context.tooltipTextStyle(),\n              ),\n              const TextSpan(text: '\\n'),\n              TextSpan(\n                text: LocaleKeys.document_plugins_optionAction_click.tr(),\n                style: context.tooltipTextStyle(),\n              ),\n              TextSpan(\n                text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(),\n                style: context.tooltipTextStyle(),\n              ),\n            ],\n          ),\n          onTap: () {\n            final selection = widget.editorState.selection;\n            if (selection != null) {\n              beforeSelection = selection.normalized;\n            }\n\n            widget.controller.show();\n\n            // update selection\n            _updateBlockSelection(context);\n          },\n        );\n      },\n    );\n  }\n\n  void _updateBlockSelection(BuildContext context) {\n    final cubit = context.read<BlockActionOptionCubit>();\n    final selection = cubit.calculateTurnIntoSelection(\n      widget.blockComponentContext.node,\n      beforeSelection,\n    );\n\n    widget.editorState.updateSelectionWithReason(\n      selection,\n      customSelectionType: SelectionType.block,\n    );\n  }\n\n  bool _isTapInBounds(Offset offset) {\n    final renderBox = this.renderBox;\n    if (renderBox == null) {\n      return false;\n    }\n\n    final localPosition = renderBox.globalToLocal(offset);\n    final result = renderBox.paintBounds.contains(localPosition);\n    if (result) {\n      beforeSelection = widget.editorState.selection?.normalized;\n    } else {\n      beforeSelection = null;\n    }\n\n    return result;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart'\n    hide QuoteBlockKeys, quoteNode;\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nenum HorizontalPosition { left, center, right }\n\nenum VerticalPosition { top, middle, bottom }\n\nList<String> nodeTypesThatCanContainChildNode = [\n  ParagraphBlockKeys.type,\n  BulletedListBlockKeys.type,\n  NumberedListBlockKeys.type,\n  QuoteBlockKeys.type,\n  TodoListBlockKeys.type,\n  ToggleListBlockKeys.type,\n];\n\nFuture<void> dragToMoveNode(\n  BuildContext context, {\n  required Node node,\n  required Offset dragOffset,\n  Path? acceptedPath,\n}) async {\n  if (acceptedPath == null) {\n    Log.info('acceptedPath is null');\n    return;\n  }\n\n  final editorState = context.read<EditorState>();\n  final targetNode = editorState.getNodeAtPath(acceptedPath);\n  if (targetNode == null) {\n    Log.info('targetNode is null');\n    return;\n  }\n\n  if (shouldIgnoreDragTarget(\n    editorState: editorState,\n    dragNode: node,\n    targetPath: acceptedPath,\n  )) {\n    Log.info('Drop ignored: node($node, ${node.path}), path($acceptedPath)');\n    return;\n  }\n\n  final position = getDragAreaPosition(context, targetNode, dragOffset);\n  if (position == null) {\n    Log.info('position is null');\n    return;\n  }\n\n  final (verticalPosition, horizontalPosition, _) = position;\n  Path newPath = targetNode.path;\n\n  // if the horizontal position is right, creating a column block to contain the target node and the drag node\n  if (horizontalPosition == HorizontalPosition.right) {\n    // 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\n    // 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\n    final transaction = editorState.transaction;\n    final targetNodeParent = targetNode.columnsParent;\n\n    if (targetNodeParent != null) {\n      final length = targetNodeParent.children.length;\n      final ratios = targetNodeParent.children\n          .map(\n            (e) =>\n                e.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ??\n                1.0 / length,\n          )\n          .map((e) => e * length / (length + 1))\n          .toList();\n\n      final columnNode = simpleColumnNode(\n        children: [node.deepCopy()],\n        ratio: 1.0 / (length + 1),\n      );\n      for (final (index, column) in targetNodeParent.children.indexed) {\n        transaction.updateNode(column, {\n          ...column.attributes,\n          SimpleColumnBlockKeys.ratio: ratios[index],\n        });\n      }\n\n      transaction.insertNode(targetNode.path.next, columnNode);\n      transaction.deleteNode(node);\n    } else {\n      final columnsNode = simpleColumnsNode(\n        children: [\n          simpleColumnNode(children: [targetNode.deepCopy()], ratio: 0.5),\n          simpleColumnNode(children: [node.deepCopy()], ratio: 0.5),\n        ],\n      );\n\n      transaction.insertNode(newPath, columnsNode);\n      transaction.deleteNode(targetNode);\n      transaction.deleteNode(node);\n    }\n\n    if (transaction.operations.isNotEmpty) {\n      await editorState.apply(transaction);\n    }\n    return;\n  } else if (horizontalPosition == HorizontalPosition.left &&\n      verticalPosition == VerticalPosition.middle) {\n    // 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\n    // 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\n    final transaction = editorState.transaction;\n    final targetNodeParent = targetNode.columnsParent;\n    if (targetNodeParent != null) {\n      // find the previous sibling node of the target node\n      final length = targetNodeParent.children.length;\n      final ratios = targetNodeParent.children\n          .map(\n            (e) =>\n                e.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ??\n                1.0 / length,\n          )\n          .map((e) => e * length / (length + 1))\n          .toList();\n      final columnNode = simpleColumnNode(\n        children: [node.deepCopy()],\n        ratio: 1.0 / (length + 1),\n      );\n\n      for (final (index, column) in targetNodeParent.children.indexed) {\n        transaction.updateNode(column, {\n          ...column.attributes,\n          SimpleColumnBlockKeys.ratio: ratios[index],\n        });\n      }\n\n      transaction.insertNode(targetNode.path.previous, columnNode);\n      transaction.deleteNode(node);\n    } else {\n      final columnsNode = simpleColumnsNode(\n        children: [\n          simpleColumnNode(children: [node.deepCopy()], ratio: 0.5),\n          simpleColumnNode(children: [targetNode.deepCopy()], ratio: 0.5),\n        ],\n      );\n\n      transaction.insertNode(newPath, columnsNode);\n      transaction.deleteNode(targetNode);\n      transaction.deleteNode(node);\n    }\n\n    if (transaction.operations.isNotEmpty) {\n      await editorState.apply(transaction);\n    }\n    return;\n  }\n  // Determine the new path based on drop position\n  // For VerticalPosition.top, we keep the target node's path\n  if (verticalPosition == VerticalPosition.bottom) {\n    if (horizontalPosition == HorizontalPosition.left) {\n      newPath = newPath.next;\n    } else if (horizontalPosition == HorizontalPosition.center &&\n        nodeTypesThatCanContainChildNode.contains(targetNode.type)) {\n      // check if the target node can contain a child node\n      newPath = newPath.child(0);\n    }\n  }\n\n  // Check if the drop should be ignored\n  if (shouldIgnoreDragTarget(\n    editorState: editorState,\n    dragNode: node,\n    targetPath: newPath,\n  )) {\n    Log.info(\n      'Drop ignored: node($node, ${node.path}), path($acceptedPath)',\n    );\n    return;\n  }\n\n  Log.info('Moving node($node, ${node.path}) to path($newPath)');\n\n  final transaction = editorState.transaction;\n  transaction.insertNode(newPath, node.deepCopy());\n  transaction.deleteNode(node);\n  await editorState.apply(transaction);\n}\n\n(VerticalPosition, HorizontalPosition, Rect)? getDragAreaPosition(\n  BuildContext context,\n  Node dragTargetNode,\n  Offset dragOffset,\n) {\n  final selectable = dragTargetNode.selectable;\n  final renderBox = selectable?.context.findRenderObject() as RenderBox?;\n  if (selectable == null || renderBox == null) {\n    return null;\n  }\n\n  // disable the table cell block\n  if (dragTargetNode.parent?.type == TableCellBlockKeys.type) {\n    return null;\n  }\n\n  final globalBlockOffset = renderBox.localToGlobal(Offset.zero);\n  final globalBlockRect = globalBlockOffset & renderBox.size;\n\n  // Check if the dragOffset is within the globalBlockRect\n  final isInside = globalBlockRect.contains(dragOffset);\n\n  if (!isInside) {\n    Log.info(\n      'the drag offset is not inside the block, dragOffset($dragOffset), globalBlockRect($globalBlockRect)',\n    );\n    return null;\n  }\n\n  // Determine the relative position\n  HorizontalPosition horizontalPosition = HorizontalPosition.left;\n  VerticalPosition verticalPosition;\n\n  // | ----------------------------- block ----------------------------- |\n  // | 1. -- 88px --| 2. ---------------------------- | 3. ---- 1/5 ---- |\n  // 1. drag the node under the block as a sibling node\n  // 2. drag the node inside the block as a child node\n  // 3. create a column block to contain the node and the drag node\n\n  // Horizontal position, please refer to the diagram above\n  // 88px is a hardcoded value, it can be changed based on the project's design\n  if (dragOffset.dx < globalBlockRect.left + 88) {\n    horizontalPosition = HorizontalPosition.left;\n  } else if (dragOffset.dx > globalBlockRect.right * 4.0 / 5.0) {\n    horizontalPosition = HorizontalPosition.right;\n  } else if (nodeTypesThatCanContainChildNode.contains(dragTargetNode.type)) {\n    horizontalPosition = HorizontalPosition.center;\n  }\n\n  // | ----------------------------------------------------------------- | <- if the drag position is in this area, the vertical position is top\n  // | ----------------------------- block ----------------------------- | <- if the drag position is in this area, the vertical position is middle\n  // | ----------------------------------------------------------------- | <- if the drag position is in this area, the vertical position is bottom\n\n  // Vertical position\n  final heightThird = globalBlockRect.height / 3;\n  if (dragOffset.dy < globalBlockRect.top + heightThird) {\n    verticalPosition = VerticalPosition.top;\n  } else if (dragOffset.dy < globalBlockRect.top + heightThird * 2) {\n    verticalPosition = VerticalPosition.middle;\n  } else {\n    verticalPosition = VerticalPosition.bottom;\n  }\n\n  return (verticalPosition, horizontalPosition, globalBlockRect);\n}\n\nbool shouldIgnoreDragTarget({\n  required EditorState editorState,\n  required Node dragNode,\n  required Path? targetPath,\n}) {\n  if (targetPath == null) {\n    return true;\n  }\n\n  if (dragNode.path.equals(targetPath)) {\n    return true;\n  }\n\n  if (dragNode.path.isAncestorOf(targetPath)) {\n    return true;\n  }\n\n  final targetNode = editorState.getNodeAtPath(targetPath);\n  if (targetNode != null &&\n      targetNode.isInTable &&\n      targetNode.type != SimpleTableBlockKeys.type) {\n    return true;\n  }\n\n  return false;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart",
    "content": "import 'dart:math';\n\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nimport 'util.dart';\n\nclass VisualDragArea extends StatelessWidget {\n  const VisualDragArea({\n    super.key,\n    required this.data,\n    required this.dragNode,\n    required this.editorState,\n  });\n\n  final DragAreaBuilderData data;\n  final Node dragNode;\n  final EditorState editorState;\n\n  @override\n  Widget build(BuildContext context) {\n    final targetNode = data.targetNode;\n\n    final ignore = shouldIgnoreDragTarget(\n      editorState: editorState,\n      dragNode: dragNode,\n      targetPath: targetNode.path,\n    );\n    if (ignore) {\n      return const SizedBox.shrink();\n    }\n\n    final selectable = targetNode.selectable;\n    final renderBox = selectable?.context.findRenderObject() as RenderBox?;\n    if (selectable == null || renderBox == null) {\n      return const SizedBox.shrink();\n    }\n\n    final position = getDragAreaPosition(\n      context,\n      targetNode,\n      data.dragOffset,\n    );\n\n    if (position == null) {\n      return const SizedBox.shrink();\n    }\n\n    final (verticalPosition, horizontalPosition, globalBlockRect) = position;\n\n    // 44 is the width of the drag indicator\n    const indicatorWidth = 44.0;\n    final width = globalBlockRect.width - indicatorWidth;\n\n    Widget child = Container(\n      height: 2,\n      width: max(width, 0.0),\n      color: Theme.of(context).colorScheme.primary,\n    );\n\n    // if the horizontal position is right, we need to show the indicator on the right side of the target node\n    //  which represent moving the target node and drag node inside the column block.\n    if (horizontalPosition == HorizontalPosition.left &&\n        verticalPosition == VerticalPosition.middle) {\n      return Positioned(\n        top: globalBlockRect.top,\n        height: globalBlockRect.height,\n        left: globalBlockRect.left + indicatorWidth,\n        child: Container(\n          width: 2,\n          color: Theme.of(context).colorScheme.primary,\n        ),\n      );\n    }\n\n    if (horizontalPosition == HorizontalPosition.right) {\n      return Positioned(\n        top: globalBlockRect.top,\n        height: globalBlockRect.height,\n        left: globalBlockRect.right - 2,\n        child: Container(\n          width: 2,\n          color: Theme.of(context).colorScheme.primary,\n        ),\n      );\n    }\n\n    // If the horizontal position is center, we need to show two indicators\n    //which represent moving the block as the child of the target node.\n    if (horizontalPosition == HorizontalPosition.center) {\n      const breakWidth = 22.0;\n      const padding = 8.0;\n      child = Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Container(\n            height: 2,\n            width: breakWidth,\n            color: Theme.of(context).colorScheme.primary,\n          ),\n          const SizedBox(width: padding),\n          Container(\n            height: 2,\n            width: width - breakWidth - padding,\n            color: Theme.of(context).colorScheme.primary,\n          ),\n        ],\n      );\n    }\n\n    return Positioned(\n      top: verticalPosition == VerticalPosition.top\n          ? globalBlockRect.top\n          : globalBlockRect.bottom,\n      // 44 is the width of the drag indicator\n      left: globalBlockRect.left + 44,\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\n/// The ... button shows on the top right corner of a block.\n///\n/// Default actions are:\n/// - delete\n/// - duplicate\n/// - insert above\n/// - insert below\n///\n/// Only works on mobile.\nclass MobileBlockActionButtons extends StatelessWidget {\n  const MobileBlockActionButtons({\n    super.key,\n    this.extendActionWidgets = const [],\n    this.showThreeDots = true,\n    required this.node,\n    required this.editorState,\n    required this.child,\n  });\n\n  final Node node;\n  final EditorState editorState;\n  final List<Widget> extendActionWidgets;\n  final Widget child;\n  final bool showThreeDots;\n\n  @override\n  Widget build(BuildContext context) {\n    if (!UniversalPlatform.isMobile) {\n      return child;\n    }\n\n    if (!showThreeDots) {\n      return GestureDetector(\n        behavior: HitTestBehavior.opaque,\n        onTap: () => _showBottomSheet(context),\n        child: child,\n      );\n    }\n\n    const padding = 10.0;\n    return Stack(\n      children: [\n        child,\n        Positioned(\n          top: padding,\n          right: padding,\n          child: FlowyIconButton(\n            icon: const FlowySvg(\n              FlowySvgs.three_dots_s,\n            ),\n            width: 20.0,\n            onPressed: () => _showBottomSheet(context),\n          ),\n        ),\n      ],\n    );\n  }\n\n  void _showBottomSheet(BuildContext context) {\n    // close the keyboard\n    editorState.updateSelectionWithReason(null, extraInfo: {});\n\n    showMobileBottomSheet(\n      context,\n      showHeader: true,\n      showCloseButton: true,\n      showDragHandle: true,\n      title: LocaleKeys.document_plugins_action.tr(),\n      builder: (context) {\n        return BlockActionBottomSheet(\n          extendActionWidgets: extendActionWidgets,\n          onAction: (action) async {\n            context.pop();\n\n            final transaction = editorState.transaction;\n            switch (action) {\n              case BlockActionBottomSheetType.delete:\n                transaction.deleteNode(node);\n                break;\n              case BlockActionBottomSheetType.duplicate:\n                transaction.insertNode(\n                  node.path.next,\n                  node.deepCopy(),\n                );\n                break;\n              case BlockActionBottomSheetType.insertAbove:\n              case BlockActionBottomSheetType.insertBelow:\n                final path = action == BlockActionBottomSheetType.insertAbove\n                    ? node.path\n                    : node.path.next;\n                transaction\n                  ..insertNode(\n                    path,\n                    paragraphNode(),\n                  )\n                  ..afterSelection = Selection.collapsed(\n                    Position(\n                      path: path,\n                    ),\n                  );\n                break;\n            }\n\n            if (transaction.operations.isNotEmpty) {\n              await editorState.apply(transaction);\n            }\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/align_option_action.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nenum OptionAlignType {\n  left,\n  center,\n  right;\n\n  static OptionAlignType fromString(String? value) {\n    switch (value) {\n      case 'left':\n        return OptionAlignType.left;\n      case 'center':\n        return OptionAlignType.center;\n      case 'right':\n        return OptionAlignType.right;\n      default:\n        return OptionAlignType.center;\n    }\n  }\n\n  FlowySvgData get svg {\n    switch (this) {\n      case OptionAlignType.left:\n        return FlowySvgs.table_align_left_s;\n      case OptionAlignType.center:\n        return FlowySvgs.table_align_center_s;\n      case OptionAlignType.right:\n        return FlowySvgs.table_align_right_s;\n    }\n  }\n\n  String get description {\n    switch (this) {\n      case OptionAlignType.left:\n        return LocaleKeys.document_plugins_optionAction_left.tr();\n      case OptionAlignType.center:\n        return LocaleKeys.document_plugins_optionAction_center.tr();\n      case OptionAlignType.right:\n        return LocaleKeys.document_plugins_optionAction_right.tr();\n    }\n  }\n}\n\nclass AlignOptionAction extends PopoverActionCell {\n  AlignOptionAction({\n    required this.editorState,\n  });\n\n  final EditorState editorState;\n\n  @override\n  Widget? leftIcon(Color iconColor) {\n    return FlowySvg(\n      align.svg,\n      size: const Size.square(18),\n    );\n  }\n\n  @override\n  String get name {\n    return LocaleKeys.document_plugins_optionAction_align.tr();\n  }\n\n  @override\n  PopoverActionCellBuilder get builder =>\n      (context, parentController, controller) {\n        final selection = editorState.selection?.normalized;\n        if (selection == null) {\n          return const SizedBox.shrink();\n        }\n        final node = editorState.getNodeAtPath(selection.start.path);\n        if (node == null) {\n          return const SizedBox.shrink();\n        }\n        final children = buildAlignOptions(context, (align) async {\n          await onAlignChanged(align);\n          controller.close();\n          parentController.close();\n        });\n        return IntrinsicHeight(\n          child: IntrinsicWidth(\n            child: Column(\n              children: children,\n            ),\n          ),\n        );\n      };\n\n  List<Widget> buildAlignOptions(\n    BuildContext context,\n    void Function(OptionAlignType) onTap,\n  ) {\n    return OptionAlignType.values.map((e) => OptionAlignWrapper(e)).map((e) {\n      final leftIcon = e.leftIcon(Theme.of(context).colorScheme.onSurface);\n      final rightIcon = e.rightIcon(Theme.of(context).colorScheme.onSurface);\n      return HoverButton(\n        onTap: () => onTap(e.inner),\n        itemHeight: ActionListSizes.itemHeight,\n        leftIcon: SizedBox(\n          width: 16,\n          height: 16,\n          child: leftIcon,\n        ),\n        name: e.name,\n        rightIcon: rightIcon,\n      );\n    }).toList();\n  }\n\n  OptionAlignType get align {\n    final selection = editorState.selection;\n    if (selection == null) {\n      return OptionAlignType.center;\n    }\n    final node = editorState.getNodeAtPath(selection.start.path);\n    final align = node?.type == SimpleTableBlockKeys.type\n        ? node?.tableAlign.key\n        : node?.attributes[blockComponentAlign];\n    return OptionAlignType.fromString(align);\n  }\n\n  Future<void> onAlignChanged(OptionAlignType align) async {\n    if (align == this.align) {\n      return;\n    }\n    final selection = editorState.selection;\n    if (selection == null) {\n      return;\n    }\n    final node = editorState.getNodeAtPath(selection.start.path);\n    if (node == null) {\n      return;\n    }\n    // the align attribute for simple table is not same as the align type,\n    // so we need to convert the align type to the align attribute\n    if (node.type == SimpleTableBlockKeys.type) {\n      await editorState.updateTableAlign(\n        tableNode: node,\n        align: TableAlign.fromString(align.name),\n      );\n    } else {\n      final transaction = editorState.transaction;\n      transaction.updateNode(node, {\n        blockComponentAlign: align.name,\n      });\n      await editorState.apply(transaction);\n    }\n  }\n}\n\nclass OptionAlignWrapper extends ActionCell {\n  OptionAlignWrapper(this.inner);\n\n  final OptionAlignType inner;\n\n  @override\n  Widget? leftIcon(Color iconColor) => FlowySvg(inner.svg);\n\n  @override\n  String get name => inner.description;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/color_option_action.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nconst optionActionColorDefaultColor = 'appflowy_theme_default_color';\n\nclass ColorOptionAction extends CustomActionCell {\n  ColorOptionAction({\n    required this.editorState,\n    required this.mutex,\n  });\n\n  final EditorState editorState;\n  final PopoverController innerController = PopoverController();\n  final PopoverMutex mutex;\n\n  @override\n  Widget buildWithContext(\n    BuildContext context,\n    PopoverController controller,\n    PopoverMutex? mutex,\n  ) {\n    return ColorOptionButton(\n      editorState: editorState,\n      mutex: this.mutex,\n      controller: controller,\n    );\n  }\n}\n\nclass ColorOptionButton extends StatefulWidget {\n  const ColorOptionButton({\n    super.key,\n    required this.editorState,\n    required this.mutex,\n    required this.controller,\n  });\n\n  final EditorState editorState;\n  final PopoverMutex mutex;\n  final PopoverController controller;\n\n  @override\n  State<ColorOptionButton> createState() => _ColorOptionButtonState();\n}\n\nclass _ColorOptionButtonState extends State<ColorOptionButton> {\n  final PopoverController innerController = PopoverController();\n  bool isOpen = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      asBarrier: true,\n      controller: innerController,\n      mutex: widget.mutex,\n      popupBuilder: (context) {\n        isOpen = true;\n        return _buildColorOptionMenu(\n          context,\n          widget.controller,\n        );\n      },\n      onClose: () => isOpen = false,\n      direction: PopoverDirection.rightWithCenterAligned,\n      animationDuration: Durations.short3,\n      beginScaleFactor: 1.0,\n      beginOpacity: 0.8,\n      child: HoverButton(\n        itemHeight: ActionListSizes.itemHeight,\n        leftIcon: const FlowySvg(\n          FlowySvgs.color_format_m,\n          size: Size.square(15),\n        ),\n        name: LocaleKeys.document_plugins_optionAction_color.tr(),\n        onTap: () {\n          if (!isOpen) {\n            innerController.show();\n          }\n        },\n      ),\n    );\n  }\n\n  Widget _buildColorOptionMenu(\n    BuildContext context,\n    PopoverController controller,\n  ) {\n    final selection = widget.editorState.selection?.normalized;\n    if (selection == null) {\n      return const SizedBox.shrink();\n    }\n\n    final node = widget.editorState.getNodeAtPath(selection.start.path);\n    if (node == null) {\n      return const SizedBox.shrink();\n    }\n\n    return _buildColorOptions(context, node, controller);\n  }\n\n  Widget _buildColorOptions(\n    BuildContext context,\n    Node node,\n    PopoverController controller,\n  ) {\n    final selection = widget.editorState.selection?.normalized;\n    if (selection == null) {\n      return const SizedBox.shrink();\n    }\n    final node = widget.editorState.getNodeAtPath(selection.start.path);\n    if (node == null) {\n      return const SizedBox.shrink();\n    }\n    final bgColor = node.attributes[blockComponentBackgroundColor] as String?;\n    final selectedColor = bgColor?.tryToColor();\n    // get default background color for callout block from themeExtension\n    final defaultColor = node.type == CalloutBlockKeys.type\n        ? AFThemeExtension.of(context).calloutBGColor\n        : Colors.transparent;\n    final colors = [\n      // reset to default background color\n      FlowyColorOption(\n        color: defaultColor,\n        i18n: LocaleKeys.document_plugins_optionAction_defaultColor.tr(),\n        id: optionActionColorDefaultColor,\n      ),\n      ...FlowyTint.values.map(\n        (e) => FlowyColorOption(\n          color: e.color(context),\n          i18n: e.tintName(AppFlowyEditorL10n.current),\n          id: e.id,\n        ),\n      ),\n    ];\n\n    return FlowyColorPicker(\n      colors: colors,\n      selected: selectedColor,\n      border: Border.all(\n        color: AFThemeExtension.of(context).onBackground,\n      ),\n      onTap: (option, index) async {\n        final editorState = widget.editorState;\n        final transaction = editorState.transaction;\n        final selectionType = editorState.selectionType;\n        final selection = editorState.selection;\n\n        // In multiple selection, we need to update all the nodes in the selection\n        if (selectionType == SelectionType.block && selection != null) {\n          final nodes = editorState.getNodesInSelection(selection.normalized);\n          for (final node in nodes) {\n            transaction.updateNode(node, {\n              blockComponentBackgroundColor: option.id,\n            });\n          }\n        } else {\n          transaction.updateNode(node, {\n            blockComponentBackgroundColor: option.id,\n          });\n        }\n\n        await widget.editorState.apply(transaction);\n\n        innerController.close();\n        controller.close();\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/depth_option_action.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nenum OptionDepthType {\n  h1(1, 'H1'),\n  h2(2, 'H2'),\n  h3(3, 'H3'),\n  h4(4, 'H4'),\n  h5(5, 'H5'),\n  h6(6, 'H6');\n\n  const OptionDepthType(this.level, this.description);\n\n  final String description;\n  final int level;\n\n  static OptionDepthType fromLevel(int? level) {\n    switch (level) {\n      case 1:\n        return OptionDepthType.h1;\n      case 2:\n        return OptionDepthType.h2;\n      case 3:\n      default:\n        return OptionDepthType.h3;\n    }\n  }\n}\n\nclass DepthOptionAction extends PopoverActionCell {\n  DepthOptionAction({\n    required this.editorState,\n  });\n\n  final EditorState editorState;\n\n  @override\n  Widget? leftIcon(Color iconColor) {\n    return FlowySvg(\n      OptionAction.depth.svg,\n      size: const Size.square(16),\n    );\n  }\n\n  @override\n  String get name => LocaleKeys.document_plugins_optionAction_depth.tr();\n\n  @override\n  PopoverActionCellBuilder get builder =>\n      (context, parentController, controller) {\n        return DepthOptionMenu(\n          onTap: (depth) async {\n            await onDepthChanged(depth);\n            parentController.close();\n            parentController.close();\n          },\n        );\n      };\n\n  OptionDepthType depth(Node node) {\n    final level = node.attributes[OutlineBlockKeys.depth];\n    return OptionDepthType.fromLevel(level);\n  }\n\n  Future<void> onDepthChanged(OptionDepthType depth) async {\n    final selection = editorState.selection;\n    final node = selection != null\n        ? editorState.getNodeAtPath(selection.start.path)\n        : null;\n\n    if (node == null || depth == this.depth(node)) return;\n\n    final transaction = editorState.transaction;\n    transaction.updateNode(\n      node,\n      {OutlineBlockKeys.depth: depth.level},\n    );\n    await editorState.apply(transaction);\n  }\n}\n\nclass DepthOptionMenu extends StatelessWidget {\n  const DepthOptionMenu({\n    super.key,\n    required this.onTap,\n  });\n\n  final Future<void> Function(OptionDepthType) onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: 42,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: buildDepthOptions(context, onTap),\n      ),\n    );\n  }\n\n  List<Widget> buildDepthOptions(\n    BuildContext context,\n    Future<void> Function(OptionDepthType) onTap,\n  ) {\n    return OptionDepthType.values\n        .map((e) => OptionDepthWrapper(e))\n        .map(\n          (e) => HoverButton(\n            onTap: () => onTap(e.inner),\n            itemHeight: ActionListSizes.itemHeight,\n            name: e.name,\n          ),\n        )\n        .toList();\n  }\n}\n\nclass OptionDepthWrapper extends ActionCell {\n  OptionDepthWrapper(this.inner);\n\n  final OptionDepthType inner;\n\n  @override\n  String get name => inner.description;\n}\n\nclass OptionActionWrapper extends ActionCell {\n  OptionActionWrapper(this.inner);\n\n  final OptionAction inner;\n\n  @override\n  Widget? leftIcon(Color iconColor) => FlowySvg(inner.svg);\n\n  @override\n  String get name => inner.description;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/divider_option_action.dart",
    "content": "import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\n\nclass DividerOptionAction extends CustomActionCell {\n  @override\n  Widget buildWithContext(\n    BuildContext context,\n    PopoverController controller,\n    PopoverMutex? mutex,\n  ) {\n    return const Padding(\n      padding: EdgeInsets.symmetric(vertical: 4.0),\n      child: Divider(\n        height: 1.0,\n        thickness: 1.0,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart'\n    hide QuoteBlockKeys, quoteNode;\nimport 'package:easy_localization/easy_localization.dart' hide TextDirection;\nimport 'package:easy_localization/easy_localization.dart';\n\nexport 'align_option_action.dart';\nexport 'color_option_action.dart';\nexport 'depth_option_action.dart';\nexport 'divider_option_action.dart';\nexport 'turn_into_option_action.dart';\n\nenum EditorOptionActionType {\n  turnInto,\n  color,\n  align,\n  depth;\n\n  Set<String> get supportTypes {\n    switch (this) {\n      case EditorOptionActionType.turnInto:\n        return {\n          ParagraphBlockKeys.type,\n          HeadingBlockKeys.type,\n          QuoteBlockKeys.type,\n          CalloutBlockKeys.type,\n          BulletedListBlockKeys.type,\n          NumberedListBlockKeys.type,\n          TodoListBlockKeys.type,\n          ToggleListBlockKeys.type,\n          SubPageBlockKeys.type,\n        };\n      case EditorOptionActionType.color:\n        return {\n          ParagraphBlockKeys.type,\n          HeadingBlockKeys.type,\n          BulletedListBlockKeys.type,\n          NumberedListBlockKeys.type,\n          QuoteBlockKeys.type,\n          TodoListBlockKeys.type,\n          CalloutBlockKeys.type,\n          OutlineBlockKeys.type,\n          ToggleListBlockKeys.type,\n        };\n      case EditorOptionActionType.align:\n        return {\n          ImageBlockKeys.type,\n          SimpleTableBlockKeys.type,\n        };\n      case EditorOptionActionType.depth:\n        return {\n          OutlineBlockKeys.type,\n        };\n    }\n  }\n}\n\nenum OptionAction {\n  delete,\n  duplicate,\n  turnInto,\n  moveUp,\n  moveDown,\n  copyLinkToBlock,\n\n  /// callout background color\n  color,\n  divider,\n  align,\n\n  // Outline block\n  depth,\n\n  // Simple table\n  setToPageWidth,\n  distributeColumnsEvenly;\n\n  FlowySvgData get svg {\n    switch (this) {\n      case OptionAction.delete:\n        return FlowySvgs.trash_s;\n      case OptionAction.duplicate:\n        return FlowySvgs.copy_s;\n      case OptionAction.turnInto:\n        return FlowySvgs.turninto_s;\n      case OptionAction.moveUp:\n        return const FlowySvgData('editor/move_up');\n      case OptionAction.moveDown:\n        return const FlowySvgData('editor/move_down');\n      case OptionAction.color:\n        return const FlowySvgData('editor/color');\n      case OptionAction.divider:\n        return const FlowySvgData('editor/divider');\n      case OptionAction.align:\n        return FlowySvgs.m_aa_bulleted_list_s;\n      case OptionAction.depth:\n        return FlowySvgs.tag_s;\n      case OptionAction.copyLinkToBlock:\n        return FlowySvgs.share_tab_copy_s;\n      case OptionAction.setToPageWidth:\n        return FlowySvgs.table_set_to_page_width_s;\n      case OptionAction.distributeColumnsEvenly:\n        return FlowySvgs.table_distribute_columns_evenly_s;\n    }\n  }\n\n  String get description {\n    switch (this) {\n      case OptionAction.delete:\n        return LocaleKeys.document_plugins_optionAction_delete.tr();\n      case OptionAction.duplicate:\n        return LocaleKeys.document_plugins_optionAction_duplicate.tr();\n      case OptionAction.turnInto:\n        return LocaleKeys.document_plugins_optionAction_turnInto.tr();\n      case OptionAction.moveUp:\n        return LocaleKeys.document_plugins_optionAction_moveUp.tr();\n      case OptionAction.moveDown:\n        return LocaleKeys.document_plugins_optionAction_moveDown.tr();\n      case OptionAction.color:\n        return LocaleKeys.document_plugins_optionAction_color.tr();\n      case OptionAction.align:\n        return LocaleKeys.document_plugins_optionAction_align.tr();\n      case OptionAction.depth:\n        return LocaleKeys.document_plugins_optionAction_depth.tr();\n      case OptionAction.copyLinkToBlock:\n        return LocaleKeys.document_plugins_optionAction_copyLinkToBlock.tr();\n      case OptionAction.divider:\n        throw UnsupportedError('Divider does not have description');\n      case OptionAction.setToPageWidth:\n        return LocaleKeys\n            .document_plugins_simpleTable_moreActions_setToPageWidth\n            .tr();\n      case OptionAction.distributeColumnsEvenly:\n        return LocaleKeys\n            .document_plugins_simpleTable_moreActions_distributeColumnsWidth\n            .tr();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart'\n    hide QuoteBlockKeys, quoteNode;\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass TurnIntoOptionAction extends CustomActionCell {\n  TurnIntoOptionAction({\n    required this.editorState,\n    required this.blockComponentBuilder,\n    required this.mutex,\n  });\n\n  final EditorState editorState;\n  final Map<String, BlockComponentBuilder> blockComponentBuilder;\n  final PopoverController innerController = PopoverController();\n  final PopoverMutex mutex;\n\n  @override\n  Widget buildWithContext(\n    BuildContext context,\n    PopoverController controller,\n    PopoverMutex? mutex,\n  ) {\n    return TurnInfoButton(\n      editorState: editorState,\n      blockComponentBuilder: blockComponentBuilder,\n      mutex: this.mutex,\n    );\n  }\n}\n\nclass TurnInfoButton extends StatefulWidget {\n  const TurnInfoButton({\n    super.key,\n    required this.editorState,\n    required this.blockComponentBuilder,\n    required this.mutex,\n  });\n\n  final EditorState editorState;\n  final Map<String, BlockComponentBuilder> blockComponentBuilder;\n  final PopoverMutex mutex;\n\n  @override\n  State<TurnInfoButton> createState() => _TurnInfoButtonState();\n}\n\nclass _TurnInfoButtonState extends State<TurnInfoButton> {\n  final PopoverController innerController = PopoverController();\n  bool isOpen = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      asBarrier: true,\n      controller: innerController,\n      mutex: widget.mutex,\n      popupBuilder: (context) {\n        isOpen = true;\n        return BlocProvider<BlockActionOptionCubit>(\n          create: (context) => BlockActionOptionCubit(\n            editorState: widget.editorState,\n            blockComponentBuilder: widget.blockComponentBuilder,\n          ),\n          child: BlocBuilder<BlockActionOptionCubit, BlockActionOptionState>(\n            builder: (context, _) => _buildTurnIntoOptionMenu(context),\n          ),\n        );\n      },\n      onClose: () => isOpen = false,\n      direction: PopoverDirection.rightWithCenterAligned,\n      animationDuration: Durations.short3,\n      beginScaleFactor: 1.0,\n      beginOpacity: 0.8,\n      child: HoverButton(\n        itemHeight: ActionListSizes.itemHeight,\n        // todo(lucas): replace the svg with the correct one\n        leftIcon: const FlowySvg(FlowySvgs.turninto_s),\n        name: LocaleKeys.document_plugins_optionAction_turnInto.tr(),\n        onTap: () {\n          if (!isOpen) {\n            innerController.show();\n          }\n        },\n      ),\n    );\n  }\n\n  Widget _buildTurnIntoOptionMenu(BuildContext context) {\n    final selection = widget.editorState.selection?.normalized;\n    // the selection may not be collapsed, for example, if a block contains some children,\n    // the selection will be the start from the current block and end at the last child block.\n    // we should take care of this case:\n    // converting a block that contains children to a heading block,\n    //  we should move all the children under the heading block.\n    if (selection == null) {\n      return const SizedBox.shrink();\n    }\n\n    final node = widget.editorState.getNodeAtPath(selection.start.path);\n    if (node == null) {\n      return const SizedBox.shrink();\n    }\n\n    return TurnIntoOptionMenu(\n      node: node,\n      hasNonSupportedTypes: _hasNonSupportedTypes(selection),\n    );\n  }\n\n  bool _hasNonSupportedTypes(Selection selection) {\n    final nodes = widget.editorState.getNodesInSelection(selection);\n    if (nodes.isEmpty) {\n      return false;\n    }\n\n    for (final node in nodes) {\n      if (!EditorOptionActionType.turnInto.supportTypes.contains(node.type)) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n}\n\nclass TurnIntoOptionMenu extends StatelessWidget {\n  const TurnIntoOptionMenu({\n    super.key,\n    required this.node,\n    required this.hasNonSupportedTypes,\n  });\n\n  final Node node;\n\n  /// Signifies whether the selection contains [Node]s that are not supported,\n  /// these often do not have a [Delta], example could be [FileBlockComponent].\n  ///\n  final bool hasNonSupportedTypes;\n\n  @override\n  Widget build(BuildContext context) {\n    if (hasNonSupportedTypes) {\n      return buildItem(\n        pateItem,\n        textSuggestionItem,\n        context.read<BlockActionOptionCubit>().editorState,\n      );\n    }\n\n    return _buildTurnIntoOptions(context, node);\n  }\n\n  Widget _buildTurnIntoOptions(BuildContext context, Node node) {\n    final editorState = context.read<BlockActionOptionCubit>().editorState;\n    SuggestionItem currentSuggestionItem = textSuggestionItem;\n    final List<SuggestionItem> suggestionItems = suggestions.sublist(0, 4);\n    final List<SuggestionItem> turnIntoItems =\n        suggestions.sublist(4, suggestions.length);\n    final textColor = Color(0xff99A1A8);\n\n    void refreshSuggestions() {\n      final selection = editorState.selection;\n      if (selection == null || !selection.isSingle) return;\n      final node = editorState.getNodeAtPath(selection.start.path);\n      if (node == null || node.delta == null) return;\n      final nodeType = node.type;\n      SuggestionType? suggestionType;\n      if (nodeType == HeadingBlockKeys.type) {\n        final level = node.attributes[HeadingBlockKeys.level] ?? 1;\n        if (level == 1) {\n          suggestionType = SuggestionType.h1;\n        } else if (level == 2) {\n          suggestionType = SuggestionType.h2;\n        } else if (level == 3) {\n          suggestionType = SuggestionType.h3;\n        }\n      } else if (nodeType == ToggleListBlockKeys.type) {\n        final level = node.attributes[ToggleListBlockKeys.level];\n        if (level == null) {\n          suggestionType = SuggestionType.toggle;\n        } else if (level == 1) {\n          suggestionType = SuggestionType.toggleH1;\n        } else if (level == 2) {\n          suggestionType = SuggestionType.toggleH2;\n        } else if (level == 3) {\n          suggestionType = SuggestionType.toggleH3;\n        }\n      } else {\n        suggestionType = nodeType2SuggestionType[nodeType];\n      }\n      if (suggestionType == null) return;\n      suggestionItems.clear();\n      turnIntoItems.clear();\n      for (final item in suggestions) {\n        if (item.type.group == suggestionType.group &&\n            item.type != suggestionType) {\n          suggestionItems.add(item);\n        } else {\n          turnIntoItems.add(item);\n        }\n      }\n      currentSuggestionItem =\n          suggestions.where((item) => item.type == suggestionType).first;\n    }\n\n    refreshSuggestions();\n\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        buildSubTitle(\n          LocaleKeys.document_toolbar_suggestions.tr(),\n          textColor,\n        ),\n        ...List.generate(suggestionItems.length, (index) {\n          return buildItem(\n            suggestionItems[index],\n            currentSuggestionItem,\n            editorState,\n          );\n        }),\n        buildSubTitle(LocaleKeys.document_toolbar_turnInto.tr(), textColor),\n        ...List.generate(turnIntoItems.length, (index) {\n          return buildItem(\n            turnIntoItems[index],\n            currentSuggestionItem,\n            editorState,\n          );\n        }),\n      ],\n    );\n  }\n\n  Widget buildSubTitle(String text, Color color) {\n    return Container(\n      height: 32,\n      margin: EdgeInsets.symmetric(horizontal: 8),\n      child: Align(\n        alignment: Alignment.centerLeft,\n        child: FlowyText.semibold(\n          text,\n          color: color,\n          figmaLineHeight: 16,\n        ),\n      ),\n    );\n  }\n\n  Widget buildItem(\n    SuggestionItem item,\n    SuggestionItem currentSuggestionItem,\n    EditorState state,\n  ) {\n    final isSelected = item.type == currentSuggestionItem.type;\n    return SizedBox(\n      height: 36,\n      child: FlowyButton(\n        leftIconSize: const Size.square(20),\n        leftIcon: FlowySvg(item.svg),\n        iconPadding: 12,\n        text: FlowyText(\n          item.title,\n          fontWeight: FontWeight.w400,\n          figmaLineHeight: 20,\n        ),\n        rightIcon: isSelected ? FlowySvg(FlowySvgs.toolbar_check_m) : null,\n        onTap: () => item.onTap.call(state, false),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'operations/ai_writer_cubit.dart';\nimport 'operations/ai_writer_entities.dart';\nimport 'operations/ai_writer_node_extension.dart';\nimport 'widgets/ai_writer_suggestion_actions.dart';\nimport 'widgets/ai_writer_prompt_input_more_button.dart';\n\nclass AiWriterBlockKeys {\n  const AiWriterBlockKeys._();\n\n  static const String type = 'ai_writer';\n\n  static const String isInitialized = 'is_initialized';\n  static const String selection = 'selection';\n  static const String command = 'command';\n\n  /// Sample usage:\n  ///\n  /// `attributes: {\n  ///   'ai_writer_delta_suggestion': 'original'\n  /// }`\n  static const String suggestion = 'ai_writer_delta_suggestion';\n  static const String suggestionOriginal = 'original';\n  static const String suggestionReplacement = 'replacement';\n}\n\nNode aiWriterNode({\n  required Selection? selection,\n  required AiWriterCommand command,\n}) {\n  return Node(\n    type: AiWriterBlockKeys.type,\n    attributes: {\n      AiWriterBlockKeys.isInitialized: false,\n      AiWriterBlockKeys.selection: selection?.toJson(),\n      AiWriterBlockKeys.command: command.index,\n    },\n  );\n}\n\nclass AIWriterBlockComponentBuilder extends BlockComponentBuilder {\n  AIWriterBlockComponentBuilder();\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    return AiWriterBlockComponent(\n      key: node.key,\n      node: node,\n      showActions: showActions(node),\n      actionBuilder: (context, state) => actionBuilder(\n        blockComponentContext,\n        state,\n      ),\n      actionTrailingBuilder: (context, state) => actionTrailingBuilder(\n        blockComponentContext,\n        state,\n      ),\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate => (node) =>\n      node.children.isEmpty &&\n      node.attributes[AiWriterBlockKeys.isInitialized] is bool &&\n      node.attributes[AiWriterBlockKeys.selection] is Map? &&\n      node.attributes[AiWriterBlockKeys.command] is int;\n}\n\nclass AiWriterBlockComponent extends BlockComponentStatefulWidget {\n  const AiWriterBlockComponent({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n  });\n\n  @override\n  State<AiWriterBlockComponent> createState() => _AIWriterBlockComponentState();\n}\n\nclass _AIWriterBlockComponentState extends State<AiWriterBlockComponent> {\n  final textController = AiPromptInputTextEditingController();\n  final overlayController = OverlayPortalController();\n  final layerLink = LayerLink();\n  final focusNode = FocusNode();\n\n  late final editorState = context.read<EditorState>();\n\n  @override\n  void initState() {\n    super.initState();\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      overlayController.show();\n      context.read<AiWriterCubit>().register(widget.node);\n    });\n  }\n\n  @override\n  void dispose() {\n    textController.dispose();\n    focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (UniversalPlatform.isMobile) {\n      return const SizedBox.shrink();\n    }\n\n    final documentId = context.read<DocumentBloc?>()?.documentId;\n\n    return BlocProvider(\n      create: (_) => AIPromptInputBloc(\n        predefinedFormat: null,\n        objectId: documentId ?? editorState.document.root.id,\n      ),\n      child: LayoutBuilder(\n        builder: (context, constraints) {\n          return OverlayPortal(\n            controller: overlayController,\n            overlayChildBuilder: (context) {\n              return Center(\n                child: CompositedTransformFollower(\n                  link: layerLink,\n                  showWhenUnlinked: false,\n                  child: Container(\n                    padding: const EdgeInsets.only(\n                      left: 40.0,\n                      bottom: 16.0,\n                    ),\n                    width: constraints.maxWidth,\n                    child: Focus(\n                      focusNode: focusNode,\n                      child: OverlayContent(\n                        editorState: editorState,\n                        node: widget.node,\n                        textController: textController,\n                      ),\n                    ),\n                  ),\n                ),\n              );\n            },\n            child: CompositedTransformTarget(\n              link: layerLink,\n              child: BlocBuilder<AiWriterCubit, AiWriterState>(\n                builder: (context, state) {\n                  return SizedBox(\n                    width: double.infinity,\n                    height: 1.0,\n                  );\n                },\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass OverlayContent extends StatefulWidget {\n  const OverlayContent({\n    super.key,\n    required this.editorState,\n    required this.node,\n    required this.textController,\n  });\n\n  final EditorState editorState;\n  final Node node;\n  final AiPromptInputTextEditingController textController;\n\n  @override\n  State<OverlayContent> createState() => _OverlayContentState();\n}\n\nclass _OverlayContentState extends State<OverlayContent> {\n  final showCommandsToggle = ValueNotifier(false);\n\n  @override\n  void dispose() {\n    showCommandsToggle.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<AiWriterCubit, AiWriterState>(\n      builder: (context, state) {\n        if (state is IdleAiWriterState ||\n            state is DocumentContentEmptyAiWriterState) {\n          return const SizedBox.shrink();\n        }\n\n        final command = (state as RegisteredAiWriter).command;\n\n        final selection = widget.node.aiWriterSelection;\n        final hasSelection = selection != null && !selection.isCollapsed;\n\n        final markdownText = switch (state) {\n          final ReadyAiWriterState ready => ready.markdownText,\n          final GeneratingAiWriterState generating => generating.markdownText,\n          _ => '',\n        };\n\n        final showSuggestedActions =\n            state is ReadyAiWriterState && !state.isFirstRun;\n        final isInitialReadyState =\n            state is ReadyAiWriterState && state.isFirstRun;\n        final showSuggestedActionsPopup =\n            showSuggestedActions && markdownText.isEmpty ||\n                (markdownText.isNotEmpty && command != AiWriterCommand.explain);\n        final showSuggestedActionsWithin = showSuggestedActions &&\n            markdownText.isNotEmpty &&\n            command == AiWriterCommand.explain;\n\n        final borderColor = Theme.of(context).isLightMode\n            ? Color(0x1F1F2329)\n            : Color(0xFF505469);\n\n        return Column(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            if (showSuggestedActionsPopup) ...[\n              Container(\n                padding: EdgeInsets.all(4.0),\n                decoration: _getModalDecoration(\n                  context,\n                  color: Theme.of(context).colorScheme.surface,\n                  borderRadius: BorderRadius.all(Radius.circular(8.0)),\n                  borderColor: borderColor,\n                ),\n                child: SuggestionActionBar(\n                  currentCommand: command,\n                  hasSelection: hasSelection,\n                  onTap: (action) {\n                    _onSelectSuggestionAction(context, action);\n                  },\n                ),\n              ),\n              const VSpace(4.0 + 1.0),\n            ],\n            Container(\n              decoration: _getModalDecoration(\n                context,\n                color: null,\n                borderColor: borderColor,\n                borderRadius: BorderRadius.all(Radius.circular(12.0)),\n              ),\n              constraints: BoxConstraints(maxHeight: 400),\n              child: Column(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  if (markdownText.isNotEmpty) ...[\n                    Flexible(\n                      child: DecoratedBox(\n                        decoration: _secondaryContentDecoration(context),\n                        child: SecondaryContentArea(\n                          markdownText: markdownText,\n                          onSelectSuggestionAction: (action) {\n                            _onSelectSuggestionAction(context, action);\n                          },\n                          command: command,\n                          showSuggestionActions: showSuggestedActionsWithin,\n                          hasSelection: hasSelection,\n                        ),\n                      ),\n                    ),\n                    Divider(height: 1.0),\n                  ],\n                  DecoratedBox(\n                    decoration: markdownText.isNotEmpty\n                        ? _mainContentDecoration(context)\n                        : _getSingleChildDeocoration(context),\n                    child: MainContentArea(\n                      textController: widget.textController,\n                      isDocumentEmpty: _isDocumentEmpty(),\n                      isInitialReadyState: isInitialReadyState,\n                      showCommandsToggle: showCommandsToggle,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n            ValueListenableBuilder(\n              valueListenable: showCommandsToggle,\n              builder: (context, value, child) {\n                if (!value || !isInitialReadyState) {\n                  return const SizedBox.shrink();\n                }\n                return Align(\n                  alignment: AlignmentDirectional.centerEnd,\n                  child: MoreAiWriterCommands(\n                    hasSelection: hasSelection,\n                    editorState: widget.editorState,\n                    onSelectCommand: (command) {\n                      final bloc = context.read<AIPromptInputBloc>();\n                      final promptId = bloc.promptId;\n                      final state = bloc.state;\n                      final showPredefinedFormats = state.showPredefinedFormats;\n                      final predefinedFormat = state.predefinedFormat;\n                      final text = widget.textController.text;\n\n                      context.read<AiWriterCubit>().runCommand(\n                            command,\n                            text,\n                            showPredefinedFormats ? predefinedFormat : null,\n                            promptId,\n                          );\n                    },\n                  ),\n                );\n              },\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  BoxDecoration _getModalDecoration(\n    BuildContext context, {\n    required Color? color,\n    required Color borderColor,\n    required BorderRadius borderRadius,\n  }) {\n    return BoxDecoration(\n      color: color,\n      border: Border.all(\n        color: borderColor,\n        strokeAlign: BorderSide.strokeAlignOutside,\n      ),\n      borderRadius: borderRadius,\n      boxShadow: Theme.of(context).isLightMode\n          ? ShadowConstants.lightSmall\n          : ShadowConstants.darkSmall,\n    );\n  }\n\n  BoxDecoration _getSingleChildDeocoration(BuildContext context) {\n    return BoxDecoration(\n      color: Theme.of(context).colorScheme.surface,\n      borderRadius: BorderRadius.all(Radius.circular(12.0)),\n    );\n  }\n\n  BoxDecoration _secondaryContentDecoration(BuildContext context) {\n    return BoxDecoration(\n      color: Theme.of(context).colorScheme.surfaceContainerHighest,\n      borderRadius: BorderRadius.vertical(top: Radius.circular(12.0)),\n    );\n  }\n\n  BoxDecoration _mainContentDecoration(BuildContext context) {\n    return BoxDecoration(\n      color: Theme.of(context).colorScheme.surface,\n      borderRadius: BorderRadius.vertical(bottom: Radius.circular(12.0)),\n    );\n  }\n\n  void _onSelectSuggestionAction(\n    BuildContext context,\n    SuggestionAction action,\n  ) {\n    final predefinedFormat =\n        context.read<AIPromptInputBloc>().state.predefinedFormat;\n    context.read<AiWriterCubit>().runResponseAction(\n          action,\n          predefinedFormat,\n        );\n  }\n\n  bool _isDocumentEmpty() {\n    if (widget.editorState.isEmptyForContinueWriting()) {\n      final documentContext = widget.editorState.document.root.context;\n      if (documentContext == null) {\n        return true;\n      }\n      final view = documentContext.read<ViewBloc>().state.view;\n      if (view.name.isEmpty) {\n        return true;\n      }\n    }\n    return false;\n  }\n}\n\nclass SecondaryContentArea extends StatelessWidget {\n  const SecondaryContentArea({\n    super.key,\n    required this.command,\n    required this.markdownText,\n    required this.showSuggestionActions,\n    required this.hasSelection,\n    required this.onSelectSuggestionAction,\n  });\n\n  final AiWriterCommand command;\n  final String markdownText;\n  final bool showSuggestionActions;\n  final bool hasSelection;\n  final void Function(SuggestionAction) onSelectSuggestionAction;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: double.infinity,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          const VSpace(8.0),\n          Container(\n            height: 24.0,\n            padding: EdgeInsets.symmetric(horizontal: 14.0),\n            alignment: AlignmentDirectional.centerStart,\n            child: FlowyText(\n              command.i18n,\n              fontSize: 12,\n              fontWeight: FontWeight.w600,\n              color: Color(0xFF666D76),\n            ),\n          ),\n          const VSpace(4.0),\n          Flexible(\n            child: SingleChildScrollView(\n              physics: ClampingScrollPhysics(),\n              padding: EdgeInsets.symmetric(horizontal: 14.0),\n              child: AIMarkdownText(\n                markdown: markdownText,\n              ),\n            ),\n          ),\n          if (showSuggestionActions) ...[\n            const VSpace(4.0),\n            Padding(\n              padding: EdgeInsets.symmetric(horizontal: 8.0),\n              child: SuggestionActionBar(\n                currentCommand: command,\n                hasSelection: hasSelection,\n                onTap: onSelectSuggestionAction,\n              ),\n            ),\n          ],\n          const VSpace(8.0),\n        ],\n      ),\n    );\n  }\n}\n\nclass MainContentArea extends StatelessWidget {\n  const MainContentArea({\n    super.key,\n    required this.textController,\n    required this.isInitialReadyState,\n    required this.isDocumentEmpty,\n    required this.showCommandsToggle,\n  });\n\n  final AiPromptInputTextEditingController textController;\n  final bool isInitialReadyState;\n  final bool isDocumentEmpty;\n  final ValueNotifier<bool> showCommandsToggle;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<AiWriterCubit, AiWriterState>(\n      builder: (context, state) {\n        final cubit = context.read<AiWriterCubit>();\n\n        if (state is ReadyAiWriterState) {\n          return DesktopPromptInput(\n            isStreaming: false,\n            hideDecoration: true,\n            hideFormats: [\n              AiWriterCommand.fixSpellingAndGrammar,\n              AiWriterCommand.improveWriting,\n              AiWriterCommand.makeLonger,\n              AiWriterCommand.makeShorter,\n            ].contains(state.command),\n            textController: textController,\n            onSubmitted: (message, format, _, promptId) {\n              cubit.runCommand(state.command, message, format, promptId);\n            },\n            onStopStreaming: () => cubit.stopStream(),\n            selectedSourcesNotifier: cubit.selectedSourcesNotifier,\n            onUpdateSelectedSources: (sources) {\n              cubit.selectedSourcesNotifier.value = [\n                ...sources,\n              ];\n            },\n            extraBottomActionButton: isInitialReadyState\n                ? ValueListenableBuilder(\n                    valueListenable: showCommandsToggle,\n                    builder: (context, value, _) {\n                      return AiWriterPromptMoreButton(\n                        isEnabled: !isDocumentEmpty,\n                        isSelected: value,\n                        onTap: () => showCommandsToggle.value = !value,\n                      );\n                    },\n                  )\n                : null,\n          );\n        }\n        if (state is GeneratingAiWriterState) {\n          return Padding(\n            padding: const EdgeInsets.all(8.0),\n            child: Row(\n              children: [\n                const HSpace(6.0),\n                Expanded(\n                  child: AILoadingIndicator(\n                    text: state.command == AiWriterCommand.explain\n                        ? LocaleKeys.ai_analyzing.tr()\n                        : LocaleKeys.ai_editing.tr(),\n                  ),\n                ),\n                const HSpace(8.0),\n                PromptInputSendButton(\n                  state: SendButtonState.streaming,\n                  onSendPressed: () {},\n                  onStopStreaming: () => cubit.stopStream(),\n                ),\n              ],\n            ),\n          );\n        }\n        if (state is ErrorAiWriterState) {\n          return Padding(\n            padding: EdgeInsets.all(8.0),\n            child: Row(\n              children: [\n                const FlowySvg(\n                  FlowySvgs.toast_error_filled_s,\n                  blendMode: null,\n                ),\n                const HSpace(8.0),\n                Expanded(\n                  child: FlowyText(\n                    state.error.message,\n                    maxLines: null,\n                  ),\n                ),\n                const HSpace(8.0),\n                FlowyIconButton(\n                  width: 32,\n                  hoverColor: Colors.transparent,\n                  icon: FlowySvg(\n                    FlowySvgs.toast_close_s,\n                    size: Size.square(20),\n                  ),\n                  onPressed: () => cubit.exit(),\n                ),\n              ],\n            ),\n          );\n        }\n        if (state is LocalAIStreamingAiWriterState) {\n          final text = switch (state.state) {\n            LocalAIStreamingState.notReady =>\n              LocaleKeys.settings_aiPage_keys_localAINotReadyRetryLater.tr(),\n            LocalAIStreamingState.disabled =>\n              LocaleKeys.settings_aiPage_keys_localAIDisabled.tr(),\n          };\n          return Padding(\n            padding: EdgeInsets.all(8.0),\n            child: Row(\n              children: [\n                const HSpace(8.0),\n                Opacity(\n                  opacity: 0.5,\n                  child: FlowyText(text),\n                ),\n              ],\n            ),\n          );\n        }\n        return const SizedBox.shrink();\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport 'operations/ai_writer_entities.dart';\n\nconst _improveWritingToolbarItemId = 'appflowy.editor.ai_improve_writing';\nconst _aiWriterToolbarItemId = 'appflowy.editor.ai_writer';\n\nfinal ToolbarItem improveWritingItem = ToolbarItem(\n  id: _improveWritingToolbarItemId,\n  group: 0,\n  isActive: onlyShowInTextTypeAndExcludeTable,\n  builder: (context, editorState, _, __, tooltipBuilder) =>\n      ImproveWritingButton(\n    editorState: editorState,\n    tooltipBuilder: tooltipBuilder,\n  ),\n);\n\nfinal ToolbarItem aiWriterItem = ToolbarItem(\n  id: _aiWriterToolbarItemId,\n  group: 0,\n  isActive: onlyShowInTextTypeAndExcludeTable,\n  builder: (context, editorState, _, __, tooltipBuilder) =>\n      AiWriterToolbarActionList(\n    editorState: editorState,\n    tooltipBuilder: tooltipBuilder,\n  ),\n);\n\nclass AiWriterToolbarActionList extends StatefulWidget {\n  const AiWriterToolbarActionList({\n    super.key,\n    required this.editorState,\n    this.tooltipBuilder,\n  });\n\n  final EditorState editorState;\n  final ToolbarTooltipBuilder? tooltipBuilder;\n\n  @override\n  State<AiWriterToolbarActionList> createState() =>\n      _AiWriterToolbarActionListState();\n}\n\nclass _AiWriterToolbarActionListState extends State<AiWriterToolbarActionList> {\n  final popoverController = PopoverController();\n  bool isSelected = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      controller: popoverController,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      offset: const Offset(0, 2.0),\n      onOpen: () => keepEditorFocusNotifier.increase(),\n      onClose: () {\n        setState(() {\n          isSelected = false;\n        });\n        keepEditorFocusNotifier.decrease();\n      },\n      popupBuilder: (context) => buildPopoverContent(),\n      child: buildChild(context),\n    );\n  }\n\n  Widget buildPopoverContent() {\n    return SeparatedColumn(\n      mainAxisSize: MainAxisSize.min,\n      separatorBuilder: () => const VSpace(4.0),\n      children: [\n        actionWrapper(AiWriterCommand.improveWriting),\n        actionWrapper(AiWriterCommand.userQuestion),\n        actionWrapper(AiWriterCommand.fixSpellingAndGrammar),\n        // actionWrapper(AiWriterCommand.summarize),\n        actionWrapper(AiWriterCommand.explain),\n        divider(),\n        actionWrapper(AiWriterCommand.makeLonger),\n        actionWrapper(AiWriterCommand.makeShorter),\n      ],\n    );\n  }\n\n  Widget actionWrapper(AiWriterCommand command) {\n    return SizedBox(\n      height: 36,\n      child: FlowyButton(\n        leftIconSize: const Size.square(20),\n        leftIcon: FlowySvg(command.icon),\n        iconPadding: 12,\n        text: FlowyText(\n          command.i18n,\n          figmaLineHeight: 20,\n        ),\n        onTap: () {\n          popoverController.close();\n          _insertAiNode(widget.editorState, command);\n        },\n      ),\n    );\n  }\n\n  Widget divider() {\n    return const Divider(\n      thickness: 1.0,\n      height: 1.0,\n    );\n  }\n\n  Widget buildChild(BuildContext context) {\n    final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme;\n    final child = FlowyIconButton(\n      width: 48,\n      height: 32,\n      isSelected: isSelected,\n      hoverColor: EditorStyleCustomizer.toolbarHoverColor(context),\n      icon: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          FlowySvg(\n            FlowySvgs.toolbar_ai_writer_m,\n            size: Size.square(20),\n            color: iconScheme.primary,\n          ),\n          HSpace(4),\n          FlowySvg(\n            FlowySvgs.toolbar_arrow_down_m,\n            size: Size(12, 20),\n            color: iconScheme.primary,\n          ),\n        ],\n      ),\n      onPressed: () {\n        if (_isAIWriterEnabled(widget.editorState)) {\n          keepEditorFocusNotifier.increase();\n          popoverController.show();\n          setState(() {\n            isSelected = true;\n          });\n        } else {\n          showToastNotification(\n            message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),\n          );\n        }\n      },\n    );\n\n    return widget.tooltipBuilder?.call(\n          context,\n          _aiWriterToolbarItemId,\n          _isAIWriterEnabled(widget.editorState)\n              ? LocaleKeys.document_plugins_aiWriter_userQuestion.tr()\n              : LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),\n          child,\n        ) ??\n        child;\n  }\n}\n\nclass ImproveWritingButton extends StatelessWidget {\n  const ImproveWritingButton({\n    super.key,\n    required this.editorState,\n    this.tooltipBuilder,\n  });\n\n  final EditorState editorState;\n  final ToolbarTooltipBuilder? tooltipBuilder;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final child = FlowyIconButton(\n      width: 36,\n      height: 32,\n      hoverColor: EditorStyleCustomizer.toolbarHoverColor(context),\n      icon: FlowySvg(\n        FlowySvgs.toolbar_ai_improve_writing_m,\n        size: Size.square(20.0),\n        color: theme.iconColorScheme.primary,\n      ),\n      onPressed: () {\n        if (_isAIWriterEnabled(editorState)) {\n          keepEditorFocusNotifier.increase();\n          _insertAiNode(editorState, AiWriterCommand.improveWriting);\n        } else {\n          showToastNotification(\n            message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),\n          );\n        }\n      },\n    );\n\n    return tooltipBuilder?.call(\n          context,\n          _aiWriterToolbarItemId,\n          _isAIWriterEnabled(editorState)\n              ? LocaleKeys.document_plugins_aiWriter_improveWriting.tr()\n              : LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),\n          child,\n        ) ??\n        child;\n  }\n}\n\nvoid _insertAiNode(EditorState editorState, AiWriterCommand command) async {\n  final selection = editorState.selection?.normalized;\n  if (selection == null) {\n    return;\n  }\n\n  final transaction = editorState.transaction\n    ..insertNode(\n      selection.end.path.next,\n      aiWriterNode(\n        selection: selection,\n        command: command,\n      ),\n    )\n    ..selectionExtraInfo = {selectionExtraInfoDisableToolbar: true};\n\n  await editorState.apply(\n    transaction,\n    options: const ApplyOptions(\n      recordUndo: false,\n      inMemoryUpdate: true,\n    ),\n    withUpdateSelection: false,\n  );\n}\n\nbool _isAIWriterEnabled(EditorState editorState) {\n  return true;\n}\n\nbool onlyShowInTextTypeAndExcludeTable(\n  EditorState editorState,\n) {\n  return onlyShowInTextType(editorState) && notShowInTable(editorState);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/shared/markdown_to_document.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nimport '../ai_writer_block_component.dart';\nimport 'ai_writer_entities.dart';\nimport 'ai_writer_node_extension.dart';\n\nFuture<void> setAiWriterNodeIsInitialized(\n  EditorState editorState,\n  Node node,\n) async {\n  final transaction = editorState.transaction\n    ..updateNode(node, {\n      AiWriterBlockKeys.isInitialized: true,\n    });\n\n  await editorState.apply(\n    transaction,\n    options: const ApplyOptions(\n      recordUndo: false,\n      inMemoryUpdate: true,\n    ),\n    withUpdateSelection: false,\n  );\n\n  final selection = node.aiWriterSelection;\n  if (selection != null && !selection.isCollapsed) {\n    unawaited(\n      editorState.updateSelectionWithReason(\n        selection,\n        extraInfo: {selectionExtraInfoDisableToolbar: true},\n      ),\n    );\n  }\n}\n\nFuture<void> removeAiWriterNode(\n  EditorState editorState,\n  Node node,\n) async {\n  final transaction = editorState.transaction..deleteNode(node);\n  await editorState.apply(\n    transaction,\n    options: const ApplyOptions(recordUndo: false),\n    withUpdateSelection: false,\n  );\n}\n\nFuture<void> formatSelection(\n  EditorState editorState,\n  Selection selection,\n  ApplySuggestionFormatType formatType,\n) async {\n  final nodes = editorState.getNodesInSelection(selection).toList();\n  if (nodes.isEmpty) {\n    return;\n  }\n  final transaction = editorState.transaction;\n\n  if (nodes.length == 1) {\n    final node = nodes.removeAt(0);\n    if (node.delta != null) {\n      final delta = Delta()\n        ..retain(selection.start.offset)\n        ..retain(\n          selection.length,\n          attributes: formatType.attributes,\n        );\n      transaction.addDeltaToComposeMap(node, delta);\n    }\n  } else {\n    final firstNode = nodes.removeAt(0);\n    final lastNode = nodes.removeLast();\n\n    if (firstNode.delta != null) {\n      final text = firstNode.delta!.toPlainText();\n      final remainderLength = text.length - selection.start.offset;\n      final delta = Delta()\n        ..retain(selection.start.offset)\n        ..retain(remainderLength, attributes: formatType.attributes);\n      transaction.addDeltaToComposeMap(firstNode, delta);\n    }\n\n    if (lastNode.delta != null) {\n      final delta = Delta()\n        ..retain(selection.end.offset, attributes: formatType.attributes);\n      transaction.addDeltaToComposeMap(lastNode, delta);\n    }\n\n    for (final node in nodes) {\n      if (node.delta == null) {\n        continue;\n      }\n      final length = node.delta!.length;\n      if (length != 0) {\n        final delta = Delta()\n          ..retain(length, attributes: formatType.attributes);\n        transaction.addDeltaToComposeMap(node, delta);\n      }\n    }\n  }\n\n  transaction.compose();\n  await editorState.apply(\n    transaction,\n    options: ApplyOptions(\n      inMemoryUpdate: true,\n      recordUndo: false,\n    ),\n    withUpdateSelection: false,\n  );\n}\n\nFuture<Position> ensurePreviousNodeIsEmptyParagraph(\n  EditorState editorState,\n  Node aiWriterNode,\n) async {\n  final previous = aiWriterNode.previous;\n  final needsEmptyParagraphNode = previous == null ||\n      previous.type != ParagraphBlockKeys.type ||\n      (previous.delta?.toPlainText().isNotEmpty ?? false);\n\n  final Position position;\n  final transaction = editorState.transaction;\n\n  if (needsEmptyParagraphNode) {\n    position = Position(path: aiWriterNode.path);\n    transaction.insertNode(aiWriterNode.path, paragraphNode());\n  } else {\n    position = Position(path: previous.path);\n  }\n  transaction.afterSelection = Selection.collapsed(position);\n\n  await editorState.apply(\n    transaction,\n    options: ApplyOptions(\n      inMemoryUpdate: true,\n      recordUndo: false,\n    ),\n  );\n\n  return position;\n}\n\nextension SaveAIResponseExtension on EditorState {\n  Future<void> insertBelow({\n    required Node node,\n    required String markdownText,\n  }) async {\n    final selection = this.selection?.normalized;\n    if (selection == null) {\n      return;\n    }\n\n    final nodes = customMarkdownToDocument(\n      markdownText,\n      tableWidth: 250.0,\n    ).root.children.map((e) => e.deepCopy()).toList();\n    if (nodes.isEmpty) {\n      return;\n    }\n\n    final insertedPath = selection.end.path.next;\n    final lastDeltaLength = nodes.lastOrNull?.delta?.length ?? 0;\n\n    final transaction = this.transaction\n      ..insertNodes(insertedPath, nodes)\n      ..afterSelection = Selection(\n        start: Position(path: insertedPath),\n        end: Position(\n          path: insertedPath.nextNPath(nodes.length - 1),\n          offset: lastDeltaLength,\n        ),\n      );\n\n    await apply(transaction);\n  }\n\n  Future<void> replace({\n    required Selection selection,\n    required String text,\n  }) async {\n    final trimmedText = text.trim();\n    if (trimmedText.isEmpty) {\n      return;\n    }\n    await switch (kdefaultReplacementType) {\n      AskAIReplacementType.markdown =>\n        _replaceWithMarkdown(selection, trimmedText),\n      AskAIReplacementType.plainText =>\n        _replaceWithPlainText(selection, trimmedText),\n    };\n  }\n\n  Future<void> _replaceWithMarkdown(\n    Selection selection,\n    String markdownText,\n  ) async {\n    final nodes = customMarkdownToDocument(markdownText)\n        .root\n        .children\n        .map((e) => e.deepCopy())\n        .toList();\n    if (nodes.isEmpty) {\n      return;\n    }\n\n    final nodesInSelection = getNodesInSelection(selection);\n    final newSelection = Selection(\n      start: selection.start,\n      end: Position(\n        path: selection.start.path.nextNPath(nodes.length - 1),\n        offset: nodes.lastOrNull?.delta?.length ?? 0,\n      ),\n    );\n\n    final transaction = this.transaction\n      ..insertNodes(selection.start.path, nodes)\n      ..deleteNodes(nodesInSelection)\n      ..afterSelection = newSelection;\n    await apply(transaction);\n  }\n\n  Future<void> _replaceWithPlainText(\n    Selection selection,\n    String plainText,\n  ) async {\n    final nodes = getNodesInSelection(selection);\n    if (nodes.isEmpty || nodes.any((element) => element.delta == null)) {\n      return;\n    }\n\n    final replaceTexts = plainText.split('\\n')\n      ..removeWhere((element) => element.isEmpty);\n    final transaction = this.transaction\n      ..replaceTexts(\n        nodes,\n        selection,\n        replaceTexts,\n      );\n    await apply(transaction);\n\n    int endOffset = replaceTexts.last.length;\n    if (replaceTexts.length == 1) {\n      endOffset += selection.start.offset;\n    }\n    final end = Position(\n      path: [selection.start.path.first + replaceTexts.length - 1],\n      offset: endOffset,\n    );\n    this.selection = Selection(\n      start: selection.start,\n      end: end,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/foundation.dart';\n\nimport '../../base/markdown_text_robot.dart';\nimport 'ai_writer_block_operations.dart';\nimport 'ai_writer_entities.dart';\nimport 'ai_writer_node_extension.dart';\n\n/// Enable the debug log for the AiWriterCubit.\n///\n/// This is useful for debugging the AI writer cubit.\nconst _aiWriterCubitDebugLog = true;\n\nclass AiWriterCubit extends Cubit<AiWriterState> {\n  AiWriterCubit({\n    required this.documentId,\n    required this.editorState,\n    this.onCreateNode,\n    this.onRemoveNode,\n    this.onAppendToDocument,\n  })  : _aiService = getIt<AIRepository>(),\n        _textRobot = MarkdownTextRobot(editorState: editorState),\n        selectedSourcesNotifier = ValueNotifier([documentId]),\n        super(IdleAiWriterState());\n\n  final String documentId;\n  final EditorState editorState;\n  final AIRepository _aiService;\n  final MarkdownTextRobot _textRobot;\n  final void Function()? onCreateNode;\n  final void Function()? onRemoveNode;\n  final void Function()? onAppendToDocument;\n\n  Node? aiWriterNode;\n\n  final List<AiWriterRecord> records = [];\n  final ValueNotifier<List<String>> selectedSourcesNotifier;\n\n  @override\n  Future<void> close() async {\n    selectedSourcesNotifier.dispose();\n    await super.close();\n  }\n\n  Future<void> exit({\n    bool withDiscard = true,\n    bool withUnformat = true,\n  }) async {\n    if (aiWriterNode == null) {\n      return;\n    }\n    if (withDiscard) {\n      await _textRobot.discard(\n        afterSelection: aiWriterNode!.aiWriterSelection,\n      );\n    }\n    _textRobot.clear();\n    _textRobot.reset();\n    onRemoveNode?.call();\n    records.clear();\n    selectedSourcesNotifier.value = [documentId];\n    emit(IdleAiWriterState());\n\n    if (withUnformat) {\n      final selection = aiWriterNode!.aiWriterSelection;\n      if (selection == null) {\n        return;\n      }\n      await formatSelection(\n        editorState,\n        selection,\n        ApplySuggestionFormatType.clear,\n      );\n    }\n    if (aiWriterNode != null) {\n      await removeAiWriterNode(editorState, aiWriterNode!);\n      aiWriterNode = null;\n    }\n  }\n\n  void register(Node node) async {\n    if (node.isAiWriterInitialized) {\n      return;\n    }\n    if (aiWriterNode != null && node.id != aiWriterNode!.id) {\n      await removeAiWriterNode(editorState, node);\n      return;\n    }\n\n    aiWriterNode = node;\n    onCreateNode?.call();\n\n    await setAiWriterNodeIsInitialized(editorState, node);\n\n    final command = node.aiWriterCommand;\n    final (run, prompt) = await _addSelectionTextToRecords(command);\n\n    _aiWriterCubitLog(\n      'command: $command, run: $run, prompt: $prompt',\n    );\n\n    if (!run) {\n      await exit();\n      return;\n    }\n\n    runCommand(command, prompt, null, null);\n  }\n\n  void runCommand(\n    AiWriterCommand command,\n    String prompt,\n    PredefinedFormat? predefinedFormat,\n    String? promptId,\n  ) async {\n    if (aiWriterNode == null) {\n      return;\n    }\n\n    await _textRobot.discard();\n    _textRobot.clear();\n\n    switch (command) {\n      case AiWriterCommand.continueWriting:\n        await _startContinueWriting(\n          command,\n          predefinedFormat,\n          promptId,\n        );\n        break;\n      case AiWriterCommand.fixSpellingAndGrammar:\n      case AiWriterCommand.improveWriting:\n      case AiWriterCommand.makeLonger:\n      case AiWriterCommand.makeShorter:\n        await _startSuggestingEdits(\n          command,\n          prompt,\n          predefinedFormat,\n          promptId,\n        );\n        break;\n      case AiWriterCommand.explain:\n        await _startInforming(command, prompt, predefinedFormat, promptId);\n        break;\n      case AiWriterCommand.userQuestion when prompt.isNotEmpty:\n        _startAskingQuestion(\n          prompt,\n          predefinedFormat,\n          promptId,\n        );\n        break;\n      case AiWriterCommand.userQuestion:\n        emit(\n          ReadyAiWriterState(AiWriterCommand.userQuestion, isFirstRun: true),\n        );\n        break;\n    }\n  }\n\n  void _retry({\n    required PredefinedFormat? predefinedFormat,\n  }) async {\n    final lastQuestion =\n        records.lastWhereOrNull((record) => record.role == AiRole.user);\n\n    if (lastQuestion != null && state is RegisteredAiWriter) {\n      runCommand(\n        (state as RegisteredAiWriter).command,\n        lastQuestion.content,\n        lastQuestion.format,\n        null,\n      );\n    }\n  }\n\n  Future<void> stopStream() async {\n    if (aiWriterNode == null) {\n      return;\n    }\n\n    if (state is GeneratingAiWriterState) {\n      final generatingState = state as GeneratingAiWriterState;\n\n      await _textRobot.stop(\n        attributes: ApplySuggestionFormatType.replace.attributes,\n      );\n\n      if (_textRobot.hasAnyResult) {\n        records.add(AiWriterRecord.ai(content: _textRobot.markdownText));\n      }\n\n      await AIEventStopCompleteText(\n        CompleteTextTaskPB(\n          taskId: generatingState.taskId,\n        ),\n      ).send();\n\n      emit(\n        ReadyAiWriterState(\n          generatingState.command,\n          isFirstRun: false,\n          markdownText: generatingState.markdownText,\n        ),\n      );\n    }\n  }\n\n  void runResponseAction(\n    SuggestionAction action, [\n    PredefinedFormat? predefinedFormat,\n  ]) async {\n    if (aiWriterNode == null) {\n      return;\n    }\n\n    if (action case SuggestionAction.rewrite || SuggestionAction.tryAgain) {\n      _retry(predefinedFormat: predefinedFormat);\n      return;\n    }\n    if (action case SuggestionAction.discard || SuggestionAction.close) {\n      await exit();\n      return;\n    }\n\n    final selection = aiWriterNode?.aiWriterSelection;\n    if (selection == null) {\n      return;\n    }\n\n    // Accept\n    //\n    // If the user clicks accept, we need to replace the selection with the AI's response\n    if (action case SuggestionAction.accept) {\n      // trim the markdown text to avoid extra new lines\n      final trimmedMarkdownText = _textRobot.markdownText.trim();\n\n      _aiWriterCubitLog(\n        'trigger accept action, markdown text: $trimmedMarkdownText',\n      );\n\n      await formatSelection(\n        editorState,\n        selection,\n        ApplySuggestionFormatType.clear,\n      );\n\n      await _textRobot.deleteAINodes();\n\n      await _textRobot.replace(\n        selection: selection,\n        markdownText: trimmedMarkdownText,\n      );\n\n      await exit(withDiscard: false, withUnformat: false);\n\n      return;\n    }\n\n    if (action case SuggestionAction.keep) {\n      await _textRobot.persist();\n      await exit(withDiscard: false);\n      return;\n    }\n\n    if (action case SuggestionAction.insertBelow) {\n      if (state is! ReadyAiWriterState) {\n        return;\n      }\n      final command = (state as ReadyAiWriterState).command;\n      final markdownText = (state as ReadyAiWriterState).markdownText;\n      if (command == AiWriterCommand.explain && markdownText.isNotEmpty) {\n        final position = await ensurePreviousNodeIsEmptyParagraph(\n          editorState,\n          aiWriterNode!,\n        );\n        _textRobot.start(position: position);\n        await _textRobot.persist(markdownText: markdownText);\n      } else if (_textRobot.hasAnyResult) {\n        await _textRobot.persist();\n      }\n\n      await formatSelection(\n        editorState,\n        selection,\n        ApplySuggestionFormatType.clear,\n      );\n      await exit(withDiscard: false);\n    }\n  }\n\n  bool hasUnusedResponse() {\n    return switch (state) {\n      ReadyAiWriterState(\n        isFirstRun: final isInitial,\n        markdownText: final markdownText,\n      ) =>\n        !isInitial && (markdownText.isNotEmpty || _textRobot.hasAnyResult),\n      GeneratingAiWriterState() => true,\n      _ => false,\n    };\n  }\n\n  Future<(bool, String)> _addSelectionTextToRecords(\n    AiWriterCommand command,\n  ) async {\n    final node = aiWriterNode;\n\n    // check the node is registered\n    if (node == null) {\n      Log.warn('[AI writer] Node is null');\n      return (false, '');\n    }\n\n    // check the selection is valid\n    final selection = node.aiWriterSelection?.normalized;\n    if (selection == null) {\n      Log.warn('[AI writer]Selection is null');\n      return (false, '');\n    }\n\n    // if the command is continue writing, we don't need to get the selection text\n    if (command == AiWriterCommand.continueWriting) {\n      return (true, '');\n    }\n\n    // if the selection is collapsed, we don't need to get the selection text\n    if (selection.isCollapsed) {\n      return (true, '');\n    }\n\n    final selectionText = await editorState.getMarkdownInSelection(selection);\n\n    if (command == AiWriterCommand.userQuestion) {\n      records.add(\n        AiWriterRecord.user(content: selectionText, format: null),\n      );\n\n      return (true, '');\n    } else {\n      return (true, selectionText);\n    }\n  }\n\n  Future<String> _getDocumentContentFromTopToPosition(Position position) async {\n    final beginningToCursorSelection = Selection(\n      start: Position(path: [0]),\n      end: position,\n    ).normalized;\n\n    final documentText =\n        (await editorState.getMarkdownInSelection(beginningToCursorSelection))\n            .trim();\n\n    final view = await ViewBackendService.getView(documentId).toNullable();\n    final viewName = view?.name ?? '';\n\n    return \"$viewName\\n$documentText\".trim();\n  }\n\n  void _startAskingQuestion(\n    String prompt,\n    PredefinedFormat? format,\n    String? promptId,\n  ) async {\n    if (aiWriterNode == null) {\n      return;\n    }\n    final command = AiWriterCommand.userQuestion;\n\n    final stream = await _aiService.streamCompletion(\n      objectId: documentId,\n      text: prompt,\n      format: format,\n      promptId: promptId,\n      history: records,\n      sourceIds: selectedSourcesNotifier.value,\n      completionType: command.toCompletionType(),\n      onStart: () async {\n        final position = await ensurePreviousNodeIsEmptyParagraph(\n          editorState,\n          aiWriterNode!,\n        );\n        _textRobot.start(position: position);\n        records.add(\n          AiWriterRecord.user(\n            content: prompt,\n            format: format,\n          ),\n        );\n      },\n      processMessage: (text) async {\n        await _textRobot.appendMarkdownText(\n          text,\n          updateSelection: false,\n          attributes: ApplySuggestionFormatType.replace.attributes,\n        );\n        onAppendToDocument?.call();\n      },\n      processAssistMessage: (text) async {\n        if (state case final GeneratingAiWriterState generatingState) {\n          emit(\n            GeneratingAiWriterState(\n              command,\n              taskId: generatingState.taskId,\n              markdownText: generatingState.markdownText + text,\n            ),\n          );\n        }\n      },\n      onEnd: () async {\n        if (state case final GeneratingAiWriterState generatingState) {\n          await _textRobot.stop(\n            attributes: ApplySuggestionFormatType.replace.attributes,\n          );\n          emit(\n            ReadyAiWriterState(\n              command,\n              isFirstRun: false,\n              markdownText: generatingState.markdownText,\n            ),\n          );\n          records.add(\n            AiWriterRecord.ai(content: _textRobot.markdownText),\n          );\n        }\n      },\n      onError: (error) async {\n        emit(ErrorAiWriterState(command, error: error));\n        records.add(\n          AiWriterRecord.ai(content: _textRobot.markdownText),\n        );\n      },\n      onLocalAIStreamingStateChange: (state) {\n        emit(LocalAIStreamingAiWriterState(command, state: state));\n      },\n    );\n\n    if (stream != null) {\n      emit(\n        GeneratingAiWriterState(\n          command,\n          taskId: stream.$1,\n        ),\n      );\n    }\n  }\n\n  Future<void> _startContinueWriting(\n    AiWriterCommand command,\n    PredefinedFormat? predefinedFormat,\n    String? promptId,\n  ) async {\n    final position = aiWriterNode?.aiWriterSelection?.start;\n    if (position == null) {\n      return;\n    }\n    final text = await _getDocumentContentFromTopToPosition(position);\n\n    if (text.isEmpty) {\n      final stateCopy = state;\n      emit(DocumentContentEmptyAiWriterState(command, onConfirm: exit));\n      emit(stateCopy);\n      return;\n    }\n\n    final stream = await _aiService.streamCompletion(\n      objectId: documentId,\n      text: text,\n      completionType: command.toCompletionType(),\n      history: records,\n      sourceIds: selectedSourcesNotifier.value,\n      format: predefinedFormat,\n      promptId: promptId,\n      onStart: () async {\n        final position = await ensurePreviousNodeIsEmptyParagraph(\n          editorState,\n          aiWriterNode!,\n        );\n        _textRobot.start(position: position);\n        records.add(\n          AiWriterRecord.user(\n            content: text,\n            format: predefinedFormat,\n          ),\n        );\n      },\n      processMessage: (text) async {\n        await _textRobot.appendMarkdownText(\n          text,\n          updateSelection: false,\n          attributes: ApplySuggestionFormatType.replace.attributes,\n        );\n        onAppendToDocument?.call();\n      },\n      processAssistMessage: (text) async {\n        if (state case final GeneratingAiWriterState generatingState) {\n          emit(\n            GeneratingAiWriterState(\n              command,\n              taskId: generatingState.taskId,\n              markdownText: generatingState.markdownText + text,\n            ),\n          );\n        }\n      },\n      onEnd: () async {\n        if (state case final GeneratingAiWriterState generatingState) {\n          await _textRobot.stop(\n            attributes: ApplySuggestionFormatType.replace.attributes,\n          );\n          emit(\n            ReadyAiWriterState(\n              command,\n              isFirstRun: false,\n              markdownText: generatingState.markdownText,\n            ),\n          );\n        }\n        records.add(\n          AiWriterRecord.ai(content: _textRobot.markdownText),\n        );\n      },\n      onError: (error) async {\n        emit(ErrorAiWriterState(command, error: error));\n        records.add(\n          AiWriterRecord.ai(content: _textRobot.markdownText),\n        );\n      },\n      onLocalAIStreamingStateChange: (state) {\n        emit(LocalAIStreamingAiWriterState(command, state: state));\n      },\n    );\n    if (stream != null) {\n      emit(\n        GeneratingAiWriterState(command, taskId: stream.$1),\n      );\n    }\n  }\n\n  Future<void> _startSuggestingEdits(\n    AiWriterCommand command,\n    String prompt,\n    PredefinedFormat? predefinedFormat,\n    String? promptId,\n  ) async {\n    final selection = aiWriterNode?.aiWriterSelection;\n    if (selection == null) {\n      return;\n    }\n    if (prompt.isEmpty) {\n      prompt = records.removeAt(0).content;\n    }\n\n    final stream = await _aiService.streamCompletion(\n      objectId: documentId,\n      text: prompt,\n      format: predefinedFormat,\n      promptId: promptId,\n      completionType: command.toCompletionType(),\n      history: records,\n      sourceIds: selectedSourcesNotifier.value,\n      onStart: () async {\n        await formatSelection(\n          editorState,\n          selection,\n          ApplySuggestionFormatType.original,\n        );\n        final position = await ensurePreviousNodeIsEmptyParagraph(\n          editorState,\n          aiWriterNode!,\n        );\n        _textRobot.start(position: position, previousSelection: selection);\n        records.add(\n          AiWriterRecord.user(\n            content: prompt,\n            format: predefinedFormat,\n          ),\n        );\n      },\n      processMessage: (text) async {\n        await _textRobot.appendMarkdownText(\n          text,\n          updateSelection: false,\n          attributes: ApplySuggestionFormatType.replace.attributes,\n        );\n        onAppendToDocument?.call();\n\n        _aiWriterCubitLog(\n          'received message: $text',\n        );\n      },\n      processAssistMessage: (text) async {\n        if (state case final GeneratingAiWriterState generatingState) {\n          emit(\n            GeneratingAiWriterState(\n              command,\n              taskId: generatingState.taskId,\n              markdownText: generatingState.markdownText + text,\n            ),\n          );\n        }\n\n        _aiWriterCubitLog(\n          'received assist message: $text',\n        );\n      },\n      onEnd: () async {\n        if (state case final GeneratingAiWriterState generatingState) {\n          await _textRobot.stop(\n            attributes: ApplySuggestionFormatType.replace.attributes,\n          );\n          emit(\n            ReadyAiWriterState(\n              command,\n              isFirstRun: false,\n              markdownText: generatingState.markdownText,\n            ),\n          );\n          records.add(\n            AiWriterRecord.ai(content: _textRobot.markdownText),\n          );\n\n          _aiWriterCubitLog(\n            'returned response: ${_textRobot.markdownText}',\n          );\n        }\n      },\n      onError: (error) async {\n        emit(ErrorAiWriterState(command, error: error));\n        records.add(\n          AiWriterRecord.ai(content: _textRobot.markdownText),\n        );\n      },\n      onLocalAIStreamingStateChange: (state) {\n        emit(LocalAIStreamingAiWriterState(command, state: state));\n      },\n    );\n    if (stream != null) {\n      emit(\n        GeneratingAiWriterState(command, taskId: stream.$1),\n      );\n    }\n  }\n\n  Future<void> _startInforming(\n    AiWriterCommand command,\n    String prompt,\n    PredefinedFormat? predefinedFormat,\n    String? promptId,\n  ) async {\n    final selection = aiWriterNode?.aiWriterSelection;\n    if (selection == null) {\n      return;\n    }\n    if (prompt.isEmpty) {\n      prompt = records.removeAt(0).content;\n    }\n\n    final stream = await _aiService.streamCompletion(\n      objectId: documentId,\n      text: prompt,\n      completionType: command.toCompletionType(),\n      history: records,\n      sourceIds: selectedSourcesNotifier.value,\n      format: predefinedFormat,\n      promptId: promptId,\n      onStart: () async {\n        records.add(\n          AiWriterRecord.user(\n            content: prompt,\n            format: predefinedFormat,\n          ),\n        );\n      },\n      processMessage: (text) async {\n        if (state case final GeneratingAiWriterState generatingState) {\n          emit(\n            GeneratingAiWriterState(\n              command,\n              taskId: generatingState.taskId,\n              markdownText: generatingState.markdownText + text,\n            ),\n          );\n        }\n      },\n      processAssistMessage: (_) async {},\n      onEnd: () async {\n        if (state case final GeneratingAiWriterState generatingState) {\n          emit(\n            ReadyAiWriterState(\n              command,\n              isFirstRun: false,\n              markdownText: generatingState.markdownText,\n            ),\n          );\n          records.add(\n            AiWriterRecord.ai(content: generatingState.markdownText),\n          );\n        }\n      },\n      onError: (error) async {\n        if (state case final GeneratingAiWriterState generatingState) {\n          records.add(\n            AiWriterRecord.ai(content: generatingState.markdownText),\n          );\n        }\n        emit(ErrorAiWriterState(command, error: error));\n      },\n      onLocalAIStreamingStateChange: (state) {\n        emit(LocalAIStreamingAiWriterState(command, state: state));\n      },\n    );\n    if (stream != null) {\n      emit(\n        GeneratingAiWriterState(command, taskId: stream.$1),\n      );\n    }\n  }\n\n  void _aiWriterCubitLog(String message) {\n    if (_aiWriterCubitDebugLog) {\n      Log.debug('[AiWriterCubit] $message');\n    }\n  }\n}\n\nmixin RegisteredAiWriter {\n  AiWriterCommand get command;\n}\n\nsealed class AiWriterState {\n  const AiWriterState();\n}\n\nclass IdleAiWriterState extends AiWriterState {\n  const IdleAiWriterState();\n}\n\nclass ReadyAiWriterState extends AiWriterState with RegisteredAiWriter {\n  const ReadyAiWriterState(\n    this.command, {\n    required this.isFirstRun,\n    this.markdownText = '',\n  });\n\n  @override\n  final AiWriterCommand command;\n\n  final bool isFirstRun;\n  final String markdownText;\n}\n\nclass GeneratingAiWriterState extends AiWriterState with RegisteredAiWriter {\n  const GeneratingAiWriterState(\n    this.command, {\n    required this.taskId,\n    this.progress = '',\n    this.markdownText = '',\n  });\n\n  @override\n  final AiWriterCommand command;\n\n  final String taskId;\n  final String progress;\n  final String markdownText;\n}\n\nclass ErrorAiWriterState extends AiWriterState with RegisteredAiWriter {\n  const ErrorAiWriterState(\n    this.command, {\n    required this.error,\n  });\n\n  @override\n  final AiWriterCommand command;\n\n  final AIError error;\n}\n\nclass DocumentContentEmptyAiWriterState extends AiWriterState\n    with RegisteredAiWriter {\n  const DocumentContentEmptyAiWriterState(\n    this.command, {\n    required this.onConfirm,\n  });\n\n  @override\n  final AiWriterCommand command;\n\n  final void Function() onConfirm;\n}\n\nclass LocalAIStreamingAiWriterState extends AiWriterState\n    with RegisteredAiWriter {\n  const LocalAIStreamingAiWriterState(\n    this.command, {\n    required this.state,\n  });\n\n  @override\n  final AiWriterCommand command;\n\n  final LocalAIStreamingState state;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:equatable/equatable.dart';\nimport 'package:flutter/material.dart';\n\nimport '../ai_writer_block_component.dart';\n\nconst kdefaultReplacementType = AskAIReplacementType.markdown;\n\nenum AskAIReplacementType {\n  markdown,\n  plainText,\n}\n\nenum SuggestionAction {\n  accept,\n  discard,\n  close,\n  tryAgain,\n  rewrite,\n  keep,\n  insertBelow;\n\n  String get i18n => switch (this) {\n        accept => LocaleKeys.suggestion_accept.tr(),\n        discard => LocaleKeys.suggestion_discard.tr(),\n        close => LocaleKeys.suggestion_close.tr(),\n        tryAgain => LocaleKeys.suggestion_tryAgain.tr(),\n        rewrite => LocaleKeys.suggestion_rewrite.tr(),\n        keep => LocaleKeys.suggestion_keep.tr(),\n        insertBelow => LocaleKeys.suggestion_insertBelow.tr(),\n      };\n\n  FlowySvg buildIcon(BuildContext context) {\n    final icon = switch (this) {\n      accept || keep => FlowySvgs.ai_fix_spelling_grammar_s,\n      discard || close => FlowySvgs.toast_close_s,\n      tryAgain || rewrite => FlowySvgs.ai_try_again_s,\n      insertBelow => FlowySvgs.suggestion_insert_below_s,\n    };\n\n    return FlowySvg(\n      icon,\n      size: Size.square(16.0),\n      color: switch (this) {\n        accept || keep => Color(0xFF278E42),\n        discard || close => Color(0xFFC40055),\n        _ => Theme.of(context).iconTheme.color,\n      },\n    );\n  }\n}\n\nenum AiWriterCommand {\n  userQuestion,\n  explain,\n  // summarize,\n  continueWriting,\n  fixSpellingAndGrammar,\n  improveWriting,\n  makeShorter,\n  makeLonger;\n\n  String defaultPrompt(String input) => switch (this) {\n        userQuestion => input,\n        explain => \"Explain this phrase in a concise manner:\\n\\n$input\",\n        // summarize => '$input\\n\\nTl;dr',\n        continueWriting =>\n          'Continue writing based on this existing text:\\n\\n$input',\n        fixSpellingAndGrammar => 'Correct this to standard English:\\n\\n$input',\n        improveWriting => 'Rewrite this in your own words:\\n\\n$input',\n        makeShorter => 'Make this text shorter:\\n\\n$input',\n        makeLonger => 'Make this text longer:\\n\\n$input',\n      };\n\n  String get i18n => switch (this) {\n        userQuestion => LocaleKeys.document_plugins_aiWriter_userQuestion.tr(),\n        explain => LocaleKeys.document_plugins_aiWriter_explain.tr(),\n        // summarize => LocaleKeys.document_plugins_aiWriter_summarize.tr(),\n        continueWriting =>\n          LocaleKeys.document_plugins_aiWriter_continueWriting.tr(),\n        fixSpellingAndGrammar =>\n          LocaleKeys.document_plugins_aiWriter_fixSpelling.tr(),\n        improveWriting =>\n          LocaleKeys.document_plugins_smartEditImproveWriting.tr(),\n        makeShorter => LocaleKeys.document_plugins_aiWriter_makeShorter.tr(),\n        makeLonger => LocaleKeys.document_plugins_aiWriter_makeLonger.tr(),\n      };\n\n  FlowySvgData get icon => switch (this) {\n        userQuestion => FlowySvgs.toolbar_ai_ask_anything_m,\n        explain => FlowySvgs.toolbar_ai_explain_m,\n        // summarize => FlowySvgs.ai_summarize_s,\n        continueWriting ||\n        improveWriting =>\n          FlowySvgs.toolbar_ai_improve_writing_m,\n        fixSpellingAndGrammar => FlowySvgs.toolbar_ai_fix_spelling_grammar_m,\n        makeShorter => FlowySvgs.toolbar_ai_make_shorter_m,\n        makeLonger => FlowySvgs.toolbar_ai_make_longer_m,\n      };\n\n  CompletionTypePB toCompletionType() => switch (this) {\n        userQuestion => CompletionTypePB.UserQuestion,\n        explain => CompletionTypePB.ExplainSelected,\n        // summarize => CompletionTypePB.Summarize,\n        continueWriting => CompletionTypePB.ContinueWriting,\n        fixSpellingAndGrammar => CompletionTypePB.SpellingAndGrammar,\n        improveWriting => CompletionTypePB.ImproveWriting,\n        makeShorter => CompletionTypePB.MakeShorter,\n        makeLonger => CompletionTypePB.MakeLonger,\n      };\n}\n\nenum ApplySuggestionFormatType {\n  original(AiWriterBlockKeys.suggestionOriginal),\n  replace(AiWriterBlockKeys.suggestionReplacement),\n  clear(null);\n\n  const ApplySuggestionFormatType(this.value);\n  final String? value;\n\n  Map<String, dynamic> get attributes => {AiWriterBlockKeys.suggestion: value};\n}\n\nenum AiRole {\n  user,\n  system,\n  ai,\n}\n\nclass AiWriterRecord extends Equatable {\n  const AiWriterRecord.user({\n    required this.content,\n    required this.format,\n  }) : role = AiRole.user;\n\n  const AiWriterRecord.ai({\n    required this.content,\n  })  : role = AiRole.ai,\n        format = null;\n\n  final AiRole role;\n  final String content;\n  final PredefinedFormat? format;\n\n  @override\n  List<Object?> get props => [role, content, format];\n\n  CompletionRecordPB toPB() {\n    return CompletionRecordPB(\n      content: content,\n      role: switch (role) {\n        AiRole.user => ChatMessageTypePB.User,\n        AiRole.system || AiRole.ai => ChatMessageTypePB.System,\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart';\nimport 'package:appflowy/shared/markdown_to_document.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nimport '../ai_writer_block_component.dart';\nimport 'ai_writer_entities.dart';\n\nextension AiWriterExtension on Node {\n  bool get isAiWriterInitialized {\n    return attributes[AiWriterBlockKeys.isInitialized];\n  }\n\n  Selection? get aiWriterSelection {\n    final selection = attributes[AiWriterBlockKeys.selection];\n    if (selection == null) {\n      return null;\n    }\n    return Selection.fromJson(selection);\n  }\n\n  AiWriterCommand get aiWriterCommand {\n    final index = attributes[AiWriterBlockKeys.command];\n    return AiWriterCommand.values[index];\n  }\n}\n\nextension AiWriterNodeExtension on EditorState {\n  Future<String> getMarkdownInSelection(Selection? selection) async {\n    selection ??= this.selection?.normalized;\n    if (selection == null || selection.isCollapsed) {\n      return '';\n    }\n\n    // if the selected nodes are not entirely selected, slice the nodes\n    final slicedNodes = <Node>[];\n    final List<Node> flattenNodes = getNodesInSelection(selection);\n    final List<Node> nodes = [];\n\n    for (final node in flattenNodes) {\n      if (nodes.any((element) => element.isParentOf(node))) {\n        continue;\n      }\n      nodes.add(node);\n    }\n\n    for (final node in nodes) {\n      final delta = node.delta;\n      if (delta == null) {\n        continue;\n      }\n\n      final slicedDelta = delta.slice(\n        node == nodes.first ? selection.startIndex : 0,\n        node == nodes.last ? selection.endIndex : delta.length,\n      );\n\n      final copiedNode = node.copyWith(\n        attributes: {\n          ...node.attributes,\n          blockComponentDelta: slicedDelta.toJson(),\n        },\n      );\n\n      slicedNodes.add(copiedNode);\n    }\n\n    for (final (i, node) in slicedNodes.indexed) {\n      final childNodesShouldBeDeleted = <Node>[];\n      for (final child in node.children) {\n        if (!child.path.inSelection(selection)) {\n          childNodesShouldBeDeleted.add(child);\n        }\n      }\n      for (final child in childNodesShouldBeDeleted) {\n        slicedNodes[i] = node.copyWith(\n          children: node.children.where((e) => e.id != child.id).toList(),\n          type: selection.startIndex != 0 ? ParagraphBlockKeys.type : node.type,\n        );\n      }\n    }\n\n    // use \\n\\n as line break to improve the ai response\n    // using \\n will cause the ai response treat the text as a single line\n    final markdown = await customDocumentToMarkdown(\n      Document.blank()..insert([0], slicedNodes),\n      lineBreak: '\\n',\n    );\n\n    // trim the last \\n if it exists\n    return markdown.trimRight();\n  }\n\n  List<String> getPlainTextInSelection(Selection? selection) {\n    selection ??= this.selection?.normalized;\n    if (selection == null || selection.isCollapsed) {\n      return [];\n    }\n\n    final res = <String>[];\n    if (selection.isCollapsed) {\n      return res;\n    }\n\n    final nodes = getNodesInSelection(selection);\n\n    for (final node in nodes) {\n      final delta = node.delta;\n      if (delta == null) {\n        continue;\n      }\n      final startIndex = node == nodes.first ? selection.startIndex : 0;\n      final endIndex = node == nodes.last ? selection.endIndex : delta.length;\n      res.add(delta.slice(startIndex, endIndex).toPlainText());\n    }\n\n    return res;\n  }\n\n  /// Determines whether the document is empty up to the selection\n  ///\n  /// If empty and the title is also empty, the continue writing option will be disabled.\n  bool isEmptyForContinueWriting({\n    Selection? selection,\n  }) {\n    if (selection != null && !selection.isCollapsed) {\n      return false;\n    }\n\n    final effectiveSelection = Selection(\n      start: Position(path: [0]),\n      end: selection?.normalized.end ??\n          this.selection?.normalized.end ??\n          Position(path: getLastSelectable()?.$1.path ?? [0]),\n    );\n\n    // if the selected nodes are not entirely selected, slice the nodes\n    final slicedNodes = <Node>[];\n    final nodes = getNodesInSelection(effectiveSelection);\n\n    for (final node in nodes) {\n      final delta = node.delta;\n      if (delta == null) {\n        continue;\n      }\n\n      final slicedDelta = delta.slice(\n        node == nodes.first ? effectiveSelection.startIndex : 0,\n        node == nodes.last ? effectiveSelection.endIndex : delta.length,\n      );\n\n      final copiedNode = node.copyWith(\n        attributes: {\n          ...node.attributes,\n          blockComponentDelta: slicedDelta.toJson(),\n        },\n      );\n\n      slicedNodes.add(copiedNode);\n    }\n\n    // using less custom parsers to avoid futures\n    final markdown = documentToMarkdown(\n      Document.blank()..insert([0], slicedNodes),\n      customParsers: [\n        const MathEquationNodeParser(),\n        const CalloutNodeParser(),\n        const ToggleListNodeParser(),\n        const CustomParagraphNodeParser(),\n        const SubPageNodeParser(),\n        const SimpleTableNodeParser(),\n        const LinkPreviewNodeParser(),\n        const FileBlockNodeParser(),\n      ],\n    );\n\n    return markdown.trim().isEmpty;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart",
    "content": "import 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\n\nclass AiWriterGestureDetector extends StatelessWidget {\n  const AiWriterGestureDetector({\n    super.key,\n    required this.behavior,\n    required this.onPointerEvent,\n    this.child,\n  });\n\n  final HitTestBehavior behavior;\n  final void Function() onPointerEvent;\n  final Widget? child;\n\n  @override\n  Widget build(BuildContext context) {\n    return RawGestureDetector(\n      behavior: behavior,\n      gestures: <Type, GestureRecognizerFactory>{\n        TapGestureRecognizer:\n            GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(\n          () => TapGestureRecognizer(),\n          (instance) => instance\n            ..onTapDown = ((_) => onPointerEvent())\n            ..onSecondaryTapDown = ((_) => onPointerEvent())\n            ..onTertiaryTapDown = ((_) => onPointerEvent()),\n        ),\n        ImmediateMultiDragGestureRecognizer:\n            GestureRecognizerFactoryWithHandlers<\n                ImmediateMultiDragGestureRecognizer>(\n          () => ImmediateMultiDragGestureRecognizer(),\n          (instance) => instance.onStart = (offset) => null,\n        ),\n      },\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/colorscheme/default_colorscheme.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\n\nimport '../operations/ai_writer_entities.dart';\n\nclass AiWriterPromptMoreButton extends StatelessWidget {\n  const AiWriterPromptMoreButton({\n    super.key,\n    required this.isEnabled,\n    required this.isSelected,\n    required this.onTap,\n  });\n\n  final bool isEnabled;\n  final bool isSelected;\n  final void Function() onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return IgnorePointer(\n      ignoring: !isEnabled,\n      child: GestureDetector(\n        onTap: onTap,\n        behavior: HitTestBehavior.opaque,\n        child: SizedBox(\n          height: DesktopAIPromptSizes.actionBarButtonSize,\n          child: FlowyHover(\n            style: const HoverStyle(\n              borderRadius: BorderRadius.all(Radius.circular(8)),\n            ),\n            isSelected: () => isSelected,\n            child: Padding(\n              padding: const EdgeInsetsDirectional.fromSTEB(6, 6, 4, 6),\n              child: Row(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  FlowyText(\n                    LocaleKeys.ai_more.tr(),\n                    fontSize: 12,\n                    figmaLineHeight: 16,\n                    color: isEnabled\n                        ? Theme.of(context).hintColor\n                        : Theme.of(context).disabledColor,\n                  ),\n                  const HSpace(2.0),\n                  FlowySvg(\n                    FlowySvgs.ai_source_drop_down_s,\n                    color: Theme.of(context).hintColor,\n                    size: const Size.square(8),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass MoreAiWriterCommands extends StatelessWidget {\n  const MoreAiWriterCommands({\n    super.key,\n    required this.hasSelection,\n    required this.editorState,\n    required this.onSelectCommand,\n  });\n\n  final EditorState editorState;\n  final bool hasSelection;\n  final void Function(AiWriterCommand) onSelectCommand;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      // add one here to take into account the border of the main message box.\n      // It is configured to be on the outside to hide some graphical\n      // artifacts.\n      margin: EdgeInsets.only(top: 4.0 + 1.0),\n      padding: EdgeInsets.all(8.0),\n      constraints: BoxConstraints(minWidth: 240.0),\n      decoration: BoxDecoration(\n        color: Theme.of(context).colorScheme.surface,\n        border: Border.all(\n          color: Theme.of(context).brightness == Brightness.light\n              ? ColorSchemeConstants.lightBorderColor\n              : ColorSchemeConstants.darkBorderColor,\n          strokeAlign: BorderSide.strokeAlignOutside,\n        ),\n        borderRadius: BorderRadius.all(Radius.circular(8.0)),\n        boxShadow: Theme.of(context).isLightMode\n            ? ShadowConstants.lightSmall\n            : ShadowConstants.darkSmall,\n      ),\n      child: IntrinsicWidth(\n        child: Column(\n          spacing: 4.0,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: _getCommands(\n            hasSelection: hasSelection,\n          ),\n        ),\n      ),\n    );\n  }\n\n  List<Widget> _getCommands({required bool hasSelection}) {\n    if (hasSelection) {\n      return [\n        _bottomButton(AiWriterCommand.improveWriting),\n        _bottomButton(AiWriterCommand.fixSpellingAndGrammar),\n        _bottomButton(AiWriterCommand.explain),\n        const Divider(height: 1.0, thickness: 1.0),\n        _bottomButton(AiWriterCommand.makeLonger),\n        _bottomButton(AiWriterCommand.makeShorter),\n      ];\n    } else {\n      return [\n        _bottomButton(AiWriterCommand.continueWriting),\n      ];\n    }\n  }\n\n  Widget _bottomButton(AiWriterCommand command) {\n    return Builder(\n      builder: (context) {\n        return FlowyButton(\n          leftIcon: FlowySvg(\n            command.icon,\n            color: Theme.of(context).iconTheme.color,\n          ),\n          leftIconSize: const Size.square(20),\n          margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),\n          text: FlowyText(\n            command.i18n,\n            figmaLineHeight: 20,\n          ),\n          onTap: () => onSelectCommand(command),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/util/throttle.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../operations/ai_writer_cubit.dart';\nimport 'ai_writer_gesture_detector.dart';\n\nclass AiWriterScrollWrapper extends StatefulWidget {\n  const AiWriterScrollWrapper({\n    super.key,\n    required this.viewId,\n    required this.editorState,\n    required this.child,\n  });\n\n  final String viewId;\n  final EditorState editorState;\n  final Widget child;\n\n  @override\n  State<AiWriterScrollWrapper> createState() => _AiWriterScrollWrapperState();\n}\n\nclass _AiWriterScrollWrapperState extends State<AiWriterScrollWrapper> {\n  final overlayController = OverlayPortalController();\n  late final throttler = Throttler();\n  late final aiWriterCubit = AiWriterCubit(\n    documentId: widget.viewId,\n    editorState: widget.editorState,\n    onCreateNode: () {\n      aiWriterRegistered = true;\n      widget.editorState.service.keyboardService?.disableShortcuts();\n    },\n    onRemoveNode: () {\n      aiWriterRegistered = false;\n      widget.editorState.service.keyboardService?.enableShortcuts();\n      widget.editorState.service.keyboardService?.enable();\n    },\n    onAppendToDocument: onAppendToDocument,\n  );\n\n  bool userHasScrolled = false;\n  bool aiWriterRegistered = false;\n  bool dialogShown = false;\n\n  @override\n  void initState() {\n    super.initState();\n    overlayController.show();\n  }\n\n  @override\n  void dispose() {\n    aiWriterCubit.close();\n    throttler.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: aiWriterCubit,\n      child: NotificationListener<ScrollNotification>(\n        onNotification: handleScrollNotification,\n        child: Focus(\n          autofocus: true,\n          onKeyEvent: handleKeyEvent,\n          child: MultiBlocListener(\n            listeners: [\n              BlocListener<AiWriterCubit, AiWriterState>(\n                listener: (context, state) {\n                  if (state is DocumentContentEmptyAiWriterState) {\n                    showConfirmDialog(\n                      context: context,\n                      title:\n                          LocaleKeys.ai_continueWritingEmptyDocumentTitle.tr(),\n                      description: LocaleKeys\n                          .ai_continueWritingEmptyDocumentDescription\n                          .tr(),\n                      onConfirm: (_) => state.onConfirm(),\n                    );\n                  }\n                },\n              ),\n              BlocListener<AiWriterCubit, AiWriterState>(\n                listenWhen: (previous, current) =>\n                    previous is GeneratingAiWriterState &&\n                    current is ReadyAiWriterState,\n                listener: (context, state) {\n                  widget.editorState.updateSelectionWithReason(null);\n                },\n              ),\n            ],\n            child: OverlayPortal(\n              controller: overlayController,\n              overlayChildBuilder: (context) {\n                return BlocBuilder<AiWriterCubit, AiWriterState>(\n                  builder: (context, state) {\n                    return AiWriterGestureDetector(\n                      behavior: state is RegisteredAiWriter\n                          ? HitTestBehavior.translucent\n                          : HitTestBehavior.deferToChild,\n                      onPointerEvent: () => onTapOutside(context),\n                    );\n                  },\n                );\n              },\n              child: widget.child,\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  bool handleScrollNotification(ScrollNotification notification) {\n    if (!aiWriterRegistered) {\n      return false;\n    }\n\n    if (notification is UserScrollNotification) {\n      debounceResetUserHasScrolled();\n      userHasScrolled = true;\n      throttler.cancel();\n    }\n\n    return false;\n  }\n\n  void debounceResetUserHasScrolled() {\n    Debounce.debounce(\n      'user_has_scrolled',\n      const Duration(seconds: 3),\n      () => userHasScrolled = false,\n    );\n  }\n\n  void onTapOutside(BuildContext context) {\n    final aiWriterCubit = context.read<AiWriterCubit>();\n\n    if (aiWriterCubit.hasUnusedResponse()) {\n      showConfirmDialog(\n        context: context,\n        title: LocaleKeys.button_discard.tr(),\n        description: LocaleKeys.document_plugins_discardResponse.tr(),\n        confirmLabel: LocaleKeys.button_discard.tr(),\n        style: ConfirmPopupStyle.cancelAndOk,\n        onConfirm: (_) => stopAndExit(),\n        onCancel: () {},\n      );\n    } else {\n      stopAndExit();\n    }\n  }\n\n  KeyEventResult handleKeyEvent(FocusNode node, KeyEvent event) {\n    if (!aiWriterRegistered) {\n      return KeyEventResult.ignored;\n    }\n    if (dialogShown) {\n      return KeyEventResult.handled;\n    }\n    if (event is! KeyDownEvent) {\n      return KeyEventResult.ignored;\n    }\n\n    switch (event.logicalKey) {\n      case LogicalKeyboardKey.escape:\n        if (aiWriterCubit.state case GeneratingAiWriterState _) {\n          aiWriterCubit.stopStream();\n        } else if (aiWriterCubit.hasUnusedResponse()) {\n          dialogShown = true;\n          showConfirmDialog(\n            context: context,\n            title: LocaleKeys.button_discard.tr(),\n            description: LocaleKeys.document_plugins_discardResponse.tr(),\n            confirmLabel: LocaleKeys.button_discard.tr(),\n            style: ConfirmPopupStyle.cancelAndOk,\n            onConfirm: (_) => stopAndExit(),\n            onCancel: () {},\n          ).then((_) => dialogShown = false);\n        } else {\n          stopAndExit();\n        }\n        return KeyEventResult.handled;\n      case LogicalKeyboardKey.keyC\n          when HardwareKeyboard.instance.isControlPressed:\n        if (aiWriterCubit.state case GeneratingAiWriterState _) {\n          aiWriterCubit.stopStream();\n        }\n        return KeyEventResult.handled;\n      default:\n        break;\n    }\n\n    return KeyEventResult.ignored;\n  }\n\n  void onAppendToDocument() {\n    if (!aiWriterRegistered || userHasScrolled) {\n      return;\n    }\n\n    throttler.call(() {\n      if (aiWriterCubit.aiWriterNode != null) {\n        final path = aiWriterCubit.aiWriterNode!.path;\n\n        if (path.isEmpty) {\n          return;\n        }\n\n        if (path.previous.isNotEmpty) {\n          final node = widget.editorState.getNodeAtPath(path.previous);\n          if (node != null && node.delta != null && node.delta!.isNotEmpty) {\n            widget.editorState.updateSelectionWithReason(\n              Selection.collapsed(\n                Position(path: path, offset: node.delta!.length),\n              ),\n            );\n            return;\n          }\n        }\n\n        widget.editorState.updateSelectionWithReason(\n          Selection.collapsed(Position(path: path)),\n        );\n      }\n    });\n  }\n\n  void stopAndExit() {\n    Future(() async {\n      await aiWriterCubit.stopStream();\n      await aiWriterCubit.exit();\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_suggestion_actions.dart",
    "content": "import 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport '../operations/ai_writer_entities.dart';\n\nclass SuggestionActionBar extends StatelessWidget {\n  const SuggestionActionBar({\n    super.key,\n    required this.currentCommand,\n    required this.hasSelection,\n    required this.onTap,\n  });\n\n  final AiWriterCommand currentCommand;\n  final bool hasSelection;\n  final void Function(SuggestionAction) onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return SeparatedRow(\n      mainAxisSize: MainAxisSize.min,\n      separatorBuilder: () => const HSpace(4.0),\n      children: _getSuggestedActions()\n          .map(\n            (action) => SuggestionActionButton(\n              action: action,\n              onTap: () => onTap(action),\n            ),\n          )\n          .toList(),\n    );\n  }\n\n  List<SuggestionAction> _getSuggestedActions() {\n    if (hasSelection) {\n      return switch (currentCommand) {\n        AiWriterCommand.userQuestion || AiWriterCommand.continueWriting => [\n            SuggestionAction.keep,\n            SuggestionAction.discard,\n            SuggestionAction.rewrite,\n          ],\n        AiWriterCommand.explain => [\n            SuggestionAction.insertBelow,\n            SuggestionAction.tryAgain,\n            SuggestionAction.close,\n          ],\n        AiWriterCommand.fixSpellingAndGrammar ||\n        AiWriterCommand.improveWriting ||\n        AiWriterCommand.makeShorter ||\n        AiWriterCommand.makeLonger =>\n          [\n            SuggestionAction.accept,\n            SuggestionAction.discard,\n            SuggestionAction.insertBelow,\n            SuggestionAction.rewrite,\n          ],\n      };\n    } else {\n      return switch (currentCommand) {\n        AiWriterCommand.userQuestion || AiWriterCommand.continueWriting => [\n            SuggestionAction.keep,\n            SuggestionAction.discard,\n            SuggestionAction.rewrite,\n          ],\n        AiWriterCommand.explain => [\n            SuggestionAction.insertBelow,\n            SuggestionAction.tryAgain,\n            SuggestionAction.close,\n          ],\n        _ => [\n            SuggestionAction.keep,\n            SuggestionAction.discard,\n            SuggestionAction.rewrite,\n          ],\n      };\n    }\n  }\n}\n\nclass SuggestionActionButton extends StatelessWidget {\n  const SuggestionActionButton({\n    super.key,\n    required this.action,\n    required this.onTap,\n  });\n\n  final SuggestionAction action;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 28,\n      child: FlowyButton(\n        text: FlowyText(\n          action.i18n,\n          figmaLineHeight: 20,\n        ),\n        leftIcon: action.buildIcon(context),\n        iconPadding: 4.0,\n        margin: const EdgeInsets.symmetric(\n          horizontal: 6.0,\n          vertical: 4.0,\n        ),\n        onTap: onTap,\n        useIntrinsicWidth: true,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nconst String leftAlignmentKey = 'left';\nconst String centerAlignmentKey = 'center';\nconst String rightAlignmentKey = 'right';\nconst String kAlignToolbarItemId = 'editor.align';\n\nfinal alignToolbarItem = ToolbarItem(\n  id: kAlignToolbarItemId,\n  group: 4,\n  isActive: onlyShowInTextType,\n  builder: (context, editorState, highlightColor, _, tooltipBuilder) {\n    final selection = editorState.selection!;\n    final nodes = editorState.getNodesInSelection(selection);\n\n    bool isSatisfyCondition(bool Function(Object? value) test) {\n      return nodes.every(\n        (n) => test(n.attributes[blockComponentAlign]),\n      );\n    }\n\n    bool isHighlight = false;\n    FlowySvgData data = FlowySvgs.toolbar_align_left_s;\n    if (isSatisfyCondition((value) => value == leftAlignmentKey)) {\n      isHighlight = true;\n      data = FlowySvgs.toolbar_align_left_s;\n    } else if (isSatisfyCondition((value) => value == centerAlignmentKey)) {\n      isHighlight = true;\n      data = FlowySvgs.toolbar_align_center_s;\n    } else if (isSatisfyCondition((value) => value == rightAlignmentKey)) {\n      isHighlight = true;\n      data = FlowySvgs.toolbar_align_right_s;\n    }\n\n    Widget child = FlowySvg(\n      data,\n      size: const Size.square(16),\n      color: isHighlight ? highlightColor : Colors.white,\n    );\n\n    child = _AlignmentButtons(\n      child: child,\n      onAlignChanged: (align) async {\n        await editorState.updateNode(\n          selection,\n          (node) => node.copyWith(\n            attributes: {\n              ...node.attributes,\n              blockComponentAlign: align,\n            },\n          ),\n        );\n      },\n    );\n\n    if (tooltipBuilder != null) {\n      child = tooltipBuilder(\n        context,\n        kAlignToolbarItemId,\n        LocaleKeys.document_plugins_optionAction_align.tr(),\n        child,\n      );\n    }\n\n    return child;\n  },\n);\n\nclass _AlignmentButtons extends StatefulWidget {\n  const _AlignmentButtons({\n    required this.child,\n    required this.onAlignChanged,\n  });\n\n  final Widget child;\n  final Function(String align) onAlignChanged;\n\n  @override\n  State<_AlignmentButtons> createState() => _AlignmentButtonsState();\n}\n\nclass _AlignmentButtonsState extends State<_AlignmentButtons> {\n  final controller = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      windowPadding: const EdgeInsets.all(0),\n      margin: const EdgeInsets.symmetric(vertical: 2.0),\n      direction: PopoverDirection.bottomWithCenterAligned,\n      offset: const Offset(0, 10),\n      decorationColor: Theme.of(context).colorScheme.onTertiary,\n      borderRadius: BorderRadius.circular(6.0),\n      popupBuilder: (_) {\n        keepEditorFocusNotifier.increase();\n        return _AlignButtons(onAlignChanged: widget.onAlignChanged);\n      },\n      onClose: () {\n        keepEditorFocusNotifier.decrease();\n      },\n      child: FlowyButton(\n        useIntrinsicWidth: true,\n        text: widget.child,\n        hoverColor: Colors.grey.withValues(alpha: 0.3),\n        onTap: () => controller.show(),\n      ),\n    );\n  }\n}\n\nclass _AlignButtons extends StatelessWidget {\n  const _AlignButtons({\n    required this.onAlignChanged,\n  });\n\n  final Function(String align) onAlignChanged;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 28,\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          const HSpace(4),\n          _AlignButton(\n            icon: FlowySvgs.toolbar_align_left_s,\n            tooltips: LocaleKeys.document_plugins_optionAction_left.tr(),\n            onTap: () => onAlignChanged(leftAlignmentKey),\n          ),\n          const _Divider(),\n          _AlignButton(\n            icon: FlowySvgs.toolbar_align_center_s,\n            tooltips: LocaleKeys.document_plugins_optionAction_center.tr(),\n            onTap: () => onAlignChanged(centerAlignmentKey),\n          ),\n          const _Divider(),\n          _AlignButton(\n            icon: FlowySvgs.toolbar_align_right_s,\n            tooltips: LocaleKeys.document_plugins_optionAction_right.tr(),\n            onTap: () => onAlignChanged(rightAlignmentKey),\n          ),\n          const HSpace(4),\n        ],\n      ),\n    );\n  }\n}\n\nclass _AlignButton extends StatelessWidget {\n  const _AlignButton({\n    required this.icon,\n    required this.tooltips,\n    required this.onTap,\n  });\n\n  final FlowySvgData icon;\n  final String tooltips;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      useIntrinsicWidth: true,\n      hoverColor: Colors.grey.withValues(alpha: 0.3),\n      onTap: onTap,\n      text: FlowyTooltip(\n        message: tooltips,\n        child: FlowySvg(\n          icon,\n          size: const Size.square(16),\n          color: Colors.white,\n        ),\n      ),\n    );\n  }\n}\n\nclass _Divider extends StatelessWidget {\n  const _Divider();\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(4),\n      child: Container(\n        width: 1,\n        color: Colors.grey,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nfinal List<CommandShortcutEvent> customTextAlignCommands = [\n  customTextLeftAlignCommand,\n  customTextCenterAlignCommand,\n  customTextRightAlignCommand,\n];\n\n/// Windows / Linux : ctrl + shift + l\n/// macOS           : ctrl + shift + l\n/// Allows the user to align text to the left\n///\n/// - support\n///   - desktop\n///   - web\n///\nfinal CommandShortcutEvent customTextLeftAlignCommand = CommandShortcutEvent(\n  key: 'Align text to the left',\n  command: 'ctrl+shift+l',\n  getDescription: LocaleKeys.settings_shortcutsPage_commands_textAlignLeft.tr,\n  handler: (editorState) => _textAlignHandler(editorState, leftAlignmentKey),\n);\n\n/// Windows / Linux : ctrl + shift + c\n/// macOS           : ctrl + shift + c\n/// Allows the user to align text to the center\n///\n/// - support\n///   - desktop\n///   - web\n///\nfinal CommandShortcutEvent customTextCenterAlignCommand = CommandShortcutEvent(\n  key: 'Align text to the center',\n  command: 'ctrl+shift+c',\n  getDescription: LocaleKeys.settings_shortcutsPage_commands_textAlignCenter.tr,\n  handler: (editorState) => _textAlignHandler(editorState, centerAlignmentKey),\n);\n\n/// Windows / Linux : ctrl + shift + r\n/// macOS           : ctrl + shift + r\n/// Allows the user to align text to the right\n///\n/// - support\n///   - desktop\n///   - web\n///\nfinal CommandShortcutEvent customTextRightAlignCommand = CommandShortcutEvent(\n  key: 'Align text to the right',\n  command: 'ctrl+shift+r',\n  getDescription: LocaleKeys.settings_shortcutsPage_commands_textAlignRight.tr,\n  handler: (editorState) => _textAlignHandler(editorState, rightAlignmentKey),\n);\n\nKeyEventResult _textAlignHandler(EditorState editorState, String align) {\n  final Selection? selection = editorState.selection;\n\n  if (selection == null) {\n    return KeyEventResult.ignored;\n  }\n\n  editorState.updateNode(\n    selection,\n    (node) => node.copyWith(\n      attributes: {\n        ...node.attributes,\n        blockComponentAlign: align,\n      },\n    ),\n  );\n\n  return KeyEventResult.handled;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart",
    "content": "import 'package:flowy_infra/theme_extension.dart';\n\n// DON'T MODIFY THIS KEY BECAUSE IT'S SAVED IN THE DATABASE!\n// Used for the block component background color\nconst themeBackgroundColors = {\n  'appflowy_them_color_tint1': FlowyTint.tint1,\n  'appflowy_them_color_tint2': FlowyTint.tint2,\n  'appflowy_them_color_tint3': FlowyTint.tint3,\n  'appflowy_them_color_tint4': FlowyTint.tint4,\n  'appflowy_them_color_tint5': FlowyTint.tint5,\n  'appflowy_them_color_tint6': FlowyTint.tint6,\n  'appflowy_them_color_tint7': FlowyTint.tint7,\n  'appflowy_them_color_tint8': FlowyTint.tint8,\n  'appflowy_them_color_tint9': FlowyTint.tint9,\n};\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/backtick_character_command.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\n\n/// ``` to code block\n///\n/// - support\n///   - desktop\n///   - mobile\n///   - web\n///\nfinal CharacterShortcutEvent formatBacktickToCodeBlock = CharacterShortcutEvent(\n  key: '``` to code block',\n  character: '`',\n  handler: (editorState) async => _convertBacktickToCodeBlock(\n    editorState: editorState,\n  ),\n);\n\nFuture<bool> _convertBacktickToCodeBlock({\n  required EditorState editorState,\n}) async {\n  final selection = editorState.selection;\n  if (selection == null || !selection.isCollapsed) {\n    return false;\n  }\n\n  final node = editorState.getNodeAtPath(selection.end.path);\n  final delta = node?.delta;\n  if (node == null || delta == null || delta.isEmpty) {\n    return false;\n  }\n\n  // only active when the backtick is at the beginning of the line\n  final keyword = '``';\n  final plainText = delta.toPlainText();\n  if (!plainText.startsWith(keyword)) {\n    return false;\n  }\n\n  final transaction = editorState.transaction;\n  final deltaWithoutKeyword = delta.compose(Delta()..delete(keyword.length));\n  transaction.insertNodes(\n    selection.end.path,\n    [\n      codeBlockNode(\n        delta: deltaWithoutKeyword,\n      ),\n      if (node.children.isNotEmpty) ...node.children.map((e) => e.copyWith()),\n    ],\n  );\n  transaction.deleteNode(node);\n  transaction.afterSelection = Selection.collapsed(\n    Position(path: selection.start.path),\n  );\n  await editorState.apply(transaction);\n\n  return true;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/build_context_extension.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/rendering.dart';\n\nextension BuildContextExtension on BuildContext {\n  /// returns a boolean value indicating whether the given offset is contained within the bounds of the specified RenderBox or not.\n  bool isOffsetInside(Offset offset) {\n    final box = findRenderObject() as RenderBox?;\n    if (box == null) {\n      return false;\n    }\n    final result = BoxHitTestResult();\n    box.hitTest(result, position: box.globalToLocal(offset));\n    return result.path.any((entry) => entry.target == box);\n  }\n\n  double get appBarHeight =>\n      AppBarTheme.of(this).toolbarHeight ?? kToolbarHeight;\n  double get statusBarHeight => statusBarAndAppBarHeight - appBarHeight;\n  double get statusBarAndAppBarHeight => MediaQuery.of(this).padding.top;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart",
    "content": "import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass BuiltInPageWidget extends StatefulWidget {\n  const BuiltInPageWidget({\n    super.key,\n    required this.node,\n    required this.editorState,\n    required this.builder,\n  });\n\n  final Node node;\n  final EditorState editorState;\n  final Widget Function(ViewPB viewPB) builder;\n\n  @override\n  State<BuiltInPageWidget> createState() => _BuiltInPageWidgetState();\n}\n\nclass _BuiltInPageWidgetState extends State<BuiltInPageWidget> {\n  late Future<FlowyResult<ViewPB, FlowyError>> future;\n\n  final focusNode = FocusNode();\n\n  String get parentViewId => widget.node.attributes[DatabaseBlockKeys.parentID];\n  String get childViewId => widget.node.attributes[DatabaseBlockKeys.viewID];\n\n  @override\n  void initState() {\n    super.initState();\n    future = ViewBackendService().getChildView(\n      parentViewId: parentViewId,\n      childViewId: childViewId,\n    );\n  }\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder<FlowyResult<ViewPB, FlowyError>>(\n      builder: (context, snapshot) {\n        final page = snapshot.data?.toNullable();\n        if (snapshot.hasData && page != null) {\n          return _build(context, page);\n        }\n\n        if (snapshot.connectionState == ConnectionState.done) {\n          // Delete the page if not found\n          WidgetsBinding.instance.addPostFrameCallback((_) => _deletePage());\n\n          return const Center(child: FlowyText('Cannot load the page'));\n        }\n\n        return const Center(child: CircularProgressIndicator());\n      },\n      future: future,\n    );\n  }\n\n  Widget _build(BuildContext context, ViewPB viewPB) {\n    return MouseRegion(\n      onEnter: (_) => widget.editorState.service.scrollService?.disable(),\n      onExit: (_) => widget.editorState.service.scrollService?.enable(),\n      child: _buildPage(context, viewPB),\n    );\n  }\n\n  Widget _buildPage(BuildContext context, ViewPB view) {\n    final verticalPadding =\n        context.read<DatabasePluginWidgetBuilderSize?>()?.verticalPadding ??\n            0.0;\n    return Focus(\n      focusNode: focusNode,\n      onFocusChange: (value) {\n        if (value) {\n          widget.editorState.service.selectionService.clearSelection();\n        }\n      },\n      child: Padding(\n        padding: EdgeInsets.symmetric(vertical: verticalPadding),\n        child: widget.builder(view),\n      ),\n    );\n  }\n\n  Future<void> _deletePage() async {\n    final transaction = widget.editorState.transaction;\n    transaction.deleteNode(widget.node);\n    await widget.editorState.apply(transaction);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/cover_title_command.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\n/// Press the backspace at the first position of first line to go to the title\n///\n/// - support\n///   - desktop\n///   - web\n///\nfinal CommandShortcutEvent backspaceToTitle = CommandShortcutEvent(\n  key: 'backspace to title',\n  command: 'backspace',\n  getDescription: () => 'backspace to title',\n  handler: (editorState) => _backspaceToTitle(\n    editorState: editorState,\n  ),\n);\n\nKeyEventResult _backspaceToTitle({\n  required EditorState editorState,\n}) {\n  final coverTitleFocusNode = editorState.document.root.context\n      ?.read<SharedEditorContext?>()\n      ?.coverTitleFocusNode;\n  if (coverTitleFocusNode == null) {\n    return KeyEventResult.ignored;\n  }\n\n  final selection = editorState.selection;\n  // only active when the backspace is at the first position of first line\n  if (selection == null ||\n      !selection.isCollapsed ||\n      !selection.start.path.equals([0]) ||\n      selection.start.offset != 0) {\n    return KeyEventResult.ignored;\n  }\n\n  final node = editorState.getNodeAtPath(selection.end.path);\n  if (node == null || node.type != ParagraphBlockKeys.type) {\n    return KeyEventResult.ignored;\n  }\n\n  // delete the first line\n  () async {\n    // only delete the first line if it is empty\n    if (node.delta == null || node.delta!.isEmpty) {\n      final transaction = editorState.transaction;\n      transaction.deleteNode(node);\n      transaction.afterSelection = null;\n      await editorState.apply(transaction);\n    }\n\n    editorState.selection = null;\n    coverTitleFocusNode.requestFocus();\n  }();\n\n  return KeyEventResult.handled;\n}\n\n/// Press the arrow left at the first position of first line to go to the title\n///\n/// - support\n///   - desktop\n///   - web\n///\nfinal CommandShortcutEvent arrowLeftToTitle = CommandShortcutEvent(\n  key: 'arrow left to title',\n  command: 'arrow left',\n  getDescription: () => 'arrow left to title',\n  handler: (editorState) => _arrowKeyToTitle(\n    editorState: editorState,\n    checkSelection: (selection) {\n      if (!selection.isCollapsed ||\n          !selection.start.path.equals([0]) ||\n          selection.start.offset != 0) {\n        return false;\n      }\n      return true;\n    },\n  ),\n);\n\n/// Press the arrow up at the first line to go to the title\n///\n/// - support\n///   - desktop\n///   - web\n///\nfinal CommandShortcutEvent arrowUpToTitle = CommandShortcutEvent(\n  key: 'arrow up to title',\n  command: 'arrow up',\n  getDescription: () => 'arrow up to title',\n  handler: (editorState) => _arrowKeyToTitle(\n    editorState: editorState,\n    checkSelection: (selection) {\n      if (!selection.isCollapsed || !selection.start.path.equals([0])) {\n        return false;\n      }\n      return true;\n    },\n  ),\n);\n\nKeyEventResult _arrowKeyToTitle({\n  required EditorState editorState,\n  required bool Function(Selection selection) checkSelection,\n}) {\n  final coverTitleFocusNode = editorState.document.root.context\n      ?.read<SharedEditorContext?>()\n      ?.coverTitleFocusNode;\n  if (coverTitleFocusNode == null) {\n    return KeyEventResult.ignored;\n  }\n\n  final selection = editorState.selection;\n  // only active when the arrow up is at the first line\n  if (selection == null || !checkSelection(selection)) {\n    return KeyEventResult.ignored;\n  }\n\n  final node = editorState.getNodeAtPath(selection.end.path);\n  if (node == null) {\n    return KeyEventResult.ignored;\n  }\n\n  editorState.selection = null;\n  coverTitleFocusNode.requestFocus();\n\n  return KeyEventResult.handled;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart",
    "content": "import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass EmojiPickerButton extends StatelessWidget {\n  EmojiPickerButton({\n    super.key,\n    required this.emoji,\n    required this.onSubmitted,\n    this.emojiPickerSize = const Size(360, 380),\n    this.emojiSize = 18.0,\n    this.defaultIcon,\n    this.offset,\n    this.direction,\n    this.title,\n    this.showBorder = true,\n    this.enable = true,\n    this.margin,\n    this.buttonSize,\n    this.documentId,\n    this.tabs = const [PickerTabType.emoji, PickerTabType.icon],\n  });\n\n  final EmojiIconData emoji;\n  final double emojiSize;\n  final Size emojiPickerSize;\n  final void Function(\n    SelectedEmojiIconResult result,\n    PopoverController? controller,\n  ) onSubmitted;\n  final PopoverController popoverController = PopoverController();\n  final Widget? defaultIcon;\n  final Offset? offset;\n  final PopoverDirection? direction;\n  final String? title;\n  final bool showBorder;\n  final bool enable;\n  final EdgeInsets? margin;\n  final Size? buttonSize;\n  final String? documentId;\n  final List<PickerTabType> tabs;\n\n  @override\n  Widget build(BuildContext context) {\n    if (UniversalPlatform.isDesktopOrWeb) {\n      return _DesktopEmojiPickerButton(\n        emoji: emoji,\n        onSubmitted: onSubmitted,\n        emojiPickerSize: emojiPickerSize,\n        emojiSize: emojiSize,\n        defaultIcon: defaultIcon,\n        offset: offset,\n        direction: direction,\n        title: title,\n        showBorder: showBorder,\n        enable: enable,\n        buttonSize: buttonSize,\n        tabs: tabs,\n        documentId: documentId,\n      );\n    }\n\n    return _MobileEmojiPickerButton(\n      emoji: emoji,\n      onSubmitted: onSubmitted,\n      emojiSize: emojiSize,\n      enable: enable,\n      title: title,\n      margin: margin,\n      tabs: tabs,\n      documentId: documentId,\n    );\n  }\n}\n\nclass _DesktopEmojiPickerButton extends StatelessWidget {\n  _DesktopEmojiPickerButton({\n    required this.emoji,\n    required this.onSubmitted,\n    this.emojiPickerSize = const Size(360, 380),\n    this.emojiSize = 18.0,\n    this.defaultIcon,\n    this.offset,\n    this.direction,\n    this.title,\n    this.showBorder = true,\n    this.enable = true,\n    this.buttonSize,\n    this.documentId,\n    this.tabs = const [PickerTabType.emoji, PickerTabType.icon],\n  });\n\n  final EmojiIconData emoji;\n  final double emojiSize;\n  final Size emojiPickerSize;\n  final void Function(\n    SelectedEmojiIconResult result,\n    PopoverController? controller,\n  ) onSubmitted;\n  final PopoverController popoverController = PopoverController();\n  final Widget? defaultIcon;\n  final Offset? offset;\n  final PopoverDirection? direction;\n  final String? title;\n  final bool showBorder;\n  final bool enable;\n  final Size? buttonSize;\n  final String? documentId;\n  final List<PickerTabType> tabs;\n\n  @override\n  Widget build(BuildContext context) {\n    final showDefault = emoji.isEmpty && defaultIcon != null;\n    return AppFlowyPopover(\n      controller: popoverController,\n      constraints: BoxConstraints.expand(\n        width: emojiPickerSize.width,\n        height: emojiPickerSize.height,\n      ),\n      offset: offset,\n      margin: EdgeInsets.zero,\n      direction: direction ?? PopoverDirection.rightWithTopAligned,\n      popupBuilder: (_) => Container(\n        width: emojiPickerSize.width,\n        height: emojiPickerSize.height,\n        padding: const EdgeInsets.all(4.0),\n        child: FlowyIconEmojiPicker(\n          initialType: emoji.type.toPickerTabType(),\n          tabs: tabs,\n          documentId: documentId,\n          onSelectedEmoji: (r) {\n            onSubmitted(r, popoverController);\n          },\n        ),\n      ),\n      child: Container(\n        width: buttonSize?.width ?? 30.0,\n        height: buttonSize?.height ?? 30.0,\n        decoration: BoxDecoration(\n          borderRadius: BorderRadius.circular(8),\n          border: showBorder\n              ? Border.all(\n                  color: Theme.of(context).dividerColor,\n                )\n              : null,\n        ),\n        child: FlowyButton(\n          margin: emoji.isEmpty && defaultIcon != null\n              ? EdgeInsets.zero\n              : const EdgeInsets.only(left: 2.0),\n          expandText: false,\n          text: showDefault\n              ? defaultIcon!\n              : RawEmojiIconWidget(emoji: emoji, emojiSize: emojiSize),\n          onTap: enable ? popoverController.show : null,\n        ),\n      ),\n    );\n  }\n}\n\nclass _MobileEmojiPickerButton extends StatelessWidget {\n  const _MobileEmojiPickerButton({\n    required this.emoji,\n    required this.onSubmitted,\n    this.emojiSize = 18.0,\n    this.enable = true,\n    this.title,\n    this.margin,\n    this.documentId,\n    this.tabs = const [PickerTabType.emoji, PickerTabType.icon],\n  });\n\n  final EmojiIconData emoji;\n  final double emojiSize;\n  final void Function(\n    SelectedEmojiIconResult result,\n    PopoverController? controller,\n  ) onSubmitted;\n  final String? title;\n  final bool enable;\n  final EdgeInsets? margin;\n  final String? documentId;\n  final List<PickerTabType> tabs;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      useIntrinsicWidth: true,\n      margin:\n          margin ?? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),\n      text: RawEmojiIconWidget(\n        emoji: emoji,\n        emojiSize: emojiSize,\n      ),\n      onTap: enable\n          ? () async {\n              final result = await context.push<EmojiIconData>(\n                Uri(\n                  path: MobileEmojiPickerScreen.routeName,\n                  queryParameters: {\n                    MobileEmojiPickerScreen.pageTitle: title,\n                    MobileEmojiPickerScreen.iconSelectedType: emoji.type.name,\n                    MobileEmojiPickerScreen.uploadDocumentId: documentId,\n                    MobileEmojiPickerScreen.selectTabs:\n                        tabs.map((e) => e.name).toList().join('-'),\n                  },\n                ).toString(),\n              );\n              if (result != null) {\n                onSubmitted(result.toSelectedResult(), null);\n              }\n            }\n          : null,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/font_colors.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass EditorFontColors {\n  static final lightColors = [\n    const Color(0x00FFFFFF),\n    const Color(0xFFE8E0FF),\n    const Color(0xFFFFE6FD),\n    const Color(0xFFFFDAE6),\n    const Color(0xFFFFEFE3),\n    const Color(0xFFF5FFDC),\n    const Color(0xFFDDFFD6),\n    const Color(0xFFDEFFF1),\n    const Color(0xFFE1FBFF),\n    const Color(0xFFFFADAD),\n    const Color(0xFFFFE088),\n    const Color(0xFFA7DF4A),\n    const Color(0xFFD4C0FF),\n    const Color(0xFFFDB2FE),\n    const Color(0xFFFFD18B),\n    const Color(0xFF65E7F0),\n    const Color(0xFF71E6B4),\n    const Color(0xFF80F1FF),\n  ];\n\n  static final darkColors = [\n    const Color(0x00FFFFFF),\n    const Color(0xFF8B80AD),\n    const Color(0xFF987195),\n    const Color(0xFF906D78),\n    const Color(0xFFA68B77),\n    const Color(0xFF88936D),\n    const Color(0xFF72936B),\n    const Color(0xFF6B9483),\n    const Color(0xFF658B90),\n    const Color(0xFF95405A),\n    const Color(0xFFA6784D),\n    const Color(0xFF6E9234),\n    const Color(0xFF6455A2),\n    const Color(0xFF924F83),\n    const Color(0xFFA48F34),\n    const Color(0xFF29A3AC),\n    const Color(0xFF2E9F84),\n    const Color(0xFF405EA6),\n  ];\n\n  // if the input color doesn't exist in the list, return the input color itself.\n  static Color? fromBuiltInColors(BuildContext context, Color? color) {\n    if (color == null) {\n      return null;\n    }\n\n    final brightness = Theme.of(context).brightness;\n\n    // if the dark mode color using light mode, return it's corresponding light color. Same for light mode.\n    if (brightness == Brightness.light) {\n      if (darkColors.contains(color)) {\n        return lightColors[darkColors.indexOf(color)];\n      }\n    } else {\n      if (lightColors.contains(color)) {\n        return darkColors[lightColors.indexOf(color)];\n      }\n    }\n    return color;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/format_arrow_character.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\n\nconst _greater = '>';\nconst _dash = '-';\nconst _equals = '=';\nconst _equalGreater = '⇒';\nconst _dashGreater = '→';\n\nconst _hyphen = '-';\nconst _emDash = '—'; // This is an em dash — not a single dash - !!\n\n/// format '=' + '>' into an ⇒\n///\n/// - support\n///   - desktop\n///   - mobile\n///   - web\n///\nfinal CharacterShortcutEvent customFormatGreaterEqual = CharacterShortcutEvent(\n  key: 'format = + > into ⇒',\n  character: _greater,\n  handler: (editorState) async => _handleDoubleCharacterReplacement(\n    editorState: editorState,\n    character: _greater,\n    replacement: _equalGreater,\n    prefixCharacter: _equals,\n  ),\n);\n\n/// format '-' + '>' into ⇒\n///\n/// - support\n///   - desktop\n///   - mobile\n///   - web\n///\nfinal CharacterShortcutEvent customFormatDashGreater = CharacterShortcutEvent(\n  key: 'format - + > into ->',\n  character: _greater,\n  handler: (editorState) async => _handleDoubleCharacterReplacement(\n    editorState: editorState,\n    character: _greater,\n    replacement: _dashGreater,\n    prefixCharacter: _dash,\n  ),\n);\n\n/// format two hyphens into an em dash\n///\n/// - support\n///   - desktop\n///   - mobile\n///   - web\n///\nfinal CharacterShortcutEvent customFormatDoubleHyphenEmDash =\n    CharacterShortcutEvent(\n  key: 'format double hyphen into an em dash',\n  character: _hyphen,\n  handler: (editorState) async => _handleDoubleCharacterReplacement(\n    editorState: editorState,\n    character: _hyphen,\n    replacement: _emDash,\n  ),\n);\n\n/// If [prefixCharacter] is null or empty, [character] is used\nFuture<bool> _handleDoubleCharacterReplacement({\n  required EditorState editorState,\n  required String character,\n  required String replacement,\n  String? prefixCharacter,\n}) async {\n  assert(character.length == 1);\n\n  final selection = editorState.selection;\n  if (selection == null) {\n    return false;\n  }\n\n  if (!selection.isCollapsed) {\n    await editorState.deleteSelection(selection);\n  }\n\n  final node = editorState.getNodeAtPath(selection.end.path);\n  final delta = node?.delta;\n  if (node == null ||\n      delta == null ||\n      delta.isEmpty ||\n      node.type == CodeBlockKeys.type) {\n    return false;\n  }\n\n  if (selection.end.offset > 0) {\n    final plain = delta.toPlainText();\n\n    final expectedPrevious =\n        prefixCharacter?.isEmpty ?? true ? character : prefixCharacter;\n\n    final previousCharacter = plain[selection.end.offset - 1];\n    if (previousCharacter != expectedPrevious) {\n      return false;\n    }\n\n    // insert the greater character first and convert it to the replacement character to support undo\n    final insert = editorState.transaction\n      ..insertText(\n        node,\n        selection.end.offset,\n        character,\n      );\n\n    await editorState.apply(\n      insert,\n      skipHistoryDebounce: true,\n    );\n\n    final afterSelection = editorState.selection;\n    if (afterSelection == null) {\n      return false;\n    }\n\n    final replace = editorState.transaction\n      ..replaceText(\n        node,\n        afterSelection.end.offset - 2,\n        2,\n        replacement,\n      );\n\n    await editorState.apply(replace);\n\n    return true;\n  }\n\n  return false;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/domain/database_view_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nextension InsertDatabase on EditorState {\n  Future<void> insertInlinePage(String parentViewId, ViewPB childView) async {\n    final selection = this.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n\n    final node = getNodeAtPath(selection.end.path);\n    if (node == null) {\n      return;\n    }\n\n    final transaction = this.transaction;\n    transaction.insertNode(\n      selection.end.path,\n      Node(\n        type: _convertPageType(childView),\n        attributes: {\n          DatabaseBlockKeys.parentID: parentViewId,\n          DatabaseBlockKeys.viewID: childView.id,\n        },\n      ),\n    );\n    await apply(transaction);\n  }\n\n  Future<void> insertReferencePage(\n    ViewPB childView,\n    ViewLayoutPB viewType,\n  ) async {\n    final selection = this.selection;\n    if (selection == null || !selection.isCollapsed) {\n      throw FlowyError(\n        msg:\n            \"Could not insert the reference page because the current selection was null or collapsed.\",\n      );\n    }\n    final node = getNodeAtPath(selection.end.path);\n    if (node == null) {\n      throw FlowyError(\n        msg:\n            \"Could not insert the reference page because the current node at the selection does not exist.\",\n      );\n    }\n\n    final Transaction transaction = viewType == ViewLayoutPB.Document\n        ? await _insertDocumentReference(childView, selection, node)\n        : await _insertDatabaseReference(childView, selection.end.path);\n\n    await apply(transaction);\n  }\n\n  Future<Transaction> _insertDocumentReference(\n    ViewPB view,\n    Selection selection,\n    Node node,\n  ) async {\n    return transaction\n      ..replaceText(\n        node,\n        selection.end.offset,\n        0,\n        MentionBlockKeys.mentionChar,\n        attributes: MentionBlockKeys.buildMentionPageAttributes(\n          mentionType: MentionType.page,\n          pageId: view.id,\n          blockId: null,\n        ),\n      );\n  }\n\n  Future<Transaction> _insertDatabaseReference(\n    ViewPB view,\n    List<int> path,\n  ) async {\n    // get the database id that the view is associated with\n    final databaseId = await DatabaseViewBackendService(viewId: view.id)\n        .getDatabaseId()\n        .then((value) => value.toNullable());\n\n    if (databaseId == null) {\n      throw StateError(\n        'The database associated with ${view.id} could not be found while attempting to create a referenced ${view.layout.name}.',\n      );\n    }\n\n    final prefix = _referencedDatabasePrefix(view.layout);\n    final ref = await ViewBackendService.createDatabaseLinkedView(\n      parentViewId: view.id,\n      name: \"$prefix ${view.nameOrDefault}\",\n      layoutType: view.layout,\n      databaseId: databaseId,\n    ).then((value) => value.toNullable());\n\n    if (ref == null) {\n      throw FlowyError(\n        msg:\n            \"The `ViewBackendService` failed to create a database reference view\",\n      );\n    }\n\n    return transaction\n      ..insertNode(\n        path,\n        Node(\n          type: _convertPageType(view),\n          attributes: {\n            DatabaseBlockKeys.parentID: view.id,\n            DatabaseBlockKeys.viewID: ref.id,\n          },\n        ),\n      );\n  }\n\n  String _referencedDatabasePrefix(ViewLayoutPB layout) {\n    switch (layout) {\n      case ViewLayoutPB.Grid:\n        return LocaleKeys.grid_referencedGridPrefix.tr();\n      case ViewLayoutPB.Board:\n        return LocaleKeys.board_referencedBoardPrefix.tr();\n      case ViewLayoutPB.Calendar:\n        return LocaleKeys.calendar_referencedCalendarPrefix.tr();\n      default:\n        throw UnimplementedError();\n    }\n  }\n\n  String _convertPageType(ViewPB viewPB) {\n    switch (viewPB.layout) {\n      case ViewLayoutPB.Grid:\n        return DatabaseBlockKeys.gridType;\n      case ViewLayoutPB.Board:\n        return DatabaseBlockKeys.boardType;\n      case ViewLayoutPB.Calendar:\n        return DatabaseBlockKeys.calendarType;\n      default:\n        throw Exception('Unknown layout type');\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nInlineActionsMenuService? _actionsMenuService;\n\nFuture<void> showLinkToPageMenu(\n  EditorState editorState,\n  SelectionMenuService menuService, {\n  ViewLayoutPB? pageType,\n  bool? insertPage,\n}) async {\n  keepEditorFocusNotifier.increase();\n\n  menuService.dismiss();\n  _actionsMenuService?.dismiss();\n\n  final rootContext = editorState.document.root.context;\n  if (rootContext == null) {\n    return;\n  }\n\n  final service = InlineActionsService(\n    context: rootContext,\n    handlers: [\n      InlinePageReferenceService(\n        currentViewId: '',\n        viewLayout: pageType,\n        customTitle: titleFromPageType(pageType),\n        insertPage: insertPage ?? pageType != ViewLayoutPB.Document,\n        limitResults: 15,\n      ),\n    ],\n  );\n\n  final List<InlineActionsResult> initialResults = [];\n  for (final handler in service.handlers) {\n    final group = await handler.search(null);\n\n    if (group.results.isNotEmpty) {\n      initialResults.add(group);\n    }\n  }\n\n  if (rootContext.mounted) {\n    _actionsMenuService = InlineActionsMenu(\n      context: rootContext,\n      editorState: editorState,\n      service: service,\n      initialResults: initialResults,\n      style: Theme.of(editorState.document.root.context!).brightness ==\n              Brightness.light\n          ? const InlineActionsMenuStyle.light()\n          : const InlineActionsMenuStyle.dark(),\n      startCharAmount: 0,\n    );\n\n    await _actionsMenuService?.show();\n  }\n}\n\nString titleFromPageType(ViewLayoutPB? layout) => switch (layout) {\n      ViewLayoutPB.Grid => LocaleKeys.inlineActions_gridReference.tr(),\n      ViewLayoutPB.Document => LocaleKeys.inlineActions_docReference.tr(),\n      ViewLayoutPB.Board => LocaleKeys.inlineActions_boardReference.tr(),\n      ViewLayoutPB.Calendar => LocaleKeys.inlineActions_calReference.tr(),\n      _ => LocaleKeys.inlineActions_pageReference.tr(),\n    };\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart';\nimport 'package:appflowy/shared/markdown_to_document.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:synchronized/synchronized.dart';\n\nconst _enableDebug = false;\n\nclass MarkdownTextRobot {\n  MarkdownTextRobot({\n    required this.editorState,\n  });\n\n  final EditorState editorState;\n\n  final Lock _lock = Lock();\n\n  /// The text position where new nodes will be inserted\n  Position? _insertPosition;\n\n  /// The markdown text to be inserted\n  String _markdownText = '';\n\n  /// The nodes inserted in the previous refresh.\n  Iterable<Node> _insertedNodes = [];\n\n  /// Only for debug via [_enableDebug].\n  final List<String> _debugMarkdownTexts = [];\n\n  /// Selection before the refresh.\n  Selection? _previousSelection;\n\n  bool get hasAnyResult => _markdownText.isNotEmpty;\n\n  String get markdownText => _markdownText;\n\n  Selection? getInsertedSelection() {\n    final position = _insertPosition;\n    if (position == null) {\n      Log.error(\"Expected non-null insert markdown text position\");\n      return null;\n    }\n\n    if (_insertedNodes.isEmpty) {\n      return Selection.collapsed(position);\n    }\n    return Selection(\n      start: position,\n      end: Position(\n        path: position.path.nextNPath(_insertedNodes.length - 1),\n      ),\n    );\n  }\n\n  List<Node> getInsertedNodes() {\n    final selection = getInsertedSelection();\n    return selection == null ? [] : editorState.getNodesInSelection(selection);\n  }\n\n  void start({\n    Selection? previousSelection,\n    Position? position,\n  }) {\n    _insertPosition = position ?? editorState.selection?.start;\n    _previousSelection = previousSelection ?? editorState.selection;\n\n    if (_enableDebug) {\n      Log.info(\n        'MarkdownTextRobot start with insert text position: $_insertPosition',\n      );\n    }\n  }\n\n  /// The text will be inserted into the document but only in memory\n  Future<void> appendMarkdownText(\n    String text, {\n    bool updateSelection = true,\n    Map<String, dynamic>? attributes,\n  }) async {\n    _markdownText += text;\n\n    await _lock.synchronized(() async {\n      await _refresh(\n        inMemoryUpdate: true,\n        updateSelection: updateSelection,\n        attributes: attributes,\n      );\n    });\n\n    if (_enableDebug) {\n      _debugMarkdownTexts.add(text);\n      Log.info(\n        'MarkdownTextRobot receive markdown: ${jsonEncode(_debugMarkdownTexts)}',\n      );\n    }\n  }\n\n  Future<void> stop({\n    Map<String, dynamic>? attributes,\n  }) async {\n    await _lock.synchronized(() async {\n      await _refresh(\n        inMemoryUpdate: true,\n        attributes: attributes,\n      );\n    });\n  }\n\n  /// Persist the text into the document\n  Future<void> persist({\n    String? markdownText,\n  }) async {\n    if (markdownText != null) {\n      _markdownText = markdownText;\n    }\n\n    await _lock.synchronized(() async {\n      await _refresh(inMemoryUpdate: false, updateSelection: true);\n    });\n\n    if (_enableDebug) {\n      Log.info('MarkdownTextRobot stop');\n      _debugMarkdownTexts.clear();\n    }\n  }\n\n  /// Replace the selected content with the AI's response\n  Future<void> replace({\n    required Selection selection,\n    required String markdownText,\n  }) async {\n    if (selection.isSingle) {\n      await _replaceInSameLine(\n        selection: selection,\n        markdownText: markdownText,\n      );\n    } else {\n      await _replaceInMultiLines(\n        selection: selection,\n        markdownText: markdownText,\n      );\n    }\n  }\n\n  /// Delete the temporary inserted AI nodes\n  Future<void> deleteAINodes() async {\n    final nodes = getInsertedNodes();\n    final transaction = editorState.transaction..deleteNodes(nodes);\n    await editorState.apply(\n      transaction,\n      options: const ApplyOptions(recordUndo: false),\n    );\n  }\n\n  /// Discard the inserted content\n  Future<void> discard({\n    Selection? afterSelection,\n  }) async {\n    final start = _insertPosition;\n    if (start == null) {\n      return;\n    }\n    if (_insertedNodes.isEmpty) {\n      return;\n    }\n\n    afterSelection ??= Selection.collapsed(start);\n\n    // fallback to the calculated position if the selection is null.\n    final end = Position(\n      path: start.path.nextNPath(_insertedNodes.length - 1),\n    );\n    final deletedNodes = editorState.getNodesInSelection(\n      Selection(start: start, end: end),\n    );\n    final transaction = editorState.transaction\n      ..deleteNodes(deletedNodes)\n      ..afterSelection = afterSelection;\n\n    await editorState.apply(\n      transaction,\n      options: const ApplyOptions(recordUndo: false, inMemoryUpdate: true),\n    );\n\n    if (_enableDebug) {\n      Log.info('MarkdownTextRobot discard');\n    }\n  }\n\n  void clear() {\n    _markdownText = '';\n    _insertedNodes = [];\n  }\n\n  void reset() {\n    _insertPosition = null;\n  }\n\n  Future<void> _refresh({\n    required bool inMemoryUpdate,\n    bool updateSelection = false,\n    Map<String, dynamic>? attributes,\n  }) async {\n    final position = _insertPosition;\n    if (position == null) {\n      Log.error(\"Expected non-null insert markdown text position\");\n      return;\n    }\n\n    // Convert markdown and deep copy the nodes, prevent ing the linked\n    // entities from being changed\n    final documentNodes = customMarkdownToDocument(\n      _markdownText,\n      tableWidth: 250.0,\n    ).root.children;\n\n    // check if the first selected node before the refresh is a numbered list node\n    final previousSelection = _previousSelection;\n    final previousSelectedNode = previousSelection == null\n        ? null\n        : editorState.getNodeAtPath(previousSelection.start.path);\n    final firstNodeIsNumberedList = previousSelectedNode != null &&\n        previousSelectedNode.type == NumberedListBlockKeys.type;\n\n    final newNodes = attributes == null\n        ? documentNodes\n        : documentNodes.mapIndexed((index, node) {\n            final n = _styleDelta(node: node, attributes: attributes);\n            n.externalValues = AINodeExternalValues(\n              isAINode: true,\n            );\n            if (index == 0 && n.type == NumberedListBlockKeys.type) {\n              if (firstNodeIsNumberedList) {\n                final builder = NumberedListIndexBuilder(\n                  editorState: editorState,\n                  node: previousSelectedNode,\n                );\n                final firstIndex = builder.indexInSameLevel;\n                n.updateAttributes({\n                  NumberedListBlockKeys.number: firstIndex,\n                });\n              }\n\n              n.externalValues = AINodeExternalValues(\n                isAINode: true,\n                isFirstNumberedListNode: true,\n              );\n            }\n            return n;\n          }).toList();\n\n    if (newNodes.isEmpty) {\n      return;\n    }\n\n    final deleteTransaction = editorState.transaction\n      ..deleteNodes(getInsertedNodes());\n\n    await editorState.apply(\n      deleteTransaction,\n      options: ApplyOptions(\n        inMemoryUpdate: inMemoryUpdate,\n        recordUndo: false,\n      ),\n    );\n\n    final insertTransaction = editorState.transaction\n      ..insertNodes(position.path, newNodes);\n\n    final lastDelta = newNodes.lastOrNull?.delta;\n    if (lastDelta != null) {\n      insertTransaction.afterSelection = Selection.collapsed(\n        Position(\n          path: position.path.nextNPath(newNodes.length - 1),\n          offset: lastDelta.length,\n        ),\n      );\n    }\n\n    await editorState.apply(\n      insertTransaction,\n      options: ApplyOptions(\n        inMemoryUpdate: inMemoryUpdate,\n        recordUndo: !inMemoryUpdate,\n      ),\n      withUpdateSelection: updateSelection,\n    );\n\n    _insertedNodes = newNodes;\n  }\n\n  Node _styleDelta({\n    required Node node,\n    required Map<String, dynamic> attributes,\n  }) {\n    if (node.delta != null) {\n      final delta = node.delta!;\n      final attributeDelta = Delta()\n        ..retain(delta.length, attributes: attributes);\n      final newDelta = delta.compose(attributeDelta);\n      final newAttributes = node.attributes;\n      newAttributes['delta'] = newDelta.toJson();\n      node.updateAttributes(newAttributes);\n    }\n\n    List<Node>? children;\n    if (node.children.isNotEmpty) {\n      children = node.children\n          .map((child) => _styleDelta(node: child, attributes: attributes))\n          .toList();\n    }\n\n    return node.copyWith(\n      children: children,\n    );\n  }\n\n  /// If the selected content is in the same line,\n  /// keep the selected node and replace the delta.\n  Future<void> _replaceInSameLine({\n    required Selection selection,\n    required String markdownText,\n  }) async {\n    if (markdownText.isEmpty) {\n      assert(false, 'Expected non-empty markdown text');\n      Log.error('Expected non-empty markdown text');\n      return;\n    }\n\n    selection = selection.normalized;\n\n    // If the selection is not a single node, do nothing.\n    if (!selection.isSingle) {\n      assert(false, 'Expected single node selection');\n      Log.error('Expected single node selection');\n      return;\n    }\n\n    final startIndex = selection.startIndex;\n    final endIndex = selection.endIndex;\n    final length = endIndex - startIndex;\n\n    // Get the selected node.\n    final node = editorState.getNodeAtPath(selection.start.path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      assert(false, 'Expected non-null node and delta');\n      Log.error('Expected non-null node and delta');\n      return;\n    }\n\n    // Convert the markdown text to delta.\n    // Question: Why we need to convert the markdown to document first?\n    // Answer: Because the markdown text may contain the list item,\n    // if we convert the markdown to delta directly, the list item will be\n    // treated as a normal text node, and the delta will be incorrect.\n    // For example, the markdown text is:\n    // ```\n    // 1. item1\n    // ```\n    // if we convert the markdown to delta directly, the delta will be:\n    // ```\n    // [\n    //   {\n    //     \"insert\": \"1. item1\"\n    //   }\n    // ]\n    // ```\n    // if we convert the markdown to document first, the document will be:\n    // ```\n    // [\n    //   {\n    //     \"type\": \"numbered_list\",\n    //     \"children\": [\n    //       {\n    //         \"insert\": \"item1\"\n    //       }\n    //     ]\n    //   }\n    // ]\n    final document = customMarkdownToDocument(markdownText);\n    final nodes = document.root.children;\n    final decoder = DeltaMarkdownDecoder();\n    final markdownDelta =\n        nodes.firstOrNull?.delta ?? decoder.convert(markdownText);\n\n    if (markdownDelta.isEmpty) {\n      assert(false, 'Expected non-empty markdown delta');\n      Log.error('Expected non-empty markdown delta');\n      return;\n    }\n\n    // Replace the delta of the selected node.\n    final transaction = editorState.transaction;\n\n    // it means the user selected the entire sentence, we just replace the node\n    if (startIndex == 0 && length == node.delta?.length) {\n      if (nodes.isNotEmpty && node.children.isNotEmpty) {\n        // merge the children of the selected node and the first node of the ai response\n        nodes[0] = nodes[0].copyWith(\n          children: [\n            ...node.children.map((e) => e.deepCopy()),\n            ...nodes[0].children,\n          ],\n        );\n      }\n      transaction\n        ..insertNodes(node.path.next, nodes)\n        ..deleteNode(node);\n    } else {\n      // it means the user selected a part of the sentence, we need to delete the\n      // selected part and insert the new delta.\n      transaction\n        ..deleteText(node, startIndex, length)\n        ..insertTextDelta(node, startIndex, markdownDelta);\n\n      // Add the remaining nodes to the document.\n      final remainingNodes = nodes.skip(1);\n      if (remainingNodes.isNotEmpty) {\n        transaction.insertNodes(\n          node.path.next,\n          remainingNodes,\n        );\n      }\n    }\n\n    await editorState.apply(transaction);\n  }\n\n  /// If the selected content is in multiple lines\n  Future<void> _replaceInMultiLines({\n    required Selection selection,\n    required String markdownText,\n  }) async {\n    selection = selection.normalized;\n\n    // If the selection is a single node, do nothing.\n    if (selection.isSingle) {\n      assert(false, 'Expected multi-line selection');\n      Log.error('Expected multi-line selection');\n      return;\n    }\n\n    final markdownNodes = customMarkdownToDocument(\n      markdownText,\n      tableWidth: 250.0,\n    ).root.children;\n\n    // Get the selected nodes.\n    final flattenNodes = editorState.getNodesInSelection(selection);\n    final nodes = <Node>[];\n    for (final node in flattenNodes) {\n      if (nodes.any((element) => element.isParentOf(node))) {\n        continue;\n      }\n      nodes.add(node);\n    }\n\n    // Note: Don't change its order, otherwise the delta will be incorrect.\n    // step 1. merge the first selected node and the first node from the ai response\n    // step 2. merge the last selected node and the last node from the ai response\n    // step 3. insert the middle nodes from the ai response\n    // step 4. delete the middle nodes\n    final transaction = editorState.transaction;\n\n    // step 1\n    final firstNode = nodes.firstOrNull;\n    final delta = firstNode?.delta;\n    final firstMarkdownNode = markdownNodes.firstOrNull;\n    final firstMarkdownDelta = firstMarkdownNode?.delta;\n    if (firstNode != null &&\n        delta != null &&\n        firstMarkdownNode != null &&\n        firstMarkdownDelta != null) {\n      final startIndex = selection.startIndex;\n      final length = delta.length - startIndex;\n\n      transaction\n        ..deleteText(firstNode, startIndex, length)\n        ..insertTextDelta(firstNode, startIndex, firstMarkdownDelta);\n\n      // if the first markdown node has children, we need to insert the children\n      // and delete the children of the first node that are in the selection.\n      if (firstMarkdownNode.children.isNotEmpty) {\n        transaction.insertNodes(\n          firstNode.path.child(0),\n          firstMarkdownNode.children.map((e) => e.deepCopy()),\n        );\n      }\n\n      final nodesToDelete =\n          firstNode.children.where((e) => e.path.inSelection(selection));\n      transaction.deleteNodes(nodesToDelete);\n    }\n\n    // step 2\n    bool handledLastNode = false;\n    final lastNode = nodes.lastOrNull;\n    final lastDelta = lastNode?.delta;\n    final lastMarkdownNode = markdownNodes.lastOrNull;\n    final lastMarkdownDelta = lastMarkdownNode?.delta;\n    if (lastNode != null &&\n        lastDelta != null &&\n        lastMarkdownNode != null &&\n        lastMarkdownDelta != null &&\n        firstNode?.id != lastNode.id) {\n      handledLastNode = true;\n\n      final endIndex = selection.endIndex;\n\n      transaction.deleteText(lastNode, 0, endIndex);\n\n      // if the last node is same as the first node, it means we have replaced the\n      // selected text in the first node.\n      if (lastMarkdownNode.id != firstMarkdownNode?.id) {\n        transaction.insertTextDelta(lastNode, 0, lastMarkdownDelta);\n\n        if (lastMarkdownNode.children.isNotEmpty) {\n          transaction\n            ..insertNodes(\n              lastNode.path.child(0),\n              lastMarkdownNode.children.map((e) => e.deepCopy()),\n            )\n            ..deleteNodes(\n              lastNode.children.where((e) => e.path.inSelection(selection)),\n            );\n        }\n      }\n    }\n\n    // step 3\n    final insertedPath = selection.start.path.nextNPath(1);\n    final insertLength = handledLastNode ? 2 : 1;\n    if (markdownNodes.length > insertLength) {\n      transaction.insertNodes(\n        insertedPath,\n        markdownNodes\n            .skip(1)\n            .take(markdownNodes.length - insertLength)\n            .toList(),\n      );\n    }\n\n    // step 4\n    final length = nodes.length - 2;\n    if (length > 0) {\n      final middleNodes = nodes.skip(1).take(length).toList();\n      transaction.deleteNodes(middleNodes);\n    }\n\n    await editorState.apply(transaction);\n  }\n}\n\nclass AINodeExternalValues extends NodeExternalValues {\n  const AINodeExternalValues({\n    this.isAINode = false,\n    this.isFirstNumberedListNode = false,\n  });\n\n  final bool isAINode;\n  final bool isFirstNumberedListNode;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart",
    "content": "import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart';\nimport 'package:appflowy/plugins/inline_actions/handlers/child_page.dart';\nimport 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nconst _bracketChar = '[';\nconst _plusChar = '+';\n\nCharacterShortcutEvent pageReferenceShortcutBrackets(\n  BuildContext context,\n  String viewId,\n  InlineActionsMenuStyle style,\n) =>\n    CharacterShortcutEvent(\n      key: 'show the inline page reference menu by [',\n      character: _bracketChar,\n      handler: (editorState) => inlinePageReferenceCommandHandler(\n        _bracketChar,\n        context,\n        viewId,\n        editorState,\n        style,\n        previousChar: _bracketChar,\n      ),\n    );\n\nCharacterShortcutEvent pageReferenceShortcutPlusSign(\n  BuildContext context,\n  String viewId,\n  InlineActionsMenuStyle style,\n) =>\n    CharacterShortcutEvent(\n      key: 'show the inline page reference menu by +',\n      character: _plusChar,\n      handler: (editorState) => inlinePageReferenceCommandHandler(\n        _plusChar,\n        context,\n        viewId,\n        editorState,\n        style,\n      ),\n    );\n\nInlineActionsMenuService? selectionMenuService;\n\nFuture<bool> inlinePageReferenceCommandHandler(\n  String character,\n  BuildContext context,\n  String currentViewId,\n  EditorState editorState,\n  InlineActionsMenuStyle style, {\n  String? previousChar,\n}) async {\n  final selection = editorState.selection;\n  if (selection == null) {\n    return false;\n  }\n\n  if (!selection.isCollapsed) {\n    await editorState.deleteSelection(selection);\n  }\n\n  // Check for previous character\n  if (previousChar != null) {\n    final node = editorState.getNodeAtPath(selection.end.path);\n    final delta = node?.delta;\n    if (node == null || delta == null || delta.isEmpty) {\n      return false;\n    }\n\n    if (selection.end.offset > 0) {\n      final plain = delta.toPlainText();\n\n      final previousCharacter = plain[selection.end.offset - 1];\n      if (previousCharacter != _bracketChar) {\n        return false;\n      }\n    }\n  }\n\n  if (!context.mounted) {\n    return false;\n  }\n\n  final service = InlineActionsService(\n    context: context,\n    handlers: [\n      if (FeatureFlag.inlineSubPageMention.isOn)\n        InlineChildPageService(currentViewId: currentViewId),\n      InlinePageReferenceService(\n        currentViewId: currentViewId,\n        limitResults: 10,\n      ),\n    ],\n  );\n\n  await editorState.insertTextAtPosition(character, position: selection.start);\n\n  final List<InlineActionsResult> initialResults = [];\n  for (final handler in service.handlers) {\n    final group = await handler.search(null);\n\n    if (group.results.isNotEmpty) {\n      initialResults.add(group);\n    }\n  }\n\n  if (context.mounted) {\n    keepEditorFocusNotifier.increase();\n    selectionMenuService?.dismiss();\n    selectionMenuService = UniversalPlatform.isMobile\n        ? MobileInlineActionsMenu(\n            context: service.context!,\n            editorState: editorState,\n            service: service,\n            initialResults: initialResults,\n            startCharAmount: previousChar != null ? 2 : 1,\n            style: style,\n          )\n        : InlineActionsMenu(\n            context: service.context!,\n            editorState: editorState,\n            service: service,\n            initialResults: initialResults,\n            style: style,\n            startCharAmount: previousChar != null ? 2 : 1,\n            cancelBySpaceHandler: () {\n              if (character == _plusChar) {\n                final currentSelection = editorState.selection;\n                if (currentSelection == null) {\n                  return false;\n                }\n                // check if the space is after the character\n                if (currentSelection.isCollapsed &&\n                    currentSelection.start.offset ==\n                        selection.start.offset + character.length) {\n                  _cancelInlinePageReferenceMenu(editorState);\n                  return true;\n                }\n              }\n              return false;\n            },\n          );\n    // disable the keyboard service\n    editorState.service.keyboardService?.disable();\n\n    await selectionMenuService?.show();\n\n    // enable the keyboard service\n    editorState.service.keyboardService?.enable();\n  }\n\n  return true;\n}\n\nvoid _cancelInlinePageReferenceMenu(EditorState editorState) {\n  selectionMenuService?.dismiss();\n  selectionMenuService = null;\n\n  // re-focus the selection\n  final selection = editorState.selection;\n  if (selection != null) {\n    editorState.updateSelectionWithReason(\n      selection,\n      reason: SelectionUpdateReason.uiEvent,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart",
    "content": "import 'dart:math';\n\n// ignore: implementation_imports\nimport 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass SelectableItemListMenu extends StatelessWidget {\n  const SelectableItemListMenu({\n    super.key,\n    required this.items,\n    required this.selectedIndex,\n    required this.onSelected,\n    this.shrinkWrap = false,\n    this.controller,\n  });\n\n  final List<String> items;\n  final int selectedIndex;\n  final void Function(int) onSelected;\n  final ItemScrollController? controller;\n\n  /// shrinkWrapping is useful in cases where you have a list of\n  /// limited amount of items. It will make the list take the minimum\n  /// amount of space required to show all the items.\n  ///\n  final bool shrinkWrap;\n\n  @override\n  Widget build(BuildContext context) {\n    return ScrollablePositionedList.builder(\n      physics: const ClampingScrollPhysics(),\n      shrinkWrap: shrinkWrap,\n      itemCount: items.length,\n      itemScrollController: controller,\n      initialScrollIndex: max(0, selectedIndex),\n      itemBuilder: (context, index) => SelectableItem(\n        isSelected: index == selectedIndex,\n        item: items[index],\n        onTap: () => onSelected(index),\n      ),\n    );\n  }\n}\n\nclass SelectableItem extends StatelessWidget {\n  const SelectableItem({\n    super.key,\n    required this.isSelected,\n    required this.item,\n    required this.onTap,\n  });\n\n  final bool isSelected;\n  final String item;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 32,\n      child: FlowyButton(\n        text: FlowyText.medium(\n          item,\n          lineHeight: 1.0,\n        ),\n        isSelected: isSelected,\n        onTap: onTap,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nclass SelectableSvgWidget extends StatelessWidget {\n  const SelectableSvgWidget({\n    super.key,\n    required this.data,\n    required this.isSelected,\n    required this.style,\n    this.size,\n    this.padding,\n  });\n\n  final FlowySvgData data;\n  final bool isSelected;\n  final SelectionMenuStyle style;\n  final Size? size;\n  final EdgeInsets? padding;\n\n  @override\n  Widget build(BuildContext context) {\n    final child = FlowySvg(\n      data,\n      size: size ?? const Size.square(16.0),\n      color: isSelected\n          ? style.selectionMenuItemSelectedIconColor\n          : style.selectionMenuItemIconColor,\n    );\n\n    if (padding != null) {\n      return Padding(padding: padding!, child: child);\n    } else {\n      return child;\n    }\n  }\n}\n\nclass SelectableIconWidget extends StatelessWidget {\n  const SelectableIconWidget({\n    super.key,\n    required this.icon,\n    required this.isSelected,\n    required this.style,\n  });\n\n  final IconData icon;\n  final bool isSelected;\n  final SelectionMenuStyle style;\n\n  @override\n  Widget build(BuildContext context) {\n    return Icon(\n      icon,\n      size: 18.0,\n      color: isSelected\n          ? style.selectionMenuItemSelectedIconColor\n          : style.selectionMenuItemIconColor,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/string_extension.dart",
    "content": "extension Capitalize on String {\n  String capitalize() {\n    if (isEmpty) return this;\n    return \"${this[0].toUpperCase()}${substring(1)}\";\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/text_robot.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:synchronized/synchronized.dart';\n\nenum TextRobotInputType {\n  character,\n  word,\n  sentence,\n}\n\nclass TextRobot {\n  TextRobot({\n    required this.editorState,\n  });\n\n  final EditorState editorState;\n  final Lock lock = Lock();\n\n  /// This function is used to insert text in a synchronized way\n  ///\n  /// It is suitable for inserting text in a loop.\n  Future<void> autoInsertTextSync(\n    String text, {\n    TextRobotInputType inputType = TextRobotInputType.word,\n    Duration delay = const Duration(milliseconds: 10),\n    String separator = '\\n',\n  }) async {\n    await lock.synchronized(() async {\n      await autoInsertText(\n        text,\n        inputType: inputType,\n        delay: delay,\n        separator: separator,\n      );\n    });\n  }\n\n  /// This function is used to insert text in an asynchronous way\n  ///\n  /// It is suitable for inserting a long paragraph or a long sentence.\n  Future<void> autoInsertText(\n    String text, {\n    TextRobotInputType inputType = TextRobotInputType.word,\n    Duration delay = const Duration(milliseconds: 10),\n    String separator = '\\n',\n  }) async {\n    if (text == separator) {\n      await insertNewParagraph(delay);\n      return;\n    }\n    final lines = _splitText(text, separator);\n    for (final line in lines) {\n      if (line.isEmpty) {\n        await insertNewParagraph(delay);\n        continue;\n      }\n      switch (inputType) {\n        case TextRobotInputType.character:\n          await insertCharacter(line, delay);\n          break;\n        case TextRobotInputType.word:\n          await insertWord(line, delay);\n          break;\n        case TextRobotInputType.sentence:\n          await insertSentence(line, delay);\n          break;\n      }\n    }\n  }\n\n  Future<void> insertCharacter(String line, Duration delay) async {\n    final iterator = line.runes.iterator;\n    while (iterator.moveNext()) {\n      await insertText(iterator.currentAsString, delay);\n    }\n  }\n\n  Future<void> insertWord(String line, Duration delay) async {\n    final words = line.split(' ');\n    if (words.length == 1 ||\n        (words.length == 2 && (words.first.isEmpty || words.last.isEmpty))) {\n      await insertText(line, delay);\n    } else {\n      for (final word in words.map((e) => '$e ')) {\n        await insertText(word, delay);\n      }\n    }\n    await Future.delayed(delay);\n  }\n\n  Future<void> insertSentence(String line, Duration delay) async {\n    await insertText(line, delay);\n  }\n\n  Future<void> insertNewParagraph(Duration delay) async {\n    final selection = editorState.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n    final next = selection.end.path.next;\n    final transaction = editorState.transaction;\n    transaction.insertNode(\n      next,\n      paragraphNode(),\n    );\n    transaction.afterSelection = Selection.collapsed(\n      Position(path: next),\n    );\n    await editorState.apply(transaction);\n    await Future.delayed(const Duration(milliseconds: 10));\n  }\n\n  Future<void> insertText(String text, Duration delay) async {\n    final selection = editorState.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n    final node = editorState.getNodeAtPath(selection.end.path);\n    if (node == null) {\n      return;\n    }\n    final transaction = editorState.transaction;\n    transaction.insertText(node, selection.endIndex, text);\n    await editorState.apply(transaction);\n    await Future.delayed(delay);\n  }\n}\n\nList<String> _splitText(String text, String separator) {\n  final parts = text.split(RegExp(separator));\n  final result = <String>[];\n\n  for (int i = 0; i < parts.length; i++) {\n    result.add(parts[i]);\n    // Only add empty string if it's not the last part and the next part is not empty\n    if (i < parts.length - 1 && parts[i + 1].isNotEmpty) {\n      result.add('');\n    }\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart",
    "content": "import 'dart:ui';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nbool _isTableType(String type) {\n  return [TableBlockKeys.type, SimpleTableBlockKeys.type].contains(type);\n}\n\nbool notShowInTable(EditorState editorState) {\n  final selection = editorState.selection;\n  if (selection == null) {\n    return false;\n  }\n  final nodes = editorState.getNodesInSelection(selection);\n  return nodes.every((element) {\n    if (_isTableType(element.type)) {\n      return false;\n    }\n    var parent = element.parent;\n    while (parent != null) {\n      if (_isTableType(parent.type)) {\n        return false;\n      }\n      parent = parent.parent;\n    }\n    return true;\n  });\n}\n\nbool onlyShowInSingleTextTypeSelectionAndExcludeTable(\n  EditorState editorState,\n) {\n  return onlyShowInSingleSelectionAndTextType(editorState) &&\n      notShowInTable(editorState);\n}\n\nbool enableSuggestions(EditorState editorState) {\n  final selection = editorState.selection;\n  if (selection == null || !selection.isSingle) {\n    return false;\n  }\n  final node = editorState.getNodeAtPath(selection.start.path);\n  if (node == null) {\n    return false;\n  }\n  if (isNarrowWindow(editorState)) return false;\n\n  return (node.delta != null && suggestionsItemTypes.contains(node.type)) &&\n      notShowInTable(editorState);\n}\n\nbool isNarrowWindow(EditorState editorState) {\n  final editorSize = editorState.renderBox?.size ?? Size.zero;\n  if (editorSize.width < 650) return true;\n  return false;\n}\n\nfinal Set<String> suggestionsItemTypes = {\n  ...toolbarItemWhiteList,\n  ToggleListBlockKeys.type,\n  TodoListBlockKeys.type,\n  CalloutBlockKeys.type,\n};\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass MenuBlockButton extends StatelessWidget {\n  const MenuBlockButton({\n    super.key,\n    required this.tooltip,\n    required this.iconData,\n    this.onTap,\n  });\n\n  final VoidCallback? onTap;\n  final String tooltip;\n  final FlowySvgData iconData;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      useIntrinsicWidth: true,\n      onTap: onTap,\n      text: FlowyTooltip(\n        message: tooltip,\n        child: FlowySvg(\n          iconData,\n          size: const Size.square(16),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/block_transaction_handler/block_transaction_handler.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\n/// A handler for transactions that involve a Block Component.\n///\nabstract class BlockTransactionHandler {\n  const BlockTransactionHandler({required this.blockType});\n\n  /// The type of the block that this handler is built for.\n  /// It's used to determine whether to call any of the handlers on certain transactions.\n  ///\n  final String blockType;\n\n  Future<void> onTransaction(\n    BuildContext context,\n    EditorState editorState,\n    List<Node> added,\n    List<Node> removed, {\n    bool isUndoRedo = false,\n    bool isPaste = false,\n    bool isDraggingNode = false,\n    String? parentViewId,\n  });\n\n  void onUndo(\n    BuildContext context,\n    EditorState editorState,\n    List<Node> before,\n    List<Node> after,\n  );\n\n  void onRedo(\n    BuildContext context,\n    EditorState editorState,\n    List<Node> before,\n    List<Node> after,\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass BulletedListIcon extends StatefulWidget {\n  const BulletedListIcon({\n    super.key,\n    required this.node,\n  });\n\n  final Node node;\n\n  static final bulletedListIcons = [\n    FlowySvgs.bulleted_list_icon_1_s,\n    FlowySvgs.bulleted_list_icon_2_s,\n    FlowySvgs.bulleted_list_icon_3_s,\n  ];\n\n  @override\n  State<BulletedListIcon> createState() => _BulletedListIconState();\n}\n\nclass _BulletedListIconState extends State<BulletedListIcon> {\n  int index = 0;\n  double size = 0.0;\n\n  @override\n  void initState() {\n    super.initState();\n\n    final textStyle =\n        context.read<EditorState>().editorStyle.textStyleConfiguration;\n    final fontSize = textStyle.text.fontSize ?? 16.0;\n    final height = textStyle.text.height ?? textStyle.lineHeight;\n    index = level % BulletedListIcon.bulletedListIcons.length;\n    size = fontSize * height;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final icon = FlowySvg(\n      BulletedListIcon.bulletedListIcons[index],\n      size: Size.square(size * 0.8),\n    );\n    return Container(\n      width: size,\n      height: size,\n      margin: const EdgeInsets.only(right: 8.0),\n      alignment: Alignment.center,\n      child: icon,\n    );\n  }\n\n  int get level {\n    var level = 0;\n    var parent = widget.node.parent;\n    while (parent != null) {\n      if (parent.type == BulletedListBlockKeys.type) {\n        level++;\n      }\n      parent = parent.parent;\n    }\n    return level;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart' show LocaleKeys;\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart'\n    show StringTranslateExtension;\nimport 'package:easy_localization/easy_localization.dart' hide TextDirection;\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../base/emoji_picker_button.dart';\n\n// defining the keys of the callout block's attributes for easy access\nclass CalloutBlockKeys {\n  const CalloutBlockKeys._();\n\n  static const String type = 'callout';\n\n  /// The content of a code block.\n  ///\n  /// The value is a String.\n  static const String delta = 'delta';\n\n  /// The background color of a callout block.\n  ///\n  /// The value is a String.\n  static const String backgroundColor = blockComponentBackgroundColor;\n\n  /// The emoji icon of a callout block.\n  ///\n  /// The value is a String.\n  static const String icon = 'icon';\n\n  /// the type of [FlowyIconType]\n  static const String iconType = 'icon_type';\n}\n\n// The one is inserted through selection menu\nNode calloutNode({\n  Delta? delta,\n  EmojiIconData? emoji,\n  Color? defaultColor,\n}) {\n  final defaultEmoji = emoji ?? EmojiIconData.emoji('📌');\n  final attributes = {\n    CalloutBlockKeys.delta: (delta ?? Delta()).toJson(),\n    CalloutBlockKeys.icon: defaultEmoji.emoji,\n    CalloutBlockKeys.iconType: defaultEmoji.type,\n    CalloutBlockKeys.backgroundColor: defaultColor?.toHex(),\n  };\n  return Node(\n    type: CalloutBlockKeys.type,\n    attributes: attributes,\n  );\n}\n\n// defining the callout block menu item in selection menu\nSelectionMenuItem calloutItem = SelectionMenuItem.node(\n  getName: LocaleKeys.document_plugins_callout.tr,\n  iconData: Icons.note,\n  keywords: [CalloutBlockKeys.type],\n  nodeBuilder: (editorState, context) =>\n      calloutNode(defaultColor: Colors.transparent),\n  replace: (_, node) => node.delta?.isEmpty ?? false,\n  updateSelection: (_, path, __, ___) {\n    return Selection.single(path: path, startOffset: 0);\n  },\n);\n\n// building the callout block widget\nclass CalloutBlockComponentBuilder extends BlockComponentBuilder {\n  CalloutBlockComponentBuilder({\n    super.configuration,\n    required this.defaultColor,\n    required this.inlinePadding,\n  });\n\n  final Color defaultColor;\n  final EdgeInsets Function(Node node) inlinePadding;\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    return CalloutBlockComponentWidget(\n      key: node.key,\n      node: node,\n      defaultColor: defaultColor,\n      inlinePadding: inlinePadding,\n      configuration: configuration,\n      showActions: showActions(node),\n      actionBuilder: (context, state) => actionBuilder(\n        blockComponentContext,\n        state,\n      ),\n      actionTrailingBuilder: (context, state) => actionTrailingBuilder(\n        blockComponentContext,\n        state,\n      ),\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate => (node) => node.delta != null;\n}\n\n// the main widget for rendering the callout block\nclass CalloutBlockComponentWidget extends BlockComponentStatefulWidget {\n  const CalloutBlockComponentWidget({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n    required this.defaultColor,\n    required this.inlinePadding,\n  });\n\n  final Color defaultColor;\n  final EdgeInsets Function(Node node) inlinePadding;\n\n  @override\n  State<CalloutBlockComponentWidget> createState() =>\n      _CalloutBlockComponentWidgetState();\n}\n\nclass _CalloutBlockComponentWidgetState\n    extends State<CalloutBlockComponentWidget>\n    with\n        SelectableMixin,\n        DefaultSelectableMixin,\n        BlockComponentConfigurable,\n        BlockComponentTextDirectionMixin,\n        BlockComponentAlignMixin,\n        BlockComponentBackgroundColorMixin,\n        NestedBlockComponentStatefulWidgetMixin {\n  // the key used to forward focus to the richtext child\n  @override\n  final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');\n\n  // the key used to identify this component\n  @override\n  GlobalKey<State<StatefulWidget>> get containerKey => widget.node.key;\n\n  @override\n  GlobalKey<State<StatefulWidget>> blockComponentKey = GlobalKey(\n    debugLabel: CalloutBlockKeys.type,\n  );\n\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  @override\n  Node get node => widget.node;\n\n  @override\n  Color get backgroundColor {\n    final color = super.backgroundColor;\n    if (color == Colors.transparent) {\n      return AFThemeExtension.of(context).calloutBGColor;\n    }\n    return color;\n  }\n\n  // get the emoji of the note block from the node's attributes or default to '📌'\n  EmojiIconData get emoji {\n    final icon = node.attributes[CalloutBlockKeys.icon];\n    final type =\n        node.attributes[CalloutBlockKeys.iconType] ?? FlowyIconType.emoji;\n    EmojiIconData result = EmojiIconData.emoji('📌');\n    try {\n      result = EmojiIconData(FlowyIconType.values.byName(type), icon);\n    } catch (_) {}\n    return result;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child = node.children.isEmpty\n        ? buildComponent(context)\n        : buildComponentWithChildren(context);\n\n    if (UniversalPlatform.isDesktop) {\n      child = Padding(\n        padding: EdgeInsets.symmetric(vertical: 2.0),\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  @override\n  Widget buildComponentWithChildren(BuildContext context) {\n    Widget child = Stack(\n      children: [\n        Positioned.fill(\n          left: UniversalPlatform.isMobile ? 0 : cachedLeft,\n          child: Container(\n            decoration: BoxDecoration(\n              borderRadius: const BorderRadius.all(Radius.circular(6.0)),\n              color: backgroundColor,\n            ),\n          ),\n        ),\n        NestedListWidget(\n          indentPadding: indentPadding.copyWith(bottom: 8),\n          child: buildComponent(context, withBackgroundColor: false),\n          children: editorState.renderer.buildList(\n            context,\n            widget.node.children,\n          ),\n        ),\n      ],\n    );\n\n    if (UniversalPlatform.isMobile) {\n      child = Padding(\n        padding: padding,\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  // build the callout block widget\n  @override\n  Widget buildComponent(\n    BuildContext context, {\n    bool withBackgroundColor = true,\n  }) {\n    final textDirection = calculateTextDirection(\n      layoutDirection: Directionality.maybeOf(context),\n    );\n    final (emojiSize, emojiButtonSize) = calculateEmojiSize();\n    final documentId = context.read<DocumentBloc?>()?.documentId;\n    Widget child = Container(\n      decoration: BoxDecoration(\n        borderRadius: const BorderRadius.all(Radius.circular(6.0)),\n        color: withBackgroundColor ? backgroundColor : null,\n      ),\n      padding: widget.inlinePadding(widget.node),\n      width: double.infinity,\n      alignment: alignment,\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        crossAxisAlignment: CrossAxisAlignment.start,\n        textDirection: textDirection,\n        children: [\n          const HSpace(6.0),\n          // the emoji picker button for the note\n          EmojiPickerButton(\n            // force to refresh the popover state\n            key: ValueKey(widget.node.id + emoji.emoji),\n            enable: editorState.editable,\n            title: '',\n            margin: UniversalPlatform.isMobile\n                ? const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0)\n                : EdgeInsets.zero,\n            emoji: emoji,\n            emojiSize: emojiSize,\n            showBorder: false,\n            buttonSize: emojiButtonSize,\n            documentId: documentId,\n            tabs: const [\n              PickerTabType.emoji,\n              PickerTabType.icon,\n              PickerTabType.custom,\n            ],\n            onSubmitted: (r, controller) {\n              setEmojiIconData(r.data);\n              if (!r.keepOpen) controller?.close();\n            },\n          ),\n          if (UniversalPlatform.isDesktopOrWeb) const HSpace(6.0),\n          Flexible(\n            child: Padding(\n              padding: const EdgeInsets.symmetric(vertical: 4.0),\n              child: buildCalloutBlockComponent(context, textDirection),\n            ),\n          ),\n          const HSpace(8.0),\n        ],\n      ),\n    );\n\n    if (UniversalPlatform.isMobile && node.children.isEmpty) {\n      child = Padding(\n        key: blockComponentKey,\n        padding: padding,\n        child: child,\n      );\n    } else {\n      child = Container(\n        key: blockComponentKey,\n        padding: EdgeInsets.zero,\n        child: child,\n      );\n    }\n\n    child = BlockSelectionContainer(\n      node: node,\n      delegate: this,\n      listenable: editorState.selectionNotifier,\n      blockColor: editorState.editorStyle.selectionColor,\n      selectionAboveBlock: true,\n      supportTypes: const [\n        BlockSelectionType.block,\n      ],\n      child: child,\n    );\n\n    if (widget.showActions && widget.actionBuilder != null) {\n      child = BlockComponentActionWrapper(\n        node: widget.node,\n        actionBuilder: widget.actionBuilder!,\n        actionTrailingBuilder: widget.actionTrailingBuilder,\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  // build the richtext child\n  Widget buildCalloutBlockComponent(\n    BuildContext context,\n    TextDirection textDirection,\n  ) {\n    return AppFlowyRichText(\n      key: forwardKey,\n      delegate: this,\n      node: widget.node,\n      editorState: editorState,\n      placeholderText: placeholderText,\n      textAlign: alignment?.toTextAlign ?? textAlign,\n      textSpanDecorator: (textSpan) => textSpan.updateTextStyle(\n        textStyleWithTextSpan(textSpan: textSpan),\n      ),\n      placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle(\n        placeholderTextStyleWithTextSpan(textSpan: textSpan),\n      ),\n      textDirection: textDirection,\n      cursorColor: editorState.editorStyle.cursorColor,\n      selectionColor: editorState.editorStyle.selectionColor,\n    );\n  }\n\n  // set the emoji of the note block\n  Future<void> setEmojiIconData(EmojiIconData data) async {\n    final transaction = editorState.transaction\n      ..updateNode(node, {\n        CalloutBlockKeys.icon: data.emoji,\n        CalloutBlockKeys.iconType: data.type.name,\n      })\n      ..afterSelection = Selection.collapsed(\n        Position(path: node.path, offset: node.delta?.length ?? 0),\n      );\n    await editorState.apply(transaction);\n  }\n\n  (double, Size) calculateEmojiSize() {\n    const double defaultEmojiSize = 16.0;\n    const Size defaultEmojiButtonSize = Size(30.0, 30.0);\n    final double emojiSize =\n        editorState.editorStyle.textStyleConfiguration.text.fontSize ??\n            defaultEmojiSize;\n    final emojiButtonSize =\n        defaultEmojiButtonSize * emojiSize / defaultEmojiSize;\n    return (emojiSize, emojiButtonSize);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/services.dart';\n\n/// Pressing Enter in a callout block will insert a newline (\\n) within the callout,\n/// while pressing Shift+Enter in a callout will insert a new paragraph next to the callout.\n///\n/// - support\n///   - desktop\n///   - mobile\n///   - web\n///\nfinal CharacterShortcutEvent insertNewLineInCalloutBlock =\n    CharacterShortcutEvent(\n  key: 'insert a new line in callout block',\n  character: '\\n',\n  handler: _insertNewLineHandler,\n);\n\nCharacterShortcutEventHandler _insertNewLineHandler = (editorState) async {\n  final selection = editorState.selection?.normalized;\n  if (selection == null) {\n    return false;\n  }\n\n  final node = editorState.getNodeAtPath(selection.start.path);\n  if (node == null || node.type != CalloutBlockKeys.type) {\n    return false;\n  }\n\n  // delete the selection\n  await editorState.deleteSelection(selection);\n\n  if (HardwareKeyboard.instance.isShiftPressed) {\n    // ignore the shift+enter event, fallback to the default behavior\n    return false;\n  } else if (node.children.isEmpty) {\n    // insert a new paragraph within the callout block\n    final path = node.path.child(0);\n    final transaction = editorState.transaction;\n    transaction.insertNode(\n      path,\n      paragraphNode(),\n    );\n    transaction.afterSelection = Selection.collapsed(\n      Position(\n        path: path,\n      ),\n    );\n    await editorState.apply(transaction);\n  }\n\n  return true;\n};\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/icon_button.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flutter/material.dart';\n\nCodeBlockCopyBuilder codeBlockCopyBuilder =\n    (_, node) => _CopyButton(node: node);\n\nclass _CopyButton extends StatelessWidget {\n  const _CopyButton({required this.node});\n\n  final Node node;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(4),\n      child: FlowyTooltip(\n        message: LocaleKeys.document_codeBlock_copyTooltip.tr(),\n        child: FlowyIconButton(\n          onPressed: () async {\n            final delta = node.delta;\n            if (delta == null) {\n              return;\n            }\n\n            final document = Document.blank()\n              ..insert([0], [node.deepCopy()])\n              ..toJson();\n\n            await getIt<ClipboardService>().setData(\n              ClipboardServiceData(\n                plainText: delta.toPlainText(),\n                inAppJson: jsonEncode(document.toJson()),\n              ),\n            );\n\n            if (context.mounted) {\n              showToastNotification(\n                message: LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(),\n              );\n            }\n          },\n          hoverColor: Theme.of(context).colorScheme.secondaryContainer,\n          icon: FlowySvg(\n            FlowySvgs.copy_s,\n            color: AFThemeExtension.of(context).textColor,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_language_screen.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n// ignore: implementation_imports\nimport 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nCodeBlockLanguagePickerBuilder codeBlockLanguagePickerBuilder = (\n  editorState,\n  supportedLanguages,\n  onLanguageSelected, {\n  selectedLanguage,\n  onMenuClose,\n  onMenuOpen,\n}) =>\n    CodeBlockLanguageSelector(\n      editorState: editorState,\n      language: selectedLanguage,\n      supportedLanguages: supportedLanguages,\n      onLanguageSelected: onLanguageSelected,\n      onMenuClose: onMenuClose,\n      onMenuOpen: onMenuOpen,\n    );\n\nclass CodeBlockLanguageSelector extends StatefulWidget {\n  const CodeBlockLanguageSelector({\n    super.key,\n    required this.editorState,\n    required this.supportedLanguages,\n    this.language,\n    required this.onLanguageSelected,\n    this.onMenuOpen,\n    this.onMenuClose,\n  });\n\n  final EditorState editorState;\n  final List<String> supportedLanguages;\n  final String? language;\n  final void Function(String) onLanguageSelected;\n  final VoidCallback? onMenuOpen;\n  final VoidCallback? onMenuClose;\n\n  @override\n  State<CodeBlockLanguageSelector> createState() =>\n      _CodeBlockLanguageSelectorState();\n}\n\nclass _CodeBlockLanguageSelectorState extends State<CodeBlockLanguageSelector> {\n  final controller = PopoverController();\n\n  @override\n  void dispose() {\n    controller.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child = Row(\n      children: [\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),\n          child: FlowyTextButton(\n            widget.language?.capitalize() ??\n                LocaleKeys.document_codeBlock_language_auto.tr(),\n            constraints: const BoxConstraints(minWidth: 50),\n            fontColor: AFThemeExtension.of(context).onBackground,\n            padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 4),\n            fillColor: Colors.transparent,\n            hoverColor: Theme.of(context).colorScheme.secondaryContainer,\n            onPressed: () async {\n              if (UniversalPlatform.isMobile) {\n                final language = await context\n                    .push<String>(MobileCodeLanguagePickerScreen.routeName);\n                if (language != null) {\n                  widget.onLanguageSelected(language);\n                }\n              }\n            },\n          ),\n        ),\n      ],\n    );\n\n    if (UniversalPlatform.isDesktopOrWeb) {\n      child = AppFlowyPopover(\n        controller: controller,\n        direction: PopoverDirection.bottomWithLeftAligned,\n        onOpen: widget.onMenuOpen,\n        constraints: const BoxConstraints(maxHeight: 300, maxWidth: 200),\n        onClose: widget.onMenuClose,\n        popupBuilder: (_) => _LanguageSelectionPopover(\n          editorState: widget.editorState,\n          language: widget.language,\n          supportedLanguages: widget.supportedLanguages,\n          onLanguageSelected: (language) {\n            widget.onLanguageSelected(language);\n            controller.close();\n          },\n        ),\n        child: child,\n      );\n    }\n\n    return child;\n  }\n}\n\nclass _LanguageSelectionPopover extends StatefulWidget {\n  const _LanguageSelectionPopover({\n    required this.editorState,\n    required this.language,\n    required this.supportedLanguages,\n    required this.onLanguageSelected,\n  });\n\n  final EditorState editorState;\n  final String? language;\n  final List<String> supportedLanguages;\n  final void Function(String) onLanguageSelected;\n\n  @override\n  State<_LanguageSelectionPopover> createState() =>\n      _LanguageSelectionPopoverState();\n}\n\nclass _LanguageSelectionPopoverState extends State<_LanguageSelectionPopover> {\n  final searchController = TextEditingController();\n  final focusNode = FocusNode();\n  late List<String> filteredLanguages =\n      widget.supportedLanguages.map((e) => e.capitalize()).toList();\n  late int selectedIndex =\n      widget.supportedLanguages.indexOf(widget.language?.toLowerCase() ?? '');\n  final ItemScrollController languageListController = ItemScrollController();\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addPostFrameCallback(\n      // This is a workaround because longer taps might break the\n      // focus, this might be an issue with the Flutter framework.\n      (_) => Future.delayed(\n        const Duration(milliseconds: 100),\n        () => focusNode.requestFocus(),\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    searchController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Shortcuts(\n      shortcuts: const {\n        SingleActivator(LogicalKeyboardKey.arrowUp):\n            _DirectionIntent(AxisDirection.up),\n        SingleActivator(LogicalKeyboardKey.arrowDown):\n            _DirectionIntent(AxisDirection.down),\n        SingleActivator(LogicalKeyboardKey.enter): ActivateIntent(),\n      },\n      child: Actions(\n        actions: {\n          _DirectionIntent: CallbackAction<_DirectionIntent>(\n            onInvoke: (intent) => onArrowKey(intent.direction),\n          ),\n          ActivateIntent: CallbackAction<ActivateIntent>(\n            onInvoke: (intent) {\n              if (selectedIndex < 0) return;\n              selectLanguage(selectedIndex);\n              return null;\n            },\n          ),\n        },\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            FlowyTextField(\n              focusNode: focusNode,\n              autoFocus: false,\n              controller: searchController,\n              hintText: LocaleKeys.document_codeBlock_searchLanguageHint.tr(),\n              onChanged: (_) => setState(() {\n                filteredLanguages = widget.supportedLanguages\n                    .where(\n                      (e) => e.contains(searchController.text.toLowerCase()),\n                    )\n                    .map((e) => e.capitalize())\n                    .toList();\n                selectedIndex =\n                    widget.supportedLanguages.indexOf(widget.language ?? '');\n              }),\n            ),\n            const VSpace(8),\n            Flexible(\n              child: SelectableItemListMenu(\n                controller: languageListController,\n                shrinkWrap: true,\n                items: filteredLanguages,\n                selectedIndex: selectedIndex,\n                onSelected: selectLanguage,\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  void onArrowKey(AxisDirection direction) {\n    if (filteredLanguages.isEmpty) return;\n    final isUp = direction == AxisDirection.up;\n    if (selectedIndex < 0) {\n      selectedIndex = isUp ? 0 : -1;\n    }\n    final length = filteredLanguages.length;\n    setState(() {\n      if (isUp) {\n        selectedIndex = selectedIndex == 0 ? length - 1 : selectedIndex - 1;\n      } else {\n        selectedIndex = selectedIndex == length - 1 ? 0 : selectedIndex + 1;\n      }\n    });\n    languageListController.scrollTo(\n      index: selectedIndex,\n      alignment: 0.5,\n      duration: const Duration(milliseconds: 300),\n    );\n  }\n\n  void selectLanguage(int index) {\n    widget.onLanguageSelected(filteredLanguages[index]);\n  }\n}\n\n/// [ScrollIntent] is not working, so using this custom Intent\nclass _DirectionIntent extends Intent {\n  const _DirectionIntent(this.direction);\n\n  final AxisDirection direction;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_menu_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nfinal codeBlockSelectionMenuItem = SelectionMenuItem.node(\n  getName: () => LocaleKeys.document_selectionMenu_codeBlock.tr(),\n  iconBuilder: (editorState, onSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.icon_code_block_s,\n    isSelected: onSelected,\n    style: style,\n  ),\n  keywords: ['code', 'codeblock'],\n  nodeBuilder: (_, __) => codeBlockNode(),\n  replace: (_, node) => node.delta?.isEmpty ?? false,\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_language_screen.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:go_router/go_router.dart';\n\nclass MobileCodeLanguagePickerScreen extends StatelessWidget {\n  const MobileCodeLanguagePickerScreen({super.key});\n\n  static const routeName = '/code_language_picker';\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: FlowyAppBar(titleText: LocaleKeys.titleBar_language.tr()),\n      body: SafeArea(\n        child: ListView.separated(\n          separatorBuilder: (_, __) => const Divider(),\n          itemCount: defaultCodeBlockSupportedLanguages.length,\n          itemBuilder: (context, index) {\n            final language = defaultCodeBlockSupportedLanguages[index];\n            return SizedBox(\n              height: 48,\n              child: FlowyTextButton(\n                language.capitalize(),\n                padding: const EdgeInsets.symmetric(\n                  horizontal: 16.0,\n                  vertical: 4.0,\n                ),\n                onPressed: () => context.pop(language),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart",
    "content": "import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nNode simpleColumnNode({\n  List<Node>? children,\n  double? ratio,\n}) {\n  return Node(\n    type: SimpleColumnBlockKeys.type,\n    children: children ?? [paragraphNode()],\n    attributes: {\n      SimpleColumnBlockKeys.ratio: ratio,\n    },\n  );\n}\n\nextension SimpleColumnBlockAttributes on Node {\n  // get the next column node of the current column node\n  // if the current column node is the last column node, return null\n  Node? get nextColumn {\n    final index = path.last;\n    final parent = this.parent;\n    if (parent == null || index == parent.children.length - 1) {\n      return null;\n    }\n    return parent.children[index + 1];\n  }\n\n  // get the previous column node of the current column node\n  // if the current column node is the first column node, return null\n  Node? get previousColumn {\n    final index = path.last;\n    final parent = this.parent;\n    if (parent == null || index == 0) {\n      return null;\n    }\n    return parent.children[index - 1];\n  }\n}\n\nclass SimpleColumnBlockKeys {\n  const SimpleColumnBlockKeys._();\n\n  static const String type = 'simple_column';\n\n  /// @Deprecated Use [SimpleColumnBlockKeys.ratio] instead.\n  ///\n  /// This field is no longer used since v0.6.9\n  @Deprecated('Use [SimpleColumnBlockKeys.ratio] instead.')\n  static const String width = 'width';\n\n  /// The ratio of the column width.\n  ///\n  /// The value is a double number between 0 and 1.\n  static const String ratio = 'ratio';\n}\n\nclass SimpleColumnBlockComponentBuilder extends BlockComponentBuilder {\n  SimpleColumnBlockComponentBuilder({\n    super.configuration,\n  });\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    return SimpleColumnBlockComponent(\n      key: node.key,\n      node: node,\n      showActions: showActions(node),\n      configuration: configuration,\n      actionBuilder: (_, state) => actionBuilder(blockComponentContext, state),\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate => (node) => node.children.isNotEmpty;\n}\n\nclass SimpleColumnBlockComponent extends BlockComponentStatefulWidget {\n  const SimpleColumnBlockComponent({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n  });\n\n  @override\n  State<SimpleColumnBlockComponent> createState() =>\n      SimpleColumnBlockComponentState();\n}\n\nclass SimpleColumnBlockComponentState extends State<SimpleColumnBlockComponent>\n    with SelectableMixin, BlockComponentConfigurable {\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  @override\n  Node get node => widget.node;\n\n  RenderBox? get _renderBox => context.findRenderObject() as RenderBox?;\n\n  final columnKey = GlobalKey();\n\n  late final EditorState editorState = context.read<EditorState>();\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child = Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: node.children.map(\n        (e) {\n          Widget child = Provider(\n            create: (_) => DatabasePluginWidgetBuilderSize(\n              verticalPadding: 0,\n              horizontalPadding: 0,\n            ),\n            child: editorState.renderer.build(context, e),\n          );\n          if (SimpleColumnsBlockConstants.enableDebugBorder) {\n            child = DecoratedBox(\n              decoration: BoxDecoration(\n                border: Border.all(\n                  color: Colors.blue,\n                ),\n              ),\n              child: child,\n            );\n          }\n          return child;\n        },\n      ).toList(),\n    );\n\n    child = Padding(\n      key: columnKey,\n      padding: padding,\n      child: child,\n    );\n\n    if (SimpleColumnsBlockConstants.enableDebugBorder) {\n      child = Container(\n        color: Colors.green.withValues(\n          alpha: 0.3,\n        ),\n        child: child,\n      );\n    }\n\n    // the column block does not support the block actions and selection\n    // because the column block is a layout wrapper, it does not have a content\n    return child;\n  }\n\n  @override\n  Position start() => Position(path: widget.node.path);\n\n  @override\n  Position end() => Position(path: widget.node.path, offset: 1);\n\n  @override\n  Position getPositionInOffset(Offset start) => end();\n\n  @override\n  bool get shouldCursorBlink => false;\n\n  @override\n  CursorStyle get cursorStyle => CursorStyle.cover;\n\n  @override\n  Rect getBlockRect({\n    bool shiftWithBaseOffset = false,\n  }) {\n    return getRectsInSelection(Selection.invalid()).first;\n  }\n\n  @override\n  Rect? getCursorRectInPosition(\n    Position position, {\n    bool shiftWithBaseOffset = false,\n  }) {\n    final rects = getRectsInSelection(\n      Selection.collapsed(position),\n      shiftWithBaseOffset: shiftWithBaseOffset,\n    );\n    return rects.firstOrNull;\n  }\n\n  @override\n  List<Rect> getRectsInSelection(\n    Selection selection, {\n    bool shiftWithBaseOffset = false,\n  }) {\n    if (_renderBox == null) {\n      return [];\n    }\n    final parentBox = context.findRenderObject();\n    final renderBox = columnKey.currentContext?.findRenderObject();\n    if (parentBox is RenderBox && renderBox is RenderBox) {\n      return [\n        renderBox.localToGlobal(Offset.zero, ancestor: parentBox) &\n            renderBox.size,\n      ];\n    }\n    return [Offset.zero & _renderBox!.size];\n  }\n\n  @override\n  Selection getSelectionInRange(Offset start, Offset end) =>\n      Selection.single(path: widget.node.path, startOffset: 0, endOffset: 1);\n\n  @override\n  Offset localToGlobal(Offset offset, {bool shiftWithBaseOffset = false}) =>\n      _renderBox!.localToGlobal(offset);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass SimpleColumnBlockWidthResizer extends StatefulWidget {\n  const SimpleColumnBlockWidthResizer({\n    super.key,\n    required this.columnNode,\n    required this.editorState,\n    this.height,\n  });\n\n  final Node columnNode;\n  final EditorState editorState;\n  final double? height;\n\n  @override\n  State<SimpleColumnBlockWidthResizer> createState() =>\n      _SimpleColumnBlockWidthResizerState();\n}\n\nclass _SimpleColumnBlockWidthResizerState\n    extends State<SimpleColumnBlockWidthResizer> {\n  bool isDragging = false;\n\n  ValueNotifier<bool> isHovering = ValueNotifier(false);\n\n  @override\n  void dispose() {\n    isHovering.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      onEnter: (_) => isHovering.value = true,\n      onExit: (_) {\n        // delay the hover state change to avoid flickering\n        Future.delayed(const Duration(milliseconds: 100), () {\n          if (!isDragging) {\n            isHovering.value = false;\n          }\n        });\n      },\n      child: GestureDetector(\n        behavior: HitTestBehavior.opaque,\n        onHorizontalDragStart: _onHorizontalDragStart,\n        onHorizontalDragUpdate: _onHorizontalDragUpdate,\n        onHorizontalDragEnd: _onHorizontalDragEnd,\n        onHorizontalDragCancel: _onHorizontalDragCancel,\n        child: ValueListenableBuilder<bool>(\n          valueListenable: isHovering,\n          builder: (context, isHovering, child) {\n            if (UniversalPlatform.isMobile) {\n              return const SizedBox.shrink();\n            }\n\n            final hide = isDraggingAppFlowyEditorBlock.value || !isHovering;\n            return MouseRegion(\n              cursor: SystemMouseCursors.resizeLeftRight,\n              child: Container(\n                width: 2,\n                height: widget.height ?? 20,\n                margin: EdgeInsets.symmetric(horizontal: 2),\n                color: !hide\n                    ? Theme.of(context).colorScheme.primary\n                    : Colors.transparent,\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  void _onHorizontalDragStart(DragStartDetails details) {\n    isDragging = true;\n    EditorGlobalConfiguration.enableDragMenu.value = false;\n  }\n\n  void _onHorizontalDragUpdate(DragUpdateDetails details) {\n    if (!isDragging) {\n      return;\n    }\n\n    // update the column width in memory\n    final columnNode = widget.columnNode;\n    final columnsNode = columnNode.columnsParent;\n    if (columnsNode == null) {\n      return;\n    }\n    final editorWidth = columnsNode.rect.width;\n    final rect = columnNode.rect;\n    final width = rect.width;\n    final originalRatio = columnNode.attributes[SimpleColumnBlockKeys.ratio];\n    final newWidth = width + details.delta.dx;\n\n    final transaction = widget.editorState.transaction;\n    final newRatio = newWidth / editorWidth;\n    transaction.updateNode(columnNode, {\n      ...columnNode.attributes,\n      SimpleColumnBlockKeys.ratio: newRatio,\n    });\n\n    if (newRatio < 0.1 && newRatio < originalRatio) {\n      return;\n    }\n\n    final nextColumn = columnNode.nextColumn;\n    if (nextColumn != null) {\n      final nextColumnRect = nextColumn.rect;\n      final nextColumnWidth = nextColumnRect.width;\n      final newNextColumnWidth = nextColumnWidth - details.delta.dx;\n      final newNextColumnRatio = newNextColumnWidth / editorWidth;\n      if (newNextColumnRatio < 0.1) {\n        return;\n      }\n      transaction.updateNode(nextColumn, {\n        ...nextColumn.attributes,\n        SimpleColumnBlockKeys.ratio: newNextColumnRatio,\n      });\n    }\n\n    transaction.updateNode(columnsNode, {\n      ...columnsNode.attributes,\n      ColumnsBlockKeys.columnCount: columnsNode.children.length,\n    });\n\n    widget.editorState.apply(\n      transaction,\n      options: ApplyOptions(inMemoryUpdate: true),\n    );\n  }\n\n  void _onHorizontalDragEnd(DragEndDetails details) {\n    isHovering.value = false;\n    EditorGlobalConfiguration.enableDragMenu.value = true;\n\n    if (!isDragging) {\n      return;\n    }\n\n    // apply the transaction again to make sure the width is updated\n    final transaction = widget.editorState.transaction;\n    final columnsNode = widget.columnNode.columnsParent;\n    if (columnsNode == null) {\n      return;\n    }\n    for (final columnNode in columnsNode.children) {\n      transaction.updateNode(columnNode, {\n        ...columnNode.attributes,\n      });\n    }\n    widget.editorState.apply(transaction);\n\n    isDragging = false;\n  }\n\n  void _onHorizontalDragCancel() {\n    isDragging = false;\n    isHovering.value = false;\n    EditorGlobalConfiguration.enableDragMenu.value = true;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_node_extension.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nextension SimpleColumnNodeExtension on Node {\n  /// Returns the parent [Node] of the current node if it is a [SimpleColumnsBlock].\n  Node? get columnsParent {\n    Node? currentNode = parent;\n    while (currentNode != null) {\n      if (currentNode.type == SimpleColumnsBlockKeys.type) {\n        return currentNode;\n      }\n      currentNode = currentNode.parent;\n    }\n    return null;\n  }\n\n  /// Returns the parent [Node] of the current node if it is a [SimpleColumnBlock].\n  Node? get columnParent {\n    Node? currentNode = parent;\n    while (currentNode != null) {\n      if (currentNode.type == SimpleColumnBlockKeys.type) {\n        return currentNode;\n      }\n      currentNode = currentNode.parent;\n    }\n    return null;\n  }\n\n  /// Returns whether the current node is in a [SimpleColumnsBlock].\n  bool get isInColumnsBlock => columnsParent != null;\n\n  /// Returns whether the current node is in a [SimpleColumnBlock].\n  bool get isInColumnBlock => columnParent != null;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart",
    "content": "import 'dart:math';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\n// if the children is not provided, it will create two columns by default.\n// if the columnCount is provided, it will create the specified number of columns.\nNode simpleColumnsNode({\n  List<Node>? children,\n  int? columnCount,\n  double? ratio,\n}) {\n  columnCount ??= 2;\n  children ??= List.generate(\n    columnCount,\n    (index) => simpleColumnNode(\n      ratio: ratio,\n      children: [paragraphNode()],\n    ),\n  );\n\n  // check the type of children\n  for (final child in children) {\n    if (child.type != SimpleColumnBlockKeys.type) {\n      Log.error('the type of children must be column, but got ${child.type}');\n    }\n  }\n\n  return Node(\n    type: SimpleColumnsBlockKeys.type,\n    children: children,\n  );\n}\n\nclass SimpleColumnsBlockKeys {\n  const SimpleColumnsBlockKeys._();\n\n  static const String type = 'simple_columns';\n}\n\nclass SimpleColumnsBlockComponentBuilder extends BlockComponentBuilder {\n  SimpleColumnsBlockComponentBuilder({super.configuration});\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    return ColumnsBlockComponent(\n      key: node.key,\n      node: node,\n      showActions: showActions(node),\n      configuration: configuration,\n      actionBuilder: (_, state) => actionBuilder(blockComponentContext, state),\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate => (node) => node.children.isNotEmpty;\n}\n\nclass ColumnsBlockComponent extends BlockComponentStatefulWidget {\n  const ColumnsBlockComponent({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n  });\n\n  @override\n  State<ColumnsBlockComponent> createState() => ColumnsBlockComponentState();\n}\n\nclass ColumnsBlockComponentState extends State<ColumnsBlockComponent>\n    with SelectableMixin, BlockComponentConfigurable {\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  @override\n  Node get node => widget.node;\n\n  RenderBox? get _renderBox => context.findRenderObject() as RenderBox?;\n\n  final columnsKey = GlobalKey();\n\n  late final EditorState editorState = context.read<EditorState>();\n\n  final ScrollController scrollController = ScrollController();\n\n  final ValueNotifier<double?> heightValueNotifier = ValueNotifier(null);\n\n  @override\n  void initState() {\n    super.initState();\n    _updateColumnsBlock();\n  }\n\n  @override\n  void dispose() {\n    scrollController.dispose();\n    heightValueNotifier.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child = Row(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: _buildChildren(),\n    );\n\n    child = Align(\n      alignment: Alignment.topLeft,\n      child: child,\n    );\n\n    child = Padding(\n      key: columnsKey,\n      padding: padding,\n      child: child,\n    );\n\n    if (SimpleColumnsBlockConstants.enableDebugBorder) {\n      child = DecoratedBox(\n        decoration: BoxDecoration(\n          border: Border.all(\n            color: Colors.red,\n            width: 3.0,\n          ),\n        ),\n        child: child,\n      );\n    }\n\n    // the columns block does not support the block actions and selection\n    // because the columns block is a layout wrapper, it does not have a content\n    return NotificationListener<SizeChangedLayoutNotification>(\n      onNotification: (v) => updateHeightValueNotifier(v),\n      child: SizeChangedLayoutNotifier(child: child),\n    );\n  }\n\n  List<Widget> _buildChildren() {\n    final length = node.children.length;\n    final children = <Widget>[];\n    for (var i = 0; i < length; i++) {\n      final childNode = node.children[i];\n      final double ratio =\n          childNode.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ??\n              1.0 / length;\n\n      Widget child = editorState.renderer.build(context, childNode);\n\n      child = Expanded(\n        flex: (max(ratio, 0.1) * 10000).toInt(),\n        child: child,\n      );\n\n      children.add(child);\n\n      if (i != length - 1) {\n        children.add(\n          ValueListenableBuilder(\n            valueListenable: heightValueNotifier,\n            builder: (context, height, child) {\n              return SimpleColumnBlockWidthResizer(\n                columnNode: childNode,\n                editorState: editorState,\n                height: height,\n              );\n            },\n          ),\n        );\n      }\n    }\n    return children;\n  }\n\n  // Update the existing columns block data\n  // if the column ratio is not existing, it will be set to 1.0 / columnCount\n  void _updateColumnsBlock() {\n    final transaction = editorState.transaction;\n    final length = node.children.length;\n    for (int i = 0; i < length; i++) {\n      final childNode = node.children[i];\n      final ratio = childNode.attributes[SimpleColumnBlockKeys.ratio];\n      if (ratio == null) {\n        transaction.updateNode(childNode, {\n          ...childNode.attributes,\n          SimpleColumnBlockKeys.ratio: 1.0 / length,\n        });\n      }\n    }\n    if (transaction.operations.isNotEmpty) {\n      editorState.apply(transaction);\n    }\n  }\n\n  bool updateHeightValueNotifier(SizeChangedLayoutNotification notification) {\n    if (!mounted) return true;\n    final height = _renderBox?.size.height;\n    if (heightValueNotifier.value == height) return true;\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      heightValueNotifier.value = height;\n    });\n    return true;\n  }\n\n  @override\n  Position start() => Position(path: widget.node.path);\n\n  @override\n  Position end() => Position(path: widget.node.path, offset: 1);\n\n  @override\n  Position getPositionInOffset(Offset start) => end();\n\n  @override\n  bool get shouldCursorBlink => false;\n\n  @override\n  CursorStyle get cursorStyle => CursorStyle.cover;\n\n  @override\n  Rect getBlockRect({\n    bool shiftWithBaseOffset = false,\n  }) {\n    return getRectsInSelection(Selection.invalid()).first;\n  }\n\n  @override\n  Rect? getCursorRectInPosition(\n    Position position, {\n    bool shiftWithBaseOffset = false,\n  }) {\n    final rects = getRectsInSelection(\n      Selection.collapsed(position),\n      shiftWithBaseOffset: shiftWithBaseOffset,\n    );\n    return rects.firstOrNull;\n  }\n\n  @override\n  List<Rect> getRectsInSelection(\n    Selection selection, {\n    bool shiftWithBaseOffset = false,\n  }) {\n    if (_renderBox == null) {\n      return [];\n    }\n    final parentBox = context.findRenderObject();\n    final renderBox = columnsKey.currentContext?.findRenderObject();\n    if (parentBox is RenderBox && renderBox is RenderBox) {\n      return [\n        renderBox.localToGlobal(Offset.zero, ancestor: parentBox) &\n            renderBox.size,\n      ];\n    }\n    return [Offset.zero & _renderBox!.size];\n  }\n\n  @override\n  Selection getSelectionInRange(Offset start, Offset end) =>\n      Selection.single(path: widget.node.path, startOffset: 0, endOffset: 1);\n\n  @override\n  Offset localToGlobal(Offset offset, {bool shiftWithBaseOffset = false}) =>\n      _renderBox!.localToGlobal(offset);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart",
    "content": "class SimpleColumnsBlockConstants {\n  const SimpleColumnsBlockConstants._();\n\n  static const double minimumColumnWidth = 128.0;\n  static const bool enableDebugBorder = false;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/context_menu/custom_context_menu.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nfinal List<List<ContextMenuItem>> customContextMenuItems = [\n  [\n    ContextMenuItem(\n      getName: LocaleKeys.document_plugins_contextMenu_copy.tr,\n      onPressed: (editorState) => customCopyCommand.execute(editorState),\n    ),\n    ContextMenuItem(\n      getName: LocaleKeys.document_plugins_contextMenu_paste.tr,\n      onPressed: (editorState) => customPasteCommand.execute(editorState),\n    ),\n    ContextMenuItem(\n      getName: LocaleKeys.document_plugins_contextMenu_pasteAsPlainText.tr,\n      onPressed: (editorState) =>\n          customPastePlainTextCommand.execute(editorState),\n    ),\n    ContextMenuItem(\n      getName: LocaleKeys.document_plugins_contextMenu_cut.tr,\n      onPressed: (editorState) => customCutCommand.execute(editorState),\n    ),\n  ],\n];\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:appflowy_backend/log.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:super_clipboard/super_clipboard.dart';\n\n/// Used for in-app copy and paste without losing the format.\n///\n/// It's a Json string representing the copied editor nodes.\nconst inAppJsonFormat = CustomValueFormat<String>(\n  applicationId: 'io.appflowy.InAppJsonType',\n  onDecode: _defaultDecode,\n  onEncode: _defaultEncode,\n);\n\n/// Used for table nodes when coping a row or a column.\nconst tableJsonFormat = CustomValueFormat<String>(\n  applicationId: 'io.appflowy.TableJsonType',\n  onDecode: _defaultDecode,\n  onEncode: _defaultEncode,\n);\n\nclass ClipboardServiceData {\n  const ClipboardServiceData({\n    this.plainText,\n    this.html,\n    this.image,\n    this.inAppJson,\n    this.tableJson,\n  });\n\n  /// The [plainText] is the plain text string.\n  ///\n  /// It should be used for pasting the plain text from the clipboard.\n  final String? plainText;\n\n  /// The [html] is the html string.\n  ///\n  /// It should be used for pasting the html from the clipboard.\n  /// For example, copy the content in the browser, and paste it in the editor.\n  final String? html;\n\n  /// The [image] is the image data.\n  ///\n  /// It should be used for pasting the image from the clipboard.\n  /// For example, copy the image in the browser or other apps, and paste it in the editor.\n  final (String, Uint8List?)? image;\n\n  /// The [inAppJson] is the json string of the editor nodes.\n  ///\n  /// It should be used for pasting the content in-app.\n  /// For example, pasting the content from document A to document B.\n  final String? inAppJson;\n\n  /// The [tableJson] is the json string of the table nodes.\n  ///\n  /// It only works for the table nodes when coping a row or a column.\n  /// Don't use it for another scenario.\n  final String? tableJson;\n}\n\nclass ClipboardService {\n  static ClipboardServiceData? _mockData;\n\n  @visibleForTesting\n  static void mockSetData(ClipboardServiceData? data) {\n    _mockData = data;\n  }\n\n  Future<void> setData(ClipboardServiceData data) async {\n    final plainText = data.plainText;\n    final html = data.html;\n    final inAppJson = data.inAppJson;\n    final image = data.image;\n    final tableJson = data.tableJson;\n\n    final item = DataWriterItem();\n    if (plainText != null) {\n      item.add(Formats.plainText(plainText));\n    }\n    if (html != null) {\n      item.add(Formats.htmlText(html));\n    }\n    if (inAppJson != null) {\n      item.add(inAppJsonFormat(inAppJson));\n    }\n    if (tableJson != null) {\n      item.add(tableJsonFormat(tableJson));\n    }\n    if (image != null && image.$2?.isNotEmpty == true) {\n      switch (image.$1) {\n        case 'png':\n          item.add(Formats.png(image.$2!));\n          break;\n        case 'jpeg':\n          item.add(Formats.jpeg(image.$2!));\n          break;\n        case 'gif':\n          item.add(Formats.gif(image.$2!));\n          break;\n        default:\n          throw Exception('unsupported image format: ${image.$1}');\n      }\n    }\n    await SystemClipboard.instance?.write([item]);\n  }\n\n  Future<void> setPlainText(String text) async {\n    await SystemClipboard.instance?.write([\n      DataWriterItem()..add(Formats.plainText(text)),\n    ]);\n  }\n\n  Future<ClipboardServiceData> getData() async {\n    if (_mockData != null) {\n      return _mockData!;\n    }\n\n    final reader = await SystemClipboard.instance?.read();\n\n    if (reader == null) {\n      return const ClipboardServiceData();\n    }\n\n    for (final item in reader.items) {\n      final availableFormats = await item.rawReader!.getAvailableFormats();\n      Log.info('availableFormats: $availableFormats');\n    }\n\n    final plainText = await reader.readValue(Formats.plainText);\n    final html = await reader.readValue(Formats.htmlText);\n    final inAppJson = await reader.readValue(inAppJsonFormat);\n    final tableJson = await reader.readValue(tableJsonFormat);\n    final uri = await reader.readValue(Formats.uri);\n    (String, Uint8List?)? image;\n    if (reader.canProvide(Formats.png)) {\n      image = ('png', await reader.readFile(Formats.png));\n    } else if (reader.canProvide(Formats.jpeg)) {\n      image = ('jpeg', await reader.readFile(Formats.jpeg));\n    } else if (reader.canProvide(Formats.gif)) {\n      image = ('gif', await reader.readFile(Formats.gif));\n    } else if (reader.canProvide(Formats.webp)) {\n      image = ('webp', await reader.readFile(Formats.webp));\n    }\n\n    return ClipboardServiceData(\n      plainText: plainText ?? uri?.uri.toString(),\n      html: html,\n      image: image,\n      inAppJson: inAppJson,\n      tableJson: tableJson,\n    );\n  }\n}\n\nextension on DataReader {\n  Future<Uint8List?>? readFile(FileFormat format) {\n    final c = Completer<Uint8List?>();\n    final progress = getFile(\n      format,\n      (file) async {\n        try {\n          final all = await file.readAll();\n          c.complete(all);\n        } catch (e) {\n          c.completeError(e);\n        }\n      },\n      onError: (e) {\n        c.completeError(e);\n      },\n    );\n    if (progress == null) {\n      c.complete(null);\n    }\n    return c.future;\n  }\n}\n\n/// The default decode function for the clipboard service.\nFuture<String?> _defaultDecode(Object value, String platformType) async {\n  if (value is PlatformDataProvider) {\n    final data = await value.getData(platformType);\n    if (data is List<int>) {\n      return utf8.decode(data, allowMalformed: true);\n    }\n    if (data is String) {\n      return Uri.decodeFull(data);\n    }\n  }\n  return null;\n}\n\n/// The default encode function for the clipboard service.\nFuture<Object> _defaultEncode(String value, String platformType) async {\n  return utf8.encode(value);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\n/// Copy.\n///\n/// - support\n///   - desktop\n///   - web\n///   - mobile\n///\nfinal CommandShortcutEvent customCopyCommand = CommandShortcutEvent(\n  key: 'copy the selected content',\n  getDescription: () => AppFlowyEditorL10n.current.cmdCopySelection,\n  command: 'ctrl+c',\n  macOSCommand: 'cmd+c',\n  handler: _copyCommandHandler,\n);\n\nCommandShortcutEventHandler _copyCommandHandler =\n    (editorState) => handleCopyCommand(editorState);\n\nKeyEventResult handleCopyCommand(\n  EditorState editorState, {\n  bool isCut = false,\n}) {\n  final selection = editorState.selection?.normalized;\n  if (selection == null) {\n    return KeyEventResult.ignored;\n  }\n\n  String? text;\n  String? html;\n  String? inAppJson;\n\n  if (selection.isCollapsed) {\n    // if the selection is collapsed, we will copy the text of the current line.\n    final node = editorState.getNodeAtPath(selection.end.path);\n    if (node == null) {\n      return KeyEventResult.ignored;\n    }\n\n    // plain text.\n    text = node.delta?.toPlainText();\n\n    // in app json\n    final document = Document.blank()\n      ..insert([0], [_handleNode(node.deepCopy(), isCut)]);\n    inAppJson = jsonEncode(document.toJson());\n\n    // html\n    html = documentToHTML(document);\n  } else {\n    // plain text.\n    text = editorState.getTextInSelection(selection).join('\\n');\n\n    final document = _buildCopiedDocument(\n      editorState,\n      selection,\n      isCut: isCut,\n    );\n\n    inAppJson = jsonEncode(document.toJson());\n\n    // html\n    html = documentToHTML(document);\n  }\n\n  () async {\n    await getIt<ClipboardService>().setData(\n      ClipboardServiceData(\n        plainText: text,\n        html: html,\n        inAppJson: inAppJson,\n      ),\n    );\n  }();\n\n  return KeyEventResult.handled;\n}\n\nDocument _buildCopiedDocument(\n  EditorState editorState,\n  Selection selection, {\n  bool isCut = false,\n}) {\n  // filter the table nodes\n  final filteredNodes = <Node>[];\n  final selectedNodes = editorState.getSelectedNodes(selection: selection);\n  final nodes = _handleSubPageNodes(selectedNodes, isCut);\n  for (final node in nodes) {\n    if (node.type == SimpleTableCellBlockKeys.type) {\n      // if the node is a table cell, we will fetch its children instead.\n      filteredNodes.addAll(node.children);\n    } else if (node.type == SimpleTableRowBlockKeys.type) {\n      // if the node is a table row, we will fetch its children's children instead.\n      filteredNodes.addAll(node.children.expand((e) => e.children));\n    } else if (node.type == SimpleColumnBlockKeys.type) {\n      // if the node is a column block, we will fetch its children instead.\n      filteredNodes.addAll(node.children);\n    } else if (node.type == SimpleColumnsBlockKeys.type) {\n      // if the node is a columns block, we will fetch its children's children instead.\n      filteredNodes.addAll(node.children.expand((e) => e.children));\n    } else {\n      filteredNodes.add(node);\n    }\n  }\n  final document = Document.blank()\n    ..insert(\n      [0],\n      filteredNodes.map((e) => e.deepCopy()),\n    );\n  return document;\n}\n\nList<Node> _handleSubPageNodes(List<Node> nodes, [bool isCut = false]) {\n  final handled = <Node>[];\n  for (final node in nodes) {\n    handled.add(_handleNode(node, isCut));\n  }\n\n  return handled;\n}\n\nNode _handleNode(Node node, [bool isCut = false]) {\n  if (!isCut) {\n    return node.deepCopy();\n  }\n\n  final newChildren = node.children.map(_handleNode).toList();\n\n  if (node.type == SubPageBlockKeys.type) {\n    return node.copyWith(\n      attributes: {\n        ...node.attributes,\n        SubPageBlockKeys.wasCopied: !isCut,\n        SubPageBlockKeys.wasCut: isCut,\n      },\n      children: newChildren,\n    );\n  }\n\n  return node.copyWith(children: newChildren);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/shared/clipboard_state.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\n/// cut.\n///\n/// - support\n///   - desktop\n///   - web\n///   - mobile\n///\nfinal CommandShortcutEvent customCutCommand = CommandShortcutEvent(\n  key: 'cut the selected content',\n  getDescription: () => AppFlowyEditorL10n.current.cmdCutSelection,\n  command: 'ctrl+x',\n  macOSCommand: 'cmd+x',\n  handler: _cutCommandHandler,\n);\n\nCommandShortcutEventHandler _cutCommandHandler = (editorState) {\n  final selection = editorState.selection;\n  if (selection == null) {\n    return KeyEventResult.ignored;\n  }\n\n  final context = editorState.document.root.context;\n  if (context == null || !context.mounted) {\n    return KeyEventResult.ignored;\n  }\n\n  context.read<ClipboardState>().didCut();\n\n  handleCopyCommand(editorState, isCut: true);\n\n  if (!selection.isCollapsed) {\n    editorState.deleteSelectionIfNeeded();\n  } else {\n    final node = editorState.getNodeAtPath(selection.end.path);\n    if (node == null) {\n      return KeyEventResult.handled;\n    }\n    // prevent to cut the node that is selecting the table.\n    if (node.parentTableNode != null) {\n      return KeyEventResult.skipRemainingHandlers;\n    }\n\n    final transaction = editorState.transaction;\n    transaction.deleteNode(node);\n    final nextNode = node.next;\n    if (nextNode != null && nextNode.delta != null) {\n      transaction.afterSelection = Selection.collapsed(\n        Position(path: node.path, offset: nextNode.delta?.length ?? 0),\n      );\n    }\n    editorState.apply(transaction);\n  }\n\n  return KeyEventResult.handled;\n};\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart",
    "content": "import 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_notification.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_block_link.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_in_app_json.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart';\nimport 'package:appflowy/shared/clipboard_state.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/default_extensions.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:string_validator/string_validator.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\n/// - support\n///   - desktop\n///   - web\n///   - mobile\n///\nfinal CommandShortcutEvent customPasteCommand = CommandShortcutEvent(\n  key: 'paste the content',\n  getDescription: () => AppFlowyEditorL10n.current.cmdPasteContent,\n  command: 'ctrl+v',\n  macOSCommand: 'cmd+v',\n  handler: _pasteCommandHandler,\n);\n\nfinal CommandShortcutEvent customPastePlainTextCommand = CommandShortcutEvent(\n  key: 'paste the plain content',\n  getDescription: () => AppFlowyEditorL10n.current.cmdPasteContent,\n  command: 'ctrl+shift+v',\n  macOSCommand: 'cmd+shift+v',\n  handler: _pastePlainCommandHandler,\n);\n\nCommandShortcutEventHandler _pasteCommandHandler = (editorState) {\n  final selection = editorState.selection;\n  if (selection == null) {\n    return KeyEventResult.ignored;\n  }\n\n  doPaste(editorState).then((_) {\n    final context = editorState.document.root.context;\n    if (context != null && context.mounted) {\n      context.read<ClipboardState>().didPaste();\n    }\n  });\n\n  return KeyEventResult.handled;\n};\n\nCommandShortcutEventHandler _pastePlainCommandHandler = (editorState) {\n  final selection = editorState.selection;\n  if (selection == null) {\n    return KeyEventResult.ignored;\n  }\n\n  doPlainPaste(editorState).then((_) {\n    final context = editorState.document.root.context;\n    if (context != null && context.mounted) {\n      context.read<ClipboardState>().didPaste();\n    }\n  });\n\n  return KeyEventResult.handled;\n};\n\nFuture<void> doPaste(EditorState editorState) async {\n  final selection = editorState.selection;\n  if (selection == null) {\n    return;\n  }\n\n  EditorNotification.paste().post();\n\n  // dispatch the paste event\n  final data = await getIt<ClipboardService>().getData();\n  final inAppJson = data.inAppJson;\n  final html = data.html;\n  final plainText = data.plainText;\n  final image = data.image;\n\n  // dump the length of the data here, don't log the data itself for privacy concerns\n  Log.info('paste command: inAppJson: ${inAppJson?.length}');\n  Log.info('paste command: html: ${html?.length}');\n  Log.info('paste command: plainText: ${plainText?.length}');\n  Log.info('paste command: image: ${image?.$2?.length}');\n\n  if (await editorState.pasteAppFlowySharePageLink(plainText)) {\n    return Log.info('Pasted block link');\n  }\n\n  // paste as link preview\n  if (await _pasteAsLinkPreview(editorState, plainText)) {\n    return Log.info('Pasted as link preview');\n  }\n\n  // Order:\n  // 1. in app json format\n  // 2. html\n  // 3. image\n  // 4. plain text\n\n  // try to paste the content in order, if any of them is failed, then try the next one\n  if (inAppJson != null && inAppJson.isNotEmpty) {\n    if (await editorState.pasteInAppJson(inAppJson)) {\n      return Log.info('Pasted in app json');\n    }\n  }\n\n  // if the image data is not null, we should handle it first\n  // because the image URL in the HTML may not be reachable due to permission issues\n  // For example, when pasting an image from Slack, the image URL provided is not public.\n  if (image != null && image.$2?.isNotEmpty == true) {\n    final documentBloc =\n        editorState.document.root.context?.read<DocumentBloc>();\n    final documentId = documentBloc?.documentId;\n    if (documentId == null || documentId.isEmpty) {\n      return;\n    }\n\n    await editorState.deleteSelectionIfNeeded();\n    final result = await editorState.pasteImage(\n      image.$1,\n      image.$2!,\n      documentId,\n      selection: selection,\n    );\n    if (result) {\n      return Log.info('Pasted image');\n    }\n  }\n\n  if (html != null && html.isNotEmpty) {\n    await editorState.deleteSelectionIfNeeded();\n    if (await editorState.pasteHtml(html)) {\n      return Log.info('Pasted html');\n    }\n  }\n\n  if (plainText != null && plainText.isNotEmpty) {\n    final currentSelection = editorState.selection;\n    if (currentSelection == null) {\n      await editorState.updateSelectionWithReason(\n        selection,\n        reason: SelectionUpdateReason.uiEvent,\n      );\n    }\n    await editorState.pasteText(plainText);\n    return Log.info('Pasted plain text');\n  }\n\n  return Log.info('unable to parse the clipboard content');\n}\n\nFuture<bool> _pasteAsLinkPreview(\n  EditorState editorState,\n  String? text,\n) async {\n  final isMobile = UniversalPlatform.isMobile;\n  // the url should contain a protocol\n  if (text == null || !isURL(text, {'require_protocol': true})) {\n    return false;\n  }\n\n  final selection = editorState.selection;\n  // Apply the update only when the selection is collapsed\n  // and at the start of the current line\n  if (selection == null ||\n      !selection.isCollapsed ||\n      selection.startIndex != 0) {\n    return false;\n  }\n\n  final node = editorState.getNodeAtPath(selection.start.path);\n  // Apply the update only when the current node is a paragraph\n  // and the paragraph is empty\n  if (node == null ||\n      node.type != ParagraphBlockKeys.type ||\n      node.delta?.toPlainText().isNotEmpty == true) {\n    return false;\n  }\n  if (!isMobile) return false;\n  final bool isImageUrl;\n  try {\n    isImageUrl = await _isImageUrl(text);\n  } catch (e) {\n    Log.info('unable to get content header');\n    return false;\n  }\n\n  if (!isImageUrl) return false;\n\n  // insert the text with link format\n  final textTransaction = editorState.transaction\n    ..insertText(\n      node,\n      0,\n      text,\n      attributes: {AppFlowyRichTextKeys.href: text},\n    );\n  await editorState.apply(\n    textTransaction,\n    skipHistoryDebounce: true,\n  );\n\n  // convert it to image or link preview node\n  final replacementInsertedNodes = [\n    isImageUrl ? imageNode(url: text) : linkPreviewNode(url: text),\n    // if the next node is null, insert a empty paragraph node\n    if (node.next == null) paragraphNode(),\n  ];\n\n  final replacementTransaction = editorState.transaction\n    ..insertNodes(\n      selection.start.path,\n      replacementInsertedNodes,\n    )\n    ..deleteNode(node)\n    ..afterSelection = Selection.collapsed(\n      Position(path: node.path.next),\n    );\n\n  await editorState.apply(replacementTransaction);\n\n  return true;\n}\n\nFuture<void> doPlainPaste(EditorState editorState) async {\n  final selection = editorState.selection;\n  if (selection == null) {\n    return;\n  }\n\n  EditorNotification.paste().post();\n\n  // dispatch the paste event\n  final data = await getIt<ClipboardService>().getData();\n  final plainText = data.plainText;\n  if (plainText != null && plainText.isNotEmpty) {\n    await editorState.pastePlainText(plainText);\n    Log.info('Pasted plain text');\n    return;\n  }\n\n  Log.info('unable to parse the clipboard content');\n  return;\n}\n\nFuture<bool> _isImageUrl(String text) async {\n  if (isNotImageUrl(text)) return false;\n  final response = await http.head(Uri.parse(text));\n\n  if (response.statusCode == 200) {\n    final contentType = response.headers['content-type'];\n    if (contentType != null) {\n      return contentType.startsWith('image/') &&\n          defaultImageExtensions.any(contentType.contains);\n    }\n  }\n\n  throw 'bad status code';\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_block_link.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nextension PasteFromBlockLink on EditorState {\n  Future<bool> pasteAppFlowySharePageLink(String? sharePageLink) async {\n    if (sharePageLink == null || sharePageLink.isEmpty) {\n      return false;\n    }\n\n    // Check if the link matches the appflowy block link format\n    final match = appflowySharePageLinkRegex.firstMatch(sharePageLink);\n\n    if (match == null) {\n      return false;\n    }\n\n    final workspaceId = match.group(1);\n    final pageId = match.group(2);\n    final blockId = match.group(3);\n\n    if (workspaceId == null || pageId == null) {\n      Log.error(\n        'Failed to extract information from block link: $sharePageLink',\n      );\n      return false;\n    }\n\n    final selection = this.selection;\n    if (selection == null) {\n      return false;\n    }\n\n    final node = getNodesInSelection(selection).firstOrNull;\n    if (node == null) {\n      return false;\n    }\n\n    // todo: if the current link is not from current workspace.\n    final transaction = this.transaction;\n    transaction.insertText(\n      node,\n      selection.startIndex,\n      MentionBlockKeys.mentionChar,\n      attributes: MentionBlockKeys.buildMentionPageAttributes(\n        mentionType: MentionType.page,\n        pageId: pageId,\n        blockId: blockId,\n      ),\n    );\n    await apply(transaction);\n\n    return true;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_file.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:cross_file/cross_file.dart';\n\nextension PasteFromFile on EditorState {\n  Future<void> dropFiles(\n    List<int> dropPath,\n    List<XFile> files,\n    String documentId,\n    bool isLocalMode,\n  ) async {\n    for (final file in files) {\n      String? path;\n      FileUrlType? type;\n      if (isLocalMode) {\n        path = await saveFileToLocalStorage(file.path);\n        type = FileUrlType.local;\n      } else {\n        (path, _) = await saveFileToCloudStorage(file.path, documentId);\n        type = FileUrlType.cloud;\n      }\n\n      if (path == null) {\n        continue;\n      }\n\n      final t = transaction\n        ..insertNode(\n          dropPath,\n          fileNode(\n            url: path,\n            type: type,\n            name: file.name,\n          ),\n        );\n      await apply(t);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/shared/markdown_to_document.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:html2md/html2md.dart' as html2md;\n\nextension PasteFromHtml on EditorState {\n  Future<bool> pasteHtml(String html) async {\n    final nodes = convertHtmlToNodes(html);\n    // if there's no nodes being converted successfully, return false\n    if (nodes.isEmpty) {\n      return false;\n    }\n    if (nodes.length == 1) {\n      await pasteSingleLineNode(nodes.first);\n      checkToShowPasteAsMenu(nodes.first);\n    } else {\n      await pasteMultiLineNodes(nodes.toList());\n    }\n    return true;\n  }\n\n  // Convert the html to document nodes.\n  // For the google docs table, it will be fallback to the markdown parser.\n  List<Node> convertHtmlToNodes(String html) {\n    List<Node> nodes = htmlToDocument(html).root.children.toList();\n\n    // 1. remove the front and back empty line\n    while (nodes.isNotEmpty && nodes.first.delta?.isEmpty == true) {\n      nodes.removeAt(0);\n    }\n    while (nodes.isNotEmpty && nodes.last.delta?.isEmpty == true) {\n      nodes.removeLast();\n    }\n\n    // 2. replace the legacy table nodes with the new simple table nodes\n    for (int i = 0; i < nodes.length; i++) {\n      final node = nodes[i];\n      if (node.type == TableBlockKeys.type) {\n        nodes[i] = _convertTableToSimpleTable(node);\n      }\n    }\n\n    // 3. verify the nodes is empty or contains google table flag\n    // The table from Google Docs will contain the flag 'Google Table'\n    const googleDocsFlag = 'docs-internal-guid-';\n    final isPasteFromGoogleDocs = html.contains(googleDocsFlag);\n    final isPasteFromAppleNotes = appleNotesRegex.hasMatch(html);\n    final containsTable = nodes.any(\n      (node) =>\n          [TableBlockKeys.type, SimpleTableBlockKeys.type].contains(node.type),\n    );\n    if ((nodes.isEmpty || isPasteFromGoogleDocs || containsTable) &&\n        !isPasteFromAppleNotes) {\n      // fallback to the markdown parser\n      final markdown = html2md.convert(html);\n      nodes = customMarkdownToDocument(markdown, tableWidth: 200)\n          .root\n          .children\n          .toList();\n    }\n\n    // 4. check if the first node and the last node is bold, because google docs will wrap the table with bold tags\n    if (isPasteFromGoogleDocs) {\n      if (nodes.isNotEmpty && nodes.first.delta?.toPlainText() == '**') {\n        nodes.removeAt(0);\n      }\n      if (nodes.isNotEmpty && nodes.last.delta?.toPlainText() == '**') {\n        nodes.removeLast();\n      }\n    }\n\n    return nodes;\n  }\n\n  // convert the legacy table node to the new simple table node\n  // from type 'table' to type 'simple_table'\n  Node _convertTableToSimpleTable(Node node) {\n    if (node.type != TableBlockKeys.type) {\n      return node;\n    }\n\n    // the table node should contains colsLen and rowsLen\n    final colsLen = node.attributes[TableBlockKeys.colsLen];\n    final rowsLen = node.attributes[TableBlockKeys.rowsLen];\n    if (colsLen == null || rowsLen == null) {\n      return node;\n    }\n\n    final rows = <List<Node>>[];\n    final children = node.children;\n    for (var i = 0; i < rowsLen; i++) {\n      final row = <Node>[];\n      for (var j = 0; j < colsLen; j++) {\n        final cell = children\n            .where(\n              (n) =>\n                  n.attributes[TableCellBlockKeys.rowPosition] == i &&\n                  n.attributes[TableCellBlockKeys.colPosition] == j,\n            )\n            .firstOrNull;\n        row.add(\n          simpleTableCellBlockNode(\n            children: cell?.children.map((e) => e.deepCopy()).toList() ??\n                [paragraphNode()],\n          ),\n        );\n      }\n      rows.add(row);\n    }\n\n    return simpleTableBlockNode(\n      children: rows.map((e) => simpleTableRowBlockNode(children: e)).toList(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart",
    "content": "import 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';\nimport 'package:appflowy/shared/patterns/file_type_patterns.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/default_extensions.dart';\nimport 'package:appflowy/workspace/application/settings/application_data_storage.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:cross_file/cross_file.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nextension PasteFromImage on EditorState {\n  Future<void> dropImages(\n    List<int> dropPath,\n    List<XFile> files,\n    String documentId,\n    bool isLocalMode,\n  ) async {\n    final imageFiles = files.where(\n      (file) =>\n          file.mimeType?.startsWith('image/') ??\n          false || imgExtensionRegex.hasMatch(file.name.toLowerCase()),\n    );\n\n    for (final file in imageFiles) {\n      String? path;\n      CustomImageType? type;\n      if (isLocalMode) {\n        path = await saveImageToLocalStorage(file.path);\n        type = CustomImageType.local;\n      } else {\n        (path, _) = await saveImageToCloudStorage(file.path, documentId);\n        type = CustomImageType.internal;\n      }\n\n      if (path == null) {\n        continue;\n      }\n\n      final t = transaction\n        ..insertNode(\n          dropPath,\n          customImageNode(url: path, type: type),\n        );\n      await apply(t);\n    }\n  }\n\n  Future<bool> pasteImage(\n    String format,\n    Uint8List imageBytes,\n    String documentId, {\n    Selection? selection,\n  }) async {\n    final context = document.root.context;\n\n    if (context == null) {\n      return false;\n    }\n\n    if (!defaultImageExtensions.contains(format)) {\n      Log.info('unsupported format: $format');\n      if (UniversalPlatform.isMobile) {\n        showToastNotification(\n          message: LocaleKeys.document_imageBlock_error_invalidImageFormat.tr(),\n        );\n      }\n      return false;\n    }\n\n    final isLocalMode = context.read<DocumentBloc>().isLocalMode;\n\n    final path = await getIt<ApplicationDataStorage>().getPath();\n    final imagePath = p.join(\n      path,\n      'images',\n    );\n\n    try {\n      // create the directory if not exists\n      final directory = Directory(imagePath);\n      if (!directory.existsSync()) {\n        await directory.create(recursive: true);\n      }\n      final copyToPath = p.join(\n        imagePath,\n        'tmp_${uuid()}.$format',\n      );\n      await File(copyToPath).writeAsBytes(imageBytes);\n      final String? path;\n\n      CustomImageType type;\n      if (isLocalMode) {\n        path = await saveImageToLocalStorage(copyToPath);\n        type = CustomImageType.local;\n      } else {\n        final result = await saveImageToCloudStorage(copyToPath, documentId);\n\n        final errorMessage = result.$2;\n\n        if (errorMessage != null && context.mounted) {\n          showToastNotification(\n            message: errorMessage,\n          );\n          return false;\n        }\n\n        path = result.$1;\n        type = CustomImageType.internal;\n      }\n\n      if (path != null) {\n        await insertImageNode(path, selection: selection, type: type);\n      }\n\n      return true;\n    } catch (e) {\n      Log.error('cannot copy image file', e);\n      if (context.mounted) {\n        showToastNotification(\n          message: LocaleKeys.document_imageBlock_error_invalidImage.tr(),\n        );\n      }\n    }\n\n    return false;\n  }\n\n  Future<void> insertImageNode(\n    String src, {\n    Selection? selection,\n    required CustomImageType type,\n  }) async {\n    selection ??= this.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n    final node = getNodeAtPath(selection.end.path);\n    if (node == null) {\n      return;\n    }\n    final transaction = this.transaction;\n    // if the current node is empty paragraph, replace it with image node\n    if (node.type == ParagraphBlockKeys.type &&\n        (node.delta?.isEmpty ?? false)) {\n      transaction\n        ..insertNode(\n          node.path,\n          customImageNode(\n            url: src,\n            type: type,\n          ),\n        )\n        ..deleteNode(node);\n    } else {\n      transaction.insertNode(\n        node.path.next,\n        customImageNode(\n          url: src,\n          type: type,\n        ),\n      );\n    }\n\n    transaction.afterSelection = Selection.collapsed(\n      Position(\n        path: node.path.next,\n      ),\n    );\n\n    return apply(transaction);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_in_app_json.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nextension PasteFromInAppJson on EditorState {\n  Future<bool> pasteInAppJson(String inAppJson) async {\n    try {\n      final nodes = Document.fromJson(jsonDecode(inAppJson)).root.children;\n\n      // skip pasting a table block to another table block\n      final containsTable =\n          nodes.any((node) => node.type == SimpleTableBlockKeys.type);\n      if (containsTable) {\n        final selectedNodes = getSelectedNodes(withCopy: false);\n        if (selectedNodes.any((node) => node.parentTableNode != null)) {\n          return false;\n        }\n      }\n\n      if (nodes.isEmpty) {\n        Log.info('pasteInAppJson: nodes is empty');\n        return false;\n      }\n      if (nodes.length == 1) {\n        Log.info('pasteInAppJson: single line node');\n        await pasteSingleLineNode(nodes.first);\n      } else {\n        Log.info('pasteInAppJson: multi line nodes');\n        await pasteMultiLineNodes(nodes.toList());\n      }\n      return true;\n    } catch (e) {\n      Log.error(\n        'Failed to paste in app json: $inAppJson, error: $e',\n      );\n    }\n    return false;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart';\nimport 'package:appflowy/shared/markdown_to_document.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nextension PasteFromPlainText on EditorState {\n  Future<void> pastePlainText(String plainText) async {\n    await deleteSelectionIfNeeded();\n    final nodes = plainText\n        .split('\\n')\n        .map(\n          (e) => e\n            ..replaceAll(r'\\r', '')\n            ..trimRight(),\n        )\n        .map((e) => Delta()..insert(e))\n        .map((e) => paragraphNode(delta: e))\n        .toList();\n    if (nodes.isEmpty) {\n      return;\n    }\n    if (nodes.length == 1) {\n      await pasteSingleLineNode(nodes.first);\n    } else {\n      await pasteMultiLineNodes(nodes.toList());\n    }\n  }\n\n  Future<void> pasteText(String plainText) async {\n    if (await pasteHtmlIfAvailable(plainText)) {\n      return;\n    }\n\n    await deleteSelectionIfNeeded();\n\n    /// try to parse the plain text as markdown\n    final nodes = customMarkdownToDocument(plainText).root.children;\n    if (nodes.isEmpty) {\n      /// if the markdown parser failed, fallback to the plain text parser\n      await pastePlainText(plainText);\n      return;\n    }\n    if (nodes.length == 1) {\n      await pasteSingleLineNode(nodes.first);\n      checkToShowPasteAsMenu(nodes.first);\n    } else {\n      await pasteMultiLineNodes(nodes.toList());\n    }\n  }\n\n  Future<bool> pasteHtmlIfAvailable(String plainText) async {\n    final selection = this.selection;\n    if (selection == null ||\n        !selection.isSingle ||\n        selection.isCollapsed ||\n        !hrefRegex.hasMatch(plainText)) {\n      return false;\n    }\n\n    final node = getNodeAtPath(selection.start.path);\n    if (node == null) {\n      return false;\n    }\n\n    final transaction = this.transaction;\n    transaction.formatText(node, selection.startIndex, selection.length, {\n      AppFlowyRichTextKeys.href: plainText,\n    });\n    await apply(transaction);\n    checkToShowPasteAsMenu(node);\n    return true;\n  }\n\n  void checkToShowPasteAsMenu(Node node) {\n    if (selection == null || !selection!.isCollapsed) return;\n    if (UniversalPlatform.isMobile) return;\n    final href = _getLinkFromNode(node);\n    if (href != null) {\n      final context = document.root.context;\n      if (context != null && context.mounted) {\n        PasteAsMenuService(context: context, editorState: this).show(href);\n      }\n    }\n  }\n\n  String? _getLinkFromNode(Node node) {\n    final delta = node.delta;\n    if (delta == null) return null;\n    final inserts = delta.whereType<TextInsert>();\n    if (inserts.isEmpty || inserts.length > 1) return null;\n    final link = inserts.first.attributes?.href;\n    if (link != null) return inserts.first.text;\n    return null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/plugins/document/application/prelude.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy/shared/flowy_gradient_colors.dart';\nimport 'package:appflowy/shared/google_fonts_extension.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:auto_size_text_field/auto_size_text_field.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\n\ndouble kDocumentCoverHeight = 98.0;\ndouble kDocumentTitlePadding = 20.0;\n\nclass DocumentImmersiveCover extends StatefulWidget {\n  const DocumentImmersiveCover({\n    super.key,\n    required this.view,\n    required this.userProfilePB,\n    required this.tabs,\n    this.fixedTitle,\n  });\n\n  final ViewPB view;\n  final UserProfilePB userProfilePB;\n  final String? fixedTitle;\n  final List<PickerTabType> tabs;\n\n  @override\n  State<DocumentImmersiveCover> createState() => _DocumentImmersiveCoverState();\n}\n\nclass _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {\n  final textEditingController = TextEditingController();\n  final scrollController = ScrollController();\n  final focusNode = FocusNode();\n\n  late PropertyValueNotifier<Selection?>? selectionNotifier =\n      context.read<DocumentBloc>().state.editorState?.selectionNotifier;\n\n  @override\n  void initState() {\n    super.initState();\n    selectionNotifier?.addListener(_unfocus);\n    if (widget.view.name.isEmpty) {\n      focusNode.requestFocus();\n    }\n  }\n\n  @override\n  void dispose() {\n    textEditingController.dispose();\n    scrollController.dispose();\n    selectionNotifier?.removeListener(_unfocus);\n    focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return IgnoreParentGestureWidget(\n      child: BlocProvider(\n        create: (context) => DocumentImmersiveCoverBloc(view: widget.view)\n          ..add(const DocumentImmersiveCoverEvent.initial()),\n        child: BlocConsumer<DocumentImmersiveCoverBloc,\n            DocumentImmersiveCoverState>(\n          listener: (context, state) {\n            if (textEditingController.text != state.name) {\n              textEditingController.text = state.name;\n            }\n          },\n          builder: (_, state) {\n            final iconAndTitle = _buildIconAndTitle(context, state);\n            if (state.cover.type == PageStyleCoverImageType.none) {\n              return Padding(\n                padding: EdgeInsets.only(\n                  top: context.statusBarAndAppBarHeight + kDocumentTitlePadding,\n                ),\n                child: iconAndTitle,\n              );\n            }\n\n            return Padding(\n              padding: const EdgeInsets.only(bottom: 16),\n              child: Stack(\n                children: [\n                  _buildCover(context, state),\n                  Positioned(\n                    left: 0,\n                    right: 0,\n                    bottom: 0,\n                    child: Padding(\n                      padding: const EdgeInsets.symmetric(vertical: 24.0),\n                      child: iconAndTitle,\n                    ),\n                  ),\n                ],\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  Widget _buildIconAndTitle(\n    BuildContext context,\n    DocumentImmersiveCoverState state,\n  ) {\n    final icon = state.icon;\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 24.0),\n      child: Row(\n        children: [\n          if (icon != null && icon.isNotEmpty) ...[\n            _buildIcon(context, icon),\n            const HSpace(8.0),\n          ],\n          Expanded(child: _buildTitle(context, state)),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildTitle(\n    BuildContext context,\n    DocumentImmersiveCoverState state,\n  ) {\n    String? fontFamily = defaultFontFamily;\n    final documentFontFamily =\n        context.read<DocumentPageStyleBloc>().state.fontFamily;\n    if (documentFontFamily != null && fontFamily != documentFontFamily) {\n      fontFamily = getGoogleFontSafely(documentFontFamily).fontFamily;\n    }\n\n    if (widget.fixedTitle != null) {\n      return FlowyText(\n        widget.fixedTitle!,\n        fontSize: 28.0,\n        fontWeight: FontWeight.w700,\n        fontFamily: fontFamily,\n        color:\n            state.cover.isNone || state.cover.isPresets ? null : Colors.white,\n        overflow: TextOverflow.ellipsis,\n      );\n    }\n\n    return AutoSizeTextField(\n      controller: textEditingController,\n      focusNode: focusNode,\n      minFontSize: 18.0,\n      decoration: InputDecoration(\n        border: InputBorder.none,\n        enabledBorder: InputBorder.none,\n        disabledBorder: InputBorder.none,\n        focusedBorder: InputBorder.none,\n        hintText: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),\n        contentPadding: EdgeInsets.zero,\n      ),\n      scrollController: scrollController,\n      keyboardType: TextInputType.text,\n      textInputAction: TextInputAction.next,\n      style: TextStyle(\n        fontSize: 28.0,\n        fontWeight: FontWeight.w700,\n        fontFamily: fontFamily,\n        color:\n            state.cover.isNone || state.cover.isPresets ? null : Colors.white,\n        overflow: TextOverflow.ellipsis,\n      ),\n      onChanged: (name) => Debounce.debounce(\n        'rename',\n        const Duration(milliseconds: 300),\n        () => _rename(name),\n      ),\n      onSubmitted: (name) {\n        // focus on the document\n        _createNewLine();\n        Debounce.debounce(\n          'rename',\n          const Duration(milliseconds: 300),\n          () => _rename(name),\n        );\n      },\n    );\n  }\n\n  Widget _buildIcon(BuildContext context, EmojiIconData icon) {\n    return GestureDetector(\n      child: ConstrainedBox(\n        constraints: const BoxConstraints.tightFor(width: 34.0),\n        child: EmojiIconWidget(\n          emoji: icon,\n          emojiSize: 26,\n        ),\n      ),\n      onTap: () async {\n        final pageStyleIconBloc = PageStyleIconBloc(view: widget.view)\n          ..add(const PageStyleIconEvent.initial());\n        await showMobileBottomSheet(\n          context,\n          showDragHandle: true,\n          showDivider: false,\n          showHeader: true,\n          title: LocaleKeys.titleBar_pageIcon.tr(),\n          backgroundColor: AFThemeExtension.of(context).background,\n          enableDraggableScrollable: true,\n          minChildSize: 0.6,\n          initialChildSize: 0.61,\n          scrollableWidgetBuilder: (_, controller) {\n            return BlocProvider.value(\n              value: pageStyleIconBloc,\n              child: Expanded(\n                child: FlowyIconEmojiPicker(\n                  initialType: icon.type.toPickerTabType(),\n                  tabs: widget.tabs,\n                  documentId: widget.view.id,\n                  onSelectedEmoji: (r) {\n                    pageStyleIconBloc.add(\n                      PageStyleIconEvent.updateIcon(r.data, true),\n                    );\n                    if (!r.keepOpen) Navigator.pop(context);\n                  },\n                ),\n              ),\n            );\n          },\n          builder: (_) => const SizedBox.shrink(),\n        );\n      },\n    );\n  }\n\n  Widget _buildCover(BuildContext context, DocumentImmersiveCoverState state) {\n    final cover = state.cover;\n    final type = cover.type;\n    final naviBarHeight = MediaQuery.of(context).padding.top;\n    final height = naviBarHeight + kDocumentCoverHeight;\n\n    if (type == PageStyleCoverImageType.customImage ||\n        type == PageStyleCoverImageType.unsplashImage) {\n      return SizedBox(\n        height: height,\n        width: double.infinity,\n        child: FlowyNetworkImage(\n          url: cover.value,\n          userProfilePB: widget.userProfilePB,\n        ),\n      );\n    }\n\n    if (type == PageStyleCoverImageType.builtInImage) {\n      return SizedBox(\n        height: height,\n        width: double.infinity,\n        child: Image.asset(\n          PageStyleCoverImageType.builtInImagePath(cover.value),\n          fit: BoxFit.cover,\n        ),\n      );\n    }\n\n    if (type == PageStyleCoverImageType.pureColor) {\n      return Container(\n        height: height,\n        width: double.infinity,\n        color: cover.value.coverColor(context),\n      );\n    }\n\n    if (type == PageStyleCoverImageType.gradientColor) {\n      return Container(\n        height: height,\n        width: double.infinity,\n        decoration: BoxDecoration(\n          gradient: FlowyGradientColor.fromId(cover.value).linear,\n        ),\n      );\n    }\n\n    if (type == PageStyleCoverImageType.localImage) {\n      return SizedBox(\n        height: height,\n        width: double.infinity,\n        child: Image.file(\n          File(cover.value),\n          fit: BoxFit.cover,\n        ),\n      );\n    }\n\n    return SizedBox(\n      height: naviBarHeight,\n      width: double.infinity,\n    );\n  }\n\n  void _unfocus() {\n    final selection = selectionNotifier?.value;\n    if (selection != null) {\n      focusNode.unfocus(disposition: UnfocusDisposition.previouslyFocusedChild);\n    }\n  }\n\n  void _rename(String name) {\n    scrollController.position.jumpTo(0);\n    context.read<ViewBloc>().add(ViewEvent.rename(name));\n  }\n\n  Future<void> _createNewLine() async {\n    focusNode.unfocus();\n\n    final selection = textEditingController.selection;\n    final text = textEditingController.text;\n    // split the text into two lines based on the cursor position\n    final parts = [\n      text.substring(0, selection.baseOffset),\n      text.substring(selection.baseOffset),\n    ];\n    textEditingController.text = parts[0];\n\n    final editorState = context.read<DocumentBloc>().state.editorState;\n    if (editorState == null) {\n      Log.info('editorState is null when creating new line');\n      return;\n    }\n\n    final transaction = editorState.transaction;\n    transaction.insertNode([0], paragraphNode(text: parts[1]));\n    await editorState.apply(transaction);\n\n    // update selection instead of using afterSelection in transaction,\n    //  because it will cause the cursor to jump\n    await editorState.updateSelectionWithReason(\n      Selection.collapsed(Position(path: [0])),\n      // trigger the keyboard service.\n      reason: SelectionUpdateReason.uiEvent,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart",
    "content": "import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_listener.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport '../../../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\n\npart 'document_immersive_cover_bloc.freezed.dart';\n\nclass DocumentImmersiveCoverBloc\n    extends Bloc<DocumentImmersiveCoverEvent, DocumentImmersiveCoverState> {\n  DocumentImmersiveCoverBloc({\n    required this.view,\n  })  : _viewListener = ViewListener(viewId: view.id),\n        super(DocumentImmersiveCoverState.initial()) {\n    on<DocumentImmersiveCoverEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            final latestView = await ViewBackendService.getView(view.id);\n            if (isClosed) return;\n            add(\n              DocumentImmersiveCoverEvent.updateCoverAndIcon(\n                latestView.fold(\n                  (s) => s.cover,\n                  (e) => view.cover,\n                ),\n                EmojiIconData.fromViewIconPB(view.icon),\n                view.name,\n              ),\n            );\n            _viewListener?.start(\n              onViewUpdated: (view) {\n                add(\n                  DocumentImmersiveCoverEvent.updateCoverAndIcon(\n                    view.cover,\n                    EmojiIconData.fromViewIconPB(view.icon),\n                    view.name,\n                  ),\n                );\n              },\n            );\n          },\n          updateCoverAndIcon: (cover, icon, name) {\n            emit(\n              state.copyWith(\n                icon: icon,\n                cover: cover ?? state.cover,\n                name: name ?? state.name,\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  final ViewPB view;\n  final ViewListener? _viewListener;\n\n  @override\n  Future<void> close() {\n    _viewListener?.stop();\n    return super.close();\n  }\n}\n\n@freezed\nclass DocumentImmersiveCoverEvent with _$DocumentImmersiveCoverEvent {\n  const factory DocumentImmersiveCoverEvent.initial() = Initial;\n\n  const factory DocumentImmersiveCoverEvent.updateCoverAndIcon(\n    PageStyleCover? cover,\n    EmojiIconData? icon,\n    String? name,\n  ) = UpdateCoverAndIcon;\n}\n\n@freezed\nclass DocumentImmersiveCoverState with _$DocumentImmersiveCoverState {\n  const factory DocumentImmersiveCoverState({\n    @Default(null) EmojiIconData? icon,\n    required PageStyleCover cover,\n    @Default('') String name,\n  }) = _DocumentImmersiveCoverState;\n\n  factory DocumentImmersiveCoverState.initial() => DocumentImmersiveCoverState(\n        cover: PageStyleCover.none(),\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/database/widgets/database_view_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/compact_mode_event.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass DatabaseBlockKeys {\n  const DatabaseBlockKeys._();\n\n  static const String gridType = 'grid';\n  static const String boardType = 'board';\n  static const String calendarType = 'calendar';\n\n  static const String parentID = 'parent_id';\n  static const String viewID = 'view_id';\n  static const String enableCompactMode = 'enable_compact_mode';\n}\n\nconst overflowTypes = {\n  DatabaseBlockKeys.gridType,\n  DatabaseBlockKeys.boardType,\n};\n\nclass DatabaseViewBlockComponentBuilder extends BlockComponentBuilder {\n  DatabaseViewBlockComponentBuilder({\n    super.configuration,\n  });\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    return DatabaseBlockComponentWidget(\n      key: node.key,\n      node: node,\n      configuration: configuration,\n      showActions: showActions(node),\n      actionBuilder: (context, state) => actionBuilder(\n        blockComponentContext,\n        state,\n      ),\n      actionTrailingBuilder: (context, state) => actionTrailingBuilder(\n        blockComponentContext,\n        state,\n      ),\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate => (node) =>\n      node.children.isEmpty &&\n      node.attributes[DatabaseBlockKeys.parentID] is String &&\n      node.attributes[DatabaseBlockKeys.viewID] is String;\n}\n\nclass DatabaseBlockComponentWidget extends BlockComponentStatefulWidget {\n  const DatabaseBlockComponentWidget({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n  });\n\n  @override\n  State<DatabaseBlockComponentWidget> createState() =>\n      _DatabaseBlockComponentWidgetState();\n}\n\nclass _DatabaseBlockComponentWidgetState\n    extends State<DatabaseBlockComponentWidget>\n    with BlockComponentConfigurable {\n  @override\n  Node get node => widget.node;\n\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  late StreamSubscription<CompactModeEvent> compactModeSubscription;\n  EditorState? editorState;\n\n  @override\n  void initState() {\n    super.initState();\n    compactModeSubscription =\n        compactModeEventBus.on<CompactModeEvent>().listen((event) {\n      if (event.id != node.id) return;\n      final newAttributes = {\n        ...node.attributes,\n        DatabaseBlockKeys.enableCompactMode: event.enable,\n      };\n      final theEditorState = editorState;\n      if (theEditorState == null) return;\n      final transaction = theEditorState.transaction;\n      transaction.updateNode(node, newAttributes);\n      theEditorState.apply(transaction);\n    });\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n    compactModeSubscription.cancel();\n    editorState = null;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final editorState = Provider.of<EditorState>(context, listen: false);\n    this.editorState = editorState;\n    Widget child = BuiltInPageWidget(\n      node: widget.node,\n      editorState: editorState,\n      builder: (view) => Provider.value(\n        value: ReferenceState(true),\n        child: DatabaseViewWidget(\n          key: ValueKey(view.id),\n          view: view,\n          actionBuilder: widget.actionBuilder,\n          showActions: widget.showActions,\n          node: widget.node,\n        ),\n      ),\n    );\n\n    child = FocusScope(\n      skipTraversal: true,\n      onFocusChange: (value) {\n        if (value && keepEditorFocusNotifier.value == 0) {\n          context.read<EditorState>().selection = null;\n        }\n      },\n      child: child,\n    );\n\n    if (!editorState.editable) {\n      child = IgnorePointer(\n        child: child,\n      );\n    }\n\n    return child;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/inline_database_menu_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nSelectionMenuItem inlineGridMenuItem(DocumentBloc documentBloc) =>\n    SelectionMenuItem(\n      getName: LocaleKeys.document_slashMenu_grid_createANewGrid.tr,\n      icon: (editorState, onSelected, style) => SelectableSvgWidget(\n        data: FlowySvgs.grid_s,\n        isSelected: onSelected,\n        style: style,\n      ),\n      keywords: ['grid', 'database'],\n      handler: (editorState, menuService, context) async {\n        // create the view inside current page\n        final parentViewId = documentBloc.documentId;\n        final value = await ViewBackendService.createView(\n          parentViewId: parentViewId,\n          name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),\n          layoutType: ViewLayoutPB.Grid,\n        );\n        value.map((r) => editorState.insertInlinePage(parentViewId, r));\n      },\n    );\n\nSelectionMenuItem inlineBoardMenuItem(DocumentBloc documentBloc) =>\n    SelectionMenuItem(\n      getName: LocaleKeys.document_slashMenu_board_createANewBoard.tr,\n      icon: (editorState, onSelected, style) => SelectableSvgWidget(\n        data: FlowySvgs.board_s,\n        isSelected: onSelected,\n        style: style,\n      ),\n      keywords: ['board', 'kanban', 'database'],\n      handler: (editorState, menuService, context) async {\n        // create the view inside current page\n        final parentViewId = documentBloc.documentId;\n        final value = await ViewBackendService.createView(\n          parentViewId: parentViewId,\n          name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),\n          layoutType: ViewLayoutPB.Board,\n        );\n        value.map((r) => editorState.insertInlinePage(parentViewId, r));\n      },\n    );\n\nSelectionMenuItem inlineCalendarMenuItem(DocumentBloc documentBloc) =>\n    SelectionMenuItem(\n      getName: LocaleKeys.document_slashMenu_calendar_createANewCalendar.tr,\n      icon: (editorState, onSelected, style) => SelectableSvgWidget(\n        data: FlowySvgs.date_s,\n        isSelected: onSelected,\n        style: style,\n      ),\n      keywords: ['calendar', 'database'],\n      handler: (editorState, menuService, context) async {\n        // create the view inside current page\n        final parentViewId = documentBloc.documentId;\n        final value = await ViewBackendService.createView(\n          parentViewId: parentViewId,\n          name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),\n          layoutType: ViewLayoutPB.Calendar,\n        );\n        value.map((r) => editorState.insertInlinePage(parentViewId, r));\n      },\n    );\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/referenced_database_menu_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\n// Document Reference\n\nSelectionMenuItem referencedDocumentMenuItem = SelectionMenuItem(\n  getName: LocaleKeys.document_plugins_referencedDocument.tr,\n  icon: (editorState, onSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.icon_document_s,\n    isSelected: onSelected,\n    style: style,\n  ),\n  keywords: ['page', 'notes', 'referenced page', 'referenced document'],\n  handler: (editorState, menuService, context) => showLinkToPageMenu(\n    editorState,\n    menuService,\n    pageType: ViewLayoutPB.Document,\n  ),\n);\n\n// Database References\n\nSelectionMenuItem referencedGridMenuItem = SelectionMenuItem(\n  getName: LocaleKeys.document_plugins_referencedGrid.tr,\n  icon: (editorState, onSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.grid_s,\n    isSelected: onSelected,\n    style: style,\n  ),\n  keywords: ['referenced', 'grid', 'database'],\n  handler: (editorState, menuService, context) => showLinkToPageMenu(\n    editorState,\n    menuService,\n    pageType: ViewLayoutPB.Grid,\n  ),\n);\n\nSelectionMenuItem referencedBoardMenuItem = SelectionMenuItem(\n  getName: LocaleKeys.document_plugins_referencedBoard.tr,\n  icon: (editorState, onSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.board_s,\n    isSelected: onSelected,\n    style: style,\n  ),\n  keywords: ['referenced', 'board', 'kanban'],\n  handler: (editorState, menuService, context) => showLinkToPageMenu(\n    editorState,\n    menuService,\n    pageType: ViewLayoutPB.Board,\n  ),\n);\n\nSelectionMenuItem referencedCalendarMenuItem = SelectionMenuItem(\n  getName: LocaleKeys.document_plugins_referencedCalendar.tr,\n  icon: (editorState, onSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.date_s,\n    isSelected: onSelected,\n    style: style,\n  ),\n  keywords: ['referenced', 'calendar', 'database'],\n  handler: (editorState, menuService, context) => showLinkToPageMenu(\n    editorState,\n    menuService,\n    pageType: ViewLayoutPB.Calendar,\n  ),\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/delta/text_delta_extension.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\ntypedef MentionPageNameGetter = Future<String> Function(String pageId);\n\nextension TextDeltaExtension on Delta {\n  /// Convert the delta to a text string.\n  ///\n  /// Unlike the [toPlainText], this method will keep the mention text\n  /// such as mentioned page name, mentioned block content.\n  ///\n  /// If the mentioned page or mentioned block not found, it will downgrade to\n  /// the default plain text.\n  Future<String> toText({\n    required MentionPageNameGetter getMentionPageName,\n  }) async {\n    final defaultPlainText = toPlainText();\n\n    String text = '';\n    final ops = iterator;\n    while (ops.moveNext()) {\n      final op = ops.current;\n      final attributes = op.attributes;\n      if (op is TextInsert) {\n        // if the text is '\\$', it means the block text is empty,\n        //  the real data is in the attributes\n        if (op.text == MentionBlockKeys.mentionChar) {\n          final mention = attributes?[MentionBlockKeys.mention];\n          final mentionPageId = mention?[MentionBlockKeys.pageId];\n          final mentionType = mention?[MentionBlockKeys.type];\n          if (mentionPageId != null) {\n            text += await getMentionPageName(mentionPageId);\n            continue;\n          } else if (mentionType == MentionType.externalLink.name) {\n            final url = mention?[MentionBlockKeys.url] ?? '';\n            final info = await LinkInfoCache.get(url);\n            text += info?.title ?? url;\n            continue;\n          }\n        }\n\n        text += op.text;\n      } else {\n        // if the delta contains other types of operations,\n        // return the default plain text\n        return defaultPlainText;\n      }\n    }\n\n    return text;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n// ignore: implementation_imports\nimport 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/overlay_util.dart';\nimport 'package:flutter/material.dart';\n\nclass ColorPicker extends StatefulWidget {\n  const ColorPicker({\n    super.key,\n    required this.title,\n    required this.selectedColorHex,\n    required this.onSubmittedColorHex,\n    required this.colorOptions,\n    this.resetText,\n    this.customColorHex,\n    this.resetIconName,\n    this.showClearButton = false,\n  });\n\n  final String title;\n  final String? selectedColorHex;\n  final String? customColorHex;\n  final void Function(String? color, bool isCustomColor) onSubmittedColorHex;\n  final String? resetText;\n  final String? resetIconName;\n  final bool showClearButton;\n\n  final List<ColorOption> colorOptions;\n\n  @override\n  State<ColorPicker> createState() => _ColorPickerState();\n}\n\nclass _ColorPickerState extends State<ColorPicker> {\n  final TextEditingController _colorHexController = TextEditingController();\n  final TextEditingController _colorOpacityController = TextEditingController();\n\n  @override\n  void initState() {\n    super.initState();\n    final selectedColorHex = widget.selectedColorHex,\n        customColorHex = widget.customColorHex;\n    _colorHexController.text =\n        _extractColorHex(customColorHex ?? selectedColorHex) ?? 'FFFFFF';\n    _colorOpacityController.text =\n        _convertHexToOpacity(customColorHex ?? selectedColorHex) ?? '100';\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return basicOverlay(\n      context,\n      width: 300,\n      height: 250,\n      children: [\n        EditorOverlayTitle(text: widget.title),\n        const SizedBox(height: 6),\n        widget.showClearButton &&\n                widget.resetText != null &&\n                widget.resetIconName != null\n            ? ResetColorButton(\n                resetText: widget.resetText!,\n                resetIconName: widget.resetIconName!,\n                onPressed: (color) =>\n                    widget.onSubmittedColorHex.call(color, false),\n              )\n            : const SizedBox.shrink(),\n        CustomColorItem(\n          colorController: _colorHexController,\n          opacityController: _colorOpacityController,\n          onSubmittedColorHex: (color) =>\n              widget.onSubmittedColorHex.call(color, true),\n        ),\n        _buildColorItems(\n          widget.colorOptions,\n          widget.selectedColorHex,\n        ),\n      ],\n    );\n  }\n\n  Widget _buildColorItems(\n    List<ColorOption> options,\n    String? selectedColor,\n  ) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: options\n          .map((e) => _buildColorItem(e, e.colorHex == selectedColor))\n          .toList(),\n    );\n  }\n\n  Widget _buildColorItem(ColorOption option, bool isChecked) {\n    return SizedBox(\n      height: 36,\n      child: TextButton.icon(\n        onPressed: () {\n          widget.onSubmittedColorHex(option.colorHex, false);\n        },\n        icon: SizedBox.square(\n          dimension: 12,\n          child: Container(\n            decoration: BoxDecoration(\n              color: option.colorHex.tryToColor(),\n              shape: BoxShape.circle,\n            ),\n          ),\n        ),\n        style: buildOverlayButtonStyle(context),\n        label: Row(\n          mainAxisAlignment: MainAxisAlignment.spaceBetween,\n          children: [\n            Expanded(\n              child: Text(\n                option.name,\n                softWrap: false,\n                maxLines: 1,\n                overflow: TextOverflow.fade,\n                style: TextStyle(\n                  color: Theme.of(context).textTheme.labelLarge?.color,\n                ),\n              ),\n            ),\n            // checkbox\n            if (isChecked) const FlowySvg(FlowySvgs.toolbar_check_m),\n          ],\n        ),\n      ),\n    );\n  }\n\n  String? _convertHexToOpacity(String? colorHex) {\n    if (colorHex == null) return null;\n    final opacityHex = colorHex.substring(2, 4);\n    final opacity = int.parse(opacityHex, radix: 16) / 2.55;\n    return opacity.toStringAsFixed(0);\n  }\n\n  String? _extractColorHex(String? colorHex) {\n    if (colorHex == null) return null;\n    return colorHex.substring(4);\n  }\n}\n\nclass ResetColorButton extends StatelessWidget {\n  const ResetColorButton({\n    super.key,\n    required this.resetText,\n    required this.resetIconName,\n    required this.onPressed,\n  });\n\n  final Function(String? color) onPressed;\n  final String resetText;\n  final String resetIconName;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: double.infinity,\n      height: 32,\n      child: TextButton.icon(\n        onPressed: () => onPressed(null),\n        icon: EditorSvg(\n          name: resetIconName,\n          width: 13,\n          height: 13,\n          color: Theme.of(context).iconTheme.color,\n        ),\n        label: Text(\n          resetText,\n          style: TextStyle(\n            color: Theme.of(context).hintColor,\n          ),\n          textAlign: TextAlign.left,\n        ),\n        style: ButtonStyle(\n          backgroundColor: WidgetStateProperty.resolveWith<Color>(\n            (Set<WidgetState> states) {\n              if (states.contains(WidgetState.hovered)) {\n                return Theme.of(context).hoverColor;\n              }\n              return Colors.transparent;\n            },\n          ),\n          alignment: Alignment.centerLeft,\n        ),\n      ),\n    );\n  }\n}\n\nclass CustomColorItem extends StatefulWidget {\n  const CustomColorItem({\n    super.key,\n    required this.colorController,\n    required this.opacityController,\n    required this.onSubmittedColorHex,\n  });\n\n  final TextEditingController colorController;\n  final TextEditingController opacityController;\n  final void Function(String color) onSubmittedColorHex;\n\n  @override\n  State<CustomColorItem> createState() => _CustomColorItemState();\n}\n\nclass _CustomColorItemState extends State<CustomColorItem> {\n  @override\n  Widget build(BuildContext context) {\n    return ExpansionTile(\n      tilePadding: const EdgeInsets.only(left: 8),\n      shape: Border.all(\n        color: Colors.transparent,\n      ), // remove the default border when it is expanded\n      title: Row(\n        children: [\n          // color sample box\n          SizedBox.square(\n            dimension: 12,\n            child: Container(\n              decoration: BoxDecoration(\n                color: Color(\n                  int.tryParse(\n                        _combineColorHexAndOpacity(\n                          widget.colorController.text,\n                          widget.opacityController.text,\n                        ),\n                      ) ??\n                      0xFFFFFFFF,\n                ),\n                shape: BoxShape.circle,\n              ),\n            ),\n          ),\n          const SizedBox(width: 8),\n          Expanded(\n            child: Text(\n              AppFlowyEditorL10n.current.customColor,\n              style: Theme.of(context).textTheme.labelLarge,\n              // same style as TextButton.icon\n            ),\n          ),\n        ],\n      ),\n      children: [\n        const SizedBox(height: 6),\n        _customColorDetailsTextField(\n          labelText: AppFlowyEditorL10n.current.hexValue,\n          controller: widget.colorController,\n          // update the color sample box when the text changes\n          onChanged: (_) => setState(() {}),\n          onSubmitted: _submitCustomColorHex,\n        ),\n        const SizedBox(height: 10),\n        _customColorDetailsTextField(\n          labelText: AppFlowyEditorL10n.current.opacity,\n          controller: widget.opacityController,\n          // update the color sample box when the text changes\n          onChanged: (_) => setState(() {}),\n          onSubmitted: _submitCustomColorHex,\n        ),\n        const SizedBox(height: 6),\n      ],\n    );\n  }\n\n  Widget _customColorDetailsTextField({\n    required String labelText,\n    required TextEditingController controller,\n    Function(String)? onChanged,\n    Function(String)? onSubmitted,\n  }) {\n    return Padding(\n      padding: const EdgeInsets.only(right: 3),\n      child: TextField(\n        controller: controller,\n        decoration: InputDecoration(\n          labelText: labelText,\n          border: OutlineInputBorder(\n            borderSide: BorderSide(\n              color: Theme.of(context).colorScheme.outline,\n            ),\n          ),\n        ),\n        style: Theme.of(context).textTheme.bodyMedium,\n        onChanged: onChanged,\n        onSubmitted: onSubmitted,\n      ),\n    );\n  }\n\n  String _combineColorHexAndOpacity(String colorHex, String opacity) {\n    colorHex = _fixColorHex(colorHex);\n    opacity = _fixOpacity(opacity);\n    final opacityHex = (int.parse(opacity) * 2.55).round().toRadixString(16);\n    return '0x$opacityHex$colorHex';\n  }\n\n  String _fixColorHex(String colorHex) {\n    if (colorHex.length > 6) {\n      colorHex = colorHex.substring(0, 6);\n    }\n    if (int.tryParse(colorHex, radix: 16) == null) {\n      colorHex = 'FFFFFF';\n    }\n    return colorHex;\n  }\n\n  String _fixOpacity(String opacity) {\n    // if opacity is 0 - 99, return it\n    // otherwise return 100\n    final RegExp regex = RegExp('^(0|[1-9][0-9]?)');\n    if (regex.hasMatch(opacity)) {\n      return opacity;\n    } else {\n      return '100';\n    }\n  }\n\n  void _submitCustomColorHex(String value) {\n    final String color = _combineColorHexAndOpacity(\n      widget.colorController.text,\n      widget.opacityController.text,\n    );\n    widget.onSubmittedColorHex(color);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nimport 'toolbar_animation.dart';\n\nclass DesktopFloatingToolbar extends StatefulWidget {\n  const DesktopFloatingToolbar({\n    super.key,\n    required this.editorState,\n    required this.child,\n    required this.onDismiss,\n    this.enableAnimation = true,\n  });\n\n  final EditorState editorState;\n  final Widget child;\n  final VoidCallback onDismiss;\n  final bool enableAnimation;\n\n  @override\n  State<DesktopFloatingToolbar> createState() => _DesktopFloatingToolbarState();\n}\n\nclass _DesktopFloatingToolbarState extends State<DesktopFloatingToolbar> {\n  EditorState get editorState => widget.editorState;\n\n  _Position? position;\n  final toolbarController = getIt<FloatingToolbarController>();\n\n  @override\n  void initState() {\n    super.initState();\n    final selection = editorState.selection;\n    if (selection == null || selection.isCollapsed) {\n      return;\n    }\n    final selectionRect = editorState.selectionRects();\n    if (selectionRect.isEmpty) return;\n    position = calculateSelectionMenuOffset(selectionRect.first);\n    toolbarController._addCallback(dismiss);\n  }\n\n  @override\n  void dispose() {\n    toolbarController._removeCallback(dismiss);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (position == null) return Container();\n    return Positioned(\n      left: position!.left,\n      top: position!.top,\n      right: position!.right,\n      child: widget.enableAnimation\n          ? ToolbarAnimationWidget(child: widget.child)\n          : widget.child,\n    );\n  }\n\n  void dismiss() {\n    widget.onDismiss.call();\n  }\n\n  _Position calculateSelectionMenuOffset(\n    Rect rect,\n  ) {\n    const toolbarHeight = 40, topLimit = toolbarHeight + 8;\n    final bool isLongMenu = onlyShowInSingleSelectionAndTextType(editorState);\n    final editorOffset =\n        editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;\n    final editorSize = editorState.renderBox?.size ?? Size.zero;\n    final menuWidth =\n        isLongMenu ? (isNarrowWindow(editorState) ? 490.0 : 660.0) : 420.0;\n    final editorRect = editorOffset & editorSize;\n    final left = rect.left, leftStart = 50;\n    final top =\n        rect.top < topLimit ? rect.bottom + topLimit : rect.top - topLimit;\n    if (left + menuWidth > editorRect.right) {\n      return _Position(\n        editorRect.right - menuWidth,\n        top,\n        null,\n      );\n    } else if (rect.left - leftStart > 0) {\n      return _Position(rect.left - leftStart, top, null);\n    } else {\n      return _Position(rect.left, top, null);\n    }\n  }\n}\n\nclass _Position {\n  _Position(this.left, this.top, this.right);\n\n  final double? left;\n  final double? top;\n  final double? right;\n}\n\nclass FloatingToolbarController {\n  final Set<VoidCallback> _dismissCallbacks = {};\n  final Set<VoidCallback> _displayListeners = {};\n\n  void _addCallback(VoidCallback callback) {\n    _dismissCallbacks.add(callback);\n    for (final listener in Set.of(_displayListeners)) {\n      listener.call();\n    }\n  }\n\n  void _removeCallback(VoidCallback callback) =>\n      _dismissCallbacks.remove(callback);\n\n  bool get isToolbarShowing => _dismissCallbacks.isNotEmpty;\n\n  void addDisplayListener(VoidCallback listener) =>\n      _displayListeners.add(listener);\n\n  void removeDisplayListener(VoidCallback listener) =>\n      _displayListeners.remove(listener);\n\n  void hideToolbar() {\n    if (_dismissCallbacks.isEmpty) return;\n    for (final callback in _dismissCallbacks) {\n      callback.call();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart';\nimport 'package:appflowy/plugins/shared/share/constants.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'link_search_text_field.dart';\n\nclass LinkCreateMenu extends StatefulWidget {\n  const LinkCreateMenu({\n    super.key,\n    required this.editorState,\n    required this.onSubmitted,\n    required this.onDismiss,\n    required this.alignment,\n    required this.currentViewId,\n    required this.initialText,\n  });\n\n  final EditorState editorState;\n  final void Function(String link, bool isPage) onSubmitted;\n  final VoidCallback onDismiss;\n  final String currentViewId;\n  final String initialText;\n  final LinkMenuAlignment alignment;\n\n  @override\n  State<LinkCreateMenu> createState() => _LinkCreateMenuState();\n}\n\nclass _LinkCreateMenuState extends State<LinkCreateMenu> {\n  late LinkSearchTextField searchTextField = LinkSearchTextField(\n    currentViewId: widget.currentViewId,\n    initialSearchText: widget.initialText,\n    onEnter: () {\n      searchTextField.onSearchResult(\n        onLink: () => onSubmittedLink(),\n        onRecentViews: () =>\n            onSubmittedPageLink(searchTextField.currentRecentView),\n        onSearchViews: () =>\n            onSubmittedPageLink(searchTextField.currentSearchedView),\n        onEmpty: () {},\n      );\n    },\n    onEscape: widget.onDismiss,\n    onDataRefresh: () {\n      if (mounted) setState(() {});\n    },\n  );\n\n  bool get isTextfieldEnable => searchTextField.isTextfieldEnable;\n\n  String get searchText => searchTextField.searchText;\n\n  bool get showAtTop => widget.alignment.isTop;\n\n  bool showErrorText = false;\n\n  @override\n  void initState() {\n    super.initState();\n    searchTextField.requestFocus();\n    searchTextField.searchRecentViews();\n    final focusNode = searchTextField.focusNode;\n    bool hasFocus = focusNode.hasFocus;\n    focusNode.addListener(() {\n      if (hasFocus != focusNode.hasFocus && mounted) {\n        setState(() {\n          hasFocus = focusNode.hasFocus;\n        });\n      }\n    });\n  }\n\n  @override\n  void dispose() {\n    searchTextField.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: 320,\n      child: Column(\n        children: showAtTop\n            ? [\n                searchTextField.buildResultContainer(\n                  margin: EdgeInsets.only(bottom: 2),\n                  context: context,\n                  onLinkSelected: onSubmittedLink,\n                  onPageLinkSelected: onSubmittedPageLink,\n                ),\n                buildSearchContainer(),\n              ]\n            : [\n                buildSearchContainer(),\n                searchTextField.buildResultContainer(\n                  margin: EdgeInsets.only(top: 2),\n                  context: context,\n                  onLinkSelected: onSubmittedLink,\n                  onPageLinkSelected: onSubmittedPageLink,\n                ),\n              ],\n      ),\n    );\n  }\n\n  Widget buildSearchContainer() {\n    final theme = AppFlowyTheme.maybeOf(context);\n    return Container(\n      width: 320,\n      decoration: buildToolbarLinkDecoration(context),\n      padding: EdgeInsets.all(8),\n      child: ValueListenableBuilder(\n        valueListenable: searchTextField.textEditingController,\n        builder: (context, _, __) {\n          return Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Row(\n                children: [\n                  Expanded(\n                    child: searchTextField.buildTextField(context: context),\n                  ),\n                  HSpace(8),\n                  FlowyTextButton(\n                    LocaleKeys.document_toolbar_insert.tr(),\n                    mainAxisAlignment: MainAxisAlignment.center,\n                    padding: EdgeInsets.zero,\n                    constraints: BoxConstraints(maxWidth: 72, minHeight: 32),\n                    fontSize: 14,\n                    fontColor: Colors.white,\n                    fillColor: theme?.fillColorScheme.themeThick,\n                    hoverColor:\n                        theme?.fillColorScheme.themeThick.withAlpha(200),\n                    lineHeight: 20 / 14,\n                    fontWeight: FontWeight.w600,\n                    onPressed: onSubmittedLink,\n                  ),\n                ],\n              ),\n              if (showErrorText)\n                Padding(\n                  padding: const EdgeInsets.only(top: 4),\n                  child: FlowyText.regular(\n                    LocaleKeys.document_plugins_file_networkUrlInvalid.tr(),\n                    color: theme?.textColorScheme.error,\n                    fontSize: 12,\n                    figmaLineHeight: 16,\n                  ),\n                ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  void onSubmittedLink() {\n    if (!isTextfieldEnable) {\n      setState(() {\n        showErrorText = true;\n      });\n      return;\n    }\n    widget.onSubmitted(searchText, false);\n  }\n\n  void onSubmittedPageLink(ViewPB view) async {\n    final workspaceId = context\n            .read<UserWorkspaceBloc?>()\n            ?.state\n            .currentWorkspace\n            ?.workspaceId ??\n        '';\n    final link = ShareConstants.buildShareUrl(\n      workspaceId: workspaceId,\n      viewId: view.id,\n    );\n    widget.onSubmitted(link, true);\n  }\n}\n\nvoid showLinkCreateMenu(\n  BuildContext context,\n  EditorState editorState,\n  Selection selection,\n  String currentViewId,\n) {\n  if (!context.mounted) return;\n  final (left, top, right, bottom, alignment) = _getPosition(editorState);\n\n  final node = editorState.getNodeAtPath(selection.end.path);\n  if (node == null) {\n    return;\n  }\n  final selectedText = editorState.getTextInSelection(selection).join();\n\n  OverlayEntry? overlay;\n\n  void dismissOverlay() {\n    keepEditorFocusNotifier.decrease();\n    overlay?.remove();\n    overlay = null;\n  }\n\n  keepEditorFocusNotifier.increase();\n  overlay = FullScreenOverlayEntry(\n    top: top,\n    bottom: bottom,\n    left: left,\n    right: right,\n    dismissCallback: () => keepEditorFocusNotifier.decrease(),\n    builder: (context) {\n      return LinkCreateMenu(\n        alignment: alignment,\n        initialText: selectedText,\n        currentViewId: currentViewId,\n        editorState: editorState,\n        onSubmitted: (link, isPage) async {\n          await editorState.formatDelta(selection, {\n            BuiltInAttributeKey.href: link,\n            kIsPageLink: isPage,\n          });\n          await editorState.updateSelectionWithReason(\n            null,\n            reason: SelectionUpdateReason.uiEvent,\n          );\n          dismissOverlay();\n        },\n        onDismiss: dismissOverlay,\n      );\n    },\n  ).build();\n\n  Overlay.of(context, rootOverlay: true).insert(overlay!);\n}\n\n// get a proper position for link menu\n(\n  double? left,\n  double? top,\n  double? right,\n  double? bottom,\n  LinkMenuAlignment alignment,\n) _getPosition(\n  EditorState editorState,\n) {\n  final rect = editorState.selectionRects().first;\n  const menuHeight = 222.0, menuWidth = 320.0;\n\n  double? left, right, top, bottom;\n  LinkMenuAlignment alignment = LinkMenuAlignment.topLeft;\n  final editorOffset = editorState.renderBox!.localToGlobal(Offset.zero),\n      editorSize = editorState.renderBox!.size;\n  final editorBottom = editorSize.height + editorOffset.dy,\n      editorRight = editorSize.width + editorOffset.dx;\n  final overflowBottom = rect.bottom + menuHeight > editorBottom,\n      overflowTop = rect.top - menuHeight < 0,\n      overflowLeft = rect.left - menuWidth < 0,\n      overflowRight = rect.right + menuWidth > editorRight;\n\n  if (overflowTop && !overflowBottom) {\n    /// show at bottom\n    top = rect.bottom;\n  } else if (overflowBottom && !overflowTop) {\n    /// show at top\n    bottom = editorBottom - rect.top;\n  } else if (!overflowTop && !overflowBottom) {\n    /// show at bottom\n    top = rect.bottom;\n  } else {\n    top = 0;\n  }\n\n  if (overflowLeft && !overflowRight) {\n    /// show at right\n    left = rect.left;\n  } else if (overflowRight && !overflowLeft) {\n    /// show at left\n    right = editorRight - rect.right;\n  } else if (!overflowLeft && !overflowRight) {\n    /// show at right\n    left = rect.left;\n  } else {\n    left = 0;\n  }\n\n  if (left != null && top != null) {\n    alignment = LinkMenuAlignment.bottomRight;\n  } else if (left != null && bottom != null) {\n    alignment = LinkMenuAlignment.topRight;\n  } else if (right != null && top != null) {\n    alignment = LinkMenuAlignment.bottomLeft;\n  } else if (right != null && bottom != null) {\n    alignment = LinkMenuAlignment.topLeft;\n  }\n\n  return (left, top, right, bottom, alignment);\n}\n\nShapeDecoration buildToolbarLinkDecoration(\n  BuildContext context, {\n  double radius = 12.0,\n}) {\n  final theme = AppFlowyTheme.of(context);\n  return ShapeDecoration(\n    color: theme.surfaceColorScheme.primary,\n    shape: RoundedRectangleBorder(\n      borderRadius: BorderRadius.circular(radius),\n    ),\n    shadows: theme.shadow.small,\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart';\nimport 'package:appflowy/plugins/shared/share/constants.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n// ignore: implementation_imports\nimport 'package:appflowy_editor/src/editor/util/link_util.dart';\nimport 'package:flutter/services.dart';\nimport 'link_create_menu.dart';\nimport 'link_search_text_field.dart';\nimport 'link_styles.dart';\n\nclass LinkEditMenu extends StatefulWidget {\n  const LinkEditMenu({\n    super.key,\n    required this.linkInfo,\n    required this.onDismiss,\n    required this.onApply,\n    required this.onRemoveLink,\n    required this.currentViewId,\n  });\n\n  final LinkInfo linkInfo;\n  final ValueChanged<LinkInfo> onApply;\n  final ValueChanged<LinkInfo> onRemoveLink;\n  final VoidCallback onDismiss;\n  final String currentViewId;\n\n  @override\n  State<LinkEditMenu> createState() => _LinkEditMenuState();\n}\n\nclass _LinkEditMenuState extends State<LinkEditMenu> {\n  ValueChanged<LinkInfo> get onRemoveLink => widget.onRemoveLink;\n\n  VoidCallback get onDismiss => widget.onDismiss;\n\n  late TextEditingController linkNameController =\n      TextEditingController(text: linkInfo.name);\n  late FocusNode textFocusNode = FocusNode(onKeyEvent: onFocusKeyEvent);\n  late FocusNode menuFocusNode = FocusNode(onKeyEvent: onFocusKeyEvent);\n  late LinkInfo linkInfo = widget.linkInfo;\n  late LinkSearchTextField searchTextField;\n  bool isShowingSearchResult = false;\n  ViewPB? currentView;\n  bool showErrorText = false;\n\n  AppFlowyThemeData? get theme => AppFlowyTheme.maybeOf(context);\n\n  @override\n  void initState() {\n    super.initState();\n    final isPageLink = linkInfo.isPage;\n    if (isPageLink) getPageView();\n    searchTextField = LinkSearchTextField(\n      initialSearchText: isPageLink ? '' : linkInfo.link,\n      initialViewId: linkInfo.viewId,\n      currentViewId: widget.currentViewId,\n      onEnter: onConfirm,\n      onEscape: () {\n        if (isShowingSearchResult) {\n          hideSearchResult();\n        } else {\n          onDismiss();\n        }\n      },\n      onDataRefresh: () {\n        if (mounted) setState(() {});\n      },\n    )..searchRecentViews();\n    makeSureHasFocus();\n  }\n\n  @override\n  void dispose() {\n    linkNameController.dispose();\n    textFocusNode.dispose();\n    menuFocusNode.dispose();\n    searchTextField.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final showingRecent =\n        searchTextField.showingRecent && isShowingSearchResult;\n    final errorHeight = showErrorText ? 20.0 : 0.0;\n    final theme = AppFlowyTheme.of(context);\n    return GestureDetector(\n      onTap: onDismiss,\n      child: Focus(\n        focusNode: menuFocusNode,\n        child: Container(\n          width: 400,\n          height: 250 + (showingRecent ? 32 : 0),\n          color: Colors.white.withAlpha(1),\n          child: Stack(\n            children: [\n              GestureDetector(\n                onTap: hideSearchResult,\n                child: Container(\n                  width: 400,\n                  height: 192 + errorHeight,\n                  decoration: buildToolbarLinkDecoration(context),\n                ),\n              ),\n              Positioned(\n                top: 16,\n                left: 20,\n                child: FlowyText.semibold(\n                  LocaleKeys.document_toolbar_pageOrURL.tr(),\n                  color: theme.textColorScheme.tertiary,\n                  fontSize: 12,\n                  figmaLineHeight: 16,\n                ),\n              ),\n              Positioned(\n                top: 80 + errorHeight,\n                left: 20,\n                child: FlowyText.semibold(\n                  LocaleKeys.document_toolbar_linkName.tr(),\n                  color: theme.textColorScheme.tertiary,\n                  fontSize: 12,\n                  figmaLineHeight: 16,\n                ),\n              ),\n              Positioned(\n                top: 144 + errorHeight,\n                left: 20,\n                child: buildButtons(),\n              ),\n              Positioned(\n                top: 100 + errorHeight,\n                left: 20,\n                child: buildNameTextField(),\n              ),\n              Positioned(\n                top: 36,\n                left: 20,\n                child: buildLinkField(),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget buildLinkField() {\n    final showPageView = linkInfo.isPage && !isShowingSearchResult;\n    Widget child;\n    if (showPageView) {\n      child = buildPageView();\n    } else if (!isShowingSearchResult) {\n      child = buildLinkView();\n    } else {\n      return SizedBox(\n        width: 360,\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            SizedBox(\n              width: 360,\n              height: 32,\n              child: searchTextField.buildTextField(\n                autofocus: true,\n                context: context,\n              ),\n            ),\n            VSpace(6),\n            searchTextField.buildResultContainer(\n              context: context,\n              width: 360,\n              onPageLinkSelected: onPageSelected,\n              onLinkSelected: onLinkSelected,\n            ),\n          ],\n        ),\n      );\n    }\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        child,\n        if (showErrorText)\n          Padding(\n            padding: const EdgeInsets.only(top: 4),\n            child: FlowyText.regular(\n              LocaleKeys.document_plugins_file_networkUrlInvalid.tr(),\n              color: theme?.textColorScheme.error,\n              fontSize: 12,\n              figmaLineHeight: 16,\n            ),\n          ),\n      ],\n    );\n  }\n\n  Widget buildButtons() {\n    return GestureDetector(\n      onTap: hideSearchResult,\n      child: SizedBox(\n        width: 360,\n        height: 32,\n        child: Row(\n          children: [\n            FlowyIconButton(\n              icon: FlowySvg(FlowySvgs.toolbar_link_unlink_m),\n              width: 32,\n              height: 32,\n              tooltipText: LocaleKeys.editor_removeLink.tr(),\n              preferBelow: false,\n              decoration: BoxDecoration(\n                borderRadius: BorderRadius.all(Radius.circular(8)),\n                border: Border.all(color: LinkStyle.borderColor(context)),\n              ),\n              onPressed: () => onRemoveLink.call(linkInfo),\n            ),\n            Spacer(),\n            DecoratedBox(\n              decoration: BoxDecoration(\n                borderRadius: BorderRadius.all(Radius.circular(8)),\n                border: Border.all(color: LinkStyle.borderColor(context)),\n              ),\n              child: FlowyTextButton(\n                LocaleKeys.button_cancel.tr(),\n                padding: EdgeInsets.zero,\n                mainAxisAlignment: MainAxisAlignment.center,\n                constraints: BoxConstraints(maxWidth: 78, minHeight: 32),\n                fontSize: 14,\n                lineHeight: 20 / 14,\n                fontColor: theme?.textColorScheme.primary,\n                fillColor: Colors.transparent,\n                fontWeight: FontWeight.w400,\n                onPressed: onDismiss,\n              ),\n            ),\n            HSpace(12),\n            ValueListenableBuilder(\n              valueListenable: linkNameController,\n              builder: (context, _, __) {\n                return FlowyTextButton(\n                  LocaleKeys.settings_appearance_documentSettings_apply.tr(),\n                  padding: EdgeInsets.zero,\n                  mainAxisAlignment: MainAxisAlignment.center,\n                  constraints: BoxConstraints(maxWidth: 78, minHeight: 32),\n                  fontSize: 14,\n                  lineHeight: 20 / 14,\n                  hoverColor: theme?.fillColorScheme.themeThick.withAlpha(200),\n                  fontColor: Colors.white,\n                  fillColor: theme?.fillColorScheme.themeThick,\n                  fontWeight: FontWeight.w400,\n                  onPressed: onApply,\n                );\n              },\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget buildNameTextField() {\n    return SizedBox(\n      width: 360,\n      height: 32,\n      child: TextFormField(\n        autovalidateMode: AutovalidateMode.onUserInteraction,\n        focusNode: textFocusNode,\n        autofocus: true,\n        textAlign: TextAlign.left,\n        controller: linkNameController,\n        style: TextStyle(\n          fontSize: 14,\n          height: 20 / 14,\n          fontWeight: FontWeight.w400,\n        ),\n        onChanged: (text) {\n          linkInfo = LinkInfo(\n            name: text,\n            link: linkInfo.link,\n            isPage: linkInfo.isPage,\n          );\n        },\n        decoration: LinkStyle.buildLinkTextFieldInputDecoration(\n          LocaleKeys.document_toolbar_linkNameHint.tr(),\n          context,\n        ),\n      ),\n    );\n  }\n\n  Widget buildPageView() {\n    late Widget child;\n    final view = currentView;\n    if (view == null) {\n      child = Center(\n        child: SizedBox.fromSize(\n          size: Size(10, 10),\n          child: CircularProgressIndicator(),\n        ),\n      );\n    } else {\n      final viewName = view.name;\n      final displayName = viewName.isEmpty\n          ? LocaleKeys.document_title_placeholder.tr()\n          : viewName;\n      child = GestureDetector(\n        onTap: showSearchResult,\n        child: MouseRegion(\n          cursor: SystemMouseCursors.click,\n          child: FlowyTooltip(\n            preferBelow: false,\n            message: displayName,\n            child: Container(\n              height: 32,\n              padding: EdgeInsets.fromLTRB(8, 0, 8, 0),\n              child: Row(\n                children: [\n                  searchTextField.buildIcon(view),\n                  HSpace(4),\n                  Flexible(\n                    child: FlowyText.regular(\n                      displayName,\n                      overflow: TextOverflow.ellipsis,\n                      figmaLineHeight: 20,\n                      fontSize: 14,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      );\n    }\n    return Container(\n      width: 360,\n      height: 32,\n      decoration: buildDecoration(),\n      child: child,\n    );\n  }\n\n  Widget buildLinkView() {\n    return Container(\n      width: 360,\n      height: 32,\n      decoration: buildDecoration(),\n      child: FlowyTooltip(\n        preferBelow: false,\n        message: linkInfo.link,\n        child: GestureDetector(\n          onTap: showSearchResult,\n          child: MouseRegion(\n            cursor: SystemMouseCursors.click,\n            child: Padding(\n              padding: EdgeInsets.fromLTRB(8, 6, 8, 6),\n              child: Row(\n                children: [\n                  FlowySvg(FlowySvgs.toolbar_link_earth_m),\n                  HSpace(8),\n                  Flexible(\n                    child: FlowyText.regular(\n                      linkInfo.link,\n                      overflow: TextOverflow.ellipsis,\n                      figmaLineHeight: 20,\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  KeyEventResult onFocusKeyEvent(FocusNode node, KeyEvent key) {\n    if (key is! KeyDownEvent) return KeyEventResult.ignored;\n    if (key.logicalKey == LogicalKeyboardKey.enter) {\n      onApply();\n      return KeyEventResult.handled;\n    } else if (key.logicalKey == LogicalKeyboardKey.escape) {\n      onDismiss();\n      return KeyEventResult.handled;\n    }\n    return KeyEventResult.ignored;\n  }\n\n  Future<void> makeSureHasFocus() async {\n    final focusNode = textFocusNode;\n    if (!mounted || focusNode.hasFocus) return;\n    focusNode.requestFocus();\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      makeSureHasFocus();\n    });\n  }\n\n  void onApply() {\n    if (isShowingSearchResult) {\n      onConfirm();\n      return;\n    }\n    if (linkInfo.link.isEmpty) {\n      widget.onRemoveLink(linkInfo);\n      return;\n    }\n    if (linkInfo.link.isEmpty || !isUri(linkInfo.link)) {\n      setState(() {\n        showErrorText = true;\n      });\n      return;\n    }\n    widget.onApply.call(linkInfo);\n    onDismiss();\n  }\n\n  void onConfirm() {\n    searchTextField.onSearchResult(\n      onLink: onLinkSelected,\n      onRecentViews: () => onPageSelected(searchTextField.currentRecentView),\n      onSearchViews: () => onPageSelected(searchTextField.currentSearchedView),\n      onEmpty: () {\n        searchTextField.unfocus();\n      },\n    );\n    menuFocusNode.requestFocus();\n  }\n\n  Future<void> getPageView() async {\n    if (!linkInfo.isPage) return;\n    final (view, isInTrash, isDeleted) =\n        await ViewBackendService.getMentionPageStatus(linkInfo.viewId);\n    if (mounted) {\n      setState(() {\n        currentView = view;\n      });\n    }\n  }\n\n  void showSearchResult() {\n    setState(() {\n      if (linkInfo.isPage) searchTextField.updateText('');\n      isShowingSearchResult = true;\n      searchTextField.requestFocus();\n    });\n  }\n\n  void hideSearchResult() {\n    setState(() {\n      isShowingSearchResult = false;\n      searchTextField.unfocus();\n      textFocusNode.unfocus();\n    });\n  }\n\n  void onLinkSelected() {\n    if (mounted) {\n      linkInfo = LinkInfo(\n        name: linkInfo.name,\n        link: searchTextField.searchText,\n      );\n      hideSearchResult();\n    }\n  }\n\n  Future<void> onPageSelected(ViewPB view) async {\n    currentView = view;\n    final link = ShareConstants.buildShareUrl(\n      workspaceId: await UserBackendService.getCurrentWorkspace().fold(\n        (s) => s.id,\n        (f) => '',\n      ),\n      viewId: view.id,\n    );\n    linkInfo = LinkInfo(\n      name: linkInfo.name,\n      link: link,\n      isPage: true,\n    );\n    searchTextField.updateText(linkInfo.link);\n    if (mounted) {\n      setState(() {\n        isShowingSearchResult = false;\n        searchTextField.unfocus();\n      });\n    }\n  }\n\n  BoxDecoration buildDecoration() => BoxDecoration(\n        borderRadius: BorderRadius.circular(8),\n        border: Border.all(color: LinkStyle.borderColor(context)),\n      );\n}\n\nclass LinkInfo {\n  LinkInfo({this.isPage = false, required this.name, required this.link});\n\n  final bool isPage;\n  final String name;\n  final String link;\n\n  Attributes toAttribute() =>\n      {AppFlowyRichTextKeys.href: link, kIsPageLink: isPage};\n\n  String get viewId => isPage ? link.split('/').lastOrNull ?? '' : '';\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_extension.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nextension LinkExtension on EditorState {\n  void removeLink(Selection selection) {\n    final node = getNodeAtPath(selection.end.path);\n    if (node == null) {\n      return;\n    }\n    final index = selection.normalized.startIndex;\n    final length = selection.length;\n    final transaction = this.transaction\n      ..formatText(\n        node,\n        index,\n        length,\n        {\n          BuiltInAttributeKey.href: null,\n          kIsPageLink: null,\n        },\n      );\n    apply(transaction);\n  }\n\n  void applyLink(Selection selection, LinkInfo info) {\n    final node = getNodeAtPath(selection.start.path);\n    if (node == null) return;\n    final transaction = this.transaction;\n    final linkName = info.name.isEmpty ? info.link : info.name;\n    transaction.replaceText(\n      node,\n      selection.startIndex,\n      selection.length,\n      linkName,\n      attributes: info.toAttribute(),\n    );\n    apply(transaction);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart",
    "content": "import 'dart:math';\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/link_toolbar_item.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'link_create_menu.dart';\nimport 'link_edit_menu.dart';\nimport 'link_extension.dart';\n\nclass LinkHoverTrigger extends StatefulWidget {\n  const LinkHoverTrigger({\n    super.key,\n    required this.editorState,\n    required this.selection,\n    required this.node,\n    required this.attribute,\n    required this.size,\n    this.delayToShow = const Duration(milliseconds: 50),\n    this.delayToHide = const Duration(milliseconds: 300),\n  });\n\n  final EditorState editorState;\n  final Selection selection;\n  final Node node;\n  final Attributes attribute;\n  final Size size;\n  final Duration delayToShow;\n  final Duration delayToHide;\n\n  @override\n  State<LinkHoverTrigger> createState() => _LinkHoverTriggerState();\n}\n\nclass _LinkHoverTriggerState extends State<LinkHoverTrigger> {\n  final hoverMenuController = PopoverController();\n  final editMenuController = PopoverController();\n  final toolbarController = getIt<FloatingToolbarController>();\n  bool isHoverMenuShowing = false;\n  bool isHoverMenuHovering = false;\n  bool isHoverTriggerHovering = false;\n\n  Size get size => widget.size;\n\n  EditorState get editorState => widget.editorState;\n\n  Selection get selection => widget.selection;\n\n  Attributes get attribute => widget.attribute;\n\n  late HoverTriggerKey triggerKey = HoverTriggerKey(widget.node.id, selection);\n\n  @override\n  void initState() {\n    super.initState();\n    getIt<LinkHoverTriggers>()._add(triggerKey, showLinkHoverMenu);\n    toolbarController.addDisplayListener(onToolbarShow);\n  }\n\n  @override\n  void dispose() {\n    hoverMenuController.close();\n    editMenuController.close();\n    getIt<LinkHoverTriggers>()._remove(triggerKey, showLinkHoverMenu);\n    toolbarController.removeDisplayListener(onToolbarShow);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final placeHolder = Container(\n      color: Colors.black.withAlpha(1),\n      width: size.width,\n      height: size.height,\n    );\n    if (UniversalPlatform.isMobile) {\n      return GestureDetector(\n        onTap: openLink,\n        onLongPress: () async {\n          await showEditLinkBottomSheet(context, selection, editorState);\n        },\n        child: placeHolder,\n      );\n    }\n    return MouseRegion(\n      cursor: SystemMouseCursors.click,\n      onEnter: (v) {\n        isHoverTriggerHovering = true;\n        Future.delayed(widget.delayToShow, () {\n          if (isHoverTriggerHovering && !isHoverMenuShowing) {\n            showLinkHoverMenu();\n          }\n        });\n      },\n      onExit: (v) {\n        isHoverTriggerHovering = false;\n        tryToDismissLinkHoverMenu();\n      },\n      child: buildHoverPopover(buildEditPopover(placeHolder)),\n    );\n  }\n\n  Widget buildHoverPopover(Widget child) {\n    return AppFlowyPopover(\n      controller: hoverMenuController,\n      direction: PopoverDirection.topWithLeftAligned,\n      offset: Offset(0, size.height),\n      onOpen: () {\n        keepEditorFocusNotifier.increase();\n        isHoverMenuShowing = true;\n      },\n      onClose: () {\n        keepEditorFocusNotifier.decrease();\n        isHoverMenuShowing = false;\n      },\n      margin: EdgeInsets.zero,\n      constraints: BoxConstraints(\n        maxWidth: max(320, size.width),\n        maxHeight: 48 + size.height,\n      ),\n      decorationColor: Colors.transparent,\n      popoverDecoration: BoxDecoration(),\n      popupBuilder: (context) => LinkHoverMenu(\n        attribute: widget.attribute,\n        triggerSize: size,\n        editable: editorState.editable,\n        onEnter: (_) {\n          isHoverMenuHovering = true;\n        },\n        onExit: (_) {\n          isHoverMenuHovering = false;\n          tryToDismissLinkHoverMenu();\n        },\n        onConvertTo: (type) => convertLinkTo(editorState, selection, type),\n        onOpenLink: openLink,\n        onCopyLink: () => copyLink(context),\n        onEditLink: showLinkEditMenu,\n        onRemoveLink: () => editorState.removeLink(selection),\n      ),\n      child: child,\n    );\n  }\n\n  Widget buildEditPopover(Widget child) {\n    final href = attribute.href ?? '',\n        isPage = attribute.isPage,\n        title = editorState.getTextInSelection(selection).join();\n    final currentViewId = context.read<DocumentBloc?>()?.documentId ?? '';\n    return AppFlowyPopover(\n      controller: editMenuController,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      offset: Offset(0, 0),\n      onOpen: () => keepEditorFocusNotifier.increase(),\n      onClose: () => keepEditorFocusNotifier.decrease(),\n      margin: EdgeInsets.zero,\n      asBarrier: true,\n      decorationColor: Colors.transparent,\n      popoverDecoration: BoxDecoration(),\n      constraints: BoxConstraints(\n        maxWidth: 400,\n        minHeight: 282,\n      ),\n      popupBuilder: (context) => LinkEditMenu(\n        currentViewId: currentViewId,\n        linkInfo: LinkInfo(name: title, link: href, isPage: isPage),\n        onDismiss: () => editMenuController.close(),\n        onApply: (info) => editorState.applyLink(selection, info),\n        onRemoveLink: (linkinfo) {\n          final replaceText =\n              linkinfo.name.isEmpty ? linkinfo.link : linkinfo.name;\n          onRemoveAndReplaceLink(editorState, selection, replaceText);\n        },\n      ),\n      child: child,\n    );\n  }\n\n  void onToolbarShow() => hoverMenuController.close();\n\n  void showLinkHoverMenu() {\n    if (UniversalPlatform.isMobile ||\n        isHoverMenuShowing ||\n        toolbarController.isToolbarShowing ||\n        !mounted) {\n      return;\n    }\n    keepEditorFocusNotifier.increase();\n    hoverMenuController.show();\n  }\n\n  void showLinkEditMenu() {\n    if (UniversalPlatform.isMobile) return;\n    keepEditorFocusNotifier.increase();\n    hoverMenuController.close();\n    editMenuController.show();\n  }\n\n  void tryToDismissLinkHoverMenu() {\n    Future.delayed(widget.delayToHide, () {\n      if (isHoverMenuHovering || isHoverTriggerHovering) {\n        return;\n      }\n      hoverMenuController.close();\n    });\n  }\n\n  Future<void> openLink() async {\n    final href = widget.attribute.href ?? '', isPage = widget.attribute.isPage;\n\n    if (isPage) {\n      final viewId = href.split('/').lastOrNull ?? '';\n      if (viewId.isEmpty) {\n        await afLaunchUrlString(href, addingHttpSchemeWhenFailed: true);\n      } else {\n        final (view, isInTrash, isDeleted) =\n            await ViewBackendService.getMentionPageStatus(viewId);\n        if (view != null) {\n          await handleMentionBlockTap(context, widget.editorState, view);\n        }\n      }\n    } else {\n      await afLaunchUrlString(href, addingHttpSchemeWhenFailed: true);\n    }\n  }\n\n  Future<void> copyLink(BuildContext context) async {\n    final href = widget.attribute.href ?? '';\n    await context.copyLink(href);\n    hoverMenuController.close();\n  }\n\n  Future<void> convertLinkTo(\n    EditorState editorState,\n    Selection selection,\n    LinkConvertMenuCommand type,\n  ) async {\n    final url = widget.attribute.href ?? '';\n    if (type == LinkConvertMenuCommand.toBookmark) {\n      await convertUrlToLinkPreview(editorState, selection, url);\n    } else if (type == LinkConvertMenuCommand.toMention) {\n      await convertUrlToMention(editorState, selection);\n    } else if (type == LinkConvertMenuCommand.toEmbed) {\n      await convertUrlToLinkPreview(\n        editorState,\n        selection,\n        url,\n        previewType: LinkEmbedKeys.embed,\n      );\n    }\n  }\n\n  void onRemoveAndReplaceLink(\n    EditorState editorState,\n    Selection selection,\n    String text,\n  ) {\n    final node = editorState.getNodeAtPath(selection.end.path);\n    if (node == null) {\n      return;\n    }\n    final index = selection.normalized.startIndex;\n    final length = selection.length;\n    final transaction = editorState.transaction\n      ..replaceText(\n        node,\n        index,\n        length,\n        text,\n        attributes: {\n          BuiltInAttributeKey.href: null,\n          kIsPageLink: null,\n        },\n      );\n    editorState.apply(transaction);\n  }\n}\n\nclass LinkHoverMenu extends StatefulWidget {\n  const LinkHoverMenu({\n    super.key,\n    required this.attribute,\n    required this.onEnter,\n    required this.onExit,\n    required this.editable,\n    required this.triggerSize,\n    required this.onCopyLink,\n    required this.onOpenLink,\n    required this.onEditLink,\n    required this.onRemoveLink,\n    required this.onConvertTo,\n  });\n\n  final Attributes attribute;\n  final PointerEnterEventListener onEnter;\n  final PointerExitEventListener onExit;\n  final Size triggerSize;\n  final VoidCallback onCopyLink;\n  final VoidCallback onOpenLink;\n  final VoidCallback onEditLink;\n  final VoidCallback onRemoveLink;\n  final bool editable;\n  final ValueChanged<LinkConvertMenuCommand> onConvertTo;\n\n  @override\n  State<LinkHoverMenu> createState() => _LinkHoverMenuState();\n}\n\nclass _LinkHoverMenuState extends State<LinkHoverMenu> {\n  ViewPB? currentView;\n  late bool isPage = widget.attribute.isPage;\n  late String href = widget.attribute.href ?? '';\n  final popoverController = PopoverController();\n  bool isConvertButtonSelected = false;\n\n  bool get editable => widget.editable;\n\n  @override\n  void initState() {\n    super.initState();\n    if (isPage) getPageView();\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n    popoverController.close();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        MouseRegion(\n          onEnter: widget.onEnter,\n          onExit: widget.onExit,\n          child: SizedBox(\n            width: max(320, widget.triggerSize.width),\n            height: 48,\n            child: Align(\n              alignment: Alignment.centerLeft,\n              child: Container(\n                width: 320,\n                height: 48,\n                decoration: buildToolbarLinkDecoration(context),\n                padding: EdgeInsets.fromLTRB(12, 8, 8, 8),\n                child: Row(\n                  children: [\n                    Expanded(child: buildLinkWidget()),\n                    Container(\n                      height: 20,\n                      width: 1,\n                      color: Color(0xffE8ECF3)\n                          .withAlpha(Theme.of(context).isLightMode ? 255 : 40),\n                      margin: EdgeInsets.symmetric(horizontal: 6),\n                    ),\n                    FlowyIconButton(\n                      icon: FlowySvg(FlowySvgs.toolbar_link_m),\n                      tooltipText: LocaleKeys.editor_copyLink.tr(),\n                      preferBelow: false,\n                      width: 36,\n                      height: 32,\n                      onPressed: widget.onCopyLink,\n                    ),\n                    FlowyIconButton(\n                      icon: FlowySvg(FlowySvgs.toolbar_link_edit_m),\n                      tooltipText: LocaleKeys.editor_editLink.tr(),\n                      hoverColor: hoverColor,\n                      preferBelow: false,\n                      width: 36,\n                      height: 32,\n                      onPressed: getTapCallback(widget.onEditLink),\n                    ),\n                    buildConvertButton(),\n                    FlowyIconButton(\n                      icon: FlowySvg(FlowySvgs.toolbar_link_unlink_m),\n                      tooltipText: LocaleKeys.editor_removeLink.tr(),\n                      hoverColor: hoverColor,\n                      preferBelow: false,\n                      width: 36,\n                      height: 32,\n                      onPressed: getTapCallback(widget.onRemoveLink),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        ),\n        MouseRegion(\n          cursor: SystemMouseCursors.click,\n          onEnter: widget.onEnter,\n          onExit: widget.onExit,\n          child: GestureDetector(\n            onTap: widget.onOpenLink,\n            child: Container(\n              width: widget.triggerSize.width,\n              height: widget.triggerSize.height,\n              color: Colors.black.withAlpha(1),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  Future<void> getPageView() async {\n    final viewId = href.split('/').lastOrNull ?? '';\n    final (view, isInTrash, isDeleted) =\n        await ViewBackendService.getMentionPageStatus(viewId);\n    if (mounted) {\n      setState(() {\n        currentView = view;\n      });\n    }\n  }\n\n  Widget buildLinkWidget() {\n    final view = currentView;\n    if (isPage && view == null) {\n      return SizedBox.square(\n        dimension: 20,\n        child: CircularProgressIndicator(),\n      );\n    }\n    String text = '';\n    if (isPage && view != null) {\n      text = view.name;\n      if (text.isEmpty) {\n        text = LocaleKeys.document_title_placeholder.tr();\n      }\n    } else {\n      text = href;\n    }\n    return FlowyTooltip(\n      message: text,\n      preferBelow: false,\n      child: FlowyText.regular(\n        text,\n        overflow: TextOverflow.ellipsis,\n        figmaLineHeight: 20,\n        fontSize: 14,\n      ),\n    );\n  }\n\n  Widget buildConvertButton() {\n    final button = FlowyIconButton(\n      icon: FlowySvg(FlowySvgs.turninto_m),\n      isSelected: isConvertButtonSelected,\n      tooltipText: LocaleKeys.editor_convertTo.tr(),\n      preferBelow: false,\n      hoverColor: hoverColor,\n      width: 36,\n      height: 32,\n      onPressed: getTapCallback(() {\n        setState(() {\n          isConvertButtonSelected = true;\n        });\n        showConvertMenu();\n      }),\n    );\n    if (!editable) return button;\n    return AppFlowyPopover(\n      offset: Offset(44, 10.0),\n      direction: PopoverDirection.bottomWithRightAligned,\n      margin: EdgeInsets.zero,\n      controller: popoverController,\n      onOpen: () => keepEditorFocusNotifier.increase(),\n      onClose: () => keepEditorFocusNotifier.decrease(),\n      popupBuilder: (context) => buildConvertMenu(),\n      child: button,\n    );\n  }\n\n  Widget buildConvertMenu() {\n    return MouseRegion(\n      onEnter: widget.onEnter,\n      onExit: widget.onExit,\n      child: Padding(\n        padding: const EdgeInsets.all(8.0),\n        child: SeparatedColumn(\n          mainAxisSize: MainAxisSize.min,\n          separatorBuilder: () => const VSpace(0.0),\n          children:\n              List.generate(LinkConvertMenuCommand.values.length, (index) {\n            final command = LinkConvertMenuCommand.values[index];\n            return SizedBox(\n              height: 36,\n              child: FlowyButton(\n                text: FlowyText(\n                  command.title,\n                  fontWeight: FontWeight.w400,\n                  figmaLineHeight: 20,\n                ),\n                onTap: () {\n                  widget.onConvertTo(command);\n                  closeConvertMenu();\n                },\n              ),\n            );\n          }),\n        ),\n      ),\n    );\n  }\n\n  Color? get hoverColor => editable ? null : Colors.transparent;\n\n  VoidCallback? getTapCallback(VoidCallback callback) {\n    if (editable) return callback;\n    return null;\n  }\n\n  void showConvertMenu() {\n    keepEditorFocusNotifier.increase();\n    popoverController.show();\n  }\n\n  void closeConvertMenu() {\n    popoverController.close();\n  }\n}\n\nclass HoverTriggerKey {\n  HoverTriggerKey(this.nodeId, this.selection);\n\n  final String nodeId;\n  final Selection selection;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is HoverTriggerKey &&\n          runtimeType == other.runtimeType &&\n          nodeId == other.nodeId &&\n          isSelectionSame(other.selection);\n\n  bool isSelectionSame(Selection other) =>\n      (selection.start == other.start && selection.end == other.end) ||\n      (selection.start == other.end && selection.end == other.start);\n\n  @override\n  int get hashCode => nodeId.hashCode ^ selection.hashCode;\n}\n\nclass LinkHoverTriggers {\n  final Map<HoverTriggerKey, Set<VoidCallback>> _map = {};\n\n  void _add(HoverTriggerKey key, VoidCallback callback) {\n    final callbacks = _map[key] ?? {};\n    callbacks.add(callback);\n    _map[key] = callbacks;\n  }\n\n  void _remove(HoverTriggerKey key, VoidCallback callback) {\n    final callbacks = _map[key] ?? {};\n    callbacks.remove(callback);\n    _map[key] = callbacks;\n  }\n\n  void call(HoverTriggerKey key) {\n    final callbacks = _map[key] ?? {};\n    if (callbacks.isEmpty) return;\n    callbacks.first.call();\n  }\n}\n\nenum LinkConvertMenuCommand {\n  toMention,\n  toBookmark,\n  toEmbed;\n\n  String get title {\n    switch (this) {\n      case toMention:\n        return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toMetion\n            .tr();\n      case toBookmark:\n        return LocaleKeys\n            .document_plugins_linkPreview_linkPreviewMenu_toBookmark\n            .tr();\n      case toEmbed:\n        return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toEmbed\n            .tr();\n    }\n  }\n\n  String get type {\n    switch (this) {\n      case toMention:\n        return MentionBlockKeys.type;\n      case toBookmark:\n        return LinkPreviewBlockKeys.type;\n      case toEmbed:\n        return LinkPreviewBlockKeys.type;\n    }\n  }\n}\n\nextension LinkExtension on BuildContext {\n  Future<void> copyLink(String link) async {\n    if (link.isEmpty) return;\n    await getIt<ClipboardService>()\n        .setData(ClipboardServiceData(plainText: link));\n    if (mounted) {\n      showToastNotification(\n        message: LocaleKeys.shareAction_copyLinkSuccess.tr(),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n// ignore: implementation_imports\nimport 'package:appflowy_editor/src/editor/util/link_util.dart';\nimport 'package:flutter/services.dart';\n\nimport 'link_create_menu.dart';\nimport 'link_styles.dart';\n\nvoid showReplaceMenu({\n  required BuildContext context,\n  required EditorState editorState,\n  required Node node,\n  String? url,\n  required LTRB ltrb,\n  required ValueChanged<String> onReplace,\n}) {\n  OverlayEntry? overlay;\n\n  void dismissOverlay() {\n    keepEditorFocusNotifier.decrease();\n    overlay?.remove();\n    overlay = null;\n  }\n\n  keepEditorFocusNotifier.increase();\n  overlay = FullScreenOverlayEntry(\n    top: ltrb.top,\n    bottom: ltrb.bottom,\n    left: ltrb.left,\n    right: ltrb.right,\n    dismissCallback: () => keepEditorFocusNotifier.decrease(),\n    builder: (context) {\n      return LinkReplaceMenu(\n        link: url ?? '',\n        onSubmitted: (link) async {\n          onReplace.call(link);\n          dismissOverlay();\n        },\n        onDismiss: dismissOverlay,\n      );\n    },\n  ).build();\n\n  Overlay.of(context, rootOverlay: true).insert(overlay!);\n}\n\nclass LinkReplaceMenu extends StatefulWidget {\n  const LinkReplaceMenu({\n    super.key,\n    required this.onSubmitted,\n    required this.link,\n    required this.onDismiss,\n  });\n\n  final ValueChanged<String> onSubmitted;\n  final VoidCallback onDismiss;\n  final String link;\n\n  @override\n  State<LinkReplaceMenu> createState() => _LinkReplaceMenuState();\n}\n\nclass _LinkReplaceMenuState extends State<LinkReplaceMenu> {\n  bool showErrorText = false;\n  late FocusNode focusNode = FocusNode(onKeyEvent: onKeyEvent);\n  late TextEditingController textEditingController =\n      TextEditingController(text: widget.link);\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      focusNode.requestFocus();\n    });\n  }\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    textEditingController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      width: 330,\n      padding: EdgeInsets.all(8),\n      decoration: buildToolbarLinkDecoration(context),\n      child: Row(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Expanded(child: buildLinkField()),\n          HSpace(8),\n          buildReplaceButton(),\n        ],\n      ),\n    );\n  }\n\n  Widget buildLinkField() {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        SizedBox(\n          height: 32,\n          child: TextFormField(\n            autovalidateMode: AutovalidateMode.onUserInteraction,\n            autofocus: true,\n            focusNode: focusNode,\n            textAlign: TextAlign.left,\n            controller: textEditingController,\n            style: TextStyle(\n              fontSize: 14,\n              height: 20 / 14,\n              fontWeight: FontWeight.w400,\n            ),\n            decoration: LinkStyle.buildLinkTextFieldInputDecoration(\n              LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_pasteHint\n                  .tr(),\n              context,\n              showErrorBorder: showErrorText,\n            ),\n          ),\n        ),\n        if (showErrorText)\n          Padding(\n            padding: const EdgeInsets.only(top: 4),\n            child: FlowyText.regular(\n              LocaleKeys.document_plugins_file_networkUrlInvalid.tr(),\n              color: AppFlowyTheme.maybeOf(context)?.textColorScheme.error,\n              fontSize: 12,\n              figmaLineHeight: 16,\n            ),\n          ),\n      ],\n    );\n  }\n\n  Widget buildReplaceButton() {\n    final fillTheme = AppFlowyTheme.maybeOf(context)?.fillColorScheme;\n    return FlowyTextButton(\n      LocaleKeys.button_replace.tr(),\n      padding: EdgeInsets.zero,\n      mainAxisAlignment: MainAxisAlignment.center,\n      constraints: BoxConstraints(maxWidth: 78, minHeight: 32),\n      fontSize: 14,\n      lineHeight: 20 / 14,\n      hoverColor: fillTheme?.themeThick.withAlpha(200),\n      fontColor: Colors.white,\n      fillColor: fillTheme?.themeThick,\n      fontWeight: FontWeight.w400,\n      onPressed: onSubmit,\n    );\n  }\n\n  void onSubmit() {\n    final link = textEditingController.text.trim();\n    if (link.isEmpty || !isUri(link)) {\n      setState(() {\n        showErrorText = true;\n      });\n      return;\n    }\n    widget.onSubmitted.call(link);\n  }\n\n  KeyEventResult onKeyEvent(FocusNode node, KeyEvent key) {\n    if (key is! KeyDownEvent) return KeyEventResult.ignored;\n    if (key.logicalKey == LogicalKeyboardKey.escape) {\n      widget.onDismiss.call();\n      return KeyEventResult.handled;\n    } else if (key.logicalKey == LogicalKeyboardKey.enter) {\n      onSubmit();\n      return KeyEventResult.handled;\n    }\n    return KeyEventResult.ignored;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_search_text_field.dart",
    "content": "import 'dart:math';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/list_extension.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/recent/cached_recent_service.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\n// ignore: implementation_imports\nimport 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart';\n// ignore: implementation_imports\nimport 'package:appflowy_editor/src/editor/util/link_util.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nimport 'link_create_menu.dart';\nimport 'link_styles.dart';\n\nclass LinkSearchTextField {\n  LinkSearchTextField({\n    this.onEscape,\n    this.onEnter,\n    this.onDataRefresh,\n    this.initialViewId = '',\n    required this.currentViewId,\n    String? initialSearchText,\n  }) : textEditingController = TextEditingController(\n          text: isUri(initialSearchText ?? '') ? initialSearchText : '',\n        );\n\n  final TextEditingController textEditingController;\n  final String initialViewId;\n  final String currentViewId;\n  final ItemScrollController searchController = ItemScrollController();\n  late FocusNode focusNode = FocusNode(onKeyEvent: onKeyEvent);\n  final List<ViewPB> searchedViews = [];\n  final List<ViewPB> recentViews = [];\n  int selectedIndex = 0;\n\n  final VoidCallback? onEscape;\n  final VoidCallback? onEnter;\n  final VoidCallback? onDataRefresh;\n\n  String get searchText => textEditingController.text;\n\n  bool get isTextfieldEnable => searchText.isNotEmpty && isUri(searchText);\n\n  bool get showingRecent => searchText.isEmpty && recentViews.isNotEmpty;\n\n  ViewPB get currentSearchedView => searchedViews[selectedIndex];\n\n  ViewPB get currentRecentView => recentViews[selectedIndex];\n\n  void dispose() {\n    textEditingController.dispose();\n    focusNode.dispose();\n    searchedViews.clear();\n    recentViews.clear();\n  }\n\n  Widget buildTextField({\n    bool autofocus = false,\n    bool showError = false,\n    required BuildContext context,\n    EdgeInsets contentPadding = const EdgeInsets.fromLTRB(8, 6, 8, 6),\n    TextStyle? textStyle,\n  }) {\n    return TextFormField(\n      autovalidateMode: AutovalidateMode.onUserInteraction,\n      autofocus: autofocus,\n      focusNode: focusNode,\n      textAlign: TextAlign.left,\n      controller: textEditingController,\n      style: textStyle ??\n          TextStyle(\n            fontSize: 14,\n            height: 20 / 14,\n            fontWeight: FontWeight.w400,\n          ),\n      onChanged: (text) {\n        if (text.isEmpty) {\n          searchedViews.clear();\n          selectedIndex = 0;\n          onDataRefresh?.call();\n        } else {\n          searchViews(text);\n        }\n      },\n      decoration: LinkStyle.buildLinkTextFieldInputDecoration(\n        LocaleKeys.document_toolbar_linkInputHint.tr(),\n        context,\n        showErrorBorder: showError,\n        contentPadding: contentPadding,\n      ),\n    );\n  }\n\n  Widget buildResultContainer({\n    EdgeInsetsGeometry? margin,\n    required BuildContext context,\n    VoidCallback? onLinkSelected,\n    ValueChanged<ViewPB>? onPageLinkSelected,\n    double width = 320.0,\n  }) {\n    return onSearchResult<Widget>(\n      onEmpty: () => SizedBox.shrink(),\n      onLink: () => Container(\n        height: 48,\n        width: width,\n        padding: EdgeInsets.all(8),\n        margin: margin,\n        decoration: buildToolbarLinkDecoration(context),\n        child: FlowyButton(\n          leftIcon: FlowySvg(FlowySvgs.toolbar_link_earth_m),\n          isSelected: true,\n          text: FlowyText.regular(\n            searchText,\n            overflow: TextOverflow.ellipsis,\n            fontSize: 14,\n            figmaLineHeight: 20,\n          ),\n          onTap: onLinkSelected,\n        ),\n      ),\n      onRecentViews: () => Container(\n        width: width,\n        height: recentViews.length.clamp(1, 5) * 32.0 + 48,\n        margin: margin,\n        padding: EdgeInsets.all(8),\n        decoration: buildToolbarLinkDecoration(context),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Container(\n              height: 32,\n              padding: EdgeInsets.all(8),\n              child: FlowyText.semibold(\n                LocaleKeys.inlineActions_recentPages.tr(),\n                color: AppFlowyTheme.of(context).textColorScheme.tertiary,\n                fontSize: 12,\n                figmaLineHeight: 16,\n              ),\n            ),\n            Flexible(\n              child: ListView.builder(\n                itemBuilder: (context, index) {\n                  final currentView = recentViews[index];\n                  return buildPageItem(\n                    currentView,\n                    index == selectedIndex,\n                    onPageLinkSelected,\n                  );\n                },\n                itemCount: recentViews.length,\n              ),\n            ),\n          ],\n        ),\n      ),\n      onSearchViews: () => Container(\n        width: width,\n        height: searchedViews.length.clamp(1, 5) * 32.0 + 16,\n        margin: margin,\n        decoration: buildToolbarLinkDecoration(context),\n        child: ScrollablePositionedList.builder(\n          padding: EdgeInsets.all(8),\n          physics: const ClampingScrollPhysics(),\n          shrinkWrap: true,\n          itemCount: searchedViews.length,\n          itemScrollController: searchController,\n          initialScrollIndex: max(0, selectedIndex),\n          itemBuilder: (context, index) {\n            final currentView = searchedViews[index];\n            return buildPageItem(\n              currentView,\n              index == selectedIndex,\n              onPageLinkSelected,\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  Widget buildPageItem(\n    ViewPB view,\n    bool isSelected,\n    ValueChanged<ViewPB>? onSubmittedPageLink,\n  ) {\n    final viewName = view.name;\n    final displayName = viewName.isEmpty\n        ? LocaleKeys.document_title_placeholder.tr()\n        : viewName;\n    final isCurrent = initialViewId == view.id;\n    return SizedBox(\n      height: 32,\n      child: FlowyButton(\n        isSelected: isSelected,\n        leftIcon: buildIcon(view, padding: EdgeInsets.zero),\n        text: FlowyText.regular(\n          displayName,\n          overflow: TextOverflow.ellipsis,\n          fontSize: 14,\n          figmaLineHeight: 20,\n        ),\n        rightIcon: isCurrent ? FlowySvg(FlowySvgs.toolbar_check_m) : null,\n        onTap: () => onSubmittedPageLink?.call(view),\n      ),\n    );\n  }\n\n  Widget buildIcon(\n    ViewPB view, {\n    EdgeInsetsGeometry padding = const EdgeInsets.only(top: 4),\n  }) {\n    if (view.icon.value.isEmpty) return view.defaultIcon(size: Size(20, 20));\n    final iconData = view.icon.toEmojiIconData();\n    return Padding(\n      padding: padding,\n      child: RawEmojiIconWidget(\n        emoji: iconData,\n        emojiSize: iconData.type == FlowyIconType.emoji ? 16 : 20,\n        lineHeight: 1,\n      ),\n    );\n  }\n\n  void requestFocus() => focusNode.requestFocus();\n\n  void unfocus() => focusNode.unfocus();\n\n  void updateText(String text) => textEditingController.text = text;\n\n  T onSearchResult<T>({\n    required ValueGetter<T> onLink,\n    required ValueGetter<T> onRecentViews,\n    required ValueGetter<T> onSearchViews,\n    required ValueGetter<T> onEmpty,\n  }) {\n    if (searchedViews.isEmpty && recentViews.isEmpty && searchText.isEmpty) {\n      return onEmpty.call();\n    }\n    if (searchedViews.isEmpty && searchText.isNotEmpty) {\n      return onLink.call();\n    }\n    if (searchedViews.isEmpty) return onRecentViews.call();\n    return onSearchViews.call();\n  }\n\n  KeyEventResult onKeyEvent(FocusNode node, KeyEvent key) {\n    if (key is! KeyDownEvent) return KeyEventResult.ignored;\n    int index = selectedIndex;\n    if (key.logicalKey == LogicalKeyboardKey.escape) {\n      onEscape?.call();\n      return KeyEventResult.handled;\n    } else if (key.logicalKey == LogicalKeyboardKey.arrowUp) {\n      index = onSearchResult(\n        onLink: () => 0,\n        onRecentViews: () {\n          int result = index - 1;\n          if (result < 0) result = recentViews.length - 1;\n          return result;\n        },\n        onSearchViews: () {\n          int result = index - 1;\n          if (result < 0) result = searchedViews.length - 1;\n          searchController.scrollTo(\n            index: result,\n            alignment: 0.5,\n            duration: const Duration(milliseconds: 300),\n          );\n          return result;\n        },\n        onEmpty: () => 0,\n      );\n      refreshIndex(index);\n      return KeyEventResult.handled;\n    } else if (key.logicalKey == LogicalKeyboardKey.arrowDown) {\n      index = onSearchResult(\n        onLink: () => 0,\n        onRecentViews: () {\n          int result = index + 1;\n          if (result >= recentViews.length) result = 0;\n          return result;\n        },\n        onSearchViews: () {\n          int result = index + 1;\n          if (result >= searchedViews.length) result = 0;\n          searchController.scrollTo(\n            index: result,\n            alignment: 0.5,\n            duration: const Duration(milliseconds: 300),\n          );\n          return result;\n        },\n        onEmpty: () => 0,\n      );\n      refreshIndex(index);\n      return KeyEventResult.handled;\n    } else if (key.logicalKey == LogicalKeyboardKey.enter) {\n      onEnter?.call();\n      return KeyEventResult.handled;\n    }\n    return KeyEventResult.ignored;\n  }\n\n  Future<void> searchRecentViews() async {\n    final recentService = getIt<CachedRecentService>();\n    final sectionViews = await recentService.recentViews();\n    final views = sectionViews\n        .unique((e) => e.item.id)\n        .map((e) => e.item)\n        .where((e) => e.id != currentViewId)\n        .take(5)\n        .toList();\n    recentViews.clear();\n    recentViews.addAll(views);\n    selectedIndex = 0;\n    onDataRefresh?.call();\n  }\n\n  Future<void> searchViews(String search) async {\n    final viewResult = await ViewBackendService.getAllViews();\n    final allViews = viewResult\n        .toNullable()\n        ?.items\n        .where(\n          (view) =>\n              (view.id != currentViewId) &&\n              (view.name.toLowerCase().contains(search.toLowerCase()) ||\n                  (view.name.isEmpty && search.isEmpty) ||\n                  (view.name.isEmpty &&\n                      LocaleKeys.menuAppHeader_defaultNewPageName\n                          .tr()\n                          .toLowerCase()\n                          .contains(search.toLowerCase()))),\n        )\n        .take(10)\n        .toList();\n    searchedViews.clear();\n    searchedViews.addAll(allViews ?? []);\n    selectedIndex = 0;\n    onDataRefresh?.call();\n  }\n\n  void refreshIndex(int index) {\n    selectedIndex = index;\n    onDataRefresh?.call();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart",
    "content": "import 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass LinkStyle {\n  static Color borderColor(BuildContext context) =>\n      Theme.of(context).isLightMode ? Color(0xFFE8ECF3) : Color(0x64BDBDBD);\n\n  static InputDecoration buildLinkTextFieldInputDecoration(\n    String hintText,\n    BuildContext context, {\n    bool showErrorBorder = false,\n    EdgeInsets? contentPadding,\n    double? radius,\n  }) {\n    final theme = AppFlowyTheme.of(context);\n    final border = OutlineInputBorder(\n      borderRadius: BorderRadius.all(Radius.circular(radius ?? 8.0)),\n      borderSide: BorderSide(color: borderColor(context)),\n    );\n    final enableBorder = border.copyWith(\n      borderSide: BorderSide(\n        color: showErrorBorder\n            ? theme.textColorScheme.error\n            : theme.fillColorScheme.themeThick,\n      ),\n    );\n    final hintStyle = TextStyle(\n      fontSize: 14,\n      height: 20 / 14,\n      fontWeight: FontWeight.w400,\n      color: theme.textColorScheme.tertiary,\n    );\n    return InputDecoration(\n      hintText: hintText,\n      hintStyle: hintStyle,\n      contentPadding: contentPadding ?? const EdgeInsets.fromLTRB(8, 6, 8, 6),\n      isDense: true,\n      border: border,\n      enabledBorder: border,\n      focusedBorder: enableBorder,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_animation.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass ToolbarAnimationWidget extends StatefulWidget {\n  const ToolbarAnimationWidget({\n    super.key,\n    required this.child,\n    this.duration = const Duration(milliseconds: 150),\n    this.beginOpacity = 0.0,\n    this.endOpacity = 1.0,\n    this.beginScaleFactor = 0.95,\n    this.endScaleFactor = 1.0,\n  });\n\n  final Widget child;\n  final Duration duration;\n  final double beginScaleFactor;\n  final double endScaleFactor;\n  final double beginOpacity;\n  final double endOpacity;\n\n  @override\n  State<ToolbarAnimationWidget> createState() => _ToolbarAnimationWidgetState();\n}\n\nclass _ToolbarAnimationWidgetState extends State<ToolbarAnimationWidget>\n    with SingleTickerProviderStateMixin {\n  late AnimationController controller;\n  late Animation<double> fadeAnimation;\n  late Animation<double> scaleAnimation;\n\n  @override\n  void initState() {\n    super.initState();\n    controller = AnimationController(\n      vsync: this,\n      duration: widget.duration,\n    );\n    fadeAnimation = _buildFadeAnimation();\n    scaleAnimation = _buildScaleAnimation();\n    controller.forward();\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AnimatedBuilder(\n      animation: controller,\n      builder: (_, child) => Opacity(\n        opacity: fadeAnimation.value,\n        child: Transform.scale(\n          scale: scaleAnimation.value,\n          child: child,\n        ),\n      ),\n      child: widget.child,\n    );\n  }\n\n  Animation<double> _buildFadeAnimation() {\n    return Tween<double>(\n      begin: widget.beginOpacity,\n      end: widget.endOpacity,\n    ).animate(\n      CurvedAnimation(\n        parent: controller,\n        curve: Curves.easeInOut,\n      ),\n    );\n  }\n\n  Animation<double> _buildScaleAnimation() {\n    return Tween<double>(\n      begin: widget.beginScaleFactor,\n      end: widget.endScaleFactor,\n    ).animate(\n      CurvedAnimation(\n        parent: controller,\n        curve: Curves.easeInOut,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/animated_gesture.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass ErrorBlockComponentBuilder extends BlockComponentBuilder {\n  ErrorBlockComponentBuilder({\n    super.configuration,\n  });\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    return ErrorBlockComponentWidget(\n      key: node.key,\n      node: node,\n      configuration: configuration,\n      showActions: showActions(node),\n      actionBuilder: (context, state) => actionBuilder(\n        blockComponentContext,\n        state,\n      ),\n      actionTrailingBuilder: (context, state) => actionTrailingBuilder(\n        blockComponentContext,\n        state,\n      ),\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate => (_) => true;\n}\n\nclass ErrorBlockComponentWidget extends BlockComponentStatefulWidget {\n  const ErrorBlockComponentWidget({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n  });\n\n  @override\n  State<ErrorBlockComponentWidget> createState() =>\n      _ErrorBlockComponentWidgetState();\n}\n\nclass _ErrorBlockComponentWidgetState extends State<ErrorBlockComponentWidget>\n    with BlockComponentConfigurable {\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  @override\n  Node get node => widget.node;\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child = Container(\n      width: double.infinity,\n      decoration: BoxDecoration(\n        color: Theme.of(context).colorScheme.surfaceContainerHighest,\n        borderRadius: BorderRadius.circular(4),\n      ),\n      child: UniversalPlatform.isDesktopOrWeb\n          ? _buildDesktopErrorBlock(context)\n          : _buildMobileErrorBlock(context),\n    );\n\n    child = Padding(\n      padding: padding,\n      child: child,\n    );\n\n    if (widget.showActions && widget.actionBuilder != null) {\n      child = BlockComponentActionWrapper(\n        node: node,\n        actionBuilder: widget.actionBuilder!,\n        actionTrailingBuilder: widget.actionTrailingBuilder,\n        child: child,\n      );\n    }\n\n    if (UniversalPlatform.isMobile) {\n      child = MobileBlockActionButtons(\n        node: node,\n        editorState: context.read<EditorState>(),\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  Widget _buildDesktopErrorBlock(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(vertical: 12),\n      child: Row(\n        children: [\n          const HSpace(12),\n          FlowyText.regular(\n            LocaleKeys.document_errorBlock_parseError.tr(args: [node.type]),\n          ),\n          const Spacer(),\n          OutlinedRoundedButton(\n            text: LocaleKeys.document_errorBlock_copyBlockContent.tr(),\n            onTap: _copyBlockContent,\n          ),\n          const HSpace(12),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildMobileErrorBlock(BuildContext context) {\n    return AnimatedGestureDetector(\n      onTapUp: _copyBlockContent,\n      child: Padding(\n        padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Padding(\n              padding: const EdgeInsets.only(left: 4.0, right: 24.0),\n              child: FlowyText.regular(\n                LocaleKeys.document_errorBlock_parseError.tr(args: [node.type]),\n                maxLines: 3,\n              ),\n            ),\n            const VSpace(6),\n            Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 4),\n              child: FlowyText.regular(\n                '(${LocaleKeys.document_errorBlock_clickToCopyTheBlockContent.tr()})',\n                color: Theme.of(context).hintColor,\n                fontSize: 12.0,\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  void _copyBlockContent() {\n    showToastNotification(\n      message: LocaleKeys.document_errorBlock_blockContentHasBeenCopied.tr(),\n    );\n\n    getIt<ClipboardService>().setData(\n      ClipboardServiceData(plainText: jsonEncode(node.toJson())),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/extensions/flowy_tint_extension.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\n\nextension FlowyTintExtension on FlowyTint {\n  String tintName(\n    AppFlowyEditorLocalizations l10n, {\n    ThemeMode? themeMode,\n    String? theme,\n  }) {\n    switch (this) {\n      case FlowyTint.tint1:\n        return l10n.lightLightTint1;\n      case FlowyTint.tint2:\n        return l10n.lightLightTint2;\n      case FlowyTint.tint3:\n        return l10n.lightLightTint3;\n      case FlowyTint.tint4:\n        return l10n.lightLightTint4;\n      case FlowyTint.tint5:\n        return l10n.lightLightTint5;\n      case FlowyTint.tint6:\n        return l10n.lightLightTint6;\n      case FlowyTint.tint7:\n        return l10n.lightLightTint7;\n      case FlowyTint.tint8:\n        return l10n.lightLightTint8;\n      case FlowyTint.tint9:\n        return l10n.lightLightTint9;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block.dart",
    "content": "export './file_block_component.dart';\nexport './file_selection_menu.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/home/toast.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:cross_file/cross_file.dart';\nimport 'package:desktop_drop/desktop_drop.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:provider/provider.dart';\nimport 'package:string_validator/string_validator.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'file_block_menu.dart';\nimport 'file_upload_menu.dart';\n\nclass FileBlockKeys {\n  const FileBlockKeys._();\n\n  static const String type = 'file';\n\n  /// The src of the file.\n  ///\n  /// The value is a String.\n  /// It can be a url for a network file or a local file path.\n  ///\n  static const String url = 'url';\n\n  /// The name of the file.\n  ///\n  /// The value is a String.\n  ///\n  static const String name = 'name';\n\n  /// The type of the url.\n  ///\n  /// The value is a FileUrlType enum.\n  ///\n  static const String urlType = 'url_type';\n\n  /// The date of the file upload.\n  ///\n  /// The value is a timestamp in ms.\n  ///\n  static const String uploadedAt = 'uploaded_at';\n\n  /// The user who uploaded the file.\n  ///\n  /// The value is a String, in form of user id.\n  ///\n  static const String uploadedBy = 'uploaded_by';\n\n  /// The GlobalKey of the FileBlockComponentState.\n  ///\n  /// **Note: This value is used in extraInfos of the Node, not in the attributes.**\n  static const String globalKey = 'global_key';\n}\n\nenum FileUrlType {\n  local,\n  network,\n  cloud;\n\n  static FileUrlType fromIntValue(int value) {\n    switch (value) {\n      case 0:\n        return FileUrlType.local;\n      case 1:\n        return FileUrlType.network;\n      case 2:\n        return FileUrlType.cloud;\n      default:\n        throw UnimplementedError();\n    }\n  }\n\n  int toIntValue() {\n    switch (this) {\n      case FileUrlType.local:\n        return 0;\n      case FileUrlType.network:\n        return 1;\n      case FileUrlType.cloud:\n        return 2;\n    }\n  }\n\n  FileUploadTypePB toFileUploadTypePB() {\n    switch (this) {\n      case FileUrlType.local:\n        return FileUploadTypePB.LocalFile;\n      case FileUrlType.network:\n        return FileUploadTypePB.NetworkFile;\n      case FileUrlType.cloud:\n        return FileUploadTypePB.CloudFile;\n    }\n  }\n}\n\nNode fileNode({\n  required String url,\n  FileUrlType type = FileUrlType.local,\n  String? name,\n}) {\n  return Node(\n    type: FileBlockKeys.type,\n    attributes: {\n      FileBlockKeys.url: url,\n      FileBlockKeys.urlType: type.toIntValue(),\n      FileBlockKeys.name: name,\n      FileBlockKeys.uploadedAt: DateTime.now().millisecondsSinceEpoch,\n    },\n  );\n}\n\nclass FileBlockComponentBuilder extends BlockComponentBuilder {\n  FileBlockComponentBuilder({super.configuration});\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    final extraInfos = node.extraInfos;\n    final key = extraInfos?[FileBlockKeys.globalKey] as GlobalKey?;\n\n    return FileBlockComponent(\n      key: key ?? node.key,\n      node: node,\n      showActions: showActions(node),\n      configuration: configuration,\n      actionBuilder: (_, state) => actionBuilder(blockComponentContext, state),\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate => (node) => node.children.isEmpty;\n}\n\nclass FileBlockComponent extends BlockComponentStatefulWidget {\n  const FileBlockComponent({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n  });\n\n  static const uploadDragKey = 'FileUploadMenu';\n\n  @override\n  State<FileBlockComponent> createState() => FileBlockComponentState();\n}\n\nclass FileBlockComponentState extends State<FileBlockComponent>\n    with SelectableMixin, BlockComponentConfigurable {\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  @override\n  Node get node => widget.node;\n\n  RenderBox? get _renderBox => context.findRenderObject() as RenderBox?;\n\n  late EditorDropManagerState? dropManagerState = UniversalPlatform.isMobile\n      ? null\n      : context.read<EditorDropManagerState?>();\n\n  final fileKey = GlobalKey();\n  final showActionsNotifier = ValueNotifier<bool>(false);\n  final controller = PopoverController();\n  final menuController = PopoverController();\n\n  late final editorState = Provider.of<EditorState>(context, listen: false);\n\n  bool alwaysShowMenu = false;\n  bool isDragging = false;\n  bool isHovering = false;\n\n  @override\n  void didChangeDependencies() {\n    if (!UniversalPlatform.isMobile) {\n      dropManagerState = context.read<EditorDropManagerState?>();\n    }\n    super.didChangeDependencies();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final url = node.attributes[FileBlockKeys.url];\n    final FileUrlType urlType =\n        FileUrlType.fromIntValue(node.attributes[FileBlockKeys.urlType] ?? 0);\n\n    Widget child = MouseRegion(\n      cursor: SystemMouseCursors.click,\n      onEnter: (_) {\n        setState(() => isHovering = true);\n        showActionsNotifier.value = true;\n      },\n      onExit: (_) {\n        setState(() => isHovering = false);\n        if (!alwaysShowMenu) {\n          showActionsNotifier.value = false;\n        }\n      },\n      opaque: false,\n      child: GestureDetector(\n        behavior: HitTestBehavior.translucent,\n        onTap: url != null && url.isNotEmpty\n            ? () async => _openFile(context, urlType, url)\n            : _openMenu,\n        child: DecoratedBox(\n          decoration: BoxDecoration(\n            color: isHovering\n                ? Theme.of(context).colorScheme.secondary\n                : Theme.of(context).colorScheme.surfaceContainerHighest,\n            borderRadius: BorderRadius.circular(4),\n            border: isDragging\n                ? Border.all(\n                    color: Theme.of(context).colorScheme.primary,\n                    width: 2,\n                  )\n                : null,\n          ),\n          child: SizedBox(\n            height: 52,\n            child: Row(\n              children: [\n                const HSpace(10),\n                FlowySvg(\n                  FlowySvgs.slash_menu_icon_file_s,\n                  color: Theme.of(context).hintColor,\n                  size: const Size.square(24),\n                ),\n                const HSpace(10),\n                ..._buildTrailing(context),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n\n    if (UniversalPlatform.isDesktopOrWeb) {\n      if (url == null || url.isEmpty) {\n        child = DropTarget(\n          enable: dropManagerState?.isDropEnabled == true ||\n              dropManagerState?.contains(FileBlockKeys.type) == true,\n          onDragEntered: (_) {\n            if (dropManagerState?.isDropEnabled == true) {\n              dropManagerState?.add(FileBlockKeys.type);\n              setState(() => isDragging = true);\n            }\n          },\n          onDragExited: (_) {\n            if (dropManagerState?.contains(FileBlockKeys.type) == true) {\n              dropManagerState?.remove(FileBlockKeys.type);\n              setState(() => isDragging = false);\n            }\n          },\n          onDragDone: (details) {\n            dropManagerState?.remove(FileBlockKeys.type);\n            insertFileFromLocal(details.files);\n          },\n          child: AppFlowyPopover(\n            controller: controller,\n            direction: PopoverDirection.bottomWithCenterAligned,\n            constraints: const BoxConstraints(\n              maxWidth: 480,\n              maxHeight: 340,\n              minHeight: 80,\n            ),\n            clickHandler: PopoverClickHandler.gestureDetector,\n            onOpen: () => dropManagerState?.add(\n              FileBlockComponent.uploadDragKey,\n            ),\n            onClose: () => dropManagerState?.remove(\n              FileBlockComponent.uploadDragKey,\n            ),\n            popupBuilder: (_) => FileUploadMenu(\n              onInsertLocalFile: insertFileFromLocal,\n              onInsertNetworkFile: insertNetworkFile,\n            ),\n            child: child,\n          ),\n        );\n      }\n\n      child = BlockSelectionContainer(\n        node: node,\n        delegate: this,\n        listenable: editorState.selectionNotifier,\n        blockColor: editorState.editorStyle.selectionColor,\n        supportTypes: const [BlockSelectionType.block],\n        child: Padding(\n          key: fileKey,\n          padding: padding,\n          child: child,\n        ),\n      );\n    } else {\n      return Padding(\n        key: fileKey,\n        padding: padding,\n        child: MobileBlockActionButtons(\n          node: widget.node,\n          editorState: editorState,\n          child: child,\n        ),\n      );\n    }\n\n    if (widget.showActions && widget.actionBuilder != null) {\n      child = BlockComponentActionWrapper(\n        node: node,\n        actionBuilder: widget.actionBuilder!,\n        actionTrailingBuilder: widget.actionTrailingBuilder,\n        child: child,\n      );\n    }\n\n    if (!UniversalPlatform.isDesktopOrWeb) {\n      // show a fixed menu on mobile\n      child = MobileBlockActionButtons(\n        node: node,\n        editorState: editorState,\n        extendActionWidgets: _buildExtendActionWidgets(context),\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  Future<void> _openFile(\n    BuildContext context,\n    FileUrlType urlType,\n    String url,\n  ) async {\n    await afLaunchUrlString(url, context: context);\n  }\n\n  void _openMenu() {\n    if (UniversalPlatform.isDesktopOrWeb) {\n      controller.show();\n      dropManagerState?.add(FileBlockComponent.uploadDragKey);\n    } else {\n      editorState.updateSelectionWithReason(null, extraInfo: {});\n      showUploadFileMobileMenu();\n    }\n  }\n\n  List<Widget> _buildTrailing(BuildContext context) {\n    if (node.attributes[FileBlockKeys.url]?.isNotEmpty == true) {\n      final name = node.attributes[FileBlockKeys.name] as String;\n      return [\n        Expanded(\n          child: FlowyText(\n            name,\n            overflow: TextOverflow.ellipsis,\n          ),\n        ),\n        const HSpace(8),\n        if (UniversalPlatform.isDesktopOrWeb) ...[\n          ValueListenableBuilder<bool>(\n            valueListenable: showActionsNotifier,\n            builder: (_, value, __) {\n              final url = node.attributes[FileBlockKeys.url];\n              if (!value || url == null || url.isEmpty) {\n                return const SizedBox.shrink();\n              }\n\n              return GestureDetector(\n                behavior: HitTestBehavior.translucent,\n                onTap: menuController.show,\n                child: AppFlowyPopover(\n                  controller: menuController,\n                  triggerActions: PopoverTriggerFlags.none,\n                  direction: PopoverDirection.bottomWithRightAligned,\n                  onClose: () {\n                    setState(\n                      () {\n                        alwaysShowMenu = false;\n                        showActionsNotifier.value = false;\n                      },\n                    );\n                  },\n                  popupBuilder: (_) {\n                    alwaysShowMenu = true;\n                    return FileBlockMenu(\n                      controller: menuController,\n                      node: node,\n                      editorState: editorState,\n                    );\n                  },\n                  child: const FileMenuTrigger(),\n                ),\n              );\n            },\n          ),\n          const HSpace(8),\n        ],\n        if (UniversalPlatform.isMobile) ...[\n          const HSpace(36),\n        ],\n      ];\n    } else {\n      return [\n        Flexible(\n          child: FlowyText(\n            isDragging\n                ? LocaleKeys.document_plugins_file_placeholderDragging.tr()\n                : LocaleKeys.document_plugins_file_placeholderText.tr(),\n            overflow: TextOverflow.ellipsis,\n            color: Theme.of(context).hintColor,\n          ),\n        ),\n      ];\n    }\n  }\n\n  // only used on mobile platform\n  List<Widget> _buildExtendActionWidgets(BuildContext context) {\n    final String? url = widget.node.attributes[FileBlockKeys.url];\n    if (url == null || url.isEmpty) {\n      return [];\n    }\n\n    final urlType = FileUrlType.fromIntValue(\n      widget.node.attributes[FileBlockKeys.urlType] ?? 0,\n    );\n\n    if (urlType != FileUrlType.network) {\n      return [];\n    }\n\n    return [\n      FlowyOptionTile.text(\n        showTopBorder: false,\n        text: LocaleKeys.editor_copyLink.tr(),\n        leftIcon: const FlowySvg(\n          FlowySvgs.m_field_copy_s,\n        ),\n        onTap: () async {\n          context.pop();\n          showSnackBarMessage(\n            context,\n            LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),\n          );\n          await getIt<ClipboardService>().setPlainText(url);\n        },\n      ),\n    ];\n  }\n\n  void showUploadFileMobileMenu() {\n    showMobileBottomSheet(\n      context,\n      title: LocaleKeys.document_plugins_file_name.tr(),\n      showHeader: true,\n      showCloseButton: true,\n      showDragHandle: true,\n      builder: (context) {\n        return Container(\n          margin: const EdgeInsets.only(top: 12.0),\n          constraints: const BoxConstraints(\n            maxHeight: 340,\n            minHeight: 80,\n          ),\n          child: FileUploadMenu(\n            onInsertLocalFile: (file) async {\n              context.pop();\n              await insertFileFromLocal(file);\n            },\n            onInsertNetworkFile: (url) async {\n              context.pop();\n              await insertNetworkFile(url);\n            },\n          ),\n        );\n      },\n    );\n  }\n\n  Future<void> insertFileFromLocal(List<XFile> files) async {\n    if (files.isEmpty) return;\n\n    final file = files.first;\n    final path = file.path;\n    final documentBloc = context.read<DocumentBloc>();\n    final isLocalMode = documentBloc.isLocalMode;\n    final urlType = isLocalMode ? FileUrlType.local : FileUrlType.cloud;\n\n    String? url;\n    String? errorMsg;\n    if (isLocalMode) {\n      url = await saveFileToLocalStorage(path);\n    } else {\n      final result =\n          await saveFileToCloudStorage(path, documentBloc.documentId);\n      url = result.$1;\n      errorMsg = result.$2;\n    }\n\n    if (errorMsg != null && mounted) {\n      return showSnackBarMessage(context, errorMsg);\n    }\n\n    // Remove the file block from the drop state manager\n    dropManagerState?.remove(FileBlockKeys.type);\n\n    final transaction = editorState.transaction;\n    transaction.updateNode(widget.node, {\n      FileBlockKeys.url: url,\n      FileBlockKeys.urlType: urlType.toIntValue(),\n      FileBlockKeys.name: file.name,\n      FileBlockKeys.uploadedAt: DateTime.now().millisecondsSinceEpoch,\n    });\n    await editorState.apply(transaction);\n  }\n\n  Future<void> insertNetworkFile(String url) async {\n    if (url.isEmpty || !isURL(url)) {\n      // show error\n      return showSnackBarMessage(\n        context,\n        LocaleKeys.document_plugins_file_networkUrlInvalid.tr(),\n      );\n    }\n\n    // Remove the file block from the drop state manager\n    dropManagerState?.remove(FileBlockKeys.type);\n\n    final uri = Uri.tryParse(url);\n    if (uri == null) {\n      return;\n    }\n\n    String name = uri.pathSegments.isNotEmpty ? uri.pathSegments.last : \"\";\n    if (name.isEmpty && uri.pathSegments.length > 1) {\n      name = uri.pathSegments[uri.pathSegments.length - 2];\n    } else if (name.isEmpty) {\n      name = uri.host;\n    }\n\n    final transaction = editorState.transaction;\n    transaction.updateNode(widget.node, {\n      FileBlockKeys.url: url,\n      FileBlockKeys.urlType: FileUrlType.network.toIntValue(),\n      FileBlockKeys.name: name,\n      FileBlockKeys.uploadedAt: DateTime.now().millisecondsSinceEpoch,\n    });\n    await editorState.apply(transaction);\n  }\n\n  @override\n  Position start() => Position(path: widget.node.path);\n\n  @override\n  Position end() => Position(path: widget.node.path, offset: 1);\n\n  @override\n  Position getPositionInOffset(Offset start) => end();\n\n  @override\n  bool get shouldCursorBlink => false;\n\n  @override\n  CursorStyle get cursorStyle => CursorStyle.cover;\n\n  @override\n  Rect getBlockRect({bool shiftWithBaseOffset = false}) {\n    final renderBox = fileKey.currentContext?.findRenderObject();\n    if (renderBox is RenderBox) {\n      return padding.topLeft & renderBox.size;\n    }\n    return Rect.zero;\n  }\n\n  @override\n  Rect? getCursorRectInPosition(\n    Position position, {\n    bool shiftWithBaseOffset = false,\n  }) {\n    final rects = getRectsInSelection(Selection.collapsed(position));\n    return rects.firstOrNull;\n  }\n\n  @override\n  List<Rect> getRectsInSelection(\n    Selection selection, {\n    bool shiftWithBaseOffset = false,\n  }) {\n    if (_renderBox == null) {\n      return [];\n    }\n    final parentBox = context.findRenderObject();\n    final renderBox = fileKey.currentContext?.findRenderObject();\n    if (parentBox is RenderBox && renderBox is RenderBox) {\n      return [\n        renderBox.localToGlobal(Offset.zero, ancestor: parentBox) &\n            renderBox.size,\n      ];\n    }\n    return [Offset.zero & _renderBox!.size];\n  }\n\n  @override\n  Selection getSelectionInRange(Offset start, Offset end) => Selection.single(\n        path: widget.node.path,\n        startOffset: 0,\n        endOffset: 1,\n      );\n\n  @override\n  Offset localToGlobal(\n    Offset offset, {\n    bool shiftWithBaseOffset = false,\n  }) =>\n      _renderBox!.localToGlobal(offset);\n}\n\n@visibleForTesting\nclass FileMenuTrigger extends StatelessWidget {\n  const FileMenuTrigger({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return const FlowyHover(\n      resetHoverOnRebuild: false,\n      child: Padding(\n        padding: EdgeInsets.all(4),\n        child: FlowySvg(\n          FlowySvgs.three_dots_s,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_menu.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/prelude.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass FileBlockMenu extends StatefulWidget {\n  const FileBlockMenu({\n    super.key,\n    required this.controller,\n    required this.node,\n    required this.editorState,\n  });\n\n  final PopoverController controller;\n  final Node node;\n  final EditorState editorState;\n\n  @override\n  State<FileBlockMenu> createState() => _FileBlockMenuState();\n}\n\nclass _FileBlockMenuState extends State<FileBlockMenu> {\n  final nameController = TextEditingController();\n  final errorMessage = ValueNotifier<String?>(null);\n  BuildContext? renameContext;\n\n  @override\n  void initState() {\n    super.initState();\n    nameController.text = widget.node.attributes[FileBlockKeys.name] ?? '';\n    nameController.selection = TextSelection(\n      baseOffset: 0,\n      extentOffset: nameController.text.length,\n    );\n  }\n\n  @override\n  void dispose() {\n    errorMessage.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final uploadedAtInMS =\n        widget.node.attributes[FileBlockKeys.uploadedAt] as int?;\n    final uploadedAt = uploadedAtInMS != null\n        ? DateTime.fromMillisecondsSinceEpoch(uploadedAtInMS)\n        : null;\n    final dateFormat = context.read<AppearanceSettingsCubit>().state.dateFormat;\n    final urlType =\n        FileUrlType.fromIntValue(widget.node.attributes[FileBlockKeys.urlType]);\n    final fileUploadType = urlType.toFileUploadTypePB();\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        HoverButton(\n          itemHeight: 20,\n          leftIcon: const FlowySvg(FlowySvgs.download_s),\n          name: LocaleKeys.button_download.tr(),\n          onTap: () {\n            final userProfile = widget.editorState.document.root.context\n                ?.read<DocumentBloc>()\n                .state\n                .userProfilePB;\n            final url = widget.node.attributes[FileBlockKeys.url];\n            final name = widget.node.attributes[FileBlockKeys.name];\n            if (url != null && name != null) {\n              final filePB = MediaFilePB(\n                url: url,\n                name: name,\n                uploadType: fileUploadType,\n              );\n              downloadMediaFile(\n                context,\n                filePB,\n                userProfile: userProfile,\n              );\n            }\n          },\n        ),\n        const VSpace(4),\n        HoverButton(\n          itemHeight: 20,\n          leftIcon: const FlowySvg(FlowySvgs.edit_s),\n          name: LocaleKeys.document_plugins_file_renameFile_title.tr(),\n          onTap: () {\n            widget.controller.close();\n            showCustomConfirmDialog(\n              context: context,\n              title: LocaleKeys.document_plugins_file_renameFile_title.tr(),\n              description:\n                  LocaleKeys.document_plugins_file_renameFile_description.tr(),\n              closeOnConfirm: false,\n              builder: (context) {\n                renameContext = context;\n                return FileRenameTextField(\n                  nameController: nameController,\n                  errorMessage: errorMessage,\n                  onSubmitted: _saveName,\n                );\n              },\n              confirmLabel: LocaleKeys.button_save.tr(),\n              onConfirm: _saveName,\n            );\n          },\n        ),\n        const VSpace(4),\n        HoverButton(\n          itemHeight: 20,\n          leftIcon: const FlowySvg(FlowySvgs.delete_s),\n          name: LocaleKeys.button_delete.tr(),\n          onTap: () {\n            final transaction = widget.editorState.transaction\n              ..deleteNode(widget.node);\n            widget.editorState.apply(transaction);\n            widget.controller.close();\n          },\n        ),\n        if (uploadedAt != null) ...[\n          const Divider(height: 12),\n          Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 8),\n            child: FlowyText.regular(\n              [FileUrlType.cloud, FileUrlType.local].contains(urlType)\n                  ? LocaleKeys.document_plugins_file_uploadedAt.tr(\n                      args: [dateFormat.formatDate(uploadedAt, false)],\n                    )\n                  : LocaleKeys.document_plugins_file_linkedAt.tr(\n                      args: [dateFormat.formatDate(uploadedAt, false)],\n                    ),\n              fontSize: 14,\n              maxLines: 2,\n              color: Theme.of(context).hintColor,\n            ),\n          ),\n          const VSpace(2),\n        ],\n      ],\n    );\n  }\n\n  void _saveName() {\n    if (nameController.text.isEmpty) {\n      errorMessage.value =\n          LocaleKeys.document_plugins_file_renameFile_nameEmptyError.tr();\n      return;\n    }\n\n    final attributes = widget.node.attributes;\n    attributes[FileBlockKeys.name] = nameController.text;\n\n    final transaction = widget.editorState.transaction\n      ..updateNode(widget.node, attributes);\n    widget.editorState.apply(transaction);\n\n    if (renameContext != null) {\n      Navigator.of(renameContext!).pop();\n    }\n  }\n}\n\nclass FileRenameTextField extends StatefulWidget {\n  const FileRenameTextField({\n    super.key,\n    required this.nameController,\n    required this.errorMessage,\n    required this.onSubmitted,\n    this.disposeController = true,\n  });\n\n  final TextEditingController nameController;\n  final ValueNotifier<String?> errorMessage;\n  final VoidCallback onSubmitted;\n\n  final bool disposeController;\n\n  @override\n  State<FileRenameTextField> createState() => _FileRenameTextFieldState();\n}\n\nclass _FileRenameTextFieldState extends State<FileRenameTextField> {\n  @override\n  void initState() {\n    super.initState();\n    widget.errorMessage.addListener(_setState);\n  }\n\n  @override\n  void dispose() {\n    widget.errorMessage.removeListener(_setState);\n    if (widget.disposeController) {\n      widget.nameController.dispose();\n    }\n    super.dispose();\n  }\n\n  void _setState() {\n    if (mounted) {\n      setState(() {});\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        FlowyTextField(\n          controller: widget.nameController,\n          onSubmitted: (_) => widget.onSubmitted(),\n        ),\n        if (widget.errorMessage.value != null)\n          Padding(\n            padding: const EdgeInsets.only(top: 8),\n            child: FlowyText(\n              widget.errorMessage.value!,\n              color: Theme.of(context).colorScheme.error,\n            ),\n          ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_selection_menu.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nextension InsertFile on EditorState {\n  Future<void> insertEmptyFileBlock(GlobalKey key) async {\n    final selection = this.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n    final path = selection.end.path;\n    final node = getNodeAtPath(path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      return;\n    }\n    final file = fileNode(url: '')..extraInfos = {'global_key': key};\n\n    final transaction = this.transaction;\n\n    // if current node is a paragraph and empty, replace it with the file block\n    if (delta.isEmpty && node.type == ParagraphBlockKeys.type) {\n      final insertedPath = path;\n      transaction.insertNode(insertedPath, file);\n      transaction.deleteNode(node);\n    } else {\n      // otherwise, insert the file block after the current node\n      final insertedPath = path.next;\n      transaction.insertNode(insertedPath, file);\n    }\n\n    return apply(transaction);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:cross_file/cross_file.dart';\nimport 'package:desktop_drop/desktop_drop.dart';\nimport 'package:dotted_border/dotted_border.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/file_picker/file_picker_service.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/style_widget/text_field.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass FileUploadMenu extends StatefulWidget {\n  const FileUploadMenu({\n    super.key,\n    required this.onInsertLocalFile,\n    required this.onInsertNetworkFile,\n    this.allowMultipleFiles = false,\n  });\n\n  final void Function(List<XFile> files) onInsertLocalFile;\n  final void Function(String url) onInsertNetworkFile;\n  final bool allowMultipleFiles;\n\n  @override\n  State<FileUploadMenu> createState() => _FileUploadMenuState();\n}\n\nclass _FileUploadMenuState extends State<FileUploadMenu> {\n  int currentTab = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    // ClipRRect is used to clip the tab indicator, so the animation doesn't overflow the dialog\n    return ClipRRect(\n      child: DefaultTabController(\n        length: 2,\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            TabBar(\n              onTap: (value) => setState(() => currentTab = value),\n              isScrollable: true,\n              indicatorWeight: 3,\n              tabAlignment: TabAlignment.start,\n              indicatorSize: TabBarIndicatorSize.label,\n              labelPadding: EdgeInsets.zero,\n              padding: EdgeInsets.zero,\n              overlayColor: WidgetStatePropertyAll(\n                UniversalPlatform.isDesktop\n                    ? Theme.of(context).colorScheme.secondary\n                    : Colors.transparent,\n              ),\n              tabs: [\n                _Tab(\n                  title: LocaleKeys.document_plugins_file_uploadTab.tr(),\n                  isSelected: currentTab == 0,\n                ),\n                _Tab(\n                  title: LocaleKeys.document_plugins_file_networkTab.tr(),\n                  isSelected: currentTab == 1,\n                ),\n              ],\n            ),\n            const Divider(height: 0),\n            if (currentTab == 0) ...[\n              _FileUploadLocal(\n                allowMultipleFiles: widget.allowMultipleFiles,\n                onFilesPicked: (files) {\n                  if (files.isNotEmpty) {\n                    widget.onInsertLocalFile(files);\n                  }\n                },\n              ),\n            ] else ...[\n              _FileUploadNetwork(onSubmit: widget.onInsertNetworkFile),\n            ],\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _Tab extends StatelessWidget {\n  const _Tab({required this.title, this.isSelected = false});\n\n  final String title;\n  final bool isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: EdgeInsets.only(\n        left: 12.0,\n        right: 12.0,\n        bottom: 8.0,\n        top: UniversalPlatform.isMobile ? 0 : 8.0,\n      ),\n      child: FlowyText.semibold(\n        title,\n        color: isSelected\n            ? AFThemeExtension.of(context).strongText\n            : Theme.of(context).hintColor,\n      ),\n    );\n  }\n}\n\nclass _FileUploadLocal extends StatefulWidget {\n  const _FileUploadLocal({\n    required this.onFilesPicked,\n    this.allowMultipleFiles = false,\n  });\n\n  final void Function(List<XFile>) onFilesPicked;\n  final bool allowMultipleFiles;\n\n  @override\n  State<_FileUploadLocal> createState() => _FileUploadLocalState();\n}\n\nclass _FileUploadLocalState extends State<_FileUploadLocal> {\n  bool isDragging = false;\n\n  @override\n  Widget build(BuildContext context) {\n    final constraints =\n        UniversalPlatform.isMobile ? const BoxConstraints(minHeight: 92) : null;\n\n    if (UniversalPlatform.isMobile) {\n      return Padding(\n        padding: const EdgeInsets.all(12),\n        child: SizedBox(\n          height: 32,\n          child: FlowyButton(\n            backgroundColor: Theme.of(context).colorScheme.primary,\n            hoverColor:\n                Theme.of(context).colorScheme.primary.withValues(alpha: 0.9),\n            showDefaultBoxDecorationOnMobile: true,\n            margin: const EdgeInsets.all(5),\n            text: FlowyText(\n              LocaleKeys.document_plugins_file_uploadMobile.tr(),\n              textAlign: TextAlign.center,\n              color: Theme.of(context).colorScheme.onPrimary,\n            ),\n            onTap: () => _uploadFile(context),\n          ),\n        ),\n      );\n    }\n\n    return Padding(\n      padding: const EdgeInsets.all(16),\n      child: DropTarget(\n        onDragEntered: (_) => setState(() => isDragging = true),\n        onDragExited: (_) => setState(() => isDragging = false),\n        onDragDone: (details) => widget.onFilesPicked(details.files),\n        child: MouseRegion(\n          cursor: SystemMouseCursors.click,\n          child: GestureDetector(\n            behavior: HitTestBehavior.translucent,\n            onTap: () => _uploadFile(context),\n            child: FlowyHover(\n              resetHoverOnRebuild: false,\n              isSelected: () => isDragging,\n              style: HoverStyle(\n                borderRadius: BorderRadius.circular(10),\n                hoverColor:\n                    isDragging ? AFThemeExtension.of(context).tint9 : null,\n              ),\n              child: Container(\n                height: 172,\n                constraints: constraints,\n                child: DottedBorder(\n                  dashPattern: const [3, 3],\n                  radius: const Radius.circular(8),\n                  borderType: BorderType.RRect,\n                  color: isDragging\n                      ? Theme.of(context).colorScheme.primary\n                      : Theme.of(context).hintColor,\n                  child: Center(\n                    child: Column(\n                      mainAxisAlignment: MainAxisAlignment.center,\n                      children: [\n                        if (isDragging) ...[\n                          FlowyText(\n                            LocaleKeys.document_plugins_file_dropFileToUpload\n                                .tr(),\n                            fontSize: 14,\n                            fontWeight: FontWeight.w500,\n                            color: Theme.of(context).hintColor,\n                          ),\n                        ] else ...[\n                          RichText(\n                            text: TextSpan(\n                              children: [\n                                TextSpan(\n                                  text: LocaleKeys\n                                      .document_plugins_file_fileUploadHint\n                                      .tr(),\n                                  style: TextStyle(\n                                    fontSize: 14,\n                                    fontWeight: FontWeight.w500,\n                                    color: Theme.of(context).hintColor,\n                                  ),\n                                ),\n                                TextSpan(\n                                  text: LocaleKeys\n                                      .document_plugins_file_fileUploadHintSuffix\n                                      .tr(),\n                                  style: TextStyle(\n                                    fontSize: 14,\n                                    fontWeight: FontWeight.w500,\n                                    color:\n                                        Theme.of(context).colorScheme.primary,\n                                  ),\n                                ),\n                              ],\n                            ),\n                          ),\n                        ],\n                      ],\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Future<void> _uploadFile(BuildContext context) async {\n    final result = await getIt<FilePickerService>().pickFiles(\n      dialogTitle: '',\n      allowMultiple: widget.allowMultipleFiles,\n    );\n\n    final List<XFile> files = result?.files.isNotEmpty ?? false\n        ? result!.files.map((f) => f.xFile).toList()\n        : const [];\n\n    widget.onFilesPicked(files);\n  }\n}\n\nclass _FileUploadNetwork extends StatefulWidget {\n  const _FileUploadNetwork({required this.onSubmit});\n\n  final void Function(String url) onSubmit;\n\n  @override\n  State<_FileUploadNetwork> createState() => _FileUploadNetworkState();\n}\n\nclass _FileUploadNetworkState extends State<_FileUploadNetwork> {\n  bool isUrlValid = true;\n  String inputText = '';\n\n  @override\n  Widget build(BuildContext context) {\n    final constraints =\n        UniversalPlatform.isMobile ? const BoxConstraints(minHeight: 92) : null;\n\n    return Container(\n      padding: const EdgeInsets.all(16),\n      constraints: constraints,\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          FlowyTextField(\n            hintText: LocaleKeys.document_plugins_file_networkHint.tr(),\n            onChanged: (value) => inputText = value,\n            onEditingComplete: submit,\n          ),\n          if (!isUrlValid) ...[\n            const VSpace(4),\n            FlowyText(\n              LocaleKeys.document_plugins_file_networkUrlInvalid.tr(),\n              color: Theme.of(context).colorScheme.error,\n              maxLines: 3,\n              textAlign: TextAlign.start,\n            ),\n          ],\n          const VSpace(16),\n          SizedBox(\n            height: 32,\n            child: FlowyButton(\n              backgroundColor: Theme.of(context).colorScheme.primary,\n              hoverColor:\n                  Theme.of(context).colorScheme.primary.withValues(alpha: 0.9),\n              showDefaultBoxDecorationOnMobile: true,\n              margin: const EdgeInsets.all(5),\n              text: FlowyText(\n                LocaleKeys.document_plugins_file_networkAction.tr(),\n                textAlign: TextAlign.center,\n                color: Theme.of(context).colorScheme.onPrimary,\n              ),\n              onTap: submit,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  void submit() {\n    if (checkUrlValidity(inputText)) {\n      return widget.onSubmit(inputText);\n    }\n\n    setState(() => isUrlValid = false);\n  }\n\n  bool checkUrlValidity(String url) => hrefRegex.hasMatch(url);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_service.dart';\nimport 'package:appflowy/shared/custom_image_cache_manager.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/xfile_ext.dart';\nimport 'package:appflowy/workspace/application/settings/application_data_storage.dart';\nimport 'package:appflowy/workspace/presentation/home/toast.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/dispatch/error.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:cross_file/cross_file.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/file_picker/file_picker_impl.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flutter/material.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:universal_platform/universal_platform.dart';\n\nFuture<String?> saveFileToLocalStorage(String localFilePath) async {\n  final path = await getIt<ApplicationDataStorage>().getPath();\n  final filePath = p.join(path, 'files');\n\n  try {\n    // create the directory if not exists\n    final directory = Directory(filePath);\n    if (!directory.existsSync()) {\n      await directory.create(recursive: true);\n    }\n    final copyToPath = p.join(\n      filePath,\n      '${uuid()}${p.extension(localFilePath)}',\n    );\n    await File(localFilePath).copy(\n      copyToPath,\n    );\n    return copyToPath;\n  } catch (e) {\n    Log.error('cannot save file', e);\n    return null;\n  }\n}\n\nFuture<(String? path, String? errorMessage)> saveFileToCloudStorage(\n  String localFilePath,\n  String documentId, [\n  bool isImage = false,\n]) async {\n  final documentService = DocumentService();\n  Log.debug(\"Uploading file from local path: $localFilePath\");\n  final result = await documentService.uploadFile(\n    localFilePath: localFilePath,\n    documentId: documentId,\n  );\n\n  return result.fold(\n    (s) async {\n      if (isImage) {\n        await CustomImageCacheManager().putFile(\n          s.url,\n          File(localFilePath).readAsBytesSync(),\n        );\n      }\n\n      return (s.url, null);\n    },\n    (err) {\n      final message = Platform.isIOS\n          ? LocaleKeys.sideBar_storageLimitDialogTitleIOS.tr()\n          : LocaleKeys.sideBar_storageLimitDialogTitle.tr();\n      if (err.isStorageLimitExceeded) {\n        return (null, message);\n      }\n      return (null, err.msg);\n    },\n  );\n}\n\n/// Downloads a MediaFilePB\n///\n/// On Mobile the file is fetched first using HTTP, and then saved using FilePicker.\n/// On Desktop the files location is picked first using FilePicker, and then the file is saved.\n///\nFuture<void> downloadMediaFile(\n  BuildContext context,\n  MediaFilePB file, {\n  VoidCallback? onDownloadBegin,\n  VoidCallback? onDownloadEnd,\n  UserProfilePB? userProfile,\n}) async {\n  if ([\n    FileUploadTypePB.NetworkFile,\n    FileUploadTypePB.LocalFile,\n  ].contains(file.uploadType)) {\n    /// When the file is a network file or a local file, we can directly open the file.\n    await afLaunchUrlString(file.url);\n  } else {\n    if (userProfile == null) {\n      showToastNotification(\n        message: LocaleKeys.grid_media_downloadFailedToken.tr(),\n      );\n      return;\n    }\n\n    final uri = Uri.parse(file.url);\n    final token = jsonDecode(userProfile.token)['access_token'];\n\n    if (UniversalPlatform.isMobile) {\n      onDownloadBegin?.call();\n\n      final response =\n          await http.get(uri, headers: {'Authorization': 'Bearer $token'});\n\n      if (response.statusCode == 200) {\n        final tempFile = File(uri.pathSegments.last);\n        final result = await FilePicker().saveFile(\n          fileName: p.basename(tempFile.path),\n          bytes: response.bodyBytes,\n        );\n\n        if (result != null && context.mounted) {\n          showToastNotification(\n            type: ToastificationType.error,\n            message: LocaleKeys.grid_media_downloadSuccess.tr(),\n          );\n        }\n      } else if (context.mounted) {\n        showToastNotification(\n          type: ToastificationType.error,\n          message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(),\n        );\n      }\n\n      onDownloadEnd?.call();\n    } else {\n      final savePath = await FilePicker().saveFile(fileName: file.name);\n      if (savePath == null) {\n        return;\n      }\n\n      onDownloadBegin?.call();\n\n      final response =\n          await http.get(uri, headers: {'Authorization': 'Bearer $token'});\n\n      if (response.statusCode == 200) {\n        final imgFile = File(savePath);\n        await imgFile.writeAsBytes(response.bodyBytes);\n\n        if (context.mounted) {\n          showToastNotification(\n            message: LocaleKeys.grid_media_downloadSuccess.tr(),\n          );\n        }\n      } else if (context.mounted) {\n        showToastNotification(\n          type: ToastificationType.error,\n          message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(),\n        );\n      }\n\n      onDownloadEnd?.call();\n    }\n  }\n}\n\nFuture<void> insertLocalFile(\n  BuildContext context,\n  XFile file, {\n  required String documentId,\n  UserProfilePB? userProfile,\n  void Function(String, bool)? onUploadSuccess,\n}) async {\n  if (file.path.isEmpty) return;\n\n  final fileType = file.fileType.toMediaFileTypePB();\n\n  // Check upload type\n  final isLocalMode = (userProfile?.workspaceType ?? WorkspaceTypePB.LocalW) ==\n      WorkspaceTypePB.LocalW;\n\n  String? path;\n  String? errorMsg;\n  if (isLocalMode) {\n    path = await saveFileToLocalStorage(file.path);\n  } else {\n    (path, errorMsg) = await saveFileToCloudStorage(\n      file.path,\n      documentId,\n      fileType == MediaFileTypePB.Image,\n    );\n  }\n\n  if (errorMsg != null) {\n    return showSnackBarMessage(context, errorMsg);\n  }\n\n  if (path == null) {\n    return;\n  }\n\n  onUploadSuccess?.call(path, isLocalMode);\n}\n\n/// [onUploadSuccess] Callback to be called when the upload is successful.\n///\n/// The callback is called for each file that is successfully uploaded.\n/// In case of an error, the error message will be shown on a per-file basis.\n///\nFuture<void> insertLocalFiles(\n  BuildContext context,\n  List<XFile> files, {\n  required String documentId,\n  UserProfilePB? userProfile,\n  void Function(\n    XFile file,\n    String path,\n    bool isLocalMode,\n  )? onUploadSuccess,\n}) async {\n  if (files.every((f) => f.path.isEmpty)) return;\n\n  // Check upload type\n  final isLocalMode = (userProfile?.workspaceType ?? WorkspaceTypePB.LocalW) ==\n      WorkspaceTypePB.LocalW;\n\n  for (final file in files) {\n    final fileType = file.fileType.toMediaFileTypePB();\n\n    String? path;\n    String? errorMsg;\n\n    if (isLocalMode) {\n      path = await saveFileToLocalStorage(file.path);\n    } else {\n      (path, errorMsg) = await saveFileToCloudStorage(\n        file.path,\n        documentId,\n        fileType == MediaFileTypePB.Image,\n      );\n    }\n\n    if (errorMsg != null) {\n      showSnackBarMessage(context, errorMsg);\n      continue;\n    }\n\n    if (path == null) {\n      continue;\n    }\n    onUploadSuccess?.call(file, path, isLocalMode);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/mobile_file_upload_menu.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:appflowy/shared/permission/permission_checker.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/file_picker/file_picker_service.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/style_widget/text_field.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:image_picker/image_picker.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass MobileFileUploadMenu extends StatefulWidget {\n  const MobileFileUploadMenu({\n    super.key,\n    required this.onInsertLocalFile,\n    required this.onInsertNetworkFile,\n    this.allowMultipleFiles = false,\n  });\n\n  final void Function(List<XFile> files) onInsertLocalFile;\n  final void Function(String url) onInsertNetworkFile;\n  final bool allowMultipleFiles;\n\n  @override\n  State<MobileFileUploadMenu> createState() => _MobileFileUploadMenuState();\n}\n\nclass _MobileFileUploadMenuState extends State<MobileFileUploadMenu> {\n  int currentTab = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    // ClipRRect is used to clip the tab indicator, so the animation doesn't overflow the dialog\n    return ClipRRect(\n      child: DefaultTabController(\n        length: 2,\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            TabBar(\n              onTap: (value) => setState(() => currentTab = value),\n              indicatorWeight: 3,\n              labelPadding: EdgeInsets.zero,\n              padding: EdgeInsets.zero,\n              overlayColor: WidgetStatePropertyAll(\n                UniversalPlatform.isDesktop\n                    ? Theme.of(context).colorScheme.secondary\n                    : Colors.transparent,\n              ),\n              tabs: [\n                _Tab(\n                  title: LocaleKeys.document_plugins_file_uploadTab.tr(),\n                  isSelected: currentTab == 0,\n                ),\n                _Tab(\n                  title: LocaleKeys.document_plugins_file_networkTab.tr(),\n                  isSelected: currentTab == 1,\n                ),\n              ],\n            ),\n            const Divider(height: 0),\n            if (currentTab == 0) ...[\n              _FileUploadLocal(\n                allowMultipleFiles: widget.allowMultipleFiles,\n                onFilesPicked: (files) {\n                  if (files.isNotEmpty) {\n                    widget.onInsertLocalFile(files);\n                  }\n                },\n              ),\n            ] else ...[\n              _FileUploadNetwork(onSubmit: widget.onInsertNetworkFile),\n            ],\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _Tab extends StatelessWidget {\n  const _Tab({required this.title, this.isSelected = false});\n\n  final String title;\n  final bool isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: EdgeInsets.only(\n        left: 12.0,\n        right: 12.0,\n        bottom: 8.0,\n        top: UniversalPlatform.isMobile ? 0 : 8.0,\n      ),\n      child: FlowyText.semibold(\n        title,\n        color: isSelected\n            ? AFThemeExtension.of(context).strongText\n            : Theme.of(context).hintColor,\n      ),\n    );\n  }\n}\n\nclass _FileUploadLocal extends StatefulWidget {\n  const _FileUploadLocal({\n    required this.onFilesPicked,\n    this.allowMultipleFiles = false,\n  });\n\n  final void Function(List<XFile>) onFilesPicked;\n  final bool allowMultipleFiles;\n\n  @override\n  State<_FileUploadLocal> createState() => _FileUploadLocalState();\n}\n\nclass _FileUploadLocalState extends State<_FileUploadLocal> {\n  bool isDragging = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(16),\n      child: Column(\n        children: [\n          SizedBox(\n            height: 36,\n            child: FlowyButton(\n              radius: Corners.s8Border,\n              backgroundColor: Theme.of(context).colorScheme.primary,\n              hoverColor:\n                  Theme.of(context).colorScheme.primary.withValues(alpha: 0.9),\n              margin: const EdgeInsets.all(5),\n              text: FlowyText(\n                LocaleKeys.document_plugins_file_uploadMobileGallery.tr(),\n                textAlign: TextAlign.center,\n                color: Theme.of(context).colorScheme.onPrimary,\n              ),\n              onTap: () => _uploadFileFromGallery(context),\n            ),\n          ),\n          const VSpace(16),\n          SizedBox(\n            height: 36,\n            child: FlowyButton(\n              radius: Corners.s8Border,\n              backgroundColor: Theme.of(context).colorScheme.primary,\n              hoverColor:\n                  Theme.of(context).colorScheme.primary.withValues(alpha: 0.9),\n              margin: const EdgeInsets.all(5),\n              text: FlowyText(\n                LocaleKeys.document_plugins_file_uploadMobile.tr(),\n                textAlign: TextAlign.center,\n                color: Theme.of(context).colorScheme.onPrimary,\n              ),\n              onTap: () => _uploadFile(context),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Future<void> _uploadFileFromGallery(BuildContext context) async {\n    final photoPermission =\n        await PermissionChecker.checkPhotoPermission(context);\n    if (!photoPermission) {\n      Log.error('Has no permission to access the photo library');\n      return;\n    }\n    // on mobile, the users can pick a image file from camera or image library\n    final files = await ImagePicker().pickMultiImage();\n\n    widget.onFilesPicked(files);\n  }\n\n  Future<void> _uploadFile(BuildContext context) async {\n    final result = await getIt<FilePickerService>().pickFiles(\n      dialogTitle: '',\n      allowMultiple: widget.allowMultipleFiles,\n    );\n\n    final List<XFile> files = result?.files.isNotEmpty ?? false\n        ? result!.files.map((f) => f.xFile).toList()\n        : const [];\n\n    widget.onFilesPicked(files);\n  }\n}\n\nclass _FileUploadNetwork extends StatefulWidget {\n  const _FileUploadNetwork({required this.onSubmit});\n\n  final void Function(String url) onSubmit;\n\n  @override\n  State<_FileUploadNetwork> createState() => _FileUploadNetworkState();\n}\n\nclass _FileUploadNetworkState extends State<_FileUploadNetwork> {\n  bool isUrlValid = true;\n  String inputText = '';\n\n  @override\n  Widget build(BuildContext context) {\n    final constraints =\n        UniversalPlatform.isMobile ? const BoxConstraints(minHeight: 92) : null;\n\n    return Container(\n      padding: const EdgeInsets.all(16),\n      constraints: constraints,\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          FlowyTextField(\n            hintText: LocaleKeys.document_plugins_file_networkHint.tr(),\n            onChanged: (value) => inputText = value,\n            onEditingComplete: submit,\n          ),\n          if (!isUrlValid) ...[\n            const VSpace(4),\n            FlowyText(\n              LocaleKeys.document_plugins_file_networkUrlInvalid.tr(),\n              color: Theme.of(context).colorScheme.error,\n              maxLines: 3,\n              textAlign: TextAlign.start,\n            ),\n          ],\n          const VSpace(16),\n          SizedBox(\n            height: 36,\n            child: FlowyButton(\n              backgroundColor: Theme.of(context).colorScheme.primary,\n              hoverColor:\n                  Theme.of(context).colorScheme.primary.withValues(alpha: 0.9),\n              radius: Corners.s8Border,\n              margin: const EdgeInsets.all(5),\n              text: FlowyText(\n                LocaleKeys.grid_media_embedLink.tr(),\n                textAlign: TextAlign.center,\n                color: Theme.of(context).colorScheme.onPrimary,\n              ),\n              onTap: submit,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  void submit() {\n    if (checkUrlValidity(inputText)) {\n      return widget.onSubmit(inputText);\n    }\n\n    setState(() => isUrlValid = false);\n  }\n\n  bool checkUrlValidity(String url) => hrefRegex.hasMatch(url);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nclass FindAndReplaceMenuWidget extends StatefulWidget {\n  const FindAndReplaceMenuWidget({\n    super.key,\n    required this.onDismiss,\n    required this.editorState,\n    required this.showReplaceMenu,\n  });\n\n  final EditorState editorState;\n  final VoidCallback onDismiss;\n\n  /// Whether to show the replace menu initially\n  final bool showReplaceMenu;\n\n  @override\n  State<FindAndReplaceMenuWidget> createState() =>\n      _FindAndReplaceMenuWidgetState();\n}\n\nclass _FindAndReplaceMenuWidgetState extends State<FindAndReplaceMenuWidget> {\n  late bool showReplaceMenu = widget.showReplaceMenu;\n\n  final findFocusNode = FocusNode();\n  final replaceFocusNode = FocusNode();\n\n  late SearchServiceV3 searchService = SearchServiceV3(\n    editorState: widget.editorState,\n  );\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      if (widget.showReplaceMenu) {\n        replaceFocusNode.requestFocus();\n      } else {\n        findFocusNode.requestFocus();\n      }\n    });\n  }\n\n  @override\n  void dispose() {\n    findFocusNode.dispose();\n    replaceFocusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Shortcuts(\n      shortcuts: const {\n        SingleActivator(LogicalKeyboardKey.escape): DismissIntent(),\n      },\n      child: Actions(\n        actions: {\n          DismissIntent: CallbackAction<DismissIntent>(\n            onInvoke: (t) => widget.onDismiss.call(),\n          ),\n        },\n        child: TextFieldTapRegion(\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Padding(\n                padding: const EdgeInsets.symmetric(vertical: 8.0),\n                child: FindMenu(\n                  onDismiss: widget.onDismiss,\n                  editorState: widget.editorState,\n                  searchService: searchService,\n                  focusNode: findFocusNode,\n                  showReplaceMenu: showReplaceMenu,\n                  onToggleShowReplace: () => setState(() {\n                    showReplaceMenu = !showReplaceMenu;\n                  }),\n                ),\n              ),\n              if (showReplaceMenu)\n                Padding(\n                  padding: const EdgeInsets.only(\n                    bottom: 8.0,\n                  ),\n                  child: ReplaceMenu(\n                    editorState: widget.editorState,\n                    searchService: searchService,\n                    focusNode: replaceFocusNode,\n                  ),\n                ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass FindMenu extends StatefulWidget {\n  const FindMenu({\n    super.key,\n    required this.editorState,\n    required this.searchService,\n    required this.showReplaceMenu,\n    required this.focusNode,\n    required this.onDismiss,\n    required this.onToggleShowReplace,\n  });\n\n  final EditorState editorState;\n  final SearchServiceV3 searchService;\n\n  final bool showReplaceMenu;\n  final FocusNode focusNode;\n\n  final VoidCallback onDismiss;\n  final void Function() onToggleShowReplace;\n\n  @override\n  State<FindMenu> createState() => _FindMenuState();\n}\n\nclass _FindMenuState extends State<FindMenu> {\n  final textController = TextEditingController();\n\n  bool caseSensitive = false;\n\n  @override\n  void initState() {\n    super.initState();\n\n    widget.searchService.matchWrappers.addListener(_setState);\n    widget.searchService.currentSelectedIndex.addListener(_setState);\n\n    textController.addListener(_searchPattern);\n  }\n\n  @override\n  void dispose() {\n    widget.searchService.matchWrappers.removeListener(_setState);\n    widget.searchService.currentSelectedIndex.removeListener(_setState);\n    widget.searchService.dispose();\n    textController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    // the selectedIndex from searchService is 0-based\n    final selectedIndex = widget.searchService.selectedIndex + 1;\n    final matches = widget.searchService.matchWrappers.value;\n    return Row(\n      children: [\n        const HSpace(4.0),\n        // expand/collapse button\n        _FindAndReplaceIcon(\n          icon: widget.showReplaceMenu\n              ? FlowySvgs.drop_menu_show_s\n              : FlowySvgs.drop_menu_hide_s,\n          tooltipText: '',\n          onPressed: widget.onToggleShowReplace,\n        ),\n        const HSpace(4.0),\n        // find text input\n        SizedBox(\n          width: 200,\n          height: 30,\n          child: TextField(\n            key: const Key('findTextField'),\n            focusNode: widget.focusNode,\n            controller: textController,\n            style: Theme.of(context).textTheme.bodyMedium,\n            onSubmitted: (_) {\n              widget.searchService.navigateToMatch();\n\n              // after update selection or navigate to match, the editor\n              // will request focus, here's a workaround to request the\n              // focus back to the text field\n              Future.delayed(\n                const Duration(milliseconds: 50),\n                () => widget.focusNode.requestFocus(),\n              );\n            },\n            decoration: _buildInputDecoration(\n              LocaleKeys.findAndReplace_find.tr(),\n            ),\n          ),\n        ),\n        // the count of matches\n        Container(\n          constraints: const BoxConstraints(minWidth: 80),\n          padding: const EdgeInsets.symmetric(horizontal: 8.0),\n          alignment: Alignment.centerLeft,\n          child: FlowyText(\n            matches.isEmpty\n                ? LocaleKeys.findAndReplace_noResult.tr()\n                : '$selectedIndex of ${matches.length}',\n          ),\n        ),\n        const HSpace(4.0),\n        // case sensitive button\n        _FindAndReplaceIcon(\n          icon: FlowySvgs.text_s,\n          tooltipText: LocaleKeys.findAndReplace_caseSensitive.tr(),\n          onPressed: () => setState(() {\n            caseSensitive = !caseSensitive;\n            widget.searchService.caseSensitive = caseSensitive;\n          }),\n          isSelected: caseSensitive,\n        ),\n        const HSpace(4.0),\n        // previous match button\n        _FindAndReplaceIcon(\n          onPressed: () => widget.searchService.navigateToMatch(moveUp: true),\n          icon: FlowySvgs.arrow_up_s,\n          tooltipText: LocaleKeys.findAndReplace_previousMatch.tr(),\n        ),\n        const HSpace(4.0),\n        // next match button\n        _FindAndReplaceIcon(\n          onPressed: () => widget.searchService.navigateToMatch(),\n          icon: FlowySvgs.arrow_down_s,\n          tooltipText: LocaleKeys.findAndReplace_nextMatch.tr(),\n        ),\n        const HSpace(4.0),\n        _FindAndReplaceIcon(\n          onPressed: widget.onDismiss,\n          icon: FlowySvgs.close_s,\n          tooltipText: LocaleKeys.findAndReplace_close.tr(),\n        ),\n        const HSpace(4.0),\n      ],\n    );\n  }\n\n  void _searchPattern() {\n    widget.searchService.findAndHighlight(textController.text);\n    _setState();\n  }\n\n  void _setState() {\n    setState(() {});\n  }\n}\n\nclass ReplaceMenu extends StatefulWidget {\n  const ReplaceMenu({\n    super.key,\n    required this.editorState,\n    required this.searchService,\n    required this.focusNode,\n  });\n\n  final EditorState editorState;\n  final SearchServiceV3 searchService;\n\n  final FocusNode focusNode;\n\n  @override\n  State<ReplaceMenu> createState() => _ReplaceMenuState();\n}\n\nclass _ReplaceMenuState extends State<ReplaceMenu> {\n  final textController = TextEditingController();\n\n  @override\n  void dispose() {\n    textController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        // placeholder for aligning the replace menu\n        const HSpace(30),\n        SizedBox(\n          width: 200,\n          height: 30,\n          child: TextField(\n            key: const Key('replaceTextField'),\n            focusNode: widget.focusNode,\n            controller: textController,\n            style: Theme.of(context).textTheme.bodyMedium,\n            onSubmitted: (_) {\n              _replaceSelectedWord();\n\n              Future.delayed(\n                const Duration(milliseconds: 50),\n                () => widget.focusNode.requestFocus(),\n              );\n            },\n            decoration: _buildInputDecoration(\n              LocaleKeys.findAndReplace_replace.tr(),\n            ),\n          ),\n        ),\n        _FindAndReplaceIcon(\n          onPressed: _replaceSelectedWord,\n          iconBuilder: (_) => const Icon(\n            Icons.find_replace_outlined,\n            size: 16,\n          ),\n          tooltipText: LocaleKeys.findAndReplace_replace.tr(),\n        ),\n        const HSpace(4.0),\n        _FindAndReplaceIcon(\n          iconBuilder: (_) => const Icon(\n            Icons.change_circle_outlined,\n            size: 16,\n          ),\n          tooltipText: LocaleKeys.findAndReplace_replaceAll.tr(),\n          onPressed: () => widget.searchService.replaceAllMatches(\n            textController.text,\n          ),\n        ),\n      ],\n    );\n  }\n\n  void _replaceSelectedWord() {\n    widget.searchService.replaceSelectedWord(textController.text);\n  }\n}\n\nclass _FindAndReplaceIcon extends StatelessWidget {\n  const _FindAndReplaceIcon({\n    required this.onPressed,\n    required this.tooltipText,\n    this.icon,\n    this.iconBuilder,\n    this.isSelected,\n  });\n\n  final VoidCallback onPressed;\n  final FlowySvgData? icon;\n  final WidgetBuilder? iconBuilder;\n  final String tooltipText;\n  final bool? isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyIconButton(\n      width: 24,\n      height: 24,\n      onPressed: onPressed,\n      icon: iconBuilder?.call(context) ??\n          (icon != null\n              ? FlowySvg(icon!, color: Theme.of(context).iconTheme.color)\n              : const Placeholder()),\n      tooltipText: tooltipText,\n      isSelected: isSelected,\n      iconColorOnHover: Theme.of(context).colorScheme.onSecondary,\n    );\n  }\n}\n\nInputDecoration _buildInputDecoration(String hintText) {\n  return InputDecoration(\n    contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),\n    border: const UnderlineInputBorder(),\n    hintText: hintText,\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/shared/google_fonts_extension.dart';\nimport 'package:appflowy/util/font_family_extension.dart';\nimport 'package:appflowy/util/levenshtein.dart';\nimport 'package:appflowy/workspace/application/appearance_defaults.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/setting_value_dropdown.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/style_widget/text_field.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:google_fonts/google_fonts.dart';\n\nclass ThemeFontFamilySetting extends StatefulWidget {\n  const ThemeFontFamilySetting({\n    super.key,\n    required this.currentFontFamily,\n  });\n\n  final String currentFontFamily;\n  static Key textFieldKey = const Key('FontFamilyTextField');\n  static Key resetButtonKey = const Key('FontFamilyResetButton');\n  static Key popoverKey = const Key('FontFamilyPopover');\n\n  @override\n  State<ThemeFontFamilySetting> createState() => _ThemeFontFamilySettingState();\n}\n\nclass _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {\n  @override\n  Widget build(BuildContext context) {\n    return SettingListTile(\n      label: LocaleKeys.settings_appearance_fontFamily_label.tr(),\n      resetButtonKey: ThemeFontFamilySetting.resetButtonKey,\n      onResetRequested: () {\n        context.read<AppearanceSettingsCubit>().resetFontFamily();\n        context\n            .read<DocumentAppearanceCubit>()\n            .syncFontFamily(DefaultAppearanceSettings.kDefaultFontFamily);\n      },\n      trailing: [\n        FontFamilyDropDown(currentFontFamily: widget.currentFontFamily),\n      ],\n    );\n  }\n}\n\nclass FontFamilyDropDown extends StatefulWidget {\n  const FontFamilyDropDown({\n    super.key,\n    required this.currentFontFamily,\n    this.onOpen,\n    this.onClose,\n    this.onFontFamilyChanged,\n    this.child,\n    this.popoverController,\n    this.offset,\n    this.onResetFont,\n  });\n\n  final String currentFontFamily;\n  final VoidCallback? onOpen;\n  final VoidCallback? onClose;\n  final void Function(String fontFamily)? onFontFamilyChanged;\n  final Widget? child;\n  final PopoverController? popoverController;\n  final Offset? offset;\n  final VoidCallback? onResetFont;\n\n  @override\n  State<FontFamilyDropDown> createState() => _FontFamilyDropDownState();\n}\n\nclass _FontFamilyDropDownState extends State<FontFamilyDropDown> {\n  final List<String> availableFonts = [\n    defaultFontFamily,\n    ...GoogleFonts.asMap().keys,\n  ];\n  final ValueNotifier<String> query = ValueNotifier('');\n\n  @override\n  void dispose() {\n    query.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final currentValue = widget.currentFontFamily.fontFamilyDisplayName;\n    return SettingValueDropDown(\n      popoverKey: ThemeFontFamilySetting.popoverKey,\n      popoverController: widget.popoverController,\n      currentValue: currentValue,\n      margin: EdgeInsets.zero,\n      boxConstraints: const BoxConstraints(\n        maxWidth: 240,\n        maxHeight: 420,\n      ),\n      onClose: () {\n        query.value = '';\n        widget.onClose?.call();\n      },\n      offset: widget.offset,\n      child: widget.child,\n      popupBuilder: (_) {\n        widget.onOpen?.call();\n        return Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            Padding(\n              padding: const EdgeInsets.all(8.0),\n              child: FlowyTextField(\n                key: ThemeFontFamilySetting.textFieldKey,\n                hintText: LocaleKeys.settings_appearance_fontFamily_search.tr(),\n                autoFocus: true,\n                debounceDuration: const Duration(milliseconds: 300),\n                onChanged: (value) {\n                  setState(() {\n                    query.value = value;\n                  });\n                },\n              ),\n            ),\n            Container(height: 1, color: Theme.of(context).dividerColor),\n            ValueListenableBuilder(\n              valueListenable: query,\n              builder: (context, value, child) {\n                var displayed = availableFonts;\n                if (value.isNotEmpty) {\n                  displayed = availableFonts\n                      .where(\n                        (font) => font\n                            .toLowerCase()\n                            .contains(value.toLowerCase().toString()),\n                      )\n                      .sorted((a, b) => levenshtein(a, b))\n                      .toList();\n                }\n                return displayed.length >= 10\n                    ? Flexible(\n                        child: ListView.builder(\n                          padding: const EdgeInsets.all(8.0),\n                          itemBuilder: (context, index) =>\n                              _fontFamilyItemButton(\n                            context,\n                            getGoogleFontSafely(displayed[index]),\n                          ),\n                          itemCount: displayed.length,\n                        ),\n                      )\n                    : Padding(\n                        padding: const EdgeInsets.all(8.0),\n                        child: Column(\n                          crossAxisAlignment: CrossAxisAlignment.start,\n                          mainAxisSize: MainAxisSize.min,\n                          children: List.generate(\n                            displayed.length,\n                            (index) => _fontFamilyItemButton(\n                              context,\n                              getGoogleFontSafely(displayed[index]),\n                            ),\n                          ),\n                        ),\n                      );\n              },\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  Widget _fontFamilyItemButton(\n    BuildContext context,\n    TextStyle style,\n  ) {\n    final buttonFontFamily =\n        style.fontFamily?.parseFontFamilyName() ?? defaultFontFamily;\n    return Tooltip(\n      message: buttonFontFamily,\n      waitDuration: const Duration(milliseconds: 150),\n      child: SizedBox(\n        key: ValueKey(buttonFontFamily),\n        height: 36,\n        child: FlowyButton(\n          onHover: (_) => FocusScope.of(context).unfocus(),\n          text: FlowyText(\n            buttonFontFamily.fontFamilyDisplayName,\n            fontFamily: buttonFontFamily,\n            figmaLineHeight: 20,\n            fontWeight: FontWeight.w400,\n          ),\n          rightIcon:\n              buttonFontFamily == widget.currentFontFamily.parseFontFamilyName()\n                  ? const FlowySvg(FlowySvgs.toolbar_check_m)\n                  : null,\n          onTap: () {\n            if (widget.onFontFamilyChanged != null) {\n              widget.onFontFamilyChanged!(buttonFontFamily);\n            } else {\n              if (widget.currentFontFamily.parseFontFamilyName() !=\n                  buttonFontFamily) {\n                context\n                    .read<AppearanceSettingsCubit>()\n                    .setFontFamily(buttonFontFamily);\n                context\n                    .read<DocumentAppearanceCubit>()\n                    .syncFontFamily(buttonFontFamily);\n              }\n            }\n            PopoverContainer.of(context).close();\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart",
    "content": "import 'dart:ui';\n\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nconst String kLocalImagesKey = 'local_images';\n\nList<String> get builtInAssetImages => [\n      'assets/images/built_in_cover_images/m_cover_image_1.jpg',\n      'assets/images/built_in_cover_images/m_cover_image_2.jpg',\n      'assets/images/built_in_cover_images/m_cover_image_3.jpg',\n      'assets/images/built_in_cover_images/m_cover_image_4.jpg',\n      'assets/images/built_in_cover_images/m_cover_image_5.jpg',\n      'assets/images/built_in_cover_images/m_cover_image_6.jpg',\n    ];\n\nclass ColorOption {\n  const ColorOption({\n    required this.colorHex,\n    required this.name,\n  });\n\n  final String colorHex;\n  final String name;\n}\n\nclass CoverColorPicker extends StatefulWidget {\n  const CoverColorPicker({\n    super.key,\n    this.selectedBackgroundColorHex,\n    required this.pickerBackgroundColor,\n    required this.backgroundColorOptions,\n    required this.pickerItemHoverColor,\n    required this.onSubmittedBackgroundColorHex,\n  });\n\n  final String? selectedBackgroundColorHex;\n  final Color pickerBackgroundColor;\n  final List<ColorOption> backgroundColorOptions;\n  final Color pickerItemHoverColor;\n  final void Function(String color) onSubmittedBackgroundColorHex;\n\n  @override\n  State<CoverColorPicker> createState() => _CoverColorPickerState();\n}\n\nclass _CoverColorPickerState extends State<CoverColorPicker> {\n  final scrollController = ScrollController();\n\n  @override\n  void dispose() {\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: 30,\n      alignment: Alignment.center,\n      child: ScrollConfiguration(\n        behavior: ScrollConfiguration.of(context).copyWith(\n          dragDevices: {\n            PointerDeviceKind.touch,\n            PointerDeviceKind.mouse,\n          },\n          platform: TargetPlatform.windows,\n        ),\n        child: SingleChildScrollView(\n          scrollDirection: Axis.horizontal,\n          child: _buildColorItems(\n            widget.backgroundColorOptions,\n            widget.selectedBackgroundColorHex,\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildColorItems(List<ColorOption> options, String? selectedColor) {\n    return Row(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: options\n          .map(\n            (e) => ColorItem(\n              option: e,\n              isChecked: e.colorHex == selectedColor,\n              hoverColor: widget.pickerItemHoverColor,\n              onTap: widget.onSubmittedBackgroundColorHex,\n            ),\n          )\n          .toList(),\n    );\n  }\n}\n\n@visibleForTesting\nclass ColorItem extends StatelessWidget {\n  const ColorItem({\n    super.key,\n    required this.option,\n    required this.isChecked,\n    required this.hoverColor,\n    required this.onTap,\n  });\n\n  final ColorOption option;\n  final bool isChecked;\n  final Color hoverColor;\n  final void Function(String) onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.only(right: 10.0),\n      child: InkWell(\n        customBorder: const CircleBorder(),\n        hoverColor: hoverColor,\n        onTap: () => onTap(option.colorHex),\n        child: SizedBox.square(\n          dimension: 25,\n          child: DecoratedBox(\n            decoration: BoxDecoration(\n              color: option.colorHex.tryToColor(),\n              shape: BoxShape.circle,\n            ),\n            child: isChecked\n                ? SizedBox.square(\n                    child: Container(\n                      margin: const EdgeInsets.all(1),\n                      decoration: BoxDecoration(\n                        border: Border.all(\n                          color: Theme.of(context).cardColor,\n                          width: 3.0,\n                        ),\n                        color: option.colorHex.tryToColor(),\n                        shape: BoxShape.circle,\n                      ),\n                    ),\n                  )\n                : null,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor_bloc.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\npart 'cover_editor_bloc.freezed.dart';\n\nclass ChangeCoverPopoverBloc\n    extends Bloc<ChangeCoverPopoverEvent, ChangeCoverPopoverState> {\n  ChangeCoverPopoverBloc({required this.editorState, required this.node})\n      : super(const ChangeCoverPopoverState.initial()) {\n    SharedPreferences.getInstance().then((prefs) {\n      _prefs = prefs;\n      _initCompleter.complete();\n    });\n\n    _dispatch();\n  }\n\n  final EditorState editorState;\n  final Node node;\n  final _initCompleter = Completer<void>();\n  late final SharedPreferences _prefs;\n\n  void _dispatch() {\n    on<ChangeCoverPopoverEvent>((event, emit) async {\n      await event.map(\n        fetchPickedImagePaths: (fetchPickedImagePaths) async {\n          final imageNames = await _getPreviouslyPickedImagePaths();\n\n          emit(\n            ChangeCoverPopoverState.loaded(\n              imageNames,\n              selectLatestImage: fetchPickedImagePaths.selectLatestImage,\n            ),\n          );\n        },\n        deleteImage: (deleteImage) async {\n          final currentState = state;\n          final currentlySelectedImage =\n              node.attributes[DocumentHeaderBlockKeys.coverDetails];\n          if (currentState is _Loaded) {\n            await _deleteImageInStorage(deleteImage.path);\n            if (currentlySelectedImage == deleteImage.path) {\n              _removeCoverImageFromNode();\n            }\n            final updateImageList = currentState.imageNames\n                .where((path) => path != deleteImage.path)\n                .toList();\n            _updateImagePathsInStorage(updateImageList);\n            emit(ChangeCoverPopoverState.loaded(updateImageList));\n          }\n        },\n        clearAllImages: (clearAllImages) async {\n          final currentState = state;\n          final currentlySelectedImage =\n              node.attributes[DocumentHeaderBlockKeys.coverDetails];\n\n          if (currentState is _Loaded) {\n            for (final image in currentState.imageNames) {\n              await _deleteImageInStorage(image);\n              if (currentlySelectedImage == image) {\n                _removeCoverImageFromNode();\n              }\n            }\n            _updateImagePathsInStorage([]);\n            emit(const ChangeCoverPopoverState.loaded([]));\n          }\n        },\n      );\n    });\n  }\n\n  Future<List<String>> _getPreviouslyPickedImagePaths() async {\n    await _initCompleter.future;\n    final imageNames = _prefs.getStringList(kLocalImagesKey) ?? [];\n    if (imageNames.isEmpty) {\n      return imageNames;\n    }\n    imageNames.removeWhere((name) => !File(name).existsSync());\n    unawaited(_prefs.setStringList(kLocalImagesKey, imageNames));\n    return imageNames;\n  }\n\n  void _updateImagePathsInStorage(List<String> imagePaths) async {\n    await _initCompleter.future;\n    await _prefs.setStringList(kLocalImagesKey, imagePaths);\n  }\n\n  Future<void> _deleteImageInStorage(String path) async {\n    final imageFile = File(path);\n    await imageFile.delete();\n  }\n\n  void _removeCoverImageFromNode() {\n    final transaction = editorState.transaction;\n    transaction.updateNode(node, {\n      DocumentHeaderBlockKeys.coverType: CoverType.none.toString(),\n      DocumentHeaderBlockKeys.icon:\n          node.attributes[DocumentHeaderBlockKeys.icon],\n    });\n    editorState.apply(transaction);\n  }\n}\n\n@freezed\nclass ChangeCoverPopoverEvent with _$ChangeCoverPopoverEvent {\n  const factory ChangeCoverPopoverEvent.fetchPickedImagePaths({\n    @Default(false) bool selectLatestImage,\n  }) = _FetchPickedImagePaths;\n\n  const factory ChangeCoverPopoverEvent.deleteImage(String path) = _DeleteImage;\n  const factory ChangeCoverPopoverEvent.clearAllImages() = _ClearAllImages;\n}\n\n@freezed\nclass ChangeCoverPopoverState with _$ChangeCoverPopoverState {\n  const factory ChangeCoverPopoverState.initial() = _Initial;\n  const factory ChangeCoverPopoverState.loading() = _Loading;\n  const factory ChangeCoverPopoverState.loaded(\n    List<String> imageNames, {\n    @Default(false) selectLatestImage,\n  }) = _Loaded;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_title.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart';\nimport 'package:appflowy/shared/text_field/text_filed_with_metric_lines.dart';\nimport 'package:appflowy/workspace/application/appearance_defaults.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass CoverTitle extends StatelessWidget {\n  const CoverTitle({\n    super.key,\n    required this.view,\n  });\n\n  final ViewPB view;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => ViewBloc(view: view)..add(const ViewEvent.initial()),\n      child: _InnerCoverTitle(\n        view: view,\n      ),\n    );\n  }\n}\n\nclass _InnerCoverTitle extends StatefulWidget {\n  const _InnerCoverTitle({\n    required this.view,\n  });\n\n  final ViewPB view;\n\n  @override\n  State<_InnerCoverTitle> createState() => _InnerCoverTitleState();\n}\n\nclass _InnerCoverTitleState extends State<_InnerCoverTitle> {\n  final titleTextController = TextEditingController();\n\n  late final editorContext = context.read<SharedEditorContext>();\n  late final editorState = context.read<EditorState>();\n  late final titleFocusNode = editorContext.coverTitleFocusNode;\n  int lineCount = 1;\n\n  bool updatingViewName = false;\n\n  @override\n  void initState() {\n    super.initState();\n\n    titleTextController.text = widget.view.name;\n    titleTextController.addListener(_onViewNameChanged);\n\n    titleFocusNode\n      ..onKeyEvent = _onKeyEvent\n      ..addListener(_onFocusChanged);\n\n    editorState.selectionNotifier.addListener(_onSelectionChanged);\n\n    _requestInitialFocus();\n  }\n\n  @override\n  void dispose() {\n    titleFocusNode\n      ..onKeyEvent = null\n      ..removeListener(_onFocusChanged);\n    titleTextController.dispose();\n    editorState.selectionNotifier.removeListener(_onSelectionChanged);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final fontStyle = Theme.of(context)\n        .textTheme\n        .bodyMedium!\n        .copyWith(fontSize: 40.0, fontWeight: FontWeight.w700);\n    final width = context.read<DocumentAppearanceCubit>().state.width;\n    return BlocConsumer<ViewBloc, ViewState>(\n      listenWhen: (previous, current) =>\n          previous.view.name != current.view.name && !updatingViewName,\n      listener: _onListen,\n      builder: (context, state) {\n        final appearance = context.read<DocumentAppearanceCubit>().state;\n        return Container(\n          constraints: BoxConstraints(maxWidth: width),\n          child: Theme(\n            data: Theme.of(context).copyWith(\n              textSelectionTheme: TextSelectionThemeData(\n                cursorColor: appearance.selectionColor,\n                selectionColor: appearance.selectionColor ??\n                    DefaultAppearanceSettings.getDefaultSelectionColor(context),\n              ),\n            ),\n            child: TextFieldWithMetricLines(\n              controller: titleTextController,\n              enabled: editorState.editable,\n              focusNode: titleFocusNode,\n              style: fontStyle,\n              onLineCountChange: (count) => lineCount = count,\n              decoration: InputDecoration(\n                border: InputBorder.none,\n                hintText: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),\n                hintStyle: fontStyle.copyWith(\n                  color: Theme.of(context).hintColor,\n                ),\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  void _requestInitialFocus() {\n    if (editorContext.requestCoverTitleFocus) {\n      void requestFocus() {\n        titleFocusNode.canRequestFocus = true;\n        titleFocusNode.requestFocus();\n        editorContext.requestCoverTitleFocus = false;\n      }\n\n      // on macOS, if we gain focus immediately, the focus won't work.\n      // It's a workaround to delay the focus request.\n      if (UniversalPlatform.isMacOS) {\n        Future.delayed(Durations.short4, () {\n          requestFocus();\n        });\n      } else {\n        WidgetsBinding.instance.addPostFrameCallback((_) {\n          requestFocus();\n        });\n      }\n    }\n  }\n\n  void _onSelectionChanged() {\n    // if title is focused and the selection is not null, clear the selection\n    if (editorState.selection != null && titleFocusNode.hasFocus) {\n      Log.info('title is focused, clear the editor selection');\n      editorState.selection = null;\n    }\n  }\n\n  void _onListen(BuildContext context, ViewState state) {\n    _requestFocusIfNeeded(widget.view, state);\n\n    if (state.view.name != titleTextController.text) {\n      titleTextController.text = state.view.name;\n    }\n  }\n\n  bool _shouldFocus(ViewPB view, ViewState? state) {\n    final name = state?.view.name ?? view.name;\n\n    if (editorState.document.root.children.isNotEmpty) {\n      return false;\n    }\n\n    // if the view's name is empty, focus on the title\n    if (name.isEmpty) {\n      return true;\n    }\n\n    return false;\n  }\n\n  void _requestFocusIfNeeded(ViewPB view, ViewState? state) {\n    final shouldFocus = _shouldFocus(view, state);\n    if (shouldFocus) {\n      titleFocusNode.requestFocus();\n    }\n  }\n\n  void _onFocusChanged() {\n    if (titleFocusNode.hasFocus) {\n      // if the document is empty, disable the keyboard service\n      final children = editorState.document.root.children;\n      final firstDelta = children.firstOrNull?.delta;\n      final isEmptyDocument =\n          children.length == 1 && (firstDelta == null || firstDelta.isEmpty);\n      if (!isEmptyDocument) {\n        return;\n      }\n\n      if (editorState.selection != null) {\n        Log.info('cover title got focus, clear the editor selection');\n        editorState.selection = null;\n      }\n\n      Log.info('cover title got focus, disable keyboard service');\n      editorState.service.keyboardService?.disable();\n    } else {\n      Log.info('cover title lost focus, enable keyboard service');\n      editorState.service.keyboardService?.enable();\n    }\n  }\n\n  void _onViewNameChanged() {\n    updatingViewName = true;\n\n    Debounce.debounce(\n      'update view name',\n      const Duration(milliseconds: 250),\n      () {\n        if (!mounted) {\n          return;\n        }\n        if (context.read<ViewBloc>().state.view.name !=\n            titleTextController.text) {\n          context\n              .read<ViewBloc>()\n              .add(ViewEvent.rename(titleTextController.text));\n        }\n        context\n            .read<ViewInfoBloc?>()\n            ?.add(ViewInfoEvent.titleChanged(titleTextController.text));\n\n        updatingViewName = false;\n      },\n    );\n  }\n\n  KeyEventResult _onKeyEvent(FocusNode focusNode, KeyEvent event) {\n    if (event is KeyUpEvent) {\n      return KeyEventResult.ignored;\n    }\n\n    if (event.logicalKey == LogicalKeyboardKey.enter) {\n      // if enter is pressed, jump the first line of editor.\n      _createNewLine();\n      return KeyEventResult.handled;\n    } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {\n      return _moveCursorToNextLine(event.logicalKey);\n    } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {\n      return _moveCursorToNextLine(event.logicalKey);\n    } else if (event.logicalKey == LogicalKeyboardKey.escape) {\n      return _exitEditing();\n    } else if (event.logicalKey == LogicalKeyboardKey.tab) {\n      return KeyEventResult.handled;\n    }\n\n    return KeyEventResult.ignored;\n  }\n\n  KeyEventResult _exitEditing() {\n    titleFocusNode.unfocus();\n    return KeyEventResult.handled;\n  }\n\n  Future<void> _createNewLine() async {\n    titleFocusNode.unfocus();\n\n    final selection = titleTextController.selection;\n    final text = titleTextController.text;\n    // split the text into two lines based on the cursor position\n    final parts = [\n      text.substring(0, selection.baseOffset),\n      text.substring(selection.baseOffset),\n    ];\n    titleTextController.text = parts[0];\n\n    final transaction = editorState.transaction;\n    transaction.insertNode([0], paragraphNode(text: parts[1]));\n    await editorState.apply(transaction);\n\n    // update selection instead of using afterSelection in transaction,\n    //  because it will cause the cursor to jump\n    await editorState.updateSelectionWithReason(\n      Selection.collapsed(Position(path: [0])),\n      // trigger the keyboard service.\n      reason: SelectionUpdateReason.uiEvent,\n    );\n  }\n\n  KeyEventResult _moveCursorToNextLine(LogicalKeyboardKey key) {\n    final selection = titleTextController.selection;\n    final text = titleTextController.text;\n\n    // if the cursor is not at the end of the text, ignore the event\n    if ((key == LogicalKeyboardKey.arrowRight || lineCount != 1) &&\n        (!selection.isCollapsed || text.length != selection.extentOffset)) {\n      return KeyEventResult.ignored;\n    }\n\n    final node = editorState.getNodeAtPath([0]);\n    if (node == null) {\n      _createNewLine();\n      return KeyEventResult.handled;\n    }\n\n    titleFocusNode.unfocus();\n\n    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n      // delay the update selection to wait for the title to unfocus\n      int offset = 0;\n      if (key == LogicalKeyboardKey.arrowDown) {\n        offset = node.delta?.length ?? 0;\n      } else if (key == LogicalKeyboardKey.arrowRight) {\n        offset = 0;\n      }\n      editorState.updateSelectionWithReason(\n        Selection.collapsed(\n          Position(path: [0], offset: offset),\n        ),\n        // trigger the keyboard service.\n        reason: SelectionUpdateReason.uiEvent,\n      );\n    });\n\n    return KeyEventResult.handled;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/custom_cover_picker_bloc.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/snap_bar.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/style_widget/text_field.dart';\nimport 'package:flowy_infra_ui/widget/rounded_button.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass CoverImagePicker extends StatefulWidget {\n  const CoverImagePicker({\n    super.key,\n    required this.onBackPressed,\n    required this.onFileSubmit,\n  });\n\n  final VoidCallback onBackPressed;\n  final Function(List<String> paths) onFileSubmit;\n\n  @override\n  State<CoverImagePicker> createState() => _CoverImagePickerState();\n}\n\nclass _CoverImagePickerState extends State<CoverImagePicker> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => CoverImagePickerBloc()\n        ..add(const CoverImagePickerEvent.initialEvent()),\n      child: BlocListener<CoverImagePickerBloc, CoverImagePickerState>(\n        listener: (context, state) {\n          state.maybeWhen(\n            networkImage: (successOrFail) {\n              successOrFail.fold(\n                (s) {},\n                (e) => showSnapBar(\n                  context,\n                  LocaleKeys.document_plugins_cover_invalidImageUrl.tr(),\n                ),\n              );\n            },\n            done: (successOrFail) {\n              successOrFail.fold(\n                (l) => widget.onFileSubmit(l),\n                (r) => showSnapBar(\n                  context,\n                  LocaleKeys.document_plugins_cover_failedToAddImageToGallery\n                      .tr(),\n                ),\n              );\n            },\n            orElse: () {},\n          );\n        },\n        child: BlocBuilder<CoverImagePickerBloc, CoverImagePickerState>(\n          builder: (context, state) {\n            return Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                state.maybeMap(\n                  loading: (_) => const SizedBox(\n                    height: 180,\n                    child: Center(\n                      child: CircularProgressIndicator(),\n                    ),\n                  ),\n                  orElse: () => CoverImagePreviewWidget(state: state),\n                ),\n                const VSpace(10),\n                NetworkImageUrlInput(\n                  onAdd: (url) {\n                    context\n                        .read<CoverImagePickerBloc>()\n                        .add(CoverImagePickerEvent.urlSubmit(url));\n                  },\n                ),\n                const VSpace(10),\n                ImagePickerActionButtons(\n                  onBackPressed: () {\n                    widget.onBackPressed();\n                  },\n                  onSave: () {\n                    context\n                        .read<CoverImagePickerBloc>()\n                        .add(CoverImagePickerEvent.saveToGallery(state));\n                  },\n                ),\n              ],\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass NetworkImageUrlInput extends StatefulWidget {\n  const NetworkImageUrlInput({super.key, required this.onAdd});\n\n  final void Function(String color) onAdd;\n\n  @override\n  State<NetworkImageUrlInput> createState() => _NetworkImageUrlInputState();\n}\n\nclass _NetworkImageUrlInputState extends State<NetworkImageUrlInput> {\n  TextEditingController urlController = TextEditingController();\n  bool get buttonDisabled => urlController.text.isEmpty;\n\n  @override\n  void initState() {\n    super.initState();\n    urlController.addListener(_updateState);\n  }\n\n  void _updateState() => setState(() {});\n\n  @override\n  void dispose() {\n    urlController.removeListener(_updateState);\n    urlController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        Expanded(\n          flex: 4,\n          child: FlowyTextField(\n            controller: urlController,\n            hintText: LocaleKeys.document_plugins_cover_enterImageUrl.tr(),\n          ),\n        ),\n        const SizedBox(\n          width: 5,\n        ),\n        Expanded(\n          child: RoundedTextButton(\n            onPressed: () {\n              urlController.text.isNotEmpty\n                  ? widget.onAdd(urlController.text)\n                  : null;\n            },\n            hoverColor: Colors.transparent,\n            fillColor: buttonDisabled\n                ? Theme.of(context).disabledColor\n                : Theme.of(context).colorScheme.primary,\n            height: 36,\n            title: LocaleKeys.document_plugins_cover_add.tr(),\n            borderRadius: Corners.s8Border,\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass ImagePickerActionButtons extends StatelessWidget {\n  const ImagePickerActionButtons({\n    super.key,\n    required this.onBackPressed,\n    required this.onSave,\n  });\n\n  final VoidCallback onBackPressed;\n  final VoidCallback onSave;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.end,\n      children: [\n        FlowyTextButton(\n          LocaleKeys.document_plugins_cover_back.tr(),\n          hoverColor: Theme.of(context).colorScheme.secondaryContainer,\n          fillColor: Colors.transparent,\n          mainAxisAlignment: MainAxisAlignment.end,\n          onPressed: () => onBackPressed(),\n        ),\n        FlowyTextButton(\n          LocaleKeys.document_plugins_cover_saveToGallery.tr(),\n          onPressed: () => onSave(),\n          hoverColor: Theme.of(context).colorScheme.secondaryContainer,\n          fillColor: Colors.transparent,\n          mainAxisAlignment: MainAxisAlignment.end,\n          fontColor: Theme.of(context).colorScheme.primary,\n        ),\n      ],\n    );\n  }\n}\n\nclass CoverImagePreviewWidget extends StatefulWidget {\n  const CoverImagePreviewWidget({super.key, required this.state});\n\n  final CoverImagePickerState state;\n\n  @override\n  State<CoverImagePreviewWidget> createState() =>\n      _CoverImagePreviewWidgetState();\n}\n\nclass _CoverImagePreviewWidgetState extends State<CoverImagePreviewWidget> {\n  DecoratedBox _buildFilePickerWidget(BuildContext ctx) {\n    return DecoratedBox(\n      decoration: BoxDecoration(\n        color: Theme.of(context).cardColor,\n        borderRadius: Corners.s6Border,\n        border: Border.fromBorderSide(\n          BorderSide(\n            color: Theme.of(context).colorScheme.primary,\n          ),\n        ),\n      ),\n      child: Column(\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          Row(\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              const FlowySvg(\n                FlowySvgs.add_s,\n                size: Size(20, 20),\n              ),\n              const SizedBox(\n                width: 3,\n              ),\n              FlowyText(\n                LocaleKeys.document_plugins_cover_pasteImageUrl.tr(),\n              ),\n            ],\n          ),\n          const VSpace(10),\n          FlowyText(\n            LocaleKeys.document_plugins_cover_or.tr(),\n            fontWeight: FontWeight.w300,\n          ),\n          const VSpace(10),\n          FlowyButton(\n            hoverColor: Theme.of(context).hoverColor,\n            onTap: () {\n              ctx\n                  .read<CoverImagePickerBloc>()\n                  .add(const CoverImagePickerEvent.pickFileImage());\n            },\n            useIntrinsicWidth: true,\n            leftIcon: const FlowySvg(\n              FlowySvgs.document_s,\n              size: Size(20, 20),\n            ),\n            text: FlowyText(\n              lineHeight: 1.0,\n              LocaleKeys.document_plugins_cover_pickFromFiles.tr(),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Positioned _buildImageDeleteButton(BuildContext ctx) {\n    return Positioned(\n      right: 10,\n      top: 10,\n      child: InkWell(\n        onTap: () {\n          ctx\n              .read<CoverImagePickerBloc>()\n              .add(const CoverImagePickerEvent.deleteImage());\n        },\n        child: DecoratedBox(\n          decoration: BoxDecoration(\n            shape: BoxShape.circle,\n            color: Theme.of(context).colorScheme.onPrimary,\n          ),\n          child: const FlowySvg(\n            FlowySvgs.close_s,\n            size: Size(20, 20),\n          ),\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        Container(\n          height: 180,\n          alignment: Alignment.center,\n          decoration: BoxDecoration(\n            color: Theme.of(context).colorScheme.secondary,\n            borderRadius: Corners.s6Border,\n            image: widget.state.whenOrNull(\n              networkImage: (successOrFail) {\n                return successOrFail.fold(\n                  (path) => DecorationImage(\n                    image: NetworkImage(path),\n                    fit: BoxFit.cover,\n                  ),\n                  (r) => null,\n                );\n              },\n              fileImage: (path) {\n                return DecorationImage(\n                  image: FileImage(File(path)),\n                  fit: BoxFit.cover,\n                );\n              },\n            ),\n          ),\n          child: widget.state.whenOrNull(\n            initial: () => _buildFilePickerWidget(context),\n            networkImage: (successOrFail) => successOrFail.fold(\n              (l) => null,\n              (r) => _buildFilePickerWidget(\n                context,\n              ),\n            ),\n          ),\n        ),\n        widget.state.maybeWhen(\n          fileImage: (_) => _buildImageDeleteButton(context),\n          networkImage: (successOrFail) => successOrFail.fold(\n            (l) => _buildImageDeleteButton(context),\n            (r) => const SizedBox.shrink(),\n          ),\n          orElse: () => const SizedBox.shrink(),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker_bloc.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/default_extensions.dart';\nimport 'package:appflowy/workspace/application/settings/prelude.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/file_picker/file_picker_service.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport 'cover_editor.dart';\n\npart 'custom_cover_picker_bloc.freezed.dart';\n\nclass CoverImagePickerBloc\n    extends Bloc<CoverImagePickerEvent, CoverImagePickerState> {\n  CoverImagePickerBloc() : super(const CoverImagePickerState.initial()) {\n    _dispatch();\n  }\n\n  void _dispatch() {\n    on<CoverImagePickerEvent>(\n      (event, emit) async {\n        await event.map(\n          initialEvent: (initialEvent) {\n            emit(const CoverImagePickerState.initial());\n          },\n          urlSubmit: (urlSubmit) async {\n            emit(const CoverImagePickerState.loading());\n            final validateImage = await _validateURL(urlSubmit.path);\n            if (validateImage) {\n              emit(\n                CoverImagePickerState.networkImage(\n                  FlowyResult.success(urlSubmit.path),\n                ),\n              );\n            } else {\n              emit(\n                CoverImagePickerState.networkImage(\n                  FlowyResult.failure(\n                    FlowyError(\n                      msg: LocaleKeys.document_plugins_cover_couldNotFetchImage\n                          .tr(),\n                    ),\n                  ),\n                ),\n              );\n            }\n          },\n          pickFileImage: (pickFileImage) async {\n            final imagePickerResults = await _pickImages();\n            if (imagePickerResults != null) {\n              emit(CoverImagePickerState.fileImage(imagePickerResults));\n            } else {\n              emit(const CoverImagePickerState.initial());\n            }\n          },\n          deleteImage: (deleteImage) {\n            emit(const CoverImagePickerState.initial());\n          },\n          saveToGallery: (saveToGallery) async {\n            emit(const CoverImagePickerState.loading());\n            final saveImage = await _saveToGallery(saveToGallery.previousState);\n            if (saveImage != null) {\n              emit(CoverImagePickerState.done(FlowyResult.success(saveImage)));\n            } else {\n              emit(\n                CoverImagePickerState.done(\n                  FlowyResult.failure(\n                    FlowyError(\n                      msg: LocaleKeys.document_plugins_cover_imageSavingFailed\n                          .tr(),\n                    ),\n                  ),\n                ),\n              );\n              emit(const CoverImagePickerState.initial());\n            }\n          },\n        );\n      },\n    );\n  }\n\n  Future<List<String>?>? _saveToGallery(CoverImagePickerState state) async {\n    final SharedPreferences prefs = await SharedPreferences.getInstance();\n    final List<String> imagePaths = prefs.getStringList(kLocalImagesKey) ?? [];\n    final directory = await _coverPath();\n\n    if (state is _FileImagePicked) {\n      try {\n        final path = state.path;\n        final newPath = p.join(directory, p.split(path).last);\n        final newFile = await File(path).copy(newPath);\n        imagePaths.add(newFile.path);\n      } catch (e) {\n        return null;\n      }\n    } else if (state is _NetworkImagePicked) {\n      try {\n        final url = state.successOrFail.fold((path) => path, (r) => null);\n        if (url != null) {\n          final response = await http.get(Uri.parse(url));\n          final newPath = p.join(directory, _networkImageName(url));\n          final imageFile = File(newPath);\n          await imageFile.create();\n          await imageFile.writeAsBytes(response.bodyBytes);\n          imagePaths.add(imageFile.absolute.path);\n        } else {\n          return null;\n        }\n      } catch (e) {\n        return null;\n      }\n    }\n    await prefs.setStringList(kLocalImagesKey, imagePaths);\n    return imagePaths;\n  }\n\n  Future<String?> _pickImages() async {\n    final result = await getIt<FilePickerService>().pickFiles(\n      dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(),\n      type: FileType.image,\n      allowedExtensions: defaultImageExtensions,\n    );\n    if (result != null && result.files.isNotEmpty) {\n      return result.files.first.path;\n    }\n    return null;\n  }\n\n  Future<String> _coverPath() async {\n    final directory = await getIt<ApplicationDataStorage>().getPath();\n    return Directory(p.join(directory, 'covers'))\n        .create(recursive: true)\n        .then((value) => value.path);\n  }\n\n  String _networkImageName(String url) {\n    return 'IMG_${DateTime.now().millisecondsSinceEpoch.toString()}.${_getExtension(\n      url,\n      fromNetwork: true,\n    )}';\n  }\n\n  String? _getExtension(\n    String path, {\n    bool fromNetwork = false,\n  }) {\n    String? ext;\n    if (!fromNetwork) {\n      final extension = p.extension(path);\n      if (extension.isEmpty) {\n        return null;\n      }\n      ext = extension;\n    } else {\n      final uri = Uri.parse(path);\n      final parameters = uri.queryParameters;\n      if (path.contains('unsplash')) {\n        final dl = parameters['dl'];\n        if (dl != null) {\n          ext = p.extension(dl);\n        }\n      } else {\n        ext = p.extension(path);\n      }\n    }\n    if (ext != null && ext.isNotEmpty) {\n      ext = ext.substring(1);\n    }\n    if (defaultImageExtensions.contains(ext)) {\n      return ext;\n    }\n    return null;\n  }\n\n  Future<bool> _validateURL(String path) async {\n    final extension = _getExtension(path, fromNetwork: true);\n    if (extension == null) {\n      return false;\n    }\n    try {\n      final response = await http.head(Uri.parse(path));\n      return response.statusCode == 200;\n    } catch (e) {\n      return false;\n    }\n  }\n}\n\n@freezed\nclass CoverImagePickerEvent with _$CoverImagePickerEvent {\n  const factory CoverImagePickerEvent.urlSubmit(String path) = _UrlSubmit;\n  const factory CoverImagePickerEvent.pickFileImage() = _PickFileImage;\n  const factory CoverImagePickerEvent.deleteImage() = _DeleteImage;\n  const factory CoverImagePickerEvent.saveToGallery(\n    CoverImagePickerState previousState,\n  ) = _SaveToGallery;\n  const factory CoverImagePickerEvent.initialEvent() = _InitialEvent;\n}\n\n@freezed\nclass CoverImagePickerState with _$CoverImagePickerState {\n  const factory CoverImagePickerState.initial() = _Initial;\n  const factory CoverImagePickerState.loading() = _Loading;\n  const factory CoverImagePickerState.networkImage(\n    FlowyResult<String, FlowyError> successOrFail,\n  ) = _NetworkImagePicked;\n  const factory CoverImagePickerState.fileImage(String path) = _FileImagePicked;\n\n  const factory CoverImagePickerState.done(\n    FlowyResult<List<String>, FlowyError> successOrFail,\n  ) = _Done;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\n\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/plugins/document/application/prelude.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy/shared/flowy_gradient_colors.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:string_validator/string_validator.dart';\n\n/// This is a transitional component that can be removed once the desktop\n///  supports immersive widgets, allowing for the exclusive use of the DocumentImmersiveCover component.\nclass DesktopCover extends StatefulWidget {\n  const DesktopCover({\n    super.key,\n    required this.view,\n    required this.editorState,\n    required this.node,\n    required this.coverType,\n    this.coverDetails,\n  });\n\n  final ViewPB view;\n  final Node node;\n  final EditorState editorState;\n  final CoverType coverType;\n  final String? coverDetails;\n\n  @override\n  State<DesktopCover> createState() => _DesktopCoverState();\n}\n\nclass _DesktopCoverState extends State<DesktopCover> {\n  CoverType get coverType => CoverType.fromString(\n        widget.node.attributes[DocumentHeaderBlockKeys.coverType],\n      );\n  String? get coverDetails =>\n      widget.node.attributes[DocumentHeaderBlockKeys.coverDetails];\n\n  @override\n  Widget build(BuildContext context) {\n    if (widget.view.extra.isEmpty) {\n      return _buildCoverImageV1();\n    }\n\n    return _buildCoverImageV2();\n  }\n\n  // version > 0.5.5\n  Widget _buildCoverImageV2() {\n    return BlocProvider(\n      create: (context) => DocumentImmersiveCoverBloc(view: widget.view)\n        ..add(const DocumentImmersiveCoverEvent.initial()),\n      child:\n          BlocBuilder<DocumentImmersiveCoverBloc, DocumentImmersiveCoverState>(\n        builder: (context, state) {\n          final cover = state.cover;\n          final type = state.cover.type;\n          const height = kCoverHeight;\n\n          if (type == PageStyleCoverImageType.customImage ||\n              type == PageStyleCoverImageType.unsplashImage) {\n            final userProfilePB =\n                context.read<DocumentBloc>().state.userProfilePB;\n            return SizedBox(\n              height: height,\n              width: double.infinity,\n              child: FlowyNetworkImage(\n                url: cover.value,\n                userProfilePB: userProfilePB,\n              ),\n            );\n          }\n\n          if (type == PageStyleCoverImageType.builtInImage) {\n            return SizedBox(\n              height: height,\n              width: double.infinity,\n              child: Image.asset(\n                PageStyleCoverImageType.builtInImagePath(cover.value),\n                fit: BoxFit.cover,\n              ),\n            );\n          }\n\n          if (type == PageStyleCoverImageType.pureColor) {\n            // try to parse the color from the tint id,\n            //  if it fails, try to parse the color as a hex string\n            final color = FlowyTint.fromId(cover.value)?.color(context) ??\n                cover.value.tryToColor();\n            return Container(\n              height: height,\n              width: double.infinity,\n              color: color,\n            );\n          }\n\n          if (type == PageStyleCoverImageType.gradientColor) {\n            return Container(\n              height: height,\n              width: double.infinity,\n              decoration: BoxDecoration(\n                gradient: FlowyGradientColor.fromId(cover.value).linear,\n              ),\n            );\n          }\n\n          if (type == PageStyleCoverImageType.localImage) {\n            return SizedBox(\n              height: height,\n              width: double.infinity,\n              child: Image.file(\n                File(cover.value),\n                fit: BoxFit.cover,\n              ),\n            );\n          }\n\n          return const SizedBox.shrink();\n        },\n      ),\n    );\n  }\n\n  // version <= 0.5.5\n  Widget _buildCoverImageV1() {\n    final detail = coverDetails;\n    if (detail == null) {\n      return const SizedBox.shrink();\n    }\n    switch (widget.coverType) {\n      case CoverType.file:\n        if (isURL(detail)) {\n          final userProfilePB =\n              context.read<DocumentBloc>().state.userProfilePB;\n          return FlowyNetworkImage(\n            url: detail,\n            userProfilePB: userProfilePB,\n            errorWidgetBuilder: (context, url, error) =>\n                const SizedBox.shrink(),\n          );\n        }\n        final imageFile = File(detail);\n        if (!imageFile.existsSync()) {\n          return const SizedBox.shrink();\n        }\n        return Image.file(\n          imageFile,\n          fit: BoxFit.cover,\n        );\n      case CoverType.asset:\n        return Image.asset(\n          PageStyleCoverImageType.builtInImagePath(detail),\n          fit: BoxFit.cover,\n        );\n      case CoverType.color:\n        final color = widget.coverDetails?.tryToColor() ?? Colors.white;\n        return Container(color: color);\n      case CoverType.none:\n        return const SizedBox.shrink();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart",
    "content": "import 'dart:io';\nimport 'dart:math';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/desktop_cover.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_listener.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu;\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/widget/rounded_button.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:string_validator/string_validator.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'cover_title.dart';\n\nconst double kCoverHeight = 280.0;\nconst double kIconHeight = 60.0;\nconst double kToolbarHeight = 40.0; // with padding to the top\n\n// Remove this widget if the desktop support immersive cover.\nclass DocumentHeaderBlockKeys {\n  const DocumentHeaderBlockKeys._();\n\n  static const String coverType = 'cover_selection_type';\n  static const String coverDetails = 'cover_selection';\n  static const String icon = 'selected_icon';\n}\n\n// for the version under 0.5.5, including 0.5.5\nenum CoverType {\n  none,\n  color,\n  file,\n  asset;\n\n  static CoverType fromString(String? value) {\n    if (value == null) {\n      return CoverType.none;\n    }\n    return CoverType.values.firstWhere(\n      (e) => e.toString() == value,\n      orElse: () => CoverType.none,\n    );\n  }\n}\n\n// This key is used to intercept the selection event in the document cover widget.\nconst _interceptorKey = 'document_cover_widget_interceptor';\n\nclass DocumentCoverWidget extends StatefulWidget {\n  const DocumentCoverWidget({\n    super.key,\n    required this.node,\n    required this.editorState,\n    required this.onIconChanged,\n    required this.view,\n    required this.tabs,\n  });\n\n  final Node node;\n  final EditorState editorState;\n  final ValueChanged<EmojiIconData> onIconChanged;\n  final ViewPB view;\n  final List<PickerTabType> tabs;\n\n  @override\n  State<DocumentCoverWidget> createState() => _DocumentCoverWidgetState();\n}\n\nclass _DocumentCoverWidgetState extends State<DocumentCoverWidget> {\n  CoverType get coverType => CoverType.fromString(\n        widget.node.attributes[DocumentHeaderBlockKeys.coverType],\n      );\n\n  String? get coverDetails =>\n      widget.node.attributes[DocumentHeaderBlockKeys.coverDetails];\n\n  String? get icon => widget.node.attributes[DocumentHeaderBlockKeys.icon];\n\n  bool get hasIcon => viewIcon.emoji.isNotEmpty;\n\n  bool get hasCover =>\n      coverType != CoverType.none ||\n      (cover != null && cover?.type != PageStyleCoverImageType.none);\n\n  RenderBox? get _renderBox => context.findRenderObject() as RenderBox?;\n\n  EmojiIconData viewIcon = EmojiIconData.none();\n\n  PageStyleCover? cover;\n  late ViewPB view;\n  late final ViewListener viewListener;\n  int retryCount = 0;\n\n  final isCoverTitleHovered = ValueNotifier<bool>(false);\n\n  late final gestureInterceptor = SelectionGestureInterceptor(\n    key: _interceptorKey,\n    canTap: (details) => !_isTapInBounds(details.globalPosition),\n    canPanStart: (details) => !_isDragInBounds(details.globalPosition),\n  );\n\n  @override\n  void initState() {\n    super.initState();\n    final icon = widget.view.icon;\n    viewIcon = EmojiIconData.fromViewIconPB(icon);\n    cover = widget.view.cover;\n    view = widget.view;\n    widget.node.addListener(_reload);\n    widget.editorState.service.selectionService\n        .registerGestureInterceptor(gestureInterceptor);\n\n    viewListener = ViewListener(viewId: widget.view.id)\n      ..start(\n        onViewUpdated: (view) {\n          setState(() {\n            viewIcon = EmojiIconData.fromViewIconPB(view.icon);\n            cover = view.cover;\n            view = view;\n          });\n        },\n      );\n  }\n\n  @override\n  void dispose() {\n    viewListener.stop();\n    widget.node.removeListener(_reload);\n    isCoverTitleHovered.dispose();\n    widget.editorState.service.selectionService\n        .unregisterGestureInterceptor(_interceptorKey);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return IgnorePointer(\n      ignoring: !widget.editorState.editable,\n      child: LayoutBuilder(\n        builder: (context, constraints) {\n          final offset = _calculateIconLeft(context, constraints);\n          return Column(\n            mainAxisSize: MainAxisSize.min,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Stack(\n                children: [\n                  SizedBox(\n                    height: _calculateOverallHeight(),\n                    child: DocumentHeaderToolbar(\n                      onIconOrCoverChanged: _saveIconOrCover,\n                      node: widget.node,\n                      editorState: widget.editorState,\n                      hasCover: hasCover,\n                      hasIcon: hasIcon,\n                      offset: offset,\n                      isCoverTitleHovered: isCoverTitleHovered,\n                      documentId: view.id,\n                      tabs: widget.tabs,\n                    ),\n                  ),\n                  if (hasCover)\n                    DocumentCover(\n                      view: view,\n                      editorState: widget.editorState,\n                      node: widget.node,\n                      coverType: coverType,\n                      coverDetails: coverDetails,\n                      onChangeCover: (type, details) =>\n                          _saveIconOrCover(cover: (type, details)),\n                    ),\n                  _buildAlignedCoverIcon(context),\n                ],\n              ),\n              _buildAlignedTitle(context),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildAlignedTitle(BuildContext context) {\n    return Center(\n      child: Container(\n        constraints: BoxConstraints(\n          maxWidth: widget.editorState.editorStyle.maxWidth ?? double.infinity,\n        ),\n        padding: widget.editorState.editorStyle.padding +\n            const EdgeInsets.symmetric(horizontal: 44),\n        child: MouseRegion(\n          onEnter: (event) => isCoverTitleHovered.value = true,\n          onExit: (event) => isCoverTitleHovered.value = false,\n          child: CoverTitle(\n            view: widget.view,\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildAlignedCoverIcon(BuildContext context) {\n    if (!hasIcon) {\n      return const SizedBox.shrink();\n    }\n\n    return Positioned(\n      bottom: hasCover ? kToolbarHeight - kIconHeight / 2 : kToolbarHeight,\n      left: 0,\n      right: 0,\n      child: Center(\n        child: Container(\n          constraints: BoxConstraints(\n            maxWidth:\n                widget.editorState.editorStyle.maxWidth ?? double.infinity,\n          ),\n          padding: widget.editorState.editorStyle.padding +\n              const EdgeInsets.symmetric(horizontal: 44),\n          child: Row(\n            children: [\n              DocumentIcon(\n                editorState: widget.editorState,\n                node: widget.node,\n                icon: viewIcon,\n                documentId: view.id,\n                onChangeIcon: (icon) => _saveIconOrCover(icon: icon),\n              ),\n              Spacer(),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  void _reload() => setState(() {});\n\n  double _calculateIconLeft(BuildContext context, BoxConstraints constraints) {\n    final editorState = context.read<EditorState>();\n    final appearanceCubit = context.read<DocumentAppearanceCubit>();\n\n    final renderBox = editorState.renderBox;\n\n    if (renderBox == null || !renderBox.hasSize) {}\n\n    var renderBoxWidth = 0.0;\n    if (renderBox != null && renderBox.hasSize) {\n      renderBoxWidth = renderBox.size.width;\n    } else if (retryCount <= 3) {\n      retryCount++;\n      // this is a workaround for the issue that the renderBox is not initialized\n      WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n        _reload();\n      });\n      return 0;\n    }\n\n    // if the renderBox width equals to 0, it means the editor is not initialized\n    final editorWidth = renderBoxWidth != 0\n        ? min(renderBoxWidth, appearanceCubit.state.width)\n        : appearanceCubit.state.width;\n\n    // left padding + editor width + right padding = the width of the editor\n    final leftOffset = (constraints.maxWidth - editorWidth) / 2.0 +\n        EditorStyleCustomizer.documentPadding.right;\n\n    // ensure the offset is not negative\n    return max(0, leftOffset);\n  }\n\n  double _calculateOverallHeight() {\n    final height = switch ((hasIcon, hasCover)) {\n      (true, true) => kCoverHeight + kToolbarHeight,\n      (true, false) => 50 + kIconHeight + kToolbarHeight,\n      (false, true) => kCoverHeight + kToolbarHeight,\n      (false, false) => kToolbarHeight,\n    };\n\n    return height;\n  }\n\n  void _saveIconOrCover({\n    (CoverType, String?)? cover,\n    EmojiIconData? icon,\n  }) async {\n    if (!widget.editorState.editable) {\n      return;\n    }\n\n    final transaction = widget.editorState.transaction;\n    final coverType = widget.node.attributes[DocumentHeaderBlockKeys.coverType];\n    final coverDetails =\n        widget.node.attributes[DocumentHeaderBlockKeys.coverDetails];\n    final Map<String, dynamic> attributes = {\n      DocumentHeaderBlockKeys.coverType: coverType,\n      DocumentHeaderBlockKeys.coverDetails: coverDetails,\n      DocumentHeaderBlockKeys.icon:\n          widget.node.attributes[DocumentHeaderBlockKeys.icon],\n      CustomImageBlockKeys.imageType: '1',\n    };\n    if (cover != null) {\n      attributes[DocumentHeaderBlockKeys.coverType] = cover.$1.toString();\n      attributes[DocumentHeaderBlockKeys.coverDetails] = cover.$2;\n    }\n    if (icon != null) {\n      attributes[DocumentHeaderBlockKeys.icon] = icon.emoji;\n      widget.onIconChanged(icon);\n    }\n\n    // compatible with version <= 0.5.5.\n    transaction.updateNode(widget.node, attributes);\n    await widget.editorState.apply(transaction);\n\n    // compatible with version > 0.5.5.\n    EditorMigration.migrateCoverIfNeeded(\n      widget.view,\n      attributes,\n      overwrite: true,\n    );\n  }\n\n  bool _isTapInBounds(Offset offset) {\n    if (_renderBox == null) {\n      return false;\n    }\n\n    final localPosition = _renderBox!.globalToLocal(offset);\n    return _renderBox!.paintBounds.contains(localPosition);\n  }\n\n  bool _isDragInBounds(Offset offset) {\n    if (_renderBox == null) {\n      return false;\n    }\n\n    final localPosition = _renderBox!.globalToLocal(offset);\n    return _renderBox!.paintBounds.contains(localPosition);\n  }\n}\n\n@visibleForTesting\nclass DocumentHeaderToolbar extends StatefulWidget {\n  const DocumentHeaderToolbar({\n    super.key,\n    required this.node,\n    required this.editorState,\n    required this.hasCover,\n    required this.hasIcon,\n    required this.onIconOrCoverChanged,\n    required this.offset,\n    this.documentId,\n    required this.isCoverTitleHovered,\n    required this.tabs,\n  });\n\n  final Node node;\n  final EditorState editorState;\n  final bool hasCover;\n  final bool hasIcon;\n  final void Function({(CoverType, String?)? cover, EmojiIconData? icon})\n      onIconOrCoverChanged;\n  final double offset;\n  final String? documentId;\n  final ValueNotifier<bool> isCoverTitleHovered;\n  final List<PickerTabType> tabs;\n\n  @override\n  State<DocumentHeaderToolbar> createState() => _DocumentHeaderToolbarState();\n}\n\nclass _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> {\n  final _popoverController = PopoverController();\n\n  bool isHidden = UniversalPlatform.isDesktopOrWeb;\n  bool isPopoverOpen = false;\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child = Container(\n      alignment: Alignment.bottomLeft,\n      width: double.infinity,\n      padding: EdgeInsets.symmetric(horizontal: widget.offset),\n      child: SizedBox(\n        height: 28,\n        child: ValueListenableBuilder<bool>(\n          valueListenable: widget.isCoverTitleHovered,\n          builder: (context, isHovered, child) {\n            return Visibility(\n              visible: !isHidden || isPopoverOpen || isHovered,\n              child: Row(\n                crossAxisAlignment: CrossAxisAlignment.stretch,\n                children: buildRowChildren(),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n\n    if (UniversalPlatform.isDesktopOrWeb) {\n      child = MouseRegion(\n        opaque: false,\n        onEnter: (event) => setHidden(false),\n        onExit: isPopoverOpen ? null : (_) => setHidden(true),\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  List<Widget> buildRowChildren() {\n    if (widget.hasCover && widget.hasIcon) {\n      return [];\n    }\n\n    final List<Widget> children = [];\n\n    if (!widget.hasCover) {\n      children.add(\n        FlowyButton(\n          leftIconSize: const Size.square(18),\n          onTap: () => widget.onIconOrCoverChanged(\n            cover: UniversalPlatform.isDesktopOrWeb\n                ? (CoverType.asset, '1')\n                : (CoverType.color, '0xffe8e0ff'),\n          ),\n          useIntrinsicWidth: true,\n          leftIcon: const FlowySvg(FlowySvgs.add_cover_s),\n          text: FlowyText.small(\n            LocaleKeys.document_plugins_cover_addCover.tr(),\n            color: Theme.of(context).hintColor,\n          ),\n        ),\n      );\n    }\n\n    if (widget.hasIcon) {\n      children.add(\n        FlowyButton(\n          onTap: () => widget.onIconOrCoverChanged(icon: EmojiIconData.none()),\n          useIntrinsicWidth: true,\n          leftIcon: const FlowySvg(FlowySvgs.add_icon_s),\n          iconPadding: 4.0,\n          text: FlowyText.small(\n            LocaleKeys.document_plugins_cover_removeIcon.tr(),\n            color: Theme.of(context).hintColor,\n          ),\n        ),\n      );\n    } else {\n      Widget child = FlowyButton(\n        useIntrinsicWidth: true,\n        leftIcon: const FlowySvg(FlowySvgs.add_icon_s),\n        iconPadding: 4.0,\n        text: FlowyText.small(\n          LocaleKeys.document_plugins_cover_addIcon.tr(),\n          color: Theme.of(context).hintColor,\n        ),\n        onTap: UniversalPlatform.isDesktop\n            ? null\n            : () async {\n                final result = await context.push<EmojiIconData>(\n                  MobileEmojiPickerScreen.routeName,\n                );\n                if (result != null) {\n                  widget.onIconOrCoverChanged(icon: result);\n                }\n              },\n      );\n\n      if (UniversalPlatform.isDesktop) {\n        child = AppFlowyPopover(\n          onClose: () => setState(() => isPopoverOpen = false),\n          controller: _popoverController,\n          offset: const Offset(0, 8),\n          direction: PopoverDirection.bottomWithCenterAligned,\n          constraints: BoxConstraints.loose(const Size(360, 380)),\n          margin: EdgeInsets.zero,\n          child: child,\n          popupBuilder: (BuildContext popoverContext) {\n            isPopoverOpen = true;\n            return FlowyIconEmojiPicker(\n              tabs: widget.tabs,\n              documentId: widget.documentId,\n              onSelectedEmoji: (r) {\n                widget.onIconOrCoverChanged(icon: r.data);\n                if (!r.keepOpen) _popoverController.close();\n              },\n            );\n          },\n        );\n      }\n\n      children.add(child);\n    }\n\n    return children;\n  }\n\n  void setHidden(bool value) {\n    if (isHidden == value) return;\n    setState(() {\n      isHidden = value;\n    });\n  }\n}\n\n@visibleForTesting\nclass DocumentCover extends StatefulWidget {\n  const DocumentCover({\n    super.key,\n    required this.view,\n    required this.node,\n    required this.editorState,\n    required this.coverType,\n    this.coverDetails,\n    required this.onChangeCover,\n  });\n\n  final ViewPB view;\n  final Node node;\n  final EditorState editorState;\n  final CoverType coverType;\n  final String? coverDetails;\n  final void Function(CoverType type, String? details) onChangeCover;\n\n  @override\n  State<DocumentCover> createState() => DocumentCoverState();\n}\n\nclass DocumentCoverState extends State<DocumentCover> {\n  final popoverController = PopoverController();\n\n  bool isOverlayButtonsHidden = true;\n  bool isPopoverOpen = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return UniversalPlatform.isDesktopOrWeb\n        ? _buildDesktopCover()\n        : _buildMobileCover();\n  }\n\n  Widget _buildDesktopCover() {\n    return SizedBox(\n      height: kCoverHeight,\n      child: MouseRegion(\n        onEnter: (event) => setOverlayButtonsHidden(false),\n        onExit: (event) =>\n            setOverlayButtonsHidden(isPopoverOpen ? false : true),\n        child: Stack(\n          children: [\n            SizedBox(\n              height: double.infinity,\n              width: double.infinity,\n              child: DesktopCover(\n                view: widget.view,\n                editorState: widget.editorState,\n                node: widget.node,\n                coverType: widget.coverType,\n                coverDetails: widget.coverDetails,\n              ),\n            ),\n            if (!isOverlayButtonsHidden) _buildCoverOverlayButtons(context),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildMobileCover() {\n    return SizedBox(\n      height: kCoverHeight,\n      child: Stack(\n        children: [\n          SizedBox(\n            height: double.infinity,\n            width: double.infinity,\n            child: _buildCoverImage(),\n          ),\n          Positioned(\n            bottom: 8,\n            right: 12,\n            child: Row(\n              children: [\n                IntrinsicWidth(\n                  child: RoundedTextButton(\n                    fontSize: 14,\n                    onPressed: () {\n                      showMobileBottomSheet(\n                        context,\n                        showHeader: true,\n                        showDragHandle: true,\n                        showCloseButton: true,\n                        title:\n                            LocaleKeys.document_plugins_cover_changeCover.tr(),\n                        builder: (context) {\n                          return Padding(\n                            padding: const EdgeInsets.only(top: 8.0),\n                            child: ConstrainedBox(\n                              constraints: const BoxConstraints(\n                                maxHeight: 340,\n                                minHeight: 80,\n                              ),\n                              child: UploadImageMenu(\n                                limitMaximumImageSize: !_isLocalMode(),\n                                supportTypes: const [\n                                  UploadImageType.color,\n                                  UploadImageType.local,\n                                  UploadImageType.url,\n                                  UploadImageType.unsplash,\n                                ],\n                                onSelectedLocalImages: (files) async {\n                                  context.pop();\n\n                                  if (files.isEmpty) {\n                                    return;\n                                  }\n\n                                  widget.onChangeCover(\n                                    CoverType.file,\n                                    files.first.path,\n                                  );\n                                },\n                                onSelectedAIImage: (_) {\n                                  throw UnimplementedError();\n                                },\n                                onSelectedNetworkImage: (url) async {\n                                  context.pop();\n                                  widget.onChangeCover(CoverType.file, url);\n                                },\n                                onSelectedColor: (color) {\n                                  context.pop();\n                                  widget.onChangeCover(CoverType.color, color);\n                                },\n                              ),\n                            ),\n                          );\n                        },\n                      );\n                    },\n                    fillColor: Theme.of(context)\n                        .colorScheme\n                        .onSurfaceVariant\n                        .withValues(alpha: 0.5),\n                    height: 32,\n                    title: LocaleKeys.document_plugins_cover_changeCover.tr(),\n                  ),\n                ),\n                const HSpace(8.0),\n                SizedBox.square(\n                  dimension: 32.0,\n                  child: DeleteCoverButton(\n                    onTap: () => widget.onChangeCover(CoverType.none, null),\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildCoverImage() {\n    final detail = widget.coverDetails;\n    if (detail == null) {\n      return const SizedBox.shrink();\n    }\n    switch (widget.coverType) {\n      case CoverType.file:\n        if (isURL(detail)) {\n          final userProfilePB =\n              context.read<DocumentBloc>().state.userProfilePB;\n          return FlowyNetworkImage(\n            url: detail,\n            userProfilePB: userProfilePB,\n            errorWidgetBuilder: (context, url, error) =>\n                const SizedBox.shrink(),\n          );\n        }\n        final imageFile = File(detail);\n        if (!imageFile.existsSync()) {\n          WidgetsBinding.instance.addPostFrameCallback((_) {\n            widget.onChangeCover(CoverType.none, null);\n          });\n          return const SizedBox.shrink();\n        }\n        return Image.file(\n          imageFile,\n          fit: BoxFit.cover,\n        );\n      case CoverType.asset:\n        return Image.asset(\n          widget.coverDetails!,\n          fit: BoxFit.cover,\n        );\n      case CoverType.color:\n        final color = widget.coverDetails?.tryToColor() ?? Colors.white;\n        return Container(color: color);\n      case CoverType.none:\n        return const SizedBox.shrink();\n    }\n  }\n\n  Widget _buildCoverOverlayButtons(BuildContext context) {\n    return Positioned(\n      bottom: 20,\n      right: 50,\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          AppFlowyPopover(\n            controller: popoverController,\n            triggerActions: PopoverTriggerFlags.none,\n            offset: const Offset(0, 8),\n            direction: PopoverDirection.bottomWithCenterAligned,\n            constraints: const BoxConstraints(\n              maxWidth: 540,\n              maxHeight: 360,\n              minHeight: 80,\n            ),\n            margin: EdgeInsets.zero,\n            onClose: () => isPopoverOpen = false,\n            child: IntrinsicWidth(\n              child: RoundedTextButton(\n                height: 28.0,\n                onPressed: () => popoverController.show(),\n                hoverColor: Theme.of(context).colorScheme.surface,\n                textColor: Theme.of(context).colorScheme.tertiary,\n                fillColor: Theme.of(context)\n                    .colorScheme\n                    .surface\n                    .withValues(alpha: 0.5),\n                title: LocaleKeys.document_plugins_cover_changeCover.tr(),\n              ),\n            ),\n            popupBuilder: (BuildContext popoverContext) {\n              isPopoverOpen = true;\n\n              return UploadImageMenu(\n                limitMaximumImageSize: !_isLocalMode(),\n                supportTypes: const [\n                  UploadImageType.color,\n                  UploadImageType.local,\n                  UploadImageType.url,\n                  UploadImageType.unsplash,\n                ],\n                onSelectedLocalImages: (files) {\n                  popoverController.close();\n                  if (files.isEmpty) {\n                    return;\n                  }\n\n                  final item = files.map((file) => file.path).first;\n                  onCoverChanged(CoverType.file, item);\n                },\n                onSelectedAIImage: (_) {\n                  throw UnimplementedError();\n                },\n                onSelectedNetworkImage: (url) {\n                  popoverController.close();\n                  onCoverChanged(CoverType.file, url);\n                },\n                onSelectedColor: (color) {\n                  popoverController.close();\n                  onCoverChanged(CoverType.color, color);\n                },\n              );\n            },\n          ),\n          const HSpace(10),\n          DeleteCoverButton(\n            onTap: () => onCoverChanged(CoverType.none, null),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Future<void> onCoverChanged(CoverType type, String? details) async {\n    final previousType = CoverType.fromString(\n      widget.node.attributes[DocumentHeaderBlockKeys.coverType],\n    );\n    final previousDetails =\n        widget.node.attributes[DocumentHeaderBlockKeys.coverDetails];\n\n    bool isFileType(CoverType type, String? details) =>\n        type == CoverType.file && details != null && !isURL(details);\n\n    if (isFileType(type, details)) {\n      if (_isLocalMode()) {\n        details = await saveImageToLocalStorage(details!);\n      } else {\n        // else we should save the image to cloud storage\n        (details, _) = await saveImageToCloudStorage(details!, widget.view.id);\n      }\n    }\n    widget.onChangeCover(type, details);\n\n    // After cover change,delete from localstorage if previous cover was image type\n    if (isFileType(previousType, previousDetails) && _isLocalMode()) {\n      await deleteImageFromLocalStorage(previousDetails);\n    }\n  }\n\n  void setOverlayButtonsHidden(bool value) {\n    if (isOverlayButtonsHidden == value) return;\n    setState(() {\n      isOverlayButtonsHidden = value;\n    });\n  }\n\n  bool _isLocalMode() {\n    return context.read<DocumentBloc>().isLocalMode;\n  }\n}\n\n@visibleForTesting\nclass DeleteCoverButton extends StatelessWidget {\n  const DeleteCoverButton({required this.onTap, super.key});\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    final fillColor = UniversalPlatform.isDesktopOrWeb\n        ? Theme.of(context).colorScheme.surface.withValues(alpha: 0.5)\n        : Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.5);\n    final svgColor = UniversalPlatform.isDesktopOrWeb\n        ? Theme.of(context).colorScheme.tertiary\n        : Theme.of(context).colorScheme.onPrimary;\n    return FlowyIconButton(\n      hoverColor: Theme.of(context).colorScheme.surface,\n      fillColor: fillColor,\n      iconPadding: const EdgeInsets.all(5),\n      width: 28,\n      icon: FlowySvg(\n        FlowySvgs.delete_s,\n        color: svgColor,\n      ),\n      onPressed: onTap,\n    );\n  }\n}\n\n@visibleForTesting\nclass DocumentIcon extends StatefulWidget {\n  const DocumentIcon({\n    super.key,\n    required this.node,\n    required this.editorState,\n    required this.icon,\n    required this.onChangeIcon,\n    this.documentId,\n  });\n\n  final Node node;\n  final EditorState editorState;\n  final EmojiIconData icon;\n  final String? documentId;\n  final ValueChanged<EmojiIconData> onChangeIcon;\n\n  @override\n  State<DocumentIcon> createState() => _DocumentIconState();\n}\n\nclass _DocumentIconState extends State<DocumentIcon> {\n  final PopoverController _popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child = EmojiIconWidget(emoji: widget.icon);\n\n    if (UniversalPlatform.isDesktopOrWeb) {\n      child = AppFlowyPopover(\n        direction: PopoverDirection.bottomWithCenterAligned,\n        controller: _popoverController,\n        offset: const Offset(0, 8),\n        constraints: BoxConstraints.loose(const Size(360, 380)),\n        margin: EdgeInsets.zero,\n        child: child,\n        popupBuilder: (BuildContext popoverContext) {\n          return FlowyIconEmojiPicker(\n            initialType: widget.icon.type.toPickerTabType(),\n            tabs: const [\n              PickerTabType.emoji,\n              PickerTabType.icon,\n              PickerTabType.custom,\n            ],\n            documentId: widget.documentId,\n            onSelectedEmoji: (r) {\n              widget.onChangeIcon(r.data);\n              if (!r.keepOpen) _popoverController.close();\n            },\n          );\n        },\n      );\n    } else {\n      child = GestureDetector(\n        child: child,\n        onTap: () async {\n          final result = await context.push<EmojiIconData>(\n            Uri(\n              path: MobileEmojiPickerScreen.routeName,\n              queryParameters: {\n                MobileEmojiPickerScreen.iconSelectedType: widget.icon.type.name,\n              },\n            ).toString(),\n          );\n          if (result != null) {\n            widget.onChangeIcon(result);\n          }\n        },\n      );\n    }\n\n    return child;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:appflowy/plugins/base/emoji/emoji_text.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy/shared/appflowy_network_svg.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_svg/flowy_svg.dart';\nimport 'package:flutter/material.dart';\nimport 'package:string_validator/string_validator.dart';\n\nimport '../../../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport '../../../../base/icon/icon_widget.dart';\n\nclass EmojiIconWidget extends StatefulWidget {\n  const EmojiIconWidget({\n    super.key,\n    required this.emoji,\n    this.emojiSize = 60,\n  });\n\n  final EmojiIconData emoji;\n  final double emojiSize;\n\n  @override\n  State<EmojiIconWidget> createState() => _EmojiIconWidgetState();\n}\n\nclass _EmojiIconWidgetState extends State<EmojiIconWidget> {\n  bool hover = true;\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      onEnter: (_) => setHidden(false),\n      onExit: (_) => setHidden(true),\n      cursor: SystemMouseCursors.click,\n      child: Container(\n        decoration: BoxDecoration(\n          color: !hover\n              ? Theme.of(context)\n                  .colorScheme\n                  .inverseSurface\n                  .withValues(alpha: 0.5)\n              : Colors.transparent,\n          borderRadius: BorderRadius.circular(8),\n        ),\n        alignment: Alignment.center,\n        child: RawEmojiIconWidget(\n          emoji: widget.emoji,\n          emojiSize: widget.emojiSize,\n        ),\n      ),\n    );\n  }\n\n  void setHidden(bool value) {\n    if (hover == value) return;\n    setState(() {\n      hover = value;\n    });\n  }\n}\n\nclass RawEmojiIconWidget extends StatefulWidget {\n  const RawEmojiIconWidget({\n    super.key,\n    required this.emoji,\n    required this.emojiSize,\n    this.enableColor = true,\n    this.lineHeight,\n  });\n\n  final EmojiIconData emoji;\n  final double emojiSize;\n  final bool enableColor;\n  final double? lineHeight;\n\n  @override\n  State<RawEmojiIconWidget> createState() => _RawEmojiIconWidgetState();\n}\n\nclass _RawEmojiIconWidgetState extends State<RawEmojiIconWidget> {\n  UserProfilePB? userProfile;\n\n  EmojiIconData get emoji => widget.emoji;\n\n  @override\n  void initState() {\n    super.initState();\n    loadUserProfile();\n  }\n\n  @override\n  void didUpdateWidget(RawEmojiIconWidget oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    loadUserProfile();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final defaultEmoji = SizedBox(\n      width: widget.emojiSize,\n      child: EmojiText(\n        emoji: '❓',\n        fontSize: widget.emojiSize,\n        textAlign: TextAlign.center,\n      ),\n    );\n    try {\n      switch (widget.emoji.type) {\n        case FlowyIconType.emoji:\n          return FlowyText.emoji(\n            widget.emoji.emoji,\n            fontSize: widget.emojiSize,\n            textAlign: TextAlign.justify,\n            lineHeight: widget.lineHeight,\n          );\n        case FlowyIconType.icon:\n          IconsData iconData = IconsData.fromJson(\n            jsonDecode(widget.emoji.emoji),\n          );\n          if (!widget.enableColor) {\n            iconData = iconData.noColor();\n          }\n\n          final iconSize = widget.emojiSize;\n          return IconWidget(\n            iconsData: iconData,\n            size: iconSize,\n          );\n        case FlowyIconType.custom:\n          final url = widget.emoji.emoji;\n          final isSvg = url.endsWith('.svg');\n          final hasUserProfile = userProfile != null;\n          if (isURL(url)) {\n            Widget child = const SizedBox.shrink();\n            if (isSvg) {\n              child = FlowyNetworkSvg(\n                url,\n                headers:\n                    hasUserProfile ? _buildRequestHeader(userProfile!) : {},\n                width: widget.emojiSize,\n                height: widget.emojiSize,\n              );\n            } else if (hasUserProfile) {\n              child = FlowyNetworkImage(\n                url: url,\n                width: widget.emojiSize,\n                height: widget.emojiSize,\n                userProfilePB: userProfile,\n                errorWidgetBuilder: (context, url, error) {\n                  return const SizedBox.shrink();\n                },\n              );\n            }\n            return SizedBox.square(\n              dimension: widget.emojiSize,\n              child: child,\n            );\n          }\n          final imageFile = File(url);\n          if (!imageFile.existsSync()) {\n            throw PathNotFoundException(url, const OSError());\n          }\n          return SizedBox.square(\n            dimension: widget.emojiSize,\n            child: isSvg\n                ? SvgPicture.file(\n                    File(url),\n                    width: widget.emojiSize,\n                    height: widget.emojiSize,\n                  )\n                : Image.file(\n                    imageFile,\n                    fit: BoxFit.cover,\n                    width: widget.emojiSize,\n                    height: widget.emojiSize,\n                  ),\n          );\n      }\n    } catch (e) {\n      Log.error(\"Display widget error: $e\");\n      return defaultEmoji;\n    }\n  }\n\n  Map<String, String> _buildRequestHeader(UserProfilePB userProfilePB) {\n    final header = <String, String>{};\n    final token = userProfilePB.token;\n    try {\n      final decodedToken = jsonDecode(token);\n      header['Authorization'] = 'Bearer ${decodedToken['access_token']}';\n    } catch (e) {\n      Log.error('Unable to decode token: $e');\n    }\n    return header;\n  }\n\n  Future<void> loadUserProfile() async {\n    if (userProfile != null) return;\n    if (emoji.type == FlowyIconType.custom) {\n      final userProfile =\n          (await UserBackendService.getCurrentUserProfile()).fold(\n        (userProfile) => userProfile,\n        (l) => null,\n      );\n      if (mounted) {\n        setState(() {\n          this.userProfile = userProfile;\n        });\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/heading/heading_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nfinal _headingData = [\n  (FlowySvgs.h1_s, LocaleKeys.editor_heading1.tr()),\n  (FlowySvgs.h2_s, LocaleKeys.editor_heading2.tr()),\n  (FlowySvgs.h3_s, LocaleKeys.editor_heading3.tr()),\n];\n\nfinal headingsToolbarItem = ToolbarItem(\n  id: 'editor.headings',\n  group: 1,\n  isActive: onlyShowInTextType,\n  builder: (context, editorState, highlightColor, _, __) {\n    final selection = editorState.selection!;\n    final node = editorState.getNodeAtPath(selection.start.path)!;\n    final delta = (node.delta ?? Delta()).toJson();\n    int level = node.attributes[HeadingBlockKeys.level] ?? 1;\n    final originLevel = level;\n    final isHighlight =\n        node.type == HeadingBlockKeys.type && (level >= 1 && level <= 3);\n    // only supports the level 1 - 3 in the toolbar, ignore the other levels\n    level = level.clamp(1, 3);\n\n    final svg = _headingData[level - 1].$1;\n    final message = _headingData[level - 1].$2;\n\n    final child = FlowyTooltip(\n      message: message,\n      preferBelow: false,\n      child: Row(\n        children: [\n          FlowySvg(\n            svg,\n            size: const Size.square(18),\n            color: isHighlight ? highlightColor : Colors.white,\n          ),\n          const HSpace(2.0),\n          const FlowySvg(\n            FlowySvgs.arrow_down_s,\n            size: Size.square(12),\n            color: Colors.grey,\n          ),\n        ],\n      ),\n    );\n    return HeadingPopup(\n      currentLevel: isHighlight ? level : -1,\n      highlightColor: highlightColor,\n      child: child,\n      onLevelChanged: (newLevel) async {\n        // same level means cancel the heading\n        final type =\n            newLevel == originLevel && node.type == HeadingBlockKeys.type\n                ? ParagraphBlockKeys.type\n                : HeadingBlockKeys.type;\n\n        if (type == HeadingBlockKeys.type) {\n          // from paragraph to heading\n          final newNode = node.copyWith(\n            type: type,\n            attributes: {\n              HeadingBlockKeys.level: newLevel,\n              blockComponentBackgroundColor:\n                  node.attributes[blockComponentBackgroundColor],\n              blockComponentTextDirection:\n                  node.attributes[blockComponentTextDirection],\n              blockComponentDelta: delta,\n            },\n          );\n          final children = node.children.map((child) => child.deepCopy());\n\n          final transaction = editorState.transaction;\n          transaction.insertNodes(\n            selection.start.path.next,\n            [newNode, ...children],\n          );\n          transaction.deleteNode(node);\n          await editorState.apply(transaction);\n        } else {\n          // from heading to paragraph\n          await editorState.formatNode(\n            selection,\n            (node) => node.copyWith(\n              type: type,\n              attributes: {\n                HeadingBlockKeys.level: newLevel,\n                blockComponentBackgroundColor:\n                    node.attributes[blockComponentBackgroundColor],\n                blockComponentTextDirection:\n                    node.attributes[blockComponentTextDirection],\n                blockComponentDelta: delta,\n              },\n            ),\n          );\n        }\n      },\n    );\n  },\n);\n\nclass HeadingPopup extends StatelessWidget {\n  const HeadingPopup({\n    super.key,\n    required this.currentLevel,\n    required this.highlightColor,\n    required this.onLevelChanged,\n    required this.child,\n  });\n\n  final int currentLevel;\n  final Color highlightColor;\n  final Function(int level) onLevelChanged;\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      windowPadding: const EdgeInsets.all(0),\n      margin: const EdgeInsets.symmetric(vertical: 2.0),\n      direction: PopoverDirection.bottomWithCenterAligned,\n      offset: const Offset(0, 10),\n      decorationColor: Theme.of(context).colorScheme.onTertiary,\n      borderRadius: BorderRadius.circular(6.0),\n      popupBuilder: (_) {\n        keepEditorFocusNotifier.increase();\n        return _HeadingButtons(\n          currentLevel: currentLevel,\n          highlightColor: highlightColor,\n          onLevelChanged: onLevelChanged,\n        );\n      },\n      onClose: () {\n        keepEditorFocusNotifier.decrease();\n      },\n      child: FlowyButton(\n        useIntrinsicWidth: true,\n        hoverColor: Colors.grey.withValues(alpha: 0.3),\n        text: child,\n      ),\n    );\n  }\n}\n\nclass _HeadingButtons extends StatelessWidget {\n  const _HeadingButtons({\n    required this.highlightColor,\n    required this.currentLevel,\n    required this.onLevelChanged,\n  });\n\n  final int currentLevel;\n  final Color highlightColor;\n  final Function(int level) onLevelChanged;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 28,\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          const HSpace(4),\n          ..._headingData.mapIndexed((index, data) {\n            final svg = data.$1;\n            final message = data.$2;\n            return [\n              HeadingButton(\n                icon: svg,\n                tooltip: message,\n                onTap: () => onLevelChanged(index + 1),\n                isHighlight: index + 1 == currentLevel,\n                highlightColor: highlightColor,\n              ),\n              index != _headingData.length - 1\n                  ? const _Divider()\n                  : const SizedBox.shrink(),\n            ];\n          }).flattened,\n          const HSpace(4),\n        ],\n      ),\n    );\n  }\n}\n\nclass HeadingButton extends StatelessWidget {\n  const HeadingButton({\n    super.key,\n    required this.icon,\n    required this.tooltip,\n    required this.onTap,\n    required this.highlightColor,\n    required this.isHighlight,\n  });\n\n  final Color highlightColor;\n  final FlowySvgData icon;\n  final String tooltip;\n  final VoidCallback onTap;\n  final bool isHighlight;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      useIntrinsicWidth: true,\n      hoverColor: Colors.grey.withValues(alpha: 0.3),\n      onTap: onTap,\n      text: FlowyTooltip(\n        message: tooltip,\n        preferBelow: true,\n        child: FlowySvg(\n          icon,\n          size: const Size.square(18),\n          color: isHighlight ? highlightColor : Colors.white,\n        ),\n      ),\n    );\n  }\n}\n\nclass _Divider extends StatelessWidget {\n  const _Divider();\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(4),\n      child: Container(\n        width: 1,\n        color: Colors.grey,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nclass EditorI18n extends AppFlowyEditorL10n {\n  // static AppFlowyEditorLocalizations current = EditorI18n();\n  EditorI18n();\n\n  @override\n  String get bold {\n    return LocaleKeys.editor_bold.tr();\n  }\n\n  /// `Bulleted List`\n  @override\n  String get bulletedList {\n    return LocaleKeys.editor_bulletedList.tr();\n  }\n\n  /// `Checkbox`\n  @override\n  String get checkbox {\n    return LocaleKeys.editor_checkbox.tr();\n  }\n\n  /// `Embed Code`\n  @override\n  String get embedCode {\n    return LocaleKeys.editor_embedCode.tr();\n  }\n\n  /// `H1`\n  @override\n  String get heading1 {\n    return LocaleKeys.editor_heading1.tr();\n  }\n\n  /// `H2`\n  @override\n  String get heading2 {\n    return LocaleKeys.editor_heading2.tr();\n  }\n\n  /// `H3`\n  @override\n  String get heading3 {\n    return LocaleKeys.editor_heading3.tr();\n  }\n\n  /// `Highlight`\n  @override\n  String get highlight {\n    return LocaleKeys.editor_highlight.tr();\n  }\n\n  /// `Color`\n  @override\n  String get color {\n    return LocaleKeys.editor_color.tr();\n  }\n\n  /// `Image`\n  @override\n  String get image {\n    return LocaleKeys.editor_image.tr();\n  }\n\n  /// `Italic`\n  @override\n  String get italic {\n    return LocaleKeys.editor_italic.tr();\n  }\n\n  /// `Link`\n  @override\n  String get link {\n    return LocaleKeys.editor_link.tr();\n  }\n\n  /// `Numbered List`\n  @override\n  String get numberedList {\n    return LocaleKeys.editor_numberedList.tr();\n  }\n\n  /// `Quote`\n  @override\n  String get quote {\n    return LocaleKeys.editor_quote.tr();\n  }\n\n  /// `Strikethrough`\n  @override\n  String get strikethrough {\n    return LocaleKeys.editor_strikethrough.tr();\n  }\n\n  /// `Text`\n  @override\n  String get text {\n    return LocaleKeys.editor_text.tr();\n  }\n\n  /// `Underline`\n  @override\n  String get underline {\n    return LocaleKeys.editor_underline.tr();\n  }\n\n  /// `Default`\n  @override\n  String get fontColorDefault {\n    return LocaleKeys.editor_fontColorDefault.tr();\n  }\n\n  /// `Gray`\n  @override\n  String get fontColorGray {\n    return LocaleKeys.editor_fontColorGray.tr();\n  }\n\n  /// `Brown`\n  @override\n  String get fontColorBrown {\n    return LocaleKeys.editor_fontColorBrown.tr();\n  }\n\n  /// `Orange`\n  @override\n  String get fontColorOrange {\n    return LocaleKeys.editor_fontColorOrange.tr();\n  }\n\n  /// `Yellow`\n  @override\n  String get fontColorYellow {\n    return LocaleKeys.editor_fontColorYellow.tr();\n  }\n\n  /// `Green`\n  @override\n  String get fontColorGreen {\n    return LocaleKeys.editor_fontColorGreen.tr();\n  }\n\n  /// `Blue`\n  @override\n  String get fontColorBlue {\n    return LocaleKeys.editor_fontColorBlue.tr();\n  }\n\n  /// `Purple`\n  @override\n  String get fontColorPurple {\n    return LocaleKeys.editor_fontColorPurple.tr();\n  }\n\n  /// `Pink`\n  @override\n  String get fontColorPink {\n    return LocaleKeys.editor_fontColorPink.tr();\n  }\n\n  /// `Red`\n  @override\n  String get fontColorRed {\n    return LocaleKeys.editor_fontColorRed.tr();\n  }\n\n  /// `Default background`\n  @override\n  String get backgroundColorDefault {\n    return LocaleKeys.editor_backgroundColorDefault.tr();\n  }\n\n  /// `Gray background`\n  @override\n  String get backgroundColorGray {\n    return LocaleKeys.editor_backgroundColorGray.tr();\n  }\n\n  /// `Brown background`\n  @override\n  String get backgroundColorBrown {\n    return LocaleKeys.editor_backgroundColorBrown.tr();\n  }\n\n  /// `Orange background`\n  @override\n  String get backgroundColorOrange {\n    return LocaleKeys.editor_backgroundColorOrange.tr();\n  }\n\n  /// `Yellow background`\n  @override\n  String get backgroundColorYellow {\n    return LocaleKeys.editor_backgroundColorYellow.tr();\n  }\n\n  /// `Green background`\n  @override\n  String get backgroundColorGreen {\n    return LocaleKeys.editor_backgroundColorGreen.tr();\n  }\n\n  /// `Blue background`\n  @override\n  String get backgroundColorBlue {\n    return LocaleKeys.editor_backgroundColorBlue.tr();\n  }\n\n  /// `Purple background`\n  @override\n  String get backgroundColorPurple {\n    return LocaleKeys.editor_backgroundColorPurple.tr();\n  }\n\n  /// `Pink background`\n  @override\n  String get backgroundColorPink {\n    return LocaleKeys.editor_backgroundColorPink.tr();\n  }\n\n  /// `Red background`\n  @override\n  String get backgroundColorRed {\n    return LocaleKeys.editor_backgroundColorRed.tr();\n  }\n\n  /// `Done`\n  @override\n  String get done {\n    return LocaleKeys.editor_done.tr();\n  }\n\n  /// `Cancel`\n  @override\n  String get cancel {\n    return LocaleKeys.editor_cancel.tr();\n  }\n\n  /// `Tint 1`\n  @override\n  String get tint1 {\n    return LocaleKeys.editor_tint1.tr();\n  }\n\n  /// `Tint 2`\n  @override\n  String get tint2 {\n    return LocaleKeys.editor_tint2.tr();\n  }\n\n  /// `Tint 3`\n  @override\n  String get tint3 {\n    return LocaleKeys.editor_tint3.tr();\n  }\n\n  /// `Tint 4`\n  @override\n  String get tint4 {\n    return LocaleKeys.editor_tint4.tr();\n  }\n\n  /// `Tint 5`\n  @override\n  String get tint5 {\n    return LocaleKeys.editor_tint5.tr();\n  }\n\n  /// `Tint 6`\n  @override\n  String get tint6 {\n    return LocaleKeys.editor_tint6.tr();\n  }\n\n  /// `Tint 7`\n  @override\n  String get tint7 {\n    return LocaleKeys.editor_tint7.tr();\n  }\n\n  /// `Tint 8`\n  @override\n  String get tint8 {\n    return LocaleKeys.editor_tint8.tr();\n  }\n\n  /// `Tint 9`\n  @override\n  String get tint9 {\n    return LocaleKeys.editor_tint9.tr();\n  }\n\n  /// `Purple`\n  @override\n  String get lightLightTint1 {\n    return LocaleKeys.editor_lightLightTint1.tr();\n  }\n\n  /// `Pink`\n  @override\n  String get lightLightTint2 {\n    return LocaleKeys.editor_lightLightTint2.tr();\n  }\n\n  /// `Light Pink`\n  @override\n  String get lightLightTint3 {\n    return LocaleKeys.editor_lightLightTint3.tr();\n  }\n\n  /// `Orange`\n  @override\n  String get lightLightTint4 {\n    return LocaleKeys.editor_lightLightTint4.tr();\n  }\n\n  /// `Yellow`\n  @override\n  String get lightLightTint5 {\n    return LocaleKeys.editor_lightLightTint5.tr();\n  }\n\n  /// `Lime`\n  @override\n  String get lightLightTint6 {\n    return LocaleKeys.editor_lightLightTint6.tr();\n  }\n\n  /// `Green`\n  @override\n  String get lightLightTint7 {\n    return LocaleKeys.editor_lightLightTint7.tr();\n  }\n\n  /// `Aqua`\n  @override\n  String get lightLightTint8 {\n    return LocaleKeys.editor_lightLightTint8.tr();\n  }\n\n  /// `Blue`\n  @override\n  String get lightLightTint9 {\n    return LocaleKeys.editor_lightLightTint9.tr();\n  }\n\n  /// `URL`\n  @override\n  String get urlHint {\n    return LocaleKeys.editor_urlHint.tr();\n  }\n\n  /// `Heading 1`\n  @override\n  String get mobileHeading1 {\n    return LocaleKeys.editor_mobileHeading1.tr();\n  }\n\n  /// `Heading 2`\n  @override\n  String get mobileHeading2 {\n    return LocaleKeys.editor_mobileHeading2.tr();\n  }\n\n  /// `Heading 3`\n  @override\n  String get mobileHeading3 {\n    return LocaleKeys.editor_mobileHeading3.tr();\n  }\n\n  /// `Text Color`\n  @override\n  String get textColor {\n    return LocaleKeys.editor_textColor.tr();\n  }\n\n  /// `Background Color`\n  @override\n  String get backgroundColor {\n    return LocaleKeys.editor_backgroundColor.tr();\n  }\n\n  /// `Add your link`\n  @override\n  String get addYourLink {\n    return LocaleKeys.editor_addYourLink.tr();\n  }\n\n  /// `Open link`\n  @override\n  String get openLink {\n    return LocaleKeys.editor_openLink.tr();\n  }\n\n  /// `Copy link`\n  @override\n  String get copyLink {\n    return LocaleKeys.editor_copyLink.tr();\n  }\n\n  /// `Remove link`\n  @override\n  String get removeLink {\n    return LocaleKeys.editor_removeLink.tr();\n  }\n\n  /// `Edit link`\n  @override\n  String get editLink {\n    return LocaleKeys.editor_editLink.tr();\n  }\n\n  /// `Text`\n  @override\n  String get linkText {\n    return LocaleKeys.editor_linkText.tr();\n  }\n\n  /// `Please enter text`\n  @override\n  String get linkTextHint {\n    return LocaleKeys.editor_linkTextHint.tr();\n  }\n\n  /// `Please enter URL`\n  @override\n  String get linkAddressHint {\n    return LocaleKeys.editor_linkAddressHint.tr();\n  }\n\n  /// `Highlight color`\n  @override\n  String get highlightColor {\n    return LocaleKeys.editor_highlightColor.tr();\n  }\n\n  /// `Clear highlight color`\n  @override\n  String get clearHighlightColor {\n    return LocaleKeys.editor_clearHighlightColor.tr();\n  }\n\n  /// `Custom color`\n  @override\n  String get customColor {\n    return LocaleKeys.editor_customColor.tr();\n  }\n\n  /// `Hex value`\n  @override\n  String get hexValue {\n    return LocaleKeys.editor_hexValue.tr();\n  }\n\n  /// `Opacity`\n  @override\n  String get opacity {\n    return LocaleKeys.editor_opacity.tr();\n  }\n\n  /// `Reset to default color`\n  @override\n  String get resetToDefaultColor {\n    return LocaleKeys.editor_resetToDefaultColor.tr();\n  }\n\n  /// `LTR`\n  @override\n  String get ltr {\n    return LocaleKeys.editor_ltr.tr();\n  }\n\n  /// `RTL`\n  @override\n  String get rtl {\n    return LocaleKeys.editor_rtl.tr();\n  }\n\n  /// `Auto`\n  @override\n  String get auto {\n    return LocaleKeys.editor_auto.tr();\n  }\n\n  /// `Cut`\n  @override\n  String get cut {\n    return LocaleKeys.editor_cut.tr();\n  }\n\n  /// `Copy`\n  @override\n  String get copy {\n    return LocaleKeys.editor_copy.tr();\n  }\n\n  /// `Paste`\n  @override\n  String get paste {\n    return LocaleKeys.editor_paste.tr();\n  }\n\n  /// `Find`\n  @override\n  String get find {\n    return LocaleKeys.editor_find.tr();\n  }\n\n  /// `Previous match`\n  @override\n  String get previousMatch {\n    return LocaleKeys.editor_previousMatch.tr();\n  }\n\n  /// `Next match`\n  @override\n  String get nextMatch {\n    return LocaleKeys.editor_nextMatch.tr();\n  }\n\n  /// `Close`\n  @override\n  String get closeFind {\n    return LocaleKeys.editor_closeFind.tr();\n  }\n\n  /// `Replace`\n  @override\n  String get replace {\n    return LocaleKeys.editor_replace.tr();\n  }\n\n  /// `Replace all`\n  @override\n  String get replaceAll {\n    return LocaleKeys.editor_replaceAll.tr();\n  }\n\n  /// `Regex`\n  @override\n  String get regex {\n    return LocaleKeys.editor_regex.tr();\n  }\n\n  /// `Case sensitive`\n  @override\n  String get caseSensitive {\n    return LocaleKeys.editor_caseSensitive.tr();\n  }\n\n  /// `Upload Image`\n  @override\n  String get uploadImage {\n    return LocaleKeys.editor_uploadImage.tr();\n  }\n\n  /// `URL Image`\n  @override\n  String get urlImage {\n    return LocaleKeys.editor_urlImage.tr();\n  }\n\n  /// `Incorrect Link`\n  @override\n  String get incorrectLink {\n    return LocaleKeys.editor_incorrectLink.tr();\n  }\n\n  /// `Upload`\n  @override\n  String get upload {\n    return LocaleKeys.editor_upload.tr();\n  }\n\n  /// `Choose an image`\n  @override\n  String get chooseImage {\n    return LocaleKeys.editor_chooseImage.tr();\n  }\n\n  /// `Loading`\n  @override\n  String get loading {\n    return LocaleKeys.editor_loading.tr();\n  }\n\n  /// `Could not load the image`\n  @override\n  String get imageLoadFailed {\n    return LocaleKeys.editor_imageLoadFailed.tr();\n  }\n\n  /// `Divider`\n  @override\n  String get divider {\n    return LocaleKeys.editor_divider.tr();\n  }\n\n  /// `Table`\n  @override\n  String get table {\n    return LocaleKeys.editor_table.tr();\n  }\n\n  /// `Add before`\n  @override\n  String get colAddBefore {\n    return LocaleKeys.editor_colAddBefore.tr();\n  }\n\n  /// `Add before`\n  @override\n  String get rowAddBefore {\n    return LocaleKeys.editor_rowAddBefore.tr();\n  }\n\n  /// `Add after`\n  @override\n  String get colAddAfter {\n    return LocaleKeys.editor_colAddAfter.tr();\n  }\n\n  /// `Add after`\n  @override\n  String get rowAddAfter {\n    return LocaleKeys.editor_rowAddAfter.tr();\n  }\n\n  /// `Remove`\n  @override\n  String get colRemove {\n    return LocaleKeys.editor_colRemove.tr();\n  }\n\n  /// `Remove`\n  @override\n  String get rowRemove {\n    return LocaleKeys.editor_rowRemove.tr();\n  }\n\n  /// `Duplicate`\n  @override\n  String get colDuplicate {\n    return LocaleKeys.editor_colDuplicate.tr();\n  }\n\n  /// `Duplicate`\n  @override\n  String get rowDuplicate {\n    return LocaleKeys.editor_rowDuplicate.tr();\n  }\n\n  /// `Clear Content`\n  @override\n  String get colClear {\n    return LocaleKeys.editor_colClear.tr();\n  }\n\n  /// `Clear Content`\n  @override\n  String get rowClear {\n    return LocaleKeys.editor_rowClear.tr();\n  }\n\n  /// `Enter a / to insert a block, or start typing`\n  @override\n  String get slashPlaceHolder {\n    return LocaleKeys.editor_slashPlaceHolder.tr();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/common.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/widgets.dart';\n\nenum CustomImageType {\n  local,\n  internal, // the images saved in self-host cloud\n  external; // the images linked from network, like unsplash, https://xxx/yyy/zzz.jpg\n\n  static CustomImageType fromIntValue(int value) {\n    switch (value) {\n      case 0:\n        return CustomImageType.local;\n      case 1:\n        return CustomImageType.internal;\n      case 2:\n        return CustomImageType.external;\n      default:\n        throw UnimplementedError();\n    }\n  }\n\n  int toIntValue() {\n    switch (this) {\n      case CustomImageType.local:\n        return 0;\n      case CustomImageType.internal:\n        return 1;\n      case CustomImageType.external:\n        return 2;\n    }\n  }\n}\n\nclass ImageBlockData {\n  factory ImageBlockData.fromJson(Map<String, dynamic> json) {\n    return ImageBlockData(\n      url: json['url'] as String? ?? '',\n      type: CustomImageType.fromIntValue(json['type'] as int),\n    );\n  }\n\n  ImageBlockData({required this.url, required this.type});\n\n  final String url;\n  final CustomImageType type;\n\n  bool get isLocal => type == CustomImageType.local;\n  bool get isNotInternal => type != CustomImageType.internal;\n\n  Map<String, dynamic> toJson() {\n    return {'url': url, 'type': type.toIntValue()};\n  }\n\n  ImageProvider toImageProvider() {\n    switch (type) {\n      case CustomImageType.internal:\n      case CustomImageType.external:\n        return NetworkImage(url);\n      case CustomImageType.local:\n        return FileImage(File(url));\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/unsupport_image_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';\nimport 'package:appflowy/shared/custom_image_cache_manager.dart';\nimport 'package:appflowy/shared/permission/permission_checker.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart' hide ResizableImage;\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:provider/provider.dart';\nimport 'package:saver_gallery/saver_gallery.dart';\nimport 'package:string_validator/string_validator.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../common.dart';\n\nconst kImagePlaceholderKey = 'imagePlaceholderKey';\n\nclass CustomImageBlockKeys {\n  const CustomImageBlockKeys._();\n\n  static const String type = 'image';\n\n  /// The align data of a image block.\n  ///\n  /// The value is a String.\n  /// left, center, right\n  static const String align = 'align';\n\n  /// The image src of a image block.\n  ///\n  /// The value is a String.\n  /// It can be a url or a base64 string(web).\n  static const String url = 'url';\n\n  /// The height of a image block.\n  ///\n  /// The value is a double.\n  static const String width = 'width';\n\n  /// The width of a image block.\n  ///\n  /// The value is a double.\n  static const String height = 'height';\n\n  /// The image type of a image block.\n  ///\n  /// The value is a CustomImageType enum.\n  static const String imageType = 'image_type';\n}\n\nNode customImageNode({\n  required String url,\n  String align = 'center',\n  double? height,\n  double? width,\n  CustomImageType type = CustomImageType.local,\n}) {\n  return Node(\n    type: CustomImageBlockKeys.type,\n    attributes: {\n      CustomImageBlockKeys.url: url,\n      CustomImageBlockKeys.align: align,\n      CustomImageBlockKeys.height: height,\n      CustomImageBlockKeys.width: width,\n      CustomImageBlockKeys.imageType: type.toIntValue(),\n    },\n  );\n}\n\ntypedef CustomImageBlockComponentMenuBuilder = Widget Function(\n  Node node,\n  CustomImageBlockComponentState state,\n  ValueNotifier<ResizableImageState> imageStateNotifier,\n);\n\nclass CustomImageBlockComponentBuilder extends BlockComponentBuilder {\n  CustomImageBlockComponentBuilder({\n    super.configuration,\n    this.showMenu = false,\n    this.menuBuilder,\n  });\n\n  /// Whether to show the menu of this block component.\n  final bool showMenu;\n\n  ///\n  final CustomImageBlockComponentMenuBuilder? menuBuilder;\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    return CustomImageBlockComponent(\n      key: node.key,\n      node: node,\n      showActions: showActions(node),\n      configuration: configuration,\n      actionBuilder: (_, state) => actionBuilder(blockComponentContext, state),\n      showMenu: showMenu,\n      menuBuilder: menuBuilder,\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate => (node) => node.children.isEmpty;\n}\n\nclass CustomImageBlockComponent extends BlockComponentStatefulWidget {\n  const CustomImageBlockComponent({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n    this.showMenu = false,\n    this.menuBuilder,\n  });\n\n  /// Whether to show the menu of this block component.\n  final bool showMenu;\n\n  final CustomImageBlockComponentMenuBuilder? menuBuilder;\n\n  @override\n  State<CustomImageBlockComponent> createState() =>\n      CustomImageBlockComponentState();\n}\n\nclass CustomImageBlockComponentState extends State<CustomImageBlockComponent>\n    with SelectableMixin, BlockComponentConfigurable {\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  @override\n  Node get node => widget.node;\n\n  final imageKey = GlobalKey();\n  RenderBox? get _renderBox => context.findRenderObject() as RenderBox?;\n\n  late final editorState = Provider.of<EditorState>(context, listen: false);\n\n  final showActionsNotifier = ValueNotifier<bool>(false);\n  final imageStateNotifier =\n      ValueNotifier<ResizableImageState>(ResizableImageState.loading);\n\n  bool alwaysShowMenu = false;\n\n  @override\n  Widget build(BuildContext context) {\n    final node = widget.node;\n    final attributes = node.attributes;\n    final src = attributes[CustomImageBlockKeys.url];\n\n    final alignment = AlignmentExtension.fromString(\n      attributes[CustomImageBlockKeys.align] ?? 'center',\n    );\n    final width = attributes[CustomImageBlockKeys.width]?.toDouble() ??\n        MediaQuery.of(context).size.width;\n    final height = attributes[CustomImageBlockKeys.height]?.toDouble();\n    final rawImageType = attributes[CustomImageBlockKeys.imageType] ?? 0;\n    final imageType = CustomImageType.fromIntValue(rawImageType);\n\n    final imagePlaceholderKey = node.extraInfos?[kImagePlaceholderKey];\n    Widget child;\n    if (src.isEmpty) {\n      child = ImagePlaceholder(\n        key: imagePlaceholderKey is GlobalKey ? imagePlaceholderKey : null,\n        node: node,\n      );\n    } else if (imageType != CustomImageType.internal &&\n        !_checkIfURLIsValid(src)) {\n      child = const UnsupportedImageWidget();\n    } else {\n      child = ResizableImage(\n        src: src,\n        width: width,\n        height: height,\n        editable: editorState.editable,\n        alignment: alignment,\n        type: imageType,\n        onStateChange: (state) => imageStateNotifier.value = state,\n        onDoubleTap: () => showDialog(\n          context: context,\n          builder: (_) => InteractiveImageViewer(\n            userProfile: context.read<DocumentBloc>().state.userProfilePB,\n            imageProvider: AFBlockImageProvider(\n              images: [ImageBlockData(url: src, type: imageType)],\n              onDeleteImage: (_) async {\n                final transaction = editorState.transaction..deleteNode(node);\n                await editorState.apply(transaction);\n              },\n            ),\n          ),\n        ),\n        onResize: (width) {\n          final transaction = editorState.transaction\n            ..updateNode(node, {CustomImageBlockKeys.width: width});\n          editorState.apply(transaction);\n        },\n      );\n    }\n\n    child = Padding(\n      padding: padding,\n      child: RepaintBoundary(\n        key: imageKey,\n        child: child,\n      ),\n    );\n\n    if (UniversalPlatform.isDesktopOrWeb) {\n      child = BlockSelectionContainer(\n        node: node,\n        delegate: this,\n        listenable: editorState.selectionNotifier,\n        blockColor: editorState.editorStyle.selectionColor,\n        selectionAboveBlock: true,\n        supportTypes: const [BlockSelectionType.block],\n        child: child,\n      );\n    }\n\n    if (widget.showActions && widget.actionBuilder != null) {\n      child = BlockComponentActionWrapper(\n        node: node,\n        actionBuilder: widget.actionBuilder!,\n        actionTrailingBuilder: widget.actionTrailingBuilder,\n        child: child,\n      );\n    }\n\n    // show a hover menu on desktop or web\n    if (UniversalPlatform.isDesktopOrWeb) {\n      if (widget.showMenu && widget.menuBuilder != null) {\n        child = MouseRegion(\n          onEnter: (_) => showActionsNotifier.value = true,\n          onExit: (_) {\n            if (!alwaysShowMenu) {\n              showActionsNotifier.value = false;\n            }\n          },\n          hitTestBehavior: HitTestBehavior.opaque,\n          opaque: false,\n          child: ValueListenableBuilder<bool>(\n            valueListenable: showActionsNotifier,\n            builder: (_, value, child) {\n              return Stack(\n                children: [\n                  editorState.editable\n                      ? BlockSelectionContainer(\n                          node: node,\n                          delegate: this,\n                          listenable: editorState.selectionNotifier,\n                          cursorColor: editorState.editorStyle.cursorColor,\n                          selectionColor:\n                              editorState.editorStyle.selectionColor,\n                          child: child!,\n                        )\n                      : child!,\n                  if (value)\n                    widget.menuBuilder!(widget.node, this, imageStateNotifier),\n                ],\n              );\n            },\n            child: child,\n          ),\n        );\n      }\n    } else {\n      // show a fixed menu on mobile\n      child = MobileBlockActionButtons(\n        showThreeDots: false,\n        node: node,\n        editorState: editorState,\n        extendActionWidgets: _buildExtendActionWidgets(context),\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  @override\n  Position start() => Position(path: widget.node.path);\n\n  @override\n  Position end() => Position(path: widget.node.path, offset: 1);\n\n  @override\n  Position getPositionInOffset(Offset start) => end();\n\n  @override\n  bool get shouldCursorBlink => false;\n\n  @override\n  CursorStyle get cursorStyle => CursorStyle.cover;\n\n  @override\n  Rect getBlockRect({\n    bool shiftWithBaseOffset = false,\n  }) {\n    final imageBox = imageKey.currentContext?.findRenderObject();\n    if (imageBox is RenderBox) {\n      return padding.topLeft & imageBox.size;\n    }\n    return Rect.zero;\n  }\n\n  @override\n  Rect? getCursorRectInPosition(\n    Position position, {\n    bool shiftWithBaseOffset = false,\n  }) {\n    final rects = getRectsInSelection(Selection.collapsed(position));\n    return rects.firstOrNull;\n  }\n\n  @override\n  List<Rect> getRectsInSelection(\n    Selection selection, {\n    bool shiftWithBaseOffset = false,\n  }) {\n    if (_renderBox == null) {\n      return [];\n    }\n    final parentBox = context.findRenderObject();\n    final imageBox = imageKey.currentContext?.findRenderObject();\n    if (parentBox is RenderBox && imageBox is RenderBox) {\n      return [\n        imageBox.localToGlobal(Offset.zero, ancestor: parentBox) &\n            imageBox.size,\n      ];\n    }\n    return [Offset.zero & _renderBox!.size];\n  }\n\n  @override\n  Selection getSelectionInRange(Offset start, Offset end) => Selection.single(\n        path: widget.node.path,\n        startOffset: 0,\n        endOffset: 1,\n      );\n\n  @override\n  Offset localToGlobal(\n    Offset offset, {\n    bool shiftWithBaseOffset = false,\n  }) =>\n      _renderBox!.localToGlobal(offset);\n\n  // only used on mobile platform\n  List<Widget> _buildExtendActionWidgets(BuildContext context) {\n    final String url = widget.node.attributes[CustomImageBlockKeys.url];\n    if (!_checkIfURLIsValid(url)) {\n      return [];\n    }\n\n    return [\n      FlowyOptionTile.text(\n        showTopBorder: false,\n        text: LocaleKeys.editor_copy.tr(),\n        leftIcon: const FlowySvg(\n          FlowySvgs.m_field_copy_s,\n        ),\n        onTap: () async {\n          context.pop();\n          showToastNotification(\n            message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),\n          );\n          await getIt<ClipboardService>().setPlainText(url);\n        },\n      ),\n      FlowyOptionTile.text(\n        showTopBorder: false,\n        text: LocaleKeys.document_imageBlock_saveImageToGallery.tr(),\n        leftIcon: const FlowySvg(\n          FlowySvgs.image_placeholder_s,\n          size: Size.square(20),\n        ),\n        onTap: () async {\n          context.pop();\n          // save the image to the photo library\n          await _saveImageToGallery(url);\n        },\n      ),\n    ];\n  }\n\n  bool _checkIfURLIsValid(dynamic url) {\n    if (url is! String) {\n      return false;\n    }\n\n    if (url.isEmpty) {\n      return false;\n    }\n\n    if (!isURL(url) && !File(url).existsSync()) {\n      return false;\n    }\n\n    return true;\n  }\n\n  Future<void> _saveImageToGallery(String url) async {\n    final permission = await PermissionChecker.checkPhotoPermission(context);\n    if (!permission) {\n      return;\n    }\n\n    final imageFile = await CustomImageCacheManager().getSingleFile(url);\n    if (imageFile.existsSync()) {\n      final result = await SaverGallery.saveImage(\n        imageFile.readAsBytesSync(),\n        fileName: imageFile.basename,\n        skipIfExists: false,\n      );\n      if (mounted) {\n        showToastNotification(\n          message: result.isSuccess\n              ? LocaleKeys.document_imageBlock_successToAddImageToGallery.tr()\n              : LocaleKeys.document_imageBlock_failedToAddImageToGallery.tr(),\n        );\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart",
    "content": "import 'dart:ui';\n\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:flutter/services.dart';\nimport 'package:provider/provider.dart';\n\nclass ImageMenu extends StatefulWidget {\n  const ImageMenu({\n    super.key,\n    required this.node,\n    required this.state,\n    required this.imageStateNotifier,\n  });\n\n  final Node node;\n  final CustomImageBlockComponentState state;\n  final ValueNotifier<ResizableImageState> imageStateNotifier;\n\n  @override\n  State<ImageMenu> createState() => _ImageMenuState();\n}\n\nclass _ImageMenuState extends State<ImageMenu> {\n  late final String? url = widget.node.attributes[CustomImageBlockKeys.url];\n\n  @override\n  Widget build(BuildContext context) {\n    final isPlaceholder = url == null || url!.isEmpty;\n    final theme = Theme.of(context);\n    return ValueListenableBuilder<ResizableImageState>(\n      valueListenable: widget.imageStateNotifier,\n      builder: (_, state, child) {\n        if (state == ResizableImageState.loading && !isPlaceholder) {\n          return const SizedBox.shrink();\n        }\n\n        return Container(\n          height: 32,\n          decoration: BoxDecoration(\n            color: theme.cardColor,\n            boxShadow: [\n              BoxShadow(\n                blurRadius: 5,\n                spreadRadius: 1,\n                color: Colors.black.withValues(alpha: 0.1),\n              ),\n            ],\n            borderRadius: BorderRadius.circular(4.0),\n          ),\n          child: Row(\n            children: [\n              const HSpace(4),\n              if (!isPlaceholder) ...[\n                MenuBlockButton(\n                  tooltip: LocaleKeys.document_imageBlock_openFullScreen.tr(),\n                  iconData: FlowySvgs.full_view_s,\n                  onTap: openFullScreen,\n                ),\n                const HSpace(4),\n                MenuBlockButton(\n                  tooltip: LocaleKeys.editor_copy.tr(),\n                  iconData: FlowySvgs.copy_s,\n                  onTap: copyImageLink,\n                ),\n                const HSpace(4),\n              ],\n              if (widget.state.editorState.editable) ...[\n                if (!isPlaceholder) ...[\n                  _ImageAlignButton(node: widget.node, state: widget.state),\n                  const _Divider(),\n                ],\n                MenuBlockButton(\n                  tooltip: LocaleKeys.button_delete.tr(),\n                  iconData: FlowySvgs.trash_s,\n                  onTap: deleteImage,\n                ),\n                const HSpace(4),\n              ],\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  Future<void> copyImageLink() async {\n    if (url != null) {\n      // paste the image url and the image data\n      final imageData = await captureImage();\n\n      try {\n        // /image\n        await getIt<ClipboardService>().setData(\n          ClipboardServiceData(\n            plainText: url!,\n            image: ('png', imageData),\n          ),\n        );\n\n        if (mounted) {\n          showToastNotification(\n            message: LocaleKeys.message_copy_success.tr(),\n          );\n        }\n      } catch (e) {\n        if (mounted) {\n          showToastNotification(\n            message: LocaleKeys.message_copy_fail.tr(),\n            type: ToastificationType.error,\n          );\n        }\n      }\n    }\n  }\n\n  Future<void> deleteImage() async {\n    final node = widget.node;\n    final editorState = context.read<EditorState>();\n    final transaction = editorState.transaction;\n    transaction.deleteNode(node);\n    transaction.afterSelection = null;\n    await editorState.apply(transaction);\n  }\n\n  void openFullScreen() {\n    showDialog(\n      context: context,\n      builder: (_) => InteractiveImageViewer(\n        userProfile: context.read<UserWorkspaceBloc?>()?.state.userProfile ??\n            context.read<DocumentBloc>().state.userProfilePB,\n        imageProvider: AFBlockImageProvider(\n          images: [\n            ImageBlockData(\n              url: url!,\n              type: CustomImageType.fromIntValue(\n                widget.node.attributes[CustomImageBlockKeys.imageType] ?? 2,\n              ),\n            ),\n          ],\n          onDeleteImage: widget.state.editorState.editable\n              ? (_) async {\n                  final transaction = widget.state.editorState.transaction;\n                  transaction.deleteNode(widget.node);\n                  await widget.state.editorState.apply(transaction);\n                }\n              : null,\n        ),\n      ),\n    );\n  }\n\n  Future<Uint8List> captureImage() async {\n    final boundary = widget.state.imageKey.currentContext?.findRenderObject()\n        as RenderRepaintBoundary?;\n    final image = await boundary?.toImage();\n    final byteData = await image?.toByteData(format: ImageByteFormat.png);\n    if (byteData == null) {\n      return Uint8List(0);\n    }\n    return byteData.buffer.asUint8List();\n  }\n}\n\nclass _ImageAlignButton extends StatefulWidget {\n  const _ImageAlignButton({required this.node, required this.state});\n\n  final Node node;\n  final CustomImageBlockComponentState state;\n\n  @override\n  State<_ImageAlignButton> createState() => _ImageAlignButtonState();\n}\n\nconst _interceptorKey = 'image-align';\n\nclass _ImageAlignButtonState extends State<_ImageAlignButton> {\n  final gestureInterceptor = SelectionGestureInterceptor(\n    key: _interceptorKey,\n    canTap: (details) => false,\n  );\n\n  String get align =>\n      widget.node.attributes[CustomImageBlockKeys.align] ?? centerAlignmentKey;\n  final popoverController = PopoverController();\n  late final EditorState editorState;\n\n  @override\n  void initState() {\n    super.initState();\n    editorState = context.read<EditorState>();\n  }\n\n  @override\n  void dispose() {\n    allowMenuClose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return IgnoreParentGestureWidget(\n      child: AppFlowyPopover(\n        onClose: allowMenuClose,\n        controller: popoverController,\n        windowPadding: const EdgeInsets.all(0),\n        margin: const EdgeInsets.all(0),\n        direction: PopoverDirection.bottomWithCenterAligned,\n        offset: const Offset(0, 10),\n        child: MenuBlockButton(\n          tooltip: LocaleKeys.document_plugins_optionAction_align.tr(),\n          iconData: iconFor(align),\n        ),\n        popupBuilder: (_) {\n          preventMenuClose();\n          return _AlignButtons(onAlignChanged: onAlignChanged);\n        },\n      ),\n    );\n  }\n\n  void onAlignChanged(String align) {\n    popoverController.close();\n\n    final transaction = editorState.transaction;\n    transaction.updateNode(widget.node, {CustomImageBlockKeys.align: align});\n    editorState.apply(transaction);\n\n    allowMenuClose();\n  }\n\n  void preventMenuClose() {\n    widget.state.alwaysShowMenu = true;\n    editorState.service.selectionService.registerGestureInterceptor(\n      gestureInterceptor,\n    );\n  }\n\n  void allowMenuClose() {\n    widget.state.alwaysShowMenu = false;\n    editorState.service.selectionService.unregisterGestureInterceptor(\n      _interceptorKey,\n    );\n  }\n\n  FlowySvgData iconFor(String alignment) {\n    switch (alignment) {\n      case rightAlignmentKey:\n        return FlowySvgs.align_right_s;\n      case centerAlignmentKey:\n        return FlowySvgs.align_center_s;\n      case leftAlignmentKey:\n      default:\n        return FlowySvgs.align_left_s;\n    }\n  }\n}\n\nclass _AlignButtons extends StatelessWidget {\n  const _AlignButtons({required this.onAlignChanged});\n\n  final Function(String align) onAlignChanged;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 32,\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          const HSpace(4),\n          MenuBlockButton(\n            tooltip: LocaleKeys.document_plugins_optionAction_left,\n            iconData: FlowySvgs.align_left_s,\n            onTap: () => onAlignChanged(leftAlignmentKey),\n          ),\n          const _Divider(),\n          MenuBlockButton(\n            tooltip: LocaleKeys.document_plugins_optionAction_center,\n            iconData: FlowySvgs.align_center_s,\n            onTap: () => onAlignChanged(centerAlignmentKey),\n          ),\n          const _Divider(),\n          MenuBlockButton(\n            tooltip: LocaleKeys.document_plugins_optionAction_right,\n            iconData: FlowySvgs.align_right_s,\n            onTap: () => onAlignChanged(rightAlignmentKey),\n          ),\n          const HSpace(4),\n        ],\n      ),\n    );\n  }\n}\n\nclass _Divider extends StatelessWidget {\n  const _Divider();\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(8),\n      child: Container(width: 1, color: Colors.grey),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/unsupport_image_widget.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\n\nclass UnsupportedImageWidget extends StatelessWidget {\n  const UnsupportedImageWidget({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return DecoratedBox(\n      decoration: BoxDecoration(\n        color: Theme.of(context).colorScheme.surfaceContainerHighest,\n        borderRadius: BorderRadius.circular(4),\n      ),\n      child: FlowyHover(\n        style: HoverStyle(borderRadius: BorderRadius.circular(4)),\n        child: SizedBox(\n          height: 52,\n          child: Row(\n            children: [\n              const HSpace(10),\n              const FlowySvg(\n                FlowySvgs.image_placeholder_s,\n                size: Size.square(24),\n              ),\n              const HSpace(10),\n              FlowyText(LocaleKeys.document_imageBlock_unableToLoadImage.tr()),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\n\nclass MobileImagePickerScreen extends StatelessWidget {\n  const MobileImagePickerScreen({super.key});\n\n  static const routeName = '/image_picker';\n\n  @override\n  Widget build(BuildContext context) => const ImagePickerPage();\n}\n\nclass ImagePickerPage extends StatelessWidget {\n  const ImagePickerPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        titleSpacing: 0,\n        title: FlowyText.semibold(\n          LocaleKeys.titleBar_pageIcon.tr(),\n          fontSize: 14.0,\n        ),\n        leading: const AppBarBackButton(),\n      ),\n      body: SafeArea(\n        child: UploadImageMenu(\n          onSubmitted: (_) {},\n          onUpload: (_) {},\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart';\nimport 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/plugins/document/application/prelude.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';\nimport 'package:appflowy/shared/patterns/file_type_patterns.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/settings/application_data_storage.dart';\nimport 'package:appflowy/workspace/presentation/home/toast.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu;\nimport 'package:desktop_drop/desktop_drop.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flowy_infra_ui/style_widget/snap_bar.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:http/http.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:string_validator/string_validator.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass ImagePlaceholder extends StatefulWidget {\n  const ImagePlaceholder({super.key, required this.node});\n\n  final Node node;\n\n  @override\n  State<ImagePlaceholder> createState() => ImagePlaceholderState();\n}\n\nclass ImagePlaceholderState extends State<ImagePlaceholder> {\n  final controller = PopoverController();\n  final documentService = DocumentService();\n  late final editorState = context.read<EditorState>();\n\n  late EditorDropManagerState? dropManagerState = UniversalPlatform.isMobile\n      ? null\n      : context.read<EditorDropManagerState?>();\n\n  bool get isDragEnabled =>\n      dropManagerState?.isDropEnabled == true ||\n      dropManagerState?.contains(CustomImageBlockKeys.type) == true;\n\n  bool showLoading = false;\n  String? errorMessage;\n\n  bool isDraggingFiles = false;\n\n  @override\n  void didChangeDependencies() {\n    if (UniversalPlatform.isMobile) {\n      dropManagerState = context.read<EditorDropManagerState>();\n    }\n    super.didChangeDependencies();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final Widget child = DecoratedBox(\n      decoration: BoxDecoration(\n        color: Theme.of(context).colorScheme.surfaceContainerHighest,\n        borderRadius: BorderRadius.circular(4),\n        border: isDraggingFiles\n            ? Border.all(\n                color: Theme.of(context).colorScheme.primary,\n                width: 2,\n              )\n            : null,\n      ),\n      child: FlowyHover(\n        style: HoverStyle(\n          borderRadius: BorderRadius.circular(4),\n        ),\n        child: SizedBox(\n          height: 52,\n          child: Row(\n            children: [\n              const HSpace(10),\n              FlowySvg(\n                FlowySvgs.slash_menu_icon_image_s,\n                size: const Size.square(24),\n                color: Theme.of(context).hintColor,\n              ),\n              const HSpace(10),\n              ..._buildTrailing(context),\n            ],\n          ),\n        ),\n      ),\n    );\n\n    if (UniversalPlatform.isDesktopOrWeb) {\n      return AppFlowyPopover(\n        controller: controller,\n        direction: PopoverDirection.bottomWithCenterAligned,\n        constraints: const BoxConstraints(\n          maxWidth: 540,\n          maxHeight: 360,\n          minHeight: 80,\n        ),\n        clickHandler: PopoverClickHandler.gestureDetector,\n        popupBuilder: (context) {\n          return UploadImageMenu(\n            allowMultipleImages: true,\n            limitMaximumImageSize: !_isLocalMode(),\n            supportTypes: const [\n              UploadImageType.local,\n              UploadImageType.url,\n              UploadImageType.unsplash,\n            ],\n            onSelectedLocalImages: (files) {\n              controller.close();\n              WidgetsBinding.instance.addPostFrameCallback((_) async {\n                final List<String> items = List.from(\n                  files\n                      .where((file) => file.path.isNotEmpty)\n                      .map((file) => file.path),\n                );\n                if (items.isNotEmpty) {\n                  await insertMultipleLocalImages(items);\n                }\n              });\n            },\n            onSelectedAIImage: (url) {\n              controller.close();\n              WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {\n                await insertAIImage(url);\n              });\n            },\n            onSelectedNetworkImage: (url) {\n              controller.close();\n              WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {\n                await insertNetworkImage(url);\n              });\n            },\n          );\n        },\n        child: DropTarget(\n          enable: isDragEnabled,\n          onDragEntered: (_) {\n            if (isDragEnabled) {\n              setState(() => isDraggingFiles = true);\n            }\n          },\n          onDragExited: (_) {\n            setState(() => isDraggingFiles = false);\n          },\n          onDragDone: (details) {\n            // Only accept files where the mimetype is an image,\n            // otherwise we assume it's a file we cannot display.\n            final imageFiles = details.files\n                .where(\n                  (file) =>\n                      file.mimeType?.startsWith('image/') ??\n                      false || imgExtensionRegex.hasMatch(file.name),\n                )\n                .toList();\n            final paths = imageFiles.map((file) => file.path).toList();\n\n            WidgetsBinding.instance.addPostFrameCallback(\n              (_) async => insertMultipleLocalImages(paths),\n            );\n          },\n          child: child,\n        ),\n      );\n    } else {\n      return MobileBlockActionButtons(\n        node: widget.node,\n        editorState: editorState,\n        child: GestureDetector(\n          onTap: () {\n            editorState.updateSelectionWithReason(null, extraInfo: {});\n            showUploadImageMenu();\n          },\n          child: child,\n        ),\n      );\n    }\n  }\n\n  List<Widget> _buildTrailing(BuildContext context) {\n    if (errorMessage != null) {\n      return [\n        Flexible(\n          child: FlowyText(\n            '${LocaleKeys.document_plugins_image_imageUploadFailed.tr()}: ${errorMessage!}',\n            maxLines: 3,\n          ),\n        ),\n      ];\n    } else if (showLoading) {\n      return [\n        FlowyText(\n          LocaleKeys.document_imageBlock_imageIsUploading.tr(),\n        ),\n        const HSpace(8),\n        const CircularProgressIndicator.adaptive(),\n      ];\n    } else {\n      return [\n        Flexible(\n          child: FlowyText(\n            UniversalPlatform.isDesktop\n                ? isDraggingFiles\n                    ? LocaleKeys.document_plugins_image_dropImageToInsert.tr()\n                    : LocaleKeys.document_plugins_image_addAnImageDesktop.tr()\n                : LocaleKeys.document_plugins_image_addAnImageMobile.tr(),\n            color: Theme.of(context).hintColor,\n          ),\n        ),\n      ];\n    }\n  }\n\n  void showUploadImageMenu() {\n    if (UniversalPlatform.isDesktopOrWeb) {\n      controller.show();\n    } else {\n      final isLocalMode = _isLocalMode();\n      showMobileBottomSheet(\n        context,\n        title: LocaleKeys.editor_image.tr(),\n        showHeader: true,\n        showCloseButton: true,\n        showDragHandle: true,\n        builder: (context) {\n          return Container(\n            margin: const EdgeInsets.only(top: 12.0),\n            constraints: const BoxConstraints(\n              maxHeight: 340,\n              minHeight: 80,\n            ),\n            child: UploadImageMenu(\n              limitMaximumImageSize: !isLocalMode,\n              supportTypes: const [\n                UploadImageType.local,\n                UploadImageType.url,\n                UploadImageType.unsplash,\n              ],\n              onSelectedLocalImages: (files) async {\n                context.pop();\n\n                final items = files\n                    .where((file) => file.path.isNotEmpty)\n                    .map((file) => file.path)\n                    .toList();\n\n                await insertMultipleLocalImages(items);\n              },\n              onSelectedAIImage: (url) async {\n                context.pop();\n                await insertAIImage(url);\n              },\n              onSelectedNetworkImage: (url) async {\n                context.pop();\n                await insertNetworkImage(url);\n              },\n            ),\n          );\n        },\n      );\n    }\n  }\n\n  Future<void> insertMultipleLocalImages(List<String> urls) async {\n    controller.close();\n\n    if (urls.isEmpty) {\n      return;\n    }\n\n    setState(() {\n      showLoading = true;\n      errorMessage = null;\n    });\n\n    bool hasError = false;\n\n    if (_isLocalMode()) {\n      final first = urls.removeAt(0);\n      final firstPath = await saveImageToLocalStorage(first);\n      final transaction = editorState.transaction;\n      transaction.updateNode(widget.node, {\n        CustomImageBlockKeys.url: firstPath,\n        CustomImageBlockKeys.imageType: CustomImageType.local.toIntValue(),\n      });\n\n      if (urls.isNotEmpty) {\n        // Create new nodes for the rest of the images:\n        final paths = await Future.wait(urls.map(saveImageToLocalStorage));\n        paths.removeWhere((url) => url == null || url.isEmpty);\n\n        transaction.insertNodes(\n          widget.node.path.next,\n          paths.map((url) => customImageNode(url: url!)).toList(),\n        );\n      }\n\n      await editorState.apply(transaction);\n    } else {\n      final transaction = editorState.transaction;\n\n      bool isFirst = true;\n      for (final url in urls) {\n        // Upload to cloud\n        final (path, error) = await saveImageToCloudStorage(\n          url,\n          context.read<DocumentBloc>().documentId,\n        );\n\n        if (error != null) {\n          hasError = true;\n\n          if (isFirst) {\n            setState(() => errorMessage = error);\n          }\n\n          continue;\n        }\n\n        if (path != null) {\n          if (isFirst) {\n            isFirst = false;\n            transaction.updateNode(widget.node, {\n              CustomImageBlockKeys.url: path,\n              CustomImageBlockKeys.imageType:\n                  CustomImageType.internal.toIntValue(),\n            });\n          } else {\n            transaction.insertNode(\n              widget.node.path.next,\n              customImageNode(\n                url: path,\n                type: CustomImageType.internal,\n              ),\n            );\n          }\n        }\n      }\n\n      await editorState.apply(transaction);\n    }\n\n    setState(() => showLoading = false);\n\n    if (hasError && mounted) {\n      showSnapBar(\n        context,\n        LocaleKeys.document_imageBlock_error_multipleImagesFailed.tr(),\n      );\n    }\n  }\n\n  Future<void> insertAIImage(String url) async {\n    if (url.isEmpty || !isURL(url)) {\n      // show error\n      return showSnackBarMessage(\n        context,\n        LocaleKeys.document_imageBlock_error_invalidImage.tr(),\n      );\n    }\n\n    final path = await getIt<ApplicationDataStorage>().getPath();\n    final imagePath = p.join(path, 'images');\n    try {\n      // create the directory if not exists\n      final directory = Directory(imagePath);\n      if (!directory.existsSync()) {\n        await directory.create(recursive: true);\n      }\n      final uri = Uri.parse(url);\n      final copyToPath = p.join(\n        imagePath,\n        '${uuid()}${p.extension(uri.path)}',\n      );\n\n      final response = await get(uri);\n      await File(copyToPath).writeAsBytes(response.bodyBytes);\n      await insertMultipleLocalImages([copyToPath]);\n      await File(copyToPath).delete();\n    } catch (e) {\n      Log.error('cannot save image file', e);\n    }\n  }\n\n  Future<void> insertNetworkImage(String url) async {\n    if (url.isEmpty || !isURL(url)) {\n      // show error\n      return showSnackBarMessage(\n        context,\n        LocaleKeys.document_imageBlock_error_invalidImage.tr(),\n      );\n    }\n\n    final transaction = editorState.transaction;\n    transaction.updateNode(widget.node, {\n      CustomImageBlockKeys.url: url,\n      CustomImageBlockKeys.imageType: CustomImageType.external.toIntValue(),\n    });\n    await editorState.apply(transaction);\n  }\n\n  bool _isLocalMode() {\n    return context.read<DocumentBloc>().isLocalMode;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_selection_menu.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nfinal customImageMenuItem = SelectionMenuItem(\n  getName: () => AppFlowyEditorL10n.current.image,\n  icon: (_, isSelected, style) => SelectionMenuIconWidget(\n    name: 'image',\n    isSelected: isSelected,\n    style: style,\n  ),\n  keywords: ['image', 'picture', 'img', 'photo'],\n  handler: (editorState, _, __) async {\n    // use the key to retrieve the state of the image block to show the popover automatically\n    final imagePlaceholderKey = GlobalKey<ImagePlaceholderState>();\n    await editorState.insertEmptyImageBlock(imagePlaceholderKey);\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      imagePlaceholderKey.currentState?.controller.show();\n    });\n  },\n);\n\nfinal multiImageMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_plugins_photoGallery_name.tr(),\n  icon: (_, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.image_s,\n    size: const Size.square(16.0),\n    isSelected: isSelected,\n    style: style,\n  ),\n  keywords: [\n    LocaleKeys.document_plugins_photoGallery_imageKeyword.tr(),\n    LocaleKeys.document_plugins_photoGallery_imageGalleryKeyword.tr(),\n    LocaleKeys.document_plugins_photoGallery_photoKeyword.tr(),\n    LocaleKeys.document_plugins_photoGallery_photoBrowserKeyword.tr(),\n    LocaleKeys.document_plugins_photoGallery_galleryKeyword.tr(),\n  ],\n  handler: (editorState, _, __) async {\n    final imagePlaceholderKey = GlobalKey<ImagePlaceholderState>();\n    await editorState.insertEmptyMultiImageBlock(imagePlaceholderKey);\n    WidgetsBinding.instance.addPostFrameCallback(\n      (_) => imagePlaceholderKey.currentState?.controller.show(),\n    );\n  },\n);\n\nextension InsertImage on EditorState {\n  Future<void> insertEmptyImageBlock(GlobalKey key) async {\n    final selection = this.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n    final path = selection.end.path;\n    final node = getNodeAtPath(path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      return;\n    }\n    final emptyImage = imageNode(url: '')\n      ..extraInfos = {kImagePlaceholderKey: key};\n\n    final insertedPath = delta.isEmpty ? path : path.next;\n    final transaction = this.transaction\n      ..insertNode(insertedPath, emptyImage)\n      ..insertNode(insertedPath, paragraphNode())\n      ..afterSelection = Selection.collapsed(Position(path: insertedPath.next));\n\n    return apply(transaction);\n  }\n\n  Future<void> insertEmptyMultiImageBlock(GlobalKey key) async {\n    final selection = this.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n    final path = selection.end.path;\n    final node = getNodeAtPath(path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      return;\n    }\n    final emptyBlock = multiImageNode()\n      ..extraInfos = {kMultiImagePlaceholderKey: key};\n\n    final insertedPath = delta.isEmpty ? path : path.next;\n    final transaction = this.transaction\n      ..insertNode(insertedPath, emptyBlock)\n      ..insertNode(insertedPath, paragraphNode())\n      ..afterSelection = Selection.collapsed(Position(path: insertedPath.next));\n\n    return apply(transaction);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/prelude.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/shared/custom_image_cache_manager.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/settings/application_data_storage.dart';\nimport 'package:appflowy/workspace/presentation/home/toast.dart';\nimport 'package:appflowy_backend/dispatch/error.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:path/path.dart' as p;\n\nFuture<String?> saveImageToLocalStorage(String localImagePath) async {\n  final path = await getIt<ApplicationDataStorage>().getPath();\n  final imagePath = p.join(\n    path,\n    'images',\n  );\n  try {\n    // create the directory if not exists\n    final directory = Directory(imagePath);\n    if (!directory.existsSync()) {\n      await directory.create(recursive: true);\n    }\n    final copyToPath = p.join(\n      imagePath,\n      '${uuid()}${p.extension(localImagePath)}',\n    );\n    await File(localImagePath).copy(\n      copyToPath,\n    );\n    return copyToPath;\n  } catch (e) {\n    Log.error('cannot save image file', e);\n    return null;\n  }\n}\n\nFuture<(String? path, String? errorMessage)> saveImageToCloudStorage(\n  String localImagePath,\n  String documentId,\n) async {\n  final documentService = DocumentService();\n  Log.debug(\"Uploading image local path: $localImagePath\");\n  final result = await documentService.uploadFile(\n    localFilePath: localImagePath,\n    documentId: documentId,\n  );\n  return result.fold(\n    (s) async {\n      await CustomImageCacheManager().putFile(\n        s.url,\n        File(localImagePath).readAsBytesSync(),\n      );\n      return (s.url, null);\n    },\n    (err) {\n      final message = Platform.isIOS\n          ? LocaleKeys.sideBar_storageLimitDialogTitleIOS.tr()\n          : LocaleKeys.sideBar_storageLimitDialogTitle.tr();\n      if (err.isStorageLimitExceeded) {\n        return (null, message);\n      } else {\n        return (null, err.msg);\n      }\n    },\n  );\n}\n\nFuture<List<ImageBlockData>> extractAndUploadImages(\n  BuildContext context,\n  List<String?> urls,\n  bool isLocalMode,\n) async {\n  final List<ImageBlockData> images = [];\n\n  bool hasError = false;\n  for (final url in urls) {\n    if (url == null || url.isEmpty) {\n      continue;\n    }\n\n    String? path;\n    String? errorMsg;\n    CustomImageType imageType = CustomImageType.local;\n\n    // If the user is using local authenticator, we save the image to local storage\n    if (isLocalMode) {\n      path = await saveImageToLocalStorage(url);\n    } else {\n      // Else we save the image to cloud storage\n      (path, errorMsg) = await saveImageToCloudStorage(\n        url,\n        context.read<DocumentBloc>().documentId,\n      );\n      imageType = CustomImageType.internal;\n    }\n\n    if (path != null && errorMsg == null) {\n      images.add(ImageBlockData(url: path, type: imageType));\n    } else {\n      hasError = true;\n    }\n  }\n\n  if (context.mounted && hasError) {\n    showSnackBarMessage(\n      context,\n      LocaleKeys.document_imageBlock_error_multipleImagesFailed.tr(),\n    );\n  }\n\n  return images;\n}\n\n@visibleForTesting\nint deleteImageTestCounter = 0;\n\nFuture<void> deleteImageFromLocalStorage(String localImagePath) async {\n  try {\n    await File(localImagePath)\n        .delete()\n        .whenComplete(() => deleteImageTestCounter++);\n  } catch (e) {\n    Log.error('cannot delete image file', e);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nfinal imageMobileToolbarItem = MobileToolbarItem.action(\n  itemIconBuilder: (_, __, ___) => const FlowySvg(FlowySvgs.m_toolbar_imae_lg),\n  actionHandler: (_, editorState) async {\n    final imagePlaceholderKey = GlobalKey<ImagePlaceholderState>();\n    await editorState.insertEmptyImageBlock(imagePlaceholderKey);\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      imagePlaceholderKey.currentState?.showUploadImageMenu();\n    });\n  },\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/image_render.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:flowy_infra/size.dart';\n\n@visibleForTesting\nclass ImageRender extends StatelessWidget {\n  const ImageRender({\n    super.key,\n    required this.image,\n    this.userProfile,\n    this.fit = BoxFit.cover,\n    this.borderRadius = Corners.s6Border,\n  });\n\n  final ImageBlockData image;\n  final UserProfilePB? userProfile;\n  final BoxFit fit;\n  final BorderRadius? borderRadius;\n\n  @override\n  Widget build(BuildContext context) {\n    final child = switch (image.type) {\n      CustomImageType.internal || CustomImageType.external => FlowyNetworkImage(\n          url: image.url,\n          userProfilePB: userProfile,\n          fit: fit,\n        ),\n      CustomImageType.local => Image.file(File(image.url), fit: fit),\n    };\n\n    return Container(\n      clipBehavior: Clip.antiAlias,\n      decoration: BoxDecoration(borderRadius: borderRadius),\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/multi_image_layouts.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy/shared/patterns/file_type_patterns.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:collection/collection.dart';\nimport 'package:desktop_drop/desktop_drop.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nimport '../image_render.dart';\n\nconst _thumbnailItemSize = 100.0, _imageHeight = 400.0;\n\nclass ImageBrowserLayout extends ImageBlockMultiLayout {\n  const ImageBrowserLayout({\n    super.key,\n    required super.node,\n    required super.editorState,\n    required super.images,\n    required super.indexNotifier,\n    required super.isLocalMode,\n    required this.onIndexChanged,\n  });\n\n  final void Function(int) onIndexChanged;\n\n  @override\n  State<ImageBrowserLayout> createState() => _ImageBrowserLayoutState();\n}\n\nclass _ImageBrowserLayoutState extends State<ImageBrowserLayout> {\n  UserProfilePB? _userProfile;\n  bool isDraggingFiles = false;\n\n  @override\n  void initState() {\n    super.initState();\n    _userProfile = context.read<UserWorkspaceBloc?>()?.state.userProfile ??\n        context.read<DocumentBloc>().state.userProfilePB;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final gallery = Stack(\n      children: [\n        Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            SizedBox(\n              height: _imageHeight,\n              width: MediaQuery.of(context).size.width,\n              child: GestureDetector(\n                onDoubleTap: () => _openInteractiveViewer(context),\n                child: ImageRender(\n                  image: widget.images[widget.indexNotifier.value],\n                  userProfile: _userProfile,\n                  fit: BoxFit.contain,\n                ),\n              ),\n            ),\n            const VSpace(8),\n            LayoutBuilder(\n              builder: (context, constraints) {\n                final maxItems =\n                    (constraints.maxWidth / (_thumbnailItemSize + 4)).floor();\n                final items = widget.images.take(maxItems).toList();\n\n                return Center(\n                  child: Wrap(\n                    children: items.mapIndexed((index, image) {\n                      final isLast = items.last == image;\n                      final amountLeft = widget.images.length - items.length;\n                      if (isLast && amountLeft > 0) {\n                        return MouseRegion(\n                          cursor: SystemMouseCursors.click,\n                          child: GestureDetector(\n                            onTap: () => _openInteractiveViewer(\n                              context,\n                              maxItems - 1,\n                            ),\n                            child: Container(\n                              width: _thumbnailItemSize,\n                              height: _thumbnailItemSize,\n                              padding: const EdgeInsets.all(2),\n                              margin: const EdgeInsets.all(2),\n                              decoration: BoxDecoration(\n                                borderRadius: Corners.s8Border,\n                                border: Border.all(\n                                  width: 2,\n                                  color: Theme.of(context).dividerColor,\n                                ),\n                              ),\n                              child: DecoratedBox(\n                                decoration: BoxDecoration(\n                                  borderRadius: Corners.s6Border,\n                                  image: image.type == CustomImageType.local\n                                      ? DecorationImage(\n                                          image: FileImage(File(image.url)),\n                                          fit: BoxFit.cover,\n                                          opacity: 0.5,\n                                        )\n                                      : null,\n                                ),\n                                child: Stack(\n                                  children: [\n                                    if (image.type != CustomImageType.local)\n                                      Positioned.fill(\n                                        child: Container(\n                                          clipBehavior: Clip.antiAlias,\n                                          decoration: const BoxDecoration(\n                                            borderRadius: Corners.s6Border,\n                                          ),\n                                          child: FlowyNetworkImage(\n                                            url: image.url,\n                                            userProfilePB: _userProfile,\n                                          ),\n                                        ),\n                                      ),\n                                    DecoratedBox(\n                                      decoration: BoxDecoration(\n                                        color:\n                                            Colors.white.withValues(alpha: 0.5),\n                                      ),\n                                      child: Center(\n                                        child: FlowyText(\n                                          '+$amountLeft',\n                                          color: AFThemeExtension.of(context)\n                                              .strongText,\n                                          fontSize: 24,\n                                          fontWeight: FontWeight.bold,\n                                        ),\n                                      ),\n                                    ),\n                                  ],\n                                ),\n                              ),\n                            ),\n                          ),\n                        );\n                      }\n\n                      return MouseRegion(\n                        cursor: SystemMouseCursors.click,\n                        child: GestureDetector(\n                          onTap: () => widget.onIndexChanged(index),\n                          child: ThumbnailItem(\n                            images: widget.images,\n                            index: index,\n                            selectedIndex: widget.indexNotifier.value,\n                            userProfile: _userProfile,\n                            onDeleted: () async {\n                              final transaction =\n                                  widget.editorState.transaction;\n\n                              final images = widget.images.toList();\n                              images.removeAt(index);\n\n                              transaction.updateNode(\n                                widget.node,\n                                {\n                                  MultiImageBlockKeys.images:\n                                      images.map((e) => e.toJson()).toList(),\n                                  MultiImageBlockKeys.layout: widget.node\n                                      .attributes[MultiImageBlockKeys.layout],\n                                },\n                              );\n\n                              await widget.editorState.apply(transaction);\n\n                              widget.onIndexChanged(\n                                widget.indexNotifier.value > 0\n                                    ? widget.indexNotifier.value - 1\n                                    : 0,\n                              );\n                            },\n                          ),\n                        ),\n                      );\n                    }).toList(),\n                  ),\n                );\n              },\n            ),\n          ],\n        ),\n        Positioned.fill(\n          child: DropTarget(\n            onDragEntered: (_) => setState(() => isDraggingFiles = true),\n            onDragExited: (_) => setState(() => isDraggingFiles = false),\n            onDragDone: (details) {\n              setState(() => isDraggingFiles = false);\n              // Only accept files where the mimetype is an image,\n              // or the file extension is a known image format,\n              // otherwise we assume it's a file we cannot display.\n              final imageFiles = details.files\n                  .where(\n                    (file) =>\n                        file.mimeType?.startsWith('image/') ??\n                        false || imgExtensionRegex.hasMatch(file.name),\n                  )\n                  .toList();\n              final paths = imageFiles.map((file) => file.path).toList();\n              WidgetsBinding.instance.addPostFrameCallback(\n                (_) async => insertLocalImages(paths),\n              );\n            },\n            child: !isDraggingFiles\n                ? const SizedBox.shrink()\n                : SizedBox.expand(\n                    child: DecoratedBox(\n                      decoration: BoxDecoration(\n                        color: Colors.white.withValues(alpha: 0.5),\n                      ),\n                      child: Center(\n                        child: Row(\n                          mainAxisAlignment: MainAxisAlignment.center,\n                          children: [\n                            const FlowySvg(\n                              FlowySvgs.download_s,\n                              size: Size.square(28),\n                            ),\n                            const HSpace(12),\n                            Flexible(\n                              child: FlowyText(\n                                LocaleKeys\n                                    .document_plugins_image_dropImageToInsert\n                                    .tr(),\n                                color: AFThemeExtension.of(context).strongText,\n                                fontSize: 22,\n                                fontWeight: FontWeight.w500,\n                                overflow: TextOverflow.ellipsis,\n                              ),\n                            ),\n                          ],\n                        ),\n                      ),\n                    ),\n                  ),\n          ),\n        ),\n      ],\n    );\n    return SizedBox(\n      height: _imageHeight + _thumbnailItemSize + 20,\n      child: gallery,\n    );\n  }\n\n  void _openInteractiveViewer(BuildContext context, [int? index]) => showDialog(\n        context: context,\n        builder: (_) => InteractiveImageViewer(\n          userProfile: _userProfile,\n          imageProvider: AFBlockImageProvider(\n            images: widget.images,\n            initialIndex: index ?? widget.indexNotifier.value,\n            onDeleteImage: (index) async {\n              final transaction = widget.editorState.transaction;\n              final newImages = widget.images.toList();\n              newImages.removeAt(index);\n\n              widget.onIndexChanged(\n                widget.indexNotifier.value > 0\n                    ? widget.indexNotifier.value - 1\n                    : 0,\n              );\n\n              if (newImages.isNotEmpty) {\n                transaction.updateNode(\n                  widget.node,\n                  {\n                    MultiImageBlockKeys.images:\n                        newImages.map((e) => e.toJson()).toList(),\n                    MultiImageBlockKeys.layout:\n                        widget.node.attributes[MultiImageBlockKeys.layout],\n                  },\n                );\n              } else {\n                transaction.deleteNode(widget.node);\n              }\n\n              await widget.editorState.apply(transaction);\n            },\n          ),\n        ),\n      );\n\n  Future<void> insertLocalImages(List<String?> urls) async {\n    if (urls.isEmpty || urls.every((path) => path?.isEmpty ?? true)) {\n      return;\n    }\n\n    final isLocalMode = context.read<DocumentBloc>().isLocalMode;\n    final transaction = widget.editorState.transaction;\n    final images = await extractAndUploadImages(context, urls, isLocalMode);\n    if (images.isEmpty) {\n      return;\n    }\n\n    final newImages = [...widget.images, ...images];\n    final imagesJson = newImages.map((image) => image.toJson()).toList();\n\n    transaction.updateNode(widget.node, {\n      MultiImageBlockKeys.images: imagesJson,\n      MultiImageBlockKeys.layout:\n          widget.node.attributes[MultiImageBlockKeys.layout],\n    });\n\n    await widget.editorState.apply(transaction);\n  }\n}\n\n@visibleForTesting\nclass ThumbnailItem extends StatefulWidget {\n  const ThumbnailItem({\n    super.key,\n    required this.images,\n    required this.index,\n    required this.selectedIndex,\n    required this.onDeleted,\n    this.userProfile,\n  });\n\n  final List<ImageBlockData> images;\n  final int index;\n  final int selectedIndex;\n  final VoidCallback onDeleted;\n  final UserProfilePB? userProfile;\n\n  @override\n  State<ThumbnailItem> createState() => _ThumbnailItemState();\n}\n\nclass _ThumbnailItemState extends State<ThumbnailItem> {\n  bool isHovering = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      onEnter: (_) => setState(() => isHovering = true),\n      onExit: (_) => setState(() => isHovering = false),\n      child: Container(\n        width: _thumbnailItemSize,\n        height: _thumbnailItemSize,\n        padding: const EdgeInsets.all(2),\n        margin: const EdgeInsets.all(2),\n        decoration: BoxDecoration(\n          borderRadius: Corners.s8Border,\n          border: Border.all(\n            width: 2,\n            color: widget.index == widget.selectedIndex\n                ? Theme.of(context).colorScheme.primary\n                : Theme.of(context).dividerColor,\n          ),\n        ),\n        child: Stack(\n          children: [\n            Positioned.fill(\n              child: ImageRender(\n                image: widget.images[widget.index],\n                userProfile: widget.userProfile,\n              ),\n            ),\n            Positioned(\n              top: 4,\n              right: 4,\n              child: AnimatedOpacity(\n                opacity: isHovering ? 1 : 0,\n                duration: const Duration(milliseconds: 100),\n                child: FlowyTooltip(\n                  message: LocaleKeys.button_delete.tr(),\n                  child: GestureDetector(\n                    behavior: HitTestBehavior.opaque,\n                    onTap: widget.onDeleted,\n                    child: FlowyHover(\n                      resetHoverOnRebuild: false,\n                      style: HoverStyle(\n                        backgroundColor: Colors.black.withValues(alpha: 0.6),\n                        hoverColor: Colors.black.withValues(alpha: 0.9),\n                      ),\n                      child: const Padding(\n                        padding: EdgeInsets.all(4),\n                        child: FlowySvg(\n                          FlowySvgs.delete_s,\n                          color: Colors.white,\n                        ),\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_grid_layout.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/image_render.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/multi_image_layouts.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';\nimport 'package:provider/provider.dart';\n\nclass ImageGridLayout extends ImageBlockMultiLayout {\n  const ImageGridLayout({\n    super.key,\n    required super.node,\n    required super.editorState,\n    required super.images,\n    required super.indexNotifier,\n    required super.isLocalMode,\n  });\n\n  @override\n  State<ImageGridLayout> createState() => _ImageGridLayoutState();\n}\n\nclass _ImageGridLayoutState extends State<ImageGridLayout> {\n  @override\n  Widget build(BuildContext context) {\n    return StaggeredGridBuilder(\n      images: widget.images,\n      onImageDoubleTapped: (index) {\n        _openInteractiveViewer(context, index);\n      },\n    );\n  }\n\n  void _openInteractiveViewer(BuildContext context, int index) => showDialog(\n        context: context,\n        builder: (_) => InteractiveImageViewer(\n          userProfile: context.read<DocumentBloc>().state.userProfilePB,\n          imageProvider: AFBlockImageProvider(\n            images: widget.images,\n            initialIndex: index,\n            onDeleteImage: (index) async {\n              final transaction = widget.editorState.transaction;\n              final newImages = widget.images.toList();\n              newImages.removeAt(index);\n\n              if (newImages.isNotEmpty) {\n                transaction.updateNode(\n                  widget.node,\n                  {\n                    MultiImageBlockKeys.images:\n                        newImages.map((e) => e.toJson()).toList(),\n                    MultiImageBlockKeys.layout:\n                        widget.node.attributes[MultiImageBlockKeys.layout],\n                  },\n                );\n              } else {\n                transaction.deleteNode(widget.node);\n              }\n\n              await widget.editorState.apply(transaction);\n            },\n          ),\n        ),\n      );\n}\n\n/// Draws a staggered grid of images, where the pattern is based\n/// on the amount of images to fill the grid at all times.\n///\n/// They will be alternating depending on the current index of the images, such that\n/// the layout is reversed in odd segments.\n///\n/// If there are 4 images in the last segment, this layout will be used:\n/// ┌─────┐┌─┐┌─┐\n/// │     │└─┘└─┘\n/// │     │┌────┐\n/// └─────┘└────┘\n///\n/// If there are 3 images in the last segment, this layout will be used:\n/// ┌─────┐┌────┐\n/// │     │└────┘\n/// │     │┌────┐\n/// └─────┘└────┘\n///\n/// If there are 2 images in the last segment, this layout will be used:\n/// ┌─────┐┌─────┐\n/// │     ││     │\n/// └─────┘└─────┘\n///\n/// If there is 1 image in the last segment, this layout will be used:\n/// ┌──────────┐\n/// │          │\n/// └──────────┘\nclass StaggeredGridBuilder extends StatefulWidget {\n  const StaggeredGridBuilder({\n    super.key,\n    required this.images,\n    required this.onImageDoubleTapped,\n  });\n\n  final List<ImageBlockData> images;\n  final void Function(int) onImageDoubleTapped;\n\n  @override\n  State<StaggeredGridBuilder> createState() => _StaggeredGridBuilderState();\n}\n\nclass _StaggeredGridBuilderState extends State<StaggeredGridBuilder> {\n  late final UserProfilePB? _userProfile;\n  final List<List<ImageBlockData>> _splitImages = [];\n\n  @override\n  void initState() {\n    super.initState();\n    _userProfile = context.read<DocumentBloc>().state.userProfilePB;\n\n    for (int i = 0; i < widget.images.length; i += 4) {\n      final end = (i + 4 < widget.images.length) ? i + 4 : widget.images.length;\n      _splitImages.add(widget.images.sublist(i, end));\n    }\n  }\n\n  @override\n  void didUpdateWidget(covariant StaggeredGridBuilder oldWidget) {\n    if (widget.images.length != oldWidget.images.length) {\n      _splitImages.clear();\n      for (int i = 0; i < widget.images.length; i += 4) {\n        final end =\n            (i + 4 < widget.images.length) ? i + 4 : widget.images.length;\n        _splitImages.add(widget.images.sublist(i, end));\n      }\n    }\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return StaggeredGrid.count(\n      crossAxisCount: 4,\n      mainAxisSpacing: 6,\n      crossAxisSpacing: 6,\n      children:\n          _splitImages.indexed.map(_buildTilesForImages).flattened.toList(),\n    );\n  }\n\n  List<Widget> _buildTilesForImages((int, List<ImageBlockData>) data) {\n    final index = data.$1;\n    final images = data.$2;\n\n    final isReversed = index.isOdd;\n\n    if (images.length == 4) {\n      return [\n        StaggeredGridTile.count(\n          crossAxisCellCount: isReversed ? 1 : 2,\n          mainAxisCellCount: isReversed ? 1 : 2,\n          child: GestureDetector(\n            onDoubleTap: () {\n              final imageIndex = index * 4;\n              widget.onImageDoubleTapped(imageIndex);\n            },\n            child: ImageRender(\n              image: images[0],\n              userProfile: _userProfile,\n              borderRadius: BorderRadius.zero,\n            ),\n          ),\n        ),\n        StaggeredGridTile.count(\n          crossAxisCellCount: 1,\n          mainAxisCellCount: 1,\n          child: GestureDetector(\n            onDoubleTap: () {\n              final imageIndex = index * 4 + 1;\n              widget.onImageDoubleTapped(imageIndex);\n            },\n            child: ImageRender(\n              image: images[1],\n              userProfile: _userProfile,\n              borderRadius: BorderRadius.zero,\n            ),\n          ),\n        ),\n        StaggeredGridTile.count(\n          crossAxisCellCount: isReversed ? 2 : 1,\n          mainAxisCellCount: isReversed ? 2 : 1,\n          child: GestureDetector(\n            onDoubleTap: () {\n              final imageIndex = index * 4 + 2;\n              widget.onImageDoubleTapped(imageIndex);\n            },\n            child: ImageRender(\n              image: images[2],\n              userProfile: _userProfile,\n              borderRadius: BorderRadius.zero,\n            ),\n          ),\n        ),\n        StaggeredGridTile.count(\n          crossAxisCellCount: 2,\n          mainAxisCellCount: 1,\n          child: GestureDetector(\n            onDoubleTap: () {\n              final imageIndex = index * 4 + 3;\n              widget.onImageDoubleTapped(imageIndex);\n            },\n            child: ImageRender(\n              image: images[3],\n              userProfile: _userProfile,\n              borderRadius: BorderRadius.zero,\n            ),\n          ),\n        ),\n      ];\n    } else if (images.length == 3) {\n      return [\n        StaggeredGridTile.count(\n          crossAxisCellCount: 2,\n          mainAxisCellCount: isReversed ? 1 : 2,\n          child: GestureDetector(\n            onDoubleTap: () {\n              final imageIndex = index * 4;\n              widget.onImageDoubleTapped(imageIndex);\n            },\n            child: ImageRender(\n              image: images[0],\n              userProfile: _userProfile,\n              borderRadius: BorderRadius.zero,\n            ),\n          ),\n        ),\n        StaggeredGridTile.count(\n          crossAxisCellCount: 2,\n          mainAxisCellCount: isReversed ? 2 : 1,\n          child: GestureDetector(\n            onDoubleTap: () {\n              final imageIndex = index * 4 + 1;\n              widget.onImageDoubleTapped(imageIndex);\n            },\n            child: ImageRender(\n              image: images[1],\n              userProfile: _userProfile,\n              borderRadius: BorderRadius.zero,\n            ),\n          ),\n        ),\n        StaggeredGridTile.count(\n          crossAxisCellCount: 2,\n          mainAxisCellCount: 1,\n          child: GestureDetector(\n            onDoubleTap: () {\n              final imageIndex = index * 4 + 2;\n              widget.onImageDoubleTapped(imageIndex);\n            },\n            child: ImageRender(\n              image: images[2],\n              userProfile: _userProfile,\n              borderRadius: BorderRadius.zero,\n            ),\n          ),\n        ),\n      ];\n    } else if (images.length == 2) {\n      return [\n        StaggeredGridTile.count(\n          crossAxisCellCount: 2,\n          mainAxisCellCount: 2,\n          child: GestureDetector(\n            onDoubleTap: () {\n              final imageIndex = index * 4;\n              widget.onImageDoubleTapped(imageIndex);\n            },\n            child: ImageRender(\n              image: images[0],\n              userProfile: _userProfile,\n              borderRadius: BorderRadius.zero,\n            ),\n          ),\n        ),\n        StaggeredGridTile.count(\n          crossAxisCellCount: 2,\n          mainAxisCellCount: 2,\n          child: GestureDetector(\n            onDoubleTap: () {\n              final imageIndex = index * 4 + 1;\n              widget.onImageDoubleTapped(imageIndex);\n            },\n            child: ImageRender(\n              image: images[1],\n              userProfile: _userProfile,\n              borderRadius: BorderRadius.zero,\n            ),\n          ),\n        ),\n      ];\n    } else {\n      return [\n        StaggeredGridTile.count(\n          crossAxisCellCount: 4,\n          mainAxisCellCount: 2,\n          child: GestureDetector(\n            onDoubleTap: () {\n              final imageIndex = index * 4;\n              widget.onImageDoubleTapped(imageIndex);\n            },\n            child: ImageRender(\n              image: images[0],\n              userProfile: _userProfile,\n              borderRadius: BorderRadius.zero,\n            ),\n          ),\n        ),\n      ];\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/multi_image_layouts.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_grid_layout.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart' hide ResizableImage;\nimport 'package:flutter/material.dart';\n\nabstract class ImageBlockMultiLayout extends StatefulWidget {\n  const ImageBlockMultiLayout({\n    super.key,\n    required this.node,\n    required this.editorState,\n    required this.images,\n    required this.indexNotifier,\n    required this.isLocalMode,\n  });\n\n  final Node node;\n  final EditorState editorState;\n  final List<ImageBlockData> images;\n  final ValueNotifier<int> indexNotifier;\n  final bool isLocalMode;\n}\n\nclass ImageLayoutRender extends StatelessWidget {\n  const ImageLayoutRender({\n    super.key,\n    required this.node,\n    required this.editorState,\n    required this.images,\n    required this.indexNotifier,\n    required this.isLocalMode,\n    required this.onIndexChanged,\n  });\n\n  final Node node;\n  final EditorState editorState;\n  final List<ImageBlockData> images;\n  final ValueNotifier<int> indexNotifier;\n  final bool isLocalMode;\n  final void Function(int) onIndexChanged;\n\n  @override\n  Widget build(BuildContext context) {\n    final layout = _getLayout();\n\n    return _buildLayout(layout);\n  }\n\n  MultiImageLayout _getLayout() {\n    return MultiImageLayout.fromIntValue(\n      node.attributes[MultiImageBlockKeys.layout] ?? 0,\n    );\n  }\n\n  Widget _buildLayout(MultiImageLayout layout) {\n    switch (layout) {\n      case MultiImageLayout.grid:\n        return ImageGridLayout(\n          node: node,\n          editorState: editorState,\n          images: images,\n          indexNotifier: indexNotifier,\n          isLocalMode: isLocalMode,\n        );\n      case MultiImageLayout.browser:\n        return ImageBrowserLayout(\n          node: node,\n          editorState: editorState,\n          images: images,\n          indexNotifier: indexNotifier,\n          isLocalMode: isLocalMode,\n          onIndexChanged: onIndexChanged,\n        );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/multi_image_layouts.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_placeholder.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nconst kMultiImagePlaceholderKey = 'multiImagePlaceholderKey';\n\nNode multiImageNode({List<ImageBlockData>? images}) => Node(\n      type: MultiImageBlockKeys.type,\n      attributes: {\n        MultiImageBlockKeys.images:\n            MultiImageData(images: images ?? []).toJson(),\n        MultiImageBlockKeys.layout: MultiImageLayout.browser.toIntValue(),\n      },\n    );\n\nclass MultiImageBlockKeys {\n  const MultiImageBlockKeys._();\n\n  static const String type = 'multi_image';\n\n  /// The image data for the block, stored as a JSON encoded list of [ImageBlockData].\n  ///\n  static const String images = 'images';\n\n  /// The layout of the images.\n  ///\n  /// The value is a MultiImageLayout enum.\n  ///\n  static const String layout = 'layout';\n}\n\ntypedef MultiImageBlockComponentMenuBuilder = Widget Function(\n  Node node,\n  MultiImageBlockComponentState state,\n  ValueNotifier<int> indexNotifier,\n  VoidCallback onImageDeleted,\n);\n\nclass MultiImageBlockComponentBuilder extends BlockComponentBuilder {\n  MultiImageBlockComponentBuilder({\n    super.configuration,\n    this.showMenu = false,\n    this.menuBuilder,\n  });\n\n  final bool showMenu;\n  final MultiImageBlockComponentMenuBuilder? menuBuilder;\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    return MultiImageBlockComponent(\n      key: node.key,\n      node: node,\n      showActions: showActions(node),\n      configuration: configuration,\n      actionBuilder: (_, state) => actionBuilder(blockComponentContext, state),\n      showMenu: showMenu,\n      menuBuilder: menuBuilder,\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate => (node) => node.children.isEmpty;\n}\n\nclass MultiImageBlockComponent extends BlockComponentStatefulWidget {\n  const MultiImageBlockComponent({\n    super.key,\n    required super.node,\n    super.showActions,\n    this.showMenu = false,\n    this.menuBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n  });\n\n  final bool showMenu;\n\n  final MultiImageBlockComponentMenuBuilder? menuBuilder;\n\n  @override\n  State<MultiImageBlockComponent> createState() =>\n      MultiImageBlockComponentState();\n}\n\nclass MultiImageBlockComponentState extends State<MultiImageBlockComponent>\n    with SelectableMixin, BlockComponentConfigurable {\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  @override\n  Node get node => widget.node;\n\n  final multiImageKey = GlobalKey();\n\n  RenderBox? get _renderBox => context.findRenderObject() as RenderBox?;\n\n  late final editorState = Provider.of<EditorState>(context, listen: false);\n\n  final showActionsNotifier = ValueNotifier<bool>(false);\n\n  ValueNotifier<int> indexNotifier = ValueNotifier(0);\n\n  bool alwaysShowMenu = false;\n\n  static const _interceptorKey = 'multi-image-block-interceptor';\n\n  late final interceptor = SelectionGestureInterceptor(\n    key: _interceptorKey,\n    canTap: (details) => _isTapInBounds(details.globalPosition),\n    canPanStart: (details) => _isTapInBounds(details.globalPosition),\n  );\n\n  @override\n  void initState() {\n    super.initState();\n    editorState.selectionService.registerGestureInterceptor(interceptor);\n  }\n\n  @override\n  void dispose() {\n    editorState.selectionService.unregisterGestureInterceptor(_interceptorKey);\n    super.dispose();\n  }\n\n  bool _isTapInBounds(Offset offset) {\n    if (_renderBox == null) {\n      // We shouldn't block any actions if the render box is not available.\n      // This has the potential to break taps on the editor completely if we\n      // accidentally return false here.\n      return true;\n    }\n\n    final localPosition = _renderBox!.globalToLocal(offset);\n    return !_renderBox!.paintBounds.contains(localPosition);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final data = MultiImageData.fromJson(\n      node.attributes[MultiImageBlockKeys.images],\n    );\n\n    Widget child;\n    if (data.images.isEmpty) {\n      final multiImagePlaceholderKey =\n          node.extraInfos?[kMultiImagePlaceholderKey];\n\n      child = MultiImagePlaceholder(\n        key: multiImagePlaceholderKey is GlobalKey\n            ? multiImagePlaceholderKey\n            : null,\n        node: node,\n      );\n    } else {\n      child = ImageLayoutRender(\n        node: node,\n        images: data.images,\n        editorState: editorState,\n        indexNotifier: indexNotifier,\n        isLocalMode: context.read<DocumentBloc>().isLocalMode,\n        onIndexChanged: (index) => setState(() => indexNotifier.value = index),\n      );\n    }\n\n    if (UniversalPlatform.isDesktopOrWeb) {\n      child = BlockSelectionContainer(\n        node: node,\n        delegate: this,\n        listenable: editorState.selectionNotifier,\n        blockColor: editorState.editorStyle.selectionColor,\n        supportTypes: const [BlockSelectionType.block],\n        child: Padding(key: multiImageKey, padding: padding, child: child),\n      );\n    } else {\n      child = Padding(key: multiImageKey, padding: padding, child: child);\n    }\n\n    if (widget.showActions && widget.actionBuilder != null) {\n      child = BlockComponentActionWrapper(\n        node: node,\n        actionBuilder: widget.actionBuilder!,\n        actionTrailingBuilder: widget.actionTrailingBuilder,\n        child: child,\n      );\n    }\n\n    if (UniversalPlatform.isDesktopOrWeb) {\n      if (widget.showMenu && widget.menuBuilder != null) {\n        child = MouseRegion(\n          onEnter: (_) => showActionsNotifier.value = true,\n          onExit: (_) {\n            if (!alwaysShowMenu) {\n              showActionsNotifier.value = false;\n            }\n          },\n          hitTestBehavior: HitTestBehavior.opaque,\n          opaque: false,\n          child: ValueListenableBuilder<bool>(\n            valueListenable: showActionsNotifier,\n            builder: (context, value, child) {\n              return Stack(\n                children: [\n                  BlockSelectionContainer(\n                    node: node,\n                    delegate: this,\n                    listenable: editorState.selectionNotifier,\n                    cursorColor: editorState.editorStyle.cursorColor,\n                    selectionColor: editorState.editorStyle.selectionColor,\n                    child: child!,\n                  ),\n                  if (value && data.images.isNotEmpty)\n                    widget.menuBuilder!(\n                      widget.node,\n                      this,\n                      indexNotifier,\n                      () => setState(\n                        () => indexNotifier.value = indexNotifier.value > 0\n                            ? indexNotifier.value - 1\n                            : 0,\n                      ),\n                    ),\n                ],\n              );\n            },\n            child: child,\n          ),\n        );\n      }\n    } else {\n      // show a fixed menu on mobile\n      child = MobileBlockActionButtons(\n        showThreeDots: false,\n        node: node,\n        editorState: editorState,\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  @override\n  Position start() => Position(path: widget.node.path);\n\n  @override\n  Position end() => Position(path: widget.node.path, offset: 1);\n\n  @override\n  Position getPositionInOffset(Offset start) => end();\n\n  @override\n  bool get shouldCursorBlink => false;\n\n  @override\n  CursorStyle get cursorStyle => CursorStyle.cover;\n\n  @override\n  Rect getBlockRect({\n    bool shiftWithBaseOffset = false,\n  }) {\n    final imageBox = multiImageKey.currentContext?.findRenderObject();\n    if (imageBox is RenderBox) {\n      return Offset.zero & imageBox.size;\n    }\n    return Rect.zero;\n  }\n\n  @override\n  Rect? getCursorRectInPosition(\n    Position position, {\n    bool shiftWithBaseOffset = false,\n  }) {\n    final rects = getRectsInSelection(Selection.collapsed(position));\n    return rects.firstOrNull;\n  }\n\n  @override\n  List<Rect> getRectsInSelection(\n    Selection selection, {\n    bool shiftWithBaseOffset = false,\n  }) {\n    if (_renderBox == null) {\n      return [];\n    }\n    final parentBox = context.findRenderObject();\n    final imageBox = multiImageKey.currentContext?.findRenderObject();\n    if (parentBox is RenderBox && imageBox is RenderBox) {\n      return [\n        imageBox.localToGlobal(Offset.zero, ancestor: parentBox) &\n            imageBox.size,\n      ];\n    }\n    return [Offset.zero & _renderBox!.size];\n  }\n\n  @override\n  Selection getSelectionInRange(Offset start, Offset end) => Selection.single(\n        path: widget.node.path,\n        startOffset: 0,\n        endOffset: 1,\n      );\n\n  @override\n  Offset localToGlobal(\n    Offset offset, {\n    bool shiftWithBaseOffset = false,\n  }) =>\n      _renderBox!.localToGlobal(offset);\n}\n\n/// The data for a multi-image block, primarily used for\n/// serializing and deserializing the block's images.\n///\nclass MultiImageData {\n  factory MultiImageData.fromJson(List<dynamic> json) {\n    final images = json\n        .map((e) => ImageBlockData.fromJson(e as Map<String, dynamic>))\n        .toList();\n    return MultiImageData(images: images);\n  }\n\n  MultiImageData({required this.images});\n\n  final List<ImageBlockData> images;\n\n  List<dynamic> toJson() => images.map((e) => e.toJson()).toList();\n}\n\nenum MultiImageLayout {\n  browser,\n  grid;\n\n  int toIntValue() {\n    switch (this) {\n      case MultiImageLayout.browser:\n        return 0;\n      case MultiImageLayout.grid:\n        return 1;\n    }\n  }\n\n  static MultiImageLayout fromIntValue(int value) {\n    switch (value) {\n      case 0:\n        return MultiImageLayout.browser;\n      case 1:\n        return MultiImageLayout.grid;\n      default:\n        throw UnimplementedError();\n    }\n  }\n\n  String get label => switch (this) {\n        browser => LocaleKeys.document_plugins_photoGallery_browserLayout.tr(),\n        grid => LocaleKeys.document_plugins_photoGallery_gridLayout.tr(),\n      };\n\n  FlowySvgData get icon => switch (this) {\n        browser => FlowySvgs.photo_layout_browser_s,\n        grid => FlowySvgs.photo_layout_grid_s,\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/workspace/application/settings/application_data_storage.dart';\nimport 'package:appflowy/workspace/presentation/home/toast.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu;\nimport 'package:cross_file/cross_file.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:provider/provider.dart';\nimport 'package:string_validator/string_validator.dart';\n\nconst _interceptorKey = 'add-image';\n\nclass MultiImageMenu extends StatefulWidget {\n  const MultiImageMenu({\n    super.key,\n    required this.node,\n    required this.state,\n    required this.indexNotifier,\n    this.isLocalMode = true,\n    required this.onImageDeleted,\n  });\n\n  final Node node;\n  final MultiImageBlockComponentState state;\n  final ValueNotifier<int> indexNotifier;\n  final bool isLocalMode;\n  final VoidCallback onImageDeleted;\n\n  @override\n  State<MultiImageMenu> createState() => _MultiImageMenuState();\n}\n\nclass _MultiImageMenuState extends State<MultiImageMenu> {\n  final gestureInterceptor = SelectionGestureInterceptor(\n    key: _interceptorKey,\n    canTap: (details) => false,\n  );\n\n  final PopoverController controller = PopoverController();\n  final PopoverController layoutController = PopoverController();\n  late List<ImageBlockData> images;\n  late final EditorState editorState;\n\n  @override\n  void initState() {\n    super.initState();\n    editorState = context.read<EditorState>();\n    images = MultiImageData.fromJson(\n      widget.node.attributes[MultiImageBlockKeys.images] ?? {},\n    ).images;\n  }\n\n  @override\n  void dispose() {\n    allowMenuClose();\n    controller.close();\n    layoutController.close();\n    super.dispose();\n  }\n\n  @override\n  void didUpdateWidget(covariant MultiImageMenu oldWidget) {\n    images = MultiImageData.fromJson(\n      widget.node.attributes[MultiImageBlockKeys.images] ?? {},\n    ).images;\n\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = Theme.of(context);\n    final layout = MultiImageLayout.fromIntValue(\n      widget.node.attributes[MultiImageBlockKeys.layout] ?? 0,\n    );\n    return Container(\n      height: 32,\n      decoration: BoxDecoration(\n        color: theme.cardColor,\n        boxShadow: [\n          BoxShadow(\n            blurRadius: 5,\n            spreadRadius: 1,\n            color: Colors.black.withValues(alpha: 0.1),\n          ),\n        ],\n        borderRadius: BorderRadius.circular(4.0),\n      ),\n      child: Row(\n        children: [\n          const HSpace(4),\n          AppFlowyPopover(\n            controller: controller,\n            direction: PopoverDirection.bottomWithRightAligned,\n            onClose: allowMenuClose,\n            constraints: const BoxConstraints(\n              maxWidth: 540,\n              maxHeight: 360,\n              minHeight: 80,\n            ),\n            offset: const Offset(0, 10),\n            popupBuilder: (context) {\n              preventMenuClose();\n              return UploadImageMenu(\n                allowMultipleImages: true,\n                supportTypes: const [\n                  UploadImageType.local,\n                  UploadImageType.url,\n                  UploadImageType.unsplash,\n                ],\n                onSelectedLocalImages: insertLocalImages,\n                onSelectedAIImage: insertAIImage,\n                onSelectedNetworkImage: insertNetworkImage,\n              );\n            },\n            child: MenuBlockButton(\n              tooltip:\n                  LocaleKeys.document_plugins_photoGallery_addImageTooltip.tr(),\n              iconData: FlowySvgs.add_s,\n              onTap: () {},\n            ),\n          ),\n          const HSpace(4),\n          AppFlowyPopover(\n            controller: layoutController,\n            onClose: allowMenuClose,\n            direction: PopoverDirection.bottomWithRightAligned,\n            offset: const Offset(0, 10),\n            constraints: const BoxConstraints(\n              maxHeight: 300,\n              maxWidth: 300,\n            ),\n            popupBuilder: (context) {\n              preventMenuClose();\n              return Column(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  _LayoutSelector(\n                    selectedLayout: layout,\n                    onSelected: (layout) {\n                      allowMenuClose();\n                      layoutController.close();\n                      final transaction = editorState.transaction;\n                      transaction.updateNode(widget.node, {\n                        MultiImageBlockKeys.images:\n                            widget.node.attributes[MultiImageBlockKeys.images],\n                        MultiImageBlockKeys.layout: layout.toIntValue(),\n                      });\n                      editorState.apply(transaction);\n                    },\n                  ),\n                ],\n              );\n            },\n            child: MenuBlockButton(\n              tooltip: LocaleKeys\n                  .document_plugins_photoGallery_changeLayoutTooltip\n                  .tr(),\n              iconData: FlowySvgs.edit_layout_s,\n              onTap: () {},\n            ),\n          ),\n          const HSpace(4),\n          MenuBlockButton(\n            tooltip: LocaleKeys.document_imageBlock_openFullScreen.tr(),\n            iconData: FlowySvgs.full_view_s,\n            onTap: openFullScreen,\n          ),\n\n          // disable the copy link button if the image is hosted on appflowy cloud\n          // because the url needs the verification token to be accessible\n          if (layout == MultiImageLayout.browser &&\n              !images[widget.indexNotifier.value].url.isAppFlowyCloudUrl) ...[\n            const HSpace(4),\n            MenuBlockButton(\n              tooltip: LocaleKeys.editor_copyLink.tr(),\n              iconData: FlowySvgs.copy_s,\n              onTap: copyImageLink,\n            ),\n          ],\n          const _Divider(),\n          MenuBlockButton(\n            tooltip: LocaleKeys.document_plugins_photoGallery_deleteBlockTooltip\n                .tr(),\n            iconData: FlowySvgs.delete_s,\n            onTap: deleteImage,\n          ),\n          const HSpace(4),\n        ],\n      ),\n    );\n  }\n\n  void copyImageLink() {\n    Clipboard.setData(\n      ClipboardData(text: images[widget.indexNotifier.value].url),\n    );\n    showToastNotification(\n      message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(),\n    );\n  }\n\n  Future<void> deleteImage() async {\n    final node = widget.node;\n    final editorState = context.read<EditorState>();\n    final transaction = editorState.transaction;\n    transaction.deleteNode(node);\n    transaction.afterSelection = null;\n    await editorState.apply(transaction);\n  }\n\n  void openFullScreen() {\n    showDialog(\n      context: context,\n      builder: (_) => InteractiveImageViewer(\n        userProfile: context.read<DocumentBloc>().state.userProfilePB,\n        imageProvider: AFBlockImageProvider(\n          images: images,\n          initialIndex: widget.indexNotifier.value,\n          onDeleteImage: (index) async {\n            final transaction = editorState.transaction;\n            final newImages = List<ImageBlockData>.from(images);\n            newImages.removeAt(index);\n\n            images = newImages;\n            widget.onImageDeleted();\n\n            final imagesJson =\n                newImages.map((image) => image.toJson()).toList();\n            transaction.updateNode(widget.node, {\n              MultiImageBlockKeys.images: imagesJson,\n              MultiImageBlockKeys.layout:\n                  widget.node.attributes[MultiImageBlockKeys.layout],\n            });\n\n            await editorState.apply(transaction);\n          },\n        ),\n      ),\n    );\n  }\n\n  void preventMenuClose() {\n    widget.state.alwaysShowMenu = true;\n    editorState.service.selectionService.registerGestureInterceptor(\n      gestureInterceptor,\n    );\n  }\n\n  void allowMenuClose() {\n    widget.state.alwaysShowMenu = false;\n    editorState.service.selectionService.unregisterGestureInterceptor(\n      _interceptorKey,\n    );\n  }\n\n  Future<void> insertLocalImages(List<XFile> files) async {\n    controller.close();\n\n    WidgetsBinding.instance.addPostFrameCallback((_) async {\n      final urls = files\n          .map((file) => file.path)\n          .where((path) => path.isNotEmpty)\n          .toList();\n\n      if (urls.isEmpty || urls.every((url) => url.isEmpty)) {\n        return;\n      }\n\n      final transaction = editorState.transaction;\n      final newImages =\n          await extractAndUploadImages(context, urls, widget.isLocalMode);\n      if (newImages.isEmpty) {\n        return;\n      }\n\n      final imagesJson =\n          [...images, ...newImages].map((i) => i.toJson()).toList();\n      transaction.updateNode(widget.node, {\n        MultiImageBlockKeys.images: imagesJson,\n        MultiImageBlockKeys.layout:\n            widget.node.attributes[MultiImageBlockKeys.layout],\n      });\n\n      await editorState.apply(transaction);\n      setState(() => images = newImages);\n    });\n  }\n\n  Future<void> insertAIImage(String url) async {\n    controller.close();\n\n    if (url.isEmpty || !isURL(url)) {\n      // show error\n      return showSnackBarMessage(\n        context,\n        LocaleKeys.document_imageBlock_error_invalidImage.tr(),\n      );\n    }\n\n    final path = await getIt<ApplicationDataStorage>().getPath();\n    final imagePath = p.join(path, 'images');\n    try {\n      // create the directory if not exists\n      final directory = Directory(imagePath);\n      if (!directory.existsSync()) {\n        await directory.create(recursive: true);\n      }\n      final uri = Uri.parse(url);\n      final copyToPath = p.join(\n        imagePath,\n        '${uuid()}${p.extension(uri.path)}',\n      );\n\n      final response = await get(uri);\n      await File(copyToPath).writeAsBytes(response.bodyBytes);\n      await insertLocalImages([XFile(copyToPath)]);\n      await File(copyToPath).delete();\n    } catch (e) {\n      Log.error('cannot save image file', e);\n    }\n  }\n\n  Future<void> insertNetworkImage(String url) async {\n    controller.close();\n\n    if (url.isEmpty || !isURL(url)) {\n      // show error\n      return showSnackBarMessage(\n        context,\n        LocaleKeys.document_imageBlock_error_invalidImage.tr(),\n      );\n    }\n\n    final transaction = editorState.transaction;\n\n    final newImages = [\n      ...images,\n      ImageBlockData(url: url, type: CustomImageType.external),\n    ];\n\n    final imagesJson = newImages.map((image) => image.toJson()).toList();\n    transaction.updateNode(widget.node, {\n      MultiImageBlockKeys.images: imagesJson,\n      MultiImageBlockKeys.layout:\n          widget.node.attributes[MultiImageBlockKeys.layout],\n    });\n\n    await editorState.apply(transaction);\n    setState(() => images = newImages);\n  }\n}\n\nclass _Divider extends StatelessWidget {\n  const _Divider();\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(8),\n      child: Container(width: 1, color: Colors.grey),\n    );\n  }\n}\n\nclass _LayoutSelector extends StatelessWidget {\n  const _LayoutSelector({\n    required this.selectedLayout,\n    required this.onSelected,\n  });\n\n  final MultiImageLayout selectedLayout;\n  final Function(MultiImageLayout) onSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return SeparatedRow(\n      separatorBuilder: () => const HSpace(6),\n      mainAxisSize: MainAxisSize.min,\n      children: MultiImageLayout.values\n          .map(\n            (layout) => MouseRegion(\n              cursor: SystemMouseCursors.click,\n              child: GestureDetector(\n                onTap: () => onSelected(layout),\n                child: Container(\n                  height: 80,\n                  width: 80,\n                  padding: const EdgeInsets.all(8),\n                  decoration: BoxDecoration(\n                    border: Border.all(\n                      width: 2,\n                      color: selectedLayout == layout\n                          ? Theme.of(context).colorScheme.primary\n                          : Theme.of(context).dividerColor,\n                    ),\n                    borderRadius: Corners.s8Border,\n                  ),\n                  child: Column(\n                    mainAxisSize: MainAxisSize.min,\n                    mainAxisAlignment: MainAxisAlignment.center,\n                    children: [\n                      FlowySvg(\n                        layout.icon,\n                        color: AFThemeExtension.of(context).strongText,\n                        size: const Size.square(24),\n                      ),\n                      const VSpace(6),\n                      FlowyText(layout.label),\n                    ],\n                  ),\n                ),\n              ),\n            ),\n          )\n          .toList(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_placeholder.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/application/document_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';\nimport 'package:appflowy/shared/patterns/file_type_patterns.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/settings/application_data_storage.dart';\nimport 'package:appflowy/workspace/presentation/home/toast.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu;\nimport 'package:desktop_drop/desktop_drop.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:http/http.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:provider/provider.dart';\nimport 'package:string_validator/string_validator.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass MultiImagePlaceholder extends StatefulWidget {\n  const MultiImagePlaceholder({super.key, required this.node});\n\n  final Node node;\n\n  @override\n  State<MultiImagePlaceholder> createState() => MultiImagePlaceholderState();\n}\n\nclass MultiImagePlaceholderState extends State<MultiImagePlaceholder> {\n  final controller = PopoverController();\n  final documentService = DocumentService();\n  late final editorState = context.read<EditorState>();\n\n  bool isDraggingFiles = false;\n\n  @override\n  Widget build(BuildContext context) {\n    final child = DecoratedBox(\n      decoration: BoxDecoration(\n        color: Theme.of(context).colorScheme.surfaceContainerHighest,\n        borderRadius: BorderRadius.circular(4),\n        border: isDraggingFiles\n            ? Border.all(\n                color: Theme.of(context).colorScheme.primary,\n                width: 2,\n              )\n            : null,\n      ),\n      child: FlowyHover(\n        style: HoverStyle(\n          borderRadius: BorderRadius.circular(4),\n        ),\n        child: Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),\n          child: Row(\n            children: [\n              FlowySvg(\n                FlowySvgs.slash_menu_icon_photo_gallery_s,\n                color: Theme.of(context).hintColor,\n                size: const Size.square(24),\n              ),\n              const HSpace(10),\n              FlowyText(\n                UniversalPlatform.isDesktop\n                    ? isDraggingFiles\n                        ? LocaleKeys.document_plugins_image_dropImageToInsert\n                            .tr()\n                        : LocaleKeys.document_plugins_image_addAnImageDesktop\n                            .tr()\n                    : LocaleKeys.document_plugins_image_addAnImageMobile.tr(),\n                color: Theme.of(context).hintColor,\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n\n    if (UniversalPlatform.isDesktopOrWeb) {\n      return AppFlowyPopover(\n        controller: controller,\n        direction: PopoverDirection.bottomWithCenterAligned,\n        constraints: const BoxConstraints(\n          maxWidth: 540,\n          maxHeight: 360,\n          minHeight: 80,\n        ),\n        clickHandler: PopoverClickHandler.gestureDetector,\n        popupBuilder: (_) {\n          return UploadImageMenu(\n            allowMultipleImages: true,\n            limitMaximumImageSize: !_isLocalMode(),\n            supportTypes: const [\n              UploadImageType.local,\n              UploadImageType.url,\n              UploadImageType.unsplash,\n            ],\n            onSelectedLocalImages: (files) {\n              controller.close();\n              WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {\n                final paths = files.map((file) => file.path).toList();\n                await insertLocalImages(paths);\n              });\n            },\n            onSelectedAIImage: (url) {\n              controller.close();\n              WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {\n                await insertAIImage(url);\n              });\n            },\n            onSelectedNetworkImage: (url) {\n              controller.close();\n              WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {\n                await insertNetworkImage(url);\n              });\n            },\n          );\n        },\n        child: DropTarget(\n          onDragEntered: (_) => setState(() => isDraggingFiles = true),\n          onDragExited: (_) => setState(() => isDraggingFiles = false),\n          onDragDone: (details) {\n            // Only accept files where the mimetype is an image,\n            // or the file extension is a known image format,\n            // otherwise we assume it's a file we cannot display.\n            final imageFiles = details.files\n                .where(\n                  (file) =>\n                      file.mimeType?.startsWith('image/') ??\n                      false || imgExtensionRegex.hasMatch(file.name),\n                )\n                .toList();\n            final paths = imageFiles.map((file) => file.path).toList();\n            WidgetsBinding.instance.addPostFrameCallback(\n              (_) async => insertLocalImages(paths),\n            );\n          },\n          child: child,\n        ),\n      );\n    } else {\n      return MobileBlockActionButtons(\n        node: widget.node,\n        editorState: editorState,\n        child: GestureDetector(\n          onTap: () {\n            editorState.updateSelectionWithReason(null, extraInfo: {});\n            showUploadImageMenu();\n          },\n          child: child,\n        ),\n      );\n    }\n  }\n\n  void showUploadImageMenu() {\n    if (UniversalPlatform.isDesktopOrWeb) {\n      controller.show();\n    } else {\n      final isLocalMode = _isLocalMode();\n      showMobileBottomSheet(\n        context,\n        title: LocaleKeys.editor_image.tr(),\n        showHeader: true,\n        showCloseButton: true,\n        showDragHandle: true,\n        builder: (context) {\n          return Container(\n            margin: const EdgeInsets.only(top: 12.0),\n            constraints: const BoxConstraints(\n              maxHeight: 340,\n              minHeight: 80,\n            ),\n            child: UploadImageMenu(\n              limitMaximumImageSize: !isLocalMode,\n              allowMultipleImages: true,\n              supportTypes: const [\n                UploadImageType.local,\n                UploadImageType.url,\n                UploadImageType.unsplash,\n              ],\n              onSelectedLocalImages: (files) async {\n                context.pop();\n                final items = files.map((file) => file.path).toList();\n                await insertLocalImages(items);\n              },\n              onSelectedAIImage: (url) async {\n                context.pop();\n                await insertAIImage(url);\n              },\n              onSelectedNetworkImage: (url) async {\n                context.pop();\n                await insertNetworkImage(url);\n              },\n            ),\n          );\n        },\n      );\n    }\n  }\n\n  Future<void> insertLocalImages(List<String?> urls) async {\n    controller.close();\n\n    if (urls.isEmpty || urls.every((path) => path?.isEmpty ?? true)) {\n      return;\n    }\n\n    final transaction = editorState.transaction;\n    final images = await extractAndUploadImages(context, urls, _isLocalMode());\n    if (images.isEmpty) {\n      return;\n    }\n\n    final imagesJson = images.map((image) => image.toJson()).toList();\n\n    transaction.updateNode(widget.node, {\n      MultiImageBlockKeys.images: imagesJson,\n      MultiImageBlockKeys.layout:\n          widget.node.attributes[MultiImageBlockKeys.layout] ??\n              MultiImageLayout.browser.toIntValue(),\n    });\n\n    await editorState.apply(transaction);\n  }\n\n  Future<void> insertAIImage(String url) async {\n    if (url.isEmpty || !isURL(url)) {\n      // show error\n      return showSnackBarMessage(\n        context,\n        LocaleKeys.document_imageBlock_error_invalidImage.tr(),\n      );\n    }\n\n    final path = await getIt<ApplicationDataStorage>().getPath();\n    final imagePath = p.join(path, 'images');\n    try {\n      // create the directory if not exists\n      final directory = Directory(imagePath);\n      if (!directory.existsSync()) {\n        await directory.create(recursive: true);\n      }\n      final uri = Uri.parse(url);\n      final copyToPath = p.join(\n        imagePath,\n        '${uuid()}${p.extension(uri.path)}',\n      );\n\n      final response = await get(uri);\n      await File(copyToPath).writeAsBytes(response.bodyBytes);\n      await insertLocalImages([copyToPath]);\n      await File(copyToPath).delete();\n    } catch (e) {\n      Log.error('cannot save image file', e);\n    }\n  }\n\n  Future<void> insertNetworkImage(String url) async {\n    if (url.isEmpty || !isURL(url)) {\n      // show error\n      return showSnackBarMessage(\n        context,\n        LocaleKeys.document_imageBlock_error_invalidImage.tr(),\n      );\n    }\n\n    final transaction = editorState.transaction;\n\n    final images = [\n      ImageBlockData(\n        url: url,\n        type: CustomImageType.external,\n      ),\n    ];\n\n    transaction.updateNode(widget.node, {\n      MultiImageBlockKeys.images:\n          images.map((image) => image.toJson()).toList(),\n      MultiImageBlockKeys.layout:\n          widget.node.attributes[MultiImageBlockKeys.layout] ??\n              MultiImageLayout.browser.toIntValue(),\n    });\n    await editorState.apply(transaction);\n  }\n\n  bool _isLocalMode() {\n    return context.read<DocumentBloc>().isLocalMode;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart",
    "content": "import 'dart:io';\nimport 'dart:math';\n\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/prelude.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_cache_manager/flutter_cache_manager.dart';\nimport 'package:string_validator/string_validator.dart';\n\nenum ResizableImageState {\n  loading,\n  loaded,\n  failed,\n}\n\nclass ResizableImage extends StatefulWidget {\n  const ResizableImage({\n    super.key,\n    required this.type,\n    required this.alignment,\n    required this.editable,\n    required this.onResize,\n    required this.width,\n    required this.src,\n    this.height,\n    this.onDoubleTap,\n    this.onStateChange,\n  });\n\n  final String src;\n  final CustomImageType type;\n  final double width;\n  final double? height;\n  final Alignment alignment;\n  final bool editable;\n  final VoidCallback? onDoubleTap;\n  final ValueChanged<ResizableImageState>? onStateChange;\n\n  final void Function(double width) onResize;\n\n  @override\n  State<ResizableImage> createState() => _ResizableImageState();\n}\n\nconst _kImageBlockComponentMinWidth = 30.0;\n\nclass _ResizableImageState extends State<ResizableImage> {\n  final documentService = DocumentService();\n\n  double initialOffset = 0;\n  double moveDistance = 0;\n  Widget? _cacheImage;\n\n  late double imageWidth;\n\n  @visibleForTesting\n  bool onFocus = false;\n\n  UserProfilePB? _userProfilePB;\n\n  @override\n  void initState() {\n    super.initState();\n\n    imageWidth = widget.width;\n\n    _userProfilePB = context.read<UserWorkspaceBloc?>()?.state.userProfile ??\n        context.read<DocumentBloc>().state.userProfilePB;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Align(\n      alignment: widget.alignment,\n      child: SizedBox(\n        width: max(_kImageBlockComponentMinWidth, imageWidth - moveDistance),\n        height: widget.height,\n        child: MouseRegion(\n          onEnter: (_) => setState(() => onFocus = true),\n          onExit: (_) => setState(() => onFocus = false),\n          child: GestureDetector(\n            onDoubleTap: widget.onDoubleTap,\n            child: _buildResizableImage(context),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildResizableImage(BuildContext context) {\n    Widget child;\n    final src = widget.src;\n    if (isURL(src)) {\n      _cacheImage = FlowyNetworkImage(\n        url: widget.src,\n        width: imageWidth - moveDistance,\n        userProfilePB: _userProfilePB,\n        onImageLoaded: (isImageInCache) {\n          if (isImageInCache) {\n            widget.onStateChange?.call(ResizableImageState.loaded);\n          }\n        },\n        progressIndicatorBuilder: (context, _, progress) {\n          if (progress.totalSize != null) {\n            if (progress.progress == 1) {\n              widget.onStateChange?.call(ResizableImageState.loaded);\n            } else {\n              widget.onStateChange?.call(ResizableImageState.loading);\n            }\n          }\n\n          return _buildLoading(context);\n        },\n        errorWidgetBuilder: (_, __, error) {\n          widget.onStateChange?.call(ResizableImageState.failed);\n          return _ImageLoadFailedWidget(\n            width: imageWidth,\n            error: error,\n            onRetry: () {\n              setState(() {\n                final retryCounter = FlowyNetworkRetryCounter();\n                retryCounter.clear(tag: src, url: src);\n              });\n            },\n          );\n        },\n      );\n\n      child = _cacheImage!;\n    } else {\n      // load local file\n      _cacheImage ??= Image.file(File(src));\n      child = _cacheImage!;\n    }\n    return Stack(\n      children: [\n        child,\n        if (widget.editable) ...[\n          _buildEdgeGesture(\n            context,\n            top: 0,\n            left: 5,\n            bottom: 0,\n            width: 5,\n            onUpdate: (distance) => setState(() => moveDistance = distance),\n          ),\n          _buildEdgeGesture(\n            context,\n            top: 0,\n            right: 5,\n            bottom: 0,\n            width: 5,\n            onUpdate: (distance) => setState(() => moveDistance = -distance),\n          ),\n        ],\n      ],\n    );\n  }\n\n  Widget _buildLoading(BuildContext context) {\n    return SizedBox(\n      height: 150,\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          SizedBox.fromSize(\n            size: const Size(18, 18),\n            child: const CircularProgressIndicator(),\n          ),\n          SizedBox.fromSize(size: const Size(10, 10)),\n          Text(AppFlowyEditorL10n.current.loading),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildEdgeGesture(\n    BuildContext context, {\n    double? top,\n    double? left,\n    double? right,\n    double? bottom,\n    double? width,\n    void Function(double distance)? onUpdate,\n  }) {\n    return Positioned(\n      top: top,\n      left: left,\n      right: right,\n      bottom: bottom,\n      width: width,\n      child: GestureDetector(\n        onHorizontalDragStart: (details) {\n          initialOffset = details.globalPosition.dx;\n        },\n        onHorizontalDragUpdate: (details) {\n          if (onUpdate != null) {\n            double offset = details.globalPosition.dx - initialOffset;\n            if (widget.alignment == Alignment.center) {\n              offset *= 2.0;\n            }\n            onUpdate(offset);\n          }\n        },\n        onHorizontalDragEnd: (details) {\n          imageWidth =\n              max(_kImageBlockComponentMinWidth, imageWidth - moveDistance);\n          initialOffset = 0;\n          moveDistance = 0;\n\n          widget.onResize(imageWidth);\n        },\n        child: MouseRegion(\n          cursor: SystemMouseCursors.resizeLeftRight,\n          child: onFocus\n              ? Center(\n                  child: Container(\n                    height: 40,\n                    decoration: BoxDecoration(\n                      color: Colors.black.withValues(alpha: 0.5),\n                      borderRadius: const BorderRadius.all(\n                        Radius.circular(5.0),\n                      ),\n                      border: Border.all(color: Colors.white),\n                    ),\n                  ),\n                )\n              : null,\n        ),\n      ),\n    );\n  }\n}\n\nclass _ImageLoadFailedWidget extends StatelessWidget {\n  const _ImageLoadFailedWidget({\n    required this.width,\n    required this.error,\n    required this.onRetry,\n  });\n\n  final double width;\n  final Object error;\n  final VoidCallback onRetry;\n\n  @override\n  Widget build(BuildContext context) {\n    final error = _getErrorMessage();\n    return Container(\n      height: 160,\n      width: width,\n      alignment: Alignment.center,\n      padding: const EdgeInsets.symmetric(vertical: 8.0),\n      decoration: BoxDecoration(\n        borderRadius: const BorderRadius.all(Radius.circular(4.0)),\n        border: Border.all(color: Colors.grey.withValues(alpha: 0.6)),\n      ),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          const FlowySvg(\n            FlowySvgs.broken_image_xl,\n            size: Size.square(36),\n          ),\n          FlowyText(\n            AppFlowyEditorL10n.current.imageLoadFailed,\n            fontSize: 14,\n          ),\n          const VSpace(4),\n          if (error != null)\n            FlowyText(\n              error,\n              textAlign: TextAlign.center,\n              color: Theme.of(context).hintColor.withValues(alpha: 0.6),\n              fontSize: 10,\n              maxLines: 2,\n            ),\n          const VSpace(12),\n          Listener(\n            onPointerDown: (event) {\n              onRetry();\n            },\n            child: OutlinedRoundedButton(\n              text: LocaleKeys.chat_retry.tr(),\n              onTap: () {},\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  String? _getErrorMessage() {\n    if (error is HttpExceptionWithStatus) {\n      return 'Error ${(error as HttpExceptionWithStatus).statusCode}';\n    }\n\n    return null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart",
    "content": "import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:unsplash_client/unsplash_client.dart';\n\nconst _accessKeyA = 'YyD-LbW5bVolHWZBq5fWRM_';\nconst _accessKeyB = '3ezkG2XchRFjhNTnK9TE';\nconst _secretKeyA = '5z4EnxaXjWjWMnuBhc0Ku0u';\nconst _secretKeyB = 'YW2bsYCZlO-REZaqmV6A';\n\nenum UnsplashImageType {\n  // the creator name is under the image\n  halfScreen,\n  // the creator name is on the image\n  fullScreen,\n}\n\ntypedef OnSelectUnsplashImage = void Function(String url);\n\nclass UnsplashImageWidget extends StatefulWidget {\n  const UnsplashImageWidget({\n    super.key,\n    this.type = UnsplashImageType.halfScreen,\n    required this.onSelectUnsplashImage,\n  });\n\n  final UnsplashImageType type;\n  final OnSelectUnsplashImage onSelectUnsplashImage;\n\n  @override\n  State<UnsplashImageWidget> createState() => _UnsplashImageWidgetState();\n}\n\nclass _UnsplashImageWidgetState extends State<UnsplashImageWidget> {\n  final unsplash = UnsplashClient(\n    settings: const ClientSettings(\n      credentials: AppCredentials(\n        accessKey: _accessKeyA + _accessKeyB,\n        secretKey: _secretKeyA + _secretKeyB,\n      ),\n    ),\n  );\n\n  late Future<List<Photo>> randomPhotos;\n\n  String query = '';\n\n  @override\n  void initState() {\n    super.initState();\n    randomPhotos = unsplash.photos\n        .random(count: 18, orientation: PhotoOrientation.landscape)\n        .goAndGet();\n  }\n\n  @override\n  void dispose() {\n    unsplash.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        SizedBox(\n          height: 44,\n          child: FlowyMobileSearchTextField(\n            onChanged: (keyword) => query = keyword,\n            onSubmitted: (_) => _search(),\n          ),\n        ),\n        const VSpace(12.0),\n        Expanded(\n          child: FutureBuilder(\n            future: randomPhotos,\n            builder: (context, value) {\n              final data = value.data;\n              if (!value.hasData ||\n                  value.connectionState != ConnectionState.done ||\n                  data == null ||\n                  data.isEmpty) {\n                return const Center(\n                  child: CircularProgressIndicator.adaptive(),\n                );\n              }\n              return _UnsplashImages(\n                type: widget.type,\n                photos: data,\n                onSelectUnsplashImage: widget.onSelectUnsplashImage,\n              );\n            },\n          ),\n        ),\n      ],\n    );\n  }\n\n  void _search() {\n    setState(() {\n      randomPhotos = unsplash.photos\n          .random(\n            count: 18,\n            orientation: PhotoOrientation.landscape,\n            query: query,\n          )\n          .goAndGet();\n    });\n  }\n}\n\nclass _UnsplashImages extends StatefulWidget {\n  const _UnsplashImages({\n    required this.type,\n    required this.photos,\n    required this.onSelectUnsplashImage,\n  });\n\n  final UnsplashImageType type;\n  final List<Photo> photos;\n  final OnSelectUnsplashImage onSelectUnsplashImage;\n\n  @override\n  State<_UnsplashImages> createState() => _UnsplashImagesState();\n}\n\nclass _UnsplashImagesState extends State<_UnsplashImages> {\n  int _selectedPhotoIndex = -1;\n\n  @override\n  Widget build(BuildContext context) {\n    const mainAxisSpacing = 16.0;\n    final crossAxisCount = switch (widget.type) {\n      UnsplashImageType.halfScreen => 3,\n      UnsplashImageType.fullScreen => 2,\n    };\n    final crossAxisSpacing = switch (widget.type) {\n      UnsplashImageType.halfScreen => 10.0,\n      UnsplashImageType.fullScreen => 16.0,\n    };\n\n    return GridView.count(\n      crossAxisCount: crossAxisCount,\n      mainAxisSpacing: mainAxisSpacing,\n      crossAxisSpacing: crossAxisSpacing,\n      childAspectRatio: 4 / 3,\n      children: widget.photos.asMap().entries.map((entry) {\n        final index = entry.key;\n        final photo = entry.value;\n        return _UnsplashImage(\n          type: widget.type,\n          photo: photo,\n          isSelected: index == _selectedPhotoIndex,\n          onTap: () {\n            widget.onSelectUnsplashImage(photo.urls.full.toString());\n            setState(() => _selectedPhotoIndex = index);\n          },\n        );\n      }).toList(),\n    );\n  }\n}\n\nclass _UnsplashImage extends StatelessWidget {\n  const _UnsplashImage({\n    required this.type,\n    required this.photo,\n    required this.onTap,\n    required this.isSelected,\n  });\n\n  final UnsplashImageType type;\n  final Photo photo;\n  final VoidCallback onTap;\n  final bool isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    final child = switch (type) {\n      UnsplashImageType.halfScreen => _buildHalfScreenImage(context),\n      UnsplashImageType.fullScreen => _buildFullScreenImage(context),\n    };\n\n    return GestureDetector(\n      onTap: onTap,\n      child: isSelected\n          ? Container(\n              clipBehavior: Clip.antiAlias,\n              decoration: ShapeDecoration(\n                shape: RoundedRectangleBorder(\n                  side: const BorderSide(width: 1.50, color: Color(0xFF00BCF0)),\n                  borderRadius: BorderRadius.circular(8.0),\n                ),\n              ),\n              padding: const EdgeInsets.all(2.0),\n              child: child,\n            )\n          : child,\n    );\n  }\n\n  Widget _buildHalfScreenImage(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.stretch,\n      children: [\n        Expanded(\n          child: Image.network(\n            photo.urls.thumb.toString(),\n            fit: BoxFit.cover,\n          ),\n        ),\n        const HSpace(2.0),\n        FlowyText('by ${photo.name}', fontSize: 10.0),\n      ],\n    );\n  }\n\n  Widget _buildFullScreenImage(BuildContext context) {\n    return ClipRRect(\n      borderRadius: BorderRadius.circular(8.0),\n      child: Stack(\n        children: [\n          LayoutBuilder(\n            builder: (_, constraints) => Image.network(\n              photo.urls.thumb.toString(),\n              fit: BoxFit.cover,\n              width: constraints.maxWidth,\n              height: constraints.maxHeight,\n            ),\n          ),\n          Positioned(\n            bottom: 9,\n            left: 10,\n            child: FlowyText.medium(\n              photo.name,\n              fontSize: 13.0,\n              color: Colors.white,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nextension on Photo {\n  String get name {\n    if (user.username.isNotEmpty) {\n      return user.username;\n    } else if (user.name.isNotEmpty) {\n      return user.name;\n    } else if (user.email?.isNotEmpty == true) {\n      return user.email!;\n    }\n\n    return user.id;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/upload_image_file_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart' hide ColorOption;\nimport 'package:cross_file/cross_file.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'widgets/embed_image_url_widget.dart';\n\nenum UploadImageType {\n  local,\n  url,\n  unsplash,\n  color;\n\n  String get description => switch (this) {\n        UploadImageType.local =>\n          LocaleKeys.document_imageBlock_upload_label.tr(),\n        UploadImageType.url =>\n          LocaleKeys.document_imageBlock_embedLink_label.tr(),\n        UploadImageType.unsplash =>\n          LocaleKeys.document_imageBlock_unsplash_label.tr(),\n        UploadImageType.color => LocaleKeys.document_plugins_cover_colors.tr(),\n      };\n}\n\nclass UploadImageMenu extends StatefulWidget {\n  const UploadImageMenu({\n    super.key,\n    required this.onSelectedLocalImages,\n    required this.onSelectedAIImage,\n    required this.onSelectedNetworkImage,\n    this.onSelectedColor,\n    this.supportTypes = UploadImageType.values,\n    this.limitMaximumImageSize = false,\n    this.allowMultipleImages = false,\n  });\n\n  final void Function(List<XFile>) onSelectedLocalImages;\n  final void Function(String url) onSelectedAIImage;\n  final void Function(String url) onSelectedNetworkImage;\n  final void Function(String color)? onSelectedColor;\n  final List<UploadImageType> supportTypes;\n  final bool limitMaximumImageSize;\n  final bool allowMultipleImages;\n\n  @override\n  State<UploadImageMenu> createState() => _UploadImageMenuState();\n}\n\nclass _UploadImageMenuState extends State<UploadImageMenu> {\n  late final List<UploadImageType> values;\n  int currentTabIndex = 0;\n\n  @override\n  void initState() {\n    super.initState();\n    values = widget.supportTypes;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return DefaultTabController(\n      length: values.length,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          TabBar(\n            onTap: (value) => setState(() {\n              currentTabIndex = value;\n            }),\n            indicatorSize: TabBarIndicatorSize.label,\n            isScrollable: true,\n            overlayColor: WidgetStatePropertyAll(\n              UniversalPlatform.isDesktop\n                  ? Theme.of(context).colorScheme.secondary\n                  : Colors.transparent,\n            ),\n            padding: EdgeInsets.zero,\n            tabs: values.map(\n              (e) {\n                final child = Padding(\n                  padding: EdgeInsets.only(\n                    left: 12.0,\n                    right: 12.0,\n                    bottom: 8.0,\n                    top: UniversalPlatform.isMobile ? 0 : 8.0,\n                  ),\n                  child: FlowyText(e.description),\n                );\n                if (UniversalPlatform.isDesktop) {\n                  return FlowyHover(\n                    style: const HoverStyle(borderRadius: BorderRadius.zero),\n                    child: child,\n                  );\n                }\n                return child;\n              },\n            ).toList(),\n          ),\n          const Divider(height: 2),\n          _buildTab(),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildTab() {\n    final constraints =\n        UniversalPlatform.isMobile ? const BoxConstraints(minHeight: 92) : null;\n    final type = values[currentTabIndex];\n    switch (type) {\n      case UploadImageType.local:\n        Widget child = UploadImageFileWidget(\n          allowMultipleImages: widget.allowMultipleImages,\n          onPickFiles: widget.onSelectedLocalImages,\n        );\n        if (UniversalPlatform.isDesktop) {\n          child = Padding(\n            padding: const EdgeInsets.all(8.0),\n            child: Container(\n              alignment: Alignment.center,\n              decoration: BoxDecoration(\n                borderRadius: BorderRadius.circular(8),\n                border: Border.all(\n                  color: Theme.of(context).colorScheme.outline,\n                ),\n              ),\n              constraints: constraints,\n              child: child,\n            ),\n          );\n        } else {\n          child = Padding(\n            padding: const EdgeInsets.symmetric(\n              horizontal: 8.0,\n              vertical: 12.0,\n            ),\n            child: child,\n          );\n        }\n        return child;\n\n      case UploadImageType.url:\n        return Container(\n          padding: const EdgeInsets.all(8.0),\n          constraints: constraints,\n          child: EmbedImageUrlWidget(\n            onSubmit: widget.onSelectedNetworkImage,\n          ),\n        );\n      case UploadImageType.unsplash:\n        return Expanded(\n          child: Padding(\n            padding: const EdgeInsets.all(8.0),\n            child: UnsplashImageWidget(\n              onSelectUnsplashImage: widget.onSelectedNetworkImage,\n            ),\n          ),\n        );\n      case UploadImageType.color:\n        final theme = Theme.of(context);\n        final padding = UniversalPlatform.isMobile\n            ? const EdgeInsets.all(16.0)\n            : const EdgeInsets.all(8.0);\n        return Container(\n          constraints: constraints,\n          padding: padding,\n          alignment: Alignment.center,\n          child: CoverColorPicker(\n            pickerBackgroundColor: theme.cardColor,\n            pickerItemHoverColor: theme.hoverColor,\n            backgroundColorOptions: FlowyTint.values\n                .map<ColorOption>(\n                  (t) => ColorOption(\n                    colorHex: t.color(context).toHex(),\n                    name: t.tintName(AppFlowyEditorL10n.current),\n                  ),\n                )\n                .toList(),\n            onSubmittedBackgroundColorHex: (color) {\n              widget.onSelectedColor?.call(color);\n            },\n          ),\n        );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass EmbedImageUrlWidget extends StatefulWidget {\n  const EmbedImageUrlWidget({\n    super.key,\n    required this.onSubmit,\n  });\n\n  final void Function(String url) onSubmit;\n\n  @override\n  State<EmbedImageUrlWidget> createState() => _EmbedImageUrlWidgetState();\n}\n\nclass _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> {\n  bool isUrlValid = true;\n  String inputText = '';\n\n  @override\n  Widget build(BuildContext context) {\n    final textField = FlowyTextField(\n      hintText: LocaleKeys.document_imageBlock_embedLink_placeholder.tr(),\n      onChanged: (value) => inputText = value,\n      onEditingComplete: submit,\n      textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(\n            fontSize: 14,\n          ),\n      hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(\n            color: Theme.of(context).hintColor,\n            fontSize: 14,\n          ),\n    );\n    return Column(\n      children: [\n        const VSpace(12),\n        UniversalPlatform.isDesktop\n            ? textField\n            : SizedBox(\n                height: 42,\n                child: textField,\n              ),\n        if (!isUrlValid) ...[\n          const VSpace(12),\n          FlowyText(\n            LocaleKeys.document_plugins_cover_invalidImageUrl.tr(),\n            color: Theme.of(context).colorScheme.error,\n          ),\n        ],\n        const VSpace(20),\n        SizedBox(\n          height: UniversalPlatform.isMobile ? 36 : 32,\n          width: 300,\n          child: FlowyButton(\n            backgroundColor: Theme.of(context).colorScheme.primary,\n            hoverColor:\n                Theme.of(context).colorScheme.primary.withValues(alpha: 0.9),\n            showDefaultBoxDecorationOnMobile: true,\n            radius:\n                UniversalPlatform.isMobile ? BorderRadius.circular(8) : null,\n            margin: const EdgeInsets.all(5),\n            text: FlowyText(\n              LocaleKeys.document_imageBlock_embedLink_label.tr(),\n              lineHeight: 1,\n              textAlign: TextAlign.center,\n              color: UniversalPlatform.isMobile\n                  ? null\n                  : Theme.of(context).colorScheme.onPrimary,\n              fontSize: UniversalPlatform.isMobile ? 14 : null,\n            ),\n            onTap: submit,\n          ),\n        ),\n        const VSpace(8),\n      ],\n    );\n  }\n\n  void submit() {\n    if (checkUrlValidity(inputText)) {\n      return widget.onSubmit(inputText);\n    }\n\n    setState(() => isUrlValid = false);\n  }\n\n  bool checkUrlValidity(String url) => imgUrlRegex.hasMatch(url);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/upload_image_file_widget.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/permission/permission_checker.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/default_extensions.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/file_picker/file_picker_service.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:image_picker/image_picker.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass UploadImageFileWidget extends StatelessWidget {\n  const UploadImageFileWidget({\n    super.key,\n    required this.onPickFiles,\n    this.allowedExtensions = defaultImageExtensions,\n    this.allowMultipleImages = false,\n  });\n\n  final void Function(List<XFile>) onPickFiles;\n  final List<String> allowedExtensions;\n  final bool allowMultipleImages;\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child = FlowyButton(\n      showDefaultBoxDecorationOnMobile: true,\n      radius: UniversalPlatform.isMobile ? BorderRadius.circular(8.0) : null,\n      text: Container(\n        margin: const EdgeInsets.all(4.0),\n        alignment: Alignment.center,\n        child: FlowyText(\n          LocaleKeys.document_imageBlock_upload_placeholder.tr(),\n        ),\n      ),\n      onTap: () => _uploadImage(context),\n    );\n\n    if (UniversalPlatform.isDesktopOrWeb) {\n      child = FlowyHover(child: child);\n    } else {\n      child = Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 8.0),\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  Future<void> _uploadImage(BuildContext context) async {\n    if (UniversalPlatform.isDesktopOrWeb) {\n      // on desktop, the users can pick a image file from folder\n      final result = await getIt<FilePickerService>().pickFiles(\n        dialogTitle: '',\n        type: FileType.custom,\n        allowedExtensions: allowedExtensions,\n        allowMultiple: allowMultipleImages,\n      );\n      onPickFiles(result?.files.map((f) => f.xFile).toList() ?? const []);\n    } else {\n      final photoPermission =\n          await PermissionChecker.checkPhotoPermission(context);\n      if (!photoPermission) {\n        Log.error('Has no permission to access the photo library');\n        return;\n      }\n      // on mobile, the users can pick a image file from camera or image library\n      final result = await ImagePicker().pickMultiImage();\n      onPickFiles(result);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/text_input.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_math_fork/flutter_math.dart';\nimport 'package:provider/provider.dart';\n\nclass InlineMathEquationKeys {\n  const InlineMathEquationKeys._();\n\n  static const formula = 'formula';\n}\n\nclass InlineMathEquation extends StatefulWidget {\n  const InlineMathEquation({\n    super.key,\n    required this.formula,\n    required this.node,\n    required this.index,\n    this.textStyle,\n  });\n\n  final Node node;\n  final int index;\n  final String formula;\n  final TextStyle? textStyle;\n\n  @override\n  State<InlineMathEquation> createState() => _InlineMathEquationState();\n}\n\nclass _InlineMathEquationState extends State<InlineMathEquation> {\n  final popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return _IgnoreParentPointer(\n      child: AppFlowyPopover(\n        controller: popoverController,\n        direction: PopoverDirection.bottomWithLeftAligned,\n        popupBuilder: (_) {\n          return MathInputTextField(\n            initialText: widget.formula,\n            onSubmit: (value) async {\n              popoverController.close();\n              if (value == widget.formula) {\n                return;\n              }\n              final editorState = context.read<EditorState>();\n              final transaction = editorState.transaction\n                ..formatText(widget.node, widget.index, 1, {\n                  InlineMathEquationKeys.formula: value,\n                });\n              await editorState.apply(transaction);\n            },\n          );\n        },\n        offset: const Offset(0, 10),\n        child: Padding(\n          padding: const EdgeInsets.symmetric(vertical: 2.0),\n          child: MouseRegion(\n            cursor: SystemMouseCursors.click,\n            child: _buildMathEquation(context),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildMathEquation(BuildContext context) {\n    final theme = Theme.of(context);\n    final longEq = Math.tex(\n      widget.formula,\n      textStyle: widget.textStyle,\n      mathStyle: MathStyle.text,\n      options: MathOptions(\n        style: MathStyle.text,\n        mathFontOptions: const FontOptions(\n          fontShape: FontStyle.italic,\n        ),\n        fontSize: widget.textStyle?.fontSize ?? 14.0,\n        color: widget.textStyle?.color ?? theme.colorScheme.onSurface,\n      ),\n      onErrorFallback: (errmsg) {\n        return FlowyText(\n          errmsg.message,\n          fontSize: widget.textStyle?.fontSize ?? 14.0,\n          color: widget.textStyle?.color ?? theme.colorScheme.onSurface,\n        );\n      },\n    );\n    return longEq;\n  }\n}\n\nclass MathInputTextField extends StatefulWidget {\n  const MathInputTextField({\n    super.key,\n    required this.initialText,\n    required this.onSubmit,\n  });\n\n  final String initialText;\n  final void Function(String value) onSubmit;\n\n  @override\n  State<MathInputTextField> createState() => _MathInputTextFieldState();\n}\n\nclass _MathInputTextFieldState extends State<MathInputTextField> {\n  late final TextEditingController textEditingController;\n\n  @override\n  void initState() {\n    super.initState();\n\n    textEditingController = TextEditingController(\n      text: widget.initialText,\n    );\n    textEditingController.selection = TextSelection(\n      baseOffset: 0,\n      extentOffset: widget.initialText.length,\n    );\n  }\n\n  @override\n  void dispose() {\n    textEditingController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: 240,\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Expanded(\n            child: FlowyFormTextInput(\n              autoFocus: true,\n              textAlign: TextAlign.left,\n              controller: textEditingController,\n              contentPadding: const EdgeInsets.symmetric(\n                vertical: 8.0,\n                horizontal: 4.0,\n              ),\n              onEditingComplete: () =>\n                  widget.onSubmit(textEditingController.text),\n            ),\n          ),\n          const HSpace(4.0),\n          FlowyButton(\n            text: FlowyText(LocaleKeys.button_done.tr()),\n            useIntrinsicWidth: true,\n            onTap: () => widget.onSubmit(textEditingController.text),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _IgnoreParentPointer extends StatelessWidget {\n  const _IgnoreParentPointer({\n    required this.child,\n  });\n\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: () {},\n      onTapDown: (_) {},\n      onDoubleTap: () {},\n      onLongPress: () {},\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nconst _kInlineMathEquationToolbarItemId = 'editor.inline_math_equation';\n\nfinal ToolbarItem inlineMathEquationItem = ToolbarItem(\n  id: _kInlineMathEquationToolbarItemId,\n  group: 4,\n  isActive: onlyShowInSingleSelectionAndTextType,\n  builder: (context, editorState, highlightColor, _, tooltipBuilder) {\n    final selection = editorState.selection!;\n    final nodes = editorState.getNodesInSelection(selection);\n    final isHighlight = nodes.allSatisfyInSelection(selection, (delta) {\n      return delta.everyAttributes(\n        (attributes) => attributes[InlineMathEquationKeys.formula] != null,\n      );\n    });\n    final child = SVGIconItemWidget(\n      iconBuilder: (_) => FlowySvg(\n        FlowySvgs.math_lg,\n        size: const Size.square(16),\n        color: isHighlight ? highlightColor : Colors.white,\n      ),\n      isHighlight: isHighlight,\n      highlightColor: highlightColor,\n      onPressed: () async {\n        final selection = editorState.selection;\n        if (selection == null || selection.isCollapsed) {\n          return;\n        }\n        final node = editorState.getNodeAtPath(selection.start.path);\n        final delta = node?.delta;\n        if (node == null || delta == null) {\n          return;\n        }\n\n        final transaction = editorState.transaction;\n        if (isHighlight) {\n          final formula = delta\n              .slice(selection.startIndex, selection.endIndex)\n              .whereType<TextInsert>()\n              .firstOrNull\n              ?.attributes?[InlineMathEquationKeys.formula];\n          assert(formula != null);\n          if (formula == null) {\n            return;\n          }\n          // clear the format\n          transaction.replaceText(\n            node,\n            selection.startIndex,\n            selection.length,\n            formula,\n            attributes: {},\n          );\n        } else {\n          final text = editorState.getTextInSelection(selection).join();\n          transaction.replaceText(\n            node,\n            selection.startIndex,\n            selection.length,\n            MentionBlockKeys.mentionChar,\n            attributes: {\n              InlineMathEquationKeys.formula: text,\n            },\n          );\n        }\n        await editorState.apply(transaction);\n      },\n    );\n\n    if (tooltipBuilder != null) {\n      return tooltipBuilder(\n        context,\n        _kInlineMathEquationToolbarItemId,\n        LocaleKeys.document_plugins_createInlineMathEquation.tr(),\n        child,\n      );\n    }\n\n    return child;\n  },\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/keyboard_interceptor/keyboard_interceptor.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass EditorKeyboardInterceptor extends AppFlowyKeyboardServiceInterceptor {\n  @override\n  Future<bool> interceptInsert(\n    TextEditingDeltaInsertion insertion,\n    EditorState editorState,\n    List<CharacterShortcutEvent> characterShortcutEvents,\n  ) async {\n    // Only check on the mobile platform: check if the inserted text is a link, if so, try to paste it as a link preview\n    final text = insertion.textInserted;\n    if (UniversalPlatform.isMobile && hrefRegex.hasMatch(text)) {\n      final result = customPasteCommand.execute(editorState);\n      return result == KeyEventResult.handled;\n    }\n    return false;\n  }\n\n  @override\n  Future<bool> interceptReplace(\n    TextEditingDeltaReplacement replacement,\n    EditorState editorState,\n    List<CharacterShortcutEvent> characterShortcutEvents,\n  ) async {\n    // Only check on the mobile platform: check if the replaced text is a link, if so, try to paste it as a link preview\n    final text = replacement.replacementText;\n    if (UniversalPlatform.isMobile && hrefRegex.hasMatch(text)) {\n      final result = customPasteCommand.execute(editorState);\n      return result == KeyEventResult.handled;\n    }\n    return false;\n  }\n\n  @override\n  Future<bool> interceptNonTextUpdate(\n    TextEditingDeltaNonTextUpdate nonTextUpdate,\n    EditorState editorState,\n    List<CharacterShortcutEvent> characterShortcutEvents,\n  ) async {\n    return _checkIfBacktickPressed(\n      editorState,\n      nonTextUpdate,\n    );\n  }\n\n  @override\n  Future<bool> interceptDelete(\n    TextEditingDeltaDeletion deletion,\n    EditorState editorState,\n  ) async {\n    // check if the current selection is in a code block\n    final (isInTableCell, selection, tableCellNode, node) =\n        editorState.isCurrentSelectionInTableCell();\n    if (!isInTableCell ||\n        selection == null ||\n        tableCellNode == null ||\n        node == null) {\n      return false;\n    }\n\n    final onlyContainsOneChild = tableCellNode.children.length == 1;\n    final isParagraphNode =\n        tableCellNode.children.first.type == ParagraphBlockKeys.type;\n    if (onlyContainsOneChild &&\n        selection.isCollapsed &&\n        selection.end.offset == 0 &&\n        isParagraphNode) {\n      return true;\n    }\n\n    return false;\n  }\n\n  /// Check if the backtick pressed event should be handled\n  Future<bool> _checkIfBacktickPressed(\n    EditorState editorState,\n    TextEditingDeltaNonTextUpdate nonTextUpdate,\n  ) async {\n    // if the composing range is not empty, it means the user is typing a text,\n    // so we don't need to handle the backtick pressed event\n    if (!nonTextUpdate.composing.isCollapsed ||\n        !nonTextUpdate.selection.isCollapsed) {\n      return false;\n    }\n\n    final selection = editorState.selection;\n    if (selection == null || !selection.isCollapsed) {\n      AppFlowyEditorLog.input.debug('selection is null or not collapsed');\n      return false;\n    }\n\n    final node = editorState.getNodesInSelection(selection).firstOrNull;\n    if (node == null) {\n      AppFlowyEditorLog.input.debug('node is null');\n      return false;\n    }\n\n    // get last character of the node\n    final plainText = node.delta?.toPlainText();\n    // three backticks to code block\n    if (plainText != '```') {\n      return false;\n    }\n\n    final transaction = editorState.transaction;\n    transaction.insertNode(\n      selection.end.path,\n      codeBlockNode(),\n    );\n    transaction.deleteNode(node);\n    transaction.afterSelection = Selection.collapsed(\n      Position(path: selection.start.path),\n    );\n    await editorState.apply(transaction);\n\n    return true;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/default_selectable_mixin.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'link_embed_menu.dart';\n\nclass LinkEmbedKeys {\n  const LinkEmbedKeys._();\n  static const String previewType = 'preview_type';\n  static const String embed = 'embed';\n  static const String align = 'align';\n}\n\nNode linkEmbedNode({required String url}) => Node(\n      type: LinkPreviewBlockKeys.type,\n      attributes: {\n        LinkPreviewBlockKeys.url: url,\n        LinkEmbedKeys.previewType: LinkEmbedKeys.embed,\n      },\n    );\n\nclass LinkEmbedBlockComponent extends BlockComponentStatefulWidget {\n  const LinkEmbedBlockComponent({\n    super.key,\n    super.showActions,\n    super.actionBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n    required super.node,\n  });\n\n  @override\n  DefaultSelectableMixinState<LinkEmbedBlockComponent> createState() =>\n      LinkEmbedBlockComponentState();\n}\n\nclass LinkEmbedBlockComponentState\n    extends DefaultSelectableMixinState<LinkEmbedBlockComponent>\n    with BlockComponentConfigurable {\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  @override\n  Node get node => widget.node;\n\n  String get url => widget.node.attributes[LinkPreviewBlockKeys.url] ?? '';\n\n  LinkLoadingStatus status = LinkLoadingStatus.loading;\n  final parser = LinkParser();\n  late LinkInfo linkInfo = LinkInfo(url: url);\n\n  final showActionsNotifier = ValueNotifier<bool>(false);\n  bool isMenuShowing = false, isHovering = false;\n\n  @override\n  void initState() {\n    super.initState();\n    parser.addLinkInfoListener((v) {\n      final hasNewInfo = !v.isEmpty(), hasOldInfo = !linkInfo.isEmpty();\n      if (mounted) {\n        setState(() {\n          if (hasNewInfo) {\n            linkInfo = v;\n            status = LinkLoadingStatus.idle;\n          } else if (!hasOldInfo) {\n            status = LinkLoadingStatus.error;\n          }\n        });\n      }\n    });\n    parser.start(url);\n  }\n\n  @override\n  void dispose() {\n    parser.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    Widget result = MouseRegion(\n      onEnter: (_) {\n        isHovering = true;\n        showActionsNotifier.value = true;\n      },\n      onExit: (_) {\n        isHovering = false;\n        Future.delayed(const Duration(milliseconds: 200), () {\n          if (isMenuShowing || isHovering) return;\n          if (mounted) showActionsNotifier.value = false;\n        });\n      },\n      child: buildChild(context),\n    );\n    final parent = node.parent;\n    EdgeInsets newPadding = padding;\n    if (parent?.type == CalloutBlockKeys.type) {\n      newPadding = padding.copyWith(right: padding.right + 10);\n    }\n\n    result = Padding(padding: newPadding, child: result);\n\n    if (widget.showActions && widget.actionBuilder != null) {\n      result = BlockComponentActionWrapper(\n        node: node,\n        actionBuilder: widget.actionBuilder!,\n        child: result,\n      );\n    }\n    return result;\n  }\n\n  Widget buildChild(BuildContext context) {\n    final theme = AppFlowyTheme.of(context),\n        fillScheme = theme.fillColorScheme,\n        borderScheme = theme.borderColorScheme;\n    Widget child;\n    final isIdle = status == LinkLoadingStatus.idle;\n    if (isIdle) {\n      child = buildContent(context);\n    } else {\n      child = buildErrorLoadingWidget(context);\n    }\n    return Container(\n      height: 450,\n      key: widgetKey,\n      decoration: BoxDecoration(\n        color: fillScheme.content,\n        borderRadius: BorderRadius.all(Radius.circular(16)),\n        border: Border.all(color: borderScheme.primary),\n      ),\n      child: Stack(\n        children: [\n          child,\n          buildMenu(context),\n        ],\n      ),\n    );\n  }\n\n  Widget buildMenu(BuildContext context) {\n    return Positioned(\n      top: 12,\n      right: 12,\n      child: ValueListenableBuilder<bool>(\n        valueListenable: showActionsNotifier,\n        builder: (context, showActions, child) {\n          if (!showActions || UniversalPlatform.isMobile) {\n            return SizedBox.shrink();\n          }\n          return LinkEmbedMenu(\n            editorState: context.read<EditorState>(),\n            node: node,\n            onReload: () {\n              setState(() {\n                status = LinkLoadingStatus.loading;\n              });\n              Future.delayed(const Duration(milliseconds: 200), () {\n                if (mounted) parser.start(url);\n              });\n            },\n            onMenuShowed: () {\n              isMenuShowing = true;\n            },\n            onMenuHided: () {\n              isMenuShowing = false;\n              if (!isHovering && mounted) {\n                showActionsNotifier.value = false;\n              }\n            },\n          );\n        },\n      ),\n    );\n  }\n\n  Widget buildContent(BuildContext context) {\n    final theme = AppFlowyTheme.of(context), textScheme = theme.textColorScheme;\n    final hasSiteName = linkInfo.siteName?.isNotEmpty ?? false;\n    return MouseRegion(\n      cursor: SystemMouseCursors.click,\n      child: GestureDetector(\n        behavior: HitTestBehavior.opaque,\n        onTap: () => afLaunchUrlString(url, addingHttpSchemeWhenFailed: true),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Expanded(\n              child: ClipRRect(\n                borderRadius:\n                    const BorderRadius.vertical(top: Radius.circular(16)),\n                child: FlowyNetworkImage(\n                  url: linkInfo.imageUrl ?? '',\n                  width: MediaQuery.of(context).size.width,\n                ),\n              ),\n            ),\n            Container(\n              height: 64,\n              padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20),\n              child: Row(\n                children: [\n                  SizedBox.square(\n                    dimension: 40,\n                    child: Center(\n                      child: linkInfo.buildIconWidget(size: Size.square(32)),\n                    ),\n                  ),\n                  HSpace(12),\n                  Expanded(\n                    child: Column(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      mainAxisSize: MainAxisSize.min,\n                      children: [\n                        if (hasSiteName) ...[\n                          FlowyText(\n                            linkInfo.siteName ?? '',\n                            color: textScheme.primary,\n                            fontSize: 14,\n                            figmaLineHeight: 20,\n                            fontWeight: FontWeight.w600,\n                            overflow: TextOverflow.ellipsis,\n                          ),\n                          VSpace(4),\n                        ],\n                        FlowyText.regular(\n                          url,\n                          color: textScheme.secondary,\n                          fontSize: 12,\n                          figmaLineHeight: 16,\n                          overflow: TextOverflow.ellipsis,\n                        ),\n                      ],\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget buildErrorLoadingWidget(BuildContext context) {\n    final theme = AppFlowyTheme.of(context), textScheme = theme.textColorScheme;\n    final isLoading = status == LinkLoadingStatus.loading;\n    return isLoading\n        ? Center(\n            child: SizedBox.square(\n              dimension: 64,\n              child: CircularProgressIndicator.adaptive(),\n            ),\n          )\n        : GestureDetector(\n            behavior: HitTestBehavior.opaque,\n            onTap: !UniversalPlatform.isMobile\n                ? null\n                : () =>\n                    afLaunchUrlString(url, addingHttpSchemeWhenFailed: true),\n            child: Center(\n              child: Column(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  SvgPicture.asset(\n                    FlowySvgs.embed_error_xl.path,\n                  ),\n                  VSpace(4),\n                  Padding(\n                    padding: const EdgeInsets.symmetric(horizontal: 24),\n                    child: RichText(\n                      maxLines: 1,\n                      overflow: TextOverflow.ellipsis,\n                      text: TextSpan(\n                        children: [\n                          TextSpan(\n                            text: '$url ',\n                            style: TextStyle(\n                              color: textScheme.secondary,\n                              fontSize: 14,\n                              height: 20 / 14,\n                              fontWeight: FontWeight.w700,\n                            ),\n                          ),\n                          TextSpan(\n                            text: LocaleKeys\n                                .document_plugins_linkPreview_linkPreviewMenu_unableToDisplay\n                                .tr(),\n                            style: TextStyle(\n                              color: textScheme.secondary,\n                              fontSize: 14,\n                              height: 20 / 14,\n                              fontWeight: FontWeight.w400,\n                            ),\n                          ),\n                        ],\n                      ),\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          );\n  }\n\n  @override\n  Node get currentNode => node;\n\n  @override\n  EdgeInsets get boxPadding => padding;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/widgets.dart';\n\nimport 'link_embed_block_component.dart';\n\nclass LinkEmbedMenu extends StatefulWidget {\n  const LinkEmbedMenu({\n    super.key,\n    required this.node,\n    required this.editorState,\n    required this.onMenuShowed,\n    required this.onMenuHided,\n    required this.onReload,\n  });\n\n  final Node node;\n  final EditorState editorState;\n  final VoidCallback onMenuShowed;\n  final VoidCallback onMenuHided;\n  final VoidCallback onReload;\n\n  @override\n  State<LinkEmbedMenu> createState() => _LinkEmbedMenuState();\n}\n\nclass _LinkEmbedMenuState extends State<LinkEmbedMenu> {\n  final turnIntoController = PopoverController();\n  final moreOptionController = PopoverController();\n  int turnIntoMenuNum = 0, moreOptionNum = 0, alignMenuNum = 0;\n  final moreOptionButtonKey = GlobalKey();\n  bool get isTurnIntoShowing => turnIntoMenuNum > 0;\n  bool get isMoreOptionShowing => moreOptionNum > 0;\n  bool get isAlignMenuShowing => alignMenuNum > 0;\n\n  Node get node => widget.node;\n  EditorState get editorState => widget.editorState;\n  bool get editable => editorState.editable;\n\n  String get url => node.attributes[LinkPreviewBlockKeys.url] ?? '';\n\n  @override\n  void dispose() {\n    super.dispose();\n    turnIntoController.close();\n    moreOptionController.close();\n    widget.onMenuHided.call();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return buildChild();\n  }\n\n  Widget buildChild() {\n    final theme = AppFlowyTheme.of(context),\n        iconScheme = theme.iconColorScheme,\n        surfaceColorScheme = theme.surfaceColorScheme;\n\n    return Container(\n      padding: EdgeInsets.all(4),\n      decoration: BoxDecoration(\n        borderRadius: BorderRadius.circular(12),\n        color: surfaceColorScheme.inverse,\n      ),\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          // FlowyIconButton(\n          //   icon: FlowySvg(\n          //     FlowySvgs.embed_fullscreen_m,\n          //     color: iconScheme.tertiary,\n          //   ),\n          //   tooltipText: LocaleKeys.document_imageBlock_openFullScreen.tr(),\n          //   preferBelow: false,\n          //   onPressed: () {},\n          // ),\n          FlowyIconButton(\n            icon: FlowySvg(\n              FlowySvgs.toolbar_link_m,\n              color: iconScheme.tertiary,\n            ),\n            radius: BorderRadius.all(Radius.circular(theme.borderRadius.m)),\n            tooltipText: LocaleKeys.editor_copyLink.tr(),\n            preferBelow: false,\n            onPressed: () => copyLink(context),\n          ),\n          buildConvertButton(),\n          buildMoreOptionButton(),\n        ],\n      ),\n    );\n  }\n\n  Widget buildConvertButton() {\n    final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme;\n    final button = FlowyIconButton(\n      icon: FlowySvg(\n        FlowySvgs.turninto_m,\n        color: iconScheme.tertiary,\n      ),\n      radius: BorderRadius.all(Radius.circular(theme.borderRadius.m)),\n      tooltipText: LocaleKeys.editor_convertTo.tr(),\n      preferBelow: false,\n      onPressed: getTapCallback(showTurnIntoMenu),\n    );\n    if (!editable) return button;\n    return AppFlowyPopover(\n      offset: Offset(0, 6),\n      direction: PopoverDirection.bottomWithRightAligned,\n      margin: EdgeInsets.zero,\n      controller: turnIntoController,\n      onOpen: () {\n        keepEditorFocusNotifier.increase();\n        turnIntoMenuNum++;\n      },\n      onClose: () {\n        keepEditorFocusNotifier.decrease();\n        turnIntoMenuNum--;\n        checkToHideMenu();\n      },\n      popupBuilder: (context) => buildConvertMenu(),\n      child: button,\n    );\n  }\n\n  Widget buildConvertMenu() {\n    final types = LinkEmbedConvertCommand.values;\n    return Padding(\n      padding: const EdgeInsets.all(8.0),\n      child: SeparatedColumn(\n        mainAxisSize: MainAxisSize.min,\n        separatorBuilder: () => const VSpace(0.0),\n        children: List.generate(types.length, (index) {\n          final command = types[index];\n          return SizedBox(\n            height: 36,\n            child: FlowyButton(\n              text: FlowyText(\n                command.title,\n                fontWeight: FontWeight.w400,\n                figmaLineHeight: 20,\n              ),\n              onTap: () {\n                if (command == LinkEmbedConvertCommand.toBookmark) {\n                  final transaction = editorState.transaction;\n                  transaction.updateNode(node, {\n                    LinkPreviewBlockKeys.url: url,\n                    LinkEmbedKeys.previewType: '',\n                  });\n                  editorState.apply(transaction);\n                } else if (command == LinkEmbedConvertCommand.toMention) {\n                  convertUrlPreviewNodeToMention(editorState, node);\n                } else if (command == LinkEmbedConvertCommand.toURL) {\n                  convertUrlPreviewNodeToLink(editorState, node);\n                }\n              },\n            ),\n          );\n        }),\n      ),\n    );\n  }\n\n  Widget buildMoreOptionButton() {\n    final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme;\n    final button = FlowyIconButton(\n      key: moreOptionButtonKey,\n      icon: FlowySvg(\n        FlowySvgs.toolbar_more_m,\n        color: iconScheme.tertiary,\n      ),\n      radius: BorderRadius.all(Radius.circular(theme.borderRadius.m)),\n      tooltipText: LocaleKeys.document_toolbar_moreOptions.tr(),\n      preferBelow: false,\n      onPressed: getTapCallback(showMoreOptionMenu),\n    );\n    if (!editable) return button;\n    return AppFlowyPopover(\n      offset: Offset(0, 6),\n      direction: PopoverDirection.bottomWithRightAligned,\n      margin: EdgeInsets.zero,\n      controller: moreOptionController,\n      onOpen: () {\n        keepEditorFocusNotifier.increase();\n        moreOptionNum++;\n      },\n      onClose: () {\n        keepEditorFocusNotifier.decrease();\n        moreOptionNum--;\n        checkToHideMenu();\n      },\n      popupBuilder: (context) => buildMoreOptionMenu(),\n      child: button,\n    );\n  }\n\n  Widget buildMoreOptionMenu() {\n    final types = LinkEmbedMenuCommand.values;\n    return Padding(\n      padding: const EdgeInsets.all(8.0),\n      child: SeparatedColumn(\n        mainAxisSize: MainAxisSize.min,\n        separatorBuilder: () => const VSpace(0.0),\n        children: List.generate(types.length, (index) {\n          final command = types[index];\n          return SizedBox(\n            height: 36,\n            child: FlowyButton(\n              text: FlowyText(\n                command.title,\n                fontWeight: FontWeight.w400,\n                figmaLineHeight: 20,\n              ),\n              onTap: () => onEmbedMenuCommand(command),\n            ),\n          );\n        }),\n      ),\n    );\n  }\n\n  void showTurnIntoMenu() {\n    keepEditorFocusNotifier.increase();\n    turnIntoController.show();\n    checkToShowMenu();\n    turnIntoMenuNum++;\n    if (isMoreOptionShowing) closeMoreOptionMenu();\n  }\n\n  void closeTurnIntoMenu() {\n    turnIntoController.close();\n    checkToHideMenu();\n  }\n\n  void showMoreOptionMenu() {\n    keepEditorFocusNotifier.increase();\n    moreOptionController.show();\n    checkToShowMenu();\n    moreOptionNum++;\n    if (isTurnIntoShowing) closeTurnIntoMenu();\n  }\n\n  void closeMoreOptionMenu() {\n    moreOptionController.close();\n    checkToHideMenu();\n  }\n\n  void checkToHideMenu() {\n    Future.delayed(Duration(milliseconds: 200), () {\n      if (!mounted) return;\n      if (!isAlignMenuShowing && !isMoreOptionShowing && !isTurnIntoShowing) {\n        widget.onMenuHided.call();\n      }\n    });\n  }\n\n  void checkToShowMenu() {\n    if (!isAlignMenuShowing && !isMoreOptionShowing && !isTurnIntoShowing) {\n      widget.onMenuShowed.call();\n    }\n  }\n\n  Future<void> copyLink(BuildContext context) async {\n    await context.copyLink(url);\n    widget.onMenuHided.call();\n  }\n\n  void onEmbedMenuCommand(LinkEmbedMenuCommand command) {\n    switch (command) {\n      case LinkEmbedMenuCommand.openLink:\n        afLaunchUrlString(url, addingHttpSchemeWhenFailed: true);\n        break;\n      case LinkEmbedMenuCommand.replace:\n        final box = moreOptionButtonKey.currentContext?.findRenderObject()\n            as RenderBox?;\n        if (box == null) return;\n        final p = box.localToGlobal(Offset.zero);\n        showReplaceMenu(\n          context: context,\n          editorState: editorState,\n          node: node,\n          url: url,\n          ltrb: LTRB(left: p.dx - 330, top: p.dy),\n          onReplace: (url) async {\n            await convertLinkBlockToOtherLinkBlock(\n              editorState,\n              node,\n              node.type,\n              url: url,\n            );\n          },\n        );\n        break;\n      case LinkEmbedMenuCommand.reload:\n        widget.onReload.call();\n        break;\n      case LinkEmbedMenuCommand.removeLink:\n        removeUrlPreviewLink(editorState, node);\n        break;\n    }\n    closeMoreOptionMenu();\n  }\n\n  VoidCallback? getTapCallback(VoidCallback callback) {\n    if (editable) return callback;\n    return null;\n  }\n}\n\nenum LinkEmbedMenuCommand {\n  openLink,\n  replace,\n  reload,\n  removeLink;\n\n  String get title {\n    switch (this) {\n      case openLink:\n        return LocaleKeys.editor_openLink.tr();\n      case replace:\n        return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_replace\n            .tr();\n      case reload:\n        return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_reload\n            .tr();\n      case removeLink:\n        return LocaleKeys\n            .document_plugins_linkPreview_linkPreviewMenu_removeLink\n            .tr();\n    }\n  }\n}\n\nenum LinkEmbedConvertCommand {\n  toMention,\n  toURL,\n  toBookmark;\n\n  String get title {\n    switch (this) {\n      case toMention:\n        return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toMetion\n            .tr();\n      case toURL:\n        return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl\n            .tr();\n      case toBookmark:\n        return LocaleKeys\n            .document_plugins_linkPreview_linkPreviewMenu_toBookmark\n            .tr();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart",
    "content": "import 'dart:convert';\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy/shared/appflowy_network_svg.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:flutter/material.dart';\nimport 'link_parsers/default_parser.dart';\nimport 'link_parsers/youtube_parser.dart';\n\nclass LinkParser {\n  final Set<ValueChanged<LinkInfo>> _listeners = <ValueChanged<LinkInfo>>{};\n  static final Map<String, LinkInfoParser> _hostToParsers = {\n    'www.youtube.com': YoutubeParser(),\n    'youtube.com': YoutubeParser(),\n    'youtu.be': YoutubeParser(),\n  };\n\n  Future<void> start(String url, {LinkInfoParser? parser}) async {\n    final uri = Uri.tryParse(LinkInfoParser.formatUrl(url)) ?? Uri.parse(url);\n    final data = await LinkInfoCache.get(uri);\n    if (data != null) {\n      refreshLinkInfo(data);\n    }\n\n    final host = uri.host;\n    final currentParser = parser ?? _hostToParsers[host] ?? DefaultParser();\n    await _getLinkInfo(uri, currentParser);\n  }\n\n  Future<LinkInfo?> _getLinkInfo(Uri uri, LinkInfoParser parser) async {\n    try {\n      final linkInfo = await parser.parse(uri) ?? LinkInfo(url: '$uri');\n      if (!linkInfo.isEmpty()) await LinkInfoCache.set(uri, linkInfo);\n      refreshLinkInfo(linkInfo);\n      return linkInfo;\n    } catch (e, s) {\n      Log.error('get link info error: ', e, s);\n      refreshLinkInfo(LinkInfo(url: '$uri'));\n      return null;\n    }\n  }\n\n  void refreshLinkInfo(LinkInfo info) {\n    for (final listener in _listeners) {\n      listener(info);\n    }\n  }\n\n  void addLinkInfoListener(ValueChanged<LinkInfo> listener) {\n    _listeners.add(listener);\n  }\n\n  void dispose() {\n    _listeners.clear();\n  }\n}\n\nclass LinkInfo {\n  factory LinkInfo.fromJson(Map<String, dynamic> json) => LinkInfo(\n        siteName: json['siteName'],\n        url: json['url'] ?? '',\n        title: json['title'],\n        description: json['description'],\n        imageUrl: json['imageUrl'],\n        faviconUrl: json['faviconUrl'],\n      );\n\n  LinkInfo({\n    required this.url,\n    this.siteName,\n    this.title,\n    this.description,\n    this.imageUrl,\n    this.faviconUrl,\n  });\n\n  final String url;\n  final String? siteName;\n  final String? title;\n  final String? description;\n  final String? imageUrl;\n  final String? faviconUrl;\n\n  Map<String, dynamic> toJson() => {\n        'url': url,\n        'siteName': siteName,\n        'title': title,\n        'description': description,\n        'imageUrl': imageUrl,\n        'faviconUrl': faviconUrl,\n      };\n\n  @override\n  String toString() {\n    return 'LinkInfo{url: $url, siteName: $siteName, title: $title, description: $description, imageUrl: $imageUrl, faviconUrl: $faviconUrl}';\n  }\n\n  bool isEmpty() {\n    return title == null;\n  }\n\n  Widget buildIconWidget({Size size = const Size.square(20.0)}) {\n    final iconUrl = faviconUrl;\n    if (iconUrl == null) {\n      return FlowySvg(FlowySvgs.toolbar_link_earth_m, size: size);\n    }\n    if (iconUrl.endsWith('.svg')) {\n      return FlowyNetworkSvg(\n        iconUrl,\n        height: size.height,\n        width: size.width,\n        errorWidget: const FlowySvg(FlowySvgs.toolbar_link_earth_m),\n      );\n    }\n    return FlowyNetworkImage(\n      url: iconUrl,\n      fit: BoxFit.contain,\n      height: size.height,\n      width: size.width,\n      errorWidgetBuilder: (context, error, stackTrace) =>\n          const FlowySvg(FlowySvgs.toolbar_link_earth_m),\n    );\n  }\n}\n\nclass LinkInfoCache {\n  static const _linkInfoPrefix = 'link_info';\n\n  static Future<LinkInfo?> get(Uri uri) async {\n    final option = await getIt<KeyValueStorage>().getWithFormat<LinkInfo?>(\n      '$_linkInfoPrefix$uri',\n      (value) => LinkInfo.fromJson(jsonDecode(value)),\n    );\n    return option;\n  }\n\n  static Future<void> set(Uri uri, LinkInfo data) async {\n    await getIt<KeyValueStorage>().set(\n      '$_linkInfoPrefix$uri',\n      jsonEncode(data.toJson()),\n    );\n  }\n}\n\nenum LinkLoadingStatus {\n  loading,\n  idle,\n  error,\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'custom_link_parser.dart';\n\nclass CustomLinkPreviewWidget extends StatelessWidget {\n  const CustomLinkPreviewWidget({\n    super.key,\n    required this.node,\n    required this.url,\n    this.title,\n    this.description,\n    this.imageUrl,\n    this.isHovering = false,\n    this.status = LinkLoadingStatus.loading,\n  });\n\n  final Node node;\n  final String? title;\n  final String? description;\n  final String? imageUrl;\n  final String url;\n  final bool isHovering;\n  final LinkLoadingStatus status;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context),\n        borderScheme = theme.borderColorScheme,\n        textScheme = theme.textColorScheme;\n    final documentFontSize = context\n            .read<EditorState>()\n            .editorStyle\n            .textStyleConfiguration\n            .text\n            .fontSize ??\n        16.0;\n    final isInDarkCallout = node.parent?.type == CalloutBlockKeys.type &&\n        !Theme.of(context).isLightMode;\n    final (fontSize, width) = UniversalPlatform.isDesktopOrWeb\n        ? (documentFontSize, 160.0)\n        : (documentFontSize - 2, 120.0);\n    final Widget child = Container(\n      clipBehavior: Clip.hardEdge,\n      decoration: BoxDecoration(\n        border: Border.all(\n          color: isHovering || isInDarkCallout\n              ? borderScheme.primaryHover\n              : borderScheme.primary,\n        ),\n        borderRadius: BorderRadius.circular(16.0),\n      ),\n      child: SizedBox(\n        height: 96,\n        child: Row(\n          crossAxisAlignment: CrossAxisAlignment.stretch,\n          children: [\n            buildImage(context),\n            Expanded(\n              child: Padding(\n                padding: const EdgeInsets.fromLTRB(20, 12, 58, 12),\n                child: status != LinkLoadingStatus.idle\n                    ? buildLoadingOrErrorWidget()\n                    : Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                        children: [\n                          if (title != null)\n                            Padding(\n                              padding: const EdgeInsets.only(bottom: 4.0),\n                              child: FlowyText.medium(\n                                title!,\n                                overflow: TextOverflow.ellipsis,\n                                fontSize: fontSize,\n                                color: textScheme.primary,\n                                figmaLineHeight: 20,\n                              ),\n                            ),\n                          if (description != null)\n                            Padding(\n                              padding: const EdgeInsets.only(bottom: 16.0),\n                              child: FlowyText(\n                                description!,\n                                overflow: TextOverflow.ellipsis,\n                                fontSize: fontSize - 4,\n                                figmaLineHeight: 16,\n                                color: textScheme.primary,\n                              ),\n                            ),\n                          FlowyText(\n                            url.toString(),\n                            overflow: TextOverflow.ellipsis,\n                            color: textScheme.secondary,\n                            fontSize: fontSize - 4,\n                            figmaLineHeight: 16,\n                          ),\n                        ],\n                      ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n\n    if (UniversalPlatform.isDesktopOrWeb) {\n      return MouseRegion(\n        cursor: SystemMouseCursors.click,\n        child: GestureDetector(\n          behavior: HitTestBehavior.opaque,\n          onTap: () => afLaunchUrlString(url, addingHttpSchemeWhenFailed: true),\n          child: child,\n        ),\n      );\n    }\n\n    return MobileBlockActionButtons(\n      node: node,\n      editorState: context.read<EditorState>(),\n      extendActionWidgets: _buildExtendActionWidgets(context),\n      child: GestureDetector(\n        behavior: HitTestBehavior.opaque,\n        onTap: () => afLaunchUrlString(url, addingHttpSchemeWhenFailed: true),\n        child: child,\n      ),\n    );\n  }\n\n  // only used on mobile platform\n  List<Widget> _buildExtendActionWidgets(BuildContext context) {\n    return [\n      FlowyOptionTile.text(\n        showTopBorder: false,\n        text: LocaleKeys.document_plugins_urlPreview_convertToLink.tr(),\n        leftIcon: const FlowySvg(\n          FlowySvgs.m_toolbar_link_m,\n          size: Size.square(18),\n        ),\n        onTap: () {\n          context.pop();\n          convertUrlPreviewNodeToLink(\n            context.read<EditorState>(),\n            node,\n          );\n        },\n      ),\n    ];\n  }\n\n  Widget buildImage(BuildContext context) {\n    if (imageUrl?.isEmpty ?? true) {\n      return SizedBox.shrink();\n    }\n    final theme = AppFlowyTheme.of(context),\n        fillScheme = theme.fillColorScheme,\n        iconScheme = theme.iconColorScheme;\n    final width = UniversalPlatform.isDesktopOrWeb ? 160.0 : 120.0;\n    return ClipRRect(\n      borderRadius: const BorderRadius.only(\n        topLeft: Radius.circular(16.0),\n        bottomLeft: Radius.circular(16.0),\n      ),\n      child: Container(\n        width: width,\n        color: fillScheme.quaternary,\n        child: FlowyNetworkImage(\n          url: imageUrl!,\n          width: width,\n          errorWidgetBuilder: (_, __, ___) => Center(\n            child: FlowySvg(\n              FlowySvgs.toolbar_link_earth_m,\n              color: iconScheme.secondary,\n              size: Size.square(30),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget buildLoadingOrErrorWidget() {\n    if (status == LinkLoadingStatus.loading) {\n      return const Center(\n        child: SizedBox(\n          height: 16,\n          width: 16,\n          child: CircularProgressIndicator.adaptive(),\n        ),\n      );\n    } else if (status == LinkLoadingStatus.error) {\n      return const Center(\n        child: SizedBox(\n          height: 16,\n          width: 16,\n          child: Icon(\n            Icons.error_outline,\n            color: Colors.red,\n          ),\n        ),\n      );\n    }\n    return SizedBox.shrink();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'custom_link_preview.dart';\nimport 'default_selectable_mixin.dart';\nimport 'link_preview_menu.dart';\n\nclass CustomLinkPreviewBlockComponentBuilder extends BlockComponentBuilder {\n  CustomLinkPreviewBlockComponentBuilder({\n    super.configuration,\n  });\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    final isEmbed =\n        node.attributes[LinkEmbedKeys.previewType] == LinkEmbedKeys.embed;\n    if (isEmbed) {\n      return LinkEmbedBlockComponent(\n        key: node.key,\n        node: node,\n        configuration: configuration,\n        showActions: showActions(node),\n        actionBuilder: (_, state) =>\n            actionBuilder(blockComponentContext, state),\n      );\n    }\n    return CustomLinkPreviewBlockComponent(\n      key: node.key,\n      node: node,\n      configuration: configuration,\n      showActions: showActions(node),\n      actionBuilder: (_, state) => actionBuilder(blockComponentContext, state),\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate =>\n      (node) => node.attributes[LinkPreviewBlockKeys.url]!.isNotEmpty;\n}\n\nclass CustomLinkPreviewBlockComponent extends BlockComponentStatefulWidget {\n  const CustomLinkPreviewBlockComponent({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n  });\n\n  @override\n  DefaultSelectableMixinState<CustomLinkPreviewBlockComponent> createState() =>\n      CustomLinkPreviewBlockComponentState();\n}\n\nclass CustomLinkPreviewBlockComponentState\n    extends DefaultSelectableMixinState<CustomLinkPreviewBlockComponent>\n    with BlockComponentConfigurable {\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  @override\n  Node get node => widget.node;\n\n  String get url => widget.node.attributes[LinkPreviewBlockKeys.url]!;\n\n  final parser = LinkParser();\n  LinkLoadingStatus status = LinkLoadingStatus.loading;\n  late LinkInfo linkInfo = LinkInfo(url: url);\n\n  final showActionsNotifier = ValueNotifier<bool>(false);\n  bool isMenuShowing = false, isHovering = false;\n\n  @override\n  void initState() {\n    super.initState();\n    parser.addLinkInfoListener((v) {\n      final hasNewInfo = !v.isEmpty(), hasOldInfo = !linkInfo.isEmpty();\n      if (mounted) {\n        setState(() {\n          if (hasNewInfo) {\n            linkInfo = v;\n            status = LinkLoadingStatus.idle;\n          } else if (!hasOldInfo) {\n            status = LinkLoadingStatus.error;\n          }\n        });\n      }\n    });\n    parser.start(url);\n  }\n\n  @override\n  void dispose() {\n    parser.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      onEnter: (_) {\n        isHovering = true;\n        showActionsNotifier.value = true;\n      },\n      onExit: (_) {\n        isHovering = false;\n        Future.delayed(const Duration(milliseconds: 200), () {\n          if (isMenuShowing || isHovering) return;\n          if (mounted) showActionsNotifier.value = false;\n        });\n      },\n      hitTestBehavior: HitTestBehavior.opaque,\n      opaque: false,\n      child: ValueListenableBuilder(\n        valueListenable: showActionsNotifier,\n        builder: (context, showActions, child) {\n          return buildPreview(showActions);\n        },\n      ),\n    );\n  }\n\n  Widget buildPreview(bool showActions) {\n    Widget child = CustomLinkPreviewWidget(\n      key: widgetKey,\n      node: node,\n      url: url,\n      isHovering: showActions,\n      title: linkInfo.siteName,\n      description: linkInfo.description,\n      imageUrl: linkInfo.imageUrl,\n      status: status,\n    );\n\n    if (widget.showActions && widget.actionBuilder != null) {\n      child = BlockComponentActionWrapper(\n        node: node,\n        actionBuilder: widget.actionBuilder!,\n        child: child,\n      );\n    }\n\n    child = Stack(\n      children: [\n        child,\n        if (showActions && UniversalPlatform.isDesktopOrWeb)\n          Positioned(\n            top: 12,\n            right: 12,\n            child: CustomLinkPreviewMenu(\n              onMenuShowed: () {\n                isMenuShowing = true;\n              },\n              onMenuHided: () {\n                isMenuShowing = false;\n                if (!isHovering && mounted) {\n                  showActionsNotifier.value = false;\n                }\n              },\n              onReload: () {\n                setState(() {\n                  status = LinkLoadingStatus.loading;\n                });\n                Future.delayed(const Duration(milliseconds: 200), () {\n                  if (mounted) parser.start(url);\n                });\n              },\n              node: node,\n            ),\n          ),\n      ],\n    );\n\n    final parent = node.parent;\n    EdgeInsets newPadding = padding;\n    if (parent?.type == CalloutBlockKeys.type) {\n      newPadding = padding.copyWith(right: padding.right + 10);\n    }\n    child = Padding(padding: newPadding, child: child);\n\n    return child;\n  }\n\n  @override\n  Node get currentNode => node;\n\n  @override\n  EdgeInsets get boxPadding => padding;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/default_selectable_mixin.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/widgets.dart';\n\nabstract class DefaultSelectableMixinState<T extends StatefulWidget>\n    extends State<T> with SelectableMixin {\n  final widgetKey = GlobalKey();\n  RenderBox? get _renderBox =>\n      widgetKey.currentContext?.findRenderObject() as RenderBox?;\n\n  Node get currentNode;\n\n  EdgeInsets get boxPadding => EdgeInsets.zero;\n\n  @override\n  Position start() => Position(path: currentNode.path);\n\n  @override\n  Position end() => Position(path: currentNode.path, offset: 1);\n\n  @override\n  Position getPositionInOffset(Offset start) => end();\n\n  @override\n  bool get shouldCursorBlink => false;\n\n  @override\n  CursorStyle get cursorStyle => CursorStyle.cover;\n\n  @override\n  Rect getBlockRect({\n    bool shiftWithBaseOffset = false,\n  }) {\n    final box = _renderBox;\n    if (box is RenderBox) {\n      return boxPadding.topLeft & box.size;\n    }\n    return Rect.zero;\n  }\n\n  @override\n  Rect? getCursorRectInPosition(\n    Position position, {\n    bool shiftWithBaseOffset = false,\n  }) {\n    final rects = getRectsInSelection(Selection.collapsed(position));\n    return rects.firstOrNull;\n  }\n\n  @override\n  List<Rect> getRectsInSelection(\n    Selection selection, {\n    bool shiftWithBaseOffset = false,\n  }) {\n    if (_renderBox == null) {\n      return [];\n    }\n    final parentBox = context.findRenderObject();\n    final box = widgetKey.currentContext?.findRenderObject();\n    if (parentBox is RenderBox && box is RenderBox) {\n      return [\n        box.localToGlobal(Offset.zero, ancestor: parentBox) & box.size,\n      ];\n    }\n    return [Offset.zero & _renderBox!.size];\n  }\n\n  @override\n  Selection getSelectionInRange(Offset start, Offset end) => Selection.single(\n        path: currentNode.path,\n        startOffset: 0,\n        endOffset: 1,\n      );\n\n  @override\n  Offset localToGlobal(Offset offset, {bool shiftWithBaseOffset = false}) =>\n      _renderBox!.localToGlobal(offset);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart';\nimport 'package:appflowy_backend/log.dart';\n// ignore: depend_on_referenced_packages\nimport 'package:html/parser.dart' as html_parser;\nimport 'package:http/http.dart' as http;\nimport 'dart:convert';\n\nabstract class LinkInfoParser {\n  Future<LinkInfo?> parse(\n    Uri link, {\n    Duration timeout = const Duration(seconds: 8),\n    Map<String, String>? headers,\n  });\n\n  static String formatUrl(String url) {\n    Uri? uri = Uri.tryParse(url);\n    if (uri == null) return url;\n    if (!uri.hasScheme) uri = Uri.tryParse('http://$url');\n    if (uri == null) return url;\n    final isHome = (uri.hasEmptyPath || uri.path == '/') && !uri.hasQuery;\n    final homeUrl = '${uri.scheme}://${uri.host}/';\n    if (isHome) return homeUrl;\n    return '$uri';\n  }\n}\n\nclass DefaultParser implements LinkInfoParser {\n  @override\n  Future<LinkInfo?> parse(\n    Uri link, {\n    Duration timeout = const Duration(seconds: 8),\n    Map<String, String>? headers,\n  }) async {\n    try {\n      final isHome = (link.hasEmptyPath || link.path == '/') && !link.hasQuery;\n      final http.Response response =\n          await http.get(link, headers: headers).timeout(timeout);\n      final code = response.statusCode;\n      if (code != 200 && isHome) {\n        throw Exception('Http request error: $code');\n      }\n\n      final contentType = response.headers['content-type'];\n      final charset = contentType?.split('charset=').lastOrNull;\n      String body = '';\n      if (charset == null ||\n          charset.toLowerCase() == 'latin-1' ||\n          charset.toLowerCase() == 'iso-8859-1') {\n        body = latin1.decode(response.bodyBytes);\n      } else {\n        body = utf8.decode(response.bodyBytes, allowMalformed: true);\n      }\n\n      final document = html_parser.parse(body);\n\n      final siteName = document\n          .querySelector('meta[property=\"og:site_name\"]')\n          ?.attributes['content'];\n\n      String? title = document\n          .querySelector('meta[property=\"og:title\"]')\n          ?.attributes['content'];\n      title ??= document.querySelector('title')?.text;\n\n      String? description = document\n          .querySelector('meta[property=\"og:description\"]')\n          ?.attributes['content'];\n      description ??= document\n          .querySelector('meta[name=\"description\"]')\n          ?.attributes['content'];\n\n      String? imageUrl = document\n          .querySelector('meta[property=\"og:image\"]')\n          ?.attributes['content'];\n      if (imageUrl != null && !imageUrl.startsWith('http')) {\n        imageUrl = link.resolve(imageUrl).toString();\n      }\n\n      final favicon =\n          'https://www.faviconextractor.com/favicon/${link.host}?larger=true';\n\n      return LinkInfo(\n        url: '$link',\n        siteName: siteName,\n        title: title,\n        description: description,\n        imageUrl: imageUrl,\n        faviconUrl: favicon,\n      );\n    } catch (e) {\n      Log.error('Parse link $link error: $e');\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/youtube_parser.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:http/http.dart' as http;\nimport 'default_parser.dart';\n\nclass YoutubeParser implements LinkInfoParser {\n  @override\n  Future<LinkInfo?> parse(\n    Uri link, {\n    Duration timeout = const Duration(seconds: 8),\n    Map<String, String>? headers,\n  }) async {\n    try {\n      final isHome = (link.hasEmptyPath || link.path == '/') && !link.hasQuery;\n      if (isHome) {\n        return DefaultParser().parse(\n          link,\n          timeout: timeout,\n          headers: headers,\n        );\n      }\n\n      final requestLink =\n          'https://www.youtube.com/oembed?url=$link&format=json';\n      final http.Response response = await http\n          .get(Uri.parse(requestLink), headers: headers)\n          .timeout(timeout);\n      final code = response.statusCode;\n      if (code != 200) {\n        throw Exception('Http request error: $code');\n      }\n\n      final youtubeInfo = YoutubeInfo.fromJson(jsonDecode(response.body));\n\n      final favicon =\n          'https://www.google.com/s2/favicons?sz=64&domain=${link.host}';\n      return LinkInfo(\n        url: '$link',\n        title: youtubeInfo.title,\n        siteName: youtubeInfo.authorName,\n        imageUrl: youtubeInfo.thumbnailUrl,\n        faviconUrl: favicon,\n      );\n    } catch (e) {\n      Log.error('Parse link $link error: $e');\n      return null;\n    }\n  }\n}\n\nclass YoutubeInfo {\n  YoutubeInfo({\n    this.title,\n    this.authorName,\n    this.version,\n    this.providerName,\n    this.providerUrl,\n    this.thumbnailUrl,\n  });\n\n  YoutubeInfo.fromJson(Map<String, dynamic> json) {\n    title = json['title'];\n    authorName = json['author_name'];\n    version = json['version'];\n    providerName = json['provider_name'];\n    providerUrl = json['provider_url'];\n    thumbnailUrl = json['thumbnail_url'];\n  }\n  String? title;\n  String? authorName;\n  String? version;\n  String? providerName;\n  String? providerUrl;\n  String? thumbnailUrl;\n\n  Map<String, dynamic> toJson() => {\n        'title': title,\n        'author_name': authorName,\n        'version': version,\n        'provider_name': providerName,\n        'provider_url': providerUrl,\n        'thumbnail_url': thumbnailUrl,\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass CustomLinkPreviewMenu extends StatefulWidget {\n  const CustomLinkPreviewMenu({\n    super.key,\n    required this.onMenuShowed,\n    required this.onMenuHided,\n    required this.onReload,\n    required this.node,\n  });\n  final VoidCallback onMenuShowed;\n  final VoidCallback onMenuHided;\n  final VoidCallback onReload;\n  final Node node;\n\n  @override\n  State<CustomLinkPreviewMenu> createState() => _CustomLinkPreviewMenuState();\n}\n\nclass _CustomLinkPreviewMenuState extends State<CustomLinkPreviewMenu> {\n  final popoverController = PopoverController();\n  final buttonKey = GlobalKey();\n  bool closed = false;\n  bool selected = false;\n\n  @override\n  void dispose() {\n    super.dispose();\n    popoverController.close();\n    widget.onMenuHided.call();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      offset: Offset(0, 0.0),\n      direction: PopoverDirection.bottomWithRightAligned,\n      margin: EdgeInsets.zero,\n      controller: popoverController,\n      onOpen: () => keepEditorFocusNotifier.increase(),\n      onClose: () {\n        keepEditorFocusNotifier.decrease();\n        if (!closed) {\n          closed = true;\n          return;\n        } else {\n          closed = false;\n          widget.onMenuHided.call();\n        }\n        setState(() {\n          selected = false;\n        });\n      },\n      popupBuilder: (context) => buildMenu(),\n      child: FlowyIconButton(\n        key: buttonKey,\n        isSelected: selected,\n        icon: FlowySvg(FlowySvgs.toolbar_more_m),\n        onPressed: showPopover,\n      ),\n    );\n  }\n\n  Widget buildMenu() {\n    final editorState = context.read<EditorState>(),\n        editable = editorState.editable;\n    return MouseRegion(\n      child: Padding(\n        padding: const EdgeInsets.all(8.0),\n        child: SeparatedColumn(\n          mainAxisSize: MainAxisSize.min,\n          separatorBuilder: () => const VSpace(0.0),\n          children:\n              List.generate(LinkPreviewMenuCommand.values.length, (index) {\n            final command = LinkPreviewMenuCommand.values[index];\n            final isCopyCommand = command == LinkPreviewMenuCommand.copyLink;\n            final enableButton = editable || (!editable && isCopyCommand);\n            return SizedBox(\n              height: 36,\n              child: FlowyButton(\n                hoverColor: enableButton ? null : Colors.transparent,\n                text: FlowyText(\n                  command.title,\n                  fontWeight: FontWeight.w400,\n                  figmaLineHeight: 20,\n                ),\n                onTap: enableButton ? () => onTap(command) : null,\n              ),\n            );\n          }),\n        ),\n      ),\n    );\n  }\n\n  Future<void> onTap(LinkPreviewMenuCommand command) async {\n    final editorState = context.read<EditorState>();\n    final node = widget.node;\n    final url = node.attributes[LinkPreviewBlockKeys.url];\n    switch (command) {\n      case LinkPreviewMenuCommand.convertToMention:\n        await convertUrlPreviewNodeToMention(editorState, node);\n        break;\n      case LinkPreviewMenuCommand.convertToUrl:\n        await convertUrlPreviewNodeToLink(editorState, node);\n        break;\n      case LinkPreviewMenuCommand.convertToEmbed:\n        final transaction = editorState.transaction;\n        transaction.updateNode(node, {\n          LinkPreviewBlockKeys.url: url,\n          LinkEmbedKeys.previewType: LinkEmbedKeys.embed,\n        });\n        await editorState.apply(transaction);\n        break;\n      case LinkPreviewMenuCommand.copyLink:\n        if (url != null) {\n          await context.copyLink(url);\n        }\n        break;\n      case LinkPreviewMenuCommand.replace:\n        final box = buttonKey.currentContext?.findRenderObject() as RenderBox?;\n        if (box == null) return;\n        final p = box.localToGlobal(Offset.zero);\n        showReplaceMenu(\n          context: context,\n          editorState: editorState,\n          node: node,\n          url: url,\n          ltrb: LTRB(left: p.dx - 330, top: p.dy),\n          onReplace: (url) async {\n            await convertLinkBlockToOtherLinkBlock(\n              editorState,\n              node,\n              node.type,\n              url: url,\n            );\n          },\n        );\n        break;\n      case LinkPreviewMenuCommand.reload:\n        widget.onReload.call();\n        break;\n      case LinkPreviewMenuCommand.removeLink:\n        await removeUrlPreviewLink(editorState, node);\n        break;\n    }\n    closePopover();\n  }\n\n  void showPopover() {\n    widget.onMenuShowed.call();\n    keepEditorFocusNotifier.increase();\n    popoverController.show();\n    setState(() {\n      selected = true;\n    });\n  }\n\n  void closePopover() {\n    popoverController.close();\n    widget.onMenuHided.call();\n  }\n}\n\nenum LinkPreviewMenuCommand {\n  convertToMention,\n  convertToUrl,\n  convertToEmbed,\n  copyLink,\n  replace,\n  reload,\n  removeLink;\n\n  String get title {\n    switch (this) {\n      case convertToMention:\n        return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toMetion\n            .tr();\n      case LinkPreviewMenuCommand.convertToUrl:\n        return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl\n            .tr();\n      case LinkPreviewMenuCommand.convertToEmbed:\n        return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toEmbed\n            .tr();\n      case LinkPreviewMenuCommand.copyLink:\n        return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_copyLink\n            .tr();\n      case LinkPreviewMenuCommand.replace:\n        return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_replace\n            .tr();\n      case LinkPreviewMenuCommand.reload:\n        return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_reload\n            .tr();\n      case LinkPreviewMenuCommand.removeLink:\n        return LocaleKeys\n            .document_plugins_linkPreview_linkPreviewMenu_removeLink\n            .tr();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nconst _menuHeighgt = 188.0, _menuWidth = 288.0;\n\nclass PasteAsMenuService {\n  PasteAsMenuService({\n    required this.context,\n    required this.editorState,\n  });\n\n  final BuildContext context;\n  final EditorState editorState;\n  OverlayEntry? _menuEntry;\n\n  void show(String href) {\n    WidgetsBinding.instance.addPostFrameCallback((_) => _show(href));\n  }\n\n  void dismiss() {\n    if (_menuEntry != null) {\n      keepEditorFocusNotifier.decrease();\n      // editorState.service.scrollService?.enable();\n      // editorState.service.keyboardService?.enable();\n    }\n    _menuEntry?.remove();\n    _menuEntry = null;\n  }\n\n  void _show(String href) {\n    final Size editorSize = editorState.renderBox?.size ?? Size.zero;\n    if (editorSize == Size.zero) return;\n    final menuPosition = editorState.calculateMenuOffset(\n      menuWidth: _menuWidth,\n      menuHeight: _menuHeighgt,\n    );\n    if (menuPosition == null) return;\n    final ltrb = menuPosition.ltrb;\n\n    _menuEntry = OverlayEntry(\n      builder: (context) => SizedBox(\n        height: editorSize.height,\n        width: editorSize.width,\n        child: GestureDetector(\n          behavior: HitTestBehavior.opaque,\n          onTap: dismiss,\n          child: Stack(\n            children: [\n              ltrb.buildPositioned(\n                child: PasteAsMenu(\n                  editorState: editorState,\n                  onSelect: (t) {\n                    final selection = editorState.selection;\n                    if (selection == null) return;\n                    final end = selection.end;\n                    final urlSelection = Selection(\n                      start: end.copyWith(offset: end.offset - href.length),\n                      end: end,\n                    );\n                    if (t == PasteMenuType.bookmark) {\n                      convertUrlToLinkPreview(editorState, urlSelection, href);\n                    } else if (t == PasteMenuType.mention) {\n                      convertUrlToMention(editorState, urlSelection);\n                    } else if (t == PasteMenuType.embed) {\n                      convertUrlToLinkPreview(\n                        editorState,\n                        urlSelection,\n                        href,\n                        previewType: LinkEmbedKeys.embed,\n                      );\n                    }\n                    dismiss();\n                  },\n                  onDismiss: dismiss,\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n\n    Overlay.of(context).insert(_menuEntry!);\n\n    keepEditorFocusNotifier.increase();\n    // editorState.service.keyboardService?.disable(showCursor: true);\n    // editorState.service.scrollService?.disable();\n  }\n}\n\nclass PasteAsMenu extends StatefulWidget {\n  const PasteAsMenu({\n    super.key,\n    required this.onSelect,\n    required this.onDismiss,\n    required this.editorState,\n  });\n  final ValueChanged<PasteMenuType?> onSelect;\n  final VoidCallback onDismiss;\n  final EditorState editorState;\n\n  @override\n  State<PasteAsMenu> createState() => _PasteAsMenuState();\n}\n\nclass _PasteAsMenuState extends State<PasteAsMenu> {\n  final focusNode = FocusNode(debugLabel: 'paste_as_menu');\n  final ValueNotifier<int> selectedIndexNotifier = ValueNotifier(0);\n\n  EditorState get editorState => widget.editorState;\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addPostFrameCallback(\n      (_) => focusNode.requestFocus(),\n    );\n    editorState.selectionNotifier.addListener(dismiss);\n  }\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    selectedIndexNotifier.dispose();\n    editorState.selectionNotifier.removeListener(dismiss);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Focus(\n      focusNode: focusNode,\n      onKeyEvent: onKeyEvent,\n      child: Container(\n        width: _menuWidth,\n        height: _menuHeighgt,\n        padding: EdgeInsets.all(6),\n        decoration: BoxDecoration(\n          borderRadius: BorderRadius.circular(8),\n          color: theme.surfaceColorScheme.primary,\n          boxShadow: theme.shadow.medium,\n        ),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Container(\n              height: 32,\n              padding: EdgeInsets.all(8),\n              child: FlowyText.semibold(\n                color: theme.textColorScheme.primary,\n                LocaleKeys.document_plugins_linkPreview_typeSelection_pasteAs\n                    .tr(),\n              ),\n            ),\n            ...List.generate(\n              PasteMenuType.values.length,\n              (i) => buildItem(PasteMenuType.values[i], i),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget buildItem(PasteMenuType type, int i) {\n    return ValueListenableBuilder(\n      valueListenable: selectedIndexNotifier,\n      builder: (context, value, child) {\n        final isSelected = i == value;\n        return SizedBox(\n          height: 36,\n          child: FlowyButton(\n            isSelected: isSelected,\n            text: FlowyText(\n              type.title,\n            ),\n            onTap: () => onSelect(type),\n          ),\n        );\n      },\n    );\n  }\n\n  void changeIndex(int index) => selectedIndexNotifier.value = index;\n\n  KeyEventResult onKeyEvent(focus, KeyEvent event) {\n    if (event is! KeyDownEvent && event is! KeyRepeatEvent) {\n      return KeyEventResult.ignored;\n    }\n\n    int index = selectedIndexNotifier.value,\n        length = PasteMenuType.values.length;\n    if (event.logicalKey == LogicalKeyboardKey.enter) {\n      onSelect(PasteMenuType.values[index]);\n      return KeyEventResult.handled;\n    } else if (event.logicalKey == LogicalKeyboardKey.escape) {\n      dismiss();\n    } else if (event.logicalKey == LogicalKeyboardKey.backspace) {\n      dismiss();\n    } else if ([LogicalKeyboardKey.arrowUp, LogicalKeyboardKey.arrowLeft]\n        .contains(event.logicalKey)) {\n      if (index == 0) {\n        index = length - 1;\n      } else {\n        index--;\n      }\n      changeIndex(index);\n      return KeyEventResult.handled;\n    } else if ([LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.arrowRight]\n        .contains(event.logicalKey)) {\n      if (index == length - 1) {\n        index = 0;\n      } else {\n        index++;\n      }\n      changeIndex(index);\n      return KeyEventResult.handled;\n    }\n    return KeyEventResult.ignored;\n  }\n\n  void onSelect(PasteMenuType type) => widget.onSelect.call(type);\n\n  void dismiss() => widget.onDismiss.call();\n}\n\nenum PasteMenuType {\n  mention,\n  url,\n  bookmark,\n  embed,\n}\n\nextension PasteMenuTypeExtension on PasteMenuType {\n  String get title {\n    switch (this) {\n      case PasteMenuType.mention:\n        return LocaleKeys.document_plugins_linkPreview_typeSelection_mention\n            .tr();\n      case PasteMenuType.url:\n        return LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr();\n      case PasteMenuType.bookmark:\n        return LocaleKeys.document_plugins_linkPreview_typeSelection_bookmark\n            .tr();\n      case PasteMenuType.embed:\n        return LocaleKeys.document_plugins_linkPreview_typeSelection_embed.tr();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/shared.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\n\nFuture<void> convertUrlPreviewNodeToLink(\n  EditorState editorState,\n  Node node,\n) async {\n  if (node.type != LinkPreviewBlockKeys.type) {\n    return;\n  }\n\n  final url = node.attributes[LinkPreviewBlockKeys.url];\n  final delta = Delta()\n    ..insert(\n      url,\n      attributes: {\n        AppFlowyRichTextKeys.href: url,\n      },\n    );\n  final transaction = editorState.transaction;\n  transaction\n    ..insertNode(node.path, paragraphNode(delta: delta))\n    ..deleteNode(node);\n  transaction.afterSelection = Selection.collapsed(\n    Position(\n      path: node.path,\n      offset: url.length,\n    ),\n  );\n  return editorState.apply(transaction);\n}\n\nFuture<void> convertUrlPreviewNodeToMention(\n  EditorState editorState,\n  Node node,\n) async {\n  if (node.type != LinkPreviewBlockKeys.type) {\n    return;\n  }\n\n  final url = node.attributes[LinkPreviewBlockKeys.url];\n  final delta = Delta()\n    ..insert(\n      MentionBlockKeys.mentionChar,\n      attributes: {\n        MentionBlockKeys.mention: {\n          MentionBlockKeys.type: MentionType.externalLink.name,\n          MentionBlockKeys.url: url,\n        },\n      },\n    );\n  final transaction = editorState.transaction;\n  transaction\n    ..insertNode(node.path, paragraphNode(delta: delta))\n    ..deleteNode(node);\n  transaction.afterSelection = Selection.collapsed(\n    Position(\n      path: node.path,\n      offset: url.length,\n    ),\n  );\n  return editorState.apply(transaction);\n}\n\nFuture<void> removeUrlPreviewLink(\n  EditorState editorState,\n  Node node,\n) async {\n  if (node.type != LinkPreviewBlockKeys.type) {\n    return;\n  }\n\n  final url = node.attributes[LinkPreviewBlockKeys.url];\n  final delta = Delta()..insert(url);\n  final transaction = editorState.transaction;\n  transaction\n    ..insertNode(node.path, paragraphNode(delta: delta))\n    ..deleteNode(node);\n  transaction.afterSelection = Selection.collapsed(\n    Position(\n      path: node.path,\n      offset: url.length,\n    ),\n  );\n  return editorState.apply(transaction);\n}\n\nFuture<void> convertUrlToLinkPreview(\n  EditorState editorState,\n  Selection selection,\n  String url, {\n  String? previewType,\n}) async {\n  final node = editorState.getNodeAtPath(selection.end.path);\n  if (node == null) {\n    return;\n  }\n  final delta = node.delta;\n  if (delta == null) return;\n  final List<TextInsert> beforeOperations = [], afterOperations = [];\n  int index = 0;\n  for (final insert in delta.whereType<TextInsert>()) {\n    if (index < selection.startIndex) {\n      beforeOperations.add(insert);\n    } else if (index >= selection.endIndex) {\n      afterOperations.add(insert);\n    }\n    index += insert.length;\n  }\n  final transaction = editorState.transaction;\n  transaction\n    ..deleteNode(node)\n    ..insertNodes(node.path.next, [\n      if (beforeOperations.isNotEmpty)\n        paragraphNode(delta: Delta(operations: beforeOperations)),\n      if (previewType == LinkEmbedKeys.embed)\n        linkEmbedNode(url: url)\n      else\n        linkPreviewNode(url: url),\n      if (afterOperations.isNotEmpty)\n        paragraphNode(delta: Delta(operations: afterOperations)),\n    ]);\n  await editorState.apply(transaction);\n}\n\nFuture<void> convertUrlToMention(\n  EditorState editorState,\n  Selection selection,\n) async {\n  final node = editorState.getNodeAtPath(selection.end.path);\n  if (node == null) {\n    return;\n  }\n  final delta = node.delta;\n  if (delta == null) return;\n  String url = '';\n  int index = 0;\n  for (final insert in delta.whereType<TextInsert>()) {\n    if (index >= selection.startIndex && index < selection.endIndex) {\n      final href = insert.attributes?.href ?? '';\n      if (href.isNotEmpty) {\n        url = href;\n        break;\n      }\n    }\n    index += insert.length;\n  }\n  final transaction = editorState.transaction;\n  transaction.replaceText(\n    node,\n    selection.startIndex,\n    selection.length,\n    MentionBlockKeys.mentionChar,\n    attributes: {\n      MentionBlockKeys.mention: {\n        MentionBlockKeys.type: MentionType.externalLink.name,\n        MentionBlockKeys.url: url,\n      },\n    },\n  );\n  await editorState.apply(transaction);\n}\n\nFuture<void> convertLinkBlockToOtherLinkBlock(\n  EditorState editorState,\n  Node node,\n  String toType, {\n  String? url,\n}) async {\n  final nodeType = node.type;\n  if (nodeType != LinkPreviewBlockKeys.type ||\n      (nodeType == toType && url == null)) {\n    return;\n  }\n  final insertedNode = <Node>[];\n\n  final afterUrl = url ?? node.attributes[LinkPreviewBlockKeys.url] ?? '';\n  final previewType = node.attributes[LinkEmbedKeys.previewType];\n  Node afterNode = node.copyWith(\n    type: toType,\n    attributes: {\n      LinkPreviewBlockKeys.url: afterUrl,\n      LinkEmbedKeys.previewType: previewType,\n      blockComponentBackgroundColor:\n          node.attributes[blockComponentBackgroundColor],\n      blockComponentTextDirection: node.attributes[blockComponentTextDirection],\n      blockComponentDelta: (node.delta ?? Delta()).toJson(),\n    },\n  );\n  afterNode = afterNode.copyWith(children: []);\n  insertedNode.add(afterNode);\n  insertedNode.addAll(node.children.map((e) => e.deepCopy()));\n  final transaction = editorState.transaction;\n  transaction.insertNodes(\n    node.path,\n    insertedNode,\n  );\n  transaction.deleteNodes([node]);\n  await editorState.apply(transaction);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flowy_infra_ui/widget/buttons/primary_button.dart';\nimport 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_math_fork/flutter_math.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass MathEquationBlockKeys {\n  const MathEquationBlockKeys._();\n\n  static const String type = 'math_equation';\n\n  /// The content of a math equation block.\n  ///\n  /// The value is a String.\n  static const String formula = 'formula';\n}\n\nNode mathEquationNode({\n  String formula = '',\n}) {\n  final attributes = {\n    MathEquationBlockKeys.formula: formula,\n  };\n  return Node(\n    type: MathEquationBlockKeys.type,\n    attributes: attributes,\n  );\n}\n\n// defining the callout block menu item for selection\nSelectionMenuItem mathEquationItem = SelectionMenuItem.node(\n  getName: LocaleKeys.document_plugins_mathEquation_name.tr,\n  iconBuilder: (editorState, onSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.icon_math_eq_s,\n    isSelected: onSelected,\n    style: style,\n  ),\n  keywords: ['tex, latex, katex', 'math equation', 'formula'],\n  nodeBuilder: (editorState, _) => mathEquationNode(),\n  replace: (_, node) => node.delta?.isEmpty ?? false,\n  updateSelection: (editorState, path, __, ___) {\n    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n      final mathEquationState =\n          editorState.getNodeAtPath(path)?.key.currentState;\n      if (mathEquationState != null &&\n          mathEquationState is MathEquationBlockComponentWidgetState) {\n        mathEquationState.showEditingDialog();\n      }\n    });\n    return null;\n  },\n);\n\nclass MathEquationBlockComponentBuilder extends BlockComponentBuilder {\n  MathEquationBlockComponentBuilder({\n    super.configuration,\n  });\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    return MathEquationBlockComponentWidget(\n      key: node.key,\n      node: node,\n      configuration: configuration,\n      showActions: showActions(node),\n      actionBuilder: (context, state) => actionBuilder(\n        blockComponentContext,\n        state,\n      ),\n      actionTrailingBuilder: (context, state) => actionTrailingBuilder(\n        blockComponentContext,\n        state,\n      ),\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate => (node) =>\n      node.children.isEmpty &&\n      node.attributes[MathEquationBlockKeys.formula] is String;\n}\n\nclass MathEquationBlockComponentWidget extends BlockComponentStatefulWidget {\n  const MathEquationBlockComponentWidget({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n  });\n\n  @override\n  State<MathEquationBlockComponentWidget> createState() =>\n      MathEquationBlockComponentWidgetState();\n}\n\nclass MathEquationBlockComponentWidgetState\n    extends State<MathEquationBlockComponentWidget>\n    with BlockComponentConfigurable {\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  @override\n  Node get node => widget.node;\n\n  String get formula =>\n      widget.node.attributes[MathEquationBlockKeys.formula] as String;\n\n  late final editorState = context.read<EditorState>();\n  final ValueNotifier<bool> isHover = ValueNotifier(false);\n\n  late final controller = TextEditingController(text: formula);\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return InkWell(\n      onHover: (value) => isHover.value = value,\n      onTap: showEditingDialog,\n      child: _build(context),\n    );\n  }\n\n  Widget _build(BuildContext context) {\n    Widget child = Container(\n      constraints: const BoxConstraints(minHeight: 52),\n      decoration: BoxDecoration(\n        color: formula.isNotEmpty\n            ? Colors.transparent\n            : Theme.of(context).colorScheme.surfaceContainerHighest,\n        borderRadius: BorderRadius.circular(4),\n      ),\n      child: FlowyHover(\n        style: HoverStyle(\n          borderRadius: BorderRadius.circular(4),\n        ),\n        child: formula.isEmpty\n            ? _buildPlaceholderWidget(context)\n            : _buildMathEquation(context),\n      ),\n    );\n\n    if (widget.showActions && widget.actionBuilder != null) {\n      child = BlockComponentActionWrapper(\n        node: node,\n        actionBuilder: widget.actionBuilder!,\n        actionTrailingBuilder: widget.actionTrailingBuilder,\n        child: child,\n      );\n    }\n\n    if (UniversalPlatform.isMobile) {\n      child = MobileBlockActionButtons(\n        node: node,\n        editorState: editorState,\n        child: child,\n      );\n    }\n\n    child = Padding(\n      padding: padding,\n      child: child,\n    );\n\n    if (UniversalPlatform.isDesktopOrWeb) {\n      child = Stack(\n        children: [\n          child,\n          Positioned(\n            right: 6,\n            top: 12,\n            child: ValueListenableBuilder<bool>(\n              valueListenable: isHover,\n              builder: (_, value, __) =>\n                  value ? _buildDeleteButton(context) : const SizedBox.shrink(),\n            ),\n          ),\n        ],\n      );\n    }\n\n    return child;\n  }\n\n  Widget _buildPlaceholderWidget(BuildContext context) {\n    return SizedBox(\n      height: 52,\n      child: Row(\n        children: [\n          const HSpace(10),\n          FlowySvg(\n            FlowySvgs.slash_menu_icon_math_equation_s,\n            color: Theme.of(context).hintColor,\n            size: const Size.square(24),\n          ),\n          const HSpace(10),\n          FlowyText(\n            LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(),\n            color: Theme.of(context).hintColor,\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildMathEquation(BuildContext context) {\n    return Center(\n      child: Math.tex(\n        formula,\n        textStyle: const TextStyle(fontSize: 20),\n      ),\n    );\n  }\n\n  Widget _buildDeleteButton(BuildContext context) {\n    return MenuBlockButton(\n      tooltip: LocaleKeys.button_delete.tr(),\n      iconData: FlowySvgs.trash_s,\n      onTap: () {\n        final transaction = editorState.transaction..deleteNode(widget.node);\n        editorState.apply(transaction);\n      },\n    );\n  }\n\n  void showEditingDialog() {\n    showDialog(\n      context: context,\n      builder: (context) {\n        return AlertDialog(\n          backgroundColor: Theme.of(context).canvasColor,\n          title: Text(\n            LocaleKeys.document_plugins_mathEquation_editMathEquation.tr(),\n          ),\n          content: KeyboardListener(\n            focusNode: FocusNode(),\n            onKeyEvent: (key) {\n              if (key.logicalKey == LogicalKeyboardKey.enter &&\n                  !HardwareKeyboard.instance.isShiftPressed) {\n                updateMathEquation(controller.text, context);\n              } else if (key.logicalKey == LogicalKeyboardKey.escape) {\n                dismiss(context);\n              }\n            },\n            child: SizedBox(\n              width: MediaQuery.of(context).size.width * 0.3,\n              child: TextField(\n                autofocus: true,\n                controller: controller,\n                maxLines: null,\n                decoration: const InputDecoration(\n                  border: OutlineInputBorder(),\n                  hintText: 'E = MC^2',\n                ),\n              ),\n            ),\n          ),\n          actions: [\n            SecondaryTextButton(\n              LocaleKeys.button_cancel.tr(),\n              mode: TextButtonMode.big,\n              onPressed: () => dismiss(context),\n            ),\n            PrimaryTextButton(\n              LocaleKeys.button_done.tr(),\n              onPressed: () => updateMathEquation(controller.text, context),\n            ),\n          ],\n          actionsPadding: const EdgeInsets.only(bottom: 20),\n          actionsAlignment: MainAxisAlignment.spaceAround,\n        );\n      },\n    );\n  }\n\n  void updateMathEquation(String mathEquation, BuildContext context) {\n    if (mathEquation == formula) {\n      dismiss(context);\n      return;\n    }\n    final transaction = editorState.transaction\n      ..updateNode(\n        widget.node,\n        {\n          MathEquationBlockKeys.formula: mathEquation,\n        },\n      );\n    editorState.apply(transaction);\n    dismiss(context);\n  }\n\n  void dismiss(BuildContext context) {\n    Navigator.of(context).pop();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_shortcut.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\n/// Windows / Linux : ctrl + shift + e\n/// macOS           : cmd + shift + e\n/// Allows the user to insert math equation by shortcut\n///\n/// - support\n///   - desktop\n///   - web\n///\nfinal CommandShortcutEvent insertInlineMathEquationCommand =\n    CommandShortcutEvent(\n  key: 'Insert inline math equation',\n  command: 'ctrl+shift+e',\n  macOSCommand: 'cmd+shift+e',\n  getDescription: LocaleKeys.document_plugins_mathEquation_name.tr,\n  handler: (editorState) {\n    final selection = editorState.selection;\n    if (selection == null || selection.isCollapsed || !selection.isSingle) {\n      return KeyEventResult.ignored;\n    }\n    final node = editorState.getNodeAtPath(selection.start.path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      return KeyEventResult.ignored;\n    }\n    if (node.delta == null || !toolbarItemWhiteList.contains(node.type)) {\n      return KeyEventResult.ignored;\n    }\n    final transaction = editorState.transaction;\n    final nodes = editorState.getNodesInSelection(selection);\n    final isHighlight = nodes.allSatisfyInSelection(selection, (delta) {\n      return delta.everyAttributes(\n        (attributes) => attributes[InlineMathEquationKeys.formula] != null,\n      );\n    });\n    if (isHighlight) {\n      final formula = delta\n          .slice(selection.startIndex, selection.endIndex)\n          .whereType<TextInsert>()\n          .firstOrNull\n          ?.attributes?[InlineMathEquationKeys.formula];\n      assert(formula != null);\n      if (formula == null) {\n        return KeyEventResult.ignored;\n      }\n      // clear the format\n      transaction.replaceText(\n        node,\n        selection.startIndex,\n        selection.length,\n        formula,\n        attributes: {},\n      );\n    } else {\n      final text = editorState.getTextInSelection(selection).join();\n      transaction.replaceText(\n        node,\n        selection.startIndex,\n        selection.length,\n        MentionBlockKeys.mentionChar,\n        attributes: {\n          InlineMathEquationKeys.formula: text,\n        },\n      );\n    }\n    editorState.apply(transaction);\n    return KeyEventResult.handled;\n  },\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/mobile_math_equation_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nfinal mathEquationMobileToolbarItem = MobileToolbarItem.action(\n  itemIconBuilder: (_, __, ___) => const SizedBox(\n    width: 22,\n    child: FlowySvg(FlowySvgs.math_lg),\n  ),\n  actionHandler: (_, editorState) async {\n    final selection = editorState.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n    final path = selection.start.path;\n    final node = editorState.getNodeAtPath(path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      return;\n    }\n    final transaction = editorState.transaction;\n    final insertedNode = mathEquationNode();\n\n    if (delta.isEmpty) {\n      transaction\n        ..insertNode(path, insertedNode)\n        ..deleteNode(node);\n    } else {\n      transaction.insertNode(\n        path.next,\n        insertedNode,\n      );\n    }\n\n    await editorState.apply(transaction);\n\n    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n      final mathEquationState =\n          editorState.getNodeAtPath(path)?.key.currentState;\n      if (mathEquationState != null &&\n          mathEquationState is MathEquationBlockComponentWidgetState) {\n        mathEquationState.showEditingDialog();\n      }\n    });\n  },\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';\nimport 'package:appflowy/plugins/trash/application/trash_service.dart';\nimport 'package:appflowy/shared/clipboard_state.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/snap_bar.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nimport '../transaction_handler/mention_transaction_handler.dart';\n\nconst _pasteIdentifier = 'child_page_transaction';\n\nclass ChildPageTransactionHandler extends MentionTransactionHandler {\n  ChildPageTransactionHandler();\n\n  @override\n  Future<void> onTransaction(\n    BuildContext context,\n    String viewId,\n    EditorState editorState,\n    List<MentionBlockData> added,\n    List<MentionBlockData> removed, {\n    bool isCut = false,\n    bool isUndoRedo = false,\n    bool isPaste = false,\n    bool isDraggingNode = false,\n    bool isTurnInto = false,\n    String? parentViewId,\n  }) async {\n    if (isDraggingNode || isTurnInto) {\n      return;\n    }\n\n    // Remove the mentions that were both added and removed in the same transaction.\n    // These were just moved around.\n    final moved = <MentionBlockData>[];\n    for (final mention in added) {\n      if (removed.any((r) => r.$2 == mention.$2)) {\n        moved.add(mention);\n      }\n    }\n\n    for (final mention in removed) {\n      if (!context.mounted || moved.any((m) => m.$2 == mention.$2)) {\n        return;\n      }\n\n      if (mention.$2[MentionBlockKeys.type] != MentionType.childPage.name) {\n        continue;\n      }\n\n      await _handleDeletion(context, mention);\n    }\n\n    if (isPaste || isUndoRedo) {\n      if (context.mounted) {\n        context.read<ClipboardState>().startHandlingPaste(_pasteIdentifier);\n      }\n\n      for (final mention in added) {\n        if (!context.mounted || moved.any((m) => m.$2 == mention.$2)) {\n          return;\n        }\n\n        if (mention.$2[MentionBlockKeys.type] != MentionType.childPage.name) {\n          continue;\n        }\n\n        await _handleAddition(\n          context,\n          editorState,\n          mention,\n          isPaste,\n          parentViewId,\n          isCut,\n        );\n      }\n\n      if (context.mounted) {\n        context.read<ClipboardState>().endHandlingPaste(_pasteIdentifier);\n      }\n    }\n  }\n\n  Future<void> _handleDeletion(\n    BuildContext context,\n    MentionBlockData data,\n  ) async {\n    final viewId = data.$2[MentionBlockKeys.pageId];\n\n    final result = await ViewBackendService.deleteView(viewId: viewId);\n    result.fold(\n      (_) {},\n      (error) {\n        Log.error(error);\n        if (context.mounted) {\n          showToastNotification(\n            message: LocaleKeys.document_plugins_subPage_errors_failedDeletePage\n                .tr(),\n          );\n        }\n      },\n    );\n  }\n\n  Future<void> _handleAddition(\n    BuildContext context,\n    EditorState editorState,\n    MentionBlockData data,\n    bool isPaste,\n    String? parentViewId,\n    bool isCut,\n  ) async {\n    if (parentViewId == null) {\n      return;\n    }\n\n    final viewId = data.$2[MentionBlockKeys.pageId];\n    if (isPaste && !isCut) {\n      _handlePasteFromCopy(\n        context,\n        editorState,\n        data.$1,\n        data.$3,\n        viewId,\n        parentViewId,\n      );\n    } else {\n      _handlePasteFromCut(viewId, parentViewId);\n    }\n  }\n\n  void _handlePasteFromCut(String viewId, String parentViewId) async {\n    // Attempt to restore from Trash just in case\n    await TrashService.putback(viewId);\n\n    final view = (await ViewBackendService.getView(viewId)).toNullable();\n    if (view == null) {\n      return Log.error('View not found: $viewId');\n    }\n\n    if (view.parentViewId == parentViewId) {\n      return;\n    }\n\n    await ViewBackendService.moveViewV2(\n      viewId: viewId,\n      newParentId: parentViewId,\n      prevViewId: null,\n    );\n  }\n\n  void _handlePasteFromCopy(\n    BuildContext context,\n    EditorState editorState,\n    Node node,\n    int index,\n    String viewId,\n    String parentViewId,\n  ) async {\n    final view = (await ViewBackendService.getView(viewId)).toNullable();\n    if (view == null) {\n      return Log.error('View not found: $viewId');\n    }\n\n    final duplicatedViewOrFailure = await ViewBackendService.duplicate(\n      view: view,\n      openAfterDuplicate: false,\n      includeChildren: true,\n      syncAfterDuplicate: true,\n      parentViewId: parentViewId,\n    );\n\n    await duplicatedViewOrFailure.fold(\n      (newView) async {\n        // The index is the index of the delta, to get the index of the mention character\n        // in all the text, we need to calculate it based on the deltas before the current delta.\n        int mentionIndex = 0;\n        for (final (i, delta) in node.delta!.indexed) {\n          if (i >= index) {\n            break;\n          }\n\n          mentionIndex += delta.length;\n        }\n\n        final transaction = editorState.transaction;\n        transaction.formatText(\n          node,\n          mentionIndex,\n          MentionBlockKeys.mentionChar.length,\n          MentionBlockKeys.buildMentionPageAttributes(\n            mentionType: MentionType.childPage,\n            pageId: newView.id,\n            blockId: null,\n          ),\n        );\n        await editorState.apply(\n          transaction,\n          options: const ApplyOptions(recordUndo: false),\n        );\n      },\n      (error) {\n        Log.error(error);\n        if (context.mounted) {\n          showSnapBar(\n            context,\n            LocaleKeys.document_plugins_subPage_errors_failedDuplicatePage.tr(),\n          );\n        }\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/date_transaction_handler.dart",
    "content": "import 'package:appflowy/shared/clipboard_state.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/user/application/reminder/reminder_extension.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flutter/material.dart';\nimport 'package:nanoid/nanoid.dart';\nimport 'package:provider/provider.dart';\n\nimport '../plugins.dart';\nimport '../transaction_handler/mention_transaction_handler.dart';\n\nconst _pasteIdentifier = 'date_transaction';\n\nclass DateTransactionHandler extends MentionTransactionHandler {\n  DateTransactionHandler();\n\n  @override\n  Future<void> onTransaction(\n    BuildContext context,\n    String viewId,\n    EditorState editorState,\n    List<MentionBlockData> added,\n    List<MentionBlockData> removed, {\n    bool isCut = false,\n    bool isUndoRedo = false,\n    bool isPaste = false,\n    bool isDraggingNode = false,\n    bool isTurnInto = false,\n    String? parentViewId,\n  }) async {\n    if (isDraggingNode || isTurnInto) {\n      return;\n    }\n\n    // Remove the mentions that were both added and removed in the same transaction.\n    // These were just moved around.\n    final moved = <MentionBlockData>[];\n    for (final mention in added) {\n      if (removed.any((r) => r.$2 == mention.$2)) {\n        moved.add(mention);\n      }\n    }\n\n    for (final mention in removed) {\n      if (!context.mounted || moved.any((m) => m.$2 == mention.$2)) {\n        return;\n      }\n\n      if (mention.$2[MentionBlockKeys.type] != MentionType.date.name) {\n        continue;\n      }\n\n      _handleDeletion(context, mention);\n    }\n\n    if (isPaste || isUndoRedo) {\n      if (context.mounted) {\n        context.read<ClipboardState>().startHandlingPaste(_pasteIdentifier);\n      }\n\n      for (final mention in added) {\n        if (!context.mounted || moved.any((m) => m.$2 == mention.$2)) {\n          return;\n        }\n\n        if (mention.$2[MentionBlockKeys.type] != MentionType.date.name) {\n          continue;\n        }\n\n        _handleAddition(\n          context,\n          viewId,\n          editorState,\n          mention,\n          isPaste,\n          isCut,\n        );\n      }\n\n      if (context.mounted) {\n        context.read<ClipboardState>().endHandlingPaste(_pasteIdentifier);\n      }\n    }\n  }\n\n  void _handleDeletion(\n    BuildContext context,\n    MentionBlockData data,\n  ) {\n    final reminderId = data.$2[MentionBlockKeys.reminderId];\n\n    if (reminderId case String _ when reminderId.isNotEmpty) {\n      getIt<ReminderBloc>()\n          .add(ReminderEvent.removeReminder(reminderId: reminderId));\n    }\n  }\n\n  void _handleAddition(\n    BuildContext context,\n    String viewId,\n    EditorState editorState,\n    MentionBlockData data,\n    bool isPaste,\n    bool isCut,\n  ) {\n    final dateData = _MentionDateBlockData.fromData(data.$2);\n    if (dateData.dateString.isEmpty) {\n      Log.error(\"mention date block doesn't have a valid date string\");\n      return;\n    }\n\n    if (isPaste && !isCut) {\n      _handlePasteFromCopy(\n        context,\n        viewId,\n        editorState,\n        data.$1,\n        data.$3,\n        dateData,\n      );\n    } else {\n      _handlePasteFromCut(viewId, data.$1, dateData);\n    }\n  }\n\n  void _handlePasteFromCut(\n    String viewId,\n    Node node,\n    _MentionDateBlockData data,\n  ) {\n    final dateTime = DateTime.tryParse(data.dateString);\n\n    if (data.reminderId == null || dateTime == null) {\n      return;\n    }\n\n    getIt<ReminderBloc>().add(\n      ReminderEvent.addById(\n        reminderId: data.reminderId!,\n        objectId: viewId,\n        scheduledAt: Int64(\n          data.reminderOption\n                  .getNotificationDateTime(dateTime)\n                  .millisecondsSinceEpoch ~/\n              1000,\n        ),\n        meta: {\n          ReminderMetaKeys.includeTime: data.includeTime.toString(),\n          ReminderMetaKeys.blockId: node.id,\n        },\n      ),\n    );\n  }\n\n  void _handlePasteFromCopy(\n    BuildContext context,\n    String viewId,\n    EditorState editorState,\n    Node node,\n    int index,\n    _MentionDateBlockData data,\n  ) async {\n    final dateTime = DateTime.tryParse(data.dateString);\n\n    if (node.delta == null) {\n      return;\n    }\n\n    if (data.reminderId == null || dateTime == null) {\n      return;\n    }\n\n    final reminderId = nanoid();\n    getIt<ReminderBloc>().add(\n      ReminderEvent.addById(\n        reminderId: reminderId,\n        objectId: viewId,\n        scheduledAt: Int64(\n          data.reminderOption\n                  .getNotificationDateTime(dateTime)\n                  .millisecondsSinceEpoch ~/\n              1000,\n        ),\n        meta: {\n          ReminderMetaKeys.includeTime: data.includeTime.toString(),\n          ReminderMetaKeys.blockId: node.id,\n        },\n      ),\n    );\n\n    final newMentionAttributes = MentionBlockKeys.buildMentionDateAttributes(\n      date: dateTime.toIso8601String(),\n      reminderId: reminderId,\n      reminderOption: data.reminderOption.name,\n      includeTime: data.includeTime,\n    );\n\n    // The index is the index of the delta, to get the index of the mention character\n    // in all the text, we need to calculate it based on the deltas before the current delta.\n    int mentionIndex = 0;\n    for (final (i, delta) in node.delta!.indexed) {\n      if (i >= index) {\n        break;\n      }\n\n      mentionIndex += delta.length;\n    }\n\n    // Required to prevent editing the same spot at the same time\n    await Future.delayed(const Duration(milliseconds: 100));\n\n    final transaction = editorState.transaction\n      ..formatText(\n        node,\n        mentionIndex,\n        MentionBlockKeys.mentionChar.length,\n        newMentionAttributes,\n      );\n\n    await editorState.apply(\n      transaction,\n      options: const ApplyOptions(recordUndo: false),\n    );\n  }\n}\n\n/// A helper class to parse and store the mention date block data\nclass _MentionDateBlockData {\n  _MentionDateBlockData.fromData(Map<String, dynamic> data) {\n    dateString = switch (data[MentionBlockKeys.date]) {\n      final String string when DateTime.tryParse(string) != null => string,\n      _ => \"\",\n    };\n    includeTime = switch (data[MentionBlockKeys.includeTime]) {\n      final bool flag => flag,\n      _ => false,\n    };\n    reminderOption = switch (data[MentionBlockKeys.reminderOption]) {\n      final String name =>\n        ReminderOption.values.firstWhereOrNull((o) => o.name == name) ??\n            ReminderOption.none,\n      _ => ReminderOption.none,\n    };\n    reminderId = switch (data[MentionBlockKeys.reminderId]) {\n      final String id\n          when id.isNotEmpty && reminderOption != ReminderOption.none =>\n        id,\n      _ => null,\n    };\n  }\n\n  late final String dateString;\n  late final bool includeTime;\n  late final String? reminderId;\n  late final ReminderOption reminderOption;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nimport 'mention_link_block.dart';\n\nenum MentionType {\n  page,\n  date,\n  externalLink,\n  childPage;\n\n  static MentionType fromString(String value) => switch (value) {\n        'page' => page,\n        'date' => date,\n        'externalLink' => externalLink,\n        'childPage' => childPage,\n        // Backwards compatibility\n        'reminder' => date,\n        _ => throw UnimplementedError(),\n      };\n}\n\nNode dateMentionNode() {\n  return paragraphNode(\n    delta: Delta(\n      operations: [\n        TextInsert(\n          MentionBlockKeys.mentionChar,\n          attributes: MentionBlockKeys.buildMentionDateAttributes(\n            date: DateTime.now().toIso8601String(),\n            reminderId: null,\n            reminderOption: null,\n            includeTime: false,\n          ),\n        ),\n      ],\n    ),\n  );\n}\n\nclass MentionBlockKeys {\n  const MentionBlockKeys._();\n\n  static const mention = 'mention';\n  static const type = 'type'; // MentionType, String\n\n  static const pageId = 'page_id';\n  static const blockId = 'block_id';\n  static const url = 'url';\n\n  // Related to Reminder and Date blocks\n  static const date = 'date'; // Start Date\n  static const includeTime = 'include_time';\n  static const reminderId = 'reminder_id'; // ReminderID\n  static const reminderOption = 'reminder_option';\n\n  static const mentionChar = '\\$';\n\n  static Map<String, dynamic> buildMentionPageAttributes({\n    required MentionType mentionType,\n    required String pageId,\n    required String? blockId,\n  }) {\n    return {\n      MentionBlockKeys.mention: {\n        MentionBlockKeys.type: mentionType.name,\n        MentionBlockKeys.pageId: pageId,\n        if (blockId != null) MentionBlockKeys.blockId: blockId,\n      },\n    };\n  }\n\n  static Map<String, dynamic> buildMentionDateAttributes({\n    required String date,\n    required String? reminderId,\n    required String? reminderOption,\n    required bool includeTime,\n  }) {\n    return {\n      MentionBlockKeys.mention: {\n        MentionBlockKeys.type: MentionType.date.name,\n        MentionBlockKeys.date: date,\n        MentionBlockKeys.includeTime: includeTime,\n        if (reminderId != null) MentionBlockKeys.reminderId: reminderId,\n        if (reminderOption != null)\n          MentionBlockKeys.reminderOption: reminderOption,\n      },\n    };\n  }\n}\n\nclass MentionBlock extends StatelessWidget {\n  const MentionBlock({\n    super.key,\n    required this.mention,\n    required this.node,\n    required this.index,\n    required this.textStyle,\n  });\n\n  final Map<String, dynamic> mention;\n  final Node node;\n  final int index;\n  final TextStyle? textStyle;\n\n  @override\n  Widget build(BuildContext context) {\n    final type = MentionType.fromString(mention[MentionBlockKeys.type]);\n    final editorState = context.read<EditorState>();\n\n    switch (type) {\n      case MentionType.page:\n        final String? pageId = mention[MentionBlockKeys.pageId] as String?;\n        if (pageId == null) {\n          return const SizedBox.shrink();\n        }\n        final String? blockId = mention[MentionBlockKeys.blockId] as String?;\n\n        return MentionPageBlock(\n          key: ValueKey(pageId),\n          editorState: editorState,\n          pageId: pageId,\n          blockId: blockId,\n          node: node,\n          textStyle: textStyle,\n          index: index,\n        );\n      case MentionType.childPage:\n        final String? pageId = mention[MentionBlockKeys.pageId] as String?;\n        if (pageId == null) {\n          return const SizedBox.shrink();\n        }\n\n        return MentionSubPageBlock(\n          key: ValueKey(pageId),\n          editorState: editorState,\n          pageId: pageId,\n          node: node,\n          textStyle: textStyle,\n          index: index,\n        );\n\n      case MentionType.date:\n        final String date = mention[MentionBlockKeys.date];\n        final reminderOption = ReminderOption.values.firstWhereOrNull(\n          (o) => o.name == mention[MentionBlockKeys.reminderOption],\n        );\n\n        return MentionDateBlock(\n          key: ValueKey(date),\n          editorState: editorState,\n          date: date,\n          node: node,\n          textStyle: textStyle,\n          index: index,\n          reminderId: mention[MentionBlockKeys.reminderId],\n          reminderOption: reminderOption ?? ReminderOption.none,\n          includeTime: mention[MentionBlockKeys.includeTime] ?? false,\n        );\n      case MentionType.externalLink:\n        final String? url = mention[MentionBlockKeys.url] as String?;\n        if (url == null) {\n          return const SizedBox.shrink();\n        }\n        return MentionLinkBlock(\n          url: url,\n          editorState: editorState,\n          node: node,\n          index: index,\n        );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/plugins/base/drag_handler.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/user/application/reminder/reminder_extension.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/mobile_date_picker.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/utils/date_time_format_ext.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/utils/user_time_format_ext.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_header.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:nanoid/non_secure.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass MentionDateBlock extends StatefulWidget {\n  const MentionDateBlock({\n    super.key,\n    required this.editorState,\n    required this.date,\n    required this.index,\n    required this.node,\n    this.textStyle,\n    this.reminderId,\n    this.reminderOption = ReminderOption.none,\n    this.includeTime = false,\n  });\n\n  final EditorState editorState;\n  final String date;\n  final int index;\n  final Node node;\n\n  /// If [isReminder] is true, then this must not be\n  /// null or empty\n  final String? reminderId;\n\n  final ReminderOption reminderOption;\n\n  final bool includeTime;\n\n  final TextStyle? textStyle;\n\n  @override\n  State<MentionDateBlock> createState() => _MentionDateBlockState();\n}\n\nclass _MentionDateBlockState extends State<MentionDateBlock> {\n  late bool _includeTime = widget.includeTime;\n  late DateTime? parsedDate = DateTime.tryParse(widget.date);\n  late String? _reminderId = widget.reminderId;\n  late ReminderOption _reminderOption = widget.reminderOption;\n\n  ReminderPB? getReminder(BuildContext context) {\n    if (!context.mounted || _reminderId == null) return null;\n    final reminderBloc = context.read<ReminderBloc?>();\n    return reminderBloc?.state.allReminders\n        .firstWhereOrNull((r) => r.id == _reminderId);\n  }\n\n  @override\n  void didUpdateWidget(covariant oldWidget) {\n    parsedDate = DateTime.tryParse(widget.date);\n    if (widget.reminderId != oldWidget.reminderId) {\n      _reminderId = widget.reminderId;\n    }\n    if (widget.includeTime != oldWidget.includeTime) {\n      _includeTime = widget.includeTime;\n    }\n    if (widget.date != oldWidget.date) {\n      parsedDate = DateTime.tryParse(widget.date);\n    }\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (parsedDate == null) {\n      return const SizedBox.shrink();\n    }\n\n    final appearance = context.read<AppearanceSettingsCubit?>();\n    final reminder = context.read<ReminderBloc?>();\n\n    if (appearance == null || reminder == null) {\n      return const SizedBox.shrink();\n    }\n\n    return BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(\n      buildWhen: (previous, current) =>\n          previous.dateFormat != current.dateFormat ||\n          previous.timeFormat != current.timeFormat,\n      builder: (context, appearance) =>\n          BlocBuilder<ReminderBloc, ReminderState>(\n        builder: (context, state) {\n          final formattedDate = appearance.dateFormat\n              .formatDate(parsedDate!, _includeTime, appearance.timeFormat);\n\n          final options = DatePickerOptions(\n            focusedDay: parsedDate,\n            selectedDay: parsedDate,\n            includeTime: _includeTime,\n            dateFormat: appearance.dateFormat,\n            timeFormat: appearance.timeFormat,\n            selectedReminderOption: _reminderOption,\n            onIncludeTimeChanged: (includeTime, dateTime, _) {\n              _includeTime = includeTime;\n\n              if (_reminderOption != ReminderOption.none) {\n                _updateReminder(\n                  widget.reminderOption,\n                  context,\n                  includeTime,\n                );\n              } else if (dateTime != null) {\n                parsedDate = dateTime;\n                _updateBlock(\n                  dateTime,\n                  includeTime: includeTime,\n                );\n              }\n            },\n            onDaySelected: (selectedDay) {\n              parsedDate = selectedDay;\n\n              if (_reminderOption != ReminderOption.none) {\n                _updateReminder(\n                  _reminderOption,\n                  context,\n                  _includeTime,\n                );\n              } else {\n                final rootContext = widget.editorState.document.root.context;\n                if (rootContext != null && _reminderId != null) {\n                  rootContext.read<ReminderBloc?>()?.add(\n                        ReminderEvent.removeReminder(reminderId: _reminderId!),\n                      );\n                }\n                _updateBlock(selectedDay, includeTime: _includeTime);\n              }\n            },\n            onReminderSelected: (reminderOption) {\n              _reminderOption = reminderOption;\n              _updateReminder(reminderOption, context, _includeTime);\n            },\n          );\n\n          Color? color;\n          final reminder = getReminder(context);\n          if (reminder != null) {\n            if (reminder.type == ReminderType.today) {\n              color = Theme.of(context).isLightMode\n                  ? const Color(0xFFFE0299)\n                  : Theme.of(context).colorScheme.error;\n            }\n          }\n          final textStyle = widget.textStyle?.copyWith(\n            color: color,\n            leadingDistribution: TextLeadingDistribution.even,\n          );\n\n          // when font size equals 14, the icon size is 16.0.\n          // scale the icon size based on the font size.\n          final iconSize = (widget.textStyle?.fontSize ?? 14.0) / 14.0 * 16.0;\n\n          return GestureDetector(\n            onTapDown: (details) {\n              _showDatePicker(\n                context: context,\n                offset: details.globalPosition,\n                options: options,\n              );\n            },\n            child: MouseRegion(\n              cursor: SystemMouseCursors.click,\n              child: Wrap(\n                crossAxisAlignment: WrapCrossAlignment.center,\n                children: [\n                  Text(\n                    '@$formattedDate',\n                    style: textStyle,\n                    strutStyle: textStyle != null\n                        ? StrutStyle.fromTextStyle(textStyle)\n                        : null,\n                  ),\n                  const HSpace(4),\n                  FlowySvg(\n                    _reminderId != null\n                        ? FlowySvgs.reminder_clock_s\n                        : FlowySvgs.date_s,\n                    size: Size.square(iconSize),\n                    color: textStyle?.color,\n                  ),\n                ],\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  void _updateBlock(\n    DateTime date, {\n    required bool includeTime,\n    String? reminderId,\n    ReminderOption? reminderOption,\n  }) {\n    final rId = reminderId ??\n        (reminderOption == ReminderOption.none ? null : _reminderId);\n\n    final transaction = widget.editorState.transaction\n      ..formatText(\n        widget.node,\n        widget.index,\n        1,\n        MentionBlockKeys.buildMentionDateAttributes(\n          date: date.toIso8601String(),\n          reminderId: rId,\n          includeTime: includeTime,\n          reminderOption: reminderOption?.name ?? widget.reminderOption.name,\n        ),\n      );\n\n    widget.editorState.apply(transaction, withUpdateSelection: false);\n\n    // Length of rendered block changes, this synchronizes\n    // the cursor with the new block render\n    widget.editorState.updateSelectionWithReason(\n      widget.editorState.selection,\n    );\n  }\n\n  void _updateReminder(\n    ReminderOption reminderOption,\n    BuildContext context, [\n    bool includeTime = false,\n  ]) {\n    final rootContext = widget.editorState.document.root.context;\n    if (parsedDate == null || rootContext == null) {\n      return;\n    }\n    final reminder = getReminder(rootContext);\n    if (reminder != null) {\n      _updateBlock(\n        parsedDate!,\n        includeTime: includeTime,\n        reminderOption: reminderOption,\n      );\n\n      if (ReminderOption.none == reminderOption) {\n        // Delete existing reminder\n        return rootContext\n            .read<ReminderBloc>()\n            .add(ReminderEvent.removeReminder(reminderId: reminder.id));\n      }\n\n      // Update existing reminder\n      return rootContext.read<ReminderBloc>().add(\n            ReminderEvent.update(\n              ReminderUpdate(\n                id: reminder.id,\n                scheduledAt:\n                    reminderOption.getNotificationDateTime(parsedDate!),\n                date: parsedDate!,\n              ),\n            ),\n          );\n    }\n\n    _reminderId ??= nanoid();\n    final reminderId = _reminderId;\n    _updateBlock(\n      parsedDate!,\n      includeTime: includeTime,\n      reminderId: reminderId,\n      reminderOption: reminderOption,\n    );\n\n    // Add new reminder\n    final viewId = rootContext.read<DocumentBloc>().documentId;\n    return rootContext.read<ReminderBloc>().add(\n          ReminderEvent.add(\n            reminder: ReminderPB(\n              id: reminderId,\n              objectId: viewId,\n              title: LocaleKeys.reminderNotification_title.tr(),\n              message: LocaleKeys.reminderNotification_message.tr(),\n              meta: {\n                ReminderMetaKeys.includeTime: false.toString(),\n                ReminderMetaKeys.blockId: widget.node.id,\n                ReminderMetaKeys.createdAt:\n                    DateTime.now().millisecondsSinceEpoch.toString(),\n              },\n              scheduledAt: Int64(parsedDate!.millisecondsSinceEpoch ~/ 1000),\n              isAck: parsedDate!.isBefore(DateTime.now()),\n            ),\n          ),\n        );\n  }\n\n  void _showDatePicker({\n    required BuildContext context,\n    required DatePickerOptions options,\n    required Offset offset,\n  }) {\n    if (!widget.editorState.editable) {\n      return;\n    }\n    if (UniversalPlatform.isMobile) {\n      SystemChannels.textInput.invokeMethod('TextInput.hide');\n\n      showMobileBottomSheet(\n        context,\n        builder: (_) => DraggableScrollableSheet(\n          expand: false,\n          snap: true,\n          initialChildSize: 0.7,\n          minChildSize: 0.4,\n          snapSizes: const [0.4, 0.7, 1.0],\n          builder: (_, controller) => _DatePickerBottomSheet(\n            controller: controller,\n            parsedDate: parsedDate,\n            options: options,\n            includeTime: _includeTime,\n            reminderOption: widget.reminderOption,\n            onReminderSelected: (option) => _updateReminder(option, context),\n          ),\n        ),\n      );\n    } else {\n      DatePickerMenu(\n        context: context,\n        editorState: widget.editorState,\n      ).show(offset, options: options);\n    }\n  }\n}\n\nclass _DatePickerBottomSheet extends StatelessWidget {\n  const _DatePickerBottomSheet({\n    required this.controller,\n    required this.parsedDate,\n    required this.options,\n    required this.includeTime,\n    required this.reminderOption,\n    required this.onReminderSelected,\n  });\n\n  final ScrollController controller;\n  final DateTime? parsedDate;\n  final DatePickerOptions options;\n  final bool includeTime;\n  final ReminderOption reminderOption;\n  final void Function(ReminderOption) onReminderSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      color: Theme.of(context).colorScheme.secondaryContainer,\n      child: ListView(\n        controller: controller,\n        children: [\n          ColoredBox(\n            color: Theme.of(context).colorScheme.surface,\n            child: const Center(child: DragHandle()),\n          ),\n          const MobileDateHeader(),\n          MobileAppFlowyDatePicker(\n            dateTime: parsedDate,\n            includeTime: includeTime,\n            isRange: options.isRange,\n            dateFormat: options.dateFormat.simplified,\n            timeFormat: options.timeFormat.simplified,\n            reminderOption: reminderOption,\n            onDaySelected: options.onDaySelected,\n            onIncludeTimeChanged: options.onIncludeTimeChanged,\n            onReminderSelected: onReminderSelected,\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart",
    "content": "import 'dart:async';\nimport 'dart:math';\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'mention_link_error_preview.dart';\nimport 'mention_link_preview.dart';\n\nclass MentionLinkBlock extends StatefulWidget {\n  const MentionLinkBlock({\n    super.key,\n    required this.url,\n    required this.editorState,\n    required this.node,\n    required this.index,\n    this.delayToShow = const Duration(milliseconds: 50),\n    this.delayToHide = const Duration(milliseconds: 300),\n  });\n\n  final String url;\n  final Duration delayToShow;\n  final Duration delayToHide;\n  final EditorState editorState;\n  final Node node;\n  final int index;\n\n  @override\n  State<MentionLinkBlock> createState() => _MentionLinkBlockState();\n}\n\nclass _MentionLinkBlockState extends State<MentionLinkBlock> {\n  final parser = LinkParser();\n  _LoadingStatus status = _LoadingStatus.loading;\n  late LinkInfo linkInfo = LinkInfo(url: url);\n  final previewController = PopoverController();\n  bool isHovering = false;\n  int previewFocusNum = 0;\n  bool isPreviewHovering = false;\n  bool showAtBottom = false;\n  final key = GlobalKey();\n\n  bool get isPreviewShowing => previewFocusNum > 0;\n  String get url => widget.url;\n\n  EditorState get editorState => widget.editorState;\n\n  bool get editable => editorState.editable;\n\n  Node get node => widget.node;\n\n  int get index => widget.index;\n\n  bool get readyForPreview =>\n      status == _LoadingStatus.idle && !linkInfo.isEmpty();\n\n  @override\n  void initState() {\n    super.initState();\n\n    parser.addLinkInfoListener((v) {\n      final hasNewInfo = !v.isEmpty(), hasOldInfo = !linkInfo.isEmpty();\n      if (mounted) {\n        setState(() {\n          if (hasNewInfo) {\n            linkInfo = v;\n            status = _LoadingStatus.idle;\n          } else if (!hasOldInfo) {\n            status = _LoadingStatus.error;\n          }\n        });\n      }\n    });\n    parser.start(url);\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n    parser.dispose();\n    previewController.close();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final child = buildIconWithTitle(context);\n\n    if (UniversalPlatform.isMobile) return child;\n\n    return AppFlowyPopover(\n      key: ValueKey(showAtBottom),\n      controller: previewController,\n      direction: showAtBottom\n          ? PopoverDirection.bottomWithLeftAligned\n          : PopoverDirection.topWithLeftAligned,\n      offset: Offset(0, showAtBottom ? -20 : 20),\n      onOpen: () {\n        keepEditorFocusNotifier.increase();\n        previewFocusNum++;\n      },\n      onClose: () {\n        keepEditorFocusNotifier.decrease();\n        previewFocusNum--;\n      },\n      decorationColor: Colors.transparent,\n      popoverDecoration: BoxDecoration(),\n      margin: EdgeInsets.zero,\n      constraints: getConstraints(),\n      borderRadius: BorderRadius.circular(16),\n      popupBuilder: (context) => readyForPreview\n          ? MentionLinkPreview(\n              linkInfo: linkInfo,\n              editable: editable,\n              showAtBottom: showAtBottom,\n              triggerSize: getSizeFromKey(),\n              onEnter: (e) {\n                isPreviewHovering = true;\n              },\n              onExit: (e) {\n                isPreviewHovering = false;\n                tryToDismissPreview();\n              },\n              onCopyLink: () => copyLink(context),\n              onConvertTo: (s) => convertTo(s),\n              onRemoveLink: removeLink,\n              onOpenLink: openLink,\n            )\n          : MentionLinkErrorPreview(\n              url: url,\n              editable: editable,\n              triggerSize: getSizeFromKey(),\n              onEnter: (e) {\n                isPreviewHovering = true;\n              },\n              onExit: (e) {\n                isPreviewHovering = false;\n                tryToDismissPreview();\n              },\n              onCopyLink: () => copyLink(context),\n              onConvertTo: (s) => convertTo(s),\n              onRemoveLink: removeLink,\n              onOpenLink: openLink,\n            ),\n      child: MouseRegion(\n        cursor: SystemMouseCursors.click,\n        onEnter: onEnter,\n        onExit: onExit,\n        child: child,\n      ),\n    );\n  }\n\n  Widget buildIconWithTitle(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final siteName = linkInfo.siteName, linkTitle = linkInfo.title ?? url;\n\n    return GestureDetector(\n      onTap: () async {\n        await afLaunchUrlString(url, addingHttpSchemeWhenFailed: true);\n      },\n      child: FlowyHoverContainer(\n        style: HoverStyle(hoverColor: Theme.of(context).colorScheme.secondary),\n        applyStyle: isHovering,\n        child: Row(\n          mainAxisSize: MainAxisSize.min,\n          key: key,\n          children: [\n            HSpace(2),\n            buildIcon(),\n            HSpace(4),\n            Flexible(\n              child: RichText(\n                overflow: TextOverflow.ellipsis,\n                text: TextSpan(\n                  children: [\n                    if (siteName != null) ...[\n                      TextSpan(\n                        text: siteName,\n                        style: theme.textStyle.body\n                            .standard(color: theme.textColorScheme.secondary),\n                      ),\n                      WidgetSpan(child: HSpace(2)),\n                    ],\n                    TextSpan(\n                      text: linkTitle,\n                      style: theme.textStyle.body\n                          .standard(color: theme.textColorScheme.primary),\n                    ),\n                  ],\n                ),\n              ),\n            ),\n            HSpace(2),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget buildIcon() {\n    const defaultWidget = FlowySvg(FlowySvgs.toolbar_link_earth_m);\n    Widget icon = defaultWidget;\n    if (status == _LoadingStatus.loading) {\n      icon = Padding(\n        padding: const EdgeInsets.all(2.0),\n        child: const CircularProgressIndicator(strokeWidth: 1),\n      );\n    } else {\n      icon = linkInfo.buildIconWidget();\n    }\n    return SizedBox(\n      height: 20,\n      width: 20,\n      child: icon,\n    );\n  }\n\n  RenderBox? get box => key.currentContext?.findRenderObject() as RenderBox?;\n\n  Size getSizeFromKey() => box?.size ?? Size.zero;\n\n  Future<void> copyLink(BuildContext context) async {\n    await context.copyLink(url);\n    previewController.close();\n  }\n\n  Future<void> openLink() async {\n    await afLaunchUrlString(url, addingHttpSchemeWhenFailed: true);\n  }\n\n  Future<void> removeLink() async {\n    final transaction = editorState.transaction\n      ..replaceText(widget.node, widget.index, 1, url, attributes: {});\n    await editorState.apply(transaction);\n  }\n\n  Future<void> convertTo(PasteMenuType type) async {\n    if (type == PasteMenuType.url) {\n      await toUrl();\n    } else if (type == PasteMenuType.bookmark) {\n      await toLinkPreview();\n    } else if (type == PasteMenuType.embed) {\n      await toLinkPreview(previewType: LinkEmbedKeys.embed);\n    }\n  }\n\n  Future<void> toUrl() async {\n    final transaction = editorState.transaction\n      ..replaceText(\n        widget.node,\n        widget.index,\n        1,\n        url,\n        attributes: {\n          AppFlowyRichTextKeys.href: url,\n        },\n      );\n    await editorState.apply(transaction);\n  }\n\n  Future<void> toLinkPreview({String? previewType}) async {\n    final selection = Selection(\n      start: Position(path: node.path, offset: index),\n      end: Position(path: node.path, offset: index + 1),\n    );\n    await convertUrlToLinkPreview(\n      editorState,\n      selection,\n      url,\n      previewType: previewType,\n    );\n  }\n\n  void changeHovering(bool hovering) {\n    if (isHovering == hovering) return;\n    if (mounted) {\n      setState(() {\n        isHovering = hovering;\n      });\n    }\n  }\n\n  void changeShowAtBottom(bool bottom) {\n    if (showAtBottom == bottom) return;\n    if (mounted) {\n      setState(() {\n        showAtBottom = bottom;\n      });\n    }\n  }\n\n  void tryToDismissPreview() {\n    Future.delayed(widget.delayToHide, () {\n      if (isHovering || isPreviewHovering) {\n        return;\n      }\n      previewController.close();\n    });\n  }\n\n  void onEnter(PointerEnterEvent e) {\n    changeHovering(true);\n    final location = box?.localToGlobal(Offset.zero) ?? Offset.zero;\n    if (readyForPreview) {\n      if (location.dy < 300) {\n        changeShowAtBottom(true);\n      } else {\n        changeShowAtBottom(false);\n      }\n    }\n    Future.delayed(widget.delayToShow, () {\n      if (isHovering && !isPreviewShowing && status != _LoadingStatus.loading) {\n        showPreview();\n      }\n    });\n  }\n\n  void onExit(PointerExitEvent e) {\n    changeHovering(false);\n    tryToDismissPreview();\n  }\n\n  void showPreview() {\n    if (!mounted) return;\n    keepEditorFocusNotifier.increase();\n    previewController.show();\n    previewFocusNum++;\n  }\n\n  BoxConstraints getConstraints() {\n    final size = getSizeFromKey();\n    if (!readyForPreview) {\n      return BoxConstraints(\n        maxWidth: max(320, size.width),\n        maxHeight: 48 + size.height,\n      );\n    }\n    final hasImage = linkInfo.imageUrl?.isNotEmpty ?? false;\n    return BoxConstraints(\n      maxWidth: max(300, size.width),\n      maxHeight: hasImage ? 300 : 180,\n    );\n  }\n}\n\nenum _LoadingStatus {\n  loading,\n  idle,\n  error,\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart",
    "content": "import 'dart:math';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nclass MentionLinkErrorPreview extends StatefulWidget {\n  const MentionLinkErrorPreview({\n    super.key,\n    required this.url,\n    required this.onEnter,\n    required this.onExit,\n    required this.onCopyLink,\n    required this.onRemoveLink,\n    required this.onConvertTo,\n    required this.onOpenLink,\n    required this.triggerSize,\n    required this.editable,\n  });\n\n  final String url;\n  final PointerEnterEventListener onEnter;\n  final PointerExitEventListener onExit;\n  final VoidCallback onCopyLink;\n  final VoidCallback onRemoveLink;\n  final VoidCallback onOpenLink;\n  final ValueChanged<PasteMenuType> onConvertTo;\n  final Size triggerSize;\n  final bool editable;\n\n  @override\n  State<MentionLinkErrorPreview> createState() =>\n      _MentionLinkErrorPreviewState();\n}\n\nclass _MentionLinkErrorPreviewState extends State<MentionLinkErrorPreview> {\n  final menuController = PopoverController();\n  bool isConvertButtonSelected = false;\n\n  @override\n  void dispose() {\n    super.dispose();\n    menuController.close();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        MouseRegion(\n          onEnter: widget.onEnter,\n          onExit: widget.onExit,\n          child: SizedBox(\n            width: max(320, widget.triggerSize.width),\n            height: 48,\n            child: Align(\n              alignment: Alignment.centerLeft,\n              child: Container(\n                width: 320,\n                height: 48,\n                decoration: buildToolbarLinkDecoration(context),\n                padding: EdgeInsets.fromLTRB(12, 8, 8, 8),\n                child: Row(\n                  children: [\n                    Expanded(child: buildLinkWidget()),\n                    Container(\n                      height: 20,\n                      width: 1,\n                      color: Color(0xffE8ECF3)\n                          .withAlpha(Theme.of(context).isLightMode ? 255 : 40),\n                      margin: EdgeInsets.symmetric(horizontal: 6),\n                    ),\n                    FlowyIconButton(\n                      icon: FlowySvg(FlowySvgs.toolbar_link_m),\n                      tooltipText: LocaleKeys.editor_copyLink.tr(),\n                      preferBelow: false,\n                      width: 36,\n                      height: 32,\n                      onPressed: widget.onCopyLink,\n                    ),\n                    buildConvertButton(),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        ),\n        MouseRegion(\n          cursor: SystemMouseCursors.click,\n          onEnter: widget.onEnter,\n          onExit: widget.onExit,\n          child: GestureDetector(\n            onTap: widget.onOpenLink,\n            child: Container(\n              width: widget.triggerSize.width,\n              height: widget.triggerSize.height,\n              color: Colors.black.withAlpha(1),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget buildLinkWidget() {\n    final url = widget.url;\n    return FlowyTooltip(\n      message: url,\n      preferBelow: false,\n      child: FlowyText.regular(\n        url,\n        overflow: TextOverflow.ellipsis,\n        figmaLineHeight: 20,\n        fontSize: 14,\n      ),\n    );\n  }\n\n  Widget buildConvertButton() {\n    return AppFlowyPopover(\n      offset: Offset(8, 10),\n      direction: PopoverDirection.bottomWithRightAligned,\n      margin: EdgeInsets.zero,\n      controller: menuController,\n      onOpen: () => keepEditorFocusNotifier.increase(),\n      onClose: () => keepEditorFocusNotifier.decrease(),\n      popupBuilder: (context) => buildConvertMenu(),\n      child: FlowyIconButton(\n        icon: FlowySvg(FlowySvgs.turninto_m),\n        isSelected: isConvertButtonSelected,\n        tooltipText: LocaleKeys.editor_convertTo.tr(),\n        preferBelow: false,\n        width: 36,\n        height: 32,\n        onPressed: () {\n          setState(() {\n            isConvertButtonSelected = true;\n          });\n          showPopover();\n        },\n      ),\n    );\n  }\n\n  Widget buildConvertMenu() {\n    return MouseRegion(\n      onEnter: widget.onEnter,\n      onExit: widget.onExit,\n      child: Padding(\n        padding: const EdgeInsets.all(8.0),\n        child: SeparatedColumn(\n          mainAxisSize: MainAxisSize.min,\n          separatorBuilder: () => const VSpace(0.0),\n          children: List.generate(MentionLinktErrorMenuCommand.values.length,\n              (index) {\n            final command = MentionLinktErrorMenuCommand.values[index];\n            return SizedBox(\n              height: 36,\n              child: FlowyButton(\n                text: FlowyText(\n                  command.title,\n                  fontWeight: FontWeight.w400,\n                  figmaLineHeight: 20,\n                ),\n                onTap: () => onTap(command),\n              ),\n            );\n          }),\n        ),\n      ),\n    );\n  }\n\n  void showPopover() {\n    keepEditorFocusNotifier.increase();\n    menuController.show();\n  }\n\n  void closePopover() {\n    menuController.close();\n  }\n\n  void onTap(MentionLinktErrorMenuCommand command) {\n    switch (command) {\n      case MentionLinktErrorMenuCommand.toURL:\n        widget.onConvertTo(PasteMenuType.url);\n        break;\n      case MentionLinktErrorMenuCommand.toBookmark:\n        widget.onConvertTo(PasteMenuType.bookmark);\n        break;\n      case MentionLinktErrorMenuCommand.toEmbed:\n        widget.onConvertTo(PasteMenuType.embed);\n        break;\n      case MentionLinktErrorMenuCommand.removeLink:\n        widget.onRemoveLink();\n        break;\n    }\n    closePopover();\n  }\n}\n\nenum MentionLinktErrorMenuCommand {\n  toURL,\n  toBookmark,\n  toEmbed,\n  removeLink;\n\n  String get title {\n    switch (this) {\n      case toURL:\n        return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl\n            .tr();\n      case toBookmark:\n        return LocaleKeys\n            .document_plugins_linkPreview_linkPreviewMenu_toBookmark\n            .tr();\n      case toEmbed:\n        return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toEmbed\n            .tr();\n      case removeLink:\n        return LocaleKeys\n            .document_plugins_linkPreview_linkPreviewMenu_removeLink\n            .tr();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nclass MentionLinkPreview extends StatefulWidget {\n  const MentionLinkPreview({\n    super.key,\n    required this.linkInfo,\n    required this.onEnter,\n    required this.onExit,\n    required this.onCopyLink,\n    required this.onRemoveLink,\n    required this.onConvertTo,\n    required this.onOpenLink,\n    required this.triggerSize,\n    required this.showAtBottom,\n    required this.editable,\n  });\n\n  final LinkInfo linkInfo;\n  final PointerEnterEventListener onEnter;\n  final PointerExitEventListener onExit;\n  final VoidCallback onCopyLink;\n  final VoidCallback onRemoveLink;\n  final VoidCallback onOpenLink;\n  final ValueChanged<PasteMenuType> onConvertTo;\n  final Size triggerSize;\n  final bool showAtBottom;\n  final bool editable;\n\n  @override\n  State<MentionLinkPreview> createState() => _MentionLinkPreviewState();\n}\n\nclass _MentionLinkPreviewState extends State<MentionLinkPreview> {\n  final menuController = PopoverController();\n  bool isSelected = false;\n\n  LinkInfo get linkInfo => widget.linkInfo;\n\n  bool get editable => widget.editable;\n\n  @override\n  void dispose() {\n    super.dispose();\n    menuController.close();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context),\n        textColorScheme = theme.textColorScheme;\n    final imageUrl = linkInfo.imageUrl ?? '',\n        description = linkInfo.description ?? '';\n    final imageHeight = 120.0;\n    final card = MouseRegion(\n      onEnter: widget.onEnter,\n      onExit: widget.onExit,\n      child: Container(\n        decoration: buildToolbarLinkDecoration(context, radius: 16),\n        width: 280,\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            if (imageUrl.isNotEmpty)\n              ClipRRect(\n                borderRadius:\n                    const BorderRadius.vertical(top: Radius.circular(16)),\n                child: FlowyNetworkImage(\n                  url: linkInfo.imageUrl ?? '',\n                  width: 280,\n                  height: imageHeight,\n                ),\n              ),\n            VSpace(12),\n            Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 16),\n              child: FlowyText.semibold(\n                linkInfo.title ?? linkInfo.siteName ?? '',\n                fontSize: 14,\n                figmaLineHeight: 20,\n                color: textColorScheme.primary,\n                overflow: TextOverflow.ellipsis,\n              ),\n            ),\n            VSpace(4),\n            if (description.isNotEmpty) ...[\n              Padding(\n                padding: const EdgeInsets.symmetric(horizontal: 16),\n                child: FlowyText(\n                  description,\n                  fontSize: 12,\n                  figmaLineHeight: 16,\n                  color: textColorScheme.secondary,\n                  maxLines: 3,\n                  overflow: TextOverflow.ellipsis,\n                ),\n              ),\n              VSpace(36),\n            ],\n            Container(\n              margin: const EdgeInsets.symmetric(horizontal: 16),\n              height: 28,\n              child: Row(\n                children: [\n                  linkInfo.buildIconWidget(size: Size.square(16)),\n                  HSpace(6),\n                  Expanded(\n                    child: FlowyText(\n                      linkInfo.siteName ?? linkInfo.url,\n                      fontSize: 12,\n                      figmaLineHeight: 16,\n                      color: textColorScheme.primary,\n                      overflow: TextOverflow.ellipsis,\n                      fontWeight: FontWeight.w700,\n                    ),\n                  ),\n                  buildMoreOptionButton(),\n                ],\n              ),\n            ),\n            VSpace(12),\n          ],\n        ),\n      ),\n    );\n\n    final clickPlaceHolder = MouseRegion(\n      cursor: SystemMouseCursors.click,\n      onEnter: widget.onEnter,\n      onExit: widget.onExit,\n      child: GestureDetector(\n        child: Container(\n          height: 20,\n          width: widget.triggerSize.width,\n          color: Colors.white.withAlpha(1),\n        ),\n        onTap: () {\n          widget.onOpenLink.call();\n          closePopover();\n        },\n      ),\n    );\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      mainAxisSize: MainAxisSize.min,\n      children: widget.showAtBottom\n          ? [clickPlaceHolder, card]\n          : [card, clickPlaceHolder],\n    );\n  }\n\n  Widget buildMoreOptionButton() {\n    return AppFlowyPopover(\n      controller: menuController,\n      direction: PopoverDirection.topWithLeftAligned,\n      onOpen: () => keepEditorFocusNotifier.increase(),\n      onClose: () => keepEditorFocusNotifier.decrease(),\n      margin: EdgeInsets.zero,\n      borderRadius: BorderRadius.circular(12),\n      popupBuilder: (context) => buildConvertMenu(),\n      child: FlowyIconButton(\n        width: 28,\n        height: 28,\n        isSelected: isSelected,\n        hoverColor: EditorStyleCustomizer.toolbarHoverColor(context),\n        icon: FlowySvg(\n          FlowySvgs.toolbar_more_m,\n          size: Size.square(20),\n        ),\n        onPressed: () {\n          setState(() {\n            isSelected = true;\n          });\n          showPopover();\n        },\n      ),\n    );\n  }\n\n  Widget buildConvertMenu() {\n    return MouseRegion(\n      onEnter: widget.onEnter,\n      onExit: widget.onExit,\n      child: Padding(\n        padding: const EdgeInsets.all(8.0),\n        child: SeparatedColumn(\n          mainAxisSize: MainAxisSize.min,\n          separatorBuilder: () => const VSpace(0.0),\n          children:\n              List.generate(MentionLinktMenuCommand.values.length, (index) {\n            final command = MentionLinktMenuCommand.values[index];\n            final isCopyCommand = command == MentionLinktMenuCommand.copyLink;\n            final enableButton = editable || (!editable && isCopyCommand);\n            return SizedBox(\n              height: 36,\n              child: FlowyButton(\n                hoverColor: enableButton ? null : Colors.transparent,\n                text: FlowyText(\n                  command.title,\n                  fontWeight: FontWeight.w400,\n                  figmaLineHeight: 20,\n                ),\n                onTap: enableButton ? () => onTap(command) : null,\n              ),\n            );\n          }),\n        ),\n      ),\n    );\n  }\n\n  void showPopover() {\n    keepEditorFocusNotifier.increase();\n    menuController.show();\n  }\n\n  void closePopover() {\n    menuController.close();\n  }\n\n  void onTap(MentionLinktMenuCommand command) {\n    switch (command) {\n      case MentionLinktMenuCommand.toURL:\n        widget.onConvertTo(PasteMenuType.url);\n        break;\n      case MentionLinktMenuCommand.toBookmark:\n        widget.onConvertTo(PasteMenuType.bookmark);\n        break;\n      case MentionLinktMenuCommand.toEmbed:\n        widget.onConvertTo(PasteMenuType.embed);\n        break;\n      case MentionLinktMenuCommand.copyLink:\n        widget.onCopyLink();\n        break;\n      case MentionLinktMenuCommand.removeLink:\n        widget.onRemoveLink();\n        break;\n    }\n    closePopover();\n  }\n}\n\nenum MentionLinktMenuCommand {\n  toURL,\n  toBookmark,\n  toEmbed,\n  copyLink,\n  removeLink;\n\n  String get title {\n    switch (this) {\n      case toURL:\n        return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl\n            .tr();\n      case toBookmark:\n        return LocaleKeys\n            .document_plugins_linkPreview_linkPreviewMenu_toBookmark\n            .tr();\n      case toEmbed:\n        return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toEmbed\n            .tr();\n      case copyLink:\n        return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_copyLink\n            .tr();\n      case removeLink:\n        return LocaleKeys\n            .document_plugins_linkPreview_linkPreviewMenu_removeLink\n            .tr();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_bloc.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';\nimport 'package:appflowy/plugins/document/application/document_listener.dart';\nimport 'package:appflowy/plugins/document/application/document_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/delta/text_delta_extension.dart';\nimport 'package:appflowy/plugins/trash/application/prelude.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'mention_page_bloc.freezed.dart';\n\ntypedef MentionPageStatus = (ViewPB? view, bool isInTrash, bool isDeleted);\n\nclass MentionPageBloc extends Bloc<MentionPageEvent, MentionPageState> {\n  MentionPageBloc({\n    required this.pageId,\n    this.blockId,\n    bool isSubPage = false,\n  })  : _isSubPage = isSubPage,\n        super(MentionPageState.initial()) {\n    on<MentionPageEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            final (view, isInTrash, isDeleted) =\n                await ViewBackendService.getMentionPageStatus(pageId);\n\n            String? blockContent;\n            if (!_isSubPage) {\n              blockContent = await _getBlockContent();\n            }\n\n            emit(\n              state.copyWith(\n                view: view,\n                isLoading: false,\n                isInTrash: isInTrash,\n                isDeleted: isDeleted,\n                blockContent: blockContent ?? '',\n              ),\n            );\n\n            if (view != null) {\n              _startListeningView();\n              _startListeningTrash();\n\n              if (!_isSubPage) {\n                _startListeningDocument();\n              }\n            }\n          },\n          didUpdateViewStatus: (view, isDeleted) async {\n            emit(\n              state.copyWith(\n                view: view,\n                isDeleted: isDeleted ?? state.isDeleted,\n              ),\n            );\n          },\n          didUpdateTrashStatus: (isInTrash) async =>\n              emit(state.copyWith(isInTrash: isInTrash)),\n          didUpdateBlockContent: (content) {\n            emit(\n              state.copyWith(\n                blockContent: content,\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  @override\n  Future<void> close() {\n    _viewListener?.stop();\n    _trashListener?.close();\n    _documentListener?.stop();\n    return super.close();\n  }\n\n  final _documentService = DocumentService();\n\n  final String pageId;\n  final String? blockId;\n  final bool _isSubPage;\n\n  ViewListener? _viewListener;\n  TrashListener? _trashListener;\n\n  DocumentListener? _documentListener;\n  BlockPB? _block;\n  String? _blockTextId;\n  Delta? _initialDelta;\n\n  void _startListeningView() {\n    _viewListener = ViewListener(viewId: pageId)\n      ..start(\n        onViewUpdated: (view) => add(\n          MentionPageEvent.didUpdateViewStatus(view: view, isDeleted: false),\n        ),\n        onViewDeleted: (_) =>\n            add(const MentionPageEvent.didUpdateViewStatus(isDeleted: true)),\n      );\n  }\n\n  void _startListeningTrash() {\n    _trashListener = TrashListener()\n      ..start(\n        trashUpdated: (trashOrFailed) {\n          final trash = trashOrFailed.toNullable();\n          if (trash != null) {\n            final isInTrash = trash.any((t) => t.id == pageId);\n            if (!isClosed) {\n              add(MentionPageEvent.didUpdateTrashStatus(isInTrash: isInTrash));\n            }\n          }\n        },\n      );\n  }\n\n  Future<String> _convertDeltaToText(Delta? delta) async {\n    if (delta == null) {\n      return _initialDelta?.toPlainText() ?? '';\n    }\n\n    return delta.toText(\n      getMentionPageName: (mentionedPageId) async {\n        if (mentionedPageId == pageId) {\n          // if the mention page is the current page, return the view name\n          return state.view?.name ?? '';\n        } else {\n          // if the mention page is not the current page, return the mention page name\n          final viewResult = await ViewBackendService.getView(mentionedPageId);\n          final name = viewResult.fold((l) => l.name, (f) => '');\n          return name;\n        }\n      },\n    );\n  }\n\n  Future<String?> _getBlockContent() async {\n    if (blockId == null) {\n      return null;\n    }\n\n    final documentNodeResult = await _documentService.getDocumentNode(\n      documentId: pageId,\n      blockId: blockId!,\n    );\n    final documentNode = documentNodeResult.fold((l) => l, (f) => null);\n    if (documentNode == null) {\n      Log.error(\n        'unable to get the document node for block $blockId in page $pageId',\n      );\n      return null;\n    }\n\n    final block = documentNode.$2;\n    final node = documentNode.$3;\n\n    _blockTextId = (node.externalValues as ExternalValues?)?.externalId;\n    _initialDelta = node.delta;\n    _block = block;\n\n    return _convertDeltaToText(_initialDelta);\n  }\n\n  void _startListeningDocument() {\n    // only observe the block content if the block id is not null\n    if (blockId == null ||\n        _blockTextId == null ||\n        _initialDelta == null ||\n        _block == null) {\n      return;\n    }\n\n    _documentListener = DocumentListener(id: pageId)\n      ..start(\n        onDocEventUpdate: (docEvent) {\n          for (final block in docEvent.events) {\n            for (final event in block.event) {\n              if (event.id == _blockTextId) {\n                if (event.command == DeltaTypePB.Updated) {\n                  _updateBlockContent(event.value);\n                } else if (event.command == DeltaTypePB.Removed) {\n                  add(const MentionPageEvent.didUpdateBlockContent(''));\n                }\n              }\n            }\n          }\n        },\n      );\n  }\n\n  Future<void> _updateBlockContent(String deltaJson) async {\n    if (_initialDelta == null || _block == null) {\n      return;\n    }\n\n    try {\n      final incremental = Delta.fromJson(jsonDecode(deltaJson));\n      final delta = _initialDelta!.compose(incremental);\n      final content = await _convertDeltaToText(delta);\n      add(MentionPageEvent.didUpdateBlockContent(content));\n      _initialDelta = delta;\n    } catch (e) {\n      Log.error('failed to update block content: $e');\n    }\n  }\n}\n\n@freezed\nclass MentionPageEvent with _$MentionPageEvent {\n  const factory MentionPageEvent.initial() = _Initial;\n  const factory MentionPageEvent.didUpdateViewStatus({\n    @Default(null) ViewPB? view,\n    @Default(null) bool? isDeleted,\n  }) = _DidUpdateViewStatus;\n  const factory MentionPageEvent.didUpdateTrashStatus({\n    required bool isInTrash,\n  }) = _DidUpdateTrashStatus;\n  const factory MentionPageEvent.didUpdateBlockContent(\n    String content,\n  ) = _DidUpdateBlockContent;\n}\n\n@freezed\nclass MentionPageState with _$MentionPageState {\n  const factory MentionPageState({\n    required bool isLoading,\n    required bool isInTrash,\n    required bool isDeleted,\n    // non-null case:\n    // - page is found\n    // - page is in trash\n    // null case:\n    // - page is deleted\n    required ViewPB? view,\n    // the plain text content of the block\n    // it doesn't contain any formatting\n    required String blockContent,\n  }) = _MentionSubPageState;\n\n  factory MentionPageState.initial() => const MentionPageState(\n        isLoading: true,\n        isInTrash: false,\n        isDeleted: false,\n        view: null,\n        blockContent: '',\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart';\nimport 'package:appflowy/plugins/trash/application/trash_service.dart';\nimport 'package:appflowy/shared/clipboard_state.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';\nimport 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart'\n    show\n        ApplyOptions,\n        Delta,\n        EditorState,\n        Node,\n        NodeIterator,\n        Path,\n        Position,\n        Selection,\n        SelectionType,\n        TextInsert,\n        TextTransaction,\n        paragraphNode;\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/scheduler.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nfinal pageMemorizer = <String, ViewPB?>{};\n\nNode pageMentionNode(String viewId) {\n  return paragraphNode(\n    delta: Delta(\n      operations: [\n        TextInsert(\n          MentionBlockKeys.mentionChar,\n          attributes: MentionBlockKeys.buildMentionPageAttributes(\n            mentionType: MentionType.page,\n            pageId: viewId,\n            blockId: null,\n          ),\n        ),\n      ],\n    ),\n  );\n}\n\nclass ReferenceState {\n  ReferenceState(this.isReference);\n\n  final bool isReference;\n}\n\nclass MentionPageBlock extends StatefulWidget {\n  const MentionPageBlock({\n    super.key,\n    required this.editorState,\n    required this.pageId,\n    required this.blockId,\n    required this.node,\n    required this.textStyle,\n    required this.index,\n  });\n\n  final EditorState editorState;\n  final String pageId;\n  final String? blockId;\n  final Node node;\n  final TextStyle? textStyle;\n\n  // Used to update the block\n  final int index;\n\n  @override\n  State<MentionPageBlock> createState() => _MentionPageBlockState();\n}\n\nclass _MentionPageBlockState extends State<MentionPageBlock> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) => MentionPageBloc(\n        pageId: widget.pageId,\n        blockId: widget.blockId,\n      )..add(const MentionPageEvent.initial()),\n      child: BlocBuilder<MentionPageBloc, MentionPageState>(\n        builder: (context, state) {\n          final view = state.view;\n          if (state.isLoading) {\n            return const SizedBox.shrink();\n          }\n\n          if (state.isDeleted || view == null) {\n            return _NoAccessMentionPageBlock(\n              textStyle: widget.textStyle,\n            );\n          }\n\n          if (UniversalPlatform.isMobile) {\n            return _MobileMentionPageBlock(\n              view: view,\n              content: state.blockContent,\n              textStyle: widget.textStyle,\n              handleTap: () => handleMentionBlockTap(\n                context,\n                widget.editorState,\n                view,\n                blockId: widget.blockId,\n              ),\n              handleDoubleTap: () => _handleDoubleTap(\n                context,\n                widget.editorState,\n                view.id,\n                widget.node,\n                widget.index,\n              ),\n            );\n          } else {\n            return _DesktopMentionPageBlock(\n              view: view,\n              content: state.blockContent,\n              textStyle: widget.textStyle,\n              showTrashHint: state.isInTrash,\n              handleTap: () => handleMentionBlockTap(\n                context,\n                widget.editorState,\n                view,\n                blockId: widget.blockId,\n              ),\n            );\n          }\n        },\n      ),\n    );\n  }\n\n  void updateSelection() {\n    WidgetsBinding.instance.addPostFrameCallback(\n      (_) => widget.editorState\n          .updateSelectionWithReason(widget.editorState.selection),\n    );\n  }\n}\n\nclass MentionSubPageBlock extends StatefulWidget {\n  const MentionSubPageBlock({\n    super.key,\n    required this.editorState,\n    required this.pageId,\n    required this.node,\n    required this.textStyle,\n    required this.index,\n  });\n\n  final EditorState editorState;\n  final String pageId;\n  final Node node;\n  final TextStyle? textStyle;\n\n  // Used to update the block\n  final int index;\n\n  @override\n  State<MentionSubPageBlock> createState() => _MentionSubPageBlockState();\n}\n\nclass _MentionSubPageBlockState extends State<MentionSubPageBlock> {\n  late bool isHandlingPaste = context.read<ClipboardState>().isHandlingPaste;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) => MentionPageBloc(pageId: widget.pageId, isSubPage: true)\n        ..add(const MentionPageEvent.initial()),\n      child: BlocConsumer<MentionPageBloc, MentionPageState>(\n        listener: (context, state) async {\n          if (state.view != null) {\n            final currentViewId = getIt<MenuSharedState>().latestOpenView?.id;\n            if (currentViewId == null) {\n              return;\n            }\n\n            if (state.view!.parentViewId != currentViewId) {\n              SchedulerBinding.instance.addPostFrameCallback((_) {\n                if (context.mounted) {\n                  turnIntoPageRef();\n                }\n              });\n            }\n          }\n        },\n        builder: (context, state) {\n          final view = state.view;\n          if (state.isLoading || isHandlingPaste) {\n            return const SizedBox.shrink();\n          }\n\n          if (state.isDeleted || view == null) {\n            return _DeletedPageBlock(textStyle: widget.textStyle);\n          }\n\n          if (UniversalPlatform.isMobile) {\n            return _MobileMentionPageBlock(\n              view: view,\n              showTrashHint: state.isInTrash,\n              textStyle: widget.textStyle,\n              handleTap: () =>\n                  handleMentionBlockTap(context, widget.editorState, view),\n              isChildPage: true,\n              content: '',\n              handleDoubleTap: () => _handleDoubleTap(\n                context,\n                widget.editorState,\n                view.id,\n                widget.node,\n                widget.index,\n              ),\n            );\n          } else {\n            return _DesktopMentionPageBlock(\n              view: view,\n              showTrashHint: state.isInTrash,\n              content: null,\n              textStyle: widget.textStyle,\n              isChildPage: true,\n              handleTap: () =>\n                  handleMentionBlockTap(context, widget.editorState, view),\n            );\n          }\n        },\n      ),\n    );\n  }\n\n  Future<ViewPB?> fetchView(String pageId) async {\n    final view = await ViewBackendService.getView(pageId).then(\n      (value) => value.toNullable(),\n    );\n\n    if (view == null) {\n      // try to fetch from trash\n      final trashViews = await TrashService().readTrash();\n      final trash = trashViews.fold(\n        (l) => l.items.firstWhereOrNull((element) => element.id == pageId),\n        (r) => null,\n      );\n      if (trash != null) {\n        return ViewPB()\n          ..id = trash.id\n          ..name = trash.name;\n      }\n    }\n\n    return view;\n  }\n\n  void updateSelection() {\n    WidgetsBinding.instance.addPostFrameCallback(\n      (_) => widget.editorState\n          .updateSelectionWithReason(widget.editorState.selection),\n    );\n  }\n\n  void turnIntoPageRef() {\n    final transaction = widget.editorState.transaction\n      ..formatText(\n        widget.node,\n        widget.index,\n        MentionBlockKeys.mentionChar.length,\n        MentionBlockKeys.buildMentionPageAttributes(\n          mentionType: MentionType.page,\n          pageId: widget.pageId,\n          blockId: null,\n        ),\n      );\n\n    widget.editorState.apply(\n      transaction,\n      withUpdateSelection: false,\n      options: const ApplyOptions(\n        recordUndo: false,\n      ),\n    );\n  }\n}\n\nPath? _findNodePathByBlockId(EditorState editorState, String blockId) {\n  final document = editorState.document;\n  final startNode = document.root.children.firstOrNull;\n  if (startNode == null) {\n    return null;\n  }\n\n  final nodeIterator = NodeIterator(\n    document: document,\n    startNode: startNode,\n  );\n  while (nodeIterator.moveNext()) {\n    final node = nodeIterator.current;\n    if (node.id == blockId) {\n      return node.path;\n    }\n  }\n\n  return null;\n}\n\nFuture<void> handleMentionBlockTap(\n  BuildContext context,\n  EditorState editorState,\n  ViewPB view, {\n  String? blockId,\n}) async {\n  final currentViewId = context.read<DocumentBloc>().documentId;\n  if (currentViewId == view.id && blockId != null) {\n    // same page\n    final path = _findNodePathByBlockId(editorState, blockId);\n    if (path != null) {\n      editorState.scrollService?.jumpTo(path.first);\n      await editorState.updateSelectionWithReason(\n        Selection.collapsed(Position(path: path)),\n        customSelectionType: SelectionType.block,\n      );\n    }\n    return;\n  }\n\n  if (UniversalPlatform.isMobile) {\n    if (context.mounted && currentViewId != view.id) {\n      await context.pushView(\n        view,\n        blockId: blockId,\n        tabs: [\n          PickerTabType.emoji,\n          PickerTabType.icon,\n          PickerTabType.custom,\n        ].map((e) => e.name).toList(),\n      );\n    }\n  } else {\n    final action = NavigationAction(\n      objectId: view.id,\n      arguments: {\n        ActionArgumentKeys.view: view,\n        ActionArgumentKeys.blockId: blockId,\n      },\n    );\n    getIt<ActionNavigationBloc>().add(\n      ActionNavigationEvent.performAction(\n        action: action,\n      ),\n    );\n  }\n}\n\nFuture<void> _handleDoubleTap(\n  BuildContext context,\n  EditorState editorState,\n  String viewId,\n  Node node,\n  int index,\n) async {\n  if (!UniversalPlatform.isMobile) {\n    return;\n  }\n\n  final currentViewId = context.read<DocumentBloc>().documentId;\n  final newView = await showPageSelectorSheet(\n    context,\n    currentViewId: currentViewId,\n    selectedViewId: viewId,\n  );\n\n  if (newView != null) {\n    // Update this nodes pageId\n    final transaction = editorState.transaction\n      ..formatText(\n        node,\n        index,\n        1,\n        MentionBlockKeys.buildMentionPageAttributes(\n          mentionType: MentionType.page,\n          pageId: newView.id,\n          blockId: null,\n        ),\n      );\n\n    await editorState.apply(transaction, withUpdateSelection: false);\n  }\n}\n\nclass _MentionPageBlockContent extends StatelessWidget {\n  const _MentionPageBlockContent({\n    required this.view,\n    required this.textStyle,\n    this.content,\n    this.showTrashHint = false,\n    this.isChildPage = false,\n  });\n\n  final ViewPB view;\n  final TextStyle? textStyle;\n  final String? content;\n  final bool showTrashHint;\n  final bool isChildPage;\n\n  @override\n  Widget build(BuildContext context) {\n    final text = _getDisplayText(context, view, content);\n\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        ..._buildPrefixIcons(context, view, content, isChildPage),\n        const HSpace(4),\n        Flexible(\n          child: FlowyText(\n            text,\n            decoration: TextDecoration.underline,\n            fontSize: textStyle?.fontSize,\n            fontWeight: textStyle?.fontWeight,\n            lineHeight: textStyle?.height,\n            overflow: TextOverflow.ellipsis,\n          ),\n        ),\n        if (showTrashHint) ...[\n          FlowyText(\n            LocaleKeys.document_mention_trashHint.tr(),\n            fontSize: textStyle?.fontSize,\n            fontWeight: textStyle?.fontWeight,\n            lineHeight: textStyle?.height,\n            color: Theme.of(context).disabledColor,\n            decoration: TextDecoration.underline,\n            decorationColor: AFThemeExtension.of(context).textColor,\n          ),\n        ],\n        const HSpace(4),\n      ],\n    );\n  }\n\n  List<Widget> _buildPrefixIcons(\n    BuildContext context,\n    ViewPB view,\n    String? content,\n    bool isChildPage,\n  ) {\n    final isSameDocument = _isSameDocument(context, view.id);\n    final shouldDisplayViewName = _shouldDisplayViewName(\n      context,\n      view.id,\n      content,\n    );\n    final isBlockContentEmpty = content == null || content.isEmpty;\n    final emojiSize = textStyle?.fontSize ?? 12.0;\n    final iconSize = textStyle?.fontSize ?? 16.0;\n\n    // if the block is from the same doc, display the paragraph mark icon '¶'\n    if (isSameDocument && !isBlockContentEmpty) {\n      return [\n        const HSpace(2),\n        FlowySvg(\n          FlowySvgs.paragraph_mark_s,\n          size: Size.square(iconSize - 2.0),\n          color: Theme.of(context).hintColor,\n        ),\n      ];\n    } else if (shouldDisplayViewName) {\n      return [\n        const HSpace(4),\n        Stack(\n          children: [\n            view.icon.value.isNotEmpty\n                ? EmojiIconWidget(\n                    emoji: view.icon.toEmojiIconData(),\n                    emojiSize: emojiSize,\n                  )\n                : view.defaultIcon(size: Size.square(iconSize + 2.0)),\n            if (!isChildPage) ...[\n              const Positioned(\n                right: 0,\n                bottom: 0,\n                child: FlowySvg(\n                  FlowySvgs.referenced_page_s,\n                  blendMode: BlendMode.dstIn,\n                ),\n              ),\n            ],\n          ],\n        ),\n      ];\n    }\n\n    return [];\n  }\n\n  String _getDisplayText(\n    BuildContext context,\n    ViewPB view,\n    String? blockContent,\n  ) {\n    final shouldDisplayViewName = _shouldDisplayViewName(\n      context,\n      view.id,\n      blockContent,\n    );\n\n    if (blockContent == null || blockContent.isEmpty) {\n      return shouldDisplayViewName\n          ? view.name\n              .orDefault(LocaleKeys.menuAppHeader_defaultNewPageName.tr())\n          : '';\n    }\n\n    return shouldDisplayViewName\n        ? '${view.name} - $blockContent'\n        : blockContent;\n  }\n\n  // display the view name or not\n  // if the block is from the same doc,\n  // 1. block content is not empty, display the **block content only**.\n  // 2. block content is empty, display the **view name**.\n  // if the block is from another doc,\n  // 1. block content is not empty, display the **view name and block content**.\n  // 2. block content is empty, display the **view name**.\n  bool _shouldDisplayViewName(\n    BuildContext context,\n    String viewId,\n    String? blockContent,\n  ) {\n    if (_isSameDocument(context, viewId)) {\n      return blockContent == null || blockContent.isEmpty;\n    }\n    return true;\n  }\n\n  bool _isSameDocument(BuildContext context, String viewId) {\n    final currentViewId = context.read<DocumentBloc?>()?.documentId;\n    return viewId == currentViewId;\n  }\n}\n\nclass _NoAccessMentionPageBlock extends StatelessWidget {\n  const _NoAccessMentionPageBlock({required this.textStyle});\n\n  final TextStyle? textStyle;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyHover(\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 4),\n        child: FlowyText(\n          LocaleKeys.document_mention_noAccess.tr(),\n          color: Theme.of(context).disabledColor,\n          decoration: TextDecoration.underline,\n          fontSize: textStyle?.fontSize,\n          fontWeight: textStyle?.fontWeight,\n        ),\n      ),\n    );\n  }\n}\n\nclass _DeletedPageBlock extends StatelessWidget {\n  const _DeletedPageBlock({required this.textStyle});\n\n  final TextStyle? textStyle;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyHover(\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 4),\n        child: FlowyText(\n          LocaleKeys.document_mention_deletedPage.tr(),\n          color: Theme.of(context).disabledColor,\n          decoration: TextDecoration.underline,\n          fontSize: textStyle?.fontSize,\n          fontWeight: textStyle?.fontWeight,\n        ),\n      ),\n    );\n  }\n}\n\nclass _MobileMentionPageBlock extends StatelessWidget {\n  const _MobileMentionPageBlock({\n    required this.view,\n    required this.content,\n    required this.textStyle,\n    required this.handleTap,\n    required this.handleDoubleTap,\n    this.showTrashHint = false,\n    this.isChildPage = false,\n  });\n\n  final TextStyle? textStyle;\n  final ViewPB view;\n  final String content;\n  final VoidCallback handleTap;\n  final VoidCallback handleDoubleTap;\n  final bool showTrashHint;\n  final bool isChildPage;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onTap: handleTap,\n      onDoubleTap: handleDoubleTap,\n      behavior: HitTestBehavior.opaque,\n      child: _MentionPageBlockContent(\n        view: view,\n        content: content,\n        textStyle: textStyle,\n        showTrashHint: showTrashHint,\n        isChildPage: isChildPage,\n      ),\n    );\n  }\n}\n\nclass _DesktopMentionPageBlock extends StatelessWidget {\n  const _DesktopMentionPageBlock({\n    required this.view,\n    required this.textStyle,\n    required this.handleTap,\n    required this.content,\n    this.showTrashHint = false,\n    this.isChildPage = false,\n  });\n\n  final TextStyle? textStyle;\n  final ViewPB view;\n  final String? content;\n  final VoidCallback handleTap;\n  final bool showTrashHint;\n  final bool isChildPage;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onTap: handleTap,\n      behavior: HitTestBehavior.opaque,\n      child: FlowyHover(\n        cursor: SystemMouseCursors.click,\n        child: _MentionPageBlockContent(\n          view: view,\n          content: content,\n          textStyle: textStyle,\n          showTrashHint: showTrashHint,\n          isChildPage: isChildPage,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/flowy_search_text_field.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\n\nFuture<ViewPB?> showPageSelectorSheet(\n  BuildContext context, {\n  String? currentViewId,\n  String? selectedViewId,\n  bool Function(ViewPB view)? filter,\n}) async {\n  filter ??= (v) => !v.isSpace && v.parentViewId.isNotEmpty;\n\n  return showMobileBottomSheet<ViewPB>(\n    context,\n    title: LocaleKeys.document_mobilePageSelector_title.tr(),\n    showHeader: true,\n    showCloseButton: true,\n    showDragHandle: true,\n    useSafeArea: false,\n    backgroundColor: Theme.of(context).colorScheme.surface,\n    builder: (context) => ConstrainedBox(\n      constraints: const BoxConstraints(\n        maxHeight: 340,\n        minHeight: 80,\n      ),\n      child: _MobilePageSelectorBody(\n        currentViewId: currentViewId,\n        selectedViewId: selectedViewId,\n        filter: filter,\n      ),\n    ),\n  );\n}\n\nclass _MobilePageSelectorBody extends StatefulWidget {\n  const _MobilePageSelectorBody({\n    this.currentViewId,\n    this.selectedViewId,\n    this.filter,\n  });\n\n  final String? currentViewId;\n  final String? selectedViewId;\n  final bool Function(ViewPB view)? filter;\n\n  @override\n  State<_MobilePageSelectorBody> createState() =>\n      _MobilePageSelectorBodyState();\n}\n\nclass _MobilePageSelectorBodyState extends State<_MobilePageSelectorBody> {\n  final searchController = TextEditingController();\n  late final Future<List<ViewPB>> _viewsFuture = _fetchViews();\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        Container(\n          margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),\n          height: 44.0,\n          child: FlowySearchTextField(\n            controller: searchController,\n            onChanged: (_) => setState(() {}),\n          ),\n        ),\n        FutureBuilder(\n          future: _viewsFuture,\n          builder: (_, snapshot) {\n            if (snapshot.connectionState == ConnectionState.waiting) {\n              return const Center(child: CircularProgressIndicator.adaptive());\n            }\n\n            if (snapshot.hasError || snapshot.data == null) {\n              return Center(\n                child: FlowyText(\n                  LocaleKeys.document_mobilePageSelector_failedToLoad.tr(),\n                ),\n              );\n            }\n\n            final views = snapshot.data!\n                .where((v) => widget.filter?.call(v) ?? true)\n                .toList();\n\n            if (widget.currentViewId != null) {\n              views.removeWhere((v) => v.id == widget.currentViewId);\n            }\n\n            final filtered = views.where(\n              (v) =>\n                  searchController.text.isEmpty ||\n                  v.name\n                      .toLowerCase()\n                      .contains(searchController.text.toLowerCase()),\n            );\n\n            if (filtered.isEmpty) {\n              return Center(\n                child: FlowyText(\n                  LocaleKeys.document_mobilePageSelector_noPagesFound.tr(),\n                ),\n              );\n            }\n\n            return Flexible(\n              child: ListView.builder(\n                itemCount: filtered.length,\n                itemBuilder: (context, index) {\n                  final view = filtered.elementAt(index);\n                  return FlowyOptionTile.checkbox(\n                    leftIcon: view.icon.value.isNotEmpty\n                        ? RawEmojiIconWidget(\n                            emoji: view.icon.toEmojiIconData(),\n                            emojiSize: 18,\n                          )\n                        : FlowySvg(\n                            view.layout.icon,\n                            size: const Size.square(20),\n                          ),\n                    text: view.name,\n                    showTopBorder: index != 0,\n                    showBottomBorder: false,\n                    isSelected: view.id == widget.selectedViewId,\n                    onTap: () => Navigator.of(context).pop(view),\n                  );\n                },\n              ),\n            );\n          },\n        ),\n      ],\n    );\n  }\n\n  Future<List<ViewPB>> _fetchViews() async =>\n      (await ViewBackendService.getAllViews()).toNullable()?.items ?? [];\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/menu/menu_extension.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nextension MenuExtension on EditorState {\n  MenuPosition? calculateMenuOffset({\n    Rect? rect,\n    required double menuWidth,\n    required double menuHeight,\n    Offset menuOffset = const Offset(0, 10),\n  }) {\n    final selectionService = service.selectionService;\n    final selectionRects = selectionService.selectionRects;\n    late Rect startRect;\n    if (rect != null) {\n      startRect = rect;\n    } else {\n      if (selectionRects.isEmpty) return null;\n      startRect = selectionRects.first;\n    }\n\n    final editorOffset = renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;\n    final editorHeight = renderBox!.size.height;\n    final editorWidth = renderBox!.size.width;\n\n    // show below default\n    Alignment alignment = Alignment.topLeft;\n    final bottomRight = startRect.bottomRight;\n    final topRight = startRect.topRight;\n    var startOffset = bottomRight + menuOffset;\n    Offset offset = Offset(\n      startOffset.dx,\n      startOffset.dy,\n    );\n\n    // show above\n    if (startOffset.dy + menuHeight >= editorOffset.dy + editorHeight) {\n      startOffset = topRight - menuOffset;\n      alignment = Alignment.bottomLeft;\n\n      offset = Offset(\n        startOffset.dx,\n        editorHeight + editorOffset.dy - startOffset.dy,\n      );\n    }\n\n    // show on right\n    if (offset.dx + menuWidth < editorOffset.dx + editorWidth) {\n      offset = Offset(\n        offset.dx,\n        offset.dy,\n      );\n    } else if (startOffset.dx - editorOffset.dx > menuWidth) {\n      // show on left\n      alignment = alignment == Alignment.topLeft\n          ? Alignment.topRight\n          : Alignment.bottomRight;\n\n      offset = Offset(\n        editorWidth - offset.dx + editorOffset.dx,\n        offset.dy,\n      );\n    }\n    return MenuPosition(align: alignment, offset: offset);\n  }\n}\n\nclass MenuPosition {\n  MenuPosition({\n    required this.align,\n    required this.offset,\n  });\n\n  final Alignment align;\n  final Offset offset;\n\n  LTRB get ltrb {\n    double? left, top, right, bottom;\n    switch (align) {\n      case Alignment.topLeft:\n        left = offset.dx;\n        top = offset.dy;\n        break;\n      case Alignment.bottomLeft:\n        left = offset.dx;\n        bottom = offset.dy;\n        break;\n      case Alignment.topRight:\n        right = offset.dx;\n        top = offset.dy;\n        break;\n      case Alignment.bottomRight:\n        right = offset.dx;\n        bottom = offset.dy;\n        break;\n    }\n\n    return LTRB(left: left, top: top, right: right, bottom: bottom);\n  }\n}\n\nclass LTRB {\n  LTRB({this.left, this.top, this.right, this.bottom});\n\n  final double? left;\n  final double? top;\n  final double? right;\n  final double? bottom;\n\n  Positioned buildPositioned({required Widget child}) => Positioned(\n        left: left,\n        top: top,\n        right: right,\n        bottom: bottom,\n        child: child,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart'\n    hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys;\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:collection/collection.dart';\nimport 'package:string_validator/string_validator.dart';\n\nclass EditorMigration {\n  // AppFlowy 0.1.x -> 0.2\n  //\n  // The cover node has been deprecated, and use page/attributes/cover instead.\n  // cover node -> page/attributes/cover\n  //\n  // mark the textNode deprecated. use paragraph node instead.\n  // text node -> paragraph node\n  // delta -> attributes/delta\n  //\n  // mark the subtype deprecated. use type instead.\n  // for example, text/checkbox -> checkbox_list\n  //\n  // some attribute keys.\n  // ...\n  static Document migrateDocument(String json) {\n    final map = jsonDecode(json);\n    assert(map['document'] != null);\n    final documentV0 = Map<String, Object>.from(map['document'] as Map);\n    final rootV0 = NodeV0.fromJson(documentV0);\n    final root = migrateNode(rootV0);\n    return Document(root: root);\n  }\n\n  static Node migrateNode(NodeV0 nodeV0) {\n    Node? node;\n    final children = nodeV0.children.map((e) => migrateNode(e)).toList();\n    final id = nodeV0.id;\n    if (id == 'editor') {\n      final coverNode = children.firstWhereOrNull(\n        (element) => element.id == 'cover',\n      );\n      if (coverNode != null) {\n        node = pageNode(\n          children: children,\n          attributes: coverNode.attributes,\n        );\n      } else {\n        node = pageNode(children: children);\n      }\n    } else if (id == 'callout') {\n      final icon = nodeV0.attributes[CalloutBlockKeys.icon] ?? '📌';\n      final iconType = nodeV0.attributes[CalloutBlockKeys.iconType] ??\n          FlowyIconType.emoji.name;\n      final delta =\n          nodeV0.children.whereType<TextNodeV0>().fold(Delta(), (p, e) {\n        final delta = migrateDelta(e.delta);\n        final textInserts = delta.whereType<TextInsert>();\n        for (final element in textInserts) {\n          p.add(element);\n        }\n        return p..insert('\\n');\n      });\n      EmojiIconData? emojiIconData;\n      try {\n        emojiIconData =\n            EmojiIconData(FlowyIconType.values.byName(iconType), icon);\n      } catch (e) {\n        Log.error(\n          'migrateNode get EmojiIconData error with :${nodeV0.attributes}',\n          e,\n        );\n      }\n      node = calloutNode(\n        emoji: emojiIconData,\n        delta: delta,\n      );\n    } else if (id == 'divider') {\n      // divider -> divider\n      node = dividerNode();\n    } else if (id == 'math_equation') {\n      // math_equation -> math_equation\n      final formula = nodeV0.attributes['math_equation'] ?? '';\n      node = mathEquationNode(formula: formula);\n    } else if (nodeV0 is TextNodeV0) {\n      final delta = migrateDelta(nodeV0.delta);\n      final deltaJson = delta.toJson();\n      final attributes = {'delta': deltaJson};\n      if (id == 'text') {\n        // text -> paragraph\n        node = paragraphNode(\n          attributes: attributes,\n          children: children,\n        );\n      } else if (nodeV0.id == 'text/heading') {\n        // text/heading -> heading\n        final heading = nodeV0.attributes.heading?.replaceAll('h', '');\n        final level = int.tryParse(heading ?? '') ?? 1;\n        node = headingNode(\n          level: level,\n          attributes: attributes,\n        );\n      } else if (id == 'text/checkbox') {\n        // text/checkbox -> todo_list\n        final checked = nodeV0.attributes.check;\n        node = todoListNode(\n          checked: checked,\n          attributes: attributes,\n          children: children,\n        );\n      } else if (id == 'text/quote') {\n        // text/quote -> quote\n        node = quoteNode(attributes: attributes);\n      } else if (id == 'text/number-list') {\n        // text/number-list -> numbered_list\n        node = numberedListNode(\n          attributes: attributes,\n          children: children,\n        );\n      } else if (id == 'text/bulleted-list') {\n        // text/bulleted-list -> bulleted_list\n        node = bulletedListNode(\n          attributes: attributes,\n          children: children,\n        );\n      } else if (id == 'text/code_block') {\n        // text/code_block -> code\n        final language = nodeV0.attributes['language'];\n        node = codeBlockNode(delta: delta, language: language);\n      }\n    } else if (id == 'cover') {\n      node = paragraphNode();\n    }\n\n    return node ?? paragraphNode(text: jsonEncode(nodeV0.toJson()));\n  }\n\n  // migrate the attributes.\n  // backgroundColor -> highlightColor\n  // color -> textColor\n  static Delta migrateDelta(Delta delta) {\n    final textInserts = delta\n        .whereType<TextInsert>()\n        .map(\n          (e) => TextInsert(\n            e.text,\n            attributes: migrateAttributes(e.attributes),\n          ),\n        )\n        .toList(growable: false);\n    return Delta(operations: textInserts.toList());\n  }\n\n  static Attributes? migrateAttributes(Attributes? attributes) {\n    if (attributes == null) {\n      return null;\n    }\n    const backgroundColor = 'backgroundColor';\n    if (attributes.containsKey(backgroundColor)) {\n      attributes[AppFlowyRichTextKeys.backgroundColor] =\n          attributes[backgroundColor];\n      attributes.remove(backgroundColor);\n    }\n    const color = 'color';\n    if (attributes.containsKey(color)) {\n      attributes[AppFlowyRichTextKeys.textColor] = attributes[color];\n      attributes.remove(color);\n    }\n    return attributes;\n  }\n\n  // Before version 0.5.5, the cover is stored in the document root.\n  // Now, the cover is stored in the view.ext.\n  static void migrateCoverIfNeeded(\n    ViewPB view,\n    Attributes attributes, {\n    bool overwrite = false,\n  }) async {\n    if (view.extra.isNotEmpty && !overwrite) {\n      return;\n    }\n\n    final coverType = CoverType.fromString(\n      attributes[DocumentHeaderBlockKeys.coverType],\n    );\n    final coverDetails = attributes[DocumentHeaderBlockKeys.coverDetails];\n\n    Map extra = {};\n\n    if (coverType == CoverType.none ||\n        coverDetails == null ||\n        coverDetails is! String) {\n      extra = {\n        ViewExtKeys.coverKey: {\n          ViewExtKeys.coverTypeKey: PageStyleCoverImageType.none.toString(),\n          ViewExtKeys.coverValueKey: '',\n        },\n      };\n    } else {\n      switch (coverType) {\n        case CoverType.asset:\n          extra = {\n            ViewExtKeys.coverKey: {\n              ViewExtKeys.coverTypeKey:\n                  PageStyleCoverImageType.builtInImage.toString(),\n              ViewExtKeys.coverValueKey: coverDetails,\n            },\n          };\n          break;\n        case CoverType.color:\n          extra = {\n            ViewExtKeys.coverKey: {\n              ViewExtKeys.coverTypeKey:\n                  PageStyleCoverImageType.pureColor.toString(),\n              ViewExtKeys.coverValueKey: coverDetails,\n            },\n          };\n          break;\n        case CoverType.file:\n          if (isURL(coverDetails)) {\n            if (coverDetails.contains('unsplash')) {\n              extra = {\n                ViewExtKeys.coverKey: {\n                  ViewExtKeys.coverTypeKey:\n                      PageStyleCoverImageType.unsplashImage.toString(),\n                  ViewExtKeys.coverValueKey: coverDetails,\n                },\n              };\n            } else {\n              extra = {\n                ViewExtKeys.coverKey: {\n                  ViewExtKeys.coverTypeKey:\n                      PageStyleCoverImageType.customImage.toString(),\n                  ViewExtKeys.coverValueKey: coverDetails,\n                },\n              };\n            }\n          } else {\n            extra = {\n              ViewExtKeys.coverKey: {\n                ViewExtKeys.coverTypeKey:\n                    PageStyleCoverImageType.localImage.toString(),\n                ViewExtKeys.coverValueKey: coverDetails,\n              },\n            };\n          }\n          break;\n        default:\n      }\n    }\n\n    if (extra.isEmpty) {\n      return;\n    }\n\n    try {\n      final current = view.extra.isNotEmpty ? jsonDecode(view.extra) : {};\n      final merged = mergeMaps(current, extra);\n      await ViewBackendService.updateView(\n        viewId: view.id,\n        extra: jsonEncode(merged),\n      );\n    } catch (e) {\n      Log.error('Failed to migrating cover: $e');\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_floating_toolbar/custom_mobile_floating_toolbar.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_animate/flutter_animate.dart';\n\nList<ContextMenuButtonItem> buildMobileFloatingToolbarItems(\n  EditorState editorState,\n  Offset offset,\n  Function closeToolbar,\n) {\n  // copy, paste, select, select all, cut\n  final selection = editorState.selection;\n  if (selection == null) {\n    return [];\n  }\n  final toolbarItems = <ContextMenuButtonItem>[];\n\n  if (!selection.isCollapsed) {\n    toolbarItems.add(\n      ContextMenuButtonItem(\n        label: LocaleKeys.editor_copy.tr(),\n        onPressed: () {\n          customCopyCommand.execute(editorState);\n          closeToolbar();\n        },\n      ),\n    );\n  }\n\n  toolbarItems.add(\n    ContextMenuButtonItem(\n      label: LocaleKeys.editor_paste.tr(),\n      onPressed: () {\n        customPasteCommand.execute(editorState);\n        closeToolbar();\n      },\n    ),\n  );\n\n  if (!selection.isCollapsed) {\n    toolbarItems.add(\n      ContextMenuButtonItem(\n        label: LocaleKeys.editor_cut.tr(),\n        onPressed: () {\n          cutCommand.execute(editorState);\n          closeToolbar();\n        },\n      ),\n    );\n  }\n\n  toolbarItems.add(\n    ContextMenuButtonItem(\n      label: LocaleKeys.editor_select.tr(),\n      onPressed: () {\n        editorState.selectWord(offset);\n        closeToolbar();\n      },\n    ),\n  );\n\n  toolbarItems.add(\n    ContextMenuButtonItem(\n      label: LocaleKeys.editor_selectAll.tr(),\n      onPressed: () {\n        selectAllCommand.execute(editorState);\n        closeToolbar();\n      },\n    ),\n  );\n\n  return toolbarItems;\n}\n\nextension on EditorState {\n  void selectWord(Offset offset) {\n    final node = service.selectionService.getNodeInOffset(offset);\n    final selection = node?.selectable?.getWordBoundaryInOffset(offset);\n    if (selection == null) {\n      return;\n    }\n    updateSelectionWithReason(selection);\n  }\n}\n\nclass CustomMobileFloatingToolbar extends StatelessWidget {\n  const CustomMobileFloatingToolbar({\n    super.key,\n    required this.editorState,\n    required this.anchor,\n    required this.closeToolbar,\n  });\n\n  final EditorState editorState;\n  final Offset anchor;\n  final VoidCallback closeToolbar;\n\n  @override\n  Widget build(BuildContext context) {\n    return Animate(\n      autoPlay: true,\n      effects: _getEffects(context),\n      child: AdaptiveTextSelectionToolbar.buttonItems(\n        buttonItems: buildMobileFloatingToolbarItems(\n          editorState,\n          anchor,\n          closeToolbar,\n        ),\n        anchors: TextSelectionToolbarAnchors(\n          primaryAnchor: anchor,\n        ),\n      ),\n    );\n  }\n\n  List<Effect> _getEffects(BuildContext context) {\n    if (Platform.isIOS) {\n      final Size(:width, :height) = MediaQuery.of(context).size;\n      final alignmentX = (anchor.dx - width / 2) / (width / 2);\n      final alignmentY = (anchor.dy - height / 2) / (height / 2);\n      return [\n        ScaleEffect(\n          curve: Curves.easeInOut,\n          alignment: Alignment(alignmentX, alignmentY),\n          duration: 250.milliseconds,\n        ),\n      ];\n    } else if (Platform.isAndroid) {\n      return [\n        const FadeEffect(\n          duration: SelectionOverlay.fadeDuration,\n        ),\n        MoveEffect(\n          curve: Curves.easeOutCubic,\n          begin: const Offset(0, 16),\n          duration: 100.milliseconds,\n        ),\n      ];\n    } else {\n      return [];\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nextension EditorStateAddBlock on EditorState {\n  Future<void> insertMathEquation(\n    Selection selection,\n  ) async {\n    final path = selection.start.path;\n    final node = getNodeAtPath(path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      return;\n    }\n    final transaction = this.transaction;\n    final insertedNode = mathEquationNode();\n    if (delta.isEmpty) {\n      transaction\n        ..insertNode(path, insertedNode)\n        ..deleteNode(node);\n    } else {\n      transaction.insertNode(\n        path.next,\n        insertedNode,\n      );\n    }\n\n    await apply(transaction);\n\n    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n      final mathEquationState = getNodeAtPath(path)?.key.currentState;\n      if (mathEquationState != null &&\n          mathEquationState is MathEquationBlockComponentWidgetState) {\n        mathEquationState.showEditingDialog();\n      }\n    });\n  }\n\n  Future<void> insertDivider(Selection selection) async {\n    // same as the [handler] of [dividerMenuItem] in Desktop\n\n    final path = selection.end.path;\n    final node = getNodeAtPath(path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      return;\n    }\n    final insertedPath = delta.isEmpty ? path : path.next;\n    final transaction = this.transaction;\n    transaction.insertNode(insertedPath, dividerNode());\n    // only insert a new paragraph node when the next node is not a paragraph node\n    //  and its delta is not empty.\n    final next = node.next;\n    if (next == null ||\n        next.type != ParagraphBlockKeys.type ||\n        next.delta?.isNotEmpty == true) {\n      transaction.insertNode(\n        insertedPath,\n        paragraphNode(),\n      );\n    }\n    transaction.selectionExtraInfo = {};\n    transaction.afterSelection = Selection.collapsed(\n      Position(path: insertedPath.next),\n    );\n    await apply(transaction);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_block_settings_screen.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nenum MobileBlockActionType {\n  delete,\n  duplicate,\n  insertAbove,\n  insertBelow,\n  color;\n\n  static List<MobileBlockActionType> get standard => [\n        MobileBlockActionType.delete,\n        MobileBlockActionType.duplicate,\n        MobileBlockActionType.insertAbove,\n        MobileBlockActionType.insertBelow,\n      ];\n\n  static MobileBlockActionType fromActionString(String actionString) {\n    return MobileBlockActionType.values.firstWhere(\n      (e) => e.actionString == actionString,\n      orElse: () => throw Exception('Unknown action string: $actionString'),\n    );\n  }\n\n  String get actionString => toString();\n\n  FlowySvgData get icon {\n    return switch (this) {\n      MobileBlockActionType.delete => FlowySvgs.m_delete_m,\n      MobileBlockActionType.duplicate => FlowySvgs.m_duplicate_m,\n      MobileBlockActionType.insertAbove => FlowySvgs.arrow_up_s,\n      MobileBlockActionType.insertBelow => FlowySvgs.arrow_down_s,\n      MobileBlockActionType.color => FlowySvgs.m_color_m,\n    };\n  }\n\n  String get i18n {\n    return switch (this) {\n      MobileBlockActionType.delete => LocaleKeys.button_delete.tr(),\n      MobileBlockActionType.duplicate => LocaleKeys.button_duplicate.tr(),\n      MobileBlockActionType.insertAbove => LocaleKeys.button_insertAbove.tr(),\n      MobileBlockActionType.insertBelow => LocaleKeys.button_insertBelow.tr(),\n      MobileBlockActionType.color =>\n        LocaleKeys.document_plugins_optionAction_color.tr(),\n    };\n  }\n}\n\nclass MobileBlockSettingsScreen extends StatelessWidget {\n  const MobileBlockSettingsScreen({super.key, required this.actions});\n\n  final List<MobileBlockActionType> actions;\n\n  static const routeName = '/block_settings';\n\n  // the action string comes from the enum MobileBlockActionType\n  // example: MobileBlockActionType.delete.actionString, MobileBlockActionType.duplicate.actionString, etc.\n  static const supportedActions = 'actions';\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        titleSpacing: 0,\n        title: FlowyText.semibold(\n          LocaleKeys.titleBar_actions.tr(),\n          fontSize: 14.0,\n        ),\n        leading: const AppBarBackButton(),\n      ),\n      body: SafeArea(\n        child: ListView.separated(\n          itemCount: actions.length,\n          itemBuilder: (context, index) {\n            final action = actions[index];\n            return FlowyButton(\n              text: Padding(\n                padding: const EdgeInsets.symmetric(\n                  horizontal: 12.0,\n                  vertical: 18.0,\n                ),\n                child: FlowyText(action.i18n),\n              ),\n              leftIcon: FlowySvg(action.icon),\n              leftIconSize: const Size.square(24),\n              onTap: () {},\n            );\n          },\n          separatorBuilder: (context, index) => const Divider(\n            height: 1.0,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_get_selection_color.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\n\nextension SelectionColor on EditorState {\n  String? getSelectionColor(String key) {\n    final selection = this.selection;\n    if (selection == null) {\n      return null;\n    }\n    String? color = toggledStyle[key];\n    if (color == null) {\n      if (selection.isCollapsed && selection.startIndex != 0) {\n        color = getDeltaAttributeValueInSelection<String>(\n          key,\n          selection.copyWith(\n            start: selection.start.copyWith(\n              offset: selection.startIndex - 1,\n            ),\n          ),\n        );\n      } else {\n        color = getDeltaAttributeValueInSelection<String>(\n          key,\n        );\n      }\n    }\n    return color;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_align_items.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_menu_item.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_popup_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nconst _left = 'left';\nconst _center = 'center';\nconst _right = 'right';\n\nclass AlignItems extends StatelessWidget {\n  AlignItems({\n    super.key,\n    required this.editorState,\n  });\n\n  final EditorState editorState;\n  final List<(String, FlowySvgData)> _alignMenuItems = [\n    (_left, FlowySvgs.m_aa_align_left_m),\n    (_center, FlowySvgs.m_aa_align_center_m),\n    (_right, FlowySvgs.m_aa_align_right_m),\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    final currentAlignItem = _getCurrentAlignItem();\n    final theme = ToolbarColorExtension.of(context);\n    return PopupMenu(\n      itemLength: _alignMenuItems.length,\n      onSelected: (index) {\n        editorState.alignBlock(\n          _alignMenuItems[index].$1,\n          selectionExtraInfo: {\n            selectionExtraInfoDoNotAttachTextService: true,\n            selectionExtraInfoDisableFloatingToolbar: true,\n          },\n        );\n      },\n      menuBuilder: (context, keys, currentIndex) {\n        final children = _alignMenuItems\n            .mapIndexed(\n              (index, e) => [\n                PopupMenuItemWrapper(\n                  key: keys[index],\n                  isSelected: currentIndex == index,\n                  icon: e.$2,\n                ),\n                if (index != 0 && index != _alignMenuItems.length - 1)\n                  const HSpace(12),\n              ],\n            )\n            .flattened\n            .toList();\n        return PopupMenuWrapper(\n          child: Row(\n            mainAxisSize: MainAxisSize.min,\n            children: children,\n          ),\n        );\n      },\n      builder: (context, key) => MobileToolbarMenuItemWrapper(\n        key: key,\n        size: const Size(82, 52),\n        onTap: () async {\n          await editorState.alignBlock(\n            currentAlignItem.$1,\n            selectionExtraInfo: {\n              selectionExtraInfoDoNotAttachTextService: true,\n              selectionExtraInfoDisableFloatingToolbar: true,\n            },\n          );\n        },\n        icon: currentAlignItem.$2,\n        isSelected: false,\n        iconPadding: const EdgeInsets.symmetric(\n          vertical: 14.0,\n        ),\n        showDownArrow: true,\n        backgroundColor: theme.toolbarMenuItemBackgroundColor,\n      ),\n    );\n  }\n\n  (String, FlowySvgData) _getCurrentAlignItem() {\n    final align = _getCurrentBlockAlign();\n    if (align == _center) {\n      return (_right, FlowySvgs.m_aa_align_right_s);\n    } else if (align == _right) {\n      return (_left, FlowySvgs.m_aa_align_left_s);\n    } else {\n      return (_center, FlowySvgs.m_aa_align_center_s);\n    }\n  }\n\n  String _getCurrentBlockAlign() {\n    final selection = editorState.selection;\n    if (selection == null) {\n      return _left;\n    }\n    final nodes = editorState.getNodesInSelection(selection);\n    String? alignString;\n    for (final node in nodes) {\n      final align = node.attributes[blockComponentAlign];\n      if (alignString == null) {\n        alignString = align;\n      } else if (alignString != align) {\n        return _left;\n      }\n    }\n    return alignString ?? _left;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_bius_items.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/material.dart';\n\nclass BIUSItems extends StatelessWidget {\n  BIUSItems({\n    super.key,\n    required this.editorState,\n  });\n\n  final EditorState editorState;\n\n  final List<(FlowySvgData, String)> _bius = [\n    (FlowySvgs.m_toolbar_bold_m, AppFlowyRichTextKeys.bold),\n    (FlowySvgs.m_toolbar_italic_m, AppFlowyRichTextKeys.italic),\n    (FlowySvgs.m_toolbar_underline_m, AppFlowyRichTextKeys.underline),\n    (FlowySvgs.m_toolbar_strike_m, AppFlowyRichTextKeys.strikethrough),\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    return IntrinsicHeight(\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: _bius\n            .mapIndexed(\n              (index, e) => [\n                _buildBIUSItem(\n                  context,\n                  index,\n                  e.$1,\n                  e.$2,\n                ),\n                if (index != 0 || index != _bius.length - 1)\n                  const ScaledVerticalDivider(),\n              ],\n            )\n            .flattened\n            .toList(),\n      ),\n    );\n  }\n\n  Widget _buildBIUSItem(\n    BuildContext context,\n    int index,\n    FlowySvgData icon,\n    String richTextKey,\n  ) {\n    final theme = ToolbarColorExtension.of(context);\n    return StatefulBuilder(\n      builder: (_, setState) => MobileToolbarMenuItemWrapper(\n        size: const Size(62, 52),\n        enableTopLeftRadius: index == 0,\n        enableBottomLeftRadius: index == 0,\n        enableTopRightRadius: index == _bius.length - 1,\n        enableBottomRightRadius: index == _bius.length - 1,\n        backgroundColor: theme.toolbarMenuItemBackgroundColor,\n        onTap: () async {\n          await editorState.toggleAttribute(\n            richTextKey,\n            selectionExtraInfo: {\n              selectionExtraInfoDisableFloatingToolbar: true,\n              selectionExtraInfoDoNotAttachTextService: true,\n            },\n          );\n          // refresh the status\n          setState(() {});\n        },\n        icon: icon,\n        isSelected: editorState.isTextDecorationSelected(richTextKey) &&\n            editorState.toggledStyle[richTextKey] != false,\n        iconPadding: const EdgeInsets.symmetric(\n          vertical: 14.0,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_menu_item.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_popup_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/link_toolbar_item.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart'\n    hide QuoteBlockKeys, quoteNode;\nimport 'package:collection/collection.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass BlockItems extends StatelessWidget {\n  BlockItems({\n    super.key,\n    required this.service,\n    required this.editorState,\n  });\n\n  final EditorState editorState;\n  final AppFlowyMobileToolbarWidgetService service;\n\n  final List<(FlowySvgData, String)> _blockItems = [\n    (FlowySvgs.m_toolbar_bulleted_list_m, BulletedListBlockKeys.type),\n    (FlowySvgs.m_toolbar_numbered_list_m, NumberedListBlockKeys.type),\n    (FlowySvgs.m_aa_quote_m, QuoteBlockKeys.type),\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    return IntrinsicHeight(\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          ..._blockItems\n              .mapIndexed(\n                (index, e) => [\n                  _buildBlockItem(\n                    context,\n                    index,\n                    e.$1,\n                    e.$2,\n                  ),\n                  if (index != 0) const ScaledVerticalDivider(),\n                ],\n              )\n              .flattened,\n          // this item is a special case, use link item here instead of block item\n          _buildLinkItem(context),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildBlockItem(\n    BuildContext context,\n    int index,\n    FlowySvgData icon,\n    String blockType,\n  ) {\n    final theme = ToolbarColorExtension.of(context);\n    return MobileToolbarMenuItemWrapper(\n      size: const Size(62, 54),\n      enableTopLeftRadius: index == 0,\n      enableBottomLeftRadius: index == 0,\n      enableTopRightRadius: false,\n      enableBottomRightRadius: false,\n      onTap: () async {\n        await _convert(blockType);\n      },\n      backgroundColor: theme.toolbarMenuItemBackgroundColor,\n      icon: icon,\n      isSelected: editorState.isBlockTypeSelected(blockType),\n      iconPadding: const EdgeInsets.symmetric(\n        vertical: 14.0,\n      ),\n    );\n  }\n\n  Widget _buildLinkItem(BuildContext context) {\n    final theme = ToolbarColorExtension.of(context);\n    final items = [\n      (AppFlowyRichTextKeys.code, FlowySvgs.m_aa_code_m),\n      // (InlineMathEquationKeys.formula, FlowySvgs.m_aa_math_s),\n    ];\n    return PopupMenu(\n      itemLength: items.length,\n      onSelected: (index) async {\n        await editorState.toggleAttribute(items[index].$1);\n      },\n      menuBuilder: (context, keys, currentIndex) {\n        final children = items\n            .mapIndexed(\n              (index, e) => [\n                PopupMenuItemWrapper(\n                  key: keys[index],\n                  isSelected: currentIndex == index,\n                  icon: e.$2,\n                ),\n                if (index != 0 || index != items.length - 1) const HSpace(12),\n              ],\n            )\n            .flattened\n            .toList();\n        return PopupMenuWrapper(\n          child: Row(\n            mainAxisSize: MainAxisSize.min,\n            children: children,\n          ),\n        );\n      },\n      builder: (context, key) => MobileToolbarMenuItemWrapper(\n        key: key,\n        size: const Size(62, 54),\n        enableTopLeftRadius: false,\n        enableBottomLeftRadius: false,\n        showDownArrow: true,\n        onTap: _onLinkItemTap,\n        backgroundColor: theme.toolbarMenuItemBackgroundColor,\n        icon: FlowySvgs.m_toolbar_link_m,\n        isSelected: false,\n        iconPadding: const EdgeInsets.symmetric(\n          vertical: 14.0,\n        ),\n      ),\n    );\n  }\n\n  void _onLinkItemTap() async {\n    final selection = editorState.selection;\n    if (selection == null) {\n      return;\n    }\n    final nodes = editorState.getNodesInSelection(selection);\n    // show edit link bottom sheet\n    final context = nodes.firstOrNull?.context;\n    if (context != null) {\n      _closeKeyboard(selection);\n\n      // keep the selection\n      unawaited(\n        editorState.updateSelectionWithReason(\n          selection,\n          extraInfo: {\n            selectionExtraInfoDisableMobileToolbarKey: true,\n            selectionExtraInfoDoNotAttachTextService: true,\n            selectionExtraInfoDisableFloatingToolbar: true,\n          },\n        ),\n      );\n      keepEditorFocusNotifier.increase();\n      await showEditLinkBottomSheet(context, selection, editorState);\n    }\n  }\n\n  void _closeKeyboard(Selection selection) {\n    editorState.updateSelectionWithReason(\n      selection,\n      extraInfo: {\n        selectionExtraInfoDisableMobileToolbarKey: true,\n        selectionExtraInfoDoNotAttachTextService: true,\n      },\n    );\n    editorState.service.keyboardService?.closeKeyboard();\n  }\n\n  Future<void> _convert(String blockType) async {\n    await editorState.convertBlockType(\n      blockType,\n      selectionExtraInfo: {\n        selectionExtraInfoDoNotAttachTextService: true,\n        selectionExtraInfoDisableFloatingToolbar: true,\n      },\n    );\n    unawaited(\n      editorState.updateSelectionWithReason(\n        editorState.selection,\n        extraInfo: {\n          selectionExtraInfoDisableFloatingToolbar: true,\n          selectionExtraInfoDoNotAttachTextService: true,\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_close_keyboard_or_menu_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass CloseKeyboardOrMenuButton extends StatelessWidget {\n  const CloseKeyboardOrMenuButton({\n    super.key,\n    required this.onPressed,\n  });\n\n  final VoidCallback onPressed;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: 62,\n      height: 42,\n      child: FlowyButton(\n        text: const FlowySvg(\n          FlowySvgs.m_toolbar_keyboard_m,\n        ),\n        onTap: onPressed,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_item.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/font_colors.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_get_selection_color.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nclass ColorItem extends StatelessWidget {\n  const ColorItem({\n    super.key,\n    required this.editorState,\n    required this.service,\n  });\n\n  final EditorState editorState;\n  final AppFlowyMobileToolbarWidgetService service;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = ToolbarColorExtension.of(context);\n    final String? selectedTextColor =\n        editorState.getSelectionColor(AppFlowyRichTextKeys.textColor);\n    final String? selectedBackgroundColor =\n        editorState.getSelectionColor(AppFlowyRichTextKeys.backgroundColor);\n    final backgroundColor = EditorFontColors.fromBuiltInColors(\n      context,\n      selectedBackgroundColor?.tryToColor(),\n    );\n    return MobileToolbarMenuItemWrapper(\n      size: const Size(82, 52),\n      onTap: () async {\n        service.closeKeyboard();\n        unawaited(\n          editorState.updateSelectionWithReason(\n            editorState.selection,\n            extraInfo: {\n              selectionExtraInfoDisableMobileToolbarKey: true,\n              selectionExtraInfoDisableFloatingToolbar: true,\n              selectionExtraInfoDoNotAttachTextService: true,\n            },\n          ),\n        );\n        keepEditorFocusNotifier.increase();\n        await showTextColorAndBackgroundColorPicker(\n          context,\n          editorState: editorState,\n          selection: editorState.selection!,\n        );\n      },\n      icon: FlowySvgs.m_aa_font_color_m,\n      iconColor: EditorFontColors.fromBuiltInColors(\n        context,\n        selectedTextColor?.tryToColor(),\n      ),\n      backgroundColor: backgroundColor ?? theme.toolbarMenuItemBackgroundColor,\n      selectedBackgroundColor: backgroundColor,\n      isSelected: selectedBackgroundColor != null,\n      showRightArrow: true,\n      iconPadding: const EdgeInsets.only(\n        top: 14.0,\n        bottom: 14.0,\n        right: 28.0,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/font_colors.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_get_selection_color.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nconst _count = 6;\n\nFuture<void> showTextColorAndBackgroundColorPicker(\n  BuildContext context, {\n  required EditorState editorState,\n  required Selection selection,\n}) async {\n  final theme = ToolbarColorExtension.of(context);\n  await showMobileBottomSheet(\n    context,\n    showHeader: true,\n    showDragHandle: true,\n    showDoneButton: true,\n    barrierColor: Colors.transparent,\n    backgroundColor: theme.toolbarMenuBackgroundColor,\n    elevation: 20,\n    title: LocaleKeys.grid_selectOption_colorPanelTitle.tr(),\n    padding: const EdgeInsets.fromLTRB(10, 4, 10, 8),\n    builder: (context) {\n      return _TextColorAndBackgroundColor(\n        editorState: editorState,\n        selection: selection,\n      );\n    },\n  );\n  Future.delayed(const Duration(milliseconds: 100), () {\n    // highlight the selected text again.\n    editorState.updateSelectionWithReason(\n      selection,\n      extraInfo: {\n        selectionExtraInfoDisableFloatingToolbar: true,\n      },\n    );\n  });\n}\n\nclass _TextColorAndBackgroundColor extends StatefulWidget {\n  const _TextColorAndBackgroundColor({\n    required this.editorState,\n    required this.selection,\n  });\n\n  final EditorState editorState;\n  final Selection selection;\n\n  @override\n  State<_TextColorAndBackgroundColor> createState() =>\n      _TextColorAndBackgroundColorState();\n}\n\nclass _TextColorAndBackgroundColorState\n    extends State<_TextColorAndBackgroundColor> {\n  @override\n  Widget build(BuildContext context) {\n    final String? selectedTextColor =\n        widget.editorState.getSelectionColor(AppFlowyRichTextKeys.textColor);\n    final String? selectedBackgroundColor = widget.editorState\n        .getSelectionColor(AppFlowyRichTextKeys.backgroundColor);\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Padding(\n          padding: const EdgeInsets.only(\n            top: 20,\n            left: 6.0,\n          ),\n          child: FlowyText(\n            LocaleKeys.editor_textColor.tr(),\n            fontSize: 14.0,\n          ),\n        ),\n        const VSpace(6.0),\n        EditorTextColorWidget(\n          selectedColor: selectedTextColor?.tryToColor(),\n          onSelectedColor: (textColor) async {\n            final hex = textColor.a == 0 ? null : textColor.toHex();\n            final selection = widget.selection;\n            if (selection.isCollapsed) {\n              widget.editorState.updateToggledStyle(\n                AppFlowyRichTextKeys.textColor,\n                hex ?? '',\n              );\n            } else {\n              await widget.editorState.formatDelta(\n                widget.selection,\n                {\n                  AppFlowyRichTextKeys.textColor: hex,\n                },\n                selectionExtraInfo: {\n                  selectionExtraInfoDisableFloatingToolbar: true,\n                  selectionExtraInfoDisableMobileToolbarKey: true,\n                  selectionExtraInfoDoNotAttachTextService: true,\n                },\n              );\n            }\n            setState(() {});\n          },\n        ),\n        Padding(\n          padding: const EdgeInsets.only(\n            top: 18.0,\n            left: 6.0,\n          ),\n          child: FlowyText(\n            LocaleKeys.editor_backgroundColor.tr(),\n            fontSize: 14.0,\n          ),\n        ),\n        const VSpace(6.0),\n        EditorBackgroundColors(\n          selectedColor: selectedBackgroundColor?.tryToColor(),\n          onSelectedColor: (backgroundColor) async {\n            final hex = backgroundColor.a == 0 ? null : backgroundColor.toHex();\n            final selection = widget.selection;\n            if (selection.isCollapsed) {\n              widget.editorState.updateToggledStyle(\n                AppFlowyRichTextKeys.backgroundColor,\n                hex ?? '',\n              );\n            } else {\n              await widget.editorState.formatDelta(\n                widget.selection,\n                {\n                  AppFlowyRichTextKeys.backgroundColor: hex,\n                },\n                selectionExtraInfo: {\n                  selectionExtraInfoDisableFloatingToolbar: true,\n                  selectionExtraInfoDisableMobileToolbarKey: true,\n                  selectionExtraInfoDoNotAttachTextService: true,\n                },\n              );\n            }\n            setState(() {});\n          },\n        ),\n      ],\n    );\n  }\n}\n\nclass EditorBackgroundColors extends StatelessWidget {\n  const EditorBackgroundColors({\n    super.key,\n    this.selectedColor,\n    required this.onSelectedColor,\n  });\n\n  final Color? selectedColor;\n  final void Function(Color color) onSelectedColor;\n\n  @override\n  Widget build(BuildContext context) {\n    final colors = Theme.of(context).brightness == Brightness.light\n        ? EditorFontColors.lightColors\n        : EditorFontColors.darkColors;\n    return GridView.count(\n      crossAxisCount: _count,\n      shrinkWrap: true,\n      physics: const NeverScrollableScrollPhysics(),\n      children: colors.mapIndexed(\n        (index, color) {\n          return _BackgroundColorItem(\n            color: color,\n            isSelected:\n                selectedColor == null ? index == 0 : selectedColor == color,\n            onTap: () => onSelectedColor(color),\n          );\n        },\n      ).toList(),\n    );\n  }\n}\n\nclass _BackgroundColorItem extends StatelessWidget {\n  const _BackgroundColorItem({\n    required this.color,\n    required this.isSelected,\n    required this.onTap,\n  });\n\n  final VoidCallback onTap;\n  final Color color;\n  final bool isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = ToolbarColorExtension.of(context);\n    return GestureDetector(\n      onTap: onTap,\n      child: Container(\n        margin: const EdgeInsets.all(6.0),\n        decoration: BoxDecoration(\n          color: color,\n          borderRadius: Corners.s12Border,\n          border: Border.all(\n            width: isSelected ? 2.0 : 1.0,\n            color: isSelected\n                ? theme.toolbarMenuItemSelectedBackgroundColor\n                : Theme.of(context).dividerColor,\n          ),\n        ),\n        alignment: Alignment.center,\n        child: isSelected\n            ? const FlowySvg(\n                FlowySvgs.m_blue_check_s,\n                size: Size.square(28.0),\n                blendMode: null,\n              )\n            : null,\n      ),\n    );\n  }\n}\n\nclass EditorTextColorWidget extends StatelessWidget {\n  EditorTextColorWidget({\n    super.key,\n    this.selectedColor,\n    required this.onSelectedColor,\n  });\n\n  final Color? selectedColor;\n  final void Function(Color color) onSelectedColor;\n\n  final colors = [\n    const Color(0x00FFFFFF),\n    const Color(0xFFDB3636),\n    const Color(0xFFEA8F06),\n    const Color(0xFF18A166),\n    const Color(0xFF205EEE),\n    const Color(0xFFC619C9),\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    return GridView.count(\n      crossAxisCount: _count,\n      shrinkWrap: true,\n      padding: EdgeInsets.zero,\n      physics: const NeverScrollableScrollPhysics(),\n      children: colors.mapIndexed(\n        (index, color) {\n          return _TextColorItem(\n            color: color,\n            isSelected:\n                selectedColor == null ? index == 0 : selectedColor == color,\n            onTap: () => onSelectedColor(color),\n          );\n        },\n      ).toList(),\n    );\n  }\n}\n\nclass _TextColorItem extends StatelessWidget {\n  const _TextColorItem({\n    required this.color,\n    required this.isSelected,\n    required this.onTap,\n  });\n\n  final VoidCallback onTap;\n  final Color color;\n  final bool isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onTap: onTap,\n      child: Container(\n        margin: const EdgeInsets.all(6.0),\n        decoration: BoxDecoration(\n          borderRadius: Corners.s12Border,\n          border: Border.all(\n            width: isSelected ? 2.0 : 1.0,\n            color: isSelected\n                ? const Color(0xff00C6F1)\n                : Theme.of(context).dividerColor,\n          ),\n        ),\n        alignment: Alignment.center,\n        child: FlowyText(\n          'A',\n          fontSize: 24,\n          color: color.a == 0 ? null : color,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_font_item.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/shared/google_fonts_extension.dart';\nimport 'package:appflowy/util/font_family_extension.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:provider/provider.dart';\n\nclass FontFamilyItem extends StatelessWidget {\n  const FontFamilyItem({\n    super.key,\n    required this.editorState,\n  });\n\n  final EditorState editorState;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = ToolbarColorExtension.of(context);\n    final fontFamily = _getCurrentSelectedFontFamilyName();\n    final systemFonFamily =\n        context.read<DocumentAppearanceCubit>().state.fontFamily;\n    return MobileToolbarMenuItemWrapper(\n      size: const Size(144, 52),\n      onTap: () async {\n        final selection = editorState.selection;\n        if (selection == null) {\n          return;\n        }\n        // disable the floating toolbar\n        unawaited(\n          editorState.updateSelectionWithReason(\n            selection,\n            extraInfo: {\n              selectionExtraInfoDisableFloatingToolbar: true,\n              selectionExtraInfoDisableMobileToolbarKey: true,\n            },\n          ),\n        );\n\n        final newFont = await context\n            .read<GoRouter>()\n            .push<String>(FontPickerScreen.routeName);\n\n        // if the selection is not collapsed, apply the font to the selection.\n        if (newFont != null && !selection.isCollapsed) {\n          if (newFont != fontFamily) {\n            await editorState.formatDelta(selection, {\n              AppFlowyRichTextKeys.fontFamily: newFont,\n            });\n          }\n        }\n\n        // wait for the font picker screen to be dismissed.\n        Future.delayed(const Duration(milliseconds: 250), () {\n          // highlight the selected text again.\n          editorState.updateSelectionWithReason(\n            selection,\n            extraInfo: {\n              selectionExtraInfoDisableFloatingToolbar: true,\n              selectionExtraInfoDisableMobileToolbarKey: false,\n            },\n          );\n          // if the selection is collapsed, save the font for the next typing.\n          if (newFont != null && selection.isCollapsed) {\n            editorState.updateToggledStyle(\n              AppFlowyRichTextKeys.fontFamily,\n              getGoogleFontSafely(newFont).fontFamily,\n            );\n          }\n        });\n      },\n      text: (fontFamily ?? systemFonFamily).fontFamilyDisplayName,\n      fontFamily: fontFamily ?? systemFonFamily,\n      backgroundColor: theme.toolbarMenuItemBackgroundColor,\n      isSelected: false,\n      enable: true,\n      showRightArrow: true,\n      iconPadding: const EdgeInsets.only(\n        top: 14.0,\n        bottom: 14.0,\n        left: 14.0,\n        right: 12.0,\n      ),\n      textPadding: const EdgeInsets.only(\n        right: 16.0,\n      ),\n    );\n  }\n\n  String? _getCurrentSelectedFontFamilyName() {\n    final toggleFontFamily =\n        editorState.toggledStyle[AppFlowyRichTextKeys.fontFamily];\n    if (toggleFontFamily is String && toggleFontFamily.isNotEmpty) {\n      return toggleFontFamily;\n    }\n    final selection = editorState.selection;\n    if (selection != null &&\n        selection.isCollapsed &&\n        selection.startIndex != 0) {\n      return editorState.getDeltaAttributeValueInSelection<String>(\n        AppFlowyRichTextKeys.fontFamily,\n        selection.copyWith(\n          start: selection.start.copyWith(\n            offset: selection.startIndex - 1,\n          ),\n        ),\n      );\n    }\n    return editorState.getDeltaAttributeValueInSelection<String>(\n      AppFlowyRichTextKeys.fontFamily,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_heading_and_text_items.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nclass HeadingsAndTextItems extends StatelessWidget {\n  const HeadingsAndTextItems({\n    super.key,\n    required this.editorState,\n  });\n\n  final EditorState editorState;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.spaceBetween,\n      children: [\n        _HeadingOrTextItem(\n          icon: FlowySvgs.m_aa_h1_m,\n          blockType: HeadingBlockKeys.type,\n          editorState: editorState,\n          level: 1,\n        ),\n        _HeadingOrTextItem(\n          icon: FlowySvgs.m_aa_h2_m,\n          blockType: HeadingBlockKeys.type,\n          editorState: editorState,\n          level: 2,\n        ),\n        _HeadingOrTextItem(\n          icon: FlowySvgs.m_aa_h3_m,\n          blockType: HeadingBlockKeys.type,\n          editorState: editorState,\n          level: 3,\n        ),\n        _HeadingOrTextItem(\n          icon: FlowySvgs.m_aa_paragraph_m,\n          blockType: ParagraphBlockKeys.type,\n          editorState: editorState,\n        ),\n      ],\n    );\n  }\n}\n\nclass _HeadingOrTextItem extends StatelessWidget {\n  const _HeadingOrTextItem({\n    required this.icon,\n    required this.blockType,\n    required this.editorState,\n    this.level,\n  });\n\n  final FlowySvgData icon;\n  final String blockType;\n  final EditorState editorState;\n  final int? level;\n\n  @override\n  Widget build(BuildContext context) {\n    final isSelected = editorState.isBlockTypeSelected(\n      blockType,\n      level: level,\n    );\n    final padding = level != null\n        ? EdgeInsets.symmetric(\n            vertical: 14.0 - (3 - level!) * 3.0,\n          )\n        : const EdgeInsets.symmetric(\n            vertical: 16.0,\n          );\n    return MobileToolbarMenuItemWrapper(\n      size: const Size(76, 52),\n      onTap: () async => _convert(isSelected),\n      icon: icon,\n      isSelected: isSelected,\n      iconPadding: padding,\n    );\n  }\n\n  Future<void> _convert(bool isSelected) async {\n    await editorState.convertBlockType(\n      blockType,\n      isSelected: isSelected,\n      extraAttributes: level != null\n          ? {\n              HeadingBlockKeys.level: level!,\n            }\n          : null,\n      selectionExtraInfo: {\n        selectionExtraInfoDoNotAttachTextService: true,\n        selectionExtraInfoDisableFloatingToolbar: true,\n      },\n    );\n    unawaited(\n      editorState.updateSelectionWithReason(\n        editorState.selection,\n        extraInfo: {\n          selectionExtraInfoDisableFloatingToolbar: true,\n          selectionExtraInfoDoNotAttachTextService: true,\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_indent_items.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nclass IndentAndOutdentItems extends StatelessWidget {\n  const IndentAndOutdentItems({\n    super.key,\n    required this.service,\n    required this.editorState,\n  });\n\n  final EditorState editorState;\n  final AppFlowyMobileToolbarWidgetService service;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = ToolbarColorExtension.of(context);\n    return IntrinsicHeight(\n      child: Row(\n        children: [\n          MobileToolbarMenuItemWrapper(\n            size: const Size(95, 52),\n            icon: FlowySvgs.m_aa_outdent_m,\n            enable: isOutdentable(editorState),\n            isSelected: false,\n            enableTopRightRadius: false,\n            enableBottomRightRadius: false,\n            iconPadding: const EdgeInsets.symmetric(vertical: 14.0),\n            backgroundColor: theme.toolbarMenuItemBackgroundColor,\n            onTap: () {\n              service.closeItemMenu();\n              outdentCommand.execute(editorState);\n            },\n          ),\n          const ScaledVerticalDivider(),\n          MobileToolbarMenuItemWrapper(\n            size: const Size(95, 52),\n            icon: FlowySvgs.m_aa_indent_m,\n            enable: isIndentable(editorState),\n            isSelected: false,\n            enableTopLeftRadius: false,\n            enableBottomLeftRadius: false,\n            iconPadding: const EdgeInsets.symmetric(vertical: 14.0),\n            backgroundColor: theme.toolbarMenuItemBackgroundColor,\n            onTap: () {\n              service.closeItemMenu();\n              indentCommand.execute(editorState);\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_menu_item.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';\nimport 'package:flowy_svg/flowy_svg.dart';\nimport 'package:flutter/material.dart';\n\nclass PopupMenuItemWrapper extends StatelessWidget {\n  const PopupMenuItemWrapper({\n    super.key,\n    required this.isSelected,\n    required this.icon,\n  });\n\n  final bool isSelected;\n  final FlowySvgData icon;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = ToolbarColorExtension.of(context);\n    return Container(\n      width: 62,\n      height: 44,\n      decoration: ShapeDecoration(\n        color: isSelected ? theme.toolbarMenuItemSelectedBackgroundColor : null,\n        shape: RoundedRectangleBorder(\n          borderRadius: BorderRadius.circular(10),\n        ),\n      ),\n      padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 9),\n      child: FlowySvg(\n        icon,\n        color: isSelected\n            ? theme.toolbarMenuIconSelectedColor\n            : theme.toolbarMenuIconColor,\n      ),\n    );\n  }\n}\n\nclass PopupMenuWrapper extends StatelessWidget {\n  const PopupMenuWrapper({\n    super.key,\n    required this.child,\n  });\n\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = ToolbarColorExtension.of(context);\n    return Container(\n      height: 64,\n      padding: const EdgeInsets.symmetric(\n        horizontal: 12,\n        vertical: 10,\n      ),\n      decoration: ShapeDecoration(\n        color: theme.toolbarMenuBackgroundColor,\n        shape: RoundedRectangleBorder(\n          borderRadius: BorderRadius.circular(12),\n        ),\n        shadows: [\n          BoxShadow(\n            color: theme.toolbarShadowColor,\n            blurRadius: 20,\n            offset: const Offset(0, 10),\n          ),\n        ],\n      ),\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_popup_menu.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nclass PopupMenu extends StatefulWidget {\n  const PopupMenu({\n    super.key,\n    required this.onSelected,\n    required this.itemLength,\n    required this.menuBuilder,\n    required this.builder,\n  });\n\n  final Widget Function(BuildContext context, Key key) builder;\n  final int itemLength;\n  final Widget Function(\n    BuildContext context,\n    List<GlobalKey> keys,\n    int currentIndex,\n  ) menuBuilder;\n  final void Function(int index) onSelected;\n\n  @override\n  State<PopupMenu> createState() => _PopupMenuState();\n}\n\nclass _PopupMenuState extends State<PopupMenu> {\n  final key = GlobalKey();\n  final indexNotifier = ValueNotifier(-1);\n  late List<GlobalKey> itemKeys;\n\n  OverlayEntry? popupMenuOverlayEntry;\n\n  Rect get rect {\n    final RenderBox renderBox =\n        key.currentContext!.findRenderObject() as RenderBox;\n    final size = renderBox.size;\n    final offset = renderBox.localToGlobal(Offset.zero);\n    return Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);\n  }\n\n  @override\n  void initState() {\n    super.initState();\n\n    indexNotifier.value = widget.itemLength - 1;\n    itemKeys = List.generate(\n      widget.itemLength,\n      (_) => GlobalKey(),\n    );\n\n    indexNotifier.addListener(HapticFeedback.mediumImpact);\n  }\n\n  @override\n  void dispose() {\n    indexNotifier.removeListener(HapticFeedback.mediumImpact);\n    indexNotifier.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onLongPressStart: (details) {\n        _showMenu(context);\n      },\n      onLongPressMoveUpdate: (details) {\n        _updateSelection(details);\n      },\n      onLongPressCancel: () {\n        _hideMenu();\n      },\n      onLongPressUp: () {\n        if (indexNotifier.value != -1) {\n          widget.onSelected(indexNotifier.value);\n        }\n        _hideMenu();\n      },\n      child: widget.builder(context, key),\n    );\n  }\n\n  void _showMenu(BuildContext context) {\n    final theme = ToolbarColorExtension.of(context);\n    _hideMenu();\n\n    indexNotifier.value = widget.itemLength - 1;\n    popupMenuOverlayEntry ??= OverlayEntry(\n      builder: (context) {\n        final screenSize = MediaQuery.of(context).size;\n        final right = screenSize.width - rect.right;\n        final bottom = screenSize.height - rect.top + 16;\n        return Positioned(\n          right: right,\n          bottom: bottom,\n          child: ColoredBox(\n            color: theme.toolbarMenuBackgroundColor,\n            child: ValueListenableBuilder(\n              valueListenable: indexNotifier,\n              builder: (context, value, _) => widget.menuBuilder(\n                context,\n                itemKeys,\n                value,\n              ),\n            ),\n          ),\n        );\n      },\n    );\n    Overlay.of(context).insert(popupMenuOverlayEntry!);\n  }\n\n  void _hideMenu() {\n    indexNotifier.value = -1;\n\n    popupMenuOverlayEntry?.remove();\n    popupMenuOverlayEntry = null;\n  }\n\n  void _updateSelection(LongPressMoveUpdateDetails details) {\n    final dx = details.globalPosition.dx;\n    for (var i = 0; i < itemKeys.length; i++) {\n      final key = itemKeys[i];\n      final RenderBox renderBox =\n          key.currentContext!.findRenderObject() as RenderBox;\n      final size = renderBox.size;\n      final offset = renderBox.localToGlobal(Offset.zero);\n      final rect = Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);\n      // ignore the position overflow\n      if ((i == 0 && dx < rect.left) ||\n          (i == itemKeys.length - 1 && dx > rect.right)) {\n        indexNotifier.value = -1;\n        break;\n      }\n      if (rect.left <= dx && dx <= rect.right) {\n        indexNotifier.value = itemKeys.indexOf(key);\n        break;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart",
    "content": "// workaround for toolbar theme color.\n\nimport 'package:flutter/material.dart';\n\nclass ToolbarColorExtension extends ThemeExtension<ToolbarColorExtension> {\n  factory ToolbarColorExtension.light() => const ToolbarColorExtension(\n        toolbarBackgroundColor: Color(0xFFFFFFFF),\n        toolbarItemIconColor: Color(0xFF1F2329),\n        toolbarItemIconDisabledColor: Color(0xFF999BA0),\n        toolbarItemIconSelectedColor: Color(0x1F232914),\n        toolbarItemSelectedBackgroundColor: Color(0xFFF2F2F2),\n        toolbarMenuBackgroundColor: Color(0xFFFFFFFF),\n        toolbarMenuItemBackgroundColor: Color(0xFFF2F2F7),\n        toolbarMenuItemSelectedBackgroundColor: Color(0xFF00BCF0),\n        toolbarMenuIconColor: Color(0xFF1F2329),\n        toolbarMenuIconDisabledColor: Color(0xFF999BA0),\n        toolbarMenuIconSelectedColor: Color(0xFFFFFFFF),\n        toolbarShadowColor: Color(0x2D000000),\n      );\n\n  factory ToolbarColorExtension.dark() => const ToolbarColorExtension(\n        toolbarBackgroundColor: Color(0xFF1F2329),\n        toolbarItemIconColor: Color(0xFFF3F3F8),\n        toolbarItemIconDisabledColor: Color(0xFF55565B),\n        toolbarItemIconSelectedColor: Color(0xFF00BCF0),\n        toolbarItemSelectedBackgroundColor: Color(0xFF3A3D43),\n        toolbarMenuBackgroundColor: Color(0xFF23262B),\n        toolbarMenuItemBackgroundColor: Color(0xFF2D3036),\n        toolbarMenuItemSelectedBackgroundColor: Color(0xFF00BCF0),\n        toolbarMenuIconColor: Color(0xFFF3F3F8),\n        toolbarMenuIconDisabledColor: Color(0xFF55565B),\n        toolbarMenuIconSelectedColor: Color(0xFF1F2329),\n        toolbarShadowColor: Color.fromARGB(80, 112, 112, 112),\n      );\n\n  factory ToolbarColorExtension.fromBrightness(Brightness brightness) =>\n      brightness == Brightness.light\n          ? ToolbarColorExtension.light()\n          : ToolbarColorExtension.dark();\n\n  const ToolbarColorExtension({\n    required this.toolbarBackgroundColor,\n    required this.toolbarItemIconColor,\n    required this.toolbarItemIconDisabledColor,\n    required this.toolbarItemIconSelectedColor,\n    required this.toolbarMenuBackgroundColor,\n    required this.toolbarMenuItemBackgroundColor,\n    required this.toolbarMenuItemSelectedBackgroundColor,\n    required this.toolbarItemSelectedBackgroundColor,\n    required this.toolbarMenuIconColor,\n    required this.toolbarMenuIconDisabledColor,\n    required this.toolbarMenuIconSelectedColor,\n    required this.toolbarShadowColor,\n  });\n\n  final Color toolbarBackgroundColor;\n\n  final Color toolbarItemIconColor;\n  final Color toolbarItemIconDisabledColor;\n  final Color toolbarItemIconSelectedColor;\n  final Color toolbarItemSelectedBackgroundColor;\n\n  final Color toolbarMenuBackgroundColor;\n  final Color toolbarMenuItemBackgroundColor;\n  final Color toolbarMenuItemSelectedBackgroundColor;\n  final Color toolbarMenuIconColor;\n  final Color toolbarMenuIconDisabledColor;\n  final Color toolbarMenuIconSelectedColor;\n\n  final Color toolbarShadowColor;\n\n  static ToolbarColorExtension of(BuildContext context) {\n    return Theme.of(context).extension<ToolbarColorExtension>()!;\n  }\n\n  @override\n  ToolbarColorExtension copyWith({\n    Color? toolbarBackgroundColor,\n    Color? toolbarItemIconColor,\n    Color? toolbarItemIconDisabledColor,\n    Color? toolbarItemIconSelectedColor,\n    Color? toolbarMenuBackgroundColor,\n    Color? toolbarItemSelectedBackgroundColor,\n    Color? toolbarMenuItemBackgroundColor,\n    Color? toolbarMenuItemSelectedBackgroundColor,\n    Color? toolbarMenuIconColor,\n    Color? toolbarMenuIconDisabledColor,\n    Color? toolbarMenuIconSelectedColor,\n    Color? toolbarShadowColor,\n  }) {\n    return ToolbarColorExtension(\n      toolbarBackgroundColor:\n          toolbarBackgroundColor ?? this.toolbarBackgroundColor,\n      toolbarItemIconColor: toolbarItemIconColor ?? this.toolbarItemIconColor,\n      toolbarItemIconDisabledColor:\n          toolbarItemIconDisabledColor ?? this.toolbarItemIconDisabledColor,\n      toolbarItemIconSelectedColor:\n          toolbarItemIconSelectedColor ?? this.toolbarItemIconSelectedColor,\n      toolbarItemSelectedBackgroundColor: toolbarItemSelectedBackgroundColor ??\n          this.toolbarItemSelectedBackgroundColor,\n      toolbarMenuBackgroundColor:\n          toolbarMenuBackgroundColor ?? this.toolbarMenuBackgroundColor,\n      toolbarMenuItemBackgroundColor:\n          toolbarMenuItemBackgroundColor ?? this.toolbarMenuItemBackgroundColor,\n      toolbarMenuItemSelectedBackgroundColor:\n          toolbarMenuItemSelectedBackgroundColor ??\n              this.toolbarMenuItemSelectedBackgroundColor,\n      toolbarMenuIconColor: toolbarMenuIconColor ?? this.toolbarMenuIconColor,\n      toolbarMenuIconDisabledColor:\n          toolbarMenuIconDisabledColor ?? this.toolbarMenuIconDisabledColor,\n      toolbarMenuIconSelectedColor:\n          toolbarMenuIconSelectedColor ?? this.toolbarMenuIconSelectedColor,\n      toolbarShadowColor: toolbarShadowColor ?? this.toolbarShadowColor,\n    );\n  }\n\n  @override\n  ToolbarColorExtension lerp(ToolbarColorExtension? other, double t) {\n    if (other is! ToolbarColorExtension) {\n      return this;\n    }\n    return ToolbarColorExtension(\n      toolbarBackgroundColor:\n          Color.lerp(toolbarBackgroundColor, other.toolbarBackgroundColor, t)!,\n      toolbarItemIconColor:\n          Color.lerp(toolbarItemIconColor, other.toolbarItemIconColor, t)!,\n      toolbarItemIconDisabledColor: Color.lerp(\n        toolbarItemIconDisabledColor,\n        other.toolbarItemIconDisabledColor,\n        t,\n      )!,\n      toolbarItemIconSelectedColor: Color.lerp(\n        toolbarItemIconSelectedColor,\n        other.toolbarItemIconSelectedColor,\n        t,\n      )!,\n      toolbarItemSelectedBackgroundColor: Color.lerp(\n        toolbarItemSelectedBackgroundColor,\n        other.toolbarItemSelectedBackgroundColor,\n        t,\n      )!,\n      toolbarMenuBackgroundColor: Color.lerp(\n        toolbarMenuBackgroundColor,\n        other.toolbarMenuBackgroundColor,\n        t,\n      )!,\n      toolbarMenuItemBackgroundColor: Color.lerp(\n        toolbarMenuItemBackgroundColor,\n        other.toolbarMenuItemBackgroundColor,\n        t,\n      )!,\n      toolbarMenuItemSelectedBackgroundColor: Color.lerp(\n        toolbarMenuItemSelectedBackgroundColor,\n        other.toolbarMenuItemSelectedBackgroundColor,\n        t,\n      )!,\n      toolbarMenuIconColor:\n          Color.lerp(toolbarMenuIconColor, other.toolbarMenuIconColor, t)!,\n      toolbarMenuIconDisabledColor: Color.lerp(\n        toolbarMenuIconDisabledColor,\n        other.toolbarMenuIconDisabledColor,\n        t,\n      )!,\n      toolbarMenuIconSelectedColor: Color.lerp(\n        toolbarMenuIconSelectedColor,\n        other.toolbarMenuIconSelectedColor,\n        t,\n      )!,\n      toolbarShadowColor:\n          Color.lerp(toolbarShadowColor, other.toolbarShadowColor, t)!,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_align_items.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_bius_items.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_item.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_font_item.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_heading_and_text_items.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_indent_items.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nfinal aaToolbarItem = AppFlowyMobileToolbarItem(\n  itemBuilder: (context, editorState, service, onMenu, _) {\n    return AppFlowyMobileToolbarIconItem(\n      editorState: editorState,\n      isSelected: () => service.showMenuNotifier.value,\n      keepSelectedStatus: true,\n      icon: FlowySvgs.m_toolbar_aa_m,\n      onTap: () => onMenu?.call(),\n    );\n  },\n  menuBuilder: (context, editorState, service) {\n    final selection = editorState.selection;\n    if (selection == null) {\n      return const SizedBox.shrink();\n    }\n    return _TextDecorationMenu(\n      editorState,\n      selection,\n      service,\n    );\n  },\n);\n\nclass _TextDecorationMenu extends StatefulWidget {\n  const _TextDecorationMenu(\n    this.editorState,\n    this.selection,\n    this.service,\n  );\n\n  final EditorState editorState;\n  final Selection selection;\n  final AppFlowyMobileToolbarWidgetService service;\n\n  @override\n  State<_TextDecorationMenu> createState() => _TextDecorationMenuState();\n}\n\nclass _TextDecorationMenuState extends State<_TextDecorationMenu> {\n  EditorState get editorState => widget.editorState;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = ToolbarColorExtension.of(context);\n    return ColoredBox(\n      color: theme.toolbarMenuBackgroundColor,\n      child: SingleChildScrollView(\n        child: Padding(\n          padding: const EdgeInsets.only(\n                top: 16,\n                bottom: 20,\n                left: 12,\n                right: 12,\n              ) *\n              context.scale,\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              HeadingsAndTextItems(\n                editorState: editorState,\n              ),\n              const ScaledVSpace(),\n              Row(\n                children: [\n                  BIUSItems(\n                    editorState: editorState,\n                  ),\n                  const Spacer(),\n                  ColorItem(\n                    editorState: editorState,\n                    service: widget.service,\n                  ),\n                ],\n              ),\n              const ScaledVSpace(),\n              Row(\n                children: [\n                  BlockItems(\n                    service: widget.service,\n                    editorState: editorState,\n                  ),\n                  const Spacer(),\n                  AlignItems(\n                    editorState: editorState,\n                  ),\n                ],\n              ),\n              const ScaledVSpace(),\n              Row(\n                children: [\n                  FontFamilyItem(\n                    editorState: editorState,\n                  ),\n                  const Spacer(),\n                  IndentAndOutdentItems(\n                    service: widget.service,\n                    editorState: editorState,\n                  ),\n                ],\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_attachment_item.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/shared/permission/permission_checker.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/startup/tasks/app_widget.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/file_picker/file_picker_service.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:image_picker/image_picker.dart';\n\n@visibleForTesting\nconst addAttachmentToolbarItemKey = ValueKey('add_attachment_toolbar_item');\n\nfinal addAttachmentItem = AppFlowyMobileToolbarItem(\n  itemBuilder: (context, editorState, service, _, onAction) {\n    return AppFlowyMobileToolbarIconItem(\n      key: addAttachmentToolbarItemKey,\n      editorState: editorState,\n      icon: FlowySvgs.media_s,\n      onTap: () {\n        final documentId = context.read<DocumentBloc>().documentId;\n        final isLocalMode = context.read<DocumentBloc>().isLocalMode;\n\n        final selection = editorState.selection;\n        service.closeKeyboard();\n\n        // delay to wait the keyboard closed.\n        Future.delayed(const Duration(milliseconds: 100), () async {\n          unawaited(\n            editorState.updateSelectionWithReason(\n              selection,\n              extraInfo: {\n                selectionExtraInfoDisableMobileToolbarKey: true,\n                selectionExtraInfoDisableFloatingToolbar: true,\n                selectionExtraInfoDoNotAttachTextService: true,\n              },\n            ),\n          );\n\n          keepEditorFocusNotifier.increase();\n          final didAddAttachment = await showAddAttachmentMenu(\n            AppGlobals.rootNavKey.currentContext!,\n            documentId: documentId,\n            isLocalMode: isLocalMode,\n            editorState: editorState,\n            selection: selection!,\n          );\n\n          if (didAddAttachment != true) {\n            WidgetsBinding.instance.addPostFrameCallback((_) {\n              editorState.updateSelectionWithReason(selection);\n            });\n          }\n        });\n      },\n    );\n  },\n);\n\nFuture<bool?> showAddAttachmentMenu(\n  BuildContext context, {\n  required String documentId,\n  required bool isLocalMode,\n  required EditorState editorState,\n  required Selection selection,\n}) async =>\n    showMobileBottomSheet<bool>(\n      context,\n      showDragHandle: true,\n      barrierColor: Colors.transparent,\n      backgroundColor:\n          ToolbarColorExtension.of(context).toolbarMenuBackgroundColor,\n      elevation: 20,\n      isScrollControlled: false,\n      enableDraggableScrollable: true,\n      builder: (_) => _AddAttachmentMenu(\n        documentId: documentId,\n        isLocalMode: isLocalMode,\n        editorState: editorState,\n        selection: selection,\n      ),\n    );\n\nclass _AddAttachmentMenu extends StatelessWidget {\n  const _AddAttachmentMenu({\n    required this.documentId,\n    required this.isLocalMode,\n    required this.editorState,\n    required this.selection,\n  });\n\n  final String documentId;\n  final bool isLocalMode;\n  final EditorState editorState;\n  final Selection selection;\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleChildScrollView(\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          MobileQuickActionButton(\n            text: LocaleKeys.document_attachmentMenu_choosePhoto.tr(),\n            icon: FlowySvgs.image_rounded_s,\n            iconSize: const Size.square(20),\n            onTap: () async => selectPhoto(context),\n          ),\n          const MobileQuickActionDivider(),\n          MobileQuickActionButton(\n            text: LocaleKeys.document_attachmentMenu_takePicture.tr(),\n            icon: FlowySvgs.camera_s,\n            iconSize: const Size.square(20),\n            onTap: () async => selectCamera(context),\n          ),\n          const MobileQuickActionDivider(),\n          MobileQuickActionButton(\n            text: LocaleKeys.document_attachmentMenu_chooseFile.tr(),\n            icon: FlowySvgs.file_s,\n            iconSize: const Size.square(20),\n            onTap: () async => selectFile(context),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Future<void> _insertNode(Node node) async {\n    Future.delayed(\n      const Duration(milliseconds: 100),\n      () async {\n        // if current selected block is a empty paragraph block, replace it with the new block.\n        if (selection.isCollapsed) {\n          final path = selection.end.path;\n          final currentNode = editorState.getNodeAtPath(path);\n          final text = currentNode?.delta?.toPlainText();\n          if (currentNode != null &&\n              currentNode.type == ParagraphBlockKeys.type &&\n              text != null &&\n              text.isEmpty) {\n            final transaction = editorState.transaction;\n            transaction.insertNode(path.next, node);\n            transaction.deleteNode(currentNode);\n            transaction.afterSelection =\n                Selection.collapsed(Position(path: path));\n            transaction.selectionExtraInfo = {};\n            return editorState.apply(transaction);\n          }\n        }\n\n        await editorState.insertBlockAfterCurrentSelection(selection, node);\n      },\n    );\n  }\n\n  Future<void> insertImage(BuildContext context, XFile image) async {\n    CustomImageType type = CustomImageType.local;\n    String? path;\n    if (isLocalMode) {\n      path = await saveImageToLocalStorage(image.path);\n    } else {\n      (path, _) = await saveImageToCloudStorage(image.path, documentId);\n      type = CustomImageType.internal;\n    }\n\n    if (path != null) {\n      final node = customImageNode(url: path, type: type);\n      await _insertNode(node);\n    }\n  }\n\n  Future<void> selectPhoto(BuildContext context) async {\n    final image = await ImagePicker().pickImage(source: ImageSource.gallery);\n\n    if (image != null && context.mounted) {\n      await insertImage(context, image);\n    } else {\n      // refocus the editor\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        editorState.updateSelectionWithReason(selection);\n      });\n    }\n\n    if (context.mounted) {\n      Navigator.pop(context);\n    }\n  }\n\n  Future<void> selectCamera(BuildContext context) async {\n    final cameraPermission =\n        await PermissionChecker.checkCameraPermission(context);\n    if (!cameraPermission) {\n      Log.error('Has no permission to access the camera');\n      return;\n    }\n\n    final image = await ImagePicker().pickImage(source: ImageSource.camera);\n\n    if (image != null && context.mounted) {\n      await insertImage(context, image);\n    }\n\n    if (context.mounted) {\n      Navigator.pop(context);\n    }\n  }\n\n  Future<void> selectFile(BuildContext context) async {\n    final result = await getIt<FilePickerService>().pickFiles();\n    final file = result?.files.first.xFile;\n    if (file != null) {\n      FileUrlType type = FileUrlType.local;\n      String? path;\n      if (isLocalMode) {\n        path = await saveFileToLocalStorage(file.path);\n      } else {\n        (path, _) = await saveFileToCloudStorage(file.path, documentId);\n        type = FileUrlType.cloud;\n      }\n\n      if (path != null) {\n        final node = fileNode(url: path, type: type, name: file.name);\n        await _insertNode(node);\n      }\n    }\n\n    if (context.mounted) {\n      Navigator.pop(context);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_menu_item_builder.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_placeholder.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/startup/tasks/app_widget.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart'\n    hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys;\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nclass AddBlockMenuItemBuilder {\n  AddBlockMenuItemBuilder({\n    required this.editorState,\n    required this.selection,\n  });\n\n  final EditorState editorState;\n  final Selection selection;\n\n  List<TypeOptionMenuItemValue<String>> buildTypeOptionMenuItemValues(\n    BuildContext context,\n  ) {\n    if (selection.isCollapsed) {\n      final node = editorState.getNodeAtPath(selection.end.path);\n      if (node?.parentTableCellNode != null) {\n        return _buildTableTypeOptionMenuItemValues(context);\n      }\n    }\n    return _buildDefaultTypeOptionMenuItemValues(context);\n  }\n\n  /// Build the default type option menu item values.\n\n  List<TypeOptionMenuItemValue<String>> _buildDefaultTypeOptionMenuItemValues(\n    BuildContext context,\n  ) {\n    final colorMap = _colorMap(context);\n    return [\n      ..._buildHeadingMenuItems(colorMap),\n      ..._buildParagraphMenuItems(colorMap),\n      ..._buildTodoListMenuItems(colorMap),\n      ..._buildTableMenuItems(colorMap),\n      ..._buildQuoteMenuItems(colorMap),\n      ..._buildListMenuItems(colorMap),\n      ..._buildToggleHeadingMenuItems(colorMap),\n      ..._buildImageMenuItems(colorMap),\n      ..._buildPhotoGalleryMenuItems(colorMap),\n      ..._buildFileMenuItems(colorMap),\n      ..._buildMentionMenuItems(context, colorMap),\n      ..._buildDividerMenuItems(colorMap),\n      ..._buildCalloutMenuItems(colorMap),\n      ..._buildCodeMenuItems(colorMap),\n      ..._buildMathEquationMenuItems(colorMap),\n    ];\n  }\n\n  /// Build the table type option menu item values.\n  List<TypeOptionMenuItemValue<String>> _buildTableTypeOptionMenuItemValues(\n    BuildContext context,\n  ) {\n    final colorMap = _colorMap(context);\n    return [\n      ..._buildHeadingMenuItems(colorMap),\n      ..._buildParagraphMenuItems(colorMap),\n      ..._buildTodoListMenuItems(colorMap),\n      ..._buildQuoteMenuItems(colorMap),\n      ..._buildListMenuItems(colorMap),\n      ..._buildToggleHeadingMenuItems(colorMap),\n      ..._buildImageMenuItems(colorMap),\n      ..._buildFileMenuItems(colorMap),\n      ..._buildMentionMenuItems(context, colorMap),\n      ..._buildDividerMenuItems(colorMap),\n      ..._buildCalloutMenuItems(colorMap),\n      ..._buildCodeMenuItems(colorMap),\n      ..._buildMathEquationMenuItems(colorMap),\n    ];\n  }\n\n  List<TypeOptionMenuItemValue<String>> _buildHeadingMenuItems(\n    Map<String, Color> colorMap,\n  ) {\n    return [\n      TypeOptionMenuItemValue(\n        value: HeadingBlockKeys.type,\n        backgroundColor: colorMap[HeadingBlockKeys.type]!,\n        text: LocaleKeys.editor_heading1.tr(),\n        icon: FlowySvgs.m_add_block_h1_s,\n        onTap: (_, __) => _insertBlock(headingNode(level: 1)),\n      ),\n      TypeOptionMenuItemValue(\n        value: HeadingBlockKeys.type,\n        backgroundColor: colorMap[HeadingBlockKeys.type]!,\n        text: LocaleKeys.editor_heading2.tr(),\n        icon: FlowySvgs.m_add_block_h2_s,\n        onTap: (_, __) => _insertBlock(headingNode(level: 2)),\n      ),\n      TypeOptionMenuItemValue(\n        value: HeadingBlockKeys.type,\n        backgroundColor: colorMap[HeadingBlockKeys.type]!,\n        text: LocaleKeys.editor_heading3.tr(),\n        icon: FlowySvgs.m_add_block_h3_s,\n        onTap: (_, __) => _insertBlock(headingNode(level: 3)),\n      ),\n    ];\n  }\n\n  List<TypeOptionMenuItemValue<String>> _buildParagraphMenuItems(\n    Map<String, Color> colorMap,\n  ) {\n    return [\n      TypeOptionMenuItemValue(\n        value: ParagraphBlockKeys.type,\n        backgroundColor: colorMap[ParagraphBlockKeys.type]!,\n        text: LocaleKeys.editor_text.tr(),\n        icon: FlowySvgs.m_add_block_paragraph_s,\n        onTap: (_, __) => _insertBlock(paragraphNode()),\n      ),\n    ];\n  }\n\n  List<TypeOptionMenuItemValue<String>> _buildTodoListMenuItems(\n    Map<String, Color> colorMap,\n  ) {\n    return [\n      TypeOptionMenuItemValue(\n        value: TodoListBlockKeys.type,\n        backgroundColor: colorMap[TodoListBlockKeys.type]!,\n        text: LocaleKeys.editor_checkbox.tr(),\n        icon: FlowySvgs.m_add_block_checkbox_s,\n        onTap: (_, __) => _insertBlock(todoListNode(checked: false)),\n      ),\n    ];\n  }\n\n  List<TypeOptionMenuItemValue<String>> _buildTableMenuItems(\n    Map<String, Color> colorMap,\n  ) {\n    return [\n      TypeOptionMenuItemValue(\n        value: SimpleTableBlockKeys.type,\n        backgroundColor: colorMap[SimpleTableBlockKeys.type]!,\n        text: LocaleKeys.editor_table.tr(),\n        icon: FlowySvgs.slash_menu_icon_simple_table_s,\n        onTap: (_, __) => _insertBlock(\n          createSimpleTableBlockNode(columnCount: 2, rowCount: 2),\n        ),\n      ),\n    ];\n  }\n\n  List<TypeOptionMenuItemValue<String>> _buildQuoteMenuItems(\n    Map<String, Color> colorMap,\n  ) {\n    return [\n      TypeOptionMenuItemValue(\n        value: QuoteBlockKeys.type,\n        backgroundColor: colorMap[QuoteBlockKeys.type]!,\n        text: LocaleKeys.editor_quote.tr(),\n        icon: FlowySvgs.m_add_block_quote_s,\n        onTap: (_, __) => _insertBlock(quoteNode()),\n      ),\n    ];\n  }\n\n  List<TypeOptionMenuItemValue<String>> _buildListMenuItems(\n    Map<String, Color> colorMap,\n  ) {\n    return [\n      // bulleted list, numbered list, toggle list\n      TypeOptionMenuItemValue(\n        value: BulletedListBlockKeys.type,\n        backgroundColor: colorMap[BulletedListBlockKeys.type]!,\n        text: LocaleKeys.editor_bulletedListShortForm.tr(),\n        icon: FlowySvgs.m_add_block_bulleted_list_s,\n        onTap: (_, __) => _insertBlock(bulletedListNode()),\n      ),\n      TypeOptionMenuItemValue(\n        value: NumberedListBlockKeys.type,\n        backgroundColor: colorMap[NumberedListBlockKeys.type]!,\n        text: LocaleKeys.editor_numberedListShortForm.tr(),\n        icon: FlowySvgs.m_add_block_numbered_list_s,\n        onTap: (_, __) => _insertBlock(numberedListNode()),\n      ),\n      TypeOptionMenuItemValue(\n        value: ToggleListBlockKeys.type,\n        backgroundColor: colorMap[ToggleListBlockKeys.type]!,\n        text: LocaleKeys.editor_toggleListShortForm.tr(),\n        icon: FlowySvgs.m_add_block_toggle_s,\n        onTap: (_, __) => _insertBlock(toggleListBlockNode()),\n      ),\n    ];\n  }\n\n  List<TypeOptionMenuItemValue<String>> _buildToggleHeadingMenuItems(\n    Map<String, Color> colorMap,\n  ) {\n    return [\n      TypeOptionMenuItemValue(\n        value: ToggleListBlockKeys.type,\n        backgroundColor: colorMap[ToggleListBlockKeys.type]!,\n        text: LocaleKeys.editor_toggleHeading1ShortForm.tr(),\n        icon: FlowySvgs.toggle_heading1_s,\n        iconPadding: const EdgeInsets.all(3),\n        onTap: (_, __) => _insertBlock(toggleHeadingNode()),\n      ),\n      TypeOptionMenuItemValue(\n        value: ToggleListBlockKeys.type,\n        backgroundColor: colorMap[ToggleListBlockKeys.type]!,\n        text: LocaleKeys.editor_toggleHeading2ShortForm.tr(),\n        icon: FlowySvgs.toggle_heading2_s,\n        iconPadding: const EdgeInsets.all(3),\n        onTap: (_, __) => _insertBlock(toggleHeadingNode(level: 2)),\n      ),\n      TypeOptionMenuItemValue(\n        value: ToggleListBlockKeys.type,\n        backgroundColor: colorMap[ToggleListBlockKeys.type]!,\n        text: LocaleKeys.editor_toggleHeading3ShortForm.tr(),\n        icon: FlowySvgs.toggle_heading3_s,\n        iconPadding: const EdgeInsets.all(3),\n        onTap: (_, __) => _insertBlock(toggleHeadingNode(level: 3)),\n      ),\n    ];\n  }\n\n  List<TypeOptionMenuItemValue<String>> _buildImageMenuItems(\n    Map<String, Color> colorMap,\n  ) {\n    return [\n      TypeOptionMenuItemValue(\n        value: ImageBlockKeys.type,\n        backgroundColor: colorMap[ImageBlockKeys.type]!,\n        text: LocaleKeys.editor_image.tr(),\n        icon: FlowySvgs.m_add_block_image_s,\n        onTap: (_, __) async {\n          AppGlobals.rootNavKey.currentContext?.pop(true);\n          Future.delayed(const Duration(milliseconds: 400), () async {\n            final imagePlaceholderKey = GlobalKey<ImagePlaceholderState>();\n            await editorState.insertEmptyImageBlock(imagePlaceholderKey);\n          });\n        },\n      ),\n    ];\n  }\n\n  List<TypeOptionMenuItemValue<String>> _buildPhotoGalleryMenuItems(\n    Map<String, Color> colorMap,\n  ) {\n    return [\n      TypeOptionMenuItemValue(\n        value: MultiImageBlockKeys.type,\n        backgroundColor: colorMap[ImageBlockKeys.type]!,\n        text: LocaleKeys.document_plugins_photoGallery_name.tr(),\n        icon: FlowySvgs.m_add_block_photo_gallery_s,\n        onTap: (_, __) async {\n          AppGlobals.rootNavKey.currentContext?.pop(true);\n          Future.delayed(const Duration(milliseconds: 400), () async {\n            final imagePlaceholderKey = GlobalKey<MultiImagePlaceholderState>();\n            await editorState.insertEmptyMultiImageBlock(imagePlaceholderKey);\n          });\n        },\n      ),\n    ];\n  }\n\n  List<TypeOptionMenuItemValue<String>> _buildFileMenuItems(\n    Map<String, Color> colorMap,\n  ) {\n    return [\n      TypeOptionMenuItemValue(\n        value: FileBlockKeys.type,\n        backgroundColor: colorMap[ImageBlockKeys.type]!,\n        text: LocaleKeys.document_plugins_file_name.tr(),\n        icon: FlowySvgs.media_s,\n        onTap: (_, __) async {\n          AppGlobals.rootNavKey.currentContext?.pop(true);\n          Future.delayed(const Duration(milliseconds: 400), () async {\n            final fileGlobalKey = GlobalKey<FileBlockComponentState>();\n            await editorState.insertEmptyFileBlock(fileGlobalKey);\n          });\n        },\n      ),\n    ];\n  }\n\n  List<TypeOptionMenuItemValue<String>> _buildMentionMenuItems(\n    BuildContext context,\n    Map<String, Color> colorMap,\n  ) {\n    return [\n      TypeOptionMenuItemValue(\n        value: ParagraphBlockKeys.type,\n        backgroundColor: colorMap[MentionBlockKeys.type]!,\n        text: LocaleKeys.editor_date.tr(),\n        icon: FlowySvgs.m_add_block_date_s,\n        onTap: (_, __) => _insertBlock(dateMentionNode()),\n      ),\n      TypeOptionMenuItemValue(\n        value: ParagraphBlockKeys.type,\n        backgroundColor: colorMap[MentionBlockKeys.type]!,\n        text: LocaleKeys.editor_page.tr(),\n        icon: FlowySvgs.icon_document_s,\n        onTap: (_, __) async {\n          AppGlobals.rootNavKey.currentContext?.pop(true);\n\n          final currentViewId = getIt<MenuSharedState>().latestOpenView?.id;\n          final view = await showPageSelectorSheet(\n            context,\n            currentViewId: currentViewId,\n          );\n\n          if (view != null) {\n            Future.delayed(const Duration(milliseconds: 100), () {\n              editorState.insertBlockAfterCurrentSelection(\n                selection,\n                pageMentionNode(view.id),\n              );\n            });\n          }\n        },\n      ),\n    ];\n  }\n\n  List<TypeOptionMenuItemValue<String>> _buildDividerMenuItems(\n    Map<String, Color> colorMap,\n  ) {\n    return [\n      TypeOptionMenuItemValue(\n        value: DividerBlockKeys.type,\n        backgroundColor: colorMap[DividerBlockKeys.type]!,\n        text: LocaleKeys.editor_divider.tr(),\n        icon: FlowySvgs.m_add_block_divider_s,\n        onTap: (_, __) {\n          AppGlobals.rootNavKey.currentContext?.pop(true);\n          Future.delayed(const Duration(milliseconds: 100), () {\n            editorState.insertDivider(selection);\n          });\n        },\n      ),\n    ];\n  }\n\n  // callout, code, math equation\n  List<TypeOptionMenuItemValue<String>> _buildCalloutMenuItems(\n    Map<String, Color> colorMap,\n  ) {\n    return [\n      TypeOptionMenuItemValue(\n        value: CalloutBlockKeys.type,\n        backgroundColor: colorMap[CalloutBlockKeys.type]!,\n        text: LocaleKeys.document_plugins_callout.tr(),\n        icon: FlowySvgs.m_add_block_callout_s,\n        onTap: (_, __) => _insertBlock(calloutNode()),\n      ),\n    ];\n  }\n\n  List<TypeOptionMenuItemValue<String>> _buildCodeMenuItems(\n    Map<String, Color> colorMap,\n  ) {\n    return [\n      TypeOptionMenuItemValue(\n        value: CodeBlockKeys.type,\n        backgroundColor: colorMap[CodeBlockKeys.type]!,\n        text: LocaleKeys.editor_codeBlockShortForm.tr(),\n        icon: FlowySvgs.m_add_block_code_s,\n        onTap: (_, __) => _insertBlock(codeBlockNode()),\n      ),\n    ];\n  }\n\n  List<TypeOptionMenuItemValue<String>> _buildMathEquationMenuItems(\n    Map<String, Color> colorMap,\n  ) {\n    return [\n      TypeOptionMenuItemValue(\n        value: MathEquationBlockKeys.type,\n        backgroundColor: colorMap[MathEquationBlockKeys.type]!,\n        text: LocaleKeys.editor_mathEquationShortForm.tr(),\n        icon: FlowySvgs.m_add_block_formula_s,\n        onTap: (_, __) {\n          AppGlobals.rootNavKey.currentContext?.pop(true);\n          Future.delayed(const Duration(milliseconds: 100), () {\n            editorState.insertMathEquation(selection);\n          });\n        },\n      ),\n    ];\n  }\n\n  Map<String, Color> _colorMap(BuildContext context) {\n    final isDarkMode = Theme.of(context).brightness == Brightness.dark;\n    if (isDarkMode) {\n      return {\n        HeadingBlockKeys.type: const Color(0xFF5465A1),\n        ParagraphBlockKeys.type: const Color(0xFF5465A1),\n        TodoListBlockKeys.type: const Color(0xFF4BB299),\n        SimpleTableBlockKeys.type: const Color(0xFF4BB299),\n        QuoteBlockKeys.type: const Color(0xFFBAAC74),\n        BulletedListBlockKeys.type: const Color(0xFFA35F94),\n        NumberedListBlockKeys.type: const Color(0xFFA35F94),\n        ToggleListBlockKeys.type: const Color(0xFFA35F94),\n        ImageBlockKeys.type: const Color(0xFFBAAC74),\n        MentionBlockKeys.type: const Color(0xFF40AAB8),\n        DividerBlockKeys.type: const Color(0xFF4BB299),\n        CalloutBlockKeys.type: const Color(0xFF66599B),\n        CodeBlockKeys.type: const Color(0xFF66599B),\n        MathEquationBlockKeys.type: const Color(0xFF66599B),\n      };\n    }\n    return {\n      HeadingBlockKeys.type: const Color(0xFFBECCFF),\n      ParagraphBlockKeys.type: const Color(0xFFBECCFF),\n      TodoListBlockKeys.type: const Color(0xFF98F4CD),\n      SimpleTableBlockKeys.type: const Color(0xFF98F4CD),\n      QuoteBlockKeys.type: const Color(0xFFFDEDA7),\n      BulletedListBlockKeys.type: const Color(0xFFFFB9EF),\n      NumberedListBlockKeys.type: const Color(0xFFFFB9EF),\n      ToggleListBlockKeys.type: const Color(0xFFFFB9EF),\n      ImageBlockKeys.type: const Color(0xFFFDEDA7),\n      MentionBlockKeys.type: const Color(0xFF91EAF5),\n      DividerBlockKeys.type: const Color(0xFF98F4CD),\n      CalloutBlockKeys.type: const Color(0xFFCABDFF),\n      CodeBlockKeys.type: const Color(0xFFCABDFF),\n      MathEquationBlockKeys.type: const Color(0xFFCABDFF),\n    };\n  }\n\n  Future<void> _insertBlock(Node node) async {\n    AppGlobals.rootNavKey.currentContext?.pop(true);\n    Future.delayed(\n      const Duration(milliseconds: 100),\n      () async {\n        // if current selected block is a empty paragraph block, replace it with the new block.\n        if (selection.isCollapsed) {\n          final currentNode = editorState.getNodeAtPath(selection.end.path);\n          final text = currentNode?.delta?.toPlainText();\n          if (currentNode != null &&\n              currentNode.type == ParagraphBlockKeys.type &&\n              text != null &&\n              text.isEmpty) {\n            final transaction = editorState.transaction;\n            transaction.insertNode(\n              selection.end.path.next,\n              node,\n            );\n            transaction.deleteNode(currentNode);\n            if (node.type == SimpleTableBlockKeys.type) {\n              transaction.afterSelection = Selection.collapsed(\n                Position(\n                  // table -> row -> cell -> paragraph\n                  path: selection.end.path + [0, 0, 0],\n                ),\n              );\n            } else {\n              transaction.afterSelection = Selection.collapsed(\n                Position(path: selection.end.path),\n              );\n            }\n            transaction.selectionExtraInfo = {};\n            await editorState.apply(transaction);\n            return;\n          }\n        }\n\n        await editorState.insertBlockAfterCurrentSelection(selection, node);\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/startup/tasks/app_widget.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nimport 'add_block_menu_item_builder.dart';\n\n@visibleForTesting\nconst addBlockToolbarItemKey = ValueKey('add_block_toolbar_item');\n\nfinal addBlockToolbarItem = AppFlowyMobileToolbarItem(\n  itemBuilder: (context, editorState, service, __, onAction) {\n    return AppFlowyMobileToolbarIconItem(\n      key: addBlockToolbarItemKey,\n      editorState: editorState,\n      icon: FlowySvgs.m_toolbar_add_m,\n      onTap: () {\n        final selection = editorState.selection;\n        service.closeKeyboard();\n\n        // delay to wait the keyboard closed.\n        Future.delayed(const Duration(milliseconds: 100), () async {\n          unawaited(\n            editorState.updateSelectionWithReason(\n              selection,\n              extraInfo: {\n                selectionExtraInfoDisableMobileToolbarKey: true,\n                selectionExtraInfoDisableFloatingToolbar: true,\n                selectionExtraInfoDoNotAttachTextService: true,\n              },\n            ),\n          );\n          keepEditorFocusNotifier.increase();\n          final didAddBlock = await showAddBlockMenu(\n            AppGlobals.rootNavKey.currentContext!,\n            editorState: editorState,\n            selection: selection!,\n          );\n          if (didAddBlock != true) {\n            unawaited(editorState.updateSelectionWithReason(selection));\n          }\n        });\n      },\n    );\n  },\n);\n\nFuture<bool?> showAddBlockMenu(\n  BuildContext context, {\n  required EditorState editorState,\n  required Selection selection,\n}) async =>\n    showMobileBottomSheet<bool>(\n      context,\n      showHeader: true,\n      showDragHandle: true,\n      showCloseButton: true,\n      title: LocaleKeys.button_add.tr(),\n      barrierColor: Colors.transparent,\n      backgroundColor:\n          ToolbarColorExtension.of(context).toolbarMenuBackgroundColor,\n      elevation: 20,\n      enableDraggableScrollable: true,\n      builder: (_) => Padding(\n        padding: EdgeInsets.all(16 * context.scale),\n        child: AddBlockMenu(selection: selection, editorState: editorState),\n      ),\n    );\n\nclass AddBlockMenu extends StatelessWidget {\n  const AddBlockMenu({\n    super.key,\n    required this.selection,\n    required this.editorState,\n  });\n\n  final Selection selection;\n  final EditorState editorState;\n\n  @override\n  Widget build(BuildContext context) {\n    final builder = AddBlockMenuItemBuilder(\n      editorState: editorState,\n      selection: selection,\n    );\n    return TypeOptionMenu<String>(\n      values: builder.buildTypeOptionMenuItemValues(context),\n      scaleFactor: context.scale,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\nimport 'dart:math';\n\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_close_keyboard_or_menu_button.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/keyboard_height_observer.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:provider/provider.dart';\nimport 'package:scrollable_positioned_list/scrollable_positioned_list.dart';\n\nabstract class AppFlowyMobileToolbarWidgetService {\n  void closeItemMenu();\n\n  void closeKeyboard();\n\n  PropertyValueNotifier<bool> get showMenuNotifier;\n}\n\nclass AppFlowyMobileToolbar extends StatefulWidget {\n  const AppFlowyMobileToolbar({\n    super.key,\n    this.toolbarHeight = 50.0,\n    required this.editorState,\n    required this.toolbarItemsBuilder,\n    required this.child,\n  });\n\n  final EditorState editorState;\n  final double toolbarHeight;\n  final List<AppFlowyMobileToolbarItem> Function(\n    Selection? selection,\n  ) toolbarItemsBuilder;\n  final Widget child;\n\n  @override\n  State<AppFlowyMobileToolbar> createState() => _AppFlowyMobileToolbarState();\n}\n\nclass _AppFlowyMobileToolbarState extends State<AppFlowyMobileToolbar> {\n  OverlayEntry? toolbarOverlay;\n\n  final isKeyboardShow = ValueNotifier(false);\n\n  @override\n  void initState() {\n    super.initState();\n\n    _insertKeyboardToolbar();\n    KeyboardHeightObserver.instance.addListener(_onKeyboardHeightChanged);\n  }\n\n  @override\n  void dispose() {\n    _removeKeyboardToolbar();\n    KeyboardHeightObserver.instance.removeListener(_onKeyboardHeightChanged);\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        Expanded(child: widget.child),\n        // add a bottom offset to make sure the toolbar is above the keyboard\n        ValueListenableBuilder(\n          valueListenable: isKeyboardShow,\n          builder: (context, isKeyboardShow, __) {\n            return SizedBox(\n              // only adding padding when the keyboard is triggered by editor\n              height: isKeyboardShow && widget.editorState.selection != null\n                  ? widget.toolbarHeight\n                  : 0,\n            );\n          },\n        ),\n      ],\n    );\n  }\n\n  void _onKeyboardHeightChanged(double height) {\n    isKeyboardShow.value = height > 0;\n  }\n\n  void _removeKeyboardToolbar() {\n    toolbarOverlay?.remove();\n    toolbarOverlay?.dispose();\n    toolbarOverlay = null;\n  }\n\n  void _insertKeyboardToolbar() {\n    _removeKeyboardToolbar();\n\n    Widget child = ValueListenableBuilder<Selection?>(\n      valueListenable: widget.editorState.selectionNotifier,\n      builder: (_, Selection? selection, __) {\n        // if the selection is null, hide the toolbar\n        if (selection == null ||\n            widget.editorState.selectionExtraInfo?[\n                    selectionExtraInfoDisableMobileToolbarKey] ==\n                true) {\n          return const SizedBox.shrink();\n        }\n\n        return RepaintBoundary(\n          child: BlocProvider.value(\n            value: context.read<DocumentBloc>(),\n            child: _MobileToolbar(\n              editorState: widget.editorState,\n              toolbarItems: widget.toolbarItemsBuilder(selection),\n              toolbarHeight: widget.toolbarHeight,\n            ),\n          ),\n        );\n      },\n    );\n\n    child = Stack(\n      children: [\n        Positioned(\n          left: 0,\n          right: 0,\n          bottom: 0,\n          child: Material(\n            child: child,\n          ),\n        ),\n      ],\n    );\n\n    final router = GoRouter.of(context);\n\n    toolbarOverlay = OverlayEntry(\n      builder: (context) {\n        return Provider.value(\n          value: router,\n          child: child,\n        );\n      },\n    );\n\n    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n      Overlay.of(context, rootOverlay: true).insert(toolbarOverlay!);\n    });\n  }\n}\n\nclass _MobileToolbar extends StatefulWidget {\n  const _MobileToolbar({\n    required this.editorState,\n    required this.toolbarItems,\n    required this.toolbarHeight,\n  });\n\n  final EditorState editorState;\n  final List<AppFlowyMobileToolbarItem> toolbarItems;\n  final double toolbarHeight;\n\n  @override\n  State<_MobileToolbar> createState() => _MobileToolbarState();\n}\n\nclass _MobileToolbarState extends State<_MobileToolbar>\n    implements AppFlowyMobileToolbarWidgetService {\n  // used to control the toolbar menu items\n  @override\n  PropertyValueNotifier<bool> showMenuNotifier = PropertyValueNotifier(false);\n\n  // when the users click the menu item, the keyboard will be hidden,\n  //  but in this case, we don't want to update the cached keyboard height.\n  // This is because we want to keep the same height when the menu is shown.\n  bool canUpdateCachedKeyboardHeight = true;\n\n  /// when the [_MobileToolbar] disposed before the keyboard height can be updated in time,\n  /// there will be an issue with the height being 0\n  /// this is used to globally record the height.\n  static double _globalCachedKeyboardHeight = 0.0;\n  ValueNotifier<double> cachedKeyboardHeight =\n      ValueNotifier(_globalCachedKeyboardHeight);\n\n  // used to check if click the same item again\n  int? selectedMenuIndex;\n\n  Selection? currentSelection;\n\n  bool closeKeyboardInitiative = false;\n\n  final ScrollOffsetListener offsetListener = ScrollOffsetListener.create();\n  late final StreamSubscription offsetSubscription;\n  ValueNotifier<double> toolbarOffset = ValueNotifier(0.0);\n\n  @override\n  void initState() {\n    super.initState();\n\n    currentSelection = widget.editorState.selection;\n    KeyboardHeightObserver.instance.addListener(_onKeyboardHeightChanged);\n    offsetSubscription = offsetListener.changes.listen((event) {\n      toolbarOffset.value += event;\n    });\n  }\n\n  @override\n  void didUpdateWidget(covariant _MobileToolbar oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    if (currentSelection != widget.editorState.selection) {\n      currentSelection = widget.editorState.selection;\n      closeItemMenu();\n      if (currentSelection != null) {\n        _showKeyboard();\n      }\n    }\n  }\n\n  @override\n  void dispose() {\n    showMenuNotifier.dispose();\n    cachedKeyboardHeight.dispose();\n    KeyboardHeightObserver.instance.removeListener(_onKeyboardHeightChanged);\n    offsetSubscription.cancel();\n    toolbarOffset.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  void reassemble() {\n    super.reassemble();\n\n    canUpdateCachedKeyboardHeight = true;\n    closeItemMenu();\n    _closeKeyboard();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    // toolbar\n    //  - if the menu is shown, the toolbar will be pushed up by the height of the menu\n    //  - otherwise, add a spacer to push the toolbar up when the keyboard is shown\n    return Column(\n      children: [\n        const Divider(\n          height: 0.5,\n          color: Color(0x7FEDEDED),\n        ),\n        _buildToolbar(context),\n        const Divider(\n          height: 0.5,\n          color: Color(0x7FEDEDED),\n        ),\n        _buildMenuOrSpacer(context),\n      ],\n    );\n  }\n\n  @override\n  void closeItemMenu() {\n    showMenuNotifier.value = false;\n  }\n\n  @override\n  void closeKeyboard() {\n    _closeKeyboard();\n  }\n\n  void showItemMenu() {\n    showMenuNotifier.value = true;\n  }\n\n  void _onKeyboardHeightChanged(double height) {\n    // if the keyboard is not closed initiative, we need to close the menu at same time\n    if (!closeKeyboardInitiative &&\n        cachedKeyboardHeight.value != 0 &&\n        height == 0) {\n      if (!widget.editorState.isDisposed) {\n        widget.editorState.selection = null;\n      }\n    }\n\n    // if the menu is shown and the height is not 0, we need to close the menu\n    if (showMenuNotifier.value && height != 0) {\n      closeItemMenu();\n    }\n\n    if (canUpdateCachedKeyboardHeight) {\n      cachedKeyboardHeight.value = height;\n\n      if (defaultTargetPlatform == TargetPlatform.android) {\n        // cache the keyboard height with the view padding in Android\n        if (cachedKeyboardHeight.value != 0) {\n          cachedKeyboardHeight.value +=\n              MediaQuery.of(context).viewPadding.bottom;\n        }\n      }\n    }\n\n    if (height == 0) {\n      closeKeyboardInitiative = false;\n    }\n  }\n\n  // toolbar list view and close keyboard/menu button\n  Widget _buildToolbar(BuildContext context) {\n    final theme = ToolbarColorExtension.of(context);\n    return Container(\n      height: widget.toolbarHeight,\n      width: MediaQuery.of(context).size.width,\n      decoration: BoxDecoration(\n        color: theme.toolbarBackgroundColor,\n        boxShadow: const [\n          BoxShadow(\n            color: Color(0x0F181818),\n            blurRadius: 40,\n            offset: Offset(0, -4),\n          ),\n        ],\n      ),\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          // toolbar list view\n          Expanded(\n            child: _ToolbarItemListView(\n              offsetListener: offsetListener,\n              toolbarItems: widget.toolbarItems,\n              editorState: widget.editorState,\n              toolbarWidgetService: this,\n              itemWithActionOnPressed: (_) {\n                if (showMenuNotifier.value) {\n                  closeItemMenu();\n                  _showKeyboard();\n                  // update the cached keyboard height after the keyboard is shown\n                  Debounce.debounce('canUpdateCachedKeyboardHeight',\n                      const Duration(milliseconds: 500), () {\n                    canUpdateCachedKeyboardHeight = true;\n                  });\n                }\n              },\n              itemWithMenuOnPressed: (index) {\n                // click the same one\n                if (selectedMenuIndex == index && showMenuNotifier.value) {\n                  // if the menu is shown, close it and show the keyboard\n                  closeItemMenu();\n                  _showKeyboard();\n                  // update the cached keyboard height after the keyboard is shown\n                  Debounce.debounce('canUpdateCachedKeyboardHeight',\n                      const Duration(milliseconds: 500), () {\n                    canUpdateCachedKeyboardHeight = true;\n                  });\n                } else {\n                  canUpdateCachedKeyboardHeight = false;\n                  selectedMenuIndex = index;\n                  showItemMenu();\n                  closeKeyboardInitiative = true;\n                  _closeKeyboard();\n                }\n              },\n            ),\n          ),\n          const Padding(\n            padding: EdgeInsets.symmetric(vertical: 13.0),\n            child: VerticalDivider(\n              width: 1.0,\n              thickness: 1.0,\n              color: Color(0xFFD9D9D9),\n            ),\n          ),\n          // close menu or close keyboard button\n          CloseKeyboardOrMenuButton(\n            onPressed: () {\n              closeKeyboardInitiative = true;\n              // close the keyboard and clear the selection\n              // if the selection is null, the keyboard and the toolbar will be hidden automatically\n              widget.editorState.selection = null;\n\n              // sometimes, the keyboard is not closed after the selection is cleared\n              if (Platform.isAndroid) {\n                SystemChannels.textInput.invokeMethod('TextInput.hide');\n              }\n            },\n          ),\n          const HSpace(4.0),\n        ],\n      ),\n    );\n  }\n\n  // if there's no menu, we need to add a spacer to push the toolbar up when the keyboard is shown\n  Widget _buildMenuOrSpacer(BuildContext context) {\n    return ValueListenableBuilder(\n      valueListenable: cachedKeyboardHeight,\n      builder: (_, height, ___) {\n        return ValueListenableBuilder(\n          valueListenable: showMenuNotifier,\n          builder: (_, showingMenu, __) {\n            var keyboardHeight = height;\n            if (defaultTargetPlatform == TargetPlatform.android) {\n              if (!showingMenu) {\n                // take the max value of the keyboard height and the view padding\n                // to make sure the toolbar is above the keyboard\n                keyboardHeight = max(\n                  keyboardHeight,\n                  MediaQuery.of(context).viewInsets.bottom,\n                );\n              }\n            }\n            if (keyboardHeight > 0) {\n              _globalCachedKeyboardHeight = keyboardHeight;\n            }\n            return SizedBox(\n              height: keyboardHeight,\n              child: (showingMenu && selectedMenuIndex != null)\n                  ? widget.toolbarItems[selectedMenuIndex!].menuBuilder?.call(\n                        context,\n                        widget.editorState,\n                        this,\n                      ) ??\n                      const SizedBox.shrink()\n                  : const SizedBox.shrink(),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  void _showKeyboard() {\n    final selection = widget.editorState.selection;\n    if (selection != null) {\n      widget.editorState.service.keyboardService?.enableKeyBoard(selection);\n    }\n  }\n\n  void _closeKeyboard() {\n    widget.editorState.service.keyboardService?.closeKeyboard();\n  }\n}\n\nclass _ToolbarItemListView extends StatefulWidget {\n  const _ToolbarItemListView({\n    required this.offsetListener,\n    required this.toolbarItems,\n    required this.editorState,\n    required this.toolbarWidgetService,\n    required this.itemWithMenuOnPressed,\n    required this.itemWithActionOnPressed,\n  });\n\n  final Function(int index) itemWithMenuOnPressed;\n  final Function(int index) itemWithActionOnPressed;\n  final List<AppFlowyMobileToolbarItem> toolbarItems;\n  final EditorState editorState;\n  final AppFlowyMobileToolbarWidgetService toolbarWidgetService;\n  final ScrollOffsetListener offsetListener;\n\n  @override\n  State<_ToolbarItemListView> createState() => _ToolbarItemListViewState();\n}\n\nclass _ToolbarItemListViewState extends State<_ToolbarItemListView> {\n  final scrollController = ItemScrollController();\n  Selection? previousSelection;\n\n  @override\n  void initState() {\n    super.initState();\n\n    widget.editorState.selectionNotifier\n        .addListener(_debounceUpdatePilotPosition);\n    previousSelection = widget.editorState.selection;\n  }\n\n  @override\n  void dispose() {\n    widget.editorState.selectionNotifier\n        .removeListener(_debounceUpdatePilotPosition);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    const left = 8.0;\n    const right = 4.0;\n    // 68.0 is the width of the close keyboard/menu button\n    final padding = _calculatePadding(left + right + 68.0);\n\n    final children = [\n      const HSpace(left),\n      ...widget.toolbarItems\n          .mapIndexed(\n            (index, element) => element.itemBuilder.call(\n              context,\n              widget.editorState,\n              widget.toolbarWidgetService,\n              element.menuBuilder != null\n                  ? () {\n                      widget.itemWithMenuOnPressed(index);\n                    }\n                  : null,\n              element.menuBuilder == null\n                  ? () {\n                      widget.itemWithActionOnPressed(index);\n                    }\n                  : null,\n            ),\n          )\n          .map((e) => [e, HSpace(padding)])\n          .flattened,\n      const HSpace(right),\n    ];\n\n    return PageStorage(\n      bucket: PageStorageBucket(),\n      child: ScrollablePositionedList.builder(\n        physics: const ClampingScrollPhysics(),\n        scrollOffsetListener: widget.offsetListener,\n        itemScrollController: scrollController,\n        scrollDirection: Axis.horizontal,\n        itemBuilder: (context, index) => children[index],\n        itemCount: children.length,\n      ),\n    );\n  }\n\n  double _calculatePadding(double extent) {\n    final screenWidth = MediaQuery.of(context).size.width;\n    final width = screenWidth - extent;\n    final int count;\n    if (screenWidth <= 340) {\n      count = 5;\n    } else if (screenWidth <= 384) {\n      count = 6;\n    } else if (screenWidth <= 430) {\n      count = 7;\n    } else {\n      count = 8;\n    }\n    // left + item count * width + item count * padding + right + close button width = screenWidth\n    return (width - count * 40.0) / count;\n  }\n\n  void _debounceUpdatePilotPosition() {\n    Debounce.debounce(\n      'updatePilotPosition',\n      const Duration(milliseconds: 250),\n      _updatePilotPosition,\n    );\n  }\n\n  void _updatePilotPosition() {\n    final selection = widget.editorState.selection;\n    if (selection == null) {\n      return;\n    }\n\n    if (previousSelection != null &&\n        previousSelection!.isCollapsed == selection.isCollapsed) {\n      return;\n    }\n\n    final toolbarItems = widget.toolbarItems;\n    // use -0.4 to make sure the pilot is in the front of the toolbar item\n    final alignment = selection.isCollapsed ? 0.0 : -0.4;\n    final index = toolbarItems.indexWhere(\n      (element) => selection.isCollapsed\n          ? element.pilotAtCollapsedSelection\n          : element.pilotAtExpandedSelection,\n    );\n    if (index != -1) {\n      scrollController.scrollTo(\n        alignment: alignment,\n        index: index,\n        duration: const Duration(\n          milliseconds: 250,\n        ),\n      );\n    }\n\n    previousSelection = selection;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/mobile/presentation/base/animated_gesture.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\n// build the toolbar item, like Aa, +, image ...\ntypedef AppFlowyMobileToolbarItemBuilder = Widget Function(\n  BuildContext context,\n  EditorState editorState,\n  AppFlowyMobileToolbarWidgetService service,\n  VoidCallback? onMenuCallback,\n  VoidCallback? onActionCallback,\n);\n\n// build the menu after clicking the toolbar item\ntypedef AppFlowyMobileToolbarItemMenuBuilder = Widget Function(\n  BuildContext context,\n  EditorState editorState,\n  AppFlowyMobileToolbarWidgetService service,\n);\n\nclass AppFlowyMobileToolbarItem {\n  /// Tool bar item that implements attribute directly(without opening menu)\n  const AppFlowyMobileToolbarItem({\n    required this.itemBuilder,\n    this.menuBuilder,\n    this.pilotAtCollapsedSelection = false,\n    this.pilotAtExpandedSelection = false,\n  });\n\n  final AppFlowyMobileToolbarItemBuilder itemBuilder;\n  final AppFlowyMobileToolbarItemMenuBuilder? menuBuilder;\n  final bool pilotAtCollapsedSelection;\n  final bool pilotAtExpandedSelection;\n}\n\nclass AppFlowyMobileToolbarIconItem extends StatefulWidget {\n  const AppFlowyMobileToolbarIconItem({\n    super.key,\n    this.icon,\n    this.keepSelectedStatus = false,\n    this.iconBuilder,\n    this.isSelected,\n    this.shouldListenToToggledStyle = false,\n    this.enable,\n    required this.onTap,\n    required this.editorState,\n  });\n\n  final FlowySvgData? icon;\n  final bool keepSelectedStatus;\n  final VoidCallback onTap;\n  final WidgetBuilder? iconBuilder;\n  final bool Function()? isSelected;\n  final bool shouldListenToToggledStyle;\n  final EditorState editorState;\n  final bool Function()? enable;\n\n  @override\n  State<AppFlowyMobileToolbarIconItem> createState() =>\n      _AppFlowyMobileToolbarIconItemState();\n}\n\nclass _AppFlowyMobileToolbarIconItemState\n    extends State<AppFlowyMobileToolbarIconItem> {\n  bool isSelected = false;\n  StreamSubscription? _subscription;\n\n  @override\n  void initState() {\n    super.initState();\n\n    isSelected = widget.isSelected?.call() ?? false;\n    if (widget.shouldListenToToggledStyle) {\n      widget.editorState.toggledStyleNotifier.addListener(_rebuild);\n      _subscription = widget.editorState.transactionStream.listen((_) {\n        _rebuild();\n      });\n    }\n  }\n\n  @override\n  void dispose() {\n    if (widget.shouldListenToToggledStyle) {\n      widget.editorState.toggledStyleNotifier.removeListener(_rebuild);\n      _subscription?.cancel();\n    }\n    super.dispose();\n  }\n\n  @override\n  void didUpdateWidget(covariant AppFlowyMobileToolbarIconItem oldWidget) {\n    super.didUpdateWidget(oldWidget);\n\n    if (widget.isSelected != null) {\n      isSelected = widget.isSelected!.call();\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = ToolbarColorExtension.of(context);\n    final enable = widget.enable?.call() ?? true;\n    return Padding(\n      padding: const EdgeInsets.symmetric(vertical: 5),\n      child: AnimatedGestureDetector(\n        scaleFactor: 0.95,\n        onTapUp: () {\n          widget.onTap();\n          _rebuild();\n        },\n        child: widget.iconBuilder?.call(context) ??\n            Container(\n              width: 40,\n              padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),\n              decoration: BoxDecoration(\n                borderRadius: BorderRadius.circular(9),\n                color: isSelected\n                    ? theme.toolbarItemSelectedBackgroundColor\n                    : null,\n              ),\n              child: FlowySvg(\n                widget.icon!,\n                color: enable\n                    ? theme.toolbarItemIconColor\n                    : theme.toolbarItemIconDisabledColor,\n              ),\n            ),\n      ),\n    );\n  }\n\n  void _rebuild() {\n    if (!mounted) {\n      return;\n    }\n    setState(() {\n      isSelected = (widget.keepSelectedStatus && widget.isSelected == null)\n          ? !isSelected\n          : widget.isSelected?.call() ?? false;\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/basic_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/font_colors.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/widgets.dart';\n\nimport 'link_toolbar_item.dart';\n\nfinal boldToolbarItem = AppFlowyMobileToolbarItem(\n  itemBuilder: (context, editorState, _, __, onAction) {\n    return AppFlowyMobileToolbarIconItem(\n      editorState: editorState,\n      shouldListenToToggledStyle: true,\n      isSelected: () =>\n          editorState.isTextDecorationSelected(\n            AppFlowyRichTextKeys.bold,\n          ) &&\n          editorState.toggledStyle[AppFlowyRichTextKeys.bold] != false,\n      icon: FlowySvgs.m_toolbar_bold_m,\n      onTap: () async => editorState.toggleAttribute(\n        AppFlowyRichTextKeys.bold,\n        selectionExtraInfo: {\n          selectionExtraInfoDisableFloatingToolbar: true,\n        },\n      ),\n    );\n  },\n);\n\nfinal italicToolbarItem = AppFlowyMobileToolbarItem(\n  itemBuilder: (context, editorState, _, __, onAction) {\n    return AppFlowyMobileToolbarIconItem(\n      editorState: editorState,\n      shouldListenToToggledStyle: true,\n      isSelected: () => editorState.isTextDecorationSelected(\n        AppFlowyRichTextKeys.italic,\n      ),\n      icon: FlowySvgs.m_toolbar_italic_m,\n      onTap: () async => editorState.toggleAttribute(\n        AppFlowyRichTextKeys.italic,\n        selectionExtraInfo: {\n          selectionExtraInfoDisableFloatingToolbar: true,\n        },\n      ),\n    );\n  },\n);\n\nfinal underlineToolbarItem = AppFlowyMobileToolbarItem(\n  itemBuilder: (context, editorState, _, __, onAction) {\n    return AppFlowyMobileToolbarIconItem(\n      editorState: editorState,\n      shouldListenToToggledStyle: true,\n      isSelected: () => editorState.isTextDecorationSelected(\n        AppFlowyRichTextKeys.underline,\n      ),\n      icon: FlowySvgs.m_toolbar_underline_m,\n      onTap: () async => editorState.toggleAttribute(\n        AppFlowyRichTextKeys.underline,\n        selectionExtraInfo: {\n          selectionExtraInfoDisableFloatingToolbar: true,\n        },\n      ),\n    );\n  },\n);\n\nfinal strikethroughToolbarItem = AppFlowyMobileToolbarItem(\n  itemBuilder: (context, editorState, _, __, onAction) {\n    return AppFlowyMobileToolbarIconItem(\n      editorState: editorState,\n      shouldListenToToggledStyle: true,\n      isSelected: () => editorState.isTextDecorationSelected(\n        AppFlowyRichTextKeys.strikethrough,\n      ),\n      icon: FlowySvgs.m_toolbar_strike_m,\n      onTap: () async => editorState.toggleAttribute(\n        AppFlowyRichTextKeys.strikethrough,\n        selectionExtraInfo: {\n          selectionExtraInfoDisableFloatingToolbar: true,\n        },\n      ),\n    );\n  },\n);\n\nfinal colorToolbarItem = AppFlowyMobileToolbarItem(\n  itemBuilder: (context, editorState, service, __, onAction) {\n    return AppFlowyMobileToolbarIconItem(\n      editorState: editorState,\n      shouldListenToToggledStyle: true,\n      icon: FlowySvgs.m_aa_font_color_m,\n      iconBuilder: (context) {\n        String? getColor(String key) {\n          final selection = editorState.selection;\n          if (selection == null) {\n            return null;\n          }\n          String? color = editorState.toggledStyle[key];\n          if (color == null) {\n            if (selection.isCollapsed && selection.startIndex != 0) {\n              color = editorState.getDeltaAttributeValueInSelection<String>(\n                key,\n                selection.copyWith(\n                  start: selection.start.copyWith(\n                    offset: selection.startIndex - 1,\n                  ),\n                ),\n              );\n            } else {\n              color = editorState.getDeltaAttributeValueInSelection<String>(\n                key,\n              );\n            }\n          }\n          return color;\n        }\n\n        final textColor = getColor(AppFlowyRichTextKeys.textColor);\n        final backgroundColor = getColor(AppFlowyRichTextKeys.backgroundColor);\n\n        return Container(\n          width: 40,\n          padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),\n          decoration: BoxDecoration(\n            borderRadius: BorderRadius.circular(9),\n            color: EditorFontColors.fromBuiltInColors(\n              context,\n              backgroundColor?.tryToColor(),\n            ),\n          ),\n          child: FlowySvg(\n            FlowySvgs.m_aa_font_color_m,\n            color: EditorFontColors.fromBuiltInColors(\n              context,\n              textColor?.tryToColor(),\n            ),\n          ),\n        );\n      },\n      onTap: () {\n        service.closeKeyboard();\n        editorState.updateSelectionWithReason(\n          editorState.selection,\n          extraInfo: {\n            selectionExtraInfoDisableMobileToolbarKey: true,\n            selectionExtraInfoDisableFloatingToolbar: true,\n            selectionExtraInfoDoNotAttachTextService: true,\n          },\n        );\n        keepEditorFocusNotifier.increase();\n        showTextColorAndBackgroundColorPicker(\n          context,\n          editorState: editorState,\n          selection: editorState.selection!,\n        );\n      },\n    );\n  },\n);\n\nfinal linkToolbarItem = AppFlowyMobileToolbarItem(\n  itemBuilder: (context, editorState, service, __, onAction) {\n    return AppFlowyMobileToolbarIconItem(\n      editorState: editorState,\n      shouldListenToToggledStyle: true,\n      icon: FlowySvgs.m_toolbar_link_m,\n      onTap: () {\n        onMobileLinkButtonTap(editorState);\n      },\n    );\n  },\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/indent_outdent_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nfinal indentToolbarItem = AppFlowyMobileToolbarItem(\n  itemBuilder: (context, editorState, _, __, onAction) {\n    return AppFlowyMobileToolbarIconItem(\n      editorState: editorState,\n      shouldListenToToggledStyle: true,\n      keepSelectedStatus: true,\n      isSelected: () => false,\n      enable: () => isIndentable(editorState),\n      icon: FlowySvgs.m_aa_indent_m,\n      onTap: () async {\n        indentCommand.execute(editorState);\n      },\n    );\n  },\n);\n\nfinal outdentToolbarItem = AppFlowyMobileToolbarItem(\n  itemBuilder: (context, editorState, _, __, onAction) {\n    return AppFlowyMobileToolbarIconItem(\n      editorState: editorState,\n      shouldListenToToggledStyle: true,\n      keepSelectedStatus: true,\n      isSelected: () => false,\n      enable: () => isOutdentable(editorState),\n      icon: FlowySvgs.m_aa_outdent_m,\n      onTap: () async {\n        outdentCommand.execute(editorState);\n      },\n    );\n  },\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/keyboard_height_observer.dart",
    "content": "import 'dart:io';\n\nimport 'package:keyboard_height_plugin/keyboard_height_plugin.dart';\n\ntypedef KeyboardHeightCallback = void Function(double height);\n\n// the KeyboardHeightPlugin only accepts one listener, so we need to create a\n//  singleton class to manage the multiple listeners.\nclass KeyboardHeightObserver {\n  KeyboardHeightObserver._() {\n    _keyboardHeightPlugin.onKeyboardHeightChanged((height) {\n      notify(height);\n    });\n  }\n\n  static final KeyboardHeightObserver instance = KeyboardHeightObserver._();\n  static double currentKeyboardHeight = 0;\n\n  final List<KeyboardHeightCallback> _listeners = [];\n  final KeyboardHeightPlugin _keyboardHeightPlugin = KeyboardHeightPlugin();\n\n  void addListener(KeyboardHeightCallback listener) {\n    _listeners.add(listener);\n  }\n\n  void removeListener(KeyboardHeightCallback listener) {\n    _listeners.remove(listener);\n  }\n\n  void dispose() {\n    _listeners.clear();\n    _keyboardHeightPlugin.dispose();\n  }\n\n  void notify(double height) {\n    // the keyboard height will notify twice with the same value on Android\n    if (Platform.isAndroid && height == currentKeyboardHeight) {\n      return;\n    }\n    for (final listener in _listeners) {\n      listener(height);\n    }\n    currentKeyboardHeight = height;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/link_toolbar_item.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_extension.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nFuture<void> onMobileLinkButtonTap(EditorState editorState) async {\n  final selection = editorState.selection;\n  if (selection == null) {\n    return;\n  }\n  final nodes = editorState.getNodesInSelection(selection);\n  // show edit link bottom sheet\n  final context = nodes.firstOrNull?.context;\n  if (context != null) {\n    // keep the selection\n    unawaited(\n      editorState.updateSelectionWithReason(\n        selection,\n        extraInfo: {\n          selectionExtraInfoDisableMobileToolbarKey: true,\n          selectionExtraInfoDoNotAttachTextService: true,\n          selectionExtraInfoDisableFloatingToolbar: true,\n        },\n      ),\n    );\n    keepEditorFocusNotifier.increase();\n    await showEditLinkBottomSheet(context, selection, editorState);\n  }\n}\n\nFuture<T?> showEditLinkBottomSheet<T>(\n  BuildContext context,\n  Selection selection,\n  EditorState editorState,\n) async {\n  final currentViewId = context.read<DocumentBloc?>()?.documentId ?? '';\n  final text = editorState.getTextInSelection(selection).join();\n  final href = editorState.getDeltaAttributeValueInSelection<String>(\n    AppFlowyRichTextKeys.href,\n    selection,\n  );\n  final isPage = editorState.getDeltaAttributeValueInSelection<bool>(\n    kIsPageLink,\n    selection,\n  );\n  final linkInfo =\n      LinkInfo(name: text, link: href ?? '', isPage: isPage ?? false);\n  return showMobileBottomSheet(\n    context,\n    showDragHandle: true,\n    padding: const EdgeInsets.symmetric(horizontal: 16),\n    builder: (context) {\n      return MobileBottomSheetEditLinkWidget(\n        currentViewId: currentViewId,\n        linkInfo: linkInfo,\n        onApply: (info) => editorState.applyLink(selection, info),\n        onRemoveLink: (_) => editorState.removeLink(selection),\n        onDispose: () {\n          editorState.service.keyboardService?.closeKeyboard();\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/list_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nfinal todoListToolbarItem = AppFlowyMobileToolbarItem(\n  itemBuilder: (context, editorState, _, __, onAction) {\n    return AppFlowyMobileToolbarIconItem(\n      editorState: editorState,\n      shouldListenToToggledStyle: true,\n      keepSelectedStatus: true,\n      isSelected: () => false,\n      icon: FlowySvgs.m_toolbar_checkbox_m,\n      onTap: () async {\n        await editorState.convertBlockType(\n          TodoListBlockKeys.type,\n          extraAttributes: {\n            TodoListBlockKeys.checked: false,\n          },\n        );\n      },\n    );\n  },\n);\n\nfinal numberedListToolbarItem = AppFlowyMobileToolbarItem(\n  itemBuilder: (context, editorState, _, __, onAction) {\n    final isSelected =\n        editorState.isBlockTypeSelected(NumberedListBlockKeys.type);\n    return AppFlowyMobileToolbarIconItem(\n      editorState: editorState,\n      shouldListenToToggledStyle: true,\n      keepSelectedStatus: true,\n      isSelected: () => isSelected,\n      icon: FlowySvgs.m_toolbar_numbered_list_m,\n      onTap: () async {\n        await editorState.convertBlockType(\n          NumberedListBlockKeys.type,\n        );\n      },\n    );\n  },\n);\n\nfinal bulletedListToolbarItem = AppFlowyMobileToolbarItem(\n  itemBuilder: (context, editorState, _, __, onAction) {\n    final isSelected =\n        editorState.isBlockTypeSelected(BulletedListBlockKeys.type);\n    return AppFlowyMobileToolbarIconItem(\n      editorState: editorState,\n      shouldListenToToggledStyle: true,\n      keepSelectedStatus: true,\n      isSelected: () => isSelected,\n      icon: FlowySvgs.m_toolbar_bulleted_list_m,\n      onTap: () async {\n        await editorState.convertBlockType(\n          BulletedListBlockKeys.type,\n        );\n      },\n    );\n  },\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/more_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\n\nfinal moreToolbarItem = AppFlowyMobileToolbarItem(\n  itemBuilder: (context, editorState, _, __, onAction) {\n    return AppFlowyMobileToolbarIconItem(\n      editorState: editorState,\n      icon: FlowySvgs.m_toolbar_more_s,\n      onTap: () {},\n    );\n  },\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/toolbar_item_builder.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nfinal _listBlockTypes = [\n  BulletedListBlockKeys.type,\n  NumberedListBlockKeys.type,\n  TodoListBlockKeys.type,\n];\n\nfinal _defaultToolbarItems = [\n  addBlockToolbarItem,\n  aaToolbarItem,\n  todoListToolbarItem,\n  bulletedListToolbarItem,\n  addAttachmentItem,\n  numberedListToolbarItem,\n  boldToolbarItem,\n  italicToolbarItem,\n  underlineToolbarItem,\n  strikethroughToolbarItem,\n  colorToolbarItem,\n  undoToolbarItem,\n  redoToolbarItem,\n];\n\nfinal _listToolbarItems = [\n  addBlockToolbarItem,\n  aaToolbarItem,\n  outdentToolbarItem,\n  indentToolbarItem,\n  todoListToolbarItem,\n  bulletedListToolbarItem,\n  numberedListToolbarItem,\n  boldToolbarItem,\n  italicToolbarItem,\n  underlineToolbarItem,\n  strikethroughToolbarItem,\n  colorToolbarItem,\n  addAttachmentItem,\n  undoToolbarItem,\n  redoToolbarItem,\n];\n\nfinal _textToolbarItems = [\n  aaToolbarItem,\n  boldToolbarItem,\n  italicToolbarItem,\n  underlineToolbarItem,\n  strikethroughToolbarItem,\n  colorToolbarItem,\n];\n\n/// Calculate the toolbar items based on the current selection.\n///\n/// Default:\n///   Add, Aa, Todo List, Image, Bulleted List, Numbered List, B, I, U, S, Color, Undo, Redo\n///\n/// Selecting text:\n///   Aa, B, I, U, S, Color\n///\n/// Selecting a list:\n///   Add, Aa, Indent, Outdent, Bulleted List, Numbered List, Todo List B, I, U, S\nList<AppFlowyMobileToolbarItem> buildMobileToolbarItems(\n  EditorState editorState,\n  Selection? selection,\n) {\n  if (selection == null) {\n    return [];\n  }\n\n  if (!selection.isCollapsed) {\n    final items = List.of(_textToolbarItems);\n    if (onlyShowInSingleSelectionAndTextType(editorState)) {\n      items.add(linkToolbarItem);\n    }\n    return items;\n  }\n\n  final allSelectedAreListType = editorState\n      .getSelectedNodes(selection: selection)\n      .every((node) => _listBlockTypes.contains(node.type));\n  if (allSelectedAreListType) {\n    return _listToolbarItems;\n  }\n\n  return _defaultToolbarItems;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/undo_redo_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/widgets.dart';\n\nfinal undoToolbarItem = AppFlowyMobileToolbarItem(\n  itemBuilder: (context, editorState, _, __, onAction) {\n    final theme = ToolbarColorExtension.of(context);\n    return AppFlowyMobileToolbarIconItem(\n      editorState: editorState,\n      iconBuilder: (context) {\n        final canUndo = editorState.undoManager.undoStack.isNonEmpty;\n        return Container(\n          width: 40,\n          padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),\n          decoration: BoxDecoration(\n            borderRadius: BorderRadius.circular(9),\n          ),\n          child: FlowySvg(\n            FlowySvgs.m_toolbar_undo_m,\n            color: canUndo\n                ? theme.toolbarItemIconColor\n                : theme.toolbarItemIconDisabledColor,\n          ),\n        );\n      },\n      onTap: () => undoCommand.execute(editorState),\n    );\n  },\n);\n\nfinal redoToolbarItem = AppFlowyMobileToolbarItem(\n  itemBuilder: (context, editorState, _, __, onAction) {\n    final theme = ToolbarColorExtension.of(context);\n    return AppFlowyMobileToolbarIconItem(\n      editorState: editorState,\n      iconBuilder: (context) {\n        final canRedo = editorState.undoManager.redoStack.isNonEmpty;\n        return Container(\n          width: 40,\n          padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),\n          decoration: BoxDecoration(\n            borderRadius: BorderRadius.circular(9),\n          ),\n          child: FlowySvg(\n            FlowySvgs.m_toolbar_redo_m,\n            color: canRedo\n                ? theme.toolbarItemIconColor\n                : theme.toolbarItemIconDisabledColor,\n          ),\n        );\n      },\n      onTap: () => redoCommand.execute(editorState),\n    );\n  },\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileToolbarMenuItemWrapper extends StatelessWidget {\n  const MobileToolbarMenuItemWrapper({\n    super.key,\n    required this.size,\n    this.icon,\n    this.text,\n    this.backgroundColor,\n    this.selectedBackgroundColor,\n    this.enable,\n    this.fontFamily,\n    required this.isSelected,\n    required this.iconPadding,\n    this.enableBottomLeftRadius = true,\n    this.enableBottomRightRadius = true,\n    this.enableTopLeftRadius = true,\n    this.enableTopRightRadius = true,\n    this.showDownArrow = false,\n    this.showRightArrow = false,\n    this.textPadding = EdgeInsets.zero,\n    required this.onTap,\n    this.iconColor,\n  });\n\n  final Size size;\n  final VoidCallback onTap;\n  final FlowySvgData? icon;\n  final String? text;\n  final bool? enable;\n  final String? fontFamily;\n  final bool isSelected;\n  final EdgeInsets iconPadding;\n  final bool enableTopLeftRadius;\n  final bool enableTopRightRadius;\n  final bool enableBottomRightRadius;\n  final bool enableBottomLeftRadius;\n  final bool showDownArrow;\n  final bool showRightArrow;\n  final Color? backgroundColor;\n  final Color? selectedBackgroundColor;\n  final EdgeInsets textPadding;\n  final Color? iconColor;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = ToolbarColorExtension.of(context);\n    Color? iconColor = this.iconColor;\n    if (iconColor == null) {\n      if (enable != null) {\n        iconColor = enable! ? null : theme.toolbarMenuIconDisabledColor;\n      } else {\n        iconColor = isSelected\n            ? theme.toolbarMenuIconSelectedColor\n            : theme.toolbarMenuIconColor;\n      }\n    }\n    final textColor =\n        enable == false ? theme.toolbarMenuIconDisabledColor : null;\n    // the ui design is based on 375.0 width\n    final scale = context.scale;\n    final radius = Radius.circular(12 * scale);\n    final Widget child;\n    if (icon != null) {\n      child = FlowySvg(icon!, color: iconColor);\n    } else if (text != null) {\n      child = Padding(\n        padding: textPadding * scale,\n        child: FlowyText(\n          text!,\n          fontSize: 16.0,\n          color: textColor,\n          fontFamily: fontFamily,\n          overflow: TextOverflow.ellipsis,\n        ),\n      );\n    } else {\n      throw ArgumentError('icon and text cannot be null at the same time');\n    }\n\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: enable == false ? null : onTap,\n      child: Stack(\n        children: [\n          Container(\n            height: size.height * scale,\n            width: size.width * scale,\n            alignment: text != null ? Alignment.centerLeft : Alignment.center,\n            decoration: BoxDecoration(\n              color: isSelected\n                  ? (selectedBackgroundColor ??\n                      theme.toolbarMenuItemSelectedBackgroundColor)\n                  : backgroundColor,\n              borderRadius: BorderRadius.only(\n                topLeft: enableTopLeftRadius ? radius : Radius.zero,\n                topRight: enableTopRightRadius ? radius : Radius.zero,\n                bottomRight: enableBottomRightRadius ? radius : Radius.zero,\n                bottomLeft: enableBottomLeftRadius ? radius : Radius.zero,\n              ),\n            ),\n            padding: iconPadding * scale,\n            child: child,\n          ),\n          if (showDownArrow)\n            Positioned(\n              right: 9.0 * scale,\n              bottom: 9.0 * scale,\n              child: const FlowySvg(FlowySvgs.m_aa_down_arrow_s),\n            ),\n          if (showRightArrow)\n            Positioned.fill(\n              right: 12.0 * scale,\n              child: Align(\n                alignment: Alignment.centerRight,\n                child: FlowySvg(\n                  FlowySvgs.m_aa_arrow_right_s,\n                  color: iconColor,\n                ),\n              ),\n            ),\n        ],\n      ),\n    );\n  }\n}\n\nclass ScaledVerticalDivider extends StatelessWidget {\n  const ScaledVerticalDivider({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return HSpace(1.5 * context.scale);\n  }\n}\n\nclass ScaledVSpace extends StatelessWidget {\n  const ScaledVSpace({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return VSpace(12.0 * context.scale);\n  }\n}\n\nextension MobileToolbarBuildContext on BuildContext {\n  double get scale => MediaQuery.of(this).size.width / 375.0;\n}\n\nfinal _blocksCanContainChildren = [\n  ParagraphBlockKeys.type,\n  BulletedListBlockKeys.type,\n  NumberedListBlockKeys.type,\n  TodoListBlockKeys.type,\n];\n\nextension MobileToolbarEditorState on EditorState {\n  bool isBlockTypeSelected(String blockType, {int? level}) {\n    final selection = this.selection;\n    if (selection == null) {\n      return false;\n    }\n    final node = getNodeAtPath(selection.start.path);\n    final type = node?.type;\n    if (node == null || type == null) {\n      return false;\n    }\n    if (level != null && blockType == HeadingBlockKeys.type) {\n      return type == blockType &&\n          node.attributes[HeadingBlockKeys.level] == level;\n    }\n    return type == blockType;\n  }\n\n  bool isTextDecorationSelected(String richTextKey) {\n    final selection = this.selection;\n    if (selection == null) {\n      return false;\n    }\n\n    final nodes = getNodesInSelection(selection);\n    bool isSelected = false;\n    if (selection.isCollapsed) {\n      if (toggledStyle.containsKey(richTextKey)) {\n        isSelected = toggledStyle[richTextKey] as bool;\n      } else {\n        if (selection.startIndex != 0) {\n          // get previous index text style\n          isSelected = nodes.allSatisfyInSelection(\n            selection.copyWith(\n              start: selection.start.copyWith(\n                offset: selection.startIndex - 1,\n              ),\n            ),\n            (delta) => delta.everyAttributes(\n              (attributes) => attributes[richTextKey] == true,\n            ),\n          );\n        }\n      }\n    } else {\n      isSelected = nodes.allSatisfyInSelection(selection, (delta) {\n        return delta.everyAttributes((attr) => attr[richTextKey] == true);\n      });\n    }\n    return isSelected;\n  }\n\n  Future<void> convertBlockType(\n    String newBlockType, {\n    Selection? selection,\n    Attributes? extraAttributes,\n    bool? isSelected,\n    Map? selectionExtraInfo,\n  }) async {\n    selection = selection ?? this.selection;\n    if (selection == null) {\n      return;\n    }\n    final node = getNodeAtPath(selection.start.path);\n    final type = node?.type;\n    if (node == null || type == null) {\n      assert(false, 'node or type is null');\n      return;\n    }\n    final selected = isSelected ?? type == newBlockType;\n\n    // if the new block type can't contain children, we need to move all the children to the parent\n    bool needToDeleteChildren = false;\n    if (!selected &&\n        node.children.isNotEmpty &&\n        !_blocksCanContainChildren.contains(newBlockType)) {\n      final transaction = this.transaction;\n      needToDeleteChildren = true;\n      transaction.insertNodes(\n        selection.end.path.next,\n        node.children.map((e) => e.deepCopy()),\n      );\n      await apply(transaction);\n    }\n    await formatNode(\n      selection,\n      (node) {\n        final attributes = {\n          ParagraphBlockKeys.delta: (node.delta ?? Delta()).toJson(),\n          // for some block types, they have extra attributes, like todo list has checked attribute, callout has icon attribute, etc.\n          if (!selected && extraAttributes != null) ...extraAttributes,\n        };\n        return node.copyWith(\n          type: selected ? ParagraphBlockKeys.type : newBlockType,\n          attributes: attributes,\n          children: needToDeleteChildren ? [] : null,\n        );\n      },\n      selectionExtraInfo: selectionExtraInfo,\n    );\n  }\n\n  Future<void> alignBlock(\n    String alignment, {\n    Selection? selection,\n    Map? selectionExtraInfo,\n  }) async {\n    await updateNode(\n      selection,\n      (node) => node.copyWith(\n        attributes: {\n          ...node.attributes,\n          blockComponentAlign: alignment,\n        },\n      ),\n      selectionExtraInfo: selectionExtraInfo,\n    );\n  }\n\n  Future<void> updateTextAndHref(\n    String? prevText,\n    String? prevHref,\n    String? text,\n    String? href, {\n    Selection? selection,\n  }) async {\n    if (prevText == null && text == null) {\n      return;\n    }\n\n    selection ??= this.selection;\n    // doesn't support multiple selection now\n    if (selection == null || !selection.isSingle) {\n      return;\n    }\n\n    final node = getNodeAtPath(selection.start.path);\n    if (node == null) {\n      return;\n    }\n\n    final transaction = this.transaction;\n\n    // insert a new link\n    if (prevText == null &&\n        text != null &&\n        text.isNotEmpty &&\n        selection.isCollapsed) {\n      final attributes = href != null && href.isNotEmpty\n          ? {AppFlowyRichTextKeys.href: href}\n          : null;\n      transaction.insertText(\n        node,\n        selection.startIndex,\n        text,\n        attributes: attributes,\n      );\n    } else if (text != null && prevText != text) {\n      // update text\n      transaction.replaceText(\n        node,\n        selection.startIndex,\n        selection.length,\n        text,\n      );\n    }\n\n    // if the text is empty, it means the user wants to remove the text\n    if (text != null && text.isNotEmpty && prevHref != href) {\n      // update href\n      transaction.formatText(\n        node,\n        selection.startIndex,\n        text.length,\n        {AppFlowyRichTextKeys.href: href?.isEmpty == true ? null : href},\n      );\n    }\n\n    await apply(transaction);\n  }\n\n  Future<void> insertBlockAfterCurrentSelection(\n    Selection selection,\n    Node node,\n  ) async {\n    final path = selection.end.path.next;\n    final transaction = this.transaction;\n    transaction.insertNode(\n      path,\n      node,\n    );\n    transaction.afterSelection = Selection.collapsed(\n      Position(path: path),\n    );\n    transaction.selectionExtraInfo = {};\n    await apply(transaction);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:numerus/roman/roman.dart';\n\nclass NumberedListIcon extends StatelessWidget {\n  const NumberedListIcon({\n    super.key,\n    required this.node,\n    required this.textDirection,\n    this.textStyle,\n  });\n\n  final Node node;\n  final TextDirection textDirection;\n  final TextStyle? textStyle;\n\n  @override\n  Widget build(BuildContext context) {\n    final textStyleConfiguration =\n        context.read<EditorState>().editorStyle.textStyleConfiguration;\n    final height =\n        textStyleConfiguration.text.height ?? textStyleConfiguration.lineHeight;\n    final combinedTextStyle = textStyle?.combine(textStyleConfiguration.text) ??\n        textStyleConfiguration.text;\n    final adjustedTextStyle = combinedTextStyle.copyWith(\n      height: height,\n      fontFeatures: [const FontFeature.tabularFigures()],\n    );\n\n    return Padding(\n      padding: const EdgeInsets.only(left: 6.0, right: 10.0),\n      child: Text(\n        node.buildLevelString(context),\n        style: adjustedTextStyle,\n        strutStyle: StrutStyle.fromTextStyle(combinedTextStyle),\n        textHeightBehavior: TextHeightBehavior(\n          applyHeightToFirstAscent:\n              textStyleConfiguration.applyHeightToFirstAscent,\n          applyHeightToLastDescent:\n              textStyleConfiguration.applyHeightToLastDescent,\n          leadingDistribution: textStyleConfiguration.leadingDistribution,\n        ),\n        textDirection: textDirection,\n      ),\n    );\n  }\n}\n\nextension NumberedListNodeIndex on Node {\n  String buildLevelString(BuildContext context) {\n    final builder = NumberedListIndexBuilder(\n      editorState: context.read<EditorState>(),\n      node: this,\n    );\n    final indexInRootLevel = builder.indexInRootLevel;\n    final indexInSameLevel = builder.indexInSameLevel;\n    final level = indexInRootLevel % 3;\n    final levelString = switch (level) {\n      1 => indexInSameLevel.latin,\n      2 => indexInSameLevel.roman,\n      _ => '$indexInSameLevel',\n    };\n    return '$levelString.';\n  }\n}\n\nclass NumberedListIndexBuilder {\n  NumberedListIndexBuilder({\n    required this.editorState,\n    required this.node,\n  });\n\n  final EditorState editorState;\n  final Node node;\n\n  // the level of the current node\n  int get indexInRootLevel {\n    var level = 0;\n    var parent = node.parent;\n    while (parent != null) {\n      if (parent.type == NumberedListBlockKeys.type) {\n        level++;\n      }\n      parent = parent.parent;\n    }\n    return level;\n  }\n\n  // the index of the current level\n  int get indexInSameLevel {\n    int level = 1;\n    Node? previous = node.previous;\n\n    // if the previous one is not a numbered list, then it is the first one\n    final aiNodeExternalValues =\n        node.externalValues?.unwrapOrNull<AINodeExternalValues>();\n\n    if (previous == null ||\n        previous.type != NumberedListBlockKeys.type ||\n        (aiNodeExternalValues != null &&\n            aiNodeExternalValues.isFirstNumberedListNode)) {\n      return node.attributes[NumberedListBlockKeys.number] ?? level;\n    }\n\n    int? startNumber;\n    while (previous != null && previous.type == NumberedListBlockKeys.type) {\n      startNumber = previous.attributes[NumberedListBlockKeys.number] as int?;\n      level++;\n      previous = previous.previous;\n\n      // break the loop if the start number is found when the current node is an AI node\n      if (aiNodeExternalValues != null && startNumber != null) {\n        return startNumber + level - 1;\n      }\n    }\n\n    if (startNumber != null) {\n      level = startNumber + level - 1;\n    }\n\n    return level;\n  }\n}\n\nextension on int {\n  String get latin {\n    String result = '';\n    int number = this;\n    while (number > 0) {\n      final int remainder = (number - 1) % 26;\n      result = String.fromCharCode(remainder + 65) + result;\n      number = (number - 1) ~/ 26;\n    }\n    return result.toLowerCase();\n  }\n\n  String get roman {\n    return toRomanNumeralString() ?? '$this';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart' hide TextDirection;\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass OutlineBlockKeys {\n  const OutlineBlockKeys._();\n\n  static const String type = 'outline';\n  static const String backgroundColor = blockComponentBackgroundColor;\n  static const String depth = 'depth';\n}\n\nNode outlineBlockNode() {\n  return Node(\n    type: OutlineBlockKeys.type,\n  );\n}\n\nenum _OutlineBlockStatus {\n  noHeadings,\n  noMatchHeadings,\n  success;\n}\n\nfinal _availableBlockTypes = [\n  HeadingBlockKeys.type,\n  ToggleListBlockKeys.type,\n];\n\nclass OutlineBlockComponentBuilder extends BlockComponentBuilder {\n  OutlineBlockComponentBuilder({\n    super.configuration,\n  });\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    return OutlineBlockWidget(\n      key: node.key,\n      node: node,\n      configuration: configuration,\n      showActions: showActions(node),\n      actionBuilder: (context, state) => actionBuilder(\n        blockComponentContext,\n        state,\n      ),\n      actionTrailingBuilder: (context, state) => actionTrailingBuilder(\n        blockComponentContext,\n        state,\n      ),\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate => (node) => node.children.isEmpty;\n}\n\nclass OutlineBlockWidget extends BlockComponentStatefulWidget {\n  const OutlineBlockWidget({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n  });\n\n  @override\n  State<OutlineBlockWidget> createState() => _OutlineBlockWidgetState();\n}\n\nclass _OutlineBlockWidgetState extends State<OutlineBlockWidget>\n    with\n        BlockComponentConfigurable,\n        BlockComponentTextDirectionMixin,\n        BlockComponentBackgroundColorMixin,\n        DefaultSelectableMixin,\n        SelectableMixin {\n  // Change the value if the heading block type supports heading levels greater than '3'\n  static const maxVisibleDepth = 6;\n\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  @override\n  Node get node => widget.node;\n\n  @override\n  late EditorState editorState = context.read<EditorState>();\n  late Stream<EditorTransactionValue> stream = editorState.transactionStream;\n\n  @override\n  GlobalKey<State<StatefulWidget>> blockComponentKey = GlobalKey(\n    debugLabel: OutlineBlockKeys.type,\n  );\n\n  @override\n  GlobalKey<State<StatefulWidget>> get containerKey => widget.node.key;\n\n  @override\n  GlobalKey<State<StatefulWidget>> get forwardKey => widget.node.key;\n\n  @override\n  Widget build(BuildContext context) {\n    return StreamBuilder(\n      stream: stream,\n      builder: (context, snapshot) {\n        Widget child = _buildOutlineBlock();\n\n        child = BlockSelectionContainer(\n          node: node,\n          delegate: this,\n          listenable: editorState.selectionNotifier,\n          remoteSelection: editorState.remoteSelections,\n          blockColor: editorState.editorStyle.selectionColor,\n          selectionAboveBlock: true,\n          supportTypes: const [\n            BlockSelectionType.block,\n          ],\n          child: child,\n        );\n\n        if (UniversalPlatform.isDesktopOrWeb) {\n          if (widget.showActions && widget.actionBuilder != null) {\n            child = BlockComponentActionWrapper(\n              node: widget.node,\n              actionBuilder: widget.actionBuilder!,\n              actionTrailingBuilder: widget.actionTrailingBuilder,\n              child: child,\n            );\n          }\n        } else {\n          child = Padding(\n            padding: padding,\n            child: MobileBlockActionButtons(\n              node: node,\n              editorState: editorState,\n              child: child,\n            ),\n          );\n        }\n\n        return child;\n      },\n    );\n  }\n\n  Widget _buildOutlineBlock() {\n    final textDirection = calculateTextDirection(\n      layoutDirection: Directionality.maybeOf(context),\n    );\n    final (status, headings) = getHeadingNodes();\n\n    Widget child;\n\n    switch (status) {\n      case _OutlineBlockStatus.noHeadings:\n        child = Align(\n          alignment: Alignment.centerLeft,\n          child: Text(\n            LocaleKeys.document_plugins_outline_addHeadingToCreateOutline.tr(),\n            style: configuration.placeholderTextStyle(node),\n          ),\n        );\n      case _OutlineBlockStatus.noMatchHeadings:\n        child = Align(\n          alignment: Alignment.centerLeft,\n          child: Text(\n            LocaleKeys.document_plugins_outline_noMatchHeadings.tr(),\n            style: configuration.placeholderTextStyle(node),\n          ),\n        );\n      case _OutlineBlockStatus.success:\n        final children = headings\n            .map(\n              (e) => Container(\n                padding: const EdgeInsets.only(\n                  bottom: 4.0,\n                ),\n                width: double.infinity,\n                child: OutlineItemWidget(\n                  node: e,\n                  textDirection: textDirection,\n                ),\n              ),\n            )\n            .toList();\n        child = Padding(\n          padding: const EdgeInsets.only(left: 15.0),\n          child: Column(\n            children: children,\n          ),\n        );\n    }\n\n    return Container(\n      key: blockComponentKey,\n      constraints: const BoxConstraints(\n        minHeight: 40.0,\n      ),\n      padding: UniversalPlatform.isMobile ? EdgeInsets.zero : padding,\n      child: Container(\n        padding: const EdgeInsets.symmetric(\n          vertical: 2.0,\n          horizontal: 5.0,\n        ),\n        decoration: BoxDecoration(\n          borderRadius: const BorderRadius.all(Radius.circular(8.0)),\n          color: backgroundColor,\n        ),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          mainAxisSize: MainAxisSize.min,\n          textDirection: textDirection,\n          children: [\n            Text(\n              LocaleKeys.document_outlineBlock_placeholder.tr(),\n              style: Theme.of(context).textTheme.titleLarge,\n            ),\n            const VSpace(8.0),\n            child,\n          ],\n        ),\n      ),\n    );\n  }\n\n  (_OutlineBlockStatus, Iterable<Node>) getHeadingNodes() {\n    final nodes = NodeIterator(\n      document: editorState.document,\n      startNode: editorState.document.root,\n    ).toList();\n    final level = node.attributes[OutlineBlockKeys.depth] ?? maxVisibleDepth;\n    var headings = nodes.where(\n      (e) => _isHeadingNode(e),\n    );\n    if (headings.isEmpty) {\n      return (_OutlineBlockStatus.noHeadings, []);\n    }\n    headings = headings.where(\n      (e) =>\n          (e.type == HeadingBlockKeys.type &&\n              e.attributes[HeadingBlockKeys.level] <= level) ||\n          (e.type == ToggleListBlockKeys.type &&\n              e.attributes[ToggleListBlockKeys.level] <= level),\n    );\n    if (headings.isEmpty) {\n      return (_OutlineBlockStatus.noMatchHeadings, []);\n    }\n    return (_OutlineBlockStatus.success, headings);\n  }\n\n  bool _isHeadingNode(Node node) {\n    if (node.type == HeadingBlockKeys.type && node.delta?.isNotEmpty == true) {\n      return true;\n    }\n\n    if (node.type == ToggleListBlockKeys.type &&\n        node.delta?.isNotEmpty == true &&\n        node.attributes[ToggleListBlockKeys.level] != null) {\n      return true;\n    }\n\n    return false;\n  }\n}\n\nclass OutlineItemWidget extends StatelessWidget {\n  OutlineItemWidget({\n    super.key,\n    required this.node,\n    required this.textDirection,\n  }) {\n    assert(_availableBlockTypes.contains(node.type));\n  }\n\n  final Node node;\n  final TextDirection textDirection;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      onTap: () => scrollToBlock(context),\n      text: Row(\n        textDirection: textDirection,\n        children: [\n          HSpace(node.leftIndent),\n          Flexible(\n            child: buildOutlineItemWidget(context),\n          ),\n        ],\n      ),\n    );\n  }\n\n  void scrollToBlock(BuildContext context) {\n    final editorState = context.read<EditorState>();\n    final editorScrollController = context.read<EditorScrollController>();\n    editorScrollController.itemScrollController.jumpTo(\n      index: node.path.first,\n      alignment: 0.5,\n    );\n    editorState.selection = Selection.collapsed(\n      Position(path: node.path, offset: node.delta?.length ?? 0),\n    );\n  }\n\n  Widget buildOutlineItemWidget(BuildContext context) {\n    final editorState = context.read<EditorState>();\n    final textStyle = editorState.editorStyle.textStyleConfiguration;\n    final style = textStyle.href.combine(textStyle.text);\n\n    final textInserted = node.delta?.whereType<TextInsert>();\n    if (textInserted == null) {\n      return const SizedBox.shrink();\n    }\n\n    final children = <InlineSpan>[];\n\n    var i = 0;\n    for (final e in textInserted) {\n      final mentionAttribute = e.attributes?[MentionBlockKeys.mention];\n      final mention =\n          mentionAttribute is Map<String, dynamic> ? mentionAttribute : null;\n      final text = e.text;\n      if (mention != null) {\n        final type = mention[MentionBlockKeys.type];\n        children.add(\n          WidgetSpan(\n            alignment: PlaceholderAlignment.middle,\n            child: MentionBlock(\n              key: ValueKey(type),\n              node: node,\n              index: i,\n              mention: mention,\n              textStyle: style,\n            ),\n          ),\n        );\n    } else {\n        children.add(\n          TextSpan(\n            text: text,\n            style: style,\n          ),\n        );\n      }\n\n      i += text.length;\n    }\n\n    return IgnorePointer(\n      child: Text.rich(\n        TextSpan(\n          children: children,\n          style: style,\n        ),\n      ),\n    );\n  }\n}\n\nextension on Node {\n  double get leftIndent {\n    assert(_availableBlockTypes.contains(type));\n\n    if (!_availableBlockTypes.contains(type)) {\n      return 0.0;\n    }\n\n    final level = attributes[HeadingBlockKeys.level] ??\n        attributes[ToggleListBlockKeys.level];\n    if (level != null) {\n      final indent = (level - 1) * 15.0;\n      return indent;\n    }\n\n    return 0.0;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_block/custom_page_block_component.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n// ignore: implementation_imports\nimport 'package:appflowy_editor/src/editor/block_component/base_component/widget/ignore_parent_gesture.dart';\n// ignore: implementation_imports\nimport 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass CustomPageBlockComponentBuilder extends BlockComponentBuilder {\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    return CustomPageBlockComponent(\n      key: blockComponentContext.node.key,\n      node: blockComponentContext.node,\n      header: blockComponentContext.header,\n      footer: blockComponentContext.footer,\n      wrapper: blockComponentContext.wrapper,\n    );\n  }\n}\n\nclass CustomPageBlockComponent extends BlockComponentStatelessWidget {\n  const CustomPageBlockComponent({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n    this.header,\n    this.footer,\n    this.wrapper,\n  });\n\n  final Widget? header;\n  final Widget? footer;\n  final BlockComponentWrapper? wrapper;\n\n  @override\n  Widget build(BuildContext context) {\n    final editorState = context.read<EditorState>();\n    final scrollController = context.read<EditorScrollController?>();\n    final items = node.children;\n\n    if (scrollController == null || scrollController.shrinkWrap) {\n      return SingleChildScrollView(\n        child: Builder(\n          builder: (context) {\n            final scroller = Scrollable.maybeOf(context);\n            if (scroller != null) {\n              editorState.updateAutoScroller(scroller);\n            }\n            return Column(\n              children: [\n                if (header != null) header!,\n                ...items.map(\n                  (e) {\n                    Widget child = editorState.renderer.build(context, e);\n                    if (wrapper != null) {\n                      child = wrapper!(context, node: e, child: child);\n                    }\n                    return Container(\n                      constraints: BoxConstraints(\n                        maxWidth:\n                            editorState.editorStyle.maxWidth ?? double.infinity,\n                      ),\n                      padding: editorState.editorStyle.padding,\n                      child: child,\n                    );\n                  },\n                ),\n                if (footer != null) footer!,\n              ],\n            );\n          },\n        ),\n      );\n    } else {\n      int extentCount = 0;\n      if (header != null) extentCount++;\n      if (footer != null) extentCount++;\n\n      return ScrollablePositionedList.builder(\n        shrinkWrap: scrollController.shrinkWrap,\n        itemCount: items.length + extentCount,\n        itemBuilder: (context, index) {\n          editorState.updateAutoScroller(Scrollable.of(context));\n          if (header != null && index == 0) {\n            return IgnoreEditorSelectionGesture(\n              child: header!,\n            );\n          }\n\n          if (footer != null && index == (items.length - 1) + extentCount) {\n            return IgnoreEditorSelectionGesture(\n              child: footer!,\n            );\n          }\n\n          final childNode = items[index - (header != null ? 1 : 0)];\n          final isOverflowType = overflowTypes.contains(childNode.type);\n\n          Widget child = editorState.renderer.build(context, childNode);\n          if (wrapper != null) {\n            child = wrapper!(context, node: childNode, child: child);\n          }\n\n          final item = Container(\n            constraints: BoxConstraints(\n              maxWidth: editorState.editorStyle.maxWidth ?? double.infinity,\n            ),\n            padding: isOverflowType\n                ? EdgeInsets.zero\n                : editorState.editorStyle.padding,\n            child: child,\n          );\n\n          return isOverflowType ? item : Center(child: item);\n        },\n        itemScrollController: scrollController.itemScrollController,\n        scrollOffsetController: scrollController.scrollOffsetController,\n        itemPositionsListener: scrollController.itemPositionsListener,\n        scrollOffsetListener: scrollController.scrollOffsetListener,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_cover_bottom_sheet.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart';\nimport 'package:appflowy/shared/feedback_gesture_detector.dart';\nimport 'package:appflowy/shared/flowy_gradient_colors.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass PageCoverBottomSheet extends StatelessWidget {\n  const PageCoverBottomSheet({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(\n      builder: (context, state) {\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 16.0),\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              const VSpace(8.0),\n\n              // pure colors\n              FlowyText(\n                LocaleKeys.pageStyle_colors.tr(),\n                color: context.pageStyleTextColor,\n                fontSize: 14.0,\n              ),\n              const VSpace(8.0),\n              _buildPureColors(context, state),\n              const VSpace(20.0),\n\n              // gradient colors\n              FlowyText(\n                LocaleKeys.pageStyle_gradient.tr(),\n                color: context.pageStyleTextColor,\n                fontSize: 14.0,\n              ),\n              const VSpace(8.0),\n              _buildGradientColors(context, state),\n              const VSpace(20.0),\n\n              // built-in images\n              FlowyText(\n                LocaleKeys.pageStyle_backgroundImage.tr(),\n                color: context.pageStyleTextColor,\n                fontSize: 14.0,\n              ),\n              const VSpace(8.0),\n              _buildBuiltImages(context, state),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildPureColors(\n    BuildContext context,\n    DocumentPageStyleState state,\n  ) {\n    return SizedBox(\n      height: 42.0,\n      child: ListView.separated(\n        scrollDirection: Axis.horizontal,\n        itemCount: FlowyTint.values.length,\n        separatorBuilder: (context, index) => const HSpace(12.0),\n        itemBuilder: (context, index) => _buildColorButton(\n          context,\n          state,\n          FlowyTint.values[index],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildGradientColors(\n    BuildContext context,\n    DocumentPageStyleState state,\n  ) {\n    return SizedBox(\n      height: 42.0,\n      child: ListView.separated(\n        scrollDirection: Axis.horizontal,\n        itemCount: FlowyGradientColor.values.length,\n        separatorBuilder: (context, index) => const HSpace(12.0),\n        itemBuilder: (context, index) => _buildGradientButton(\n          context,\n          state,\n          FlowyGradientColor.values[index],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildColorButton(\n    BuildContext context,\n    DocumentPageStyleState state,\n    FlowyTint tint,\n  ) {\n    final isSelected =\n        state.coverImage.isPureColor && state.coverImage.value == tint.id;\n\n    final child = !isSelected\n        ? Container(\n            width: 42,\n            height: 42,\n            decoration: ShapeDecoration(\n              color: tint.color(context),\n              shape: RoundedRectangleBorder(\n                borderRadius: BorderRadius.circular(21),\n              ),\n            ),\n          )\n        : Container(\n            width: 42,\n            height: 42,\n            decoration: ShapeDecoration(\n              color: Colors.transparent,\n              shape: RoundedRectangleBorder(\n                side: BorderSide(\n                  width: 1.50,\n                  color: Theme.of(context).colorScheme.primary,\n                ),\n                borderRadius: BorderRadius.circular(21),\n              ),\n            ),\n            alignment: Alignment.center,\n            child: Container(\n              width: 34,\n              height: 34,\n              decoration: ShapeDecoration(\n                color: tint.color(context),\n                shape: RoundedRectangleBorder(\n                  borderRadius: BorderRadius.circular(17),\n                ),\n              ),\n            ),\n          );\n\n    return FeedbackGestureDetector(\n      onTap: () {\n        context.read<DocumentPageStyleBloc>().add(\n              DocumentPageStyleEvent.updateCoverImage(\n                PageStyleCover(\n                  type: PageStyleCoverImageType.pureColor,\n                  value: tint.id,\n                ),\n              ),\n            );\n      },\n      child: child,\n    );\n  }\n\n  Widget _buildGradientButton(\n    BuildContext context,\n    DocumentPageStyleState state,\n    FlowyGradientColor gradientColor,\n  ) {\n    final isSelected = state.coverImage.isGradient &&\n        state.coverImage.value == gradientColor.id;\n\n    final child = !isSelected\n        ? Container(\n            width: 42,\n            height: 42,\n            decoration: ShapeDecoration(\n              gradient: gradientColor.linear,\n              shape: RoundedRectangleBorder(\n                borderRadius: BorderRadius.circular(21),\n              ),\n            ),\n          )\n        : Container(\n            width: 42,\n            height: 42,\n            decoration: ShapeDecoration(\n              color: Colors.transparent,\n              shape: RoundedRectangleBorder(\n                side: BorderSide(\n                  width: 1.50,\n                  color: Theme.of(context).colorScheme.primary,\n                ),\n                borderRadius: BorderRadius.circular(21),\n              ),\n            ),\n            alignment: Alignment.center,\n            child: Container(\n              width: 34,\n              height: 34,\n              decoration: ShapeDecoration(\n                gradient: gradientColor.linear,\n                shape: RoundedRectangleBorder(\n                  borderRadius: BorderRadius.circular(17),\n                ),\n              ),\n            ),\n          );\n\n    return FeedbackGestureDetector(\n      onTap: () {\n        context.read<DocumentPageStyleBloc>().add(\n              DocumentPageStyleEvent.updateCoverImage(\n                PageStyleCover(\n                  type: PageStyleCoverImageType.gradientColor,\n                  value: gradientColor.id,\n                ),\n              ),\n            );\n      },\n      child: child,\n    );\n  }\n\n  Widget _buildBuiltImages(\n    BuildContext context,\n    DocumentPageStyleState state,\n  ) {\n    final imageNames = ['1', '2', '3', '4', '5', '6'];\n    return GridView.builder(\n      shrinkWrap: true,\n      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(\n        crossAxisCount: 3,\n        crossAxisSpacing: 12,\n        mainAxisSpacing: 12,\n        childAspectRatio: 16.0 / 9.0,\n      ),\n      itemCount: imageNames.length,\n      itemBuilder: (context, index) => _buildBuiltInImage(\n        context,\n        state,\n        imageNames[index],\n      ),\n    );\n  }\n\n  Widget _buildBuiltInImage(\n    BuildContext context,\n    DocumentPageStyleState state,\n    String imageName,\n  ) {\n    final asset = PageStyleCoverImageType.builtInImagePath(imageName);\n    final isSelected =\n        state.coverImage.isBuiltInImage && state.coverImage.value == imageName;\n    final image = ClipRRect(\n      borderRadius: BorderRadius.circular(4),\n      child: Image.asset(\n        asset,\n        fit: BoxFit.cover,\n      ),\n    );\n    final child = !isSelected\n        ? image\n        : Container(\n            clipBehavior: Clip.antiAlias,\n            decoration: ShapeDecoration(\n              shape: RoundedRectangleBorder(\n                side: const BorderSide(width: 1.50, color: Color(0xFF00BCF0)),\n                borderRadius: BorderRadius.circular(6),\n              ),\n            ),\n            padding: const EdgeInsets.all(2.0),\n            child: image,\n          );\n\n    return FeedbackGestureDetector(\n      onTap: () {\n        context.read<DocumentPageStyleBloc>().add(\n              DocumentPageStyleEvent.updateCoverImage(\n                PageStyleCover(\n                  type: PageStyleCoverImageType.builtInImage,\n                  value: imageName,\n                ),\n              ),\n            );\n      },\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_cover_bottom_sheet.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy/shared/feedback_gesture_detector.dart';\nimport 'package:appflowy/shared/flowy_gradient_colors.dart';\nimport 'package:appflowy/shared/permission/permission_checker.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/snap_bar.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:image_picker/image_picker.dart';\n\nclass PageStyleCoverImage extends StatelessWidget {\n  PageStyleCoverImage({\n    super.key,\n    required this.documentId,\n  });\n\n  final String documentId;\n  late final ImagePicker _imagePicker = ImagePicker();\n\n  @override\n  Widget build(BuildContext context) {\n    final backgroundColor = context.pageStyleBackgroundColor;\n    return BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(\n      builder: (context, state) {\n        return Column(\n          children: [\n            _buildOptionGroup(context, backgroundColor, state),\n            const VSpace(16.0),\n            _buildPreview(context, state),\n          ],\n        );\n      },\n    );\n  }\n\n  Widget _buildOptionGroup(\n    BuildContext context,\n    Color backgroundColor,\n    DocumentPageStyleState state,\n  ) {\n    return Container(\n      decoration: BoxDecoration(\n        color: backgroundColor,\n        borderRadius: const BorderRadius.horizontal(\n          left: Radius.circular(12),\n          right: Radius.circular(12),\n        ),\n      ),\n      padding: const EdgeInsets.all(4.0),\n      child: Row(\n        children: [\n          _CoverOptionButton(\n            showLeftCorner: true,\n            showRightCorner: false,\n            selected: state.coverImage.isPresets,\n            onTap: () => _showPresets(context),\n            child: const _PresetCover(),\n          ),\n          _CoverOptionButton(\n            showLeftCorner: false,\n            showRightCorner: false,\n            selected: state.coverImage.isPhoto,\n            onTap: () => _pickImage(context),\n            child: const _PhotoCover(),\n          ),\n          _CoverOptionButton(\n            showLeftCorner: false,\n            showRightCorner: true,\n            selected: state.coverImage.isUnsplashImage,\n            onTap: () => _showUnsplash(context),\n            child: const _UnsplashCover(),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildPreview(\n    BuildContext context,\n    DocumentPageStyleState state,\n  ) {\n    final cover = state.coverImage;\n    if (cover.isNone) {\n      return const SizedBox.shrink();\n    }\n\n    final value = cover.value;\n    final type = cover.type;\n\n    Widget preview = const SizedBox.shrink();\n\n    if (type == PageStyleCoverImageType.customImage ||\n        type == PageStyleCoverImageType.unsplashImage) {\n      final userProfilePB =\n          context.read<MobileViewPageBloc>().state.userProfilePB;\n      preview = FlowyNetworkImage(\n        url: value,\n        userProfilePB: userProfilePB,\n      );\n    }\n\n    if (type == PageStyleCoverImageType.builtInImage) {\n      preview = Image.asset(\n        PageStyleCoverImageType.builtInImagePath(value),\n        fit: BoxFit.cover,\n      );\n    }\n\n    if (type == PageStyleCoverImageType.pureColor) {\n      final color = value.coverColor(context);\n      if (color != null) {\n        preview = ColoredBox(\n          color: color,\n        );\n      }\n    }\n\n    if (type == PageStyleCoverImageType.gradientColor) {\n      preview = Container(\n        decoration: BoxDecoration(\n          gradient: FlowyGradientColor.fromId(value).linear,\n        ),\n      );\n    }\n\n    if (type == PageStyleCoverImageType.localImage) {\n      preview = Image.file(\n        File(value),\n        fit: BoxFit.cover,\n      );\n    }\n\n    return Row(\n      children: [\n        FlowyText(LocaleKeys.pageStyle_image.tr()),\n        const Spacer(),\n        Container(\n          width: 40,\n          height: 28,\n          clipBehavior: Clip.hardEdge,\n          decoration: BoxDecoration(\n            borderRadius: const BorderRadius.all(Radius.circular(6.0)),\n            border: Border.all(color: const Color(0x1F222533)),\n          ),\n          child: ClipRRect(\n            borderRadius: const BorderRadius.all(Radius.circular(5.0)),\n            child: preview,\n          ),\n        ),\n      ],\n    );\n  }\n\n  void _showPresets(BuildContext context) {\n    final pageStyleBloc = context.read<DocumentPageStyleBloc>();\n\n    context.pop();\n\n    showMobileBottomSheet(\n      context,\n      showDragHandle: true,\n      showDivider: false,\n      showDoneButton: true,\n      showHeader: true,\n      showRemoveButton: true,\n      onRemove: () {\n        pageStyleBloc.add(\n          DocumentPageStyleEvent.updateCoverImage(\n            PageStyleCover.none(),\n          ),\n        );\n      },\n      title: LocaleKeys.pageStyle_presets.tr(),\n      backgroundColor: AFThemeExtension.of(context).background,\n      builder: (_) {\n        return BlocProvider.value(\n          value: pageStyleBloc,\n          child: const PageCoverBottomSheet(),\n        );\n      },\n    );\n  }\n\n  Future<void> _pickImage(BuildContext context) async {\n    final photoPermission =\n        await PermissionChecker.checkPhotoPermission(context);\n    if (!photoPermission) {\n      Log.error('Has no permission to access the photo library');\n      return;\n    }\n\n    XFile? result;\n    try {\n      result = await _imagePicker.pickImage(source: ImageSource.gallery);\n    } catch (e) {\n      Log.error('Error while picking image: $e');\n      return;\n    }\n\n    final path = result?.path;\n    if (path != null && context.mounted) {\n      final String? result;\n      final userProfile = await UserBackendService.getCurrentUserProfile().fold(\n        (s) => s,\n        (f) => null,\n      );\n      final isAppFlowyCloud =\n          userProfile?.workspaceType == WorkspaceTypePB.ServerW;\n      final PageStyleCoverImageType type;\n      if (!isAppFlowyCloud) {\n        result = await saveImageToLocalStorage(path);\n        type = PageStyleCoverImageType.localImage;\n      } else {\n        // else we should save the image to cloud storage\n        (result, _) = await saveImageToCloudStorage(path, documentId);\n        type = PageStyleCoverImageType.customImage;\n      }\n      if (!context.mounted) {\n        return;\n      }\n      if (result == null) {\n        return showSnapBar(\n          context,\n          LocaleKeys.document_plugins_image_imageUploadFailed.tr(),\n        );\n      }\n\n      context.read<DocumentPageStyleBloc>().add(\n            DocumentPageStyleEvent.updateCoverImage(\n              PageStyleCover(type: type, value: result),\n            ),\n          );\n    }\n  }\n\n  void _showUnsplash(BuildContext context) {\n    final pageStyleBloc = context.read<DocumentPageStyleBloc>();\n    final backgroundColor = AFThemeExtension.of(context).background;\n    final maxHeight = MediaQuery.of(context).size.height * 0.6;\n\n    context.pop();\n\n    showMobileBottomSheet(\n      context,\n      showDragHandle: true,\n      showDivider: false,\n      showDoneButton: true,\n      showHeader: true,\n      showRemoveButton: true,\n      title: LocaleKeys.pageStyle_unsplash.tr(),\n      backgroundColor: backgroundColor,\n      onRemove: () {\n        pageStyleBloc.add(\n          DocumentPageStyleEvent.updateCoverImage(\n            PageStyleCover.none(),\n          ),\n        );\n      },\n      builder: (_) {\n        return ConstrainedBox(\n          constraints: BoxConstraints(maxHeight: maxHeight, minHeight: 80),\n          child: BlocProvider.value(\n            value: pageStyleBloc,\n            child: Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 16.0),\n              child: UnsplashImageWidget(\n                type: UnsplashImageType.fullScreen,\n                onSelectUnsplashImage: (url) {\n                  pageStyleBloc.add(\n                    DocumentPageStyleEvent.updateCoverImage(\n                      PageStyleCover(\n                        type: PageStyleCoverImageType.unsplashImage,\n                        value: url,\n                      ),\n                    ),\n                  );\n                },\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass _UnsplashCover extends StatelessWidget {\n  const _UnsplashCover();\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        const FlowySvg(FlowySvgs.m_page_style_unsplash_m),\n        const VSpace(4.0),\n        FlowyText(\n          LocaleKeys.pageStyle_unsplash.tr(),\n          fontSize: 12.0,\n        ),\n      ],\n    );\n  }\n}\n\nclass _PhotoCover extends StatelessWidget {\n  const _PhotoCover();\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        const FlowySvg(FlowySvgs.m_page_style_photo_m),\n        const VSpace(4.0),\n        FlowyText(\n          LocaleKeys.pageStyle_photo.tr(),\n          fontSize: 12.0,\n        ),\n      ],\n    );\n  }\n}\n\nclass _PresetCover extends StatelessWidget {\n  const _PresetCover();\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        const FlowySvg(\n          FlowySvgs.m_page_style_presets_m,\n          blendMode: null,\n        ),\n        const VSpace(4.0),\n        FlowyText(\n          LocaleKeys.pageStyle_presets.tr(),\n          fontSize: 12.0,\n        ),\n      ],\n    );\n  }\n}\n\nclass _CoverOptionButton extends StatelessWidget {\n  const _CoverOptionButton({\n    required this.showLeftCorner,\n    required this.showRightCorner,\n    required this.child,\n    required this.onTap,\n    required this.selected,\n  });\n\n  final Widget child;\n  final bool showLeftCorner;\n  final bool showRightCorner;\n  final bool selected;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      child: FeedbackGestureDetector(\n        feedbackType: HapticFeedbackType.medium,\n        onTap: onTap,\n        child: AnimatedContainer(\n          height: 64,\n          duration: Durations.medium1,\n          decoration: selected\n              ? ShapeDecoration(\n                  color: const Color(0x141AC3F2),\n                  shape: RoundedRectangleBorder(\n                    side: const BorderSide(\n                      width: 1.50,\n                      color: Color(0xFF1AC3F2),\n                    ),\n                    borderRadius: BorderRadius.circular(12),\n                  ),\n                )\n              : null,\n          alignment: Alignment.center,\n          child: child,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\n\nclass PageStyleIcon extends StatefulWidget {\n  const PageStyleIcon({\n    super.key,\n    required this.view,\n    required this.tabs,\n  });\n\n  final ViewPB view;\n  final List<PickerTabType> tabs;\n\n  @override\n  State<PageStyleIcon> createState() => _PageStyleIconState();\n}\n\nclass _PageStyleIconState extends State<PageStyleIcon> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) => PageStyleIconBloc(view: widget.view)\n        ..add(const PageStyleIconEvent.initial()),\n      child: BlocBuilder<PageStyleIconBloc, PageStyleIconState>(\n        builder: (context, state) {\n          final icon = state.icon;\n          return GestureDetector(\n            onTap: () => icon == null ? null : _showIconSelector(context, icon),\n            behavior: HitTestBehavior.opaque,\n            child: Container(\n              height: 52,\n              decoration: BoxDecoration(\n                color: context.pageStyleBackgroundColor,\n                borderRadius: BorderRadius.circular(12.0),\n              ),\n              child: Row(\n                children: [\n                  const HSpace(16.0),\n                  FlowyText(LocaleKeys.document_plugins_emoji.tr()),\n                  const Spacer(),\n                  (icon?.isEmpty ?? true)\n                      ? FlowyText(\n                          LocaleKeys.pageStyle_none.tr(),\n                          fontSize: 16.0,\n                        )\n                      : RawEmojiIconWidget(\n                          emoji: icon!,\n                          emojiSize: 16.0,\n                        ),\n                  const HSpace(6.0),\n                  const FlowySvg(FlowySvgs.m_page_style_arrow_right_s),\n                  const HSpace(12.0),\n                ],\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  void _showIconSelector(BuildContext context, EmojiIconData icon) {\n    Navigator.pop(context);\n    final pageStyleIconBloc = PageStyleIconBloc(view: widget.view)\n      ..add(const PageStyleIconEvent.initial());\n    showMobileBottomSheet(\n      context,\n      showDragHandle: true,\n      showDivider: false,\n      showHeader: true,\n      title: LocaleKeys.titleBar_pageIcon.tr(),\n      backgroundColor: AFThemeExtension.of(context).background,\n      enableDraggableScrollable: true,\n      minChildSize: 0.6,\n      initialChildSize: 0.61,\n      scrollableWidgetBuilder: (ctx, controller) {\n        return BlocProvider.value(\n          value: pageStyleIconBloc,\n          child: Expanded(\n            child: FlowyIconEmojiPicker(\n              initialType: icon.type.toPickerTabType(),\n              documentId: widget.view.id,\n              tabs: widget.tabs,\n              onSelectedEmoji: (r) {\n                pageStyleIconBloc.add(\n                  PageStyleIconEvent.updateIcon(r.data, true),\n                );\n                if (!r.keepOpen) Navigator.pop(ctx);\n              },\n            ),\n          ),\n        );\n      },\n      builder: (_) => const SizedBox.shrink(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart",
    "content": "import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/workspace/application/view/view_listener.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart '_page_style_icon_bloc.freezed.dart';\n\nclass PageStyleIconBloc extends Bloc<PageStyleIconEvent, PageStyleIconState> {\n  PageStyleIconBloc({\n    required this.view,\n  })  : _viewListener = ViewListener(viewId: view.id),\n        super(PageStyleIconState.initial()) {\n    on<PageStyleIconEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            add(\n              PageStyleIconEvent.updateIcon(\n                view.icon.toEmojiIconData(),\n                false,\n              ),\n            );\n            _viewListener?.start(\n              onViewUpdated: (view) {\n                add(\n                  PageStyleIconEvent.updateIcon(\n                    view.icon.toEmojiIconData(),\n                    false,\n                  ),\n                );\n              },\n            );\n          },\n          updateIcon: (icon, shouldUpdateRemote) async {\n            emit(state.copyWith(icon: icon));\n            if (shouldUpdateRemote && icon != null) {\n              await ViewBackendService.updateViewIcon(\n                view: view,\n                viewIcon: icon,\n              );\n            }\n          },\n        );\n      },\n    );\n  }\n\n  final ViewPB view;\n  final ViewListener? _viewListener;\n\n  @override\n  Future<void> close() {\n    _viewListener?.stop();\n    return super.close();\n  }\n}\n\n@freezed\nclass PageStyleIconEvent with _$PageStyleIconEvent {\n  const factory PageStyleIconEvent.initial() = Initial;\n\n  const factory PageStyleIconEvent.updateIcon(\n    EmojiIconData? icon,\n    bool shouldUpdateRemote,\n  ) = UpdateIconInner;\n}\n\n@freezed\nclass PageStyleIconState with _$PageStyleIconState {\n  const factory PageStyleIconState({\n    @Default(null) EmojiIconData? icon,\n  }) = _PageStyleIconState;\n\n  factory PageStyleIconState.initial() => const PageStyleIconState();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart';\nimport 'package:appflowy/shared/feedback_gesture_detector.dart';\nimport 'package:appflowy/util/font_family_extension.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nconst kPageStyleLayoutHeight = 52.0;\n\nclass PageStyleLayout extends StatelessWidget {\n  const PageStyleLayout({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(\n      builder: (context, state) {\n        return Column(\n          children: [\n            Row(\n              children: [\n                _OptionGroup<PageStyleFontLayout>(\n                  options: const [\n                    PageStyleFontLayout.small,\n                    PageStyleFontLayout.normal,\n                    PageStyleFontLayout.large,\n                  ],\n                  selectedOption: state.fontLayout,\n                  onTap: (option) => context\n                      .read<DocumentPageStyleBloc>()\n                      .add(DocumentPageStyleEvent.updateFont(option)),\n                ),\n                const HSpace(14),\n                _OptionGroup<PageStyleLineHeightLayout>(\n                  options: const [\n                    PageStyleLineHeightLayout.small,\n                    PageStyleLineHeightLayout.normal,\n                    PageStyleLineHeightLayout.large,\n                  ],\n                  selectedOption: state.lineHeightLayout,\n                  onTap: (option) => context\n                      .read<DocumentPageStyleBloc>()\n                      .add(DocumentPageStyleEvent.updateLineHeight(option)),\n                ),\n              ],\n            ),\n            const VSpace(12.0),\n            const _FontButton(),\n          ],\n        );\n      },\n    );\n  }\n}\n\nclass _OptionGroup<T> extends StatelessWidget {\n  const _OptionGroup({\n    required this.options,\n    required this.selectedOption,\n    required this.onTap,\n  });\n\n  final List<T> options;\n  final T selectedOption;\n  final void Function(T option) onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      child: DecoratedBox(\n        decoration: BoxDecoration(\n          color: context.pageStyleBackgroundColor,\n          borderRadius: const BorderRadius.horizontal(\n            left: Radius.circular(12),\n            right: Radius.circular(12),\n          ),\n        ),\n        child: Row(\n          children: options.map((option) {\n            final child = _buildSvg(option);\n            final showLeftCorner = option == options.first;\n            final showRightCorner = option == options.last;\n            return _buildOptionButton(\n              child,\n              showLeftCorner,\n              showRightCorner,\n              selectedOption == option,\n              () => onTap(option),\n            );\n          }).toList(),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildOptionButton(\n    Widget child,\n    bool showLeftCorner,\n    bool showRightCorner,\n    bool selected,\n    VoidCallback onTap,\n  ) {\n    return Expanded(\n      child: FeedbackGestureDetector(\n        feedbackType: HapticFeedbackType.medium,\n        onTap: onTap,\n        child: AnimatedContainer(\n          height: kPageStyleLayoutHeight,\n          duration: Durations.medium1,\n          decoration: selected\n              ? ShapeDecoration(\n                  color: const Color(0x141AC3F2),\n                  shape: RoundedRectangleBorder(\n                    side: const BorderSide(\n                      width: 1.50,\n                      color: Color(0xFF1AC3F2),\n                    ),\n                    borderRadius: BorderRadius.circular(12),\n                  ),\n                )\n              : null,\n          alignment: Alignment.center,\n          child: child,\n        ),\n      ),\n    );\n  }\n\n  Widget _buildSvg(dynamic option) {\n    if (option is PageStyleFontLayout) {\n      return switch (option) {\n        PageStyleFontLayout.small =>\n          const FlowySvg(FlowySvgs.m_font_size_small_s),\n        PageStyleFontLayout.normal =>\n          const FlowySvg(FlowySvgs.m_font_size_normal_s),\n        PageStyleFontLayout.large =>\n          const FlowySvg(FlowySvgs.m_font_size_large_s),\n      };\n    } else if (option is PageStyleLineHeightLayout) {\n      return switch (option) {\n        PageStyleLineHeightLayout.small =>\n          const FlowySvg(FlowySvgs.m_layout_small_s),\n        PageStyleLineHeightLayout.normal =>\n          const FlowySvg(FlowySvgs.m_layout_normal_s),\n        PageStyleLineHeightLayout.large =>\n          const FlowySvg(FlowySvgs.m_layout_large_s),\n      };\n    }\n    throw ArgumentError('Invalid option type');\n  }\n}\n\nclass _FontButton extends StatelessWidget {\n  const _FontButton();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(\n      builder: (context, state) {\n        final fontFamilyDisplayName =\n            (state.fontFamily ?? defaultFontFamily).fontFamilyDisplayName;\n        return GestureDetector(\n          onTap: () => _showFontSelector(context),\n          behavior: HitTestBehavior.opaque,\n          child: Container(\n            height: kPageStyleLayoutHeight,\n            decoration: BoxDecoration(\n              color: context.pageStyleBackgroundColor,\n              borderRadius: BorderRadius.circular(12.0),\n            ),\n            child: Row(\n              children: [\n                const HSpace(16.0),\n                FlowyText(LocaleKeys.titleBar_font.tr()),\n                const Spacer(),\n                FlowyText(\n                  fontFamilyDisplayName,\n                  color: context.pageStyleTextColor,\n                ),\n                const HSpace(6.0),\n                const FlowySvg(FlowySvgs.m_page_style_arrow_right_s),\n                const HSpace(12.0),\n              ],\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  void _showFontSelector(BuildContext context) {\n    final pageStyleBloc = context.read<DocumentPageStyleBloc>();\n    context.pop();\n\n    showMobileBottomSheet(\n      context,\n      showDragHandle: true,\n      showDivider: false,\n      showDoneButton: true,\n      showHeader: true,\n      title: LocaleKeys.titleBar_font.tr(),\n      backgroundColor: AFThemeExtension.of(context).background,\n      enableDraggableScrollable: true,\n      minChildSize: 0.6,\n      initialChildSize: 0.61,\n      scrollableWidgetBuilder: (_, controller) {\n        return BlocProvider.value(\n          value: pageStyleBloc,\n          child: BlocBuilder<DocumentPageStyleBloc, DocumentPageStyleState>(\n            builder: (context, state) {\n              return Expanded(\n                child: Scrollbar(\n                  controller: controller,\n                  child: FontSelector(\n                    scrollController: controller,\n                    selectedFontFamilyName:\n                        state.fontFamily ?? defaultFontFamily,\n                    onFontFamilySelected: (fontFamilyName) {\n                      pageStyleBloc.add(\n                        DocumentPageStyleEvent.updateFontFamily(\n                          fontFamilyName,\n                        ),\n                      );\n                    },\n                  ),\n                ),\n              );\n            },\n          ),\n        );\n      },\n      builder: (_) => const SizedBox.shrink(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart",
    "content": "import 'package:flutter/material.dart';\n\nextension PageStyleUtil on BuildContext {\n  Color get pageStyleBackgroundColor {\n    final themeMode = Theme.of(this).brightness;\n    return themeMode == Brightness.light\n        ? const Color(0xFFF5F5F8)\n        : const Color(0xFF303030);\n  }\n\n  Color get pageStyleTextColor {\n    final themeMode = Theme.of(this).brightness;\n    return themeMode == Brightness.light\n        ? const Color(0x7F1F2225)\n        : Colors.white54;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/page_style_bottom_sheet.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass PageStyleBottomSheet extends StatelessWidget {\n  const PageStyleBottomSheet({\n    super.key,\n    required this.view,\n    required this.tabs,\n  });\n\n  final ViewPB view;\n  final List<PickerTabType> tabs;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 16.0),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          // cover image\n          FlowyText(\n            LocaleKeys.pageStyle_coverImage.tr(),\n            color: context.pageStyleTextColor,\n            fontSize: 14.0,\n          ),\n          const VSpace(8.0),\n          PageStyleCoverImage(documentId: view.id),\n          const VSpace(20.0),\n          // layout: font size, line height and font family.\n          FlowyText(\n            LocaleKeys.pageStyle_layout.tr(),\n            color: context.pageStyleTextColor,\n            fontSize: 14.0,\n          ),\n          const VSpace(8.0),\n          const PageStyleLayout(),\n          const VSpace(20.0),\n          // icon\n          FlowyText(\n            LocaleKeys.pageStyle_pageIcon.tr(),\n            color: context.pageStyleTextColor,\n            fontSize: 14.0,\n          ),\n          const VSpace(8.0),\n          PageStyleIcon(\n            view: view,\n            tabs: tabs,\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nclass CalloutNodeParser extends NodeParser {\n  const CalloutNodeParser();\n\n  @override\n  String get id => CalloutBlockKeys.type;\n\n  @override\n  String transform(Node node, DocumentMarkdownEncoder? encoder) {\n    final delta = node.delta ?? Delta()\n      ..insert('');\n    final String markdown = DeltaMarkdownEncoder()\n        .convert(delta)\n        .split('\\n')\n        .map((e) => '> $e')\n        .join('\\n');\n    final type = node.attributes[CalloutBlockKeys.iconType];\n    final icon = type == FlowyIconType.emoji.name || type == null || type == \"\"\n        ? node.attributes[CalloutBlockKeys.icon]\n        : null;\n\n    final content = icon == null ? markdown : \"> $icon\\n$markdown\";\n\n    return '''\n$content\n\n''';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_image_node_parser.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:archive/archive.dart';\nimport 'package:path/path.dart' as p;\n\nimport '../image/custom_image_block_component/custom_image_block_component.dart';\n\nclass CustomImageNodeParser extends NodeParser {\n  const CustomImageNodeParser();\n\n  @override\n  String get id => ImageBlockKeys.type;\n\n  @override\n  String transform(Node node, DocumentMarkdownEncoder? encoder) {\n    assert(node.children.isEmpty);\n    final url = node.attributes[CustomImageBlockKeys.url];\n    assert(url != null);\n    return '![]($url)\\n';\n  }\n}\n\nclass CustomImageNodeFileParser extends NodeParser {\n  const CustomImageNodeFileParser(this.files, this.dirPath);\n\n  final List<Future<ArchiveFile>> files;\n  final String dirPath;\n\n  @override\n  String get id => ImageBlockKeys.type;\n\n  @override\n  String transform(Node node, DocumentMarkdownEncoder? encoder) {\n    assert(node.children.isEmpty);\n    final url = node.attributes[CustomImageBlockKeys.url];\n    final hasFile = File(url).existsSync();\n    if (hasFile) {\n      final bytes = File(url).readAsBytesSync();\n      files.add(\n        Future.value(\n          ArchiveFile(p.join(dirPath, p.basename(url)), bytes.length, bytes),\n        ),\n      );\n      return '![](${p.join(dirPath, p.basename(url))})\\n';\n    }\n    assert(url != null);\n    return '![]($url)\\n';\n  }\n}\n\nclass CustomMultiImageNodeFileParser extends NodeParser {\n  const CustomMultiImageNodeFileParser(this.files, this.dirPath);\n\n  final List<Future<ArchiveFile>> files;\n  final String dirPath;\n\n  @override\n  String get id => MultiImageBlockKeys.type;\n\n  @override\n  String transform(Node node, DocumentMarkdownEncoder? encoder) {\n    assert(node.children.isEmpty);\n    final images = node.attributes[MultiImageBlockKeys.images] as List;\n    final List<String> markdownImages = [];\n    for (final image in images) {\n      final String url = image['url'] ?? '';\n      if (url.isEmpty) continue;\n      final hasFile = File(url).existsSync();\n      if (hasFile) {\n        final bytes = File(url).readAsBytesSync();\n        final filePath = p.join(dirPath, p.basename(url));\n        files.add(\n          Future.value(ArchiveFile(filePath, bytes.length, bytes)),\n        );\n        markdownImages.add('![]($filePath)');\n      } else {\n        markdownImages.add('![]($url)');\n      }\n    }\n    return markdownImages.join('\\n');\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_paragraph_node_parser.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nclass CustomParagraphNodeParser extends NodeParser {\n  const CustomParagraphNodeParser();\n\n  @override\n  String get id => ParagraphBlockKeys.type;\n\n  @override\n  String transform(Node node, DocumentMarkdownEncoder? encoder) {\n    final delta = node.delta;\n    if (delta != null) {\n      for (final o in delta) {\n        final attribute = o.attributes ?? {};\n        final Map? mention = attribute[MentionBlockKeys.mention] ?? {};\n        if (mention == null) continue;\n\n        /// filter date reminder node, and return it\n        final String date = mention[MentionBlockKeys.date] ?? '';\n        if (date.isNotEmpty) {\n          final dateTime = DateTime.tryParse(date);\n          if (dateTime == null) continue;\n          return '${DateFormat.yMMMd().format(dateTime)}\\n';\n        }\n\n        /// filter reference page\n        final String pageId = mention[MentionBlockKeys.pageId] ?? '';\n        if (pageId.isNotEmpty) {\n          return '[]($pageId)\\n';\n        }\n      }\n    }\n    return const TextNodeParser().transform(node, encoder);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/database_node_parser.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart';\nimport 'package:appflowy/workspace/application/settings/share/export_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:archive/archive.dart';\nimport 'package:path/path.dart' as p;\n\nabstract class DatabaseNodeParser extends NodeParser {\n  DatabaseNodeParser(this.files, this.dirPath);\n\n  final List<Future<ArchiveFile>> files;\n  final String dirPath;\n\n  @override\n  String transform(Node node, DocumentMarkdownEncoder? encoder) {\n    final String viewId = node.attributes[DatabaseBlockKeys.viewID] ?? '';\n    if (viewId.isEmpty) return '';\n    files.add(_convertDatabaseToCSV(viewId));\n    return '[](${p.join(dirPath, '$viewId.csv')})\\n';\n  }\n\n  Future<ArchiveFile> _convertDatabaseToCSV(String viewId) async {\n    final result = await BackendExportService.exportDatabaseAsCSV(viewId);\n    final filePath = p.join(dirPath, '$viewId.csv');\n    ArchiveFile file = ArchiveFile.string(filePath, '');\n    result.fold(\n      (s) => file = ArchiveFile.string(filePath, s.data),\n      (f) => Log.error('convertDatabaseToCSV error with $viewId, error: $f'),\n    );\n    return file;\n  }\n}\n\nclass GridNodeParser extends DatabaseNodeParser {\n  GridNodeParser(super.files, super.dirPath);\n\n  @override\n  String get id => DatabaseBlockKeys.gridType;\n}\n\nclass BoardNodeParser extends DatabaseNodeParser {\n  BoardNodeParser(super.files, super.dirPath);\n\n  @override\n  String get id => DatabaseBlockKeys.boardType;\n}\n\nclass CalendarNodeParser extends DatabaseNodeParser {\n  CalendarNodeParser(super.files, super.dirPath);\n\n  @override\n  String get id => DatabaseBlockKeys.calendarType;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart",
    "content": "export 'callout_node_parser.dart';\nexport 'custom_image_node_parser.dart';\nexport 'custom_paragraph_node_parser.dart';\nexport 'database_node_parser.dart';\nexport 'file_block_node_parser.dart';\nexport 'link_preview_node_parser.dart';\nexport 'math_equation_node_parser.dart';\nexport 'simple_table_node_parser.dart';\nexport 'toggle_list_node_parser.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/file_block_node_parser.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nclass FileBlockNodeParser extends NodeParser {\n  const FileBlockNodeParser();\n\n  @override\n  String get id => FileBlockKeys.type;\n\n  @override\n  String transform(Node node, DocumentMarkdownEncoder? encoder) {\n    final name = node.attributes[FileBlockKeys.name];\n    final url = node.attributes[FileBlockKeys.url];\n    if (name == null || url == null) {\n      return '';\n    }\n    return '[$name]($url)\\n';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/link_preview_node_parser.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\n\nclass LinkPreviewNodeParser extends NodeParser {\n  const LinkPreviewNodeParser();\n\n  @override\n  String get id => LinkPreviewBlockKeys.type;\n\n  @override\n  String transform(Node node, DocumentMarkdownEncoder? encoder) {\n    final href = node.attributes[LinkPreviewBlockKeys.url];\n    if (href == null) {\n      return '';\n    }\n    return '[$href]($href)\\n';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_code_parser.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:markdown/markdown.dart' as md;\n\nclass MarkdownCodeBlockParser extends CustomMarkdownParser {\n  const MarkdownCodeBlockParser();\n\n  @override\n  List<Node> transform(\n    md.Node element,\n    List<CustomMarkdownParser> parsers, {\n    MarkdownListType listType = MarkdownListType.unknown,\n    int? startNumber,\n  }) {\n    if (element is! md.Element) {\n      return [];\n    }\n\n    if (element.tag != 'pre') {\n      return [];\n    }\n\n    final ec = element.children;\n    if (ec == null || ec.isEmpty) {\n      return [];\n    }\n\n    final code = ec.first;\n    if (code is! md.Element || code.tag != 'code') {\n      return [];\n    }\n\n    String? language;\n    if (code.attributes.containsKey('class')) {\n      final classes = code.attributes['class']!.split(' ');\n      final languageClass = classes.firstWhere(\n        (c) => c.startsWith('language-'),\n        orElse: () => '',\n      );\n      language = languageClass.substring('language-'.length);\n    }\n\n    return [\n      codeBlockNode(\n        language: language,\n        delta: Delta()..insert(code.textContent.trimRight()),\n      ),\n    ];\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_parsers.dart",
    "content": "export 'markdown_code_parser.dart';\nexport 'markdown_simple_table_parser.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_simple_table_parser.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:markdown/markdown.dart' as md;\nimport 'package:universal_platform/universal_platform.dart';\n\nclass MarkdownSimpleTableParser extends CustomMarkdownParser {\n  const MarkdownSimpleTableParser({\n    this.tableWidth,\n  });\n\n  final double? tableWidth;\n\n  @override\n  List<Node> transform(\n    md.Node element,\n    List<CustomMarkdownParser> parsers, {\n    MarkdownListType listType = MarkdownListType.unknown,\n    int? startNumber,\n  }) {\n    if (element is! md.Element) {\n      return [];\n    }\n\n    if (element.tag != 'table') {\n      return [];\n    }\n\n    final ec = element.children;\n    if (ec == null || ec.isEmpty) {\n      return [];\n    }\n\n    final th = ec\n        .whereType<md.Element>()\n        .where((e) => e.tag == 'thead')\n        .firstOrNull\n        ?.children\n        ?.whereType<md.Element>()\n        .where((e) => e.tag == 'tr')\n        .expand((e) => e.children?.whereType<md.Element>().toList() ?? [])\n        .where((e) => e.tag == 'th')\n        .toList();\n\n    final tr = ec\n        .whereType<md.Element>()\n        .where((e) => e.tag == 'tbody')\n        .firstOrNull\n        ?.children\n        ?.whereType<md.Element>()\n        .where((e) => e.tag == 'tr')\n        .toList();\n\n    if (th == null || tr == null || th.isEmpty || tr.isEmpty) {\n      return [];\n    }\n\n    final rows = <Node>[];\n\n    // Add header cells\n\n    rows.add(\n      simpleTableRowBlockNode(\n        children: th\n            .map(\n              (e) => simpleTableCellBlockNode(\n                children: [\n                  paragraphNode(\n                    delta: DeltaMarkdownDecoder().convertNodes(e.children),\n                  ),\n                ],\n              ),\n            )\n            .toList(),\n      ),\n    );\n\n    // Add body cells\n    for (var i = 0; i < tr.length; i++) {\n      final td = tr[i]\n          .children\n          ?.whereType<md.Element>()\n          .where((e) => e.tag == 'td')\n          .toList();\n\n      if (td == null || td.isEmpty) {\n        continue;\n      }\n\n      rows.add(\n        simpleTableRowBlockNode(\n          children: td\n              .map(\n                (e) => simpleTableCellBlockNode(\n                  children: [\n                    paragraphNode(\n                      delta: DeltaMarkdownDecoder().convertNodes(e.children),\n                    ),\n                  ],\n                ),\n              )\n              .toList(),\n        ),\n      );\n    }\n\n    return [\n      simpleTableBlockNode(\n        children: rows,\n        enableHeaderRow: true,\n        columnWidths: UniversalPlatform.isMobile || tableWidth == null\n            ? null\n            : {for (var i = 0; i < th.length; i++) i.toString(): tableWidth!},\n      ),\n    ];\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/math_equation_node_parser.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nclass MathEquationNodeParser extends NodeParser {\n  const MathEquationNodeParser();\n\n  @override\n  String get id => MathEquationBlockKeys.type;\n\n  @override\n  String transform(Node node, DocumentMarkdownEncoder? encoder) {\n    return '\\$\\$${node.attributes[MathEquationBlockKeys.formula]}\\$\\$';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/simple_table_node_parser.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\n/// Parser for converting SimpleTable nodes to markdown format\nclass SimpleTableNodeParser extends NodeParser {\n  const SimpleTableNodeParser();\n\n  @override\n  String get id => SimpleTableBlockKeys.type;\n\n  @override\n  String transform(Node node, DocumentMarkdownEncoder? encoder) {\n    try {\n      final tableData = _extractTableData(node, encoder);\n      if (tableData.isEmpty) {\n        return '';\n      }\n      return _buildMarkdownTable(tableData);\n    } catch (e) {\n      return '';\n    }\n  }\n\n  /// Extracts table data from the node structure into a 2D list of strings\n  /// Each inner list represents a row, and each string represents a cell's content\n  List<List<String>> _extractTableData(\n    Node node,\n    DocumentMarkdownEncoder? encoder,\n  ) {\n    final tableData = <List<String>>[];\n    final rows = node.children;\n\n    for (final row in rows) {\n      final rowData = _extractRowData(row, encoder);\n      tableData.add(rowData);\n    }\n\n    return tableData;\n  }\n\n  /// Extracts data from a single table row\n  List<String> _extractRowData(Node row, DocumentMarkdownEncoder? encoder) {\n    final rowData = <String>[];\n    final cells = row.children;\n\n    for (final cell in cells) {\n      final content = _extractCellContent(cell, encoder);\n      rowData.add(content);\n    }\n\n    return rowData;\n  }\n\n  /// Extracts and formats content from a single table cell\n  String _extractCellContent(Node cell, DocumentMarkdownEncoder? encoder) {\n    final contentBuffer = StringBuffer();\n\n    for (final child in cell.children) {\n      final delta = child.delta;\n\n      // if the node doesn't contain delta, fallback to the encoder\n      final content = delta != null\n          ? DeltaMarkdownEncoder().convert(delta)\n          : encoder?.convertNodes([child]).trim() ?? '';\n\n      // Escape pipe characters to prevent breaking markdown table structure\n      contentBuffer.write(content.replaceAll('|', '\\\\|'));\n    }\n\n    return contentBuffer.toString();\n  }\n\n  /// Builds a markdown table string from the extracted table data\n  /// First row is treated as header, followed by separator row and data rows\n  String _buildMarkdownTable(List<List<String>> tableData) {\n    final markdown = StringBuffer();\n    final columnCount = tableData[0].length;\n\n    // Add header row\n    markdown.writeln('|${tableData[0].join('|')}|');\n\n    // Add separator row\n    markdown.writeln('|${List.filled(columnCount, '---').join('|')}|');\n\n    // Add data rows (skip header row)\n    for (int i = 1; i < tableData.length; i++) {\n      markdown.writeln('|${tableData[i].join('|')}|');\n    }\n\n    return markdown.toString();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nclass SubPageNodeParser extends NodeParser {\n  const SubPageNodeParser();\n\n  @override\n  String get id => SubPageBlockKeys.type;\n\n  @override\n  String transform(Node node, DocumentMarkdownEncoder? encoder) {\n    final String viewId = node.attributes[SubPageBlockKeys.viewId] ?? '';\n    if (viewId.isNotEmpty) {\n      final view = pageMemorizer[viewId];\n      return '[$viewId](${view?.name ?? ''})\\n';\n    }\n    return '';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/toggle_list_node_parser.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nenum ToggleListExportStyle {\n  github,\n  markdown,\n}\n\nclass ToggleListNodeParser extends NodeParser {\n  const ToggleListNodeParser({\n    this.exportStyle = ToggleListExportStyle.markdown,\n  });\n\n  final ToggleListExportStyle exportStyle;\n\n  @override\n  String get id => ToggleListBlockKeys.type;\n\n  @override\n  String transform(Node node, DocumentMarkdownEncoder? encoder) {\n    final delta = node.delta ?? Delta()\n      ..insert('');\n    String markdown = DeltaMarkdownEncoder().convert(delta);\n    final details = encoder?.convertNodes(\n      node.children,\n      withIndent: true,\n    );\n    switch (exportStyle) {\n      case ToggleListExportStyle.github:\n        return '''<details>\n<summary>$markdown</summary>\n\n$details\n</details>\n''';\n      case ToggleListExportStyle.markdown:\n        markdown = '- $markdown\\n';\n        if (details != null && details.isNotEmpty) {\n          markdown += details;\n        }\n        return markdown;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart",
    "content": "export 'actions/block_action_list.dart';\nexport 'actions/option/option_actions.dart';\nexport 'ai/ai_writer_block_component.dart';\nexport 'ai/ai_writer_toolbar_item.dart';\nexport 'align_toolbar_item/align_toolbar_item.dart';\nexport 'base/backtick_character_command.dart';\nexport 'base/cover_title_command.dart';\nexport 'base/toolbar_extension.dart';\nexport 'bulleted_list/bulleted_list_icon.dart';\nexport 'callout/callout_block_component.dart';\nexport 'code_block/code_block_language_selector.dart';\nexport 'code_block/code_block_menu_item.dart';\nexport 'columns/simple_column_block_component.dart';\nexport 'columns/simple_column_block_width_resizer.dart';\nexport 'columns/simple_column_node_extension.dart';\nexport 'columns/simple_columns_block_component.dart';\nexport 'context_menu/custom_context_menu.dart';\nexport 'copy_and_paste/custom_copy_command.dart';\nexport 'copy_and_paste/custom_cut_command.dart';\nexport 'copy_and_paste/custom_paste_command.dart';\nexport 'database/database_view_block_component.dart';\nexport 'database/inline_database_menu_item.dart';\nexport 'database/referenced_database_menu_item.dart';\nexport 'error/error_block_component_builder.dart';\nexport 'extensions/flowy_tint_extension.dart';\nexport 'file/file_block.dart';\nexport 'find_and_replace/find_and_replace_menu.dart';\nexport 'font/customize_font_toolbar_item.dart';\nexport 'header/cover_editor_bloc.dart';\nexport 'header/custom_cover_picker.dart';\nexport 'header/document_cover_widget.dart';\nexport 'heading/heading_toolbar_item.dart';\nexport 'image/custom_image_block_component/custom_image_block_component.dart';\nexport 'image/custom_image_block_component/image_menu.dart';\nexport 'image/image_selection_menu.dart';\nexport 'image/mobile_image_toolbar_item.dart';\nexport 'image/multi_image_block_component/multi_image_block_component.dart';\nexport 'image/multi_image_block_component/multi_image_menu.dart';\nexport 'inline_math_equation/inline_math_equation.dart';\nexport 'inline_math_equation/inline_math_equation_toolbar_item.dart';\nexport 'keyboard_interceptor/keyboard_interceptor.dart';\nexport 'link_preview/custom_link_preview.dart';\nexport 'link_preview/link_preview_menu.dart';\nexport 'math_equation/math_equation_block_component.dart';\nexport 'math_equation/mobile_math_equation_toolbar_item.dart';\nexport 'mention/mention_block.dart';\nexport 'mobile_floating_toolbar/custom_mobile_floating_toolbar.dart';\nexport 'mobile_toolbar_v3/aa_toolbar_item.dart';\nexport 'mobile_toolbar_v3/add_attachment_item.dart';\nexport 'mobile_toolbar_v3/add_block_toolbar_item.dart';\nexport 'mobile_toolbar_v3/appflowy_mobile_toolbar.dart';\nexport 'mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart';\nexport 'mobile_toolbar_v3/basic_toolbar_item.dart';\nexport 'mobile_toolbar_v3/indent_outdent_toolbar_item.dart';\nexport 'mobile_toolbar_v3/list_toolbar_item.dart';\nexport 'mobile_toolbar_v3/more_toolbar_item.dart';\nexport 'mobile_toolbar_v3/toolbar_item_builder.dart';\nexport 'mobile_toolbar_v3/undo_redo_toolbar_item.dart';\nexport 'mobile_toolbar_v3/util.dart';\nexport 'numbered_list/numbered_list_icon.dart';\nexport 'outline/outline_block_component.dart';\nexport 'parsers/document_markdown_parsers.dart';\nexport 'parsers/markdown_parsers.dart';\nexport 'parsers/markdown_simple_table_parser.dart';\nexport 'quote/quote_block_component.dart';\nexport 'quote/quote_block_shortcuts.dart';\nexport 'shortcuts/character_shortcuts.dart';\nexport 'shortcuts/command_shortcuts.dart';\nexport 'simple_table/simple_table.dart';\nexport 'slash_menu/slash_command.dart';\nexport 'slash_menu/slash_menu_items_builder.dart';\nexport 'sub_page/sub_page_block_component.dart';\nexport 'table/table_menu.dart';\nexport 'table/table_option_action.dart';\nexport 'todo_list/todo_list_icon.dart';\nexport 'toggle/toggle_block_component.dart';\nexport 'toggle/toggle_block_shortcuts.dart';\nexport 'video/video_block_component.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\n/// In memory cache of the quote block height to avoid flashing when the quote block is updated.\nMap<String, double> _quoteBlockHeightCache = {};\n\ntypedef QuoteBlockIconBuilder = Widget Function(\n  BuildContext context,\n  Node node,\n);\n\nclass QuoteBlockKeys {\n  const QuoteBlockKeys._();\n\n  static const String type = 'quote';\n\n  static const String delta = blockComponentDelta;\n\n  static const String backgroundColor = blockComponentBackgroundColor;\n\n  static const String textDirection = blockComponentTextDirection;\n}\n\nNode quoteNode({\n  Delta? delta,\n  String? textDirection,\n  Attributes? attributes,\n  Iterable<Node>? children,\n}) {\n  attributes ??= {'delta': (delta ?? Delta()).toJson()};\n  return Node(\n    type: QuoteBlockKeys.type,\n    attributes: {\n      ...attributes,\n      if (textDirection != null) QuoteBlockKeys.textDirection: textDirection,\n    },\n    children: children ?? [],\n  );\n}\n\nclass QuoteBlockComponentBuilder extends BlockComponentBuilder {\n  QuoteBlockComponentBuilder({\n    super.configuration,\n    this.iconBuilder,\n  });\n\n  final QuoteBlockIconBuilder? iconBuilder;\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    return QuoteBlockComponentWidget(\n      key: node.key,\n      node: node,\n      configuration: configuration,\n      iconBuilder: iconBuilder,\n      showActions: showActions(node),\n      actionBuilder: (context, state) => actionBuilder(\n        blockComponentContext,\n        state,\n      ),\n      actionTrailingBuilder: (context, state) => actionTrailingBuilder(\n        blockComponentContext,\n        state,\n      ),\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate => (node) => node.delta != null;\n}\n\nclass QuoteBlockComponentWidget extends BlockComponentStatefulWidget {\n  const QuoteBlockComponentWidget({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n    this.iconBuilder,\n  });\n\n  final QuoteBlockIconBuilder? iconBuilder;\n\n  @override\n  State<QuoteBlockComponentWidget> createState() =>\n      _QuoteBlockComponentWidgetState();\n}\n\nclass _QuoteBlockComponentWidgetState extends State<QuoteBlockComponentWidget>\n    with\n        SelectableMixin,\n        DefaultSelectableMixin,\n        BlockComponentConfigurable,\n        BlockComponentBackgroundColorMixin,\n        BlockComponentTextDirectionMixin,\n        BlockComponentAlignMixin,\n        NestedBlockComponentStatefulWidgetMixin {\n  @override\n  final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');\n\n  @override\n  GlobalKey<State<StatefulWidget>> get containerKey => widget.node.key;\n\n  @override\n  GlobalKey<State<StatefulWidget>> blockComponentKey = GlobalKey(\n    debugLabel: QuoteBlockKeys.type,\n  );\n\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  @override\n  Node get node => widget.node;\n\n  late ValueNotifier<double> quoteBlockHeightNotifier = ValueNotifier(\n    _quoteBlockHeightCache[node.id] ?? 0,\n  );\n\n  StreamSubscription<EditorTransactionValue>? _transactionSubscription;\n\n  final GlobalKey layoutBuilderKey = GlobalKey();\n\n  @override\n  void initState() {\n    super.initState();\n\n    _updateQuoteBlockHeight();\n  }\n\n  @override\n  void dispose() {\n    _transactionSubscription?.cancel();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return NotificationListener<SizeChangedLayoutNotification>(\n      key: layoutBuilderKey,\n      onNotification: (notification) {\n        _updateQuoteBlockHeight();\n        return true;\n      },\n      child: SizeChangedLayoutNotifier(\n        child: node.children.isEmpty\n            ? buildComponent(context)\n            : buildComponentWithChildren(context),\n      ),\n    );\n  }\n\n  @override\n  Widget buildComponentWithChildren(BuildContext context) {\n    final Widget child = Stack(\n      children: [\n        Positioned.fill(\n          left: UniversalPlatform.isMobile ? padding.left : cachedLeft,\n          right: UniversalPlatform.isMobile ? padding.right : 0,\n          child: Container(\n            color: backgroundColor,\n          ),\n        ),\n        NestedListWidget(\n          indentPadding: indentPadding,\n          child: buildComponent(context, withBackgroundColor: false),\n          children: editorState.renderer.buildList(\n            context,\n            widget.node.children,\n          ),\n        ),\n      ],\n    );\n\n    return child;\n  }\n\n  @override\n  Widget buildComponent(\n    BuildContext context, {\n    bool withBackgroundColor = true,\n  }) {\n    final textDirection = calculateTextDirection(\n      layoutDirection: Directionality.maybeOf(context),\n    );\n\n    Widget child = AppFlowyRichText(\n      key: forwardKey,\n      delegate: this,\n      node: widget.node,\n      editorState: editorState,\n      textAlign: alignment?.toTextAlign ?? textAlign,\n      placeholderText: placeholderText,\n      textSpanDecorator: (textSpan) => textSpan.updateTextStyle(\n        textStyleWithTextSpan(textSpan: textSpan),\n      ),\n      placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle(\n        placeholderTextStyleWithTextSpan(textSpan: textSpan),\n      ),\n      textDirection: textDirection,\n      cursorColor: editorState.editorStyle.cursorColor,\n      selectionColor: editorState.editorStyle.selectionColor,\n      cursorWidth: editorState.editorStyle.cursorWidth,\n    );\n\n    child = Container(\n      width: double.infinity,\n      alignment: alignment,\n      child: IntrinsicHeight(\n        child: Row(\n          crossAxisAlignment: CrossAxisAlignment.stretch,\n          mainAxisSize: MainAxisSize.min,\n          textDirection: textDirection,\n          children: [\n            widget.iconBuilder != null\n                ? widget.iconBuilder!(context, node)\n                : ValueListenableBuilder<double>(\n                    valueListenable: quoteBlockHeightNotifier,\n                    builder: (context, height, child) {\n                      return QuoteIcon(height: height);\n                    },\n                  ),\n            Flexible(\n              child: child,\n            ),\n          ],\n        ),\n      ),\n    );\n\n    child = Container(\n      color: withBackgroundColor ? backgroundColor : null,\n      child: Padding(\n        key: blockComponentKey,\n        padding: padding,\n        child: child,\n      ),\n    );\n\n    child = BlockSelectionContainer(\n      node: node,\n      delegate: this,\n      listenable: editorState.selectionNotifier,\n      remoteSelection: editorState.remoteSelections,\n      blockColor: editorState.editorStyle.selectionColor,\n      selectionAboveBlock: true,\n      supportTypes: const [\n        BlockSelectionType.block,\n      ],\n      child: child,\n    );\n\n    if (widget.showActions && widget.actionBuilder != null) {\n      child = BlockComponentActionWrapper(\n        node: node,\n        actionBuilder: widget.actionBuilder!,\n        actionTrailingBuilder: widget.actionTrailingBuilder,\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  void _updateQuoteBlockHeight() {\n    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n      final renderObject = layoutBuilderKey.currentContext?.findRenderObject();\n      double height = _quoteBlockHeightCache[node.id] ?? 0;\n      if (renderObject != null && renderObject is RenderBox) {\n        if (UniversalPlatform.isMobile) {\n          height = renderObject.size.height - padding.top;\n        } else {\n          height = renderObject.size.height - padding.top * 2;\n        }\n      } else {\n        height = 0;\n      }\n\n      quoteBlockHeightNotifier.value = height;\n      _quoteBlockHeightCache[node.id] = height;\n    });\n  }\n}\n\nclass QuoteIcon extends StatelessWidget {\n  const QuoteIcon({\n    super.key,\n    this.height = 0,\n  });\n\n  final double height;\n\n  @override\n  Widget build(BuildContext context) {\n    final textScaleFactor =\n        context.read<EditorState>().editorStyle.textScaleFactor;\n    return Container(\n      alignment: Alignment.center,\n      constraints:\n          const BoxConstraints(minWidth: 22, minHeight: 22, maxHeight: 22) *\n              textScaleFactor,\n      padding: const EdgeInsets.only(right: 6.0),\n      child: SizedBox(\n        width: 3 * textScaleFactor,\n        // use overflow box to ensure the container can overflow the height so that the children of the quote block can have the quote\n        child: OverflowBox(\n          alignment: Alignment.topCenter,\n          maxHeight: height,\n          child: Container(\n            width: 3 * textScaleFactor,\n            height: height,\n            color: Theme.of(context).colorScheme.primary,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/services.dart';\n\n/// Pressing Enter in a quote block will insert a newline (\\n) within the quote,\n/// while pressing Shift+Enter in a quote will insert a new paragraph next to the quote.\n///\n/// - support\n///   - desktop\n///   - mobile\n///   - web\n///\nfinal CharacterShortcutEvent insertNewLineInQuoteBlock = CharacterShortcutEvent(\n  key: 'insert a new line in quote block',\n  character: '\\n',\n  handler: _insertNewLineHandler,\n);\n\nCharacterShortcutEventHandler _insertNewLineHandler = (editorState) async {\n  final selection = editorState.selection?.normalized;\n  if (selection == null) {\n    return false;\n  }\n\n  final node = editorState.getNodeAtPath(selection.start.path);\n  if (node == null || node.type != QuoteBlockKeys.type) {\n    return false;\n  }\n\n  // delete the selection\n  await editorState.deleteSelection(selection);\n\n  if (HardwareKeyboard.instance.isShiftPressed) {\n    // ignore the shift+enter event, fallback to the default behavior\n    return false;\n  } else if (node.children.isEmpty &&\n      selection.endIndex == node.delta?.length) {\n    // insert a new paragraph within the callout block\n    final path = node.path.child(0);\n    final transaction = editorState.transaction;\n    transaction.insertNode(\n      path,\n      paragraphNode(),\n    );\n    transaction.afterSelection = Selection.collapsed(\n      Position(\n        path: path,\n      ),\n    );\n    await editorState.apply(transaction);\n    return true;\n  }\n\n  return false;\n};\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart",
    "content": "import 'package:flutter/widgets.dart';\n\n/// Shared context for the editor plugins.\n///\n/// For example, the backspace command requires the focus node of the cover title.\n/// so we need to use the shared context to get the focus node.\n///\nclass SharedEditorContext {\n  SharedEditorContext() : _coverTitleFocusNode = FocusNode();\n\n  // The focus node of the cover title.\n  final FocusNode _coverTitleFocusNode;\n\n  bool requestCoverTitleFocus = false;\n\n  bool isInDatabaseRowPage = false;\n\n  FocusNode get coverTitleFocusNode => _coverTitleFocusNode;\n\n  void dispose() {\n    _coverTitleFocusNode.dispose();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/character_shortcuts.dart",
    "content": "import 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_configuration.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/format_arrow_character.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/shortcuts/heading_block_shortcuts.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/shortcuts/numbered_list_block_shortcuts.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/plugins/emoji/emoji_actions_command.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_command.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:flutter/material.dart';\n\nList<CharacterShortcutEvent> buildCharacterShortcutEvents(\n  BuildContext context,\n  DocumentBloc documentBloc,\n  EditorStyleCustomizer styleCustomizer,\n  InlineActionsService inlineActionsService,\n  SlashMenuItemsBuilder slashMenuItemsBuilder,\n) {\n  return [\n    // code block\n    formatBacktickToCodeBlock,\n    ...codeBlockCharacterEvents,\n\n    // callout block\n    insertNewLineInCalloutBlock,\n\n    // quote block\n    insertNewLineInQuoteBlock,\n\n    // toggle list\n    formatGreaterToToggleList,\n    insertChildNodeInsideToggleList,\n\n    // customize the slash menu command\n    customAppFlowySlashCommand(\n      itemsBuilder: slashMenuItemsBuilder,\n      style: styleCustomizer.selectionMenuStyleBuilder(),\n      supportSlashMenuNodeTypes: supportSlashMenuNodeTypes,\n    ),\n\n    customFormatGreaterEqual,\n    customFormatDashGreater,\n    customFormatDoubleHyphenEmDash,\n\n    customFormatNumberToNumberedList,\n    customFormatSignToHeading,\n\n    ...standardCharacterShortcutEvents\n      ..removeWhere(\n        (shortcut) => [\n          slashCommand, // Remove default slash command\n          formatGreaterEqual, // Overridden by customFormatGreaterEqual\n          formatNumberToNumberedList, // Overridden by customFormatNumberToNumberedList\n          formatSignToHeading, // Overridden by customFormatSignToHeading\n          formatDoubleHyphenEmDash, // Overridden by customFormatDoubleHyphenEmDash\n        ].contains(shortcut),\n      ),\n\n    /// Inline Actions\n    /// - Reminder\n    /// - Inline-page reference\n    inlineActionsCommand(\n      inlineActionsService,\n      style: styleCustomizer.inlineActionsMenuStyleBuilder(),\n    ),\n\n    /// Inline page menu\n    /// - Using `[[`\n    pageReferenceShortcutBrackets(\n      context,\n      documentBloc.documentId,\n      styleCustomizer.inlineActionsMenuStyleBuilder(),\n    ),\n\n    /// - Using `+`\n    pageReferenceShortcutPlusSign(\n      context,\n      documentBloc.documentId,\n      styleCustomizer.inlineActionsMenuStyleBuilder(),\n    ),\n\n    /// show emoji list\n    /// - Using `:`\n    emojiCommand(context),\n  ];\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/command_shortcuts.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/math_equation/math_equation_shortcut.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/shortcuts/custom_delete_command.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'exit_edit_mode_command.dart';\n\nfinal List<CommandShortcutEvent> defaultCommandShortcutEvents = [\n  ...commandShortcutEvents.map((e) => e.copyWith()),\n];\n\n// Command shortcuts are order-sensitive. Verify order when modifying.\nList<CommandShortcutEvent> commandShortcutEvents = [\n  ...simpleTableCommands,\n\n  customExitEditingCommand,\n  backspaceToTitle,\n  removeToggleHeadingStyle,\n\n  arrowUpToTitle,\n  arrowLeftToTitle,\n\n  toggleToggleListCommand,\n\n  ...localizedCodeBlockCommands,\n\n  customCopyCommand,\n  customPasteCommand,\n  customPastePlainTextCommand,\n  customCutCommand,\n  customUndoCommand,\n  customRedoCommand,\n\n  ...customTextAlignCommands,\n\n  customDeleteCommand,\n  insertInlineMathEquationCommand,\n\n  // remove standard shortcuts for copy, cut, paste, todo\n  ...standardCommandShortcutEvents\n    ..removeWhere(\n      (shortcut) => [\n        copyCommand,\n        cutCommand,\n        pasteCommand,\n        pasteTextWithoutFormattingCommand,\n        toggleTodoListCommand,\n        undoCommand,\n        redoCommand,\n        exitEditingCommand,\n        ...tableCommands,\n        deleteCommand,\n      ].contains(shortcut),\n    ),\n\n  emojiShortcutEvent,\n];\n\nfinal _codeBlockLocalization = CodeBlockLocalizations(\n  codeBlockNewParagraph:\n      LocaleKeys.settings_shortcutsPage_commands_codeBlockNewParagraph.tr(),\n  codeBlockIndentLines:\n      LocaleKeys.settings_shortcutsPage_commands_codeBlockIndentLines.tr(),\n  codeBlockOutdentLines:\n      LocaleKeys.settings_shortcutsPage_commands_codeBlockOutdentLines.tr(),\n  codeBlockSelectAll:\n      LocaleKeys.settings_shortcutsPage_commands_codeBlockSelectAll.tr(),\n  codeBlockPasteText:\n      LocaleKeys.settings_shortcutsPage_commands_codeBlockPasteText.tr(),\n  codeBlockAddTwoSpaces:\n      LocaleKeys.settings_shortcutsPage_commands_codeBlockAddTwoSpaces.tr(),\n);\n\nfinal localizedCodeBlockCommands = codeBlockCommands(\n  localizations: _codeBlockLocalization,\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/custom_delete_command.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\n/// Delete key event.\n///\n/// - support\n///   - desktop\n///   - web\n///\nfinal CommandShortcutEvent customDeleteCommand = CommandShortcutEvent(\n  key: 'Delete Key',\n  getDescription: () => AppFlowyEditorL10n.current.cmdDeleteRight,\n  command: 'delete, shift+delete',\n  handler: _deleteCommandHandler,\n);\n\nCommandShortcutEventHandler _deleteCommandHandler = (editorState) {\n  final selection = editorState.selection;\n  final selectionType = editorState.selectionType;\n  if (selection == null) {\n    return KeyEventResult.ignored;\n  }\n  if (selectionType == SelectionType.block) {\n    return _deleteInBlockSelection(editorState);\n  } else if (selection.isCollapsed) {\n    return _deleteInCollapsedSelection(editorState);\n  } else {\n    return _deleteInNotCollapsedSelection(editorState);\n  }\n};\n\n/// Handle delete key event when selection is collapsed.\nCommandShortcutEventHandler _deleteInCollapsedSelection = (editorState) {\n  final selection = editorState.selection;\n  if (selection == null || !selection.isCollapsed) {\n    return KeyEventResult.ignored;\n  }\n\n  final position = selection.start;\n  final node = editorState.getNodeAtPath(position.path);\n  final delta = node?.delta;\n  if (node == null || delta == null) {\n    return KeyEventResult.ignored;\n  }\n\n  final transaction = editorState.transaction;\n\n  if (position.offset == delta.length) {\n    final Node? tableParent =\n        node.findParent((element) => element.type == SimpleTableBlockKeys.type);\n    Node? nextTableParent;\n    final next = node.findDownward((element) {\n      nextTableParent = element\n          .findParent((element) => element.type == SimpleTableBlockKeys.type);\n      // break if only one is in a table or they're in different tables\n      return tableParent != nextTableParent ||\n          // merge the next node with delta\n          element.delta != null;\n    });\n    // table nodes should be deleted using the table menu\n    // in-table paragraphs should only be deleted inside the table\n    if (next != null && tableParent == nextTableParent) {\n      if (next.children.isNotEmpty) {\n        final path = node.path + [node.children.length];\n        transaction.insertNodes(path, next.children);\n      }\n      transaction\n        ..deleteNode(next)\n        ..mergeText(\n          node,\n          next,\n        );\n      editorState.apply(transaction);\n      return KeyEventResult.handled;\n    }\n  } else {\n    final nextIndex = delta.nextRunePosition(position.offset);\n    if (nextIndex <= delta.length) {\n      transaction.deleteText(\n        node,\n        position.offset,\n        nextIndex - position.offset,\n      );\n      editorState.apply(transaction);\n      return KeyEventResult.handled;\n    }\n  }\n\n  return KeyEventResult.ignored;\n};\n\n/// Handle delete key event when selection is not collapsed.\nCommandShortcutEventHandler _deleteInNotCollapsedSelection = (editorState) {\n  final selection = editorState.selection;\n  if (selection == null || selection.isCollapsed) {\n    return KeyEventResult.ignored;\n  }\n  editorState.deleteSelection(\n    selection,\n    ignoreNodeTypes: [\n      SimpleTableCellBlockKeys.type,\n      TableCellBlockKeys.type,\n    ],\n  );\n  return KeyEventResult.handled;\n};\n\nCommandShortcutEventHandler _deleteInBlockSelection = (editorState) {\n  final selection = editorState.selection;\n  if (selection == null || editorState.selectionType != SelectionType.block) {\n    return KeyEventResult.ignored;\n  }\n  final transaction = editorState.transaction;\n  transaction.deleteNodesAtPath(selection.start.path);\n  editorState\n      .apply(transaction)\n      .then((value) => editorState.selectionType = null);\n\n  return KeyEventResult.handled;\n};\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/exit_edit_mode_command.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\n/// End key event.\n///\n/// - support\n///   - desktop\n///   - web\n///\nfinal CommandShortcutEvent customExitEditingCommand = CommandShortcutEvent(\n  key: 'exit the editing mode',\n  getDescription: () => AppFlowyEditorL10n.current.cmdExitEditing,\n  command: 'escape',\n  handler: _exitEditingCommandHandler,\n);\n\nCommandShortcutEventHandler _exitEditingCommandHandler = (editorState) {\n  if (editorState.selection == null) {\n    return KeyEventResult.ignored;\n  }\n  editorState.selection = null;\n  editorState.service.keyboardService?.closeKeyboard();\n  return KeyEventResult.handled;\n};\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/heading_block_shortcuts.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\n/// Convert '# ' to bulleted list\n///\n/// - support\n///   - desktop\n///   - mobile\n///   - web\n///\nCharacterShortcutEvent customFormatSignToHeading = CharacterShortcutEvent(\n  key: 'format sign to heading list',\n  character: ' ',\n  handler: (editorState) async => formatMarkdownSymbol(\n    editorState,\n    (node) => true,\n    (_, text, selection) {\n      final characters = text.split('');\n      // only supports h1 to h6 levels\n      // if the characters is empty, the every function will return true directly\n      return characters.isNotEmpty &&\n          characters.every((element) => element == '#') &&\n          characters.length < 7;\n    },\n    (text, node, delta) {\n      final numberOfSign = text.split('').length;\n      final type = node.type;\n\n      // if current node is toggle block, try to convert it to toggle heading block.\n      if (type == ToggleListBlockKeys.type) {\n        final collapsed =\n            node.attributes[ToggleListBlockKeys.collapsed] as bool?;\n        return [\n          toggleHeadingNode(\n            level: numberOfSign,\n            delta: delta.compose(Delta()..delete(numberOfSign)),\n            collapsed: collapsed ?? false,\n            children: node.children.map((child) => child.deepCopy()),\n          ),\n        ];\n      }\n      return [\n        headingNode(\n          level: numberOfSign,\n          delta: delta.compose(Delta()..delete(numberOfSign)),\n        ),\n        if (node.children.isNotEmpty) ...node.children,\n      ];\n    },\n  ),\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/numbered_list_block_shortcuts.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\n/// Convert 'num. ' to bulleted list\n///\n/// - support\n///   - desktop\n///   - mobile\n///\n/// In heading block and toggle heading block, this shortcut will be ignored.\nCharacterShortcutEvent customFormatNumberToNumberedList =\n    CharacterShortcutEvent(\n  key: 'format number to numbered list',\n  character: ' ',\n  handler: (editorState) async => formatMarkdownSymbol(\n    editorState,\n    (node) => node.type != NumberedListBlockKeys.type,\n    (node, text, selection) {\n      final shouldBeIgnored = _shouldBeIgnored(node);\n      if (shouldBeIgnored) {\n        return false;\n      }\n\n      final match = numberedListRegex.firstMatch(text);\n      if (match == null) {\n        return false;\n      }\n\n      final matchText = match.group(0);\n      final numberText = match.group(1);\n\n      if (matchText == null || numberText == null) {\n        return false;\n      }\n\n      // if the previous one is numbered list,\n      // we should check the current number is the next number of the previous one\n      Node? previous = node.previous;\n      int level = 0;\n      int? startNumber;\n      while (previous != null && previous.type == NumberedListBlockKeys.type) {\n        startNumber = previous.attributes[NumberedListBlockKeys.number] as int?;\n        level++;\n        previous = previous.previous;\n      }\n      if (startNumber != null) {\n        final currentNumber = int.tryParse(numberText);\n        if (currentNumber == null || currentNumber != startNumber + level) {\n          return false;\n        }\n      }\n\n      return selection.endIndex == matchText.length;\n    },\n    (text, node, delta) {\n      final match = numberedListRegex.firstMatch(text);\n      final matchText = match?.group(0);\n      if (matchText == null) {\n        return [node];\n      }\n\n      final number = matchText.substring(0, matchText.length - 1);\n      final composedDelta = delta.compose(\n        Delta()..delete(matchText.length),\n      );\n      return [\n        node.copyWith(\n          type: NumberedListBlockKeys.type,\n          attributes: {\n            NumberedListBlockKeys.delta: composedDelta.toJson(),\n            NumberedListBlockKeys.number: int.tryParse(number),\n          },\n        ),\n      ];\n    },\n  ),\n);\n\nbool _shouldBeIgnored(Node node) {\n  final type = node.type;\n\n  // ignore heading block\n  if (type == HeadingBlockKeys.type) {\n    return true;\n  }\n\n  // ignore toggle heading block\n  final level = node.attributes[ToggleListBlockKeys.level] as int?;\n  if (type == ToggleListBlockKeys.type && level != null) {\n    return true;\n  }\n\n  return false;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart",
    "content": "export 'simple_table_block_component.dart';\nexport 'simple_table_cell_block_component.dart';\nexport 'simple_table_constants.dart';\nexport 'simple_table_more_action.dart';\nexport 'simple_table_operations/simple_table_operations.dart';\nexport 'simple_table_row_block_component.dart';\nexport 'simple_table_shortcuts/simple_table_commands.dart';\nexport 'simple_table_widgets/widgets.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_widget.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\ntypedef SimpleTableColumnWidthMap = Map<String, double>;\ntypedef SimpleTableRowAlignMap = Map<String, String>;\ntypedef SimpleTableColumnAlignMap = Map<String, String>;\ntypedef SimpleTableColorMap = Map<String, String>;\ntypedef SimpleTableAttributeMap = Map<String, bool>;\n\nclass SimpleTableBlockKeys {\n  const SimpleTableBlockKeys._();\n\n  static const String type = 'simple_table';\n\n  /// enable header row\n  /// it's a bool value, default is false\n  static const String enableHeaderRow = 'enable_header_row';\n\n  /// enable column header\n  /// it's a bool value, default is false\n  static const String enableHeaderColumn = 'enable_header_column';\n\n  /// column colors\n  /// it's a [SimpleTableColorMap] value, {column_index: color, ...}\n  /// the number of colors should be the same as the number of columns\n  static const String columnColors = 'column_colors';\n\n  /// row colors\n  /// it's a [SimpleTableColorMap] value, {row_index: color, ...}\n  /// the number of colors should be the same as the number of rows\n  static const String rowColors = 'row_colors';\n\n  /// column alignments\n  /// it's a [SimpleTableColumnAlignMap] value, {column_index: align, ...}\n  /// the value should be one of the following: 'left', 'center', 'right'\n  static const String columnAligns = 'column_aligns';\n\n  /// row alignments\n  /// it's a [SimpleTableRowAlignMap] value, {row_index: align, ...}\n  /// the value should be one of the following: 'top', 'center', 'bottom'\n  static const String rowAligns = 'row_aligns';\n\n  /// column bold attributes\n  /// it's a [SimpleTableAttributeMap] value, {column_index: attribute, ...}\n  /// the attribute should be one of the following: true, false\n  static const String columnBoldAttributes = 'column_bold_attributes';\n\n  /// row bold attributes\n  /// it's a [SimpleTableAttributeMap] value, {row_index: true, ...}\n  /// the attribute should be one of the following: true, false\n  static const String rowBoldAttributes = 'row_bold_attributes';\n\n  /// column text color attributes\n  /// it's a [SimpleTableColorMap] value, {column_index: color_hex_code, ...}\n  /// the attribute should be the color hex color or appflowy_theme_color\n  static const String columnTextColors = 'column_text_colors';\n\n  /// row text color attributes\n  /// it's a [SimpleTableColorMap] value, {row_index: color_hex_code, ...}\n  /// the attribute should be the color hex color or appflowy_theme_color\n  static const String rowTextColors = 'row_text_colors';\n\n  /// column widths\n  /// it's a [SimpleTableColumnWidthMap] value, {column_index: width, ...}\n  static const String columnWidths = 'column_widths';\n\n  /// distribute column widths evenly\n  /// if the user distributed the column widths evenly before, the value should be true,\n  /// and for the newly added column, using the width of the previous column.\n  /// it's a bool value, default is false\n  static const String distributeColumnWidthsEvenly =\n      'distribute_column_widths_evenly';\n}\n\nNode simpleTableBlockNode({\n  bool enableHeaderRow = false,\n  bool enableHeaderColumn = false,\n  SimpleTableColorMap? columnColors,\n  SimpleTableColorMap? rowColors,\n  SimpleTableColumnAlignMap? columnAligns,\n  SimpleTableRowAlignMap? rowAligns,\n  SimpleTableColumnWidthMap? columnWidths,\n  required List<Node> children,\n}) {\n  assert(children.every((e) => e.type == SimpleTableRowBlockKeys.type));\n\n  return Node(\n    type: SimpleTableBlockKeys.type,\n    attributes: {\n      SimpleTableBlockKeys.enableHeaderRow: enableHeaderRow,\n      SimpleTableBlockKeys.enableHeaderColumn: enableHeaderColumn,\n      SimpleTableBlockKeys.columnColors: columnColors,\n      SimpleTableBlockKeys.rowColors: rowColors,\n      SimpleTableBlockKeys.columnAligns: columnAligns,\n      SimpleTableBlockKeys.rowAligns: rowAligns,\n      SimpleTableBlockKeys.columnWidths: columnWidths,\n    },\n    children: children,\n  );\n}\n\n/// Create a simple table block node with the given column and row count.\n///\n/// The table will have cells filled with paragraph nodes.\n///\n/// For example, if you want to create a table with 2 columns and 3 rows, you can use:\n/// ```dart\n/// final table = createSimpleTableBlockNode(columnCount: 2, rowCount: 3);\n/// ```\n///\n/// | cell 1 | cell 2 |\n/// | cell 3 | cell 4 |\n/// | cell 5 | cell 6 |\nNode createSimpleTableBlockNode({\n  required int columnCount,\n  required int rowCount,\n  String? defaultContent,\n  String Function(int rowIndex, int columnIndex)? contentBuilder,\n}) {\n  final rows = List.generate(rowCount, (rowIndex) {\n    final cells = List.generate(\n      columnCount,\n      (columnIndex) => simpleTableCellBlockNode(\n        children: [\n          paragraphNode(\n            text: defaultContent ?? contentBuilder?.call(rowIndex, columnIndex),\n          ),\n        ],\n      ),\n    );\n    return simpleTableRowBlockNode(children: cells);\n  });\n\n  return simpleTableBlockNode(children: rows);\n}\n\nclass SimpleTableBlockComponentBuilder extends BlockComponentBuilder {\n  SimpleTableBlockComponentBuilder({\n    super.configuration,\n    this.alwaysDistributeColumnWidths = false,\n  });\n\n  final bool alwaysDistributeColumnWidths;\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    return SimpleTableBlockWidget(\n      key: node.key,\n      node: node,\n      configuration: configuration,\n      alwaysDistributeColumnWidths: alwaysDistributeColumnWidths,\n      showActions: showActions(node),\n      actionBuilder: (context, state) => actionBuilder(\n        blockComponentContext,\n        state,\n      ),\n      actionTrailingBuilder: (context, state) => actionTrailingBuilder(\n        blockComponentContext,\n        state,\n      ),\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate => (node) => node.children.isNotEmpty;\n}\n\nclass SimpleTableBlockWidget extends BlockComponentStatefulWidget {\n  const SimpleTableBlockWidget({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n    required this.alwaysDistributeColumnWidths,\n  });\n\n  final bool alwaysDistributeColumnWidths;\n\n  @override\n  State<SimpleTableBlockWidget> createState() => _SimpleTableBlockWidgetState();\n}\n\nclass _SimpleTableBlockWidgetState extends State<SimpleTableBlockWidget>\n    with\n        SelectableMixin,\n        BlockComponentConfigurable,\n        BlockComponentTextDirectionMixin,\n        BlockComponentBackgroundColorMixin {\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  @override\n  Node get node => widget.node;\n\n  @override\n  late EditorState editorState = context.read<EditorState>();\n\n  final tableKey = GlobalKey();\n\n  final simpleTableContext = SimpleTableContext();\n  final scrollController = ScrollController();\n\n  @override\n  void initState() {\n    super.initState();\n\n    editorState.selectionNotifier.addListener(_onSelectionChanged);\n  }\n\n  @override\n  void dispose() {\n    simpleTableContext.dispose();\n    editorState.selectionNotifier.removeListener(_onSelectionChanged);\n    scrollController.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child = SimpleTableWidget(\n      node: node,\n      simpleTableContext: simpleTableContext,\n      alwaysDistributeColumnWidths: widget.alwaysDistributeColumnWidths,\n    );\n\n    if (UniversalPlatform.isDesktop) {\n      child = Transform.translate(\n        offset: Offset(\n          -SimpleTableConstants.tableLeftPadding,\n          0,\n        ),\n        child: child,\n      );\n    }\n\n    child = Container(\n      alignment: Alignment.topLeft,\n      padding: padding,\n      child: child,\n    );\n\n    if (UniversalPlatform.isDesktop) {\n      child = Provider.value(\n        value: simpleTableContext,\n        child: MouseRegion(\n          onEnter: (event) =>\n              simpleTableContext.isHoveringOnTableBlock.value = true,\n          onExit: (event) {\n            simpleTableContext.isHoveringOnTableBlock.value = false;\n          },\n          child: child,\n        ),\n      );\n    }\n\n    if (widget.showActions && widget.actionBuilder != null) {\n      child = BlockComponentActionWrapper(\n        node: node,\n        actionBuilder: widget.actionBuilder!,\n        actionTrailingBuilder: widget.actionTrailingBuilder,\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  void _onSelectionChanged() {\n    final selection = editorState.selectionNotifier.value;\n    final selectionType = editorState.selectionType;\n    if (selectionType == SelectionType.block &&\n        widget.node.path.inSelection(selection)) {\n      simpleTableContext.isSelectingTable.value = true;\n    } else {\n      simpleTableContext.isSelectingTable.value = false;\n    }\n  }\n\n  RenderBox get _renderBox => context.findRenderObject() as RenderBox;\n\n  @override\n  Position start() => Position(path: widget.node.path);\n\n  @override\n  Position end() => Position(path: widget.node.path, offset: 1);\n\n  @override\n  Position getPositionInOffset(Offset start) => end();\n\n  @override\n  List<Rect> getRectsInSelection(\n    Selection selection, {\n    bool shiftWithBaseOffset = false,\n  }) {\n    final parentBox = context.findRenderObject();\n    final tableBox = tableKey.currentContext?.findRenderObject();\n    if (parentBox is RenderBox && tableBox is RenderBox) {\n      return [\n        (shiftWithBaseOffset\n                ? tableBox.localToGlobal(Offset.zero, ancestor: parentBox)\n                : Offset.zero) &\n            tableBox.size,\n      ];\n    }\n    return [Offset.zero & _renderBox.size];\n  }\n\n  @override\n  Selection getSelectionInRange(Offset start, Offset end) => Selection.single(\n        path: widget.node.path,\n        startOffset: 0,\n        endOffset: 1,\n      );\n\n  @override\n  bool get shouldCursorBlink => false;\n\n  @override\n  CursorStyle get cursorStyle => CursorStyle.cover;\n\n  @override\n  Offset localToGlobal(\n    Offset offset, {\n    bool shiftWithBaseOffset = false,\n  }) =>\n      _renderBox.localToGlobal(offset);\n\n  @override\n  Rect getBlockRect({\n    bool shiftWithBaseOffset = false,\n  }) {\n    return getRectsInSelection(Selection.invalid()).first;\n  }\n\n  @override\n  Rect? getCursorRectInPosition(\n    Position position, {\n    bool shiftWithBaseOffset = false,\n  }) {\n    final size = _renderBox.size;\n    return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass SimpleTableCellBlockKeys {\n  const SimpleTableCellBlockKeys._();\n\n  static const String type = 'simple_table_cell';\n}\n\nNode simpleTableCellBlockNode({\n  List<Node>? children,\n}) {\n  // Default children is a paragraph node.\n  children ??= [\n    paragraphNode(),\n  ];\n\n  return Node(\n    type: SimpleTableCellBlockKeys.type,\n    children: children,\n  );\n}\n\nclass SimpleTableCellBlockComponentBuilder extends BlockComponentBuilder {\n  SimpleTableCellBlockComponentBuilder({\n    super.configuration,\n    this.alwaysDistributeColumnWidths = false,\n  });\n\n  final bool alwaysDistributeColumnWidths;\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    return SimpleTableCellBlockWidget(\n      key: node.key,\n      node: node,\n      configuration: configuration,\n      alwaysDistributeColumnWidths: alwaysDistributeColumnWidths,\n      showActions: showActions(node),\n      actionBuilder: (context, state) => actionBuilder(\n        blockComponentContext,\n        state,\n      ),\n      actionTrailingBuilder: (context, state) => actionTrailingBuilder(\n        blockComponentContext,\n        state,\n      ),\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate => (node) => true;\n}\n\nclass SimpleTableCellBlockWidget extends BlockComponentStatefulWidget {\n  const SimpleTableCellBlockWidget({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n    required this.alwaysDistributeColumnWidths,\n  });\n\n  final bool alwaysDistributeColumnWidths;\n\n  @override\n  State<SimpleTableCellBlockWidget> createState() =>\n      SimpleTableCellBlockWidgetState();\n}\n\n@visibleForTesting\nclass SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>\n    with\n        BlockComponentConfigurable,\n        BlockComponentTextDirectionMixin,\n        BlockComponentBackgroundColorMixin {\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  @override\n  Node get node => widget.node;\n\n  @override\n  late EditorState editorState = context.read<EditorState>();\n\n  late SimpleTableContext? simpleTableContext =\n      context.read<SimpleTableContext?>();\n  late final borderBuilder = SimpleTableBorderBuilder(\n    context: context,\n    simpleTableContext: simpleTableContext!,\n    node: node,\n  );\n\n  /// Notify if the cell is editing.\n  ValueNotifier<bool> isEditingCellNotifier = ValueNotifier(false);\n\n  /// Notify if the cell is hit by the reordering offset.\n  ///\n  /// This value is only available on mobile.\n  ValueNotifier<bool> isReorderingHitCellNotifier = ValueNotifier(false);\n\n  @override\n  void initState() {\n    super.initState();\n\n    simpleTableContext?.isSelectingTable.addListener(_onSelectingTableChanged);\n    simpleTableContext?.reorderingOffset\n        .addListener(_onReorderingOffsetChanged);\n    node.parentTableNode?.addListener(_onSelectingTableChanged);\n    editorState.selectionNotifier.addListener(_onSelectionChanged);\n\n    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n      _onSelectionChanged();\n    });\n  }\n\n  @override\n  void dispose() {\n    simpleTableContext?.isSelectingTable.removeListener(\n      _onSelectingTableChanged,\n    );\n    simpleTableContext?.reorderingOffset.removeListener(\n      _onReorderingOffsetChanged,\n    );\n    node.parentTableNode?.removeListener(_onSelectingTableChanged);\n    editorState.selectionNotifier.removeListener(_onSelectionChanged);\n    isEditingCellNotifier.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (simpleTableContext == null) {\n      return const SizedBox.shrink();\n    }\n\n    Widget child = Stack(\n      clipBehavior: Clip.none,\n      children: [\n        _buildCell(),\n        if (editorState.editable) ...[\n          if (node.columnIndex == 0)\n            Positioned(\n              // if the cell is in the first row, add padding to the top of the cell\n              // to make the row action button clickable.\n              top: node.rowIndex == 0\n                  ? SimpleTableConstants.tableHitTestTopPadding\n                  : 0,\n              bottom: 0,\n              left: -SimpleTableConstants.tableLeftPadding,\n              child: _buildRowMoreActionButton(),\n            ),\n          if (node.rowIndex == 0)\n            Positioned(\n              left: node.columnIndex == 0\n                  ? SimpleTableConstants.tableHitTestLeftPadding\n                  : 0,\n              right: 0,\n              child: _buildColumnMoreActionButton(),\n            ),\n          if (node.columnIndex == 0 && node.rowIndex == 0)\n            Positioned(\n              left: 2,\n              top: 2,\n              child: _buildTableActionMenu(),\n            ),\n          Positioned(\n            right: 0,\n            top: node.rowIndex == 0\n                ? SimpleTableConstants.tableHitTestTopPadding\n                : 0,\n            bottom: 0,\n            child: SimpleTableColumnResizeHandle(\n              node: node,\n            ),\n          ),\n        ],\n      ],\n    );\n\n    if (UniversalPlatform.isDesktop) {\n      child = MouseRegion(\n        hitTestBehavior: HitTestBehavior.opaque,\n        onEnter: (event) => simpleTableContext!.hoveringTableCell.value = node,\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  Widget _buildCell() {\n    if (simpleTableContext == null) {\n      return const SizedBox.shrink();\n    }\n\n    return UniversalPlatform.isDesktop\n        ? _buildDesktopCell()\n        : _buildMobileCell();\n  }\n\n  Widget _buildDesktopCell() {\n    return Padding(\n      // add padding to the top of the cell if it is the first row, otherwise the\n      //  column action button is not clickable.\n      // issue: https://github.com/flutter/flutter/issues/75747\n      padding: EdgeInsets.only(\n        top: node.rowIndex == 0\n            ? SimpleTableConstants.tableHitTestTopPadding\n            : 0,\n        left: node.columnIndex == 0\n            ? SimpleTableConstants.tableHitTestLeftPadding\n            : 0,\n      ),\n      // TODO(Lucas): find a better way to handle the multiple value listenable builder\n      // There's flutter pub can do that.\n      child: ValueListenableBuilder<bool>(\n        valueListenable: isEditingCellNotifier,\n        builder: (context, isEditingCell, child) {\n          return ValueListenableBuilder(\n            valueListenable: simpleTableContext!.selectingColumn,\n            builder: (context, selectingColumn, _) {\n              return ValueListenableBuilder(\n                valueListenable: simpleTableContext!.selectingRow,\n                builder: (context, selectingRow, _) {\n                  return ValueListenableBuilder(\n                    valueListenable: simpleTableContext!.hoveringTableCell,\n                    builder: (context, hoveringTableCell, _) {\n                      return DecoratedBox(\n                        decoration: _buildDecoration(),\n                        child: child!,\n                      );\n                    },\n                  );\n                },\n              );\n            },\n          );\n        },\n        child: Container(\n          padding: SimpleTableConstants.cellEdgePadding,\n          constraints: const BoxConstraints(\n            minWidth: SimpleTableConstants.minimumColumnWidth,\n          ),\n          width: widget.alwaysDistributeColumnWidths ? null : node.columnWidth,\n          child: node.children.isEmpty\n              ? Column(\n                  children: [\n                    // expand the cell to make the empty cell content clickable\n                    Expanded(\n                      child: _buildEmptyCellContent(),\n                    ),\n                  ],\n                )\n              : Column(\n                  children: [\n                    ...node.children.map(_buildCellContent),\n                    _buildEmptyCellContent(height: 12),\n                  ],\n                ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildMobileCell() {\n    return Padding(\n      padding: EdgeInsets.only(\n        top: node.rowIndex == 0\n            ? SimpleTableConstants.tableHitTestTopPadding\n            : 0,\n        left: node.columnIndex == 0\n            ? SimpleTableConstants.tableHitTestLeftPadding\n            : 0,\n      ),\n      child: ValueListenableBuilder<bool>(\n        valueListenable: isEditingCellNotifier,\n        builder: (context, isEditingCell, child) {\n          return ValueListenableBuilder(\n            valueListenable: simpleTableContext!.selectingColumn,\n            builder: (context, selectingColumn, _) {\n              return ValueListenableBuilder(\n                valueListenable: simpleTableContext!.selectingRow,\n                builder: (context, selectingRow, _) {\n                  return ValueListenableBuilder(\n                    valueListenable: isReorderingHitCellNotifier,\n                    builder: (context, isReorderingHitCellNotifier, _) {\n                      final previousCell = node.getPreviousCellInSameRow();\n                      return Stack(\n                        children: [\n                          DecoratedBox(\n                            decoration: _buildDecoration(),\n                            child: child!,\n                          ),\n                          Positioned(\n                            right: 0,\n                            top: 0,\n                            bottom: 0,\n                            child: SimpleTableColumnResizeHandle(\n                              node: node,\n                            ),\n                          ),\n                          if (node.columnIndex != 0 && previousCell != null)\n                            Positioned(\n                              left: 0,\n                              top: 0,\n                              bottom: 0,\n                              // pass the previous node to the resize handle\n                              // to make the resize handle work correctly\n                              child: SimpleTableColumnResizeHandle(\n                                node: previousCell,\n                                isPreviousCell: true,\n                              ),\n                            ),\n                        ],\n                      );\n                    },\n                  );\n                },\n              );\n            },\n          );\n        },\n        child: Container(\n          padding: SimpleTableConstants.cellEdgePadding,\n          constraints: const BoxConstraints(\n            minWidth: SimpleTableConstants.minimumColumnWidth,\n          ),\n          width: node.columnWidth,\n          child: node.children.isEmpty\n              ? _buildEmptyCellContent()\n              : Column(\n                  children: node.children.map(_buildCellContent).toList(),\n                ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildCellContent(Node childNode) {\n    final alignment = _buildAlignment();\n\n    Widget child = IntrinsicWidth(\n      child: editorState.renderer.build(context, childNode),\n    );\n\n    final notSupportAlignmentBlocks = [\n      DividerBlockKeys.type,\n      CalloutBlockKeys.type,\n      MathEquationBlockKeys.type,\n      CodeBlockKeys.type,\n      SubPageBlockKeys.type,\n      FileBlockKeys.type,\n      CustomImageBlockKeys.type,\n    ];\n    if (notSupportAlignmentBlocks.contains(childNode.type)) {\n      child = SizedBox(\n        width: double.infinity,\n        child: child,\n      );\n    } else {\n      child = Align(\n        alignment: alignment,\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  Widget _buildEmptyCellContent({\n    double? height,\n  }) {\n    // if the table cell is empty, we should allow the user to tap on it to create a new paragraph.\n    final lastChild = node.children.lastOrNull;\n    if (lastChild != null && lastChild.delta?.isEmpty != null) {\n      return const SizedBox.shrink();\n    }\n\n    Widget child = GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: () {\n        final transaction = editorState.transaction;\n        final length = node.children.length;\n        final path = node.path.child(length);\n        transaction\n          ..insertNode(\n            path,\n            paragraphNode(),\n          )\n          ..afterSelection = Selection.collapsed(Position(path: path));\n        editorState.apply(transaction);\n      },\n    );\n\n    if (height != null) {\n      child = SizedBox(\n        height: height,\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  Widget _buildRowMoreActionButton() {\n    final rowIndex = node.rowIndex;\n\n    return SimpleTableMoreActionMenu(\n      tableCellNode: node,\n      index: rowIndex,\n      type: SimpleTableMoreActionType.row,\n    );\n  }\n\n  Widget _buildColumnMoreActionButton() {\n    final columnIndex = node.columnIndex;\n\n    return SimpleTableMoreActionMenu(\n      tableCellNode: node,\n      index: columnIndex,\n      type: SimpleTableMoreActionType.column,\n    );\n  }\n\n  Widget _buildTableActionMenu() {\n    final tableNode = node.parentTableNode;\n\n    // the table action menu is only available on mobile platform.\n    if (tableNode == null || UniversalPlatform.isDesktop) {\n      return const SizedBox.shrink();\n    }\n\n    return SimpleTableActionMenu(\n      tableNode: tableNode,\n      editorState: editorState,\n    );\n  }\n\n  Alignment _buildAlignment() {\n    Alignment alignment = Alignment.topLeft;\n    if (node.columnAlign != TableAlign.left) {\n      alignment = node.columnAlign.alignment;\n    } else if (node.rowAlign != TableAlign.left) {\n      alignment = node.rowAlign.alignment;\n    }\n    return alignment;\n  }\n\n  Decoration _buildDecoration() {\n    final backgroundColor = _buildBackgroundColor();\n    final border = borderBuilder.buildBorder(\n      isEditingCell: isEditingCellNotifier.value,\n    );\n\n    return BoxDecoration(\n      border: border,\n      color: backgroundColor,\n    );\n  }\n\n  Color? _buildBackgroundColor() {\n    // Priority: highlight color > column color > row color > header color > default color\n    final isSelectingTable =\n        simpleTableContext?.isSelectingTable.value ?? false;\n    if (isSelectingTable) {\n      return Theme.of(context).colorScheme.primary.withValues(alpha: 0.1);\n    }\n\n    final columnColor = node.buildColumnColor(context);\n    if (columnColor != null && columnColor != Colors.transparent) {\n      return columnColor;\n    }\n\n    final rowColor = node.buildRowColor(context);\n    if (rowColor != null && rowColor != Colors.transparent) {\n      return rowColor;\n    }\n\n    // Check if the cell is in the header.\n    // If the cell is in the header, set the background color to the default header color.\n    // Otherwise, set the background color to null.\n    if (_isInHeader()) {\n      return context.simpleTableDefaultHeaderColor;\n    }\n\n    return Colors.transparent;\n  }\n\n  bool _isInHeader() {\n    final isHeaderColumnEnabled = node.isHeaderColumnEnabled;\n    final isHeaderRowEnabled = node.isHeaderRowEnabled;\n    final cellPosition = node.cellPosition;\n    final isFirstColumn = cellPosition.$1 == 0;\n    final isFirstRow = cellPosition.$2 == 0;\n\n    return isHeaderColumnEnabled && isFirstRow ||\n        isHeaderRowEnabled && isFirstColumn;\n  }\n\n  void _onSelectingTableChanged() {\n    if (mounted) {\n      setState(() {});\n    }\n  }\n\n  void _onSelectionChanged() {\n    final selection = editorState.selection;\n\n    // check if the selection is in the cell\n    if (selection != null &&\n        node.path.isAncestorOf(selection.start.path) &&\n        node.path.isAncestorOf(selection.end.path)) {\n      isEditingCellNotifier.value = true;\n      simpleTableContext?.isEditingCell.value = node;\n    } else {\n      isEditingCellNotifier.value = false;\n    }\n\n    // if the selection is null or the selection is collapsed, set the isEditingCell to null.\n    if (selection == null) {\n      simpleTableContext?.isEditingCell.value = null;\n    } else if (selection.isCollapsed) {\n      // if the selection is collapsed, check if the selection is in the cell.\n      final selectedNode =\n          editorState.getNodesInSelection(selection).firstOrNull;\n      if (selectedNode != null) {\n        final tableNode = selectedNode.parentTableNode;\n        if (tableNode == null || tableNode.id != node.parentTableNode?.id) {\n          simpleTableContext?.isEditingCell.value = null;\n        }\n      } else {\n        simpleTableContext?.isEditingCell.value = null;\n      }\n    }\n  }\n\n  /// Calculate if the cell is hit by the reordering offset.\n  /// If the cell is hit, set the isReorderingCell to true.\n  void _onReorderingOffsetChanged() {\n    final simpleTableContext = this.simpleTableContext;\n    if (UniversalPlatform.isDesktop || simpleTableContext == null) {\n      return;\n    }\n\n    final isReordering = simpleTableContext.isReordering;\n    if (!isReordering) {\n      return;\n    }\n\n    final isReorderingColumn = simpleTableContext.isReorderingColumn.value.$1;\n    final isReorderingRow = simpleTableContext.isReorderingRow.value.$1;\n    if (!isReorderingColumn && !isReorderingRow) {\n      return;\n    }\n\n    final reorderingOffset = simpleTableContext.reorderingOffset.value;\n\n    final renderBox = node.renderBox;\n    if (renderBox == null) {\n      return;\n    }\n\n    final cellRect = renderBox.localToGlobal(Offset.zero) & renderBox.size;\n\n    bool isHitCurrentCell = false;\n    if (isReorderingColumn) {\n      isHitCurrentCell = cellRect.left < reorderingOffset.dx &&\n          cellRect.right > reorderingOffset.dx;\n    } else if (isReorderingRow) {\n      isHitCurrentCell = cellRect.top < reorderingOffset.dy &&\n          cellRect.bottom > reorderingOffset.dy;\n    }\n\n    isReorderingHitCellNotifier.value = isHitCurrentCell;\n    if (isHitCurrentCell) {\n      if (isReorderingColumn) {\n        if (simpleTableContext.isReorderingHitIndex.value != node.columnIndex) {\n          HapticFeedback.lightImpact();\n\n          simpleTableContext.isReorderingHitIndex.value = node.columnIndex;\n        }\n      } else if (isReorderingRow) {\n        if (simpleTableContext.isReorderingHitIndex.value != node.rowIndex) {\n          HapticFeedback.lightImpact();\n\n          simpleTableContext.isReorderingHitIndex.value = node.rowIndex;\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nconst _enableTableDebugLog = false;\n\nclass SimpleTableContext {\n  SimpleTableContext() {\n    if (_enableTableDebugLog) {\n      isHoveringOnColumnsAndRows.addListener(\n        _onHoveringOnColumnsAndRowsChanged,\n      );\n      isHoveringOnTableArea.addListener(\n        _onHoveringOnTableAreaChanged,\n      );\n      hoveringTableCell.addListener(_onHoveringTableNodeChanged);\n      selectingColumn.addListener(_onSelectingColumnChanged);\n      selectingRow.addListener(_onSelectingRowChanged);\n      isSelectingTable.addListener(_onSelectingTableChanged);\n      isHoveringOnTableBlock.addListener(_onHoveringOnTableBlockChanged);\n      isReorderingColumn.addListener(_onDraggingColumnChanged);\n      isReorderingRow.addListener(_onDraggingRowChanged);\n    }\n  }\n\n  /// the area only contains the columns and rows,\n  ///  the add row button, add column button, and add column and row button are not part of the table area\n  final ValueNotifier<bool> isHoveringOnColumnsAndRows = ValueNotifier(false);\n\n  /// the table area contains the columns and rows,\n  ///  the add row button, add column button, and add column and row button are not part of the table area,\n  ///  not including the selection area and padding\n  final ValueNotifier<bool> isHoveringOnTableArea = ValueNotifier(false);\n\n  /// the table block area contains the table area and the add row button, add column button, and add column and row button\n  ///  also, the table block area contains the selection area and padding\n  final ValueNotifier<bool> isHoveringOnTableBlock = ValueNotifier(false);\n\n  /// the hovering table cell is the cell that the mouse is hovering on\n  final ValueNotifier<Node?> hoveringTableCell = ValueNotifier(null);\n\n  /// the hovering on resize handle is the resize handle that the mouse is hovering on\n  final ValueNotifier<Node?> hoveringOnResizeHandle = ValueNotifier(null);\n\n  /// the selecting column is the column that the user is selecting\n  final ValueNotifier<int?> selectingColumn = ValueNotifier(null);\n\n  /// the selecting row is the row that the user is selecting\n  final ValueNotifier<int?> selectingRow = ValueNotifier(null);\n\n  /// the is selecting table is the table that the user is selecting\n  final ValueNotifier<bool> isSelectingTable = ValueNotifier(false);\n\n  /// isReorderingColumn is a tuple of (isReordering, columnIndex)\n  final ValueNotifier<(bool, int)> isReorderingColumn =\n      ValueNotifier((false, -1));\n\n  /// isReorderingRow is a tuple of (isReordering, rowIndex)\n  final ValueNotifier<(bool, int)> isReorderingRow = ValueNotifier((false, -1));\n\n  /// reorderingOffset is the offset of the reordering\n  //\n  /// This value is only available when isReordering is true\n  final ValueNotifier<Offset> reorderingOffset = ValueNotifier(Offset.zero);\n\n  /// isDraggingRow to expand the rows of the table\n  bool isDraggingRow = false;\n\n  /// isDraggingColumn to expand the columns of the table\n  bool isDraggingColumn = false;\n\n  bool get isReordering =>\n      isReorderingColumn.value.$1 || isReorderingRow.value.$1;\n\n  /// isEditingCell is the cell that the user is editing\n  ///\n  /// This value is available on mobile only\n  final ValueNotifier<Node?> isEditingCell = ValueNotifier(null);\n\n  /// isReorderingHitCell is the cell that the user is reordering\n  ///\n  /// This value is available on mobile only\n  final ValueNotifier<int?> isReorderingHitIndex = ValueNotifier(null);\n\n  /// resizingCell is the cell that the user is resizing\n  ///\n  /// This value is available on mobile only\n  final ValueNotifier<Node?> resizingCell = ValueNotifier(null);\n\n  /// Scroll controller for the table\n  ScrollController? horizontalScrollController;\n\n  void _onHoveringOnColumnsAndRowsChanged() {\n    if (!_enableTableDebugLog) {\n      return;\n    }\n\n    Log.debug('isHoveringOnTable: ${isHoveringOnColumnsAndRows.value}');\n  }\n\n  void _onHoveringTableNodeChanged() {\n    if (!_enableTableDebugLog) {\n      return;\n    }\n\n    final node = hoveringTableCell.value;\n    if (node == null) {\n      return;\n    }\n\n    Log.debug('hoveringTableNode: $node, ${node.cellPosition}');\n  }\n\n  void _onSelectingColumnChanged() {\n    if (!_enableTableDebugLog) {\n      return;\n    }\n\n    Log.debug('selectingColumn: ${selectingColumn.value}');\n  }\n\n  void _onSelectingRowChanged() {\n    if (!_enableTableDebugLog) {\n      return;\n    }\n\n    Log.debug('selectingRow: ${selectingRow.value}');\n  }\n\n  void _onSelectingTableChanged() {\n    if (!_enableTableDebugLog) {\n      return;\n    }\n\n    Log.debug('isSelectingTable: ${isSelectingTable.value}');\n  }\n\n  void _onHoveringOnTableBlockChanged() {\n    if (!_enableTableDebugLog) {\n      return;\n    }\n\n    Log.debug('isHoveringOnTableBlock: ${isHoveringOnTableBlock.value}');\n  }\n\n  void _onHoveringOnTableAreaChanged() {\n    if (!_enableTableDebugLog) {\n      return;\n    }\n\n    Log.debug('isHoveringOnTableArea: ${isHoveringOnTableArea.value}');\n  }\n\n  void _onDraggingColumnChanged() {\n    if (!_enableTableDebugLog) {\n      return;\n    }\n\n    Log.debug('isDraggingColumn: ${isReorderingColumn.value}');\n  }\n\n  void _onDraggingRowChanged() {\n    if (!_enableTableDebugLog) {\n      return;\n    }\n\n    Log.debug('isDraggingRow: ${isReorderingRow.value}');\n  }\n\n  void dispose() {\n    isHoveringOnColumnsAndRows.dispose();\n    isHoveringOnTableBlock.dispose();\n    isHoveringOnTableArea.dispose();\n    hoveringTableCell.dispose();\n    hoveringOnResizeHandle.dispose();\n    selectingColumn.dispose();\n    selectingRow.dispose();\n    isSelectingTable.dispose();\n    isReorderingColumn.dispose();\n    isReorderingRow.dispose();\n    reorderingOffset.dispose();\n    isEditingCell.dispose();\n    isReorderingHitIndex.dispose();\n    resizingCell.dispose();\n  }\n}\n\nclass SimpleTableConstants {\n  /// Table\n  static const defaultColumnWidth = 160.0;\n  static const minimumColumnWidth = 36.0;\n\n  static const defaultRowHeight = 36.0;\n\n  static double get tableHitTestTopPadding =>\n      UniversalPlatform.isDesktop ? 8.0 : 24.0;\n  static double get tableHitTestLeftPadding =>\n      UniversalPlatform.isDesktop ? 0.0 : 24.0;\n  static double get tableLeftPadding => UniversalPlatform.isDesktop ? 8.0 : 0.0;\n\n  static const tableBottomPadding =\n      addRowButtonHeight + 3 * addRowButtonPadding;\n  static const tableRightPadding =\n      addColumnButtonWidth + 2 * SimpleTableConstants.addColumnButtonPadding;\n\n  static EdgeInsets get tablePadding => EdgeInsets.only(\n        // don't add padding to the top of the table, the first row will have padding\n        //  to make the column action button clickable.\n        bottom: tableBottomPadding,\n        left: tableLeftPadding,\n        right: tableRightPadding,\n      );\n\n  static double get tablePageOffset => UniversalPlatform.isMobile\n      ? EditorStyleCustomizer.optionMenuWidth +\n          EditorStyleCustomizer.nodeHorizontalPadding * 2\n      : EditorStyleCustomizer.optionMenuWidth + 12;\n\n  // Add row button\n  static const addRowButtonHeight = 16.0;\n  static const addRowButtonPadding = 4.0;\n  static const addRowButtonRadius = 4.0;\n  static const addRowButtonRightPadding =\n      addColumnButtonWidth + addColumnButtonPadding * 2;\n\n  // Add column button\n  static const addColumnButtonWidth = 16.0;\n  static const addColumnButtonPadding = 2.0;\n  static const addColumnButtonRadius = 4.0;\n  static const addColumnButtonBottomPadding =\n      addRowButtonHeight + 3 * addRowButtonPadding;\n\n  // Add column and row button\n  static const addColumnAndRowButtonWidth = addColumnButtonWidth;\n  static const addColumnAndRowButtonHeight = addRowButtonHeight;\n  static const addColumnAndRowButtonCornerRadius = addColumnButtonWidth / 2.0;\n  static const addColumnAndRowButtonBottomPadding = 2.5 * addRowButtonPadding;\n\n  // Table cell\n  static EdgeInsets get cellEdgePadding => UniversalPlatform.isDesktop\n      ? const EdgeInsets.symmetric(\n          horizontal: 9.0,\n          vertical: 2.0,\n        )\n      : const EdgeInsets.only(\n          left: 8.0,\n          right: 8.0,\n          bottom: 6.0,\n        );\n  static const cellBorderWidth = 1.0;\n  static const resizeHandleWidth = 3.0;\n\n  static const borderType = SimpleTableBorderRenderType.cell;\n\n  // Table more action\n  static const moreActionHeight = 34.0;\n  static const moreActionPadding = EdgeInsets.symmetric(vertical: 2.0);\n  static const moreActionHorizontalMargin =\n      EdgeInsets.symmetric(horizontal: 6.0);\n\n  /// Only displaying the add row / add column / add column and row button\n  ///   when hovering on the last row / last column / last cell.\n  static const enableHoveringLogicV2 = true;\n\n  /// Enable the drag to expand the table\n  static const enableDragToExpandTable = false;\n\n  /// Action sheet hit test area on Mobile\n  static const rowActionSheetHitTestAreaWidth = 24.0;\n  static const columnActionSheetHitTestAreaHeight = 24.0;\n\n  static const actionSheetQuickActionSectionHeight = 44.0;\n  static const actionSheetInsertSectionHeight = 52.0;\n  static const actionSheetContentSectionHeight = 44.0;\n  static const actionSheetNormalActionSectionHeight = 48.0;\n  static const actionSheetButtonRadius = 12.0;\n\n  static const actionSheetBottomSheetHeight = 320.0;\n}\n\nenum SimpleTableBorderRenderType {\n  cell,\n  table,\n}\n\nextension SimpleTableColors on BuildContext {\n  Color get simpleTableBorderColor => Theme.of(this).isLightMode\n      ? const Color(0xFFE4E5E5)\n      : const Color(0xFF3A3F49);\n\n  Color get simpleTableDividerColor => Theme.of(this).isLightMode\n      ? const Color(0x141F2329)\n      : const Color(0xFF23262B).withValues(alpha: 0.5);\n\n  Color get simpleTableMoreActionBackgroundColor => Theme.of(this).isLightMode\n      ? const Color(0xFFF2F3F5)\n      : const Color(0xFF2D3036);\n\n  Color get simpleTableMoreActionBorderColor => Theme.of(this).isLightMode\n      ? const Color(0xFFCFD3D9)\n      : const Color(0xFF44484E);\n\n  Color get simpleTableMoreActionHoverColor => Theme.of(this).isLightMode\n      ? const Color(0xFF00C8FF)\n      : const Color(0xFF00C8FF);\n\n  Color get simpleTableDefaultHeaderColor => Theme.of(this).isLightMode\n      ? const Color(0xFFF2F2F2)\n      : const Color(0x08FFFFFF);\n\n  Color get simpleTableActionButtonBackgroundColor => Theme.of(this).isLightMode\n      ? const Color(0xFFFFFFFF)\n      : const Color(0xFF2D3036);\n\n  Color get simpleTableInsertActionBackgroundColor => Theme.of(this).isLightMode\n      ? const Color(0xFFF2F2F7)\n      : const Color(0xFF2D3036);\n\n  Color? get simpleTableQuickActionBackgroundColor =>\n      Theme.of(this).isLightMode ? null : const Color(0xFFBBC3CD);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_more_action.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nenum SimpleTableMoreActionType {\n  column,\n  row;\n\n  List<SimpleTableMoreAction> buildDesktopActions({\n    required int index,\n    required int columnLength,\n    required int rowLength,\n  }) {\n    // there're two special cases:\n    // 1. if the table only contains one row or one column, remove the delete action\n    // 2. if the index is 0, add the enable header action\n    switch (this) {\n      case SimpleTableMoreActionType.row:\n        return [\n          SimpleTableMoreAction.insertAbove,\n          SimpleTableMoreAction.insertBelow,\n          SimpleTableMoreAction.divider,\n          if (index == 0) SimpleTableMoreAction.enableHeaderRow,\n          SimpleTableMoreAction.backgroundColor,\n          SimpleTableMoreAction.align,\n          SimpleTableMoreAction.divider,\n          SimpleTableMoreAction.setToPageWidth,\n          SimpleTableMoreAction.distributeColumnsEvenly,\n          SimpleTableMoreAction.divider,\n          SimpleTableMoreAction.duplicate,\n          SimpleTableMoreAction.clearContents,\n          if (rowLength > 1) SimpleTableMoreAction.delete,\n        ];\n      case SimpleTableMoreActionType.column:\n        return [\n          SimpleTableMoreAction.insertLeft,\n          SimpleTableMoreAction.insertRight,\n          SimpleTableMoreAction.divider,\n          if (index == 0) SimpleTableMoreAction.enableHeaderColumn,\n          SimpleTableMoreAction.backgroundColor,\n          SimpleTableMoreAction.align,\n          SimpleTableMoreAction.divider,\n          SimpleTableMoreAction.setToPageWidth,\n          SimpleTableMoreAction.distributeColumnsEvenly,\n          SimpleTableMoreAction.divider,\n          SimpleTableMoreAction.duplicate,\n          SimpleTableMoreAction.clearContents,\n          if (columnLength > 1) SimpleTableMoreAction.delete,\n        ];\n    }\n  }\n\n  List<List<SimpleTableMoreAction>> buildMobileActions({\n    required int index,\n    required int columnLength,\n    required int rowLength,\n  }) {\n    // the actions on mobile are not the same as the desktop ones\n    // the mobile actions are grouped into different sections\n    switch (this) {\n      case SimpleTableMoreActionType.row:\n        return [\n          if (index == 0) [SimpleTableMoreAction.enableHeaderRow],\n          [\n            SimpleTableMoreAction.setToPageWidth,\n            SimpleTableMoreAction.distributeColumnsEvenly,\n          ],\n          [\n            SimpleTableMoreAction.duplicateRow,\n            SimpleTableMoreAction.clearContents,\n          ],\n        ];\n      case SimpleTableMoreActionType.column:\n        return [\n          if (index == 0) [SimpleTableMoreAction.enableHeaderColumn],\n          [\n            SimpleTableMoreAction.setToPageWidth,\n            SimpleTableMoreAction.distributeColumnsEvenly,\n          ],\n          [\n            SimpleTableMoreAction.duplicateColumn,\n            SimpleTableMoreAction.clearContents,\n          ],\n        ];\n    }\n  }\n\n  FlowySvgData get reorderIconSvg {\n    switch (this) {\n      case SimpleTableMoreActionType.column:\n        return FlowySvgs.table_reorder_column_s;\n      case SimpleTableMoreActionType.row:\n        return FlowySvgs.table_reorder_row_s;\n    }\n  }\n\n  @override\n  String toString() {\n    return switch (this) {\n      SimpleTableMoreActionType.column => 'column',\n      SimpleTableMoreActionType.row => 'row',\n    };\n  }\n}\n\nenum SimpleTableMoreAction {\n  insertLeft,\n  insertRight,\n  insertAbove,\n  insertBelow,\n  duplicate,\n  clearContents,\n  delete,\n  align,\n  backgroundColor,\n  enableHeaderColumn,\n  enableHeaderRow,\n  setToPageWidth,\n  distributeColumnsEvenly,\n  divider,\n\n  // these actions are only available on mobile\n  duplicateRow,\n  duplicateColumn,\n  cut,\n  copy,\n  paste,\n  bold,\n  textColor,\n  textBackgroundColor,\n  duplicateTable,\n  copyLinkToBlock;\n\n  String get name {\n    return switch (this) {\n      SimpleTableMoreAction.align =>\n        LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),\n      SimpleTableMoreAction.backgroundColor =>\n        LocaleKeys.document_plugins_simpleTable_moreActions_color.tr(),\n      SimpleTableMoreAction.enableHeaderColumn =>\n        LocaleKeys.document_plugins_simpleTable_moreActions_headerColumn.tr(),\n      SimpleTableMoreAction.enableHeaderRow =>\n        LocaleKeys.document_plugins_simpleTable_moreActions_headerRow.tr(),\n      SimpleTableMoreAction.insertLeft =>\n        LocaleKeys.document_plugins_simpleTable_moreActions_insertLeft.tr(),\n      SimpleTableMoreAction.insertRight =>\n        LocaleKeys.document_plugins_simpleTable_moreActions_insertRight.tr(),\n      SimpleTableMoreAction.insertBelow =>\n        LocaleKeys.document_plugins_simpleTable_moreActions_insertBelow.tr(),\n      SimpleTableMoreAction.insertAbove =>\n        LocaleKeys.document_plugins_simpleTable_moreActions_insertAbove.tr(),\n      SimpleTableMoreAction.clearContents =>\n        LocaleKeys.document_plugins_simpleTable_moreActions_clearContents.tr(),\n      SimpleTableMoreAction.delete =>\n        LocaleKeys.document_plugins_simpleTable_moreActions_delete.tr(),\n      SimpleTableMoreAction.duplicate =>\n        LocaleKeys.document_plugins_simpleTable_moreActions_duplicate.tr(),\n      SimpleTableMoreAction.setToPageWidth =>\n        LocaleKeys.document_plugins_simpleTable_moreActions_setToPageWidth.tr(),\n      SimpleTableMoreAction.distributeColumnsEvenly => LocaleKeys\n          .document_plugins_simpleTable_moreActions_distributeColumnsWidth\n          .tr(),\n      SimpleTableMoreAction.duplicateRow =>\n        LocaleKeys.document_plugins_simpleTable_moreActions_duplicateRow.tr(),\n      SimpleTableMoreAction.duplicateColumn => LocaleKeys\n          .document_plugins_simpleTable_moreActions_duplicateColumn\n          .tr(),\n      SimpleTableMoreAction.duplicateTable =>\n        LocaleKeys.document_plugins_simpleTable_moreActions_duplicateTable.tr(),\n      SimpleTableMoreAction.copyLinkToBlock =>\n        LocaleKeys.document_plugins_optionAction_copyLinkToBlock.tr(),\n      SimpleTableMoreAction.bold ||\n      SimpleTableMoreAction.textColor ||\n      SimpleTableMoreAction.textBackgroundColor ||\n      SimpleTableMoreAction.cut ||\n      SimpleTableMoreAction.copy ||\n      SimpleTableMoreAction.paste =>\n        throw UnimplementedError(),\n      SimpleTableMoreAction.divider => throw UnimplementedError(),\n    };\n  }\n\n  FlowySvgData get leftIconSvg {\n    return switch (this) {\n      SimpleTableMoreAction.insertLeft => FlowySvgs.table_insert_left_s,\n      SimpleTableMoreAction.insertRight => FlowySvgs.table_insert_right_s,\n      SimpleTableMoreAction.insertAbove => FlowySvgs.table_insert_above_s,\n      SimpleTableMoreAction.insertBelow => FlowySvgs.table_insert_below_s,\n      SimpleTableMoreAction.duplicate => FlowySvgs.duplicate_s,\n      SimpleTableMoreAction.clearContents => FlowySvgs.table_clear_content_s,\n      SimpleTableMoreAction.delete => FlowySvgs.trash_s,\n      SimpleTableMoreAction.setToPageWidth =>\n        FlowySvgs.table_set_to_page_width_s,\n      SimpleTableMoreAction.distributeColumnsEvenly =>\n        FlowySvgs.table_distribute_columns_evenly_s,\n      SimpleTableMoreAction.enableHeaderColumn =>\n        FlowySvgs.table_header_column_s,\n      SimpleTableMoreAction.enableHeaderRow => FlowySvgs.table_header_row_s,\n      SimpleTableMoreAction.duplicateRow => FlowySvgs.m_table_duplicate_s,\n      SimpleTableMoreAction.duplicateColumn => FlowySvgs.m_table_duplicate_s,\n      SimpleTableMoreAction.cut => FlowySvgs.m_table_quick_action_cut_s,\n      SimpleTableMoreAction.copy => FlowySvgs.m_table_quick_action_copy_s,\n      SimpleTableMoreAction.paste => FlowySvgs.m_table_quick_action_paste_s,\n      SimpleTableMoreAction.bold => FlowySvgs.m_aa_bold_s,\n      SimpleTableMoreAction.duplicateTable => FlowySvgs.m_table_duplicate_s,\n      SimpleTableMoreAction.copyLinkToBlock => FlowySvgs.m_copy_link_s,\n      SimpleTableMoreAction.align => FlowySvgs.m_aa_align_left_s,\n      SimpleTableMoreAction.textColor =>\n        throw UnsupportedError('text color icon is not supported'),\n      SimpleTableMoreAction.textBackgroundColor =>\n        throw UnsupportedError('text background color icon is not supported'),\n      SimpleTableMoreAction.divider =>\n        throw UnsupportedError('divider icon is not supported'),\n      SimpleTableMoreAction.backgroundColor =>\n        throw UnsupportedError('background color icon is not supported'),\n    };\n  }\n}\n\nclass SimpleTableMoreActionMenu extends StatefulWidget {\n  const SimpleTableMoreActionMenu({\n    super.key,\n    required this.index,\n    required this.type,\n    required this.tableCellNode,\n  });\n\n  final int index;\n  final SimpleTableMoreActionType type;\n  final Node tableCellNode;\n\n  @override\n  State<SimpleTableMoreActionMenu> createState() =>\n      _SimpleTableMoreActionMenuState();\n}\n\nclass _SimpleTableMoreActionMenuState extends State<SimpleTableMoreActionMenu> {\n  ValueNotifier<bool> isShowingMenu = ValueNotifier(false);\n  ValueNotifier<bool> isEditingCellNotifier = ValueNotifier(false);\n\n  late final editorState = context.read<EditorState>();\n  late final simpleTableContext = context.read<SimpleTableContext>();\n\n  @override\n  void initState() {\n    super.initState();\n\n    editorState.selectionNotifier.addListener(_onSelectionChanged);\n  }\n\n  @override\n  void dispose() {\n    isShowingMenu.dispose();\n    isEditingCellNotifier.dispose();\n\n    editorState.selectionNotifier.removeListener(_onSelectionChanged);\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Align(\n      alignment: widget.type == SimpleTableMoreActionType.row\n          ? UniversalPlatform.isDesktop\n              ? Alignment.centerLeft\n              : Alignment.centerRight\n          : Alignment.topCenter,\n      child: UniversalPlatform.isDesktop\n          ? _buildDesktopMenu()\n          : _buildMobileMenu(),\n    );\n  }\n\n  // On desktop, the menu is a popup and only shows when hovering.\n  Widget _buildDesktopMenu() {\n    return ValueListenableBuilder<bool>(\n      valueListenable: isShowingMenu,\n      builder: (context, isShowingMenu, child) {\n        return ValueListenableBuilder(\n          valueListenable: simpleTableContext.hoveringTableCell,\n          builder: (context, hoveringTableNode, child) {\n            final reorderingIndex = switch (widget.type) {\n              SimpleTableMoreActionType.column =>\n                simpleTableContext.isReorderingColumn.value.$2,\n              SimpleTableMoreActionType.row =>\n                simpleTableContext.isReorderingRow.value.$2,\n            };\n            final isReordering = simpleTableContext.isReordering;\n            if (isReordering) {\n              // when reordering, hide the menu for another column or row that is not the current dragging one.\n              if (reorderingIndex != widget.index) {\n                return const SizedBox.shrink();\n              } else {\n                return child!;\n              }\n            }\n\n            final hoveringIndex =\n                widget.type == SimpleTableMoreActionType.column\n                    ? hoveringTableNode?.columnIndex\n                    : hoveringTableNode?.rowIndex;\n\n            if (hoveringIndex != widget.index && !isShowingMenu) {\n              return const SizedBox.shrink();\n            }\n\n            return child!;\n          },\n          child: SimpleTableMoreActionPopup(\n            index: widget.index,\n            isShowingMenu: this.isShowingMenu,\n            type: widget.type,\n          ),\n        );\n      },\n    );\n  }\n\n  // On mobile, the menu is a action sheet and always shows.\n  Widget _buildMobileMenu() {\n    return ValueListenableBuilder(\n      valueListenable: isShowingMenu,\n      builder: (context, isShowingMenu, child) {\n        return ValueListenableBuilder(\n          valueListenable: simpleTableContext.isEditingCell,\n          builder: (context, isEditingCell, child) {\n            if (isShowingMenu) {\n              return child!;\n            }\n\n            if (isEditingCell == null) {\n              return const SizedBox.shrink();\n            }\n\n            final columnIndex = isEditingCell.columnIndex;\n            final rowIndex = isEditingCell.rowIndex;\n\n            switch (widget.type) {\n              case SimpleTableMoreActionType.column:\n                if (columnIndex != widget.index) {\n                  return const SizedBox.shrink();\n                }\n              case SimpleTableMoreActionType.row:\n                if (rowIndex != widget.index) {\n                  return const SizedBox.shrink();\n                }\n            }\n\n            return child!;\n          },\n          child: SimpleTableMobileDraggableReorderButton(\n            index: widget.index,\n            type: widget.type,\n            cellNode: widget.tableCellNode,\n            isShowingMenu: this.isShowingMenu,\n            editorState: editorState,\n            simpleTableContext: simpleTableContext,\n          ),\n        );\n      },\n    );\n  }\n\n  void _onSelectionChanged() {\n    final selection = editorState.selection;\n\n    // check if the selection is in the cell\n    if (selection != null &&\n        widget.tableCellNode.path.isAncestorOf(selection.start.path) &&\n        widget.tableCellNode.path.isAncestorOf(selection.end.path)) {\n      isEditingCellNotifier.value = true;\n    } else {\n      isEditingCellNotifier.value = false;\n    }\n  }\n}\n\n/// This widget is only used on mobile\nclass SimpleTableActionMenu extends StatelessWidget {\n  const SimpleTableActionMenu({\n    super.key,\n    required this.tableNode,\n    required this.editorState,\n  });\n\n  final Node tableNode;\n  final EditorState editorState;\n\n  @override\n  Widget build(BuildContext context) {\n    final simpleTableContext = context.read<SimpleTableContext>();\n    return ValueListenableBuilder<Node?>(\n      valueListenable: simpleTableContext.isEditingCell,\n      builder: (context, isEditingCell, child) {\n        if (isEditingCell == null) {\n          return const SizedBox.shrink();\n        }\n\n        return GestureDetector(\n          behavior: HitTestBehavior.opaque,\n          onTap: () {\n            editorState.service.keyboardService?.closeKeyboard();\n            // delay the bottom sheet show to make sure the keyboard is closed\n            Future.delayed(Durations.short3, () {\n              if (context.mounted) {\n                _showTableActionBottomSheet(context);\n              }\n            });\n          },\n          child: Container(\n            width: 20,\n            height: 20,\n            alignment: Alignment.center,\n            child: const FlowySvg(\n              FlowySvgs.drag_element_s,\n              size: Size.square(18.0),\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Future<void> _showTableActionBottomSheet(BuildContext context) async {\n    // check if the table node is a simple table\n    assert(tableNode.type == SimpleTableBlockKeys.type);\n\n    if (tableNode.type != SimpleTableBlockKeys.type) {\n      Log.error('The table node is not a simple table');\n      return;\n    }\n\n    final beforeSelection = editorState.selection;\n\n    // increase the keep editor focus notifier to prevent the editor from losing focus\n    keepEditorFocusNotifier.increase();\n\n    unawaited(\n      editorState.updateSelectionWithReason(\n        Selection.collapsed(\n          Position(\n            path: tableNode.path,\n          ),\n        ),\n        customSelectionType: SelectionType.block,\n        extraInfo: {\n          selectionExtraInfoDisableMobileToolbarKey: true,\n          selectionExtraInfoDoNotAttachTextService: true,\n        },\n      ),\n    );\n\n    if (!context.mounted) {\n      return;\n    }\n\n    final simpleTableContext = context.read<SimpleTableContext>();\n\n    simpleTableContext.isSelectingTable.value = true;\n\n    // show the bottom sheet\n    await showMobileBottomSheet(\n      context,\n      showDragHandle: true,\n      showDivider: false,\n      useSafeArea: false,\n      enablePadding: false,\n      builder: (context) => Provider.value(\n        value: simpleTableContext,\n        child: SimpleTableBottomSheet(\n          tableNode: tableNode,\n          editorState: editorState,\n        ),\n      ),\n    );\n\n    simpleTableContext.isSelectingTable.value = false;\n    keepEditorFocusNotifier.decrease();\n\n    // remove the extra info\n    if (beforeSelection != null) {\n      await editorState.updateSelectionWithReason(\n        beforeSelection,\n        customSelectionType: SelectionType.inline,\n        reason: SelectionUpdateReason.uiEvent,\n        extraInfo: {},\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_content_operation.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nextension TableContentOperation on EditorState {\n  /// Clear the content of the column at the given index.\n  ///\n  /// Before:\n  /// Given column index: 0\n  /// Row 1: | 0 | 1 | ← The content of these cells will be cleared\n  /// Row 2: | 2 | 3 |\n  ///\n  /// Call this function with column index 0 will clear the first column of the table.\n  ///\n  /// After:\n  /// Row 1: |   |   |\n  /// Row 2: | 2 | 3 |\n  Future<void> clearContentAtRowIndex({\n    required Node tableNode,\n    required int rowIndex,\n  }) async {\n    assert(tableNode.type == SimpleTableBlockKeys.type);\n\n    if (tableNode.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    if (rowIndex < 0 || rowIndex >= tableNode.rowLength) {\n      Log.warn('clear content in row: index out of range: $rowIndex');\n      return;\n    }\n\n    Log.info('clear content in row: $rowIndex in table ${tableNode.id}');\n\n    final transaction = this.transaction;\n\n    final row = tableNode.children[rowIndex];\n    for (var i = 0; i < row.children.length; i++) {\n      final cell = row.children[i];\n      transaction.insertNode(cell.path.next, simpleTableCellBlockNode());\n      transaction.deleteNode(cell);\n    }\n    await apply(transaction);\n  }\n\n  /// Clear the content of the row at the given index.\n  ///\n  /// Before:\n  /// Given row index: 1\n  ///              ↓ The content of these cells will be cleared\n  /// Row 1: | 0 | 1 |\n  /// Row 2: | 2 | 3 |\n  ///\n  /// Call this function with row index 1 will clear the second row of the table.\n  ///\n  /// After:\n  /// Row 1: | 0 |   |\n  /// Row 2: | 2 |   |\n  Future<void> clearContentAtColumnIndex({\n    required Node tableNode,\n    required int columnIndex,\n  }) async {\n    assert(tableNode.type == SimpleTableBlockKeys.type);\n\n    if (tableNode.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    if (columnIndex < 0 || columnIndex >= tableNode.columnLength) {\n      Log.warn('clear content in column: index out of range: $columnIndex');\n      return;\n    }\n\n    Log.info('clear content in column: $columnIndex in table ${tableNode.id}');\n\n    final transaction = this.transaction;\n    for (var i = 0; i < tableNode.rowLength; i++) {\n      final row = tableNode.children[i];\n      final cell = columnIndex >= row.children.length\n          ? row.children.last\n          : row.children[columnIndex];\n      transaction.insertNode(cell.path.next, simpleTableCellBlockNode());\n      transaction.deleteNode(cell);\n    }\n    await apply(transaction);\n  }\n\n  /// Clear the content of the table.\n  Future<void> clearAllContent({\n    required Node tableNode,\n  }) async {\n    assert(tableNode.type == SimpleTableBlockKeys.type);\n\n    if (tableNode.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    for (var i = 0; i < tableNode.rowLength; i++) {\n      await clearContentAtRowIndex(tableNode: tableNode, rowIndex: i);\n    }\n  }\n\n  /// Copy the selected column to the clipboard.\n  ///\n  /// If the [clearContent] is true, the content of the column will be cleared after\n  /// copying.\n  Future<ClipboardServiceData?> copyColumn({\n    required Node tableNode,\n    required int columnIndex,\n    bool clearContent = false,\n  }) async {\n    assert(tableNode.type == SimpleTableBlockKeys.type);\n\n    if (tableNode.type != SimpleTableBlockKeys.type) {\n      return null;\n    }\n\n    if (columnIndex < 0 || columnIndex >= tableNode.columnLength) {\n      Log.warn('copy column: index out of range: $columnIndex');\n      return null;\n    }\n\n    // the plain text content of the column\n    final List<String> content = [];\n\n    // the cells of the column\n    final List<Node> cells = [];\n\n    for (var i = 0; i < tableNode.rowLength; i++) {\n      final row = tableNode.children[i];\n      final cell = columnIndex >= row.children.length\n          ? row.children.last\n          : row.children[columnIndex];\n      final startNode = cell.getFirstFocusableChild();\n      final endNode = cell.getLastFocusableChild();\n      if (startNode == null || endNode == null) {\n        continue;\n      }\n      final plainText = getTextInSelection(\n        Selection(\n          start: Position(path: startNode.path),\n          end: Position(\n            path: endNode.path,\n            offset: endNode.delta?.length ?? 0,\n          ),\n        ),\n      );\n      content.add(plainText.join('\\n'));\n      cells.add(cell.deepCopy());\n    }\n\n    final plainText = content.join('\\n');\n    final document = Document.blank()..insert([0], cells);\n\n    if (clearContent) {\n      await clearContentAtColumnIndex(\n        tableNode: tableNode,\n        columnIndex: columnIndex,\n      );\n    }\n\n    return ClipboardServiceData(\n      plainText: plainText,\n      tableJson: jsonEncode(document.toJson()),\n    );\n  }\n\n  /// Copy the selected row to the clipboard.\n  ///\n  /// If the [clearContent] is true, the content of the row will be cleared after\n  /// copying.\n  Future<ClipboardServiceData?> copyRow({\n    required Node tableNode,\n    required int rowIndex,\n    bool clearContent = false,\n  }) async {\n    assert(tableNode.type == SimpleTableBlockKeys.type);\n\n    if (tableNode.type != SimpleTableBlockKeys.type) {\n      return null;\n    }\n\n    if (rowIndex < 0 || rowIndex >= tableNode.rowLength) {\n      Log.warn('copy row: index out of range: $rowIndex');\n      return null;\n    }\n\n    // the plain text content of the row\n    final List<String> content = [];\n\n    // the cells of the row\n    final List<Node> cells = [];\n\n    final row = tableNode.children[rowIndex];\n    for (var i = 0; i < row.children.length; i++) {\n      final cell = row.children[i];\n      final startNode = cell.getFirstFocusableChild();\n      final endNode = cell.getLastFocusableChild();\n      if (startNode == null || endNode == null) {\n        continue;\n      }\n      final plainText = getTextInSelection(\n        Selection(\n          start: Position(path: startNode.path),\n          end: Position(\n            path: endNode.path,\n            offset: endNode.delta?.length ?? 0,\n          ),\n        ),\n      );\n      content.add(plainText.join('\\n'));\n      cells.add(cell.deepCopy());\n    }\n\n    final plainText = content.join('\\n');\n    final document = Document.blank()..insert([0], cells);\n\n    if (clearContent) {\n      await clearContentAtRowIndex(\n        tableNode: tableNode,\n        rowIndex: rowIndex,\n      );\n    }\n\n    return ClipboardServiceData(\n      plainText: plainText,\n      tableJson: jsonEncode(document.toJson()),\n    );\n  }\n\n  /// Copy the selected table to the clipboard.\n  Future<ClipboardServiceData?> copyTable({\n    required Node tableNode,\n    bool clearContent = false,\n  }) async {\n    assert(tableNode.type == SimpleTableBlockKeys.type);\n\n    if (tableNode.type != SimpleTableBlockKeys.type) {\n      return null;\n    }\n\n    // the plain text content of the table\n    final List<String> content = [];\n\n    // the cells of the table\n    final List<Node> cells = [];\n\n    for (var i = 0; i < tableNode.rowLength; i++) {\n      final row = tableNode.children[i];\n      for (var j = 0; j < row.children.length; j++) {\n        final cell = row.children[j];\n        final startNode = cell.getFirstFocusableChild();\n        final endNode = cell.getLastFocusableChild();\n        if (startNode == null || endNode == null) {\n          continue;\n        }\n        final plainText = getTextInSelection(\n          Selection(\n            start: Position(path: startNode.path),\n            end: Position(\n              path: endNode.path,\n              offset: endNode.delta?.length ?? 0,\n            ),\n          ),\n        );\n        content.add(plainText.join('\\n'));\n        cells.add(cell.deepCopy());\n      }\n    }\n\n    final plainText = content.join('\\n');\n    final document = Document.blank()..insert([0], cells);\n\n    if (clearContent) {\n      await clearAllContent(tableNode: tableNode);\n    }\n\n    return ClipboardServiceData(\n      plainText: plainText,\n      tableJson: jsonEncode(document.toJson()),\n    );\n  }\n\n  /// Paste the clipboard content to the table column.\n  Future<void> pasteColumn({\n    required Node tableNode,\n    required int columnIndex,\n  }) async {\n    assert(tableNode.type == SimpleTableBlockKeys.type);\n\n    if (tableNode.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    if (columnIndex < 0 || columnIndex >= tableNode.columnLength) {\n      Log.warn('paste column: index out of range: $columnIndex');\n      return;\n    }\n\n    final clipboardData = await getIt<ClipboardService>().getData();\n    final tableJson = clipboardData.tableJson;\n    if (tableJson == null) {\n      return;\n    }\n\n    try {\n      final document = Document.fromJson(jsonDecode(tableJson));\n      final cells = document.root.children;\n      final transaction = this.transaction;\n      for (var i = 0; i < tableNode.rowLength; i++) {\n        final nodes = i < cells.length ? cells[i].children : <Node>[];\n        final row = tableNode.children[i];\n        final cell = columnIndex >= row.children.length\n            ? row.children.last\n            : row.children[columnIndex];\n        if (nodes.isNotEmpty) {\n          transaction.insertNodes(\n            cell.path.child(0),\n            nodes,\n          );\n          transaction.deleteNodes(cell.children);\n        }\n      }\n      await apply(transaction);\n    } catch (e) {\n      Log.error('paste column: failed to paste: $e');\n    }\n  }\n\n  /// Paste the clipboard content to the table row.\n  Future<void> pasteRow({\n    required Node tableNode,\n    required int rowIndex,\n  }) async {\n    assert(tableNode.type == SimpleTableBlockKeys.type);\n\n    if (tableNode.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    if (rowIndex < 0 || rowIndex >= tableNode.rowLength) {\n      Log.warn('paste row: index out of range: $rowIndex');\n      return;\n    }\n\n    final clipboardData = await getIt<ClipboardService>().getData();\n    final tableJson = clipboardData.tableJson;\n    if (tableJson == null) {\n      return;\n    }\n\n    try {\n      final document = Document.fromJson(jsonDecode(tableJson));\n      final cells = document.root.children;\n      final transaction = this.transaction;\n      final row = tableNode.children[rowIndex];\n      for (var i = 0; i < row.children.length; i++) {\n        final nodes = i < cells.length ? cells[i].children : <Node>[];\n        final cell = row.children[i];\n        if (nodes.isNotEmpty) {\n          transaction.insertNodes(\n            cell.path.child(0),\n            nodes,\n          );\n          transaction.deleteNodes(cell.children);\n        }\n      }\n      await apply(transaction);\n    } catch (e) {\n      Log.error('paste row: failed to paste: $e');\n    }\n  }\n\n  /// Paste the clipboard content to the table.\n  Future<void> pasteTable({\n    required Node tableNode,\n  }) async {\n    assert(tableNode.type == SimpleTableBlockKeys.type);\n\n    if (tableNode.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    final clipboardData = await getIt<ClipboardService>().getData();\n    final tableJson = clipboardData.tableJson;\n    if (tableJson == null) {\n      return;\n    }\n\n    try {\n      final document = Document.fromJson(jsonDecode(tableJson));\n      final cells = document.root.children;\n      final transaction = this.transaction;\n      for (var i = 0; i < tableNode.rowLength; i++) {\n        final row = tableNode.children[i];\n        for (var j = 0; j < row.children.length; j++) {\n          final cell = row.children[j];\n          final node = i + j < cells.length ? cells[i + j] : null;\n          if (node != null && node.children.isNotEmpty) {\n            transaction.insertNodes(\n              cell.path.child(0),\n              node.children,\n            );\n            transaction.deleteNodes(cell.children);\n          }\n        }\n      }\n      await apply(transaction);\n    } catch (e) {\n      Log.error('paste row: failed to paste: $e');\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_delete_operation.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nextension TableDeletionOperations on EditorState {\n  /// Delete a row at the given index.\n  ///\n  /// Before:\n  /// Given index: 0\n  /// Row 1: |   |   |   | ← This row will be deleted\n  /// Row 2: |   |   |   |\n  ///\n  /// Call this function with index 0 will delete the first row of the table.\n  ///\n  /// After:\n  /// Row 1: |   |   |   |\n  Future<void> deleteRowInTable(\n    Node tableNode,\n    int rowIndex, {\n    bool inMemoryUpdate = false,\n  }) async {\n    assert(tableNode.type == SimpleTableBlockKeys.type);\n\n    if (tableNode.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    final rowLength = tableNode.rowLength;\n    if (rowIndex < 0 || rowIndex >= rowLength) {\n      Log.warn(\n        'delete row: index out of range: $rowIndex, row length: $rowLength',\n      );\n      return;\n    }\n\n    Log.info('delete row: $rowIndex in table ${tableNode.id}');\n\n    final attributes = tableNode.mapTableAttributes(\n      tableNode,\n      type: TableMapOperationType.deleteRow,\n      index: rowIndex,\n    );\n\n    final row = tableNode.children[rowIndex];\n    final transaction = this.transaction;\n    transaction.deleteNode(row);\n    if (attributes != null) {\n      transaction.updateNode(tableNode, attributes);\n    }\n    await apply(\n      transaction,\n      options: ApplyOptions(\n        inMemoryUpdate: inMemoryUpdate,\n      ),\n    );\n  }\n\n  /// Delete a column at the given index.\n  ///\n  /// Before:\n  /// Given index: 2\n  ///                  ↓ This column will be deleted\n  /// Row 1: | 0 | 1 | 2 |\n  /// Row 2: |   |   |   |\n  ///\n  /// Call this function with index 2 will delete the third column of the table.\n  ///\n  /// After:\n  /// Row 1: | 0 | 1 |\n  /// Row 2: |   |   |\n  Future<void> deleteColumnInTable(\n    Node tableNode,\n    int columnIndex, {\n    bool inMemoryUpdate = false,\n  }) async {\n    assert(tableNode.type == SimpleTableBlockKeys.type);\n\n    if (tableNode.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    final rowLength = tableNode.rowLength;\n    final columnLength = tableNode.columnLength;\n    if (columnIndex < 0 || columnIndex >= columnLength) {\n      Log.warn(\n        'delete column: index out of range: $columnIndex, column length: $columnLength',\n      );\n      return;\n    }\n\n    Log.info('delete column: $columnIndex in table ${tableNode.id}');\n\n    final attributes = tableNode.mapTableAttributes(\n      tableNode,\n      type: TableMapOperationType.deleteColumn,\n      index: columnIndex,\n    );\n\n    final transaction = this.transaction;\n    for (var i = 0; i < rowLength; i++) {\n      final row = tableNode.children[i];\n      transaction.deleteNode(row.children[columnIndex]);\n    }\n    if (attributes != null) {\n      transaction.updateNode(tableNode, attributes);\n    }\n    await apply(\n      transaction,\n      options: ApplyOptions(\n        inMemoryUpdate: inMemoryUpdate,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_duplicate_operation.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nextension TableDuplicationOperations on EditorState {\n  /// Duplicate a row at the given index.\n  ///\n  /// Before:\n  /// | 0 | 1 | 2 |\n  /// | 3 | 4 | 5 | ← This row will be duplicated\n  ///\n  /// Call this function with index 1 will duplicate the second row of the table.\n  ///\n  /// After:\n  /// | 0 | 1 | 2 |\n  /// | 3 | 4 | 5 |\n  /// | 3 | 4 | 5 | ← New row\n  Future<void> duplicateRowInTable(Node node, int index) async {\n    assert(node.type == SimpleTableBlockKeys.type);\n\n    if (node.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    final columnLength = node.columnLength;\n    final rowLength = node.rowLength;\n\n    if (index < 0 || index >= rowLength) {\n      Log.warn(\n        'duplicate row: index out of range: $index, row length: $rowLength',\n      );\n      return;\n    }\n\n    Log.info(\n      'duplicate row in table ${node.id} at index: $index, column length: $columnLength, row length: $rowLength',\n    );\n\n    final attributes = node.mapTableAttributes(\n      node,\n      type: TableMapOperationType.duplicateRow,\n      index: index,\n    );\n\n    final newRow = node.children[index].deepCopy();\n    final transaction = this.transaction;\n    final path = index >= columnLength\n        ? node.children.last.path.next\n        : node.children[index].path;\n    transaction.insertNode(path, newRow);\n    if (attributes != null) {\n      transaction.updateNode(node, attributes);\n    }\n    await apply(transaction);\n  }\n\n  Future<void> duplicateColumnInTable(Node node, int index) async {\n    assert(node.type == SimpleTableBlockKeys.type);\n\n    if (node.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    final columnLength = node.columnLength;\n    final rowLength = node.rowLength;\n\n    if (index < 0 || index >= columnLength) {\n      Log.warn(\n        'duplicate column: index out of range: $index, column length: $columnLength',\n      );\n      return;\n    }\n\n    Log.info(\n      'duplicate column in table ${node.id} at index: $index, column length: $columnLength, row length: $rowLength',\n    );\n\n    final attributes = node.mapTableAttributes(\n      node,\n      type: TableMapOperationType.duplicateColumn,\n      index: index,\n    );\n\n    final transaction = this.transaction;\n    for (var i = 0; i < rowLength; i++) {\n      final row = node.children[i];\n      final path = index >= rowLength\n          ? row.children.last.path.next\n          : row.children[index].path;\n      final newCell = row.children[index].deepCopy();\n      transaction.insertNode(\n        path,\n        newCell,\n      );\n    }\n    if (attributes != null) {\n      transaction.updateNode(node, attributes);\n    }\n    await apply(transaction);\n  }\n\n  /// Duplicate the table.\n  ///\n  /// This function will duplicate the table and insert it after the original table.\n  Future<void> duplicateTable({\n    required Node tableNode,\n  }) async {\n    assert(tableNode.type == SimpleTableBlockKeys.type);\n\n    if (tableNode.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    final transaction = this.transaction;\n    final newTable = tableNode.deepCopy();\n    transaction.insertNode(tableNode.path.next, newTable);\n    await apply(transaction);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_header_operation.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nextension TableHeaderOperation on EditorState {\n  /// Toggle the enable header column of the table.\n  Future<void> toggleEnableHeaderColumn({\n    required Node tableNode,\n    required bool enable,\n  }) async {\n    assert(tableNode.type == SimpleTableBlockKeys.type);\n\n    if (tableNode.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    Log.info(\n      'toggle enable header column: $enable in table ${tableNode.id}',\n    );\n\n    final columnColors = tableNode.columnColors;\n\n    final transaction = this.transaction;\n    transaction.updateNode(tableNode, {\n      SimpleTableBlockKeys.enableHeaderColumn: enable,\n      // remove the previous background color if the header column is enable again\n      if (enable)\n        SimpleTableBlockKeys.columnColors: columnColors\n          ..removeWhere((key, _) => key == '0'),\n    });\n    await apply(transaction);\n  }\n\n  /// Toggle the enable header row of the table.\n  Future<void> toggleEnableHeaderRow({\n    required Node tableNode,\n    required bool enable,\n  }) async {\n    assert(tableNode.type == SimpleTableBlockKeys.type);\n\n    if (tableNode.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    Log.info('toggle enable header row: $enable in table ${tableNode.id}');\n\n    final rowColors = tableNode.rowColors;\n\n    final transaction = this.transaction;\n    transaction.updateNode(tableNode, {\n      SimpleTableBlockKeys.enableHeaderRow: enable,\n      // remove the previous background color if the header row is enable again\n      if (enable)\n        SimpleTableBlockKeys.rowColors: rowColors\n          ..removeWhere((key, _) => key == '0'),\n    });\n    await apply(transaction);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_insert_operation.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nextension TableInsertionOperations on EditorState {\n  /// Add a row at the end of the table.\n  ///\n  /// Before:\n  /// Row 1: |   |   |   |\n  /// Row 2: |   |   |   |\n  ///\n  /// Call this function will add a row at the end of the table.\n  ///\n  /// After:\n  /// Row 1: |   |   |   |\n  /// Row 2: |   |   |   |\n  /// Row 3: |   |   |   | ← New row\n  ///\n  Future<void> addRowInTable(Node tableNode) async {\n    assert(tableNode.type == SimpleTableBlockKeys.type);\n\n    if (tableNode.type != SimpleTableBlockKeys.type) {\n      Log.warn('node is not a table node: ${tableNode.type}');\n      return;\n    }\n\n    await insertRowInTable(tableNode, tableNode.rowLength);\n  }\n\n  /// Add a column at the end of the table.\n  ///\n  /// Before:\n  /// Row 1: |   |   |   |\n  /// Row 2: |   |   |   |\n  ///\n  /// Call this function will add a column at the end of the table.\n  ///\n  /// After:\n  ///                      ↓ New column\n  /// Row 1: |   |   |   |   |\n  /// Row 2: |   |   |   |   |\n  Future<void> addColumnInTable(Node node) async {\n    assert(node.type == SimpleTableBlockKeys.type);\n\n    if (node.type != SimpleTableBlockKeys.type) {\n      Log.warn('node is not a table node: ${node.type}');\n      return;\n    }\n\n    await insertColumnInTable(node, node.columnLength);\n  }\n\n  /// Add a column and a row at the end of the table.\n  ///\n  /// Before:\n  /// Row 1: |   |   |   |\n  /// Row 2: |   |   |   |\n  ///\n  /// Call this function will add a column and a row at the end of the table.\n  ///\n  /// After:\n  ///                      ↓ New column\n  /// Row 1: |   |   |   |   |\n  /// Row 2: |   |   |   |   |\n  /// Row 3: |   |   |   |   | ← New row\n  Future<void> addColumnAndRowInTable(Node node) async {\n    assert(node.type == SimpleTableBlockKeys.type);\n\n    if (node.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    await addColumnInTable(node);\n    await addRowInTable(node);\n  }\n\n  /// Add a column at the given index.\n  ///\n  /// Before:\n  /// Given index: 1\n  /// Row 1: | 0 | 1 |\n  /// Row 2: |   |   |\n  ///\n  /// Call this function with index 1 will add a column at the second position of the table.\n  ///\n  /// After:       ↓ New column\n  /// Row 1: | 0 |   | 1 |\n  /// Row 2: |   |   |   |\n  Future<void> insertColumnInTable(\n    Node node,\n    int index, {\n    bool inMemoryUpdate = false,\n  }) async {\n    assert(node.type == SimpleTableBlockKeys.type);\n\n    if (node.type != SimpleTableBlockKeys.type) {\n      Log.warn('node is not a table node: ${node.type}');\n      return;\n    }\n\n    final columnLength = node.rowLength;\n    final rowLength = node.columnLength;\n\n    Log.info(\n      'add column in table ${node.id} at index: $index, column length: $columnLength, row length: $rowLength',\n    );\n\n    if (index < 0) {\n      Log.warn(\n        'insert column: index out of range: $index, column length: $columnLength',\n      );\n      return;\n    }\n\n    final attributes = node.mapTableAttributes(\n      node,\n      type: TableMapOperationType.insertColumn,\n      index: index,\n    );\n\n    final transaction = this.transaction;\n    for (var i = 0; i < columnLength; i++) {\n      final row = node.children[i];\n      // if the index is greater than the row length, we add the new column at the end of the row.\n      final path = index >= rowLength\n          ? row.children.last.path.next\n          : row.children[index].path;\n      transaction.insertNode(\n        path,\n        simpleTableCellBlockNode(),\n      );\n    }\n    if (attributes != null) {\n      transaction.updateNode(node, attributes);\n    }\n    await apply(\n      transaction,\n      options: ApplyOptions(\n        inMemoryUpdate: inMemoryUpdate,\n      ),\n    );\n  }\n\n  /// Add a row at the given index.\n  ///\n  /// Before:\n  /// Given index: 1\n  /// Row 1: |   |   |\n  /// Row 2: |   |   |\n  ///\n  /// Call this function with index 1 will add a row at the second position of the table.\n  ///\n  /// After:\n  /// Row 1: |   |   |\n  /// Row 2: |   |   |\n  /// Row 3: |   |   | ← New row\n  Future<void> insertRowInTable(\n    Node node,\n    int index, {\n    bool inMemoryUpdate = false,\n  }) async {\n    assert(node.type == SimpleTableBlockKeys.type);\n\n    if (node.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    if (index < 0) {\n      Log.warn(\n        'insert row: index out of range: $index',\n      );\n      return;\n    }\n\n    final columnLength = node.rowLength;\n    final rowLength = node.columnLength;\n\n    Log.info(\n      'insert row in table ${node.id} at index: $index, column length: $columnLength, row length: $rowLength',\n    );\n\n    final newRow = simpleTableRowBlockNode(\n      children: [\n        for (var i = 0; i < rowLength; i++) simpleTableCellBlockNode(),\n      ],\n    );\n\n    final attributes = node.mapTableAttributes(\n      node,\n      type: TableMapOperationType.insertRow,\n      index: index,\n    );\n\n    final transaction = this.transaction;\n    final path = index >= columnLength\n        ? node.children.last.path.next\n        : node.children[index].path;\n    transaction.insertNode(path, newRow);\n    if (attributes != null) {\n      transaction.updateNode(node, attributes);\n    }\n    await apply(\n      transaction,\n      options: ApplyOptions(\n        inMemoryUpdate: inMemoryUpdate,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nenum TableMapOperationType {\n  insertRow,\n  deleteRow,\n  insertColumn,\n  deleteColumn,\n  duplicateRow,\n  duplicateColumn,\n  reorderColumn,\n  reorderRow,\n}\n\nextension TableMapOperation on Node {\n  Attributes? mapTableAttributes(\n    Node node, {\n    required TableMapOperationType type,\n    required int index,\n    // Only used for reorder column operation\n    int? toIndex,\n  }) {\n    assert(this.type == SimpleTableBlockKeys.type);\n\n    if (this.type != SimpleTableBlockKeys.type) {\n      return null;\n    }\n\n    Attributes? attributes;\n\n    switch (type) {\n      case TableMapOperationType.insertRow:\n        attributes = _mapRowInsertionAttributes(index);\n      case TableMapOperationType.insertColumn:\n        attributes = _mapColumnInsertionAttributes(index);\n      case TableMapOperationType.duplicateRow:\n        attributes = _mapRowDuplicationAttributes(index);\n      case TableMapOperationType.duplicateColumn:\n        attributes = _mapColumnDuplicationAttributes(index);\n      case TableMapOperationType.deleteRow:\n        attributes = _mapRowDeletionAttributes(index);\n      case TableMapOperationType.deleteColumn:\n        attributes = _mapColumnDeletionAttributes(index);\n      case TableMapOperationType.reorderColumn:\n        if (toIndex != null) {\n          attributes = _mapColumnReorderingAttributes(index, toIndex);\n        }\n      case TableMapOperationType.reorderRow:\n        if (toIndex != null) {\n          attributes = _mapRowReorderingAttributes(index, toIndex);\n        }\n    }\n\n    // clear the attributes that are null\n    attributes?.removeWhere(\n      (key, value) => value == null,\n    );\n\n    return attributes;\n  }\n\n  /// Map the attributes of a row insertion operation.\n  ///\n  /// When inserting a row, the attributes of the table after the index should be updated\n  /// For example:\n  /// Before:\n  /// |  0  |  1  |  2  |\n  /// |  3  |  4  |  5  | ← insert a new row here\n  ///\n  /// The original attributes of the table:\n  /// {\n  ///   \"rowColors\": {\n  ///     0: \"#FF0000\",\n  ///     1: \"#00FF00\",\n  ///   }\n  /// }\n  ///\n  /// Insert a row at index 1:\n  /// |  0  |  1  |  2  |\n  /// |     |     |     | ← new row\n  /// |  3  |  4  |  5  |\n  ///\n  /// The new attributes of the table:\n  /// {\n  ///   \"rowColors\": {\n  ///     0: \"#FF0000\",\n  ///     2: \"#00FF00\", ← The attributes of the original second row\n  ///   }\n  /// }\n  Attributes? _mapRowInsertionAttributes(int index) {\n    final attributes = this.attributes;\n    try {\n      final rowColors = _remapSource(\n        this.rowColors,\n        index,\n        comparator: (iKey, index) => iKey >= index,\n      );\n\n      final rowAligns = _remapSource(\n        this.rowAligns,\n        index,\n        comparator: (iKey, index) => iKey >= index,\n      );\n\n      final rowBoldAttributes = _remapSource(\n        this.rowBoldAttributes,\n        index,\n        comparator: (iKey, index) => iKey >= index,\n      );\n\n      final rowTextColors = _remapSource(\n        this.rowTextColors,\n        index,\n        comparator: (iKey, index) => iKey >= index,\n      );\n\n      return attributes\n          .mergeValues(\n            SimpleTableBlockKeys.rowColors,\n            rowColors,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.rowAligns,\n            rowAligns,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.rowBoldAttributes,\n            rowBoldAttributes,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.rowTextColors,\n            rowTextColors,\n          );\n    } catch (e) {\n      Log.warn('Failed to map row insertion attributes: $e');\n      return attributes;\n    }\n  }\n\n  /// Map the attributes of a column insertion operation.\n  ///\n  /// When inserting a column, the attributes of the table after the index should be updated\n  /// For example:\n  /// Before:\n  /// |  0  |  1  |\n  /// |  2  |  3  |\n  ///\n  /// The original attributes of the table:\n  /// {\n  ///   \"columnColors\": {\n  ///     0: \"#FF0000\",\n  ///     1: \"#00FF00\",\n  ///   }\n  /// }\n  ///\n  /// Insert a column at index 1:\n  /// |  0  |     |  1  |\n  /// |  2  |     |  3  |\n  ///\n  /// The new attributes of the table:\n  /// {\n  ///   \"columnColors\": {\n  ///     0: \"#FF0000\",\n  ///     2: \"#00FF00\", ← The attributes of the original second column\n  ///   }\n  /// }\n  Attributes? _mapColumnInsertionAttributes(int index) {\n    final attributes = this.attributes;\n    try {\n      final columnColors = _remapSource(\n        this.columnColors,\n        index,\n        comparator: (iKey, index) => iKey >= index,\n      );\n\n      final columnAligns = _remapSource(\n        this.columnAligns,\n        index,\n        comparator: (iKey, index) => iKey >= index,\n      );\n\n      final columnWidths = _remapSource(\n        this.columnWidths,\n        index,\n        comparator: (iKey, index) => iKey >= index,\n      );\n\n      final columnBoldAttributes = _remapSource(\n        this.columnBoldAttributes,\n        index,\n        comparator: (iKey, index) => iKey >= index,\n      );\n\n      final columnTextColors = _remapSource(\n        this.columnTextColors,\n        index,\n        comparator: (iKey, index) => iKey >= index,\n      );\n\n      final bool distributeColumnWidthsEvenly =\n          attributes[SimpleTableBlockKeys.distributeColumnWidthsEvenly] ??\n              false;\n\n      if (distributeColumnWidthsEvenly) {\n        // if the distribute column widths evenly flag is true,\n        // we should distribute the column widths evenly\n        columnWidths[index.toString()] = columnWidths.values.firstOrNull;\n      }\n\n      return attributes\n          .mergeValues(\n            SimpleTableBlockKeys.columnColors,\n            columnColors,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.columnAligns,\n            columnAligns,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.columnWidths,\n            columnWidths,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.columnBoldAttributes,\n            columnBoldAttributes,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.columnTextColors,\n            columnTextColors,\n          );\n    } catch (e) {\n      Log.warn('Failed to map row insertion attributes: $e');\n      return attributes;\n    }\n  }\n\n  /// Map the attributes of a row duplication operation.\n  ///\n  /// When duplicating a row, the attributes of the table after the index should be updated\n  /// For example:\n  /// Before:\n  /// |  0  |  1  |  2  |\n  /// |  3  |  4  |  5  |\n  ///\n  /// The original attributes of the table:\n  /// {\n  ///   \"rowColors\": {\n  ///     0: \"#FF0000\",\n  ///     1: \"#00FF00\",\n  ///   }\n  /// }\n  ///\n  /// Duplicate the row at index 1:\n  /// |  0  |  1  |  2  |\n  /// |  3  |  4  |  5  |\n  /// |  3  |  4  |  5  | ← duplicated row\n  ///\n  /// The new attributes of the table:\n  /// {\n  ///   \"rowColors\": {\n  ///     0: \"#FF0000\",\n  ///     1: \"#00FF00\",\n  ///     2: \"#00FF00\", ← The attributes of the original second row\n  ///   }\n  /// }\n  Attributes? _mapRowDuplicationAttributes(int index) {\n    final attributes = this.attributes;\n    try {\n      final (rowColors, duplicatedRowColor) = _findDuplicatedEntryAndRemap(\n        this.rowColors,\n        index,\n      );\n\n      final (rowAligns, duplicatedRowAlign) = _findDuplicatedEntryAndRemap(\n        this.rowAligns,\n        index,\n      );\n\n      final (rowBoldAttributes, duplicatedRowBoldAttribute) =\n          _findDuplicatedEntryAndRemap(\n        this.rowBoldAttributes,\n        index,\n      );\n\n      final (rowTextColors, duplicatedRowTextColor) =\n          _findDuplicatedEntryAndRemap(\n        this.rowTextColors,\n        index,\n      );\n\n      return attributes\n          .mergeValues(\n            SimpleTableBlockKeys.rowColors,\n            rowColors,\n            duplicatedEntry: duplicatedRowColor,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.rowAligns,\n            rowAligns,\n            duplicatedEntry: duplicatedRowAlign,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.rowBoldAttributes,\n            rowBoldAttributes,\n            duplicatedEntry: duplicatedRowBoldAttribute,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.rowTextColors,\n            rowTextColors,\n            duplicatedEntry: duplicatedRowTextColor,\n          );\n    } catch (e) {\n      Log.warn('Failed to map row insertion attributes: $e');\n      return attributes;\n    }\n  }\n\n  /// Map the attributes of a column duplication operation.\n  ///\n  /// When duplicating a column, the attributes of the table after the index should be updated\n  /// For example:\n  /// Before:\n  /// |  0  |  1  |\n  /// |  2  |  3  |\n  ///\n  /// The original attributes of the table:\n  /// {\n  ///   \"columnColors\": {\n  ///     0: \"#FF0000\",\n  ///     1: \"#00FF00\",\n  ///   }\n  /// }\n  ///\n  /// Duplicate the column at index 1:\n  /// |  0  |  1  |  1  | ← duplicated column\n  /// |  2  |  3  |  2  | ← duplicated column\n  ///\n  /// The new attributes of the table:\n  /// {\n  ///   \"columnColors\": {\n  ///     0: \"#FF0000\",\n  ///     1: \"#00FF00\",\n  ///     2: \"#00FF00\", ← The attributes of the original second column\n  ///   }\n  /// }\n  Attributes? _mapColumnDuplicationAttributes(int index) {\n    final attributes = this.attributes;\n    try {\n      final (columnColors, duplicatedColumnColor) =\n          _findDuplicatedEntryAndRemap(\n        this.columnColors,\n        index,\n      );\n\n      final (columnAligns, duplicatedColumnAlign) =\n          _findDuplicatedEntryAndRemap(\n        this.columnAligns,\n        index,\n      );\n\n      final (columnWidths, duplicatedColumnWidth) =\n          _findDuplicatedEntryAndRemap(\n        this.columnWidths,\n        index,\n      );\n\n      final (columnBoldAttributes, duplicatedColumnBoldAttribute) =\n          _findDuplicatedEntryAndRemap(\n        this.columnBoldAttributes,\n        index,\n      );\n\n      final (columnTextColors, duplicatedColumnTextColor) =\n          _findDuplicatedEntryAndRemap(\n        this.columnTextColors,\n        index,\n      );\n\n      return attributes\n          .mergeValues(\n            SimpleTableBlockKeys.columnColors,\n            columnColors,\n            duplicatedEntry: duplicatedColumnColor,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.columnAligns,\n            columnAligns,\n            duplicatedEntry: duplicatedColumnAlign,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.columnWidths,\n            columnWidths,\n            duplicatedEntry: duplicatedColumnWidth,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.columnBoldAttributes,\n            columnBoldAttributes,\n            duplicatedEntry: duplicatedColumnBoldAttribute,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.columnTextColors,\n            columnTextColors,\n            duplicatedEntry: duplicatedColumnTextColor,\n          );\n    } catch (e) {\n      Log.warn('Failed to map column duplication attributes: $e');\n      return attributes;\n    }\n  }\n\n  /// Map the attributes of a column deletion operation.\n  ///\n  /// When deleting a column, the attributes of the table after the index should be updated\n  ///\n  /// For example:\n  /// Before:\n  /// |  0  |  1  |  2  |\n  /// |  3  |  4  |  5  |\n  ///\n  /// The original attributes of the table:\n  /// {\n  ///   \"columnColors\": {\n  ///     0: \"#FF0000\",\n  ///     2: \"#00FF00\",\n  ///   }\n  /// }\n  ///\n  /// Delete the column at index 1:\n  /// |  0  |  2  |\n  /// |  3  |  5  |\n  ///\n  /// The new attributes of the table:\n  /// {\n  ///   \"columnColors\": {\n  ///     0: \"#FF0000\",\n  ///     1: \"#00FF00\", ← The attributes of the original second column\n  ///   }\n  /// }\n  Attributes? _mapColumnDeletionAttributes(int index) {\n    final attributes = this.attributes;\n    try {\n      final columnColors = _remapSource(\n        this.columnColors,\n        index,\n        increment: false,\n        comparator: (iKey, index) => iKey > index,\n        filterIndex: index,\n      );\n\n      final columnAligns = _remapSource(\n        this.columnAligns,\n        index,\n        increment: false,\n        comparator: (iKey, index) => iKey > index,\n        filterIndex: index,\n      );\n\n      final columnWidths = _remapSource(\n        this.columnWidths,\n        index,\n        increment: false,\n        comparator: (iKey, index) => iKey > index,\n        filterIndex: index,\n      );\n\n      final columnBoldAttributes = _remapSource(\n        this.columnBoldAttributes,\n        index,\n        increment: false,\n        comparator: (iKey, index) => iKey > index,\n        filterIndex: index,\n      );\n\n      final columnTextColors = _remapSource(\n        this.columnTextColors,\n        index,\n        increment: false,\n        comparator: (iKey, index) => iKey > index,\n        filterIndex: index,\n      );\n\n      return attributes\n          .mergeValues(\n            SimpleTableBlockKeys.columnColors,\n            columnColors,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.columnAligns,\n            columnAligns,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.columnWidths,\n            columnWidths,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.columnBoldAttributes,\n            columnBoldAttributes,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.columnTextColors,\n            columnTextColors,\n          );\n    } catch (e) {\n      Log.warn('Failed to map column deletion attributes: $e');\n      return attributes;\n    }\n  }\n\n  /// Map the attributes of a row deletion operation.\n  ///\n  /// When deleting a row, the attributes of the table after the index should be updated\n  ///\n  /// For example:\n  /// Before:\n  /// |  0  |  1  |  2  | ← delete this row\n  /// |  3  |  4  |  5  |\n  ///\n  /// The original attributes of the table:\n  /// {\n  ///   \"rowColors\": {\n  ///     0: \"#FF0000\",\n  ///     1: \"#00FF00\",\n  ///   }\n  /// }\n  ///\n  /// Delete the row at index 0:\n  /// |  3  |  4  |  5  |\n  ///\n  /// The new attributes of the table:\n  /// {\n  ///   \"rowColors\": {\n  ///     0: \"#00FF00\",\n  ///   }\n  /// }\n  Attributes? _mapRowDeletionAttributes(int index) {\n    final attributes = this.attributes;\n    try {\n      final rowColors = _remapSource(\n        this.rowColors,\n        index,\n        increment: false,\n        comparator: (iKey, index) => iKey > index,\n        filterIndex: index,\n      );\n\n      final rowAligns = _remapSource(\n        this.rowAligns,\n        index,\n        increment: false,\n        comparator: (iKey, index) => iKey > index,\n        filterIndex: index,\n      );\n\n      final rowBoldAttributes = _remapSource(\n        this.rowBoldAttributes,\n        index,\n        increment: false,\n        comparator: (iKey, index) => iKey > index,\n        filterIndex: index,\n      );\n\n      final rowTextColors = _remapSource(\n        this.rowTextColors,\n        index,\n        increment: false,\n        comparator: (iKey, index) => iKey > index,\n        filterIndex: index,\n      );\n\n      return attributes\n          .mergeValues(\n            SimpleTableBlockKeys.rowColors,\n            rowColors,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.rowAligns,\n            rowAligns,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.rowBoldAttributes,\n            rowBoldAttributes,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.rowTextColors,\n            rowTextColors,\n          );\n    } catch (e) {\n      Log.warn('Failed to map row deletion attributes: $e');\n      return attributes;\n    }\n  }\n\n  /// Map the attributes of a column reordering operation.\n  ///\n  ///\n  /// Examples:\n  /// Case 1:\n  ///\n  /// When reordering a column, if the from index is greater than the to index,\n  /// the attributes of the table before the from index should be updated.\n  ///\n  /// Before:\n  ///          ↓ reorder this column from index 1 to index 0\n  /// |  0  |  1  |  2  |\n  /// |  3  |  4  |  5  |\n  ///\n  /// The original attributes of the table:\n  /// {\n  ///   \"rowColors\": {\n  ///     0: \"#FF0000\",\n  ///     1: \"#00FF00\",\n  ///     2: \"#0000FF\",\n  ///   }\n  /// }\n  ///\n  /// After reordering:\n  /// |  1  |  0  |  2  |\n  /// |  4  |  3  |  5  |\n  ///\n  /// The new attributes of the table:\n  /// {\n  ///   \"rowColors\": {\n  ///     0: \"#00FF00\", ← The attributes of the original second column\n  ///     1: \"#FF0000\", ← The attributes of the original first column\n  ///     2: \"#0000FF\",\n  ///   }\n  /// }\n  ///\n  /// Case 2:\n  ///\n  /// When reordering a column, if the from index is less than the to index,\n  /// the attributes of the table after the from index should be updated.\n  ///\n  /// Before:\n  ///          ↓ reorder this column from index 1 to index 2\n  /// |  0  |  1  |  2  |\n  /// |  3  |  4  |  5  |\n  ///\n  /// The original attributes of the table:\n  /// {\n  ///   \"columnColors\": {\n  ///     0: \"#FF0000\",\n  ///     1: \"#00FF00\",\n  ///     2: \"#0000FF\",\n  ///   }\n  /// }\n  ///\n  /// After reordering:\n  /// |  0  |  2  |  1  |\n  /// |  3  |  5  |  4  |\n  ///\n  /// The new attributes of the table:\n  /// {\n  ///   \"columnColors\": {\n  ///     0: \"#FF0000\",\n  ///     1: \"#0000FF\", ← The attributes of the original third column\n  ///     2: \"#00FF00\", ← The attributes of the original second column\n  ///   }\n  /// }\n  Attributes? _mapColumnReorderingAttributes(int fromIndex, int toIndex) {\n    final attributes = this.attributes;\n    try {\n      final duplicatedColumnColor = this.columnColors[fromIndex.toString()];\n      final duplicatedColumnAlign = this.columnAligns[fromIndex.toString()];\n      final duplicatedColumnWidth = this.columnWidths[fromIndex.toString()];\n      final duplicatedColumnBoldAttribute =\n          this.columnBoldAttributes[fromIndex.toString()];\n      final duplicatedColumnTextColor =\n          this.columnTextColors[fromIndex.toString()];\n\n      /// Case 1: fromIndex > toIndex\n      /// Before:\n      /// Row 0: | 0 | 1 | 2 |\n      /// Row 1: | 3 | 4 | 5 |\n      /// Row 2: | 6 | 7 | 8 |\n      ///\n      /// columnColors = {\n      ///   \"0\": \"#FF0000\",\n      ///   \"1\": \"#00FF00\",\n      ///   \"2\": \"#0000FF\" ← Move this column (index 2)\n      /// }\n      ///\n      /// Move column 2 to index 0:\n      /// Row 0: | 2 | 0 | 1 |\n      /// Row 1: | 5 | 3 | 4 |\n      /// Row 2: | 8 | 6 | 7 |\n      ///\n      /// columnColors = {\n      ///   \"0\": \"#0000FF\", ← Moved here\n      ///   \"1\": \"#FF0000\",\n      ///   \"2\": \"#00FF00\"\n      /// }\n      ///\n      /// Case 2: fromIndex < toIndex\n      /// Before:\n      /// Row 0: | 0 | 1 | 2 |\n      /// Row 1: | 3 | 4 | 5 |\n      /// Row 2: | 6 | 7 | 8 |\n      ///\n      /// columnColors = {\n      ///   \"0\": \"#FF0000\" ← Move this column (index 0)\n      ///   \"1\": \"#00FF00\",\n      ///   \"2\": \"#0000FF\"\n      /// }\n      ///\n      /// Move column 0 to index 2:\n      /// Row 0: | 1 | 2 | 0 |\n      /// Row 1: | 4 | 5 | 3 |\n      /// Row 2: | 7 | 8 | 6 |\n      ///\n      /// columnColors = {\n      ///   \"0\": \"#00FF00\",\n      ///   \"1\": \"#0000FF\",\n      ///   \"2\": \"#FF0000\" ← Moved here\n      /// }\n      final columnColors = _remapSource(\n        this.columnColors,\n        fromIndex,\n        increment: fromIndex > toIndex,\n        comparator: (iKey, index) {\n          if (fromIndex > toIndex) {\n            return iKey < fromIndex && iKey >= toIndex;\n          } else {\n            return iKey > fromIndex && iKey <= toIndex;\n          }\n        },\n        filterIndex: fromIndex,\n      );\n\n      final columnAligns = _remapSource(\n        this.columnAligns,\n        fromIndex,\n        increment: fromIndex > toIndex,\n        comparator: (iKey, index) {\n          if (fromIndex > toIndex) {\n            return iKey < fromIndex && iKey >= toIndex;\n          } else {\n            return iKey > fromIndex && iKey <= toIndex;\n          }\n        },\n        filterIndex: fromIndex,\n      );\n\n      final columnWidths = _remapSource(\n        this.columnWidths,\n        fromIndex,\n        increment: fromIndex > toIndex,\n        comparator: (iKey, index) {\n          if (fromIndex > toIndex) {\n            return iKey < fromIndex && iKey >= toIndex;\n          } else {\n            return iKey > fromIndex && iKey <= toIndex;\n          }\n        },\n        filterIndex: fromIndex,\n      );\n\n      final columnBoldAttributes = _remapSource(\n        this.columnBoldAttributes,\n        fromIndex,\n        increment: fromIndex > toIndex,\n        comparator: (iKey, index) {\n          if (fromIndex > toIndex) {\n            return iKey < fromIndex && iKey >= toIndex;\n          } else {\n            return iKey > fromIndex && iKey <= toIndex;\n          }\n        },\n        filterIndex: fromIndex,\n      );\n\n      final columnTextColors = _remapSource(\n        this.columnTextColors,\n        fromIndex,\n        increment: fromIndex > toIndex,\n        comparator: (iKey, index) {\n          if (fromIndex > toIndex) {\n            return iKey < fromIndex && iKey >= toIndex;\n          } else {\n            return iKey > fromIndex && iKey <= toIndex;\n          }\n        },\n        filterIndex: fromIndex,\n      );\n\n      return attributes\n          .mergeValues(\n            SimpleTableBlockKeys.columnColors,\n            columnColors,\n            duplicatedEntry: duplicatedColumnColor != null\n                ? MapEntry(\n                    toIndex.toString(),\n                    duplicatedColumnColor,\n                  )\n                : null,\n            removeNullValue: true,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.columnAligns,\n            columnAligns,\n            duplicatedEntry: duplicatedColumnAlign != null\n                ? MapEntry(\n                    toIndex.toString(),\n                    duplicatedColumnAlign,\n                  )\n                : null,\n            removeNullValue: true,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.columnWidths,\n            columnWidths,\n            duplicatedEntry: duplicatedColumnWidth != null\n                ? MapEntry(\n                    toIndex.toString(),\n                    duplicatedColumnWidth,\n                  )\n                : null,\n            removeNullValue: true,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.columnBoldAttributes,\n            columnBoldAttributes,\n            duplicatedEntry: duplicatedColumnBoldAttribute != null\n                ? MapEntry(\n                    toIndex.toString(),\n                    duplicatedColumnBoldAttribute,\n                  )\n                : null,\n            removeNullValue: true,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.columnTextColors,\n            columnTextColors,\n            duplicatedEntry: duplicatedColumnTextColor != null\n                ? MapEntry(\n                    toIndex.toString(),\n                    duplicatedColumnTextColor,\n                  )\n                : null,\n            removeNullValue: true,\n          );\n    } catch (e) {\n      Log.warn('Failed to map column deletion attributes: $e');\n      return attributes;\n    }\n  }\n\n  /// Map the attributes of a row reordering operation.\n  ///\n  /// See [_mapColumnReorderingAttributes] for more details.\n  Attributes? _mapRowReorderingAttributes(int fromIndex, int toIndex) {\n    final attributes = this.attributes;\n    try {\n      final duplicatedRowColor = this.rowColors[fromIndex.toString()];\n      final duplicatedRowAlign = this.rowAligns[fromIndex.toString()];\n      final duplicatedRowBoldAttribute =\n          this.rowBoldAttributes[fromIndex.toString()];\n      final duplicatedRowTextColor = this.rowTextColors[fromIndex.toString()];\n\n      /// Example:\n      /// Case 1: fromIndex > toIndex\n      /// Before:\n      /// Row 0: | 0 | 1 | 2 |\n      /// Row 1: | 3 | 4 | 5 | ← Move this row (index 1)\n      /// Row 2: | 6 | 7 | 8 |\n      ///\n      /// rowColors = {\n      ///   \"0\": \"#FF0000\",\n      ///   \"1\": \"#00FF00\", ← This will be moved\n      ///   \"2\": \"#0000FF\"\n      /// }\n      ///\n      /// Move row 1 to index 0:\n      /// Row 0: | 3 | 4 | 5 | ← Moved here\n      /// Row 1: | 0 | 1 | 2 |\n      /// Row 2: | 6 | 7 | 8 |\n      ///\n      /// rowColors = {\n      ///   \"0\": \"#00FF00\", ← Moved here\n      ///   \"1\": \"#FF0000\",\n      ///   \"2\": \"#0000FF\"\n      /// }\n      ///\n      /// Case 2: fromIndex < toIndex\n      /// Before:\n      /// Row 0: | 0 | 1 | 2 |\n      /// Row 1: | 3 | 4 | 5 | ← Move this row (index 1)\n      /// Row 2: | 6 | 7 | 8 |\n      ///\n      /// rowColors = {\n      ///   \"0\": \"#FF0000\",\n      ///   \"1\": \"#00FF00\", ← This will be moved\n      ///   \"2\": \"#0000FF\"\n      /// }\n      ///\n      /// Move row 1 to index 2:\n      /// Row 0: | 0 | 1 | 2 |\n      /// Row 1: | 3 | 4 | 5 |\n      /// Row 2: | 6 | 7 | 8 | ← Moved here\n      ///\n      /// rowColors = {\n      ///   \"0\": \"#FF0000\",\n      ///   \"1\": \"#0000FF\",\n      ///   \"2\": \"#00FF00\" ← Moved here\n      /// }\n      final rowColors = _remapSource(\n        this.rowColors,\n        fromIndex,\n        increment: fromIndex > toIndex,\n        comparator: (iKey, index) {\n          if (fromIndex > toIndex) {\n            return iKey < fromIndex && iKey >= toIndex;\n          } else {\n            return iKey > fromIndex && iKey <= toIndex;\n          }\n        },\n        filterIndex: fromIndex,\n      );\n\n      final rowAligns = _remapSource(\n        this.rowAligns,\n        fromIndex,\n        increment: fromIndex > toIndex,\n        comparator: (iKey, index) {\n          if (fromIndex > toIndex) {\n            return iKey < fromIndex && iKey >= toIndex;\n          } else {\n            return iKey > fromIndex && iKey <= toIndex;\n          }\n        },\n        filterIndex: fromIndex,\n      );\n\n      final rowBoldAttributes = _remapSource(\n        this.rowBoldAttributes,\n        fromIndex,\n        increment: fromIndex > toIndex,\n        comparator: (iKey, index) {\n          if (fromIndex > toIndex) {\n            return iKey < fromIndex && iKey >= toIndex;\n          } else {\n            return iKey > fromIndex && iKey <= toIndex;\n          }\n        },\n        filterIndex: fromIndex,\n      );\n\n      final rowTextColors = _remapSource(\n        this.rowTextColors,\n        fromIndex,\n        increment: fromIndex > toIndex,\n        comparator: (iKey, index) {\n          if (fromIndex > toIndex) {\n            return iKey < fromIndex && iKey >= toIndex;\n          } else {\n            return iKey > fromIndex && iKey <= toIndex;\n          }\n        },\n        filterIndex: fromIndex,\n      );\n\n      return attributes\n          .mergeValues(\n            SimpleTableBlockKeys.rowColors,\n            rowColors,\n            duplicatedEntry: duplicatedRowColor != null\n                ? MapEntry(\n                    toIndex.toString(),\n                    duplicatedRowColor,\n                  )\n                : null,\n            removeNullValue: true,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.rowAligns,\n            rowAligns,\n            duplicatedEntry: duplicatedRowAlign != null\n                ? MapEntry(\n                    toIndex.toString(),\n                    duplicatedRowAlign,\n                  )\n                : null,\n            removeNullValue: true,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.rowBoldAttributes,\n            rowBoldAttributes,\n            duplicatedEntry: duplicatedRowBoldAttribute != null\n                ? MapEntry(\n                    toIndex.toString(),\n                    duplicatedRowBoldAttribute,\n                  )\n                : null,\n            removeNullValue: true,\n          )\n          .mergeValues(\n            SimpleTableBlockKeys.rowTextColors,\n            rowTextColors,\n            duplicatedEntry: duplicatedRowTextColor != null\n                ? MapEntry(\n                    toIndex.toString(),\n                    duplicatedRowTextColor,\n                  )\n                : null,\n            removeNullValue: true,\n          );\n    } catch (e) {\n      Log.warn('Failed to map row reordering attributes: $e');\n      return attributes;\n    }\n  }\n}\n\n/// Find the duplicated entry and remap the source.\n///\n/// All the entries after the index will be remapped to the new index.\n(Map<String, dynamic> newSource, MapEntry? duplicatedEntry)\n    _findDuplicatedEntryAndRemap(\n  Map<String, dynamic> source,\n  int index, {\n  bool increment = true,\n}) {\n  MapEntry? duplicatedEntry;\n  final newSource = source.map((key, value) {\n    final iKey = int.parse(key);\n    if (iKey == index) {\n      duplicatedEntry = MapEntry(key, value);\n    }\n    if (iKey >= index) {\n      return MapEntry((iKey + (increment ? 1 : -1)).toString(), value);\n    }\n    return MapEntry(key, value);\n  });\n  return (newSource, duplicatedEntry);\n}\n\n/// Remap the source to the new index.\n///\n/// All the entries after the index will be remapped to the new index.\nMap<String, dynamic> _remapSource(\n  Map<String, dynamic> source,\n  int index, {\n  bool increment = true,\n  required bool Function(int iKey, int index) comparator,\n  int? filterIndex,\n}) {\n  var newSource = {...source};\n  if (filterIndex != null) {\n    newSource.remove(filterIndex.toString());\n  }\n  newSource = newSource.map((key, value) {\n    final iKey = int.parse(key);\n    if (comparator(iKey, index)) {\n      return MapEntry((iKey + (increment ? 1 : -1)).toString(), value);\n    }\n    return MapEntry(key, value);\n  });\n  return newSource;\n}\n\nextension TableMapOperationAttributes on Attributes {\n  Attributes mergeValues(\n    String key,\n    Map<String, dynamic> newSource, {\n    MapEntry? duplicatedEntry,\n    bool removeNullValue = false,\n  }) {\n    final result = {...this};\n\n    if (duplicatedEntry != null) {\n      newSource[duplicatedEntry.key] = duplicatedEntry.value;\n    }\n\n    if (removeNullValue) {\n      // remove the null value\n      newSource.removeWhere((key, value) => value == null);\n    }\n\n    result[key] = newSource;\n\n    return result;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_page.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/material.dart';\n\ntypedef TableCellPosition = (int, int);\n\nenum TableAlign {\n  left,\n  center,\n  right;\n\n  static TableAlign fromString(String align) {\n    return TableAlign.values.firstWhere(\n      (e) => e.key.toLowerCase() == align.toLowerCase(),\n      orElse: () => TableAlign.left,\n    );\n  }\n\n  String get name => switch (this) {\n        TableAlign.left => 'Left',\n        TableAlign.center => 'Center',\n        TableAlign.right => 'Right',\n      };\n\n  // The key used in the attributes of the table node.\n  //\n  // Example:\n  //\n  // attributes[SimpleTableBlockKeys.columnAligns] = {0: 'left', 1: 'center', 2: 'right'}\n  String get key => switch (this) {\n        TableAlign.left => 'left',\n        TableAlign.center => 'center',\n        TableAlign.right => 'right',\n      };\n\n  FlowySvgData get leftIconSvg => switch (this) {\n        TableAlign.left => FlowySvgs.table_align_left_s,\n        TableAlign.center => FlowySvgs.table_align_center_s,\n        TableAlign.right => FlowySvgs.table_align_right_s,\n      };\n\n  Alignment get alignment => switch (this) {\n        TableAlign.left => Alignment.topLeft,\n        TableAlign.center => Alignment.topCenter,\n        TableAlign.right => Alignment.topRight,\n      };\n\n  TextAlign get textAlign => switch (this) {\n        TableAlign.left => TextAlign.left,\n        TableAlign.center => TextAlign.center,\n        TableAlign.right => TextAlign.right,\n      };\n}\n\nextension TableNodeExtension on Node {\n  /// The number of rows in the table.\n  ///\n  /// The acceptable node is a table node, table row node or table cell node.\n  ///\n  /// Example:\n  ///\n  /// Row 1: |   |   |   |\n  /// Row 2: |   |   |   |\n  ///\n  /// The row length is 2.\n  int get rowLength {\n    final parentTableNode = this.parentTableNode;\n\n    if (parentTableNode == null ||\n        parentTableNode.type != SimpleTableBlockKeys.type) {\n      return -1;\n    }\n\n    return parentTableNode.children.length;\n  }\n\n  /// The number of rows in the table.\n  ///\n  /// The acceptable node is a table node, table row node or table cell node.\n  ///\n  /// Example:\n  ///\n  /// Row 1: |   |   |   |\n  /// Row 2: |   |   |   |\n  ///\n  /// The column length is 3.\n  int get columnLength {\n    final parentTableNode = this.parentTableNode;\n\n    if (parentTableNode == null ||\n        parentTableNode.type != SimpleTableBlockKeys.type) {\n      return -1;\n    }\n\n    return parentTableNode.children.firstOrNull?.children.length ?? 0;\n  }\n\n  TableCellPosition get cellPosition {\n    assert(type == SimpleTableCellBlockKeys.type);\n    return (rowIndex, columnIndex);\n  }\n\n  int get rowIndex {\n    if (type == SimpleTableCellBlockKeys.type) {\n      if (path.parent.isEmpty) {\n        return -1;\n      }\n      return path.parent.last;\n    } else if (type == SimpleTableRowBlockKeys.type) {\n      return path.last;\n    }\n    return -1;\n  }\n\n  int get columnIndex {\n    assert(type == SimpleTableCellBlockKeys.type);\n    if (path.isEmpty) {\n      return -1;\n    }\n    return path.last;\n  }\n\n  bool get isHeaderColumnEnabled {\n    try {\n      return parentTableNode\n              ?.attributes[SimpleTableBlockKeys.enableHeaderColumn] ??\n          false;\n    } catch (e) {\n      Log.warn('get is header column enabled: $e');\n      return false;\n    }\n  }\n\n  bool get isHeaderRowEnabled {\n    try {\n      return parentTableNode\n              ?.attributes[SimpleTableBlockKeys.enableHeaderRow] ??\n          false;\n    } catch (e) {\n      Log.warn('get is header row enabled: $e');\n      return false;\n    }\n  }\n\n  TableAlign get rowAlign {\n    final parentTableNode = this.parentTableNode;\n\n    if (parentTableNode == null) {\n      return TableAlign.left;\n    }\n\n    try {\n      final rowAligns =\n          parentTableNode.attributes[SimpleTableBlockKeys.rowAligns];\n      final align = rowAligns?[rowIndex.toString()];\n      return TableAlign.values.firstWhere(\n        (e) => e.key == align,\n        orElse: () => TableAlign.left,\n      );\n    } catch (e) {\n      Log.warn('get row align: $e');\n      return TableAlign.left;\n    }\n  }\n\n  TableAlign get columnAlign {\n    final parentTableNode = this.parentTableNode;\n\n    if (parentTableNode == null) {\n      return TableAlign.left;\n    }\n\n    try {\n      final columnAligns =\n          parentTableNode.attributes[SimpleTableBlockKeys.columnAligns];\n      final align = columnAligns?[columnIndex.toString()];\n      return TableAlign.values.firstWhere(\n        (e) => e.key == align,\n        orElse: () => TableAlign.left,\n      );\n    } catch (e) {\n      Log.warn('get column align: $e');\n      return TableAlign.left;\n    }\n  }\n\n  Node? get parentTableNode {\n    Node? tableNode;\n\n    if (type == SimpleTableBlockKeys.type) {\n      tableNode = this;\n    } else if (type == SimpleTableRowBlockKeys.type) {\n      tableNode = parent;\n    } else if (type == SimpleTableCellBlockKeys.type) {\n      tableNode = parent?.parent;\n    } else {\n      return parent?.parentTableNode;\n    }\n\n    if (tableNode == null || tableNode.type != SimpleTableBlockKeys.type) {\n      return null;\n    }\n\n    return tableNode;\n  }\n\n  Node? get parentTableCellNode {\n    Node? tableCellNode;\n\n    if (type == SimpleTableCellBlockKeys.type) {\n      tableCellNode = this;\n    } else {\n      return parent?.parentTableCellNode;\n    }\n\n    return tableCellNode;\n  }\n\n  /// Whether the current node is in a table.\n  bool get isInTable {\n    return parentTableNode != null;\n  }\n\n  double get columnWidth {\n    final parentTableNode = this.parentTableNode;\n\n    if (parentTableNode == null) {\n      return SimpleTableConstants.defaultColumnWidth;\n    }\n\n    try {\n      final columnWidths =\n          parentTableNode.attributes[SimpleTableBlockKeys.columnWidths];\n      final width = columnWidths?[columnIndex.toString()] as Object?;\n      if (width == null) {\n        return SimpleTableConstants.defaultColumnWidth;\n      }\n      return width.toDouble(\n        defaultValue: SimpleTableConstants.defaultColumnWidth,\n      );\n    } catch (e) {\n      Log.warn('get column width: $e');\n      return SimpleTableConstants.defaultColumnWidth;\n    }\n  }\n\n  /// Build the row color.\n  ///\n  /// Default is null.\n  Color? buildRowColor(BuildContext context) {\n    try {\n      final rawRowColors =\n          parentTableNode?.attributes[SimpleTableBlockKeys.rowColors];\n      if (rawRowColors == null) {\n        return null;\n      }\n      final color = rawRowColors[rowIndex.toString()];\n      if (color == null) {\n        return null;\n      }\n      return buildEditorCustomizedColor(context, this, color);\n    } catch (e) {\n      Log.warn('get row color: $e');\n      return null;\n    }\n  }\n\n  /// Build the column color.\n  ///\n  /// Default is null.\n  Color? buildColumnColor(BuildContext context) {\n    try {\n      final columnColors =\n          parentTableNode?.attributes[SimpleTableBlockKeys.columnColors];\n      if (columnColors == null) {\n        return null;\n      }\n      final color = columnColors[columnIndex.toString()];\n      if (color == null) {\n        return null;\n      }\n      return buildEditorCustomizedColor(context, this, color);\n    } catch (e) {\n      Log.warn('get column color: $e');\n      return null;\n    }\n  }\n\n  /// Whether the current node is in the header column.\n  ///\n  /// Default is false.\n  bool get isInHeaderColumn {\n    final parentTableNode = parent?.parentTableNode;\n    if (parentTableNode == null ||\n        parentTableNode.type != SimpleTableBlockKeys.type) {\n      return false;\n    }\n    return parentTableNode.isHeaderColumnEnabled &&\n        parentTableCellNode?.columnIndex == 0;\n  }\n\n  /// Whether the current cell is bold in the column.\n  ///\n  /// Default is false.\n  bool get isInBoldColumn {\n    final parentTableCellNode = this.parentTableCellNode;\n    final parentTableNode = this.parentTableNode;\n    if (parentTableCellNode == null ||\n        parentTableNode == null ||\n        parentTableNode.type != SimpleTableBlockKeys.type) {\n      return false;\n    }\n\n    final columnIndex = parentTableCellNode.columnIndex;\n    final columnBoldAttributes = parentTableNode.columnBoldAttributes;\n    return columnBoldAttributes[columnIndex.toString()] ?? false;\n  }\n\n  /// Whether the current cell is bold in the row.\n  ///\n  /// Default is false.\n  bool get isInBoldRow {\n    final parentTableCellNode = this.parentTableCellNode;\n    final parentTableNode = this.parentTableNode;\n    if (parentTableCellNode == null ||\n        parentTableNode == null ||\n        parentTableNode.type != SimpleTableBlockKeys.type) {\n      return false;\n    }\n\n    final rowIndex = parentTableCellNode.rowIndex;\n    final rowBoldAttributes = parentTableNode.rowBoldAttributes;\n    return rowBoldAttributes[rowIndex.toString()] ?? false;\n  }\n\n  /// Get the text color of the current cell in the column.\n  ///\n  /// Default is null.\n  String? get textColorInColumn {\n    final parentTableCellNode = this.parentTableCellNode;\n    final parentTableNode = this.parentTableNode;\n    if (parentTableCellNode == null ||\n        parentTableNode == null ||\n        parentTableNode.type != SimpleTableBlockKeys.type) {\n      return null;\n    }\n\n    final columnIndex = parentTableCellNode.columnIndex;\n    return parentTableNode.columnTextColors[columnIndex.toString()];\n  }\n\n  /// Get the text color of the current cell in the row.\n  ///\n  /// Default is null.\n  String? get textColorInRow {\n    final parentTableCellNode = this.parentTableCellNode;\n    final parentTableNode = this.parentTableNode;\n    if (parentTableCellNode == null ||\n        parentTableNode == null ||\n        parentTableNode.type != SimpleTableBlockKeys.type) {\n      return null;\n    }\n\n    final rowIndex = parentTableCellNode.rowIndex;\n    return parentTableNode.rowTextColors[rowIndex.toString()];\n  }\n\n  /// Whether the current node is in the header row.\n  ///\n  /// Default is false.\n  bool get isInHeaderRow {\n    final parentTableNode = parent?.parentTableNode;\n    if (parentTableNode == null ||\n        parentTableNode.type != SimpleTableBlockKeys.type) {\n      return false;\n    }\n    return parentTableNode.isHeaderRowEnabled &&\n        parentTableCellNode?.rowIndex == 0;\n  }\n\n  /// Get the row aligns.\n  SimpleTableRowAlignMap get rowAligns {\n    final rawRowAligns =\n        parentTableNode?.attributes[SimpleTableBlockKeys.rowAligns];\n    if (rawRowAligns == null) {\n      return SimpleTableRowAlignMap();\n    }\n    try {\n      return SimpleTableRowAlignMap.from(rawRowAligns);\n    } catch (e) {\n      Log.warn('get row aligns: $e');\n      return SimpleTableRowAlignMap();\n    }\n  }\n\n  /// Get the row colors.\n  SimpleTableColorMap get rowColors {\n    final rawRowColors =\n        parentTableNode?.attributes[SimpleTableBlockKeys.rowColors];\n    if (rawRowColors == null) {\n      return SimpleTableColorMap();\n    }\n    try {\n      return SimpleTableColorMap.from(rawRowColors);\n    } catch (e) {\n      Log.warn('get row colors: $e');\n      return SimpleTableColorMap();\n    }\n  }\n\n  /// Get the column colors.\n  SimpleTableColorMap get columnColors {\n    final rawColumnColors =\n        parentTableNode?.attributes[SimpleTableBlockKeys.columnColors];\n    if (rawColumnColors == null) {\n      return SimpleTableColorMap();\n    }\n    try {\n      return SimpleTableColorMap.from(rawColumnColors);\n    } catch (e) {\n      Log.warn('get column colors: $e');\n      return SimpleTableColorMap();\n    }\n  }\n\n  /// Get the column aligns.\n  SimpleTableColumnAlignMap get columnAligns {\n    final rawColumnAligns =\n        parentTableNode?.attributes[SimpleTableBlockKeys.columnAligns];\n    if (rawColumnAligns == null) {\n      return SimpleTableRowAlignMap();\n    }\n    try {\n      return SimpleTableRowAlignMap.from(rawColumnAligns);\n    } catch (e) {\n      Log.warn('get column aligns: $e');\n      return SimpleTableRowAlignMap();\n    }\n  }\n\n  /// Get the column widths.\n  SimpleTableColumnWidthMap get columnWidths {\n    final rawColumnWidths =\n        parentTableNode?.attributes[SimpleTableBlockKeys.columnWidths];\n    if (rawColumnWidths == null) {\n      return SimpleTableColumnWidthMap();\n    }\n    try {\n      return SimpleTableColumnWidthMap.from(rawColumnWidths);\n    } catch (e) {\n      Log.warn('get column widths: $e');\n      return SimpleTableColumnWidthMap();\n    }\n  }\n\n  /// Get the column text colors\n  SimpleTableColorMap get columnTextColors {\n    final rawColumnTextColors =\n        parentTableNode?.attributes[SimpleTableBlockKeys.columnTextColors];\n    if (rawColumnTextColors == null) {\n      return SimpleTableColorMap();\n    }\n    try {\n      return SimpleTableColorMap.from(rawColumnTextColors);\n    } catch (e) {\n      Log.warn('get column text colors: $e');\n      return SimpleTableColorMap();\n    }\n  }\n\n  /// Get the row text colors\n  SimpleTableColorMap get rowTextColors {\n    final rawRowTextColors =\n        parentTableNode?.attributes[SimpleTableBlockKeys.rowTextColors];\n    if (rawRowTextColors == null) {\n      return SimpleTableColorMap();\n    }\n    try {\n      return SimpleTableColorMap.from(rawRowTextColors);\n    } catch (e) {\n      Log.warn('get row text colors: $e');\n      return SimpleTableColorMap();\n    }\n  }\n\n  /// Get the column bold attributes\n  SimpleTableAttributeMap get columnBoldAttributes {\n    final rawColumnBoldAttributes =\n        parentTableNode?.attributes[SimpleTableBlockKeys.columnBoldAttributes];\n    if (rawColumnBoldAttributes == null) {\n      return SimpleTableAttributeMap();\n    }\n    try {\n      return SimpleTableAttributeMap.from(rawColumnBoldAttributes);\n    } catch (e) {\n      Log.warn('get column bold attributes: $e');\n      return SimpleTableAttributeMap();\n    }\n  }\n\n  /// Get the row bold attributes\n  SimpleTableAttributeMap get rowBoldAttributes {\n    final rawRowBoldAttributes =\n        parentTableNode?.attributes[SimpleTableBlockKeys.rowBoldAttributes];\n    if (rawRowBoldAttributes == null) {\n      return SimpleTableAttributeMap();\n    }\n    try {\n      return SimpleTableAttributeMap.from(rawRowBoldAttributes);\n    } catch (e) {\n      Log.warn('get row bold attributes: $e');\n      return SimpleTableAttributeMap();\n    }\n  }\n\n  /// Get the width of the table.\n  double get width {\n    double currentColumnWidth = 0;\n    for (var i = 0; i < columnLength; i++) {\n      final columnWidth =\n          columnWidths[i.toString()] ?? SimpleTableConstants.defaultColumnWidth;\n      currentColumnWidth += columnWidth;\n    }\n    return currentColumnWidth;\n  }\n\n  /// Get the previous cell in the same column. If the row index is 0, it will return the same cell.\n  Node? getPreviousCellInSameColumn() {\n    assert(type == SimpleTableCellBlockKeys.type);\n    final parentTableNode = this.parentTableNode;\n    if (parentTableNode == null) {\n      return null;\n    }\n\n    final columnIndex = this.columnIndex;\n    final rowIndex = this.rowIndex;\n\n    if (rowIndex == 0) {\n      return this;\n    }\n\n    final previousColumn = parentTableNode.children[rowIndex - 1];\n    final previousCell = previousColumn.children[columnIndex];\n    return previousCell;\n  }\n\n  /// Get the next cell in the same column. If the row index is the last row, it will return the same cell.\n  Node? getNextCellInSameColumn() {\n    assert(type == SimpleTableCellBlockKeys.type);\n    final parentTableNode = this.parentTableNode;\n    if (parentTableNode == null) {\n      return null;\n    }\n\n    final columnIndex = this.columnIndex;\n    final rowIndex = this.rowIndex;\n\n    if (rowIndex == parentTableNode.rowLength - 1) {\n      return this;\n    }\n\n    final nextColumn = parentTableNode.children[rowIndex + 1];\n    final nextCell = nextColumn.children[columnIndex];\n    return nextCell;\n  }\n\n  /// Get the right cell in the same row. If the column index is the last column, it will return the same cell.\n  Node? getNextCellInSameRow() {\n    assert(type == SimpleTableCellBlockKeys.type);\n    final parentTableNode = this.parentTableNode;\n    if (parentTableNode == null) {\n      return null;\n    }\n\n    final columnIndex = this.columnIndex;\n    final rowIndex = this.rowIndex;\n\n    // the last cell\n    if (columnIndex == parentTableNode.columnLength - 1 &&\n        rowIndex == parentTableNode.rowLength - 1) {\n      return this;\n    }\n\n    if (columnIndex == parentTableNode.columnLength - 1) {\n      final nextRow = parentTableNode.children[rowIndex + 1];\n      final nextCell = nextRow.children.first;\n      return nextCell;\n    }\n\n    final nextColumn = parentTableNode.children[rowIndex];\n    final nextCell = nextColumn.children[columnIndex + 1];\n    return nextCell;\n  }\n\n  /// Get the previous cell in the same row. If the column index is 0, it will return the same cell.\n  Node? getPreviousCellInSameRow() {\n    assert(type == SimpleTableCellBlockKeys.type);\n    final parentTableNode = this.parentTableNode;\n    if (parentTableNode == null) {\n      return null;\n    }\n\n    final columnIndex = this.columnIndex;\n    final rowIndex = this.rowIndex;\n\n    if (columnIndex == 0 && rowIndex == 0) {\n      return this;\n    }\n\n    if (columnIndex == 0) {\n      final previousRow = parentTableNode.children[rowIndex - 1];\n      final previousCell = previousRow.children.last;\n      return previousCell;\n    }\n\n    final previousColumn = parentTableNode.children[rowIndex];\n    final previousCell = previousColumn.children[columnIndex - 1];\n    return previousCell;\n  }\n\n  /// Get the previous focusable sibling.\n  ///\n  /// If the current node is the first child of its parent, it will return itself.\n  Node? getPreviousFocusableSibling() {\n    final parent = this.parent;\n    if (parent == null) {\n      return null;\n    }\n    final parentTableNode = this.parentTableNode;\n    if (parentTableNode == null) {\n      return null;\n    }\n    if (parentTableNode.path == [0]) {\n      return this;\n    }\n    final previous = parentTableNode.previous;\n    if (previous == null) {\n      return null;\n    }\n    var children = previous.children;\n    if (children.isEmpty) {\n      return previous;\n    }\n    while (children.isNotEmpty) {\n      children = children.last.children;\n    }\n    return children.lastWhere((c) => c.delta != null);\n  }\n\n  /// Get the next focusable sibling.\n  ///\n  /// If the current node is the last child of its parent, it will return itself.\n  Node? getNextFocusableSibling() {\n    final next = this.next;\n    if (next == null) {\n      return null;\n    }\n    return next;\n  }\n\n  /// Is the last cell in the table.\n  bool get isLastCellInTable {\n    return columnIndex + 1 == parentTableNode?.columnLength &&\n        rowIndex + 1 == parentTableNode?.rowLength;\n  }\n\n  /// Is the first cell in the table.\n  bool get isFirstCellInTable {\n    return columnIndex == 0 && rowIndex == 0;\n  }\n\n  /// Get the table cell node by the row index and column index.\n  ///\n  /// If the current node is not a table cell node, it will return null.\n  /// Or if the row index or column index is out of range, it will return null.\n  Node? getTableCellNode({\n    required int rowIndex,\n    required int columnIndex,\n  }) {\n    assert(type == SimpleTableBlockKeys.type);\n\n    if (type != SimpleTableBlockKeys.type) {\n      return null;\n    }\n\n    if (rowIndex < 0 || rowIndex >= rowLength) {\n      return null;\n    }\n\n    if (columnIndex < 0 || columnIndex >= columnLength) {\n      return null;\n    }\n\n    return children[rowIndex].children[columnIndex];\n  }\n\n  String? getTableCellContent({\n    required int rowIndex,\n    required int columnIndex,\n  }) {\n    final cell = getTableCellNode(rowIndex: rowIndex, columnIndex: columnIndex);\n    if (cell == null) {\n      return null;\n    }\n    final content = cell.children\n        .map((e) => e.delta?.toPlainText())\n        .where((e) => e != null)\n        .join('\\n');\n    return content;\n  }\n\n  /// Return the first empty row in the table from bottom to top.\n  ///\n  /// Example:\n  ///\n  /// | A | B | C |\n  /// |   |   |   |\n  /// | E | F | G |\n  /// | H | I | J |\n  /// |   |   |   | <--- The first empty row is the row at index 3.\n  /// |   |   |   |\n  ///\n  /// The first empty row is the row at index 3.\n  (int, Node)? getFirstEmptyRowFromBottom() {\n    assert(type == SimpleTableBlockKeys.type);\n\n    if (type != SimpleTableBlockKeys.type) {\n      return null;\n    }\n\n    (int, Node)? result;\n\n    for (var i = children.length - 1; i >= 0; i--) {\n      final row = children[i];\n\n      // Check if all cells in this row are empty\n      final hasContent = row.children.any((cell) {\n        final content = getTableCellContent(\n          rowIndex: i,\n          columnIndex: row.children.indexOf(cell),\n        );\n        return content != null && content.isNotEmpty;\n      });\n\n      if (!hasContent) {\n        if (result != null) {\n          final (index, _) = result;\n          if (i <= index) {\n            result = (i, row);\n          }\n        } else {\n          result = (i, row);\n        }\n      }\n    }\n\n    return result;\n  }\n\n  /// Return the first empty column in the table from right to left.\n  ///\n  /// Example:\n  ///                  ↓ The first empty column is the column at index 3.\n  /// | A | C |  | E |  |  |\n  /// | B | D |  | F |  |  |\n  ///\n  /// The first empty column is the column at index 3.\n  int? getFirstEmptyColumnFromRight() {\n    assert(type == SimpleTableBlockKeys.type);\n\n    if (type != SimpleTableBlockKeys.type) {\n      return null;\n    }\n\n    int? result;\n\n    for (var i = columnLength - 1; i >= 0; i--) {\n      bool hasContent = false;\n      for (var j = 0; j < rowLength; j++) {\n        final content = getTableCellContent(\n          rowIndex: j,\n          columnIndex: i,\n        );\n        if (content != null && content.isNotEmpty) {\n          hasContent = true;\n        }\n      }\n      if (!hasContent) {\n        if (result != null) {\n          final index = result;\n          if (i <= index) {\n            result = i;\n          }\n        } else {\n          result = i;\n        }\n      }\n    }\n\n    return result;\n  }\n\n  /// Get first focusable child in the table cell.\n  ///\n  /// If the current node is not a table cell node, it will return null.\n  Node? getFirstFocusableChild() {\n    if (children.isEmpty) {\n      return this;\n    }\n    return children.first.getFirstFocusableChild();\n  }\n\n  /// Get last focusable child in the table cell.\n  ///\n  /// If the current node is not a table cell node, it will return null.\n  Node? getLastFocusableChild() {\n    if (children.isEmpty) {\n      return this;\n    }\n    return children.last.getLastFocusableChild();\n  }\n\n  /// Get table align of column\n  ///\n  /// If one of the align is not same as the others, it will return TableAlign.left.\n  TableAlign get allColumnAlign {\n    final alignSet = columnAligns.values.toSet();\n    if (alignSet.length == 1) {\n      return TableAlign.fromString(alignSet.first);\n    }\n    return TableAlign.left;\n  }\n\n  /// Get table align of row\n  ///\n  /// If one of the align is not same as the others, it will return TableAlign.left.\n  TableAlign get allRowAlign {\n    final alignSet = rowAligns.values.toSet();\n    if (alignSet.length == 1) {\n      return TableAlign.fromString(alignSet.first);\n    }\n    return TableAlign.left;\n  }\n\n  /// Get table align of the table.\n  ///\n  /// If one of the align is not same as the others, it will return TableAlign.left.\n  TableAlign get tableAlign {\n    if (allColumnAlign != TableAlign.left) {\n      return allColumnAlign;\n    } else if (allRowAlign != TableAlign.left) {\n      return allRowAlign;\n    }\n    return TableAlign.left;\n  }\n}\n\nextension on Object {\n  double toDouble({double defaultValue = 0}) {\n    if (this is double) {\n      return this as double;\n    }\n    if (this is String) {\n      return double.tryParse(this as String) ?? defaultValue;\n    }\n    if (this is int) {\n      return (this as int).toDouble();\n    }\n    return defaultValue;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart",
    "content": "export 'simple_table_content_operation.dart';\nexport 'simple_table_delete_operation.dart';\nexport 'simple_table_duplicate_operation.dart';\nexport 'simple_table_header_operation.dart';\nexport 'simple_table_insert_operation.dart';\nexport 'simple_table_map_operation.dart';\nexport 'simple_table_node_extension.dart';\nexport 'simple_table_reorder_operation.dart';\nexport 'simple_table_style_operation.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_reorder_operation.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nextension SimpleTableReorderOperation on EditorState {\n  /// Reorder the column of the table.\n  ///\n  /// If the from index is equal to the to index, do nothing.\n  /// The node's type can be [SimpleTableCellBlockKeys.type] or [SimpleTableRowBlockKeys.type] or [SimpleTableBlockKeys.type].\n  Future<void> reorderColumn(\n    Node node, {\n    required int fromIndex,\n    required int toIndex,\n  }) async {\n    if (fromIndex == toIndex) {\n      return;\n    }\n\n    final tableNode = node.parentTableNode;\n\n    if (tableNode == null) {\n      assert(tableNode == null);\n      return;\n    }\n\n    final columnLength = tableNode.columnLength;\n    final rowLength = tableNode.rowLength;\n\n    if (fromIndex < 0 ||\n        fromIndex >= columnLength ||\n        toIndex < 0 ||\n        toIndex >= columnLength) {\n      Log.warn(\n        'reorder column: index out of range: fromIndex: $fromIndex, toIndex: $toIndex, column length: $columnLength',\n      );\n      return;\n    }\n\n    Log.info(\n      'reorder column in table ${node.id} at fromIndex: $fromIndex, toIndex: $toIndex, column length: $columnLength, row length: $rowLength',\n    );\n\n    final attributes = tableNode.mapTableAttributes(\n      tableNode,\n      type: TableMapOperationType.reorderColumn,\n      index: fromIndex,\n      toIndex: toIndex,\n    );\n\n    final transaction = this.transaction;\n    for (var i = 0; i < rowLength; i++) {\n      final row = tableNode.children[i];\n      final from = row.children[fromIndex];\n      final to = row.children[toIndex];\n      final path = fromIndex < toIndex ? to.path.next : to.path;\n      transaction.insertNode(path, from.deepCopy());\n      transaction.deleteNode(from);\n    }\n    if (attributes != null) {\n      transaction.updateNode(tableNode, attributes);\n    }\n    await apply(transaction);\n  }\n\n  /// Reorder the row of the table.\n  ///\n  /// If the from index is equal to the to index, do nothing.\n  /// The node's type can be [SimpleTableCellBlockKeys.type] or [SimpleTableRowBlockKeys.type] or [SimpleTableBlockKeys.type].\n  Future<void> reorderRow(\n    Node node, {\n    required int fromIndex,\n    required int toIndex,\n  }) async {\n    if (fromIndex == toIndex) {\n      return;\n    }\n\n    final tableNode = node.parentTableNode;\n\n    if (tableNode == null) {\n      assert(tableNode == null);\n      return;\n    }\n\n    final columnLength = tableNode.columnLength;\n    final rowLength = tableNode.rowLength;\n\n    if (fromIndex < 0 ||\n        fromIndex >= rowLength ||\n        toIndex < 0 ||\n        toIndex >= rowLength) {\n      Log.warn(\n        'reorder row: index out of range: fromIndex: $fromIndex, toIndex: $toIndex, row length: $rowLength',\n      );\n      return;\n    }\n\n    Log.info(\n      'reorder row in table ${node.id} at fromIndex: $fromIndex, toIndex: $toIndex, column length: $columnLength, row length: $rowLength',\n    );\n\n    final attributes = tableNode.mapTableAttributes(\n      tableNode,\n      type: TableMapOperationType.reorderRow,\n      index: fromIndex,\n      toIndex: toIndex,\n    );\n\n    final transaction = this.transaction;\n    final from = tableNode.children[fromIndex];\n    final to = tableNode.children[toIndex];\n    final path = fromIndex < toIndex ? to.path.next : to.path;\n    transaction.insertNode(path, from.deepCopy());\n    transaction.deleteNode(from);\n    if (attributes != null) {\n      transaction.updateNode(tableNode, attributes);\n    }\n    await apply(transaction);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_style_operation.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nextension TableOptionOperation on EditorState {\n  /// Update the column width of the table in memory. Call this function when dragging the table column.\n  ///\n  /// The deltaX is the change of the column width.\n  Future<void> updateColumnWidthInMemory({\n    required Node tableCellNode,\n    required double deltaX,\n  }) async {\n    assert(tableCellNode.type == SimpleTableCellBlockKeys.type);\n\n    if (tableCellNode.type != SimpleTableCellBlockKeys.type) {\n      return;\n    }\n\n    // when dragging the table column, we need to update the column width in memory.\n    // so that the table can render the column with the new width.\n    // but don't need to persist to the database immediately.\n    // only persist to the database when the drag is completed.\n    final columnIndex = tableCellNode.columnIndex;\n    final parentTableNode = tableCellNode.parentTableNode;\n    if (parentTableNode == null) {\n      Log.warn('parent table node is null');\n      return;\n    }\n\n    final width = tableCellNode.columnWidth + deltaX;\n\n    try {\n      final columnWidths =\n          parentTableNode.attributes[SimpleTableBlockKeys.columnWidths] ??\n              SimpleTableColumnWidthMap();\n      final newAttributes = {\n        ...parentTableNode.attributes,\n        SimpleTableBlockKeys.columnWidths: {\n          ...columnWidths,\n          columnIndex.toString(): width.clamp(\n            SimpleTableConstants.minimumColumnWidth,\n            double.infinity,\n          ),\n        },\n      };\n\n      parentTableNode.updateAttributes(newAttributes);\n    } catch (e) {\n      Log.warn('update column width in memory: $e');\n    }\n  }\n\n  /// Update the column width of the table. Call this function after the drag is completed.\n  Future<void> updateColumnWidth({\n    required Node tableCellNode,\n    required double width,\n  }) async {\n    assert(tableCellNode.type == SimpleTableCellBlockKeys.type);\n\n    if (tableCellNode.type != SimpleTableCellBlockKeys.type) {\n      return;\n    }\n\n    final columnIndex = tableCellNode.columnIndex;\n    final parentTableNode = tableCellNode.parentTableNode;\n    if (parentTableNode == null) {\n      Log.warn('parent table node is null');\n      return;\n    }\n\n    final transaction = this.transaction;\n    final columnWidths =\n        parentTableNode.attributes[SimpleTableBlockKeys.columnWidths] ??\n            SimpleTableColumnWidthMap();\n    transaction.updateNode(parentTableNode, {\n      SimpleTableBlockKeys.columnWidths: {\n        ...columnWidths,\n        columnIndex.toString(): width.clamp(\n          SimpleTableConstants.minimumColumnWidth,\n          double.infinity,\n        ),\n      },\n      // reset the distribute column widths evenly flag\n      SimpleTableBlockKeys.distributeColumnWidthsEvenly: false,\n    });\n    await apply(transaction);\n  }\n\n  /// Update the align of the column at the index where the table cell node is located.\n  ///\n  /// Before:\n  /// Given table cell node:\n  /// Row 1: | 0 | 1 |\n  /// Row 2: |2  |3  | ← This column will be updated\n  ///\n  /// Call this function will update the align of the column where the table cell node is located.\n  ///\n  /// After:\n  /// Row 1: | 0 | 1 |\n  /// Row 2: | 2 | 3 | ← This column is updated, texts are aligned to the center\n  Future<void> updateColumnAlign({\n    required Node tableCellNode,\n    required TableAlign align,\n  }) async {\n    await clearColumnTextAlign(tableCellNode: tableCellNode);\n\n    final columnIndex = tableCellNode.columnIndex;\n    await _updateTableAttributes(\n      tableCellNode: tableCellNode,\n      attributeKey: SimpleTableBlockKeys.columnAligns,\n      source: tableCellNode.columnAligns,\n      duplicatedEntry: MapEntry(columnIndex.toString(), align.key),\n    );\n  }\n\n  /// Update the align of the row at the index where the table cell node is located.\n  ///\n  /// Before:\n  /// Given table cell node:\n  ///              ↓ This row will be updated\n  /// Row 1: | 0 |1  |\n  /// Row 2: | 2 |3  |\n  ///\n  /// Call this function will update the align of the row where the table cell node is located.\n  ///\n  /// After:\n  ///              ↓ This row is updated, texts are aligned to the center\n  /// Row 1: | 0 | 1 |\n  /// Row 2: | 2 | 3 |\n  Future<void> updateRowAlign({\n    required Node tableCellNode,\n    required TableAlign align,\n  }) async {\n    await clearRowTextAlign(tableCellNode: tableCellNode);\n\n    final rowIndex = tableCellNode.rowIndex;\n    await _updateTableAttributes(\n      tableCellNode: tableCellNode,\n      attributeKey: SimpleTableBlockKeys.rowAligns,\n      source: tableCellNode.rowAligns,\n      duplicatedEntry: MapEntry(rowIndex.toString(), align.key),\n    );\n  }\n\n  /// Update the align of the table.\n  ///\n  /// This function will update the align of the table.\n  ///\n  /// The align is the align to be updated.\n  Future<void> updateTableAlign({\n    required Node tableNode,\n    required TableAlign align,\n  }) async {\n    assert(tableNode.type == SimpleTableBlockKeys.type);\n\n    if (tableNode.type != SimpleTableBlockKeys.type) {\n      return;\n    }\n\n    final transaction = this.transaction;\n    Attributes attributes = tableNode.attributes;\n    for (var i = 0; i < tableNode.columnLength; i++) {\n      attributes = attributes.mergeValues(\n        SimpleTableBlockKeys.columnAligns,\n        attributes[SimpleTableBlockKeys.columnAligns] ??\n            SimpleTableColumnAlignMap(),\n        duplicatedEntry: MapEntry(i.toString(), align.key),\n      );\n    }\n    transaction.updateNode(tableNode, attributes);\n    await apply(transaction);\n  }\n\n  /// Update the background color of the column at the index where the table cell node is located.\n  Future<void> updateColumnBackgroundColor({\n    required Node tableCellNode,\n    required String color,\n  }) async {\n    final columnIndex = tableCellNode.columnIndex;\n    await _updateTableAttributes(\n      tableCellNode: tableCellNode,\n      attributeKey: SimpleTableBlockKeys.columnColors,\n      source: tableCellNode.columnColors,\n      duplicatedEntry: MapEntry(columnIndex.toString(), color),\n    );\n  }\n\n  /// Update the background color of the row at the index where the table cell node is located.\n  Future<void> updateRowBackgroundColor({\n    required Node tableCellNode,\n    required String color,\n  }) async {\n    final rowIndex = tableCellNode.rowIndex;\n    await _updateTableAttributes(\n      tableCellNode: tableCellNode,\n      attributeKey: SimpleTableBlockKeys.rowColors,\n      source: tableCellNode.rowColors,\n      duplicatedEntry: MapEntry(rowIndex.toString(), color),\n    );\n  }\n\n  /// Set the column width of the table to the page width.\n  ///\n  /// Example:\n  ///\n  /// Before:\n  /// | 0 |   1   |\n  /// | 3 |   4   |\n  ///\n  /// After:\n  /// |  0  |    1    | <- the column's width will be expanded based on the percentage of the page width\n  /// |  3  |    4    |\n  ///\n  /// This function will update the table width.\n  Future<void> setColumnWidthToPageWidth({\n    required Node tableNode,\n  }) async {\n    final columnLength = tableNode.columnLength;\n    double? pageWidth = tableNode.renderBox?.size.width;\n    if (pageWidth == null) {\n      Log.warn('table node render box is null');\n      return;\n    }\n    pageWidth -= SimpleTableConstants.tablePageOffset;\n\n    final transaction = this.transaction;\n    final columnWidths = tableNode.columnWidths;\n    final ratio = pageWidth / tableNode.width;\n    for (var i = 0; i < columnLength; i++) {\n      final columnWidth =\n          columnWidths[i.toString()] ?? SimpleTableConstants.defaultColumnWidth;\n      columnWidths[i.toString()] = (columnWidth * ratio).clamp(\n        SimpleTableConstants.minimumColumnWidth,\n        double.infinity,\n      );\n    }\n    transaction.updateNode(tableNode, {\n      SimpleTableBlockKeys.columnWidths: columnWidths,\n      SimpleTableBlockKeys.distributeColumnWidthsEvenly: false,\n    });\n    await apply(transaction);\n  }\n\n  /// Distribute the column width of the table to the page width.\n  ///\n  /// Example:\n  ///\n  /// Before:\n  /// Before:\n  /// | 0 |   1   |\n  /// | 3 |   4   |\n  ///\n  /// After:\n  /// |  0  |  1  | <- the column's width will be expanded based on the percentage of the page width\n  /// |  3  |  4  |\n  ///\n  /// This function will not update table width.\n  Future<void> distributeColumnWidthToPageWidth({\n    required Node tableNode,\n  }) async {\n    // Disable in mobile\n    if (UniversalPlatform.isMobile) {\n      return;\n    }\n\n    final columnLength = tableNode.columnLength;\n    final tableWidth = tableNode.width;\n    final columnWidth = (tableWidth / columnLength).clamp(\n      SimpleTableConstants.minimumColumnWidth,\n      double.infinity,\n    );\n    final transaction = this.transaction;\n    final columnWidths = tableNode.columnWidths;\n    for (var i = 0; i < columnLength; i++) {\n      columnWidths[i.toString()] = columnWidth;\n    }\n    transaction.updateNode(tableNode, {\n      SimpleTableBlockKeys.columnWidths: columnWidths,\n      SimpleTableBlockKeys.distributeColumnWidthsEvenly: true,\n    });\n    await apply(transaction);\n  }\n\n  /// Update the bold attribute of the column\n  Future<void> toggleColumnBoldAttribute({\n    required Node tableCellNode,\n    required bool isBold,\n  }) async {\n    final columnIndex = tableCellNode.columnIndex;\n    await _updateTableAttributes(\n      tableCellNode: tableCellNode,\n      attributeKey: SimpleTableBlockKeys.columnBoldAttributes,\n      source: tableCellNode.columnBoldAttributes,\n      duplicatedEntry: MapEntry(columnIndex.toString(), isBold),\n    );\n  }\n\n  /// Update the bold attribute of the row\n  Future<void> toggleRowBoldAttribute({\n    required Node tableCellNode,\n    required bool isBold,\n  }) async {\n    final rowIndex = tableCellNode.rowIndex;\n    await _updateTableAttributes(\n      tableCellNode: tableCellNode,\n      attributeKey: SimpleTableBlockKeys.rowBoldAttributes,\n      source: tableCellNode.rowBoldAttributes,\n      duplicatedEntry: MapEntry(rowIndex.toString(), isBold),\n    );\n  }\n\n  /// Update the text color of the column\n  Future<void> updateColumnTextColor({\n    required Node tableCellNode,\n    required String color,\n  }) async {\n    final columnIndex = tableCellNode.columnIndex;\n    await _updateTableAttributes(\n      tableCellNode: tableCellNode,\n      attributeKey: SimpleTableBlockKeys.columnTextColors,\n      source: tableCellNode.columnTextColors,\n      duplicatedEntry: MapEntry(columnIndex.toString(), color),\n    );\n  }\n\n  /// Update the text color of the row\n  Future<void> updateRowTextColor({\n    required Node tableCellNode,\n    required String color,\n  }) async {\n    final rowIndex = tableCellNode.rowIndex;\n    await _updateTableAttributes(\n      tableCellNode: tableCellNode,\n      attributeKey: SimpleTableBlockKeys.rowTextColors,\n      source: tableCellNode.rowTextColors,\n      duplicatedEntry: MapEntry(rowIndex.toString(), color),\n    );\n  }\n\n  /// Update the attributes of the table.\n  ///\n  /// This function is used to update the attributes of the table.\n  ///\n  /// The attribute key is the key of the attribute to be updated.\n  /// The source is the original value of the attribute.\n  /// The duplicatedEntry is the entry of the attribute to be updated.\n  Future<void> _updateTableAttributes({\n    required Node tableCellNode,\n    required String attributeKey,\n    required Map<String, dynamic> source,\n    MapEntry? duplicatedEntry,\n  }) async {\n    assert(tableCellNode.type == SimpleTableCellBlockKeys.type);\n\n    final parentTableNode = tableCellNode.parentTableNode;\n\n    if (parentTableNode == null) {\n      Log.warn('parent table node is null');\n      return;\n    }\n\n    final columnIndex = tableCellNode.columnIndex;\n\n    Log.info(\n      'update $attributeKey: $source at column $columnIndex in table ${parentTableNode.id}',\n    );\n\n    final transaction = this.transaction;\n    final attributes = parentTableNode.attributes.mergeValues(\n      attributeKey,\n      source,\n      duplicatedEntry: duplicatedEntry,\n    );\n    transaction.updateNode(parentTableNode, attributes);\n    await apply(transaction);\n  }\n\n  /// Clear the text align of the column at the index where the table cell node is located.\n  Future<void> clearColumnTextAlign({\n    required Node tableCellNode,\n  }) async {\n    final parentTableNode = tableCellNode.parentTableNode;\n    if (parentTableNode == null) {\n      Log.warn('parent table node is null');\n      return;\n    }\n    final columnIndex = tableCellNode.columnIndex;\n    final transaction = this.transaction;\n    for (var i = 0; i < parentTableNode.rowLength; i++) {\n      final cell = parentTableNode.getTableCellNode(\n        rowIndex: i,\n        columnIndex: columnIndex,\n      );\n      if (cell == null) {\n        continue;\n      }\n      for (final child in cell.children) {\n        transaction.updateNode(child, {\n          blockComponentAlign: null,\n        });\n      }\n    }\n    if (transaction.operations.isNotEmpty) {\n      await apply(transaction);\n    }\n  }\n\n  /// Clear the text align of the row at the index where the table cell node is located.\n  Future<void> clearRowTextAlign({\n    required Node tableCellNode,\n  }) async {\n    final parentTableNode = tableCellNode.parentTableNode;\n    if (parentTableNode == null) {\n      Log.warn('parent table node is null');\n      return;\n    }\n    final rowIndex = tableCellNode.rowIndex;\n    final transaction = this.transaction;\n    for (var i = 0; i < parentTableNode.columnLength; i++) {\n      final cell = parentTableNode.getTableCellNode(\n        rowIndex: rowIndex,\n        columnIndex: i,\n      );\n      if (cell == null) {\n        continue;\n      }\n      for (final child in cell.children) {\n        transaction.updateNode(\n          child,\n          {\n            blockComponentAlign: null,\n          },\n        );\n      }\n    }\n    if (transaction.operations.isNotEmpty) {\n      await apply(transaction);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass SimpleTableRowBlockKeys {\n  const SimpleTableRowBlockKeys._();\n\n  static const String type = 'simple_table_row';\n}\n\nNode simpleTableRowBlockNode({\n  List<Node> children = const [],\n}) {\n  return Node(\n    type: SimpleTableRowBlockKeys.type,\n    children: children,\n  );\n}\n\nclass SimpleTableRowBlockComponentBuilder extends BlockComponentBuilder {\n  SimpleTableRowBlockComponentBuilder({\n    super.configuration,\n    this.alwaysDistributeColumnWidths = false,\n  });\n\n  final bool alwaysDistributeColumnWidths;\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    return SimpleTableRowBlockWidget(\n      key: node.key,\n      node: node,\n      configuration: configuration,\n      alwaysDistributeColumnWidths: alwaysDistributeColumnWidths,\n      showActions: showActions(node),\n      actionBuilder: (context, state) => actionBuilder(\n        blockComponentContext,\n        state,\n      ),\n      actionTrailingBuilder: (context, state) => actionTrailingBuilder(\n        blockComponentContext,\n        state,\n      ),\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate => (_) => true;\n}\n\nclass SimpleTableRowBlockWidget extends BlockComponentStatefulWidget {\n  const SimpleTableRowBlockWidget({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n    required this.alwaysDistributeColumnWidths,\n  });\n\n  final bool alwaysDistributeColumnWidths;\n\n  @override\n  State<SimpleTableRowBlockWidget> createState() =>\n      _SimpleTableRowBlockWidgetState();\n}\n\nclass _SimpleTableRowBlockWidgetState extends State<SimpleTableRowBlockWidget>\n    with\n        BlockComponentConfigurable,\n        BlockComponentTextDirectionMixin,\n        BlockComponentBackgroundColorMixin {\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  @override\n  Node get node => widget.node;\n\n  @override\n  late EditorState editorState = context.read<EditorState>();\n\n  @override\n  Widget build(BuildContext context) {\n    if (node.children.isEmpty) {\n      return const SizedBox.shrink();\n    }\n\n    return IntrinsicHeight(\n      child: Row(\n        crossAxisAlignment: CrossAxisAlignment.stretch,\n        children: _buildCells(),\n      ),\n    );\n  }\n\n  List<Widget> _buildCells() {\n    final List<Widget> cells = [];\n\n    for (var i = 0; i < node.children.length; i++) {\n      // border\n      if (i == 0 &&\n          SimpleTableConstants.borderType ==\n              SimpleTableBorderRenderType.table) {\n        cells.add(const SimpleTableRowDivider());\n      }\n\n      final child = editorState.renderer.build(context, node.children[i]);\n      cells.add(\n        widget.alwaysDistributeColumnWidths ? Flexible(child: child) : child,\n      );\n\n      // border\n      if (SimpleTableConstants.borderType ==\n          SimpleTableBorderRenderType.table) {\n        cells.add(const SimpleTableRowDivider());\n      }\n    }\n\n    return cells;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_down_command.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_navigation_command.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/material.dart';\n\nfinal CommandShortcutEvent arrowDownInTableCell = CommandShortcutEvent(\n  key: 'Press arrow down in table cell',\n  getDescription: () =>\n      AppFlowyEditorL10n.current.cmdTableMoveToDownCellAtSameOffset,\n  command: 'arrow down',\n  handler: _arrowDownInTableCellHandler,\n);\n\n/// Move the selection to the next cell in the same column.\n///\n/// Only handle the case when the selection is in the first line of the cell.\nKeyEventResult _arrowDownInTableCellHandler(EditorState editorState) {\n  final (isInTableCell, selection, tableCellNode, node) =\n      editorState.isCurrentSelectionInTableCell();\n  if (!isInTableCell ||\n      selection == null ||\n      tableCellNode == null ||\n      node == null) {\n    return KeyEventResult.ignored;\n  }\n\n  final isInLastLine = node.path.last + 1 == node.parent?.children.length;\n  if (!isInLastLine) {\n    return KeyEventResult.ignored;\n  }\n\n  Selection? newSelection = editorState.selection;\n  final rowIndex = tableCellNode.rowIndex;\n  final parentTableNode = tableCellNode.parentTableNode;\n  if (parentTableNode == null) {\n    return KeyEventResult.ignored;\n  }\n\n  if (rowIndex == parentTableNode.rowLength - 1) {\n    // focus on the next block\n    final nextNode = tableCellNode.next;\n    final nextBlock = tableCellNode.parentTableNode?.next;\n    if (nextNode != null) {\n      final nextFocusableSibling = parentTableNode.getNextFocusableSibling();\n      if (nextFocusableSibling != null) {\n        final length = nextFocusableSibling.delta?.length ?? 0;\n        newSelection = Selection.collapsed(\n          Position(\n            path: nextFocusableSibling.path,\n            offset: length,\n          ),\n        );\n      }\n    } else if (nextBlock != null) {\n      if (nextBlock.type != SimpleTableBlockKeys.type) {\n        newSelection = Selection.collapsed(\n          Position(\n            path: nextBlock.path,\n          ),\n        );\n      } else {\n        return tableNavigationArrowDownCommand.handler(editorState);\n      }\n    }\n  } else {\n    // focus on next cell in the same column\n    final nextCell = tableCellNode.getNextCellInSameColumn();\n    if (nextCell != null) {\n      final offset = selection.end.offset;\n      // get the first children of the next cell\n      final firstChild = nextCell.children.firstWhereOrNull(\n        (c) => c.delta != null,\n      );\n      if (firstChild != null) {\n        final length = firstChild.delta?.length ?? 0;\n        newSelection = Selection.collapsed(\n          Position(\n            path: firstChild.path,\n            offset: offset.clamp(0, length),\n          ),\n        );\n      }\n    }\n  }\n\n  if (newSelection != null) {\n    editorState.updateSelectionWithReason(newSelection);\n  }\n\n  return KeyEventResult.handled;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_left_command.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nfinal CommandShortcutEvent arrowLeftInTableCell = CommandShortcutEvent(\n  key: 'Press arrow left in table cell',\n  getDescription: () => AppFlowyEditorL10n\n      .current.cmdTableMoveToRightCellIfItsAtTheEndOfCurrentCell,\n  command: 'arrow left',\n  handler: (editorState) => editorState.moveToPreviousCell(\n    editorState,\n    (result) {\n      // only handle the case when the selection is at the beginning of the cell\n      if (0 != result.$2?.end.offset) {\n        return false;\n      }\n      return true;\n    },\n  ),\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_right_command.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nfinal CommandShortcutEvent arrowRightInTableCell = CommandShortcutEvent(\n  key: 'Press arrow right in table cell',\n  getDescription: () => AppFlowyEditorL10n\n      .current.cmdTableMoveToRightCellIfItsAtTheEndOfCurrentCell,\n  command: 'arrow right',\n  handler: (editorState) => editorState.moveToNextCell(\n    editorState,\n    (result) {\n      // only handle the case when the selection is at the end of the cell\n      final node = result.$4;\n      final length = node?.delta?.length ?? 0;\n      final selection = result.$2;\n      return selection?.end.offset == length;\n    },\n  ),\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_up_command.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/material.dart';\n\nfinal CommandShortcutEvent arrowUpInTableCell = CommandShortcutEvent(\n  key: 'Press arrow up in table cell',\n  getDescription: () =>\n      AppFlowyEditorL10n.current.cmdTableMoveToUpCellAtSameOffset,\n  command: 'arrow up',\n  handler: _arrowUpInTableCellHandler,\n);\n\n/// Move the selection to the previous cell in the same column.\n///\n/// Only handle the case when the selection is in the first line of the cell.\nKeyEventResult _arrowUpInTableCellHandler(EditorState editorState) {\n  final (isInTableCell, selection, tableCellNode, node) =\n      editorState.isCurrentSelectionInTableCell();\n  if (!isInTableCell ||\n      selection == null ||\n      tableCellNode == null ||\n      node == null) {\n    return KeyEventResult.ignored;\n  }\n\n  final isInFirstLine = node.path.last == 0;\n  if (!isInFirstLine) {\n    return KeyEventResult.ignored;\n  }\n\n  Selection? newSelection = editorState.selection;\n  final rowIndex = tableCellNode.rowIndex;\n  if (rowIndex == 0) {\n    // focus on the previous block\n    final previousNode = tableCellNode.parentTableNode;\n    if (previousNode != null) {\n      final previousFocusableSibling =\n          previousNode.getPreviousFocusableSibling();\n      if (previousFocusableSibling != null) {\n        final length = previousFocusableSibling.delta?.length ?? 0;\n        newSelection = Selection.collapsed(\n          Position(\n            path: previousFocusableSibling.path,\n            offset: length,\n          ),\n        );\n      }\n    }\n  } else {\n    // focus on previous cell in the same column\n    final previousCell = tableCellNode.getPreviousCellInSameColumn();\n    if (previousCell != null) {\n      final offset = selection.end.offset;\n      // get the last children of the previous cell\n      final lastChild = previousCell.children.lastWhereOrNull(\n        (c) => c.delta != null,\n      );\n      if (lastChild != null) {\n        final length = lastChild.delta?.length ?? 0;\n        newSelection = Selection.collapsed(\n          Position(\n            path: lastChild.path,\n            offset: offset.clamp(0, length),\n          ),\n        );\n      }\n    }\n  }\n\n  if (newSelection != null) {\n    editorState.updateSelectionWithReason(newSelection);\n  }\n\n  return KeyEventResult.handled;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_backspace_command.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:flutter/material.dart';\n\nfinal CommandShortcutEvent backspaceInTableCell = CommandShortcutEvent(\n  key: 'Press backspace in table cell',\n  getDescription: () => 'Ignore the backspace key in table cell',\n  command: 'backspace',\n  handler: _backspaceInTableCellHandler,\n);\n\nKeyEventResult _backspaceInTableCellHandler(EditorState editorState) {\n  final (isInTableCell, selection, tableCellNode, node) =\n      editorState.isCurrentSelectionInTableCell();\n  if (!isInTableCell ||\n      selection == null ||\n      tableCellNode == null ||\n      node == null) {\n    return KeyEventResult.ignored;\n  }\n\n  final onlyContainsOneChild = tableCellNode.children.length == 1;\n  final firstChild = tableCellNode.children.first;\n  final isParagraphNode = firstChild.type == ParagraphBlockKeys.type;\n  final isCodeBlock = firstChild.type == CodeBlockKeys.type;\n  if (onlyContainsOneChild &&\n      selection.isCollapsed &&\n      selection.end.offset == 0) {\n    if (isParagraphNode && firstChild.children.isEmpty) {\n      return KeyEventResult.skipRemainingHandlers;\n    } else if (isCodeBlock) {\n      // replace the codeblock with a paragraph\n      final transaction = editorState.transaction;\n      transaction.insertNode(node.path, paragraphNode());\n      transaction.deleteNode(node);\n      transaction.afterSelection = Selection.collapsed(\n        Position(\n          path: node.path,\n        ),\n      );\n      editorState.apply(transaction);\n      return KeyEventResult.handled;\n    }\n  }\n\n  return KeyEventResult.ignored;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/material.dart';\n\ntypedef IsInTableCellResult = (\n  bool isInTableCell,\n  Selection? selection,\n  Node? tableCellNode,\n  Node? node,\n);\n\nextension TableCommandExtension on EditorState {\n  /// Return a tuple, the first element is a boolean indicating whether the current selection is in a table cell,\n  /// the second element is the node that is the parent of the table cell if the current selection is in a table cell,\n  /// otherwise it is null.\n  /// The third element is the node that is the current selection.\n  IsInTableCellResult isCurrentSelectionInTableCell() {\n    final selection = this.selection;\n    if (selection == null) {\n      return (false, null, null, null);\n    }\n\n    if (selection.isCollapsed) {\n      // if the selection is collapsed, check if the node is in a table cell\n      final node = document.nodeAtPath(selection.end.path);\n      final tableCellParent = node?.findParent(\n        (node) => node.type == SimpleTableCellBlockKeys.type,\n      );\n      final isInTableCell = tableCellParent != null;\n      return (isInTableCell, selection, tableCellParent, node);\n    } else {\n      // if the selection is not collapsed, check if the start and end nodes are in a table cell\n      final startNode = document.nodeAtPath(selection.start.path);\n      final endNode = document.nodeAtPath(selection.end.path);\n      final startNodeInTableCell = startNode?.findParent(\n        (node) => node.type == SimpleTableCellBlockKeys.type,\n      );\n      final endNodeInTableCell = endNode?.findParent(\n        (node) => node.type == SimpleTableCellBlockKeys.type,\n      );\n      final isInSameTableCell = startNodeInTableCell != null &&\n          endNodeInTableCell != null &&\n          startNodeInTableCell.path.equals(endNodeInTableCell.path);\n      return (isInSameTableCell, selection, startNodeInTableCell, endNode);\n    }\n  }\n\n  /// Move the selection to the previous cell\n  KeyEventResult moveToPreviousCell(\n    EditorState editorState,\n    bool Function(IsInTableCellResult result) shouldHandle,\n  ) {\n    final (isInTableCell, selection, tableCellNode, node) =\n        editorState.isCurrentSelectionInTableCell();\n    if (!isInTableCell ||\n        selection == null ||\n        tableCellNode == null ||\n        node == null) {\n      return KeyEventResult.ignored;\n    }\n\n    if (!shouldHandle((isInTableCell, selection, tableCellNode, node))) {\n      return KeyEventResult.ignored;\n    }\n\n    if (isOutdentable(editorState)) {\n      return outdentCommand.execute(editorState);\n    }\n\n    Selection? newSelection;\n\n    final previousCell = tableCellNode.getPreviousCellInSameRow();\n    if (previousCell != null && !previousCell.path.equals(tableCellNode.path)) {\n      // get the last children of the previous cell\n      final lastChild = previousCell.children.lastWhereOrNull(\n        (c) => c.delta != null,\n      );\n      if (lastChild != null) {\n        newSelection = Selection.collapsed(\n          Position(\n            path: lastChild.path,\n            offset: lastChild.delta?.length ?? 0,\n          ),\n        );\n      }\n    } else {\n      // focus on the previous block\n      final previousNode = tableCellNode.parentTableNode;\n      if (previousNode != null) {\n        final previousFocusableSibling =\n            previousNode.getPreviousFocusableSibling();\n        if (previousFocusableSibling != null) {\n          final length = previousFocusableSibling.delta?.length ?? 0;\n          newSelection = Selection.collapsed(\n            Position(\n              path: previousFocusableSibling.path,\n              offset: length,\n            ),\n          );\n        }\n      }\n    }\n\n    if (newSelection != null) {\n      editorState.updateSelectionWithReason(newSelection);\n    }\n\n    return KeyEventResult.handled;\n  }\n\n  /// Move the selection to the next cell\n  KeyEventResult moveToNextCell(\n    EditorState editorState,\n    bool Function(IsInTableCellResult result) shouldHandle,\n  ) {\n    final (isInTableCell, selection, tableCellNode, node) =\n        editorState.isCurrentSelectionInTableCell();\n    if (!isInTableCell ||\n        selection == null ||\n        tableCellNode == null ||\n        node == null) {\n      return KeyEventResult.ignored;\n    }\n\n    if (!shouldHandle((isInTableCell, selection, tableCellNode, node))) {\n      return KeyEventResult.ignored;\n    }\n\n    Selection? newSelection;\n\n    if (isIndentable(editorState)) {\n      return indentCommand.execute(editorState);\n    }\n\n    final nextCell = tableCellNode.getNextCellInSameRow();\n    if (nextCell != null && !nextCell.path.equals(tableCellNode.path)) {\n      // get the first children of the next cell\n      final firstChild = nextCell.children.firstWhereOrNull(\n        (c) => c.delta != null,\n      );\n      if (firstChild != null) {\n        newSelection = Selection.collapsed(\n          Position(\n            path: firstChild.path,\n          ),\n        );\n      }\n    } else {\n      // focus on the previous block\n      final nextNode = tableCellNode.parentTableNode;\n      if (nextNode != null) {\n        final nextFocusableSibling = nextNode.getNextFocusableSibling();\n        nextNode.getNextFocusableSibling();\n        if (nextFocusableSibling != null) {\n          newSelection = Selection.collapsed(\n            Position(\n              path: nextFocusableSibling.path,\n            ),\n          );\n        }\n      }\n    }\n\n    if (newSelection != null) {\n      editorState.updateSelectionWithReason(newSelection);\n    }\n\n    return KeyEventResult.handled;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_commands.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_down_command.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_left_command.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_right_command.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_up_command.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_backspace_command.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_enter_command.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_navigation_command.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_select_all_command.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_tab_command.dart';\n\nfinal simpleTableCommands = [\n  tableNavigationArrowDownCommand,\n  arrowUpInTableCell,\n  arrowDownInTableCell,\n  arrowLeftInTableCell,\n  arrowRightInTableCell,\n  tabInTableCell,\n  shiftTabInTableCell,\n  backspaceInTableCell,\n  selectAllInTableCellCommand,\n  enterInTableCell,\n];\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_enter_command.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nfinal CommandShortcutEvent enterInTableCell = CommandShortcutEvent(\n  key: 'Press enter in table cell',\n  getDescription: () => 'Press the enter key in table cell',\n  command: 'enter',\n  handler: _enterInTableCellHandler,\n);\n\nKeyEventResult _enterInTableCellHandler(EditorState editorState) {\n  final (isInTableCell, selection, tableCellNode, node) =\n      editorState.isCurrentSelectionInTableCell();\n  if (!isInTableCell ||\n      selection == null ||\n      tableCellNode == null ||\n      node == null ||\n      !selection.isCollapsed) {\n    return KeyEventResult.ignored;\n  }\n\n  // check if the shift key is pressed, if so, we should return false to let the system handle it.\n  final isShiftPressed = HardwareKeyboard.instance.isShiftPressed;\n  if (isShiftPressed) {\n    return KeyEventResult.ignored;\n  }\n\n  final delta = node.delta;\n  if (!indentableBlockTypes.contains(node.type) || delta == null) {\n    return KeyEventResult.ignored;\n  }\n\n  if (selection.startIndex == 0 && delta.isEmpty) {\n    // clear the style\n    if (node.parent?.type != SimpleTableCellBlockKeys.type) {\n      if (outdentCommand.execute(editorState) == KeyEventResult.handled) {\n        return KeyEventResult.handled;\n      }\n    }\n    if (node.type != CalloutBlockKeys.type) {\n      return convertToParagraphCommand.execute(editorState);\n    }\n  }\n\n  return KeyEventResult.ignored;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_navigation_command.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nfinal CommandShortcutEvent tableNavigationArrowDownCommand =\n    CommandShortcutEvent(\n  key: 'table navigation',\n  getDescription: () => 'table navigation',\n  command: 'arrow down',\n  handler: _tableNavigationArrowDownHandler,\n);\n\nKeyEventResult _tableNavigationArrowDownHandler(EditorState editorState) {\n  final selection = editorState.selection;\n  if (selection == null || !selection.isCollapsed) {\n    return KeyEventResult.ignored;\n  }\n\n  final nextNode = editorState.getNodeAtPath(selection.start.path.next);\n  if (nextNode == null) {\n    return KeyEventResult.ignored;\n  }\n\n  if (nextNode.type == SimpleTableBlockKeys.type) {\n    final firstCell = nextNode.getTableCellNode(rowIndex: 0, columnIndex: 0);\n    if (firstCell != null) {\n      final firstFocusableChild = firstCell.getFirstFocusableChild();\n      if (firstFocusableChild != null) {\n        editorState.updateSelectionWithReason(\n          Selection.collapsed(\n            Position(path: firstFocusableChild.path),\n          ),\n        );\n        return KeyEventResult.handled;\n      }\n    }\n  }\n\n  return KeyEventResult.ignored;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_select_all_command.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/material.dart';\n\nfinal CommandShortcutEvent selectAllInTableCellCommand = CommandShortcutEvent(\n  key: 'Select all contents in table cell',\n  getDescription: () => 'Select all contents in table cell',\n  command: 'ctrl+a',\n  macOSCommand: 'cmd+a',\n  handler: _selectAllInTableCellHandler,\n);\n\nKeyEventResult _selectAllInTableCellHandler(EditorState editorState) {\n  final (isInTableCell, selection, tableCellNode, _) =\n      editorState.isCurrentSelectionInTableCell();\n  if (!isInTableCell || selection == null || tableCellNode == null) {\n    return KeyEventResult.ignored;\n  }\n\n  final firstFocusableChild = tableCellNode.children.firstWhereOrNull(\n    (e) => e.delta != null,\n  );\n  final lastFocusableChild = tableCellNode.lastChildWhere(\n    (e) => e.delta != null,\n  );\n  if (firstFocusableChild == null || lastFocusableChild == null) {\n    return KeyEventResult.ignored;\n  }\n\n  final afterSelection = Selection(\n    start: Position(path: firstFocusableChild.path),\n    end: Position(\n      path: lastFocusableChild.path,\n      offset: lastFocusableChild.delta?.length ?? 0,\n    ),\n  );\n\n  if (afterSelection == editorState.selection) {\n    // Focus on the cell already\n    return KeyEventResult.ignored;\n  } else {\n    editorState.selection = afterSelection;\n    return KeyEventResult.handled;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_tab_command.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\nfinal CommandShortcutEvent tabInTableCell = CommandShortcutEvent(\n  key: 'Press tab in table cell',\n  getDescription: () => 'Move the selection to the next cell',\n  command: 'tab',\n  handler: (editorState) => editorState.moveToNextCell(\n    editorState,\n    (result) {\n      final tableCellNode = result.$3;\n      if (tableCellNode?.isLastCellInTable ?? false) {\n        return false;\n      }\n      return true;\n    },\n  ),\n);\n\nfinal CommandShortcutEvent shiftTabInTableCell = CommandShortcutEvent(\n  key: 'Press shift + tab in table cell',\n  getDescription: () => 'Move the selection to the previous cell',\n  command: 'shift+tab',\n  handler: (editorState) => editorState.moveToPreviousCell(\n    editorState,\n    (result) {\n      final tableCellNode = result.$3;\n      if (tableCellNode?.isFirstCellInTable ?? false) {\n        return false;\n      }\n      return true;\n    },\n  ),\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_desktop_simple_table_widget.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass DesktopSimpleTableWidget extends StatefulWidget {\n  const DesktopSimpleTableWidget({\n    super.key,\n    required this.simpleTableContext,\n    required this.node,\n    this.enableAddColumnButton = true,\n    this.enableAddRowButton = true,\n    this.enableAddColumnAndRowButton = true,\n    this.enableHoverEffect = true,\n    this.isFeedback = false,\n    this.alwaysDistributeColumnWidths = false,\n  });\n\n  /// Refer to [SimpleTableWidget.node].\n  final Node node;\n\n  /// Refer to [SimpleTableWidget.simpleTableContext].\n  final SimpleTableContext simpleTableContext;\n\n  /// Refer to [SimpleTableWidget.enableAddColumnButton].\n  final bool enableAddColumnButton;\n\n  /// Refer to [SimpleTableWidget.enableAddRowButton].\n  final bool enableAddRowButton;\n\n  /// Refer to [SimpleTableWidget.enableAddColumnAndRowButton].\n  final bool enableAddColumnAndRowButton;\n\n  /// Refer to [SimpleTableWidget.enableHoverEffect].\n  final bool enableHoverEffect;\n\n  /// Refer to [SimpleTableWidget.isFeedback].\n  final bool isFeedback;\n\n  /// Refer to [SimpleTableWidget.alwaysDistributeColumnWidths].\n  final bool alwaysDistributeColumnWidths;\n\n  @override\n  State<DesktopSimpleTableWidget> createState() =>\n      _DesktopSimpleTableWidgetState();\n}\n\nclass _DesktopSimpleTableWidgetState extends State<DesktopSimpleTableWidget> {\n  SimpleTableContext get simpleTableContext => widget.simpleTableContext;\n\n  final scrollController = ScrollController();\n  late final editorState = context.read<EditorState>();\n\n  @override\n  void initState() {\n    super.initState();\n\n    simpleTableContext.horizontalScrollController = scrollController;\n  }\n\n  @override\n  void dispose() {\n    simpleTableContext.horizontalScrollController = null;\n    scrollController.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return widget.isFeedback ? _buildFeedbackTable() : _buildDesktopTable();\n  }\n\n  Widget _buildFeedbackTable() {\n    return Provider.value(\n      value: simpleTableContext,\n      child: IntrinsicWidth(\n        child: IntrinsicHeight(\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: _buildRows(),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildDesktopTable() {\n    // table content\n    // IntrinsicHeight is used to make the table size fit the content.\n    Widget child = IntrinsicHeight(\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        crossAxisAlignment: CrossAxisAlignment.stretch,\n        children: _buildRows(),\n      ),\n    );\n\n    if (widget.alwaysDistributeColumnWidths) {\n      child = Padding(\n        padding: SimpleTableConstants.tablePadding,\n        child: child,\n      );\n    } else {\n      child = Scrollbar(\n        controller: scrollController,\n        child: SingleChildScrollView(\n          controller: scrollController,\n          scrollDirection: Axis.horizontal,\n          child: Padding(\n            padding: SimpleTableConstants.tablePadding,\n            // IntrinsicWidth is used to make the table size fit the content.\n            child: IntrinsicWidth(child: child),\n          ),\n        ),\n      );\n    }\n\n    if (widget.enableHoverEffect) {\n      child = MouseRegion(\n        onEnter: (event) =>\n            simpleTableContext.isHoveringOnTableArea.value = true,\n        onExit: (event) {\n          simpleTableContext.isHoveringOnTableArea.value = false;\n        },\n        child: Provider.value(\n          value: simpleTableContext,\n          child: Stack(\n            children: [\n              MouseRegion(\n                hitTestBehavior: HitTestBehavior.opaque,\n                onEnter: (event) =>\n                    simpleTableContext.isHoveringOnColumnsAndRows.value = true,\n                onExit: (event) {\n                  simpleTableContext.isHoveringOnColumnsAndRows.value = false;\n                  simpleTableContext.hoveringTableCell.value = null;\n                },\n                child: child,\n              ),\n              if (editorState.editable) ...[\n                if (widget.enableAddColumnButton)\n                  SimpleTableAddColumnHoverButton(\n                    editorState: editorState,\n                    tableNode: widget.node,\n                  ),\n                if (widget.enableAddRowButton)\n                  SimpleTableAddRowHoverButton(\n                    editorState: editorState,\n                    tableNode: widget.node,\n                  ),\n                if (widget.enableAddColumnAndRowButton)\n                  SimpleTableAddColumnAndRowHoverButton(\n                    editorState: editorState,\n                    node: widget.node,\n                  ),\n              ],\n            ],\n          ),\n        ),\n      );\n    }\n\n    return child;\n  }\n\n  List<Widget> _buildRows() {\n    final List<Widget> rows = [];\n\n    if (SimpleTableConstants.borderType == SimpleTableBorderRenderType.table) {\n      rows.add(const SimpleTableColumnDivider());\n    }\n\n    for (final child in widget.node.children) {\n      rows.add(editorState.renderer.build(context, child));\n\n      if (SimpleTableConstants.borderType ==\n          SimpleTableBorderRenderType.table) {\n        rows.add(const SimpleTableColumnDivider());\n      }\n    }\n\n    return rows;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_mobile_simple_table_widget.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass MobileSimpleTableWidget extends StatefulWidget {\n  const MobileSimpleTableWidget({\n    super.key,\n    required this.simpleTableContext,\n    required this.node,\n    this.enableAddColumnButton = true,\n    this.enableAddRowButton = true,\n    this.enableAddColumnAndRowButton = true,\n    this.enableHoverEffect = true,\n    this.isFeedback = false,\n    this.alwaysDistributeColumnWidths = false,\n  });\n\n  /// Refer to [SimpleTableWidget.node].\n  final Node node;\n\n  /// Refer to [SimpleTableWidget.simpleTableContext].\n  final SimpleTableContext simpleTableContext;\n\n  /// Refer to [SimpleTableWidget.enableAddColumnButton].\n  final bool enableAddColumnButton;\n\n  /// Refer to [SimpleTableWidget.enableAddRowButton].\n  final bool enableAddRowButton;\n\n  /// Refer to [SimpleTableWidget.enableAddColumnAndRowButton].\n  final bool enableAddColumnAndRowButton;\n\n  /// Refer to [SimpleTableWidget.enableHoverEffect].\n  final bool enableHoverEffect;\n\n  /// Refer to [SimpleTableWidget.isFeedback].\n  final bool isFeedback;\n\n  /// Refer to [SimpleTableWidget.alwaysDistributeColumnWidths].\n  final bool alwaysDistributeColumnWidths;\n\n  @override\n  State<MobileSimpleTableWidget> createState() =>\n      _MobileSimpleTableWidgetState();\n}\n\nclass _MobileSimpleTableWidgetState extends State<MobileSimpleTableWidget> {\n  SimpleTableContext get simpleTableContext => widget.simpleTableContext;\n\n  final scrollController = ScrollController();\n  late final editorState = context.read<EditorState>();\n\n  @override\n  void initState() {\n    super.initState();\n\n    simpleTableContext.horizontalScrollController = scrollController;\n  }\n\n  @override\n  void dispose() {\n    simpleTableContext.horizontalScrollController = null;\n    scrollController.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return widget.isFeedback ? _buildFeedbackTable() : _buildMobileTable();\n  }\n\n  Widget _buildFeedbackTable() {\n    return Provider.value(\n      value: simpleTableContext,\n      child: IntrinsicWidth(\n        child: IntrinsicHeight(\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: _buildRows(),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildMobileTable() {\n    return Provider.value(\n      value: simpleTableContext,\n      child: SingleChildScrollView(\n        controller: scrollController,\n        scrollDirection: Axis.horizontal,\n        child: IntrinsicWidth(\n          child: IntrinsicHeight(\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: _buildRows(),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  List<Widget> _buildRows() {\n    final List<Widget> rows = [];\n\n    if (SimpleTableConstants.borderType == SimpleTableBorderRenderType.table) {\n      rows.add(const SimpleTableColumnDivider());\n    }\n\n    for (final child in widget.node.children) {\n      rows.add(editorState.renderer.build(context, child));\n\n      if (SimpleTableConstants.borderType ==\n          SimpleTableBorderRenderType.table) {\n        rows.add(const SimpleTableColumnDivider());\n      }\n    }\n\n    return rows;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/mobile/presentation/base/animated_gesture.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\n/// Base class for all simple table bottom sheet actions\nabstract class ISimpleTableBottomSheetActions extends StatelessWidget {\n  const ISimpleTableBottomSheetActions({\n    super.key,\n    required this.type,\n    required this.cellNode,\n    required this.editorState,\n  });\n\n  final SimpleTableMoreActionType type;\n  final Node cellNode;\n  final EditorState editorState;\n}\n\n/// Quick actions for the table cell\n///\n/// - Copy\n/// - Paste\n/// - Cut\n/// - Delete\nclass SimpleTableCellQuickActions extends ISimpleTableBottomSheetActions {\n  const SimpleTableCellQuickActions({\n    super.key,\n    required super.type,\n    required super.cellNode,\n    required super.editorState,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: SimpleTableConstants.actionSheetQuickActionSectionHeight,\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceEvenly,\n        children: [\n          SimpleTableQuickAction(\n            type: SimpleTableMoreAction.cut,\n            onTap: () => _onActionTap(\n              context,\n              SimpleTableMoreAction.cut,\n            ),\n          ),\n          SimpleTableQuickAction(\n            type: SimpleTableMoreAction.copy,\n            onTap: () => _onActionTap(\n              context,\n              SimpleTableMoreAction.copy,\n            ),\n          ),\n          SimpleTableQuickAction(\n            type: SimpleTableMoreAction.paste,\n            onTap: () => _onActionTap(\n              context,\n              SimpleTableMoreAction.paste,\n            ),\n          ),\n          SimpleTableQuickAction(\n            type: SimpleTableMoreAction.delete,\n            onTap: () => _onActionTap(\n              context,\n              SimpleTableMoreAction.delete,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  void _onActionTap(BuildContext context, SimpleTableMoreAction action) {\n    final tableNode = cellNode.parentTableNode;\n    if (tableNode == null) {\n      Log.error('unable to find table node when performing action: $action');\n      return;\n    }\n\n    switch (action) {\n      case SimpleTableMoreAction.cut:\n        _onCut(tableNode);\n      case SimpleTableMoreAction.copy:\n        _onCopy(tableNode);\n      case SimpleTableMoreAction.paste:\n        _onPaste(tableNode);\n      case SimpleTableMoreAction.delete:\n        _onDelete(tableNode);\n      default:\n        assert(false, 'Unsupported action: $type');\n    }\n\n    // close the action menu\n    Navigator.of(context).pop();\n  }\n\n  Future<void> _onCut(Node tableNode) async {\n    ClipboardServiceData? data;\n\n    switch (type) {\n      case SimpleTableMoreActionType.column:\n        data = await editorState.copyColumn(\n          tableNode: tableNode,\n          columnIndex: cellNode.columnIndex,\n          clearContent: true,\n        );\n      case SimpleTableMoreActionType.row:\n        data = await editorState.copyRow(\n          tableNode: tableNode,\n          rowIndex: cellNode.rowIndex,\n          clearContent: true,\n        );\n    }\n\n    if (data != null) {\n      await getIt<ClipboardService>().setData(data);\n    }\n  }\n\n  Future<void> _onCopy(\n    Node tableNode,\n  ) async {\n    ClipboardServiceData? data;\n\n    switch (type) {\n      case SimpleTableMoreActionType.column:\n        data = await editorState.copyColumn(\n          tableNode: tableNode,\n          columnIndex: cellNode.columnIndex,\n        );\n      case SimpleTableMoreActionType.row:\n        data = await editorState.copyRow(\n          tableNode: tableNode,\n          rowIndex: cellNode.rowIndex,\n        );\n    }\n\n    if (data != null) {\n      await getIt<ClipboardService>().setData(data);\n    }\n  }\n\n  void _onPaste(Node tableNode) {\n    switch (type) {\n      case SimpleTableMoreActionType.column:\n        editorState.pasteColumn(\n          tableNode: tableNode,\n          columnIndex: cellNode.columnIndex,\n        );\n      case SimpleTableMoreActionType.row:\n        editorState.pasteRow(\n          tableNode: tableNode,\n          rowIndex: cellNode.rowIndex,\n        );\n    }\n  }\n\n  void _onDelete(Node tableNode) {\n    switch (type) {\n      case SimpleTableMoreActionType.column:\n        editorState.deleteColumnInTable(\n          tableNode,\n          cellNode.columnIndex,\n        );\n      case SimpleTableMoreActionType.row:\n        editorState.deleteRowInTable(\n          tableNode,\n          cellNode.rowIndex,\n        );\n    }\n  }\n}\n\nclass SimpleTableQuickAction extends StatelessWidget {\n  const SimpleTableQuickAction({\n    super.key,\n    required this.type,\n    required this.onTap,\n    this.isEnabled = true,\n  });\n\n  final SimpleTableMoreAction type;\n  final VoidCallback onTap;\n  final bool isEnabled;\n\n  @override\n  Widget build(BuildContext context) {\n    return Opacity(\n      opacity: isEnabled ? 1.0 : 0.5,\n      child: AnimatedGestureDetector(\n        onTapUp: isEnabled ? onTap : null,\n        child: FlowySvg(\n          type.leftIconSvg,\n          blendMode:\n              type == SimpleTableMoreAction.delete ? null : BlendMode.srcIn,\n          size: const Size.square(24),\n          color: context.simpleTableQuickActionBackgroundColor,\n        ),\n      ),\n    );\n  }\n}\n\n/// Insert actions\n///\n/// - Column: Insert left or insert right\n/// - Row: Insert above or insert below\nclass SimpleTableInsertActions extends ISimpleTableBottomSheetActions {\n  const SimpleTableInsertActions({\n    super.key,\n    required super.type,\n    required super.cellNode,\n    required super.editorState,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.symmetric(horizontal: 16),\n      height: SimpleTableConstants.actionSheetInsertSectionHeight,\n      child: _buildAction(context),\n    );\n  }\n\n  Widget _buildAction(BuildContext context) {\n    return switch (type) {\n      SimpleTableMoreActionType.row => Row(\n          children: [\n            SimpleTableInsertAction(\n              type: SimpleTableMoreAction.insertAbove,\n              enableLeftBorder: true,\n              onTap: (increaseCounter) async => _onActionTap(\n                context,\n                type: SimpleTableMoreAction.insertAbove,\n                increaseCounter: increaseCounter,\n              ),\n            ),\n            const HSpace(2),\n            SimpleTableInsertAction(\n              type: SimpleTableMoreAction.insertBelow,\n              enableRightBorder: true,\n              onTap: (increaseCounter) async => _onActionTap(\n                context,\n                type: SimpleTableMoreAction.insertBelow,\n                increaseCounter: increaseCounter,\n              ),\n            ),\n          ],\n        ),\n      SimpleTableMoreActionType.column => Row(\n          children: [\n            SimpleTableInsertAction(\n              type: SimpleTableMoreAction.insertLeft,\n              enableLeftBorder: true,\n              onTap: (increaseCounter) async => _onActionTap(\n                context,\n                type: SimpleTableMoreAction.insertLeft,\n                increaseCounter: increaseCounter,\n              ),\n            ),\n            const HSpace(2),\n            SimpleTableInsertAction(\n              type: SimpleTableMoreAction.insertRight,\n              enableRightBorder: true,\n              onTap: (increaseCounter) async => _onActionTap(\n                context,\n                type: SimpleTableMoreAction.insertRight,\n                increaseCounter: increaseCounter,\n              ),\n            ),\n          ],\n        ),\n    };\n  }\n\n  Future<void> _onActionTap(\n    BuildContext context, {\n    required SimpleTableMoreAction type,\n    required int increaseCounter,\n  }) async {\n    final simpleTableContext = context.read<SimpleTableContext>();\n    final tableNode = cellNode.parentTableNode;\n    if (tableNode == null) {\n      Log.error('unable to find table node when performing action: $type');\n      return;\n    }\n\n    switch (type) {\n      case SimpleTableMoreAction.insertAbove:\n        // update the highlight status for the selecting row\n        simpleTableContext.selectingRow.value = cellNode.rowIndex + 1;\n        await editorState.insertRowInTable(\n          tableNode,\n          cellNode.rowIndex,\n        );\n      case SimpleTableMoreAction.insertBelow:\n        await editorState.insertRowInTable(\n          tableNode,\n          cellNode.rowIndex + 1,\n        );\n        // scroll to the next cell position\n        editorState.scrollService?.scrollTo(\n          SimpleTableConstants.defaultRowHeight,\n          duration: Durations.short3,\n        );\n      case SimpleTableMoreAction.insertLeft:\n        // update the highlight status for the selecting column\n        simpleTableContext.selectingColumn.value = cellNode.columnIndex + 1;\n        await editorState.insertColumnInTable(\n          tableNode,\n          cellNode.columnIndex,\n        );\n      case SimpleTableMoreAction.insertRight:\n        await editorState.insertColumnInTable(\n          tableNode,\n          cellNode.columnIndex + 1,\n        );\n        final horizontalScrollController =\n            simpleTableContext.horizontalScrollController;\n        if (horizontalScrollController != null) {\n          final previousWidth = horizontalScrollController.offset;\n          horizontalScrollController.jumpTo(\n            previousWidth + SimpleTableConstants.defaultColumnWidth,\n          );\n        }\n\n      default:\n        assert(false, 'Unsupported action: $type');\n    }\n  }\n}\n\nclass SimpleTableInsertAction extends StatefulWidget {\n  const SimpleTableInsertAction({\n    super.key,\n    required this.type,\n    this.enableLeftBorder = false,\n    this.enableRightBorder = false,\n    required this.onTap,\n  });\n\n  final SimpleTableMoreAction type;\n  final bool enableLeftBorder;\n  final bool enableRightBorder;\n  final ValueChanged<int> onTap;\n\n  @override\n  State<SimpleTableInsertAction> createState() =>\n      _SimpleTableInsertActionState();\n}\n\nclass _SimpleTableInsertActionState extends State<SimpleTableInsertAction> {\n  // used to count how many times the action is tapped\n  int increaseCounter = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      child: DecoratedBox(\n        decoration: ShapeDecoration(\n          color: context.simpleTableInsertActionBackgroundColor,\n          shape: _buildBorder(),\n        ),\n        child: AnimatedGestureDetector(\n          onTapUp: () => widget.onTap(increaseCounter++),\n          child: Column(\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              Padding(\n                padding: const EdgeInsets.all(1),\n                child: FlowySvg(\n                  widget.type.leftIconSvg,\n                  size: const Size.square(22),\n                ),\n              ),\n              FlowyText(\n                widget.type.name,\n                fontSize: 12,\n                figmaLineHeight: 16,\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  RoundedRectangleBorder _buildBorder() {\n    const radius = Radius.circular(\n      SimpleTableConstants.actionSheetButtonRadius,\n    );\n    return RoundedRectangleBorder(\n      borderRadius: BorderRadius.only(\n        topLeft: widget.enableLeftBorder ? radius : Radius.zero,\n        bottomLeft: widget.enableLeftBorder ? radius : Radius.zero,\n        topRight: widget.enableRightBorder ? radius : Radius.zero,\n        bottomRight: widget.enableRightBorder ? radius : Radius.zero,\n      ),\n    );\n  }\n}\n\n/// Cell Action buttons\n///\n/// - Distribute columns evenly\n/// - Set to page width\n/// - Duplicate row\n/// - Duplicate column\n/// - Clear contents\nclass SimpleTableCellActionButtons extends ISimpleTableBottomSheetActions {\n  const SimpleTableCellActionButtons({\n    super.key,\n    required super.type,\n    required super.cellNode,\n    required super.editorState,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 16),\n      child: Column(\n        children: _buildActions(context),\n      ),\n    );\n  }\n\n  List<Widget> _buildActions(BuildContext context) {\n    // the actions are grouped into different sections\n    // we need to get the index of the table cell node\n    // and the length of the columns and rows\n    final (index, columnLength, rowLength) = switch (type) {\n      SimpleTableMoreActionType.row => (\n          cellNode.rowIndex,\n          cellNode.columnLength,\n          cellNode.rowLength,\n        ),\n      SimpleTableMoreActionType.column => (\n          cellNode.columnIndex,\n          cellNode.rowLength,\n          cellNode.columnLength,\n        ),\n    };\n    final actionGroups = type.buildMobileActions(\n      index: index,\n      columnLength: columnLength,\n      rowLength: rowLength,\n    );\n    final List<Widget> widgets = [];\n\n    for (final (actionGroupIndex, actionGroup) in actionGroups.indexed) {\n      for (final (index, action) in actionGroup.indexed) {\n        widgets.add(\n          // enable the corner border if the cell is the first or last in the group\n          switch (action) {\n            SimpleTableMoreAction.enableHeaderColumn =>\n              SimpleTableHeaderActionButton(\n                type: action,\n                isEnabled: cellNode.isHeaderColumnEnabled,\n                onTap: (value) => _onActionTap(\n                  context,\n                  action: action,\n                  toggleHeaderValue: value,\n                ),\n              ),\n            SimpleTableMoreAction.enableHeaderRow =>\n              SimpleTableHeaderActionButton(\n                type: action,\n                isEnabled: cellNode.isHeaderRowEnabled,\n                onTap: (value) => _onActionTap(\n                  context,\n                  action: action,\n                  toggleHeaderValue: value,\n                ),\n              ),\n            _ => SimpleTableActionButton(\n                type: action,\n                enableTopBorder: index == 0,\n                enableBottomBorder: index == actionGroup.length - 1,\n                onTap: () => _onActionTap(\n                  context,\n                  action: action,\n                ),\n              ),\n          },\n        );\n        // if the action is not the first or last in the group, add a divider\n        if (index != actionGroup.length - 1) {\n          widgets.add(const FlowyDivider());\n        }\n      }\n\n      // add padding to separate the action groups\n      if (actionGroupIndex != actionGroups.length - 1) {\n        widgets.add(const VSpace(16));\n      }\n    }\n\n    return widgets;\n  }\n\n  void _onActionTap(\n    BuildContext context, {\n    required SimpleTableMoreAction action,\n    bool toggleHeaderValue = false,\n  }) {\n    final tableNode = cellNode.parentTableNode;\n    if (tableNode == null) {\n      Log.error('unable to find table node when performing action: $action');\n      return;\n    }\n\n    switch (action) {\n      case SimpleTableMoreAction.enableHeaderColumn:\n        editorState.toggleEnableHeaderColumn(\n          tableNode: tableNode,\n          enable: toggleHeaderValue,\n        );\n      case SimpleTableMoreAction.enableHeaderRow:\n        editorState.toggleEnableHeaderRow(\n          tableNode: tableNode,\n          enable: toggleHeaderValue,\n        );\n      case SimpleTableMoreAction.distributeColumnsEvenly:\n        editorState.distributeColumnWidthToPageWidth(tableNode: tableNode);\n      case SimpleTableMoreAction.setToPageWidth:\n        editorState.setColumnWidthToPageWidth(tableNode: tableNode);\n      case SimpleTableMoreAction.duplicateRow:\n        editorState.duplicateRowInTable(\n          tableNode,\n          cellNode.rowIndex,\n        );\n      case SimpleTableMoreAction.duplicateColumn:\n        editorState.duplicateColumnInTable(\n          tableNode,\n          cellNode.columnIndex,\n        );\n      case SimpleTableMoreAction.clearContents:\n        switch (type) {\n          case SimpleTableMoreActionType.column:\n            editorState.clearContentAtColumnIndex(\n              tableNode: tableNode,\n              columnIndex: cellNode.columnIndex,\n            );\n          case SimpleTableMoreActionType.row:\n            editorState.clearContentAtRowIndex(\n              tableNode: tableNode,\n              rowIndex: cellNode.rowIndex,\n            );\n        }\n      default:\n        assert(false, 'Unsupported action: $action');\n        break;\n    }\n\n    // close the action menu\n    // keep the action menu open if the action is enable header row or enable header column\n    if (action != SimpleTableMoreAction.enableHeaderRow &&\n        action != SimpleTableMoreAction.enableHeaderColumn) {\n      Navigator.of(context).pop();\n    }\n  }\n}\n\n/// Header action button\n///\n/// - Enable header column\n/// - Enable header row\n///\n/// Notes: These actions are only available for the first column or first row\nclass SimpleTableHeaderActionButton extends StatefulWidget {\n  const SimpleTableHeaderActionButton({\n    super.key,\n    required this.isEnabled,\n    required this.type,\n    this.onTap,\n  });\n\n  final bool isEnabled;\n  final SimpleTableMoreAction type;\n  final void Function(bool value)? onTap;\n\n  @override\n  State<SimpleTableHeaderActionButton> createState() =>\n      _SimpleTableHeaderActionButtonState();\n}\n\nclass _SimpleTableHeaderActionButtonState\n    extends State<SimpleTableHeaderActionButton> {\n  bool value = false;\n\n  @override\n  void initState() {\n    super.initState();\n\n    value = widget.isEnabled;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SimpleTableActionButton(\n      type: widget.type,\n      enableTopBorder: true,\n      enableBottomBorder: true,\n      onTap: _toggle,\n      rightIconBuilder: (context) {\n        return Container(\n          width: 36,\n          height: 24,\n          margin: const EdgeInsets.only(right: 16),\n          child: FittedBox(\n            fit: BoxFit.fill,\n            child: CupertinoSwitch(\n              value: value,\n              activeTrackColor: Theme.of(context).colorScheme.primary,\n              onChanged: (_) => _toggle(),\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  void _toggle() {\n    setState(() {\n      value = !value;\n    });\n\n    widget.onTap?.call(value);\n  }\n}\n\n/// Align text action button\n///\n/// - Align text to left\n/// - Align text to center\n/// - Align text to right\n///\n/// Notes: These actions are only available for the table\nclass SimpleTableAlignActionButton extends StatefulWidget {\n  const SimpleTableAlignActionButton({\n    super.key,\n    required this.onTap,\n  });\n\n  final VoidCallback onTap;\n\n  @override\n  State<SimpleTableAlignActionButton> createState() =>\n      _SimpleTableAlignActionButtonState();\n}\n\nclass _SimpleTableAlignActionButtonState\n    extends State<SimpleTableAlignActionButton> {\n  @override\n  Widget build(BuildContext context) {\n    return SimpleTableActionButton(\n      type: SimpleTableMoreAction.align,\n      enableTopBorder: true,\n      enableBottomBorder: true,\n      onTap: widget.onTap,\n      rightIconBuilder: (context) {\n        return const Padding(\n          padding: EdgeInsets.only(right: 16),\n          child: FlowySvg(FlowySvgs.m_aa_arrow_right_s),\n        );\n      },\n    );\n  }\n}\n\nclass SimpleTableActionButton extends StatelessWidget {\n  const SimpleTableActionButton({\n    super.key,\n    required this.type,\n    this.enableTopBorder = false,\n    this.enableBottomBorder = false,\n    this.rightIconBuilder,\n    this.onTap,\n  });\n\n  final SimpleTableMoreAction type;\n  final bool enableTopBorder;\n  final bool enableBottomBorder;\n  final WidgetBuilder? rightIconBuilder;\n  final void Function()? onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: onTap,\n      child: Container(\n        height: SimpleTableConstants.actionSheetNormalActionSectionHeight,\n        decoration: ShapeDecoration(\n          color: context.simpleTableActionButtonBackgroundColor,\n          shape: _buildBorder(),\n        ),\n        child: Row(\n          children: [\n            const HSpace(16),\n            Padding(\n              padding: const EdgeInsets.all(1.0),\n              child: FlowySvg(\n                type.leftIconSvg,\n                size: const Size.square(20),\n              ),\n            ),\n            const HSpace(12),\n            FlowyText(\n              type.name,\n              fontSize: 14,\n              figmaLineHeight: 20,\n            ),\n            const Spacer(),\n            rightIconBuilder?.call(context) ?? const SizedBox.shrink(),\n          ],\n        ),\n      ),\n    );\n  }\n\n  RoundedRectangleBorder _buildBorder() {\n    const radius = Radius.circular(\n      SimpleTableConstants.actionSheetButtonRadius,\n    );\n    return RoundedRectangleBorder(\n      borderRadius: BorderRadius.only(\n        topLeft: enableTopBorder ? radius : Radius.zero,\n        topRight: enableTopBorder ? radius : Radius.zero,\n        bottomLeft: enableBottomBorder ? radius : Radius.zero,\n        bottomRight: enableBottomBorder ? radius : Radius.zero,\n      ),\n    );\n  }\n}\n\nclass SimpleTableContentActions extends ISimpleTableBottomSheetActions {\n  const SimpleTableContentActions({\n    super.key,\n    required super.type,\n    required super.cellNode,\n    required super.editorState,\n    required this.onTextColorSelected,\n    required this.onCellBackgroundColorSelected,\n    required this.onAlignTap,\n    this.selectedTextColor,\n    this.selectedCellBackgroundColor,\n    this.selectedAlign,\n  });\n\n  final VoidCallback onTextColorSelected;\n  final VoidCallback onCellBackgroundColorSelected;\n  final ValueChanged<TableAlign> onAlignTap;\n\n  final Color? selectedTextColor;\n  final Color? selectedCellBackgroundColor;\n  final TableAlign? selectedAlign;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: SimpleTableConstants.actionSheetContentSectionHeight,\n      padding: const EdgeInsets.symmetric(horizontal: 16),\n      child: Row(\n        children: [\n          SimpleTableContentBoldAction(\n            isBold: type == SimpleTableMoreActionType.column\n                ? cellNode.isInBoldColumn\n                : cellNode.isInBoldRow,\n            toggleBold: _toggleBold,\n          ),\n          const HSpace(2),\n          SimpleTableContentTextColorAction(\n            onTap: onTextColorSelected,\n            selectedTextColor: selectedTextColor,\n          ),\n          const HSpace(2),\n          SimpleTableContentCellBackgroundColorAction(\n            onTap: onCellBackgroundColorSelected,\n            selectedCellBackgroundColor: selectedCellBackgroundColor,\n          ),\n          const HSpace(16),\n          SimpleTableContentAlignmentAction(\n            align: selectedAlign ?? TableAlign.left,\n            onTap: onAlignTap,\n          ),\n        ],\n      ),\n    );\n  }\n\n  void _toggleBold(bool isBold) {\n    switch (type) {\n      case SimpleTableMoreActionType.column:\n        editorState.toggleColumnBoldAttribute(\n          tableCellNode: cellNode,\n          isBold: isBold,\n        );\n      case SimpleTableMoreActionType.row:\n        editorState.toggleRowBoldAttribute(\n          tableCellNode: cellNode,\n          isBold: isBold,\n        );\n    }\n  }\n}\n\nclass SimpleTableContentBoldAction extends StatefulWidget {\n  const SimpleTableContentBoldAction({\n    super.key,\n    required this.toggleBold,\n    required this.isBold,\n  });\n\n  final ValueChanged<bool> toggleBold;\n  final bool isBold;\n\n  @override\n  State<SimpleTableContentBoldAction> createState() =>\n      _SimpleTableContentBoldActionState();\n}\n\nclass _SimpleTableContentBoldActionState\n    extends State<SimpleTableContentBoldAction> {\n  bool isBold = false;\n\n  @override\n  void initState() {\n    super.initState();\n\n    isBold = widget.isBold;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      child: SimpleTableContentActionDecorator(\n        backgroundColor: isBold ? Theme.of(context).colorScheme.primary : null,\n        enableLeftBorder: true,\n        child: AnimatedGestureDetector(\n          onTapUp: () {\n            setState(() {\n              isBold = !isBold;\n            });\n            widget.toggleBold.call(isBold);\n          },\n          child: FlowySvg(\n            FlowySvgs.m_aa_bold_s,\n            size: const Size.square(24),\n            color: isBold ? Theme.of(context).colorScheme.onPrimary : null,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass SimpleTableContentTextColorAction extends StatelessWidget {\n  const SimpleTableContentTextColorAction({\n    super.key,\n    required this.onTap,\n    this.selectedTextColor,\n  });\n\n  final VoidCallback onTap;\n  final Color? selectedTextColor;\n\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      child: SimpleTableContentActionDecorator(\n        child: AnimatedGestureDetector(\n          onTapUp: onTap,\n          child: Row(\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              FlowySvg(\n                FlowySvgs.m_table_text_color_m,\n                color: selectedTextColor,\n              ),\n              const HSpace(10),\n              const FlowySvg(\n                FlowySvgs.m_aa_arrow_right_s,\n                size: Size.square(12),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass SimpleTableContentCellBackgroundColorAction extends StatelessWidget {\n  const SimpleTableContentCellBackgroundColorAction({\n    super.key,\n    required this.onTap,\n    this.selectedCellBackgroundColor,\n  });\n\n  final VoidCallback onTap;\n  final Color? selectedCellBackgroundColor;\n\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      child: SimpleTableContentActionDecorator(\n        enableRightBorder: true,\n        child: AnimatedGestureDetector(\n          onTapUp: onTap,\n          child: Row(\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              _buildTextBackgroundColorPreview(),\n              const HSpace(10),\n              FlowySvg(\n                FlowySvgs.m_aa_arrow_right_s,\n                size: const Size.square(12),\n                color: selectedCellBackgroundColor,\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildTextBackgroundColorPreview() {\n    return Container(\n      width: 24,\n      height: 24,\n      decoration: ShapeDecoration(\n        color: selectedCellBackgroundColor ?? const Color(0xFFFFE6FD),\n        shape: RoundedRectangleBorder(\n          side: const BorderSide(\n            color: Color(0xFFCFD3D9),\n          ),\n          borderRadius: BorderRadius.circular(100),\n        ),\n      ),\n    );\n  }\n}\n\nclass SimpleTableContentAlignmentAction extends StatelessWidget {\n  const SimpleTableContentAlignmentAction({\n    super.key,\n    this.align = TableAlign.left,\n    required this.onTap,\n  });\n\n  final TableAlign align;\n  final ValueChanged<TableAlign> onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      child: SimpleTableContentActionDecorator(\n        enableLeftBorder: true,\n        enableRightBorder: true,\n        child: AnimatedGestureDetector(\n          onTapUp: _onTap,\n          child: Row(\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              FlowySvg(\n                align.leftIconSvg,\n                size: const Size.square(24),\n              ),\n              const HSpace(10),\n              const FlowySvg(\n                FlowySvgs.m_aa_arrow_right_s,\n                size: Size.square(12),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  void _onTap() {\n    final nextAlign = switch (align) {\n      TableAlign.left => TableAlign.center,\n      TableAlign.center => TableAlign.right,\n      TableAlign.right => TableAlign.left,\n    };\n\n    onTap(nextAlign);\n  }\n}\n\nclass SimpleTableContentActionDecorator extends StatelessWidget {\n  const SimpleTableContentActionDecorator({\n    super.key,\n    this.enableLeftBorder = false,\n    this.enableRightBorder = false,\n    this.backgroundColor,\n    required this.child,\n  });\n\n  final bool enableLeftBorder;\n  final bool enableRightBorder;\n  final Color? backgroundColor;\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: SimpleTableConstants.actionSheetNormalActionSectionHeight,\n      padding: const EdgeInsets.symmetric(vertical: 10),\n      decoration: ShapeDecoration(\n        color:\n            backgroundColor ?? context.simpleTableInsertActionBackgroundColor,\n        shape: _buildBorder(),\n      ),\n      child: child,\n    );\n  }\n\n  RoundedRectangleBorder _buildBorder() {\n    const radius = Radius.circular(\n      SimpleTableConstants.actionSheetButtonRadius,\n    );\n    return RoundedRectangleBorder(\n      borderRadius: BorderRadius.only(\n        topLeft: enableLeftBorder ? radius : Radius.zero,\n        topRight: enableRightBorder ? radius : Radius.zero,\n        bottomLeft: enableLeftBorder ? radius : Radius.zero,\n        bottomRight: enableRightBorder ? radius : Radius.zero,\n      ),\n    );\n  }\n}\n\nclass SimpleTableActionButtons extends StatelessWidget {\n  const SimpleTableActionButtons({\n    super.key,\n    required this.tableNode,\n    required this.editorState,\n    required this.onAlignTap,\n  });\n\n  final Node tableNode;\n  final EditorState editorState;\n  final VoidCallback onAlignTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 16),\n      child: Column(\n        children: _buildActions(context),\n      ),\n    );\n  }\n\n  List<Widget> _buildActions(BuildContext context) {\n    final actionGroups = [\n      [\n        SimpleTableMoreAction.setToPageWidth,\n        SimpleTableMoreAction.distributeColumnsEvenly,\n      ],\n      [\n        SimpleTableMoreAction.align,\n      ],\n      [\n        SimpleTableMoreAction.duplicateTable,\n        SimpleTableMoreAction.copyLinkToBlock,\n      ]\n    ];\n    final List<Widget> widgets = [];\n\n    for (final (actionGroupIndex, actionGroup) in actionGroups.indexed) {\n      for (final (index, action) in actionGroup.indexed) {\n        widgets.add(\n          // enable the corner border if the cell is the first or last in the group\n          switch (action) {\n            SimpleTableMoreAction.align => SimpleTableAlignActionButton(\n                onTap: () => _onActionTap(\n                  context,\n                  action: action,\n                ),\n              ),\n            _ => SimpleTableActionButton(\n                type: action,\n                enableTopBorder: index == 0,\n                enableBottomBorder: index == actionGroup.length - 1,\n                onTap: () => _onActionTap(\n                  context,\n                  action: action,\n                ),\n              ),\n          },\n        );\n        // if the action is not the first or last in the group, add a divider\n        if (index != actionGroup.length - 1) {\n          widgets.add(const FlowyDivider());\n        }\n      }\n\n      // add padding to separate the action groups\n      if (actionGroupIndex != actionGroups.length - 1) {\n        widgets.add(const VSpace(16));\n      }\n    }\n\n    return widgets;\n  }\n\n  void _onActionTap(\n    BuildContext context, {\n    required SimpleTableMoreAction action,\n  }) {\n    final optionCubit = BlockActionOptionCubit(\n      editorState: editorState,\n      blockComponentBuilder: {},\n    );\n    switch (action) {\n      case SimpleTableMoreAction.setToPageWidth:\n        editorState.setColumnWidthToPageWidth(tableNode: tableNode);\n      case SimpleTableMoreAction.distributeColumnsEvenly:\n        editorState.distributeColumnWidthToPageWidth(tableNode: tableNode);\n      case SimpleTableMoreAction.duplicateTable:\n        optionCubit.handleAction(OptionAction.duplicate, tableNode);\n      case SimpleTableMoreAction.copyLinkToBlock:\n        optionCubit.handleAction(OptionAction.copyLinkToBlock, tableNode);\n      case SimpleTableMoreAction.align:\n        onAlignTap();\n      default:\n        assert(false, 'Unsupported action: $action');\n        break;\n    }\n\n    // close the action menu\n    if (action != SimpleTableMoreAction.align) {\n      Navigator.of(context).pop();\n    }\n  }\n}\n\nclass SimpleTableContentAlignAction extends StatefulWidget {\n  const SimpleTableContentAlignAction({\n    super.key,\n    required this.isSelected,\n    required this.align,\n    required this.onTap,\n  });\n\n  final bool isSelected;\n  final VoidCallback onTap;\n  final TableAlign align;\n\n  @override\n  State<SimpleTableContentAlignAction> createState() =>\n      _SimpleTableContentAlignActionState();\n}\n\nclass _SimpleTableContentAlignActionState\n    extends State<SimpleTableContentAlignAction> {\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      child: SimpleTableContentActionDecorator(\n        backgroundColor:\n            widget.isSelected ? Theme.of(context).colorScheme.primary : null,\n        enableLeftBorder: widget.align == TableAlign.left,\n        enableRightBorder: widget.align == TableAlign.right,\n        child: AnimatedGestureDetector(\n          onTapUp: widget.onTap,\n          child: FlowySvg(\n            widget.align.leftIconSvg,\n            size: const Size.square(24),\n            color: widget.isSelected\n                ? Theme.of(context).colorScheme.onPrimary\n                : null,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\n/// Quick actions for the table\n///\n/// - Copy\n/// - Paste\n/// - Cut\n/// - Delete\nclass SimpleTableQuickActions extends StatelessWidget {\n  const SimpleTableQuickActions({\n    super.key,\n    required this.tableNode,\n    required this.editorState,\n  });\n\n  final Node tableNode;\n  final EditorState editorState;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: SimpleTableConstants.actionSheetQuickActionSectionHeight,\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceEvenly,\n        children: [\n          SimpleTableQuickAction(\n            type: SimpleTableMoreAction.cut,\n            onTap: () => _onActionTap(\n              context,\n              SimpleTableMoreAction.cut,\n            ),\n          ),\n          SimpleTableQuickAction(\n            type: SimpleTableMoreAction.copy,\n            onTap: () => _onActionTap(\n              context,\n              SimpleTableMoreAction.copy,\n            ),\n          ),\n          SimpleTableQuickAction(\n            type: SimpleTableMoreAction.paste,\n            onTap: () => _onActionTap(\n              context,\n              SimpleTableMoreAction.paste,\n            ),\n          ),\n          SimpleTableQuickAction(\n            type: SimpleTableMoreAction.delete,\n            onTap: () => _onActionTap(\n              context,\n              SimpleTableMoreAction.delete,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  void _onActionTap(BuildContext context, SimpleTableMoreAction action) {\n    switch (action) {\n      case SimpleTableMoreAction.cut:\n        _onCut(tableNode);\n      case SimpleTableMoreAction.copy:\n        _onCopy(tableNode);\n      case SimpleTableMoreAction.paste:\n        _onPaste(tableNode);\n      case SimpleTableMoreAction.delete:\n        _onDelete(tableNode);\n      default:\n        assert(false, 'Unsupported action: $action');\n    }\n\n    // close the action menu\n    Navigator.of(context).pop();\n  }\n\n  Future<void> _onCut(Node tableNode) async {\n    final data = await editorState.copyTable(\n      tableNode: tableNode,\n      clearContent: true,\n    );\n    if (data != null) {\n      await getIt<ClipboardService>().setData(data);\n    }\n  }\n\n  Future<void> _onCopy(Node tableNode) async {\n    final data = await editorState.copyTable(\n      tableNode: tableNode,\n    );\n    if (data != null) {\n      await getIt<ClipboardService>().setData(data);\n    }\n  }\n\n  void _onPaste(Node tableNode) => editorState.pasteTable(\n        tableNode: tableNode,\n      );\n\n  void _onDelete(Node tableNode) {\n    final transaction = editorState.transaction;\n    transaction.deleteNode(tableNode);\n    editorState.apply(transaction);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_action_sheet.dart",
    "content": "import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_feedback.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:provider/provider.dart';\n\nclass SimpleTableMobileDraggableReorderButton extends StatelessWidget {\n  const SimpleTableMobileDraggableReorderButton({\n    super.key,\n    required this.cellNode,\n    required this.index,\n    required this.isShowingMenu,\n    required this.type,\n    required this.editorState,\n    required this.simpleTableContext,\n  });\n\n  final Node cellNode;\n  final int index;\n  final ValueNotifier<bool> isShowingMenu;\n  final SimpleTableMoreActionType type;\n  final EditorState editorState;\n  final SimpleTableContext simpleTableContext;\n\n  @override\n  Widget build(BuildContext context) {\n    return Draggable<int>(\n      data: index,\n      onDragStarted: () => _startDragging(),\n      onDragUpdate: (details) => _onDragUpdate(details),\n      onDragEnd: (_) => _stopDragging(),\n      feedback: SimpleTableFeedback(\n        editorState: editorState,\n        node: cellNode,\n        type: type,\n        index: index,\n      ),\n      child: SimpleTableMobileReorderButton(\n        index: index,\n        type: type,\n        node: cellNode,\n        isShowingMenu: isShowingMenu,\n      ),\n    );\n  }\n\n  void _startDragging() {\n    HapticFeedback.lightImpact();\n\n    isShowingMenu.value = true;\n    editorState.selection = null;\n\n    switch (type) {\n      case SimpleTableMoreActionType.column:\n        simpleTableContext.isReorderingColumn.value = (true, index);\n\n      case SimpleTableMoreActionType.row:\n        simpleTableContext.isReorderingRow.value = (true, index);\n    }\n  }\n\n  void _onDragUpdate(DragUpdateDetails details) {\n    simpleTableContext.reorderingOffset.value = details.globalPosition;\n  }\n\n  void _stopDragging() {\n    isShowingMenu.value = false;\n\n    switch (type) {\n      case SimpleTableMoreActionType.column:\n        _reorderColumn();\n      case SimpleTableMoreActionType.row:\n        _reorderRow();\n    }\n\n    simpleTableContext.reorderingOffset.value = Offset.zero;\n    simpleTableContext.isReorderingHitIndex.value = null;\n\n    switch (type) {\n      case SimpleTableMoreActionType.column:\n        simpleTableContext.isReorderingColumn.value = (false, -1);\n        break;\n      case SimpleTableMoreActionType.row:\n        simpleTableContext.isReorderingRow.value = (false, -1);\n        break;\n    }\n  }\n\n  void _reorderColumn() {\n    final fromIndex = simpleTableContext.isReorderingColumn.value.$2;\n    final toIndex = simpleTableContext.isReorderingHitIndex.value;\n    if (toIndex == null) {\n      return;\n    }\n\n    editorState.reorderColumn(\n      cellNode,\n      fromIndex: fromIndex,\n      toIndex: toIndex,\n    );\n  }\n\n  void _reorderRow() {\n    final fromIndex = simpleTableContext.isReorderingRow.value.$2;\n    final toIndex = simpleTableContext.isReorderingHitIndex.value;\n    if (toIndex == null) {\n      return;\n    }\n\n    editorState.reorderRow(\n      cellNode,\n      fromIndex: fromIndex,\n      toIndex: toIndex,\n    );\n  }\n}\n\nclass SimpleTableMobileReorderButton extends StatefulWidget {\n  const SimpleTableMobileReorderButton({\n    super.key,\n    required this.index,\n    required this.type,\n    required this.node,\n    required this.isShowingMenu,\n  });\n\n  final int index;\n  final SimpleTableMoreActionType type;\n  final Node node;\n  final ValueNotifier<bool> isShowingMenu;\n\n  @override\n  State<SimpleTableMobileReorderButton> createState() =>\n      _SimpleTableMobileReorderButtonState();\n}\n\nclass _SimpleTableMobileReorderButtonState\n    extends State<SimpleTableMobileReorderButton> {\n  late final EditorState editorState = context.read<EditorState>();\n  late final SimpleTableContext simpleTableContext =\n      context.read<SimpleTableContext>();\n\n  @override\n  void initState() {\n    super.initState();\n\n    simpleTableContext.selectingRow.addListener(_onUpdateShowingMenu);\n    simpleTableContext.selectingColumn.addListener(_onUpdateShowingMenu);\n  }\n\n  @override\n  void dispose() {\n    simpleTableContext.selectingRow.removeListener(_onUpdateShowingMenu);\n    simpleTableContext.selectingColumn.removeListener(_onUpdateShowingMenu);\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onTap: () async => _onSelecting(),\n      behavior: HitTestBehavior.opaque,\n      child: SizedBox(\n        height: widget.type == SimpleTableMoreActionType.column\n            ? SimpleTableConstants.columnActionSheetHitTestAreaHeight\n            : null,\n        width: widget.type == SimpleTableMoreActionType.row\n            ? SimpleTableConstants.rowActionSheetHitTestAreaWidth\n            : null,\n        child: Align(\n          child: SimpleTableReorderButton(\n            isShowingMenu: widget.isShowingMenu,\n            type: widget.type,\n          ),\n        ),\n      ),\n    );\n  }\n\n  Future<void> _onSelecting() async {\n    widget.isShowingMenu.value = true;\n\n    // update the selecting row or column\n    switch (widget.type) {\n      case SimpleTableMoreActionType.column:\n        simpleTableContext.selectingColumn.value = widget.index;\n        simpleTableContext.selectingRow.value = null;\n        break;\n      case SimpleTableMoreActionType.row:\n        simpleTableContext.selectingRow.value = widget.index;\n        simpleTableContext.selectingColumn.value = null;\n    }\n\n    editorState.selection = null;\n\n    // show the bottom sheet\n    await showMobileBottomSheet(\n      context,\n      useSafeArea: false,\n      showDragHandle: true,\n      showDivider: false,\n      enablePadding: false,\n      builder: (context) => Provider.value(\n        value: simpleTableContext,\n        child: SimpleTableCellBottomSheet(\n          type: widget.type,\n          cellNode: widget.node,\n          editorState: editorState,\n        ),\n      ),\n    );\n\n    // reset the selecting row or column\n    simpleTableContext.selectingRow.value = null;\n    simpleTableContext.selectingColumn.value = null;\n\n    widget.isShowingMenu.value = false;\n  }\n\n  void _onUpdateShowingMenu() {\n    // highlight the reorder button when the row or column is selected\n    final selectingRow = simpleTableContext.selectingRow.value;\n    final selectingColumn = simpleTableContext.selectingColumn.value;\n\n    if (selectingRow == widget.index &&\n        widget.type == SimpleTableMoreActionType.row) {\n      widget.isShowingMenu.value = true;\n    } else if (selectingColumn == widget.index &&\n        widget.type == SimpleTableMoreActionType.column) {\n      widget.isShowingMenu.value = true;\n    } else {\n      widget.isShowingMenu.value = false;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_add_column_and_row_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass SimpleTableAddColumnAndRowHoverButton extends StatelessWidget {\n  const SimpleTableAddColumnAndRowHoverButton({\n    super.key,\n    required this.editorState,\n    required this.node,\n  });\n\n  final EditorState editorState;\n  final Node node;\n\n  @override\n  Widget build(BuildContext context) {\n    assert(node.type == SimpleTableBlockKeys.type);\n\n    if (node.type != SimpleTableBlockKeys.type) {\n      return const SizedBox.shrink();\n    }\n\n    return ValueListenableBuilder(\n      valueListenable: context.read<SimpleTableContext>().isHoveringOnTableArea,\n      builder: (context, isHoveringOnTableArea, child) {\n        return ValueListenableBuilder(\n          valueListenable: context.read<SimpleTableContext>().hoveringTableCell,\n          builder: (context, hoveringTableCell, child) {\n            bool shouldShow = isHoveringOnTableArea;\n            if (hoveringTableCell != null &&\n                SimpleTableConstants.enableHoveringLogicV2) {\n              shouldShow = hoveringTableCell.isLastCellInTable;\n            }\n            return shouldShow\n                ? Positioned(\n                    bottom:\n                        SimpleTableConstants.addColumnAndRowButtonBottomPadding,\n                    right: SimpleTableConstants.addColumnButtonPadding,\n                    child: SimpleTableAddColumnAndRowButton(\n                      onTap: () {\n                        // cancel the selection to avoid flashing the selection\n                        editorState.selection = null;\n\n                        editorState.addColumnAndRowInTable(node);\n                      },\n                    ),\n                  )\n                : const SizedBox.shrink();\n          },\n        );\n      },\n    );\n  }\n}\n\nclass SimpleTableAddColumnAndRowButton extends StatelessWidget {\n  const SimpleTableAddColumnAndRowButton({\n    super.key,\n    this.onTap,\n  });\n\n  final VoidCallback? onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      message: LocaleKeys.document_plugins_simpleTable_clickToAddNewRowAndColumn\n          .tr(),\n      child: GestureDetector(\n        behavior: HitTestBehavior.translucent,\n        onTap: onTap,\n        child: MouseRegion(\n          cursor: SystemMouseCursors.click,\n          child: Container(\n            width: SimpleTableConstants.addColumnAndRowButtonWidth,\n            height: SimpleTableConstants.addColumnAndRowButtonHeight,\n            decoration: BoxDecoration(\n              borderRadius: BorderRadius.circular(\n                SimpleTableConstants.addColumnAndRowButtonCornerRadius,\n              ),\n              color: context.simpleTableMoreActionBackgroundColor,\n            ),\n            child: const FlowySvg(\n              FlowySvgs.add_s,\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_add_column_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass SimpleTableAddColumnHoverButton extends StatefulWidget {\n  const SimpleTableAddColumnHoverButton({\n    super.key,\n    required this.editorState,\n    required this.tableNode,\n  });\n\n  final EditorState editorState;\n  final Node tableNode;\n\n  @override\n  State<SimpleTableAddColumnHoverButton> createState() =>\n      _SimpleTableAddColumnHoverButtonState();\n}\n\nclass _SimpleTableAddColumnHoverButtonState\n    extends State<SimpleTableAddColumnHoverButton> {\n  late final interceptorKey =\n      'simple_table_add_column_hover_button_${widget.tableNode.id}';\n\n  SelectionGestureInterceptor? interceptor;\n\n  Offset? startDraggingOffset;\n  int? initialColumnCount;\n\n  @override\n  void initState() {\n    super.initState();\n\n    interceptor = SelectionGestureInterceptor(\n      key: interceptorKey,\n      canTap: (details) => !_isTapInBounds(details.globalPosition),\n    );\n    widget.editorState.service.selectionService\n        .registerGestureInterceptor(interceptor!);\n  }\n\n  @override\n  void dispose() {\n    widget.editorState.service.selectionService.unregisterGestureInterceptor(\n      interceptorKey,\n    );\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    assert(widget.tableNode.type == SimpleTableBlockKeys.type);\n\n    if (widget.tableNode.type != SimpleTableBlockKeys.type) {\n      return const SizedBox.shrink();\n    }\n\n    return ValueListenableBuilder(\n      valueListenable: context.read<SimpleTableContext>().isHoveringOnTableArea,\n      builder: (context, isHoveringOnTableArea, _) {\n        return ValueListenableBuilder(\n          valueListenable: context.read<SimpleTableContext>().hoveringTableCell,\n          builder: (context, hoveringTableCell, _) {\n            bool shouldShow = isHoveringOnTableArea;\n            if (hoveringTableCell != null &&\n                SimpleTableConstants.enableHoveringLogicV2) {\n              shouldShow = hoveringTableCell.columnIndex + 1 ==\n                  hoveringTableCell.columnLength;\n            }\n            return Positioned(\n              top: SimpleTableConstants.tableHitTestTopPadding -\n                  SimpleTableConstants.cellBorderWidth,\n              bottom: SimpleTableConstants.addColumnButtonBottomPadding,\n              right: 0,\n              child: Opacity(\n                opacity: shouldShow ? 1.0 : 0.0,\n                child: SimpleTableAddColumnButton(\n                  onTap: () {\n                    // cancel the selection to avoid flashing the selection\n                    widget.editorState.selection = null;\n\n                    widget.editorState.addColumnInTable(widget.tableNode);\n                  },\n                  onHorizontalDragStart: (details) {\n                    context.read<SimpleTableContext>().isDraggingColumn = true;\n                    startDraggingOffset = details.globalPosition;\n                    initialColumnCount = widget.tableNode.columnLength;\n                  },\n                  onHorizontalDragEnd: (details) {\n                    context.read<SimpleTableContext>().isDraggingColumn = false;\n                  },\n                  onHorizontalDragUpdate: (details) {\n                    _insertColumnInMemory(details);\n                  },\n                ),\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  bool _isTapInBounds(Offset offset) {\n    final renderBox = context.findRenderObject() as RenderBox?;\n    if (renderBox == null) {\n      return false;\n    }\n\n    final localPosition = renderBox.globalToLocal(offset);\n    final result = renderBox.paintBounds.contains(localPosition);\n\n    return result;\n  }\n\n  void _insertColumnInMemory(DragUpdateDetails details) {\n    if (!SimpleTableConstants.enableDragToExpandTable) {\n      return;\n    }\n\n    if (startDraggingOffset == null || initialColumnCount == null) {\n      return;\n    }\n\n    // calculate the horizontal offset from the start dragging offset\n    final horizontalOffset =\n        details.globalPosition.dx - startDraggingOffset!.dx;\n\n    const columnWidth = SimpleTableConstants.defaultColumnWidth;\n    final columnDelta = (horizontalOffset / columnWidth).round();\n\n    // if the change is less than 1 column, skip the operation\n    if (columnDelta.abs() < 1) {\n      return;\n    }\n\n    final firstEmptyColumnFromRight =\n        widget.tableNode.getFirstEmptyColumnFromRight();\n    if (firstEmptyColumnFromRight == null) {\n      return;\n    }\n\n    final currentColumnCount = widget.tableNode.columnLength;\n    final targetColumnCount = initialColumnCount! + columnDelta;\n\n    // There're 3 cases that we don't want to proceed:\n    // 1. targetColumnCount < 0: the table at least has 1 column\n    // 2. targetColumnCount == currentColumnCount: the table has no change\n    // 3. targetColumnCount <= initialColumnCount: the table has less columns than the initial column count\n    if (targetColumnCount <= 0 ||\n        targetColumnCount == currentColumnCount ||\n        targetColumnCount <= firstEmptyColumnFromRight) {\n      return;\n    }\n\n    if (targetColumnCount > currentColumnCount) {\n      widget.editorState.insertColumnInTable(\n        widget.tableNode,\n        targetColumnCount,\n        inMemoryUpdate: true,\n      );\n    } else {\n      widget.editorState.deleteColumnInTable(\n        widget.tableNode,\n        targetColumnCount,\n        inMemoryUpdate: true,\n      );\n    }\n  }\n}\n\nclass SimpleTableAddColumnButton extends StatelessWidget {\n  const SimpleTableAddColumnButton({\n    super.key,\n    this.onTap,\n    required this.onHorizontalDragStart,\n    required this.onHorizontalDragEnd,\n    required this.onHorizontalDragUpdate,\n  });\n\n  final VoidCallback? onTap;\n  final void Function(DragStartDetails) onHorizontalDragStart;\n  final void Function(DragEndDetails) onHorizontalDragEnd;\n  final void Function(DragUpdateDetails) onHorizontalDragUpdate;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      message: LocaleKeys.document_plugins_simpleTable_clickToAddNewColumn.tr(),\n      child: GestureDetector(\n        behavior: HitTestBehavior.translucent,\n        onTap: onTap,\n        onHorizontalDragStart: onHorizontalDragStart,\n        onHorizontalDragEnd: onHorizontalDragEnd,\n        onHorizontalDragUpdate: onHorizontalDragUpdate,\n        child: MouseRegion(\n          cursor: SystemMouseCursors.click,\n          child: Container(\n            width: SimpleTableConstants.addColumnButtonWidth,\n            margin: const EdgeInsets.symmetric(\n              horizontal: SimpleTableConstants.addColumnButtonPadding,\n            ),\n            decoration: BoxDecoration(\n              borderRadius: BorderRadius.circular(\n                SimpleTableConstants.addColumnButtonRadius,\n              ),\n              color: context.simpleTableMoreActionBackgroundColor,\n            ),\n            child: const FlowySvg(\n              FlowySvgs.add_s,\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_add_row_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass SimpleTableAddRowHoverButton extends StatefulWidget {\n  const SimpleTableAddRowHoverButton({\n    super.key,\n    required this.editorState,\n    required this.tableNode,\n  });\n\n  final EditorState editorState;\n  final Node tableNode;\n\n  @override\n  State<SimpleTableAddRowHoverButton> createState() =>\n      _SimpleTableAddRowHoverButtonState();\n}\n\nclass _SimpleTableAddRowHoverButtonState\n    extends State<SimpleTableAddRowHoverButton> {\n  late final interceptorKey =\n      'simple_table_add_row_hover_button_${widget.tableNode.id}';\n\n  SelectionGestureInterceptor? interceptor;\n\n  Offset? startDraggingOffset;\n  int? initialRowCount;\n\n  @override\n  void initState() {\n    super.initState();\n\n    interceptor = SelectionGestureInterceptor(\n      key: interceptorKey,\n      canTap: (details) => !_isTapInBounds(details.globalPosition),\n    );\n    widget.editorState.service.selectionService\n        .registerGestureInterceptor(interceptor!);\n  }\n\n  @override\n  void dispose() {\n    widget.editorState.service.selectionService.unregisterGestureInterceptor(\n      interceptorKey,\n    );\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    assert(widget.tableNode.type == SimpleTableBlockKeys.type);\n\n    if (widget.tableNode.type != SimpleTableBlockKeys.type) {\n      return const SizedBox.shrink();\n    }\n\n    final simpleTableContext = context.read<SimpleTableContext>();\n    return ValueListenableBuilder(\n      valueListenable: simpleTableContext.isHoveringOnTableArea,\n      builder: (context, isHoveringOnTableArea, child) {\n        return ValueListenableBuilder(\n          valueListenable: simpleTableContext.hoveringTableCell,\n          builder: (context, hoveringTableCell, _) {\n            bool shouldShow = isHoveringOnTableArea;\n            if (hoveringTableCell != null &&\n                SimpleTableConstants.enableHoveringLogicV2) {\n              shouldShow =\n                  hoveringTableCell.rowIndex + 1 == hoveringTableCell.rowLength;\n            }\n            if (simpleTableContext.isDraggingRow) {\n              shouldShow = true;\n            }\n            return shouldShow ? child! : const SizedBox.shrink();\n          },\n        );\n      },\n      child: Positioned(\n        bottom: 2 * SimpleTableConstants.addRowButtonPadding,\n        left: SimpleTableConstants.tableLeftPadding -\n            SimpleTableConstants.cellBorderWidth,\n        right: SimpleTableConstants.addRowButtonRightPadding,\n        child: SimpleTableAddRowButton(\n          onTap: () {\n            // cancel the selection to avoid flashing the selection\n            widget.editorState.selection = null;\n\n            widget.editorState.addRowInTable(\n              widget.tableNode,\n            );\n          },\n          onVerticalDragStart: (details) {\n            context.read<SimpleTableContext>().isDraggingRow = true;\n            startDraggingOffset = details.globalPosition;\n            initialRowCount = widget.tableNode.children.length;\n          },\n          onVerticalDragEnd: (details) {\n            context.read<SimpleTableContext>().isDraggingRow = false;\n          },\n          onVerticalDragUpdate: (details) {\n            _insertRowInMemory(details);\n          },\n        ),\n      ),\n    );\n  }\n\n  bool _isTapInBounds(Offset offset) {\n    final renderBox = context.findRenderObject() as RenderBox?;\n    if (renderBox == null) {\n      return false;\n    }\n\n    final localPosition = renderBox.globalToLocal(offset);\n    final result = renderBox.paintBounds.contains(localPosition);\n\n    return result;\n  }\n\n  void _insertRowInMemory(DragUpdateDetails details) {\n    if (!SimpleTableConstants.enableDragToExpandTable) {\n      return;\n    }\n\n    if (startDraggingOffset == null || initialRowCount == null) {\n      return;\n    }\n\n    // calculate the vertical offset from the start dragging offset\n    final verticalOffset = details.globalPosition.dy - startDraggingOffset!.dy;\n\n    const rowHeight = SimpleTableConstants.defaultRowHeight;\n    final rowDelta = (verticalOffset / rowHeight).round();\n\n    // if the change is less than 1 row, skip the operation\n    if (rowDelta.abs() < 1) {\n      return;\n    }\n\n    final firstEmptyRowFromBottom =\n        widget.tableNode.getFirstEmptyRowFromBottom();\n    if (firstEmptyRowFromBottom == null) {\n      return;\n    }\n\n    final currentRowCount = widget.tableNode.children.length;\n    final targetRowCount = initialRowCount! + rowDelta;\n\n    // There're 3 cases that we don't want to proceed:\n    // 1. targetRowCount < 0: the table at least has 1 row\n    // 2. targetRowCount == currentRowCount: the table has no change\n    // 3. targetRowCount <= initialRowCount: the table has less rows than the initial row count\n    if (targetRowCount <= 0 ||\n        targetRowCount == currentRowCount ||\n        targetRowCount <= firstEmptyRowFromBottom.$1) {\n      return;\n    }\n\n    if (targetRowCount > currentRowCount) {\n      widget.editorState.insertRowInTable(\n        widget.tableNode,\n        targetRowCount,\n        inMemoryUpdate: true,\n      );\n    } else {\n      widget.editorState.deleteRowInTable(\n        widget.tableNode,\n        targetRowCount,\n        inMemoryUpdate: true,\n      );\n    }\n  }\n}\n\nclass SimpleTableAddRowButton extends StatelessWidget {\n  const SimpleTableAddRowButton({\n    super.key,\n    this.onTap,\n    required this.onVerticalDragStart,\n    required this.onVerticalDragEnd,\n    required this.onVerticalDragUpdate,\n  });\n\n  final VoidCallback? onTap;\n  final void Function(DragStartDetails) onVerticalDragStart;\n  final void Function(DragEndDetails) onVerticalDragEnd;\n  final void Function(DragUpdateDetails) onVerticalDragUpdate;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      message: LocaleKeys.document_plugins_simpleTable_clickToAddNewRow.tr(),\n      child: GestureDetector(\n        behavior: HitTestBehavior.opaque,\n        onTap: onTap,\n        onVerticalDragStart: onVerticalDragStart,\n        onVerticalDragEnd: onVerticalDragEnd,\n        onVerticalDragUpdate: onVerticalDragUpdate,\n        child: MouseRegion(\n          cursor: SystemMouseCursors.click,\n          child: Container(\n            height: SimpleTableConstants.addRowButtonHeight,\n            margin: const EdgeInsets.symmetric(\n              vertical: SimpleTableConstants.addColumnButtonPadding,\n            ),\n            decoration: BoxDecoration(\n              borderRadius: BorderRadius.circular(\n                SimpleTableConstants.addRowButtonRadius,\n              ),\n              color: context.simpleTableMoreActionBackgroundColor,\n            ),\n            child: const FlowySvg(\n              FlowySvgs.add_s,\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_align_button.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass SimpleTableAlignMenu extends StatefulWidget {\n  const SimpleTableAlignMenu({\n    super.key,\n    required this.type,\n    required this.tableCellNode,\n    this.mutex,\n  });\n\n  final SimpleTableMoreActionType type;\n  final Node tableCellNode;\n  final PopoverMutex? mutex;\n\n  @override\n  State<SimpleTableAlignMenu> createState() => _SimpleTableAlignMenuState();\n}\n\nclass _SimpleTableAlignMenuState extends State<SimpleTableAlignMenu> {\n  @override\n  Widget build(BuildContext context) {\n    final align = switch (widget.type) {\n      SimpleTableMoreActionType.column => widget.tableCellNode.columnAlign,\n      SimpleTableMoreActionType.row => widget.tableCellNode.rowAlign,\n    };\n    return AppFlowyPopover(\n      mutex: widget.mutex,\n      child: SimpleTableBasicButton(\n        leftIconSvg: align.leftIconSvg,\n        text: LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),\n        onTap: () {},\n      ),\n      popupBuilder: (popoverContext) {\n        void onClose() => PopoverContainer.of(popoverContext).closeAll();\n        return Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            _buildAlignButton(context, TableAlign.left, onClose),\n            _buildAlignButton(context, TableAlign.center, onClose),\n            _buildAlignButton(context, TableAlign.right, onClose),\n          ],\n        );\n      },\n    );\n  }\n\n  Widget _buildAlignButton(\n    BuildContext context,\n    TableAlign align,\n    VoidCallback onClose,\n  ) {\n    return SimpleTableBasicButton(\n      leftIconSvg: align.leftIconSvg,\n      text: align.name,\n      onTap: () {\n        switch (widget.type) {\n          case SimpleTableMoreActionType.column:\n            context.read<EditorState>().updateColumnAlign(\n                  tableCellNode: widget.tableCellNode,\n                  align: align,\n                );\n            break;\n          case SimpleTableMoreActionType.row:\n            context.read<EditorState>().updateRowAlign(\n                  tableCellNode: widget.tableCellNode,\n                  align: align,\n                );\n            break;\n        }\n\n        onClose();\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_background_menu.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass SimpleTableBackgroundColorMenu extends StatefulWidget {\n  const SimpleTableBackgroundColorMenu({\n    super.key,\n    required this.type,\n    required this.tableCellNode,\n    this.mutex,\n  });\n\n  final SimpleTableMoreActionType type;\n  final Node tableCellNode;\n  final PopoverMutex? mutex;\n\n  @override\n  State<SimpleTableBackgroundColorMenu> createState() =>\n      _SimpleTableBackgroundColorMenuState();\n}\n\nclass _SimpleTableBackgroundColorMenuState\n    extends State<SimpleTableBackgroundColorMenu> {\n  @override\n  Widget build(BuildContext context) {\n    final theme = AFThemeExtension.of(context);\n    final backgroundColor = switch (widget.type) {\n      SimpleTableMoreActionType.row =>\n        widget.tableCellNode.buildRowColor(context),\n      SimpleTableMoreActionType.column =>\n        widget.tableCellNode.buildColumnColor(context),\n    };\n    return AppFlowyPopover(\n      mutex: widget.mutex,\n      popupBuilder: (popoverContext) {\n        return _buildColorOptionMenu(\n          context,\n          theme: theme,\n          onClose: () => PopoverContainer.of(popoverContext).closeAll(),\n        );\n      },\n      direction: PopoverDirection.rightWithCenterAligned,\n      child: SimpleTableBasicButton(\n        leftIconBuilder: (onHover) => ColorOptionIcon(\n          color: backgroundColor ?? Colors.transparent,\n        ),\n        text: LocaleKeys.document_plugins_simpleTable_moreActions_color.tr(),\n        onTap: () {},\n      ),\n    );\n  }\n\n  Widget _buildColorOptionMenu(\n    BuildContext context, {\n    required AFThemeExtension theme,\n    required VoidCallback onClose,\n  }) {\n    final colors = [\n      // reset to default background color\n      FlowyColorOption(\n        color: Colors.transparent,\n        i18n: LocaleKeys.document_plugins_optionAction_defaultColor.tr(),\n        id: optionActionColorDefaultColor,\n      ),\n      ...FlowyTint.values.map(\n        (e) => FlowyColorOption(\n          color: e.color(context, theme: theme),\n          i18n: e.tintName(AppFlowyEditorL10n.current),\n          id: e.id,\n        ),\n      ),\n    ];\n\n    return FlowyColorPicker(\n      colors: colors,\n      border: Border.all(\n        color: theme.onBackground,\n      ),\n      onTap: (option, index) {\n        switch (widget.type) {\n          case SimpleTableMoreActionType.column:\n            context.read<EditorState>().updateColumnBackgroundColor(\n                  tableCellNode: widget.tableCellNode,\n                  color: option.id,\n                );\n            break;\n          case SimpleTableMoreActionType.row:\n            context.read<EditorState>().updateRowBackgroundColor(\n                  tableCellNode: widget.tableCellNode,\n                  color: option.id,\n                );\n            break;\n        }\n\n        onClose();\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_basic_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass SimpleTableBasicButton extends StatelessWidget {\n  const SimpleTableBasicButton({\n    super.key,\n    required this.text,\n    required this.onTap,\n    this.leftIconSvg,\n    this.leftIconBuilder,\n    this.rightIcon,\n  });\n\n  final FlowySvgData? leftIconSvg;\n  final String text;\n  final VoidCallback onTap;\n  final Widget Function(bool onHover)? leftIconBuilder;\n  final Widget? rightIcon;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: SimpleTableConstants.moreActionHeight,\n      padding: SimpleTableConstants.moreActionPadding,\n      child: FlowyIconTextButton(\n        margin: SimpleTableConstants.moreActionHorizontalMargin,\n        leftIconBuilder: _buildLeftIcon,\n        iconPadding: 10.0,\n        textBuilder: (onHover) => FlowyText.regular(\n          text,\n          fontSize: 14.0,\n          figmaLineHeight: 18.0,\n        ),\n        onTap: onTap,\n        rightIconBuilder: (onHover) => rightIcon ?? const SizedBox.shrink(),\n      ),\n    );\n  }\n\n  Widget _buildLeftIcon(bool onHover) {\n    if (leftIconBuilder != null) {\n      return leftIconBuilder!(onHover);\n    }\n    return leftIconSvg != null\n        ? FlowySvg(leftIconSvg!)\n        : const SizedBox.shrink();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_border_builder.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass SimpleTableBorderBuilder {\n  SimpleTableBorderBuilder({\n    required this.context,\n    required this.simpleTableContext,\n    required this.node,\n  });\n\n  final BuildContext context;\n  final SimpleTableContext simpleTableContext;\n  final Node node;\n\n  /// Build the border for the cell.\n  Border? buildBorder({\n    bool isEditingCell = false,\n  }) {\n    if (SimpleTableConstants.borderType != SimpleTableBorderRenderType.cell) {\n      return null;\n    }\n\n    // check if the cell is in the selected column\n    final isCellInSelectedColumn =\n        node.columnIndex == simpleTableContext.selectingColumn.value;\n\n    // check if the cell is in the selected row\n    final isCellInSelectedRow =\n        node.rowIndex == simpleTableContext.selectingRow.value;\n\n    final isReordering = simpleTableContext.isReordering &&\n        (simpleTableContext.isReorderingColumn.value.$1 ||\n            simpleTableContext.isReorderingRow.value.$1);\n\n    final editorState = context.read<EditorState>();\n    final editable = editorState.editable;\n\n    if (!editable) {\n      return buildCellBorder();\n    } else if (isReordering) {\n      return buildReorderingBorder();\n    } else if (simpleTableContext.isSelectingTable.value) {\n      return buildSelectingTableBorder();\n    } else if (isCellInSelectedColumn) {\n      return buildColumnHighlightBorder();\n    } else if (isCellInSelectedRow) {\n      return buildRowHighlightBorder();\n    } else if (isEditingCell) {\n      return buildEditingBorder();\n    } else {\n      return buildCellBorder();\n    }\n  }\n\n  /// the column border means the `VERTICAL` border of the cell\n  ///\n  ///      ____\n  /// | 1 | 2 |\n  /// | 3 | 4 |\n  ///     |___|\n  ///\n  /// the border wrapping the cell 2 and cell 4 is the column border\n  Border buildColumnHighlightBorder() {\n    return Border(\n      left: _buildHighlightBorderSide(),\n      right: _buildHighlightBorderSide(),\n      top: node.rowIndex == 0\n          ? _buildHighlightBorderSide()\n          : _buildLightBorderSide(),\n      bottom: node.rowIndex + 1 == node.parentTableNode?.rowLength\n          ? _buildHighlightBorderSide()\n          : _buildLightBorderSide(),\n    );\n  }\n\n  /// the row border means the `HORIZONTAL` border of the cell\n  ///\n  ///  ________\n  /// | 1 | 2 |\n  /// |_______|\n  /// | 3 | 4 |\n  ///\n  /// the border wrapping the cell 1 and cell 2 is the row border\n  Border buildRowHighlightBorder() {\n    return Border(\n      top: _buildHighlightBorderSide(),\n      bottom: _buildHighlightBorderSide(),\n      left: node.columnIndex == 0\n          ? _buildHighlightBorderSide()\n          : _buildLightBorderSide(),\n      right: node.columnIndex + 1 == node.parentTableNode?.columnLength\n          ? _buildHighlightBorderSide()\n          : _buildLightBorderSide(),\n    );\n  }\n\n  /// Build the border for the reordering state.\n  ///\n  /// For example, when reordering a column, we should highlight the border of the\n  /// current column we're hovering.\n  Border buildReorderingBorder() {\n    final isReorderingColumn = simpleTableContext.isReorderingColumn.value.$1;\n    final isReorderingRow = simpleTableContext.isReorderingRow.value.$1;\n\n    if (isReorderingColumn) {\n      return _buildColumnReorderingBorder();\n    } else if (isReorderingRow) {\n      return _buildRowReorderingBorder();\n    }\n\n    return buildCellBorder();\n  }\n\n  /// Build the border for the cell without any state.\n  Border buildCellBorder() {\n    return Border(\n      top: node.rowIndex == 0\n          ? _buildDefaultBorderSide()\n          : _buildLightBorderSide(),\n      bottom: node.rowIndex + 1 == node.parentTableNode?.rowLength\n          ? _buildDefaultBorderSide()\n          : _buildLightBorderSide(),\n      left: node.columnIndex == 0\n          ? _buildDefaultBorderSide()\n          : _buildLightBorderSide(),\n      right: node.columnIndex + 1 == node.parentTableNode?.columnLength\n          ? _buildDefaultBorderSide()\n          : _buildLightBorderSide(),\n    );\n  }\n\n  /// Build the border for the editing state.\n  Border buildEditingBorder() {\n    return Border.all(\n      color: Theme.of(context).colorScheme.primary,\n      width: 2,\n    );\n  }\n\n  /// Build the border for the selecting table state.\n  Border buildSelectingTableBorder() {\n    final rowIndex = node.rowIndex;\n    final columnIndex = node.columnIndex;\n\n    return Border(\n      top:\n          rowIndex == 0 ? _buildHighlightBorderSide() : _buildLightBorderSide(),\n      bottom: rowIndex + 1 == node.parentTableNode?.rowLength\n          ? _buildHighlightBorderSide()\n          : _buildLightBorderSide(),\n      left: columnIndex == 0\n          ? _buildHighlightBorderSide()\n          : _buildLightBorderSide(),\n      right: columnIndex + 1 == node.parentTableNode?.columnLength\n          ? _buildHighlightBorderSide()\n          : _buildLightBorderSide(),\n    );\n  }\n\n  Border _buildColumnReorderingBorder() {\n    assert(simpleTableContext.isReordering);\n\n    final isDraggingInCurrentColumn =\n        simpleTableContext.isReorderingColumn.value.$2 == node.columnIndex;\n    // if the dragging column is the current column, don't show the highlight border\n    if (isDraggingInCurrentColumn) {\n      return buildCellBorder();\n    }\n\n    bool isHitCurrentCell = false;\n\n    if (UniversalPlatform.isDesktop) {\n      // On desktop, we use the dragging column index to determine the highlight border\n      // Check if the hovering table cell column index hit the current node column index\n      isHitCurrentCell =\n          simpleTableContext.hoveringTableCell.value?.columnIndex ==\n              node.columnIndex;\n    } else if (UniversalPlatform.isMobile) {\n      // On mobile, we use the isReorderingHitIndex to determine the highlight border\n      isHitCurrentCell =\n          simpleTableContext.isReorderingHitIndex.value == node.columnIndex;\n    }\n\n    // if the hovering column is not the current column, don't show the highlight border\n    if (!isHitCurrentCell) {\n      return buildCellBorder();\n    }\n\n    // if the dragging column index is less than the current column index, show the\n    // highlight border on the left side\n    final isLeftSide =\n        simpleTableContext.isReorderingColumn.value.$2 > node.columnIndex;\n    // if the dragging column index is greater than the current column index, show\n    // the highlight border on the right side\n    final isRightSide =\n        simpleTableContext.isReorderingColumn.value.$2 < node.columnIndex;\n\n    return Border(\n      top: node.rowIndex == 0\n          ? _buildDefaultBorderSide()\n          : _buildLightBorderSide(),\n      bottom: node.rowIndex + 1 == node.parentTableNode?.rowLength\n          ? _buildDefaultBorderSide()\n          : _buildLightBorderSide(),\n      left: isLeftSide ? _buildHighlightBorderSide() : _buildLightBorderSide(),\n      right:\n          isRightSide ? _buildHighlightBorderSide() : _buildLightBorderSide(),\n    );\n  }\n\n  Border _buildRowReorderingBorder() {\n    assert(simpleTableContext.isReordering);\n\n    final isDraggingInCurrentRow =\n        simpleTableContext.isReorderingRow.value.$2 == node.rowIndex;\n    // if the dragging row is the current row, don't show the highlight border\n    if (isDraggingInCurrentRow) {\n      return buildCellBorder();\n    }\n\n    bool isHitCurrentCell = false;\n\n    if (UniversalPlatform.isDesktop) {\n      // On desktop, we use the dragging row index to determine the highlight border\n      // Check if the hovering table cell row index hit the current node row index\n      isHitCurrentCell =\n          simpleTableContext.hoveringTableCell.value?.rowIndex == node.rowIndex;\n    } else if (UniversalPlatform.isMobile) {\n      // On mobile, we use the isReorderingHitIndex to determine the highlight border\n      isHitCurrentCell =\n          simpleTableContext.isReorderingHitIndex.value == node.rowIndex;\n    }\n\n    if (!isHitCurrentCell) {\n      return buildCellBorder();\n    }\n\n    // For the row reordering, we only need to update the top and bottom border\n    final isTopSide =\n        simpleTableContext.isReorderingRow.value.$2 > node.rowIndex;\n    final isBottomSide =\n        simpleTableContext.isReorderingRow.value.$2 < node.rowIndex;\n\n    return Border(\n      top: isTopSide ? _buildHighlightBorderSide() : _buildLightBorderSide(),\n      bottom:\n          isBottomSide ? _buildHighlightBorderSide() : _buildLightBorderSide(),\n      left: node.columnIndex == 0\n          ? _buildDefaultBorderSide()\n          : _buildLightBorderSide(),\n      right: node.columnIndex + 1 == node.parentTableNode?.columnLength\n          ? _buildDefaultBorderSide()\n          : _buildLightBorderSide(),\n    );\n  }\n\n  BorderSide _buildHighlightBorderSide() {\n    return BorderSide(\n      color: Theme.of(context).colorScheme.primary,\n      width: 2,\n    );\n  }\n\n  BorderSide _buildLightBorderSide() {\n    return BorderSide(\n      color: context.simpleTableBorderColor,\n      width: 0.5,\n    );\n  }\n\n  BorderSide _buildDefaultBorderSide() {\n    return BorderSide(\n      color: context.simpleTableBorderColor,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_bottom_sheet.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nenum _SimpleTableBottomSheetMenuState {\n  cellActionMenu,\n  textColor,\n  textBackgroundColor,\n  tableActionMenu,\n  align,\n}\n\n/// This bottom sheet is used for the column or row action menu.\n/// When selecting a cell and tapping the action menu button around the cell,\n/// this bottom sheet will be shown.\n///\n/// Note: This widget is only used for mobile.\nclass SimpleTableCellBottomSheet extends StatefulWidget {\n  const SimpleTableCellBottomSheet({\n    super.key,\n    required this.type,\n    required this.cellNode,\n    required this.editorState,\n    this.scrollController,\n  });\n\n  final SimpleTableMoreActionType type;\n  final Node cellNode;\n  final EditorState editorState;\n  final ScrollController? scrollController;\n\n  @override\n  State<SimpleTableCellBottomSheet> createState() =>\n      _SimpleTableCellBottomSheetState();\n}\n\nclass _SimpleTableCellBottomSheetState\n    extends State<SimpleTableCellBottomSheet> {\n  _SimpleTableBottomSheetMenuState menuState =\n      _SimpleTableBottomSheetMenuState.cellActionMenu;\n\n  Color? selectedTextColor;\n  Color? selectedCellBackgroundColor;\n  TableAlign? selectedAlign;\n\n  @override\n  void initState() {\n    super.initState();\n\n    selectedTextColor = switch (widget.type) {\n      SimpleTableMoreActionType.column =>\n        widget.cellNode.textColorInColumn?.tryToColor(),\n      SimpleTableMoreActionType.row =>\n        widget.cellNode.textColorInRow?.tryToColor(),\n    };\n\n    selectedCellBackgroundColor = switch (widget.type) {\n      SimpleTableMoreActionType.column =>\n        widget.cellNode.buildColumnColor(context),\n      SimpleTableMoreActionType.row => widget.cellNode.buildRowColor(context),\n    };\n\n    selectedAlign = switch (widget.type) {\n      SimpleTableMoreActionType.column => widget.cellNode.columnAlign,\n      SimpleTableMoreActionType.row => widget.cellNode.rowAlign,\n    };\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        // header\n        _buildHeader(),\n\n        // content\n        ...menuState == _SimpleTableBottomSheetMenuState.cellActionMenu\n            ? _buildScrollableContent()\n            : _buildNonScrollableContent(),\n      ],\n    );\n  }\n\n  Widget _buildHeader() {\n    switch (menuState) {\n      case _SimpleTableBottomSheetMenuState.cellActionMenu:\n        return BottomSheetHeader(\n          showBackButton: false,\n          showCloseButton: true,\n          showDoneButton: false,\n          showRemoveButton: false,\n          title: widget.type.name.capitalize(),\n          onClose: () => Navigator.pop(context),\n        );\n      case _SimpleTableBottomSheetMenuState.textColor ||\n            _SimpleTableBottomSheetMenuState.textBackgroundColor:\n        return BottomSheetHeader(\n          showBackButton: false,\n          showCloseButton: true,\n          showDoneButton: true,\n          showRemoveButton: false,\n          title: widget.type.name.capitalize(),\n          onClose: () => setState(() {\n            menuState = _SimpleTableBottomSheetMenuState.cellActionMenu;\n          }),\n          onDone: (_) => Navigator.pop(context),\n        );\n      default:\n        throw UnimplementedError('Unsupported menu state: $menuState');\n    }\n  }\n\n  List<Widget> _buildScrollableContent() {\n    return [\n      SizedBox(\n        height: SimpleTableConstants.actionSheetBottomSheetHeight,\n        child: Scrollbar(\n          controller: widget.scrollController,\n          child: SingleChildScrollView(\n            controller: widget.scrollController,\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                ..._buildContent(),\n\n                // safe area padding\n                VSpace(context.bottomSheetPadding() * 2),\n              ],\n            ),\n          ),\n        ),\n      ),\n    ];\n  }\n\n  List<Widget> _buildNonScrollableContent() {\n    return [\n      ..._buildContent(),\n\n      // safe area padding\n      VSpace(context.bottomSheetPadding()),\n    ];\n  }\n\n  List<Widget> _buildContent() {\n    switch (menuState) {\n      case _SimpleTableBottomSheetMenuState.cellActionMenu:\n        return _buildActionButtons();\n      case _SimpleTableBottomSheetMenuState.textColor:\n        return _buildTextColor();\n      case _SimpleTableBottomSheetMenuState.textBackgroundColor:\n        return _buildTextBackgroundColor();\n      default:\n        throw UnimplementedError('Unsupported menu state: $menuState');\n    }\n  }\n\n  List<Widget> _buildActionButtons() {\n    return [\n      // copy, cut, paste, delete\n      SimpleTableCellQuickActions(\n        type: widget.type,\n        cellNode: widget.cellNode,\n        editorState: widget.editorState,\n      ),\n      const VSpace(12),\n\n      // insert row, insert column\n      SimpleTableInsertActions(\n        type: widget.type,\n        cellNode: widget.cellNode,\n        editorState: widget.editorState,\n      ),\n      const VSpace(12),\n\n      // content actions\n      SimpleTableContentActions(\n        type: widget.type,\n        cellNode: widget.cellNode,\n        editorState: widget.editorState,\n        selectedAlign: selectedAlign,\n        selectedTextColor: selectedTextColor,\n        selectedCellBackgroundColor: selectedCellBackgroundColor,\n        onTextColorSelected: () {\n          setState(() {\n            menuState = _SimpleTableBottomSheetMenuState.textColor;\n          });\n        },\n        onCellBackgroundColorSelected: () {\n          setState(() {\n            menuState = _SimpleTableBottomSheetMenuState.textBackgroundColor;\n          });\n        },\n        onAlignTap: _onAlignTap,\n      ),\n      const VSpace(16),\n\n      // action buttons\n      SimpleTableCellActionButtons(\n        type: widget.type,\n        cellNode: widget.cellNode,\n        editorState: widget.editorState,\n      ),\n    ];\n  }\n\n  List<Widget> _buildTextColor() {\n    return [\n      Padding(\n        padding: const EdgeInsets.symmetric(\n          horizontal: 16.0,\n        ),\n        child: FlowyText(\n          LocaleKeys.document_plugins_simpleTable_moreActions_textColor.tr(),\n          fontSize: 14.0,\n        ),\n      ),\n      const VSpace(12.0),\n      Padding(\n        padding: const EdgeInsets.symmetric(\n          horizontal: 8.0,\n        ),\n        child: EditorTextColorWidget(\n          onSelectedColor: _onTextColorSelected,\n          selectedColor: selectedTextColor,\n        ),\n      ),\n    ];\n  }\n\n  List<Widget> _buildTextBackgroundColor() {\n    return [\n      Padding(\n        padding: const EdgeInsets.symmetric(\n          horizontal: 16.0,\n        ),\n        child: FlowyText(\n          LocaleKeys\n              .document_plugins_simpleTable_moreActions_cellBackgroundColor\n              .tr(),\n          fontSize: 14.0,\n        ),\n      ),\n      const VSpace(12.0),\n      Padding(\n        padding: const EdgeInsets.symmetric(\n          horizontal: 8.0,\n        ),\n        child: EditorBackgroundColors(\n          onSelectedColor: _onCellBackgroundColorSelected,\n          selectedColor: selectedCellBackgroundColor,\n        ),\n      ),\n    ];\n  }\n\n  void _onTextColorSelected(Color color) {\n    final hex = color.a == 0 ? null : color.toHex();\n    switch (widget.type) {\n      case SimpleTableMoreActionType.column:\n        widget.editorState.updateColumnTextColor(\n          tableCellNode: widget.cellNode,\n          color: hex ?? '',\n        );\n      case SimpleTableMoreActionType.row:\n        widget.editorState.updateRowTextColor(\n          tableCellNode: widget.cellNode,\n          color: hex ?? '',\n        );\n    }\n\n    setState(() {\n      selectedTextColor = color;\n    });\n  }\n\n  void _onCellBackgroundColorSelected(Color color) {\n    final hex = color.a == 0 ? null : color.toHex();\n    switch (widget.type) {\n      case SimpleTableMoreActionType.column:\n        widget.editorState.updateColumnBackgroundColor(\n          tableCellNode: widget.cellNode,\n          color: hex ?? '',\n        );\n      case SimpleTableMoreActionType.row:\n        widget.editorState.updateRowBackgroundColor(\n          tableCellNode: widget.cellNode,\n          color: hex ?? '',\n        );\n    }\n\n    setState(() {\n      selectedCellBackgroundColor = color;\n    });\n  }\n\n  void _onAlignTap(TableAlign align) {\n    switch (widget.type) {\n      case SimpleTableMoreActionType.column:\n        widget.editorState.updateColumnAlign(\n          tableCellNode: widget.cellNode,\n          align: align,\n        );\n      case SimpleTableMoreActionType.row:\n        widget.editorState.updateRowAlign(\n          tableCellNode: widget.cellNode,\n          align: align,\n        );\n    }\n\n    setState(() {\n      selectedAlign = align;\n    });\n  }\n}\n\n/// This bottom sheet is used for the table action menu.\n/// When selecting a table and tapping the action menu button on the top-left corner of the table,\n/// this bottom sheet will be shown.\n///\n/// Note: This widget is only used for mobile.\nclass SimpleTableBottomSheet extends StatefulWidget {\n  const SimpleTableBottomSheet({\n    super.key,\n    required this.tableNode,\n    required this.editorState,\n    this.scrollController,\n  });\n\n  final Node tableNode;\n  final EditorState editorState;\n  final ScrollController? scrollController;\n\n  @override\n  State<SimpleTableBottomSheet> createState() => _SimpleTableBottomSheetState();\n}\n\nclass _SimpleTableBottomSheetState extends State<SimpleTableBottomSheet> {\n  _SimpleTableBottomSheetMenuState menuState =\n      _SimpleTableBottomSheetMenuState.tableActionMenu;\n\n  TableAlign? selectedAlign;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        // header\n        _buildHeader(),\n\n        // content\n        SizedBox(\n          height: SimpleTableConstants.actionSheetBottomSheetHeight,\n          child: Scrollbar(\n            controller: widget.scrollController,\n            child: SingleChildScrollView(\n              controller: widget.scrollController,\n              child: Column(\n                children: [\n                  // content\n                  ..._buildContent(),\n\n                  // safe area padding\n                  VSpace(context.bottomSheetPadding() * 2),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildHeader() {\n    switch (menuState) {\n      case _SimpleTableBottomSheetMenuState.tableActionMenu:\n        return BottomSheetHeader(\n          showBackButton: false,\n          showCloseButton: true,\n          showDoneButton: false,\n          showRemoveButton: false,\n          title: LocaleKeys.document_plugins_simpleTable_headerName_table.tr(),\n          onClose: () => Navigator.pop(context),\n        );\n      case _SimpleTableBottomSheetMenuState.align:\n        return BottomSheetHeader(\n          showBackButton: true,\n          showCloseButton: false,\n          showDoneButton: true,\n          showRemoveButton: false,\n          title: LocaleKeys.document_plugins_simpleTable_headerName_table.tr(),\n          onBack: () => setState(() {\n            menuState = _SimpleTableBottomSheetMenuState.tableActionMenu;\n          }),\n          onDone: (_) => Navigator.pop(context),\n        );\n      default:\n        throw UnimplementedError('Unsupported menu state: $menuState');\n    }\n  }\n\n  List<Widget> _buildContent() {\n    switch (menuState) {\n      case _SimpleTableBottomSheetMenuState.tableActionMenu:\n        return _buildActionButtons();\n      case _SimpleTableBottomSheetMenuState.align:\n        return _buildAlign();\n      default:\n        throw UnimplementedError('Unsupported menu state: $menuState');\n    }\n  }\n\n  List<Widget> _buildActionButtons() {\n    return [\n      // quick actions\n      // copy, cut, paste, delete\n      SimpleTableQuickActions(\n        tableNode: widget.tableNode,\n        editorState: widget.editorState,\n      ),\n      const VSpace(24),\n\n      // action buttons\n      SimpleTableActionButtons(\n        tableNode: widget.tableNode,\n        editorState: widget.editorState,\n        onAlignTap: _onTapAlignButton,\n      ),\n    ];\n  }\n\n  List<Widget> _buildAlign() {\n    return [\n      Padding(\n        padding: const EdgeInsets.symmetric(\n          horizontal: 16.0,\n        ),\n        child: Row(\n          children: [\n            _buildAlignButton(TableAlign.left),\n            const HSpace(2),\n            _buildAlignButton(TableAlign.center),\n            const HSpace(2),\n            _buildAlignButton(TableAlign.right),\n          ],\n        ),\n      ),\n    ];\n  }\n\n  Widget _buildAlignButton(TableAlign align) {\n    return SimpleTableContentAlignAction(\n      onTap: () => _onTapAlign(align),\n      align: align,\n      isSelected: selectedAlign == align,\n    );\n  }\n\n  void _onTapAlignButton() {\n    setState(() {\n      menuState = _SimpleTableBottomSheetMenuState.align;\n    });\n  }\n\n  void _onTapAlign(TableAlign align) {\n    setState(() {\n      selectedAlign = align;\n    });\n\n    widget.editorState.updateTableAlign(\n      tableNode: widget.tableNode,\n      align: align,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_column_resize_handle.dart",
    "content": "import 'dart:ui';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass SimpleTableColumnResizeHandle extends StatefulWidget {\n  const SimpleTableColumnResizeHandle({\n    super.key,\n    required this.node,\n    this.isPreviousCell = false,\n  });\n\n  final Node node;\n  final bool isPreviousCell;\n\n  @override\n  State<SimpleTableColumnResizeHandle> createState() =>\n      _SimpleTableColumnResizeHandleState();\n}\n\nclass _SimpleTableColumnResizeHandleState\n    extends State<SimpleTableColumnResizeHandle> {\n  late final simpleTableContext = context.read<SimpleTableContext>();\n\n  bool isStartDragging = false;\n\n  // record the previous position of the drag, only used on mobile\n  double previousDx = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    return UniversalPlatform.isMobile\n        ? _buildMobileResizeHandle()\n        : _buildDesktopResizeHandle();\n  }\n\n  Widget _buildDesktopResizeHandle() {\n    return MouseRegion(\n      cursor: SystemMouseCursors.resizeColumn,\n      onEnter: (_) => _onEnterHoverArea(),\n      onExit: (event) => _onExitHoverArea(),\n      child: GestureDetector(\n        onHorizontalDragStart: _onHorizontalDragStart,\n        onHorizontalDragUpdate: _onHorizontalDragUpdate,\n        onHorizontalDragEnd: _onHorizontalDragEnd,\n        child: ValueListenableBuilder(\n          valueListenable: simpleTableContext.hoveringOnResizeHandle,\n          builder: (context, hoveringOnResizeHandle, child) {\n            // when reordering a column, the resize handle should not be shown\n            final isSameRowIndex = hoveringOnResizeHandle?.columnIndex ==\n                    widget.node.columnIndex &&\n                !simpleTableContext.isReordering;\n            return Opacity(\n              opacity: isSameRowIndex ? 1.0 : 0.0,\n              child: child,\n            );\n          },\n          child: Container(\n            height: double.infinity,\n            width: SimpleTableConstants.resizeHandleWidth,\n            color: Theme.of(context).colorScheme.primary,\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildMobileResizeHandle() {\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onLongPressStart: _onLongPressStart,\n      onLongPressMoveUpdate: _onLongPressMoveUpdate,\n      onLongPressEnd: _onLongPressEnd,\n      onLongPressCancel: _onLongPressCancel,\n      child: ValueListenableBuilder(\n        valueListenable: simpleTableContext.resizingCell,\n        builder: (context, resizingCell, child) {\n          final isSameColumnIndex =\n              widget.node.columnIndex == resizingCell?.columnIndex;\n          if (!isSameColumnIndex) {\n            return child!;\n          }\n          return Container(\n            width: 10,\n            alignment: !widget.isPreviousCell\n                ? Alignment.centerRight\n                : Alignment.centerLeft,\n            child: Container(\n              width: 2,\n              color: Theme.of(context).colorScheme.primary,\n            ),\n          );\n        },\n        child: Container(\n          width: 10,\n          color: Colors.transparent,\n        ),\n      ),\n    );\n  }\n\n  void _onEnterHoverArea() {\n    simpleTableContext.hoveringOnResizeHandle.value = widget.node;\n  }\n\n  void _onExitHoverArea() {\n    Future.delayed(const Duration(milliseconds: 100), () {\n      // the onExit event will be triggered before dragging started.\n      // delay the hiding of the resize handle to avoid flickering.\n      if (!isStartDragging) {\n        simpleTableContext.hoveringOnResizeHandle.value = null;\n      }\n    });\n  }\n\n  void _onHorizontalDragStart(DragStartDetails details) {\n    // disable the two-finger drag on trackpad\n    if (details.kind == PointerDeviceKind.trackpad) {\n      return;\n    }\n\n    isStartDragging = true;\n  }\n\n  void _onLongPressStart(LongPressStartDetails details) {\n    isStartDragging = true;\n    simpleTableContext.resizingCell.value = widget.node;\n\n    HapticFeedback.lightImpact();\n  }\n\n  void _onHorizontalDragUpdate(DragUpdateDetails details) {\n    if (!isStartDragging) {\n      return;\n    }\n\n    // only update the column width in memory,\n    //  the actual update will be applied in _onHorizontalDragEnd\n    context.read<EditorState>().updateColumnWidthInMemory(\n          tableCellNode: widget.node,\n          deltaX: details.delta.dx,\n        );\n  }\n\n  void _onLongPressMoveUpdate(LongPressMoveUpdateDetails details) {\n    if (!isStartDragging) {\n      return;\n    }\n\n    // only update the column width in memory,\n    //  the actual update will be applied in _onHorizontalDragEnd\n    context.read<EditorState>().updateColumnWidthInMemory(\n          tableCellNode: widget.node,\n          deltaX: details.offsetFromOrigin.dx - previousDx,\n        );\n\n    previousDx = details.offsetFromOrigin.dx;\n  }\n\n  void _onHorizontalDragEnd(DragEndDetails details) {\n    if (!isStartDragging) {\n      return;\n    }\n\n    isStartDragging = false;\n    context.read<SimpleTableContext>().hoveringOnResizeHandle.value = null;\n\n    // apply the updated column width\n    context.read<EditorState>().updateColumnWidth(\n          tableCellNode: widget.node,\n          width: widget.node.columnWidth,\n        );\n  }\n\n  void _onLongPressEnd(LongPressEndDetails details) {\n    if (!isStartDragging) {\n      return;\n    }\n\n    isStartDragging = false;\n\n    // apply the updated column width\n    context.read<EditorState>().updateColumnWidth(\n          tableCellNode: widget.node,\n          width: widget.node.columnWidth,\n        );\n\n    previousDx = 0;\n\n    simpleTableContext.resizingCell.value = null;\n  }\n\n  void _onLongPressCancel() {\n    isStartDragging = false;\n    simpleTableContext.resizingCell.value = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_divider.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:flutter/material.dart';\n\nclass SimpleTableRowDivider extends StatelessWidget {\n  const SimpleTableRowDivider({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return VerticalDivider(\n      color: context.simpleTableBorderColor,\n      width: 1.0,\n    );\n  }\n}\n\nclass SimpleTableColumnDivider extends StatelessWidget {\n  const SimpleTableColumnDivider({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Divider(\n      color: context.simpleTableBorderColor,\n      height: 1.0,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_feedback.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass SimpleTableFeedback extends StatefulWidget {\n  const SimpleTableFeedback({\n    super.key,\n    required this.editorState,\n    required this.node,\n    required this.type,\n    required this.index,\n  });\n\n  /// The node of the table.\n  /// Its type must be one of the following:\n  ///   [SimpleTableBlockKeys.type], [SimpleTableRowBlockKeys.type], [SimpleTableCellBlockKeys.type].\n  final Node node;\n\n  /// The type of the more action.\n  ///\n  /// If the type is [SimpleTableMoreActionType.column], the feedback will use index as column index.\n  /// If the type is [SimpleTableMoreActionType.row], the feedback will use index as row index.\n  final SimpleTableMoreActionType type;\n\n  /// The index of the column or row.\n  final int index;\n\n  final EditorState editorState;\n\n  @override\n  State<SimpleTableFeedback> createState() => _SimpleTableFeedbackState();\n}\n\nclass _SimpleTableFeedbackState extends State<SimpleTableFeedback> {\n  final simpleTableContext = SimpleTableContext();\n  late final Node dummyNode;\n\n  @override\n  void initState() {\n    super.initState();\n\n    assert(\n      [\n        SimpleTableBlockKeys.type,\n        SimpleTableRowBlockKeys.type,\n        SimpleTableCellBlockKeys.type,\n      ].contains(widget.node.type),\n      'The node type must be one of the following: '\n      '[SimpleTableBlockKeys.type], [SimpleTableRowBlockKeys.type], [SimpleTableCellBlockKeys.type].',\n    );\n\n    simpleTableContext.isSelectingTable.value = true;\n    dummyNode = _buildDummyNode();\n  }\n\n  @override\n  void dispose() {\n    simpleTableContext.dispose();\n    dummyNode.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      color: Colors.transparent,\n      child: Provider.value(\n        value: widget.editorState,\n        child: SimpleTableWidget(\n          node: dummyNode,\n          simpleTableContext: simpleTableContext,\n          enableAddColumnButton: false,\n          enableAddRowButton: false,\n          enableAddColumnAndRowButton: false,\n          enableHoverEffect: false,\n          isFeedback: true,\n        ),\n      ),\n    );\n  }\n\n  /// Build the dummy node for the feedback.\n  ///\n  /// For example,\n  ///\n  /// If the type is [SimpleTableMoreActionType.row], we should build the dummy table node using the data from the first row of the table node.\n  /// If the type is [SimpleTableMoreActionType.column], we should build the dummy table node using the data from the first column of the table node.\n  Node _buildDummyNode() {\n    // deep copy the table node to avoid mutating the original node\n    final tableNode = widget.node.parentTableNode?.deepCopy();\n    if (tableNode == null) {\n      return simpleTableBlockNode(children: []);\n    }\n\n    switch (widget.type) {\n      case SimpleTableMoreActionType.row:\n        if (widget.index >= tableNode.rowLength || widget.index < 0) {\n          return simpleTableBlockNode(children: []);\n        }\n\n        final row = tableNode.children[widget.index];\n        return tableNode.copyWith(\n          children: [row],\n          attributes: {\n            ...tableNode.attributes,\n            if (widget.index != 0) SimpleTableBlockKeys.enableHeaderRow: false,\n          },\n        );\n      case SimpleTableMoreActionType.column:\n        if (widget.index >= tableNode.columnLength || widget.index < 0) {\n          return simpleTableBlockNode(children: []);\n        }\n\n        final rows = tableNode.children.map((row) {\n          final cell = row.children[widget.index];\n          return simpleTableRowBlockNode(children: [cell]);\n        }).toList();\n\n        final columnWidth = tableNode.columnWidths[widget.index.toString()] ??\n            SimpleTableConstants.defaultColumnWidth;\n\n        return tableNode.copyWith(\n          children: rows,\n          attributes: {\n            ...tableNode.attributes,\n            SimpleTableBlockKeys.columnWidths: {\n              '0': columnWidth,\n            },\n            if (widget.index != 0)\n              SimpleTableBlockKeys.enableHeaderColumn: false,\n          },\n        );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_more_action_popup.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass SimpleTableMoreActionPopup extends StatefulWidget {\n  const SimpleTableMoreActionPopup({\n    super.key,\n    required this.index,\n    required this.isShowingMenu,\n    required this.type,\n  });\n\n  final int index;\n  final ValueNotifier<bool> isShowingMenu;\n  final SimpleTableMoreActionType type;\n\n  @override\n  State<SimpleTableMoreActionPopup> createState() =>\n      _SimpleTableMoreActionPopupState();\n}\n\nclass _SimpleTableMoreActionPopupState\n    extends State<SimpleTableMoreActionPopup> {\n  late final editorState = context.read<EditorState>();\n\n  SelectionGestureInterceptor? gestureInterceptor;\n\n  RenderBox? get renderBox => context.findRenderObject() as RenderBox?;\n\n  late final simpleTableContext = context.read<SimpleTableContext>();\n  Node? tableNode;\n  Node? tableCellNode;\n\n  @override\n  void initState() {\n    super.initState();\n\n    tableCellNode = context.read<SimpleTableContext>().hoveringTableCell.value;\n    tableNode = tableCellNode?.parentTableNode;\n    gestureInterceptor = SelectionGestureInterceptor(\n      key: 'simple_table_more_action_popup_interceptor_${tableCellNode?.id}',\n      canTap: (details) => !_isTapInBounds(details.globalPosition),\n    );\n    editorState.service.selectionService.registerGestureInterceptor(\n      gestureInterceptor!,\n    );\n  }\n\n  @override\n  void dispose() {\n    if (gestureInterceptor != null) {\n      editorState.service.selectionService.unregisterGestureInterceptor(\n        gestureInterceptor!.key,\n      );\n    }\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (tableNode == null) {\n      return const SizedBox.shrink();\n    }\n\n    return AppFlowyPopover(\n      onOpen: () => _onOpen(tableCellNode: tableCellNode),\n      onClose: () => _onClose(),\n      canClose: () async {\n        return true;\n      },\n      direction: widget.type == SimpleTableMoreActionType.row\n          ? PopoverDirection.bottomWithCenterAligned\n          : PopoverDirection.bottomWithLeftAligned,\n      offset: widget.type == SimpleTableMoreActionType.row\n          ? const Offset(24, 14)\n          : const Offset(-14, 8),\n      clickHandler: PopoverClickHandler.gestureDetector,\n      popupBuilder: (_) => _buildPopup(tableCellNode: tableCellNode),\n      child: SimpleTableDraggableReorderButton(\n        editorState: editorState,\n        simpleTableContext: simpleTableContext,\n        node: tableNode!,\n        index: widget.index,\n        isShowingMenu: widget.isShowingMenu,\n        type: widget.type,\n      ),\n    );\n  }\n\n  Widget _buildPopup({Node? tableCellNode}) {\n    if (tableCellNode == null) {\n      return const SizedBox.shrink();\n    }\n    return MultiProvider(\n      providers: [\n        Provider.value(\n          value: context.read<SimpleTableContext>(),\n        ),\n        Provider.value(\n          value: context.read<EditorState>(),\n        ),\n      ],\n      child: SimpleTableMoreActionList(\n        type: widget.type,\n        index: widget.index,\n        tableCellNode: tableCellNode,\n      ),\n    );\n  }\n\n  void _onOpen({Node? tableCellNode}) {\n    widget.isShowingMenu.value = true;\n\n    switch (widget.type) {\n      case SimpleTableMoreActionType.column:\n        context.read<SimpleTableContext>().selectingColumn.value =\n            tableCellNode?.columnIndex;\n      case SimpleTableMoreActionType.row:\n        context.read<SimpleTableContext>().selectingRow.value =\n            tableCellNode?.rowIndex;\n    }\n\n    // Workaround to clear the selection after the menu is opened.\n    Future.delayed(Durations.short3, () {\n      if (!editorState.isDisposed) {\n        editorState.selection = null;\n      }\n    });\n  }\n\n  void _onClose() {\n    widget.isShowingMenu.value = false;\n\n    // clear the selecting index\n    context.read<SimpleTableContext>().selectingColumn.value = null;\n    context.read<SimpleTableContext>().selectingRow.value = null;\n  }\n\n  bool _isTapInBounds(Offset offset) {\n    if (renderBox == null) {\n      return false;\n    }\n\n    final localPosition = renderBox!.globalToLocal(offset);\n    final result = renderBox!.paintBounds.contains(localPosition);\n    if (result) {\n      editorState.selection = null;\n    }\n    return result;\n  }\n}\n\nclass SimpleTableMoreActionList extends StatefulWidget {\n  const SimpleTableMoreActionList({\n    super.key,\n    required this.type,\n    required this.index,\n    required this.tableCellNode,\n    this.mutex,\n  });\n\n  final SimpleTableMoreActionType type;\n  final int index;\n  final Node tableCellNode;\n  final PopoverMutex? mutex;\n\n  @override\n  State<SimpleTableMoreActionList> createState() =>\n      _SimpleTableMoreActionListState();\n}\n\nclass _SimpleTableMoreActionListState extends State<SimpleTableMoreActionList> {\n  // ensure the background color menu and align menu exclusive\n  final mutex = PopoverMutex();\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: widget.type\n          .buildDesktopActions(\n            index: widget.index,\n            columnLength: widget.tableCellNode.columnLength,\n            rowLength: widget.tableCellNode.rowLength,\n          )\n          .map(\n            (action) => SimpleTableMoreActionItem(\n              type: widget.type,\n              action: action,\n              tableCellNode: widget.tableCellNode,\n              popoverMutex: mutex,\n            ),\n          )\n          .toList(),\n    );\n  }\n}\n\nclass SimpleTableMoreActionItem extends StatefulWidget {\n  const SimpleTableMoreActionItem({\n    super.key,\n    required this.type,\n    required this.action,\n    required this.tableCellNode,\n    required this.popoverMutex,\n  });\n\n  final SimpleTableMoreActionType type;\n  final SimpleTableMoreAction action;\n  final Node tableCellNode;\n  final PopoverMutex popoverMutex;\n\n  @override\n  State<SimpleTableMoreActionItem> createState() =>\n      _SimpleTableMoreActionItemState();\n}\n\nclass _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {\n  final isEnableHeader = ValueNotifier(false);\n\n  @override\n  void initState() {\n    super.initState();\n\n    _initEnableHeader();\n  }\n\n  @override\n  void dispose() {\n    isEnableHeader.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (widget.action == SimpleTableMoreAction.divider) {\n      return _buildDivider(context);\n    } else if (widget.action == SimpleTableMoreAction.align) {\n      return _buildAlignMenu(context);\n    } else if (widget.action == SimpleTableMoreAction.backgroundColor) {\n      return _buildBackgroundColorMenu(context);\n    } else if (widget.action == SimpleTableMoreAction.enableHeaderColumn) {\n      return _buildEnableHeaderButton(context);\n    } else if (widget.action == SimpleTableMoreAction.enableHeaderRow) {\n      return _buildEnableHeaderButton(context);\n    }\n\n    return _buildActionButton(context);\n  }\n\n  Widget _buildDivider(BuildContext context) {\n    return const FlowyDivider(\n      padding: EdgeInsets.symmetric(\n        vertical: 4.0,\n      ),\n    );\n  }\n\n  Widget _buildAlignMenu(BuildContext context) {\n    return SimpleTableAlignMenu(\n      type: widget.type,\n      tableCellNode: widget.tableCellNode,\n      mutex: widget.popoverMutex,\n    );\n  }\n\n  Widget _buildBackgroundColorMenu(BuildContext context) {\n    return SimpleTableBackgroundColorMenu(\n      type: widget.type,\n      tableCellNode: widget.tableCellNode,\n      mutex: widget.popoverMutex,\n    );\n  }\n\n  Widget _buildEnableHeaderButton(BuildContext context) {\n    return SimpleTableBasicButton(\n      text: widget.action.name,\n      leftIconSvg: widget.action.leftIconSvg,\n      rightIcon: ValueListenableBuilder(\n        valueListenable: isEnableHeader,\n        builder: (context, isEnableHeader, child) {\n          return Toggle(\n            value: isEnableHeader,\n            onChanged: (value) => _toggleEnableHeader(),\n            padding: EdgeInsets.zero,\n          );\n        },\n      ),\n      onTap: _toggleEnableHeader,\n    );\n  }\n\n  Widget _buildActionButton(BuildContext context) {\n    return Container(\n      height: SimpleTableConstants.moreActionHeight,\n      padding: SimpleTableConstants.moreActionPadding,\n      child: FlowyIconTextButton(\n        margin: SimpleTableConstants.moreActionHorizontalMargin,\n        leftIconBuilder: (onHover) => FlowySvg(\n          widget.action.leftIconSvg,\n          color: widget.action == SimpleTableMoreAction.delete && onHover\n              ? Theme.of(context).colorScheme.error\n              : null,\n        ),\n        iconPadding: 10.0,\n        textBuilder: (onHover) => FlowyText.regular(\n          widget.action.name,\n          fontSize: 14.0,\n          figmaLineHeight: 18.0,\n          color: widget.action == SimpleTableMoreAction.delete && onHover\n              ? Theme.of(context).colorScheme.error\n              : null,\n        ),\n        onTap: _onAction,\n      ),\n    );\n  }\n\n  void _onAction() {\n    switch (widget.action) {\n      case SimpleTableMoreAction.delete:\n        switch (widget.type) {\n          case SimpleTableMoreActionType.column:\n            _deleteColumn();\n            break;\n          case SimpleTableMoreActionType.row:\n            _deleteRow();\n            break;\n        }\n      case SimpleTableMoreAction.insertLeft:\n        _insertColumnLeft();\n      case SimpleTableMoreAction.insertRight:\n        _insertColumnRight();\n      case SimpleTableMoreAction.insertAbove:\n        _insertRowAbove();\n      case SimpleTableMoreAction.insertBelow:\n        _insertRowBelow();\n      case SimpleTableMoreAction.clearContents:\n        _clearContent();\n      case SimpleTableMoreAction.duplicate:\n        switch (widget.type) {\n          case SimpleTableMoreActionType.column:\n            _duplicateColumn();\n            break;\n          case SimpleTableMoreActionType.row:\n            _duplicateRow();\n            break;\n        }\n      case SimpleTableMoreAction.setToPageWidth:\n        _setToPageWidth();\n      case SimpleTableMoreAction.distributeColumnsEvenly:\n        _distributeColumnsEvenly();\n      default:\n        break;\n    }\n\n    PopoverContainer.of(context).close();\n  }\n\n  void _setToPageWidth() {\n    final value = _getTableAndTableCellAndCellPosition();\n    if (value == null) {\n      return;\n    }\n    final (table, _, _) = value;\n    final editorState = context.read<EditorState>();\n    editorState.setColumnWidthToPageWidth(tableNode: table);\n  }\n\n  void _distributeColumnsEvenly() {\n    final value = _getTableAndTableCellAndCellPosition();\n    if (value == null) {\n      return;\n    }\n    final (table, _, _) = value;\n    final editorState = context.read<EditorState>();\n    editorState.distributeColumnWidthToPageWidth(tableNode: table);\n  }\n\n  void _duplicateRow() {\n    final value = _getTableAndTableCellAndCellPosition();\n    if (value == null) {\n      return;\n    }\n    final (table, node, _) = value;\n    final editorState = context.read<EditorState>();\n    editorState.duplicateRowInTable(table, node.rowIndex);\n  }\n\n  void _duplicateColumn() {\n    final value = _getTableAndTableCellAndCellPosition();\n    if (value == null) {\n      return;\n    }\n    final (table, node, _) = value;\n    final editorState = context.read<EditorState>();\n    editorState.duplicateColumnInTable(table, node.columnIndex);\n  }\n\n  void _toggleEnableHeader() {\n    final value = _getTableAndTableCellAndCellPosition();\n    if (value == null) {\n      return;\n    }\n\n    isEnableHeader.value = !isEnableHeader.value;\n\n    final (table, _, _) = value;\n    final editorState = context.read<EditorState>();\n    switch (widget.type) {\n      case SimpleTableMoreActionType.column:\n        editorState.toggleEnableHeaderColumn(\n          tableNode: table,\n          enable: isEnableHeader.value,\n        );\n      case SimpleTableMoreActionType.row:\n        editorState.toggleEnableHeaderRow(\n          tableNode: table,\n          enable: isEnableHeader.value,\n        );\n    }\n\n    PopoverContainer.of(context).close();\n  }\n\n  void _clearContent() {\n    final value = _getTableAndTableCellAndCellPosition();\n    if (value == null) {\n      return;\n    }\n    final (table, node, _) = value;\n    final editorState = context.read<EditorState>();\n    if (widget.type == SimpleTableMoreActionType.column) {\n      editorState.clearContentAtColumnIndex(\n        tableNode: table,\n        columnIndex: node.columnIndex,\n      );\n    } else if (widget.type == SimpleTableMoreActionType.row) {\n      editorState.clearContentAtRowIndex(\n        tableNode: table,\n        rowIndex: node.rowIndex,\n      );\n    }\n  }\n\n  void _insertColumnLeft() {\n    final value = _getTableAndTableCellAndCellPosition();\n    if (value == null) {\n      return;\n    }\n    final (table, node, _) = value;\n    final columnIndex = node.columnIndex;\n    final editorState = context.read<EditorState>();\n    editorState.insertColumnInTable(table, columnIndex);\n\n    final cell = table.getTableCellNode(\n      rowIndex: 0,\n      columnIndex: columnIndex,\n    );\n    if (cell == null) {\n      return;\n    }\n\n    // update selection\n    editorState.selection = Selection.collapsed(\n      Position(\n        path: cell.path.child(0),\n      ),\n    );\n  }\n\n  void _insertColumnRight() {\n    final value = _getTableAndTableCellAndCellPosition();\n    if (value == null) {\n      return;\n    }\n    final (table, node, _) = value;\n    final columnIndex = node.columnIndex;\n    final editorState = context.read<EditorState>();\n    editorState.insertColumnInTable(table, columnIndex + 1);\n\n    final cell = table.getTableCellNode(\n      rowIndex: 0,\n      columnIndex: columnIndex + 1,\n    );\n    if (cell == null) {\n      return;\n    }\n\n    // update selection\n    editorState.selection = Selection.collapsed(\n      Position(\n        path: cell.path.child(0),\n      ),\n    );\n  }\n\n  void _insertRowAbove() {\n    final value = _getTableAndTableCellAndCellPosition();\n    if (value == null) {\n      return;\n    }\n    final (table, node, _) = value;\n    final rowIndex = node.rowIndex;\n    final editorState = context.read<EditorState>();\n    editorState.insertRowInTable(table, rowIndex);\n\n    final cell = table.getTableCellNode(rowIndex: rowIndex, columnIndex: 0);\n    if (cell == null) {\n      return;\n    }\n\n    // update selection\n    editorState.selection = Selection.collapsed(\n      Position(\n        path: cell.path.child(0),\n      ),\n    );\n  }\n\n  void _insertRowBelow() {\n    final value = _getTableAndTableCellAndCellPosition();\n    if (value == null) {\n      return;\n    }\n    final (table, node, _) = value;\n    final rowIndex = node.rowIndex;\n    final editorState = context.read<EditorState>();\n    editorState.insertRowInTable(table, rowIndex + 1);\n\n    final cell = table.getTableCellNode(rowIndex: rowIndex + 1, columnIndex: 0);\n    if (cell == null) {\n      return;\n    }\n\n    // update selection\n    editorState.selection = Selection.collapsed(\n      Position(\n        path: cell.path.child(0),\n      ),\n    );\n  }\n\n  void _deleteRow() {\n    final value = _getTableAndTableCellAndCellPosition();\n    if (value == null) {\n      return;\n    }\n    final (table, node, _) = value;\n    final rowIndex = node.rowIndex;\n    final editorState = context.read<EditorState>();\n    editorState.deleteRowInTable(table, rowIndex);\n  }\n\n  void _deleteColumn() {\n    final value = _getTableAndTableCellAndCellPosition();\n    if (value == null) {\n      return;\n    }\n    final (table, node, _) = value;\n    final columnIndex = node.columnIndex;\n    final editorState = context.read<EditorState>();\n    editorState.deleteColumnInTable(table, columnIndex);\n  }\n\n  (Node, Node, TableCellPosition)? _getTableAndTableCellAndCellPosition() {\n    final cell = widget.tableCellNode;\n    final table = cell.parent?.parent;\n    if (table == null || table.type != SimpleTableBlockKeys.type) {\n      return null;\n    }\n    return (table, cell, cell.cellPosition);\n  }\n\n  void _initEnableHeader() {\n    final value = _getTableAndTableCellAndCellPosition();\n    if (value != null) {\n      final (table, _, _) = value;\n      if (widget.type == SimpleTableMoreActionType.column) {\n        isEnableHeader.value = table\n                .attributes[SimpleTableBlockKeys.enableHeaderColumn] as bool? ??\n            false;\n      } else if (widget.type == SimpleTableMoreActionType.row) {\n        isEnableHeader.value =\n            table.attributes[SimpleTableBlockKeys.enableHeaderRow] as bool? ??\n                false;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_reorder_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_feedback.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nclass SimpleTableDraggableReorderButton extends StatelessWidget {\n  const SimpleTableDraggableReorderButton({\n    super.key,\n    required this.node,\n    required this.index,\n    required this.isShowingMenu,\n    required this.type,\n    required this.editorState,\n    required this.simpleTableContext,\n  });\n\n  final Node node;\n  final int index;\n  final ValueNotifier<bool> isShowingMenu;\n  final SimpleTableMoreActionType type;\n  final EditorState editorState;\n  final SimpleTableContext simpleTableContext;\n\n  @override\n  Widget build(BuildContext context) {\n    return Draggable<int>(\n      data: index,\n      onDragStarted: () => _startDragging(),\n      onDragUpdate: (details) => _onDragUpdate(details),\n      onDragEnd: (_) => _stopDragging(),\n      feedback: SimpleTableFeedback(\n        editorState: editorState,\n        node: node,\n        type: type,\n        index: index,\n      ),\n      child: SimpleTableReorderButton(\n        isShowingMenu: isShowingMenu,\n        type: type,\n      ),\n    );\n  }\n\n  void _startDragging() {\n    switch (type) {\n      case SimpleTableMoreActionType.column:\n        simpleTableContext.isReorderingColumn.value = (true, index);\n        break;\n      case SimpleTableMoreActionType.row:\n        simpleTableContext.isReorderingRow.value = (true, index);\n        break;\n    }\n  }\n\n  void _onDragUpdate(DragUpdateDetails details) {\n    simpleTableContext.reorderingOffset.value = details.globalPosition;\n  }\n\n  void _stopDragging() {\n    switch (type) {\n      case SimpleTableMoreActionType.column:\n        _reorderColumn();\n      case SimpleTableMoreActionType.row:\n        _reorderRow();\n    }\n\n    simpleTableContext.reorderingOffset.value = Offset.zero;\n\n    switch (type) {\n      case SimpleTableMoreActionType.column:\n        simpleTableContext.isReorderingColumn.value = (false, -1);\n        break;\n      case SimpleTableMoreActionType.row:\n        simpleTableContext.isReorderingRow.value = (false, -1);\n        break;\n    }\n  }\n\n  void _reorderColumn() {\n    final fromIndex = simpleTableContext.isReorderingColumn.value.$2;\n    final toIndex = simpleTableContext.hoveringTableCell.value?.columnIndex;\n    if (toIndex == null) {\n      return;\n    }\n\n    editorState.reorderColumn(\n      node,\n      fromIndex: fromIndex,\n      toIndex: toIndex,\n    );\n  }\n\n  void _reorderRow() {\n    final fromIndex = simpleTableContext.isReorderingRow.value.$2;\n    final toIndex = simpleTableContext.hoveringTableCell.value?.rowIndex;\n    if (toIndex == null) {\n      return;\n    }\n\n    editorState.reorderRow(\n      node,\n      fromIndex: fromIndex,\n      toIndex: toIndex,\n    );\n  }\n}\n\nclass SimpleTableReorderButton extends StatelessWidget {\n  const SimpleTableReorderButton({\n    super.key,\n    required this.isShowingMenu,\n    required this.type,\n  });\n\n  final ValueNotifier<bool> isShowingMenu;\n  final SimpleTableMoreActionType type;\n\n  @override\n  Widget build(BuildContext context) {\n    return ValueListenableBuilder(\n      valueListenable: isShowingMenu,\n      builder: (context, isShowingMenu, child) {\n        return MouseRegion(\n          cursor: SystemMouseCursors.click,\n          child: Container(\n            decoration: BoxDecoration(\n              color: isShowingMenu\n                  ? context.simpleTableMoreActionHoverColor\n                  : Theme.of(context).colorScheme.surface,\n              borderRadius: BorderRadius.circular(8.0),\n              border: Border.all(\n                color: context.simpleTableMoreActionBorderColor,\n              ),\n            ),\n            height: 16.0,\n            width: 16.0,\n            child: FlowySvg(\n              type.reorderIconSvg,\n              color: isShowingMenu ? Colors.white : null,\n              size: const Size.square(16.0),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_widget.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '_desktop_simple_table_widget.dart';\nimport '_mobile_simple_table_widget.dart';\n\nclass SimpleTableWidget extends StatefulWidget {\n  const SimpleTableWidget({\n    super.key,\n    required this.simpleTableContext,\n    required this.node,\n    this.enableAddColumnButton = true,\n    this.enableAddRowButton = true,\n    this.enableAddColumnAndRowButton = true,\n    this.enableHoverEffect = true,\n    this.isFeedback = false,\n    this.alwaysDistributeColumnWidths = false,\n  });\n\n  /// The node of the table.\n  ///\n  /// Its type must be [SimpleTableBlockKeys.type].\n  final Node node;\n\n  /// The context of the simple table.\n  final SimpleTableContext simpleTableContext;\n\n  /// Whether to show the add column button.\n  ///\n  /// For the feedback widget builder, it should be false.\n  final bool enableAddColumnButton;\n\n  /// Whether to show the add row button.\n  ///\n  /// For the feedback widget builder, it should be false.\n  final bool enableAddRowButton;\n\n  /// Whether to show the add column and row button.\n  ///\n  /// For the feedback widget builder, it should be false.\n  final bool enableAddColumnAndRowButton;\n\n  /// Whether to enable the hover effect.\n  ///\n  /// For the feedback widget builder, it should be false.\n  final bool enableHoverEffect;\n\n  /// Whether the widget is a feedback widget.\n  final bool isFeedback;\n\n  /// Whether the columns should ignore their widths and fill available space\n  final bool alwaysDistributeColumnWidths;\n\n  @override\n  State<SimpleTableWidget> createState() => _SimpleTableWidgetState();\n}\n\nclass _SimpleTableWidgetState extends State<SimpleTableWidget> {\n  @override\n  Widget build(BuildContext context) {\n    return UniversalPlatform.isDesktop\n        ? DesktopSimpleTableWidget(\n            simpleTableContext: widget.simpleTableContext,\n            node: widget.node,\n            enableAddColumnButton: widget.enableAddColumnButton,\n            enableAddRowButton: widget.enableAddRowButton,\n            enableAddColumnAndRowButton: widget.enableAddColumnAndRowButton,\n            enableHoverEffect: widget.enableHoverEffect,\n            isFeedback: widget.isFeedback,\n            alwaysDistributeColumnWidths: widget.alwaysDistributeColumnWidths,\n          )\n        : MobileSimpleTableWidget(\n            simpleTableContext: widget.simpleTableContext,\n            node: widget.node,\n            enableAddColumnButton: widget.enableAddColumnButton,\n            enableAddRowButton: widget.enableAddRowButton,\n            enableAddColumnAndRowButton: widget.enableAddColumnAndRowButton,\n            enableHoverEffect: widget.enableHoverEffect,\n            isFeedback: widget.isFeedback,\n            alwaysDistributeColumnWidths: widget.alwaysDistributeColumnWidths,\n          );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/widgets.dart",
    "content": "export 'simple_table_action_sheet.dart';\nexport 'simple_table_add_column_and_row_button.dart';\nexport 'simple_table_add_column_button.dart';\nexport 'simple_table_add_row_button.dart';\nexport 'simple_table_align_button.dart';\nexport 'simple_table_background_menu.dart';\nexport 'simple_table_basic_button.dart';\nexport 'simple_table_border_builder.dart';\nexport 'simple_table_bottom_sheet.dart';\nexport 'simple_table_column_resize_handle.dart';\nexport 'simple_table_divider.dart';\nexport 'simple_table_more_action_popup.dart';\nexport 'simple_table_reorder_button.dart';\nexport 'simple_table_widget.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_command.dart",
    "content": "import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu.dart';\nimport 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_configuration.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\ntypedef SlashMenuItemsBuilder = List<SelectionMenuItem> Function(\n  EditorState editorState,\n  Node node,\n);\n\n/// Show the slash menu\n///\n/// - support\n///   - desktop\n///\nfinal CharacterShortcutEvent appFlowySlashCommand = CharacterShortcutEvent(\n  key: 'show the slash menu',\n  character: '/',\n  handler: (editorState) async => _showSlashMenu(\n    editorState,\n    itemsBuilder: (_, __) => standardSelectionMenuItems,\n    supportSlashMenuNodeTypes: supportSlashMenuNodeTypes,\n  ),\n);\n\nCharacterShortcutEvent customAppFlowySlashCommand({\n  required SlashMenuItemsBuilder itemsBuilder,\n  bool shouldInsertSlash = true,\n  bool deleteKeywordsByDefault = false,\n  bool singleColumn = true,\n  SelectionMenuStyle style = SelectionMenuStyle.light,\n  required Set<String> supportSlashMenuNodeTypes,\n}) {\n  return CharacterShortcutEvent(\n    key: 'show the slash menu',\n    character: '/',\n    handler: (editorState) => _showSlashMenu(\n      editorState,\n      shouldInsertSlash: shouldInsertSlash,\n      deleteKeywordsByDefault: deleteKeywordsByDefault,\n      singleColumn: singleColumn,\n      style: style,\n      supportSlashMenuNodeTypes: supportSlashMenuNodeTypes,\n      itemsBuilder: itemsBuilder,\n    ),\n  );\n}\n\nSelectionMenuService? _selectionMenuService;\n\nFuture<bool> _showSlashMenu(\n  EditorState editorState, {\n  required SlashMenuItemsBuilder itemsBuilder,\n  bool shouldInsertSlash = true,\n  bool singleColumn = true,\n  bool deleteKeywordsByDefault = false,\n  SelectionMenuStyle style = SelectionMenuStyle.light,\n  required Set<String> supportSlashMenuNodeTypes,\n}) async {\n  final selection = editorState.selection;\n  if (selection == null) {\n    return false;\n  }\n\n  // delete the selection\n  if (!selection.isCollapsed) {\n    await editorState.deleteSelection(selection);\n  }\n\n  final afterSelection = editorState.selection;\n  if (afterSelection == null || !afterSelection.isCollapsed) {\n    assert(false, 'the selection should be collapsed');\n    return true;\n  }\n\n  final node = editorState.getNodeAtPath(selection.start.path);\n\n  // only enable in white-list nodes\n  if (node == null ||\n      !_isSupportSlashMenuNode(node, supportSlashMenuNodeTypes)) {\n    return false;\n  }\n\n  final items = itemsBuilder(editorState, node);\n\n  // insert the slash character\n  if (shouldInsertSlash) {\n    keepEditorFocusNotifier.increase();\n    await editorState.insertTextAtPosition('/', position: selection.start);\n  }\n\n  // show the slash menu\n\n  final context = editorState.getNodeAtPath(selection.start.path)?.context;\n  if (context != null && context.mounted) {\n    final isLight = Theme.of(context).brightness == Brightness.light;\n    _selectionMenuService?.dismiss();\n    _selectionMenuService = UniversalPlatform.isMobile\n        ? MobileSelectionMenu(\n            context: context,\n            editorState: editorState,\n            selectionMenuItems: items,\n            deleteSlashByDefault: shouldInsertSlash,\n            deleteKeywordsByDefault: deleteKeywordsByDefault,\n            singleColumn: singleColumn,\n            style: isLight\n                ? MobileSelectionMenuStyle.light\n                : MobileSelectionMenuStyle.dark,\n            startOffset: editorState.selection?.start.offset ?? 0,\n          )\n        : SelectionMenu(\n            context: context,\n            editorState: editorState,\n            selectionMenuItems: items,\n            deleteSlashByDefault: shouldInsertSlash,\n            deleteKeywordsByDefault: deleteKeywordsByDefault,\n            singleColumn: singleColumn,\n            style: style,\n          );\n\n    // disable the keyboard service\n    editorState.service.keyboardService?.disable();\n\n    await _selectionMenuService?.show();\n    // enable the keyboard service\n    editorState.service.keyboardService?.enable();\n  }\n\n  if (shouldInsertSlash) {\n    WidgetsBinding.instance.addPostFrameCallback(\n      (timeStamp) => keepEditorFocusNotifier.decrease(),\n    );\n  }\n\n  return true;\n}\n\nbool _isSupportSlashMenuNode(\n  Node node,\n  Set<String> supportSlashMenuNodeWhiteList,\n) {\n  // Check if current node type is supported\n  if (!supportSlashMenuNodeWhiteList.contains(node.type)) {\n    return false;\n  }\n\n  // If node has a parent and level > 1, recursively check parent nodes\n  if (node.level > 1 && node.parent != null) {\n    return _isSupportSlashMenuNode(\n      node.parent!,\n      supportSlashMenuNodeWhiteList,\n    );\n  }\n\n  return true;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/ai_writer_item.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'slash_menu_item_builder.dart';\n\nfinal _keywords = [\n  'ai',\n  'openai',\n  'writer',\n  'ai writer',\n  'autogenerator',\n];\n\nSelectionMenuItem aiWriterSlashMenuItem = SelectionMenuItem(\n  getName: LocaleKeys.document_slashMenu_name_aiWriter.tr,\n  keywords: [\n    ..._keywords,\n    LocaleKeys.document_slashMenu_name_aiWriter.tr(),\n  ],\n  handler: (editorState, _, __) async =>\n      _insertAiWriter(editorState, AiWriterCommand.userQuestion),\n  icon: (_, isSelected, style) => SelectableSvgWidget(\n    data: AiWriterCommand.userQuestion.icon,\n    isSelected: isSelected,\n    style: style,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n);\n\nSelectionMenuItem continueWritingSlashMenuItem = SelectionMenuItem(\n  getName: LocaleKeys.document_plugins_aiWriter_continueWriting.tr,\n  keywords: [\n    ..._keywords,\n    LocaleKeys.document_plugins_aiWriter_continueWriting.tr(),\n  ],\n  handler: (editorState, _, __) async =>\n      _insertAiWriter(editorState, AiWriterCommand.continueWriting),\n  icon: (_, isSelected, style) => SelectableSvgWidget(\n    data: AiWriterCommand.continueWriting.icon,\n    isSelected: isSelected,\n    style: style,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n);\n\nFuture<void> _insertAiWriter(\n  EditorState editorState,\n  AiWriterCommand action,\n) async {\n  final selection = editorState.selection;\n  if (selection == null || !selection.isCollapsed) {\n    return;\n  }\n\n  final node = editorState.getNodeAtPath(selection.end.path);\n  if (node == null || node.delta == null) {\n    return;\n  }\n  final newNode = aiWriterNode(\n    selection: selection,\n    command: action,\n  );\n\n  // default insert after\n  final path = node.path.next;\n  final transaction = editorState.transaction\n    ..insertNode(path, newNode)\n    ..afterSelection = null;\n\n  await editorState.apply(\n    transaction,\n    options: const ApplyOptions(\n      recordUndo: false,\n      inMemoryUpdate: true,\n    ),\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/bulleted_list_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'slash_menu_item_builder.dart';\n\nfinal _keywords = [\n  'bulleted list',\n  'list',\n  'unordered list',\n  'ul',\n];\n\n/// Bulleted list menu item\nfinal bulletedListSlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_bulletedList.tr(),\n  keywords: _keywords,\n  handler: (editorState, _, __) {\n    insertBulletedListAfterSelection(editorState);\n  },\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (editorState, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_bulleted_list_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/callout_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nimport 'slash_menu_item_builder.dart';\n\nfinal _keywords = [\n  'callout',\n];\n\n/// Callout menu item\nSelectionMenuItem calloutSlashMenuItem = SelectionMenuItem.node(\n  getName: LocaleKeys.document_plugins_callout.tr,\n  keywords: _keywords,\n  nodeBuilder: (editorState, context) =>\n      calloutNode(defaultColor: Colors.transparent),\n  replace: (_, node) => node.delta?.isEmpty ?? false,\n  updateSelection: (_, path, __, ___) {\n    return Selection.single(path: path, startOffset: 0);\n  },\n  nameBuilder: slashMenuItemNameBuilder,\n  iconBuilder: (editorState, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_callout_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/code_block_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_item_builder.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nfinal _keywords = [\n  'code',\n  'code block',\n  'codeblock',\n];\n\n// code block menu item\nSelectionMenuItem codeBlockSlashMenuItem = SelectionMenuItem.node(\n  getName: () => LocaleKeys.document_slashMenu_name_code.tr(),\n  keywords: _keywords,\n  nodeBuilder: (_, __) => codeBlockNode(),\n  replace: (_, node) => node.delta?.isEmpty ?? false,\n  nameBuilder: slashMenuItemNameBuilder,\n  iconBuilder: (_, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_code_block_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/database_items.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/prelude.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'slash_menu_item_builder.dart';\n\nfinal _gridKeywords = ['grid', 'database'];\nfinal _kanbanKeywords = ['board', 'kanban', 'database'];\nfinal _calendarKeywords = ['calendar', 'database'];\n\nfinal _linkedDocKeywords = [\n  'page',\n  'notes',\n  'referenced page',\n  'referenced document',\n  'referenced database',\n  'link to database',\n  'link to document',\n  'link to page',\n  'link to grid',\n  'link to board',\n  'link to calendar',\n];\nfinal _linkedGridKeywords = [\n  'referenced',\n  'grid',\n  'database',\n  'linked',\n];\nfinal _linkedKanbanKeywords = [\n  'referenced',\n  'board',\n  'kanban',\n  'linked',\n];\nfinal _linkedCalendarKeywords = [\n  'referenced',\n  'calendar',\n  'database',\n  'linked',\n];\n\n/// Grid menu item\nSelectionMenuItem gridSlashMenuItem(DocumentBloc documentBloc) {\n  return SelectionMenuItem(\n    getName: () => LocaleKeys.document_slashMenu_name_grid.tr(),\n    keywords: _gridKeywords,\n    handler: (editorState, menuService, context) async {\n      // create the view inside current page\n      final parentViewId = documentBloc.documentId;\n      final value = await ViewBackendService.createView(\n        parentViewId: parentViewId,\n        name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),\n        layoutType: ViewLayoutPB.Grid,\n      );\n      value.map((r) => editorState.insertInlinePage(parentViewId, r));\n    },\n    nameBuilder: slashMenuItemNameBuilder,\n    icon: (editorState, onSelected, style) => SelectableSvgWidget(\n      data: FlowySvgs.slash_menu_icon_grid_s,\n      isSelected: onSelected,\n      style: style,\n    ),\n  );\n}\n\nSelectionMenuItem kanbanSlashMenuItem(DocumentBloc documentBloc) {\n  return SelectionMenuItem(\n    getName: () => LocaleKeys.document_slashMenu_name_kanban.tr(),\n    keywords: _kanbanKeywords,\n    handler: (editorState, menuService, context) async {\n      // create the view inside current page\n      final parentViewId = documentBloc.documentId;\n      final value = await ViewBackendService.createView(\n        parentViewId: parentViewId,\n        name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),\n        layoutType: ViewLayoutPB.Board,\n      );\n      value.map((r) => editorState.insertInlinePage(parentViewId, r));\n    },\n    nameBuilder: slashMenuItemNameBuilder,\n    icon: (editorState, onSelected, style) => SelectableSvgWidget(\n      data: FlowySvgs.slash_menu_icon_kanban_s,\n      isSelected: onSelected,\n      style: style,\n    ),\n  );\n}\n\nSelectionMenuItem calendarSlashMenuItem(DocumentBloc documentBloc) {\n  return SelectionMenuItem(\n    getName: () => LocaleKeys.document_slashMenu_name_calendar.tr(),\n    keywords: _calendarKeywords,\n    handler: (editorState, menuService, context) async {\n      // create the view inside current page\n      final parentViewId = documentBloc.documentId;\n      final value = await ViewBackendService.createView(\n        parentViewId: parentViewId,\n        name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),\n        layoutType: ViewLayoutPB.Calendar,\n      );\n      value.map((r) => editorState.insertInlinePage(parentViewId, r));\n    },\n    nameBuilder: slashMenuItemNameBuilder,\n    icon: (editorState, onSelected, style) => SelectableSvgWidget(\n      data: FlowySvgs.slash_menu_icon_calendar_s,\n      isSelected: onSelected,\n      style: style,\n    ),\n  );\n}\n\n// linked doc menu item\nfinal linkToPageSlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_linkedDoc.tr(),\n  keywords: _linkedDocKeywords,\n  handler: (editorState, menuService, context) => showLinkToPageMenu(\n    editorState,\n    menuService,\n    // enable database and document references\n    insertPage: false,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (editorState, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_doc_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n\n// linked grid & board & calendar menu item\nSelectionMenuItem referencedGridSlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_linkedGrid.tr(),\n  keywords: _linkedGridKeywords,\n  handler: (editorState, menuService, context) => showLinkToPageMenu(\n    editorState,\n    menuService,\n    pageType: ViewLayoutPB.Grid,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (editorState, onSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_grid_s,\n    isSelected: onSelected,\n    style: style,\n  ),\n);\n\nSelectionMenuItem referencedKanbanSlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_linkedKanban.tr(),\n  keywords: _linkedKanbanKeywords,\n  handler: (editorState, menuService, context) => showLinkToPageMenu(\n    editorState,\n    menuService,\n    pageType: ViewLayoutPB.Board,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (editorState, onSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_kanban_s,\n    isSelected: onSelected,\n    style: style,\n  ),\n);\n\nSelectionMenuItem referencedCalendarSlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_linkedCalendar.tr(),\n  keywords: _linkedCalendarKeywords,\n  handler: (editorState, menuService, context) => showLinkToPageMenu(\n    editorState,\n    menuService,\n    pageType: ViewLayoutPB.Calendar,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (editorState, onSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_calendar_s,\n    isSelected: onSelected,\n    style: style,\n  ),\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/date_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_item_builder.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nfinal _keywords = [\n  'insert date',\n  'date',\n  'time',\n  'reminder',\n  'schedule',\n];\n\n// date or reminder menu item\nSelectionMenuItem dateOrReminderSlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),\n  keywords: _keywords,\n  handler: (editorState, _, __) async => editorState.insertDateReference(),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (_, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_date_or_reminder_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n\nextension on EditorState {\n  Future<void> insertDateReference() async {\n    final selection = this.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n\n    final node = getNodeAtPath(selection.end.path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      return;\n    }\n\n    final transaction = this.transaction\n      ..replaceText(\n        node,\n        selection.start.offset,\n        0,\n        MentionBlockKeys.mentionChar,\n        attributes: MentionBlockKeys.buildMentionDateAttributes(\n          date: DateTime.now().toIso8601String(),\n          reminderId: null,\n          reminderOption: null,\n          includeTime: false,\n        ),\n      );\n\n    await apply(transaction);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/divider_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'slash_menu_item_builder.dart';\n\nfinal _keywords = [\n  'divider',\n  'separator',\n  'line',\n  'break',\n  'horizontal line',\n];\n\n/// Divider menu item\nfinal dividerSlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_divider.tr(),\n  keywords: _keywords,\n  handler: (editorState, _, __) async => editorState.insertDividerBlock(),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (editorState, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_divider_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n\nextension on EditorState {\n  Future<void> insertDividerBlock() async {\n    final selection = this.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n    final path = selection.end.path;\n    final node = getNodeAtPath(path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      return;\n    }\n    final insertedPath = delta.isEmpty ? path : path.next;\n    final transaction = this.transaction\n      ..insertNode(insertedPath, dividerNode())\n      ..insertNode(insertedPath, paragraphNode())\n      ..afterSelection = Selection.collapsed(Position(path: insertedPath.next));\n    await apply(transaction);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/emoji_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/plugins/emoji/emoji_actions_command.dart';\nimport 'package:appflowy/plugins/emoji/emoji_menu.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'slash_menu_item_builder.dart';\n\nfinal _keywords = [\n  'emoji',\n  'reaction',\n  'emoticon',\n];\n\n// emoji menu item\nSelectionMenuItem emojiSlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_emoji.tr(),\n  keywords: _keywords,\n  handler: (editorState, menuService, context) => editorState.showEmojiPicker(\n    context,\n    menuService: menuService,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (_, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_emoji_picker_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n\nextension on EditorState {\n  Future<void> showEmojiPicker(\n    BuildContext context, {\n    required SelectionMenuService menuService,\n  }) async {\n    final container = Overlay.of(context);\n    menuService.dismiss();\n    if (UniversalPlatform.isMobile || selection == null) {\n      return;\n    }\n\n    final node = getNodeAtPath(selection!.end.path);\n    final delta = node?.delta;\n    if (node == null || delta == null || node.type == CodeBlockKeys.type) {\n      return;\n    }\n    emojiMenuService = EmojiMenu(editorState: this, overlay: container);\n    emojiMenuService?.show('');\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/file_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nimport 'slash_menu_items.dart';\n\nfinal _keywords = [\n  'file upload',\n  'pdf',\n  'zip',\n  'archive',\n  'upload',\n  'attachment',\n];\n\n// file menu item\nSelectionMenuItem fileSlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_file.tr(),\n  keywords: _keywords,\n  handler: (editorState, _, __) async => editorState.insertFileBlock(),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (_, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_file_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n\nextension on EditorState {\n  Future<void> insertFileBlock() async {\n    final fileGlobalKey = GlobalKey<FileBlockComponentState>();\n    await insertEmptyFileBlock(fileGlobalKey);\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      fileGlobalKey.currentState?.controller.show();\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/heading_items.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'slash_menu_item_builder.dart';\n\nfinal _h1Keywords = [\n  'heading 1',\n  'h1',\n  'heading1',\n];\nfinal _h2Keywords = [\n  'heading 2',\n  'h2',\n  'heading2',\n];\nfinal _h3Keywords = [\n  'heading 3',\n  'h3',\n  'heading3',\n];\n\nfinal _toggleH1Keywords = [\n  'toggle heading 1',\n  'toggle h1',\n  'toggle heading1',\n  'toggleheading1',\n  'toggleh1',\n];\nfinal _toggleH2Keywords = [\n  'toggle heading 2',\n  'toggle h2',\n  'toggle heading2',\n  'toggleheading2',\n  'toggleh2',\n];\nfinal _toggleH3Keywords = [\n  'toggle heading 3',\n  'toggle h3',\n  'toggle heading3',\n  'toggleheading3',\n  'toggleh3',\n];\n\n// heading 1 - 3 menu items\nfinal heading1SlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_heading1.tr(),\n  keywords: _h1Keywords,\n  handler: (editorState, _, __) async => insertHeadingAfterSelection(\n    editorState,\n    1,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (editorState, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_h1_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n\nfinal heading2SlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_heading2.tr(),\n  keywords: _h2Keywords,\n  handler: (editorState, _, __) async => insertHeadingAfterSelection(\n    editorState,\n    2,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (editorState, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_h2_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n\nfinal heading3SlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_heading3.tr(),\n  keywords: _h3Keywords,\n  handler: (editorState, _, __) async => insertHeadingAfterSelection(\n    editorState,\n    3,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (editorState, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_h3_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n\n// toggle heading 1 menu item\n// heading 1 - 3 menu items\nfinal toggleHeading1SlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_toggleHeading1.tr(),\n  keywords: _toggleH1Keywords,\n  handler: (editorState, _, __) async => insertNodeAfterSelection(\n    editorState,\n    toggleHeadingNode(),\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (editorState, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.toggle_heading1_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n\nfinal toggleHeading2SlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_toggleHeading2.tr(),\n  keywords: _toggleH2Keywords,\n  handler: (editorState, _, __) async => insertNodeAfterSelection(\n    editorState,\n    toggleHeadingNode(level: 2),\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (editorState, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.toggle_heading2_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n\nfinal toggleHeading3SlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_toggleHeading3.tr(),\n  keywords: _toggleH3Keywords,\n  handler: (editorState, _, __) async => insertNodeAfterSelection(\n    editorState,\n    toggleHeadingNode(level: 3),\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (editorState, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.toggle_heading3_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/image_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nimport 'slash_menu_item_builder.dart';\n\nfinal _keywords = [\n  'image',\n  'photo',\n  'picture',\n  'img',\n];\n\n/// Image menu item\nfinal imageSlashMenuItem = buildImageSlashMenuItem();\n\nSelectionMenuItem buildImageSlashMenuItem({FlowySvgData? svg}) =>\n    SelectionMenuItem(\n      getName: () => LocaleKeys.document_slashMenu_name_image.tr(),\n      keywords: _keywords,\n      handler: (editorState, _, __) async => editorState.insertImageBlock(),\n      nameBuilder: slashMenuItemNameBuilder,\n      icon: (editorState, isSelected, style) => SelectableSvgWidget(\n        data: svg ?? FlowySvgs.slash_menu_icon_image_s,\n        isSelected: isSelected,\n        style: style,\n      ),\n    );\n\nextension on EditorState {\n  Future<void> insertImageBlock() async {\n    // use the key to retrieve the state of the image block to show the popover automatically\n    final imagePlaceholderKey = GlobalKey<ImagePlaceholderState>();\n    await insertEmptyImageBlock(imagePlaceholderKey);\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      imagePlaceholderKey.currentState?.controller.show();\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/math_equation_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nimport 'slash_menu_item_builder.dart';\n\nfinal _keywords = [\n  'tex',\n  'latex',\n  'katex',\n  'math equation',\n  'formula',\n];\n\n// math equation\nSelectionMenuItem mathEquationSlashMenuItem = SelectionMenuItem.node(\n  getName: () => LocaleKeys.document_slashMenu_name_mathEquation.tr(),\n  keywords: _keywords,\n  nodeBuilder: (editorState, _) => mathEquationNode(),\n  replace: (_, node) => node.delta?.isEmpty ?? false,\n  updateSelection: (editorState, path, __, ___) {\n    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n      final mathEquationState =\n          editorState.getNodeAtPath(path)?.key.currentState;\n      if (mathEquationState != null &&\n          mathEquationState is MathEquationBlockComponentWidgetState) {\n        mathEquationState.showEditingDialog();\n      }\n    });\n    return null;\n  },\n  nameBuilder: slashMenuItemNameBuilder,\n  iconBuilder: (editorState, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_math_equation_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'slash_menu_items.dart';\n\nfinal List<SelectionMenuItem> mobileItems = [\n  textStyleMobileSlashMenuItem,\n  listMobileSlashMenuItem,\n  toggleListMobileSlashMenuItem,\n  fileAndMediaMobileSlashMenuItem,\n  mobileTableSlashMenuItem,\n  visualsMobileSlashMenuItem,\n  dateOrReminderSlashMenuItem,\n  buildSubpageSlashMenuItem(svg: FlowySvgs.type_page_m),\n  advancedMobileSlashMenuItem,\n];\n\nfinal List<SelectionMenuItem> mobileItemsInTale = [\n  textStyleMobileSlashMenuItem,\n  listMobileSlashMenuItem,\n  toggleListMobileSlashMenuItem,\n  fileAndMediaMobileSlashMenuItem,\n  visualsMobileSlashMenuItem,\n  dateOrReminderSlashMenuItem,\n  buildSubpageSlashMenuItem(svg: FlowySvgs.type_page_m),\n  advancedMobileSlashMenuItem,\n];\n\nSelectionMenuItemHandler _handler = (_, __, ___) {};\n\nMobileSelectionMenuItem textStyleMobileSlashMenuItem = MobileSelectionMenuItem(\n  getName: LocaleKeys.document_slashMenu_name_textStyle.tr,\n  handler: _handler,\n  icon: (_, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_text_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  children: [\n    paragraphSlashMenuItem,\n    heading1SlashMenuItem,\n    heading2SlashMenuItem,\n    heading3SlashMenuItem,\n  ],\n);\n\nMobileSelectionMenuItem listMobileSlashMenuItem = MobileSelectionMenuItem(\n  getName: LocaleKeys.document_slashMenu_name_list.tr,\n  handler: _handler,\n  icon: (_, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_bulleted_list_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  children: [\n    todoListSlashMenuItem,\n    bulletedListSlashMenuItem,\n    numberedListSlashMenuItem,\n  ],\n);\n\nMobileSelectionMenuItem toggleListMobileSlashMenuItem = MobileSelectionMenuItem(\n  getName: LocaleKeys.document_slashMenu_name_toggle.tr,\n  handler: _handler,\n  icon: (_, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_toggle_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  children: [\n    toggleListSlashMenuItem,\n    toggleHeading1SlashMenuItem,\n    toggleHeading2SlashMenuItem,\n    toggleHeading3SlashMenuItem,\n  ],\n);\n\nMobileSelectionMenuItem fileAndMediaMobileSlashMenuItem =\n    MobileSelectionMenuItem(\n  getName: LocaleKeys.document_slashMenu_name_fileAndMedia.tr,\n  handler: _handler,\n  icon: (_, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_file_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  children: [\n    buildImageSlashMenuItem(svg: FlowySvgs.slash_menu_image_m),\n    photoGallerySlashMenuItem,\n    fileSlashMenuItem,\n  ],\n);\n\nMobileSelectionMenuItem visualsMobileSlashMenuItem = MobileSelectionMenuItem(\n  getName: LocaleKeys.document_slashMenu_name_visuals.tr,\n  handler: _handler,\n  icon: (_, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_visuals_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  children: [\n    calloutSlashMenuItem,\n    dividerSlashMenuItem,\n    quoteSlashMenuItem,\n  ],\n);\n\nMobileSelectionMenuItem advancedMobileSlashMenuItem = MobileSelectionMenuItem(\n  getName: LocaleKeys.document_slashMenu_name_advanced.tr,\n  handler: _handler,\n  icon: (_, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.drag_element_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  children: [\n    codeBlockSlashMenuItem,\n    mathEquationSlashMenuItem,\n  ],\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/numbered_list_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'slash_menu_item_builder.dart';\n\nfinal _keywords = [\n  'numbered list',\n  'list',\n  'ordered list',\n  'ol',\n];\n\n/// Numbered list menu item\nfinal numberedListSlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_numberedList.tr(),\n  keywords: _keywords,\n  handler: (editorState, _, __) async => insertNumberedListAfterSelection(\n    editorState,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (editorState, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_numbered_list_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/outline_item.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nimport 'slash_menu_item_builder.dart';\n\nfinal _keywords = [\n  'outline',\n  'table of contents',\n  'toc',\n  'tableofcontents',\n];\n\n/// Outline menu item\nSelectionMenuItem outlineSlashMenuItem = SelectionMenuItem(\n  getName: LocaleKeys.document_selectionMenu_outline.tr,\n  keywords: _keywords,\n  handler: (editorState, _, __) async => editorState.insertOutline(),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (editorState, onSelected, style) {\n    return Icon(\n      Icons.list_alt,\n      color: onSelected\n          ? style.selectionMenuItemSelectedIconColor\n          : style.selectionMenuItemIconColor,\n      size: 16.0,\n    );\n  },\n);\n\nextension on EditorState {\n  Future<void> insertOutline() async {\n    final selection = this.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n\n    final node = getNodeAtPath(selection.end.path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      return;\n    }\n\n    final transaction = this.transaction;\n    final bReplace = node.delta?.isEmpty ?? false;\n\n    //default insert after\n    var path = node.path.next;\n    if (bReplace) {\n      path = node.path;\n    }\n\n    final nextNode = getNodeAtPath(path.next);\n\n    transaction\n      ..insertNodes(\n        path,\n        [\n          outlineBlockNode(),\n          if (nextNode == null || nextNode.delta == null) paragraphNode(),\n        ],\n      )\n      ..afterSelection = Selection.collapsed(\n        Position(path: path.next),\n      );\n\n    if (bReplace) {\n      transaction.deleteNode(node);\n    }\n\n    await apply(transaction);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/paragraph_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'slash_menu_item_builder.dart';\n\nfinal _keywords = [\n  'text',\n  'paragraph',\n];\n\n// paragraph menu item\nfinal paragraphSlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_text.tr(),\n  keywords: _keywords,\n  handler: (editorState, _, __) async => insertNodeAfterSelection(\n    editorState,\n    paragraphNode(),\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (editorState, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_text_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/photo_gallery_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nimport 'slash_menu_items.dart';\n\nfinal _keywords = [\n  LocaleKeys.document_plugins_photoGallery_imageKeyword.tr(),\n  LocaleKeys.document_plugins_photoGallery_imageGalleryKeyword.tr(),\n  LocaleKeys.document_plugins_photoGallery_photoKeyword.tr(),\n  LocaleKeys.document_plugins_photoGallery_photoBrowserKeyword.tr(),\n  LocaleKeys.document_plugins_photoGallery_galleryKeyword.tr(),\n];\n\n// photo gallery menu item\nSelectionMenuItem photoGallerySlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_photoGallery.tr(),\n  keywords: _keywords,\n  handler: (editorState, _, __) async => editorState.insertPhotoGalleryBlock(),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (_, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_photo_gallery_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n\nextension on EditorState {\n  Future<void> insertPhotoGalleryBlock() async {\n    final imagePlaceholderKey = GlobalKey<ImagePlaceholderState>();\n    await insertEmptyMultiImageBlock(imagePlaceholderKey);\n\n    WidgetsBinding.instance.addPostFrameCallback(\n      (_) => imagePlaceholderKey.currentState?.controller.show(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/quote_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'slash_menu_item_builder.dart';\n\nfinal _keywords = [\n  'quote',\n  'refer',\n  'blockquote',\n  'citation',\n];\n\n/// Quote menu item\nfinal quoteSlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_quote.tr(),\n  keywords: _keywords,\n  handler: (editorState, _, __) async => insertQuoteAfterSelection(editorState),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (editorState, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_quote_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_item_builder.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nfinal _baseKeywords = [\n  'columns',\n  'column block',\n];\n\nfinal _twoColumnsKeywords = [\n  ..._baseKeywords,\n  'two columns',\n  '2 columns',\n];\n\nfinal _threeColumnsKeywords = [\n  ..._baseKeywords,\n  'three columns',\n  '3 columns',\n];\n\nfinal _fourColumnsKeywords = [\n  ..._baseKeywords,\n  'four columns',\n  '4 columns',\n];\n\n// 2 columns menu item\nSelectionMenuItem twoColumnsSlashMenuItem = SelectionMenuItem.node(\n  getName: () => LocaleKeys.document_slashMenu_name_twoColumns.tr(),\n  keywords: _twoColumnsKeywords,\n  nodeBuilder: (editorState, __) => _buildColumnsNode(editorState, 2),\n  replace: (_, node) => node.delta?.isEmpty ?? false,\n  nameBuilder: slashMenuItemNameBuilder,\n  iconBuilder: (_, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_two_columns_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n  updateSelection: (_, path, __, ___) {\n    return Selection.single(\n      path: path.child(0).child(0),\n      startOffset: 0,\n    );\n  },\n);\n\n// 3 columns menu item\nSelectionMenuItem threeColumnsSlashMenuItem = SelectionMenuItem.node(\n  getName: () => LocaleKeys.document_slashMenu_name_threeColumns.tr(),\n  keywords: _threeColumnsKeywords,\n  nodeBuilder: (editorState, __) => _buildColumnsNode(editorState, 3),\n  replace: (_, node) => node.delta?.isEmpty ?? false,\n  nameBuilder: slashMenuItemNameBuilder,\n  iconBuilder: (_, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_three_columns_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n  updateSelection: (_, path, __, ___) {\n    return Selection.single(\n      path: path.child(0).child(0),\n      startOffset: 0,\n    );\n  },\n);\n\n// 4 columns menu item\nSelectionMenuItem fourColumnsSlashMenuItem = SelectionMenuItem.node(\n  getName: () => LocaleKeys.document_slashMenu_name_fourColumns.tr(),\n  keywords: _fourColumnsKeywords,\n  nodeBuilder: (editorState, __) => _buildColumnsNode(editorState, 4),\n  replace: (_, node) => node.delta?.isEmpty ?? false,\n  nameBuilder: slashMenuItemNameBuilder,\n  iconBuilder: (_, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_four_columns_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n  updateSelection: (_, path, __, ___) {\n    return Selection.single(\n      path: path.child(0).child(0),\n      startOffset: 0,\n    );\n  },\n);\n\nNode _buildColumnsNode(EditorState editorState, int columnCount) {\n  return simpleColumnsNode(\n    columnCount: columnCount,\n    ratio: 1.0 / columnCount,\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_table_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'slash_menu_item_builder.dart';\n\nfinal _keywords = [\n  'table',\n  'rows',\n  'columns',\n  'data',\n];\n\n// table menu item\nSelectionMenuItem tableSlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_table.tr(),\n  keywords: _keywords,\n  handler: (editorState, _, __) async => editorState.insertSimpleTable(),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (_, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_simple_table_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n\nSelectionMenuItem mobileTableSlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.document_slashMenu_name_simpleTable.tr(),\n  keywords: _keywords,\n  handler: (editorState, _, __) async => editorState.insertSimpleTable(),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (_, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_simple_table_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n\nextension on EditorState {\n  Future<void> insertSimpleTable() async {\n    final selection = this.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n\n    final currentNode = getNodeAtPath(selection.end.path);\n    if (currentNode == null) {\n      return;\n    }\n\n    // create a simple table with 2 columns and 2 rows\n    final tableNode = createSimpleTableBlockNode(\n      columnCount: 2,\n      rowCount: 2,\n    );\n\n    final transaction = this.transaction;\n    final delta = currentNode.delta;\n    if (delta != null && delta.isEmpty) {\n      final path = selection.end.path;\n      transaction\n        ..insertNode(path, tableNode)\n        ..deleteNode(currentNode);\n      transaction.afterSelection = Selection.collapsed(\n        Position(\n          // table -> row -> cell -> paragraph\n          path: path + [0, 0, 0],\n        ),\n      );\n    } else {\n      final path = selection.end.path.next;\n      transaction.insertNode(path, tableNode);\n      transaction.afterSelection = Selection.collapsed(\n        Position(\n          // table -> row -> cell -> paragraph\n          path: path + [0, 0, 0],\n        ),\n      );\n    }\n\n    await apply(transaction);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_item_builder.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\n/// Builder function for the slash menu item.\nWidget slashMenuItemNameBuilder(\n  String name,\n  SelectionMenuStyle style,\n  bool isSelected,\n) {\n  return SlashMenuItemNameBuilder(\n    name: name,\n    style: style,\n    isSelected: isSelected,\n  );\n}\n\nWidget slashMenuItemIconBuilder(\n  FlowySvgData data,\n  bool isSelected,\n  SelectionMenuStyle style,\n) {\n  return SelectableSvgWidget(\n    data: data,\n    isSelected: isSelected,\n    style: style,\n  );\n}\n\n/// Build the name of the slash menu item.\nclass SlashMenuItemNameBuilder extends StatelessWidget {\n  const SlashMenuItemNameBuilder({\n    super.key,\n    required this.name,\n    required this.style,\n    required this.isSelected,\n  });\n\n  /// The name of the slash menu item.\n  final String name;\n\n  /// The style of the slash menu item.\n  final SelectionMenuStyle style;\n\n  /// Whether the slash menu item is selected.\n  final bool isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    final isMobile = UniversalPlatform.isMobile;\n    return FlowyText.regular(\n      name,\n      fontSize: isMobile ? 16.0 : 12.0,\n      figmaLineHeight: 15.0,\n      color: isSelected\n          ? style.selectionMenuItemSelectedTextColor\n          : style.selectionMenuItemTextColor,\n    );\n  }\n}\n\n/// Build the icon of the slash menu item.\nclass SlashMenuIconBuilder extends StatelessWidget {\n  const SlashMenuIconBuilder({\n    super.key,\n    required this.data,\n    required this.isSelected,\n    required this.style,\n  });\n\n  /// The data of the icon.\n  final FlowySvgData data;\n\n  /// Whether the slash menu item is selected.\n  final bool isSelected;\n\n  /// The style of the slash menu item.\n  final SelectionMenuStyle style;\n\n  @override\n  Widget build(BuildContext context) {\n    final isMobile = UniversalPlatform.isMobile;\n    return SelectableSvgWidget(\n      data: data,\n      isSelected: isSelected,\n      size: isMobile ? Size.square(20) : null,\n      style: style,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_items.dart",
    "content": "export 'ai_writer_item.dart';\nexport 'bulleted_list_item.dart';\nexport 'callout_item.dart';\nexport 'code_block_item.dart';\nexport 'database_items.dart';\nexport 'date_item.dart';\nexport 'divider_item.dart';\nexport 'emoji_item.dart';\nexport 'file_item.dart';\nexport 'heading_items.dart';\nexport 'image_item.dart';\nexport 'math_equation_item.dart';\nexport 'numbered_list_item.dart';\nexport 'outline_item.dart';\nexport 'paragraph_item.dart';\nexport 'photo_gallery_item.dart';\nexport 'quote_item.dart';\nexport 'simple_columns_item.dart';\nexport 'simple_table_item.dart';\nexport 'slash_menu_item_builder.dart';\nexport 'sub_page_item.dart';\nexport 'todo_list_item.dart';\nexport 'toggle_list_item.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/sub_page_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'slash_menu_items.dart';\n\nfinal _keywords = [\n  LocaleKeys.document_slashMenu_subPage_keyword1.tr(),\n  LocaleKeys.document_slashMenu_subPage_keyword2.tr(),\n  LocaleKeys.document_slashMenu_subPage_keyword3.tr(),\n  LocaleKeys.document_slashMenu_subPage_keyword4.tr(),\n  LocaleKeys.document_slashMenu_subPage_keyword5.tr(),\n  LocaleKeys.document_slashMenu_subPage_keyword6.tr(),\n  LocaleKeys.document_slashMenu_subPage_keyword7.tr(),\n  LocaleKeys.document_slashMenu_subPage_keyword8.tr(),\n];\n\n// Sub-page menu item\nSelectionMenuItem subPageSlashMenuItem = buildSubpageSlashMenuItem();\n\nSelectionMenuItem buildSubpageSlashMenuItem({FlowySvgData? svg}) =>\n    SelectionMenuItem.node(\n      getName: () => LocaleKeys.document_slashMenu_subPage_name.tr(),\n      keywords: _keywords,\n      updateSelection: (editorState, path, __, ___) {\n        final context = editorState.document.root.context;\n        if (context != null) {\n          final isInDatabase =\n              context.read<SharedEditorContext>().isInDatabaseRowPage;\n          if (isInDatabase) {\n            Navigator.of(context).pop();\n          }\n        }\n        return Selection.collapsed(Position(path: path));\n      },\n      replace: (_, node) => node.delta?.isEmpty ?? false,\n      nodeBuilder: (_, __) => subPageNode(),\n      nameBuilder: slashMenuItemNameBuilder,\n      iconBuilder: (_, isSelected, style) => SelectableSvgWidget(\n        data: svg ?? FlowySvgs.insert_document_s,\n        isSelected: isSelected,\n        style: style,\n      ),\n    );\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/todo_list_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'slash_menu_item_builder.dart';\n\nfinal _keywords = [\n  'checkbox',\n  'todo',\n  'list',\n  'to-do',\n  'task',\n];\n\nfinal todoListSlashMenuItem = SelectionMenuItem(\n  getName: () => LocaleKeys.editor_checkbox.tr(),\n  keywords: _keywords,\n  handler: (editorState, _, __) async => insertCheckboxAfterSelection(\n    editorState,\n  ),\n  nameBuilder: slashMenuItemNameBuilder,\n  icon: (editorState, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_checkbox_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/toggle_list_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'slash_menu_item_builder.dart';\n\nfinal _keywords = [\n  'collapsed list',\n  'toggle list',\n  'list',\n  'dropdown',\n];\n\n// toggle menu item\nSelectionMenuItem toggleListSlashMenuItem = SelectionMenuItem.node(\n  getName: () => LocaleKeys.document_slashMenu_name_toggleList.tr(),\n  keywords: _keywords,\n  nodeBuilder: (editorState, _) => toggleListBlockNode(),\n  replace: (_, node) => node.delta?.isEmpty ?? false,\n  nameBuilder: slashMenuItemNameBuilder,\n  iconBuilder: (editorState, isSelected, style) => SelectableSvgWidget(\n    data: FlowySvgs.slash_menu_icon_toggle_s,\n    isSelected: isSelected,\n    style: style,\n  ),\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart",
    "content": "import 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'slash_menu_items/mobile_items.dart';\nimport 'slash_menu_items/slash_menu_items.dart';\n\n/// Build slash menu items\n///\nList<SelectionMenuItem> slashMenuItemsBuilder({\n  bool isLocalMode = false,\n  DocumentBloc? documentBloc,\n  EditorState? editorState,\n  Node? node,\n  ViewPB? view,\n}) {\n  final isInTable = node != null && node.parentTableCellNode != null;\n  final isMobile = UniversalPlatform.isMobile;\n  bool isEmpty = false;\n  if (editorState == null || editorState.isEmptyForContinueWriting()) {\n    if (view == null || view.name.isEmpty) {\n      isEmpty = true;\n    }\n  }\n  if (isMobile) {\n    if (isInTable) {\n      return mobileItemsInTale;\n    } else {\n      return mobileItems;\n    }\n  } else {\n    if (isInTable) {\n      return _simpleTableSlashMenuItems();\n    } else {\n      return _defaultSlashMenuItems(\n        isLocalMode: isLocalMode,\n        documentBloc: documentBloc,\n        isEmpty: isEmpty,\n      );\n    }\n  }\n}\n\n/// The default slash menu items are used in the text-based block.\n///\n/// Except for the simple table block, the slash menu items in the table block are\n/// built by the `tableSlashMenuItem` function.\n/// If in local mode, disable the ai writer feature\n///\n/// The linked database relies on the documentBloc, so it's required to pass in\n/// the documentBloc when building the slash menu items. If the documentBloc is\n/// not provided, the linked database items will be disabled.\n///\n///\nList<SelectionMenuItem> _defaultSlashMenuItems({\n  bool isLocalMode = false,\n  DocumentBloc? documentBloc,\n  bool isEmpty = false,\n}) {\n  return [\n    // ai\n    if (!isEmpty) continueWritingSlashMenuItem,\n    aiWriterSlashMenuItem,\n\n    paragraphSlashMenuItem,\n\n    // heading 1-3\n    heading1SlashMenuItem,\n    heading2SlashMenuItem,\n    heading3SlashMenuItem,\n\n    // image\n    imageSlashMenuItem,\n\n    // list\n    bulletedListSlashMenuItem,\n    numberedListSlashMenuItem,\n    todoListSlashMenuItem,\n\n    // divider\n    dividerSlashMenuItem,\n\n    // quote\n    quoteSlashMenuItem,\n\n    // simple table\n    tableSlashMenuItem,\n\n    // link to page\n    linkToPageSlashMenuItem,\n\n    // columns\n    // 2-4 columns\n    twoColumnsSlashMenuItem,\n    threeColumnsSlashMenuItem,\n    fourColumnsSlashMenuItem,\n\n    // grid\n    if (documentBloc != null) gridSlashMenuItem(documentBloc),\n    referencedGridSlashMenuItem,\n\n    // kanban\n    if (documentBloc != null) kanbanSlashMenuItem(documentBloc),\n    referencedKanbanSlashMenuItem,\n\n    // calendar\n    if (documentBloc != null) calendarSlashMenuItem(documentBloc),\n    referencedCalendarSlashMenuItem,\n\n    // callout\n    calloutSlashMenuItem,\n\n    // outline\n    outlineSlashMenuItem,\n\n    // math equation\n    mathEquationSlashMenuItem,\n\n    // code block\n    codeBlockSlashMenuItem,\n\n    // toggle list - toggle headings\n    toggleListSlashMenuItem,\n    toggleHeading1SlashMenuItem,\n    toggleHeading2SlashMenuItem,\n    toggleHeading3SlashMenuItem,\n\n    // emoji\n    emojiSlashMenuItem,\n\n    // date or reminder\n    dateOrReminderSlashMenuItem,\n\n    // photo gallery\n    photoGallerySlashMenuItem,\n\n    // file\n    fileSlashMenuItem,\n\n    // sub page\n    subPageSlashMenuItem,\n  ];\n}\n\n/// The slash menu items in the simple table block.\n///\n/// There're some blocks should be excluded in the slash menu items.\n///\n/// - Database Items\n/// - Image Gallery\nList<SelectionMenuItem> _simpleTableSlashMenuItems() {\n  return [\n    paragraphSlashMenuItem,\n\n    // heading 1-3\n    heading1SlashMenuItem,\n    heading2SlashMenuItem,\n    heading3SlashMenuItem,\n\n    // image\n    imageSlashMenuItem,\n\n    // list\n    bulletedListSlashMenuItem,\n    numberedListSlashMenuItem,\n    todoListSlashMenuItem,\n\n    // divider\n    dividerSlashMenuItem,\n\n    // quote\n    quoteSlashMenuItem,\n\n    // link to page\n    linkToPageSlashMenuItem,\n\n    // callout\n    calloutSlashMenuItem,\n\n    // math equation\n    mathEquationSlashMenuItem,\n\n    // code block\n    codeBlockSlashMenuItem,\n\n    // toggle list - toggle headings\n    toggleListSlashMenuItem,\n    toggleHeading1SlashMenuItem,\n    toggleHeading2SlashMenuItem,\n    toggleHeading3SlashMenuItem,\n\n    // emoji\n    emojiSlashMenuItem,\n\n    // date or reminder\n    dateOrReminderSlashMenuItem,\n\n    // file\n    fileSlashMenuItem,\n\n    // sub page\n    subPageSlashMenuItem,\n  ];\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/block_transaction_handler.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/block_transaction_handler/block_transaction_handler.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart';\nimport 'package:appflowy/plugins/trash/application/trash_service.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/snap_bar.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass SubPageBlockTransactionHandler extends BlockTransactionHandler {\n  SubPageBlockTransactionHandler() : super(blockType: SubPageBlockKeys.type);\n\n  final List<String> _beingCreated = [];\n\n  @override\n  void onRedo(\n    BuildContext context,\n    EditorState editorState,\n    List<Node> before,\n    List<Node> after,\n  ) {\n    _handleUndoRedo(context, editorState, before, after);\n  }\n\n  @override\n  void onUndo(\n    BuildContext context,\n    EditorState editorState,\n    List<Node> before,\n    List<Node> after,\n  ) {\n    _handleUndoRedo(context, editorState, before, after);\n  }\n\n  void _handleUndoRedo(\n    BuildContext context,\n    EditorState editorState,\n    List<Node> before,\n    List<Node> after,\n  ) {\n    final additions = after.where((e) => !before.contains(e)).toList();\n    final removals = before.where((e) => !after.contains(e)).toList();\n\n    // Removals goes to trash\n    for (final node in removals) {\n      _subPageDeleted(context, editorState, node);\n    }\n\n    // Additions are moved to this view\n    for (final node in additions) {\n      _subPageAdded(context, editorState, node);\n    }\n  }\n\n  @override\n  Future<void> onTransaction(\n    BuildContext context,\n    EditorState editorState,\n    List<Node> added,\n    List<Node> removed, {\n    bool isUndoRedo = false,\n    bool isPaste = false,\n    bool isDraggingNode = false,\n    String? parentViewId,\n  }) async {\n    if (isDraggingNode) {\n      return;\n    }\n\n    for (final node in removed) {\n      if (!context.mounted) return;\n      await _subPageDeleted(context, editorState, node);\n    }\n\n    for (final node in added) {\n      if (!context.mounted) return;\n      await _subPageAdded(\n        context,\n        editorState,\n        node,\n        isPaste: isPaste,\n        parentViewId: parentViewId,\n      );\n    }\n  }\n\n  Future<void> _subPageDeleted(\n    BuildContext context,\n    EditorState editorState,\n    Node node,\n  ) async {\n    if (node.type != blockType) {\n      return;\n    }\n\n    final view = node.attributes[SubPageBlockKeys.viewId];\n    if (view == null) {\n      return;\n    }\n\n    // We move the view to Trash\n    final result = await ViewBackendService.deleteView(viewId: view);\n    result.fold(\n      (_) {},\n      (error) {\n        Log.error(error);\n        if (context.mounted) {\n          showSnapBar(\n            context,\n            LocaleKeys.document_plugins_subPage_errors_failedDeletePage.tr(),\n          );\n        }\n      },\n    );\n  }\n\n  Future<void> _subPageAdded(\n    BuildContext context,\n    EditorState editorState,\n    Node node, {\n    bool isPaste = false,\n    String? parentViewId,\n  }) async {\n    if (node.type != blockType || _beingCreated.contains(node.id)) {\n      return;\n    }\n\n    final viewId = node.attributes[SubPageBlockKeys.viewId];\n    if (viewId == null && parentViewId != null) {\n      _beingCreated.add(node.id);\n\n      // This is a new Node, we need to create the view\n      final viewOrResult = await ViewBackendService.createView(\n        layoutType: ViewLayoutPB.Document,\n        parentViewId: parentViewId,\n        name: '',\n      );\n\n      await viewOrResult.fold(\n        (view) async {\n          final transaction = editorState.transaction\n            ..updateNode(node, {SubPageBlockKeys.viewId: view.id});\n          await editorState\n              .apply(\n            transaction,\n            withUpdateSelection: false,\n            options: const ApplyOptions(recordUndo: false),\n          )\n              .then((_) async {\n            editorState.reload();\n\n            // Open the new page\n            if (UniversalPlatform.isDesktop) {\n              getIt<TabsBloc>().openPlugin(view);\n            } else {\n              if (context.mounted) {\n                await context.pushView(view);\n              }\n            }\n          });\n        },\n        (error) async {\n          Log.error(error);\n          showSnapBar(\n            context,\n            LocaleKeys.document_plugins_subPage_errors_failedCreatePage.tr(),\n          );\n\n          // Remove the node because it failed\n          final transaction = editorState.transaction..deleteNode(node);\n          await editorState.apply(\n            transaction,\n            withUpdateSelection: false,\n            options: const ApplyOptions(recordUndo: false),\n          );\n        },\n      );\n\n      _beingCreated.remove(node.id);\n    } else if (isPaste) {\n      final wasCut = node.attributes[SubPageBlockKeys.wasCut];\n      if (wasCut == true && parentViewId != null) {\n        // Just in case, we try to put back from trash before moving\n        await TrashService.putback(viewId);\n\n        final viewOrResult = await ViewBackendService.moveViewV2(\n          viewId: viewId,\n          newParentId: parentViewId,\n          prevViewId: null,\n        );\n\n        viewOrResult.fold(\n          (_) {},\n          (error) {\n            Log.error(error);\n            showSnapBar(\n              context,\n              LocaleKeys.document_plugins_subPage_errors_failedMovePage.tr(),\n            );\n          },\n        );\n      } else {\n        final viewId = node.attributes[SubPageBlockKeys.viewId];\n        if (viewId == null) {\n          return;\n        }\n\n        final viewOrResult = await ViewBackendService.getView(viewId);\n        return viewOrResult.fold(\n          (view) async {\n            final duplicatedViewOrResult = await ViewBackendService.duplicate(\n              view: view,\n              openAfterDuplicate: false,\n              includeChildren: true,\n              syncAfterDuplicate: true,\n              parentViewId: parentViewId,\n            );\n\n            return duplicatedViewOrResult.fold(\n              (view) async {\n                final transaction = editorState.transaction\n                  ..updateNode(node, {\n                    SubPageBlockKeys.viewId: view.id,\n                    SubPageBlockKeys.wasCut: false,\n                    SubPageBlockKeys.wasCopied: false,\n                  });\n                await editorState.apply(\n                  transaction,\n                  withUpdateSelection: false,\n                  options: const ApplyOptions(recordUndo: false),\n                );\n                editorState.reload();\n              },\n              (error) {\n                Log.error(error);\n                if (context.mounted) {\n                  showSnapBar(\n                    context,\n                    LocaleKeys\n                        .document_plugins_subPage_errors_failedDuplicatePage\n                        .tr(),\n                  );\n                }\n              },\n            );\n          },\n          (error) async {\n            Log.error(error);\n\n            final transaction = editorState.transaction..deleteNode(node);\n            await editorState.apply(\n              transaction,\n              withUpdateSelection: false,\n              options: const ApplyOptions(recordUndo: false),\n            );\n            editorState.reload();\n            if (context.mounted) {\n              showSnapBar(\n                context,\n                LocaleKeys\n                    .document_plugins_subPage_errors_failedDuplicateFindView\n                    .tr(),\n              );\n            }\n          },\n        );\n      }\n    } else {\n      // Try to restore from trash, and move to parent view\n      await TrashService.putback(viewId);\n\n      // Check if View needs to be moved\n      if (parentViewId != null) {\n        final view = pageMemorizer[viewId] ??\n            (await ViewBackendService.getView(viewId)).toNullable();\n        if (view == null) {\n          return Log.error('View not found: $viewId');\n        }\n\n        if (view.parentViewId == parentViewId) {\n          return;\n        }\n\n        await ViewBackendService.moveViewV2(\n          viewId: viewId,\n          newParentId: parentViewId,\n          prevViewId: null,\n        );\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart';\nimport 'package:appflowy/plugins/trash/application/trash_listener.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_listener.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nNode subPageNode({String? viewId}) {\n  return Node(\n    type: SubPageBlockKeys.type,\n    attributes: {SubPageBlockKeys.viewId: viewId},\n  );\n}\n\nclass SubPageBlockKeys {\n  const SubPageBlockKeys._();\n\n  static const String type = 'sub_page';\n\n  /// The ID of the View which is being linked to.\n  ///\n  static const String viewId = \"view_id\";\n\n  /// Signifies whether the block was inserted after a Copy operation.\n  ///\n  static const String wasCopied = \"was_copied\";\n\n  /// Signifies whether the block was inserted after a Cut operation.\n  ///\n  static const String wasCut = \"was_cut\";\n}\n\nclass SubPageBlockComponentBuilder extends BlockComponentBuilder {\n  SubPageBlockComponentBuilder({super.configuration});\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    return SubPageBlockComponent(\n      key: node.key,\n      node: node,\n      showActions: showActions(node),\n      configuration: configuration,\n      actionBuilder: (_, state) => actionBuilder(blockComponentContext, state),\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate => (node) => node.children.isEmpty;\n}\n\nclass SubPageBlockComponent extends BlockComponentStatefulWidget {\n  const SubPageBlockComponent({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n  });\n\n  @override\n  State<SubPageBlockComponent> createState() => SubPageBlockComponentState();\n}\n\nclass SubPageBlockComponentState extends State<SubPageBlockComponent>\n    with SelectableMixin, BlockComponentConfigurable {\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  @override\n  Node get node => widget.node;\n\n  RenderBox? get _renderBox => context.findRenderObject() as RenderBox?;\n\n  final subPageKey = GlobalKey();\n\n  ViewListener? viewListener;\n  TrashListener? trashListener;\n  Future<ViewPB?>? viewFuture;\n\n  bool isHovering = false;\n  bool isHandlingPaste = false;\n\n  EditorState get editorState => context.read<EditorState>();\n\n  String? parentId;\n\n  @override\n  void initState() {\n    super.initState();\n    final viewId = node.attributes[SubPageBlockKeys.viewId];\n    if (viewId != null) {\n      viewFuture = fetchView(viewId);\n      viewListener = ViewListener(viewId: viewId)\n        ..start(onViewUpdated: onViewUpdated);\n      trashListener = TrashListener()..start(trashUpdated: didUpdateTrash);\n    }\n  }\n\n  @override\n  void didUpdateWidget(SubPageBlockComponent oldWidget) {\n    final viewId = node.attributes[SubPageBlockKeys.viewId];\n    final oldViewId = viewListener?.viewId ??\n        oldWidget.node.attributes[SubPageBlockKeys.viewId];\n    if (viewId != null && (viewId != oldViewId || viewListener == null)) {\n      viewFuture = fetchView(viewId);\n      viewListener?.stop();\n      viewListener = ViewListener(viewId: viewId)\n        ..start(onViewUpdated: onViewUpdated);\n    }\n    super.didUpdateWidget(oldWidget);\n  }\n\n  void didUpdateTrash(FlowyResult<List<TrashPB>, FlowyError> trashOrFailed) {\n    final trashList = trashOrFailed.toNullable();\n    if (trashList == null) {\n      return;\n    }\n\n    final viewId = node.attributes[SubPageBlockKeys.viewId];\n    if (trashList.any((t) => t.id == viewId)) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        if (mounted) {\n          final transaction = editorState.transaction..deleteNode(node);\n          editorState.apply(\n            transaction,\n            withUpdateSelection: false,\n            options: const ApplyOptions(recordUndo: false),\n          );\n        }\n      });\n    }\n  }\n\n  void onViewUpdated(ViewPB view) {\n    pageMemorizer[view.id] = view;\n    viewFuture = Future<ViewPB>.value(view);\n    editorState.reload();\n\n    if (parentId != view.parentViewId && parentId != null) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        if (mounted) {\n          final transaction = editorState.transaction..deleteNode(node);\n          editorState.apply(\n            transaction,\n            withUpdateSelection: false,\n            options: const ApplyOptions(recordUndo: false),\n          );\n        }\n      });\n    }\n  }\n\n  @override\n  void dispose() {\n    viewListener?.stop();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder<ViewPB?>(\n      initialData: pageMemorizer[node.attributes[SubPageBlockKeys.viewId]],\n      future: viewFuture,\n      builder: (context, snapshot) {\n        if (snapshot.connectionState != ConnectionState.done) {\n          return const SizedBox.shrink();\n        }\n\n        final view = snapshot.data;\n        if (view == null) {\n          WidgetsBinding.instance.addPostFrameCallback((_) {\n            if (mounted) {\n              final transaction = editorState.transaction..deleteNode(node);\n              editorState.apply(\n                transaction,\n                withUpdateSelection: false,\n                options: const ApplyOptions(recordUndo: false),\n              );\n            }\n          });\n\n          return const SizedBox.shrink();\n        }\n\n        final textStyle = textStyleWithTextSpan();\n\n        Widget child = Padding(\n          padding: const EdgeInsets.symmetric(vertical: 2),\n          child: MouseRegion(\n            cursor: SystemMouseCursors.click,\n            onEnter: (_) => setState(() => isHovering = true),\n            onExit: (_) => setState(() => isHovering = false),\n            opaque: false,\n            child: Container(\n              clipBehavior: Clip.antiAlias,\n              decoration: BoxDecoration(\n                borderRadius: BorderRadius.circular(4),\n              ),\n              child: BlockSelectionContainer(\n                node: node,\n                delegate: this,\n                listenable: editorState.selectionNotifier,\n                remoteSelection: editorState.remoteSelections,\n                blockColor: editorState.editorStyle.selectionColor,\n                cursorColor: editorState.editorStyle.cursorColor,\n                selectionColor: editorState.editorStyle.selectionColor,\n                supportTypes: const [\n                  BlockSelectionType.block,\n                  BlockSelectionType.cursor,\n                  BlockSelectionType.selection,\n                ],\n                child: GestureDetector(\n                  // TODO(Mathias): Handle mobile tap\n                  onTap:\n                      isHandlingPaste ? null : () => _openSubPage(view: view),\n                  child: DecoratedBox(\n                    decoration: BoxDecoration(\n                      color: isHovering\n                          ? Theme.of(context).colorScheme.secondary\n                          : null,\n                      borderRadius: BorderRadius.circular(4),\n                    ),\n                    child: SizedBox(\n                      height: 32,\n                      child: Row(\n                        children: [\n                          const HSpace(10),\n                          view.icon.value.isNotEmpty\n                              ? RawEmojiIconWidget(\n                                  emoji: view.icon.toEmojiIconData(),\n                                  emojiSize: textStyle.fontSize ?? 16.0,\n                                )\n                              : view.defaultIcon(),\n                          const HSpace(6),\n                          Flexible(\n                            child: FlowyText(\n                              view.nameOrDefault,\n                              fontSize: textStyle.fontSize,\n                              fontWeight: textStyle.fontWeight,\n                              decoration: TextDecoration.underline,\n                              lineHeight: textStyle.height,\n                              overflow: TextOverflow.ellipsis,\n                            ),\n                          ),\n                          if (isHandlingPaste) ...[\n                            FlowyText(\n                              LocaleKeys\n                                  .document_plugins_subPage_handlingPasteHint\n                                  .tr(),\n                              fontSize: textStyle.fontSize,\n                              fontWeight: textStyle.fontWeight,\n                              lineHeight: textStyle.height,\n                              color: Theme.of(context).hintColor,\n                            ),\n                            const HSpace(10),\n                            const SizedBox(\n                              height: 16,\n                              width: 16,\n                              child: CircularProgressIndicator(\n                                strokeWidth: 1.5,\n                              ),\n                            ),\n                          ],\n                          const HSpace(10),\n                        ],\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ),\n        );\n\n        if (widget.showActions && widget.actionBuilder != null) {\n          child = BlockComponentActionWrapper(\n            node: node,\n            actionBuilder: widget.actionBuilder!,\n            actionTrailingBuilder: widget.actionTrailingBuilder,\n            child: child,\n          );\n        }\n\n        if (UniversalPlatform.isMobile) {\n          child = Padding(\n            padding: padding,\n            child: child,\n          );\n        }\n\n        return child;\n      },\n    );\n  }\n\n  Future<ViewPB?> fetchView(String pageId) async {\n    final view = await ViewBackendService.getView(pageId).then(\n      (res) => res.toNullable(),\n    );\n\n    if (view == null) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        if (mounted) {\n          final transaction = editorState.transaction..deleteNode(node);\n          editorState.apply(\n            transaction,\n            withUpdateSelection: false,\n            options: const ApplyOptions(recordUndo: false),\n          );\n        }\n      });\n    }\n\n    parentId = view?.parentViewId;\n    return view;\n  }\n\n  @override\n  Position start() => Position(path: widget.node.path);\n\n  @override\n  Position end() => Position(path: widget.node.path, offset: 1);\n\n  @override\n  Position getPositionInOffset(Offset start) => end();\n\n  @override\n  bool get shouldCursorBlink => false;\n\n  @override\n  CursorStyle get cursorStyle => CursorStyle.cover;\n\n  @override\n  Rect getBlockRect({\n    bool shiftWithBaseOffset = false,\n  }) {\n    return getRectsInSelection(Selection.invalid()).first;\n  }\n\n  @override\n  Rect? getCursorRectInPosition(\n    Position position, {\n    bool shiftWithBaseOffset = false,\n  }) {\n    final rects = getRectsInSelection(\n      Selection.collapsed(position),\n      shiftWithBaseOffset: shiftWithBaseOffset,\n    );\n    return rects.firstOrNull;\n  }\n\n  @override\n  List<Rect> getRectsInSelection(\n    Selection selection, {\n    bool shiftWithBaseOffset = false,\n  }) {\n    if (_renderBox == null) {\n      return [];\n    }\n    final parentBox = context.findRenderObject();\n    final renderBox = subPageKey.currentContext?.findRenderObject();\n    if (parentBox is RenderBox && renderBox is RenderBox) {\n      return [\n        renderBox.localToGlobal(Offset.zero, ancestor: parentBox) &\n            renderBox.size,\n      ];\n    }\n    return [Offset.zero & _renderBox!.size];\n  }\n\n  @override\n  Selection getSelectionInRange(Offset start, Offset end) =>\n      Selection.single(path: widget.node.path, startOffset: 0, endOffset: 1);\n\n  @override\n  Offset localToGlobal(Offset offset, {bool shiftWithBaseOffset = false}) =>\n      _renderBox!.localToGlobal(offset);\n\n  void _openSubPage({\n    required ViewPB view,\n  }) {\n    if (UniversalPlatform.isDesktop) {\n      final isInDatabase =\n          context.read<SharedEditorContext>().isInDatabaseRowPage;\n      if (isInDatabase) {\n        Navigator.of(context).pop();\n      }\n\n      getIt<TabsBloc>().add(\n        TabsEvent.openPlugin(\n          plugin: view.plugin(),\n          view: view,\n        ),\n      );\n    } else if (UniversalPlatform.isMobile) {\n      context.pushView(view);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_transaction_handler.dart",
    "content": "import 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:flutter/widgets.dart';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/block_transaction_handler.dart';\nimport 'package:appflowy/plugins/trash/application/trash_service.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/snap_bar.dart';\n\nclass SubPageTransactionHandler extends BlockTransactionHandler {\n  SubPageTransactionHandler() : super(type: SubPageBlockKeys.type);\n\n  final List<String> _beingCreated = [];\n\n  @override\n  Future<void> onTransaction(\n    BuildContext context,\n    String viewId,\n    EditorState editorState,\n    List<Node> added,\n    List<Node> removed, {\n    bool isCut = false,\n    bool isUndoRedo = false,\n    bool isPaste = false,\n    bool isDraggingNode = false,\n    bool isTurnInto = false,\n    String? parentViewId,\n  }) async {\n    if (isDraggingNode || isTurnInto) {\n      return;\n    }\n\n    for (final node in removed) {\n      if (!context.mounted) return;\n      await _subPageDeleted(context, node);\n    }\n\n    for (final node in added) {\n      if (!context.mounted) return;\n      await _subPageAdded(\n        context,\n        editorState,\n        node,\n        isCut: isCut,\n        isPaste: isPaste,\n        parentViewId: parentViewId,\n      );\n    }\n  }\n\n  Future<void> _subPageDeleted(\n    BuildContext context,\n    Node node,\n  ) async {\n    if (node.type != type) {\n      return;\n    }\n\n    final view = node.attributes[SubPageBlockKeys.viewId];\n    if (view == null) {\n      return;\n    }\n\n    final result = await ViewBackendService.deleteView(viewId: view);\n    result.fold(\n      (_) {},\n      (error) {\n        Log.error(error);\n        if (context.mounted) {\n          showSnapBar(\n            context,\n            LocaleKeys.document_plugins_subPage_errors_failedDeletePage.tr(),\n          );\n        }\n      },\n    );\n  }\n\n  Future<void> _subPageAdded(\n    BuildContext context,\n    EditorState editorState,\n    Node node, {\n    bool isCut = false,\n    bool isPaste = false,\n    String? parentViewId,\n  }) async {\n    if (node.type != type || _beingCreated.contains(node.id)) {\n      return;\n    }\n\n    final viewId = node.attributes[SubPageBlockKeys.viewId];\n    if (viewId == null && parentViewId != null) {\n      _beingCreated.add(node.id);\n\n      // This is a new Node, we need to create the view\n      final viewOrResult = await ViewBackendService.createView(\n        name: '',\n        layoutType: ViewLayoutPB.Document,\n        parentViewId: parentViewId,\n      );\n\n      await viewOrResult.fold(\n        (view) async {\n          final transaction = editorState.transaction\n            ..updateNode(node, {SubPageBlockKeys.viewId: view.id});\n          await editorState.apply(\n            transaction,\n            withUpdateSelection: false,\n            options: const ApplyOptions(recordUndo: false),\n          );\n          editorState.reload();\n\n          // Open view\n          getIt<TabsBloc>().openPlugin(view);\n        },\n        (error) async {\n          Log.error(error);\n          showSnapBar(\n            context,\n            LocaleKeys.document_plugins_subPage_errors_failedCreatePage.tr(),\n          );\n\n          // Remove the node because it failed\n          final transaction = editorState.transaction..deleteNode(node);\n          await editorState.apply(\n            transaction,\n            withUpdateSelection: false,\n            options: const ApplyOptions(recordUndo: false),\n          );\n        },\n      );\n\n      _beingCreated.remove(node.id);\n    } else if (isPaste) {\n      if (isCut && parentViewId != null) {\n        await TrashService.putback(viewId);\n\n        final viewOrResult = await ViewBackendService.moveViewV2(\n          viewId: viewId,\n          newParentId: parentViewId,\n          prevViewId: null,\n        );\n\n        viewOrResult.fold(\n          (_) {},\n          (error) {\n            Log.error(error);\n            showSnapBar(\n              context,\n              LocaleKeys.document_plugins_subPage_errors_failedMovePage.tr(),\n            );\n          },\n        );\n      } else {\n        final viewId = node.attributes[SubPageBlockKeys.viewId];\n        if (viewId == null) {\n          return;\n        }\n\n        final viewOrResult = await ViewBackendService.getView(viewId);\n        return viewOrResult.fold(\n          (view) async {\n            final duplicatedViewOrResult = await ViewBackendService.duplicate(\n              view: view,\n              openAfterDuplicate: false,\n              includeChildren: true,\n              syncAfterDuplicate: true,\n              parentViewId: parentViewId,\n            );\n\n            return duplicatedViewOrResult.fold(\n              (view) async {\n                final transaction = editorState.transaction\n                  ..updateNode(node, {\n                    SubPageBlockKeys.viewId: view.id,\n                    SubPageBlockKeys.wasCut: false,\n                    SubPageBlockKeys.wasCopied: false,\n                  });\n                await editorState.apply(\n                  transaction,\n                  withUpdateSelection: false,\n                  options: const ApplyOptions(recordUndo: false),\n                );\n                editorState.reload();\n              },\n              (error) {\n                Log.error(error);\n                if (context.mounted) {\n                  showSnapBar(\n                    context,\n                    LocaleKeys\n                        .document_plugins_subPage_errors_failedDuplicatePage\n                        .tr(),\n                  );\n                }\n              },\n            );\n          },\n          (error) async {\n            Log.error(error);\n\n            final transaction = editorState.transaction..deleteNode(node);\n            await editorState.apply(\n              transaction,\n              withUpdateSelection: false,\n              options: const ApplyOptions(recordUndo: false),\n            );\n            editorState.reload();\n            if (context.mounted) {\n              showSnapBar(\n                context,\n                LocaleKeys\n                    .document_plugins_subPage_errors_failedDuplicateFindView\n                    .tr(),\n              );\n            }\n          },\n        );\n      }\n    } else {\n      // Try to restore from trash, and move to parent view\n      await TrashService.putback(viewId);\n\n      // Check if View needs to be moved\n      if (parentViewId != null) {\n        final view = (await ViewBackendService.getView(viewId)).toNullable();\n        if (view == null) {\n          return Log.error('View not found: $viewId');\n        }\n\n        if (view.parentViewId == parentViewId) {\n          return;\n        }\n\n        await ViewBackendService.moveViewV2(\n          viewId: viewId,\n          newParentId: parentViewId,\n          prevViewId: null,\n        );\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_menu.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_option_action.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\n\nconst tableActions = <TableOptionAction>[\n  TableOptionAction.addAfter,\n  TableOptionAction.addBefore,\n  TableOptionAction.delete,\n  TableOptionAction.duplicate,\n  TableOptionAction.clear,\n  TableOptionAction.bgColor,\n];\n\nclass TableMenu extends StatelessWidget {\n  const TableMenu({\n    super.key,\n    required this.node,\n    required this.editorState,\n    required this.position,\n    required this.dir,\n    this.onBuild,\n    this.onClose,\n  });\n\n  final Node node;\n  final EditorState editorState;\n  final int position;\n  final TableDirection dir;\n  final VoidCallback? onBuild;\n  final VoidCallback? onClose;\n\n  @override\n  Widget build(BuildContext context) {\n    final actions = tableActions.map((action) {\n      switch (action) {\n        case TableOptionAction.bgColor:\n          return TableColorOptionAction(\n            node: node,\n            editorState: editorState,\n            position: position,\n            dir: dir,\n          );\n        default:\n          return TableOptionActionWrapper(action);\n      }\n    }).toList();\n\n    return PopoverActionList<PopoverAction>(\n      direction: dir == TableDirection.col\n          ? PopoverDirection.bottomWithCenterAligned\n          : PopoverDirection.rightWithTopAligned,\n      actions: actions,\n      onPopupBuilder: onBuild,\n      onClosed: onClose,\n      onSelected: (action, controller) {\n        if (action is TableOptionActionWrapper) {\n          _onSelectAction(action.inner);\n          controller.close();\n        }\n      },\n      buildChild: (controller) => _buildOptionButton(controller, context),\n    );\n  }\n\n  Widget _buildOptionButton(\n    PopoverController controller,\n    BuildContext context,\n  ) {\n    return Card(\n      elevation: 1.0,\n      child: MouseRegion(\n        cursor: SystemMouseCursors.click,\n        child: GestureDetector(\n          onTap: () => controller.show(),\n          child: Transform.rotate(\n            angle: dir == TableDirection.col ? math.pi / 2 : 0,\n            child: const FlowySvg(\n              FlowySvgs.drag_element_s,\n              size: Size.square(18.0),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  void _onSelectAction(TableOptionAction action) {\n    switch (action) {\n      case TableOptionAction.addAfter:\n        TableActions.add(node, position + 1, editorState, dir);\n        break;\n      case TableOptionAction.addBefore:\n        TableActions.add(node, position, editorState, dir);\n        break;\n      case TableOptionAction.delete:\n        TableActions.delete(node, position, editorState, dir);\n        break;\n      case TableOptionAction.clear:\n        TableActions.clear(node, position, editorState, dir);\n        break;\n      case TableOptionAction.duplicate:\n        TableActions.duplicate(node, position, editorState, dir);\n        break;\n      default:\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/extensions/flowy_tint_extension.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/extension.dart';\nimport 'package:flutter/material.dart';\n\nconst tableCellDefaultColor = 'appflowy_table_cell_default_color';\n\nenum TableOptionAction {\n  addAfter,\n  addBefore,\n  delete,\n  duplicate,\n  clear,\n\n  /// row|cell background color\n  bgColor;\n\n  Widget icon(Color? color) {\n    switch (this) {\n      case TableOptionAction.addAfter:\n        return const FlowySvg(FlowySvgs.add_s);\n      case TableOptionAction.addBefore:\n        return const FlowySvg(FlowySvgs.add_s);\n      case TableOptionAction.delete:\n        return const FlowySvg(FlowySvgs.delete_s);\n      case TableOptionAction.duplicate:\n        return const FlowySvg(FlowySvgs.copy_s);\n      case TableOptionAction.clear:\n        return const FlowySvg(FlowySvgs.close_s);\n      case TableOptionAction.bgColor:\n        return const FlowySvg(\n          FlowySvgs.color_format_m,\n          size: Size.square(12),\n        ).padding(all: 2.0);\n    }\n  }\n\n  String get description {\n    switch (this) {\n      case TableOptionAction.addAfter:\n        return LocaleKeys.document_plugins_table_addAfter.tr();\n      case TableOptionAction.addBefore:\n        return LocaleKeys.document_plugins_table_addBefore.tr();\n      case TableOptionAction.delete:\n        return LocaleKeys.document_plugins_table_delete.tr();\n      case TableOptionAction.duplicate:\n        return LocaleKeys.document_plugins_table_duplicate.tr();\n      case TableOptionAction.clear:\n        return LocaleKeys.document_plugins_table_clear.tr();\n      case TableOptionAction.bgColor:\n        return LocaleKeys.document_plugins_table_bgColor.tr();\n    }\n  }\n}\n\nclass TableOptionActionWrapper extends ActionCell {\n  TableOptionActionWrapper(this.inner);\n\n  final TableOptionAction inner;\n\n  @override\n  Widget? leftIcon(Color iconColor) => inner.icon(iconColor);\n\n  @override\n  String get name => inner.description;\n}\n\nclass TableColorOptionAction extends PopoverActionCell {\n  TableColorOptionAction({\n    required this.node,\n    required this.editorState,\n    required this.position,\n    required this.dir,\n  });\n\n  final Node node;\n  final EditorState editorState;\n  final int position;\n  final TableDirection dir;\n\n  @override\n  Widget? leftIcon(Color iconColor) =>\n      TableOptionAction.bgColor.icon(iconColor);\n\n  @override\n  String get name => TableOptionAction.bgColor.description;\n\n  @override\n  Widget Function(\n    BuildContext context,\n    PopoverController parentController,\n    PopoverController controller,\n  ) get builder => (context, parentController, controller) {\n        int row = 0, col = position;\n        if (dir == TableDirection.row) {\n          col = 0;\n          row = position;\n        }\n\n        final cell = node.children.firstWhereOrNull(\n          (n) =>\n              n.attributes[TableCellBlockKeys.colPosition] == col &&\n              n.attributes[TableCellBlockKeys.rowPosition] == row,\n        );\n        final key = dir == TableDirection.col\n            ? TableCellBlockKeys.colBackgroundColor\n            : TableCellBlockKeys.rowBackgroundColor;\n        final bgColor = cell?.attributes[key] as String?;\n        final selectedColor = bgColor?.tryToColor();\n        // get default background color from themeExtension\n        final defaultColor = AFThemeExtension.of(context).tableCellBGColor;\n        final colors = [\n          // reset to default background color\n          FlowyColorOption(\n            color: defaultColor,\n            i18n: LocaleKeys.document_plugins_optionAction_defaultColor.tr(),\n            id: tableCellDefaultColor,\n          ),\n          ...FlowyTint.values.map(\n            (e) => FlowyColorOption(\n              color: e.color(context),\n              i18n: e.tintName(AppFlowyEditorL10n.current),\n              id: e.id,\n            ),\n          ),\n        ];\n\n        return FlowyColorPicker(\n          colors: colors,\n          selected: selectedColor,\n          border: Border.all(\n            color: AFThemeExtension.of(context).onBackground,\n          ),\n          onTap: (option, index) async {\n            final backgroundColor =\n                selectedColor != option.color ? option.id : '';\n            TableActions.setBgColor(\n              node,\n              position,\n              editorState,\n              backgroundColor,\n              dir,\n            );\n\n            controller.close();\n            parentController.close();\n          },\n        );\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/todo_list/todo_list_icon.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass TodoListIcon extends StatelessWidget {\n  const TodoListIcon({\n    super.key,\n    required this.node,\n    required this.onCheck,\n  });\n\n  final Node node;\n  final VoidCallback onCheck;\n\n  @override\n  Widget build(BuildContext context) {\n    // the icon height should be equal to the text height * text font size\n    final textStyle =\n        context.read<EditorState>().editorStyle.textStyleConfiguration;\n    final fontSize = textStyle.text.fontSize ?? 16.0;\n    final height = textStyle.text.height ?? textStyle.lineHeight;\n    final iconSize = fontSize * height;\n\n    final checked = node.attributes[TodoListBlockKeys.checked] ?? false;\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: () {\n        HapticFeedback.lightImpact();\n        onCheck();\n      },\n      child: Container(\n        constraints: BoxConstraints(\n          minWidth: iconSize,\n          minHeight: iconSize,\n        ),\n        margin: const EdgeInsets.only(right: 8.0),\n        alignment: Alignment.center,\n        child: FlowySvg(\n          checked\n              ? FlowySvgs.m_todo_list_checked_s\n              : FlowySvgs.m_todo_list_unchecked_s,\n          blendMode: checked ? null : BlendMode.srcIn,\n          size: Size.square(iconSize * 0.9),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart' hide TextDirection;\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass ToggleListBlockKeys {\n  const ToggleListBlockKeys._();\n\n  static const String type = 'toggle_list';\n\n  /// The content of a code block.\n  ///\n  /// The value is a String.\n  static const String delta = blockComponentDelta;\n\n  static const String backgroundColor = blockComponentBackgroundColor;\n\n  static const String textDirection = blockComponentTextDirection;\n\n  /// The value is a bool.\n  static const String collapsed = 'collapsed';\n\n  /// The value is a int.\n  ///\n  /// If this value is not null, the block represent a toggle heading.\n  static const String level = 'level';\n}\n\nNode toggleListBlockNode({\n  String? text,\n  Delta? delta,\n  bool collapsed = false,\n  String? textDirection,\n  Attributes? attributes,\n  Iterable<Node>? children,\n}) {\n  delta ??= Delta()..insert(text ?? '');\n  return Node(\n    type: ToggleListBlockKeys.type,\n    children: children ?? [],\n    attributes: {\n      if (textDirection != null)\n        ToggleListBlockKeys.textDirection: textDirection,\n      ToggleListBlockKeys.collapsed: collapsed,\n      ToggleListBlockKeys.delta: delta.toJson(),\n      if (attributes != null) ...attributes,\n    },\n  );\n}\n\nNode toggleHeadingNode({\n  int level = 1,\n  String? text,\n  Delta? delta,\n  bool collapsed = false,\n  String? textDirection,\n  Attributes? attributes,\n  Iterable<Node>? children,\n}) {\n  // only support level 1 - 6\n  level = level.clamp(1, 6);\n  return toggleListBlockNode(\n    text: text,\n    delta: delta,\n    collapsed: collapsed,\n    textDirection: textDirection,\n    children: children,\n    attributes: {\n      if (attributes != null) ...attributes,\n      ToggleListBlockKeys.level: level,\n    },\n  );\n}\n\n// defining the toggle list block menu item\nSelectionMenuItem toggleListBlockItem = SelectionMenuItem.node(\n  getName: LocaleKeys.document_plugins_toggleList.tr,\n  iconData: Icons.arrow_right,\n  keywords: ['collapsed list', 'toggle list', 'list'],\n  nodeBuilder: (editorState, _) => toggleListBlockNode(),\n  replace: (_, node) => node.delta?.isEmpty ?? false,\n);\n\nclass ToggleListBlockComponentBuilder extends BlockComponentBuilder {\n  ToggleListBlockComponentBuilder({\n    super.configuration,\n    this.padding = const EdgeInsets.all(0),\n    this.textStyleBuilder,\n  });\n\n  final EdgeInsets padding;\n\n  /// The text style of the toggle heading block.\n  final TextStyle Function(int level)? textStyleBuilder;\n\n  @override\n  BlockComponentWidget build(BlockComponentContext blockComponentContext) {\n    final node = blockComponentContext.node;\n    return ToggleListBlockComponentWidget(\n      key: node.key,\n      node: node,\n      configuration: configuration,\n      padding: padding,\n      textStyleBuilder: textStyleBuilder,\n      showActions: showActions(node),\n      actionBuilder: (context, state) => actionBuilder(\n        blockComponentContext,\n        state,\n      ),\n      actionTrailingBuilder: (context, state) => actionTrailingBuilder(\n        blockComponentContext,\n        state,\n      ),\n    );\n  }\n\n  @override\n  BlockComponentValidate get validate => (node) => node.delta != null;\n}\n\nclass ToggleListBlockComponentWidget extends BlockComponentStatefulWidget {\n  const ToggleListBlockComponentWidget({\n    super.key,\n    required super.node,\n    super.showActions,\n    super.actionBuilder,\n    super.actionTrailingBuilder,\n    super.configuration = const BlockComponentConfiguration(),\n    this.padding = const EdgeInsets.all(0),\n    this.textStyleBuilder,\n  });\n\n  final EdgeInsets padding;\n  final TextStyle Function(int level)? textStyleBuilder;\n\n  @override\n  State<ToggleListBlockComponentWidget> createState() =>\n      _ToggleListBlockComponentWidgetState();\n}\n\nclass _ToggleListBlockComponentWidgetState\n    extends State<ToggleListBlockComponentWidget>\n    with\n        SelectableMixin,\n        DefaultSelectableMixin,\n        BlockComponentConfigurable,\n        BlockComponentBackgroundColorMixin,\n        NestedBlockComponentStatefulWidgetMixin,\n        BlockComponentTextDirectionMixin,\n        BlockComponentAlignMixin {\n  // the key used to forward focus to the richtext child\n  @override\n  final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text');\n\n  @override\n  BlockComponentConfiguration get configuration => widget.configuration;\n\n  @override\n  GlobalKey<State<StatefulWidget>> get containerKey => node.key;\n\n  @override\n  GlobalKey<State<StatefulWidget>> blockComponentKey = GlobalKey(\n    debugLabel: ToggleListBlockKeys.type,\n  );\n\n  @override\n  Node get node => widget.node;\n\n  @override\n  EdgeInsets get indentPadding => configuration.indentPadding(\n        node,\n        calculateTextDirection(\n          layoutDirection: Directionality.maybeOf(context),\n        ),\n      );\n\n  bool get collapsed => node.attributes[ToggleListBlockKeys.collapsed] ?? false;\n\n  int? get level => node.attributes[ToggleListBlockKeys.level] as int?;\n\n  @override\n  Widget build(BuildContext context) {\n    return collapsed\n        ? buildComponent(context)\n        : buildComponentWithChildren(context);\n  }\n\n  @override\n  Widget buildComponentWithChildren(BuildContext context) {\n    return Stack(\n      children: [\n        if (backgroundColor != Colors.transparent)\n          Positioned.fill(\n            left: cachedLeft,\n            top: padding.top,\n            child: Container(\n              width: UniversalPlatform.isDesktop ? double.infinity : null,\n              color: backgroundColor,\n            ),\n          ),\n        Provider(\n          create: (context) =>\n              DatabasePluginWidgetBuilderSize(horizontalPadding: 0.0),\n          child: NestedListWidget(\n            indentPadding: indentPadding,\n            child: buildComponent(context),\n            children: editorState.renderer.buildList(\n              context,\n              widget.node.children,\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  @override\n  Widget buildComponent(\n    BuildContext context, {\n    bool withBackgroundColor = false,\n  }) {\n    Widget child = _buildToggleBlock();\n\n    child = BlockSelectionContainer(\n      node: node,\n      delegate: this,\n      listenable: editorState.selectionNotifier,\n      blockColor: editorState.editorStyle.selectionColor,\n      supportTypes: const [\n        BlockSelectionType.block,\n      ],\n      child: child,\n    );\n\n    child = Padding(\n      padding: padding,\n      child: Container(\n        key: blockComponentKey,\n        color: withBackgroundColor ||\n                (backgroundColor != Colors.transparent && collapsed)\n            ? backgroundColor\n            : null,\n        child: child,\n      ),\n    );\n\n    if (widget.showActions && widget.actionBuilder != null) {\n      child = BlockComponentActionWrapper(\n        node: node,\n        actionBuilder: widget.actionBuilder!,\n        actionTrailingBuilder: widget.actionTrailingBuilder,\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  Widget _buildToggleBlock() {\n    final textDirection = calculateTextDirection(\n      layoutDirection: Directionality.maybeOf(context),\n    );\n    final crossAxisAlignment = textDirection == TextDirection.ltr\n        ? CrossAxisAlignment.start\n        : CrossAxisAlignment.end;\n\n    return Container(\n      width: double.infinity,\n      alignment: alignment,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        crossAxisAlignment: crossAxisAlignment,\n        children: [\n          Row(\n            mainAxisSize: MainAxisSize.min,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            textDirection: textDirection,\n            children: [\n              _buildExpandIcon(),\n              Flexible(\n                child: _buildRichText(),\n              ),\n            ],\n          ),\n          _buildPlaceholder(),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildPlaceholder() {\n    // if the toggle block is collapsed or it contains children, don't show the\n    // placeholder.\n    if (collapsed || node.children.isNotEmpty) {\n      return const SizedBox.shrink();\n    }\n\n    return Padding(\n      padding: UniversalPlatform.isMobile\n          ? const EdgeInsets.symmetric(horizontal: 26.0)\n          : indentPadding,\n      child: FlowyButton(\n        text: FlowyText(\n          buildPlaceholderText(),\n          color: Theme.of(context).hintColor,\n        ),\n        margin: const EdgeInsets.symmetric(horizontal: 3.0, vertical: 8),\n        onTap: onAddContent,\n      ),\n    );\n  }\n\n  Widget _buildRichText() {\n    final textDirection = calculateTextDirection(\n      layoutDirection: Directionality.maybeOf(context),\n    );\n    final level = node.attributes[ToggleListBlockKeys.level];\n    return AppFlowyRichText(\n      key: forwardKey,\n      delegate: this,\n      node: widget.node,\n      editorState: editorState,\n      placeholderText: placeholderText,\n      lineHeight: 1.5,\n      textSpanDecorator: (textSpan) {\n        var result = textSpan.updateTextStyle(\n          textStyleWithTextSpan(textSpan: textSpan),\n        );\n        if (level != null) {\n          result = result.updateTextStyle(\n            widget.textStyleBuilder?.call(level),\n          );\n        }\n        return result;\n      },\n      placeholderTextSpanDecorator: (textSpan) {\n        var result = textSpan.updateTextStyle(\n          textStyleWithTextSpan(textSpan: textSpan),\n        );\n        if (level != null && widget.textStyleBuilder != null) {\n          result = result.updateTextStyle(\n            widget.textStyleBuilder?.call(level),\n          );\n        }\n        return result.updateTextStyle(\n          placeholderTextStyleWithTextSpan(textSpan: textSpan),\n        );\n      },\n      textDirection: textDirection,\n      textAlign: alignment?.toTextAlign ?? textAlign,\n      cursorColor: editorState.editorStyle.cursorColor,\n      selectionColor: editorState.editorStyle.selectionColor,\n    );\n  }\n\n  Widget _buildExpandIcon() {\n    double buttonHeight = UniversalPlatform.isDesktop ? 22.0 : 26.0;\n    final textDirection = calculateTextDirection(\n      layoutDirection: Directionality.maybeOf(context),\n    );\n\n    if (level != null) {\n      // top padding * 2 + button height = height of the heading text\n      final textStyle = widget.textStyleBuilder?.call(level ?? 1);\n      final fontSize = textStyle?.fontSize;\n      final lineHeight = textStyle?.height ?? 1.5;\n\n      if (fontSize != null) {\n        buttonHeight = fontSize * lineHeight;\n      }\n    }\n\n    final turns = switch (textDirection) {\n      TextDirection.ltr => collapsed ? 0.0 : 0.25,\n      TextDirection.rtl => collapsed ? -0.5 : -0.75,\n    };\n\n    return Container(\n      constraints: BoxConstraints(\n        minWidth: 26,\n        minHeight: buttonHeight,\n      ),\n      alignment: Alignment.center,\n      child: FlowyButton(\n        margin: const EdgeInsets.all(2.0),\n        useIntrinsicWidth: true,\n        onTap: onCollapsed,\n        text: AnimatedRotation(\n          turns: turns,\n          duration: const Duration(milliseconds: 200),\n          child: const Icon(\n            Icons.arrow_right,\n            size: 18.0,\n          ),\n        ),\n      ),\n    );\n  }\n\n  Future<void> onCollapsed() async {\n    final transaction = editorState.transaction\n      ..updateNode(node, {\n        ToggleListBlockKeys.collapsed: !collapsed,\n      });\n    transaction.afterSelection = editorState.selection;\n    await editorState.apply(transaction);\n  }\n\n  Future<void> onAddContent() async {\n    final transaction = editorState.transaction;\n    final path = node.path.child(0);\n    transaction.insertNode(\n      path,\n      paragraphNode(),\n    );\n    transaction.afterSelection = Selection.collapsed(Position(path: path));\n    await editorState.apply(transaction);\n  }\n\n  String buildPlaceholderText() {\n    if (level != null) {\n      return LocaleKeys.document_plugins_emptyToggleHeading.tr(\n        args: [level.toString()],\n      );\n    }\n    return LocaleKeys.document_plugins_emptyToggleList.tr();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nconst _greater = '>';\n\n/// Convert '> ' to toggle list\n///\n/// - support\n///   - desktop\n///   - mobile\n///   - web\n///\nCharacterShortcutEvent formatGreaterToToggleList = CharacterShortcutEvent(\n  key: 'format greater to toggle list',\n  character: ' ',\n  handler: (editorState) async => _formatGreaterSymbol(\n    editorState,\n    (node) => node.type != ToggleListBlockKeys.type,\n    (_, text, __) => text == _greater,\n    (text, node, delta, afterSelection) async => _formatGreaterToToggleHeading(\n      editorState,\n      text,\n      node,\n      delta,\n      afterSelection,\n    ),\n  ),\n);\n\nFuture<void> _formatGreaterToToggleHeading(\n  EditorState editorState,\n  String text,\n  Node node,\n  Delta delta,\n  Selection afterSelection,\n) async {\n  final type = node.type;\n  int? level;\n  if (type == ToggleListBlockKeys.type) {\n    level = node.attributes[ToggleListBlockKeys.level] as int?;\n  } else if (type == HeadingBlockKeys.type) {\n    level = node.attributes[HeadingBlockKeys.level] as int?;\n  }\n  delta = delta.compose(Delta()..delete(_greater.length));\n  // if the previous block is heading block, convert it to toggle heading block\n  if (type == HeadingBlockKeys.type && level != null) {\n    await BlockActionOptionCubit.turnIntoSingleToggleHeading(\n      type: ToggleListBlockKeys.type,\n      selectedNodes: [node],\n      level: level,\n      delta: delta,\n      editorState: editorState,\n      afterSelection: afterSelection,\n    );\n    return;\n  }\n\n  final transaction = editorState.transaction;\n  transaction\n    ..insertNode(\n      node.path,\n      toggleListBlockNode(\n        delta: delta,\n        children: node.children.map((e) => e.deepCopy()).toList(),\n      ),\n    )\n    ..deleteNode(node);\n  transaction.afterSelection = afterSelection;\n  await editorState.apply(transaction);\n}\n\n/// Press enter key to insert child node inside the toggle list\n///\n/// - support\n///   - desktop\n///   - mobile\n///   - web\nCharacterShortcutEvent insertChildNodeInsideToggleList = CharacterShortcutEvent(\n  key: 'insert child node inside toggle list',\n  character: '\\n',\n  handler: (editorState) async {\n    final selection = editorState.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return false;\n    }\n    final node = editorState.getNodeAtPath(selection.start.path);\n    final delta = node?.delta;\n    if (node == null ||\n        node.type != ToggleListBlockKeys.type ||\n        delta == null) {\n      return false;\n    }\n    final slicedDelta = delta.slice(selection.start.offset);\n    final transaction = editorState.transaction;\n    final bool collapsed =\n        node.attributes[ToggleListBlockKeys.collapsed] ?? false;\n    if (collapsed) {\n      // if the delta is empty, clear the format\n      if (delta.isEmpty) {\n        transaction\n          ..insertNode(\n            selection.start.path.next,\n            paragraphNode(),\n          )\n          ..deleteNode(node)\n          ..afterSelection = Selection.collapsed(\n            Position(path: selection.start.path),\n          );\n      } else if (selection.startIndex == 0) {\n        // insert a paragraph block above the current toggle list block\n        transaction.insertNode(selection.start.path, paragraphNode());\n        transaction.afterSelection = Selection.collapsed(\n          Position(path: selection.start.path.next),\n        );\n      } else {\n        // insert a toggle list block below the current toggle list block\n        transaction\n          ..deleteText(node, selection.startIndex, slicedDelta.length)\n          ..insertNodes(\n            selection.start.path.next,\n            [\n              toggleListBlockNode(collapsed: true, delta: slicedDelta),\n            ],\n          )\n          ..afterSelection = Selection.collapsed(\n            Position(path: selection.start.path.next),\n          );\n      }\n    } else {\n      // insert a paragraph block inside the current toggle list block\n      transaction\n        ..deleteText(node, selection.startIndex, slicedDelta.length)\n        ..insertNode(\n          selection.start.path + [0],\n          paragraphNode(delta: slicedDelta),\n        )\n        ..afterSelection = Selection.collapsed(\n          Position(path: selection.start.path + [0]),\n        );\n    }\n    await editorState.apply(transaction);\n    return true;\n  },\n);\n\n/// cmd/ctrl + enter to close or open the toggle list\n///\n/// - support\n///   - desktop\n///   - web\n///\n\n// toggle the todo list\nfinal CommandShortcutEvent toggleToggleListCommand = CommandShortcutEvent(\n  key: 'toggle the toggle list',\n  getDescription: () => AppFlowyEditorL10n.current.cmdToggleTodoList,\n  command: 'ctrl+enter',\n  macOSCommand: 'cmd+enter',\n  handler: _toggleToggleListCommandHandler,\n);\n\nCommandShortcutEventHandler _toggleToggleListCommandHandler = (editorState) {\n  if (UniversalPlatform.isMobile) {\n    assert(false, 'enter key is not supported on mobile platform.');\n    return KeyEventResult.ignored;\n  }\n\n  final selection = editorState.selection;\n  if (selection == null) {\n    return KeyEventResult.ignored;\n  }\n  final nodes = editorState.getNodesInSelection(selection);\n  if (nodes.isEmpty || nodes.length > 1) {\n    return KeyEventResult.ignored;\n  }\n\n  final node = nodes.first;\n  if (node.type != ToggleListBlockKeys.type) {\n    return KeyEventResult.ignored;\n  }\n\n  final collapsed = node.attributes[ToggleListBlockKeys.collapsed] as bool;\n  final transaction = editorState.transaction;\n  transaction.updateNode(node, {\n    ToggleListBlockKeys.collapsed: !collapsed,\n  });\n  transaction.afterSelection = selection;\n  editorState.apply(transaction);\n  return KeyEventResult.handled;\n};\n\n/// Press the backspace at the first position of first line to go to the title\n///\n/// - support\n///   - desktop\n///   - web\n///\nfinal CommandShortcutEvent removeToggleHeadingStyle = CommandShortcutEvent(\n  key: 'remove toggle heading style',\n  command: 'backspace',\n  getDescription: () => 'remove toggle heading style',\n  handler: (editorState) => _removeToggleHeadingStyle(\n    editorState: editorState,\n  ),\n);\n\n// convert the toggle heading block to heading block\nKeyEventResult _removeToggleHeadingStyle({\n  required EditorState editorState,\n}) {\n  final selection = editorState.selection;\n  if (selection == null ||\n      !selection.isCollapsed ||\n      selection.start.offset != 0) {\n    return KeyEventResult.ignored;\n  }\n\n  final node = editorState.getNodeAtPath(selection.start.path);\n  if (node == null || node.type != ToggleListBlockKeys.type) {\n    return KeyEventResult.ignored;\n  }\n\n  final level = node.attributes[ToggleListBlockKeys.level] as int?;\n  if (level == null) {\n    return KeyEventResult.ignored;\n  }\n\n  final transaction = editorState.transaction;\n  transaction.updateNode(node, {\n    ToggleListBlockKeys.level: null,\n  });\n  transaction.afterSelection = selection;\n  editorState.apply(transaction);\n\n  return KeyEventResult.handled;\n}\n\n/// Formats the current node to specified markdown style.\n///\n/// For example,\n///   bulleted list: '- '\n///   numbered list: '1. '\n///   quote: '\" '\n///   ...\n///\n/// The [nodeBuilder] can return a list of nodes, which will be inserted\n///   into the document.\n/// For example, when converting a bulleted list to a heading and the heading is\n///  not allowed to contain children, then the [nodeBuilder] should return a list\n///  of nodes, which contains the heading node and the children nodes.\nFuture<bool> _formatGreaterSymbol(\n  EditorState editorState,\n  bool Function(Node node) shouldFormat,\n  bool Function(\n    Node node,\n    String text,\n    Selection selection,\n  ) predicate,\n  Future<void> Function(\n    String text,\n    Node node,\n    Delta delta,\n    Selection afterSelection,\n  ) onFormat,\n) async {\n  final selection = editorState.selection;\n  if (selection == null || !selection.isCollapsed) {\n    return false;\n  }\n\n  final position = selection.end;\n  final node = editorState.getNodeAtPath(position.path);\n\n  if (node == null || !shouldFormat(node)) {\n    return false;\n  }\n\n  // Get the text from the start of the document until the selection.\n  final delta = node.delta;\n  if (delta == null) {\n    return false;\n  }\n  final text = delta.toPlainText().substring(0, selection.end.offset);\n\n  // If the text doesn't match the predicate, then we don't want to\n  // format it.\n  if (!predicate(node, text, selection)) {\n    return false;\n  }\n\n  final afterSelection = Selection.collapsed(\n    Position(\n      path: node.path,\n    ),\n  );\n\n  await onFormat(text, node, delta, afterSelection);\n\n  return true;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_page.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n// ignore: implementation_imports\nimport 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/tooltip_util.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/icon_button.dart';\nimport 'package:flutter/material.dart';\n\nimport 'custom_placeholder_toolbar_item.dart';\nimport 'toolbar_id_enum.dart';\n\nfinal List<ToolbarItem> customMarkdownFormatItems = [\n  _FormatToolbarItem(\n    id: ToolbarId.bold,\n    name: 'bold',\n    svg: FlowySvgs.toolbar_bold_m,\n  ),\n  group1PaddingItem,\n  _FormatToolbarItem(\n    id: ToolbarId.underline,\n    name: 'underline',\n    svg: FlowySvgs.toolbar_underline_m,\n  ),\n  group1PaddingItem,\n  _FormatToolbarItem(\n    id: ToolbarId.italic,\n    name: 'italic',\n    svg: FlowySvgs.toolbar_inline_italic_m,\n  ),\n];\n\nfinal ToolbarItem customInlineCodeItem = _FormatToolbarItem(\n  id: ToolbarId.code,\n  name: 'code',\n  svg: FlowySvgs.toolbar_inline_code_m,\n  group: 2,\n);\n\nclass _FormatToolbarItem extends ToolbarItem {\n  _FormatToolbarItem({\n    required ToolbarId id,\n    required String name,\n    required FlowySvgData svg,\n    super.group = 1,\n  }) : super(\n          id: id.id,\n          isActive: showInAnyTextType,\n          builder: (\n            context,\n            editorState,\n            highlightColor,\n            iconColor,\n            tooltipBuilder,\n          ) {\n            final selection = editorState.selection!;\n            final nodes = editorState.getNodesInSelection(selection);\n            final isHighlight = nodes.allSatisfyInSelection(\n              selection,\n              (delta) =>\n                  delta.isNotEmpty &&\n                  delta.everyAttributes((attr) => attr[name] == true),\n            );\n\n            final hoverColor = isHighlight\n                ? highlightColor\n                : EditorStyleCustomizer.toolbarHoverColor(context);\n            final isDark = !Theme.of(context).isLightMode;\n            final theme = AppFlowyTheme.of(context);\n\n            final child = FlowyIconButton(\n              width: 36,\n              height: 32,\n              hoverColor: hoverColor,\n              isSelected: isHighlight,\n              icon: FlowySvg(\n                svg,\n                size: Size.square(20.0),\n                color: (isDark && isHighlight)\n                    ? Color(0xFF282E3A)\n                    : theme.iconColorScheme.primary,\n              ),\n              onPressed: () => editorState.toggleAttribute(\n                name,\n                selection: selection,\n              ),\n            );\n\n            if (tooltipBuilder != null) {\n              return tooltipBuilder(\n                context,\n                id.id,\n                _getTooltipText(id),\n                child,\n              );\n            }\n            return child;\n          },\n        );\n}\n\nString _getTooltipText(ToolbarId id) {\n  switch (id) {\n    case ToolbarId.underline:\n      return '${LocaleKeys.toolbar_underline.tr()}${shortcutTooltips(\n        '⌘ + U',\n        'CTRL + U',\n        'CTRL + U',\n      )}';\n    case ToolbarId.bold:\n      return '${LocaleKeys.toolbar_bold.tr()}${shortcutTooltips(\n        '⌘ + B',\n        'CTRL + B',\n        'CTRL + B',\n      )}';\n    case ToolbarId.italic:\n      return '${LocaleKeys.toolbar_italic.tr()}${shortcutTooltips(\n        '⌘ + I',\n        'CTRL + I',\n        'CTRL + I',\n      )}';\n    case ToolbarId.code:\n      return '${LocaleKeys.document_toolbar_inlineCode.tr()}${shortcutTooltips(\n        '⌘ + E',\n        'CTRL + E',\n        'CTRL + E',\n      )}';\n    default:\n      return '';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_page.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart' hide ColorPicker;\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport 'toolbar_id_enum.dart';\n\nString? _customHighlightColorHex;\n\nfinal customHighlightColorItem = ToolbarItem(\n  id: ToolbarId.highlightColor.id,\n  group: 1,\n  isActive: showInAnyTextType,\n  builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) =>\n      HighlightColorPickerWidget(\n    editorState: editorState,\n    tooltipBuilder: tooltipBuilder,\n    highlightColor: highlightColor,\n  ),\n);\n\nclass HighlightColorPickerWidget extends StatefulWidget {\n  const HighlightColorPickerWidget({\n    super.key,\n    required this.editorState,\n    this.tooltipBuilder,\n    required this.highlightColor,\n  });\n\n  final EditorState editorState;\n  final ToolbarTooltipBuilder? tooltipBuilder;\n  final Color highlightColor;\n\n  @override\n  State<HighlightColorPickerWidget> createState() =>\n      _HighlightColorPickerWidgetState();\n}\n\nclass _HighlightColorPickerWidgetState\n    extends State<HighlightColorPickerWidget> {\n  final popoverController = PopoverController();\n\n  bool isSelected = false;\n\n  EditorState get editorState => widget.editorState;\n\n  Color get highlightColor => widget.highlightColor;\n\n  @override\n  void dispose() {\n    super.dispose();\n    popoverController.close();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (editorState.selection == null) {\n      return const SizedBox.shrink();\n    }\n    final selectionRectList = editorState.selectionRects();\n    final top =\n        selectionRectList.isEmpty ? 0.0 : selectionRectList.first.height;\n    return AppFlowyPopover(\n      controller: popoverController,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      offset: Offset(0, top),\n      onOpen: () => keepEditorFocusNotifier.increase(),\n      onClose: () {\n        setState(() {\n          isSelected = false;\n        });\n        keepEditorFocusNotifier.decrease();\n      },\n      margin: EdgeInsets.zero,\n      popupBuilder: (context) => buildPopoverContent(),\n      child: buildChild(context),\n    );\n  }\n\n  Widget buildChild(BuildContext context) {\n    final theme = AppFlowyTheme.of(context),\n        iconColor = theme.iconColorScheme.primary;\n\n    final child = FlowyIconButton(\n      width: 36,\n      height: 32,\n      hoverColor: EditorStyleCustomizer.toolbarHoverColor(context),\n      icon: SizedBox(\n        width: 20,\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            FlowySvg(\n              FlowySvgs.toolbar_text_highlight_m,\n              size: Size(20, 16),\n              color: iconColor,\n            ),\n            buildColorfulDivider(iconColor),\n          ],\n        ),\n      ),\n      onPressed: () {\n        setState(() {\n          isSelected = true;\n        });\n        showPopover();\n      },\n    );\n\n    return widget.tooltipBuilder?.call(\n          context,\n          ToolbarId.highlightColor.id,\n          AppFlowyEditorL10n.current.highlightColor,\n          child,\n        ) ??\n        child;\n  }\n\n  Widget buildColorfulDivider(Color? iconColor) {\n    final List<String> colors = [];\n    final selection = editorState.selection!;\n    final nodes = editorState.getNodesInSelection(selection);\n    final isHighLight = nodes.allSatisfyInSelection(selection, (delta) {\n      if (delta.everyAttributes((attr) => attr.isEmpty)) {\n        return false;\n      }\n\n      return delta.everyAttributes((attr) {\n        final textColorHex = attr[AppFlowyRichTextKeys.backgroundColor];\n        if (textColorHex != null) colors.add(textColorHex);\n        return (textColorHex != null);\n      });\n    });\n\n    final colorLength = colors.length;\n    if (colors.isEmpty || !isHighLight) {\n      return Container(\n        width: 20,\n        height: 4,\n        color: iconColor,\n      );\n    }\n    return SizedBox(\n      width: 20,\n      height: 4,\n      child: Row(\n        children: List.generate(colorLength, (index) {\n          final currentColor = int.tryParse(colors[index]);\n          return Container(\n            width: 20 / colorLength,\n            height: 4,\n            color: currentColor == null ? iconColor : Color(currentColor),\n          );\n        }),\n      ),\n    );\n  }\n\n  Widget buildPopoverContent() {\n    final List<String> colors = [];\n\n    final selection = editorState.selection!;\n    final nodes = editorState.getNodesInSelection(selection);\n    final isHighlight = nodes.allSatisfyInSelection(selection, (delta) {\n      if (delta.everyAttributes((attr) => attr.isEmpty)) {\n        return false;\n      }\n\n      return delta.everyAttributes((attributes) {\n        final highlightColorHex =\n            attributes[AppFlowyRichTextKeys.backgroundColor];\n        if (highlightColorHex != null) colors.add(highlightColorHex);\n        return highlightColorHex != null;\n      });\n    });\n    bool showClearButton = false;\n    nodes.allSatisfyInSelection(selection, (delta) {\n      if (!showClearButton) {\n        showClearButton = delta.whereType<TextInsert>().any(\n          (element) {\n            return element.attributes?[AppFlowyRichTextKeys.backgroundColor] !=\n                null;\n          },\n        );\n      }\n      return true;\n    });\n    return MouseRegion(\n      child: ColorPicker(\n        title: AppFlowyEditorL10n.current.highlightColor,\n        showClearButton: showClearButton,\n        selectedColorHex:\n            (colors.length == 1 && isHighlight) ? colors.first : null,\n        customColorHex: _customHighlightColorHex,\n        colorOptions: generateHighlightColorOptions(),\n        onSubmittedColorHex: (color, isCustomColor) {\n          if (isCustomColor) {\n            _customHighlightColorHex = color;\n          }\n          formatHighlightColor(\n            editorState,\n            editorState.selection,\n            color,\n            withUpdateSelection: true,\n          );\n          hidePopover();\n        },\n        resetText: AppFlowyEditorL10n.current.clearHighlightColor,\n        resetIconName: 'clear_highlight_color',\n      ),\n    );\n  }\n\n  void showPopover() {\n    keepEditorFocusNotifier.increase();\n    popoverController.show();\n  }\n\n  void hidePopover() {\n    popoverController.close();\n    keepEditorFocusNotifier.decrease();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/icon_button.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'toolbar_id_enum.dart';\n\nconst kIsPageLink = 'is_page_link';\n\nfinal customLinkItem = ToolbarItem(\n  id: ToolbarId.link.id,\n  group: 4,\n  isActive: (state) =>\n      !isNarrowWindow(state) && onlyShowInSingleSelectionAndTextType(state),\n  builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) {\n    final selection = editorState.selection!;\n    final nodes = editorState.getNodesInSelection(selection);\n    final isHref = nodes.allSatisfyInSelection(selection, (delta) {\n      return delta.everyAttributes(\n        (attributes) => attributes[AppFlowyRichTextKeys.href] != null,\n      );\n    });\n\n    final isDark = !Theme.of(context).isLightMode;\n    final hoverColor = isHref\n        ? highlightColor\n        : EditorStyleCustomizer.toolbarHoverColor(context);\n    final theme = AppFlowyTheme.of(context);\n    final child = FlowyIconButton(\n      width: 36,\n      height: 32,\n      hoverColor: hoverColor,\n      isSelected: isHref,\n      icon: FlowySvg(\n        FlowySvgs.toolbar_link_m,\n        size: Size.square(20.0),\n        color: (isDark && isHref)\n            ? Color(0xFF282E3A)\n            : theme.iconColorScheme.primary,\n      ),\n      onPressed: () {\n        getIt<FloatingToolbarController>().hideToolbar();\n        if (!isHref) {\n          final viewId = context.read<DocumentBloc?>()?.documentId ?? '';\n          showLinkCreateMenu(context, editorState, selection, viewId);\n        } else {\n          WidgetsBinding.instance.addPostFrameCallback((_) {\n            getIt<LinkHoverTriggers>()\n                .call(HoverTriggerKey(nodes.first.id, selection));\n          });\n        }\n      },\n    );\n\n    if (tooltipBuilder != null) {\n      return tooltipBuilder(\n        context,\n        ToolbarId.highlightColor.id,\n        AppFlowyEditorL10n.current.link,\n        child,\n      );\n    }\n\n    return child;\n  },\n);\n\nextension AttributeExtension on Attributes {\n  bool get isPage {\n    if (this[kIsPageLink] is bool) {\n      return this[kIsPageLink];\n    }\n    return false;\n  }\n}\n\nenum LinkMenuAlignment {\n  topLeft,\n  topRight,\n  bottomLeft,\n  bottomRight,\n}\n\nextension LinkMenuAlignmentExtension on LinkMenuAlignment {\n  bool get isTop =>\n      this == LinkMenuAlignment.topLeft || this == LinkMenuAlignment.topRight;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_page.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport 'toolbar_id_enum.dart';\n\nfinal ToolbarItem customPlaceholderItem = ToolbarItem(\n  id: ToolbarId.placeholder.id,\n  group: -1,\n  isActive: (editorState) => true,\n  builder: (context, __, ___, ____, _____) {\n    final isDark = Theme.of(context).brightness == Brightness.dark;\n    return Padding(\n      padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 5),\n      child: Container(\n        width: 1,\n        color: Color(0xffE8ECF3).withAlpha(isDark ? 40 : 255),\n      ),\n    );\n  },\n);\n\nToolbarItem buildPaddingPlaceholderItem(\n  int group, {\n  bool Function(EditorState editorState)? isActive,\n}) =>\n    ToolbarItem(\n      id: ToolbarId.paddingPlaceHolder.id,\n      group: group,\n      isActive: isActive,\n      builder: (context, __, ___, ____, _____) => HSpace(4),\n    );\n\nToolbarItem group0PaddingItem = buildPaddingPlaceholderItem(\n  0,\n  isActive: onlyShowInTextTypeAndExcludeTable,\n);\n\nToolbarItem group1PaddingItem =\n    buildPaddingPlaceholderItem(1, isActive: showInAnyTextType);\n\nToolbarItem group4PaddingItem = buildPaddingPlaceholderItem(\n  4,\n  isActive: (state) =>\n      !isNarrowWindow(state) && onlyShowInSingleSelectionAndTextType(state),\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport 'toolbar_id_enum.dart';\n\nfinal ToolbarItem customTextAlignItem = ToolbarItem(\n  id: ToolbarId.textAlign.id,\n  group: 4,\n  isActive: (state) =>\n      !isNarrowWindow(state) && onlyShowInSingleSelectionAndTextType(state),\n  builder: (\n    context,\n    editorState,\n    highlightColor,\n    iconColor,\n    tooltipBuilder,\n  ) {\n    return TextAlignActionList(\n      editorState: editorState,\n      tooltipBuilder: tooltipBuilder,\n      highlightColor: highlightColor,\n    );\n  },\n);\n\nclass TextAlignActionList extends StatefulWidget {\n  const TextAlignActionList({\n    super.key,\n    required this.editorState,\n    required this.highlightColor,\n    this.tooltipBuilder,\n    this.child,\n    this.onSelect,\n    this.popoverController,\n    this.popoverDirection = PopoverDirection.bottomWithLeftAligned,\n    this.showOffset = const Offset(0, 2),\n  });\n\n  final EditorState editorState;\n  final ToolbarTooltipBuilder? tooltipBuilder;\n  final Color highlightColor;\n  final Widget? child;\n  final VoidCallback? onSelect;\n  final PopoverController? popoverController;\n  final PopoverDirection popoverDirection;\n  final Offset showOffset;\n\n  @override\n  State<TextAlignActionList> createState() => _TextAlignActionListState();\n}\n\nclass _TextAlignActionListState extends State<TextAlignActionList> {\n  late PopoverController popoverController =\n      widget.popoverController ?? PopoverController();\n\n  bool isSelected = false;\n\n  EditorState get editorState => widget.editorState;\n\n  Color get highlightColor => widget.highlightColor;\n\n  @override\n  void dispose() {\n    super.dispose();\n    popoverController.close();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      controller: popoverController,\n      direction: widget.popoverDirection,\n      offset: widget.showOffset,\n      onOpen: () => keepEditorFocusNotifier.increase(),\n      onClose: () {\n        setState(() {\n          isSelected = false;\n        });\n        keepEditorFocusNotifier.decrease();\n      },\n      popupBuilder: (context) => buildPopoverContent(),\n      child: widget.child ?? buildChild(context),\n    );\n  }\n\n  void showPopover() {\n    keepEditorFocusNotifier.increase();\n    popoverController.show();\n  }\n\n  Widget buildChild(BuildContext context) {\n    final theme = AppFlowyTheme.of(context),\n        iconColor = theme.iconColorScheme.primary;\n    final child = FlowyIconButton(\n      width: 48,\n      height: 32,\n      isSelected: isSelected,\n      hoverColor: EditorStyleCustomizer.toolbarHoverColor(context),\n      icon: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          FlowySvg(\n            FlowySvgs.toolbar_alignment_m,\n            size: Size.square(20),\n            color: iconColor,\n          ),\n          HSpace(4),\n          FlowySvg(\n            FlowySvgs.toolbar_arrow_down_m,\n            size: Size(12, 20),\n            color: iconColor,\n          ),\n        ],\n      ),\n      onPressed: () {\n        setState(() {\n          isSelected = true;\n        });\n        showPopover();\n      },\n    );\n\n    return widget.tooltipBuilder?.call(\n          context,\n          ToolbarId.textAlign.id,\n          LocaleKeys.document_toolbar_textAlign.tr(),\n          child,\n        ) ??\n        child;\n  }\n\n  Widget buildPopoverContent() {\n    return MouseRegion(\n      child: SeparatedColumn(\n        mainAxisSize: MainAxisSize.min,\n        separatorBuilder: () => const VSpace(4.0),\n        children: List.generate(TextAlignCommand.values.length, (index) {\n          final command = TextAlignCommand.values[index];\n          final selection = editorState.selection!;\n          final nodes = editorState.getNodesInSelection(selection);\n          final isHighlight = nodes.every(\n            (n) => n.attributes[blockComponentAlign] == command.name,\n          );\n\n          return SizedBox(\n            height: 36,\n            child: FlowyButton(\n              leftIconSize: const Size.square(20),\n              leftIcon: FlowySvg(command.svg),\n              iconPadding: 12,\n              text: FlowyText(\n                command.title,\n                fontWeight: FontWeight.w400,\n                figmaLineHeight: 20,\n              ),\n              rightIcon:\n                  isHighlight ? FlowySvg(FlowySvgs.toolbar_check_m) : null,\n              onTap: () {\n                command.onAlignChanged(editorState);\n                widget.onSelect?.call();\n                popoverController.close();\n              },\n            ),\n          );\n        }),\n      ),\n    );\n  }\n}\n\nenum TextAlignCommand {\n  left(FlowySvgs.toolbar_text_align_left_m),\n  center(FlowySvgs.toolbar_text_align_center_m),\n  right(FlowySvgs.toolbar_text_align_right_m);\n\n  const TextAlignCommand(this.svg);\n\n  final FlowySvgData svg;\n\n  String get title {\n    switch (this) {\n      case left:\n        return LocaleKeys.document_toolbar_alignLeft.tr();\n      case center:\n        return LocaleKeys.document_toolbar_alignCenter.tr();\n      case right:\n        return LocaleKeys.document_toolbar_alignRight.tr();\n    }\n  }\n\n  Future<void> onAlignChanged(EditorState editorState) async {\n    final selection = editorState.selection!;\n\n    await editorState.updateNode(\n      selection,\n      (node) => node.copyWith(\n        attributes: {\n          ...node.attributes,\n          blockComponentAlign: name,\n        },\n      ),\n      selectionExtraInfo: {\n        selectionExtraInfoDoNotAttachTextService: true,\n        selectionExtraInfoDisableFloatingToolbar: true,\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_page.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart' hide ColorPicker;\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'toolbar_id_enum.dart';\n\nString? _customColorHex;\n\nfinal customTextColorItem = ToolbarItem(\n  id: ToolbarId.textColor.id,\n  group: 1,\n  isActive: showInAnyTextType,\n  builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) =>\n      TextColorPickerWidget(\n    editorState: editorState,\n    tooltipBuilder: tooltipBuilder,\n    highlightColor: highlightColor,\n  ),\n);\n\nclass TextColorPickerWidget extends StatefulWidget {\n  const TextColorPickerWidget({\n    super.key,\n    required this.editorState,\n    this.tooltipBuilder,\n    required this.highlightColor,\n  });\n\n  final EditorState editorState;\n  final ToolbarTooltipBuilder? tooltipBuilder;\n  final Color highlightColor;\n\n  @override\n  State<TextColorPickerWidget> createState() => _TextColorPickerWidgetState();\n}\n\nclass _TextColorPickerWidgetState extends State<TextColorPickerWidget> {\n  final popoverController = PopoverController();\n\n  bool isSelected = false;\n\n  EditorState get editorState => widget.editorState;\n\n  Color get highlightColor => widget.highlightColor;\n\n  @override\n  void dispose() {\n    super.dispose();\n    popoverController.close();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (editorState.selection == null) {\n      return const SizedBox.shrink();\n    }\n    final selectionRectList = editorState.selectionRects();\n    final top =\n        selectionRectList.isEmpty ? 0.0 : selectionRectList.first.height;\n    return AppFlowyPopover(\n      controller: popoverController,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      offset: Offset(0, top),\n      onOpen: () => keepEditorFocusNotifier.increase(),\n      onClose: () {\n        setState(() {\n          isSelected = false;\n        });\n        keepEditorFocusNotifier.decrease();\n      },\n      margin: EdgeInsets.zero,\n      popupBuilder: (context) => buildPopoverContent(),\n      child: buildChild(context),\n    );\n  }\n\n  Widget buildChild(BuildContext context) {\n    final theme = AppFlowyTheme.of(context),\n        iconColor = theme.iconColorScheme.primary;\n    final child = FlowyIconButton(\n      width: 36,\n      height: 32,\n      hoverColor: EditorStyleCustomizer.toolbarHoverColor(context),\n      icon: SizedBox(\n        width: 20,\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            FlowySvg(\n              FlowySvgs.toolbar_text_color_m,\n              size: Size(20, 16),\n              color: iconColor,\n            ),\n            buildColorfulDivider(iconColor),\n          ],\n        ),\n      ),\n      onPressed: () {\n        setState(() {\n          isSelected = true;\n        });\n        showPopover();\n      },\n    );\n\n    return widget.tooltipBuilder?.call(\n          context,\n          ToolbarId.textColor.id,\n          LocaleKeys.document_toolbar_textColor.tr(),\n          child,\n        ) ??\n        child;\n  }\n\n  Widget buildColorfulDivider(Color? iconColor) {\n    final List<String> colors = [];\n    final selection = editorState.selection!;\n    final nodes = editorState.getNodesInSelection(selection);\n    final isHighLight = nodes.allSatisfyInSelection(selection, (delta) {\n      if (delta.everyAttributes((attr) => attr.isEmpty)) {\n        return false;\n      }\n\n      return delta.everyAttributes((attr) {\n        final textColorHex = attr[AppFlowyRichTextKeys.textColor];\n        if (textColorHex != null) colors.add(textColorHex);\n        return (textColorHex != null);\n      });\n    });\n\n    final colorLength = colors.length;\n    if (colors.isEmpty || !isHighLight) {\n      return Container(\n        width: 20,\n        height: 4,\n        color: iconColor,\n      );\n    }\n    return SizedBox(\n      width: 20,\n      height: 4,\n      child: Row(\n        children: List.generate(colorLength, (index) {\n          final currentColor = int.tryParse(colors[index]);\n          return Container(\n            width: 20 / colorLength,\n            height: 4,\n            color: currentColor == null ? iconColor : Color(currentColor),\n          );\n        }),\n      ),\n    );\n  }\n\n  Widget buildPopoverContent() {\n    bool showClearButton = false;\n    final List<String> colors = [];\n    final selection = editorState.selection!;\n    final nodes = editorState.getNodesInSelection(selection);\n    final isHighLight = nodes.allSatisfyInSelection(selection, (delta) {\n      if (delta.everyAttributes((attr) => attr.isEmpty)) {\n        return false;\n      }\n\n      return delta.everyAttributes((attr) {\n        final textColorHex = attr[AppFlowyRichTextKeys.textColor];\n        if (textColorHex != null) colors.add(textColorHex);\n        return (textColorHex != null);\n      });\n    });\n    nodes.allSatisfyInSelection(\n      selection,\n      (delta) {\n        if (!showClearButton) {\n          showClearButton = delta.whereType<TextInsert>().any(\n            (element) {\n              return element.attributes?[AppFlowyRichTextKeys.textColor] !=\n                  null;\n            },\n          );\n        }\n        return true;\n      },\n    );\n    return MouseRegion(\n      child: ColorPicker(\n        title: LocaleKeys.document_toolbar_textColor.tr(),\n        showClearButton: showClearButton,\n        selectedColorHex:\n            (colors.length == 1 && isHighLight) ? colors.first : null,\n        customColorHex: _customColorHex,\n        colorOptions: generateTextColorOptions(),\n        onSubmittedColorHex: (color, isCustomColor) {\n          if (isCustomColor) {\n            _customColorHex = color;\n          }\n          formatFontColor(\n            editorState,\n            editorState.selection,\n            color,\n            withUpdateSelection: true,\n          );\n          hidePopover();\n        },\n        resetText: AppFlowyEditorL10n.current.resetToDefaultColor,\n        resetIconName: 'reset_text_color',\n      ),\n    );\n  }\n\n  void showPopover() {\n    keepEditorFocusNotifier.increase();\n    popoverController.show();\n  }\n\n  void hidePopover() {\n    popoverController.close();\n    keepEditorFocusNotifier.decrease();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_page.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n// ignore: implementation_imports\nimport 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/tooltip_util.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'custom_text_align_toolbar_item.dart';\nimport 'text_suggestions_toolbar_item.dart';\n\nconst _kMoreOptionItemId = 'editor.more_option';\nconst kFontToolbarItemId = 'editor.font';\n\n@visibleForTesting\nconst kFontFamilyToolbarItemKey = ValueKey('FontFamilyToolbarItem');\n\nfinal ToolbarItem moreOptionItem = ToolbarItem(\n  id: _kMoreOptionItemId,\n  group: 5,\n  isActive: showInAnyTextType,\n  builder: (\n    context,\n    editorState,\n    highlightColor,\n    iconColor,\n    tooltipBuilder,\n  ) {\n    return MoreOptionActionList(\n      editorState: editorState,\n      tooltipBuilder: tooltipBuilder,\n      highlightColor: highlightColor,\n    );\n  },\n);\n\nclass MoreOptionActionList extends StatefulWidget {\n  const MoreOptionActionList({\n    super.key,\n    required this.editorState,\n    required this.highlightColor,\n    this.tooltipBuilder,\n  });\n\n  final EditorState editorState;\n  final ToolbarTooltipBuilder? tooltipBuilder;\n  final Color highlightColor;\n\n  @override\n  State<MoreOptionActionList> createState() => _MoreOptionActionListState();\n}\n\nclass _MoreOptionActionListState extends State<MoreOptionActionList> {\n  final popoverController = PopoverController();\n  PopoverController fontPopoverController = PopoverController();\n  PopoverController suggestionsPopoverController = PopoverController();\n  PopoverController textAlignPopoverController = PopoverController();\n\n  bool isSelected = false;\n\n  EditorState get editorState => widget.editorState;\n\n  Color get highlightColor => widget.highlightColor;\n\n  MoreOptionCommand? tappedCommand;\n\n  @override\n  void dispose() {\n    super.dispose();\n    popoverController.close();\n    fontPopoverController.close();\n    suggestionsPopoverController.close();\n    textAlignPopoverController.close();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      controller: popoverController,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      offset: const Offset(0, 2.0),\n      onOpen: () => keepEditorFocusNotifier.increase(),\n      onClose: () {\n        setState(() {\n          isSelected = false;\n        });\n        keepEditorFocusNotifier.decrease();\n      },\n      popupBuilder: (context) => buildPopoverContent(),\n      child: buildChild(context),\n    );\n  }\n\n  void showPopover() {\n    keepEditorFocusNotifier.increase();\n    popoverController.show();\n  }\n\n  Widget buildChild(BuildContext context) {\n    final iconColor = Theme.of(context).iconTheme.color;\n    final child = FlowyIconButton(\n      width: 36,\n      height: 32,\n      isSelected: isSelected,\n      hoverColor: EditorStyleCustomizer.toolbarHoverColor(context),\n      icon: FlowySvg(\n        FlowySvgs.toolbar_more_m,\n        size: Size.square(20),\n        color: iconColor,\n      ),\n      onPressed: () {\n        setState(() {\n          isSelected = true;\n        });\n        showPopover();\n      },\n    );\n\n    return widget.tooltipBuilder?.call(\n          context,\n          _kMoreOptionItemId,\n          LocaleKeys.document_toolbar_moreOptions.tr(),\n          child,\n        ) ??\n        child;\n  }\n\n  Color? getFormulaColor() {\n    if (isFormulaHighlight(editorState)) {\n      return widget.highlightColor;\n    }\n    return null;\n  }\n\n  Color? getStrikethroughColor() {\n    final selection = editorState.selection;\n    if (selection == null || selection.isCollapsed) {\n      return null;\n    }\n    final node = editorState.getNodeAtPath(selection.start.path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      return null;\n    }\n\n    final nodes = editorState.getNodesInSelection(selection);\n    final isHighlight = nodes.allSatisfyInSelection(\n      selection,\n      (delta) =>\n          delta.isNotEmpty &&\n          delta.everyAttributes(\n            (attr) => attr[MoreOptionCommand.strikethrough.name] == true,\n          ),\n    );\n    return isHighlight ? widget.highlightColor : null;\n  }\n\n  Widget buildPopoverContent() {\n    final showFormula = onlyShowInSingleSelectionAndTextType(editorState);\n    const fontColor = Color(0xff99A1A8);\n    final isNarrow = isNarrowWindow(editorState);\n    return MouseRegion(\n      child: SeparatedColumn(\n        mainAxisSize: MainAxisSize.min,\n        separatorBuilder: () => const VSpace(4.0),\n        children: [\n          if (isNarrow) ...[\n            buildTurnIntoSelector(),\n            buildCommandItem(MoreOptionCommand.link),\n            buildTextAlignSelector(),\n          ],\n          buildFontSelector(),\n          buildCommandItem(\n            MoreOptionCommand.strikethrough,\n            rightIcon: FlowyText(\n              shortcutTooltips(\n                '⌘⇧S',\n                'Ctrl⇧S',\n                'Ctrl⇧S',\n              ).trim(),\n              color: fontColor,\n              fontSize: 12,\n              figmaLineHeight: 16,\n              fontWeight: FontWeight.w400,\n            ),\n          ),\n          if (showFormula)\n            buildCommandItem(\n              MoreOptionCommand.formula,\n              rightIcon: FlowyText(\n                shortcutTooltips(\n                  '⌘⇧E',\n                  'Ctrl⇧E',\n                  'Ctrl⇧E',\n                ).trim(),\n                color: fontColor,\n                fontSize: 12,\n                figmaLineHeight: 16,\n                fontWeight: FontWeight.w400,\n              ),\n            ),\n        ],\n      ),\n    );\n  }\n\n  Widget buildCommandItem(\n    MoreOptionCommand command, {\n    Widget? rightIcon,\n    VoidCallback? onTap,\n  }) {\n    final isFontCommand = command == MoreOptionCommand.font;\n    return SizedBox(\n      height: 36,\n      child: FlowyButton(\n        key: isFontCommand ? kFontFamilyToolbarItemKey : null,\n        leftIconSize: const Size.square(20),\n        leftIcon: FlowySvg(command.svg),\n        rightIcon: rightIcon,\n        iconPadding: 12,\n        text: FlowyText(\n          command.title,\n          figmaLineHeight: 20,\n          fontWeight: FontWeight.w400,\n        ),\n        onTap: onTap ??\n            () {\n              command.onExecute(editorState, context);\n              hideOtherPopovers(command);\n              if (command != MoreOptionCommand.font) {\n                popoverController.close();\n              }\n            },\n      ),\n    );\n  }\n\n  Widget buildFontSelector() {\n    final selection = editorState.selection!;\n    final String? currentFontFamily = editorState\n        .getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.fontFamily);\n    return FontFamilyDropDown(\n      currentFontFamily: currentFontFamily ?? '',\n      offset: const Offset(-240, 0),\n      popoverController: fontPopoverController,\n      onOpen: () => keepEditorFocusNotifier.increase(),\n      onClose: () => keepEditorFocusNotifier.decrease(),\n      onFontFamilyChanged: (fontFamily) async {\n        fontPopoverController.close();\n        popoverController.close();\n        try {\n          await editorState.formatDelta(selection, {\n            AppFlowyRichTextKeys.fontFamily: fontFamily,\n          });\n        } catch (e) {\n          Log.error('Failed to set font family: $e');\n        }\n      },\n      onResetFont: () async {\n        fontPopoverController.close();\n        popoverController.close();\n        await editorState\n            .formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null});\n      },\n      child: buildCommandItem(\n        MoreOptionCommand.font,\n        rightIcon: FlowySvg(FlowySvgs.toolbar_arrow_right_m),\n      ),\n    );\n  }\n\n  Widget buildTurnIntoSelector() {\n    final selectionRects = editorState.selectionRects();\n    double height = -6;\n    if (selectionRects.isNotEmpty) height = selectionRects.first.height;\n    return SuggestionsActionList(\n      editorState: editorState,\n      popoverController: suggestionsPopoverController,\n      popoverDirection: PopoverDirection.leftWithTopAligned,\n      showOffset: Offset(-8, height),\n      onSelect: () => getIt<FloatingToolbarController>().hideToolbar(),\n      child: buildCommandItem(\n        MoreOptionCommand.suggestions,\n        rightIcon: FlowySvg(FlowySvgs.toolbar_arrow_right_m),\n        onTap: () {\n          if (tappedCommand == MoreOptionCommand.suggestions) return;\n          hideOtherPopovers(MoreOptionCommand.suggestions);\n          keepEditorFocusNotifier.increase();\n          suggestionsPopoverController.show();\n        },\n      ),\n    );\n  }\n\n  Widget buildTextAlignSelector() {\n    return TextAlignActionList(\n      editorState: editorState,\n      popoverController: textAlignPopoverController,\n      popoverDirection: PopoverDirection.leftWithTopAligned,\n      showOffset: Offset(-8, 0),\n      onSelect: () => getIt<FloatingToolbarController>().hideToolbar(),\n      highlightColor: highlightColor,\n      child: buildCommandItem(\n        MoreOptionCommand.textAlign,\n        rightIcon: FlowySvg(FlowySvgs.toolbar_arrow_right_m),\n        onTap: () {\n          if (tappedCommand == MoreOptionCommand.textAlign) return;\n          hideOtherPopovers(MoreOptionCommand.textAlign);\n          keepEditorFocusNotifier.increase();\n          textAlignPopoverController.show();\n        },\n      ),\n    );\n  }\n\n  void hideOtherPopovers(MoreOptionCommand currentCommand) {\n    if (tappedCommand == currentCommand) return;\n    if (tappedCommand == MoreOptionCommand.font) {\n      fontPopoverController.close();\n      fontPopoverController = PopoverController();\n    } else if (tappedCommand == MoreOptionCommand.suggestions) {\n      suggestionsPopoverController.close();\n      suggestionsPopoverController = PopoverController();\n    } else if (tappedCommand == MoreOptionCommand.textAlign) {\n      textAlignPopoverController.close();\n      textAlignPopoverController = PopoverController();\n    }\n    tappedCommand = currentCommand;\n  }\n}\n\nenum MoreOptionCommand {\n  suggestions(FlowySvgs.turninto_s),\n  link(FlowySvgs.toolbar_link_m),\n  textAlign(\n    FlowySvgs.toolbar_alignment_m,\n  ),\n  font(FlowySvgs.type_font_m),\n  strikethrough(FlowySvgs.type_strikethrough_m),\n  formula(FlowySvgs.type_formula_m);\n\n  const MoreOptionCommand(this.svg);\n\n  final FlowySvgData svg;\n\n  String get title {\n    switch (this) {\n      case suggestions:\n        return LocaleKeys.document_toolbar_turnInto.tr();\n      case link:\n        return LocaleKeys.document_toolbar_link.tr();\n      case textAlign:\n        return LocaleKeys.button_align.tr();\n      case font:\n        return LocaleKeys.document_toolbar_font.tr();\n      case strikethrough:\n        return LocaleKeys.editor_strikethrough.tr();\n      case formula:\n        return LocaleKeys.document_toolbar_equation.tr();\n    }\n  }\n\n  Future<void> onExecute(EditorState editorState, BuildContext context) async {\n    final selection = editorState.selection!;\n    if (this == link) {\n      final nodes = editorState.getNodesInSelection(selection);\n      final isHref = nodes.allSatisfyInSelection(selection, (delta) {\n        return delta.everyAttributes(\n          (attributes) => attributes[AppFlowyRichTextKeys.href] != null,\n        );\n      });\n      getIt<FloatingToolbarController>().hideToolbar();\n      if (isHref) {\n        getIt<LinkHoverTriggers>().call(\n          HoverTriggerKey(nodes.first.id, selection),\n        );\n      } else {\n        final viewId = context.read<DocumentBloc?>()?.documentId ?? '';\n        showLinkCreateMenu(context, editorState, selection, viewId);\n      }\n    } else if (this == strikethrough) {\n      await editorState.toggleAttribute(name);\n    } else if (this == formula) {\n      final node = editorState.getNodeAtPath(selection.start.path);\n      final delta = node?.delta;\n      if (node == null || delta == null) {\n        return;\n      }\n\n      final transaction = editorState.transaction;\n      final isHighlight = isFormulaHighlight(editorState);\n      if (isHighlight) {\n        final formula = delta\n            .slice(selection.startIndex, selection.endIndex)\n            .whereType<TextInsert>()\n            .firstOrNull\n            ?.attributes?[InlineMathEquationKeys.formula];\n        assert(formula != null);\n        if (formula == null) {\n          return;\n        }\n        // clear the format\n        transaction.replaceText(\n          node,\n          selection.startIndex,\n          selection.length,\n          formula,\n          attributes: {},\n        );\n      } else {\n        final text = editorState.getTextInSelection(selection).join();\n        transaction.replaceText(\n          node,\n          selection.startIndex,\n          selection.length,\n          MentionBlockKeys.mentionChar,\n          attributes: {\n            InlineMathEquationKeys.formula: text,\n          },\n        );\n      }\n      await editorState.apply(transaction);\n    }\n  }\n}\n\nbool isFormulaHighlight(EditorState editorState) {\n  final selection = editorState.selection;\n  if (selection == null || selection.isCollapsed) {\n    return false;\n  }\n  final node = editorState.getNodeAtPath(selection.start.path);\n  final delta = node?.delta;\n  if (node == null || delta == null) {\n    return false;\n  }\n\n  final nodes = editorState.getNodesInSelection(selection);\n  return nodes.allSatisfyInSelection(selection, (delta) {\n    return delta.everyAttributes(\n      (attributes) => attributes[InlineMathEquationKeys.formula] != null,\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport 'toolbar_id_enum.dart';\n\nfinal ToolbarItem customTextHeadingItem = ToolbarItem(\n  id: ToolbarId.textHeading.id,\n  group: 1,\n  isActive: onlyShowInSingleTextTypeSelectionAndExcludeTable,\n  builder: (\n    context,\n    editorState,\n    highlightColor,\n    iconColor,\n    tooltipBuilder,\n  ) {\n    return TextHeadingActionList(\n      editorState: editorState,\n      tooltipBuilder: tooltipBuilder,\n    );\n  },\n);\n\nclass TextHeadingActionList extends StatefulWidget {\n  const TextHeadingActionList({\n    super.key,\n    required this.editorState,\n    this.tooltipBuilder,\n  });\n\n  final EditorState editorState;\n  final ToolbarTooltipBuilder? tooltipBuilder;\n\n  @override\n  State<TextHeadingActionList> createState() => _TextHeadingActionListState();\n}\n\nclass _TextHeadingActionListState extends State<TextHeadingActionList> {\n  final popoverController = PopoverController();\n\n  bool isSelected = false;\n\n  @override\n  void dispose() {\n    super.dispose();\n    popoverController.close();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      controller: popoverController,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      offset: const Offset(0, 2.0),\n      onOpen: () => keepEditorFocusNotifier.increase(),\n      onClose: () {\n        setState(() {\n          isSelected = false;\n        });\n        keepEditorFocusNotifier.decrease();\n      },\n      popupBuilder: (context) => buildPopoverContent(),\n      child: buildChild(context),\n    );\n  }\n\n  void showPopover() {\n    keepEditorFocusNotifier.increase();\n    popoverController.show();\n  }\n\n  Widget buildChild(BuildContext context) {\n    final theme = AppFlowyTheme.of(context),\n        iconColor = theme.iconColorScheme.primary;\n    final child = FlowyIconButton(\n      width: 48,\n      height: 32,\n      isSelected: isSelected,\n      hoverColor: EditorStyleCustomizer.toolbarHoverColor(context),\n      icon: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          FlowySvg(\n            FlowySvgs.toolbar_text_format_m,\n            size: Size.square(20),\n            color: iconColor,\n          ),\n          HSpace(4),\n          FlowySvg(\n            FlowySvgs.toolbar_arrow_down_m,\n            size: Size(12, 20),\n            color: iconColor,\n          ),\n        ],\n      ),\n      onPressed: () {\n        setState(() {\n          isSelected = true;\n        });\n        showPopover();\n      },\n    );\n\n    return widget.tooltipBuilder?.call(\n          context,\n          ToolbarId.textHeading.id,\n          LocaleKeys.document_toolbar_textSize.tr(),\n          child,\n        ) ??\n        child;\n  }\n\n  Widget buildPopoverContent() {\n    final selectingCommand = getSelectingCommand();\n    return MouseRegion(\n      child: SeparatedColumn(\n        mainAxisSize: MainAxisSize.min,\n        separatorBuilder: () => const VSpace(4.0),\n        children: List.generate(TextHeadingCommand.values.length, (index) {\n          final command = TextHeadingCommand.values[index];\n          return SizedBox(\n            height: 36,\n            child: FlowyButton(\n              leftIconSize: const Size.square(20),\n              leftIcon: FlowySvg(command.svg),\n              iconPadding: 12,\n              text: FlowyText(\n                command.title,\n                fontWeight: FontWeight.w400,\n                figmaLineHeight: 20,\n              ),\n              rightIcon: selectingCommand == command\n                  ? FlowySvg(FlowySvgs.toolbar_check_m)\n                  : null,\n              onTap: () {\n                if (command == selectingCommand) return;\n                command.onExecute(widget.editorState);\n                popoverController.close();\n              },\n            ),\n          );\n        }),\n      ),\n    );\n  }\n\n  TextHeadingCommand? getSelectingCommand() {\n    final editorState = widget.editorState;\n    final selection = editorState.selection;\n    if (selection == null || !selection.isSingle) {\n      return null;\n    }\n    final node = editorState.getNodeAtPath(selection.start.path);\n    if (node == null || node.delta == null) {\n      return null;\n    }\n    final nodeType = node.type;\n    if (nodeType == ParagraphBlockKeys.type) return TextHeadingCommand.text;\n    if (nodeType == HeadingBlockKeys.type) {\n      final level = node.attributes[HeadingBlockKeys.level] ?? 1;\n      if (level == 1) return TextHeadingCommand.h1;\n      if (level == 2) return TextHeadingCommand.h2;\n      if (level == 3) return TextHeadingCommand.h3;\n    }\n    return null;\n  }\n}\n\nenum TextHeadingCommand {\n  text(FlowySvgs.type_text_m),\n  h1(FlowySvgs.type_h1_m),\n  h2(FlowySvgs.type_h2_m),\n  h3(FlowySvgs.type_h3_m);\n\n  const TextHeadingCommand(this.svg);\n\n  final FlowySvgData svg;\n\n  String get title {\n    switch (this) {\n      case text:\n        return AppFlowyEditorL10n.current.text;\n      case h1:\n        return LocaleKeys.document_toolbar_h1.tr();\n      case h2:\n        return LocaleKeys.document_toolbar_h2.tr();\n      case h3:\n        return LocaleKeys.document_toolbar_h3.tr();\n    }\n  }\n\n  void onExecute(EditorState state) {\n    switch (this) {\n      case text:\n        formatNodeToText(state);\n        break;\n      case h1:\n        _turnInto(state, 1);\n        break;\n      case h2:\n        _turnInto(state, 2);\n        break;\n      case h3:\n        _turnInto(state, 3);\n        break;\n    }\n  }\n\n  Future<void> _turnInto(EditorState state, int level) async {\n    final selection = state.selection!;\n    final node = state.getNodeAtPath(selection.start.path)!;\n    await BlockActionOptionCubit.turnIntoBlock(\n      HeadingBlockKeys.type,\n      node,\n      state,\n      level: level,\n      keepSelection: true,\n    );\n  }\n}\n\nvoid formatNodeToText(EditorState editorState) {\n  final selection = editorState.selection!;\n  final node = editorState.getNodeAtPath(selection.start.path)!;\n  final delta = (node.delta ?? Delta()).toJson();\n  editorState.formatNode(\n    selection,\n    (node) => node.copyWith(\n      type: ParagraphBlockKeys.type,\n      attributes: {\n        blockComponentDelta: delta,\n        blockComponentBackgroundColor:\n            node.attributes[blockComponentBackgroundColor],\n        blockComponentTextDirection:\n            node.attributes[blockComponentTextDirection],\n      },\n    ),\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart",
    "content": "import 'dart:collection';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart'\n    hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys;\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\n\nimport 'text_heading_toolbar_item.dart';\nimport 'toolbar_id_enum.dart';\n\n@visibleForTesting\nconst kSuggestionsItemKey = ValueKey('SuggestionsItem');\n\n@visibleForTesting\nconst kSuggestionsItemListKey = ValueKey('SuggestionsItemList');\n\nfinal ToolbarItem suggestionsItem = ToolbarItem(\n  id: ToolbarId.suggestions.id,\n  group: 3,\n  isActive: enableSuggestions,\n  builder: (\n    context,\n    editorState,\n    highlightColor,\n    iconColor,\n    tooltipBuilder,\n  ) {\n    return SuggestionsActionList(\n      editorState: editorState,\n      tooltipBuilder: tooltipBuilder,\n    );\n  },\n);\n\nclass SuggestionsActionList extends StatefulWidget {\n  const SuggestionsActionList({\n    super.key,\n    required this.editorState,\n    this.tooltipBuilder,\n    this.child,\n    this.onSelect,\n    this.popoverController,\n    this.popoverDirection = PopoverDirection.bottomWithLeftAligned,\n    this.showOffset = const Offset(0, 2),\n  });\n\n  final EditorState editorState;\n  final ToolbarTooltipBuilder? tooltipBuilder;\n  final Widget? child;\n  final VoidCallback? onSelect;\n  final PopoverController? popoverController;\n  final PopoverDirection popoverDirection;\n  final Offset showOffset;\n\n  @override\n  State<SuggestionsActionList> createState() => _SuggestionsActionListState();\n}\n\nclass _SuggestionsActionListState extends State<SuggestionsActionList> {\n  late PopoverController popoverController =\n      widget.popoverController ?? PopoverController();\n\n  bool isSelected = false;\n\n  final List<SuggestionItem> suggestionItems = suggestions.sublist(0, 4);\n  final List<SuggestionItem> turnIntoItems =\n      suggestions.sublist(4, suggestions.length);\n\n  EditorState get editorState => widget.editorState;\n\n  SuggestionItem currentSuggestionItem = textSuggestionItem;\n\n  @override\n  void initState() {\n    super.initState();\n    refreshSuggestions();\n    editorState.selectionNotifier.addListener(refreshSuggestions);\n  }\n\n  @override\n  void dispose() {\n    editorState.selectionNotifier.removeListener(refreshSuggestions);\n    popoverController.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      controller: popoverController,\n      direction: widget.popoverDirection,\n      offset: widget.showOffset,\n      onOpen: () => keepEditorFocusNotifier.increase(),\n      onClose: () {\n        setState(() {\n          isSelected = false;\n        });\n        keepEditorFocusNotifier.decrease();\n      },\n      constraints: const BoxConstraints(maxWidth: 240, maxHeight: 400),\n      popupBuilder: (context) => buildPopoverContent(context),\n      child: widget.child ?? buildChild(context),\n    );\n  }\n\n  void showPopover() {\n    keepEditorFocusNotifier.increase();\n    popoverController.show();\n  }\n\n  Widget buildChild(BuildContext context) {\n    final theme = AppFlowyTheme.of(context),\n        iconColor = theme.iconColorScheme.primary;\n    final child = FlowyHover(\n      isSelected: () => isSelected,\n      style: HoverStyle(\n        hoverColor: EditorStyleCustomizer.toolbarHoverColor(context),\n        foregroundColorOnHover: Theme.of(context).iconTheme.color,\n      ),\n      resetHoverOnRebuild: false,\n      child: FlowyTooltip(\n        preferBelow: true,\n        child: RawMaterialButton(\n          key: kSuggestionsItemKey,\n          constraints: BoxConstraints(maxHeight: 32, minWidth: 60),\n          clipBehavior: Clip.antiAlias,\n          hoverElevation: 0,\n          highlightElevation: 0,\n          shape: RoundedRectangleBorder(borderRadius: Corners.s6Border),\n          fillColor: Colors.transparent,\n          hoverColor: Colors.transparent,\n          focusColor: Colors.transparent,\n          splashColor: Colors.transparent,\n          highlightColor: Colors.transparent,\n          elevation: 0,\n          onPressed: () {\n            setState(() {\n              isSelected = true;\n            });\n            showPopover();\n          },\n          child: Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 8),\n            child: Row(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                FlowyText(\n                  currentSuggestionItem.title,\n                  fontWeight: FontWeight.w400,\n                  figmaLineHeight: 20,\n                ),\n                HSpace(4),\n                FlowySvg(\n                  FlowySvgs.toolbar_arrow_down_m,\n                  size: Size(12, 20),\n                  color: iconColor,\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n\n    return widget.tooltipBuilder?.call(\n          context,\n          ToolbarId.suggestions.id,\n          currentSuggestionItem.title,\n          child,\n        ) ??\n        child;\n  }\n\n  Widget buildPopoverContent(BuildContext context) {\n    final textColor = Color(0xff99A1A8);\n    return MouseRegion(\n      child: SingleChildScrollView(\n        key: kSuggestionsItemListKey,\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            buildSubTitle(\n              LocaleKeys.document_toolbar_suggestions.tr(),\n              textColor,\n            ),\n            ...List.generate(suggestionItems.length, (index) {\n              return buildItem(suggestionItems[index]);\n            }),\n            buildSubTitle(LocaleKeys.document_toolbar_turnInto.tr(), textColor),\n            ...List.generate(turnIntoItems.length, (index) {\n              return buildItem(turnIntoItems[index]);\n            }),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget buildItem(SuggestionItem item) {\n    final isSelected = item.type == currentSuggestionItem.type;\n    return SizedBox(\n      height: 36,\n      child: FlowyButton(\n        leftIconSize: const Size.square(20),\n        leftIcon: FlowySvg(item.svg),\n        iconPadding: 12,\n        text: FlowyText(\n          item.title,\n          fontWeight: FontWeight.w400,\n          figmaLineHeight: 20,\n        ),\n        rightIcon: isSelected ? FlowySvg(FlowySvgs.toolbar_check_m) : null,\n        onTap: () {\n          item.onTap(widget.editorState, true);\n          widget.onSelect?.call();\n          popoverController.close();\n        },\n      ),\n    );\n  }\n\n  Widget buildSubTitle(String text, Color color) {\n    return Container(\n      height: 32,\n      margin: EdgeInsets.symmetric(horizontal: 8),\n      child: Align(\n        alignment: Alignment.centerLeft,\n        child: FlowyText.semibold(\n          text,\n          color: color,\n          figmaLineHeight: 16,\n        ),\n      ),\n    );\n  }\n\n  void refreshSuggestions() {\n    final selection = editorState.selection;\n    if (selection == null || !selection.isSingle) {\n      return;\n    }\n    final node = editorState.getNodeAtPath(selection.start.path);\n    if (node == null || node.delta == null) {\n      return;\n    }\n    final nodeType = node.type;\n    SuggestionType? suggestionType;\n    if (nodeType == HeadingBlockKeys.type) {\n      final level = node.attributes[HeadingBlockKeys.level] ?? 1;\n      if (level == 1) {\n        suggestionType = SuggestionType.h1;\n      } else if (level == 2) {\n        suggestionType = SuggestionType.h2;\n      } else if (level == 3) {\n        suggestionType = SuggestionType.h3;\n      }\n    } else if (nodeType == ToggleListBlockKeys.type) {\n      final level = node.attributes[ToggleListBlockKeys.level];\n      if (level == null) {\n        suggestionType = SuggestionType.toggle;\n      } else if (level == 1) {\n        suggestionType = SuggestionType.toggleH1;\n      } else if (level == 2) {\n        suggestionType = SuggestionType.toggleH2;\n      } else if (level == 3) {\n        suggestionType = SuggestionType.toggleH3;\n      }\n    } else {\n      suggestionType = nodeType2SuggestionType[nodeType];\n    }\n    if (suggestionType == null) return;\n    suggestionItems.clear();\n    turnIntoItems.clear();\n    for (final item in suggestions) {\n      if (item.type.group == suggestionType.group &&\n          item.type != suggestionType) {\n        suggestionItems.add(item);\n      } else {\n        turnIntoItems.add(item);\n      }\n    }\n    currentSuggestionItem =\n        suggestions.where((item) => item.type == suggestionType).first;\n    if (mounted) setState(() {});\n  }\n}\n\nclass SuggestionItem {\n  SuggestionItem({\n    required this.type,\n    required this.title,\n    required this.svg,\n    required this.onTap,\n  });\n\n  final SuggestionType type;\n  final String title;\n  final FlowySvgData svg;\n  final Function(EditorState state, bool keepSelection) onTap;\n}\n\nenum SuggestionGroup { textHeading, list, toggle, quote, page }\n\nenum SuggestionType {\n  text(SuggestionGroup.textHeading),\n  h1(SuggestionGroup.textHeading),\n  h2(SuggestionGroup.textHeading),\n  h3(SuggestionGroup.textHeading),\n  checkbox(SuggestionGroup.list),\n  bulleted(SuggestionGroup.list),\n  numbered(SuggestionGroup.list),\n  toggle(SuggestionGroup.toggle),\n  toggleH1(SuggestionGroup.toggle),\n  toggleH2(SuggestionGroup.toggle),\n  toggleH3(SuggestionGroup.toggle),\n  callOut(SuggestionGroup.quote),\n  quote(SuggestionGroup.quote),\n  page(SuggestionGroup.page);\n\n  const SuggestionType(this.group);\n\n  final SuggestionGroup group;\n}\n\nfinal textSuggestionItem = SuggestionItem(\n  type: SuggestionType.text,\n  title: AppFlowyEditorL10n.current.text,\n  svg: FlowySvgs.type_text_m,\n  onTap: (state, _) => formatNodeToText(state),\n);\n\nfinal h1SuggestionItem = SuggestionItem(\n  type: SuggestionType.h1,\n  title: LocaleKeys.document_toolbar_h1.tr(),\n  svg: FlowySvgs.type_h1_m,\n  onTap: (state, keepSelection) => _turnInto(\n    state,\n    HeadingBlockKeys.type,\n    level: 1,\n    keepSelection: keepSelection,\n  ),\n);\n\nfinal h2SuggestionItem = SuggestionItem(\n  type: SuggestionType.h2,\n  title: LocaleKeys.document_toolbar_h2.tr(),\n  svg: FlowySvgs.type_h2_m,\n  onTap: (state, keepSelection) => _turnInto(\n    state,\n    HeadingBlockKeys.type,\n    level: 2,\n    keepSelection: keepSelection,\n  ),\n);\n\nfinal h3SuggestionItem = SuggestionItem(\n  type: SuggestionType.h3,\n  title: LocaleKeys.document_toolbar_h3.tr(),\n  svg: FlowySvgs.type_h3_m,\n  onTap: (state, keepSelection) => _turnInto(\n    state,\n    HeadingBlockKeys.type,\n    level: 3,\n    keepSelection: keepSelection,\n  ),\n);\n\nfinal checkboxSuggestionItem = SuggestionItem(\n  type: SuggestionType.checkbox,\n  title: LocaleKeys.editor_checkbox.tr(),\n  svg: FlowySvgs.type_todo_m,\n  onTap: (state, keepSelection) => _turnInto(\n    state,\n    TodoListBlockKeys.type,\n    keepSelection: keepSelection,\n  ),\n);\n\nfinal bulletedSuggestionItem = SuggestionItem(\n  type: SuggestionType.bulleted,\n  title: LocaleKeys.editor_bulletedListShortForm.tr(),\n  svg: FlowySvgs.type_bulleted_list_m,\n  onTap: (state, keepSelection) => _turnInto(\n    state,\n    BulletedListBlockKeys.type,\n    keepSelection: keepSelection,\n  ),\n);\n\nfinal numberedSuggestionItem = SuggestionItem(\n  type: SuggestionType.numbered,\n  title: LocaleKeys.editor_numberedListShortForm.tr(),\n  svg: FlowySvgs.type_numbered_list_m,\n  onTap: (state, keepSelection) => _turnInto(\n    state,\n    NumberedListBlockKeys.type,\n    keepSelection: keepSelection,\n  ),\n);\n\nfinal toggleSuggestionItem = SuggestionItem(\n  type: SuggestionType.toggle,\n  title: LocaleKeys.editor_toggleListShortForm.tr(),\n  svg: FlowySvgs.type_toggle_list_m,\n  onTap: (state, keepSelection) => _turnInto(\n    state,\n    ToggleListBlockKeys.type,\n    keepSelection: keepSelection,\n  ),\n);\n\nfinal toggleH1SuggestionItem = SuggestionItem(\n  type: SuggestionType.toggleH1,\n  title: LocaleKeys.editor_toggleHeading1ShortForm.tr(),\n  svg: FlowySvgs.type_toggle_h1_m,\n  onTap: (state, keepSelection) => _turnInto(\n    state,\n    ToggleListBlockKeys.type,\n    level: 1,\n    keepSelection: keepSelection,\n  ),\n);\n\nfinal toggleH2SuggestionItem = SuggestionItem(\n  type: SuggestionType.toggleH2,\n  title: LocaleKeys.editor_toggleHeading2ShortForm.tr(),\n  svg: FlowySvgs.type_toggle_h2_m,\n  onTap: (state, keepSelection) => _turnInto(\n    state,\n    ToggleListBlockKeys.type,\n    level: 2,\n    keepSelection: keepSelection,\n  ),\n);\n\nfinal toggleH3SuggestionItem = SuggestionItem(\n  type: SuggestionType.toggleH3,\n  title: LocaleKeys.editor_toggleHeading3ShortForm.tr(),\n  svg: FlowySvgs.type_toggle_h3_m,\n  onTap: (state, keepSelection) => _turnInto(\n    state,\n    ToggleListBlockKeys.type,\n    level: 3,\n    keepSelection: keepSelection,\n  ),\n);\n\nfinal callOutSuggestionItem = SuggestionItem(\n  type: SuggestionType.callOut,\n  title: LocaleKeys.document_plugins_callout.tr(),\n  svg: FlowySvgs.type_callout_m,\n  onTap: (state, keepSelection) => _turnInto(\n    state,\n    CalloutBlockKeys.type,\n    keepSelection: keepSelection,\n  ),\n);\n\nfinal quoteSuggestionItem = SuggestionItem(\n  type: SuggestionType.quote,\n  title: LocaleKeys.editor_quote.tr(),\n  svg: FlowySvgs.type_quote_m,\n  onTap: (state, keepSelection) => _turnInto(\n    state,\n    QuoteBlockKeys.type,\n    keepSelection: keepSelection,\n  ),\n);\n\nfinal pateItem = SuggestionItem(\n  type: SuggestionType.page,\n  title: LocaleKeys.editor_page.tr(),\n  svg: FlowySvgs.icon_document_s,\n  onTap: (state, keepSelection) => _turnInto(\n    state,\n    SubPageBlockKeys.type,\n    viewId: getIt<MenuSharedState>().latestOpenView?.id,\n    keepSelection: keepSelection,\n  ),\n);\n\nFuture<void> _turnInto(\n  EditorState state,\n  String type, {\n  int? level,\n  String? viewId,\n  bool keepSelection = true,\n}) async {\n  final selection = state.selection!;\n  final node = state.getNodeAtPath(selection.start.path)!;\n  await BlockActionOptionCubit.turnIntoBlock(\n    type,\n    node,\n    state,\n    level: level,\n    currentViewId: viewId,\n    keepSelection: keepSelection,\n  );\n}\n\nfinal suggestions = UnmodifiableListView([\n  textSuggestionItem,\n  h1SuggestionItem,\n  h2SuggestionItem,\n  h3SuggestionItem,\n  checkboxSuggestionItem,\n  bulletedSuggestionItem,\n  numberedSuggestionItem,\n  toggleSuggestionItem,\n  toggleH1SuggestionItem,\n  toggleH2SuggestionItem,\n  toggleH3SuggestionItem,\n  callOutSuggestionItem,\n  quoteSuggestionItem,\n  pateItem,\n]);\n\nfinal nodeType2SuggestionType = UnmodifiableMapView({\n  ParagraphBlockKeys.type: SuggestionType.text,\n  NumberedListBlockKeys.type: SuggestionType.numbered,\n  BulletedListBlockKeys.type: SuggestionType.bulleted,\n  QuoteBlockKeys.type: SuggestionType.quote,\n  TodoListBlockKeys.type: SuggestionType.checkbox,\n  CalloutBlockKeys.type: SuggestionType.callOut,\n});\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/toolbar_id_enum.dart",
    "content": "enum ToolbarId {\n  bold,\n  underline,\n  italic,\n  code,\n  highlightColor,\n  textColor,\n  link,\n  placeholder,\n  paddingPlaceHolder,\n  textAlign,\n  moreOption,\n  textHeading,\n  suggestions,\n}\n\nextension ToolbarIdExtension on ToolbarId {\n  String get id => 'editor.$name';\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/block_transaction_handler.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_handler.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\n/// A handler for transactions that involve a Block Component.\n///\n/// This is a subclass of [EditorTransactionHandler] that is used for block components.\n/// Specifically this transaction handler only needs to concern itself with changes to\n/// a [Node], and doesn't care about text deltas.\n///\nabstract class BlockTransactionHandler extends EditorTransactionHandler<Node> {\n  const BlockTransactionHandler({required super.type})\n      : super(livesInDelta: false);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_handler.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\n/// A handler for transactions that involve a Block Component.\n/// The [T] type is the type of data that this transaction handler takes.\n///\n/// In case of a block component, the [T] type should be a [Node].\n/// In case of a mention component, the [T] type should be a [Map].\n///\nabstract class EditorTransactionHandler<T> {\n  const EditorTransactionHandler({\n    required this.type,\n    this.livesInDelta = false,\n  });\n\n  /// The type of the block/mention that this handler is built for.\n  /// It's used to determine whether to call any of the handlers on certain transactions.\n  ///\n  final String type;\n\n  /// If the block is a \"mention\" type, it lives inside the [Delta] of a [Node].\n  ///\n  final bool livesInDelta;\n\n  Future<void> onTransaction(\n    BuildContext context,\n    String viewId,\n    EditorState editorState,\n    List<T> added,\n    List<T> removed, {\n    bool isCut = false,\n    bool isUndoRedo = false,\n    bool isPaste = false,\n    bool isDraggingNode = false,\n    bool isTurnInto = false,\n    String? parentViewId,\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/document/presentation/editor_notification.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/date_transaction_handler.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_transaction_handler.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_handler.dart';\nimport 'package:appflowy/shared/clipboard_state.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'mention_transaction_handler.dart';\n\nfinal _transactionHandlers = <EditorTransactionHandler>[\n  if (FeatureFlag.inlineSubPageMention.isOn) ...[\n    SubPageTransactionHandler(),\n    ChildPageTransactionHandler(),\n  ],\n  DateTransactionHandler(),\n];\n\n/// Handles delegating transactions to appropriate handlers.\n///\n/// Such as the [ChildPageTransactionHandler] for inline child pages.\n///\nclass EditorTransactionService extends StatefulWidget {\n  const EditorTransactionService({\n    super.key,\n    required this.viewId,\n    required this.editorState,\n    required this.child,\n  });\n\n  final String viewId;\n  final EditorState editorState;\n  final Widget child;\n\n  @override\n  State<EditorTransactionService> createState() =>\n      _EditorTransactionServiceState();\n}\n\nclass _EditorTransactionServiceState extends State<EditorTransactionService> {\n  StreamSubscription<EditorTransactionValue>? transactionSubscription;\n\n  bool isUndoRedo = false;\n  bool isPaste = false;\n  bool isDraggingNode = false;\n  bool isTurnInto = false;\n\n  @override\n  void initState() {\n    super.initState();\n    transactionSubscription =\n        widget.editorState.transactionStream.listen(onEditorTransaction);\n    EditorNotification.addListener(onEditorNotification);\n  }\n\n  @override\n  void dispose() {\n    EditorNotification.removeListener(onEditorNotification);\n    transactionSubscription?.cancel();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return widget.child;\n  }\n\n  void onEditorNotification(EditorNotificationType type) {\n    if ([EditorNotificationType.undo, EditorNotificationType.redo]\n        .contains(type)) {\n      isUndoRedo = true;\n    } else if (type == EditorNotificationType.paste) {\n      isPaste = true;\n    } else if (type == EditorNotificationType.dragStart) {\n      isDraggingNode = true;\n    } else if (type == EditorNotificationType.dragEnd) {\n      isDraggingNode = false;\n    } else if (type == EditorNotificationType.turnInto) {\n      isTurnInto = true;\n    }\n\n    if (type == EditorNotificationType.undo) {\n      undoCommand.execute(widget.editorState);\n    } else if (type == EditorNotificationType.redo) {\n      redoCommand.execute(widget.editorState);\n    } else if (type == EditorNotificationType.exitEditing &&\n        widget.editorState.selection != null) {\n      // If the editor is disposed, we don't need to reset the selection.\n      if (!widget.editorState.isDisposed) {\n        widget.editorState.selection = null;\n      }\n    }\n  }\n\n  /// Collects all nodes of a certain type, including those that are nested.\n  ///\n  List<Node> collectMatchingNodes(\n    Node node,\n    String type, {\n    bool livesInDelta = false,\n  }) {\n    final List<Node> matchingNodes = [];\n    if (node.type == type) {\n      matchingNodes.add(node);\n    }\n\n    if (livesInDelta && node.attributes[blockComponentDelta] != null) {\n      final deltas = node.attributes[blockComponentDelta];\n      if (deltas is List) {\n        for (final delta in deltas) {\n          if (delta['attributes'] != null &&\n              delta['attributes'][type] != null) {\n            matchingNodes.add(node);\n          }\n        }\n      }\n    }\n\n    for (final child in node.children) {\n      matchingNodes.addAll(\n        collectMatchingNodes(\n          child,\n          type,\n          livesInDelta: livesInDelta,\n        ),\n      );\n    }\n\n    return matchingNodes;\n  }\n\n  void onEditorTransaction(EditorTransactionValue event) {\n    final time = event.$1;\n    final transaction = event.$2;\n\n    if (time == TransactionTime.before) {\n      return;\n    }\n\n    final Map<String, dynamic> added = {\n      for (final handler in _transactionHandlers)\n        handler.type: handler.livesInDelta ? <MentionBlockData>[] : <Node>[],\n    };\n    final Map<String, dynamic> removed = {\n      for (final handler in _transactionHandlers)\n        handler.type: handler.livesInDelta ? <MentionBlockData>[] : <Node>[],\n    };\n\n    // based on the type of the transaction handler\n    final uniqueTransactionHandlers = <String, EditorTransactionHandler>{};\n    for (final handler in _transactionHandlers) {\n      uniqueTransactionHandlers.putIfAbsent(handler.type, () => handler);\n    }\n\n    for (final op in transaction.operations) {\n      if (op is InsertOperation) {\n        for (final n in op.nodes) {\n          for (final handler in uniqueTransactionHandlers.values) {\n            if (handler.livesInDelta) {\n              added[handler.type]!\n                  .addAll(extractMentionsForType(n, handler.type));\n            } else {\n              added[handler.type]!\n                  .addAll(collectMatchingNodes(n, handler.type));\n            }\n          }\n        }\n      } else if (op is DeleteOperation) {\n        for (final n in op.nodes) {\n          for (final handler in uniqueTransactionHandlers.values) {\n            if (handler.livesInDelta) {\n              removed[handler.type]!.addAll(\n                extractMentionsForType(n, handler.type, false),\n              );\n            } else {\n              removed[handler.type]!\n                  .addAll(collectMatchingNodes(n, handler.type));\n            }\n          }\n        }\n      } else if (op is UpdateOperation) {\n        final node = widget.editorState.getNodeAtPath(op.path);\n        if (node == null) {\n          continue;\n        }\n\n        if (op.attributes[blockComponentDelta] is! List ||\n            op.oldAttributes[blockComponentDelta] is! List) {\n          continue;\n        }\n\n        final deltaBefore =\n            Delta.fromJson(op.oldAttributes[blockComponentDelta]);\n        final deltaAfter = Delta.fromJson(op.attributes[blockComponentDelta]);\n\n        final (add, del) = diffDeltas(deltaBefore, deltaAfter);\n\n        bool fetchedMentions = false;\n        for (final handler in _transactionHandlers) {\n          if (!handler.livesInDelta || fetchedMentions) {\n            continue;\n          }\n\n          if (add.isNotEmpty) {\n            final mentionBlockDatas =\n                getMentionBlockData(handler.type, node, add);\n\n            added[handler.type]!.addAll(mentionBlockDatas);\n          }\n\n          if (del.isNotEmpty) {\n            final mentionBlockDatas = getMentionBlockData(\n              handler.type,\n              node,\n              del,\n            );\n\n            removed[handler.type]!.addAll(mentionBlockDatas);\n          }\n\n          fetchedMentions = true;\n        }\n      }\n    }\n\n    for (final handler in _transactionHandlers) {\n      final additions = added[handler.type] ?? [];\n      final removals = removed[handler.type] ?? [];\n\n      if (additions.isEmpty && removals.isEmpty) {\n        continue;\n      }\n\n      handler.onTransaction(\n        context,\n        widget.viewId,\n        widget.editorState,\n        additions,\n        removals,\n        isCut: context.read<ClipboardState>().isCut,\n        isUndoRedo: isUndoRedo,\n        isPaste: isPaste,\n        isDraggingNode: isDraggingNode,\n        isTurnInto: isTurnInto,\n        parentViewId: widget.viewId,\n      );\n    }\n\n    isUndoRedo = false;\n    isPaste = false;\n    isTurnInto = false;\n  }\n\n  /// Takes an iterable of [TextInsert] and returns a list of [MentionBlockData].\n  /// This is used to extract mentions from a list of text inserts, of a certain type.\n  List<MentionBlockData> getMentionBlockData(\n    String type,\n    Node node,\n    Iterable<TextInsert> textInserts,\n  ) {\n    // Additions contain all the text inserts that were added in this\n    // transaction, we only care about the ones that fit the handlers type.\n\n    // Filter out the text inserts where the attribute for the handler type is present.\n    final relevantTextInserts =\n        textInserts.where((ti) => ti.attributes?[type] != null);\n\n    // Map it to a list of MentionBlockData.\n    final mentionBlockDatas = relevantTextInserts.map<MentionBlockData>((ti) {\n      // For some text inserts (mostly additions), we might need to modify them after the transaction,\n      // so we pass the index of the delta to the handler.\n      final index = node.delta?.toList().indexOf(ti) ?? -1;\n      return (node, ti.attributes![type], index);\n    }).toList();\n\n    return mentionBlockDatas;\n  }\n\n  List<MentionBlockData> extractMentionsForType(\n    Node node,\n    String mentionType, [\n    bool includeIndex = true,\n  ]) {\n    final changes = <MentionBlockData>[];\n\n    final nodesWithDelta = collectMatchingNodes(\n      node,\n      mentionType,\n      livesInDelta: true,\n    );\n\n    for (final paragraphNode in nodesWithDelta) {\n      final textInserts = paragraphNode.attributes[blockComponentDelta];\n      if (textInserts == null || textInserts is! List || textInserts.isEmpty) {\n        continue;\n      }\n\n      for (final (index, textInsert) in textInserts.indexed) {\n        if (textInsert['attributes'] != null &&\n            textInsert['attributes'][mentionType] != null) {\n          changes.add(\n            (\n              paragraphNode,\n              textInsert['attributes'][mentionType],\n              includeIndex ? index : -1,\n            ),\n          );\n        }\n      }\n    }\n\n    return changes;\n  }\n\n  (Iterable<TextInsert>, Iterable<TextInsert>) diffDeltas(\n    Delta before,\n    Delta after,\n  ) {\n    final diff = before.diff(after);\n    final inverted = diff.invert(before);\n    final del = inverted.whereType<TextInsert>();\n    final add = diff.whereType<TextInsert>();\n\n    return (add, del);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/mention_transaction_handler.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_handler.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\n/// The data used to handle transactions for mentions.\n///\n/// [Node] is the block node.\n/// [Map] is the data of the mention block.\n/// [int] is the index of the mention block in the list of deltas (after transaction apply).\n///\ntypedef MentionBlockData = (Node, Map<String, dynamic>, int);\n\nabstract class MentionTransactionHandler\n    extends EditorTransactionHandler<MentionBlockData> {\n  const MentionTransactionHandler()\n      : super(type: MentionBlockKeys.mention, livesInDelta: true);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_notification.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\n/// Undo\n///\n/// - support\n///   - desktop\n///   - web\n///\nfinal CommandShortcutEvent customUndoCommand = CommandShortcutEvent(\n  key: 'undo',\n  getDescription: () => AppFlowyEditorL10n.current.cmdUndo,\n  command: 'ctrl+z',\n  macOSCommand: 'cmd+z',\n  handler: (editorState) {\n    final context = editorState.document.root.context;\n    if (context == null) {\n      return KeyEventResult.ignored;\n    }\n    final editorContext = context.read<SharedEditorContext>();\n    if (editorContext.coverTitleFocusNode.hasFocus) {\n      return KeyEventResult.ignored;\n    }\n\n    EditorNotification.undo().post();\n    return KeyEventResult.handled;\n  },\n);\n\n/// Redo\n///\n/// - support\n///   - desktop\n///   - web\n///\nfinal CommandShortcutEvent customRedoCommand = CommandShortcutEvent(\n  key: 'redo',\n  getDescription: () => AppFlowyEditorL10n.current.cmdRedo,\n  command: 'ctrl+y,ctrl+shift+z',\n  macOSCommand: 'cmd+shift+z',\n  handler: (editorState) {\n    final context = editorState.document.root.context;\n    if (context == null) {\n      return KeyEventResult.ignored;\n    }\n    final editorContext = context.read<SharedEditorContext>();\n    if (editorContext.coverTitleFocusNode.hasFocus) {\n      return KeyEventResult.ignored;\n    }\n\n    EditorNotification.redo().post();\n    return KeyEventResult.handled;\n  },\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/video/video_block_component.dart",
    "content": "class VideoBlockKeys {\n  const VideoBlockKeys._();\n\n  static const String type = 'video';\n  static const String url = 'url';\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/font_colors.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';\nimport 'package:appflowy/shared/google_fonts_extension.dart';\nimport 'package:appflowy/util/font_family_extension.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/appearance_defaults.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:google_fonts/google_fonts.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'editor_plugins/desktop_toolbar/link/link_hover_menu.dart';\nimport 'editor_plugins/toolbar_item/more_option_toolbar_item.dart';\n\nclass EditorStyleCustomizer {\n  EditorStyleCustomizer({\n    required this.context,\n    required this.padding,\n    this.width,\n    this.editorState,\n  });\n\n  final BuildContext context;\n  final EdgeInsets padding;\n  final double? width;\n  final EditorState? editorState;\n\n  static const double maxDocumentWidth = 480 * 4;\n  static const double minDocumentWidth = 480;\n\n  static EdgeInsets get documentPadding => UniversalPlatform.isMobile\n      ? EdgeInsets.zero\n      : EdgeInsets.only(\n          left: 40,\n          right: 40 + EditorStyleCustomizer.optionMenuWidth,\n        );\n\n  static double get nodeHorizontalPadding =>\n      UniversalPlatform.isMobile ? 24 : 0;\n\n  static EdgeInsets get documentPaddingWithOptionMenu =>\n      documentPadding + EdgeInsets.only(left: optionMenuWidth);\n\n  static double get optionMenuWidth => UniversalPlatform.isMobile ? 0 : 44;\n\n  static Color? toolbarHoverColor(BuildContext context) {\n    return Theme.of(context).brightness == Brightness.dark\n        ? Theme.of(context).colorScheme.secondary\n        : AFThemeExtension.of(context).toolbarHoverColor;\n  }\n\n  EditorStyle style() {\n    if (UniversalPlatform.isDesktopOrWeb) {\n      return desktop();\n    } else if (UniversalPlatform.isMobile) {\n      return mobile();\n    }\n    throw UnimplementedError();\n  }\n\n  EditorStyle desktop() {\n    final theme = Theme.of(context);\n    final afThemeExtension = AFThemeExtension.of(context);\n    final appearanceFont = context.read<AppearanceSettingsCubit>().state.font;\n    final appearance = context.read<DocumentAppearanceCubit>().state;\n    final fontSize = appearance.fontSize;\n    String fontFamily = appearance.fontFamily;\n    if (fontFamily.isEmpty && appearanceFont.isNotEmpty) {\n      fontFamily = appearanceFont;\n    }\n\n    final cursorColor = (editorState?.editable ?? true)\n        ? (appearance.cursorColor ??\n            DefaultAppearanceSettings.getDefaultCursorColor(context))\n        : Colors.transparent;\n\n    return EditorStyle.desktop(\n      padding: padding,\n      maxWidth: width,\n      cursorColor: cursorColor,\n      selectionColor: appearance.selectionColor ??\n          DefaultAppearanceSettings.getDefaultSelectionColor(context),\n      defaultTextDirection: appearance.defaultTextDirection,\n      textStyleConfiguration: TextStyleConfiguration(\n        lineHeight: 1.4,\n        // on Windows, if applyHeightToFirstAscent is true, the first line will be too high.\n        // it will cause the first line not aligned with the prefix icon.\n        applyHeightToFirstAscent: UniversalPlatform.isWindows ? false : true,\n        applyHeightToLastDescent: true,\n        text: baseTextStyle(fontFamily).copyWith(\n          fontSize: fontSize,\n          color: afThemeExtension.onBackground,\n        ),\n        bold: baseTextStyle(fontFamily, fontWeight: FontWeight.bold).copyWith(\n          fontWeight: FontWeight.w600,\n        ),\n        italic: baseTextStyle(fontFamily).copyWith(fontStyle: FontStyle.italic),\n        underline: baseTextStyle(fontFamily).copyWith(\n          decoration: TextDecoration.underline,\n        ),\n        strikethrough: baseTextStyle(fontFamily).copyWith(\n          decoration: TextDecoration.lineThrough,\n        ),\n        href: baseTextStyle(fontFamily).copyWith(\n          color: theme.colorScheme.primary,\n          decoration: TextDecoration.underline,\n        ),\n        code: GoogleFonts.robotoMono(\n          textStyle: baseTextStyle(fontFamily).copyWith(\n            fontSize: fontSize,\n            fontWeight: FontWeight.normal,\n            color: Colors.red,\n            backgroundColor:\n                theme.colorScheme.inverseSurface.withValues(alpha: 0.8),\n          ),\n        ),\n      ),\n      textSpanDecorator: customizeAttributeDecorator,\n      textScaleFactor:\n          context.watch<AppearanceSettingsCubit>().state.textScaleFactor,\n      textSpanOverlayBuilder: _buildTextSpanOverlay,\n    );\n  }\n\n  EditorStyle mobile() {\n    final afThemeExtension = AFThemeExtension.of(context);\n    final pageStyle = context.read<DocumentPageStyleBloc>().state;\n    final theme = Theme.of(context);\n    final fontSize = pageStyle.fontLayout.fontSize;\n    final lineHeight = pageStyle.lineHeightLayout.lineHeight;\n    final fontFamily = pageStyle.fontFamily ??\n        context.read<AppearanceSettingsCubit>().state.font;\n    final defaultTextDirection =\n        context.read<DocumentAppearanceCubit>().state.defaultTextDirection;\n    final textScaleFactor =\n        context.read<AppearanceSettingsCubit>().state.textScaleFactor;\n    final baseTextStyle = this.baseTextStyle(fontFamily);\n\n    return EditorStyle.mobile(\n      padding: padding,\n      defaultTextDirection: defaultTextDirection,\n      textStyleConfiguration: TextStyleConfiguration(\n        lineHeight: lineHeight,\n        text: baseTextStyle.copyWith(\n          fontSize: fontSize,\n          color: afThemeExtension.onBackground,\n        ),\n        bold: baseTextStyle.copyWith(fontWeight: FontWeight.w600),\n        italic: baseTextStyle.copyWith(fontStyle: FontStyle.italic),\n        underline: baseTextStyle.copyWith(decoration: TextDecoration.underline),\n        strikethrough: baseTextStyle.copyWith(\n          decoration: TextDecoration.lineThrough,\n        ),\n        href: baseTextStyle.copyWith(\n          color: theme.colorScheme.primary,\n          decoration: TextDecoration.underline,\n        ),\n        code: GoogleFonts.robotoMono(\n          textStyle: baseTextStyle.copyWith(\n            fontSize: fontSize,\n            fontWeight: FontWeight.normal,\n            color: Colors.red,\n            backgroundColor: Colors.grey.withValues(alpha: 0.3),\n          ),\n        ),\n        applyHeightToFirstAscent: true,\n        applyHeightToLastDescent: true,\n      ),\n      textSpanDecorator: customizeAttributeDecorator,\n      magnifierSize: const Size(144, 96),\n      textScaleFactor: textScaleFactor,\n      mobileDragHandleLeftExtend: 12.0,\n      mobileDragHandleWidthExtend: 24.0,\n      textSpanOverlayBuilder: _buildTextSpanOverlay,\n    );\n  }\n\n  TextStyle headingStyleBuilder(int level) {\n    final String? fontFamily;\n    final List<double> fontSizes;\n    final double fontSize;\n    if (UniversalPlatform.isMobile) {\n      final state = context.read<DocumentPageStyleBloc>().state;\n      fontFamily = state.fontFamily;\n      fontSize = state.fontLayout.fontSize;\n      fontSizes = state.fontLayout.headingFontSizes;\n    } else {\n      fontFamily = context\n          .read<DocumentAppearanceCubit>()\n          .state\n          .fontFamily\n          .orDefault(context.read<AppearanceSettingsCubit>().state.font);\n      fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;\n      fontSizes = [\n        fontSize + 16,\n        fontSize + 12,\n        fontSize + 8,\n        fontSize + 4,\n        fontSize + 2,\n        fontSize,\n      ];\n    }\n    return baseTextStyle(fontFamily, fontWeight: FontWeight.w600).copyWith(\n      fontSize: fontSizes.elementAtOrNull(level - 1) ?? fontSize,\n    );\n  }\n\n  CodeBlockStyle codeBlockStyleBuilder() {\n    final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;\n    final fontFamily =\n        context.read<DocumentAppearanceCubit>().state.codeFontFamily;\n\n    return CodeBlockStyle(\n      textStyle: baseTextStyle(fontFamily).copyWith(\n        fontSize: fontSize,\n        height: 1.5,\n        color: AFThemeExtension.of(context).onBackground,\n      ),\n      backgroundColor: AFThemeExtension.of(context).calloutBGColor,\n      foregroundColor: AFThemeExtension.of(context).textColor.withAlpha(155),\n    );\n  }\n\n  TextStyle calloutBlockStyleBuilder() {\n    if (UniversalPlatform.isMobile) {\n      final afThemeExtension = AFThemeExtension.of(context);\n      final pageStyle = context.read<DocumentPageStyleBloc>().state;\n      final fontSize = pageStyle.fontLayout.fontSize;\n      final fontFamily = pageStyle.fontFamily ?? defaultFontFamily;\n      final baseTextStyle = this.baseTextStyle(fontFamily);\n      return baseTextStyle.copyWith(\n        fontSize: fontSize,\n        color: afThemeExtension.onBackground,\n      );\n    } else {\n      final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;\n      return baseTextStyle(null).copyWith(\n        fontSize: fontSize,\n        height: 1.5,\n      );\n    }\n  }\n\n  TextStyle outlineBlockPlaceholderStyleBuilder() {\n    final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;\n    return TextStyle(\n      fontFamily: defaultFontFamily,\n      fontSize: fontSize,\n      height: 1.5,\n      color: AFThemeExtension.of(context).onBackground.withValues(alpha: 0.6),\n    );\n  }\n\n  TextStyle subPageBlockTextStyleBuilder() {\n    if (UniversalPlatform.isMobile) {\n      final pageStyle = context.read<DocumentPageStyleBloc>().state;\n      final fontSize = pageStyle.fontLayout.fontSize;\n      final fontFamily = pageStyle.fontFamily ?? defaultFontFamily;\n      final baseTextStyle = this.baseTextStyle(fontFamily);\n      return baseTextStyle.copyWith(\n        fontSize: fontSize,\n      );\n    } else {\n      final fontSize = context.read<DocumentAppearanceCubit>().state.fontSize;\n      return baseTextStyle(null).copyWith(\n        fontSize: fontSize,\n        height: 1.5,\n      );\n    }\n  }\n\n  SelectionMenuStyle selectionMenuStyleBuilder() {\n    final theme = Theme.of(context);\n    final afThemeExtension = AFThemeExtension.of(context);\n    return SelectionMenuStyle(\n      selectionMenuBackgroundColor: theme.cardColor,\n      selectionMenuItemTextColor: afThemeExtension.onBackground,\n      selectionMenuItemIconColor: afThemeExtension.onBackground,\n      selectionMenuItemSelectedIconColor: theme.colorScheme.onSurface,\n      selectionMenuItemSelectedTextColor: theme.colorScheme.onSurface,\n      selectionMenuItemSelectedColor: afThemeExtension.greyHover,\n      selectionMenuUnselectedLabelColor: afThemeExtension.onBackground,\n      selectionMenuDividerColor: afThemeExtension.greyHover,\n      selectionMenuLinkBorderColor: afThemeExtension.greyHover,\n      selectionMenuInvalidLinkColor: afThemeExtension.onBackground,\n      selectionMenuButtonColor: afThemeExtension.greyHover,\n      selectionMenuButtonTextColor: afThemeExtension.onBackground,\n      selectionMenuButtonIconColor: afThemeExtension.onBackground,\n      selectionMenuButtonBorderColor: afThemeExtension.greyHover,\n      selectionMenuTabIndicatorColor: afThemeExtension.greyHover,\n    );\n  }\n\n  InlineActionsMenuStyle inlineActionsMenuStyleBuilder() {\n    final theme = Theme.of(context);\n    final afThemeExtension = AFThemeExtension.of(context);\n    return InlineActionsMenuStyle(\n      backgroundColor: theme.cardColor,\n      groupTextColor: afThemeExtension.onBackground.withValues(alpha: .8),\n      menuItemTextColor: afThemeExtension.onBackground,\n      menuItemSelectedColor: theme.colorScheme.secondary,\n      menuItemSelectedTextColor: theme.colorScheme.onSurface,\n    );\n  }\n\n  TextStyle baseTextStyle(String? fontFamily, {FontWeight? fontWeight}) {\n    if (fontFamily == null) {\n      return TextStyle(fontWeight: fontWeight);\n    } else if (fontFamily == defaultFontFamily) {\n      return TextStyle(fontFamily: fontFamily, fontWeight: fontWeight);\n    }\n\n    try {\n      return getGoogleFontSafely(fontFamily, fontWeight: fontWeight);\n    } on Exception {\n      if ([defaultFontFamily, builtInCodeFontFamily].contains(fontFamily)) {\n        return TextStyle(fontFamily: fontFamily, fontWeight: fontWeight);\n      }\n\n      return TextStyle(fontWeight: fontWeight);\n    }\n  }\n\n  InlineSpan customizeAttributeDecorator(\n    BuildContext context,\n    Node node,\n    int index,\n    TextInsert text,\n    TextSpan before,\n    TextSpan after,\n  ) {\n    final attributes = text.attributes;\n    if (attributes == null) {\n      return before;\n    }\n\n    final suggestion = attributes[AiWriterBlockKeys.suggestion] as String?;\n    final newStyle = suggestion == null\n        ? after.style\n        : _styleSuggestion(after.style, suggestion);\n\n    if (attributes.backgroundColor != null) {\n      final color = EditorFontColors.fromBuiltInColors(\n        context,\n        attributes.backgroundColor!,\n      );\n      if (color != null) {\n        return TextSpan(\n          text: before.text,\n          style: newStyle?.merge(\n            TextStyle(backgroundColor: color),\n          ),\n        );\n      }\n    }\n\n    // try to refresh font here.\n    if (attributes.fontFamily != null) {\n      try {\n        if (before.text?.contains('_regular') == true) {\n          getGoogleFontSafely(attributes.fontFamily!.parseFontFamilyName());\n        } else {\n          return TextSpan(\n            text: before.text,\n            style: newStyle?.merge(\n              getGoogleFontSafely(attributes.fontFamily!),\n            ),\n          );\n        }\n      } catch (_) {\n        // ignore\n      }\n    }\n\n    // Inline Mentions (Page Reference, Date, Reminder, etc.)\n    final mention =\n        attributes[MentionBlockKeys.mention] as Map<String, dynamic>?;\n    if (mention != null) {\n      final type = mention[MentionBlockKeys.type];\n      return WidgetSpan(\n        alignment: PlaceholderAlignment.middle,\n        style: newStyle,\n        child: MentionBlock(\n          key: ValueKey(\n            switch (type) {\n              MentionType.page => mention[MentionBlockKeys.pageId],\n              MentionType.date => mention[MentionBlockKeys.date],\n              _ => MentionBlockKeys.mention,\n            },\n          ),\n          node: node,\n          index: index,\n          mention: mention,\n          textStyle: newStyle,\n        ),\n      );\n    }\n\n    // customize the inline math equation block\n    final formula = attributes[InlineMathEquationKeys.formula];\n    if (formula is String) {\n      return WidgetSpan(\n        style: after.style,\n        alignment: PlaceholderAlignment.middle,\n        child: InlineMathEquation(\n          node: node,\n          index: index,\n          formula: formula,\n          textStyle: after.style ?? style().textStyleConfiguration.text,\n        ),\n      );\n    }\n\n    // customize the link on mobile\n    final href = attributes[AppFlowyRichTextKeys.href] as String?;\n    if (UniversalPlatform.isMobile && href != null) {\n      return TextSpan(style: before.style, text: text.text);\n    }\n\n    if (suggestion != null) {\n      return TextSpan(\n        text: before.text,\n        style: newStyle,\n      );\n    }\n\n    if (href != null) {\n      return TextSpan(\n        style: before.style,\n        text: text.text,\n        mouseCursor: SystemMouseCursors.click,\n      );\n    } else {\n      return before;\n    }\n  }\n\n  Widget buildToolbarItemTooltip(\n    BuildContext context,\n    String id,\n    String message,\n    Widget child,\n  ) {\n    final tooltipMessage = _buildTooltipMessage(id, message);\n    child = FlowyTooltip(\n      richMessage: tooltipMessage,\n      preferBelow: false,\n      verticalOffset: 24,\n      child: child,\n    );\n\n    // the align/font toolbar item doesn't need the hover effect\n    final toolbarItemsWithoutHover = {\n      kFontToolbarItemId,\n      kAlignToolbarItemId,\n    };\n\n    if (!toolbarItemsWithoutHover.contains(id)) {\n      child = Padding(\n        padding: const EdgeInsets.symmetric(vertical: 6),\n        child: FlowyHover(\n          style: HoverStyle(\n            hoverColor: Colors.grey.withValues(alpha: 0.3),\n          ),\n          child: child,\n        ),\n      );\n    }\n\n    return child;\n  }\n\n  TextSpan _buildTooltipMessage(String id, String message) {\n    final markdownItemTooltips = {\n      'underline': (LocaleKeys.toolbar_underline.tr(), 'U'),\n      'bold': (LocaleKeys.toolbar_bold.tr(), 'B'),\n      'italic': (LocaleKeys.toolbar_italic.tr(), 'I'),\n      'strikethrough': (LocaleKeys.toolbar_strike.tr(), 'Shift+S'),\n      'code': (LocaleKeys.toolbar_inlineCode.tr(), 'E'),\n      'editor.inline_math_equation': (\n        LocaleKeys.document_plugins_createInlineMathEquation.tr(),\n        'Shift+E'\n      ),\n    };\n\n    final markdownItemIds = markdownItemTooltips.keys.toSet();\n    // the items without shortcuts\n    if (!markdownItemIds.contains(id)) {\n      return TextSpan(\n        text: message,\n        style: context.tooltipTextStyle(),\n      );\n    }\n\n    final tooltip = markdownItemTooltips[id];\n    if (tooltip == null) {\n      return TextSpan(\n        text: message,\n        style: context.tooltipTextStyle(),\n      );\n    }\n\n    final textSpan = TextSpan(\n      children: [\n        TextSpan(\n          text: '${tooltip.$1}\\n',\n          style: context.tooltipTextStyle(),\n        ),\n        TextSpan(\n          text: (Platform.isMacOS ? '⌘+' : 'Ctrl+') + tooltip.$2,\n          style: context.tooltipTextStyle()?.copyWith(\n                color: Theme.of(context).hintColor,\n              ),\n        ),\n      ],\n    );\n\n    return textSpan;\n  }\n\n  TextStyle? _styleSuggestion(TextStyle? style, String suggestion) {\n    if (style == null) {\n      return null;\n    }\n    final isLight = Theme.of(context).isLightMode;\n    final textColor = isLight ? Color(0xFF007296) : Color(0xFF49CFF4);\n    final underlineColor = isLight ? Color(0x33005A7A) : Color(0x3349CFF4);\n    return switch (suggestion) {\n      AiWriterBlockKeys.suggestionOriginal => style.copyWith(\n          color: Theme.of(context).disabledColor,\n          decoration: TextDecoration.lineThrough,\n        ),\n      AiWriterBlockKeys.suggestionReplacement => style.copyWith(\n          color: textColor,\n          decoration: TextDecoration.underline,\n          decorationColor: underlineColor,\n          decorationThickness: 1.0,\n        ),\n      _ => style,\n    };\n  }\n\n  List<Widget> _buildTextSpanOverlay(\n    BuildContext context,\n    Node node,\n    SelectableMixin delegate,\n  ) {\n    final delta = node.delta;\n    if (delta == null) return [];\n    final widgets = <Widget>[];\n    final textInserts = delta.whereType<TextInsert>();\n    int index = 0;\n    final editorState = context.read<EditorState>();\n    for (final textInsert in textInserts) {\n      if (textInsert.attributes?.href != null) {\n        final nodeSelection = Selection(\n          start: Position(path: node.path, offset: index),\n          end: Position(\n            path: node.path,\n            offset: index + textInsert.length,\n          ),\n        );\n        final rectList = delegate.getRectsInSelection(nodeSelection);\n        if (rectList.isNotEmpty) {\n          for (final rect in rectList) {\n            widgets.add(\n              Positioned(\n                left: rect.left,\n                top: rect.top,\n                child: SizedBox(\n                  width: rect.width,\n                  height: rect.height,\n                  child: LinkHoverTrigger(\n                    editorState: editorState,\n                    selection: nodeSelection,\n                    attribute: textInsert.attributes!,\n                    node: node,\n                    size: rect.size,\n                  ),\n                ),\n              ),\n            );\n          }\n        }\n      }\n      index += textInsert.length;\n    }\n    return widgets;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'emoji_menu.dart';\n\nconst _emojiCharacter = ':';\nfinal _letterRegExp = RegExp(r'^[a-zA-Z]$');\n\nCharacterShortcutEvent emojiCommand(BuildContext context) =>\n    CharacterShortcutEvent(\n      key: 'Opens Emoji Menu',\n      character: '',\n      regExp: _letterRegExp,\n      handler: (editorState) async {\n        return false;\n      },\n      handlerWithCharacter: (editorState, character) {\n        emojiMenuService = EmojiMenu(\n          overlay: Overlay.of(context),\n          editorState: editorState,\n        );\n        return emojiCommandHandler(editorState, context, character);\n      },\n    );\n\nEmojiMenuService? emojiMenuService;\n\nFuture<bool> emojiCommandHandler(\n  EditorState editorState,\n  BuildContext context,\n  String character,\n) async {\n  final selection = editorState.selection;\n\n  if (UniversalPlatform.isMobile || selection == null) {\n    return false;\n  }\n\n  final node = editorState.getNodeAtPath(selection.end.path);\n  final delta = node?.delta;\n  if (node == null || delta == null || node.type == CodeBlockKeys.type) {\n    return false;\n  }\n\n  if (selection.end.offset > 0) {\n    final plain = delta.toPlainText();\n\n    final previousCharacter = plain[selection.end.offset - 1];\n    if (previousCharacter != _emojiCharacter) return false;\n    if (!context.mounted) return false;\n\n    if (!selection.isCollapsed) return false;\n\n    await editorState.insertTextAtPosition(\n      character,\n      position: selection.start,\n    );\n\n    emojiMenuService?.show(character);\n    return true;\n  }\n\n  return false;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart",
    "content": "import 'dart:math';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/base/emoji/emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\n\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_emoji_mart/flutter_emoji_mart.dart';\n\nimport 'emoji_menu.dart';\n\nclass EmojiHandler extends StatefulWidget {\n  const EmojiHandler({\n    super.key,\n    required this.editorState,\n    required this.menuService,\n    required this.onDismiss,\n    required this.onSelectionUpdate,\n    required this.onEmojiSelect,\n    this.cancelBySpaceHandler,\n    this.initialSearchText = '',\n  });\n\n  final EditorState editorState;\n  final EmojiMenuService menuService;\n  final VoidCallback onDismiss;\n  final VoidCallback onSelectionUpdate;\n  final SelectEmojiItemHandler onEmojiSelect;\n  final String initialSearchText;\n  final bool Function()? cancelBySpaceHandler;\n\n  @override\n  State<EmojiHandler> createState() => _EmojiHandlerState();\n}\n\nclass _EmojiHandlerState extends State<EmojiHandler> {\n  final focusNode = FocusNode(debugLabel: 'emoji_menu_handler');\n  final scrollController = ScrollController();\n  late EmojiData emojiData;\n  final List<Emoji> searchedEmojis = [];\n  bool loaded = false;\n  int invalidCounter = 0;\n  late int startOffset;\n  late String _search = widget.initialSearchText;\n  double emojiHeight = 36.0;\n  String lastSearchedQuery = '';\n  final configuration = EmojiPickerConfiguration(\n    defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none,\n  );\n\n  int get startCharAmount => widget.initialSearchText.length;\n\n  set search(String search) {\n    _search = search;\n    _doSearch();\n  }\n\n  final ValueNotifier<int> selectedIndexNotifier = ValueNotifier(0);\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addPostFrameCallback(\n      (_) => focusNode.requestFocus(),\n    );\n\n    startOffset =\n        (widget.editorState.selection?.endIndex ?? 0) - startCharAmount;\n\n    if (kCachedEmojiData != null) {\n      loadEmojis(kCachedEmojiData!);\n    } else {\n      EmojiData.builtIn().then(\n        (value) {\n          kCachedEmojiData = value;\n          loadEmojis(value);\n        },\n      );\n    }\n  }\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    selectedIndexNotifier.dispose();\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final noEmojis = searchedEmojis.isEmpty;\n    return Focus(\n      focusNode: focusNode,\n      onKeyEvent: onKeyEvent,\n      child: Container(\n        constraints: const BoxConstraints(maxHeight: 392, maxWidth: 360),\n        padding: noEmojis\n            ? EdgeInsets.zero\n            : const EdgeInsets.symmetric(vertical: 16),\n        decoration: BoxDecoration(\n          borderRadius: BorderRadius.circular(6.0),\n          color: Theme.of(context).cardColor,\n          boxShadow: [\n            BoxShadow(\n              blurRadius: 5,\n              spreadRadius: 1,\n              color: Colors.black.withAlpha(25),\n            ),\n          ],\n        ),\n        child: loaded ? buildEmojis() : buildLoading(),\n      ),\n    );\n  }\n\n  Widget buildLoading() {\n    return SizedBox(\n      width: 400,\n      height: 40,\n      child: Center(\n        child: SizedBox.square(\n          dimension: 20,\n          child: CircularProgressIndicator(),\n        ),\n      ),\n    );\n  }\n\n  Widget buildEmojis() {\n    final noEmojis = searchedEmojis.isEmpty;\n    if (noEmojis) {\n      return SizedBox(\n        width: 400,\n        height: emojiHeight,\n        child: Center(\n          child: FlowyText.regular(LocaleKeys.inlineActions_noResults.tr()),\n        ),\n      );\n    }\n    return SizedBox(\n      height:\n          (searchedEmojis.length / configuration.perLine).ceil() * emojiHeight,\n      child: GridView.builder(\n        controller: scrollController,\n        itemCount: searchedEmojis.length,\n        padding: const EdgeInsets.symmetric(horizontal: 16),\n        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\n          crossAxisCount: configuration.perLine,\n        ),\n        itemBuilder: (context, index) {\n          final currentEmoji = searchedEmojis[index];\n          final emojiId = currentEmoji.id;\n          final emoji = emojiData.getEmojiById(\n            emojiId,\n            skinTone: configuration.defaultSkinTone,\n          );\n          return ValueListenableBuilder(\n            valueListenable: selectedIndexNotifier,\n            builder: (context, value, child) {\n              final isSelected = value == index;\n              return SizedBox.square(\n                dimension: emojiHeight,\n                child: FlowyButton(\n                  isSelected: isSelected,\n                  margin: EdgeInsets.zero,\n                  radius: Corners.s8Border,\n                  text: ManualTooltip(\n                    key: ValueKey('$emojiId-$isSelected'),\n                    message: currentEmoji.name,\n                    showAutomaticlly: isSelected,\n                    preferBelow: false,\n                    child: FlowyText.emoji(\n                      emoji,\n                      fontSize: configuration.emojiSize,\n                    ),\n                  ),\n                  onTap: () => onSelect(index),\n                ),\n              );\n            },\n          );\n        },\n      ),\n    );\n  }\n\n  void changeSelectedIndex(int index) => selectedIndexNotifier.value = index;\n\n  void loadEmojis(EmojiData data) {\n    emojiData = data;\n    searchedEmojis.clear();\n    searchedEmojis.addAll(emojiData.emojis.values);\n    if (mounted) {\n      setState(() {\n        loaded = true;\n      });\n    }\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      _doSearch();\n    });\n  }\n\n  void _doSearch() {\n    if (!loaded || !mounted) return;\n    final enableEmptySearch = widget.initialSearchText.isEmpty;\n    if ((_search.startsWith(' ') || _search.isEmpty) && !enableEmptySearch) {\n      widget.onDismiss.call();\n      return;\n    }\n    final searchEmojiData = emojiData.filterByKeyword(_search);\n    setState(() {\n      searchedEmojis.clear();\n      searchedEmojis.addAll(searchEmojiData.emojis.values);\n      changeSelectedIndex(0);\n      _scrollToItem();\n    });\n    if (searchedEmojis.isEmpty) {\n      if (lastSearchedQuery.isEmpty) {\n        lastSearchedQuery = _search;\n      }\n      if (_search.length - lastSearchedQuery.length >= 5) {\n        widget.onDismiss.call();\n      }\n    } else {\n      lastSearchedQuery = '';\n    }\n  }\n\n  KeyEventResult onKeyEvent(focus, KeyEvent event) {\n    if (event is! KeyDownEvent && event is! KeyRepeatEvent) {\n      return KeyEventResult.ignored;\n    }\n\n    const moveKeys = [\n      LogicalKeyboardKey.arrowUp,\n      LogicalKeyboardKey.arrowDown,\n      LogicalKeyboardKey.arrowLeft,\n      LogicalKeyboardKey.arrowRight,\n    ];\n\n    if (event.logicalKey == LogicalKeyboardKey.enter) {\n      onSelect(selectedIndexNotifier.value);\n      return KeyEventResult.handled;\n    } else if (event.logicalKey == LogicalKeyboardKey.escape) {\n      // Workaround to bring focus back to editor\n      widget.editorState\n          .updateSelectionWithReason(widget.editorState.selection);\n      widget.onDismiss.call();\n    } else if (event.logicalKey == LogicalKeyboardKey.backspace) {\n      if (_search.isEmpty) {\n        if (widget.initialSearchText.isEmpty) {\n          widget.onDismiss.call();\n          return KeyEventResult.handled;\n        }\n        if (_canDeleteLastCharacter()) {\n          widget.editorState.deleteBackward();\n        } else {\n          // Workaround for editor regaining focus\n          widget.editorState.apply(\n            widget.editorState.transaction\n              ..afterSelection = widget.editorState.selection,\n          );\n        }\n        widget.onDismiss.call();\n      } else {\n        widget.onSelectionUpdate();\n        widget.editorState.deleteBackward();\n        _deleteCharacterAtSelection();\n      }\n\n      return KeyEventResult.handled;\n    } else if (event.character != null &&\n        !moveKeys.contains(event.logicalKey)) {\n      /// Prevents dismissal of context menu by notifying the parent\n      /// that the selection change occurred from the handler.\n      widget.onSelectionUpdate();\n\n      if (event.logicalKey == LogicalKeyboardKey.space) {\n        final cancelBySpaceHandler = widget.cancelBySpaceHandler;\n        if (cancelBySpaceHandler != null && cancelBySpaceHandler()) {\n          return KeyEventResult.handled;\n        }\n      }\n\n      // Interpolation to avoid having a getter for private variable\n      _insertCharacter(event.character!);\n      return KeyEventResult.handled;\n    } else if (moveKeys.contains(event.logicalKey)) {\n      _moveSelection(event.logicalKey);\n      return KeyEventResult.handled;\n    }\n\n    return KeyEventResult.handled;\n  }\n\n  void onSelect(int index) {\n    widget.onEmojiSelect.call(\n      context,\n      (startOffset - startCharAmount, startOffset + _search.length),\n      emojiData.getEmojiById(searchedEmojis[index].id),\n    );\n    widget.onDismiss.call();\n  }\n\n  void _insertCharacter(String character) {\n    widget.editorState.insertTextAtCurrentSelection(character);\n\n    final selection = widget.editorState.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n\n    final delta = widget.editorState.getNodeAtPath(selection.end.path)?.delta;\n    if (delta == null) {\n      return;\n    }\n\n    search = widget.editorState\n        .getTextInSelection(\n          selection.copyWith(\n            start: selection.start.copyWith(offset: startOffset),\n            end: selection.start\n                .copyWith(offset: startOffset + _search.length + 1),\n          ),\n        )\n        .join();\n  }\n\n  void _moveSelection(LogicalKeyboardKey key) {\n    final index = selectedIndexNotifier.value,\n        perLine = configuration.perLine,\n        remainder = index % perLine,\n        length = searchedEmojis.length,\n        currentLine = index ~/ perLine,\n        maxLine = (length / perLine).ceil();\n\n    final heightBefore = currentLine * emojiHeight;\n    if (key == LogicalKeyboardKey.arrowUp) {\n      if (currentLine == 0) {\n        final exceptLine = max(0, maxLine - 1);\n        changeSelectedIndex(min(exceptLine * perLine + remainder, length - 1));\n      } else if (currentLine > 0) {\n        changeSelectedIndex(index - perLine);\n      }\n    } else if (key == LogicalKeyboardKey.arrowDown) {\n      if (currentLine == maxLine - 1) {\n        changeSelectedIndex(remainder);\n      } else if (currentLine < maxLine - 1) {\n        changeSelectedIndex(min(index + perLine, length - 1));\n      }\n    } else if (key == LogicalKeyboardKey.arrowLeft) {\n      if (index == 0) {\n        changeSelectedIndex(length - 1);\n      } else if (index > 0) {\n        changeSelectedIndex(index - 1);\n      }\n    } else if (key == LogicalKeyboardKey.arrowRight) {\n      if (index == length - 1) {\n        changeSelectedIndex(0);\n      } else if (index < length - 1) {\n        changeSelectedIndex(index + 1);\n      }\n    }\n    final heightAfter =\n        (selectedIndexNotifier.value ~/ configuration.perLine) * emojiHeight;\n\n    if (mounted && (heightAfter != heightBefore)) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        _scrollToItem();\n      });\n    }\n  }\n\n  void _scrollToItem() {\n    final noEmojis = searchedEmojis.isEmpty;\n    if (noEmojis || !mounted || !scrollController.hasClients) return;\n    final currentItem = selectedIndexNotifier.value;\n    final exceptHeight = (currentItem ~/ configuration.perLine) * emojiHeight;\n    final maxExtent = scrollController.position.maxScrollExtent;\n    final jumpTo = (exceptHeight - maxExtent > 10 * emojiHeight)\n        ? exceptHeight\n        : min(exceptHeight, maxExtent);\n    scrollController.animateTo(\n      jumpTo,\n      duration: Duration(milliseconds: 300),\n      curve: Curves.linear,\n    );\n  }\n\n  void _deleteCharacterAtSelection() {\n    final selection = widget.editorState.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n\n    final node = widget.editorState.getNodeAtPath(selection.end.path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      return;\n    }\n\n    search = delta.toPlainText().substring(\n          startOffset,\n          startOffset + _search.length - 1,\n        );\n  }\n\n  bool _canDeleteLastCharacter() {\n    final selection = widget.editorState.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return false;\n    }\n\n    final delta = widget.editorState.getNodeAtPath(selection.start.path)?.delta;\n    if (delta == null) {\n      return false;\n    }\n\n    return delta.isNotEmpty;\n  }\n}\n\ntypedef SelectEmojiItemHandler = void Function(\n  BuildContext context,\n  (int start, int end) replacement,\n  String emoji,\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nimport 'emoji_actions_command.dart';\nimport 'emoji_handler.dart';\n\nabstract class EmojiMenuService {\n  void show(String character);\n\n  void dismiss();\n}\n\nclass EmojiMenu extends EmojiMenuService {\n  EmojiMenu({\n    required this.overlay,\n    required this.editorState,\n    this.cancelBySpaceHandler,\n    this.menuHeight = 400,\n    this.menuWidth = 300,\n  });\n\n  final EditorState editorState;\n  final double menuHeight;\n  final double menuWidth;\n  final OverlayState overlay;\n  final bool Function()? cancelBySpaceHandler;\n\n  Offset _offset = Offset.zero;\n  Alignment _alignment = Alignment.topLeft;\n  OverlayEntry? _menuEntry;\n  bool selectionChangedByMenu = false;\n  String initialCharacter = '';\n\n  @override\n  void dismiss() {\n    if (_menuEntry != null) {\n      editorState.service.keyboardService?.enable();\n      editorState.service.scrollService?.enable();\n      keepEditorFocusNotifier.decrease();\n    }\n\n    _menuEntry?.remove();\n    _menuEntry = null;\n\n    // workaround: SelectionService has been released after hot reload.\n    final isSelectionDisposed =\n        editorState.service.selectionServiceKey.currentState == null;\n    if (!isSelectionDisposed) {\n      final selectionService = editorState.service.selectionService;\n      selectionService.currentSelection.removeListener(_onSelectionChange);\n    }\n    emojiMenuService = null;\n  }\n\n  void _onSelectionUpdate() => selectionChangedByMenu = true;\n\n  @override\n  void show(String character) {\n    initialCharacter = character;\n    WidgetsBinding.instance.addPostFrameCallback((_) => _show());\n  }\n\n  void _show() {\n    final selectionService = editorState.service.selectionService;\n    final selectionRects = selectionService.selectionRects;\n    if (selectionRects.isEmpty) {\n      return;\n    }\n\n    final Size editorSize = editorState.renderBox!.size;\n\n    calculateSelectionMenuOffset(selectionRects.first);\n\n    final (left, top, right, bottom) = _getPosition();\n\n    _menuEntry = OverlayEntry(\n      builder: (context) => SizedBox(\n        height: editorSize.height,\n        width: editorSize.width,\n\n        // GestureDetector handles clicks outside of the context menu,\n        // to dismiss the context menu.\n        child: GestureDetector(\n          behavior: HitTestBehavior.opaque,\n          onTap: dismiss,\n          child: Stack(\n            children: [\n              Positioned(\n                top: top,\n                bottom: bottom,\n                left: left,\n                right: right,\n                child: EmojiHandler(\n                  editorState: editorState,\n                  menuService: this,\n                  onDismiss: dismiss,\n                  onSelectionUpdate: _onSelectionUpdate,\n                  cancelBySpaceHandler: cancelBySpaceHandler,\n                  initialSearchText: initialCharacter,\n                  onEmojiSelect: (\n                    BuildContext context,\n                    (int, int) replacement,\n                    String emoji,\n                  ) async {\n                    final selection = editorState.selection;\n\n                    if (selection == null) return;\n                    final node =\n                        editorState.document.nodeAtPath(selection.end.path);\n                    if (node == null) return;\n                    final transaction = editorState.transaction\n                      ..deleteText(\n                        node,\n                        replacement.$1,\n                        replacement.$2 - replacement.$1,\n                      )\n                      ..insertText(\n                        node,\n                        replacement.$1,\n                        emoji,\n                      );\n                    await editorState.apply(transaction);\n                  },\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n\n    overlay.insert(_menuEntry!);\n\n    keepEditorFocusNotifier.increase();\n    editorState.service.keyboardService?.disable(showCursor: true);\n    editorState.service.scrollService?.disable();\n    selectionService.currentSelection.addListener(_onSelectionChange);\n  }\n\n  void _onSelectionChange() {\n    // workaround: SelectionService has been released after hot reload.\n    final isSelectionDisposed =\n        editorState.service.selectionServiceKey.currentState == null;\n    if (!isSelectionDisposed) {\n      final selectionService = editorState.service.selectionService;\n      if (selectionService.currentSelection.value == null) {\n        return;\n      }\n    }\n\n    if (!selectionChangedByMenu) {\n      return dismiss();\n    }\n\n    selectionChangedByMenu = false;\n  }\n\n  (double? left, double? top, double? right, double? bottom) _getPosition() {\n    double? left, top, right, bottom;\n    switch (_alignment) {\n      case Alignment.topLeft:\n        left = _offset.dx;\n        top = _offset.dy;\n        break;\n      case Alignment.bottomLeft:\n        left = _offset.dx;\n        bottom = _offset.dy;\n        break;\n      case Alignment.topRight:\n        right = _offset.dx;\n        top = _offset.dy;\n        break;\n      case Alignment.bottomRight:\n        right = _offset.dx;\n        bottom = _offset.dy;\n        break;\n    }\n\n    return (left, top, right, bottom);\n  }\n\n  void calculateSelectionMenuOffset(Rect rect) {\n    // Workaround: We can customize the padding through the [EditorStyle],\n    // but the coordinates of overlay are not properly converted currently.\n    // Just subtract the padding here as a result.\n    const menuOffset = Offset(0, 10);\n    final editorOffset =\n        editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;\n    final editorHeight = editorState.renderBox!.size.height;\n    final editorWidth = editorState.renderBox!.size.width;\n\n    // show below default\n    _alignment = Alignment.topLeft;\n    final bottomRight = rect.bottomRight;\n    final topRight = rect.topRight;\n    var offset = bottomRight + menuOffset;\n    _offset = Offset(\n      offset.dx,\n      offset.dy,\n    );\n\n    // show above\n    if (offset.dy + menuHeight >= editorOffset.dy + editorHeight) {\n      offset = topRight - menuOffset;\n      _alignment = Alignment.bottomLeft;\n\n      _offset = Offset(\n        offset.dx,\n        editorHeight + editorOffset.dy - offset.dy,\n      );\n    }\n\n    // show on right\n    if (_offset.dx + menuWidth < editorOffset.dx + editorWidth) {\n      _offset = Offset(\n        _offset.dx,\n        _offset.dy,\n      );\n    } else if (offset.dx - editorOffset.dx > menuWidth) {\n      // show on left\n      _alignment = _alignment == Alignment.topLeft\n          ? Alignment.topRight\n          : Alignment.bottomRight;\n\n      _offset = Offset(\n        editorWidth - _offset.dx + editorOffset.dx,\n        _offset.dy,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/child_page.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';\nimport 'package:appflowy/plugins/inline_actions/service_handler.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nclass InlineChildPageService extends InlineActionsDelegate {\n  InlineChildPageService({required this.currentViewId});\n\n  final String currentViewId;\n\n  @override\n  Future<InlineActionsResult> search(String? search) async {\n    final List<InlineActionsMenuItem> results = [];\n    if (search != null && search.isNotEmpty) {\n      results.add(\n        InlineActionsMenuItem(\n          label: LocaleKeys.inlineActions_createPage.tr(args: [search]),\n          iconBuilder: (_) => const FlowySvg(FlowySvgs.add_s),\n          onSelected: (context, editorState, service, replacement) =>\n              _onSelected(context, editorState, service, replacement, search),\n        ),\n      );\n    }\n\n    return InlineActionsResult(results: results);\n  }\n\n  Future<void> _onSelected(\n    BuildContext context,\n    EditorState editorState,\n    InlineActionsMenuService service,\n    (int, int) replacement,\n    String? search,\n  ) async {\n    final selection = editorState.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n\n    final node = editorState.getNodeAtPath(selection.start.path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      return;\n    }\n\n    final view = (await ViewBackendService.createView(\n      layoutType: ViewLayoutPB.Document,\n      parentViewId: currentViewId,\n      name: search!,\n    ))\n        .toNullable();\n\n    if (view == null) {\n      return Log.error('Failed to create view');\n    }\n\n    // preload the page info\n    pageMemorizer[view.id] = view;\n    final transaction = editorState.transaction\n      ..replaceText(\n        node,\n        replacement.$1,\n        replacement.$2,\n        MentionBlockKeys.mentionChar,\n        attributes: MentionBlockKeys.buildMentionPageAttributes(\n          mentionType: MentionType.childPage,\n          pageId: view.id,\n          blockId: null,\n        ),\n      );\n\n    await editorState.apply(transaction);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart",
    "content": "import 'package:appflowy/date/date_service.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';\nimport 'package:appflowy/plugins/inline_actions/service_handler.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nfinal _keywords = [\n  LocaleKeys.inlineActions_date.tr().toLowerCase(),\n];\n\nclass DateReferenceService extends InlineActionsDelegate {\n  DateReferenceService(this.context) {\n    // Initialize locale\n    _locale = context.locale.toLanguageTag();\n\n    // Initializes options\n    _setOptions();\n  }\n\n  final BuildContext context;\n\n  late String _locale;\n  late List<InlineActionsMenuItem> _allOptions;\n\n  List<InlineActionsMenuItem> options = [];\n\n  @override\n  Future<InlineActionsResult> search([\n    String? search,\n  ]) async {\n    // Checks if Locale has changed since last\n    _setLocale();\n\n    // Filters static options\n    _filterOptions(search);\n\n    // Searches for date by pattern\n    _searchDate(search);\n\n    // Searches for date by natural language prompt\n    await _searchDateNLP(search);\n\n    return InlineActionsResult(\n      title: LocaleKeys.inlineActions_date.tr(),\n      results: options,\n    );\n  }\n\n  void _filterOptions(String? search) {\n    if (search == null || search.isEmpty) {\n      options = _allOptions;\n      return;\n    }\n\n    options = _allOptions\n        .where(\n          (option) =>\n              option.keywords != null &&\n              option.keywords!.isNotEmpty &&\n              option.keywords!.any(\n                (keyword) => keyword.contains(search.toLowerCase()),\n              ),\n        )\n        .toList();\n\n    if (options.isEmpty && _keywords.any((k) => search.startsWith(k))) {\n      _setOptions();\n      options = _allOptions;\n    }\n  }\n\n  void _searchDate(String? search) {\n    if (search == null || search.isEmpty) {\n      return;\n    }\n\n    try {\n      final date = DateFormat.yMd(_locale).parse(search);\n      options.insert(0, _itemFromDate(date));\n    } catch (_) {\n      return;\n    }\n  }\n\n  Future<void> _searchDateNLP(String? search) async {\n    if (search == null || search.isEmpty) {\n      return;\n    }\n\n    final result = await DateService.queryDate(search);\n\n    result.fold(\n      (date) => options.insert(0, _itemFromDate(date)),\n      (_) {},\n    );\n  }\n\n  Future<void> _insertDateReference(\n    EditorState editorState,\n    DateTime date,\n    int start,\n    int end,\n  ) async {\n    final selection = editorState.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n\n    final node = editorState.getNodeAtPath(selection.end.path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      return;\n    }\n\n    final transaction = editorState.transaction\n      ..replaceText(\n        node,\n        start,\n        end,\n        MentionBlockKeys.mentionChar,\n        attributes: MentionBlockKeys.buildMentionDateAttributes(\n          date: date.toIso8601String(),\n          includeTime: false,\n          reminderId: null,\n          reminderOption: null,\n        ),\n      );\n\n    await editorState.apply(transaction);\n  }\n\n  void _setOptions() {\n    final today = DateTime.now();\n    final tomorrow = today.add(const Duration(days: 1));\n    final yesterday = today.subtract(const Duration(days: 1));\n\n    late InlineActionsMenuItem todayItem;\n    late InlineActionsMenuItem tomorrowItem;\n    late InlineActionsMenuItem yesterdayItem;\n\n    try {\n      todayItem = _itemFromDate(\n        today,\n        LocaleKeys.relativeDates_today.tr(),\n        [DateFormat.yMd(_locale).format(today)],\n      );\n      tomorrowItem = _itemFromDate(\n        tomorrow,\n        LocaleKeys.relativeDates_tomorrow.tr(),\n        [DateFormat.yMd(_locale).format(tomorrow)],\n      );\n      yesterdayItem = _itemFromDate(\n        yesterday,\n        LocaleKeys.relativeDates_yesterday.tr(),\n        [DateFormat.yMd(_locale).format(yesterday)],\n      );\n    } catch (e) {\n      todayItem = _itemFromDate(today);\n      tomorrowItem = _itemFromDate(tomorrow);\n      yesterdayItem = _itemFromDate(yesterday);\n    }\n\n    _allOptions = [\n      todayItem,\n      tomorrowItem,\n      yesterdayItem,\n    ];\n  }\n\n  /// Sets Locale on each search to make sure\n  /// keywords are localized\n  void _setLocale() {\n    final locale = context.locale.toLanguageTag();\n\n    if (locale != _locale) {\n      _locale = locale;\n      _setOptions();\n    }\n  }\n\n  InlineActionsMenuItem _itemFromDate(\n    DateTime date, [\n    String? label,\n    List<String>? keywords,\n  ]) {\n    late String labelStr;\n    if (label != null) {\n      labelStr = label;\n    } else {\n      try {\n        labelStr = DateFormat.yMd(_locale).format(date);\n      } catch (e) {\n        // fallback to en-US\n        labelStr = DateFormat.yMd('en-US').format(date);\n      }\n    }\n\n    return InlineActionsMenuItem(\n      label: labelStr.capitalize(),\n      keywords: [labelStr.toLowerCase(), ...?keywords],\n      onSelected: (context, editorState, menuService, replace) =>\n          _insertDateReference(\n        editorState,\n        date,\n        replace.$1,\n        replace.$2,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';\nimport 'package:appflowy/plugins/inline_actions/service_handler.dart';\nimport 'package:appflowy/shared/flowy_error_page.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/list_extension.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/recent/cached_recent_service.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';\nimport 'package:flutter/material.dart';\n\n// const _channel = \"InlinePageReference\";\n\n// TODO(Mathias): Clean up and use folder search instead\nclass InlinePageReferenceService extends InlineActionsDelegate {\n  InlinePageReferenceService({\n    required this.currentViewId,\n    this.viewLayout,\n    this.customTitle,\n    this.insertPage = false,\n    this.limitResults = 5,\n  }) : assert(limitResults > 0, 'limitResults must be greater than 0') {\n    init();\n  }\n\n  final Completer _initCompleter = Completer<void>();\n\n  final String currentViewId;\n  final ViewLayoutPB? viewLayout;\n  final String? customTitle;\n\n  /// Defaults to false, if set to true the Page\n  /// will be inserted as a Reference\n  /// When false, a link to the view will be inserted\n  ///\n  final bool insertPage;\n\n  /// Defaults to 5\n  /// Will limit the page reference results\n  /// to [limitResults].\n  ///\n  final int limitResults;\n\n  late final CachedRecentService _recentService;\n\n  bool _recentViewsInitialized = false;\n  late final List<InlineActionsMenuItem> _recentViews;\n\n  Future<List<InlineActionsMenuItem>> _getRecentViews() async {\n    if (_recentViewsInitialized) {\n      return _recentViews;\n    }\n\n    _recentViewsInitialized = true;\n\n    final sectionViews = await _recentService.recentViews();\n    final views =\n        sectionViews.unique((e) => e.item.id).map((e) => e.item).toList();\n\n    // Filter by viewLayout\n    views.retainWhere(\n      (i) =>\n          currentViewId != i.id &&\n          (viewLayout == null || i.layout == viewLayout),\n    );\n\n    // Map to InlineActionsMenuItem, then take 5 items\n    return _recentViews = views.map(_fromView).take(5).toList();\n  }\n\n  bool _viewsInitialized = false;\n  late final List<ViewPB> _allViews;\n\n  Future<List<ViewPB>> _getViews() async {\n    if (_viewsInitialized) {\n      return _allViews;\n    }\n\n    _viewsInitialized = true;\n\n    final viewResult = await ViewBackendService.getAllViews();\n    return _allViews = viewResult\n            .toNullable()\n            ?.items\n            .where((v) => viewLayout == null || v.layout == viewLayout)\n            .toList() ??\n        const [];\n  }\n\n  Future<void> init() async {\n    _recentService = getIt<CachedRecentService>();\n    // _searchListener.start(onResultsClosed: _onResults);\n  }\n\n  @override\n  Future<void> dispose() async {\n    if (!_initCompleter.isCompleted) {\n      _initCompleter.complete();\n    }\n\n    await super.dispose();\n  }\n\n  @override\n  Future<InlineActionsResult> search([\n    String? search,\n  ]) async {\n    final isSearching = search != null && search.isNotEmpty;\n\n    late List<InlineActionsMenuItem> items;\n    if (isSearching) {\n      final allViews = await _getViews();\n\n      items = allViews\n          .where(\n            (view) =>\n                view.id != currentViewId &&\n                    view.name.toLowerCase().contains(search.toLowerCase()) ||\n                (view.name.isEmpty && search.isEmpty) ||\n                (view.name.isEmpty &&\n                    LocaleKeys.menuAppHeader_defaultNewPageName\n                        .tr()\n                        .toLowerCase()\n                        .contains(search.toLowerCase())),\n          )\n          .take(limitResults)\n          .map((view) => _fromView(view))\n          .toList();\n    } else {\n      items = await _getRecentViews();\n    }\n\n    return InlineActionsResult(\n      title: customTitle?.isNotEmpty == true\n          ? customTitle!\n          : isSearching\n              ? LocaleKeys.inlineActions_pageReference.tr()\n              : LocaleKeys.inlineActions_recentPages.tr(),\n      results: items,\n    );\n  }\n\n  Future<void> _onInsertPageRef(\n    ViewPB view,\n    BuildContext context,\n    EditorState editorState,\n    (int, int) replace,\n  ) async {\n    final selection = editorState.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n\n    final node = editorState.getNodeAtPath(selection.start.path);\n\n    if (node != null) {\n      // Delete search term\n      if (replace.$2 > 0) {\n        final transaction = editorState.transaction\n          ..deleteText(node, replace.$1, replace.$2);\n        await editorState.apply(transaction);\n      }\n\n      // Insert newline before inserting referenced database\n      if (node.delta?.toPlainText().isNotEmpty == true) {\n        await editorState.insertNewLine();\n      }\n    }\n\n    try {\n      await editorState.insertReferencePage(view, view.layout);\n    } on FlowyError catch (e) {\n      if (context.mounted) {\n        return Dialogs.show(\n          context,\n          child: AppFlowyErrorPage(\n            error: e,\n          ),\n        );\n      }\n    }\n  }\n\n  Future<void> _onInsertLinkRef(\n    ViewPB view,\n    BuildContext context,\n    EditorState editorState,\n    InlineActionsMenuService menuService,\n    (int, int) replace,\n  ) async {\n    final selection = editorState.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n\n    final node = editorState.getNodeAtPath(selection.start.path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      return;\n    }\n\n    // @page name -> $\n    // preload the page infos\n    pageMemorizer[view.id] = view;\n    final transaction = editorState.transaction\n      ..replaceText(\n        node,\n        replace.$1,\n        replace.$2,\n        MentionBlockKeys.mentionChar,\n        attributes: MentionBlockKeys.buildMentionPageAttributes(\n          mentionType: MentionType.page,\n          pageId: view.id,\n          blockId: null,\n        ),\n      );\n\n    await editorState.apply(transaction);\n  }\n\n  InlineActionsMenuItem _fromView(ViewPB view) => InlineActionsMenuItem(\n        keywords: [view.nameOrDefault.toLowerCase()],\n        label: view.nameOrDefault,\n        iconBuilder: (onSelected) {\n          final child = view.icon.value.isNotEmpty\n              ? RawEmojiIconWidget(\n                  emoji: view.icon.toEmojiIconData(),\n                  emojiSize: 16.0,\n                  lineHeight: 18.0 / 16.0,\n                )\n              : view.defaultIcon(size: const Size(16, 16));\n          return SizedBox(\n            width: 16,\n            child: child,\n          );\n        },\n        onSelected: (context, editorState, menu, replace) => insertPage\n            ? _onInsertPageRef(view, context, editorState, replace)\n            : _onInsertLinkRef(view, context, editorState, menu, replace),\n      );\n\n// Future<InlineActionsMenuItem?> _fromSearchResult(\n//   SearchResultPB result,\n// ) async {\n//   final viewRes = await ViewBackendService.getView(result.viewId);\n//   final view = viewRes.toNullable();\n//   if (view == null) {\n//     return null;\n//   }\n\n//   return _fromView(view);\n// }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart",
    "content": "import 'package:appflowy/date/date_service.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';\nimport 'package:appflowy/plugins/inline_actions/service_handler.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/user/application/reminder/reminder_extension.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:nanoid/nanoid.dart';\n\nfinal _keywords = [\n  LocaleKeys.inlineActions_reminder_groupTitle.tr().toLowerCase(),\n  LocaleKeys.inlineActions_reminder_shortKeyword.tr().toLowerCase(),\n];\n\nclass ReminderReferenceService extends InlineActionsDelegate {\n  ReminderReferenceService(this.context) {\n    // Initialize locale\n    _locale = context.locale.toLanguageTag();\n\n    // Initializes options\n    _setOptions();\n  }\n\n  final BuildContext context;\n\n  late String _locale;\n  late List<InlineActionsMenuItem> _allOptions;\n\n  List<InlineActionsMenuItem> options = [];\n\n  @override\n  Future<InlineActionsResult> search([\n    String? search,\n  ]) async {\n    // Checks if Locale has changed since last\n    _setLocale();\n\n    // Filters static options\n    _filterOptions(search);\n\n    // Searches for date by pattern\n    _searchDate(search);\n\n    // Searches for date by natural language prompt\n    await _searchDateNLP(search);\n\n    return _groupFromResults(options);\n  }\n\n  InlineActionsResult _groupFromResults([\n    List<InlineActionsMenuItem>? options,\n  ]) =>\n      InlineActionsResult(\n        title: LocaleKeys.inlineActions_reminder_groupTitle.tr(),\n        results: options ?? [],\n        startsWithKeywords: [\n          LocaleKeys.inlineActions_reminder_groupTitle.tr().toLowerCase(),\n          LocaleKeys.inlineActions_reminder_shortKeyword.tr().toLowerCase(),\n        ],\n      );\n\n  void _filterOptions(String? search) {\n    if (search == null || search.isEmpty) {\n      options = _allOptions;\n      return;\n    }\n\n    options = _allOptions\n        .where(\n          (option) =>\n              option.keywords != null &&\n              option.keywords!.isNotEmpty &&\n              option.keywords!.any(\n                (keyword) => keyword.contains(search.toLowerCase()),\n              ),\n        )\n        .toList();\n\n    if (options.isEmpty && _keywords.any((k) => search.startsWith(k))) {\n      _setOptions();\n      options = _allOptions;\n    }\n  }\n\n  void _searchDate(String? search) {\n    if (search == null || search.isEmpty) {\n      return;\n    }\n\n    try {\n      final date = DateFormat.yMd(_locale).parse(search);\n      options.insert(0, _itemFromDate(date));\n    } catch (_) {\n      return;\n    }\n  }\n\n  Future<void> _searchDateNLP(String? search) async {\n    if (search == null || search.isEmpty) {\n      return;\n    }\n\n    final result = await DateService.queryDate(search);\n\n    result.fold(\n      (date) {\n        // Only insert dates in the future\n        if (DateTime.now().isBefore(date)) {\n          options.insert(0, _itemFromDate(date));\n        }\n      },\n      (_) {},\n    );\n  }\n\n  Future<void> _insertReminderReference(\n    EditorState editorState,\n    DateTime date,\n    int start,\n    int end,\n  ) async {\n    final selection = editorState.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n\n    final node = editorState.getNodeAtPath(selection.end.path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      return;\n    }\n\n    final viewId = context.read<DocumentBloc>().documentId;\n    final reminder = _reminderFromDate(date, viewId, node);\n\n    final transaction = editorState.transaction\n      ..replaceText(\n        node,\n        start,\n        end,\n        MentionBlockKeys.mentionChar,\n        attributes: MentionBlockKeys.buildMentionDateAttributes(\n          date: date.toIso8601String(),\n          reminderId: reminder.id,\n          reminderOption: ReminderOption.atTimeOfEvent.name,\n          includeTime: false,\n        ),\n      );\n\n    await editorState.apply(transaction);\n\n    if (context.mounted) {\n      context.read<ReminderBloc>().add(ReminderEvent.add(reminder: reminder));\n    }\n  }\n\n  void _setOptions() {\n    final today = DateTime.now();\n    final tomorrow = today.add(const Duration(days: 1));\n    final oneWeek = today.add(const Duration(days: 7));\n\n    late InlineActionsMenuItem todayItem;\n    late InlineActionsMenuItem oneWeekItem;\n\n    try {\n      todayItem = _itemFromDate(\n        tomorrow,\n        LocaleKeys.relativeDates_tomorrow.tr(),\n        [DateFormat.yMd(_locale).format(tomorrow)],\n      );\n    } catch (e) {\n      todayItem = _itemFromDate(today);\n    }\n\n    try {\n      oneWeekItem = _itemFromDate(\n        oneWeek,\n        LocaleKeys.relativeDates_oneWeek.tr(),\n        [DateFormat.yMd(_locale).format(oneWeek)],\n      );\n    } catch (e) {\n      oneWeekItem = _itemFromDate(oneWeek);\n    }\n\n    _allOptions = [\n      todayItem,\n      oneWeekItem,\n    ];\n  }\n\n  /// Sets Locale on each search to make sure\n  /// keywords are localized\n  void _setLocale() {\n    final locale = context.locale.toLanguageTag();\n\n    if (locale != _locale) {\n      _locale = locale;\n      _setOptions();\n    }\n  }\n\n  InlineActionsMenuItem _itemFromDate(\n    DateTime date, [\n    String? label,\n    List<String>? keywords,\n  ]) {\n    late String labelStr;\n    if (label != null) {\n      labelStr = label;\n    } else {\n      try {\n        labelStr = DateFormat.yMd(_locale).format(date);\n      } catch (e) {\n        // fallback to en-US\n        labelStr = DateFormat.yMd('en-US').format(date);\n      }\n    }\n\n    return InlineActionsMenuItem(\n      label: labelStr.capitalize(),\n      keywords: [labelStr.toLowerCase(), ...?keywords],\n      onSelected: (context, editorState, menuService, replace) =>\n          _insertReminderReference(editorState, date, replace.$1, replace.$2),\n    );\n  }\n\n  ReminderPB _reminderFromDate(DateTime date, String viewId, Node node) {\n    return ReminderPB(\n      id: nanoid(),\n      objectId: viewId,\n      title: LocaleKeys.reminderNotification_title.tr(),\n      message: LocaleKeys.reminderNotification_message.tr(),\n      meta: {\n        ReminderMetaKeys.includeTime: false.toString(),\n        ReminderMetaKeys.blockId: node.id,\n        ReminderMetaKeys.createdAt:\n            DateTime.now().millisecondsSinceEpoch.toString(),\n      },\n      scheduledAt: Int64(date.millisecondsSinceEpoch ~/ 1000),\n      isAck: date.isBefore(DateTime.now()),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_command.dart",
    "content": "import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nconst inlineActionCharacter = '@';\n\nCharacterShortcutEvent inlineActionsCommand(\n  InlineActionsService inlineActionsService, {\n  InlineActionsMenuStyle style = const InlineActionsMenuStyle.light(),\n}) =>\n    CharacterShortcutEvent(\n      key: 'Opens Inline Actions Menu',\n      character: inlineActionCharacter,\n      handler: (editorState) => inlineActionsCommandHandler(\n        editorState,\n        inlineActionsService,\n        style,\n      ),\n    );\n\nInlineActionsMenuService? selectionMenuService;\n\nFuture<bool> inlineActionsCommandHandler(\n  EditorState editorState,\n  InlineActionsService service,\n  InlineActionsMenuStyle style,\n) async {\n  final selection = editorState.selection;\n  if (selection == null) {\n    return false;\n  }\n\n  if (!selection.isCollapsed) {\n    await editorState.deleteSelection(selection);\n  }\n\n  await editorState.insertTextAtPosition(\n    inlineActionCharacter,\n    position: selection.start,\n  );\n\n  final List<InlineActionsResult> initialResults = [];\n  for (final handler in service.handlers) {\n    final group = await handler.search(null);\n\n    if (group.results.isNotEmpty) {\n      initialResults.add(group);\n    }\n  }\n\n  if (service.context != null) {\n    keepEditorFocusNotifier.increase();\n    selectionMenuService?.dismiss();\n    selectionMenuService = UniversalPlatform.isMobile\n        ? MobileInlineActionsMenu(\n            context: service.context!,\n            editorState: editorState,\n            service: service,\n            initialResults: initialResults,\n            style: style,\n          )\n        : InlineActionsMenu(\n            context: service.context!,\n            editorState: editorState,\n            service: service,\n            initialResults: initialResults,\n            style: style,\n          );\n\n    // disable the keyboard service\n    editorState.service.keyboardService?.disable();\n\n    await selectionMenuService?.show();\n\n    // enable the keyboard service\n    editorState.service.keyboardService?.enable();\n  }\n\n  return true;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_menu.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';\nimport 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\nabstract class InlineActionsMenuService {\n  InlineActionsMenuStyle get style;\n\n  Future<void> show();\n\n  void dismiss();\n}\n\nclass InlineActionsMenu extends InlineActionsMenuService {\n  InlineActionsMenu({\n    required this.context,\n    required this.editorState,\n    required this.service,\n    required this.initialResults,\n    required this.style,\n    this.startCharAmount = 1,\n    this.cancelBySpaceHandler,\n  });\n\n  final BuildContext context;\n  final EditorState editorState;\n  final InlineActionsService service;\n  final List<InlineActionsResult> initialResults;\n  final bool Function()? cancelBySpaceHandler;\n\n  @override\n  final InlineActionsMenuStyle style;\n\n  final int startCharAmount;\n\n  OverlayEntry? _menuEntry;\n  bool selectionChangedByMenu = false;\n\n  @override\n  void dismiss() {\n    if (_menuEntry != null) {\n      editorState.service.keyboardService?.enable();\n      editorState.service.scrollService?.enable();\n      keepEditorFocusNotifier.decrease();\n    }\n\n    _menuEntry?.remove();\n    _menuEntry = null;\n\n    // workaround: SelectionService has been released after hot reload.\n    final isSelectionDisposed =\n        editorState.service.selectionServiceKey.currentState == null;\n    if (!isSelectionDisposed) {\n      final selectionService = editorState.service.selectionService;\n      selectionService.currentSelection.removeListener(_onSelectionChange);\n    }\n  }\n\n  void _onSelectionUpdate() => selectionChangedByMenu = true;\n\n  @override\n  Future<void> show() {\n    final completer = Completer<void>();\n    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n      _show();\n      completer.complete();\n    });\n    return completer.future;\n  }\n\n  void _show() {\n    dismiss();\n\n    final selectionService = editorState.service.selectionService;\n    final selectionRects = selectionService.selectionRects;\n    if (selectionRects.isEmpty) {\n      return;\n    }\n\n    const double menuHeight = 300.0;\n    const double menuWidth = 200.0;\n    const Offset menuOffset = Offset(0, 10);\n    final Offset editorOffset =\n        editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;\n    final Size editorSize = editorState.renderBox!.size;\n\n    // Default to opening the overlay below\n    Alignment alignment = Alignment.topLeft;\n\n    final firstRect = selectionRects.first;\n    Offset offset = firstRect.bottomRight + menuOffset;\n\n    // Show above\n    if (offset.dy + menuHeight >= editorOffset.dy + editorSize.height) {\n      offset = firstRect.topRight - menuOffset;\n      alignment = Alignment.bottomLeft;\n\n      offset = Offset(\n        offset.dx,\n        MediaQuery.of(context).size.height - offset.dy,\n      );\n    }\n\n    // Show on the left\n    final windowWidth = MediaQuery.of(context).size.width;\n    if (offset.dx > (windowWidth - menuWidth)) {\n      alignment = alignment == Alignment.topLeft\n          ? Alignment.topRight\n          : Alignment.bottomRight;\n\n      offset = Offset(\n        windowWidth - offset.dx,\n        offset.dy,\n      );\n    }\n\n    final (left, top, right, bottom) = _getPosition(alignment, offset);\n\n    _menuEntry = OverlayEntry(\n      builder: (context) => SizedBox(\n        height: editorSize.height,\n        width: editorSize.width,\n\n        // GestureDetector handles clicks outside of the context menu,\n        // to dismiss the context menu.\n        child: GestureDetector(\n          behavior: HitTestBehavior.opaque,\n          onTap: dismiss,\n          child: Stack(\n            children: [\n              Positioned(\n                top: top,\n                bottom: bottom,\n                left: left,\n                right: right,\n                child: SingleChildScrollView(\n                  scrollDirection: Axis.horizontal,\n                  child: InlineActionsHandler(\n                    service: service,\n                    results: initialResults,\n                    editorState: editorState,\n                    menuService: this,\n                    onDismiss: dismiss,\n                    onSelectionUpdate: _onSelectionUpdate,\n                    style: style,\n                    startCharAmount: startCharAmount,\n                    cancelBySpaceHandler: cancelBySpaceHandler,\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n\n    Overlay.of(context).insert(_menuEntry!);\n\n    editorState.service.keyboardService?.disable(showCursor: true);\n    editorState.service.scrollService?.disable();\n    selectionService.currentSelection.addListener(_onSelectionChange);\n  }\n\n  void _onSelectionChange() {\n    // workaround: SelectionService has been released after hot reload.\n    final isSelectionDisposed =\n        editorState.service.selectionServiceKey.currentState == null;\n    if (!isSelectionDisposed) {\n      final selectionService = editorState.service.selectionService;\n      if (selectionService.currentSelection.value == null) {\n        return;\n      }\n    }\n\n    if (!selectionChangedByMenu) {\n      return dismiss();\n    }\n\n    selectionChangedByMenu = false;\n  }\n\n  (double? left, double? top, double? right, double? bottom) _getPosition(\n    Alignment alignment,\n    Offset offset,\n  ) {\n    double? left, top, right, bottom;\n    switch (alignment) {\n      case Alignment.topLeft:\n        left = offset.dx;\n        top = offset.dy;\n        break;\n      case Alignment.bottomLeft:\n        left = offset.dx;\n        bottom = offset.dy;\n        break;\n      case Alignment.topRight:\n        right = offset.dx;\n        top = offset.dy;\n        break;\n      case Alignment.bottomRight:\n        right = offset.dx;\n        bottom = offset.dy;\n        break;\n    }\n\n    return (left, top, right, bottom);\n  }\n}\n\nclass InlineActionsMenuStyle {\n  InlineActionsMenuStyle({\n    required this.backgroundColor,\n    required this.groupTextColor,\n    required this.menuItemTextColor,\n    required this.menuItemSelectedColor,\n    required this.menuItemSelectedTextColor,\n  });\n\n  const InlineActionsMenuStyle.light()\n      : backgroundColor = Colors.white,\n        groupTextColor = const Color(0xFF555555),\n        menuItemTextColor = const Color(0xFF333333),\n        menuItemSelectedColor = const Color(0xFFE0F8FF),\n        menuItemSelectedTextColor = const Color.fromARGB(255, 56, 91, 247);\n\n  const InlineActionsMenuStyle.dark()\n      : backgroundColor = const Color(0xFF282E3A),\n        groupTextColor = const Color(0xFFBBC3CD),\n        menuItemTextColor = const Color(0xFFBBC3CD),\n        menuItemSelectedColor = const Color(0xFF00BCF0),\n        menuItemSelectedTextColor = const Color(0xFF131720);\n\n  /// The background color of the context menu itself\n  ///\n  final Color backgroundColor;\n\n  /// The color of the [InlineActionsGroup]'s title text\n  ///\n  final Color groupTextColor;\n\n  /// The text color of an [InlineActionsMenuItem]\n  ///\n  final Color menuItemTextColor;\n\n  /// The background of the currently selected [InlineActionsMenuItem]\n  ///\n  final Color menuItemSelectedColor;\n\n  /// The text color of the currently selected [InlineActionsMenuItem]\n  ///\n  final Color menuItemSelectedTextColor;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_result.dart",
    "content": "import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\n\ntypedef SelectItemHandler = void Function(\n  BuildContext context,\n  EditorState editorState,\n  InlineActionsMenuService menuService,\n  (int start, int end) replacement,\n);\n\nclass InlineActionsMenuItem {\n  InlineActionsMenuItem({\n    required this.label,\n    this.iconBuilder,\n    this.keywords,\n    this.onSelected,\n  });\n\n  final String label;\n  final Widget Function(bool onSelected)? iconBuilder;\n  final List<String>? keywords;\n  final SelectItemHandler? onSelected;\n}\n\nclass InlineActionsResult {\n  InlineActionsResult({\n    this.title,\n    required this.results,\n    this.startsWithKeywords,\n  });\n\n  /// Localized title to be displayed above the results\n  /// of the current group.\n  ///\n  /// If null, no title will be displayed.\n  ///\n  final String? title;\n\n  /// List of results that will be displayed for this group\n  /// made up of [SelectionMenuItem]s.\n  ///\n  final List<InlineActionsMenuItem> results;\n\n  /// If the search term start with one of these keyword,\n  /// the results will be reordered such that these results\n  /// will be above.\n  ///\n  final List<String>? startsWithKeywords;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_service.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/plugins/inline_actions/service_handler.dart';\n\nabstract class _InlineActionsProvider {\n  void dispose();\n}\n\nclass InlineActionsService extends _InlineActionsProvider {\n  InlineActionsService({\n    required this.context,\n    required this.handlers,\n  });\n\n  /// The [BuildContext] in which to show the [InlineActionsMenu]\n  ///\n  BuildContext? context;\n\n  final List<InlineActionsDelegate> handlers;\n\n  /// This is a workaround for not having a mounted check.\n  /// Thus when the widget that uses the service is disposed,\n  /// we set the [BuildContext] to null.\n  ///\n  @override\n  Future<void> dispose() async {\n    for (final handler in handlers) {\n      await handler.dispose();\n    }\n    context = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/inline_actions/service_handler.dart",
    "content": "import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';\n\nabstract class InlineActionsDelegate {\n  Future<InlineActionsResult> search(String? search);\n\n  Future<void> dispose() async {}\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_service.dart';\nimport 'package:appflowy/plugins/inline_actions/widgets/inline_actions_menu_group.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\n/// All heights are in physical pixels\nconst double _groupTextHeight = 14; // 12 height + 2 bottom spacing\nconst double _groupBottomSpacing = 6;\nconst double _itemHeight = 30; // 26 height + 4 vertical spacing (2*2)\n\nconst double kInlineMenuHeight = 300;\nconst double kInlineMenuWidth = 400;\nconst double _contentHeight = 260;\n\nextension _StartWithsSort on List<InlineActionsResult> {\n  void sortByStartsWithKeyword(String search) => sort(\n        (a, b) {\n          final aCount = a.startsWithKeywords\n                  ?.where(\n                    (key) => search.toLowerCase().startsWith(key),\n                  )\n                  .length ??\n              0;\n\n          final bCount = b.startsWithKeywords\n                  ?.where(\n                    (key) => search.toLowerCase().startsWith(key),\n                  )\n                  .length ??\n              0;\n\n          if (aCount > bCount) {\n            return -1;\n          } else if (bCount > aCount) {\n            return 1;\n          }\n\n          return 0;\n        },\n      );\n}\n\nconst _invalidSearchesAmount = 10;\n\nclass InlineActionsHandler extends StatefulWidget {\n  const InlineActionsHandler({\n    super.key,\n    required this.service,\n    required this.results,\n    required this.editorState,\n    required this.menuService,\n    required this.onDismiss,\n    required this.onSelectionUpdate,\n    required this.style,\n    this.startCharAmount = 1,\n    this.cancelBySpaceHandler,\n  });\n\n  final InlineActionsService service;\n  final List<InlineActionsResult> results;\n  final EditorState editorState;\n  final InlineActionsMenuService menuService;\n  final VoidCallback onDismiss;\n  final VoidCallback onSelectionUpdate;\n  final InlineActionsMenuStyle style;\n  final int startCharAmount;\n  final bool Function()? cancelBySpaceHandler;\n\n  @override\n  State<InlineActionsHandler> createState() => _InlineActionsHandlerState();\n}\n\nclass _InlineActionsHandlerState extends State<InlineActionsHandler> {\n  final _focusNode = FocusNode(debugLabel: 'inline_actions_menu_handler');\n  final _scrollController = ScrollController();\n\n  late List<InlineActionsResult> results = widget.results;\n  int invalidCounter = 0;\n  late int startOffset;\n\n  String _search = '';\n  set search(String search) {\n    _search = search;\n    _doSearch();\n  }\n\n  Future<void> _doSearch() async {\n    final List<InlineActionsResult> newResults = [];\n    for (final handler in widget.service.handlers) {\n      final group = await handler.search(_search);\n\n      if (group.results.isNotEmpty) {\n        newResults.add(group);\n      }\n    }\n\n    invalidCounter = results.every((group) => group.results.isEmpty)\n        ? invalidCounter + 1\n        : 0;\n\n    if (invalidCounter >= _invalidSearchesAmount) {\n      widget.onDismiss();\n\n      // Workaround to bring focus back to editor\n      await widget.editorState\n          .updateSelectionWithReason(widget.editorState.selection);\n\n      return;\n    }\n\n    _resetSelection();\n\n    newResults.sortByStartsWithKeyword(_search);\n    setState(() => results = newResults);\n  }\n\n  void _resetSelection() {\n    _selectedGroup = 0;\n    _selectedIndex = 0;\n  }\n\n  int _selectedGroup = 0;\n  int _selectedIndex = 0;\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addPostFrameCallback(\n      (_) => _focusNode.requestFocus(),\n    );\n\n    startOffset = widget.editorState.selection?.endIndex ?? 0;\n  }\n\n  @override\n  void dispose() {\n    _scrollController.dispose();\n    _focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Focus(\n      focusNode: _focusNode,\n      onKeyEvent: onKeyEvent,\n      child: Container(\n        constraints: const BoxConstraints(\n          maxHeight: kInlineMenuHeight,\n          minWidth: kInlineMenuWidth,\n        ),\n        decoration: BoxDecoration(\n          color: widget.style.backgroundColor,\n          borderRadius: BorderRadius.circular(6.0),\n          boxShadow: [\n            BoxShadow(\n              blurRadius: 5,\n              spreadRadius: 1,\n              color: Colors.black.withValues(alpha: 0.1),\n            ),\n          ],\n        ),\n        child: Padding(\n          padding: const EdgeInsets.all(8.0),\n          child: noResults\n              ? SizedBox(\n                  width: 150,\n                  child: FlowyText.regular(\n                    LocaleKeys.inlineActions_noResults.tr(),\n                  ),\n                )\n              : SingleChildScrollView(\n                  controller: _scrollController,\n                  physics: const ClampingScrollPhysics(),\n                  child: Column(\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: results\n                        .where((g) => g.results.isNotEmpty)\n                        .mapIndexed(\n                          (index, group) => InlineActionsGroup(\n                            result: group,\n                            editorState: widget.editorState,\n                            menuService: widget.menuService,\n                            style: widget.style,\n                            onSelected: widget.onDismiss,\n                            startOffset: startOffset - widget.startCharAmount,\n                            endOffset: _search.length + widget.startCharAmount,\n                            isLastGroup: index == results.length - 1,\n                            isGroupSelected: _selectedGroup == index,\n                            selectedIndex: _selectedIndex,\n                          ),\n                        )\n                        .toList(),\n                  ),\n                ),\n        ),\n      ),\n    );\n  }\n\n  bool get noResults =>\n      results.isEmpty || results.every((e) => e.results.isEmpty);\n\n  int get groupLength => results.length;\n\n  int lengthOfGroup(int index) =>\n      results.length > index ? results[index].results.length : -1;\n\n  InlineActionsMenuItem handlerOf(int groupIndex, int handlerIndex) =>\n      results[groupIndex].results[handlerIndex];\n\n  KeyEventResult onKeyEvent(focus, KeyEvent event) {\n    if (event is! KeyDownEvent) {\n      return KeyEventResult.ignored;\n    }\n\n    const moveKeys = [\n      LogicalKeyboardKey.arrowUp,\n      LogicalKeyboardKey.arrowDown,\n      LogicalKeyboardKey.tab,\n    ];\n\n    if (event.logicalKey == LogicalKeyboardKey.enter) {\n      if (_selectedGroup <= groupLength &&\n          _selectedIndex <= lengthOfGroup(_selectedGroup)) {\n        handlerOf(_selectedGroup, _selectedIndex).onSelected?.call(\n          context,\n          widget.editorState,\n          widget.menuService,\n          (\n            startOffset - widget.startCharAmount,\n            _search.length + widget.startCharAmount\n          ),\n        );\n\n        widget.onDismiss();\n        return KeyEventResult.handled;\n      }\n\n      if (noResults) {\n        // Workaround to bring focus back to editor\n        widget.editorState\n            .updateSelectionWithReason(widget.editorState.selection);\n        widget.editorState.insertNewLine();\n\n        widget.onDismiss();\n        return KeyEventResult.handled;\n      }\n    } else if (event.logicalKey == LogicalKeyboardKey.escape) {\n      // Workaround to bring focus back to editor\n      widget.editorState\n          .updateSelectionWithReason(widget.editorState.selection);\n\n      widget.onDismiss();\n    } else if (event.logicalKey == LogicalKeyboardKey.backspace) {\n      if (_search.isEmpty) {\n        if (_canDeleteLastCharacter()) {\n          widget.editorState.deleteBackward();\n        } else {\n          // Workaround for editor regaining focus\n          widget.editorState.apply(\n            widget.editorState.transaction\n              ..afterSelection = widget.editorState.selection,\n          );\n        }\n        widget.onDismiss();\n      } else {\n        widget.onSelectionUpdate();\n        widget.editorState.deleteBackward();\n        _deleteCharacterAtSelection();\n      }\n\n      return KeyEventResult.handled;\n    } else if (event.character != null &&\n        ![\n          ...moveKeys,\n          LogicalKeyboardKey.arrowLeft,\n          LogicalKeyboardKey.arrowRight,\n        ].contains(event.logicalKey)) {\n      /// Prevents dismissal of context menu by notifying the parent\n      /// that the selection change occurred from the handler.\n      widget.onSelectionUpdate();\n\n      if (event.logicalKey == LogicalKeyboardKey.space) {\n        final cancelBySpaceHandler = widget.cancelBySpaceHandler;\n        if (cancelBySpaceHandler != null && cancelBySpaceHandler()) {\n          return KeyEventResult.handled;\n        }\n      }\n\n      // Interpolation to avoid having a getter for private variable\n      _insertCharacter(event.character!);\n      return KeyEventResult.handled;\n    } else if (moveKeys.contains(event.logicalKey)) {\n      _moveSelection(event.logicalKey);\n      return KeyEventResult.handled;\n    }\n\n    if ([LogicalKeyboardKey.arrowLeft, LogicalKeyboardKey.arrowRight]\n        .contains(event.logicalKey)) {\n      widget.onSelectionUpdate();\n\n      event.logicalKey == LogicalKeyboardKey.arrowLeft\n          ? widget.editorState.moveCursorForward()\n          : widget.editorState.moveCursorBackward(SelectionMoveRange.character);\n\n      /// If cursor moves before @ then dismiss menu\n      /// If cursor moves after @search.length then dismiss menu\n      final selection = widget.editorState.selection;\n      if (selection != null &&\n          (selection.endIndex < startOffset ||\n              selection.endIndex > (startOffset + _search.length))) {\n        widget.onDismiss();\n      }\n\n      /// Workaround: When using the move cursor methods, it seems the\n      ///  focus goes back to the editor, this makes sure this handler\n      ///  receives the next keypress.\n      ///\n      _focusNode.requestFocus();\n\n      return KeyEventResult.handled;\n    }\n\n    return KeyEventResult.handled;\n  }\n\n  void _insertCharacter(String character) {\n    widget.editorState.insertTextAtCurrentSelection(character);\n\n    final selection = widget.editorState.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n\n    final delta = widget.editorState.getNodeAtPath(selection.end.path)?.delta;\n    if (delta == null) {\n      return;\n    }\n\n    search = widget.editorState\n        .getTextInSelection(\n          selection.copyWith(\n            start: selection.start.copyWith(offset: startOffset),\n            end: selection.start\n                .copyWith(offset: startOffset + _search.length + 1),\n          ),\n        )\n        .join();\n  }\n\n  void _moveSelection(LogicalKeyboardKey key) {\n    bool didChange = false;\n\n    if (key == LogicalKeyboardKey.arrowUp ||\n        (key == LogicalKeyboardKey.tab &&\n            HardwareKeyboard.instance.isShiftPressed)) {\n      if (_selectedIndex == 0 && _selectedGroup > 0) {\n        _selectedGroup -= 1;\n        _selectedIndex = lengthOfGroup(_selectedGroup) - 1;\n        didChange = true;\n      } else if (_selectedIndex > 0) {\n        _selectedIndex -= 1;\n        didChange = true;\n      }\n    } else if ([LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.tab]\n        .contains(key)) {\n      if (_selectedIndex < lengthOfGroup(_selectedGroup) - 1) {\n        _selectedIndex += 1;\n        didChange = true;\n      } else if (_selectedGroup < groupLength - 1) {\n        _selectedGroup += 1;\n        _selectedIndex = 0;\n        didChange = true;\n      }\n    }\n\n    if (mounted && didChange) {\n      setState(() {});\n      _scrollToItem();\n    }\n  }\n\n  void _scrollToItem() {\n    final groups = _selectedGroup + 1;\n\n    int items = 0;\n    for (int i = 0; i <= _selectedGroup; i++) {\n      items += lengthOfGroup(i);\n    }\n\n    // Remove the leftover items\n    items -= lengthOfGroup(_selectedGroup) - (_selectedIndex + 1);\n\n    /// The offset is roughly calculated by:\n    /// - Amount of Groups passed\n    /// - Amount of Items passed\n    final double offset =\n        (_groupTextHeight + _groupBottomSpacing) * groups + _itemHeight * items;\n\n    // We have a buffer so that when moving up, we show items above the currently\n    // selected item. The buffer is the height of 2 items\n    if (offset <= _scrollController.offset + _itemHeight * 2) {\n      // We want to show the user some options above the newly\n      // focused one, therefore we take the offset and subtract\n      // the height of three items (current + 2)\n      _scrollController.animateTo(\n        offset - _itemHeight * 3,\n        duration: const Duration(milliseconds: 200),\n        curve: Curves.easeIn,\n      );\n    } else if (offset >\n        _scrollController.offset +\n            _contentHeight -\n            _itemHeight -\n            _groupTextHeight) {\n      // The same here, we want to show the options below the\n      // newly focused item when moving downwards, therefore we add\n      // 2 times the item height to the offset\n      _scrollController.animateTo(\n        offset - _contentHeight + _itemHeight * 2,\n        duration: const Duration(milliseconds: 200),\n        curve: Curves.easeIn,\n      );\n    }\n  }\n\n  void _deleteCharacterAtSelection() {\n    final selection = widget.editorState.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return;\n    }\n\n    final node = widget.editorState.getNodeAtPath(selection.end.path);\n    final delta = node?.delta;\n    if (node == null || delta == null) {\n      return;\n    }\n\n    search = delta.toPlainText().substring(\n          startOffset,\n          startOffset - 1 + _search.length,\n        );\n  }\n\n  bool _canDeleteLastCharacter() {\n    final selection = widget.editorState.selection;\n    if (selection == null || !selection.isCollapsed) {\n      return false;\n    }\n\n    final delta = widget.editorState.getNodeAtPath(selection.start.path)?.delta;\n    if (delta == null) {\n      return false;\n    }\n\n    return delta.isNotEmpty;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart",
    "content": "import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';\nimport 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';\nimport 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\n\nclass InlineActionsGroup extends StatelessWidget {\n  const InlineActionsGroup({\n    super.key,\n    required this.result,\n    required this.editorState,\n    required this.menuService,\n    required this.style,\n    required this.onSelected,\n    required this.startOffset,\n    required this.endOffset,\n    this.isLastGroup = false,\n    this.isGroupSelected = false,\n    this.selectedIndex = 0,\n  });\n\n  final InlineActionsResult result;\n  final EditorState editorState;\n  final InlineActionsMenuService menuService;\n  final InlineActionsMenuStyle style;\n  final VoidCallback onSelected;\n  final int startOffset;\n  final int endOffset;\n\n  final bool isLastGroup;\n  final bool isGroupSelected;\n  final int selectedIndex;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: isLastGroup ? EdgeInsets.zero : const EdgeInsets.only(bottom: 6),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          if (result.title != null) ...[\n            FlowyText.medium(result.title!, color: style.groupTextColor),\n            const SizedBox(height: 4),\n          ],\n          ...result.results.mapIndexed(\n            (index, item) => InlineActionsWidget(\n              item: item,\n              editorState: editorState,\n              menuService: menuService,\n              isSelected: isGroupSelected && index == selectedIndex,\n              style: style,\n              onSelected: onSelected,\n              startOffset: startOffset,\n              endOffset: endOffset,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass InlineActionsWidget extends StatefulWidget {\n  const InlineActionsWidget({\n    super.key,\n    required this.item,\n    required this.editorState,\n    required this.menuService,\n    required this.isSelected,\n    required this.style,\n    required this.onSelected,\n    required this.startOffset,\n    required this.endOffset,\n  });\n\n  final InlineActionsMenuItem item;\n  final EditorState editorState;\n  final InlineActionsMenuService menuService;\n  final bool isSelected;\n  final InlineActionsMenuStyle style;\n  final VoidCallback onSelected;\n  final int startOffset;\n  final int endOffset;\n\n  @override\n  State<InlineActionsWidget> createState() => _InlineActionsWidgetState();\n}\n\nclass _InlineActionsWidgetState extends State<InlineActionsWidget> {\n  @override\n  Widget build(BuildContext context) {\n    final iconBuilder = widget.item.iconBuilder;\n    final hasIcon = iconBuilder != null;\n    return Padding(\n      padding: const EdgeInsets.symmetric(vertical: 2),\n      child: SizedBox(\n        width: kInlineMenuWidth,\n        child: FlowyButton(\n          expand: true,\n          isSelected: widget.isSelected,\n          text: Row(\n            children: [\n              if (hasIcon) ...[\n                iconBuilder.call(widget.isSelected),\n                SizedBox(width: 12),\n              ],\n              Flexible(\n                child: FlowyText.regular(\n                  widget.item.label,\n                  figmaLineHeight: 18,\n                  overflow: TextOverflow.ellipsis,\n                ),\n              ),\n            ],\n          ),\n          onTap: _onPressed,\n        ),\n      ),\n    );\n  }\n\n  void _onPressed() {\n    widget.onSelected();\n    widget.item.onSelected?.call(\n      context,\n      widget.editorState,\n      widget.menuService,\n      (widget.startOffset, widget.endOffset),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/shared/callback_shortcuts.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\ntypedef AFBindingCallback = bool Function();\n\nclass AFCallbackShortcuts extends StatelessWidget {\n  const AFCallbackShortcuts({\n    super.key,\n    required this.bindings,\n    required this.child,\n  });\n\n  // The bindings for the shortcuts\n  //\n  // The result of the callback will be used to determine if the event is handled\n  final Map<ShortcutActivator, AFBindingCallback> bindings;\n  final Widget child;\n\n  bool _applyKeyEventBinding(ShortcutActivator activator, KeyEvent event) {\n    if (activator.accepts(event, HardwareKeyboard.instance)) {\n      return bindings[activator]?.call() ?? false;\n    }\n    return false;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Focus(\n      canRequestFocus: false,\n      skipTraversal: true,\n      onKeyEvent: (FocusNode node, KeyEvent event) {\n        KeyEventResult result = KeyEventResult.ignored;\n        for (final ShortcutActivator activator in bindings.keys) {\n          result = _applyKeyEventBinding(activator, event)\n              ? KeyEventResult.handled\n              : result;\n        }\n        return result;\n      },\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/shared/cover_type_ext.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\n\nextension IntoCoverTypePB on CoverType {\n  CoverTypePB into() => switch (this) {\n        CoverType.color => CoverTypePB.ColorCover,\n        CoverType.asset => CoverTypePB.AssetCover,\n        CoverType.file => CoverTypePB.FileCover,\n        _ => CoverTypePB.FileCover,\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/shared/share/_shared.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/features/share_tab/logic/share_tab_bloc.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';\nimport 'package:appflowy/plugins/shared/share/share_bloc.dart';\nimport 'package:appflowy/plugins/shared/share/share_menu.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\n\nclass ShareMenuButton extends StatefulWidget {\n  const ShareMenuButton({\n    super.key,\n    required this.tabs,\n  });\n\n  final List<ShareMenuTab> tabs;\n\n  @override\n  State<ShareMenuButton> createState() => _ShareMenuButtonState();\n}\n\nclass _ShareMenuButtonState extends State<ShareMenuButton> {\n  final popoverController = AFPopoverController();\n  final popoverGroupId = SharePopoverGroupId();\n\n  @override\n  void initState() {\n    super.initState();\n\n    popoverController.addListener(() {\n      if (context.mounted && popoverController.isOpen) {\n        context.read<ShareBloc>().add(const ShareEvent.updatePublishStatus());\n      }\n    });\n  }\n\n  @override\n  void dispose() {\n    popoverController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final shareBloc = context.read<ShareBloc>();\n    final databaseBloc = context.read<DatabaseTabBarBloc?>();\n    final userWorkspaceBloc = context.read<UserWorkspaceBloc>();\n    final shareWithUserBloc = context.read<ShareTabBloc>();\n    // final animationDuration = const Duration(milliseconds: 120);\n\n    return BlocBuilder<ShareBloc, ShareState>(\n      builder: (context, state) {\n        return AFPopover(\n          controller: popoverController,\n          groupId: popoverGroupId,\n          anchor: AFAnchorAuto(\n            offset: const Offset(-176, 12),\n          ),\n          // Enable animation\n          // effects: [\n          //   FadeEffect(duration: animationDuration),\n          //   ScaleEffect(\n          //     duration: animationDuration,\n          //     begin: Offset(0.95, 0.95),\n          //     end: Offset(1, 1),\n          //     alignment: Alignment.topRight,\n          //   ),\n          //   MoveEffect(\n          //     duration: animationDuration,\n          //     begin: Offset(20, -20),\n          //     end: Offset(0, 0),\n          //     curve: Curves.easeOutQuad,\n          //   ),\n          // ],\n          popover: (_) {\n            return ConstrainedBox(\n              constraints: const BoxConstraints(\n                maxWidth: 460,\n              ),\n              child: MultiBlocProvider(\n                providers: [\n                  if (databaseBloc != null)\n                    BlocProvider.value(\n                      value: databaseBloc,\n                    ),\n                  BlocProvider.value(value: shareBloc),\n                  BlocProvider.value(value: userWorkspaceBloc),\n                  BlocProvider.value(value: shareWithUserBloc),\n                ],\n                child: Provider.value(\n                  value: popoverGroupId,\n                  child: ShareMenu(\n                    tabs: widget.tabs,\n                    viewName: state.viewName,\n                    onClose: () {\n                      popoverController.hide();\n                    },\n                  ),\n                ),\n              ),\n            );\n          },\n          child: AFFilledTextButton.primary(\n            text: LocaleKeys.shareAction_buttonText.tr(),\n            onTap: () {\n              popoverController.show();\n\n              /// Fetch the shared users when the popover is shown\n              context.read<ShareTabBloc>().add(ShareTabEvent.loadSharedUsers());\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/shared/share/constants.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/startup/startup.dart';\n\nclass ShareConstants {\n  static const String testBaseWebDomain = 'test.appflowy.com';\n  static const String defaultBaseWebDomain = 'https://appflowy.com';\n\n  static String buildPublishUrl({\n    required String nameSpace,\n    required String publishName,\n  }) {\n    final baseShareDomain =\n        getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_web_domain;\n    final url = '$baseShareDomain/$nameSpace/$publishName'.addSchemaIfNeeded();\n    return url;\n  }\n\n  static String buildNamespaceUrl({\n    required String nameSpace,\n    bool withHttps = false,\n  }) {\n    final baseShareDomain =\n        getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_web_domain;\n    String url = baseShareDomain.addSchemaIfNeeded();\n    if (!withHttps) {\n      url = url.replaceFirst('https://', '');\n    }\n    return '$url/$nameSpace';\n  }\n\n  static String buildShareUrl({\n    required String workspaceId,\n    required String viewId,\n    String? blockId,\n  }) {\n    final baseShareDomain =\n        getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_web_domain;\n    final url = '$baseShareDomain/app/$workspaceId/$viewId'.addSchemaIfNeeded();\n    if (blockId == null || blockId.isEmpty) {\n      return url;\n    }\n    return '$url?blockId=$blockId';\n  }\n}\n\nextension on String {\n  String addSchemaIfNeeded() {\n    final schema = Uri.parse(this).scheme;\n    // if the schema is empty, add https schema by default\n    if (schema.isEmpty) {\n      return 'https://$this';\n    }\n    return this;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/shared/share/share_bloc.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/export/document_exporter.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/file_picker/file_picker_service.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass ExportTab extends StatelessWidget {\n  const ExportTab({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final view = context.read<ShareBloc>().view;\n\n    if (view.layout == ViewLayoutPB.Document) {\n      return _buildDocumentExportTab(context);\n    }\n\n    return _buildDatabaseExportTab(context);\n  }\n\n  Widget _buildDocumentExportTab(BuildContext context) {\n    return Column(\n      children: [\n        const VSpace(10),\n        _ExportButton(\n          title: LocaleKeys.shareAction_html.tr(),\n          svg: FlowySvgs.export_html_s,\n          onTap: () => _exportHTML(context),\n        ),\n        const VSpace(10),\n        _ExportButton(\n          title: LocaleKeys.shareAction_markdown.tr(),\n          svg: FlowySvgs.export_markdown_s,\n          onTap: () => _exportMarkdown(context),\n        ),\n        const VSpace(10),\n        _ExportButton(\n          title: LocaleKeys.shareAction_clipboard.tr(),\n          svg: FlowySvgs.duplicate_s,\n          onTap: () => _exportToClipboard(context),\n        ),\n        if (kDebugMode) ...[\n          const VSpace(10),\n          _ExportButton(\n            title: 'JSON (Debug Mode)',\n            svg: FlowySvgs.duplicate_s,\n            onTap: () => _exportJSON(context),\n          ),\n        ],\n      ],\n    );\n  }\n\n  Widget _buildDatabaseExportTab(BuildContext context) {\n    return Column(\n      children: [\n        const VSpace(10),\n        _ExportButton(\n          title: LocaleKeys.shareAction_csv.tr(),\n          svg: FlowySvgs.database_layout_s,\n          onTap: () => _exportCSV(context),\n        ),\n        if (kDebugMode) ...[\n          const VSpace(10),\n          _ExportButton(\n            title: 'Raw Database Data (Debug Mode)',\n            svg: FlowySvgs.duplicate_s,\n            onTap: () => _exportRawDatabaseData(context),\n          ),\n        ],\n      ],\n    );\n  }\n\n  Future<void> _exportHTML(BuildContext context) async {\n    final viewName = context.read<ShareBloc>().state.viewName;\n    final exportPath = await getIt<FilePickerService>().saveFile(\n      dialogTitle: '',\n      fileName: '${viewName.toFileName()}.html',\n    );\n    if (context.mounted && exportPath != null) {\n      context.read<ShareBloc>().add(\n            ShareEvent.share(\n              ShareType.html,\n              exportPath,\n            ),\n          );\n    }\n  }\n\n  Future<void> _exportMarkdown(BuildContext context) async {\n    final viewName = context.read<ShareBloc>().state.viewName;\n    final exportPath = await getIt<FilePickerService>().saveFile(\n      dialogTitle: '',\n      fileName: '${viewName.toFileName()}.zip',\n    );\n    if (context.mounted && exportPath != null) {\n      context.read<ShareBloc>().add(\n            ShareEvent.share(\n              ShareType.markdown,\n              exportPath,\n            ),\n          );\n    }\n  }\n\n  Future<void> _exportJSON(BuildContext context) async {\n    final viewName = context.read<ShareBloc>().state.viewName;\n    final exportPath = await getIt<FilePickerService>().saveFile(\n      dialogTitle: '',\n      fileName: '${viewName.toFileName()}.json',\n    );\n    if (context.mounted && exportPath != null) {\n      context.read<ShareBloc>().add(\n            ShareEvent.share(\n              ShareType.json,\n              exportPath,\n            ),\n          );\n    }\n  }\n\n  Future<void> _exportCSV(BuildContext context) async {\n    final viewName = context.read<ShareBloc>().state.viewName;\n    final exportPath = await getIt<FilePickerService>().saveFile(\n      dialogTitle: '',\n      fileName: '${viewName.toFileName()}.csv',\n    );\n    if (context.mounted && exportPath != null) {\n      context.read<ShareBloc>().add(\n            ShareEvent.share(\n              ShareType.csv,\n              exportPath,\n            ),\n          );\n    }\n  }\n\n  Future<void> _exportRawDatabaseData(BuildContext context) async {\n    final viewName = context.read<ShareBloc>().state.viewName;\n    final exportPath = await getIt<FilePickerService>().saveFile(\n      dialogTitle: '',\n      fileName: '${viewName.toFileName()}.json',\n    );\n    if (context.mounted && exportPath != null) {\n      context.read<ShareBloc>().add(\n            ShareEvent.share(\n              ShareType.rawDatabaseData,\n              exportPath,\n            ),\n          );\n    }\n  }\n\n  Future<void> _exportToClipboard(BuildContext context) async {\n    final documentExporter = DocumentExporter(context.read<ShareBloc>().view);\n    final result = await documentExporter.export(DocumentExportType.markdown);\n    result.fold(\n      (markdown) {\n        getIt<ClipboardService>().setData(\n          ClipboardServiceData(plainText: markdown),\n        );\n        showToastNotification(\n          message: LocaleKeys.message_copy_success.tr(),\n        );\n      },\n      (error) => showToastNotification(message: error.msg),\n    );\n  }\n}\n\nclass _ExportButton extends StatelessWidget {\n  const _ExportButton({\n    required this.title,\n    required this.svg,\n    required this.onTap,\n  });\n\n  final String title;\n  final FlowySvgData svg;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    final color = Theme.of(context).isLightMode\n        ? const Color(0x1E14171B)\n        : Colors.white.withValues(alpha: 0.1);\n    final radius = BorderRadius.circular(10.0);\n    return FlowyButton(\n      margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),\n      iconPadding: 12,\n      decoration: BoxDecoration(\n        border: Border.all(\n          color: color,\n        ),\n        borderRadius: radius,\n      ),\n      radius: radius,\n      text: FlowyText(\n        title,\n        lineHeight: 1.0,\n      ),\n      leftIcon: FlowySvg(svg),\n      onTap: onTap,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/shared/share/publish_color_extension.dart",
    "content": "import 'package:appflowy/util/theme_extension.dart';\nimport 'package:flutter/material.dart';\n\nclass ShareMenuColors {\n  static Color borderColor(BuildContext context) {\n    final borderColor = Theme.of(context).isLightMode\n        ? const Color(0x1E14171B)\n        : Colors.white.withValues(alpha: 0.1);\n    return borderColor;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/shared/share/publish_name_generator.dart",
    "content": "String replaceInvalidChars(String input) {\n  final RegExp invalidCharsRegex = RegExp('[^a-zA-Z0-9-]');\n  return input.replaceAll(invalidCharsRegex, '-');\n}\n\nFuture<String> generateNameSpace() async {\n  return '';\n}\n\n// The backend limits the publish name to a maximum of 120 characters.\n// If the combined length of the ID and the name exceeds 120 characters,\n// we will truncate the name to ensure the final result is within the limit.\n// The name should only contain alphanumeric characters and hyphens.\nFuture<String> generatePublishName(String id, String name) async {\n  if (name.length >= 120 - id.length) {\n    name = name.substring(0, 120 - id.length);\n  }\n  return replaceInvalidChars('$name-$id');\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/shared/share/constants.dart';\nimport 'package:appflowy/plugins/shared/share/publish_color_extension.dart';\nimport 'package:appflowy/plugins/shared/share/publish_name_generator.dart';\nimport 'package:appflowy/plugins/shared/share/share_bloc.dart';\nimport 'package:appflowy/shared/error_code/error_code_map.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flowy_infra_ui/widget/rounded_button.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass PublishTab extends StatelessWidget {\n  const PublishTab({\n    super.key,\n    required this.viewName,\n  });\n\n  final String viewName;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocConsumer<ShareBloc, ShareState>(\n      listener: (context, state) {\n        _showToast(context, state);\n      },\n      builder: (context, state) {\n        if (state.isPublished) {\n          return _PublishedWidget(\n            url: state.url,\n            pathName: state.pathName,\n            namespace: state.namespace,\n            onVisitSite: (url) => afLaunchUrlString(url),\n            onUnPublish: () {\n              context.read<ShareBloc>().add(const ShareEvent.unPublish());\n            },\n          );\n        } else {\n          return _PublishWidget(\n            onPublish: (selectedViews) async {\n              final id = context.read<ShareBloc>().view.id;\n              final lastPublishName = context.read<ShareBloc>().state.pathName;\n              final publishName = lastPublishName.orDefault(\n                await generatePublishName(\n                  id,\n                  viewName,\n                ),\n              );\n\n              if (selectedViews.isNotEmpty) {\n                Log.info(\n                  'Publishing views: ${selectedViews.map((e) => e.name)}',\n                );\n              }\n\n              if (context.mounted) {\n                context.read<ShareBloc>().add(\n                      ShareEvent.publish(\n                        '',\n                        publishName,\n                        selectedViews.map((e) => e.id).toList(),\n                      ),\n                    );\n              }\n            },\n          );\n        }\n      },\n    );\n  }\n\n  void _showToast(BuildContext context, ShareState state) {\n    if (state.publishResult != null) {\n      state.publishResult!.fold(\n        (value) => showToastNotification(\n          message: LocaleKeys.publish_publishSuccessfully.tr(),\n        ),\n        (error) => showToastNotification(\n          message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}',\n          type: ToastificationType.error,\n        ),\n      );\n    } else if (state.unpublishResult != null) {\n      state.unpublishResult!.fold(\n        (value) => showToastNotification(\n          message: LocaleKeys.publish_unpublishSuccessfully.tr(),\n        ),\n        (error) => showToastNotification(\n          message: LocaleKeys.publish_unpublishFailed.tr(),\n          description: error.msg,\n          type: ToastificationType.error,\n        ),\n      );\n    } else if (state.updatePathNameResult != null) {\n      state.updatePathNameResult!.fold(\n        (value) => showToastNotification(\n          message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),\n        ),\n        (error) {\n          Log.error('update path name failed: $error');\n\n          showToastNotification(\n            message: LocaleKeys.settings_sites_error_updatePathNameFailed.tr(),\n            type: ToastificationType.error,\n            description: error.code.publishErrorMessage,\n          );\n        },\n      );\n    }\n  }\n}\n\nclass _PublishedWidget extends StatefulWidget {\n  const _PublishedWidget({\n    required this.url,\n    required this.pathName,\n    required this.namespace,\n    required this.onVisitSite,\n    required this.onUnPublish,\n  });\n\n  final String url;\n  final String pathName;\n  final String namespace;\n  final void Function(String url) onVisitSite;\n  final VoidCallback onUnPublish;\n\n  @override\n  State<_PublishedWidget> createState() => _PublishedWidgetState();\n}\n\nclass _PublishedWidgetState extends State<_PublishedWidget> {\n  final controller = TextEditingController();\n\n  @override\n  void initState() {\n    super.initState();\n    controller.text = widget.pathName;\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        const VSpace(16),\n        const _PublishTabHeader(),\n        const VSpace(16),\n        _PublishUrl(\n          namespace: widget.namespace,\n          controller: controller,\n          onCopy: (_) {\n            final url = context.read<ShareBloc>().state.url;\n\n            getIt<ClipboardService>().setData(\n              ClipboardServiceData(plainText: url),\n            );\n\n            showToastNotification(\n              message: LocaleKeys.message_copy_success.tr(),\n            );\n          },\n          onSubmitted: (pathName) {\n            context.read<ShareBloc>().add(ShareEvent.updatePathName(pathName));\n          },\n        ),\n        const VSpace(16),\n        Row(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            const Spacer(),\n            UnPublishButton(\n              onUnPublish: widget.onUnPublish,\n            ),\n            const HSpace(6),\n            _buildVisitSiteButton(),\n          ],\n        ),\n      ],\n    );\n  }\n\n  Widget _buildVisitSiteButton() {\n    return RoundedTextButton(\n      width: 108,\n      height: 36,\n      onPressed: () {\n        final url = context.read<ShareBloc>().state.url;\n        widget.onVisitSite(url);\n      },\n      title: LocaleKeys.shareAction_visitSite.tr(),\n      borderRadius: const BorderRadius.all(Radius.circular(10)),\n      fillColor: Theme.of(context).colorScheme.primary,\n      hoverColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.9),\n      textColor: Theme.of(context).colorScheme.onPrimary,\n    );\n  }\n}\n\nclass UnPublishButton extends StatelessWidget {\n  const UnPublishButton({\n    super.key,\n    required this.onUnPublish,\n  });\n\n  final VoidCallback onUnPublish;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: 108,\n      height: 36,\n      child: FlowyButton(\n        decoration: BoxDecoration(\n          borderRadius: BorderRadius.circular(10),\n          border: Border.all(color: ShareMenuColors.borderColor(context)),\n        ),\n        radius: BorderRadius.circular(10),\n        text: FlowyText.regular(\n          lineHeight: 1.0,\n          LocaleKeys.shareAction_unPublish.tr(),\n          textAlign: TextAlign.center,\n        ),\n        onTap: onUnPublish,\n      ),\n    );\n  }\n}\n\nclass _PublishWidget extends StatefulWidget {\n  const _PublishWidget({\n    required this.onPublish,\n  });\n\n  final void Function(List<ViewPB> selectedViews) onPublish;\n\n  @override\n  State<_PublishWidget> createState() => _PublishWidgetState();\n}\n\nclass _PublishWidgetState extends State<_PublishWidget> {\n  List<ViewPB> _selectedViews = [];\n\n  @override\n  Widget build(BuildContext context) {\n    final accessLevel = context.read<PageAccessLevelBloc>().state.accessLevel;\n\n    Widget publishButton = PublishButton(\n      onPublish: () {\n        if (context.read<ShareBloc>().view.layout.isDatabaseView) {\n          // check if any database is selected\n          if (_selectedViews.isEmpty) {\n            showToastNotification(\n              message: LocaleKeys.publish_noDatabaseSelected.tr(),\n            );\n            return;\n          }\n        }\n\n        widget.onPublish(_selectedViews);\n      },\n    );\n\n    if (accessLevel == ShareAccessLevel.readOnly) {\n      // readonly user can't publish a page.\n      publishButton = FlowyTooltip(\n        message: 'You are a readonly user, you can\\'t publish a page.',\n        child: AbsorbPointer(\n          child: publishButton,\n        ),\n      );\n    }\n\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        const VSpace(16),\n        const _PublishTabHeader(),\n        const VSpace(16),\n        // if current view is a database, show the database selector\n        if (context.read<ShareBloc>().view.layout.isDatabaseView) ...[\n          _PublishDatabaseSelector(\n            view: context.read<ShareBloc>().view,\n            onSelected: (selectedDatabases) {\n              _selectedViews = selectedDatabases;\n            },\n          ),\n          const VSpace(16),\n        ],\n        publishButton,\n      ],\n    );\n  }\n}\n\nclass PublishButton extends StatelessWidget {\n  const PublishButton({\n    super.key,\n    required this.onPublish,\n  });\n\n  final VoidCallback onPublish;\n\n  @override\n  Widget build(BuildContext context) {\n    return PrimaryRoundedButton(\n      text: LocaleKeys.shareAction_publish.tr(),\n      useIntrinsicWidth: false,\n      margin: const EdgeInsets.symmetric(vertical: 9.0),\n      fontSize: 14.0,\n      figmaLineHeight: 18.0,\n      onTap: onPublish,\n    );\n  }\n}\n\nclass _PublishTabHeader extends StatelessWidget {\n  const _PublishTabHeader();\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Row(\n          children: [\n            const FlowySvg(FlowySvgs.share_publish_s),\n            const HSpace(6),\n            FlowyText(LocaleKeys.shareAction_publishToTheWeb.tr()),\n          ],\n        ),\n        const VSpace(4),\n        FlowyText.regular(\n          LocaleKeys.shareAction_publishToTheWebHint.tr(),\n          fontSize: 12,\n          maxLines: 3,\n          color: Theme.of(context).hintColor,\n        ),\n      ],\n    );\n  }\n}\n\nclass _PublishUrl extends StatefulWidget {\n  const _PublishUrl({\n    required this.namespace,\n    required this.controller,\n    required this.onCopy,\n    required this.onSubmitted,\n  });\n\n  final String namespace;\n  final TextEditingController controller;\n  final void Function(String url) onCopy;\n  final void Function(String url) onSubmitted;\n\n  @override\n  State<_PublishUrl> createState() => _PublishUrlState();\n}\n\nclass _PublishUrlState extends State<_PublishUrl> {\n  final focusNode = FocusNode();\n  bool showSaveButton = false;\n\n  @override\n  void initState() {\n    super.initState();\n    focusNode.addListener(_onFocusChanged);\n  }\n\n  void _onFocusChanged() => setState(() => showSaveButton = focusNode.hasFocus);\n\n  @override\n  void dispose() {\n    focusNode.removeListener(_onFocusChanged);\n    focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 36,\n      child: FlowyTextField(\n        autoFocus: false,\n        controller: widget.controller,\n        focusNode: focusNode,\n        enableBorderColor: ShareMenuColors.borderColor(context),\n        prefixIcon: _buildPrefixIcon(context),\n        suffixIcon: _buildSuffixIcon(context),\n        textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(\n              fontSize: 14,\n              height: 18.0 / 14.0,\n            ),\n      ),\n    );\n  }\n\n  Widget _buildPrefixIcon(BuildContext context) {\n    return ConstrainedBox(\n      constraints: const BoxConstraints(maxWidth: 230),\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          const HSpace(8.0),\n          Flexible(\n            child: FlowyText.regular(\n              ShareConstants.buildNamespaceUrl(\n                nameSpace: '${widget.namespace}/',\n              ),\n              fontSize: 14,\n              figmaLineHeight: 18.0,\n              overflow: TextOverflow.ellipsis,\n            ),\n          ),\n          const HSpace(6.0),\n          const Padding(\n            padding: EdgeInsets.symmetric(vertical: 2.0),\n            child: VerticalDivider(\n              thickness: 1.0,\n              width: 1.0,\n            ),\n          ),\n          const HSpace(6.0),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildSuffixIcon(BuildContext context) {\n    return showSaveButton\n        ? _buildSaveButton(context)\n        : _buildCopyLinkIcon(context);\n  }\n\n  Widget _buildSaveButton(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6),\n      child: FlowyButton(\n        useIntrinsicWidth: true,\n        text: FlowyText.regular(\n          LocaleKeys.button_save.tr(),\n          figmaLineHeight: 18.0,\n        ),\n        onTap: () {\n          widget.onSubmitted(widget.controller.text);\n          focusNode.unfocus();\n        },\n      ),\n    );\n  }\n\n  Widget _buildCopyLinkIcon(BuildContext context) {\n    return FlowyHover(\n      style: const HoverStyle(\n        contentMargin: EdgeInsets.all(4),\n      ),\n      child: GestureDetector(\n        behavior: HitTestBehavior.opaque,\n        onTap: () => widget.onCopy(widget.controller.text),\n        child: Container(\n          width: 32,\n          height: 32,\n          alignment: Alignment.center,\n          padding: const EdgeInsets.all(6),\n          decoration: const BoxDecoration(\n            border: Border(left: BorderSide(color: Color(0x141F2329))),\n          ),\n          child: const FlowySvg(\n            FlowySvgs.m_toolbar_link_m,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\n// used to select which database view should be published\nclass _PublishDatabaseSelector extends StatefulWidget {\n  const _PublishDatabaseSelector({\n    required this.view,\n    required this.onSelected,\n  });\n\n  final ViewPB view;\n  final void Function(List<ViewPB> selectedDatabases) onSelected;\n\n  @override\n  State<_PublishDatabaseSelector> createState() =>\n      _PublishDatabaseSelectorState();\n}\n\nclass _PublishDatabaseSelectorState extends State<_PublishDatabaseSelector> {\n  final PropertyValueNotifier<List<(ViewPB, bool)>> _databaseStatus =\n      PropertyValueNotifier<List<(ViewPB, bool)>>([]);\n  late final _borderColor = Theme.of(context).hintColor.withValues(alpha: 0.3);\n\n  @override\n  void initState() {\n    super.initState();\n\n    _databaseStatus.addListener(_onDatabaseStatusChanged);\n    _databaseStatus.value = context\n        .read<DatabaseTabBarBloc>()\n        .state\n        .tabBars\n        .map((e) => (e.view, true))\n        .toList();\n  }\n\n  void _onDatabaseStatusChanged() {\n    final selectedDatabases =\n        _databaseStatus.value.where((e) => e.$2).map((e) => e.$1).toList();\n    widget.onSelected(selectedDatabases);\n  }\n\n  @override\n  void dispose() {\n    _databaseStatus.removeListener(_onDatabaseStatusChanged);\n    _databaseStatus.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(\n      builder: (context, state) {\n        return Container(\n          clipBehavior: Clip.antiAlias,\n          decoration: ShapeDecoration(\n            shape: RoundedRectangleBorder(\n              side: BorderSide(color: _borderColor),\n              borderRadius: BorderRadius.circular(8),\n            ),\n          ),\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              const VSpace(10),\n              _buildSelectedDatabaseCount(context),\n              const VSpace(10),\n              _buildDivider(context),\n              const VSpace(10),\n              ...state.tabBars.map(\n                (e) => _buildDatabaseSelector(context, e),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildDivider(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 12),\n      child: Divider(\n        color: _borderColor,\n        thickness: 1,\n        height: 1,\n      ),\n    );\n  }\n\n  Widget _buildSelectedDatabaseCount(BuildContext context) {\n    return ValueListenableBuilder(\n      valueListenable: _databaseStatus,\n      builder: (context, selectedDatabases, child) {\n        final count = selectedDatabases.where((e) => e.$2).length;\n        return Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 12),\n          child: FlowyText(\n            LocaleKeys.publish_database.plural(count).tr(),\n            color: Theme.of(context).hintColor,\n            fontSize: 13,\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildDatabaseSelector(BuildContext context, DatabaseTabBar tabBar) {\n    final isPrimaryDatabase = tabBar.view.id == widget.view.id;\n    return ValueListenableBuilder(\n      valueListenable: _databaseStatus,\n      builder: (context, selectedDatabases, child) {\n        final isSelected = selectedDatabases.any(\n          (e) => e.$1.id == tabBar.view.id && e.$2,\n        );\n        return _DatabaseSelectorItem(\n          tabBar: tabBar,\n          isSelected: isSelected,\n          isPrimaryDatabase: isPrimaryDatabase,\n          onTap: () {\n            // unable to deselect the primary database\n            if (isPrimaryDatabase) {\n              showToastNotification(\n                message:\n                    LocaleKeys.publish_unableToDeselectPrimaryDatabase.tr(),\n              );\n              return;\n            }\n\n            // toggle the selection status\n            _databaseStatus.value = _databaseStatus.value\n                .map(\n                  (e) =>\n                      e.$1.id == tabBar.view.id ? (e.$1, !e.$2) : (e.$1, e.$2),\n                )\n                .toList();\n          },\n        );\n      },\n    );\n  }\n}\n\nclass _DatabaseSelectorItem extends StatelessWidget {\n  const _DatabaseSelectorItem({\n    required this.tabBar,\n    required this.isSelected,\n    required this.onTap,\n    required this.isPrimaryDatabase,\n  });\n\n  final DatabaseTabBar tabBar;\n  final bool isSelected;\n  final VoidCallback onTap;\n  final bool isPrimaryDatabase;\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child = _buildItem(context);\n\n    if (!isPrimaryDatabase) {\n      child = FlowyHover(\n        child: GestureDetector(\n          behavior: HitTestBehavior.opaque,\n          onTap: onTap,\n          child: child,\n        ),\n      );\n    } else {\n      child = FlowyTooltip(\n        message: LocaleKeys.publish_mustSelectPrimaryDatabase.tr(),\n        child: MouseRegion(\n          cursor: SystemMouseCursors.forbidden,\n          child: child,\n        ),\n      );\n    }\n\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),\n      child: child,\n    );\n  }\n\n  Widget _buildItem(BuildContext context) {\n    final svg = isPrimaryDatabase\n        ? FlowySvgs.unable_select_s\n        : isSelected\n            ? FlowySvgs.check_filled_s\n            : FlowySvgs.uncheck_s;\n    final blendMode = isPrimaryDatabase ? BlendMode.srcIn : null;\n    return Container(\n      height: 30,\n      padding: const EdgeInsets.symmetric(horizontal: 8),\n      child: Row(\n        children: [\n          FlowySvg(\n            svg,\n            blendMode: blendMode,\n            size: const Size.square(18),\n          ),\n          const HSpace(9.0),\n          FlowySvg(\n            tabBar.view.layout.icon,\n            size: const Size.square(16),\n          ),\n          const HSpace(6.0),\n          FlowyText.regular(\n            tabBar.view.name,\n            fontSize: 14,\n            overflow: TextOverflow.ellipsis,\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/workspace/application/export/document_exporter.dart';\nimport 'package:appflowy/workspace/application/settings/share/export_service.dart';\nimport 'package:appflowy/workspace/application/view/view_listener.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport 'constants.dart';\n\npart 'share_bloc.freezed.dart';\n\nclass ShareBloc extends Bloc<ShareEvent, ShareState> {\n  ShareBloc({\n    required this.view,\n  }) : super(ShareState.initial()) {\n    on<ShareEvent>((event, emit) async {\n      await event.when(\n        initial: () async {\n          viewListener = ViewListener(viewId: view.id)\n            ..start(\n              onViewUpdated: (value) {\n                add(ShareEvent.updateViewName(value.name, value.id));\n              },\n              onViewMoveToTrash: (p0) {\n                add(const ShareEvent.setPublishStatus(false));\n              },\n            );\n\n          add(const ShareEvent.updatePublishStatus());\n        },\n        share: (type, path) async => _share(\n          type,\n          path,\n          emit,\n        ),\n        publish: (nameSpace, publishName, selectedViewIds) => _publish(\n          nameSpace,\n          publishName,\n          selectedViewIds,\n          emit,\n        ),\n        unPublish: () async => _unpublish(emit),\n        updatePublishStatus: () async => _updatePublishStatus(emit),\n        updateViewName: (viewName, viewId) async {\n          emit(\n            state.copyWith(\n              viewName: viewName,\n              viewId: viewId,\n              updatePathNameResult: null,\n              publishResult: null,\n              unpublishResult: null,\n            ),\n          );\n        },\n        setPublishStatus: (isPublished) {\n          emit(\n            state.copyWith(\n              isPublished: isPublished,\n              url: isPublished ? state.url : '',\n            ),\n          );\n        },\n        updatePathName: (pathName) async => _updatePathName(\n          pathName,\n          emit,\n        ),\n        clearPathNameResult: () async {\n          emit(\n            state.copyWith(\n              updatePathNameResult: null,\n            ),\n          );\n        },\n      );\n    });\n  }\n\n  final ViewPB view;\n  late final ViewListener viewListener;\n\n  late final documentExporter = DocumentExporter(view);\n\n  @override\n  Future<void> close() async {\n    await viewListener.stop();\n    return super.close();\n  }\n\n  Future<void> _share(\n    ShareType type,\n    String? path,\n    Emitter<ShareState> emit,\n  ) async {\n    if (ShareType.unimplemented.contains(type)) {\n      Log.error('DocumentShareType $type is not implemented');\n      return;\n    }\n\n    emit(state.copyWith(isLoading: true));\n\n    final result = await _export(type, path);\n\n    emit(\n      state.copyWith(\n        isLoading: false,\n        exportResult: result,\n      ),\n    );\n  }\n\n  Future<void> _publish(\n    String nameSpace,\n    String publishName,\n    List<String> selectedViewIds,\n    Emitter<ShareState> emit,\n  ) async {\n    // set space name\n    try {\n      final result =\n          await ViewBackendService.getPublishNameSpace().getOrThrow();\n\n      await ViewBackendService.publish(\n        view,\n        name: publishName,\n        selectedViewIds: selectedViewIds,\n      ).getOrThrow();\n\n      emit(\n        state.copyWith(\n          isPublished: true,\n          publishResult: FlowySuccess(null),\n          unpublishResult: null,\n          namespace: result.namespace,\n          pathName: publishName,\n          url: ShareConstants.buildPublishUrl(\n            nameSpace: result.namespace,\n            publishName: publishName,\n          ),\n        ),\n      );\n\n      Log.info('publish success: ${result.namespace}/$publishName');\n    } catch (e) {\n      Log.error('publish error: $e');\n\n      emit(\n        state.copyWith(\n          isPublished: false,\n          publishResult: FlowyResult.failure(\n            FlowyError(msg: 'publish error: $e'),\n          ),\n          unpublishResult: null,\n          url: '',\n        ),\n      );\n    }\n  }\n\n  Future<void> _unpublish(Emitter<ShareState> emit) async {\n    emit(\n      state.copyWith(\n        publishResult: null,\n        unpublishResult: null,\n      ),\n    );\n\n    final result = await ViewBackendService.unpublish(view);\n    final isPublished = !result.isSuccess;\n    result.onFailure((f) {\n      Log.error('unpublish error: $f');\n    });\n\n    emit(\n      state.copyWith(\n        isPublished: isPublished,\n        publishResult: null,\n        unpublishResult: result,\n        url: result.fold((_) => '', (_) => state.url),\n      ),\n    );\n  }\n\n  Future<void> _updatePublishStatus(Emitter<ShareState> emit) async {\n    final publishInfo = await ViewBackendService.getPublishInfo(view);\n    final enablePublish = await UserBackendService.getCurrentUserProfile().fold(\n      (v) => v.workspaceType == WorkspaceTypePB.ServerW,\n      (p) => false,\n    );\n\n    // skip the \"Record not found\" error, it's because the view is not published yet\n    publishInfo.fold(\n      (s) {\n        Log.info(\n          'get publish info success: $publishInfo for view: ${view.name}(${view.id})',\n        );\n      },\n      (f) {\n        if (![\n          ErrorCode.RecordNotFound,\n          ErrorCode.LocalVersionNotSupport,\n        ].contains(f.code)) {\n          Log.info(\n            'get publish info failed: $f for view: ${view.name}(${view.id})',\n          );\n        }\n      },\n    );\n\n    String workspaceId = state.workspaceId;\n    if (workspaceId.isEmpty) {\n      workspaceId = await UserBackendService.getCurrentWorkspace().fold(\n        (s) => s.id,\n        (f) => '',\n      );\n    }\n\n    final (isPublished, namespace, pathName, url) = publishInfo.fold(\n      (s) {\n        return (\n          // if the unpublishedAtTimestampSec is not set, it means the view is not unpublished.\n          !s.hasUnpublishedAtTimestampSec(),\n          s.namespace,\n          s.publishName,\n          ShareConstants.buildPublishUrl(\n            nameSpace: s.namespace,\n            publishName: s.publishName,\n          ),\n        );\n      },\n      (f) => (false, '', '', ''),\n    );\n\n    emit(\n      state.copyWith(\n        isPublished: isPublished,\n        namespace: namespace,\n        pathName: pathName,\n        url: url,\n        viewName: view.name,\n        enablePublish: enablePublish,\n        workspaceId: workspaceId,\n        viewId: view.id,\n      ),\n    );\n  }\n\n  Future<void> _updatePathName(\n    String pathName,\n    Emitter<ShareState> emit,\n  ) async {\n    emit(\n      state.copyWith(\n        updatePathNameResult: null,\n      ),\n    );\n\n    if (pathName.isEmpty) {\n      emit(\n        state.copyWith(\n          updatePathNameResult: FlowyResult.failure(\n            FlowyError(\n              code: ErrorCode.ViewNameInvalid,\n              msg: 'Path name is invalid',\n            ),\n          ),\n        ),\n      );\n      return;\n    }\n\n    final request = SetPublishNamePB()\n      ..viewId = view.id\n      ..newName = pathName;\n    final result = await FolderEventSetPublishName(request).send();\n    emit(\n      state.copyWith(\n        updatePathNameResult: result,\n        publishResult: null,\n        unpublishResult: null,\n        pathName: result.fold(\n          (_) => pathName,\n          (f) => state.pathName,\n        ),\n        url: result.fold(\n          (s) => ShareConstants.buildPublishUrl(\n            nameSpace: state.namespace,\n            publishName: pathName,\n          ),\n          (f) => state.url,\n        ),\n      ),\n    );\n  }\n\n  Future<FlowyResult<ShareType, FlowyError>> _export(\n    ShareType type,\n    String? path,\n  ) async {\n    final FlowyResult<String, FlowyError> result;\n    if (type == ShareType.csv) {\n      final exportResult = await BackendExportService.exportDatabaseAsCSV(\n        view.id,\n      );\n      result = exportResult.fold(\n        (s) => FlowyResult.success(s.data),\n        (f) => FlowyResult.failure(f),\n      );\n    } else if (type == ShareType.rawDatabaseData) {\n      final exportResult = await BackendExportService.exportDatabaseAsRawData(\n        view.id,\n      );\n      result = exportResult.fold(\n        (s) => FlowyResult.success(s.data),\n        (f) => FlowyResult.failure(f),\n      );\n    } else {\n      result =\n          await documentExporter.export(type.documentExportType, path: path);\n    }\n    return result.fold(\n      (s) {\n        if (path != null) {\n          switch (type) {\n            case ShareType.html:\n            case ShareType.csv:\n            case ShareType.json:\n            case ShareType.rawDatabaseData:\n              File(path).writeAsStringSync(s);\n              return FlowyResult.success(type);\n            case ShareType.markdown:\n              return FlowyResult.success(type);\n            default:\n              break;\n          }\n        }\n        return FlowyResult.failure(FlowyError());\n      },\n      (f) => FlowyResult.failure(f),\n    );\n  }\n}\n\nenum ShareType {\n  // available in document\n  markdown,\n  html,\n  text,\n  link,\n  json,\n\n  // only available in database\n  csv,\n  rawDatabaseData;\n\n  static List<ShareType> get unimplemented => [link];\n\n  DocumentExportType get documentExportType {\n    switch (this) {\n      case ShareType.markdown:\n        return DocumentExportType.markdown;\n      case ShareType.html:\n        return DocumentExportType.html;\n      case ShareType.text:\n        return DocumentExportType.text;\n      case ShareType.json:\n        return DocumentExportType.json;\n      case ShareType.csv:\n        throw UnsupportedError('DocumentShareType.csv is not supported');\n      case ShareType.link:\n        throw UnsupportedError('DocumentShareType.link is not supported');\n      case ShareType.rawDatabaseData:\n        throw UnsupportedError(\n          'DocumentShareType.rawDatabaseData is not supported',\n        );\n    }\n  }\n}\n\n@freezed\nclass ShareEvent with _$ShareEvent {\n  const factory ShareEvent.initial() = _Initial;\n\n  const factory ShareEvent.share(\n    ShareType type,\n    String? path,\n  ) = _Share;\n\n  const factory ShareEvent.publish(\n    String nameSpace,\n    String pageId,\n    List<String> selectedViewIds,\n  ) = _Publish;\n\n  const factory ShareEvent.unPublish() = _UnPublish;\n\n  const factory ShareEvent.updateViewName(String name, String viewId) =\n      _UpdateViewName;\n\n  const factory ShareEvent.updatePublishStatus() = _UpdatePublishStatus;\n\n  const factory ShareEvent.setPublishStatus(bool isPublished) =\n      _SetPublishStatus;\n\n  const factory ShareEvent.updatePathName(String pathName) = _UpdatePathName;\n\n  const factory ShareEvent.clearPathNameResult() = _ClearPathNameResult;\n}\n\n@freezed\nclass ShareState with _$ShareState {\n  const factory ShareState({\n    required bool isPublished,\n    required bool isLoading,\n    required String url,\n    required String viewName,\n    required bool enablePublish,\n    FlowyResult<ShareType, FlowyError>? exportResult,\n    FlowyResult<void, FlowyError>? publishResult,\n    FlowyResult<void, FlowyError>? unpublishResult,\n    FlowyResult<void, FlowyError>? updatePathNameResult,\n    required String viewId,\n    required String workspaceId,\n    required String namespace,\n    required String pathName,\n  }) = _ShareState;\n\n  factory ShareState.initial() => const ShareState(\n        isLoading: false,\n        isPublished: false,\n        enablePublish: true,\n        url: '',\n        viewName: '',\n        viewId: '',\n        workspaceId: '',\n        namespace: '',\n        pathName: '',\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart",
    "content": "import 'package:appflowy/features/share_tab/data/repositories/rust_share_with_user_repository_impl.dart';\nimport 'package:appflowy/features/share_tab/logic/share_tab_bloc.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';\nimport 'package:appflowy/plugins/shared/share/_shared.dart';\nimport 'package:appflowy/plugins/shared/share/share_bloc.dart';\nimport 'package:appflowy/plugins/shared/share/share_menu.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass ShareButton extends StatelessWidget {\n  const ShareButton({\n    super.key,\n    required this.view,\n  });\n\n  final ViewPB view;\n\n  @override\n  Widget build(BuildContext context) {\n    final workspaceBloc = context.read<UserWorkspaceBloc>();\n    final workspaceId = workspaceBloc.state.currentWorkspace?.workspaceId ?? '';\n    final workspaceType = workspaceBloc.state.currentWorkspace?.workspaceType;\n\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider(\n          create: (context) =>\n              getIt<ShareBloc>(param1: view)..add(const ShareEvent.initial()),\n        ),\n        if (view.layout.isDatabaseView)\n          BlocProvider(\n            create: (context) => DatabaseTabBarBloc(\n              view: view,\n              compactModeId: view.id,\n              enableCompactMode: false,\n            )..add(const DatabaseTabBarEvent.initial()),\n          ),\n        BlocProvider(\n          create: (context) {\n            final bloc = ShareTabBloc(\n              repository: RustShareWithUserRepositoryImpl(),\n              pageId: view.id,\n              workspaceId: workspaceId,\n            );\n\n            if (workspaceType != WorkspaceTypePB.LocalW) {\n              bloc.add(ShareTabEvent.initialize());\n            }\n\n            return bloc;\n          },\n        ),\n      ],\n      child: BlocListener<ShareBloc, ShareState>(\n        listener: (context, state) {\n          if (!state.isLoading && state.exportResult != null) {\n            state.exportResult!.fold(\n              (data) => _handleExportSuccess(context, data),\n              (error) => _handleExportError(context, error),\n            );\n          }\n        },\n        child: BlocBuilder<ShareBloc, ShareState>(\n          builder: (context, state) {\n            final tabs = [\n              if (state.enablePublish) ...[\n                // share the same permission with publish\n                ShareMenuTab.share,\n                ShareMenuTab.publish,\n              ],\n              ShareMenuTab.exportAs,\n            ];\n\n            return ShareMenuButton(tabs: tabs);\n          },\n        ),\n      ),\n    );\n  }\n\n  void _handleExportSuccess(BuildContext context, ShareType shareType) {\n    switch (shareType) {\n      case ShareType.markdown:\n      case ShareType.html:\n      case ShareType.csv:\n        showToastNotification(\n          message: LocaleKeys.settings_files_exportFileSuccess.tr(),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  void _handleExportError(BuildContext context, FlowyError error) {\n    showToastNotification(\n      message:\n          '${LocaleKeys.settings_files_exportFileFail.tr()}: ${error.code}',\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/shared/share/share_menu.dart",
    "content": "import 'package:appflowy/features/share_tab/presentation/share_tab.dart'\n    as share_section;\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart';\nimport 'package:appflowy/plugins/shared/share/export_tab.dart';\nimport 'package:appflowy/plugins/shared/share/share_bloc.dart';\nimport 'package:appflowy/plugins/shared/share/share_tab.dart' as share_plugin;\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/workspace/application/settings/plan/settings_plan_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pbenum.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'publish_tab.dart';\n\nenum ShareMenuTab {\n  share,\n  publish,\n  exportAs;\n\n  String get i18n {\n    switch (this) {\n      case ShareMenuTab.share:\n        return LocaleKeys.shareAction_shareTab.tr();\n      case ShareMenuTab.publish:\n        return LocaleKeys.shareAction_publishTab.tr();\n      case ShareMenuTab.exportAs:\n        return LocaleKeys.shareAction_exportAsTab.tr();\n    }\n  }\n}\n\nclass ShareMenu extends StatefulWidget {\n  const ShareMenu({\n    super.key,\n    required this.tabs,\n    required this.viewName,\n    required this.onClose,\n  });\n\n  final List<ShareMenuTab> tabs;\n  final String viewName;\n  final VoidCallback onClose;\n\n  @override\n  State<ShareMenu> createState() => _ShareMenuState();\n}\n\nclass _ShareMenuState extends State<ShareMenu>\n    with SingleTickerProviderStateMixin {\n  late ShareMenuTab selectedTab = widget.tabs.first;\n  late final tabController = TabController(\n    length: widget.tabs.length,\n    vsync: this,\n    initialIndex: widget.tabs.indexOf(selectedTab),\n  );\n\n  @override\n  Widget build(BuildContext context) {\n    if (widget.tabs.isEmpty) {\n      return const SizedBox.shrink();\n    }\n\n    final theme = AppFlowyTheme.of(context);\n\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        VSpace(theme.spacing.xs),\n        Container(\n          alignment: Alignment.centerLeft,\n          height: 28,\n          child: _buildTabBar(context),\n        ),\n        const AFDivider(),\n        Padding(\n          padding: EdgeInsets.symmetric(horizontal: theme.spacing.m),\n          child: _buildTab(context),\n        ),\n      ],\n    );\n  }\n\n  @override\n  void dispose() {\n    tabController.dispose();\n    super.dispose();\n  }\n\n  Widget _buildTabBar(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final children = [\n      for (final tab in widget.tabs)\n        Padding(\n          padding: EdgeInsets.only(bottom: theme.spacing.s),\n          child: _Segment(\n            tab: tab,\n            isSelected: selectedTab == tab,\n          ),\n        ),\n    ];\n    return TabBar(\n      indicatorSize: TabBarIndicatorSize.label,\n      indicator: RoundUnderlineTabIndicator(\n        width: 68.0,\n        borderSide: BorderSide(\n          color: Theme.of(context).colorScheme.primary,\n          width: 3,\n        ),\n        insets: const EdgeInsets.only(bottom: -1),\n      ),\n      isScrollable: true,\n      controller: tabController,\n      tabs: children,\n      onTap: (index) {\n        setState(() {\n          selectedTab = widget.tabs[index];\n        });\n      },\n    );\n  }\n\n  Widget _buildTab(BuildContext context) {\n    switch (selectedTab) {\n      case ShareMenuTab.publish:\n        return PublishTab(\n          viewName: widget.viewName,\n        );\n      case ShareMenuTab.exportAs:\n        return const ExportTab();\n      case ShareMenuTab.share:\n        if (FeatureFlag.sharedSection.isOn) {\n          final workspace =\n              context.read<UserWorkspaceBloc>().state.currentWorkspace;\n          final workspaceId = workspace?.workspaceId ??\n              context.read<ShareBloc>().state.workspaceId;\n          final pageId = context.read<ShareBloc>().state.viewId;\n          final isInProPlan = context\n                  .read<UserWorkspaceBloc>()\n                  .state\n                  .workspaceSubscriptionInfo\n                  ?.plan ==\n              WorkspacePlanPB.ProPlan;\n\n          return share_section.ShareTab(\n            workspaceId: workspaceId,\n            pageId: pageId,\n            workspaceName: workspace?.name ?? '',\n            workspaceIcon: workspace?.icon ?? '',\n            isInProPlan: isInProPlan,\n            onUpgradeToPro: () {\n              widget.onClose();\n\n              _showUpgradeToProDialog(context);\n            },\n          );\n        }\n\n        return const share_plugin.ShareTab();\n    }\n  }\n\n  void _showUpgradeToProDialog(BuildContext context) {\n    final state = context.read<UserWorkspaceBloc>().state;\n    final workspace = state.currentWorkspace;\n    if (workspace == null) {\n      Log.error('workspace is null');\n      return;\n    }\n\n    final workspaceId = workspace.workspaceId;\n    final subscriptionInfo = state.workspaceSubscriptionInfo;\n    final userProfile = state.userProfile;\n    if (subscriptionInfo == null) {\n      Log.error('subscriptionInfo is null');\n      return;\n    }\n\n    final role = workspace.role;\n    final title = switch (role) {\n      AFRolePB.Owner =>\n        LocaleKeys.shareTab_upgradeToInviteGuest_title_owner.tr(),\n      AFRolePB.Member =>\n        LocaleKeys.shareTab_upgradeToInviteGuest_title_member.tr(),\n      AFRolePB.Guest ||\n      _ =>\n        LocaleKeys.shareTab_upgradeToInviteGuest_title_guest.tr(),\n    };\n    final description = switch (role) {\n      AFRolePB.Owner =>\n        LocaleKeys.shareTab_upgradeToInviteGuest_description_owner.tr(),\n      AFRolePB.Member =>\n        LocaleKeys.shareTab_upgradeToInviteGuest_description_member.tr(),\n      AFRolePB.Guest ||\n      _ =>\n        LocaleKeys.shareTab_upgradeToInviteGuest_description_guest.tr(),\n    };\n    final style = switch (role) {\n      AFRolePB.Owner => ConfirmPopupStyle.cancelAndOk,\n      AFRolePB.Member || AFRolePB.Guest || _ => ConfirmPopupStyle.onlyOk,\n    };\n    final confirmLabel = switch (role) {\n      AFRolePB.Owner => LocaleKeys.shareTab_upgrade.tr(),\n      AFRolePB.Member || AFRolePB.Guest || _ => LocaleKeys.button_ok.tr(),\n    };\n\n    if (role == AFRolePB.Owner) {\n      showDialog(\n        context: context,\n        builder: (_) => BlocProvider<SettingsPlanBloc>(\n          create: (_) => SettingsPlanBloc(\n            workspaceId: workspaceId,\n            userId: userProfile.id,\n          )..add(const SettingsPlanEvent.started()),\n          child: SettingsPlanComparisonDialog(\n            workspaceId: workspaceId,\n            subscriptionInfo: subscriptionInfo,\n          ),\n        ),\n      );\n    } else {\n      showConfirmDialog(\n        context: Navigator.of(context, rootNavigator: true).context,\n        title: title,\n        description: description,\n        style: style,\n        confirmLabel: confirmLabel,\n        confirmButtonColor: Theme.of(context).colorScheme.primary,\n        onConfirm: (context) {\n          // fixme: show the upgrade to pro dialog\n        },\n      );\n    }\n  }\n}\n\nclass _Segment extends StatefulWidget {\n  const _Segment({\n    required this.tab,\n    required this.isSelected,\n  });\n\n  final bool isSelected;\n  final ShareMenuTab tab;\n\n  @override\n  State<_Segment> createState() => _SegmentState();\n}\n\nclass _SegmentState extends State<_Segment> {\n  bool isHovered = false;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    final textColor = widget.isSelected || isHovered\n        ? theme.textColorScheme.primary\n        : theme.textColorScheme.secondary;\n\n    Widget child = MouseRegion(\n      onEnter: (_) => setState(() => isHovered = true),\n      onExit: (_) => setState(() => isHovered = false),\n      child: Text(\n        widget.tab.i18n,\n        textAlign: TextAlign.center,\n        style: theme.textStyle.body.enhanced(\n          color: textColor,\n        ),\n      ),\n    );\n\n    if (widget.tab == ShareMenuTab.publish) {\n      final isPublished = context.watch<ShareBloc>().state.isPublished;\n      // show checkmark icon if published\n      if (isPublished) {\n        child = Row(\n          children: [\n            const FlowySvg(\n              FlowySvgs.published_checkmark_s,\n              blendMode: null,\n            ),\n            const HSpace(6),\n            child,\n          ],\n        );\n      }\n    }\n\n    return child;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/shared/share/share_bloc.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'constants.dart';\n\nclass ShareTab extends StatelessWidget {\n  const ShareTab({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return const Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        VSpace(18),\n        _ShareTabHeader(),\n        VSpace(2),\n        _ShareTabDescription(),\n        VSpace(14),\n        _ShareTabContent(),\n      ],\n    );\n  }\n}\n\nclass _ShareTabHeader extends StatelessWidget {\n  const _ShareTabHeader();\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        const FlowySvg(FlowySvgs.share_tab_icon_s),\n        const HSpace(6),\n        FlowyText.medium(\n          LocaleKeys.shareAction_shareTabTitle.tr(),\n          figmaLineHeight: 18.0,\n        ),\n      ],\n    );\n  }\n}\n\nclass _ShareTabDescription extends StatelessWidget {\n  const _ShareTabDescription();\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 2.0),\n      child: FlowyText.regular(\n        LocaleKeys.shareAction_shareTabDescription.tr(),\n        fontSize: 13.0,\n        figmaLineHeight: 18.0,\n        color: Theme.of(context).hintColor,\n      ),\n    );\n  }\n}\n\nclass _ShareTabContent extends StatelessWidget {\n  const _ShareTabContent();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<ShareBloc, ShareState>(\n      builder: (context, state) {\n        final shareUrl = ShareConstants.buildShareUrl(\n          workspaceId: state.workspaceId,\n          viewId: state.viewId,\n        );\n        return Row(\n          children: [\n            Expanded(\n              child: SizedBox(\n                height: 36,\n                child: FlowyTextField(\n                  text: shareUrl, // todo: add workspace id + view id\n                  readOnly: true,\n                  borderRadius: BorderRadius.circular(10),\n                ),\n              ),\n            ),\n            const HSpace(8.0),\n            PrimaryRoundedButton(\n              margin: const EdgeInsets.symmetric(\n                vertical: 9.0,\n                horizontal: 14.0,\n              ),\n              text: LocaleKeys.button_copyLink.tr(),\n              figmaLineHeight: 18.0,\n              leftIcon: FlowySvg(\n                FlowySvgs.share_tab_copy_s,\n                color: Theme.of(context).colorScheme.onPrimary,\n              ),\n              onTap: () => _copy(context, shareUrl),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  void _copy(BuildContext context, String url) {\n    getIt<ClipboardService>().setData(\n      ClipboardServiceData(plainText: url),\n    );\n\n    showToastNotification(\n      message: LocaleKeys.message_copy_success.tr(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/shared/sync_indicator.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/application/sync/database_sync_bloc.dart';\nimport 'package:appflowy/plugins/document/application/document_sync_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass DocumentSyncIndicator extends StatelessWidget {\n  const DocumentSyncIndicator({\n    super.key,\n    required this.view,\n  });\n\n  final ViewPB view;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) =>\n          DocumentSyncBloc(view: view)..add(const DocumentSyncEvent.initial()),\n      child: BlocBuilder<DocumentSyncBloc, DocumentSyncBlocState>(\n        builder: (context, state) {\n          // don't show indicator if user is local\n          if (!state.shouldShowIndicator) {\n            return const SizedBox.shrink();\n          }\n          final Color color;\n          final String hintText;\n\n          if (!state.isNetworkConnected) {\n            color = Colors.grey;\n            hintText = LocaleKeys.newSettings_syncState_noNetworkConnected.tr();\n          } else {\n            switch (state.syncState) {\n              case DocumentSyncState.SyncFinished:\n                color = Colors.green;\n                hintText = LocaleKeys.newSettings_syncState_synced.tr();\n                break;\n              case DocumentSyncState.Syncing:\n              case DocumentSyncState.InitSyncBegin:\n                color = Colors.yellow;\n                hintText = LocaleKeys.newSettings_syncState_syncing.tr();\n                break;\n              default:\n                return const SizedBox.shrink();\n            }\n          }\n\n          return FlowyTooltip(\n            message: hintText,\n            child: Container(\n              decoration: BoxDecoration(\n                shape: BoxShape.circle,\n                color: color,\n              ),\n              width: 8,\n              height: 8,\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass DatabaseSyncIndicator extends StatelessWidget {\n  const DatabaseSyncIndicator({\n    super.key,\n    required this.view,\n  });\n\n  final ViewPB view;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) =>\n          DatabaseSyncBloc(view: view)..add(const DatabaseSyncEvent.initial()),\n      child: BlocBuilder<DatabaseSyncBloc, DatabaseSyncBlocState>(\n        builder: (context, state) {\n          // don't show indicator if user is local\n          if (!state.shouldShowIndicator) {\n            return const SizedBox.shrink();\n          }\n          final Color color;\n          final String hintText;\n\n          if (!state.isNetworkConnected) {\n            color = Colors.grey;\n            hintText = LocaleKeys.newSettings_syncState_noNetworkConnected.tr();\n          } else {\n            switch (state.syncState) {\n              case DatabaseSyncState.SyncFinished:\n                color = Colors.green;\n                hintText = LocaleKeys.newSettings_syncState_synced.tr();\n                break;\n              case DatabaseSyncState.Syncing:\n              case DatabaseSyncState.InitSyncBegin:\n                color = Colors.yellow;\n                hintText = LocaleKeys.newSettings_syncState_syncing.tr();\n                break;\n              default:\n                return const SizedBox.shrink();\n            }\n          }\n\n          return FlowyTooltip(\n            message: hintText,\n            child: Container(\n              decoration: BoxDecoration(\n                shape: BoxShape.circle,\n                color: color,\n              ),\n              width: 8,\n              height: 8,\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/trash/application/prelude.dart",
    "content": "export 'trash_bloc.dart';\nexport 'trash_listener.dart';\nexport 'trash_service.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/trash/application/trash_bloc.dart",
    "content": "import 'package:appflowy/plugins/trash/application/trash_listener.dart';\nimport 'package:appflowy/plugins/trash/application/trash_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'trash_bloc.freezed.dart';\n\nclass TrashBloc extends Bloc<TrashEvent, TrashState> {\n  TrashBloc()\n      : _service = TrashService(),\n        _listener = TrashListener(),\n        super(TrashState.init()) {\n    _dispatch();\n  }\n\n  final TrashService _service;\n  final TrashListener _listener;\n\n  void _dispatch() {\n    on<TrashEvent>((event, emit) async {\n      await event.map(\n        initial: (e) async {\n          _listener.start(trashUpdated: _listenTrashUpdated);\n          final result = await _service.readTrash();\n\n          emit(\n            result.fold(\n              (object) => state.copyWith(\n                objects: object.items,\n                successOrFailure: FlowyResult.success(null),\n              ),\n              (error) =>\n                  state.copyWith(successOrFailure: FlowyResult.failure(error)),\n            ),\n          );\n        },\n        didReceiveTrash: (e) async {\n          emit(state.copyWith(objects: e.trash));\n        },\n        putback: (e) async {\n          final result = await TrashService.putback(e.trashId);\n          await _handleResult(result, emit);\n        },\n        delete: (e) async {\n          final result = await _service.deleteViews([e.trash.id]);\n          await _handleResult(result, emit);\n        },\n        deleteAll: (e) async {\n          final result = await _service.deleteAll();\n          await _handleResult(result, emit);\n        },\n        restoreAll: (e) async {\n          final result = await _service.restoreAll();\n          await _handleResult(result, emit);\n        },\n      );\n    });\n  }\n\n  Future<void> _handleResult(\n    FlowyResult<dynamic, FlowyError> result,\n    Emitter<TrashState> emit,\n  ) async {\n    emit(\n      result.fold(\n        (l) => state.copyWith(successOrFailure: FlowyResult.success(null)),\n        (error) => state.copyWith(successOrFailure: FlowyResult.failure(error)),\n      ),\n    );\n  }\n\n  void _listenTrashUpdated(\n    FlowyResult<List<TrashPB>, FlowyError> trashOrFailed,\n  ) {\n    trashOrFailed.fold(\n      (trash) {\n        add(TrashEvent.didReceiveTrash(trash));\n      },\n      (error) {\n        Log.error(error);\n      },\n    );\n  }\n\n  @override\n  Future<void> close() async {\n    await _listener.close();\n    return super.close();\n  }\n}\n\n@freezed\nclass TrashEvent with _$TrashEvent {\n  const factory TrashEvent.initial() = Initial;\n  const factory TrashEvent.didReceiveTrash(List<TrashPB> trash) = ReceiveTrash;\n  const factory TrashEvent.putback(String trashId) = Putback;\n  const factory TrashEvent.delete(TrashPB trash) = Delete;\n  const factory TrashEvent.restoreAll() = RestoreAll;\n  const factory TrashEvent.deleteAll() = DeleteAll;\n}\n\n@freezed\nclass TrashState with _$TrashState {\n  const factory TrashState({\n    required List<TrashPB> objects,\n    required FlowyResult<void, FlowyError> successOrFailure,\n  }) = _TrashState;\n\n  factory TrashState.init() => TrashState(\n        objects: [],\n        successOrFailure: FlowyResult.success(null),\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/trash/application/trash_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/folder_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\ntypedef TrashUpdatedCallback = void Function(\n  FlowyResult<List<TrashPB>, FlowyError> trashOrFailed,\n);\n\nclass TrashListener {\n  StreamSubscription<SubscribeObject>? _subscription;\n  TrashUpdatedCallback? _trashUpdated;\n  FolderNotificationParser? _parser;\n\n  void start({TrashUpdatedCallback? trashUpdated}) {\n    _trashUpdated = trashUpdated;\n    _parser = FolderNotificationParser(\n      id: \"trash\",\n      callback: _observableCallback,\n    );\n    _subscription =\n        RustStreamReceiver.listen((observable) => _parser?.parse(observable));\n  }\n\n  void _observableCallback(\n    FolderNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case FolderNotification.DidUpdateTrash:\n        if (_trashUpdated != null) {\n          result.fold(\n            (payload) {\n              final repeatedTrash = RepeatedTrashPB.fromBuffer(payload);\n              _trashUpdated!(FlowyResult.success(repeatedTrash.items));\n            },\n            (error) => _trashUpdated!(FlowyResult.failure(error)),\n          );\n        }\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> close() async {\n    _parser = null;\n    await _subscription?.cancel();\n    _trashUpdated = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/trash/application/trash_service.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass TrashService {\n  Future<FlowyResult<RepeatedTrashPB, FlowyError>> readTrash() {\n    return FolderEventListTrashItems().send();\n  }\n\n  static Future<FlowyResult<void, FlowyError>> putback(String trashId) {\n    final id = TrashIdPB.create()..id = trashId;\n\n    return FolderEventRestoreTrashItem(id).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> deleteViews(List<String> trash) {\n    final items = trash.map((trash) {\n      return TrashIdPB.create()..id = trash;\n    });\n\n    final ids = RepeatedTrashIdPB(items: items);\n    return FolderEventPermanentlyDeleteTrashItem(ids).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> restoreAll() {\n    return FolderEventRecoverAllTrashItems().send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> deleteAll() {\n    return FolderEventPermanentlyDeleteAllTrashItem().send();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/trash/src/sizes.dart",
    "content": "class TrashSizes {\n  static double scale = 0.8;\n  static double get headerHeight => 60 * scale;\n  static double get fileNameWidth => 320 * scale;\n  static double get lashModifyWidth => 230 * scale;\n  static double get createTimeWidth => 230 * scale;\n  // padding between createTime and action icon\n  static double get padding => 40 * scale;\n  static double get actionIconWidth => 40 * scale;\n  static double get totalWidth =>\n      TrashSizes.fileNameWidth +\n      TrashSizes.lashModifyWidth +\n      TrashSizes.createTimeWidth +\n      TrashSizes.padding +\n      // restore and delete icon\n      2 * TrashSizes.actionIconWidth;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/trash/src/trash_cell.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/icon_button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart';\nimport 'package:flutter/material.dart';\nimport 'package:fixnum/fixnum.dart' as $fixnum;\n\nimport 'sizes.dart';\n\nclass TrashCell extends StatelessWidget {\n  const TrashCell({\n    super.key,\n    required this.object,\n    required this.onRestore,\n    required this.onDelete,\n  });\n\n  final VoidCallback onRestore;\n  final VoidCallback onDelete;\n  final TrashPB object;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        SizedBox(\n          width: TrashSizes.fileNameWidth,\n          child: FlowyText(\n            object.name.isEmpty\n                ? LocaleKeys.menuAppHeader_defaultNewPageName.tr()\n                : object.name,\n          ),\n        ),\n        SizedBox(\n          width: TrashSizes.lashModifyWidth,\n          child: FlowyText(dateFormatter(object.modifiedTime)),\n        ),\n        SizedBox(\n          width: TrashSizes.createTimeWidth,\n          child: FlowyText(dateFormatter(object.createTime)),\n        ),\n        const Spacer(),\n        FlowyIconButton(\n          iconColorOnHover: Theme.of(context).colorScheme.onSurface,\n          width: TrashSizes.actionIconWidth,\n          onPressed: onRestore,\n          iconPadding: const EdgeInsets.all(5),\n          icon: const FlowySvg(FlowySvgs.restore_s),\n        ),\n        const HSpace(20),\n        FlowyIconButton(\n          iconColorOnHover: Theme.of(context).colorScheme.onSurface,\n          width: TrashSizes.actionIconWidth,\n          onPressed: onDelete,\n          iconPadding: const EdgeInsets.all(5),\n          icon: const FlowySvg(FlowySvgs.delete_s),\n        ),\n      ],\n    );\n  }\n\n  String dateFormatter($fixnum.Int64 inputTimestamps) {\n    final outputFormat = DateFormat('MM/dd/yyyy hh:mm a');\n    final date =\n        DateTime.fromMillisecondsSinceEpoch(inputTimestamps.toInt() * 1000);\n    final outputDate = outputFormat.format(date);\n    return outputDate;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/trash/src/trash_header.dart",
    "content": "import 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\n\nimport 'sizes.dart';\n\nclass TrashHeaderDelegate extends SliverPersistentHeaderDelegate {\n  TrashHeaderDelegate();\n\n  @override\n  Widget build(\n    BuildContext context,\n    double shrinkOffset,\n    bool overlapsContent,\n  ) {\n    return TrashHeader();\n  }\n\n  @override\n  double get maxExtent => TrashSizes.headerHeight;\n\n  @override\n  double get minExtent => TrashSizes.headerHeight;\n\n  @override\n  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {\n    return false;\n  }\n}\n\nclass TrashHeaderItem {\n  TrashHeaderItem({required this.width, required this.title});\n\n  double width;\n  String title;\n}\n\nclass TrashHeader extends StatelessWidget {\n  TrashHeader({super.key});\n\n  final List<TrashHeaderItem> items = [\n    TrashHeaderItem(\n      title: LocaleKeys.trash_pageHeader_fileName.tr(),\n      width: TrashSizes.fileNameWidth,\n    ),\n    TrashHeaderItem(\n      title: LocaleKeys.trash_pageHeader_lastModified.tr(),\n      width: TrashSizes.lashModifyWidth,\n    ),\n    TrashHeaderItem(\n      title: LocaleKeys.trash_pageHeader_created.tr(),\n      width: TrashSizes.createTimeWidth,\n    ),\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    final headerItems = List<Widget>.empty(growable: true);\n    items.asMap().forEach((index, item) {\n      headerItems.add(\n        SizedBox(\n          width: item.width,\n          child: FlowyText(\n            item.title,\n            color: Theme.of(context).disabledColor,\n          ),\n        ),\n      );\n    });\n\n    return Container(\n      color: Theme.of(context).colorScheme.surface,\n      child: Row(\n        crossAxisAlignment: CrossAxisAlignment.stretch,\n        children: [\n          ...headerItems,\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/trash/trash.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/workspace/presentation/home/home_stack.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\n\nimport 'trash_page.dart';\n\nexport \"./src/sizes.dart\";\nexport \"./src/trash_cell.dart\";\nexport \"./src/trash_header.dart\";\n\nclass TrashPluginBuilder extends PluginBuilder {\n  @override\n  Plugin build(dynamic data) {\n    return TrashPlugin(pluginType: pluginType);\n  }\n\n  @override\n  String get menuName => \"TrashPB\";\n\n  @override\n  FlowySvgData get icon => FlowySvgs.trash_m;\n\n  @override\n  PluginType get pluginType => PluginType.trash;\n\n  @override\n  ViewLayoutPB get layoutType => ViewLayoutPB.Document;\n}\n\nclass TrashPluginConfig implements PluginConfig {\n  @override\n  bool get creatable => false;\n}\n\nclass TrashPlugin extends Plugin {\n  TrashPlugin({required PluginType pluginType}) : _pluginType = pluginType;\n\n  final PluginType _pluginType;\n\n  @override\n  PluginWidgetBuilder get widgetBuilder => TrashPluginDisplay();\n\n  @override\n  PluginId get id => \"TrashStack\";\n\n  @override\n  PluginType get pluginType => _pluginType;\n}\n\nclass TrashPluginDisplay extends PluginWidgetBuilder {\n  @override\n  String? get viewName => LocaleKeys.trash_text.tr();\n\n  @override\n  Widget get leftBarItem => FlowyText.medium(LocaleKeys.trash_text.tr());\n\n  @override\n  Widget tabBarItem(String pluginId, [bool shortForm = false]) => leftBarItem;\n\n  @override\n  Widget? get rightBarItem => null;\n\n  @override\n  Widget buildWidget({\n    required PluginContext context,\n    required bool shrinkWrap,\n    Map<String, dynamic>? data,\n  }) =>\n      const TrashPage(key: ValueKey('TrashPage'));\n\n  @override\n  List<NavigationItem> get navigationItems => [this];\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/trash/src/sizes.dart';\nimport 'package:appflowy/plugins/trash/src/trash_header.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';\nimport 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';\nimport 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:styled_widget/styled_widget.dart';\n\nimport 'application/trash_bloc.dart';\nimport 'src/trash_cell.dart';\n\nclass TrashPage extends StatefulWidget {\n  const TrashPage({super.key});\n\n  @override\n  State<TrashPage> createState() => _TrashPageState();\n}\n\nclass _TrashPageState extends State<TrashPage> {\n  final ScrollController _scrollController = ScrollController();\n\n  @override\n  void dispose() {\n    _scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    const horizontalPadding = 80.0;\n    return BlocProvider(\n      create: (context) => getIt<TrashBloc>()..add(const TrashEvent.initial()),\n      child: BlocBuilder<TrashBloc, TrashState>(\n        builder: (context, state) {\n          return SizedBox.expand(\n            child: Column(\n              children: [\n                _renderTopBar(context, state),\n                const VSpace(32),\n                _renderTrashList(context, state),\n              ],\n            ).padding(horizontal: horizontalPadding, vertical: 48),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _renderTrashList(BuildContext context, TrashState state) {\n    const barSize = 6.0;\n    return Expanded(\n      child: ScrollbarListStack(\n        axis: Axis.vertical,\n        controller: _scrollController,\n        scrollbarPadding: EdgeInsets.only(top: TrashSizes.headerHeight),\n        barSize: barSize,\n        child: StyledSingleChildScrollView(\n          barSize: barSize,\n          axis: Axis.horizontal,\n          child: SizedBox(\n            width: TrashSizes.totalWidth,\n            child: ScrollConfiguration(\n              behavior: const ScrollBehavior().copyWith(scrollbars: false),\n              child: CustomScrollView(\n                shrinkWrap: true,\n                physics: StyledScrollPhysics(),\n                controller: _scrollController,\n                slivers: [\n                  _renderListHeader(context, state),\n                  _renderListBody(context, state),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _renderTopBar(BuildContext context, TrashState state) {\n    return SizedBox(\n      height: 36,\n      child: Row(\n        crossAxisAlignment: CrossAxisAlignment.stretch,\n        children: [\n          FlowyText.semibold(\n            LocaleKeys.trash_text.tr(),\n            fontSize: FontSizes.s16,\n            color: Theme.of(context).colorScheme.tertiary,\n          ),\n          const Spacer(),\n          IntrinsicWidth(\n            child: FlowyButton(\n              text: FlowyText.medium(\n                LocaleKeys.trash_restoreAll.tr(),\n                lineHeight: 1.0,\n              ),\n              leftIcon: const FlowySvg(FlowySvgs.restore_s),\n              onTap: () => showCancelAndConfirmDialog(\n                context: context,\n                confirmLabel: LocaleKeys.trash_restore.tr(),\n                title: LocaleKeys.trash_confirmRestoreAll_title.tr(),\n                description: LocaleKeys.trash_confirmRestoreAll_caption.tr(),\n                onConfirm: (_) => context\n                    .read<TrashBloc>()\n                    .add(const TrashEvent.restoreAll()),\n              ),\n            ),\n          ),\n          const HSpace(6),\n          IntrinsicWidth(\n            child: FlowyButton(\n              text: FlowyText.medium(\n                LocaleKeys.trash_deleteAll.tr(),\n                lineHeight: 1.0,\n              ),\n              leftIcon: const FlowySvg(FlowySvgs.delete_s),\n              onTap: () => showConfirmDeletionDialog(\n                context: context,\n                name: LocaleKeys.trash_confirmDeleteAll_title.tr(),\n                description: LocaleKeys.trash_confirmDeleteAll_caption.tr(),\n                onConfirm: () =>\n                    context.read<TrashBloc>().add(const TrashEvent.deleteAll()),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _renderListHeader(BuildContext context, TrashState state) {\n    return SliverPersistentHeader(\n      delegate: TrashHeaderDelegate(),\n      floating: true,\n      pinned: true,\n    );\n  }\n\n  Widget _renderListBody(BuildContext context, TrashState state) {\n    return SliverList(\n      delegate: SliverChildBuilderDelegate(\n        (BuildContext context, int index) {\n          final object = state.objects[index];\n          return SizedBox(\n            height: 42,\n            child: TrashCell(\n              object: object,\n              onRestore: () => showCancelAndConfirmDialog(\n                context: context,\n                title:\n                    LocaleKeys.trash_restorePage_title.tr(args: [object.name]),\n                description: LocaleKeys.trash_restorePage_caption.tr(),\n                confirmLabel: LocaleKeys.trash_restore.tr(),\n                onConfirm: (_) => context\n                    .read<TrashBloc>()\n                    .add(TrashEvent.putback(object.id)),\n              ),\n              onDelete: () => showConfirmDeletionDialog(\n                context: context,\n                name: object.name.trim().isEmpty\n                    ? LocaleKeys.menuAppHeader_defaultNewPageName.tr()\n                    : object.name,\n                description:\n                    LocaleKeys.deletePagePrompt_deletePermanentDescription.tr(),\n                onConfirm: () =>\n                    context.read<TrashBloc>().add(TrashEvent.delete(object)),\n              ),\n            ),\n          );\n        },\n        childCount: state.objects.length,\n        addAutomaticKeepAlives: false,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/plugins/util.dart",
    "content": "import 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/workspace/application/view/view_listener.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter/material.dart';\n\nclass ViewPluginNotifier extends PluginNotifier<DeletedViewPB?> {\n  ViewPluginNotifier({\n    required this.view,\n  }) : _viewListener = ViewListener(viewId: view.id) {\n    _viewListener?.start(\n      onViewUpdated: (updatedView) => view = updatedView,\n      onViewMoveToTrash: (result) => result.fold(\n        (deletedView) => isDeleted.value = deletedView,\n        (err) => Log.error(err),\n      ),\n    );\n  }\n\n  ViewPB view;\n  final ViewListener? _viewListener;\n\n  @override\n  final ValueNotifier<DeletedViewPB?> isDeleted = ValueNotifier(null);\n\n  @override\n  void dispose() {\n    isDeleted.dispose();\n    _viewListener?.stop();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/af_image.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\n\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\n\nclass AFImage extends StatelessWidget {\n  const AFImage({\n    super.key,\n    required this.url,\n    required this.uploadType,\n    this.height,\n    this.width,\n    this.fit = BoxFit.cover,\n    this.userProfile,\n    this.borderRadius,\n  }) : assert(\n          uploadType != FileUploadTypePB.CloudFile || userProfile != null,\n          'userProfile must be provided for accessing files from AF Cloud',\n        );\n\n  final String url;\n  final FileUploadTypePB uploadType;\n  final double? height;\n  final double? width;\n  final BoxFit fit;\n  final UserProfilePB? userProfile;\n  final BorderRadius? borderRadius;\n\n  @override\n  Widget build(BuildContext context) {\n    if (uploadType == FileUploadTypePB.CloudFile && userProfile == null) {\n      return const SizedBox.shrink();\n    }\n\n    Widget child;\n    if (uploadType == FileUploadTypePB.NetworkFile) {\n      child = Image.network(\n        url,\n        height: height,\n        width: width,\n        fit: fit,\n        isAntiAlias: true,\n        errorBuilder: (context, error, stackTrace) {\n          return const SizedBox.shrink();\n        },\n      );\n    } else if (uploadType == FileUploadTypePB.LocalFile) {\n      child = Image.file(\n        File(url),\n        height: height,\n        width: width,\n        fit: fit,\n        isAntiAlias: true,\n        errorBuilder: (context, error, stackTrace) {\n          return const SizedBox.shrink();\n        },\n      );\n    } else {\n      child = FlowyNetworkImage(\n        url: url,\n        userProfilePB: userProfile,\n        height: height,\n        width: width,\n        errorWidgetBuilder: (context, url, error) {\n          return const SizedBox.shrink();\n        },\n      );\n    }\n\n    if (borderRadius != null) {\n      child = ClipRRect(\n        clipBehavior: Clip.antiAliasWithSaveLayer,\n        borderRadius: borderRadius!,\n        child: child,\n      );\n    }\n\n    return child;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/af_role_pb_extension.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nextension AFRolePBExtension on AFRolePB {\n  bool get isOwner => this == AFRolePB.Owner;\n\n  bool get isMember => this == AFRolePB.Member;\n\n  bool get canInvite => isOwner;\n\n  bool get canDelete => isOwner;\n\n  bool get canUpdate => isOwner;\n\n  bool get canLeave => this != AFRolePB.Owner;\n\n  String get description {\n    switch (this) {\n      case AFRolePB.Owner:\n        return LocaleKeys.settings_appearance_members_owner.tr();\n      case AFRolePB.Member:\n        return LocaleKeys.settings_appearance_members_member.tr();\n      case AFRolePB.Guest:\n        return LocaleKeys.settings_appearance_members_guest.tr();\n    }\n    throw UnimplementedError('Unknown role: $this');\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/af_user_profile_extension.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\n\nextension UserProfilePBExtension on UserProfilePB {\n  String? get authToken {\n    try {\n      final map = jsonDecode(token) as Map<String, dynamic>;\n      return map['access_token'] as String?;\n    } catch (e) {\n      Log.error('Failed to decode auth token: $e');\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/appflowy_cache_manager.dart",
    "content": "import 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:path_provider/path_provider.dart';\n\nclass FlowyCacheManager {\n  final _caches = <ICache>[];\n\n  // if you add a new cache, you should register it here.\n  void registerCache(ICache cache) {\n    _caches.add(cache);\n  }\n\n  void unregisterAllCache(ICache cache) {\n    _caches.clear();\n  }\n\n  Future<void> clearAllCache() async {\n    try {\n      for (final cache in _caches) {\n        await cache.clearAll();\n      }\n\n      Log.info('Cache cleared');\n    } catch (e) {\n      Log.error(e);\n    }\n  }\n\n  Future<int> getCacheSize() async {\n    try {\n      int tmpDirSize = 0;\n      for (final cache in _caches) {\n        tmpDirSize += await cache.cacheSize();\n      }\n      Log.info('Cache size: $tmpDirSize');\n      return tmpDirSize;\n    } catch (e) {\n      Log.error(e);\n      return 0;\n    }\n  }\n}\n\nabstract class ICache {\n  Future<int> cacheSize();\n  Future<void> clearAll();\n}\n\nclass TemporaryDirectoryCache implements ICache {\n  @override\n  Future<int> cacheSize() async {\n    final tmpDir = await getTemporaryDirectory();\n    final tmpDirStat = await tmpDir.stat();\n    return tmpDirStat.size;\n  }\n\n  @override\n  Future<void> clearAll() async {\n    final tmpDir = await getTemporaryDirectory();\n    await tmpDir.delete(recursive: true);\n  }\n}\n\nclass FeatureFlagCache implements ICache {\n  @override\n  Future<int> cacheSize() async {\n    return 0;\n  }\n\n  @override\n  Future<void> clearAll() async {\n    await FeatureFlag.clear();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/custom_image_cache_manager.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:cached_network_image/cached_network_image.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_cache_manager/flutter_cache_manager.dart';\nimport 'package:string_validator/string_validator.dart';\n\n/// This widget handles the downloading and caching of either internal or network images.\n/// It will append the access token to the URL if the URL is internal.\nclass FlowyNetworkImage extends StatefulWidget {\n  const FlowyNetworkImage({\n    super.key,\n    this.userProfilePB,\n    this.width,\n    this.height,\n    this.fit = BoxFit.cover,\n    this.progressIndicatorBuilder,\n    this.errorWidgetBuilder,\n    required this.url,\n    this.maxRetries = 5,\n    this.retryDuration = const Duration(seconds: 6),\n    this.retryErrorCodes = const {404},\n    this.onImageLoaded,\n  });\n\n  /// The URL of the image.\n  final String url;\n\n  /// The width of the image.\n  final double? width;\n\n  /// The height of the image.\n  final double? height;\n\n  /// The fit of the image.\n  final BoxFit fit;\n\n  /// The user profile.\n  ///\n  /// If the userProfilePB is not null, the image will be downloaded with the access token.\n  final UserProfilePB? userProfilePB;\n\n  /// The progress indicator builder.\n  final ProgressIndicatorBuilder? progressIndicatorBuilder;\n\n  /// The error widget builder.\n  final LoadingErrorWidgetBuilder? errorWidgetBuilder;\n\n  /// Retry loading the image if it fails.\n  final int maxRetries;\n\n  /// Retry duration\n  final Duration retryDuration;\n\n  /// Retry error codes.\n  final Set<int> retryErrorCodes;\n\n  final void Function(bool isImageInCache)? onImageLoaded;\n\n  @override\n  FlowyNetworkImageState createState() => FlowyNetworkImageState();\n}\n\nclass FlowyNetworkImageState extends State<FlowyNetworkImage> {\n  final manager = CustomImageCacheManager();\n  final retryCounter = FlowyNetworkRetryCounter();\n\n  // This is used to clear the retry count when the widget is disposed in case of the url is the same.\n  String? retryTag;\n\n  @override\n  void initState() {\n    super.initState();\n\n    assert(isURL(widget.url));\n\n    if (widget.url.isAppFlowyCloudUrl) {\n      assert(\n        widget.userProfilePB != null && widget.userProfilePB!.token.isNotEmpty,\n      );\n    }\n\n    retryTag = retryCounter.add(widget.url);\n\n    manager.getFileFromCache(widget.url).then((file) {\n      widget.onImageLoaded?.call(\n        file != null &&\n            file.file.path.isNotEmpty &&\n            file.originalUrl == widget.url,\n      );\n    });\n  }\n\n  @override\n  void reassemble() {\n    super.reassemble();\n\n    if (retryTag != null) {\n      retryCounter.clear(\n        tag: retryTag!,\n        url: widget.url,\n        maxRetries: widget.maxRetries,\n      );\n    }\n  }\n\n  @override\n  void dispose() {\n    if (retryTag != null) {\n      retryCounter.clear(\n        tag: retryTag!,\n        url: widget.url,\n        maxRetries: widget.maxRetries,\n      );\n    }\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ListenableBuilder(\n      listenable: retryCounter,\n      builder: (context, child) {\n        final retryCount = retryCounter.getRetryCount(widget.url);\n        return CachedNetworkImage(\n          key: ValueKey('${widget.url}_$retryCount'),\n          cacheManager: manager,\n          httpHeaders: _buildRequestHeader(),\n          imageUrl: widget.url,\n          fit: widget.fit,\n          width: widget.width,\n          height: widget.height,\n          progressIndicatorBuilder: widget.progressIndicatorBuilder,\n          errorWidget: _errorWidgetBuilder,\n          errorListener: (value) async {\n            Log.error(\n              'Unable to load image: ${value.toString()} - retryCount: $retryCount',\n            );\n\n            // clear the cache and retry\n            await manager.removeFile(widget.url);\n            _retryLoadImage();\n          },\n        );\n      },\n    );\n  }\n\n  /// if the error is 404 and the retry count is less than the max retries, it return a loading indicator.\n  Widget _errorWidgetBuilder(BuildContext context, String url, Object error) {\n    final retryCount = retryCounter.getRetryCount(url);\n    if (error is HttpExceptionWithStatus) {\n      if (widget.retryErrorCodes.contains(error.statusCode) &&\n          retryCount < widget.maxRetries) {\n        final fakeDownloadProgress = DownloadProgress(url, null, 0);\n        return widget.progressIndicatorBuilder?.call(\n              context,\n              url,\n              fakeDownloadProgress,\n            ) ??\n            const Center(\n              child: _SensitiveContent(),\n            );\n      }\n\n      if (error.statusCode == 422) {\n        // Unprocessable Entity: Used when the server understands the request but cannot process it due to\n        //semantic issues (e.g., sensitive keywords).\n        return const _SensitiveContent();\n      }\n    }\n\n    return widget.errorWidgetBuilder?.call(context, url, error) ??\n        const SizedBox.shrink();\n  }\n\n  Map<String, String> _buildRequestHeader() {\n    final header = <String, String>{};\n    final token = widget.userProfilePB?.token;\n    if (token != null) {\n      try {\n        final decodedToken = jsonDecode(token);\n        header['Authorization'] = 'Bearer ${decodedToken['access_token']}';\n      } catch (e) {\n        Log.error('Unable to decode token: $e');\n      }\n    }\n    return header;\n  }\n\n  void _retryLoadImage() {\n    final retryCount = retryCounter.getRetryCount(widget.url);\n    if (retryCount < widget.maxRetries) {\n      Future.delayed(widget.retryDuration, () {\n        Log.debug(\n          'Retry load image: ${widget.url}, retry count: $retryCount',\n        );\n        // Increment the retry count for the URL to trigger the image rebuild.\n        retryCounter.increment(widget.url);\n      });\n    }\n  }\n}\n\n/// This class is used to count the number of retries for a given URL.\n@visibleForTesting\nclass FlowyNetworkRetryCounter with ChangeNotifier {\n  FlowyNetworkRetryCounter._();\n\n  factory FlowyNetworkRetryCounter() => _instance;\n  static final _instance = FlowyNetworkRetryCounter._();\n\n  final Map<String, int> _values = <String, int>{};\n  Map<String, int> get values => {..._values};\n\n  /// Get the retry count for a given URL.\n  int getRetryCount(String url) => _values[url] ?? 0;\n\n  /// Add a new URL to the retry counter. Don't call notifyListeners() here.\n  ///\n  /// This function will return a tag, use it to clear the retry count.\n  /// Because the url may be the same, we need to add a unique tag to the url.\n  String add(String url) {\n    _values.putIfAbsent(url, () => 0);\n    return url + uuid();\n  }\n\n  /// Increment the retry count for a given URL.\n  void increment(String url) {\n    final count = _values[url];\n    if (count == null) {\n      _values[url] = 1;\n    } else {\n      _values[url] = count + 1;\n    }\n    notifyListeners();\n  }\n\n  /// Clear the retry count for a given tag.\n  void clear({\n    required String tag,\n    required String url,\n    int? maxRetries,\n  }) {\n    _values.remove(tag);\n\n    final retryCount = _values[url];\n    if (maxRetries == null ||\n        (retryCount != null && retryCount >= maxRetries)) {\n      _values.remove(url);\n    }\n  }\n\n  /// Reset the retry counter.\n  void reset() {\n    _values.clear();\n  }\n}\n\nclass _SensitiveContent extends StatelessWidget {\n  const _SensitiveContent();\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyText(LocaleKeys.ai_contentPolicyViolation.tr());\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/appflowy_network_svg.dart",
    "content": "import 'dart:developer';\nimport 'dart:io';\n\nimport 'package:flowy_svg/flowy_svg.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_cache_manager/flutter_cache_manager.dart';\n\nimport 'custom_image_cache_manager.dart';\n\nclass FlowyNetworkSvg extends StatefulWidget {\n  FlowyNetworkSvg(\n    this.url, {\n    Key? key,\n    this.cacheKey,\n    this.placeholder,\n    this.errorWidget,\n    this.width,\n    this.height,\n    this.headers,\n    this.fit = BoxFit.contain,\n    this.alignment = Alignment.center,\n    this.matchTextDirection = false,\n    this.allowDrawingOutsideViewBox = false,\n    this.semanticsLabel,\n    this.excludeFromSemantics = false,\n    this.theme = const SvgTheme(),\n    this.fadeDuration = Duration.zero,\n    this.colorFilter,\n    this.placeholderBuilder,\n    BaseCacheManager? cacheManager,\n  })  : cacheManager = cacheManager ?? CustomImageCacheManager(),\n        super(key: key ?? ValueKey(url));\n\n  final String url;\n  final String? cacheKey;\n  final Widget? placeholder;\n  final Widget? errorWidget;\n  final double? width;\n  final double? height;\n  final ColorFilter? colorFilter;\n  final Map<String, String>? headers;\n  final BoxFit fit;\n  final AlignmentGeometry alignment;\n  final bool matchTextDirection;\n  final bool allowDrawingOutsideViewBox;\n  final String? semanticsLabel;\n  final bool excludeFromSemantics;\n  final SvgTheme theme;\n  final Duration fadeDuration;\n  final WidgetBuilder? placeholderBuilder;\n  final BaseCacheManager cacheManager;\n\n  @override\n  State<FlowyNetworkSvg> createState() => _FlowyNetworkSvgState();\n\n  static Future<void> preCache(\n    String imageUrl, {\n    String? cacheKey,\n    BaseCacheManager? cacheManager,\n  }) {\n    final key = cacheKey ?? _generateKeyFromUrl(imageUrl);\n    cacheManager ??= DefaultCacheManager();\n    return cacheManager.downloadFile(key);\n  }\n\n  static Future<void> clearCacheForUrl(\n    String imageUrl, {\n    String? cacheKey,\n    BaseCacheManager? cacheManager,\n  }) {\n    final key = cacheKey ?? _generateKeyFromUrl(imageUrl);\n    cacheManager ??= DefaultCacheManager();\n    return cacheManager.removeFile(key);\n  }\n\n  static Future<void> clearCache({BaseCacheManager? cacheManager}) {\n    cacheManager ??= DefaultCacheManager();\n    return cacheManager.emptyCache();\n  }\n\n  static String _generateKeyFromUrl(String url) => url.split('?').first;\n}\n\nclass _FlowyNetworkSvgState extends State<FlowyNetworkSvg>\n    with SingleTickerProviderStateMixin {\n  bool _isLoading = false;\n  bool _isError = false;\n  File? _imageFile;\n  late String _cacheKey;\n\n  late final AnimationController _controller;\n  late final Animation<double> _animation;\n\n  @override\n  void initState() {\n    super.initState();\n    _cacheKey =\n        widget.cacheKey ?? FlowyNetworkSvg._generateKeyFromUrl(widget.url);\n    _controller = AnimationController(\n      vsync: this,\n      duration: widget.fadeDuration,\n    );\n    _animation = Tween(begin: 0.0, end: 1.0).animate(_controller);\n    _loadImage();\n  }\n\n  Future<void> _loadImage() async {\n    try {\n      _setToLoadingAfter15MsIfNeeded();\n\n      var file = (await widget.cacheManager.getFileFromMemory(_cacheKey))?.file;\n\n      file ??= await widget.cacheManager.getSingleFile(\n        widget.url,\n        key: _cacheKey,\n        headers: widget.headers ?? {},\n      );\n\n      _imageFile = file;\n      _isLoading = false;\n\n      _setState();\n\n      await _controller.forward();\n    } catch (e) {\n      log('CachedNetworkSVGImage: $e');\n\n      _isError = true;\n      _isLoading = false;\n\n      _setState();\n    }\n  }\n\n  void _setToLoadingAfter15MsIfNeeded() => Future.delayed(\n        const Duration(milliseconds: 15),\n        () {\n          if (!_isLoading && _imageFile == null && !_isError) {\n            _isLoading = true;\n            _setState();\n          }\n        },\n      );\n\n  void _setState() => mounted ? setState(() {}) : null;\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: widget.width,\n      height: widget.height,\n      child: _buildImage(),\n    );\n  }\n\n  Widget _buildImage() {\n    if (_isLoading) return _buildPlaceholderWidget();\n\n    if (_isError) return _buildErrorWidget();\n\n    return FadeTransition(\n      opacity: _animation,\n      child: _buildSVGImage(),\n    );\n  }\n\n  Widget _buildPlaceholderWidget() =>\n      Center(child: widget.placeholder ?? const SizedBox());\n\n  Widget _buildErrorWidget() =>\n      Center(child: widget.errorWidget ?? const SizedBox());\n\n  Widget _buildSVGImage() {\n    if (_imageFile == null) return const SizedBox();\n\n    return SvgPicture.file(\n      _imageFile!,\n      fit: widget.fit,\n      width: widget.width,\n      height: widget.height,\n      alignment: widget.alignment,\n      matchTextDirection: widget.matchTextDirection,\n      allowDrawingOutsideViewBox: widget.allowDrawingOutsideViewBox,\n      colorFilter: widget.colorFilter,\n      semanticsLabel: widget.semanticsLabel,\n      excludeFromSemantics: widget.excludeFromSemantics,\n      placeholderBuilder: widget.placeholderBuilder,\n      theme: widget.theme,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/clipboard_state.dart",
    "content": "import 'package:flutter/foundation.dart';\n\n/// Class to hold the state of the Clipboard.\n///\n/// Essentially for document in-app json paste, we need to be able\n/// to differentiate between a cut-paste and a copy-paste.\n///\n/// When a cut-pase has occurred, the next paste operation should be\n/// seen as a copy-paste.\n///\nclass ClipboardState {\n  ClipboardState();\n\n  bool _isCut = false;\n\n  bool get isCut => _isCut;\n\n  final ValueNotifier<bool> isHandlingPasteNotifier = ValueNotifier(false);\n  bool get isHandlingPaste => isHandlingPasteNotifier.value;\n\n  final Set<String> _handlingPasteIds = {};\n\n  void dispose() {\n    isHandlingPasteNotifier.dispose();\n  }\n\n  void didCut() {\n    _isCut = true;\n  }\n\n  void didPaste() {\n    _isCut = false;\n  }\n\n  void startHandlingPaste(String id) {\n    _handlingPasteIds.add(id);\n    isHandlingPasteNotifier.value = true;\n  }\n\n  void endHandlingPaste(String id) {\n    _handlingPasteIds.remove(id);\n    if (_handlingPasteIds.isEmpty) {\n      isHandlingPasteNotifier.value = false;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/colors.dart",
    "content": "import 'package:appflowy/util/theme_extension.dart';\nimport 'package:flutter/material.dart';\n\nextension SharedColors on BuildContext {\n  Color get proPrimaryColor {\n    return Theme.of(this).isLightMode\n        ? const Color(0xFF653E8C)\n        : const Color(0xFFE8E2EE);\n  }\n\n  Color get proSecondaryColor {\n    return Theme.of(this).isLightMode\n        ? const Color(0xFFE8E2EE)\n        : const Color(0xFF653E8C);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/conditional_listenable_builder.dart",
    "content": "import 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\n\nclass ConditionalListenableBuilder<T> extends StatefulWidget {\n  const ConditionalListenableBuilder({\n    super.key,\n    required this.valueListenable,\n    required this.buildWhen,\n    required this.builder,\n    this.child,\n  });\n\n  /// The [ValueListenable] whose value you depend on in order to build.\n  ///\n  /// This widget does not ensure that the [ValueListenable]'s value is not\n  /// null, therefore your [builder] may need to handle null values.\n  final ValueListenable<T> valueListenable;\n\n  /// The [buildWhen] function will be called on each value change of the\n  /// [valueListenable]. If the [buildWhen] function returns true, the [builder]\n  /// will be called with the new value of the [valueListenable].\n  ///\n  final bool Function(T previous, T current) buildWhen;\n\n  /// A [ValueWidgetBuilder] which builds a widget depending on the\n  /// [valueListenable]'s value.\n  ///\n  /// Can incorporate a [valueListenable] value-independent widget subtree\n  /// from the [child] parameter into the returned widget tree.\n  final ValueWidgetBuilder<T> builder;\n\n  /// A [valueListenable]-independent widget which is passed back to the [builder].\n  ///\n  /// This argument is optional and can be null if the entire widget subtree the\n  /// [builder] builds depends on the value of the [valueListenable]. For\n  /// example, in the case where the [valueListenable] is a [String] and the\n  /// [builder] returns a [Text] widget with the current [String] value, there\n  /// would be no useful [child].\n  final Widget? child;\n\n  @override\n  State<ConditionalListenableBuilder> createState() =>\n      _ConditionalListenableBuilderState<T>();\n}\n\nclass _ConditionalListenableBuilderState<T>\n    extends State<ConditionalListenableBuilder<T>> {\n  late T value;\n\n  @override\n  void initState() {\n    super.initState();\n    value = widget.valueListenable.value;\n    widget.valueListenable.addListener(_valueChanged);\n  }\n\n  @override\n  void didUpdateWidget(ConditionalListenableBuilder<T> oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    if (oldWidget.valueListenable != widget.valueListenable) {\n      oldWidget.valueListenable.removeListener(_valueChanged);\n      value = widget.valueListenable.value;\n      widget.valueListenable.addListener(_valueChanged);\n    }\n  }\n\n  @override\n  void dispose() {\n    widget.valueListenable.removeListener(_valueChanged);\n    super.dispose();\n  }\n\n  void _valueChanged() {\n    if (widget.buildWhen(value, widget.valueListenable.value)) {\n      setState(() {\n        value = widget.valueListenable.value;\n      });\n    } else {\n      value = widget.valueListenable.value;\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return widget.builder(context, value, widget.child);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/custom_image_cache_manager.dart",
    "content": "import 'package:appflowy/shared/appflowy_cache_manager.dart';\nimport 'package:appflowy/startup/tasks/prelude.dart';\nimport 'package:file/file.dart' hide FileSystem;\nimport 'package:file/local.dart';\nimport 'package:flutter_cache_manager/flutter_cache_manager.dart';\nimport 'package:path/path.dart' as p;\n\nclass CustomImageCacheManager extends CacheManager\n    with ImageCacheManager\n    implements ICache {\n  CustomImageCacheManager._()\n      : super(\n          Config(\n            key,\n            fileSystem: CustomIOFileSystem(key),\n          ),\n        );\n\n  factory CustomImageCacheManager() => _instance;\n\n  static final CustomImageCacheManager _instance = CustomImageCacheManager._();\n\n  static const key = 'image_cache';\n\n  @override\n  Future<int> cacheSize() async {\n    // https://github.com/Baseflow/flutter_cache_manager/issues/239#issuecomment-719475429\n    // this package does not provide a way to get the cache size\n    return 0;\n  }\n\n  @override\n  Future<void> clearAll() async {\n    await emptyCache();\n  }\n}\n\nclass CustomIOFileSystem implements FileSystem {\n  CustomIOFileSystem(this._cacheKey) : _fileDir = createDirectory(_cacheKey);\n  final Future<Directory> _fileDir;\n  final String _cacheKey;\n\n  static Future<Directory> createDirectory(String key) async {\n    final baseDir = await appFlowyApplicationDataDirectory();\n    final path = p.join(baseDir.path, key);\n\n    const fs = LocalFileSystem();\n    final directory = fs.directory(path);\n    await directory.create(recursive: true);\n    return directory;\n  }\n\n  @override\n  Future<File> createFile(String name) async {\n    final directory = await _fileDir;\n    if (!(await directory.exists())) {\n      await createDirectory(_cacheKey);\n    }\n    return directory.childFile(name);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/easy_localiation_service.dart",
    "content": "import 'package:easy_localization/easy_localization.dart';\n// ignore: implementation_imports\nimport 'package:easy_localization/src/easy_localization_controller.dart';\nimport 'package:flutter/widgets.dart';\n\nclass EasyLocalizationService {\n  EasyLocalizationService();\n\n  late EasyLocalizationController? controller;\n\n  String getFallbackTranslation(String token) {\n    final translations = controller?.fallbackTranslations;\n\n    return translations?.get(token).toString() ?? '';\n  }\n\n  String getTranslation(String token) {\n    final translations = controller?.translations;\n\n    return translations?.get(token).toString() ?? '';\n  }\n\n  void init(BuildContext context) {\n    controller = EasyLocalization.of(context)?.delegate.localizationController;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/error_code/error_code_map.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nextension PublishNameErrorCodeMap on ErrorCode {\n  String? get publishErrorMessage {\n    return switch (this) {\n      ErrorCode.PublishNameAlreadyExists =>\n        LocaleKeys.settings_sites_error_publishNameAlreadyInUse.tr(),\n      ErrorCode.PublishNameInvalidCharacter => LocaleKeys\n          .settings_sites_error_publishNameContainsInvalidCharacters\n          .tr(),\n      ErrorCode.PublishNameTooLong =>\n        LocaleKeys.settings_sites_error_publishNameTooLong.tr(),\n      ErrorCode.UserUnauthorized =>\n        LocaleKeys.settings_sites_error_publishPermissionDenied.tr(),\n      ErrorCode.ViewNameInvalid =>\n        LocaleKeys.settings_sites_error_publishNameCannotBeEmpty.tr(),\n      _ => null,\n    };\n  }\n}\n\nextension DomainErrorCodeMap on ErrorCode {\n  String? get namespaceErrorMessage {\n    return switch (this) {\n      ErrorCode.CustomNamespaceRequirePlanUpgrade =>\n        LocaleKeys.settings_sites_error_proPlanLimitation.tr(),\n      ErrorCode.CustomNamespaceAlreadyTaken =>\n        LocaleKeys.settings_sites_error_namespaceAlreadyInUse.tr(),\n      ErrorCode.InvalidNamespace ||\n      ErrorCode.InvalidRequest =>\n        LocaleKeys.settings_sites_error_invalidNamespace.tr(),\n      ErrorCode.CustomNamespaceTooLong =>\n        LocaleKeys.settings_sites_error_namespaceTooLong.tr(),\n      ErrorCode.CustomNamespaceTooShort =>\n        LocaleKeys.settings_sites_error_namespaceTooShort.tr(),\n      ErrorCode.CustomNamespaceReserved =>\n        LocaleKeys.settings_sites_error_namespaceIsReserved.tr(),\n      ErrorCode.CustomNamespaceInvalidCharacter =>\n        LocaleKeys.settings_sites_error_namespaceContainsInvalidCharacters.tr(),\n      _ => null,\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/error_page/error_page.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\n\nclass FlowyErrorPage extends StatelessWidget {\n  factory FlowyErrorPage.error(\n    Error e, {\n    required String howToFix,\n    Key? key,\n    List<Widget>? actions,\n  }) =>\n      FlowyErrorPage._(\n        e.toString(),\n        stackTrace: e.stackTrace?.toString(),\n        howToFix: howToFix,\n        key: key,\n        actions: actions,\n      );\n\n  factory FlowyErrorPage.message(\n    String message, {\n    required String howToFix,\n    String? stackTrace,\n    Key? key,\n    List<Widget>? actions,\n  }) =>\n      FlowyErrorPage._(\n        message,\n        key: key,\n        stackTrace: stackTrace,\n        howToFix: howToFix,\n        actions: actions,\n      );\n\n  factory FlowyErrorPage.exception(\n    Exception e, {\n    required String howToFix,\n    String? stackTrace,\n    Key? key,\n    List<Widget>? actions,\n  }) =>\n      FlowyErrorPage._(\n        e.toString(),\n        stackTrace: stackTrace,\n        key: key,\n        howToFix: howToFix,\n        actions: actions,\n      );\n\n  const FlowyErrorPage._(\n    this.message, {\n    required this.howToFix,\n    this.stackTrace,\n    super.key,\n    this.actions,\n  });\n\n  static const _titleFontSize = 24.0;\n  static const _titleToMessagePadding = 8.0;\n\n  final List<Widget>? actions;\n  final String howToFix;\n  final String message;\n  final String? stackTrace;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(8.0),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          const FlowyText.medium(\n            \"AppFlowy Error\",\n            fontSize: _titleFontSize,\n          ),\n          const SizedBox(height: _titleToMessagePadding),\n          Listener(\n            behavior: HitTestBehavior.translucent,\n            onPointerDown: (_) async {\n              await getIt<ClipboardService>().setData(\n                ClipboardServiceData(plainText: message),\n              );\n              if (context.mounted) {\n                ScaffoldMessenger.of(context).showSnackBar(\n                  SnackBar(\n                    backgroundColor:\n                        Theme.of(context).colorScheme.surfaceContainerHighest,\n                    content: FlowyText(\n                      'Message copied to clipboard',\n                      fontSize: kIsWeb || !Platform.isIOS && !Platform.isAndroid\n                          ? 14\n                          : 12,\n                    ),\n                  ),\n                );\n              }\n            },\n            child: FlowyHover(\n              style: HoverStyle(\n                backgroundColor:\n                    Theme.of(context).colorScheme.tertiaryContainer,\n              ),\n              cursor: SystemMouseCursors.click,\n              child: FlowyTooltip(\n                message: 'Click to copy message',\n                child: Padding(\n                  padding: const EdgeInsets.all(4),\n                  child: FlowyText.semibold(message, maxLines: 10),\n                ),\n              ),\n            ),\n          ),\n          const SizedBox(height: _titleToMessagePadding),\n          FlowyText.regular(howToFix, maxLines: 10),\n          const SizedBox(height: _titleToMessagePadding),\n          GitHubRedirectButton(\n            title: 'Unexpected error',\n            message: message,\n            stackTrace: stackTrace,\n          ),\n          const SizedBox(height: _titleToMessagePadding),\n          if (stackTrace != null) StackTracePreview(stackTrace!),\n          if (actions != null)\n            Row(\n              mainAxisAlignment: MainAxisAlignment.end,\n              children: actions!,\n            ),\n        ],\n      ),\n    );\n  }\n}\n\nclass StackTracePreview extends StatelessWidget {\n  const StackTracePreview(\n    this.stackTrace, {\n    super.key,\n  });\n\n  final String stackTrace;\n\n  @override\n  Widget build(BuildContext context) {\n    return ConstrainedBox(\n      constraints: const BoxConstraints(\n        minWidth: 350,\n        maxWidth: 450,\n      ),\n      child: Card(\n        elevation: 0,\n        shape: RoundedRectangleBorder(\n          borderRadius: BorderRadius.circular(8),\n        ),\n        clipBehavior: Clip.antiAlias,\n        margin: EdgeInsets.zero,\n        child: Padding(\n          padding: const EdgeInsets.all(8.0),\n          child: Column(\n            children: [\n              const Align(\n                alignment: Alignment.centerLeft,\n                child: FlowyText.semibold(\n                  \"Stack Trace\",\n                ),\n              ),\n              Container(\n                height: 120,\n                padding: const EdgeInsets.symmetric(vertical: 8),\n                child: SingleChildScrollView(\n                  child: Text(\n                    stackTrace,\n                    style: Theme.of(context).textTheme.bodySmall,\n                  ),\n                ),\n              ),\n              Align(\n                alignment: Alignment.centerRight,\n                child: FlowyButton(\n                  hoverColor: AFThemeExtension.of(context).onBackground,\n                  text: const FlowyText(\n                    \"Copy\",\n                  ),\n                  useIntrinsicWidth: true,\n                  onTap: () => getIt<ClipboardService>().setData(\n                    ClipboardServiceData(plainText: stackTrace),\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass GitHubRedirectButton extends StatelessWidget {\n  const GitHubRedirectButton({\n    super.key,\n    this.title,\n    this.message,\n    this.stackTrace,\n  });\n\n  final String? title;\n  final String? message;\n  final String? stackTrace;\n\n  static const _height = 32.0;\n\n  Uri get _gitHubNewBugUri => Uri(\n        scheme: 'https',\n        host: 'github.com',\n        path: '/AppFlowy-IO/AppFlowy/issues/new',\n        query:\n            'assignees=&labels=&projects=&template=bug_report.yaml&os=$_platform&title=%5BBug%5D+$title&context=$_contextString',\n      );\n\n  String get _contextString {\n    if (message == null && stackTrace == null) {\n      return '';\n    }\n\n    String msg = \"\";\n    if (message != null) {\n      msg += 'Error message:%0A```%0A$message%0A```%0A';\n    }\n\n    if (stackTrace != null) {\n      msg += 'StackTrace:%0A```%0A$stackTrace%0A```%0A';\n    }\n\n    return msg;\n  }\n\n  String get _platform {\n    if (kIsWeb) {\n      return 'Web';\n    }\n\n    return Platform.operatingSystem;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      leftIconSize: const Size.square(_height),\n      text: FlowyText(LocaleKeys.appName.tr()),\n      useIntrinsicWidth: true,\n      leftIcon: const Padding(\n        padding: EdgeInsets.all(4.0),\n        child: FlowySvg(FlowySvgData('login/github-mark')),\n      ),\n      onTap: () async {\n        await afLaunchUri(_gitHubNewBugUri);\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/feature_flags.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:collection/collection.dart';\n\ntypedef FeatureFlagMap = Map<FeatureFlag, bool>;\n\n/// The [FeatureFlag] is used to control the front-end features of the app.\n///\n/// For example, if your feature is still under development,\n///   you can set the value to `false` to hide the feature.\nenum FeatureFlag {\n  // used to control the visibility of the collaborative workspace feature\n  // if it's on, you can see the workspace list and the workspace settings\n  // in the top-left corner of the app\n  collaborativeWorkspace,\n\n  // used to control the visibility of the members settings\n  // if it's on, you can see the members settings in the settings page\n  membersSettings,\n\n  // used to control the sync feature of the document\n  // if it's on, the document will be synced the events from server in real-time\n  syncDocument,\n\n  // used to control the sync feature of the database\n  // if it's on, the collaborators will show in the database\n  syncDatabase,\n\n  // used for the search feature\n  search,\n\n  // used for controlling whether to show plan+billing options in settings\n  planBilling,\n\n  // used for space design\n  spaceDesign,\n\n  // used for the inline sub-page mention\n  inlineSubPageMention,\n\n  // used for the shared section\n  sharedSection,\n\n  // used for ignore the conflicted feature flag\n  unknown;\n\n  static Future<void> initialize() async {\n    final values = await getIt<KeyValueStorage>().getWithFormat<FeatureFlagMap>(\n          KVKeys.featureFlag,\n          (value) => Map.from(jsonDecode(value)).map(\n            (key, value) {\n              final k = FeatureFlag.values.firstWhereOrNull(\n                    (e) => e.name == key,\n                  ) ??\n                  FeatureFlag.unknown;\n              return MapEntry(k, value as bool);\n            },\n          ),\n        ) ??\n        {};\n\n    _values = {\n      ...{for (final flag in FeatureFlag.values) flag: false},\n      ...values,\n    };\n  }\n\n  static UnmodifiableMapView<FeatureFlag, bool> get data =>\n      UnmodifiableMapView(_values);\n\n  Future<void> turnOn() async {\n    await update(true);\n  }\n\n  Future<void> turnOff() async {\n    await update(false);\n  }\n\n  Future<void> update(bool value) async {\n    _values[this] = value;\n\n    await getIt<KeyValueStorage>().set(\n      KVKeys.featureFlag,\n      jsonEncode(\n        _values.map((key, value) => MapEntry(key.name, value)),\n      ),\n    );\n  }\n\n  static Future<void> clear() async {\n    _values = {};\n    await getIt<KeyValueStorage>().remove(KVKeys.featureFlag);\n  }\n\n  bool get isOn {\n    if ([\n      FeatureFlag.planBilling,\n      // release this feature in version 0.6.1\n      FeatureFlag.spaceDesign,\n      // release this feature in version 0.5.9\n      FeatureFlag.search,\n      // release this feature in version 0.5.6\n      FeatureFlag.collaborativeWorkspace,\n      FeatureFlag.membersSettings,\n      // release this feature in version 0.5.4\n      FeatureFlag.syncDatabase,\n      FeatureFlag.syncDocument,\n      FeatureFlag.inlineSubPageMention,\n    ].contains(this)) {\n      return true;\n    }\n\n    if (_values.containsKey(this)) {\n      return _values[this]!;\n    }\n\n    switch (this) {\n      case FeatureFlag.planBilling:\n      case FeatureFlag.search:\n      case FeatureFlag.syncDocument:\n      case FeatureFlag.syncDatabase:\n      case FeatureFlag.spaceDesign:\n      case FeatureFlag.inlineSubPageMention:\n      case FeatureFlag.collaborativeWorkspace:\n      case FeatureFlag.membersSettings:\n        return true;\n      case FeatureFlag.sharedSection:\n      case FeatureFlag.unknown:\n        return false;\n    }\n  }\n\n  String get description {\n    switch (this) {\n      case FeatureFlag.collaborativeWorkspace:\n        return 'if it\\'s on, you can see the workspace list and the workspace settings in the top-left corner of the app';\n      case FeatureFlag.membersSettings:\n        return 'if it\\'s on, you can see the members settings in the settings page';\n      case FeatureFlag.syncDocument:\n        return 'if it\\'s on, the document will be synced in real-time';\n      case FeatureFlag.syncDatabase:\n        return 'if it\\'s on, the collaborators will show in the database';\n      case FeatureFlag.search:\n        return 'if it\\'s on, the command palette and search button will be available';\n      case FeatureFlag.planBilling:\n        return 'if it\\'s on, plan and billing pages will be available in Settings';\n      case FeatureFlag.spaceDesign:\n        return 'if it\\'s on, the space design feature will be available';\n      case FeatureFlag.inlineSubPageMention:\n        return 'if it\\'s on, the inline sub-page mention feature will be available';\n      case FeatureFlag.sharedSection:\n        return 'if it\\'s on, the shared section will be available';\n      case FeatureFlag.unknown:\n        return '';\n    }\n  }\n\n  String get key => 'appflowy_feature_flag_${toString()}';\n}\n\nFeatureFlagMap _values = {};\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/feedback_gesture_detector.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nenum HapticFeedbackType {\n  light,\n  medium,\n  heavy,\n  selection,\n  vibrate;\n\n  void call() {\n    switch (this) {\n      case HapticFeedbackType.light:\n        HapticFeedback.lightImpact();\n        break;\n      case HapticFeedbackType.medium:\n        HapticFeedback.mediumImpact();\n        break;\n      case HapticFeedbackType.heavy:\n        HapticFeedback.heavyImpact();\n        break;\n      case HapticFeedbackType.selection:\n        HapticFeedback.selectionClick();\n        break;\n      case HapticFeedbackType.vibrate:\n        HapticFeedback.vibrate();\n        break;\n    }\n  }\n}\n\nclass FeedbackGestureDetector extends GestureDetector {\n  FeedbackGestureDetector({\n    super.key,\n    HitTestBehavior behavior = HitTestBehavior.opaque,\n    HapticFeedbackType feedbackType = HapticFeedbackType.light,\n    required Widget child,\n    required VoidCallback onTap,\n  }) : super(\n          behavior: behavior,\n          onTap: () {\n            feedbackType.call();\n            onTap();\n          },\n          child: child,\n        );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/flowy_error_page.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/animated_gesture.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass AppFlowyErrorPage extends StatelessWidget {\n  const AppFlowyErrorPage({\n    super.key,\n    this.error,\n  });\n\n  final FlowyError? error;\n\n  @override\n  Widget build(BuildContext context) {\n    if (UniversalPlatform.isMobile) {\n      return _MobileSyncErrorPage(error: error);\n    } else {\n      return _DesktopSyncErrorPage(error: error);\n    }\n  }\n}\n\nclass _MobileSyncErrorPage extends StatelessWidget {\n  const _MobileSyncErrorPage({\n    this.error,\n  });\n\n  final FlowyError? error;\n\n  @override\n  Widget build(BuildContext context) {\n    return AnimatedGestureDetector(\n      scaleFactor: 0.99,\n      onTapUp: () {\n        getIt<ClipboardService>().setPlainText(error.toString());\n        showToastNotification(\n          message: LocaleKeys.message_copy_success.tr(),\n          bottomPadding: 0,\n        );\n      },\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          const FlowySvg(\n            FlowySvgs.icon_warning_xl,\n            blendMode: null,\n          ),\n          const VSpace(16.0),\n          FlowyText.medium(\n            LocaleKeys.error_syncError.tr(),\n            fontSize: 15,\n          ),\n          const VSpace(8.0),\n          Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 24.0),\n            child: FlowyText.regular(\n              LocaleKeys.error_syncErrorHint.tr(),\n              fontSize: 13,\n              color: Theme.of(context).hintColor,\n              textAlign: TextAlign.center,\n              maxLines: 10,\n            ),\n          ),\n          const VSpace(2.0),\n          FlowyText.regular(\n            '(${LocaleKeys.error_clickToCopy.tr()})',\n            fontSize: 13,\n            color: Theme.of(context).hintColor,\n            textAlign: TextAlign.center,\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _DesktopSyncErrorPage extends StatelessWidget {\n  const _DesktopSyncErrorPage({\n    this.error,\n  });\n\n  final FlowyError? error;\n\n  @override\n  Widget build(BuildContext context) {\n    return AnimatedGestureDetector(\n      scaleFactor: 0.995,\n      onTapUp: () {\n        getIt<ClipboardService>().setPlainText(error.toString());\n        showToastNotification(\n          message: LocaleKeys.message_copy_success.tr(),\n          bottomPadding: 0,\n        );\n      },\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          const FlowySvg(\n            FlowySvgs.icon_warning_xl,\n            blendMode: null,\n          ),\n          const VSpace(16.0),\n          FlowyText.medium(\n            error?.code.toString() ?? '',\n            fontSize: 16,\n          ),\n          const VSpace(8.0),\n          RichText(\n            text: TextSpan(\n              children: [\n                TextSpan(\n                  text: LocaleKeys.errorDialog_howToFixFallbackHint1.tr(),\n                  style: TextStyle(\n                    fontSize: 14,\n                    color: Theme.of(context).hintColor,\n                  ),\n                ),\n                TextSpan(\n                  text: 'Github',\n                  style: TextStyle(\n                    fontSize: 14,\n                    color: Theme.of(context).colorScheme.primary,\n                    decoration: TextDecoration.underline,\n                  ),\n                  recognizer: TapGestureRecognizer()\n                    ..onTap = () {\n                      afLaunchUrlString(\n                        'https://github.com/AppFlowy-IO/AppFlowy/issues/new?template=bug_report.yaml',\n                      );\n                    },\n                ),\n                TextSpan(\n                  text: LocaleKeys.errorDialog_howToFixFallbackHint2.tr(),\n                  style: TextStyle(\n                    fontSize: 14,\n                    color: Theme.of(context).hintColor,\n                  ),\n                ),\n              ],\n            ),\n          ),\n          const VSpace(8.0),\n          FlowyText.regular(\n            '(${LocaleKeys.error_clickToCopy.tr()})',\n            fontSize: 14,\n            color: Theme.of(context).hintColor,\n            textAlign: TextAlign.center,\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/flowy_gradient_colors.dart",
    "content": "import 'package:flutter/material.dart';\n\nenum FlowyGradientColor {\n  gradient1,\n  gradient2,\n  gradient3,\n  gradient4,\n  gradient5,\n  gradient6,\n  gradient7;\n\n  static FlowyGradientColor fromId(String id) {\n    return FlowyGradientColor.values.firstWhere(\n      (element) => element.id == id,\n      orElse: () => FlowyGradientColor.gradient1,\n    );\n  }\n\n  String get id {\n    // DON'T change this name because it's saved in the database!\n    switch (this) {\n      case FlowyGradientColor.gradient1:\n        return 'appflowy_them_color_gradient1';\n      case FlowyGradientColor.gradient2:\n        return 'appflowy_them_color_gradient2';\n      case FlowyGradientColor.gradient3:\n        return 'appflowy_them_color_gradient3';\n      case FlowyGradientColor.gradient4:\n        return 'appflowy_them_color_gradient4';\n      case FlowyGradientColor.gradient5:\n        return 'appflowy_them_color_gradient5';\n      case FlowyGradientColor.gradient6:\n        return 'appflowy_them_color_gradient6';\n      case FlowyGradientColor.gradient7:\n        return 'appflowy_them_color_gradient7';\n    }\n  }\n\n  LinearGradient get linear {\n    switch (this) {\n      case FlowyGradientColor.gradient1:\n        return const LinearGradient(\n          begin: Alignment(-0.35, -0.94),\n          end: Alignment(0.35, 0.94),\n          colors: [Color(0xFF34BDAF), Color(0xFFB682D4)],\n        );\n      case FlowyGradientColor.gradient2:\n        return const LinearGradient(\n          begin: Alignment(0.00, -1.00),\n          end: Alignment(0, 1),\n          colors: [Color(0xFF4CC2CC), Color(0xFFE17570)],\n        );\n      case FlowyGradientColor.gradient3:\n        return const LinearGradient(\n          begin: Alignment(0.00, -1.00),\n          end: Alignment(0, 1),\n          colors: [Color(0xFFAF70E0), Color(0xFFED7196)],\n        );\n      case FlowyGradientColor.gradient4:\n        return const LinearGradient(\n          begin: Alignment(0.00, -1.00),\n          end: Alignment(0, 1),\n          colors: [Color(0xFFA348D6), Color(0xFF44A7DE)],\n        );\n      case FlowyGradientColor.gradient5:\n        return const LinearGradient(\n          begin: Alignment(0.38, -0.93),\n          end: Alignment(-0.38, 0.93),\n          colors: [Color(0xFF5749C9), Color(0xFFBB4997)],\n        );\n      case FlowyGradientColor.gradient6:\n        return const LinearGradient(\n          begin: Alignment(0.00, -1.00),\n          end: Alignment(0, 1),\n          colors: [Color(0xFF036FFA), Color(0xFF00B8E5)],\n        );\n      case FlowyGradientColor.gradient7:\n        return const LinearGradient(\n          begin: Alignment(0.62, -0.79),\n          end: Alignment(-0.62, 0.79),\n          colors: [Color(0xFFF0C6CF), Color(0xFFDECCE2), Color(0xFFCAD3F9)],\n        );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/google_fonts_extension.dart",
    "content": "import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:flutter/material.dart';\nimport 'package:google_fonts/google_fonts.dart';\n\nconst _defaultFontFamilies = [\n  defaultFontFamily,\n  builtInCodeFontFamily,\n];\n\n// if the font family is not available, google fonts packages will throw an exception\n// this method will return the system font family if the font family is not available\nTextStyle getGoogleFontSafely(\n  String fontFamily, {\n  FontWeight? fontWeight,\n  double? fontSize,\n  Color? fontColor,\n  double? letterSpacing,\n  double? lineHeight,\n}) {\n  // if the font family is the built-in font family, we can use it directly\n  if (_defaultFontFamilies.contains(fontFamily)) {\n    return TextStyle(\n      fontFamily: fontFamily.isEmpty ? null : fontFamily,\n      fontWeight: fontWeight,\n      fontSize: fontSize,\n      color: fontColor,\n      letterSpacing: letterSpacing,\n      height: lineHeight,\n    );\n  } else {\n    try {\n      return GoogleFonts.getFont(\n        fontFamily,\n        fontWeight: fontWeight,\n        fontSize: fontSize,\n        color: fontColor,\n        letterSpacing: letterSpacing,\n        height: lineHeight,\n      );\n    } catch (_) {}\n  }\n\n  return TextStyle(\n    fontWeight: fontWeight,\n    fontSize: fontSize,\n    color: fontColor,\n    letterSpacing: letterSpacing,\n    height: lineHeight,\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/icon_emoji_picker/colors.dart",
    "content": "import 'package:appflowy/util/theme_extension.dart';\nimport 'package:flutter/material.dart';\n\nextension PickerColors on BuildContext {\n  Color get pickerTextColor {\n    return Theme.of(this).isLightMode\n        ? const Color(0x80171717)\n        : Colors.white.withValues(alpha: 0.5);\n  }\n\n  Color get pickerIconColor {\n    return Theme.of(this).isLightMode ? const Color(0xFF171717) : Colors.white;\n  }\n\n  Color get pickerSearchBarBorderColor {\n    return Theme.of(this).isLightMode\n        ? const Color(0x1E171717)\n        : Colors.white.withValues(alpha: 0.12);\n  }\n\n  Color get pickerButtonBoarderColor {\n    return Theme.of(this).isLightMode\n        ? const Color(0x1E171717)\n        : Colors.white.withValues(alpha: 0.12);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_search_bar.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_emoji_mart/flutter_emoji_mart.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'colors.dart';\n\ntypedef EmojiKeywordChangedCallback = void Function(String keyword);\ntypedef EmojiSkinToneChanged = void Function(EmojiSkinTone skinTone);\n\nclass FlowyEmojiSearchBar extends StatefulWidget {\n  const FlowyEmojiSearchBar({\n    super.key,\n    this.ensureFocus = false,\n    required this.emojiData,\n    required this.onKeywordChanged,\n    required this.onSkinToneChanged,\n    required this.onRandomEmojiSelected,\n  });\n\n  final bool ensureFocus;\n  final EmojiData emojiData;\n  final EmojiKeywordChangedCallback onKeywordChanged;\n  final EmojiSkinToneChanged onSkinToneChanged;\n  final EmojiSelectedCallback onRandomEmojiSelected;\n\n  @override\n  State<FlowyEmojiSearchBar> createState() => _FlowyEmojiSearchBarState();\n}\n\nclass _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> {\n  final TextEditingController controller = TextEditingController();\n  EmojiSkinTone skinTone = lastSelectedEmojiSkinTone ?? EmojiSkinTone.none;\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: EdgeInsets.symmetric(\n        vertical: 12.0,\n        horizontal: UniversalPlatform.isDesktopOrWeb ? 0.0 : 8.0,\n      ),\n      child: Row(\n        children: [\n          Expanded(\n            child: _SearchTextField(\n              onKeywordChanged: widget.onKeywordChanged,\n              ensureFocus: widget.ensureFocus,\n            ),\n          ),\n          const HSpace(8.0),\n          _RandomEmojiButton(\n            skinTone: skinTone,\n            emojiData: widget.emojiData,\n            onRandomEmojiSelected: widget.onRandomEmojiSelected,\n          ),\n          const HSpace(8.0),\n          FlowyEmojiSkinToneSelector(\n            onEmojiSkinToneChanged: (v) {\n              setState(() {\n                skinTone = v;\n              });\n              widget.onSkinToneChanged.call(v);\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _RandomEmojiButton extends StatelessWidget {\n  const _RandomEmojiButton({\n    required this.skinTone,\n    required this.emojiData,\n    required this.onRandomEmojiSelected,\n  });\n\n  final EmojiSkinTone skinTone;\n  final EmojiData emojiData;\n  final EmojiSelectedCallback onRandomEmojiSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      width: 36,\n      height: 36,\n      decoration: ShapeDecoration(\n        shape: RoundedRectangleBorder(\n          side: BorderSide(color: context.pickerButtonBoarderColor),\n          borderRadius: BorderRadius.circular(8),\n        ),\n      ),\n      child: FlowyTooltip(\n        message: LocaleKeys.emoji_random.tr(),\n        child: FlowyButton(\n          useIntrinsicWidth: true,\n          text: const FlowySvg(\n            FlowySvgs.icon_shuffle_s,\n          ),\n          onTap: () {\n            final random = emojiData.random;\n            final emojiId = random.$1;\n            final emoji = emojiData.getEmojiById(\n              emojiId,\n              skinTone: skinTone,\n            );\n            onRandomEmojiSelected(\n              emojiId,\n              emoji,\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass _SearchTextField extends StatefulWidget {\n  const _SearchTextField({\n    required this.onKeywordChanged,\n    this.ensureFocus = false,\n  });\n\n  final EmojiKeywordChangedCallback onKeywordChanged;\n  final bool ensureFocus;\n\n  @override\n  State<_SearchTextField> createState() => _SearchTextFieldState();\n}\n\nclass _SearchTextFieldState extends State<_SearchTextField> {\n  final TextEditingController controller = TextEditingController();\n  final FocusNode focusNode = FocusNode();\n\n  @override\n  void initState() {\n    super.initState();\n\n    /// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState]\n    /// this is to ensure that focus can be regained within a short period of time\n    if (widget.ensureFocus) {\n      Future.delayed(const Duration(milliseconds: 200), () {\n        if (!mounted || focusNode.hasFocus) return;\n        focusNode.requestFocus();\n      });\n    }\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    focusNode.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 36.0,\n      child: FlowyTextField(\n        focusNode: focusNode,\n        hintText: LocaleKeys.search_label.tr(),\n        hintStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(\n              fontSize: 14.0,\n              fontWeight: FontWeight.w400,\n              color: Theme.of(context).hintColor,\n            ),\n        enableBorderColor: context.pickerSearchBarBorderColor,\n        controller: controller,\n        onChanged: widget.onKeywordChanged,\n        prefixIcon: const Padding(\n          padding: EdgeInsets.only(\n            left: 14.0,\n            right: 8.0,\n          ),\n          child: FlowySvg(\n            FlowySvgs.search_s,\n          ),\n        ),\n        prefixIconConstraints: const BoxConstraints(\n          maxHeight: 20.0,\n        ),\n        suffixIcon: Padding(\n          padding: const EdgeInsets.all(4.0),\n          child: FlowyButton(\n            text: const FlowySvg(\n              FlowySvgs.m_app_bar_close_s,\n            ),\n            margin: EdgeInsets.zero,\n            useIntrinsicWidth: true,\n            onTap: () {\n              if (controller.text.isNotEmpty) {\n                controller.clear();\n                widget.onKeywordChanged('');\n              } else {\n                focusNode.unfocus();\n              }\n            },\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_skin_tone.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_emoji_mart/flutter_emoji_mart.dart';\n\nimport 'colors.dart';\n\n// use a temporary global value to store last selected skin tone\nEmojiSkinTone? lastSelectedEmojiSkinTone;\n\n@visibleForTesting\nValueKey emojiSkinToneKey(String icon) {\n  return ValueKey('emoji_skin_tone_$icon');\n}\n\nclass FlowyEmojiSkinToneSelector extends StatefulWidget {\n  const FlowyEmojiSkinToneSelector({\n    super.key,\n    required this.onEmojiSkinToneChanged,\n  });\n\n  final EmojiSkinToneChanged onEmojiSkinToneChanged;\n\n  @override\n  State<FlowyEmojiSkinToneSelector> createState() =>\n      _FlowyEmojiSkinToneSelectorState();\n}\n\nclass _FlowyEmojiSkinToneSelectorState\n    extends State<FlowyEmojiSkinToneSelector> {\n  EmojiSkinTone skinTone = EmojiSkinTone.none;\n  final controller = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      direction: PopoverDirection.bottomWithCenterAligned,\n      controller: controller,\n      popupBuilder: (context) {\n        return Row(\n          mainAxisAlignment: MainAxisAlignment.spaceBetween,\n          children: EmojiSkinTone.values\n              .map(\n                (e) => _buildIconButton(\n                  e.icon,\n                  () {\n                    setState(() => lastSelectedEmojiSkinTone = e);\n                    widget.onEmojiSkinToneChanged(e);\n                    controller.close();\n                  },\n                ),\n              )\n              .toList(),\n        );\n      },\n      child: FlowyTooltip(\n        message: LocaleKeys.emoji_selectSkinTone.tr(),\n        child: _buildIconButton(\n          lastSelectedEmojiSkinTone?.icon ?? '👋',\n          () => controller.show(),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildIconButton(String icon, VoidCallback onPressed) {\n    return Container(\n      width: 36,\n      height: 36,\n      decoration: BoxDecoration(\n        border: Border.all(color: context.pickerButtonBoarderColor),\n        borderRadius: BorderRadius.circular(8),\n      ),\n      child: FlowyButton(\n        key: emojiSkinToneKey(icon),\n        margin: EdgeInsets.zero,\n        text: FlowyText.emoji(\n          icon,\n          fontSize: 24.0,\n        ),\n        onTap: onPressed,\n      ),\n    );\n  }\n}\n\nextension EmojiSkinToneIcon on EmojiSkinTone {\n  String get icon {\n    switch (this) {\n      case EmojiSkinTone.none:\n        return '👋';\n      case EmojiSkinTone.light:\n        return '👋🏻';\n      case EmojiSkinTone.mediumLight:\n        return '👋🏼';\n      case EmojiSkinTone.medium:\n        return '👋🏽';\n      case EmojiSkinTone.mediumDark:\n        return '👋🏾';\n      case EmojiSkinTone.dark:\n        return '👋🏿';\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart",
    "content": "import 'dart:math';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/base/emoji/emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/icon.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart' hide Icon;\nimport 'package:flutter/services.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'icon_uploader.dart';\n\nextension ToProto on FlowyIconType {\n  ViewIconTypePB toProto() {\n    switch (this) {\n      case FlowyIconType.emoji:\n        return ViewIconTypePB.Emoji;\n      case FlowyIconType.icon:\n        return ViewIconTypePB.Icon;\n      case FlowyIconType.custom:\n        return ViewIconTypePB.Url;\n    }\n  }\n}\n\nextension FromProto on ViewIconTypePB {\n  FlowyIconType fromProto() {\n    switch (this) {\n      case ViewIconTypePB.Emoji:\n        return FlowyIconType.emoji;\n      case ViewIconTypePB.Icon:\n        return FlowyIconType.icon;\n      case ViewIconTypePB.Url:\n        return FlowyIconType.custom;\n      default:\n        return FlowyIconType.custom;\n    }\n  }\n}\n\nextension ToEmojiIconData on ViewIconPB {\n  EmojiIconData toEmojiIconData() => EmojiIconData(ty.fromProto(), value);\n}\n\nenum FlowyIconType {\n  emoji,\n  icon,\n  custom;\n}\n\nextension FlowyIconTypeToPickerTabType on FlowyIconType {\n  PickerTabType? toPickerTabType() => name.toPickerTabType();\n}\n\nclass EmojiIconData {\n  factory EmojiIconData.none() => const EmojiIconData(FlowyIconType.icon, '');\n\n  factory EmojiIconData.emoji(String emoji) =>\n      EmojiIconData(FlowyIconType.emoji, emoji);\n\n  factory EmojiIconData.icon(IconsData icon) =>\n      EmojiIconData(FlowyIconType.icon, icon.iconString);\n\n  factory EmojiIconData.custom(String url) =>\n      EmojiIconData(FlowyIconType.custom, url);\n\n  const EmojiIconData(\n    this.type,\n    this.emoji,\n  );\n\n  final FlowyIconType type;\n  final String emoji;\n\n  static EmojiIconData fromViewIconPB(ViewIconPB v) {\n    return EmojiIconData(v.ty.fromProto(), v.value);\n  }\n\n  ViewIconPB toViewIcon() {\n    return ViewIconPB()\n      ..ty = type.toProto()\n      ..value = emoji;\n  }\n\n  bool get isEmpty => emoji.isEmpty;\n\n  bool get isNotEmpty => emoji.isNotEmpty;\n}\n\nclass SelectedEmojiIconResult {\n  SelectedEmojiIconResult(this.data, this.keepOpen);\n\n  final EmojiIconData data;\n  final bool keepOpen;\n\n  FlowyIconType get type => data.type;\n\n  String get emoji => data.emoji;\n}\n\nextension EmojiIconDataToSelectedResultExtension on EmojiIconData {\n  SelectedEmojiIconResult toSelectedResult({bool keepOpen = false}) =>\n      SelectedEmojiIconResult(this, keepOpen);\n}\n\nclass FlowyIconEmojiPicker extends StatefulWidget {\n  const FlowyIconEmojiPicker({\n    super.key,\n    this.onSelectedEmoji,\n    this.initialType,\n    this.documentId,\n    this.enableBackgroundColorSelection = true,\n    this.tabs = const [\n      PickerTabType.emoji,\n      PickerTabType.icon,\n    ],\n  });\n\n  final ValueChanged<SelectedEmojiIconResult>? onSelectedEmoji;\n  final bool enableBackgroundColorSelection;\n  final List<PickerTabType> tabs;\n  final PickerTabType? initialType;\n  final String? documentId;\n\n  @override\n  State<FlowyIconEmojiPicker> createState() => _FlowyIconEmojiPickerState();\n}\n\nclass _FlowyIconEmojiPickerState extends State<FlowyIconEmojiPicker>\n    with SingleTickerProviderStateMixin {\n  late TabController controller;\n  int currentIndex = 0;\n\n  @override\n  void initState() {\n    super.initState();\n    final initialType = widget.initialType;\n    if (initialType != null) {\n      currentIndex = max(widget.tabs.indexOf(initialType), 0);\n    }\n    controller = TabController(\n      initialIndex: currentIndex,\n      length: widget.tabs.length,\n      vsync: this,\n    );\n    controller.addListener(() {\n      final currentType = widget.tabs[currentIndex];\n      if (currentType == PickerTabType.custom) {\n        SystemChannels.textInput.invokeMethod('TextInput.hide');\n      }\n    });\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        Container(\n          height: 46,\n          padding: const EdgeInsets.only(left: 4.0, right: 12.0),\n          child: Row(\n            children: [\n              Expanded(\n                child: PickerTab(\n                  controller: controller,\n                  tabs: widget.tabs,\n                  onTap: (index) => currentIndex = index,\n                ),\n              ),\n              _RemoveIconButton(\n                onTap: () {\n                  widget.onSelectedEmoji\n                      ?.call(EmojiIconData.none().toSelectedResult());\n                },\n              ),\n            ],\n          ),\n        ),\n        const FlowyDivider(),\n        Expanded(\n          child: TabBarView(\n            controller: controller,\n            children: widget.tabs.map((tab) {\n              switch (tab) {\n                case PickerTabType.emoji:\n                  return _buildEmojiPicker();\n                case PickerTabType.icon:\n                  return _buildIconPicker();\n                case PickerTabType.custom:\n                  return _buildIconUploader();\n              }\n            }).toList(),\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildEmojiPicker() {\n    return FlowyEmojiPicker(\n      ensureFocus: true,\n      emojiPerLine: _getEmojiPerLine(context),\n      onEmojiSelected: (r) {\n        widget.onSelectedEmoji?.call(\n          EmojiIconData.emoji(r.emoji).toSelectedResult(keepOpen: r.isRandom),\n        );\n        SystemChannels.textInput.invokeMethod('TextInput.hide');\n      },\n    );\n  }\n\n  int _getEmojiPerLine(BuildContext context) {\n    if (UniversalPlatform.isDesktopOrWeb) {\n      return 9;\n    }\n    final width = MediaQuery.of(context).size.width;\n    return width ~/ 40.0; // the size of the emoji\n  }\n\n  Widget _buildIconPicker() {\n    return FlowyIconPicker(\n      ensureFocus: true,\n      enableBackgroundColorSelection: widget.enableBackgroundColorSelection,\n      onSelectedIcon: (r) {\n        widget.onSelectedEmoji?.call(\n          r.data.toEmojiIconData().toSelectedResult(keepOpen: r.isRandom),\n        );\n        SystemChannels.textInput.invokeMethod('TextInput.hide');\n      },\n    );\n  }\n\n  Widget _buildIconUploader() {\n    return IconUploader(\n      documentId: widget.documentId ?? '',\n      ensureFocus: true,\n      onUrl: (url) {\n        widget.onSelectedEmoji\n            ?.call(SelectedEmojiIconResult(EmojiIconData.custom(url), false));\n      },\n    );\n  }\n}\n\nclass _RemoveIconButton extends StatelessWidget {\n  const _RemoveIconButton({required this.onTap});\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 32,\n      child: FlowyButton(\n        onTap: onTap,\n        useIntrinsicWidth: true,\n        text: FlowyText(\n          fontSize: 14.0,\n          figmaLineHeight: 16.0,\n          fontWeight: FontWeight.w500,\n          LocaleKeys.button_remove.tr(),\n          color: Theme.of(context).hintColor,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon.dart",
    "content": "import 'package:json_annotation/json_annotation.dart';\n\npart 'icon.g.dart';\n\n@JsonSerializable()\nclass IconGroup {\n  factory IconGroup.fromJson(Map<String, dynamic> json) {\n    final group = _$IconGroupFromJson(json);\n    // Set the iconGroup reference for each icon\n    for (final icon in group.icons) {\n      icon.iconGroup = group;\n    }\n    return group;\n  }\n\n  factory IconGroup.fromMapEntry(MapEntry<String, dynamic> entry) =>\n      IconGroup.fromJson({\n        'name': entry.key,\n        'icons': entry.value,\n      });\n\n  IconGroup({\n    required this.name,\n    required this.icons,\n  }) {\n    // Set the iconGroup reference for each icon\n    for (final icon in icons) {\n      icon.iconGroup = this;\n    }\n  }\n\n  final String name;\n  final List<Icon> icons;\n\n  String get displayName => name.replaceAll('_', ' ');\n\n  IconGroup filter(String keyword) {\n    final lowercaseKey = keyword.toLowerCase();\n    final filteredIcons = icons\n        .where(\n          (icon) =>\n              icon.keywords\n                  .any((k) => k.toLowerCase().contains(lowercaseKey)) ||\n              icon.name.toLowerCase().contains(lowercaseKey),\n        )\n        .toList();\n    return IconGroup(name: name, icons: filteredIcons);\n  }\n\n  String? getSvgContent(String iconName) {\n    final icon = icons.firstWhere(\n      (icon) => icon.name == iconName,\n    );\n    return icon.content;\n  }\n\n  Map<String, dynamic> toJson() => _$IconGroupToJson(this);\n}\n\n@JsonSerializable()\nclass Icon {\n  factory Icon.fromJson(Map<String, dynamic> json) => _$IconFromJson(json);\n\n  Icon({\n    required this.name,\n    required this.keywords,\n    required this.content,\n  });\n\n  final String name;\n  final List<String> keywords;\n  final String content;\n\n  // Add reference to parent IconGroup\n  IconGroup? iconGroup;\n\n  String get displayName => name.replaceAll('-', ' ');\n\n  Map<String, dynamic> toJson() => _$IconToJson(this);\n\n  String get iconPath {\n    if (iconGroup == null) {\n      return '';\n    }\n    return '${iconGroup!.name}/$name';\n  }\n}\n\nclass RecentIcon {\n  factory RecentIcon.fromJson(Map<String, dynamic> json) =>\n      RecentIcon(_$IconFromJson(json), json['groupName'] ?? '');\n\n  RecentIcon(this.icon, this.groupName);\n\n  final Icon icon;\n  final String groupName;\n\n  String get name => icon.name;\n\n  List<String> get keywords => icon.keywords;\n\n  String get content => icon.content;\n\n  Map<String, dynamic> toJson() => _$IconToJson(\n        Icon(name: name, keywords: keywords, content: content),\n      )..addAll({'groupName': groupName});\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_color_picker.dart",
    "content": "import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\n\nclass IconColorPicker extends StatelessWidget {\n  const IconColorPicker({\n    super.key,\n    required this.onSelected,\n  });\n\n  final void Function(String color) onSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return GridView.count(\n      shrinkWrap: true,\n      crossAxisCount: 6,\n      mainAxisSpacing: 4.0,\n      children: builtInSpaceColors.map((color) {\n        return FlowyHover(\n          style: HoverStyle(borderRadius: BorderRadius.circular(8.0)),\n          child: GestureDetector(\n            onTap: () => onSelected(color),\n            child: Container(\n              width: 34,\n              height: 34,\n              padding: const EdgeInsets.all(5.0),\n              child: Container(\n                clipBehavior: Clip.antiAlias,\n                decoration: ShapeDecoration(\n                  color: Color(int.parse(color)),\n                  shape: RoundedRectangleBorder(\n                    side: const BorderSide(color: Color(0x2D333333)),\n                    borderRadius: BorderRadius.circular(8),\n                  ),\n                ),\n              ),\n            ),\n          ),\n        );\n      }).toList(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart",
    "content": "import 'dart:convert';\nimport 'dart:math';\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_search_bar.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';\nimport 'package:appflowy/util/debounce.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart' hide Icon;\nimport 'package:flutter/services.dart';\n\nimport 'colors.dart';\nimport 'icon_color_picker.dart';\n\n// cache the icon groups to avoid loading them multiple times\nList<IconGroup>? kIconGroups;\nconst _kRecentIconGroupName = 'Recent';\n\nextension IconGroupFilter on List<IconGroup> {\n  String? findSvgContent(String key) {\n    final values = key.split('/');\n    if (values.length != 2) {\n      return null;\n    }\n    final groupName = values[0];\n    final iconName = values[1];\n    final svgString = kIconGroups\n        ?.firstWhereOrNull(\n          (group) => group.name == groupName,\n        )\n        ?.icons\n        .firstWhereOrNull(\n          (icon) => icon.name == iconName,\n        )\n        ?.content;\n    return svgString;\n  }\n\n  (IconGroup, Icon) randomIcon() {\n    final random = Random();\n    final group = this[random.nextInt(length)];\n    final icon = group.icons[random.nextInt(group.icons.length)];\n    return (group, icon);\n  }\n}\n\nFuture<List<IconGroup>> loadIconGroups() async {\n  if (kIconGroups != null) {\n    return kIconGroups!;\n  }\n\n  final stopwatch = Stopwatch()..start();\n  final jsonString = await rootBundle.loadString('assets/icons/icons.json');\n  try {\n    final json = jsonDecode(jsonString) as Map<String, dynamic>;\n    final iconGroups = json.entries.map(IconGroup.fromMapEntry).toList();\n    kIconGroups = iconGroups;\n    return iconGroups;\n  } catch (e) {\n    Log.error('Failed to decode icons.json', e);\n    return [];\n  } finally {\n    stopwatch.stop();\n    Log.info('Loaded icon groups in ${stopwatch.elapsedMilliseconds}ms');\n  }\n}\n\nclass IconPickerResult {\n  IconPickerResult(this.data, this.isRandom);\n\n  final IconsData data;\n  final bool isRandom;\n}\n\nextension IconsDataToIconPickerResultExtension on IconsData {\n  IconPickerResult toResult({bool isRandom = false}) =>\n      IconPickerResult(this, isRandom);\n}\n\nclass FlowyIconPicker extends StatefulWidget {\n  const FlowyIconPicker({\n    super.key,\n    required this.onSelectedIcon,\n    required this.enableBackgroundColorSelection,\n    this.iconPerLine = 9,\n    this.ensureFocus = false,\n  });\n\n  final bool enableBackgroundColorSelection;\n  final ValueChanged<IconPickerResult> onSelectedIcon;\n  final int iconPerLine;\n  final bool ensureFocus;\n\n  @override\n  State<FlowyIconPicker> createState() => _FlowyIconPickerState();\n}\n\nclass _FlowyIconPickerState extends State<FlowyIconPicker> {\n  final List<IconGroup> iconGroups = [];\n  bool loaded = false;\n  final ValueNotifier<String> keyword = ValueNotifier('');\n  final debounce = Debounce(duration: const Duration(milliseconds: 150));\n\n  Future<void> loadIcons() async {\n    final localIcons = await loadIconGroups();\n    final recentIcons = await RecentIcons.getIcons();\n    if (recentIcons.isNotEmpty) {\n      final filterRecentIcons = recentIcons\n          .sublist(\n            0,\n            min(recentIcons.length, widget.iconPerLine),\n          )\n          .skipWhile((e) => e.groupName.isEmpty)\n          .map((e) => e.icon)\n          .toList();\n      if (filterRecentIcons.isNotEmpty) {\n        iconGroups.add(\n          IconGroup(\n            name: _kRecentIconGroupName,\n            icons: filterRecentIcons,\n          ),\n        );\n      }\n    }\n    iconGroups.addAll(localIcons);\n    if (mounted) {\n      setState(() {\n        loaded = true;\n      });\n    }\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    loadIcons();\n  }\n\n  @override\n  void dispose() {\n    keyword.dispose();\n    debounce.dispose();\n    iconGroups.clear();\n    loaded = false;\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 16.0),\n          child: IconSearchBar(\n            ensureFocus: widget.ensureFocus,\n            onRandomTap: () {\n              final value = kIconGroups?.randomIcon();\n              if (value == null) {\n                return;\n              }\n              final color = widget.enableBackgroundColorSelection\n                  ? generateRandomSpaceColor()\n                  : null;\n              widget.onSelectedIcon(\n                IconsData(\n                  value.$1.name,\n                  value.$2.name,\n                  color,\n                ).toResult(isRandom: true),\n              );\n              RecentIcons.putIcon(RecentIcon(value.$2, value.$1.name));\n            },\n            onKeywordChanged: (keyword) => {\n              debounce.call(() {\n                this.keyword.value = keyword;\n              }),\n            },\n          ),\n        ),\n        Expanded(\n          child: loaded\n              ? _buildIcons(iconGroups)\n              : const Center(\n                  child: SizedBox.square(\n                    dimension: 24.0,\n                    child: CircularProgressIndicator(\n                      strokeWidth: 2.0,\n                    ),\n                  ),\n                ),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildIcons(List<IconGroup> iconGroups) {\n    return ValueListenableBuilder(\n      valueListenable: keyword,\n      builder: (_, keyword, __) {\n        if (keyword.isNotEmpty) {\n          final filteredIconGroups = iconGroups\n              .map((iconGroup) => iconGroup.filter(keyword))\n              .where((iconGroup) => iconGroup.icons.isNotEmpty)\n              .toList();\n          return IconPicker(\n            iconGroups: filteredIconGroups,\n            enableBackgroundColorSelection:\n                widget.enableBackgroundColorSelection,\n            onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()),\n            iconPerLine: widget.iconPerLine,\n          );\n        }\n        return IconPicker(\n          iconGroups: iconGroups,\n          enableBackgroundColorSelection: widget.enableBackgroundColorSelection,\n          onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()),\n          iconPerLine: widget.iconPerLine,\n        );\n      },\n    );\n  }\n}\n\nclass IconsData {\n  IconsData(this.groupName, this.iconName, this.color);\n\n  final String groupName;\n  final String iconName;\n  final String? color;\n\n  String get iconString => jsonEncode({\n        'groupName': groupName,\n        'iconName': iconName,\n        if (color != null) 'color': color,\n      });\n\n  EmojiIconData toEmojiIconData() => EmojiIconData.icon(this);\n\n  IconsData noColor() => IconsData(groupName, iconName, null);\n\n  static IconsData fromJson(dynamic json) {\n    return IconsData(\n      json['groupName'],\n      json['iconName'],\n      json['color'],\n    );\n  }\n\n  String? get svgString => kIconGroups\n      ?.firstWhereOrNull((group) => group.name == groupName)\n      ?.icons\n      .firstWhereOrNull((icon) => icon.name == iconName)\n      ?.content;\n}\n\nclass IconPicker extends StatefulWidget {\n  const IconPicker({\n    super.key,\n    required this.onSelectedIcon,\n    required this.enableBackgroundColorSelection,\n    required this.iconGroups,\n    required this.iconPerLine,\n  });\n\n  final List<IconGroup> iconGroups;\n  final int iconPerLine;\n  final bool enableBackgroundColorSelection;\n  final ValueChanged<IconsData> onSelectedIcon;\n\n  @override\n  State<IconPicker> createState() => _IconPickerState();\n}\n\nclass _IconPickerState extends State<IconPicker> {\n  final mutex = PopoverMutex();\n  PopoverController? childPopoverController;\n\n  @override\n  void dispose() {\n    super.dispose();\n    childPopoverController = null;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onTap: hideColorSelector,\n      child: NotificationListener(\n        onNotification: (notificationInfo) {\n          if (notificationInfo is ScrollStartNotification) {\n            hideColorSelector();\n          }\n          return true;\n        },\n        child: ListView.builder(\n          itemCount: widget.iconGroups.length,\n          padding: const EdgeInsets.symmetric(horizontal: 16.0),\n          itemBuilder: (context, index) {\n            final iconGroup = widget.iconGroups[index];\n            return Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                FlowyText(\n                  iconGroup.displayName.capitalize(),\n                  fontSize: 12,\n                  figmaLineHeight: 18.0,\n                  color: context.pickerTextColor,\n                ),\n                const VSpace(4.0),\n                GridView.builder(\n                  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\n                    crossAxisCount: widget.iconPerLine,\n                  ),\n                  itemCount: iconGroup.icons.length,\n                  physics: const NeverScrollableScrollPhysics(),\n                  shrinkWrap: true,\n                  itemBuilder: (context, index) {\n                    final icon = iconGroup.icons[index];\n                    return widget.enableBackgroundColorSelection\n                        ? _Icon(\n                            icon: icon,\n                            mutex: mutex,\n                            onOpen: (childPopoverController) {\n                              this.childPopoverController =\n                                  childPopoverController;\n                            },\n                            onSelectedColor: (context, color) {\n                              String groupName = iconGroup.name;\n                              if (groupName == _kRecentIconGroupName) {\n                                groupName = getGroupName(index);\n                              }\n                              widget.onSelectedIcon(\n                                IconsData(\n                                  groupName,\n                                  icon.name,\n                                  color,\n                                ),\n                              );\n                              RecentIcons.putIcon(RecentIcon(icon, groupName));\n                              PopoverContainer.of(context).close();\n                            },\n                          )\n                        : _IconNoBackground(\n                            icon: icon,\n                            onSelectedIcon: () {\n                              String groupName = iconGroup.name;\n                              if (groupName == _kRecentIconGroupName) {\n                                groupName = getGroupName(index);\n                              }\n                              widget.onSelectedIcon(\n                                IconsData(\n                                  groupName,\n                                  icon.name,\n                                  null,\n                                ),\n                              );\n                              RecentIcons.putIcon(RecentIcon(icon, groupName));\n                            },\n                          );\n                  },\n                ),\n                const VSpace(12.0),\n                if (index == widget.iconGroups.length - 1) ...[\n                  const StreamlinePermit(),\n                  const VSpace(12.0),\n                ],\n              ],\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  void hideColorSelector() {\n    childPopoverController?.close();\n    childPopoverController = null;\n  }\n\n  String getGroupName(int index) {\n    final recentIcons = RecentIcons.getIconsSync();\n    try {\n      return recentIcons[index].groupName;\n    } catch (e) {\n      Log.error('getGroupName with index: $index error', e);\n      return '';\n    }\n  }\n}\n\nclass _IconNoBackground extends StatelessWidget {\n  const _IconNoBackground({\n    required this.icon,\n    required this.onSelectedIcon,\n    this.isSelected = false,\n  });\n\n  final Icon icon;\n  final bool isSelected;\n  final VoidCallback onSelectedIcon;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      message: icon.displayName,\n      preferBelow: false,\n      child: FlowyButton(\n        isSelected: isSelected,\n        useIntrinsicWidth: true,\n        onTap: () => onSelectedIcon(),\n        margin: const EdgeInsets.all(8.0),\n        text: Center(\n          child: FlowySvg.string(\n            icon.content,\n            size: const Size.square(20),\n            color: context.pickerIconColor,\n            opacity: 0.7,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _Icon extends StatefulWidget {\n  const _Icon({\n    required this.icon,\n    required this.mutex,\n    required this.onSelectedColor,\n    this.onOpen,\n  });\n\n  final Icon icon;\n  final PopoverMutex mutex;\n  final void Function(BuildContext context, String color) onSelectedColor;\n  final ValueChanged<PopoverController>? onOpen;\n\n  @override\n  State<_Icon> createState() => _IconState();\n}\n\nclass _IconState extends State<_Icon> {\n  final PopoverController _popoverController = PopoverController();\n  bool isSelected = false;\n\n  @override\n  void dispose() {\n    super.dispose();\n    _popoverController.close();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      direction: PopoverDirection.bottomWithCenterAligned,\n      controller: _popoverController,\n      offset: const Offset(0, 6),\n      mutex: widget.mutex,\n      onClose: () {\n        updateIsSelected(false);\n      },\n      clickHandler: PopoverClickHandler.gestureDetector,\n      child: _IconNoBackground(\n        icon: widget.icon,\n        isSelected: isSelected,\n        onSelectedIcon: () {\n          updateIsSelected(true);\n          _popoverController.show();\n          widget.onOpen?.call(_popoverController);\n        },\n      ),\n      popupBuilder: (context) {\n        return Container(\n          padding: const EdgeInsets.all(6.0),\n          child: IconColorPicker(\n            onSelected: (color) => widget.onSelectedColor(context, color),\n          ),\n        );\n      },\n    );\n  }\n\n  void updateIsSelected(bool isSelected) {\n    setState(() {\n      this.isSelected = isSelected;\n    });\n  }\n}\n\nclass StreamlinePermit extends StatelessWidget {\n  const StreamlinePermit({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    // Open source icons from Streamline\n    final textStyle = TextStyle(\n      fontSize: 12.0,\n      height: 18.0 / 12.0,\n      fontWeight: FontWeight.w500,\n      color: context.pickerTextColor,\n    );\n    return RichText(\n      text: TextSpan(\n        children: [\n          TextSpan(\n            text: '${LocaleKeys.emoji_openSourceIconsFrom.tr()} ',\n            style: textStyle,\n          ),\n          TextSpan(\n            text: 'Streamline',\n            style: textStyle.copyWith(\n              decoration: TextDecoration.underline,\n              color: Theme.of(context).colorScheme.primary,\n            ),\n            recognizer: TapGestureRecognizer()\n              ..onTap = () {\n                afLaunchUrlString('https://www.streamlinehq.com/');\n              },\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_search_bar.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_emoji_mart/flutter_emoji_mart.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'colors.dart';\n\ntypedef IconKeywordChangedCallback = void Function(String keyword);\ntypedef EmojiSkinToneChanged = void Function(EmojiSkinTone skinTone);\n\nclass IconSearchBar extends StatefulWidget {\n  const IconSearchBar({\n    super.key,\n    required this.onRandomTap,\n    required this.onKeywordChanged,\n    this.ensureFocus = false,\n  });\n\n  final VoidCallback onRandomTap;\n  final bool ensureFocus;\n  final IconKeywordChangedCallback onKeywordChanged;\n\n  @override\n  State<IconSearchBar> createState() => _IconSearchBarState();\n}\n\nclass _IconSearchBarState extends State<IconSearchBar> {\n  final TextEditingController controller = TextEditingController();\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: EdgeInsets.symmetric(\n        vertical: 12.0,\n        horizontal: UniversalPlatform.isDesktopOrWeb ? 0.0 : 8.0,\n      ),\n      child: Row(\n        children: [\n          Expanded(\n            child: _SearchTextField(\n              onKeywordChanged: widget.onKeywordChanged,\n              ensureFocus: widget.ensureFocus,\n            ),\n          ),\n          const HSpace(8.0),\n          _RandomIconButton(\n            onRandomTap: widget.onRandomTap,\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _RandomIconButton extends StatelessWidget {\n  const _RandomIconButton({\n    required this.onRandomTap,\n  });\n\n  final VoidCallback onRandomTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      width: 36,\n      height: 36,\n      decoration: ShapeDecoration(\n        shape: RoundedRectangleBorder(\n          side: BorderSide(color: context.pickerButtonBoarderColor),\n          borderRadius: BorderRadius.circular(8),\n        ),\n      ),\n      child: FlowyTooltip(\n        message: LocaleKeys.emoji_random.tr(),\n        child: FlowyButton(\n          useIntrinsicWidth: true,\n          text: const FlowySvg(\n            FlowySvgs.icon_shuffle_s,\n          ),\n          onTap: onRandomTap,\n        ),\n      ),\n    );\n  }\n}\n\nclass _SearchTextField extends StatefulWidget {\n  const _SearchTextField({\n    required this.onKeywordChanged,\n    this.ensureFocus = false,\n  });\n\n  final IconKeywordChangedCallback onKeywordChanged;\n  final bool ensureFocus;\n\n  @override\n  State<_SearchTextField> createState() => _SearchTextFieldState();\n}\n\nclass _SearchTextFieldState extends State<_SearchTextField> {\n  final TextEditingController controller = TextEditingController();\n  final FocusNode focusNode = FocusNode();\n\n  @override\n  void initState() {\n    super.initState();\n\n    /// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState]\n    /// this is to ensure that focus can be regained within a short period of time\n    if (widget.ensureFocus) {\n      Future.delayed(const Duration(milliseconds: 200), () {\n        if (!mounted || focusNode.hasFocus) return;\n        focusNode.requestFocus();\n      });\n    }\n  }\n\n  @override\n  void dispose() {\n    controller.dispose();\n    focusNode.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 36.0,\n      child: FlowyTextField(\n        focusNode: focusNode,\n        hintText: LocaleKeys.search_label.tr(),\n        hintStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(\n              fontSize: 14.0,\n              fontWeight: FontWeight.w400,\n              color: Theme.of(context).hintColor,\n            ),\n        enableBorderColor: context.pickerSearchBarBorderColor,\n        controller: controller,\n        onChanged: widget.onKeywordChanged,\n        prefixIcon: const Padding(\n          padding: EdgeInsets.only(\n            left: 14.0,\n            right: 8.0,\n          ),\n          child: FlowySvg(\n            FlowySvgs.search_s,\n          ),\n        ),\n        prefixIconConstraints: const BoxConstraints(\n          maxHeight: 20.0,\n        ),\n        suffixIcon: Padding(\n          padding: const EdgeInsets.all(4.0),\n          child: FlowyButton(\n            text: const FlowySvg(\n              FlowySvgs.m_app_bar_close_s,\n            ),\n            margin: EdgeInsets.zero,\n            useIntrinsicWidth: true,\n            onTap: () {\n              if (controller.text.isNotEmpty) {\n                controller.clear();\n                widget.onKeywordChanged('');\n              } else {\n                focusNode.unfocus();\n              }\n            },\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy/shared/appflowy_network_svg.dart';\nimport 'package:appflowy/shared/custom_image_cache_manager.dart';\nimport 'package:appflowy/shared/patterns/file_type_patterns.dart';\nimport 'package:appflowy/shared/permission/permission_checker.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/util/default_extensions.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:desktop_drop/desktop_drop.dart';\nimport 'package:dotted_border/dotted_border.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/file_picker/file_picker_service.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_svg/flowy_svg.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart';\nimport 'package:image_picker/image_picker.dart';\nimport 'package:string_validator/string_validator.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\n@visibleForTesting\nclass IconUploader extends StatefulWidget {\n  const IconUploader({\n    super.key,\n    required this.onUrl,\n    required this.documentId,\n    this.ensureFocus = false,\n  });\n\n  final ValueChanged<String> onUrl;\n  final String documentId;\n  final bool ensureFocus;\n\n  @override\n  State<IconUploader> createState() => _IconUploaderState();\n}\n\nclass _IconUploaderState extends State<IconUploader> {\n  bool isActive = false;\n  bool isHovering = false;\n  bool isUploading = false;\n\n  final List<_Image> pickedImages = [];\n  final FocusNode focusNode = FocusNode();\n\n  @override\n  void initState() {\n    super.initState();\n\n    /// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState]\n    /// this is to ensure that focus can be regained within a short period of time\n    if (widget.ensureFocus) {\n      Future.delayed(const Duration(milliseconds: 200), () {\n        if (!mounted || focusNode.hasFocus) return;\n        focusNode.requestFocus();\n      });\n    }\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      enableDocumentDragNotifier.value = false;\n    });\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      enableDocumentDragNotifier.value = true;\n    });\n    focusNode.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Shortcuts(\n      shortcuts: {\n        LogicalKeySet(\n          Platform.isMacOS\n              ? LogicalKeyboardKey.meta\n              : LogicalKeyboardKey.control,\n          LogicalKeyboardKey.keyV,\n        ): _PasteIntent(),\n      },\n      child: Actions(\n        actions: {\n          _PasteIntent: CallbackAction<_PasteIntent>(\n            onInvoke: (intent) => pasteAsAnImage(),\n          ),\n        },\n        child: Focus(\n          autofocus: true,\n          focusNode: focusNode,\n          child: Padding(\n            padding: const EdgeInsets.all(16),\n            child: Column(\n              children: [\n                Expanded(\n                  child: DropTarget(\n                    onDragEntered: (_) => setState(() => isActive = true),\n                    onDragExited: (_) => setState(() => isActive = false),\n                    onDragDone: (details) => loadImage(details.files),\n                    child: MouseRegion(\n                      cursor: SystemMouseCursors.click,\n                      onEnter: (_) => setState(() => isHovering = true),\n                      onExit: (_) => setState(() => isHovering = false),\n                      child: GestureDetector(\n                        behavior: HitTestBehavior.opaque,\n                        onTap: pickImage,\n                        child: DottedBorder(\n                          dashPattern: const [3, 3],\n                          radius: const Radius.circular(8),\n                          borderType: BorderType.RRect,\n                          color: isActive\n                              ? Theme.of(context).colorScheme.primary\n                              : Theme.of(context).hintColor,\n                          child: Container(\n                            alignment: Alignment.center,\n                            decoration: isHovering\n                                ? BoxDecoration(\n                                    color: Color(0x0F1F2329),\n                                    borderRadius: BorderRadius.circular(8),\n                                  )\n                                : null,\n                            child: pickedImages.isEmpty\n                                ? (isActive\n                                    ? hoveringWidget()\n                                    : dragHint(context))\n                                : previewImage(),\n                          ),\n                        ),\n                      ),\n                    ),\n                  ),\n                ),\n                Padding(\n                  padding: const EdgeInsets.only(top: 16),\n                  child: Row(\n                    children: [\n                      Spacer(),\n                      if (pickedImages.isNotEmpty)\n                        Padding(\n                          padding: EdgeInsets.only(right: 8),\n                          child: _ChangeIconButton(\n                            onTap: pickImage,\n                          ),\n                        ),\n                      _ConfirmButton(\n                        onTap: uploadImage,\n                        enable: pickedImages.isNotEmpty,\n                      ),\n                    ],\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget hoveringWidget() {\n    return Container(\n      color: Color(0xffE0F8FF),\n      child: Center(\n        child: FlowyText(\n          LocaleKeys.emojiIconPicker_iconUploader_dropToUpload.tr(),\n        ),\n      ),\n    );\n  }\n\n  Widget dragHint(BuildContext context) {\n    final style = TextStyle(\n      fontSize: 14,\n      color: Color(0xff666D76),\n      fontWeight: FontWeight.w500,\n    );\n    return Padding(\n      padding: EdgeInsets.symmetric(horizontal: 32),\n      child: RichText(\n        textAlign: TextAlign.center,\n        text: TextSpan(\n          children: [\n            TextSpan(\n              text:\n                  LocaleKeys.emojiIconPicker_iconUploader_placeholderLeft.tr(),\n            ),\n            TextSpan(\n              text: LocaleKeys.emojiIconPicker_iconUploader_placeholderUpload\n                  .tr(),\n              style: style.copyWith(color: Color(0xff00BCF0)),\n            ),\n            TextSpan(\n              text:\n                  LocaleKeys.emojiIconPicker_iconUploader_placeholderRight.tr(),\n              mouseCursor: SystemMouseCursors.click,\n            ),\n          ],\n          style: style,\n        ),\n      ),\n    );\n  }\n\n  Widget previewImage() {\n    final image = pickedImages.first;\n    final url = image.url;\n    if (image is _FileImage) {\n      if (url.endsWith(_svgSuffix)) {\n        return SvgPicture.file(\n          File(url),\n          width: 200,\n          height: 200,\n        );\n      }\n      return Image.file(\n        File(url),\n        width: 200,\n        height: 200,\n      );\n    } else if (image is _NetworkImage) {\n      if (url.endsWith(_svgSuffix)) {\n        return FlowyNetworkSvg(\n          url,\n          width: 200,\n          height: 200,\n        );\n      }\n      return FlowyNetworkImage(\n        width: 200,\n        height: 200,\n        url: url,\n      );\n    }\n    return const SizedBox.shrink();\n  }\n\n  void loadImage(List<XFile> files) {\n    final imageFiles = files\n        .where(\n          (file) =>\n              file.mimeType?.startsWith('image/') ??\n              false ||\n                  imgExtensionRegex.hasMatch(file.name) ||\n                  file.name.endsWith(_svgSuffix),\n        )\n        .toList();\n    if (imageFiles.isEmpty) return;\n    if (mounted) {\n      setState(() {\n        pickedImages.clear();\n        pickedImages.add(_FileImage(imageFiles.first.path));\n      });\n    }\n  }\n\n  Future<void> pickImage() async {\n    if (UniversalPlatform.isDesktopOrWeb) {\n      // on desktop, the users can pick a image file from folder\n      final result = await getIt<FilePickerService>().pickFiles(\n        dialogTitle: '',\n        type: FileType.custom,\n        allowedExtensions: List.of(defaultImageExtensions)..add('svg'),\n      );\n      loadImage(result?.files.map((f) => f.xFile).toList() ?? const []);\n    } else {\n      final photoPermission =\n          await PermissionChecker.checkPhotoPermission(context);\n      if (!photoPermission) {\n        Log.error('Has no permission to access the photo library');\n        return;\n      }\n      // on mobile, the users can pick a image file from camera or image library\n      final result = await ImagePicker().pickMultiImage();\n      loadImage(result);\n    }\n  }\n\n  Future<void> uploadImage() async {\n    if (pickedImages.isEmpty || isUploading) return;\n    isUploading = true;\n    String? result;\n    final userProfileResult = await UserBackendService.getCurrentUserProfile();\n    final userProfile = userProfileResult.fold(\n      (userProfile) => userProfile,\n      (l) => null,\n    );\n    final isLocalMode =\n        (userProfile?.workspaceType ?? WorkspaceTypePB.LocalW) ==\n            WorkspaceTypePB.LocalW;\n    if (isLocalMode) {\n      result = await pickedImages.first.saveToLocal();\n    } else {\n      result = await pickedImages.first.uploadToCloud(widget.documentId);\n    }\n    isUploading = false;\n    if (result?.isNotEmpty ?? false) {\n      widget.onUrl.call(result!);\n    }\n  }\n\n  Future<void> pasteAsAnImage() async {\n    final data = await getIt<ClipboardService>().getData();\n    final plainText = data.plainText;\n    Log.info('pasteAsAnImage plainText:$plainText');\n    if (plainText == null) return;\n    if (isURL(plainText) && (await validateImage(plainText))) {\n      setState(() {\n        pickedImages.clear();\n        pickedImages.add(_NetworkImage(plainText));\n      });\n    }\n  }\n\n  Future<bool> validateImage(String imageUrl) async {\n    Response res;\n    try {\n      res = await get(Uri.parse(imageUrl));\n    } catch (e) {\n      return false;\n    }\n    if (res.statusCode != 200) return false;\n    final Map<String, dynamic> data = res.headers;\n    return checkIfImage(data['content-type']);\n  }\n\n  bool checkIfImage(String? param) {\n    if (param == 'image/jpeg' ||\n        param == 'image/png' ||\n        param == 'image/gif' ||\n        param == 'image/tiff' ||\n        param == 'image/webp' ||\n        param == 'image/svg+xml' ||\n        param == 'image/svg') {\n      return true;\n    }\n    return false;\n  }\n}\n\nclass _ChangeIconButton extends StatelessWidget {\n  const _ChangeIconButton({required this.onTap});\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    final isDark = Theme.of(context).brightness == Brightness.dark;\n    return SizedBox(\n      height: 32,\n      width: 84,\n      child: FlowyButton(\n        text: FlowyText(\n          LocaleKeys.emojiIconPicker_iconUploader_change.tr(),\n          fontSize: 14.0,\n          fontWeight: FontWeight.w500,\n          figmaLineHeight: 20.0,\n          color: isDark ? Colors.white : Color(0xff1F2329),\n          textAlign: TextAlign.center,\n          overflow: TextOverflow.ellipsis,\n        ),\n        margin: const EdgeInsets.symmetric(horizontal: 14.0),\n        backgroundColor: Theme.of(context).colorScheme.surface,\n        hoverColor:\n            (isDark ? Colors.white : Color(0xffD1D8E0)).withValues(alpha: 0.9),\n        decoration: BoxDecoration(\n          border: Border.all(color: isDark ? Colors.white : Color(0xffD1D8E0)),\n          borderRadius: BorderRadius.circular(10),\n        ),\n        onTap: onTap,\n      ),\n    );\n  }\n}\n\nclass _ConfirmButton extends StatelessWidget {\n  const _ConfirmButton({required this.onTap, this.enable = true});\n\n  final VoidCallback onTap;\n  final bool enable;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 32,\n      child: Opacity(\n        opacity: enable ? 1.0 : 0.5,\n        child: PrimaryRoundedButton(\n          text: LocaleKeys.button_confirm.tr(),\n          figmaLineHeight: 20.0,\n          onTap: enable ? onTap : null,\n        ),\n      ),\n    );\n  }\n}\n\nconst _svgSuffix = '.svg';\n\nclass _PasteIntent extends Intent {}\n\nabstract class _Image {\n  String get url;\n\n  Future<String?> saveToLocal();\n\n  Future<String?> uploadToCloud(String documentId);\n\n  String get pureUrl => url.split('?').first;\n}\n\nclass _FileImage extends _Image {\n  _FileImage(this.url);\n\n  @override\n  final String url;\n\n  @override\n  Future<String?> saveToLocal() => saveImageToLocalStorage(url);\n\n  @override\n  Future<String?> uploadToCloud(String documentId) async {\n    final (url, errorMsg) = await saveImageToCloudStorage(\n      this.url,\n      documentId,\n    );\n    if (errorMsg?.isNotEmpty ?? false) {\n      Log.error('upload icon image :${this.url} error :$errorMsg');\n    }\n    return url;\n  }\n}\n\nclass _NetworkImage extends _Image {\n  _NetworkImage(this.url);\n\n  @override\n  final String url;\n\n  @override\n  Future<String?> saveToLocal() async {\n    final file = await CustomImageCacheManager().downloadFile(pureUrl);\n    return file.file.path;\n  }\n\n  @override\n  Future<String?> uploadToCloud(String documentId) async {\n    final file = await CustomImageCacheManager().downloadFile(pureUrl);\n    final (url, errorMsg) = await saveImageToCloudStorage(\n      file.file.path,\n      documentId,\n    );\n    if (errorMsg?.isNotEmpty ?? false) {\n      Log.error('upload icon image :${this.url} error :$errorMsg');\n    }\n    return url;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/icon_emoji_picker/recent_icons.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/shared/icon_emoji_picker/icon.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:flutter/foundation.dart';\n\nimport '../../core/config/kv.dart';\nimport '../../core/config/kv_keys.dart';\nimport '../../startup/startup.dart';\nimport 'flowy_icon_emoji_picker.dart';\n\nclass RecentIcons {\n  static final Map<String, List<String>> _dataMap = {};\n  static bool _loaded = false;\n  static const maxLength = 20;\n\n  /// To prevent the Recent Icon feature from affecting the unit tests of the Icon Selector.\n  @visibleForTesting\n  static bool enable = true;\n\n  static Future<void> putEmoji(String id) async {\n    await _put(FlowyIconType.emoji, id);\n  }\n\n  static Future<void> putIcon(RecentIcon icon) async {\n    await _put(\n      FlowyIconType.icon,\n      jsonEncode(icon.toJson()),\n    );\n  }\n\n  static Future<List<String>> getEmojiIds() async {\n    await _load();\n    return _dataMap[FlowyIconType.emoji.name] ?? [];\n  }\n\n  static Future<List<RecentIcon>> getIcons() async {\n    await _load();\n    return getIconsSync();\n  }\n\n  static List<RecentIcon> getIconsSync() {\n    final iconList = _dataMap[FlowyIconType.icon.name] ?? [];\n    try {\n      final List<RecentIcon> result = [];\n      for (final map in iconList) {\n        final recentIcon =\n            RecentIcon.fromJson(jsonDecode(map) as Map<String, dynamic>);\n        if (recentIcon.groupName.isEmpty) {\n          continue;\n        }\n        result.add(recentIcon);\n      }\n      return result;\n    } catch (e) {\n      Log.error('RecentIcons getIcons with :$iconList', e);\n    }\n    return [];\n  }\n\n  @visibleForTesting\n  static void clear() {\n    _dataMap.clear();\n    getIt<KeyValueStorage>().remove(KVKeys.recentIcons);\n  }\n\n  static Future<void> _save() async {\n    await getIt<KeyValueStorage>().set(\n      KVKeys.recentIcons,\n      jsonEncode(_dataMap),\n    );\n  }\n\n  static Future<void> _load() async {\n    if (_loaded || !enable) {\n      return;\n    }\n    final storage = getIt<KeyValueStorage>();\n    final value = await storage.get(KVKeys.recentIcons);\n    if (value == null || value.isEmpty) {\n      _loaded = true;\n      return;\n    }\n    try {\n      final data = jsonDecode(value) as Map;\n      _dataMap\n        ..clear()\n        ..addAll(\n          Map<String, List<String>>.from(\n            data.map((k, v) => MapEntry(k, List<String>.from(v))),\n          ),\n        );\n    } catch (e) {\n      Log.error('RecentIcons load failed with: $value', e);\n    }\n    _loaded = true;\n  }\n\n  static Future<void> _put(FlowyIconType key, String value) async {\n    await _load();\n    if (!enable) return;\n    final list = _dataMap[key.name] ?? [];\n    list.remove(value);\n    list.insert(0, value);\n    if (list.length > maxLength) list.removeLast();\n    _dataMap[key.name] = list;\n    await _save();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/icon_emoji_picker/tab.dart",
    "content": "import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart';\nimport 'package:flutter/material.dart';\n\nenum PickerTabType {\n  emoji,\n  icon,\n  custom;\n\n  String get tr {\n    switch (this) {\n      case PickerTabType.emoji:\n        return 'Emojis';\n      case PickerTabType.icon:\n        return 'Icons';\n      case PickerTabType.custom:\n        return 'Upload';\n    }\n  }\n}\n\nextension StringToPickerTabType on String {\n  PickerTabType? toPickerTabType() {\n    try {\n      return PickerTabType.values.byName(this);\n    } on ArgumentError {\n      return null;\n    }\n  }\n}\n\nclass PickerTab extends StatelessWidget {\n  const PickerTab({\n    super.key,\n    this.onTap,\n    required this.controller,\n    required this.tabs,\n  });\n\n  final List<PickerTabType> tabs;\n  final TabController controller;\n  final ValueChanged<int>? onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    final baseStyle = Theme.of(context).textTheme.bodyMedium;\n    final style = baseStyle?.copyWith(\n      fontWeight: FontWeight.w500,\n      fontSize: 14.0,\n      height: 16.0 / 14.0,\n    );\n    return TabBar(\n      controller: controller,\n      indicatorSize: TabBarIndicatorSize.label,\n      indicatorColor: Theme.of(context).colorScheme.primary,\n      isScrollable: true,\n      labelStyle: style,\n      labelColor: baseStyle?.color,\n      labelPadding: const EdgeInsets.symmetric(horizontal: 12.0),\n      unselectedLabelStyle: style?.copyWith(\n        color: Theme.of(context).hintColor,\n      ),\n      overlayColor: WidgetStateProperty.all(Colors.transparent),\n      indicator: RoundUnderlineTabIndicator(\n        width: 34.0,\n        borderSide: BorderSide(\n          color: Theme.of(context).colorScheme.primary,\n          width: 3,\n        ),\n      ),\n      onTap: onTap,\n      tabs: tabs\n          .map(\n            (tab) => Tab(\n              text: tab.tr,\n            ),\n          )\n          .toList(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/list_extension.dart",
    "content": "extension Unique<E, Id> on List<E> {\n  List<E> unique([Id Function(E element)? id]) {\n    final ids = <dynamic>{};\n    final list = [...this];\n    list.retainWhere((x) => ids.add(id != null ? id(x) : x as Id));\n    return list;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/loading.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/material.dart';\n\nclass Loading {\n  Loading(this.context);\n\n  BuildContext? loadingContext;\n  final BuildContext context;\n\n  bool hasStopped = false;\n\n  void start() => unawaited(\n        showDialog<void>(\n          context: context,\n          barrierDismissible: false,\n          builder: (BuildContext context) {\n            loadingContext = context;\n\n            if (hasStopped) {\n              WidgetsBinding.instance.addPostFrameCallback((_) {\n                Navigator.of(loadingContext!).maybePop();\n                loadingContext = null;\n              });\n            }\n\n            return const SimpleDialog(\n              elevation: 0.0,\n              backgroundColor:\n                  Colors.transparent, // can change this to your preferred color\n              children: [\n                Center(\n                  child: CircularProgressIndicator(),\n                ),\n              ],\n            );\n          },\n        ),\n      );\n\n  void stop() {\n    if (loadingContext != null) {\n      Navigator.of(loadingContext!).pop();\n      loadingContext = null;\n    }\n\n    hasStopped = true;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/markdown_to_document.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:archive/archive.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:share_plus/share_plus.dart';\n\nDocument customMarkdownToDocument(\n  String markdown, {\n  double? tableWidth,\n}) {\n  return markdownToDocument(\n    markdown,\n    markdownParsers: [\n      const MarkdownCodeBlockParser(),\n      MarkdownSimpleTableParser(tableWidth: tableWidth),\n    ],\n  );\n}\n\nFuture<String> customDocumentToMarkdown(\n  Document document, {\n  String path = '',\n  AsyncValueSetter<Archive>? onArchive,\n  String lineBreak = '',\n}) async {\n  final List<Future<ArchiveFile>> fileFutures = [];\n\n  /// create root Archive and directory\n  final id = document.root.id,\n      archive = Archive(),\n      resourceDir = ArchiveFile('$id/', 0, null)..isFile = false,\n      fileName = p.basenameWithoutExtension(path),\n      dirName = resourceDir.name;\n\n  String markdown = '';\n  try {\n    markdown = documentToMarkdown(\n      document,\n      lineBreak: lineBreak,\n      customParsers: [\n        const MathEquationNodeParser(),\n        const CalloutNodeParser(),\n        const ToggleListNodeParser(),\n        CustomImageNodeFileParser(fileFutures, dirName),\n        CustomMultiImageNodeFileParser(fileFutures, dirName),\n        GridNodeParser(fileFutures, dirName),\n        BoardNodeParser(fileFutures, dirName),\n        CalendarNodeParser(fileFutures, dirName),\n        const CustomParagraphNodeParser(),\n        const SubPageNodeParser(),\n        const SimpleTableNodeParser(),\n        const LinkPreviewNodeParser(),\n        const FileBlockNodeParser(),\n      ],\n    );\n  } catch (e) {\n    Log.error('documentToMarkdown error: $e');\n  }\n\n  /// create resource directory\n  if (fileFutures.isNotEmpty) archive.addFile(resourceDir);\n\n  for (final fileFuture in fileFutures) {\n    archive.addFile(await fileFuture);\n  }\n\n  /// add markdown file to Archive\n  final dataBytes = utf8.encode(markdown);\n  archive.addFile(ArchiveFile('$fileName-$id.md', dataBytes.length, dataBytes));\n\n  if (archive.isNotEmpty && path.isNotEmpty) {\n    if (onArchive == null) {\n      final zipEncoder = ZipEncoder();\n      final zip = zipEncoder.encode(archive);\n      if (zip != null) {\n        final zipFile = await File(path).writeAsBytes(zip);\n        if (Platform.isIOS) {\n          await Share.shareUri(zipFile.uri);\n          await zipFile.delete();\n        } else if (Platform.isAndroid) {\n          await Share.shareXFiles([XFile(zipFile.path)]);\n          await zipFile.delete();\n        }\n        Log.info('documentToMarkdownFiles to $path');\n      }\n    } else {\n      await onArchive.call(archive);\n    }\n  }\n  return markdown;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/patterns/common_patterns.dart",
    "content": "const _trailingZerosPattern = r'^(\\d+(?:\\.\\d*?[1-9](?=0|\\b))?)\\.?0*$';\nfinal trailingZerosRegex = RegExp(_trailingZerosPattern);\n\nconst _hrefPattern =\n    r'https?://(?:www\\.)?[a-zA-Z0-9\\-\\.]+\\.[a-zA-Z]{2,}(?:/[^\\s]*)?';\nfinal hrefRegex = RegExp(_hrefPattern);\n\n/// This pattern allows for both HTTP and HTTPS Scheme\n/// It allows for query parameters\n/// It only allows the following image extensions: .png, .jpg, .jpeg, .gif, .webm, .webp, .bmp\n///\nconst _imgUrlPattern =\n    r'(https?:\\/\\/)([^\\s([\"<,>/]*)(\\/)[^\\s[\",><]*(.png|.jpg|.jpeg|.gif|.webm|.webp|.bmp)(\\?[^\\s[\",><]*)?';\nfinal imgUrlRegex = RegExp(_imgUrlPattern);\n\nconst _singleLineMarkdownImagePattern = \"^!\\\\[.*\\\\]\\\\(($_hrefPattern)\\\\)\\$\";\nfinal singleLineMarkdownImageRegex = RegExp(_singleLineMarkdownImagePattern);\n\n/// This pattern allows for both HTTP and HTTPS Scheme\n/// It allows for query parameters\n/// It only allows the following video extensions:\n///  .mp4, .mov, .avi, .webm, .flv, .m4v (mpeg), .mpeg, .h264,\n///\nconst _videoUrlPattern =\n    r'(https?:\\/\\/)([^\\s([\"<,>/]*)(\\/)[^\\s[\",><]*(.mp4|.mov|.avi|.webm|.flv|.m4v|.mpeg|.h264)(\\?[^\\s[\",><]*)?';\nfinal videoUrlRegex = RegExp(_videoUrlPattern);\n\n/// This pattern matches both youtube.com and shortened youtu.be urls.\n///\nconst _youtubeUrlPattern = r'^(https?:\\/\\/)?(www\\.)?(youtube\\.com|youtu\\.be)\\/';\nfinal youtubeUrlRegex = RegExp(_youtubeUrlPattern);\n\nconst _appflowyCloudUrlPattern = r'^(https:\\/\\/)(.*)(\\.appflowy\\.cloud\\/)(.*)';\nfinal appflowyCloudUrlRegex = RegExp(_appflowyCloudUrlPattern);\n\nconst _camelCasePattern = '(?<=[a-z])[A-Z]';\nfinal camelCaseRegex = RegExp(_camelCasePattern);\n\nconst _macOSVolumesPattern = '^/Volumes/[^/]+';\nfinal macOSVolumesRegex = RegExp(_macOSVolumesPattern);\n\nconst appflowySharePageLinkPattern =\n    r'^https://appflowy\\.com/app/([^/]+)/([^?]+)(?:\\?blockId=(.+))?$';\nfinal appflowySharePageLinkRegex = RegExp(appflowySharePageLinkPattern);\n\nconst _numberedListPattern = r'^(\\d+)\\.';\nfinal numberedListRegex = RegExp(_numberedListPattern);\n\nconst _localPathPattern = r'^(file:\\/\\/|\\/|\\\\|[a-zA-Z]:[/\\\\]|\\.{1,2}[/\\\\])';\nfinal localPathRegex = RegExp(_localPathPattern, caseSensitive: false);\n\nconst _wordPattern = r\"\\S+\";\nfinal wordRegex = RegExp(_wordPattern);\n\nconst _appleNotesPattern =\n    r'<meta\\s+name=\"Generator\"\\s+content=\"Cocoa HTML Writer\"\\s*>\\s*<meta\\s+name=\"CocoaVersion\"\\s+content=\"\\d+\"\\s*>';\nfinal appleNotesRegex = RegExp(_appleNotesPattern);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/patterns/date_time_patterns.dart",
    "content": "/// RegExp to match Twelve Hour formats\n/// Source: https://stackoverflow.com/a/33906224\n///\n/// Matches eg: \"05:05 PM\", \"5:50 Pm\", \"10:59 am\", etc.\n///\nconst _twelveHourTimePattern =\n    r'\\b((1[0-2]|0?[1-9]):([0-5][0-9]) ([AaPp][Mm]))';\nfinal twelveHourTimeRegex = RegExp(_twelveHourTimePattern);\nbool isTwelveHourTime(String? time) => twelveHourTimeRegex.hasMatch(time ?? '');\n\n/// RegExp to match Twenty Four Hour formats\n/// Source: https://stackoverflow.com/a/7536768\n///\n/// Matches eg: \"0:01\", \"04:59\", \"16:30\", etc.\n///\nconst _twentyFourHourtimePattern = r'^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$';\nfinal tewentyFourHourTimeRegex = RegExp(_twentyFourHourtimePattern);\nbool isTwentyFourHourTime(String? time) =>\n    tewentyFourHourTimeRegex.hasMatch(time ?? '');\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/patterns/file_type_patterns.dart",
    "content": "/// This pattern matches a file extension that is an image.\n///\nconst _imgExtensionPattern = r'\\.(gif|jpe?g|tiff?|png|webp|bmp)$';\nfinal imgExtensionRegex = RegExp(_imgExtensionPattern);\n\n/// This pattern matches a file extension that is a video.\n///\nconst _videoExtensionPattern = r'\\.(mp4|mov|avi|webm|flv|m4v|mpeg|h264)$';\nfinal videoExtensionRegex = RegExp(_videoExtensionPattern);\n\n/// This pattern matches a file extension that is an audio.\n///\nconst _audioExtensionPattern = r'\\.(mp3|wav|ogg|flac|aac|wma|alac|aiff)$';\nfinal audioExtensionRegex = RegExp(_audioExtensionPattern);\n\n/// This pattern matches a file extension that is a document.\n///\nconst _documentExtensionPattern = r'\\.(pdf|doc|docx)$';\nfinal documentExtensionRegex = RegExp(_documentExtensionPattern);\n\n/// This pattern matches a file extension that is an archive.\n///\nconst _archiveExtensionPattern = r'\\.(zip|tar|gz|7z|rar)$';\nfinal archiveExtensionRegex = RegExp(_archiveExtensionPattern);\n\n/// This pattern matches a file extension that is a text.\n///\nconst _textExtensionPattern = r'\\.(txt|md|html|css|js|json|xml|csv)$';\nfinal textExtensionRegex = RegExp(_textExtensionPattern);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/permission/permission_checker.dart",
    "content": "// Check if the user has the required permission to access the device's\n//  - camera\n//  - storage\n//  - ...\nimport 'dart:async';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';\nimport 'package:appflowy/startup/tasks/device_info_task.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:permission_handler/permission_handler.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass PermissionChecker {\n  static Future<bool> checkPhotoPermission(BuildContext context) async {\n    // check the permission first\n    final status = await Permission.photos.status;\n    // if the permission is permanently denied, we should open the app settings\n    if (status.isPermanentlyDenied && context.mounted) {\n      unawaited(\n        showFlowyMobileConfirmDialog(\n          context,\n          title: FlowyText.semibold(\n            LocaleKeys.pageStyle_photoPermissionTitle.tr(),\n            maxLines: 3,\n            textAlign: TextAlign.center,\n          ),\n          content: FlowyText(\n            LocaleKeys.pageStyle_photoPermissionDescription.tr(),\n            maxLines: 5,\n            textAlign: TextAlign.center,\n            fontSize: 12.0,\n          ),\n          actionAlignment: ConfirmDialogActionAlignment.vertical,\n          actionButtonTitle: LocaleKeys.pageStyle_openSettings.tr(),\n          actionButtonColor: Colors.blue,\n          cancelButtonTitle: LocaleKeys.pageStyle_doNotAllow.tr(),\n          cancelButtonColor: Colors.blue,\n          onActionButtonPressed: () {\n            openAppSettings();\n          },\n        ),\n      );\n\n      return false;\n    } else if (status.isDenied) {\n      // https://github.com/Baseflow/flutter-permission-handler/issues/1262#issuecomment-2006340937\n      Permission permission = Permission.photos;\n      if (UniversalPlatform.isAndroid &&\n          ApplicationInfo.androidSDKVersion <= 32) {\n        permission = Permission.storage;\n      }\n      // if the permission is denied, we should request the permission\n      final newStatus = await permission.request();\n      if (newStatus.isDenied) {\n        return false;\n      }\n    }\n\n    return true;\n  }\n\n  static Future<bool> checkCameraPermission(BuildContext context) async {\n    // check the permission first\n    final status = await Permission.camera.status;\n    // if the permission is permanently denied, we should open the app settings\n    if (status.isPermanentlyDenied && context.mounted) {\n      unawaited(\n        showFlowyMobileConfirmDialog(\n          context,\n          title: FlowyText.semibold(\n            LocaleKeys.pageStyle_cameraPermissionTitle.tr(),\n            maxLines: 3,\n            textAlign: TextAlign.center,\n          ),\n          content: FlowyText(\n            LocaleKeys.pageStyle_cameraPermissionDescription.tr(),\n            maxLines: 5,\n            textAlign: TextAlign.center,\n            fontSize: 12.0,\n          ),\n          actionAlignment: ConfirmDialogActionAlignment.vertical,\n          actionButtonTitle: LocaleKeys.pageStyle_openSettings.tr(),\n          actionButtonColor: Colors.blue,\n          cancelButtonTitle: LocaleKeys.pageStyle_doNotAllow.tr(),\n          cancelButtonColor: Colors.blue,\n          onActionButtonPressed: openAppSettings,\n        ),\n      );\n\n      return false;\n    } else if (status.isDenied) {\n      final newStatus = await Permission.camera.request();\n      if (newStatus.isDenied) {\n        return false;\n      }\n    }\n\n    return true;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/popup_menu/appflowy_popup_menu.dart",
    "content": "// This file is copied from Flutter source code,\n// and modified to fit AppFlowy's needs.\n\n// changes:\n// 1. remove the default ink effect\n// 2. remove the tooltip\n// 3. support customize transition animation\n\n// Copyright 2014 The Flutter Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style license that can be\n// found in the LICENSE file.\n\nimport 'package:appflowy/mobile/presentation/base/animated_gesture.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:flutter/scheduler.dart';\n\n// Examples can assume:\n// enum Commands { heroAndScholar, hurricaneCame }\n// late bool _heroAndScholar;\n// late dynamic _selection;\n// late BuildContext context;\n// void setState(VoidCallback fn) { }\n// enum Menu { itemOne, itemTwo, itemThree, itemFour }\n\nconst Duration _kMenuDuration = Duration(milliseconds: 300);\nconst double _kMenuCloseIntervalEnd = 2.0 / 3.0;\nconst double _kMenuDividerHeight = 16.0;\nconst double _kMenuMaxWidth = 5.0 * _kMenuWidthStep;\nconst double _kMenuMinWidth = 2.0 * _kMenuWidthStep;\nconst double _kMenuVerticalPadding = 8.0;\nconst double _kMenuWidthStep = 56.0;\nconst double _kMenuScreenPadding = 8.0;\n\nGlobalKey<_PopupMenuState>? _kPopupMenuKey;\nvoid closePopupMenu() {\n  _kPopupMenuKey?.currentState?.dismiss();\n  _kPopupMenuKey = null;\n}\n\n/// A base class for entries in a Material Design popup menu.\n///\n/// The popup menu widget uses this interface to interact with the menu items.\n/// To show a popup menu, use the [showMenu] function. To create a button that\n/// shows a popup menu, consider using [PopupMenuButton].\n///\n/// The type `T` is the type of the value(s) the entry represents. All the\n/// entries in a given menu must represent values with consistent types.\n///\n/// A [PopupMenuEntry] may represent multiple values, for example a row with\n/// several icons, or a single entry, for example a menu item with an icon (see\n/// [PopupMenuItem]), or no value at all (for example, [PopupMenuDivider]).\n///\n/// See also:\n///\n///  * [PopupMenuItem], a popup menu entry for a single value.\n///  * [PopupMenuDivider], a popup menu entry that is just a horizontal line.\n///  * [CheckedPopupMenuItem], a popup menu item with a checkmark.\n///  * [showMenu], a method to dynamically show a popup menu at a given location.\n///  * [PopupMenuButton], an [IconButton] that automatically shows a menu when\n///    it is tapped.\nabstract class PopupMenuEntry<T> extends StatefulWidget {\n  /// Abstract const constructor. This constructor enables subclasses to provide\n  /// const constructors so that they can be used in const expressions.\n  const PopupMenuEntry({super.key});\n\n  /// The amount of vertical space occupied by this entry.\n  ///\n  /// This value is used at the time the [showMenu] method is called, if the\n  /// `initialValue` argument is provided, to determine the position of this\n  /// entry when aligning the selected entry over the given `position`. It is\n  /// otherwise ignored.\n  double get height;\n\n  /// Whether this entry represents a particular value.\n  ///\n  /// This method is used by [showMenu], when it is called, to align the entry\n  /// representing the `initialValue`, if any, to the given `position`, and then\n  /// later is called on each entry to determine if it should be highlighted (if\n  /// the method returns true, the entry will have its background color set to\n  /// the ambient [ThemeData.highlightColor]). If `initialValue` is null, then\n  /// this method is not called.\n  ///\n  /// If the [PopupMenuEntry] represents a single value, this should return true\n  /// if the argument matches that value. If it represents multiple values, it\n  /// should return true if the argument matches any of them.\n  bool represents(T? value);\n}\n\n/// A horizontal divider in a Material Design popup menu.\n///\n/// This widget adapts the [Divider] for use in popup menus.\n///\n/// See also:\n///\n///  * [PopupMenuItem], for the kinds of items that this widget divides.\n///  * [showMenu], a method to dynamically show a popup menu at a given location.\n///  * [PopupMenuButton], an [IconButton] that automatically shows a menu when\n///    it is tapped.\nclass PopupMenuDivider extends PopupMenuEntry<Never> {\n  /// Creates a horizontal divider for a popup menu.\n  ///\n  /// By default, the divider has a height of 16 logical pixels.\n  const PopupMenuDivider({super.key, this.height = _kMenuDividerHeight});\n\n  /// The height of the divider entry.\n  ///\n  /// Defaults to 16 pixels.\n  @override\n  final double height;\n\n  @override\n  bool represents(void value) => false;\n\n  @override\n  State<PopupMenuDivider> createState() => _PopupMenuDividerState();\n}\n\nclass _PopupMenuDividerState extends State<PopupMenuDivider> {\n  @override\n  Widget build(BuildContext context) => Divider(height: widget.height);\n}\n\n// This widget only exists to enable _PopupMenuRoute to save the sizes of\n// each menu item. The sizes are used by _PopupMenuRouteLayout to compute the\n// y coordinate of the menu's origin so that the center of selected menu\n// item lines up with the center of its PopupMenuButton.\nclass _MenuItem extends SingleChildRenderObjectWidget {\n  const _MenuItem({\n    required this.onLayout,\n    required super.child,\n  });\n\n  final ValueChanged<Size> onLayout;\n\n  @override\n  RenderObject createRenderObject(BuildContext context) {\n    return _RenderMenuItem(onLayout);\n  }\n\n  @override\n  void updateRenderObject(\n    BuildContext context,\n    covariant _RenderMenuItem renderObject,\n  ) {\n    renderObject.onLayout = onLayout;\n  }\n}\n\nclass _RenderMenuItem extends RenderShiftedBox {\n  _RenderMenuItem(this.onLayout, [RenderBox? child]) : super(child);\n\n  ValueChanged<Size> onLayout;\n\n  @override\n  Size computeDryLayout(BoxConstraints constraints) {\n    return child?.getDryLayout(constraints) ?? Size.zero;\n  }\n\n  @override\n  void performLayout() {\n    if (child == null) {\n      size = Size.zero;\n    } else {\n      child!.layout(constraints, parentUsesSize: true);\n      size = constraints.constrain(child!.size);\n      final BoxParentData childParentData = child!.parentData! as BoxParentData;\n      childParentData.offset = Offset.zero;\n    }\n    onLayout(size);\n  }\n}\n\n/// An item in a Material Design popup menu.\n///\n/// To show a popup menu, use the [showMenu] function. To create a button that\n/// shows a popup menu, consider using [PopupMenuButton].\n///\n/// To show a checkmark next to a popup menu item, consider using\n/// [CheckedPopupMenuItem].\n///\n/// Typically the [child] of a [PopupMenuItem] is a [Text] widget. More\n/// elaborate menus with icons can use a [ListTile]. By default, a\n/// [PopupMenuItem] is [kMinInteractiveDimension] pixels high. If you use a widget\n/// with a different height, it must be specified in the [height] property.\n///\n/// {@tool snippet}\n///\n/// Here, a [Text] widget is used with a popup menu item. The `Menu` type\n/// is an enum, not shown here.\n///\n/// ```dart\n/// const PopupMenuItem<Menu>(\n///   value: Menu.itemOne,\n///   child: Text('Item 1'),\n/// )\n/// ```\n/// {@end-tool}\n///\n/// See the example at [PopupMenuButton] for how this example could be used in a\n/// complete menu, and see the example at [CheckedPopupMenuItem] for one way to\n/// keep the text of [PopupMenuItem]s that use [Text] widgets in their [child]\n/// slot aligned with the text of [CheckedPopupMenuItem]s or of [PopupMenuItem]\n/// that use a [ListTile] in their [child] slot.\n///\n/// See also:\n///\n///  * [PopupMenuDivider], which can be used to divide items from each other.\n///  * [CheckedPopupMenuItem], a variant of [PopupMenuItem] with a checkmark.\n///  * [showMenu], a method to dynamically show a popup menu at a given location.\n///  * [PopupMenuButton], an [IconButton] that automatically shows a menu when\n///    it is tapped.\nclass PopupMenuItem<T> extends PopupMenuEntry<T> {\n  /// Creates an item for a popup menu.\n  ///\n  /// By default, the item is [enabled].\n  const PopupMenuItem({\n    super.key,\n    this.value,\n    this.onTap,\n    this.enabled = true,\n    this.height = kMinInteractiveDimension,\n    this.padding,\n    this.textStyle,\n    this.labelTextStyle,\n    this.mouseCursor,\n    required this.child,\n  });\n\n  /// The value that will be returned by [showMenu] if this entry is selected.\n  final T? value;\n\n  /// Called when the menu item is tapped.\n  final VoidCallback? onTap;\n\n  /// Whether the user is permitted to select this item.\n  ///\n  /// Defaults to true. If this is false, then the item will not react to\n  /// touches.\n  final bool enabled;\n\n  /// The minimum height of the menu item.\n  ///\n  /// Defaults to [kMinInteractiveDimension] pixels.\n  @override\n  final double height;\n\n  /// The padding of the menu item.\n  ///\n  /// The [height] property may interact with the applied padding. For example,\n  /// If a [height] greater than the height of the sum of the padding and [child]\n  /// is provided, then the padding's effect will not be visible.\n  ///\n  /// If this is null and [ThemeData.useMaterial3] is true, the horizontal padding\n  /// defaults to 12.0 on both sides.\n  ///\n  /// If this is null and [ThemeData.useMaterial3] is false, the horizontal padding\n  /// defaults to 16.0 on both sides.\n  final EdgeInsets? padding;\n\n  /// The text style of the popup menu item.\n  ///\n  /// If this property is null, then [PopupMenuThemeData.textStyle] is used.\n  /// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.titleMedium]\n  /// of [ThemeData.textTheme] is used.\n  final TextStyle? textStyle;\n\n  /// The label style of the popup menu item.\n  ///\n  /// When [ThemeData.useMaterial3] is true, this styles the text of the popup menu item.\n  ///\n  /// If this property is null, then [PopupMenuThemeData.labelTextStyle] is used.\n  /// If [PopupMenuThemeData.labelTextStyle] is also null, then [TextTheme.labelLarge]\n  /// is used with the [ColorScheme.onSurface] color when popup menu item is enabled and\n  /// the [ColorScheme.onSurface] color with 0.38 opacity when the popup menu item is disabled.\n  final WidgetStateProperty<TextStyle?>? labelTextStyle;\n\n  /// {@template flutter.material.popupmenu.mouseCursor}\n  /// The cursor for a mouse pointer when it enters or is hovering over the\n  /// widget.\n  ///\n  /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],\n  /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s:\n  ///\n  ///  * [WidgetState.hovered].\n  ///  * [WidgetState.focused].\n  ///  * [WidgetState.disabled].\n  /// {@endtemplate}\n  ///\n  /// If null, then the value of [PopupMenuThemeData.mouseCursor] is used. If\n  /// that is also null, then [WidgetStateMouseCursor.clickable] is used.\n  final MouseCursor? mouseCursor;\n\n  /// The widget below this widget in the tree.\n  ///\n  /// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An\n  /// appropriate [DefaultTextStyle] is put in scope for the child. In either\n  /// case, the text should be short enough that it won't wrap.\n  final Widget? child;\n\n  @override\n  bool represents(T? value) => value == this.value;\n\n  @override\n  PopupMenuItemState<T, PopupMenuItem<T>> createState() =>\n      PopupMenuItemState<T, PopupMenuItem<T>>();\n}\n\n/// The [State] for [PopupMenuItem] subclasses.\n///\n/// By default this implements the basic styling and layout of Material Design\n/// popup menu items.\n///\n/// The [buildChild] method can be overridden to adjust exactly what gets placed\n/// in the menu. By default it returns [PopupMenuItem.child].\n///\n/// The [handleTap] method can be overridden to adjust exactly what happens when\n/// the item is tapped. By default, it uses [Navigator.pop] to return the\n/// [PopupMenuItem.value] from the menu route.\n///\n/// This class takes two type arguments. The second, `W`, is the exact type of\n/// the [Widget] that is using this [State]. It must be a subclass of\n/// [PopupMenuItem]. The first, `T`, must match the type argument of that widget\n/// class, and is the type of values returned from this menu.\nclass PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {\n  /// The menu item contents.\n  ///\n  /// Used by the [build] method.\n  ///\n  /// By default, this returns [PopupMenuItem.child]. Override this to put\n  /// something else in the menu entry.\n  @protected\n  Widget? buildChild() => widget.child;\n\n  /// The handler for when the user selects the menu item.\n  ///\n  /// Used by the [InkWell] inserted by the [build] method.\n  ///\n  /// By default, uses [Navigator.pop] to return the [PopupMenuItem.value] from\n  /// the menu route.\n  @protected\n  void handleTap() {\n    // Need to pop the navigator first in case onTap may push new route onto navigator.\n    Navigator.pop<T>(context, widget.value);\n\n    widget.onTap?.call();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final ThemeData theme = Theme.of(context);\n    final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);\n    final PopupMenuThemeData defaults = theme.useMaterial3\n        ? _PopupMenuDefaultsM3(context)\n        : _PopupMenuDefaultsM2(context);\n    final Set<WidgetState> states = <WidgetState>{\n      if (!widget.enabled) WidgetState.disabled,\n    };\n\n    TextStyle style = theme.useMaterial3\n        ? (widget.labelTextStyle?.resolve(states) ??\n            popupMenuTheme.labelTextStyle?.resolve(states)! ??\n            defaults.labelTextStyle!.resolve(states)!)\n        : (widget.textStyle ?? popupMenuTheme.textStyle ?? defaults.textStyle!);\n\n    if (!widget.enabled && !theme.useMaterial3) {\n      style = style.copyWith(color: theme.disabledColor);\n    }\n\n    Widget item = AnimatedDefaultTextStyle(\n      style: style,\n      duration: kThemeChangeDuration,\n      child: Container(\n        alignment: AlignmentDirectional.centerStart,\n        constraints: BoxConstraints(minHeight: widget.height),\n        padding: widget.padding ??\n            (theme.useMaterial3\n                ? _PopupMenuDefaultsM3.menuHorizontalPadding\n                : _PopupMenuDefaultsM2.menuHorizontalPadding),\n        child: buildChild(),\n      ),\n    );\n\n    if (!widget.enabled) {\n      final bool isDark = theme.brightness == Brightness.dark;\n      item = IconTheme.merge(\n        data: IconThemeData(opacity: isDark ? 0.5 : 0.38),\n        child: item,\n      );\n    }\n\n    return MergeSemantics(\n      child: Semantics(\n        enabled: widget.enabled,\n        button: true,\n        child: GestureDetector(\n          onTap: widget.enabled ? handleTap : null,\n          behavior: HitTestBehavior.opaque,\n          child: ListTileTheme.merge(\n            contentPadding: EdgeInsets.zero,\n            titleTextStyle: style,\n            child: item,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\n/// An item with a checkmark in a Material Design popup menu.\n///\n/// To show a popup menu, use the [showMenu] function. To create a button that\n/// shows a popup menu, consider using [PopupMenuButton].\n///\n/// A [CheckedPopupMenuItem] is kMinInteractiveDimension pixels high, which\n/// matches the default minimum height of a [PopupMenuItem]. The horizontal\n/// layout uses [ListTile]; the checkmark is an [Icons.done] icon, shown in the\n/// [ListTile.leading] position.\n///\n/// {@tool snippet}\n///\n/// Suppose a `Commands` enum exists that lists the possible commands from a\n/// particular popup menu, including `Commands.heroAndScholar` and\n/// `Commands.hurricaneCame`, and further suppose that there is a\n/// `_heroAndScholar` member field which is a boolean. The example below shows a\n/// menu with one menu item with a checkmark that can toggle the boolean, and\n/// one menu item without a checkmark for selecting the second option. (It also\n/// shows a divider placed between the two menu items.)\n///\n/// ```dart\n/// PopupMenuButton<Commands>(\n///   onSelected: (Commands result) {\n///     switch (result) {\n///       case Commands.heroAndScholar:\n///         setState(() { _heroAndScholar = !_heroAndScholar; });\n///       case Commands.hurricaneCame:\n///         // ...handle hurricane option\n///         break;\n///       // ...other items handled here\n///     }\n///   },\n///   itemBuilder: (BuildContext context) => <PopupMenuEntry<Commands>>[\n///     CheckedPopupMenuItem<Commands>(\n///       checked: _heroAndScholar,\n///       value: Commands.heroAndScholar,\n///       child: const Text('Hero and scholar'),\n///     ),\n///     const PopupMenuDivider(),\n///     const PopupMenuItem<Commands>(\n///       value: Commands.hurricaneCame,\n///       child: ListTile(leading: Icon(null), title: Text('Bring hurricane')),\n///     ),\n///     // ...other items listed here\n///   ],\n/// )\n/// ```\n/// {@end-tool}\n///\n/// In particular, observe how the second menu item uses a [ListTile] with a\n/// blank [Icon] in the [ListTile.leading] position to get the same alignment as\n/// the item with the checkmark.\n///\n/// See also:\n///\n///  * [PopupMenuItem], a popup menu entry for picking a command (as opposed to\n///    toggling a value).\n///  * [PopupMenuDivider], a popup menu entry that is just a horizontal line.\n///  * [showMenu], a method to dynamically show a popup menu at a given location.\n///  * [PopupMenuButton], an [IconButton] that automatically shows a menu when\n///    it is tapped.\nclass CheckedPopupMenuItem<T> extends PopupMenuItem<T> {\n  /// Creates a popup menu item with a checkmark.\n  ///\n  /// By default, the menu item is [enabled] but unchecked. To mark the item as\n  /// checked, set [checked] to true.\n  const CheckedPopupMenuItem({\n    super.key,\n    super.value,\n    this.checked = false,\n    super.enabled,\n    super.padding,\n    super.height,\n    super.labelTextStyle,\n    super.mouseCursor,\n    super.child,\n    super.onTap,\n  });\n\n  /// Whether to display a checkmark next to the menu item.\n  ///\n  /// Defaults to false.\n  ///\n  /// When true, an [Icons.done] checkmark is displayed.\n  ///\n  /// When this popup menu item is selected, the checkmark will fade in or out\n  /// as appropriate to represent the implied new state.\n  final bool checked;\n\n  /// The widget below this widget in the tree.\n  ///\n  /// Typically a [Text]. An appropriate [DefaultTextStyle] is put in scope for\n  /// the child. The text should be short enough that it won't wrap.\n  ///\n  /// This widget is placed in the [ListTile.title] slot of a [ListTile] whose\n  /// [ListTile.leading] slot is an [Icons.done] icon.\n  @override\n  Widget? get child => super.child;\n\n  @override\n  PopupMenuItemState<T, CheckedPopupMenuItem<T>> createState() =>\n      _CheckedPopupMenuItemState<T>();\n}\n\nclass _CheckedPopupMenuItemState<T>\n    extends PopupMenuItemState<T, CheckedPopupMenuItem<T>>\n    with SingleTickerProviderStateMixin {\n  static const Duration _fadeDuration = Duration(milliseconds: 150);\n  late AnimationController _controller;\n  Animation<double> get _opacity => _controller.view;\n\n  @override\n  void initState() {\n    super.initState();\n    _controller = AnimationController(duration: _fadeDuration, vsync: this)\n      ..value = widget.checked ? 1.0 : 0.0\n      ..addListener(_updateState);\n  }\n\n  // Called when animation changed\n  void _updateState() => setState(() {});\n\n  @override\n  void dispose() {\n    _controller.removeListener(_updateState);\n    _controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  void handleTap() {\n    // This fades the checkmark in or out when tapped.\n    if (widget.checked) {\n      _controller.reverse();\n    } else {\n      _controller.forward();\n    }\n    super.handleTap();\n  }\n\n  @override\n  Widget buildChild() {\n    final ThemeData theme = Theme.of(context);\n    final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);\n    final PopupMenuThemeData defaults = theme.useMaterial3\n        ? _PopupMenuDefaultsM3(context)\n        : _PopupMenuDefaultsM2(context);\n    final Set<WidgetState> states = <WidgetState>{\n      if (widget.checked) WidgetState.selected,\n    };\n    final WidgetStateProperty<TextStyle?>? effectiveLabelTextStyle =\n        widget.labelTextStyle ??\n            popupMenuTheme.labelTextStyle ??\n            defaults.labelTextStyle;\n    return IgnorePointer(\n      child: ListTileTheme.merge(\n        contentPadding: EdgeInsets.zero,\n        child: ListTile(\n          enabled: widget.enabled,\n          titleTextStyle: effectiveLabelTextStyle?.resolve(states),\n          leading: FadeTransition(\n            opacity: _opacity,\n            child: Icon(_controller.isDismissed ? null : Icons.done),\n          ),\n          title: widget.child,\n        ),\n      ),\n    );\n  }\n}\n\nclass _PopupMenu<T> extends StatefulWidget {\n  const _PopupMenu({\n    super.key,\n    required this.itemKeys,\n    required this.route,\n    required this.semanticLabel,\n    this.constraints,\n    required this.clipBehavior,\n  });\n\n  final List<GlobalKey> itemKeys;\n  final _PopupMenuRoute<T> route;\n  final String? semanticLabel;\n  final BoxConstraints? constraints;\n  final Clip clipBehavior;\n\n  @override\n  State<_PopupMenu<T>> createState() => _PopupMenuState<T>();\n}\n\nclass _PopupMenuState<T> extends State<_PopupMenu<T>> {\n  @override\n  Widget build(BuildContext context) {\n    final double unit = 1.0 /\n        (widget.route.items.length +\n            1.5); // 1.0 for the width and 0.5 for the last item's fade.\n    final List<Widget> children = <Widget>[];\n    final ThemeData theme = Theme.of(context);\n    final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);\n    final PopupMenuThemeData defaults = theme.useMaterial3\n        ? _PopupMenuDefaultsM3(context)\n        : _PopupMenuDefaultsM2(context);\n\n    for (int i = 0; i < widget.route.items.length; i += 1) {\n      final double start = (i + 1) * unit;\n      final double end = clampDouble(start + 1.5 * unit, 0.0, 1.0);\n      final CurvedAnimation opacity = CurvedAnimation(\n        parent: widget.route.animation!,\n        curve: Interval(start, end),\n      );\n      Widget item = widget.route.items[i];\n      if (widget.route.initialValue != null &&\n          widget.route.items[i].represents(widget.route.initialValue)) {\n        item = ColoredBox(\n          color: Theme.of(context).highlightColor,\n          child: item,\n        );\n      }\n      children.add(\n        _MenuItem(\n          onLayout: (Size size) {\n            widget.route.itemSizes[i] = size;\n          },\n          child: FadeTransition(\n            key: widget.itemKeys[i],\n            opacity: opacity,\n            child: item,\n          ),\n        ),\n      );\n    }\n\n    final _CurveTween opacity =\n        _CurveTween(curve: const Interval(0.0, 1.0 / 3.0));\n    final _CurveTween width = _CurveTween(curve: Interval(0.0, unit));\n    final _CurveTween height =\n        _CurveTween(curve: Interval(0.0, unit * widget.route.items.length));\n\n    final Widget child = ConstrainedBox(\n      constraints: widget.constraints ??\n          const BoxConstraints(\n            minWidth: _kMenuMinWidth,\n            maxWidth: _kMenuMaxWidth,\n          ),\n      child: IntrinsicWidth(\n        stepWidth: _kMenuWidthStep,\n        child: Semantics(\n          scopesRoute: true,\n          namesRoute: true,\n          explicitChildNodes: true,\n          label: widget.semanticLabel,\n          child: SingleChildScrollView(\n            padding: const EdgeInsets.symmetric(\n              vertical: _kMenuVerticalPadding,\n            ),\n            child: ListBody(children: children),\n          ),\n        ),\n      ),\n    );\n\n    return AnimatedBuilder(\n      animation: widget.route.animation!,\n      builder: (BuildContext context, Widget? child) {\n        return FadeTransition(\n          opacity: opacity.animate(widget.route.animation!),\n          child: Material(\n            shape: widget.route.shape ?? popupMenuTheme.shape ?? defaults.shape,\n            color: widget.route.color ?? popupMenuTheme.color ?? defaults.color,\n            clipBehavior: widget.clipBehavior,\n            type: MaterialType.card,\n            elevation: widget.route.elevation ??\n                popupMenuTheme.elevation ??\n                defaults.elevation!,\n            shadowColor: widget.route.shadowColor ??\n                popupMenuTheme.shadowColor ??\n                defaults.shadowColor,\n            surfaceTintColor: widget.route.surfaceTintColor ??\n                popupMenuTheme.surfaceTintColor ??\n                defaults.surfaceTintColor,\n            child: Align(\n              alignment: AlignmentDirectional.topEnd,\n              widthFactor: width.evaluate(widget.route.animation!),\n              heightFactor: height.evaluate(widget.route.animation!),\n              child: child,\n            ),\n          ),\n        );\n      },\n      child: child,\n    );\n  }\n\n  @override\n  void dispose() {\n    _kPopupMenuKey = null;\n    super.dispose();\n  }\n\n  void dismiss() {\n    if (_kPopupMenuKey == null) {\n      return;\n    }\n\n    Navigator.of(context).pop();\n    _kPopupMenuKey = null;\n  }\n}\n\n// Positioning of the menu on the screen.\nclass _PopupMenuRouteLayout extends SingleChildLayoutDelegate {\n  _PopupMenuRouteLayout(\n    this.position,\n    this.itemSizes,\n    this.selectedItemIndex,\n    this.textDirection,\n    this.padding,\n    this.avoidBounds,\n  );\n\n  // Rectangle of underlying button, relative to the overlay's dimensions.\n  final RelativeRect position;\n\n  // The sizes of each item are computed when the menu is laid out, and before\n  // the route is laid out.\n  List<Size?> itemSizes;\n\n  // The index of the selected item, or null if PopupMenuButton.initialValue\n  // was not specified.\n  final int? selectedItemIndex;\n\n  // Whether to prefer going to the left or to the right.\n  final TextDirection textDirection;\n\n  // The padding of unsafe area.\n  EdgeInsets padding;\n\n  // List of rectangles that we should avoid overlapping. Unusable screen area.\n  final Set<Rect> avoidBounds;\n\n  // We put the child wherever position specifies, so long as it will fit within\n  // the specified parent size padded (inset) by 8. If necessary, we adjust the\n  // child's position so that it fits.\n\n  @override\n  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {\n    // The menu can be at most the size of the overlay minus 8.0 pixels in each\n    // direction.\n    return BoxConstraints.loose(constraints.biggest).deflate(\n      const EdgeInsets.all(_kMenuScreenPadding) + padding,\n    );\n  }\n\n  @override\n  Offset getPositionForChild(Size size, Size childSize) {\n    final double y = position.top;\n\n    // Find the ideal horizontal position.\n    // size: The size of the overlay.\n    // childSize: The size of the menu, when fully open, as determined by\n    // getConstraintsForChild.\n    double x;\n    if (position.left > position.right) {\n      // Menu button is closer to the right edge, so grow to the left, aligned to the right edge.\n      x = size.width - position.right - childSize.width;\n    } else if (position.left < position.right) {\n      // Menu button is closer to the left edge, so grow to the right, aligned to the left edge.\n      x = position.left;\n    } else {\n      // Menu button is equidistant from both edges, so grow in reading direction.\n      x = switch (textDirection) {\n        TextDirection.rtl => size.width - position.right - childSize.width,\n        TextDirection.ltr => position.left,\n      };\n    }\n    final Offset wantedPosition = Offset(x, y);\n    final Offset originCenter = position.toRect(Offset.zero & size).center;\n    final Iterable<Rect> subScreens =\n        DisplayFeatureSubScreen.subScreensInBounds(\n      Offset.zero & size,\n      avoidBounds,\n    );\n    final Rect subScreen = _closestScreen(subScreens, originCenter);\n    return _fitInsideScreen(subScreen, childSize, wantedPosition);\n  }\n\n  Rect _closestScreen(Iterable<Rect> screens, Offset point) {\n    Rect closest = screens.first;\n    for (final Rect screen in screens) {\n      if ((screen.center - point).distance <\n          (closest.center - point).distance) {\n        closest = screen;\n      }\n    }\n    return closest;\n  }\n\n  Offset _fitInsideScreen(Rect screen, Size childSize, Offset wantedPosition) {\n    double x = wantedPosition.dx;\n    double y = wantedPosition.dy;\n    // Avoid going outside an area defined as the rectangle 8.0 pixels from the\n    // edge of the screen in every direction.\n    if (x < screen.left + _kMenuScreenPadding + padding.left) {\n      x = screen.left + _kMenuScreenPadding + padding.left;\n    } else if (x + childSize.width >\n        screen.right - _kMenuScreenPadding - padding.right) {\n      x = screen.right - childSize.width - _kMenuScreenPadding - padding.right;\n    }\n    if (y < screen.top + _kMenuScreenPadding + padding.top) {\n      y = _kMenuScreenPadding + padding.top;\n    } else if (y + childSize.height >\n        screen.bottom - _kMenuScreenPadding - padding.bottom) {\n      y = screen.bottom -\n          childSize.height -\n          _kMenuScreenPadding -\n          padding.bottom;\n    }\n\n    return Offset(x, y);\n  }\n\n  @override\n  bool shouldRelayout(_PopupMenuRouteLayout oldDelegate) {\n    // If called when the old and new itemSizes have been initialized then\n    // we expect them to have the same length because there's no practical\n    // way to change length of the items list once the menu has been shown.\n    assert(itemSizes.length == oldDelegate.itemSizes.length);\n\n    return position != oldDelegate.position ||\n        selectedItemIndex != oldDelegate.selectedItemIndex ||\n        textDirection != oldDelegate.textDirection ||\n        !listEquals(itemSizes, oldDelegate.itemSizes) ||\n        padding != oldDelegate.padding ||\n        !setEquals(avoidBounds, oldDelegate.avoidBounds);\n  }\n}\n\nclass _PopupMenuRoute<T> extends PopupRoute<T> {\n  _PopupMenuRoute({\n    required this.position,\n    required this.items,\n    required this.itemKeys,\n    this.initialValue,\n    this.elevation,\n    this.surfaceTintColor,\n    this.shadowColor,\n    required this.barrierLabel,\n    this.semanticLabel,\n    this.shape,\n    this.color,\n    required this.capturedThemes,\n    this.constraints,\n    required this.clipBehavior,\n    super.settings,\n    this.popUpAnimationStyle,\n  })  : itemSizes = List<Size?>.filled(items.length, null),\n        // Menus always cycle focus through their items irrespective of the\n        // focus traversal edge behavior set in the Navigator.\n        super(traversalEdgeBehavior: TraversalEdgeBehavior.closedLoop);\n\n  final RelativeRect position;\n  final List<PopupMenuEntry<T>> items;\n  final List<GlobalKey> itemKeys;\n  final List<Size?> itemSizes;\n  final T? initialValue;\n  final double? elevation;\n  final Color? surfaceTintColor;\n  final Color? shadowColor;\n  final String? semanticLabel;\n  final ShapeBorder? shape;\n  final Color? color;\n  final CapturedThemes capturedThemes;\n  final BoxConstraints? constraints;\n  final Clip clipBehavior;\n  final AnimationStyle? popUpAnimationStyle;\n\n  @override\n  Animation<double> createAnimation() {\n    if (popUpAnimationStyle != AnimationStyle.noAnimation) {\n      return CurvedAnimation(\n        parent: super.createAnimation(),\n        curve: popUpAnimationStyle?.curve ?? Curves.easeInBack,\n        reverseCurve: popUpAnimationStyle?.reverseCurve ??\n            const Interval(0.0, _kMenuCloseIntervalEnd),\n      );\n    }\n    return super.createAnimation();\n  }\n\n  void scrollTo(int selectedItemIndex) {\n    SchedulerBinding.instance.addPostFrameCallback((_) {\n      if (itemKeys[selectedItemIndex].currentContext != null) {\n        Scrollable.ensureVisible(itemKeys[selectedItemIndex].currentContext!);\n      }\n    });\n  }\n\n  @override\n  Duration get transitionDuration =>\n      popUpAnimationStyle?.duration ?? _kMenuDuration;\n\n  @override\n  bool get barrierDismissible => true;\n\n  @override\n  Color? get barrierColor => null;\n\n  @override\n  final String barrierLabel;\n\n  @override\n  Widget buildTransitions(\n    BuildContext context,\n    Animation<double> animation,\n    Animation<double> secondaryAnimation,\n    Widget child,\n  ) {\n    if (!animation.isCompleted) {\n      final screenWidth = MediaQuery.of(context).size.width;\n      final screenHeight = MediaQuery.of(context).size.height;\n      final size = position.toSize(Size(screenWidth, screenHeight));\n      final center = size.width / 2.0;\n      final alignment = FractionalOffset(\n        (screenWidth - position.right - center) / screenWidth,\n        (screenHeight - position.bottom - center) / screenHeight,\n      );\n      child = FadeTransition(\n        opacity: animation,\n        child: ScaleTransition(\n          alignment: alignment,\n          scale: animation,\n          child: child,\n        ),\n      );\n    }\n    return child;\n  }\n\n  @override\n  Widget buildPage(\n    BuildContext context,\n    Animation<double> animation,\n    Animation<double> secondaryAnimation,\n  ) {\n    int? selectedItemIndex;\n    if (initialValue != null) {\n      for (int index = 0;\n          selectedItemIndex == null && index < items.length;\n          index += 1) {\n        if (items[index].represents(initialValue)) {\n          selectedItemIndex = index;\n        }\n      }\n    }\n    if (selectedItemIndex != null) {\n      scrollTo(selectedItemIndex);\n    }\n\n    _kPopupMenuKey ??= GlobalKey<_PopupMenuState>();\n    final Widget menu = _PopupMenu<T>(\n      key: _kPopupMenuKey,\n      route: this,\n      itemKeys: itemKeys,\n      semanticLabel: semanticLabel,\n      constraints: constraints,\n      clipBehavior: clipBehavior,\n    );\n    final MediaQueryData mediaQuery = MediaQuery.of(context);\n    return MediaQuery.removePadding(\n      context: context,\n      removeTop: true,\n      removeBottom: true,\n      removeLeft: true,\n      removeRight: true,\n      child: Builder(\n        builder: (BuildContext context) {\n          return CustomSingleChildLayout(\n            delegate: _PopupMenuRouteLayout(\n              position,\n              itemSizes,\n              selectedItemIndex,\n              Directionality.of(context),\n              mediaQuery.padding,\n              _avoidBounds(mediaQuery),\n            ),\n            child: capturedThemes.wrap(menu),\n          );\n        },\n      ),\n    );\n  }\n\n  Set<Rect> _avoidBounds(MediaQueryData mediaQuery) {\n    return DisplayFeatureSubScreen.avoidBounds(mediaQuery).toSet();\n  }\n}\n\n/// Show a popup menu that contains the `items` at `position`.\n///\n/// The `items` parameter must not be empty.\n///\n/// If `initialValue` is specified then the first item with a matching value\n/// will be highlighted and the value of `position` gives the rectangle whose\n/// vertical center will be aligned with the vertical center of the highlighted\n/// item (when possible).\n///\n/// If `initialValue` is not specified then the top of the menu will be aligned\n/// with the top of the `position` rectangle.\n///\n/// In both cases, the menu position will be adjusted if necessary to fit on the\n/// screen.\n///\n/// Horizontally, the menu is positioned so that it grows in the direction that\n/// has the most room. For example, if the `position` describes a rectangle on\n/// the left edge of the screen, then the left edge of the menu is aligned with\n/// the left edge of the `position`, and the menu grows to the right. If both\n/// edges of the `position` are equidistant from the opposite edge of the\n/// screen, then the ambient [Directionality] is used as a tie-breaker,\n/// preferring to grow in the reading direction.\n///\n/// The positioning of the `initialValue` at the `position` is implemented by\n/// iterating over the `items` to find the first whose\n/// [PopupMenuEntry.represents] method returns true for `initialValue`, and then\n/// summing the values of [PopupMenuEntry.height] for all the preceding widgets\n/// in the list.\n///\n/// The `elevation` argument specifies the z-coordinate at which to place the\n/// menu. The elevation defaults to 8, the appropriate elevation for popup\n/// menus.\n///\n/// The `context` argument is used to look up the [Navigator] and [Theme] for\n/// the menu. It is only used when the method is called. Its corresponding\n/// widget can be safely removed from the tree before the popup menu is closed.\n///\n/// The `useRootNavigator` argument is used to determine whether to push the\n/// menu to the [Navigator] furthest from or nearest to the given `context`. It\n/// is `false` by default.\n///\n/// The `semanticLabel` argument is used by accessibility frameworks to\n/// announce screen transitions when the menu is opened and closed. If this\n/// label is not provided, it will default to\n/// [MaterialLocalizations.popupMenuLabel].\n///\n/// The `clipBehavior` argument is used to clip the shape of the menu. Defaults to\n/// [Clip.none].\n///\n/// See also:\n///\n///  * [PopupMenuItem], a popup menu entry for a single value.\n///  * [PopupMenuDivider], a popup menu entry that is just a horizontal line.\n///  * [CheckedPopupMenuItem], a popup menu item with a checkmark.\n///  * [PopupMenuButton], which provides an [IconButton] that shows a menu by\n///    calling this method automatically.\n///  * [SemanticsConfiguration.namesRoute], for a description of edge triggered\n///    semantics.\nFuture<T?> showMenu<T>({\n  required BuildContext context,\n  required RelativeRect position,\n  required List<PopupMenuEntry<T>> items,\n  T? initialValue,\n  double? elevation,\n  Color? shadowColor,\n  Color? surfaceTintColor,\n  String? semanticLabel,\n  ShapeBorder? shape,\n  Color? color,\n  bool useRootNavigator = false,\n  BoxConstraints? constraints,\n  Clip clipBehavior = Clip.none,\n  RouteSettings? routeSettings,\n  AnimationStyle? popUpAnimationStyle,\n}) {\n  assert(items.isNotEmpty);\n  assert(debugCheckHasMaterialLocalizations(context));\n\n  switch (Theme.of(context).platform) {\n    case TargetPlatform.iOS:\n    case TargetPlatform.macOS:\n      break;\n    case TargetPlatform.android:\n    case TargetPlatform.fuchsia:\n    case TargetPlatform.linux:\n    case TargetPlatform.windows:\n      semanticLabel ??= MaterialLocalizations.of(context).popupMenuLabel;\n  }\n\n  final List<GlobalKey> menuItemKeys =\n      List<GlobalKey>.generate(items.length, (int index) => GlobalKey());\n  final NavigatorState navigator =\n      Navigator.of(context, rootNavigator: useRootNavigator);\n  return navigator.push(\n    _PopupMenuRoute<T>(\n      position: position,\n      items: items,\n      itemKeys: menuItemKeys,\n      initialValue: initialValue,\n      elevation: elevation,\n      shadowColor: shadowColor,\n      surfaceTintColor: surfaceTintColor,\n      semanticLabel: semanticLabel,\n      barrierLabel: MaterialLocalizations.of(context).menuDismissLabel,\n      shape: shape,\n      color: color,\n      capturedThemes:\n          InheritedTheme.capture(from: context, to: navigator.context),\n      constraints: constraints,\n      clipBehavior: clipBehavior,\n      settings: routeSettings,\n      popUpAnimationStyle: popUpAnimationStyle,\n    ),\n  );\n}\n\n/// Signature for the callback invoked when a menu item is selected. The\n/// argument is the value of the [PopupMenuItem] that caused its menu to be\n/// dismissed.\n///\n/// Used by [PopupMenuButton.onSelected].\ntypedef PopupMenuItemSelected<T> = void Function(T value);\n\n/// Signature for the callback invoked when a [PopupMenuButton] is dismissed\n/// without selecting an item.\n///\n/// Used by [PopupMenuButton.onCanceled].\ntypedef PopupMenuCanceled = void Function();\n\n/// Signature used by [PopupMenuButton] to lazily construct the items shown when\n/// the button is pressed.\n///\n/// Used by [PopupMenuButton.itemBuilder].\ntypedef PopupMenuItemBuilder<T> = List<PopupMenuEntry<T>> Function(\n  BuildContext context,\n);\n\n/// Displays a menu when pressed and calls [onSelected] when the menu is dismissed\n/// because an item was selected. The value passed to [onSelected] is the value of\n/// the selected menu item.\n///\n/// One of [child] or [icon] may be provided, but not both. If [icon] is provided,\n/// then [PopupMenuButton] behaves like an [IconButton].\n///\n/// If both are null, then a standard overflow icon is created (depending on the\n/// platform).\n///\n/// ## Updating to [MenuAnchor]\n///\n/// There is a Material 3 component,\n/// [MenuAnchor] that is preferred for applications that are configured\n/// for Material 3 (see [ThemeData.useMaterial3]).\n/// The [MenuAnchor] widget's visuals\n/// are a little bit different, see the Material 3 spec at\n/// <https://m3.material.io/components/menus/guidelines> for\n/// more details.\n///\n/// The [MenuAnchor] widget's API is also slightly different.\n/// [MenuAnchor]'s were built to be lower level interface for\n/// creating menus that are displayed from an anchor.\n///\n/// There are a few steps you would take to migrate from\n/// [PopupMenuButton] to [MenuAnchor]:\n///\n/// 1. Instead of using the [PopupMenuButton.itemBuilder] to build\n/// a list of [PopupMenuEntry]s, you would use the [MenuAnchor.menuChildren]\n/// which takes a list of [Widget]s. Usually, you would use a list of\n/// [MenuItemButton]s as shown in the example below.\n///\n/// 2. Instead of using the [PopupMenuButton.onSelected] callback, you would\n/// set individual callbacks for each of the [MenuItemButton]s using the\n/// [MenuItemButton.onPressed] property.\n///\n/// 3. To anchor the [MenuAnchor] to a widget, you would use the [MenuAnchor.builder]\n/// to return the widget of choice - usually a [TextButton] or an [IconButton].\n///\n/// 4. You may want to style the [MenuItemButton]s, see the [MenuItemButton]\n/// documentation for details.\n///\n/// Use the sample below for an example of migrating from [PopupMenuButton] to\n/// [MenuAnchor].\n///\n/// {@tool dartpad}\n/// This example shows a menu with three items, selecting between an enum's\n/// values and setting a `selectedMenu` field based on the selection.\n///\n/// ** See code in examples/api/lib/material/popup_menu/popup_menu.0.dart **\n/// {@end-tool}\n///\n/// {@tool dartpad}\n/// This example shows how to migrate the above to a [MenuAnchor].\n///\n/// ** See code in examples/api/lib/material/menu_anchor/menu_anchor.2.dart **\n/// {@end-tool}\n///\n/// {@tool dartpad}\n/// This sample shows the creation of a popup menu, as described in:\n/// https://m3.material.io/components/menus/overview\n///\n/// ** See code in examples/api/lib/material/popup_menu/popup_menu.1.dart **\n/// {@end-tool}\n///\n/// {@tool dartpad}\n/// This sample showcases how to override the [PopupMenuButton] animation\n/// curves and duration using [AnimationStyle].\n///\n/// ** See code in examples/api/lib/material/popup_menu/popup_menu.2.dart **\n/// {@end-tool}\n///\n/// See also:\n///\n///  * [PopupMenuItem], a popup menu entry for a single value.\n///  * [PopupMenuDivider], a popup menu entry that is just a horizontal line.\n///  * [CheckedPopupMenuItem], a popup menu item with a checkmark.\n///  * [showMenu], a method to dynamically show a popup menu at a given location.\nclass PopupMenuButton<T> extends StatefulWidget {\n  /// Creates a button that shows a popup menu.\n  const PopupMenuButton({\n    super.key,\n    required this.itemBuilder,\n    this.initialValue,\n    this.onOpened,\n    this.onSelected,\n    this.onCanceled,\n    this.tooltip,\n    this.elevation,\n    this.shadowColor,\n    this.surfaceTintColor,\n    this.padding = const EdgeInsets.all(8.0),\n    this.child,\n    this.splashRadius,\n    this.icon,\n    this.iconSize,\n    this.offset = Offset.zero,\n    this.enabled = true,\n    this.shape,\n    this.color,\n    this.iconColor,\n    this.enableFeedback,\n    this.constraints,\n    this.position,\n    this.clipBehavior = Clip.none,\n    this.useRootNavigator = false,\n    this.popUpAnimationStyle,\n    this.routeSettings,\n    this.style,\n  }) : assert(\n          !(child != null && icon != null),\n          'You can only pass [child] or [icon], not both.',\n        );\n\n  /// Called when the button is pressed to create the items to show in the menu.\n  final PopupMenuItemBuilder<T> itemBuilder;\n\n  /// The value of the menu item, if any, that should be highlighted when the menu opens.\n  final T? initialValue;\n\n  /// Called when the popup menu is shown.\n  final VoidCallback? onOpened;\n\n  /// Called when the user selects a value from the popup menu created by this button.\n  ///\n  /// If the popup menu is dismissed without selecting a value, [onCanceled] is\n  /// called instead.\n  final PopupMenuItemSelected<T>? onSelected;\n\n  /// Called when the user dismisses the popup menu without selecting an item.\n  ///\n  /// If the user selects a value, [onSelected] is called instead.\n  final PopupMenuCanceled? onCanceled;\n\n  /// Text that describes the action that will occur when the button is pressed.\n  ///\n  /// This text is displayed when the user long-presses on the button and is\n  /// used for accessibility.\n  final String? tooltip;\n\n  /// The z-coordinate at which to place the menu when open. This controls the\n  /// size of the shadow below the menu.\n  ///\n  /// Defaults to 8, the appropriate elevation for popup menus.\n  final double? elevation;\n\n  /// The color used to paint the shadow below the menu.\n  ///\n  /// If null then the ambient [PopupMenuThemeData.shadowColor] is used.\n  /// If that is null too, then the overall theme's [ThemeData.shadowColor]\n  /// (default black) is used.\n  final Color? shadowColor;\n\n  /// The color used as an overlay on [color] to indicate elevation.\n  ///\n  /// This is not recommended for use. [Material 3 spec](https://m3.material.io/styles/color/the-color-system/color-roles)\n  /// introduced a set of tone-based surfaces and surface containers in its [ColorScheme],\n  /// which provide more flexibility. The intention is to eventually remove surface tint color from\n  /// the framework.\n  ///\n  /// If null, [PopupMenuThemeData.surfaceTintColor] is used. If that\n  /// is also null, the default value is [Colors.transparent].\n  ///\n  /// See [Material.surfaceTintColor] for more details on how this\n  /// overlay is applied.\n  final Color? surfaceTintColor;\n\n  /// Matches IconButton's 8 dps padding by default. In some cases, notably where\n  /// this button appears as the trailing element of a list item, it's useful to be able\n  /// to set the padding to zero.\n  final EdgeInsetsGeometry padding;\n\n  /// The splash radius.\n  ///\n  /// If null, default splash radius of [InkWell] or [IconButton] is used.\n  final double? splashRadius;\n\n  /// If provided, [child] is the widget used for this button\n  /// and the button will utilize an [InkWell] for taps.\n  final Widget? child;\n\n  /// If provided, the [icon] is used for this button\n  /// and the button will behave like an [IconButton].\n  final Widget? icon;\n\n  /// The offset is applied relative to the initial position\n  /// set by the [position].\n  ///\n  /// When not set, the offset defaults to [Offset.zero].\n  final Offset offset;\n\n  /// Whether this popup menu button is interactive.\n  ///\n  /// Defaults to true.\n  ///\n  /// If true, the button will respond to presses by displaying the menu.\n  ///\n  /// If false, the button is styled with the disabled color from the\n  /// current [Theme] and will not respond to presses or show the popup\n  /// menu and [onSelected], [onCanceled] and [itemBuilder] will not be called.\n  ///\n  /// This can be useful in situations where the app needs to show the button,\n  /// but doesn't currently have anything to show in the menu.\n  final bool enabled;\n\n  /// If provided, the shape used for the menu.\n  ///\n  /// If this property is null, then [PopupMenuThemeData.shape] is used.\n  /// If [PopupMenuThemeData.shape] is also null, then the default shape for\n  /// [MaterialType.card] is used. This default shape is a rectangle with\n  /// rounded edges of BorderRadius.circular(2.0).\n  final ShapeBorder? shape;\n\n  /// If provided, the background color used for the menu.\n  ///\n  /// If this property is null, then [PopupMenuThemeData.color] is used.\n  /// If [PopupMenuThemeData.color] is also null, then\n  /// [ThemeData.cardColor] is used in Material 2. In Material3, defaults to\n  /// [ColorScheme.surfaceContainer].\n  final Color? color;\n\n  /// If provided, this color is used for the button icon.\n  ///\n  /// If this property is null, then [PopupMenuThemeData.iconColor] is used.\n  /// If [PopupMenuThemeData.iconColor] is also null then defaults to\n  /// [IconThemeData.color].\n  final Color? iconColor;\n\n  /// Whether detected gestures should provide acoustic and/or haptic feedback.\n  ///\n  /// For example, on Android a tap will produce a clicking sound and a\n  /// long-press will produce a short vibration, when feedback is enabled.\n  ///\n  /// See also:\n  ///\n  ///  * [Feedback] for providing platform-specific feedback to certain actions.\n  final bool? enableFeedback;\n\n  /// If provided, the size of the [Icon].\n  ///\n  /// If this property is null, then [IconThemeData.size] is used.\n  /// If [IconThemeData.size] is also null, then\n  /// default size is 24.0 pixels.\n  final double? iconSize;\n\n  /// Optional size constraints for the menu.\n  ///\n  /// When unspecified, defaults to:\n  /// ```dart\n  /// const BoxConstraints(\n  ///   minWidth: 2.0 * 56.0,\n  ///   maxWidth: 5.0 * 56.0,\n  /// )\n  /// ```\n  ///\n  /// The default constraints ensure that the menu width matches maximum width\n  /// recommended by the Material Design guidelines.\n  /// Specifying this parameter enables creation of menu wider than\n  /// the default maximum width.\n  final BoxConstraints? constraints;\n\n  /// Whether the popup menu is positioned over or under the popup menu button.\n  ///\n  /// [offset] is used to change the position of the popup menu relative to the\n  /// position set by this parameter.\n  ///\n  /// If this property is `null`, then [PopupMenuThemeData.position] is used. If\n  /// [PopupMenuThemeData.position] is also `null`, then the position defaults\n  /// to [PopupMenuPosition.over] which makes the popup menu appear directly\n  /// over the button that was used to create it.\n  final PopupMenuPosition? position;\n\n  /// {@macro flutter.material.Material.clipBehavior}\n  ///\n  /// The [clipBehavior] argument is used the clip shape of the menu.\n  ///\n  /// Defaults to [Clip.none].\n  final Clip clipBehavior;\n\n  /// Used to determine whether to push the menu to the [Navigator] furthest\n  /// from or nearest to the given `context`.\n  ///\n  /// Defaults to false.\n  final bool useRootNavigator;\n\n  /// Used to override the default animation curves and durations of the popup\n  /// menu's open and close transitions.\n  ///\n  /// If [AnimationStyle.curve] is provided, it will be used to override\n  /// the default popup animation curve. Otherwise, defaults to [Curves.linear].\n  ///\n  /// If [AnimationStyle.reverseCurve] is provided, it will be used to\n  /// override the default popup animation reverse curve. Otherwise, defaults to\n  /// `Interval(0.0, 2.0 / 3.0)`.\n  ///\n  /// If [AnimationStyle.duration] is provided, it will be used to override\n  /// the default popup animation duration. Otherwise, defaults to 300ms.\n  ///\n  /// To disable the theme animation, use [AnimationStyle.noAnimation].\n  ///\n  /// If this is null, then the default animation will be used.\n  final AnimationStyle? popUpAnimationStyle;\n\n  /// Optional route settings for the menu.\n  ///\n  /// See [RouteSettings] for details.\n  final RouteSettings? routeSettings;\n\n  /// Customizes this icon button's appearance.\n  ///\n  /// The [style] is only used for Material 3 [IconButton]s. If [ThemeData.useMaterial3]\n  /// is set to true, [style] is preferred for icon button customization, and any\n  /// parameters defined in [style] will override the same parameters in [IconButton].\n  ///\n  /// Null by default.\n  final ButtonStyle? style;\n\n  @override\n  PopupMenuButtonState<T> createState() => PopupMenuButtonState<T>();\n}\n\n/// The [State] for a [PopupMenuButton].\n///\n/// See [showButtonMenu] for a way to programmatically open the popup menu\n/// of your button state.\nclass PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {\n  /// A method to show a popup menu with the items supplied to\n  /// [PopupMenuButton.itemBuilder] at the position of your [PopupMenuButton].\n  ///\n  /// By default, it is called when the user taps the button and [PopupMenuButton.enabled]\n  /// is set to `true`. Moreover, you can open the button by calling the method manually.\n  ///\n  /// You would access your [PopupMenuButtonState] using a [GlobalKey] and\n  /// show the menu of the button with `globalKey.currentState.showButtonMenu`.\n  void showButtonMenu() {\n    final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);\n    final RenderBox button = context.findRenderObject()! as RenderBox;\n    final RenderBox overlay =\n        Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;\n    final PopupMenuPosition popupMenuPosition =\n        widget.position ?? popupMenuTheme.position ?? PopupMenuPosition.over;\n    late Offset offset;\n    switch (popupMenuPosition) {\n      case PopupMenuPosition.over:\n        offset = widget.offset;\n      case PopupMenuPosition.under:\n        offset = Offset(0.0, button.size.height) + widget.offset;\n        if (widget.child == null) {\n          // Remove the padding of the icon button.\n          offset -= Offset(0.0, widget.padding.vertical / 2);\n        }\n    }\n    final RelativeRect position = RelativeRect.fromRect(\n      Rect.fromPoints(\n        button.localToGlobal(offset, ancestor: overlay),\n        button.localToGlobal(\n          button.size.bottomRight(Offset.zero) + offset,\n          ancestor: overlay,\n        ),\n      ),\n      Offset.zero & overlay.size,\n    );\n    final List<PopupMenuEntry<T>> items = widget.itemBuilder(context);\n    // Only show the menu if there is something to show\n    if (items.isNotEmpty) {\n      var popUpAnimationStyle = widget.popUpAnimationStyle;\n      if (popUpAnimationStyle == null &&\n          defaultTargetPlatform == TargetPlatform.iOS) {\n        popUpAnimationStyle = AnimationStyle(\n          curve: Curves.easeInOut,\n          duration: const Duration(milliseconds: 300),\n        );\n      }\n      widget.onOpened?.call();\n      showMenu<T?>(\n        context: context,\n        elevation: widget.elevation ?? popupMenuTheme.elevation,\n        shadowColor: widget.shadowColor ?? popupMenuTheme.shadowColor,\n        surfaceTintColor:\n            widget.surfaceTintColor ?? popupMenuTheme.surfaceTintColor,\n        items: items,\n        initialValue: widget.initialValue,\n        position: position,\n        shape: widget.shape ?? popupMenuTheme.shape,\n        color: widget.color ?? popupMenuTheme.color,\n        constraints: widget.constraints,\n        clipBehavior: widget.clipBehavior,\n        useRootNavigator: widget.useRootNavigator,\n        popUpAnimationStyle: popUpAnimationStyle,\n        routeSettings: widget.routeSettings,\n      ).then<void>((T? newValue) {\n        if (!mounted) {\n          return null;\n        }\n        if (newValue == null) {\n          widget.onCanceled?.call();\n          return null;\n        }\n        widget.onSelected?.call(newValue);\n      });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final IconThemeData iconTheme = IconTheme.of(context);\n    final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);\n    final bool enableFeedback = widget.enableFeedback ??\n        PopupMenuTheme.of(context).enableFeedback ??\n        true;\n\n    assert(debugCheckHasMaterialLocalizations(context));\n\n    if (widget.child != null) {\n      return AnimatedGestureDetector(\n        scaleFactor: 0.95,\n        onTapUp: widget.enabled ? showButtonMenu : null,\n        child: widget.child!,\n      );\n    }\n\n    return IconButton(\n      icon: widget.icon ?? Icon(Icons.adaptive.more),\n      padding: widget.padding,\n      splashRadius: widget.splashRadius,\n      iconSize: widget.iconSize ?? popupMenuTheme.iconSize ?? iconTheme.size,\n      color: widget.iconColor ?? popupMenuTheme.iconColor ?? iconTheme.color,\n      tooltip:\n          widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,\n      onPressed: widget.enabled ? showButtonMenu : null,\n      enableFeedback: enableFeedback,\n      style: widget.style,\n    );\n  }\n}\n\nclass _PopupMenuDefaultsM2 extends PopupMenuThemeData {\n  _PopupMenuDefaultsM2(this.context) : super(elevation: 8.0);\n\n  final BuildContext context;\n  late final ThemeData _theme = Theme.of(context);\n  late final TextTheme _textTheme = _theme.textTheme;\n\n  @override\n  TextStyle? get textStyle => _textTheme.titleMedium;\n\n  static EdgeInsets menuHorizontalPadding =\n      const EdgeInsets.symmetric(horizontal: 16.0);\n}\n\n// BEGIN GENERATED TOKEN PROPERTIES - PopupMenu\n\n// Do not edit by hand. The code between the \"BEGIN GENERATED\" and\n// \"END GENERATED\" comments are generated from data in the Material\n// Design token database by the script:\n//   dev/tools/gen_defaults/bin/gen_defaults.dart.\n\nclass _PopupMenuDefaultsM3 extends PopupMenuThemeData {\n  _PopupMenuDefaultsM3(this.context) : super(elevation: 3.0);\n\n  final BuildContext context;\n  late final ThemeData _theme = Theme.of(context);\n  late final ColorScheme _colors = _theme.colorScheme;\n  late final TextTheme _textTheme = _theme.textTheme;\n\n  @override\n  WidgetStateProperty<TextStyle?>? get labelTextStyle {\n    return WidgetStateProperty.resolveWith((Set<WidgetState> states) {\n      final TextStyle style = _textTheme.labelLarge!;\n      if (states.contains(WidgetState.disabled)) {\n        return style.apply(color: _colors.onSurface.withValues(alpha: 0.38));\n      }\n      return style.apply(color: _colors.onSurface);\n    });\n  }\n\n  @override\n  Color? get color => _colors.surfaceContainer;\n\n  @override\n  Color? get shadowColor => _colors.shadow;\n\n  @override\n  Color? get surfaceTintColor => Colors.transparent;\n\n  @override\n  ShapeBorder? get shape => const RoundedRectangleBorder(\n        borderRadius: BorderRadius.all(Radius.circular(4.0)),\n      );\n\n  // TODO(tahatesser): This is taken from https://m3.material.io/components/menus/specs\n  // Update this when the token is available.\n  static EdgeInsets menuHorizontalPadding =\n      const EdgeInsets.symmetric(horizontal: 12.0);\n}\n// END GENERATED TOKEN PROPERTIES - PopupMenu\n\nextension PopupMenuColors on BuildContext {\n  Color get popupMenuBackgroundColor {\n    if (Theme.of(this).brightness == Brightness.light) {\n      return Theme.of(this).colorScheme.surface;\n    }\n    return const Color(0xFF23262B);\n  }\n}\n\nclass _CurveTween extends Animatable<double> {\n  /// Creates a curve tween.\n  _CurveTween({required this.curve});\n\n  /// The curve to use when transforming the value of the animation.\n  Curve curve;\n\n  @override\n  double transform(double t) {\n    return curve.transform(t.clamp(0, 1));\n  }\n\n  @override\n  String toString() =>\n      '${objectRuntimeType(this, 'CurveTween')}(curve: $curve)';\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/settings/show_settings.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/af_focus_manager.dart';\nimport 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'\n    show UserProfilePB;\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nfinal GlobalKey _settingsDialogKey = GlobalKey();\n\n// show settings dialog with user profile for fully customized settings dialog\nvoid showSettingsDialog(\n  BuildContext context,\n  UserProfilePB userProfile, [\n  UserWorkspaceBloc? bloc,\n  SettingsPage? initPage,\n]) {\n  AFFocusManager.of(context).notifyLoseFocus();\n  showDialog(\n    context: context,\n    builder: (dialogContext) => MultiBlocProvider(\n      key: _settingsDialogKey,\n      providers: [\n        BlocProvider<DocumentAppearanceCubit>.value(\n          value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),\n        ),\n        BlocProvider.value(value: bloc ?? context.read<UserWorkspaceBloc>()),\n      ],\n      child: SettingsDialog(\n        userProfile,\n        initPage: initPage,\n        didLogout: () async {\n          // Pop the dialog using the dialog context\n          Navigator.of(dialogContext).pop();\n          await runAppFlowy();\n        },\n        dismissDialog: () {\n          if (Navigator.of(dialogContext).canPop()) {\n            return Navigator.of(dialogContext).pop();\n          }\n          Log.warn(\"Can't pop dialog context\");\n        },\n        restartApp: () async {\n          // Pop the dialog using the dialog context\n          Navigator.of(dialogContext).pop();\n          await runAppFlowy();\n        },\n      ),\n    ),\n  );\n}\n\n// show settings dialog without user profile for simple settings dialog\n// only support\n//  - language\n//  - self-host\n//  - support\nvoid showSimpleSettingsDialog(BuildContext context) {\n  showDialog(\n    context: context,\n    builder: (dialogContext) => const SimpleSettingsDialog(),\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/text_field/text_filed_with_metric_lines.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass TextFieldWithMetricLines extends StatefulWidget {\n  const TextFieldWithMetricLines({\n    super.key,\n    this.controller,\n    this.focusNode,\n    this.maxLines,\n    this.style,\n    this.decoration,\n    this.onLineCountChange,\n    this.enabled = true,\n  });\n\n  final TextEditingController? controller;\n  final FocusNode? focusNode;\n  final int? maxLines;\n  final TextStyle? style;\n  final InputDecoration? decoration;\n  final void Function(int count)? onLineCountChange;\n  final bool enabled;\n\n  @override\n  State<TextFieldWithMetricLines> createState() =>\n      _TextFieldWithMetricLinesState();\n}\n\nclass _TextFieldWithMetricLinesState extends State<TextFieldWithMetricLines> {\n  final key = GlobalKey();\n  late final controller = widget.controller ?? TextEditingController();\n\n  @override\n  void initState() {\n    super.initState();\n\n    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\n      updateDisplayedLineCount(context);\n    });\n  }\n\n  @override\n  void dispose() {\n    if (widget.controller == null) {\n      // dispose the controller if it was created by this widget\n      controller.dispose();\n    }\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return TextField(\n      key: key,\n      enabled: widget.enabled,\n      controller: widget.controller,\n      focusNode: widget.focusNode,\n      maxLines: widget.maxLines,\n      style: widget.style,\n      decoration: widget.decoration,\n      onChanged: (_) => updateDisplayedLineCount(context),\n    );\n  }\n\n  // calculate the number of lines that would be displayed in the text field\n  void updateDisplayedLineCount(BuildContext context) {\n    if (widget.onLineCountChange == null) {\n      return;\n    }\n\n    final renderObject = key.currentContext?.findRenderObject();\n    if (renderObject == null || renderObject is! RenderBox) {\n      return;\n    }\n\n    final size = renderObject.size;\n    final text = controller.buildTextSpan(\n      context: context,\n      style: widget.style,\n      withComposing: false,\n    );\n    final textPainter = TextPainter(\n      text: text,\n      textDirection: Directionality.of(context),\n    );\n\n    textPainter.layout(minWidth: size.width, maxWidth: size.width);\n\n    final lines = textPainter.computeLineMetrics().length;\n    widget.onLineCountChange?.call(lines);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/time_format.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';\nimport 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\nimport 'package:time/time.dart';\n\nString formatTimestampWithContext(\n  BuildContext context, {\n  required int timestamp,\n  String? prefix,\n}) {\n  final now = DateTime.now();\n  final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);\n  final difference = now.difference(dateTime);\n  final String date;\n\n  final dateFormat = context.read<AppearanceSettingsCubit>().state.dateFormat;\n  final timeFormat = context.read<AppearanceSettingsCubit>().state.timeFormat;\n\n  if (difference.inMinutes < 1) {\n    date = LocaleKeys.sideBar_justNow.tr();\n  } else if (difference.inHours < 1 && dateTime.isToday) {\n    // Less than 1 hour\n    date = LocaleKeys.sideBar_minutesAgo\n        .tr(namedArgs: {'count': difference.inMinutes.toString()});\n  } else if (difference.inHours >= 1 && dateTime.isToday) {\n    // in same day\n    date = timeFormat.formatTime(dateTime);\n  } else {\n    date = dateFormat.formatDate(dateTime, false);\n  }\n\n  if (difference.inHours >= 1 && prefix != null) {\n    return '$prefix $date';\n  }\n\n  return date;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/version_checker/version_checker.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/startup/tasks/device_info_task.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:auto_updater/auto_updater.dart';\nimport 'package:collection/collection.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:universal_platform/universal_platform.dart';\nimport 'package:xml/xml.dart' as xml;\n\nfinal versionChecker = VersionChecker();\n\n/// Version checker class to handle update checks using appcast XML feeds\nclass VersionChecker {\n  factory VersionChecker() => _instance;\n\n  VersionChecker._internal();\n  String? _feedUrl;\n\n  static final VersionChecker _instance = VersionChecker._internal();\n\n  /// Sets the appcast XML feed URL\n  void setFeedUrl(String url) {\n    _feedUrl = url;\n\n    if (UniversalPlatform.isWindows || UniversalPlatform.isMacOS) {\n      autoUpdater.setFeedURL(url);\n      // disable the auto update check\n      autoUpdater.setScheduledCheckInterval(0);\n    }\n  }\n\n  /// Checks for updates by fetching and parsing the appcast XML\n  /// Returns a list of [AppcastItem] or throws an exception if the feed URL is not set\n  Future<AppcastItem?> checkForUpdateInformation() async {\n    if (_feedUrl == null) {\n      Log.error('Feed URL is not set');\n      return null;\n    }\n\n    try {\n      final response = await http.get(Uri.parse(_feedUrl!));\n      if (response.statusCode != 200) {\n        Log.info('Failed to fetch appcast XML: ${response.statusCode}');\n        return null;\n      }\n\n      // Parse XML content\n      final document = xml.XmlDocument.parse(response.body);\n      final items = document.findAllElements('item');\n\n      // Convert XML items to AppcastItem objects\n      return items\n          .map(_parseAppcastItem)\n          .nonNulls\n          .firstWhereOrNull((e) => e.os == ApplicationInfo.os);\n    } catch (e) {\n      Log.info('Failed to check for updates: $e');\n    }\n\n    return null;\n  }\n\n  /// For Windows and macOS, calling this API will trigger the auto updater to check for updates\n  /// For Linux, it will open the official website in the browser if there is a new version\n\n  Future<void> checkForUpdate() async {\n    if (UniversalPlatform.isLinux) {\n      // open the official website in the browser\n      await afLaunchUrlString('https://appflowy.com/download');\n    } else {\n      await autoUpdater.checkForUpdates();\n    }\n  }\n\n  AppcastItem? _parseAppcastItem(xml.XmlElement item) {\n    final enclosure = item.findElements('enclosure').firstOrNull;\n    return AppcastItem.fromJson({\n      'title': item.findElements('title').firstOrNull?.innerText,\n      'versionString': item\n          .findElements('sparkle:shortVersionString')\n          .firstOrNull\n          ?.innerText,\n      'displayVersionString': item\n          .findElements('sparkle:shortVersionString')\n          .firstOrNull\n          ?.innerText,\n      'releaseNotesUrl':\n          item.findElements('releaseNotesLink').firstOrNull?.innerText,\n      'pubDate': item.findElements('pubDate').firstOrNull?.innerText,\n      'fileURL': enclosure?.getAttribute('url') ?? '',\n      'os': enclosure?.getAttribute('sparkle:os') ?? '',\n      'criticalUpdate':\n          enclosure?.getAttribute('sparkle:criticalUpdate') ?? false,\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/shared/window_title_bar.dart",
    "content": "import 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\nimport 'package:window_manager/window_manager.dart';\n\nclass WindowsButtonListener extends WindowListener {\n  WindowsButtonListener();\n\n  final ValueNotifier<bool> isMaximized = ValueNotifier(false);\n\n  @override\n  void onWindowMaximize() => isMaximized.value = true;\n\n  @override\n  void onWindowUnmaximize() => isMaximized.value = false;\n\n  void dispose() => isMaximized.dispose();\n}\n\nclass WindowTitleBar extends StatefulWidget {\n  const WindowTitleBar({\n    super.key,\n    this.leftChildren = const [],\n  });\n\n  final List<Widget> leftChildren;\n\n  @override\n  State<WindowTitleBar> createState() => _WindowTitleBarState();\n}\n\nclass _WindowTitleBarState extends State<WindowTitleBar> {\n  late final WindowsButtonListener? windowsButtonListener;\n  bool isMaximized = false;\n\n  @override\n  void initState() {\n    super.initState();\n\n    if (UniversalPlatform.isWindows || UniversalPlatform.isLinux) {\n      windowsButtonListener = WindowsButtonListener();\n      windowManager.addListener(windowsButtonListener!);\n      windowsButtonListener!.isMaximized.addListener(_isMaximizedChanged);\n    } else {\n      windowsButtonListener = null;\n    }\n\n    windowManager\n        .isMaximized()\n        .then((v) => mounted ? setState(() => isMaximized = v) : null);\n  }\n\n  void _isMaximizedChanged() {\n    if (mounted) {\n      setState(() => isMaximized = windowsButtonListener!.isMaximized.value);\n    }\n  }\n\n  @override\n  void dispose() {\n    if (windowsButtonListener != null) {\n      windowManager.removeListener(windowsButtonListener!);\n      windowsButtonListener!.isMaximized.removeListener(_isMaximizedChanged);\n      windowsButtonListener?.dispose();\n    }\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final brightness = Theme.of(context).brightness;\n\n    return Container(\n      height: 40,\n      decoration: BoxDecoration(\n        color: Theme.of(context).colorScheme.surfaceContainerHighest,\n      ),\n      child: DragToMoveArea(\n        child: Row(\n          children: [\n            const HSpace(4),\n            ...widget.leftChildren,\n            const Spacer(),\n            WindowCaptionButton.minimize(\n              brightness: brightness,\n              onPressed: () => windowManager.minimize(),\n            ),\n            if (isMaximized) ...[\n              WindowCaptionButton.unmaximize(\n                brightness: brightness,\n                onPressed: () => windowManager.unmaximize(),\n              ),\n            ] else ...[\n              WindowCaptionButton.maximize(\n                brightness: brightness,\n                onPressed: () => windowManager.maximize(),\n              ),\n            ],\n            WindowCaptionButton.close(\n              brightness: brightness,\n              onPressed: () => windowManager.close(),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/deps_resolver.dart",
    "content": "import 'package:appflowy/ai/service/appflowy_ai_service.dart';\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/network_monitor.dart';\nimport 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/mobile/presentation/search/view_ancestor_cache.dart';\nimport 'package:appflowy/plugins/document/application/prelude.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/trash/application/prelude.dart';\nimport 'package:appflowy/shared/appflowy_cache_manager.dart';\nimport 'package:appflowy/shared/custom_image_cache_manager.dart';\nimport 'package:appflowy/shared/easy_localiation_service.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/startup/tasks/appflowy_cloud_task.dart';\nimport 'package:appflowy/user/application/auth/af_cloud_auth_service.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/application/prelude.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/user/application/user_listener.dart';\nimport 'package:appflowy/user/presentation/router.dart';\nimport 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';\nimport 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/recent/cached_recent_service.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/desktop_appearance.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/mobile_appearance.dart';\nimport 'package:appflowy/workspace/application/settings/prelude.dart';\nimport 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart';\nimport 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/user/prelude.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy/workspace/application/workspace/prelude.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flowy_infra/file_picker/file_picker_impl.dart';\nimport 'package:flowy_infra/file_picker/file_picker_service.dart';\nimport 'package:fluttertoast/fluttertoast.dart';\nimport 'package:get_it/get_it.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass DependencyResolver {\n  static Future<void> resolve(\n    GetIt getIt,\n    IntegrationMode mode,\n  ) async {\n    // getIt.registerFactory<KeyValueStorage>(() => RustKeyValue());\n    getIt.registerFactory<KeyValueStorage>(() => DartKeyValue());\n\n    await _resolveCloudDeps(getIt);\n    _resolveUserDeps(getIt, mode);\n    _resolveHomeDeps(getIt);\n    _resolveFolderDeps(getIt);\n    _resolveCommonService(getIt, mode);\n  }\n}\n\nFuture<void> _resolveCloudDeps(GetIt getIt) async {\n  final env = await AppFlowyCloudSharedEnv.fromEnv();\n  Log.info(\"cloud setting: $env\");\n  getIt.registerFactory<AppFlowyCloudSharedEnv>(() => env);\n  getIt.registerFactory<AIRepository>(() => AppFlowyAIService());\n\n  if (isAppFlowyCloudEnabled) {\n    getIt.registerSingleton(\n      AppFlowyCloudDeepLink(),\n      dispose: (obj) async {\n        await obj.dispose();\n      },\n    );\n  }\n}\n\nvoid _resolveCommonService(\n  GetIt getIt,\n  IntegrationMode mode,\n) async {\n  getIt.registerFactory<FilePickerService>(() => FilePicker());\n\n  getIt.registerFactory<ApplicationDataStorage>(\n    () => mode.isTest ? MockApplicationDataStorage() : ApplicationDataStorage(),\n  );\n\n  getIt.registerFactory<ClipboardService>(\n    () => ClipboardService(),\n  );\n\n  // theme\n  getIt.registerFactory<BaseAppearance>(\n    () => UniversalPlatform.isMobile ? MobileAppearance() : DesktopAppearance(),\n  );\n\n  getIt.registerFactory<FlowyCacheManager>(\n    () => FlowyCacheManager()\n      ..registerCache(TemporaryDirectoryCache())\n      ..registerCache(CustomImageCacheManager())\n      ..registerCache(FeatureFlagCache()),\n  );\n\n  getIt.registerSingleton<EasyLocalizationService>(EasyLocalizationService());\n}\n\nvoid _resolveUserDeps(GetIt getIt, IntegrationMode mode) {\n  switch (currentCloudType()) {\n    case AuthenticatorType.local:\n      getIt.registerFactory<AuthService>(\n        () => BackendAuthService(\n          AuthTypePB.Local,\n        ),\n      );\n      break;\n    case AuthenticatorType.appflowyCloud:\n    case AuthenticatorType.appflowyCloudSelfHost:\n    case AuthenticatorType.appflowyCloudDevelop:\n      getIt.registerFactory<AuthService>(() => AppFlowyCloudAuthService());\n      break;\n  }\n\n  getIt.registerFactory<AuthRouter>(() => AuthRouter());\n\n  getIt.registerFactory<SignInBloc>(\n    () => SignInBloc(getIt<AuthService>()),\n  );\n  getIt.registerFactory<SignUpBloc>(\n    () => SignUpBloc(getIt<AuthService>()),\n  );\n\n  getIt.registerFactory<SplashRouter>(() => SplashRouter());\n  getIt.registerFactory<EditPanelBloc>(() => EditPanelBloc());\n  getIt.registerFactory<SplashBloc>(() => SplashBloc());\n  getIt.registerLazySingleton<NetworkListener>(() => NetworkListener());\n  getIt.registerLazySingleton<CachedRecentService>(() => CachedRecentService());\n  getIt.registerLazySingleton<ViewAncestorCache>(() => ViewAncestorCache());\n  getIt.registerLazySingleton<SubscriptionSuccessListenable>(\n    () => SubscriptionSuccessListenable(),\n  );\n}\n\nvoid _resolveHomeDeps(GetIt getIt) {\n  getIt.registerSingleton(FToast());\n\n  getIt.registerSingleton(MenuSharedState());\n\n  getIt.registerFactoryParam<UserListener, UserProfilePB, void>(\n    (user, _) => UserListener(userProfile: user),\n  );\n\n  // share\n  getIt.registerFactoryParam<ShareBloc, ViewPB, void>(\n    (view, _) => ShareBloc(view: view),\n  );\n\n  getIt.registerSingleton<ActionNavigationBloc>(ActionNavigationBloc());\n\n  getIt.registerLazySingleton<TabsBloc>(() => TabsBloc());\n\n  getIt.registerSingleton<ReminderBloc>(ReminderBloc());\n\n  getIt.registerSingleton<RenameViewBloc>(RenameViewBloc(PopoverController()));\n}\n\nvoid _resolveFolderDeps(GetIt getIt) {\n  // Workspace\n  getIt.registerFactoryParam<WorkspaceListener, UserProfilePB, String>(\n    (user, workspaceId) =>\n        WorkspaceListener(user: user, workspaceId: workspaceId),\n  );\n\n  getIt.registerFactoryParam<ViewBloc, ViewPB, void>(\n    (view, _) => ViewBloc(\n      view: view,\n    ),\n  );\n\n  // User\n  getIt.registerFactoryParam<SettingsUserViewBloc, UserProfilePB, void>(\n    (user, _) => SettingsUserViewBloc(user),\n  );\n\n  // Trash\n  getIt.registerLazySingleton<TrashService>(() => TrashService());\n  getIt.registerLazySingleton<TrashListener>(() => TrashListener());\n  getIt.registerFactory<TrashBloc>(\n    () => TrashBloc(),\n  );\n\n  // Favorite\n  getIt.registerFactory<FavoriteBloc>(() => FavoriteBloc());\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/entry_point.dart",
    "content": "import 'package:appflowy/startup/launch_configuration.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/presentation/screens/splash_screen.dart';\nimport 'package:flutter/material.dart';\n\nclass AppFlowyApplication implements EntryPoint {\n  @override\n  Widget create(LaunchConfiguration config) {\n    return SplashScreen(isAnon: config.isAnon);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/launch_configuration.dart",
    "content": "class LaunchConfiguration {\n  const LaunchConfiguration({\n    this.isAnon = false,\n    required this.version,\n    required this.rustEnvs,\n  });\n\n  // APP will automatically register after launching.\n  final bool isAnon;\n  final String version;\n  //\n  final Map<String, String> rustEnvs;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/plugin/plugin.dart",
    "content": "library;\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/home/home_stack.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:flutter/widgets.dart';\n\nexport \"./src/sandbox.dart\";\n\nenum PluginType {\n  document,\n  blank,\n  trash,\n  grid,\n  board,\n  calendar,\n  databaseDocument,\n  chat,\n}\n\ntypedef PluginId = String;\n\nabstract class Plugin {\n  PluginId get id;\n\n  PluginWidgetBuilder get widgetBuilder;\n\n  PluginNotifier? get notifier => null;\n\n  PluginType get pluginType;\n\n  void init() {}\n\n  void dispose() {\n    notifier?.dispose();\n  }\n}\n\nabstract class PluginNotifier<T> {\n  /// Notify if the plugin get deleted\n  ValueNotifier<T> get isDeleted;\n\n  void dispose() {}\n}\n\nabstract class PluginBuilder {\n  Plugin build(dynamic data);\n\n  String get menuName;\n\n  FlowySvgData get icon;\n\n  /// The type of this [Plugin]. Each [Plugin] should have a unique [PluginType]\n  PluginType get pluginType;\n\n  /// The layoutType is used in the backend to determine the layout of the view.\n  /// Currently, AppFlowy supports 4 layout types: Document, Grid, Board, Calendar.\n  ViewLayoutPB? get layoutType;\n}\n\nabstract class PluginConfig {\n  // Return false will disable the user to create it. For example, a trash plugin shouldn't be created by the user,\n  bool get creatable => true;\n}\n\nabstract class PluginWidgetBuilder with NavigationItem {\n  List<NavigationItem> get navigationItems;\n\n  EdgeInsets get contentPadding =>\n      const EdgeInsets.symmetric(horizontal: 40, vertical: 28);\n\n  Widget buildWidget({\n    required PluginContext context,\n    required bool shrinkWrap,\n    Map<String, dynamic>? data,\n  });\n}\n\nclass PluginContext {\n  PluginContext({\n    this.userProfile,\n    this.onDeleted,\n  });\n\n  // calls when widget of the plugin get deleted\n  final Function(ViewPB, int?)? onDeleted;\n  final UserProfilePB? userProfile;\n}\n\nvoid registerPlugin({required PluginBuilder builder, PluginConfig? config}) {\n  getIt<PluginSandbox>()\n      .registerPlugin(builder.pluginType, builder, config: config);\n}\n\n/// Make the correct plugin from the [pluginType] and [data]. If the plugin\n///  is not registered, it will return a blank plugin.\nPlugin makePlugin({required PluginType pluginType, dynamic data}) {\n  final plugin = getIt<PluginSandbox>().buildPlugin(pluginType, data);\n  return plugin;\n}\n\nList<PluginBuilder> pluginBuilders() {\n  final pluginBuilders = getIt<PluginSandbox>().builders;\n  final pluginConfigs = getIt<PluginSandbox>().pluginConfigs;\n  return pluginBuilders.where(\n    (builder) {\n      final config = pluginConfigs[builder.pluginType]?.creatable;\n      return config ?? true;\n    },\n  ).toList();\n}\n\nenum FlowyPluginException {\n  invalidData,\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/plugin/src/runner.dart",
    "content": "class PluginRunner {}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/plugin/src/sandbox.dart",
    "content": "import 'dart:collection';\n\nimport 'package:appflowy/plugins/blank/blank.dart';\nimport 'package:flutter/services.dart';\n\nimport '../plugin.dart';\nimport 'runner.dart';\n\nclass PluginSandbox {\n  PluginSandbox() {\n    pluginRunner = PluginRunner();\n  }\n\n  final LinkedHashMap<PluginType, PluginBuilder> _pluginBuilders =\n      LinkedHashMap();\n  final Map<PluginType, PluginConfig> _pluginConfigs =\n      <PluginType, PluginConfig>{};\n  late PluginRunner pluginRunner;\n\n  int indexOf(PluginType pluginType) {\n    final index =\n        _pluginBuilders.keys.toList().indexWhere((ty) => ty == pluginType);\n    if (index == -1) {\n      throw PlatformException(\n        code: '-1',\n        message: \"Can't find the flowy plugin type: $pluginType\",\n      );\n    }\n    return index;\n  }\n\n  /// Build a plugin from [data] with [pluginType]\n  /// If the [pluginType] is not registered, it will return a blank plugin\n  Plugin buildPlugin(PluginType pluginType, dynamic data) {\n    final builder = _pluginBuilders[pluginType] ?? BlankPluginBuilder();\n    return builder.build(data);\n  }\n\n  void registerPlugin(\n    PluginType pluginType,\n    PluginBuilder builder, {\n    PluginConfig? config,\n  }) {\n    if (_pluginBuilders.containsKey(pluginType)) {\n      return;\n    }\n    _pluginBuilders[pluginType] = builder;\n\n    if (config != null) {\n      _pluginConfigs[pluginType] = config;\n    }\n  }\n\n  List<PluginType> get supportPluginTypes => _pluginBuilders.keys.toList();\n\n  List<PluginBuilder> get builders => _pluginBuilders.values.toList();\n\n  Map<PluginType, PluginConfig> get pluginConfigs => _pluginConfigs;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/startup.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart';\nimport 'package:appflowy/util/expand_views.dart';\nimport 'package:appflowy/workspace/application/settings/prelude.dart';\nimport 'package:appflowy_backend/appflowy_backend.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:get_it/get_it.dart';\nimport 'package:package_info_plus/package_info_plus.dart';\nimport 'package:synchronized/synchronized.dart';\n\nimport 'deps_resolver.dart';\nimport 'entry_point.dart';\nimport 'launch_configuration.dart';\nimport 'plugin/plugin.dart';\nimport 'tasks/af_navigator_observer.dart';\nimport 'tasks/file_storage_task.dart';\nimport 'tasks/prelude.dart';\n\nfinal getIt = GetIt.instance;\n\nabstract class EntryPoint {\n  Widget create(LaunchConfiguration config);\n}\n\nclass FlowyRunnerContext {\n  FlowyRunnerContext({required this.applicationDataDirectory});\n\n  final Directory applicationDataDirectory;\n}\n\nFuture<void> runAppFlowy({bool isAnon = false}) async {\n  Log.info('restart AppFlowy: isAnon: $isAnon');\n\n  if (kReleaseMode) {\n    await FlowyRunner.run(\n      AppFlowyApplication(),\n      integrationMode(),\n      isAnon: isAnon,\n    );\n  } else {\n    // When running the app in integration test mode, we need to\n    // specify the mode to run the app again.\n    await FlowyRunner.run(\n      AppFlowyApplication(),\n      FlowyRunner.currentMode,\n      didInitGetItCallback: IntegrationTestHelper.didInitGetItCallback,\n      rustEnvsBuilder: IntegrationTestHelper.rustEnvsBuilder,\n      isAnon: isAnon,\n    );\n  }\n}\n\nclass FlowyRunner {\n  // This variable specifies the initial mode of the app when it is launched for the first time.\n  // The same mode will be automatically applied in subsequent executions when the runAppFlowy()\n  // method is called.\n  static var currentMode = integrationMode();\n\n  static Future<FlowyRunnerContext> run(\n    EntryPoint f,\n    IntegrationMode mode, {\n    // This callback is triggered after the initialization of 'getIt',\n    // which is used for dependency injection throughout the app.\n    // If your functionality depends on 'getIt', ensure to register\n    // your callback here to execute any necessary actions post-initialization.\n    Future Function()? didInitGetItCallback,\n    // Passing the envs to the backend\n    Map<String, String> Function()? rustEnvsBuilder,\n    // Indicate whether the app is running in anonymous mode.\n    // Note: when the app is running in anonymous mode, the user no need to\n    // sign in, and the app will only save the data in the local storage.\n    bool isAnon = false,\n  }) async {\n    currentMode = mode;\n\n    // Only set the mode when it's not release mode\n    if (!kReleaseMode) {\n      IntegrationTestHelper.didInitGetItCallback = didInitGetItCallback;\n      IntegrationTestHelper.rustEnvsBuilder = rustEnvsBuilder;\n    }\n\n    // Disable the log in test mode\n    Log.shared.disableLog = mode.isTest;\n\n    // Clear and dispose tasks from previous AppLaunch\n    if (getIt.isRegistered(instance: AppLauncher)) {\n      await getIt<AppLauncher>().dispose();\n    }\n\n    // Clear all the states in case of rebuilding.\n    await getIt.reset();\n\n    final config = LaunchConfiguration(\n      isAnon: isAnon,\n      // Unit test can't use the package_info_plus plugin\n      version: mode.isUnitTest\n          ? '1.0.0'\n          : await PackageInfo.fromPlatform().then((value) => value.version),\n      rustEnvs: rustEnvsBuilder?.call() ?? {},\n    );\n\n    // Specify the env\n    await initGetIt(getIt, mode, f, config);\n    await didInitGetItCallback?.call();\n\n    final applicationDataDirectory =\n        await getIt<ApplicationDataStorage>().getPath().then(\n              (value) => Directory(value),\n            );\n\n    // add task\n    final launcher = getIt<AppLauncher>();\n    launcher.addTasks(\n      [\n        // this task should be first task, for handling platform errors.\n        // don't catch errors in test mode\n        if (!mode.isUnitTest && !mode.isIntegrationTest)\n          const PlatformErrorCatcherTask(),\n        // this task should be second task, for handling memory leak.\n        // there's a flag named _enable in memory_leak_detector.dart. If it's false, the task will be ignored.\n        MemoryLeakDetectorTask(),\n        DebugTask(),\n        const FeatureFlagTask(),\n\n        // localization\n        const InitLocalizationTask(),\n        // init the app window\n        InitAppWindowTask(),\n        // Init Rust SDK\n        InitRustSDKTask(customApplicationPath: applicationDataDirectory),\n        // Load Plugins, like document, grid ...\n        const PluginLoadTask(),\n        const FileStorageTask(),\n\n        // init the app widget\n        // ignore in test mode\n        if (!mode.isUnitTest) ...[\n          // The DeviceOrApplicationInfoTask should be placed before the AppWidgetTask to fetch the app information.\n          // It is unable to get the device information from the test environment.\n          const ApplicationInfoTask(),\n          // The auto update task should be placed after the ApplicationInfoTask to fetch the latest version.\n          if (!mode.isIntegrationTest) AutoUpdateTask(),\n          const HotKeyTask(),\n          if (isAppFlowyCloudEnabled) InitAppFlowyCloudTask(),\n          const InitAppWidgetTask(),\n          const InitPlatformServiceTask(),\n          const RecentServiceTask(),\n        ],\n      ],\n    );\n    await launcher.launch(); // execute the tasks\n\n    return FlowyRunnerContext(\n      applicationDataDirectory: applicationDataDirectory,\n    );\n  }\n}\n\nFuture<void> initGetIt(\n  GetIt getIt,\n  IntegrationMode mode,\n  EntryPoint f,\n  LaunchConfiguration config,\n) async {\n  getIt.registerFactory<EntryPoint>(() => f);\n  getIt.registerLazySingleton<FlowySDK>(\n    () {\n      return FlowySDK();\n    },\n    dispose: (sdk) async {\n      await sdk.dispose();\n    },\n  );\n  getIt.registerLazySingleton<AppLauncher>(\n    () => AppLauncher(\n      context: LaunchContext(\n        getIt,\n        mode,\n        config,\n      ),\n    ),\n    dispose: (launcher) async {\n      await launcher.dispose();\n    },\n  );\n  getIt.registerSingleton<PluginSandbox>(PluginSandbox());\n  getIt.registerSingleton<ViewExpanderRegistry>(ViewExpanderRegistry());\n  getIt.registerSingleton<LinkHoverTriggers>(LinkHoverTriggers());\n  getIt.registerSingleton<AFNavigatorObserver>(AFNavigatorObserver());\n  getIt.registerSingleton<FloatingToolbarController>(\n    FloatingToolbarController(),\n  );\n\n  await DependencyResolver.resolve(getIt, mode);\n}\n\nclass LaunchContext {\n  LaunchContext(this.getIt, this.env, this.config);\n\n  GetIt getIt;\n  IntegrationMode env;\n  LaunchConfiguration config;\n}\n\nenum LaunchTaskType {\n  dataProcessing,\n  appLauncher,\n}\n\n/// The interface of an app launch task, which will trigger\n/// some nonresident indispensable task in app launching task.\nclass LaunchTask {\n  const LaunchTask();\n\n  LaunchTaskType get type => LaunchTaskType.dataProcessing;\n\n  @mustCallSuper\n  Future<void> initialize(LaunchContext context) async {\n    Log.info('LaunchTask: $runtimeType initialize');\n  }\n\n  @mustCallSuper\n  Future<void> dispose() async {\n    Log.info('LaunchTask: $runtimeType dispose');\n  }\n}\n\nclass AppLauncher {\n  AppLauncher({\n    required this.context,\n  });\n\n  final LaunchContext context;\n  final List<LaunchTask> tasks = [];\n  final lock = Lock();\n\n  void addTask(LaunchTask task) {\n    lock.synchronized(() {\n      Log.info('AppLauncher: adding task: $task');\n      tasks.add(task);\n    });\n  }\n\n  void addTasks(Iterable<LaunchTask> tasks) {\n    lock.synchronized(() {\n      Log.info('AppLauncher: adding tasks: ${tasks.map((e) => e.runtimeType)}');\n      this.tasks.addAll(tasks);\n    });\n  }\n\n  Future<void> launch() async {\n    await lock.synchronized(() async {\n      final startTime = Stopwatch()..start();\n      Log.info('AppLauncher: start initializing tasks');\n\n      for (final task in tasks) {\n        final startTaskTime = Stopwatch()..start();\n        await task.initialize(context);\n        final endTaskTime = startTaskTime.elapsed.inMilliseconds;\n        Log.info(\n          'AppLauncher: task ${task.runtimeType} initialized in $endTaskTime ms',\n        );\n      }\n\n      final endTime = startTime.elapsed.inMilliseconds;\n      Log.info('AppLauncher: tasks initialized in $endTime ms');\n    });\n  }\n\n  Future<void> dispose() async {\n    await lock.synchronized(() async {\n      Log.info('AppLauncher: start clearing tasks');\n\n      for (final task in tasks) {\n        await task.dispose();\n      }\n\n      tasks.clear();\n\n      Log.info('AppLauncher: tasks cleared');\n    });\n  }\n}\n\nenum IntegrationMode {\n  develop,\n  release,\n  unitTest,\n  integrationTest;\n\n  // test mode\n  bool get isTest => isUnitTest || isIntegrationTest;\n\n  bool get isUnitTest => this == IntegrationMode.unitTest;\n\n  bool get isIntegrationTest => this == IntegrationMode.integrationTest;\n\n  // release mode\n  bool get isRelease => this == IntegrationMode.release;\n\n  // develop mode\n  bool get isDevelop => this == IntegrationMode.develop;\n}\n\nIntegrationMode integrationMode() {\n  if (Platform.environment.containsKey('FLUTTER_TEST')) {\n    return IntegrationMode.unitTest;\n  }\n\n  if (kReleaseMode) {\n    return IntegrationMode.release;\n  }\n\n  return IntegrationMode.develop;\n}\n\n/// Only used for integration test\nclass IntegrationTestHelper {\n  static Future Function()? didInitGetItCallback;\n  static Map<String, String> Function()? rustEnvsBuilder;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/af_navigator_observer.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass AFNavigatorObserver extends NavigatorObserver {\n  final Set<ValueChanged<RouteInfo>> _listeners = {};\n\n  void addListener(ValueChanged<RouteInfo> listener) {\n    _listeners.add(listener);\n  }\n\n  void removeListener(ValueChanged<RouteInfo> listener) {\n    _listeners.remove(listener);\n  }\n\n  @override\n  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {\n    for (final listener in Set.of(_listeners)) {\n      listener(PushRouterInfo(newRoute: route, oldRoute: previousRoute));\n    }\n  }\n\n  @override\n  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {\n    for (final listener in Set.of(_listeners)) {\n      listener(PopRouterInfo(newRoute: route, oldRoute: previousRoute));\n    }\n  }\n\n  @override\n  void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {\n    for (final listener in Set.of(_listeners)) {\n      listener(ReplaceRouterInfo(newRoute: newRoute, oldRoute: oldRoute));\n    }\n  }\n}\n\nabstract class RouteInfo {\n  RouteInfo({this.oldRoute, this.newRoute});\n\n  final Route? oldRoute;\n  final Route? newRoute;\n}\n\nclass PushRouterInfo extends RouteInfo {\n  PushRouterInfo({super.newRoute, super.oldRoute});\n}\n\nclass PopRouterInfo extends RouteInfo {\n  PopRouterInfo({super.newRoute, super.oldRoute});\n}\n\nclass ReplaceRouterInfo extends RouteInfo {\n  ReplaceRouterInfo({super.newRoute, super.oldRoute});\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/mobile/application/mobile_router.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/shared/clipboard_state.dart';\nimport 'package:appflowy/shared/easy_localiation_service.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/user_settings_service.dart';\nimport 'package:appflowy/util/font_family_extension.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';\nimport 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';\nimport 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';\nimport 'package:appflowy/workspace/application/notification/notification_service.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';\nimport 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:provider/provider.dart';\nimport 'package:toastification/toastification.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'prelude.dart';\n\nclass InitAppWidgetTask extends LaunchTask {\n  const InitAppWidgetTask();\n\n  @override\n  LaunchTaskType get type => LaunchTaskType.appLauncher;\n\n  @override\n  Future<void> initialize(LaunchContext context) async {\n    await super.initialize(context);\n\n    WidgetsFlutterBinding.ensureInitialized();\n\n    await NotificationService.initialize();\n\n    await loadIconGroups();\n\n    final widget = context.getIt<EntryPoint>().create(context.config);\n    final appearanceSetting =\n        await UserSettingsBackendService().getAppearanceSetting();\n    final dateTimeSettings =\n        await UserSettingsBackendService().getDateTimeSettings();\n\n    // If the passed-in context is not the same as the context of the\n    // application widget, the application widget will be rebuilt.\n    final app = ApplicationWidget(\n      key: ValueKey(context),\n      appearanceSetting: appearanceSetting,\n      dateTimeSettings: dateTimeSettings,\n      appTheme: await appTheme(appearanceSetting.theme),\n      child: widget,\n    );\n\n    runApp(\n      EasyLocalization(\n        supportedLocales: const [\n          // In alphabetical order\n          Locale('am', 'ET'),\n          Locale('ar', 'SA'),\n          Locale('ca', 'ES'),\n          Locale('cs', 'CZ'),\n          Locale('ckb', 'KU'),\n          Locale('de', 'DE'),\n          Locale('en', 'US'),\n          Locale('en', 'GB'),\n          Locale('es', 'VE'),\n          Locale('eu', 'ES'),\n          Locale('el', 'GR'),\n          Locale('fr', 'FR'),\n          Locale('fr', 'CA'),\n          Locale('he'),\n          Locale('hu', 'HU'),\n          Locale('id', 'ID'),\n          Locale('it', 'IT'),\n          Locale('ja', 'JP'),\n          Locale('ko', 'KR'),\n          Locale('pl', 'PL'),\n          Locale('pt', 'BR'),\n          Locale('ru', 'RU'),\n          Locale('sv', 'SE'),\n          Locale('th', 'TH'),\n          Locale('tr', 'TR'),\n          Locale('uk', 'UA'),\n          Locale('ur'),\n          Locale('vi', 'VN'),\n          Locale('zh', 'CN'),\n          Locale('zh', 'TW'),\n          Locale('fa'),\n          Locale('hin'),\n          Locale('mr', 'IN'),\n        ],\n        path: 'assets/translations',\n        fallbackLocale: const Locale('en', 'US'),\n        useFallbackTranslations: true,\n        child: Builder(\n          builder: (context) {\n            getIt.get<EasyLocalizationService>().init(context);\n            return app;\n          },\n        ),\n      ),\n    );\n\n    return;\n  }\n}\n\nclass ApplicationWidget extends StatefulWidget {\n  const ApplicationWidget({\n    super.key,\n    required this.child,\n    required this.appTheme,\n    required this.appearanceSetting,\n    required this.dateTimeSettings,\n  });\n\n  final Widget child;\n  final AppTheme appTheme;\n  final AppearanceSettingsPB appearanceSetting;\n  final DateTimeSettingsPB dateTimeSettings;\n\n  @override\n  State<ApplicationWidget> createState() => _ApplicationWidgetState();\n}\n\nclass _ApplicationWidgetState extends State<ApplicationWidget> {\n  late final GoRouter routerConfig;\n\n  final _commandPaletteNotifier = ValueNotifier(CommandPaletteNotifierValue());\n\n  final themeBuilder = AppFlowyDefaultTheme();\n\n  @override\n  void initState() {\n    super.initState();\n    // Avoid rebuild routerConfig when the appTheme is changed.\n    routerConfig = generateRouter(widget.child);\n  }\n\n  @override\n  void dispose() {\n    _commandPaletteNotifier.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        if (FeatureFlag.search.isOn)\n          BlocProvider<CommandPaletteBloc>(create: (_) => CommandPaletteBloc()),\n        BlocProvider<AppearanceSettingsCubit>(\n          create: (_) => AppearanceSettingsCubit(\n            widget.appearanceSetting,\n            widget.dateTimeSettings,\n            widget.appTheme,\n          )..readLocaleWhenAppLaunch(context),\n        ),\n        BlocProvider<NotificationSettingsCubit>(\n          create: (_) => NotificationSettingsCubit(),\n        ),\n        BlocProvider<DocumentAppearanceCubit>(\n          create: (_) => DocumentAppearanceCubit()..fetch(),\n        ),\n        BlocProvider.value(value: getIt<RenameViewBloc>()),\n        BlocProvider.value(value: getIt<ActionNavigationBloc>()),\n      ],\n      child: BlocListener<ActionNavigationBloc, ActionNavigationState>(\n        listenWhen: (_, curr) => curr.action != null,\n        listener: (context, state) {\n          final action = state.action;\n          WidgetsBinding.instance.addPostFrameCallback((_) {\n            if (action?.type == ActionType.openView &&\n                UniversalPlatform.isDesktop) {\n              final view =\n                  action!.arguments?[ActionArgumentKeys.view] as ViewPB?;\n              final nodePath = action.arguments?[ActionArgumentKeys.nodePath];\n              final blockId = action.arguments?[ActionArgumentKeys.blockId];\n              if (view != null) {\n                getIt<TabsBloc>().openPlugin(\n                  view,\n                  arguments: {\n                    PluginArgumentKeys.selection: nodePath,\n                    PluginArgumentKeys.blockId: blockId,\n                  },\n                );\n              }\n            } else if (action?.type == ActionType.openRow &&\n                UniversalPlatform.isMobile) {\n              final view = action!.arguments?[ActionArgumentKeys.view];\n              if (view != null) {\n                final view = action.arguments?[ActionArgumentKeys.view];\n                final rowId = action.arguments?[ActionArgumentKeys.rowId];\n                AppGlobals.rootNavKey.currentContext?.pushView(\n                  view,\n                  arguments: {\n                    PluginArgumentKeys.rowId: rowId,\n                  },\n                );\n              }\n            }\n          });\n        },\n        child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(\n          builder: (context, state) {\n            _setSystemOverlayStyle(state);\n            return Provider(\n              create: (_) => ClipboardState(),\n              dispose: (_, state) => state.dispose(),\n              child: ToastificationWrapper(\n                child: Listener(\n                  onPointerSignal: (pointerSignal) {\n                    /// This is a workaround to deal with below question:\n                    /// When the mouse hovers over the tooltip, the scroll event is intercepted by it\n                    /// Here, we listen for the scroll event and then remove the tooltip to avoid that situation\n                    if (pointerSignal is PointerScrollEvent) {\n                      Tooltip.dismissAllToolTips();\n                    }\n                  },\n                  child: MaterialApp.router(\n                    debugShowCheckedModeBanner: false,\n                    theme: state.lightTheme,\n                    darkTheme: state.darkTheme,\n                    themeMode: state.themeMode,\n                    localizationsDelegates: context.localizationDelegates,\n                    supportedLocales: context.supportedLocales,\n                    locale: state.locale,\n                    routerConfig: routerConfig,\n                    builder: (context, child) {\n                      final brightness = Theme.of(context).brightness;\n                      final fontFamily = state.font\n                          .orDefault(defaultFontFamily)\n                          .fontFamilyName;\n\n                      return AnimatedAppFlowyTheme(\n                        data: brightness == Brightness.light\n                            ? themeBuilder.light(fontFamily: fontFamily)\n                            : themeBuilder.dark(fontFamily: fontFamily),\n                        child: MediaQuery(\n                          // use the 1.0 as the textScaleFactor to avoid the text size\n                          //  affected by the system setting.\n                          data: MediaQuery.of(context).copyWith(\n                            textScaler:\n                                TextScaler.linear(state.textScaleFactor),\n                          ),\n                          child: overlayManagerBuilder(\n                            context,\n                            !UniversalPlatform.isMobile &&\n                                    FeatureFlag.search.isOn\n                                ? CommandPalette(\n                                    notifier: _commandPaletteNotifier,\n                                    child: child,\n                                  )\n                                : child,\n                          ),\n                        ),\n                      );\n                    },\n                  ),\n                ),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  void _setSystemOverlayStyle(AppearanceSettingsState state) {\n    if (Platform.isAndroid) {\n      SystemChrome.setEnabledSystemUIMode(\n        SystemUiMode.edgeToEdge,\n        overlays: [],\n      );\n      SystemChrome.setSystemUIOverlayStyle(\n        const SystemUiOverlayStyle(\n          systemNavigationBarColor: Colors.transparent,\n        ),\n      );\n    }\n  }\n}\n\nclass AppGlobals {\n  static GlobalKey<NavigatorState> rootNavKey = GlobalKey();\n\n  static NavigatorState get nav => rootNavKey.currentState!;\n\n  static BuildContext get context => rootNavKey.currentContext!;\n}\n\nFuture<AppTheme> appTheme(String themeName) async {\n  if (themeName.isEmpty) {\n    return AppTheme.fallback;\n  } else {\n    try {\n      return await AppTheme.fromName(themeName);\n    } catch (e) {\n      return AppTheme.fallback;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/app_window_size_manager.dart",
    "content": "import 'dart:convert';\nimport 'dart:ui';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/startup/startup.dart';\n\nclass WindowSizeManager {\n  static const double minWindowHeight = 640.0;\n  static const double minWindowWidth = 640.0;\n  // Preventing failed assertion due to Texture Descriptor Validation\n  static const double maxWindowHeight = 8192.0;\n  static const double maxWindowWidth = 8192.0;\n\n  // Default windows size\n  static const double defaultWindowHeight = 960.0;\n  static const double defaultWindowWidth = 1280.0;\n\n  static const double maxScaleFactor = 2.0;\n  static const double minScaleFactor = 0.5;\n\n  static const width = 'width';\n  static const height = 'height';\n\n  static const String dx = 'dx';\n  static const String dy = 'dy';\n\n  Future<void> setSize(Size size) async {\n    final windowSize = {\n      height: size.height.clamp(minWindowHeight, maxWindowHeight),\n      width: size.width.clamp(minWindowWidth, maxWindowWidth),\n    };\n\n    await getIt<KeyValueStorage>().set(\n      KVKeys.windowSize,\n      jsonEncode(windowSize),\n    );\n  }\n\n  Future<Size> getSize() async {\n    final defaultWindowSize = jsonEncode(\n      {\n        WindowSizeManager.height: defaultWindowHeight,\n        WindowSizeManager.width: defaultWindowWidth,\n      },\n    );\n    final windowSize = await getIt<KeyValueStorage>().get(KVKeys.windowSize);\n    final size = json.decode(\n      windowSize ?? defaultWindowSize,\n    );\n    final double width = size[WindowSizeManager.width] ?? minWindowWidth;\n    final double height = size[WindowSizeManager.height] ?? minWindowHeight;\n    return Size(\n      width.clamp(minWindowWidth, maxWindowWidth),\n      height.clamp(minWindowHeight, maxWindowHeight),\n    );\n  }\n\n  Future<void> setPosition(Offset offset) async {\n    await getIt<KeyValueStorage>().set(\n      KVKeys.windowPosition,\n      jsonEncode({\n        dx: offset.dx,\n        dy: offset.dy,\n      }),\n    );\n  }\n\n  Future<Offset?> getPosition() async {\n    final position = await getIt<KeyValueStorage>().get(KVKeys.windowPosition);\n    if (position == null) {\n      return null;\n    }\n    final offset = json.decode(position);\n    return Offset(offset[dx], offset[dy]);\n  }\n\n  Future<double> getScaleFactor() async {\n    final scaleFactor = await getIt<KeyValueStorage>().getWithFormat<double>(\n          KVKeys.scaleFactor,\n          (value) => double.tryParse(value) ?? 1.0,\n        ) ??\n        1.0;\n    return scaleFactor.clamp(minScaleFactor, maxScaleFactor);\n  }\n\n  Future<void> setScaleFactor(double scaleFactor) async {\n    await getIt<KeyValueStorage>().set(\n      KVKeys.scaleFactor,\n      '${scaleFactor.clamp(minScaleFactor, maxScaleFactor)}',\n    );\n  }\n\n  /// Set the window maximized status\n  Future<void> setWindowMaximized(bool isMaximized) async {\n    await getIt<KeyValueStorage>()\n        .set(KVKeys.windowMaximized, isMaximized.toString());\n  }\n\n  /// Get the window maximized status\n  Future<bool> getWindowMaximized() async {\n    return await getIt<KeyValueStorage>().getWithFormat<bool>(\n          KVKeys.windowMaximized,\n          (v) => bool.tryParse(v) ?? false,\n        ) ??\n        false;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:app_links/app_links.dart';\nimport 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/startup/tasks/app_widget.dart';\nimport 'package:appflowy/startup/tasks/deeplink/deeplink_handler.dart';\nimport 'package:appflowy/startup/tasks/deeplink/expire_login_deeplink_handler.dart';\nimport 'package:appflowy/startup/tasks/deeplink/invitation_deeplink_handler.dart';\nimport 'package:appflowy/startup/tasks/deeplink/login_deeplink_handler.dart';\nimport 'package:appflowy/startup/tasks/deeplink/open_app_deeplink_handler.dart';\nimport 'package:appflowy/startup/tasks/deeplink/payment_deeplink_handler.dart';\nimport 'package:appflowy/user/application/auth/auth_error.dart';\nimport 'package:appflowy/user/application/user_auth_listener.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter/material.dart';\nimport 'package:url_protocol/url_protocol.dart';\n\nconst appflowyDeepLinkSchema = 'appflowy-flutter';\n\nclass AppFlowyCloudDeepLink {\n  AppFlowyCloudDeepLink() {\n    _deepLinkHandlerRegistry = DeepLinkHandlerRegistry.instance\n      ..register(LoginDeepLinkHandler())\n      ..register(PaymentDeepLinkHandler())\n      ..register(InvitationDeepLinkHandler())\n      ..register(ExpireLoginDeepLinkHandler())\n      ..register(OpenAppDeepLinkHandler());\n\n    _deepLinkSubscription = _AppLinkWrapper.instance.listen(\n      (Uri? uri) async {\n        Log.info('onDeepLink: ${uri.toString()}');\n        await _handleUri(uri);\n      },\n      onError: (Object err, StackTrace stackTrace) {\n        Log.error('on DeepLink stream error: ${err.toString()}', stackTrace);\n        _deepLinkSubscription.cancel();\n      },\n    );\n    if (Platform.isWindows) {\n      // register deep link for Windows\n      registerProtocolHandler(appflowyDeepLinkSchema);\n    }\n  }\n\n  ValueNotifier<DeepLinkResult?>? _stateNotifier = ValueNotifier(null);\n\n  Completer<FlowyResult<UserProfilePB, FlowyError>>? _completer;\n\n  set completer(Completer<FlowyResult<UserProfilePB, FlowyError>>? value) {\n    Log.debug('AppFlowyCloudDeepLink: $hashCode completer');\n    _completer = value;\n  }\n\n  late final StreamSubscription<Uri?> _deepLinkSubscription;\n  late final DeepLinkHandlerRegistry _deepLinkHandlerRegistry;\n\n  Future<void> dispose() async {\n    Log.debug('AppFlowyCloudDeepLink: $hashCode dispose');\n    await _deepLinkSubscription.cancel();\n\n    _stateNotifier?.dispose();\n    _stateNotifier = null;\n    completer = null;\n  }\n\n  void registerCompleter(\n    Completer<FlowyResult<UserProfilePB, FlowyError>> completer,\n  ) {\n    this.completer = completer;\n  }\n\n  VoidCallback subscribeDeepLinkLoadingState(\n    ValueChanged<DeepLinkResult> listener,\n  ) {\n    void listenerFn() {\n      if (_stateNotifier?.value != null) {\n        listener(_stateNotifier!.value!);\n      }\n    }\n\n    _stateNotifier?.addListener(listenerFn);\n    return listenerFn;\n  }\n\n  void unsubscribeDeepLinkLoadingState(VoidCallback listener) =>\n      _stateNotifier?.removeListener(listener);\n\n  Future<void> passGotrueTokenResponse(\n    GotrueTokenResponsePB gotrueTokenResponse,\n  ) async {\n    final uri = _buildDeepLinkUri(gotrueTokenResponse);\n    await _handleUri(uri);\n  }\n\n  Future<void> _handleUri(\n    Uri? uri,\n  ) async {\n    _stateNotifier?.value = DeepLinkResult(state: DeepLinkState.none);\n\n    if (uri == null) {\n      Log.error('onDeepLinkError: Unexpected empty deep link callback');\n      _completer?.complete(FlowyResult.failure(AuthError.emptyDeepLink));\n      completer = null;\n      return;\n    }\n\n    await _deepLinkHandlerRegistry.processDeepLink(\n      uri: uri,\n      onStateChange: (handler, state) {\n        // only handle the login deep link\n        if (handler is LoginDeepLinkHandler) {\n          _stateNotifier?.value = DeepLinkResult(state: state);\n        }\n      },\n      onResult: (handler, result) async {\n        if (handler is LoginDeepLinkHandler &&\n            result is FlowyResult<UserProfilePB, FlowyError>) {\n          // If there is no completer, runAppFlowy() will be called.\n          if (_completer == null) {\n            await result.fold(\n              (_) async {\n                await runAppFlowy();\n              },\n              (err) {\n                Log.error(err);\n                final context = AppGlobals.rootNavKey.currentState?.context;\n                if (context != null) {\n                  showToastNotification(\n                    message: err.msg,\n                  );\n                }\n              },\n            );\n          } else {\n            _completer?.complete(result);\n            completer = null;\n          }\n        } else if (handler is ExpireLoginDeepLinkHandler) {\n          result.onFailure(\n            (error) {\n              final context = AppGlobals.rootNavKey.currentState?.context;\n              if (context != null) {\n                showToastNotification(\n                  message: error.msg,\n                  type: ToastificationType.error,\n                );\n              }\n            },\n          );\n        }\n      },\n      onError: (error) {\n        Log.error('onDeepLinkError: Unexpected deep link: $error');\n        if (_completer == null) {\n          final context = AppGlobals.rootNavKey.currentState?.context;\n          if (context != null) {\n            showToastNotification(\n              message: error.msg,\n              type: ToastificationType.error,\n            );\n          }\n        } else {\n          _completer?.complete(FlowyResult.failure(error));\n          completer = null;\n        }\n      },\n    );\n  }\n\n  Uri? _buildDeepLinkUri(GotrueTokenResponsePB gotrueTokenResponse) {\n    final params = <String, String>{};\n\n    if (gotrueTokenResponse.hasAccessToken() &&\n        gotrueTokenResponse.accessToken.isNotEmpty) {\n      params['access_token'] = gotrueTokenResponse.accessToken;\n    }\n\n    if (gotrueTokenResponse.hasExpiresAt()) {\n      params['expires_at'] = gotrueTokenResponse.expiresAt.toString();\n    }\n\n    if (gotrueTokenResponse.hasExpiresIn()) {\n      params['expires_in'] = gotrueTokenResponse.expiresIn.toString();\n    }\n\n    if (gotrueTokenResponse.hasProviderRefreshToken() &&\n        gotrueTokenResponse.providerRefreshToken.isNotEmpty) {\n      params['provider_refresh_token'] =\n          gotrueTokenResponse.providerRefreshToken;\n    }\n\n    if (gotrueTokenResponse.hasProviderAccessToken() &&\n        gotrueTokenResponse.providerAccessToken.isNotEmpty) {\n      params['provider_token'] = gotrueTokenResponse.providerAccessToken;\n    }\n\n    if (gotrueTokenResponse.hasRefreshToken() &&\n        gotrueTokenResponse.refreshToken.isNotEmpty) {\n      params['refresh_token'] = gotrueTokenResponse.refreshToken;\n    }\n\n    if (gotrueTokenResponse.hasTokenType() &&\n        gotrueTokenResponse.tokenType.isNotEmpty) {\n      params['token_type'] = gotrueTokenResponse.tokenType;\n    }\n\n    if (params.isEmpty) {\n      return null;\n    }\n\n    final fragment = params.entries\n        .map(\n          (e) =>\n              '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}',\n        )\n        .join('&');\n\n    return Uri.parse('appflowy-flutter://login-callback#$fragment');\n  }\n}\n\nclass InitAppFlowyCloudTask extends LaunchTask {\n  UserAuthStateListener? _authStateListener;\n  bool isLoggingOut = false;\n\n  @override\n  Future<void> initialize(LaunchContext context) async {\n    await super.initialize(context);\n\n    if (!isAppFlowyCloudEnabled) {\n      return;\n    }\n    _authStateListener = UserAuthStateListener();\n\n    _authStateListener?.start(\n      didSignIn: () {\n        isLoggingOut = false;\n      },\n      onInvalidAuth: (message) async {\n        Log.error(message);\n        if (!isLoggingOut) {\n          await runAppFlowy();\n        }\n      },\n    );\n  }\n\n  @override\n  Future<void> dispose() async {\n    await super.dispose();\n\n    await _authStateListener?.stop();\n    _authStateListener = null;\n  }\n}\n\n// wrapper for AppLinks to support multiple listeners\nclass _AppLinkWrapper {\n  _AppLinkWrapper._() {\n    _appLinkSubscription = _appLinks.uriLinkStream.listen((event) {\n      _streamSubscription.sink.add(event);\n    });\n  }\n\n  static final _AppLinkWrapper instance = _AppLinkWrapper._();\n\n  final AppLinks _appLinks = AppLinks();\n  final _streamSubscription = StreamController<Uri?>.broadcast();\n  late final StreamSubscription<Uri?> _appLinkSubscription;\n\n  StreamSubscription<Uri?> listen(\n    void Function(Uri?) listener, {\n    Function? onError,\n    bool? cancelOnError,\n  }) {\n    return _streamSubscription.stream.listen(\n      listener,\n      onError: onError,\n      cancelOnError: cancelOnError,\n    );\n  }\n\n  void dispose() {\n    _streamSubscription.close();\n    _appLinkSubscription.cancel();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/auto_update_task.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/version_checker/version_checker.dart';\nimport 'package:appflowy/startup/tasks/app_widget.dart';\nimport 'package:appflowy/startup/tasks/device_info_task.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:auto_updater/auto_updater.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../startup.dart';\n\nclass AutoUpdateTask extends LaunchTask {\n  AutoUpdateTask();\n\n  static const _feedUrl =\n      'https://github.com/AppFlowy-IO/AppFlowy/releases/latest/download/appcast-{os}-{arch}.xml';\n  final _listener = _AppFlowyAutoUpdaterListener();\n\n  @override\n  Future<void> initialize(LaunchContext context) async {\n    await super.initialize(context);\n\n    // the auto updater is not supported on mobile\n    if (UniversalPlatform.isMobile) {\n      return;\n    }\n\n    // don't use await here, because the auto updater is not a blocking operation\n    unawaited(_setupAutoUpdater());\n\n    ApplicationInfo.isCriticalUpdateNotifier.addListener(\n      _showCriticalUpdateDialog,\n    );\n  }\n\n  @override\n  Future<void> dispose() async {\n    await super.dispose();\n\n    autoUpdater.removeListener(_listener);\n\n    ApplicationInfo.isCriticalUpdateNotifier.removeListener(\n      _showCriticalUpdateDialog,\n    );\n  }\n\n  // On macOS and windows, we use auto_updater to check for updates.\n  // On linux, we use the version checker to check for updates because the auto_updater is not supported.\n  Future<void> _setupAutoUpdater() async {\n    Log.info(\n      '[AutoUpdate] current version: ${ApplicationInfo.applicationVersion}, current cpu architecture: ${ApplicationInfo.architecture}',\n    );\n\n    // Since the appcast.xml is not supported the arch, we separate the feed url by os and arch.\n    final feedUrl = _feedUrl\n        .replaceAll('{os}', ApplicationInfo.os)\n        .replaceAll('{arch}', ApplicationInfo.architecture);\n\n    // the auto updater is only supported on macOS and windows, so we don't need to check the platform\n    if (UniversalPlatform.isMacOS || UniversalPlatform.isWindows) {\n      autoUpdater.addListener(_listener);\n    }\n\n    Log.info('[AutoUpdate] feed url: $feedUrl');\n\n    versionChecker.setFeedUrl(feedUrl);\n    final item = await versionChecker.checkForUpdateInformation();\n    if (item != null) {\n      ApplicationInfo.latestAppcastItem = item;\n      ApplicationInfo.latestVersionNotifier.value =\n          item.displayVersionString ?? '';\n    }\n  }\n\n  void _showCriticalUpdateDialog() {\n    showCustomConfirmDialog(\n      context: AppGlobals.rootNavKey.currentContext!,\n      title: LocaleKeys.autoUpdate_criticalUpdateTitle.tr(),\n      description: LocaleKeys.autoUpdate_criticalUpdateDescription.tr(\n        namedArgs: {\n          'currentVersion': ApplicationInfo.applicationVersion,\n          'newVersion': ApplicationInfo.latestVersion,\n        },\n      ),\n      builder: (context) => const SizedBox.shrink(),\n      // if the update is critical, dont allow the user to dismiss the dialog\n      barrierDismissible: false,\n      showCloseButton: false,\n      enableKeyboardListener: false,\n      closeOnConfirm: false,\n      confirmLabel: LocaleKeys.autoUpdate_criticalUpdateButton.tr(),\n      onConfirm: () async {\n        await versionChecker.checkForUpdate();\n      },\n    );\n  }\n}\n\nclass _AppFlowyAutoUpdaterListener extends UpdaterListener {\n  @override\n  void onUpdaterBeforeQuitForUpdate(AppcastItem? item) {}\n\n  @override\n  void onUpdaterCheckingForUpdate(Appcast? appcast) {\n    // Due to the reason documented in the following link, the update will not be found if the user has skipped the update.\n    // We have to check the skipped version manually.\n    // https://sparkle-project.org/documentation/api-reference/Classes/SPUUpdater.html#/c:objc(cs)SPUUpdater(im)checkForUpdateInformation\n    final items = appcast?.items;\n    if (items != null) {\n      final String? currentPlatform;\n      if (UniversalPlatform.isMacOS) {\n        currentPlatform = 'macos';\n      } else if (UniversalPlatform.isWindows) {\n        currentPlatform = 'windows';\n      } else {\n        currentPlatform = null;\n      }\n\n      final matchingItem = items.firstWhereOrNull(\n        (item) => item.os == currentPlatform,\n      );\n\n      if (matchingItem != null) {\n        _updateVersionNotifier(matchingItem);\n\n        Log.info(\n          '[AutoUpdate] latest version: ${matchingItem.displayVersionString}',\n        );\n      }\n    }\n  }\n\n  @override\n  void onUpdaterError(UpdaterError? error) {\n    Log.error('[AutoUpdate] On update error: $error');\n  }\n\n  @override\n  void onUpdaterUpdateNotAvailable(UpdaterError? error) {\n    Log.info('[AutoUpdate] Update not available $error');\n  }\n\n  @override\n  void onUpdaterUpdateAvailable(AppcastItem? item) {\n    _updateVersionNotifier(item);\n\n    Log.info('[AutoUpdate] Update available: ${item?.displayVersionString}');\n  }\n\n  @override\n  void onUpdaterUpdateDownloaded(AppcastItem? item) {\n    Log.info('[AutoUpdate] Update downloaded: ${item?.displayVersionString}');\n  }\n\n  @override\n  void onUpdaterUpdateCancelled(AppcastItem? item) {\n    _updateVersionNotifier(item);\n\n    Log.info('[AutoUpdate] Update cancelled: ${item?.displayVersionString}');\n  }\n\n  @override\n  void onUpdaterUpdateInstalled(AppcastItem? item) {\n    _updateVersionNotifier(item);\n\n    Log.info('[AutoUpdate] Update installed: ${item?.displayVersionString}');\n  }\n\n  @override\n  void onUpdaterUpdateSkipped(AppcastItem? item) {\n    _updateVersionNotifier(item);\n\n    Log.info('[AutoUpdate] Update skipped: ${item?.displayVersionString}');\n  }\n\n  void _updateVersionNotifier(AppcastItem? item) {\n    if (item != null) {\n      ApplicationInfo.latestAppcastItem = item;\n      ApplicationInfo.latestVersionNotifier.value =\n          item.displayVersionString ?? '';\n    }\n  }\n}\n\nclass AppFlowyAutoUpdateVersion {\n  AppFlowyAutoUpdateVersion({\n    required this.latestVersion,\n    required this.currentVersion,\n    required this.isForceUpdate,\n  });\n\n  factory AppFlowyAutoUpdateVersion.initial() => AppFlowyAutoUpdateVersion(\n        latestVersion: '0.0.0',\n        currentVersion: '0.0.0',\n        isForceUpdate: false,\n      );\n\n  final String latestVersion;\n  final String currentVersion;\n\n  final bool isForceUpdate;\n\n  bool get isUpdateAvailable => latestVersion != currentVersion;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart",
    "content": "import 'package:appflowy/startup/startup.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:talker/talker.dart';\nimport 'package:talker_bloc_logger/talker_bloc_logger.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass DebugTask extends LaunchTask {\n  DebugTask();\n\n  final Talker talker = Talker();\n\n  @override\n  Future<void> initialize(LaunchContext context) async {\n    await super.initialize(context);\n\n    // hide the keyboard on mobile\n    if (UniversalPlatform.isMobile && kDebugMode) {\n      await SystemChannels.textInput.invokeMethod('TextInput.hide');\n    }\n\n    // log the bloc events\n    if (kDebugMode) {\n      Bloc.observer = TalkerBlocObserver(\n        talker: talker,\n        settings: TalkerBlocLoggerSettings(\n          enabled: false,\n          printEventFullData: false,\n          printStateFullData: false,\n          printChanges: true,\n          printClosings: true,\n          printCreations: true,\n          transitionFilter: (bloc, transition) {\n            // By default, observe all transitions\n            // You can add your own filter here if needed\n            //  when you want to observer a specific bloc\n            return true;\n          },\n        ),\n      );\n\n      // enable rust request tracing\n      // Dispatch.enableTracing = true;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/deeplink/deeplink_handler.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\ntypedef DeepLinkResultHandler = void Function(\n  DeepLinkHandler handler,\n  FlowyResult<dynamic, FlowyError> result,\n);\n\ntypedef DeepLinkStateHandler = void Function(\n  DeepLinkHandler handler,\n  DeepLinkState state,\n);\n\ntypedef DeepLinkErrorHandler = void Function(\n  FlowyError error,\n);\n\nabstract class DeepLinkHandler<T> {\n  /// Checks if this handler should handle the given URI\n  bool canHandle(Uri uri);\n\n  /// Handles the deep link URI\n\n  Future<FlowyResult<T, FlowyError>> handle({\n    required Uri uri,\n    required DeepLinkStateHandler onStateChange,\n  });\n}\n\nclass DeepLinkHandlerRegistry {\n  DeepLinkHandlerRegistry._();\n  static final instance = DeepLinkHandlerRegistry._();\n\n  final List<DeepLinkHandler> _handlers = [];\n\n  /// Register a new DeepLink handler\n  void register(DeepLinkHandler handler) {\n    _handlers.add(handler);\n  }\n\n  Future<void> processDeepLink({\n    required Uri uri,\n    required DeepLinkStateHandler onStateChange,\n    required DeepLinkResultHandler onResult,\n    required DeepLinkErrorHandler onError,\n  }) async {\n    Log.info('Processing DeepLink: ${uri.toString()}');\n\n    bool handled = false;\n\n    for (final handler in _handlers) {\n      if (handler.canHandle(uri)) {\n        Log.info('Handler ${handler.runtimeType} will handle the DeepLink');\n\n        final result = await handler.handle(\n          uri: uri,\n          onStateChange: onStateChange,\n        );\n\n        onResult(handler, result);\n\n        handled = true;\n        break;\n      }\n    }\n\n    if (!handled) {\n      Log.error('No handler found for DeepLink: ${uri.toString()}');\n\n      onError(\n        FlowyError(msg: 'No handler found for DeepLink: ${uri.toString()}'),\n      );\n    }\n  }\n}\n\nclass DeepLinkResult<T> {\n  DeepLinkResult({\n    required this.state,\n    this.result,\n  });\n  final DeepLinkState state;\n  final FlowyResult<T, FlowyError>? result;\n}\n\nenum DeepLinkState {\n  none,\n  loading,\n  finish,\n  error,\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/deeplink/expire_login_deeplink_handler.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/startup/tasks/deeplink/deeplink_handler.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\n/// Expire login deeplink example:\n/// appflowy-flutter:%23error=access_denied&error_code=403&error_description=Email+link+is+invalid+or+has+expired\nclass ExpireLoginDeepLinkHandler extends DeepLinkHandler<void> {\n  @override\n  bool canHandle(Uri uri) {\n    final isExpireLogin = uri.toString().contains('error=access_denied');\n    if (!isExpireLogin) {\n      return false;\n    }\n\n    return true;\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> handle({\n    required Uri uri,\n    required DeepLinkStateHandler onStateChange,\n  }) async {\n    return FlowyResult.failure(\n      FlowyError(\n        msg: 'Magic link is invalid or has expired',\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/deeplink/invitation_deeplink_handler.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/startup/tasks/deeplink/deeplink_handler.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/workspace_notifier.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\n// invitation callback deeplink example:\n// appflowy-flutter://invitation-callback?workspace_id=b2d11122-1fc8-474d-9ef1-ec12fea7ffe8&user_id=275966408418922496\nclass InvitationDeepLinkHandler extends DeepLinkHandler<void> {\n  static const invitationCallbackHost = 'invitation-callback';\n  static const invitationCallbackWorkspaceId = 'workspace_id';\n  static const invitationCallbackEmail = 'email';\n\n  @override\n  bool canHandle(Uri uri) {\n    final isInvitationCallback = uri.host == invitationCallbackHost;\n    if (!isInvitationCallback) {\n      return false;\n    }\n\n    final containsWorkspaceId =\n        uri.queryParameters.containsKey(invitationCallbackWorkspaceId);\n    if (!containsWorkspaceId) {\n      return false;\n    }\n\n    final containsEmail =\n        uri.queryParameters.containsKey(invitationCallbackEmail);\n    if (!containsEmail) {\n      return false;\n    }\n\n    return true;\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> handle({\n    required Uri uri,\n    required DeepLinkStateHandler onStateChange,\n  }) async {\n    final workspaceId = uri.queryParameters[invitationCallbackWorkspaceId];\n    final email = uri.queryParameters[invitationCallbackEmail];\n    if (workspaceId == null) {\n      return FlowyResult.failure(\n        FlowyError(\n          msg: 'Workspace ID is required',\n        ),\n      );\n    }\n\n    if (email == null) {\n      return FlowyResult.failure(\n        FlowyError(\n          msg: 'Email is required',\n        ),\n      );\n    }\n\n    openWorkspaceNotifier.value = WorkspaceNotifyValue(\n      workspaceId: workspaceId,\n      email: email,\n    );\n\n    return FlowyResult.success(null);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/deeplink/login_deeplink_handler.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/startup/tasks/deeplink/deeplink_handler.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/application/auth/device_id.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass LoginDeepLinkHandler extends DeepLinkHandler<UserProfilePB> {\n  @override\n  bool canHandle(Uri uri) {\n    final containsAccessToken = uri.fragment.contains('access_token');\n    if (!containsAccessToken) {\n      return false;\n    }\n\n    return true;\n  }\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> handle({\n    required Uri uri,\n    required DeepLinkStateHandler onStateChange,\n  }) async {\n    final deviceId = await getDeviceId();\n    final payload = OauthSignInPB(\n      authType: AuthTypePB.Server,\n      map: {\n        AuthServiceMapKeys.signInURL: uri.toString(),\n        AuthServiceMapKeys.deviceId: deviceId,\n      },\n    );\n\n    onStateChange(this, DeepLinkState.loading);\n\n    final result = await UserEventOauthSignIn(payload).send();\n\n    onStateChange(this, DeepLinkState.finish);\n\n    return result;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/deeplink/open_app_deeplink_handler.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/startup/tasks/deeplink/deeplink_handler.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass OpenAppDeepLinkHandler extends DeepLinkHandler<void> {\n  @override\n  bool canHandle(Uri uri) {\n    return uri.toString() == 'appflowy-flutter://';\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> handle({\n    required Uri uri,\n    required DeepLinkStateHandler onStateChange,\n  }) async {\n    return FlowyResult.success(null);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/deeplink/payment_deeplink_handler.dart",
    "content": "import 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/startup/tasks/deeplink/deeplink_handler.dart';\nimport 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass PaymentDeepLinkHandler extends DeepLinkHandler {\n  @override\n  bool canHandle(Uri uri) {\n    return uri.host == 'payment-success';\n  }\n\n  @override\n  Future<FlowyResult<dynamic, FlowyError>> handle({\n    required Uri uri,\n    required DeepLinkStateHandler onStateChange,\n  }) async {\n    Log.debug(\"Payment success deep link: ${uri.toString()}\");\n    final plan = uri.queryParameters['plan'];\n    getIt<SubscriptionSuccessListenable>().onPaymentSuccess(plan);\n    return FlowyResult.success(null);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy_backend/log.dart';\nimport 'package:auto_updater/auto_updater.dart';\nimport 'package:device_info_plus/device_info_plus.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:package_info_plus/package_info_plus.dart';\nimport 'package:version/version.dart';\n\nimport '../startup.dart';\n\nclass ApplicationInfo {\n  static int androidSDKVersion = -1;\n  static String applicationVersion = '';\n  static String buildNumber = '';\n  static String deviceId = '';\n  static String architecture = '';\n  static String os = '';\n\n  // macOS major version\n  static int? macOSMajorVersion;\n  static int? macOSMinorVersion;\n\n  // latest version\n  static ValueNotifier<String> latestVersionNotifier = ValueNotifier('');\n  // the version number is like 0.9.0\n  static String get latestVersion => latestVersionNotifier.value;\n\n  // If the latest version is greater than the current version, it means there is an update available\n  static bool get isUpdateAvailable {\n    try {\n      if (latestVersion.isEmpty) {\n        return false;\n      }\n      return Version.parse(latestVersion) > Version.parse(applicationVersion);\n    } catch (e) {\n      return false;\n    }\n  }\n\n  // the latest appcast item\n  static AppcastItem? _latestAppcastItem;\n  static AppcastItem? get latestAppcastItem => _latestAppcastItem;\n  static set latestAppcastItem(AppcastItem? value) {\n    _latestAppcastItem = value;\n\n    isCriticalUpdateNotifier.value = value?.criticalUpdate == true;\n  }\n\n  // is critical update\n  static ValueNotifier<bool> isCriticalUpdateNotifier = ValueNotifier(false);\n  static bool get isCriticalUpdate => isCriticalUpdateNotifier.value;\n}\n\nclass ApplicationInfoTask extends LaunchTask {\n  const ApplicationInfoTask();\n\n  @override\n  Future<void> initialize(LaunchContext context) async {\n    await super.initialize(context);\n    final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();\n    final PackageInfo packageInfo = await PackageInfo.fromPlatform();\n\n    if (Platform.isMacOS) {\n      final macInfo = await deviceInfoPlugin.macOsInfo;\n      ApplicationInfo.macOSMajorVersion = macInfo.majorVersion;\n      ApplicationInfo.macOSMinorVersion = macInfo.minorVersion;\n    }\n\n    if (Platform.isAndroid) {\n      final androidInfo = await deviceInfoPlugin.androidInfo;\n      ApplicationInfo.androidSDKVersion = androidInfo.version.sdkInt;\n    }\n\n    ApplicationInfo.applicationVersion = packageInfo.version;\n    ApplicationInfo.buildNumber = packageInfo.buildNumber;\n\n    String? deviceId;\n    String? architecture;\n    String? os;\n    try {\n      if (Platform.isAndroid) {\n        final AndroidDeviceInfo androidInfo =\n            await deviceInfoPlugin.androidInfo;\n        deviceId = androidInfo.device;\n        architecture = androidInfo.supportedAbis.firstOrNull;\n        os = 'android';\n      } else if (Platform.isIOS) {\n        final IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo;\n        deviceId = iosInfo.identifierForVendor;\n        architecture = iosInfo.utsname.machine;\n        os = 'ios';\n      } else if (Platform.isMacOS) {\n        final MacOsDeviceInfo macInfo = await deviceInfoPlugin.macOsInfo;\n        deviceId = macInfo.systemGUID;\n        architecture = macInfo.arch;\n        os = 'macos';\n      } else if (Platform.isWindows) {\n        final WindowsDeviceInfo windowsInfo =\n            await deviceInfoPlugin.windowsInfo;\n        deviceId = windowsInfo.deviceId;\n        // we only support x86_64 on Windows\n        architecture = 'x86_64';\n        os = 'windows';\n      } else if (Platform.isLinux) {\n        final LinuxDeviceInfo linuxInfo = await deviceInfoPlugin.linuxInfo;\n        deviceId = linuxInfo.machineId;\n        // we only support x86_64 on Linux\n        architecture = 'x86_64';\n        os = 'linux';\n      } else {\n        deviceId = null;\n        architecture = null;\n        os = null;\n      }\n    } catch (e) {\n      Log.error('Failed to get platform version, $e');\n    }\n\n    ApplicationInfo.deviceId = deviceId ?? '';\n    ApplicationInfo.architecture = architecture ?? '';\n    ApplicationInfo.os = os ?? '';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/feature_flag_task.dart",
    "content": "import 'package:appflowy/shared/feature_flags.dart';\nimport 'package:flutter/foundation.dart';\n\nimport '../startup.dart';\n\nclass FeatureFlagTask extends LaunchTask {\n  const FeatureFlagTask();\n\n  @override\n  Future<void> initialize(LaunchContext context) async {\n    await super.initialize(context);\n\n    // the hotkey manager is not supported on mobile\n    if (!kDebugMode) {\n      return;\n    }\n\n    await FeatureFlag.initialize();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/file_storage_task.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi';\nimport 'dart:isolate';\n\nimport 'package:flutter/foundation.dart';\n\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-storage/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:fixnum/fixnum.dart';\n\nimport '../startup.dart';\n\nclass FileStorageTask extends LaunchTask {\n  const FileStorageTask();\n\n  @override\n  Future<void> initialize(LaunchContext context) async {\n    await super.initialize(context);\n\n    context.getIt.registerSingleton(\n      FileStorageService(),\n      dispose: (service) async => service.dispose(),\n    );\n  }\n}\n\nclass FileStorageService {\n  FileStorageService() {\n    _port.handler = _controller.add;\n    _subscription = _controller.stream.listen(\n      (event) {\n        final fileProgress = FileProgress.fromJsonString(event);\n        if (fileProgress != null) {\n          Log.debug(\n            \"FileStorageService upload file: ${fileProgress.fileUrl} ${fileProgress.progress}\",\n          );\n          final notifier = _notifierList[fileProgress.fileUrl];\n          if (notifier != null) {\n            notifier.value = fileProgress;\n          }\n        }\n      },\n    );\n\n    if (!integrationMode().isTest) {\n      final payload = RegisterStreamPB()\n        ..port = Int64(_port.sendPort.nativePort);\n      FileStorageEventRegisterStream(payload).send();\n    }\n  }\n\n  final Map<String, AutoRemoveNotifier<FileProgress>> _notifierList = {};\n  final RawReceivePort _port = RawReceivePort();\n  final StreamController<String> _controller = StreamController.broadcast();\n  late StreamSubscription<String> _subscription;\n\n  AutoRemoveNotifier<FileProgress> onFileProgress({required String fileUrl}) {\n    _notifierList.remove(fileUrl)?.dispose();\n\n    final notifier = AutoRemoveNotifier<FileProgress>(\n      FileProgress(fileUrl: fileUrl, progress: 0),\n      notifierList: _notifierList,\n      fileId: fileUrl,\n    );\n    _notifierList[fileUrl] = notifier;\n\n    // trigger the initial file state\n    getFileState(fileUrl);\n\n    return notifier;\n  }\n\n  Future<FlowyResult<FileStatePB, FlowyError>> getFileState(String url) {\n    final payload = QueryFilePB()..url = url;\n    return FileStorageEventQueryFile(payload).send();\n  }\n\n  Future<void> dispose() async {\n    // dispose all notifiers\n    for (final notifier in _notifierList.values) {\n      notifier.dispose();\n    }\n\n    await _controller.close();\n    await _subscription.cancel();\n    _port.close();\n  }\n}\n\nclass FileProgress {\n  FileProgress({\n    required this.fileUrl,\n    required this.progress,\n    this.error,\n  });\n\n  static FileProgress? fromJson(Map<String, dynamic>? json) {\n    if (json == null) {\n      return null;\n    }\n\n    try {\n      if (json.containsKey('file_url') && json.containsKey('progress')) {\n        return FileProgress(\n          fileUrl: json['file_url'] as String,\n          progress: (json['progress'] as num).toDouble(),\n          error: json['error'] as String?,\n        );\n      }\n    } catch (e) {\n      Log.error('unable to parse file progress: $e');\n    }\n    return null;\n  }\n\n  // Method to parse a JSON string and return a FileProgress object or null\n  static FileProgress? fromJsonString(String jsonString) {\n    try {\n      final Map<String, dynamic> jsonMap = jsonDecode(jsonString);\n      return FileProgress.fromJson(jsonMap);\n    } catch (e) {\n      return null;\n    }\n  }\n\n  final double progress;\n  final String fileUrl;\n  final String? error;\n}\n\nclass AutoRemoveNotifier<T> extends ValueNotifier<T> {\n  AutoRemoveNotifier(\n    super.value, {\n    required this.fileId,\n    required Map<String, AutoRemoveNotifier<FileProgress>> notifierList,\n  }) : _notifierList = notifierList;\n\n  final String fileId;\n  final Map<String, AutoRemoveNotifier<FileProgress>> _notifierList;\n\n  @override\n  void dispose() {\n    _notifierList.remove(fileId);\n    super.dispose();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/mobile/presentation/chat/mobile_chat_screen.dart';\nimport 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart';\nimport 'package:appflowy/mobile/presentation/database/card/card.dart';\nimport 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart';\nimport 'package:appflowy/mobile/presentation/database/field/mobile_create_field_screen.dart';\nimport 'package:appflowy/mobile/presentation/database/field/mobile_edit_field_screen.dart';\nimport 'package:appflowy/mobile/presentation/database/mobile_calendar_events_screen.dart';\nimport 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';\nimport 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';\nimport 'package:appflowy/mobile/presentation/favorite/mobile_favorite_page.dart';\nimport 'package:appflowy/mobile/presentation/notifications/mobile_notifications_multiple_select_page.dart';\nimport 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';\nimport 'package:appflowy/mobile/presentation/presentation.dart';\nimport 'package:appflowy/mobile/presentation/search/mobile_search_page.dart';\nimport 'package:appflowy/mobile/presentation/setting/cloud/appflowy_cloud_page.dart';\nimport 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart';\nimport 'package:appflowy/mobile/presentation/setting/language/language_picker_screen.dart';\nimport 'package:appflowy/mobile/presentation/setting/launch_settings_page.dart';\nimport 'package:appflowy/mobile/presentation/setting/workspace/add_members_screen.dart';\nimport 'package:appflowy/mobile/presentation/setting/workspace/invite_members_screen.dart';\nimport 'package:appflowy/plugins/base/color/color_picker_screen.dart';\nimport 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_language_screen.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_block_settings_screen.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/startup/tasks/app_widget.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/presentation/presentation.dart';\nimport 'package:appflowy/workspace/presentation/home/desktop_home_screen.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flowy_infra/time/duration.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:sheet/route.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../../shared/icon_emoji_picker/tab.dart';\nimport 'af_navigator_observer.dart';\n\nGoRouter generateRouter(Widget child) {\n  return GoRouter(\n    navigatorKey: AppGlobals.rootNavKey,\n    observers: [getIt.get<AFNavigatorObserver>()],\n    initialLocation: '/',\n    routes: [\n      // Root route is SplashScreen.\n      // It needs LaunchConfiguration as a parameter, so we get it from ApplicationWidget's child.\n      _rootRoute(child),\n      // Routes in both desktop and mobile\n      _signInScreenRoute(),\n      _skipLogInScreenRoute(),\n      _workspaceErrorScreenRoute(),\n      // Desktop only\n      if (UniversalPlatform.isDesktop) _desktopHomeScreenRoute(),\n      // Mobile only\n      if (UniversalPlatform.isMobile) ...[\n        // settings\n        _mobileHomeSettingPageRoute(),\n        _mobileCloudSettingAppFlowyCloudPageRoute(),\n        _mobileLaunchSettingsPageRoute(),\n        _mobileFeatureFlagPageRoute(),\n\n        // view page\n        _mobileEditorScreenRoute(),\n        _mobileGridScreenRoute(),\n        _mobileBoardScreenRoute(),\n        _mobileCalendarScreenRoute(),\n        _mobileChatScreenRoute(),\n        // card detail page\n        _mobileCardDetailScreenRoute(),\n        _mobileDateCellEditScreenRoute(),\n        _mobileNewPropertyPageRoute(),\n        _mobileEditPropertyPageRoute(),\n\n        // home\n        // MobileHomeSettingPage is outside the bottom navigation bar, thus it is not in the StatefulShellRoute.\n        _mobileHomeScreenWithNavigationBarRoute(),\n\n        // trash\n        _mobileHomeTrashPageRoute(),\n\n        // emoji picker\n        _mobileEmojiPickerPageRoute(),\n        _mobileImagePickerPageRoute(),\n\n        // color picker\n        _mobileColorPickerPageRoute(),\n\n        // code language picker\n        _mobileCodeLanguagePickerPageRoute(),\n        _mobileLanguagePickerPageRoute(),\n        _mobileFontPickerPageRoute(),\n\n        // calendar related\n        _mobileCalendarEventsPageRoute(),\n\n        _mobileBlockSettingsPageRoute(),\n\n        // notifications\n        _mobileNotificationMultiSelectPageRoute(),\n\n        // invite members\n        _mobileInviteMembersPageRoute(),\n        _mobileAddMembersPageRoute(),\n      ],\n\n      // Desktop and Mobile\n      GoRoute(\n        path: WorkspaceStartScreen.routeName,\n        pageBuilder: (context, state) {\n          final args = state.extra as Map<String, dynamic>;\n          return CustomTransitionPage(\n            child: WorkspaceStartScreen(\n              userProfile: args[WorkspaceStartScreen.argUserProfile],\n            ),\n            transitionsBuilder: _buildFadeTransition,\n            transitionDuration: _slowDuration,\n          );\n        },\n      ),\n    ],\n  );\n}\n\n/// We use StatefulShellRoute to create a StatefulNavigationShell(ScaffoldWithNavBar) to access to multiple pages, and each page retains its own state.\nStatefulShellRoute _mobileHomeScreenWithNavigationBarRoute() {\n  return StatefulShellRoute.indexedStack(\n    builder: (\n      BuildContext context,\n      GoRouterState state,\n      StatefulNavigationShell navigationShell,\n    ) {\n      // Return the widget that implements the custom shell (in this case\n      // using a BottomNavigationBar). The StatefulNavigationShell is passed\n      // to be able access the state of the shell and to navigate to other\n      // branches in a stateful way.\n      return MobileBottomNavigationBar(navigationShell: navigationShell);\n    },\n    pageBuilder: (context, state, navigationShell) {\n      String name = MobileHomeScreen.routeName;\n      switch (navigationShell.currentIndex) {\n        case 0:\n          name = MobileHomeScreen.routeName;\n          break;\n        case 1:\n          name = MobileSearchScreen.routeName;\n          break;\n        case 2:\n          name = MobileFavoriteScreen.routeName;\n          break;\n        case 3:\n          name = MobileNotificationsScreenV2.routeName;\n          break;\n      }\n      return MaterialExtendedPage(\n        child: MobileBottomNavigationBar(navigationShell: navigationShell),\n        name: name,\n      );\n    },\n    branches: <StatefulShellBranch>[\n      StatefulShellBranch(\n        routes: <RouteBase>[\n          GoRoute(\n            path: MobileHomeScreen.routeName,\n            pageBuilder: (context, state) => MaterialExtendedPage(\n              child: const MobileHomeScreen(),\n              name: MobileHomeScreen.routeName,\n            ),\n          ),\n        ],\n      ),\n      StatefulShellBranch(\n        routes: <RouteBase>[\n          GoRoute(\n            name: MobileSearchScreen.routeName,\n            path: MobileSearchScreen.routeName,\n            pageBuilder: (context, state) => MaterialExtendedPage(\n              child: const MobileSearchScreen(),\n              name: MobileSearchScreen.routeName,\n            ),\n          ),\n        ],\n      ),\n      StatefulShellBranch(\n        routes: <RouteBase>[\n          GoRoute(\n            name: MobileFavoriteScreen.routeName,\n            path: MobileFavoriteScreen.routeName,\n            pageBuilder: (context, state) => MaterialExtendedPage(\n              child: const MobileFavoriteScreen(),\n              name: MobileFavoriteScreen.routeName,\n            ),\n          ),\n        ],\n      ),\n      StatefulShellBranch(\n        routes: <RouteBase>[\n          GoRoute(\n            name: MobileNotificationsScreenV2.routeName,\n            path: MobileNotificationsScreenV2.routeName,\n            pageBuilder: (context, state) => MaterialExtendedPage(\n              child: const MobileNotificationsScreenV2(),\n              name: MobileNotificationsScreenV2.routeName,\n            ),\n          ),\n        ],\n      ),\n    ],\n  );\n}\n\nGoRoute _mobileHomeSettingPageRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: MobileHomeSettingPage.routeName,\n    pageBuilder: (context, state) {\n      return const MaterialExtendedPage(\n        child: MobileHomeSettingPage(),\n        name: MobileHomeSettingPage.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileNotificationMultiSelectPageRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: MobileNotificationsMultiSelectScreen.routeName,\n    pageBuilder: (context, state) {\n      return const MaterialExtendedPage(\n        child: MobileNotificationsMultiSelectScreen(),\n        name: MobileNotificationsMultiSelectScreen.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileInviteMembersPageRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: InviteMembersScreen.routeName,\n    pageBuilder: (context, state) {\n      return const MaterialExtendedPage(\n        child: InviteMembersScreen(),\n        name: InviteMembersScreen.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileAddMembersPageRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: AddMembersScreen.routeName,\n    pageBuilder: (context, state) {\n      return const MaterialExtendedPage(\n        child: AddMembersScreen(),\n        name: AddMembersScreen.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileCloudSettingAppFlowyCloudPageRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: AppFlowyCloudPage.routeName,\n    pageBuilder: (context, state) {\n      return const MaterialExtendedPage(\n        child: AppFlowyCloudPage(),\n        name: AppFlowyCloudPage.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileLaunchSettingsPageRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: MobileLaunchSettingsPage.routeName,\n    pageBuilder: (context, state) {\n      return const MaterialExtendedPage(\n        child: MobileLaunchSettingsPage(),\n        name: MobileLaunchSettingsPage.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileFeatureFlagPageRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: FeatureFlagScreen.routeName,\n    pageBuilder: (context, state) {\n      return const MaterialExtendedPage(\n        child: FeatureFlagScreen(),\n        name: FeatureFlagScreen.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileHomeTrashPageRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: MobileHomeTrashPage.routeName,\n    pageBuilder: (context, state) {\n      return const MaterialExtendedPage(\n        child: MobileHomeTrashPage(),\n        name: MobileHomeTrashPage.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileBlockSettingsPageRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: MobileBlockSettingsScreen.routeName,\n    pageBuilder: (context, state) {\n      final actionsString =\n          state.uri.queryParameters[MobileBlockSettingsScreen.supportedActions];\n      final actions = actionsString\n          ?.split(',')\n          .map(MobileBlockActionType.fromActionString)\n          .toList();\n      return MaterialExtendedPage(\n        child: MobileBlockSettingsScreen(\n          actions: actions ?? MobileBlockActionType.standard,\n        ),\n        name: MobileBlockSettingsScreen.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileEmojiPickerPageRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: MobileEmojiPickerScreen.routeName,\n    pageBuilder: (context, state) {\n      final title =\n          state.uri.queryParameters[MobileEmojiPickerScreen.pageTitle];\n      final selectTabs =\n          state.uri.queryParameters[MobileEmojiPickerScreen.selectTabs] ?? '';\n      final selectedType = state\n          .uri.queryParameters[MobileEmojiPickerScreen.iconSelectedType]\n          ?.toPickerTabType();\n      final documentId =\n          state.uri.queryParameters[MobileEmojiPickerScreen.uploadDocumentId];\n      List<PickerTabType> tabs = [];\n      try {\n        tabs = selectTabs\n            .split('-')\n            .map((e) => PickerTabType.values.byName(e))\n            .toList();\n      } on ArgumentError catch (e) {\n        Log.error('convert selectTabs to pickerTab error', e);\n      }\n      return MaterialExtendedPage(\n        child: tabs.isEmpty\n            ? MobileEmojiPickerScreen(\n                title: title,\n                selectedType: selectedType,\n                documentId: documentId,\n              )\n            : MobileEmojiPickerScreen(\n                title: title,\n                selectedType: selectedType,\n                tabs: tabs,\n                documentId: documentId,\n              ),\n        name: MobileEmojiPickerScreen.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileColorPickerPageRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: MobileColorPickerScreen.routeName,\n    pageBuilder: (context, state) {\n      final title =\n          state.uri.queryParameters[MobileColorPickerScreen.pageTitle] ?? '';\n      return MaterialExtendedPage(\n        child: MobileColorPickerScreen(title: title),\n        name: MobileColorPickerScreen.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileImagePickerPageRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: MobileImagePickerScreen.routeName,\n    pageBuilder: (context, state) {\n      return const MaterialExtendedPage(\n        child: MobileImagePickerScreen(),\n        name: MobileImagePickerScreen.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileCodeLanguagePickerPageRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: MobileCodeLanguagePickerScreen.routeName,\n    pageBuilder: (context, state) {\n      return const MaterialExtendedPage(\n        child: MobileCodeLanguagePickerScreen(),\n        name: MobileCodeLanguagePickerScreen.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileLanguagePickerPageRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: LanguagePickerScreen.routeName,\n    pageBuilder: (context, state) {\n      return const MaterialExtendedPage(\n        child: LanguagePickerScreen(),\n        name: LanguagePickerScreen.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileFontPickerPageRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: FontPickerScreen.routeName,\n    pageBuilder: (context, state) {\n      return const MaterialExtendedPage(\n        child: FontPickerScreen(),\n        name: FontPickerScreen.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileNewPropertyPageRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: MobileNewPropertyScreen.routeName,\n    pageBuilder: (context, state) {\n      final viewId = state\n          .uri.queryParameters[MobileNewPropertyScreen.argViewId] as String;\n      final fieldTypeId =\n          state.uri.queryParameters[MobileNewPropertyScreen.argFieldTypeId] ??\n              FieldType.RichText.value.toString();\n      final value = int.parse(fieldTypeId);\n      return MaterialExtendedPage(\n        fullscreenDialog: true,\n        child: MobileNewPropertyScreen(\n          viewId: viewId,\n          fieldType: FieldType.valueOf(value),\n        ),\n        name: MobileNewPropertyScreen.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileEditPropertyPageRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: MobileEditPropertyScreen.routeName,\n    pageBuilder: (context, state) {\n      final args = state.extra as Map<String, dynamic>;\n      return MaterialExtendedPage(\n        fullscreenDialog: true,\n        child: MobileEditPropertyScreen(\n          viewId: args[MobileEditPropertyScreen.argViewId],\n          field: args[MobileEditPropertyScreen.argField],\n        ),\n        name: MobileEditPropertyScreen.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileCalendarEventsPageRoute() {\n  return GoRoute(\n    path: MobileCalendarEventsScreen.routeName,\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    pageBuilder: (context, state) {\n      final args = state.extra as Map<String, dynamic>;\n\n      return MaterialExtendedPage(\n        child: MobileCalendarEventsScreen(\n          calendarBloc: args[MobileCalendarEventsScreen.calendarBlocKey],\n          date: args[MobileCalendarEventsScreen.calendarDateKey],\n          events: args[MobileCalendarEventsScreen.calendarEventsKey],\n          rowCache: args[MobileCalendarEventsScreen.calendarRowCacheKey],\n          viewId: args[MobileCalendarEventsScreen.calendarViewIdKey],\n        ),\n        name: MobileCalendarEventsScreen.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _desktopHomeScreenRoute() {\n  return GoRoute(\n    path: DesktopHomeScreen.routeName,\n    pageBuilder: (context, state) {\n      return CustomTransitionPage(\n        child: const DesktopHomeScreen(),\n        transitionsBuilder: _buildFadeTransition,\n        transitionDuration: _slowDuration,\n      );\n    },\n  );\n}\n\nGoRoute _workspaceErrorScreenRoute() {\n  return GoRoute(\n    path: WorkspaceErrorScreen.routeName,\n    pageBuilder: (context, state) {\n      final args = state.extra as Map<String, dynamic>;\n      return CustomTransitionPage(\n        child: WorkspaceErrorScreen(\n          error: args[WorkspaceErrorScreen.argError],\n          userFolder: args[WorkspaceErrorScreen.argUserFolder],\n        ),\n        transitionsBuilder: _buildFadeTransition,\n        transitionDuration: _slowDuration,\n      );\n    },\n  );\n}\n\nGoRoute _skipLogInScreenRoute() {\n  return GoRoute(\n    path: SkipLogInScreen.routeName,\n    pageBuilder: (context, state) {\n      return CustomTransitionPage(\n        child: const SkipLogInScreen(),\n        transitionsBuilder: _buildFadeTransition,\n        transitionDuration: _slowDuration,\n      );\n    },\n  );\n}\n\nGoRoute _signInScreenRoute() {\n  return GoRoute(\n    path: SignInScreen.routeName,\n    pageBuilder: (context, state) {\n      return CustomTransitionPage(\n        child: const SignInScreen(),\n        transitionsBuilder: _buildFadeTransition,\n        transitionDuration: _slowDuration,\n      );\n    },\n  );\n}\n\nGoRoute _mobileEditorScreenRoute() {\n  return GoRoute(\n    path: MobileDocumentScreen.routeName,\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    pageBuilder: (context, state) {\n      final id = state.uri.queryParameters[MobileDocumentScreen.viewId]!;\n      final title = state.uri.queryParameters[MobileDocumentScreen.viewTitle];\n      final showMoreButton = bool.tryParse(\n        state.uri.queryParameters[MobileDocumentScreen.viewShowMoreButton] ??\n            'true',\n      );\n      final fixedTitle =\n          state.uri.queryParameters[MobileDocumentScreen.viewFixedTitle];\n      final blockId =\n          state.uri.queryParameters[MobileDocumentScreen.viewBlockId];\n\n      final selectTabs =\n          state.uri.queryParameters[MobileDocumentScreen.viewSelectTabs] ?? '';\n      List<PickerTabType> tabs = [];\n      try {\n        tabs = selectTabs\n            .split('-')\n            .map((e) => PickerTabType.values.byName(e))\n            .toList();\n      } on ArgumentError catch (e) {\n        Log.error('convert selectTabs to pickerTab error', e);\n      }\n      if (tabs.isEmpty) {\n        tabs = const [PickerTabType.emoji, PickerTabType.icon];\n      }\n\n      return MaterialExtendedPage(\n        child: MobileDocumentScreen(\n          id: id,\n          title: title,\n          showMoreButton: showMoreButton ?? true,\n          fixedTitle: fixedTitle,\n          blockId: blockId,\n          tabs: tabs,\n        ),\n        name: MobileDocumentScreen.routeName,\n      );\n    },\n  );\n}\n\nGoRoute _mobileChatScreenRoute() {\n  return GoRoute(\n    path: MobileChatScreen.routeName,\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    pageBuilder: (context, state) {\n      final id = state.uri.queryParameters[MobileChatScreen.viewId]!;\n      final title = state.uri.queryParameters[MobileChatScreen.viewTitle];\n\n      return MaterialExtendedPage(\n        child: MobileChatScreen(id: id, title: title),\n      );\n    },\n  );\n}\n\nGoRoute _mobileGridScreenRoute() {\n  return GoRoute(\n    path: MobileGridScreen.routeName,\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    pageBuilder: (context, state) {\n      final id = state.uri.queryParameters[MobileGridScreen.viewId]!;\n      final title = state.uri.queryParameters[MobileGridScreen.viewTitle];\n      final arguments = state.uri.queryParameters[MobileGridScreen.viewArgs];\n\n      return MaterialExtendedPage(\n        child: MobileGridScreen(\n          id: id,\n          title: title,\n          arguments: arguments != null ? jsonDecode(arguments) : null,\n        ),\n      );\n    },\n  );\n}\n\nGoRoute _mobileBoardScreenRoute() {\n  return GoRoute(\n    path: MobileBoardScreen.routeName,\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    pageBuilder: (context, state) {\n      final id = state.uri.queryParameters[MobileBoardScreen.viewId]!;\n      final title = state.uri.queryParameters[MobileBoardScreen.viewTitle];\n      return MaterialExtendedPage(\n        child: MobileBoardScreen(\n          id: id,\n          title: title,\n        ),\n      );\n    },\n  );\n}\n\nGoRoute _mobileCalendarScreenRoute() {\n  return GoRoute(\n    path: MobileCalendarScreen.routeName,\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    pageBuilder: (context, state) {\n      final id = state.uri.queryParameters[MobileCalendarScreen.viewId]!;\n      final title = state.uri.queryParameters[MobileCalendarScreen.viewTitle]!;\n      return MaterialExtendedPage(\n        child: MobileCalendarScreen(\n          id: id,\n          title: title,\n        ),\n      );\n    },\n  );\n}\n\nGoRoute _mobileCardDetailScreenRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: MobileRowDetailPage.routeName,\n    pageBuilder: (context, state) {\n      var extra = state.extra as Map<String, dynamic>?;\n\n      if (kDebugMode && extra == null) {\n        extra = _dynamicValues;\n      }\n\n      if (extra == null) {\n        return const MaterialExtendedPage(\n          child: SizedBox.shrink(),\n        );\n      }\n\n      final databaseController =\n          extra[MobileRowDetailPage.argDatabaseController];\n      final rowId = extra[MobileRowDetailPage.argRowId]!;\n\n      if (kDebugMode) {\n        _dynamicValues = extra;\n      }\n\n      return MaterialExtendedPage(\n        child: MobileRowDetailPage(\n          databaseController: databaseController,\n          rowId: rowId,\n        ),\n      );\n    },\n  );\n}\n\nGoRoute _mobileDateCellEditScreenRoute() {\n  return GoRoute(\n    parentNavigatorKey: AppGlobals.rootNavKey,\n    path: MobileDateCellEditScreen.routeName,\n    pageBuilder: (context, state) {\n      final args = state.extra as Map<String, dynamic>;\n      final controller = args[MobileDateCellEditScreen.dateCellController];\n      final fullScreen = args[MobileDateCellEditScreen.fullScreen];\n      return CustomTransitionPage(\n        transitionsBuilder: (_, __, ___, child) => child,\n        fullscreenDialog: true,\n        opaque: false,\n        barrierDismissible: true,\n        barrierColor: Theme.of(context).bottomSheetTheme.modalBarrierColor,\n        child: MobileDateCellEditScreen(\n          controller: controller,\n          showAsFullScreen: fullScreen ?? true,\n        ),\n      );\n    },\n  );\n}\n\nGoRoute _rootRoute(Widget child) {\n  return GoRoute(\n    path: '/',\n    redirect: (context, state) async {\n      // 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.\n      final userResponse = await getIt<AuthService>().getUser();\n      final routeName = userResponse.fold(\n        (user) => DesktopHomeScreen.routeName,\n        (error) => null,\n      );\n      if (routeName != null && !UniversalPlatform.isMobile) return routeName;\n\n      return null;\n    },\n    // Root route is SplashScreen.\n    // It needs LaunchConfiguration as a parameter, so we get it from ApplicationWidget's child.\n    pageBuilder: (context, state) => MaterialExtendedPage(\n      child: child,\n    ),\n  );\n}\n\nWidget _buildFadeTransition(\n  BuildContext context,\n  Animation<double> animation,\n  Animation<double> secondaryAnimation,\n  Widget child,\n) =>\n    FadeTransition(opacity: animation, child: child);\n\nDuration _slowDuration = Duration(\n  milliseconds: RouteDurations.slow.inMilliseconds.round(),\n);\n\n// ONLY USE IN DEBUG MODE\n// this is a workaround for the issue of GoRouter not supporting extra with complex types\n// https://github.com/flutter/flutter/issues/137248\nMap<String, dynamic> _dynamicValues = {};\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/hot_key.dart",
    "content": "import 'package:hotkey_manager/hotkey_manager.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../startup.dart';\n\nclass HotKeyTask extends LaunchTask {\n  const HotKeyTask();\n\n  @override\n  Future<void> initialize(LaunchContext context) async {\n    await super.initialize(context);\n\n    // the hotkey manager is not supported on mobile\n    if (UniversalPlatform.isMobile) {\n      return;\n    }\n    await hotKeyManager.unregisterAll();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/load_plugin.dart",
    "content": "import 'package:appflowy/plugins/ai_chat/chat.dart';\nimport 'package:appflowy/plugins/database/calendar/calendar.dart';\nimport 'package:appflowy/plugins/database/board/board.dart';\nimport 'package:appflowy/plugins/database/grid/grid.dart';\nimport 'package:appflowy/plugins/database_document/database_document_plugin.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/plugins/blank/blank.dart';\nimport 'package:appflowy/plugins/document/document.dart';\nimport 'package:appflowy/plugins/trash/trash.dart';\n\nclass PluginLoadTask extends LaunchTask {\n  const PluginLoadTask();\n\n  @override\n  LaunchTaskType get type => LaunchTaskType.dataProcessing;\n\n  @override\n  Future<void> initialize(LaunchContext context) async {\n    await super.initialize(context);\n\n    registerPlugin(builder: BlankPluginBuilder(), config: BlankPluginConfig());\n    registerPlugin(builder: TrashPluginBuilder(), config: TrashPluginConfig());\n    registerPlugin(builder: DocumentPluginBuilder());\n    registerPlugin(builder: GridPluginBuilder(), config: GridPluginConfig());\n    registerPlugin(builder: BoardPluginBuilder(), config: BoardPluginConfig());\n    registerPlugin(\n      builder: CalendarPluginBuilder(),\n      config: CalendarPluginConfig(),\n    );\n    registerPlugin(\n      builder: DatabaseDocumentPluginBuilder(),\n      config: DatabaseDocumentPluginConfig(),\n    );\n    registerPlugin(\n      builder: DatabaseDocumentPluginBuilder(),\n      config: DatabaseDocumentPluginConfig(),\n    );\n    registerPlugin(\n      builder: AIChatPluginBuilder(),\n      config: AIChatPluginConfig(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/localization.dart",
    "content": "import 'package:easy_localization/easy_localization.dart';\n\nimport '../startup.dart';\n\nclass InitLocalizationTask extends LaunchTask {\n  const InitLocalizationTask();\n\n  @override\n  Future<void> initialize(LaunchContext context) async {\n    await super.initialize(context);\n\n    await EasyLocalization.ensureInitialized();\n    EasyLocalization.logger.enableBuildModes = [];\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/memory_leak_detector.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/foundation.dart';\nimport 'package:leak_tracker/leak_tracker.dart';\n\nimport '../startup.dart';\n\nbool enableMemoryLeakDetect = false;\nbool dumpMemoryLeakPerSecond = false;\n\nvoid dumpMemoryLeak({\n  LeakType type = LeakType.notDisposed,\n}) async {\n  final details = await LeakTracking.collectLeaks();\n  details.dumpDetails(type);\n}\n\nclass MemoryLeakDetectorTask extends LaunchTask {\n  MemoryLeakDetectorTask();\n\n  Timer? _timer;\n\n  @override\n  Future<void> initialize(LaunchContext context) async {\n    await super.initialize(context);\n\n    if (!kDebugMode || !enableMemoryLeakDetect) {\n      return;\n    }\n\n    LeakTracking.start();\n    LeakTracking.phase = const PhaseSettings(\n      leakDiagnosticConfig: LeakDiagnosticConfig(\n        collectRetainingPathForNotGCed: true,\n        collectStackTraceOnStart: true,\n      ),\n    );\n\n    FlutterMemoryAllocations.instance.addListener((p0) {\n      LeakTracking.dispatchObjectEvent(p0.toMap());\n    });\n\n    // dump memory leak per second if needed\n    if (dumpMemoryLeakPerSecond) {\n      _timer = Timer.periodic(const Duration(seconds: 1), (_) async {\n        final summary = await LeakTracking.checkLeaks();\n        if (summary.isEmpty) {\n          return;\n        }\n\n        dumpMemoryLeak();\n      });\n    }\n  }\n\n  @override\n  Future<void> dispose() async {\n    await super.dispose();\n\n    if (!kDebugMode || !enableMemoryLeakDetect) {\n      return;\n    }\n\n    if (dumpMemoryLeakPerSecond) {\n      _timer?.cancel();\n      _timer = null;\n    }\n\n    LeakTracking.stop();\n  }\n}\n\nextension on LeakType {\n  String get desc => switch (this) {\n        LeakType.notDisposed => 'not disposed',\n        LeakType.notGCed => 'not GCed',\n        LeakType.gcedLate => 'GCed late'\n      };\n}\n\nfinal _dumpablePackages = [\n  'package:appflowy/',\n  'package:appflowy_editor/',\n];\n\nextension on Leaks {\n  void dumpDetails(LeakType type) {\n    final summary = '${type.desc}: ${switch (type) {\n      LeakType.notDisposed => '${notDisposed.length}',\n      LeakType.notGCed => '${notGCed.length}',\n      LeakType.gcedLate => '${gcedLate.length}'\n    }}';\n    debugPrint(summary);\n    final details = switch (type) {\n      LeakType.notDisposed => notDisposed,\n      LeakType.notGCed => notGCed,\n      LeakType.gcedLate => gcedLate\n    };\n\n    // only dump the code in appflowy\n    for (final value in details) {\n      final stack = value.context![ContextKeys.startCallstack]! as StackTrace;\n      final stackInAppFlowy = stack\n          .toString()\n          .split('\\n')\n          .where(\n            (stack) =>\n                // ignore current file call stack\n                !stack.contains('memory_leak_detector') &&\n                _dumpablePackages.any((pkg) => stack.contains(pkg)),\n          )\n          .join('\\n');\n\n      // ignore the untreatable leak\n      if (stackInAppFlowy.isEmpty) {\n        continue;\n      }\n\n      final object = value.type;\n      debugPrint('''\n$object ${type.desc}\n$stackInAppFlowy\n''');\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/platform_error_catcher.dart",
    "content": "import 'package:appflowy_backend/log.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\n\nimport '../startup.dart';\n\nclass PlatformErrorCatcherTask extends LaunchTask {\n  const PlatformErrorCatcherTask();\n\n  @override\n  Future<void> initialize(LaunchContext context) async {\n    await super.initialize(context);\n\n    // Handle platform errors not caught by Flutter.\n    // Reduces the likelihood of the app crashing, and logs the error.\n    // only active in non debug mode.\n    if (!kDebugMode) {\n      PlatformDispatcher.instance.onError = (error, stack) {\n        Log.error('Uncaught platform error', error, stack);\n        return true;\n      };\n    }\n\n    ErrorWidget.builder = (details) {\n      if (kDebugMode) {\n        return Container(\n          width: double.infinity,\n          height: 30,\n          color: Colors.red,\n          child: FlowyText(\n            'ERROR: ${details.exceptionAsString()}',\n            color: Colors.white,\n          ),\n        );\n      }\n\n      // hide the error widget in release mode\n      return const SizedBox.shrink();\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/platform_service.dart",
    "content": "import 'package:appflowy/core/network_monitor.dart';\nimport '../startup.dart';\n\nclass InitPlatformServiceTask extends LaunchTask {\n  const InitPlatformServiceTask();\n\n  @override\n  LaunchTaskType get type => LaunchTaskType.dataProcessing;\n\n  @override\n  Future<void> initialize(LaunchContext context) async {\n    await super.initialize(context);\n\n    return getIt<NetworkListener>().start();\n  }\n\n  @override\n  Future<void> dispose() async {\n    await super.dispose();\n\n    await getIt<NetworkListener>().stop();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/prelude.dart",
    "content": "export 'app_widget.dart';\nexport 'appflowy_cloud_task.dart';\nexport 'auto_update_task.dart';\nexport 'debug_task.dart';\nexport 'device_info_task.dart';\nexport 'feature_flag_task.dart';\nexport 'generate_router.dart';\nexport 'hot_key.dart';\nexport 'load_plugin.dart';\nexport 'localization.dart';\nexport 'memory_leak_detector.dart';\nexport 'platform_error_catcher.dart';\nexport 'platform_service.dart';\nexport 'recent_service_task.dart';\nexport 'rust_sdk.dart';\nexport 'windows.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/recent_service_task.dart",
    "content": "import 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/log.dart';\n\nclass RecentServiceTask extends LaunchTask {\n  const RecentServiceTask();\n\n  @override\n  Future<void> initialize(LaunchContext context) async {\n    await super.initialize(context);\n\n    Log.info('[CachedRecentService] Initialized');\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:appflowy/env/backend_env.dart';\nimport 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/user/application/auth/device_id.dart';\nimport 'package:appflowy_backend/appflowy_backend.dart';\nimport 'package:path/path.dart' as path;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../startup.dart';\n\nclass InitRustSDKTask extends LaunchTask {\n  const InitRustSDKTask({\n    this.customApplicationPath,\n  });\n\n  // Customize the RustSDK initialization path\n  final Directory? customApplicationPath;\n\n  @override\n  LaunchTaskType get type => LaunchTaskType.dataProcessing;\n\n  @override\n  Future<void> initialize(LaunchContext context) async {\n    await super.initialize(context);\n\n    final root = await getApplicationSupportDirectory();\n    final applicationPath = await appFlowyApplicationDataDirectory();\n    final dir = customApplicationPath ?? applicationPath;\n    final deviceId = await getDeviceId();\n\n    // Pass the environment variables to the Rust SDK\n    final env = _makeAppFlowyConfiguration(\n      root.path,\n      context.config.version,\n      dir.path,\n      applicationPath.path,\n      deviceId,\n      rustEnvs: context.config.rustEnvs,\n    );\n    await context.getIt<FlowySDK>().init(jsonEncode(env.toJson()));\n  }\n}\n\nAppFlowyConfiguration _makeAppFlowyConfiguration(\n  String root,\n  String appVersion,\n  String customAppPath,\n  String originAppPath,\n  String deviceId, {\n  required Map<String, String> rustEnvs,\n}) {\n  final env = getIt<AppFlowyCloudSharedEnv>();\n  return AppFlowyConfiguration(\n    root: root,\n    app_version: appVersion,\n    custom_app_path: customAppPath,\n    origin_app_path: originAppPath,\n    device_id: deviceId,\n    platform: Platform.operatingSystem,\n    authenticator_type: env.authenticatorType.value,\n    appflowy_cloud_config: env.appflowyCloudConfig,\n    envs: rustEnvs,\n  );\n}\n\n/// The default directory to store the user data. The directory can be\n/// customized by the user via the [ApplicationDataStorage]\nFuture<Directory> appFlowyApplicationDataDirectory() async {\n  switch (integrationMode()) {\n    case IntegrationMode.develop:\n      final Directory documentsDir = await getApplicationSupportDirectory()\n          .then((directory) => directory.create());\n      return Directory(path.join(documentsDir.path, 'data_dev'));\n    case IntegrationMode.release:\n      final Directory documentsDir = await getApplicationSupportDirectory();\n      return Directory(path.join(documentsDir.path, 'data'));\n    case IntegrationMode.unitTest:\n    case IntegrationMode.integrationTest:\n      return Directory(path.join(Directory.current.path, '.sandbox'));\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/startup/tasks/windows.dart",
    "content": "import 'dart:async';\nimport 'dart:ui';\n\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/startup/tasks/app_window_size_manager.dart';\nimport 'package:bitsdojo_window/bitsdojo_window.dart';\nimport 'package:flutter/material.dart';\nimport 'package:scaled_app/scaled_app.dart';\nimport 'package:window_manager/window_manager.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass InitAppWindowTask extends LaunchTask with WindowListener {\n  InitAppWindowTask({this.title = 'AppFlowy'});\n\n  final String title;\n  final windowSizeManager = WindowSizeManager();\n\n  @override\n  Future<void> initialize(LaunchContext context) async {\n    await super.initialize(context);\n\n    // Don't initialize in tests or on web\n    if (context.env.isTest || UniversalPlatform.isWeb) {\n      return;\n    }\n\n    if (UniversalPlatform.isMobile) {\n      final scale = await windowSizeManager.getScaleFactor();\n      ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => scale;\n      return;\n    }\n\n    await windowManager.ensureInitialized();\n    windowManager.addListener(this);\n\n    final windowSize = await windowSizeManager.getSize();\n    final windowOptions = WindowOptions(\n      size: windowSize,\n      minimumSize: const Size(\n        WindowSizeManager.minWindowWidth,\n        WindowSizeManager.minWindowHeight,\n      ),\n      maximumSize: const Size(\n        WindowSizeManager.maxWindowWidth,\n        WindowSizeManager.maxWindowHeight,\n      ),\n      title: title,\n    );\n\n    final position = await windowSizeManager.getPosition();\n\n    if (UniversalPlatform.isWindows) {\n      await windowManager.setTitleBarStyle(TitleBarStyle.hidden);\n\n      doWhenWindowReady(() async {\n        appWindow.minSize = windowOptions.minimumSize;\n        appWindow.maxSize = windowOptions.maximumSize;\n        appWindow.size = windowSize;\n\n        if (position != null) {\n          appWindow.position = position;\n        }\n\n        /// on Windows we maximize the window if it was previously closed\n        /// from a maximized state.\n        final isMaximized = await windowSizeManager.getWindowMaximized();\n        if (isMaximized) {\n          appWindow.maximize();\n        }\n      });\n    } else {\n      await windowManager.waitUntilReadyToShow(windowOptions, () async {\n        await windowManager.show();\n        await windowManager.focus();\n\n        if (position != null) {\n          await windowManager.setPosition(position);\n        }\n      });\n    }\n\n    unawaited(\n      windowSizeManager.getScaleFactor().then(\n            (v) => ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => v,\n          ),\n    );\n  }\n\n  @override\n  Future<void> onWindowMaximize() async {\n    super.onWindowMaximize();\n    await windowSizeManager.setWindowMaximized(true);\n    await windowSizeManager.setPosition(Offset.zero);\n  }\n\n  @override\n  Future<void> onWindowUnmaximize() async {\n    super.onWindowUnmaximize();\n    await windowSizeManager.setWindowMaximized(false);\n\n    final position = await windowManager.getPosition();\n    return windowSizeManager.setPosition(position);\n  }\n\n  @override\n  void onWindowEnterFullScreen() async {\n    super.onWindowEnterFullScreen();\n    await windowSizeManager.setWindowMaximized(true);\n    await windowSizeManager.setPosition(Offset.zero);\n  }\n\n  @override\n  Future<void> onWindowLeaveFullScreen() async {\n    super.onWindowLeaveFullScreen();\n    await windowSizeManager.setWindowMaximized(false);\n\n    final position = await windowManager.getPosition();\n    return windowSizeManager.setPosition(position);\n  }\n\n  @override\n  Future<void> onWindowResize() async {\n    super.onWindowResize();\n\n    final currentWindowSize = await windowManager.getSize();\n    return windowSizeManager.setSize(currentWindowSize);\n  }\n\n  @override\n  void onWindowMoved() async {\n    super.onWindowMoved();\n\n    final position = await windowManager.getPosition();\n    return windowSizeManager.setPosition(position);\n  }\n\n  @override\n  Future<void> dispose() async {\n    await super.dispose();\n\n    windowManager.removeListener(this);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/anon_user_bloc.dart",
    "content": "import 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'anon_user_bloc.freezed.dart';\n\nclass AnonUserBloc extends Bloc<AnonUserEvent, AnonUserState> {\n  AnonUserBloc() : super(AnonUserState.initial()) {\n    on<AnonUserEvent>((event, emit) async {\n      await event.when(\n        initial: () async {\n          await _loadHistoricalUsers();\n        },\n        didLoadAnonUsers: (List<UserProfilePB> anonUsers) {\n          emit(state.copyWith(anonUsers: anonUsers));\n        },\n        openAnonUser: (anonUser) async {\n          await UserBackendService.openAnonUser();\n          emit(state.copyWith(openedAnonUser: anonUser));\n        },\n      );\n    });\n  }\n\n  Future<void> _loadHistoricalUsers() async {\n    final result = await UserBackendService.getAnonUser();\n    result.fold(\n      (anonUser) {\n        add(AnonUserEvent.didLoadAnonUsers([anonUser]));\n      },\n      (error) {\n        if (error.code != ErrorCode.RecordNotFound) {\n          Log.error(error);\n        }\n      },\n    );\n  }\n}\n\n@freezed\nclass AnonUserEvent with _$AnonUserEvent {\n  const factory AnonUserEvent.initial() = _Initial;\n  const factory AnonUserEvent.didLoadAnonUsers(\n    List<UserProfilePB> historicalUsers,\n  ) = _DidLoadHistoricalUsers;\n  const factory AnonUserEvent.openAnonUser(UserProfilePB anonUser) =\n      _OpenHistoricalUser;\n}\n\n@freezed\nclass AnonUserState with _$AnonUserState {\n  const factory AnonUserState({\n    required List<UserProfilePB> anonUsers,\n    required UserProfilePB? openedAnonUser,\n  }) = _AnonUserState;\n\n  factory AnonUserState.initial() => const AnonUserState(\n        anonUsers: [],\n        openedAnonUser: null,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/startup/tasks/appflowy_cloud_task.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/application/auth/backend_auth_service.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nimport 'auth_error.dart';\n\nclass AppFlowyCloudAuthService implements AuthService {\n  AppFlowyCloudAuthService();\n\n  final BackendAuthService _backendAuthService = BackendAuthService(\n    AuthTypePB.Server,\n  );\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> signUp({\n    required String name,\n    required String email,\n    required String password,\n    Map<String, String> params = const {},\n  }) async {\n    throw UnimplementedError();\n  }\n\n  @override\n  Future<FlowyResult<GotrueTokenResponsePB, FlowyError>>\n      signInWithEmailPassword({\n    required String email,\n    required String password,\n    Map<String, String> params = const {},\n  }) async {\n    return _backendAuthService.signInWithEmailPassword(\n      email: email,\n      password: password,\n      params: params,\n    );\n  }\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> signUpWithOAuth({\n    required String platform,\n    Map<String, String> params = const {},\n  }) async {\n    final provider = ProviderTypePBExtension.fromPlatform(platform);\n\n    // Get the oauth url from the backend\n    final result = await UserEventGetOauthURLWithProvider(\n      OauthProviderPB.create()..provider = provider,\n    ).send();\n\n    return result.fold(\n      (data) async {\n        // Open the webview with oauth url\n        final uri = Uri.parse(data.oauthUrl);\n        final isSuccess = await afLaunchUri(\n          uri,\n          mode: LaunchMode.externalApplication,\n          webOnlyWindowName: '_self',\n        );\n\n        final completer = Completer<FlowyResult<UserProfilePB, FlowyError>>();\n        if (isSuccess) {\n          // The [AppFlowyCloudDeepLink] must be registered before using the\n          // [AppFlowyCloudAuthService].\n          if (getIt.isRegistered<AppFlowyCloudDeepLink>()) {\n            getIt<AppFlowyCloudDeepLink>().registerCompleter(completer);\n          } else {\n            throw Exception('AppFlowyCloudDeepLink is not registered');\n          }\n        } else {\n          completer.complete(\n            FlowyResult.failure(AuthError.unableToGetDeepLink),\n          );\n        }\n\n        return completer.future;\n      },\n      (r) => FlowyResult.failure(r),\n    );\n  }\n\n  @override\n  Future<void> signOut() async {\n    await _backendAuthService.signOut();\n  }\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> signUpAsGuest({\n    Map<String, String> params = const {},\n  }) async {\n    return _backendAuthService.signUpAsGuest();\n  }\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> signInWithMagicLink({\n    required String email,\n    Map<String, String> params = const {},\n  }) async {\n    return _backendAuthService.signInWithMagicLink(\n      email: email,\n      params: params,\n    );\n  }\n\n  @override\n  Future<FlowyResult<GotrueTokenResponsePB, FlowyError>> signInWithPasscode({\n    required String email,\n    required String passcode,\n  }) async {\n    return _backendAuthService.signInWithPasscode(\n      email: email,\n      passcode: passcode,\n    );\n  }\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> getUser() async {\n    return UserBackendService.getCurrentUserProfile();\n  }\n}\n\nextension ProviderTypePBExtension on ProviderTypePB {\n  static ProviderTypePB fromPlatform(String platform) {\n    switch (platform) {\n      case 'github':\n        return ProviderTypePB.Github;\n      case 'google':\n        return ProviderTypePB.Google;\n      case 'discord':\n        return ProviderTypePB.Discord;\n      case 'apple':\n        return ProviderTypePB.Apple;\n      default:\n        throw UnimplementedError();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/application/auth/backend_auth_service.dart';\nimport 'package:appflowy/user/application/auth/device_id.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flutter/material.dart';\n\n/// Only used for testing.\nclass AppFlowyCloudMockAuthService implements AuthService {\n  AppFlowyCloudMockAuthService({String? email})\n      : userEmail = email ?? \"${uuid()}@appflowy.io\";\n\n  final String userEmail;\n\n  final BackendAuthService _appFlowyAuthService =\n      BackendAuthService(AuthTypePB.Server);\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> signUp({\n    required String name,\n    required String email,\n    required String password,\n    Map<String, String> params = const {},\n  }) async {\n    throw UnimplementedError();\n  }\n\n  @override\n  Future<FlowyResult<GotrueTokenResponsePB, FlowyError>>\n      signInWithEmailPassword({\n    required String email,\n    required String password,\n    Map<String, String> params = const {},\n  }) async {\n    throw UnimplementedError();\n  }\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> signUpWithOAuth({\n    required String platform,\n    Map<String, String> params = const {},\n  }) async {\n    final payload = SignInUrlPayloadPB.create()\n      ..authenticator = AuthTypePB.Server\n      // don't use nanoid here, the gotrue server will transform the email\n      ..email = userEmail;\n\n    final deviceId = await getDeviceId();\n    final getSignInURLResult = await UserEventGenerateSignInURL(payload).send();\n\n    return getSignInURLResult.fold(\n      (urlPB) async {\n        final payload = OauthSignInPB(\n          authType: AuthTypePB.Server,\n          map: {\n            AuthServiceMapKeys.signInURL: urlPB.signInUrl,\n            AuthServiceMapKeys.deviceId: deviceId,\n          },\n        );\n        Log.info(\"UserEventOauthSignIn with payload: $payload\");\n        return UserEventOauthSignIn(payload).send().then((value) {\n          value.fold(\n            (l) => null,\n            (err) {\n              debugPrint(\"mock auth service Error: $err\");\n              Log.error(err);\n            },\n          );\n          return value;\n        });\n      },\n      (r) {\n        debugPrint(\"mock auth service error: $r\");\n        return FlowyResult.failure(r);\n      },\n    );\n  }\n\n  @override\n  Future<void> signOut() async {\n    await _appFlowyAuthService.signOut();\n  }\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> signUpAsGuest({\n    Map<String, String> params = const {},\n  }) async {\n    return _appFlowyAuthService.signUpAsGuest();\n  }\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> signInWithMagicLink({\n    required String email,\n    Map<String, String> params = const {},\n  }) async {\n    throw UnimplementedError();\n  }\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> getUser() async {\n    return UserBackendService.getCurrentUserProfile();\n  }\n\n  @override\n  Future<FlowyResult<GotrueTokenResponsePB, FlowyError>> signInWithPasscode({\n    required String email,\n    required String passcode,\n  }) async {\n    throw UnimplementedError();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\n\nclass AuthError {\n  static final signInWithOauthError = FlowyError()\n    ..msg = 'sign in with oauth error -10003'\n    ..code = ErrorCode.UserUnauthorized;\n\n  static final emptyDeepLink = FlowyError()\n    ..msg = 'Unexpected empty DeepLink'\n    ..code = ErrorCode.UnexpectedCalendarFieldType;\n\n  static final deepLinkError = FlowyError()\n    ..msg = 'DeepLink error'\n    ..code = ErrorCode.Internal;\n\n  static final unableToGetDeepLink = FlowyError()\n    ..msg = 'Unable to get the deep link'\n    ..code = ErrorCode.Internal;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass AuthServiceMapKeys {\n  const AuthServiceMapKeys._();\n\n  static const String email = 'email';\n  static const String deviceId = 'device_id';\n  static const String signInURL = 'sign_in_url';\n}\n\n/// `AuthService` is an abstract class that defines methods related to user authentication.\n///\n/// This service provides various methods for user sign-in, sign-up,\n/// OAuth-based registration, and other related functionalities.\nabstract class AuthService {\n  /// Authenticates a user with their email and password.\n  ///\n  /// - `email`: The email address of the user.\n  /// - `password`: The password of the user.\n  /// - `params`: Additional parameters for authentication (optional).\n  ///\n  /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError].\n\n  Future<FlowyResult<GotrueTokenResponsePB, FlowyError>>\n      signInWithEmailPassword({\n    required String email,\n    required String password,\n    Map<String, String> params,\n  });\n\n  /// Registers a new user with their name, email, and password.\n  ///\n  /// - `name`: The name of the user.\n  /// - `email`: The email address of the user.\n  /// - `password`: The password of the user.\n  /// - `params`: Additional parameters for registration (optional).\n  ///\n  /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError].\n  Future<FlowyResult<UserProfilePB, FlowyError>> signUp({\n    required String name,\n    required String email,\n    required String password,\n    Map<String, String> params,\n  });\n\n  /// Registers a new user with an OAuth platform.\n  ///\n  /// - `platform`: The OAuth platform name.\n  /// - `params`: Additional parameters for OAuth registration (optional).\n  ///\n  /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError].\n  Future<FlowyResult<UserProfilePB, FlowyError>> signUpWithOAuth({\n    required String platform,\n    Map<String, String> params,\n  });\n\n  /// Registers a user as a guest.\n  ///\n  /// - `params`: Additional parameters for guest registration (optional).\n  ///\n  /// Returns a default [UserProfilePB].\n  Future<FlowyResult<UserProfilePB, FlowyError>> signUpAsGuest({\n    Map<String, String> params,\n  });\n\n  /// Authenticates a user with a magic link sent to their email.\n  ///\n  /// - `email`: The email address of the user.\n  /// - `params`: Additional parameters for authentication with magic link (optional).\n  ///\n  /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError].\n  Future<FlowyResult<void, FlowyError>> signInWithMagicLink({\n    required String email,\n    Map<String, String> params,\n  });\n\n  /// Authenticates a user with a passcode sent to their email.\n  ///\n  /// - `email`: The email address of the user.\n  /// - `passcode`: The passcode of the user.\n  ///\n  /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError].\n  Future<FlowyResult<GotrueTokenResponsePB, FlowyError>> signInWithPasscode({\n    required String email,\n    required String passcode,\n  });\n\n  /// Signs out the currently authenticated user.\n  Future<void> signOut();\n\n  /// Retrieves the currently authenticated user's profile.\n  ///\n  /// Returns [UserProfilePB] if the user has signed in, otherwise returns [FlowyError].\n  Future<FlowyResult<UserProfilePB, FlowyError>> getUser();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart",
    "content": "import 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'\n    show SignInPayloadPB, SignUpPayloadPB, UserProfilePB;\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport '../../../generated/locale_keys.g.dart';\nimport 'device_id.dart';\n\nclass BackendAuthService implements AuthService {\n  BackendAuthService(this.authType);\n\n  final AuthTypePB authType;\n\n  @override\n  Future<FlowyResult<GotrueTokenResponsePB, FlowyError>>\n      signInWithEmailPassword({\n    required String email,\n    required String password,\n    Map<String, String> params = const {},\n  }) async {\n    final request = SignInPayloadPB.create()\n      ..email = email\n      ..password = password\n      ..authType = authType\n      ..deviceId = await getDeviceId();\n    return UserEventSignInWithEmailPassword(request).send();\n  }\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> signUp({\n    required String name,\n    required String email,\n    required String password,\n    Map<String, String> params = const {},\n  }) async {\n    final request = SignUpPayloadPB.create()\n      ..name = name\n      ..email = email\n      ..password = password\n      ..authType = authType\n      ..deviceId = await getDeviceId();\n    final response = await UserEventSignUp(request).send().then(\n          (value) => value,\n        );\n    return response;\n  }\n\n  @override\n  Future<void> signOut({\n    Map<String, String> params = const {},\n  }) async {\n    await UserEventSignOut().send();\n    return;\n  }\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> signUpAsGuest({\n    Map<String, String> params = const {},\n  }) async {\n    const password = \"Guest!@123456\";\n    final userEmail = \"anon@appflowy.io\";\n\n    final request = SignUpPayloadPB.create()\n      ..name = LocaleKeys.defaultUsername.tr()\n      ..email = userEmail\n      ..password = password\n      // When sign up as guest, the auth type is always local.\n      ..authType = AuthTypePB.Local\n      ..deviceId = await getDeviceId();\n    final response = await UserEventSignUp(request).send().then(\n          (value) => value,\n        );\n    return response;\n  }\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> signUpWithOAuth({\n    required String platform,\n    AuthTypePB authType = AuthTypePB.Local,\n    Map<String, String> params = const {},\n  }) async {\n    return FlowyResult.failure(\n      FlowyError.create()\n        ..code = ErrorCode.Internal\n        ..msg = \"Unsupported sign up action\",\n    );\n  }\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> getUser() async {\n    return UserBackendService.getCurrentUserProfile();\n  }\n\n  @override\n  Future<FlowyResult<UserProfilePB, FlowyError>> signInWithMagicLink({\n    required String email,\n    Map<String, String> params = const {},\n  }) async {\n    // No need to pass the redirect URL.\n    return UserBackendService.signInWithMagicLink(email, '');\n  }\n\n  @override\n  Future<FlowyResult<GotrueTokenResponsePB, FlowyError>> signInWithPasscode({\n    required String email,\n    required String passcode,\n  }) async {\n    return UserBackendService.signInWithPasscode(email, passcode);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/auth/device_id.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:device_info_plus/device_info_plus.dart';\nimport 'package:flutter/services.dart';\n\nfinal DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();\n\nFuture<String> getDeviceId() async {\n  if (integrationMode().isTest) {\n    return \"test_device_id\";\n  }\n\n  String? deviceId;\n  try {\n    if (Platform.isAndroid) {\n      final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;\n      deviceId = androidInfo.device;\n    } else if (Platform.isIOS) {\n      final IosDeviceInfo iosInfo = await deviceInfo.iosInfo;\n      deviceId = iosInfo.identifierForVendor;\n    } else if (Platform.isMacOS) {\n      final MacOsDeviceInfo macInfo = await deviceInfo.macOsInfo;\n      deviceId = macInfo.systemGUID;\n    } else if (Platform.isWindows) {\n      final WindowsDeviceInfo windowsInfo = await deviceInfo.windowsInfo;\n      deviceId = windowsInfo.deviceId;\n    } else if (Platform.isLinux) {\n      final LinuxDeviceInfo linuxInfo = await deviceInfo.linuxInfo;\n      deviceId = linuxInfo.machineId;\n    }\n  } on PlatformException {\n    Log.error('Failed to get platform version');\n  }\n  return deviceId ?? '';\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/notification_filter/notification_filter_bloc.dart",
    "content": "import 'package:bloc/bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'notification_filter_bloc.freezed.dart';\n\nclass NotificationFilterBloc\n    extends Bloc<NotificationFilterEvent, NotificationFilterState> {\n  NotificationFilterBloc() : super(const NotificationFilterState()) {\n    on<NotificationFilterEvent>((event, emit) async {\n      event.when(\n        reset: () => emit(const NotificationFilterState()),\n        toggleShowUnreadsOnly: () => emit(\n          state.copyWith(showUnreadsOnly: !state.showUnreadsOnly),\n        ),\n      );\n    });\n  }\n}\n\n@freezed\nclass NotificationFilterEvent with _$NotificationFilterEvent {\n  const factory NotificationFilterEvent.toggleShowUnreadsOnly() =\n      _ToggleShowUnreadsOnly;\n\n  const factory NotificationFilterEvent.reset() = _Reset;\n}\n\n@freezed\nclass NotificationFilterState with _$NotificationFilterState {\n  const NotificationFilterState._();\n\n  const factory NotificationFilterState({\n    @Default(false) bool showUnreadsOnly,\n  }) = _NotificationFilterState;\n\n  // If state is not default values, then there are custom changes\n  bool get hasFilters => showUnreadsOnly != false;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/user/application/password/password_http_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'password_bloc.freezed.dart';\n\nclass PasswordBloc extends Bloc<PasswordEvent, PasswordState> {\n  PasswordBloc(this.userProfile) : super(PasswordState.initial()) {\n    on<PasswordEvent>(\n      (event, emit) async {\n        await event.when(\n          init: () async => _init(),\n          changePassword: (oldPassword, newPassword) async => _onChangePassword(\n            emit,\n            oldPassword: oldPassword,\n            newPassword: newPassword,\n          ),\n          setupPassword: (newPassword) async => _onSetupPassword(\n            emit,\n            newPassword: newPassword,\n          ),\n          forgotPassword: (email) async => _onForgotPassword(\n            emit,\n            email: email,\n          ),\n          checkHasPassword: () async => _onCheckHasPassword(\n            emit,\n          ),\n          cancel: () {},\n        );\n      },\n    );\n  }\n\n  final UserProfilePB userProfile;\n  late final PasswordHttpService passwordHttpService;\n\n  bool _isInitialized = false;\n\n  Future<void> _init() async {\n    if (userProfile.userAuthType == AuthTypePB.Local) {\n      Log.debug('PasswordBloc: skip init because user is local authenticator');\n      return;\n    }\n\n    final baseUrl = await getAppFlowyCloudUrl();\n    try {\n      final authToken = jsonDecode(userProfile.token)['access_token'];\n      passwordHttpService = PasswordHttpService(\n        baseUrl: baseUrl,\n        authToken: authToken,\n      );\n      _isInitialized = true;\n    } catch (e) {\n      Log.error('PasswordBloc: _init: error: $e');\n    }\n  }\n\n  Future<void> _onChangePassword(\n    Emitter<PasswordState> emit, {\n    required String oldPassword,\n    required String newPassword,\n  }) async {\n    if (!_isInitialized) {\n      Log.info('changePassword: not initialized');\n      return;\n    }\n\n    if (state.isSubmitting) {\n      Log.info('changePassword: already submitting');\n      return;\n    }\n\n    _clearState(emit, true);\n\n    final result = await passwordHttpService.changePassword(\n      currentPassword: oldPassword,\n      newPassword: newPassword,\n    );\n\n    emit(\n      state.copyWith(\n        isSubmitting: false,\n        changePasswordResult: result,\n      ),\n    );\n  }\n\n  Future<void> _onSetupPassword(\n    Emitter<PasswordState> emit, {\n    required String newPassword,\n  }) async {\n    if (!_isInitialized) {\n      Log.info('setupPassword: not initialized');\n      return;\n    }\n\n    if (state.isSubmitting) {\n      Log.info('setupPassword: already submitting');\n      return;\n    }\n\n    _clearState(emit, true);\n\n    final result = await passwordHttpService.setupPassword(\n      newPassword: newPassword,\n    );\n\n    emit(\n      state.copyWith(\n        isSubmitting: false,\n        hasPassword: result.fold(\n          (success) => true,\n          (error) => false,\n        ),\n        setupPasswordResult: result,\n      ),\n    );\n  }\n\n  Future<void> _onForgotPassword(\n    Emitter<PasswordState> emit, {\n    required String email,\n  }) async {\n    if (!_isInitialized) {\n      Log.info('forgotPassword: not initialized');\n      return;\n    }\n\n    if (state.isSubmitting) {\n      Log.info('forgotPassword: already submitting');\n      return;\n    }\n\n    _clearState(emit, true);\n\n    final result = await passwordHttpService.forgotPassword(email: email);\n\n    emit(\n      state.copyWith(\n        isSubmitting: false,\n        forgotPasswordResult: result,\n      ),\n    );\n  }\n\n  Future<void> _onCheckHasPassword(Emitter<PasswordState> emit) async {\n    if (!_isInitialized) {\n      Log.info('checkHasPassword: not initialized');\n      return;\n    }\n\n    if (state.isSubmitting) {\n      Log.info('checkHasPassword: already submitting');\n      return;\n    }\n\n    _clearState(emit, true);\n\n    final result = await passwordHttpService.checkHasPassword();\n\n    emit(\n      state.copyWith(\n        isSubmitting: false,\n        hasPassword: result.fold(\n          (success) => success,\n          (error) => false,\n        ),\n        checkHasPasswordResult: result,\n      ),\n    );\n  }\n\n  void _clearState(Emitter<PasswordState> emit, bool isSubmitting) {\n    emit(\n      state.copyWith(\n        isSubmitting: isSubmitting,\n        changePasswordResult: null,\n        setupPasswordResult: null,\n        forgotPasswordResult: null,\n        checkHasPasswordResult: null,\n      ),\n    );\n  }\n}\n\n@freezed\nclass PasswordEvent with _$PasswordEvent {\n  const factory PasswordEvent.init() = Init;\n\n  // Change password\n  const factory PasswordEvent.changePassword({\n    required String oldPassword,\n    required String newPassword,\n  }) = ChangePassword;\n\n  // Setup password\n  const factory PasswordEvent.setupPassword({\n    required String newPassword,\n  }) = SetupPassword;\n\n  // Forgot password\n  const factory PasswordEvent.forgotPassword({\n    required String email,\n  }) = ForgotPassword;\n\n  // Check has password\n  const factory PasswordEvent.checkHasPassword() = CheckHasPassword;\n\n  // Cancel operation\n  const factory PasswordEvent.cancel() = Cancel;\n}\n\n@freezed\nclass PasswordState with _$PasswordState {\n  const factory PasswordState({\n    required bool isSubmitting,\n    required bool hasPassword,\n    required FlowyResult<bool, FlowyError>? changePasswordResult,\n    required FlowyResult<bool, FlowyError>? setupPasswordResult,\n    required FlowyResult<bool, FlowyError>? forgotPasswordResult,\n    required FlowyResult<bool, FlowyError>? checkHasPasswordResult,\n  }) = _PasswordState;\n\n  factory PasswordState.initial() => const PasswordState(\n        isSubmitting: false,\n        hasPassword: false,\n        changePasswordResult: null,\n        setupPasswordResult: null,\n        forgotPasswordResult: null,\n        checkHasPasswordResult: null,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:http/http.dart' as http;\n\nenum PasswordEndpoint {\n  changePassword,\n  forgotPassword,\n  setupPassword,\n  checkHasPassword,\n  verifyResetPasswordToken;\n\n  String get path {\n    switch (this) {\n      case PasswordEndpoint.changePassword:\n        return '/gotrue/user/change-password';\n      case PasswordEndpoint.forgotPassword:\n        return '/gotrue/recover';\n      case PasswordEndpoint.setupPassword:\n        return '/gotrue/user/change-password';\n      case PasswordEndpoint.checkHasPassword:\n        return '/gotrue/user/auth-info';\n      case PasswordEndpoint.verifyResetPasswordToken:\n        return '/gotrue/verify';\n    }\n  }\n\n  String get method {\n    switch (this) {\n      case PasswordEndpoint.changePassword:\n      case PasswordEndpoint.setupPassword:\n      case PasswordEndpoint.forgotPassword:\n      case PasswordEndpoint.verifyResetPasswordToken:\n        return 'POST';\n      case PasswordEndpoint.checkHasPassword:\n        return 'GET';\n    }\n  }\n\n  Uri uri(String baseUrl) => Uri.parse('$baseUrl$path');\n}\n\nclass PasswordHttpService {\n  PasswordHttpService({\n    required this.baseUrl,\n    required this.authToken,\n  });\n\n  final String baseUrl;\n\n  String authToken;\n\n  final http.Client client = http.Client();\n\n  Map<String, String> get headers => {\n        'Content-Type': 'application/json',\n        'Authorization': 'Bearer $authToken',\n      };\n\n  /// Changes the user's password\n  ///\n  /// [currentPassword] - The user's current password\n  /// [newPassword] - The new password to set\n  Future<FlowyResult<bool, FlowyError>> changePassword({\n    required String currentPassword,\n    required String newPassword,\n  }) async {\n    final result = await _makeRequest(\n      endpoint: PasswordEndpoint.changePassword,\n      body: {\n        'current_password': currentPassword,\n        'password': newPassword,\n      },\n      errorMessage: 'Failed to change password',\n    );\n\n    return result.fold(\n      (data) => FlowyResult.success(true),\n      (error) => FlowyResult.failure(error),\n    );\n  }\n\n  /// Sends a password reset email to the user\n  ///\n  /// [email] - The email address of the user\n  Future<FlowyResult<bool, FlowyError>> forgotPassword({\n    required String email,\n  }) async {\n    final result = await _makeRequest(\n      endpoint: PasswordEndpoint.forgotPassword,\n      body: {'email': email},\n      errorMessage: 'Failed to send password reset email',\n    );\n\n    return result.fold(\n      (data) => FlowyResult.success(true),\n      (error) => FlowyResult.failure(error),\n    );\n  }\n\n  /// Sets up a password for a user that doesn't have one\n  ///\n  /// [newPassword] - The new password to set\n  Future<FlowyResult<bool, FlowyError>> setupPassword({\n    required String newPassword,\n  }) async {\n    final result = await _makeRequest(\n      endpoint: PasswordEndpoint.setupPassword,\n      body: {'password': newPassword},\n      errorMessage: 'Failed to setup password',\n    );\n\n    return result.fold(\n      (data) => FlowyResult.success(true),\n      (error) => FlowyResult.failure(error),\n    );\n  }\n\n  /// Checks if the user has a password set\n  Future<FlowyResult<bool, FlowyError>> checkHasPassword() async {\n    final result = await _makeRequest(\n      endpoint: PasswordEndpoint.checkHasPassword,\n      errorMessage: 'Failed to check password status',\n    );\n\n    try {\n      return result.fold(\n        (data) => FlowyResult.success(data['has_password'] ?? false),\n        (error) => FlowyResult.failure(error),\n      );\n    } catch (e) {\n      return FlowyResult.failure(\n        FlowyError(msg: 'Failed to check password status: $e'),\n      );\n    }\n  }\n\n  // Verify the reset password token\n  Future<FlowyResult<String, FlowyError>> verifyResetPasswordToken({\n    required String email,\n    required String token,\n  }) async {\n    final result = await _makeRequest(\n      endpoint: PasswordEndpoint.verifyResetPasswordToken,\n      body: {\n        'type': 'recovery',\n        'email': email,\n        'token': token,\n      },\n      errorMessage: 'Failed to verify reset password token',\n    );\n\n    try {\n      return result.fold(\n        (data) {\n          final authToken = data['access_token'];\n          return FlowyResult.success(authToken);\n        },\n        (error) => FlowyResult.failure(error),\n      );\n    } catch (e) {\n      return FlowyResult.failure(\n        FlowyError(msg: 'Failed to verify reset password token: $e'),\n      );\n    }\n  }\n\n  /// Makes a request to the specified endpoint with the given body\n  Future<FlowyResult<dynamic, FlowyError>> _makeRequest({\n    required PasswordEndpoint endpoint,\n    Map<String, dynamic>? body,\n    String errorMessage = 'Request failed',\n  }) async {\n    try {\n      final uri = endpoint.uri(baseUrl);\n      http.Response response;\n\n      if (endpoint.method == 'POST') {\n        response = await client.post(\n          uri,\n          headers: headers,\n          body: body != null ? jsonEncode(body) : null,\n        );\n      } else if (endpoint.method == 'GET') {\n        response = await client.get(\n          uri,\n          headers: headers,\n        );\n      } else {\n        return FlowyResult.failure(\n          FlowyError(msg: 'Invalid request method: ${endpoint.method}'),\n        );\n      }\n\n      if (response.statusCode == 200) {\n        if (response.body.isNotEmpty) {\n          return FlowyResult.success(jsonDecode(response.body));\n        }\n        return FlowyResult.success(true);\n      } else {\n        final errorBody =\n            response.body.isNotEmpty ? jsonDecode(response.body) : {};\n\n        // the checkHasPassword endpoint will return 403, which is not an error\n        if (endpoint != PasswordEndpoint.checkHasPassword) {\n          Log.info(\n            '${endpoint.name} request failed: ${response.statusCode}, $errorBody ',\n          );\n        }\n\n        ErrorCode errorCode = ErrorCode.Internal;\n\n        if (response.statusCode == 422) {\n          errorCode = ErrorCode.NewPasswordTooWeak;\n        }\n\n        return FlowyResult.failure(\n          FlowyError(\n            code: errorCode,\n            msg: errorBody['msg'] ?? errorMessage,\n          ),\n        );\n      }\n    } catch (e) {\n      Log.error('${endpoint.name} request failed: error: $e');\n\n      return FlowyResult.failure(\n        FlowyError(msg: 'Network error: ${e.toString()}'),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/prelude.dart",
    "content": "export 'auth/backend_auth_service.dart';\nexport './sign_in_bloc.dart';\nexport './sign_up_bloc.dart';\nexport './splash_bloc.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';\nimport 'package:appflowy/plugins/document/application/document_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';\nimport 'package:appflowy/shared/list_extension.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/reminder/reminder_extension.dart';\nimport 'package:appflowy/user/application/reminder/reminder_service.dart';\nimport 'package:appflowy/util/int64_extension.dart';\nimport 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';\nimport 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flutter/material.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:protobuf/protobuf.dart';\n\npart 'reminder_bloc.freezed.dart';\n\nclass ReminderBloc extends Bloc<ReminderEvent, ReminderState> {\n  ReminderBloc() : super(ReminderState()) {\n    Log.info('ReminderBloc created');\n\n    _actionBloc = getIt<ActionNavigationBloc>();\n    _reminderService = const ReminderService();\n    timer = _periodicCheck();\n    _listener = AppLifecycleListener(\n      onResume: () {\n        if (!isClosed) {\n          add(const ReminderEvent.resetTimer());\n        }\n      },\n    );\n\n    _dispatch();\n  }\n\n  late final ActionNavigationBloc _actionBloc;\n  late final ReminderService _reminderService;\n  Timer? timer;\n  late final AppLifecycleListener _listener;\n  final _deepEquality = DeepCollectionEquality();\n\n  bool hasReminder(String reminderId) =>\n      state.allReminders.where((e) => e.id == reminderId).firstOrNull != null;\n\n  final List<ViewPB> _allViews = [];\n\n  void _dispatch() {\n    on<ReminderEvent>(\n      (event, emit) async {\n        await event.when(\n          started: () async {\n            add(const ReminderEvent.refresh());\n          },\n          refresh: () async {\n            final result = await _reminderService.fetchReminders();\n            final views = await ViewBackendService.getAllViews();\n            views.onSuccess((views) {\n              _allViews.clear();\n              _allViews.addAll(views.items);\n            });\n\n            await result.fold(\n              (reminders) async {\n                final availableReminders =\n                    await filterAvailableReminders(reminders);\n                // only print the reminder ids are not the same as the previous ones\n                final previousReminderIds =\n                    state.reminders.map((e) => e.id).toSet();\n                final newReminderIds =\n                    availableReminders.map((e) => e.id).toSet();\n                final diff = _deepEquality.equals(\n                  previousReminderIds,\n                  newReminderIds,\n                );\n                if (!diff) {\n                  Log.info(\n                    'Fetched reminders on refresh: ${availableReminders.length}',\n                  );\n                }\n                if (!isClosed && !emit.isDone) {\n                  emit(\n                    state.copyWith(\n                      reminders: availableReminders,\n                      serverReminders: reminders,\n                    ),\n                  );\n                }\n              },\n              (error) {\n                Log.error('Failed to fetch reminders: $error');\n              },\n            );\n          },\n          removeReminder: (reminderId) async {\n            final result = await _reminderService.removeReminder(\n              reminderId: reminderId,\n            );\n\n            result.fold(\n              (_) {\n                Log.info('Removed reminder: $reminderId');\n                final reminders = List.of(state.reminders);\n                final index = reminders\n                    .indexWhere((reminder) => reminder.id == reminderId);\n                if (index != -1) {\n                  reminders.removeAt(index);\n                  emit(state.copyWith(reminders: reminders));\n                }\n              },\n              (error) => Log.error(\n                'Failed to remove reminder($reminderId): $error',\n              ),\n            );\n          },\n          removeReminders: (reminderIds) async {\n            Log.info('Remove reminders: $reminderIds');\n            final removedIds = <String>{};\n            for (final reminderId in reminderIds) {\n              final result = await _reminderService.removeReminder(\n                reminderId: reminderId,\n              );\n              if (result.isSuccess) {\n                Log.info('Removed reminder: $reminderId');\n                removedIds.add(reminderId);\n              } else {\n                Log.error('Failed to remove reminder: $reminderId');\n              }\n            }\n            emit(\n              state.copyWith(\n                reminders: state.reminders\n                    .where((reminder) => !removedIds.contains(reminder.id))\n                    .toList(),\n              ),\n            );\n          },\n          add: (reminder) async {\n            // check the timestamp in the reminder\n            if (reminder.createdAt == null) {\n              reminder.freeze();\n              reminder = reminder.rebuild((update) {\n                update.meta[ReminderMetaKeys.createdAt] =\n                    DateTime.now().millisecondsSinceEpoch.toString();\n              });\n            }\n            if (hasReminder(reminder.id)) {\n              Log.error('Reminder: ${reminder.id} failed to be added again');\n              return;\n            }\n\n            final result = await _reminderService.addReminder(\n              reminder: reminder,\n            );\n\n            return result.fold(\n              (_) async {\n                Log.info('Added reminder: ${reminder.id}');\n                Log.info('Before adding reminder: ${state.reminders.length}');\n                final showRightNow = !DateTime.now()\n                        .isBefore(reminder.scheduledAt.toDateTime()) &&\n                    !reminder.isRead;\n\n                if (showRightNow) {\n                  final reminders = [...state.reminders, reminder];\n                  Log.info('After adding reminder: ${reminders.length}');\n                  emit(state.copyWith(reminders: reminders));\n                }\n              },\n              (error) {\n                Log.error('Failed to add reminder: $error');\n              },\n            );\n          },\n          addById: (reminderId, objectId, scheduledAt, meta) async => add(\n            ReminderEvent.add(\n              reminder: ReminderPB(\n                id: reminderId,\n                objectId: objectId,\n                title: LocaleKeys.reminderNotification_title.tr(),\n                message: LocaleKeys.reminderNotification_message.tr(),\n                scheduledAt: scheduledAt,\n                isAck: scheduledAt.toDateTime().isBefore(DateTime.now()),\n                meta: meta,\n              ),\n            ),\n          ),\n          update: (updateObject) async {\n            final reminder = state.allReminders.firstWhereOrNull(\n              (r) => r.id == updateObject.id,\n            );\n\n            if (reminder == null) {\n              return;\n            }\n\n            final newReminder = updateObject.merge(a: reminder);\n            final failureOrUnit = await _reminderService.updateReminder(\n              reminder: newReminder,\n            );\n\n            Log.info('Updating reminder: ${newReminder.id}');\n\n            await failureOrUnit.fold((_) async {\n              Log.info('Updated reminder: ${newReminder.id}');\n              final index =\n                  state.reminders.indexWhere((r) => r.id == newReminder.id);\n              if (index == -1) {\n                if (await checkReminderAvailable(\n                  newReminder,\n                  state.allReminders.map((e) => e.id).toSet(),\n                )) {\n                  emit(\n                    state\n                        .copyWith(reminders: [...state.reminders, newReminder]),\n                  );\n                }\n                return;\n              }\n              final reminders = [...state.reminders];\n              if (await checkReminderAvailable(\n                newReminder,\n                state.allReminders.map((e) => e.id).toSet(),\n              )) {\n                reminders.replaceRange(index, index + 1, [newReminder]);\n                emit(state.copyWith(reminders: reminders));\n              } else {\n                reminders.removeAt(index);\n                emit(state.copyWith(reminders: reminders));\n              }\n            }, (error) {\n              Log.error(\n                'Failed to update reminder(${newReminder.id}): $error',\n              );\n            });\n          },\n          pressReminder: (reminderId, path, view) {\n            final reminder =\n                state.reminders.firstWhereOrNull((r) => r.id == reminderId);\n\n            if (reminder == null) {\n              return;\n            }\n\n            add(\n              ReminderEvent.update(\n                ReminderUpdate(\n                  id: reminderId,\n                  isRead: state.pastReminders.contains(reminder),\n                ),\n              ),\n            );\n\n            String? rowId;\n            if (view?.layout != ViewLayoutPB.Document) {\n              rowId = reminder.meta[ReminderMetaKeys.rowId];\n            }\n\n            final action = NavigationAction(\n              objectId: reminder.objectId,\n              arguments: {\n                ActionArgumentKeys.view: view,\n                ActionArgumentKeys.nodePath: path,\n                ActionArgumentKeys.rowId: rowId,\n              },\n            );\n\n            if (!isClosed) {\n              _actionBloc.add(\n                ActionNavigationEvent.performAction(\n                  action: action,\n                  nextActions: [\n                    action.copyWith(\n                      type: rowId != null\n                          ? ActionType.openRow\n                          : ActionType.jumpToBlock,\n                    ),\n                  ],\n                ),\n              );\n            }\n          },\n          markAsRead: (reminderIds) async {\n            final reminders = await _onMarkAsRead(reminderIds: reminderIds);\n\n            Log.info('Marked reminders as read: $reminderIds');\n\n            emit(\n              state.copyWith(\n                reminders: reminders,\n              ),\n            );\n          },\n          archive: (reminderIds) async {\n            final reminders = await _onArchived(\n              isArchived: true,\n              reminderIds: reminderIds,\n            );\n\n            Log.info('Archived reminders: $reminderIds');\n\n            emit(\n              state.copyWith(\n                reminders: reminders,\n              ),\n            );\n          },\n          markAllRead: () async {\n            final reminders = await _onMarkAsRead();\n\n            Log.info('Marked all reminders as read');\n\n            emit(\n              state.copyWith(\n                reminders: reminders,\n              ),\n            );\n          },\n          archiveAll: () async {\n            final reminders = await _onArchived(isArchived: true);\n\n            Log.info('Archived all reminders');\n\n            emit(\n              state.copyWith(\n                reminders: reminders,\n              ),\n            );\n          },\n          unarchiveAll: () async {\n            final reminders = await _onArchived(isArchived: false);\n            emit(\n              state.copyWith(\n                reminders: reminders,\n              ),\n            );\n          },\n          resetTimer: () {\n            timer?.cancel();\n            timer = _periodicCheck();\n          },\n        );\n      },\n    );\n  }\n\n  @override\n  Future<void> close() async {\n    Log.info('ReminderBloc closed');\n    _listener.dispose();\n    timer?.cancel();\n    await super.close();\n  }\n\n  /// Mark the reminder as read\n  ///\n  /// If the [reminderIds] is null, all unread reminders will be marked as read\n  /// Otherwise, only the reminders with the given IDs will be marked as read\n  Future<List<ReminderPB>> _onMarkAsRead({\n    List<String>? reminderIds,\n  }) async {\n    final Iterable<ReminderPB> remindersToUpdate;\n\n    if (reminderIds != null) {\n      remindersToUpdate = state.reminders.where(\n        (reminder) => reminderIds.contains(reminder.id) && !reminder.isRead,\n      );\n    } else {\n      // Get all reminders that are not matching the isArchived flag\n      remindersToUpdate = state.reminders.where(\n        (reminder) => !reminder.isRead,\n      );\n    }\n\n    for (final reminder in remindersToUpdate) {\n      reminder.isRead = true;\n\n      await _reminderService.updateReminder(reminder: reminder);\n      Log.info('Mark reminder ${reminder.id} as read');\n    }\n\n    return state.reminders.map((e) {\n      if (reminderIds != null && !reminderIds.contains(e.id)) {\n        return e;\n      }\n\n      if (e.isRead) {\n        return e;\n      }\n\n      e.freeze();\n      return e.rebuild((update) {\n        update.isRead = true;\n      });\n    }).toList();\n  }\n\n  /// Archive or unarchive reminders\n  ///\n  /// If the [reminderIds] is null, all reminders will be archived\n  /// Otherwise, only the reminders with the given IDs will be archived or unarchived\n  Future<List<ReminderPB>> _onArchived({\n    required bool isArchived,\n    List<String>? reminderIds,\n  }) async {\n    final Iterable<ReminderPB> remindersToUpdate;\n\n    if (reminderIds != null) {\n      remindersToUpdate = state.reminders.where(\n        (reminder) =>\n            reminderIds.contains(reminder.id) &&\n            reminder.isArchived != isArchived,\n      );\n    } else {\n      // Get all reminders that are not matching the isArchived flag\n      remindersToUpdate = state.reminders.where(\n        (reminder) => reminder.isArchived != isArchived,\n      );\n    }\n\n    for (final reminder in remindersToUpdate) {\n      reminder.isRead = isArchived;\n      reminder.meta[ReminderMetaKeys.isArchived] = isArchived.toString();\n      await _reminderService.updateReminder(reminder: reminder);\n      Log.info('Reminder ${reminder.id} is archived: $isArchived');\n    }\n\n    return state.reminders.map((e) {\n      if (reminderIds != null && !reminderIds.contains(e.id)) {\n        return e;\n      }\n\n      if (e.isArchived == isArchived) {\n        return e;\n      }\n\n      e.freeze();\n      return e.rebuild((update) {\n        update.isRead = isArchived;\n        update.meta[ReminderMetaKeys.isArchived] = isArchived.toString();\n      });\n    }).toList();\n  }\n\n  Timer _periodicCheck() {\n    return Timer.periodic(\n      const Duration(seconds: 30),\n      (_) async {\n        if (!isClosed) add(const ReminderEvent.refresh());\n      },\n    );\n  }\n\n  Future<bool> checkReminderAvailable(\n    ReminderPB reminder,\n    Set<String> reminderIds, {\n    Set<String> removeIds = const {},\n  }) async {\n    /// check if schedule time is coming\n    final scheduledAt = reminder.scheduledAt.toDateTime();\n    if (!DateTime.now().isAfter(scheduledAt) && !reminder.isRead) {\n      return false;\n    }\n\n    /// check if view is not null\n    final viewId = reminder.objectId;\n    final view = _allViews.firstWhereOrNull((e) => e.id == viewId);\n    if (view == null) {\n      removeIds.add(reminder.id);\n      return false;\n    }\n\n    if (view.isDatabase) {\n      return true;\n    } else {\n      /// blockId is null means no node\n      final blockId = reminder.meta[ReminderMetaKeys.blockId];\n      if (blockId == null) {\n        removeIds.add(reminder.id);\n        return false;\n      }\n\n      /// check if document is not null\n      final document = await DocumentService()\n          .openDocument(documentId: viewId)\n          .fold((s) => s.toDocument(), (_) => null);\n      if (document == null) {\n        removeIds.add(reminder.id);\n        return false;\n      }\n      Node? searchById(Node current, String id) {\n        if (current.id == id) {\n          return current;\n        }\n        if (current.children.isNotEmpty) {\n          for (final child in current.children) {\n            final node = searchById(child, id);\n\n            if (node != null) {\n              return node;\n            }\n          }\n        }\n        return null;\n      }\n\n      /// check if node is not null\n      final node = searchById(document.root, blockId);\n      if (node == null) {\n        removeIds.add(reminder.id);\n        return false;\n      }\n      final textInserts = node.delta?.whereType<TextInsert>();\n      if (textInserts == null) return false;\n      for (final text in textInserts) {\n        final mention =\n            text.attributes?[MentionBlockKeys.mention] as Map<String, dynamic>?;\n        final reminderId = mention?[MentionBlockKeys.reminderId] as String?;\n        if (reminderIds.contains(reminderId)) {\n          return true;\n        }\n      }\n      removeIds.add(reminder.id);\n      return false;\n    }\n  }\n\n  Future<List<ReminderPB>> filterAvailableReminders(\n    List<ReminderPB> reminders, {\n    bool removeUnavailableReminder = false,\n  }) async {\n    final List<ReminderPB> availableReminders = [];\n    final reminderIds = reminders.map((e) => e.id).toSet();\n    final removeIds = <String>{};\n\n    for (final r in reminders) {\n      if (await checkReminderAvailable(r, reminderIds, removeIds: removeIds)) {\n        availableReminders.add(r);\n      }\n    }\n\n    if (removeUnavailableReminder) {\n      Log.info('Remove unavailable reminder: $removeIds');\n      add(ReminderEvent.removeReminders(removeIds));\n    }\n\n    return availableReminders;\n  }\n}\n\n@freezed\nclass ReminderEvent with _$ReminderEvent {\n  // On startup we fetch all reminders and upcoming ones\n  const factory ReminderEvent.started() = _Started;\n\n  // Remove a reminder\n  const factory ReminderEvent.removeReminder({required String reminderId}) =\n      _RemoveReminder;\n\n  // Remove reminders\n  const factory ReminderEvent.removeReminders(Set<String> reminderIds) =\n      _RemoveReminders;\n\n  // Add a reminder\n  const factory ReminderEvent.add({required ReminderPB reminder}) = _Add;\n\n  // Add a reminder\n  const factory ReminderEvent.addById({\n    required String reminderId,\n    required String objectId,\n    required Int64 scheduledAt,\n    @Default(null) Map<String, String>? meta,\n  }) = _AddById;\n\n  // Update a reminder (eg. isAck, isRead, etc.)\n  const factory ReminderEvent.update(ReminderUpdate update) = _Update;\n\n  // Event to mark specific reminders as read, takes a list of reminder IDs\n  const factory ReminderEvent.markAsRead(List<String> reminderIds) =\n      _MarkAsRead;\n\n  // Event to mark all unread reminders as read\n  const factory ReminderEvent.markAllRead() = _MarkAllRead;\n\n  // Event to archive specific reminders, takes a list of reminder IDs\n  const factory ReminderEvent.archive(List<String> reminderIds) = _Archive;\n\n  // Event to archive all reminders\n  const factory ReminderEvent.archiveAll() = _ArchiveAll;\n\n  // Event to unarchive all reminders\n  const factory ReminderEvent.unarchiveAll() = _UnarchiveAll;\n\n  // Event to handle reminder press action\n  const factory ReminderEvent.pressReminder({\n    required String reminderId,\n    @Default(null) int? path,\n    @Default(null) ViewPB? view,\n  }) = _PressReminder;\n\n  // Event to refresh reminders\n  const factory ReminderEvent.refresh() = _Refresh;\n  const factory ReminderEvent.resetTimer() = _ResetTimer;\n}\n\n/// Object used to merge updates with\n/// a [ReminderPB]\n///\nclass ReminderUpdate {\n  ReminderUpdate({\n    required this.id,\n    this.isAck,\n    this.isRead,\n    this.scheduledAt,\n    this.includeTime,\n    this.isArchived,\n    this.date,\n  });\n\n  final String id;\n  final bool? isAck;\n  final bool? isRead;\n  final DateTime? scheduledAt;\n  final bool? includeTime;\n  final bool? isArchived;\n  final DateTime? date;\n\n  ReminderPB merge({required ReminderPB a}) {\n    final isAcknowledged = isAck == null && scheduledAt != null\n        ? scheduledAt!.isBefore(DateTime.now())\n        : a.isAck;\n\n    final meta = {...a.meta};\n    if (includeTime != a.includeTime) {\n      meta[ReminderMetaKeys.includeTime] = includeTime.toString();\n    }\n\n    if (isArchived != a.isArchived) {\n      meta[ReminderMetaKeys.isArchived] = isArchived.toString();\n    }\n\n    if (date != a.date && date != null) {\n      meta[ReminderMetaKeys.date] = date!.millisecondsSinceEpoch.toString();\n    }\n\n    return ReminderPB(\n      id: a.id,\n      objectId: a.objectId,\n      scheduledAt: scheduledAt != null\n          ? Int64(scheduledAt!.millisecondsSinceEpoch ~/ 1000)\n          : a.scheduledAt,\n      isAck: isAcknowledged,\n      isRead: isRead ?? a.isRead,\n      title: a.title,\n      message: a.message,\n      meta: meta,\n    );\n  }\n}\n\nclass ReminderState {\n  ReminderState({\n    List<ReminderPB>? reminders,\n    this.serverReminders = const [],\n  }) {\n    _reminders = [];\n    pastReminders = [];\n    upcomingReminders = [];\n\n    if (reminders?.isEmpty ?? true) {\n      return;\n    }\n\n    final now = DateTime.now();\n\n    for (final ReminderPB reminder in reminders ?? []) {\n      final scheduledDate = reminder.scheduledAt.toDateTime();\n\n      if (scheduledDate.isBefore(now)) {\n        pastReminders.add(reminder);\n      } else {\n        upcomingReminders.add(reminder);\n      }\n    }\n\n    pastReminders.sort((a, b) => a.scheduledAt.compareTo(b.scheduledAt));\n    upcomingReminders.sort((a, b) => a.scheduledAt.compareTo(b.scheduledAt));\n    _reminders\n        .addAll([...List.of(pastReminders), ...List.of(upcomingReminders)]);\n  }\n\n  late final List<ReminderPB> _reminders;\n  List<ReminderPB> get reminders => _reminders.unique((e) => e.id);\n  List<ReminderPB> get allReminders =>\n      [...serverReminders, ..._reminders].unique((e) => e.id);\n\n  late final List<ReminderPB> pastReminders;\n  late final List<ReminderPB> upcomingReminders;\n  final List<ReminderPB> serverReminders;\n\n  ReminderState copyWith({\n    List<ReminderPB>? reminders,\n    List<ReminderPB>? serverReminders,\n  }) =>\n      ReminderState(\n        reminders: reminders ?? _reminders,\n        serverReminders: serverReminders ?? this.serverReminders,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/reminder/reminder_extension.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\n\nclass ReminderMetaKeys {\n  static String includeTime = \"include_time\";\n  static String blockId = \"block_id\";\n  static String rowId = \"row_id\";\n  static String createdAt = \"created_at\";\n  static String isArchived = \"is_archived\";\n  static String date = \"date\";\n}\n\nenum ReminderType {\n  past,\n  today,\n  other,\n}\n\nextension ReminderExtension on ReminderPB {\n  bool? get includeTime {\n    final String? includeTimeStr = meta[ReminderMetaKeys.includeTime];\n\n    return includeTimeStr != null ? includeTimeStr == true.toString() : null;\n  }\n\n  String? get blockId => meta[ReminderMetaKeys.blockId];\n\n  String? get rowId => meta[ReminderMetaKeys.rowId];\n\n  int? get createdAt {\n    final t = meta[ReminderMetaKeys.createdAt];\n    return t != null ? int.tryParse(t) : null;\n  }\n\n  bool get isArchived {\n    final t = meta[ReminderMetaKeys.isArchived];\n    return t != null ? t == true.toString() : false;\n  }\n\n  DateTime? get date {\n    final t = meta[ReminderMetaKeys.date];\n    return t != null ? DateTime.fromMillisecondsSinceEpoch(int.parse(t)) : null;\n  }\n\n  ReminderType get type {\n    final date = this.date?.millisecondsSinceEpoch;\n\n    if (date == null) {\n      return ReminderType.other;\n    }\n\n    final now = DateTime.now().millisecondsSinceEpoch;\n\n    if (date < now) {\n      return ReminderType.past;\n    }\n\n    final difference = date - now;\n    const oneDayInMilliseconds = 24 * 60 * 60 * 1000;\n\n    if (difference < oneDayInMilliseconds) {\n      return ReminderType.today;\n    }\n\n    return ReminderType.other;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/reminder/reminder_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/user_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass UserAwarenessListener {\n  UserAwarenessListener({\n    required this.workspaceId,\n  });\n\n  final String workspaceId;\n\n  UserNotificationParser? _userParser;\n  StreamSubscription<SubscribeObject>? _subscription;\n  void Function()? onLoadedUserAwareness;\n  void Function(ReminderPB)? onDidUpdateReminder;\n\n  /// [onLoadedUserAwareness] is called when the user awareness is loaded. After this, can\n  /// call fetch reminders releated events\n  ///\n  void start({\n    void Function()? onLoadedUserAwareness,\n    void Function(ReminderPB)? onDidUpdateReminder,\n  }) {\n    this.onLoadedUserAwareness = onLoadedUserAwareness;\n    this.onDidUpdateReminder = onDidUpdateReminder;\n\n    _userParser = UserNotificationParser(\n      id: workspaceId,\n      callback: _userNotificationCallback,\n    );\n\n    _subscription = RustStreamReceiver.listen((observable) {\n      _userParser?.parse(observable);\n    });\n  }\n\n  void stop() {\n    _userParser = null;\n    _subscription?.cancel();\n    _subscription = null;\n  }\n\n  void _userNotificationCallback(\n    UserNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case UserNotification.DidLoadUserAwareness:\n        onLoadedUserAwareness?.call();\n        break;\n      case UserNotification.DidUpdateReminder:\n        result.map((r) => onDidUpdateReminder?.call(ReminderPB.fromBuffer(r)));\n        break;\n      default:\n        break;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/reminder/reminder_service.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\n/// Interface for a Reminder Service that handles\n/// communication to the backend\n///\nabstract class IReminderService {\n  Future<FlowyResult<List<ReminderPB>, FlowyError>> fetchReminders();\n\n  Future<FlowyResult<void, FlowyError>> removeReminder({\n    required String reminderId,\n  });\n\n  Future<FlowyResult<void, FlowyError>> addReminder({\n    required ReminderPB reminder,\n  });\n\n  Future<FlowyResult<void, FlowyError>> updateReminder({\n    required ReminderPB reminder,\n  });\n}\n\nclass ReminderService implements IReminderService {\n  const ReminderService();\n\n  @override\n  Future<FlowyResult<void, FlowyError>> addReminder({\n    required ReminderPB reminder,\n  }) async {\n    final unitOrFailure = await UserEventCreateReminder(reminder).send();\n\n    return unitOrFailure;\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> updateReminder({\n    required ReminderPB reminder,\n  }) async {\n    final unitOrFailure = await UserEventUpdateReminder(reminder).send();\n\n    return unitOrFailure;\n  }\n\n  @override\n  Future<FlowyResult<List<ReminderPB>, FlowyError>> fetchReminders() async {\n    final resultOrFailure = await UserEventGetAllReminders().send();\n\n    return resultOrFailure.fold(\n      (s) => FlowyResult.success(s.items),\n      (e) => FlowyResult.failure(e),\n    );\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> removeReminder({\n    required String reminderId,\n  }) async {\n    final request = ReminderIdentifierPB(id: reminderId);\n    final unitOrFailure = await UserEventRemoveReminder(request).send();\n\n    return unitOrFailure;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/startup/tasks/appflowy_cloud_task.dart';\nimport 'package:appflowy/startup/tasks/deeplink/deeplink_handler.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/application/password/password_http_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'\n    show UserProfilePB;\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'sign_in_bloc.freezed.dart';\n\nclass SignInBloc extends Bloc<SignInEvent, SignInState> {\n  SignInBloc(this.authService) : super(SignInState.initial()) {\n    if (isAppFlowyCloudEnabled) {\n      deepLinkStateListener =\n          getIt<AppFlowyCloudDeepLink>().subscribeDeepLinkLoadingState((value) {\n        if (isClosed) return;\n\n        add(SignInEvent.deepLinkStateChange(value));\n      });\n\n      getAppFlowyCloudUrl().then((baseUrl) {\n        passwordService = PasswordHttpService(\n          baseUrl: baseUrl,\n          authToken:\n              '', // the user is not signed in yet, the auth token should be empty\n        );\n      });\n    }\n\n    on<SignInEvent>(\n      (event, emit) async {\n        await event.when(\n          signInWithEmailAndPassword: (email, password) async =>\n              _onSignInWithEmailAndPassword(\n            emit,\n            email: email,\n            password: password,\n          ),\n          signInWithOAuth: (platform) async => _onSignInWithOAuth(\n            emit,\n            platform: platform,\n          ),\n          signInAsGuest: () async => _onSignInAsGuest(emit),\n          signInWithMagicLink: (email) async => _onSignInWithMagicLink(\n            emit,\n            email: email,\n          ),\n          signInWithPasscode: (email, passcode) async => _onSignInWithPasscode(\n            emit,\n            email: email,\n            passcode: passcode,\n          ),\n          deepLinkStateChange: (result) => _onDeepLinkStateChange(emit, result),\n          cancel: () {\n            emit(\n              state.copyWith(\n                isSubmitting: false,\n                emailError: null,\n                passwordError: null,\n                successOrFail: null,\n              ),\n            );\n          },\n          emailChanged: (email) async {\n            emit(\n              state.copyWith(\n                email: email,\n                emailError: null,\n                successOrFail: null,\n              ),\n            );\n          },\n          passwordChanged: (password) async {\n            emit(\n              state.copyWith(\n                password: password,\n                passwordError: null,\n                successOrFail: null,\n              ),\n            );\n          },\n          switchLoginType: (type) {\n            emit(state.copyWith(loginType: type));\n          },\n          forgotPassword: (email) => _onForgotPassword(emit, email: email),\n          validateResetPasswordToken: (email, token) async =>\n              _onValidateResetPasswordToken(\n            emit,\n            email: email,\n            token: token,\n          ),\n          resetPassword: (email, newPassword) async => _onResetPassword(\n            emit,\n            email: email,\n            newPassword: newPassword,\n          ),\n        );\n      },\n    );\n  }\n\n  final AuthService authService;\n  PasswordHttpService? passwordService;\n  VoidCallback? deepLinkStateListener;\n\n  @override\n  Future<void> close() {\n    deepLinkStateListener?.call();\n    if (isAppFlowyCloudEnabled && deepLinkStateListener != null) {\n      getIt<AppFlowyCloudDeepLink>().unsubscribeDeepLinkLoadingState(\n        deepLinkStateListener!,\n      );\n    }\n    return super.close();\n  }\n\n  Future<void> _onDeepLinkStateChange(\n    Emitter<SignInState> emit,\n    DeepLinkResult result,\n  ) async {\n    final deepLinkState = result.state;\n\n    switch (deepLinkState) {\n      case DeepLinkState.none:\n        break;\n      case DeepLinkState.loading:\n        emit(\n          state.copyWith(\n            isSubmitting: true,\n            emailError: null,\n            passwordError: null,\n            successOrFail: null,\n          ),\n        );\n      case DeepLinkState.finish:\n        final newState = result.result?.fold(\n          (s) => state.copyWith(\n            isSubmitting: false,\n            successOrFail: FlowyResult.success(s),\n          ),\n          (f) => _stateFromCode(f),\n        );\n        if (newState != null) {\n          emit(newState);\n        }\n      case DeepLinkState.error:\n        emit(state.copyWith(isSubmitting: false));\n    }\n  }\n\n  Future<void> _onSignInWithEmailAndPassword(\n    Emitter<SignInState> emit, {\n    required String email,\n    required String password,\n  }) async {\n    emit(\n      state.copyWith(\n        isSubmitting: true,\n        emailError: null,\n        passwordError: null,\n        successOrFail: null,\n      ),\n    );\n    final result = await authService.signInWithEmailPassword(\n      email: email,\n      password: password,\n    );\n    emit(\n      result.fold(\n        (gotrueTokenResponse) {\n          getIt<AppFlowyCloudDeepLink>().passGotrueTokenResponse(\n            gotrueTokenResponse,\n          );\n          return state.copyWith(\n            isSubmitting: false,\n          );\n        },\n        (error) => _stateFromCode(error),\n      ),\n    );\n  }\n\n  Future<void> _onSignInWithOAuth(\n    Emitter<SignInState> emit, {\n    required String platform,\n  }) async {\n    emit(\n      state.copyWith(\n        isSubmitting: true,\n        emailError: null,\n        passwordError: null,\n        successOrFail: null,\n      ),\n    );\n\n    final result = await authService.signUpWithOAuth(platform: platform);\n    emit(\n      result.fold(\n        (userProfile) => state.copyWith(\n          isSubmitting: false,\n          successOrFail: FlowyResult.success(userProfile),\n        ),\n        (error) => _stateFromCode(error),\n      ),\n    );\n  }\n\n  Future<void> _onSignInWithMagicLink(\n    Emitter<SignInState> emit, {\n    required String email,\n  }) async {\n    if (state.isSubmitting) {\n      Log.error('Sign in with magic link is already in progress');\n      return;\n    }\n\n    Log.info('Sign in with magic link: $email');\n\n    emit(\n      state.copyWith(\n        isSubmitting: true,\n        emailError: null,\n        passwordError: null,\n        successOrFail: null,\n      ),\n    );\n\n    final result = await authService.signInWithMagicLink(email: email);\n\n    emit(\n      result.fold(\n        (userProfile) => state.copyWith(\n          isSubmitting: false,\n        ),\n        (error) => _stateFromCode(error),\n      ),\n    );\n  }\n\n  Future<void> _onSignInWithPasscode(\n    Emitter<SignInState> emit, {\n    required String email,\n    required String passcode,\n  }) async {\n    if (state.isSubmitting) {\n      Log.error('Sign in with passcode is already in progress');\n      return;\n    }\n\n    Log.info('Sign in with passcode: $email, $passcode');\n\n    emit(\n      state.copyWith(\n        isSubmitting: true,\n        emailError: null,\n        passwordError: null,\n        successOrFail: null,\n      ),\n    );\n\n    final result = await authService.signInWithPasscode(\n      email: email,\n      passcode: passcode,\n    );\n\n    emit(\n      result.fold(\n        (gotrueTokenResponse) {\n          getIt<AppFlowyCloudDeepLink>().passGotrueTokenResponse(\n            gotrueTokenResponse,\n          );\n          return state.copyWith(\n            isSubmitting: false,\n          );\n        },\n        (error) => _stateFromCode(error),\n      ),\n    );\n  }\n\n  Future<void> _onSignInAsGuest(\n    Emitter<SignInState> emit,\n  ) async {\n    emit(\n      state.copyWith(\n        isSubmitting: true,\n        emailError: null,\n        passwordError: null,\n        successOrFail: null,\n      ),\n    );\n\n    final result = await authService.signUpAsGuest();\n    emit(\n      result.fold(\n        (userProfile) => state.copyWith(\n          isSubmitting: false,\n          successOrFail: FlowyResult.success(userProfile),\n        ),\n        (error) => _stateFromCode(error),\n      ),\n    );\n  }\n\n  Future<void> _onForgotPassword(\n    Emitter<SignInState> emit, {\n    required String email,\n  }) async {\n    if (state.isSubmitting) {\n      Log.error('Forgot password is already in progress');\n      return;\n    }\n\n    emit(\n      state.copyWith(\n        isSubmitting: true,\n        forgotPasswordSuccessOrFail: null,\n        validateResetPasswordTokenSuccessOrFail: null,\n        resetPasswordSuccessOrFail: null,\n      ),\n    );\n\n    final result = await passwordService?.forgotPassword(email: email);\n\n    result?.fold(\n      (success) {\n        emit(\n          state.copyWith(\n            isSubmitting: false,\n            forgotPasswordSuccessOrFail: FlowyResult.success(true),\n          ),\n        );\n      },\n      (error) {\n        emit(\n          state.copyWith(\n            isSubmitting: false,\n            forgotPasswordSuccessOrFail: FlowyResult.failure(error),\n          ),\n        );\n      },\n    );\n  }\n\n  Future<void> _onValidateResetPasswordToken(\n    Emitter<SignInState> emit, {\n    required String email,\n    required String token,\n  }) async {\n    if (state.isSubmitting) {\n      Log.error('Validate reset password token is already in progress');\n      return;\n    }\n\n    Log.info('Validate reset password token: $email, $token');\n\n    emit(\n      state.copyWith(\n        isSubmitting: true,\n        validateResetPasswordTokenSuccessOrFail: null,\n        resetPasswordSuccessOrFail: null,\n      ),\n    );\n\n    final result = await passwordService?.verifyResetPasswordToken(\n      email: email,\n      token: token,\n    );\n\n    result?.fold(\n      (authToken) {\n        Log.info('Validate reset password token success: $authToken');\n\n        passwordService?.authToken = authToken;\n\n        emit(\n          state.copyWith(\n            isSubmitting: false,\n            validateResetPasswordTokenSuccessOrFail: FlowyResult.success(true),\n          ),\n        );\n      },\n      (error) {\n        Log.error('Validate reset password token failed: $error');\n\n        emit(\n          state.copyWith(\n            isSubmitting: false,\n            validateResetPasswordTokenSuccessOrFail: FlowyResult.failure(error),\n          ),\n        );\n      },\n    );\n  }\n\n  Future<void> _onResetPassword(\n    Emitter<SignInState> emit, {\n    required String email,\n    required String newPassword,\n  }) async {\n    if (state.isSubmitting) {\n      Log.error('Reset password is already in progress');\n      return;\n    }\n\n    Log.info('Reset password: $email, ${newPassword.hashCode}');\n\n    emit(\n      state.copyWith(\n        isSubmitting: true,\n        resetPasswordSuccessOrFail: null,\n      ),\n    );\n\n    final result = await passwordService?.setupPassword(\n      newPassword: newPassword,\n    );\n\n    result?.fold(\n      (success) {\n        Log.info('Reset password success');\n        emit(\n          state.copyWith(\n            isSubmitting: false,\n            resetPasswordSuccessOrFail: FlowyResult.success(true),\n          ),\n        );\n      },\n      (error) {\n        Log.error('Reset password failed: $error');\n        emit(\n          state.copyWith(\n            isSubmitting: false,\n            resetPasswordSuccessOrFail: FlowyResult.failure(error),\n          ),\n        );\n      },\n    );\n  }\n\n  SignInState _stateFromCode(FlowyError error) {\n    Log.error('SignInState _stateFromCode: ${error.msg}');\n\n    switch (error.code) {\n      case ErrorCode.EmailFormatInvalid:\n        return state.copyWith(\n          isSubmitting: false,\n          emailError: error.msg,\n          passwordError: null,\n        );\n      case ErrorCode.PasswordFormatInvalid:\n        return state.copyWith(\n          isSubmitting: false,\n          passwordError: error.msg,\n          emailError: null,\n        );\n      case ErrorCode.UserUnauthorized:\n        final errorMsg = error.msg;\n        String msg = LocaleKeys.signIn_generalError.tr();\n        if (errorMsg.contains('rate limit') ||\n            errorMsg.contains('For security purposes')) {\n          msg = LocaleKeys.signIn_tooFrequentVerificationCodeRequest.tr();\n        } else if (errorMsg.contains('invalid')) {\n          msg = LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr();\n        } else if (errorMsg.contains('Invalid login credentials')) {\n          msg = LocaleKeys.signIn_invalidLoginCredentials.tr();\n        }\n        return state.copyWith(\n          isSubmitting: false,\n          successOrFail: FlowyResult.failure(\n            FlowyError(msg: msg),\n          ),\n        );\n      default:\n        return state.copyWith(\n          isSubmitting: false,\n          successOrFail: FlowyResult.failure(\n            FlowyError(msg: LocaleKeys.signIn_generalError.tr()),\n          ),\n        );\n    }\n  }\n}\n\n@freezed\nclass SignInEvent with _$SignInEvent {\n  // Sign in methods\n  const factory SignInEvent.signInWithEmailAndPassword({\n    required String email,\n    required String password,\n  }) = SignInWithEmailAndPassword;\n  const factory SignInEvent.signInWithOAuth({\n    required String platform,\n  }) = SignInWithOAuth;\n  const factory SignInEvent.signInAsGuest() = SignInAsGuest;\n  const factory SignInEvent.signInWithMagicLink({\n    required String email,\n  }) = SignInWithMagicLink;\n  const factory SignInEvent.signInWithPasscode({\n    required String email,\n    required String passcode,\n  }) = SignInWithPasscode;\n\n  // Event handlers\n  const factory SignInEvent.emailChanged({\n    required String email,\n  }) = EmailChanged;\n  const factory SignInEvent.passwordChanged({\n    required String password,\n  }) = PasswordChanged;\n  const factory SignInEvent.deepLinkStateChange(DeepLinkResult result) =\n      DeepLinkStateChange;\n\n  const factory SignInEvent.cancel() = Cancel;\n  const factory SignInEvent.switchLoginType(LoginType type) = SwitchLoginType;\n\n  // password\n  const factory SignInEvent.forgotPassword({\n    required String email,\n  }) = ForgotPassword;\n\n  const factory SignInEvent.validateResetPasswordToken({\n    required String email,\n    required String token,\n  }) = ValidateResetPasswordToken;\n\n  const factory SignInEvent.resetPassword({\n    required String email,\n    required String newPassword,\n  }) = ResetPassword;\n}\n\n// we support sign in directly without sign up, but we want to allow the users to sign up if they want to\n// this type is only for the UI to know which form to show\nenum LoginType {\n  signIn,\n  signUp,\n}\n\n@freezed\nclass SignInState with _$SignInState {\n  const factory SignInState({\n    String? email,\n    String? password,\n    required bool isSubmitting,\n    required String? passwordError,\n    required String? emailError,\n    required FlowyResult<UserProfilePB, FlowyError>? successOrFail,\n    required FlowyResult<bool, FlowyError>? forgotPasswordSuccessOrFail,\n    required FlowyResult<bool, FlowyError>?\n        validateResetPasswordTokenSuccessOrFail,\n    required FlowyResult<bool, FlowyError>? resetPasswordSuccessOrFail,\n    @Default(LoginType.signIn) LoginType loginType,\n  }) = _SignInState;\n\n  factory SignInState.initial() => const SignInState(\n        isSubmitting: false,\n        passwordError: null,\n        emailError: null,\n        successOrFail: null,\n        forgotPasswordSuccessOrFail: null,\n        validateResetPasswordTokenSuccessOrFail: null,\n        resetPasswordSuccessOrFail: null,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/sign_up_bloc.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'\n    show UserProfilePB;\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'sign_up_bloc.freezed.dart';\n\nclass SignUpBloc extends Bloc<SignUpEvent, SignUpState> {\n  SignUpBloc(this.authService) : super(SignUpState.initial()) {\n    _dispatch();\n  }\n\n  final AuthService authService;\n\n  void _dispatch() {\n    on<SignUpEvent>(\n      (event, emit) async {\n        await event.map(\n          signUpWithUserEmailAndPassword: (e) async {\n            await _performActionOnSignUp(emit);\n          },\n          emailChanged: (_EmailChanged value) async {\n            emit(\n              state.copyWith(\n                email: value.email,\n                emailError: null,\n                successOrFail: null,\n              ),\n            );\n          },\n          passwordChanged: (_PasswordChanged value) async {\n            emit(\n              state.copyWith(\n                password: value.password,\n                passwordError: null,\n                successOrFail: null,\n              ),\n            );\n          },\n          repeatPasswordChanged: (_RepeatPasswordChanged value) async {\n            emit(\n              state.copyWith(\n                repeatedPassword: value.password,\n                repeatPasswordError: null,\n                successOrFail: null,\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  Future<void> _performActionOnSignUp(Emitter<SignUpState> emit) async {\n    emit(\n      state.copyWith(\n        isSubmitting: true,\n        successOrFail: null,\n      ),\n    );\n\n    final password = state.password;\n    final repeatedPassword = state.repeatedPassword;\n    if (password == null) {\n      emit(\n        state.copyWith(\n          isSubmitting: false,\n          passwordError: LocaleKeys.signUp_emptyPasswordError.tr(),\n        ),\n      );\n      return;\n    }\n\n    if (repeatedPassword == null) {\n      emit(\n        state.copyWith(\n          isSubmitting: false,\n          repeatPasswordError: LocaleKeys.signUp_repeatPasswordEmptyError.tr(),\n        ),\n      );\n      return;\n    }\n\n    if (password != repeatedPassword) {\n      emit(\n        state.copyWith(\n          isSubmitting: false,\n          repeatPasswordError: LocaleKeys.signUp_unmatchedPasswordError.tr(),\n        ),\n      );\n      return;\n    }\n\n    emit(\n      state.copyWith(\n        passwordError: null,\n        repeatPasswordError: null,\n      ),\n    );\n\n    final result = await authService.signUp(\n      name: state.email ?? '',\n      password: state.password ?? '',\n      email: state.email ?? '',\n    );\n    emit(\n      result.fold(\n        (profile) => state.copyWith(\n          isSubmitting: false,\n          successOrFail: FlowyResult.success(profile),\n          emailError: null,\n          passwordError: null,\n          repeatPasswordError: null,\n        ),\n        (error) => stateFromCode(error),\n      ),\n    );\n  }\n\n  SignUpState stateFromCode(FlowyError error) {\n    switch (error.code) {\n      case ErrorCode.EmailFormatInvalid:\n        return state.copyWith(\n          isSubmitting: false,\n          emailError: error.msg,\n          passwordError: null,\n          successOrFail: null,\n        );\n      case ErrorCode.PasswordFormatInvalid:\n        return state.copyWith(\n          isSubmitting: false,\n          passwordError: error.msg,\n          emailError: null,\n          successOrFail: null,\n        );\n      default:\n        return state.copyWith(\n          isSubmitting: false,\n          successOrFail: FlowyResult.failure(error),\n        );\n    }\n  }\n}\n\n@freezed\nclass SignUpEvent with _$SignUpEvent {\n  const factory SignUpEvent.signUpWithUserEmailAndPassword() =\n      SignUpWithUserEmailAndPassword;\n  const factory SignUpEvent.emailChanged(String email) = _EmailChanged;\n  const factory SignUpEvent.passwordChanged(String password) = _PasswordChanged;\n  const factory SignUpEvent.repeatPasswordChanged(String password) =\n      _RepeatPasswordChanged;\n}\n\n@freezed\nclass SignUpState with _$SignUpState {\n  const factory SignUpState({\n    String? email,\n    String? password,\n    String? repeatedPassword,\n    required bool isSubmitting,\n    required String? passwordError,\n    required String? repeatPasswordError,\n    required String? emailError,\n    required FlowyResult<UserProfilePB, FlowyError>? successOrFail,\n  }) = _SignUpState;\n\n  factory SignUpState.initial() => const SignUpState(\n        isSubmitting: false,\n        passwordError: null,\n        repeatPasswordError: null,\n        emailError: null,\n        successOrFail: null,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/splash_bloc.dart",
    "content": "import 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/domain/auth_state.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'splash_bloc.freezed.dart';\n\nclass SplashBloc extends Bloc<SplashEvent, SplashState> {\n  SplashBloc() : super(SplashState.initial()) {\n    on<SplashEvent>((event, emit) async {\n      await event.map(\n        getUser: (val) async {\n          final response = await getIt<AuthService>().getUser();\n          final authState = response.fold(\n            (user) => AuthState.authenticated(user),\n            (error) => AuthState.unauthenticated(error),\n          );\n          emit(state.copyWith(auth: authState));\n        },\n      );\n    });\n  }\n}\n\n@freezed\nclass SplashEvent with _$SplashEvent {\n  const factory SplashEvent.getUser() = _GetUser;\n}\n\n@freezed\nclass SplashState with _$SplashState {\n  const factory SplashState({\n    required AuthState auth,\n  }) = _SplashState;\n\n  factory SplashState.initial() => const SplashState(\n        auth: AuthState.initial(),\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/user_auth_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/user_notification.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/notification.pb.dart'\n    as user;\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass UserAuthStateListener {\n  void Function(String)? _onInvalidAuth;\n  void Function()? _didSignIn;\n  StreamSubscription<SubscribeObject>? _subscription;\n  UserNotificationParser? _userParser;\n\n  void start({\n    void Function(String)? onInvalidAuth,\n    void Function()? didSignIn,\n  }) {\n    _onInvalidAuth = onInvalidAuth;\n    _didSignIn = didSignIn;\n\n    _userParser = UserNotificationParser(\n      id: \"auth_state_change_notification\",\n      callback: _userNotificationCallback,\n    );\n    _subscription = RustStreamReceiver.listen((observable) {\n      _userParser?.parse(observable);\n    });\n  }\n\n  Future<void> stop() async {\n    _userParser = null;\n    await _subscription?.cancel();\n    _onInvalidAuth = null;\n  }\n\n  void _userNotificationCallback(\n    user.UserNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case user.UserNotification.UserAuthStateChanged:\n        result.fold(\n          (payload) {\n            final pb = AuthStateChangedPB.fromBuffer(payload);\n            switch (pb.state) {\n              case AuthStatePB.AuthStateSignIn:\n                _didSignIn?.call();\n                break;\n              case AuthStatePB.InvalidAuth:\n                _onInvalidAuth?.call(pb.message);\n                break;\n              default:\n                break;\n            }\n          },\n          (r) => Log.error(r),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/user_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/folder_notification.dart';\nimport 'package:appflowy/core/notification/user_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/notification.pb.dart'\n    as user;\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flowy_infra/notifier.dart';\nimport 'package:flutter/foundation.dart';\n\ntypedef DidUpdateUserWorkspaceCallback = void Function(\n  UserWorkspacePB workspace,\n);\ntypedef DidUpdateUserWorkspacesCallback = void Function(\n  RepeatedUserWorkspacePB workspaces,\n);\ntypedef UserProfileNotifyValue = FlowyResult<UserProfilePB, FlowyError>;\ntypedef DidUpdateUserWorkspaceSetting = void Function(\n  WorkspaceSettingsPB settings,\n);\n\nclass UserListener {\n  UserListener({\n    required UserProfilePB userProfile,\n  }) : _userProfile = userProfile;\n\n  final UserProfilePB _userProfile;\n\n  UserNotificationParser? _userParser;\n  StreamSubscription<SubscribeObject>? _subscription;\n  PublishNotifier<UserProfileNotifyValue>? _profileNotifier = PublishNotifier();\n\n  /// Update notification about _all_ of the users workspaces\n  ///\n  DidUpdateUserWorkspacesCallback? onUserWorkspaceListUpdated;\n\n  /// Update notification about _one_ workspace\n  ///\n  DidUpdateUserWorkspaceCallback? onUserWorkspaceUpdated;\n  DidUpdateUserWorkspaceSetting? onUserWorkspaceSettingUpdated;\n  DidUpdateUserWorkspaceCallback? onUserWorkspaceOpened;\n  void start({\n    void Function(UserProfileNotifyValue)? onProfileUpdated,\n    DidUpdateUserWorkspacesCallback? onUserWorkspaceListUpdated,\n    void Function(UserWorkspacePB)? onUserWorkspaceUpdated,\n    DidUpdateUserWorkspaceSetting? onUserWorkspaceSettingUpdated,\n  }) {\n    if (onProfileUpdated != null) {\n      _profileNotifier?.addPublishListener(onProfileUpdated);\n    }\n\n    this.onUserWorkspaceListUpdated = onUserWorkspaceListUpdated;\n    this.onUserWorkspaceUpdated = onUserWorkspaceUpdated;\n    this.onUserWorkspaceSettingUpdated = onUserWorkspaceSettingUpdated;\n    _userParser = UserNotificationParser(\n      id: _userProfile.id.toString(),\n      callback: _userNotificationCallback,\n    );\n    _subscription = RustStreamReceiver.listen((observable) {\n      _userParser?.parse(observable);\n    });\n  }\n\n  Future<void> stop() async {\n    _userParser = null;\n    await _subscription?.cancel();\n    _profileNotifier?.dispose();\n    _profileNotifier = null;\n  }\n\n  void _userNotificationCallback(\n    user.UserNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case user.UserNotification.DidUpdateUserProfile:\n        result.fold(\n          (payload) => _profileNotifier?.value =\n              FlowyResult.success(UserProfilePB.fromBuffer(payload)),\n          (error) => _profileNotifier?.value = FlowyResult.failure(error),\n        );\n        break;\n      case user.UserNotification.DidUpdateUserWorkspaces:\n        result.map(\n          (r) {\n            final value = RepeatedUserWorkspacePB.fromBuffer(r);\n            onUserWorkspaceListUpdated?.call(value);\n          },\n        );\n        break;\n      case user.UserNotification.DidUpdateUserWorkspace:\n        result.map(\n          (r) => onUserWorkspaceUpdated?.call(UserWorkspacePB.fromBuffer(r)),\n        );\n      case user.UserNotification.DidUpdateWorkspaceSetting:\n        result.map(\n          (r) => onUserWorkspaceSettingUpdated\n              ?.call(WorkspaceSettingsPB.fromBuffer(r)),\n        );\n        break;\n      case user.UserNotification.DidOpenWorkspace:\n        result.fold(\n          (payload) => _profileNotifier?.value =\n              FlowyResult.success(UserProfilePB.fromBuffer(payload)),\n          (error) => _profileNotifier?.value = FlowyResult.failure(error),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n}\n\ntypedef WorkspaceLatestNotifyValue = FlowyResult<WorkspaceLatestPB, FlowyError>;\n\nclass FolderListener {\n  FolderListener({\n    required this.workspaceId,\n  });\n\n  final String workspaceId;\n\n  final PublishNotifier<WorkspaceLatestNotifyValue> _latestChangedNotifier =\n      PublishNotifier();\n\n  FolderNotificationListener? _listener;\n\n  void start({\n    void Function(WorkspaceLatestNotifyValue)? onLatestUpdated,\n  }) {\n    if (onLatestUpdated != null) {\n      _latestChangedNotifier.addPublishListener(onLatestUpdated);\n    }\n\n    _listener = FolderNotificationListener(\n      objectId: workspaceId,\n      handler: _handleObservableType,\n    );\n  }\n\n  void _handleObservableType(\n    FolderNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case FolderNotification.DidUpdateWorkspaceSetting:\n        result.fold(\n          (payload) => _latestChangedNotifier.value =\n              FlowyResult.success(WorkspaceLatestPB.fromBuffer(payload)),\n          (error) => _latestChangedNotifier.value = FlowyResult.failure(error),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _listener?.stop();\n    _latestChangedNotifier.dispose();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/user_service.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flutter/foundation.dart';\n\nabstract class IUserBackendService {\n  Future<FlowyResult<void, FlowyError>> cancelSubscription(\n    String workspaceId,\n    SubscriptionPlanPB plan,\n    String? reason,\n  );\n  Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(\n    String workspaceId,\n    SubscriptionPlanPB plan,\n  );\n}\n\nconst _baseBetaUrl = 'https://beta.appflowy.com';\nconst _baseProdUrl = 'https://appflowy.com';\n\nclass UserBackendService implements IUserBackendService {\n  UserBackendService({required this.userId});\n\n  final Int64 userId;\n\n  static Future<FlowyResult<UserProfilePB, FlowyError>>\n      getCurrentUserProfile() async {\n    final result = await UserEventGetUserProfile().send();\n    return result;\n  }\n\n  Future<FlowyResult<void, FlowyError>> updateUserProfile({\n    String? name,\n    String? password,\n    String? email,\n    String? iconUrl,\n  }) {\n    final payload = UpdateUserProfilePayloadPB.create()..id = userId;\n\n    if (name != null) {\n      payload.name = name;\n    }\n\n    if (password != null) {\n      payload.password = password;\n    }\n\n    if (email != null) {\n      payload.email = email;\n    }\n\n    if (iconUrl != null) {\n      payload.iconUrl = iconUrl;\n    }\n\n    return UserEventUpdateUserProfile(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> deleteWorkspace({\n    required String workspaceId,\n  }) {\n    throw UnimplementedError();\n  }\n\n  static Future<FlowyResult<UserProfilePB, FlowyError>> signInWithMagicLink(\n    String email,\n    String redirectTo,\n  ) async {\n    final payload = MagicLinkSignInPB(email: email, redirectTo: redirectTo);\n    return UserEventMagicLinkSignIn(payload).send();\n  }\n\n  static Future<FlowyResult<GotrueTokenResponsePB, FlowyError>>\n      signInWithPasscode(\n    String email,\n    String passcode,\n  ) async {\n    final payload = PasscodeSignInPB(email: email, passcode: passcode);\n    return UserEventPasscodeSignIn(payload).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> signInWithPassword(\n    String email,\n    String password,\n  ) {\n    final payload = SignInPayloadPB(\n      email: email,\n      password: password,\n    );\n    return UserEventSignInWithEmailPassword(payload).send();\n  }\n\n  static Future<FlowyResult<void, FlowyError>> signOut() {\n    return UserEventSignOut().send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> initUser() async {\n    return UserEventInitUser().send();\n  }\n\n  static Future<FlowyResult<UserProfilePB, FlowyError>> getAnonUser() async {\n    return UserEventGetAnonUser().send();\n  }\n\n  static Future<FlowyResult<void, FlowyError>> openAnonUser() async {\n    return UserEventOpenAnonUser().send();\n  }\n\n  Future<FlowyResult<List<UserWorkspacePB>, FlowyError>> getWorkspaces() {\n    return UserEventGetAllWorkspace().send().then((value) {\n      return value.fold(\n        (workspaces) => FlowyResult.success(workspaces.items),\n        (error) => FlowyResult.failure(error),\n      );\n    });\n  }\n\n  static Future<FlowyResult<UserWorkspacePB, FlowyError>> getWorkspaceById(\n    String workspaceId,\n  ) async {\n    final result = await UserEventGetAllWorkspace().send();\n    return result.fold(\n      (workspaces) {\n        final workspace = workspaces.items.firstWhere(\n          (workspace) => workspace.workspaceId == workspaceId,\n        );\n        return FlowyResult.success(workspace);\n      },\n      (error) => FlowyResult.failure(error),\n    );\n  }\n\n  Future<FlowyResult<void, FlowyError>> openWorkspace(\n    String workspaceId,\n    WorkspaceTypePB workspaceType,\n  ) {\n    final payload = OpenUserWorkspacePB()\n      ..workspaceId = workspaceId\n      ..workspaceType = workspaceType;\n    return UserEventOpenWorkspace(payload).send();\n  }\n\n  static Future<FlowyResult<WorkspacePB, FlowyError>> getCurrentWorkspace() {\n    return FolderEventReadCurrentWorkspace().send().then((result) {\n      return result.fold(\n        (workspace) => FlowyResult.success(workspace),\n        (error) => FlowyResult.failure(error),\n      );\n    });\n  }\n\n  Future<FlowyResult<UserWorkspacePB, FlowyError>> createUserWorkspace(\n    String name,\n    WorkspaceTypePB workspaceType,\n  ) {\n    final request = CreateWorkspacePB.create()\n      ..name = name\n      ..workspaceType = workspaceType;\n    return UserEventCreateWorkspace(request).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> deleteWorkspaceById(\n    String workspaceId,\n  ) {\n    final request = UserWorkspaceIdPB.create()..workspaceId = workspaceId;\n    return UserEventDeleteWorkspace(request).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> renameWorkspace(\n    String workspaceId,\n    String name,\n  ) {\n    final request = RenameWorkspacePB()\n      ..workspaceId = workspaceId\n      ..newName = name;\n    return UserEventRenameWorkspace(request).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> updateWorkspaceIcon(\n    String workspaceId,\n    String icon,\n  ) {\n    final request = ChangeWorkspaceIconPB()\n      ..workspaceId = workspaceId\n      ..newIcon = icon;\n    return UserEventChangeWorkspaceIcon(request).send();\n  }\n\n  Future<FlowyResult<RepeatedWorkspaceMemberPB, FlowyError>>\n      getWorkspaceMembers(\n    String workspaceId,\n  ) async {\n    final data = QueryWorkspacePB()..workspaceId = workspaceId;\n    return UserEventGetWorkspaceMembers(data).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> addWorkspaceMember(\n    String workspaceId,\n    String email,\n  ) async {\n    final data = AddWorkspaceMemberPB()\n      ..workspaceId = workspaceId\n      ..email = email;\n    return UserEventAddWorkspaceMember(data).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> inviteWorkspaceMember(\n    String workspaceId,\n    String email, {\n    AFRolePB? role,\n  }) async {\n    final data = WorkspaceMemberInvitationPB()\n      ..workspaceId = workspaceId\n      ..inviteeEmail = email;\n    if (role != null) {\n      data.role = role;\n    }\n    return UserEventInviteWorkspaceMember(data).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> removeWorkspaceMember(\n    String workspaceId,\n    String email,\n  ) async {\n    final data = RemoveWorkspaceMemberPB()\n      ..workspaceId = workspaceId\n      ..email = email;\n    return UserEventRemoveWorkspaceMember(data).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> updateWorkspaceMember(\n    String workspaceId,\n    String email,\n    AFRolePB role,\n  ) async {\n    final data = UpdateWorkspaceMemberPB()\n      ..workspaceId = workspaceId\n      ..email = email\n      ..role = role;\n    return UserEventUpdateWorkspaceMember(data).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> leaveWorkspace(\n    String workspaceId,\n  ) async {\n    final data = UserWorkspaceIdPB.create()..workspaceId = workspaceId;\n    return UserEventLeaveWorkspace(data).send();\n  }\n\n  static Future<FlowyResult<WorkspaceSubscriptionInfoPB, FlowyError>>\n      getWorkspaceSubscriptionInfo(String workspaceId) {\n    final params = UserWorkspaceIdPB.create()..workspaceId = workspaceId;\n    return UserEventGetWorkspaceSubscriptionInfo(params).send();\n  }\n\n  @override\n  Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(\n    String workspaceId,\n    SubscriptionPlanPB plan,\n  ) {\n    final request = SubscribeWorkspacePB()\n      ..workspaceId = workspaceId\n      ..recurringInterval = RecurringIntervalPB.Year\n      ..workspaceSubscriptionPlan = plan\n      ..successUrl =\n          '${kDebugMode ? _baseBetaUrl : _baseProdUrl}/after-payment?plan=${plan.toRecognizable()}';\n    return UserEventSubscribeWorkspace(request).send();\n  }\n\n  @override\n  Future<FlowyResult<void, FlowyError>> cancelSubscription(\n    String workspaceId,\n    SubscriptionPlanPB plan, [\n    String? reason,\n  ]) {\n    final request = CancelWorkspaceSubscriptionPB()\n      ..workspaceId = workspaceId\n      ..plan = plan;\n\n    if (reason != null) {\n      request.reason = reason;\n    }\n\n    return UserEventCancelWorkspaceSubscription(request).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> updateSubscriptionPeriod(\n    String workspaceId,\n    SubscriptionPlanPB plan,\n    RecurringIntervalPB interval,\n  ) {\n    final request = UpdateWorkspaceSubscriptionPaymentPeriodPB()\n      ..workspaceId = workspaceId\n      ..plan = plan\n      ..recurringInterval = interval;\n\n    return UserEventUpdateWorkspaceSubscriptionPaymentPeriod(request).send();\n  }\n\n  // NOTE: This function is irreversible and will delete the current user's account.\n  static Future<FlowyResult<void, FlowyError>> deleteCurrentAccount() {\n    return UserEventDeleteAccount().send();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/user_settings_service.dart",
    "content": "import 'package:appflowy_backend/appflowy_backend.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass UserSettingsBackendService {\n  const UserSettingsBackendService();\n\n  Future<AppearanceSettingsPB> getAppearanceSetting() async {\n    final result = await UserEventGetAppearanceSetting().send();\n\n    return result.fold(\n      (AppearanceSettingsPB setting) => setting,\n      (error) =>\n          throw FlowySDKException(ExceptionType.AppearanceSettingsIsEmpty),\n    );\n  }\n\n  Future<FlowyResult<UserSettingPB, FlowyError>> getUserSetting() {\n    return UserEventGetUserSetting().send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> setAppearanceSetting(\n    AppearanceSettingsPB setting,\n  ) {\n    return UserEventSetAppearanceSetting(setting).send();\n  }\n\n  Future<DateTimeSettingsPB> getDateTimeSettings() async {\n    final result = await UserEventGetDateTimeSettings().send();\n\n    return result.fold(\n      (DateTimeSettingsPB setting) => setting,\n      (error) =>\n          throw FlowySDKException(ExceptionType.AppearanceSettingsIsEmpty),\n    );\n  }\n\n  Future<FlowyResult<void, FlowyError>> setDateTimeSettings(\n    DateTimeSettingsPB settings,\n  ) async {\n    return UserEventSetDateTimeSettings(settings).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> setNotificationSettings(\n    NotificationSettingsPB settings,\n  ) async {\n    return UserEventSetNotificationSettings(settings).send();\n  }\n\n  Future<NotificationSettingsPB> getNotificationSettings() async {\n    final result = await UserEventGetNotificationSettings().send();\n\n    return result.fold(\n      (NotificationSettingsPB setting) => setting,\n      (error) =>\n          throw FlowySDKException(ExceptionType.AppearanceSettingsIsEmpty),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart",
    "content": "import 'package:appflowy/plugins/database/application/defines.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'workspace_error_bloc.freezed.dart';\n\nclass WorkspaceErrorBloc\n    extends Bloc<WorkspaceErrorEvent, WorkspaceErrorState> {\n  WorkspaceErrorBloc({required this.userFolder, required FlowyError error})\n      : super(WorkspaceErrorState.initial(error)) {\n    _dispatch();\n  }\n\n  final UserFolderPB userFolder;\n\n  void _dispatch() {\n    on<WorkspaceErrorEvent>(\n      (event, emit) async {\n        event.when(\n          init: () {\n            // _loadSnapshots();\n          },\n          didResetWorkspace: (result) {\n            result.fold(\n              (_) {\n                emit(\n                  state.copyWith(\n                    loadingState: LoadingState.finish(result),\n                    workspaceState: const WorkspaceState.reset(),\n                  ),\n                );\n              },\n              (err) {\n                emit(state.copyWith(loadingState: LoadingState.finish(result)));\n              },\n            );\n          },\n          logout: () {\n            emit(\n              state.copyWith(\n                workspaceState: const WorkspaceState.logout(),\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\n@freezed\nclass WorkspaceErrorEvent with _$WorkspaceErrorEvent {\n  const factory WorkspaceErrorEvent.init() = _Init;\n  const factory WorkspaceErrorEvent.logout() = _DidLogout;\n  const factory WorkspaceErrorEvent.didResetWorkspace(\n    FlowyResult<void, FlowyError> result,\n  ) = _DidResetWorkspace;\n}\n\n@freezed\nclass WorkspaceErrorState with _$WorkspaceErrorState {\n  const factory WorkspaceErrorState({\n    required FlowyError initialError,\n    LoadingState? loadingState,\n    required WorkspaceState workspaceState,\n  }) = _WorkspaceErrorState;\n\n  factory WorkspaceErrorState.initial(FlowyError error) => WorkspaceErrorState(\n        initialError: error,\n        workspaceState: const WorkspaceState.initial(),\n      );\n}\n\n@freezed\nclass WorkspaceState with _$WorkspaceState {\n  const factory WorkspaceState.initial() = _Initial;\n  const factory WorkspaceState.logout() = _Logout;\n  const factory WorkspaceState.reset() = _Reset;\n  const factory WorkspaceState.createNewWorkspace() = _NewWorkspace;\n  const factory WorkspaceState.restoreFromSnapshot() = _RestoreFromSnapshot;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/domain/auth_state.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'\n    show UserProfilePB;\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\npart 'auth_state.freezed.dart';\n\n@freezed\nclass AuthState with _$AuthState {\n  const factory AuthState.authenticated(UserProfilePB userProfile) =\n      Authenticated;\n  const factory AuthState.unauthenticated(FlowyError error) = Unauthenticated;\n  const factory AuthState.initial() = _Initial;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/anon_user.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/application/anon_user_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass AnonUserList extends StatelessWidget {\n  const AnonUserList({required this.didOpenUser, super.key});\n\n  final VoidCallback didOpenUser;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => AnonUserBloc()\n        ..add(\n          const AnonUserEvent.initial(),\n        ),\n      child: BlocBuilder<AnonUserBloc, AnonUserState>(\n        builder: (context, state) {\n          if (state.anonUsers.isEmpty) {\n            return const SizedBox.shrink();\n          } else {\n            return Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Opacity(\n                  opacity: 0.6,\n                  child: FlowyText.regular(\n                    LocaleKeys.settings_menu_historicalUserListTooltip.tr(),\n                    fontSize: 13,\n                    maxLines: null,\n                  ),\n                ),\n                const VSpace(6),\n                Expanded(\n                  child: ListView.builder(\n                    itemBuilder: (context, index) {\n                      final user = state.anonUsers[index];\n                      return AnonUserItem(\n                        key: ValueKey(user.id),\n                        user: user,\n                        isSelected: false,\n                        didOpenUser: didOpenUser,\n                      );\n                    },\n                    itemCount: state.anonUsers.length,\n                  ),\n                ),\n              ],\n            );\n          }\n        },\n      ),\n    );\n  }\n}\n\nclass AnonUserItem extends StatelessWidget {\n  const AnonUserItem({\n    super.key,\n    required this.user,\n    required this.isSelected,\n    required this.didOpenUser,\n  });\n\n  final UserProfilePB user;\n  final bool isSelected;\n  final VoidCallback didOpenUser;\n\n  @override\n  Widget build(BuildContext context) {\n    final icon = isSelected ? const FlowySvg(FlowySvgs.check_s) : null;\n    final isDisabled = isSelected || user.userAuthType != AuthTypePB.Local;\n    final desc = \"${user.name}\\t ${user.userAuthType}\\t\";\n    final child = SizedBox(\n      height: 30,\n      child: FlowyButton(\n        disable: isDisabled,\n        text: FlowyText.medium(\n          desc,\n          fontSize: 12,\n        ),\n        rightIcon: icon,\n        onTap: () {\n          context.read<AnonUserBloc>().add(AnonUserEvent.openAnonUser(user));\n          didOpenUser();\n        },\n      ),\n    );\n    return child;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart",
    "content": "import 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/presentation/router.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:flutter/material.dart';\n\nvoid handleOpenWorkspaceError(BuildContext context, FlowyError error) {\n  Log.error(error);\n  switch (error.code) {\n    case ErrorCode.WorkspaceDataNotSync:\n      final userFolder = UserFolderPB.fromBuffer(error.payload);\n      getIt<AuthRouter>().pushWorkspaceErrorScreen(context, userFolder, error);\n      break;\n    case ErrorCode.InvalidEncryptSecret:\n    case ErrorCode.NetworkError:\n      showToastNotification(\n        message: error.msg,\n        type: ToastificationType.error,\n      );\n      break;\n    default:\n      showToastNotification(\n        message: error.msg,\n        type: ToastificationType.error,\n        callbacks: ToastificationCallbacks(\n          onDismissed: (_) {\n            getIt<AuthService>().signOut();\n            runAppFlowy();\n          },\n        ),\n      );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart",
    "content": "export 'handle_open_workspace_error.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/presentation.dart",
    "content": "export 'screens/screens.dart';\nexport 'widgets/widgets.dart';\nexport 'anon_user.dart';\nexport 'router.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/router.dart",
    "content": "import 'package:appflowy/mobile/presentation/home/mobile_home_page.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/presentation/screens/screens.dart';\nimport 'package:appflowy/workspace/presentation/home/desktop_home_screen.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'\n    show UserProfilePB;\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass AuthRouter {\n  void pushForgetPasswordScreen(BuildContext context) {}\n\n  void pushWorkspaceStartScreen(\n    BuildContext context,\n    UserProfilePB userProfile,\n  ) {\n    getIt<SplashRouter>().pushWorkspaceStartScreen(context, userProfile);\n  }\n\n  /// Navigates to the home screen based on the current workspace and platform.\n  ///\n  /// This function takes in a [BuildContext] and a [UserProfilePB] object to\n  /// determine the user's settings and then navigate to the appropriate home screen\n  /// (`MobileHomeScreen` for mobile platforms, `DesktopHomeScreen` for others).\n  ///\n  /// It first fetches the current workspace settings using [FolderEventGetCurrentWorkspace].\n  /// If the workspace settings are successfully fetched, it navigates to the home screen.\n  /// If there's an error, it defaults to the workspace start screen.\n  ///\n  /// @param [context] BuildContext for navigating to the appropriate screen.\n  /// @param [userProfile] UserProfilePB object containing the details of the current user.\n  ///\n  Future<void> goHomeScreen(\n    BuildContext context,\n    UserProfilePB userProfile,\n  ) async {\n    final result = await FolderEventGetCurrentWorkspaceSetting().send();\n    result.fold(\n      (workspaceSetting) {\n        // Replace SignInScreen or SkipLogInScreen as root page.\n        // If user click back button, it will exit app rather than go back to SignInScreen or SkipLogInScreen\n        if (UniversalPlatform.isMobile) {\n          context.go(\n            MobileHomeScreen.routeName,\n          );\n        } else {\n          context.go(\n            DesktopHomeScreen.routeName,\n          );\n        }\n      },\n      (error) => pushWorkspaceStartScreen(context, userProfile),\n    );\n  }\n\n  Future<void> pushWorkspaceErrorScreen(\n    BuildContext context,\n    UserFolderPB userFolder,\n    FlowyError error,\n  ) async {\n    await context.push(\n      WorkspaceErrorScreen.routeName,\n      extra: {\n        WorkspaceErrorScreen.argUserFolder: userFolder,\n        WorkspaceErrorScreen.argError: error,\n      },\n    );\n  }\n}\n\nclass SplashRouter {\n  // Unused for now, it was planed to be used in SignUpScreen.\n  // To let user choose workspace than navigate to corresponding home screen.\n  Future<void> pushWorkspaceStartScreen(\n    BuildContext context,\n    UserProfilePB userProfile,\n  ) async {\n    await context.push(\n      WorkspaceStartScreen.routeName,\n      extra: {\n        WorkspaceStartScreen.argUserProfile: userProfile,\n      },\n    );\n\n    final result = await FolderEventGetCurrentWorkspaceSetting().send();\n    result.fold(\n      (workspaceSettingPB) => pushHomeScreen(context),\n      (r) => null,\n    );\n  }\n\n  void pushHomeScreen(\n    BuildContext context,\n  ) {\n    if (UniversalPlatform.isMobile) {\n      context.push(\n        MobileHomeScreen.routeName,\n      );\n    } else {\n      context.push(\n        DesktopHomeScreen.routeName,\n      );\n    }\n  }\n\n  void goHomeScreen(\n    BuildContext context,\n  ) {\n    if (UniversalPlatform.isMobile) {\n      context.go(\n        MobileHomeScreen.routeName,\n      );\n    } else {\n      context.go(\n        DesktopHomeScreen.routeName,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart",
    "content": "export 'sign_in_screen/sign_in_screen.dart';\nexport 'skip_log_in_screen.dart';\nexport 'splash_screen.dart';\nexport 'workspace_error_screen.dart';\nexport 'workspace_start_screen/workspace_start_screen.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart",
    "content": "import 'package:appflowy/core/frameless_window.dart';\nimport 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/settings/show_settings.dart';\nimport 'package:appflowy/shared/window_title_bar.dart';\nimport 'package:appflowy/user/application/sign_in_bloc.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';\nimport 'package:appflowy/user/presentation/widgets/widgets.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\nimport 'package:window_manager/window_manager.dart';\n\nclass DesktopSignInScreen extends StatefulWidget {\n  const DesktopSignInScreen({\n    super.key,\n  });\n\n  @override\n  State<DesktopSignInScreen> createState() => _DesktopSignInScreenState();\n}\n\nclass _DesktopSignInScreenState extends State<DesktopSignInScreen>\n    with WindowListener {\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return BlocBuilder<SignInBloc, SignInState>(\n      builder: (context, state) {\n        final bottomPadding = UniversalPlatform.isDesktop ? 20.0 : 24.0;\n        return Scaffold(\n          appBar: _buildAppBar(),\n          body: Center(\n            child: AuthFormContainer(\n              children: [\n                const Spacer(),\n\n                // logo and title\n                FlowyLogoTitle(\n                  title: LocaleKeys.welcomeText.tr(),\n                  logoSize: Size.square(36),\n                ),\n                VSpace(theme.spacing.xxl),\n\n                // continue with email and password\n                isLocalAuthEnabled\n                    ? const SignInAnonymousButtonV3()\n                    : const ContinueWithEmailAndPassword(),\n\n                VSpace(theme.spacing.xxl),\n\n                // third-party sign in.\n                if (isAuthEnabled) ...[\n                  const _OrDivider(),\n                  VSpace(theme.spacing.xxl),\n                  const ThirdPartySignInButtons(),\n                  VSpace(theme.spacing.xxl),\n                ],\n\n                // sign in agreement\n                const SignInAgreement(),\n\n                const Spacer(),\n\n                // anonymous sign in and settings\n                const Row(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    DesktopSignInSettingsButton(),\n                    HSpace(20),\n                    SignInAnonymousButtonV2(),\n                  ],\n                ),\n                VSpace(bottomPadding),\n              ],\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  PreferredSize _buildAppBar() {\n    return PreferredSize(\n      preferredSize: Size.fromHeight(UniversalPlatform.isWindows ? 40 : 60),\n      child: UniversalPlatform.isWindows\n          ? const WindowTitleBar()\n          : const MoveWindowDetector(),\n    );\n  }\n\n  @override\n  void onWindowFocus() {\n    // https://pub.dev/packages/window_manager#windows\n    // must call setState once when the window is focused\n    setState(() {});\n  }\n}\n\nclass DesktopSignInSettingsButton extends StatelessWidget {\n  const DesktopSignInSettingsButton({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return AFGhostIconTextButton(\n      text: LocaleKeys.signIn_settings.tr(),\n      textColor: (context, isHovering, disabled) {\n        return theme.textColorScheme.secondary;\n      },\n      size: AFButtonSize.s,\n      padding: EdgeInsets.symmetric(\n        horizontal: theme.spacing.m,\n        vertical: theme.spacing.xs,\n      ),\n      onTap: () => showSimpleSettingsDialog(context),\n      iconBuilder: (context, isHovering, disabled) {\n        return FlowySvg(\n          FlowySvgs.settings_s,\n          size: Size.square(20),\n          color: theme.textColorScheme.secondary,\n        );\n      },\n    );\n  }\n}\n\nclass _OrDivider extends StatelessWidget {\n  const _OrDivider();\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Row(\n      children: [\n        Flexible(\n          child: AFDivider(),\n        ),\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 10),\n          child: Text(\n            LocaleKeys.signIn_or.tr(),\n            style: theme.textStyle.body.standard(\n              color: theme.textColorScheme.secondary,\n            ),\n          ),\n        ),\n        Flexible(\n          child: AFDivider(),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_loading_screen.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';\nimport 'package:appflowy/user/application/sign_in_bloc.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MobileLoadingScreen extends StatelessWidget {\n  const MobileLoadingScreen({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    const double spacing = 16;\n\n    return Scaffold(\n      appBar: FlowyAppBar(\n        showDivider: false,\n        onTapLeading: () => context.read<SignInBloc>().add(\n              const SignInEvent.cancel(),\n            ),\n      ),\n      body: Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            FlowyText(LocaleKeys.signIn_signingInText.tr()),\n            const VSpace(spacing),\n            const CircularProgressIndicator(),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/setting/launch_settings_page.dart';\nimport 'package:appflowy/user/application/sign_in_bloc.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';\nimport 'package:appflowy/user/presentation/widgets/flowy_logo_title.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nclass MobileSignInScreen extends StatelessWidget {\n  const MobileSignInScreen({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SignInBloc, SignInState>(\n      builder: (context, state) {\n        final theme = AppFlowyTheme.of(context);\n        return Scaffold(\n          resizeToAvoidBottomInset: false,\n          body: Padding(\n            padding: const EdgeInsets.symmetric(vertical: 38, horizontal: 40),\n            child: Column(\n              children: [\n                const Spacer(),\n                FlowyLogoTitle(title: LocaleKeys.welcomeText.tr()),\n                VSpace(theme.spacing.xxl),\n                isLocalAuthEnabled\n                    ? const SignInAnonymousButtonV3()\n                    : const ContinueWithEmailAndPassword(),\n                VSpace(theme.spacing.xxl),\n                if (isAuthEnabled) ...[\n                  _buildThirdPartySignInButtons(context),\n                  VSpace(theme.spacing.xxl),\n                ],\n                const SignInAgreement(),\n                const Spacer(),\n                _buildSettingsButton(context),\n              ],\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildThirdPartySignInButtons(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Column(\n      children: [\n        Row(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Expanded(child: Divider()),\n            Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 8),\n              child: Text(\n                LocaleKeys.signIn_or.tr(),\n                style: TextStyle(\n                  fontSize: 16,\n                  color: theme.textColorScheme.secondary,\n                ),\n              ),\n            ),\n            const Expanded(child: Divider()),\n          ],\n        ),\n        const VSpace(16),\n        // expand third-party sign in buttons on Android by default.\n        // on iOS, the github and discord buttons are collapsed by default.\n        ThirdPartySignInButtons(\n          expanded: Platform.isAndroid,\n        ),\n      ],\n    );\n  }\n\n  Widget _buildSettingsButton(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        AFGhostIconTextButton(\n          text: LocaleKeys.signIn_settings.tr(),\n          textColor: (context, isHovering, disabled) {\n            return theme.textColorScheme.secondary;\n          },\n          size: AFButtonSize.s,\n          padding: EdgeInsets.symmetric(\n            horizontal: theme.spacing.m,\n            vertical: theme.spacing.xs,\n          ),\n          onTap: () => context.push(MobileLaunchSettingsPage.routeName),\n          iconBuilder: (context, isHovering, disabled) {\n            return FlowySvg(\n              FlowySvgs.settings_s,\n              size: Size.square(20),\n              color: theme.textColorScheme.secondary,\n            );\n          },\n        ),\n        const HSpace(24),\n        isLocalAuthEnabled\n            ? const ChangeCloudModeButton()\n            : const SignInAnonymousButtonV2(),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart",
    "content": "import 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/sign_in_bloc.dart';\nimport 'package:appflowy/user/presentation/router.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass SignInScreen extends StatelessWidget {\n  const SignInScreen({super.key});\n\n  static const routeName = '/SignInScreen';\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => getIt<SignInBloc>(),\n      child: BlocConsumer<SignInBloc, SignInState>(\n        listener: _showSignInError,\n        builder: (context, state) {\n          return UniversalPlatform.isDesktop\n              ? const DesktopSignInScreen()\n              : const MobileSignInScreen();\n        },\n      ),\n    );\n  }\n\n  void _showSignInError(BuildContext context, SignInState state) {\n    final successOrFail = state.successOrFail;\n    if (successOrFail != null) {\n      successOrFail.fold(\n        (userProfile) {\n          getIt<AuthRouter>().goHomeScreen(context, userProfile);\n        },\n        (error) {\n          Log.error('Sign in error: $error');\n        },\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button/anonymous_sign_in_button.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass AnonymousSignInButton extends StatelessWidget {\n  const AnonymousSignInButton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return AFGhostButton.normal(\n      onTap: () {},\n      builder: (context, isHovering, disabled) {\n        return const Placeholder();\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/anon_user_bloc.dart';\nimport 'package:appflowy/user/application/sign_in_bloc.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SignInAnonymousButtonV3 extends StatelessWidget {\n  const SignInAnonymousButtonV3({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SignInBloc, SignInState>(\n      builder: (context, signInState) {\n        return BlocProvider(\n          create: (context) => AnonUserBloc()\n            ..add(\n              const AnonUserEvent.initial(),\n            ),\n          child: BlocListener<AnonUserBloc, AnonUserState>(\n            listener: (context, state) async {\n              if (state.openedAnonUser != null) {\n                await runAppFlowy();\n              }\n            },\n            child: BlocBuilder<AnonUserBloc, AnonUserState>(\n              builder: (context, state) {\n                final text = LocaleKeys.signIn_continueWithLocalModel.tr();\n                final onTap = state.anonUsers.isEmpty\n                    ? () {\n                        context\n                            .read<SignInBloc>()\n                            .add(const SignInEvent.signInAsGuest());\n                      }\n                    : () {\n                        final bloc = context.read<AnonUserBloc>();\n                        final user = bloc.state.anonUsers.first;\n                        bloc.add(AnonUserEvent.openAnonUser(user));\n                      };\n                return AFFilledTextButton.primary(\n                  text: text,\n                  size: AFButtonSize.l,\n                  alignment: Alignment.center,\n                  onTap: onTap,\n                );\n              },\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/back_to_login_in_button.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nclass BackToLoginButton extends StatelessWidget {\n  const BackToLoginButton({\n    super.key,\n    required this.onTap,\n  });\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return AFGhostTextButton(\n      text: LocaleKeys.signIn_backToLogin.tr(),\n      size: AFButtonSize.s,\n      onTap: onTap,\n      padding: EdgeInsets.zero,\n      textColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (isHovering) {\n          return theme.textColorScheme.actionHover;\n        }\n        return theme.textColorScheme.action;\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_button.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass ContinueWithButton extends StatelessWidget {\n  const ContinueWithButton({\n    super.key,\n    required this.onTap,\n    required this.text,\n  });\n\n  final VoidCallback onTap;\n  final String text;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return AFFilledTextButton.primary(\n      size: AFButtonSize.l,\n      alignment: Alignment.center,\n      text: text,\n      onTap: onTap,\n      textStyle: theme.textStyle.body.enhanced(\n        color: theme.textColorScheme.onFill,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nclass ContinueWithEmail extends StatelessWidget {\n  const ContinueWithEmail({\n    super.key,\n    required this.onTap,\n  });\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return AFFilledTextButton.primary(\n      text: LocaleKeys.signIn_continueWithEmail.tr(),\n      size: AFButtonSize.l,\n      alignment: Alignment.center,\n      onTap: onTap,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/application/sign_in_bloc.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:string_validator/string_validator.dart';\n\nclass ContinueWithEmailAndPassword extends StatefulWidget {\n  const ContinueWithEmailAndPassword({super.key});\n\n  @override\n  State<ContinueWithEmailAndPassword> createState() =>\n      _ContinueWithEmailAndPasswordState();\n}\n\nclass _ContinueWithEmailAndPasswordState\n    extends State<ContinueWithEmailAndPassword> {\n  final controller = TextEditingController();\n  final focusNode = FocusNode();\n  final emailKey = GlobalKey<AFTextFieldState>();\n\n  bool _hasPushedContinueWithMagicLinkOrPasscodePage = false;\n\n  @override\n  void dispose() {\n    controller.dispose();\n    focusNode.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return BlocListener<SignInBloc, SignInState>(\n      listener: (context, state) {\n        final successOrFail = state.successOrFail;\n        // only push the continue with magic link or passcode page if the magic link is sent successfully\n        if (successOrFail != null) {\n          successOrFail.fold(\n            (_) => emailKey.currentState?.clearError(),\n            (error) => emailKey.currentState?.syncError(\n              errorText: error.msg,\n            ),\n          );\n        } else if (successOrFail == null && !state.isSubmitting) {\n          emailKey.currentState?.clearError();\n        }\n      },\n      child: Column(\n        children: [\n          AFTextField(\n            key: emailKey,\n            controller: controller,\n            hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(),\n            onSubmitted: (value) => _signInWithEmail(\n              context,\n              value,\n            ),\n          ),\n          VSpace(theme.spacing.l),\n          ContinueWithEmail(\n            onTap: () => _signInWithEmail(\n              context,\n              controller.text,\n            ),\n          ),\n          VSpace(theme.spacing.l),\n          ContinueWithPassword(\n            onTap: () {\n              final email = controller.text;\n\n              if (!isEmail(email)) {\n                emailKey.currentState?.syncError(\n                  errorText: LocaleKeys.signIn_invalidEmail.tr(),\n                );\n                return;\n              }\n\n              _pushContinueWithPasswordPage(\n                context,\n                email,\n              );\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  void _signInWithEmail(BuildContext context, String email) {\n    if (!isEmail(email)) {\n      emailKey.currentState?.syncError(\n        errorText: LocaleKeys.signIn_invalidEmail.tr(),\n      );\n      return;\n    }\n\n    context\n        .read<SignInBloc>()\n        .add(SignInEvent.signInWithMagicLink(email: email));\n\n    _pushContinueWithMagicLinkOrPasscodePage(\n      context,\n      email,\n    );\n  }\n\n  void _pushContinueWithMagicLinkOrPasscodePage(\n    BuildContext context,\n    String email,\n  ) {\n    if (_hasPushedContinueWithMagicLinkOrPasscodePage) {\n      return;\n    }\n\n    final signInBloc = context.read<SignInBloc>();\n\n    // push the a continue with magic link or passcode screen\n    Navigator.push(\n      context,\n      MaterialPageRoute(\n        builder: (context) => BlocProvider.value(\n          value: signInBloc,\n          child: ContinueWithMagicLinkOrPasscodePage(\n            email: email,\n            backToLogin: () {\n              Navigator.pop(context);\n\n              emailKey.currentState?.clearError();\n\n              _hasPushedContinueWithMagicLinkOrPasscodePage = false;\n            },\n            onEnterPasscode: (passcode) {\n              signInBloc.add(\n                SignInEvent.signInWithPasscode(\n                  email: email,\n                  passcode: passcode,\n                ),\n              );\n            },\n          ),\n        ),\n      ),\n    );\n\n    _hasPushedContinueWithMagicLinkOrPasscodePage = true;\n  }\n\n  void _pushContinueWithPasswordPage(\n    BuildContext context,\n    String email,\n  ) {\n    final signInBloc = context.read<SignInBloc>();\n    Navigator.push(\n      context,\n      MaterialPageRoute(\n        settings: const RouteSettings(name: '/continue-with-password'),\n        builder: (context) => BlocProvider.value(\n          value: signInBloc,\n          child: ContinueWithPasswordPage(\n            email: email,\n            backToLogin: () {\n              emailKey.currentState?.clearError();\n              Navigator.pop(context);\n            },\n            onEnterPassword: (password) => signInBloc.add(\n              SignInEvent.signInWithEmailAndPassword(\n                email: email,\n                password: password,\n              ),\n            ),\n            onForgotPassword: () {\n              // todo: implement forgot password\n            },\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/application/sign_in_bloc.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/back_to_login_in_button.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_button.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/title_logo.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/verifying_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass ContinueWithMagicLinkOrPasscodePage extends StatefulWidget {\n  const ContinueWithMagicLinkOrPasscodePage({\n    super.key,\n    required this.backToLogin,\n    required this.email,\n    required this.onEnterPasscode,\n  });\n\n  final String email;\n  final VoidCallback backToLogin;\n  final ValueChanged<String> onEnterPasscode;\n\n  @override\n  State<ContinueWithMagicLinkOrPasscodePage> createState() =>\n      _ContinueWithMagicLinkOrPasscodePageState();\n}\n\nclass _ContinueWithMagicLinkOrPasscodePageState\n    extends State<ContinueWithMagicLinkOrPasscodePage> {\n  final passcodeController = TextEditingController();\n\n  bool isEnteringPasscode = false;\n\n  ToastificationItem? toastificationItem;\n\n  final inputPasscodeKey = GlobalKey<AFTextFieldState>();\n\n  bool isSubmitting = false;\n\n  @override\n  void dispose() {\n    passcodeController.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocListener<SignInBloc, SignInState>(\n      listener: (context, state) {\n        final successOrFail = state.successOrFail;\n        if (successOrFail != null && successOrFail.isFailure) {\n          successOrFail.onFailure((error) {\n            inputPasscodeKey.currentState?.syncError(\n              errorText: LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr(),\n            );\n          });\n        }\n\n        if (state.isSubmitting != isSubmitting) {\n          setState(() => isSubmitting = state.isSubmitting);\n        }\n      },\n      child: Scaffold(\n        body: Center(\n          child: SizedBox(\n            width: 320,\n            child: Column(\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                // Logo, title and description\n                _buildLogoTitleAndDescription(),\n\n                // Enter code manually\n                ..._buildEnterCodeManually(),\n\n                // Back to login\n                BackToLoginButton(\n                  onTap: widget.backToLogin,\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  List<Widget> _buildEnterCodeManually() {\n    // todo: ask designer to provide the spacing\n    final spacing = VSpace(20);\n\n    if (!isEnteringPasscode) {\n      return [\n        AFFilledTextButton.primary(\n          text: LocaleKeys.signIn_enterCodeManually.tr(),\n          onTap: () => setState(() => isEnteringPasscode = true),\n          size: AFButtonSize.l,\n          alignment: Alignment.center,\n        ),\n        spacing,\n      ];\n    }\n\n    return [\n      // Enter code manually\n      AFTextField(\n        key: inputPasscodeKey,\n        controller: passcodeController,\n        hintText: LocaleKeys.signIn_enterCode.tr(),\n        keyboardType: TextInputType.number,\n        autoFocus: true,\n        onSubmitted: (passcode) {\n          if (passcode.isEmpty) {\n            inputPasscodeKey.currentState?.syncError(\n              errorText: LocaleKeys.signIn_invalidVerificationCode.tr(),\n            );\n          } else {\n            widget.onEnterPasscode(passcode);\n          }\n        },\n      ),\n      // todo: ask designer to provide the spacing\n      VSpace(12),\n\n      // continue to login\n      isSubmitting\n          ? const VerifyingButton()\n          : ContinueWithButton(\n              text: LocaleKeys.signIn_continueWithLoginCode.tr(),\n              onTap: () {\n                final passcode = passcodeController.text;\n                if (passcode.isEmpty) {\n                  inputPasscodeKey.currentState?.syncError(\n                    errorText: LocaleKeys.signIn_invalidVerificationCode.tr(),\n                  );\n                } else {\n                  widget.onEnterPasscode(passcode);\n                }\n              },\n            ),\n\n      spacing,\n    ];\n  }\n\n  Widget _buildLogoTitleAndDescription() {\n    final theme = AppFlowyTheme.of(context);\n\n    if (!isEnteringPasscode) {\n      return TitleLogo(\n        title: LocaleKeys.signIn_checkYourEmail.tr(),\n        description: LocaleKeys.signIn_temporaryVerificationLinkSent.tr(),\n        informationBuilder: (context) => Text(\n          widget.email,\n          style: theme.textStyle.body.enhanced(\n            color: theme.textColorScheme.primary,\n          ),\n        ),\n      );\n    } else {\n      return TitleLogo(\n        title: LocaleKeys.signIn_enterCode.tr(),\n        description: LocaleKeys.signIn_temporaryVerificationCodeSent.tr(),\n        informationBuilder: (context) => Text(\n          widget.email,\n          style: theme.textStyle.body.enhanced(\n            color: theme.textColorScheme.primary,\n          ),\n          textAlign: TextAlign.center,\n        ),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nclass ContinueWithPassword extends StatelessWidget {\n  const ContinueWithPassword({\n    super.key,\n    required this.onTap,\n  });\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return AFOutlinedTextButton.normal(\n      text: LocaleKeys.signIn_continueWithPassword.tr(),\n      size: AFButtonSize.l,\n      alignment: Alignment.center,\n      onTap: onTap,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/application/sign_in_bloc.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/back_to_login_in_button.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_button.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/forgot_password_page.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/title_logo.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/verifying_button.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass ContinueWithPasswordPage extends StatefulWidget {\n  const ContinueWithPasswordPage({\n    super.key,\n    required this.backToLogin,\n    required this.email,\n    required this.onEnterPassword,\n    required this.onForgotPassword,\n  });\n\n  final String email;\n  final VoidCallback backToLogin;\n  final ValueChanged<String> onEnterPassword;\n  final VoidCallback onForgotPassword;\n\n  @override\n  State<ContinueWithPasswordPage> createState() =>\n      _ContinueWithPasswordPageState();\n}\n\nclass _ContinueWithPasswordPageState extends State<ContinueWithPasswordPage> {\n  final passwordController = TextEditingController();\n  final inputPasswordKey = GlobalKey<AFTextFieldState>();\n\n  bool isSubmitting = false;\n\n  @override\n  void dispose() {\n    passwordController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Center(\n        child: SizedBox(\n          width: 320,\n          child: BlocListener<SignInBloc, SignInState>(\n            listener: (context, state) {\n              final successOrFail = state.successOrFail;\n              if (successOrFail != null && successOrFail.isFailure) {\n                successOrFail.onFailure((error) {\n                  inputPasswordKey.currentState?.syncError(\n                    errorText: LocaleKeys.signIn_invalidLoginCredentials.tr(),\n                  );\n                });\n              } else if (state.passwordError != null) {\n                inputPasswordKey.currentState?.syncError(\n                  errorText: LocaleKeys.signIn_invalidLoginCredentials.tr(),\n                );\n              } else {\n                inputPasswordKey.currentState?.clearError();\n              }\n\n              if (isSubmitting != state.isSubmitting) {\n                setState(() {\n                  isSubmitting = state.isSubmitting;\n                });\n              }\n            },\n            child: Column(\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                // Logo and title\n                _buildLogoAndTitle(),\n\n                // Password input and buttons\n                ..._buildPasswordSection(),\n\n                // Back to login\n                BackToLoginButton(\n                  onTap: widget.backToLogin,\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildLogoAndTitle() {\n    final theme = AppFlowyTheme.of(context);\n    return TitleLogo(\n      title: LocaleKeys.signIn_enterPassword.tr(),\n      informationBuilder: (context) => // email display\n          RichText(\n        text: TextSpan(\n          children: [\n            TextSpan(\n              text: LocaleKeys.signIn_loginAs.tr(),\n              style: theme.textStyle.body.standard(\n                color: theme.textColorScheme.primary,\n              ),\n            ),\n            TextSpan(\n              text: ' ${widget.email}',\n              style: theme.textStyle.body.enhanced(\n                color: theme.textColorScheme.primary,\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  List<Widget> _buildPasswordSection() {\n    final theme = AppFlowyTheme.of(context);\n    final iconSize = 20.0;\n\n    return [\n      // Password input\n      AFTextField(\n        key: inputPasswordKey,\n        controller: passwordController,\n        hintText: LocaleKeys.signIn_enterPassword.tr(),\n        autoFocus: true,\n        obscureText: true,\n        autofillHints: const [AutofillHints.password],\n        suffixIconConstraints: BoxConstraints.tightFor(\n          width: iconSize + theme.spacing.m,\n          height: iconSize,\n        ),\n        suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon(\n          isObscured: isObscured,\n          onTap: () {\n            inputPasswordKey.currentState?.syncObscured(!isObscured);\n          },\n        ),\n        onSubmitted: widget.onEnterPassword,\n      ),\n      // todo: ask designer to provide the spacing\n      VSpace(8),\n\n      // Forgot password button\n      Align(\n        alignment: Alignment.centerLeft,\n        child: AFGhostTextButton(\n          text: LocaleKeys.signIn_forgotPassword.tr(),\n          size: AFButtonSize.s,\n          padding: EdgeInsets.zero,\n          onTap: () => _pushForgotPasswordPage(),\n          textStyle: theme.textStyle.body.standard(\n            color: theme.textColorScheme.action,\n          ),\n          textColor: (context, isHovering, disabled) {\n            final theme = AppFlowyTheme.of(context);\n            if (isHovering) {\n              return theme.textColorScheme.actionHover;\n            }\n            return theme.textColorScheme.action;\n          },\n        ),\n      ),\n      VSpace(theme.spacing.xxl),\n\n      // Continue button\n      isSubmitting\n          ? const VerifyingButton()\n          : ContinueWithButton(\n              text: LocaleKeys.web_continue.tr(),\n              onTap: () => widget.onEnterPassword(passwordController.text),\n            ),\n      VSpace(20),\n    ];\n  }\n\n  Future<void> _pushForgotPasswordPage() async {\n    final signInBloc = context.read<SignInBloc>();\n    final baseUrl = await getAppFlowyCloudUrl();\n\n    if (mounted && context.mounted) {\n      await Navigator.push(\n        context,\n        MaterialPageRoute(\n          settings: const RouteSettings(name: '/forgot-password'),\n          builder: (context) => BlocProvider.value(\n            value: signInBloc,\n            child: ForgotPasswordPage(\n              email: widget.email,\n              backToLogin: widget.backToLogin,\n              baseUrl: baseUrl,\n            ),\n          ),\n        ),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/forgot_password_page.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/application/password/password_http_service.dart';\nimport 'package:appflowy/user/application/sign_in_bloc.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/back_to_login_in_button.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_button.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/reset_password_page.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/title_logo.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/verifying_button.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:string_validator/string_validator.dart';\n\nclass ForgotPasswordPage extends StatefulWidget {\n  const ForgotPasswordPage({\n    super.key,\n    required this.backToLogin,\n    required this.email,\n    required this.baseUrl,\n  });\n\n  final String email;\n  final VoidCallback backToLogin;\n  final String baseUrl;\n\n  @override\n  State<ForgotPasswordPage> createState() => _ForgotPasswordPageState();\n}\n\nclass _ForgotPasswordPageState extends State<ForgotPasswordPage> {\n  final passwordController = TextEditingController();\n  final inputPasswordKey = GlobalKey<AFTextFieldState>();\n\n  bool isSubmitting = false;\n\n  late final PasswordHttpService forgotPasswordService = PasswordHttpService(\n    baseUrl: widget.baseUrl,\n    // leave the auth token empty, the user is not signed in yet\n    authToken: '',\n  );\n\n  @override\n  void initState() {\n    super.initState();\n\n    passwordController.text = widget.email;\n  }\n\n  @override\n  void dispose() {\n    passwordController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Center(\n        child: SizedBox(\n          width: 320,\n          child: BlocListener<SignInBloc, SignInState>(\n            listener: (context, state) {\n              final successOrFail = state.successOrFail;\n              if (successOrFail != null && successOrFail.isFailure) {\n                successOrFail.onFailure((error) {\n                  inputPasswordKey.currentState?.syncError(\n                    errorText: LocaleKeys.signIn_invalidLoginCredentials.tr(),\n                  );\n                });\n              } else if (state.passwordError != null) {\n                inputPasswordKey.currentState?.syncError(\n                  errorText: LocaleKeys.signIn_invalidLoginCredentials.tr(),\n                );\n              } else {\n                inputPasswordKey.currentState?.clearError();\n              }\n\n              if (isSubmitting != state.isSubmitting) {\n                setState(() {\n                  isSubmitting = state.isSubmitting;\n                });\n              }\n            },\n            child: Column(\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                // Logo and title\n                _buildLogoAndTitle(),\n\n                // Password input and buttons\n                ..._buildPasswordSection(),\n\n                // Back to login\n                BackToLoginButton(\n                  onTap: widget.backToLogin,\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildLogoAndTitle() {\n    return TitleLogo(\n      title: LocaleKeys.signIn_resetPassword.tr(),\n      description: LocaleKeys.signIn_resetPasswordDescription.tr(),\n    );\n  }\n\n  List<Widget> _buildPasswordSection() {\n    final theme = AppFlowyTheme.of(context);\n    final iconSize = 20.0;\n\n    return [\n      // Password input\n      AFTextField(\n        key: inputPasswordKey,\n        controller: passwordController,\n        hintText: LocaleKeys.signIn_enterPassword.tr(),\n        autoFocus: true,\n        suffixIconConstraints: BoxConstraints.tightFor(\n          width: iconSize + theme.spacing.m,\n          height: iconSize,\n        ),\n        onSubmitted: (_) => _onSubmit(),\n      ),\n\n      VSpace(theme.spacing.xxl),\n\n      // Continue button\n      isSubmitting\n          ? const VerifyingButton()\n          : ContinueWithButton(\n              text: LocaleKeys.button_submit.tr(),\n              onTap: _onSubmit,\n            ),\n      VSpace(theme.spacing.xxl),\n    ];\n  }\n\n  Future<void> _onSubmit() async {\n    final email = passwordController.text;\n    if (!isEmail(email)) {\n      inputPasswordKey.currentState?.syncError(\n        errorText: LocaleKeys.signIn_invalidEmail.tr(),\n      );\n      return;\n    }\n\n    final signInBloc = context.read<SignInBloc>();\n\n    setState(() {\n      isSubmitting = true;\n    });\n\n    final result = await forgotPasswordService.forgotPassword(email: email);\n\n    setState(() {\n      isSubmitting = false;\n    });\n\n    result.fold(\n      (success) {\n        // push the email to the next screen\n        Navigator.push(\n          context,\n          MaterialPageRoute(\n            settings: const RouteSettings(name: '/reset-password'),\n            builder: (context) => BlocProvider.value(\n              value: signInBloc,\n              child: ResetPasswordPage(\n                email: email,\n                backToLogin: widget.backToLogin,\n                baseUrl: widget.baseUrl,\n              ),\n            ),\n          ),\n        );\n      },\n      (error) {\n        inputPasswordKey.currentState?.syncError(\n          errorText: error.toString(),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/reset_password.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/application/sign_in_bloc.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/back_to_login_in_button.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_button.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/title_logo.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/verifying_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass ResetPasswordWidget extends StatefulWidget {\n  const ResetPasswordWidget({\n    super.key,\n    required this.backToLogin,\n    required this.email,\n    required this.baseUrl,\n    required this.onValidateResetPasswordToken,\n  });\n\n  final String email;\n  final VoidCallback backToLogin;\n  final String baseUrl;\n  final ValueChanged<bool> onValidateResetPasswordToken;\n\n  @override\n  State<ResetPasswordWidget> createState() => _ResetPasswordWidgetState();\n}\n\nclass _ResetPasswordWidgetState extends State<ResetPasswordWidget> {\n  final passcodeController = TextEditingController();\n\n  ToastificationItem? toastificationItem;\n\n  final inputPasscodeKey = GlobalKey<AFTextFieldState>();\n\n  bool isSubmitting = false;\n\n  @override\n  void dispose() {\n    passcodeController.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocConsumer<SignInBloc, SignInState>(\n      listener: (context, state) {\n        final successOrFail = state.validateResetPasswordTokenSuccessOrFail;\n        if (successOrFail != null) {\n          successOrFail.fold(\n            (success) {\n              widget.onValidateResetPasswordToken(true);\n            },\n            (error) {\n              widget.onValidateResetPasswordToken(false);\n              inputPasscodeKey.currentState?.syncError(\n                errorText: LocaleKeys.signIn_resetPasswordFailed.tr(),\n              );\n            },\n          );\n        }\n\n        if (state.isSubmitting != isSubmitting) {\n          setState(() => isSubmitting = state.isSubmitting);\n        }\n      },\n      builder: (context, state) {\n        return Scaffold(\n          body: Center(\n            child: SizedBox(\n              width: 320,\n              child: Column(\n                mainAxisAlignment: MainAxisAlignment.center,\n                children: [\n                  // Logo, title and description\n                  _buildLogoTitleAndDescription(),\n\n                  // Enter code manually\n                  ..._buildEnterCodeManually(),\n\n                  // Back to login\n                  BackToLoginButton(\n                    onTap: widget.backToLogin,\n                  ),\n                ],\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  List<Widget> _buildEnterCodeManually() {\n    // todo: ask designer to provide the spacing\n    final spacing = VSpace(20);\n\n    return [\n      // Enter code manually\n      AFTextField(\n        key: inputPasscodeKey,\n        controller: passcodeController,\n        hintText: LocaleKeys.signIn_enterCode.tr(),\n        keyboardType: TextInputType.number,\n        autoFocus: true,\n        onSubmitted: (passcode) {\n          if (passcode.isEmpty) {\n            inputPasscodeKey.currentState?.syncError(\n              errorText: LocaleKeys.signIn_invalidVerificationCode.tr(),\n            );\n          } else {\n            //\n          }\n        },\n      ),\n      // todo: ask designer to provide the spacing\n      VSpace(12),\n\n      // continue to login\n      isSubmitting\n          ? const VerifyingButton()\n          : ContinueWithButton(\n              text: LocaleKeys.signIn_continueToResetPassword.tr(),\n              onTap: () {\n                final passcode = passcodeController.text;\n                if (passcode.isEmpty) {\n                  inputPasscodeKey.currentState?.syncError(\n                    errorText: LocaleKeys.signIn_invalidVerificationCode.tr(),\n                  );\n                } else {\n                  _onSubmit();\n                }\n              },\n            ),\n\n      spacing,\n    ];\n  }\n\n  Widget _buildLogoTitleAndDescription() {\n    final theme = AppFlowyTheme.of(context);\n    return TitleLogo(\n      title: LocaleKeys.signIn_checkYourEmail.tr(),\n      description: LocaleKeys.signIn_temporaryVerificationLinkSent.tr(),\n      informationBuilder: (context) => Text(\n        widget.email,\n        style: theme.textStyle.body.enhanced(\n          color: theme.textColorScheme.primary,\n        ),\n        textAlign: TextAlign.center,\n      ),\n    );\n  }\n\n  Future<void> _onSubmit() async {\n    final passcode = passcodeController.text;\n    if (passcode.isEmpty) {\n      inputPasscodeKey.currentState?.syncError(\n        errorText: LocaleKeys.signIn_invalidVerificationCode.tr(),\n      );\n      return;\n    }\n\n    context.read<SignInBloc>().add(\n          ValidateResetPasswordToken(\n            email: widget.email,\n            token: passcode,\n          ),\n        );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/reset_password_page.dart",
    "content": "import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/reset_password.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/set_new_password.dart';\nimport 'package:flutter/material.dart';\n\nenum ResetPasswordPageState {\n  enterPasscode,\n  setNewPassword,\n}\n\nclass ResetPasswordPage extends StatefulWidget {\n  const ResetPasswordPage({\n    super.key,\n    required this.backToLogin,\n    required this.email,\n    required this.baseUrl,\n  });\n\n  final String email;\n  final VoidCallback backToLogin;\n  final String baseUrl;\n\n  @override\n  State<ResetPasswordPage> createState() => _ResetPasswordPageState();\n}\n\nclass _ResetPasswordPageState extends State<ResetPasswordPage> {\n  ResetPasswordPageState state = ResetPasswordPageState.enterPasscode;\n\n  @override\n  void dispose() {\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return switch (state) {\n      ResetPasswordPageState.enterPasscode => ResetPasswordWidget(\n          email: widget.email,\n          backToLogin: widget.backToLogin,\n          baseUrl: widget.baseUrl,\n          onValidateResetPasswordToken: (isValid) {\n            setState(() {\n              state = ResetPasswordPageState.setNewPassword;\n            });\n          },\n        ),\n      ResetPasswordPageState.setNewPassword => SetNewPasswordWidget(\n          backToLogin: widget.backToLogin,\n          email: widget.email,\n        ),\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/set_new_password.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/application/sign_in_bloc.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/back_to_login_in_button.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_button.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/title_logo.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/verifying_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SetNewPasswordWidget extends StatefulWidget {\n  const SetNewPasswordWidget({\n    super.key,\n    required this.backToLogin,\n    required this.email,\n  });\n\n  final String email;\n  final VoidCallback backToLogin;\n\n  @override\n  State<SetNewPasswordWidget> createState() => _SetNewPasswordWidgetState();\n}\n\nclass _SetNewPasswordWidgetState extends State<SetNewPasswordWidget> {\n  final newPasswordController = TextEditingController();\n  final confirmPasswordController = TextEditingController();\n\n  final newPasswordKey = GlobalKey<AFTextFieldState>();\n  final confirmPasswordKey = GlobalKey<AFTextFieldState>();\n\n  bool isSubmitting = false;\n\n  @override\n  void dispose() {\n    newPasswordController.dispose();\n    confirmPasswordController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final spacing = theme.spacing.xxl;\n    return BlocConsumer<SignInBloc, SignInState>(\n      listener: (context, state) {\n        final successOrFail = state.resetPasswordSuccessOrFail;\n        if (successOrFail != null) {\n          successOrFail.fold(\n            (success) {\n              showToastNotification(\n                message: LocaleKeys.signIn_resetPasswordSuccess.tr(),\n              );\n              // pop until the login screen is found\n              Navigator.popUntil(context, (route) {\n                return route.settings.name == '/continue-with-password';\n              });\n            },\n            (error) {\n              if (error.code == ErrorCode.NewPasswordTooWeak) {\n                newPasswordKey.currentState?.syncError(\n                  errorText: LocaleKeys.signIn_passwordMustContain.tr(),\n                );\n              } else {\n                newPasswordKey.currentState?.syncError(\n                  errorText: error.msg,\n                );\n              }\n            },\n          );\n        }\n        // Handle state changes and validation results here\n        if (state.isSubmitting != isSubmitting) {\n          setState(() => isSubmitting = state.isSubmitting);\n        }\n      },\n      builder: (context, state) {\n        return Scaffold(\n          body: Center(\n            child: SizedBox(\n              width: 320,\n              child: Column(\n                mainAxisAlignment: MainAxisAlignment.center,\n                children: [\n                  _buildLogoAndTitle(),\n                  _buildPasswordFields(),\n                  VSpace(spacing),\n                  _buildResetButton(),\n                  VSpace(spacing),\n                  BackToLoginButton(\n                    onTap: widget.backToLogin,\n                  ),\n                ],\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildLogoAndTitle() {\n    final theme = AppFlowyTheme.of(context);\n    return TitleLogo(\n      title: LocaleKeys.signIn_resetPassword.tr(),\n      informationBuilder: (context) => RichText(\n        text: TextSpan(\n          children: [\n            TextSpan(\n              text: LocaleKeys.signIn_enterNewPasswordFor.tr(),\n              style: theme.textStyle.body.standard(\n                color: theme.textColorScheme.primary,\n              ),\n            ),\n            TextSpan(\n              text: widget.email,\n              style: theme.textStyle.body.enhanced(\n                color: theme.textColorScheme.primary,\n              ),\n            ),\n          ],\n        ),\n        textAlign: TextAlign.center,\n      ),\n    );\n  }\n\n  Widget _buildPasswordFields() {\n    final theme = AppFlowyTheme.of(context);\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Text(\n          LocaleKeys.signIn_newPassword.tr(),\n          style: theme.textStyle.caption.enhanced(\n            color: theme.textColorScheme.secondary,\n          ),\n        ),\n        const VSpace(8),\n        AFTextField(\n          key: newPasswordKey,\n          controller: newPasswordController,\n          obscureText: true,\n          autofillHints: const [AutofillHints.password],\n          hintText: LocaleKeys.signIn_enterNewPassword.tr(),\n          onSubmitted: (_) => _validateAndSubmit(),\n        ),\n        const VSpace(16),\n        Text(\n          LocaleKeys.signIn_confirmPassword.tr(),\n          style: theme.textStyle.caption.enhanced(\n            color: theme.textColorScheme.secondary,\n          ),\n        ),\n        const VSpace(8),\n        AFTextField(\n          key: confirmPasswordKey,\n          controller: confirmPasswordController,\n          obscureText: true,\n          autofillHints: const [AutofillHints.password],\n          hintText: LocaleKeys.signIn_confirmNewPassword.tr(),\n          onSubmitted: (_) => _validateAndSubmit(),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildResetButton() {\n    return isSubmitting\n        ? const VerifyingButton()\n        : ContinueWithButton(\n            text: LocaleKeys.signIn_resetPassword.tr(),\n            onTap: _validateAndSubmit,\n          );\n  }\n\n  void _validateAndSubmit() {\n    final newPassword = newPasswordController.text;\n    final confirmPassword = confirmPasswordController.text;\n\n    if (newPassword.isEmpty) {\n      newPasswordKey.currentState?.syncError(\n        errorText: LocaleKeys.signIn_newPasswordCannotBeEmpty.tr(),\n      );\n      return;\n    }\n\n    if (confirmPassword.isEmpty) {\n      confirmPasswordKey.currentState?.syncError(\n        errorText: LocaleKeys.signIn_confirmPasswordCannotBeEmpty.tr(),\n      );\n      return;\n    }\n\n    if (newPassword != confirmPassword) {\n      confirmPasswordKey.currentState?.syncError(\n        errorText: LocaleKeys.signIn_passwordsDoNotMatch.tr(),\n      );\n      return;\n    }\n\n    // Add the reset password event to the bloc\n    context.read<SignInBloc>().add(\n          ResetPassword(\n            email: widget.email,\n            newPassword: newPassword,\n          ),\n        );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/title_logo.dart",
    "content": "import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\n\nclass TitleLogo extends StatelessWidget {\n  const TitleLogo({\n    super.key,\n    required this.title,\n    this.description,\n    this.informationBuilder,\n  });\n\n  final String title;\n  final String? description;\n  final WidgetBuilder? informationBuilder;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final spacing = VSpace(theme.spacing.xxl);\n\n    return Column(\n      children: [\n        // logo\n        const AFLogo(),\n        spacing,\n\n        // title\n        Text(\n          title,\n          style: theme.textStyle.heading3.enhanced(\n            color: theme.textColorScheme.primary,\n          ),\n        ),\n        spacing,\n\n        // description\n        if (description != null)\n          Text(\n            description!,\n            style: theme.textStyle.body.standard(\n              color: theme.textColorScheme.primary,\n            ),\n            textAlign: TextAlign.center,\n          ),\n\n        if (informationBuilder != null) informationBuilder!(context),\n\n        spacing,\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/verifying_button.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\n\nclass VerifyingButton extends StatelessWidget {\n  const VerifyingButton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Opacity(\n      opacity: 0.7,\n      child: AFFilledButton.disabled(\n        size: AFButtonSize.l,\n        backgroundColor: theme.fillColorScheme.themeThick,\n        builder: (context, isHovering, disabled) {\n          return Row(\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              SizedBox.square(\n                dimension: 15.0,\n                child: CircularProgressIndicator(\n                  color: theme.textColorScheme.onFill,\n                  strokeWidth: 3.0,\n                ),\n              ),\n              HSpace(theme.spacing.l),\n              Text(\n                LocaleKeys.signIn_verifying.tr(),\n                style: theme.textStyle.body.enhanced(\n                  color: theme.textColorScheme.onFill,\n                ),\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:flutter/material.dart';\n\nclass AFLogo extends StatelessWidget {\n  const AFLogo({\n    super.key,\n    this.size = const Size.square(36),\n  });\n\n  final Size size;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowySvg(\n      FlowySvgs.app_logo_xl,\n      blendMode: null,\n      size: size,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/application/sign_in_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:string_validator/string_validator.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass SignInWithMagicLinkButtons extends StatefulWidget {\n  const SignInWithMagicLinkButtons({super.key});\n\n  @override\n  State<SignInWithMagicLinkButtons> createState() =>\n      _SignInWithMagicLinkButtonsState();\n}\n\nclass _SignInWithMagicLinkButtonsState\n    extends State<SignInWithMagicLinkButtons> {\n  final controller = TextEditingController();\n  final FocusNode _focusNode = FocusNode();\n\n  @override\n  void dispose() {\n    controller.dispose();\n    _focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        SizedBox(\n          height: UniversalPlatform.isMobile ? 38.0 : 48.0,\n          child: FlowyTextField(\n            autoFocus: false,\n            focusNode: _focusNode,\n            controller: controller,\n            borderRadius: BorderRadius.circular(4.0),\n            hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(),\n            hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(\n                  fontSize: 14.0,\n                  color: Theme.of(context).hintColor,\n                ),\n            textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(\n                  fontSize: 14.0,\n                ),\n            keyboardType: TextInputType.emailAddress,\n            onSubmitted: (_) => _sendMagicLink(context, controller.text),\n            onTapOutside: (_) => _focusNode.unfocus(),\n          ),\n        ),\n        const VSpace(12),\n        _ConfirmButton(\n          onTap: () => _sendMagicLink(context, controller.text),\n        ),\n      ],\n    );\n  }\n\n  void _sendMagicLink(BuildContext context, String email) {\n    if (!isEmail(email)) {\n      showToastNotification(\n        message: LocaleKeys.signIn_invalidEmail.tr(),\n        type: ToastificationType.error,\n      );\n      return;\n    }\n\n    context\n        .read<SignInBloc>()\n        .add(SignInEvent.signInWithMagicLink(email: email));\n\n    showConfirmDialog(\n      context: context,\n      title: LocaleKeys.signIn_magicLinkSent.tr(),\n      description: LocaleKeys.signIn_magicLinkSentDescription.tr(),\n    );\n  }\n}\n\nclass _ConfirmButton extends StatelessWidget {\n  const _ConfirmButton({\n    required this.onTap,\n  });\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SignInBloc, SignInState>(\n      builder: (context, state) {\n        final name = switch (state.loginType) {\n          LoginType.signIn => LocaleKeys.signIn_signInWithMagicLink.tr(),\n          LoginType.signUp => LocaleKeys.signIn_signUpWithMagicLink.tr(),\n        };\n        if (UniversalPlatform.isMobile) {\n          return ElevatedButton(\n            style: ElevatedButton.styleFrom(\n              minimumSize: const Size(double.infinity, 32),\n              maximumSize: const Size(double.infinity, 38),\n            ),\n            onPressed: onTap,\n            child: FlowyText(\n              name,\n              fontSize: 14,\n              color: Theme.of(context).colorScheme.onPrimary,\n            ),\n          );\n        } else {\n          return SizedBox(\n            height: 48,\n            child: FlowyButton(\n              isSelected: true,\n              onTap: onTap,\n              hoverColor: Theme.of(context).colorScheme.primary,\n              text: FlowyText.medium(\n                name,\n                textAlign: TextAlign.center,\n                color: Theme.of(context).colorScheme.onPrimary,\n              ),\n              radius: Corners.s6Border,\n            ),\n          );\n        }\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\n\nclass SignInAgreement extends StatelessWidget {\n  const SignInAgreement({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final textStyle = theme.textStyle.caption.standard(\n      color: theme.textColorScheme.secondary,\n    );\n    final underlinedTextStyle = theme.textStyle.caption.underline(\n      color: theme.textColorScheme.secondary,\n    );\n    return RichText(\n      textAlign: TextAlign.center,\n      text: TextSpan(\n        children: [\n          TextSpan(\n            text: LocaleKeys.web_signInAgreement.tr(),\n            style: textStyle,\n          ),\n          TextSpan(\n            text: '${LocaleKeys.web_termOfUse.tr()} ',\n            style: underlinedTextStyle,\n            mouseCursor: SystemMouseCursors.click,\n            recognizer: TapGestureRecognizer()\n              ..onTap = () => afLaunchUrlString('https://appflowy.com/terms'),\n          ),\n          TextSpan(\n            text: '${LocaleKeys.web_and.tr()} ',\n            style: textStyle,\n          ),\n          TextSpan(\n            text: LocaleKeys.web_privacyPolicy.tr(),\n            style: underlinedTextStyle,\n            mouseCursor: SystemMouseCursors.click,\n            recognizer: TapGestureRecognizer()\n              ..onTap = () => afLaunchUrlString('https://appflowy.com/privacy'),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/anon_user_bloc.dart';\nimport 'package:appflowy/user/application/sign_in_bloc.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SignInAnonymousButtonV2 extends StatelessWidget {\n  const SignInAnonymousButtonV2({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SignInBloc, SignInState>(\n      builder: (context, signInState) {\n        return BlocProvider(\n          create: (context) => AnonUserBloc()\n            ..add(\n              const AnonUserEvent.initial(),\n            ),\n          child: BlocListener<AnonUserBloc, AnonUserState>(\n            listener: (context, state) async {\n              if (state.openedAnonUser != null) {\n                await runAppFlowy();\n              }\n            },\n            child: BlocBuilder<AnonUserBloc, AnonUserState>(\n              builder: (context, state) {\n                final theme = AppFlowyTheme.of(context);\n                final onTap = state.anonUsers.isEmpty\n                    ? () {\n                        context\n                            .read<SignInBloc>()\n                            .add(const SignInEvent.signInAsGuest());\n                      }\n                    : () {\n                        final bloc = context.read<AnonUserBloc>();\n                        final user = bloc.state.anonUsers.first;\n                        bloc.add(AnonUserEvent.openAnonUser(user));\n                      };\n                return AFGhostIconTextButton(\n                  text: LocaleKeys.signIn_anonymousMode.tr(),\n                  textColor: (context, isHovering, disabled) {\n                    return theme.textColorScheme.secondary;\n                  },\n                  padding: EdgeInsets.symmetric(\n                    horizontal: theme.spacing.m,\n                    vertical: theme.spacing.xs,\n                  ),\n                  size: AFButtonSize.s,\n                  onTap: onTap,\n                  iconBuilder: (context, isHovering, disabled) {\n                    return FlowySvg(\n                      FlowySvgs.anonymous_mode_m,\n                      color: theme.textColorScheme.secondary,\n                    );\n                  },\n                );\n              },\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass ChangeCloudModeButton extends StatelessWidget {\n  const ChangeCloudModeButton({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return AFGhostIconTextButton(\n      text: LocaleKeys.signIn_switchToAppFlowyCloud.tr(),\n      textColor: (context, isHovering, disabled) {\n        return theme.textColorScheme.secondary;\n      },\n      size: AFButtonSize.s,\n      padding: EdgeInsets.symmetric(\n        horizontal: theme.spacing.m,\n        vertical: theme.spacing.xs,\n      ),\n      onTap: () async {\n        await useAppFlowyBetaCloudWithURL(\n          kAppflowyCloudUrl,\n          AuthenticatorType.appflowyCloud,\n        );\n        await runAppFlowy();\n      },\n      iconBuilder: (context, isHovering, disabled) {\n        return FlowySvg(\n          FlowySvgs.cloud_mode_m,\n          size: Size.square(20),\n          color: theme.textColorScheme.secondary,\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileLogoutButton extends StatelessWidget {\n  const MobileLogoutButton({\n    super.key,\n    required this.text,\n    this.textColor,\n    required this.onPressed,\n  });\n\n  final String text;\n  final Color? textColor;\n  final VoidCallback onPressed;\n\n  @override\n  Widget build(BuildContext context) {\n    return AFOutlinedTextButton.normal(\n      alignment: Alignment.center,\n      text: text,\n      onTap: onPressed,\n      size: AFButtonSize.l,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/switch_sign_in_sign_up_button.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/application/sign_in_bloc.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SwitchSignInSignUpButton extends StatelessWidget {\n  const SwitchSignInSignUpButton({\n    super.key,\n    required this.onTap,\n  });\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SignInBloc, SignInState>(\n      builder: (context, state) {\n        return MouseRegion(\n          cursor: SystemMouseCursors.click,\n          child: GestureDetector(\n            onTap: onTap,\n            child: Row(\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                FlowyText(\n                  switch (state.loginType) {\n                    LoginType.signIn =>\n                      LocaleKeys.signIn_dontHaveAnAccount.tr(),\n                    LoginType.signUp =>\n                      LocaleKeys.signIn_alreadyHaveAnAccount.tr(),\n                  },\n                  fontSize: 12,\n                ),\n                const HSpace(4),\n                FlowyText(\n                  switch (state.loginType) {\n                    LoginType.signIn => LocaleKeys.signIn_createAccount.tr(),\n                    LoginType.signUp => LocaleKeys.signIn_logIn.tr(),\n                  },\n                  color: Colors.blue,\n                  fontSize: 12,\n                ),\n              ],\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nenum ThirdPartySignInButtonType {\n  apple,\n  google,\n  github,\n  discord,\n  anonymous;\n\n  String get provider {\n    switch (this) {\n      case ThirdPartySignInButtonType.apple:\n        return 'apple';\n      case ThirdPartySignInButtonType.google:\n        return 'google';\n      case ThirdPartySignInButtonType.github:\n        return 'github';\n      case ThirdPartySignInButtonType.discord:\n        return 'discord';\n      case ThirdPartySignInButtonType.anonymous:\n        throw UnsupportedError('Anonymous session does not have a provider');\n    }\n  }\n\n  FlowySvgData get icon {\n    switch (this) {\n      case ThirdPartySignInButtonType.apple:\n        return FlowySvgs.m_apple_icon_xl;\n      case ThirdPartySignInButtonType.google:\n        return FlowySvgs.m_google_icon_xl;\n      case ThirdPartySignInButtonType.github:\n        return FlowySvgs.m_github_icon_xl;\n      case ThirdPartySignInButtonType.discord:\n        return FlowySvgs.m_discord_icon_xl;\n      case ThirdPartySignInButtonType.anonymous:\n        return FlowySvgs.m_discord_icon_xl;\n    }\n  }\n\n  String get labelText {\n    switch (this) {\n      case ThirdPartySignInButtonType.apple:\n        return LocaleKeys.signIn_signInWithApple.tr();\n      case ThirdPartySignInButtonType.google:\n        return LocaleKeys.signIn_signInWithGoogle.tr();\n      case ThirdPartySignInButtonType.github:\n        return LocaleKeys.signIn_signInWithGithub.tr();\n      case ThirdPartySignInButtonType.discord:\n        return LocaleKeys.signIn_signInWithDiscord.tr();\n      case ThirdPartySignInButtonType.anonymous:\n        return 'Anonymous session';\n    }\n  }\n\n  // https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple\n  Color backgroundColor(BuildContext context) {\n    final isDarkMode = Theme.of(context).brightness == Brightness.dark;\n    switch (this) {\n      case ThirdPartySignInButtonType.apple:\n        return isDarkMode ? Colors.white : Colors.black;\n      case ThirdPartySignInButtonType.google:\n      case ThirdPartySignInButtonType.github:\n      case ThirdPartySignInButtonType.discord:\n      case ThirdPartySignInButtonType.anonymous:\n        return isDarkMode ? Colors.black : Colors.grey.shade100;\n    }\n  }\n\n  Color textColor(BuildContext context) {\n    final isDarkMode = Theme.of(context).brightness == Brightness.dark;\n    switch (this) {\n      case ThirdPartySignInButtonType.apple:\n        return isDarkMode ? Colors.black : Colors.white;\n      case ThirdPartySignInButtonType.google:\n      case ThirdPartySignInButtonType.github:\n      case ThirdPartySignInButtonType.discord:\n      case ThirdPartySignInButtonType.anonymous:\n        return isDarkMode ? Colors.white : Colors.black;\n    }\n  }\n\n  BlendMode? get blendMode {\n    switch (this) {\n      case ThirdPartySignInButtonType.apple:\n      case ThirdPartySignInButtonType.github:\n        return BlendMode.srcIn;\n      default:\n        return null;\n    }\n  }\n}\n\nclass MobileThirdPartySignInButton extends StatelessWidget {\n  const MobileThirdPartySignInButton({\n    super.key,\n    this.height = 38,\n    this.fontSize = 14.0,\n    required this.onTap,\n    required this.type,\n  });\n\n  final VoidCallback onTap;\n  final double height;\n  final double fontSize;\n  final ThirdPartySignInButtonType type;\n\n  @override\n  Widget build(BuildContext context) {\n    return AFOutlinedIconTextButton.normal(\n      text: type.labelText,\n      onTap: onTap,\n      size: AFButtonSize.l,\n      iconBuilder: (context, isHovering, disabled) {\n        return FlowySvg(\n          type.icon,\n          size: Size.square(16),\n          blendMode: type.blendMode,\n        );\n      },\n    );\n  }\n}\n\nclass DesktopThirdPartySignInButton extends StatelessWidget {\n  const DesktopThirdPartySignInButton({\n    super.key,\n    required this.type,\n    required this.onTap,\n  });\n\n  final ThirdPartySignInButtonType type;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return AFOutlinedIconTextButton.normal(\n      text: type.labelText,\n      onTap: onTap,\n      size: AFButtonSize.l,\n      iconBuilder: (context, isHovering, disabled) {\n        return FlowySvg(\n          type.icon,\n          size: Size.square(18),\n          blendMode: type.blendMode,\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/user/application/sign_in_bloc.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'third_party_sign_in_button.dart';\n\ntypedef _SignInCallback = void Function(ThirdPartySignInButtonType signInType);\n\n@visibleForTesting\nconst Key signInWithGoogleButtonKey = Key('signInWithGoogleButton');\n\nclass ThirdPartySignInButtons extends StatelessWidget {\n  /// Used in DesktopSignInScreen, MobileSignInScreen and SettingThirdPartyLogin\n  const ThirdPartySignInButtons({\n    super.key,\n    this.expanded = false,\n  });\n\n  final bool expanded;\n\n  @override\n  Widget build(BuildContext context) {\n    if (UniversalPlatform.isDesktopOrWeb) {\n      return _DesktopThirdPartySignIn(\n        onSignIn: (type) => _signIn(context, type.provider),\n      );\n    } else {\n      return _MobileThirdPartySignIn(\n        isExpanded: expanded,\n        onSignIn: (type) => _signIn(context, type.provider),\n      );\n    }\n  }\n\n  void _signIn(BuildContext context, String provider) {\n    context.read<SignInBloc>().add(\n          SignInEvent.signInWithOAuth(platform: provider),\n        );\n  }\n}\n\nclass _DesktopThirdPartySignIn extends StatefulWidget {\n  const _DesktopThirdPartySignIn({\n    required this.onSignIn,\n  });\n\n  final _SignInCallback onSignIn;\n\n  @override\n  State<_DesktopThirdPartySignIn> createState() =>\n      _DesktopThirdPartySignInState();\n}\n\nclass _DesktopThirdPartySignInState extends State<_DesktopThirdPartySignIn> {\n  bool isExpanded = false;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Column(\n      children: [\n        DesktopThirdPartySignInButton(\n          key: signInWithGoogleButtonKey,\n          type: ThirdPartySignInButtonType.google,\n          onTap: () => widget.onSignIn(ThirdPartySignInButtonType.google),\n        ),\n        VSpace(theme.spacing.l),\n        DesktopThirdPartySignInButton(\n          type: ThirdPartySignInButtonType.apple,\n          onTap: () => widget.onSignIn(ThirdPartySignInButtonType.apple),\n        ),\n        ...isExpanded ? _buildExpandedButtons() : _buildCollapsedButtons(),\n      ],\n    );\n  }\n\n  List<Widget> _buildExpandedButtons() {\n    final theme = AppFlowyTheme.of(context);\n    return [\n      VSpace(theme.spacing.l),\n      DesktopThirdPartySignInButton(\n        type: ThirdPartySignInButtonType.github,\n        onTap: () => widget.onSignIn(ThirdPartySignInButtonType.github),\n      ),\n      VSpace(theme.spacing.l),\n      DesktopThirdPartySignInButton(\n        type: ThirdPartySignInButtonType.discord,\n        onTap: () => widget.onSignIn(ThirdPartySignInButtonType.discord),\n      ),\n    ];\n  }\n\n  List<Widget> _buildCollapsedButtons() {\n    final theme = AppFlowyTheme.of(context);\n    return [\n      VSpace(theme.spacing.l),\n      AFGhostTextButton(\n        text: 'More options',\n        padding: EdgeInsets.zero,\n        textColor: (context, isHovering, disabled) {\n          if (isHovering) {\n            return theme.textColorScheme.actionHover;\n          }\n          return theme.textColorScheme.action;\n        },\n        onTap: () {\n          setState(() {\n            isExpanded = !isExpanded;\n          });\n        },\n      ),\n    ];\n  }\n}\n\nclass _MobileThirdPartySignIn extends StatefulWidget {\n  const _MobileThirdPartySignIn({\n    required this.isExpanded,\n    required this.onSignIn,\n  });\n\n  final bool isExpanded;\n  final _SignInCallback onSignIn;\n\n  @override\n  State<_MobileThirdPartySignIn> createState() =>\n      _MobileThirdPartySignInState();\n}\n\nclass _MobileThirdPartySignInState extends State<_MobileThirdPartySignIn> {\n  static const padding = 8.0;\n\n  bool isExpanded = false;\n\n  @override\n  void initState() {\n    super.initState();\n\n    isExpanded = widget.isExpanded;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        // only display apple sign in button on iOS\n        if (Platform.isIOS) ...[\n          MobileThirdPartySignInButton(\n            type: ThirdPartySignInButtonType.apple,\n            onTap: () => widget.onSignIn(ThirdPartySignInButtonType.apple),\n          ),\n          const VSpace(padding),\n        ],\n        MobileThirdPartySignInButton(\n          key: signInWithGoogleButtonKey,\n          type: ThirdPartySignInButtonType.google,\n          onTap: () => widget.onSignIn(ThirdPartySignInButtonType.google),\n        ),\n        ...isExpanded ? _buildExpandedButtons() : _buildCollapsedButtons(),\n      ],\n    );\n  }\n\n  List<Widget> _buildExpandedButtons() {\n    return [\n      const VSpace(padding),\n      MobileThirdPartySignInButton(\n        type: ThirdPartySignInButtonType.github,\n        onTap: () => widget.onSignIn(ThirdPartySignInButtonType.github),\n      ),\n      const VSpace(padding),\n      MobileThirdPartySignInButton(\n        type: ThirdPartySignInButtonType.discord,\n        onTap: () => widget.onSignIn(ThirdPartySignInButtonType.discord),\n      ),\n    ];\n  }\n\n  List<Widget> _buildCollapsedButtons() {\n    final theme = AppFlowyTheme.of(context);\n    return [\n      const VSpace(padding * 2),\n      AFGhostTextButton(\n        text: 'More options',\n        textColor: (context, isHovering, disabled) {\n          if (isHovering) {\n            return theme.textColorScheme.actionHover;\n          }\n          return theme.textColorScheme.action;\n        },\n        onTap: () {\n          setState(() {\n            isExpanded = !isExpanded;\n          });\n        },\n      ),\n    ];\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart",
    "content": "export 'continue_with/continue_with_email_and_password.dart';\nexport 'sign_in_agreement.dart';\nexport 'sign_in_anonymous_button.dart';\nexport 'sign_in_or_logout_button.dart';\nexport 'third_party_sign_in_button/third_party_sign_in_button.dart';\n// export 'switch_sign_in_sign_up_button.dart';\nexport 'third_party_sign_in_button/third_party_sign_in_buttons.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart",
    "content": "import 'package:appflowy/core/frameless_window.dart';\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/anon_user_bloc.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/presentation/router.dart';\nimport 'package:appflowy/user/presentation/widgets/widgets.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/language.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass SkipLogInScreen extends StatefulWidget {\n  const SkipLogInScreen({super.key});\n\n  static const routeName = '/SkipLogInScreen';\n\n  @override\n  State<SkipLogInScreen> createState() => _SkipLogInScreenState();\n}\n\nclass _SkipLogInScreenState extends State<SkipLogInScreen> {\n  var _didCustomizeFolder = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: const _SkipLoginMoveWindow(),\n      body: Center(child: _renderBody(context)),\n    );\n  }\n\n  Widget _renderBody(BuildContext context) {\n    final size = MediaQuery.of(context).size;\n    return Column(\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        const Spacer(),\n        FlowyLogoTitle(\n          title: LocaleKeys.welcomeText.tr(),\n          logoSize: Size.square(UniversalPlatform.isMobile ? 80 : 40),\n        ),\n        const VSpace(32),\n        GoButton(\n          onPressed: () {\n            if (_didCustomizeFolder) {\n              _relaunchAppAndAutoRegister();\n            } else {\n              _autoRegister(context);\n            }\n          },\n        ),\n        // if (Env.enableCustomCloud) ...[\n        //   const VSpace(10),\n        //   const SizedBox(\n        //     width: 340,\n        //     child: _SetupYourServer(),\n        //   ),\n        // ],\n        const VSpace(32),\n        SizedBox(\n          width: size.width * 0.7,\n          child: FolderWidget(\n            createFolderCallback: () async => _didCustomizeFolder = true,\n          ),\n        ),\n        const Spacer(),\n        const SkipLoginPageFooter(),\n        const VSpace(20),\n      ],\n    );\n  }\n\n  Future<void> _autoRegister(BuildContext context) async {\n    final result = await getIt<AuthService>().signUpAsGuest();\n    result.fold(\n      (user) => getIt<AuthRouter>().goHomeScreen(context, user),\n      (error) => Log.error(error),\n    );\n  }\n\n  Future<void> _relaunchAppAndAutoRegister() async => runAppFlowy(isAnon: true);\n}\n\nclass SkipLoginPageFooter extends StatelessWidget {\n  const SkipLoginPageFooter({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    // The placeholderWidth should be greater than the longest width of the LanguageSelectorOnWelcomePage\n    const double placeholderWidth = 180;\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 16),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          if (!UniversalPlatform.isMobile) const HSpace(placeholderWidth),\n          const Expanded(child: SubscribeButtons()),\n          const SizedBox(\n            width: placeholderWidth,\n            height: 28,\n            child: Row(\n              children: [\n                Spacer(),\n                LanguageSelectorOnWelcomePage(),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass SubscribeButtons extends StatelessWidget {\n  const SubscribeButtons({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Wrap(\n      alignment: WrapAlignment.center,\n      children: [\n        Row(\n          mainAxisAlignment: MainAxisAlignment.center,\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            FlowyText.regular(\n              LocaleKeys.youCanAlso.tr(),\n              fontSize: FontSizes.s12,\n            ),\n            FlowyTextButton(\n              LocaleKeys.githubStarText.tr(),\n              padding: const EdgeInsets.symmetric(horizontal: 4),\n              fontWeight: FontWeight.w500,\n              fontColor: Theme.of(context).colorScheme.primary,\n              hoverColor: Colors.transparent,\n              fillColor: Colors.transparent,\n              onPressed: () =>\n                  afLaunchUrlString('https://github.com/AppFlowy-IO/appflowy'),\n            ),\n          ],\n        ),\n        Row(\n          mainAxisAlignment: MainAxisAlignment.center,\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            FlowyText.regular(LocaleKeys.and.tr(), fontSize: FontSizes.s12),\n            FlowyTextButton(\n              LocaleKeys.subscribeNewsletterText.tr(),\n              padding: const EdgeInsets.symmetric(horizontal: 4.0),\n              fontWeight: FontWeight.w500,\n              fontColor: Theme.of(context).colorScheme.primary,\n              hoverColor: Colors.transparent,\n              fillColor: Colors.transparent,\n              onPressed: () =>\n                  afLaunchUrlString('https://www.appflowy.io/blog'),\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n}\n\nclass LanguageSelectorOnWelcomePage extends StatelessWidget {\n  const LanguageSelectorOnWelcomePage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      offset: const Offset(0, -450),\n      direction: PopoverDirection.bottomWithRightAligned,\n      child: FlowyButton(\n        useIntrinsicWidth: true,\n        text: Row(\n          mainAxisSize: MainAxisSize.min,\n          mainAxisAlignment: MainAxisAlignment.end,\n          children: [\n            const FlowySvg(FlowySvgs.ethernet_m, size: Size.square(20)),\n            const HSpace(4),\n            Builder(\n              builder: (context) {\n                final currentLocale =\n                    context.watch<AppearanceSettingsCubit>().state.locale;\n                return FlowyText(languageFromLocale(currentLocale));\n              },\n            ),\n            const FlowySvg(FlowySvgs.drop_menu_hide_m, size: Size.square(20)),\n          ],\n        ),\n      ),\n      popupBuilder: (BuildContext context) {\n        final easyLocalization = EasyLocalization.of(context);\n        if (easyLocalization == null) {\n          return const SizedBox.shrink();\n        }\n\n        return LanguageItemsListView(\n          allLocales: easyLocalization.supportedLocales,\n        );\n      },\n    );\n  }\n}\n\nclass LanguageItemsListView extends StatelessWidget {\n  const LanguageItemsListView({super.key, required this.allLocales});\n\n  final List<Locale> allLocales;\n\n  @override\n  Widget build(BuildContext context) {\n    // get current locale from cubit\n    final state = context.watch<AppearanceSettingsCubit>().state;\n    return ConstrainedBox(\n      constraints: const BoxConstraints(maxHeight: 400),\n      child: ListView.builder(\n        itemCount: allLocales.length,\n        itemBuilder: (context, index) {\n          final locale = allLocales[index];\n          return LanguageItem(locale: locale, currentLocale: state.locale);\n        },\n      ),\n    );\n  }\n}\n\nclass LanguageItem extends StatelessWidget {\n  const LanguageItem({\n    super.key,\n    required this.locale,\n    required this.currentLocale,\n  });\n\n  final Locale locale;\n  final Locale currentLocale;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 32,\n      child: FlowyButton(\n        text: FlowyText.medium(\n          languageFromLocale(locale),\n        ),\n        rightIcon:\n            currentLocale == locale ? const FlowySvg(FlowySvgs.check_s) : null,\n        onTap: () {\n          if (currentLocale != locale) {\n            context.read<AppearanceSettingsCubit>().setLocale(context, locale);\n          }\n          PopoverContainer.of(context).close();\n        },\n      ),\n    );\n  }\n}\n\nclass GoButton extends StatelessWidget {\n  const GoButton({super.key, required this.onPressed});\n\n  final VoidCallback onPressed;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => AnonUserBloc()..add(const AnonUserEvent.initial()),\n      child: BlocListener<AnonUserBloc, AnonUserState>(\n        listener: (context, state) async {\n          if (state.openedAnonUser != null) {\n            await runAppFlowy();\n          }\n        },\n        child: BlocBuilder<AnonUserBloc, AnonUserState>(\n          builder: (context, state) {\n            final text = state.anonUsers.isEmpty\n                ? LocaleKeys.letsGoButtonText.tr()\n                : LocaleKeys.signIn_continueAnonymousUser.tr();\n\n            final textWidget = Row(\n              children: [\n                Expanded(\n                  child: FlowyText.medium(\n                    text,\n                    textAlign: TextAlign.center,\n                    fontSize: 14,\n                  ),\n                ),\n              ],\n            );\n\n            return SizedBox(\n              width: 340,\n              height: 48,\n              child: FlowyButton(\n                isSelected: true,\n                text: textWidget,\n                radius: Corners.s6Border,\n                onTap: () {\n                  if (state.anonUsers.isNotEmpty) {\n                    final bloc = context.read<AnonUserBloc>();\n                    final historicalUser = state.anonUsers.first;\n                    bloc.add(\n                      AnonUserEvent.openAnonUser(historicalUser),\n                    );\n                  } else {\n                    onPressed();\n                  }\n                },\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass _SkipLoginMoveWindow extends StatelessWidget\n    implements PreferredSizeWidget {\n  const _SkipLoginMoveWindow();\n\n  @override\n  Widget build(BuildContext context) =>\n      const Row(children: [Expanded(child: MoveWindowDetector())]);\n\n  @override\n  Size get preferredSize => const Size.fromHeight(55.0);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/application/splash_bloc.dart';\nimport 'package:appflowy/user/domain/auth_state.dart';\nimport 'package:appflowy/user/presentation/helpers/helpers.dart';\nimport 'package:appflowy/user/presentation/router.dart';\nimport 'package:appflowy/user/presentation/screens/screens.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass SplashScreen extends StatelessWidget {\n  /// Root Page of the app.\n  const SplashScreen({super.key, required this.isAnon});\n\n  final bool isAnon;\n\n  @override\n  Widget build(BuildContext context) {\n    if (isAnon) {\n      return FutureBuilder<void>(\n        future: _registerIfNeeded(),\n        builder: (context, snapshot) {\n          if (snapshot.connectionState != ConnectionState.done) {\n            return const SizedBox.shrink();\n          }\n          return _buildChild(context);\n        },\n      );\n    } else {\n      return _buildChild(context);\n    }\n  }\n\n  BlocProvider<SplashBloc> _buildChild(BuildContext context) {\n    return BlocProvider(\n      create: (context) =>\n          getIt<SplashBloc>()..add(const SplashEvent.getUser()),\n      child: Scaffold(\n        body: BlocListener<SplashBloc, SplashState>(\n          listener: (context, state) {\n            state.auth.map(\n              authenticated: (r) => _handleAuthenticated(context, r),\n              unauthenticated: (r) => _handleUnauthenticated(context, r),\n              initial: (r) => {},\n            );\n          },\n          child: const Body(),\n        ),\n      ),\n    );\n  }\n\n  /// Handles the authentication flow once a user is authenticated.\n  Future<void> _handleAuthenticated(\n    BuildContext context,\n    Authenticated authenticated,\n  ) async {\n    final result = await FolderEventGetCurrentWorkspaceSetting().send();\n    result.fold(\n      (workspaceSetting) {\n        // After login, replace Splash screen by corresponding home screen\n        getIt<SplashRouter>().goHomeScreen(\n          context,\n        );\n      },\n      (error) => handleOpenWorkspaceError(context, error),\n    );\n  }\n\n  void _handleUnauthenticated(BuildContext context, Unauthenticated result) {\n    // replace Splash screen as root page\n    if (isAuthEnabled || UniversalPlatform.isMobile) {\n      context.go(SignInScreen.routeName);\n    } else {\n      // if the env is not configured, we will skip to the 'skip login screen'.\n      context.go(SkipLogInScreen.routeName);\n    }\n  }\n\n  Future<void> _registerIfNeeded() async {\n    final result = await UserEventGetUserProfile().send();\n    if (result.isFailure) {\n      await getIt<AuthService>().signUpAsGuest();\n    }\n  }\n}\n\nclass Body extends StatelessWidget {\n  const Body({super.key});\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      alignment: Alignment.center,\n      child: UniversalPlatform.isMobile\n          ? const FlowySvg(FlowySvgs.app_logo_xl, blendMode: null)\n          : const _DesktopSplashBody(),\n    );\n  }\n}\n\nclass _DesktopSplashBody extends StatelessWidget {\n  const _DesktopSplashBody();\n\n  @override\n  Widget build(BuildContext context) {\n    final size = MediaQuery.of(context).size;\n    return SingleChildScrollView(\n      child: Stack(\n        alignment: Alignment.center,\n        children: [\n          Image(\n            fit: BoxFit.cover,\n            width: size.width,\n            height: size.height,\n            image: const AssetImage(\n              'assets/images/appflowy_launch_splash.jpg',\n            ),\n          ),\n          const CircularProgressIndicator.adaptive(),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/snap_bar.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../application/workspace_error_bloc.dart';\n\nclass WorkspaceErrorScreen extends StatelessWidget {\n  const WorkspaceErrorScreen({\n    super.key,\n    required this.userFolder,\n    required this.error,\n  });\n\n  final UserFolderPB userFolder;\n  final FlowyError error;\n\n  static const routeName = \"/WorkspaceErrorScreen\";\n  // arguments names to used in GoRouter\n  static const argError = \"error\";\n  static const argUserFolder = \"userFolder\";\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      extendBody: true,\n      body: BlocProvider(\n        create: (context) => WorkspaceErrorBloc(\n          userFolder: userFolder,\n          error: error,\n        )..add(const WorkspaceErrorEvent.init()),\n        child: MultiBlocListener(\n          listeners: [\n            BlocListener<WorkspaceErrorBloc, WorkspaceErrorState>(\n              listenWhen: (previous, current) =>\n                  previous.workspaceState != current.workspaceState,\n              listener: (context, state) async {\n                await state.workspaceState.when(\n                  initial: () {},\n                  logout: () async {\n                    await getIt<AuthService>().signOut();\n                    await runAppFlowy();\n                  },\n                  reset: () async {\n                    await getIt<AuthService>().signOut();\n                    await runAppFlowy();\n                  },\n                  restoreFromSnapshot: () {},\n                  createNewWorkspace: () {},\n                );\n              },\n            ),\n            BlocListener<WorkspaceErrorBloc, WorkspaceErrorState>(\n              listenWhen: (previous, current) =>\n                  previous.loadingState != current.loadingState,\n              listener: (context, state) async {\n                state.loadingState?.when(\n                  loading: () {},\n                  finish: (error) {\n                    error.fold(\n                      (_) {},\n                      (err) {\n                        showSnapBar(context, err.msg);\n                      },\n                    );\n                  },\n                  idle: () {},\n                );\n              },\n            ),\n          ],\n          child: BlocBuilder<WorkspaceErrorBloc, WorkspaceErrorState>(\n            builder: (context, state) {\n              final List<Widget> children = [\n                WorkspaceErrorDescription(error: error),\n              ];\n\n              children.addAll([\n                const VSpace(50),\n                const LogoutButton(),\n                const VSpace(20),\n              ]);\n\n              return Center(\n                child: SizedBox(\n                  width: 500,\n                  child: IntrinsicHeight(\n                    child: Column(\n                      children: children,\n                    ),\n                  ),\n                ),\n              );\n            },\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass WorkspaceErrorDescription extends StatelessWidget {\n  const WorkspaceErrorDescription({super.key, required this.error});\n\n  final FlowyError error;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<WorkspaceErrorBloc, WorkspaceErrorState>(\n      builder: (context, state) {\n        return Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            FlowyText.medium(\n              state.initialError.msg.toString(),\n              fontSize: 14,\n              maxLines: 10,\n            ),\n            FlowyText.medium(\n              \"Error code: ${state.initialError.code.value.toString()}\",\n              fontSize: 12,\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n\nclass LogoutButton extends StatelessWidget {\n  const LogoutButton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 40,\n      width: 200,\n      child: FlowyButton(\n        text: FlowyText.medium(\n          LocaleKeys.settings_menu_logout.tr(),\n          textAlign: TextAlign.center,\n        ),\n        onTap: () async {\n          context.read<WorkspaceErrorBloc>().add(\n                const WorkspaceErrorEvent.logout(),\n              );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/desktop_workspace_start_screen.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/flowy_error_page.dart';\nimport 'package:appflowy/workspace/application/workspace/prelude.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\n\nclass DesktopWorkspaceStartScreen extends StatelessWidget {\n  const DesktopWorkspaceStartScreen({super.key, required this.workspaceState});\n\n  final WorkspaceState workspaceState;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Padding(\n        padding: const EdgeInsets.all(60.0),\n        child: Column(\n          children: [\n            _renderBody(workspaceState),\n            _renderCreateButton(context),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nWidget _renderBody(WorkspaceState state) {\n  final body = state.successOrFailure.fold(\n    (_) => _renderList(state.workspaces),\n    (error) => Center(\n      child: AppFlowyErrorPage(\n        error: error,\n      ),\n    ),\n  );\n  return body;\n}\n\nWidget _renderList(List<WorkspacePB> workspaces) {\n  return Expanded(\n    child: StyledListView(\n      itemBuilder: (BuildContext context, int index) {\n        final workspace = workspaces[index];\n        return _WorkspaceItem(\n          workspace: workspace,\n          onPressed: (workspace) => _popToWorkspace(context, workspace),\n        );\n      },\n      itemCount: workspaces.length,\n    ),\n  );\n}\n\nclass _WorkspaceItem extends StatelessWidget {\n  const _WorkspaceItem({\n    required this.workspace,\n    required this.onPressed,\n  });\n\n  final WorkspacePB workspace;\n  final void Function(WorkspacePB workspace) onPressed;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 46,\n      child: FlowyTextButton(\n        workspace.name,\n        hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        fontSize: 14,\n        onPressed: () => onPressed(workspace),\n      ),\n    );\n  }\n}\n\nWidget _renderCreateButton(BuildContext context) {\n  return SizedBox(\n    width: 200,\n    height: 40,\n    child: FlowyTextButton(\n      LocaleKeys.workspace_create.tr(),\n      fontSize: 14,\n      hoverColor: AFThemeExtension.of(context).lightGreyHover,\n      onPressed: () {\n        // same method as in mobile\n        context.read<WorkspaceBloc>().add(\n              WorkspaceEvent.createWorkspace(\n                LocaleKeys.workspace_hint.tr(),\n                \"\",\n              ),\n            );\n      },\n    ),\n  );\n}\n\n// same method as in mobile\nvoid _popToWorkspace(BuildContext context, WorkspacePB workspace) {\n  context.pop(workspace.id);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/flowy_error_page.dart';\nimport 'package:appflowy/workspace/application/workspace/prelude.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\n// TODO: needs refactor when multiple workspaces are supported\nclass MobileWorkspaceStartScreen extends StatefulWidget {\n  const MobileWorkspaceStartScreen({\n    super.key,\n    required this.workspaceState,\n  });\n\n  @override\n  State<MobileWorkspaceStartScreen> createState() =>\n      _MobileWorkspaceStartScreenState();\n  final WorkspaceState workspaceState;\n}\n\nclass _MobileWorkspaceStartScreenState\n    extends State<MobileWorkspaceStartScreen> {\n  WorkspacePB? selectedWorkspace;\n  final TextEditingController controller = TextEditingController();\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final style = Theme.of(context);\n    final size = MediaQuery.of(context).size;\n    const double spacing = 16.0;\n    final List<DropdownMenuEntry<WorkspacePB>> workspaceEntries =\n        <DropdownMenuEntry<WorkspacePB>>[];\n    for (final WorkspacePB workspace in widget.workspaceState.workspaces) {\n      workspaceEntries.add(\n        DropdownMenuEntry<WorkspacePB>(\n          value: workspace,\n          label: workspace.name,\n        ),\n      );\n    }\n\n// render the workspace dropdown menu if success, otherwise render error page\n    final body = widget.workspaceState.successOrFailure.fold(\n      (_) {\n        return Padding(\n          padding: const EdgeInsets.fromLTRB(50, 0, 50, 30),\n          child: Column(\n            children: [\n              const Spacer(),\n              const FlowySvg(\n                FlowySvgs.app_logo_xl,\n                size: Size.square(64),\n                blendMode: null,\n              ),\n              const VSpace(spacing * 2),\n              Text(\n                LocaleKeys.workspace_chooseWorkspace.tr(),\n                style: style.textTheme.displaySmall,\n                textAlign: TextAlign.center,\n              ),\n              const VSpace(spacing * 4),\n              DropdownMenu<WorkspacePB>(\n                width: size.width - 100,\n                // TODO: The following code cause the bad state error, need to fix it\n                // initialSelection: widget.workspaceState.workspaces.first,\n                label: const Text('Workspace'),\n                controller: controller,\n                dropdownMenuEntries: workspaceEntries,\n                onSelected: (WorkspacePB? workspace) {\n                  setState(() {\n                    selectedWorkspace = workspace;\n                  });\n                },\n              ),\n              const Spacer(),\n              // TODO: needs to implement create workspace in the future\n              // TextButton(\n              //   child: Text(\n              //     LocaleKeys.workspace_create.tr(),\n              //     style: style.textTheme.labelMedium,\n              //     textAlign: TextAlign.center,\n              //   ),\n              //   onPressed: () {\n              //     setState(() {\n              //          // same method as in desktop\n              // context.read<WorkspaceBloc>().add(\n              //       WorkspaceEvent.createWorkspace(\n              //         LocaleKeys.workspace_hint.tr(),\n              //         \"\",\n              //       ),\n              //     );\n              //     });\n              //   },\n              // ),\n              const VSpace(spacing / 2),\n              ElevatedButton(\n                style: ElevatedButton.styleFrom(\n                  minimumSize: const Size(double.infinity, 56),\n                ),\n                onPressed: () {\n                  if (selectedWorkspace == null) {\n                    // If user didn't choose any workspace, pop to the initial workspace(first workspace)\n                    _popToWorkspace(\n                      context,\n                      widget.workspaceState.workspaces.first,\n                    );\n                    return;\n                  }\n                  // pop to the selected workspace\n                  _popToWorkspace(\n                    context,\n                    selectedWorkspace!,\n                  );\n                },\n                child: Text(LocaleKeys.signUp_getStartedText.tr()),\n              ),\n              const VSpace(spacing),\n            ],\n          ),\n        );\n      },\n      (error) {\n        return Center(\n          child: AppFlowyErrorPage(\n            error: error,\n          ),\n        );\n      },\n    );\n\n    return Scaffold(\n      body: body,\n    );\n  }\n}\n\n// same method as in desktop\nvoid _popToWorkspace(BuildContext context, WorkspacePB workspace) {\n  context.pop(workspace.id);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/workspace_start_screen.dart",
    "content": "import 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/presentation/screens/workspace_start_screen/desktop_workspace_start_screen.dart';\nimport 'package:appflowy/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart';\nimport 'package:appflowy/workspace/application/workspace/workspace_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\n// For future use\nclass WorkspaceStartScreen extends StatelessWidget {\n  /// To choose which screen is going to open\n  const WorkspaceStartScreen({super.key, required this.userProfile});\n\n  final UserProfilePB userProfile;\n\n  static const routeName = \"/WorkspaceStartScreen\";\n  static const argUserProfile = \"userProfile\";\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) => getIt<WorkspaceBloc>(param1: userProfile)\n        ..add(const WorkspaceEvent.initial()),\n      child: BlocBuilder<WorkspaceBloc, WorkspaceState>(\n        builder: (context, state) {\n          if (UniversalPlatform.isMobile) {\n            return MobileWorkspaceStartScreen(\n              workspaceState: state,\n            );\n          }\n          return DesktopWorkspaceStartScreen(\n            workspaceState: state,\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass AuthFormContainer extends StatelessWidget {\n  const AuthFormContainer({\n    super.key,\n    required this.children,\n  });\n\n  final List<Widget> children;\n\n  static const double width = 320;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: width,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: children,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart",
    "content": "import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass FlowyLogoTitle extends StatelessWidget {\n  const FlowyLogoTitle({\n    super.key,\n    required this.title,\n    this.logoSize = const Size.square(40),\n  });\n\n  final String title;\n  final Size logoSize;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return SizedBox(\n      child: Column(\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          AFLogo(size: logoSize),\n          const VSpace(20),\n          Text(\n            title,\n            style: theme.textStyle.heading3.enhanced(\n              color: theme.textColorScheme.primary,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/widgets/folder_widget.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/workspace/application/settings/prelude.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/file_picker/file_picker_service.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';\nimport 'package:flutter/material.dart';\nimport 'package:fluttertoast/fluttertoast.dart';\nimport 'package:google_fonts/google_fonts.dart';\n\nimport '../../../generated/locale_keys.g.dart';\nimport '../../../startup/startup.dart';\nimport '../../../workspace/presentation/home/toast.dart';\n\nenum _FolderPage {\n  options,\n  create,\n  open,\n}\n\nclass FolderWidget extends StatefulWidget {\n  const FolderWidget({\n    super.key,\n    required this.createFolderCallback,\n  });\n\n  final Future<void> Function() createFolderCallback;\n\n  @override\n  State<FolderWidget> createState() => _FolderWidgetState();\n}\n\nclass _FolderWidgetState extends State<FolderWidget> {\n  var page = _FolderPage.options;\n\n  @override\n  Widget build(BuildContext context) {\n    return _mapIndexToWidget(context);\n  }\n\n  Widget _mapIndexToWidget(BuildContext context) {\n    switch (page) {\n      case _FolderPage.options:\n        return FolderOptionsWidget(\n          onPressedOpen: () {\n            _openFolder();\n          },\n        );\n      case _FolderPage.create:\n        return CreateFolderWidget(\n          onPressedBack: () {\n            setState(() => page = _FolderPage.options);\n          },\n          onPressedCreate: widget.createFolderCallback,\n        );\n      case _FolderPage.open:\n        return const SizedBox.shrink();\n    }\n  }\n\n  Future<void> _openFolder() async {\n    final path = await getIt<FilePickerService>().getDirectoryPath();\n    if (path != null) {\n      await getIt<ApplicationDataStorage>().setCustomPath(path);\n      await widget.createFolderCallback();\n      setState(() {});\n    }\n  }\n}\n\nclass FolderOptionsWidget extends StatelessWidget {\n  const FolderOptionsWidget({\n    super.key,\n    required this.onPressedOpen,\n  });\n\n  final VoidCallback onPressedOpen;\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: getIt<ApplicationDataStorage>().getPath(),\n      builder: (context, result) {\n        final subtitle = result.hasData ? result.data! : '';\n        return _FolderCard(\n          icon: const FlowySvg(FlowySvgs.archive_m),\n          title: LocaleKeys.settings_files_defineWhereYourDataIsStored.tr(),\n          subtitle: subtitle,\n          trailing: _buildTextButton(\n            context,\n            LocaleKeys.settings_files_set.tr(),\n            onPressedOpen,\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass CreateFolderWidget extends StatefulWidget {\n  const CreateFolderWidget({\n    super.key,\n    required this.onPressedBack,\n    required this.onPressedCreate,\n  });\n\n  final VoidCallback onPressedBack;\n  final Future<void> Function() onPressedCreate;\n\n  @override\n  State<CreateFolderWidget> createState() => CreateFolderWidgetState();\n}\n\n@visibleForTesting\nclass CreateFolderWidgetState extends State<CreateFolderWidget> {\n  var _folderName = 'appflowy';\n  @visibleForTesting\n  var directory = '';\n\n  final _fToast = FToast();\n\n  @override\n  void initState() {\n    super.initState();\n    _fToast.init(context);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        Align(\n          alignment: Alignment.centerLeft,\n          child: TextButton.icon(\n            onPressed: widget.onPressedBack,\n            icon: const Icon(Icons.arrow_back_rounded),\n            label: const Text('Back'),\n          ),\n        ),\n        _FolderCard(\n          title: LocaleKeys.settings_files_location.tr(),\n          subtitle: LocaleKeys.settings_files_locationDesc.tr(),\n          trailing: SizedBox(\n            width: 120,\n            child: FlowyTextField(\n              hintText: LocaleKeys.settings_files_folderHintText.tr(),\n              onChanged: (name) => _folderName = name,\n              onSubmitted: (name) => setState(\n                () => _folderName = name,\n              ),\n            ),\n          ),\n        ),\n        _FolderCard(\n          title: LocaleKeys.settings_files_folderPath.tr(),\n          subtitle: _path,\n          trailing: _buildTextButton(\n            context,\n            LocaleKeys.settings_files_browser.tr(),\n            () async {\n              final dir = await getIt<FilePickerService>().getDirectoryPath();\n              if (dir != null) {\n                setState(() => directory = dir);\n              }\n            },\n          ),\n        ),\n        const VSpace(4.0),\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 4.0),\n          child: _buildTextButton(\n            context,\n            LocaleKeys.settings_files_create.tr(),\n            () async {\n              if (_path.isEmpty) {\n                _showToast(\n                  LocaleKeys.settings_files_locationCannotBeEmpty.tr(),\n                );\n              } else {\n                await getIt<ApplicationDataStorage>().setCustomPath(_path);\n                await widget.onPressedCreate();\n              }\n            },\n          ),\n        ),\n      ],\n    );\n  }\n\n  String get _path {\n    if (directory.isEmpty) return '';\n    final String path;\n    if (Platform.isMacOS) {\n      path = directory.replaceAll('/Volumes/Macintosh HD', '');\n    } else {\n      path = directory;\n    }\n    return '$path/$_folderName';\n  }\n\n  void _showToast(String message) {\n    _fToast.showToast(\n      child: FlowyMessageToast(message: message),\n      gravity: ToastGravity.CENTER,\n    );\n  }\n}\n\nWidget _buildTextButton(\n  BuildContext context,\n  String title,\n  VoidCallback onPressed,\n) {\n  return SecondaryTextButton(\n    title,\n    mode: TextButtonMode.small,\n    onPressed: onPressed,\n  );\n}\n\nclass _FolderCard extends StatelessWidget {\n  const _FolderCard({\n    required this.title,\n    required this.subtitle,\n    this.trailing,\n    this.icon,\n  });\n\n  final String title;\n  final String subtitle;\n  final Widget? icon;\n  final Widget? trailing;\n\n  @override\n  Widget build(BuildContext context) {\n    const cardSpacing = 16.0;\n    return Card(\n      child: Padding(\n        padding: const EdgeInsets.symmetric(\n          vertical: cardSpacing,\n          horizontal: cardSpacing,\n        ),\n        child: Row(\n          children: [\n            if (icon != null)\n              Padding(\n                padding: const EdgeInsets.only(right: cardSpacing),\n                child: icon!,\n              ),\n            Expanded(\n              child: Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  Row(\n                    children: [\n                      Flexible(\n                        child: FlowyText.regular(\n                          title,\n                          fontSize: FontSizes.s14,\n                          fontFamily: GoogleFonts.poppins(\n                            fontWeight: FontWeight.w500,\n                          ).fontFamily,\n                          overflow: TextOverflow.ellipsis,\n                        ),\n                      ),\n                      Tooltip(\n                        decoration: BoxDecoration(\n                          color: Theme.of(context).colorScheme.surface,\n                          borderRadius: BorderRadius.circular(6),\n                        ),\n                        preferBelow: false,\n                        richMessage: WidgetSpan(\n                          alignment: PlaceholderAlignment.baseline,\n                          baseline: TextBaseline.alphabetic,\n                          child: Container(\n                            color: Theme.of(context).colorScheme.surface,\n                            padding: const EdgeInsets.all(10),\n                            constraints: const BoxConstraints(maxWidth: 450),\n                            child: FlowyText(\n                              LocaleKeys.settings_menu_customPathPrompt.tr(),\n                              maxLines: null,\n                            ),\n                          ),\n                        ),\n                        child: const FlowyIconButton(\n                          icon: Icon(\n                            Icons.warning_amber_rounded,\n                            size: 20,\n                            color: Colors.orangeAccent,\n                          ),\n                        ),\n                      ),\n                    ],\n                  ),\n                  const VSpace(4),\n                  FlowyText.regular(\n                    subtitle,\n                    overflow: TextOverflow.ellipsis,\n                    fontSize: FontSizes.s12,\n                    fontFamily: GoogleFonts.poppins(\n                      fontWeight: FontWeight.w300,\n                    ).fontFamily,\n                  ),\n                ],\n              ),\n            ),\n            if (trailing != null) ...[\n              const HSpace(cardSpacing),\n              trailing!,\n            ],\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/user/presentation/widgets/widgets.dart",
    "content": "export 'folder_widget.dart';\nexport 'flowy_logo_title.dart';\nexport 'auth_form_container.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/built_in_svgs.dart",
    "content": "final builtInSVGIcons = [\n  '1F9CC',\n  '1F9DB',\n  '1F9DD-200D-2642-FE0F',\n  '1F9DE-200D-2642-FE0F',\n  '1F9DF',\n  '1F42F',\n  '1F43A',\n  '1F431',\n  '1F435',\n  '1F600',\n  '1F984',\n];\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/color_generator/color_generator.dart",
    "content": "import 'package:flutter/material.dart';\n\n// the color set generated from AI\nfinal _builtInColorSet = [\n  (const Color(0xFF8A2BE2), const Color(0xFFF0E6FF)),\n  (const Color(0xFF2E8B57), const Color(0xFFE0FFF0)),\n  (const Color(0xFF1E90FF), const Color(0xFFE6F3FF)),\n  (const Color(0xFFFF7F50), const Color(0xFFFFF0E6)),\n  (const Color(0xFFFF69B4), const Color(0xFFFFE6F0)),\n  (const Color(0xFF20B2AA), const Color(0xFFE0FFFF)),\n  (const Color(0xFFDC143C), const Color(0xFFFFE6E6)),\n  (const Color(0xFF8B4513), const Color(0xFFFFF0E6)),\n];\n\nextension type ColorGenerator(String value) {\n  Color toColor() {\n    final int hash = value.codeUnits.fold(0, (int acc, int unit) => acc + unit);\n    final double hue = (hash % 360).toDouble();\n    return HSLColor.fromAHSL(1.0, hue, 0.5, 0.8).toColor();\n  }\n\n  // shuffle a color from the built-in color set, for the same name, the result should be the same\n  (Color, Color) randomColor() {\n    final hash = value.codeUnits.fold(0, (int acc, int unit) => acc + unit);\n    final index = hash % _builtInColorSet.length;\n    return _builtInColorSet[index];\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/color_to_hex_string.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:flutter/material.dart';\n\nextension ColorExtension on Color {\n  /// return a hex string in 0xff000000 format\n  String toHexString() {\n    final alpha = (a * 255).toInt().toRadixString(16).padLeft(2, '0');\n    final red = (r * 255).toInt().toRadixString(16).padLeft(2, '0');\n    final green = (g * 255).toInt().toRadixString(16).padLeft(2, '0');\n    final blue = (b * 255).toInt().toRadixString(16).padLeft(2, '0');\n\n    return '0x$alpha$red$green$blue'.toLowerCase();\n  }\n\n  /// return a random color\n  static Color random({double opacity = 1.0}) {\n    return Color((math.Random().nextDouble() * 0xFFFFFF).toInt())\n        .withValues(alpha: opacity);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/debounce.dart",
    "content": "import 'dart:async';\n\nclass Debounce {\n  Debounce({\n    this.duration = const Duration(milliseconds: 1000),\n  });\n\n  final Duration duration;\n  Timer? _timer;\n\n  void call(Function action) {\n    dispose();\n\n    _timer = Timer(duration, () {\n      action();\n    });\n  }\n\n  void dispose() {\n    _timer?.cancel();\n    _timer = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/default_extensions.dart",
    "content": "/// List of default file extensions used for images.\n///\n/// This is used to make sure that only images that are allowed are picked/uploaded. The extensions\n/// should be supported by Flutter, to avoid causing issues.\n///\n/// See [Image-class documentation](https://api.flutter.dev/flutter/widgets/Image-class.html)\n///\nconst List<String> defaultImageExtensions = [\n  'jpg',\n  'png',\n  'jpeg',\n  'gif',\n  'webp',\n  'bmp',\n];\n\nbool isNotImageUrl(String url) {\n  final nonImageSuffixRegex = RegExp(\n    r'(\\.(io|html|php|json|txt|js|css|xml|md|log)(\\?.*)?(#.*)?$)|/$',\n    caseSensitive: false,\n  );\n  return nonImageSuffixRegex.hasMatch(url);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/expand_views.dart",
    "content": "import 'package:flutter/cupertino.dart';\n\nclass ViewExpanderRegistry {\n  /// the key is view id\n  final Map<String, Set<ViewExpander>> _viewExpanders = {};\n\n  bool isViewExpanded(String id) => getExpander(id)?.isViewExpanded ?? false;\n\n  void register(String id, ViewExpander expander) {\n    final expanders = _viewExpanders[id] ?? {};\n    expanders.add(expander);\n    _viewExpanders[id] = expanders;\n  }\n\n  void unregister(String id, ViewExpander expander) {\n    final expanders = _viewExpanders[id] ?? {};\n    expanders.remove(expander);\n    if (expanders.isEmpty) {\n      _viewExpanders.remove(id);\n    } else {\n      _viewExpanders[id] = expanders;\n    }\n  }\n\n  ViewExpander? getExpander(String id) {\n    final expanders = _viewExpanders[id] ?? {};\n    return expanders.isEmpty ? null : expanders.first;\n  }\n}\n\nclass ViewExpander {\n  ViewExpander(this._isExpandedCallback, this._expandCallback);\n\n  final ValueGetter<bool> _isExpandedCallback;\n  final VoidCallback _expandCallback;\n\n  bool get isViewExpanded => _isExpandedCallback.call();\n\n  void expand() => _expandCallback.call();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/field_type_extension.dart",
    "content": "import 'dart:ui';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:protobuf/protobuf.dart';\n\nextension FieldTypeExtension on FieldType {\n  String get i18n => switch (this) {\n        FieldType.RichText => LocaleKeys.grid_field_textFieldName.tr(),\n        FieldType.Number => LocaleKeys.grid_field_numberFieldName.tr(),\n        FieldType.DateTime => LocaleKeys.grid_field_dateFieldName.tr(),\n        FieldType.SingleSelect =>\n          LocaleKeys.grid_field_singleSelectFieldName.tr(),\n        FieldType.MultiSelect =>\n          LocaleKeys.grid_field_multiSelectFieldName.tr(),\n        FieldType.Checkbox => LocaleKeys.grid_field_checkboxFieldName.tr(),\n        FieldType.Checklist => LocaleKeys.grid_field_checklistFieldName.tr(),\n        FieldType.URL => LocaleKeys.grid_field_urlFieldName.tr(),\n        FieldType.LastEditedTime =>\n          LocaleKeys.grid_field_updatedAtFieldName.tr(),\n        FieldType.CreatedTime => LocaleKeys.grid_field_createdAtFieldName.tr(),\n        FieldType.Relation => LocaleKeys.grid_field_relationFieldName.tr(),\n        FieldType.Summary => LocaleKeys.grid_field_summaryFieldName.tr(),\n        FieldType.Time => LocaleKeys.grid_field_timeFieldName.tr(),\n        FieldType.Translate => LocaleKeys.grid_field_translateFieldName.tr(),\n        FieldType.Media => LocaleKeys.grid_field_mediaFieldName.tr(),\n        _ => throw UnimplementedError(),\n      };\n\n  FlowySvgData get svgData => switch (this) {\n        FieldType.RichText => FlowySvgs.text_s,\n        FieldType.Number => FlowySvgs.number_s,\n        FieldType.DateTime => FlowySvgs.date_s,\n        FieldType.SingleSelect => FlowySvgs.single_select_s,\n        FieldType.MultiSelect => FlowySvgs.multiselect_s,\n        FieldType.Checkbox => FlowySvgs.checkbox_s,\n        FieldType.URL => FlowySvgs.url_s,\n        FieldType.Checklist => FlowySvgs.checklist_s,\n        FieldType.LastEditedTime => FlowySvgs.time_s,\n        FieldType.CreatedTime => FlowySvgs.time_s,\n        FieldType.Relation => FlowySvgs.relation_s,\n        FieldType.Summary => FlowySvgs.ai_summary_s,\n        FieldType.Time => FlowySvgs.timer_start_s,\n        FieldType.Translate => FlowySvgs.ai_translate_s,\n        FieldType.Media => FlowySvgs.media_s,\n        _ => throw UnimplementedError(),\n      };\n\n  FlowySvgData? get rightIcon => switch (this) {\n        FieldType.Summary => FlowySvgs.ai_indicator_s,\n        FieldType.Translate => FlowySvgs.ai_indicator_s,\n        _ => null,\n      };\n\n  Color get mobileIconBackgroundColor => switch (this) {\n        FieldType.RichText => const Color(0xFFBECCFF),\n        FieldType.Number => const Color(0xFFCABDFF),\n        FieldType.URL => const Color(0xFFFFB9EF),\n        FieldType.SingleSelect => const Color(0xFFBECCFF),\n        FieldType.MultiSelect => const Color(0xFFBECCFF),\n        FieldType.DateTime => const Color(0xFFFDEDA7),\n        FieldType.LastEditedTime => const Color(0xFFFDEDA7),\n        FieldType.CreatedTime => const Color(0xFFFDEDA7),\n        FieldType.Checkbox => const Color(0xFF98F4CD),\n        FieldType.Checklist => const Color(0xFF98F4CD),\n        FieldType.Relation => const Color(0xFFFDEDA7),\n        FieldType.Summary => const Color(0xFFBECCFF),\n        FieldType.Time => const Color(0xFFFDEDA7),\n        FieldType.Translate => const Color(0xFFBECCFF),\n        FieldType.Media => const Color(0xFF91EBF5),\n        _ => throw UnimplementedError(),\n      };\n\n  // TODO(RS): inner icon color isn't always white\n  Color get mobileIconBackgroundColorDark => switch (this) {\n        FieldType.RichText => const Color(0xFF6859A7),\n        FieldType.Number => const Color(0xFF6859A7),\n        FieldType.URL => const Color(0xFFA75C96),\n        FieldType.SingleSelect => const Color(0xFF5366AB),\n        FieldType.MultiSelect => const Color(0xFF5366AB),\n        FieldType.DateTime => const Color(0xFFB0A26D),\n        FieldType.LastEditedTime => const Color(0xFFB0A26D),\n        FieldType.CreatedTime => const Color(0xFFB0A26D),\n        FieldType.Checkbox => const Color(0xFF42AD93),\n        FieldType.Checklist => const Color(0xFF42AD93),\n        FieldType.Relation => const Color(0xFFFDEDA7),\n        FieldType.Summary => const Color(0xFF6859A7),\n        FieldType.Time => const Color(0xFFFDEDA7),\n        FieldType.Translate => const Color(0xFF6859A7),\n        FieldType.Media => const Color(0xFF91EBF5),\n        _ => throw UnimplementedError(),\n      };\n\n  bool get canBeGroup => switch (this) {\n        FieldType.URL ||\n        FieldType.Checkbox ||\n        FieldType.MultiSelect ||\n        FieldType.SingleSelect ||\n        FieldType.DateTime =>\n          true,\n        _ => false\n      };\n\n  bool get canCreateFilter => switch (this) {\n        FieldType.Number ||\n        FieldType.Checkbox ||\n        FieldType.MultiSelect ||\n        FieldType.RichText ||\n        FieldType.SingleSelect ||\n        FieldType.Checklist ||\n        FieldType.URL ||\n        FieldType.DateTime ||\n        FieldType.CreatedTime ||\n        FieldType.LastEditedTime =>\n          true,\n        _ => false\n      };\n\n  bool get canCreateSort => switch (this) {\n        FieldType.RichText ||\n        FieldType.Checkbox ||\n        FieldType.Number ||\n        FieldType.DateTime ||\n        FieldType.SingleSelect ||\n        FieldType.MultiSelect ||\n        FieldType.LastEditedTime ||\n        FieldType.CreatedTime ||\n        FieldType.Checklist ||\n        FieldType.URL ||\n        FieldType.Time =>\n          true,\n        _ => false\n      };\n\n  bool get canEditHeader => switch (this) {\n        FieldType.MultiSelect => true,\n        FieldType.SingleSelect => true,\n        _ => false,\n      };\n\n  bool get canCreateNewGroup => switch (this) {\n        FieldType.MultiSelect => true,\n        FieldType.SingleSelect => true,\n        _ => false,\n      };\n\n  bool get canDeleteGroup => switch (this) {\n        FieldType.URL ||\n        FieldType.SingleSelect ||\n        FieldType.MultiSelect ||\n        FieldType.DateTime =>\n          true,\n        _ => false,\n      };\n\n  List<ProtobufEnum> get groupConditions {\n    switch (this) {\n      case FieldType.DateTime:\n        return DateConditionPB.values;\n      default:\n        return [];\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/file_extension.dart",
    "content": "import 'dart:io';\n\nextension FileSizeExtension on String {\n  int? get fileSize {\n    final file = File(this);\n    if (file.existsSync()) {\n      return file.lengthSync();\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/font_family_extension.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/google_fonts_extension.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nextension FontFamilyExtension on String {\n  String parseFontFamilyName() => replaceAll('_regular', '')\n      .replaceAllMapped(camelCaseRegex, (m) => ' ${m.group(0)}');\n\n  // display the default font name if the font family name is empty\n  //  or using the default font family\n  String get fontFamilyDisplayName => isEmpty || this == defaultFontFamily\n      ? LocaleKeys.settings_appearance_fontFamily_defaultFont.tr()\n      : parseFontFamilyName();\n\n  // the font display name is not the same as the font family name\n  // for example, the display name is \"Noto Sans HK\"\n  // the font family name is \"NotoSansHK_Regular\"\n  String get fontFamilyName => isEmpty || this == defaultFontFamily\n      ? defaultFontFamily\n      : getGoogleFontSafely(this).fontFamily ?? defaultFontFamily;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/int64_extension.dart",
    "content": "import 'package:fixnum/fixnum.dart';\n\nextension DateConversion on Int64 {\n  DateTime toDateTime() => DateTime.fromMillisecondsSinceEpoch(toInt() * 1000);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/json_print.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy_backend/log.dart';\nimport 'package:flutter/material.dart';\n\nconst JsonEncoder _encoder = JsonEncoder.withIndent('  ');\nvoid prettyPrintJson(Object? object) {\n  Log.trace(_encoder.convert(object));\n  debugPrint(_encoder.convert(object));\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/levenshtein.dart",
    "content": "import 'dart:math';\n\nint levenshtein(String s, String t, {bool caseSensitive = true}) {\n  if (!caseSensitive) {\n    s = s.toLowerCase();\n    t = t.toLowerCase();\n  }\n\n  if (s == t) return 0;\n\n  final v0 = List<int>.generate(t.length + 1, (i) => i);\n  final v1 = List<int>.filled(t.length + 1, 0);\n\n  for (var i = 0; i < s.length; i++) {\n    v1[0] = i + 1;\n\n    for (var j = 0; j < t.length; j++) {\n      final cost = (s[i] == t[j]) ? 0 : 1;\n      v1[j + 1] = min(v1[j] + 1, min(v0[j + 1] + 1, v0[j] + cost));\n    }\n\n    v0.setAll(0, v1);\n  }\n\n  return v1[t.length];\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/navigator_context_extension.dart",
    "content": "import 'package:flutter/material.dart';\n\nextension NavigatorContext on BuildContext {\n  void popToHome() {\n    Navigator.of(this).popUntil((route) {\n      if (route.settings.name == '/') {\n        return true;\n      }\n      return false;\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/share_log_files.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:archive/archive_io.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\nimport 'package:share_plus/share_plus.dart';\n\nFuture<void> shareLogFiles(BuildContext? context) async {\n  final dir = await getApplicationSupportDirectory();\n  final zipEncoder = ZipEncoder();\n\n  final archiveLogFiles = dir\n      .listSync(recursive: true)\n      .where((e) => p.basename(e.path).startsWith('log.'))\n      .map((e) {\n    final bytes = File(e.path).readAsBytesSync();\n    return ArchiveFile(p.basename(e.path), bytes.length, bytes);\n  });\n\n  if (archiveLogFiles.isEmpty) {\n    if (context != null && context.mounted) {\n      showToastNotification(\n        message: LocaleKeys.noLogFiles.tr(),\n        type: ToastificationType.error,\n      );\n    }\n    return;\n  }\n\n  final archive = Archive();\n  for (final file in archiveLogFiles) {\n    archive.addFile(file);\n  }\n\n  final zip = zipEncoder.encode(archive);\n  if (zip == null) {\n    if (context != null && context.mounted) {\n      showToastNotification(\n        message: LocaleKeys.noLogFiles.tr(),\n        type: ToastificationType.error,\n      );\n    }\n    return;\n  }\n\n  // create a zipped appflowy logs file\n  try {\n    final tempDirectory = await getTemporaryDirectory();\n    final path = Platform.isAndroid ? tempDirectory.path : dir.path;\n    final zipFile =\n        await File(p.join(path, 'appflowy_logs.zip')).writeAsBytes(zip);\n\n    if (Platform.isIOS) {\n      await Share.shareUri(zipFile.uri);\n      // delete the zipped appflowy logs file\n      await zipFile.delete();\n    } else if (Platform.isAndroid) {\n      await Share.shareXFiles([XFile(zipFile.path)]);\n      // delete the zipped appflowy logs file\n      await zipFile.delete();\n    } else {\n      // open the directory\n      await afLaunchUri(zipFile.uri);\n    }\n  } catch (e) {\n    if (context != null && context.mounted) {\n      showToastNotification(\n        message: e.toString(),\n        type: ToastificationType.error,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/string_extension.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/shared/icon_emoji_picker/icon.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart' hide Icon;\n\nextension StringExtension on String {\n  static const _specialCharacters = r'\\/:*?\"<>| ';\n\n  /// Encode a string to a file name.\n  ///\n  /// Normalizes the string to remove special characters and replaces the \"\\/:*?\"<>|\" with underscores.\n  String toFileName() {\n    final buffer = StringBuffer();\n    for (final character in characters) {\n      if (_specialCharacters.contains(character)) {\n        buffer.write('_');\n      } else {\n        buffer.write(character);\n      }\n    }\n    return buffer.toString();\n  }\n\n  /// Returns the file size of the file at the given path.\n  ///\n  /// Returns null if the file does not exist.\n  int? get fileSize {\n    final file = File(this);\n    if (file.existsSync()) {\n      return file.lengthSync();\n    }\n    return null;\n  }\n\n  /// Returns true if the string is a appflowy cloud url.\n  bool get isAppFlowyCloudUrl => appflowyCloudUrlRegex.hasMatch(this);\n\n  /// Returns the color of the string.\n  ///\n  /// ONLY used for the cover.\n  Color? coverColor(BuildContext context) {\n    // try to parse the color from the tint id,\n    //  if it fails, try to parse the color as a hex string\n    return FlowyTint.fromId(this)?.color(context) ?? tryToColor();\n  }\n\n  String orDefault(String defaultValue) {\n    return isEmpty ? defaultValue : this;\n  }\n}\n\nextension NullableStringExtension on String? {\n  String orDefault(String defaultValue) {\n    return this?.isEmpty ?? true ? defaultValue : this ?? '';\n  }\n}\n\nextension IconExtension on String {\n  Icon? get icon {\n    final values = split('/');\n    if (values.length != 2) {\n      return null;\n    }\n    final iconGroup = IconGroup(name: values.first, icons: []);\n    if (kDebugMode) {\n      // Ensure the icon group and icon exist\n      assert(kIconGroups!.any((group) => group.name == values.first));\n      assert(\n        kIconGroups!\n            .firstWhere((group) => group.name == values.first)\n            .icons\n            .any((icon) => icon.name == values.last),\n      );\n    }\n    return Icon(\n      content: values.last,\n      name: values.last,\n      keywords: [],\n    )..iconGroup = iconGroup;\n  }\n}\n\nextension CounterExtension on String {\n  Counters getCounter() {\n    final wordCount = wordRegex.allMatches(this).length;\n    final charCount = runes.length;\n    return Counters(wordCount: wordCount, charCount: charCount);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/theme_extension.dart",
    "content": "import 'package:flutter/material.dart';\n\nextension IsLightMode on ThemeData {\n  bool get isLightMode => brightness == Brightness.light;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/theme_mode_extension.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nextension LabelTextPhrasing on ThemeMode {\n  String get labelText => switch (this) {\n        ThemeMode.light => LocaleKeys.settings_appearance_themeMode_light.tr(),\n        ThemeMode.dark => LocaleKeys.settings_appearance_themeMode_dark.tr(),\n        ThemeMode.system =>\n          LocaleKeys.settings_appearance_themeMode_system.tr(),\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/throttle.dart",
    "content": "import 'dart:async';\n\nclass Throttler {\n  Throttler({\n    this.duration = const Duration(milliseconds: 1000),\n  });\n\n  final Duration duration;\n  Timer? _timer;\n\n  void call(Function callback) {\n    if (_timer?.isActive ?? false) return;\n\n    _timer = Timer(duration, () {\n      callback();\n    });\n  }\n\n  void cancel() {\n    _timer?.cancel();\n  }\n\n  void dispose() {\n    _timer?.cancel();\n    _timer = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/time.dart",
    "content": "final RegExp timerRegExp =\n    RegExp(r'(?:(?<hours>\\d*)h)? ?(?:(?<minutes>\\d*)m)?');\n\nint? parseTime(String timerStr) {\n  int? res = int.tryParse(timerStr);\n  if (res != null) {\n    return res;\n  }\n\n  final matches = timerRegExp.firstMatch(timerStr);\n  if (matches == null) {\n    return null;\n  }\n  final hours = int.tryParse(matches.namedGroup('hours') ?? \"\");\n  final minutes = int.tryParse(matches.namedGroup('minutes') ?? \"\");\n  if (hours == null && minutes == null) {\n    return null;\n  }\n\n  final expected =\n      \"${hours != null ? '${hours}h' : ''}${hours != null && minutes != null ? ' ' : ''}${minutes != null ? '${minutes}m' : ''}\";\n  if (timerStr != expected) {\n    return null;\n  }\n\n  res = 0;\n  res += hours != null ? hours * 60 : res;\n  res += minutes ?? 0;\n\n  return res;\n}\n\nString formatTime(int minutes) {\n  if (minutes >= 60) {\n    if (minutes % 60 == 0) {\n      return \"${minutes ~/ 60}h\";\n    }\n    return \"${minutes ~/ 60}h ${minutes % 60}m\";\n  } else if (minutes >= 0) {\n    return \"${minutes}m\";\n  }\n  return \"\";\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/util/xfile_ext.dart",
    "content": "import 'package:appflowy/shared/patterns/file_type_patterns.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pbenum.dart';\nimport 'package:cross_file/cross_file.dart';\n\nenum FileType {\n  other,\n  image,\n  link,\n  document,\n  archive,\n  video,\n  audio,\n  text;\n}\n\nextension TypeRecognizer on XFile {\n  FileType get fileType {\n    // Prefer mime over using regexp as it is more reliable.\n    // Refer to Microsoft Documentation for common mime types: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types\n    if (mimeType?.isNotEmpty == true) {\n      if (mimeType!.contains('image')) {\n        return FileType.image;\n      }\n      if (mimeType!.contains('video')) {\n        return FileType.video;\n      }\n      if (mimeType!.contains('audio')) {\n        return FileType.audio;\n      }\n      if (mimeType!.contains('text')) {\n        return FileType.text;\n      }\n      if (mimeType!.contains('application')) {\n        if (mimeType!.contains('pdf') ||\n            mimeType!.contains('doc') ||\n            mimeType!.contains('docx')) {\n          return FileType.document;\n        }\n        if (mimeType!.contains('zip') ||\n            mimeType!.contains('tar') ||\n            mimeType!.contains('gz') ||\n            mimeType!.contains('7z') ||\n            // archive is used in eg. Java archives (jar)\n            mimeType!.contains('archive') ||\n            mimeType!.contains('rar')) {\n          return FileType.archive;\n        }\n        if (mimeType!.contains('rtf')) {\n          return FileType.text;\n        }\n      }\n\n      return FileType.other;\n    }\n\n    // Check if the file is an image\n    if (imgExtensionRegex.hasMatch(path)) {\n      return FileType.image;\n    }\n\n    // Check if the file is a video\n    if (videoExtensionRegex.hasMatch(path)) {\n      return FileType.video;\n    }\n\n    // Check if the file is an audio\n    if (audioExtensionRegex.hasMatch(path)) {\n      return FileType.audio;\n    }\n\n    // Check if the file is a document\n    if (documentExtensionRegex.hasMatch(path)) {\n      return FileType.document;\n    }\n\n    // Check if the file is an archive\n    if (archiveExtensionRegex.hasMatch(path)) {\n      return FileType.archive;\n    }\n\n    // Check if the file is a text\n    if (textExtensionRegex.hasMatch(path)) {\n      return FileType.text;\n    }\n\n    return FileType.other;\n  }\n}\n\nextension ToMediaFileTypePB on FileType {\n  MediaFileTypePB toMediaFileTypePB() {\n    switch (this) {\n      case FileType.image:\n        return MediaFileTypePB.Image;\n      case FileType.video:\n        return MediaFileTypePB.Video;\n      case FileType.audio:\n        return MediaFileTypePB.Audio;\n      case FileType.document:\n        return MediaFileTypePB.Document;\n      case FileType.archive:\n        return MediaFileTypePB.Archive;\n      case FileType.text:\n        return MediaFileTypePB.Text;\n      default:\n        return MediaFileTypePB.Other;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/action_navigation/action_navigation_bloc.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'action_navigation_bloc.freezed.dart';\n\nclass ActionNavigationBloc\n    extends Bloc<ActionNavigationEvent, ActionNavigationState> {\n  ActionNavigationBloc() : super(const ActionNavigationState.initial()) {\n    on<ActionNavigationEvent>((event, emit) async {\n      await event.when(\n        performAction: (action, showErrorToast, nextActions) async {\n          NavigationAction currentAction = action;\n          if (currentAction.arguments?[ActionArgumentKeys.view] == null &&\n              action.type == ActionType.openView) {\n            final result = await ViewBackendService.getView(action.objectId);\n            final view = result.toNullable();\n            if (view != null) {\n              if (currentAction.arguments == null) {\n                currentAction = currentAction.copyWith(arguments: {});\n              }\n              currentAction.arguments?.addAll({ActionArgumentKeys.view: view});\n\n            } else {\n              Log.error('Open view failed: ${action.objectId}');\n              if (showErrorToast) {\n                showToastNotification(\n                  message: LocaleKeys.search_pageNotExist.tr(),\n                  type: ToastificationType.error,\n                );\n              }\n            }\n          }\n\n          emit(state.copyWith(action: currentAction, nextActions: nextActions));\n\n          if (nextActions.isNotEmpty) {\n            final newActions = [...nextActions];\n            final next = newActions.removeAt(0);\n\n            add(\n              ActionNavigationEvent.performAction(\n                action: next,\n                nextActions: newActions,\n              ),\n            );\n          } else {\n            emit(state.setNoAction());\n          }\n        },\n      );\n    });\n  }\n}\n\n@freezed\nclass ActionNavigationEvent with _$ActionNavigationEvent {\n  const factory ActionNavigationEvent.performAction({\n    required NavigationAction action,\n    @Default(false) bool showErrorToast,\n    @Default([]) List<NavigationAction> nextActions,\n  }) = _PerformAction;\n}\n\nclass ActionNavigationState {\n  const ActionNavigationState.initial()\n      : action = null,\n        nextActions = const [];\n\n  const ActionNavigationState({\n    required this.action,\n    this.nextActions = const [],\n  });\n\n  final NavigationAction? action;\n  final List<NavigationAction> nextActions;\n\n  ActionNavigationState copyWith({\n    NavigationAction? action,\n    List<NavigationAction>? nextActions,\n  }) =>\n      ActionNavigationState(\n        action: action ?? this.action,\n        nextActions: nextActions ?? this.nextActions,\n      );\n\n  ActionNavigationState setNoAction() =>\n      const ActionNavigationState(action: null);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/action_navigation/navigation_action.dart",
    "content": "enum ActionType {\n  openView,\n  jumpToBlock,\n  openRow,\n}\n\nclass ActionArgumentKeys {\n  static String view = \"view\";\n  static String nodePath = \"node_path\";\n  static String blockId = \"block_id\";\n  static String rowId = \"row_id\";\n}\n\n/// A [NavigationAction] is used to communicate with the\n/// [ActionNavigationBloc] to perform actions based on an event\n/// triggered by pressing a notification, such as opening a specific\n/// view and jumping to a specific block.\n///\nclass NavigationAction {\n  const NavigationAction({\n    this.type = ActionType.openView,\n    this.arguments,\n    required this.objectId,\n  });\n\n  final ActionType type;\n\n  final String objectId;\n  final Map<String, dynamic>? arguments;\n\n  NavigationAction copyWith({\n    ActionType? type,\n    String? objectId,\n    Map<String, dynamic>? arguments,\n  }) =>\n      NavigationAction(\n        type: type ?? this.type,\n        objectId: objectId ?? this.objectId,\n        arguments: arguments ?? this.arguments,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/appearance_defaults.dart",
    "content": "import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:flowy_infra/theme.dart';\nimport 'package:flutter/material.dart';\n\n/// A class for the default appearance settings for the app\nclass DefaultAppearanceSettings {\n  static const kDefaultFontFamily = defaultFontFamily;\n  static const kDefaultThemeMode = ThemeMode.system;\n  static const kDefaultThemeName = \"Default\";\n  static const kDefaultTheme = BuiltInTheme.defaultTheme;\n\n  static Color getDefaultCursorColor(BuildContext context) {\n    return Theme.of(context).colorScheme.primary;\n  }\n\n  static Color getDefaultSelectionColor(BuildContext context) {\n    return Theme.of(context).colorScheme.primary.withValues(alpha: 0.2);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/trash/application/trash_listener.dart';\nimport 'package:appflowy/plugins/trash/application/trash_service.dart';\nimport 'package:appflowy/workspace/application/command_palette/search_service.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'command_palette_bloc.freezed.dart';\n\nclass Debouncer {\n  Debouncer({required this.delay});\n\n  final Duration delay;\n  Timer? _timer;\n\n  void run(void Function() action) {\n    _timer?.cancel();\n    _timer = Timer(delay, action);\n  }\n\n  void dispose() {\n    _timer?.cancel();\n  }\n}\n\nclass CommandPaletteBloc\n    extends Bloc<CommandPaletteEvent, CommandPaletteState> {\n  CommandPaletteBloc() : super(CommandPaletteState.initial()) {\n    on<_SearchChanged>(_onSearchChanged);\n    on<_PerformSearch>(_onPerformSearch);\n    on<_NewSearchStream>(_onNewSearchStream);\n    on<_ResultsChanged>(_onResultsChanged);\n    on<_TrashChanged>(_onTrashChanged);\n    on<_WorkspaceChanged>(_onWorkspaceChanged);\n    on<_ClearSearch>(_onClearSearch);\n    on<_GoingToAskAI>(_onGoingToAskAI);\n    on<_AskedAI>(_onAskedAI);\n    on<_RefreshCachedViews>(_onRefreshCachedViews);\n    on<_UpdateCachedViews>(_onUpdateCachedViews);\n\n    _initTrash();\n    _refreshCachedViews();\n  }\n\n  final Debouncer _searchDebouncer = Debouncer(\n    delay: const Duration(milliseconds: 300),\n  );\n  final TrashService _trashService = TrashService();\n  final TrashListener _trashListener = TrashListener();\n  String? _activeQuery;\n\n  @override\n  Future<void> close() {\n    _trashListener.close();\n    _searchDebouncer.dispose();\n    state.searchResponseStream?.dispose();\n    return super.close();\n  }\n\n  Future<void> _initTrash() async {\n    _trashListener.start(\n      trashUpdated: (trashOrFailed) => add(\n        CommandPaletteEvent.trashChanged(\n          trash: trashOrFailed.toNullable(),\n        ),\n      ),\n    );\n\n    final trashOrFailure = await _trashService.readTrash();\n    trashOrFailure.fold(\n      (trash) {\n        if (!isClosed) {\n          add(CommandPaletteEvent.trashChanged(trash: trash.items));\n        }\n      },\n      (error) => debugPrint('Failed to load trash: $error'),\n    );\n  }\n\n  Future<void> _refreshCachedViews() async {\n    /// Sometimes non-existent views appear in the search results\n    /// and the icon data for the search results is empty\n    /// Fetching all views can temporarily resolve these issues\n    final repeatedViewPB =\n        (await ViewBackendService.getAllViews()).toNullable();\n    if (repeatedViewPB == null || isClosed) return;\n    add(CommandPaletteEvent.updateCachedViews(views: repeatedViewPB.items));\n  }\n\n  FutureOr<void> _onRefreshCachedViews(\n    _RefreshCachedViews event,\n    Emitter<CommandPaletteState> emit,\n  ) {\n    _refreshCachedViews();\n  }\n\n  FutureOr<void> _onUpdateCachedViews(\n    _UpdateCachedViews event,\n    Emitter<CommandPaletteState> emit,\n  ) {\n    final cachedViews = <String, ViewPB>{};\n    for (final view in event.views) {\n      cachedViews[view.id] = view;\n    }\n    emit(state.copyWith(cachedViews: cachedViews));\n  }\n\n  FutureOr<void> _onSearchChanged(\n    _SearchChanged event,\n    Emitter<CommandPaletteState> emit,\n  ) {\n    _searchDebouncer.run(\n      () {\n        if (!isClosed) {\n          add(CommandPaletteEvent.performSearch(search: event.search));\n        }\n      },\n    );\n  }\n\n  FutureOr<void> _onPerformSearch(\n    _PerformSearch event,\n    Emitter<CommandPaletteState> emit,\n  ) async {\n    if (event.search.isEmpty) {\n      emit(\n        state.copyWith(\n          query: null,\n          searching: false,\n          serverResponseItems: [],\n          localResponseItems: [],\n          combinedResponseItems: {},\n          resultSummaries: [],\n          generatingAIOverview: false,\n        ),\n      );\n    } else {\n      emit(state.copyWith(query: event.search, searching: true));\n      _activeQuery = event.search;\n\n      unawaited(\n        SearchBackendService.performSearch(\n          event.search,\n        ).then(\n          (result) => result.fold(\n            (stream) {\n              if (!isClosed && _activeQuery == event.search) {\n                add(CommandPaletteEvent.newSearchStream(stream: stream));\n              }\n            },\n            (error) {\n              debugPrint('Search error: $error');\n              if (!isClosed) {\n                add(\n                  CommandPaletteEvent.resultsChanged(\n                    searchId: '',\n                    searching: false,\n                    generatingAIOverview: false,\n                  ),\n                );\n              }\n            },\n          ),\n        ),\n      );\n    }\n  }\n\n  FutureOr<void> _onNewSearchStream(\n    _NewSearchStream event,\n    Emitter<CommandPaletteState> emit,\n  ) {\n    state.searchResponseStream?.dispose();\n    emit(\n      state.copyWith(\n        searchId: event.stream.searchId,\n        searchResponseStream: event.stream,\n      ),\n    );\n\n    event.stream.listen(\n      onLocalItems: (items, searchId) => _handleResultsUpdate(\n        searchId: searchId,\n        localItems: items,\n      ),\n      onServerItems: (items, searchId, searching, generatingAIOverview) =>\n          _handleResultsUpdate(\n        searchId: searchId,\n        summaries: [], // when got server search result, summaries should be empty\n        serverItems: items,\n        searching: searching,\n        generatingAIOverview: generatingAIOverview,\n      ),\n      onSummaries: (summaries, searchId, searching, generatingAIOverview) =>\n          _handleResultsUpdate(\n        searchId: searchId,\n        summaries: summaries,\n        searching: searching,\n        generatingAIOverview: generatingAIOverview,\n      ),\n      onFinished: (searchId) => _handleResultsUpdate(\n        searchId: searchId,\n        searching: false,\n      ),\n    );\n  }\n\n  void _handleResultsUpdate({\n    required String searchId,\n    List<SearchResponseItemPB>? serverItems,\n    List<LocalSearchResponseItemPB>? localItems,\n    List<SearchSummaryPB>? summaries,\n    bool searching = true,\n    bool generatingAIOverview = false,\n  }) {\n    if (_isActiveSearch(searchId)) {\n      add(\n        CommandPaletteEvent.resultsChanged(\n          searchId: searchId,\n          serverItems: serverItems,\n          localItems: localItems,\n          summaries: summaries,\n          searching: searching,\n          generatingAIOverview: generatingAIOverview,\n        ),\n      );\n    }\n  }\n\n  FutureOr<void> _onResultsChanged(\n    _ResultsChanged event,\n    Emitter<CommandPaletteState> emit,\n  ) async {\n    if (state.searchId != event.searchId) return;\n\n    final combinedItems = <String, SearchResultItem>{};\n    for (final item in event.serverItems ?? state.serverResponseItems) {\n      combinedItems[item.id] = SearchResultItem(\n        id: item.id,\n        icon: item.icon,\n        displayName: item.displayName,\n        content: item.content,\n        workspaceId: item.workspaceId,\n      );\n    }\n\n    for (final item in event.localItems ?? state.localResponseItems) {\n      combinedItems.putIfAbsent(\n        item.id,\n        () => SearchResultItem(\n          id: item.id,\n          icon: item.icon,\n          displayName: item.displayName,\n          content: '',\n          workspaceId: item.workspaceId,\n        ),\n      );\n    }\n\n    emit(\n      state.copyWith(\n        serverResponseItems: event.serverItems ?? state.serverResponseItems,\n        localResponseItems: event.localItems ?? state.localResponseItems,\n        resultSummaries: event.summaries ?? state.resultSummaries,\n        combinedResponseItems: combinedItems,\n        searching: event.searching,\n        generatingAIOverview: event.generatingAIOverview,\n      ),\n    );\n  }\n\n  FutureOr<void> _onTrashChanged(\n    _TrashChanged event,\n    Emitter<CommandPaletteState> emit,\n  ) async {\n    if (event.trash != null) {\n      emit(state.copyWith(trash: event.trash!));\n    } else {\n      final trashOrFailure = await _trashService.readTrash();\n      trashOrFailure.fold((trash) {\n        emit(state.copyWith(trash: trash.items));\n      }, (error) {\n        // Optionally handle error; otherwise, we simply do nothing.\n      });\n    }\n  }\n\n  FutureOr<void> _onWorkspaceChanged(\n    _WorkspaceChanged event,\n    Emitter<CommandPaletteState> emit,\n  ) {\n    emit(\n      state.copyWith(\n        query: '',\n        serverResponseItems: [],\n        localResponseItems: [],\n        combinedResponseItems: {},\n        resultSummaries: [],\n        searching: false,\n        generatingAIOverview: false,\n      ),\n    );\n    _refreshCachedViews();\n  }\n\n  FutureOr<void> _onClearSearch(\n    _ClearSearch event,\n    Emitter<CommandPaletteState> emit,\n  ) {\n    emit(CommandPaletteState.initial().copyWith(trash: state.trash));\n  }\n\n  FutureOr<void> _onGoingToAskAI(\n    _GoingToAskAI event,\n    Emitter<CommandPaletteState> emit,\n  ) {\n    emit(state.copyWith(askAI: true, askAISources: event.sources));\n  }\n\n  FutureOr<void> _onAskedAI(\n    _AskedAI event,\n    Emitter<CommandPaletteState> emit,\n  ) {\n    emit(state.copyWith(askAI: false, askAISources: null));\n  }\n\n  bool _isActiveSearch(String searchId) =>\n      !isClosed && state.searchId == searchId;\n}\n\n@freezed\nclass CommandPaletteEvent with _$CommandPaletteEvent {\n  const factory CommandPaletteEvent.searchChanged({required String search}) =\n      _SearchChanged;\n  const factory CommandPaletteEvent.performSearch({required String search}) =\n      _PerformSearch;\n  const factory CommandPaletteEvent.newSearchStream({\n    required SearchResponseStream stream,\n  }) = _NewSearchStream;\n  const factory CommandPaletteEvent.resultsChanged({\n    required String searchId,\n    required bool searching,\n    required bool generatingAIOverview,\n    List<SearchResponseItemPB>? serverItems,\n    List<LocalSearchResponseItemPB>? localItems,\n    List<SearchSummaryPB>? summaries,\n  }) = _ResultsChanged;\n\n  const factory CommandPaletteEvent.trashChanged({\n    @Default(null) List<TrashPB>? trash,\n  }) = _TrashChanged;\n  const factory CommandPaletteEvent.workspaceChanged({\n    @Default(null) String? workspaceId,\n  }) = _WorkspaceChanged;\n  const factory CommandPaletteEvent.clearSearch() = _ClearSearch;\n  const factory CommandPaletteEvent.goingToAskAI({\n    @Default(null) List<SearchSourcePB>? sources,\n  }) = _GoingToAskAI;\n  const factory CommandPaletteEvent.askedAI() = _AskedAI;\n  const factory CommandPaletteEvent.refreshCachedViews() = _RefreshCachedViews;\n  const factory CommandPaletteEvent.updateCachedViews({\n    required List<ViewPB> views,\n  }) = _UpdateCachedViews;\n}\n\nclass SearchResultItem {\n  const SearchResultItem({\n    required this.id,\n    required this.icon,\n    required this.content,\n    required this.displayName,\n    this.workspaceId,\n  });\n\n  final String id;\n  final String content;\n  final ResultIconPB icon;\n  final String displayName;\n  final String? workspaceId;\n}\n\n@freezed\nclass CommandPaletteState with _$CommandPaletteState {\n  const CommandPaletteState._();\n  const factory CommandPaletteState({\n    @Default(null) String? query,\n    @Default([]) List<SearchResponseItemPB> serverResponseItems,\n    @Default([]) List<LocalSearchResponseItemPB> localResponseItems,\n    @Default({}) Map<String, SearchResultItem> combinedResponseItems,\n    @Default({}) Map<String, ViewPB> cachedViews,\n    @Default([]) List<SearchSummaryPB> resultSummaries,\n    @Default(null) SearchResponseStream? searchResponseStream,\n    required bool searching,\n    required bool generatingAIOverview,\n    @Default(false) bool askAI,\n    @Default(null) List<SearchSourcePB>? askAISources,\n    @Default([]) List<TrashPB> trash,\n    @Default(null) String? searchId,\n  }) = _CommandPaletteState;\n\n  factory CommandPaletteState.initial() => const CommandPaletteState(\n        searching: false,\n        generatingAIOverview: false,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nextension GetIcon on ResultIconPB {\n  Widget? getIcon({\n    double size = 18.0,\n    double lineHeight = 1.0,\n    Color? iconColor,\n  }) {\n    final iconValue = value, iconType = ty;\n    if (iconType == ResultIconTypePB.Emoji) {\n      if (iconValue.isEmpty) return null;\n      if (UniversalPlatform.isMobile) {\n        return Text(\n          iconValue,\n          strutStyle: StrutStyle(\n            fontSize: size,\n            height: lineHeight,\n\n            /// currently [forceStrutHeight] set to true seems only work in iOS\n            forceStrutHeight: UniversalPlatform.isIOS,\n            leadingDistribution: TextLeadingDistribution.even,\n          ),\n        );\n      }\n      return RawEmojiIconWidget(\n        emoji: EmojiIconData.emoji(iconValue),\n        emojiSize: size,\n        lineHeight: lineHeight,\n      );\n    } else if (iconType == ResultIconTypePB.Icon ||\n        iconType == ResultIconTypePB.Url) {\n      if (_resultIconValueTypes.contains(iconValue)) {\n        return FlowySvg(\n          getViewSvg(),\n          size: Size.square(size),\n          color: iconColor,\n        );\n      }\n      return RawEmojiIconWidget(\n        emoji: EmojiIconData(iconType.toFlowyIconType(), iconValue),\n        emojiSize: size,\n        lineHeight: lineHeight,\n      );\n    }\n    return null;\n  }\n}\n\nextension ResultIconTypePBToFlowyIconType on ResultIconTypePB {\n  FlowyIconType toFlowyIconType() {\n    switch (this) {\n      case ResultIconTypePB.Emoji:\n        return FlowyIconType.emoji;\n      case ResultIconTypePB.Icon:\n        return FlowyIconType.icon;\n      case ResultIconTypePB.Url:\n        return FlowyIconType.custom;\n      default:\n        return FlowyIconType.custom;\n    }\n  }\n}\n\nextension _ToViewIcon on ResultIconPB {\n  FlowySvgData getViewSvg() => switch (value) {\n        \"0\" => FlowySvgs.icon_document_s,\n        \"1\" => FlowySvgs.icon_grid_s,\n        \"2\" => FlowySvgs.icon_board_s,\n        \"3\" => FlowySvgs.icon_calendar_s,\n        \"4\" => FlowySvgs.chat_ai_page_s,\n        _ => FlowySvgs.icon_document_s,\n      };\n}\n\nconst _resultIconValueTypes = {'0', '1', '2', '3', '4'};\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'search_result_list_bloc.freezed.dart';\n\nclass SearchResultListBloc\n    extends Bloc<SearchResultListEvent, SearchResultListState> {\n  SearchResultListBloc() : super(SearchResultListState.initial()) {\n    // Register event handlers\n    on<_OnHoverSummary>(_onHoverSummary);\n    on<_OnHoverResult>(_onHoverResult);\n    on<_OpenPage>(_onOpenPage);\n  }\n\n  FutureOr<void> _onHoverSummary(\n    _OnHoverSummary event,\n    Emitter<SearchResultListState> emit,\n  ) {\n    emit(\n      state.copyWith(\n        hoveredSummary: event.summary,\n        hoveredResult: null,\n        userHovered: event.userHovered,\n        openPageId: null,\n      ),\n    );\n  }\n\n  FutureOr<void> _onHoverResult(\n    _OnHoverResult event,\n    Emitter<SearchResultListState> emit,\n  ) {\n    emit(\n      state.copyWith(\n        hoveredSummary: null,\n        hoveredResult: event.item,\n        userHovered: event.userHovered,\n        openPageId: null,\n      ),\n    );\n  }\n\n  FutureOr<void> _onOpenPage(\n    _OpenPage event,\n    Emitter<SearchResultListState> emit,\n  ) {\n    emit(state.copyWith(openPageId: event.pageId));\n  }\n}\n\n@freezed\nclass SearchResultListEvent with _$SearchResultListEvent {\n  const factory SearchResultListEvent.onHoverSummary({\n    required SearchSummaryPB summary,\n    required bool userHovered,\n  }) = _OnHoverSummary;\n  const factory SearchResultListEvent.onHoverResult({\n    required SearchResultItem item,\n    required bool userHovered,\n  }) = _OnHoverResult;\n\n  const factory SearchResultListEvent.openPage({\n    required String pageId,\n  }) = _OpenPage;\n}\n\n@freezed\nclass SearchResultListState with _$SearchResultListState {\n  const SearchResultListState._();\n  const factory SearchResultListState({\n    @Default(null) SearchSummaryPB? hoveredSummary,\n    @Default(null) SearchResultItem? hoveredResult,\n    @Default(null) String? openPageId,\n    @Default(false) bool userHovered,\n  }) = _SearchResultListState;\n\n  factory SearchResultListState.initial() => const SearchResultListState();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart",
    "content": "import 'dart:async';\nimport 'dart:ffi';\nimport 'dart:isolate';\nimport 'dart:typed_data';\n\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-search/query.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:fixnum/fixnum.dart';\n\nclass SearchBackendService {\n  static Future<FlowyResult<SearchResponseStream, FlowyError>> performSearch(\n    String keyword,\n  ) async {\n    final searchId = DateTime.now().millisecondsSinceEpoch.toString();\n    final stream = SearchResponseStream(searchId: searchId);\n\n    final request = SearchQueryPB(\n      search: keyword,\n      searchId: searchId,\n      streamPort: Int64(stream.nativePort),\n    );\n\n    unawaited(SearchEventStreamSearch(request).send());\n    return FlowyResult.success(stream);\n  }\n}\n\nclass SearchResponseStream {\n  SearchResponseStream({required this.searchId}) {\n    _port.handler = _controller.add;\n    _subscription = _controller.stream.listen(\n      (Uint8List data) => _onResultsChanged(data),\n    );\n  }\n\n  final String searchId;\n  final RawReceivePort _port = RawReceivePort();\n  final StreamController<Uint8List> _controller = StreamController.broadcast();\n  late StreamSubscription<Uint8List> _subscription;\n  void Function(\n    List<SearchResponseItemPB> items,\n    String searchId,\n    bool searching,\n    bool generatingAIOverview,\n  )? _onServerItems;\n  void Function(\n    List<SearchSummaryPB> summaries,\n    String searchId,\n    bool searching,\n    bool generatingAIOverview,\n  )? _onSummaries;\n\n  void Function(\n    List<LocalSearchResponseItemPB> items,\n    String searchId,\n  )? _onLocalItems;\n\n  void Function(String searchId)? _onFinished;\n  int get nativePort => _port.sendPort.nativePort;\n\n  Future<void> dispose() async {\n    await _subscription.cancel();\n    _port.close();\n  }\n\n  void _onResultsChanged(Uint8List data) {\n    final searchState = SearchStatePB.fromBuffer(data);\n\n    if (searchState.hasResponse()) {\n      if (searchState.response.hasSearchResult()) {\n        _onServerItems?.call(\n          searchState.response.searchResult.items,\n          searchId,\n          searchState.response.searching,\n          searchState.response.generatingAiSummary,\n        );\n      }\n      if (searchState.response.hasSearchSummary()) {\n        _onSummaries?.call(\n          searchState.response.searchSummary.items,\n          searchId,\n          searchState.response.searching,\n          searchState.response.generatingAiSummary,\n        );\n      }\n\n      if (searchState.response.hasLocalSearchResult()) {\n        _onLocalItems?.call(\n          searchState.response.localSearchResult.items,\n          searchId,\n        );\n      }\n    } else {\n      _onFinished?.call(searchId);\n    }\n  }\n\n  void listen({\n    required void Function(\n      List<SearchResponseItemPB> items,\n      String searchId,\n      bool isLoading,\n      bool generatingAIOverview,\n    )? onServerItems,\n    required void Function(\n      List<SearchSummaryPB> summaries,\n      String searchId,\n      bool isLoading,\n      bool generatingAIOverview,\n    )? onSummaries,\n    required void Function(\n      List<LocalSearchResponseItemPB> items,\n      String searchId,\n    )? onLocalItems,\n    required void Function(String searchId)? onFinished,\n  }) {\n    _onServerItems = onServerItems;\n    _onSummaries = onSummaries;\n    _onLocalItems = onLocalItems;\n    _onFinished = onFinished;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/edit_panel/edit_context.dart",
    "content": "import 'package:equatable/equatable.dart';\nimport 'package:flutter/material.dart';\n\nabstract class EditPanelContext extends Equatable {\n  const EditPanelContext({\n    required this.identifier,\n    required this.title,\n    required this.child,\n  });\n\n  final String identifier;\n  final String title;\n  final Widget child;\n\n  @override\n  List<Object> get props => [identifier];\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/edit_panel/edit_panel_bloc.dart",
    "content": "import 'package:appflowy/workspace/application/edit_panel/edit_context.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'edit_panel_bloc.freezed.dart';\n\nclass EditPanelBloc extends Bloc<EditPanelEvent, EditPanelState> {\n  EditPanelBloc() : super(EditPanelState.initial()) {\n    on<EditPanelEvent>((event, emit) async {\n      await event.map(\n        startEdit: (e) async {\n          emit(state.copyWith(isEditing: true, editContext: e.context));\n        },\n        endEdit: (value) async {\n          emit(state.copyWith(isEditing: false, editContext: null));\n        },\n      );\n    });\n  }\n}\n\n@freezed\nclass EditPanelEvent with _$EditPanelEvent {\n  const factory EditPanelEvent.startEdit(EditPanelContext context) = _StartEdit;\n\n  const factory EditPanelEvent.endEdit(EditPanelContext context) = _EndEdit;\n}\n\n@freezed\nclass EditPanelState with _$EditPanelState {\n  const factory EditPanelState({\n    required bool isEditing,\n    required EditPanelContext? editContext,\n  }) = _EditPanelState;\n\n  factory EditPanelState.initial() => const EditPanelState(\n        isEditing: false,\n        editContext: null,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';\nimport 'package:appflowy/plugins/document/application/prelude.dart';\nimport 'package:appflowy/shared/markdown_to_document.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nenum DocumentExportType {\n  json,\n  markdown,\n  text,\n  html,\n}\n\nclass DocumentExporter {\n  const DocumentExporter(\n    this.view,\n  );\n\n  final ViewPB view;\n\n  Future<FlowyResult<String, FlowyError>> export(\n    DocumentExportType type, {\n    String? path,\n  }) async {\n    final documentService = DocumentService();\n    final result = await documentService.openDocument(documentId: view.id);\n    return result.fold(\n      (r) async {\n        final document = r.toDocument();\n        if (document == null) {\n          return FlowyResult.failure(\n            FlowyError(\n              msg: LocaleKeys.settings_files_exportFileFail.tr(),\n            ),\n          );\n        }\n        switch (type) {\n          case DocumentExportType.json:\n            return FlowyResult.success(jsonEncode(document));\n          case DocumentExportType.markdown:\n            if (path != null) {\n              await customDocumentToMarkdown(document, path: path);\n              return FlowyResult.success('');\n            } else {\n              return FlowyResult.success(\n                await customDocumentToMarkdown(document),\n              );\n            }\n          case DocumentExportType.text:\n            throw UnimplementedError();\n          case DocumentExportType.html:\n            final html = documentToHTML(\n              document,\n            );\n            return FlowyResult.success(html);\n        }\n      },\n      (error) => FlowyResult.failure(error),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart",
    "content": "import 'package:appflowy/workspace/application/favorite/favorite_service.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport 'favorite_listener.dart';\n\npart 'favorite_bloc.freezed.dart';\n\nclass FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {\n  FavoriteBloc() : super(FavoriteState.initial()) {\n    _dispatch();\n  }\n\n  final _service = FavoriteService();\n  final _listener = FavoriteListener();\n  bool isReordering = false;\n\n  @override\n  Future<void> close() async {\n    await _listener.stop();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<FavoriteEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            _listener.start(\n              favoritesUpdated: _onFavoritesUpdated,\n            );\n            add(const FavoriteEvent.fetchFavorites());\n          },\n          fetchFavorites: () async {\n            final result = await _service.readFavorites();\n            emit(\n              result.fold(\n                (favoriteViews) {\n                  final views = favoriteViews.items.toList();\n                  final pinnedViews =\n                      views.where((v) => v.item.isPinned).toList();\n                  final unpinnedViews =\n                      views.where((v) => !v.item.isPinned).toList();\n                  return state.copyWith(\n                    isLoading: false,\n                    views: views,\n                    pinnedViews: pinnedViews,\n                    unpinnedViews: unpinnedViews,\n                  );\n                },\n                (error) => state.copyWith(\n                  isLoading: false,\n                  views: [],\n                ),\n              ),\n            );\n          },\n          toggle: (view) async {\n            final isFavorited = state.views.any((v) => v.item.id == view.id);\n            if (isFavorited) {\n              await _service.unpinFavorite(view);\n            } else if (state.pinnedViews.length < 3) {\n              // pin the view if there are less than 3 pinned views\n              await _service.pinFavorite(view);\n            }\n\n            await _service.toggleFavorite(view.id);\n          },\n          pin: (view) async {\n            await _service.pinFavorite(view);\n            add(const FavoriteEvent.fetchFavorites());\n          },\n          unpin: (view) async {\n            await _service.unpinFavorite(view);\n            add(const FavoriteEvent.fetchFavorites());\n          },\n          reorder: (oldIndex, newIndex) async {\n            /// TODO: this is a workaround to reorder the favorite views\n            isReordering = true;\n            final pinnedViews = state.pinnedViews.toList();\n            if (oldIndex < newIndex) newIndex -= 1;\n            final target = pinnedViews.removeAt(oldIndex);\n            pinnedViews.insert(newIndex, target);\n            emit(state.copyWith(pinnedViews: pinnedViews));\n            for (final view in pinnedViews) {\n              await _service.toggleFavorite(view.item.id);\n              await _service.toggleFavorite(view.item.id);\n            }\n            if (!isClosed) {\n              add(const FavoriteEvent.fetchFavorites());\n            }\n            isReordering = false;\n          },\n        );\n      },\n    );\n  }\n\n  void _onFavoritesUpdated(\n    FlowyResult<RepeatedViewPB, FlowyError> favoriteOrFailed,\n    bool didFavorite,\n  ) {\n    if (!isReordering) {\n      favoriteOrFailed.fold(\n        (favorite) => add(const FetchFavorites()),\n        (error) => Log.error(error),\n      );\n    }\n  }\n}\n\n@freezed\nclass FavoriteEvent with _$FavoriteEvent {\n  const factory FavoriteEvent.initial() = Initial;\n\n  const factory FavoriteEvent.toggle(ViewPB view) = ToggleFavorite;\n\n  const factory FavoriteEvent.fetchFavorites() = FetchFavorites;\n\n  const factory FavoriteEvent.pin(ViewPB view) = PinFavorite;\n\n  const factory FavoriteEvent.unpin(ViewPB view) = UnpinFavorite;\n\n  const factory FavoriteEvent.reorder(int oldIndex, int newIndex) =\n      ReorderFavorite;\n}\n\n@freezed\nclass FavoriteState with _$FavoriteState {\n  const factory FavoriteState({\n    @Default([]) List<SectionViewPB> views,\n    @Default([]) List<SectionViewPB> pinnedViews,\n    @Default([]) List<SectionViewPB> unpinnedViews,\n    @Default(true) bool isLoading,\n  }) = _FavoriteState;\n\n  factory FavoriteState.initial() => const FavoriteState();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_listener.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/core/notification/folder_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter/foundation.dart';\n\ntypedef FavoriteUpdated = void Function(\n  FlowyResult<RepeatedViewPB, FlowyError> result,\n  bool isFavorite,\n);\n\nclass FavoriteListener {\n  StreamSubscription<SubscribeObject>? _streamSubscription;\n  FolderNotificationParser? _parser;\n\n  FavoriteUpdated? _favoriteUpdated;\n\n  void start({\n    FavoriteUpdated? favoritesUpdated,\n  }) {\n    _favoriteUpdated = favoritesUpdated;\n    _parser = FolderNotificationParser(\n      id: 'favorite',\n      callback: _observableCallback,\n    );\n    _streamSubscription = RustStreamReceiver.listen(\n      (observable) => _parser?.parse(observable),\n    );\n  }\n\n  void _observableCallback(\n    FolderNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case FolderNotification.DidFavoriteView:\n        result.onSuccess(\n          (success) => _favoriteUpdated?.call(\n            FlowyResult.success(RepeatedViewPB.fromBuffer(success)),\n            true,\n          ),\n        );\n      case FolderNotification.DidUnfavoriteView:\n        result.map(\n          (success) => _favoriteUpdated?.call(\n            FlowyResult.success(RepeatedViewPB.fromBuffer(success)),\n            false,\n          ),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    _parser = null;\n    await _streamSubscription?.cancel();\n    _streamSubscription = null;\n    _favoriteUpdated = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:collection/collection.dart';\n\nclass FavoriteService {\n  Future<FlowyResult<RepeatedFavoriteViewPB, FlowyError>> readFavorites() {\n    final result = FolderEventReadFavorites().send();\n    return result.then((result) {\n      return result.fold(\n        (favoriteViews) {\n          return FlowyResult.success(\n            RepeatedFavoriteViewPB(\n              items: favoriteViews.items.where((e) => !e.item.isSpace),\n            ),\n          );\n        },\n        (error) => FlowyResult.failure(error),\n      );\n    });\n  }\n\n  Future<FlowyResult<void, FlowyError>> toggleFavorite(String viewId) async {\n    final id = RepeatedViewIdPB.create()..items.add(viewId);\n    return FolderEventToggleFavorite(id).send();\n  }\n\n  Future<FlowyResult<void, FlowyError>> pinFavorite(ViewPB view) async {\n    return pinOrUnpinFavorite(view, true);\n  }\n\n  Future<FlowyResult<void, FlowyError>> unpinFavorite(ViewPB view) async {\n    return pinOrUnpinFavorite(view, false);\n  }\n\n  Future<FlowyResult<void, FlowyError>> pinOrUnpinFavorite(\n    ViewPB view,\n    bool isPinned,\n  ) async {\n    try {\n      final current =\n          view.extra.isNotEmpty ? jsonDecode(view.extra) : <String, dynamic>{};\n      final merged = mergeMaps(\n        current,\n        <String, dynamic>{ViewExtKeys.isPinnedKey: isPinned},\n      );\n      await ViewBackendService.updateView(\n        viewId: view.id,\n        extra: jsonEncode(merged),\n      );\n    } catch (e) {\n      return FlowyResult.failure(FlowyError(msg: 'Failed to pin favorite: $e'));\n    }\n\n    return FlowyResult.success(null);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/favorite/prelude.dart",
    "content": "export 'favorite_bloc.dart';\nexport 'favorite_listener.dart';\nexport 'favorite_service.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart",
    "content": "import 'package:appflowy/user/application/user_listener.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'\n    show WorkspaceLatestPB;\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'home_bloc.freezed.dart';\n\nclass HomeBloc extends Bloc<HomeEvent, HomeState> {\n  HomeBloc(WorkspaceLatestPB workspaceSetting)\n      : _workspaceListener = FolderListener(\n          workspaceId: workspaceSetting.workspaceId,\n        ),\n        super(HomeState.initial(workspaceSetting)) {\n    _dispatch(workspaceSetting);\n  }\n\n  final FolderListener _workspaceListener;\n\n  @override\n  Future<void> close() async {\n    await _workspaceListener.stop();\n    return super.close();\n  }\n\n  void _dispatch(WorkspaceLatestPB workspaceSetting) {\n    on<HomeEvent>(\n      (event, emit) async {\n        await event.map(\n          initial: (_Initial value) {\n            Future.delayed(const Duration(milliseconds: 300), () {\n              if (!isClosed) {\n                add(HomeEvent.didReceiveWorkspaceSetting(workspaceSetting));\n              }\n            });\n\n            _workspaceListener.start(\n              onLatestUpdated: (result) {\n                result.fold(\n                  (latest) => add(HomeEvent.didReceiveWorkspaceSetting(latest)),\n                  (r) => Log.error(r),\n                );\n              },\n            );\n          },\n          showLoading: (e) async {\n            emit(state.copyWith(isLoading: e.isLoading));\n          },\n          didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) {\n            // the latest view is shared across all the members of the workspace.\n\n            final latestView = value.setting.hasLatestView()\n                ? value.setting.latestView\n                : state.latestView;\n\n            if (latestView != null && latestView.isSpace) {\n              // If the latest view is a space, we don't need to open it.\n              return;\n            }\n\n            emit(\n              state.copyWith(\n                workspaceSetting: value.setting,\n                latestView: latestView,\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\n@freezed\nclass HomeEvent with _$HomeEvent {\n  const factory HomeEvent.initial() = _Initial;\n  const factory HomeEvent.showLoading(bool isLoading) = _ShowLoading;\n  const factory HomeEvent.didReceiveWorkspaceSetting(\n    WorkspaceLatestPB setting,\n  ) = _DidReceiveWorkspaceSetting;\n}\n\n@freezed\nclass HomeState with _$HomeState {\n  const factory HomeState({\n    required bool isLoading,\n    required WorkspaceLatestPB workspaceSetting,\n    ViewPB? latestView,\n  }) = _HomeState;\n\n  factory HomeState.initial(WorkspaceLatestPB workspaceSetting) => HomeState(\n        isLoading: false,\n        workspaceSetting: workspaceSetting,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart",
    "content": "import 'package:appflowy/user/application/user_listener.dart';\nimport 'package:appflowy/workspace/application/edit_panel/edit_context.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'\n    show WorkspaceLatestPB;\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/time/duration.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'home_setting_bloc.freezed.dart';\n\nclass HomeSettingBloc extends Bloc<HomeSettingEvent, HomeSettingState> {\n  HomeSettingBloc(\n    WorkspaceLatestPB workspaceSetting,\n    AppearanceSettingsCubit appearanceSettingsCubit,\n    double screenWidthPx,\n  )   : _listener = FolderListener(workspaceId: workspaceSetting.workspaceId),\n        _appearanceSettingsCubit = appearanceSettingsCubit,\n        super(\n          HomeSettingState.initial(\n            workspaceSetting,\n            appearanceSettingsCubit.state,\n            screenWidthPx,\n          ),\n        ) {\n    _dispatch();\n  }\n\n  final FolderListener _listener;\n  final AppearanceSettingsCubit _appearanceSettingsCubit;\n\n  @override\n  Future<void> close() async {\n    await _listener.stop();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<HomeSettingEvent>(\n      (event, emit) async {\n        await event.map(\n          initial: (_Initial value) {},\n          setEditPanel: (e) async {\n            emit(state.copyWith(panelContext: e.editContext));\n          },\n          dismissEditPanel: (value) async {\n            emit(state.copyWith(panelContext: null));\n          },\n          didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) {\n            emit(state.copyWith(workspaceSetting: value.setting));\n          },\n          changeMenuStatus: (_CollapseMenu e) {\n            final status = e.status;\n            if (state.menuStatus == status) return;\n            if (status != MenuStatus.floating) {\n              _appearanceSettingsCubit.saveIsMenuCollapsed(\n                status == MenuStatus.expanded ? false : true,\n              );\n            }\n            emit(\n              state.copyWith(menuStatus: status),\n            );\n          },\n          collapseNotificationPanel: (_) {\n            final isNotificationPanelCollapsed =\n                !state.isNotificationPanelCollapsed;\n            emit(\n              state.copyWith(\n                isNotificationPanelCollapsed: isNotificationPanelCollapsed,\n              ),\n            );\n          },\n          checkScreenSize: (_CheckScreenSize e) {\n            final bool isScreenSmall =\n                e.screenWidthPx < PageBreaks.tabletLandscape;\n            if (state.isScreenSmall == isScreenSmall) return;\n            if (state.hasColappsedMenuManually) {\n              emit(state.copyWith(isScreenSmall: isScreenSmall));\n            } else {\n              MenuStatus menuStatus = state.menuStatus;\n              if (isScreenSmall && menuStatus == MenuStatus.expanded) {\n                menuStatus = MenuStatus.hidden;\n              } else if (!isScreenSmall && menuStatus == MenuStatus.hidden) {\n                menuStatus = MenuStatus.expanded;\n              }\n              emit(\n                state.copyWith(\n                  menuStatus: menuStatus,\n                  isScreenSmall: isScreenSmall,\n                ),\n              );\n            }\n          },\n          editPanelResizeStart: (_EditPanelResizeStart e) {\n            emit(\n              state.copyWith(\n                resizeType: MenuResizeType.drag,\n                resizeStart: state.resizeOffset,\n              ),\n            );\n          },\n          editPanelResized: (_EditPanelResized e) {\n            final newPosition =\n                (state.resizeStart + e.offset).clamp(0, 200).toDouble();\n            if (state.resizeOffset != newPosition) {\n              emit(state.copyWith(resizeOffset: newPosition));\n            }\n          },\n          editPanelResizeEnd: (_EditPanelResizeEnd e) {\n            _appearanceSettingsCubit.saveMenuOffset(state.resizeOffset);\n            emit(state.copyWith(resizeType: MenuResizeType.slide));\n          },\n        );\n      },\n    );\n  }\n\n  bool get isMenuHidden => state.menuStatus == MenuStatus.hidden;\n\n  bool get isMenuExpanded => state.menuStatus == MenuStatus.expanded;\n\n  void collapseMenu() {\n    if (isMenuExpanded) {\n      add(HomeSettingEvent.changeMenuStatus(MenuStatus.hidden));\n    } else if (isMenuHidden) {\n      add(HomeSettingEvent.changeMenuStatus(MenuStatus.expanded));\n    }\n  }\n}\n\nenum MenuResizeType {\n  slide,\n  drag,\n}\n\nextension MenuResizeTypeExtension on MenuResizeType {\n  Duration duration() {\n    switch (this) {\n      case MenuResizeType.drag:\n        return 30.milliseconds;\n      case MenuResizeType.slide:\n        return 350.milliseconds;\n    }\n  }\n}\n\n@freezed\nclass HomeSettingEvent with _$HomeSettingEvent {\n  const factory HomeSettingEvent.initial() = _Initial;\n\n  const factory HomeSettingEvent.setEditPanel(EditPanelContext editContext) =\n      _ShowEditPanel;\n\n  const factory HomeSettingEvent.dismissEditPanel() = _DismissEditPanel;\n\n  const factory HomeSettingEvent.didReceiveWorkspaceSetting(\n    WorkspaceLatestPB setting,\n  ) = _DidReceiveWorkspaceSetting;\n\n  const factory HomeSettingEvent.changeMenuStatus(MenuStatus status) =\n      _CollapseMenu;\n\n  const factory HomeSettingEvent.collapseNotificationPanel() =\n      _CollapseNotificationPanel;\n\n  const factory HomeSettingEvent.checkScreenSize(double screenWidthPx) =\n      _CheckScreenSize;\n\n  const factory HomeSettingEvent.editPanelResized(double offset) =\n      _EditPanelResized;\n\n  const factory HomeSettingEvent.editPanelResizeStart() = _EditPanelResizeStart;\n\n  const factory HomeSettingEvent.editPanelResizeEnd() = _EditPanelResizeEnd;\n}\n\n@freezed\nclass HomeSettingState with _$HomeSettingState {\n  const factory HomeSettingState({\n    required EditPanelContext? panelContext,\n    required WorkspaceLatestPB workspaceSetting,\n    required bool unauthorized,\n    required MenuStatus menuStatus,\n    required bool isNotificationPanelCollapsed,\n    required bool isScreenSmall,\n    required bool hasColappsedMenuManually,\n    required double resizeOffset,\n    required double resizeStart,\n    required MenuResizeType resizeType,\n  }) = _HomeSettingState;\n\n  factory HomeSettingState.initial(\n    WorkspaceLatestPB workspaceSetting,\n    AppearanceSettingsState appearanceSettingsState,\n    double screenWidthPx,\n  ) {\n    return HomeSettingState(\n      panelContext: null,\n      workspaceSetting: workspaceSetting,\n      unauthorized: false,\n      menuStatus: appearanceSettingsState.isMenuCollapsed\n          ? MenuStatus.hidden\n          : MenuStatus.expanded,\n      isNotificationPanelCollapsed: true,\n      isScreenSmall: screenWidthPx < PageBreaks.tabletLandscape,\n      hasColappsedMenuManually: false,\n      resizeOffset: appearanceSettingsState.menuOffset,\n      resizeStart: 0,\n      resizeType: MenuResizeType.slide,\n    );\n  }\n}\n\nenum MenuStatus { hidden, expanded, floating }\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/home/prelude.dart",
    "content": "\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/menu/menu_user_bloc.dart",
    "content": "import 'package:appflowy/user/application/user_listener.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'menu_user_bloc.freezed.dart';\n\nclass MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {\n  MenuUserBloc(this.userProfile, this.workspaceId)\n      : _userListener = UserListener(userProfile: userProfile),\n        _userWorkspaceListener = FolderListener(\n          workspaceId: workspaceId,\n        ),\n        _userService = UserBackendService(userId: userProfile.id),\n        super(MenuUserState.initial(userProfile)) {\n    _dispatch();\n  }\n\n  final String workspaceId;\n  final UserBackendService _userService;\n  final UserListener _userListener;\n  final FolderListener _userWorkspaceListener;\n  final UserProfilePB userProfile;\n\n  @override\n  Future<void> close() async {\n    await _userListener.stop();\n    await _userWorkspaceListener.stop();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<MenuUserEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            _userListener.start(onProfileUpdated: _profileUpdated);\n            await _initUser();\n          },\n          didReceiveUserProfile: (UserProfilePB newUserProfile) {\n            emit(state.copyWith(userProfile: newUserProfile));\n          },\n          updateUserName: (String name) {\n            _userService.updateUserProfile(name: name).then((result) {\n              result.fold(\n                (l) => null,\n                (err) => Log.error(err),\n              );\n            });\n          },\n        );\n      },\n    );\n  }\n\n  Future<void> _initUser() async {\n    final result = await _userService.initUser();\n    result.fold((l) => null, (error) => Log.error(error));\n  }\n\n  void _profileUpdated(\n    FlowyResult<UserProfilePB, FlowyError> userProfileOrFailed,\n  ) {\n    if (isClosed) {\n      return;\n    }\n    userProfileOrFailed.fold(\n      (profile) => add(MenuUserEvent.didReceiveUserProfile(profile)),\n      (err) => Log.error(err),\n    );\n  }\n}\n\n@freezed\nclass MenuUserEvent with _$MenuUserEvent {\n  const factory MenuUserEvent.initial() = _Initial;\n  const factory MenuUserEvent.updateUserName(String name) = _UpdateUserName;\n  const factory MenuUserEvent.didReceiveUserProfile(\n    UserProfilePB newUserProfile,\n  ) = _DidReceiveUserProfile;\n}\n\n@freezed\nclass MenuUserState with _$MenuUserState {\n  const factory MenuUserState({\n    required UserProfilePB userProfile,\n    required List<WorkspacePB>? workspaces,\n    required FlowyResult<void, String> successOrFailure,\n  }) = _MenuUserState;\n\n  factory MenuUserState.initial(UserProfilePB userProfile) => MenuUserState(\n        userProfile: userProfile,\n        workspaces: null,\n        successOrFailure: FlowyResult.success(null),\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/menu/prelude.dart",
    "content": "export 'menu_user_bloc.dart';\nexport 'sidebar_sections_bloc.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/workspace/workspace_sections_listener.dart';\nimport 'package:appflowy/workspace/application/workspace/workspace_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'sidebar_sections_bloc.freezed.dart';\n\nclass SidebarSection {\n  const SidebarSection({\n    required this.publicViews,\n    required this.privateViews,\n  });\n\n  const SidebarSection.empty()\n      : publicViews = const [],\n        privateViews = const [];\n\n  final List<ViewPB> publicViews;\n  final List<ViewPB> privateViews;\n\n  List<ViewPB> get views => publicViews + privateViews;\n\n  SidebarSection copyWith({\n    List<ViewPB>? publicViews,\n    List<ViewPB>? privateViews,\n  }) {\n    return SidebarSection(\n      publicViews: publicViews ?? this.publicViews,\n      privateViews: privateViews ?? this.privateViews,\n    );\n  }\n}\n\n/// The [SidebarSectionsBloc] is responsible for\n///   managing the root views in different sections of the workspace.\nclass SidebarSectionsBloc\n    extends Bloc<SidebarSectionsEvent, SidebarSectionsState> {\n  SidebarSectionsBloc() : super(SidebarSectionsState.initial()) {\n    on<SidebarSectionsEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: (userProfile, workspaceId) async {\n            _initial(userProfile, workspaceId);\n            final sectionViews = await _getSectionViews();\n            if (sectionViews != null) {\n              final containsSpace = _containsSpace(sectionViews);\n              emit(\n                state.copyWith(\n                  section: sectionViews,\n                  containsSpace: containsSpace,\n                ),\n              );\n            }\n          },\n          reset: (userProfile, workspaceId) async {\n            _reset(userProfile, workspaceId);\n            final sectionViews = await _getSectionViews();\n            if (sectionViews != null) {\n              final containsSpace = _containsSpace(sectionViews);\n              emit(\n                state.copyWith(\n                  section: sectionViews,\n                  containsSpace: containsSpace,\n                ),\n              );\n            }\n          },\n          createRootViewInSection: (name, section, index) async {\n            final result = await _workspaceService.createView(\n              name: name,\n              viewSection: section,\n              index: index,\n            );\n            result.fold(\n              (view) => emit(\n                state.copyWith(\n                  lastCreatedRootView: view,\n                  createRootViewResult: FlowyResult.success(null),\n                ),\n              ),\n              (error) {\n                Log.error('Failed to create root view: $error');\n                emit(\n                  state.copyWith(\n                    createRootViewResult: FlowyResult.failure(error),\n                  ),\n                );\n              },\n            );\n          },\n          receiveSectionViewsUpdate: (sectionViews) async {\n            final section = sectionViews.section;\n            switch (section) {\n              case ViewSectionPB.Public:\n                emit(\n                  state.copyWith(\n                    containsSpace: state.containsSpace ||\n                        sectionViews.views.any((view) => view.isSpace),\n                    section: state.section.copyWith(\n                      publicViews: sectionViews.views,\n                    ),\n                  ),\n                );\n              case ViewSectionPB.Private:\n                emit(\n                  state.copyWith(\n                    containsSpace: state.containsSpace ||\n                        sectionViews.views.any((view) => view.isSpace),\n                    section: state.section.copyWith(\n                      privateViews: sectionViews.views,\n                    ),\n                  ),\n                );\n                break;\n              default:\n                break;\n            }\n          },\n          moveRootView: (fromIndex, toIndex, fromSection, toSection) async {\n            final views = fromSection == ViewSectionPB.Public\n                ? List<ViewPB>.from(state.section.publicViews)\n                : List<ViewPB>.from(state.section.privateViews);\n            if (fromIndex < 0 || fromIndex >= views.length) {\n              Log.error(\n                'Invalid fromIndex: $fromIndex, maxIndex: ${views.length - 1}',\n              );\n              return;\n            }\n            final view = views[fromIndex];\n            final result = await _workspaceService.moveView(\n              viewId: view.id,\n              fromIndex: fromIndex,\n              toIndex: toIndex,\n            );\n            result.fold(\n              (value) {\n                views.insert(toIndex, views.removeAt(fromIndex));\n                var newState = state;\n                if (fromSection == ViewSectionPB.Public) {\n                  newState = newState.copyWith(\n                    section: newState.section.copyWith(publicViews: views),\n                  );\n                } else if (fromSection == ViewSectionPB.Private) {\n                  newState = newState.copyWith(\n                    section: newState.section.copyWith(privateViews: views),\n                  );\n                }\n                emit(newState);\n              },\n              (error) {\n                Log.error('Failed to move root view: $error');\n              },\n            );\n          },\n          reload: (userProfile, workspaceId) async {\n            _initial(userProfile, workspaceId);\n            final sectionViews = await _getSectionViews();\n            if (sectionViews != null) {\n              final containsSpace = _containsSpace(sectionViews);\n              emit(\n                state.copyWith(\n                  section: sectionViews,\n                  containsSpace: containsSpace,\n                ),\n              );\n              // try to open the fist view in public section or private section\n              if (sectionViews.publicViews.isNotEmpty) {\n                getIt<TabsBloc>().add(\n                  TabsEvent.openPlugin(\n                    plugin: sectionViews.publicViews.first.plugin(),\n                  ),\n                );\n              } else if (sectionViews.privateViews.isNotEmpty) {\n                getIt<TabsBloc>().add(\n                  TabsEvent.openPlugin(\n                    plugin: sectionViews.privateViews.first.plugin(),\n                  ),\n                );\n              } else {\n                getIt<TabsBloc>().add(\n                  TabsEvent.openPlugin(\n                    plugin: makePlugin(pluginType: PluginType.blank),\n                  ),\n                );\n              }\n            }\n          },\n        );\n      },\n    );\n  }\n\n  late WorkspaceService _workspaceService;\n  WorkspaceSectionsListener? _listener;\n\n  @override\n  Future<void> close() async {\n    await _listener?.stop();\n    _listener = null;\n    return super.close();\n  }\n\n  ViewSectionPB? getViewSection(ViewPB view) {\n    final publicViews = state.section.publicViews.map((e) => e.id);\n    final privateViews = state.section.privateViews.map((e) => e.id);\n    if (publicViews.contains(view.id)) {\n      return ViewSectionPB.Public;\n    } else if (privateViews.contains(view.id)) {\n      return ViewSectionPB.Private;\n    } else {\n      return null;\n    }\n  }\n\n  Future<SidebarSection?> _getSectionViews() async {\n    try {\n      final publicViews = await _workspaceService.getPublicViews().getOrThrow();\n      final privateViews =\n          await _workspaceService.getPrivateViews().getOrThrow();\n      return SidebarSection(\n        publicViews: publicViews,\n        privateViews: privateViews,\n      );\n    } catch (e) {\n      Log.error('Failed to get section views: $e');\n      return null;\n    }\n  }\n\n  bool _containsSpace(SidebarSection section) {\n    return section.publicViews.any((view) => view.isSpace) ||\n        section.privateViews.any((view) => view.isSpace);\n  }\n\n  void _initial(UserProfilePB userProfile, String workspaceId) {\n    _workspaceService = WorkspaceService(\n      workspaceId: workspaceId,\n      userId: userProfile.id,\n    );\n\n    _listener = WorkspaceSectionsListener(\n      user: userProfile,\n      workspaceId: workspaceId,\n    )..start(\n        sectionChanged: (result) {\n          if (!isClosed) {\n            result.fold(\n              (s) => add(SidebarSectionsEvent.receiveSectionViewsUpdate(s)),\n              (f) => Log.error('Failed to receive section views: $f'),\n            );\n          }\n        },\n      );\n  }\n\n  void _reset(UserProfilePB userProfile, String workspaceId) {\n    _listener?.stop();\n    _listener = null;\n\n    _initial(userProfile, workspaceId);\n  }\n}\n\n@freezed\nclass SidebarSectionsEvent with _$SidebarSectionsEvent {\n  const factory SidebarSectionsEvent.initial(\n    UserProfilePB userProfile,\n    String workspaceId,\n  ) = _Initial;\n  const factory SidebarSectionsEvent.reset(\n    UserProfilePB userProfile,\n    String workspaceId,\n  ) = _Reset;\n  const factory SidebarSectionsEvent.createRootViewInSection({\n    required String name,\n    required ViewSectionPB viewSection,\n    int? index,\n  }) = _CreateRootViewInSection;\n  const factory SidebarSectionsEvent.moveRootView({\n    required int fromIndex,\n    required int toIndex,\n    required ViewSectionPB fromSection,\n    required ViewSectionPB toSection,\n  }) = _MoveRootView;\n  const factory SidebarSectionsEvent.receiveSectionViewsUpdate(\n    SectionViewsPB sectionViews,\n  ) = _ReceiveSectionViewsUpdate;\n  const factory SidebarSectionsEvent.reload(\n    UserProfilePB userProfile,\n    String workspaceId,\n  ) = _Reload;\n}\n\n@freezed\nclass SidebarSectionsState with _$SidebarSectionsState {\n  const factory SidebarSectionsState({\n    required SidebarSection section,\n    @Default(null) ViewPB? lastCreatedRootView,\n    FlowyResult<void, FlowyError>? createRootViewResult,\n    @Default(true) bool containsSpace,\n  }) = _SidebarSectionsState;\n\n  factory SidebarSectionsState.initial() => const SidebarSectionsState(\n        section: SidebarSection.empty(),\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart",
    "content": "import 'package:flutter/foundation.dart';\nimport 'package:local_notifier/local_notifier.dart';\n\n/// The app name used in the local notification.\n///\n/// DO NOT Use i18n here, because the i18n plugin is not ready\n///   before the local notification is initialized.\nconst _localNotifierAppName = 'AppFlowy';\n\n/// Manages Local Notifications\n///\n/// Currently supports:\n///  - MacOS\n///  - Windows\n///  - Linux\n///\nclass NotificationService {\n  static Future<void> initialize() async {\n    await localNotifier.setup(\n      appName: _localNotifierAppName,\n      // Don't create a shortcut on Windows, because the setup.exe will create a shortcut\n      shortcutPolicy: ShortcutPolicy.requireNoCreate,\n    );\n  }\n}\n\n/// Creates and shows a Notification\n///\nclass NotificationMessage {\n  NotificationMessage({\n    required String title,\n    required String body,\n    String? identifier,\n    VoidCallback? onClick,\n  }) {\n    _notification = LocalNotification(\n      identifier: identifier,\n      title: title,\n      body: body,\n    )..onClick = onClick;\n\n    _show();\n  }\n\n  late final LocalNotification _notification;\n\n  void _show() => _notification.show();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/shared/list_extension.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/recent/recent_listener.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flutter/foundation.dart';\n\n/// This is a lazy-singleton to share recent views across the application.\n///\n/// Use-cases:\n/// - Desktop: Command Palette recent view history\n/// - Desktop: (Documents) Inline-page reference recent view history\n/// - Mobile: Recent view history on home screen\n///\n/// See the related [LaunchTask] in [RecentServiceTask].\n///\nclass CachedRecentService {\n  CachedRecentService();\n\n  Completer<void> _completer = Completer();\n\n  ValueNotifier<List<SectionViewPB>> notifier = ValueNotifier(const []);\n\n  List<SectionViewPB> get _recentViews => notifier.value;\n  set _recentViews(List<SectionViewPB> value) => notifier.value = value;\n\n  final _listener = RecentViewsListener();\n\n  bool isDisposed = false;\n\n  Future<List<SectionViewPB>> recentViews() async {\n    if (_isInitialized || _completer.isCompleted) return _recentViews;\n\n    _isInitialized = true;\n\n    _listener.start(recentViewsUpdated: _recentViewsUpdated);\n    _recentViews = await _readRecentViews().fold(\n      (s) => s.items.unique((e) => e.item.id),\n      (_) => [],\n    );\n    _completer.complete();\n\n    return _recentViews;\n  }\n\n  /// Updates the recent views history\n  Future<FlowyResult<void, FlowyError>> updateRecentViews(\n    List<String> viewIds,\n    bool addInRecent,\n  ) async {\n    final List<String> duplicatedViewIds = [];\n    for (final viewId in viewIds) {\n      for (final view in _recentViews) {\n        if (view.item.id == viewId) {\n          duplicatedViewIds.add(viewId);\n        }\n      }\n    }\n    return FolderEventUpdateRecentViews(\n      UpdateRecentViewPayloadPB(\n        viewIds: addInRecent ? viewIds : duplicatedViewIds,\n        addInRecent: addInRecent,\n      ),\n    ).send();\n  }\n\n  Future<FlowyResult<RepeatedRecentViewPB, FlowyError>>\n      _readRecentViews() async {\n    final payload = ReadRecentViewsPB(start: Int64(), limit: Int64(100));\n    final result = await FolderEventReadRecentViews(payload).send();\n    return result.fold(\n      (recentViews) {\n        return FlowyResult.success(\n          RepeatedRecentViewPB(\n            // filter the space view and the orphan view\n            items: recentViews.items.where(\n              (e) => !e.item.isSpace && e.item.id != e.item.parentViewId,\n            ),\n          ),\n        );\n      },\n      (error) => FlowyResult.failure(error),\n    );\n  }\n\n  bool _isInitialized = false;\n\n  Future<void> reset() async {\n    await _listener.stop();\n    _resetCompleter();\n    _isInitialized = false;\n    _recentViews = const [];\n  }\n\n  Future<void> dispose() async {\n    if (isDisposed) return;\n\n    isDisposed = true;\n    notifier.dispose();\n    await _listener.stop();\n  }\n\n  void _recentViewsUpdated(\n    FlowyResult<RepeatedViewIdPB, FlowyError> result,\n  ) async {\n    final viewIds = result.toNullable();\n    if (viewIds != null) {\n      _recentViews = await _readRecentViews().fold(\n        (s) => s.items.unique((e) => e.item.id),\n        (_) => [],\n      );\n    }\n  }\n\n  void _resetCompleter() {\n    if (!_completer.isCompleted) {\n      _completer.complete();\n    }\n    _completer = Completer<void>();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/recent/prelude.dart",
    "content": "export 'cached_recent_service.dart';\nexport 'recent_views_bloc.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/recent/recent_listener.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/core/notification/folder_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter/foundation.dart';\n\ntypedef RecentViewsUpdated = void Function(\n  FlowyResult<RepeatedViewIdPB, FlowyError> result,\n);\n\nclass RecentViewsListener {\n  StreamSubscription<SubscribeObject>? _streamSubscription;\n  FolderNotificationParser? _parser;\n\n  RecentViewsUpdated? _recentViewsUpdated;\n\n  void start({\n    RecentViewsUpdated? recentViewsUpdated,\n  }) {\n    _recentViewsUpdated = recentViewsUpdated;\n    _parser = FolderNotificationParser(\n      id: 'recent_views',\n      callback: _observableCallback,\n    );\n    _streamSubscription = RustStreamReceiver.listen(\n      (observable) => _parser?.parse(observable),\n    );\n  }\n\n  void _observableCallback(\n    FolderNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    if (_recentViewsUpdated == null) {\n      return;\n    }\n\n    result.fold(\n      (payload) {\n        final view = RepeatedViewIdPB.fromBuffer(payload);\n        _recentViewsUpdated?.call(\n          FlowyResult.success(view),\n        );\n      },\n      (error) => _recentViewsUpdated?.call(\n        FlowyResult.failure(error),\n      ),\n    );\n  }\n\n  Future<void> stop() async {\n    _parser = null;\n    await _streamSubscription?.cancel();\n    _recentViewsUpdated = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/recent/recent_views_bloc.dart",
    "content": "import 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/recent/cached_recent_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'recent_views_bloc.freezed.dart';\n\nclass RecentViewsBloc extends Bloc<RecentViewsEvent, RecentViewsState> {\n  RecentViewsBloc() : super(RecentViewsState.initial()) {\n    _service = getIt<CachedRecentService>();\n    _dispatch();\n  }\n\n  late final CachedRecentService _service;\n\n  @override\n  Future<void> close() async {\n    _service.notifier.removeListener(_onRecentViewsUpdated);\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<RecentViewsEvent>(\n      (event, emit) async {\n        await event.map(\n          initial: (e) async {\n            _service.notifier.addListener(_onRecentViewsUpdated);\n            add(const RecentViewsEvent.fetchRecentViews());\n          },\n          addRecentViews: (e) async {\n            await _service.updateRecentViews(e.viewIds, true);\n          },\n          removeRecentViews: (e) async {\n            await _service.updateRecentViews(e.viewIds, false);\n          },\n          fetchRecentViews: (e) async {\n            emit(\n              state.copyWith(\n                isLoading: false,\n                views: await _service.recentViews(),\n              ),\n            );\n          },\n          resetRecentViews: (e) async {\n            await _service.reset();\n            add(const RecentViewsEvent.fetchRecentViews());\n          },\n          hoverView: (e) async {\n            emit(\n              state.copyWith(hoveredView: e.view),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  void _onRecentViewsUpdated() =>\n      add(const RecentViewsEvent.fetchRecentViews());\n}\n\n@freezed\nclass RecentViewsEvent with _$RecentViewsEvent {\n  const factory RecentViewsEvent.initial() = Initial;\n  const factory RecentViewsEvent.addRecentViews(List<String> viewIds) =\n      AddRecentViews;\n  const factory RecentViewsEvent.removeRecentViews(List<String> viewIds) =\n      RemoveRecentViews;\n  const factory RecentViewsEvent.fetchRecentViews() = FetchRecentViews;\n  const factory RecentViewsEvent.resetRecentViews() = ResetRecentViews;\n  const factory RecentViewsEvent.hoverView(ViewPB view) = HoverView;\n}\n\n@freezed\nclass RecentViewsState with _$RecentViewsState {\n  const factory RecentViewsState({\n    required List<SectionViewPB> views,\n    @Default(true) bool isLoading,\n    @Default(null) ViewPB? hoveredView,\n  }) = _RecentViewsState;\n\n  factory RecentViewsState.initial() => const RecentViewsState(views: []);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport 'local_llm_listener.dart';\n\npart 'local_ai_bloc.freezed.dart';\n\nclass LocalAiPluginBloc extends Bloc<LocalAiPluginEvent, LocalAiPluginState> {\n  LocalAiPluginBloc() : super(const LoadingLocalAiPluginState()) {\n    on<LocalAiPluginEvent>(_handleEvent);\n    _startListening();\n    _getLocalAiState();\n  }\n\n  final listener = LocalAIStateListener();\n\n  @override\n  Future<void> close() async {\n    await listener.stop();\n    return super.close();\n  }\n\n  Future<void> _handleEvent(\n    LocalAiPluginEvent event,\n    Emitter<LocalAiPluginState> emit,\n  ) async {\n    if (isClosed) {\n      return;\n    }\n\n    await event.when(\n      didReceiveAiState: (aiState) {\n        emit(\n          LocalAiPluginState.ready(\n            isEnabled: aiState.enabled,\n            isReady: aiState.isReady,\n            lackOfResource:\n                aiState.hasLackOfResource() ? aiState.lackOfResource : null,\n          ),\n        );\n      },\n      didReceiveLackOfResources: (resources) {\n        state.maybeMap(\n          ready: (readyState) {\n            emit(readyState.copyWith(lackOfResource: resources));\n          },\n          orElse: () {},\n        );\n      },\n      toggle: () async {\n        emit(LocalAiPluginState.loading());\n        await AIEventToggleLocalAI().send().fold(\n          (aiState) {\n            if (!isClosed) {\n              add(LocalAiPluginEvent.didReceiveAiState(aiState));\n            }\n          },\n          Log.error,\n        );\n      },\n      restart: () async {\n        emit(LocalAiPluginState.loading());\n        await AIEventRestartLocalAI().send();\n      },\n    );\n  }\n\n  void _startListening() {\n    listener.start(\n      stateCallback: (pluginState) {\n        if (!isClosed) {\n          add(LocalAiPluginEvent.didReceiveAiState(pluginState));\n        }\n      },\n      resourceCallback: (data) {\n        if (!isClosed) {\n          add(LocalAiPluginEvent.didReceiveLackOfResources(data));\n        }\n      },\n    );\n  }\n\n  void _getLocalAiState() {\n    AIEventGetLocalAIState().send().fold(\n      (aiState) {\n        if (!isClosed) {\n          add(LocalAiPluginEvent.didReceiveAiState(aiState));\n        }\n      },\n      Log.error,\n    );\n  }\n}\n\n@freezed\nclass LocalAiPluginEvent with _$LocalAiPluginEvent {\n  const factory LocalAiPluginEvent.didReceiveAiState(LocalAIPB aiState) =\n      _DidReceiveAiState;\n  const factory LocalAiPluginEvent.didReceiveLackOfResources(\n    LackOfAIResourcePB resources,\n  ) = _DidReceiveLackOfResources;\n  const factory LocalAiPluginEvent.toggle() = _Toggle;\n  const factory LocalAiPluginEvent.restart() = _Restart;\n}\n\n@freezed\nclass LocalAiPluginState with _$LocalAiPluginState {\n  const LocalAiPluginState._();\n\n  const factory LocalAiPluginState.ready({\n    required bool isEnabled,\n    required bool isReady,\n    required LackOfAIResourcePB? lackOfResource,\n  }) = ReadyLocalAiPluginState;\n\n  const factory LocalAiPluginState.loading() = LoadingLocalAiPluginState;\n\n  bool get isEnabled {\n    return maybeWhen(\n      ready: (isEnabled, _, ___) => isEnabled,\n      orElse: () => false,\n    );\n  }\n\n  bool get showIndicator {\n    return maybeWhen(\n      ready: (isEnabled, isReady, lackOfResource) =>\n          isReady || lackOfResource != null,\n      orElse: () => false,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'local_ai_on_boarding_bloc.freezed.dart';\n\nclass LocalAIOnBoardingBloc\n    extends Bloc<LocalAIOnBoardingEvent, LocalAIOnBoardingState> {\n  LocalAIOnBoardingBloc(\n    this.userProfile,\n    this.currentWorkspaceMemberRole,\n    this.workspaceId,\n  ) : super(const LocalAIOnBoardingState()) {\n    _userService = UserBackendService(userId: userProfile.id);\n    _successListenable = getIt<SubscriptionSuccessListenable>();\n    _successListenable.addListener(_onPaymentSuccessful);\n    _dispatch();\n  }\n\n  void _onPaymentSuccessful() {\n    if (isClosed) {\n      return;\n    }\n\n    add(\n      LocalAIOnBoardingEvent.paymentSuccessful(\n        _successListenable.subscribedPlan,\n      ),\n    );\n  }\n\n  final UserProfilePB userProfile;\n  final AFRolePB? currentWorkspaceMemberRole;\n  final String workspaceId;\n  late final IUserBackendService _userService;\n  late final SubscriptionSuccessListenable _successListenable;\n\n  @override\n  Future<void> close() async {\n    _successListenable.removeListener(_onPaymentSuccessful);\n    await super.close();\n  }\n\n  void _dispatch() {\n    on<LocalAIOnBoardingEvent>((event, emit) {\n      event.when(\n        started: () {\n          _loadSubscriptionPlans();\n        },\n        addSubscription: (plan) async {\n          emit(state.copyWith(isLoading: true));\n          final result = await _userService.createSubscription(\n            workspaceId,\n            plan,\n          );\n\n          result.fold(\n            (pl) => afLaunchUrlString(pl.paymentLink),\n            (f) => Log.error(\n              'Failed to fetch paymentlink for $plan: ${f.msg}',\n              f,\n            ),\n          );\n        },\n        didGetSubscriptionPlans: (result) {\n          result.fold(\n            (workspaceSubInfo) {\n              final isPurchaseAILocal = workspaceSubInfo.addOns.any((addOn) {\n                return addOn.type == WorkspaceAddOnPBType.AddOnAiLocal;\n              });\n\n              emit(\n                state.copyWith(isPurchaseAILocal: isPurchaseAILocal),\n              );\n            },\n            (err) {\n              Log.warn(\"Failed to get subscription plans: $err\");\n            },\n          );\n        },\n        paymentSuccessful: (SubscriptionPlanPB? plan) {\n          if (plan == SubscriptionPlanPB.AiLocal) {\n            emit(state.copyWith(isPurchaseAILocal: true, isLoading: false));\n          }\n        },\n      );\n    });\n  }\n\n  void _loadSubscriptionPlans() {\n    final payload = UserWorkspaceIdPB()..workspaceId = workspaceId;\n    UserEventGetWorkspaceSubscriptionInfo(payload).send().then((result) {\n      if (!isClosed) {\n        add(LocalAIOnBoardingEvent.didGetSubscriptionPlans(result));\n      }\n    });\n  }\n}\n\n@freezed\nclass LocalAIOnBoardingEvent with _$LocalAIOnBoardingEvent {\n  const factory LocalAIOnBoardingEvent.started() = _Started;\n  const factory LocalAIOnBoardingEvent.addSubscription(\n    SubscriptionPlanPB plan,\n  ) = _AddSubscription;\n  const factory LocalAIOnBoardingEvent.paymentSuccessful(\n    SubscriptionPlanPB? plan,\n  ) = _PaymentSuccessful;\n  const factory LocalAIOnBoardingEvent.didGetSubscriptionPlans(\n    FlowyResult<WorkspaceSubscriptionInfoPB, FlowyError> result,\n  ) = _LoadSubscriptionPlans;\n}\n\n@freezed\nclass LocalAIOnBoardingState with _$LocalAIOnBoardingState {\n  const factory LocalAIOnBoardingState({\n    @Default(false) bool isPurchaseAILocal,\n    @Default(false) bool isLoading,\n  }) = _LocalAIOnBoardingState;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/plugins/ai_chat/application/chat_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\ntypedef PluginStateCallback = void Function(LocalAIPB state);\ntypedef PluginResourceCallback = void Function(LackOfAIResourcePB data);\n\nclass LocalAIStateListener {\n  LocalAIStateListener() {\n    _parser =\n        ChatNotificationParser(id: \"appflowy_ai_plugin\", callback: _callback);\n    _subscription = RustStreamReceiver.listen(\n      (observable) => _parser?.parse(observable),\n    );\n  }\n\n  StreamSubscription<SubscribeObject>? _subscription;\n  ChatNotificationParser? _parser;\n\n  PluginStateCallback? stateCallback;\n  PluginResourceCallback? resourceCallback;\n\n  void start({\n    PluginStateCallback? stateCallback,\n    PluginResourceCallback? resourceCallback,\n  }) {\n    this.stateCallback = stateCallback;\n    this.resourceCallback = resourceCallback;\n  }\n\n  void _callback(\n    ChatNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    result.map((r) {\n      switch (ty) {\n        case ChatNotification.UpdateLocalAIState:\n          stateCallback?.call(LocalAIPB.fromBuffer(r));\n          break;\n        case ChatNotification.LocalAIResourceUpdated:\n          resourceCallback?.call(LackOfAIResourcePB.fromBuffer(r));\n          break;\n        default:\n          break;\n      }\n    });\n  }\n\n  Future<void> stop() async {\n    await _subscription?.cancel();\n    _subscription = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:collection/collection.dart';\nimport 'package:equatable/equatable.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'ollama_setting_bloc.freezed.dart';\n\nconst kDefaultChatModel = 'llama3.1:latest';\nconst kDefaultEmbeddingModel = 'nomic-embed-text:latest';\n\n/// Extension methods to map between PB and UI models\nclass OllamaSettingBloc extends Bloc<OllamaSettingEvent, OllamaSettingState> {\n  OllamaSettingBloc() : super(const OllamaSettingState()) {\n    on<_Started>(_handleStarted);\n    on<_DidLoadLocalModels>(_onLoadLocalModels);\n    on<_DidLoadSetting>(_onLoadSetting);\n    on<_UpdateSetting>(_onLoadSetting);\n    on<_OnEdit>(_onEdit);\n    on<_OnSubmit>(_onSubmit);\n    on<_SetDefaultModel>(_onSetDefaultModel);\n  }\n\n  Future<void> _handleStarted(\n    _Started event,\n    Emitter<OllamaSettingState> emit,\n  ) async {\n    try {\n      final results = await Future.wait([\n        AIEventGetLocalModelSelection().send().then((r) => r.getOrThrow()),\n        AIEventGetLocalAISetting().send().then((r) => r.getOrThrow()),\n      ]);\n\n      final models = results[0] as ModelSelectionPB;\n      final setting = results[1] as LocalAISettingPB;\n\n      if (!isClosed) {\n        add(OllamaSettingEvent.didLoadLocalModels(models));\n        add(OllamaSettingEvent.didLoadSetting(setting));\n      }\n    } catch (e, st) {\n      Log.error('Failed to load initial AI data: $e\\n$st');\n    }\n  }\n\n  void _onLoadLocalModels(\n    _DidLoadLocalModels event,\n    Emitter<OllamaSettingState> emit,\n  ) {\n    emit(state.copyWith(localModels: event.models));\n  }\n\n  void _onLoadSetting(\n    dynamic event,\n    Emitter<OllamaSettingState> emit,\n  ) {\n    final setting = (event as dynamic).setting as LocalAISettingPB;\n    final submitted = setting.toSubmittedItems();\n    emit(\n      state.copyWith(\n        setting: setting,\n        inputItems: setting.toInputItems(),\n        submittedItems: submitted,\n        originalMap: {\n          for (final item in submitted) item.settingType: item.content,\n        },\n        isEdited: false,\n      ),\n    );\n  }\n\n  void _onEdit(\n    _OnEdit event,\n    Emitter<OllamaSettingState> emit,\n  ) {\n    final updated = state.submittedItems\n        .map(\n          (item) => item.settingType == event.settingType\n              ? item.copyWith(content: event.content)\n              : item,\n        )\n        .toList();\n\n    final currentMap = {for (final i in updated) i.settingType: i.content};\n    final isEdited = !const MapEquality<SettingType, String>()\n        .equals(state.originalMap, currentMap);\n\n    emit(state.copyWith(submittedItems: updated, isEdited: isEdited));\n  }\n\n  void _onSubmit(\n    _OnSubmit event,\n    Emitter<OllamaSettingState> emit,\n  ) {\n    final pb = LocalAISettingPB();\n    for (final item in state.submittedItems) {\n      switch (item.settingType) {\n        case SettingType.serverUrl:\n          pb.serverUrl = item.content;\n          break;\n        case SettingType.chatModel:\n          pb.globalChatModel = state.selectedModel?.name ?? item.content;\n          break;\n        case SettingType.embeddingModel:\n          pb.embeddingModelName = item.content;\n          break;\n      }\n    }\n    add(OllamaSettingEvent.updateSetting(pb));\n    AIEventUpdateLocalAISetting(pb).send().fold(\n          (_) => Log.info('AI setting updated successfully'),\n          (err) => Log.error('Update AI setting failed: $err'),\n        );\n  }\n\n  void _onSetDefaultModel(\n    _SetDefaultModel event,\n    Emitter<OllamaSettingState> emit,\n  ) {\n    emit(state.copyWith(selectedModel: event.model, isEdited: true));\n  }\n}\n\n/// Setting types for mapping\nenum SettingType {\n  serverUrl,\n  chatModel,\n  embeddingModel;\n\n  String get title {\n    switch (this) {\n      case SettingType.serverUrl:\n        return 'Ollama server url';\n      case SettingType.chatModel:\n        return 'Default model name';\n      case SettingType.embeddingModel:\n        return 'Embedding model name';\n    }\n  }\n}\n\n/// Input field representation\nclass SettingItem extends Equatable {\n  const SettingItem({\n    required this.content,\n    required this.hintText,\n    required this.settingType,\n    this.editable = true,\n  });\n\n  final String content;\n  final String hintText;\n  final SettingType settingType;\n  final bool editable;\n\n  @override\n  List<Object?> get props => [content, settingType, editable];\n}\n\n/// Items pending submission\nclass SubmittedItem extends Equatable {\n  const SubmittedItem({\n    required this.content,\n    required this.settingType,\n  });\n\n  final String content;\n  final SettingType settingType;\n\n  /// Returns a copy of this SubmittedItem with given fields updated.\n  SubmittedItem copyWith({\n    String? content,\n    SettingType? settingType,\n  }) {\n    return SubmittedItem(\n      content: content ?? this.content,\n      settingType: settingType ?? this.settingType,\n    );\n  }\n\n  @override\n  List<Object?> get props => [content, settingType];\n}\n\n@freezed\nclass OllamaSettingEvent with _$OllamaSettingEvent {\n  const factory OllamaSettingEvent.started() = _Started;\n  const factory OllamaSettingEvent.didLoadLocalModels(\n    ModelSelectionPB models,\n  ) = _DidLoadLocalModels;\n  const factory OllamaSettingEvent.didLoadSetting(\n    LocalAISettingPB setting,\n  ) = _DidLoadSetting;\n  const factory OllamaSettingEvent.updateSetting(\n    LocalAISettingPB setting,\n  ) = _UpdateSetting;\n  const factory OllamaSettingEvent.setDefaultModel(\n    AIModelPB model,\n  ) = _SetDefaultModel;\n  const factory OllamaSettingEvent.onEdit(\n    String content,\n    SettingType settingType,\n  ) = _OnEdit;\n  const factory OllamaSettingEvent.submit() = _OnSubmit;\n}\n\n@freezed\nclass OllamaSettingState with _$OllamaSettingState {\n  const factory OllamaSettingState({\n    LocalAISettingPB? setting,\n    @Default([]) List<SettingItem> inputItems,\n    AIModelPB? selectedModel,\n    ModelSelectionPB? localModels,\n    AIModelPB? defaultModel,\n    @Default([]) List<SubmittedItem> submittedItems,\n    @Default(false) bool isEdited,\n    @Default({}) Map<SettingType, String> originalMap,\n  }) = _OllamaSettingState;\n}\n\nextension on LocalAISettingPB {\n  List<SettingItem> toInputItems() => [\n        SettingItem(\n          content: serverUrl,\n          hintText: 'http://localhost:11434',\n          settingType: SettingType.serverUrl,\n        ),\n        SettingItem(\n          content: embeddingModelName,\n          hintText: kDefaultEmbeddingModel,\n          settingType: SettingType.embeddingModel,\n          editable: false, // embedding model is not editable\n        ),\n      ];\n\n  List<SubmittedItem> toSubmittedItems() => [\n        SubmittedItem(\n          content: serverUrl,\n          settingType: SettingType.serverUrl,\n        ),\n        SubmittedItem(\n          content: globalChatModel,\n          settingType: SettingType.chatModel,\n        ),\n        SubmittedItem(\n          content: embeddingModelName,\n          settingType: SettingType.embeddingModel,\n        ),\n      ];\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart",
    "content": "import 'package:appflowy/plugins/ai_chat/application/ai_model_switch_listener.dart';\nimport 'package:appflowy/user/application/user_listener.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'settings_ai_bloc.freezed.dart';\n\nconst String aiModelsGlobalActiveModel = \"global_active_model\";\n\nclass SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {\n  SettingsAIBloc(\n    this.userProfile,\n    this.workspaceId,\n  )   : _userListener = UserListener(userProfile: userProfile),\n        _aiModelSwitchListener =\n            AIModelSwitchListener(objectId: aiModelsGlobalActiveModel),\n        super(\n          SettingsAIState(\n            userProfile: userProfile,\n          ),\n        ) {\n    _aiModelSwitchListener.start(\n      onUpdateSelectedModel: (model) {\n        if (!isClosed) {\n          _loadModelList();\n        }\n      },\n    );\n    _dispatch();\n  }\n\n  final UserListener _userListener;\n  final UserProfilePB userProfile;\n  final String workspaceId;\n  final AIModelSwitchListener _aiModelSwitchListener;\n\n  @override\n  Future<void> close() async {\n    await _userListener.stop();\n    await _aiModelSwitchListener.stop();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<SettingsAIEvent>((event, emit) async {\n      await event.when(\n        started: () {\n          _userListener.start(\n            onProfileUpdated: _onProfileUpdated,\n            onUserWorkspaceSettingUpdated: (settings) {\n              if (!isClosed) {\n                add(SettingsAIEvent.didLoadWorkspaceSetting(settings));\n              }\n            },\n          );\n          _loadModelList();\n          _loadUserWorkspaceSetting();\n        },\n        didReceiveUserProfile: (userProfile) {\n          emit(state.copyWith(userProfile: userProfile));\n        },\n        toggleAISearch: () {\n          emit(\n            state.copyWith(enableSearchIndexing: !state.enableSearchIndexing),\n          );\n          _updateUserWorkspaceSetting(\n            disableSearchIndexing:\n                !(state.aiSettings?.disableSearchIndexing ?? false),\n          );\n        },\n        selectModel: (AIModelPB model) async {\n          await AIEventUpdateSelectedModel(\n            UpdateSelectedModelPB(\n              source: aiModelsGlobalActiveModel,\n              selectedModel: model,\n            ),\n          ).send();\n        },\n        didLoadWorkspaceSetting: (WorkspaceSettingsPB settings) {\n          emit(\n            state.copyWith(\n              aiSettings: settings,\n              enableSearchIndexing: !settings.disableSearchIndexing,\n            ),\n          );\n        },\n        didLoadAvailableModels: (ModelSelectionPB models) {\n          emit(\n            state.copyWith(\n              availableModels: models,\n            ),\n          );\n        },\n      );\n    });\n  }\n\n  Future<FlowyResult<void, FlowyError>> _updateUserWorkspaceSetting({\n    bool? disableSearchIndexing,\n    String? model,\n  }) async {\n    final payload = UpdateUserWorkspaceSettingPB(\n      workspaceId: workspaceId,\n    );\n    if (disableSearchIndexing != null) {\n      payload.disableSearchIndexing = disableSearchIndexing;\n    }\n    if (model != null) {\n      payload.aiModel = model;\n    }\n    final result = await UserEventUpdateWorkspaceSetting(payload).send();\n    result.fold(\n      (ok) => Log.info('Update workspace setting success'),\n      (err) => Log.error('Update workspace setting failed: $err'),\n    );\n    return result;\n  }\n\n  void _onProfileUpdated(\n    FlowyResult<UserProfilePB, FlowyError> userProfileOrFailed,\n  ) =>\n      userProfileOrFailed.fold(\n        (profile) => add(SettingsAIEvent.didReceiveUserProfile(profile)),\n        (err) => Log.error(err),\n      );\n\n  void _loadModelList() {\n    final payload = ModelSourcePB(source: aiModelsGlobalActiveModel);\n    AIEventGetSettingModelSelection(payload).send().then((result) {\n      result.fold((models) {\n        if (!isClosed) {\n          add(SettingsAIEvent.didLoadAvailableModels(models));\n        }\n      }, (err) {\n        Log.error(err);\n      });\n    });\n  }\n\n  void _loadUserWorkspaceSetting() {\n    final payload = UserWorkspaceIdPB(workspaceId: workspaceId);\n    UserEventGetWorkspaceSetting(payload).send().then((result) {\n      result.fold((settings) {\n        if (!isClosed) {\n          add(SettingsAIEvent.didLoadWorkspaceSetting(settings));\n        }\n      }, (err) {\n        Log.error(err);\n      });\n    });\n  }\n}\n\n@freezed\nclass SettingsAIEvent with _$SettingsAIEvent {\n  const factory SettingsAIEvent.started() = _Started;\n  const factory SettingsAIEvent.didLoadWorkspaceSetting(\n    WorkspaceSettingsPB settings,\n  ) = _DidLoadWorkspaceSetting;\n\n  const factory SettingsAIEvent.toggleAISearch() = _toggleAISearch;\n\n  const factory SettingsAIEvent.selectModel(AIModelPB model) = _SelectAIModel;\n\n  const factory SettingsAIEvent.didReceiveUserProfile(\n    UserProfilePB newUserProfile,\n  ) = _DidReceiveUserProfile;\n\n  const factory SettingsAIEvent.didLoadAvailableModels(\n    ModelSelectionPB models,\n  ) = _DidLoadAvailableModels;\n}\n\n@freezed\nclass SettingsAIState with _$SettingsAIState {\n  const factory SettingsAIState({\n    required UserProfilePB userProfile,\n    WorkspaceSettingsPB? aiSettings,\n    ModelSelectionPB? availableModels,\n    @Default(true) bool enableSearchIndexing,\n  }) = _SettingsAIState;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/user_settings_service.dart';\nimport 'package:appflowy/util/color_to_hex_string.dart';\nimport 'package:appflowy/workspace/application/appearance_defaults.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart'\n    show AppFlowyEditorLocalizations;\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\npart 'appearance_cubit.freezed.dart';\n\n/// [AppearanceSettingsCubit] is used to modify the appearance of AppFlowy.\n/// It includes:\n/// - [AppTheme]\n/// - [ThemeMode]\n/// - [TextStyle]'s\n/// - [Locale]\n/// - [UserDateFormatPB]\n/// - [UserTimeFormatPB]\n///\nclass AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {\n  AppearanceSettingsCubit(\n    AppearanceSettingsPB appearanceSettings,\n    DateTimeSettingsPB dateTimeSettings,\n    AppTheme appTheme,\n  )   : _appearanceSettings = appearanceSettings,\n        _dateTimeSettings = dateTimeSettings,\n        super(\n          AppearanceSettingsState.initial(\n            appTheme,\n            appearanceSettings.themeMode,\n            appearanceSettings.font,\n            appearanceSettings.layoutDirection,\n            appearanceSettings.textDirection,\n            appearanceSettings.enableRtlToolbarItems,\n            appearanceSettings.locale,\n            appearanceSettings.isMenuCollapsed,\n            appearanceSettings.menuOffset,\n            dateTimeSettings.dateFormat,\n            dateTimeSettings.timeFormat,\n            dateTimeSettings.timezoneId,\n            appearanceSettings.documentSetting.cursorColor.isEmpty\n                ? null\n                : Color(\n                    int.parse(appearanceSettings.documentSetting.cursorColor),\n                  ),\n            appearanceSettings.documentSetting.selectionColor.isEmpty\n                ? null\n                : Color(\n                    int.parse(\n                      appearanceSettings.documentSetting.selectionColor,\n                    ),\n                  ),\n            1.0,\n          ),\n        ) {\n    readTextScaleFactor();\n  }\n\n  final AppearanceSettingsPB _appearanceSettings;\n  final DateTimeSettingsPB _dateTimeSettings;\n\n  Future<void> setTextScaleFactor(double textScaleFactor) async {\n    // only saved in local storage, this value is not synced across devices\n    await getIt<KeyValueStorage>().set(\n      KVKeys.textScaleFactor,\n      textScaleFactor.toString(),\n    );\n\n    // don't allow the text scale factor to be greater than 1.0, it will cause\n    // ui issues\n    emit(state.copyWith(textScaleFactor: textScaleFactor.clamp(0.7, 1.0)));\n  }\n\n  Future<void> readTextScaleFactor() async {\n    final textScaleFactor = await getIt<KeyValueStorage>().getWithFormat(\n          KVKeys.textScaleFactor,\n          (value) => double.parse(value),\n        ) ??\n        1.0;\n    emit(state.copyWith(textScaleFactor: textScaleFactor.clamp(0.7, 1.0)));\n  }\n\n  /// Update selected theme in the user's settings and emit an updated state\n  /// with the AppTheme named [themeName].\n  Future<void> setTheme(String themeName) async {\n    _appearanceSettings.theme = themeName;\n    unawaited(_saveAppearanceSettings());\n    try {\n      final theme = await AppTheme.fromName(themeName);\n      emit(state.copyWith(appTheme: theme));\n    } catch (e) {\n      Log.error(\"Error setting theme: $e\");\n      if (UniversalPlatform.isMacOS) {\n        showToastNotification(\n          message:\n              LocaleKeys.settings_workspacePage_theme_failedToLoadThemes.tr(),\n          type: ToastificationType.error,\n        );\n      }\n    }\n  }\n\n  /// Reset the current user selected theme back to the default\n  Future<void> resetTheme() =>\n      setTheme(DefaultAppearanceSettings.kDefaultThemeName);\n\n  /// Update the theme mode in the user's settings and emit an updated state.\n  void setThemeMode(ThemeMode themeMode) {\n    _appearanceSettings.themeMode = _themeModeToPB(themeMode);\n    _saveAppearanceSettings();\n    emit(state.copyWith(themeMode: themeMode));\n  }\n\n  /// Resets the current brightness setting\n  void resetThemeMode() =>\n      setThemeMode(DefaultAppearanceSettings.kDefaultThemeMode);\n\n  /// Toggle the theme mode\n  void toggleThemeMode() {\n    final currentThemeMode = state.themeMode;\n    setThemeMode(\n      currentThemeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light,\n    );\n  }\n\n  void setLayoutDirection(LayoutDirection layoutDirection) {\n    _appearanceSettings.layoutDirection = layoutDirection.toLayoutDirectionPB();\n    _saveAppearanceSettings();\n    emit(state.copyWith(layoutDirection: layoutDirection));\n  }\n\n  void setTextDirection(AppFlowyTextDirection textDirection) {\n    _appearanceSettings.textDirection = textDirection.toTextDirectionPB();\n    _saveAppearanceSettings();\n    emit(state.copyWith(textDirection: textDirection));\n  }\n\n  void setEnableRTLToolbarItems(bool value) {\n    _appearanceSettings.enableRtlToolbarItems = value;\n    _saveAppearanceSettings();\n    emit(state.copyWith(enableRtlToolbarItems: value));\n  }\n\n  /// Update selected font in the user's settings and emit an updated state\n  /// with the font name.\n  void setFontFamily(String fontFamilyName) {\n    _appearanceSettings.font = fontFamilyName;\n    _saveAppearanceSettings();\n    emit(state.copyWith(font: fontFamilyName));\n  }\n\n  /// Resets the current font family for the user preferences\n  void resetFontFamily() =>\n      setFontFamily(DefaultAppearanceSettings.kDefaultFontFamily);\n\n  /// Update document cursor color in the appearance settings and emit an updated state.\n  void setDocumentCursorColor(Color color) {\n    _appearanceSettings.documentSetting.cursorColor = color.toHexString();\n    _saveAppearanceSettings();\n    emit(state.copyWith(documentCursorColor: color));\n  }\n\n  /// Reset document cursor color in the appearance settings\n  void resetDocumentCursorColor() {\n    _appearanceSettings.documentSetting.cursorColor = '';\n    _saveAppearanceSettings();\n    emit(state.copyWith(documentCursorColor: null));\n  }\n\n  /// Update document selection color in the appearance settings and emit an updated state.\n  void setDocumentSelectionColor(Color color) {\n    _appearanceSettings.documentSetting.selectionColor = color.toHexString();\n    _saveAppearanceSettings();\n    emit(state.copyWith(documentSelectionColor: color));\n  }\n\n  /// Reset document selection color in the appearance settings\n  void resetDocumentSelectionColor() {\n    _appearanceSettings.documentSetting.selectionColor = '';\n    _saveAppearanceSettings();\n    emit(state.copyWith(documentSelectionColor: null));\n  }\n\n  /// Updates the current locale and notify the listeners the locale was\n  /// changed. Fallback to [en] locale if [newLocale] is not supported.\n  void setLocale(BuildContext context, Locale newLocale) {\n    if (!context.supportedLocales.contains(newLocale)) {\n      // Log.warn(\"Unsupported locale: $newLocale, Fallback to locale: en\");\n      newLocale = const Locale('en', 'US');\n    }\n\n    context.setLocale(newLocale).catchError((e) {\n      Log.warn('Catch error in setLocale: $e}');\n    });\n\n    // Sync the app's locale with the editor (initialization and update)\n    AppFlowyEditorLocalizations.load(newLocale);\n\n    if (state.locale != newLocale) {\n      _appearanceSettings.locale.languageCode = newLocale.languageCode;\n      _appearanceSettings.locale.countryCode = newLocale.countryCode ?? \"\";\n      _saveAppearanceSettings();\n      emit(state.copyWith(locale: newLocale));\n    }\n  }\n\n  // Saves the menus current visibility\n  void saveIsMenuCollapsed(bool collapsed) {\n    _appearanceSettings.isMenuCollapsed = collapsed;\n    _saveAppearanceSettings();\n  }\n\n  // Saves the current resize offset of the menu\n  void saveMenuOffset(double offset) {\n    _appearanceSettings.menuOffset = offset;\n    _saveAppearanceSettings();\n  }\n\n  /// Saves key/value setting to disk.\n  /// Removes the key if the passed in value is null\n  void setKeyValue(String key, String? value) {\n    if (key.isEmpty) {\n      Log.warn(\"The key should not be empty\");\n      return;\n    }\n\n    if (value == null) {\n      _appearanceSettings.settingKeyValue.remove(key);\n    }\n\n    if (_appearanceSettings.settingKeyValue[key] != value) {\n      if (value == null) {\n        _appearanceSettings.settingKeyValue.remove(key);\n      } else {\n        _appearanceSettings.settingKeyValue[key] = value;\n      }\n    }\n    _saveAppearanceSettings();\n  }\n\n  String? getValue(String key) {\n    if (key.isEmpty) {\n      Log.warn(\"The key should not be empty\");\n      return null;\n    }\n    return _appearanceSettings.settingKeyValue[key];\n  }\n\n  /// Called when the application launches.\n  /// Uses the device locale when the application is opened for the first time.\n  void readLocaleWhenAppLaunch(BuildContext context) {\n    if (_appearanceSettings.resetToDefault) {\n      _appearanceSettings.resetToDefault = false;\n      _saveAppearanceSettings();\n      setLocale(context, context.deviceLocale);\n      return;\n    }\n\n    setLocale(context, state.locale);\n  }\n\n  void setDateFormat(UserDateFormatPB format) {\n    _dateTimeSettings.dateFormat = format;\n    _saveDateTimeSettings();\n    emit(state.copyWith(dateFormat: format));\n  }\n\n  void setTimeFormat(UserTimeFormatPB format) {\n    _dateTimeSettings.timeFormat = format;\n    _saveDateTimeSettings();\n    emit(state.copyWith(timeFormat: format));\n  }\n\n  Future<void> _saveDateTimeSettings() async {\n    final result = await UserSettingsBackendService()\n        .setDateTimeSettings(_dateTimeSettings);\n    result.fold(\n      (_) => null,\n      (error) => Log.error(error),\n    );\n  }\n\n  Future<void> _saveAppearanceSettings() async {\n    final result = await UserSettingsBackendService()\n        .setAppearanceSetting(_appearanceSettings);\n    result.fold(\n      (l) => null,\n      (error) => Log.error(error),\n    );\n  }\n}\n\nThemeMode _themeModeFromPB(ThemeModePB themeModePB) {\n  switch (themeModePB) {\n    case ThemeModePB.Light:\n      return ThemeMode.light;\n    case ThemeModePB.Dark:\n      return ThemeMode.dark;\n    case ThemeModePB.System:\n    default:\n      return ThemeMode.system;\n  }\n}\n\nThemeModePB _themeModeToPB(ThemeMode themeMode) {\n  switch (themeMode) {\n    case ThemeMode.light:\n      return ThemeModePB.Light;\n    case ThemeMode.dark:\n      return ThemeModePB.Dark;\n    case ThemeMode.system:\n      return ThemeModePB.System;\n  }\n}\n\nenum LayoutDirection {\n  ltrLayout,\n  rtlLayout;\n\n  static LayoutDirection fromLayoutDirectionPB(\n    LayoutDirectionPB layoutDirectionPB,\n  ) =>\n      layoutDirectionPB == LayoutDirectionPB.RTLLayout\n          ? LayoutDirection.rtlLayout\n          : LayoutDirection.ltrLayout;\n\n  LayoutDirectionPB toLayoutDirectionPB() => this == LayoutDirection.rtlLayout\n      ? LayoutDirectionPB.RTLLayout\n      : LayoutDirectionPB.LTRLayout;\n}\n\nenum AppFlowyTextDirection {\n  ltr,\n  rtl,\n  auto;\n\n  static AppFlowyTextDirection fromTextDirectionPB(\n    TextDirectionPB? textDirectionPB,\n  ) {\n    switch (textDirectionPB) {\n      case TextDirectionPB.LTR:\n        return AppFlowyTextDirection.ltr;\n      case TextDirectionPB.RTL:\n        return AppFlowyTextDirection.rtl;\n      case TextDirectionPB.AUTO:\n        return AppFlowyTextDirection.auto;\n      default:\n        return AppFlowyTextDirection.ltr;\n    }\n  }\n\n  TextDirectionPB toTextDirectionPB() {\n    switch (this) {\n      case AppFlowyTextDirection.ltr:\n        return TextDirectionPB.LTR;\n      case AppFlowyTextDirection.rtl:\n        return TextDirectionPB.RTL;\n      case AppFlowyTextDirection.auto:\n        return TextDirectionPB.AUTO;\n    }\n  }\n}\n\n@freezed\nclass AppearanceSettingsState with _$AppearanceSettingsState {\n  const AppearanceSettingsState._();\n\n  const factory AppearanceSettingsState({\n    required AppTheme appTheme,\n    required ThemeMode themeMode,\n    required String font,\n    required LayoutDirection layoutDirection,\n    required AppFlowyTextDirection textDirection,\n    required bool enableRtlToolbarItems,\n    required Locale locale,\n    required bool isMenuCollapsed,\n    required double menuOffset,\n    required UserDateFormatPB dateFormat,\n    required UserTimeFormatPB timeFormat,\n    required String timezoneId,\n    required Color? documentCursorColor,\n    required Color? documentSelectionColor,\n    required double textScaleFactor,\n  }) = _AppearanceSettingsState;\n\n  factory AppearanceSettingsState.initial(\n    AppTheme appTheme,\n    ThemeModePB themeModePB,\n    String font,\n    LayoutDirectionPB layoutDirectionPB,\n    TextDirectionPB? textDirectionPB,\n    bool enableRtlToolbarItems,\n    LocaleSettingsPB localePB,\n    bool isMenuCollapsed,\n    double menuOffset,\n    UserDateFormatPB dateFormat,\n    UserTimeFormatPB timeFormat,\n    String timezoneId,\n    Color? documentCursorColor,\n    Color? documentSelectionColor,\n    double textScaleFactor,\n  ) {\n    return AppearanceSettingsState(\n      appTheme: appTheme,\n      font: font,\n      layoutDirection: LayoutDirection.fromLayoutDirectionPB(layoutDirectionPB),\n      textDirection: AppFlowyTextDirection.fromTextDirectionPB(textDirectionPB),\n      enableRtlToolbarItems: enableRtlToolbarItems,\n      themeMode: _themeModeFromPB(themeModePB),\n      locale: Locale(localePB.languageCode, localePB.countryCode),\n      isMenuCollapsed: isMenuCollapsed,\n      menuOffset: menuOffset,\n      dateFormat: dateFormat,\n      timeFormat: timeFormat,\n      timezoneId: timezoneId,\n      documentCursorColor: documentCursorColor,\n      documentSelectionColor: documentSelectionColor,\n      textScaleFactor: textScaleFactor,\n    );\n  }\n\n  ThemeData get lightTheme => _getThemeData(Brightness.light);\n\n  ThemeData get darkTheme => _getThemeData(Brightness.dark);\n\n  ThemeData _getThemeData(Brightness brightness) {\n    return getIt<BaseAppearance>().getThemeData(\n      appTheme,\n      brightness,\n      font,\n      builtInCodeFontFamily,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart",
    "content": "import 'package:appflowy/shared/google_fonts_extension.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme.dart';\nimport 'package:flutter/material.dart';\n\n// the default font family is empty, so we can use the default font family of the platform\n// the system will choose the default font family of the platform\n// iOS: San Francisco\n// Android: Roboto\n// Desktop: Based on the OS\nconst defaultFontFamily = '';\n\nconst builtInCodeFontFamily = 'RobotoMono';\n\nabstract class BaseAppearance {\n  final white = const Color(0xFFFFFFFF);\n\n  final Set<WidgetState> scrollbarInteractiveStates = <WidgetState>{\n    WidgetState.pressed,\n    WidgetState.hovered,\n    WidgetState.dragged,\n  };\n\n  TextStyle getFontStyle({\n    required String fontFamily,\n    double? fontSize,\n    FontWeight? fontWeight,\n    Color? fontColor,\n    double? letterSpacing,\n    double? lineHeight,\n  }) {\n    fontSize = fontSize ?? FontSizes.s14;\n    fontWeight = fontWeight ?? FontWeight.w400;\n    letterSpacing = fontSize * (letterSpacing ?? 0.005);\n\n    final textStyle = TextStyle(\n      fontFamily: fontFamily.isEmpty ? null : fontFamily,\n      fontSize: fontSize,\n      color: fontColor,\n      fontWeight: fontWeight,\n      letterSpacing: letterSpacing,\n      height: lineHeight,\n    );\n\n    if (fontFamily == defaultFontFamily) {\n      return textStyle;\n    }\n\n    try {\n      return getGoogleFontSafely(\n        fontFamily,\n        fontSize: fontSize,\n        fontColor: fontColor,\n        fontWeight: fontWeight,\n        letterSpacing: letterSpacing,\n        lineHeight: lineHeight,\n      );\n    } catch (e) {\n      return textStyle;\n    }\n  }\n\n  TextTheme getTextTheme({\n    required String fontFamily,\n    required Color fontColor,\n  }) {\n    return TextTheme(\n      displayLarge: getFontStyle(\n        fontFamily: fontFamily,\n        fontSize: FontSizes.s32,\n        fontColor: fontColor,\n        fontWeight: FontWeight.w600,\n        lineHeight: 42.0,\n      ), // h2\n      displayMedium: getFontStyle(\n        fontFamily: fontFamily,\n        fontSize: FontSizes.s24,\n        fontColor: fontColor,\n        fontWeight: FontWeight.w600,\n        lineHeight: 34.0,\n      ), // h3\n      displaySmall: getFontStyle(\n        fontFamily: fontFamily,\n        fontSize: FontSizes.s20,\n        fontColor: fontColor,\n        fontWeight: FontWeight.w600,\n        lineHeight: 28.0,\n      ), // h4\n      titleLarge: getFontStyle(\n        fontFamily: fontFamily,\n        fontSize: FontSizes.s18,\n        fontColor: fontColor,\n        fontWeight: FontWeight.w600,\n      ), // title\n      titleMedium: getFontStyle(\n        fontFamily: fontFamily,\n        fontSize: FontSizes.s16,\n        fontColor: fontColor,\n        fontWeight: FontWeight.w600,\n      ), // heading\n      titleSmall: getFontStyle(\n        fontFamily: fontFamily,\n        fontSize: FontSizes.s14,\n        fontColor: fontColor,\n        fontWeight: FontWeight.w600,\n      ), // subheading\n      bodyMedium: getFontStyle(\n        fontFamily: fontFamily,\n        fontColor: fontColor,\n      ), // body-regular\n      bodySmall: getFontStyle(\n        fontFamily: fontFamily,\n        fontColor: fontColor,\n        fontWeight: FontWeight.w400,\n      ), // body-thin\n    );\n  }\n\n  ThemeData getThemeData(\n    AppTheme appTheme,\n    Brightness brightness,\n    String fontFamily,\n    String codeFontFamily,\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart",
    "content": "import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\n\nclass DesktopAppearance extends BaseAppearance {\n  @override\n  ThemeData getThemeData(\n    AppTheme appTheme,\n    Brightness brightness,\n    String fontFamily,\n    String codeFontFamily,\n  ) {\n    assert(codeFontFamily.isNotEmpty);\n\n    fontFamily = fontFamily.isEmpty ? defaultFontFamily : fontFamily;\n\n    final isLight = brightness == Brightness.light;\n    final theme = isLight ? appTheme.lightTheme : appTheme.darkTheme;\n\n    final colorScheme = ColorScheme(\n      brightness: brightness,\n      primary: theme.primary,\n      onPrimary: theme.onPrimary,\n      primaryContainer: theme.main2,\n      onPrimaryContainer: white,\n      // page title hover color\n      secondary: theme.hoverBG1,\n      onSecondary: theme.shader1,\n      // setting value hover color\n      secondaryContainer: theme.selector,\n      onSecondaryContainer: theme.topbarBg,\n      tertiary: theme.shader7,\n      // Editor: toolbarColor\n      onTertiary: theme.toolbarColor,\n      tertiaryContainer: theme.questionBubbleBG,\n      surface: theme.surface,\n      // text&icon color when it is hovered\n      onSurface: theme.hoverFG,\n      // grey hover color\n      inverseSurface: theme.hoverBG3,\n      onError: theme.onPrimary,\n      error: theme.red,\n      outline: theme.shader4,\n      surfaceContainerHighest: theme.sidebarBg,\n      shadow: theme.shadow,\n    );\n\n    // Due to Desktop version has multiple themes, it relies on the current theme to build the ThemeData\n    return ThemeData(\n      visualDensity: VisualDensity.standard,\n      useMaterial3: false,\n      brightness: brightness,\n      dialogBackgroundColor: theme.surface,\n      textTheme: getTextTheme(\n        fontFamily: fontFamily,\n        fontColor: theme.text,\n      ),\n      textButtonTheme: const TextButtonThemeData(\n        style: ButtonStyle(\n          minimumSize: WidgetStatePropertyAll(Size.zero),\n        ),\n      ),\n      textSelectionTheme: TextSelectionThemeData(\n        cursorColor: theme.main2,\n        selectionHandleColor: theme.main2,\n      ),\n      iconTheme: IconThemeData(color: theme.icon),\n      tooltipTheme: TooltipThemeData(\n        textStyle: getFontStyle(\n          fontFamily: fontFamily,\n          fontSize: FontSizes.s11,\n          fontWeight: FontWeight.w400,\n          fontColor: theme.surface,\n        ),\n      ),\n      scaffoldBackgroundColor: theme.surface,\n      snackBarTheme: SnackBarThemeData(\n        backgroundColor: colorScheme.primary,\n        contentTextStyle: TextStyle(color: colorScheme.onSurface),\n      ),\n      scrollbarTheme: ScrollbarThemeData(\n        thumbColor: WidgetStateProperty.resolveWith(\n          (states) => states.any(scrollbarInteractiveStates.contains)\n              ? theme.scrollbarHoverColor\n              : theme.scrollbarColor,\n        ),\n        thickness: WidgetStateProperty.resolveWith((_) => 4.0),\n        crossAxisMargin: 0.0,\n        mainAxisMargin: 6.0,\n        radius: Corners.s10Radius,\n      ),\n      materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,\n      //dropdown menu color\n      canvasColor: theme.surface,\n      dividerColor: theme.divider,\n      hintColor: theme.hint,\n      //action item hover color\n      hoverColor: theme.hoverBG2,\n      disabledColor: theme.shader4,\n      highlightColor: theme.main1,\n      indicatorColor: theme.main1,\n      cardColor: theme.input,\n      colorScheme: colorScheme,\n\n      extensions: [\n        AFThemeExtension(\n          warning: theme.yellow,\n          success: theme.green,\n          tint1: theme.tint1,\n          tint2: theme.tint2,\n          tint3: theme.tint3,\n          tint4: theme.tint4,\n          tint5: theme.tint5,\n          tint6: theme.tint6,\n          tint7: theme.tint7,\n          tint8: theme.tint8,\n          tint9: theme.tint9,\n          textColor: theme.text,\n          secondaryTextColor: theme.secondaryText,\n          strongText: theme.strongText,\n          greyHover: theme.hoverBG1,\n          greySelect: theme.bg3,\n          lightGreyHover: theme.hoverBG3,\n          toggleOffFill: theme.shader5,\n          progressBarBGColor: theme.progressBarBGColor,\n          toggleButtonBGColor: theme.toggleButtonBGColor,\n          calendarWeekendBGColor: theme.calendarWeekendBGColor,\n          gridRowCountColor: theme.gridRowCountColor,\n          code: getFontStyle(\n            fontFamily: codeFontFamily,\n            fontColor: theme.shader3,\n          ),\n          callout: getFontStyle(\n            fontFamily: fontFamily,\n            fontSize: FontSizes.s11,\n            fontColor: theme.shader3,\n          ),\n          calloutBGColor: theme.hoverBG3,\n          tableCellBGColor: theme.surface,\n          caption: getFontStyle(\n            fontFamily: fontFamily,\n            fontSize: FontSizes.s11,\n            fontWeight: FontWeight.w400,\n            fontColor: theme.hint,\n          ),\n          onBackground: theme.text,\n          background: theme.surface,\n          borderColor: theme.borderColor,\n          scrollbarColor: theme.scrollbarColor,\n          scrollbarHoverColor: theme.scrollbarHoverColor,\n          lightIconColor: theme.lightIconColor,\n          toolbarHoverColor: theme.toolbarHoverColor,\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart",
    "content": "// ThemeData in mobile\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\n\nclass MobileAppearance extends BaseAppearance {\n  static const _primaryColor = Color(0xFF00BCF0); //primary 100\n  static const _onBackgroundColor = Color(0xff2F3030); // text/title color\n  static const _onSurfaceColor = Color(0xff676666); // text/body color\n  static const _onSecondaryColor = Color(0xFFC5C7CB); // text/body2 color\n  static const _hintColorInDarkMode = Color(0xff626262); // hint color\n\n  @override\n  ThemeData getThemeData(\n    AppTheme appTheme,\n    Brightness brightness,\n    String fontFamily,\n    String codeFontFamily,\n  ) {\n    assert(codeFontFamily.isNotEmpty);\n\n    final fontStyle = getFontStyle(\n      fontFamily: fontFamily,\n      fontSize: 16.0,\n      fontWeight: FontWeight.w400,\n    );\n\n    final isLight = brightness == Brightness.light;\n    final codeFontStyle = getFontStyle(fontFamily: codeFontFamily);\n\n    final theme = isLight ? appTheme.lightTheme : appTheme.darkTheme;\n\n    final colorTheme = isLight\n        ? ColorScheme(\n            brightness: brightness,\n            primary: _primaryColor,\n            onPrimary: Colors.white,\n            // group card header background color\n            primaryContainer: const Color(0xffF1F1F4), // primary 20\n            // group card & property edit background color\n            secondary: const Color(0xfff7f8fc), // shade 10\n            onSecondary: _onSecondaryColor,\n            // hidden group title & card text color\n            tertiary: const Color(0xff858585), // for light text\n            error: const Color(0xffFB006D),\n            onError: const Color(0xffFB006D),\n            outline: const Color(0xffe3e3e3),\n            outlineVariant: const Color(0xffCBD5E0).withValues(alpha: 0.24),\n            //Snack bar\n            surface: Colors.white,\n            onSurface: _onSurfaceColor, // text/body color\n            surfaceContainerHighest: theme.sidebarBg,\n          )\n        : ColorScheme(\n            brightness: brightness,\n            primary: _primaryColor,\n            onPrimary: Colors.black,\n            secondary: const Color(0xff2d2d2d), //temp\n            onSecondary: Colors.white,\n            tertiary: const Color(0xff858585), // temp\n            error: const Color(0xffFB006D),\n            onError: const Color(0xffFB006D),\n            outline: _hintColorInDarkMode,\n            outlineVariant: Colors.black,\n            //Snack bar\n            surface: const Color(0xFF171A1F),\n            onSurface: const Color(0xffC5C6C7), // text/body color\n            surfaceContainerHighest: theme.sidebarBg,\n          );\n    final hintColor = isLight ? const Color(0x991F2329) : _hintColorInDarkMode;\n    final onBackground = isLight ? _onBackgroundColor : Colors.white;\n    final background = isLight ? Colors.white : const Color(0xff121212);\n\n    return ThemeData(\n      useMaterial3: false,\n      primaryColor: colorTheme.primary, //primary 100\n      primaryColorLight: const Color(0xFF57B5F8), //primary 80\n      dividerColor: colorTheme.outline, //caption\n      hintColor: hintColor,\n      disabledColor: colorTheme.outline,\n      scaffoldBackgroundColor: background,\n      appBarTheme: AppBarTheme(\n        toolbarHeight: 44.0,\n        foregroundColor: onBackground,\n        backgroundColor: background,\n        centerTitle: false,\n        titleTextStyle: TextStyle(\n          color: onBackground,\n          fontSize: 18,\n          fontWeight: FontWeight.w600,\n          letterSpacing: 0.05,\n        ),\n        shadowColor: colorTheme.outlineVariant,\n      ),\n      radioTheme: RadioThemeData(\n        fillColor: WidgetStateProperty.resolveWith((states) {\n          if (states.contains(WidgetState.selected)) {\n            return colorTheme.primary;\n          }\n          return colorTheme.outline;\n        }),\n      ),\n      // button\n      elevatedButtonTheme: ElevatedButtonThemeData(\n        style: ButtonStyle(\n          fixedSize: WidgetStateProperty.all(const Size.fromHeight(48)),\n          elevation: WidgetStateProperty.all(0),\n          textStyle: WidgetStateProperty.all(\n            TextStyle(\n              fontSize: 14,\n              fontFamily: fontStyle.fontFamily,\n              fontWeight: FontWeight.w600,\n            ),\n          ),\n          shadowColor: WidgetStateProperty.all(null),\n          foregroundColor: WidgetStateProperty.all(Colors.white),\n          backgroundColor: WidgetStateProperty.resolveWith<Color>(\n            (Set<WidgetState> states) {\n              if (states.contains(WidgetState.disabled)) {\n                return _primaryColor;\n              }\n              return colorTheme.primary;\n            },\n          ),\n        ),\n      ),\n      outlinedButtonTheme: OutlinedButtonThemeData(\n        style: ButtonStyle(\n          textStyle: WidgetStateProperty.all(\n            TextStyle(\n              fontSize: 14,\n              fontFamily: fontStyle.fontFamily,\n              fontWeight: FontWeight.w500,\n            ),\n          ),\n          foregroundColor: WidgetStateProperty.all(onBackground),\n          backgroundColor: WidgetStateProperty.all(background),\n          shape: WidgetStateProperty.all(\n            RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),\n          ),\n          side: WidgetStateProperty.all(\n            BorderSide(color: colorTheme.outline, width: 0.5),\n          ),\n          padding: WidgetStateProperty.all(\n            const EdgeInsets.symmetric(horizontal: 8, vertical: 12),\n          ),\n        ),\n      ),\n      textButtonTheme: TextButtonThemeData(\n        style: ButtonStyle(\n          textStyle: WidgetStateProperty.all(fontStyle),\n        ),\n      ),\n      // text\n      fontFamily: fontStyle.fontFamily,\n      textTheme: TextTheme(\n        displayLarge: const TextStyle(\n          color: _primaryColor,\n          fontSize: 32,\n          fontWeight: FontWeight.w700,\n          height: 1.20,\n          letterSpacing: 0.16,\n        ),\n        displayMedium: fontStyle.copyWith(\n          color: onBackground,\n          fontSize: 32,\n          fontWeight: FontWeight.w600,\n          height: 1.20,\n          letterSpacing: 0.16,\n        ),\n        // H1 Semi 26\n        displaySmall: fontStyle.copyWith(\n          color: onBackground,\n          fontWeight: FontWeight.w600,\n          height: 1.10,\n          letterSpacing: 0.13,\n        ),\n        // body2 14 Regular\n        bodyMedium: fontStyle.copyWith(\n          color: onBackground,\n          fontWeight: FontWeight.w400,\n          letterSpacing: 0.07,\n        ),\n        // Trash empty title\n        labelLarge: fontStyle.copyWith(\n          color: onBackground,\n          fontSize: 22,\n          fontWeight: FontWeight.w600,\n          letterSpacing: -0.3,\n        ),\n        // setting item title\n        labelMedium: fontStyle.copyWith(\n          color: onBackground,\n          fontSize: 18,\n          fontWeight: FontWeight.w500,\n        ),\n        // setting group title\n        labelSmall: fontStyle.copyWith(\n          color: onBackground,\n          fontSize: 16,\n          fontWeight: FontWeight.w600,\n          letterSpacing: 0.05,\n        ),\n      ),\n      inputDecorationTheme: InputDecorationTheme(\n        contentPadding: const EdgeInsets.all(8),\n        focusedBorder: const OutlineInputBorder(\n          borderSide: BorderSide(\n            width: 2,\n            color: _primaryColor,\n          ),\n          borderRadius: BorderRadius.all(Radius.circular(6)),\n        ),\n        focusedErrorBorder: OutlineInputBorder(\n          borderSide: BorderSide(color: colorTheme.error),\n          borderRadius: const BorderRadius.all(Radius.circular(6)),\n        ),\n        errorBorder: OutlineInputBorder(\n          borderSide: BorderSide(color: colorTheme.error),\n          borderRadius: const BorderRadius.all(Radius.circular(6)),\n        ),\n        enabledBorder: OutlineInputBorder(\n          borderSide: BorderSide(\n            color: colorTheme.outline,\n          ),\n          borderRadius: const BorderRadius.all(Radius.circular(6)),\n        ),\n      ),\n      colorScheme: colorTheme,\n      indicatorColor: Colors.blue,\n      extensions: [\n        AFThemeExtension(\n          warning: theme.yellow,\n          success: theme.green,\n          tint1: theme.tint1,\n          tint2: theme.tint2,\n          tint3: theme.tint3,\n          tint4: theme.tint4,\n          tint5: theme.tint5,\n          tint6: theme.tint6,\n          tint7: theme.tint7,\n          tint8: theme.tint8,\n          tint9: theme.tint9,\n          textColor: theme.text,\n          secondaryTextColor: theme.secondaryText,\n          strongText: theme.strongText,\n          greyHover: theme.hoverBG1,\n          greySelect: theme.bg3,\n          lightGreyHover: theme.hoverBG3,\n          toggleOffFill: theme.shader5,\n          progressBarBGColor: theme.progressBarBGColor,\n          toggleButtonBGColor: theme.toggleButtonBGColor,\n          calendarWeekendBGColor: theme.calendarWeekendBGColor,\n          gridRowCountColor: theme.gridRowCountColor,\n          code: codeFontStyle.copyWith(\n            color: theme.shader3,\n          ),\n          callout: fontStyle.copyWith(\n            fontSize: FontSizes.s11,\n            color: theme.shader3,\n          ),\n          calloutBGColor: theme.hoverBG3,\n          tableCellBGColor: theme.surface,\n          caption: fontStyle.copyWith(\n            fontSize: FontSizes.s11,\n            fontWeight: FontWeight.w400,\n            color: theme.hint,\n          ),\n          onBackground: onBackground,\n          background: background,\n          borderColor: theme.borderColor,\n          scrollbarColor: theme.scrollbarColor,\n          scrollbarHoverColor: theme.scrollbarHoverColor,\n          lightIconColor: theme.lightIconColor,\n          toolbarHoverColor: theme.toolbarHoverColor,\n        ),\n        ToolbarColorExtension.fromBrightness(brightness),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/appflowy_cloud_setting_bloc.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/settings/cloud_setting_listener.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'appflowy_cloud_setting_bloc.freezed.dart';\n\nclass AppFlowyCloudSettingBloc\n    extends Bloc<AppFlowyCloudSettingEvent, AppFlowyCloudSettingState> {\n  AppFlowyCloudSettingBloc(CloudSettingPB setting)\n      : _listener = UserCloudConfigListener(),\n        super(AppFlowyCloudSettingState.initial(setting, false)) {\n    _dispatch();\n    _getWorkspaceType();\n  }\n\n  final UserCloudConfigListener _listener;\n\n  @override\n  Future<void> close() async {\n    await _listener.stop();\n    return super.close();\n  }\n\n  void _getWorkspaceType() {\n    UserEventGetUserProfile().send().then((value) {\n      if (isClosed) {\n        return;\n      }\n\n      value.fold(\n        (profile) => add(\n          AppFlowyCloudSettingEvent.workspaceTypeChanged(\n            profile.workspaceType,\n          ),\n        ),\n        (error) => Log.error(error),\n      );\n    });\n  }\n\n  void _dispatch() {\n    on<AppFlowyCloudSettingEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            await getSyncLogEnabled().then((value) {\n              emit(state.copyWith(isSyncLogEnabled: value));\n            });\n\n            _listener.start(\n              onSettingChanged: (result) {\n                if (isClosed) {\n                  return;\n                }\n                result.fold(\n                  (setting) =>\n                      add(AppFlowyCloudSettingEvent.didReceiveSetting(setting)),\n                  (error) => Log.error(error),\n                );\n              },\n            );\n          },\n          enableSync: (isEnable) async {\n            final config = UpdateCloudConfigPB.create()..enableSync = isEnable;\n            await UserEventSetCloudConfig(config).send();\n          },\n          enableSyncLog: (isEnable) async {\n            await setSyncLogEnabled(isEnable);\n            emit(state.copyWith(isSyncLogEnabled: isEnable));\n          },\n          didReceiveSetting: (CloudSettingPB setting) {\n            emit(\n              state.copyWith(\n                setting: setting,\n                showRestartHint: setting.serverUrl.isNotEmpty,\n              ),\n            );\n          },\n          workspaceTypeChanged: (WorkspaceTypePB workspaceType) {\n            emit(state.copyWith(workspaceType: workspaceType));\n          },\n        );\n      },\n    );\n  }\n}\n\n@freezed\nclass AppFlowyCloudSettingEvent with _$AppFlowyCloudSettingEvent {\n  const factory AppFlowyCloudSettingEvent.initial() = _Initial;\n  const factory AppFlowyCloudSettingEvent.enableSync(bool isEnable) =\n      _EnableSync;\n  const factory AppFlowyCloudSettingEvent.enableSyncLog(bool isEnable) =\n      _EnableSyncLog;\n  const factory AppFlowyCloudSettingEvent.didReceiveSetting(\n    CloudSettingPB setting,\n  ) = _DidUpdateSetting;\n  const factory AppFlowyCloudSettingEvent.workspaceTypeChanged(\n    WorkspaceTypePB workspaceType,\n  ) = _WorkspaceTypeChanged;\n}\n\n@freezed\nclass AppFlowyCloudSettingState with _$AppFlowyCloudSettingState {\n  const factory AppFlowyCloudSettingState({\n    required CloudSettingPB setting,\n    required bool showRestartHint,\n    required bool isSyncLogEnabled,\n    required WorkspaceTypePB workspaceType,\n  }) = _AppFlowyCloudSettingState;\n\n  factory AppFlowyCloudSettingState.initial(\n    CloudSettingPB setting,\n    bool isSyncLogEnabled,\n  ) =>\n      AppFlowyCloudSettingState(\n        setting: setting,\n        showRestartHint: setting.serverUrl.isNotEmpty,\n        isSyncLogEnabled: isSyncLogEnabled,\n        workspaceType: WorkspaceTypePB.ServerW,\n      );\n}\n\nFlowyResult<void, String> validateUrl(String url) {\n  try {\n    // Use Uri.parse to validate the url.\n    final uri = Uri.parse(url);\n    if (uri.isScheme('HTTP') || uri.isScheme('HTTPS')) {\n      return FlowyResult.success(null);\n    } else {\n      return FlowyResult.failure(\n        LocaleKeys.settings_menu_invalidCloudURLScheme.tr(),\n      );\n    }\n  } catch (e) {\n    return FlowyResult.failure(e.toString());\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/appflowy_cloud_urls_bloc.dart",
    "content": "import 'package:appflowy/env/backend_env.dart';\nimport 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'appflowy_cloud_urls_bloc.freezed.dart';\n\nclass AppFlowyCloudURLsBloc\n    extends Bloc<AppFlowyCloudURLsEvent, AppFlowyCloudURLsState> {\n  AppFlowyCloudURLsBloc() : super(AppFlowyCloudURLsState.initial()) {\n    on<AppFlowyCloudURLsEvent>((event, emit) async {\n      await event.when(\n        initial: () async {},\n        updateServerUrl: (url) {\n          emit(\n            state.copyWith(\n              updatedServerUrl: url,\n              urlError: null,\n              showRestartHint: url.isNotEmpty,\n            ),\n          );\n        },\n        updateBaseWebDomain: (url) {\n          emit(\n            state.copyWith(\n              updatedBaseWebDomain: url,\n              urlError: null,\n              showRestartHint: url.isNotEmpty,\n            ),\n          );\n        },\n        confirmUpdate: () async {\n          if (state.updatedServerUrl.isEmpty) {\n            emit(\n              state.copyWith(\n                updatedServerUrl: \"\",\n                urlError:\n                    LocaleKeys.settings_menu_appFlowyCloudUrlCanNotBeEmpty.tr(),\n                restartApp: false,\n              ),\n            );\n          } else {\n            bool isSuccess = false;\n\n            await validateUrl(state.updatedServerUrl).fold(\n              (url) async {\n                await useSelfHostedAppFlowyCloud(url);\n                isSuccess = true;\n              },\n              (err) async => emit(state.copyWith(urlError: err)),\n            );\n\n            await validateUrl(state.updatedBaseWebDomain).fold(\n              (url) async {\n                await useBaseWebDomain(url);\n                isSuccess = true;\n              },\n              (err) async => emit(state.copyWith(urlError: err)),\n            );\n\n            if (isSuccess) {\n              add(const AppFlowyCloudURLsEvent.didSaveConfig());\n            }\n          }\n        },\n        didSaveConfig: () {\n          emit(\n            state.copyWith(\n              urlError: null,\n              restartApp: true,\n            ),\n          );\n        },\n      );\n    });\n  }\n}\n\n@freezed\nclass AppFlowyCloudURLsEvent with _$AppFlowyCloudURLsEvent {\n  const factory AppFlowyCloudURLsEvent.initial() = _Initial;\n  const factory AppFlowyCloudURLsEvent.updateServerUrl(String text) =\n      _ServerUrl;\n  const factory AppFlowyCloudURLsEvent.updateBaseWebDomain(String text) =\n      _UpdateBaseWebDomain;\n  const factory AppFlowyCloudURLsEvent.confirmUpdate() = _UpdateConfig;\n  const factory AppFlowyCloudURLsEvent.didSaveConfig() = _DidSaveConfig;\n}\n\n@freezed\nclass AppFlowyCloudURLsState with _$AppFlowyCloudURLsState {\n  const factory AppFlowyCloudURLsState({\n    required AppFlowyCloudConfiguration config,\n    required String updatedServerUrl,\n    required String updatedBaseWebDomain,\n    required String? urlError,\n    required bool restartApp,\n    required bool showRestartHint,\n  }) = _AppFlowyCloudURLsState;\n\n  factory AppFlowyCloudURLsState.initial() => AppFlowyCloudURLsState(\n        config: getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig,\n        urlError: null,\n        updatedServerUrl:\n            getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_url,\n        updatedBaseWebDomain:\n            getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_web_domain,\n        showRestartHint: getIt<AppFlowyCloudSharedEnv>()\n            .appflowyCloudConfig\n            .base_url\n            .isNotEmpty,\n        restartApp: false,\n      );\n}\n\nFlowyResult<String, String> validateUrl(String url) {\n  try {\n    // Use Uri.parse to validate the url.\n    final uri = Uri.parse(removeTrailingSlash(url));\n    if (uri.isScheme('HTTP') || uri.isScheme('HTTPS')) {\n      return FlowyResult.success(uri.toString());\n    } else {\n      return FlowyResult.failure(\n        LocaleKeys.settings_menu_invalidCloudURLScheme.tr(),\n      );\n    }\n  } catch (e) {\n    return FlowyResult.failure(e.toString());\n  }\n}\n\nString removeTrailingSlash(String input) {\n  if (input.endsWith('/')) {\n    return input.substring(0, input.length - 1);\n  }\n  return input;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/application_data_storage.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/foundation.dart';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:path/path.dart' as p;\n\nimport '../../../startup/tasks/prelude.dart';\n\nconst appFlowyDataFolder = \"AppFlowyDataDoNotRename\";\n\nclass ApplicationDataStorage {\n  ApplicationDataStorage();\n  String? _cachePath;\n\n  /// Set the custom path to store the data.\n  /// If the path is not exists, the path will be created.\n  /// If the path is invalid, the path will be set to the default path.\n  Future<void> setCustomPath(String path) async {\n    if (kIsWeb || Platform.isAndroid || Platform.isIOS) {\n      Log.info('LocalFileStorage is not supported on this platform.');\n      return;\n    }\n\n    if (Platform.isMacOS) {\n      // remove the prefix `/Volumes/*`\n      path = path.replaceFirst(macOSVolumesRegex, '');\n    } else if (Platform.isWindows) {\n      path = path.replaceAll('/', '\\\\');\n    }\n\n    // If the path is not ends with `AppFlowyData`, we will append the\n    // `AppFlowyData` to the path. If the path is ends with `AppFlowyData`,\n    // which means the path is the custom path.\n    if (p.basename(path) != appFlowyDataFolder) {\n      path = p.join(path, appFlowyDataFolder);\n    }\n\n    // create the directory if not exists.\n    final directory = Directory(path);\n    if (!directory.existsSync()) {\n      await directory.create(recursive: true);\n    }\n\n    await setPath(path);\n  }\n\n  Future<void> setPath(String path) async {\n    if (kIsWeb || Platform.isAndroid || Platform.isIOS) {\n      Log.info('LocalFileStorage is not supported on this platform.');\n      return;\n    }\n\n    await getIt<KeyValueStorage>().set(KVKeys.pathLocation, path);\n    // clear the cache path, and not set the cache path to the new path because the set path may be invalid\n    _cachePath = null;\n  }\n\n  Future<String> getPath() async {\n    if (_cachePath != null) {\n      return _cachePath!;\n    }\n\n    final response = await getIt<KeyValueStorage>().get(KVKeys.pathLocation);\n\n    String path;\n    if (response == null) {\n      final directory = await appFlowyApplicationDataDirectory();\n      path = directory.path;\n    } else {\n      path = response;\n    }\n    _cachePath = path;\n\n    // if the path is not exists means the path is invalid, so we should clear the kv store\n    if (!Directory(path).existsSync()) {\n      await getIt<KeyValueStorage>().clear();\n      final directory = await appFlowyApplicationDataDirectory();\n      path = directory.path;\n    }\n\n    return path;\n  }\n}\n\nclass MockApplicationDataStorage extends ApplicationDataStorage {\n  MockApplicationDataStorage();\n\n  // this value will be clear after setup\n  // only for the initial step\n  @visibleForTesting\n  static String? initialPath;\n\n  @override\n  Future<String> getPath() async {\n    final path = initialPath;\n    if (path != null) {\n      initialPath = null;\n      await super.setPath(path);\n      return Future.value(path);\n    }\n    return super.getPath();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/billing/settings_billing_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/foundation.dart';\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';\nimport 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart';\nimport 'package:appflowy/workspace/application/workspace/workspace_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pbserver.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:equatable/equatable.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:protobuf/protobuf.dart';\n\npart 'settings_billing_bloc.freezed.dart';\n\nclass SettingsBillingBloc\n    extends Bloc<SettingsBillingEvent, SettingsBillingState> {\n  SettingsBillingBloc({\n    required this.workspaceId,\n    required Int64 userId,\n  }) : super(const _Initial()) {\n    _userService = UserBackendService(userId: userId);\n    _service = WorkspaceService(workspaceId: workspaceId, userId: userId);\n    _successListenable = getIt<SubscriptionSuccessListenable>();\n    _successListenable.addListener(_onPaymentSuccessful);\n\n    on<SettingsBillingEvent>((event, emit) async {\n      await event.when(\n        started: () async {\n          emit(const SettingsBillingState.loading());\n\n          FlowyError? error;\n\n          final result = await UserBackendService.getWorkspaceSubscriptionInfo(\n            workspaceId,\n          );\n\n          final subscriptionInfo = result.fold(\n            (s) => s,\n            (e) {\n              error = e;\n              return null;\n            },\n          );\n\n          if (subscriptionInfo == null || error != null) {\n            return emit(SettingsBillingState.error(error: error));\n          }\n\n          if (!_billingPortalCompleter.isCompleted) {\n            unawaited(_fetchBillingPortal());\n            unawaited(\n              _billingPortalCompleter.future.then(\n                (result) {\n                  if (isClosed) return;\n\n                  result.fold(\n                    (portal) {\n                      _billingPortal = portal;\n                      add(\n                        SettingsBillingEvent.billingPortalFetched(\n                          billingPortal: portal,\n                        ),\n                      );\n                    },\n                    (e) => Log.error('Error fetching billing portal: $e'),\n                  );\n                },\n              ),\n            );\n          }\n\n          emit(\n            SettingsBillingState.ready(\n              subscriptionInfo: subscriptionInfo,\n              billingPortal: _billingPortal,\n            ),\n          );\n        },\n        billingPortalFetched: (billingPortal) async => state.maybeWhen(\n          orElse: () {},\n          ready: (subscriptionInfo, _, plan, isLoading) => emit(\n            SettingsBillingState.ready(\n              subscriptionInfo: subscriptionInfo,\n              billingPortal: billingPortal,\n              successfulPlanUpgrade: plan,\n              isLoading: isLoading,\n            ),\n          ),\n        ),\n        openCustomerPortal: () async {\n          if (_billingPortalCompleter.isCompleted && _billingPortal != null) {\n            return afLaunchUrlString(_billingPortal!.url);\n          }\n          await _billingPortalCompleter.future;\n          if (_billingPortal != null) {\n            await afLaunchUrlString(_billingPortal!.url);\n          }\n        },\n        addSubscription: (plan) async {\n          final result =\n              await _userService.createSubscription(workspaceId, plan);\n\n          result.fold(\n            (link) => afLaunchUrlString(link.paymentLink),\n            (f) => Log.error(f.msg, f),\n          );\n        },\n        cancelSubscription: (plan, reason) async {\n          final s = state.mapOrNull(ready: (s) => s);\n          if (s == null) {\n            return;\n          }\n\n          emit(s.copyWith(isLoading: true));\n\n          final result =\n              await _userService.cancelSubscription(workspaceId, plan, reason);\n          final successOrNull = result.fold(\n            (_) => true,\n            (f) {\n              Log.error(\n                'Failed to cancel subscription of ${plan.label}: ${f.msg}',\n                f,\n              );\n              return null;\n            },\n          );\n\n          if (successOrNull != true) {\n            return;\n          }\n\n          final subscriptionInfo = state.mapOrNull(\n            ready: (s) => s.subscriptionInfo,\n          );\n\n          // This is impossible, but for good measure\n          if (subscriptionInfo == null) {\n            return;\n          }\n\n          subscriptionInfo.freeze();\n          final newInfo = subscriptionInfo.rebuild((value) {\n            if (plan.isAddOn) {\n              value.addOns.removeWhere(\n                (addon) => addon.addOnSubscription.subscriptionPlan == plan,\n              );\n            }\n\n            if (plan == WorkspacePlanPB.ProPlan &&\n                value.plan == WorkspacePlanPB.ProPlan) {\n              value.plan = WorkspacePlanPB.FreePlan;\n              value.planSubscription.freeze();\n              value.planSubscription = value.planSubscription.rebuild((sub) {\n                sub.status = WorkspaceSubscriptionStatusPB.Active;\n                sub.subscriptionPlan = SubscriptionPlanPB.Free;\n              });\n            }\n          });\n\n          emit(\n            SettingsBillingState.ready(\n              subscriptionInfo: newInfo,\n              billingPortal: _billingPortal,\n            ),\n          );\n        },\n        paymentSuccessful: (plan) async {\n          final result = await UserBackendService.getWorkspaceSubscriptionInfo(\n            workspaceId,\n          );\n\n          final subscriptionInfo = result.toNullable();\n          if (subscriptionInfo != null) {\n            emit(\n              SettingsBillingState.ready(\n                subscriptionInfo: subscriptionInfo,\n                billingPortal: _billingPortal,\n              ),\n            );\n          }\n        },\n        updatePeriod: (plan, interval) async {\n          final s = state.mapOrNull(ready: (s) => s);\n          if (s == null) {\n            return;\n          }\n\n          emit(s.copyWith(isLoading: true));\n\n          final result = await _userService.updateSubscriptionPeriod(\n            workspaceId,\n            plan,\n            interval,\n          );\n          final successOrNull = result.fold((_) => true, (f) {\n            Log.error(\n              'Failed to update subscription period of ${plan.label}: ${f.msg}',\n              f,\n            );\n            return null;\n          });\n\n          if (successOrNull != true) {\n            return emit(s.copyWith(isLoading: false));\n          }\n\n          // Fetch new subscription info\n          final newResult =\n              await UserBackendService.getWorkspaceSubscriptionInfo(\n            workspaceId,\n          );\n\n          final newSubscriptionInfo = newResult.toNullable();\n          if (newSubscriptionInfo != null) {\n            emit(\n              SettingsBillingState.ready(\n                subscriptionInfo: newSubscriptionInfo,\n                billingPortal: _billingPortal,\n              ),\n            );\n          }\n        },\n      );\n    });\n  }\n\n  late final String workspaceId;\n  late final WorkspaceService _service;\n  late final UserBackendService _userService;\n  final _billingPortalCompleter =\n      Completer<FlowyResult<BillingPortalPB, FlowyError>>();\n\n  BillingPortalPB? _billingPortal;\n  late final SubscriptionSuccessListenable _successListenable;\n\n  @override\n  Future<void> close() {\n    _successListenable.removeListener(_onPaymentSuccessful);\n    return super.close();\n  }\n\n  Future<void> _fetchBillingPortal() async {\n    final billingPortalResult = await _service.getBillingPortal();\n    _billingPortalCompleter.complete(billingPortalResult);\n  }\n\n  Future<void> _onPaymentSuccessful() async => add(\n        SettingsBillingEvent.paymentSuccessful(\n          plan: _successListenable.subscribedPlan,\n        ),\n      );\n}\n\n@freezed\nclass SettingsBillingEvent with _$SettingsBillingEvent {\n  const factory SettingsBillingEvent.started() = _Started;\n\n  const factory SettingsBillingEvent.billingPortalFetched({\n    required BillingPortalPB billingPortal,\n  }) = _BillingPortalFetched;\n\n  const factory SettingsBillingEvent.openCustomerPortal() = _OpenCustomerPortal;\n\n  const factory SettingsBillingEvent.addSubscription(SubscriptionPlanPB plan) =\n      _AddSubscription;\n\n  const factory SettingsBillingEvent.cancelSubscription(\n    SubscriptionPlanPB plan, {\n    @Default(null) String? reason,\n  }) = _CancelSubscription;\n\n  const factory SettingsBillingEvent.paymentSuccessful({\n    SubscriptionPlanPB? plan,\n  }) = _PaymentSuccessful;\n\n  const factory SettingsBillingEvent.updatePeriod({\n    required SubscriptionPlanPB plan,\n    required RecurringIntervalPB interval,\n  }) = _UpdatePeriod;\n}\n\n@freezed\nclass SettingsBillingState extends Equatable with _$SettingsBillingState {\n  const SettingsBillingState._();\n\n  const factory SettingsBillingState.initial() = _Initial;\n\n  const factory SettingsBillingState.loading() = _Loading;\n\n  const factory SettingsBillingState.error({\n    @Default(null) FlowyError? error,\n  }) = _Error;\n\n  const factory SettingsBillingState.ready({\n    required WorkspaceSubscriptionInfoPB subscriptionInfo,\n    required BillingPortalPB? billingPortal,\n    @Default(null) SubscriptionPlanPB? successfulPlanUpgrade,\n    @Default(false) bool isLoading,\n  }) = _Ready;\n\n  @override\n  List<Object?> get props => maybeWhen(\n        orElse: () => const [],\n        error: (error) => [error],\n        ready: (subscription, billingPortal, plan, isLoading) => [\n          subscription,\n          billingPortal,\n          plan,\n          isLoading,\n          ...subscription.addOns,\n        ],\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/cloud_setting_bloc.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'cloud_setting_bloc.freezed.dart';\n\nclass CloudSettingBloc extends Bloc<CloudSettingEvent, CloudSettingState> {\n  CloudSettingBloc(AuthenticatorType cloudType)\n      : super(CloudSettingState.initial(cloudType)) {\n    on<CloudSettingEvent>((event, emit) async {\n      await event.when(\n        initial: () async {},\n        updateCloudType: (AuthenticatorType newCloudType) async {\n          emit(state.copyWith(cloudType: newCloudType));\n        },\n      );\n    });\n  }\n}\n\n@freezed\nclass CloudSettingEvent with _$CloudSettingEvent {\n  const factory CloudSettingEvent.initial() = _Initial;\n  const factory CloudSettingEvent.updateCloudType(\n    AuthenticatorType newCloudType,\n  ) = _UpdateCloudType;\n}\n\n@freezed\nclass CloudSettingState with _$CloudSettingState {\n  const factory CloudSettingState({\n    required AuthenticatorType cloudType,\n  }) = _CloudSettingState;\n\n  factory CloudSettingState.initial(AuthenticatorType cloudType) =>\n      CloudSettingState(\n        cloudType: cloudType,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/cloud_setting_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nimport '../../../core/notification/user_notification.dart';\n\nclass UserCloudConfigListener {\n  UserCloudConfigListener();\n\n  UserNotificationParser? _userParser;\n  StreamSubscription<SubscribeObject>? _subscription;\n  void Function(FlowyResult<CloudSettingPB, FlowyError>)? _onSettingChanged;\n\n  void start({\n    void Function(FlowyResult<CloudSettingPB, FlowyError>)? onSettingChanged,\n  }) {\n    _onSettingChanged = onSettingChanged;\n    _userParser = UserNotificationParser(\n      id: 'user_cloud_config',\n      callback: _userNotificationCallback,\n    );\n    _subscription = RustStreamReceiver.listen((observable) {\n      _userParser?.parse(observable);\n    });\n  }\n\n  Future<void> stop() async {\n    _userParser = null;\n    await _subscription?.cancel();\n    _onSettingChanged = null;\n  }\n\n  void _userNotificationCallback(\n    UserNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case UserNotification.DidUpdateCloudConfig:\n        result.fold(\n          (payload) => _onSettingChanged\n              ?.call(FlowyResult.success(CloudSettingPB.fromBuffer(payload))),\n          (error) => _onSettingChanged?.call(FlowyResult.failure(error)),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/create_file_settings_cubit.dart",
    "content": "import 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass CreateFileSettingsCubit extends Cubit<bool> {\n  CreateFileSettingsCubit(super.initialState) {\n    getInitialSettings();\n  }\n\n  Future<void> toggle({bool? value}) async {\n    await getIt<KeyValueStorage>().set(\n      KVKeys.showRenameDialogWhenCreatingNewFile,\n      (value ?? !state).toString(),\n    );\n    emit(value ?? !state);\n  }\n\n  Future<void> getInitialSettings() async {\n    final settingsOrFailure = await getIt<KeyValueStorage>().getWithFormat(\n      KVKeys.showRenameDialogWhenCreatingNewFile,\n      (value) => bool.parse(value),\n    );\n    emit(settingsOrFailure ?? false);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/date_time/date_format_ext.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nconst _localFmt = 'MM/dd/y';\nconst _usFmt = 'y/MM/dd';\nconst _isoFmt = 'y-MM-dd';\nconst _friendlyFmt = 'MMM dd, y';\nconst _dmyFmt = 'dd/MM/y';\n\nextension DateFormatter on UserDateFormatPB {\n  DateFormat get toFormat {\n    try {\n      return DateFormat(_toFormat[this] ?? _friendlyFmt);\n    } catch (_) {\n      // fallback to en-US\n      return DateFormat(_toFormat[this] ?? _friendlyFmt, 'en-US');\n    }\n  }\n\n  String formatDate(\n    DateTime date,\n    bool includeTime, [\n    UserTimeFormatPB? timeFormat,\n  ]) {\n    final format = toFormat;\n\n    if (includeTime) {\n      switch (timeFormat) {\n        case UserTimeFormatPB.TwentyFourHour:\n          return format.add_Hm().format(date);\n        case UserTimeFormatPB.TwelveHour:\n          return format.add_jm().format(date);\n        default:\n          return format.format(date);\n      }\n    }\n\n    return format.format(date);\n  }\n}\n\nfinal _toFormat = {\n  UserDateFormatPB.Locally: _localFmt,\n  UserDateFormatPB.US: _usFmt,\n  UserDateFormatPB.ISO: _isoFmt,\n  UserDateFormatPB.Friendly: _friendlyFmt,\n  UserDateFormatPB.DayMonthYear: _dmyFmt,\n};\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nextension TimeFormatter on UserTimeFormatPB {\n  DateFormat get toFormat => _toFormat[this]!;\n\n  String formatTime(DateTime date) => toFormat.format(date);\n}\n\nfinal _toFormat = {\n  UserTimeFormatPB.TwentyFourHour: DateFormat.Hm(),\n  UserTimeFormatPB.TwelveHour: DateFormat.jm(),\n};\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/file_storage/file_storage_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/notification_helper.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-storage/notification.pb.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass StoregeNotificationParser\n    extends NotificationParser<StorageNotification, FlowyError> {\n  StoregeNotificationParser({\n    super.id,\n    required super.callback,\n  }) : super(\n          tyParser: (ty, source) =>\n              source == \"storage\" ? StorageNotification.valueOf(ty) : null,\n          errorParser: (bytes) => FlowyError.fromBuffer(bytes),\n        );\n}\n\nclass StoreageNotificationListener {\n  StoreageNotificationListener({\n    void Function(FlowyError error)? onError,\n  }) : _parser = StoregeNotificationParser(\n          callback: (\n            StorageNotification ty,\n            FlowyResult<Uint8List, FlowyError> result,\n          ) {\n            result.fold(\n              (data) {\n                try {\n                  switch (ty) {\n                    case StorageNotification.FileStorageLimitExceeded:\n                      onError?.call(FlowyError.fromBuffer(data));\n                      break;\n                    case StorageNotification.SingleFileLimitExceeded:\n                      onError?.call(FlowyError.fromBuffer(data));\n                      break;\n                  }\n                } catch (e) {\n                  Log.error(\n                    \"$StoreageNotificationListener deserialize PB fail\",\n                    e,\n                  );\n                }\n              },\n              (err) {\n                Log.error(\"Error in StoreageNotificationListener\", err);\n              },\n            );\n          },\n        ) {\n    _subscription =\n        RustStreamReceiver.listen((observable) => _parser?.parse(observable));\n  }\n\n  StoregeNotificationParser? _parser;\n  StreamSubscription<SubscribeObject>? _subscription;\n\n  Future<void> stop() async {\n    _parser = null;\n    await _subscription?.cancel();\n    _subscription = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/notifications/notification_settings_cubit.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/user_settings_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'notification_settings_cubit.freezed.dart';\n\nclass NotificationSettingsCubit extends Cubit<NotificationSettingsState> {\n  NotificationSettingsCubit() : super(NotificationSettingsState.initial()) {\n    _initialize();\n  }\n\n  final Completer<void> _initCompleter = Completer();\n\n  late final NotificationSettingsPB _notificationSettings;\n\n  Future<void> _initialize() async {\n    _notificationSettings =\n        await UserSettingsBackendService().getNotificationSettings();\n\n    final showNotificationSetting = await getIt<KeyValueStorage>()\n        .getWithFormat(KVKeys.showNotificationIcon, (v) => bool.parse(v));\n\n    emit(\n      state.copyWith(\n        isNotificationsEnabled: _notificationSettings.notificationsEnabled,\n        isShowNotificationsIconEnabled: showNotificationSetting ?? true,\n      ),\n    );\n\n    _initCompleter.complete();\n  }\n\n  Future<void> toggleNotificationsEnabled() async {\n    await _initCompleter.future;\n\n    _notificationSettings.notificationsEnabled = !state.isNotificationsEnabled;\n\n    emit(\n      state.copyWith(\n        isNotificationsEnabled: _notificationSettings.notificationsEnabled,\n      ),\n    );\n\n    await _saveNotificationSettings();\n  }\n\n  Future<void> toggleShowNotificationIconEnabled() async {\n    await _initCompleter.future;\n\n    emit(\n      state.copyWith(\n        isShowNotificationsIconEnabled: !state.isShowNotificationsIconEnabled,\n      ),\n    );\n  }\n\n  Future<void> _saveNotificationSettings() async {\n    await _initCompleter.future;\n\n    await getIt<KeyValueStorage>().set(\n      KVKeys.showNotificationIcon,\n      state.isShowNotificationsIconEnabled.toString(),\n    );\n\n    final result = await UserSettingsBackendService()\n        .setNotificationSettings(_notificationSettings);\n    result.fold(\n      (r) => null,\n      (error) => Log.error(error),\n    );\n  }\n}\n\n@freezed\nclass NotificationSettingsState with _$NotificationSettingsState {\n  const NotificationSettingsState._();\n\n  const factory NotificationSettingsState({\n    required bool isNotificationsEnabled,\n    required bool isShowNotificationsIconEnabled,\n  }) = _NotificationSettingsState;\n\n  factory NotificationSettingsState.initial() =>\n      const NotificationSettingsState(\n        isNotificationsEnabled: true,\n        isShowNotificationsIconEnabled: true,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart",
    "content": "import 'package:flutter/foundation.dart';\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';\nimport 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart';\nimport 'package:appflowy/workspace/application/workspace/workspace_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pbserver.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:protobuf/protobuf.dart';\n\npart 'settings_plan_bloc.freezed.dart';\n\nclass SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {\n  SettingsPlanBloc({\n    required this.workspaceId,\n    required Int64 userId,\n  }) : super(const _Initial()) {\n    _service = WorkspaceService(\n      workspaceId: workspaceId,\n      userId: userId,\n    );\n    _userService = UserBackendService(userId: userId);\n    _successListenable = getIt<SubscriptionSuccessListenable>();\n    _successListenable.addListener(_onPaymentSuccessful);\n\n    on<SettingsPlanEvent>((event, emit) async {\n      await event.when(\n        started: (withSuccessfulUpgrade, shouldLoad) async {\n          if (shouldLoad) {\n            emit(const SettingsPlanState.loading());\n          }\n\n          final snapshots = await Future.wait([\n            _service.getWorkspaceUsage(),\n            UserBackendService.getWorkspaceSubscriptionInfo(workspaceId),\n          ]);\n\n          FlowyError? error;\n\n          final usageResult = snapshots.first.fold(\n            (s) => s as WorkspaceUsagePB?,\n            (f) {\n              error = f;\n              return null;\n            },\n          );\n\n          final subscriptionInfo = snapshots[1].fold(\n            (s) => s as WorkspaceSubscriptionInfoPB,\n            (f) {\n              error = f;\n              return null;\n            },\n          );\n\n          if (usageResult == null ||\n              subscriptionInfo == null ||\n              error != null) {\n            return emit(SettingsPlanState.error(error: error));\n          }\n\n          emit(\n            SettingsPlanState.ready(\n              workspaceUsage: usageResult,\n              subscriptionInfo: subscriptionInfo,\n              successfulPlanUpgrade: withSuccessfulUpgrade,\n            ),\n          );\n\n          if (withSuccessfulUpgrade != null) {\n            emit(\n              SettingsPlanState.ready(\n                workspaceUsage: usageResult,\n                subscriptionInfo: subscriptionInfo,\n              ),\n            );\n          }\n        },\n        addSubscription: (plan) async {\n          final result = await _userService.createSubscription(\n            workspaceId,\n            plan,\n          );\n\n          result.fold(\n            (pl) => afLaunchUrlString(pl.paymentLink),\n            (f) => Log.error(\n              'Failed to fetch paymentlink for $plan: ${f.msg}',\n              f,\n            ),\n          );\n        },\n        cancelSubscription: (reason) async {\n          final newState = state\n              .mapOrNull(ready: (state) => state)\n              ?.copyWith(downgradeProcessing: true);\n          emit(newState ?? state);\n\n          // We can hardcode the subscription plan here because we cannot cancel addons\n          // on the Plan page\n          final result = await _userService.cancelSubscription(\n            workspaceId,\n            SubscriptionPlanPB.Pro,\n            reason,\n          );\n\n          final successOrNull = result.fold(\n            (_) => true,\n            (f) {\n              Log.error('Failed to cancel subscription of Pro: ${f.msg}', f);\n              return null;\n            },\n          );\n\n          if (successOrNull != true) {\n            return;\n          }\n\n          final subscriptionInfo = state.mapOrNull(\n            ready: (s) => s.subscriptionInfo,\n          );\n\n          // This is impossible, but for good measure\n          if (subscriptionInfo == null) {\n            return;\n          }\n\n          // We assume their new plan is Free, since we only have Pro plan\n          // at the moment.\n          subscriptionInfo.freeze();\n          final newInfo = subscriptionInfo.rebuild((value) {\n            value.plan = WorkspacePlanPB.FreePlan;\n            value.planSubscription.freeze();\n            value.planSubscription = value.planSubscription.rebuild((sub) {\n              sub.status = WorkspaceSubscriptionStatusPB.Active;\n              sub.subscriptionPlan = SubscriptionPlanPB.Free;\n            });\n          });\n\n          // We need to remove unlimited indicator for storage and\n          // AI usage, if they don't have an addon that changes this behavior.\n          final usage = state.mapOrNull(ready: (s) => s.workspaceUsage)!;\n\n          usage.freeze();\n          final newUsage = usage.rebuild((value) {\n            if (!newInfo.hasAIMax) {\n              value.aiResponsesUnlimited = false;\n            }\n\n            value.storageBytesUnlimited = false;\n          });\n\n          emit(\n            SettingsPlanState.ready(\n              subscriptionInfo: newInfo,\n              workspaceUsage: newUsage,\n            ),\n          );\n        },\n        paymentSuccessful: (plan) {\n          final readyState = state.mapOrNull(ready: (state) => state);\n          if (readyState == null) {\n            return;\n          }\n\n          add(\n            SettingsPlanEvent.started(\n              withSuccessfulUpgrade: plan,\n              shouldLoad: false,\n            ),\n          );\n        },\n      );\n    });\n  }\n\n  late final String workspaceId;\n  late final WorkspaceService _service;\n  late final IUserBackendService _userService;\n  late final SubscriptionSuccessListenable _successListenable;\n\n  Future<void> _onPaymentSuccessful() async => add(\n        SettingsPlanEvent.paymentSuccessful(\n          plan: _successListenable.subscribedPlan,\n        ),\n      );\n\n  @override\n  Future<void> close() async {\n    _successListenable.removeListener(_onPaymentSuccessful);\n    return super.close();\n  }\n}\n\n@freezed\nclass SettingsPlanEvent with _$SettingsPlanEvent {\n  const factory SettingsPlanEvent.started({\n    @Default(null) SubscriptionPlanPB? withSuccessfulUpgrade,\n    @Default(true) bool shouldLoad,\n  }) = _Started;\n\n  const factory SettingsPlanEvent.addSubscription(SubscriptionPlanPB plan) =\n      _AddSubscription;\n\n  const factory SettingsPlanEvent.cancelSubscription({\n    @Default(null) String? reason,\n  }) = _CancelSubscription;\n\n  const factory SettingsPlanEvent.paymentSuccessful({\n    @Default(null) SubscriptionPlanPB? plan,\n  }) = _PaymentSuccessful;\n}\n\n@freezed\nclass SettingsPlanState with _$SettingsPlanState {\n  const factory SettingsPlanState.initial() = _Initial;\n\n  const factory SettingsPlanState.loading() = _Loading;\n\n  const factory SettingsPlanState.error({\n    @Default(null) FlowyError? error,\n  }) = _Error;\n\n  const factory SettingsPlanState.ready({\n    required WorkspaceUsagePB workspaceUsage,\n    required WorkspaceSubscriptionInfoPB subscriptionInfo,\n    @Default(null) SubscriptionPlanPB? successfulPlanUpgrade,\n    @Default(false) bool downgradeProcessing,\n  }) = _Ready;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/plan/workspace_subscription_ext.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pbserver.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nextension SubscriptionInfoHelpers on WorkspaceSubscriptionInfoPB {\n  String get label => switch (plan) {\n        WorkspacePlanPB.FreePlan =>\n          LocaleKeys.settings_planPage_planUsage_currentPlan_freeTitle.tr(),\n        WorkspacePlanPB.ProPlan =>\n          LocaleKeys.settings_planPage_planUsage_currentPlan_proTitle.tr(),\n        WorkspacePlanPB.TeamPlan =>\n          LocaleKeys.settings_planPage_planUsage_currentPlan_teamTitle.tr(),\n        _ => 'N/A',\n      };\n\n  String get info => switch (plan) {\n        WorkspacePlanPB.FreePlan =>\n          LocaleKeys.settings_planPage_planUsage_currentPlan_freeInfo.tr(),\n        WorkspacePlanPB.ProPlan =>\n          LocaleKeys.settings_planPage_planUsage_currentPlan_proInfo.tr(),\n        WorkspacePlanPB.TeamPlan =>\n          LocaleKeys.settings_planPage_planUsage_currentPlan_teamInfo.tr(),\n        _ => 'N/A',\n      };\n\n  bool get isBillingPortalEnabled {\n    if (plan != WorkspacePlanPB.FreePlan || addOns.isNotEmpty) {\n      return true;\n    }\n\n    return false;\n  }\n}\n\nextension AllSubscriptionLabels on SubscriptionPlanPB {\n  String get label => switch (this) {\n        SubscriptionPlanPB.Free =>\n          LocaleKeys.settings_planPage_planUsage_currentPlan_freeTitle.tr(),\n        SubscriptionPlanPB.Pro =>\n          LocaleKeys.settings_planPage_planUsage_currentPlan_proTitle.tr(),\n        SubscriptionPlanPB.Team =>\n          LocaleKeys.settings_planPage_planUsage_currentPlan_teamTitle.tr(),\n        SubscriptionPlanPB.AiMax =>\n          LocaleKeys.settings_billingPage_addons_aiMax_label.tr(),\n        SubscriptionPlanPB.AiLocal =>\n          LocaleKeys.settings_billingPage_addons_aiOnDevice_label.tr(),\n        _ => 'N/A',\n      };\n}\n\nextension WorkspaceSubscriptionStatusExt on WorkspaceSubscriptionInfoPB {\n  bool get isCanceled =>\n      planSubscription.status == WorkspaceSubscriptionStatusPB.Canceled;\n}\n\nextension WorkspaceAddonsExt on WorkspaceSubscriptionInfoPB {\n  bool get hasAIMax =>\n      addOns.any((addon) => addon.type == WorkspaceAddOnPBType.AddOnAiMax);\n\n  bool get hasAIOnDevice =>\n      addOns.any((addon) => addon.type == WorkspaceAddOnPBType.AddOnAiLocal);\n}\n\n/// These have to match [SubscriptionSuccessListenable.subscribedPlan] labels\nextension ToRecognizable on SubscriptionPlanPB {\n  String? toRecognizable() => switch (this) {\n        SubscriptionPlanPB.Free => 'free',\n        SubscriptionPlanPB.Pro => 'pro',\n        SubscriptionPlanPB.Team => 'team',\n        SubscriptionPlanPB.AiMax => 'ai_max',\n        SubscriptionPlanPB.AiLocal => 'ai_local',\n        _ => null,\n      };\n}\n\nextension PlanHelper on SubscriptionPlanPB {\n  /// Returns true if the plan is an add-on and not\n  /// a workspace plan.\n  ///\n  bool get isAddOn => switch (this) {\n        SubscriptionPlanPB.AiMax => true,\n        SubscriptionPlanPB.AiLocal => true,\n        _ => false,\n      };\n\n  String get priceMonthBilling => switch (this) {\n        SubscriptionPlanPB.Free => 'US\\$0',\n        SubscriptionPlanPB.Pro => 'US\\$12.5',\n        SubscriptionPlanPB.Team => 'US\\$15',\n        SubscriptionPlanPB.AiMax => 'US\\$10',\n        SubscriptionPlanPB.AiLocal => 'US\\$10',\n        _ => 'US\\$0',\n      };\n\n  String get priceAnnualBilling => switch (this) {\n        SubscriptionPlanPB.Free => 'US\\$0',\n        SubscriptionPlanPB.Pro => 'US\\$10',\n        SubscriptionPlanPB.Team => 'US\\$12.5',\n        SubscriptionPlanPB.AiMax => 'US\\$8',\n        SubscriptionPlanPB.AiLocal => 'US\\$8',\n        _ => 'US\\$0',\n      };\n}\n\nextension IntervalLabel on RecurringIntervalPB {\n  String get label => switch (this) {\n        RecurringIntervalPB.Month =>\n          LocaleKeys.settings_billingPage_monthlyInterval.tr(),\n        RecurringIntervalPB.Year =>\n          LocaleKeys.settings_billingPage_annualInterval.tr(),\n        _ => LocaleKeys.settings_billingPage_monthlyInterval.tr(),\n      };\n\n  String get priceInfo => switch (this) {\n        RecurringIntervalPB.Month =>\n          LocaleKeys.settings_billingPage_monthlyPriceInfo.tr(),\n        RecurringIntervalPB.Year =>\n          LocaleKeys.settings_billingPage_annualPriceInfo.tr(),\n        _ => LocaleKeys.settings_billingPage_monthlyPriceInfo.tr(),\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/plan/workspace_usage_ext.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:intl/intl.dart';\n\nfinal _storageNumberFormat = NumberFormat()\n  ..maximumFractionDigits = 2\n  ..minimumFractionDigits = 0;\n\nextension PresentableUsage on WorkspaceUsagePB {\n  String get totalBlobInGb {\n    if (storageBytesLimit == 0) {\n      return '0';\n    }\n    return _storageNumberFormat\n        .format(storageBytesLimit.toInt() / (1024 * 1024 * 1024));\n  }\n\n  /// We use [NumberFormat] to format the current blob in GB.\n  ///\n  /// Where the [totalBlobBytes] is the total blob bytes in bytes.\n  /// And [NumberFormat.maximumFractionDigits] is set to 2.\n  /// And [NumberFormat.minimumFractionDigits] is set to 0.\n  ///\n  String get currentBlobInGb =>\n      _storageNumberFormat.format(storageBytes.toInt() / 1024 / 1024 / 1024);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/prelude.dart",
    "content": "export 'application_data_storage.dart';\nexport 'create_file_settings_cubit.dart';\nexport 'settings_dialog_bloc.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/setting_file_importer_bloc.dart",
    "content": "import 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/plugins/database/application/defines.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/import_data.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'setting_file_importer_bloc.freezed.dart';\n\nclass SettingFileImportBloc\n    extends Bloc<SettingFileImportEvent, SettingFileImportState> {\n  SettingFileImportBloc() : super(SettingFileImportState.initial()) {\n    on<SettingFileImportEvent>(\n      (event, emit) async {\n        await event.when(\n          importAppFlowyDataFolder: (String path) async {\n            final formattedDate =\n                DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());\n            final spaceId =\n                await getIt<KeyValueStorage>().get(KVKeys.lastOpenedSpaceId);\n\n            final payload = ImportAppFlowyDataPB.create()\n              ..path = path\n              ..importContainerName = \"import_$formattedDate\";\n\n            if (spaceId != null) {\n              payload.parentViewId = spaceId;\n            }\n\n            emit(\n              state.copyWith(loadingState: const LoadingState.loading()),\n            );\n            final result =\n                await UserEventImportAppFlowyDataFolder(payload).send();\n            if (!isClosed) {\n              add(SettingFileImportEvent.finishImport(result));\n            }\n          },\n          finishImport: (result) {\n            result.fold(\n              (l) {\n                emit(\n                  state.copyWith(\n                    successOrFail: FlowyResult.success(null),\n                    loadingState:\n                        LoadingState.finish(FlowyResult.success(null)),\n                  ),\n                );\n              },\n              (err) {\n                Log.error(err);\n                emit(\n                  state.copyWith(\n                    successOrFail: FlowyResult.failure(err),\n                    loadingState: LoadingState.finish(FlowyResult.failure(err)),\n                  ),\n                );\n              },\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\n@freezed\nclass SettingFileImportEvent with _$SettingFileImportEvent {\n  const factory SettingFileImportEvent.importAppFlowyDataFolder(String path) =\n      _ImportAppFlowyDataFolder;\n  const factory SettingFileImportEvent.finishImport(\n    FlowyResult<void, FlowyError> result,\n  ) = _ImportResult;\n}\n\n@freezed\nclass SettingFileImportState with _$SettingFileImportState {\n  const factory SettingFileImportState({\n    required LoadingState loadingState,\n    required FlowyResult<void, FlowyError>? successOrFail,\n  }) = _SettingFileImportState;\n\n  factory SettingFileImportState.initial() => const SettingFileImportState(\n        loadingState: LoadingState.idle(),\n        successOrFail: null,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart",
    "content": "import 'package:appflowy/user/application/user_listener.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'settings_dialog_bloc.freezed.dart';\n\nenum SettingsPage {\n  // NEW\n  account,\n  workspace,\n  manageData,\n  shortcuts,\n  ai,\n  plan,\n  billing,\n  sites,\n  // OLD\n  notifications,\n  cloud,\n  member,\n  featureFlags,\n}\n\nclass SettingsDialogBloc\n    extends Bloc<SettingsDialogEvent, SettingsDialogState> {\n  SettingsDialogBloc(\n    UserProfilePB userProfile,\n    this.currentWorkspaceMemberRole, {\n    SettingsPage? initPage,\n  })  : _userListener = UserListener(userProfile: userProfile),\n        super(SettingsDialogState.initial(userProfile, initPage)) {\n    _dispatch();\n  }\n\n  final AFRolePB? currentWorkspaceMemberRole;\n  final UserListener _userListener;\n\n  @override\n  Future<void> close() async {\n    await _userListener.stop();\n    await super.close();\n  }\n\n  void _dispatch() {\n    on<SettingsDialogEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            _userListener.start(onProfileUpdated: _profileUpdated);\n\n            final isBillingEnabled = await _isBillingEnabled(\n              state.userProfile,\n              currentWorkspaceMemberRole,\n            );\n            if (isBillingEnabled) {\n              emit(state.copyWith(isBillingEnabled: true));\n            }\n          },\n          didReceiveUserProfile: (UserProfilePB newUserProfile) {\n            emit(state.copyWith(userProfile: newUserProfile));\n          },\n          setSelectedPage: (SettingsPage page) {\n            emit(state.copyWith(page: page));\n          },\n        );\n      },\n    );\n  }\n\n  void _profileUpdated(\n    FlowyResult<UserProfilePB, FlowyError> userProfileOrFailed,\n  ) {\n    userProfileOrFailed.fold(\n      (newUserProfile) {\n        if (!isClosed) {\n          add(SettingsDialogEvent.didReceiveUserProfile(newUserProfile));\n        }\n      },\n      (err) => Log.error(err),\n    );\n  }\n\n  Future<bool> _isBillingEnabled(\n    UserProfilePB userProfile, [\n    AFRolePB? currentWorkspaceMemberRole,\n  ]) async {\n    if ([\n      WorkspaceTypePB.LocalW,\n    ].contains(userProfile.workspaceType)) {\n      return false;\n    }\n\n    if (currentWorkspaceMemberRole == null ||\n        currentWorkspaceMemberRole != AFRolePB.Owner) {\n      return false;\n    }\n\n    if (kDebugMode) {\n      return true;\n    }\n\n    final result = await UserEventGetCloudConfig().send();\n    return result.fold(\n      (cloudSetting) {\n        final whiteList = [\n          \"https://beta.appflowy.cloud\",\n          \"https://test.appflowy.cloud\",\n        ];\n\n        return whiteList.contains(cloudSetting.serverUrl);\n      },\n      (err) {\n        Log.error(\"Failed to get cloud config: $err\");\n        return false;\n      },\n    );\n  }\n}\n\n@freezed\nclass SettingsDialogEvent with _$SettingsDialogEvent {\n  const factory SettingsDialogEvent.initial() = _Initial;\n  const factory SettingsDialogEvent.didReceiveUserProfile(\n    UserProfilePB newUserProfile,\n  ) = _DidReceiveUserProfile;\n  const factory SettingsDialogEvent.setSelectedPage(SettingsPage page) =\n      _SetViewIndex;\n}\n\n@freezed\nclass SettingsDialogState with _$SettingsDialogState {\n  const factory SettingsDialogState({\n    required UserProfilePB userProfile,\n    required SettingsPage page,\n    required bool isBillingEnabled,\n  }) = _SettingsDialogState;\n\n  factory SettingsDialogState.initial(\n    UserProfilePB userProfile,\n    SettingsPage? page,\n  ) =>\n      SettingsDialogState(\n        userProfile: userProfile,\n        page: page ?? SettingsPage.account,\n        isBillingEnabled: false,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/settings_file_exporter_cubit.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SettingsFileExportState {\n  SettingsFileExportState({\n    required this.views,\n  }) {\n    initialize();\n  }\n\n  List<ViewPB> get selectedViews {\n    final selectedViews = <ViewPB>[];\n    for (var i = 0; i < views.length; i++) {\n      if (selectedApps[i]) {\n        for (var j = 0; j < views[i].childViews.length; j++) {\n          if (selectedItems[i][j]) {\n            selectedViews.add(views[i].childViews[j]);\n          }\n        }\n      }\n    }\n    return selectedViews;\n  }\n\n  List<ViewPB> views;\n  List<bool> expanded = [];\n  List<bool> selectedApps = [];\n  List<List<bool>> selectedItems = [];\n\n  SettingsFileExportState copyWith({\n    List<ViewPB>? views,\n    List<bool>? expanded,\n    List<bool>? selectedApps,\n    List<List<bool>>? selectedItems,\n  }) {\n    final state = SettingsFileExportState(\n      views: views ?? this.views,\n    );\n    state.expanded = expanded ?? this.expanded;\n    state.selectedApps = selectedApps ?? this.selectedApps;\n    state.selectedItems = selectedItems ?? this.selectedItems;\n    return state;\n  }\n\n  void initialize() {\n    expanded = views.map((e) => true).toList();\n    selectedApps = views.map((e) => true).toList();\n    selectedItems =\n        views.map((e) => e.childViews.map((e) => true).toList()).toList();\n  }\n}\n\nclass SettingsFileExporterCubit extends Cubit<SettingsFileExportState> {\n  SettingsFileExporterCubit({\n    required List<ViewPB> views,\n  }) : super(SettingsFileExportState(views: views));\n\n  void selectOrDeselectAllItems() {\n    final List<List<bool>> selectedItems = state.selectedItems;\n    final isSelectAll =\n        selectedItems.expand((element) => element).every((element) => element);\n    for (var i = 0; i < selectedItems.length; i++) {\n      for (var j = 0; j < selectedItems[i].length; j++) {\n        selectedItems[i][j] = !isSelectAll;\n      }\n    }\n    emit(state.copyWith(selectedItems: selectedItems));\n  }\n\n  void selectOrDeselectItem(int outerIndex, int innerIndex) {\n    final selectedItems = state.selectedItems;\n    selectedItems[outerIndex][innerIndex] =\n        !selectedItems[outerIndex][innerIndex];\n    emit(state.copyWith(selectedItems: selectedItems));\n  }\n\n  void expandOrUnexpandApp(int outerIndex) {\n    final expanded = state.expanded;\n    expanded[outerIndex] = !expanded[outerIndex];\n    emit(state.copyWith(expanded: expanded));\n  }\n\n  Map<String, List<String>> fetchSelectedPages() {\n    final views = state.views;\n    final selectedItems = state.selectedItems;\n    final Map<String, List<String>> result = {};\n    for (var i = 0; i < selectedItems.length; i++) {\n      final selectedItem = selectedItems[i];\n      final ids = <String>[];\n      for (var j = 0; j < selectedItem.length; j++) {\n        if (selectedItem[j]) {\n          ids.add(views[i].childViews[j].id);\n        }\n      }\n      if (ids.isNotEmpty) {\n        result[views[i].id] = ids;\n      }\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/share/export_service.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/share_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass BackendExportService {\n  static Future<FlowyResult<DatabaseExportDataPB, FlowyError>>\n      exportDatabaseAsCSV(\n    String viewId,\n  ) async {\n    final payload = DatabaseViewIdPB.create()..value = viewId;\n    return DatabaseEventExportCSV(payload).send();\n  }\n\n  static Future<FlowyResult<DatabaseExportDataPB, FlowyError>>\n      exportDatabaseAsRawData(\n    String viewId,\n  ) async {\n    final payload = DatabaseViewIdPB.create()..value = viewId;\n    return DatabaseEventExportRawDatabaseData(payload).send();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/share/import_service.dart",
    "content": "import 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\nclass ImportPayload {\n  ImportPayload({\n    required this.name,\n    required this.data,\n    required this.layout,\n  });\n\n  final String name;\n  final List<int> data;\n  final ViewLayoutPB layout;\n}\n\nclass ImportBackendService {\n  static Future<FlowyResult<RepeatedViewPB, FlowyError>> importPages(\n    String parentViewId,\n    List<ImportItemPayloadPB> values,\n  ) async {\n    final request = ImportPayloadPB(\n      parentViewId: parentViewId,\n      items: values,\n    );\n\n    return FolderEventImportData(request).send();\n  }\n\n  static Future<FlowyResult<void, FlowyError>> importZipFiles(\n    List<ImportZipPB> values,\n  ) async {\n    for (final value in values) {\n      final result = await FolderEventImportZipFile(value).send();\n      if (result.isFailure) {\n        return result;\n      }\n    }\n    return FlowyResult.success(null);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'settings_shortcuts_cubit.freezed.dart';\n\n@freezed\nclass ShortcutsState with _$ShortcutsState {\n  const factory ShortcutsState({\n    @Default(<CommandShortcutEvent>[])\n    List<CommandShortcutEvent> commandShortcutEvents,\n    @Default(ShortcutsStatus.initial) ShortcutsStatus status,\n    @Default('') String error,\n  }) = _ShortcutsState;\n}\n\nenum ShortcutsStatus {\n  initial,\n  updating,\n  success,\n  failure;\n\n  /// Helper getter for when the [ShortcutsStatus] signifies\n  /// that the shortcuts have not been loaded yet.\n  ///\n  bool get isLoading => [initial, updating].contains(this);\n\n  /// Helper getter for when the [ShortcutsStatus] signifies\n  /// a failure by itself being [ShortcutsStatus.failure]\n  ///\n  bool get isFailure => this == ShortcutsStatus.failure;\n\n  /// Helper getter for when the [ShortcutsStatus] signifies\n  /// a success by itself being [ShortcutsStatus.success]\n  ///\n  bool get isSuccess => this == ShortcutsStatus.success;\n}\n\nclass ShortcutsCubit extends Cubit<ShortcutsState> {\n  ShortcutsCubit(this.service) : super(const ShortcutsState());\n\n  final SettingsShortcutService service;\n\n  Future<void> fetchShortcuts() async {\n    emit(\n      state.copyWith(\n        status: ShortcutsStatus.updating,\n        error: '',\n      ),\n    );\n\n    try {\n      final customizeShortcuts = await service.getCustomizeShortcuts();\n      await service.updateCommandShortcuts(\n        commandShortcutEvents,\n        customizeShortcuts,\n      );\n\n      //sort the shortcuts\n      commandShortcutEvents.sort(\n        (a, b) => a.key.toLowerCase().compareTo(b.key.toLowerCase()),\n      );\n\n      emit(\n        state.copyWith(\n          status: ShortcutsStatus.success,\n          commandShortcutEvents: commandShortcutEvents,\n          error: '',\n        ),\n      );\n    } catch (e) {\n      emit(\n        state.copyWith(\n          status: ShortcutsStatus.failure,\n          error: LocaleKeys.settings_shortcutsPage_couldNotLoadErrorMsg.tr(),\n        ),\n      );\n    }\n  }\n\n  Future<void> updateAllShortcuts() async {\n    emit(state.copyWith(status: ShortcutsStatus.updating, error: ''));\n\n    try {\n      await service.saveAllShortcuts(state.commandShortcutEvents);\n      emit(state.copyWith(status: ShortcutsStatus.success, error: ''));\n    } catch (e) {\n      emit(\n        state.copyWith(\n          status: ShortcutsStatus.failure,\n          error: LocaleKeys.settings_shortcutsPage_couldNotSaveErrorMsg.tr(),\n        ),\n      );\n    }\n  }\n\n  Future<void> resetToDefault() async {\n    emit(state.copyWith(status: ShortcutsStatus.updating, error: ''));\n\n    try {\n      await service.saveAllShortcuts(defaultCommandShortcutEvents);\n      await fetchShortcuts();\n    } catch (e) {\n      emit(\n        state.copyWith(\n          status: ShortcutsStatus.failure,\n          error: LocaleKeys.settings_shortcutsPage_couldNotSaveErrorMsg.tr(),\n        ),\n      );\n    }\n  }\n\n  /// Checks if the new command is conflicting with other shortcut\n  /// We also check using the key, whether this command is a codeblock\n  /// shortcut, if so we only check a conflict with other codeblock shortcut.\n  CommandShortcutEvent? getConflict(\n    CommandShortcutEvent currentShortcut,\n    String command,\n  ) {\n    // check if currentShortcut is a codeblock shortcut.\n    final isCodeBlockCommand = currentShortcut.isCodeBlockCommand;\n\n    for (final shortcut in state.commandShortcutEvents) {\n      final keybindings = shortcut.command.split(',');\n      if (keybindings.contains(command) &&\n          shortcut.isCodeBlockCommand == isCodeBlockCommand) {\n        return shortcut;\n      }\n    }\n\n    return null;\n  }\n}\n\nextension on CommandShortcutEvent {\n  bool get isCodeBlockCommand => localizedCodeBlockCommands.contains(this);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_service.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/settings/application_data_storage.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:path/path.dart' as p;\n\nimport 'shortcuts_model.dart';\n\nclass SettingsShortcutService {\n  /// If file is non null then the SettingsShortcutService uses that\n  /// file to store all the shortcuts, otherwise uses the default\n  /// Document Directory.\n  /// Typically we only intend to pass a file during testing.\n  SettingsShortcutService({\n    File? file,\n  }) {\n    _initializeService(file);\n  }\n\n  late final File _file;\n  final _initCompleter = Completer<void>();\n\n  /// Takes in commandShortcuts as an input and saves them to the shortcuts.JSON file.\n  Future<void> saveAllShortcuts(\n    List<CommandShortcutEvent> commandShortcuts,\n  ) async {\n    final shortcuts = EditorShortcuts(\n      commandShortcuts: commandShortcuts.toCommandShortcutModelList(),\n    );\n\n    await _file.writeAsString(\n      jsonEncode(shortcuts.toJson()),\n      flush: true,\n    );\n  }\n\n  /// Checks the file for saved shortcuts. If shortcuts do NOT exist then returns\n  /// an empty list. If shortcuts exist\n  /// then calls an utility method i.e getShortcutsFromJson which returns the saved shortcuts.\n  Future<List<CommandShortcutModel>> getCustomizeShortcuts() async {\n    await _initCompleter.future;\n    final shortcutsInJson = await _file.readAsString();\n\n    if (shortcutsInJson.isEmpty) {\n      return [];\n    } else {\n      return getShortcutsFromJson(shortcutsInJson);\n    }\n  }\n\n  // Extracts shortcuts from the saved json file. The shortcuts in the saved file consist of [List<CommandShortcutModel>].\n  // This list needs to be converted to List<CommandShortcutEvent\\>. This function is intended to facilitate the same.\n  List<CommandShortcutModel> getShortcutsFromJson(String savedJson) {\n    final shortcuts = EditorShortcuts.fromJson(jsonDecode(savedJson));\n    return shortcuts.commandShortcuts;\n  }\n\n  Future<void> updateCommandShortcuts(\n    List<CommandShortcutEvent> commandShortcuts,\n    List<CommandShortcutModel> customizeShortcuts,\n  ) async {\n    for (final shortcut in customizeShortcuts) {\n      final shortcutEvent = commandShortcuts.firstWhereOrNull(\n        (s) => s.key == shortcut.key && s.command != shortcut.command,\n      );\n      shortcutEvent?.updateCommand(command: shortcut.command);\n    }\n  }\n\n  Future<void> resetToDefaultShortcuts() async {\n    await _initCompleter.future;\n    await saveAllShortcuts(defaultCommandShortcutEvents);\n  }\n\n  // Accesses the shortcuts.json file within the default AppFlowy Document Directory or creates a new file if it already doesn't exist.\n  Future<void> _initializeService(File? file) async {\n    _file = file ?? await _defaultShortcutFile();\n    _initCompleter.complete();\n  }\n\n  //returns the default file for storing shortcuts\n  Future<File> _defaultShortcutFile() async {\n    final path = await getIt<ApplicationDataStorage>().getPath();\n    return File(\n      p.join(path, 'shortcuts', 'shortcuts.json'),\n    )..createSync(recursive: true);\n  }\n}\n\nextension on List<CommandShortcutEvent> {\n  /// Utility method for converting a CommandShortcutEvent List to a\n  /// CommandShortcutModal List. This is necessary for creating shortcuts\n  /// object, which is used for saving the shortcuts list.\n  List<CommandShortcutModel> toCommandShortcutModelList() =>\n      map((e) => CommandShortcutModel.fromCommandEvent(e)).toList();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/shortcuts_model.dart",
    "content": "import 'package:appflowy_editor/appflowy_editor.dart';\n\nclass EditorShortcuts {\n  factory EditorShortcuts.fromJson(Map<String, dynamic> json) =>\n      EditorShortcuts(\n        commandShortcuts: List<CommandShortcutModel>.from(\n          json[\"commandShortcuts\"].map((x) => CommandShortcutModel.fromJson(x)),\n        ),\n      );\n\n  EditorShortcuts({required this.commandShortcuts});\n\n  final List<CommandShortcutModel> commandShortcuts;\n\n  Map<String, dynamic> toJson() => {\n        \"commandShortcuts\":\n            List<dynamic>.from(commandShortcuts.map((x) => x.toJson())),\n      };\n}\n\nclass CommandShortcutModel {\n  factory CommandShortcutModel.fromCommandEvent(\n    CommandShortcutEvent commandShortcutEvent,\n  ) =>\n      CommandShortcutModel(\n        key: commandShortcutEvent.key,\n        command: commandShortcutEvent.command,\n      );\n\n  factory CommandShortcutModel.fromJson(Map<String, dynamic> json) =>\n      CommandShortcutModel(\n        key: json[\"key\"],\n        command: json[\"command\"] ?? '',\n      );\n\n  const CommandShortcutModel({required this.key, required this.command});\n\n  final String key;\n  final String command;\n\n  Map<String, dynamic> toJson() => {\"key\": key, \"command\": command};\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is CommandShortcutModel &&\n          key == other.key &&\n          command == other.command;\n\n  @override\n  int get hashCode => key.hashCode ^ command.hashCode;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart",
    "content": "import 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:collection/collection.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:protobuf/protobuf.dart';\n\npart 'workspace_settings_bloc.freezed.dart';\n\nclass WorkspaceSettingsBloc\n    extends Bloc<WorkspaceSettingsEvent, WorkspaceSettingsState> {\n  WorkspaceSettingsBloc() : super(WorkspaceSettingsState.initial()) {\n    on<WorkspaceSettingsEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: (userProfile, workspace) async {\n            _userService = UserBackendService(userId: userProfile.id);\n\n            try {\n              final currentWorkspace =\n                  await UserBackendService.getCurrentWorkspace().getOrThrow();\n\n              final workspaces =\n                  await _userService!.getWorkspaces().getOrThrow();\n              if (workspaces.isEmpty) {\n                workspaces.add(\n                  UserWorkspacePB.create()\n                    ..workspaceId = currentWorkspace.id\n                    ..name = currentWorkspace.name\n                    ..createdAtTimestamp = currentWorkspace.createTime,\n                );\n              }\n\n              final currentWorkspaceInList = workspaces.firstWhereOrNull(\n                    (e) => e.workspaceId == currentWorkspace.id,\n                  ) ??\n                  workspaces.firstOrNull;\n\n              // We emit here because the next event might take longer.\n              emit(state.copyWith(workspace: currentWorkspaceInList));\n\n              if (currentWorkspaceInList == null) {\n                return;\n              }\n\n              final members = await _getWorkspaceMembers(\n                currentWorkspaceInList.workspaceId,\n              );\n\n              emit(\n                state.copyWith(\n                  workspace: currentWorkspaceInList,\n                  members: members,\n                ),\n              );\n            } catch (e) {\n              Log.error('Failed to get or create current workspace');\n            }\n          },\n          updateWorkspaceName: (name) async {\n            final request = RenameWorkspacePB(\n              workspaceId: state.workspace?.workspaceId,\n              newName: name,\n            );\n            final result = await UserEventRenameWorkspace(request).send();\n\n            state.workspace!.freeze();\n            final update = state.workspace!.rebuild((p0) => p0.name = name);\n\n            result.fold(\n              (_) => emit(state.copyWith(workspace: update)),\n              (e) => Log.error('Failed to rename workspace: $e'),\n            );\n          },\n          updateWorkspaceIcon: (icon) async {\n            if (state.workspace == null) {\n              return null;\n            }\n\n            final request = ChangeWorkspaceIconPB()\n              ..workspaceId = state.workspace!.workspaceId\n              ..newIcon = icon;\n            final result = await UserEventChangeWorkspaceIcon(request).send();\n\n            result.fold(\n              (_) {\n                state.workspace!.freeze();\n                final newWorkspace =\n                    state.workspace!.rebuild((p0) => p0.icon = icon);\n\n                return emit(state.copyWith(workspace: newWorkspace));\n              },\n              (e) => Log.error('Failed to update workspace icon: $e'),\n            );\n          },\n          deleteWorkspace: () async =>\n              emit(state.copyWith(deleteWorkspace: true)),\n          leaveWorkspace: () async =>\n              emit(state.copyWith(leaveWorkspace: true)),\n        );\n      },\n    );\n  }\n\n  UserBackendService? _userService;\n\n  Future<List<WorkspaceMemberPB>> _getWorkspaceMembers(\n    String workspaceId,\n  ) async {\n    final data = QueryWorkspacePB()..workspaceId = workspaceId;\n    final result = await UserEventGetWorkspaceMembers(data).send();\n    return result.fold(\n      (s) => s.items,\n      (e) {\n        Log.error('Failed to read workspace members: $e');\n        return [];\n      },\n    );\n  }\n}\n\n@freezed\nclass WorkspaceSettingsEvent with _$WorkspaceSettingsEvent {\n  const factory WorkspaceSettingsEvent.initial({\n    required UserProfilePB userProfile,\n    @Default(null) UserWorkspacePB? workspace,\n  }) = Initial;\n\n  // Workspace itself\n  const factory WorkspaceSettingsEvent.updateWorkspaceName(String name) =\n      UpdateWorkspaceName;\n  const factory WorkspaceSettingsEvent.updateWorkspaceIcon(String icon) =\n      UpdateWorkspaceIcon;\n  const factory WorkspaceSettingsEvent.deleteWorkspace() = DeleteWorkspace;\n  const factory WorkspaceSettingsEvent.leaveWorkspace() = LeaveWorkspace;\n}\n\n@freezed\nclass WorkspaceSettingsState with _$WorkspaceSettingsState {\n  const factory WorkspaceSettingsState({\n    @Default(null) UserWorkspacePB? workspace,\n    @Default([]) List<WorkspaceMemberPB> members,\n    @Default(false) bool deleteWorkspace,\n    @Default(false) bool leaveWorkspace,\n  }) = _WorkspaceSettingsState;\n\n  factory WorkspaceSettingsState.initial() => const WorkspaceSettingsState();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/sidebar/billing/sidebar_plan_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/settings/file_storage/file_storage_listener.dart';\nimport 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart';\nimport 'package:appflowy/workspace/application/workspace/workspace_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/dispatch/error.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'sidebar_plan_bloc.freezed.dart';\n\nclass SidebarPlanBloc extends Bloc<SidebarPlanEvent, SidebarPlanState> {\n  SidebarPlanBloc() : super(const SidebarPlanState()) {\n    // 1. Listen to user subscription payment callback. After user client 'Open AppFlowy', this listenable will be triggered.\n    _subscriptionListener = getIt<SubscriptionSuccessListenable>();\n    _subscriptionListener.addListener(_onPaymentSuccessful);\n\n    // 2. Listen to the storage notification\n    _storageListener = StoreageNotificationListener(\n      onError: (error) {\n        if (!isClosed) {\n          add(SidebarPlanEvent.receiveError(error));\n        }\n      },\n    );\n\n    // 3. Listen to specific error codes\n    _globalErrorListener = GlobalErrorCodeNotifier.add(\n      onError: (error) {\n        if (!isClosed) {\n          add(SidebarPlanEvent.receiveError(error));\n        }\n      },\n      onErrorIf: (error) {\n        const relevantErrorCodes = {\n          ErrorCode.AIResponseLimitExceeded,\n          ErrorCode.FileStorageLimitExceeded,\n        };\n        return relevantErrorCodes.contains(error.code);\n      },\n    );\n\n    on<SidebarPlanEvent>(_handleEvent);\n  }\n\n  void _onPaymentSuccessful() {\n    final plan = _subscriptionListener.subscribedPlan;\n    Log.info(\"Subscription success listenable triggered: $plan\");\n\n    if (!isClosed) {\n      // Notify the user that they have switched to a new plan. It would be better if we use websocket to\n      // notify the client when plan switching.\n      if (state.workspaceId != null) {\n        final payload = SuccessWorkspaceSubscriptionPB(\n          workspaceId: state.workspaceId,\n        );\n\n        if (plan != null) {\n          payload.plan = plan;\n        }\n\n        UserEventNotifyDidSwitchPlan(payload).send().then((result) {\n          result.fold(\n            // After the user has switched to a new plan, we need to refresh the workspace usage.\n            (_) => _checkWorkspaceUsage(),\n            (error) => Log.error(\"NotifyDidSwitchPlan failed: $error\"),\n          );\n        });\n      } else {\n        Log.error(\n          \"Unexpected empty workspace id when subscription success listenable triggered. It should not happen. If happens, it must be a bug\",\n        );\n      }\n    }\n  }\n\n  Future<void> dispose() async {\n    if (_globalErrorListener != null) {\n      GlobalErrorCodeNotifier.remove(_globalErrorListener!);\n    }\n    _subscriptionListener.removeListener(_onPaymentSuccessful);\n    await _storageListener?.stop();\n    _storageListener = null;\n  }\n\n  ErrorListener? _globalErrorListener;\n  StoreageNotificationListener? _storageListener;\n  late final SubscriptionSuccessListenable _subscriptionListener;\n\n  Future<void> _handleEvent(\n    SidebarPlanEvent event,\n    Emitter<SidebarPlanState> emit,\n  ) async {\n    await event.when(\n      receiveError: (FlowyError error) async {\n        if (error.code == ErrorCode.AIResponseLimitExceeded) {\n          emit(\n            state.copyWith(\n              tierIndicator: const SidebarToastTierIndicator.aiMaxiLimitHit(),\n            ),\n          );\n        } else if (error.code == ErrorCode.FileStorageLimitExceeded) {\n          emit(\n            state.copyWith(\n              tierIndicator: const SidebarToastTierIndicator.storageLimitHit(),\n            ),\n          );\n        } else if (error.code == ErrorCode.SingleUploadLimitExceeded) {\n          emit(\n            state.copyWith(\n              tierIndicator:\n                  const SidebarToastTierIndicator.singleFileLimitHit(),\n            ),\n          );\n        } else {\n          Log.error(\"Unhandle Unexpected error: $error\");\n        }\n      },\n      init: (String workspaceId, UserProfilePB userProfile) {\n        emit(\n          state.copyWith(\n            workspaceId: workspaceId,\n            userProfile: userProfile,\n          ),\n        );\n\n        _checkWorkspaceUsage();\n      },\n      updateWorkspaceUsage: (WorkspaceUsagePB usage) {\n        // when the user's storage bytes are limited, show the upgrade tier button\n        if (!usage.storageBytesUnlimited) {\n          if (usage.storageBytes >= usage.storageBytesLimit) {\n            add(\n              const SidebarPlanEvent.updateTierIndicator(\n                SidebarToastTierIndicator.storageLimitHit(),\n              ),\n            );\n\n            /// Checks if the user needs to upgrade to the Pro Plan.\n            /// If the user needs to upgrade, it means they don't need to enable the AI max tier.\n            /// This function simply returns without performing any further actions.\n            return;\n          }\n        }\n\n        // when user's AI responses are limited, show the AI max tier button.\n        if (!usage.aiResponsesUnlimited) {\n          if (usage.aiResponsesCount >= usage.aiResponsesCountLimit) {\n            add(\n              const SidebarPlanEvent.updateTierIndicator(\n                SidebarToastTierIndicator.aiMaxiLimitHit(),\n              ),\n            );\n            return;\n          }\n        }\n\n        // hide the tier indicator\n        add(\n          const SidebarPlanEvent.updateTierIndicator(\n            SidebarToastTierIndicator.loading(),\n          ),\n        );\n      },\n      updateTierIndicator: (SidebarToastTierIndicator indicator) {\n        emit(\n          state.copyWith(\n            tierIndicator: indicator,\n          ),\n        );\n      },\n      changedWorkspace: (workspaceId) {\n        emit(state.copyWith(workspaceId: workspaceId));\n        _checkWorkspaceUsage();\n      },\n    );\n  }\n\n  Future<void> _checkWorkspaceUsage() async {\n    if (state.workspaceId == null || state.userProfile == null) {\n      return;\n    }\n\n    await WorkspaceService(\n      workspaceId: state.workspaceId!,\n      userId: state.userProfile!.id,\n    ).getWorkspaceUsage().then((result) {\n      result.fold(\n        (usage) {\n          if (!isClosed) {\n            // if the user cannot fetch the workspace usage,\n            // clear the tier indicator\n            if (usage == null) {\n              add(\n                const SidebarPlanEvent.updateTierIndicator(\n                  SidebarToastTierIndicator.loading(),\n                ),\n              );\n            } else {\n              add(SidebarPlanEvent.updateWorkspaceUsage(usage));\n            }\n          }\n        },\n        (error) => Log.error(\"Failed to get workspace usage: $error\"),\n      );\n    });\n  }\n}\n\n@freezed\nclass SidebarPlanEvent with _$SidebarPlanEvent {\n  const factory SidebarPlanEvent.init(\n    String workspaceId,\n    UserProfilePB userProfile,\n  ) = _Init;\n  const factory SidebarPlanEvent.updateWorkspaceUsage(\n    WorkspaceUsagePB usage,\n  ) = _UpdateWorkspaceUsage;\n  const factory SidebarPlanEvent.updateTierIndicator(\n    SidebarToastTierIndicator indicator,\n  ) = _UpdateTierIndicator;\n  const factory SidebarPlanEvent.receiveError(FlowyError error) = _ReceiveError;\n\n  const factory SidebarPlanEvent.changedWorkspace({\n    required String workspaceId,\n  }) = _ChangedWorkspace;\n}\n\n@freezed\nclass SidebarPlanState with _$SidebarPlanState {\n  const factory SidebarPlanState({\n    FlowyError? error,\n    UserProfilePB? userProfile,\n    String? workspaceId,\n    WorkspaceUsagePB? usage,\n    @Default(SidebarToastTierIndicator.loading())\n    SidebarToastTierIndicator tierIndicator,\n  }) = _SidebarPlanState;\n}\n\n@freezed\nclass SidebarToastTierIndicator with _$SidebarToastTierIndicator {\n  const factory SidebarToastTierIndicator.storageLimitHit() = _StorageLimitHit;\n  const factory SidebarToastTierIndicator.singleFileLimitHit() =\n      _SingleFileLimitHit;\n  const factory SidebarToastTierIndicator.aiMaxiLimitHit() = _aiMaxLimitHit;\n  const factory SidebarToastTierIndicator.loading() = _Loading;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'folder_bloc.freezed.dart';\n\nenum FolderSpaceType {\n  favorite,\n  private,\n  public,\n  unknown;\n\n  ViewSectionPB get toViewSectionPB {\n    switch (this) {\n      case FolderSpaceType.private:\n        return ViewSectionPB.Private;\n      case FolderSpaceType.public:\n        return ViewSectionPB.Public;\n      case FolderSpaceType.favorite:\n      case FolderSpaceType.unknown:\n        throw UnimplementedError();\n    }\n  }\n}\n\nclass FolderBloc extends Bloc<FolderEvent, FolderState> {\n  FolderBloc({\n    required FolderSpaceType type,\n  }) : super(FolderState.initial(type)) {\n    on<FolderEvent>((event, emit) async {\n      await event.map(\n        initial: (e) async {\n          // fetch the expand status\n          final isExpanded = await _getFolderExpandStatus();\n          emit(state.copyWith(isExpanded: isExpanded));\n        },\n        expandOrUnExpand: (e) async {\n          final isExpanded = e.isExpanded ?? !state.isExpanded;\n          await _setFolderExpandStatus(isExpanded);\n          emit(state.copyWith(isExpanded: isExpanded));\n        },\n      );\n    });\n  }\n\n  Future<void> _setFolderExpandStatus(bool isExpanded) async {\n    final result = await getIt<KeyValueStorage>().get(KVKeys.expandedViews);\n    var map = {};\n    if (result != null) {\n      map = jsonDecode(result);\n    }\n    if (isExpanded) {\n      // set expand status to true if it's not expanded\n      map[state.type.name] = true;\n    } else {\n      // remove the expand status if it's expanded\n      map.remove(state.type.name);\n    }\n    await getIt<KeyValueStorage>().set(KVKeys.expandedViews, jsonEncode(map));\n  }\n\n  Future<bool> _getFolderExpandStatus() async {\n    return getIt<KeyValueStorage>().get(KVKeys.expandedViews).then((result) {\n      if (result == null) {\n        return true;\n      }\n      final map = jsonDecode(result);\n      return map[state.type.name] ?? true;\n    });\n  }\n}\n\n@freezed\nclass FolderEvent with _$FolderEvent {\n  const factory FolderEvent.initial() = Initial;\n  const factory FolderEvent.expandOrUnExpand({\n    bool? isExpanded,\n  }) = ExpandOrUnExpand;\n}\n\n@freezed\nclass FolderState with _$FolderState {\n  const factory FolderState({\n    required FolderSpaceType type,\n    required bool isExpanded,\n  }) = _FolderState;\n\n  factory FolderState.initial(\n    FolderSpaceType type,\n  ) =>\n      FolderState(\n        type: type,\n        isExpanded: true,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/sidebar/rename_view/rename_view_bloc.dart",
    "content": "import 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'rename_view_bloc.freezed.dart';\n\nclass RenameViewBloc extends Bloc<RenameViewEvent, RenameViewState> {\n  RenameViewBloc(PopoverController controller)\n      : _controller = controller,\n        super(RenameViewState(controller: controller)) {\n    on<RenameViewEvent>((event, emit) {\n      event.when(\n        open: () => _controller.show(),\n      );\n    });\n  }\n\n  final PopoverController _controller;\n\n  @override\n  Future<void> close() async {\n    _controller.close();\n    await super.close();\n  }\n}\n\n@freezed\nclass RenameViewEvent with _$RenameViewEvent {\n  const factory RenameViewEvent.open() = _Open;\n}\n\n@freezed\nclass RenameViewState with _$RenameViewState {\n  const factory RenameViewState({\n    required PopoverController controller,\n  }) = _RenameViewState;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/shared/list_extension.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy/workspace/application/workspace/prelude.dart';\nimport 'package:appflowy/workspace/application/workspace/workspace_sections_listener.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'\n    hide AFRolePB;\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:protobuf/protobuf.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\npart 'space_bloc.freezed.dart';\n\nenum SpacePermission {\n  publicToAll,\n  private,\n}\n\nclass SidebarSection {\n  const SidebarSection({\n    required this.publicViews,\n    required this.privateViews,\n  });\n\n  const SidebarSection.empty()\n      : publicViews = const [],\n        privateViews = const [];\n\n  final List<ViewPB> publicViews;\n  final List<ViewPB> privateViews;\n\n  List<ViewPB> get views => publicViews + privateViews;\n\n  SidebarSection copyWith({\n    List<ViewPB>? publicViews,\n    List<ViewPB>? privateViews,\n  }) {\n    return SidebarSection(\n      publicViews: publicViews ?? this.publicViews,\n      privateViews: privateViews ?? this.privateViews,\n    );\n  }\n}\n\n/// The [SpaceBloc] is responsible for\n///   managing the root views in different sections of the workspace.\nclass SpaceBloc extends Bloc<SpaceEvent, SpaceState> {\n  SpaceBloc({\n    required this.userProfile,\n    required this.workspaceId,\n  }) : super(SpaceState.initial()) {\n    on<SpaceEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: (openFirstPage) async {\n            this.openFirstPage = openFirstPage;\n\n            _initial(userProfile, workspaceId);\n\n            final (spaces, publicViews, privateViews) = await _getSpaces();\n\n            final currentSpace = await _getLastOpenedSpace(spaces);\n            final isExpanded = await _getSpaceExpandStatus(currentSpace);\n            emit(\n              state.copyWith(\n                spaces: spaces,\n                currentSpace: currentSpace,\n                isExpanded: isExpanded,\n                shouldShowUpgradeDialog: false,\n                isInitialized: true,\n              ),\n            );\n\n            if (openFirstPage) {\n              if (currentSpace != null) {\n                if (!isClosed) {\n                  add(SpaceEvent.open(space: currentSpace));\n                }\n              }\n            }\n          },\n          create: (\n            name,\n            icon,\n            iconColor,\n            permission,\n            createNewPageByDefault,\n            openAfterCreate,\n          ) async {\n            final space = await _createSpace(\n              name: name,\n              icon: icon,\n              iconColor: iconColor,\n              permission: permission,\n            );\n\n            Log.info('create space: $space');\n\n            if (space != null) {\n              emit(\n                state.copyWith(\n                  spaces: [...state.spaces, space],\n                  currentSpace: space,\n                ),\n              );\n              add(SpaceEvent.open(space: space));\n              Log.info('open space: ${space.name}(${space.id})');\n\n              if (createNewPageByDefault) {\n                add(\n                  SpaceEvent.createPage(\n                    name: '',\n                    index: 0,\n                    layout: ViewLayoutPB.Document,\n                    openAfterCreate: openAfterCreate,\n                  ),\n                );\n                Log.info('create page: ${space.name}(${space.id})');\n              }\n            }\n          },\n          delete: (space) async {\n            if (state.spaces.length <= 1) {\n              return;\n            }\n\n            final deletedSpace = space ?? state.currentSpace;\n            if (deletedSpace == null) {\n              return;\n            }\n\n            await ViewBackendService.deleteView(viewId: deletedSpace.id);\n\n            Log.info('delete space: ${deletedSpace.name}(${deletedSpace.id})');\n          },\n          rename: (space, name) async {\n            add(\n              SpaceEvent.update(\n                space: space,\n                name: name,\n                icon: space.spaceIcon,\n                iconColor: space.spaceIconColor,\n                permission: space.spacePermission,\n              ),\n            );\n          },\n          changeIcon: (space, icon, iconColor) async {\n            add(\n              SpaceEvent.update(\n                space: space,\n                icon: icon,\n                iconColor: iconColor,\n              ),\n            );\n          },\n          update: (space, name, icon, iconColor, permission) async {\n            space ??= state.currentSpace;\n            if (space == null) {\n              Log.error('update space failed, space is null');\n              return;\n            }\n\n            if (name != null) {\n              await _rename(space, name);\n            }\n\n            if (icon != null || iconColor != null || permission != null) {\n              try {\n                final extra = space.extra;\n                final current = extra.isNotEmpty == true\n                    ? jsonDecode(extra)\n                    : <String, dynamic>{};\n                final updated = <String, dynamic>{};\n                if (icon != null) {\n                  updated[ViewExtKeys.spaceIconKey] = icon;\n                }\n                if (iconColor != null) {\n                  updated[ViewExtKeys.spaceIconColorKey] = iconColor;\n                }\n                if (permission != null) {\n                  updated[ViewExtKeys.spacePermissionKey] = permission.index;\n                }\n                final merged = mergeMaps(current, updated);\n                await ViewBackendService.updateView(\n                  viewId: space.id,\n                  extra: jsonEncode(merged),\n                );\n\n                Log.info(\n                  'update space: ${space.name}(${space.id}), merged: $merged',\n                );\n              } catch (e) {\n                Log.error('Failed to migrating cover: $e');\n              }\n            } else if (icon == null) {\n              try {\n                final extra = space.extra;\n                final Map<String, dynamic> current = extra.isNotEmpty == true\n                    ? jsonDecode(extra)\n                    : <String, dynamic>{};\n                current.remove(ViewExtKeys.spaceIconKey);\n                current.remove(ViewExtKeys.spaceIconColorKey);\n                await ViewBackendService.updateView(\n                  viewId: space.id,\n                  extra: jsonEncode(current),\n                );\n\n                Log.info(\n                  'update space: ${space.name}(${space.id}), current: $current',\n                );\n              } catch (e) {\n                Log.error('Failed to migrating cover: $e');\n              }\n            }\n\n            if (permission != null) {\n              await ViewBackendService.updateViewsVisibility(\n                [space],\n                permission == SpacePermission.publicToAll,\n              );\n            }\n          },\n          open: (space, afterOpen) async {\n            await _openSpace(space);\n            final isExpanded = await _getSpaceExpandStatus(space);\n            final views = await ViewBackendService.getChildViews(\n              viewId: space.id,\n            );\n            final currentSpace = views.fold(\n              (views) {\n                space.freeze();\n                return space.rebuild((b) {\n                  b.childViews.clear();\n                  b.childViews.addAll(views);\n                });\n              },\n              (_) => space,\n            );\n            emit(\n              state.copyWith(\n                currentSpace: currentSpace,\n                isExpanded: isExpanded,\n              ),\n            );\n\n            // don't open the page automatically on mobile\n            if (UniversalPlatform.isDesktop) {\n              // open the first page by default\n              if (currentSpace.childViews.isNotEmpty) {\n                final firstPage = currentSpace.childViews.first;\n                emit(\n                  state.copyWith(\n                    lastCreatedPage: firstPage,\n                  ),\n                );\n              } else {\n                emit(\n                  state.copyWith(\n                    lastCreatedPage: ViewPB(),\n                  ),\n                );\n              }\n            }\n             afterOpen?.call();\n          },\n          expand: (space, isExpanded) async {\n            await _setSpaceExpandStatus(space, isExpanded);\n            emit(state.copyWith(isExpanded: isExpanded));\n          },\n          createPage: (name, layout, index, openAfterCreate) async {\n            final parentViewId = state.currentSpace?.id;\n            if (parentViewId == null) {\n              return;\n            }\n\n            final result = await ViewBackendService.createView(\n              name: name,\n              layoutType: layout,\n              parentViewId: parentViewId,\n              index: index,\n              openAfterCreate: openAfterCreate,\n            );\n            result.fold(\n              (view) {\n                emit(\n                  state.copyWith(\n                    lastCreatedPage: openAfterCreate ? view : null,\n                    createPageResult: FlowyResult.success(null),\n                  ),\n                );\n              },\n              (error) {\n                Log.error('Failed to create root view: $error');\n                emit(\n                  state.copyWith(\n                    createPageResult: FlowyResult.failure(error),\n                  ),\n                );\n              },\n            );\n          },\n          didReceiveSpaceUpdate: () async {\n            final (spaces, _, _) = await _getSpaces();\n            final currentSpace = await _getLastOpenedSpace(spaces);\n\n            emit(\n              state.copyWith(\n                spaces: spaces,\n                currentSpace: currentSpace,\n              ),\n            );\n          },\n          reset: (userProfile, workspaceId, openFirstPage) async {\n            if (this.workspaceId == workspaceId) {\n              return;\n            }\n\n            _reset(userProfile, workspaceId);\n\n            add(\n              SpaceEvent.initial(\n                openFirstPage: openFirstPage,\n              ),\n            );\n          },\n          migrate: () async {\n            final result = await migrate();\n            emit(state.copyWith(shouldShowUpgradeDialog: !result));\n          },\n          switchToNextSpace: () async {\n            final spaces = state.spaces;\n            if (spaces.isEmpty) {\n              return;\n            }\n\n            final currentSpace = state.currentSpace;\n            if (currentSpace == null) {\n              return;\n            }\n            final currentIndex = spaces.indexOf(currentSpace);\n            final nextIndex = (currentIndex + 1) % spaces.length;\n            final nextSpace = spaces[nextIndex];\n            add(SpaceEvent.open(space: nextSpace));\n          },\n          duplicate: (space) async {\n            space ??= state.currentSpace;\n            if (space == null) {\n              Log.error('duplicate space failed, space is null');\n              return;\n            }\n\n            Log.info('duplicate space: ${space.name}(${space.id})');\n\n            emit(state.copyWith(isDuplicatingSpace: true));\n\n            final newSpace = await _duplicateSpace(space);\n            // open the duplicated space\n            if (newSpace != null) {\n              add(const SpaceEvent.didReceiveSpaceUpdate());\n              add(SpaceEvent.open(space: newSpace));\n            }\n\n            emit(state.copyWith(isDuplicatingSpace: false));\n          },\n        );\n      },\n    );\n  }\n\n  late WorkspaceService _workspaceService;\n  late String workspaceId;\n  late UserProfilePB userProfile;\n  WorkspaceSectionsListener? _listener;\n  bool openFirstPage = false;\n\n  @override\n  Future<void> close() async {\n    await _listener?.stop();\n    _listener = null;\n    return super.close();\n  }\n\n  Future<(List<ViewPB>, List<ViewPB>, List<ViewPB>)> _getSpaces() async {\n    final sectionViews = await _getSectionViews();\n    if (sectionViews == null || sectionViews.views.isEmpty) {\n      return (<ViewPB>[], <ViewPB>[], <ViewPB>[]);\n    }\n\n    final publicViews = sectionViews.publicViews.unique((e) => e.id);\n    final privateViews = sectionViews.privateViews.unique((e) => e.id);\n\n    final publicSpaces = publicViews.where((e) => e.isSpace);\n    final privateSpaces = privateViews.where((e) => e.isSpace);\n\n    return ([...publicSpaces, ...privateSpaces], publicViews, privateViews);\n  }\n\n  Future<ViewPB?> _createSpace({\n    required String name,\n    required String icon,\n    required String iconColor,\n    required SpacePermission permission,\n    String? viewId,\n  }) async {\n    final section = switch (permission) {\n      SpacePermission.publicToAll => ViewSectionPB.Public,\n      SpacePermission.private => ViewSectionPB.Private,\n    };\n\n    final extra = {\n      ViewExtKeys.isSpaceKey: true,\n      ViewExtKeys.spaceIconKey: icon,\n      ViewExtKeys.spaceIconColorKey: iconColor,\n      ViewExtKeys.spacePermissionKey: permission.index,\n      ViewExtKeys.spaceCreatedAtKey: DateTime.now().millisecondsSinceEpoch,\n    };\n    final result = await _workspaceService.createView(\n      name: name,\n      viewSection: section,\n      setAsCurrent: true,\n      viewId: viewId,\n      extra: jsonEncode(extra),\n    );\n    return await result.fold((space) async {\n      Log.info('Space created: $space');\n      return space;\n    }, (error) {\n      Log.error('Failed to create space: $error');\n      return null;\n    });\n  }\n\n  Future<ViewPB> _rename(ViewPB space, String name) async {\n    final result =\n        await ViewBackendService.updateView(viewId: space.id, name: name);\n    return result.fold((_) {\n      space.freeze();\n      return space.rebuild((b) => b.name = name);\n    }, (error) {\n      Log.error('Failed to rename space: $error');\n      return space;\n    });\n  }\n\n  Future<SidebarSection?> _getSectionViews() async {\n    try {\n      final publicViews = await _workspaceService.getPublicViews().getOrThrow();\n      final privateViews =\n          await _workspaceService.getPrivateViews().getOrThrow();\n      return SidebarSection(\n        publicViews: publicViews,\n        privateViews: privateViews,\n      );\n    } catch (e) {\n      Log.error('Failed to get section views: $e');\n      return null;\n    }\n  }\n\n  void _initial(UserProfilePB userProfile, String workspaceId) {\n    _workspaceService = WorkspaceService(\n      workspaceId: workspaceId,\n      userId: userProfile.id,\n    );\n\n    this.userProfile = userProfile;\n    this.workspaceId = workspaceId;\n\n    _listener = WorkspaceSectionsListener(\n      user: userProfile,\n      workspaceId: workspaceId,\n    )..start(\n        sectionChanged: (result) async {\n          if (isClosed) {\n            return;\n          }\n          add(const SpaceEvent.didReceiveSpaceUpdate());\n        },\n      );\n  }\n\n  void _reset(UserProfilePB userProfile, String workspaceId) {\n    _listener?.stop();\n    _listener = null;\n\n    this.userProfile = userProfile;\n    this.workspaceId = workspaceId;\n  }\n\n  Future<ViewPB?> _getLastOpenedSpace(List<ViewPB> spaces) async {\n    if (spaces.isEmpty) {\n      return null;\n    }\n\n    final spaceId =\n        await getIt<KeyValueStorage>().get(KVKeys.lastOpenedSpaceId);\n    if (spaceId == null) {\n      return spaces.first;\n    }\n\n    final space =\n        spaces.firstWhereOrNull((e) => e.id == spaceId) ?? spaces.first;\n    return space;\n  }\n\n  Future<void> _openSpace(ViewPB space) async {\n    await getIt<KeyValueStorage>().set(KVKeys.lastOpenedSpaceId, space.id);\n  }\n\n  Future<void> _setSpaceExpandStatus(ViewPB? space, bool isExpanded) async {\n    if (space == null) {\n      return;\n    }\n\n    final result = await getIt<KeyValueStorage>().get(KVKeys.expandedViews);\n    var map = {};\n    if (result != null) {\n      map = jsonDecode(result);\n    }\n    if (isExpanded) {\n      // set expand status to true if it's not expanded\n      map[space.id] = true;\n    } else {\n      // remove the expand status if it's expanded\n      map.remove(space.id);\n    }\n    await getIt<KeyValueStorage>().set(KVKeys.expandedViews, jsonEncode(map));\n  }\n\n  Future<bool> _getSpaceExpandStatus(ViewPB? space) async {\n    if (space == null) {\n      return true;\n    }\n\n    return getIt<KeyValueStorage>().get(KVKeys.expandedViews).then((result) {\n      if (result == null) {\n        return true;\n      }\n      final map = jsonDecode(result);\n      return map[space.id] ?? true;\n    });\n  }\n\n  Future<bool> migrate({bool auto = true}) async {\n    try {\n      final user =\n          await UserBackendService.getCurrentUserProfile().getOrThrow();\n      final service = UserBackendService(userId: user.id);\n      final members =\n          await service.getWorkspaceMembers(workspaceId).getOrThrow();\n      final isOwner = members.items\n          .any((e) => e.role == AFRolePB.Owner && e.email == user.email);\n\n      if (members.items.isEmpty) {\n        return true;\n      }\n\n      // only one member in the workspace, migrate it immediately\n      // only the owner can migrate the public space\n      if (members.items.length == 1 || isOwner) {\n        // create a new public space and a new private space\n        // move all the views in the workspace to the new public/private space\n        var publicViews = await _workspaceService.getPublicViews().getOrThrow();\n        final containsPublicSpace = publicViews.any(\n          (e) => e.isSpace && e.spacePermission == SpacePermission.publicToAll,\n        );\n        publicViews = publicViews.where((e) => !e.isSpace).toList();\n\n        for (final view in publicViews) {\n          Log.info(\n            'migrating: the public view should be migrated: ${view.name}(${view.id})',\n          );\n        }\n\n        // if there is already a public space, don't migrate the public space\n        // only migrate the public space if there are any public views\n        if (publicViews.isEmpty || containsPublicSpace) {\n          return true;\n        }\n\n        final viewId = fixedUuid(\n          user.id.toInt() + workspaceId.hashCode,\n          UuidType.publicSpace,\n        );\n        final publicSpace = await _createSpace(\n          name: 'Shared',\n          icon: builtInSpaceIcons.first,\n          iconColor: builtInSpaceColors.first,\n          permission: SpacePermission.publicToAll,\n          viewId: viewId,\n        );\n\n        Log.info('migrating: created a new public space: ${publicSpace?.id}');\n\n        if (publicSpace != null) {\n          for (final view in publicViews.reversed) {\n            if (view.isSpace) {\n              continue;\n            }\n            await ViewBackendService.moveViewV2(\n              viewId: view.id,\n              newParentId: publicSpace.id,\n              prevViewId: null,\n            );\n            Log.info(\n              'migrating: migrate ${view.name}(${view.id}) to public space(${publicSpace.id})',\n            );\n          }\n        }\n      }\n\n      // create a new private space\n      final viewId = fixedUuid(user.id.toInt(), UuidType.privateSpace);\n      var privateViews = await _workspaceService.getPrivateViews().getOrThrow();\n      // if there is already a private space, don't migrate the private space\n      final containsPrivateSpace = privateViews.any(\n        (e) => e.isSpace && e.spacePermission == SpacePermission.private,\n      );\n      privateViews = privateViews.where((e) => !e.isSpace).toList();\n\n      for (final view in privateViews) {\n        Log.info(\n          'migrating: the private view should be migrated: ${view.name}(${view.id})',\n        );\n      }\n\n      if (privateViews.isEmpty || containsPrivateSpace) {\n        return true;\n      }\n      // only migrate the private space if there are any private views\n      final privateSpace = await _createSpace(\n        name: 'Private',\n        icon: builtInSpaceIcons.last,\n        iconColor: builtInSpaceColors.last,\n        permission: SpacePermission.private,\n        viewId: viewId,\n      );\n      Log.info('migrating: created a new private space: ${privateSpace?.id}');\n\n      if (privateSpace != null) {\n        for (final view in privateViews.reversed) {\n          if (view.isSpace) {\n            continue;\n          }\n          await ViewBackendService.moveViewV2(\n            viewId: view.id,\n            newParentId: privateSpace.id,\n            prevViewId: null,\n          );\n          Log.info(\n            'migrating: migrate ${view.name}(${view.id}) to private space(${privateSpace.id})',\n          );\n        }\n      }\n\n      return true;\n    } catch (e) {\n      Log.error('migrate space error: $e');\n      return false;\n    }\n  }\n\n  Future<bool> shouldShowUpgradeDialog({\n    required List<ViewPB> spaces,\n    required List<ViewPB> publicViews,\n    required List<ViewPB> privateViews,\n  }) async {\n    final publicSpaces = spaces.where(\n      (e) => e.spacePermission == SpacePermission.publicToAll,\n    );\n    if (publicSpaces.isEmpty && publicViews.isNotEmpty) {\n      return true;\n    }\n\n    final privateSpaces = spaces.where(\n      (e) => e.spacePermission == SpacePermission.private,\n    );\n    if (privateSpaces.isEmpty && privateViews.isNotEmpty) {\n      return true;\n    }\n\n    return false;\n  }\n\n  Future<ViewPB?> _duplicateSpace(ViewPB space) async {\n    // if the space is not duplicated, try to create a new space\n    final icon = space.spaceIcon.orDefault(builtInSpaceIcons.first);\n    final iconColor = space.spaceIconColor.orDefault(builtInSpaceColors.first);\n    final newSpace = await _createSpace(\n      name: '${space.name} (copy)',\n      icon: icon,\n      iconColor: iconColor,\n      permission: space.spacePermission,\n    );\n\n    if (newSpace == null) {\n      return null;\n    }\n\n    for (final view in space.childViews) {\n      await ViewBackendService.duplicate(\n        view: view,\n        openAfterDuplicate: true,\n        syncAfterDuplicate: true,\n        includeChildren: true,\n        parentViewId: newSpace.id,\n        suffix: '',\n      );\n    }\n\n    Log.info('Space duplicated: $newSpace');\n\n    return newSpace;\n  }\n}\n\n@freezed\nclass SpaceEvent with _$SpaceEvent {\n  const factory SpaceEvent.initial({\n    required bool openFirstPage,\n  }) = _Initial;\n  const factory SpaceEvent.create({\n    required String name,\n    required String icon,\n    required String iconColor,\n    required SpacePermission permission,\n    required bool createNewPageByDefault,\n    required bool openAfterCreate,\n  }) = _Create;\n  const factory SpaceEvent.rename({\n    required ViewPB space,\n    required String name,\n  }) = _Rename;\n  const factory SpaceEvent.changeIcon({\n    ViewPB? space,\n    String? icon,\n    String? iconColor,\n  }) = _ChangeIcon;\n  const factory SpaceEvent.duplicate({\n    ViewPB? space,\n  }) = _Duplicate;\n  const factory SpaceEvent.update({\n    ViewPB? space,\n    String? name,\n    String? icon,\n    String? iconColor,\n    SpacePermission? permission,\n  }) = _Update;\n  const factory SpaceEvent.open({\n    required ViewPB space,\n    VoidCallback? afterOpen,\n  }) = _Open;\n  const factory SpaceEvent.expand(ViewPB space, bool isExpanded) = _Expand;\n  const factory SpaceEvent.createPage({\n    required String name,\n    required ViewLayoutPB layout,\n    int? index,\n    required bool openAfterCreate,\n  }) = _CreatePage;\n  const factory SpaceEvent.delete(ViewPB? space) = _Delete;\n  const factory SpaceEvent.didReceiveSpaceUpdate() = _DidReceiveSpaceUpdate;\n  const factory SpaceEvent.reset(\n    UserProfilePB userProfile,\n    String workspaceId,\n    bool openFirstPage,\n  ) = _Reset;\n  const factory SpaceEvent.migrate() = _Migrate;\n  const factory SpaceEvent.switchToNextSpace() = _SwitchToNextSpace;\n}\n\n@freezed\nclass SpaceState with _$SpaceState {\n  const factory SpaceState({\n    // use root view with space attributes to represent the space\n    @Default([]) List<ViewPB> spaces,\n    @Default(null) ViewPB? currentSpace,\n    @Default(true) bool isExpanded,\n    @Default(null) ViewPB? lastCreatedPage,\n    FlowyResult<void, FlowyError>? createPageResult,\n    @Default(false) bool shouldShowUpgradeDialog,\n    @Default(false) bool isDuplicatingSpace,\n    @Default(false) bool isInitialized,\n  }) = _SpaceState;\n\n  factory SpaceState.initial() => const SpaceState();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_search_bloc.dart",
    "content": "import 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'space_search_bloc.freezed.dart';\n\nclass SpaceSearchBloc extends Bloc<SpaceSearchEvent, SpaceSearchState> {\n  SpaceSearchBloc() : super(SpaceSearchState.initial()) {\n    on<SpaceSearchEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            _allViews = await ViewBackendService.getAllViews().fold(\n              (s) => s.items,\n              (_) => <ViewPB>[],\n            );\n          },\n          search: (query) {\n            if (query.isEmpty) {\n              emit(\n                state.copyWith(\n                  queryResults: null,\n                ),\n              );\n            } else {\n              final queryResults = _allViews.where(\n                (view) => view.name.toLowerCase().contains(query.toLowerCase()),\n              );\n              emit(\n                state.copyWith(\n                  queryResults: queryResults.toList(),\n                ),\n              );\n            }\n          },\n        );\n      },\n    );\n  }\n\n  late final List<ViewPB> _allViews;\n}\n\n@freezed\nclass SpaceSearchEvent with _$SpaceSearchEvent {\n  const factory SpaceSearchEvent.initial() = _Initial;\n  const factory SpaceSearchEvent.search(String query) = _Search;\n}\n\n@freezed\nclass SpaceSearchState with _$SpaceSearchState {\n  const factory SpaceSearchState({\n    List<ViewPB>? queryResults,\n  }) = _SpaceSearchState;\n\n  factory SpaceSearchState.initial() => const SpaceSearchState();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/subscription_success_listenable/subscription_success_listenable.dart",
    "content": "import 'package:appflowy_backend/log.dart';\nimport 'package:flutter/foundation.dart';\n\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\n\nclass SubscriptionSuccessListenable extends ChangeNotifier {\n  SubscriptionSuccessListenable();\n\n  String? _plan;\n\n  SubscriptionPlanPB? get subscribedPlan => switch (_plan) {\n        'free' => SubscriptionPlanPB.Free,\n        'pro' => SubscriptionPlanPB.Pro,\n        'team' => SubscriptionPlanPB.Team,\n        'ai_max' => SubscriptionPlanPB.AiMax,\n        'ai_local' => SubscriptionPlanPB.AiLocal,\n        _ => null,\n      };\n\n  void onPaymentSuccess(String? plan) {\n    Log.info(\"Payment success: $plan\");\n    _plan = plan;\n    notifyListeners();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/plugins/blank/blank.dart';\nimport 'package:appflowy/plugins/util.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/expand_views.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy/workspace/presentation/home/home_stack.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:collection/collection.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'tabs_bloc.freezed.dart';\n\nclass TabsBloc extends Bloc<TabsEvent, TabsState> {\n  TabsBloc() : super(TabsState()) {\n    menuSharedState = getIt<MenuSharedState>();\n    _dispatch();\n  }\n\n  late final MenuSharedState menuSharedState;\n\n  String? _lastOpenedPluginId;\n  String? _lastOpenedViewId;\n  DateTime? _lastOpenTime;\n  static const _deduplicationWindow = Duration(milliseconds: 500);\n\n  @override\n  Future<void> close() {\n    state.dispose();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<TabsEvent>(\n      (event, emit) async {\n        event.when(\n          selectTab: (int index) {\n            if (index != state.currentIndex &&\n                index >= 0 &&\n                index < state.pages) {\n              emit(state.copyWith(currentIndex: index));\n              _setLatestOpenView();\n            }\n          },\n          moveTab: () {},\n          closeTab: (String pluginId) {\n            final pm = state._pageManagers\n                .firstWhereOrNull((pm) => pm.plugin.id == pluginId);\n            if (pm?.isPinned == true) {\n              return;\n            }\n\n            emit(state.closeView(pluginId));\n            _setLatestOpenView();\n          },\n          closeCurrentTab: () {\n            if (state.currentPageManager.isPinned) {\n              return;\n            }\n\n            emit(state.closeView(state.currentPageManager.plugin.id));\n            _setLatestOpenView();\n          },\n          openTab: (Plugin plugin, ViewPB view) {\n            state.currentPageManager\n              ..hideSecondaryPlugin()\n              ..setSecondaryPlugin(BlankPagePlugin());\n            emit(state.openView(plugin));\n            _setLatestOpenView(view);\n          },\n          openPlugin: (Plugin plugin, ViewPB? view, bool setLatest) {\n            final now = DateTime.now();\n\n            // deduplicate. skip if same plugin and view were just opened\n            if (_lastOpenedPluginId == plugin.id &&\n                _lastOpenedViewId == view?.id &&\n                _lastOpenTime != null) {\n              final timeSinceLastOpen = now.difference(_lastOpenTime!);\n              if (timeSinceLastOpen < _deduplicationWindow) {\n                return;\n              }\n            }\n\n            _lastOpenedPluginId = plugin.id;\n            _lastOpenedViewId = view?.id;\n            _lastOpenTime = now;\n\n            state.currentPageManager\n              ..hideSecondaryPlugin()\n              ..setSecondaryPlugin(BlankPagePlugin());\n            emit(state.openPlugin(plugin: plugin, setLatest: setLatest));\n            if (setLatest) {\n              // the space view should be filtered out.\n              if (view != null && view.isSpace) {\n                return;\n              }\n              _setLatestOpenView(view);\n              if (view != null) _expandAncestors(view);\n            }\n          },\n          closeOtherTabs: (String pluginId) {\n            final pageManagers = [\n              ...state._pageManagers\n                  .where((pm) => pm.plugin.id == pluginId || pm.isPinned),\n            ];\n\n            int newIndex;\n            if (state.currentPageManager.isPinned) {\n              // Retain current index if it's already pinned\n              newIndex = state.currentIndex;\n            } else {\n              final pm = state._pageManagers\n                  .firstWhereOrNull((pm) => pm.plugin.id == pluginId);\n              newIndex = pm != null ? pageManagers.indexOf(pm) : 0;\n            }\n\n            emit(\n              state.copyWith(\n                currentIndex: newIndex,\n                pageManagers: pageManagers,\n              ),\n            );\n\n            _setLatestOpenView();\n          },\n          togglePin: (String pluginId) {\n            final pm = state._pageManagers\n                .firstWhereOrNull((pm) => pm.plugin.id == pluginId);\n            if (pm != null) {\n              final index = state._pageManagers.indexOf(pm);\n\n              int newIndex = state.currentIndex;\n              if (pm.isPinned) {\n                // Unpinning logic\n                final indexOfFirstUnpinnedTab =\n                    state._pageManagers.indexWhere((tab) => !tab.isPinned);\n\n                // Determine the correct insertion point\n                final newUnpinnedIndex = indexOfFirstUnpinnedTab != -1\n                    ? indexOfFirstUnpinnedTab // Insert before the first unpinned tab\n                    : state._pageManagers\n                        .length; // Append at the end if no unpinned tabs exist\n\n                state._pageManagers.removeAt(index);\n\n                final adjustedUnpinnedIndex = newUnpinnedIndex > index\n                    ? newUnpinnedIndex - 1\n                    : newUnpinnedIndex;\n\n                state._pageManagers.insert(adjustedUnpinnedIndex, pm);\n                newIndex = _adjustCurrentIndex(\n                  currentIndex: state.currentIndex,\n                  tabIndex: index,\n                  newIndex: adjustedUnpinnedIndex,\n                );\n              } else {\n                // Pinning logic\n                final indexOfLastPinnedTab =\n                    state._pageManagers.lastIndexWhere((tab) => tab.isPinned);\n                final newPinnedIndex = indexOfLastPinnedTab + 1;\n\n                state._pageManagers.removeAt(index);\n\n                final adjustedPinnedIndex = newPinnedIndex > index\n                    ? newPinnedIndex - 1\n                    : newPinnedIndex;\n\n                state._pageManagers.insert(adjustedPinnedIndex, pm);\n                newIndex = _adjustCurrentIndex(\n                  currentIndex: state.currentIndex,\n                  tabIndex: index,\n                  newIndex: adjustedPinnedIndex,\n                );\n              }\n\n              pm.isPinned = !pm.isPinned;\n\n              emit(\n                state.copyWith(\n                  currentIndex: newIndex,\n                  pageManagers: [...state._pageManagers],\n                ),\n              );\n            }\n          },\n          openSecondaryPlugin: (plugin, view) {\n            state.currentPageManager\n              ..setSecondaryPlugin(plugin)\n              ..showSecondaryPlugin();\n          },\n          closeSecondaryPlugin: () {\n            final pageManager = state.currentPageManager;\n            pageManager.hideSecondaryPlugin();\n          },\n          expandSecondaryPlugin: () {\n            final pageManager = state.currentPageManager;\n            pageManager\n              ..hideSecondaryPlugin()\n              ..expandSecondaryPlugin();\n            _setLatestOpenView();\n          },\n          switchWorkspace: (workspaceId) {\n            final pluginId = state.currentPageManager.plugin.id;\n\n            // Close all tabs except current\n            final pagesToClose = [\n              ...state._pageManagers\n                  .where((pm) => pm.plugin.id != pluginId && !pm.isPinned),\n            ];\n\n            if (pagesToClose.isNotEmpty) {\n              final newstate = state;\n              for (final pm in pagesToClose) {\n                newstate.closeView(pm.plugin.id);\n              }\n              emit(newstate.copyWith(currentIndex: 0));\n            }\n          },\n        );\n      },\n    );\n  }\n\n  void _setLatestOpenView([ViewPB? view]) {\n    if (view != null) {\n      menuSharedState.latestOpenView = view;\n    } else {\n      final pageManager = state.currentPageManager;\n      final notifier = pageManager.plugin.notifier;\n      if (notifier is ViewPluginNotifier &&\n          menuSharedState.latestOpenView?.id != notifier.view.id) {\n        menuSharedState.latestOpenView = notifier.view;\n      }\n    }\n  }\n\n  Future<void> _expandAncestors(ViewPB view) async {\n    final viewExpanderRegistry = getIt.get<ViewExpanderRegistry>();\n    if (viewExpanderRegistry.isViewExpanded(view.parentViewId)) return;\n    final value = await getIt<KeyValueStorage>().get(KVKeys.expandedViews);\n    try {\n      final Map expandedViews = value == null ? {} : jsonDecode(value);\n      final List<String> ancestors =\n          await ViewBackendService.getViewAncestors(view.id)\n              .fold((s) => s.items.map((e) => e.id).toList(), (f) => []);\n      ViewExpander? viewExpander;\n      for (final id in ancestors) {\n        expandedViews[id] = true;\n        final expander = viewExpanderRegistry.getExpander(id);\n        if (expander == null) continue;\n        if (!expander.isViewExpanded && viewExpander == null) {\n          viewExpander = expander;\n        }\n      }\n      await getIt<KeyValueStorage>()\n          .set(KVKeys.expandedViews, jsonEncode(expandedViews));\n      viewExpander?.expand();\n    } catch (e) {\n      Log.error('expandAncestors error', e);\n    }\n  }\n\n  int _adjustCurrentIndex({\n    required int currentIndex,\n    required int tabIndex,\n    required int newIndex,\n  }) {\n    if (tabIndex < currentIndex && newIndex >= currentIndex) {\n      return currentIndex - 1; // Tab moved forward, shift currentIndex back\n    } else if (tabIndex > currentIndex && newIndex <= currentIndex) {\n      return currentIndex + 1; // Tab moved backward, shift currentIndex forward\n    } else if (tabIndex == currentIndex) {\n      return newIndex; // Tab is the current tab, update to newIndex\n    }\n\n    return currentIndex;\n  }\n\n  /// Adds a [TabsEvent.openTab] event for the provided [ViewPB]\n  void openTab(ViewPB view) =>\n      add(TabsEvent.openTab(plugin: view.plugin(), view: view));\n\n  /// Adds a [TabsEvent.openPlugin] event for the provided [ViewPB]\n  void openPlugin(\n    ViewPB view, {\n    Map<String, dynamic> arguments = const {},\n  }) {\n    add(\n      TabsEvent.openPlugin(\n        plugin: view.plugin(arguments: arguments),\n        view: view,\n      ),\n    );\n  }\n}\n\n@freezed\nclass TabsEvent with _$TabsEvent {\n  const factory TabsEvent.moveTab() = _MoveTab;\n\n  const factory TabsEvent.closeTab(String pluginId) = _CloseTab;\n\n  const factory TabsEvent.closeOtherTabs(String pluginId) = _CloseOtherTabs;\n\n  const factory TabsEvent.closeCurrentTab() = _CloseCurrentTab;\n\n  const factory TabsEvent.selectTab(int index) = _SelectTab;\n\n  const factory TabsEvent.togglePin(String pluginId) = _TogglePin;\n\n  const factory TabsEvent.openTab({\n    required Plugin plugin,\n    required ViewPB view,\n  }) = _OpenTab;\n\n  const factory TabsEvent.openPlugin({\n    required Plugin plugin,\n    ViewPB? view,\n    @Default(true) bool setLatest,\n  }) = _OpenPlugin;\n\n  const factory TabsEvent.openSecondaryPlugin({\n    required Plugin plugin,\n    ViewPB? view,\n  }) = _OpenSecondaryPlugin;\n\n  const factory TabsEvent.closeSecondaryPlugin() = _CloseSecondaryPlugin;\n\n  const factory TabsEvent.expandSecondaryPlugin() = _ExpandSecondaryPlugin;\n\n  const factory TabsEvent.switchWorkspace(String workspaceId) =\n      _SwitchWorkspace;\n}\n\nclass TabsState {\n  TabsState({\n    this.currentIndex = 0,\n    List<PageManager>? pageManagers,\n  }) : _pageManagers = pageManagers ?? [PageManager()];\n\n  final int currentIndex;\n  final List<PageManager> _pageManagers;\n\n  int get pages => _pageManagers.length;\n\n  PageManager get currentPageManager => _pageManagers[currentIndex];\n\n  List<PageManager> get pageManagers => _pageManagers;\n\n  bool get isAllPinned => _pageManagers.every((pm) => pm.isPinned);\n\n  /// This opens a new tab given a [Plugin].\n  ///\n  /// If the [Plugin.id] is already associated with an open tab,\n  /// then it selects that tab.\n  ///\n  TabsState openView(Plugin plugin) {\n    final selectExistingPlugin = _selectPluginIfOpen(plugin.id);\n\n    if (selectExistingPlugin == null) {\n      _pageManagers.add(PageManager()..setPlugin(plugin, true));\n\n      return copyWith(\n        currentIndex: pages - 1,\n        pageManagers: [..._pageManagers],\n      );\n    }\n\n    return selectExistingPlugin;\n  }\n\n  TabsState closeView(String pluginId) {\n    // Avoid closing the only open tab\n    if (_pageManagers.length == 1) {\n      return this;\n    }\n\n    _pageManagers.removeWhere((pm) => pm.plugin.id == pluginId);\n\n    /// If currentIndex is greater than the amount of allowed indices\n    /// And the current selected tab isn't the first (index 0)\n    ///   as currentIndex cannot be -1\n    /// Then decrease currentIndex by 1\n    final newIndex = currentIndex > pages - 1 && currentIndex > 0\n        ? currentIndex - 1\n        : currentIndex;\n\n    return copyWith(\n      currentIndex: newIndex,\n      pageManagers: [..._pageManagers],\n    );\n  }\n\n  /// This opens a plugin in the current selected tab,\n  /// due to how Document currently works, only one tab\n  /// per plugin can currently be active.\n  ///\n  /// If the plugin is already open in a tab, then that tab\n  /// will become selected.\n  ///\n  TabsState openPlugin({required Plugin plugin, bool setLatest = true}) {\n    final selectExistingPlugin = _selectPluginIfOpen(plugin.id);\n\n    if (selectExistingPlugin == null) {\n      final pageManagers = [..._pageManagers];\n      pageManagers[currentIndex].setPlugin(plugin, setLatest);\n\n      return copyWith(pageManagers: pageManagers);\n    }\n\n    return selectExistingPlugin;\n  }\n\n  /// Checks if a [Plugin.id] is already associated with an open tab.\n  /// Returns a [TabState] with new index if there is a match.\n  ///\n  /// If no match it returns null\n  ///\n  TabsState? _selectPluginIfOpen(String id) {\n    final index = _pageManagers.indexWhere((pm) => pm.plugin.id == id);\n\n    if (index == -1) {\n      return null;\n    }\n\n    if (index == currentIndex) {\n      return this;\n    }\n\n    return copyWith(currentIndex: index);\n  }\n\n  TabsState copyWith({\n    int? currentIndex,\n    List<PageManager>? pageManagers,\n  }) =>\n      TabsState(\n        currentIndex: currentIndex ?? this.currentIndex,\n        pageManagers: pageManagers ?? _pageManagers,\n      );\n\n  void dispose() {\n    for (final manager in pageManagers) {\n      manager.dispose();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/user/prelude.dart",
    "content": "export 'settings_user_bloc.dart';\nexport 'user_workspace_bloc.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart",
    "content": "import 'package:appflowy/user/application/user_listener.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'settings_user_bloc.freezed.dart';\n\nclass SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {\n  SettingsUserViewBloc(this.userProfile)\n      : _userListener = UserListener(userProfile: userProfile),\n        _userService = UserBackendService(userId: userProfile.id),\n        super(SettingsUserState.initial(userProfile)) {\n    _dispatch();\n  }\n\n  final UserBackendService _userService;\n  final UserListener _userListener;\n  final UserProfilePB userProfile;\n\n  @override\n  Future<void> close() async {\n    await _userListener.stop();\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<SettingsUserEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            _loadUserProfile();\n            _userListener.start(onProfileUpdated: _profileUpdated);\n          },\n          didReceiveUserProfile: (UserProfilePB newUserProfile) {\n            emit(state.copyWith(userProfile: newUserProfile));\n          },\n          updateUserName: (String name) {\n            _userService.updateUserProfile(name: name).then((result) {\n              result.fold(\n                (l) => null,\n                (err) => Log.error(err),\n              );\n            });\n          },\n          updateUserIcon: (String iconUrl) {\n            _userService.updateUserProfile(iconUrl: iconUrl).then((result) {\n              result.fold(\n                (l) => null,\n                (err) => Log.error(err),\n              );\n            });\n          },\n          updateUserEmail: (String email) {\n            _userService.updateUserProfile(email: email).then((result) {\n              result.fold(\n                (l) => null,\n                (err) => Log.error(err),\n              );\n            });\n          },\n          updateUserPassword: (String oldPassword, String newPassword) {\n            _userService\n                .updateUserProfile(password: newPassword)\n                .then((result) {\n              result.fold(\n                (l) => null,\n                (err) => Log.error(err),\n              );\n            });\n          },\n          removeUserIcon: () {\n            // Empty Icon URL = No icon\n            _userService.updateUserProfile(iconUrl: \"\").then((result) {\n              result.fold(\n                (l) => null,\n                (err) => Log.error(err),\n              );\n            });\n          },\n        );\n      },\n    );\n  }\n\n  void _loadUserProfile() {\n    UserBackendService.getCurrentUserProfile().then((result) {\n      if (isClosed) {\n        return;\n      }\n\n      result.fold(\n        (userProfile) => add(\n          SettingsUserEvent.didReceiveUserProfile(userProfile),\n        ),\n        (err) => Log.error(err),\n      );\n    });\n  }\n\n  void _profileUpdated(\n    FlowyResult<UserProfilePB, FlowyError> userProfileOrFailed,\n  ) =>\n      userProfileOrFailed.fold(\n        (newUserProfile) =>\n            add(SettingsUserEvent.didReceiveUserProfile(newUserProfile)),\n        (err) => Log.error(err),\n      );\n}\n\n@freezed\nclass SettingsUserEvent with _$SettingsUserEvent {\n  const factory SettingsUserEvent.initial() = _Initial;\n  const factory SettingsUserEvent.updateUserName({\n    required String name,\n  }) = _UpdateUserName;\n  const factory SettingsUserEvent.updateUserEmail({\n    required String email,\n  }) = _UpdateEmail;\n  const factory SettingsUserEvent.updateUserIcon({\n    required String iconUrl,\n  }) = _UpdateUserIcon;\n  const factory SettingsUserEvent.updateUserPassword({\n    required String oldPassword,\n    required String newPassword,\n  }) = _UpdateUserPassword;\n  const factory SettingsUserEvent.removeUserIcon() = _RemoveUserIcon;\n  const factory SettingsUserEvent.didReceiveUserProfile(\n    UserProfilePB newUserProfile,\n  ) = _DidReceiveUserProfile;\n}\n\n@freezed\nclass SettingsUserState with _$SettingsUserState {\n  const factory SettingsUserState({\n    required UserProfilePB userProfile,\n    required FlowyResult<void, String> successOrFailure,\n  }) = _SettingsUserState;\n\n  factory SettingsUserState.initial(UserProfilePB userProfile) =>\n      SettingsUserState(\n        userProfile: userProfile,\n        successOrFailure: FlowyResult.success(null),\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart",
    "content": "export 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/view/prelude.dart",
    "content": "export 'view_bloc.dart';\nexport 'view_listener.dart';\nexport 'view_service.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/expand_views.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_listener.dart';\nimport 'package:appflowy/workspace/application/recent/cached_recent_service.dart';\nimport 'package:appflowy/workspace/application/view/view_listener.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:protobuf/protobuf.dart';\n\npart 'view_bloc.freezed.dart';\n\nclass ViewBloc extends Bloc<ViewEvent, ViewState> {\n  ViewBloc({\n    required this.view,\n    this.shouldLoadChildViews = true,\n    this.engagedInExpanding = false,\n  })  : viewBackendSvc = ViewBackendService(),\n        listener = ViewListener(viewId: view.id),\n        favoriteListener = FavoriteListener(),\n        super(ViewState.init(view)) {\n    _dispatch();\n    if (engagedInExpanding) {\n      expander = ViewExpander(\n        () => state.isExpanded,\n        () => add(const ViewEvent.setIsExpanded(true)),\n      );\n      getIt<ViewExpanderRegistry>().register(view.id, expander);\n    }\n  }\n\n  final ViewPB view;\n  final ViewBackendService viewBackendSvc;\n  final ViewListener listener;\n  final FavoriteListener favoriteListener;\n  final bool shouldLoadChildViews;\n  final bool engagedInExpanding;\n  late ViewExpander expander;\n\n  @override\n  Future<void> close() async {\n    await listener.stop();\n    await favoriteListener.stop();\n    if (engagedInExpanding) {\n      getIt<ViewExpanderRegistry>().unregister(view.id, expander);\n    }\n    return super.close();\n  }\n\n  void _dispatch() {\n    on<ViewEvent>(\n      (event, emit) async {\n        await event.map(\n          initial: (e) async {\n            listener.start(\n              onViewUpdated: (result) {\n                add(ViewEvent.viewDidUpdate(FlowyResult.success(result)));\n              },\n              onViewChildViewsUpdated: (result) async {\n                final view = await _updateChildViews(result);\n                if (!isClosed && view != null) {\n                  add(ViewEvent.viewUpdateChildView(view));\n                }\n              },\n            );\n            favoriteListener.start(\n              favoritesUpdated: (result, isFavorite) {\n                result.fold(\n                  (result) {\n                    final current = result.items\n                        .firstWhereOrNull((v) => v.id == state.view.id);\n                    if (current != null) {\n                      add(\n                        ViewEvent.viewDidUpdate(\n                          FlowyResult.success(current),\n                        ),\n                      );\n                    }\n                  },\n                  (error) {},\n                );\n              },\n            );\n            final isExpanded = await _getViewIsExpanded(view);\n            emit(state.copyWith(isExpanded: isExpanded, view: view));\n            if (shouldLoadChildViews) {\n              await _loadChildViews(emit);\n            }\n          },\n          setIsEditing: (e) {\n            emit(state.copyWith(isEditing: e.isEditing));\n          },\n          setIsExpanded: (e) async {\n            if (e.isExpanded && !state.isExpanded) {\n              await _loadViewsWhenExpanded(emit, true);\n            } else {\n              emit(state.copyWith(isExpanded: e.isExpanded));\n            }\n            await _setViewIsExpanded(view, e.isExpanded);\n          },\n          viewDidUpdate: (e) async {\n            final result = await ViewBackendService.getView(view.id);\n            final view_ = result.fold((l) => l, (r) => null);\n            e.result.fold(\n              (view) async {\n                // ignore child view changes because it only contains one level\n                // children data.\n                if (_isSameViewIgnoreChildren(view, state.view)) {\n                  // do nothing.\n                }\n                emit(\n                  state.copyWith(\n                    view: view_ ?? view,\n                    successOrFailure: FlowyResult.success(null),\n                  ),\n                );\n              },\n              (error) => emit(\n                state.copyWith(successOrFailure: FlowyResult.failure(error)),\n              ),\n            );\n          },\n          rename: (e) async {\n            final result = await ViewBackendService.updateView(\n              viewId: view.id,\n              name: e.newName,\n            );\n            emit(\n              result.fold(\n                (l) {\n                  final view = state.view;\n                  view.freeze();\n                  final newView = view.rebuild(\n                    (b) => b.name = e.newName,\n                  );\n                  Log.info('rename view: ${newView.id} to ${newView.name}');\n                  return state.copyWith(\n                    successOrFailure: FlowyResult.success(null),\n                    view: newView,\n                  );\n                },\n                (error) {\n                  Log.error('rename view failed: $error');\n                  return state.copyWith(\n                    successOrFailure: FlowyResult.failure(error),\n                  );\n                },\n              ),\n            );\n          },\n          delete: (e) async {\n            // unpublish the page and all its child pages if they are published\n            await _unpublishPage(view);\n\n            final result = await ViewBackendService.deleteView(viewId: view.id);\n\n            emit(\n              result.fold(\n                (l) {\n                  return state.copyWith(\n                    successOrFailure: FlowyResult.success(null),\n                    isDeleted: true,\n                  );\n                },\n                (error) => state.copyWith(\n                  successOrFailure: FlowyResult.failure(error),\n                ),\n              ),\n            );\n            await getIt<CachedRecentService>().updateRecentViews(\n              [view.id],\n              false,\n            );\n          },\n          duplicate: (e) async {\n            final result = await ViewBackendService.duplicate(\n              view: view,\n              openAfterDuplicate: true,\n              syncAfterDuplicate: true,\n              includeChildren: true,\n              suffix: ' (${LocaleKeys.menuAppHeader_pageNameSuffix.tr()})',\n            );\n            emit(\n              result.fold(\n                (l) =>\n                    state.copyWith(successOrFailure: FlowyResult.success(null)),\n                (error) => state.copyWith(\n                  successOrFailure: FlowyResult.failure(error),\n                ),\n              ),\n            );\n          },\n          move: (value) async {\n            final result = await ViewBackendService.moveViewV2(\n              viewId: value.from.id,\n              newParentId: value.newParentId,\n              prevViewId: value.prevId,\n              fromSection: value.fromSection,\n              toSection: value.toSection,\n            );\n            emit(\n              result.fold(\n                (l) {\n                  return state.copyWith(\n                    successOrFailure: FlowyResult.success(null),\n                  );\n                },\n                (error) => state.copyWith(\n                  successOrFailure: FlowyResult.failure(error),\n                ),\n              ),\n            );\n          },\n          createView: (e) async {\n            final result = await ViewBackendService.createView(\n              parentViewId: view.id,\n              name: e.name,\n              layoutType: e.layoutType,\n              ext: {},\n              openAfterCreate: e.openAfterCreated,\n              section: e.section,\n            );\n            emit(\n              result.fold(\n                (view) => state.copyWith(\n                  lastCreatedView: view,\n                  successOrFailure: FlowyResult.success(null),\n                ),\n                (error) => state.copyWith(\n                  successOrFailure: FlowyResult.failure(error),\n                ),\n              ),\n            );\n          },\n          viewUpdateChildView: (e) async {\n            emit(\n              state.copyWith(\n                view: e.result,\n              ),\n            );\n          },\n          updateViewVisibility: (value) async {\n            final view = value.view;\n            await ViewBackendService.updateViewsVisibility(\n              [view],\n              value.isPublic,\n            );\n          },\n          updateIcon: (value) async {\n            await ViewBackendService.updateViewIcon(\n              view: view,\n              viewIcon: view.icon.toEmojiIconData(),\n            );\n          },\n          collapseAllPages: (value) async {\n            for (final childView in view.childViews) {\n              await _setViewIsExpanded(childView, false);\n            }\n            add(const ViewEvent.setIsExpanded(false));\n          },\n          unpublish: (value) async {\n            if (value.sync) {\n              await _unpublishPage(view);\n            } else {\n              unawaited(_unpublishPage(view));\n            }\n          },\n        );\n      },\n    );\n  }\n\n  Future<void> _loadViewsWhenExpanded(\n    Emitter<ViewState> emit,\n    bool isExpanded,\n  ) async {\n    if (!isExpanded) {\n      emit(\n        state.copyWith(\n          view: view,\n          isExpanded: false,\n          isLoading: false,\n        ),\n      );\n      return;\n    }\n\n    final viewsOrFailed =\n        await ViewBackendService.getChildViews(viewId: state.view.id);\n\n    viewsOrFailed.fold(\n      (childViews) {\n        state.view.freeze();\n        final viewWithChildViews = state.view.rebuild((b) {\n          b.childViews.clear();\n          b.childViews.addAll(childViews);\n        });\n        emit(\n          state.copyWith(\n            view: viewWithChildViews,\n            isExpanded: true,\n            isLoading: false,\n          ),\n        );\n      },\n      (error) => emit(\n        state.copyWith(\n          successOrFailure: FlowyResult.failure(error),\n          isExpanded: true,\n          isLoading: false,\n        ),\n      ),\n    );\n  }\n\n  Future<void> _loadChildViews(\n    Emitter<ViewState> emit,\n  ) async {\n    final viewsOrFailed =\n        await ViewBackendService.getChildViews(viewId: state.view.id);\n\n    viewsOrFailed.fold(\n      (childViews) {\n        state.view.freeze();\n        final viewWithChildViews = state.view.rebuild((b) {\n          b.childViews.clear();\n          b.childViews.addAll(childViews);\n        });\n        emit(\n          state.copyWith(\n            view: viewWithChildViews,\n          ),\n        );\n      },\n      (error) => emit(\n        state.copyWith(\n          successOrFailure: FlowyResult.failure(error),\n        ),\n      ),\n    );\n  }\n\n  Future<void> _setViewIsExpanded(ViewPB view, bool isExpanded) async {\n    final result = await getIt<KeyValueStorage>().get(KVKeys.expandedViews);\n    final Map map;\n    if (result != null) {\n      map = jsonDecode(result);\n    } else {\n      map = {};\n    }\n    if (isExpanded) {\n      map[view.id] = true;\n    } else {\n      map.remove(view.id);\n    }\n    await getIt<KeyValueStorage>().set(KVKeys.expandedViews, jsonEncode(map));\n  }\n\n  Future<bool> _getViewIsExpanded(ViewPB view) {\n    return getIt<KeyValueStorage>().get(KVKeys.expandedViews).then((result) {\n      if (result == null) {\n        return false;\n      }\n      final map = jsonDecode(result);\n      return map[view.id] ?? false;\n    });\n  }\n\n  Future<ViewPB?> _updateChildViews(\n    ChildViewUpdatePB update,\n  ) async {\n    if (update.createChildViews.isNotEmpty) {\n      // refresh the child views if the update isn't empty\n      // because there's no info to get the inserted index.\n      assert(update.parentViewId == this.view.id);\n      final view = await ViewBackendService.getView(\n        update.parentViewId,\n      );\n      return view.fold((l) => l, (r) => null);\n    }\n\n    final view = state.view;\n    view.freeze();\n    final childViews = [...view.childViews];\n    if (update.deleteChildViews.isNotEmpty) {\n      childViews.removeWhere((v) => update.deleteChildViews.contains(v.id));\n      return view.rebuild((p0) {\n        p0.childViews.clear();\n        p0.childViews.addAll(childViews);\n      });\n    }\n\n    if (update.updateChildViews.isNotEmpty && update.parentViewId.isNotEmpty) {\n      final view = await ViewBackendService.getView(update.parentViewId);\n      final childViews = view.fold((l) => l.childViews, (r) => []);\n      bool isSameOrder = true;\n      if (childViews.length == update.updateChildViews.length) {\n        for (var i = 0; i < childViews.length; i++) {\n          if (childViews[i].id != update.updateChildViews[i].id) {\n            isSameOrder = false;\n            break;\n          }\n        }\n      } else {\n        isSameOrder = false;\n      }\n      if (!isSameOrder) {\n        return view.fold((l) => l, (r) => null);\n      }\n    }\n\n    return null;\n  }\n\n  // unpublish the page and all its child pages\n  Future<void> _unpublishPage(ViewPB views) async {\n    final (_, publishedPages) = await ViewBackendService.containPublishedPage(\n      view,\n    );\n\n    await Future.wait(\n      publishedPages.map((view) async {\n        Log.info('unpublishing page: ${view.id}, ${view.name}');\n        await ViewBackendService.unpublish(view);\n      }),\n    );\n  }\n\n  bool _isSameViewIgnoreChildren(ViewPB from, ViewPB to) {\n    return _hash(from) == _hash(to);\n  }\n\n  int _hash(ViewPB view) => Object.hash(\n        view.id,\n        view.name,\n        view.createTime,\n        view.icon,\n        view.parentViewId,\n        view.layout,\n      );\n}\n\n@freezed\nclass ViewEvent with _$ViewEvent {\n  const factory ViewEvent.initial() = Initial;\n\n  const factory ViewEvent.setIsEditing(bool isEditing) = SetEditing;\n\n  const factory ViewEvent.setIsExpanded(bool isExpanded) = SetIsExpanded;\n\n  const factory ViewEvent.rename(String newName) = Rename;\n\n  const factory ViewEvent.delete() = Delete;\n\n  const factory ViewEvent.duplicate() = Duplicate;\n\n  const factory ViewEvent.move(\n    ViewPB from,\n    String newParentId,\n    String? prevId,\n    ViewSectionPB? fromSection,\n    ViewSectionPB? toSection,\n  ) = Move;\n\n  const factory ViewEvent.createView(\n    String name,\n    ViewLayoutPB layoutType, {\n    /// open the view after created\n    @Default(true) bool openAfterCreated,\n    ViewSectionPB? section,\n  }) = CreateView;\n\n  const factory ViewEvent.viewDidUpdate(\n    FlowyResult<ViewPB, FlowyError> result,\n  ) = ViewDidUpdate;\n\n  const factory ViewEvent.viewUpdateChildView(ViewPB result) =\n      ViewUpdateChildView;\n\n  const factory ViewEvent.updateViewVisibility(\n    ViewPB view,\n    bool isPublic,\n  ) = UpdateViewVisibility;\n\n  const factory ViewEvent.updateIcon(String? icon) = UpdateIcon;\n\n  const factory ViewEvent.collapseAllPages() = CollapseAllPages;\n\n  // this event will unpublish the page and all its child pages if they are published\n  const factory ViewEvent.unpublish({required bool sync}) = Unpublish;\n}\n\n@freezed\nclass ViewState with _$ViewState {\n  const factory ViewState({\n    required ViewPB view,\n    required bool isEditing,\n    required bool isExpanded,\n    required FlowyResult<void, FlowyError> successOrFailure,\n    @Default(false) bool isDeleted,\n    @Default(true) bool isLoading,\n    @Default(null) ViewPB? lastCreatedView,\n  }) = _ViewState;\n\n  factory ViewState.init(ViewPB view) => ViewState(\n        view: view,\n        isExpanded: false,\n        isEditing: false,\n        successOrFailure: FlowyResult.success(null),\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/plugins/ai_chat/chat.dart';\nimport 'package:appflowy/plugins/database/board/presentation/board_page.dart';\nimport 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/mobile_grid_page.dart';\nimport 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';\nimport 'package:appflowy/plugins/document/document.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nclass PluginArgumentKeys {\n  static String selection = \"selection\";\n  static String rowId = \"row_id\";\n  static String blockId = \"block_id\";\n}\n\nclass ViewExtKeys {\n  // used for customizing the font family.\n  static String fontKey = 'font';\n\n  // used for customizing the font layout.\n  static String fontLayoutKey = 'font_layout';\n\n  // used for customizing the line height layout.\n  static String lineHeightLayoutKey = 'line_height_layout';\n\n  // cover keys\n  static String coverKey = 'cover';\n  static String coverTypeKey = 'type';\n  static String coverValueKey = 'value';\n\n  // is pinned\n  static String isPinnedKey = 'is_pinned';\n\n  // space\n  static String isSpaceKey = 'is_space';\n  static String spaceCreatorKey = 'space_creator';\n  static String spaceCreatedAtKey = 'space_created_at';\n  static String spaceIconKey = 'space_icon';\n  static String spaceIconColorKey = 'space_icon_color';\n  static String spacePermissionKey = 'space_permission';\n}\n\nextension MinimalViewExtension on FolderViewMinimalPB {\n  Widget defaultIcon({Size? size}) => FlowySvg(\n        switch (layout) {\n          ViewLayoutPB.Board => FlowySvgs.icon_board_s,\n          ViewLayoutPB.Calendar => FlowySvgs.icon_calendar_s,\n          ViewLayoutPB.Grid => FlowySvgs.icon_grid_s,\n          ViewLayoutPB.Document => FlowySvgs.icon_document_s,\n          ViewLayoutPB.Chat => FlowySvgs.chat_ai_page_s,\n          _ => FlowySvgs.icon_document_s,\n        },\n        size: size,\n      );\n}\n\nextension ViewExtension on ViewPB {\n  String get nameOrDefault =>\n      name.isEmpty ? LocaleKeys.menuAppHeader_defaultNewPageName.tr() : name;\n\n  bool get isDocument => pluginType == PluginType.document;\n  bool get isDatabase => [\n        PluginType.grid,\n        PluginType.board,\n        PluginType.calendar,\n      ].contains(pluginType);\n\n  Widget defaultIcon({Size? size}) => FlowySvg(\n        switch (layout) {\n          ViewLayoutPB.Board => FlowySvgs.icon_board_s,\n          ViewLayoutPB.Calendar => FlowySvgs.icon_calendar_s,\n          ViewLayoutPB.Grid => FlowySvgs.icon_grid_s,\n          ViewLayoutPB.Document => FlowySvgs.icon_document_s,\n          ViewLayoutPB.Chat => FlowySvgs.chat_ai_page_s,\n          _ => FlowySvgs.icon_document_s,\n        },\n        size: size,\n      );\n\n  PluginType get pluginType => switch (layout) {\n        ViewLayoutPB.Board => PluginType.board,\n        ViewLayoutPB.Calendar => PluginType.calendar,\n        ViewLayoutPB.Document => PluginType.document,\n        ViewLayoutPB.Grid => PluginType.grid,\n        ViewLayoutPB.Chat => PluginType.chat,\n        _ => throw UnimplementedError(),\n      };\n\n  Plugin plugin({\n    Map<String, dynamic> arguments = const {},\n  }) {\n    switch (layout) {\n      case ViewLayoutPB.Board:\n      case ViewLayoutPB.Calendar:\n      case ViewLayoutPB.Grid:\n        final String? rowId = arguments[PluginArgumentKeys.rowId];\n\n        return DatabaseTabBarViewPlugin(\n          view: this,\n          pluginType: pluginType,\n          initialRowId: rowId,\n        );\n      case ViewLayoutPB.Document:\n        final selectionValue = arguments[PluginArgumentKeys.selection];\n        Selection? initialSelection;\n        if (selectionValue is Selection) initialSelection = selectionValue;\n\n        final String? initialBlockId = arguments[PluginArgumentKeys.blockId];\n\n        return DocumentPlugin(\n          view: this,\n          pluginType: pluginType,\n          initialSelection: initialSelection,\n          initialBlockId: initialBlockId,\n        );\n      case ViewLayoutPB.Chat:\n        return AIChatPagePlugin(view: this);\n    }\n    throw UnimplementedError;\n  }\n\n  DatabaseTabBarItemBuilder tabBarItem() => switch (layout) {\n        ViewLayoutPB.Board => BoardPageTabBarBuilderImpl(),\n        ViewLayoutPB.Calendar => CalendarPageTabBarBuilderImpl(),\n        ViewLayoutPB.Grid => DesktopGridTabBarBuilderImpl(),\n        _ => throw UnimplementedError,\n      };\n\n  DatabaseTabBarItemBuilder mobileTabBarItem() => switch (layout) {\n        ViewLayoutPB.Board => BoardPageTabBarBuilderImpl(),\n        ViewLayoutPB.Calendar => CalendarPageTabBarBuilderImpl(),\n        ViewLayoutPB.Grid => MobileGridTabBarBuilderImpl(),\n        _ => throw UnimplementedError,\n      };\n\n  FlowySvgData get iconData => layout.icon;\n\n  bool get isSpace {\n    try {\n      if (extra.isEmpty) {\n        return false;\n      }\n\n      final ext = jsonDecode(extra);\n      final isSpace = ext[ViewExtKeys.isSpaceKey] ?? false;\n      return isSpace;\n    } catch (e) {\n      return false;\n    }\n  }\n\n  SpacePermission get spacePermission {\n    try {\n      final ext = jsonDecode(extra);\n      final permission = ext[ViewExtKeys.spacePermissionKey] ?? 1;\n      return SpacePermission.values[permission];\n    } catch (e) {\n      return SpacePermission.private;\n    }\n  }\n\n  FlowySvg? buildSpaceIconSvg(BuildContext context, {Size? size}) {\n    try {\n      if (extra.isEmpty) {\n        return null;\n      }\n\n      final ext = jsonDecode(extra);\n      final icon = ext[ViewExtKeys.spaceIconKey];\n      final color = ext[ViewExtKeys.spaceIconColorKey];\n      if (icon == null || color == null) {\n        return null;\n      }\n      // before version 0.6.7\n      if (icon.contains('space_icon')) {\n        return FlowySvg(\n          FlowySvgData('assets/flowy_icons/16x/$icon.svg'),\n          color: Theme.of(context).colorScheme.surface,\n        );\n      }\n\n      final values = icon.split('/');\n      if (values.length != 2) {\n        return null;\n      }\n      final groupName = values[0];\n      final iconName = values[1];\n      final svgString = kIconGroups\n          ?.firstWhereOrNull(\n            (group) => group.name == groupName,\n          )\n          ?.icons\n          .firstWhereOrNull(\n            (icon) => icon.name == iconName,\n          )\n          ?.content;\n      if (svgString == null) {\n        return null;\n      }\n      return FlowySvg.string(\n        svgString,\n        color: Theme.of(context).colorScheme.surface,\n        size: size,\n      );\n    } catch (e) {\n      return null;\n    }\n  }\n\n  String? get spaceIcon {\n    try {\n      final ext = jsonDecode(extra);\n      final icon = ext[ViewExtKeys.spaceIconKey];\n      return icon;\n    } catch (e) {\n      return null;\n    }\n  }\n\n  String? get spaceIconColor {\n    try {\n      final ext = jsonDecode(extra);\n      final color = ext[ViewExtKeys.spaceIconColorKey];\n      return color;\n    } catch (e) {\n      return null;\n    }\n  }\n\n  bool get isPinned {\n    try {\n      final ext = jsonDecode(extra);\n      final isPinned = ext[ViewExtKeys.isPinnedKey] ?? false;\n      return isPinned;\n    } catch (e) {\n      return false;\n    }\n  }\n\n  PageStyleCover? get cover {\n    if (layout != ViewLayoutPB.Document) {\n      return null;\n    }\n\n    if (extra.isEmpty) {\n      return null;\n    }\n\n    try {\n      final ext = jsonDecode(extra);\n      final cover = ext[ViewExtKeys.coverKey] ?? {};\n      final coverType = cover[ViewExtKeys.coverTypeKey] ??\n          PageStyleCoverImageType.none.toString();\n      final coverValue = cover[ViewExtKeys.coverValueKey] ?? '';\n      return PageStyleCover(\n        type: PageStyleCoverImageType.fromString(coverType),\n        value: coverValue,\n      );\n    } catch (e) {\n      return null;\n    }\n  }\n\n  PageStyleLineHeightLayout get lineHeightLayout {\n    if (layout != ViewLayoutPB.Document) {\n      return PageStyleLineHeightLayout.normal;\n    }\n    try {\n      final ext = jsonDecode(extra);\n      final lineHeight = ext[ViewExtKeys.lineHeightLayoutKey];\n      return PageStyleLineHeightLayout.fromString(lineHeight);\n    } catch (e) {\n      return PageStyleLineHeightLayout.normal;\n    }\n  }\n\n  PageStyleFontLayout get fontLayout {\n    if (layout != ViewLayoutPB.Document) {\n      return PageStyleFontLayout.normal;\n    }\n    try {\n      final ext = jsonDecode(extra);\n      final fontLayout = ext[ViewExtKeys.fontLayoutKey];\n      return PageStyleFontLayout.fromString(fontLayout);\n    } catch (e) {\n      return PageStyleFontLayout.normal;\n    }\n  }\n\n  @visibleForTesting\n  set isSpace(bool value) {\n    try {\n      if (extra.isEmpty) {\n        extra = jsonEncode({ViewExtKeys.isSpaceKey: value});\n      } else {\n        final ext = jsonDecode(extra);\n        ext[ViewExtKeys.isSpaceKey] = value;\n        extra = jsonEncode(ext);\n      }\n    } catch (e) {\n      extra = jsonEncode({ViewExtKeys.isSpaceKey: value});\n    }\n  }\n}\n\nextension ViewLayoutExtension on ViewLayoutPB {\n  FlowySvgData get icon => switch (this) {\n        ViewLayoutPB.Board => FlowySvgs.icon_board_s,\n        ViewLayoutPB.Calendar => FlowySvgs.icon_calendar_s,\n        ViewLayoutPB.Grid => FlowySvgs.icon_grid_s,\n        ViewLayoutPB.Document => FlowySvgs.icon_document_s,\n        ViewLayoutPB.Chat => FlowySvgs.chat_ai_page_s,\n        _ => FlowySvgs.icon_document_s,\n      };\n\n  bool get isDocumentView => switch (this) {\n        ViewLayoutPB.Document => true,\n        ViewLayoutPB.Chat ||\n        ViewLayoutPB.Grid ||\n        ViewLayoutPB.Board ||\n        ViewLayoutPB.Calendar =>\n          false,\n        _ => throw Exception('Unknown layout type'),\n      };\n\n  bool get isDatabaseView => switch (this) {\n        ViewLayoutPB.Grid ||\n        ViewLayoutPB.Board ||\n        ViewLayoutPB.Calendar =>\n          true,\n        ViewLayoutPB.Document || ViewLayoutPB.Chat => false,\n        _ => throw Exception('Unknown layout type'),\n      };\n\n  String get defaultName => switch (this) {\n        ViewLayoutPB.Document => '',\n        _ => LocaleKeys.menuAppHeader_defaultNewPageName.tr(),\n      };\n\n  bool get shrinkWrappable => switch (this) {\n        ViewLayoutPB.Grid => true,\n        ViewLayoutPB.Board => true,\n        _ => false,\n      };\n\n  double get pluginHeight => switch (this) {\n        ViewLayoutPB.Document || ViewLayoutPB.Board || ViewLayoutPB.Chat => 450,\n        ViewLayoutPB.Calendar => 650,\n        ViewLayoutPB.Grid => double.infinity,\n        _ => throw UnimplementedError(),\n      };\n}\n\nextension ViewFinder on List<ViewPB> {\n  ViewPB? findView(String id) {\n    for (final view in this) {\n      if (view.id == id) {\n        return view;\n      }\n\n      if (view.childViews.isNotEmpty) {\n        final v = view.childViews.findView(id);\n        if (v != null) {\n          return v;\n        }\n      }\n    }\n\n    return null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/view/view_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/folder_notification.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\n\n// Delete the view from trash, which means the view was deleted permanently\ntypedef DeleteViewNotifyValue = FlowyResult<ViewPB, FlowyError>;\n// The view get updated\ntypedef UpdateViewNotifiedValue = ViewPB;\n// Restore the view from trash\ntypedef RestoreViewNotifiedValue = FlowyResult<ViewPB, FlowyError>;\n// Move the view to trash\ntypedef MoveToTrashNotifiedValue = FlowyResult<DeletedViewPB, FlowyError>;\n\nclass ViewListener {\n  ViewListener({required this.viewId});\n\n  StreamSubscription<SubscribeObject>? _subscription;\n  void Function(UpdateViewNotifiedValue)? _updatedViewNotifier;\n  void Function(ChildViewUpdatePB)? _updateViewChildViewsNotifier;\n  void Function(DeleteViewNotifyValue)? _deletedNotifier;\n  void Function(RestoreViewNotifiedValue)? _restoredNotifier;\n  void Function(MoveToTrashNotifiedValue)? _moveToTrashNotifier;\n  bool _isDisposed = false;\n\n  FolderNotificationParser? _parser;\n  final String viewId;\n\n  void start({\n    void Function(UpdateViewNotifiedValue)? onViewUpdated,\n    void Function(ChildViewUpdatePB)? onViewChildViewsUpdated,\n    void Function(DeleteViewNotifyValue)? onViewDeleted,\n    void Function(RestoreViewNotifiedValue)? onViewRestored,\n    void Function(MoveToTrashNotifiedValue)? onViewMoveToTrash,\n  }) {\n    if (_isDisposed) {\n      Log.warn(\"ViewListener is already disposed\");\n      return;\n    }\n\n    _updatedViewNotifier = onViewUpdated;\n    _deletedNotifier = onViewDeleted;\n    _restoredNotifier = onViewRestored;\n    _moveToTrashNotifier = onViewMoveToTrash;\n    _updateViewChildViewsNotifier = onViewChildViewsUpdated;\n\n    _parser = FolderNotificationParser(\n      id: viewId,\n      callback: (ty, result) {\n        _handleObservableType(ty, result);\n      },\n    );\n\n    _subscription =\n        RustStreamReceiver.listen((observable) => _parser?.parse(observable));\n  }\n\n  void _handleObservableType(\n    FolderNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case FolderNotification.DidUpdateView:\n        result.fold(\n          (payload) {\n            final view = ViewPB.fromBuffer(payload);\n            _updatedViewNotifier?.call(view);\n          },\n          (error) => Log.error(error),\n        );\n        break;\n      case FolderNotification.DidUpdateChildViews:\n        result.fold(\n          (payload) {\n            final pb = ChildViewUpdatePB.fromBuffer(payload);\n            _updateViewChildViewsNotifier?.call(pb);\n          },\n          (error) => Log.error(error),\n        );\n        break;\n      case FolderNotification.DidDeleteView:\n        result.fold(\n          (payload) => _deletedNotifier\n              ?.call(FlowyResult.success(ViewPB.fromBuffer(payload))),\n          (error) => _deletedNotifier?.call(FlowyResult.failure(error)),\n        );\n        break;\n      case FolderNotification.DidRestoreView:\n        result.fold(\n          (payload) => _restoredNotifier\n              ?.call(FlowyResult.success(ViewPB.fromBuffer(payload))),\n          (error) => _restoredNotifier?.call(FlowyResult.failure(error)),\n        );\n        break;\n      case FolderNotification.DidMoveViewToTrash:\n        result.fold(\n          (payload) => _moveToTrashNotifier\n              ?.call(FlowyResult.success(DeletedViewPB.fromBuffer(payload))),\n          (error) => _moveToTrashNotifier?.call(FlowyResult.failure(error)),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    _isDisposed = true;\n    _parser = null;\n    await _subscription?.cancel();\n    _updatedViewNotifier = null;\n    _deletedNotifier = null;\n    _restoredNotifier = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_bloc.dart';\nimport 'package:appflowy/plugins/trash/application/trash_service.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:collection/collection.dart';\n\nclass ViewBackendService {\n  static Future<FlowyResult<ViewPB, FlowyError>> createView({\n    /// The [layoutType] is the type of the view.\n    required ViewLayoutPB layoutType,\n\n    /// The [parentViewId] is the parent view id.\n    required String parentViewId,\n\n    /// The [name] is the name of the view.\n    required String name,\n\n    /// The default value of [openAfterCreate] is false, meaning the view will\n    /// not be opened nor set as the current view. However, if set to true, the\n    /// view will be opened and set as the current view. Upon relaunching the\n    /// app, this view will be opened\n    bool openAfterCreate = false,\n\n    /// The initial data should be a JSON that represent the DocumentDataPB.\n    /// Currently, only support create document with initial data.\n    List<int>? initialDataBytes,\n\n    /// The [ext] is used to pass through the custom configuration\n    /// to the backend.\n    /// Linking the view to the existing database, it needs to pass\n    /// the database id. For example: \"database_id\": \"xxx\"\n    ///\n    Map<String, String> ext = const {},\n\n    /// The [index] is the index of the view in the parent view.\n    /// If the index is null, the view will be added to the end of the list.\n    int? index,\n    ViewSectionPB? section,\n    final String? viewId,\n  }) {\n    final payload = CreateViewPayloadPB.create()\n      ..parentViewId = parentViewId\n      ..name = name\n      ..layout = layoutType\n      ..setAsCurrent = openAfterCreate\n      ..initialData = initialDataBytes ?? [];\n\n    if (ext.isNotEmpty) {\n      payload.meta.addAll(ext);\n    }\n\n    if (index != null) {\n      payload.index = index;\n    }\n\n    if (section != null) {\n      payload.section = section;\n    }\n\n    if (viewId != null) {\n      payload.viewId = viewId;\n    }\n\n    return FolderEventCreateView(payload).send();\n  }\n\n  /// The orphan view is meant to be a view that is not attached to any parent view. By default, this\n  /// view will not be shown in the view list unless it is attached to a parent view that is shown in\n  /// the view list.\n  static Future<FlowyResult<ViewPB, FlowyError>> createOrphanView({\n    required String viewId,\n    required ViewLayoutPB layoutType,\n    required String name,\n    String? desc,\n\n    /// The initial data should be a JSON that represent the DocumentDataPB.\n    /// Currently, only support create document with initial data.\n    List<int>? initialDataBytes,\n  }) {\n    final payload = CreateOrphanViewPayloadPB.create()\n      ..viewId = viewId\n      ..name = name\n      ..layout = layoutType\n      ..initialData = initialDataBytes ?? [];\n\n    return FolderEventCreateOrphanView(payload).send();\n  }\n\n  static Future<FlowyResult<ViewPB, FlowyError>> createDatabaseLinkedView({\n    required String parentViewId,\n    required String databaseId,\n    required ViewLayoutPB layoutType,\n    required String name,\n  }) {\n    return createView(\n      layoutType: layoutType,\n      parentViewId: parentViewId,\n      name: name,\n      ext: {'database_id': databaseId},\n    );\n  }\n\n  /// Returns a list of views that are the children of the given [viewId].\n  static Future<FlowyResult<List<ViewPB>, FlowyError>> getChildViews({\n    required String viewId,\n  }) {\n    if (viewId.isEmpty) {\n      return Future.value(\n        FlowyResult<List<ViewPB>, FlowyError>.success(<ViewPB>[]),\n      );\n    }\n\n    final payload = ViewIdPB.create()..value = viewId;\n\n    return FolderEventGetView(payload).send().then((result) {\n      return result.fold(\n        (view) => FlowyResult.success(view.childViews),\n        (error) => FlowyResult.failure(error),\n      );\n    });\n  }\n\n  static Future<FlowyResult<void, FlowyError>> deleteView({\n    required String viewId,\n  }) {\n    final request = RepeatedViewIdPB.create()..items.add(viewId);\n    return FolderEventDeleteView(request).send();\n  }\n\n  static Future<FlowyResult<void, FlowyError>> deleteViews({\n    required List<String> viewIds,\n  }) {\n    final request = RepeatedViewIdPB.create()..items.addAll(viewIds);\n    return FolderEventDeleteView(request).send();\n  }\n\n  static Future<FlowyResult<ViewPB, FlowyError>> duplicate({\n    required ViewPB view,\n    required bool openAfterDuplicate,\n    // should include children views\n    required bool includeChildren,\n    String? parentViewId,\n    String? suffix,\n    required bool syncAfterDuplicate,\n  }) {\n    final payload = DuplicateViewPayloadPB.create()\n      ..viewId = view.id\n      ..openAfterDuplicate = openAfterDuplicate\n      ..includeChildren = includeChildren\n      ..syncAfterCreate = syncAfterDuplicate;\n\n    if (parentViewId != null) {\n      payload.parentViewId = parentViewId;\n    }\n\n    if (suffix != null) {\n      payload.suffix = suffix;\n    }\n\n    return FolderEventDuplicateView(payload).send();\n  }\n\n  static Future<FlowyResult<void, FlowyError>> favorite({\n    required String viewId,\n  }) {\n    final request = RepeatedViewIdPB.create()..items.add(viewId);\n    return FolderEventToggleFavorite(request).send();\n  }\n\n  static Future<FlowyResult<ViewPB, FlowyError>> updateView({\n    required String viewId,\n    String? name,\n    bool? isFavorite,\n    String? extra,\n  }) {\n    final payload = UpdateViewPayloadPB.create()..viewId = viewId;\n\n    if (name != null) {\n      payload.name = name;\n    }\n\n    if (isFavorite != null) {\n      payload.isFavorite = isFavorite;\n    }\n\n    if (extra != null) {\n      payload.extra = extra;\n    }\n\n    return FolderEventUpdateView(payload).send();\n  }\n\n  static Future<FlowyResult<void, FlowyError>> updateViewIcon({\n    required ViewPB view,\n    required EmojiIconData viewIcon,\n  }) {\n    final viewId = view.id;\n    final oldIcon = view.icon.toEmojiIconData();\n    final icon = viewIcon.toViewIcon();\n    final payload = UpdateViewIconPayloadPB.create()\n      ..viewId = viewId\n      ..icon = icon;\n    if (oldIcon.type == FlowyIconType.custom &&\n        viewIcon.emoji != oldIcon.emoji) {\n      DocumentEventDeleteFile(\n        DeleteFilePB(url: oldIcon.emoji),\n      ).send().onFailure((e) {\n        Log.error(\n          'updateViewIcon error while deleting :${oldIcon.emoji}, error: ${e.msg}, ${e.code}',\n        );\n      });\n    }\n    return FolderEventUpdateViewIcon(payload).send();\n  }\n\n  // deprecated\n  static Future<FlowyResult<void, FlowyError>> moveView({\n    required String viewId,\n    required int fromIndex,\n    required int toIndex,\n  }) {\n    final payload = MoveViewPayloadPB.create()\n      ..viewId = viewId\n      ..from = fromIndex\n      ..to = toIndex;\n\n    return FolderEventMoveView(payload).send();\n  }\n\n  /// Move the view to the new parent view.\n  ///\n  /// supports nested view\n  /// if the [prevViewId] is null, the view will be moved to the beginning of the list\n  static Future<FlowyResult<void, FlowyError>> moveViewV2({\n    required String viewId,\n    required String newParentId,\n    required String? prevViewId,\n    ViewSectionPB? fromSection,\n    ViewSectionPB? toSection,\n  }) {\n    final payload = MoveNestedViewPayloadPB(\n      viewId: viewId,\n      newParentId: newParentId,\n      prevViewId: prevViewId,\n      fromSection: fromSection,\n      toSection: toSection,\n    );\n\n    return FolderEventMoveNestedView(payload).send();\n  }\n\n  /// Fetches a flattened list of all Views.\n  ///\n  /// Views do not contain their children in this list, as they all exist\n  /// in the same level in this version.\n  ///\n  static Future<FlowyResult<RepeatedViewPB, FlowyError>> getAllViews() async {\n    return FolderEventGetAllViews().send();\n  }\n\n  static Future<FlowyResult<ViewPB, FlowyError>> getView(\n    String viewId,\n  ) async {\n    if (viewId.isEmpty) {\n      Log.error('ViewId is empty');\n    }\n    final payload = ViewIdPB.create()..value = viewId;\n    return FolderEventGetView(payload).send();\n  }\n\n  static Future<MentionPageStatus> getMentionPageStatus(String pageId) async {\n    final view = await ViewBackendService.getView(pageId).then(\n      (value) => value.toNullable(),\n    );\n\n    // found the page\n    if (view != null) {\n      return (view, false, false);\n    }\n\n    // if the view is not found, try to fetch from trash\n    final trashViews = await TrashService().readTrash();\n    final trash = trashViews.fold(\n      (l) => l.items.firstWhereOrNull((element) => element.id == pageId),\n      (r) => null,\n    );\n    if (trash != null) {\n      final trashView = ViewPB()\n        ..id = trash.id\n        ..name = trash.name;\n      return (trashView, true, false);\n    }\n\n    // the page was deleted\n    return (null, false, true);\n  }\n\n  static Future<FlowyResult<RepeatedViewPB, FlowyError>> getViewAncestors(\n    String viewId,\n  ) async {\n    final payload = ViewIdPB.create()..value = viewId;\n    return FolderEventGetViewAncestors(payload).send();\n  }\n\n  Future<FlowyResult<ViewPB, FlowyError>> getChildView({\n    required String parentViewId,\n    required String childViewId,\n  }) async {\n    final payload = ViewIdPB.create()..value = parentViewId;\n    return FolderEventGetView(payload).send().then((result) {\n      return result.fold(\n        (app) => FlowyResult.success(\n          app.childViews.firstWhere((e) => e.id == childViewId),\n        ),\n        (error) => FlowyResult.failure(error),\n      );\n    });\n  }\n\n  static Future<FlowyResult<void, FlowyError>> updateViewsVisibility(\n    List<ViewPB> views,\n    bool isPublic,\n  ) async {\n    final payload = UpdateViewVisibilityStatusPayloadPB(\n      viewIds: views.map((e) => e.id).toList(),\n      isPublic: isPublic,\n    );\n    return FolderEventUpdateViewVisibilityStatus(payload).send();\n  }\n\n  static Future<FlowyResult<PublishInfoResponsePB, FlowyError>> getPublishInfo(\n    ViewPB view,\n  ) async {\n    final payload = ViewIdPB()..value = view.id;\n    return FolderEventGetPublishInfo(payload).send();\n  }\n\n  static Future<FlowyResult<void, FlowyError>> publish(\n    ViewPB view, {\n    String? name,\n    List<String>? selectedViewIds,\n  }) async {\n    final payload = PublishViewParamsPB()..viewId = view.id;\n\n    if (name != null) {\n      payload.publishName = name;\n    }\n\n    if (selectedViewIds != null && selectedViewIds.isNotEmpty) {\n      payload.selectedViewIds = RepeatedViewIdPB(items: selectedViewIds);\n    }\n\n    return FolderEventPublishView(payload).send();\n  }\n\n  static Future<FlowyResult<void, FlowyError>> unpublish(\n    ViewPB view,\n  ) async {\n    final payload = UnpublishViewsPayloadPB(viewIds: [view.id]);\n    return FolderEventUnpublishViews(payload).send();\n  }\n\n  static Future<FlowyResult<void, FlowyError>> setPublishNameSpace(\n    String name,\n  ) async {\n    final payload = SetPublishNamespacePayloadPB()..newNamespace = name;\n    return FolderEventSetPublishNamespace(payload).send();\n  }\n\n  static Future<FlowyResult<PublishNamespacePB, FlowyError>>\n      getPublishNameSpace() async {\n    return FolderEventGetPublishNamespace().send();\n  }\n\n  static Future<List<ViewPB>> getAllChildViews(ViewPB view) async {\n    final views = <ViewPB>[];\n\n    final childViews =\n        await ViewBackendService.getChildViews(viewId: view.id).fold(\n      (s) => s,\n      (f) => [],\n    );\n\n    for (final child in childViews) {\n      // filter the view itself\n      if (child.id == view.id) {\n        continue;\n      }\n      views.add(child);\n      views.addAll(await getAllChildViews(child));\n    }\n\n    return views;\n  }\n\n  static Future<(bool, List<ViewPB>)> containPublishedPage(ViewPB view) async {\n    final childViews = await ViewBackendService.getAllChildViews(view);\n    final views = [view, ...childViews];\n    final List<ViewPB> publishedPages = [];\n\n    for (final view in views) {\n      final publishInfo = await ViewBackendService.getPublishInfo(view);\n      if (publishInfo.isSuccess) {\n        publishedPages.add(view);\n      }\n    }\n\n    return (publishedPages.isNotEmpty, publishedPages);\n  }\n\n  static Future<FlowyResult<void, FlowyError>> lockView(String viewId) async {\n    final payload = ViewIdPB()..value = viewId;\n    return FolderEventLockView(payload).send();\n  }\n\n  static Future<FlowyResult<void, FlowyError>> unlockView(String viewId) async {\n    final payload = ViewIdPB()..value = viewId;\n    return FolderEventUnlockView(payload).send();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/view_info/view_info_bloc.dart",
    "content": "import 'package:appflowy/util/int64_extension.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:bloc/bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'view_info_bloc.freezed.dart';\n\nclass ViewInfoBloc extends Bloc<ViewInfoEvent, ViewInfoState> {\n  ViewInfoBloc({required this.view}) : super(ViewInfoState.initial()) {\n    on<ViewInfoEvent>((event, emit) {\n      event.when(\n        started: () {\n          emit(\n            state.copyWith(\n              createdAt: view.createTime.toDateTime(),\n              titleCounters: view.name.getCounter(),\n            ),\n          );\n        },\n        unregisterEditorState: () {\n          _clearWordCountService();\n          emit(state.copyWith(documentCounters: null));\n        },\n        registerEditorState: (editorState) {\n          _clearWordCountService();\n          _wordCountService = WordCountService(editorState: editorState)\n            ..addListener(_onWordCountChanged)\n            ..register();\n\n          emit(\n            state.copyWith(\n              documentCounters: _wordCountService!.documentCounters,\n            ),\n          );\n        },\n        wordCountChanged: () {\n          emit(\n            state.copyWith(\n              documentCounters: _wordCountService?.documentCounters,\n            ),\n          );\n        },\n        titleChanged: (s) {\n          emit(\n            state.copyWith(\n              titleCounters: s.getCounter(),\n            ),\n          );\n        },\n        workspaceTypeChanged: (s) {\n          emit(\n            state.copyWith(workspaceType: s),\n          );\n        },\n      );\n    });\n\n    UserEventGetUserProfile().send().then((value) {\n      value.fold(\n        (s) {\n          if (!isClosed) {\n            add(ViewInfoEvent.workspaceTypeChanged(s.workspaceType));\n          }\n        },\n        (e) => Log.error('Failed to get user profile: $e'),\n      );\n    });\n  }\n\n  final ViewPB view;\n\n  WordCountService? _wordCountService;\n\n  @override\n  Future<void> close() async {\n    _clearWordCountService();\n    await super.close();\n  }\n\n  void _onWordCountChanged() => add(const ViewInfoEvent.wordCountChanged());\n\n  void _clearWordCountService() {\n    _wordCountService\n      ?..removeListener(_onWordCountChanged)\n      ..dispose();\n    _wordCountService = null;\n  }\n}\n\n@freezed\nclass ViewInfoEvent with _$ViewInfoEvent {\n  const factory ViewInfoEvent.started() = _Started;\n\n  const factory ViewInfoEvent.unregisterEditorState() = _UnregisterEditorState;\n\n  const factory ViewInfoEvent.registerEditorState({\n    required EditorState editorState,\n  }) = _RegisterEditorState;\n\n  const factory ViewInfoEvent.wordCountChanged() = _WordCountChanged;\n\n  const factory ViewInfoEvent.titleChanged(String title) = _TitleChanged;\n\n  const factory ViewInfoEvent.workspaceTypeChanged(\n    WorkspaceTypePB workspaceType,\n  ) = _WorkspaceTypeChanged;\n}\n\n@freezed\nclass ViewInfoState with _$ViewInfoState {\n  const factory ViewInfoState({\n    required Counters? documentCounters,\n    required Counters? titleCounters,\n    required DateTime? createdAt,\n    required WorkspaceTypePB? workspaceType,\n  }) = _ViewInfoState;\n\n  factory ViewInfoState.initial() => const ViewInfoState(\n        documentCounters: null,\n        titleCounters: null,\n        createdAt: null,\n        workspaceType: null,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart",
    "content": "import 'package:appflowy/plugins/trash/application/prelude.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'view_title_bar_bloc.freezed.dart';\n\nclass ViewTitleBarBloc extends Bloc<ViewTitleBarEvent, ViewTitleBarState> {\n  ViewTitleBarBloc({required this.view}) : super(ViewTitleBarState.initial()) {\n    on<ViewTitleBarEvent>(\n      (event, emit) async {\n        await event.when(\n          reload: () async {\n            final List<ViewPB> ancestors =\n                await ViewBackendService.getViewAncestors(view.id).fold(\n              (s) => s.items,\n              (f) => [],\n            );\n\n            final isDeleted = (await trashService.readTrash()).fold(\n              (s) => s.items.any((t) => t.id == view.id),\n              (f) => false,\n            );\n\n            emit(state.copyWith(ancestors: ancestors, isDeleted: isDeleted));\n          },\n          trashUpdated: (trash) {\n            if (trash.any((t) => t.id == view.id)) {\n              emit(state.copyWith(isDeleted: true));\n            }\n          },\n        );\n      },\n    );\n\n    trashService = TrashService();\n    viewListener = ViewListener(viewId: view.id)\n      ..start(\n        onViewChildViewsUpdated: (_) {\n          if (!isClosed) {\n            add(const ViewTitleBarEvent.reload());\n          }\n        },\n      );\n    trashListener = TrashListener()\n      ..start(\n        trashUpdated: (trashOrFailed) {\n          final trash = trashOrFailed.toNullable();\n          if (trash != null && !isClosed) {\n            add(ViewTitleBarEvent.trashUpdated(trash: trash));\n          }\n        },\n      );\n\n    if (!isClosed) {\n      add(const ViewTitleBarEvent.reload());\n    }\n  }\n\n  final ViewPB view;\n  late final TrashService trashService;\n  late final ViewListener viewListener;\n  late final TrashListener trashListener;\n\n  @override\n  Future<void> close() {\n    trashListener.close();\n    viewListener.stop();\n    return super.close();\n  }\n}\n\n@freezed\nclass ViewTitleBarEvent with _$ViewTitleBarEvent {\n  const factory ViewTitleBarEvent.reload() = Reload;\n  const factory ViewTitleBarEvent.trashUpdated({\n    required List<TrashPB> trash,\n  }) = TrashUpdated;\n}\n\n@freezed\nclass ViewTitleBarState with _$ViewTitleBarState {\n  const factory ViewTitleBarState({\n    required List<ViewPB> ancestors,\n    @Default(false) bool isDeleted,\n  }) = _ViewTitleBarState;\n\n  factory ViewTitleBarState.initial() => const ViewTitleBarState(ancestors: []);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bloc.dart",
    "content": "import 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport '../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\n\npart 'view_title_bloc.freezed.dart';\n\nclass ViewTitleBloc extends Bloc<ViewTitleEvent, ViewTitleState> {\n  ViewTitleBloc({\n    required this.view,\n  })  : viewListener = ViewListener(viewId: view.id),\n        super(ViewTitleState.initial()) {\n    on<ViewTitleEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            emit(\n              state.copyWith(\n                name: view.name,\n                icon: view.icon.toEmojiIconData(),\n                view: view,\n              ),\n            );\n\n            viewListener.start(\n              onViewUpdated: (view) {\n                add(\n                  ViewTitleEvent.updateNameOrIcon(\n                    view.name,\n                    view.icon.toEmojiIconData(),\n                    view,\n                  ),\n                );\n              },\n            );\n          },\n          updateNameOrIcon: (name, icon, view) async {\n            emit(\n              state.copyWith(\n                name: name,\n                icon: icon,\n                view: view,\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  final ViewPB view;\n  final ViewListener viewListener;\n\n  @override\n  Future<void> close() {\n    viewListener.stop();\n    return super.close();\n  }\n}\n\n@freezed\nclass ViewTitleEvent with _$ViewTitleEvent {\n  const factory ViewTitleEvent.initial() = Initial;\n\n  const factory ViewTitleEvent.updateNameOrIcon(\n    String name,\n    EmojiIconData icon,\n    ViewPB? view,\n  ) = UpdateNameOrIcon;\n}\n\n@freezed\nclass ViewTitleState with _$ViewTitleState {\n  const factory ViewTitleState({\n    required String name,\n    required EmojiIconData icon,\n    @Default(null) ViewPB? view,\n  }) = _ViewTitleState;\n\n  factory ViewTitleState.initial() => ViewTitleState(\n        name: '',\n        icon: EmojiIconData.none(),\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/workspace/prelude.dart",
    "content": "export 'workspace_bloc.dart';\nexport 'workspace_listener.dart';\nexport 'workspace_service.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart",
    "content": "import 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'workspace_bloc.freezed.dart';\n\nclass WorkspaceBloc extends Bloc<WorkspaceEvent, WorkspaceState> {\n  WorkspaceBloc({required this.userService}) : super(WorkspaceState.initial()) {\n    _dispatch();\n  }\n\n  final UserBackendService userService;\n\n  void _dispatch() {\n    on<WorkspaceEvent>(\n      (event, emit) async {\n        await event.map(\n          initial: (e) async {\n            await _fetchWorkspaces(emit);\n          },\n          createWorkspace: (e) async {\n            await _createWorkspace(e.name, e.desc, emit);\n          },\n          workspacesReceived: (e) async {\n            emit(\n              e.workspacesOrFail.fold(\n                (workspaces) => state.copyWith(\n                  workspaces: workspaces,\n                  successOrFailure: FlowyResult.success(null),\n                ),\n                (error) => state.copyWith(\n                  successOrFailure: FlowyResult.failure(error),\n                ),\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  Future<void> _fetchWorkspaces(Emitter<WorkspaceState> emit) async {\n    final workspacesOrFailed = await userService.getWorkspaces();\n    emit(\n      workspacesOrFailed.fold(\n        (workspaces) => state.copyWith(\n          workspaces: [],\n          successOrFailure: FlowyResult.success(null),\n        ),\n        (error) {\n          Log.error(error);\n          return state.copyWith(successOrFailure: FlowyResult.failure(error));\n        },\n      ),\n    );\n  }\n\n  Future<void> _createWorkspace(\n    String name,\n    String desc,\n    Emitter<WorkspaceState> emit,\n  ) async {\n    final result =\n        await userService.createUserWorkspace(name, WorkspaceTypePB.ServerW);\n    emit(\n      result.fold(\n        (workspace) {\n          return state.copyWith(successOrFailure: FlowyResult.success(null));\n        },\n        (error) {\n          Log.error(error);\n          return state.copyWith(successOrFailure: FlowyResult.failure(error));\n        },\n      ),\n    );\n  }\n}\n\n@freezed\nclass WorkspaceEvent with _$WorkspaceEvent {\n  const factory WorkspaceEvent.initial() = Initial;\n  const factory WorkspaceEvent.createWorkspace(String name, String desc) =\n      CreateWorkspace;\n  const factory WorkspaceEvent.workspacesReceived(\n    FlowyResult<List<WorkspacePB>, FlowyError> workspacesOrFail,\n  ) = WorkspacesReceived;\n}\n\n@freezed\nclass WorkspaceState with _$WorkspaceState {\n  const factory WorkspaceState({\n    required bool isLoading,\n    required List<WorkspacePB> workspaces,\n    required FlowyResult<void, FlowyError> successOrFailure,\n  }) = _WorkspaceState;\n\n  factory WorkspaceState.initial() => WorkspaceState(\n        isLoading: false,\n        workspaces: List.empty(),\n        successOrFailure: FlowyResult.success(null),\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/folder_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'\n    show UserProfilePB;\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flowy_infra/notifier.dart';\n\ntypedef RootViewsNotifyValue = FlowyResult<List<ViewPB>, FlowyError>;\ntypedef WorkspaceNotifyValue = FlowyResult<WorkspacePB, FlowyError>;\n\n/// The [WorkspaceListener] listens to the changes including the below:\n///\n/// - The root views of the workspace. (Not including the views are inside the root views)\n/// - The workspace itself.\nclass WorkspaceListener {\n  WorkspaceListener({required this.user, required this.workspaceId});\n\n  final UserProfilePB user;\n  final String workspaceId;\n\n  PublishNotifier<RootViewsNotifyValue>? _appsChangedNotifier =\n      PublishNotifier();\n  PublishNotifier<WorkspaceNotifyValue>? _workspaceUpdatedNotifier =\n      PublishNotifier();\n\n  FolderNotificationListener? _listener;\n\n  void start({\n    void Function(RootViewsNotifyValue)? appsChanged,\n    void Function(WorkspaceNotifyValue)? onWorkspaceUpdated,\n  }) {\n    if (appsChanged != null) {\n      _appsChangedNotifier?.addPublishListener(appsChanged);\n    }\n\n    if (onWorkspaceUpdated != null) {\n      _workspaceUpdatedNotifier?.addPublishListener(onWorkspaceUpdated);\n    }\n\n    _listener = FolderNotificationListener(\n      objectId: workspaceId,\n      handler: _handleObservableType,\n    );\n  }\n\n  void _handleObservableType(\n    FolderNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case FolderNotification.DidUpdateWorkspace:\n        result.fold(\n          (payload) => _workspaceUpdatedNotifier?.value =\n              FlowyResult.success(WorkspacePB.fromBuffer(payload)),\n          (error) =>\n              _workspaceUpdatedNotifier?.value = FlowyResult.failure(error),\n        );\n        break;\n      case FolderNotification.DidUpdateWorkspaceViews:\n        result.fold(\n          (payload) => _appsChangedNotifier?.value =\n              FlowyResult.success(RepeatedViewPB.fromBuffer(payload).items),\n          (error) => _appsChangedNotifier?.value = FlowyResult.failure(error),\n        );\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    await _listener?.stop();\n    _appsChangedNotifier?.dispose();\n    _appsChangedNotifier = null;\n\n    _workspaceUpdatedNotifier?.dispose();\n    _workspaceUpdatedNotifier = null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_sections_listener.dart",
    "content": "import 'dart:async';\nimport 'dart:typed_data';\n\nimport 'package:appflowy/core/notification/folder_notification.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'\n    show UserProfilePB;\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flowy_infra/notifier.dart';\n\ntypedef SectionNotifyValue = FlowyResult<SectionViewsPB, FlowyError>;\n\n/// The [WorkspaceSectionsListener] listens to the changes including the below:\n///\n/// - The root views inside different section of the workspace. (Not including the views are inside the root views)\n///   depends on the section type(s).\nclass WorkspaceSectionsListener {\n  WorkspaceSectionsListener({\n    required this.user,\n    required this.workspaceId,\n  });\n\n  final UserProfilePB user;\n  final String workspaceId;\n\n  final _sectionNotifier = PublishNotifier<SectionNotifyValue>();\n  late final FolderNotificationListener _listener;\n\n  void start({\n    void Function(SectionNotifyValue)? sectionChanged,\n  }) {\n    if (sectionChanged != null) {\n      _sectionNotifier.addPublishListener(sectionChanged);\n    }\n\n    _listener = FolderNotificationListener(\n      objectId: workspaceId,\n      handler: _handleObservableType,\n    );\n  }\n\n  void _handleObservableType(\n    FolderNotification ty,\n    FlowyResult<Uint8List, FlowyError> result,\n  ) {\n    switch (ty) {\n      case FolderNotification.DidUpdateSectionViews:\n        final FlowyResult<SectionViewsPB, FlowyError> value = result.fold(\n          (s) => FlowyResult.success(\n            SectionViewsPB.fromBuffer(s),\n          ),\n          (f) => FlowyResult.failure(f),\n        );\n        _sectionNotifier.value = value;\n        break;\n      default:\n        break;\n    }\n  }\n\n  Future<void> stop() async {\n    _sectionNotifier.dispose();\n\n    await _listener.stop();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:fixnum/fixnum.dart' as fixnum;\n\nclass WorkspaceService {\n  WorkspaceService({required this.workspaceId, required this.userId});\n\n  final String workspaceId;\n  final fixnum.Int64 userId;\n\n  Future<FlowyResult<ViewPB, FlowyError>> createView({\n    required String name,\n    required ViewSectionPB viewSection,\n    int? index,\n    ViewLayoutPB? layout,\n    bool? setAsCurrent,\n    String? viewId,\n    String? extra,\n  }) {\n    final payload = CreateViewPayloadPB.create()\n      ..parentViewId = workspaceId\n      ..name = name\n      ..layout = layout ?? ViewLayoutPB.Document\n      ..section = viewSection;\n\n    if (index != null) {\n      payload.index = index;\n    }\n\n    if (setAsCurrent != null) {\n      payload.setAsCurrent = setAsCurrent;\n    }\n\n    if (viewId != null) {\n      payload.viewId = viewId;\n    }\n\n    if (extra != null) {\n      payload.extra = extra;\n    }\n\n    return FolderEventCreateView(payload).send();\n  }\n\n  Future<FlowyResult<WorkspacePB, FlowyError>> getWorkspace() {\n    return FolderEventReadCurrentWorkspace().send();\n  }\n\n  Future<FlowyResult<List<ViewPB>, FlowyError>> getPublicViews() {\n    final payload = GetWorkspaceViewPB.create()..value = workspaceId;\n    return FolderEventReadWorkspaceViews(payload).send().then((result) {\n      return result.fold(\n        (views) => FlowyResult.success(views.items),\n        (error) => FlowyResult.failure(error),\n      );\n    });\n  }\n\n  Future<FlowyResult<List<ViewPB>, FlowyError>> getPrivateViews() {\n    final payload = GetWorkspaceViewPB.create()..value = workspaceId;\n    return FolderEventReadPrivateViews(payload).send().then((result) {\n      return result.fold(\n        (views) => FlowyResult.success(views.items),\n        (error) => FlowyResult.failure(error),\n      );\n    });\n  }\n\n  Future<FlowyResult<void, FlowyError>> moveView({\n    required String viewId,\n    required int fromIndex,\n    required int toIndex,\n  }) {\n    final payload = MoveViewPayloadPB.create()\n      ..viewId = viewId\n      ..from = fromIndex\n      ..to = toIndex;\n\n    return FolderEventMoveView(payload).send();\n  }\n\n  Future<FlowyResult<WorkspaceUsagePB?, FlowyError>> getWorkspaceUsage() async {\n    final payload = UserWorkspaceIdPB(workspaceId: workspaceId);\n    return UserEventGetWorkspaceUsage(payload).send();\n  }\n\n  Future<FlowyResult<BillingPortalPB, FlowyError>> getBillingPortal() {\n    return UserEventGetBillingPortal().send();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/recent_views_list.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/search_results_list.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pbenum.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'widgets/search_ask_ai_entrance.dart';\n\nclass CommandPalette extends InheritedWidget {\n  CommandPalette({\n    super.key,\n    required Widget? child,\n    required this.notifier,\n  }) : super(\n          child: _CommandPaletteController(notifier: notifier, child: child),\n        );\n\n  final ValueNotifier<CommandPaletteNotifierValue> notifier;\n\n  static CommandPalette of(BuildContext context) {\n    final CommandPalette? result =\n        context.dependOnInheritedWidgetOfExactType<CommandPalette>();\n\n    assert(result != null, \"CommandPalette could not be found\");\n\n    return result!;\n  }\n\n  static CommandPalette? maybeOf(BuildContext context) =>\n      context.dependOnInheritedWidgetOfExactType<CommandPalette>();\n\n  void toggle({\n    UserWorkspaceBloc? workspaceBloc,\n    SpaceBloc? spaceBloc,\n  }) {\n    final value = notifier.value;\n    notifier.value = notifier.value.copyWith(\n      isOpen: !value.isOpen,\n      userWorkspaceBloc: workspaceBloc,\n      spaceBloc: spaceBloc,\n    );\n  }\n\n  void updateBlocs({\n    UserWorkspaceBloc? workspaceBloc,\n    SpaceBloc? spaceBloc,\n  }) {\n    notifier.value = notifier.value.copyWith(\n      userWorkspaceBloc: workspaceBloc,\n      spaceBloc: spaceBloc,\n    );\n  }\n\n  @override\n  bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;\n}\n\nclass _ToggleCommandPaletteIntent extends Intent {\n  const _ToggleCommandPaletteIntent();\n}\n\nclass _CommandPaletteController extends StatefulWidget {\n  const _CommandPaletteController({\n    required this.child,\n    required this.notifier,\n  });\n\n  final Widget? child;\n  final ValueNotifier<CommandPaletteNotifierValue> notifier;\n\n  @override\n  State<_CommandPaletteController> createState() =>\n      _CommandPaletteControllerState();\n}\n\nclass _CommandPaletteControllerState extends State<_CommandPaletteController> {\n  late ValueNotifier<CommandPaletteNotifierValue> _toggleNotifier =\n      widget.notifier;\n  bool _isOpen = false;\n\n  @override\n  void initState() {\n    super.initState();\n    _toggleNotifier.addListener(_onToggle);\n  }\n\n  @override\n  void dispose() {\n    _toggleNotifier.removeListener(_onToggle);\n    super.dispose();\n  }\n\n  @override\n  void didUpdateWidget(_CommandPaletteController oldWidget) {\n    if (oldWidget.notifier != widget.notifier) {\n      oldWidget.notifier.removeListener(_onToggle);\n      _toggleNotifier = widget.notifier;\n      _toggleNotifier.addListener(_onToggle);\n    }\n    super.didUpdateWidget(oldWidget);\n  }\n\n  void _onToggle() {\n    if (_toggleNotifier.value.isOpen && !_isOpen) {\n      _isOpen = true;\n      final workspaceBloc = _toggleNotifier.value.userWorkspaceBloc;\n      final spaceBloc = _toggleNotifier.value.spaceBloc;\n      final commandBloc = context.read<CommandPaletteBloc>();\n      Log.info(\n        'CommandPalette onToggle: workspaceType ${workspaceBloc?.state.userProfile.workspaceType}',\n      );\n      commandBloc.add(CommandPaletteEvent.refreshCachedViews());\n      FlowyOverlay.show(\n        context: context,\n        builder: (_) => MultiBlocProvider(\n          providers: [\n            BlocProvider.value(value: commandBloc),\n            if (workspaceBloc != null) BlocProvider.value(value: workspaceBloc),\n            if (spaceBloc != null) BlocProvider.value(value: spaceBloc),\n          ],\n          child: CommandPaletteModal(shortcutBuilder: _buildShortcut),\n        ),\n      ).then((_) {\n        _isOpen = false;\n        _toggleNotifier.value = _toggleNotifier.value.copyWith(isOpen: false);\n      });\n    } else if (!_toggleNotifier.value.isOpen && _isOpen) {\n      FlowyOverlay.pop(context);\n      _isOpen = false;\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) =>\n      _buildShortcut(widget.child ?? const SizedBox.shrink());\n\n  Widget _buildShortcut(Widget child) => FocusableActionDetector(\n        actions: {\n          _ToggleCommandPaletteIntent:\n              CallbackAction<_ToggleCommandPaletteIntent>(\n            onInvoke: (intent) => _toggleNotifier.value = _toggleNotifier.value\n                .copyWith(isOpen: !_toggleNotifier.value.isOpen),\n          ),\n        },\n        shortcuts: {\n          LogicalKeySet(\n            UniversalPlatform.isMacOS\n                ? LogicalKeyboardKey.meta\n                : LogicalKeyboardKey.control,\n            LogicalKeyboardKey.keyP,\n          ): const _ToggleCommandPaletteIntent(),\n        },\n        child: child,\n      );\n}\n\nclass CommandPaletteModal extends StatelessWidget {\n  const CommandPaletteModal({super.key, required this.shortcutBuilder});\n\n  final Widget Function(Widget) shortcutBuilder;\n\n  @override\n  Widget build(BuildContext context) {\n    final workspaceState = context.read<UserWorkspaceBloc?>()?.state;\n    final showAskingAI =\n        workspaceState?.userProfile.workspaceType == WorkspaceTypePB.ServerW;\n    return BlocListener<CommandPaletteBloc, CommandPaletteState>(\n      listener: (_, state) {\n        if (state.askAI && context.mounted) {\n          if (Navigator.canPop(context)) FlowyOverlay.pop(context);\n          final currentWorkspace = workspaceState?.workspaces;\n          final spaceBloc = context.read<SpaceBloc?>();\n          if (currentWorkspace != null && spaceBloc != null) {\n            spaceBloc.add(\n              SpaceEvent.createPage(\n                name: '',\n                layout: ViewLayoutPB.Chat,\n                index: 0,\n                openAfterCreate: true,\n              ),\n            );\n          }\n        }\n      },\n      child: BlocBuilder<CommandPaletteBloc, CommandPaletteState>(\n        builder: (context, state) {\n          final theme = AppFlowyTheme.of(context);\n          final noQuery = state.query?.isEmpty ?? true, hasQuery = !noQuery;\n          final hasResult = state.combinedResponseItems.isNotEmpty,\n              searching = state.searching;\n          final spaceXl = theme.spacing.xl;\n          return FlowyDialog(\n            backgroundColor: theme.surfaceColorScheme.layer01,\n            alignment: Alignment.topCenter,\n            insetPadding: const EdgeInsets.only(top: 100),\n            constraints: const BoxConstraints(\n              maxHeight: 640,\n              maxWidth: 960,\n              minWidth: 572,\n              minHeight: 640,\n            ),\n            expandHeight: false,\n            child: shortcutBuilder(\n              // Change mainAxisSize to max so Expanded works correctly.\n              Padding(\n                padding: EdgeInsets.fromLTRB(spaceXl, spaceXl, spaceXl, 0),\n                child: Column(\n                  children: [\n                    SearchField(query: state.query, isLoading: searching),\n                    if (noQuery)\n                      Flexible(\n                        child: RecentViewsList(\n                          onSelected: () => FlowyOverlay.pop(context),\n                        ),\n                      ),\n                    if (hasResult && hasQuery)\n                      Flexible(\n                        child: SearchResultList(\n                          cachedViews: state.cachedViews,\n                          resultItems:\n                              state.combinedResponseItems.values.toList(),\n                          resultSummaries: state.resultSummaries,\n                        ),\n                      )\n                    // When there are no results and the query is not empty and not loading,\n                    // show the no results message, centered in the available space.\n                    else if (hasQuery && !searching) ...[\n                      if (showAskingAI) SearchAskAiEntrance(),\n                      Expanded(\n                        child: const NoSearchResultsHint(),\n                      ),\n                    ],\n                    if (hasQuery && searching && !hasResult)\n                      // Show a loading indicator when searching\n                      Expanded(\n                        child: Center(\n                          child: Center(\n                            child: CircularProgressIndicator.adaptive(),\n                          ),\n                        ),\n                      ),\n                  ],\n                ),\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\n/// Updated _NoResultsHint now centers its content.\nclass NoSearchResultsHint extends StatelessWidget {\n  const NoSearchResultsHint({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context),\n        textColor = theme.textColorScheme.secondary;\n    return Center(\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          FlowySvg(\n            FlowySvgs.m_home_search_icon_m,\n            color: theme.iconColorScheme.secondary,\n            size: Size.square(24),\n          ),\n          const VSpace(8),\n          Text(\n            LocaleKeys.search_noResultForSearching.tr(),\n            style: theme.textStyle.body.enhanced(color: textColor),\n            maxLines: 1,\n            overflow: TextOverflow.ellipsis,\n          ),\n          const VSpace(4),\n          RichText(\n            textAlign: TextAlign.center,\n            text: TextSpan(\n              text: LocaleKeys.search_noResultForSearchingHintWithoutTrash.tr(),\n              style: theme.textStyle.caption.standard(color: textColor),\n              children: [\n                TextSpan(\n                  text: LocaleKeys.trash_text.tr(),\n                  style: theme.textStyle.caption.underline(color: textColor),\n                  recognizer: TapGestureRecognizer()\n                    ..onTap = () {\n                      FlowyOverlay.pop(context);\n                      getIt<MenuSharedState>().latestOpenView = null;\n                      getIt<TabsBloc>().add(\n                        TabsEvent.openPlugin(\n                          plugin: makePlugin(pluginType: PluginType.trash),\n                        ),\n                      );\n                    },\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass CommandPaletteNotifierValue {\n  CommandPaletteNotifierValue({\n    this.isOpen = false,\n    this.userWorkspaceBloc,\n    this.spaceBloc,\n  });\n\n  final bool isOpen;\n  final UserWorkspaceBloc? userWorkspaceBloc;\n  final SpaceBloc? spaceBloc;\n\n  CommandPaletteNotifierValue copyWith({\n    bool? isOpen,\n    UserWorkspaceBloc? userWorkspaceBloc,\n    SpaceBloc? spaceBloc,\n  }) {\n    return CommandPaletteNotifierValue(\n      isOpen: isOpen ?? this.isOpen,\n      userWorkspaceBloc: userWorkspaceBloc ?? this.userWorkspaceBloc,\n      spaceBloc: spaceBloc ?? this.spaceBloc,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/command_palette/navigation_bloc_extension.dart",
    "content": "import 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';\nimport 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';\n\nextension NavigationBlocExtension on String {\n  void navigateTo() {\n    getIt<ActionNavigationBloc>().add(\n      ActionNavigationEvent.performAction(\n        action: NavigationAction(objectId: this),\n        showErrorToast: true,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/page_preview.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/mobile/presentation/search/mobile_view_ancestors.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy/shared/flowy_gradient_colors.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/util/int64_extension.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart' hide TextDirection;\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass PagePreview extends StatelessWidget {\n  const PagePreview({\n    super.key,\n    required this.view,\n    required this.onViewOpened,\n  });\n  final ViewPB view;\n  final VoidCallback onViewOpened;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final backgroundColor = Theme.of(context).isLightMode\n        ? Color(0xffF8FAFF)\n        : theme.surfaceColorScheme.layer02;\n\n    return BlocProvider(\n      create: (context) => DocumentImmersiveCoverBloc(view: view)\n        ..add(const DocumentImmersiveCoverEvent.initial()),\n      child:\n          BlocBuilder<DocumentImmersiveCoverBloc, DocumentImmersiveCoverState>(\n        builder: (context, state) {\n          final cover = buildCover(state, context);\n          return Container(\n            height: MediaQuery.of(context).size.height,\n            width: 280,\n            decoration: BoxDecoration(\n              color: backgroundColor,\n              borderRadius: BorderRadius.circular(12),\n            ),\n            margin: EdgeInsets.only(\n              top: theme.spacing.xs,\n              bottom: theme.spacing.xl,\n            ),\n            child: Stack(\n              children: [\n                SingleChildScrollView(\n                  child: Column(\n                    mainAxisSize: MainAxisSize.min,\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      ClipRRect(\n                        borderRadius: BorderRadius.only(\n                          topLeft: Radius.circular(12),\n                          topRight: Radius.circular(12),\n                        ),\n                        child: cover ?? VSpace(80),\n                      ),\n                      VSpace(24),\n                      Padding(\n                        padding: const EdgeInsets.symmetric(horizontal: 20),\n                        child: Column(\n                          crossAxisAlignment: CrossAxisAlignment.start,\n                          mainAxisSize: MainAxisSize.min,\n                          children: [\n                            buildTitle(context, view),\n                            buildPath(context, view),\n                            ...buildTime(\n                              context,\n                              LocaleKeys.commandPalette_created.tr(),\n                              view.createTime.toDateTime(),\n                            ),\n                            if (view.lastEdited != view.createTime)\n                              ...buildTime(\n                                context,\n                                LocaleKeys.commandPalette_edited.tr(),\n                                view.lastEdited.toDateTime(),\n                              ),\n                          ],\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n                Positioned(\n                  top: 70,\n                  left: 20,\n                  child: SizedBox.square(\n                    dimension: 24,\n                    child: Center(child: buildIcon(theme, view, cover != null)),\n                  ),\n                ),\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget? buildCover(DocumentImmersiveCoverState state, BuildContext context) {\n    final cover = state.cover;\n    final type = state.cover.type;\n    const height = 80.0;\n    if (type == PageStyleCoverImageType.customImage ||\n        type == PageStyleCoverImageType.unsplashImage) {\n      final userProfile = context.read<UserWorkspaceBloc?>()?.state.userProfile;\n      if (userProfile == null) return null;\n\n      return SizedBox(\n        height: height,\n        width: double.infinity,\n        child: FlowyNetworkImage(\n          url: cover.value,\n          userProfilePB: userProfile,\n        ),\n      );\n    }\n\n    if (type == PageStyleCoverImageType.builtInImage) {\n      return SizedBox(\n        height: height,\n        width: double.infinity,\n        child: Image.asset(\n          PageStyleCoverImageType.builtInImagePath(cover.value),\n          fit: BoxFit.cover,\n        ),\n      );\n    }\n\n    if (type == PageStyleCoverImageType.pureColor) {\n      final color = FlowyTint.fromId(cover.value)?.color(context) ??\n          cover.value.tryToColor();\n      return Container(\n        height: height,\n        width: double.infinity,\n        color: color,\n      );\n    }\n\n    if (type == PageStyleCoverImageType.gradientColor) {\n      return Container(\n        height: height,\n        width: double.infinity,\n        decoration: BoxDecoration(\n          gradient: FlowyGradientColor.fromId(cover.value).linear,\n        ),\n      );\n    }\n\n    if (type == PageStyleCoverImageType.localImage) {\n      return SizedBox(\n        height: height,\n        width: double.infinity,\n        child: Image.file(\n          File(cover.value),\n          fit: BoxFit.cover,\n        ),\n      );\n    }\n\n    return null;\n  }\n\n  Widget buildIcon(AppFlowyThemeData theme, ViewPB view, bool hasCover) {\n    final hasIcon = view.icon.value.isNotEmpty;\n    if (!hasIcon && hasCover) return const SizedBox.shrink();\n    return hasIcon\n        ? RawEmojiIconWidget(\n            emoji: view.icon.toEmojiIconData(),\n            emojiSize: 16.0,\n            lineHeight: 20 / 16,\n          )\n        : FlowySvg(\n            view.iconData,\n            size: const Size.square(20),\n            color: theme.iconColorScheme.secondary,\n          );\n  }\n\n  Widget buildTitle(BuildContext context, ViewPB view) {\n    final theme = AppFlowyTheme.of(context);\n    final titleStyle = theme.textStyle.heading4\n            .enhanced(color: theme.textColorScheme.primary),\n        titleHoverStyle =\n            titleStyle.copyWith(decoration: TextDecoration.underline);\n    return LayoutBuilder(\n      builder: (context, constrains) {\n        final maxWidth = constrains.maxWidth;\n        String displayText = view.nameOrDefault;\n        final painter = TextPainter(\n          text: TextSpan(text: displayText, style: titleStyle),\n          maxLines: 3,\n          textDirection: TextDirection.ltr,\n          ellipsis: '...     ',\n        );\n        painter.layout(maxWidth: maxWidth);\n        if (painter.didExceedMaxLines) {\n          final lines = painter.computeLineMetrics();\n          final lastLine = lines.last;\n          final offset = Offset(\n            lastLine.left + lastLine.width,\n            lines.map((e) => e.height).reduce((a, b) => a + b),\n          );\n          final range = painter.getPositionForOffset(offset);\n          displayText = '${displayText.substring(0, range.offset)}...';\n        }\n        return AFBaseButton(\n          borderColor: (_, __, ___, ____) => Colors.transparent,\n          borderRadius: 0,\n          onTap: onViewOpened,\n          padding: EdgeInsets.zero,\n          builder: (context, isHovering, disabled) {\n            return RichText(\n              text: TextSpan(\n                children: [\n                  TextSpan(\n                    text: displayText,\n                    style: isHovering ? titleHoverStyle : titleStyle,\n                  ),\n                  WidgetSpan(\n                    alignment: PlaceholderAlignment.middle,\n                    child: Padding(\n                      padding: const EdgeInsets.only(left: 4),\n                      child: FlowyTooltip(\n                        message: LocaleKeys.settings_files_open.tr(),\n                        child: FlowySvg(\n                          FlowySvgs.search_open_tab_m,\n                          color: theme.iconColorScheme.secondary,\n                          size: const Size.square(20),\n                        ),\n                      ),\n                    ),\n                  ),\n                ],\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  Widget buildPath(BuildContext context, ViewPB view) {\n    final theme = AppFlowyTheme.of(context);\n    return BlocProvider(\n      key: ValueKey(view.id),\n      create: (context) => ViewAncestorBloc(view.id),\n      child: BlocBuilder<ViewAncestorBloc, ViewAncestorState>(\n        builder: (context, state) {\n          final isEmpty = state.ancestor.ancestors.isEmpty;\n          if (!state.isLoading && isEmpty) return const SizedBox.shrink();\n          return Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              VSpace(20),\n              Text(\n                LocaleKeys.commandPalette_location.tr(),\n                style: theme.textStyle.caption\n                    .standard(color: theme.textColorScheme.primary),\n              ),\n              state.buildPath(\n                context,\n                style: theme.textStyle.caption.standard(\n                  color: theme.textColorScheme.secondary,\n                ),\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  List<Widget> buildTime(BuildContext context, String title, DateTime time) {\n    final theme = AppFlowyTheme.of(context);\n    final appearanceSettings = context.watch<AppearanceSettingsCubit>().state;\n    final dateFormat = appearanceSettings.dateFormat,\n        timeFormat = appearanceSettings.timeFormat;\n    return [\n      VSpace(12),\n      Text(\n        title,\n        style: theme.textStyle.caption\n            .standard(color: theme.textColorScheme.primary),\n      ),\n      Text(\n        dateFormat.formatDate(time, true, timeFormat),\n        style: theme.textStyle.caption\n            .standard(color: theme.textColorScheme.secondary),\n      ),\n    ];\n  }\n}\n\nclass SomethingWentWrong extends StatelessWidget {\n  const SomethingWentWrong({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return SizedBox(\n      width: 300,\n      child: Row(\n        children: [\n          AFDivider(axis: Axis.vertical),\n          Expanded(\n            child: Padding(\n              padding: const EdgeInsets.only(bottom: 100),\n              child: Center(\n                child: Column(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    FlowySvg(\n                      FlowySvgs.something_wrong_warning_m,\n                      color: theme.iconColorScheme.secondary,\n                      size: Size.square(24),\n                    ),\n                    const VSpace(8),\n                    Text(\n                      LocaleKeys.search_somethingWentWrong.tr(),\n                      style: theme.textStyle.body\n                          .enhanced(color: theme.textColorScheme.secondary),\n                      maxLines: 1,\n                      overflow: TextOverflow.ellipsis,\n                    ),\n                    const VSpace(4),\n                    Text(\n                      LocaleKeys.search_tryAgainOrLater.tr(),\n                      style: theme.textStyle.caption\n                          .standard(color: theme.textColorScheme.secondary),\n                      maxLines: 1,\n                      overflow: TextOverflow.ellipsis,\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/navigation_bloc_extension.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/search_icon.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pbenum.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'page_preview.dart';\nimport 'search_ask_ai_entrance.dart';\n\nclass RecentViewsList extends StatelessWidget {\n  const RecentViewsList({super.key, required this.onSelected});\n\n  final VoidCallback onSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) =>\n          RecentViewsBloc()..add(const RecentViewsEvent.initial()),\n      child: BlocBuilder<RecentViewsBloc, RecentViewsState>(\n        builder: (context, state) {\n          return LayoutBuilder(\n            builder: (context, constrains) {\n              final maxWidth = constrains.maxWidth;\n              final hidePreview = maxWidth < 884;\n              return Row(\n                children: [\n                  buildLeftPanel(state, context, hidePreview),\n                  if (!hidePreview) buildPreview(state),\n                ],\n              );\n            },\n          );\n        },\n      ),\n    );\n  }\n\n  Widget buildLeftPanel(\n    RecentViewsState state,\n    BuildContext context,\n    bool hidePreview,\n  ) {\n    final workspaceState = context.read<UserWorkspaceBloc?>()?.state;\n    final showAskingAI =\n        workspaceState?.userProfile.workspaceType == WorkspaceTypePB.ServerW;\n    return Flexible(\n      child: Align(\n        alignment: Alignment.topLeft,\n        child: ScrollControllerBuilder(\n          builder: (context, controller) {\n            return Padding(\n              padding: EdgeInsets.only(right: hidePreview ? 0 : 6),\n              child: FlowyScrollbar(\n                controller: controller,\n                thumbVisibility: false,\n                child: SingleChildScrollView(\n                  controller: controller,\n                  physics: const ClampingScrollPhysics(),\n                  child: Padding(\n                    padding: EdgeInsets.only(\n                      right: hidePreview ? 0 : 6,\n                    ),\n                    child: Column(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      children: [\n                        if (showAskingAI) SearchAskAiEntrance(),\n                        buildTitle(context),\n                        buildViewList(state, context, hidePreview),\n                        VSpace(16),\n                      ],\n                    ),\n                  ),\n                ),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  Widget buildTitle(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return Container(\n      padding: EdgeInsets.symmetric(\n        horizontal: theme.spacing.m,\n        vertical: theme.spacing.s,\n      ),\n      child: Text(\n        LocaleKeys.sideBar_recent.tr(),\n        style: theme.textStyle.body\n            .enhanced(color: theme.textColorScheme.secondary)\n            .copyWith(\n              letterSpacing: 0.2,\n              height: 22 / 16,\n            ),\n      ),\n    );\n  }\n\n  Widget buildViewList(\n    RecentViewsState state,\n    BuildContext context,\n    bool hidePreview,\n  ) {\n    final recentViews = state.views.map((e) => e.item).toSet().toList();\n\n    if (recentViews.isEmpty) {\n      return const SizedBox.shrink();\n    }\n    return ListView.builder(\n      shrinkWrap: true,\n      physics: const NeverScrollableScrollPhysics(),\n      itemCount: recentViews.length,\n      itemBuilder: (_, index) {\n        final view = recentViews[index];\n\n        return SearchRecentViewCell(\n          key: ValueKey(view.id),\n          icon: SizedBox.square(\n            dimension: 20,\n            child: Center(child: view.buildIcon(context)),\n          ),\n          view: view,\n          onSelected: onSelected,\n          isNarrowWindow: hidePreview,\n        );\n      },\n    );\n  }\n\n  Widget buildPreview(RecentViewsState state) {\n    final hoveredView = state.hoveredView;\n    if (hoveredView == null) {\n      return SizedBox.shrink();\n    }\n    return Align(\n      alignment: Alignment.topLeft,\n      child: PagePreview(\n        key: ValueKey(hoveredView.id),\n        view: hoveredView,\n        onViewOpened: () {\n          hoveredView.id.navigateTo();\n          onSelected();\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_ask_ai_entrance.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'search_summary_cell.dart';\n\nclass SearchAskAiEntrance extends StatelessWidget {\n  const SearchAskAiEntrance({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final bloc = context.read<CommandPaletteBloc?>(), state = bloc?.state;\n    if (bloc == null || state == null) return _AskAIFor();\n\n    final generatingAIOverview = state.generatingAIOverview;\n    if (generatingAIOverview) return _AISearching();\n\n    final hasMockSummary = _mockSummary?.isNotEmpty ?? false,\n        hasSummaries = state.resultSummaries.isNotEmpty;\n    if (hasMockSummary || hasSummaries) return _AIOverview();\n\n    return _AskAIFor();\n  }\n}\n\nclass _AskAIFor extends StatelessWidget {\n  const _AskAIFor();\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final spaceM = theme.spacing.m, spaceL = theme.spacing.l;\n    return Padding(\n      padding: EdgeInsets.symmetric(vertical: theme.spacing.xs),\n      child: AFBaseButton(\n        borderRadius: spaceM,\n        padding: EdgeInsets.all(spaceL),\n        backgroundColor: (context, isHovering, disable) {\n          if (isHovering) {\n            return theme.fillColorScheme.contentHover;\n          }\n          return Colors.transparent;\n        },\n        borderColor: (context, isHovering, disable, isFocused) =>\n            Colors.transparent,\n        builder: (ctx, isHovering, disable) {\n          return Row(\n            children: [\n              SizedBox.square(\n                dimension: 20,\n                child: Center(\n                  child: FlowySvg(\n                    FlowySvgs.m_home_ai_chat_icon_m,\n                    size: Size.square(20),\n                    blendMode: null,\n                  ),\n                ),\n              ),\n              HSpace(8),\n              buildText(context),\n            ],\n          );\n        },\n        onTap: () {\n          context\n              .read<CommandPaletteBloc?>()\n              ?.add(CommandPaletteEvent.goingToAskAI());\n        },\n      ),\n    );\n  }\n\n  Widget buildText(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final bloc = context.read<CommandPaletteBloc?>();\n    final queryText = bloc?.state.query ?? '';\n    if (queryText.isEmpty) {\n      return Text(\n        LocaleKeys.search_askAIAnything.tr(),\n        style: theme.textStyle.body\n            .enhanced(color: theme.textColorScheme.primary)\n            .copyWith(height: 22 / 14),\n      );\n    }\n    return Flexible(\n      child: RichText(\n        maxLines: 1,\n        overflow: TextOverflow.ellipsis,\n        text: TextSpan(\n          children: [\n            TextSpan(\n              text: LocaleKeys.search_askAIFor.tr(),\n              style: theme.textStyle.body\n                  .standard(color: theme.textColorScheme.primary),\n            ),\n            TextSpan(\n              text: ' \"$queryText\"',\n              style: theme.textStyle.body\n                  .enhanced(color: theme.textColorScheme.primary)\n                  .copyWith(height: 22 / 14),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _AISearching extends StatelessWidget {\n  const _AISearching();\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Padding(\n      padding: EdgeInsets.symmetric(vertical: theme.spacing.xs),\n      child: Padding(\n        padding: EdgeInsets.all(theme.spacing.l),\n        child: Row(\n          children: [\n            SizedBox.square(\n              dimension: 20,\n              child: Center(\n                child: FlowySvg(\n                  FlowySvgs.m_home_ai_chat_icon_m,\n                  size: Size.square(20),\n                  blendMode: null,\n                ),\n              ),\n            ),\n            HSpace(8),\n            Text(\n              LocaleKeys.search_searching.tr(),\n              style: theme.textStyle.body\n                  .standard(color: theme.textColorScheme.secondary)\n                  .copyWith(height: 22 / 14),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _AIOverview extends StatelessWidget {\n  const _AIOverview();\n\n  @override\n  Widget build(BuildContext context) {\n    final bloc = context.read<CommandPaletteBloc?>(), state = bloc?.state;\n    final theme = AppFlowyTheme.of(context);\n    final summaries = _mockSummary ?? state?.resultSummaries ?? [];\n    if (summaries.isEmpty) {\n      return const SizedBox.shrink();\n    }\n    final xl = theme.spacing.xl, m = theme.spacing.m, l = theme.spacing.l;\n    return Padding(\n      padding: EdgeInsets.fromLTRB(m, l, m, xl),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          buildHeader(context),\n          VSpace(theme.spacing.l),\n          LayoutBuilder(\n            builder: (context, constrains) {\n              final summary = summaries.first;\n              return SearchSummaryCell(\n                key: ValueKey(summary.content.trim()),\n                summary: summary,\n                maxWidth: constrains.maxWidth,\n                theme: AppFlowyTheme.of(context),\n                textStyle: theme.textStyle.body\n                    .standard(color: theme.textColorScheme.primary)\n                    .copyWith(height: 22 / 14),\n              );\n            },\n          ),\n          VSpace(12),\n          SizedBox(\n            width: 143,\n            child: AFOutlinedButton.normal(\n              padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),\n              borderRadius: 16,\n              builder: (context, hovering, disabled) {\n                return Row(\n                  children: [\n                    FlowySvg(\n                      FlowySvgs.chat_ai_page_s,\n                      size: Size.square(20),\n                      color: theme.iconColorScheme.primary,\n                    ),\n                    HSpace(6),\n                    Text(\n                      LocaleKeys.commandPalette_aiAskFollowUp.tr(),\n                      style: theme.textStyle.body.enhanced(\n                        color: theme.textColorScheme.primary,\n                      ),\n                    ),\n                  ],\n                );\n              },\n              onTap: () {\n                context.read<CommandPaletteBloc?>()?.add(\n                      CommandPaletteEvent.goingToAskAI(\n                        sources: summaries.first.sources,\n                      ),\n                    );\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget buildHeader(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return Row(\n      children: [\n        FlowySvg(\n          FlowySvgs.ai_searching_icon_m,\n          size: Size.square(20),\n          blendMode: null,\n        ),\n        HSpace(theme.spacing.l),\n        Text(\n          LocaleKeys.commandPalette_aiOverview.tr(),\n          style: theme.textStyle.body\n              .enhanced(color: theme.textColorScheme.secondary)\n              .copyWith(height: 22 / 16, letterSpacing: 0.2),\n        ),\n      ],\n    );\n  }\n}\n\nList<SearchSummaryPB>? _mockSummary;\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text_field.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SearchField extends StatefulWidget {\n  const SearchField({super.key, this.query, this.isLoading = false});\n\n  final String? query;\n  final bool isLoading;\n\n  @override\n  State<SearchField> createState() => _SearchFieldState();\n}\n\nclass _SearchFieldState extends State<SearchField> {\n  late final FocusNode focusNode;\n  late final TextEditingController controller;\n\n  @override\n  void initState() {\n    super.initState();\n    controller = TextEditingController(text: widget.query);\n    focusNode = FocusNode(onKeyEvent: _handleKeyEvent);\n    focusNode.requestFocus();\n    // Update the text selection after the first frame\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      controller.selection = TextSelection(\n        baseOffset: 0,\n        extentOffset: controller.text.length,\n      );\n    });\n  }\n\n  KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) {\n    if (node.hasFocus &&\n        event is KeyDownEvent &&\n        event.logicalKey == LogicalKeyboardKey.arrowDown) {\n      node.nextFocus();\n      return KeyEventResult.handled;\n    }\n    return KeyEventResult.ignored;\n  }\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    controller.dispose();\n    super.dispose();\n  }\n\n  Widget _buildSuffixIcon(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Padding(\n      padding: EdgeInsets.only(left: theme.spacing.m, right: theme.spacing.l),\n      child: FlowyTooltip(\n        message: LocaleKeys.commandPalette_clearSearchTooltip.tr(),\n        child: MouseRegion(\n          cursor: SystemMouseCursors.click,\n          child: GestureDetector(\n            behavior: HitTestBehavior.opaque,\n            onTap: _clearSearch,\n            child: SizedBox.square(\n              dimension: 28,\n              child: Center(\n                child: FlowySvg(\n                  FlowySvgs.search_clear_m,\n                  color: AppFlowyTheme.of(context).iconColorScheme.tertiary,\n                  size: const Size.square(20),\n                ),\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final radius = BorderRadius.circular(theme.spacing.l);\n    final workspace =\n        context.read<UserWorkspaceBloc?>()?.state.currentWorkspace;\n\n    return Container(\n      height: 44,\n      margin: EdgeInsets.only(bottom: theme.spacing.m),\n      child: ValueListenableBuilder<TextEditingValue>(\n        valueListenable: controller,\n        builder: (context, value, _) {\n          final hasText = value.text.trim().isNotEmpty;\n          return FlowyTextField(\n            focusNode: focusNode,\n            cursorHeight: 22,\n            controller: controller,\n            textStyle: theme.textStyle.heading4\n                .standard(color: theme.textColorScheme.primary),\n            decoration: InputDecoration(\n              contentPadding:\n                  EdgeInsets.symmetric(vertical: 11, horizontal: 12),\n              enabledBorder: OutlineInputBorder(\n                borderSide: BorderSide(color: theme.borderColorScheme.primary),\n                borderRadius: radius,\n              ),\n              isDense: false,\n              hintText: LocaleKeys.search_searchFieldHint\n                  .tr(args: ['${workspace?.name}']),\n              hintStyle: theme.textStyle.heading4\n                  .standard(color: theme.textColorScheme.tertiary),\n              hintMaxLines: 1,\n              counterText: \"\",\n              focusedBorder: OutlineInputBorder(\n                borderRadius: radius,\n                borderSide:\n                    BorderSide(color: theme.borderColorScheme.themeThick),\n              ),\n              prefixIcon: Padding(\n                padding: const EdgeInsets.only(left: 12, right: 8),\n                child: FlowySvg(\n                  FlowySvgs.search_icon_m,\n                  color: theme.iconColorScheme.secondary,\n                  size: Size.square(20),\n                ),\n              ),\n              prefixIconConstraints: BoxConstraints.loose(Size(40, 20)),\n              suffixIconConstraints:\n                  hasText ? BoxConstraints.loose(Size(48, 28)) : null,\n              suffixIcon: hasText ? _buildSuffixIcon(context) : null,\n            ),\n            onChanged: (value) => context\n                .read<CommandPaletteBloc>()\n                .add(CommandPaletteEvent.searchChanged(search: value)),\n          );\n        },\n      ),\n    );\n  }\n\n  void _clearSearch() {\n    controller.clear();\n    context\n        .read<CommandPaletteBloc>()\n        .add(const CommandPaletteEvent.clearSearch());\n    focusNode.requestFocus();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_icon.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/workspace/application/command_palette/search_result_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nextension SearchIconExtension on ViewPB {\n  Widget buildIcon(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return icon.value.isNotEmpty\n        ? SizedBox(\n            width: 16,\n            child: RawEmojiIconWidget(\n              emoji: icon.toEmojiIconData(),\n              emojiSize: 16,\n              lineHeight: 20 / 16,\n            ),\n          )\n        : FlowySvg(\n            iconData,\n            size: const Size.square(18),\n            color: theme.iconColorScheme.secondary,\n          );\n  }\n}\n\nextension SearchIconItemExtension on ResultIconPB {\n  Widget? buildIcon(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final color = theme.iconColorScheme.secondary;\n\n    if (ty == ResultIconTypePB.Emoji) {\n      return SizedBox(\n        width: 16,\n        child: getIcon(size: 16, lineHeight: 20 / 16, iconColor: color) ??\n            SizedBox.shrink(),\n      );\n    } else {\n      return getIcon(iconColor: color) ?? SizedBox.shrink();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart",
    "content": "import 'package:appflowy/mobile/presentation/search/mobile_view_ancestors.dart';\nimport 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/navigation_bloc_extension.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SearchRecentViewCell extends StatefulWidget {\n  const SearchRecentViewCell({\n    super.key,\n    required this.icon,\n    required this.view,\n    required this.onSelected,\n    required this.isNarrowWindow,\n  });\n\n  final Widget icon;\n  final ViewPB view;\n  final VoidCallback onSelected;\n  final bool isNarrowWindow;\n\n  @override\n  State<SearchRecentViewCell> createState() => _SearchRecentViewCellState();\n}\n\nclass _SearchRecentViewCellState extends State<SearchRecentViewCell> {\n  final focusNode = FocusNode();\n\n  ViewPB get view => widget.view;\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final spaceL = theme.spacing.l;\n    final bloc = context.read<RecentViewsBloc>(), state = bloc.state;\n    final hoveredView = state.hoveredView, hasHovered = hoveredView != null;\n    final hovering = hoveredView == view;\n\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: () => _handleSelection(view.id),\n      child: Focus(\n        focusNode: focusNode,\n        onKeyEvent: (node, event) {\n          if (event is! KeyDownEvent) return KeyEventResult.ignored;\n          if (event.logicalKey == LogicalKeyboardKey.enter) {\n            _handleSelection(view.id);\n            return KeyEventResult.handled;\n          }\n          return KeyEventResult.ignored;\n        },\n        onFocusChange: (hasFocus) {\n          if (hasFocus && !hovering) {\n            bloc.add(RecentViewsEvent.hoverView(view));\n          }\n        },\n        child: FlowyHover(\n          onHover: (value) {\n            if (hoveredView == view) return;\n            bloc.add(RecentViewsEvent.hoverView(view));\n          },\n          style: HoverStyle(\n            borderRadius: BorderRadius.circular(8),\n            hoverColor: theme.fillColorScheme.contentHover,\n            foregroundColorOnHover: AFThemeExtension.of(context).textColor,\n          ),\n          isSelected: () => hovering,\n          child: Padding(\n            padding: EdgeInsets.all(spaceL),\n            child: Row(\n              children: [\n                widget.icon,\n                HSpace(8),\n                Container(\n                  constraints: BoxConstraints(\n                    maxWidth:\n                        (!widget.isNarrowWindow && hasHovered) ? 480.0 : 680.0,\n                  ),\n                  child: Text(\n                    view.nameOrDefault,\n                    maxLines: 1,\n                    style: theme.textStyle.body\n                        .enhanced(color: theme.textColorScheme.primary)\n                        .copyWith(height: 22 / 14),\n                    overflow: TextOverflow.ellipsis,\n                  ),\n                ),\n                Flexible(child: buildPath(theme)),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget buildPath(AppFlowyThemeData theme) {\n    return BlocProvider(\n      key: ValueKey(view.id),\n      create: (context) => ViewAncestorBloc(view.id),\n      child: BlocBuilder<ViewAncestorBloc, ViewAncestorState>(\n        builder: (context, state) {\n          if (state.ancestor.ancestors.isEmpty) return const SizedBox.shrink();\n          return state.buildOnelinePath(context);\n        },\n      ),\n    );\n  }\n\n  /// Helper to handle the selection action.\n  void _handleSelection(String id) {\n    widget.onSelected();\n    id.navigateTo();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/search/mobile_search_cell.dart';\nimport 'package:appflowy/mobile/presentation/search/mobile_view_ancestors.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';\nimport 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/search_icon.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'page_preview.dart';\n\nclass SearchResultCell extends StatefulWidget {\n  const SearchResultCell({\n    super.key,\n    required this.item,\n    required this.isNarrowWindow,\n    this.view,\n    this.query,\n    this.isHovered = false,\n  });\n\n  final SearchResultItem item;\n  final ViewPB? view;\n  final String? query;\n  final bool isHovered;\n  final bool isNarrowWindow;\n\n  @override\n  State<SearchResultCell> createState() => _SearchResultCellState();\n}\n\nclass _SearchResultCellState extends State<SearchResultCell> {\n  bool _hasFocus = false;\n  final focusNode = FocusNode();\n\n  String get viewId => item.id;\n  SearchResultItem get item => widget.item;\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    super.dispose();\n  }\n\n  /// Helper to handle the selection action.\n  void _handleSelection() {\n    context.read<SearchResultListBloc>().add(\n          SearchResultListEvent.openPage(pageId: viewId),\n        );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final title = item.displayName.orDefault(\n      LocaleKeys.menuAppHeader_defaultNewPageName.tr(),\n    );\n    final searchResultBloc = context.read<SearchResultListBloc>();\n    final hasHovered = searchResultBloc.state.hoveredResult != null;\n\n    final theme = AppFlowyTheme.of(context);\n    final titleStyle = theme.textStyle.body\n        .enhanced(color: theme.textColorScheme.primary)\n        .copyWith(height: 22 / 14);\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: _handleSelection,\n      child: Focus(\n        focusNode: focusNode,\n        onKeyEvent: (node, event) {\n          if (event is! KeyDownEvent) return KeyEventResult.ignored;\n          if (event.logicalKey == LogicalKeyboardKey.enter) {\n            _handleSelection();\n            return KeyEventResult.handled;\n          }\n          return KeyEventResult.ignored;\n        },\n        onFocusChange: (hasFocus) {\n          setState(() {\n            searchResultBloc.add(\n              SearchResultListEvent.onHoverResult(\n                item: item,\n                userHovered: true,\n              ),\n            );\n            _hasFocus = hasFocus;\n          });\n        },\n        child: FlowyHover(\n          onHover: (value) {\n            context.read<SearchResultListBloc>().add(\n                  SearchResultListEvent.onHoverResult(\n                    item: item,\n                    userHovered: true,\n                  ),\n                );\n          },\n          isSelected: () => _hasFocus || widget.isHovered,\n          style: HoverStyle(\n            borderRadius: BorderRadius.circular(8),\n            hoverColor: theme.fillColorScheme.contentHover,\n            foregroundColorOnHover: AFThemeExtension.of(context).textColor,\n          ),\n          child: Padding(\n            padding: EdgeInsets.all(theme.spacing.l),\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Row(\n                  children: [\n                    SizedBox.square(\n                      dimension: 20,\n                      child: Center(child: buildIcon(theme)),\n                    ),\n                    HSpace(8),\n                    Container(\n                      constraints: BoxConstraints(\n                        maxWidth: (!widget.isNarrowWindow && hasHovered)\n                            ? 480.0\n                            : 680.0,\n                      ),\n                      child: RichText(\n                        maxLines: 1,\n                        textAlign: TextAlign.center,\n                        overflow: TextOverflow.ellipsis,\n                        text: buildHighLightSpan(\n                          content: title,\n                          normal: titleStyle,\n                          highlight: titleStyle.copyWith(\n                            backgroundColor: theme.fillColorScheme.themeSelect,\n                          ),\n                        ),\n                      ),\n                    ),\n                    Flexible(child: buildPath(theme)),\n                  ],\n                ),\n                ...buildSummary(theme),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget buildIcon(AppFlowyThemeData theme) {\n    final view = widget.view;\n    if (view != null) return view.buildIcon(context);\n    return item.icon.buildIcon(context) ?? const SizedBox.shrink();\n  }\n\n  Widget buildPath(AppFlowyThemeData theme) {\n    return BlocProvider(\n      key: ValueKey(item.id),\n      create: (context) => ViewAncestorBloc(item.id),\n      child: BlocBuilder<ViewAncestorBloc, ViewAncestorState>(\n        builder: (context, state) {\n          if (state.ancestor.ancestors.isEmpty) return const SizedBox.shrink();\n          return state.buildOnelinePath(context);\n        },\n      ),\n    );\n  }\n\n  TextSpan buildHighLightSpan({\n    required String content,\n    required TextStyle normal,\n    required TextStyle highlight,\n  }) {\n    final queryText = (widget.query ?? '').trim();\n    if (queryText.isEmpty) {\n      return TextSpan(text: content, style: normal);\n    }\n    final contents = content.splitIncludeSeparator(queryText);\n    return TextSpan(\n      children: List.generate(contents.length, (index) {\n        final content = contents[index];\n        final isHighlight = content.toLowerCase() == queryText.toLowerCase();\n        return TextSpan(\n          text: content,\n          style: isHighlight ? highlight : normal,\n        );\n      }),\n    );\n  }\n\n  List<Widget> buildSummary(AppFlowyThemeData theme) {\n    if (item.content.isEmpty) return [];\n    final style = theme.textStyle.caption\n        .standard(color: theme.textColorScheme.secondary)\n        .copyWith(letterSpacing: 0.1);\n    return [\n      VSpace(4),\n      Padding(\n        padding: const EdgeInsets.only(left: 28),\n        child: RichText(\n          maxLines: 2,\n          overflow: TextOverflow.ellipsis,\n          text: buildHighLightSpan(\n            content: item.content,\n            normal: style,\n            highlight: style.copyWith(\n              backgroundColor: theme.fillColorScheme.themeSelect,\n              color: theme.textColorScheme.primary,\n            ),\n          ),\n        ),\n      ),\n    ];\n  }\n}\n\nclass SearchResultPreview extends StatelessWidget {\n  const SearchResultPreview({\n    super.key,\n    required this.view,\n  });\n\n  final ViewPB view;\n\n  @override\n  Widget build(BuildContext context) {\n    return PagePreview(\n      view: view,\n      key: ValueKey(view.id),\n      onViewOpened: () {\n        context\n            .read<SearchResultListBloc?>()\n            ?.add(SearchResultListEvent.openPage(pageId: view.id));\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';\nimport 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/navigation_bloc_extension.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/search_ask_ai_entrance.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pbenum.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'page_preview.dart';\nimport 'search_result_cell.dart';\n\nclass SearchResultList extends StatefulWidget {\n  const SearchResultList({\n    required this.cachedViews,\n    required this.resultItems,\n    required this.resultSummaries,\n    super.key,\n  });\n\n  final Map<String, ViewPB> cachedViews;\n  final List<SearchResultItem> resultItems;\n  final List<SearchSummaryPB> resultSummaries;\n\n  @override\n  State<SearchResultList> createState() => _SearchResultListState();\n}\n\nclass _SearchResultListState extends State<SearchResultList> {\n  late final SearchResultListBloc bloc;\n\n  @override\n  void initState() {\n    super.initState();\n    bloc = SearchResultListBloc();\n  }\n\n  @override\n  void dispose() {\n    bloc.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider.value(\n      value: bloc,\n      child: BlocListener<SearchResultListBloc, SearchResultListState>(\n        listener: (context, state) {\n          final pageId = state.openPageId;\n          if (pageId != null && pageId.isNotEmpty) {\n            FlowyOverlay.pop(context);\n            pageId.navigateTo();\n          }\n        },\n        child: BlocBuilder<SearchResultListBloc, SearchResultListState>(\n          builder: (context, state) {\n            final hasHoverResult = state.hoveredResult != null;\n            return LayoutBuilder(\n              builder: (context, constrains) {\n                final maxWidth = constrains.maxWidth;\n                final hidePreview = maxWidth < 884;\n                return Row(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Expanded(child: _buildResultsSection(context, hidePreview)),\n                    if (!hidePreview && hasHoverResult)\n                      const SearchCellPreview(),\n                  ],\n                );\n              },\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  Widget _buildSectionHeader(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Padding(\n      padding: EdgeInsets.symmetric(\n        vertical: theme.spacing.s,\n        horizontal: theme.spacing.m,\n      ),\n      child: Text(\n        LocaleKeys.commandPalette_bestMatches.tr(),\n        style: theme.textStyle.body\n            .enhanced(color: theme.textColorScheme.secondary)\n            .copyWith(\n              letterSpacing: 0.2,\n              height: 22 / 16,\n            ),\n      ),\n    );\n  }\n\n  Widget _buildResultsSection(BuildContext context, bool hidePreview) {\n    final workspaceState = context.read<UserWorkspaceBloc?>()?.state;\n    final showAskingAI =\n        workspaceState?.userProfile.workspaceType == WorkspaceTypePB.ServerW;\n    if (widget.resultItems.isEmpty) return const SizedBox.shrink();\n    List<SearchResultItem> resultItems = widget.resultItems;\n    final hasCachedViews = widget.cachedViews.isNotEmpty;\n    if (hasCachedViews) {\n      resultItems = widget.resultItems\n          .where((item) => widget.cachedViews[item.id] != null)\n          .toList();\n    }\n    return ScrollControllerBuilder(\n      builder: (context, controller) {\n        final hoveredId = bloc.state.hoveredResult?.id;\n        return Padding(\n          padding: EdgeInsets.only(right: hidePreview ? 0 : 6),\n          child: FlowyScrollbar(\n            controller: controller,\n            thumbVisibility: false,\n            child: SingleChildScrollView(\n              controller: controller,\n              physics: ClampingScrollPhysics(),\n              child: Padding(\n                padding: EdgeInsets.only(\n                  right: hidePreview ? 0 : 6,\n                ),\n                child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    if (showAskingAI) SearchAskAiEntrance(),\n                    Column(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      mainAxisSize: MainAxisSize.min,\n                      children: [\n                        _buildSectionHeader(context),\n                        ListView.builder(\n                          physics: const NeverScrollableScrollPhysics(),\n                          shrinkWrap: true,\n                          itemCount: resultItems.length,\n                          itemBuilder: (_, index) {\n                            final item = resultItems[index];\n                            return SearchResultCell(\n                              key: ValueKey(item.id),\n                              item: item,\n                              isNarrowWindow: hidePreview,\n                              view: widget.cachedViews[item.id],\n                              isHovered: hoveredId == item.id,\n                              query: context\n                                  .read<CommandPaletteBloc?>()\n                                  ?.state\n                                  .query,\n                            );\n                          },\n                        ),\n                        VSpace(16),\n                      ],\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass SearchCellPreview extends StatelessWidget {\n  const SearchCellPreview({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SearchResultListBloc, SearchResultListState>(\n      builder: (context, state) {\n        final hoverdId = state.hoveredResult?.id ?? '';\n        final commandPaletteState = context.read<CommandPaletteBloc>().state;\n        final view = commandPaletteState.cachedViews[hoverdId];\n        if (view != null) {\n          return SearchResultPreview(view: view);\n        }\n        return SomethingWentWrong();\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/search/mobile_search_cell.dart';\nimport 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';\nimport 'package:appflowy/workspace/application/command_palette/search_result_ext.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/navigation_bloc_extension.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/widgets/search_icon.dart';\nimport 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart' hide TextDirection;\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SearchSummaryCell extends StatefulWidget {\n  const SearchSummaryCell({\n    required this.summary,\n    required this.maxWidth,\n    required this.theme,\n    required this.textStyle,\n    super.key,\n  });\n\n  final SearchSummaryPB summary;\n  final double maxWidth;\n  final AppFlowyThemeData theme;\n  final TextStyle textStyle;\n\n  @override\n  State<SearchSummaryCell> createState() => _SearchSummaryCellState();\n}\n\nclass _SearchSummaryCellState extends State<SearchSummaryCell> {\n  late TextPainter _painter;\n  late _TextInfo _textInfo = _TextInfo.normal(summary.content);\n  bool tappedShowMore = false;\n  final maxLines = 5;\n  final popoverController = PopoverController();\n  bool isLinkHovering = false, isReferenceHovering = false;\n\n  SearchSummaryPB get summary => widget.summary;\n  double get maxWidth => widget.maxWidth;\n  AppFlowyThemeData get theme => widget.theme;\n\n  TextStyle get textStyle => widget.textStyle;\n\n  TextStyle get showMoreStyle => theme.textStyle.body.standard(\n        color: theme.textColorScheme.secondary,\n      );\n\n  TextStyle get showMoreUnderlineStyle => theme.textStyle.body.underline(\n        color: theme.textColorScheme.secondary,\n      );\n\n  @override\n  void initState() {\n    super.initState();\n    refreshTextPainter();\n  }\n\n  @override\n  void didUpdateWidget(SearchSummaryCell oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    if (oldWidget.maxWidth != maxWidth) {\n      refreshTextPainter();\n    }\n  }\n\n  @override\n  void dispose() {\n    _painter.dispose();\n    popoverController.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final showReference = summary.sources.isNotEmpty;\n    final query = summary.highlights.isEmpty\n        ? context.read<CommandPaletteBloc?>()?.state.query\n        : summary.highlights;\n\n    return _textInfo.build(\n      context: context,\n      normal: textStyle,\n      more: showMoreStyle,\n      query: query ?? '',\n      moreUnderline: showMoreUnderlineStyle,\n      showMore: () {\n        setState(() {\n          tappedShowMore = true;\n          _textInfo = _TextInfo.normal(summary.content);\n        });\n      },\n      normalWidgetSpan: showReference\n          ? WidgetSpan(\n              child: Padding(\n                padding: const EdgeInsets.only(left: 4),\n                child: MouseRegion(\n                  cursor: SystemMouseCursors.click,\n                  onEnter: (event) {\n                    if (!isLinkHovering) {\n                      isLinkHovering = true;\n                      showPopover();\n                    }\n                  },\n                  onExit: (event) {\n                    if (isLinkHovering) {\n                      isLinkHovering = false;\n                      tryToHidePopover();\n                    }\n                  },\n                  child: buildReferenceIcon(),\n                ),\n              ),\n            )\n          : null,\n    );\n  }\n\n  Widget buildReferenceIcon() {\n    final iconSize = Size(21, 15), placeholderHeight = iconSize.height + 10.0;\n    return AppFlowyPopover(\n      offset: Offset(0, -iconSize.height),\n      constraints:\n          BoxConstraints(maxWidth: 360, maxHeight: 420 + placeholderHeight),\n      direction: PopoverDirection.bottomWithCenterAligned,\n      margin: EdgeInsets.zero,\n      controller: popoverController,\n      decorationColor: Colors.transparent,\n      popoverDecoration: BoxDecoration(),\n      popupBuilder: (popoverContext) => MouseRegion(\n        onEnter: (event) {\n          if (!isReferenceHovering) {\n            isReferenceHovering = true;\n          }\n        },\n        onExit: (event) {\n          if (isReferenceHovering) {\n            isReferenceHovering = false;\n            tryToHidePopover();\n          }\n        },\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            SizedBox(height: placeholderHeight),\n            Flexible(\n              child: ReferenceSources(\n                summary.sources,\n                onClose: () {\n                  hidePopover();\n                  if (context.mounted) FlowyOverlay.pop(context);\n                },\n              ),\n            ),\n          ],\n        ),\n      ),\n      child: Container(\n        width: iconSize.width,\n        height: iconSize.height,\n        decoration: BoxDecoration(\n          color: Theme.of(context).colorScheme.secondary,\n          borderRadius: BorderRadius.circular(6),\n        ),\n        child: FlowySvg(\n          FlowySvgs.toolbar_link_m,\n          color: theme.iconColorScheme.primary,\n          size: Size.square(10),\n        ),\n      ),\n    );\n  }\n\n  void showPopover() {\n    keepEditorFocusNotifier.increase();\n    popoverController.show();\n  }\n\n  void hidePopover() {\n    popoverController.close();\n    keepEditorFocusNotifier.decrease();\n  }\n\n  void tryToHidePopover() {\n    if (isLinkHovering || isReferenceHovering) return;\n    Future.delayed(Duration(milliseconds: 500), () {\n      if (!context.mounted) return;\n      if (isLinkHovering || isReferenceHovering) return;\n      hidePopover();\n    });\n  }\n\n  void refreshTextPainter() {\n    final content = summary.content,\n        ellipsis = ' ...${LocaleKeys.search_seeMore.tr()}';\n    if (!tappedShowMore) {\n      _painter = TextPainter(\n        text: TextSpan(text: content, style: textStyle),\n        textDirection: TextDirection.ltr,\n        maxLines: maxLines,\n        ellipsis: ellipsis,\n      );\n      _painter.layout(maxWidth: maxWidth);\n      if (_painter.didExceedMaxLines) {\n        final lines = _painter.computeLineMetrics();\n        final lastLine = lines.last;\n        final offset = Offset(\n          lastLine.left + lastLine.width,\n          lines.map((e) => e.height).reduce((a, b) => a + b),\n        );\n        final range = _painter.getPositionForOffset(offset);\n        final text = content.substring(0, range.offset);\n        _textInfo = _TextInfo.overflow(text);\n      } else {\n        _textInfo = _TextInfo.normal(content);\n      }\n    }\n  }\n}\n\nclass _TextInfo {\n  _TextInfo({required this.text, required this.isOverflow});\n\n  _TextInfo.normal(this.text) : isOverflow = false;\n\n  _TextInfo.overflow(this.text) : isOverflow = true;\n\n  final String text;\n  final bool isOverflow;\n\n  Widget build({\n    required BuildContext context,\n    required TextStyle normal,\n    required TextStyle more,\n    required TextStyle moreUnderline,\n    required VoidCallback showMore,\n    required String query,\n    WidgetSpan? normalWidgetSpan,\n  }) {\n    final theme = AppFlowyTheme.of(context);\n    if (isOverflow) {\n      return SelectionArea(\n        child: Text.rich(\n          TextSpan(\n            children: [\n              _buildHighLightSpan(\n                content: text,\n                normal: normal,\n                query: query,\n                highlight: normal.copyWith(\n                  backgroundColor: theme.fillColorScheme.themeSelect,\n                ),\n              ),\n              TextSpan(\n                text: ' ...',\n                style: normal,\n              ),\n              WidgetSpan(\n                alignment: PlaceholderAlignment.middle,\n                child: AFBaseButton(\n                  padding: EdgeInsets.zero,\n                  builder: (context, isHovering, disabled) =>\n                      SelectionContainer.disabled(\n                    child: Text(\n                      LocaleKeys.search_seeMore.tr(),\n                      style: isHovering ? moreUnderline : more,\n                    ),\n                  ),\n                  borderColor: (_, __, ___, ____) => Colors.transparent,\n                  borderRadius: 0,\n                  onTap: showMore,\n                ),\n              ),\n            ],\n          ),\n        ),\n      );\n    } else {\n      return SelectionArea(\n        child: Text.rich(\n          TextSpan(\n            children: [\n              _buildHighLightSpan(\n                content: text,\n                normal: normal,\n                query: query,\n                highlight: normal.copyWith(\n                  backgroundColor: theme.fillColorScheme.themeSelect,\n                ),\n              ),\n              if (normalWidgetSpan != null) normalWidgetSpan,\n            ],\n          ),\n        ),\n      );\n    }\n  }\n\n  TextSpan _buildHighLightSpan({\n    required String content,\n    required TextStyle normal,\n    required TextStyle highlight,\n    String? query,\n  }) {\n    final queryText = (query ?? '').trim();\n    if (queryText.isEmpty) {\n      return TextSpan(text: content, style: normal);\n    }\n    final contents = content.splitIncludeSeparator(queryText);\n    return TextSpan(\n      children: List.generate(contents.length, (index) {\n        final content = contents[index];\n        final isHighlight = content.toLowerCase() == queryText.toLowerCase();\n        return TextSpan(\n          text: content,\n          style: isHighlight ? highlight : normal,\n        );\n      }),\n    );\n  }\n}\n\nclass ReferenceSources extends StatelessWidget {\n  const ReferenceSources(\n    this.sources, {\n    super.key,\n    this.onClose,\n  });\n\n  final List<SearchSourcePB> sources;\n  final VoidCallback? onClose;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final bloc = context.read<CommandPaletteBloc?>(), state = bloc?.state;\n\n    return Container(\n      decoration: ShapeDecoration(\n        color: theme.surfaceColorScheme.primary,\n        shape: RoundedRectangleBorder(\n          borderRadius: BorderRadius.circular(theme.spacing.m),\n        ),\n        shadows: theme.shadow.small,\n      ),\n      padding: EdgeInsets.all(8),\n      child: SingleChildScrollView(\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Padding(\n              padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8),\n              child: Text(\n                LocaleKeys.commandPalette_aiOverviewSource.tr(),\n                style: theme.textStyle.body.enhanced(\n                  color: theme.textColorScheme.secondary,\n                ),\n              ),\n            ),\n            ListView.separated(\n              shrinkWrap: true,\n              physics: const NeverScrollableScrollPhysics(),\n              itemBuilder: (context, index) {\n                final source = sources[index];\n                final view = state?.cachedViews[source.id];\n\n                final displayName = source.displayName.isEmpty\n                    ? LocaleKeys.menuAppHeader_defaultNewPageName.tr()\n                    : source.displayName;\n                final spaceM = theme.spacing.m, spaceL = theme.spacing.l;\n\n                return AFBaseButton(\n                  borderRadius: spaceM,\n                  onTap: () {\n                    source.id.navigateTo();\n                    onClose?.call();\n                  },\n                  padding: EdgeInsets.symmetric(\n                    vertical: spaceL,\n                    horizontal: spaceM,\n                  ),\n                  backgroundColor: (context, isHovering, disable) {\n                    if (isHovering) {\n                      return theme.fillColorScheme.contentHover;\n                    }\n                    return Colors.transparent;\n                  },\n                  borderColor: (context, isHovering, disabled, isFocused) =>\n                      Colors.transparent,\n                  builder: (context, isHovering, disabled) => Row(\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      SizedBox.square(\n                        dimension: 20,\n                        child: Center(\n                          child: view?.buildIcon(context) ??\n                              source.icon.buildIcon(context),\n                        ),\n                      ),\n                      HSpace(8),\n                      Flexible(\n                        child: Text(\n                          displayName,\n                          maxLines: 1,\n                          overflow: TextOverflow.ellipsis,\n                          style: theme.textStyle.body.standard(\n                            color: theme.textColorScheme.primary,\n                          ),\n                        ),\n                      ),\n                    ],\n                  ),\n                );\n              },\n              separatorBuilder: (context, index) => AFDivider(),\n              itemCount: sources.length,\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget buildIcon(ResultIconPB icon, AppFlowyThemeData theme) {\n    if (icon.ty == ResultIconTypePB.Emoji) {\n      return icon.getIcon(size: 16, lineHeight: 21 / 16) ?? SizedBox.shrink();\n    } else {\n      return icon.getIcon(iconColor: theme.iconColorScheme.primary) ??\n          SizedBox.shrink();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/af_focus_manager.dart",
    "content": "import 'package:flutter/material.dart';\n\n/// Simple ChangeNotifier that can be listened to, notifies the\n/// application on events that should trigger focus loss.\n///\n/// Eg. lose focus in AppFlowyEditor\n///\nabstract class ShouldLoseFocus with ChangeNotifier {}\n\n/// Private implementation to allow the [AFFocusManager] to\n/// call [notifyListeners] without being directly invokable.\n///\nclass _ShouldLoseFocusImpl extends ShouldLoseFocus {\n  void notify() => notifyListeners();\n}\n\nclass AFFocusManager extends InheritedWidget {\n  AFFocusManager({super.key, required super.child});\n\n  final ShouldLoseFocus loseFocusNotifier = _ShouldLoseFocusImpl();\n\n  void notifyLoseFocus() {\n    (loseFocusNotifier as _ShouldLoseFocusImpl).notify();\n  }\n\n  @override\n  bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;\n\n  static AFFocusManager of(BuildContext context) {\n    final AFFocusManager? result =\n        context.dependOnInheritedWidgetOfExactType<AFFocusManager>();\n\n    assert(result != null, \"AFFocusManager could not be found\");\n    return result!;\n  }\n\n  static AFFocusManager? maybeOf(BuildContext context) {\n    return context.dependOnInheritedWidgetOfExactType<AFFocusManager>();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart",
    "content": "import 'package:appflowy/features/workspace/data/repositories/rust_workspace_repository_impl.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/plugins/blank/blank.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/startup/tasks/memory_leak_detector.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/home/home_bloc.dart';\nimport 'package:appflowy/workspace/application/home/home_setting_bloc.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';\nimport 'package:appflowy/workspace/presentation/home/af_focus_manager.dart';\nimport 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';\nimport 'package:appflowy/workspace/presentation/home/hotkeys.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart';\nimport 'package:appflowy/workspace/presentation/widgets/edit_panel/panel_animation.dart';\nimport 'package:appflowy/workspace/presentation/widgets/float_bubble/question_bubble.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'\n    show UserProfilePB;\nimport 'package:collection/collection.dart';\nimport 'package:flowy_infra_ui/style_widget/container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:sized_context/sized_context.dart';\nimport 'package:styled_widget/styled_widget.dart';\n\nimport '../notifications/notification_panel.dart';\nimport '../widgets/edit_panel/edit_panel.dart';\nimport '../widgets/sidebar_resizer.dart';\nimport 'home_layout.dart';\nimport 'home_stack.dart';\nimport 'menu/sidebar/slider_menu_hover_trigger.dart';\n\nclass DesktopHomeScreen extends StatelessWidget {\n  const DesktopHomeScreen({super.key});\n\n  static const routeName = '/DesktopHomeScreen';\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: Future.wait([\n        FolderEventGetCurrentWorkspaceSetting().send(),\n        getIt<AuthService>().getUser(),\n      ]),\n      builder: (context, snapshots) {\n        if (!snapshots.hasData) {\n          return _buildLoading();\n        }\n\n        final workspaceLatest = snapshots.data?[0].fold(\n          (workspaceLatestPB) => workspaceLatestPB as WorkspaceLatestPB,\n          (error) => null,\n        );\n\n        final userProfile = snapshots.data?[1].fold(\n          (userProfilePB) => userProfilePB as UserProfilePB,\n          (error) => null,\n        );\n\n        // In the unlikely case either of the above is null, eg.\n        // when a workspace is already open this can happen.\n        if (workspaceLatest == null || userProfile == null) {\n          return const WorkspaceFailedScreen();\n        }\n\n        return AFFocusManager(\n          child: MultiBlocProvider(\n            key: ValueKey(userProfile.id),\n            providers: [\n              BlocProvider.value(\n                value: getIt<ReminderBloc>(),\n              ),\n              BlocProvider<TabsBloc>.value(value: getIt<TabsBloc>()),\n              BlocProvider<HomeBloc>(\n                create: (_) =>\n                    HomeBloc(workspaceLatest)..add(const HomeEvent.initial()),\n              ),\n              BlocProvider<HomeSettingBloc>(\n                create: (_) => HomeSettingBloc(\n                  workspaceLatest,\n                  context.read<AppearanceSettingsCubit>(),\n                  context.widthPx,\n                )..add(const HomeSettingEvent.initial()),\n              ),\n              BlocProvider<FavoriteBloc>(\n                create: (context) =>\n                    FavoriteBloc()..add(const FavoriteEvent.initial()),\n              ),\n            ],\n            child: Scaffold(\n              floatingActionButton: enableMemoryLeakDetect\n                  ? const FloatingActionButton(\n                      onPressed: dumpMemoryLeak,\n                      child: Icon(Icons.memory),\n                    )\n                  : null,\n              body: BlocListener<HomeBloc, HomeState>(\n                listenWhen: (p, c) => p.latestView != c.latestView,\n                listener: (context, state) {\n                  final view = state.latestView;\n                  if (view != null) {\n                    // Only open the last opened view if the [TabsState.currentPageManager] current opened plugin is blank and the last opened view is not null.\n                    // 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.\n                    final currentPageManager =\n                        context.read<TabsBloc>().state.currentPageManager;\n\n                    if (currentPageManager.plugin.pluginType ==\n                        PluginType.blank) {\n                      getIt<TabsBloc>().add(\n                        TabsEvent.openPlugin(plugin: view.plugin()),\n                      );\n                    }\n\n                    // switch to the space that contains the last opened view\n                    _switchToSpace(view);\n                  }\n                },\n                child: BlocBuilder<HomeSettingBloc, HomeSettingState>(\n                  buildWhen: (previous, current) => previous != current,\n                  builder: (context, state) => BlocProvider(\n                    create: (_) => UserWorkspaceBloc(\n                      userProfile: userProfile,\n                      repository: RustWorkspaceRepositoryImpl(\n                        userId: userProfile.id,\n                      ),\n                    )..add(UserWorkspaceEvent.initialize()),\n                    child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(\n                      listenWhen: (previous, current) =>\n                          previous.currentWorkspace != current.currentWorkspace,\n                      listener: (context, state) {\n                        if (!context.mounted) return;\n                        final workspaceBloc =\n                            context.read<UserWorkspaceBloc?>();\n                        final spaceBloc = context.read<SpaceBloc?>();\n                        CommandPalette.maybeOf(context)?.updateBlocs(\n                          workspaceBloc: workspaceBloc,\n                          spaceBloc: spaceBloc,\n                        );\n                      },\n                      child: HomeHotKeys(\n                        userProfile: userProfile,\n                        child: FlowyContainer(\n                          Theme.of(context).colorScheme.surface,\n                          child: _buildBody(\n                            context,\n                            userProfile,\n                            workspaceLatest,\n                          ),\n                        ),\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildLoading() =>\n      const Center(child: CircularProgressIndicator.adaptive());\n\n  Widget _buildBody(\n    BuildContext context,\n    UserProfilePB userProfile,\n    WorkspaceLatestPB workspaceSetting,\n  ) {\n    final layout = HomeLayout(context);\n    final homeStack = HomeStack(\n      layout: layout,\n      delegate: DesktopHomeScreenStackAdaptor(context),\n      userProfile: userProfile,\n    );\n    final sidebar = _buildHomeSidebar(\n      context,\n      layout: layout,\n      userProfile: userProfile,\n      workspaceSetting: workspaceSetting,\n    );\n    final notificationPanel = NotificationPanel();\n    final sliderHoverTrigger = SliderMenuHoverTrigger();\n\n    final homeMenuResizer =\n        layout.showMenu ? const SidebarResizer() : const SizedBox.shrink();\n    final editPanel = _buildEditPanel(context, layout: layout);\n\n    return _layoutWidgets(\n      layout: layout,\n      homeStack: homeStack,\n      sidebar: sidebar,\n      editPanel: editPanel,\n      bubble: const QuestionBubble(),\n      homeMenuResizer: homeMenuResizer,\n      notificationPanel: notificationPanel,\n      sliderHoverTrigger: sliderHoverTrigger,\n    );\n  }\n\n  Widget _buildHomeSidebar(\n    BuildContext context, {\n    required HomeLayout layout,\n    required UserProfilePB userProfile,\n    required WorkspaceLatestPB workspaceSetting,\n  }) {\n    final homeMenu = HomeSideBar(\n      userProfile: userProfile,\n      workspaceSetting: workspaceSetting,\n    );\n    return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu));\n  }\n\n  Widget _buildEditPanel(\n    BuildContext context, {\n    required HomeLayout layout,\n  }) {\n    final homeBloc = context.read<HomeSettingBloc>();\n    return BlocBuilder<HomeSettingBloc, HomeSettingState>(\n      buildWhen: (previous, current) =>\n          previous.panelContext != current.panelContext,\n      builder: (context, state) {\n        final panelContext = state.panelContext;\n        if (panelContext == null) {\n          return const SizedBox.shrink();\n        }\n\n        return FocusTraversalGroup(\n          child: RepaintBoundary(\n            child: EditPanel(\n              panelContext: panelContext,\n              onEndEdit: () => homeBloc.add(\n                const HomeSettingEvent.dismissEditPanel(),\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _layoutWidgets({\n    required HomeLayout layout,\n    required Widget sidebar,\n    required Widget homeStack,\n    required Widget editPanel,\n    required Widget bubble,\n    required Widget homeMenuResizer,\n    required Widget notificationPanel,\n    required Widget sliderHoverTrigger,\n  }) {\n    final isSliderbarShowing = layout.showMenu;\n    return Stack(\n      children: [\n        homeStack\n            .constrained(minWidth: 500)\n            .positioned(\n              left: layout.homePageLOffset,\n              right: layout.homePageROffset,\n              bottom: 0,\n              top: 0,\n              animate: true,\n            )\n            .animate(layout.animDuration, Curves.easeOutQuad),\n        bubble\n            .positioned(right: 20, bottom: 16, animate: true)\n            .animate(layout.animDuration, Curves.easeOut),\n        editPanel\n            .animatedPanelX(\n              duration: layout.animDuration.inMilliseconds * 0.001,\n              closeX: layout.editPanelWidth,\n              isClosed: !layout.showEditPanel,\n              curve: Curves.easeOutQuad,\n            )\n            .positioned(\n              top: 0,\n              right: 0,\n              bottom: 0,\n              width: layout.editPanelWidth,\n            ),\n        notificationPanel\n            .animatedPanelX(\n              closeX: -layout.notificationPanelWidth,\n              isClosed: !layout.showNotificationPanel,\n              curve: Curves.easeOutQuad,\n              duration: layout.animDuration.inMilliseconds * 0.001,\n            )\n            .positioned(\n              left: isSliderbarShowing ? layout.menuWidth : 0,\n              top: isSliderbarShowing ? 0 : 52,\n              width: layout.notificationPanelWidth,\n              bottom: 0,\n            ),\n        sidebar\n            .animatedPanelX(\n              closeX: -layout.menuWidth,\n              isClosed: !isSliderbarShowing,\n              curve: Curves.easeOutQuad,\n              duration: layout.animDuration.inMilliseconds * 0.001,\n            )\n            .positioned(left: 0, top: 0, width: layout.menuWidth, bottom: 0),\n        homeMenuResizer\n            .positioned(left: layout.menuWidth)\n            .animate(layout.animDuration, Curves.easeOutQuad),\n      ],\n    );\n  }\n\n  Future<void> _switchToSpace(ViewPB view) async {\n    final ancestors = await ViewBackendService.getViewAncestors(view.id);\n    final space = ancestors.fold(\n      (ancestors) =>\n          ancestors.items.firstWhereOrNull((ancestor) => ancestor.isSpace),\n      (error) => null,\n    );\n    if (space?.id != switchToSpaceNotifier.value?.id) {\n      switchToSpaceNotifier.value = space;\n    }\n  }\n}\n\nclass DesktopHomeScreenStackAdaptor extends HomeStackDelegate {\n  DesktopHomeScreenStackAdaptor(this.buildContext);\n\n  final BuildContext buildContext;\n\n  @override\n  void didDeleteStackWidget(ViewPB view, int? index) {\n    ViewBackendService.getView(view.parentViewId).then(\n      (result) => result.fold(\n        (parentView) {\n          final List<ViewPB> views = parentView.childViews;\n          if (views.isNotEmpty) {\n            ViewPB lastView = views.last;\n            if (index != null && index != 0 && views.length > index - 1) {\n              lastView = views[index - 1];\n            }\n\n            return getIt<TabsBloc>()\n                .add(TabsEvent.openPlugin(plugin: lastView.plugin()));\n          }\n\n          getIt<TabsBloc>()\n              .add(TabsEvent.openPlugin(plugin: BlankPagePlugin()));\n        },\n        (err) => Log.error(err),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/errors/workspace_failed_screen.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/rounded_button.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:package_info_plus/package_info_plus.dart';\n\nclass WorkspaceFailedScreen extends StatefulWidget {\n  const WorkspaceFailedScreen({super.key});\n\n  @override\n  State<WorkspaceFailedScreen> createState() => _WorkspaceFailedScreenState();\n}\n\nclass _WorkspaceFailedScreenState extends State<WorkspaceFailedScreen> {\n  String version = '';\n  final String os = Platform.operatingSystem;\n\n  @override\n  void initState() {\n    super.initState();\n    initVersion();\n  }\n\n  Future<void> initVersion() async {\n    final platformInfo = await PackageInfo.fromPlatform();\n    setState(() {\n      version = platformInfo.version;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      child: Scaffold(\n        body: Center(\n          child: SizedBox(\n            width: 400,\n            child: Column(\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                Text(LocaleKeys.workspace_failedToLoad.tr()),\n                const VSpace(20),\n                Row(\n                  children: [\n                    Flexible(\n                      child: RoundedTextButton(\n                        title:\n                            LocaleKeys.workspace_errorActions_reportIssue.tr(),\n                        height: 40,\n                        onPressed: () => afLaunchUrlString(\n                          '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',\n                        ),\n                      ),\n                    ),\n                    const HSpace(20),\n                    Flexible(\n                      child: RoundedTextButton(\n                        title: LocaleKeys.workspace_errorActions_reachOut.tr(),\n                        height: 40,\n                        onPressed: () =>\n                            afLaunchUrlString('https://discord.gg/JucBXeU2FE'),\n                      ),\n                    ),\n                  ],\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/home_layout.dart",
    "content": "import 'dart:io' show Platform;\nimport 'dart:math';\n\nimport 'package:appflowy/workspace/application/home/home_setting_bloc.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n// ignore: import_of_legacy_library_into_null_safe\nimport 'package:sized_context/sized_context.dart';\n\nimport 'home_sizes.dart';\n\nclass HomeLayout {\n  HomeLayout(BuildContext context) {\n    final homeSetting = context.read<HomeSettingBloc>().state;\n    showEditPanel = homeSetting.panelContext != null;\n\n    menuWidth = max(\n      HomeSizes.minimumSidebarWidth + homeSetting.resizeOffset,\n      HomeSizes.minimumSidebarWidth,\n    );\n\n    final screenWidthPx = context.widthPx;\n    context\n        .read<HomeSettingBloc>()\n        .add(HomeSettingEvent.checkScreenSize(screenWidthPx));\n\n    showMenu = homeSetting.menuStatus == MenuStatus.expanded;\n    if (showMenu) {\n      menuIsDrawer = context.widthPx <= PageBreaks.tabletPortrait;\n    }\n\n    showNotificationPanel = !homeSetting.isNotificationPanelCollapsed;\n\n    homePageLOffset = (showMenu && !menuIsDrawer) ? menuWidth : 0.0;\n\n    menuSpacing = !showMenu && Platform.isMacOS ? 80.0 : 0.0;\n    animDuration = homeSetting.resizeType.duration();\n    editPanelWidth = HomeSizes.editPanelWidth;\n    notificationPanelWidth = MediaQuery.of(context).size.width -\n        (showEditPanel ? editPanelWidth : 0);\n    homePageROffset = showEditPanel ? editPanelWidth : 0;\n  }\n\n  late bool showEditPanel;\n  late double menuWidth;\n  late bool showMenu;\n  late bool menuIsDrawer;\n  late bool showNotificationPanel;\n  late double homePageLOffset;\n  late double menuSpacing;\n  late Duration animDuration;\n  late double editPanelWidth;\n  late double notificationPanelWidth;\n  late double homePageROffset;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart",
    "content": "class HomeSizes {\n  static const double menuAddButtonHeight = 60;\n  static const double topBarHeight = 44;\n  static const double editPanelTopBarHeight = 60;\n  static const double editPanelWidth = 400;\n  static const double notificationPanelWidth = 380;\n  static const double tabBarHeight = 40;\n  static const double tabBarWidth = 200;\n  static const double workspaceSectionHeight = 32;\n  static const double searchSectionHeight = 30;\n  static const double newPageSectionHeight = 30;\n  static const double minimumSidebarWidth = 268;\n}\n\nclass HomeInsets {\n  static const double topBarTitleHorizontalPadding = 12;\n  static const double topBarTitleVerticalPadding = 12;\n}\n\nclass HomeSpaceViewSizes {\n  static const double leftPadding = 16.0;\n  static const double viewHeight = 30.0;\n\n  // mobile, m represents mobile\n  static const double mViewHeight = 48.0;\n  static const double mViewButtonDimension = 34.0;\n  static const double mHorizontalPadding = 20.0;\n  static const double mVerticalPadding = 12.0;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\nimport 'dart:math';\n\nimport 'package:appflowy/core/frameless_window.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/blank/blank.dart';\nimport 'package:appflowy/shared/window_title_bar.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/home/home_setting_bloc.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy/workspace/presentation/home/navigation.dart';\nimport 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';\nimport 'package:appflowy/workspace/presentation/home/toast.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/scheduler.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\nimport 'package:time/time.dart';\nimport 'package:universal_platform/universal_platform.dart';\nimport 'package:window_manager/window_manager.dart';\n\nimport 'home_layout.dart';\n\ntypedef NavigationCallback = void Function(String id);\n\nabstract class HomeStackDelegate {\n  void didDeleteStackWidget(ViewPB view, int? index);\n}\n\nclass HomeStack extends StatefulWidget {\n  const HomeStack({\n    super.key,\n    required this.delegate,\n    required this.layout,\n    required this.userProfile,\n  });\n\n  final HomeStackDelegate delegate;\n  final HomeLayout layout;\n  final UserProfilePB userProfile;\n\n  @override\n  State<HomeStack> createState() => _HomeStackState();\n}\n\nclass _HomeStackState extends State<HomeStack> with WindowListener {\n  int selectedIndex = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<TabsBloc>.value(\n      value: getIt<TabsBloc>(),\n      child: BlocBuilder<TabsBloc, TabsState>(\n        builder: (context, state) => Column(\n          children: [\n            if (UniversalPlatform.isWindows)\n              WindowTitleBar(leftChildren: [_buildToggleMenuButton(context)]),\n            Padding(\n              padding: EdgeInsets.only(left: widget.layout.menuSpacing),\n              child: TabsManager(\n                onIndexChanged: (index) {\n                  if (selectedIndex != index) {\n                    // Unfocus editor to hide selection toolbar\n                    FocusScope.of(context).unfocus();\n\n                    context.read<TabsBloc>().add(TabsEvent.selectTab(index));\n                    setState(() => selectedIndex = index);\n                  }\n                },\n              ),\n            ),\n            Expanded(\n              child: IndexedStack(\n                index: selectedIndex,\n                children: state.pageManagers\n                    .map(\n                      (pm) => LayoutBuilder(\n                        builder: (context, constraints) {\n                          return Row(\n                            children: [\n                              Expanded(\n                                child: Column(\n                                  children: [\n                                    pm.stackTopBar(layout: widget.layout),\n                                    Expanded(\n                                      child: PageStack(\n                                        pageManager: pm,\n                                        delegate: widget.delegate,\n                                        userProfile: widget.userProfile,\n                                      ),\n                                    ),\n                                  ],\n                                ),\n                              ),\n                              SecondaryView(\n                                pageManager: pm,\n                                adaptedPercentageWidth:\n                                    constraints.maxWidth * 3 / 7,\n                              ),\n                            ],\n                          );\n                        },\n                      ),\n                    )\n                    .toList(),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildToggleMenuButton(BuildContext context) {\n    if (context.read<HomeSettingBloc>().isMenuExpanded) {\n      return const SizedBox.shrink();\n    }\n\n    final textSpan = TextSpan(\n      children: [\n        TextSpan(\n          text: '${LocaleKeys.sideBar_openSidebar.tr()}\\n',\n          style: context.tooltipTextStyle(),\n        ),\n        TextSpan(\n          text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\\\',\n          style: context\n              .tooltipTextStyle()\n              ?.copyWith(color: Theme.of(context).hintColor),\n        ),\n      ],\n    );\n\n    return FlowyTooltip(\n      richMessage: textSpan,\n      child: Listener(\n        behavior: HitTestBehavior.translucent,\n        onPointerDown: (_) {\n          final isMenuExpanded = context.read<HomeSettingBloc>().isMenuExpanded;\n          final status =\n              isMenuExpanded ? MenuStatus.hidden : MenuStatus.expanded;\n          context\n              .read<HomeSettingBloc>()\n              .add(HomeSettingEvent.changeMenuStatus(status));\n        },\n        child: FlowyHover(\n          child: Container(\n            width: 24,\n            padding: const EdgeInsets.all(4),\n            child: const RotatedBox(\n              quarterTurns: 2,\n              child: FlowySvg(FlowySvgs.hide_menu_s),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  @override\n  void onWindowFocus() {\n    // https://pub.dev/packages/window_manager#windows\n    // must call setState once when the window is focused\n    setState(() {});\n  }\n}\n\nclass PageStack extends StatefulWidget {\n  const PageStack({\n    super.key,\n    required this.pageManager,\n    required this.delegate,\n    required this.userProfile,\n  });\n\n  final PageManager pageManager;\n  final HomeStackDelegate delegate;\n  final UserProfilePB userProfile;\n\n  @override\n  State<PageStack> createState() => _PageStackState();\n}\n\nclass _PageStackState extends State<PageStack>\n    with AutomaticKeepAliveClientMixin {\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n\n    return Container(\n      color: Theme.of(context).colorScheme.surface,\n      child: FocusTraversalGroup(\n        child: widget.pageManager.stackWidget(\n          userProfile: widget.userProfile,\n          onDeleted: (view, index) {\n            widget.delegate.didDeleteStackWidget(view, index);\n          },\n        ),\n      ),\n    );\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n}\n\nclass SecondaryView extends StatefulWidget {\n  const SecondaryView({\n    super.key,\n    required this.pageManager,\n    required this.adaptedPercentageWidth,\n  });\n\n  final PageManager pageManager;\n  final double adaptedPercentageWidth;\n\n  @override\n  State<SecondaryView> createState() => _SecondaryViewState();\n}\n\nclass _SecondaryViewState extends State<SecondaryView>\n    with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {\n  final overlayController = OverlayPortalController();\n  final layerLink = LayerLink();\n\n  late final ValueNotifier<double> widthNotifier;\n\n  late final AnimationController animationController;\n  late Animation<double> widthAnimation;\n  late final Animation<Offset> offsetAnimation;\n\n  late bool hasSecondaryView;\n\n  CurvedAnimation get curveAnimation => CurvedAnimation(\n        parent: animationController,\n        curve: Curves.easeOut,\n      );\n\n  @override\n  void initState() {\n    super.initState();\n    widget.pageManager.showSecondaryPluginNotifier\n        .addListener(onShowSecondaryChanged);\n    final width = widget.pageManager.showSecondaryPluginNotifier.value\n        ? max(450.0, widget.adaptedPercentageWidth)\n        : 0.0;\n    widthNotifier = ValueNotifier<double>(width)\n      ..addListener(updateWidthAnimation);\n\n    animationController = AnimationController(\n      duration: const Duration(milliseconds: 300),\n      vsync: this,\n    );\n\n    widthAnimation = Tween<double>(\n      begin: 0.0,\n      end: width,\n    ).animate(curveAnimation);\n    offsetAnimation = Tween<Offset>(\n      begin: const Offset(1.0, 0.0),\n      end: Offset.zero,\n    ).animate(curveAnimation);\n\n    widget.pageManager.secondaryNotifier.addListener(onSecondaryViewChanged);\n    onSecondaryViewChanged();\n\n    overlayController.show();\n  }\n\n  @override\n  void dispose() {\n    widget.pageManager.showSecondaryPluginNotifier\n        .removeListener(onShowSecondaryChanged);\n    widget.pageManager.secondaryNotifier.removeListener(onSecondaryViewChanged);\n    widthNotifier.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n    final isLightMode = Theme.of(context).isLightMode;\n    return OverlayPortal(\n      controller: overlayController,\n      overlayChildBuilder: (context) {\n        return ValueListenableBuilder(\n          valueListenable: widget.pageManager.showSecondaryPluginNotifier,\n          builder: (context, isShowing, child) {\n            return CompositedTransformFollower(\n              link: layerLink,\n              followerAnchor: Alignment.topRight,\n              offset: const Offset(0.0, 120.0),\n              child: Align(\n                alignment: AlignmentDirectional.topEnd,\n                child: AnimatedSwitcher(\n                  duration: 150.milliseconds,\n                  transitionBuilder: (child, animation) {\n                    return NonClippingSizeTransition(\n                      sizeFactor: animation,\n                      axis: Axis.horizontal,\n                      axisAlignment: -1,\n                      child: child,\n                    );\n                  },\n                  child: isShowing || !hasSecondaryView\n                      ? const SizedBox.shrink()\n                      : GestureDetector(\n                          onTap: () => widget.pageManager\n                              .showSecondaryPluginNotifier.value = true,\n                          child: Container(\n                            height: 36,\n                            width: 36,\n                            decoration: BoxDecoration(\n                              borderRadius: getBorderRadius(),\n                              color: Theme.of(context).colorScheme.surface,\n                              boxShadow: [\n                                BoxShadow(\n                                  offset: const Offset(0, 4),\n                                  blurRadius: 20,\n                                  color: isLightMode\n                                      ? const Color(0x1F1F2329)\n                                      : Theme.of(context)\n                                          .shadowColor\n                                          .withValues(alpha: 0.08),\n                                ),\n                              ],\n                            ),\n                            child: FlowyHover(\n                              style: HoverStyle(\n                                borderRadius: getBorderRadius(),\n                                border: getBorder(context),\n                              ),\n                              child: const Center(\n                                child: FlowySvg(\n                                  FlowySvgs.rename_s,\n                                  size: Size.square(16.0),\n                                ),\n                              ),\n                            ),\n                          ),\n                        ),\n                ),\n              ),\n            );\n          },\n        );\n      },\n      child: CompositedTransformTarget(\n        link: layerLink,\n        child: Container(\n          color: Theme.of(context).colorScheme.surface,\n          child: FocusTraversalGroup(\n            child: ValueListenableBuilder(\n              valueListenable: widthNotifier,\n              builder: (context, value, child) {\n                return AnimatedBuilder(\n                  animation: Listenable.merge([\n                    widthAnimation,\n                    offsetAnimation,\n                  ]),\n                  builder: (context, child) {\n                    return Container(\n                      width: widthAnimation.value,\n                      alignment: Alignment(\n                        offsetAnimation.value.dx,\n                        offsetAnimation.value.dy,\n                      ),\n                      child: OverflowBox(\n                        alignment: AlignmentDirectional.centerStart,\n                        maxWidth: value,\n                        child: SecondaryViewResizer(\n                          pageManager: widget.pageManager,\n                          notifier: widthNotifier,\n                          child: Column(\n                            children: [\n                              widget.pageManager.stackSecondaryTopBar(value),\n                              Expanded(\n                                child: widget.pageManager\n                                    .stackSecondaryWidget(value),\n                              ),\n                            ],\n                          ),\n                        ),\n                      ),\n                    );\n                  },\n                );\n              },\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  BoxBorder getBorder(BuildContext context) {\n    final isLightMode = Theme.of(context).isLightMode;\n    final borderSide = BorderSide(\n      color: isLightMode\n          ? const Color(0x141F2329)\n          : Theme.of(context).dividerColor,\n    );\n\n    return Border(\n      left: borderSide,\n      top: borderSide,\n      bottom: borderSide,\n    );\n  }\n\n  BorderRadius getBorderRadius() {\n    return const BorderRadius.only(\n      topLeft: Radius.circular(12.0),\n      bottomLeft: Radius.circular(12.0),\n    );\n  }\n\n  void onSecondaryViewChanged() {\n    hasSecondaryView = widget.pageManager.secondaryNotifier.plugin.pluginType !=\n        PluginType.blank;\n  }\n\n  void onShowSecondaryChanged() async {\n    if (widget.pageManager.showSecondaryPluginNotifier.value) {\n      widthNotifier.value = max(450.0, widget.adaptedPercentageWidth);\n      updateWidthAnimation();\n      await animationController.forward();\n    } else {\n      updateWidthAnimation();\n      await animationController.reverse();\n      setState(() => widthNotifier.value = 0.0);\n    }\n  }\n\n  void updateWidthAnimation() {\n    widthAnimation = Tween<double>(\n      begin: 0.0,\n      end: widthNotifier.value,\n    ).animate(curveAnimation);\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n}\n\nclass SecondaryViewResizer extends StatefulWidget {\n  const SecondaryViewResizer({\n    super.key,\n    required this.pageManager,\n    required this.notifier,\n    required this.child,\n  });\n\n  final PageManager pageManager;\n  final ValueNotifier<double> notifier;\n  final Widget child;\n\n  @override\n  State<SecondaryViewResizer> createState() => _SecondaryViewResizerState();\n}\n\nclass _SecondaryViewResizerState extends State<SecondaryViewResizer> {\n  final overlayController = OverlayPortalController();\n  final layerLink = LayerLink();\n\n  bool isHover = false;\n  bool isDragging = false;\n  Timer? showHoverTimer;\n\n  @override\n  void initState() {\n    super.initState();\n    overlayController.show();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return OverlayPortal(\n      controller: overlayController,\n      overlayChildBuilder: (context) {\n        return CompositedTransformFollower(\n          showWhenUnlinked: false,\n          link: layerLink,\n          targetAnchor: Alignment.center,\n          followerAnchor: Alignment.center,\n          child: Center(\n            child: MouseRegion(\n              cursor: SystemMouseCursors.resizeLeftRight,\n              onEnter: (_) {\n                showHoverTimer = Timer(const Duration(milliseconds: 500), () {\n                  setState(() => isHover = true);\n                });\n              },\n              onExit: (_) {\n                showHoverTimer?.cancel();\n                setState(() => isHover = false);\n              },\n              child: GestureDetector(\n                behavior: HitTestBehavior.opaque,\n                onHorizontalDragStart: (_) => setState(() => isDragging = true),\n                onHorizontalDragUpdate: (details) {\n                  final newWidth = MediaQuery.sizeOf(context).width -\n                      details.globalPosition.dx;\n                  if (newWidth >= 450.0) {\n                    widget.notifier.value = newWidth;\n                  }\n                },\n                onHorizontalDragEnd: (_) => setState(() => isDragging = false),\n                child: TweenAnimationBuilder(\n                  tween: ColorTween(\n                    end: isHover || isDragging\n                        ? Theme.of(context).colorScheme.primary\n                        : Colors.transparent,\n                  ),\n                  duration: const Duration(milliseconds: 100),\n                  builder: (context, color, child) {\n                    return SizedBox(\n                      width: 11,\n                      child: Center(\n                        child: Container(\n                          color: color,\n                          width: 2,\n                        ),\n                      ),\n                    );\n                  },\n                ),\n              ),\n            ),\n          ),\n        );\n      },\n      child: Row(\n        crossAxisAlignment: CrossAxisAlignment.stretch,\n        children: [\n          CompositedTransformTarget(\n            link: layerLink,\n            child: Container(\n              width: 1,\n              color: Theme.of(context).dividerColor,\n            ),\n          ),\n          Flexible(child: widget.child),\n        ],\n      ),\n    );\n  }\n}\n\nclass FadingIndexedStack extends StatefulWidget {\n  const FadingIndexedStack({\n    super.key,\n    required this.index,\n    required this.children,\n    this.duration = const Duration(milliseconds: 250),\n  });\n\n  final int index;\n  final List<Widget> children;\n  final Duration duration;\n\n  @override\n  FadingIndexedStackState createState() => FadingIndexedStackState();\n}\n\nclass FadingIndexedStackState extends State<FadingIndexedStack> {\n  double _targetOpacity = 1;\n\n  @override\n  void initState() {\n    super.initState();\n    initToastWithContext(context);\n  }\n\n  @override\n  void didUpdateWidget(FadingIndexedStack oldWidget) {\n    if (oldWidget.index == widget.index) return;\n    _targetOpacity = 0;\n    SchedulerBinding.instance.addPostFrameCallback(\n      (_) => setState(() => _targetOpacity = 1),\n    );\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return TweenAnimationBuilder<double>(\n      duration: _targetOpacity > 0 ? widget.duration : 0.milliseconds,\n      tween: Tween(begin: 0, end: _targetOpacity),\n      builder: (_, value, child) => Opacity(opacity: value, child: child),\n      child: IndexedStack(index: widget.index, children: widget.children),\n    );\n  }\n}\n\nabstract mixin class NavigationItem {\n  String? get viewName;\n  Widget get leftBarItem;\n  Widget? get rightBarItem => null;\n  Widget tabBarItem(String pluginId, [bool shortForm = false]);\n\n  NavigationCallback get action => (id) => throw UnimplementedError();\n}\n\nclass PageNotifier extends ChangeNotifier {\n  PageNotifier({Plugin? plugin})\n      : _plugin = plugin ?? makePlugin(pluginType: PluginType.blank);\n\n  Plugin _plugin;\n\n  Widget get titleWidget => _plugin.widgetBuilder.leftBarItem;\n\n  Widget tabBarWidget(\n    String pluginId, [\n    bool shortForm = false,\n  ]) =>\n      _plugin.widgetBuilder.tabBarItem(pluginId, shortForm);\n\n  void setPlugin(\n    Plugin newPlugin, {\n    required bool setLatest,\n    bool disposeExisting = true,\n  }) {\n    if (newPlugin.id != plugin.id && disposeExisting) {\n      _plugin.dispose();\n    }\n\n    // Set the plugin view as the latest view.\n    if (setLatest && newPlugin.id.isNotEmpty) {\n      FolderEventSetLatestView(ViewIdPB(value: newPlugin.id)).send();\n    }\n\n    _plugin = newPlugin;\n    notifyListeners();\n  }\n\n  Plugin get plugin => _plugin;\n}\n\n// PageManager manages the view for one Tab\nclass PageManager {\n  PageManager();\n\n  final PageNotifier _notifier = PageNotifier();\n  final PageNotifier _secondaryNotifier = PageNotifier();\n\n  PageNotifier get notifier => _notifier;\n  PageNotifier get secondaryNotifier => _secondaryNotifier;\n\n  bool isPinned = false;\n\n  final showSecondaryPluginNotifier = ValueNotifier(false);\n\n  Plugin get plugin => _notifier.plugin;\n\n  void setPlugin(Plugin newPlugin, bool setLatest, [bool init = true]) {\n    if (init) {\n      newPlugin.init();\n    }\n    _notifier.setPlugin(newPlugin, setLatest: setLatest);\n  }\n\n  void setSecondaryPlugin(Plugin newPlugin) {\n    newPlugin.init();\n    _secondaryNotifier.setPlugin(newPlugin, setLatest: false);\n  }\n\n  void expandSecondaryPlugin() {\n    _notifier.setPlugin(_secondaryNotifier.plugin, setLatest: true);\n    _secondaryNotifier.setPlugin(\n      BlankPagePlugin(),\n      setLatest: false,\n      disposeExisting: false,\n    );\n  }\n\n  void showSecondaryPlugin() {\n    showSecondaryPluginNotifier.value = true;\n  }\n\n  void hideSecondaryPlugin() {\n    showSecondaryPluginNotifier.value = false;\n  }\n\n  Widget stackTopBar({required HomeLayout layout}) {\n    return ChangeNotifierProvider.value(\n      value: _notifier,\n      child: Selector<PageNotifier, Widget>(\n        selector: (context, notifier) => notifier.titleWidget,\n        builder: (_, __, child) => MoveWindowDetector(\n          child: HomeTopBar(layout: layout),\n        ),\n      ),\n    );\n  }\n\n  Widget stackWidget({\n    required UserProfilePB userProfile,\n    required Function(ViewPB, int?) onDeleted,\n  }) {\n    return ChangeNotifierProvider.value(\n      value: _notifier,\n      child: Consumer<PageNotifier>(\n        builder: (_, notifier, __) {\n          if (notifier.plugin.pluginType == PluginType.blank) {\n            return const BlankPage();\n          }\n\n          return FadingIndexedStack(\n            index: getIt<PluginSandbox>().indexOf(notifier.plugin.pluginType),\n            children: getIt<PluginSandbox>().supportPluginTypes.map(\n              (pluginType) {\n                if (pluginType == notifier.plugin.pluginType) {\n                  final builder = notifier.plugin.widgetBuilder;\n                  final pluginWidget = builder.buildWidget(\n                    context: PluginContext(\n                      onDeleted: onDeleted,\n                      userProfile: userProfile,\n                    ),\n                    shrinkWrap: false,\n                  );\n\n                  return Padding(\n                    padding: builder.contentPadding,\n                    child: pluginWidget,\n                  );\n                }\n\n                return const BlankPage();\n              },\n            ).toList(),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget stackSecondaryWidget(double width) {\n    return ValueListenableBuilder(\n      valueListenable: showSecondaryPluginNotifier,\n      builder: (context, value, child) {\n        if (width == 0.0) {\n          return const SizedBox.shrink();\n        }\n\n        return child!;\n      },\n      child: ChangeNotifierProvider.value(\n        value: _secondaryNotifier,\n        child: Selector<PageNotifier, PluginWidgetBuilder>(\n          selector: (context, notifier) => notifier.plugin.widgetBuilder,\n          builder: (_, widgetBuilder, __) {\n            return widgetBuilder.buildWidget(\n              context: PluginContext(),\n              shrinkWrap: false,\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  Widget stackSecondaryTopBar(double width) {\n    return ValueListenableBuilder(\n      valueListenable: showSecondaryPluginNotifier,\n      builder: (context, value, child) {\n        if (width == 0.0) {\n          return const SizedBox.shrink();\n        }\n\n        return child!;\n      },\n      child: ChangeNotifierProvider.value(\n        value: _secondaryNotifier,\n        child: Selector<PageNotifier, PluginWidgetBuilder>(\n          selector: (context, notifier) => notifier.plugin.widgetBuilder,\n          builder: (_, widgetBuilder, __) {\n            return const MoveWindowDetector(\n              child: HomeSecondaryTopBar(),\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  void dispose() {\n    _notifier.dispose();\n    _secondaryNotifier.dispose();\n    showSecondaryPluginNotifier.dispose();\n  }\n}\n\nclass HomeTopBar extends StatefulWidget {\n  const HomeTopBar({super.key, required this.layout});\n\n  final HomeLayout layout;\n\n  @override\n  State<HomeTopBar> createState() => _HomeTopBarState();\n}\n\nclass _HomeTopBarState extends State<HomeTopBar>\n    with AutomaticKeepAliveClientMixin {\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n\n    return Container(\n      decoration: BoxDecoration(\n        color: Theme.of(context).colorScheme.surface,\n      ),\n      height: HomeSizes.topBarHeight + HomeInsets.topBarTitleVerticalPadding,\n      child: Padding(\n        padding: const EdgeInsets.symmetric(\n          horizontal: HomeInsets.topBarTitleHorizontalPadding,\n          vertical: HomeInsets.topBarTitleVerticalPadding,\n        ),\n        child: Row(\n          children: [\n            HSpace(widget.layout.menuSpacing),\n            const FlowyNavigation(),\n            const HSpace(16),\n            ChangeNotifierProvider.value(\n              value: Provider.of<PageNotifier>(context, listen: false),\n              child: Consumer(\n                builder: (_, PageNotifier notifier, __) =>\n                    notifier.plugin.widgetBuilder.rightBarItem ??\n                    const SizedBox.shrink(),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  @override\n  bool get wantKeepAlive => true;\n}\n\nclass HomeSecondaryTopBar extends StatelessWidget {\n  const HomeSecondaryTopBar({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      decoration: BoxDecoration(\n        color: Theme.of(context).colorScheme.surfaceContainerHighest,\n      ),\n      height: HomeSizes.topBarHeight + HomeInsets.topBarTitleVerticalPadding,\n      child: Padding(\n        padding: const EdgeInsets.symmetric(\n          horizontal: HomeInsets.topBarTitleHorizontalPadding,\n          vertical: HomeInsets.topBarTitleVerticalPadding,\n        ),\n        child: Row(\n          children: [\n            FlowyIconButton(\n              width: 24,\n              tooltipText: LocaleKeys.sideBar_closeSidebar.tr(),\n              radius: const BorderRadius.all(Radius.circular(8.0)),\n              icon: const FlowySvg(\n                FlowySvgs.show_menu_s,\n                size: Size.square(16),\n              ),\n              onPressed: () {\n                getIt<TabsBloc>().add(const TabsEvent.closeSecondaryPlugin());\n              },\n            ),\n            const HSpace(8.0),\n            FlowyIconButton(\n              width: 24,\n              tooltipText: LocaleKeys.sideBar_expandSidebar.tr(),\n              radius: const BorderRadius.all(Radius.circular(8.0)),\n              icon: const FlowySvg(\n                FlowySvgs.full_view_s,\n                size: Size.square(16),\n              ),\n              onPressed: () {\n                getIt<TabsBloc>().add(const TabsEvent.expandSecondaryPlugin());\n              },\n            ),\n            Expanded(\n              child: Align(\n                alignment: AlignmentDirectional.centerEnd,\n                child: ChangeNotifierProvider.value(\n                  value: Provider.of<PageNotifier>(context, listen: false),\n                  child: Consumer(\n                    builder: (_, PageNotifier notifier, __) =>\n                        notifier.plugin.widgetBuilder.rightBarItem ??\n                        const SizedBox.shrink(),\n                  ),\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\n/// A version of Flutter's built in SizeTransition widget that clips the child\n/// more sparingly than the original.\nclass NonClippingSizeTransition extends AnimatedWidget {\n  const NonClippingSizeTransition({\n    super.key,\n    this.axis = Axis.vertical,\n    required Animation<double> sizeFactor,\n    this.axisAlignment = 0.0,\n    this.fixedCrossAxisSizeFactor,\n    this.child,\n  })  : assert(\n          fixedCrossAxisSizeFactor == null || fixedCrossAxisSizeFactor >= 0.0,\n        ),\n        super(listenable: sizeFactor);\n\n  /// [Axis.horizontal] if [sizeFactor] modifies the width, otherwise\n  /// [Axis.vertical].\n  final Axis axis;\n\n  /// The animation that controls the (clipped) size of the child.\n  ///\n  /// The width or height (depending on the [axis] value) of this widget will be\n  /// its intrinsic width or height multiplied by [sizeFactor]'s value at the\n  /// current point in the animation.\n  ///\n  /// If the value of [sizeFactor] is less than one, the child will be clipped\n  /// in the appropriate axis.\n  Animation<double> get sizeFactor => listenable as Animation<double>;\n\n  /// Describes how to align the child along the axis that [sizeFactor] is\n  /// modifying.\n  ///\n  /// A value of -1.0 indicates the top when [axis] is [Axis.vertical], and the\n  /// start when [axis] is [Axis.horizontal]. The start is on the left when the\n  /// text direction in effect is [TextDirection.ltr] and on the right when it\n  /// is [TextDirection.rtl].\n  ///\n  /// A value of 1.0 indicates the bottom or end, depending upon the [axis].\n  ///\n  /// A value of 0.0 (the default) indicates the center for either [axis] value.\n  final double axisAlignment;\n\n  /// The factor by which to multiply the cross axis size of the child.\n  ///\n  /// If the value of [fixedCrossAxisSizeFactor] is less than one, the child\n  /// will be clipped along the appropriate axis.\n  ///\n  /// If `null` (the default), the cross axis size is as large as the parent.\n  final double? fixedCrossAxisSizeFactor;\n\n  /// The widget below this widget in the tree.\n  ///\n  /// {@macro flutter.widgets.ProxyWidget.child}\n  final Widget? child;\n\n  @override\n  Widget build(BuildContext context) {\n    final AlignmentDirectional alignment;\n    final Edge edge;\n    if (axis == Axis.vertical) {\n      alignment = AlignmentDirectional(-1.0, axisAlignment);\n      edge = switch (axisAlignment) { -1.0 => Edge.bottom, _ => Edge.top };\n    } else {\n      alignment = AlignmentDirectional(axisAlignment, -1.0);\n      edge = switch (axisAlignment) { -1.0 => Edge.right, _ => Edge.left };\n    }\n    return ClipRect(\n      clipper: EdgeRectClipper(edge: edge, margin: 20),\n      child: Align(\n        alignment: alignment,\n        heightFactor: axis == Axis.vertical\n            ? max(sizeFactor.value, 0.0)\n            : fixedCrossAxisSizeFactor,\n        widthFactor: axis == Axis.horizontal\n            ? max(sizeFactor.value, 0.0)\n            : fixedCrossAxisSizeFactor,\n        child: child,\n      ),\n    );\n  }\n}\n\nclass EdgeRectClipper extends CustomClipper<Rect> {\n  const EdgeRectClipper({\n    required this.edge,\n    required this.margin,\n  });\n\n  final Edge edge;\n  final double margin;\n\n  @override\n  Rect getClip(Size size) {\n    return switch (edge) {\n      Edge.left =>\n        Rect.fromLTRB(0.0, -margin, size.width + margin, size.height + margin),\n      Edge.right =>\n        Rect.fromLTRB(-margin, -margin, size.width, size.height + margin),\n      Edge.top =>\n        Rect.fromLTRB(-margin, 0.0, size.width + margin, size.height + margin),\n      Edge.bottom => Rect.fromLTRB(-margin, -margin, size.width, size.height),\n    };\n  }\n\n  @override\n  bool shouldReclip(covariant CustomClipper<Rect> oldClipper) => false;\n}\n\nenum Edge {\n  left,\n  top,\n  right,\n  bottom;\n\n  bool get isHorizontal => switch (this) {\n        left || right => true,\n        _ => false,\n      };\n\n  bool get isVertical => !isHorizontal;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/startup/tasks/app_window_size_manager.dart';\nimport 'package:appflowy/workspace/application/home/home_setting_bloc.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:flutter/material.dart';\nimport 'package:hotkey_manager/hotkey_manager.dart';\nimport 'package:provider/provider.dart';\nimport 'package:scaled_app/scaled_app.dart';\n\ntypedef KeyDownHandler = void Function(HotKey hotKey);\n\nValueNotifier<int> switchToTheNextSpace = ValueNotifier(0);\nValueNotifier<int> createNewPageNotifier = ValueNotifier(0);\nValueNotifier<ViewPB?> switchToSpaceNotifier = ValueNotifier(null);\n\n@visibleForTesting\nfinal zoomInKeyCodes = [KeyCode.equal, KeyCode.numpadAdd, KeyCode.add];\n@visibleForTesting\nfinal zoomOutKeyCodes = [KeyCode.minus, KeyCode.numpadSubtract];\n@visibleForTesting\nfinal resetZoomKeyCodes = [KeyCode.digit0, KeyCode.numpad0];\n\n// Use a global value to store the zoom level and update it in the hotkeys.\n@visibleForTesting\ndouble appflowyScaleFactor = 1.0;\n\n/// Helper class that utilizes the global [HotKeyManager] to easily\n/// add a [HotKey] with different handlers.\n///\n/// Makes registration of a [HotKey] simple and easy to read, and makes\n/// sure the [KeyDownHandler], and other handlers, are grouped with the\n/// relevant [HotKey].\n///\nclass HotKeyItem {\n  HotKeyItem({\n    required this.hotKey,\n    this.keyDownHandler,\n  });\n\n  final HotKey hotKey;\n  final KeyDownHandler? keyDownHandler;\n\n  void register() =>\n      hotKeyManager.register(hotKey, keyDownHandler: keyDownHandler);\n}\n\nclass HomeHotKeys extends StatefulWidget {\n  const HomeHotKeys({\n    super.key,\n    required this.userProfile,\n    required this.child,\n  });\n\n  final UserProfilePB userProfile;\n  final Widget child;\n\n  @override\n  State<HomeHotKeys> createState() => _HomeHotKeysState();\n}\n\nclass _HomeHotKeysState extends State<HomeHotKeys> {\n  final windowSizeManager = WindowSizeManager();\n\n  late final items = [\n    // Collapse sidebar menu (using slash)\n    HotKeyItem(\n      hotKey: HotKey(\n        KeyCode.backslash,\n        modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control],\n        scope: HotKeyScope.inapp,\n      ),\n      keyDownHandler: (_) => colappsedMenus(context),\n    ),\n\n    // Collapse sidebar menu (using .)\n    HotKeyItem(\n      hotKey: HotKey(\n        KeyCode.period,\n        modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control],\n        scope: HotKeyScope.inapp,\n      ),\n      keyDownHandler: (_) => colappsedMenus(context),\n    ),\n\n    // Toggle theme mode light/dark\n    HotKeyItem(\n      hotKey: HotKey(\n        KeyCode.keyL,\n        modifiers: [\n          Platform.isMacOS ? KeyModifier.meta : KeyModifier.control,\n          KeyModifier.shift,\n        ],\n        scope: HotKeyScope.inapp,\n      ),\n      keyDownHandler: (_) =>\n          context.read<AppearanceSettingsCubit>().toggleThemeMode(),\n    ),\n\n    // Close current tab\n    HotKeyItem(\n      hotKey: HotKey(\n        KeyCode.keyW,\n        modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control],\n        scope: HotKeyScope.inapp,\n      ),\n      keyDownHandler: (_) =>\n          context.read<TabsBloc>().add(const TabsEvent.closeCurrentTab()),\n    ),\n\n    // Go to previous tab\n    HotKeyItem(\n      hotKey: HotKey(\n        KeyCode.pageUp,\n        modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control],\n        scope: HotKeyScope.inapp,\n      ),\n      keyDownHandler: (_) => _selectTab(context, -1),\n    ),\n\n    // Go to next tab\n    HotKeyItem(\n      hotKey: HotKey(\n        KeyCode.pageDown,\n        modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control],\n        scope: HotKeyScope.inapp,\n      ),\n      keyDownHandler: (_) => _selectTab(context, 1),\n    ),\n\n    // Rename current view\n    HotKeyItem(\n      hotKey: HotKey(\n        KeyCode.f2,\n        scope: HotKeyScope.inapp,\n      ),\n      keyDownHandler: (_) =>\n          getIt<RenameViewBloc>().add(const RenameViewEvent.open()),\n    ),\n\n    // Scale up/down the app\n    // In some keyboards, the system returns equal as + keycode, while others may return add as + keycode, so add them both as zoom in key.\n    ...zoomInKeyCodes.map(\n      (keycode) => HotKeyItem(\n        hotKey: HotKey(\n          keycode,\n          modifiers: [\n            Platform.isMacOS ? KeyModifier.meta : KeyModifier.control,\n          ],\n          scope: HotKeyScope.inapp,\n        ),\n        keyDownHandler: (_) => _scaleWithStep(0.1),\n      ),\n    ),\n\n    ...zoomOutKeyCodes.map(\n      (keycode) => HotKeyItem(\n        hotKey: HotKey(\n          keycode,\n          modifiers: [\n            Platform.isMacOS ? KeyModifier.meta : KeyModifier.control,\n          ],\n          scope: HotKeyScope.inapp,\n        ),\n        keyDownHandler: (_) => _scaleWithStep(-0.1),\n      ),\n    ),\n\n    // Reset app scaling\n    ...resetZoomKeyCodes.map(\n      (keycode) => HotKeyItem(\n        hotKey: HotKey(\n          keycode,\n          modifiers: [\n            Platform.isMacOS ? KeyModifier.meta : KeyModifier.control,\n          ],\n          scope: HotKeyScope.inapp,\n        ),\n        keyDownHandler: (_) => _scale(1),\n      ),\n    ),\n\n    // Switch to the next space\n    HotKeyItem(\n      hotKey: HotKey(\n        KeyCode.keyO,\n        modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control],\n        scope: HotKeyScope.inapp,\n      ),\n      keyDownHandler: (_) => switchToTheNextSpace.value++,\n    ),\n\n    // Create a new page\n    HotKeyItem(\n      hotKey: HotKey(\n        KeyCode.keyN,\n        modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control],\n        scope: HotKeyScope.inapp,\n      ),\n      keyDownHandler: (_) => createNewPageNotifier.value++,\n    ),\n\n    // Open settings dialog\n    openSettingsHotKey(context),\n  ];\n\n  @override\n  void initState() {\n    super.initState();\n    _registerHotKeys(context);\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    _registerHotKeys(context);\n  }\n\n  @override\n  Widget build(BuildContext context) => widget.child;\n\n  void _registerHotKeys(BuildContext context) {\n    for (final element in items) {\n      element.register();\n    }\n  }\n\n  void _selectTab(BuildContext context, int change) {\n    final bloc = context.read<TabsBloc>();\n    bloc.add(TabsEvent.selectTab(bloc.state.currentIndex + change));\n  }\n\n  Future<void> _scaleWithStep(double step) async {\n    final currentScaleFactor = await windowSizeManager.getScaleFactor();\n\n    double textScale = (currentScaleFactor + step).clamp(\n      WindowSizeManager.minScaleFactor,\n      WindowSizeManager.maxScaleFactor,\n    );\n\n    // only keep 2 decimal places\n    textScale = double.parse(textScale.toStringAsFixed(2));\n\n    Log.info('scale the app from $currentScaleFactor to $textScale');\n\n    await _scale(textScale);\n  }\n\n  Future<void> _scale(double scaleFactor) async {\n    if (FlowyRunner.currentMode == IntegrationMode.integrationTest) {\n      // The integration test will fail if we check the scale factor in the test.\n      // #0      ScaledWidgetsFlutterBinding.Eval ()\n      // #1      ScaledWidgetsFlutterBinding.instance (package:scaled_app/scaled_app.dart:66:62)\n      appflowyScaleFactor = double.parse(scaleFactor.toStringAsFixed(2));\n    } else {\n      ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => scaleFactor;\n    }\n\n    await windowSizeManager.setScaleFactor(scaleFactor);\n  }\n\n  void colappsedMenus(BuildContext context) {\n    final bloc = context.read<HomeSettingBloc>();\n    final isNotificationPanelCollapsed =\n        bloc.state.isNotificationPanelCollapsed;\n    if (!isNotificationPanelCollapsed) {\n      bloc.add(const HomeSettingEvent.collapseNotificationPanel());\n    } else {\n      bloc.collapseMenu();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/menu_shared_state.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter/material.dart';\n\nclass MenuSharedState {\n  MenuSharedState({\n    ViewPB? view,\n  }) {\n    _latestOpenView.value = view;\n  }\n\n  final ValueNotifier<ViewPB?> _latestOpenView = ValueNotifier<ViewPB?>(null);\n\n  ViewPB? get latestOpenView => _latestOpenView.value;\n  ValueNotifier<ViewPB?> get notifier => _latestOpenView;\n\n  set latestOpenView(ViewPB? view) {\n    if (_latestOpenView.value?.id != view?.id) {\n      _latestOpenView.value = view;\n    }\n  }\n\n  void addLatestViewListener(VoidCallback listener) {\n    _latestOpenView.addListener(listener);\n  }\n\n  void removeLatestViewListener(VoidCallback listener) {\n    _latestOpenView.removeListener(listener);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass FavoriteFolder extends StatefulWidget {\n  const FavoriteFolder({super.key, required this.views});\n\n  final List<ViewPB> views;\n\n  @override\n  State<FavoriteFolder> createState() => _FavoriteFolderState();\n}\n\nclass _FavoriteFolderState extends State<FavoriteFolder> {\n  final isHovered = ValueNotifier(false);\n\n  @override\n  void dispose() {\n    isHovered.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (widget.views.isEmpty) {\n      return const SizedBox.shrink();\n    }\n\n    return BlocProvider<FolderBloc>(\n      create: (context) => FolderBloc(type: FolderSpaceType.favorite)\n        ..add(const FolderEvent.initial()),\n      child: BlocBuilder<FolderBloc, FolderState>(\n        builder: (context, state) {\n          return MouseRegion(\n            onEnter: (_) => isHovered.value = true,\n            onExit: (_) => isHovered.value = false,\n            child: Column(\n              children: [\n                FavoriteHeader(\n                  onPressed: () => context\n                      .read<FolderBloc>()\n                      .add(const FolderEvent.expandOrUnExpand()),\n                ),\n                buildReorderListView(context, state),\n                if (state.isExpanded) ...[\n                  // more button\n                  const VSpace(2),\n                  const FavoriteMoreButton(),\n                ],\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget buildReorderListView(\n    BuildContext context,\n    FolderState state,\n  ) {\n    if (!state.isExpanded) return const SizedBox.shrink();\n\n    final favoriteBloc = context.read<FavoriteBloc>();\n    final pinnedViews =\n        favoriteBloc.state.pinnedViews.map((e) => e.item).toList();\n\n    if (pinnedViews.isEmpty) return const SizedBox.shrink();\n    if (pinnedViews.length == 1) {\n      return buildViewItem(pinnedViews.first);\n    }\n\n    return Theme(\n      data: Theme.of(context).copyWith(\n        canvasColor: Colors.transparent,\n        shadowColor: Colors.transparent,\n      ),\n      child: ReorderableListView.builder(\n        shrinkWrap: true,\n        buildDefaultDragHandles: false,\n        itemCount: pinnedViews.length,\n        padding: EdgeInsets.zero,\n        physics: const NeverScrollableScrollPhysics(),\n        itemBuilder: (context, i) {\n          final view = pinnedViews[i];\n          return ReorderableDragStartListener(\n            key: ValueKey(view.id),\n            index: i,\n            child: DecoratedBox(\n              decoration: const BoxDecoration(color: Colors.transparent),\n              child: buildViewItem(view),\n            ),\n          );\n        },\n        onReorder: (oldIndex, newIndex) {\n          favoriteBloc.add(FavoriteEvent.reorder(oldIndex, newIndex));\n        },\n      ),\n    );\n  }\n\n  Widget buildViewItem(ViewPB view) {\n    return ViewItem(\n      key: ValueKey('${FolderSpaceType.favorite.name} ${view.id}'),\n      spaceType: FolderSpaceType.favorite,\n      isDraggable: false,\n      isFirstChild: view.id == widget.views.first.id,\n      isFeedback: false,\n      view: view,\n      enableRightClickContext: true,\n      leftPadding: HomeSpaceViewSizes.leftPadding,\n      leftIconBuilder: (_, __) => const HSpace(HomeSpaceViewSizes.leftPadding),\n      level: 0,\n      isHovered: isHovered,\n      rightIconsBuilder: (context, view) => [\n        Listener(\n          child: FavoriteMoreActions(view: view),\n          onPointerDown: (e) {\n            context.read<ViewBloc>().add(const ViewEvent.setIsEditing(true));\n          },\n        ),\n        const HSpace(8.0),\n        Listener(\n          child: FavoritePinAction(view: view),\n          onPointerDown: (e) {\n            context.read<ViewBloc>().add(const ViewEvent.setIsEditing(true));\n          },\n        ),\n        const HSpace(4.0),\n      ],\n      shouldRenderChildren: false,\n      shouldLoadChildViews: false,\n      onTertiarySelected: (_, view) => context.read<TabsBloc>().openTab(view),\n      onSelected: (_, view) {\n        if (HardwareKeyboard.instance.isControlPressed) {\n          context.read<TabsBloc>().openTab(view);\n        }\n\n        context.read<TabsBloc>().openPlugin(view);\n      },\n    );\n  }\n}\n\nclass FavoriteHeader extends StatelessWidget {\n  const FavoriteHeader({super.key, required this.onPressed});\n\n  final VoidCallback onPressed;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return AFGhostIconTextButton.primary(\n      text: LocaleKeys.sideBar_favorites.tr(),\n      mainAxisAlignment: MainAxisAlignment.start,\n      size: AFButtonSize.l,\n      onTap: onPressed,\n      // todo: ask the designer to provide the token.\n      padding: EdgeInsets.symmetric(\n        horizontal: 4,\n        vertical: 6,\n      ),\n      borderRadius: theme.borderRadius.s,\n      iconBuilder: (context, isHover, disabled) => const FlowySvg(\n        FlowySvgs.favorite_header_m,\n        blendMode: null,\n      ),\n    );\n  }\n}\n\nclass FavoriteMoreButton extends StatelessWidget {\n  const FavoriteMoreButton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final favoriteBloc = context.watch<FavoriteBloc>();\n    final tabsBloc = context.read<TabsBloc>();\n    final unpinnedViews = favoriteBloc.state.unpinnedViews;\n    // only show the more button if there are unpinned views\n    if (unpinnedViews.isEmpty) {\n      return const SizedBox.shrink();\n    }\n\n    const minWidth = 260.0;\n    return AppFlowyPopover(\n      constraints: const BoxConstraints(\n        minWidth: minWidth,\n      ),\n      popupBuilder: (_) => MultiBlocProvider(\n        providers: [\n          BlocProvider.value(value: favoriteBloc),\n          BlocProvider.value(value: tabsBloc),\n        ],\n        child: const FavoriteMenu(minWidth: minWidth),\n      ),\n      margin: EdgeInsets.zero,\n      child: FlowyButton(\n        margin: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 7.0),\n        leftIcon: const FlowySvg(FlowySvgs.workspace_three_dots_s),\n        text: FlowyText.regular(LocaleKeys.button_more.tr()),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nconst double _kHorizontalPadding = 10.0;\nconst double _kVerticalPadding = 10.0;\n\nclass FavoriteMenu extends StatelessWidget {\n  const FavoriteMenu({super.key, required this.minWidth});\n\n  final double minWidth;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.only(\n        left: _kHorizontalPadding,\n        right: _kHorizontalPadding,\n        top: _kVerticalPadding,\n        bottom: _kVerticalPadding,\n      ),\n      child: BlocProvider(\n        create: (context) =>\n            FavoriteMenuBloc()..add(const FavoriteMenuEvent.initial()),\n        child: BlocBuilder<FavoriteMenuBloc, FavoriteMenuState>(\n          builder: (context, state) {\n            return Column(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                const VSpace(4),\n                SpaceSearchField(\n                  width: minWidth - 2 * _kHorizontalPadding,\n                  onSearch: (context, text) {\n                    context\n                        .read<FavoriteMenuBloc>()\n                        .add(FavoriteMenuEvent.search(text));\n                  },\n                ),\n                const VSpace(12),\n                _FavoriteGroups(\n                  minWidth: minWidth,\n                  state: state,\n                ),\n              ],\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass _FavoriteGroupedViews extends StatelessWidget {\n  const _FavoriteGroupedViews({\n    required this.views,\n  });\n\n  final List<ViewPB> views;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: views\n          .map(\n            (e) => ViewItem(\n              key: ValueKey(e.id),\n              view: e,\n              spaceType: FolderSpaceType.favorite,\n              level: 0,\n              onSelected: (_, view) {\n                context.read<TabsBloc>().openPlugin(view);\n                PopoverContainer.maybeOf(context)?.close();\n              },\n              isFeedback: false,\n              isDraggable: false,\n              shouldRenderChildren: false,\n              extendBuilder: (view) => view.isPinned\n                  ? [\n                      const HSpace(4.0),\n                      const FlowySvg(\n                        FlowySvgs.favorite_pin_s,\n                        blendMode: null,\n                      ),\n                    ]\n                  : [],\n              leftIconBuilder: (_, __) => const HSpace(4.0),\n              rightIconsBuilder: (_, view) => [\n                FavoriteMoreActions(view: view),\n                const HSpace(6.0),\n                FavoritePinAction(view: view),\n                const HSpace(4.0),\n              ],\n            ),\n          )\n          .toList(),\n    );\n  }\n}\n\nclass _FavoriteGroups extends StatelessWidget {\n  const _FavoriteGroups({\n    required this.minWidth,\n    required this.state,\n  });\n\n  final double minWidth;\n  final FavoriteMenuState state;\n\n  @override\n  Widget build(BuildContext context) {\n    final today = _buildGroups(\n      context,\n      state.todayViews,\n      LocaleKeys.sideBar_today.tr(),\n    );\n    final thisWeek = _buildGroups(\n      context,\n      state.thisWeekViews,\n      LocaleKeys.sideBar_thisWeek.tr(),\n    );\n    final others = _buildGroups(\n      context,\n      state.otherViews,\n      LocaleKeys.sideBar_others.tr(),\n    );\n\n    return Container(\n      width: minWidth - 2 * _kHorizontalPadding,\n      constraints: const BoxConstraints(\n        maxHeight: 300,\n      ),\n      child: SingleChildScrollView(\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            if (today.isNotEmpty) ...[\n              ...today,\n            ],\n            if (thisWeek.isNotEmpty) ...[\n              if (today.isNotEmpty) ...[\n                const FlowyDivider(),\n                const VSpace(16),\n              ],\n              ...thisWeek,\n            ],\n            if ((thisWeek.isNotEmpty || today.isNotEmpty) &&\n                others.isNotEmpty) ...[\n              const FlowyDivider(),\n              const VSpace(16),\n            ],\n            ...others.isNotEmpty && (today.isNotEmpty || thisWeek.isNotEmpty)\n                ? others\n                : _buildGroups(\n                    context,\n                    state.otherViews,\n                    LocaleKeys.sideBar_others.tr(),\n                    showHeader: false,\n                  ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  List<Widget> _buildGroups(\n    BuildContext context,\n    List<ViewPB> views,\n    String title, {\n    bool showHeader = true,\n  }) {\n    return [\n      if (views.isNotEmpty) ...[\n        if (showHeader)\n          FlowyText(\n            title,\n            fontSize: 12.0,\n            color: Theme.of(context).hintColor,\n          ),\n        const VSpace(2),\n        _FavoriteGroupedViews(views: views),\n        const VSpace(8),\n      ],\n    ];\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart",
    "content": "import 'package:appflowy/workspace/application/favorite/favorite_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'favorite_menu_bloc.freezed.dart';\n\nclass FavoriteMenuBloc extends Bloc<FavoriteMenuEvent, FavoriteMenuState> {\n  FavoriteMenuBloc() : super(FavoriteMenuState.initial()) {\n    on<FavoriteMenuEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            final favoriteViews = await _service.readFavorites();\n            List<ViewPB> views = [];\n            List<ViewPB> todayViews = [];\n            List<ViewPB> thisWeekViews = [];\n            List<ViewPB> otherViews = [];\n\n            favoriteViews.onSuccess((s) {\n              _source = s;\n              (views, todayViews, thisWeekViews, otherViews) = _getViews(s);\n            });\n\n            emit(\n              state.copyWith(\n                views: views,\n                queriedViews: views,\n                todayViews: todayViews,\n                thisWeekViews: thisWeekViews,\n                otherViews: otherViews,\n              ),\n            );\n          },\n          search: (query) async {\n            if (_source == null) {\n              return;\n            }\n            var (views, todayViews, thisWeekViews, otherViews) =\n                _getViews(_source!);\n            var queriedViews = views;\n\n            if (query.isNotEmpty) {\n              queriedViews = _filter(views, query);\n              todayViews = _filter(todayViews, query);\n              thisWeekViews = _filter(thisWeekViews, query);\n              otherViews = _filter(otherViews, query);\n            }\n\n            emit(\n              state.copyWith(\n                views: views,\n                queriedViews: queriedViews,\n                todayViews: todayViews,\n                thisWeekViews: thisWeekViews,\n                otherViews: otherViews,\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  final FavoriteService _service = FavoriteService();\n  RepeatedFavoriteViewPB? _source;\n\n  List<ViewPB> _filter(List<ViewPB> views, String query) => views\n      .where((view) => view.name.toLowerCase().contains(query.toLowerCase()))\n      .toList();\n\n  // all, today, last week, other\n  (List<ViewPB>, List<ViewPB>, List<ViewPB>, List<ViewPB>) _getViews(\n    RepeatedFavoriteViewPB source,\n  ) {\n    final now = DateTime.now();\n\n    final List<ViewPB> views = source.items.map((v) => v.item).toList();\n    final List<ViewPB> todayViews = [];\n    final List<ViewPB> thisWeekViews = [];\n    final List<ViewPB> otherViews = [];\n\n    for (final favoriteView in source.items) {\n      final view = favoriteView.item;\n      final date = DateTime.fromMillisecondsSinceEpoch(\n        favoriteView.timestamp.toInt() * 1000,\n      );\n      final diff = now.difference(date).inDays;\n      if (diff == 0) {\n        todayViews.add(view);\n      } else if (diff < 7) {\n        thisWeekViews.add(view);\n      } else {\n        otherViews.add(view);\n      }\n    }\n\n    return (views, todayViews, thisWeekViews, otherViews);\n  }\n}\n\n@freezed\nclass FavoriteMenuEvent with _$FavoriteMenuEvent {\n  const factory FavoriteMenuEvent.initial() = Initial;\n  const factory FavoriteMenuEvent.search(String query) = Search;\n}\n\n@freezed\nclass FavoriteMenuState with _$FavoriteMenuState {\n  const factory FavoriteMenuState({\n    @Default([]) List<ViewPB> views,\n    @Default([]) List<ViewPB> queriedViews,\n    @Default([]) List<ViewPB> todayViews,\n    @Default([]) List<ViewPB> thisWeekViews,\n    @Default([]) List<ViewPB> otherViews,\n  }) = _FavoriteMenuState;\n\n  factory FavoriteMenuState.initial() => const FavoriteMenuState();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/icon_button.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass FavoriteMoreActions extends StatelessWidget {\n  const FavoriteMoreActions({super.key, required this.view});\n\n  final ViewPB view;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(),\n      child: ViewMoreActionPopover(\n        view: view,\n        spaceType: FolderSpaceType.favorite,\n        isExpanded: false,\n        onEditing: (value) =>\n            context.read<ViewBloc>().add(ViewEvent.setIsEditing(value)),\n        onAction: (action, _) {\n          switch (action) {\n            case ViewMoreActionType.favorite:\n            case ViewMoreActionType.unFavorite:\n              context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view));\n              PopoverContainer.maybeOf(context)?.closeAll();\n              break;\n            case ViewMoreActionType.rename:\n              showAFTextFieldDialog(\n                context: context,\n                title: LocaleKeys.disclosureAction_rename.tr(),\n                initialValue: view.nameOrDefault,\n                maxLength: 256,\n                onConfirm: (newValue) {\n                  // can not use bloc here because it has been disposed.\n                  ViewBackendService.updateView(\n                    viewId: view.id,\n                    name: newValue,\n                  );\n                },\n              );\n              PopoverContainer.maybeOf(context)?.closeAll();\n              break;\n\n            case ViewMoreActionType.openInNewTab:\n              getIt<TabsBloc>().openTab(view);\n              break;\n            case ViewMoreActionType.delete:\n            case ViewMoreActionType.duplicate:\n            default:\n              throw UnsupportedError('$action is not supported');\n          }\n        },\n        buildChild: (popover) => FlowyIconButton(\n          width: 24,\n          icon: const FlowySvg(FlowySvgs.workspace_three_dots_s),\n          onPressed: () {\n            popover.show();\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass FavoritePinAction extends StatelessWidget {\n  const FavoritePinAction({super.key, required this.view});\n\n  final ViewPB view;\n\n  @override\n  Widget build(BuildContext context) {\n    final tooltip = view.isPinned\n        ? LocaleKeys.favorite_removeFromSidebar.tr()\n        : LocaleKeys.favorite_addToSidebar.tr();\n    final icon = FlowySvg(\n      view.isPinned\n          ? FlowySvgs.favorite_section_unpin_s\n          : FlowySvgs.favorite_section_pin_s,\n    );\n    return FlowyTooltip(\n      message: tooltip,\n      child: FlowyIconButton(\n        width: 24,\n        icon: icon,\n        onPressed: () {\n          view.isPinned\n              ? context.read<FavoriteBloc>().add(FavoriteEvent.unpin(view))\n              : context.read<FavoriteBloc>().add(FavoriteEvent.pin(view));\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_bloc.dart",
    "content": "import 'package:appflowy/workspace/application/favorite/favorite_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'favorite_pin_bloc.freezed.dart';\n\nclass FavoritePinBloc extends Bloc<FavoritePinEvent, FavoritePinState> {\n  FavoritePinBloc() : super(FavoritePinState.initial()) {\n    on<FavoritePinEvent>(\n      (event, emit) async {\n        await event.when(\n          initial: () async {\n            final List<ViewPB> views = await _service\n                .readFavorites()\n                .fold((s) => s.items.map((v) => v.item).toList(), (f) => []);\n            emit(state.copyWith(views: views, queriedViews: views));\n          },\n          search: (query) async {\n            if (query.isEmpty) {\n              emit(state.copyWith(queriedViews: state.views));\n              return;\n            }\n\n            final queriedViews = state.views\n                .where(\n                  (view) =>\n                      view.name.toLowerCase().contains(query.toLowerCase()),\n                )\n                .toList();\n            emit(state.copyWith(queriedViews: queriedViews));\n          },\n        );\n      },\n    );\n  }\n\n  final FavoriteService _service = FavoriteService();\n}\n\n@freezed\nclass FavoritePinEvent with _$FavoritePinEvent {\n  const factory FavoritePinEvent.initial() = Initial;\n  const factory FavoritePinEvent.search(String query) = Search;\n}\n\n@freezed\nclass FavoritePinState with _$FavoritePinState {\n  const factory FavoritePinState({\n    @Default([]) List<ViewPB> views,\n    @Default([]) List<ViewPB> queriedViews,\n    @Default([]) List<List<ViewPB>> todayViews,\n    @Default([]) List<List<ViewPB>> lastWeekViews,\n    @Default([]) List<List<ViewPB>> otherViews,\n  }) = _FavoritePinState;\n\n  factory FavoritePinState.initial() => const FavoritePinState();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/widgets.dart';\n\nclass FolderHeader extends StatefulWidget {\n  const FolderHeader({\n    super.key,\n    required this.title,\n    required this.expandButtonTooltip,\n    required this.addButtonTooltip,\n    required this.onPressed,\n    required this.onAdded,\n    required this.isExpanded,\n  });\n\n  final String title;\n  final String expandButtonTooltip;\n  final String addButtonTooltip;\n  final VoidCallback onPressed;\n  final VoidCallback onAdded;\n  final bool isExpanded;\n\n  @override\n  State<FolderHeader> createState() => _FolderHeaderState();\n}\n\nclass _FolderHeaderState extends State<FolderHeader> {\n  final isHovered = ValueNotifier(false);\n\n  @override\n  void dispose() {\n    isHovered.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: HomeSizes.workspaceSectionHeight,\n      child: MouseRegion(\n        onEnter: (_) => isHovered.value = true,\n        onExit: (_) => isHovered.value = false,\n        child: FlowyButton(\n          onTap: widget.onPressed,\n          margin: const EdgeInsets.only(left: 6.0, right: 4.0),\n          rightIcon: ValueListenableBuilder(\n            valueListenable: isHovered,\n            builder: (context, onHover, child) =>\n                Opacity(opacity: onHover ? 1 : 0, child: child),\n            child: FlowyIconButton(\n              width: 24,\n              iconPadding: const EdgeInsets.all(4.0),\n              tooltipText: widget.addButtonTooltip,\n              icon: const FlowySvg(FlowySvgs.view_item_add_s),\n              onPressed: widget.onAdded,\n            ),\n          ),\n          iconPadding: 10.0,\n          text: Row(\n            children: [\n              FlowyText(\n                widget.title,\n                lineHeight: 1.15,\n              ),\n              const HSpace(4.0),\n              FlowySvg(\n                widget.isExpanded\n                    ? FlowySvgs.workspace_drop_down_menu_show_s\n                    : FlowySvgs.workspace_drop_down_menu_hide_s,\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SectionFolder extends StatefulWidget {\n  const SectionFolder({\n    super.key,\n    required this.title,\n    required this.spaceType,\n    required this.views,\n    this.isHoverEnabled = true,\n    required this.expandButtonTooltip,\n    required this.addButtonTooltip,\n  });\n\n  final String title;\n  final FolderSpaceType spaceType;\n  final List<ViewPB> views;\n  final bool isHoverEnabled;\n  final String expandButtonTooltip;\n  final String addButtonTooltip;\n\n  @override\n  State<SectionFolder> createState() => _SectionFolderState();\n}\n\nclass _SectionFolderState extends State<SectionFolder> {\n  final isHovered = ValueNotifier(false);\n\n  @override\n  void dispose() {\n    isHovered.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      onEnter: (_) => isHovered.value = true,\n      onExit: (_) => isHovered.value = false,\n      child: BlocProvider<FolderBloc>(\n        create: (_) => FolderBloc(type: widget.spaceType)\n          ..add(const FolderEvent.initial()),\n        child: BlocBuilder<FolderBloc, FolderState>(\n          builder: (context, state) => Column(\n            children: [\n              _buildHeader(context),\n              // Pages\n              const VSpace(4.0),\n              ..._buildViews(context, state, isHovered),\n              // Add a placeholder if there are no views\n              _buildDraggablePlaceholder(context),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildHeader(BuildContext context) {\n    return FolderHeader(\n      title: widget.title,\n      isExpanded: context.watch<FolderBloc>().state.isExpanded,\n      expandButtonTooltip: widget.expandButtonTooltip,\n      addButtonTooltip: widget.addButtonTooltip,\n      onPressed: () =>\n          context.read<FolderBloc>().add(const FolderEvent.expandOrUnExpand()),\n      onAdded: () {\n        context.read<SidebarSectionsBloc>().add(\n              SidebarSectionsEvent.createRootViewInSection(\n                name: '',\n                index: 0,\n                viewSection: widget.spaceType.toViewSectionPB,\n              ),\n            );\n\n        context\n            .read<FolderBloc>()\n            .add(const FolderEvent.expandOrUnExpand(isExpanded: true));\n      },\n    );\n  }\n\n  Iterable<Widget> _buildViews(\n    BuildContext context,\n    FolderState state,\n    ValueNotifier<bool> isHovered,\n  ) {\n    if (!state.isExpanded) {\n      return [];\n    }\n\n    return widget.views.map(\n      (view) => ViewItem(\n        key: ValueKey('${widget.spaceType.name} ${view.id}'),\n        spaceType: widget.spaceType,\n        engagedInExpanding: true,\n        isFirstChild: view.id == widget.views.first.id,\n        view: view,\n        level: 0,\n        leftPadding: HomeSpaceViewSizes.leftPadding,\n        isFeedback: false,\n        isHovered: isHovered,\n        enableRightClickContext: true,\n        onSelected: (viewContext, view) {\n          if (HardwareKeyboard.instance.isControlPressed) {\n            context.read<TabsBloc>().openTab(view);\n          }\n\n          context.read<TabsBloc>().openPlugin(view);\n        },\n        onTertiarySelected: (viewContext, view) =>\n            context.read<TabsBloc>().openTab(view),\n        isHoverEnabled: widget.isHoverEnabled,\n      ),\n    );\n  }\n\n  Widget _buildDraggablePlaceholder(BuildContext context) {\n    if (widget.views.isNotEmpty) {\n      return const SizedBox.shrink();\n    }\n    final parentViewId =\n        context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId;\n    return ViewItem(\n      spaceType: widget.spaceType,\n      view: ViewPB(parentViewId: parentViewId ?? ''),\n      level: 0,\n      leftPadding: HomeSpaceViewSizes.leftPadding,\n      isFeedback: false,\n      onSelected: (_, __) {},\n      isHoverEnabled: widget.isHoverEnabled,\n      isPlaceholder: true,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\n\nimport 'sidebar_footer_button.dart';\n\nclass SidebarFooter extends StatelessWidget {\n  const SidebarFooter({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        if (FeatureFlag.planBilling.isOn)\n          BillingGateGuard(\n            builder: (context) {\n              return const SidebarToast();\n            },\n          ),\n        Row(\n          // mainAxisAlignment: MainAxisAlignment.spaceBetween,\n          children: [\n            const Expanded(child: SidebarTemplateButton()),\n            _buildVerticalDivider(context),\n            const Expanded(child: SidebarTrashButton()),\n          ],\n        ),\n      ],\n    );\n  }\n\n  Widget _buildVerticalDivider(BuildContext context) {\n    return Container(\n      width: 1.0,\n      height: 14,\n      margin: const EdgeInsets.symmetric(horizontal: 4),\n      color: AFThemeExtension.of(context).borderColor,\n    );\n  }\n}\n\nclass SidebarTemplateButton extends StatelessWidget {\n  const SidebarTemplateButton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return SidebarFooterButton(\n      leftIconSize: const Size.square(16.0),\n      leftIcon: const FlowySvg(\n        FlowySvgs.icon_template_s,\n      ),\n      text: LocaleKeys.template_label.tr(),\n      onTap: () => afLaunchUrlString('https://appflowy.com/templates'),\n    );\n  }\n}\n\nclass SidebarTrashButton extends StatelessWidget {\n  const SidebarTrashButton({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ValueListenableBuilder(\n      valueListenable: getIt<MenuSharedState>().notifier,\n      builder: (context, value, child) {\n        return SidebarFooterButton(\n          leftIconSize: const Size.square(18.0),\n          leftIcon: const FlowySvg(\n            FlowySvgs.icon_delete_s,\n          ),\n          text: LocaleKeys.trash_text.tr(),\n          onTap: () {\n            getIt<MenuSharedState>().latestOpenView = null;\n            getIt<TabsBloc>().add(\n              TabsEvent.openPlugin(\n                plugin: makePlugin(pluginType: PluginType.trash),\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\nclass SidebarWidgetButton extends StatelessWidget {\n  const SidebarWidgetButton({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      cursor: SystemMouseCursors.click,\n      child: GestureDetector(\n        onTap: () {},\n        child: const FlowySvg(FlowySvgs.sidebar_footer_widget_s),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer_button.dart",
    "content": "import 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\n// This button style is used in\n// - Trash button\n// - Template button\nclass SidebarFooterButton extends StatelessWidget {\n  const SidebarFooterButton({\n    super.key,\n    required this.leftIcon,\n    required this.leftIconSize,\n    required this.text,\n    required this.onTap,\n  });\n\n  final Widget leftIcon;\n  final Size leftIconSize;\n  final String text;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: HomeSizes.workspaceSectionHeight,\n      child: FlowyButton(\n        leftIcon: leftIcon,\n        leftIconSize: leftIconSize,\n        margin: const EdgeInsets.all(4.0),\n        expandText: false,\n        text: Padding(\n          padding: const EdgeInsets.only(right: 6.0),\n          child: FlowyText(\n            text,\n            fontWeight: FontWeight.w400,\n            figmaLineHeight: 18.0,\n          ),\n        ),\n        onTap: onTap,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/af_role_pb_extension.dart';\nimport 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';\nimport 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/billing/sidebar_plan_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SidebarToast extends StatelessWidget {\n  const SidebarToast({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocConsumer<SidebarPlanBloc, SidebarPlanState>(\n      listener: (_, state) {\n        // Show a dialog when the user hits the storage limit, After user click ok, it will navigate to the plan page.\n        // Even though the dislog is dissmissed, if the user triggers the storage limit again, the dialog will show again.\n        state.tierIndicator.maybeWhen(\n          storageLimitHit: () => WidgetsBinding.instance.addPostFrameCallback(\n            (_) => _showStorageLimitDialog(context),\n          ),\n          singleFileLimitHit: () =>\n              WidgetsBinding.instance.addPostFrameCallback(\n            (_) => _showSingleFileLimitDialog(context),\n          ),\n          orElse: () {},\n        );\n      },\n      builder: (_, state) {\n        return state.tierIndicator.when(\n          loading: () => const SizedBox.shrink(),\n          storageLimitHit: () => PlanIndicator(\n            planName: SubscriptionPlanPB.Free.label,\n            text: LocaleKeys.sideBar_upgradeToPro.tr(),\n            onTap: () => _handleOnTap(context, SubscriptionPlanPB.Pro),\n            reason: LocaleKeys.sideBar_storageLimitDialogTitle.tr(),\n          ),\n          aiMaxiLimitHit: () => PlanIndicator(\n            planName: SubscriptionPlanPB.AiMax.label,\n            text: LocaleKeys.sideBar_upgradeToAIMax.tr(),\n            onTap: () => _handleOnTap(context, SubscriptionPlanPB.AiMax),\n            reason: LocaleKeys.sideBar_aiResponseLimitTitle.tr(),\n          ),\n          singleFileLimitHit: () => const SizedBox.shrink(),\n        );\n      },\n    );\n  }\n\n  void _showStorageLimitDialog(BuildContext context) => showConfirmDialog(\n        context: context,\n        title: LocaleKeys.sideBar_purchaseStorageSpace.tr(),\n        description: LocaleKeys.sideBar_storageLimitDialogTitle.tr(),\n        confirmLabel:\n            LocaleKeys.settings_comparePlanDialog_actions_upgrade.tr(),\n        onConfirm: (_) {\n          WidgetsBinding.instance.addPostFrameCallback(\n            (_) => _handleOnTap(context, SubscriptionPlanPB.Pro),\n          );\n        },\n      );\n\n  void _showSingleFileLimitDialog(BuildContext context) => showConfirmDialog(\n        context: context,\n        title: LocaleKeys.sideBar_upgradeToPro.tr(),\n        description:\n            LocaleKeys.sideBar_singleFileProPlanLimitationDescription.tr(),\n        confirmLabel:\n            LocaleKeys.settings_comparePlanDialog_actions_upgrade.tr(),\n        onConfirm: (_) {\n          WidgetsBinding.instance.addPostFrameCallback(\n            (_) => _handleOnTap(context, SubscriptionPlanPB.Pro),\n          );\n        },\n      );\n\n  void _handleOnTap(BuildContext context, SubscriptionPlanPB plan) {\n    final userProfile = context.read<SidebarPlanBloc>().state.userProfile;\n    if (userProfile == null) {\n      return Log.error(\n        'UserProfile is null, this should NOT happen! Please file a bug report',\n      );\n    }\n\n    final userWorkspaceBloc = context.read<UserWorkspaceBloc>();\n    final role = userWorkspaceBloc.state.currentWorkspace?.role;\n    if (role == null) {\n      return Log.error(\n        \"Member is null. It should not happen. If you see this error, it's a bug\",\n      );\n    }\n\n    // Only if the user is the workspace owner will we navigate to the plan page.\n    if (role.isOwner) {\n      showSettingsDialog(\n        context,\n        userWorkspaceBloc: userWorkspaceBloc,\n        initPage: SettingsPage.plan,\n      );\n    } else {\n      final String message;\n      if (plan == SubscriptionPlanPB.AiMax) {\n        message = Platform.isIOS\n            ? LocaleKeys.sideBar_askOwnerToUpgradeToAIMaxIOS.tr()\n            : LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr();\n      } else {\n        message = Platform.isIOS\n            ? LocaleKeys.sideBar_askOwnerToUpgradeToProIOS.tr()\n            : LocaleKeys.sideBar_askOwnerToUpgradeToPro.tr();\n      }\n\n      showDialog(\n        context: context,\n        barrierDismissible: false,\n        useRootNavigator: false,\n        builder: (dialogContext) => _AskOwnerToChangePlan(\n          message: message,\n          onOkPressed: () {},\n        ),\n      );\n    }\n  }\n}\n\nclass PlanIndicator extends StatefulWidget {\n  const PlanIndicator({\n    super.key,\n    required this.planName,\n    required this.text,\n    required this.onTap,\n    required this.reason,\n  });\n\n  final String planName;\n  final String reason;\n  final String text;\n  final Function() onTap;\n\n  @override\n  State<PlanIndicator> createState() => _PlanIndicatorState();\n}\n\nclass _PlanIndicatorState extends State<PlanIndicator> {\n  final popoverController = PopoverController();\n\n  @override\n  void dispose() {\n    popoverController.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    const textGradient = LinearGradient(\n      begin: Alignment.bottomLeft,\n      end: Alignment.bottomRight,\n      colors: [Color(0xFF8032FF), Color(0xFFEF35FF)],\n      stops: [0.1545, 0.8225],\n    );\n\n    final backgroundGradient = LinearGradient(\n      begin: Alignment.topLeft,\n      end: Alignment.bottomRight,\n      colors: [\n        const Color(0xFF8032FF).withValues(alpha: .1),\n        const Color(0xFFEF35FF).withValues(alpha: .1),\n      ],\n    );\n\n    return AppFlowyPopover(\n      controller: popoverController,\n      direction: PopoverDirection.rightWithBottomAligned,\n      offset: const Offset(10, -12),\n      popupBuilder: (context) {\n        return Padding(\n          padding: const EdgeInsets.all(8.0),\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              FlowyText(\n                widget.text,\n                color: AFThemeExtension.of(context).strongText,\n              ),\n              const VSpace(12),\n              Opacity(\n                opacity: 0.7,\n                child: FlowyText.regular(\n                  widget.reason,\n                  maxLines: null,\n                  lineHeight: 1.3,\n                  textAlign: TextAlign.center,\n                ),\n              ),\n              const VSpace(12),\n              Row(\n                children: [\n                  Expanded(\n                    child: MouseRegion(\n                      cursor: SystemMouseCursors.click,\n                      child: GestureDetector(\n                        behavior: HitTestBehavior.translucent,\n                        onTap: () {\n                          popoverController.close();\n                          widget.onTap();\n                        },\n                        child: Container(\n                          padding: const EdgeInsets.symmetric(\n                            horizontal: 10,\n                            vertical: 8,\n                          ),\n                          decoration: BoxDecoration(\n                            color: Theme.of(context).colorScheme.primary,\n                            borderRadius: BorderRadius.circular(9),\n                          ),\n                          child: Center(\n                            child: FlowyText(\n                              LocaleKeys\n                                  .settings_comparePlanDialog_actions_upgrade\n                                  .tr(),\n                              color: Colors.white,\n                              fontSize: 12,\n                              strutStyle: const StrutStyle(\n                                forceStrutHeight: true,\n                              ),\n                            ),\n                          ),\n                        ),\n                      ),\n                    ),\n                  ),\n                ],\n              ),\n            ],\n          ),\n        );\n      },\n      child: MouseRegion(\n        cursor: SystemMouseCursors.click,\n        child: Container(\n          margin: const EdgeInsets.only(bottom: 12),\n          padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10),\n          clipBehavior: Clip.antiAlias,\n          decoration: BoxDecoration(\n            gradient: backgroundGradient,\n            borderRadius: BorderRadius.circular(10),\n          ),\n          child: Row(\n            children: [\n              const FlowySvg(\n                FlowySvgs.upgrade_storage_s,\n                blendMode: null,\n              ),\n              const HSpace(6),\n              ShaderMask(\n                shaderCallback: (bounds) => textGradient.createShader(bounds),\n                blendMode: BlendMode.srcIn,\n                child: FlowyText(\n                  widget.text,\n                  color: AFThemeExtension.of(context).strongText,\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _AskOwnerToChangePlan extends StatelessWidget {\n  const _AskOwnerToChangePlan({\n    required this.message,\n    required this.onOkPressed,\n  });\n  final String message;\n  final VoidCallback onOkPressed;\n\n  @override\n  Widget build(BuildContext context) {\n    return NavigatorOkCancelDialog(\n      message: message,\n      okTitle: LocaleKeys.button_ok.tr(),\n      onOkPressed: onOkPressed,\n      titleUpperCase: false,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_upgrade_application_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass SidebarUpgradeApplicationButton extends StatelessWidget {\n  const SidebarUpgradeApplicationButton({\n    super.key,\n    required this.onUpdateButtonTap,\n    required this.onCloseButtonTap,\n  });\n\n  final VoidCallback onUpdateButtonTap;\n  final VoidCallback onCloseButtonTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.all(12),\n      decoration: BoxDecoration(\n        color: context.sidebarUpgradeButtonBackground,\n        borderRadius: BorderRadius.circular(16),\n      ),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          // title\n          _buildTitle(),\n          const VSpace(2),\n          // description\n          _buildDescription(),\n          const VSpace(10),\n          // update button\n          _buildUpdateButton(),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildTitle() {\n    return Row(\n      children: [\n        const FlowySvg(\n          FlowySvgs.sidebar_upgrade_version_s,\n          blendMode: null,\n        ),\n        const HSpace(6),\n        FlowyText.medium(\n          LocaleKeys.autoUpdate_bannerUpdateTitle.tr(),\n          fontSize: 14,\n          figmaLineHeight: 18,\n        ),\n        const Spacer(),\n        FlowyButton(\n          useIntrinsicWidth: true,\n          text: const FlowySvg(FlowySvgs.upgrade_close_s),\n          onTap: onCloseButtonTap,\n        ),\n      ],\n    );\n  }\n\n  Widget _buildDescription() {\n    return Opacity(\n      opacity: 0.7,\n      child: FlowyText(\n        LocaleKeys.autoUpdate_bannerUpdateDescription.tr(),\n        fontSize: 13,\n        figmaLineHeight: 16,\n        maxLines: null,\n      ),\n    );\n  }\n\n  Widget _buildUpdateButton() {\n    return MouseRegion(\n      cursor: SystemMouseCursors.click,\n      child: GestureDetector(\n        behavior: HitTestBehavior.translucent,\n        onTap: onUpdateButtonTap,\n        child: Container(\n          padding: const EdgeInsets.symmetric(\n            horizontal: 10,\n            vertical: 6,\n          ),\n          decoration: ShapeDecoration(\n            color: const Color(0xFFA44AFD),\n            shape: RoundedRectangleBorder(\n              borderRadius: BorderRadius.circular(9),\n            ),\n          ),\n          child: FlowyText.medium(\n            LocaleKeys.autoUpdate_settingsUpdateButton.tr(),\n            color: Colors.white,\n            fontSize: 12.0,\n            figmaLineHeight: 15.0,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nextension on BuildContext {\n  Color get sidebarUpgradeButtonBackground => Theme.of(this).isLightMode\n      ? const Color(0xB2EBE4FF)\n      : const Color(0xB239275B);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart",
    "content": "import 'dart:io' show Platform;\n\nimport 'package:appflowy/core/frameless_window.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/home/home_setting_bloc.dart';\nimport 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\n/// Sidebar top menu is the top bar of the sidebar.\n///\n/// in the top menu, we have:\n///   - appflowy icon (Windows or Linux)\n///   - close / expand sidebar button\nclass SidebarTopMenu extends StatelessWidget {\n  const SidebarTopMenu({\n    super.key,\n    required this.isSidebarOnHover,\n  });\n\n  final ValueNotifier<bool> isSidebarOnHover;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(\n      builder: (context, _) => SizedBox(\n        height: !UniversalPlatform.isWindows ? HomeSizes.topBarHeight : 45,\n        child: MoveWindowDetector(\n          child: Row(\n            children: [\n              _buildLogoIcon(context),\n              const Spacer(),\n              _buildCollapseMenuButton(context),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildLogoIcon(BuildContext context) {\n    if (Platform.isMacOS) {\n      return const SizedBox.shrink();\n    }\n\n    final svgData = Theme.of(context).brightness == Brightness.dark\n        ? FlowySvgs.app_logo_with_text_dark_xl\n        : FlowySvgs.app_logo_with_text_light_xl;\n\n    return Padding(\n      padding: const EdgeInsets.only(top: 12.0, left: 8),\n      child: FlowySvg(\n        svgData,\n        size: const Size(92, 17),\n        blendMode: null,\n      ),\n    );\n  }\n\n  Widget _buildCollapseMenuButton(BuildContext context) {\n    final settingState = context.read<HomeSettingBloc?>()?.state;\n    final isNotificationPanelCollapsed =\n        settingState?.isNotificationPanelCollapsed ?? true;\n\n    final textSpan = TextSpan(\n      children: [\n        TextSpan(\n          text: LocaleKeys.sideBar_closeSidebar.tr(),\n          style: context.tooltipTextStyle(),\n        ),\n        if (isNotificationPanelCollapsed)\n          TextSpan(\n            text: '\\n${Platform.isMacOS ? '⌘+.' : 'Ctrl+\\\\'}',\n            style: context\n                .tooltipTextStyle()\n                ?.copyWith(color: Theme.of(context).hintColor),\n          ),\n      ],\n    );\n    final theme = AppFlowyTheme.of(context);\n\n    return ValueListenableBuilder(\n      valueListenable: isSidebarOnHover,\n      builder: (_, value, ___) => Opacity(\n        opacity: value ? 1 : 0,\n        child: Padding(\n          padding: const EdgeInsets.only(top: 12.0, right: 6.0),\n          child: FlowyTooltip(\n            richMessage: textSpan,\n            child: Listener(\n              behavior: HitTestBehavior.translucent,\n              onPointerDown: (_) =>\n                  context.read<HomeSettingBloc>().collapseMenu(),\n              child: FlowyHover(\n                child: SizedBox(\n                  width: 24,\n                  child: FlowySvg(\n                    FlowySvgs.double_back_arrow_m,\n                    color: theme.iconColorScheme.secondary,\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'\n    show UserProfilePB;\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\n// keep this widget in case we need to roll back (lucas.xu)\nclass SidebarUser extends StatelessWidget {\n  const SidebarUser({\n    super.key,\n    required this.userProfile,\n  });\n\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    final workspaceId =\n        context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??\n            '';\n    return BlocProvider<MenuUserBloc>(\n      create: (_) => MenuUserBloc(userProfile, workspaceId),\n      child: BlocBuilder<MenuUserBloc, MenuUserState>(\n        builder: (context, state) => Row(\n          children: [\n            const HSpace(4),\n            UserAvatar(\n              iconUrl: state.userProfile.iconUrl,\n              name: state.userProfile.name,\n              size: AFAvatarSize.s,\n              decoration: ShapeDecoration(\n                color: const Color(0xFFFBE8FB),\n                shape: RoundedRectangleBorder(\n                  side: const BorderSide(width: 0.50, color: Color(0x19171717)),\n                  borderRadius: BorderRadius.circular(8),\n                ),\n              ),\n            ),\n            const HSpace(8),\n            Expanded(child: _buildUserName(context, state)),\n            UserSettingButton(),\n            const HSpace(8.0),\n            NotificationButton(key: ValueKey(userProfile.id)),\n            const HSpace(10.0),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildUserName(BuildContext context, MenuUserState state) {\n    final String name = _userName(state.userProfile);\n    return FlowyText.medium(\n      name,\n      overflow: TextOverflow.ellipsis,\n      color: Theme.of(context).colorScheme.tertiary,\n      fontSize: 15.0,\n    );\n  }\n\n  /// Return the user name, if the user name is empty, return the default user name.\n  String _userName(UserProfilePB userProfile) {\n    String name = userProfile.name;\n    if (name.isEmpty) {\n      name = LocaleKeys.defaultUsername.tr();\n    }\n    return name;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_panel.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';\nimport 'package:appflowy/shared/markdown_to_document.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/settings/share/import_service.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/import/import_type.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/file_picker/file_picker_service.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/container.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:path/path.dart' as p;\n\ntypedef ImportCallback = void Function(\n  ImportType type,\n  String name,\n  List<int>? document,\n);\n\nFuture<void> showImportPanel(\n  String parentViewId,\n  BuildContext context,\n  ImportCallback callback,\n) async {\n  await FlowyOverlay.show(\n    context: context,\n    builder: (context) => FlowyDialog(\n      backgroundColor: Theme.of(context).colorScheme.surface,\n      title: FlowyText.semibold(\n        LocaleKeys.moreAction_import.tr(),\n        fontSize: 20,\n        color: Theme.of(context).colorScheme.tertiary,\n      ),\n      child: Padding(\n        padding: const EdgeInsets.symmetric(\n          vertical: 10.0,\n          horizontal: 20.0,\n        ),\n        child: ImportPanel(\n          parentViewId: parentViewId,\n          importCallback: callback,\n        ),\n      ),\n    ),\n  );\n}\n\nclass ImportPanel extends StatefulWidget {\n  const ImportPanel({\n    super.key,\n    required this.parentViewId,\n    required this.importCallback,\n  });\n\n  final String parentViewId;\n  final ImportCallback importCallback;\n\n  @override\n  State<ImportPanel> createState() => _ImportPanelState();\n}\n\nclass _ImportPanelState extends State<ImportPanel> {\n  final flowyContainerFocusNode = FocusNode();\n  final ValueNotifier<bool> showLoading = ValueNotifier(false);\n\n  @override\n  void dispose() {\n    flowyContainerFocusNode.dispose();\n    showLoading.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final width = MediaQuery.of(context).size.width * 0.7;\n    final height = width * 0.5;\n    return KeyboardListener(\n      autofocus: true,\n      focusNode: flowyContainerFocusNode,\n      onKeyEvent: (event) {\n        if (event is KeyDownEvent &&\n            event.physicalKey == PhysicalKeyboardKey.escape) {\n          FlowyOverlay.pop(context);\n        }\n      },\n      child: Stack(\n        children: [\n          FlowyContainer(\n            Theme.of(context).colorScheme.surface,\n            height: height,\n            width: width,\n            child: GridView.count(\n              childAspectRatio: 1 / .2,\n              crossAxisCount: 2,\n              children: ImportType.values\n                  .where((element) => element.enableOnRelease)\n                  .map(\n                    (e) => Card(\n                      child: FlowyButton(\n                        leftIcon: e.icon(context),\n                        leftIconSize: const Size.square(20),\n                        text: FlowyText.medium(\n                          e.toString(),\n                          fontSize: 15,\n                          overflow: TextOverflow.ellipsis,\n                          color: Theme.of(context).colorScheme.tertiary,\n                        ),\n                        onTap: () async {\n                          await _importFile(widget.parentViewId, e);\n                          if (context.mounted) {\n                            FlowyOverlay.pop(context);\n                          }\n                        },\n                      ),\n                    ),\n                  )\n                  .toList(),\n            ),\n          ),\n          ValueListenableBuilder(\n            valueListenable: showLoading,\n            builder: (context, showLoading, child) {\n              if (!showLoading) {\n                return const SizedBox.shrink();\n              }\n              return const Center(\n                child: CircularProgressIndicator(),\n              );\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  Future<void> _importFile(String parentViewId, ImportType importType) async {\n    final result = await getIt<FilePickerService>().pickFiles(\n      type: FileType.custom,\n      allowMultiple: importType.allowMultiSelect,\n      allowedExtensions: importType.allowedExtensions,\n    );\n    if (result == null || result.files.isEmpty) {\n      return;\n    }\n\n    showLoading.value = true;\n\n    final importValues = <ImportItemPayloadPB>[];\n    for (final file in result.files) {\n      final path = file.path;\n      if (path == null) {\n        continue;\n      }\n      final name = p.basenameWithoutExtension(path);\n\n      switch (importType) {\n        case ImportType.historyDatabase:\n          final data = await File(path).readAsString();\n          importValues.add(\n            ImportItemPayloadPB.create()\n              ..name = name\n              ..data = utf8.encode(data)\n              ..viewLayout = ViewLayoutPB.Grid\n              ..importType = ImportTypePB.HistoryDatabase,\n          );\n          break;\n        case ImportType.historyDocument:\n        case ImportType.markdownOrText:\n          final data = await File(path).readAsString();\n          final bytes = _documentDataFrom(importType, data);\n          if (bytes != null) {\n            importValues.add(\n              ImportItemPayloadPB.create()\n                ..name = name\n                ..data = bytes\n                ..viewLayout = ViewLayoutPB.Document\n                ..importType = ImportTypePB.Markdown,\n            );\n          }\n          break;\n        case ImportType.csv:\n          final data = await File(path).readAsString();\n          importValues.add(\n            ImportItemPayloadPB.create()\n              ..name = name\n              ..data = utf8.encode(data)\n              ..viewLayout = ViewLayoutPB.Grid\n              ..importType = ImportTypePB.CSV,\n          );\n          break;\n        case ImportType.afDatabase:\n          final data = await File(path).readAsString();\n          importValues.add(\n            ImportItemPayloadPB.create()\n              ..name = name\n              ..data = utf8.encode(data)\n              ..viewLayout = ViewLayoutPB.Grid\n              ..importType = ImportTypePB.AFDatabase,\n          );\n          break;\n      }\n    }\n\n    if (importValues.isNotEmpty) {\n      await ImportBackendService.importPages(\n        parentViewId,\n        importValues,\n      );\n    }\n\n    showLoading.value = false;\n    widget.importCallback(importType, '', null);\n  }\n}\n\nUint8List? _documentDataFrom(ImportType importType, String data) {\n  switch (importType) {\n    case ImportType.historyDocument:\n      final document = EditorMigration.migrateDocument(data);\n      return DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer();\n    case ImportType.markdownOrText:\n      final document = customMarkdownToDocument(data);\n      return DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer();\n    default:\n      assert(false, 'Unsupported Type $importType');\n      return null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_type.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\n\nenum ImportType {\n  historyDocument,\n  historyDatabase,\n  markdownOrText,\n  csv,\n  afDatabase;\n\n  @override\n  String toString() {\n    switch (this) {\n      case ImportType.historyDocument:\n        return LocaleKeys.importPanel_documentFromV010.tr();\n      case ImportType.historyDatabase:\n        return LocaleKeys.importPanel_databaseFromV010.tr();\n      case ImportType.markdownOrText:\n        return LocaleKeys.importPanel_textAndMarkdown.tr();\n      case ImportType.csv:\n        return LocaleKeys.importPanel_csv.tr();\n      case ImportType.afDatabase:\n        return LocaleKeys.importPanel_database.tr();\n    }\n  }\n\n  WidgetBuilder get icon => (context) {\n        final FlowySvgData svg;\n        switch (this) {\n          case ImportType.historyDatabase:\n            svg = FlowySvgs.document_s;\n          case ImportType.historyDocument:\n          case ImportType.csv:\n          case ImportType.afDatabase:\n            svg = FlowySvgs.board_s;\n          case ImportType.markdownOrText:\n            svg = FlowySvgs.text_s;\n        }\n\n        return FlowySvg(\n          svg,\n          color: Theme.of(context).colorScheme.tertiary,\n        );\n      };\n\n  bool get enableOnRelease {\n    switch (this) {\n      case ImportType.historyDatabase:\n      case ImportType.historyDocument:\n      case ImportType.afDatabase:\n        return kDebugMode;\n      default:\n        return true;\n    }\n  }\n\n  List<String> get allowedExtensions {\n    switch (this) {\n      case ImportType.historyDocument:\n        return ['afdoc'];\n      case ImportType.historyDatabase:\n      case ImportType.afDatabase:\n        return ['afdb'];\n      case ImportType.markdownOrText:\n        return ['md', 'txt'];\n      case ImportType.csv:\n        return ['csv'];\n    }\n  }\n\n  bool get allowMultiSelect {\n    switch (this) {\n      case ImportType.historyDocument:\n      case ImportType.historyDatabase:\n      case ImportType.csv:\n      case ImportType.afDatabase:\n      case ImportType.markdownOrText:\n        return true;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/move_to/move_page_menu.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_search_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\ntypedef MovePageMenuOnSelected = void Function(ViewPB space, ViewPB view);\n\nclass MovePageMenu extends StatefulWidget {\n  const MovePageMenu({\n    super.key,\n    required this.sourceView,\n    required this.onSelected,\n  });\n\n  final ViewPB sourceView;\n  final MovePageMenuOnSelected onSelected;\n\n  @override\n  State<MovePageMenu> createState() => _MovePageMenuState();\n}\n\nclass _MovePageMenuState extends State<MovePageMenu> {\n  final isExpandedNotifier = PropertyValueNotifier(true);\n  final isHoveredNotifier = ValueNotifier(true);\n\n  @override\n  void dispose() {\n    isExpandedNotifier.dispose();\n    isHoveredNotifier.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) => SpaceSearchBloc()..add(const SpaceSearchEvent.initial()),\n      child: BlocBuilder<SpaceBloc, SpaceState>(\n        builder: (context, state) {\n          final space = state.currentSpace;\n          if (space == null) {\n            return const SizedBox.shrink();\n          }\n\n          return Column(\n            children: [\n              SpaceSearchField(\n                width: 240,\n                onSearch: (context, value) => context\n                    .read<SpaceSearchBloc>()\n                    .add(SpaceSearchEvent.search(value)),\n              ),\n              const VSpace(10),\n              BlocBuilder<SpaceSearchBloc, SpaceSearchState>(\n                builder: (context, state) {\n                  if (state.queryResults == null) {\n                    return Expanded(child: _buildSpace(space));\n                  }\n                  return Expanded(\n                    child: _buildGroupedViews(space, state.queryResults!),\n                  );\n                },\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildGroupedViews(ViewPB space, List<ViewPB> views) {\n    final groupedViews = views\n        .where((v) => !_shouldIgnoreView(v, widget.sourceView) && !v.isSpace)\n        .toList();\n    return _MovePageGroupedViews(\n      views: groupedViews,\n      onSelected: (view) => widget.onSelected(space, view),\n    );\n  }\n\n  Column _buildSpace(ViewPB space) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        SpacePopup(\n          useIntrinsicWidth: false,\n          expand: true,\n          height: 30,\n          showCreateButton: false,\n          child: FlowyTooltip(\n            message: LocaleKeys.space_switchSpace.tr(),\n            child: CurrentSpace(\n              // move the page to current space\n              onTapBlankArea: () => widget.onSelected(space, space),\n              space: space,\n            ),\n          ),\n        ),\n        Expanded(\n          child: SingleChildScrollView(\n            physics: const ClampingScrollPhysics(),\n            child: SpacePages(\n              key: ValueKey(space.id),\n              space: space,\n              isHovered: isHoveredNotifier,\n              isExpandedNotifier: isExpandedNotifier,\n              shouldIgnoreView: (view) {\n                if (_shouldIgnoreView(view, widget.sourceView)) {\n                  return IgnoreViewType.hide;\n                }\n                if (view.layout != ViewLayoutPB.Document) {\n                  return IgnoreViewType.disable;\n                }\n                return IgnoreViewType.none;\n              },\n              // hide the hover status and disable the editing actions\n              disableSelectedStatus: true,\n              // hide the ... and + buttons\n              rightIconsBuilder: (context, view) => [],\n              onSelected: (_, view) => widget.onSelected(space, view),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass _MovePageGroupedViews extends StatelessWidget {\n  const _MovePageGroupedViews({required this.views, required this.onSelected});\n\n  final List<ViewPB> views;\n  final void Function(ViewPB view) onSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleChildScrollView(\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: views\n            .map(\n              (view) => ViewItem(\n                key: ValueKey(view.id),\n                view: view,\n                spaceType: FolderSpaceType.unknown,\n                level: 0,\n                onSelected: (_, view) => onSelected(view),\n                isFeedback: false,\n                isDraggable: false,\n                shouldRenderChildren: false,\n                leftIconBuilder: (_, __) => const HSpace(0.0),\n                rightIconsBuilder: (_, view) => [],\n              ),\n            )\n            .toList(),\n      ),\n    );\n  }\n}\n\nbool _shouldIgnoreView(ViewPB view, ViewPB sourceView) {\n  return view.id == sourceView.id;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SidebarFolder extends StatelessWidget {\n  const SidebarFolder({\n    super.key,\n    this.isHoverEnabled = true,\n    required this.userProfile,\n  });\n\n  final bool isHoverEnabled;\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    const sectionPadding = 16.0;\n    return ValueListenableBuilder(\n      valueListenable: getIt<MenuSharedState>().notifier,\n      builder: (context, value, child) {\n        return Column(\n          children: [\n            const VSpace(4.0),\n            // favorite\n            BlocBuilder<FavoriteBloc, FavoriteState>(\n              builder: (context, state) {\n                if (state.views.isEmpty) {\n                  return const SizedBox.shrink();\n                }\n                return FavoriteFolder(\n                  views: state.views.map((e) => e.item).toList(),\n                );\n              },\n            ),\n            // public or private\n            BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(\n              builder: (context, state) {\n                // only show public and private section if the workspace is collaborative and not local\n                final isCollaborativeWorkspace =\n                    context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn;\n\n                // only show public and private section if the workspace is collaborative\n                return Column(\n                  children: isCollaborativeWorkspace\n                      ? [\n                          // public\n                          const VSpace(sectionPadding),\n                          PublicSectionFolder(views: state.section.publicViews),\n\n                          // private\n                          const VSpace(sectionPadding),\n                          PrivateSectionFolder(\n                            views: state.section.privateViews,\n                          ),\n                        ]\n                      : [\n                          // personal\n                          const VSpace(sectionPadding),\n                          PersonalSectionFolder(\n                            views: state.section.publicViews,\n                          ),\n                        ],\n                );\n              },\n            ),\n            const VSpace(200),\n          ],\n        );\n      },\n    );\n  }\n}\n\nclass PrivateSectionFolder extends SectionFolder {\n  PrivateSectionFolder({super.key, required super.views})\n      : super(\n          title: LocaleKeys.sideBar_private.tr(),\n          spaceType: FolderSpaceType.private,\n          expandButtonTooltip: LocaleKeys.sideBar_clickToHidePrivate.tr(),\n          addButtonTooltip: LocaleKeys.sideBar_addAPageToPrivate.tr(),\n        );\n}\n\nclass PublicSectionFolder extends SectionFolder {\n  PublicSectionFolder({super.key, required super.views})\n      : super(\n          title: LocaleKeys.sideBar_workspace.tr(),\n          spaceType: FolderSpaceType.public,\n          expandButtonTooltip: LocaleKeys.sideBar_clickToHideWorkspace.tr(),\n          addButtonTooltip: LocaleKeys.sideBar_addAPageToWorkspace.tr(),\n        );\n}\n\nclass PersonalSectionFolder extends SectionFolder {\n  PersonalSectionFolder({super.key, required super.views})\n      : super(\n          title: LocaleKeys.sideBar_personal.tr(),\n          spaceType: FolderSpaceType.public,\n          expandButtonTooltip: LocaleKeys.sideBar_clickToHidePersonal.tr(),\n          addButtonTooltip: LocaleKeys.sideBar_addAPage.tr(),\n        );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy/workspace/presentation/home/hotkeys.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SidebarNewPageButton extends StatefulWidget {\n  const SidebarNewPageButton({\n    super.key,\n  });\n\n  @override\n  State<SidebarNewPageButton> createState() => _SidebarNewPageButtonState();\n}\n\nclass _SidebarNewPageButtonState extends State<SidebarNewPageButton> {\n  @override\n  void initState() {\n    super.initState();\n    createNewPageNotifier.addListener(_createNewPage);\n  }\n\n  @override\n  void dispose() {\n    createNewPageNotifier.removeListener(_createNewPage);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.symmetric(horizontal: 8),\n      height: HomeSizes.newPageSectionHeight,\n      child: FlowyButton(\n        onTap: () async => _createNewPage(),\n        leftIcon: const FlowySvg(\n          FlowySvgs.new_app_m,\n          blendMode: null,\n        ),\n        leftIconSize: const Size.square(24.0),\n        margin: const EdgeInsets.only(left: 4.0),\n        iconPadding: 8.0,\n        text: FlowyText.regular(\n          LocaleKeys.newPageText.tr(),\n          lineHeight: 1.15,\n        ),\n      ),\n    );\n  }\n\n  Future<void> _createNewPage() async {\n    // if the workspace is collaborative, create the view in the private section by default.\n    final section = context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn\n        ? ViewSectionPB.Private\n        : ViewSectionPB.Public;\n    final spaceState = context.read<SpaceBloc>().state;\n    if (spaceState.spaces.isNotEmpty) {\n      context.read<SpaceBloc>().add(\n            const SpaceEvent.createPage(\n              name: '',\n              index: 0,\n              layout: ViewLayoutPB.Document,\n              openAfterCreate: true,\n            ),\n          );\n    } else {\n      context.read<SidebarSectionsBloc>().add(\n            SidebarSectionsEvent.createRootViewInSection(\n              name: '',\n              viewSection: section,\n              index: 0,\n            ),\n          );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/password/password_bloc.dart';\nimport 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/af_focus_manager.dart';\nimport 'package:appflowy/workspace/presentation/home/hotkeys.dart';\nimport 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:hotkey_manager/hotkey_manager.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nfinal GlobalKey _settingsDialogKey = GlobalKey();\n\nHotKeyItem openSettingsHotKey(\n  BuildContext context,\n) =>\n    HotKeyItem(\n      hotKey: HotKey(\n        KeyCode.comma,\n        scope: HotKeyScope.inapp,\n        modifiers: [\n          UniversalPlatform.isMacOS ? KeyModifier.meta : KeyModifier.control,\n        ],\n      ),\n      keyDownHandler: (_) {\n        if (_settingsDialogKey.currentContext == null) {\n          showSettingsDialog(\n            context,\n            userWorkspaceBloc: context.read<UserWorkspaceBloc>(),\n          );\n        } else {\n          Navigator.of(context, rootNavigator: true)\n              .popUntil((route) => route.isFirst);\n        }\n      },\n    );\n\nclass UserSettingButton extends StatefulWidget {\n  const UserSettingButton({\n    super.key,\n    this.isHover = false,\n  });\n\n  final bool isHover;\n\n  @override\n  State<UserSettingButton> createState() => _UserSettingButtonState();\n}\n\nclass _UserSettingButtonState extends State<UserSettingButton> {\n  late UserWorkspaceBloc _userWorkspaceBloc;\n  late PasswordBloc _passwordBloc;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _userWorkspaceBloc = context.read<UserWorkspaceBloc>();\n    _passwordBloc = PasswordBloc(_userWorkspaceBloc.state.userProfile)\n      ..add(PasswordEvent.init())\n      ..add(PasswordEvent.checkHasPassword());\n  }\n\n  @override\n  void didChangeDependencies() {\n    _userWorkspaceBloc = context.read<UserWorkspaceBloc>();\n\n    super.didChangeDependencies();\n  }\n\n  @override\n  void dispose() {\n    _passwordBloc.close();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox.square(\n      dimension: 28.0,\n      child: FlowyTooltip(\n        message: LocaleKeys.settings_menu_open.tr(),\n        child: BlocProvider.value(\n          value: _passwordBloc,\n          child: FlowyButton(\n            onTap: () => showSettingsDialog(\n              context,\n              userWorkspaceBloc: _userWorkspaceBloc,\n              passwordBloc: _passwordBloc,\n            ),\n            margin: EdgeInsets.zero,\n            text: FlowySvg(\n              FlowySvgs.settings_s,\n              color: widget.isHover\n                  ? Theme.of(context).colorScheme.onSurface\n                  : null,\n              opacity: 0.7,\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nvoid showSettingsDialog(\n  BuildContext context, {\n  required UserWorkspaceBloc userWorkspaceBloc,\n  PasswordBloc? passwordBloc,\n  SettingsPage? initPage,\n}) {\n  final userProfile = context.read<UserWorkspaceBloc>().state.userProfile;\n  AFFocusManager.maybeOf(context)?.notifyLoseFocus();\n  showDialog(\n    context: context,\n    builder: (dialogContext) => MultiBlocProvider(\n      key: _settingsDialogKey,\n      providers: [\n        passwordBloc != null\n            ? BlocProvider<PasswordBloc>.value(\n                value: passwordBloc,\n              )\n            : BlocProvider(\n                create: (context) => PasswordBloc(userProfile)\n                  ..add(PasswordEvent.init())\n                  ..add(PasswordEvent.checkHasPassword()),\n              ),\n        BlocProvider<DocumentAppearanceCubit>.value(\n          value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),\n        ),\n        BlocProvider.value(\n          value: userWorkspaceBloc,\n        ),\n      ],\n      child: SettingsDialog(\n        userProfile,\n        initPage: initPage,\n        didLogout: () async {\n          // Pop the dialog using the dialog context\n          Navigator.of(dialogContext).pop();\n          await runAppFlowy();\n        },\n        dismissDialog: () {\n          if (Navigator.of(dialogContext).canPop()) {\n            return Navigator.of(dialogContext).pop();\n          }\n          Log.warn(\"Can't pop dialog context\");\n        },\n        restartApp: () async {\n          // Pop the dialog using the dialog context\n          Navigator.of(dialogContext).pop();\n          await runAppFlowy();\n        },\n      ),\n    ),\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/search/view_ancestor_cache.dart';\nimport 'package:appflowy/plugins/blank/blank.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_notification.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/shared/loading.dart';\nimport 'package:appflowy/shared/version_checker/version_checker.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/startup/tasks/device_info_task.dart';\nimport 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';\nimport 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';\nimport 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/favorite/prelude.dart';\nimport 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';\nimport 'package:appflowy/workspace/application/recent/cached_recent_service.dart';\nimport 'package:appflowy/workspace/application/sidebar/billing/sidebar_plan_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_upgrade_application_button.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_migration.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'\n    show UserProfilePB;\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nLoading? _duplicateSpaceLoading;\n\n/// Home Sidebar is the left side bar of the home page.\n///\n/// in the sidebar, we have:\n///   - user icon, user name\n///   - settings\n///   - scrollable document list\n///   - trash\nclass HomeSideBar extends StatelessWidget {\n  const HomeSideBar({\n    super.key,\n    required this.userProfile,\n    required this.workspaceSetting,\n  });\n\n  final UserProfilePB userProfile;\n\n  final WorkspaceLatestPB workspaceSetting;\n\n  @override\n  Widget build(BuildContext context) {\n    // Workspace Bloc: control the current workspace\n    //   |\n    //   +-- Workspace Menu\n    //   |    |\n    //   |    +-- Workspace List: control to switch workspace\n    //   |    |\n    //   |    +-- Workspace Settings\n    //   |    |\n    //   |    +-- Notification Center\n    //   |\n    //   +-- Favorite Section\n    //   |\n    //   +-- Public Or Private Section: control the sections of the workspace\n    //   |\n    //   +-- Trash Section\n    return BlocProvider(\n      create: (context) => SidebarPlanBloc()\n        ..add(SidebarPlanEvent.init(workspaceSetting.workspaceId, userProfile)),\n      child: BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(\n        listenWhen: (prev, curr) =>\n            prev.currentWorkspace?.workspaceId !=\n            curr.currentWorkspace?.workspaceId,\n        listener: (context, state) {\n          if (FeatureFlag.search.isOn) {\n            // Notify command palette that workspace has changed\n            context.read<CommandPaletteBloc>().add(\n                  CommandPaletteEvent.workspaceChanged(\n                    workspaceId: state.currentWorkspace?.workspaceId,\n                  ),\n                );\n          }\n\n          if (state.currentWorkspace != null) {\n            context.read<SidebarPlanBloc>().add(\n                  SidebarPlanEvent.changedWorkspace(\n                    workspaceId: state.currentWorkspace!.workspaceId,\n                  ),\n                );\n          }\n\n          // Re-initialize workspace-specific services\n          getIt<CachedRecentService>().reset();\n        },\n        // Rebuild the whole sidebar when the current workspace changes\n        buildWhen: (previous, current) =>\n            previous.currentWorkspace?.workspaceId !=\n            current.currentWorkspace?.workspaceId,\n        builder: (context, state) {\n          if (state.currentWorkspace == null) {\n            return const SizedBox.shrink();\n          }\n\n          final workspaceId = state.currentWorkspace?.workspaceId ??\n              workspaceSetting.workspaceId;\n          return MultiBlocProvider(\n            providers: [\n              BlocProvider.value(value: getIt<ActionNavigationBloc>()),\n              BlocProvider(\n                create: (_) => SidebarSectionsBloc()\n                  ..add(SidebarSectionsEvent.initial(userProfile, workspaceId)),\n              ),\n              BlocProvider(\n                create: (_) => SpaceBloc(\n                  userProfile: userProfile,\n                  workspaceId: workspaceId,\n                )..add(const SpaceEvent.initial(openFirstPage: false)),\n              ),\n            ],\n            child: MultiBlocListener(\n              listeners: [\n                BlocListener<SidebarSectionsBloc, SidebarSectionsState>(\n                  listenWhen: (p, c) =>\n                      p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,\n                  listener: (context, state) => context.read<TabsBloc>().add(\n                        TabsEvent.openPlugin(\n                          plugin: state.lastCreatedRootView!.plugin(),\n                        ),\n                      ),\n                ),\n                BlocListener<SpaceBloc, SpaceState>(\n                  listenWhen: (prev, curr) =>\n                      prev.lastCreatedPage?.id != curr.lastCreatedPage?.id ||\n                      prev.isDuplicatingSpace != curr.isDuplicatingSpace,\n                  listener: (context, state) {\n                    final page = state.lastCreatedPage;\n                    if (page == null || page.id.isEmpty) {\n                      // open the blank page\n                      context\n                          .read<TabsBloc>()\n                          .add(TabsEvent.openPlugin(plugin: BlankPagePlugin()));\n                    } else {\n                      context.read<TabsBloc>().add(\n                            TabsEvent.openPlugin(\n                              plugin: state.lastCreatedPage!.plugin(),\n                            ),\n                          );\n                    }\n\n                    if (state.isDuplicatingSpace) {\n                      _duplicateSpaceLoading ??= Loading(context);\n                      _duplicateSpaceLoading?.start();\n                    } else if (_duplicateSpaceLoading != null) {\n                      _duplicateSpaceLoading?.stop();\n                      _duplicateSpaceLoading = null;\n                    }\n                  },\n                ),\n                BlocListener<ActionNavigationBloc, ActionNavigationState>(\n                  listenWhen: (_, curr) => curr.action != null,\n                  listener: _onNotificationAction,\n                ),\n                BlocListener<UserWorkspaceBloc, UserWorkspaceState>(\n                  listener: (context, state) {\n                    final actionType = state.actionResult?.actionType;\n\n                    if (actionType == WorkspaceActionType.create ||\n                        actionType == WorkspaceActionType.delete ||\n                        actionType == WorkspaceActionType.open) {\n                      if (context.read<SpaceBloc>().state.spaces.isEmpty) {\n                        context.read<SidebarSectionsBloc>().add(\n                              SidebarSectionsEvent.reload(\n                                userProfile,\n                                state.currentWorkspace?.workspaceId ??\n                                    workspaceSetting.workspaceId,\n                              ),\n                            );\n                      } else {\n                        context.read<SpaceBloc>().add(\n                              SpaceEvent.reset(\n                                userProfile,\n                                state.currentWorkspace?.workspaceId ??\n                                    workspaceSetting.workspaceId,\n                                true,\n                              ),\n                            );\n                      }\n\n                      context\n                          .read<FavoriteBloc>()\n                          .add(const FavoriteEvent.fetchFavorites());\n                    }\n                  },\n                ),\n              ],\n              child: _Sidebar(userProfile: userProfile),\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  void _onNotificationAction(\n    BuildContext context,\n    ActionNavigationState state,\n  ) {\n    final action = state.action;\n    if (action?.type == ActionType.openView) {\n      final view = action!.arguments?[ActionArgumentKeys.view];\n      if (view != null) {\n        final Map<String, dynamic> arguments = {};\n        final nodePath = action.arguments?[ActionArgumentKeys.nodePath];\n        if (nodePath != null) {\n          arguments[PluginArgumentKeys.selection] = Selection.collapsed(\n            Position(path: [nodePath]),\n          );\n        }\n\n        checkForSpace(\n          context.read<SpaceBloc>(),\n          view,\n          () => openView(action, context, view, arguments),\n        );\n        openView(action, context, view, arguments);\n      }\n    }\n  }\n\n  Future<void> checkForSpace(\n    SpaceBloc spaceBloc,\n    ViewPB view,\n    VoidCallback afterOpen,\n  ) async {\n    /// open space\n    final acestorCache = getIt<ViewAncestorCache>();\n    final ancestor = await acestorCache.getAncestor(view.id);\n    if (ancestor?.ancestors.isEmpty ?? true) return;\n    final firstAncestor = ancestor!.ancestors.first;\n    if (firstAncestor.id != spaceBloc.state.currentSpace?.id) {\n      final space =\n          (await ViewBackendService.getView(firstAncestor.id)).toNullable();\n      if (space != null) {\n        Log.info(\n          'Switching space from (${firstAncestor.name}-${firstAncestor.id}) to (${space.name}-${space.id})',\n        );\n        spaceBloc.add(SpaceEvent.open(space: space, afterOpen: afterOpen));\n      }\n    }\n  }\n\n  void openView(\n    NavigationAction action,\n    BuildContext context,\n    ViewPB view,\n    Map<String, dynamic> arguments,\n  ) {\n    final blockId = action.arguments?[ActionArgumentKeys.blockId];\n    if (blockId != null) {\n      arguments[PluginArgumentKeys.blockId] = blockId;\n    }\n\n    final rowId = action.arguments?[ActionArgumentKeys.rowId];\n    if (rowId != null) {\n      arguments[PluginArgumentKeys.rowId] = rowId;\n    }\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      if (context.mounted) {\n        context.read<TabsBloc>().openPlugin(view, arguments: arguments);\n      }\n    });\n  }\n}\n\nclass _Sidebar extends StatefulWidget {\n  const _Sidebar({required this.userProfile});\n\n  final UserProfilePB userProfile;\n\n  @override\n  State<_Sidebar> createState() => _SidebarState();\n}\n\nclass _SidebarState extends State<_Sidebar> {\n  final _scrollController = ScrollController();\n  Timer? _scrollDebounce;\n  bool _isScrolling = false;\n  final _isHovered = ValueNotifier(false);\n  final _scrollOffset = ValueNotifier<double>(0);\n\n  // mute the update button during the current application lifecycle.\n  final _muteUpdateButton = ValueNotifier(false);\n\n  @override\n  void initState() {\n    super.initState();\n    _scrollController.addListener(_onScrollChanged);\n  }\n\n  @override\n  void dispose() {\n    _scrollDebounce?.cancel();\n    _scrollController.removeListener(_onScrollChanged);\n    _scrollController.dispose();\n    _scrollOffset.dispose();\n    _isHovered.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    const menuHorizontalInset = EdgeInsets.symmetric(horizontal: 8);\n    return MouseRegion(\n      onEnter: (_) => _isHovered.value = true,\n      onExit: (_) => _isHovered.value = false,\n      child: DecoratedBox(\n        decoration: BoxDecoration(\n          color: Theme.of(context).colorScheme.surfaceContainerHighest,\n          border: Border(\n            right: BorderSide(color: Theme.of(context).dividerColor),\n          ),\n        ),\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            // top menu\n            Padding(\n              padding: menuHorizontalInset,\n              child: SidebarTopMenu(\n                isSidebarOnHover: _isHovered,\n              ),\n            ),\n            // user or workspace, setting\n            BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(\n              builder: (context, state) => Container(\n                height: HomeSizes.workspaceSectionHeight,\n                padding: menuHorizontalInset - const EdgeInsets.only(right: 6),\n                // if the workspaces are empty, show the user profile instead\n                child: state.isCollabWorkspaceOn && state.workspaces.isNotEmpty\n                    ? SidebarWorkspace(userProfile: widget.userProfile)\n                    : SidebarUser(userProfile: widget.userProfile),\n              ),\n            ),\n            if (FeatureFlag.search.isOn) ...[\n              const VSpace(6),\n              Container(\n                padding: menuHorizontalInset,\n                height: HomeSizes.searchSectionHeight,\n                child: const _SidebarSearchButton(),\n              ),\n            ],\n\n            if (context\n                    .read<UserWorkspaceBloc>()\n                    .state\n                    .currentWorkspace\n                    ?.role !=\n                AFRolePB.Guest) ...[\n              const VSpace(6.0),\n              // new page button\n              const SidebarNewPageButton(),\n            ],\n\n            // scrollable document list\n            const VSpace(12.0),\n            Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 12.0),\n              child: ValueListenableBuilder(\n                valueListenable: _scrollOffset,\n                builder: (_, offset, child) => Opacity(\n                  opacity: offset > 0 ? 1 : 0,\n                  child: child,\n                ),\n                child: const FlowyDivider(),\n              ),\n            ),\n\n            _renderFolderOrSpace(menuHorizontalInset),\n\n            // trash\n            Padding(\n              padding: menuHorizontalInset +\n                  const EdgeInsets.symmetric(horizontal: 4.0),\n              child: const FlowyDivider(),\n            ),\n            const VSpace(8),\n\n            _renderUpgradeSpaceButton(menuHorizontalInset),\n            _buildUpgradeApplicationButton(menuHorizontalInset),\n\n            const VSpace(8),\n            Padding(\n              padding: menuHorizontalInset +\n                  const EdgeInsets.symmetric(horizontal: 4.0),\n              child: const SidebarFooter(),\n            ),\n            const VSpace(14),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _renderFolderOrSpace(EdgeInsets menuHorizontalInset) {\n    final spaceState = context.read<SpaceBloc>().state;\n    final workspaceState = context.read<UserWorkspaceBloc>().state;\n\n    if (!spaceState.isInitialized) {\n      return const SizedBox.shrink();\n    }\n\n    // there's no space or the workspace is not collaborative,\n    // show the folder section (Workspace, Private, Personal)\n    // otherwise, show the space\n    final sidebarSectionBloc = context.watch<SidebarSectionsBloc>();\n    final containsSpace = sidebarSectionBloc.state.containsSpace;\n\n    if (containsSpace && spaceState.spaces.isEmpty) {\n      context.read<SpaceBloc>().add(const SpaceEvent.didReceiveSpaceUpdate());\n    }\n\n    return !containsSpace ||\n            spaceState.spaces.isEmpty ||\n            !workspaceState.isCollabWorkspaceOn\n        ? Expanded(\n            child: Padding(\n              padding: menuHorizontalInset - const EdgeInsets.only(right: 6),\n              child: SingleChildScrollView(\n                padding: const EdgeInsets.only(right: 6),\n                controller: _scrollController,\n                physics: const ClampingScrollPhysics(),\n                child: SidebarFolder(\n                  userProfile: widget.userProfile,\n                  isHoverEnabled: !_isScrolling,\n                ),\n              ),\n            ),\n          )\n        : Expanded(\n            child: Padding(\n              padding: menuHorizontalInset - const EdgeInsets.only(right: 6),\n              child: FlowyScrollbar(\n                controller: _scrollController,\n                child: SingleChildScrollView(\n                  padding: const EdgeInsets.only(right: 6),\n                  controller: _scrollController,\n                  physics: const ClampingScrollPhysics(),\n                  child: SidebarSpace(\n                    userProfile: widget.userProfile,\n                    isHoverEnabled: !_isScrolling,\n                  ),\n                ),\n              ),\n            ),\n          );\n  }\n\n  Widget _renderUpgradeSpaceButton(EdgeInsets menuHorizontalInset) {\n    final spaceState = context.watch<SpaceBloc>().state;\n    final workspaceState = context.read<UserWorkspaceBloc>().state;\n    return !spaceState.shouldShowUpgradeDialog ||\n            !workspaceState.isCollabWorkspaceOn\n        ? const SizedBox.shrink()\n        : Padding(\n            padding: menuHorizontalInset +\n                const EdgeInsets.only(\n                  left: 4.0,\n                  right: 4.0,\n                  top: 8.0,\n                ),\n            child: const SpaceMigration(),\n          );\n  }\n\n  Widget _buildUpgradeApplicationButton(EdgeInsets menuHorizontalInset) {\n    return ValueListenableBuilder(\n      valueListenable: _muteUpdateButton,\n      builder: (_, mute, child) {\n        if (mute) {\n          return const SizedBox.shrink();\n        }\n\n        return ValueListenableBuilder(\n          valueListenable: ApplicationInfo.latestVersionNotifier,\n          builder: (_, latestVersion, child) {\n            if (!ApplicationInfo.isUpdateAvailable) {\n              return const SizedBox.shrink();\n            }\n\n            return Padding(\n              padding: menuHorizontalInset +\n                  const EdgeInsets.only(\n                    left: 4.0,\n                    right: 4.0,\n                  ),\n              child: SidebarUpgradeApplicationButton(\n                onUpdateButtonTap: () {\n                  versionChecker.checkForUpdate();\n                },\n                onCloseButtonTap: () {\n                  _muteUpdateButton.value = true;\n                },\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  void _onScrollChanged() {\n    setState(() => _isScrolling = true);\n\n    _scrollDebounce?.cancel();\n    _scrollDebounce =\n        Timer(const Duration(milliseconds: 300), _setScrollStopped);\n\n    _scrollOffset.value = _scrollController.offset;\n  }\n\n  void _setScrollStopped() {\n    if (mounted) {\n      setState(() => _isScrolling = false);\n    }\n  }\n}\n\nclass _SidebarSearchButton extends StatelessWidget {\n  const _SidebarSearchButton();\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      richMessage: TextSpan(\n        children: [\n          TextSpan(\n            text: '${LocaleKeys.search_sidebarSearchIcon.tr()}\\n',\n            style: context.tooltipTextStyle(),\n          ),\n          TextSpan(\n            text: Platform.isMacOS ? '⌘+P' : 'Ctrl+P',\n            style: context\n                .tooltipTextStyle()\n                ?.copyWith(color: Theme.of(context).hintColor),\n          ),\n        ],\n      ),\n      child: FlowyButton(\n        onTap: () {\n          // exit editing mode when doing search to avoid the toolbar showing up\n          EditorNotification.exitEditing().post();\n          final workspaceBloc = context.read<UserWorkspaceBloc?>();\n          final spaceBloc = context.read<SpaceBloc?>();\n          CommandPalette.of(context).toggle(\n            workspaceBloc: workspaceBloc,\n            spaceBloc: spaceBloc,\n          );\n        },\n        leftIcon: const FlowySvg(FlowySvgs.search_s),\n        iconPadding: 12.0,\n        margin: const EdgeInsets.only(left: 8.0),\n        text: FlowyText.regular(LocaleKeys.search_label.tr()),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/slider_menu_hover_trigger.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass SliderMenuHoverTrigger extends StatelessWidget {\n  const SliderMenuHoverTrigger({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      width: 50,\n      color: Colors.black,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/_extension.dart",
    "content": "import 'package:appflowy/util/theme_extension.dart';\nimport 'package:flutter/material.dart';\n\nextension SpacePermissionColorExtension on BuildContext {\n  Color get enableBorderColor => Theme.of(this).isLightMode\n      ? const Color(0x1E171717)\n      : const Color(0xFF3A3F49);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/create_space_popup.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/_extension.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass CreateSpacePopup extends StatefulWidget {\n  const CreateSpacePopup({super.key});\n\n  @override\n  State<CreateSpacePopup> createState() => _CreateSpacePopupState();\n}\n\nclass _CreateSpacePopupState extends State<CreateSpacePopup> {\n  String spaceName = LocaleKeys.space_defaultSpaceName.tr();\n  String? spaceIcon = kDefaultSpaceIconId;\n  String? spaceIconColor = builtInSpaceColors.first;\n  SpacePermission spacePermission = SpacePermission.publicToAll;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),\n      width: 524,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          FlowyText(\n            LocaleKeys.space_createNewSpace.tr(),\n            fontSize: 18.0,\n            figmaLineHeight: 24.0,\n          ),\n          const VSpace(2.0),\n          FlowyText(\n            LocaleKeys.space_createSpaceDescription.tr(),\n            fontSize: 14.0,\n            fontWeight: FontWeight.w300,\n            color: Theme.of(context).hintColor,\n            figmaLineHeight: 18.0,\n            maxLines: 2,\n          ),\n          const VSpace(16.0),\n          SizedBox.square(\n            dimension: 56,\n            child: SpaceIconPopup(\n              onIconChanged: (icon, iconColor) {\n                spaceIcon = icon;\n                spaceIconColor = iconColor;\n              },\n            ),\n          ),\n          const VSpace(8.0),\n          _SpaceNameTextField(\n            onChanged: (value) => spaceName = value,\n            onSubmitted: (value) {\n              spaceName = value;\n              _createSpace();\n            },\n          ),\n          const VSpace(20.0),\n          SpacePermissionSwitch(\n            onPermissionChanged: (value) => spacePermission = value,\n          ),\n          const VSpace(20.0),\n          SpaceCancelOrConfirmButton(\n            confirmButtonName: LocaleKeys.button_create.tr(),\n            onCancel: () => Navigator.of(context).pop(),\n            onConfirm: () => _createSpace(),\n          ),\n        ],\n      ),\n    );\n  }\n\n  void _createSpace() {\n    context.read<SpaceBloc>().add(\n          SpaceEvent.create(\n            name: spaceName,\n            // fixme: space issue\n            icon: spaceIcon!,\n            iconColor: spaceIconColor!,\n            permission: spacePermission,\n            createNewPageByDefault: true,\n            openAfterCreate: true,\n          ),\n        );\n\n    Navigator.of(context).pop();\n  }\n}\n\nclass _SpaceNameTextField extends StatelessWidget {\n  const _SpaceNameTextField({\n    required this.onChanged,\n    required this.onSubmitted,\n  });\n\n  final void Function(String name) onChanged;\n  final void Function(String name) onSubmitted;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        FlowyText.regular(\n          LocaleKeys.space_spaceName.tr(),\n          fontSize: 14.0,\n          color: Theme.of(context).hintColor,\n          figmaLineHeight: 18.0,\n        ),\n        const VSpace(6.0),\n        SizedBox(\n          height: 40,\n          child: FlowyTextField(\n            hintText: LocaleKeys.space_spaceNamePlaceholder.tr(),\n            onChanged: onChanged,\n            onSubmitted: onSubmitted,\n            enableBorderColor: context.enableBorderColor,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/manage_space_popup.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass ManageSpacePopup extends StatefulWidget {\n  const ManageSpacePopup({super.key});\n\n  @override\n  State<ManageSpacePopup> createState() => _ManageSpacePopupState();\n}\n\nclass _ManageSpacePopupState extends State<ManageSpacePopup> {\n  String? spaceName;\n  String? spaceIcon;\n  String? spaceIconColor;\n  SpacePermission? spacePermission;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),\n      width: 500,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          FlowyText(\n            LocaleKeys.space_manage.tr(),\n            fontSize: 18.0,\n          ),\n          const VSpace(16.0),\n          _SpaceNameTextField(\n            onNameChanged: (name) => spaceName = name,\n            onIconChanged: (icon, color) {\n              spaceIcon = icon;\n              spaceIconColor = color;\n            },\n          ),\n          const VSpace(16.0),\n          SpacePermissionSwitch(\n            spacePermission:\n                context.read<SpaceBloc>().state.currentSpace?.spacePermission,\n            onPermissionChanged: (value) => spacePermission = value,\n          ),\n          const VSpace(16.0),\n          SpaceCancelOrConfirmButton(\n            confirmButtonName: LocaleKeys.button_save.tr(),\n            onCancel: () => Navigator.of(context).pop(),\n            onConfirm: () {\n              context.read<SpaceBloc>().add(\n                    SpaceEvent.update(\n                      name: spaceName,\n                      icon: spaceIcon,\n                      iconColor: spaceIconColor,\n                      permission: spacePermission,\n                    ),\n                  );\n\n              Navigator.of(context).pop();\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _SpaceNameTextField extends StatelessWidget {\n  const _SpaceNameTextField({\n    required this.onNameChanged,\n    required this.onIconChanged,\n  });\n\n  final void Function(String name) onNameChanged;\n  final void Function(String? icon, String? color) onIconChanged;\n\n  @override\n  Widget build(BuildContext context) {\n    final space = context.read<SpaceBloc>().state.currentSpace;\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        FlowyText.regular(\n          LocaleKeys.space_spaceName.tr(),\n          fontSize: 14.0,\n          color: Theme.of(context).hintColor,\n        ),\n        const VSpace(8.0),\n        SizedBox(\n          height: 40,\n          child: Row(\n            children: [\n              SizedBox.square(\n                dimension: 40,\n                child: SpaceIconPopup(\n                  space: space,\n                  cornerRadius: 12,\n                  icon: space?.spaceIcon,\n                  iconColor: space?.spaceIconColor,\n                  onIconChanged: onIconChanged,\n                ),\n              ),\n              const HSpace(12),\n              Expanded(\n                child: SizedBox(\n                  height: 40,\n                  child: FlowyTextField(\n                    text: space?.name,\n                    onChanged: onNameChanged,\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/_extension.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass SpacePermissionSwitch extends StatefulWidget {\n  const SpacePermissionSwitch({\n    super.key,\n    required this.onPermissionChanged,\n    this.spacePermission,\n    this.showArrow = false,\n  });\n\n  final SpacePermission? spacePermission;\n  final void Function(SpacePermission permission) onPermissionChanged;\n  final bool showArrow;\n\n  @override\n  State<SpacePermissionSwitch> createState() => _SpacePermissionSwitchState();\n}\n\nclass _SpacePermissionSwitchState extends State<SpacePermissionSwitch> {\n  late SpacePermission spacePermission =\n      widget.spacePermission ?? SpacePermission.publicToAll;\n  final popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        FlowyText.regular(\n          LocaleKeys.space_permission.tr(),\n          fontSize: 14.0,\n          color: Theme.of(context).hintColor,\n          figmaLineHeight: 18.0,\n        ),\n        const VSpace(6.0),\n        AppFlowyPopover(\n          controller: popoverController,\n          direction: PopoverDirection.bottomWithCenterAligned,\n          constraints: const BoxConstraints(maxWidth: 500),\n          offset: const Offset(0, 4),\n          margin: EdgeInsets.zero,\n          popupBuilder: (_) => _buildPermissionButtons(),\n          child: DecoratedBox(\n            decoration: ShapeDecoration(\n              shape: RoundedRectangleBorder(\n                side: BorderSide(color: context.enableBorderColor),\n                borderRadius: BorderRadius.circular(10),\n              ),\n            ),\n            child: SpacePermissionButton(\n              showArrow: true,\n              permission: spacePermission,\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildPermissionButtons() {\n    return SizedBox(\n      width: 452,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          SpacePermissionButton(\n            permission: SpacePermission.publicToAll,\n            onTap: () => _onPermissionChanged(SpacePermission.publicToAll),\n          ),\n          SpacePermissionButton(\n            permission: SpacePermission.private,\n            onTap: () => _onPermissionChanged(SpacePermission.private),\n          ),\n        ],\n      ),\n    );\n  }\n\n  void _onPermissionChanged(SpacePermission permission) {\n    widget.onPermissionChanged(permission);\n\n    setState(() {\n      spacePermission = permission;\n    });\n\n    popoverController.close();\n  }\n}\n\nclass SpacePermissionButton extends StatelessWidget {\n  const SpacePermissionButton({\n    super.key,\n    required this.permission,\n    this.onTap,\n    this.showArrow = false,\n  });\n\n  final SpacePermission permission;\n  final VoidCallback? onTap;\n  final bool showArrow;\n\n  @override\n  Widget build(BuildContext context) {\n    final (title, desc, icon) = switch (permission) {\n      SpacePermission.publicToAll => (\n          LocaleKeys.space_publicPermission.tr(),\n          LocaleKeys.space_publicPermissionDescription.tr(),\n          FlowySvgs.space_permission_public_s\n        ),\n      SpacePermission.private => (\n          LocaleKeys.space_privatePermission.tr(),\n          LocaleKeys.space_privatePermissionDescription.tr(),\n          FlowySvgs.space_permission_private_s\n        ),\n    };\n\n    return FlowyButton(\n      margin: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0),\n      radius: showArrow ? BorderRadius.circular(10) : BorderRadius.zero,\n      iconPadding: 16.0,\n      leftIcon: FlowySvg(icon),\n      leftIconSize: const Size.square(20),\n      rightIcon: showArrow\n          ? const FlowySvg(FlowySvgs.space_permission_dropdown_s)\n          : null,\n      borderColor: Theme.of(context).isLightMode\n          ? const Color(0x1E171717)\n          : const Color(0xFF3A3F49),\n      text: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          FlowyText.regular(title),\n          const VSpace(4.0),\n          FlowyText.regular(\n            desc,\n            fontSize: 12.0,\n            color: Theme.of(context).hintColor,\n          ),\n        ],\n      ),\n      onTap: onTap,\n    );\n  }\n}\n\nclass SpaceCancelOrConfirmButton extends StatelessWidget {\n  const SpaceCancelOrConfirmButton({\n    super.key,\n    required this.onCancel,\n    required this.onConfirm,\n    required this.confirmButtonName,\n    this.confirmButtonColor,\n    this.confirmButtonBuilder,\n  });\n\n  final VoidCallback onCancel;\n  final VoidCallback onConfirm;\n  final String confirmButtonName;\n  final Color? confirmButtonColor;\n  final WidgetBuilder? confirmButtonBuilder;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.end,\n      children: [\n        AFOutlinedTextButton.normal(\n          size: UniversalPlatform.isDesktop ? AFButtonSize.m : AFButtonSize.l,\n          text: LocaleKeys.button_cancel.tr(),\n          textStyle: theme.textStyle.body.standard(\n            color: theme.textColorScheme.primary,\n          ),\n          onTap: onCancel,\n        ),\n        const HSpace(12.0),\n        if (confirmButtonBuilder != null) ...[\n          confirmButtonBuilder!(context),\n        ] else ...[\n          DecoratedBox(\n            decoration: ShapeDecoration(\n              color:\n                  confirmButtonColor ?? Theme.of(context).colorScheme.primary,\n              shape: RoundedRectangleBorder(\n                borderRadius: BorderRadius.circular(8),\n              ),\n            ),\n            child: FlowyButton(\n              useIntrinsicWidth: true,\n              margin:\n                  const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0),\n              radius: BorderRadius.circular(8),\n              text: FlowyText.regular(\n                confirmButtonName,\n                lineHeight: 1.0,\n                color: Theme.of(context).colorScheme.onPrimary,\n              ),\n              onTap: onConfirm,\n            ),\n          ),\n        ],\n      ],\n    );\n  }\n}\n\nclass SpaceOkButton extends StatelessWidget {\n  const SpaceOkButton({\n    super.key,\n    required this.onConfirm,\n    required this.confirmButtonName,\n    this.confirmButtonColor,\n  });\n\n  final VoidCallback onConfirm;\n  final String confirmButtonName;\n  final Color? confirmButtonColor;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.end,\n      children: [\n        PrimaryRoundedButton(\n          text: confirmButtonName,\n          margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0),\n          radius: 8.0,\n          onTap: onConfirm,\n        ),\n      ],\n    );\n  }\n}\n\nenum ConfirmPopupStyle {\n  onlyOk,\n  cancelAndOk,\n}\n\nclass ConfirmPopupColor {\n  static Color titleColor(BuildContext context) {\n    return AppFlowyTheme.of(context).textColorScheme.primary;\n  }\n\n  static Color descriptionColor(BuildContext context) {\n    return AppFlowyTheme.of(context).textColorScheme.primary;\n  }\n}\n\nclass ConfirmPopup extends StatefulWidget {\n  const ConfirmPopup({\n    super.key,\n    this.style = ConfirmPopupStyle.cancelAndOk,\n    required this.title,\n    required this.description,\n    required this.onConfirm,\n    this.onCancel,\n    this.confirmLabel,\n    this.titleStyle,\n    this.descriptionStyle,\n    this.confirmButtonColor,\n    this.confirmButtonBuilder,\n    this.child,\n    this.closeOnAction = true,\n    this.showCloseButton = true,\n    this.enableKeyboardListener = true,\n  });\n\n  final String title;\n  final TextStyle? titleStyle;\n  final String description;\n  final TextStyle? descriptionStyle;\n  final void Function(BuildContext context) onConfirm;\n  final VoidCallback? onCancel;\n  final Color? confirmButtonColor;\n  final ConfirmPopupStyle style;\n\n  /// The label of the confirm button.\n  ///\n  /// Defaults to 'Delete' for [ConfirmPopupStyle.cancelAndOk] style.\n  /// Defaults to 'Ok' for [ConfirmPopupStyle.onlyOk] style.\n  ///\n  final String? confirmLabel;\n\n  /// Allows to add a child to the popup.\n  ///\n  /// This is useful when you want to add more content to the popup.\n  /// The child will be placed below the description.\n  ///\n  final Widget? child;\n\n  /// Decides whether the popup should be closed when the confirm button is clicked.\n  /// Defaults to true.\n  ///\n  final bool closeOnAction;\n\n  /// Show close button.\n  /// Defaults to true.\n  ///\n  final bool showCloseButton;\n\n  /// Enable keyboard listener.\n  /// Defaults to true.\n  ///\n  final bool enableKeyboardListener;\n\n  /// Allows to build a custom confirm button.\n  ///\n  final WidgetBuilder? confirmButtonBuilder;\n\n  @override\n  State<ConfirmPopup> createState() => _ConfirmPopupState();\n}\n\nclass _ConfirmPopupState extends State<ConfirmPopup> {\n  final focusNode = FocusNode();\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return KeyboardListener(\n      focusNode: focusNode,\n      autofocus: true,\n      onKeyEvent: (event) {\n        if (widget.enableKeyboardListener) {\n          if (event is KeyDownEvent &&\n              event.logicalKey == LogicalKeyboardKey.escape) {\n            Navigator.of(context).pop();\n          } else if (event is KeyUpEvent &&\n              event.logicalKey == LogicalKeyboardKey.enter) {\n            widget.onConfirm(context);\n            if (widget.closeOnAction) {\n              Navigator.of(context).pop();\n            }\n          }\n        }\n      },\n      child: Container(\n        decoration: BoxDecoration(\n          borderRadius: BorderRadius.circular(theme.borderRadius.xl),\n          color: AppFlowyTheme.of(context).surfaceColorScheme.primary,\n        ),\n        padding: EdgeInsets.symmetric(\n          horizontal: theme.spacing.xxl,\n          vertical: theme.spacing.xxl,\n        ),\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            _buildTitle(),\n            if (widget.description.isNotEmpty) ...[\n              VSpace(theme.spacing.l),\n              _buildDescription(),\n            ],\n            if (widget.child != null) ...[\n              const VSpace(12),\n              widget.child!,\n            ],\n            VSpace(theme.spacing.xxl),\n            _buildStyledButton(context),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildTitle() {\n    final theme = AppFlowyTheme.of(context);\n    return Row(\n      children: [\n        Expanded(\n          child: Text(\n            widget.title,\n            style: widget.titleStyle ??\n                theme.textStyle.heading4.prominent(\n                  color: ConfirmPopupColor.titleColor(context),\n                ),\n            overflow: TextOverflow.ellipsis,\n          ),\n        ),\n        const HSpace(6.0),\n        if (widget.showCloseButton) ...[\n          AFGhostButton.normal(\n            size: AFButtonSize.s,\n            padding: EdgeInsets.all(theme.spacing.xs),\n            onTap: () => Navigator.of(context).pop(),\n            builder: (context, isHovering, disabled) => FlowySvg(\n              FlowySvgs.password_close_m,\n              size: const Size.square(20),\n            ),\n          ),\n        ],\n      ],\n    );\n  }\n\n  Widget _buildDescription() {\n    if (widget.description.isEmpty) {\n      return const SizedBox.shrink();\n    }\n\n    final theme = AppFlowyTheme.of(context);\n\n    return Text(\n      widget.description,\n      style: widget.descriptionStyle ??\n          theme.textStyle.body.standard(\n            color: ConfirmPopupColor.descriptionColor(context),\n          ),\n      maxLines: 5,\n    );\n  }\n\n  Widget _buildStyledButton(BuildContext context) {\n    switch (widget.style) {\n      case ConfirmPopupStyle.onlyOk:\n        if (widget.confirmButtonBuilder != null) {\n          return widget.confirmButtonBuilder!(context);\n        }\n\n        return SpaceOkButton(\n          onConfirm: () {\n            widget.onConfirm(context);\n            if (widget.closeOnAction) {\n              Navigator.of(context).pop();\n            }\n          },\n          confirmButtonName: widget.confirmLabel ?? LocaleKeys.button_ok.tr(),\n          confirmButtonColor: widget.confirmButtonColor ??\n              Theme.of(context).colorScheme.primary,\n        );\n      case ConfirmPopupStyle.cancelAndOk:\n        return SpaceCancelOrConfirmButton(\n          onCancel: () {\n            widget.onCancel?.call();\n            Navigator.of(context).pop();\n          },\n          onConfirm: () {\n            widget.onConfirm(context);\n            if (widget.closeOnAction) {\n              Navigator.of(context).pop();\n            }\n          },\n          confirmButtonName:\n              widget.confirmLabel ?? LocaleKeys.space_delete.tr(),\n          confirmButtonColor:\n              widget.confirmButtonColor ?? Theme.of(context).colorScheme.error,\n          confirmButtonBuilder: widget.confirmButtonBuilder,\n        );\n    }\n  }\n}\n\nclass SpacePopup extends StatelessWidget {\n  const SpacePopup({\n    super.key,\n    this.height,\n    this.useIntrinsicWidth = true,\n    this.expand = false,\n    required this.showCreateButton,\n    required this.child,\n  });\n\n  final bool showCreateButton;\n  final bool useIntrinsicWidth;\n  final bool expand;\n  final double? height;\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: height ?? HomeSizes.workspaceSectionHeight,\n      child: AppFlowyPopover(\n        constraints: const BoxConstraints(maxWidth: 260),\n        direction: PopoverDirection.bottomWithLeftAligned,\n        clickHandler: PopoverClickHandler.gestureDetector,\n        offset: const Offset(0, 4),\n        popupBuilder: (_) => BlocProvider.value(\n          value: context.read<SpaceBloc>(),\n          child: SidebarSpaceMenu(\n            showCreateButton: showCreateButton,\n          ),\n        ),\n        child: FlowyButton(\n          useIntrinsicWidth: useIntrinsicWidth,\n          expand: expand,\n          margin: const EdgeInsets.only(left: 3.0, right: 4.0),\n          iconPadding: 10.0,\n          text: child,\n        ),\n      ),\n    );\n  }\n}\n\nclass CurrentSpace extends StatelessWidget {\n  const CurrentSpace({\n    super.key,\n    this.onTapBlankArea,\n    required this.space,\n    this.isHovered = false,\n  });\n\n  final ViewPB space;\n  final VoidCallback? onTapBlankArea;\n  final bool isHovered;\n\n  @override\n  Widget build(BuildContext context) {\n    final child = Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        SpaceIcon(\n          dimension: 22,\n          space: space,\n          svgSize: 12,\n          cornerRadius: 8.0,\n        ),\n        const HSpace(10),\n        Flexible(\n          child: FlowyText.medium(\n            space.name,\n            fontSize: 14.0,\n            figmaLineHeight: 18.0,\n            overflow: TextOverflow.ellipsis,\n            color: isHovered ? Theme.of(context).colorScheme.onSurface : null,\n          ),\n        ),\n        const HSpace(4.0),\n        FlowySvg(\n          context.read<SpaceBloc>().state.isExpanded\n              ? FlowySvgs.workspace_drop_down_menu_show_s\n              : FlowySvgs.workspace_drop_down_menu_hide_s,\n          color: isHovered ? Theme.of(context).colorScheme.onSurface : null,\n        ),\n      ],\n    );\n\n    if (onTapBlankArea != null) {\n      return Row(\n        children: [\n          Expanded(\n            flex: 2,\n            child: FlowyHover(\n              child: Padding(\n                padding: const EdgeInsets.all(2.0),\n                child: child,\n              ),\n            ),\n          ),\n          Expanded(\n            child: FlowyTooltip(\n              message: LocaleKeys.space_movePageToSpace.tr(),\n              child: GestureDetector(\n                onTap: onTapBlankArea,\n              ),\n            ),\n          ),\n        ],\n      );\n    }\n\n    return child;\n  }\n}\n\nclass SpacePages extends StatelessWidget {\n  const SpacePages({\n    super.key,\n    required this.space,\n    required this.isHovered,\n    required this.isExpandedNotifier,\n    required this.onSelected,\n    this.rightIconsBuilder,\n    this.disableSelectedStatus = false,\n    this.onTertiarySelected,\n    this.shouldIgnoreView,\n  });\n\n  final ViewPB space;\n  final ValueNotifier<bool> isHovered;\n  final PropertyValueNotifier<bool> isExpandedNotifier;\n  final bool disableSelectedStatus;\n  final ViewItemRightIconsBuilder? rightIconsBuilder;\n  final ViewItemOnSelected onSelected;\n  final ViewItemOnSelected? onTertiarySelected;\n  final IgnoreViewType Function(ViewPB view)? shouldIgnoreView;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) => ViewBloc(view: space)..add(const ViewEvent.initial()),\n      child: BlocBuilder<ViewBloc, ViewState>(\n        builder: (context, state) {\n          // filter the child views that should be ignored\n          List<ViewPB> childViews = state.view.childViews;\n          if (shouldIgnoreView != null) {\n            childViews = childViews\n                .where((v) => shouldIgnoreView!(v) != IgnoreViewType.hide)\n                .toList();\n          }\n          return Column(\n            mainAxisSize: MainAxisSize.min,\n            children: childViews\n                .map(\n                  (view) => ViewItem(\n                    key: ValueKey('${space.id} ${view.id}'),\n                    spaceType:\n                        space.spacePermission == SpacePermission.publicToAll\n                            ? FolderSpaceType.public\n                            : FolderSpaceType.private,\n                    isFirstChild: view.id == childViews.first.id,\n                    view: view,\n                    level: 0,\n                    leftPadding: HomeSpaceViewSizes.leftPadding,\n                    isFeedback: false,\n                    isHovered: isHovered,\n                    enableRightClickContext: !disableSelectedStatus,\n                    disableSelectedStatus: disableSelectedStatus,\n                    isExpandedNotifier: isExpandedNotifier,\n                    rightIconsBuilder: rightIconsBuilder,\n                    onSelected: onSelected,\n                    onTertiarySelected: onTertiarySelected,\n                    shouldIgnoreView: shouldIgnoreView,\n                  ),\n                )\n                .toList(),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass SpaceSearchField extends StatefulWidget {\n  const SpaceSearchField({\n    super.key,\n    required this.width,\n    required this.onSearch,\n  });\n\n  final double width;\n  final void Function(BuildContext context, String text) onSearch;\n\n  @override\n  State<SpaceSearchField> createState() => _SpaceSearchFieldState();\n}\n\nclass _SpaceSearchFieldState extends State<SpaceSearchField> {\n  final focusNode = FocusNode();\n\n  @override\n  void initState() {\n    super.initState();\n    focusNode.requestFocus();\n  }\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: 30,\n      width: widget.width,\n      clipBehavior: Clip.antiAlias,\n      decoration: ShapeDecoration(\n        shape: RoundedRectangleBorder(\n          side: const BorderSide(\n            width: 1.20,\n            strokeAlign: BorderSide.strokeAlignOutside,\n            color: Color(0xFF00BCF0),\n          ),\n          borderRadius: BorderRadius.circular(8),\n        ),\n      ),\n      child: CupertinoSearchTextField(\n        onChanged: (text) => widget.onSearch(context, text),\n        padding: EdgeInsets.zero,\n        focusNode: focusNode,\n        placeholder: LocaleKeys.search_label.tr(),\n        prefixIcon: const FlowySvg(FlowySvgs.magnifier_s),\n        prefixInsets: const EdgeInsets.only(left: 12.0, right: 8.0),\n        suffixIcon: const Icon(Icons.close),\n        suffixInsets: const EdgeInsets.only(right: 8.0),\n        itemSize: 16.0,\n        decoration: const BoxDecoration(\n          color: Colors.transparent,\n        ),\n        placeholderStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(\n              color: Theme.of(context).hintColor,\n              fontWeight: FontWeight.w400,\n            ),\n        style: Theme.of(context).textTheme.bodyMedium?.copyWith(\n              fontWeight: FontWeight.w400,\n            ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space.dart",
    "content": "import 'package:appflowy/features/shared_section/presentation/shared_section.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/hotkeys.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/create_space_popup.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'\n    hide AFRolePB;\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\n\nclass SidebarSpace extends StatelessWidget {\n  const SidebarSpace({\n    super.key,\n    this.isHoverEnabled = true,\n    required this.userProfile,\n  });\n\n  final bool isHoverEnabled;\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    final currentWorkspace =\n        context.watch<UserWorkspaceBloc>().state.currentWorkspace;\n    final currentWorkspaceId = currentWorkspace?.workspaceId ?? '';\n\n    // only show spaces if the user role is member or owner\n    final currentUserRole = currentWorkspace?.role;\n    final shouldShowSpaces = [\n      AFRolePB.Member,\n      AFRolePB.Owner,\n    ].contains(currentUserRole);\n\n    return ValueListenableBuilder(\n      valueListenable: getIt<MenuSharedState>().notifier,\n      builder: (_, __, ___) => Provider.value(\n        value: userProfile,\n        child: Column(\n          children: [\n            const VSpace(4.0),\n            // favorite\n            BlocBuilder<FavoriteBloc, FavoriteState>(\n              builder: (context, state) {\n                if (state.views.isEmpty) {\n                  return const SizedBox.shrink();\n                }\n\n                return Padding(\n                  padding: const EdgeInsets.only(bottom: 16.0),\n                  child: FavoriteFolder(\n                    views: state.views.map((e) => e.item).toList(),\n                  ),\n                );\n              },\n            ),\n\n            // shared\n            if (FeatureFlag.sharedSection.isOn) ...[\n              SharedSection(\n                key: ValueKey(currentWorkspaceId),\n                workspaceId: currentWorkspaceId,\n              ),\n            ],\n\n            // spaces\n            if (shouldShowSpaces) ...[\n              // spaces\n              const _Space(),\n            ],\n\n            const VSpace(200),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _Space extends StatefulWidget {\n  const _Space();\n\n  @override\n  State<_Space> createState() => _SpaceState();\n}\n\nclass _SpaceState extends State<_Space> {\n  final isHovered = ValueNotifier(false);\n  final isExpandedNotifier = PropertyValueNotifier(false);\n\n  @override\n  void initState() {\n    super.initState();\n\n    switchToTheNextSpace.addListener(_switchToNextSpace);\n    switchToSpaceNotifier.addListener(_switchToSpace);\n  }\n\n  @override\n  void dispose() {\n    switchToTheNextSpace.removeListener(_switchToNextSpace);\n    isHovered.dispose();\n    isExpandedNotifier.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final currentWorkspace =\n        context.watch<UserWorkspaceBloc>().state.currentWorkspace;\n    return BlocBuilder<SpaceBloc, SpaceState>(\n      builder: (context, state) {\n        if (state.spaces.isEmpty) {\n          return const SizedBox.shrink();\n        }\n\n        final currentSpace = state.currentSpace ?? state.spaces.first;\n\n        return Column(\n          children: [\n            SidebarSpaceHeader(\n              isExpanded: state.isExpanded,\n              space: currentSpace,\n              onAdded: (layout) => _showCreatePagePopup(\n                context,\n                currentSpace,\n                layout,\n              ),\n              onCreateNewSpace: () => _showCreateSpaceDialog(context),\n              onCollapseAllPages: () => isExpandedNotifier.value = true,\n            ),\n            if (state.isExpanded)\n              MouseRegion(\n                onEnter: (_) => isHovered.value = true,\n                onExit: (_) => isHovered.value = false,\n                child: SpacePages(\n                  key: ValueKey(\n                    Object.hashAll([\n                      currentWorkspace?.workspaceId ?? '',\n                      currentSpace.id,\n                    ]),\n                  ),\n                  isExpandedNotifier: isExpandedNotifier,\n                  space: currentSpace,\n                  isHovered: isHovered,\n                  onSelected: (context, view) {\n                    if (HardwareKeyboard.instance.isControlPressed) {\n                      context.read<TabsBloc>().openTab(view);\n                    }\n                    context.read<TabsBloc>().openPlugin(view);\n                  },\n                  onTertiarySelected: (context, view) =>\n                      context.read<TabsBloc>().openTab(view),\n                ),\n              ),\n          ],\n        );\n      },\n    );\n  }\n\n  void _showCreateSpaceDialog(BuildContext context) {\n    final spaceBloc = context.read<SpaceBloc>();\n    showDialog(\n      context: context,\n      builder: (_) => Dialog(\n        shape: RoundedRectangleBorder(\n          borderRadius: BorderRadius.circular(12.0),\n        ),\n        child: BlocProvider.value(\n          value: spaceBloc,\n          child: const CreateSpacePopup(),\n        ),\n      ),\n    );\n  }\n\n  void _showCreatePagePopup(\n    BuildContext context,\n    ViewPB space,\n    ViewLayoutPB layout,\n  ) {\n    context.read<SpaceBloc>().add(\n          SpaceEvent.createPage(\n            name: '',\n            layout: layout,\n            index: 0,\n            openAfterCreate: true,\n          ),\n        );\n\n    context.read<SpaceBloc>().add(SpaceEvent.expand(space, true));\n  }\n\n  void _switchToNextSpace() {\n    context.read<SpaceBloc>().add(const SpaceEvent.switchToNextSpace());\n  }\n\n  void _switchToSpace() {\n    if (!mounted || !context.mounted) {\n      return;\n    }\n\n    final space = switchToSpaceNotifier.value;\n    if (space == null) {\n      return;\n    }\n\n    context.read<SpaceBloc>().add(SpaceEvent.open(space: space));\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/manage_space_popup.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart' hide Icon;\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SidebarSpaceHeader extends StatefulWidget {\n  const SidebarSpaceHeader({\n    super.key,\n    required this.space,\n    required this.onAdded,\n    required this.onCreateNewSpace,\n    required this.onCollapseAllPages,\n    required this.isExpanded,\n  });\n\n  final ViewPB space;\n  final void Function(ViewLayoutPB layout) onAdded;\n  final VoidCallback onCreateNewSpace;\n  final VoidCallback onCollapseAllPages;\n  final bool isExpanded;\n\n  @override\n  State<SidebarSpaceHeader> createState() => _SidebarSpaceHeaderState();\n}\n\nclass _SidebarSpaceHeaderState extends State<SidebarSpaceHeader> {\n  final isHovered = ValueNotifier(false);\n  final onEditing = ValueNotifier(false);\n\n  @override\n  void dispose() {\n    isHovered.dispose();\n    onEditing.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ValueListenableBuilder(\n      valueListenable: isHovered,\n      builder: (context, onHover, child) {\n        return MouseRegion(\n          onEnter: (_) => isHovered.value = true,\n          onExit: (_) => isHovered.value = false,\n          child: GestureDetector(\n            onTap: () => context\n                .read<SpaceBloc>()\n                .add(SpaceEvent.expand(widget.space, !widget.isExpanded)),\n            child: _buildSpaceName(onHover),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildSpaceName(bool isHovered) {\n    return Container(\n      height: HomeSizes.workspaceSectionHeight,\n      decoration: BoxDecoration(\n        borderRadius: const BorderRadius.all(Radius.circular(6)),\n        color: isHovered ? Theme.of(context).colorScheme.secondary : null,\n      ),\n      child: Stack(\n        alignment: Alignment.center,\n        children: [\n          ValueListenableBuilder(\n            valueListenable: onEditing,\n            builder: (context, onEditing, child) => Positioned(\n              left: 3,\n              top: 3,\n              bottom: 3,\n              right: isHovered || onEditing ? 88 : 0,\n              child: SpacePopup(\n                showCreateButton: true,\n                child: _buildChild(isHovered),\n              ),\n            ),\n          ),\n          Positioned(\n            right: 4,\n            child: _buildRightIcon(isHovered),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildChild(bool isHovered) {\n    final textSpan = TextSpan(\n      children: [\n        TextSpan(\n          text: '${LocaleKeys.space_quicklySwitch.tr()}\\n',\n          style: context.tooltipTextStyle(),\n        ),\n        TextSpan(\n          text: Platform.isMacOS ? '⌘+O' : 'Ctrl+O',\n          style: context\n              .tooltipTextStyle()\n              ?.copyWith(color: Theme.of(context).hintColor),\n        ),\n      ],\n    );\n    return FlowyTooltip(\n      richMessage: textSpan,\n      child: CurrentSpace(\n        space: widget.space,\n        isHovered: isHovered,\n      ),\n    );\n  }\n\n  Widget _buildRightIcon(bool isHovered) {\n    return ValueListenableBuilder(\n      valueListenable: onEditing,\n      builder: (context, onEditing, child) => Opacity(\n        opacity: isHovered || onEditing ? 1 : 0,\n        child: Row(\n          children: [\n            SpaceMorePopup(\n              space: widget.space,\n              onEditing: (value) => this.onEditing.value = value,\n              onAction: _onAction,\n              isHovered: isHovered,\n            ),\n            const HSpace(8.0),\n            FlowyTooltip(\n              message: LocaleKeys.sideBar_addAPage.tr(),\n              child: ViewAddButton(\n                parentViewId: widget.space.id,\n                onEditing: (_) {},\n                onSelected: (\n                  pluginBuilder,\n                  name,\n                  initialDataBytes,\n                  openAfterCreated,\n                  createNewView,\n                ) {\n                  if (pluginBuilder.layoutType == ViewLayoutPB.Document) {\n                    name = '';\n                  }\n                  if (createNewView) {\n                    widget.onAdded(pluginBuilder.layoutType!);\n                  }\n                },\n                isHovered: isHovered,\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Future<void> _onAction(SpaceMoreActionType type, dynamic data) async {\n    switch (type) {\n      case SpaceMoreActionType.rename:\n        await _showRenameDialog();\n        break;\n      case SpaceMoreActionType.changeIcon:\n        if (data is SelectedEmojiIconResult) {\n          if (data.type == FlowyIconType.icon) {\n            try {\n              final iconsData = IconsData.fromJson(jsonDecode(data.emoji));\n              context.read<SpaceBloc>().add(\n                    SpaceEvent.changeIcon(\n                      icon: '${iconsData.groupName}/${iconsData.iconName}',\n                      iconColor: iconsData.color,\n                    ),\n                  );\n            } on FormatException catch (e) {\n              context\n                  .read<SpaceBloc>()\n                  .add(const SpaceEvent.changeIcon(icon: ''));\n              Log.warn('SidebarSpaceHeader changeIcon error:$e');\n            }\n          }\n        }\n        break;\n      case SpaceMoreActionType.manage:\n        _showManageSpaceDialog(context);\n        break;\n      case SpaceMoreActionType.addNewSpace:\n        widget.onCreateNewSpace();\n        break;\n      case SpaceMoreActionType.collapseAllPages:\n        widget.onCollapseAllPages();\n        break;\n      case SpaceMoreActionType.delete:\n        _showDeleteSpaceDialog(context);\n        break;\n      case SpaceMoreActionType.duplicate:\n        context.read<SpaceBloc>().add(const SpaceEvent.duplicate());\n        break;\n      case SpaceMoreActionType.divider:\n        break;\n    }\n  }\n\n  Future<void> _showRenameDialog() async {\n    await showAFTextFieldDialog(\n      context: context,\n      title: LocaleKeys.space_rename.tr(),\n      initialValue: widget.space.name,\n      hintText: LocaleKeys.space_spaceName.tr(),\n      onConfirm: (name) {\n        context.read<SpaceBloc>().add(\n              SpaceEvent.rename(\n                space: widget.space,\n                name: name,\n              ),\n            );\n      },\n    );\n  }\n\n  void _showManageSpaceDialog(BuildContext context) {\n    final spaceBloc = context.read<SpaceBloc>();\n    showDialog(\n      context: context,\n      builder: (_) {\n        return Dialog(\n          shape: RoundedRectangleBorder(\n            borderRadius: BorderRadius.circular(12.0),\n          ),\n          child: BlocProvider.value(\n            value: spaceBloc,\n            child: const ManageSpacePopup(),\n          ),\n        );\n      },\n    );\n  }\n\n  void _showDeleteSpaceDialog(BuildContext context) {\n    final spaceBloc = context.read<SpaceBloc>();\n    final space = spaceBloc.state.currentSpace;\n    final name = space != null ? space.name : '';\n    showConfirmDeletionDialog(\n      context: context,\n      name: name,\n      description: LocaleKeys.space_deleteConfirmationDescription.tr(),\n      onConfirm: () {\n        context.read<SpaceBloc>().add(const SpaceEvent.delete(null));\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/create_space_popup.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SidebarSpaceMenu extends StatelessWidget {\n  const SidebarSpaceMenu({\n    super.key,\n    required this.showCreateButton,\n  });\n\n  final bool showCreateButton;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SpaceBloc, SpaceState>(\n      builder: (context, state) {\n        return Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            const VSpace(4.0),\n            for (final space in state.spaces)\n              SizedBox(\n                height: HomeSpaceViewSizes.viewHeight,\n                child: SidebarSpaceMenuItem(\n                  space: space,\n                  isSelected: state.currentSpace?.id == space.id,\n                ),\n              ),\n            if (showCreateButton) ...[\n              const Padding(\n                padding: EdgeInsets.symmetric(vertical: 8.0),\n                child: FlowyDivider(),\n              ),\n              const SizedBox(\n                height: HomeSpaceViewSizes.viewHeight,\n                child: _CreateSpaceButton(),\n              ),\n            ],\n          ],\n        );\n      },\n    );\n  }\n}\n\nclass SidebarSpaceMenuItem extends StatelessWidget {\n  const SidebarSpaceMenuItem({\n    super.key,\n    required this.space,\n    required this.isSelected,\n  });\n\n  final ViewPB space;\n  final bool isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      text: Row(\n        children: [\n          Flexible(\n            child: FlowyText.regular(\n              space.name,\n              overflow: TextOverflow.ellipsis,\n            ),\n          ),\n          const HSpace(6.0),\n          if (space.spacePermission == SpacePermission.private)\n            FlowyTooltip(\n              message: LocaleKeys.space_privatePermissionDescription.tr(),\n              child: const FlowySvg(\n                FlowySvgs.space_lock_s,\n              ),\n            ),\n        ],\n      ),\n      iconPadding: 10,\n      leftIcon: SpaceIcon(\n        dimension: 20,\n        space: space,\n        svgSize: 12.0,\n        cornerRadius: 6.0,\n      ),\n      leftIconSize: const Size.square(20),\n      rightIcon: isSelected\n          ? const FlowySvg(\n              FlowySvgs.workspace_selected_s,\n              blendMode: null,\n            )\n          : null,\n      onTap: () {\n        context.read<SpaceBloc>().add(SpaceEvent.open(space: space));\n        PopoverContainer.of(context).close();\n      },\n    );\n  }\n}\n\nclass _CreateSpaceButton extends StatelessWidget {\n  const _CreateSpaceButton();\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      text: FlowyText.regular(LocaleKeys.space_createNewSpace.tr()),\n      iconPadding: 10,\n      leftIcon: const FlowySvg(\n        FlowySvgs.space_add_s,\n      ),\n      onTap: () {\n        PopoverContainer.of(context).close();\n        _showCreateSpaceDialog(context);\n      },\n    );\n  }\n\n  void _showCreateSpaceDialog(BuildContext context) {\n    final spaceBloc = context.read<SpaceBloc>();\n    showDialog(\n      context: context,\n      builder: (_) {\n        return Dialog(\n          shape: RoundedRectangleBorder(\n            borderRadius: BorderRadius.circular(12.0),\n          ),\n          child: BlocProvider.value(\n            value: spaceBloc,\n            child: const CreateSpacePopup(),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_action_type.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nenum SpaceMoreActionType {\n  delete,\n  rename,\n  changeIcon,\n  collapseAllPages,\n  divider,\n  addNewSpace,\n  manage,\n  duplicate,\n}\n\nextension ViewMoreActionTypeExtension on SpaceMoreActionType {\n  String get name {\n    switch (this) {\n      case SpaceMoreActionType.delete:\n        return LocaleKeys.space_delete.tr();\n      case SpaceMoreActionType.rename:\n        return LocaleKeys.space_rename.tr();\n      case SpaceMoreActionType.changeIcon:\n        return LocaleKeys.space_changeIcon.tr();\n      case SpaceMoreActionType.collapseAllPages:\n        return LocaleKeys.space_collapseAllSubPages.tr();\n      case SpaceMoreActionType.addNewSpace:\n        return LocaleKeys.space_addNewSpace.tr();\n      case SpaceMoreActionType.manage:\n        return LocaleKeys.space_manage.tr();\n      case SpaceMoreActionType.duplicate:\n        return LocaleKeys.space_duplicate.tr();\n      case SpaceMoreActionType.divider:\n        return '';\n    }\n  }\n\n  FlowySvgData get leftIconSvg {\n    switch (this) {\n      case SpaceMoreActionType.delete:\n        return FlowySvgs.trash_s;\n      case SpaceMoreActionType.rename:\n        return FlowySvgs.view_item_rename_s;\n      case SpaceMoreActionType.changeIcon:\n        return FlowySvgs.change_icon_s;\n      case SpaceMoreActionType.collapseAllPages:\n        return FlowySvgs.collapse_all_page_s;\n      case SpaceMoreActionType.addNewSpace:\n        return FlowySvgs.space_add_s;\n      case SpaceMoreActionType.manage:\n        return FlowySvgs.space_manage_s;\n      case SpaceMoreActionType.duplicate:\n        return FlowySvgs.duplicate_s;\n      case SpaceMoreActionType.divider:\n        throw UnsupportedError('Divider does not have an icon');\n    }\n  }\n\n  Widget get rightIcon {\n    switch (this) {\n      case SpaceMoreActionType.changeIcon:\n      case SpaceMoreActionType.rename:\n      case SpaceMoreActionType.collapseAllPages:\n      case SpaceMoreActionType.divider:\n      case SpaceMoreActionType.delete:\n      case SpaceMoreActionType.addNewSpace:\n      case SpaceMoreActionType.manage:\n      case SpaceMoreActionType.duplicate:\n        return const SizedBox.shrink();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_icon.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass SpaceIcon extends StatelessWidget {\n  const SpaceIcon({\n    super.key,\n    required this.dimension,\n    this.textDimension,\n    this.cornerRadius = 0,\n    required this.space,\n    this.svgSize,\n  });\n\n  final double dimension;\n  final double? textDimension;\n  final double cornerRadius;\n  final ViewPB space;\n  final double? svgSize;\n\n  @override\n  Widget build(BuildContext context) {\n    final (icon, color) = _buildSpaceIcon(context);\n\n    return ClipRRect(\n      borderRadius: BorderRadius.circular(cornerRadius),\n      child: Container(\n        width: dimension,\n        height: dimension,\n        color: color,\n        child: Center(\n          child: icon,\n        ),\n      ),\n    );\n  }\n\n  (Widget, Color?) _buildSpaceIcon(BuildContext context) {\n    final spaceIcon = space.spaceIcon;\n    if (spaceIcon == null || spaceIcon.isEmpty == true) {\n      // if space icon is null, use the first character of space name as icon\n      return _buildEmptySpaceIcon(context);\n    } else {\n      return _buildCustomSpaceIcon(context);\n    }\n  }\n\n  (Widget, Color?) _buildEmptySpaceIcon(BuildContext context) {\n    final name = space.name.isNotEmpty ? space.name.capitalize()[0] : '';\n    final icon = FlowyText.medium(\n      name,\n      color: Theme.of(context).colorScheme.surface,\n      fontSize: svgSize,\n      figmaLineHeight: textDimension ?? dimension,\n    );\n    Color? color;\n    try {\n      final defaultColor = builtInSpaceColors.firstOrNull;\n      if (defaultColor != null) {\n        color = Color(int.parse(defaultColor));\n      }\n    } catch (e) {\n      Log.error('Failed to parse default space icon color: $e');\n    }\n    return (icon, color);\n  }\n\n  (Widget, Color?) _buildCustomSpaceIcon(BuildContext context) {\n    final spaceIconColor = space.spaceIconColor;\n\n    final svg = space.buildSpaceIconSvg(\n      context,\n      size: svgSize != null ? Size.square(svgSize!) : null,\n    );\n    Widget icon;\n    if (svg == null) {\n      icon = const SizedBox.shrink();\n    } else {\n      icon = svgSize == null ||\n              space.spaceIcon?.contains(ViewExtKeys.spaceIconKey) == true\n          ? svg\n          : SizedBox.square(dimension: svgSize!, child: svg);\n    }\n\n    Color color = Colors.transparent;\n    if (spaceIconColor != null && spaceIconColor.isNotEmpty) {\n      try {\n        color = Color(int.parse(spaceIconColor));\n      } catch (e) {\n        Log.error(\n          'Failed to parse space icon color: $e, value: $spaceIconColor',\n        );\n      }\n    }\n\n    return (icon, color);\n  }\n}\n\nconst kDefaultSpaceIconId = 'interface_essential/home-3';\n\nclass DefaultSpaceIcon extends StatelessWidget {\n  const DefaultSpaceIcon({\n    super.key,\n    required this.dimension,\n    required this.iconDimension,\n    this.cornerRadius = 0,\n  });\n\n  final double dimension;\n  final double cornerRadius;\n  final double iconDimension;\n\n  @override\n  Widget build(BuildContext context) {\n    final svgContent = kIconGroups?.findSvgContent(\n      kDefaultSpaceIconId,\n    );\n\n    final Widget svg;\n    if (svgContent != null) {\n      svg = FlowySvg.string(\n        svgContent,\n        size: Size.square(iconDimension),\n        color: Theme.of(context).colorScheme.surface,\n      );\n    } else {\n      svg = FlowySvg(\n        FlowySvgData('assets/flowy_icons/16x/${builtInSpaceIcons.first}.svg'),\n        color: Theme.of(context).colorScheme.surface,\n        size: Size.square(iconDimension),\n      );\n    }\n\n    final color = Color(int.parse(builtInSpaceColors.first));\n    return ClipRRect(\n      borderRadius: BorderRadius.circular(cornerRadius),\n      child: Container(\n        width: dimension,\n        height: dimension,\n        color: color,\n        child: Center(\n          child: svg,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart",
    "content": "import 'dart:convert';\nimport 'dart:math';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart' hide Icon;\n\nfinal builtInSpaceColors = [\n  '0xFFA34AFD',\n  '0xFFFB006D',\n  '0xFF00C8FF',\n  '0xFFFFBA00',\n  '0xFFF254BC',\n  '0xFF2AC985',\n  '0xFFAAD93D',\n  '0xFF535CE4',\n  '0xFF808080',\n  '0xFFD2515F',\n  '0xFF409BF8',\n  '0xFFFF8933',\n];\n\nString generateRandomSpaceColor() {\n  final random = Random();\n  return builtInSpaceColors[random.nextInt(builtInSpaceColors.length)];\n}\n\nfinal builtInSpaceIcons =\n    List.generate(15, (index) => 'space_icon_${index + 1}');\n\nclass SpaceIconPopup extends StatefulWidget {\n  const SpaceIconPopup({\n    super.key,\n    this.icon,\n    this.iconColor,\n    this.cornerRadius = 16,\n    this.space,\n    required this.onIconChanged,\n  });\n\n  final String? icon;\n  final String? iconColor;\n  final ViewPB? space;\n  final void Function(String? icon, String? color) onIconChanged;\n  final double cornerRadius;\n\n  @override\n  State<SpaceIconPopup> createState() => _SpaceIconPopupState();\n}\n\nclass _SpaceIconPopupState extends State<SpaceIconPopup> {\n  late ValueNotifier<String?> selectedIcon = ValueNotifier<String?>(\n    widget.icon,\n  );\n  late ValueNotifier<String> selectedColor = ValueNotifier<String>(\n    widget.iconColor ?? builtInSpaceColors.first,\n  );\n\n  @override\n  void dispose() {\n    selectedColor.dispose();\n    selectedIcon.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      offset: const Offset(0, 4),\n      constraints: BoxConstraints.loose(const Size(360, 432)),\n      margin: const EdgeInsets.all(0),\n      direction: PopoverDirection.bottomWithCenterAligned,\n      child: _buildPreview(),\n      popupBuilder: (context) {\n        return FlowyIconEmojiPicker(\n          tabs: const [PickerTabType.icon],\n          onSelectedEmoji: (r) {\n            if (r.type == FlowyIconType.icon) {\n              try {\n                final iconsData = IconsData.fromJson(jsonDecode(r.emoji));\n                final color = iconsData.color;\n                selectedIcon.value =\n                    '${iconsData.groupName}/${iconsData.iconName}';\n                if (color != null) {\n                  selectedColor.value = color;\n                }\n                widget.onIconChanged(selectedIcon.value, selectedColor.value);\n              } on FormatException catch (e) {\n                selectedIcon.value = '';\n                widget.onIconChanged(selectedIcon.value, selectedColor.value);\n                Log.warn('SpaceIconPopup onSelectedEmoji error:$e');\n              }\n            }\n            PopoverContainer.of(context).close();\n          },\n        );\n      },\n    );\n  }\n\n  Widget _buildPreview() {\n    bool onHover = false;\n    return StatefulBuilder(\n      builder: (context, setState) {\n        return MouseRegion(\n          cursor: SystemMouseCursors.click,\n          onEnter: (event) => setState(() => onHover = true),\n          onExit: (event) => setState(() => onHover = false),\n          child: ValueListenableBuilder(\n            valueListenable: selectedColor,\n            builder: (_, color, __) {\n              return ValueListenableBuilder(\n                valueListenable: selectedIcon,\n                builder: (_, value, __) {\n                  Widget child;\n                  if (value == null) {\n                    if (widget.space == null) {\n                      child = DefaultSpaceIcon(\n                        cornerRadius: widget.cornerRadius,\n                        dimension: 32,\n                        iconDimension: 32,\n                      );\n                    } else {\n                      child = SpaceIcon(\n                        dimension: 32,\n                        space: widget.space!,\n                        svgSize: 24,\n                        cornerRadius: widget.cornerRadius,\n                      );\n                    }\n                  } else if (value.contains('space_icon')) {\n                    child = ClipRRect(\n                      borderRadius: BorderRadius.circular(widget.cornerRadius),\n                      child: Container(\n                        color: Color(int.parse(color)),\n                        child: Align(\n                          child: FlowySvg(\n                            FlowySvgData('assets/flowy_icons/16x/$value.svg'),\n                            size: const Size.square(42),\n                            color: Theme.of(context).colorScheme.surface,\n                          ),\n                        ),\n                      ),\n                    );\n                  } else {\n                    final content = kIconGroups?.findSvgContent(value);\n                    if (content == null) {\n                      child = const SizedBox.shrink();\n                    } else {\n                      child = ClipRRect(\n                        borderRadius:\n                            BorderRadius.circular(widget.cornerRadius),\n                        child: Container(\n                          color: Color(int.parse(color)),\n                          child: Align(\n                            child: FlowySvg.string(\n                              content,\n                              size: const Size.square(24),\n                              color: Theme.of(context).colorScheme.surface,\n                            ),\n                          ),\n                        ),\n                      );\n                    }\n                  }\n\n                  if (onHover) {\n                    return Stack(\n                      children: [\n                        Positioned.fill(\n                          child: Opacity(opacity: 0.2, child: child),\n                        ),\n                        const Center(\n                          child: FlowySvg(\n                            FlowySvgs.view_item_rename_s,\n                            size: Size.square(20),\n                          ),\n                        ),\n                      ],\n                    );\n                  }\n                  return child;\n                },\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass SpaceIconPicker extends StatefulWidget {\n  const SpaceIconPicker({\n    super.key,\n    required this.onIconChanged,\n    this.skipFirstNotification = false,\n    this.icon,\n    this.iconColor,\n  });\n\n  final bool skipFirstNotification;\n  final void Function(String icon, String color) onIconChanged;\n  final String? icon;\n  final String? iconColor;\n\n  @override\n  State<SpaceIconPicker> createState() => _SpaceIconPickerState();\n}\n\nclass _SpaceIconPickerState extends State<SpaceIconPicker> {\n  late ValueNotifier<String> selectedColor =\n      ValueNotifier<String>(widget.iconColor ?? builtInSpaceColors.first);\n  late ValueNotifier<String> selectedIcon =\n      ValueNotifier<String>(widget.icon ?? builtInSpaceIcons.first);\n\n  @override\n  void initState() {\n    super.initState();\n\n    if (!widget.skipFirstNotification) {\n      widget.onIconChanged(selectedIcon.value, selectedColor.value);\n    }\n\n    selectedColor.addListener(_onColorChanged);\n    selectedIcon.addListener(_onIconChanged);\n  }\n\n  void _onColorChanged() {\n    widget.onIconChanged(selectedIcon.value, selectedColor.value);\n  }\n\n  void _onIconChanged() {\n    widget.onIconChanged(selectedIcon.value, selectedColor.value);\n  }\n\n  @override\n  void dispose() {\n    selectedColor.removeListener(_onColorChanged);\n    selectedColor.dispose();\n\n    selectedIcon.removeListener(_onIconChanged);\n    selectedIcon.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        FlowyText.regular(\n          LocaleKeys.space_spaceIconBackground.tr(),\n          color: Theme.of(context).hintColor,\n        ),\n        const VSpace(10.0),\n        _Colors(\n          selectedColor: selectedColor.value,\n          onColorSelected: (color) => selectedColor.value = color,\n        ),\n        const VSpace(12.0),\n        FlowyText.regular(\n          LocaleKeys.space_spaceIcon.tr(),\n          color: Theme.of(context).hintColor,\n        ),\n        const VSpace(10.0),\n        ValueListenableBuilder(\n          valueListenable: selectedColor,\n          builder: (_, value, ___) => _Icons(\n            selectedColor: value,\n            selectedIcon: selectedIcon.value,\n            onIconSelected: (icon) => selectedIcon.value = icon,\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass _Colors extends StatefulWidget {\n  const _Colors({\n    required this.selectedColor,\n    required this.onColorSelected,\n  });\n\n  final String selectedColor;\n  final void Function(String color) onColorSelected;\n\n  @override\n  State<_Colors> createState() => _ColorsState();\n}\n\nclass _ColorsState extends State<_Colors> {\n  late String selectedColor = widget.selectedColor;\n\n  @override\n  Widget build(BuildContext context) {\n    return GridView.count(\n      shrinkWrap: true,\n      crossAxisCount: 6,\n      mainAxisSpacing: 4.0,\n      children: builtInSpaceColors.map((color) {\n        return GestureDetector(\n          onTap: () {\n            setState(() => selectedColor = color);\n\n            widget.onColorSelected(color);\n          },\n          child: Container(\n            margin: const EdgeInsets.all(2.0),\n            padding: const EdgeInsets.all(2.0),\n            decoration: selectedColor == color\n                ? ShapeDecoration(\n                    shape: RoundedRectangleBorder(\n                      side: const BorderSide(\n                        width: 1.50,\n                        strokeAlign: BorderSide.strokeAlignOutside,\n                        color: Color(0xFF00BCF0),\n                      ),\n                      borderRadius: BorderRadius.circular(20),\n                    ),\n                  )\n                : null,\n            child: DecoratedBox(\n              decoration: BoxDecoration(\n                color: Color(int.parse(color)),\n                borderRadius: BorderRadius.circular(20.0),\n              ),\n            ),\n          ),\n        );\n      }).toList(),\n    );\n  }\n}\n\nclass _Icons extends StatefulWidget {\n  const _Icons({\n    required this.selectedColor,\n    required this.selectedIcon,\n    required this.onIconSelected,\n  });\n\n  final String selectedColor;\n  final String selectedIcon;\n  final void Function(String color) onIconSelected;\n\n  @override\n  State<_Icons> createState() => _IconsState();\n}\n\nclass _IconsState extends State<_Icons> {\n  late String selectedIcon = widget.selectedIcon;\n\n  @override\n  Widget build(BuildContext context) {\n    return GridView.count(\n      shrinkWrap: true,\n      crossAxisCount: 5,\n      mainAxisSpacing: 8.0,\n      crossAxisSpacing: 12.0,\n      children: builtInSpaceIcons.map((icon) {\n        return GestureDetector(\n          onTap: () {\n            setState(() => selectedIcon = icon);\n\n            widget.onIconSelected(icon);\n          },\n          child: ClipRRect(\n            borderRadius: BorderRadius.circular(8.0),\n            child: FlowySvg(\n              FlowySvgData('assets/flowy_icons/16x/$icon.svg'),\n              color: Color(int.parse(widget.selectedColor)),\n              blendMode: BlendMode.srcOut,\n            ),\n          ),\n        );\n      }).toList(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_migration.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SpaceMigration extends StatefulWidget {\n  const SpaceMigration({super.key});\n\n  @override\n  State<SpaceMigration> createState() => _SpaceMigrationState();\n}\n\nclass _SpaceMigrationState extends State<SpaceMigration> {\n  bool _isExpanded = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.all(12),\n      clipBehavior: Clip.antiAlias,\n      decoration: ShapeDecoration(\n        color: Theme.of(context).isLightMode\n            ? const Color(0x66F5EAFF)\n            : const Color(0x1AFFFFFF),\n        shape: RoundedRectangleBorder(\n          side: const BorderSide(\n            strokeAlign: BorderSide.strokeAlignOutside,\n            color: Color(0x339327FF),\n          ),\n          borderRadius: BorderRadius.circular(10),\n        ),\n      ),\n      child: _isExpanded\n          ? _buildExpandedMigrationContent()\n          : _buildCollapsedMigrationContent(),\n    );\n  }\n\n  Widget _buildExpandedMigrationContent() {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        _MigrationTitle(\n          onClose: () => setState(() => _isExpanded = false),\n        ),\n        const VSpace(6.0),\n        Opacity(\n          opacity: 0.7,\n          child: FlowyText.regular(\n            LocaleKeys.space_upgradeSpaceDescription.tr(),\n            maxLines: null,\n            fontSize: 13.0,\n            lineHeight: 1.3,\n          ),\n        ),\n        const VSpace(12.0),\n        _ExpandedUpgradeButton(\n          onUpgrade: () =>\n              context.read<SpaceBloc>().add(const SpaceEvent.migrate()),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildCollapsedMigrationContent() {\n    const linearGradient = LinearGradient(\n      begin: Alignment.bottomLeft,\n      end: Alignment.bottomRight,\n      colors: [Color(0xFF8032FF), Color(0xFFEF35FF)],\n      stops: [0.1545, 0.8225],\n    );\n    return GestureDetector(\n      behavior: HitTestBehavior.translucent,\n      onTap: () => setState(() => _isExpanded = true),\n      child: Row(\n        children: [\n          const FlowySvg(\n            FlowySvgs.upgrade_s,\n            blendMode: null,\n          ),\n          const HSpace(8.0),\n          Expanded(\n            child: ShaderMask(\n              shaderCallback: (Rect bounds) =>\n                  linearGradient.createShader(bounds),\n              blendMode: BlendMode.srcIn,\n              child: FlowyText(\n                LocaleKeys.space_upgradeYourSpace.tr(),\n              ),\n            ),\n          ),\n          const FlowySvg(\n            FlowySvgs.space_arrow_right_s,\n            blendMode: null,\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _MigrationTitle extends StatelessWidget {\n  const _MigrationTitle({required this.onClose});\n\n  final VoidCallback? onClose;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        const FlowySvg(\n          FlowySvgs.upgrade_s,\n          blendMode: null,\n        ),\n        const HSpace(8.0),\n        Expanded(\n          child: FlowyText(\n            LocaleKeys.space_upgradeSpaceTitle.tr(),\n            maxLines: 3,\n            lineHeight: 1.2,\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass _ExpandedUpgradeButton extends StatelessWidget {\n  const _ExpandedUpgradeButton({required this.onUpgrade});\n\n  final VoidCallback? onUpgrade;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      behavior: HitTestBehavior.translucent,\n      onTap: onUpgrade,\n      child: Container(\n        padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),\n        decoration: ShapeDecoration(\n          color: const Color(0xFFA44AFD),\n          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(9)),\n        ),\n        child: FlowyText(\n          LocaleKeys.space_upgrade.tr(),\n          color: Colors.white,\n          fontSize: 12.0,\n          strutStyle: const StrutStyle(forceStrutHeight: true),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/af_role_pb_extension.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SpaceMorePopup extends StatelessWidget {\n  const SpaceMorePopup({\n    super.key,\n    required this.space,\n    required this.onAction,\n    required this.onEditing,\n    this.isHovered = false,\n  });\n\n  final ViewPB space;\n  final void Function(SpaceMoreActionType type, dynamic data) onAction;\n  final void Function(bool value) onEditing;\n  final bool isHovered;\n\n  @override\n  Widget build(BuildContext context) {\n    final wrappers = _buildActionTypeWrappers();\n    return PopoverActionList<SpaceMoreActionTypeWrapper>(\n      direction: PopoverDirection.bottomWithLeftAligned,\n      offset: const Offset(0, 8),\n      actions: wrappers,\n      constraints: const BoxConstraints(\n        minWidth: 260,\n      ),\n      buildChild: (popover) {\n        return FlowyIconButton(\n          width: 24,\n          icon: FlowySvg(\n            FlowySvgs.workspace_three_dots_s,\n            color: isHovered ? Theme.of(context).colorScheme.onSurface : null,\n          ),\n          tooltipText: LocaleKeys.space_manage.tr(),\n          onPressed: () {\n            onEditing(true);\n            popover.show();\n          },\n        );\n      },\n      onSelected: (_, __) {},\n      onClosed: () => onEditing(false),\n    );\n  }\n\n  List<SpaceMoreActionTypeWrapper> _buildActionTypeWrappers() {\n    final actionTypes = _buildActionTypes();\n    return actionTypes\n        .map(\n          (e) => SpaceMoreActionTypeWrapper(e, (controller, data) {\n            onAction(e, data);\n            controller.close();\n          }),\n        )\n        .toList();\n  }\n\n  List<SpaceMoreActionType> _buildActionTypes() {\n    return [\n      SpaceMoreActionType.rename,\n      SpaceMoreActionType.changeIcon,\n      SpaceMoreActionType.manage,\n      SpaceMoreActionType.duplicate,\n      SpaceMoreActionType.divider,\n      SpaceMoreActionType.addNewSpace,\n      SpaceMoreActionType.collapseAllPages,\n      SpaceMoreActionType.divider,\n      SpaceMoreActionType.delete,\n    ];\n  }\n}\n\nclass SpaceMoreActionTypeWrapper extends CustomActionCell {\n  SpaceMoreActionTypeWrapper(this.inner, this.onTap);\n\n  final SpaceMoreActionType inner;\n  final void Function(PopoverController controller, dynamic data) onTap;\n\n  @override\n  Widget buildWithContext(\n    BuildContext context,\n    PopoverController controller,\n    PopoverMutex? mutex,\n  ) {\n    if (inner == SpaceMoreActionType.divider) {\n      return _buildDivider();\n    } else if (inner == SpaceMoreActionType.changeIcon) {\n      return _buildEmojiActionButton(context, controller);\n    } else {\n      return _buildNormalActionButton(context, controller);\n    }\n  }\n\n  Widget _buildNormalActionButton(\n    BuildContext context,\n    PopoverController controller,\n  ) {\n    return _buildActionButton(context, () => onTap(controller, null));\n  }\n\n  Widget _buildEmojiActionButton(\n    BuildContext context,\n    PopoverController controller,\n  ) {\n    final child = _buildActionButton(context, null);\n    return AppFlowyPopover(\n      constraints: BoxConstraints.loose(const Size(360, 432)),\n      margin: const EdgeInsets.all(0),\n      clickHandler: PopoverClickHandler.gestureDetector,\n      offset: const Offset(0, -40),\n      popupBuilder: (context) {\n        return FlowyIconEmojiPicker(\n          tabs: const [PickerTabType.icon],\n          onSelectedEmoji: (r) => onTap(controller, r),\n        );\n      },\n      child: child,\n    );\n  }\n\n  Widget _buildDivider() {\n    return const Padding(\n      padding: EdgeInsets.all(8.0),\n      child: FlowyDivider(),\n    );\n  }\n\n  Widget _buildActionButton(\n    BuildContext context,\n    VoidCallback? onTap,\n  ) {\n    final spaceBloc = context.read<SpaceBloc>();\n    final spaces = spaceBloc.state.spaces;\n    final currentSpace = spaceBloc.state.currentSpace;\n\n    final isOwner = context\n            .read<UserWorkspaceBloc?>()\n            ?.state\n            .currentWorkspace\n            ?.role\n            .isOwner ??\n        false;\n    final isPageCreator =\n        currentSpace?.createdBy == context.read<UserProfilePB>().id;\n    final allowToDelete = isOwner || isPageCreator;\n\n    bool disable = false;\n    var message = '';\n    if (inner == SpaceMoreActionType.delete) {\n      if (spaces.length <= 1) {\n        disable = true;\n        message = LocaleKeys.space_unableToDeleteLastSpace.tr();\n      } else if (!allowToDelete) {\n        disable = true;\n        message = LocaleKeys.space_unableToDeleteSpaceNotCreatedByYou.tr();\n      }\n    }\n\n    final child = Container(\n      height: 34,\n      padding: const EdgeInsets.symmetric(vertical: 2.0),\n      child: Opacity(\n        opacity: disable ? 0.3 : 1.0,\n        child: FlowyIconTextButton(\n          disable: disable,\n          margin: const EdgeInsets.symmetric(horizontal: 6),\n          iconPadding: 10.0,\n          onTap: onTap,\n          leftIconBuilder: (onHover) => FlowySvg(\n            inner.leftIconSvg,\n            color: inner == SpaceMoreActionType.delete && onHover\n                ? Theme.of(context).colorScheme.error\n                : null,\n          ),\n          rightIconBuilder: (_) => inner.rightIcon,\n          textBuilder: (onHover) => FlowyText.regular(\n            inner.name,\n            fontSize: 14.0,\n            figmaLineHeight: 18.0,\n            color: inner == SpaceMoreActionType.delete && onHover\n                ? Theme.of(context).colorScheme.error\n                : null,\n          ),\n        ),\n      ),\n    );\n\n    if (inner == SpaceMoreActionType.delete) {\n      return FlowyTooltip(\n        message: message,\n        child: child,\n      );\n    }\n\n    return child;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_import_notion.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/settings/share/import_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/import.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass NotionImporter extends StatelessWidget {\n  const NotionImporter({required this.filePath, super.key});\n\n  final String filePath;\n\n  @override\n  Widget build(BuildContext context) {\n    return ConstrainedBox(\n      constraints: const BoxConstraints(minHeight: 30, maxHeight: 200),\n      child: FutureBuilder(\n        future: _uploadFile(),\n        builder: (context, snapshots) {\n          if (!snapshots.hasData) {\n            return const _Uploading();\n          }\n\n          final result = snapshots.data;\n          if (result == null) {\n            return const _UploadSuccess();\n          } else {\n            return result.fold(\n              (_) => const _UploadSuccess(),\n              (err) => _UploadError(error: err),\n            );\n          }\n        },\n      ),\n    );\n  }\n\n  Future<FlowyResult<void, FlowyError>> _uploadFile() async {\n    final importResult = await ImportBackendService.importZipFiles(\n      [ImportZipPB()..filePath = filePath],\n    );\n\n    return importResult;\n  }\n}\n\nclass _UploadSuccess extends StatelessWidget {\n  const _UploadSuccess();\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyText(\n      fontSize: 16,\n      LocaleKeys.settings_common_uploadNotionSuccess.tr(),\n      maxLines: 10,\n    );\n  }\n}\n\nclass _Uploading extends StatelessWidget {\n  const _Uploading();\n\n  @override\n  Widget build(BuildContext context) {\n    return IntrinsicHeight(\n      child: Center(\n        child: Column(\n          children: [\n            const CircularProgressIndicator.adaptive(),\n            const VSpace(12),\n            FlowyText(\n              fontSize: 16,\n              LocaleKeys.settings_common_uploadingFile.tr(),\n              maxLines: null,\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _UploadError extends StatelessWidget {\n  const _UploadError({required this.error});\n\n  final FlowyError error;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyText(error.msg, maxLines: 10);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/af_role_pb_extension.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nenum WorkspaceMoreAction {\n  rename,\n  delete,\n  leave,\n  divider,\n}\n\nclass WorkspaceMoreActionList extends StatefulWidget {\n  const WorkspaceMoreActionList({\n    super.key,\n    required this.workspace,\n    required this.popoverMutex,\n  });\n\n  final UserWorkspacePB workspace;\n  final PopoverMutex popoverMutex;\n\n  @override\n  State<WorkspaceMoreActionList> createState() =>\n      _WorkspaceMoreActionListState();\n}\n\nclass _WorkspaceMoreActionListState extends State<WorkspaceMoreActionList> {\n  bool isPopoverOpen = false;\n\n  @override\n  Widget build(BuildContext context) {\n    final myRole = context.read<WorkspaceMemberBloc>().state.myRole;\n    final actions = [];\n    if (myRole.isOwner) {\n      actions.add(WorkspaceMoreAction.rename);\n      actions.add(WorkspaceMoreAction.divider);\n      actions.add(WorkspaceMoreAction.delete);\n    } else if (myRole.canLeave) {\n      actions.add(WorkspaceMoreAction.leave);\n    }\n    if (actions.isEmpty) {\n      return const SizedBox.shrink();\n    }\n    return PopoverActionList<_WorkspaceMoreActionWrapper>(\n      direction: PopoverDirection.bottomWithLeftAligned,\n      actions: actions\n          .map(\n            (action) => _WorkspaceMoreActionWrapper(\n              action,\n              widget.workspace,\n              () => PopoverContainer.of(context).closeAll(),\n            ),\n          )\n          .toList(),\n      mutex: widget.popoverMutex,\n      constraints: const BoxConstraints(minWidth: 220),\n      animationDuration: Durations.short3,\n      slideDistance: 2,\n      beginScaleFactor: 1.0,\n      beginOpacity: 0.8,\n      onClosed: () => isPopoverOpen = false,\n      asBarrier: true,\n      buildChild: (controller) {\n        return SizedBox.square(\n          dimension: 24.0,\n          child: FlowyButton(\n            margin: const EdgeInsets.symmetric(horizontal: 4.0),\n            text: const FlowySvg(\n              FlowySvgs.workspace_three_dots_s,\n            ),\n            onTap: () {\n              if (!isPopoverOpen) {\n                controller.show();\n                isPopoverOpen = true;\n              }\n            },\n          ),\n        );\n      },\n      onSelected: (action, controller) {},\n    );\n  }\n}\n\nclass _WorkspaceMoreActionWrapper extends CustomActionCell {\n  _WorkspaceMoreActionWrapper(\n    this.inner,\n    this.workspace,\n    this.closeWorkspaceMenu,\n  );\n\n  final WorkspaceMoreAction inner;\n  final UserWorkspacePB workspace;\n  final VoidCallback closeWorkspaceMenu;\n\n  @override\n  Widget buildWithContext(\n    BuildContext context,\n    PopoverController controller,\n    PopoverMutex? mutex,\n  ) {\n    if (inner == WorkspaceMoreAction.divider) {\n      return const Divider();\n    }\n\n    return _buildActionButton(context, controller);\n  }\n\n  Widget _buildActionButton(\n    BuildContext context,\n    PopoverController controller,\n  ) {\n    return FlowyIconTextButton(\n      leftIconBuilder: (onHover) => buildLeftIcon(context, onHover),\n      iconPadding: 10.0,\n      textBuilder: (onHover) => FlowyText.regular(\n        name,\n        fontSize: 14.0,\n        figmaLineHeight: 18.0,\n        color: [WorkspaceMoreAction.delete, WorkspaceMoreAction.leave]\n                    .contains(inner) &&\n                onHover\n            ? Theme.of(context).colorScheme.error\n            : null,\n      ),\n      margin: const EdgeInsets.all(6),\n      onTap: () async {\n        PopoverContainer.of(context).closeAll();\n        closeWorkspaceMenu();\n\n        final workspaceBloc = context.read<UserWorkspaceBloc>();\n        switch (inner) {\n          case WorkspaceMoreAction.divider:\n            break;\n          case WorkspaceMoreAction.delete:\n            await showConfirmDeletionDialog(\n              context: context,\n              name: workspace.name,\n              description: LocaleKeys.workspace_deleteWorkspaceHintText.tr(),\n              onConfirm: () {\n                workspaceBloc.add(\n                  UserWorkspaceEvent.deleteWorkspace(\n                    workspaceId: workspace.workspaceId,\n                  ),\n                );\n              },\n            );\n          case WorkspaceMoreAction.rename:\n            await showAFTextFieldDialog(\n              context: context,\n              title: LocaleKeys.workspace_renameWorkspace.tr(),\n              initialValue: workspace.name,\n              hintText: '',\n              onConfirm: (name) async {\n                workspaceBloc.add(\n                  UserWorkspaceEvent.renameWorkspace(\n                    workspaceId: workspace.workspaceId,\n                    name: name,\n                  ),\n                );\n              },\n            );\n          case WorkspaceMoreAction.leave:\n            await showConfirmDialog(\n              context: context,\n              title: LocaleKeys.workspace_leaveCurrentWorkspace.tr(),\n              description:\n                  LocaleKeys.workspace_leaveCurrentWorkspacePrompt.tr(),\n              confirmLabel: LocaleKeys.button_yes.tr(),\n              onConfirm: (_) {\n                workspaceBloc.add(\n                  UserWorkspaceEvent.leaveWorkspace(\n                    workspaceId: workspace.workspaceId,\n                  ),\n                );\n              },\n            );\n        }\n      },\n    );\n  }\n\n  String get name {\n    switch (inner) {\n      case WorkspaceMoreAction.delete:\n        return LocaleKeys.button_delete.tr();\n      case WorkspaceMoreAction.rename:\n        return LocaleKeys.button_rename.tr();\n      case WorkspaceMoreAction.leave:\n        return LocaleKeys.workspace_leaveCurrentWorkspace.tr();\n      case WorkspaceMoreAction.divider:\n        return '';\n    }\n  }\n\n  Widget buildLeftIcon(BuildContext context, bool onHover) {\n    switch (inner) {\n      case WorkspaceMoreAction.delete:\n        return FlowySvg(\n          FlowySvgs.trash_s,\n          color: onHover ? Theme.of(context).colorScheme.error : null,\n        );\n      case WorkspaceMoreAction.rename:\n        return const FlowySvg(FlowySvgs.view_item_rename_s);\n      case WorkspaceMoreAction.leave:\n        return FlowySvg(\n          FlowySvgs.logout_s,\n          color: onHover ? Theme.of(context).colorScheme.error : null,\n        );\n      case WorkspaceMoreAction.divider:\n        return const SizedBox.shrink();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/util/color_generator/color_generator.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass WorkspaceIcon extends StatefulWidget {\n  const WorkspaceIcon({\n    super.key,\n    required this.workspaceIcon,\n    required this.workspaceName,\n    required this.iconSize,\n    required this.isEditable,\n    required this.fontSize,\n    required this.onSelected,\n    required this.borderRadius,\n    required this.emojiSize,\n    required this.figmaLineHeight,\n    this.showBorder = true,\n  });\n\n  final String workspaceIcon;\n  final String workspaceName;\n  final double iconSize;\n  final bool isEditable;\n  final double fontSize;\n  final double? emojiSize;\n  final void Function(EmojiIconData) onSelected;\n  final double borderRadius;\n  final double figmaLineHeight;\n  final bool showBorder;\n\n  @override\n  State<WorkspaceIcon> createState() => _WorkspaceIconState();\n}\n\nclass _WorkspaceIconState extends State<WorkspaceIcon> {\n  final controller = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    final (textColor, backgroundColor) =\n        ColorGenerator(widget.workspaceName).randomColor();\n\n    Widget child = widget.workspaceIcon.isNotEmpty\n        ? FlowyText.emoji(\n            widget.workspaceIcon,\n            fontSize: widget.emojiSize,\n            figmaLineHeight: widget.figmaLineHeight,\n            optimizeEmojiAlign: true,\n          )\n        : FlowyText.semibold(\n            widget.workspaceName.isEmpty\n                ? ''\n                : widget.workspaceName.substring(0, 1),\n            fontSize: widget.fontSize,\n            color: textColor,\n          );\n\n    child = Container(\n      alignment: Alignment.center,\n      width: widget.iconSize,\n      height: widget.iconSize,\n      decoration: BoxDecoration(\n        color: widget.workspaceIcon.isEmpty ? backgroundColor : null,\n        borderRadius: BorderRadius.circular(widget.borderRadius),\n        border: widget.showBorder\n            ? Border.all(color: const Color(0x1A717171))\n            : null,\n      ),\n      child: child,\n    );\n\n    if (widget.isEditable) {\n      child = _buildEditableIcon(child);\n    }\n\n    return child;\n  }\n\n  Widget _buildEditableIcon(Widget child) {\n    if (UniversalPlatform.isDesktopOrWeb) {\n      return AppFlowyPopover(\n        offset: const Offset(0, 8),\n        controller: controller,\n        direction: PopoverDirection.bottomWithLeftAligned,\n        constraints: BoxConstraints.loose(const Size(364, 356)),\n        clickHandler: PopoverClickHandler.gestureDetector,\n        margin: const EdgeInsets.all(0),\n        popupBuilder: (_) => FlowyIconEmojiPicker(\n          tabs: const [PickerTabType.emoji],\n          onSelectedEmoji: (r) {\n            widget.onSelected(r.data);\n            if (!r.keepOpen) {\n              controller.close();\n            }\n          },\n        ),\n        child: MouseRegion(\n          cursor: SystemMouseCursors.click,\n          child: child,\n        ),\n      );\n    }\n\n    return GestureDetector(\n      onTap: () async {\n        final result = await context.push<EmojiIconData>(\n          Uri(\n            path: MobileEmojiPickerScreen.routeName,\n            queryParameters: {\n              MobileEmojiPickerScreen.pageTitle:\n                  LocaleKeys.settings_workspacePage_workspaceIcon_title.tr(),\n              MobileEmojiPickerScreen.selectTabs: [PickerTabType.emoji.name],\n            },\n          ).toString(),\n        );\n        if (result != null) {\n          widget.onSelected(result);\n        }\n      },\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/guest_tag.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/file_picker/file_picker_service.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '_sidebar_import_notion.dart';\n\n@visibleForTesting\nconst createWorkspaceButtonKey = ValueKey('createWorkspaceButton');\n\n@visibleForTesting\nconst importNotionButtonKey = ValueKey('importNotionButton');\n\nclass WorkspacesMenu extends StatefulWidget {\n  const WorkspacesMenu({\n    super.key,\n    required this.userProfile,\n    required this.currentWorkspace,\n    required this.workspaces,\n  });\n\n  final UserProfilePB userProfile;\n  final UserWorkspacePB currentWorkspace;\n  final List<UserWorkspacePB> workspaces;\n\n  @override\n  State<WorkspacesMenu> createState() => _WorkspacesMenuState();\n}\n\nclass _WorkspacesMenuState extends State<WorkspacesMenu> {\n  final popoverMutex = PopoverMutex();\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        // user email\n        Padding(\n          padding: const EdgeInsets.only(left: 10.0, top: 6.0, right: 10.0),\n          child: Row(\n            children: [\n              Expanded(\n                child: FlowyText.medium(\n                  _getUserInfo(),\n                  fontSize: 12.0,\n                  overflow: TextOverflow.ellipsis,\n                  color: Theme.of(context).hintColor,\n                ),\n              ),\n              const HSpace(4.0),\n              WorkspaceMoreButton(\n                popoverMutex: popoverMutex,\n              ),\n              const HSpace(8.0),\n            ],\n          ),\n        ),\n        const Padding(\n          padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 6.0),\n          child: Divider(height: 1.0),\n        ),\n        // workspace list\n        Flexible(\n          child: SingleChildScrollView(\n            padding: const EdgeInsets.symmetric(horizontal: 6.0),\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                for (final workspace in widget.workspaces) ...[\n                  WorkspaceMenuItem(\n                    key: ValueKey(workspace.workspaceId),\n                    workspace: workspace,\n                    userProfile: widget.userProfile,\n                    isSelected: workspace.workspaceId ==\n                        widget.currentWorkspace.workspaceId,\n                    popoverMutex: popoverMutex,\n                  ),\n                  const VSpace(6.0),\n                ],\n              ],\n            ),\n          ),\n        ),\n        // add new workspace\n        const Padding(\n          padding: EdgeInsets.symmetric(horizontal: 6.0),\n          child: _CreateWorkspaceButton(),\n        ),\n\n        if (UniversalPlatform.isDesktop) ...[\n          const Padding(\n            padding: EdgeInsets.only(left: 6.0, top: 6.0, right: 6.0),\n            child: _ImportNotionButton(),\n          ),\n        ],\n\n        const VSpace(6.0),\n      ],\n    );\n  }\n\n  String _getUserInfo() {\n    if (widget.userProfile.email.isNotEmpty) {\n      return widget.userProfile.email;\n    }\n\n    if (widget.userProfile.name.isNotEmpty) {\n      return widget.userProfile.name;\n    }\n\n    return LocaleKeys.defaultUsername.tr();\n  }\n}\n\nclass WorkspaceMenuItem extends StatefulWidget {\n  const WorkspaceMenuItem({\n    super.key,\n    required this.workspace,\n    required this.userProfile,\n    required this.isSelected,\n    required this.popoverMutex,\n  });\n\n  final UserProfilePB userProfile;\n  final UserWorkspacePB workspace;\n  final bool isSelected;\n  final PopoverMutex popoverMutex;\n\n  @override\n  State<WorkspaceMenuItem> createState() => _WorkspaceMenuItemState();\n}\n\nclass _WorkspaceMenuItemState extends State<WorkspaceMenuItem> {\n  final ValueNotifier<bool> isHovered = ValueNotifier(false);\n\n  @override\n  void dispose() {\n    isHovered.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) => WorkspaceMemberBloc(\n        userProfile: widget.userProfile,\n        workspace: widget.workspace,\n      )..add(const WorkspaceMemberEvent.initial()),\n      child: BlocBuilder<WorkspaceMemberBloc, WorkspaceMemberState>(\n        builder: (context, state) {\n          // settings right icon inside the flowy button will\n          //  cause the popover dismiss intermediately when click the right icon.\n          // so using the stack to put the right icon on the flowy button.\n          return SizedBox(\n            height: 44,\n            child: MouseRegion(\n              onEnter: (_) => isHovered.value = true,\n              onExit: (_) => isHovered.value = false,\n              child: Stack(\n                alignment: Alignment.center,\n                children: [\n                  _WorkspaceInfo(\n                    isSelected: widget.isSelected,\n                    workspace: widget.workspace,\n                  ),\n                  Positioned(left: 4, child: _buildLeftIcon(context)),\n                  Positioned(\n                    right: 4.0,\n                    child: Align(child: _buildRightIcon(context, isHovered)),\n                  ),\n                ],\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildLeftIcon(BuildContext context) {\n    return FlowyTooltip(\n      message: LocaleKeys.document_plugins_cover_changeIcon.tr(),\n      child: WorkspaceIcon(\n        workspaceName: widget.workspace.name,\n        workspaceIcon: widget.workspace.icon,\n        iconSize: 36,\n        emojiSize: 24.0,\n        fontSize: 18.0,\n        figmaLineHeight: 26.0,\n        borderRadius: 12.0,\n        isEditable: true,\n        onSelected: (result) => context.read<UserWorkspaceBloc>().add(\n              UserWorkspaceEvent.updateWorkspaceIcon(\n                workspaceId: widget.workspace.workspaceId,\n                icon: result.emoji,\n              ),\n            ),\n      ),\n    );\n  }\n\n  Widget _buildRightIcon(BuildContext context, ValueNotifier<bool> isHovered) {\n    return Row(\n      children: [\n        // only the owner can update or delete workspace.\n        if (!context.read<WorkspaceMemberBloc>().state.isLoading)\n          ValueListenableBuilder(\n            valueListenable: isHovered,\n            builder: (context, value, child) {\n              return Padding(\n                padding: const EdgeInsets.only(left: 8.0),\n                child: Opacity(\n                  opacity: value ? 1.0 : 0.0,\n                  child: child,\n                ),\n              );\n            },\n            child: WorkspaceMoreActionList(\n              workspace: widget.workspace,\n              popoverMutex: widget.popoverMutex,\n            ),\n          ),\n        const HSpace(8.0),\n        if (widget.isSelected) ...[\n          const Padding(\n            padding: EdgeInsets.all(5.0),\n            child: FlowySvg(\n              FlowySvgs.workspace_selected_s,\n              blendMode: null,\n              size: Size.square(14.0),\n            ),\n          ),\n          const HSpace(8.0),\n        ],\n      ],\n    );\n  }\n}\n\nclass _WorkspaceInfo extends StatelessWidget {\n  const _WorkspaceInfo({\n    required this.isSelected,\n    required this.workspace,\n  });\n\n  final bool isSelected;\n  final UserWorkspacePB workspace;\n\n  @override\n  Widget build(BuildContext context) {\n    final memberCount = workspace.memberCount.toInt();\n    return FlowyButton(\n      onTap: () => _openWorkspace(context),\n      iconPadding: 10.0,\n      leftIconSize: const Size.square(32),\n      leftIcon: const SizedBox.square(dimension: 32),\n      rightIcon: const HSpace(32.0),\n      text: Row(\n        children: [\n          Flexible(\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                // workspace name\n                FlowyText.medium(\n                  workspace.name,\n                  fontSize: 14.0,\n                  figmaLineHeight: 17.0,\n                  overflow: TextOverflow.ellipsis,\n                  withTooltip: true,\n                ),\n                if (workspace.role != AFRolePB.Guest) ...[\n                  // workspace members count\n                  FlowyText.regular(\n                    memberCount == 0\n                        ? ''\n                        : LocaleKeys.settings_appearance_members_membersCount\n                            .plural(\n                            memberCount,\n                          ),\n                    fontSize: 10.0,\n                    figmaLineHeight: 12.0,\n                    color: Theme.of(context).hintColor,\n                  ),\n                ],\n              ],\n            ),\n          ),\n          if (workspace.role == AFRolePB.Guest) ...[\n            const HSpace(6.0),\n            GuestTag(),\n          ],\n        ],\n      ),\n    );\n  }\n\n  void _openWorkspace(BuildContext context) {\n    if (!isSelected) {\n      Log.info('open workspace: ${workspace.workspaceId}');\n\n      // Persist and close other tabs when switching workspace, restore tabs for new workspace\n      getIt<TabsBloc>().add(TabsEvent.switchWorkspace(workspace.workspaceId));\n\n      context.read<UserWorkspaceBloc>().add(\n            UserWorkspaceEvent.openWorkspace(\n              workspaceId: workspace.workspaceId,\n              workspaceType: workspace.workspaceType,\n            ),\n          );\n\n      PopoverContainer.of(context).closeAll();\n    }\n  }\n}\n\nclass _CreateWorkspaceButton extends StatelessWidget {\n  const _CreateWorkspaceButton();\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 40,\n      child: FlowyButton(\n        key: createWorkspaceButtonKey,\n        onTap: () {\n          _showCreateWorkspaceDialog(context);\n          PopoverContainer.of(context).closeAll();\n        },\n        margin: const EdgeInsets.symmetric(horizontal: 4.0),\n        text: Row(\n          children: [\n            _buildLeftIcon(context),\n            const HSpace(8.0),\n            FlowyText.regular(\n              LocaleKeys.workspace_create.tr(),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildLeftIcon(BuildContext context) {\n    return Container(\n      width: 36.0,\n      height: 36.0,\n      padding: const EdgeInsets.all(7.0),\n      decoration: BoxDecoration(\n        borderRadius: BorderRadius.circular(12),\n        border: Border.all(\n          color: const Color(0x01717171).withValues(alpha: 0.12),\n          width: 0.8,\n        ),\n      ),\n      child: const FlowySvg(FlowySvgs.add_workspace_s),\n    );\n  }\n\n  Future<void> _showCreateWorkspaceDialog(BuildContext context) async {\n    if (context.mounted) {\n      final workspaceBloc = context.read<UserWorkspaceBloc>();\n      await showAFTextFieldDialog(\n        context: context,\n        title: LocaleKeys.workspace_create.tr(),\n        initialValue: '',\n        onConfirm: (name) {\n          workspaceBloc.add(\n            UserWorkspaceEvent.createWorkspace(\n              name: name,\n              workspaceType: WorkspaceTypePB.ServerW,\n            ),\n          );\n        },\n      );\n    }\n  }\n}\n\nclass _ImportNotionButton extends StatelessWidget {\n  const _ImportNotionButton();\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 40,\n      child: FlowyButton(\n        key: importNotionButtonKey,\n        onTap: () {\n          _showImportNotinoDialog(context);\n        },\n        margin: const EdgeInsets.symmetric(horizontal: 4.0),\n        text: Row(\n          children: [\n            _buildLeftIcon(context),\n            const HSpace(8.0),\n            FlowyText.regular(\n              LocaleKeys.workspace_importFromNotion.tr(),\n            ),\n          ],\n        ),\n        rightIcon: FlowyTooltip(\n          message: LocaleKeys.workspace_learnMore.tr(),\n          preferBelow: true,\n          child: FlowyIconButton(\n            icon: const FlowySvg(\n              FlowySvgs.information_s,\n            ),\n            onPressed: () {\n              afLaunchUrlString(\n                'https://docs.appflowy.io/docs/guides/import-from-notion',\n              );\n            },\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildLeftIcon(BuildContext context) {\n    return Container(\n      width: 36.0,\n      height: 36.0,\n      padding: const EdgeInsets.all(7.0),\n      decoration: BoxDecoration(\n        borderRadius: BorderRadius.circular(12),\n        border: Border.all(\n          color: const Color(0x01717171).withValues(alpha: 0.12),\n          width: 0.8,\n        ),\n      ),\n      child: const FlowySvg(FlowySvgs.add_workspace_s),\n    );\n  }\n\n  Future<void> _showImportNotinoDialog(BuildContext context) async {\n    final result = await getIt<FilePickerService>().pickFiles(\n      type: FileType.custom,\n      allowedExtensions: ['zip'],\n    );\n\n    if (result == null || result.files.isEmpty) {\n      return;\n    }\n\n    final path = result.files.first.path;\n    if (path == null) {\n      return;\n    }\n\n    if (context.mounted) {\n      PopoverContainer.of(context).closeAll();\n      await NavigatorCustomDialog(\n        hideCancelButton: true,\n        confirm: () {},\n        child: NotionImporter(\n          filePath: path,\n        ),\n      ).show(context);\n    } else {\n      Log.error('context is not mounted when showing import notion dialog');\n    }\n  }\n}\n\n@visibleForTesting\nclass WorkspaceMoreButton extends StatelessWidget {\n  const WorkspaceMoreButton({\n    super.key,\n    required this.popoverMutex,\n  });\n\n  final PopoverMutex popoverMutex;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      direction: PopoverDirection.bottomWithLeftAligned,\n      offset: const Offset(0, 6),\n      mutex: popoverMutex,\n      asBarrier: true,\n      popupBuilder: (_) => FlowyButton(\n        margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 7.0),\n        leftIcon: const FlowySvg(FlowySvgs.workspace_logout_s),\n        iconPadding: 10.0,\n        text: FlowyText.regular(LocaleKeys.button_logout.tr()),\n        onTap: () async {\n          await getIt<AuthService>().signOut();\n          await runAppFlowy();\n        },\n      ),\n      child: SizedBox.square(\n        dimension: 24.0,\n        child: FlowyButton(\n          useIntrinsicWidth: true,\n          margin: EdgeInsets.zero,\n          text: const FlowySvg(\n            FlowySvgs.workspace_three_dots_s,\n            size: Size.square(16.0),\n          ),\n          onTap: () {},\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/loading.dart';\nimport 'package:appflowy/workspace/application/home/home_setting_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/workspace_notifier.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SidebarWorkspace extends StatefulWidget {\n  const SidebarWorkspace({super.key, required this.userProfile});\n\n  final UserProfilePB userProfile;\n\n  @override\n  State<SidebarWorkspace> createState() => _SidebarWorkspaceState();\n}\n\nclass _SidebarWorkspaceState extends State<SidebarWorkspace> {\n  Loading? loadingIndicator;\n\n  final ValueNotifier<bool> onHover = ValueNotifier(false);\n  int maxRetryCount = 3;\n  int retryCount = 0;\n\n  @override\n  void initState() {\n    super.initState();\n\n    openWorkspaceNotifier.addListener(_openWorkspaceFromInvitation);\n  }\n\n  @override\n  void dispose() {\n    onHover.dispose();\n    openWorkspaceNotifier.removeListener(_openWorkspaceFromInvitation);\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(\n      listenWhen: (previous, current) =>\n          previous.actionResult != current.actionResult,\n      listener: _showResultDialog,\n      builder: (context, state) {\n        final currentWorkspace = state.currentWorkspace;\n        if (currentWorkspace == null) {\n          return const SizedBox.shrink();\n        }\n        return MouseRegion(\n          onEnter: (_) => onHover.value = true,\n          onExit: (_) => onHover.value = false,\n          child: ValueListenableBuilder(\n            valueListenable: onHover,\n            builder: (_, onHover, child) {\n              return Container(\n                margin: const EdgeInsets.only(right: 8.0),\n                decoration: BoxDecoration(\n                  borderRadius: BorderRadius.circular(8.0),\n                  color: onHover\n                      ? Theme.of(context).colorScheme.secondary\n                      : Colors.transparent,\n                ),\n                child: Row(\n                  children: [\n                    Expanded(\n                      child: SidebarSwitchWorkspaceButton(\n                        userProfile: widget.userProfile,\n                        currentWorkspace: currentWorkspace,\n                        isHover: onHover,\n                      ),\n                    ),\n                    UserSettingButton(\n                      isHover: onHover,\n                    ),\n                    const HSpace(8.0),\n                    NotificationButton(\n                      isHover: onHover,\n                      key: ValueKey(currentWorkspace.workspaceId),\n                    ),\n                    const HSpace(4.0),\n                  ],\n                ),\n              );\n            },\n          ),\n        );\n      },\n    );\n  }\n\n  void _showResultDialog(BuildContext context, UserWorkspaceState state) {\n    final actionResult = state.actionResult;\n    if (actionResult == null) {\n      return;\n    }\n\n    final settingBloc = context.read<HomeSettingBloc?>();\n    if (settingBloc?.state.isNotificationPanelCollapsed == false) {\n      settingBloc?.add(HomeSettingEvent.collapseNotificationPanel());\n    }\n\n    final actionType = actionResult.actionType;\n    final result = actionResult.result;\n    final isLoading = actionResult.isLoading;\n\n    if (isLoading) {\n      loadingIndicator ??= Loading(context)..start();\n      return;\n    } else {\n      loadingIndicator?.stop();\n      loadingIndicator = null;\n    }\n\n    if (result == null) {\n      return;\n    }\n\n    result.onFailure((f) {\n      Log.error(\n        '[Workspace] Failed to perform ${actionType.toString()} action: $f',\n      );\n    });\n\n    // show a confirmation dialog if the action is create and the result is LimitExceeded failure\n    if (actionType == WorkspaceActionType.create &&\n        result.isFailure &&\n        result.getFailure().code == ErrorCode.WorkspaceLimitExceeded) {\n      showDialog(\n        context: context,\n        builder: (context) => NavigatorOkCancelDialog(\n          message: LocaleKeys.workspace_createLimitExceeded.tr(),\n        ),\n      );\n      return;\n    }\n\n    final String? message;\n    switch (actionType) {\n      case WorkspaceActionType.create:\n        message = result.fold(\n          (s) => LocaleKeys.workspace_createSuccess.tr(),\n          (e) => '${LocaleKeys.workspace_createFailed.tr()}: ${e.msg}',\n        );\n        break;\n      case WorkspaceActionType.delete:\n        message = result.fold(\n          (s) => LocaleKeys.workspace_deleteSuccess.tr(),\n          (e) => '${LocaleKeys.workspace_deleteFailed.tr()}: ${e.msg}',\n        );\n        break;\n      case WorkspaceActionType.open:\n        message = result.fold(\n          (s) => LocaleKeys.workspace_openSuccess.tr(),\n          (e) => '${LocaleKeys.workspace_openFailed.tr()}: ${e.msg}',\n        );\n\n        break;\n      case WorkspaceActionType.updateIcon:\n        message = result.fold(\n          (s) => LocaleKeys.workspace_updateIconSuccess.tr(),\n          (e) => '${LocaleKeys.workspace_updateIconFailed.tr()}: ${e.msg}',\n        );\n        break;\n      case WorkspaceActionType.rename:\n        message = result.fold(\n          (s) => LocaleKeys.workspace_renameSuccess.tr(),\n          (e) => '${LocaleKeys.workspace_renameFailed.tr()}: ${e.msg}',\n        );\n        break;\n\n      case WorkspaceActionType.fetchWorkspaces:\n      case WorkspaceActionType.none:\n      case WorkspaceActionType.leave:\n      case WorkspaceActionType.fetchSubscriptionInfo:\n        message = null;\n        break;\n    }\n\n    if (message != null) {\n      showToastNotification(\n        message: message,\n        type: result.fold(\n          (_) => ToastificationType.success,\n          (_) => ToastificationType.error,\n        ),\n      );\n    }\n  }\n\n  // This function is a workaround, when we support open the workspace from invitation deep link,\n  // we should refactor the code here\n  void _openWorkspaceFromInvitation() {\n    final value = openWorkspaceNotifier.value;\n    final workspaceId = value?.workspaceId;\n    final email = value?.email;\n\n    if (workspaceId == null) {\n      Log.info('No workspace id to open');\n      return;\n    }\n\n    if (email == null) {\n      Log.info('Open workspace from invitation with no email');\n      return;\n    }\n\n    final state = context.read<UserWorkspaceBloc>().state;\n    final currentWorkspace = state.currentWorkspace;\n    if (currentWorkspace?.workspaceId == workspaceId) {\n      Log.info('Already in the workspace');\n      return;\n    }\n\n    if (email != widget.userProfile.email) {\n      Log.info(\n        'Current user email: ${widget.userProfile.email} is not the same as the email in the invitation: $email',\n      );\n      return;\n    }\n\n    final openWorkspace = state.workspaces.firstWhereOrNull(\n      (workspace) => workspace.workspaceId == workspaceId,\n    );\n\n    if (openWorkspace == null) {\n      Log.error('Workspace not found, try to fetch workspaces');\n\n      context.read<UserWorkspaceBloc>().add(\n            UserWorkspaceEvent.fetchWorkspaces(\n              initialWorkspaceId: workspaceId,\n            ),\n          );\n\n      Future.delayed(\n        Duration(milliseconds: 250 + retryCount * 250),\n        () {\n          if (retryCount >= maxRetryCount) {\n            openWorkspaceNotifier.value = null;\n            retryCount = 0;\n            Log.error('Failed to open workspace from invitation');\n            return;\n          }\n\n          retryCount++;\n          _openWorkspaceFromInvitation();\n        },\n      );\n\n      return;\n    }\n\n    context.read<UserWorkspaceBloc>().add(\n          UserWorkspaceEvent.openWorkspace(\n            workspaceId: workspaceId,\n            workspaceType: openWorkspace.workspaceType,\n          ),\n        );\n\n    openWorkspaceNotifier.value = null;\n  }\n}\n\nclass SidebarSwitchWorkspaceButton extends StatefulWidget {\n  const SidebarSwitchWorkspaceButton({\n    super.key,\n    required this.userProfile,\n    required this.currentWorkspace,\n    this.isHover = false,\n  });\n\n  final UserWorkspacePB currentWorkspace;\n  final UserProfilePB userProfile;\n  final bool isHover;\n\n  @override\n  State<SidebarSwitchWorkspaceButton> createState() =>\n      _SidebarSwitchWorkspaceButtonState();\n}\n\nclass _SidebarSwitchWorkspaceButtonState\n    extends State<SidebarSwitchWorkspaceButton> {\n  final PopoverController _popoverController = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      direction: PopoverDirection.bottomWithCenterAligned,\n      offset: const Offset(0, 5),\n      constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),\n      margin: EdgeInsets.zero,\n      animationDuration: Durations.short3,\n      beginScaleFactor: 1.0,\n      beginOpacity: 0.8,\n      controller: _popoverController,\n      triggerActions: PopoverTriggerFlags.none,\n      onOpen: () {\n        context\n            .read<UserWorkspaceBloc>()\n            .add(UserWorkspaceEvent.fetchWorkspaces());\n      },\n      popupBuilder: (_) {\n        return BlocProvider<UserWorkspaceBloc>.value(\n          value: context.read<UserWorkspaceBloc>(),\n          child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(\n            builder: (context, state) {\n              final currentWorkspace = state.currentWorkspace;\n              final workspaces = state.workspaces;\n              if (currentWorkspace == null) {\n                return const SizedBox.shrink();\n              }\n              return WorkspacesMenu(\n                userProfile: widget.userProfile,\n                currentWorkspace: currentWorkspace,\n                workspaces: workspaces,\n              );\n            },\n          ),\n        );\n      },\n      child: _SideBarSwitchWorkspaceButtonChild(\n        currentWorkspace: widget.currentWorkspace,\n        popoverController: _popoverController,\n        isHover: widget.isHover,\n      ),\n    );\n  }\n}\n\nclass _SideBarSwitchWorkspaceButtonChild extends StatelessWidget {\n  const _SideBarSwitchWorkspaceButtonChild({\n    required this.popoverController,\n    required this.currentWorkspace,\n    required this.isHover,\n  });\n\n  final PopoverController popoverController;\n  final UserWorkspacePB currentWorkspace;\n  final bool isHover;\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      cursor: SystemMouseCursors.click,\n      child: GestureDetector(\n        onTap: () {\n          context.read<UserWorkspaceBloc>().add(\n                UserWorkspaceEvent.fetchWorkspaces(),\n              );\n          popoverController.show();\n        },\n        behavior: HitTestBehavior.opaque,\n        child: SizedBox(\n          height: 30,\n          child: Row(\n            children: [\n              const HSpace(4.0),\n              WorkspaceIcon(\n                workspaceIcon: currentWorkspace.icon,\n                workspaceName: currentWorkspace.name,\n                iconSize: 26,\n                fontSize: 16,\n                emojiSize: 20,\n                isEditable: false,\n                showBorder: false,\n                borderRadius: 8.0,\n                figmaLineHeight: 18.0,\n                onSelected: (result) => context.read<UserWorkspaceBloc>().add(\n                      UserWorkspaceEvent.updateWorkspaceIcon(\n                        workspaceId: currentWorkspace.workspaceId,\n                        icon: result.emoji,\n                      ),\n                    ),\n              ),\n              const HSpace(6),\n              Flexible(\n                child: FlowyText.medium(\n                  currentWorkspace.name,\n                  color:\n                      isHover ? Theme.of(context).colorScheme.onSurface : null,\n                  overflow: TextOverflow.ellipsis,\n                  withTooltip: true,\n                  fontSize: 15.0,\n                ),\n              ),\n              if (isHover) ...[\n                const HSpace(4),\n                FlowySvg(\n                  FlowySvgs.workspace_drop_down_menu_show_s,\n                  color:\n                      isHover ? Theme.of(context).colorScheme.onSurface : null,\n                ),\n              ],\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/workspace_notifier.dart",
    "content": "// Workaround for open workspace from invitation deep link\nimport 'package:flutter/material.dart';\n\nValueNotifier<WorkspaceNotifyValue?> openWorkspaceNotifier =\n    ValueNotifier(null);\n\nclass WorkspaceNotifyValue {\n  WorkspaceNotifyValue({\n    this.workspaceId,\n    this.email,\n  });\n\n  final String? workspaceId;\n  final String? email;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart",
    "content": "import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/widgets/draggable_item/draggable_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nenum DraggableHoverPosition {\n  none,\n  top,\n  center,\n  bottom,\n}\n\nconst kDraggableViewItemDividerHeight = 2.0;\n\nclass DraggableViewItem extends StatefulWidget {\n  const DraggableViewItem({\n    super.key,\n    required this.view,\n    this.feedback,\n    required this.child,\n    this.isFirstChild = false,\n    this.centerHighlightColor,\n    this.topHighlightColor,\n    this.bottomHighlightColor,\n    this.onDragging,\n    this.onMove,\n  });\n\n  final Widget child;\n  final WidgetBuilder? feedback;\n  final ViewPB view;\n  final bool isFirstChild;\n  final Color? centerHighlightColor;\n  final Color? topHighlightColor;\n  final Color? bottomHighlightColor;\n  final void Function(bool isDragging)? onDragging;\n  final void Function(ViewPB from, ViewPB to)? onMove;\n\n  @override\n  State<DraggableViewItem> createState() => _DraggableViewItemState();\n}\n\nclass _DraggableViewItemState extends State<DraggableViewItem> {\n  DraggableHoverPosition position = DraggableHoverPosition.none;\n  final hoverColor = const Color(0xFF00C8FF);\n\n  @override\n  Widget build(BuildContext context) {\n    // add top border if the draggable item is on the top of the list\n    // highlight the draggable item if the draggable item is on the center\n    // add bottom border if the draggable item is on the bottom of the list\n    final child = UniversalPlatform.isMobile\n        ? _buildMobileDraggableItem()\n        : _buildDesktopDraggableItem();\n\n    return DraggableItem<ViewPB>(\n      data: widget.view,\n      onDragging: widget.onDragging,\n      onWillAcceptWithDetails: (data) => true,\n      onMove: (data) {\n        final renderBox = context.findRenderObject() as RenderBox;\n        final offset = renderBox.globalToLocal(data.offset);\n\n        if (offset.dx > renderBox.size.width) {\n          return;\n        }\n\n        final position = _computeHoverPosition(offset, renderBox.size);\n        if (!_shouldAccept(data.data, position)) {\n          return;\n        }\n        _updatePosition(position);\n      },\n      onLeave: (_) => _updatePosition(\n        DraggableHoverPosition.none,\n      ),\n      onAcceptWithDetails: (details) {\n        final data = details.data;\n        _move(data, widget.view);\n        _updatePosition(DraggableHoverPosition.none);\n      },\n      feedback: IntrinsicWidth(\n        child: Opacity(\n          opacity: 0.5,\n          child: widget.feedback?.call(context) ?? child,\n        ),\n      ),\n      child: child,\n    );\n  }\n\n  Widget _buildDesktopDraggableItem() {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        // only show the top border when the draggable item is the first child\n        if (widget.isFirstChild)\n          Divider(\n            height: kDraggableViewItemDividerHeight,\n            thickness: kDraggableViewItemDividerHeight,\n            color: position == DraggableHoverPosition.top\n                ? widget.topHighlightColor ?? hoverColor\n                : Colors.transparent,\n          ),\n        DecoratedBox(\n          decoration: BoxDecoration(\n            borderRadius: BorderRadius.circular(6.0),\n            color: position == DraggableHoverPosition.center\n                ? widget.centerHighlightColor ??\n                    hoverColor.withValues(alpha: 0.5)\n                : Colors.transparent,\n          ),\n          child: widget.child,\n        ),\n        Divider(\n          height: kDraggableViewItemDividerHeight,\n          thickness: kDraggableViewItemDividerHeight,\n          color: position == DraggableHoverPosition.bottom\n              ? widget.bottomHighlightColor ?? hoverColor\n              : Colors.transparent,\n        ),\n      ],\n    );\n  }\n\n  Widget _buildMobileDraggableItem() {\n    return Stack(\n      children: [\n        if (widget.isFirstChild)\n          Positioned(\n            top: 0,\n            left: 0,\n            right: 0,\n            height: kDraggableViewItemDividerHeight,\n            child: Divider(\n              height: kDraggableViewItemDividerHeight,\n              thickness: kDraggableViewItemDividerHeight,\n              color: position == DraggableHoverPosition.top\n                  ? widget.topHighlightColor ??\n                      Theme.of(context).colorScheme.secondary\n                  : Colors.transparent,\n            ),\n          ),\n        DecoratedBox(\n          decoration: BoxDecoration(\n            borderRadius: BorderRadius.circular(4.0),\n            color: position == DraggableHoverPosition.center\n                ? widget.centerHighlightColor ??\n                    Theme.of(context)\n                        .colorScheme\n                        .secondary\n                        .withValues(alpha: 0.5)\n                : Colors.transparent,\n          ),\n          child: widget.child,\n        ),\n        Positioned(\n          bottom: 0,\n          left: 0,\n          right: 0,\n          height: kDraggableViewItemDividerHeight,\n          child: Divider(\n            height: kDraggableViewItemDividerHeight,\n            thickness: kDraggableViewItemDividerHeight,\n            color: position == DraggableHoverPosition.bottom\n                ? widget.bottomHighlightColor ??\n                    Theme.of(context).colorScheme.secondary\n                : Colors.transparent,\n          ),\n        ),\n      ],\n    );\n  }\n\n  void _updatePosition(DraggableHoverPosition position) {\n    if (UniversalPlatform.isMobile && position != this.position) {\n      HapticFeedback.mediumImpact();\n    }\n    setState(() => this.position = position);\n  }\n\n  void _move(ViewPB from, ViewPB to) {\n    if (position == DraggableHoverPosition.center &&\n        to.layout != ViewLayoutPB.Document) {\n      // not support moving into a database\n      return;\n    }\n\n    if (widget.onMove != null) {\n      widget.onMove?.call(from, to);\n      return;\n    }\n\n    final fromSection = getViewSection(from);\n    final toSection = getViewSection(to);\n\n    switch (position) {\n      case DraggableHoverPosition.top:\n        context.read<ViewBloc>().add(\n              ViewEvent.move(\n                from,\n                to.parentViewId,\n                null,\n                fromSection,\n                toSection,\n              ),\n            );\n        break;\n      case DraggableHoverPosition.bottom:\n        context.read<ViewBloc>().add(\n              ViewEvent.move(\n                from,\n                to.parentViewId,\n                to.id,\n                fromSection,\n                toSection,\n              ),\n            );\n        break;\n      case DraggableHoverPosition.center:\n        context.read<ViewBloc>().add(\n              ViewEvent.move(\n                from,\n                to.id,\n                to.childViews.lastOrNull?.id,\n                fromSection,\n                toSection,\n              ),\n            );\n        break;\n      case DraggableHoverPosition.none:\n        break;\n    }\n  }\n\n  DraggableHoverPosition _computeHoverPosition(Offset offset, Size size) {\n    final threshold = size.height / 5.0;\n    if (widget.isFirstChild && offset.dy < -5.0) {\n      return DraggableHoverPosition.top;\n    }\n    if (offset.dy > threshold) {\n      return DraggableHoverPosition.bottom;\n    }\n    return DraggableHoverPosition.center;\n  }\n\n  bool _shouldAccept(ViewPB data, DraggableHoverPosition position) {\n    // could not move the view to a database\n    if (widget.view.layout.isDatabaseView &&\n        position == DraggableHoverPosition.center) {\n      return false;\n    }\n\n    // ignore moving the view to itself\n    if (data.id == widget.view.id) {\n      return false;\n    }\n\n    // ignore moving the view to its child view\n    if (data.containsView(widget.view)) {\n      return false;\n    }\n\n    return true;\n  }\n\n  ViewSectionPB? getViewSection(ViewPB view) {\n    return context.read<SidebarSectionsBloc>().getViewSection(view);\n  }\n}\n\nextension on ViewPB {\n  bool containsView(ViewPB view) {\n    if (id == view.id) {\n      return true;\n    }\n\n    return childViews.any((v) => v.containsView(view));\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nenum ViewMoreActionType {\n  delete,\n  favorite,\n  unFavorite,\n  duplicate,\n  copyLink, // not supported yet.\n  rename,\n  moveTo,\n  openInNewTab,\n  changeIcon,\n  collapseAllPages, // including sub pages\n  divider,\n  lastModified,\n  created,\n  lockPage,\n  leaveSharedPage;\n\n  static const disableInLockedView = [\n    delete,\n    rename,\n    moveTo,\n    changeIcon,\n  ];\n}\n\nextension ViewMoreActionTypeExtension on ViewMoreActionType {\n  String get name {\n    switch (this) {\n      case ViewMoreActionType.delete:\n        return LocaleKeys.disclosureAction_delete.tr();\n      case ViewMoreActionType.favorite:\n        return LocaleKeys.disclosureAction_favorite.tr();\n      case ViewMoreActionType.unFavorite:\n        return LocaleKeys.disclosureAction_unfavorite.tr();\n      case ViewMoreActionType.duplicate:\n        return LocaleKeys.disclosureAction_duplicate.tr();\n      case ViewMoreActionType.copyLink:\n        return LocaleKeys.disclosureAction_copyLink.tr();\n      case ViewMoreActionType.rename:\n        return LocaleKeys.disclosureAction_rename.tr();\n      case ViewMoreActionType.moveTo:\n        return LocaleKeys.disclosureAction_moveTo.tr();\n      case ViewMoreActionType.openInNewTab:\n        return LocaleKeys.disclosureAction_openNewTab.tr();\n      case ViewMoreActionType.changeIcon:\n        return LocaleKeys.disclosureAction_changeIcon.tr();\n      case ViewMoreActionType.collapseAllPages:\n        return LocaleKeys.disclosureAction_collapseAllPages.tr();\n      case ViewMoreActionType.lockPage:\n        return LocaleKeys.disclosureAction_lockPage.tr();\n      case ViewMoreActionType.leaveSharedPage:\n        return 'Leave';\n      case ViewMoreActionType.divider:\n      case ViewMoreActionType.lastModified:\n      case ViewMoreActionType.created:\n        return '';\n    }\n  }\n\n  FlowySvgData get leftIconSvg {\n    switch (this) {\n      case ViewMoreActionType.delete:\n        return FlowySvgs.trash_s;\n      case ViewMoreActionType.favorite:\n        return FlowySvgs.favorite_s;\n      case ViewMoreActionType.unFavorite:\n        return FlowySvgs.unfavorite_s;\n      case ViewMoreActionType.duplicate:\n        return FlowySvgs.duplicate_s;\n      case ViewMoreActionType.rename:\n        return FlowySvgs.view_item_rename_s;\n      case ViewMoreActionType.moveTo:\n        return FlowySvgs.move_to_s;\n      case ViewMoreActionType.openInNewTab:\n        return FlowySvgs.view_item_open_in_new_tab_s;\n      case ViewMoreActionType.changeIcon:\n        return FlowySvgs.change_icon_s;\n      case ViewMoreActionType.collapseAllPages:\n        return FlowySvgs.collapse_all_page_s;\n      case ViewMoreActionType.lockPage:\n        return FlowySvgs.lock_page_s;\n      case ViewMoreActionType.leaveSharedPage:\n        return FlowySvgs.leave_workspace_s;\n      case ViewMoreActionType.divider:\n      case ViewMoreActionType.lastModified:\n      case ViewMoreActionType.copyLink:\n      case ViewMoreActionType.created:\n        throw UnsupportedError('No left icon for $this');\n    }\n  }\n\n  Widget get rightIcon {\n    switch (this) {\n      case ViewMoreActionType.changeIcon:\n      case ViewMoreActionType.moveTo:\n      case ViewMoreActionType.favorite:\n      case ViewMoreActionType.unFavorite:\n      case ViewMoreActionType.duplicate:\n      case ViewMoreActionType.copyLink:\n      case ViewMoreActionType.rename:\n      case ViewMoreActionType.openInNewTab:\n      case ViewMoreActionType.collapseAllPages:\n      case ViewMoreActionType.divider:\n      case ViewMoreActionType.delete:\n      case ViewMoreActionType.lastModified:\n      case ViewMoreActionType.created:\n      case ViewMoreActionType.lockPage:\n      case ViewMoreActionType.leaveSharedPage:\n        return const SizedBox.shrink();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/document.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/import/import_panel.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass ViewAddButton extends StatelessWidget {\n  const ViewAddButton({\n    super.key,\n    required this.parentViewId,\n    required this.onEditing,\n    required this.onSelected,\n    this.isHovered = false,\n  });\n\n  final String parentViewId;\n  final void Function(bool value) onEditing;\n  final Function(\n    PluginBuilder,\n    String? name,\n    List<int>? initialDataBytes,\n    bool openAfterCreated,\n    bool createNewView,\n  ) onSelected;\n  final bool isHovered;\n\n  List<PopoverAction> get _actions {\n    return [\n      // document, grid, kanban, calendar\n      ...pluginBuilders().map(\n        (pluginBuilder) => ViewAddButtonActionWrapper(\n          pluginBuilder: pluginBuilder,\n        ),\n      ),\n      // import from ...\n      ...getIt<PluginSandbox>().builders.whereType<DocumentPluginBuilder>().map(\n            (pluginBuilder) => ViewImportActionWrapper(\n              pluginBuilder: pluginBuilder,\n            ),\n          ),\n    ];\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return PopoverActionList<PopoverAction>(\n      direction: PopoverDirection.bottomWithLeftAligned,\n      actions: _actions,\n      offset: const Offset(0, 8),\n      constraints: const BoxConstraints(\n        minWidth: 200,\n      ),\n      buildChild: (popover) {\n        return FlowyIconButton(\n          width: 24,\n          icon: FlowySvg(\n            FlowySvgs.view_item_add_s,\n            color: isHovered ? Theme.of(context).colorScheme.onSurface : null,\n          ),\n          onPressed: () {\n            onEditing(true);\n            popover.show();\n          },\n        );\n      },\n      onSelected: (action, popover) {\n        onEditing(false);\n        if (action is ViewAddButtonActionWrapper) {\n          _showViewAddButtonActions(context, action);\n        } else if (action is ViewImportActionWrapper) {\n          _showViewImportAction(context, action);\n        }\n        popover.close();\n      },\n      onClosed: () {\n        onEditing(false);\n      },\n    );\n  }\n\n  void _showViewAddButtonActions(\n    BuildContext context,\n    ViewAddButtonActionWrapper action,\n  ) {\n    onSelected(action.pluginBuilder, null, null, true, true);\n  }\n\n  void _showViewImportAction(\n    BuildContext context,\n    ViewImportActionWrapper action,\n  ) {\n    showImportPanel(\n      parentViewId,\n      context,\n      (type, name, initialDataBytes) {\n        onSelected(action.pluginBuilder, null, null, true, false);\n      },\n    );\n  }\n}\n\nclass ViewAddButtonActionWrapper extends ActionCell {\n  ViewAddButtonActionWrapper({\n    required this.pluginBuilder,\n  });\n\n  final PluginBuilder pluginBuilder;\n\n  @override\n  Widget? leftIcon(Color iconColor) => FlowySvg(\n        pluginBuilder.icon,\n        size: const Size.square(16),\n      );\n\n  @override\n  String get name => pluginBuilder.menuName;\n\n  PluginType get pluginType => pluginBuilder.pluginType;\n}\n\nclass ViewImportActionWrapper extends ActionCell {\n  ViewImportActionWrapper({\n    required this.pluginBuilder,\n  });\n\n  final DocumentPluginBuilder pluginBuilder;\n\n  @override\n  Widget? leftIcon(Color iconColor) => const FlowySvg(FlowySvgs.icon_import_s);\n\n  @override\n  String get name => LocaleKeys.moreAction_import.tr();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/view/prelude.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy/workspace/presentation/home/hotkeys.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart';\nimport 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\ntypedef ViewItemOnSelected = void Function(BuildContext context, ViewPB view);\ntypedef ViewItemLeftIconBuilder = Widget Function(\n  BuildContext context,\n  ViewPB view,\n);\ntypedef ViewItemRightIconsBuilder = List<Widget> Function(\n  BuildContext context,\n  ViewPB view,\n);\n\nenum IgnoreViewType { none, hide, disable }\n\nclass ViewItem extends StatelessWidget {\n  const ViewItem({\n    super.key,\n    required this.view,\n    this.parentView,\n    required this.spaceType,\n    required this.level,\n    this.leftPadding = 10,\n    required this.onSelected,\n    this.onTertiarySelected,\n    this.isFirstChild = false,\n    this.isDraggable = true,\n    required this.isFeedback,\n    this.height = HomeSpaceViewSizes.viewHeight,\n    this.isHoverEnabled = false,\n    this.isPlaceholder = false,\n    this.isHovered,\n    this.shouldRenderChildren = true,\n    this.leftIconBuilder,\n    this.rightIconsBuilder,\n    this.shouldLoadChildViews = true,\n    this.isExpandedNotifier,\n    this.extendBuilder,\n    this.disableSelectedStatus,\n    this.shouldIgnoreView,\n    this.engagedInExpanding = false,\n    this.enableRightClickContext = false,\n  });\n\n  final ViewPB view;\n  final ViewPB? parentView;\n\n  final FolderSpaceType spaceType;\n\n  // indicate the level of the view item\n  // used to calculate the left padding\n  final int level;\n\n  // the left padding of the view item for each level\n  // the left padding of the each level = level * leftPadding\n  final double leftPadding;\n\n  // Selected by normal conventions\n  final ViewItemOnSelected onSelected;\n\n  // Selected by middle mouse button\n  final ViewItemOnSelected? onTertiarySelected;\n\n  // used for indicating the first child of the parent view, so that we can\n  // add top border to the first child\n  final bool isFirstChild;\n\n  // it should be false when it's rendered as feedback widget inside DraggableItem\n  final bool isDraggable;\n\n  // identify if the view item is rendered as feedback widget inside DraggableItem\n  final bool isFeedback;\n\n  final double height;\n\n  final bool isHoverEnabled;\n\n  // all the view movement depends on the [ViewItem] widget, so we have to add a\n  // placeholder widget to receive the drop event when moving view across sections.\n  final bool isPlaceholder;\n\n  // used for control the expand/collapse icon\n  final ValueNotifier<bool>? isHovered;\n\n  // render the child views of the view\n  final bool shouldRenderChildren;\n\n  // custom the left icon widget, if it's null, the default expand/collapse icon will be used\n  final ViewItemLeftIconBuilder? leftIconBuilder;\n\n  // custom the right icon widget, if it's null, the default ... and + button will be used\n  final ViewItemRightIconsBuilder? rightIconsBuilder;\n\n  final bool shouldLoadChildViews;\n  final PropertyValueNotifier<bool>? isExpandedNotifier;\n\n  final List<Widget> Function(ViewPB view)? extendBuilder;\n\n  // disable the selected status of the view item\n  final bool? disableSelectedStatus;\n\n  // ignore the views when rendering the child views\n  final IgnoreViewType Function(ViewPB view)? shouldIgnoreView;\n\n  /// Whether to add right-click to show the view action context menu\n  ///\n  final bool enableRightClickContext;\n\n  /// to record the ViewBlock which is expanded or collapsed\n  final bool engagedInExpanding;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) => ViewBloc(\n        view: view,\n        shouldLoadChildViews: shouldLoadChildViews,\n        engagedInExpanding: engagedInExpanding,\n      )..add(const ViewEvent.initial()),\n      child: BlocConsumer<ViewBloc, ViewState>(\n        listenWhen: (p, c) =>\n            c.lastCreatedView != null &&\n            p.lastCreatedView?.id != c.lastCreatedView!.id,\n        listener: (context, state) =>\n            context.read<TabsBloc>().openPlugin(state.lastCreatedView!),\n        builder: (context, state) {\n          // filter the child views that should be ignored\n          List<ViewPB> childViews = state.view.childViews;\n          if (shouldIgnoreView != null) {\n            childViews = childViews\n                .where((v) => shouldIgnoreView!(v) != IgnoreViewType.hide)\n                .toList();\n          }\n\n          final Widget child = InnerViewItem(\n            view: state.view,\n            parentView: parentView,\n            childViews: childViews,\n            spaceType: spaceType,\n            level: level,\n            leftPadding: leftPadding,\n            showActions: state.isEditing,\n            enableRightClickContext: enableRightClickContext,\n            isExpanded: state.isExpanded,\n            disableSelectedStatus: disableSelectedStatus,\n            onSelected: onSelected,\n            onTertiarySelected: onTertiarySelected,\n            isFirstChild: isFirstChild,\n            isDraggable: isDraggable,\n            isFeedback: isFeedback,\n            height: height,\n            isHoverEnabled: isHoverEnabled,\n            isPlaceholder: isPlaceholder,\n            isHovered: isHovered,\n            shouldRenderChildren: shouldRenderChildren,\n            leftIconBuilder: leftIconBuilder,\n            rightIconsBuilder: rightIconsBuilder,\n            isExpandedNotifier: isExpandedNotifier,\n            extendBuilder: extendBuilder,\n            shouldIgnoreView: shouldIgnoreView,\n            engagedInExpanding: engagedInExpanding,\n          );\n\n          if (shouldIgnoreView?.call(view) == IgnoreViewType.disable) {\n            return Opacity(\n              opacity: 0.5,\n              child: FlowyTooltip(\n                message: LocaleKeys.space_cannotMovePageToDatabase.tr(),\n                child: MouseRegion(\n                  cursor: SystemMouseCursors.forbidden,\n                  child: IgnorePointer(child: child),\n                ),\n              ),\n            );\n          }\n\n          return child;\n        },\n      ),\n    );\n  }\n}\n\n// TODO: We shouldn't have local global variables\nbool _isDragging = false;\n\nclass InnerViewItem extends StatefulWidget {\n  const InnerViewItem({\n    super.key,\n    required this.view,\n    required this.parentView,\n    required this.childViews,\n    required this.spaceType,\n    this.isDraggable = true,\n    this.isExpanded = true,\n    required this.level,\n    required this.leftPadding,\n    required this.showActions,\n    this.enableRightClickContext = false,\n    required this.onSelected,\n    this.onTertiarySelected,\n    this.isFirstChild = false,\n    required this.isFeedback,\n    required this.height,\n    this.isHoverEnabled = true,\n    this.isPlaceholder = false,\n    this.isHovered,\n    this.shouldRenderChildren = true,\n    required this.leftIconBuilder,\n    required this.rightIconsBuilder,\n    this.isExpandedNotifier,\n    required this.extendBuilder,\n    this.disableSelectedStatus,\n    this.engagedInExpanding = false,\n    required this.shouldIgnoreView,\n  });\n\n  final ViewPB view;\n  final ViewPB? parentView;\n  final List<ViewPB> childViews;\n  final FolderSpaceType spaceType;\n\n  final bool isDraggable;\n  final bool isExpanded;\n  final bool isFirstChild;\n\n  // identify if the view item is rendered as feedback widget inside DraggableItem\n  final bool isFeedback;\n\n  final int level;\n  final double leftPadding;\n\n  final bool showActions;\n  final bool enableRightClickContext;\n  final ViewItemOnSelected onSelected;\n  final ViewItemOnSelected? onTertiarySelected;\n  final double height;\n\n  final bool isHoverEnabled;\n  final bool isPlaceholder;\n  final bool? disableSelectedStatus;\n  final ValueNotifier<bool>? isHovered;\n  final bool shouldRenderChildren;\n  final ViewItemLeftIconBuilder? leftIconBuilder;\n  final ViewItemRightIconsBuilder? rightIconsBuilder;\n\n  final PropertyValueNotifier<bool>? isExpandedNotifier;\n  final List<Widget> Function(ViewPB view)? extendBuilder;\n  final IgnoreViewType Function(ViewPB view)? shouldIgnoreView;\n  final bool engagedInExpanding;\n\n  @override\n  State<InnerViewItem> createState() => _InnerViewItemState();\n}\n\nclass _InnerViewItemState extends State<InnerViewItem> {\n  @override\n  void initState() {\n    super.initState();\n    widget.isExpandedNotifier?.addListener(_collapseAllPages);\n  }\n\n  @override\n  void dispose() {\n    widget.isExpandedNotifier?.removeListener(_collapseAllPages);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child = ValueListenableBuilder(\n      valueListenable: getIt<MenuSharedState>().notifier,\n      builder: (context, value, _) {\n        final isSelected = value?.id == widget.view.id;\n        return SingleInnerViewItem(\n          view: widget.view,\n          parentView: widget.parentView,\n          level: widget.level,\n          showActions: widget.showActions,\n          enableRightClickContext: widget.enableRightClickContext,\n          spaceType: widget.spaceType,\n          onSelected: widget.onSelected,\n          onTertiarySelected: widget.onTertiarySelected,\n          isExpanded: widget.isExpanded,\n          isDraggable: widget.isDraggable,\n          leftPadding: widget.leftPadding,\n          isFeedback: widget.isFeedback,\n          height: widget.height,\n          isPlaceholder: widget.isPlaceholder,\n          isHovered: widget.isHovered,\n          leftIconBuilder: widget.leftIconBuilder,\n          rightIconsBuilder: widget.rightIconsBuilder,\n          extendBuilder: widget.extendBuilder,\n          disableSelectedStatus: widget.disableSelectedStatus,\n          shouldIgnoreView: widget.shouldIgnoreView,\n          isSelected: isSelected,\n        );\n      },\n    );\n\n    // if the view is expanded and has child views, render its child views\n    if (widget.isExpanded &&\n        widget.shouldRenderChildren &&\n        widget.childViews.isNotEmpty) {\n      final children = widget.childViews.map((childView) {\n        return ViewItem(\n          key: ValueKey('${widget.spaceType.name} ${childView.id}'),\n          parentView: widget.view,\n          spaceType: widget.spaceType,\n          isFirstChild: childView.id == widget.childViews.first.id,\n          view: childView,\n          level: widget.level + 1,\n          enableRightClickContext: widget.enableRightClickContext,\n          onSelected: widget.onSelected,\n          onTertiarySelected: widget.onTertiarySelected,\n          isDraggable: widget.isDraggable,\n          disableSelectedStatus: widget.disableSelectedStatus,\n          leftPadding: widget.leftPadding,\n          isFeedback: widget.isFeedback,\n          isPlaceholder: widget.isPlaceholder,\n          isHovered: widget.isHovered,\n          leftIconBuilder: widget.leftIconBuilder,\n          rightIconsBuilder: widget.rightIconsBuilder,\n          extendBuilder: widget.extendBuilder,\n          shouldIgnoreView: widget.shouldIgnoreView,\n          engagedInExpanding: widget.engagedInExpanding,\n        );\n      }).toList();\n\n      child = Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [child, ...children],\n      );\n    }\n\n    // wrap the child with DraggableItem if isDraggable is true\n    if ((widget.isDraggable || widget.isPlaceholder) &&\n        !isReferencedDatabaseView(widget.view, widget.parentView)) {\n      child = DraggableViewItem(\n        isFirstChild: widget.isFirstChild,\n        view: widget.view,\n        onDragging: (isDragging) => _isDragging = isDragging,\n        onMove: widget.isPlaceholder\n            ? (from, to) => moveViewCrossSpace(\n                  context,\n                  null,\n                  widget.view,\n                  widget.parentView,\n                  widget.spaceType,\n                  from,\n                  to.parentViewId,\n                )\n            : null,\n        feedback: (context) => Container(\n          width: 250,\n          decoration: BoxDecoration(\n            color: Brightness.light == Theme.of(context).brightness\n                ? Colors.white\n                : Colors.black54,\n            borderRadius: BorderRadius.circular(8),\n          ),\n          child: ViewItem(\n            view: widget.view,\n            parentView: widget.parentView,\n            spaceType: widget.spaceType,\n            level: widget.level,\n            onSelected: widget.onSelected,\n            onTertiarySelected: widget.onTertiarySelected,\n            isDraggable: false,\n            leftPadding: widget.leftPadding,\n            isFeedback: true,\n            enableRightClickContext: widget.enableRightClickContext,\n            leftIconBuilder: widget.leftIconBuilder,\n            rightIconsBuilder: widget.rightIconsBuilder,\n            extendBuilder: widget.extendBuilder,\n            shouldIgnoreView: widget.shouldIgnoreView,\n          ),\n        ),\n        child: child,\n      );\n    } else {\n      // keep the same height of the DraggableItem\n      child = Padding(\n        padding: const EdgeInsets.only(top: kDraggableViewItemDividerHeight),\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  void _collapseAllPages() {\n    if (widget.isExpandedNotifier?.value == true) {\n      context.read<ViewBloc>().add(const ViewEvent.collapseAllPages());\n    }\n  }\n}\n\nclass SingleInnerViewItem extends StatefulWidget {\n  const SingleInnerViewItem({\n    super.key,\n    required this.view,\n    required this.parentView,\n    required this.isExpanded,\n    required this.level,\n    required this.leftPadding,\n    this.isDraggable = true,\n    required this.spaceType,\n    required this.showActions,\n    this.enableRightClickContext = false,\n    required this.onSelected,\n    this.onTertiarySelected,\n    required this.isFeedback,\n    required this.height,\n    this.isHoverEnabled = true,\n    this.isPlaceholder = false,\n    this.isHovered,\n    required this.leftIconBuilder,\n    required this.rightIconsBuilder,\n    required this.extendBuilder,\n    required this.disableSelectedStatus,\n    required this.shouldIgnoreView,\n    required this.isSelected,\n  });\n\n  final ViewPB view;\n  final ViewPB? parentView;\n  final bool isExpanded;\n\n  // identify if the view item is rendered as feedback widget inside DraggableItem\n  final bool isFeedback;\n\n  final int level;\n  final double leftPadding;\n\n  final bool isDraggable;\n  final bool showActions;\n  final bool enableRightClickContext;\n  final ViewItemOnSelected onSelected;\n  final ViewItemOnSelected? onTertiarySelected;\n  final FolderSpaceType spaceType;\n  final double height;\n\n  final bool isHoverEnabled;\n  final bool isPlaceholder;\n  final bool? disableSelectedStatus;\n  final ValueNotifier<bool>? isHovered;\n  final ViewItemLeftIconBuilder? leftIconBuilder;\n  final ViewItemRightIconsBuilder? rightIconsBuilder;\n\n  final List<Widget> Function(ViewPB view)? extendBuilder;\n  final IgnoreViewType Function(ViewPB view)? shouldIgnoreView;\n  final bool isSelected;\n\n  @override\n  State<SingleInnerViewItem> createState() => _SingleInnerViewItemState();\n}\n\nclass _SingleInnerViewItemState extends State<SingleInnerViewItem> {\n  final controller = PopoverController();\n  final viewMoreActionController = PopoverController();\n\n  bool isIconPickerOpened = false;\n\n  DateTime? _lastClickTime;\n  static const _clickThrottleDuration = Duration(milliseconds: 200);\n\n  void _handleViewTap() {\n    final now = DateTime.now();\n\n    if (_lastClickTime != null) {\n      final timeSinceLastClick = now.difference(_lastClickTime!);\n      if (timeSinceLastClick < _clickThrottleDuration) {\n        return;\n      }\n    }\n\n    _lastClickTime = now;\n    widget.onSelected(context, widget.view);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    bool isSelected = widget.isSelected;\n\n    if (widget.disableSelectedStatus == true) {\n      isSelected = false;\n    }\n\n    if (widget.isPlaceholder) {\n      return const SizedBox(height: 4, width: double.infinity);\n    }\n\n    if (widget.isFeedback || !widget.isHoverEnabled) {\n      return _buildViewItem(\n        false,\n        !widget.isHoverEnabled ? isSelected : false,\n      );\n    }\n\n    return FlowyHover(\n      style: HoverStyle(hoverColor: Theme.of(context).colorScheme.secondary),\n      resetHoverOnRebuild: widget.showActions || !isIconPickerOpened,\n      buildWhenOnHover: () =>\n          !widget.showActions && !_isDragging && !isIconPickerOpened,\n      isSelected: () => widget.showActions || isSelected,\n      builder: (_, onHover) => _buildViewItem(onHover, isSelected),\n    );\n  }\n\n  Widget _buildViewItem(bool onHover, [bool isSelected = false]) {\n    final name = FlowyText.regular(\n      widget.view.nameOrDefault,\n      overflow: TextOverflow.ellipsis,\n      fontSize: 14.0,\n      figmaLineHeight: 18.0,\n    );\n    final children = [\n      const HSpace(2),\n      // expand icon or placeholder\n      widget.leftIconBuilder?.call(context, widget.view) ?? _buildLeftIcon(),\n      const HSpace(2),\n      // icon\n      _buildViewIconButton(),\n      const HSpace(6),\n      // title\n      Expanded(\n        child: widget.extendBuilder != null\n            ? Row(\n                children: [\n                  Flexible(child: name),\n                  ...widget.extendBuilder!(widget.view),\n                ],\n              )\n            : name,\n      ),\n    ];\n\n    // hover action\n    if (widget.showActions || onHover) {\n      if (widget.rightIconsBuilder != null) {\n        children.addAll(widget.rightIconsBuilder!(context, widget.view));\n      } else {\n        // ··· more action button\n        children.add(\n          _buildViewMoreActionButton(\n            context,\n            viewMoreActionController,\n            (_) => FlowyTooltip(\n              message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(),\n              child: FlowyIconButton(\n                width: 24,\n                icon: const FlowySvg(FlowySvgs.workspace_three_dots_s),\n                onPressed: viewMoreActionController.show,\n              ),\n            ),\n          ),\n        );\n        // only support add button for document layout\n        if (widget.view.layout == ViewLayoutPB.Document) {\n          // + button\n          children.add(const HSpace(8.0));\n          children.add(_buildViewAddButton(context));\n        }\n        children.add(const HSpace(4.0));\n      }\n    }\n\n    final child = GestureDetector(\n      behavior: HitTestBehavior.translucent,\n      onTap: _handleViewTap,\n      onTertiaryTapDown: (_) =>\n          widget.onTertiarySelected?.call(context, widget.view),\n      child: SizedBox(\n        height: widget.height,\n        child: Padding(\n          padding: EdgeInsets.only(left: widget.level * widget.leftPadding),\n          child: Listener(\n            onPointerDown: (event) {\n              if (event.buttons == kSecondaryMouseButton &&\n                  widget.enableRightClickContext) {\n                viewMoreActionController.showAt(\n                  // We add some horizontal offset\n                  event.position + const Offset(4, 0),\n                );\n              }\n            },\n            behavior: HitTestBehavior.opaque,\n            child: Row(children: children),\n          ),\n        ),\n      ),\n    );\n\n    if (isSelected) {\n      final popoverController = getIt<RenameViewBloc>().state.controller;\n      return AppFlowyPopover(\n        controller: popoverController,\n        triggerActions: PopoverTriggerFlags.none,\n        offset: const Offset(0, 5),\n        direction: PopoverDirection.bottomWithLeftAligned,\n        popupBuilder: (_) => RenameViewPopover(\n          view: widget.view,\n          name: widget.view.name,\n          emoji: widget.view.icon.toEmojiIconData(),\n          popoverController: popoverController,\n          showIconChanger: false,\n        ),\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  Widget _buildViewIconButton() {\n    final iconData = widget.view.icon.toEmojiIconData();\n    final icon = iconData.isNotEmpty\n        ? RawEmojiIconWidget(\n            emoji: iconData,\n            emojiSize: 16.0,\n            lineHeight: 18.0 / 16.0,\n          )\n        : Opacity(opacity: 0.6, child: widget.view.defaultIcon());\n\n    final Widget child = AppFlowyPopover(\n      offset: const Offset(20, 0),\n      controller: controller,\n      direction: PopoverDirection.rightWithCenterAligned,\n      constraints: BoxConstraints.loose(const Size(364, 356)),\n      margin: const EdgeInsets.all(0),\n      onClose: () => setState(() => isIconPickerOpened = false),\n      child: GestureDetector(\n        // prevent the tap event from being passed to the parent widget\n        onTap: () {},\n        child: FlowyTooltip(\n          message: LocaleKeys.document_plugins_cover_changeIcon.tr(),\n          child: SizedBox(width: 16.0, child: icon),\n        ),\n      ),\n      popupBuilder: (context) {\n        isIconPickerOpened = true;\n        return FlowyIconEmojiPicker(\n          initialType: iconData.type.toPickerTabType(),\n          tabs: const [\n            PickerTabType.emoji,\n            PickerTabType.icon,\n            PickerTabType.custom,\n          ],\n          documentId: widget.view.id,\n          onSelectedEmoji: (r) {\n            ViewBackendService.updateViewIcon(\n              view: widget.view,\n              viewIcon: r.data,\n            );\n            if (!r.keepOpen) controller.close();\n          },\n        );\n      },\n    );\n\n    if (widget.view.isLocked) {\n      return LockPageButtonWrapper(\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  // > button or · button\n  // show > if the view is expandable.\n  // show · if the view can't contain child views.\n  Widget _buildLeftIcon() {\n    return ViewItemDefaultLeftIcon(\n      view: widget.view,\n      parentView: widget.parentView,\n      isExpanded: widget.isExpanded,\n      leftPadding: widget.leftPadding,\n      isHovered: widget.isHovered,\n    );\n  }\n\n  // + button\n  Widget _buildViewAddButton(BuildContext context) {\n    return FlowyTooltip(\n      message: LocaleKeys.menuAppHeader_addPageTooltip.tr(),\n      child: ViewAddButton(\n        parentViewId: widget.view.id,\n        onEditing: (value) =>\n            context.read<ViewBloc>().add(ViewEvent.setIsEditing(value)),\n        onSelected: _onSelected,\n      ),\n    );\n  }\n\n  void _onSelected(\n    PluginBuilder pluginBuilder,\n    String? name,\n    List<int>? initialDataBytes,\n    bool openAfterCreated,\n    bool createNewView,\n  ) {\n    final viewBloc = context.read<ViewBloc>();\n\n    // the name of new document should be empty\n    final viewName = ![ViewLayoutPB.Document, ViewLayoutPB.Chat]\n            .contains(pluginBuilder.layoutType)\n        ? LocaleKeys.menuAppHeader_defaultNewPageName.tr()\n        : '';\n    viewBloc.add(\n      ViewEvent.createView(\n        viewName,\n        pluginBuilder.layoutType!,\n        openAfterCreated: openAfterCreated,\n        section: widget.spaceType.toViewSectionPB,\n      ),\n    );\n\n    viewBloc.add(const ViewEvent.setIsExpanded(true));\n  }\n\n  // ··· more action button\n  Widget _buildViewMoreActionButton(\n    BuildContext context,\n    PopoverController controller,\n    Widget Function(PopoverController) buildChild,\n  ) {\n    return BlocProvider(\n      create: (context) => SpaceBloc(\n        userProfile: context.read<SpaceBloc>().userProfile,\n        workspaceId: context.read<SpaceBloc>().workspaceId,\n      )..add(const SpaceEvent.initial(openFirstPage: false)),\n      child: ViewMoreActionPopover(\n        view: widget.view,\n        controller: controller,\n        isExpanded: widget.isExpanded,\n        spaceType: widget.spaceType,\n        onEditing: (value) =>\n            context.read<ViewBloc>().add(ViewEvent.setIsEditing(value)),\n        buildChild: buildChild,\n        onAction: (action, data) async {\n          switch (action) {\n            case ViewMoreActionType.favorite:\n            case ViewMoreActionType.unFavorite:\n              context\n                  .read<FavoriteBloc>()\n                  .add(FavoriteEvent.toggle(widget.view));\n              break;\n            case ViewMoreActionType.rename:\n              unawaited(\n                showAFTextFieldDialog(\n                  context: context,\n                  title: LocaleKeys.disclosureAction_rename.tr(),\n                  initialValue: widget.view.nameOrDefault,\n                  onConfirm: (newValue) {\n                    context.read<ViewBloc>().add(ViewEvent.rename(newValue));\n                  },\n                  maxLength: 256,\n                ),\n              );\n              break;\n            case ViewMoreActionType.delete:\n              // get if current page contains published child views\n              final (containPublishedPage, _) =\n                  await ViewBackendService.containPublishedPage(widget.view);\n              if (containPublishedPage && context.mounted) {\n                await showConfirmDeletionDialog(\n                  context: context,\n                  name: widget.view.name,\n                  description: LocaleKeys.publish_containsPublishedPage.tr(),\n                  onConfirm: () =>\n                      context.read<ViewBloc>().add(const ViewEvent.delete()),\n                );\n              } else if (context.mounted) {\n                context.read<ViewBloc>().add(const ViewEvent.delete());\n              }\n              break;\n            case ViewMoreActionType.duplicate:\n              context.read<ViewBloc>().add(const ViewEvent.duplicate());\n              break;\n            case ViewMoreActionType.openInNewTab:\n              context.read<TabsBloc>().openTab(widget.view);\n              break;\n            case ViewMoreActionType.collapseAllPages:\n              context.read<ViewBloc>().add(const ViewEvent.collapseAllPages());\n              break;\n            case ViewMoreActionType.changeIcon:\n              if (data is! SelectedEmojiIconResult) {\n                return;\n              }\n              await ViewBackendService.updateViewIcon(\n                view: widget.view,\n                viewIcon: data.data,\n              );\n              break;\n            case ViewMoreActionType.moveTo:\n              final value = data;\n              if (value is! (ViewPB, ViewPB)) {\n                return;\n              }\n              final space = value.$1;\n              final target = value.$2;\n              moveViewCrossSpace(\n                context,\n                space,\n                widget.view,\n                widget.parentView,\n                widget.spaceType,\n                widget.view,\n                target.id,\n              );\n            default:\n              throw UnsupportedError('$action is not supported');\n          }\n        },\n      ),\n    );\n  }\n}\n\nclass _DotIconWidget extends StatelessWidget {\n  const _DotIconWidget();\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.all(6.0),\n      child: Container(\n        width: 4,\n        height: 4,\n        decoration: BoxDecoration(\n          color: Theme.of(context).iconTheme.color,\n          borderRadius: BorderRadius.circular(2),\n        ),\n      ),\n    );\n  }\n}\n\n// 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.\nbool isReferencedDatabaseView(ViewPB view, ViewPB? parentView) {\n  if (parentView == null) {\n    return false;\n  }\n  return view.layout.isDatabaseView && parentView.layout.isDatabaseView;\n}\n\nvoid moveViewCrossSpace(\n  BuildContext context,\n  ViewPB? toSpace,\n  ViewPB view,\n  ViewPB? parentView,\n  FolderSpaceType spaceType,\n  ViewPB from,\n  String toId,\n) {\n  if (isReferencedDatabaseView(view, parentView)) {\n    return;\n  }\n\n  if (from.id == toId) {\n    return;\n  }\n\n  final currentSpace = context.read<SpaceBloc>().state.currentSpace;\n  if (currentSpace != null &&\n      toSpace != null &&\n      currentSpace.id != toSpace.id) {\n    Log.info(\n      'Move view(${from.name}) to another space(${toSpace.name}), unpublish the view',\n    );\n    context.read<ViewBloc>().add(const ViewEvent.unpublish(sync: false));\n\n    switchToSpaceNotifier.value = toSpace;\n  }\n\n  context.read<ViewBloc>().add(ViewEvent.move(from, toId, null, null, null));\n}\n\nclass ViewItemDefaultLeftIcon extends StatelessWidget {\n  const ViewItemDefaultLeftIcon({\n    super.key,\n    required this.view,\n    required this.parentView,\n    required this.isExpanded,\n    required this.leftPadding,\n    required this.isHovered,\n  });\n\n  final ViewPB view;\n  final ViewPB? parentView;\n  final bool isExpanded;\n  final double leftPadding;\n  final ValueNotifier<bool>? isHovered;\n\n  @override\n  Widget build(BuildContext context) {\n    if (isReferencedDatabaseView(view, parentView)) {\n      return const _DotIconWidget();\n    }\n\n    if (context.read<ViewBloc>().state.view.childViews.isEmpty) {\n      return HSpace(leftPadding);\n    }\n\n    final child = FlowyHover(\n      child: GestureDetector(\n        child: FlowySvg(\n          isExpanded\n              ? FlowySvgs.view_item_expand_s\n              : FlowySvgs.view_item_unexpand_s,\n          size: const Size.square(16.0),\n        ),\n        onTap: () =>\n            context.read<ViewBloc>().add(ViewEvent.setIsExpanded(!isExpanded)),\n      ),\n    );\n\n    if (isHovered != null) {\n      return ValueListenableBuilder<bool>(\n        valueListenable: isHovered!,\n        builder: (_, isHovered, child) =>\n            Opacity(opacity: isHovered ? 1.0 : 0.0, child: child),\n        child: child,\n      );\n    }\n\n    return child;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/move_to/move_page_menu.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';\nimport 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\n/// ··· button beside the view name\nclass ViewMoreActionPopover extends StatelessWidget {\n  const ViewMoreActionPopover({\n    super.key,\n    required this.view,\n    this.controller,\n    required this.onEditing,\n    required this.onAction,\n    required this.spaceType,\n    required this.isExpanded,\n    required this.buildChild,\n    this.showAtCursor = false,\n  });\n\n  final ViewPB view;\n  final PopoverController? controller;\n  final void Function(bool value) onEditing;\n  final void Function(ViewMoreActionType type, dynamic data) onAction;\n  final FolderSpaceType spaceType;\n  final bool isExpanded;\n  final Widget Function(PopoverController) buildChild;\n  final bool showAtCursor;\n\n  @override\n  Widget build(BuildContext context) {\n    final wrappers = _buildActionTypeWrappers();\n    return PopoverActionList<ViewMoreActionTypeWrapper>(\n      controller: controller,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      offset: const Offset(0, 8),\n      actions: wrappers,\n      constraints: const BoxConstraints(minWidth: 260),\n      onPopupBuilder: () => onEditing(true),\n      buildChild: buildChild,\n      onSelected: (_, __) {},\n      onClosed: () => onEditing(false),\n      showAtCursor: showAtCursor,\n    );\n  }\n\n  List<ViewMoreActionTypeWrapper> _buildActionTypeWrappers() {\n    final actionTypes = _buildActionTypes();\n    return actionTypes.map(\n      (e) {\n        final actionWrapper =\n            ViewMoreActionTypeWrapper(e, view, (controller, data) {\n          onEditing(false);\n          onAction(e, data);\n          bool enableClose = true;\n          if (data is SelectedEmojiIconResult) {\n            if (data.keepOpen) enableClose = false;\n          }\n          if (enableClose) controller.close();\n        });\n\n        return actionWrapper;\n      },\n    ).toList();\n  }\n\n  List<ViewMoreActionType> _buildActionTypes() {\n    final List<ViewMoreActionType> actionTypes = [];\n\n    if (spaceType == FolderSpaceType.favorite) {\n      actionTypes.addAll([\n        ViewMoreActionType.unFavorite,\n        ViewMoreActionType.divider,\n        ViewMoreActionType.rename,\n        ViewMoreActionType.openInNewTab,\n      ]);\n    } else {\n      actionTypes.add(\n        view.isFavorite\n            ? ViewMoreActionType.unFavorite\n            : ViewMoreActionType.favorite,\n      );\n\n      actionTypes.addAll([\n        ViewMoreActionType.divider,\n        ViewMoreActionType.rename,\n      ]);\n\n      // Chat doesn't change icon and duplicate\n      if (view.layout != ViewLayoutPB.Chat) {\n        actionTypes.addAll([\n          ViewMoreActionType.changeIcon,\n          ViewMoreActionType.duplicate,\n        ]);\n      }\n\n      actionTypes.addAll([\n        ViewMoreActionType.moveTo,\n        ViewMoreActionType.delete,\n        ViewMoreActionType.divider,\n      ]);\n\n      // Chat doesn't change collapse\n      // Only show collapse all pages if the view has child views\n      if (view.layout != ViewLayoutPB.Chat &&\n          view.childViews.isNotEmpty &&\n          isExpanded) {\n        actionTypes.add(ViewMoreActionType.collapseAllPages);\n        actionTypes.add(ViewMoreActionType.divider);\n      }\n\n      actionTypes.add(ViewMoreActionType.openInNewTab);\n    }\n\n    return actionTypes;\n  }\n}\n\nclass ViewMoreActionTypeWrapper extends CustomActionCell {\n  ViewMoreActionTypeWrapper(\n    this.inner,\n    this.sourceView,\n    this.onTap, {\n    this.moveActionDirection,\n    this.moveActionOffset,\n  });\n\n  final ViewMoreActionType inner;\n  final ViewPB sourceView;\n  final void Function(PopoverController controller, dynamic data) onTap;\n\n  // custom the move to action button\n  final PopoverDirection? moveActionDirection;\n  final Offset? moveActionOffset;\n\n  @override\n  Widget buildWithContext(\n    BuildContext context,\n    PopoverController controller,\n    PopoverMutex? mutex,\n  ) {\n    Widget child;\n\n    if (inner == ViewMoreActionType.divider) {\n      child = _buildDivider();\n    } else if (inner == ViewMoreActionType.lastModified) {\n      child = _buildLastModified(context);\n    } else if (inner == ViewMoreActionType.created) {\n      child = _buildCreated(context);\n    } else if (inner == ViewMoreActionType.changeIcon) {\n      child = _buildEmojiActionButton(context, controller);\n    } else if (inner == ViewMoreActionType.moveTo) {\n      child = _buildMoveToActionButton(context, controller);\n    } else {\n      child = _buildNormalActionButton(context, controller);\n    }\n\n    if (ViewMoreActionType.disableInLockedView.contains(inner) &&\n        sourceView.isLocked) {\n      child = LockPageButtonWrapper(\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  Widget _buildNormalActionButton(\n    BuildContext context,\n    PopoverController controller,\n  ) {\n    return _buildActionButton(context, () => onTap(controller, null));\n  }\n\n  Widget _buildEmojiActionButton(\n    BuildContext context,\n    PopoverController controller,\n  ) {\n    final child = _buildActionButton(context, null);\n\n    return AppFlowyPopover(\n      constraints: BoxConstraints.loose(const Size(364, 356)),\n      margin: const EdgeInsets.all(0),\n      clickHandler: PopoverClickHandler.gestureDetector,\n      popupBuilder: (_) => FlowyIconEmojiPicker(\n        tabs: const [\n          PickerTabType.emoji,\n          PickerTabType.icon,\n          PickerTabType.custom,\n        ],\n        documentId: sourceView.id,\n        initialType: sourceView.icon.toEmojiIconData().type.toPickerTabType(),\n        onSelectedEmoji: (result) => onTap(controller, result),\n      ),\n      child: child,\n    );\n  }\n\n  Widget _buildMoveToActionButton(\n    BuildContext context,\n    PopoverController controller,\n  ) {\n    final userProfile = context.read<SpaceBloc>().userProfile;\n    // move to feature doesn't support in local mode\n    if (userProfile.workspaceType != WorkspaceTypePB.ServerW) {\n      return const SizedBox.shrink();\n    }\n    return BlocProvider.value(\n      value: context.read<SpaceBloc>(),\n      child: BlocBuilder<SpaceBloc, SpaceState>(\n        builder: (context, state) {\n          final child = _buildActionButton(context, null);\n          return AppFlowyPopover(\n            constraints: const BoxConstraints(\n              maxWidth: 260,\n              maxHeight: 345,\n            ),\n            margin: const EdgeInsets.symmetric(\n              horizontal: 14.0,\n              vertical: 12.0,\n            ),\n            clickHandler: PopoverClickHandler.gestureDetector,\n            direction:\n                moveActionDirection ?? PopoverDirection.rightWithTopAligned,\n            offset: moveActionOffset,\n            popupBuilder: (_) {\n              return BlocProvider.value(\n                value: context.read<SpaceBloc>(),\n                child: MovePageMenu(\n                  sourceView: sourceView,\n                  onSelected: (space, view) {\n                    onTap(controller, (space, view));\n                  },\n                ),\n              );\n            },\n            child: child,\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildDivider() {\n    return const Padding(\n      padding: EdgeInsets.all(8.0),\n      child: FlowyDivider(),\n    );\n  }\n\n  Widget _buildLastModified(BuildContext context) {\n    return Container(\n      height: 40,\n      width: double.infinity,\n      padding: const EdgeInsets.symmetric(horizontal: 8.0),\n      child: const Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n      ),\n    );\n  }\n\n  Widget _buildCreated(BuildContext context) {\n    return Container(\n      height: 40,\n      width: double.infinity,\n      padding: const EdgeInsets.symmetric(horizontal: 8.0),\n      child: const Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n      ),\n    );\n  }\n\n  Widget _buildActionButton(\n    BuildContext context,\n    VoidCallback? onTap,\n  ) {\n    return Container(\n      height: 34,\n      padding: const EdgeInsets.symmetric(vertical: 2.0),\n      child: FlowyIconTextButton(\n        margin: const EdgeInsets.symmetric(horizontal: 6),\n        onTap: onTap,\n        // show the error color when delete is hovered\n        leftIconBuilder: (onHover) => FlowySvg(\n          inner.leftIconSvg,\n          color: inner == ViewMoreActionType.delete && onHover\n              ? Theme.of(context).colorScheme.error\n              : null,\n        ),\n        rightIconBuilder: (_) => inner.rightIcon,\n        iconPadding: 10.0,\n        textBuilder: (onHover) => FlowyText.regular(\n          inner.name,\n          fontSize: 14.0,\n          lineHeight: 1.0,\n          figmaLineHeight: 18.0,\n          color: inner == ViewMoreActionType.delete && onHover\n              ? Theme.of(context).colorScheme.error\n              : null,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/home/home_setting_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_stack.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/style_widget/icon_button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\nimport 'package:styled_widget/styled_widget.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport '../notifications/number_red_dot.dart';\n\nclass NavigationNotifier with ChangeNotifier {\n  NavigationNotifier({required this.navigationItems});\n\n  List<NavigationItem> navigationItems;\n\n  void update(PageNotifier notifier) {\n    if (navigationItems != notifier.plugin.widgetBuilder.navigationItems) {\n      navigationItems = notifier.plugin.widgetBuilder.navigationItems;\n      notifyListeners();\n    }\n  }\n}\n\nclass FlowyNavigation extends StatelessWidget {\n  const FlowyNavigation({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ChangeNotifierProxyProvider<PageNotifier, NavigationNotifier>(\n      create: (_) {\n        final notifier = Provider.of<PageNotifier>(context, listen: false);\n        return NavigationNotifier(\n          navigationItems: notifier.plugin.widgetBuilder.navigationItems,\n        );\n      },\n      update: (_, notifier, controller) => controller!..update(notifier),\n      child: Expanded(\n        child: Row(\n          children: [\n            _renderCollapse(context),\n            Selector<NavigationNotifier, List<NavigationItem>>(\n              selector: (context, notifier) => notifier.navigationItems,\n              builder: (ctx, items, child) => Expanded(\n                child: Row(\n                  children: _renderNavigationItems(items),\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _renderCollapse(BuildContext context) {\n    return BlocBuilder<HomeSettingBloc, HomeSettingState>(\n      buildWhen: (p, c) => p.menuStatus != c.menuStatus,\n      builder: (context, state) {\n        if (!UniversalPlatform.isWindows &&\n            state.menuStatus == MenuStatus.hidden) {\n          final textSpan = TextSpan(\n            children: [\n              TextSpan(\n                text: '${LocaleKeys.sideBar_openSidebar.tr()}\\n',\n                style: context.tooltipTextStyle(),\n              ),\n              TextSpan(\n                text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\\\',\n                style: context\n                    .tooltipTextStyle()\n                    ?.copyWith(color: Theme.of(context).hintColor),\n              ),\n            ],\n          );\n          final theme = AppFlowyTheme.of(context);\n          return Padding(\n            padding: const EdgeInsets.only(right: 8.0),\n            child: SizedBox(\n              width: 24,\n              height: 24,\n              child: Stack(\n                children: [\n                  RotationTransition(\n                    turns: const AlwaysStoppedAnimation(180 / 360),\n                    child: FlowyTooltip(\n                      richMessage: textSpan,\n                      child: Listener(\n                        onPointerDown: (event) =>\n                            context.read<HomeSettingBloc>().collapseMenu(),\n                        child: SizedBox(\n                          width: 24,\n                          height: 24,\n                          child: FlowyIconButton(\n                            width: 24,\n                            onPressed: () {},\n                            icon: FlowySvg(\n                              FlowySvgs.double_back_arrow_m,\n                              color: theme.iconColorScheme.secondary,\n                            ),\n                          ),\n                        ),\n                      ),\n                    ),\n                  ),\n                  Align(\n                    alignment: Alignment.topRight,\n                    child: NumberedRedDot.desktop(),\n                  ),\n                ],\n              ),\n            ),\n          );\n        }\n\n        return const SizedBox.shrink();\n      },\n    );\n  }\n\n  List<Widget> _renderNavigationItems(List<NavigationItem> items) {\n    if (items.isEmpty) {\n      return [];\n    }\n\n    final List<NavigationItem> newItems = _filter(items);\n    final Widget last = NaviItemWidget(newItems.removeLast());\n\n    final List<Widget> widgets = List.empty(growable: true);\n    // widgets.addAll(newItems.map((item) => NaviItemDivider(child: NaviItemWidget(item))).toList());\n\n    for (final item in newItems) {\n      widgets.add(NaviItemWidget(item));\n      widgets.add(const Text('/'));\n    }\n\n    widgets.add(last);\n\n    return widgets;\n  }\n\n  List<NavigationItem> _filter(List<NavigationItem> items) {\n    final length = items.length;\n    if (length > 4) {\n      final first = items[0];\n      final ellipsisItems = items.getRange(1, length - 2).toList();\n      final last = items.getRange(length - 2, length).toList();\n      return [\n        first,\n        EllipsisNaviItem(items: ellipsisItems),\n        ...last,\n      ];\n    } else {\n      return items;\n    }\n  }\n}\n\nclass NaviItemWidget extends StatelessWidget {\n  const NaviItemWidget(this.item, {super.key});\n\n  final NavigationItem item;\n\n  @override\n  Widget build(BuildContext context) {\n    return Expanded(\n      child: item.leftBarItem.padding(horizontal: 2, vertical: 2),\n    );\n  }\n}\n\nclass EllipsisNaviItem extends NavigationItem {\n  EllipsisNaviItem({required this.items});\n\n  final List<NavigationItem> items;\n\n  @override\n  String? get viewName => null;\n\n  @override\n  Widget get leftBarItem => FlowyText.medium('...', fontSize: FontSizes.s16);\n\n  @override\n  Widget tabBarItem(String pluginId, [bool shortForm = false]) => leftBarItem;\n\n  @override\n  NavigationCallback get action => (id) {};\n}\n\nTextSpan sidebarTooltipTextSpan(BuildContext context, String hintText) =>\n    TextSpan(\n      children: [\n        TextSpan(\n          text: \"$hintText\\n\",\n        ),\n        TextSpan(\n          text: Platform.isMacOS ? \"⌘+.\" : \"Ctrl+\\\\\",\n        ),\n      ],\n    );\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy/workspace/presentation/home/home_stack.dart';\n\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:provider/provider.dart';\n\nclass FlowyTab extends StatefulWidget {\n  const FlowyTab({\n    super.key,\n    required this.pageManager,\n    required this.isCurrent,\n    required this.onTap,\n    required this.isAllPinned,\n  });\n\n  final PageManager pageManager;\n  final bool isCurrent;\n  final VoidCallback onTap;\n\n  /// Signifies whether all tabs are pinned\n  ///\n  final bool isAllPinned;\n\n  @override\n  State<FlowyTab> createState() => _FlowyTabState();\n}\n\nclass _FlowyTabState extends State<FlowyTab> {\n  final controller = PopoverController();\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: widget.pageManager.isPinned ? 54 : null,\n      child: _wrapInTooltip(\n        widget.pageManager.plugin.widgetBuilder.viewName,\n        child: FlowyHover(\n          resetHoverOnRebuild: false,\n          style: HoverStyle(\n            borderRadius: BorderRadius.zero,\n            backgroundColor: widget.isCurrent\n                ? Theme.of(context).colorScheme.surface\n                : Theme.of(context).colorScheme.surfaceContainerHighest,\n            hoverColor:\n                widget.isCurrent ? Theme.of(context).colorScheme.surface : null,\n          ),\n          builder: (context, isHovering) => AppFlowyPopover(\n            controller: controller,\n            offset: const Offset(4, 4),\n            triggerActions: PopoverTriggerFlags.secondaryClick,\n            showAtCursor: true,\n            popupBuilder: (_) => BlocProvider.value(\n              value: context.read<TabsBloc>(),\n              child: TabMenu(\n                controller: controller,\n                pageId: widget.pageManager.plugin.id,\n                isPinned: widget.pageManager.isPinned,\n                isAllPinned: widget.isAllPinned,\n              ),\n            ),\n            child: ChangeNotifierProvider.value(\n              value: widget.pageManager.notifier,\n              child: Consumer<PageNotifier>(\n                builder: (context, value, _) => Padding(\n                  padding: const EdgeInsets.symmetric(horizontal: 16.0),\n                  // We use a Listener to avoid gesture detector onPanStart debounce\n                  child: Listener(\n                    onPointerDown: (event) {\n                      if (event.buttons == kPrimaryButton) {\n                        widget.onTap();\n                      }\n                    },\n                    child: GestureDetector(\n                      behavior: HitTestBehavior.opaque,\n                      // Stop move window detector\n                      onPanStart: (_) {},\n                      child: Container(\n                        constraints: BoxConstraints(\n                          maxWidth: HomeSizes.tabBarWidth,\n                          minWidth: widget.pageManager.isPinned ? 54 : 100,\n                        ),\n                        height: HomeSizes.tabBarHeight,\n                        child: Row(\n                          mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                          children: [\n                            Flexible(\n                              child: widget.pageManager.notifier.tabBarWidget(\n                                widget.pageManager.plugin.id,\n                                widget.pageManager.isPinned,\n                              ),\n                            ),\n                            if (!widget.pageManager.isPinned) ...[\n                              Visibility(\n                                visible: isHovering,\n                                child: SizedBox(\n                                  width: 26,\n                                  height: 26,\n                                  child: FlowyIconButton(\n                                    onPressed: () => _closeTab(context),\n                                    icon: const FlowySvg(\n                                      FlowySvgs.close_s,\n                                      size: Size.square(22),\n                                    ),\n                                  ),\n                                ),\n                              ),\n                            ],\n                          ],\n                        ),\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  void _closeTab(BuildContext context) => context\n      .read<TabsBloc>()\n      .add(TabsEvent.closeTab(widget.pageManager.plugin.id));\n\n  Widget _wrapInTooltip(String? viewName, {required Widget child}) {\n    if (viewName != null) {\n      return FlowyTooltip(\n        message: viewName,\n        child: child,\n      );\n    }\n\n    return child;\n  }\n}\n\n@visibleForTesting\nclass TabMenu extends StatelessWidget {\n  const TabMenu({\n    super.key,\n    required this.controller,\n    required this.pageId,\n    required this.isPinned,\n    required this.isAllPinned,\n  });\n\n  final PopoverController controller;\n  final String pageId;\n  final bool isPinned;\n  final bool isAllPinned;\n\n  @override\n  Widget build(BuildContext context) {\n    return SeparatedColumn(\n      separatorBuilder: () => const VSpace(4),\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        Opacity(\n          opacity: isPinned ? 0.5 : 1,\n          child: _wrapInTooltip(\n            shouldWrap: isPinned,\n            message: LocaleKeys.tabMenu_closeDisabledHint.tr(),\n            child: FlowyButton(\n              text: FlowyText.regular(LocaleKeys.tabMenu_close.tr()),\n              onTap: () => _closeTab(context),\n              disable: isPinned,\n            ),\n          ),\n        ),\n        Opacity(\n          opacity: isAllPinned ? 0.5 : 1,\n          child: _wrapInTooltip(\n            shouldWrap: true,\n            message: isAllPinned\n                ? LocaleKeys.tabMenu_closeOthersDisabledHint.tr()\n                : LocaleKeys.tabMenu_closeOthersHint.tr(),\n            child: FlowyButton(\n              text: FlowyText.regular(\n                LocaleKeys.tabMenu_closeOthers.tr(),\n              ),\n              onTap: () => _closeOtherTabs(context),\n              disable: isAllPinned,\n            ),\n          ),\n        ),\n        const Divider(height: 0.5),\n        FlowyButton(\n          text: FlowyText.regular(\n            isPinned\n                ? LocaleKeys.tabMenu_unpinTab.tr()\n                : LocaleKeys.tabMenu_pinTab.tr(),\n          ),\n          onTap: () => _togglePin(context),\n        ),\n      ],\n    );\n  }\n\n  Widget _wrapInTooltip({\n    required bool shouldWrap,\n    String? message,\n    required Widget child,\n  }) {\n    if (shouldWrap) {\n      return FlowyTooltip(\n        message: message,\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  void _closeTab(BuildContext context) {\n    context.read<TabsBloc>().add(TabsEvent.closeTab(pageId));\n    controller.close();\n  }\n\n  void _closeOtherTabs(BuildContext context) {\n    context.read<TabsBloc>().add(TabsEvent.closeOtherTabs(pageId));\n    controller.close();\n  }\n\n  void _togglePin(BuildContext context) {\n    context.read<TabsBloc>().add(TabsEvent.togglePin(pageId));\n    controller.close();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart",
    "content": "import 'package:appflowy/core/frameless_window.dart';\nimport 'package:flutter/material.dart';\n\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass TabsManager extends StatelessWidget {\n  const TabsManager({super.key, required this.onIndexChanged});\n\n  final void Function(int) onIndexChanged;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocConsumer<TabsBloc, TabsState>(\n      listenWhen: (prev, curr) =>\n          prev.currentIndex != curr.currentIndex || prev.pages != curr.pages,\n      listener: (context, state) => onIndexChanged(state.currentIndex),\n      builder: (context, state) {\n        if (state.pages == 1) {\n          return const SizedBox.shrink();\n        }\n\n        final isAllPinned = state.isAllPinned;\n\n        return Container(\n          alignment: Alignment.bottomLeft,\n          height: HomeSizes.tabBarHeight,\n          decoration: BoxDecoration(\n            color: Theme.of(context).colorScheme.surfaceContainerHighest,\n          ),\n          child: MoveWindowDetector(\n            child: Row(\n              children: state.pageManagers.map((pm) {\n                return Flexible(\n                  child: ConstrainedBox(\n                    constraints: const BoxConstraints(\n                      maxWidth: HomeSizes.tabBarWidth,\n                    ),\n                    child: FlowyTab(\n                      key: ValueKey('tab-${pm.plugin.id}'),\n                      pageManager: pm,\n                      isCurrent: state.currentPageManager == pm,\n                      isAllPinned: isAllPinned,\n                      onTap: () {\n                        if (state.currentPageManager != pm) {\n                          final index = state.pageManagers.indexOf(pm);\n                          onIndexChanged(index);\n                        }\n                      },\n                    ),\n                  ),\n                );\n              }).toList(),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\nimport 'package:fluttertoast/fluttertoast.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass FlowyMessageToast extends StatelessWidget {\n  const FlowyMessageToast({required this.message, super.key});\n\n  final String message;\n\n  @override\n  Widget build(BuildContext context) {\n    return DecoratedBox(\n      decoration: BoxDecoration(\n        borderRadius: const BorderRadius.all(Radius.circular(4)),\n        color: Theme.of(context).colorScheme.surface,\n      ),\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),\n        child: FlowyText.medium(\n          message,\n          fontSize: FontSizes.s16,\n          maxLines: 3,\n        ),\n      ),\n    );\n  }\n}\n\nvoid initToastWithContext(BuildContext context) {\n  getIt<FToast>().init(context);\n}\n\nvoid showMessageToast(\n  String message, {\n  BuildContext? context,\n  ToastGravity gravity = ToastGravity.BOTTOM,\n}) {\n  final child = FlowyMessageToast(message: message);\n  final toast = context == null ? getIt<FToast>() : (FToast()..init(context));\n  toast.showToast(\n    child: child,\n    gravity: gravity,\n    toastDuration: const Duration(seconds: 3),\n  );\n}\n\nvoid showSnackBarMessage(\n  BuildContext context,\n  String message, {\n  bool showCancel = false,\n  Duration duration = const Duration(seconds: 4),\n}) {\n  ScaffoldMessenger.of(context).showSnackBar(\n    SnackBar(\n      backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,\n      duration: duration,\n      action: !showCancel\n          ? null\n          : SnackBarAction(\n              label: LocaleKeys.button_cancel.tr(),\n              textColor: Colors.white,\n              onPressed: () {\n                ScaffoldMessenger.of(context).hideCurrentSnackBar();\n              },\n            ),\n      content: FlowyText(\n        message,\n        maxLines: 2,\n        fontSize: UniversalPlatform.isDesktop ? 14 : 12,\n      ),\n    ),\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_panel.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/workspace/application/home/home_setting_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'widgets/notification_tab.dart';\nimport 'widgets/notification_tab_bar.dart';\n\nclass NotificationPanel extends StatefulWidget {\n  const NotificationPanel({super.key});\n\n  @override\n  State<NotificationPanel> createState() => _NotificationPanelState();\n}\n\nclass _NotificationPanelState extends State<NotificationPanel>\n    with SingleTickerProviderStateMixin {\n  late TabController tabController;\n  final PopoverController moreActionController = PopoverController();\n\n  final tabs = [\n    NotificationTabType.inbox,\n    NotificationTabType.unread,\n    NotificationTabType.archive,\n  ];\n\n  @override\n  void initState() {\n    super.initState();\n    tabController = TabController(length: 3, vsync: this);\n  }\n\n  @override\n  void dispose() {\n    tabController.dispose();\n    moreActionController.close();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final settingBloc = context.read<HomeSettingBloc>();\n    final theme = AppFlowyTheme.of(context);\n    return GestureDetector(\n      onTap: () =>\n          settingBloc.add(HomeSettingEvent.collapseNotificationPanel()),\n      child: Container(\n        color: Colors.transparent,\n        child: Align(\n          alignment: Alignment.centerLeft,\n          child: GestureDetector(\n            onTap: () {},\n            child: Container(\n              width: 380,\n              decoration: BoxDecoration(\n                color: theme.backgroundColorScheme.primary,\n                boxShadow: theme.shadow.small,\n              ),\n              padding: EdgeInsets.symmetric(vertical: 14),\n              child: Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  buildTitle(\n                    context: context,\n                    onHide: () => settingBloc\n                        .add(HomeSettingEvent.collapseNotificationPanel()),\n                  ),\n                  const VSpace(12),\n                  NotificationTabBar(\n                    tabController: tabController,\n                    tabs: tabs,\n                  ),\n                  const VSpace(14),\n                  Expanded(\n                    child: TabBarView(\n                      controller: tabController,\n                      children:\n                          tabs.map((e) => NotificationTab(tabType: e)).toList(),\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget buildTitle({\n    required BuildContext context,\n    required VoidCallback onHide,\n  }) {\n    final theme = AppFlowyTheme.of(context);\n    return Container(\n      padding: const EdgeInsets.symmetric(horizontal: 16),\n      height: 24,\n      child: Row(\n        children: [\n          FlowyText.medium(\n            LocaleKeys.notificationHub_title.tr(),\n            fontSize: 16,\n            figmaLineHeight: 24,\n          ),\n          Spacer(),\n          FlowyIconButton(\n            width: 24,\n            icon: FlowySvg(\n              FlowySvgs.double_back_arrow_m,\n              color: theme.iconColorScheme.secondary,\n            ),\n            richTooltipText: colappsedButtonTooltip(context),\n            onPressed: onHide,\n          ),\n          HSpace(8),\n          buildMoreActionButton(context),\n        ],\n      ),\n    );\n  }\n\n  Widget buildMoreActionButton(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return AppFlowyPopover(\n      constraints: BoxConstraints.loose(const Size(240, 78)),\n      offset: const Offset(-24, 24),\n      margin: EdgeInsets.zero,\n      controller: moreActionController,\n      onOpen: () => keepEditorFocusNotifier.increase(),\n      onClose: () => keepEditorFocusNotifier.decrease(),\n      popupBuilder: (_) => buildMoreActions(),\n      child: FlowyIconButton(\n        width: 24,\n        icon: FlowySvg(\n          FlowySvgs.three_dots_m,\n          color: theme.iconColorScheme.secondary,\n        ),\n        onPressed: () {\n          keepEditorFocusNotifier.increase();\n          moreActionController.show();\n        },\n      ),\n    );\n  }\n\n  Widget buildMoreActions() {\n    return Container(\n      padding: EdgeInsets.all(8),\n      decoration: BoxDecoration(\n        color: Theme.of(context).cardColor,\n        borderRadius: BorderRadius.all(Radius.circular(8)),\n        boxShadow: [\n          BoxShadow(\n            offset: Offset(0, 4),\n            blurRadius: 24,\n            color: Color(0x0000001F),\n          ),\n        ],\n      ),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          SizedBox(\n            height: 30,\n            child: FlowyButton(\n              text: FlowyText.regular(\n                LocaleKeys.settings_notifications_settings_markAllAsRead.tr(),\n              ),\n              leftIcon: FlowySvg(FlowySvgs.m_notification_mark_as_read_s),\n              onTap: () {\n                showToastNotification(\n                  message:\n                      LocaleKeys.notificationHub_markAllAsReadSucceedToast.tr(),\n                );\n                context\n                    .read<ReminderBloc>()\n                    .add(const ReminderEvent.markAllRead());\n                moreActionController.close();\n              },\n            ),\n          ),\n          VSpace(2),\n          SizedBox(\n            height: 30,\n            child: FlowyButton(\n              text: FlowyText.regular(\n                LocaleKeys.settings_notifications_settings_archiveAll.tr(),\n              ),\n              leftIcon: FlowySvg(FlowySvgs.m_notification_archived_s),\n              onTap: () {\n                showToastNotification(\n                  message: LocaleKeys\n                      .notificationHub_markAllAsArchivedSucceedToast\n                      .tr(),\n                );\n                context\n                    .read<ReminderBloc>()\n                    .add(const ReminderEvent.archiveAll());\n                moreActionController.close();\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  TextSpan colappsedButtonTooltip(BuildContext context) {\n    return TextSpan(\n      children: [\n        TextSpan(\n          text: '${LocaleKeys.notificationHub_closeNotification.tr()}\\n',\n          style: context.tooltipTextStyle(),\n        ),\n        TextSpan(\n          text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\\\',\n          style: context\n              .tooltipTextStyle()\n              ?.copyWith(color: Theme.of(context).hintColor),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/notifications/number_red_dot.dart",
    "content": "import 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass NumberedRedDot extends StatelessWidget {\n  const NumberedRedDot.desktop({\n    super.key,\n    this.fontSize = 10,\n  }) : size = const NumberedSize(\n          min: Size.square(14),\n          middle: Size(17, 14),\n          max: Size(24, 14),\n        );\n\n  const NumberedRedDot.mobile({\n    super.key,\n    this.fontSize = 14,\n  }) : size = const NumberedSize(\n          min: Size.square(20),\n          middle: Size(26, 20),\n          max: Size(35, 20),\n        );\n\n  const NumberedRedDot({\n    super.key,\n    required this.size,\n    required this.fontSize,\n  });\n  final NumberedSize size;\n  final double fontSize;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return BlocBuilder<ReminderBloc, ReminderState>(\n      builder: (context, state) {\n        int unreadReminder = 0;\n        for (final reminder in state.reminders) {\n          if (!reminder.isRead) unreadReminder++;\n        }\n        if (unreadReminder == 0) return SizedBox.shrink();\n        final overNumber = unreadReminder > 99;\n        Size size = this.size.min;\n        if (unreadReminder >= 10 && unreadReminder <= 99) {\n          size = this.size.middle;\n        } else if (unreadReminder > 99) {\n          size = this.size.max;\n        }\n        return Container(\n          width: size.width,\n          height: size.height,\n          decoration: BoxDecoration(\n            color: theme.borderColorScheme.errorThick,\n            borderRadius: BorderRadius.all(Radius.circular(size.height / 2)),\n          ),\n          child: Center(\n            child: Text(\n              overNumber ? '99+' : '$unreadReminder',\n              textAlign: TextAlign.center,\n              style: TextStyle(\n                fontWeight: FontWeight.w500,\n                color: Colors.white,\n                fontSize: fontSize,\n                height: 1,\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass NumberedSize {\n  const NumberedSize({\n    required this.min,\n    required this.middle,\n    required this.max,\n  });\n\n  final Size min;\n  final Size middle;\n  final Size max;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/notifications/reminder_extension.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';\nimport 'package:collection/collection.dart';\n\nextension ReminderSort on Iterable<ReminderPB> {\n  List<ReminderPB> sortByScheduledAt() =>\n      sorted((a, b) => b.scheduledAt.compareTo(a.scheduledAt));\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/flowy_tab.dart",
    "content": "import 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass FlowyTabItem extends StatelessWidget {\n  const FlowyTabItem({\n    super.key,\n    required this.label,\n    required this.isSelected,\n  });\n\n  final String label;\n  final bool isSelected;\n\n  static const double mobileHeight = 40;\n  static const EdgeInsets mobilePadding = EdgeInsets.symmetric(horizontal: 12);\n\n  static const double desktopHeight = 26;\n  static const EdgeInsets desktopPadding = EdgeInsets.symmetric(horizontal: 8);\n\n  @override\n  Widget build(BuildContext context) {\n    return Tab(\n      height: UniversalPlatform.isMobile ? mobileHeight : desktopHeight,\n      child: Padding(\n        padding: UniversalPlatform.isMobile ? mobilePadding : desktopPadding,\n        child: FlowyText.regular(\n          label,\n          color: isSelected\n              ? AFThemeExtension.of(context).textColor\n              : Theme.of(context).hintColor,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/inbox_action_bar.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass InboxActionBar extends StatelessWidget {\n  const InboxActionBar({\n    super.key,\n    required this.showUnreadsOnly,\n  });\n\n  final bool showUnreadsOnly;\n\n  @override\n  Widget build(BuildContext context) {\n    return DecoratedBox(\n      decoration: BoxDecoration(\n        border: Border(\n          bottom: BorderSide(\n            color: AFThemeExtension.of(context).calloutBGColor,\n          ),\n        ),\n      ),\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),\n        child: Row(\n          mainAxisAlignment: MainAxisAlignment.spaceBetween,\n          children: [\n            _MarkAsReadButton(\n              onMarkAllRead: () => context\n                  .read<ReminderBloc>()\n                  .add(const ReminderEvent.markAllRead()),\n            ),\n            _ToggleUnreadsButton(\n              showUnreadsOnly: showUnreadsOnly,\n              onToggled: (_) => context\n                  .read<NotificationFilterBloc>()\n                  .add(const NotificationFilterEvent.toggleShowUnreadsOnly()),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _ToggleUnreadsButton extends StatefulWidget {\n  const _ToggleUnreadsButton({\n    required this.onToggled,\n    this.showUnreadsOnly = false,\n  });\n\n  final Function(bool) onToggled;\n  final bool showUnreadsOnly;\n\n  @override\n  State<_ToggleUnreadsButton> createState() => _ToggleUnreadsButtonState();\n}\n\nclass _ToggleUnreadsButtonState extends State<_ToggleUnreadsButton> {\n  late bool showUnreadsOnly = widget.showUnreadsOnly;\n\n  @override\n  Widget build(BuildContext context) {\n    return SegmentedButton<bool>(\n      onSelectionChanged: (Set<bool> newSelection) {\n        setState(() => showUnreadsOnly = newSelection.first);\n        widget.onToggled(showUnreadsOnly);\n      },\n      showSelectedIcon: false,\n      style: ButtonStyle(\n        tapTargetSize: MaterialTapTargetSize.shrinkWrap,\n        side: WidgetStatePropertyAll(\n          BorderSide(color: Theme.of(context).dividerColor),\n        ),\n        shape: const WidgetStatePropertyAll(\n          RoundedRectangleBorder(\n            borderRadius: Corners.s6Border,\n          ),\n        ),\n        foregroundColor: WidgetStateProperty.resolveWith<Color>(\n          (state) {\n            if (state.contains(WidgetState.selected)) {\n              return Theme.of(context).colorScheme.onPrimary;\n            }\n\n            return AFThemeExtension.of(context).textColor;\n          },\n        ),\n        backgroundColor: WidgetStateProperty.resolveWith<Color>(\n          (state) {\n            if (state.contains(WidgetState.selected)) {\n              return Theme.of(context).colorScheme.primary;\n            }\n\n            if (state.contains(WidgetState.hovered)) {\n              return AFThemeExtension.of(context).lightGreyHover;\n            }\n\n            return Theme.of(context).cardColor;\n          },\n        ),\n      ),\n      segments: [\n        ButtonSegment<bool>(\n          value: false,\n          label: Text(\n            LocaleKeys.notificationHub_actions_showAll.tr(),\n            style: const TextStyle(fontSize: 12),\n          ),\n        ),\n        ButtonSegment<bool>(\n          value: true,\n          label: Text(\n            LocaleKeys.notificationHub_actions_showUnreads.tr(),\n            style: const TextStyle(fontSize: 12),\n          ),\n        ),\n      ],\n      selected: <bool>{showUnreadsOnly},\n    );\n  }\n}\n\nclass _MarkAsReadButton extends StatefulWidget {\n  const _MarkAsReadButton({this.onMarkAllRead});\n\n  final VoidCallback? onMarkAllRead;\n\n  @override\n  State<_MarkAsReadButton> createState() => _MarkAsReadButtonState();\n}\n\nclass _MarkAsReadButtonState extends State<_MarkAsReadButton> {\n  bool _isHovering = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return Opacity(\n      opacity: widget.onMarkAllRead != null ? 1 : 0.5,\n      child: FlowyHover(\n        onHover: (isHovering) => setState(() => _isHovering = isHovering),\n        resetHoverOnRebuild: false,\n        child: FlowyTextButton(\n          LocaleKeys.notificationHub_actions_markAllRead.tr(),\n          fontColor: widget.onMarkAllRead != null && _isHovering\n              ? Theme.of(context).colorScheme.onSurface\n              : AFThemeExtension.of(context).textColor,\n          heading: FlowySvg(\n            FlowySvgs.checklist_s,\n            color: widget.onMarkAllRead != null && _isHovering\n                ? Theme.of(context).colorScheme.onSurface\n                : AFThemeExtension.of(context).textColor,\n          ),\n          hoverColor: widget.onMarkAllRead != null && _isHovering\n              ? Theme.of(context).colorScheme.primary\n              : null,\n          onPressed: widget.onMarkAllRead,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/workspace/application/home/home_setting_bloc.dart';\nimport 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';\nimport 'package:appflowy/workspace/presentation/notifications/number_red_dot.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass NotificationButton extends StatefulWidget {\n  const NotificationButton({\n    super.key,\n    this.isHover = false,\n  });\n\n  final bool isHover;\n\n  @override\n  State<NotificationButton> createState() => _NotificationButtonState();\n}\n\nclass _NotificationButtonState extends State<NotificationButton> {\n  final mutex = PopoverMutex();\n\n  @override\n  void initState() {\n    super.initState();\n    getIt<ReminderBloc>().add(const ReminderEvent.started());\n  }\n\n  @override\n  void dispose() {\n    mutex.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<ReminderBloc>.value(\n      value: getIt<ReminderBloc>(),\n      child: BlocBuilder<HomeSettingBloc, HomeSettingState>(\n        builder: (homeSettingContext, homeSettingState) {\n          return BlocBuilder<NotificationSettingsCubit,\n              NotificationSettingsState>(\n            builder: (notificationSettingsContext, notificationSettingsState) {\n              final homeSettingBloc = context.read<HomeSettingBloc>();\n              return BlocBuilder<ReminderBloc, ReminderState>(\n                builder: (context, state) {\n                  return notificationSettingsState\n                          .isShowNotificationsIconEnabled\n                      ? _buildNotificationIcon(\n                          context,\n                          state.reminders,\n                          () => homeSettingBloc.add(\n                            HomeSettingEvent.collapseNotificationPanel(),\n                          ),\n                        )\n                      : const SizedBox.shrink();\n                },\n              );\n            },\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildNotificationIcon(\n    BuildContext context,\n    List<ReminderPB> reminders,\n    VoidCallback onTap,\n  ) {\n    return SizedBox.square(\n      dimension: 28.0,\n      child: Stack(\n        children: [\n          Center(\n            child: SizedBox.square(\n              dimension: 28.0,\n              child: FlowyButton(\n                useIntrinsicWidth: true,\n                margin: EdgeInsets.zero,\n                text: FlowySvg(\n                  FlowySvgs.notification_s,\n                  color: widget.isHover\n                      ? Theme.of(context).colorScheme.onSurface\n                      : null,\n                  opacity: 0.7,\n                ),\n                onTap: onTap,\n              ),\n            ),\n          ),\n          Align(\n            alignment: Alignment.topRight,\n            child: NumberedRedDot.desktop(),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_content_v2.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';\nimport 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';\nimport 'package:appflowy/mobile/presentation/notifications/widgets/shared.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass NotificationItemContentV2 extends StatelessWidget {\n  const NotificationItemContentV2({super.key, required this.reminder});\n  final ReminderPB reminder;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<NotificationReminderBloc, NotificationReminderState>(\n      builder: (context, state) {\n        final view = state.view;\n        if (view == null) {\n          return const SizedBox.shrink();\n        }\n        final theme = AppFlowyTheme.of(context);\n\n        return Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            _buildHeader(state.scheduledAt, theme),\n            _buildPageName(context, state.isLocked, state.pageTitle, theme),\n            _buildContent(view, state.nodes, theme),\n          ],\n        );\n      },\n    );\n  }\n\n  Widget _buildHeader(String createAt, AppFlowyThemeData theme) {\n    return SizedBox(\n      height: 22,\n      child: Row(\n        children: [\n          FlowyText.medium(\n            LocaleKeys.settings_notifications_titles_reminder.tr(),\n            fontSize: 14,\n            figmaLineHeight: 22,\n            color: theme.textColorScheme.primary,\n          ),\n          Spacer(),\n          if (createAt.isNotEmpty)\n            FlowyText.regular(\n              createAt,\n              fontSize: 12,\n              figmaLineHeight: 16,\n              color: theme.textColorScheme.secondary,\n            ),\n          if (!reminder.isRead) ...[\n            HSpace(4),\n            const UnreadRedDot(),\n          ],\n        ],\n      ),\n    );\n  }\n\n  Widget _buildPageName(\n    BuildContext context,\n    bool isLocked,\n    String pageTitle,\n    AppFlowyThemeData theme,\n  ) {\n    return SizedBox(\n      height: 18,\n      child: Row(\n        children: [\n          FlowyText.regular(\n            LocaleKeys.notificationHub_mentionedYou.tr(),\n            fontSize: 12,\n            figmaLineHeight: 18,\n            color: theme.textColorScheme.secondary,\n          ),\n          const NotificationEllipse(),\n          if (isLocked)\n            Padding(\n              padding: EdgeInsets.only(right: 5),\n              child: FlowySvg(\n                FlowySvgs.notification_lock_s,\n                color: theme.iconColorScheme.secondary,\n              ),\n            ),\n          Flexible(\n            child: FlowyText.regular(\n              pageTitle,\n              fontSize: 12,\n              figmaLineHeight: 18,\n              color: theme.textColorScheme.secondary,\n              overflow: TextOverflow.ellipsis,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildContent(\n    ViewPB view,\n    List<Node>? nodes,\n    AppFlowyThemeData theme,\n  ) {\n    if (view.layout.isDocumentView && nodes != null) {\n      return IntrinsicHeight(\n        child: BlocProvider(\n          create: (context) => DocumentPageStyleBloc(view: view),\n          child: NotificationDocumentContent(reminder: reminder, nodes: nodes),\n        ),\n      );\n    } else if (view.layout.isDatabaseView) {\n      return FlowyText(\n        reminder.message,\n        fontSize: 14,\n        figmaLineHeight: 22,\n        color: theme.textColorScheme.primary,\n        maxLines: 3,\n        overflow: TextOverflow.ellipsis,\n      );\n    }\n    return const SizedBox.shrink();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_hub_title.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\n\nclass NotificationHubTitle extends StatelessWidget {\n  const NotificationHubTitle({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 16) +\n          const EdgeInsets.only(top: 12, bottom: 4),\n      child: FlowyText.semibold(\n        LocaleKeys.notificationHub_title.tr(),\n        color: Theme.of(context).colorScheme.tertiary,\n        fontSize: 16,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass NotificationItem extends StatefulWidget {\n  const NotificationItem({\n    super.key,\n    required this.reminder,\n    required this.title,\n    required this.scheduled,\n    required this.body,\n    required this.isRead,\n    this.block,\n    this.includeTime = false,\n    this.readOnly = false,\n    this.onAction,\n    this.onReadChanged,\n    this.view,\n  });\n\n  final ReminderPB reminder;\n  final String title;\n  final Int64 scheduled;\n  final String body;\n  final bool isRead;\n  final ViewPB? view;\n\n  /// If [block] is provided, then [body] will be shown only if\n  /// [block] fails to fetch.\n  ///\n  /// [block] is rendered as a result of a [FutureBuilder].\n  ///\n  final Future<Node?>? block;\n\n  final bool includeTime;\n  final bool readOnly;\n\n  final void Function(int? path)? onAction;\n  final void Function(bool isRead)? onReadChanged;\n\n  @override\n  State<NotificationItem> createState() => _NotificationItemState();\n}\n\nclass _NotificationItemState extends State<NotificationItem> {\n  final PopoverMutex mutex = PopoverMutex();\n  bool _isHovering = false;\n  int? path;\n\n  late final String infoString;\n\n  @override\n  void initState() {\n    super.initState();\n    widget.block?.then((b) => path = b?.path.first);\n    infoString = _buildInfoString();\n  }\n\n  @override\n  void dispose() {\n    mutex.dispose();\n    super.dispose();\n  }\n\n  String _buildInfoString() {\n    String scheduledString =\n        _scheduledString(widget.scheduled, widget.includeTime);\n\n    if (widget.view != null) {\n      scheduledString = '$scheduledString - ${widget.view!.name}';\n    }\n\n    return scheduledString;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      onEnter: (_) => _onHover(true),\n      onExit: (_) => _onHover(false),\n      cursor: widget.onAction != null\n          ? SystemMouseCursors.click\n          : MouseCursor.defer,\n      child: Stack(\n        children: [\n          GestureDetector(\n            onTap: () => widget.onAction?.call(path),\n            child: AbsorbPointer(\n              child: DecoratedBox(\n                decoration: BoxDecoration(\n                  border: Border(\n                    bottom: UniversalPlatform.isMobile\n                        ? BorderSide(\n                            color: AFThemeExtension.of(context).calloutBGColor,\n                          )\n                        : BorderSide.none,\n                  ),\n                ),\n                child: Opacity(\n                  opacity: widget.isRead && !widget.readOnly ? 0.5 : 1,\n                  child: DecoratedBox(\n                    decoration: BoxDecoration(\n                      color: _isHovering && widget.onAction != null\n                          ? AFThemeExtension.of(context).lightGreyHover\n                          : Colors.transparent,\n                      border: widget.isRead || widget.readOnly\n                          ? null\n                          : Border(\n                              left: BorderSide(\n                                width: UniversalPlatform.isMobile ? 4 : 2,\n                                color: Theme.of(context).colorScheme.primary,\n                              ),\n                            ),\n                    ),\n                    child: Padding(\n                      padding: const EdgeInsets.symmetric(\n                        vertical: 10,\n                        horizontal: 16,\n                      ),\n                      child: Row(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        children: [\n                          FlowySvg(\n                            FlowySvgs.time_s,\n                            size: Size.square(\n                              UniversalPlatform.isMobile ? 24 : 20,\n                            ),\n                            color: AFThemeExtension.of(context).textColor,\n                          ),\n                          const HSpace(16),\n                          Expanded(\n                            child: Column(\n                              crossAxisAlignment: CrossAxisAlignment.start,\n                              children: [\n                                FlowyText.semibold(\n                                  widget.title,\n                                  fontSize:\n                                      UniversalPlatform.isMobile ? 16 : 14,\n                                  color: AFThemeExtension.of(context).textColor,\n                                ),\n                                FlowyText.regular(\n                                  infoString,\n                                  fontSize:\n                                      UniversalPlatform.isMobile ? 12 : 10,\n                                ),\n                                const VSpace(5),\n                                Container(\n                                  padding: const EdgeInsets.all(8),\n                                  decoration: BoxDecoration(\n                                    borderRadius: Corners.s8Border,\n                                    color:\n                                        Theme.of(context).colorScheme.surface,\n                                  ),\n                                  child: _NotificationContent(\n                                    block: widget.block,\n                                    reminder: widget.reminder,\n                                    body: widget.body,\n                                  ),\n                                ),\n                              ],\n                            ),\n                          ),\n                        ],\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ),\n          if (UniversalPlatform.isMobile && !widget.readOnly ||\n              _isHovering && !widget.readOnly)\n            Positioned(\n              right: UniversalPlatform.isMobile ? 8 : 4,\n              top: UniversalPlatform.isMobile ? 8 : 4,\n              child: NotificationItemActions(\n                isRead: widget.isRead,\n                onReadChanged: widget.onReadChanged,\n              ),\n            ),\n        ],\n      ),\n    );\n  }\n\n  String _scheduledString(Int64 secondsSinceEpoch, bool includeTime) {\n    final appearance = context.read<AppearanceSettingsCubit>().state;\n    return appearance.dateFormat.formatDate(\n      DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch.toInt() * 1000),\n      includeTime,\n      appearance.timeFormat,\n    );\n  }\n\n  void _onHover(bool isHovering) => setState(() => _isHovering = isHovering);\n}\n\nclass _NotificationContent extends StatelessWidget {\n  const _NotificationContent({\n    required this.body,\n    required this.reminder,\n    required this.block,\n  });\n\n  final String body;\n  final ReminderPB reminder;\n  final Future<Node?>? block;\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder<Node?>(\n      future: block,\n      builder: (context, snapshot) {\n        if (snapshot.hasError || !snapshot.hasData || snapshot.data == null) {\n          return FlowyText.regular(body, maxLines: 4);\n        }\n\n        return IntrinsicHeight(\n          child: NotificationDocumentContent(\n            nodes: [snapshot.data!],\n            reminder: reminder,\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass NotificationItemActions extends StatelessWidget {\n  const NotificationItemActions({\n    super.key,\n    required this.isRead,\n    this.onReadChanged,\n  });\n\n  final bool isRead;\n  final void Function(bool isRead)? onReadChanged;\n\n  @override\n  Widget build(BuildContext context) {\n    final double size = UniversalPlatform.isMobile ? 40.0 : 30.0;\n\n    return Container(\n      height: size,\n      decoration: BoxDecoration(\n        color: Theme.of(context).cardColor,\n        border: Border.all(\n          color: AFThemeExtension.of(context).lightGreyHover,\n        ),\n        borderRadius: BorderRadius.circular(6),\n      ),\n      child: IntrinsicHeight(\n        child: Row(\n          children: [\n            if (isRead) ...[\n              FlowyIconButton(\n                height: size,\n                width: size,\n                radius: BorderRadius.circular(4),\n                tooltipText:\n                    LocaleKeys.reminderNotification_tooltipMarkUnread.tr(),\n                icon: const FlowySvg(FlowySvgs.restore_s),\n                iconColorOnHover: Theme.of(context).colorScheme.onSurface,\n                onPressed: () => onReadChanged?.call(false),\n              ),\n            ] else ...[\n              FlowyIconButton(\n                height: size,\n                width: size,\n                radius: BorderRadius.circular(4),\n                tooltipText:\n                    LocaleKeys.reminderNotification_tooltipMarkRead.tr(),\n                iconColorOnHover: Theme.of(context).colorScheme.onSurface,\n                icon: const FlowySvg(FlowySvgs.messages_s),\n                onPressed: () => onReadChanged?.call(true),\n              ),\n            ],\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item_v2.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';\nimport 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';\nimport 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';\nimport 'package:appflowy/plugins/document/application/document_service.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/user/application/reminder/reminder_extension.dart';\nimport 'package:appflowy/workspace/application/home/home_setting_bloc.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'notification_content_v2.dart';\n\nclass NotificationItemV2 extends StatelessWidget {\n  const NotificationItemV2({\n    super.key,\n    required this.tabType,\n    required this.reminder,\n  });\n\n  final NotificationTabType tabType;\n  final ReminderPB reminder;\n\n  @override\n  Widget build(BuildContext context) {\n    final settings = context.read<AppearanceSettingsCubit>().state;\n    final dateFormate = settings.dateFormat;\n    final timeFormate = settings.timeFormat;\n    return BlocProvider<NotificationReminderBloc>(\n      create: (context) => NotificationReminderBloc()\n        ..add(\n          NotificationReminderEvent.initial(\n            reminder,\n            dateFormate,\n            timeFormate,\n          ),\n        ),\n      child: BlocBuilder<NotificationReminderBloc, NotificationReminderState>(\n        builder: (context, state) {\n          final reminderBloc = context.read<ReminderBloc>();\n          final homeSetting = context.read<HomeSettingBloc>();\n          if (state.status == NotificationReminderStatus.loading ||\n              state.status == NotificationReminderStatus.initial) {\n            return const SizedBox.shrink();\n          }\n\n          if (state.status == NotificationReminderStatus.error) {\n            // error handle.\n            return const SizedBox.shrink();\n          }\n\n          final child = Padding(\n            padding: const EdgeInsets.fromLTRB(16, 14, 14, 10),\n            child: _InnerNotificationItem(\n              tabType: tabType,\n              reminder: reminder,\n            ),\n          );\n\n          return Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 8),\n            child: FlowyHover(\n              style: HoverStyle(\n                hoverColor: Theme.of(context).colorScheme.secondary,\n                borderRadius: BorderRadius.all(Radius.circular(8)),\n              ),\n              resetHoverOnRebuild: false,\n              builder: (context, hover) {\n                return GestureDetector(\n                  behavior: HitTestBehavior.translucent,\n                  child: Stack(\n                    children: [\n                      child,\n                      if (hover) buildActions(context),\n                    ],\n                  ),\n                  onTap: () async {\n                    final view = state.view;\n                    if (view == null) {\n                      return;\n                    }\n\n                    homeSetting\n                        .add(HomeSettingEvent.collapseNotificationPanel());\n\n                    final documentFuture = DocumentService().openDocument(\n                      documentId: reminder.objectId,\n                    );\n\n                    final blockId = reminder.meta[ReminderMetaKeys.blockId];\n\n                    int? path;\n                    if (blockId != null) {\n                      final node =\n                          await _getNodeFromDocument(documentFuture, blockId);\n                      path = node?.path.first;\n                    }\n\n                    reminderBloc.add(\n                      ReminderEvent.pressReminder(\n                        reminderId: reminder.id,\n                        path: path,\n                      ),\n                    );\n                  },\n                );\n              },\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget buildActions(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final borderColor = theme.borderColorScheme.primary;\n    final decoration = BoxDecoration(\n      border: Border.all(color: borderColor),\n      borderRadius: BorderRadius.all(Radius.circular(6)),\n      color: theme.surfaceColorScheme.primary,\n    );\n\n    Widget child;\n    if (tabType == NotificationTabType.archive) {\n      child = Container(\n        width: 32,\n        height: 28,\n        decoration: decoration,\n        child: FlowyIconButton(\n          tooltipText: LocaleKeys.notificationHub_unarchiveTooltip.tr(),\n          icon: FlowySvg(FlowySvgs.notification_unarchive_s),\n          onPressed: () {\n            context.read<ReminderBloc>().add(\n                  ReminderEvent.update(\n                    ReminderUpdate(\n                      id: reminder.id,\n                      isArchived: false,\n                    ),\n                  ),\n                );\n          },\n          width: 24,\n          height: 24,\n        ),\n      );\n    } else {\n      child = Container(\n        padding: EdgeInsets.fromLTRB(4, 2, 4, 2),\n        decoration: decoration,\n        child: Row(\n          children: [\n            if (!reminder.isRead) ...[\n              FlowyIconButton(\n                tooltipText: LocaleKeys.notificationHub_markAsReadTooltip.tr(),\n                icon: FlowySvg(FlowySvgs.notification_markasread_s),\n                width: 24,\n                height: 24,\n                onPressed: () {\n                  context.read<ReminderBloc>().add(\n                        ReminderEvent.update(\n                          ReminderUpdate(\n                            id: reminder.id,\n                            isRead: true,\n                          ),\n                        ),\n                      );\n\n                  showToastNotification(\n                    message:\n                        LocaleKeys.notificationHub_markAsReadSucceedToast.tr(),\n                  );\n                },\n              ),\n              HSpace(6),\n            ],\n            FlowyIconButton(\n              tooltipText: LocaleKeys.notificationHub_archivedTooltip.tr(),\n              icon: FlowySvg(\n                FlowySvgs.notification_archive_s,\n              ),\n              width: 24,\n              height: 24,\n              onPressed: () {\n                context.read<ReminderBloc>().add(\n                      ReminderEvent.update(\n                        ReminderUpdate(\n                          id: reminder.id,\n                          isArchived: true,\n                          isRead: true,\n                        ),\n                      ),\n                    );\n\n                showToastNotification(\n                  message: LocaleKeys.notificationHub_markAsArchivedSucceedToast\n                      .tr(),\n                );\n              },\n            ),\n          ],\n        ),\n      );\n    }\n    return Positioned(\n      top: 8,\n      right: 8,\n      child: child,\n    );\n  }\n\n  Future<Node?> _getNodeFromDocument(\n    Future<FlowyResult<DocumentDataPB, FlowyError>> documentFuture,\n    String blockId,\n  ) async {\n    final document = (await documentFuture).fold(\n      (document) => document,\n      (_) => null,\n    );\n\n    if (document == null) {\n      return null;\n    }\n\n    final rootNode = document.toDocument()?.root;\n    if (rootNode == null) {\n      return null;\n    }\n\n    return _searchById(rootNode, blockId);\n  }\n\n  Node? _searchById(Node current, String id) {\n    if (current.id == id) {\n      return current;\n    }\n\n    if (current.children.isNotEmpty) {\n      for (final child in current.children) {\n        final node = _searchById(child, id);\n\n        if (node != null) {\n          return node;\n        }\n      }\n    }\n\n    return null;\n  }\n}\n\nclass _InnerNotificationItem extends StatelessWidget {\n  const _InnerNotificationItem({\n    required this.reminder,\n    required this.tabType,\n  });\n\n  final NotificationTabType tabType;\n  final ReminderPB reminder;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        NotificationIcon(reminder: reminder, atSize: 14),\n        const HSpace(12.0),\n        Expanded(\n          child: NotificationItemContentV2(reminder: reminder),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_tab.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/notifications/widgets/empty.dart';\nimport 'package:appflowy/shared/list_extension.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/user/application/reminder/reminder_extension.dart';\nimport 'package:appflowy/util/int64_extension.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/appflowy_backend.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'notification_item_v2.dart';\nimport 'notification_tab_bar.dart';\n\nclass NotificationTab extends StatefulWidget {\n  const NotificationTab({\n    super.key,\n    required this.tabType,\n  });\n\n  final NotificationTabType tabType;\n\n  @override\n  State<NotificationTab> createState() => _NotificationTabState();\n}\n\nclass _NotificationTabState extends State<NotificationTab>\n    with AutomaticKeepAliveClientMixin {\n  @override\n  bool get wantKeepAlive => true;\n\n  @override\n  Widget build(BuildContext context) {\n    super.build(context);\n\n    return BlocBuilder<ReminderBloc, ReminderState>(\n      builder: (context, state) {\n        final reminders = _filterReminders(state.reminders);\n\n        if (reminders.isEmpty) return EmptyNotification(type: widget.tabType);\n\n        final dateTimeNow = DateTime.now();\n        final List<ReminderPB> todayReminders = [];\n        final List<ReminderPB> olderReminders = [];\n        for (final reminder in reminders) {\n          final scheduledAt = reminder.scheduledAt.toDateTime();\n          if (dateTimeNow.difference(scheduledAt).inDays < 1) {\n            todayReminders.add(reminder);\n          } else {\n            olderReminders.add(reminder);\n          }\n        }\n\n        final child = SingleChildScrollView(\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              buildReminders(\n                LocaleKeys.notificationHub_today.tr(),\n                todayReminders,\n              ),\n              buildReminders(\n                LocaleKeys.notificationHub_older.tr(),\n                olderReminders,\n              ),\n            ],\n          ),\n        );\n\n        return RefreshIndicator.adaptive(\n          onRefresh: () async => _onRefresh(context),\n          child: child,\n        );\n      },\n    );\n  }\n\n  Widget buildReminders(\n    String title,\n    List<ReminderPB> reminders,\n  ) {\n    if (reminders.isEmpty) return SizedBox.shrink();\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 16),\n          child: FlowyText.regular(\n            title,\n            fontSize: 14,\n            figmaLineHeight: 18,\n          ),\n        ),\n        const VSpace(4),\n        ...List.generate(reminders.length, (index) {\n          final reminder = reminders[index];\n          return NotificationItemV2(\n            key: ValueKey('${widget.tabType}_${reminder.id}'),\n            tabType: widget.tabType,\n            reminder: reminder,\n          );\n        }),\n      ],\n    );\n  }\n\n  Future<void> _onRefresh(BuildContext context) async {\n    context.read<ReminderBloc>().add(const ReminderEvent.refresh());\n\n    // at least 0.5 seconds to dismiss the refresh indicator.\n    // otherwise, it will be dismissed immediately.\n    await context.read<ReminderBloc>().stream.firstOrNull;\n    await Future.delayed(const Duration(milliseconds: 500));\n\n    if (context.mounted) {\n      showToastNotification(\n        message: LocaleKeys.settings_notifications_refreshSuccess.tr(),\n      );\n    }\n  }\n\n  List<ReminderPB> _filterReminders(List<ReminderPB> reminders) {\n    switch (widget.tabType) {\n      case NotificationTabType.inbox:\n        return reminders.reversed\n            .where((reminder) => !reminder.isArchived)\n            .toList()\n            .unique((reminder) => reminder.id);\n      case NotificationTabType.archive:\n        return reminders.reversed\n            .where((reminder) => reminder.isArchived)\n            .toList()\n            .unique((reminder) => reminder.id);\n      case NotificationTabType.unread:\n        return reminders.reversed\n            .where((reminder) => !reminder.isRead)\n            .toList()\n            .unique((reminder) => reminder.id);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_tab_bar.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nenum NotificationTabType {\n  inbox,\n  unread,\n  archive;\n\n  String get tr {\n    switch (this) {\n      case NotificationTabType.inbox:\n        return LocaleKeys.settings_notifications_tabs_inbox.tr();\n      case NotificationTabType.unread:\n        return LocaleKeys.settings_notifications_tabs_unread.tr();\n      case NotificationTabType.archive:\n        return LocaleKeys.settings_notifications_tabs_archived.tr();\n    }\n  }\n}\n\nclass NotificationTabBar extends StatelessWidget {\n  const NotificationTabBar({\n    super.key,\n    required this.tabController,\n    this.height = 32,\n    required this.tabs,\n  });\n\n  final double height;\n  final List<NotificationTabType> tabs;\n  final TabController tabController;\n\n  @override\n  Widget build(BuildContext context) {\n    final baseStyle = Theme.of(context).textTheme.bodyMedium;\n    final labelStyle = baseStyle?.copyWith(\n      fontWeight: FontWeight.w500,\n      fontSize: 16.0,\n      height: 22.0 / 16.0,\n    );\n    final unselectedLabelStyle = baseStyle?.copyWith(\n      fontWeight: FontWeight.w400,\n      fontSize: 15.0,\n      height: 22.0 / 15.0,\n    );\n\n    return Container(\n      height: height,\n      padding: const EdgeInsets.only(left: 16),\n      child: TabBar(\n        controller: tabController,\n        tabs: tabs.map((e) => Tab(text: e.tr)).toList(),\n        indicatorSize: TabBarIndicatorSize.label,\n        isScrollable: true,\n        labelStyle: labelStyle,\n        labelColor: baseStyle?.color,\n        labelPadding: const EdgeInsets.only(right: 20),\n        unselectedLabelStyle: unselectedLabelStyle,\n        overlayColor: WidgetStateProperty.all(Colors.transparent),\n        indicator: const RoundUnderlineTabIndicator(\n          width: 28.0,\n          borderSide: BorderSide(\n            color: Color(0xFF00C8FF),\n            width: 3,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_view.dart",
    "content": "import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';\nimport 'package:appflowy/plugins/document/application/prelude.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/user/application/reminder/reminder_extension.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/notification_item.dart';\nimport 'package:appflowy/workspace/presentation/notifications/widgets/notifications_hub_empty.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter/material.dart';\n\n/// Displays a Lsit of Notifications, currently used primarily to\n/// display Reminders.\n///\n/// Optimized for both Mobile & Desktop use\n///\nclass NotificationsView extends StatelessWidget {\n  const NotificationsView({\n    super.key,\n    required this.shownReminders,\n    required this.reminderBloc,\n    required this.views,\n    this.isUpcoming = false,\n    this.onAction,\n    this.onReadChanged,\n    this.actionBar,\n  });\n\n  final List<ReminderPB> shownReminders;\n  final ReminderBloc reminderBloc;\n  final List<ViewPB> views;\n  final bool isUpcoming;\n  final Function(ReminderPB reminder, int? path, ViewPB? view)? onAction;\n  final Function(ReminderPB reminder, bool isRead)? onReadChanged;\n  final Widget? actionBar;\n\n  @override\n  Widget build(BuildContext context) {\n    if (shownReminders.isEmpty) {\n      return Column(\n        children: [\n          if (actionBar != null) actionBar!,\n          const Expanded(child: NotificationsHubEmpty()),\n        ],\n      );\n    }\n\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        if (actionBar != null) actionBar!,\n        Expanded(\n          child: SingleChildScrollView(\n            child: Column(\n              children: [\n                ...shownReminders.map(\n                  (ReminderPB reminder) {\n                    final blockId = reminder.meta[ReminderMetaKeys.blockId];\n\n                    final documentService = DocumentService();\n                    final documentFuture = documentService.openDocument(\n                      documentId: reminder.objectId,\n                    );\n\n                    Future<Node?>? nodeBuilder;\n                    if (blockId != null) {\n                      nodeBuilder =\n                          _getNodeFromDocument(documentFuture, blockId);\n                    }\n\n                    final view = views.findView(reminder.objectId);\n                    return NotificationItem(\n                      reminder: reminder,\n                      key: ValueKey(reminder.id),\n                      title: reminder.title,\n                      scheduled: reminder.scheduledAt,\n                      body: reminder.message,\n                      block: nodeBuilder,\n                      isRead: reminder.isRead,\n                      includeTime: reminder.includeTime ?? false,\n                      readOnly: isUpcoming,\n                      onReadChanged: (isRead) =>\n                          onReadChanged?.call(reminder, isRead),\n                      onAction: (path) => onAction?.call(reminder, path, view),\n                      view: view,\n                    );\n                  },\n                ),\n              ],\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  Future<Node?> _getNodeFromDocument(\n    Future<FlowyResult<DocumentDataPB, FlowyError>> documentFuture,\n    String blockId,\n  ) async {\n    final document = (await documentFuture).fold(\n      (document) => document,\n      (_) => null,\n    );\n\n    if (document == null) {\n      return null;\n    }\n\n    final rootNode = document.toDocument()?.root;\n    if (rootNode == null) {\n      return null;\n    }\n\n    return _searchById(rootNode, blockId);\n  }\n}\n\n/// Recursively iterates a [Node] and compares by its [id]\n///\nNode? _searchById(Node current, String id) {\n  if (current.id == id) {\n    return current;\n  }\n\n  if (current.children.isNotEmpty) {\n    for (final child in current.children) {\n      final node = _searchById(child, id);\n\n      if (node != null) {\n        return node;\n      }\n    }\n  }\n\n  return null;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notifications_hub_empty.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\n\nclass NotificationsHubEmpty extends StatelessWidget {\n  const NotificationsHubEmpty({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Center(\n      child: Padding(\n        padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            FlowyText(\n              LocaleKeys.notificationHub_emptyTitle.tr(),\n              fontWeight: FontWeight.w700,\n              fontSize: 14,\n            ),\n            const VSpace(8),\n            FlowyText.regular(\n              LocaleKeys.notificationHub_emptyBody.tr(),\n              textAlign: TextAlign.center,\n              maxLines: 2,\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/version_checker/version_checker.dart';\nimport 'package:appflowy/startup/tasks/device_info_task.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass SettingsAppVersion extends StatelessWidget {\n  const SettingsAppVersion({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return ApplicationInfo.isUpdateAvailable\n        ? const _UpdateAppSection()\n        : _buildIsUpToDate(context);\n  }\n\n  Widget _buildIsUpToDate(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Text(\n          LocaleKeys.settings_accountPage_isUpToDate.tr(),\n          style: theme.textStyle.body.enhanced(\n            color: theme.textColorScheme.primary,\n          ),\n        ),\n        VSpace(theme.spacing.s),\n        Text(\n          LocaleKeys.settings_accountPage_officialVersion.tr(\n            namedArgs: {\n              'version': ApplicationInfo.applicationVersion,\n            },\n          ),\n          style: theme.textStyle.caption.standard(\n            color: theme.textColorScheme.secondary,\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass _UpdateAppSection extends StatelessWidget {\n  const _UpdateAppSection();\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        Expanded(child: _buildDescription(context)),\n        _buildUpdateButton(),\n      ],\n    );\n  }\n\n  Widget _buildUpdateButton() {\n    return PrimaryRoundedButton(\n      text: LocaleKeys.autoUpdate_settingsUpdateButton.tr(),\n      margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),\n      fontWeight: FontWeight.w500,\n      radius: 8.0,\n      onTap: () {\n        Log.info('[AutoUpdater] Checking for updates');\n        versionChecker.checkForUpdate();\n      },\n    );\n  }\n\n  Widget _buildDescription(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Row(\n          children: [\n            _buildRedDot(),\n            const HSpace(6),\n            Flexible(\n              child: FlowyText.medium(\n                LocaleKeys.autoUpdate_settingsUpdateTitle.tr(\n                  namedArgs: {\n                    'newVersion': ApplicationInfo.latestVersion,\n                  },\n                ),\n                figmaLineHeight: 17,\n                overflow: TextOverflow.ellipsis,\n              ),\n            ),\n          ],\n        ),\n        const VSpace(4),\n        _buildCurrentVersionAndLatestVersion(context),\n      ],\n    );\n  }\n\n  Widget _buildCurrentVersionAndLatestVersion(BuildContext context) {\n    return Row(\n      children: [\n        Flexible(\n          child: Opacity(\n            opacity: 0.7,\n            child: FlowyText.regular(\n              LocaleKeys.autoUpdate_settingsUpdateDescription.tr(\n                namedArgs: {\n                  'currentVersion': ApplicationInfo.applicationVersion,\n                  'newVersion': ApplicationInfo.latestVersion,\n                },\n              ),\n              fontSize: 12,\n              figmaLineHeight: 13,\n              overflow: TextOverflow.ellipsis,\n            ),\n          ),\n        ),\n        const HSpace(6),\n        MouseRegion(\n          cursor: SystemMouseCursors.click,\n          child: GestureDetector(\n            onTap: () {\n              afLaunchUrlString('https://www.appflowy.io/what-is-new');\n            },\n            child: FlowyText.regular(\n              LocaleKeys.autoUpdate_settingsUpdateWhatsNew.tr(),\n              decoration: TextDecoration.underline,\n              color: Theme.of(context).colorScheme.primary,\n              fontSize: 12,\n              figmaLineHeight: 13,\n              overflow: TextOverflow.ellipsis,\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildRedDot() {\n    return Container(\n      width: 8,\n      height: 8,\n      decoration: const BoxDecoration(\n        color: Color(0xFFFB006D),\n        shape: BoxShape.circle,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account.dart",
    "content": "export 'account_deletion.dart';\nexport 'account_sign_in_out.dart';\nexport 'account_user_profile.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/loading.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/util/navigator_context_extension.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nconst _acceptableConfirmTexts = [\n  'delete my account',\n  'deletemyaccount',\n  'DELETE MY ACCOUNT',\n  'DELETEMYACCOUNT',\n];\n\nclass AccountDeletionButton extends StatefulWidget {\n  const AccountDeletionButton({\n    super.key,\n  });\n\n  @override\n  State<AccountDeletionButton> createState() => _AccountDeletionButtonState();\n}\n\nclass _AccountDeletionButtonState extends State<AccountDeletionButton> {\n  final textEditingController = TextEditingController();\n  final isCheckedNotifier = ValueNotifier(false);\n\n  @override\n  void dispose() {\n    textEditingController.dispose();\n    isCheckedNotifier.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Row(\n      children: [\n        Expanded(\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              Text(\n                LocaleKeys.button_deleteAccount.tr(),\n                style: theme.textStyle.heading4.enhanced(\n                  color: theme.textColorScheme.primary,\n                ),\n              ),\n              const VSpace(4),\n              Text(\n                LocaleKeys.newSettings_myAccount_deleteAccount_description.tr(),\n                maxLines: 2,\n                overflow: TextOverflow.ellipsis,\n                style: theme.textStyle.caption.standard(\n                  color: theme.textColorScheme.secondary,\n                ),\n              ),\n            ],\n          ),\n        ),\n        AFOutlinedTextButton.destructive(\n          text: LocaleKeys.button_deleteAccount.tr(),\n          textStyle: theme.textStyle.body.standard(\n            color: theme.textColorScheme.error,\n            weight: FontWeight.w400,\n          ),\n          onTap: () {\n            isCheckedNotifier.value = false;\n            textEditingController.clear();\n\n            showCancelAndDeleteDialog(\n              context: context,\n              title: LocaleKeys.newSettings_myAccount_deleteAccount_title.tr(),\n              description: '',\n              builder: (_) => _AccountDeletionDialog(\n                controller: textEditingController,\n                isChecked: isCheckedNotifier,\n              ),\n              onDelete: () => deleteMyAccount(\n                context,\n                textEditingController.text.trim(),\n                isCheckedNotifier.value,\n                onSuccess: () {\n                  context.popToHome();\n                },\n              ),\n            );\n          },\n        ),\n      ],\n    );\n  }\n}\n\nclass _AccountDeletionDialog extends StatelessWidget {\n  const _AccountDeletionDialog({\n    required this.controller,\n    required this.isChecked,\n  });\n\n  final TextEditingController controller;\n  final ValueNotifier<bool> isChecked;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        FlowyText.regular(\n          LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint1.tr(),\n          fontSize: 14.0,\n          figmaLineHeight: 18.0,\n          maxLines: 2,\n          color: ConfirmPopupColor.descriptionColor(context),\n        ),\n        const VSpace(12.0),\n        FlowyTextField(\n          hintText:\n              LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint3.tr(),\n          controller: controller,\n        ),\n        const VSpace(16),\n        Row(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            GestureDetector(\n              onTap: () => isChecked.value = !isChecked.value,\n              child: ValueListenableBuilder<bool>(\n                valueListenable: isChecked,\n                builder: (context, isChecked, _) {\n                  return FlowySvg(\n                    isChecked ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,\n                    size: const Size.square(16.0),\n                    blendMode: isChecked ? null : BlendMode.srcIn,\n                  );\n                },\n              ),\n            ),\n            const HSpace(6.0),\n            Expanded(\n              child: FlowyText.regular(\n                LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint2\n                    .tr(),\n                fontSize: 14.0,\n                figmaLineHeight: 16.0,\n                maxLines: 3,\n                color: ConfirmPopupColor.descriptionColor(context),\n              ),\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n}\n\nbool _isConfirmTextValid(String text) {\n  // don't convert the text to lower case or upper case,\n  //  just check if the text is in the list\n  return _acceptableConfirmTexts.contains(text) ||\n      text == LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint3.tr();\n}\n\nFuture<void> deleteMyAccount(\n  BuildContext context,\n  String confirmText,\n  bool isChecked, {\n  VoidCallback? onSuccess,\n  VoidCallback? onFailure,\n}) async {\n  final bottomPadding = UniversalPlatform.isMobile\n      ? MediaQuery.of(context).viewInsets.bottom\n      : 0.0;\n\n  if (!isChecked) {\n    showToastNotification(\n      type: ToastificationType.warning,\n      bottomPadding: bottomPadding,\n      message: LocaleKeys\n          .newSettings_myAccount_deleteAccount_checkToConfirmError\n          .tr(),\n    );\n    return;\n  }\n  if (!context.mounted) {\n    return;\n  }\n\n  if (confirmText.isEmpty || !_isConfirmTextValid(confirmText)) {\n    showToastNotification(\n      type: ToastificationType.warning,\n      bottomPadding: bottomPadding,\n      message: LocaleKeys\n          .newSettings_myAccount_deleteAccount_confirmTextValidationFailed\n          .tr(),\n    );\n    return;\n  }\n\n  final loading = Loading(context)..start();\n\n  await UserBackendService.deleteCurrentAccount().fold(\n    (s) {\n      Log.info('account deletion success');\n\n      loading.stop();\n      showToastNotification(\n        message: LocaleKeys\n            .newSettings_myAccount_deleteAccount_deleteAccountSuccess\n            .tr(),\n      );\n\n      // delay 1 second to make sure the toast notification is shown\n      Future.delayed(const Duration(seconds: 1), () async {\n        onSuccess?.call();\n\n        // restart the application\n        await runAppFlowy();\n      });\n    },\n    (f) {\n      Log.error('account deletion failed, error: $f');\n\n      loading.stop();\n      showToastNotification(\n        type: ToastificationType.error,\n        bottomPadding: bottomPadding,\n        message: f.msg,\n      );\n\n      onFailure?.call();\n    },\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/application/password/password_bloc.dart';\nimport 'package:appflowy/user/application/prelude.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart';\nimport 'package:appflowy/util/navigator_context_extension.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/password/change_password.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/password/setup_password.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/setting_third_party_login.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass AccountSignInOutSection extends StatelessWidget {\n  const AccountSignInOutSection({\n    super.key,\n    required this.userProfile,\n    required this.onAction,\n    this.signIn = true,\n  });\n\n  final UserProfilePB userProfile;\n  final VoidCallback onAction;\n  final bool signIn;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Row(\n      children: [\n        Text(\n          LocaleKeys.settings_accountPage_login_title.tr(),\n          style: theme.textStyle.body.enhanced(\n            color: theme.textColorScheme.primary,\n          ),\n        ),\n        const Spacer(),\n        AccountSignInOutButton(\n          userProfile: userProfile,\n          onAction: onAction,\n          signIn: signIn,\n        ),\n      ],\n    );\n  }\n}\n\nclass AccountSignInOutButton extends StatelessWidget {\n  const AccountSignInOutButton({\n    super.key,\n    required this.userProfile,\n    required this.onAction,\n    this.signIn = true,\n  });\n\n  final UserProfilePB userProfile;\n  final VoidCallback onAction;\n  final bool signIn;\n\n  @override\n  Widget build(BuildContext context) {\n    return AFFilledTextButton.primary(\n      text: signIn\n          ? LocaleKeys.settings_accountPage_login_loginLabel.tr()\n          : LocaleKeys.settings_accountPage_login_logoutLabel.tr(),\n      onTap: () =>\n          signIn ? _showSignInDialog(context) : _showLogoutDialog(context),\n    );\n  }\n\n  void _showLogoutDialog(BuildContext context) {\n    showCancelAndConfirmDialog(\n      context: context,\n      title: LocaleKeys.settings_accountPage_login_logoutLabel.tr(),\n      description: LocaleKeys.settings_menu_logoutPrompt.tr(),\n      confirmLabel: LocaleKeys.button_yes.tr(),\n      onConfirm: (_) async {\n        await getIt<AuthService>().signOut();\n        onAction();\n      },\n    );\n  }\n\n  Future<void> _showSignInDialog(BuildContext context) async {\n    await showDialog(\n      context: context,\n      builder: (context) => BlocProvider<SignInBloc>(\n        create: (context) => getIt<SignInBloc>(),\n        child: const FlowyDialog(\n          constraints: BoxConstraints(maxHeight: 485, maxWidth: 375),\n          child: _SignInDialogContent(),\n        ),\n      ),\n    );\n  }\n}\n\nclass ChangePasswordSection extends StatelessWidget {\n  const ChangePasswordSection({\n    super.key,\n    required this.userProfile,\n  });\n\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return BlocBuilder<PasswordBloc, PasswordState>(\n      builder: (context, state) {\n        return Row(\n          children: [\n            Text(\n              LocaleKeys.newSettings_myAccount_password_title.tr(),\n              style: theme.textStyle.body.enhanced(\n                color: theme.textColorScheme.primary,\n              ),\n            ),\n            const Spacer(),\n            state.hasPassword\n                ? AFFilledTextButton.primary(\n                    text: LocaleKeys\n                        .newSettings_myAccount_password_changePassword\n                        .tr(),\n                    onTap: () => _showChangePasswordDialog(context),\n                  )\n                : AFFilledTextButton.primary(\n                    text: LocaleKeys\n                        .newSettings_myAccount_password_setupPassword\n                        .tr(),\n                    onTap: () => _showSetPasswordDialog(context),\n                  ),\n          ],\n        );\n      },\n    );\n  }\n\n  Future<void> _showChangePasswordDialog(BuildContext context) async {\n    final theme = AppFlowyTheme.of(context);\n    await showDialog(\n      context: context,\n      barrierDismissible: false,\n      builder: (_) => MultiBlocProvider(\n        providers: [\n          BlocProvider<PasswordBloc>.value(\n            value: context.read<PasswordBloc>(),\n          ),\n          BlocProvider<SignInBloc>.value(\n            value: getIt<SignInBloc>(),\n          ),\n        ],\n        child: Dialog(\n          shape: RoundedRectangleBorder(\n            borderRadius: BorderRadius.circular(theme.borderRadius.xl),\n          ),\n          child: ChangePasswordDialogContent(\n            userProfile: userProfile,\n          ),\n        ),\n      ),\n    );\n  }\n\n  Future<void> _showSetPasswordDialog(BuildContext context) async {\n    final theme = AppFlowyTheme.of(context);\n    await showDialog(\n      context: context,\n      barrierDismissible: false,\n      builder: (_) => MultiBlocProvider(\n        providers: [\n          BlocProvider<PasswordBloc>.value(\n            value: context.read<PasswordBloc>(),\n          ),\n          BlocProvider<SignInBloc>.value(\n            value: getIt<SignInBloc>(),\n          ),\n        ],\n        child: Dialog(\n          shape: RoundedRectangleBorder(\n            borderRadius: BorderRadius.circular(theme.borderRadius.xl),\n          ),\n          child: SetupPasswordDialogContent(\n            userProfile: userProfile,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _SignInDialogContent extends StatelessWidget {\n  const _SignInDialogContent();\n\n  @override\n  Widget build(BuildContext context) {\n    return ScaffoldMessenger(\n      child: Scaffold(\n        body: Padding(\n          padding: const EdgeInsets.all(24),\n          child: SingleChildScrollView(\n            child: Column(\n              children: [\n                const _DialogHeader(),\n                const _DialogTitle(),\n                const VSpace(16),\n                const ContinueWithEmailAndPassword(),\n                if (isAuthEnabled) ...[\n                  const VSpace(20),\n                  const _OrDivider(),\n                  const VSpace(10),\n                  SettingThirdPartyLogin(\n                    didLogin: () {\n                      context.popToHome();\n                    },\n                  ),\n                ],\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _DialogHeader extends StatelessWidget {\n  const _DialogHeader();\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.spaceBetween,\n      children: [\n        _buildBackButton(context),\n        _buildCloseButton(context),\n      ],\n    );\n  }\n\n  Widget _buildBackButton(BuildContext context) {\n    return GestureDetector(\n      onTap: Navigator.of(context).pop,\n      child: MouseRegion(\n        cursor: SystemMouseCursors.click,\n        child: Row(\n          children: [\n            const FlowySvg(FlowySvgs.arrow_back_m, size: Size.square(24)),\n            const HSpace(8),\n            FlowyText.semibold(LocaleKeys.button_back.tr(), fontSize: 16),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildCloseButton(BuildContext context) {\n    return GestureDetector(\n      onTap: Navigator.of(context).pop,\n      child: MouseRegion(\n        cursor: SystemMouseCursors.click,\n        child: FlowySvg(\n          FlowySvgs.m_close_m,\n          size: const Size.square(20),\n          color: Theme.of(context).colorScheme.outline,\n        ),\n      ),\n    );\n  }\n}\n\nclass _DialogTitle extends StatelessWidget {\n  const _DialogTitle();\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        Flexible(\n          child: FlowyText.medium(\n            LocaleKeys.settings_accountPage_login_loginLabel.tr(),\n            fontSize: 22,\n            color: Theme.of(context).colorScheme.tertiary,\n            maxLines: null,\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass _OrDivider extends StatelessWidget {\n  const _OrDivider();\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        const Flexible(child: Divider(thickness: 1)),\n        Padding(\n          padding: const EdgeInsets.symmetric(horizontal: 10),\n          child: FlowyText.regular(LocaleKeys.signIn_or.tr()),\n        ),\n        const Flexible(child: Divider(thickness: 1)),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_user_profile.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/workspace/application/user/settings_user_bloc.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../../../../shared/icon_emoji_picker/tab.dart';\n\n// Account name and account avatar\nclass AccountUserProfile extends StatefulWidget {\n  const AccountUserProfile({\n    super.key,\n    required this.name,\n    required this.iconUrl,\n    this.onSave,\n  });\n\n  final String name;\n  final String iconUrl;\n  final void Function(String)? onSave;\n\n  @override\n  State<AccountUserProfile> createState() => _AccountUserProfileState();\n}\n\nclass _AccountUserProfileState extends State<AccountUserProfile> {\n  late final TextEditingController nameController =\n      TextEditingController(text: widget.name);\n  final FocusNode focusNode = FocusNode();\n  bool isEditing = false;\n  bool isHovering = false;\n\n  @override\n  void initState() {\n    super.initState();\n\n    focusNode\n      ..addListener(_handleFocusChange)\n      ..onKeyEvent = _handleKeyEvent;\n  }\n\n  @override\n  void dispose() {\n    nameController.dispose();\n    focusNode.removeListener(_handleFocusChange);\n    focusNode.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        _buildAvatar(),\n        const HSpace(16),\n        Flexible(\n          child: isEditing ? _buildEditingField() : _buildNameDisplay(),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildAvatar() {\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: () => _showIconPickerDialog(context),\n      child: FlowyHover(\n        resetHoverOnRebuild: false,\n        onHover: (state) => setState(() => isHovering = state),\n        style: HoverStyle(\n          hoverColor: Colors.transparent,\n          borderRadius: BorderRadius.circular(100),\n        ),\n        child: FlowyTooltip(\n          message:\n              LocaleKeys.settings_accountPage_general_changeProfilePicture.tr(),\n          verticalOffset: 28,\n          child: AFAvatar(\n            url: widget.iconUrl,\n            name: widget.name,\n            size: AFAvatarSize.l,\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildNameDisplay() {\n    final theme = AppFlowyTheme.of(context);\n    return Padding(\n      padding: const EdgeInsets.only(top: 12),\n      child: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Flexible(\n            child: Text(\n              widget.name,\n              overflow: TextOverflow.ellipsis,\n              style: theme.textStyle.body.standard(\n                color: theme.textColorScheme.primary,\n              ),\n            ),\n          ),\n          const HSpace(4),\n          AFGhostButton.normal(\n            size: AFButtonSize.s,\n            padding: EdgeInsets.all(theme.spacing.xs),\n            onTap: () => setState(() => isEditing = true),\n            builder: (context, isHovering, disabled) => FlowySvg(\n              FlowySvgs.toolbar_link_edit_m,\n              size: const Size.square(20),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildEditingField() {\n    return SettingsInputField(\n      textController: nameController,\n      value: widget.name,\n      focusNode: focusNode..requestFocus(),\n      onCancel: () => setState(() => isEditing = false),\n      onSave: (_) => _saveChanges(),\n    );\n  }\n\n  Future<void> _showIconPickerDialog(BuildContext context) {\n    return showDialog(\n      context: context,\n      builder: (dialogContext) => SimpleDialog(\n        children: [\n          Container(\n            height: 380,\n            width: 360,\n            margin: const EdgeInsets.all(0),\n            child: FlowyIconEmojiPicker(\n              tabs: const [PickerTabType.emoji],\n              onSelectedEmoji: (r) {\n                context\n                    .read<SettingsUserViewBloc>()\n                    .add(SettingsUserEvent.updateUserIcon(iconUrl: r.emoji));\n                Navigator.of(dialogContext).pop();\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  void _handleFocusChange() {\n    if (!focusNode.hasFocus && isEditing && mounted) {\n      _saveChanges();\n    }\n  }\n\n  KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) {\n    if (event is KeyDownEvent &&\n        event.logicalKey == LogicalKeyboardKey.escape &&\n        isEditing &&\n        mounted) {\n      setState(() => isEditing = false);\n      return KeyEventResult.handled;\n    }\n    return KeyEventResult.ignored;\n  }\n\n  void _saveChanges() {\n    widget.onSave?.call(nameController.text);\n    setState(() => isEditing = false);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/email/email_section.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/widgets.dart';\n\nclass SettingsEmailSection extends StatelessWidget {\n  const SettingsEmailSection({\n    super.key,\n    required this.userProfile,\n  });\n\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Text(\n          LocaleKeys.settings_accountPage_email_title.tr(),\n          style: theme.textStyle.body.enhanced(\n            color: theme.textColorScheme.primary,\n          ),\n        ),\n        VSpace(theme.spacing.s),\n        Text(\n          userProfile.email,\n          style: theme.textStyle.caption.standard(\n            color: theme.textColorScheme.secondary,\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/change_password.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/application/password/password_bloc.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/password/error_extensions.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass ChangePasswordDialogContent extends StatefulWidget {\n  const ChangePasswordDialogContent({\n    super.key,\n    required this.userProfile,\n    this.showTitle = true,\n    this.showCloseAndSaveButton = true,\n    this.showSaveButton = false,\n    this.padding = const EdgeInsets.symmetric(horizontal: 20, vertical: 16),\n  });\n\n  final UserProfilePB userProfile;\n\n  // display the title\n  final bool showTitle;\n\n  // display the desktop style close and save button\n  final bool showCloseAndSaveButton;\n\n  // display the mobile style save button\n  final bool showSaveButton;\n\n  final EdgeInsets padding;\n\n  @override\n  State<ChangePasswordDialogContent> createState() =>\n      _ChangePasswordDialogContentState();\n}\n\nclass _ChangePasswordDialogContentState\n    extends State<ChangePasswordDialogContent> {\n  final currentPasswordTextFieldKey = GlobalKey<AFTextFieldState>();\n  final newPasswordTextFieldKey = GlobalKey<AFTextFieldState>();\n  final confirmPasswordTextFieldKey = GlobalKey<AFTextFieldState>();\n\n  final currentPasswordController = TextEditingController();\n  final newPasswordController = TextEditingController();\n  final confirmPasswordController = TextEditingController();\n\n  final iconSize = 20.0;\n\n  @override\n  void dispose() {\n    currentPasswordController.dispose();\n    newPasswordController.dispose();\n    confirmPasswordController.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return BlocListener<PasswordBloc, PasswordState>(\n      listener: _onPasswordStateChanged,\n      child: Container(\n        padding: widget.padding,\n        constraints: const BoxConstraints(maxWidth: 400),\n        decoration: BoxDecoration(\n          borderRadius: BorderRadius.circular(theme.borderRadius.xl),\n        ),\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            if (widget.showTitle) ...[\n              _buildTitle(context),\n              VSpace(theme.spacing.xl),\n            ],\n            ..._buildCurrentPasswordFields(context),\n            VSpace(theme.spacing.xl),\n            ..._buildNewPasswordFields(context),\n            VSpace(theme.spacing.xl),\n            ..._buildConfirmPasswordFields(context),\n            VSpace(theme.spacing.xl),\n            if (widget.showCloseAndSaveButton) ...[\n              _buildSubmitButton(context),\n            ],\n            if (widget.showSaveButton) ...[\n              _buildSaveButton(context),\n            ],\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildTitle(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.spaceBetween,\n      children: [\n        Text(\n          LocaleKeys.newSettings_myAccount_password_changePassword.tr(),\n          style: theme.textStyle.heading4.prominent(\n            color: theme.textColorScheme.primary,\n          ),\n        ),\n        const Spacer(),\n        AFGhostButton.normal(\n          size: AFButtonSize.s,\n          padding: EdgeInsets.all(theme.spacing.xs),\n          onTap: () => Navigator.of(context).pop(),\n          builder: (context, isHovering, disabled) => FlowySvg(\n            FlowySvgs.password_close_m,\n            size: const Size.square(20),\n          ),\n        ),\n      ],\n    );\n  }\n\n  List<Widget> _buildCurrentPasswordFields(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return [\n      Text(\n        LocaleKeys.newSettings_myAccount_password_currentPassword.tr(),\n        style: theme.textStyle.caption.enhanced(\n          color: theme.textColorScheme.secondary,\n        ),\n      ),\n      VSpace(theme.spacing.xs),\n      AFTextField(\n        key: currentPasswordTextFieldKey,\n        controller: currentPasswordController,\n        hintText: LocaleKeys\n            .newSettings_myAccount_password_hint_enterYourCurrentPassword\n            .tr(),\n        keyboardType: TextInputType.visiblePassword,\n        obscureText: true,\n        autofillHints: const [AutofillHints.password],\n        suffixIconConstraints: BoxConstraints.tightFor(\n          width: iconSize + theme.spacing.m,\n          height: iconSize,\n        ),\n        suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon(\n          isObscured: isObscured,\n          onTap: () {\n            currentPasswordTextFieldKey.currentState?.syncObscured(!isObscured);\n          },\n        ),\n      ),\n    ];\n  }\n\n  List<Widget> _buildNewPasswordFields(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return [\n      Text(\n        LocaleKeys.newSettings_myAccount_password_newPassword.tr(),\n        style: theme.textStyle.caption.enhanced(\n          color: theme.textColorScheme.secondary,\n        ),\n      ),\n      VSpace(theme.spacing.xs),\n      AFTextField(\n        key: newPasswordTextFieldKey,\n        controller: newPasswordController,\n        hintText: LocaleKeys\n            .newSettings_myAccount_password_hint_enterYourNewPassword\n            .tr(),\n        keyboardType: TextInputType.visiblePassword,\n        obscureText: true,\n        autofillHints: const [AutofillHints.password],\n        suffixIconConstraints: BoxConstraints.tightFor(\n          width: iconSize + theme.spacing.m,\n          height: iconSize,\n        ),\n        suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon(\n          isObscured: isObscured,\n          onTap: () {\n            newPasswordTextFieldKey.currentState?.syncObscured(!isObscured);\n          },\n        ),\n      ),\n    ];\n  }\n\n  List<Widget> _buildConfirmPasswordFields(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return [\n      Text(\n        LocaleKeys.newSettings_myAccount_password_confirmNewPassword.tr(),\n        style: theme.textStyle.caption.enhanced(\n          color: theme.textColorScheme.secondary,\n        ),\n      ),\n      VSpace(theme.spacing.xs),\n      AFTextField(\n        key: confirmPasswordTextFieldKey,\n        controller: confirmPasswordController,\n        hintText: LocaleKeys\n            .newSettings_myAccount_password_hint_confirmYourNewPassword\n            .tr(),\n        keyboardType: TextInputType.visiblePassword,\n        obscureText: true,\n        autofillHints: const [AutofillHints.password],\n        suffixIconConstraints: BoxConstraints.tightFor(\n          width: iconSize + theme.spacing.m,\n          height: iconSize,\n        ),\n        suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon(\n          isObscured: isObscured,\n          onTap: () {\n            confirmPasswordTextFieldKey.currentState?.syncObscured(!isObscured);\n          },\n        ),\n      ),\n    ];\n  }\n\n  Widget _buildSubmitButton(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.end,\n      children: [\n        AFOutlinedTextButton.normal(\n          text: LocaleKeys.button_cancel.tr(),\n          textStyle: theme.textStyle.body.standard(\n            color: theme.textColorScheme.primary,\n            weight: FontWeight.w400,\n          ),\n          onTap: () => Navigator.of(context).pop(),\n        ),\n        HSpace(theme.spacing.l),\n        AFFilledTextButton.primary(\n          text: LocaleKeys.button_save.tr(),\n          textStyle: theme.textStyle.body.standard(\n            color: theme.textColorScheme.onFill,\n            weight: FontWeight.w400,\n          ),\n          onTap: () => _save(context),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildSaveButton(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return AFFilledTextButton.primary(\n      text: LocaleKeys.button_save.tr(),\n      textStyle: theme.textStyle.body.standard(\n        color: theme.textColorScheme.onFill,\n      ),\n      size: AFButtonSize.l,\n      alignment: Alignment.center,\n      onTap: () => _save(context),\n    );\n  }\n\n  void _save(BuildContext context) async {\n    _resetError();\n\n    final currentPassword = currentPasswordController.text;\n    final newPassword = newPasswordController.text;\n    final confirmPassword = confirmPasswordController.text;\n\n    if (currentPassword.isEmpty) {\n      currentPasswordTextFieldKey.currentState?.syncError(\n        errorText: LocaleKeys\n            .newSettings_myAccount_password_error_currentPasswordIsRequired\n            .tr(),\n      );\n      return;\n    }\n\n    if (newPassword.isEmpty) {\n      newPasswordTextFieldKey.currentState?.syncError(\n        errorText: LocaleKeys\n            .newSettings_myAccount_password_error_newPasswordIsRequired\n            .tr(),\n      );\n      return;\n    }\n\n    if (confirmPassword.isEmpty) {\n      confirmPasswordTextFieldKey.currentState?.syncError(\n        errorText: LocaleKeys\n            .newSettings_myAccount_password_error_confirmPasswordIsRequired\n            .tr(),\n      );\n      return;\n    }\n\n    if (newPassword != confirmPassword) {\n      confirmPasswordTextFieldKey.currentState?.syncError(\n        errorText: LocaleKeys\n            .newSettings_myAccount_password_error_passwordsDoNotMatch\n            .tr(),\n      );\n      return;\n    }\n\n    if (newPassword == currentPassword) {\n      newPasswordTextFieldKey.currentState?.syncError(\n        errorText: LocaleKeys\n            .newSettings_myAccount_password_error_newPasswordIsSameAsCurrent\n            .tr(),\n      );\n      return;\n    }\n\n    // all the verification passed, save the new password\n    context.read<PasswordBloc>().add(\n          PasswordEvent.changePassword(\n            oldPassword: currentPassword,\n            newPassword: newPassword,\n          ),\n        );\n  }\n\n  void _resetError() {\n    currentPasswordTextFieldKey.currentState?.clearError();\n    newPasswordTextFieldKey.currentState?.clearError();\n    confirmPasswordTextFieldKey.currentState?.clearError();\n  }\n\n  void _onPasswordStateChanged(BuildContext context, PasswordState state) {\n    bool hasError = false;\n    String message = '';\n\n    final changePasswordResult = state.changePasswordResult;\n    final setPasswordResult = state.setupPasswordResult;\n\n    if (changePasswordResult != null) {\n      changePasswordResult.fold(\n        (success) {\n          message = LocaleKeys\n              .newSettings_myAccount_password_toast_passwordUpdatedSuccessfully\n              .tr();\n        },\n        (error) {\n          hasError = true;\n          message = LocaleKeys\n              .newSettings_myAccount_password_toast_passwordUpdatedFailed\n              .tr();\n\n          if (AFPasswordErrorExtension.incorrectPasswordPattern\n              .hasMatch(error.msg)) {\n            currentPasswordTextFieldKey.currentState?.syncError(\n              errorText: AFPasswordErrorExtension.getErrorMessage(error),\n            );\n          } else if (AFPasswordErrorExtension.tooShortPasswordPattern\n              .hasMatch(error.msg)) {\n            newPasswordTextFieldKey.currentState?.syncError(\n              errorText: AFPasswordErrorExtension.getErrorMessage(error),\n            );\n          } else if (AFPasswordErrorExtension.tooLongPasswordPattern\n              .hasMatch(error.msg)) {\n            newPasswordTextFieldKey.currentState?.syncError(\n              errorText: AFPasswordErrorExtension.getErrorMessage(error),\n            );\n          } else if (error.code == ErrorCode.NewPasswordTooWeak) {\n            newPasswordTextFieldKey.currentState?.syncError(\n              errorText: LocaleKeys.signIn_passwordMustContain.tr(),\n            );\n          } else {\n            newPasswordTextFieldKey.currentState?.syncError(\n              errorText: error.msg,\n            );\n          }\n        },\n      );\n    } else if (setPasswordResult != null) {\n      setPasswordResult.fold(\n        (success) {\n          message = LocaleKeys\n              .newSettings_myAccount_password_toast_passwordSetupSuccessfully\n              .tr();\n        },\n        (error) {\n          hasError = true;\n        },\n      );\n    }\n\n    if (!state.isSubmitting && message.isNotEmpty) {\n      if (!hasError) {\n        showToastNotification(\n          message: message,\n        );\n        Navigator.of(context).pop();\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/error_extensions.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nclass AFPasswordErrorExtension {\n  static final RegExp incorrectPasswordPattern =\n      RegExp('Incorrect current password');\n  static final RegExp tooShortPasswordPattern =\n      RegExp(r'Password should be at least (\\d+) characters');\n  static final RegExp tooLongPasswordPattern =\n      RegExp(r'Password cannot be longer than (\\d+) characters');\n\n  static String getErrorMessage(FlowyError error) {\n    final msg = error.msg;\n    if (incorrectPasswordPattern.hasMatch(msg)) {\n      return LocaleKeys\n          .newSettings_myAccount_password_error_currentPasswordIsIncorrect\n          .tr();\n    } else if (tooShortPasswordPattern.hasMatch(msg)) {\n      return LocaleKeys\n          .newSettings_myAccount_password_error_passwordShouldBeAtLeast6Characters\n          .tr(\n        namedArgs: {\n          'min': tooShortPasswordPattern.firstMatch(msg)?.group(1) ?? '6',\n        },\n      );\n    } else if (tooLongPasswordPattern.hasMatch(msg)) {\n      return LocaleKeys\n          .newSettings_myAccount_password_error_passwordCannotBeLongerThan72Characters\n          .tr(\n        namedArgs: {\n          'max': tooLongPasswordPattern.firstMatch(msg)?.group(1) ?? '72',\n        },\n      );\n    }\n\n    return msg;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass PasswordSuffixIcon extends StatelessWidget {\n  const PasswordSuffixIcon({\n    super.key,\n    required this.isObscured,\n    required this.onTap,\n  });\n\n  final bool isObscured;\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return MouseRegion(\n      cursor: SystemMouseCursors.click,\n      child: GestureDetector(\n        onTap: onTap,\n        behavior: HitTestBehavior.opaque,\n        child: Padding(\n          padding: EdgeInsets.only(right: theme.spacing.m),\n          child: FlowySvg(\n            isObscured ? FlowySvgs.show_s : FlowySvgs.hide_s,\n            color: theme.textColorScheme.secondary,\n            size: const Size.square(20),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/setup_password.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/application/password/password_bloc.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/password/error_extensions.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SetupPasswordDialogContent extends StatefulWidget {\n  const SetupPasswordDialogContent({\n    super.key,\n    required this.userProfile,\n    this.showCloseAndSaveButton = true,\n    this.showSaveButton = false,\n    this.showTitle = true,\n    this.padding = const EdgeInsets.symmetric(horizontal: 20, vertical: 16),\n  });\n\n  final UserProfilePB userProfile;\n\n  // display the desktop style close and save button\n  final bool showCloseAndSaveButton;\n\n  // display the mobile style save button\n  final bool showSaveButton;\n\n  // display the title\n  final bool showTitle;\n\n  // padding\n  final EdgeInsets padding;\n\n  @override\n  State<SetupPasswordDialogContent> createState() =>\n      _SetupPasswordDialogContentState();\n}\n\nclass _SetupPasswordDialogContentState\n    extends State<SetupPasswordDialogContent> {\n  final passwordTextFieldKey = GlobalKey<AFTextFieldState>();\n  final confirmPasswordTextFieldKey = GlobalKey<AFTextFieldState>();\n\n  final passwordController = TextEditingController();\n  final confirmPasswordController = TextEditingController();\n\n  final iconSize = 20.0;\n\n  @override\n  void dispose() {\n    passwordController.dispose();\n    confirmPasswordController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return BlocListener<PasswordBloc, PasswordState>(\n      listener: _onPasswordStateChanged,\n      child: Container(\n        padding: widget.padding,\n        constraints: const BoxConstraints(maxWidth: 400),\n        decoration: BoxDecoration(\n          borderRadius: BorderRadius.circular(theme.borderRadius.xl),\n        ),\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            if (widget.showTitle) ...[\n              _buildTitle(context),\n              VSpace(theme.spacing.xl),\n            ],\n            ..._buildPasswordFields(context),\n            VSpace(theme.spacing.xl),\n            ..._buildConfirmPasswordFields(context),\n            VSpace(theme.spacing.xl),\n            if (widget.showCloseAndSaveButton) ...[\n              _buildSubmitButton(context),\n            ],\n            if (widget.showSaveButton) ...[\n              _buildSaveButton(context),\n            ],\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildTitle(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.spaceBetween,\n      children: [\n        Text(\n          LocaleKeys.newSettings_myAccount_password_setupPassword.tr(),\n          style: theme.textStyle.heading4.prominent(\n            color: theme.textColorScheme.primary,\n          ),\n        ),\n        const Spacer(),\n        AFGhostButton.normal(\n          size: AFButtonSize.s,\n          padding: EdgeInsets.all(theme.spacing.xs),\n          onTap: () => Navigator.of(context).pop(),\n          builder: (context, isHovering, disabled) => FlowySvg(\n            FlowySvgs.password_close_m,\n            size: const Size.square(20),\n          ),\n        ),\n      ],\n    );\n  }\n\n  List<Widget> _buildPasswordFields(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return [\n      Text(\n        LocaleKeys.newSettings_myAccount_password_title.tr(),\n        style: theme.textStyle.caption.enhanced(\n          color: theme.textColorScheme.secondary,\n        ),\n      ),\n      VSpace(theme.spacing.xs),\n      AFTextField(\n        key: passwordTextFieldKey,\n        controller: passwordController,\n        hintText: LocaleKeys\n            .newSettings_myAccount_password_hint_confirmYourPassword\n            .tr(),\n        keyboardType: TextInputType.visiblePassword,\n        obscureText: true,\n        autofillHints: const [AutofillHints.password],\n        suffixIconConstraints: BoxConstraints.tightFor(\n          width: iconSize + theme.spacing.m,\n          height: iconSize,\n        ),\n        suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon(\n          isObscured: isObscured,\n          onTap: () {\n            passwordTextFieldKey.currentState?.syncObscured(!isObscured);\n          },\n        ),\n      ),\n    ];\n  }\n\n  List<Widget> _buildConfirmPasswordFields(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return [\n      Text(\n        LocaleKeys.newSettings_myAccount_password_confirmPassword.tr(),\n        style: theme.textStyle.caption.enhanced(\n          color: theme.textColorScheme.secondary,\n        ),\n      ),\n      VSpace(theme.spacing.xs),\n      AFTextField(\n        key: confirmPasswordTextFieldKey,\n        controller: confirmPasswordController,\n        hintText: LocaleKeys\n            .newSettings_myAccount_password_hint_confirmYourPassword\n            .tr(),\n        keyboardType: TextInputType.visiblePassword,\n        obscureText: true,\n        autofillHints: const [AutofillHints.password],\n        suffixIconConstraints: BoxConstraints.tightFor(\n          width: iconSize + theme.spacing.m,\n          height: iconSize,\n        ),\n        suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon(\n          isObscured: isObscured,\n          onTap: () {\n            confirmPasswordTextFieldKey.currentState?.syncObscured(!isObscured);\n          },\n        ),\n      ),\n    ];\n  }\n\n  Widget _buildSubmitButton(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.end,\n      children: [\n        AFOutlinedTextButton.normal(\n          text: LocaleKeys.button_cancel.tr(),\n          textStyle: theme.textStyle.body.standard(\n            color: theme.textColorScheme.primary,\n            weight: FontWeight.w400,\n          ),\n          onTap: () => Navigator.of(context).pop(),\n        ),\n        HSpace(theme.spacing.l),\n        AFFilledTextButton.primary(\n          text: LocaleKeys.button_save.tr(),\n          textStyle: theme.textStyle.body.standard(\n            color: theme.textColorScheme.onFill,\n            weight: FontWeight.w400,\n          ),\n          onTap: () => _save(context),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildSaveButton(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return AFFilledTextButton.primary(\n      text: LocaleKeys.button_save.tr(),\n      textStyle: theme.textStyle.body.standard(\n        color: theme.textColorScheme.onFill,\n      ),\n      size: AFButtonSize.l,\n      alignment: Alignment.center,\n      onTap: () => _save(context),\n    );\n  }\n\n  void _save(BuildContext context) async {\n    _resetError();\n\n    final password = passwordController.text;\n    final confirmPassword = confirmPasswordController.text;\n\n    if (password.isEmpty) {\n      passwordTextFieldKey.currentState?.syncError(\n        errorText: LocaleKeys\n            .newSettings_myAccount_password_error_newPasswordIsRequired\n            .tr(),\n      );\n      return;\n    }\n\n    if (confirmPassword.isEmpty) {\n      confirmPasswordTextFieldKey.currentState?.syncError(\n        errorText: LocaleKeys\n            .newSettings_myAccount_password_error_confirmPasswordIsRequired\n            .tr(),\n      );\n      return;\n    }\n\n    if (password != confirmPassword) {\n      confirmPasswordTextFieldKey.currentState?.syncError(\n        errorText: LocaleKeys\n            .newSettings_myAccount_password_error_passwordsDoNotMatch\n            .tr(),\n      );\n      return;\n    }\n\n    // all the verification passed, save the password\n    context.read<PasswordBloc>().add(\n          PasswordEvent.setupPassword(\n            newPassword: password,\n          ),\n        );\n  }\n\n  void _resetError() {\n    passwordTextFieldKey.currentState?.clearError();\n    confirmPasswordTextFieldKey.currentState?.clearError();\n  }\n\n  void _onPasswordStateChanged(BuildContext context, PasswordState state) {\n    bool hasError = false;\n    String message = '';\n\n    final setPasswordResult = state.setupPasswordResult;\n\n    if (setPasswordResult != null) {\n      setPasswordResult.fold(\n        (success) {\n          message = LocaleKeys\n              .newSettings_myAccount_password_toast_passwordSetupSuccessfully\n              .tr();\n        },\n        (error) {\n          hasError = true;\n          if (AFPasswordErrorExtension.tooShortPasswordPattern\n              .hasMatch(error.msg)) {\n            passwordTextFieldKey.currentState?.syncError(\n              errorText: AFPasswordErrorExtension.getErrorMessage(error),\n            );\n          } else if (AFPasswordErrorExtension.tooLongPasswordPattern\n              .hasMatch(error.msg)) {\n            passwordTextFieldKey.currentState?.syncError(\n              errorText: AFPasswordErrorExtension.getErrorMessage(error),\n            );\n          } else if (error.code == ErrorCode.NewPasswordTooWeak) {\n            passwordTextFieldKey.currentState?.syncError(\n              errorText: LocaleKeys.signIn_passwordMustContain.tr(),\n            );\n          } else {\n            passwordTextFieldKey.currentState?.syncError(\n              errorText: error.msg,\n            );\n          }\n        },\n      );\n    }\n\n    if (!state.isSubmitting && message.isNotEmpty) {\n      if (!hasError) {\n        showToastNotification(\n          message: message,\n        );\n\n        Navigator.of(context).pop();\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/fix_data_widget.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nclass FixDataWidget extends StatelessWidget {\n  const FixDataWidget({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return SettingsCategory(\n      title: LocaleKeys.settings_manageDataPage_data_fixYourData.tr(),\n      children: [\n        SingleSettingAction(\n          labelMaxLines: 4,\n          label: LocaleKeys.settings_manageDataPage_data_fixYourDataDescription\n              .tr(),\n          buttonLabel: LocaleKeys.settings_manageDataPage_data_fixButton.tr(),\n          onPressed: () {\n            WorkspaceDataManager.checkWorkspaceHealth(dryRun: true);\n          },\n        ),\n      ],\n    );\n  }\n}\n\nclass WorkspaceDataManager {\n  static Future<void> checkWorkspaceHealth({\n    required bool dryRun,\n  }) async {\n    try {\n      final currentWorkspace =\n          await UserBackendService.getCurrentWorkspace().getOrThrow();\n      // get all the views in the workspace\n      final result = await ViewBackendService.getAllViews().getOrThrow();\n      final allViews = result.items;\n\n      // dump all the views in the workspace\n      dumpViews('all views', allViews);\n\n      // get the workspace\n      final workspaces = allViews.where(\n        (e) => e.parentViewId == '' && e.id == currentWorkspace.id,\n      );\n      dumpViews('workspaces', workspaces.toList());\n\n      if (workspaces.length != 1) {\n        Log.error('Failed to fix workspace: workspace not found');\n        // there should be only one workspace\n        return;\n      }\n\n      final workspace = workspaces.first;\n\n      // check the health of the spaces\n      await checkSpaceHealth(workspace: workspace, allViews: allViews);\n\n      // check the health of the views\n      await checkViewHealth(workspace: workspace, allViews: allViews);\n\n      // add other checks here\n      // ...\n    } catch (e) {\n      Log.error('Failed to fix space relation: $e');\n    }\n  }\n\n  static Future<void> checkSpaceHealth({\n    required ViewPB workspace,\n    required List<ViewPB> allViews,\n    bool dryRun = true,\n  }) async {\n    try {\n      final workspaceChildViews =\n          await ViewBackendService.getChildViews(viewId: workspace.id)\n              .getOrThrow();\n      final workspaceChildViewIds =\n          workspaceChildViews.map((e) => e.id).toSet();\n      final spaces = allViews.where((e) => e.isSpace).toList();\n\n      for (final space in spaces) {\n        // the space is the top level view, so its parent view id should be the workspace id\n        // and the workspace should have the space in its child views\n        if (space.parentViewId != workspace.id ||\n            !workspaceChildViewIds.contains(space.id)) {\n          Log.info('found an issue: space is not in the workspace: $space');\n          if (!dryRun) {\n            // move the space to the workspace if it is not in the workspace\n            await ViewBackendService.moveViewV2(\n              viewId: space.id,\n              newParentId: workspace.id,\n              prevViewId: null,\n            );\n          }\n          workspaceChildViewIds.add(space.id);\n        }\n      }\n    } catch (e) {\n      Log.error('Failed to check space health: $e');\n    }\n  }\n\n  static Future<List<ViewPB>> checkViewHealth({\n    ViewPB? workspace,\n    List<ViewPB>? allViews,\n    bool dryRun = true,\n  }) async {\n    // Views whose parent view does not have the view in its child views\n    final List<ViewPB> unlistedChildViews = [];\n    // Views whose parent is not in allViews\n    final List<ViewPB> orphanViews = [];\n    // Row pages\n    final List<ViewPB> rowPageViews = [];\n\n    try {\n      if (workspace == null || allViews == null) {\n        final currentWorkspace =\n            await UserBackendService.getCurrentWorkspace().getOrThrow();\n        // get all the views in the workspace\n        final result = await ViewBackendService.getAllViews().getOrThrow();\n        allViews = result.items;\n        workspace = allViews.firstWhereOrNull(\n          (e) => e.id == currentWorkspace.id,\n        );\n      }\n\n      for (final view in allViews) {\n        if (view.parentViewId == '') {\n          continue;\n        }\n\n        final parentView = allViews.firstWhereOrNull(\n          (e) => e.id == view.parentViewId,\n        );\n\n        if (parentView == null) {\n          orphanViews.add(view);\n          continue;\n        }\n\n        if (parentView.id == view.id) {\n          rowPageViews.add(view);\n          continue;\n        }\n\n        final childViewsOfParent =\n            await ViewBackendService.getChildViews(viewId: parentView.id)\n                .getOrThrow();\n        final result = childViewsOfParent.any((e) => e.id == view.id);\n        if (!result) {\n          unlistedChildViews.add(view);\n        }\n      }\n    } catch (e) {\n      Log.error('Failed to check space health: $e');\n      return [];\n    }\n\n    for (final view in unlistedChildViews) {\n      Log.info(\n        '[workspace] found an issue: view is not in the parent view\\'s child views, view: ${view.toProto3Json()}}',\n      );\n    }\n\n    for (final view in orphanViews) {\n      Log.info('[workspace] orphanViews: ${view.toProto3Json()}');\n    }\n\n    for (final view in rowPageViews) {\n      Log.info('[workspace] rowPageViews: ${view.toProto3Json()}');\n    }\n\n    if (!dryRun && unlistedChildViews.isNotEmpty) {\n      Log.info(\n        '[workspace] start to fix ${unlistedChildViews.length} unlistedChildViews ...',\n      );\n      for (final view in unlistedChildViews) {\n        // move the view to the parent view if it is not in the parent view's child views\n        Log.info(\n          '[workspace] move view: $view to its parent view ${view.parentViewId}',\n        );\n        await ViewBackendService.moveViewV2(\n          viewId: view.id,\n          newParentId: view.parentViewId,\n          prevViewId: null,\n        );\n      }\n\n      Log.info('[workspace] end to fix unlistedChildViews');\n    }\n\n    if (unlistedChildViews.isEmpty && orphanViews.isEmpty) {\n      Log.info('[workspace] all views are healthy');\n    }\n\n    Log.info('[workspace] done checking view health');\n\n    return unlistedChildViews;\n  }\n\n  static void dumpViews(String prefix, List<ViewPB> views) {\n    for (int i = 0; i < views.length; i++) {\n      final view = views[i];\n      Log.info('$prefix $i: $view)');\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:expandable/expandable.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'ollama_setting.dart';\nimport 'plugin_status_indicator.dart';\n\nclass LocalAISetting extends StatefulWidget {\n  const LocalAISetting({super.key});\n\n  @override\n  State<LocalAISetting> createState() => _LocalAISettingState();\n}\n\nclass _LocalAISettingState extends State<LocalAISetting> {\n  final expandableController = ExpandableController(initialExpanded: false);\n\n  @override\n  void dispose() {\n    expandableController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => LocalAiPluginBloc(),\n      child: BlocConsumer<LocalAiPluginBloc, LocalAiPluginState>(\n        listener: (context, state) {\n          expandableController.value = state.isEnabled;\n        },\n        builder: (context, state) {\n          return ExpandablePanel(\n            controller: expandableController,\n            theme: ExpandableThemeData(\n              tapBodyToCollapse: false,\n              hasIcon: false,\n              tapBodyToExpand: false,\n              tapHeaderToExpand: false,\n            ),\n            header: LocalAiSettingHeader(\n              isEnabled: state.isEnabled,\n            ),\n            collapsed: const SizedBox.shrink(),\n            expanded: Padding(\n              padding: EdgeInsets.only(top: 12),\n              child: LocalAISettingPanel(),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass LocalAiSettingHeader extends StatelessWidget {\n  const LocalAiSettingHeader({\n    super.key,\n    required this.isEnabled,\n  });\n\n  final bool isEnabled;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return Row(\n      children: [\n        Expanded(\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Row(\n                children: [\n                  Text(\n                    LocaleKeys.settings_aiPage_keys_localAIToggleTitle.tr(),\n                    style: theme.textStyle.body.enhanced(\n                      color: theme.textColorScheme.primary,\n                    ),\n                  ),\n                  HSpace(theme.spacing.s),\n                  FlowyTooltip(\n                    message: LocaleKeys.workspace_learnMore.tr(),\n                    child: AFGhostButton.normal(\n                      padding: EdgeInsets.zero,\n                      builder: (context, isHovering, disabled) {\n                        return FlowySvg(\n                          FlowySvgs.ai_explain_m,\n                          size: Size.square(20),\n                        );\n                      },\n                      onTap: () {\n                        afLaunchUrlString(\n                          'https://appflowy.com/guide/appflowy-local-ai-ollama',\n                        );\n                      },\n                    ),\n                  ),\n                ],\n              ),\n              const VSpace(4),\n              FlowyText(\n                LocaleKeys.settings_aiPage_keys_localAIToggleSubTitle.tr(),\n                maxLines: 3,\n                fontSize: 12,\n              ),\n            ],\n          ),\n        ),\n        Toggle(\n          value: isEnabled,\n          onChanged: (value) {\n            _onToggleChanged(value, context);\n          },\n        ),\n      ],\n    );\n  }\n\n  void _onToggleChanged(bool value, BuildContext context) {\n    if (value) {\n      context.read<LocalAiPluginBloc>().add(const LocalAiPluginEvent.toggle());\n    } else {\n      showConfirmDialog(\n        context: context,\n        title: LocaleKeys.settings_aiPage_keys_disableLocalAITitle.tr(),\n        description:\n            LocaleKeys.settings_aiPage_keys_disableLocalAIDescription.tr(),\n        confirmLabel: LocaleKeys.button_confirm.tr(),\n        onConfirm: (_) {\n          context\n              .read<LocalAiPluginBloc>()\n              .add(const LocalAiPluginEvent.toggle());\n        },\n      );\n    }\n  }\n}\n\nclass LocalAISettingPanel extends StatelessWidget {\n  const LocalAISettingPanel({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<LocalAiPluginBloc, LocalAiPluginState>(\n      builder: (context, state) {\n        if (state is! ReadyLocalAiPluginState) {\n          return const SizedBox.shrink();\n        }\n\n        return Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            const LocalAIStatusIndicator(),\n            const VSpace(10),\n            OllamaSettingPage(),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_settings_ai_view.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass LocalSettingsAIView extends StatelessWidget {\n  const LocalSettingsAIView({\n    super.key,\n    required this.userProfile,\n    required this.workspaceId,\n  });\n\n  final UserProfilePB userProfile;\n  final String workspaceId;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<SettingsAIBloc>(\n      create: (_) => SettingsAIBloc(userProfile, workspaceId)\n        ..add(const SettingsAIEvent.started()),\n      child: SettingsBody(\n        title: LocaleKeys.settings_aiPage_title.tr(),\n        description: \"\",\n        children: [\n          const LocalAISetting(),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart",
    "content": "import 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';\nimport 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass AIModelSelection extends StatelessWidget {\n  const AIModelSelection({super.key});\n  static const double height = 49;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SettingsAIBloc, SettingsAIState>(\n      builder: (context, state) {\n        final models = state.availableModels?.models;\n        if (models == null) {\n          return const SizedBox(\n            // Using same height as SettingsDropdown to avoid layout shift\n            height: height,\n          );\n        }\n\n        final localModels = models.where((model) => model.isLocal).toList();\n        final cloudModels = models.where((model) => !model.isLocal).toList();\n        final selectedModel = state.availableModels!.selectedModel;\n\n        return Padding(\n          padding: const EdgeInsets.symmetric(vertical: 6),\n          child: Row(\n            children: [\n              Expanded(\n                child: FlowyText.medium(\n                  LocaleKeys.settings_aiPage_keys_llmModelType.tr(),\n                  overflow: TextOverflow.ellipsis,\n                ),\n              ),\n              Flexible(\n                child: SettingsDropdown<AIModelPB>(\n                  key: ValueKey(selectedModel.name),\n                  onChanged: (model) => context\n                      .read<SettingsAIBloc>()\n                      .add(SettingsAIEvent.selectModel(model)),\n                  selectedOption: selectedModel,\n                  selectOptionCompare: (left, right) =>\n                      left?.name == right?.name,\n                  options: [...localModels, ...cloudModels]\n                      .map(\n                        (model) => buildDropdownMenuEntry<AIModelPB>(\n                          context,\n                          value: model,\n                          label:\n                              model.isLocal ? \"${model.i18n} 🔐\" : model.i18n,\n                          subLabel: model.desc,\n                          maximumHeight: height,\n                        ),\n                      )\n                      .toList(),\n                ),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart",
    "content": "import 'package:appflowy/workspace/application/settings/ai/ollama_setting_bloc.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/style_widget/text_field.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nclass OllamaSettingPage extends StatelessWidget {\n  const OllamaSettingPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) =>\n          OllamaSettingBloc()..add(const OllamaSettingEvent.started()),\n      child: BlocBuilder<OllamaSettingBloc, OllamaSettingState>(\n        buildWhen: (previous, current) =>\n            previous.inputItems != current.inputItems ||\n            previous.isEdited != current.isEdited,\n        builder: (context, state) {\n          return Container(\n            decoration: BoxDecoration(\n              color: Theme.of(context).colorScheme.surfaceContainerHighest,\n              borderRadius: const BorderRadius.all(Radius.circular(8.0)),\n            ),\n            padding: EdgeInsets.all(12),\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              spacing: 10,\n              children: [\n                for (final item in state.inputItems)\n                  _SettingItemWidget(item: item),\n                const LocalAIModelSelection(),\n                _SaveButton(isEdited: state.isEdited),\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _SettingItemWidget extends StatelessWidget {\n  const _SettingItemWidget({required this.item});\n\n  final SettingItem item;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      key: ValueKey(item.content + item.settingType.title),\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        FlowyText(\n          item.settingType.title,\n          fontSize: 12,\n          figmaLineHeight: 16,\n        ),\n        const VSpace(4),\n        SizedBox(\n          height: 32,\n          child: FlowyTooltip(\n            message: item.editable\n                ? null\n                : LocaleKeys.settings_aiPage_keys_readOnlyField.tr(),\n            child: FlowyTextField(\n              autoFocus: false,\n              hintText: item.hintText,\n              text: item.content,\n              readOnly: !item.editable,\n              onChanged: item.editable\n                  ? (content) {\n                      context.read<OllamaSettingBloc>().add(\n                            OllamaSettingEvent.onEdit(\n                              content,\n                              item.settingType,\n                            ),\n                          );\n                    }\n                  : null,\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass _SaveButton extends StatelessWidget {\n  const _SaveButton({required this.isEdited});\n\n  final bool isEdited;\n\n  @override\n  Widget build(BuildContext context) {\n    return Align(\n      alignment: AlignmentDirectional.centerEnd,\n      child: FlowyTooltip(\n        message: isEdited ? null : 'No changes',\n        child: SizedBox(\n          child: FlowyButton(\n            text: FlowyText(\n              'Apply',\n              figmaLineHeight: 20,\n              color: Theme.of(context).colorScheme.onPrimary,\n            ),\n            disable: !isEdited,\n            expandText: false,\n            margin: EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0),\n            backgroundColor: Theme.of(context).colorScheme.primary,\n            hoverColor: Theme.of(context).colorScheme.primary.withAlpha(200),\n            onTap: () {\n              if (isEdited) {\n                context\n                    .read<OllamaSettingBloc>()\n                    .add(const OllamaSettingEvent.submit());\n              }\n            },\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass LocalAIModelSelection extends StatelessWidget {\n  const LocalAIModelSelection({super.key});\n  static const double height = 49;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<OllamaSettingBloc, OllamaSettingState>(\n      buildWhen: (previous, current) =>\n          previous.localModels != current.localModels,\n      builder: (context, state) {\n        final models = state.localModels;\n        if (models == null) {\n          return const SizedBox(\n            // Using same height as SettingsDropdown to avoid layout shift\n            height: height,\n          );\n        }\n\n        return Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            FlowyText.medium(\n              LocaleKeys.settings_aiPage_keys_globalLLMModel.tr(),\n              fontSize: 12,\n              figmaLineHeight: 16,\n            ),\n            const VSpace(4),\n            SizedBox(\n              height: 40,\n              child: SettingsDropdown<AIModelPB>(\n                key: const Key('_AIModelSelection'),\n                textStyle: Theme.of(context).textTheme.bodySmall,\n                onChanged: (model) => context\n                    .read<OllamaSettingBloc>()\n                    .add(OllamaSettingEvent.setDefaultModel(model)),\n                selectedOption: models.selectedModel,\n                selectOptionCompare: (left, right) => left?.name == right?.name,\n                options: models.models\n                    .map(\n                      (model) => buildDropdownMenuEntry<AIModelPB>(\n                        context,\n                        value: model,\n                        label: model.i18n,\n                        subLabel: model.desc,\n                        maximumHeight: height,\n                      ),\n                    )\n                    .toList(),\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass LocalAIStatusIndicator extends StatelessWidget {\n  const LocalAIStatusIndicator({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<LocalAiPluginBloc, LocalAiPluginState>(\n      builder: (context, state) {\n        return state.maybeWhen(\n          ready: (_, isReady, lackOfResource) {\n            if (lackOfResource != null) {\n              return _LackOfResource(resource: lackOfResource);\n            }\n\n            return switch (isReady) {\n              true => const _LocalAIRunning(),\n              false => const _RestartPluginButton(),\n            };\n          },\n          orElse: () => const SizedBox.shrink(),\n        );\n      },\n    );\n  }\n}\n\nclass _RestartPluginButton extends StatelessWidget {\n  const _RestartPluginButton();\n\n  @override\n  Widget build(BuildContext context) {\n    final textStyle =\n        Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1.5);\n\n    return Container(\n      decoration: BoxDecoration(\n        color: Theme.of(context).isLightMode\n            ? const Color(0x80FFE7EE)\n            : const Color(0x80591734),\n        borderRadius: BorderRadius.all(Radius.circular(8.0)),\n      ),\n      padding: const EdgeInsets.all(8.0),\n      child: Row(\n        children: [\n          const FlowySvg(\n            FlowySvgs.toast_error_filled_s,\n            size: Size.square(20.0),\n            blendMode: null,\n          ),\n          const HSpace(8),\n          Expanded(\n            child: RichText(\n              maxLines: 3,\n              text: TextSpan(\n                children: [\n                  TextSpan(\n                    text:\n                        LocaleKeys.settings_aiPage_keys_failToLoadLocalAI.tr(),\n                    style: textStyle,\n                  ),\n                  TextSpan(\n                    text: ' ',\n                    style: textStyle,\n                  ),\n                  TextSpan(\n                    text: LocaleKeys.settings_aiPage_keys_restartLocalAI.tr(),\n                    style: textStyle?.copyWith(\n                      fontWeight: FontWeight.w600,\n                      decoration: TextDecoration.underline,\n                    ),\n                    recognizer: TapGestureRecognizer()\n                      ..onTap = () {\n                        context\n                            .read<LocalAiPluginBloc>()\n                            .add(const LocalAiPluginEvent.restart());\n                      },\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _LocalAIRunning extends StatelessWidget {\n  const _LocalAIRunning();\n\n  @override\n  Widget build(BuildContext context) {\n    final runningText = LocaleKeys.settings_aiPage_keys_localAIRunning.tr();\n\n    return Container(\n      decoration: const BoxDecoration(\n        color: Color(0xFFEDF7ED),\n        borderRadius: BorderRadius.all(Radius.circular(8.0)),\n      ),\n      padding: const EdgeInsets.all(8.0),\n      child: Row(\n        children: [\n          const FlowySvg(\n            FlowySvgs.download_success_s,\n            color: Color(0xFF2E7D32),\n          ),\n          const HSpace(6),\n          Expanded(\n            child: FlowyText(\n              runningText,\n              color: const Color(0xFF1E4620),\n              maxLines: 3,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _LackOfResource extends StatelessWidget {\n  const _LackOfResource({required this.resource});\n\n  final LackOfAIResourcePB resource;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      decoration: BoxDecoration(\n        color: Theme.of(context).isLightMode\n            ? const Color(0x80FFE7EE)\n            : const Color(0x80591734),\n        borderRadius: BorderRadius.all(Radius.circular(8.0)),\n      ),\n      padding: const EdgeInsets.all(8.0),\n      child: Row(\n        children: [\n          FlowySvg(\n            FlowySvgs.toast_error_filled_s,\n            size: const Size.square(20.0),\n            blendMode: null,\n          ),\n          const HSpace(8),\n          Expanded(\n            child: switch (resource.resourceType) {\n              LackOfAIResourceTypePB.PluginExecutableNotReady =>\n                _buildNoLAI(context),\n              LackOfAIResourceTypePB.OllamaServerNotReady =>\n                _buildNoOllama(context),\n              LackOfAIResourceTypePB.MissingModel =>\n                _buildNoModel(context, resource.missingModelNames),\n              _ => const SizedBox.shrink(),\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  TextStyle? _textStyle(BuildContext context) {\n    return Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1.5);\n  }\n\n  Widget _buildNoLAI(BuildContext context) {\n    final textStyle = _textStyle(context);\n    return RichText(\n      maxLines: 3,\n      text: TextSpan(\n        children: [\n          TextSpan(\n            text: LocaleKeys.settings_aiPage_keys_laiNotReady.tr(),\n            style: textStyle,\n          ),\n          TextSpan(text: ' ', style: _textStyle(context)),\n          ..._downloadInstructions(textStyle),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildNoOllama(BuildContext context) {\n    final textStyle = _textStyle(context);\n    return RichText(\n      maxLines: 3,\n      text: TextSpan(\n        children: [\n          TextSpan(\n            text: LocaleKeys.settings_aiPage_keys_ollamaNotReady.tr(),\n            style: textStyle,\n          ),\n          TextSpan(text: ' ', style: textStyle),\n          ..._downloadInstructions(textStyle),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildNoModel(BuildContext context, List<String> modelNames) {\n    final textStyle = _textStyle(context);\n\n    return RichText(\n      maxLines: 3,\n      text: TextSpan(\n        children: [\n          TextSpan(\n            text: LocaleKeys.settings_aiPage_keys_modelsMissing.tr(),\n            style: textStyle,\n          ),\n          TextSpan(\n            text: modelNames.join(', '),\n            style: textStyle,\n          ),\n          TextSpan(\n            text: ' ',\n            style: textStyle,\n          ),\n          TextSpan(\n            text: LocaleKeys.settings_aiPage_keys_pleaseFollowThese.tr(),\n            style: textStyle,\n          ),\n          TextSpan(\n            text: ' ',\n            style: textStyle,\n          ),\n          TextSpan(\n            text: LocaleKeys.settings_aiPage_keys_instructions.tr(),\n            style: textStyle?.copyWith(\n              fontWeight: FontWeight.w600,\n              decoration: TextDecoration.underline,\n            ),\n            recognizer: TapGestureRecognizer()\n              ..onTap = () {\n                afLaunchUrlString(\n                  \"https://appflowy.com/guide/appflowy-local-ai-ollama\",\n                );\n              },\n          ),\n          TextSpan(\n            text: ' ',\n            style: textStyle,\n          ),\n          TextSpan(\n            text: LocaleKeys.settings_aiPage_keys_downloadModel.tr(),\n            style: textStyle,\n          ),\n        ],\n      ),\n    );\n  }\n\n  List<TextSpan> _downloadInstructions(TextStyle? textStyle) {\n    return [\n      TextSpan(\n        text: LocaleKeys.settings_aiPage_keys_pleaseFollowThese.tr(),\n        style: textStyle,\n      ),\n      TextSpan(\n        text: ' ',\n        style: textStyle,\n      ),\n      TextSpan(\n        text: LocaleKeys.settings_aiPage_keys_instructions.tr(),\n        style: textStyle?.copyWith(\n          fontWeight: FontWeight.w600,\n          decoration: TextDecoration.underline,\n        ),\n        recognizer: TapGestureRecognizer()\n          ..onTap = () {\n            afLaunchUrlString(\n              \"https://appflowy.com/guide/appflowy-local-ai-ollama\",\n            );\n          },\n      ),\n      TextSpan(text: ' ', style: textStyle),\n      TextSpan(\n        text: LocaleKeys.settings_aiPage_keys_installOllamaLai.tr(),\n        style: textStyle,\n      ),\n    ];\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SettingsAIView extends StatelessWidget {\n  const SettingsAIView({\n    super.key,\n    required this.userProfile,\n    required this.currentWorkspaceMemberRole,\n    required this.workspaceId,\n  });\n\n  final UserProfilePB userProfile;\n  final AFRolePB? currentWorkspaceMemberRole;\n  final String workspaceId;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<SettingsAIBloc>(\n      create: (_) => SettingsAIBloc(userProfile, workspaceId)\n        ..add(const SettingsAIEvent.started()),\n      child: SettingsBody(\n        title: LocaleKeys.settings_aiPage_title.tr(),\n        description: LocaleKeys.settings_aiPage_keys_aiSettingsDescription.tr(),\n        children: [\n          const AIModelSelection(),\n          const _AISearchToggle(value: false),\n          const LocalAISetting(),\n        ],\n      ),\n    );\n  }\n}\n\nclass _AISearchToggle extends StatelessWidget {\n  const _AISearchToggle({required this.value});\n\n  final bool value;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        Row(\n          children: [\n            FlowyText.medium(\n              LocaleKeys.settings_aiPage_keys_enableAISearchTitle.tr(),\n            ),\n            const Spacer(),\n            BlocBuilder<SettingsAIBloc, SettingsAIState>(\n              builder: (context, state) {\n                if (state.aiSettings == null) {\n                  return const Padding(\n                    padding: EdgeInsets.only(top: 6),\n                    child: SizedBox(\n                      height: 26,\n                      width: 26,\n                      child: CircularProgressIndicator.adaptive(),\n                    ),\n                  );\n                } else {\n                  return Toggle(\n                    value: state.enableSearchIndexing,\n                    onChanged: (_) => context\n                        .read<SettingsAIBloc>()\n                        .add(const SettingsAIEvent.toggleAISearch()),\n                  );\n                }\n              },\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/user/settings_user_bloc.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/about/app_version.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/account.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/email/email_section.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SettingsAccountView extends StatefulWidget {\n  const SettingsAccountView({\n    super.key,\n    required this.userProfile,\n    required this.didLogin,\n    required this.didLogout,\n  });\n\n  final UserProfilePB userProfile;\n\n  // Called when the user signs in from the setting dialog\n  final VoidCallback didLogin;\n\n  // Called when the user logout in the setting dialog\n  final VoidCallback didLogout;\n\n  @override\n  State<SettingsAccountView> createState() => _SettingsAccountViewState();\n}\n\nclass _SettingsAccountViewState extends State<SettingsAccountView> {\n  late String userName = widget.userProfile.name;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<SettingsUserViewBloc>(\n      create: (context) =>\n          getIt<SettingsUserViewBloc>(param1: widget.userProfile)\n            ..add(const SettingsUserEvent.initial()),\n      child: BlocBuilder<SettingsUserViewBloc, SettingsUserState>(\n        builder: (context, state) {\n          return SettingsBody(\n            title: LocaleKeys.newSettings_myAccount_title.tr(),\n            children: [\n              // user profile\n              SettingsCategory(\n                title: LocaleKeys.newSettings_myAccount_myProfile.tr(),\n                children: [\n                  AccountUserProfile(\n                    name: userName,\n                    iconUrl: state.userProfile.iconUrl,\n                    onSave: (newName) {\n                      // Pseudo change the name to update the UI before the backend\n                      // processes the request. This is to give the user a sense of\n                      // immediate feedback, and avoid UI flickering.\n                      setState(() => userName = newName);\n                      context\n                          .read<SettingsUserViewBloc>()\n                          .add(SettingsUserEvent.updateUserName(name: newName));\n                    },\n                  ),\n                ],\n              ),\n\n              // user email\n              // Only show email if the user is authenticated and not using local auth\n              if (isAuthEnabled &&\n                  state.userProfile.userAuthType != AuthTypePB.Local) ...[\n                SettingsCategory(\n                  title: LocaleKeys.newSettings_myAccount_myAccount.tr(),\n                  children: [\n                    SettingsEmailSection(\n                      userProfile: state.userProfile,\n                    ),\n                    ChangePasswordSection(\n                      userProfile: state.userProfile,\n                    ),\n                    AccountSignInOutSection(\n                      userProfile: state.userProfile,\n                      onAction:\n                          state.userProfile.userAuthType == AuthTypePB.Local\n                              ? widget.didLogin\n                              : widget.didLogout,\n                      signIn:\n                          state.userProfile.userAuthType == AuthTypePB.Local,\n                    ),\n                  ],\n                ),\n              ],\n\n              if (isAuthEnabled &&\n                  state.userProfile.userAuthType == AuthTypePB.Local) ...[\n                SettingsCategory(\n                  title: LocaleKeys.settings_accountPage_login_title.tr(),\n                  children: [\n                    AccountSignInOutSection(\n                      userProfile: state.userProfile,\n                      onAction:\n                          state.userProfile.userAuthType == AuthTypePB.Local\n                              ? widget.didLogin\n                              : widget.didLogout,\n                      signIn:\n                          state.userProfile.userAuthType == AuthTypePB.Local,\n                    ),\n                  ],\n                ),\n              ],\n\n              // App version\n              SettingsCategory(\n                title: LocaleKeys.newSettings_myAccount_aboutAppFlowy.tr(),\n                children: const [\n                  SettingsAppVersion(),\n                ],\n              ),\n\n              // user deletion\n              if (widget.userProfile.userAuthType == AuthTypePB.Server)\n                const AccountDeletionButton(),\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart",
    "content": "import 'package:appflowy/shared/flowy_error_page.dart';\nimport 'package:appflowy/shared/loading.dart';\nimport 'package:appflowy/util/int64_extension.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/billing/settings_billing_bloc.dart';\nimport 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';\nimport 'package:appflowy/workspace/application/settings/plan/settings_plan_bloc.dart';\nimport 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_dashed_divider.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../../../generated/locale_keys.g.dart';\n\nconst _buttonsMinWidth = 100.0;\n\nclass SettingsBillingView extends StatefulWidget {\n  const SettingsBillingView({\n    super.key,\n    required this.workspaceId,\n    required this.user,\n  });\n\n  final String workspaceId;\n  final UserProfilePB user;\n\n  @override\n  State<SettingsBillingView> createState() => _SettingsBillingViewState();\n}\n\nclass _SettingsBillingViewState extends State<SettingsBillingView> {\n  Loading? loadingIndicator;\n  RecurringIntervalPB? selectedInterval;\n  final ValueNotifier<bool> enablePlanChangeNotifier = ValueNotifier(false);\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<SettingsBillingBloc>(\n      create: (_) => SettingsBillingBloc(\n        workspaceId: widget.workspaceId,\n        userId: widget.user.id,\n      )..add(const SettingsBillingEvent.started()),\n      child: BlocConsumer<SettingsBillingBloc, SettingsBillingState>(\n        listenWhen: (previous, current) =>\n            previous.mapOrNull(ready: (s) => s.isLoading) !=\n            current.mapOrNull(ready: (s) => s.isLoading),\n        listener: (context, state) {\n          if (state.mapOrNull(ready: (s) => s.isLoading) == true) {\n            loadingIndicator = Loading(context)..start();\n          } else {\n            loadingIndicator?.stop();\n            loadingIndicator = null;\n          }\n        },\n        builder: (context, state) {\n          return state.map(\n            initial: (_) => const SizedBox.shrink(),\n            loading: (_) => const Center(\n              child: SizedBox(\n                height: 24,\n                width: 24,\n                child: CircularProgressIndicator.adaptive(strokeWidth: 3),\n              ),\n            ),\n            error: (state) {\n              if (state.error != null) {\n                return Padding(\n                  padding: const EdgeInsets.all(16),\n                  child: Center(\n                    child: AppFlowyErrorPage(\n                      error: state.error!,\n                    ),\n                  ),\n                );\n              }\n\n              return ErrorWidget.withDetails(message: 'Something went wrong!');\n            },\n            ready: (state) {\n              final billingPortalEnabled =\n                  state.subscriptionInfo.isBillingPortalEnabled;\n\n              return SettingsBody(\n                title: LocaleKeys.settings_billingPage_title.tr(),\n                children: [\n                  SettingsCategory(\n                    title: LocaleKeys.settings_billingPage_plan_title.tr(),\n                    children: [\n                      SingleSettingAction(\n                        onPressed: () => _openPricingDialog(\n                          context,\n                          widget.workspaceId,\n                          widget.user.id,\n                          state.subscriptionInfo,\n                        ),\n                        fontWeight: FontWeight.w500,\n                        label: state.subscriptionInfo.label,\n                        buttonLabel: LocaleKeys\n                            .settings_billingPage_plan_planButtonLabel\n                            .tr(),\n                        minWidth: _buttonsMinWidth,\n                      ),\n                      if (billingPortalEnabled)\n                        SingleSettingAction(\n                          onPressed: () {\n                            SettingsAlertDialog(\n                              title: LocaleKeys\n                                  .settings_billingPage_changePeriod\n                                  .tr(),\n                              enableConfirmNotifier: enablePlanChangeNotifier,\n                              children: [\n                                ChangePeriod(\n                                  plan: state.subscriptionInfo.planSubscription\n                                      .subscriptionPlan,\n                                  selectedInterval: state.subscriptionInfo\n                                      .planSubscription.interval,\n                                  onSelected: (interval) {\n                                    enablePlanChangeNotifier.value = interval !=\n                                        state.subscriptionInfo.planSubscription\n                                            .interval;\n                                    selectedInterval = interval;\n                                  },\n                                ),\n                              ],\n                              confirm: () {\n                                if (selectedInterval !=\n                                    state.subscriptionInfo.planSubscription\n                                        .interval) {\n                                  context.read<SettingsBillingBloc>().add(\n                                        SettingsBillingEvent.updatePeriod(\n                                          plan: state\n                                              .subscriptionInfo\n                                              .planSubscription\n                                              .subscriptionPlan,\n                                          interval: selectedInterval!,\n                                        ),\n                                      );\n                                }\n                                Navigator.of(context).pop();\n                              },\n                            ).show(context);\n                          },\n                          label: LocaleKeys\n                              .settings_billingPage_plan_billingPeriod\n                              .tr(),\n                          description: state\n                              .subscriptionInfo.planSubscription.interval.label,\n                          fontWeight: FontWeight.w500,\n                          buttonLabel: LocaleKeys\n                              .settings_billingPage_plan_periodButtonLabel\n                              .tr(),\n                          minWidth: _buttonsMinWidth,\n                        ),\n                    ],\n                  ),\n                  if (billingPortalEnabled)\n                    SettingsCategory(\n                      title: LocaleKeys\n                          .settings_billingPage_paymentDetails_title\n                          .tr(),\n                      children: [\n                        SingleSettingAction(\n                          onPressed: () => context\n                              .read<SettingsBillingBloc>()\n                              .add(\n                                const SettingsBillingEvent.openCustomerPortal(),\n                              ),\n                          label: LocaleKeys\n                              .settings_billingPage_paymentDetails_methodLabel\n                              .tr(),\n                          fontWeight: FontWeight.w500,\n                          buttonLabel: LocaleKeys\n                              .settings_billingPage_paymentDetails_methodButtonLabel\n                              .tr(),\n                          minWidth: _buttonsMinWidth,\n                        ),\n                      ],\n                    ),\n                  SettingsCategory(\n                    title: LocaleKeys.settings_billingPage_addons_title.tr(),\n                    children: [\n                      _AITile(\n                        plan: SubscriptionPlanPB.AiMax,\n                        label: LocaleKeys\n                            .settings_billingPage_addons_aiMax_label\n                            .tr(),\n                        description: LocaleKeys\n                            .settings_billingPage_addons_aiMax_description,\n                        activeDescription: LocaleKeys\n                            .settings_billingPage_addons_aiMax_activeDescription,\n                        canceledDescription: LocaleKeys\n                            .settings_billingPage_addons_aiMax_canceledDescription,\n                        subscriptionInfo:\n                            state.subscriptionInfo.addOns.firstWhereOrNull(\n                          (a) => a.type == WorkspaceAddOnPBType.AddOnAiMax,\n                        ),\n                      ),\n                      const SettingsDashedDivider(),\n                    ],\n                  ),\n                ],\n              );\n            },\n          );\n        },\n      ),\n    );\n  }\n\n  void _openPricingDialog(\n    BuildContext context,\n    String workspaceId,\n    Int64 userId,\n    WorkspaceSubscriptionInfoPB subscriptionInfo,\n  ) =>\n      showDialog<bool?>(\n        context: context,\n        builder: (_) => BlocProvider<SettingsPlanBloc>(\n          create: (_) =>\n              SettingsPlanBloc(workspaceId: workspaceId, userId: widget.user.id)\n                ..add(const SettingsPlanEvent.started()),\n          child: SettingsPlanComparisonDialog(\n            workspaceId: workspaceId,\n            subscriptionInfo: subscriptionInfo,\n          ),\n        ),\n      ).then((didChangePlan) {\n        if (didChangePlan == true && context.mounted) {\n          context\n              .read<SettingsBillingBloc>()\n              .add(const SettingsBillingEvent.started());\n        }\n      });\n}\n\nclass _AITile extends StatefulWidget {\n  const _AITile({\n    required this.label,\n    required this.description,\n    required this.canceledDescription,\n    required this.activeDescription,\n    required this.plan,\n    this.subscriptionInfo,\n  });\n\n  final String label;\n  final String description;\n  final String canceledDescription;\n  final String activeDescription;\n  final SubscriptionPlanPB plan;\n  final WorkspaceAddOnPB? subscriptionInfo;\n\n  @override\n  State<_AITile> createState() => _AITileState();\n}\n\nclass _AITileState extends State<_AITile> {\n  RecurringIntervalPB? selectedInterval;\n\n  final enableConfirmNotifier = ValueNotifier<bool>(false);\n\n  @override\n  Widget build(BuildContext context) {\n    final isCanceled = widget.subscriptionInfo?.addOnSubscription.status ==\n        WorkspaceSubscriptionStatusPB.Canceled;\n\n    final dateFormat = context.read<AppearanceSettingsCubit>().state.dateFormat;\n\n    return Column(\n      children: [\n        SingleSettingAction(\n          label: widget.label,\n          description: widget.subscriptionInfo != null && isCanceled\n              ? widget.canceledDescription.tr(\n                  args: [\n                    dateFormat.formatDate(\n                      widget.subscriptionInfo!.addOnSubscription.endDate\n                          .toDateTime(),\n                      false,\n                    ),\n                  ],\n                )\n              : widget.subscriptionInfo != null\n                  ? widget.activeDescription.tr(\n                      args: [\n                        dateFormat.formatDate(\n                          widget.subscriptionInfo!.addOnSubscription.endDate\n                              .toDateTime(),\n                          false,\n                        ),\n                      ],\n                    )\n                  : widget.description.tr(),\n          buttonLabel: widget.subscriptionInfo != null\n              ? isCanceled\n                  ? LocaleKeys.settings_billingPage_addons_renewLabel.tr()\n                  : LocaleKeys.settings_billingPage_addons_removeLabel.tr()\n              : LocaleKeys.settings_billingPage_addons_addLabel.tr(),\n          fontWeight: FontWeight.w500,\n          minWidth: _buttonsMinWidth,\n          onPressed: () async {\n            if (widget.subscriptionInfo != null) {\n              await showConfirmDialog(\n                context: context,\n                style: ConfirmPopupStyle.cancelAndOk,\n                title: LocaleKeys.settings_billingPage_addons_removeDialog_title\n                    .tr(args: [widget.plan.label]).tr(),\n                description: LocaleKeys\n                    .settings_billingPage_addons_removeDialog_description\n                    .tr(namedArgs: {\"plan\": widget.plan.label.tr()}),\n                confirmLabel: LocaleKeys.button_confirm.tr(),\n                onConfirm: (_) => context\n                    .read<SettingsBillingBloc>()\n                    .add(SettingsBillingEvent.cancelSubscription(widget.plan)),\n              );\n            } else {\n              // Add the addon\n              context\n                  .read<SettingsBillingBloc>()\n                  .add(SettingsBillingEvent.addSubscription(widget.plan));\n            }\n          },\n        ),\n        if (widget.subscriptionInfo != null) ...[\n          const VSpace(10),\n          SingleSettingAction(\n            label: LocaleKeys.settings_billingPage_planPeriod.tr(\n              args: [\n                widget\n                    .subscriptionInfo!.addOnSubscription.subscriptionPlan.label,\n              ],\n            ),\n            description:\n                widget.subscriptionInfo!.addOnSubscription.interval.label,\n            buttonLabel:\n                LocaleKeys.settings_billingPage_plan_periodButtonLabel.tr(),\n            minWidth: _buttonsMinWidth,\n            onPressed: () {\n              enableConfirmNotifier.value = false;\n              SettingsAlertDialog(\n                title: LocaleKeys.settings_billingPage_changePeriod.tr(),\n                enableConfirmNotifier: enableConfirmNotifier,\n                children: [\n                  ChangePeriod(\n                    plan: widget\n                        .subscriptionInfo!.addOnSubscription.subscriptionPlan,\n                    selectedInterval:\n                        widget.subscriptionInfo!.addOnSubscription.interval,\n                    onSelected: (interval) {\n                      enableConfirmNotifier.value = interval !=\n                          widget.subscriptionInfo!.addOnSubscription.interval;\n                      selectedInterval = interval;\n                    },\n                  ),\n                ],\n                confirm: () {\n                  if (selectedInterval !=\n                      widget.subscriptionInfo!.addOnSubscription.interval) {\n                    context.read<SettingsBillingBloc>().add(\n                          SettingsBillingEvent.updatePeriod(\n                            plan: widget.subscriptionInfo!.addOnSubscription\n                                .subscriptionPlan,\n                            interval: selectedInterval!,\n                          ),\n                        );\n                  }\n                  Navigator.of(context).pop();\n                },\n              ).show(context);\n            },\n          ),\n        ],\n      ],\n    );\n  }\n}\n\nclass ChangePeriod extends StatefulWidget {\n  const ChangePeriod({\n    super.key,\n    required this.plan,\n    required this.selectedInterval,\n    required this.onSelected,\n  });\n\n  final SubscriptionPlanPB plan;\n  final RecurringIntervalPB selectedInterval;\n  final Function(RecurringIntervalPB interval) onSelected;\n\n  @override\n  State<ChangePeriod> createState() => _ChangePeriodState();\n}\n\nclass _ChangePeriodState extends State<ChangePeriod> {\n  RecurringIntervalPB? _selectedInterval;\n\n  @override\n  void initState() {\n    super.initState();\n    _selectedInterval = widget.selectedInterval;\n  }\n\n  @override\n  void didChangeDependencies() {\n    _selectedInterval = widget.selectedInterval;\n    super.didChangeDependencies();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        _PeriodSelector(\n          price: widget.plan.priceMonthBilling,\n          interval: RecurringIntervalPB.Month,\n          isSelected: _selectedInterval == RecurringIntervalPB.Month,\n          isCurrent: widget.selectedInterval == RecurringIntervalPB.Month,\n          onSelected: () {\n            widget.onSelected(RecurringIntervalPB.Month);\n            setState(\n              () => _selectedInterval = RecurringIntervalPB.Month,\n            );\n          },\n        ),\n        const VSpace(16),\n        _PeriodSelector(\n          price: widget.plan.priceAnnualBilling,\n          interval: RecurringIntervalPB.Year,\n          isSelected: _selectedInterval == RecurringIntervalPB.Year,\n          isCurrent: widget.selectedInterval == RecurringIntervalPB.Year,\n          onSelected: () {\n            widget.onSelected(RecurringIntervalPB.Year);\n            setState(\n              () => _selectedInterval = RecurringIntervalPB.Year,\n            );\n          },\n        ),\n      ],\n    );\n  }\n}\n\nclass _PeriodSelector extends StatelessWidget {\n  const _PeriodSelector({\n    required this.price,\n    required this.interval,\n    required this.onSelected,\n    required this.isSelected,\n    required this.isCurrent,\n  });\n\n  final String price;\n  final RecurringIntervalPB interval;\n  final VoidCallback onSelected;\n  final bool isSelected;\n  final bool isCurrent;\n\n  @override\n  Widget build(BuildContext context) {\n    return Opacity(\n      opacity: isCurrent && !isSelected ? 0.7 : 1,\n      child: GestureDetector(\n        onTap: isCurrent ? null : onSelected,\n        child: DecoratedBox(\n          decoration: BoxDecoration(\n            border: Border.all(\n              color: isSelected\n                  ? Theme.of(context).colorScheme.primary\n                  : Theme.of(context).dividerColor,\n            ),\n            borderRadius: BorderRadius.circular(12),\n          ),\n          child: Padding(\n            padding: const EdgeInsets.all(16),\n            child: Row(\n              children: [\n                Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Row(\n                      children: [\n                        FlowyText(\n                          interval.label,\n                          fontSize: 16,\n                          fontWeight: FontWeight.w500,\n                        ),\n                        if (isCurrent) ...[\n                          const HSpace(8),\n                          DecoratedBox(\n                            decoration: BoxDecoration(\n                              color: Theme.of(context).colorScheme.primary,\n                              borderRadius: BorderRadius.circular(6),\n                            ),\n                            child: Padding(\n                              padding: const EdgeInsets.symmetric(\n                                horizontal: 6,\n                                vertical: 1,\n                              ),\n                              child: FlowyText(\n                                LocaleKeys\n                                    .settings_billingPage_currentPeriodBadge\n                                    .tr(),\n                                fontSize: 11,\n                                fontWeight: FontWeight.w500,\n                                color: Colors.white,\n                              ),\n                            ),\n                          ),\n                        ],\n                      ],\n                    ),\n                    const VSpace(8),\n                    FlowyText(\n                      price,\n                      fontSize: 14,\n                      fontWeight: FontWeight.w500,\n                    ),\n                    const VSpace(4),\n                    FlowyText(\n                      interval.priceInfo,\n                      fontWeight: FontWeight.w400,\n                      fontSize: 12,\n                    ),\n                  ],\n                ),\n                const Spacer(),\n                if (!isCurrent && !isSelected || isSelected) ...[\n                  DecoratedBox(\n                    decoration: BoxDecoration(\n                      shape: BoxShape.circle,\n                      border: Border.all(\n                        width: 1.5,\n                        color: isSelected\n                            ? Theme.of(context).colorScheme.primary\n                            : Theme.of(context).dividerColor,\n                      ),\n                    ),\n                    child: SizedBox(\n                      height: 22,\n                      width: 22,\n                      child: Center(\n                        child: SizedBox(\n                          width: 10,\n                          height: 10,\n                          child: DecoratedBox(\n                            decoration: BoxDecoration(\n                              shape: BoxShape.circle,\n                              color: isSelected\n                                  ? Theme.of(context).colorScheme.primary\n                                  : Colors.transparent,\n                            ),\n                          ),\n                        ),\n                      ),\n                    ),\n                  ),\n                ],\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/features/settings/settings.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/appflowy_cache_manager.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/share_log_files.dart';\nimport 'package:appflowy/workspace/application/settings/setting_file_importer_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/toast.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/fix_data_widget.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/file_picker/file_picker_service.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:fluttertoast/fluttertoast.dart';\n\nimport '../shared/setting_action.dart';\n\nclass SettingsManageDataView extends StatelessWidget {\n  const SettingsManageDataView({\n    super.key,\n    required this.workspace,\n    required this.userProfile,\n  });\n\n  final UserWorkspacePB workspace;\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<DataLocationBloc>(\n      create: (_) => DataLocationBloc(\n        repository: const RustSettingsRepositoryImpl(),\n      )..add(DataLocationEvent.initial()),\n      child: BlocConsumer<DataLocationBloc, DataLocationState>(\n        listenWhen: (previous, current) =>\n            previous.didResetToDefault != current.didResetToDefault,\n        listener: (context, state) {\n          if (state.didResetToDefault) {\n            Navigator.of(context).pop();\n            runAppFlowy(isAnon: true);\n          }\n        },\n        builder: (context, state) {\n          // final _ = state.userDataLocation?.isCustom ?? false;\n          final isCloudWorkspace =\n              workspace.workspaceType == WorkspaceTypePB.ServerW;\n          final path = state.userDataLocation?.path;\n\n          return SettingsBody(\n            title: LocaleKeys.settings_manageDataPage_title.tr(),\n            description: LocaleKeys.settings_manageDataPage_description.tr(),\n            children: [\n              SettingsCategory(\n                title:\n                    LocaleKeys.settings_manageDataPage_dataStorage_title.tr(),\n                tooltip:\n                    LocaleKeys.settings_manageDataPage_dataStorage_tooltip.tr(),\n                actions: [\n                  if (isCloudWorkspace)\n                    SettingAction(\n                      tooltip: LocaleKeys\n                          .settings_manageDataPage_dataStorage_actions_resetTooltip\n                          .tr(),\n                      icon: const FlowySvg(\n                        FlowySvgs.restore_s,\n                        size: Size.square(20),\n                      ),\n                      label: LocaleKeys.settings_common_reset.tr(),\n                      onPressed: () {\n                        showSimpleAFDialog(\n                          context: context,\n                          title: LocaleKeys\n                              .settings_manageDataPage_dataStorage_resetDialog_title\n                              .tr(),\n                          content: LocaleKeys\n                              .settings_manageDataPage_dataStorage_resetDialog_description\n                              .tr(),\n                          primaryAction: (\n                            LocaleKeys.button_confirm.tr(),\n                            (_) {\n                              context\n                                  .read<DataLocationBloc>()\n                                  .add(DataLocationResetToDefault());\n                            }\n                          ),\n                          secondaryAction: (\n                            LocaleKeys.button_cancel.tr(),\n                            (_) {},\n                          ),\n                        );\n                      },\n                    ),\n                ],\n                children: path == null\n                    ? [\n                        const CircularProgressIndicator(),\n                      ]\n                    : [\n                        _CurrentPath(path: path),\n                        if (isCloudWorkspace) _DataPathActions(path: path),\n                      ],\n              ),\n              SettingsCategory(\n                title: LocaleKeys.settings_manageDataPage_importData_title.tr(),\n                tooltip:\n                    LocaleKeys.settings_manageDataPage_importData_tooltip.tr(),\n                children: const [_ImportDataField()],\n              ),\n              if (kDebugMode) ...[\n                SettingsCategory(\n                  title: LocaleKeys.settings_files_exportData.tr(),\n                  children: const [\n                    SettingsExportFileWidget(),\n                    FixDataWidget(),\n                  ],\n                ),\n              ],\n              SettingsCategory(\n                title: LocaleKeys.workspace_errorActions_exportLogFiles.tr(),\n                children: [\n                  SingleSettingAction(\n                    labelMaxLines: 4,\n                    label:\n                        LocaleKeys.workspace_errorActions_exportLogFiles.tr(),\n                    buttonLabel: LocaleKeys.settings_files_export.tr(),\n                    onPressed: () {\n                      shareLogFiles(context);\n                    },\n                  ),\n                ],\n              ),\n              SettingsCategory(\n                title: LocaleKeys.settings_manageDataPage_cache_title.tr(),\n                children: [\n                  SingleSettingAction(\n                    labelMaxLines: 4,\n                    label: LocaleKeys.settings_manageDataPage_cache_description\n                        .tr(),\n                    buttonLabel:\n                        LocaleKeys.settings_manageDataPage_cache_title.tr(),\n                    onPressed: () {\n                      showCancelAndConfirmDialog(\n                        context: context,\n                        title: LocaleKeys\n                            .settings_manageDataPage_cache_dialog_title\n                            .tr(),\n                        description: LocaleKeys\n                            .settings_manageDataPage_cache_dialog_description\n                            .tr(),\n                        confirmLabel: LocaleKeys.button_ok.tr(),\n                        onConfirm: (_) async {\n                          // clear all cache\n                          await getIt<FlowyCacheManager>().clearAllCache();\n\n                          // check the workspace and space health\n                          await WorkspaceDataManager.checkViewHealth(\n                            dryRun: false,\n                          );\n\n                          if (context.mounted) {\n                            showToastNotification(\n                              message: LocaleKeys\n                                  .settings_manageDataPage_cache_dialog_successHint\n                                  .tr(),\n                            );\n                          }\n                        },\n                      );\n                    },\n                  ),\n                ],\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n\n// class _EncryptDataSetting extends StatelessWidget {\n//   const _EncryptDataSetting({required this.userProfile});\n\n//   final UserProfilePB userProfile;\n\n//   @override\n//   Widget build(BuildContext context) {\n//     return BlocProvider<EncryptSecretBloc>.value(\n//       value: context.read<EncryptSecretBloc>(),\n//       child: BlocBuilder<EncryptSecretBloc, EncryptSecretState>(\n//         builder: (context, state) {\n//           if (state.loadingState?.isLoading() == true) {\n//             return const Row(\n//               children: [\n//                 SizedBox(\n//                   width: 20,\n//                   height: 20,\n//                   child: CircularProgressIndicator(\n//                     strokeWidth: 3,\n//                   ),\n//                 ),\n//                 HSpace(16),\n//                 FlowyText.medium(\n//                   'Encrypting data...',\n//                   fontSize: 14,\n//                 ),\n//               ],\n//             );\n//           }\n\n//           if (userProfile.encryptionType == EncryptionTypePB.NoEncryption) {\n//             return Row(\n//               children: [\n//                 SizedBox(\n//                   height: 42,\n//                   child: FlowyTextButton(\n//                     LocaleKeys.settings_manageDataPage_encryption_action.tr(),\n//                     padding: const EdgeInsets.symmetric(\n//                       horizontal: 24,\n//                       vertical: 12,\n//                     ),\n//                     fontWeight: FontWeight.w600,\n//                     radius: BorderRadius.circular(12),\n//                     fillColor: Theme.of(context).colorScheme.primary,\n//                     hoverColor: const Color(0xFF005483),\n//                     fontHoverColor: Colors.white,\n//                     onPressed: () => SettingsAlertDialog(\n//                       title: LocaleKeys\n//                           .settings_manageDataPage_encryption_dialog_title\n//                           .tr(),\n//                       subtitle: LocaleKeys\n//                           .settings_manageDataPage_encryption_dialog_description\n//                           .tr(),\n//                       confirmLabel: LocaleKeys\n//                           .settings_manageDataPage_encryption_dialog_title\n//                           .tr(),\n//                       implyLeading: true,\n//                       // Generate a secret one time for the user\n//                       confirm: () => context\n//                           .read<EncryptSecretBloc>()\n//                           .add(const EncryptSecretEvent.setEncryptSecret('')),\n//                     ).show(context),\n//                   ),\n//                 ),\n//               ],\n//             );\n//           }\n//           // Show encryption secret for copy/save\n//           return const SizedBox.shrink();\n//         },\n//       ),\n//     );\n//   }\n// }\n\nclass _ImportDataField extends StatefulWidget {\n  const _ImportDataField();\n\n  @override\n  State<_ImportDataField> createState() => _ImportDataFieldState();\n}\n\nclass _ImportDataFieldState extends State<_ImportDataField> {\n  final _fToast = FToast();\n\n  @override\n  void initState() {\n    super.initState();\n    _fToast.init(context);\n  }\n\n  @override\n  void dispose() {\n    _fToast.removeQueuedCustomToasts();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<SettingFileImportBloc>(\n      create: (context) => SettingFileImportBloc(),\n      child: BlocConsumer<SettingFileImportBloc, SettingFileImportState>(\n        listenWhen: (previous, current) =>\n            previous.successOrFail != current.successOrFail,\n        listener: (_, state) => state.successOrFail?.fold(\n          (_) => _showToast(LocaleKeys.settings_menu_importSuccess.tr()),\n          (_) => _showToast(LocaleKeys.settings_menu_importFailed.tr()),\n        ),\n        builder: (context, state) {\n          return SingleSettingAction(\n            label:\n                LocaleKeys.settings_manageDataPage_importData_description.tr(),\n            labelMaxLines: 2,\n            buttonLabel:\n                LocaleKeys.settings_manageDataPage_importData_action.tr(),\n            onPressed: () async {\n              final path = await getIt<FilePickerService>().getDirectoryPath();\n              if (path == null || !context.mounted) {\n                return;\n              }\n\n              context\n                  .read<SettingFileImportBloc>()\n                  .add(SettingFileImportEvent.importAppFlowyDataFolder(path));\n            },\n          );\n        },\n      ),\n    );\n  }\n\n  void _showToast(String message) {\n    _fToast.showToast(\n      child: FlowyMessageToast(message: message),\n      gravity: ToastGravity.CENTER,\n    );\n  }\n}\n\nclass _CurrentPath extends StatefulWidget {\n  const _CurrentPath({required this.path});\n\n  final String path;\n\n  @override\n  State<_CurrentPath> createState() => _CurrentPathState();\n}\n\nclass _CurrentPathState extends State<_CurrentPath> {\n  Timer? linkCopiedTimer;\n  bool showCopyMessage = false;\n  bool isHovering = false;\n\n  @override\n  void dispose() {\n    linkCopiedTimer?.cancel();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return Column(\n      children: [\n        Row(\n          children: [\n            Expanded(\n              child: FlowyTooltip(\n                message: LocaleKeys\n                    .settings_manageDataPage_dataStorage_actions_openTooltip\n                    .tr(),\n                child: GestureDetector(\n                  onTap: () => {\n                    afLaunchUri(Uri.file(widget.path)),\n                  },\n                  child: MouseRegion(\n                    cursor: SystemMouseCursors.click,\n                    onEnter: (_) => setState(() => isHovering = true),\n                    onExit: (_) => setState(() => isHovering = false),\n                    child: Text(\n                      widget.path,\n                      maxLines: 2,\n                      style: theme.textStyle.body\n                          .standard(color: theme.textColorScheme.action)\n                          .copyWith(\n                            decoration:\n                                isHovering ? TextDecoration.underline : null,\n                          ),\n                      overflow: TextOverflow.ellipsis,\n                    ),\n                  ),\n                ),\n              ),\n            ),\n            HSpace(\n              theme.spacing.m,\n            ),\n            IndexedStack(\n              alignment: Alignment.centerRight,\n              index: showCopyMessage ? 0 : 1,\n              children: [\n                Container(\n                  decoration: BoxDecoration(\n                    color: AFThemeExtension.of(context).tint7,\n                    borderRadius: BorderRadius.circular(8),\n                  ),\n                  padding: EdgeInsets.symmetric(\n                    horizontal: theme.spacing.l,\n                    vertical: theme.spacing.m,\n                  ),\n                  child: Text(\n                    LocaleKeys\n                        .settings_manageDataPage_dataStorage_actions_copiedHint\n                        .tr(),\n                    style: theme.textStyle.body.standard(\n                      color: theme.textColorScheme.primary,\n                    ),\n                    overflow: TextOverflow.ellipsis,\n                  ),\n                ),\n                FlowyTooltip(\n                  message: LocaleKeys\n                      .settings_manageDataPage_dataStorage_actions_copy\n                      .tr(),\n                  child: AFGhostButton.normal(\n                    builder: (context, _, __) {\n                      return FlowySvg(\n                        FlowySvgs.copy_s,\n                        size: Size.square(20),\n                        color: theme.textColorScheme.primary,\n                      );\n                    },\n                    padding: EdgeInsets.all(theme.spacing.m),\n                    onTap: () => _copyLink(widget.path),\n                  ),\n                ),\n              ],\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n\n  void _copyLink(String? path) {\n    AppFlowyClipboard.setData(text: path);\n    setState(() => showCopyMessage = true);\n    linkCopiedTimer?.cancel();\n    linkCopiedTimer = Timer(\n      const Duration(milliseconds: 300),\n      () {\n        if (mounted) {\n          setState(() => showCopyMessage = false);\n        }\n      },\n    );\n  }\n}\n\nclass _DataPathActions extends StatelessWidget {\n  const _DataPathActions({required this.path});\n\n  final String path;\n\n  @override\n  Widget build(BuildContext context) {\n    return AFFilledTextButton.primary(\n      text: LocaleKeys.settings_manageDataPage_dataStorage_actions_change.tr(),\n      onTap: () async {\n        final path = await getIt<FilePickerService>().getDirectoryPath();\n        if (!context.mounted || path == null || path == path) {\n          return;\n        }\n\n        context\n            .read<DataLocationBloc>()\n            .add(DataLocationEvent.setCustomPath(path));\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/shared/loading.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/settings/plan/settings_plan_bloc.dart';\nimport 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/cancel_plan_survey_dialog.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../../../generated/locale_keys.g.dart';\n\nclass SettingsPlanComparisonDialog extends StatefulWidget {\n  const SettingsPlanComparisonDialog({\n    super.key,\n    required this.workspaceId,\n    required this.subscriptionInfo,\n  });\n\n  final String workspaceId;\n  final WorkspaceSubscriptionInfoPB subscriptionInfo;\n\n  @override\n  State<SettingsPlanComparisonDialog> createState() =>\n      _SettingsPlanComparisonDialogState();\n}\n\nclass _SettingsPlanComparisonDialogState\n    extends State<SettingsPlanComparisonDialog> {\n  final horizontalController = ScrollController();\n  final verticalController = ScrollController();\n\n  late WorkspaceSubscriptionInfoPB currentInfo = widget.subscriptionInfo;\n\n  Loading? loadingIndicator;\n\n  @override\n  void dispose() {\n    horizontalController.dispose();\n    verticalController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final isLM = Theme.of(context).isLightMode;\n\n    return BlocConsumer<SettingsPlanBloc, SettingsPlanState>(\n      listener: (context, state) {\n        final readyState = state.mapOrNull(ready: (state) => state);\n\n        if (readyState == null) {\n          return;\n        }\n\n        if (readyState.downgradeProcessing) {\n          loadingIndicator = Loading(context)..start();\n        } else {\n          loadingIndicator?.stop();\n          loadingIndicator = null;\n        }\n\n        if (readyState.successfulPlanUpgrade != null) {\n          showConfirmDialog(\n            context: context,\n            title: LocaleKeys.settings_comparePlanDialog_paymentSuccess_title\n                .tr(args: [readyState.successfulPlanUpgrade!.label]),\n            description: LocaleKeys\n                .settings_comparePlanDialog_paymentSuccess_description\n                .tr(args: [readyState.successfulPlanUpgrade!.label]),\n            confirmLabel: LocaleKeys.button_close.tr(),\n            onConfirm: (_) {},\n          );\n        }\n\n        setState(() => currentInfo = readyState.subscriptionInfo);\n      },\n      builder: (context, state) => FlowyDialog(\n        constraints: const BoxConstraints(maxWidth: 784, minWidth: 674),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Padding(\n              padding: const EdgeInsets.only(top: 24, left: 24, right: 24),\n              child: Row(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  FlowyText.semibold(\n                    LocaleKeys.settings_comparePlanDialog_title.tr(),\n                    fontSize: 24,\n                    color: AFThemeExtension.of(context).strongText,\n                  ),\n                  const Spacer(),\n                  GestureDetector(\n                    onTap: () => Navigator.of(context).pop(\n                      currentInfo.plan != widget.subscriptionInfo.plan,\n                    ),\n                    child: MouseRegion(\n                      cursor: SystemMouseCursors.click,\n                      child: FlowySvg(\n                        FlowySvgs.m_close_m,\n                        size: const Size.square(20),\n                        color: AFThemeExtension.of(context).strongText,\n                      ),\n                    ),\n                  ),\n                ],\n              ),\n            ),\n            const VSpace(16),\n            Flexible(\n              child: SingleChildScrollView(\n                controller: horizontalController,\n                scrollDirection: Axis.horizontal,\n                child: SingleChildScrollView(\n                  controller: verticalController,\n                  padding: const EdgeInsets.only(\n                    left: 24,\n                    right: 24,\n                    bottom: 24,\n                  ),\n                  child: Column(\n                    mainAxisSize: MainAxisSize.min,\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      Row(\n                        mainAxisSize: MainAxisSize.min,\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        children: [\n                          SizedBox(\n                            width: 250,\n                            child: Column(\n                              mainAxisSize: MainAxisSize.min,\n                              crossAxisAlignment: CrossAxisAlignment.start,\n                              children: [\n                                const VSpace(30),\n                                SizedBox(\n                                  height: 116,\n                                  child: FlowyText.semibold(\n                                    LocaleKeys\n                                        .settings_comparePlanDialog_planFeatures\n                                        .tr(),\n                                    fontSize: 24,\n                                    maxLines: 2,\n                                    color: isLM\n                                        ? const Color(0xFF5C3699)\n                                        : const Color(0xFFE8E0FF),\n                                  ),\n                                ),\n                                const SizedBox(height: 116),\n                                const SizedBox(height: 56),\n                                ..._planLabels.map(\n                                  (e) => _ComparisonCell(\n                                    label: e.label,\n                                    tooltip: e.tooltip,\n                                  ),\n                                ),\n                              ],\n                            ),\n                          ),\n                          _PlanTable(\n                            title: LocaleKeys\n                                .settings_comparePlanDialog_freePlan_title\n                                .tr(),\n                            description: LocaleKeys\n                                .settings_comparePlanDialog_freePlan_description\n                                .tr(),\n                            price: LocaleKeys\n                                .settings_comparePlanDialog_freePlan_price\n                                .tr(\n                              args: [\n                                SubscriptionPlanPB.Free.priceMonthBilling,\n                              ],\n                            ),\n                            priceInfo: LocaleKeys\n                                .settings_comparePlanDialog_freePlan_priceInfo\n                                .tr(),\n                            cells: _freeLabels,\n                            isCurrent:\n                                currentInfo.plan == WorkspacePlanPB.FreePlan,\n                            buttonType: WorkspacePlanPB.FreePlan.buttonTypeFor(\n                              currentInfo.plan,\n                            ),\n                            onSelected: () async {\n                              if (currentInfo.plan ==\n                                      WorkspacePlanPB.FreePlan ||\n                                  currentInfo.isCanceled) {\n                                return;\n                              }\n\n                              final reason =\n                                  await showCancelSurveyDialog(context);\n                              if (reason == null || !context.mounted) {\n                                return;\n                              }\n\n                              await showConfirmDialog(\n                                context: context,\n                                title: LocaleKeys\n                                    .settings_comparePlanDialog_downgradeDialog_title\n                                    .tr(args: [currentInfo.label]),\n                                description: LocaleKeys\n                                    .settings_comparePlanDialog_downgradeDialog_description\n                                    .tr(),\n                                confirmLabel: LocaleKeys\n                                    .settings_comparePlanDialog_downgradeDialog_downgradeLabel\n                                    .tr(),\n                                style: ConfirmPopupStyle.cancelAndOk,\n                                onConfirm: (_) =>\n                                    context.read<SettingsPlanBloc>().add(\n                                          SettingsPlanEvent.cancelSubscription(\n                                            reason: reason,\n                                          ),\n                                        ),\n                              );\n                            },\n                          ),\n                          _PlanTable(\n                            title: LocaleKeys\n                                .settings_comparePlanDialog_proPlan_title\n                                .tr(),\n                            description: LocaleKeys\n                                .settings_comparePlanDialog_proPlan_description\n                                .tr(),\n                            price: LocaleKeys\n                                .settings_comparePlanDialog_proPlan_price\n                                .tr(\n                              args: [SubscriptionPlanPB.Pro.priceAnnualBilling],\n                            ),\n                            priceInfo: LocaleKeys\n                                .settings_comparePlanDialog_proPlan_priceInfo\n                                .tr(\n                              args: [SubscriptionPlanPB.Pro.priceMonthBilling],\n                            ),\n                            cells: _proLabels,\n                            isCurrent:\n                                currentInfo.plan == WorkspacePlanPB.ProPlan,\n                            buttonType: WorkspacePlanPB.ProPlan.buttonTypeFor(\n                              currentInfo.plan,\n                            ),\n                            onSelected: () =>\n                                context.read<SettingsPlanBloc>().add(\n                                      const SettingsPlanEvent.addSubscription(\n                                        SubscriptionPlanPB.Pro,\n                                      ),\n                                    ),\n                          ),\n                        ],\n                      ),\n                    ],\n                  ),\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nenum _PlanButtonType {\n  none,\n  upgrade,\n  downgrade;\n\n  bool get isDowngrade => this == downgrade;\n  bool get isUpgrade => this == upgrade;\n}\n\nextension _ButtonTypeFrom on WorkspacePlanPB {\n  /// Returns the button type for the given plan, taking the\n  /// current plan as [other].\n  ///\n  _PlanButtonType buttonTypeFor(WorkspacePlanPB other) {\n    /// Current plan, no action\n    if (this == other) {\n      return _PlanButtonType.none;\n    }\n\n    // Free plan, can downgrade if not on the free plan\n    if (this == WorkspacePlanPB.FreePlan && other != WorkspacePlanPB.FreePlan) {\n      return _PlanButtonType.downgrade;\n    }\n\n    // Else we can assume it's an upgrade\n    return _PlanButtonType.upgrade;\n  }\n}\n\nclass _PlanTable extends StatelessWidget {\n  const _PlanTable({\n    required this.title,\n    required this.description,\n    required this.price,\n    required this.priceInfo,\n    required this.cells,\n    required this.isCurrent,\n    required this.onSelected,\n    this.buttonType = _PlanButtonType.none,\n  });\n\n  final String title;\n  final String description;\n  final String price;\n  final String priceInfo;\n\n  final List<_CellItem> cells;\n  final bool isCurrent;\n  final VoidCallback onSelected;\n  final _PlanButtonType buttonType;\n\n  @override\n  Widget build(BuildContext context) {\n    final highlightPlan = !isCurrent && buttonType == _PlanButtonType.upgrade;\n    final isLM = Theme.of(context).isLightMode;\n\n    return Container(\n      width: 215,\n      decoration: BoxDecoration(\n        borderRadius: BorderRadius.circular(24),\n        gradient: !highlightPlan\n            ? null\n            : LinearGradient(\n                colors: [\n                  isLM ? const Color(0xFF251D37) : const Color(0xFF7459AD),\n                  isLM ? const Color(0xFF7547C0) : const Color(0xFFDDC8FF),\n                ],\n              ),\n      ),\n      padding: !highlightPlan\n          ? const EdgeInsets.only(top: 4)\n          : const EdgeInsets.all(4),\n      child: Container(\n        padding: isCurrent\n            ? const EdgeInsets.only(bottom: 22)\n            : const EdgeInsets.symmetric(vertical: 22),\n        decoration: BoxDecoration(\n          borderRadius: BorderRadius.circular(22),\n          color: Theme.of(context).cardColor,\n        ),\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            if (isCurrent) const _CurrentBadge(),\n            const VSpace(4),\n            _Heading(\n              title: title,\n              description: description,\n              isPrimary: !highlightPlan,\n            ),\n            _Heading(\n              title: price,\n              description: priceInfo,\n              isPrimary: !highlightPlan,\n            ),\n            if (buttonType == _PlanButtonType.none) ...[\n              const SizedBox(height: 56),\n            ] else ...[\n              Opacity(\n                opacity: 1,\n                child: Padding(\n                  padding: EdgeInsets.only(\n                    left: 12 + (buttonType.isUpgrade ? 12 : 0),\n                  ),\n                  child: _ActionButton(\n                    label: buttonType.isUpgrade\n                        ? LocaleKeys.settings_comparePlanDialog_actions_upgrade\n                            .tr()\n                        : LocaleKeys\n                            .settings_comparePlanDialog_actions_downgrade\n                            .tr(),\n                    onPressed: onSelected,\n                    isUpgrade: buttonType.isUpgrade,\n                    useGradientBorder: buttonType.isUpgrade,\n                  ),\n                ),\n              ),\n            ],\n            ...cells.map(\n              (cell) => _ComparisonCell(\n                label: cell.label,\n                icon: cell.icon,\n                isHighlighted: highlightPlan,\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _CurrentBadge extends StatelessWidget {\n  const _CurrentBadge();\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      margin: const EdgeInsets.only(left: 12),\n      height: 22,\n      width: 72,\n      decoration: BoxDecoration(\n        color: Theme.of(context).isLightMode\n            ? const Color(0xFF4F3F5F)\n            : const Color(0xFFE8E0FF),\n        borderRadius: BorderRadius.circular(4),\n      ),\n      child: Center(\n        child: FlowyText.medium(\n          LocaleKeys.settings_comparePlanDialog_current.tr(),\n          fontSize: 12,\n          color: Theme.of(context).isLightMode ? Colors.white : Colors.black,\n        ),\n      ),\n    );\n  }\n}\n\nclass _ComparisonCell extends StatelessWidget {\n  const _ComparisonCell({\n    this.label,\n    this.icon,\n    this.tooltip,\n    this.isHighlighted = false,\n  });\n\n  final String? label;\n  final FlowySvgData? icon;\n  final String? tooltip;\n  final bool isHighlighted;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.symmetric(horizontal: 12) +\n          EdgeInsets.only(left: isHighlighted ? 12 : 0),\n      height: 36,\n      decoration: BoxDecoration(\n        border: Border(\n          bottom: BorderSide(\n            color: Theme.of(context).dividerColor,\n          ),\n        ),\n      ),\n      child: Row(\n        children: [\n          if (icon != null) ...[\n            FlowySvg(\n              icon!,\n              color: AFThemeExtension.of(context).strongText,\n            ),\n          ] else if (label != null) ...[\n            Expanded(\n              child: FlowyText.medium(\n                label!,\n                lineHeight: 1.2,\n                color: AFThemeExtension.of(context).strongText,\n              ),\n            ),\n          ],\n          if (tooltip != null)\n            FlowyTooltip(\n              message: tooltip,\n              child: FlowySvg(\n                FlowySvgs.information_s,\n                color: AFThemeExtension.of(context).strongText,\n              ),\n            ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _ActionButton extends StatelessWidget {\n  const _ActionButton({\n    required this.label,\n    required this.onPressed,\n    required this.isUpgrade,\n    this.useGradientBorder = false,\n  });\n\n  final String label;\n  final VoidCallback? onPressed;\n  final bool isUpgrade;\n  final bool useGradientBorder;\n\n  @override\n  Widget build(BuildContext context) {\n    final isLM = Theme.of(context).isLightMode;\n\n    return SizedBox(\n      height: 56,\n      child: Row(\n        children: [\n          GestureDetector(\n            onTap: onPressed,\n            child: MouseRegion(\n              cursor: onPressed != null\n                  ? SystemMouseCursors.click\n                  : MouseCursor.defer,\n              child: _drawBorder(\n                context,\n                isLM: isLM,\n                isUpgrade: isUpgrade,\n                child: Container(\n                  height: 36,\n                  width: 148,\n                  decoration: BoxDecoration(\n                    color: useGradientBorder\n                        ? Theme.of(context).cardColor\n                        : Colors.transparent,\n                    border: Border.all(color: Colors.transparent),\n                    borderRadius: BorderRadius.circular(14),\n                  ),\n                  child: Center(child: _drawText(label, isLM, isUpgrade)),\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _drawText(String text, bool isLM, bool isUpgrade) {\n    final child = FlowyText(\n      text,\n      fontSize: 14,\n      lineHeight: 1.2,\n      fontWeight: useGradientBorder ? FontWeight.w600 : FontWeight.w500,\n      color: isUpgrade ? const Color(0xFFC49BEC) : null,\n    );\n\n    if (!useGradientBorder || !isLM) {\n      return child;\n    }\n\n    return ShaderMask(\n      blendMode: BlendMode.srcIn,\n      shaderCallback: (bounds) => const LinearGradient(\n        transform: GradientRotation(-1.55),\n        stops: [0.4, 1],\n        colors: [Color(0xFF251D37), Color(0xFF7547C0)],\n      ).createShader(Rect.fromLTWH(0, 0, bounds.width, bounds.height)),\n      child: child,\n    );\n  }\n\n  Widget _drawBorder(\n    BuildContext context, {\n    required bool isLM,\n    required bool isUpgrade,\n    required Widget child,\n  }) {\n    return Container(\n      padding: const EdgeInsets.all(2),\n      decoration: BoxDecoration(\n        gradient: isUpgrade\n            ? LinearGradient(\n                transform: const GradientRotation(-1.2),\n                stops: const [0.4, 1],\n                colors: [\n                  isLM ? const Color(0xFF251D37) : const Color(0xFF7459AD),\n                  isLM ? const Color(0xFF7547C0) : const Color(0xFFDDC8FF),\n                ],\n              )\n            : null,\n        border: isUpgrade ? null : Border.all(color: const Color(0xFF333333)),\n        borderRadius: BorderRadius.circular(16),\n      ),\n      child: child,\n    );\n  }\n}\n\nclass _Heading extends StatelessWidget {\n  const _Heading({\n    required this.title,\n    this.description,\n    this.isPrimary = true,\n  });\n\n  final String title;\n  final String? description;\n  final bool isPrimary;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: 185,\n      height: 116,\n      child: Padding(\n        padding: EdgeInsets.only(left: 12 + (!isPrimary ? 12 : 0)),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Row(\n              children: [\n                Expanded(\n                  child: FlowyText.semibold(\n                    title,\n                    fontSize: 24,\n                    overflow: TextOverflow.ellipsis,\n                    color: isPrimary\n                        ? AFThemeExtension.of(context).strongText\n                        : Theme.of(context).isLightMode\n                            ? const Color(0xFF5C3699)\n                            : const Color(0xFFC49BEC),\n                  ),\n                ),\n              ],\n            ),\n            if (description != null && description!.isNotEmpty) ...[\n              const VSpace(4),\n              Flexible(\n                child: FlowyText.regular(\n                  description!,\n                  fontSize: 12,\n                  maxLines: 5,\n                  lineHeight: 1.5,\n                ),\n              ),\n            ],\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _PlanItem {\n  const _PlanItem({required this.label, this.tooltip});\n\n  final String label;\n  final String? tooltip;\n}\n\nfinal _planLabels = [\n  _PlanItem(\n    label: LocaleKeys.settings_comparePlanDialog_planLabels_itemOne.tr(),\n  ),\n  _PlanItem(\n    label: LocaleKeys.settings_comparePlanDialog_planLabels_itemTwo.tr(),\n  ),\n  _PlanItem(\n    label: LocaleKeys.settings_comparePlanDialog_planLabels_itemThree.tr(),\n  ),\n  _PlanItem(\n    label: LocaleKeys.settings_comparePlanDialog_planLabels_itemFour.tr(),\n  ),\n  _PlanItem(\n    label: LocaleKeys.settings_comparePlanDialog_planLabels_itemFive.tr(),\n    tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipFive.tr(),\n  ),\n  _PlanItem(\n    label:\n        LocaleKeys.settings_comparePlanDialog_planLabels_intelligentSearch.tr(),\n  ),\n  _PlanItem(\n    label: LocaleKeys.settings_comparePlanDialog_planLabels_itemSix.tr(),\n    tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipSix.tr(),\n  ),\n  _PlanItem(\n    label: LocaleKeys.settings_comparePlanDialog_planLabels_itemSeven.tr(),\n    tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipSix.tr(),\n  ),\n  _PlanItem(\n    label: LocaleKeys.settings_comparePlanDialog_planLabels_itemFileUpload.tr(),\n  ),\n  _PlanItem(\n    label:\n        LocaleKeys.settings_comparePlanDialog_planLabels_customNamespace.tr(),\n    tooltip: LocaleKeys\n        .settings_comparePlanDialog_planLabels_customNamespaceTooltip\n        .tr(),\n  ),\n];\n\nclass _CellItem {\n  const _CellItem({this.label, this.icon});\n\n  final String? label;\n  final FlowySvgData? icon;\n}\n\nfinal List<_CellItem> _freeLabels = [\n  _CellItem(\n    label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemOne.tr(),\n  ),\n  _CellItem(\n    label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemTwo.tr(),\n  ),\n  _CellItem(\n    label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemThree.tr(),\n  ),\n  _CellItem(\n    label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemFour.tr(),\n    icon: FlowySvgs.check_m,\n  ),\n  _CellItem(\n    label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemFive.tr(),\n  ),\n  _CellItem(\n    label:\n        LocaleKeys.settings_comparePlanDialog_freeLabels_intelligentSearch.tr(),\n    icon: FlowySvgs.check_m,\n  ),\n  _CellItem(\n    label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemSix.tr(),\n  ),\n  _CellItem(\n    label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemSeven.tr(),\n  ),\n  _CellItem(\n    label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemFileUpload.tr(),\n  ),\n  const _CellItem(\n    label: '',\n  ),\n];\n\nfinal List<_CellItem> _proLabels = [\n  _CellItem(\n    label: LocaleKeys.settings_comparePlanDialog_proLabels_itemOne.tr(),\n  ),\n  _CellItem(\n    label: LocaleKeys.settings_comparePlanDialog_proLabels_itemTwo.tr(),\n  ),\n  _CellItem(\n    label: LocaleKeys.settings_comparePlanDialog_proLabels_itemThree.tr(),\n  ),\n  _CellItem(\n    label: LocaleKeys.settings_comparePlanDialog_proLabels_itemFour.tr(),\n    icon: FlowySvgs.check_m,\n  ),\n  _CellItem(\n    label: LocaleKeys.settings_comparePlanDialog_proLabels_itemFive.tr(),\n  ),\n  _CellItem(\n    label:\n        LocaleKeys.settings_comparePlanDialog_proLabels_intelligentSearch.tr(),\n    icon: FlowySvgs.check_m,\n  ),\n  _CellItem(\n    label: LocaleKeys.settings_comparePlanDialog_proLabels_itemSix.tr(),\n  ),\n  _CellItem(\n    label: LocaleKeys.settings_comparePlanDialog_proLabels_itemSeven.tr(),\n  ),\n  _CellItem(\n    label: LocaleKeys.settings_comparePlanDialog_proLabels_itemFileUpload.tr(),\n  ),\n  const _CellItem(\n    label: '',\n    icon: FlowySvgs.check_m,\n  ),\n];\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/colors.dart';\nimport 'package:appflowy/shared/flowy_error_page.dart';\nimport 'package:appflowy/shared/loading.dart';\nimport 'package:appflowy/util/int64_extension.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';\nimport 'package:appflowy/workspace/application/settings/plan/settings_plan_bloc.dart';\nimport 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';\nimport 'package:appflowy/workspace/application/settings/plan/workspace_usage_ext.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/flowy_gradient_button.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SettingsPlanView extends StatefulWidget {\n  const SettingsPlanView({\n    super.key,\n    required this.workspaceId,\n    required this.user,\n  });\n\n  final String workspaceId;\n  final UserProfilePB user;\n\n  @override\n  State<SettingsPlanView> createState() => _SettingsPlanViewState();\n}\n\nclass _SettingsPlanViewState extends State<SettingsPlanView> {\n  Loading? loadingIndicator;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<SettingsPlanBloc>(\n      create: (context) => SettingsPlanBloc(\n        workspaceId: widget.workspaceId,\n        userId: widget.user.id,\n      )..add(const SettingsPlanEvent.started()),\n      child: BlocConsumer<SettingsPlanBloc, SettingsPlanState>(\n        listenWhen: (previous, current) =>\n            previous.mapOrNull(ready: (s) => s.downgradeProcessing) !=\n            current.mapOrNull(ready: (s) => s.downgradeProcessing),\n        listener: (context, state) {\n          if (state.mapOrNull(ready: (s) => s.downgradeProcessing) == true) {\n            loadingIndicator = Loading(context)..start();\n          } else {\n            loadingIndicator?.stop();\n            loadingIndicator = null;\n          }\n        },\n        builder: (context, state) {\n          return state.map(\n            initial: (_) => const SizedBox.shrink(),\n            loading: (_) => const Center(\n              child: SizedBox(\n                height: 24,\n                width: 24,\n                child: CircularProgressIndicator.adaptive(strokeWidth: 3),\n              ),\n            ),\n            error: (state) {\n              if (state.error != null) {\n                return Padding(\n                  padding: const EdgeInsets.all(16),\n                  child: Center(\n                    child: AppFlowyErrorPage(\n                      error: state.error!,\n                    ),\n                  ),\n                );\n              }\n\n              return ErrorWidget.withDetails(message: 'Something went wrong!');\n            },\n            ready: (state) => SettingsBody(\n              autoSeparate: false,\n              title: LocaleKeys.settings_planPage_title.tr(),\n              children: [\n                _PlanUsageSummary(\n                  usage: state.workspaceUsage,\n                  subscriptionInfo: state.subscriptionInfo,\n                ),\n                const VSpace(16),\n                _CurrentPlanBox(subscriptionInfo: state.subscriptionInfo),\n                const VSpace(16),\n                FlowyText(\n                  LocaleKeys.settings_planPage_planUsage_addons_title.tr(),\n                  fontSize: 18,\n                  color: AFThemeExtension.of(context).strongText,\n                  fontWeight: FontWeight.w600,\n                ),\n                const VSpace(8),\n                Row(\n                  children: [\n                    Flexible(\n                      child: _AddOnBox(\n                        title: LocaleKeys\n                            .settings_planPage_planUsage_addons_aiMax_title\n                            .tr(),\n                        description: LocaleKeys\n                            .settings_planPage_planUsage_addons_aiMax_description\n                            .tr(),\n                        price: LocaleKeys\n                            .settings_planPage_planUsage_addons_aiMax_price\n                            .tr(\n                          args: [SubscriptionPlanPB.AiMax.priceAnnualBilling],\n                        ),\n                        priceInfo: LocaleKeys\n                            .settings_planPage_planUsage_addons_aiMax_priceInfo\n                            .tr(),\n                        recommend: '',\n                        buttonText: state.subscriptionInfo.hasAIMax\n                            ? LocaleKeys\n                                .settings_planPage_planUsage_addons_activeLabel\n                                .tr()\n                            : LocaleKeys\n                                .settings_planPage_planUsage_addons_addLabel\n                                .tr(),\n                        isActive: state.subscriptionInfo.hasAIMax,\n                        plan: SubscriptionPlanPB.AiMax,\n                      ),\n                    ),\n                    const HSpace(8),\n                  ],\n                ),\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _CurrentPlanBox extends StatefulWidget {\n  const _CurrentPlanBox({required this.subscriptionInfo});\n\n  final WorkspaceSubscriptionInfoPB subscriptionInfo;\n\n  @override\n  State<_CurrentPlanBox> createState() => _CurrentPlanBoxState();\n}\n\nclass _CurrentPlanBoxState extends State<_CurrentPlanBox> {\n  late SettingsPlanBloc planBloc;\n\n  @override\n  void initState() {\n    super.initState();\n    planBloc = context.read<SettingsPlanBloc>();\n  }\n\n  @override\n  void didChangeDependencies() {\n    planBloc = context.read<SettingsPlanBloc>();\n    super.didChangeDependencies();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        Container(\n          margin: const EdgeInsets.only(top: 16),\n          padding: const EdgeInsets.all(16),\n          decoration: BoxDecoration(\n            border: Border.all(color: const Color(0xFFBDBDBD)),\n            borderRadius: BorderRadius.circular(16),\n          ),\n          child: Column(\n            children: [\n              Row(\n                children: [\n                  Expanded(\n                    flex: 6,\n                    child: Column(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      children: [\n                        const VSpace(4),\n                        FlowyText.semibold(\n                          widget.subscriptionInfo.label,\n                          fontSize: 24,\n                          color: AFThemeExtension.of(context).strongText,\n                        ),\n                        const VSpace(8),\n                        FlowyText.regular(\n                          widget.subscriptionInfo.info,\n                          fontSize: 14,\n                          color: AFThemeExtension.of(context).strongText,\n                          maxLines: 3,\n                        ),\n                      ],\n                    ),\n                  ),\n                  Flexible(\n                    flex: 5,\n                    child: Row(\n                      mainAxisAlignment: MainAxisAlignment.center,\n                      children: [\n                        ConstrainedBox(\n                          constraints: const BoxConstraints(maxWidth: 220),\n                          child: FlowyGradientButton(\n                            label: LocaleKeys\n                                .settings_planPage_planUsage_currentPlan_upgrade\n                                .tr(),\n                            onPressed: () => _openPricingDialog(\n                              context,\n                              context.read<SettingsPlanBloc>().workspaceId,\n                              widget.subscriptionInfo,\n                            ),\n                          ),\n                        ),\n                      ],\n                    ),\n                  ),\n                ],\n              ),\n              if (widget.subscriptionInfo.isCanceled) ...[\n                const VSpace(12),\n                FlowyText(\n                  LocaleKeys\n                      .settings_planPage_planUsage_currentPlan_canceledInfo\n                      .tr(\n                    args: [_canceledDate(context)],\n                  ),\n                  maxLines: 5,\n                  fontSize: 12,\n                  color: Theme.of(context).colorScheme.error,\n                ),\n              ],\n            ],\n          ),\n        ),\n        Positioned(\n          top: 0,\n          left: 0,\n          child: Container(\n            height: 30,\n            padding: const EdgeInsets.symmetric(horizontal: 24),\n            decoration: const BoxDecoration(\n              color: Color(0xFF4F3F5F),\n              borderRadius: BorderRadius.only(\n                topLeft: Radius.circular(4),\n                topRight: Radius.circular(4),\n                bottomRight: Radius.circular(4),\n              ),\n            ),\n            child: Center(\n              child: FlowyText.semibold(\n                LocaleKeys.settings_planPage_planUsage_currentPlan_bannerLabel\n                    .tr(),\n                fontSize: 14,\n                color: Colors.white,\n              ),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  String _canceledDate(BuildContext context) {\n    final appearance = context.read<AppearanceSettingsCubit>().state;\n    return appearance.dateFormat.formatDate(\n      widget.subscriptionInfo.planSubscription.endDate.toDateTime(),\n      false,\n    );\n  }\n\n  void _openPricingDialog(\n    BuildContext context,\n    String workspaceId,\n    WorkspaceSubscriptionInfoPB subscriptionInfo,\n  ) =>\n      showDialog(\n        context: context,\n        builder: (_) => BlocProvider<SettingsPlanBloc>.value(\n          value: planBloc,\n          child: SettingsPlanComparisonDialog(\n            workspaceId: workspaceId,\n            subscriptionInfo: subscriptionInfo,\n          ),\n        ),\n      );\n}\n\nclass _PlanUsageSummary extends StatelessWidget {\n  const _PlanUsageSummary({\n    required this.usage,\n    required this.subscriptionInfo,\n  });\n\n  final WorkspaceUsagePB usage;\n  final WorkspaceSubscriptionInfoPB subscriptionInfo;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        FlowyText.semibold(\n          LocaleKeys.settings_planPage_planUsage_title.tr(),\n          maxLines: 2,\n          fontSize: 16,\n          overflow: TextOverflow.ellipsis,\n          color: AFThemeExtension.of(context).secondaryTextColor,\n        ),\n        const VSpace(16),\n        Row(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            Expanded(\n              child: _UsageBox(\n                title: LocaleKeys.settings_planPage_planUsage_storageLabel.tr(),\n                unlimitedLabel: LocaleKeys\n                    .settings_planPage_planUsage_unlimitedStorageLabel\n                    .tr(),\n                unlimited: usage.storageBytesUnlimited,\n                label: LocaleKeys.settings_planPage_planUsage_storageUsage.tr(\n                  args: [\n                    usage.currentBlobInGb,\n                    usage.totalBlobInGb,\n                  ],\n                ),\n                value: usage.storageBytes.toInt() /\n                    usage.storageBytesLimit.toInt(),\n              ),\n            ),\n            Expanded(\n              child: _UsageBox(\n                title:\n                    LocaleKeys.settings_planPage_planUsage_aiResponseLabel.tr(),\n                label:\n                    LocaleKeys.settings_planPage_planUsage_aiResponseUsage.tr(\n                  args: [\n                    usage.aiResponsesCount.toString(),\n                    usage.aiResponsesCountLimit.toString(),\n                  ],\n                ),\n                unlimitedLabel: LocaleKeys\n                    .settings_planPage_planUsage_unlimitedAILabel\n                    .tr(),\n                unlimited: usage.aiResponsesUnlimited,\n                value: usage.aiResponsesCount.toInt() /\n                    usage.aiResponsesCountLimit.toInt(),\n              ),\n            ),\n          ],\n        ),\n        const VSpace(16),\n        SeparatedColumn(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          separatorBuilder: () => const VSpace(4),\n          children: [\n            if (subscriptionInfo.plan == WorkspacePlanPB.FreePlan) ...[\n              _ToggleMore(\n                value: false,\n                label:\n                    LocaleKeys.settings_planPage_planUsage_memberProToggle.tr(),\n                badgeLabel:\n                    LocaleKeys.settings_planPage_planUsage_proBadge.tr(),\n                onTap: () async {\n                  context.read<SettingsPlanBloc>().add(\n                        const SettingsPlanEvent.addSubscription(\n                          SubscriptionPlanPB.Pro,\n                        ),\n                      );\n                  await Future.delayed(const Duration(seconds: 2), () {});\n                },\n              ),\n            ],\n            if (!subscriptionInfo.hasAIMax && !usage.aiResponsesUnlimited) ...[\n              _ToggleMore(\n                value: false,\n                label: LocaleKeys.settings_planPage_planUsage_aiMaxToggle.tr(),\n                badgeLabel:\n                    LocaleKeys.settings_planPage_planUsage_proBadge.tr(),\n                onTap: () async {\n                  context.read<SettingsPlanBloc>().add(\n                        const SettingsPlanEvent.addSubscription(\n                          SubscriptionPlanPB.AiMax,\n                        ),\n                      );\n                  await Future.delayed(const Duration(seconds: 2), () {});\n                },\n              ),\n            ],\n          ],\n        ),\n      ],\n    );\n  }\n}\n\nclass _UsageBox extends StatelessWidget {\n  const _UsageBox({\n    required this.title,\n    required this.label,\n    required this.value,\n    required this.unlimitedLabel,\n    this.unlimited = false,\n  });\n\n  final String title;\n  final String label;\n  final double value;\n\n  final String unlimitedLabel;\n\n  // Replaces the progress bar with an unlimited badge\n  final bool unlimited;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        FlowyText.medium(\n          title,\n          fontSize: 11,\n          color: AFThemeExtension.of(context).secondaryTextColor,\n        ),\n        if (unlimited) ...[\n          Padding(\n            padding: const EdgeInsets.only(top: 4),\n            child: Row(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                const FlowySvg(\n                  FlowySvgs.check_circle_outlined_s,\n                  color: Color(0xFF9C00FB),\n                ),\n                const HSpace(4),\n                FlowyText(\n                  unlimitedLabel,\n                  fontWeight: FontWeight.w500,\n                  fontSize: 11,\n                ),\n              ],\n            ),\n          ),\n        ] else ...[\n          const VSpace(4),\n          _PlanProgressIndicator(label: label, progress: value),\n        ],\n      ],\n    );\n  }\n}\n\nclass _ToggleMore extends StatefulWidget {\n  const _ToggleMore({\n    required this.value,\n    required this.label,\n    this.badgeLabel,\n    this.onTap,\n  });\n\n  final bool value;\n  final String label;\n  final String? badgeLabel;\n  final Future<void> Function()? onTap;\n\n  @override\n  State<_ToggleMore> createState() => _ToggleMoreState();\n}\n\nclass _ToggleMoreState extends State<_ToggleMore> {\n  late bool toggleValue = widget.value;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        Toggle(\n          value: toggleValue,\n          padding: EdgeInsets.zero,\n          onChanged: (_) async {\n            if (widget.onTap == null || toggleValue) {\n              return;\n            }\n\n            setState(() => toggleValue = !toggleValue);\n            await widget.onTap!();\n\n            if (mounted) {\n              setState(() => toggleValue = !toggleValue);\n            }\n          },\n        ),\n        const HSpace(10),\n        FlowyText.regular(\n          widget.label,\n          fontSize: 14,\n          color: AFThemeExtension.of(context).strongText,\n        ),\n        if (widget.badgeLabel != null && widget.badgeLabel!.isNotEmpty) ...[\n          const HSpace(10),\n          SizedBox(\n            height: 26,\n            child: Badge(\n              padding: const EdgeInsets.symmetric(horizontal: 10),\n              backgroundColor: context.proSecondaryColor,\n              label: FlowyText.semibold(\n                widget.badgeLabel!,\n                fontSize: 12,\n                color: context.proPrimaryColor,\n              ),\n            ),\n          ),\n        ],\n      ],\n    );\n  }\n}\n\nclass _PlanProgressIndicator extends StatelessWidget {\n  const _PlanProgressIndicator({required this.label, required this.progress});\n\n  final String label;\n  final double progress;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = Theme.of(context);\n    return Row(\n      children: [\n        Expanded(\n          child: Container(\n            height: 8,\n            decoration: BoxDecoration(\n              borderRadius: BorderRadius.circular(8),\n              color: AFThemeExtension.of(context).progressBarBGColor,\n              border: Border.all(\n                color: const Color(0xFFDDF1F7).withValues(\n                  alpha: theme.brightness == Brightness.light ? 1 : 0.1,\n                ),\n              ),\n            ),\n            child: ClipRRect(\n              borderRadius: BorderRadius.circular(8),\n              child: Stack(\n                children: [\n                  FractionallySizedBox(\n                    widthFactor: progress,\n                    child: Container(\n                      decoration: BoxDecoration(\n                        color: progress >= 1\n                            ? theme.colorScheme.error\n                            : theme.colorScheme.primary,\n                      ),\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n        const HSpace(8),\n        FlowyText.medium(\n          label,\n          fontSize: 11,\n          color: AFThemeExtension.of(context).secondaryTextColor,\n        ),\n        const HSpace(16),\n      ],\n    );\n  }\n}\n\nclass _AddOnBox extends StatelessWidget {\n  const _AddOnBox({\n    required this.title,\n    required this.description,\n    required this.price,\n    required this.priceInfo,\n    required this.recommend,\n    required this.buttonText,\n    required this.isActive,\n    required this.plan,\n  });\n\n  final String title;\n  final String description;\n  final String price;\n  final String priceInfo;\n  final String recommend;\n  final String buttonText;\n  final bool isActive;\n  final SubscriptionPlanPB plan;\n\n  @override\n  Widget build(BuildContext context) {\n    final isLM = Theme.of(context).isLightMode;\n\n    return Container(\n      height: 220,\n      padding: const EdgeInsets.symmetric(\n        horizontal: 16,\n        vertical: 12,\n      ),\n      decoration: BoxDecoration(\n        border: Border.all(\n          color: isActive ? const Color(0xFFBDBDBD) : const Color(0xFF9C00FB),\n        ),\n        color: const Color(0xFFF7F8FC).withValues(alpha: 0.05),\n        borderRadius: BorderRadius.circular(16),\n      ),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          FlowyText.semibold(\n            title,\n            fontSize: 14,\n            color: AFThemeExtension.of(context).strongText,\n          ),\n          const VSpace(10),\n          FlowyText.regular(\n            description,\n            fontSize: 12,\n            color: AFThemeExtension.of(context).secondaryTextColor,\n            maxLines: 4,\n          ),\n          const VSpace(10),\n          FlowyText(\n            price,\n            fontSize: 24,\n            color: AFThemeExtension.of(context).strongText,\n          ),\n          FlowyText(\n            priceInfo,\n            fontSize: 12,\n            color: AFThemeExtension.of(context).strongText,\n          ),\n          const VSpace(12),\n          Row(\n            children: [\n              Expanded(\n                child: FlowyText(\n                  recommend,\n                  color: AFThemeExtension.of(context).secondaryTextColor,\n                  fontSize: 11,\n                  maxLines: 2,\n                ),\n              ),\n            ],\n          ),\n          const Spacer(),\n          Row(\n            children: [\n              Expanded(\n                child: FlowyTextButton(\n                  buttonText,\n                  heading: isActive\n                      ? const FlowySvg(\n                          FlowySvgs.check_circle_outlined_s,\n                          color: Color(0xFF9C00FB),\n                        )\n                      : null,\n                  mainAxisAlignment: MainAxisAlignment.center,\n                  padding:\n                      const EdgeInsets.symmetric(horizontal: 16, vertical: 7),\n                  fillColor: isActive\n                      ? const Color(0xFFE8E2EE)\n                      : isLM\n                          ? Colors.transparent\n                          : const Color(0xFF5C3699),\n                  constraints: const BoxConstraints(minWidth: 115),\n                  radius: Corners.s16Border,\n                  hoverColor: isActive\n                      ? const Color(0xFFE8E2EE)\n                      : isLM\n                          ? const Color(0xFF5C3699)\n                          : const Color(0xFF4d3472),\n                  fontColor:\n                      isLM || isActive ? const Color(0xFF5C3699) : Colors.white,\n                  fontHoverColor:\n                      isActive ? const Color(0xFF5C3699) : Colors.white,\n                  borderColor: isActive\n                      ? const Color(0xFFE8E2EE)\n                      : isLM\n                          ? const Color(0xFF5C3699)\n                          : const Color(0xFF4d3472),\n                  fontSize: 12,\n                  onPressed: isActive\n                      ? null\n                      : () => context\n                          .read<SettingsPlanBloc>()\n                          .add(SettingsPlanEvent.addSubscription(plan)),\n                ),\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n}\n\n/// Uncomment if we need it in the future\n// class _DealBox extends StatelessWidget {\n//   const _DealBox();\n\n//   @override\n//   Widget build(BuildContext context) {\n//     final isLM = Theme.of(context).brightness == Brightness.light;\n\n//     return Container(\n//       clipBehavior: Clip.antiAlias,\n//       decoration: BoxDecoration(\n//         gradient: LinearGradient(\n//           stops: isLM ? null : [.2, .3, .6],\n//           transform: isLM ? null : const GradientRotation(-.9),\n//           begin: isLM ? Alignment.centerLeft : Alignment.topRight,\n//           end: isLM ? Alignment.centerRight : Alignment.bottomLeft,\n//           colors: [\n//             isLM\n//                 ? const Color(0xFF7547C0).withAlpha(60)\n//                 : const Color(0xFF7547C0),\n//             if (!isLM) const Color.fromARGB(255, 94, 57, 153),\n//             isLM\n//                 ? const Color(0xFF251D37).withAlpha(60)\n//                 : const Color(0xFF251D37),\n//           ],\n//         ),\n//         borderRadius: BorderRadius.circular(16),\n//       ),\n//       child: Stack(\n//         children: [\n//           Padding(\n//             padding: const EdgeInsets.all(16),\n//             child: Row(\n//               children: [\n//                 Expanded(\n//                   child: Column(\n//                     crossAxisAlignment: CrossAxisAlignment.start,\n//                     children: [\n//                       const VSpace(18),\n//                       FlowyText.semibold(\n//                         LocaleKeys.settings_planPage_planUsage_deal_title.tr(),\n//                         fontSize: 24,\n//                         color: Theme.of(context).colorScheme.tertiary,\n//                       ),\n//                       const VSpace(8),\n//                       FlowyText.medium(\n//                         LocaleKeys.settings_planPage_planUsage_deal_info.tr(),\n//                         maxLines: 6,\n//                         color: Theme.of(context).colorScheme.tertiary,\n//                       ),\n//                       const VSpace(8),\n//                       FlowyGradientButton(\n//                         label: LocaleKeys\n//                             .settings_planPage_planUsage_deal_viewPlans\n//                             .tr(),\n//                         fontWeight: FontWeight.w500,\n//                         backgroundColor: isLM ? null : Colors.white,\n//                         textColor: isLM\n//                             ? Colors.white\n//                             : Theme.of(context).colorScheme.onPrimary,\n//                       ),\n//                     ],\n//                   ),\n//                 ),\n//               ],\n//             ),\n//           ),\n//           Positioned(\n//             right: 0,\n//             top: 9,\n//             child: Container(\n//               height: 32,\n//               padding: const EdgeInsets.symmetric(horizontal: 16),\n//               decoration: BoxDecoration(\n//                 gradient: LinearGradient(\n//                   transform: const GradientRotation(.7),\n//                   colors: [\n//                     if (isLM) const Color(0xFF7156DF),\n//                     isLM\n//                         ? const Color(0xFF3B2E8A)\n//                         : const Color(0xFFCE006F).withAlpha(150),\n//                     isLM ? const Color(0xFF261A48) : const Color(0xFF431459),\n//                   ],\n//                 ),\n//               ),\n//               child: Center(\n//                 child: FlowyText.semibold(\n//                   LocaleKeys.settings_planPage_planUsage_deal_bannerLabel.tr(),\n//                   fontSize: 16,\n//                   color: Colors.white,\n//                 ),\n//               ),\n//             ),\n//           ),\n//         ],\n//       ),\n//     );\n//   }\n// }\n\n/// Uncomment if we need it in the future\n// class _AddAICreditBox extends StatelessWidget {\n//   const _AddAICreditBox();\n\n//   @override\n//   Widget build(BuildContext context) {\n//     return DecoratedBox(\n//       decoration: BoxDecoration(\n//         border: Border.all(color: const Color(0xFFBDBDBD)),\n//         borderRadius: BorderRadius.circular(16),\n//       ),\n//       child: Padding(\n//         padding: const EdgeInsets.all(16),\n//         child: Column(\n//           crossAxisAlignment: CrossAxisAlignment.start,\n//           children: [\n//             FlowyText.semibold(\n//               LocaleKeys.settings_planPage_planUsage_aiCredit_title.tr(),\n//               fontSize: 18,\n//               color: AFThemeExtension.of(context).secondaryTextColor,\n//             ),\n//             const VSpace(8),\n//             Row(\n//               crossAxisAlignment: CrossAxisAlignment.start,\n//               children: [\n//                 Flexible(\n//                   flex: 5,\n//                   child: ConstrainedBox(\n//                     constraints: const BoxConstraints(maxWidth: 180),\n//                     child: Column(\n//                       mainAxisSize: MainAxisSize.min,\n//                       crossAxisAlignment: CrossAxisAlignment.start,\n//                       children: [\n//                         FlowyText.semibold(\n//                           LocaleKeys.settings_planPage_planUsage_aiCredit_price\n//                               .tr(args: ['5\\$]),\n//                           fontSize: 24,\n//                         ),\n//                         FlowyText.medium(\n//                           LocaleKeys\n//                               .settings_planPage_planUsage_aiCredit_priceDescription\n//                               .tr(),\n//                           fontSize: 14,\n//                           color:\n//                               AFThemeExtension.of(context).secondaryTextColor,\n//                         ),\n//                         const VSpace(8),\n//                         FlowyGradientButton(\n//                           label: LocaleKeys\n//                               .settings_planPage_planUsage_aiCredit_purchase\n//                               .tr(),\n//                         ),\n//                       ],\n//                     ),\n//                   ),\n//                 ),\n//                 const HSpace(16),\n//                 Flexible(\n//                   flex: 6,\n//                   child: Column(\n//                     crossAxisAlignment: CrossAxisAlignment.start,\n//                     mainAxisSize: MainAxisSize.min,\n//                     children: [\n//                       FlowyText.regular(\n//                         LocaleKeys.settings_planPage_planUsage_aiCredit_info\n//                             .tr(),\n//                         overflow: TextOverflow.ellipsis,\n//                         maxLines: 5,\n//                       ),\n//                       const VSpace(8),\n//                       SeparatedColumn(\n//                         separatorBuilder: () => const VSpace(4),\n//                         children: [\n//                           _AIStarItem(\n//                             label: LocaleKeys\n//                                 .settings_planPage_planUsage_aiCredit_infoItemOne\n//                                 .tr(),\n//                           ),\n//                           _AIStarItem(\n//                             label: LocaleKeys\n//                                 .settings_planPage_planUsage_aiCredit_infoItemTwo\n//                                 .tr(),\n//                           ),\n//                         ],\n//                       ),\n//                     ],\n//                   ),\n//                 ),\n//               ],\n//             ),\n//           ],\n//         ),\n//       ),\n//     );\n//   }\n// }\n\n/// Uncomment if we need it in the future\n// class _AIStarItem extends StatelessWidget {\n//   const _AIStarItem({required this.label});\n\n//   final String label;\n\n//   @override\n//   Widget build(BuildContext context) {\n//     return Row(\n//       children: [\n//         const FlowySvg(FlowySvgs.ai_star_s, color: Color(0xFF750D7E)),\n//         const HSpace(4),\n//         Expanded(child: FlowyText(label, maxLines: 2)),\n//       ],\n//     );\n//   }\n// }\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/math_equation/math_equation_shortcut.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart';\nimport 'package:appflowy/shared/error_page/error_page.dart';\nimport 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass SettingsShortcutsView extends StatefulWidget {\n  const SettingsShortcutsView({super.key});\n\n  @override\n  State<SettingsShortcutsView> createState() => _SettingsShortcutsViewState();\n}\n\nclass _SettingsShortcutsViewState extends State<SettingsShortcutsView> {\n  String _query = '';\n  bool _isEditing = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (_) =>\n          ShortcutsCubit(SettingsShortcutService())..fetchShortcuts(),\n      child: Builder(\n        builder: (context) => SettingsBody(\n          title: LocaleKeys.settings_shortcutsPage_title.tr(),\n          autoSeparate: false,\n          children: [\n            Row(\n              children: [\n                Flexible(\n                  child: _SearchBar(\n                    onSearchChanged: (v) => setState(() => _query = v),\n                  ),\n                ),\n                const HSpace(10),\n                _ResetButton(\n                  onReset: () {\n                    showConfirmDialog(\n                      context: context,\n                      title: LocaleKeys.settings_shortcutsPage_resetDialog_title\n                          .tr(),\n                      description: LocaleKeys\n                          .settings_shortcutsPage_resetDialog_description\n                          .tr(),\n                      confirmLabel: LocaleKeys\n                          .settings_shortcutsPage_resetDialog_buttonLabel\n                          .tr(),\n                      onConfirm: (_) {\n                        context.read<ShortcutsCubit>().resetToDefault();\n                        Navigator.of(context).pop();\n                      },\n                      style: ConfirmPopupStyle.cancelAndOk,\n                    );\n                  },\n                ),\n              ],\n            ),\n            BlocBuilder<ShortcutsCubit, ShortcutsState>(\n              builder: (context, state) {\n                final filtered = state.commandShortcutEvents\n                    .where(\n                      (e) => e.afLabel\n                          .toLowerCase()\n                          .contains(_query.toLowerCase()),\n                    )\n                    .toList();\n\n                return Column(\n                  children: [\n                    const VSpace(16),\n                    if (state.status.isLoading) ...[\n                      const CircularProgressIndicator(),\n                    ] else if (state.status.isFailure) ...[\n                      FlowyErrorPage.message(\n                        LocaleKeys.settings_shortcutsPage_errorPage_message\n                            .tr(args: [state.error]),\n                        howToFix: LocaleKeys\n                            .settings_shortcutsPage_errorPage_howToFix\n                            .tr(),\n                      ),\n                    ] else ...[\n                      ListView.builder(\n                        physics: const NeverScrollableScrollPhysics(),\n                        shrinkWrap: true,\n                        itemCount: filtered.length,\n                        itemBuilder: (context, index) => ShortcutSettingTile(\n                          command: filtered[index],\n                          canStartEditing: () => !_isEditing,\n                          onStartEditing: () =>\n                              setState(() => _isEditing = true),\n                          onFinishEditing: () =>\n                              setState(() => _isEditing = false),\n                        ),\n                      ),\n                    ],\n                  ],\n                );\n              },\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _SearchBar extends StatelessWidget {\n  const _SearchBar({this.onSearchChanged});\n\n  final void Function(String)? onSearchChanged;\n\n  @override\n  Widget build(BuildContext context) {\n    return AFTextField(\n      onChanged: onSearchChanged,\n      hintText: LocaleKeys.settings_shortcutsPage_searchHint.tr(),\n    );\n  }\n}\n\nclass _ResetButton extends StatelessWidget {\n  const _ResetButton({this.onReset});\n\n  final void Function()? onReset;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      behavior: HitTestBehavior.translucent,\n      onTap: onReset,\n      child: FlowyHover(\n        child: Padding(\n          padding: const EdgeInsets.symmetric(\n            vertical: 4.0,\n            horizontal: 6,\n          ),\n          child: Row(\n            children: [\n              const FlowySvg(\n                FlowySvgs.restore_s,\n                size: Size.square(20),\n              ),\n              const HSpace(6),\n              SizedBox(\n                height: 16,\n                child: FlowyText.regular(\n                  LocaleKeys.settings_shortcutsPage_actions_resetDefault.tr(),\n                  color: AFThemeExtension.of(context).strongText,\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass ShortcutSettingTile extends StatefulWidget {\n  const ShortcutSettingTile({\n    super.key,\n    required this.command,\n    required this.onStartEditing,\n    required this.onFinishEditing,\n    required this.canStartEditing,\n  });\n\n  final CommandShortcutEvent command;\n  final VoidCallback onStartEditing;\n  final VoidCallback onFinishEditing;\n  final bool Function() canStartEditing;\n\n  @override\n  State<ShortcutSettingTile> createState() => _ShortcutSettingTileState();\n}\n\nclass _ShortcutSettingTileState extends State<ShortcutSettingTile> {\n  final keybindController = TextEditingController();\n\n  late final FocusNode focusNode;\n\n  bool isHovering = false;\n  bool isEditing = false;\n  bool canClickOutside = false;\n\n  @override\n  void initState() {\n    super.initState();\n    focusNode = FocusNode(\n      onKeyEvent: (focusNode, key) {\n        if (key is! KeyDownEvent && key is! KeyRepeatEvent) {\n          return KeyEventResult.ignored;\n        }\n\n        if (key.logicalKey == LogicalKeyboardKey.enter &&\n            !HardwareKeyboard.instance.isShiftPressed) {\n          if (keybindController.text == widget.command.command) {\n            _finishEditing();\n            return KeyEventResult.handled;\n          }\n\n          final conflict = context.read<ShortcutsCubit>().getConflict(\n                widget.command,\n                keybindController.text,\n              );\n\n          if (conflict != null) {\n            canClickOutside = true;\n            SettingsAlertDialog(\n              title: LocaleKeys.settings_shortcutsPage_conflictDialog_title\n                  .tr(args: [keybindController.text]),\n              confirm: () {\n                conflict.clearCommand();\n                _updateCommand();\n                Navigator.of(context).pop();\n              },\n              confirmLabel: LocaleKeys\n                  .settings_shortcutsPage_conflictDialog_confirmLabel\n                  .tr(),\n              children: [\n                RichText(\n                  textAlign: TextAlign.center,\n                  text: TextSpan(\n                    style: Theme.of(context).textTheme.bodySmall?.copyWith(\n                          fontSize: 16,\n                          fontWeight: FontWeight.normal,\n                        ),\n                    children: [\n                      TextSpan(\n                        text: LocaleKeys\n                            .settings_shortcutsPage_conflictDialog_descriptionPrefix\n                            .tr(),\n                      ),\n                      TextSpan(\n                        text: conflict.afLabel,\n                        style: Theme.of(context).textTheme.bodySmall?.copyWith(\n                              fontSize: 16,\n                              fontWeight: FontWeight.bold,\n                            ),\n                      ),\n                      TextSpan(\n                        text: LocaleKeys\n                            .settings_shortcutsPage_conflictDialog_descriptionSuffix\n                            .tr(args: [keybindController.text]),\n                      ),\n                    ],\n                  ),\n                ),\n              ],\n            ).show(context).then((_) => canClickOutside = false);\n          } else {\n            _updateCommand();\n          }\n        } else if (key.logicalKey == LogicalKeyboardKey.escape) {\n          _finishEditing();\n        } else {\n          // Extract complete keybinding\n          setState(() => keybindController.text = key.toCommand);\n        }\n\n        return KeyEventResult.handled;\n      },\n    );\n  }\n\n  void _finishEditing() => setState(() {\n        isEditing = false;\n        keybindController.clear();\n        widget.onFinishEditing();\n      });\n\n  void _updateCommand() {\n    widget.command.updateCommand(command: keybindController.text);\n    context.read<ShortcutsCubit>().updateAllShortcuts();\n    _finishEditing();\n  }\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    keybindController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.symmetric(vertical: 4),\n      decoration: BoxDecoration(\n        border: Border(\n          top: BorderSide(color: Theme.of(context).dividerColor),\n        ),\n      ),\n      child: FlowyHover(\n        cursor: MouseCursor.defer,\n        style: HoverStyle(\n          hoverColor: Theme.of(context).colorScheme.secondaryContainer,\n          borderRadius: BorderRadius.zero,\n        ),\n        resetHoverOnRebuild: false,\n        builder: (context, isHovering) => Padding(\n          padding: const EdgeInsets.symmetric(vertical: 4),\n          child: Row(\n            children: [\n              const HSpace(8),\n              Expanded(\n                child: Padding(\n                  padding: const EdgeInsets.only(right: 10),\n                  child: FlowyText.regular(\n                    widget.command.afLabel,\n                    fontSize: 14,\n                    lineHeight: 1,\n                    maxLines: 2,\n                    color: AFThemeExtension.of(context).strongText,\n                  ),\n                ),\n              ),\n              Expanded(\n                child: isEditing\n                    ? _renderKeybindEditor()\n                    : _renderKeybindings(isHovering),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _renderKeybindings(bool isHovering) => Row(\n        children: [\n          if (widget.command.keybindings.isNotEmpty) ...[\n            ..._toParts(widget.command.keybindings.first).map(\n              (key) => KeyBadge(keyLabel: key),\n            ),\n          ] else ...[\n            const SizedBox(height: 24),\n          ],\n          const Spacer(),\n          if (isHovering)\n            GestureDetector(\n              onTap: () {\n                if (widget.canStartEditing()) {\n                  setState(() {\n                    widget.onStartEditing();\n                    isEditing = true;\n                  });\n                }\n              },\n              child: MouseRegion(\n                cursor: SystemMouseCursors.click,\n                child: FlowyTooltip(\n                  message: LocaleKeys.settings_shortcutsPage_editTooltip.tr(),\n                  child: const FlowySvg(\n                    FlowySvgs.edit_s,\n                    size: Size.square(16),\n                  ),\n                ),\n              ),\n            ),\n          const HSpace(8),\n        ],\n      );\n\n  Widget _renderKeybindEditor() => TapRegion(\n        onTapOutside: canClickOutside ? null : (_) => _finishEditing(),\n        child: FlowyTextField(\n          focusNode: focusNode,\n          controller: keybindController,\n          hintText: LocaleKeys.settings_shortcutsPage_editBindingHint.tr(),\n          onChanged: (_) => setState(() {}),\n          suffixIcon: keybindController.text.isNotEmpty\n              ? MouseRegion(\n                  cursor: SystemMouseCursors.click,\n                  child: GestureDetector(\n                    onTap: () => setState(() => keybindController.clear()),\n                    child: const FlowySvg(\n                      FlowySvgs.close_s,\n                      size: Size.square(10),\n                    ),\n                  ),\n                )\n              : null,\n        ),\n      );\n\n  List<String> _toParts(Keybinding binding) {\n    final List<String> keys = [];\n\n    if (binding.isControlPressed) {\n      keys.add('ctrl');\n    }\n    if (binding.isMetaPressed) {\n      keys.add('meta');\n    }\n    if (binding.isShiftPressed) {\n      keys.add('shift');\n    }\n    if (binding.isAltPressed) {\n      keys.add('alt');\n    }\n\n    return keys..add(binding.keyLabel);\n  }\n}\n\n@visibleForTesting\nclass KeyBadge extends StatelessWidget {\n  const KeyBadge({super.key, required this.keyLabel});\n\n  final String keyLabel;\n\n  @override\n  Widget build(BuildContext context) {\n    if (iconData == null && keyLabel.isEmpty) {\n      return const SizedBox.shrink();\n    }\n\n    return Container(\n      height: 24,\n      margin: const EdgeInsets.only(right: 4),\n      padding: const EdgeInsets.symmetric(horizontal: 8),\n      decoration: BoxDecoration(\n        color: AFThemeExtension.of(context).greySelect,\n        borderRadius: Corners.s4Border,\n        boxShadow: [\n          BoxShadow(\n            color: Colors.black.withValues(alpha: 0.25),\n            blurRadius: 1,\n            offset: const Offset(0, 1),\n          ),\n        ],\n      ),\n      child: Center(\n        child: iconData != null\n            ? FlowySvg(iconData!, color: Colors.black)\n            : FlowyText.medium(\n                keyLabel.toLowerCase(),\n                fontSize: 12,\n                color: Colors.black,\n              ),\n      ),\n    );\n  }\n\n  FlowySvgData? get iconData => switch (keyLabel) {\n        'meta' => FlowySvgs.keyboard_meta_s,\n        'arrow left' => FlowySvgs.keyboard_arrow_left_s,\n        'arrow right' => FlowySvgs.keyboard_arrow_right_s,\n        'arrow up' => FlowySvgs.keyboard_arrow_up_s,\n        'arrow down' => FlowySvgs.keyboard_arrow_down_s,\n        'shift' => FlowySvgs.keyboard_shift_s,\n        'tab' => FlowySvgs.keyboard_tab_s,\n        'enter' || 'return' => FlowySvgs.keyboard_return_s,\n        'opt' || 'option' => FlowySvgs.keyboard_option_s,\n        _ => null,\n      };\n}\n\nextension ToCommand on KeyEvent {\n  String get toCommand {\n    String command = '';\n    if (HardwareKeyboard.instance.isControlPressed) {\n      command += 'ctrl+';\n    }\n    if (HardwareKeyboard.instance.isMetaPressed) {\n      command += 'meta+';\n    }\n    if (HardwareKeyboard.instance.isShiftPressed) {\n      command += 'shift+';\n    }\n    if (HardwareKeyboard.instance.isAltPressed) {\n      command += 'alt+';\n    }\n\n    if ([\n      LogicalKeyboardKey.control,\n      LogicalKeyboardKey.controlLeft,\n      LogicalKeyboardKey.controlRight,\n      LogicalKeyboardKey.meta,\n      LogicalKeyboardKey.metaLeft,\n      LogicalKeyboardKey.metaRight,\n      LogicalKeyboardKey.alt,\n      LogicalKeyboardKey.altLeft,\n      LogicalKeyboardKey.altRight,\n      LogicalKeyboardKey.shift,\n      LogicalKeyboardKey.shiftLeft,\n      LogicalKeyboardKey.shiftRight,\n    ].contains(logicalKey)) {\n      return command;\n    }\n\n    final keyPressed = keyToCodeMapping.keys.firstWhere(\n      (k) => keyToCodeMapping[k] == logicalKey.keyId,\n      orElse: () => '',\n    );\n\n    return command += keyPressed;\n  }\n}\n\nextension CommandLabel on CommandShortcutEvent {\n  String get afLabel {\n    String? label;\n\n    if (key == toggleToggleListCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_toggleToDoList.tr();\n    } else if (key == insertNewParagraphNextToCodeBlockCommand('').key) {\n      label = LocaleKeys\n          .settings_shortcutsPage_keybindings_insertNewParagraphInCodeblock\n          .tr();\n    } else if (key == pasteInCodeblock('').key) {\n      label =\n          LocaleKeys.settings_shortcutsPage_keybindings_pasteInCodeblock.tr();\n    } else if (key == selectAllInCodeBlockCommand('').key) {\n      label =\n          LocaleKeys.settings_shortcutsPage_keybindings_selectAllCodeblock.tr();\n    } else if (key == tabToInsertSpacesInCodeBlockCommand('').key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_indentLineCodeblock\n          .tr();\n    } else if (key == tabToDeleteSpacesInCodeBlockCommand('').key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_outdentLineCodeblock\n          .tr();\n    } else if (key == tabSpacesAtCurosrInCodeBlockCommand('').key) {\n      label = LocaleKeys\n          .settings_shortcutsPage_keybindings_twoSpacesCursorCodeblock\n          .tr();\n    } else if (key == customCopyCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_copy.tr();\n    } else if (key == customPasteCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_paste.tr();\n    } else if (key == customCutCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_cut.tr();\n    } else if (key == customTextLeftAlignCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_alignLeft.tr();\n    } else if (key == customTextCenterAlignCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_alignCenter.tr();\n    } else if (key == customTextRightAlignCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_alignRight.tr();\n    } else if (key == insertInlineMathEquationCommand.key) {\n      label = LocaleKeys\n          .settings_shortcutsPage_keybindings_insertInlineMathEquation\n          .tr();\n    } else if (key == undoCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_undo.tr();\n    } else if (key == redoCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_redo.tr();\n    } else if (key == convertToParagraphCommand.key) {\n      label =\n          LocaleKeys.settings_shortcutsPage_keybindings_convertToParagraph.tr();\n    } else if (key == backspaceCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_backspace.tr();\n    } else if (key == deleteLeftWordCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_deleteLeftWord.tr();\n    } else if (key == deleteLeftSentenceCommand.key) {\n      label =\n          LocaleKeys.settings_shortcutsPage_keybindings_deleteLeftSentence.tr();\n    } else if (key == deleteCommand.key) {\n      label = UniversalPlatform.isMacOS\n          ? LocaleKeys.settings_shortcutsPage_keybindings_deleteMacOS.tr()\n          : LocaleKeys.settings_shortcutsPage_keybindings_delete.tr();\n    } else if (key == deleteRightWordCommand.key) {\n      label =\n          LocaleKeys.settings_shortcutsPage_keybindings_deleteRightWord.tr();\n    } else if (key == moveCursorLeftCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorLeft.tr();\n    } else if (key == moveCursorToBeginCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorBeginning\n          .tr();\n    } else if (key == moveCursorToLeftWordCommand.key) {\n      label =\n          LocaleKeys.settings_shortcutsPage_keybindings_moveCursorLeftWord.tr();\n    } else if (key == moveCursorLeftSelectCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorLeftSelect\n          .tr();\n    } else if (key == moveCursorBeginSelectCommand.key) {\n      label = LocaleKeys\n          .settings_shortcutsPage_keybindings_moveCursorBeginSelect\n          .tr();\n    } else if (key == moveCursorLeftWordSelectCommand.key) {\n      label = LocaleKeys\n          .settings_shortcutsPage_keybindings_moveCursorLeftWordSelect\n          .tr();\n    } else if (key == moveCursorRightCommand.key) {\n      label =\n          LocaleKeys.settings_shortcutsPage_keybindings_moveCursorRight.tr();\n    } else if (key == moveCursorToEndCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorEnd.tr();\n    } else if (key == moveCursorToRightWordCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorRightWord\n          .tr();\n    } else if (key == moveCursorRightSelectCommand.key) {\n      label = LocaleKeys\n          .settings_shortcutsPage_keybindings_moveCursorRightSelect\n          .tr();\n    } else if (key == moveCursorEndSelectCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorEndSelect\n          .tr();\n    } else if (key == moveCursorRightWordSelectCommand.key) {\n      label = LocaleKeys\n          .settings_shortcutsPage_keybindings_moveCursorRightWordSelect\n          .tr();\n    } else if (key == moveCursorUpCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorUp.tr();\n    } else if (key == moveCursorTopSelectCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorTopSelect\n          .tr();\n    } else if (key == moveCursorTopCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorTop.tr();\n    } else if (key == moveCursorUpSelectCommand.key) {\n      label =\n          LocaleKeys.settings_shortcutsPage_keybindings_moveCursorUpSelect.tr();\n    } else if (key == moveCursorDownCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorDown.tr();\n    } else if (key == moveCursorBottomSelectCommand.key) {\n      label = LocaleKeys\n          .settings_shortcutsPage_keybindings_moveCursorBottomSelect\n          .tr();\n    } else if (key == moveCursorBottomCommand.key) {\n      label =\n          LocaleKeys.settings_shortcutsPage_keybindings_moveCursorBottom.tr();\n    } else if (key == moveCursorDownSelectCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_moveCursorDownSelect\n          .tr();\n    } else if (key == homeCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_home.tr();\n    } else if (key == endCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_end.tr();\n    } else if (key == toggleBoldCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_toggleBold.tr();\n    } else if (key == toggleItalicCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_toggleItalic.tr();\n    } else if (key == toggleUnderlineCommand.key) {\n      label =\n          LocaleKeys.settings_shortcutsPage_keybindings_toggleUnderline.tr();\n    } else if (key == toggleStrikethroughCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_toggleStrikethrough\n          .tr();\n    } else if (key == toggleCodeCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_toggleCode.tr();\n    } else if (key == toggleHighlightCommand.key) {\n      label =\n          LocaleKeys.settings_shortcutsPage_keybindings_toggleHighlight.tr();\n    } else if (key == showLinkMenuCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_showLinkMenu.tr();\n    } else if (key == openInlineLinkCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_openInlineLink.tr();\n    } else if (key == openLinksCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_openLinks.tr();\n    } else if (key == indentCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_indent.tr();\n    } else if (key == outdentCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_outdent.tr();\n    } else if (key == exitEditingCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_exit.tr();\n    } else if (key == pageUpCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_pageUp.tr();\n    } else if (key == pageDownCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_pageDown.tr();\n    } else if (key == selectAllCommand.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_selectAll.tr();\n    } else if (key == pasteTextWithoutFormattingCommand.key) {\n      label = LocaleKeys\n          .settings_shortcutsPage_keybindings_pasteWithoutFormatting\n          .tr();\n    } else if (key == emojiShortcutEvent.key) {\n      label =\n          LocaleKeys.settings_shortcutsPage_keybindings_showEmojiPicker.tr();\n    } else if (key == enterInTableCell.key) {\n      label =\n          LocaleKeys.settings_shortcutsPage_keybindings_enterInTableCell.tr();\n    } else if (key == leftInTableCell.key) {\n      label =\n          LocaleKeys.settings_shortcutsPage_keybindings_leftInTableCell.tr();\n    } else if (key == rightInTableCell.key) {\n      label =\n          LocaleKeys.settings_shortcutsPage_keybindings_rightInTableCell.tr();\n    } else if (key == upInTableCell.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_upInTableCell.tr();\n    } else if (key == downInTableCell.key) {\n      label =\n          LocaleKeys.settings_shortcutsPage_keybindings_downInTableCell.tr();\n    } else if (key == tabInTableCell.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_tabInTableCell.tr();\n    } else if (key == shiftTabInTableCell.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_shiftTabInTableCell\n          .tr();\n    } else if (key == backSpaceInTableCell.key) {\n      label = LocaleKeys.settings_shortcutsPage_keybindings_backSpaceInTableCell\n          .tr();\n    }\n\n    return label ?? description?.capitalize() ?? '';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:appflowy/shared/af_role_pb_extension.dart';\nimport 'package:appflowy/shared/google_fonts_extension.dart';\nimport 'package:appflowy/util/font_family_extension.dart';\nimport 'package:appflowy/workspace/application/appearance_defaults.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';\nimport 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart';\nimport 'package:appflowy/workspace/application/settings/workspace/workspace_settings_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';\nimport 'package:appflowy/workspace/presentation/home/toast.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/document_color_setting_button.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/setting_action.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_dashed_divider.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_radio_select.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/language.dart';\nimport 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart';\nimport 'package:flowy_infra/plugins/bloc/dynamic_plugin_event.dart';\nimport 'package:flowy_infra/plugins/bloc/dynamic_plugin_state.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:google_fonts/google_fonts.dart';\n\nclass SettingsWorkspaceView extends StatelessWidget {\n  const SettingsWorkspaceView({\n    super.key,\n    required this.userProfile,\n    this.currentWorkspaceMemberRole,\n  });\n\n  final UserProfilePB userProfile;\n  final AFRolePB? currentWorkspaceMemberRole;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<WorkspaceSettingsBloc>(\n      create: (context) => WorkspaceSettingsBloc()\n        ..add(WorkspaceSettingsEvent.initial(userProfile: userProfile)),\n      child: BlocConsumer<WorkspaceSettingsBloc, WorkspaceSettingsState>(\n        listener: (context, state) {\n          if (state.deleteWorkspace) {\n            context.read<UserWorkspaceBloc>().add(\n                  UserWorkspaceEvent.deleteWorkspace(\n                    workspaceId: state.workspace!.workspaceId,\n                  ),\n                );\n            Navigator.of(context).pop();\n          }\n          if (state.leaveWorkspace) {\n            context.read<UserWorkspaceBloc>().add(\n                  UserWorkspaceEvent.leaveWorkspace(\n                    workspaceId: state.workspace!.workspaceId,\n                  ),\n                );\n            Navigator.of(context).pop();\n          }\n        },\n        builder: (context, state) {\n          return SettingsBody(\n            title: LocaleKeys.settings_workspacePage_title.tr(),\n            description: LocaleKeys.settings_workspacePage_description.tr(),\n            autoSeparate: false,\n            children: [\n              // We don't allow changing workspace name/icon for local/offline\n              if (userProfile.workspaceType != WorkspaceTypePB.LocalW) ...[\n                SettingsCategory(\n                  title: LocaleKeys.settings_workspacePage_workspaceName_title\n                      .tr(),\n                  children: [\n                    _WorkspaceNameSetting(\n                      currentWorkspaceMemberRole: currentWorkspaceMemberRole,\n                    ),\n                  ],\n                ),\n                const SettingsCategorySpacer(),\n                SettingsCategory(\n                  title: LocaleKeys.settings_workspacePage_workspaceIcon_title\n                      .tr(),\n                  description: LocaleKeys\n                      .settings_workspacePage_workspaceIcon_description\n                      .tr(),\n                  children: [\n                    _WorkspaceIconSetting(\n                      enableEdit: currentWorkspaceMemberRole?.isOwner ?? false,\n                      workspace: state.workspace,\n                    ),\n                  ],\n                ),\n                const SettingsCategorySpacer(),\n              ],\n              SettingsCategory(\n                title: LocaleKeys.settings_workspacePage_appearance_title.tr(),\n                children: const [AppearanceSelector()],\n              ),\n              const VSpace(16),\n              // const SettingsCategorySpacer(),\n              SettingsCategory(\n                title: LocaleKeys.settings_workspacePage_theme_title.tr(),\n                description:\n                    LocaleKeys.settings_workspacePage_theme_description.tr(),\n                children: const [\n                  _ThemeDropdown(),\n                  _DocumentCursorColorSetting(),\n                  _DocumentSelectionColorSetting(),\n                  DocumentPaddingSetting(),\n                ],\n              ),\n              const SettingsCategorySpacer(),\n              SettingsCategory(\n                title:\n                    LocaleKeys.settings_workspacePage_workspaceFont_title.tr(),\n                children: [\n                  _FontSelectorDropdown(\n                    currentFont:\n                        context.read<AppearanceSettingsCubit>().state.font,\n                  ),\n                  SettingsDashedDivider(\n                    color: Theme.of(context).colorScheme.outline,\n                  ),\n                  SettingsCategory(\n                    title: LocaleKeys.settings_workspacePage_textDirection_title\n                        .tr(),\n                    children: const [\n                      TextDirectionSelect(),\n                      EnableRTLItemsSwitcher(),\n                    ],\n                  ),\n                ],\n              ),\n              const VSpace(16),\n              SettingsCategory(\n                title: LocaleKeys.settings_workspacePage_layoutDirection_title\n                    .tr(),\n                children: const [_LayoutDirectionSelect()],\n              ),\n              const SettingsCategorySpacer(),\n\n              SettingsCategory(\n                title: LocaleKeys.settings_workspacePage_dateTime_title.tr(),\n                children: [\n                  const _DateTimeFormatLabel(),\n                  const _TimeFormatSwitcher(),\n                  SettingsDashedDivider(\n                    color: Theme.of(context).colorScheme.outline,\n                  ),\n                  const _DateFormatDropdown(),\n                ],\n              ),\n              const SettingsCategorySpacer(),\n\n              SettingsCategory(\n                title: LocaleKeys.settings_workspacePage_language_title.tr(),\n                children: const [LanguageDropdown()],\n              ),\n              const SettingsCategorySpacer(),\n\n              if (userProfile.workspaceType != WorkspaceTypePB.LocalW) ...[\n                SingleSettingAction(\n                  label: LocaleKeys.settings_workspacePage_manageWorkspace_title\n                      .tr(),\n                  fontSize: 16,\n                  fontWeight: FontWeight.w600,\n                  onPressed: () => showConfirmDialog(\n                    context: context,\n                    title: currentWorkspaceMemberRole?.isOwner ?? false\n                        ? LocaleKeys\n                            .settings_workspacePage_deleteWorkspacePrompt_title\n                            .tr()\n                        : LocaleKeys\n                            .settings_workspacePage_leaveWorkspacePrompt_title\n                            .tr(),\n                    description: currentWorkspaceMemberRole?.isOwner ?? false\n                        ? LocaleKeys\n                            .settings_workspacePage_deleteWorkspacePrompt_content\n                            .tr()\n                        : LocaleKeys\n                            .settings_workspacePage_leaveWorkspacePrompt_content\n                            .tr(),\n                    style: ConfirmPopupStyle.cancelAndOk,\n                    onConfirm: (_) => context.read<WorkspaceSettingsBloc>().add(\n                          currentWorkspaceMemberRole?.isOwner ?? false\n                              ? const WorkspaceSettingsEvent.deleteWorkspace()\n                              : const WorkspaceSettingsEvent.leaveWorkspace(),\n                        ),\n                  ),\n                  buttonType: SingleSettingsButtonType.danger,\n                  buttonLabel: currentWorkspaceMemberRole?.isOwner ?? false\n                      ? LocaleKeys\n                          .settings_workspacePage_manageWorkspace_deleteWorkspace\n                          .tr()\n                      : LocaleKeys\n                          .settings_workspacePage_manageWorkspace_leaveWorkspace\n                          .tr(),\n                ),\n              ],\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _WorkspaceNameSetting extends StatefulWidget {\n  const _WorkspaceNameSetting({\n    this.currentWorkspaceMemberRole,\n  });\n\n  final AFRolePB? currentWorkspaceMemberRole;\n\n  @override\n  State<_WorkspaceNameSetting> createState() => _WorkspaceNameSettingState();\n}\n\nclass _WorkspaceNameSettingState extends State<_WorkspaceNameSetting> {\n  final TextEditingController workspaceNameController = TextEditingController();\n  final focusNode = FocusNode();\n\n  Timer? debounce;\n  bool isSaving = false;\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    workspaceNameController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocConsumer<WorkspaceSettingsBloc, WorkspaceSettingsState>(\n      listener: (_, state) {\n        if (isSaving) {\n          return;\n        }\n\n        final newName = state.workspace?.name;\n        if (newName != null && newName != workspaceNameController.text) {\n          workspaceNameController.text = newName;\n        }\n      },\n      builder: (_, state) {\n        if (widget.currentWorkspaceMemberRole == null ||\n            !widget.currentWorkspaceMemberRole!.isOwner) {\n          return Padding(\n            padding: const EdgeInsets.symmetric(vertical: 2.5),\n            child: FlowyText.regular(\n              workspaceNameController.text,\n              fontSize: 14,\n            ),\n          );\n        }\n\n        return Flexible(\n          child: SettingsInputField(\n            textController: workspaceNameController,\n            value: workspaceNameController.text,\n            focusNode: focusNode,\n            onSave: (_) =>\n                _saveWorkspaceName(name: workspaceNameController.text),\n            onChanged: _debounceSaveName,\n            hideActions: true,\n          ),\n        );\n      },\n    );\n  }\n\n  void _debounceSaveName(String name) {\n    isSaving = true;\n\n    debounce?.cancel();\n    debounce = Timer(\n      const Duration(milliseconds: 300),\n      () {\n        _saveWorkspaceName(name: name);\n        isSaving = false;\n      },\n    );\n  }\n\n  void _saveWorkspaceName({required String name}) {\n    if (name.isNotEmpty) {\n      context\n          .read<WorkspaceSettingsBloc>()\n          .add(WorkspaceSettingsEvent.updateWorkspaceName(name));\n    }\n  }\n}\n\n@visibleForTesting\nclass LanguageDropdown extends StatelessWidget {\n  const LanguageDropdown({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(\n      builder: (context, state) {\n        return SettingsDropdown<Locale>(\n          key: const Key('LanguageDropdown'),\n          expandWidth: false,\n          onChanged: (locale) => context\n              .read<AppearanceSettingsCubit>()\n              .setLocale(context, locale),\n          selectedOption: state.locale,\n          options: EasyLocalization.of(context)!\n              .supportedLocales\n              .map(\n                (locale) => buildDropdownMenuEntry<Locale>(\n                  context,\n                  selectedValue: state.locale,\n                  value: locale,\n                  label: languageFromLocale(locale),\n                ),\n              )\n              .toList(),\n        );\n      },\n    );\n  }\n}\n\nclass _WorkspaceIconSetting extends StatelessWidget {\n  const _WorkspaceIconSetting({\n    required this.enableEdit,\n    this.workspace,\n  });\n\n  final bool enableEdit;\n  final UserWorkspacePB? workspace;\n\n  @override\n  Widget build(BuildContext context) {\n    if (workspace == null) {\n      return const SizedBox(\n        height: 64,\n        width: 64,\n        child: CircularProgressIndicator(),\n      );\n    }\n\n    Widget child = WorkspaceIcon(\n      workspaceIcon: workspace!.icon,\n      workspaceName: workspace!.name,\n      iconSize: 64.0,\n      emojiSize: 24.0,\n      fontSize: 24.0,\n      figmaLineHeight: 26.0,\n      borderRadius: 18.0,\n      isEditable: true,\n      onSelected: (r) => context\n          .read<WorkspaceSettingsBloc>()\n          .add(WorkspaceSettingsEvent.updateWorkspaceIcon(r.emoji)),\n    );\n\n    if (!enableEdit) {\n      child = IgnorePointer(\n        child: child,\n      );\n    }\n\n    return child;\n  }\n}\n\n@visibleForTesting\nclass TextDirectionSelect extends StatelessWidget {\n  const TextDirectionSelect({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(\n      builder: (context, state) {\n        final selectedItem = state.textDirection;\n\n        return SettingsRadioSelect<AppFlowyTextDirection>(\n          onChanged: (item) {\n            context\n                .read<AppearanceSettingsCubit>()\n                .setTextDirection(item.value);\n            context\n                .read<DocumentAppearanceCubit>()\n                .syncDefaultTextDirection(item.value.name);\n          },\n          items: [\n            SettingsRadioItem(\n              value: AppFlowyTextDirection.ltr,\n              icon: const FlowySvg(FlowySvgs.textdirection_ltr_m),\n              label: LocaleKeys.settings_workspacePage_textDirection_leftToRight\n                  .tr(),\n              isSelected: selectedItem == AppFlowyTextDirection.ltr,\n            ),\n            SettingsRadioItem(\n              value: AppFlowyTextDirection.rtl,\n              icon: const FlowySvg(FlowySvgs.textdirection_rtl_m),\n              label: LocaleKeys.settings_workspacePage_textDirection_rightToLeft\n                  .tr(),\n              isSelected: selectedItem == AppFlowyTextDirection.rtl,\n            ),\n            SettingsRadioItem(\n              value: AppFlowyTextDirection.auto,\n              icon: const FlowySvg(FlowySvgs.textdirection_auto_m),\n              label: LocaleKeys.settings_workspacePage_textDirection_auto.tr(),\n              isSelected: selectedItem == AppFlowyTextDirection.auto,\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n\n@visibleForTesting\nclass EnableRTLItemsSwitcher extends StatelessWidget {\n  const EnableRTLItemsSwitcher({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        Expanded(\n          child: FlowyText.regular(\n            LocaleKeys.settings_workspacePage_textDirection_enableRTLItems.tr(),\n            fontSize: 16,\n          ),\n        ),\n        const HSpace(16),\n        Toggle(\n          value: context\n              .watch<AppearanceSettingsCubit>()\n              .state\n              .enableRtlToolbarItems,\n          onChanged: (value) => context\n              .read<AppearanceSettingsCubit>()\n              .setEnableRTLToolbarItems(value),\n        ),\n      ],\n    );\n  }\n}\n\nclass _LayoutDirectionSelect extends StatelessWidget {\n  const _LayoutDirectionSelect();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(\n      builder: (context, state) {\n        return SettingsRadioSelect<LayoutDirection>(\n          onChanged: (item) => context\n              .read<AppearanceSettingsCubit>()\n              .setLayoutDirection(item.value),\n          items: [\n            SettingsRadioItem(\n              value: LayoutDirection.ltrLayout,\n              icon: const FlowySvg(FlowySvgs.textdirection_ltr_m),\n              label: LocaleKeys\n                  .settings_workspacePage_layoutDirection_leftToRight\n                  .tr(),\n              isSelected: state.layoutDirection == LayoutDirection.ltrLayout,\n            ),\n            SettingsRadioItem(\n              value: LayoutDirection.rtlLayout,\n              icon: const FlowySvg(FlowySvgs.textdirection_rtl_m),\n              label: LocaleKeys\n                  .settings_workspacePage_layoutDirection_rightToLeft\n                  .tr(),\n              isSelected: state.layoutDirection == LayoutDirection.rtlLayout,\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n\nclass _DateFormatDropdown extends StatelessWidget {\n  const _DateFormatDropdown();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(\n      builder: (context, state) {\n        return Padding(\n          padding: const EdgeInsets.only(top: 8.0),\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              FlowyText.regular(\n                LocaleKeys.settings_workspacePage_dateTime_dateFormat_label\n                    .tr(),\n                fontSize: 16,\n              ),\n              const VSpace(8),\n              SettingsDropdown<UserDateFormatPB>(\n                key: const Key('DateFormatDropdown'),\n                expandWidth: false,\n                onChanged: (format) => context\n                    .read<AppearanceSettingsCubit>()\n                    .setDateFormat(format),\n                selectedOption: state.dateFormat,\n                options: UserDateFormatPB.values\n                    .map(\n                      (format) => buildDropdownMenuEntry<UserDateFormatPB>(\n                        context,\n                        value: format,\n                        label: _formatLabel(format),\n                      ),\n                    )\n                    .toList(),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  String _formatLabel(UserDateFormatPB format) => switch (format) {\n        UserDateFormatPB.Locally =>\n          LocaleKeys.settings_workspacePage_dateTime_dateFormat_local.tr(),\n        UserDateFormatPB.US =>\n          LocaleKeys.settings_workspacePage_dateTime_dateFormat_us.tr(),\n        UserDateFormatPB.ISO =>\n          LocaleKeys.settings_workspacePage_dateTime_dateFormat_iso.tr(),\n        UserDateFormatPB.Friendly =>\n          LocaleKeys.settings_workspacePage_dateTime_dateFormat_friendly.tr(),\n        UserDateFormatPB.DayMonthYear =>\n          LocaleKeys.settings_workspacePage_dateTime_dateFormat_dmy.tr(),\n        _ => \"Unknown format\",\n      };\n}\n\nclass _DateTimeFormatLabel extends StatelessWidget {\n  const _DateTimeFormatLabel();\n\n  @override\n  Widget build(BuildContext context) {\n    final now = DateTime.now();\n\n    return BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(\n      builder: (context, state) {\n        return FlowyText.regular(\n          LocaleKeys.settings_workspacePage_dateTime_example.tr(\n            args: [\n              state.dateFormat.formatDate(now, false),\n              state.timeFormat.formatTime(now),\n              now.timeZoneName,\n            ],\n          ),\n          maxLines: 2,\n          fontSize: 16,\n          color: AFThemeExtension.of(context).secondaryTextColor,\n        );\n      },\n    );\n  }\n}\n\nclass _TimeFormatSwitcher extends StatelessWidget {\n  const _TimeFormatSwitcher();\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        Expanded(\n          child: FlowyText.regular(\n            LocaleKeys.settings_workspacePage_dateTime_24HourTime.tr(),\n            fontSize: 16,\n          ),\n        ),\n        const HSpace(16),\n        Toggle(\n          value: context.watch<AppearanceSettingsCubit>().state.timeFormat ==\n              UserTimeFormatPB.TwentyFourHour,\n          onChanged: (value) =>\n              context.read<AppearanceSettingsCubit>().setTimeFormat(\n                    value\n                        ? UserTimeFormatPB.TwentyFourHour\n                        : UserTimeFormatPB.TwelveHour,\n                  ),\n        ),\n      ],\n    );\n  }\n}\n\nclass _ThemeDropdown extends StatelessWidget {\n  const _ThemeDropdown();\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<DynamicPluginBloc>(\n      create: (context) => DynamicPluginBloc()..add(DynamicPluginEvent.load()),\n      child: BlocBuilder<DynamicPluginBloc, DynamicPluginState>(\n        buildWhen: (_, current) => current is Ready,\n        builder: (context, state) {\n          final appearance = context.watch<AppearanceSettingsCubit>().state;\n          final isLightMode = Theme.of(context).brightness == Brightness.light;\n\n          final customThemes = state.whenOrNull(\n            ready: (ps) => ps.map((p) => p.theme).whereType<AppTheme>(),\n          );\n\n          return SettingsDropdown<String>(\n            key: const Key('ThemeSelectorDropdown'),\n            actions: [\n              SettingAction(\n                tooltip: LocaleKeys\n                    .settings_workspacePage_theme_uploadCustomThemeTooltip\n                    .tr(),\n                icon: const FlowySvg(FlowySvgs.folder_m, size: Size.square(20)),\n                onPressed: () => Dialogs.show(\n                  context,\n                  child: BlocProvider<DynamicPluginBloc>.value(\n                    value: context.read<DynamicPluginBloc>(),\n                    child: const FlowyDialog(\n                      constraints: BoxConstraints(maxHeight: 300),\n                      child: ThemeUploadWidget(),\n                    ),\n                  ),\n                ).then((val) {\n                  if (val != null && context.mounted) {\n                    showSnackBarMessage(\n                      context,\n                      LocaleKeys.settings_appearance_themeUpload_uploadSuccess\n                          .tr(),\n                    );\n                  }\n                }),\n              ),\n              SettingAction(\n                icon: const FlowySvg(\n                  FlowySvgs.restore_s,\n                  size: Size.square(20),\n                ),\n                label: LocaleKeys.settings_common_reset.tr(),\n                onPressed: () => context\n                    .read<AppearanceSettingsCubit>()\n                    .setTheme(AppTheme.builtins.first.themeName),\n              ),\n            ],\n            onChanged: (theme) =>\n                context.read<AppearanceSettingsCubit>().setTheme(theme),\n            selectedOption: appearance.appTheme.themeName,\n            options: [\n              ...AppTheme.builtins.map(\n                (t) {\n                  final theme = isLightMode ? t.lightTheme : t.darkTheme;\n\n                  return buildDropdownMenuEntry<String>(\n                    context,\n                    selectedValue: appearance.appTheme.themeName,\n                    value: t.themeName,\n                    label: t.themeName,\n                    leadingWidget: _ThemeLeading(color: theme.sidebarBg),\n                  );\n                },\n              ),\n              ...?customThemes?.map(\n                (t) {\n                  final theme = isLightMode ? t.lightTheme : t.darkTheme;\n\n                  return buildDropdownMenuEntry<String>(\n                    context,\n                    selectedValue: appearance.appTheme.themeName,\n                    value: t.themeName,\n                    label: t.themeName,\n                    leadingWidget: _ThemeLeading(color: theme.sidebarBg),\n                    trailingWidget: FlowyIconButton(\n                      icon: const FlowySvg(FlowySvgs.delete_s),\n                      iconColorOnHover: Theme.of(context).colorScheme.onSurface,\n                      onPressed: () {\n                        context.read<DynamicPluginBloc>().add(\n                              DynamicPluginEvent.removePlugin(\n                                name: t.themeName,\n                              ),\n                            );\n\n                        if (appearance.appTheme.themeName == t.themeName) {\n                          context\n                              .read<AppearanceSettingsCubit>()\n                              .setTheme(AppTheme.builtins.first.themeName);\n                        }\n                      },\n                    ),\n                  );\n                },\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _ThemeLeading extends StatelessWidget {\n  const _ThemeLeading({required this.color});\n\n  final Color color;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      width: 16,\n      height: 16,\n      decoration: BoxDecoration(\n        color: color,\n        borderRadius: Corners.s4Border,\n        border: Border.all(color: Theme.of(context).colorScheme.outline),\n      ),\n    );\n  }\n}\n\n@visibleForTesting\nclass AppearanceSelector extends StatelessWidget {\n  const AppearanceSelector({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final themeMode = context.read<AppearanceSettingsCubit>().state.themeMode;\n\n    return Row(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        ...ThemeMode.values.map(\n          (t) => Padding(\n            padding: const EdgeInsets.only(right: 16),\n            child: GestureDetector(\n              behavior: HitTestBehavior.opaque,\n              onTap: () =>\n                  context.read<AppearanceSettingsCubit>().setThemeMode(t),\n              child: FlowyHover(\n                style: HoverStyle.transparent(\n                  foregroundColorOnHover:\n                      AFThemeExtension.of(context).textColor,\n                ),\n                child: Column(\n                  children: [\n                    Container(\n                      width: 88,\n                      height: 72,\n                      decoration: BoxDecoration(\n                        border: Border.all(\n                          color: t == themeMode\n                              ? Theme.of(context).colorScheme.onSecondary\n                              : Theme.of(context).colorScheme.outline,\n                        ),\n                        borderRadius: Corners.s4Border,\n                        image: DecorationImage(\n                          fit: BoxFit.cover,\n                          image: AssetImage(\n                            'assets/images/appearance/${t.name.toLowerCase()}.png',\n                          ),\n                        ),\n                      ),\n                      child: t != themeMode\n                          ? null\n                          : const _SelectedModeIndicator(),\n                    ),\n                    const VSpace(6),\n                    FlowyText.regular(getLabel(t), textAlign: TextAlign.center),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  String getLabel(ThemeMode t) => switch (t) {\n        ThemeMode.system =>\n          LocaleKeys.settings_workspacePage_appearance_options_system.tr(),\n        ThemeMode.light =>\n          LocaleKeys.settings_workspacePage_appearance_options_light.tr(),\n        ThemeMode.dark =>\n          LocaleKeys.settings_workspacePage_appearance_options_dark.tr(),\n      };\n}\n\nclass _SelectedModeIndicator extends StatelessWidget {\n  const _SelectedModeIndicator();\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        Positioned(\n          top: 4,\n          left: 4,\n          child: Material(\n            shape: const CircleBorder(),\n            elevation: 2,\n            child: Container(\n              decoration: const BoxDecoration(\n                shape: BoxShape.circle,\n              ),\n              height: 16,\n              width: 16,\n              child: const FlowySvg(\n                FlowySvgs.settings_selected_theme_m,\n                size: Size.square(16),\n                blendMode: BlendMode.dstIn,\n              ),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass _FontSelectorDropdown extends StatefulWidget {\n  const _FontSelectorDropdown({required this.currentFont});\n\n  final String currentFont;\n\n  @override\n  State<_FontSelectorDropdown> createState() => _FontSelectorDropdownState();\n}\n\nclass _FontSelectorDropdownState extends State<_FontSelectorDropdown> {\n  late final _options = [defaultFontFamily, ...GoogleFonts.asMap().keys];\n  final _focusNode = FocusNode();\n  final _controller = PopoverController();\n  late final ScrollController _scrollController;\n  final _textController = TextEditingController();\n\n  @override\n  void initState() {\n    super.initState();\n    const itemExtent = 32;\n    final index = _options.indexOf(widget.currentFont);\n    final newPosition = (index * itemExtent).toDouble();\n    _scrollController = ScrollController(initialScrollOffset: newPosition);\n\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      _textController.text = context\n          .read<AppearanceSettingsCubit>()\n          .state\n          .font\n          .fontFamilyDisplayName;\n    });\n  }\n\n  @override\n  void dispose() {\n    _controller.close();\n    _focusNode.dispose();\n    _scrollController.dispose();\n    _textController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final appearance = context.watch<AppearanceSettingsCubit>().state;\n    return LayoutBuilder(\n      builder: (context, constraints) => AppFlowyPopover(\n        margin: EdgeInsets.zero,\n        controller: _controller,\n        skipTraversal: true,\n        triggerActions: PopoverTriggerFlags.none,\n        onClose: () {\n          _focusNode.unfocus();\n          setState(() {});\n        },\n        direction: PopoverDirection.bottomWithLeftAligned,\n        constraints: BoxConstraints(\n          maxHeight: 150,\n          maxWidth: constraints.maxWidth - 90,\n        ),\n        borderRadius: const BorderRadius.all(Radius.circular(4.0)),\n        popupBuilder: (_) => _FontListPopup(\n          currentFont: appearance.font,\n          scrollController: _scrollController,\n          controller: _controller,\n          options: _options,\n          textController: _textController,\n          focusNode: _focusNode,\n        ),\n        child: Row(\n          children: [\n            Expanded(\n              child: TapRegion(\n                behavior: HitTestBehavior.translucent,\n                onTapOutside: (_) {\n                  _focusNode.unfocus();\n                  setState(() {});\n                },\n                child: Listener(\n                  onPointerDown: (_) {\n                    _focusNode.requestFocus();\n                    setState(() {});\n                    _controller.show();\n                  },\n                  child: FlowyTextField(\n                    autoFocus: false,\n                    focusNode: _focusNode,\n                    controller: _textController,\n                    decoration: InputDecoration(\n                      suffixIcon: const MouseRegion(\n                        cursor: SystemMouseCursors.click,\n                        child: Icon(Icons.arrow_drop_down),\n                      ),\n                      counterText: '',\n                      contentPadding: const EdgeInsets.symmetric(\n                        vertical: 12,\n                        horizontal: 18,\n                      ),\n                      enabledBorder: OutlineInputBorder(\n                        borderSide: BorderSide(\n                          color: Theme.of(context).colorScheme.outline,\n                        ),\n                        borderRadius: Corners.s8Border,\n                      ),\n                      focusedBorder: OutlineInputBorder(\n                        borderSide: BorderSide(\n                          color: Theme.of(context).colorScheme.primary,\n                        ),\n                        borderRadius: Corners.s8Border,\n                      ),\n                      errorBorder: OutlineInputBorder(\n                        borderSide: BorderSide(\n                          color: Theme.of(context).colorScheme.error,\n                        ),\n                        borderRadius: Corners.s8Border,\n                      ),\n                      focusedErrorBorder: OutlineInputBorder(\n                        borderSide: BorderSide(\n                          color: Theme.of(context).colorScheme.error,\n                        ),\n                        borderRadius: Corners.s8Border,\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n            ),\n            const HSpace(16),\n            GestureDetector(\n              behavior: HitTestBehavior.opaque,\n              onTap: () => context\n                  .read<AppearanceSettingsCubit>()\n                  .setFontFamily(defaultFontFamily),\n              child: SizedBox(\n                height: 26,\n                child: FlowyHover(\n                  resetHoverOnRebuild: false,\n                  child: Padding(\n                    padding:\n                        const EdgeInsets.symmetric(horizontal: 4, vertical: 2),\n                    child: Row(\n                      children: [\n                        const FlowySvg(\n                          FlowySvgs.restore_s,\n                          size: Size.square(20),\n                        ),\n                        const HSpace(4),\n                        FlowyText.regular(\n                          LocaleKeys.settings_common_reset.tr(),\n                        ),\n                      ],\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _FontListPopup extends StatefulWidget {\n  const _FontListPopup({\n    required this.controller,\n    required this.scrollController,\n    required this.options,\n    required this.currentFont,\n    required this.textController,\n    required this.focusNode,\n  });\n\n  final ScrollController scrollController;\n  final List<String> options;\n  final String currentFont;\n  final TextEditingController textController;\n  final FocusNode focusNode;\n  final PopoverController controller;\n\n  @override\n  State<_FontListPopup> createState() => _FontListPopupState();\n}\n\nclass _FontListPopupState extends State<_FontListPopup> {\n  late List<String> _filteredOptions = widget.options;\n\n  @override\n  void initState() {\n    super.initState();\n    widget.textController.addListener(_onTextFieldChanged);\n  }\n\n  void _onTextFieldChanged() {\n    final value = widget.textController.text;\n\n    if (value.trim().isEmpty) {\n      _filteredOptions = widget.options;\n    } else {\n      if (value.fontFamilyDisplayName ==\n          widget.currentFont.fontFamilyDisplayName) {\n        return;\n      }\n\n      _filteredOptions = widget.options\n          .where(\n            (f) =>\n                f.toLowerCase().contains(value.trim().toLowerCase()) ||\n                f.fontFamilyDisplayName\n                    .toLowerCase()\n                    .contains(value.trim().fontFamilyDisplayName.toLowerCase()),\n          )\n          .toList();\n\n      // Default font family is \"\", but the display name is \"System\",\n      // which means it's hard compared to other font families to find this one.\n      if (!_filteredOptions.contains(defaultFontFamily) &&\n          'system'.contains(value.trim().toLowerCase())) {\n        _filteredOptions.insert(0, defaultFontFamily);\n      }\n    }\n\n    setState(() {});\n  }\n\n  @override\n  void dispose() {\n    widget.textController.removeListener(_onTextFieldChanged);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      type: MaterialType.transparency,\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          if (_filteredOptions.isEmpty)\n            Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),\n              child: FlowyText.medium(\n                LocaleKeys.settings_workspacePage_workspaceFont_noFontHint.tr(),\n              ),\n            ),\n          Flexible(\n            child: ListView.separated(\n              shrinkWrap: _filteredOptions.length < 10,\n              controller: widget.scrollController,\n              padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),\n              itemCount: _filteredOptions.length,\n              separatorBuilder: (_, __) => const VSpace(6),\n              itemBuilder: (context, index) {\n                final font = _filteredOptions[index];\n                final isSelected = widget.currentFont == font;\n                return SizedBox(\n                  height: 29,\n                  child: ListTile(\n                    minVerticalPadding: 0,\n                    selected: isSelected,\n                    dense: true,\n                    hoverColor: Theme.of(context)\n                        .colorScheme\n                        .onSurface\n                        .withValues(alpha: 0.12),\n                    selectedTileColor: Theme.of(context)\n                        .colorScheme\n                        .primary\n                        .withValues(alpha: 0.12),\n                    contentPadding:\n                        const EdgeInsets.symmetric(horizontal: 6, vertical: 4),\n                    minTileHeight: 0,\n                    onTap: () {\n                      context\n                          .read<AppearanceSettingsCubit>()\n                          .setFontFamily(font);\n\n                      widget.textController.text = font.fontFamilyDisplayName;\n\n                      // This is a workaround such that when dialog rebuilds due\n                      // to font changing, the font selector won't retain focus.\n                      widget.focusNode.parent?.requestFocus();\n\n                      widget.controller.close();\n                    },\n                    title: Align(\n                      alignment: AlignmentDirectional.centerStart,\n                      child: Text(\n                        font.fontFamilyDisplayName,\n                        style: TextStyle(\n                          color: AFThemeExtension.of(context).textColor,\n                          fontFamily: getGoogleFontSafely(font).fontFamily,\n                        ),\n                      ),\n                    ),\n                    trailing:\n                        isSelected ? const FlowySvg(FlowySvgs.check_s) : null,\n                  ),\n                );\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _DocumentCursorColorSetting extends StatelessWidget {\n  const _DocumentCursorColorSetting();\n\n  @override\n  Widget build(BuildContext context) {\n    final label =\n        LocaleKeys.settings_appearance_documentSettings_cursorColor.tr();\n    return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(\n      builder: (context, state) {\n        return SettingListTile(\n          label: label,\n          resetButtonKey: const Key('DocumentCursorColorResetButton'),\n          onResetRequested: () {\n            showConfirmDialog(\n              context: context,\n              title:\n                  LocaleKeys.settings_workspacePage_resetCursorColor_title.tr(),\n              description: LocaleKeys\n                  .settings_workspacePage_resetCursorColor_description\n                  .tr(),\n              style: ConfirmPopupStyle.cancelAndOk,\n              confirmLabel: LocaleKeys.settings_common_reset.tr(),\n              onConfirm: (_) => context\n                ..read<AppearanceSettingsCubit>().resetDocumentCursorColor()\n                ..read<DocumentAppearanceCubit>().syncCursorColor(null),\n            );\n          },\n          trailing: [\n            DocumentColorSettingButton(\n              key: const Key('DocumentCursorColorSettingButton'),\n              currentColor: state.cursorColor ??\n                  DefaultAppearanceSettings.getDefaultCursorColor(context),\n              previewWidgetBuilder: (color) => _CursorColorValueWidget(\n                cursorColor: color ??\n                    DefaultAppearanceSettings.getDefaultCursorColor(context),\n              ),\n              dialogTitle: label,\n              onApply: (color) => context\n                ..read<AppearanceSettingsCubit>().setDocumentCursorColor(color)\n                ..read<DocumentAppearanceCubit>().syncCursorColor(color),\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n\nclass _CursorColorValueWidget extends StatelessWidget {\n  const _CursorColorValueWidget({required this.cursorColor});\n\n  final Color cursorColor;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        Container(color: cursorColor, width: 2, height: 16),\n        FlowyText(\n          LocaleKeys.appName.tr(),\n          // To avoid the text color changes when it is hovered in dark mode\n          color: AFThemeExtension.of(context).onBackground,\n        ),\n      ],\n    );\n  }\n}\n\nclass _DocumentSelectionColorSetting extends StatelessWidget {\n  const _DocumentSelectionColorSetting();\n\n  @override\n  Widget build(BuildContext context) {\n    final label =\n        LocaleKeys.settings_appearance_documentSettings_selectionColor.tr();\n\n    return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(\n      builder: (context, state) {\n        return SettingListTile(\n          label: label,\n          resetButtonKey: const Key('DocumentSelectionColorResetButton'),\n          onResetRequested: () {\n            showConfirmDialog(\n              context: context,\n              title: LocaleKeys.settings_workspacePage_resetSelectionColor_title\n                  .tr(),\n              description: LocaleKeys\n                  .settings_workspacePage_resetSelectionColor_description\n                  .tr(),\n              style: ConfirmPopupStyle.cancelAndOk,\n              confirmLabel: LocaleKeys.settings_common_reset.tr(),\n              onConfirm: (_) => context\n                ..read<AppearanceSettingsCubit>().resetDocumentSelectionColor()\n                ..read<DocumentAppearanceCubit>().syncSelectionColor(null),\n            );\n          },\n          trailing: [\n            DocumentColorSettingButton(\n              currentColor: state.selectionColor ??\n                  DefaultAppearanceSettings.getDefaultSelectionColor(context),\n              previewWidgetBuilder: (color) => _SelectionColorValueWidget(\n                selectionColor: color ??\n                    DefaultAppearanceSettings.getDefaultSelectionColor(context),\n              ),\n              dialogTitle: label,\n              onApply: (c) => context\n                ..read<AppearanceSettingsCubit>().setDocumentSelectionColor(c)\n                ..read<DocumentAppearanceCubit>().syncSelectionColor(c),\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n\nclass _SelectionColorValueWidget extends StatelessWidget {\n  const _SelectionColorValueWidget({required this.selectionColor});\n\n  final Color selectionColor;\n\n  @override\n  Widget build(BuildContext context) {\n    // To avoid the text color changes when it is hovered in dark mode\n    final textColor = AFThemeExtension.of(context).onBackground;\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        Container(\n          color: selectionColor,\n          child: FlowyText(\n            LocaleKeys.settings_appearance_documentSettings_app.tr(),\n            color: textColor,\n          ),\n        ),\n        FlowyText(\n          LocaleKeys.settings_appearance_documentSettings_flowy.tr(),\n          color: textColor,\n        ),\n      ],\n    );\n  }\n}\n\nclass DocumentPaddingSetting extends StatelessWidget {\n  const DocumentPaddingSetting({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(\n      builder: (context, state) {\n        return Column(\n          children: [\n            Row(\n              children: [\n                FlowyText.medium(\n                  LocaleKeys.settings_appearance_documentSettings_width.tr(),\n                ),\n                const Spacer(),\n                SettingsResetButton(\n                  onResetRequested: () =>\n                      context.read<DocumentAppearanceCubit>().syncWidth(null),\n                ),\n              ],\n            ),\n            const VSpace(6),\n            Container(\n              height: 32,\n              padding: const EdgeInsets.only(right: 4),\n              child: _DocumentPaddingSlider(\n                onPaddingChanged: (value) {\n                  context.read<DocumentAppearanceCubit>().syncWidth(value);\n                },\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n\nclass _DocumentPaddingSlider extends StatefulWidget {\n  const _DocumentPaddingSlider({\n    required this.onPaddingChanged,\n  });\n\n  final void Function(double) onPaddingChanged;\n\n  @override\n  State<_DocumentPaddingSlider> createState() => _DocumentPaddingSliderState();\n}\n\nclass _DocumentPaddingSliderState extends State<_DocumentPaddingSlider> {\n  late double width;\n\n  @override\n  void initState() {\n    super.initState();\n\n    width = context.read<DocumentAppearanceCubit>().state.width;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(\n      builder: (context, state) {\n        if (state.width != width) {\n          width = state.width;\n        }\n        return SliderTheme(\n          data: Theme.of(context).sliderTheme.copyWith(\n                showValueIndicator: ShowValueIndicator.never,\n                thumbShape: const RoundSliderThumbShape(\n                  enabledThumbRadius: 8,\n                ),\n                overlayShape: SliderComponentShape.noThumb,\n              ),\n          child: Slider(\n            value: width.clamp(\n              EditorStyleCustomizer.minDocumentWidth,\n              EditorStyleCustomizer.maxDocumentWidth,\n            ),\n            min: EditorStyleCustomizer.minDocumentWidth,\n            max: EditorStyleCustomizer.maxDocumentWidth,\n            divisions: 10,\n            onChanged: (value) {\n              setState(() => width = value);\n\n              widget.onPaddingChanged(value);\n            },\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/shared/share/constants.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nclass SettingsPageSitesConstants {\n  static const threeDotsButtonWidth = 26.0;\n  static const alignPadding = 6.0;\n\n  static final dateFormat = DateFormat('MMM d, yyyy');\n\n  static final publishedViewHeaderTitles = [\n    LocaleKeys.settings_sites_publishedPage_page.tr(),\n    LocaleKeys.settings_sites_publishedPage_pathName.tr(),\n    LocaleKeys.settings_sites_publishedPage_date.tr(),\n  ];\n\n  static final namespaceHeaderTitles = [\n    LocaleKeys.settings_sites_namespaceHeader.tr(),\n    LocaleKeys.settings_sites_homepageHeader.tr(),\n  ];\n\n  // the published view name is longer than the other two, so we give it more flex\n  static final publishedViewItemFlexes = [1, 1, 1];\n}\n\nclass SettingsPageSitesEvent {\n  static void visitSite(\n    PublishInfoViewPB publishInfoView, {\n    String? nameSpace,\n  }) {\n    // visit the site\n    final url = ShareConstants.buildPublishUrl(\n      nameSpace: nameSpace ?? publishInfoView.info.namespace,\n      publishName: publishInfoView.info.publishName,\n    );\n    afLaunchUrlString(url);\n  }\n\n  static void copySiteLink(\n    BuildContext context,\n    PublishInfoViewPB publishInfoView, {\n    String? nameSpace,\n  }) {\n    final url = ShareConstants.buildPublishUrl(\n      nameSpace: nameSpace ?? publishInfoView.info.namespace,\n      publishName: publishInfoView.info.publishName,\n    );\n    getIt<ClipboardService>().setData(ClipboardServiceData(plainText: url));\n    showToastNotification(\n      message: LocaleKeys.message_copy_success.tr(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_header.dart",
    "content": "import 'package:appflowy/workspace/presentation/settings/pages/sites/constants.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass DomainHeader extends StatelessWidget {\n  const DomainHeader({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        ...SettingsPageSitesConstants.namespaceHeaderTitles.map(\n          (title) => Expanded(\n            child: FlowyText.medium(\n              title,\n              fontSize: 14.0,\n              textAlign: TextAlign.left,\n            ),\n          ),\n        ),\n        // it used to align the three dots button in the published page item\n        const HSpace(SettingsPageSitesConstants.threeDotsButtonWidth),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/shared/share/constants.dart';\nimport 'package:appflowy/shared/af_role_pb_extension.dart';\nimport 'package:appflowy/shared/colors.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/constants.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_more_action.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/domain/home_page_menu.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/publish_info_view_item.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/settings_sites_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass DomainItem extends StatelessWidget {\n  const DomainItem({\n    super.key,\n    required this.namespace,\n    required this.homepage,\n  });\n\n  final String namespace;\n  final String homepage;\n\n  @override\n  Widget build(BuildContext context) {\n    final namespaceUrl = ShareConstants.buildNamespaceUrl(\n      nameSpace: namespace,\n    );\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.spaceEvenly,\n      children: [\n        // Namespace\n        Expanded(\n          child: _buildNamespace(context, namespaceUrl),\n        ),\n        // Homepage\n        Expanded(\n          child: _buildHomepage(context),\n        ),\n        // ... button\n        DomainMoreAction(namespace: namespace),\n      ],\n    );\n  }\n\n  Widget _buildNamespace(BuildContext context, String namespaceUrl) {\n    return Container(\n      alignment: Alignment.centerLeft,\n      padding: const EdgeInsets.only(right: 12.0),\n      child: FlowyTooltip(\n        message: '${LocaleKeys.shareAction_visitSite.tr()}\\n$namespaceUrl',\n        child: FlowyButton(\n          useIntrinsicWidth: true,\n          text: FlowyText(\n            namespaceUrl,\n            fontSize: 14.0,\n            overflow: TextOverflow.ellipsis,\n          ),\n          onTap: () {\n            final namespaceUrl = ShareConstants.buildNamespaceUrl(\n              nameSpace: namespace,\n              withHttps: true,\n            );\n            afLaunchUrlString(namespaceUrl);\n          },\n        ),\n      ),\n    );\n  }\n\n  Widget _buildHomepage(BuildContext context) {\n    final plan = context.read<SettingsSitesBloc>().state.subscriptionInfo?.plan;\n\n    if (plan == null) {\n      return const SizedBox.shrink();\n    }\n\n    final isFreePlan = plan == WorkspacePlanPB.FreePlan;\n    if (isFreePlan) {\n      return const Padding(\n        padding: EdgeInsets.only(\n          left: SettingsPageSitesConstants.alignPadding,\n        ),\n        child: _FreePlanUpgradeButton(),\n      );\n    }\n\n    return const _HomePageButton();\n  }\n}\n\nclass _HomePageButton extends StatelessWidget {\n  const _HomePageButton();\n\n  @override\n  Widget build(BuildContext context) {\n    final settingsSitesState = context.watch<SettingsSitesBloc>().state;\n    if (settingsSitesState.isLoading) {\n      return const SizedBox.shrink();\n    }\n\n    final isOwner = context\n            .watch<UserWorkspaceBloc>()\n            .state\n            .currentWorkspace\n            ?.role\n            .isOwner ??\n        false;\n\n    final homePageView = settingsSitesState.homePageView;\n    Widget child = homePageView == null\n        ? _defaultHomePageButton(context)\n        : PublishInfoViewItem(\n            publishInfoView: homePageView,\n            margin: isOwner ? null : EdgeInsets.zero,\n          );\n\n    if (isOwner) {\n      child = _buildHomePageButtonForOwner(\n        context,\n        homePageView: homePageView,\n        child: child,\n      );\n    } else {\n      child = _buildHomePageButtonForNonOwner(context, child);\n    }\n\n    return Container(\n      alignment: Alignment.centerLeft,\n      child: child,\n    );\n  }\n\n  Widget _buildHomePageButtonForOwner(\n    BuildContext context, {\n    required PublishInfoViewPB? homePageView,\n    required Widget child,\n  }) {\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        Flexible(\n          child: AppFlowyPopover(\n            direction: PopoverDirection.bottomWithCenterAligned,\n            constraints: const BoxConstraints(\n              maxWidth: 260,\n              maxHeight: 345,\n            ),\n            margin: const EdgeInsets.symmetric(\n              horizontal: 14.0,\n              vertical: 12.0,\n            ),\n            popupBuilder: (_) {\n              final bloc = context.read<SettingsSitesBloc>();\n              return BlocProvider.value(\n                value: bloc,\n                child: SelectHomePageMenu(\n                  userProfile: bloc.user,\n                  workspaceId: bloc.workspaceId,\n                  onSelected: (view) {},\n                ),\n              );\n            },\n            child: child,\n          ),\n        ),\n        if (homePageView != null)\n          FlowyTooltip(\n            message: LocaleKeys.settings_sites_clearHomePage.tr(),\n            child: FlowyButton(\n              margin: const EdgeInsets.all(4.0),\n              useIntrinsicWidth: true,\n              onTap: () {\n                context.read<SettingsSitesBloc>().add(\n                      const SettingsSitesEvent.removeHomePage(),\n                    );\n              },\n              text: const FlowySvg(\n                FlowySvgs.close_m,\n                size: Size.square(19.0),\n              ),\n            ),\n          ),\n      ],\n    );\n  }\n\n  Widget _buildHomePageButtonForNonOwner(\n    BuildContext context,\n    Widget child,\n  ) {\n    return FlowyTooltip(\n      message: LocaleKeys\n          .settings_sites_namespace_onlyWorkspaceOwnerCanSetHomePage\n          .tr(),\n      child: IgnorePointer(\n        child: child,\n      ),\n    );\n  }\n\n  Widget _defaultHomePageButton(BuildContext context) {\n    return FlowyButton(\n      useIntrinsicWidth: true,\n      leftIcon: const FlowySvg(\n        FlowySvgs.search_s,\n      ),\n      leftIconSize: const Size.square(14.0),\n      text: FlowyText(\n        LocaleKeys.settings_sites_selectHomePage.tr(),\n        figmaLineHeight: 18.0,\n      ),\n    );\n  }\n}\n\nclass _FreePlanUpgradeButton extends StatelessWidget {\n  const _FreePlanUpgradeButton();\n\n  @override\n  Widget build(BuildContext context) {\n    final isOwner = context\n            .watch<UserWorkspaceBloc>()\n            .state\n            .currentWorkspace\n            ?.role\n            .isOwner ??\n        false;\n    return Container(\n      alignment: Alignment.centerLeft,\n      child: FlowyTooltip(\n        message: LocaleKeys.settings_sites_homePage_upgradeToPro.tr(),\n        child: PrimaryRoundedButton(\n          text: 'Pro ↗',\n          fontSize: 12.0,\n          figmaLineHeight: 16.0,\n          fontWeight: FontWeight.w600,\n          radius: 8.0,\n          textColor: context.proPrimaryColor,\n          backgroundColor: context.proSecondaryColor,\n          margin: const EdgeInsets.symmetric(\n            horizontal: 8.0,\n            vertical: 6.0,\n          ),\n          hoverColor: context.proSecondaryColor.withValues(alpha: 0.9),\n          onTap: () {\n            if (isOwner) {\n              showToastNotification(\n                message:\n                    LocaleKeys.settings_sites_namespace_redirectToPayment.tr(),\n                type: ToastificationType.error,\n              );\n\n              context.read<SettingsSitesBloc>().add(\n                    const SettingsSitesEvent.upgradeSubscription(),\n                  );\n            } else {\n              showToastNotification(\n                message: LocaleKeys\n                    .settings_sites_namespace_pleaseAskOwnerToSetHomePage\n                    .tr(),\n                type: ToastificationType.error,\n              );\n            }\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_more_action.dart",
    "content": "import 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/af_role_pb_extension.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/constants.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/settings_sites_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass DomainMoreAction extends StatefulWidget {\n  const DomainMoreAction({\n    super.key,\n    required this.namespace,\n  });\n\n  final String namespace;\n\n  @override\n  State<DomainMoreAction> createState() => _DomainMoreActionState();\n}\n\nclass _DomainMoreActionState extends State<DomainMoreAction> {\n  @override\n  void initState() {\n    super.initState();\n\n    // update the current workspace to ensure the owner check is correct\n    context.read<UserWorkspaceBloc>().add(UserWorkspaceEvent.initialize());\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      constraints: const BoxConstraints(maxWidth: 188),\n      offset: const Offset(6, 0),\n      animationDuration: Durations.short3,\n      beginScaleFactor: 1.0,\n      beginOpacity: 0.8,\n      child: const SizedBox(\n        width: SettingsPageSitesConstants.threeDotsButtonWidth,\n        child: FlowyButton(\n          useIntrinsicWidth: true,\n          text: FlowySvg(FlowySvgs.three_dots_s),\n        ),\n      ),\n      popupBuilder: (builderContext) {\n        return BlocProvider.value(\n          value: context.read<SettingsSitesBloc>(),\n          child: _buildUpdateNamespaceButton(\n            context,\n            builderContext,\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildUpdateNamespaceButton(\n    BuildContext context,\n    BuildContext builderContext,\n  ) {\n    final child = _buildActionButton(\n      context,\n      builderContext,\n      type: _ActionType.updateNamespace,\n    );\n\n    final plan = context.read<SettingsSitesBloc>().state.subscriptionInfo?.plan;\n\n    if (plan != WorkspacePlanPB.ProPlan) {\n      return _buildForbiddenActionButton(\n        context,\n        tooltipMessage: LocaleKeys.settings_sites_namespace_upgradeToPro.tr(),\n        child: child,\n      );\n    }\n\n    final isOwner = context\n            .watch<UserWorkspaceBloc>()\n            .state\n            .currentWorkspace\n            ?.role\n            .isOwner ??\n        false;\n\n    if (!isOwner) {\n      return _buildForbiddenActionButton(\n        context,\n        tooltipMessage: LocaleKeys\n            .settings_sites_error_onlyWorkspaceOwnerCanUpdateNamespace\n            .tr(),\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  Widget _buildForbiddenActionButton(\n    BuildContext context, {\n    required String tooltipMessage,\n    required Widget child,\n  }) {\n    return Opacity(\n      opacity: 0.5,\n      child: FlowyTooltip(\n        message: tooltipMessage,\n        child: MouseRegion(\n          cursor: SystemMouseCursors.forbidden,\n          child: IgnorePointer(child: child),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildActionButton(\n    BuildContext context,\n    BuildContext builderContext, {\n    required _ActionType type,\n  }) {\n    return Container(\n      height: 34,\n      padding: const EdgeInsets.symmetric(vertical: 2.0),\n      child: FlowyIconTextButton(\n        margin: const EdgeInsets.symmetric(horizontal: 6),\n        iconPadding: 10.0,\n        onTap: () => _onTap(context, builderContext, type),\n        leftIconBuilder: (onHover) => FlowySvg(\n          type.leftIconSvg,\n        ),\n        textBuilder: (onHover) => FlowyText.regular(\n          type.name,\n          fontSize: 14.0,\n          figmaLineHeight: 18.0,\n          overflow: TextOverflow.ellipsis,\n        ),\n      ),\n    );\n  }\n\n  void _onTap(\n    BuildContext context,\n    BuildContext builderContext,\n    _ActionType type,\n  ) {\n    switch (type) {\n      case _ActionType.updateNamespace:\n        _showSettingsDialog(\n          context,\n          builderContext,\n        );\n        break;\n      case _ActionType.removeHomePage:\n        context.read<SettingsSitesBloc>().add(\n              const SettingsSitesEvent.removeHomePage(),\n            );\n        break;\n    }\n\n    PopoverContainer.of(builderContext).closeAll();\n  }\n\n  void _showSettingsDialog(\n    BuildContext context,\n    BuildContext builderContext,\n  ) {\n    showDialog(\n      context: context,\n      builder: (_) {\n        return BlocProvider.value(\n          value: context.read<SettingsSitesBloc>(),\n          child: Dialog(\n            shape: RoundedRectangleBorder(\n              borderRadius: BorderRadius.circular(12.0),\n            ),\n            child: SizedBox(\n              width: 460,\n              child: DomainSettingsDialog(\n                namespace: widget.namespace,\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nenum _ActionType {\n  updateNamespace,\n  removeHomePage,\n}\n\nextension _ActionTypeExtension on _ActionType {\n  String get name => switch (this) {\n        _ActionType.updateNamespace =>\n          LocaleKeys.settings_sites_updateNamespace.tr(),\n        _ActionType.removeHomePage =>\n          LocaleKeys.settings_sites_removeHomepage.tr(),\n      };\n\n  FlowySvgData get leftIconSvg => switch (this) {\n        _ActionType.updateNamespace => FlowySvgs.view_item_rename_s,\n        _ActionType.removeHomePage => FlowySvgs.trash_s,\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/shared/share/constants.dart';\nimport 'package:appflowy/plugins/shared/share/publish_color_extension.dart';\nimport 'package:appflowy/shared/error_code/error_code_map.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/settings_sites_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass DomainSettingsDialog extends StatefulWidget {\n  const DomainSettingsDialog({\n    super.key,\n    required this.namespace,\n  });\n\n  final String namespace;\n\n  @override\n  State<DomainSettingsDialog> createState() => _DomainSettingsDialogState();\n}\n\nclass _DomainSettingsDialogState extends State<DomainSettingsDialog> {\n  final focusNode = FocusNode();\n  final controller = TextEditingController();\n  late final controllerText = ValueNotifier<String>(widget.namespace);\n  String errorHintText = '';\n\n  @override\n  void initState() {\n    super.initState();\n\n    controller.text = widget.namespace;\n    controller.addListener(_onTextChanged);\n  }\n\n  void _onTextChanged() => controllerText.value = controller.text;\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    controller.removeListener(_onTextChanged);\n    controller.dispose();\n    controllerText.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocListener<SettingsSitesBloc, SettingsSitesState>(\n      listener: _onListener,\n      child: KeyboardListener(\n        focusNode: focusNode,\n        autofocus: true,\n        onKeyEvent: (event) {\n          if (event is KeyDownEvent &&\n              event.logicalKey == LogicalKeyboardKey.escape) {\n            Navigator.of(context).pop();\n          }\n        },\n        child: Container(\n          padding: const EdgeInsets.all(20),\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              _buildTitle(),\n              const VSpace(12),\n              _buildNamespaceDescription(),\n              const VSpace(20),\n              _buildNamespaceTextField(),\n              _buildPreviewNamespace(),\n              _buildErrorHintText(),\n              const VSpace(20),\n              _buildButtons(),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildTitle() {\n    return Row(\n      children: [\n        FlowyText(\n          LocaleKeys.settings_sites_namespace_updateExistingNamespace.tr(),\n          fontSize: 16.0,\n          figmaLineHeight: 22.0,\n          fontWeight: FontWeight.w500,\n          overflow: TextOverflow.ellipsis,\n        ),\n        const HSpace(6.0),\n        FlowyTooltip(\n          message: LocaleKeys.settings_sites_namespace_tooltip.tr(),\n          child: const FlowySvg(FlowySvgs.information_s),\n        ),\n        const HSpace(6.0),\n        const Spacer(),\n        FlowyButton(\n          margin: const EdgeInsets.all(3),\n          useIntrinsicWidth: true,\n          text: const FlowySvg(\n            FlowySvgs.upgrade_close_s,\n            size: Size.square(18.0),\n          ),\n          onTap: () => Navigator.of(context).pop(),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildNamespaceDescription() {\n    return FlowyText(\n      LocaleKeys.settings_sites_namespace_description.tr(),\n      fontSize: 14.0,\n      color: Theme.of(context).hintColor,\n      figmaLineHeight: 16.0,\n      maxLines: 3,\n    );\n  }\n\n  Widget _buildNamespaceTextField() {\n    return SizedBox(\n      height: 36,\n      child: FlowyTextField(\n        autoFocus: false,\n        controller: controller,\n        enableBorderColor: ShareMenuColors.borderColor(context),\n      ),\n    );\n  }\n\n  Widget _buildButtons() {\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.end,\n      children: [\n        OutlinedRoundedButton(\n          text: LocaleKeys.button_cancel.tr(),\n          onTap: () => Navigator.of(context).pop(),\n        ),\n        const HSpace(12.0),\n        PrimaryRoundedButton(\n          text: LocaleKeys.button_save.tr(),\n          radius: 8.0,\n          margin: const EdgeInsets.symmetric(\n            horizontal: 16.0,\n            vertical: 9.0,\n          ),\n          onTap: _onSave,\n        ),\n      ],\n    );\n  }\n\n  Widget _buildErrorHintText() {\n    if (errorHintText.isEmpty) {\n      return const SizedBox.shrink();\n    }\n\n    return Padding(\n      padding: const EdgeInsets.only(top: 4.0, left: 2.0),\n      child: FlowyText(\n        errorHintText,\n        fontSize: 12.0,\n        figmaLineHeight: 18.0,\n        color: Theme.of(context).colorScheme.error,\n      ),\n    );\n  }\n\n  Widget _buildPreviewNamespace() {\n    return ValueListenableBuilder<String>(\n      valueListenable: controllerText,\n      builder: (context, value, child) {\n        final url = ShareConstants.buildNamespaceUrl(\n          nameSpace: value,\n        );\n        return Padding(\n          padding: const EdgeInsets.only(top: 4.0, left: 2.0),\n          child: Opacity(\n            opacity: 0.8,\n            child: FlowyText(\n              url,\n              fontSize: 14.0,\n              figmaLineHeight: 18.0,\n              withTooltip: true,\n              overflow: TextOverflow.ellipsis,\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  void _onSave() {\n    // listen on the result\n    context\n        .read<SettingsSitesBloc>()\n        .add(SettingsSitesEvent.updateNamespace(controller.text));\n  }\n\n  void _onListener(BuildContext context, SettingsSitesState state) {\n    final actionResult = state.actionResult;\n    final type = actionResult?.actionType;\n    final result = actionResult?.result;\n    if (type != SettingsSitesActionType.updateNamespace || result == null) {\n      return;\n    }\n\n    result.fold(\n      (s) {\n        showToastNotification(\n          message: LocaleKeys.settings_sites_success_namespaceUpdated.tr(),\n        );\n\n        Navigator.of(context).pop();\n      },\n      (f) {\n        final basicErrorMessage =\n            LocaleKeys.settings_sites_error_failedToUpdateNamespace.tr();\n        final errorMessage = f.code.namespaceErrorMessage;\n\n        setState(() {\n          errorHintText = errorMessage.orDefault(basicErrorMessage);\n        });\n\n        Log.error('Failed to update namespace: $f');\n\n        showToastNotification(\n          message: basicErrorMessage,\n          type: ToastificationType.error,\n          description: errorMessage,\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/home_page_menu.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/publish_info_view_item.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/settings_sites_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\ntypedef OnSelectedHomePage = void Function(ViewPB view);\n\nclass SelectHomePageMenu extends StatefulWidget {\n  const SelectHomePageMenu({\n    super.key,\n    required this.onSelected,\n    required this.userProfile,\n    required this.workspaceId,\n  });\n\n  final OnSelectedHomePage onSelected;\n  final UserProfilePB userProfile;\n  final String workspaceId;\n\n  @override\n  State<SelectHomePageMenu> createState() => _SelectHomePageMenuState();\n}\n\nclass _SelectHomePageMenuState extends State<SelectHomePageMenu> {\n  List<PublishInfoViewPB> source = [];\n  List<PublishInfoViewPB> views = [];\n\n  @override\n  void initState() {\n    super.initState();\n\n    source = context.read<SettingsSitesBloc>().state.publishedViews;\n    views = [...source];\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (views.isEmpty) {\n      return _buildNoPublishedViews();\n    }\n\n    return _buildMenu(context);\n  }\n\n  Widget _buildNoPublishedViews() {\n    return FlowyText.regular(\n      LocaleKeys.settings_sites_publishedPage_noPublishedPages.tr(),\n      color: Theme.of(context).hintColor,\n      overflow: TextOverflow.ellipsis,\n    );\n  }\n\n  Widget _buildMenu(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        SpaceSearchField(\n          width: 240,\n          onSearch: (context, value) => _onSearch(value),\n        ),\n        const VSpace(10),\n        Expanded(\n          child: SingleChildScrollView(\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                ...views.map(\n                  (view) => Padding(\n                    padding: const EdgeInsets.symmetric(vertical: 4.0),\n                    child: PublishInfoViewItem(\n                      publishInfoView: view,\n                      useIntrinsicWidth: false,\n                      onTap: () {\n                        context.read<SettingsSitesBloc>().add(\n                              SettingsSitesEvent.setHomePage(view.info.viewId),\n                            );\n\n                        PopoverContainer.of(context).close();\n                      },\n                    ),\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  void _onSearch(String value) {\n    setState(() {\n      if (value.isEmpty) {\n        views = source;\n      } else {\n        views = source\n            .where(\n              (view) =>\n                  view.view.name.toLowerCase().contains(value.toLowerCase()),\n            )\n            .toList();\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/publish_info_view_item.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass PublishInfoViewItem extends StatelessWidget {\n  const PublishInfoViewItem({\n    super.key,\n    required this.publishInfoView,\n    this.onTap,\n    this.useIntrinsicWidth = true,\n    this.margin,\n    this.extraTooltipMessage,\n  });\n\n  final PublishInfoViewPB publishInfoView;\n  final VoidCallback? onTap;\n  final bool useIntrinsicWidth;\n  final EdgeInsets? margin;\n  final String? extraTooltipMessage;\n\n  @override\n  Widget build(BuildContext context) {\n    final name = publishInfoView.view.name.orDefault(\n      LocaleKeys.menuAppHeader_defaultNewPageName.tr(),\n    );\n    final tooltipMessage =\n        extraTooltipMessage != null ? '$extraTooltipMessage\\n$name' : name;\n    return Container(\n      alignment: Alignment.centerLeft,\n      child: FlowyButton(\n        margin: margin,\n        useIntrinsicWidth: useIntrinsicWidth,\n        mainAxisAlignment: MainAxisAlignment.start,\n        leftIcon: _buildIcon(),\n        text: FlowyTooltip(\n          message: tooltipMessage,\n          child: FlowyText.regular(\n            name,\n            fontSize: 14.0,\n            figmaLineHeight: 18.0,\n            overflow: TextOverflow.ellipsis,\n          ),\n        ),\n        onTap: onTap,\n      ),\n    );\n  }\n\n  Widget _buildIcon() {\n    final icon = publishInfoView.view.icon.toEmojiIconData();\n    return icon.isNotEmpty\n        ? RawEmojiIconWidget(\n            emoji: icon,\n            emojiSize: 16.0,\n            lineHeight: 1.1,\n          )\n        : publishInfoView.view.defaultIcon();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_item.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/shared/share/constants.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/navigator_context_extension.dart';\nimport 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';\nimport 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/constants.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/publish_info_view_item.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_more_action.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass PublishedViewItem extends StatelessWidget {\n  const PublishedViewItem({\n    super.key,\n    required this.publishInfoView,\n  });\n\n  final PublishInfoViewPB publishInfoView;\n\n  @override\n  Widget build(BuildContext context) {\n    final flexes = SettingsPageSitesConstants.publishedViewItemFlexes;\n\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.spaceEvenly,\n      children: [\n        // Published page name\n        Expanded(\n          flex: flexes[0],\n          child: _buildPublishedPageName(context),\n        ),\n\n        // Published Name\n        Expanded(\n          flex: flexes[1],\n          child: _buildPublishedName(context),\n        ),\n\n        // Published at\n        Expanded(\n          flex: flexes[2],\n          child: Padding(\n            padding: const EdgeInsets.only(\n              left: SettingsPageSitesConstants.alignPadding,\n            ),\n            child: _buildPublishedAt(context),\n          ),\n        ),\n\n        // More actions\n        PublishedViewMoreAction(\n          publishInfoView: publishInfoView,\n        ),\n      ],\n    );\n  }\n\n  Widget _buildPublishedPageName(BuildContext context) {\n    return PublishInfoViewItem(\n      extraTooltipMessage:\n          LocaleKeys.settings_sites_publishedPage_clickToOpenPageInApp.tr(),\n      publishInfoView: publishInfoView,\n      onTap: () {\n        context.popToHome();\n\n        getIt<ActionNavigationBloc>().add(\n          ActionNavigationEvent.performAction(\n            action: NavigationAction(\n              objectId: publishInfoView.view.viewId,\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildPublishedAt(BuildContext context) {\n    final formattedDate = SettingsPageSitesConstants.dateFormat.format(\n      DateTime.fromMillisecondsSinceEpoch(\n        publishInfoView.info.publishTimestampSec.toInt() * 1000,\n      ),\n    );\n    return FlowyText(\n      formattedDate,\n      fontSize: 14.0,\n      overflow: TextOverflow.ellipsis,\n    );\n  }\n\n  Widget _buildPublishedName(BuildContext context) {\n    return Container(\n      alignment: Alignment.centerLeft,\n      padding: const EdgeInsets.only(right: 48.0),\n      child: FlowyButton(\n        useIntrinsicWidth: true,\n        onTap: () {\n          final url = ShareConstants.buildPublishUrl(\n            nameSpace: publishInfoView.info.namespace,\n            publishName: publishInfoView.info.publishName,\n          );\n          afLaunchUrlString(url);\n        },\n        text: FlowyTooltip(\n          message:\n              '${LocaleKeys.settings_sites_publishedPage_clickToOpenPageInBrowser.tr()}\\n${publishInfoView.info.publishName}',\n          child: FlowyText(\n            publishInfoView.info.publishName,\n            fontSize: 14.0,\n            figmaLineHeight: 18.0,\n            overflow: TextOverflow.ellipsis,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_item_header.dart",
    "content": "import 'package:appflowy/workspace/presentation/settings/pages/sites/constants.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass PublishViewItemHeader extends StatelessWidget {\n  const PublishViewItemHeader({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final items = List.generate(\n      SettingsPageSitesConstants.publishedViewHeaderTitles.length,\n      (index) => (\n        title: SettingsPageSitesConstants.publishedViewHeaderTitles[index],\n        flex: SettingsPageSitesConstants.publishedViewItemFlexes[index],\n      ),\n    );\n\n    return Row(\n      children: [\n        ...items.map(\n          (item) => Expanded(\n            flex: item.flex,\n            child: FlowyText.medium(\n              item.title,\n              fontSize: 14.0,\n              textAlign: TextAlign.left,\n            ),\n          ),\n        ),\n        // it used to align the three dots button in the published page item\n        const HSpace(SettingsPageSitesConstants.threeDotsButtonWidth),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_more_action.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/constants.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/settings_sites_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass PublishedViewMoreAction extends StatelessWidget {\n  const PublishedViewMoreAction({\n    super.key,\n    required this.publishInfoView,\n  });\n\n  final PublishInfoViewPB publishInfoView;\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      constraints: const BoxConstraints(maxWidth: 168),\n      offset: const Offset(6, 0),\n      animationDuration: Durations.short3,\n      beginScaleFactor: 1.0,\n      beginOpacity: 0.8,\n      child: const SizedBox(\n        width: SettingsPageSitesConstants.threeDotsButtonWidth,\n        child: FlowyButton(\n          useIntrinsicWidth: true,\n          text: FlowySvg(FlowySvgs.three_dots_s),\n        ),\n      ),\n      popupBuilder: (builderContext) {\n        return BlocProvider.value(\n          value: context.read<SettingsSitesBloc>(),\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              _buildActionButton(\n                context,\n                builderContext,\n                type: _ActionType.viewSite,\n              ),\n              _buildActionButton(\n                context,\n                builderContext,\n                type: _ActionType.copySiteLink,\n              ),\n              _buildActionButton(\n                context,\n                builderContext,\n                type: _ActionType.unpublish,\n              ),\n              _buildActionButton(\n                context,\n                builderContext,\n                type: _ActionType.customUrl,\n              ),\n              _buildActionButton(\n                context,\n                builderContext,\n                type: _ActionType.settings,\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildActionButton(\n    BuildContext context,\n    BuildContext builderContext, {\n    required _ActionType type,\n  }) {\n    return Container(\n      height: 34,\n      padding: const EdgeInsets.symmetric(vertical: 2.0),\n      child: FlowyIconTextButton(\n        margin: const EdgeInsets.symmetric(horizontal: 6),\n        iconPadding: 10.0,\n        onTap: () => _onTap(context, builderContext, type),\n        leftIconBuilder: (onHover) => FlowySvg(\n          type.leftIconSvg,\n        ),\n        textBuilder: (onHover) => FlowyText.regular(\n          type.name,\n          fontSize: 14.0,\n          figmaLineHeight: 18.0,\n        ),\n      ),\n    );\n  }\n\n  void _onTap(\n    BuildContext context,\n    BuildContext builderContext,\n    _ActionType type,\n  ) {\n    switch (type) {\n      case _ActionType.viewSite:\n        SettingsPageSitesEvent.visitSite(\n          publishInfoView,\n          nameSpace: context.read<SettingsSitesBloc>().state.namespace,\n        );\n        break;\n      case _ActionType.copySiteLink:\n        SettingsPageSitesEvent.copySiteLink(\n          context,\n          publishInfoView,\n          nameSpace: context.read<SettingsSitesBloc>().state.namespace,\n        );\n        break;\n      case _ActionType.settings:\n        _showSettingsDialog(\n          context,\n          builderContext,\n        );\n        break;\n      case _ActionType.unpublish:\n        context.read<SettingsSitesBloc>().add(\n              SettingsSitesEvent.unpublishView(publishInfoView.info.viewId),\n            );\n        PopoverContainer.maybeOf(builderContext)?.close();\n        break;\n      case _ActionType.customUrl:\n        _showSettingsDialog(\n          context,\n          builderContext,\n        );\n        break;\n    }\n\n    PopoverContainer.of(builderContext).closeAll();\n  }\n\n  void _showSettingsDialog(\n    BuildContext context,\n    BuildContext builderContext,\n  ) {\n    showDialog(\n      context: context,\n      builder: (_) {\n        return BlocProvider.value(\n          value: context.read<SettingsSitesBloc>(),\n          child: Dialog(\n            shape: RoundedRectangleBorder(\n              borderRadius: BorderRadius.circular(12.0),\n            ),\n            child: SizedBox(\n              width: 440,\n              child: PublishedViewSettingsDialog(\n                publishInfoView: publishInfoView,\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nenum _ActionType {\n  viewSite,\n  copySiteLink,\n  settings,\n  unpublish,\n  customUrl;\n\n  String get name => switch (this) {\n        _ActionType.viewSite => LocaleKeys.shareAction_visitSite.tr(),\n        _ActionType.copySiteLink => LocaleKeys.shareAction_copyLink.tr(),\n        _ActionType.settings => LocaleKeys.settings_popupMenuItem_settings.tr(),\n        _ActionType.unpublish => LocaleKeys.shareAction_unPublish.tr(),\n        _ActionType.customUrl => LocaleKeys.settings_sites_customUrl.tr(),\n      };\n\n  FlowySvgData get leftIconSvg => switch (this) {\n        _ActionType.viewSite => FlowySvgs.share_publish_s,\n        _ActionType.copySiteLink => FlowySvgs.copy_s,\n        _ActionType.settings => FlowySvgs.settings_s,\n        _ActionType.unpublish => FlowySvgs.delete_s,\n        _ActionType.customUrl => FlowySvgs.edit_s,\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/shared/share/publish_color_extension.dart';\nimport 'package:appflowy/shared/error_code/error_code_map.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/constants.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/settings_sites_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass PublishedViewSettingsDialog extends StatefulWidget {\n  const PublishedViewSettingsDialog({\n    super.key,\n    required this.publishInfoView,\n  });\n\n  final PublishInfoViewPB publishInfoView;\n\n  @override\n  State<PublishedViewSettingsDialog> createState() =>\n      _PublishedViewSettingsDialogState();\n}\n\nclass _PublishedViewSettingsDialogState\n    extends State<PublishedViewSettingsDialog> {\n  final focusNode = FocusNode();\n  final controller = TextEditingController();\n\n  @override\n  void initState() {\n    super.initState();\n\n    controller.text = widget.publishInfoView.info.publishName;\n  }\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    controller.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocListener<SettingsSitesBloc, SettingsSitesState>(\n      listener: _onListener,\n      child: KeyboardListener(\n        focusNode: focusNode,\n        autofocus: true,\n        onKeyEvent: (event) {\n          if (event is KeyDownEvent &&\n              event.logicalKey == LogicalKeyboardKey.escape) {\n            Navigator.of(context).pop();\n          }\n        },\n        child: Container(\n          padding: const EdgeInsets.all(20),\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              _buildTitle(),\n              const VSpace(20),\n              _buildPublishNameLabel(),\n              const VSpace(8),\n              _buildPublishNameTextField(),\n              const VSpace(20),\n              _buildButtons(),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildTitle() {\n    return Row(\n      children: [\n        Expanded(\n          child: FlowyText(\n            LocaleKeys.settings_sites_publishedPage_settings.tr(),\n            fontSize: 16.0,\n            figmaLineHeight: 22.0,\n            fontWeight: FontWeight.w500,\n            overflow: TextOverflow.ellipsis,\n          ),\n        ),\n        const HSpace(6.0),\n        FlowyButton(\n          margin: const EdgeInsets.all(3),\n          useIntrinsicWidth: true,\n          text: const FlowySvg(\n            FlowySvgs.upgrade_close_s,\n            size: Size.square(18.0),\n          ),\n          onTap: () => Navigator.of(context).pop(),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildPublishNameLabel() {\n    return FlowyText(\n      LocaleKeys.settings_sites_publishedPage_pathName.tr(),\n      fontSize: 14.0,\n      color: Theme.of(context).hintColor,\n    );\n  }\n\n  Widget _buildPublishNameTextField() {\n    return Row(\n      children: [\n        Expanded(\n          child: SizedBox(\n            height: 36,\n            child: FlowyTextField(\n              autoFocus: false,\n              controller: controller,\n              enableBorderColor: ShareMenuColors.borderColor(context),\n              textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(\n                    height: 1.4,\n                  ),\n            ),\n          ),\n        ),\n        const HSpace(12.0),\n        OutlinedRoundedButton(\n          text: LocaleKeys.button_save.tr(),\n          radius: 8.0,\n          margin: const EdgeInsets.symmetric(\n            horizontal: 16.0,\n            vertical: 11.0,\n          ),\n          onTap: _savePublishName,\n        ),\n      ],\n    );\n  }\n\n  Widget _buildButtons() {\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.end,\n      children: [\n        OutlinedRoundedButton(\n          text: LocaleKeys.shareAction_unPublish.tr(),\n          onTap: _unpublishView,\n        ),\n        const HSpace(12.0),\n        PrimaryRoundedButton(\n          text: LocaleKeys.shareAction_visitSite.tr(),\n          radius: 8.0,\n          margin: const EdgeInsets.symmetric(\n            horizontal: 16.0,\n            vertical: 9.0,\n          ),\n          onTap: _visitSite,\n        ),\n      ],\n    );\n  }\n\n  void _savePublishName() {\n    context.read<SettingsSitesBloc>().add(\n          SettingsSitesEvent.updatePublishName(\n            widget.publishInfoView.info.viewId,\n            controller.text,\n          ),\n        );\n  }\n\n  void _unpublishView() {\n    context.read<SettingsSitesBloc>().add(\n          SettingsSitesEvent.unpublishView(\n            widget.publishInfoView.info.viewId,\n          ),\n        );\n\n    Navigator.of(context).pop();\n  }\n\n  void _visitSite() {\n    SettingsPageSitesEvent.visitSite(\n      widget.publishInfoView,\n      nameSpace: context.read<SettingsSitesBloc>().state.namespace,\n    );\n  }\n\n  void _onListener(BuildContext context, SettingsSitesState state) {\n    final actionResult = state.actionResult;\n    final result = actionResult?.result;\n    if (actionResult == null ||\n        result == null ||\n        actionResult.actionType != SettingsSitesActionType.updatePublishName) {\n      return;\n    }\n\n    result.fold(\n      (s) {\n        showToastNotification(\n          message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(),\n        );\n        Navigator.of(context).pop();\n      },\n      (f) {\n        Log.error('update path name failed: $f');\n\n        showToastNotification(\n          message: LocaleKeys.settings_sites_error_updatePathNameFailed.tr(),\n          type: ToastificationType.error,\n          description: f.code.publishErrorMessage,\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:protobuf/protobuf.dart' hide FieldInfo;\n\npart 'settings_sites_bloc.freezed.dart';\n\n// workspaceId -> namespace\nMap<String, String?> _namespaceCache = {};\n\nclass SettingsSitesBloc extends Bloc<SettingsSitesEvent, SettingsSitesState> {\n  SettingsSitesBloc({\n    required this.workspaceId,\n    required this.user,\n  }) : super(const SettingsSitesState()) {\n    on<SettingsSitesEvent>((event, emit) async {\n      await event.when(\n        initial: () async => _initial(emit),\n        upgradeSubscription: () async => _upgradeSubscription(emit),\n        unpublishView: (viewId) async => _unpublishView(\n          viewId,\n          emit,\n        ),\n        updateNamespace: (namespace) async => _updateNamespace(\n          namespace,\n          emit,\n        ),\n        updatePublishName: (viewId, name) async => _updatePublishName(\n          viewId,\n          name,\n          emit,\n        ),\n        setHomePage: (viewId) async => _setHomePage(\n          viewId,\n          emit,\n        ),\n        removeHomePage: () async => _removeHomePage(emit),\n      );\n    });\n  }\n\n  final String workspaceId;\n  final UserProfilePB user;\n\n  Future<void> _initial(Emitter<SettingsSitesState> emit) async {\n    final lastNamespace = _namespaceCache[workspaceId] ?? '';\n    emit(\n      state.copyWith(\n        isLoading: true,\n        namespace: lastNamespace,\n      ),\n    );\n\n    // Combine fetching subscription info and namespace\n    final (subscriptionInfo, namespace) = await (\n      _fetchUserSubscription(),\n      _fetchPublishNamespace(),\n    ).wait;\n\n    emit(\n      state.copyWith(\n        subscriptionInfo: subscriptionInfo,\n        namespace: namespace,\n      ),\n    );\n\n    // This request is not blocking, render the namespace and subscription info first.\n    final (publishViews, homePageId) = await (\n      _fetchPublishedViews(),\n      _fetchHomePageView(),\n    ).wait;\n\n    final homePageView = publishViews.firstWhereOrNull(\n      (view) => view.info.viewId == homePageId,\n    );\n\n    emit(\n      state.copyWith(\n        publishedViews: publishViews,\n        homePageView: homePageView,\n        isLoading: false,\n      ),\n    );\n  }\n\n  Future<WorkspaceSubscriptionInfoPB?> _fetchUserSubscription() async {\n    final result = await UserBackendService.getWorkspaceSubscriptionInfo(\n      workspaceId,\n    );\n    return result.fold((s) => s, (f) {\n      Log.error('Failed to fetch user subscription info: $f');\n      return null;\n    });\n  }\n\n  Future<String> _fetchPublishNamespace() async {\n    final result = await FolderEventGetPublishNamespace().send();\n    _namespaceCache[workspaceId] = result.fold((s) => s.namespace, (_) => null);\n    return _namespaceCache[workspaceId] ?? '';\n  }\n\n  Future<List<PublishInfoViewPB>> _fetchPublishedViews() async {\n    final result = await FolderEventListPublishedViews().send();\n    return result.fold(\n      // new -> old\n      (s) => s.items.sorted(\n        (a, b) =>\n            b.info.publishTimestampSec.toInt() -\n            a.info.publishTimestampSec.toInt(),\n      ),\n      (_) => [],\n    );\n  }\n\n  Future<void> _unpublishView(\n    String viewId,\n    Emitter<SettingsSitesState> emit,\n  ) async {\n    emit(\n      state.copyWith(\n        actionResult: const SettingsSitesActionResult(\n          actionType: SettingsSitesActionType.unpublishView,\n          isLoading: true,\n          result: null,\n        ),\n      ),\n    );\n\n    final request = UnpublishViewsPayloadPB(viewIds: [viewId]);\n    final result = await FolderEventUnpublishViews(request).send();\n    final publishedViews = result.fold(\n      (_) => state.publishedViews\n          .where((view) => view.info.viewId != viewId)\n          .toList(),\n      (_) => state.publishedViews,\n    );\n\n    final isHomepage = result.fold(\n      (_) => state.homePageView?.info.viewId == viewId,\n      (_) => false,\n    );\n\n    emit(\n      state.copyWith(\n        publishedViews: publishedViews,\n        actionResult: SettingsSitesActionResult(\n          actionType: SettingsSitesActionType.unpublishView,\n          isLoading: false,\n          result: result,\n        ),\n        homePageView: isHomepage ? null : state.homePageView,\n      ),\n    );\n  }\n\n  Future<void> _updateNamespace(\n    String namespace,\n    Emitter<SettingsSitesState> emit,\n  ) async {\n    emit(\n      state.copyWith(\n        actionResult: const SettingsSitesActionResult(\n          actionType: SettingsSitesActionType.updateNamespace,\n          isLoading: true,\n          result: null,\n        ),\n      ),\n    );\n\n    final request = SetPublishNamespacePayloadPB()..newNamespace = namespace;\n    final result = await FolderEventSetPublishNamespace(request).send();\n\n    emit(\n      state.copyWith(\n        namespace: result.fold((_) => namespace, (_) => state.namespace),\n        actionResult: SettingsSitesActionResult(\n          actionType: SettingsSitesActionType.updateNamespace,\n          isLoading: false,\n          result: result,\n        ),\n      ),\n    );\n  }\n\n  Future<void> _updatePublishName(\n    String viewId,\n    String name,\n    Emitter<SettingsSitesState> emit,\n  ) async {\n    emit(\n      state.copyWith(\n        actionResult: const SettingsSitesActionResult(\n          actionType: SettingsSitesActionType.updatePublishName,\n          isLoading: true,\n          result: null,\n        ),\n      ),\n    );\n\n    final request = SetPublishNamePB()\n      ..viewId = viewId\n      ..newName = name;\n    final result = await FolderEventSetPublishName(request).send();\n    final publishedViews = result.fold(\n      (_) => state.publishedViews.map((view) {\n        view.freeze();\n        if (view.info.viewId == viewId) {\n          view = view.rebuild((b) {\n            final info = b.info;\n            info.freeze();\n            b.info = info.rebuild((b) => b.publishName = name);\n          });\n        }\n        return view;\n      }).toList(),\n      (_) => state.publishedViews,\n    );\n\n    emit(\n      state.copyWith(\n        publishedViews: publishedViews,\n        actionResult: SettingsSitesActionResult(\n          actionType: SettingsSitesActionType.updatePublishName,\n          isLoading: false,\n          result: result,\n        ),\n      ),\n    );\n  }\n\n  Future<void> _upgradeSubscription(Emitter<SettingsSitesState> emit) async {\n    final userService = UserBackendService(userId: user.id);\n    final result = await userService.createSubscription(\n      workspaceId,\n      SubscriptionPlanPB.Pro,\n    );\n\n    result.onSuccess((s) {\n      afLaunchUrlString(s.paymentLink);\n    });\n\n    emit(\n      state.copyWith(\n        actionResult: SettingsSitesActionResult(\n          actionType: SettingsSitesActionType.upgradeSubscription,\n          isLoading: false,\n          result: result,\n        ),\n      ),\n    );\n  }\n\n  Future<void> _setHomePage(\n    String? viewId,\n    Emitter<SettingsSitesState> emit,\n  ) async {\n    if (viewId == null) {\n      return;\n    }\n    final viewIdPB = ViewIdPB()..value = viewId;\n    final result = await FolderEventSetDefaultPublishView(viewIdPB).send();\n    final homePageView = state.publishedViews.firstWhereOrNull(\n      (view) => view.info.viewId == viewId,\n    );\n\n    emit(\n      state.copyWith(\n        homePageView: homePageView,\n        actionResult: SettingsSitesActionResult(\n          actionType: SettingsSitesActionType.setHomePage,\n          isLoading: false,\n          result: result,\n        ),\n      ),\n    );\n  }\n\n  Future<void> _removeHomePage(Emitter<SettingsSitesState> emit) async {\n    final result = await FolderEventRemoveDefaultPublishView().send();\n\n    emit(\n      state.copyWith(\n        homePageView: result.fold((_) => null, (_) => state.homePageView),\n        actionResult: SettingsSitesActionResult(\n          actionType: SettingsSitesActionType.removeHomePage,\n          isLoading: false,\n          result: result,\n        ),\n      ),\n    );\n  }\n\n  Future<String?> _fetchHomePageView() async {\n    final result = await FolderEventGetDefaultPublishInfo().send();\n    return result.fold((s) => s.viewId, (_) => null);\n  }\n}\n\n@freezed\nclass SettingsSitesState with _$SettingsSitesState {\n  const factory SettingsSitesState({\n    @Default([]) List<PublishInfoViewPB> publishedViews,\n    SettingsSitesActionResult? actionResult,\n    @Default('') String namespace,\n    @Default(null) WorkspaceSubscriptionInfoPB? subscriptionInfo,\n    @Default(true) bool isLoading,\n    @Default(null) PublishInfoViewPB? homePageView,\n  }) = _SettingsSitesState;\n\n  factory SettingsSitesState.initial() => const SettingsSitesState();\n}\n\n@freezed\nclass SettingsSitesEvent with _$SettingsSitesEvent {\n  const factory SettingsSitesEvent.initial() = _Initial;\n  const factory SettingsSitesEvent.unpublishView(String viewId) =\n      _UnpublishView;\n  const factory SettingsSitesEvent.updateNamespace(String namespace) =\n      _UpdateNamespace;\n  const factory SettingsSitesEvent.updatePublishName(\n    String viewId,\n    String name,\n  ) = _UpdatePublishName;\n  const factory SettingsSitesEvent.upgradeSubscription() = _UpgradeSubscription;\n  const factory SettingsSitesEvent.setHomePage(String? viewId) = _SetHomePage;\n  const factory SettingsSitesEvent.removeHomePage() = _RemoveHomePage;\n}\n\nenum SettingsSitesActionType {\n  none,\n  unpublishView,\n  updateNamespace,\n  fetchPublishedViews,\n  updatePublishName,\n  fetchUserSubscription,\n  upgradeSubscription,\n  setHomePage,\n  removeHomePage,\n}\n\nclass SettingsSitesActionResult {\n  const SettingsSitesActionResult({\n    required this.actionType,\n    required this.isLoading,\n    required this.result,\n  });\n\n  factory SettingsSitesActionResult.none() => const SettingsSitesActionResult(\n        actionType: SettingsSitesActionType.none,\n        isLoading: false,\n        result: null,\n      );\n\n  final SettingsSitesActionType actionType;\n  final FlowyResult<void, FlowyError>? result;\n  final bool isLoading;\n\n  @override\n  String toString() {\n    return 'SettingsSitesActionResult(actionType: $actionType, isLoading: $isLoading, result: $result)';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_view.dart",
    "content": "import 'package:appflowy/features/workspace/data/repositories/rust_workspace_repository_impl.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/constants.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_header.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_item.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_item.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_item_header.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/settings_sites_bloc.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SettingsSitesPage extends StatelessWidget {\n  const SettingsSitesPage({\n    super.key,\n    required this.workspaceId,\n    required this.user,\n  });\n\n  final String workspaceId;\n  final UserProfilePB user;\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider<SettingsSitesBloc>(\n          create: (context) => SettingsSitesBloc(\n            workspaceId: workspaceId,\n            user: user,\n          )..add(const SettingsSitesEvent.initial()),\n        ),\n        BlocProvider<UserWorkspaceBloc>(\n          create: (context) => UserWorkspaceBloc(\n            userProfile: user,\n            repository: RustWorkspaceRepositoryImpl(\n              userId: user.id,\n            ),\n          )..add(UserWorkspaceEvent.initialize()),\n        ),\n      ],\n      child: const _SettingsSitesPageView(),\n    );\n  }\n}\n\nclass _SettingsSitesPageView extends StatelessWidget {\n  const _SettingsSitesPageView();\n\n  @override\n  Widget build(BuildContext context) {\n    return SettingsBody(\n      title: LocaleKeys.settings_sites_title.tr(),\n      autoSeparate: false,\n      children: [\n        // Domain / Namespace\n        _buildNamespaceCategory(context),\n        const VSpace(36),\n        // All published pages\n        _buildPublishedViewsCategory(context),\n      ],\n    );\n  }\n\n  Widget _buildNamespaceCategory(BuildContext context) {\n    return SettingsCategory(\n      title: LocaleKeys.settings_sites_namespaceHeader.tr(),\n      description: LocaleKeys.settings_sites_namespaceDescription.tr(),\n      descriptionColor: Theme.of(context).hintColor,\n      children: [\n        const FlowyDivider(),\n        BlocConsumer<SettingsSitesBloc, SettingsSitesState>(\n          listener: _onListener,\n          builder: (context, state) {\n            return SeparatedColumn(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              separatorBuilder: () => const FlowyDivider(\n                padding: EdgeInsets.symmetric(vertical: 12.0),\n              ),\n              children: [\n                const DomainHeader(),\n                ConstrainedBox(\n                  constraints: const BoxConstraints(minHeight: 36.0),\n                  child: Transform.translate(\n                    offset: const Offset(\n                      -SettingsPageSitesConstants.alignPadding,\n                      0,\n                    ),\n                    child: DomainItem(\n                      namespace: state.namespace,\n                      homepage: '',\n                    ),\n                  ),\n                ),\n              ],\n            );\n          },\n        ),\n      ],\n    );\n  }\n\n  Widget _buildPublishedViewsCategory(BuildContext context) {\n    return SettingsCategory(\n      title: LocaleKeys.settings_sites_publishedPage_title.tr(),\n      description: LocaleKeys.settings_sites_publishedPage_description.tr(),\n      descriptionColor: Theme.of(context).hintColor,\n      children: [\n        const FlowyDivider(),\n        BlocBuilder<SettingsSitesBloc, SettingsSitesState>(\n          builder: (context, state) {\n            return SeparatedColumn(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              separatorBuilder: () => const FlowyDivider(\n                padding: EdgeInsets.symmetric(vertical: 12.0),\n              ),\n              children: _buildPublishedViewsResult(context, state),\n            );\n          },\n        ),\n      ],\n    );\n  }\n\n  List<Widget> _buildPublishedViewsResult(\n    BuildContext context,\n    SettingsSitesState state,\n  ) {\n    final publishedViews = state.publishedViews;\n    final List<Widget> children = [\n      const PublishViewItemHeader(),\n    ];\n\n    if (!state.isLoading) {\n      if (publishedViews.isEmpty) {\n        children.add(\n          FlowyText.regular(\n            LocaleKeys.settings_sites_publishedPage_emptyHinText.tr(),\n            color: Theme.of(context).hintColor,\n          ),\n        );\n      } else {\n        children.addAll(\n          publishedViews.map(\n            (view) => Transform.translate(\n              offset: const Offset(\n                -SettingsPageSitesConstants.alignPadding,\n                0,\n              ),\n              child: PublishedViewItem(publishInfoView: view),\n            ),\n          ),\n        );\n      }\n    } else {\n      children.add(\n        const Center(\n          child: SizedBox(\n            height: 24,\n            width: 24,\n            child: CircularProgressIndicator.adaptive(strokeWidth: 3),\n          ),\n        ),\n      );\n    }\n\n    return children;\n  }\n\n  void _onListener(BuildContext context, SettingsSitesState state) {\n    final actionResult = state.actionResult;\n    final type = actionResult?.actionType;\n    final result = actionResult?.result;\n    if (type == SettingsSitesActionType.upgradeSubscription && result != null) {\n      result.onFailure((f) {\n        Log.error('Failed to generate payment link for Pro Plan: ${f.msg}');\n\n        showToastNotification(\n          message:\n              LocaleKeys.settings_sites_error_failedToGeneratePaymentLink.tr(),\n          type: ToastificationType.error,\n        );\n      });\n    } else if (type == SettingsSitesActionType.unpublishView &&\n        result != null) {\n      result.fold((_) {\n        showToastNotification(\n          message: LocaleKeys.publish_unpublishSuccessfully.tr(),\n        );\n      }, (f) {\n        Log.error('Failed to unpublish view: ${f.msg}');\n\n        showToastNotification(\n          message: LocaleKeys.publish_unpublishFailed.tr(),\n          type: ToastificationType.error,\n          description: f.msg,\n        );\n      });\n    } else if (type == SettingsSitesActionType.setHomePage && result != null) {\n      result.fold((s) {\n        showToastNotification(\n          message: LocaleKeys.settings_sites_success_setHomepageSuccess.tr(),\n        );\n      }, (f) {\n        Log.error('Failed to set homepage: ${f.msg}');\n\n        showToastNotification(\n          message: LocaleKeys.settings_sites_error_setHomepageFailed.tr(),\n          type: ToastificationType.error,\n        );\n      });\n    } else if (type == SettingsSitesActionType.removeHomePage &&\n        result != null) {\n      result.fold((s) {\n        showToastNotification(\n          message: LocaleKeys.settings_sites_success_removeHomePageSuccess.tr(),\n        );\n      }, (f) {\n        Log.error('Failed to remove homepage: ${f.msg}');\n\n        showToastNotification(\n          message: LocaleKeys.settings_sites_error_removeHomePageFailed.tr(),\n          type: ToastificationType.error,\n        );\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/shared/share/constants.dart';\nimport 'package:appflowy/shared/appflowy_cache_manager.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/share_log_files.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/appflowy_cloud_urls_bloc.dart';\nimport 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/settings_billing_view.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/settings_manage_data_view.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/settings_plan_view.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/settings_shortcuts_view.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/sites/settings_sites_view.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/web_url_hint_widget.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'pages/setting_ai_view/local_settings_ai_view.dart';\nimport 'widgets/setting_cloud.dart';\n\n@visibleForTesting\nconst kSelfHostedTextInputFieldKey =\n    ValueKey('self_hosted_url_input_text_field');\n@visibleForTesting\nconst kSelfHostedWebTextInputFieldKey =\n    ValueKey('self_hosted_web_url_input_text_field');\n\nclass SettingsDialog extends StatelessWidget {\n  SettingsDialog(\n    this.user, {\n    required this.dismissDialog,\n    required this.didLogout,\n    required this.restartApp,\n    this.initPage,\n  }) : super(key: ValueKey(user.id));\n\n  final UserProfilePB user;\n  final SettingsPage? initPage;\n  final VoidCallback dismissDialog;\n  final VoidCallback didLogout;\n  final VoidCallback restartApp;\n\n  @override\n  Widget build(BuildContext context) {\n    final width = MediaQuery.of(context).size.width * 0.6;\n    final theme = AppFlowyTheme.of(context);\n    final currentWorkspaceMemberRole =\n        context.read<UserWorkspaceBloc>().state.currentWorkspace?.role;\n    return BlocProvider<SettingsDialogBloc>(\n      create: (context) => SettingsDialogBloc(\n        user,\n        currentWorkspaceMemberRole,\n        initPage: initPage,\n      )..add(const SettingsDialogEvent.initial()),\n      child: BlocBuilder<SettingsDialogBloc, SettingsDialogState>(\n        builder: (context, state) => FlowyDialog(\n          width: width,\n          constraints: const BoxConstraints(minWidth: 564),\n          child: ScaffoldMessenger(\n            child: Scaffold(\n              backgroundColor: theme.backgroundColorScheme.primary,\n              body: Row(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  SizedBox(\n                    width: 204,\n                    child: SettingsMenu(\n                      userProfile: user,\n                      changeSelectedPage: (index) => context\n                          .read<SettingsDialogBloc>()\n                          .add(SettingsDialogEvent.setSelectedPage(index)),\n                      currentPage:\n                          context.read<SettingsDialogBloc>().state.page,\n                      currentUserRole: currentWorkspaceMemberRole,\n                      isBillingEnabled: state.isBillingEnabled,\n                    ),\n                  ),\n                  AFDivider(\n                    axis: Axis.vertical,\n                    color: theme.borderColorScheme.primary,\n                  ),\n                  BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(\n                    builder: (context, state) {\n                      return Expanded(\n                        child: getSettingsView(\n                          state.currentWorkspace!,\n                          context.read<SettingsDialogBloc>().state.page,\n                          state.userProfile,\n                          state.currentWorkspace?.role,\n                        ),\n                      );\n                    },\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget getSettingsView(\n    UserWorkspacePB workspace,\n    SettingsPage page,\n    UserProfilePB user,\n    AFRolePB? currentWorkspaceMemberRole,\n  ) {\n    switch (page) {\n      case SettingsPage.account:\n        return SettingsAccountView(\n          userProfile: user,\n          didLogout: didLogout,\n          didLogin: dismissDialog,\n        );\n      case SettingsPage.workspace:\n        return SettingsWorkspaceView(\n          userProfile: user,\n          currentWorkspaceMemberRole: currentWorkspaceMemberRole,\n        );\n      case SettingsPage.manageData:\n        return SettingsManageDataView(\n          userProfile: user,\n          workspace: workspace,\n        );\n      case SettingsPage.notifications:\n        return const SettingsNotificationsView();\n      case SettingsPage.cloud:\n        return SettingCloud(restartAppFlowy: () => restartApp());\n      case SettingsPage.shortcuts:\n        return const SettingsShortcutsView();\n      case SettingsPage.ai:\n        if (user.workspaceType == WorkspaceTypePB.ServerW) {\n          return SettingsAIView(\n            key: ValueKey(workspace.workspaceId),\n            userProfile: user,\n            currentWorkspaceMemberRole: currentWorkspaceMemberRole,\n            workspaceId: workspace.workspaceId,\n          );\n        } else {\n          return LocalSettingsAIView(\n            key: ValueKey(workspace.workspaceId),\n            userProfile: user,\n            workspaceId: workspace.workspaceId,\n          );\n        }\n      case SettingsPage.member:\n        return WorkspaceMembersPage(\n          userProfile: user,\n          workspaceId: workspace.workspaceId,\n        );\n      case SettingsPage.plan:\n        return SettingsPlanView(\n          workspaceId: workspace.workspaceId,\n          user: user,\n        );\n      case SettingsPage.billing:\n        return SettingsBillingView(\n          workspaceId: workspace.workspaceId,\n          user: user,\n        );\n      case SettingsPage.sites:\n        return SettingsSitesPage(\n          workspaceId: workspace.workspaceId,\n          user: user,\n        );\n      case SettingsPage.featureFlags:\n        return const FeatureFlagsPage();\n    }\n  }\n}\n\nclass SimpleSettingsDialog extends StatefulWidget {\n  const SimpleSettingsDialog({super.key});\n\n  @override\n  State<SimpleSettingsDialog> createState() => _SimpleSettingsDialogState();\n}\n\nclass _SimpleSettingsDialogState extends State<SimpleSettingsDialog> {\n  SettingsPage page = SettingsPage.cloud;\n\n  @override\n  Widget build(BuildContext context) {\n    final settings = context.watch<AppearanceSettingsCubit>().state;\n\n    return FlowyDialog(\n      width: MediaQuery.of(context).size.width * 0.7,\n      constraints: const BoxConstraints(maxWidth: 784, minWidth: 564),\n      child: SingleChildScrollView(\n        child: Padding(\n          padding: const EdgeInsets.all(24.0),\n          child: Column(\n            mainAxisSize: MainAxisSize.min,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              // header\n              FlowyText(\n                LocaleKeys.signIn_settings.tr(),\n                fontSize: 36.0,\n                fontWeight: FontWeight.w600,\n              ),\n              const VSpace(18.0),\n\n              // language\n              _LanguageSettings(key: ValueKey('language${settings.hashCode}')),\n              const VSpace(22.0),\n\n              // self-host cloud\n              _SelfHostSettings(key: ValueKey('selfhost${settings.hashCode}')),\n              const VSpace(22.0),\n\n              // support\n              _SupportSettings(key: ValueKey('support${settings.hashCode}')),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _LanguageSettings extends StatelessWidget {\n  const _LanguageSettings({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return SettingsCategory(\n      title: LocaleKeys.settings_workspacePage_language_title.tr(),\n      children: const [LanguageDropdown()],\n    );\n  }\n}\n\nclass _SelfHostSettings extends StatefulWidget {\n  const _SelfHostSettings({\n    super.key,\n  });\n\n  @override\n  State<_SelfHostSettings> createState() => _SelfHostSettingsState();\n}\n\nclass _SelfHostSettingsState extends State<_SelfHostSettings> {\n  final cloudUrlTextController = TextEditingController();\n  final webUrlTextController = TextEditingController();\n\n  AuthenticatorType type = AuthenticatorType.appflowyCloud;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _fetchUrls();\n  }\n\n  @override\n  void dispose() {\n    cloudUrlTextController.dispose();\n    webUrlTextController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return SettingsCategory(\n      title: LocaleKeys.settings_menu_cloudAppFlowy.tr(),\n      children: [\n        Flexible(\n          child: SettingsServerDropdownMenu(\n            selectedServer: type,\n            onSelected: _onSelected,\n          ),\n        ),\n        if (type == AuthenticatorType.appflowyCloudSelfHost) _buildInputField(),\n      ],\n    );\n  }\n\n  Widget _buildInputField() {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        _SelfHostUrlField(\n          textFieldKey: kSelfHostedTextInputFieldKey,\n          textController: cloudUrlTextController,\n          title: LocaleKeys.settings_menu_cloudURL.tr(),\n          hintText: LocaleKeys.settings_menu_cloudURLHint.tr(),\n          onSave: (url) => _saveUrl(\n            cloudUrl: url,\n            webUrl: webUrlTextController.text,\n            type: AuthenticatorType.appflowyCloudSelfHost,\n          ),\n        ),\n        const VSpace(12.0),\n        _SelfHostUrlField(\n          textFieldKey: kSelfHostedWebTextInputFieldKey,\n          textController: webUrlTextController,\n          title: LocaleKeys.settings_menu_webURL.tr(),\n          hintText: LocaleKeys.settings_menu_webURLHint.tr(),\n          hintBuilder: (context) => const WebUrlHintWidget(),\n          onSave: (url) => _saveUrl(\n            cloudUrl: cloudUrlTextController.text,\n            webUrl: url,\n            type: AuthenticatorType.appflowyCloudSelfHost,\n          ),\n        ),\n        const VSpace(12.0),\n        _buildSaveButton(),\n      ],\n    );\n  }\n\n  Widget _buildSaveButton() {\n    return Container(\n      height: 36,\n      constraints: const BoxConstraints(minWidth: 78),\n      child: OutlinedRoundedButton(\n        text: LocaleKeys.button_save.tr(),\n        onTap: () => _saveUrl(\n          cloudUrl: cloudUrlTextController.text,\n          webUrl: webUrlTextController.text,\n          type: AuthenticatorType.appflowyCloudSelfHost,\n        ),\n      ),\n    );\n  }\n\n  void _onSelected(AuthenticatorType type) {\n    if (type == this.type) {\n      return;\n    }\n\n    Log.info('Switching server type to $type');\n\n    setState(() {\n      this.type = type;\n    });\n\n    if (type == AuthenticatorType.appflowyCloud) {\n      cloudUrlTextController.text = kAppflowyCloudUrl;\n      webUrlTextController.text = ShareConstants.defaultBaseWebDomain;\n      _saveUrl(\n        cloudUrl: kAppflowyCloudUrl,\n        webUrl: ShareConstants.defaultBaseWebDomain,\n        type: type,\n      );\n    }\n  }\n\n  Future<void> _saveUrl({\n    required String cloudUrl,\n    required String webUrl,\n    required AuthenticatorType type,\n  }) async {\n    if (cloudUrl.isEmpty || webUrl.isEmpty) {\n      showToastNotification(\n        message: LocaleKeys.settings_menu_pleaseInputValidURL.tr(),\n        type: ToastificationType.error,\n      );\n      return;\n    }\n\n    final isValid = await _validateUrl(cloudUrl) && await _validateUrl(webUrl);\n\n    if (mounted) {\n      if (isValid) {\n        showToastNotification(\n          message: LocaleKeys.settings_menu_changeUrl.tr(args: [cloudUrl]),\n        );\n\n        Navigator.of(context).pop();\n\n        await useBaseWebDomain(webUrl);\n        await useAppFlowyBetaCloudWithURL(cloudUrl, type);\n\n        await runAppFlowy();\n      } else {\n        showToastNotification(\n          message: LocaleKeys.settings_menu_pleaseInputValidURL.tr(),\n          type: ToastificationType.error,\n        );\n      }\n    }\n  }\n\n  Future<bool> _validateUrl(String url) async {\n    return await validateUrl(url).fold(\n      (url) async {\n        return true;\n      },\n      (err) {\n        Log.error(err);\n        return false;\n      },\n    );\n  }\n\n  Future<void> _fetchUrls() async {\n    await Future.wait([\n      getAppFlowyCloudUrl(),\n      getAppFlowyShareDomain(),\n    ]).then((values) {\n      if (values.length != 2) {\n        return;\n      }\n\n      cloudUrlTextController.text = values[0];\n      webUrlTextController.text = values[1];\n\n      if (kAppflowyCloudUrl != values[0]) {\n        setState(() {\n          type = AuthenticatorType.appflowyCloudSelfHost;\n        });\n      }\n    });\n  }\n}\n\n@visibleForTesting\nextension SettingsServerDropdownMenuExtension on AuthenticatorType {\n  String get label {\n    switch (this) {\n      case AuthenticatorType.appflowyCloud:\n        return LocaleKeys.settings_menu_cloudAppFlowy.tr();\n      case AuthenticatorType.appflowyCloudSelfHost:\n        return LocaleKeys.settings_menu_cloudAppFlowySelfHost.tr();\n      default:\n        throw Exception('Unsupported server type: $this');\n    }\n  }\n}\n\n@visibleForTesting\nclass SettingsServerDropdownMenu extends StatelessWidget {\n  const SettingsServerDropdownMenu({\n    super.key,\n    required this.selectedServer,\n    required this.onSelected,\n  });\n\n  final AuthenticatorType selectedServer;\n  final void Function(AuthenticatorType type) onSelected;\n\n  // in the settings page from sign in page, we only support appflowy cloud and self-hosted\n  static final supportedServers = [\n    AuthenticatorType.appflowyCloud,\n    AuthenticatorType.appflowyCloudSelfHost,\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    return SettingsDropdown<AuthenticatorType>(\n      expandWidth: false,\n      onChanged: onSelected,\n      selectedOption: selectedServer,\n      options: supportedServers\n          .map(\n            (serverType) => buildDropdownMenuEntry<AuthenticatorType>(\n              context,\n              selectedValue: selectedServer,\n              value: serverType,\n              label: serverType.label,\n            ),\n          )\n          .toList(),\n    );\n  }\n}\n\nclass _SupportSettings extends StatelessWidget {\n  const _SupportSettings({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return SettingsCategory(\n      title: LocaleKeys.settings_mobile_support.tr(),\n      children: [\n        // export logs\n        Row(\n          children: [\n            FlowyText(\n              LocaleKeys.workspace_errorActions_exportLogFiles.tr(),\n            ),\n            const Spacer(),\n            ConstrainedBox(\n              constraints: const BoxConstraints(minWidth: 78),\n              child: OutlinedRoundedButton(\n                text: LocaleKeys.settings_files_export.tr(),\n                onTap: () {\n                  shareLogFiles(context);\n                },\n              ),\n            ),\n          ],\n        ),\n        // clear cache\n        Row(\n          children: [\n            FlowyText(\n              LocaleKeys.settings_files_clearCache.tr(),\n            ),\n            const Spacer(),\n            ConstrainedBox(\n              constraints: const BoxConstraints(minWidth: 78),\n              child: OutlinedRoundedButton(\n                text: LocaleKeys.button_clear.tr(),\n                onTap: () async {\n                  await getIt<FlowyCacheManager>().clearAllCache();\n                  if (context.mounted) {\n                    showToastNotification(\n                      message: LocaleKeys\n                          .settings_manageDataPage_cache_dialog_successHint\n                          .tr(),\n                    );\n                  }\n                },\n              ),\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n}\n\nclass _SelfHostUrlField extends StatelessWidget {\n  const _SelfHostUrlField({\n    required this.textController,\n    required this.title,\n    required this.hintText,\n    required this.onSave,\n    this.textFieldKey,\n    this.hintBuilder,\n  });\n\n  final TextEditingController textController;\n  final String title;\n  final String hintText;\n  final ValueChanged<String> onSave;\n  final Key? textFieldKey;\n  final WidgetBuilder? hintBuilder;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        _buildHintWidget(context),\n        const VSpace(6.0),\n        SizedBox(\n          height: 36,\n          child: FlowyTextField(\n            key: textFieldKey,\n            controller: textController,\n            autoFocus: false,\n            textStyle: const TextStyle(\n              fontSize: 14,\n              fontWeight: FontWeight.w400,\n            ),\n            hintText: hintText,\n            onEditingComplete: () => onSave(textController.text),\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildHintWidget(BuildContext context) {\n    return Row(\n      children: [\n        FlowyText(\n          title,\n          overflow: TextOverflow.ellipsis,\n        ),\n        hintBuilder?.call(context) ?? const SizedBox.shrink(),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/shared/google_fonts_extension.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\n\nDropdownMenuEntry<T> buildDropdownMenuEntry<T>(\n  BuildContext context, {\n  required T value,\n  required String label,\n  String subLabel = '',\n  T? selectedValue,\n  Widget? leadingWidget,\n  Widget? trailingWidget,\n  String? fontFamily,\n  double maximumHeight = 29,\n}) {\n  final fontFamilyUsed = fontFamily != null\n      ? getGoogleFontSafely(fontFamily).fontFamily ?? defaultFontFamily\n      : defaultFontFamily;\n  Widget? labelWidget;\n  if (subLabel.isNotEmpty) {\n    labelWidget = Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        FlowyText.regular(\n          label,\n          fontSize: 14,\n        ),\n        const VSpace(4),\n        FlowyText.regular(\n          subLabel,\n          fontSize: 10,\n        ),\n      ],\n    );\n  } else {\n    labelWidget = FlowyText.regular(\n      label,\n      fontSize: 14,\n      textAlign: TextAlign.start,\n      fontFamily: fontFamilyUsed,\n    );\n  }\n\n  return DropdownMenuEntry<T>(\n    style: ButtonStyle(\n      foregroundColor:\n          WidgetStatePropertyAll(Theme.of(context).colorScheme.primary),\n      padding: WidgetStateProperty.all(\n        const EdgeInsets.symmetric(horizontal: 6, vertical: 4),\n      ),\n      minimumSize: const WidgetStatePropertyAll(Size(double.infinity, 29)),\n      maximumSize: WidgetStatePropertyAll(Size(double.infinity, maximumHeight)),\n    ),\n    value: value,\n    label: label,\n    leadingIcon: leadingWidget,\n    labelWidget: labelWidget,\n    trailingIcon: Row(\n      children: [\n        if (trailingWidget != null) ...[\n          trailingWidget,\n          const HSpace(8),\n        ],\n        value == selectedValue\n            ? const FlowySvg(FlowySvgs.check_s)\n            : const SizedBox.shrink(),\n      ],\n    ),\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/document_color_setting_button.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/util/color_to_hex_string.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/utils/hex_opacity_string_extension.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flex_color_picker/flex_color_picker.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';\n\nclass DocumentColorSettingButton extends StatefulWidget {\n  const DocumentColorSettingButton({\n    super.key,\n    required this.currentColor,\n    required this.previewWidgetBuilder,\n    required this.dialogTitle,\n    required this.onApply,\n  });\n\n  /// current color from backend\n  final Color currentColor;\n\n  /// Build a preview widget with the given color\n  /// It shows both on the [DocumentColorSettingButton] and [_DocumentColorSettingDialog]\n  final Widget Function(Color? color) previewWidgetBuilder;\n\n  final String dialogTitle;\n\n  final void Function(Color selectedColorOnDialog) onApply;\n\n  @override\n  State<DocumentColorSettingButton> createState() =>\n      _DocumentColorSettingButtonState();\n}\n\nclass _DocumentColorSettingButtonState\n    extends State<DocumentColorSettingButton> {\n  late Color newColor = widget.currentColor;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      margin: const EdgeInsets.all(8),\n      text: widget.previewWidgetBuilder.call(widget.currentColor),\n      hoverColor: Theme.of(context).colorScheme.secondaryContainer,\n      expandText: false,\n      onTap: () => SettingsAlertDialog(\n        title: widget.dialogTitle,\n        confirm: () {\n          widget.onApply(newColor);\n          Navigator.of(context).pop();\n        },\n        children: [\n          _DocumentColorSettingDialog(\n            formKey: GlobalKey<FormState>(),\n            currentColor: widget.currentColor,\n            previewWidgetBuilder: widget.previewWidgetBuilder,\n            onChanged: (color) => newColor = color,\n          ),\n        ],\n      ).show(context),\n    );\n  }\n}\n\nclass _DocumentColorSettingDialog extends StatefulWidget {\n  const _DocumentColorSettingDialog({\n    required this.formKey,\n    required this.currentColor,\n    required this.previewWidgetBuilder,\n    required this.onChanged,\n  });\n\n  final GlobalKey<FormState> formKey;\n  final Color currentColor;\n  final Widget Function(Color?) previewWidgetBuilder;\n  final void Function(Color selectedColor) onChanged;\n\n  @override\n  State<_DocumentColorSettingDialog> createState() =>\n      DocumentColorSettingDialogState();\n}\n\nclass DocumentColorSettingDialogState\n    extends State<_DocumentColorSettingDialog> {\n  /// The color displayed in the dialog.\n  /// It is `null` when the user didn't enter a valid color value.\n  late Color? selectedColorOnDialog;\n  late String currentColorHexString;\n  late TextEditingController hexController;\n  late TextEditingController opacityController;\n\n  @override\n  void initState() {\n    super.initState();\n    selectedColorOnDialog = widget.currentColor;\n    currentColorHexString = ColorExtension(widget.currentColor).toHexString();\n    hexController = TextEditingController(\n      text: currentColorHexString.extractHex(),\n    );\n    opacityController = TextEditingController(\n      text: currentColorHexString.extractOpacity(),\n    );\n  }\n\n  @override\n  void dispose() {\n    hexController.dispose();\n    opacityController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        SizedBox(\n          width: 100,\n          height: 40,\n          child: Center(\n            child: widget.previewWidgetBuilder(\n              selectedColorOnDialog,\n            ),\n          ),\n        ),\n        const VSpace(8),\n        Form(\n          key: widget.formKey,\n          child: Column(\n            children: [\n              _ColorSettingTextField(\n                controller: hexController,\n                labelText: LocaleKeys.editor_hexValue.tr(),\n                hintText: '6fc9e7',\n                onChanged: (_) => _updateSelectedColor(),\n                onFieldSubmitted: (_) => _updateSelectedColor(),\n                validator: (v) => validateHexValue(v, opacityController.text),\n                suffixIcon: Padding(\n                  padding: const EdgeInsets.all(6.0),\n                  child: FlowyIconButton(\n                    onPressed: () => _showColorPickerDialog(\n                      context: context,\n                      currentColor: widget.currentColor,\n                      updateColor: _updateColor,\n                    ),\n                    icon: const FlowySvg(\n                      FlowySvgs.m_aa_color_s,\n                      size: Size.square(20),\n                    ),\n                  ),\n                ),\n              ),\n              const VSpace(8),\n              _ColorSettingTextField(\n                controller: opacityController,\n                labelText: LocaleKeys.editor_opacity.tr(),\n                hintText: '50',\n                onChanged: (_) => _updateSelectedColor(),\n                onFieldSubmitted: (_) => _updateSelectedColor(),\n                validator: (value) => validateOpacityValue(value),\n              ),\n            ],\n          ),\n        ),\n      ],\n    );\n  }\n\n  void _updateSelectedColor() {\n    if (widget.formKey.currentState!.validate()) {\n      setState(() {\n        final colorValue = int.tryParse(\n          hexController.text.combineHexWithOpacity(opacityController.text),\n        );\n        // colorValue has been validated in the _ColorSettingTextField for hex value and it won't be null as this point\n        selectedColorOnDialog = Color(colorValue!);\n        widget.onChanged(selectedColorOnDialog!);\n      });\n    }\n  }\n\n  void _updateColor(Color color) {\n    setState(() {\n      hexController.text = ColorExtension(color).toHexString().extractHex();\n      opacityController.text =\n          ColorExtension(color).toHexString().extractOpacity();\n    });\n    _updateSelectedColor();\n  }\n}\n\nclass _ColorSettingTextField extends StatelessWidget {\n  const _ColorSettingTextField({\n    required this.controller,\n    required this.labelText,\n    required this.hintText,\n    required this.onFieldSubmitted,\n    this.suffixIcon,\n    this.onChanged,\n    this.validator,\n  });\n\n  final TextEditingController controller;\n  final String labelText;\n  final String hintText;\n  final void Function(String) onFieldSubmitted;\n  final Widget? suffixIcon;\n  final void Function(String)? onChanged;\n  final String? Function(String?)? validator;\n\n  @override\n  Widget build(BuildContext context) {\n    final style = Theme.of(context);\n    return TextFormField(\n      controller: controller,\n      decoration: InputDecoration(\n        labelText: labelText,\n        hintText: hintText,\n        suffixIcon: suffixIcon,\n        border: OutlineInputBorder(\n          borderSide: BorderSide(color: style.colorScheme.outline),\n        ),\n        enabledBorder: OutlineInputBorder(\n          borderSide: BorderSide(color: style.colorScheme.outline),\n        ),\n      ),\n      style: style.textTheme.bodyMedium,\n      onChanged: onChanged,\n      onFieldSubmitted: onFieldSubmitted,\n      validator: validator,\n      autovalidateMode: AutovalidateMode.onUserInteraction,\n    );\n  }\n}\n\nString? validateHexValue(String? hexValue, String opacityValue) {\n  if (hexValue == null || hexValue.isEmpty) {\n    return LocaleKeys.settings_appearance_documentSettings_hexEmptyError.tr();\n  }\n  if (hexValue.length != 6) {\n    return LocaleKeys.settings_appearance_documentSettings_hexLengthError.tr();\n  }\n\n  if (validateOpacityValue(opacityValue) == null) {\n    final colorValue =\n        int.tryParse(hexValue.combineHexWithOpacity(opacityValue));\n\n    if (colorValue == null) {\n      return LocaleKeys.settings_appearance_documentSettings_hexInvalidError\n          .tr();\n    }\n  }\n\n  return null;\n}\n\nString? validateOpacityValue(String? value) {\n  if (value == null || value.isEmpty) {\n    return LocaleKeys.settings_appearance_documentSettings_opacityEmptyError\n        .tr();\n  }\n\n  final opacityInt = int.tryParse(value);\n  if (opacityInt == null || opacityInt > 100 || opacityInt <= 0) {\n    return LocaleKeys.settings_appearance_documentSettings_opacityRangeError\n        .tr();\n  }\n  return null;\n}\n\nconst _kColorCircleWidth = 32.0;\nconst _kColorCircleHeight = 32.0;\nconst _kColorCircleRadius = 20.0;\nconst _kColorOpacityThumbRadius = 23.0;\nconst _kDialogButtonPaddingHorizontal = 24.0;\nconst _kDialogButtonPaddingVertical = 12.0;\nconst _kColorsColumnSpacing = 12.0;\n\nclass _ColorPicker extends StatelessWidget {\n  const _ColorPicker({\n    required this.selectedColor,\n    required this.onColorChanged,\n  });\n\n  final Color selectedColor;\n  final void Function(Color) onColorChanged;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = Theme.of(context);\n\n    return ColorPicker(\n      width: _kColorCircleWidth,\n      height: _kColorCircleHeight,\n      borderRadius: _kColorCircleRadius,\n      enableOpacity: true,\n      opacityThumbRadius: _kColorOpacityThumbRadius,\n      columnSpacing: _kColorsColumnSpacing,\n      enableTooltips: false,\n      hasBorder: true,\n      borderColor: theme.colorScheme.outline,\n      pickersEnabled: const {\n        ColorPickerType.both: false,\n        ColorPickerType.primary: true,\n        ColorPickerType.accent: true,\n        ColorPickerType.wheel: true,\n      },\n      subheading: Text(\n        LocaleKeys.settings_appearance_documentSettings_colorShade.tr(),\n        style: theme.textTheme.labelLarge,\n      ),\n      opacitySubheading: Text(\n        LocaleKeys.settings_appearance_documentSettings_opacity.tr(),\n        style: theme.textTheme.labelLarge,\n      ),\n      onColorChanged: onColorChanged,\n    );\n  }\n}\n\nclass _ColorPickerActions extends StatelessWidget {\n  const _ColorPickerActions({\n    required this.onReset,\n    required this.onUpdate,\n  });\n\n  final VoidCallback onReset;\n  final VoidCallback onUpdate;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        SizedBox(\n          height: 24,\n          child: FlowyTextButton(\n            LocaleKeys.button_cancel.tr(),\n            padding: const EdgeInsets.symmetric(\n              horizontal: _kDialogButtonPaddingHorizontal,\n              vertical: _kDialogButtonPaddingVertical,\n            ),\n            fontColor: AFThemeExtension.of(context).textColor,\n            fillColor: Colors.transparent,\n            hoverColor: Colors.transparent,\n            radius: Corners.s12Border,\n            onPressed: onReset,\n          ),\n        ),\n        const HSpace(8),\n        SizedBox(\n          height: 48,\n          child: FlowyTextButton(\n            LocaleKeys.button_done.tr(),\n            padding: const EdgeInsets.symmetric(\n              horizontal: _kDialogButtonPaddingHorizontal,\n              vertical: _kDialogButtonPaddingVertical,\n            ),\n            radius: Corners.s12Border,\n            fontHoverColor: Colors.white,\n            fillColor: Theme.of(context).colorScheme.primary,\n            hoverColor: const Color(0xFF005483),\n            onPressed: onUpdate,\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nvoid _showColorPickerDialog({\n  required BuildContext context,\n  String? title,\n  required Color currentColor,\n  required void Function(Color) updateColor,\n}) {\n  Color selectedColor = currentColor;\n\n  showDialog(\n    context: context,\n    barrierColor: const Color.fromARGB(128, 0, 0, 0),\n    builder: (context) => FlowyDialog(\n      expandHeight: false,\n      title: Row(\n        children: [\n          const FlowySvg(FlowySvgs.m_aa_color_s),\n          const HSpace(12),\n          FlowyText(\n            title ??\n                LocaleKeys.settings_appearance_documentSettings_pickColor.tr(),\n            fontSize: 20,\n          ),\n        ],\n      ),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          _ColorPicker(\n            selectedColor: selectedColor,\n            onColorChanged: (color) => selectedColor = color,\n          ),\n          Padding(\n            padding: const EdgeInsets.all(16),\n            child: Row(\n              mainAxisAlignment: MainAxisAlignment.end,\n              children: [\n                const HSpace(8),\n                _ColorPickerActions(\n                  onReset: () {\n                    updateColor(currentColor);\n                    Navigator.of(context).pop();\n                  },\n                  onUpdate: () {\n                    updateColor(selectedColor);\n                    Navigator.of(context).pop();\n                  },\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    ),\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/flowy_gradient_button.dart",
    "content": "import 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\n\nclass FlowyGradientButton extends StatefulWidget {\n  const FlowyGradientButton({\n    super.key,\n    required this.label,\n    this.onPressed,\n    this.fontWeight = FontWeight.w600,\n    this.textColor = Colors.white,\n    this.backgroundColor,\n  });\n\n  final String label;\n  final VoidCallback? onPressed;\n  final FontWeight fontWeight;\n\n  /// Used to provide a custom foreground color for the button, used in cases\n  /// where a custom [backgroundColor] is provided and the default text color\n  /// does not have enough contrast.\n  ///\n  final Color textColor;\n\n  /// Used to provide a custom background color for the button, this will\n  /// override the gradient behavior, and is mostly used in rare cases\n  /// where the gradient doesn't have contrast with the background.\n  ///\n  final Color? backgroundColor;\n\n  @override\n  State<FlowyGradientButton> createState() => _FlowyGradientButtonState();\n}\n\nclass _FlowyGradientButtonState extends State<FlowyGradientButton> {\n  bool isHovering = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return Listener(\n      onPointerDown: (_) => widget.onPressed?.call(),\n      child: MouseRegion(\n        onEnter: (_) => setState(() => isHovering = true),\n        onExit: (_) => setState(() => isHovering = false),\n        cursor: SystemMouseCursors.click,\n        child: AnimatedContainer(\n          duration: const Duration(milliseconds: 200),\n          decoration: BoxDecoration(\n            boxShadow: [\n              BoxShadow(\n                blurRadius: 4,\n                color: Colors.black.withValues(alpha: 0.25),\n                offset: const Offset(0, 2),\n              ),\n            ],\n            borderRadius: BorderRadius.circular(16),\n            color: widget.backgroundColor,\n            gradient: widget.backgroundColor != null\n                ? null\n                : LinearGradient(\n                    begin: Alignment.topLeft,\n                    end: Alignment.bottomRight,\n                    colors: [\n                      isHovering\n                          ? const Color.fromARGB(255, 57, 40, 92)\n                          : const Color(0xFF44326B),\n                      isHovering\n                          ? const Color.fromARGB(255, 96, 53, 164)\n                          : const Color(0xFF7547C0),\n                    ],\n                  ),\n          ),\n          child: Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),\n            child: FlowyText(\n              widget.label,\n              fontSize: 16,\n              fontWeight: widget.fontWeight,\n              color: widget.textColor,\n              maxLines: 2,\n              textAlign: TextAlign.center,\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_action.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\n\nclass SettingAction extends StatelessWidget {\n  const SettingAction({\n    super.key,\n    required this.onPressed,\n    required this.icon,\n    this.label,\n    this.tooltip,\n  });\n\n  final VoidCallback onPressed;\n  final Widget icon;\n  final String? label;\n  final String? tooltip;\n\n  @override\n  Widget build(BuildContext context) {\n    final child = GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: onPressed,\n      child: SizedBox(\n        height: 26,\n        child: FlowyHover(\n          resetHoverOnRebuild: false,\n          child: Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),\n            child: Row(\n              children: [\n                icon,\n                if (label != null) ...[\n                  const HSpace(4),\n                  FlowyText.regular(label!),\n                ],\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n\n    if (tooltip != null) {\n      return FlowyTooltip(\n        message: tooltip!,\n        child: child,\n      );\n    }\n\n    return child;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_list_tile.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass SettingListTile extends StatelessWidget {\n  const SettingListTile({\n    super.key,\n    this.resetTooltipText,\n    this.resetButtonKey,\n    required this.label,\n    this.hint,\n    this.trailing,\n    this.subtitle,\n    this.onResetRequested,\n  });\n\n  final String label;\n  final String? hint;\n  final String? resetTooltipText;\n  final Key? resetButtonKey;\n  final List<Widget>? trailing;\n  final List<Widget>? subtitle;\n  final VoidCallback? onResetRequested;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        Expanded(\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              FlowyText.medium(\n                label,\n                fontSize: 14,\n                overflow: TextOverflow.ellipsis,\n              ),\n              if (hint != null)\n                Padding(\n                  padding: const EdgeInsets.only(bottom: 4),\n                  child: FlowyText.regular(\n                    hint!,\n                    fontSize: 10,\n                    color: Theme.of(context).hintColor,\n                  ),\n                ),\n              if (subtitle != null) ...subtitle!,\n            ],\n          ),\n        ),\n        if (trailing != null) ...trailing!,\n        if (onResetRequested != null)\n          SettingsResetButton(\n            key: resetButtonKey,\n            resetTooltipText: resetTooltipText,\n            onResetRequested: onResetRequested,\n          ),\n      ],\n    );\n  }\n}\n\nclass SettingsResetButton extends StatelessWidget {\n  const SettingsResetButton({\n    super.key,\n    this.resetTooltipText,\n    this.onResetRequested,\n  });\n\n  final String? resetTooltipText;\n  final VoidCallback? onResetRequested;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyIconButton(\n      hoverColor: Theme.of(context).colorScheme.secondaryContainer,\n      width: 24,\n      icon: FlowySvg(\n        FlowySvgs.restore_s,\n        color: Theme.of(context).iconTheme.color,\n        size: const Size.square(20),\n      ),\n      iconColorOnHover: Theme.of(context).colorScheme.onPrimary,\n      tooltipText:\n          resetTooltipText ?? LocaleKeys.settings_appearance_resetSetting.tr(),\n      onPressed: onResetRequested,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart",
    "content": "import 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass SettingValueDropDown extends StatefulWidget {\n  const SettingValueDropDown({\n    super.key,\n    required this.currentValue,\n    required this.popupBuilder,\n    this.popoverKey,\n    this.onClose,\n    this.child,\n    this.popoverController,\n    this.offset,\n    this.boxConstraints,\n    this.margin = const EdgeInsets.all(6),\n  });\n\n  final String currentValue;\n  final Key? popoverKey;\n  final Widget Function(BuildContext) popupBuilder;\n  final void Function()? onClose;\n  final Widget? child;\n  final PopoverController? popoverController;\n  final Offset? offset;\n  final BoxConstraints? boxConstraints;\n  final EdgeInsets margin;\n\n  @override\n  State<SettingValueDropDown> createState() => _SettingValueDropDownState();\n}\n\nclass _SettingValueDropDownState extends State<SettingValueDropDown> {\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      key: widget.popoverKey,\n      controller: widget.popoverController,\n      direction: PopoverDirection.bottomWithCenterAligned,\n      margin: widget.margin,\n      popupBuilder: widget.popupBuilder,\n      constraints: widget.boxConstraints ??\n          const BoxConstraints(\n            minWidth: 80,\n            maxWidth: 160,\n            maxHeight: 400,\n          ),\n      offset: widget.offset,\n      onClose: widget.onClose,\n      child: widget.child ??\n          FlowyTextButton(\n            widget.currentValue,\n            fontColor: AFThemeExtension.maybeOf(context)?.onBackground,\n            fillColor: Colors.transparent,\n            onPressed: () {},\n          ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_actionable_input.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\n\nclass SettingsActionableInput extends StatelessWidget {\n  const SettingsActionableInput({\n    super.key,\n    required this.controller,\n    this.focusNode,\n    this.placeholder,\n    this.onSave,\n    this.actions = const [],\n  });\n\n  final TextEditingController controller;\n  final FocusNode? focusNode;\n  final String? placeholder;\n  final Function(String)? onSave;\n  final List<Widget> actions;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        Flexible(\n          child: SizedBox(\n            height: 48,\n            child: FlowyTextField(\n              controller: controller,\n              focusNode: focusNode,\n              hintText: placeholder,\n              autoFocus: false,\n              isDense: false,\n              suffixIconConstraints:\n                  BoxConstraints.tight(const Size(23 + 18, 24)),\n              textStyle: const TextStyle(\n                fontWeight: FontWeight.w500,\n                fontSize: 14,\n              ),\n              onSubmitted: onSave,\n            ),\n          ),\n        ),\n        if (actions.isNotEmpty) ...[\n          const HSpace(8),\n          SeparatedRow(\n            separatorBuilder: () => const HSpace(16),\n            children: actions,\n          ),\n        ],\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';\nimport 'package:flutter/material.dart';\n\nclass SettingsAlertDialog extends StatefulWidget {\n  const SettingsAlertDialog({\n    super.key,\n    this.icon,\n    required this.title,\n    this.subtitle,\n    this.children,\n    this.cancel,\n    this.confirm,\n    this.confirmLabel,\n    this.hideCancelButton = false,\n    this.isDangerous = false,\n    this.implyLeading = false,\n    this.enableConfirmNotifier,\n  });\n\n  final Widget? icon;\n  final String title;\n  final String? subtitle;\n  final List<Widget>? children;\n  final void Function()? cancel;\n  final void Function()? confirm;\n  final String? confirmLabel;\n  final bool hideCancelButton;\n  final bool isDangerous;\n  final ValueNotifier<bool>? enableConfirmNotifier;\n\n  /// If true, a back button will show in the top left corner\n  final bool implyLeading;\n\n  @override\n  State<SettingsAlertDialog> createState() => _SettingsAlertDialogState();\n}\n\nclass _SettingsAlertDialogState extends State<SettingsAlertDialog> {\n  bool enableConfirm = true;\n\n  @override\n  void initState() {\n    super.initState();\n    if (widget.enableConfirmNotifier != null) {\n      widget.enableConfirmNotifier!.addListener(_updateEnableConfirm);\n      enableConfirm = widget.enableConfirmNotifier!.value;\n    }\n  }\n\n  void _updateEnableConfirm() {\n    setState(() => enableConfirm = widget.enableConfirmNotifier!.value);\n  }\n\n  @override\n  void dispose() {\n    if (widget.enableConfirmNotifier != null) {\n      widget.enableConfirmNotifier!.removeListener(_updateEnableConfirm);\n    }\n    super.dispose();\n  }\n\n  @override\n  void didUpdateWidget(covariant SettingsAlertDialog oldWidget) {\n    oldWidget.enableConfirmNotifier?.removeListener(_updateEnableConfirm);\n    widget.enableConfirmNotifier?.addListener(_updateEnableConfirm);\n    enableConfirm = widget.enableConfirmNotifier?.value ?? true;\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return StyledDialog(\n      maxHeight: 600,\n      maxWidth: 600,\n      padding: const EdgeInsets.all(24),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          Row(\n            mainAxisAlignment: MainAxisAlignment.end,\n            children: [\n              if (widget.implyLeading) ...[\n                GestureDetector(\n                  onTap: Navigator.of(context).pop,\n                  child: MouseRegion(\n                    cursor: SystemMouseCursors.click,\n                    child: Row(\n                      children: [\n                        const FlowySvg(\n                          FlowySvgs.arrow_back_m,\n                          size: Size.square(24),\n                        ),\n                        const HSpace(8),\n                        FlowyText.semibold(\n                          LocaleKeys.button_back.tr(),\n                          fontSize: 16,\n                        ),\n                      ],\n                    ),\n                  ),\n                ),\n              ],\n              const Spacer(),\n              GestureDetector(\n                onTap: Navigator.of(context).pop,\n                child: MouseRegion(\n                  cursor: SystemMouseCursors.click,\n                  child: FlowySvg(\n                    FlowySvgs.m_close_m,\n                    size: const Size.square(20),\n                    color: Theme.of(context).colorScheme.outline,\n                  ),\n                ),\n              ),\n            ],\n          ),\n          if (widget.icon != null) ...[\n            widget.icon!,\n            const VSpace(16),\n          ],\n          Row(\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              Flexible(\n                child: FlowyText.medium(\n                  widget.title,\n                  fontSize: 22,\n                  color: Theme.of(context).colorScheme.tertiary,\n                  maxLines: null,\n                ),\n              ),\n            ],\n          ),\n          if (widget.subtitle?.isNotEmpty ?? false) ...[\n            const VSpace(16),\n            Row(\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                Flexible(\n                  child: FlowyText.regular(\n                    widget.subtitle!,\n                    fontSize: 16,\n                    color: Theme.of(context).colorScheme.tertiary,\n                    textAlign: TextAlign.center,\n                    maxLines: null,\n                  ),\n                ),\n              ],\n            ),\n          ],\n          if (widget.children?.isNotEmpty ?? false) ...[\n            const VSpace(16),\n            ...widget.children!,\n          ],\n          if (widget.confirm != null || !widget.hideCancelButton) ...[\n            const VSpace(20),\n          ],\n          _Actions(\n            hideCancelButton: widget.hideCancelButton,\n            confirmLabel: widget.confirmLabel,\n            cancel: widget.cancel,\n            confirm: widget.confirm,\n            isDangerous: widget.isDangerous,\n            enableConfirm: enableConfirm,\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass _Actions extends StatelessWidget {\n  const _Actions({\n    required this.hideCancelButton,\n    this.confirmLabel,\n    this.cancel,\n    this.confirm,\n    this.isDangerous = false,\n    this.enableConfirm = true,\n  });\n\n  final bool hideCancelButton;\n  final String? confirmLabel;\n  final VoidCallback? cancel;\n  final VoidCallback? confirm;\n  final bool isDangerous;\n  final bool enableConfirm;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      mainAxisAlignment: MainAxisAlignment.center,\n      children: [\n        if (!hideCancelButton) ...[\n          SizedBox(\n            height: 48,\n            child: PrimaryRoundedButton(\n              text: LocaleKeys.button_cancel.tr(),\n              margin: const EdgeInsets.symmetric(\n                horizontal: 24,\n                vertical: 12,\n              ),\n              fontWeight: FontWeight.w600,\n              radius: 12.0,\n              onTap: () {\n                cancel?.call();\n                Navigator.of(context).pop();\n              },\n            ),\n          ),\n        ],\n        if (confirm != null && !hideCancelButton) ...[\n          const HSpace(8),\n        ],\n        if (confirm != null) ...[\n          SizedBox(\n            height: 48,\n            child: FlowyTextButton(\n              confirmLabel ?? LocaleKeys.button_confirm.tr(),\n              padding: const EdgeInsets.symmetric(\n                horizontal: 24,\n                vertical: 12,\n              ),\n              radius: Corners.s12Border,\n              fontColor: isDangerous ? Colors.white : null,\n              fontHoverColor: !enableConfirm ? null : Colors.white,\n              fillColor: !enableConfirm\n                  ? Theme.of(context).dividerColor\n                  : isDangerous\n                      ? Theme.of(context).colorScheme.error\n                      : Theme.of(context).colorScheme.primary,\n              hoverColor: !enableConfirm\n                  ? Theme.of(context).dividerColor\n                  : isDangerous\n                      ? Theme.of(context).colorScheme.error\n                      : const Color(0xFF005483),\n              onPressed: enableConfirm ? confirm : null,\n            ),\n          ),\n        ],\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart",
    "content": "import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass SettingsBody extends StatelessWidget {\n  const SettingsBody({\n    super.key,\n    required this.title,\n    this.description,\n    this.descriptionBuilder,\n    this.autoSeparate = true,\n    required this.children,\n  });\n\n  final String title;\n  final String? description;\n  final WidgetBuilder? descriptionBuilder;\n  final bool autoSeparate;\n  final List<Widget> children;\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleChildScrollView(\n      physics: const ClampingScrollPhysics(),\n      padding: const EdgeInsets.all(24),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          SettingsHeader(\n            title: title,\n            description: description,\n            descriptionBuilder: descriptionBuilder,\n          ),\n          SettingsCategorySpacer(),\n          Flexible(\n            child: SeparatedColumn(\n              mainAxisSize: MainAxisSize.min,\n              separatorBuilder: () => autoSeparate\n                  ? const SettingsCategorySpacer()\n                  : const SizedBox.shrink(),\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: children,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\n/// Renders a simple category taking a title and the list\n/// of children (settings) to be rendered.\n///\nclass SettingsCategory extends StatelessWidget {\n  const SettingsCategory({\n    super.key,\n    required this.title,\n    this.description,\n    this.descriptionColor,\n    this.tooltip,\n    this.actions,\n    required this.children,\n  });\n\n  final String title;\n  final String? description;\n  final Color? descriptionColor;\n  final String? tooltip;\n  final List<Widget>? actions;\n  final List<Widget> children;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Row(\n          children: [\n            Text(\n              title,\n              style: theme.textStyle.heading4.enhanced(\n                color: theme.textColorScheme.primary,\n              ),\n              maxLines: 2,\n              overflow: TextOverflow.ellipsis,\n            ),\n            if (tooltip != null) ...[\n              const HSpace(4),\n              FlowyTooltip(\n                message: tooltip,\n                child: const FlowySvg(FlowySvgs.information_s),\n              ),\n            ],\n            const Spacer(),\n            if (actions != null) ...actions!,\n          ],\n        ),\n        const VSpace(16),\n        if (description?.isNotEmpty ?? false) ...[\n          FlowyText.regular(\n            description!,\n            maxLines: 4,\n            fontSize: 12,\n            overflow: TextOverflow.ellipsis,\n            color: descriptionColor,\n          ),\n          const VSpace(8),\n        ],\n        SeparatedColumn(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          separatorBuilder: () =>\n              children.length > 1 ? const VSpace(16) : const SizedBox.shrink(),\n          children: children,\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\n/// This is used to create a uniform space and divider\n/// between categories in settings.\n///\nclass SettingsCategorySpacer extends StatelessWidget {\n  const SettingsCategorySpacer({\n    super.key,\n    this.topSpacing,\n    this.bottomSpacing,\n  });\n\n  final double? topSpacing;\n  final double? bottomSpacing;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Padding(\n      padding: EdgeInsets.only(\n        top: topSpacing ?? theme.spacing.l,\n        bottom: bottomSpacing ?? theme.spacing.l,\n      ),\n      child: Divider(\n        color: theme.borderColorScheme.primary,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dashed_divider.dart",
    "content": "import 'package:flutter/material.dart';\n\n/// Renders a dashed divider\n///\n/// The length of each dash is the same as the gap.\n///\nclass SettingsDashedDivider extends StatelessWidget {\n  const SettingsDashedDivider({\n    super.key,\n    this.color,\n    this.height,\n    this.strokeWidth = 1.0,\n    this.gap = 3.0,\n    this.direction = Axis.horizontal,\n  });\n\n  // The color of the divider, defaults to the theme's divider color\n  final Color? color;\n\n  // The height of the divider, this will surround the divider equally\n  final double? height;\n\n  // Thickness of the divider\n  final double strokeWidth;\n\n  // Gap between the dashes\n  final double gap;\n\n  // Direction of the divider\n  final Axis direction;\n\n  @override\n  Widget build(BuildContext context) {\n    final double padding =\n        height != null && height! > 0 ? (height! - strokeWidth) / 2 : 0;\n\n    return LayoutBuilder(\n      builder: (context, constraints) {\n        final items = _calculateItems(constraints);\n        return Padding(\n          padding: EdgeInsets.symmetric(\n            vertical: direction == Axis.horizontal ? padding : 0,\n            horizontal: direction == Axis.vertical ? padding : 0,\n          ),\n          child: Wrap(\n            direction: direction,\n            children: List.generate(\n              items,\n              (index) => Container(\n                margin: EdgeInsets.only(\n                  right: direction == Axis.horizontal ? gap : 0,\n                  bottom: direction == Axis.vertical ? gap : 0,\n                ),\n                width: direction == Axis.horizontal ? gap : strokeWidth,\n                height: direction == Axis.vertical ? gap : strokeWidth,\n                decoration: BoxDecoration(\n                  color: color ?? Theme.of(context).dividerColor,\n                  borderRadius: BorderRadius.circular(1.0),\n                ),\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  int _calculateItems(BoxConstraints constraints) {\n    final double totalLength = direction == Axis.horizontal\n        ? constraints.maxWidth\n        : constraints.maxHeight;\n\n    return (totalLength / (gap * 2)).floor();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart",
    "content": "import 'package:appflowy/flutter/af_dropdown_menu.dart';\nimport 'package:appflowy/shared/google_fonts_extension.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SettingsDropdown<T> extends StatefulWidget {\n  const SettingsDropdown({\n    super.key,\n    required this.selectedOption,\n    required this.options,\n    this.onChanged,\n    this.actions,\n    this.expandWidth = true,\n    this.selectOptionCompare,\n    this.textStyle,\n  });\n\n  final T selectedOption;\n  final CompareFunction<T>? selectOptionCompare;\n  final List<DropdownMenuEntry<T>> options;\n  final void Function(T)? onChanged;\n  final List<Widget>? actions;\n  final bool expandWidth;\n  final TextStyle? textStyle;\n\n  @override\n  State<SettingsDropdown<T>> createState() => _SettingsDropdownState<T>();\n}\n\nclass _SettingsDropdownState<T> extends State<SettingsDropdown<T>> {\n  late final TextEditingController controller = TextEditingController(\n    text: widget.selectedOption is String\n        ? widget.selectedOption as String\n        : widget.options\n                .firstWhereOrNull((e) => e.value == widget.selectedOption)\n                ?.label ??\n            '',\n  );\n\n  @override\n  Widget build(BuildContext context) {\n    final fontFamily = context.read<AppearanceSettingsCubit>().state.font;\n    final fontFamilyUsed =\n        getGoogleFontSafely(fontFamily).fontFamily ?? defaultFontFamily;\n\n    return Row(\n      children: [\n        Expanded(\n          child: AFDropdownMenu<T>(\n            controller: controller,\n            expandedInsets: widget.expandWidth ? EdgeInsets.zero : null,\n            initialSelection: widget.selectedOption,\n            dropdownMenuEntries: widget.options,\n            selectOptionCompare: widget.selectOptionCompare,\n            textStyle: widget.textStyle ??\n                Theme.of(context).textTheme.bodyLarge?.copyWith(\n                      fontFamily: fontFamilyUsed,\n                      fontWeight: FontWeight.w400,\n                    ),\n            menuStyle: MenuStyle(\n              maximumSize:\n                  const WidgetStatePropertyAll(Size(double.infinity, 250)),\n              elevation: const WidgetStatePropertyAll(10),\n              shadowColor:\n                  WidgetStatePropertyAll(Colors.black.withValues(alpha: 0.4)),\n              backgroundColor: WidgetStatePropertyAll(\n                Theme.of(context).cardColor,\n              ),\n              padding: const WidgetStatePropertyAll(\n                EdgeInsets.symmetric(horizontal: 6, vertical: 8),\n              ),\n              alignment: Alignment.bottomLeft,\n            ),\n            inputDecorationTheme: InputDecorationTheme(\n              contentPadding: const EdgeInsets.symmetric(\n                vertical: 12,\n                horizontal: 18,\n              ),\n              enabledBorder: OutlineInputBorder(\n                borderSide: BorderSide(\n                  color: Theme.of(context).colorScheme.outline,\n                ),\n                borderRadius: Corners.s8Border,\n              ),\n              focusedBorder: OutlineInputBorder(\n                borderSide: BorderSide(\n                  color: Theme.of(context).colorScheme.primary,\n                ),\n                borderRadius: Corners.s8Border,\n              ),\n              errorBorder: OutlineInputBorder(\n                borderSide: BorderSide(\n                  color: Theme.of(context).colorScheme.error,\n                ),\n                borderRadius: Corners.s8Border,\n              ),\n              focusedErrorBorder: OutlineInputBorder(\n                borderSide: BorderSide(\n                  color: Theme.of(context).colorScheme.error,\n                ),\n                borderRadius: Corners.s8Border,\n              ),\n            ),\n            onSelected: (v) async {\n              v != null ? widget.onChanged?.call(v) : null;\n            },\n          ),\n        ),\n        if (widget.actions?.isNotEmpty == true) ...[\n          const HSpace(16),\n          SeparatedRow(\n            separatorBuilder: () => const HSpace(8),\n            children: widget.actions!,\n          ),\n        ],\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\n/// Renders a simple header for the settings view\n///\nclass SettingsHeader extends StatelessWidget {\n  const SettingsHeader({\n    super.key,\n    required this.title,\n    this.description,\n    this.descriptionBuilder,\n  });\n\n  final String title;\n  final String? description;\n  final WidgetBuilder? descriptionBuilder;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Text(\n          title,\n          style: theme.textStyle.heading2.enhanced(\n            color: theme.textColorScheme.primary,\n          ),\n        ),\n        if (descriptionBuilder != null) ...[\n          VSpace(theme.spacing.xs),\n          descriptionBuilder!(context),\n        ] else if (description?.isNotEmpty == true) ...[\n          VSpace(theme.spacing.xs),\n          Text(\n            description!,\n            maxLines: 4,\n            style: theme.textStyle.caption.standard(\n              color: theme.textColorScheme.secondary,\n            ),\n          ),\n        ],\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_input_field.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\n/// This is used to describe a settings input field\n///\n/// The input will have secondary action of \"save\" and \"cancel\"\n/// which will only be shown when the input has changed.\n///\n/// _Note: The label can overflow and will be ellipsized._\n///\nclass SettingsInputField extends StatefulWidget {\n  const SettingsInputField({\n    super.key,\n    this.label,\n    this.textController,\n    this.focusNode,\n    this.obscureText = false,\n    this.value,\n    this.placeholder,\n    this.tooltip,\n    this.onSave,\n    this.onCancel,\n    this.hideActions = false,\n    this.onChanged,\n  });\n\n  final String? label;\n  final TextEditingController? textController;\n  final FocusNode? focusNode;\n\n  /// If true, the input field will be obscured\n  /// and an option to toggle to show the text will be provided.\n  ///\n  final bool obscureText;\n\n  final String? value;\n  final String? placeholder;\n  final String? tooltip;\n\n  /// If true the save and cancel options will not show below the\n  /// input field.\n  ///\n  final bool hideActions;\n\n  final void Function(String)? onSave;\n\n  /// The action to be performed when the cancel button is pressed.\n  ///\n  /// If null the button will **NOT** be disabled! Instead it will\n  /// reset the input to the original value.\n  ///\n  final void Function()? onCancel;\n\n  final void Function(String)? onChanged;\n\n  @override\n  State<SettingsInputField> createState() => _SettingsInputFieldState();\n}\n\nclass _SettingsInputFieldState extends State<SettingsInputField> {\n  late final controller =\n      widget.textController ?? TextEditingController(text: widget.value);\n  late final FocusNode focusNode = widget.focusNode ?? FocusNode();\n  late bool obscureText = widget.obscureText;\n\n  @override\n  void dispose() {\n    if (widget.focusNode == null) {\n      focusNode.dispose();\n    }\n    if (widget.textController == null) {\n      controller.dispose();\n    }\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        Row(\n          children: [\n            if (widget.label?.isNotEmpty == true) ...[\n              Flexible(\n                child: FlowyText.medium(\n                  widget.label!,\n                  color: AFThemeExtension.of(context).secondaryTextColor,\n                ),\n              ),\n            ],\n            if (widget.tooltip != null) ...[\n              const HSpace(4),\n              FlowyTooltip(\n                message: widget.tooltip,\n                child: const FlowySvg(FlowySvgs.information_s),\n              ),\n            ],\n          ],\n        ),\n        if (widget.label?.isNotEmpty ?? false || widget.tooltip != null)\n          const VSpace(8),\n        AFTextField(\n          controller: controller,\n          focusNode: focusNode,\n          hintText: widget.placeholder,\n          obscureText: obscureText,\n          onSubmitted: widget.onSave,\n          onChanged: (_) {\n            widget.onChanged?.call(controller.text);\n            setState(() {});\n          },\n          suffixIconBuilder: (context, isObscured) => widget.obscureText\n              ? PasswordSuffixIcon(\n                  isObscured: isObscured,\n                  onTap: () {\n                    setState(() => obscureText = !obscureText);\n                  },\n                )\n              : null,\n        ),\n        if (!widget.hideActions &&\n            ((widget.value == null && controller.text.isNotEmpty) ||\n                widget.value != null && widget.value != controller.text)) ...[\n          const VSpace(8),\n          Row(\n            children: [\n              const Spacer(),\n              SizedBox(\n                height: 21,\n                child: FlowyTextButton(\n                  LocaleKeys.button_save.tr(),\n                  fontWeight: FontWeight.normal,\n                  padding: EdgeInsets.zero,\n                  fillColor: Colors.transparent,\n                  hoverColor: Colors.transparent,\n                  fontColor: AFThemeExtension.of(context).textColor,\n                  onPressed: () => widget.onSave?.call(controller.text),\n                ),\n              ),\n              const HSpace(24),\n              SizedBox(\n                height: 21,\n                child: FlowyTextButton(\n                  LocaleKeys.button_cancel.tr(),\n                  fontWeight: FontWeight.normal,\n                  padding: EdgeInsets.zero,\n                  fillColor: Colors.transparent,\n                  hoverColor: Colors.transparent,\n                  fontColor: AFThemeExtension.of(context).textColor,\n                  onPressed: () {\n                    setState(() => controller.text = widget.value ?? '');\n                    widget.onCancel?.call();\n                  },\n                ),\n              ),\n            ],\n          ),\n        ],\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_radio_select.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\n\nclass SettingsRadioItem<T> {\n  const SettingsRadioItem({\n    required this.value,\n    required this.label,\n    required this.isSelected,\n    this.icon,\n  });\n\n  final T value;\n  final String label;\n  final bool isSelected;\n  final Widget? icon;\n}\n\nclass SettingsRadioSelect<T> extends StatelessWidget {\n  const SettingsRadioSelect({\n    super.key,\n    required this.items,\n    required this.onChanged,\n    this.selectedItem,\n  });\n\n  final List<SettingsRadioItem<T>> items;\n  final void Function(SettingsRadioItem<T>) onChanged;\n  final SettingsRadioItem<T>? selectedItem;\n\n  @override\n  Widget build(BuildContext context) {\n    return Wrap(\n      spacing: 24,\n      runSpacing: 8,\n      children: items\n          .map(\n            (i) => GestureDetector(\n              onTap: () => onChanged(i),\n              child: Row(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  Container(\n                    width: 14,\n                    height: 14,\n                    padding: const EdgeInsets.all(2),\n                    decoration: BoxDecoration(\n                      shape: BoxShape.circle,\n                      border: Border.all(\n                        color: AFThemeExtension.of(context).textColor,\n                      ),\n                    ),\n                    child: DecoratedBox(\n                      decoration: BoxDecoration(\n                        color: i.isSelected\n                            ? AFThemeExtension.of(context).textColor\n                            : Colors.transparent,\n                        shape: BoxShape.circle,\n                      ),\n                    ),\n                  ),\n                  const HSpace(8),\n                  if (i.icon != null) ...[i.icon!, const HSpace(4)],\n                  FlowyText.regular(i.label, fontSize: 14),\n                ],\n              ),\n            ),\n          )\n          .toList(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_subcategory.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\n\n/// Renders a simple category taking a title and the list\n/// of children (settings) to be rendered.\n///\nclass SettingsSubcategory extends StatelessWidget {\n  const SettingsSubcategory({\n    super.key,\n    required this.title,\n    required this.children,\n  });\n\n  final String title;\n  final List<Widget> children;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        FlowyText.medium(\n          title,\n          color: AFThemeExtension.of(context).secondaryTextColor,\n          maxLines: 2,\n          fontSize: 14,\n          overflow: TextOverflow.ellipsis,\n        ),\n        const VSpace(8),\n        SeparatedColumn(\n          mainAxisSize: MainAxisSize.min,\n          separatorBuilder: () =>\n              children.length > 1 ? const VSpace(16) : const SizedBox.shrink(),\n          children: children,\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart",
    "content": "import 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\n\nenum SingleSettingsButtonType {\n  primary,\n  danger,\n  highlight;\n\n  bool get isPrimary => this == primary;\n  bool get isDangerous => this == danger;\n  bool get isHighlight => this == highlight;\n}\n\n/// This is used to describe a single setting action\n///\n/// This will render a simple action that takes the title,\n/// the button label, and the button action.\n///\n/// _Note: The label can overflow and will be ellipsized,\n/// unless maxLines is overriden._\n///\nclass SingleSettingAction extends StatelessWidget {\n  const SingleSettingAction({\n    super.key,\n    required this.label,\n    this.description,\n    this.labelMaxLines,\n    required this.buttonLabel,\n    this.onPressed,\n    this.buttonType = SingleSettingsButtonType.primary,\n    this.fontSize = 14,\n    this.fontWeight = FontWeight.normal,\n    this.minWidth,\n  });\n\n  final String label;\n  final String? description;\n  final int? labelMaxLines;\n  final String buttonLabel;\n\n  /// The action to be performed when the button is pressed\n  ///\n  /// If null the button will be rendered as disabled.\n  ///\n  final VoidCallback? onPressed;\n\n  final SingleSettingsButtonType buttonType;\n\n  final double fontSize;\n  final FontWeight fontWeight;\n  final double? minWidth;\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        Expanded(\n          child: Column(\n            children: [\n              Row(\n                children: [\n                  Expanded(\n                    child: FlowyText(\n                      label,\n                      fontSize: fontSize,\n                      fontWeight: fontWeight,\n                      maxLines: labelMaxLines,\n                      overflow: TextOverflow.ellipsis,\n                      color: AFThemeExtension.of(context).secondaryTextColor,\n                    ),\n                  ),\n                ],\n              ),\n              if (description != null) ...[\n                const VSpace(4),\n                Row(\n                  children: [\n                    Expanded(\n                      child: FlowyText.regular(\n                        description!,\n                        fontSize: 11,\n                        color: AFThemeExtension.of(context).secondaryTextColor,\n                        maxLines: 2,\n                      ),\n                    ),\n                  ],\n                ),\n              ],\n            ],\n          ),\n        ),\n        const HSpace(24),\n        ConstrainedBox(\n          constraints: BoxConstraints(\n            minWidth: minWidth ?? 0.0,\n            maxHeight: 32,\n            minHeight: 32,\n          ),\n          child: FlowyTextButton(\n            buttonLabel,\n            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7),\n            fillColor: fillColor(context),\n            radius: Corners.s8Border,\n            hoverColor: hoverColor(context),\n            fontColor: fontColor(context),\n            fontHoverColor: fontHoverColor(context),\n            borderColor: borderColor(context),\n            fontSize: 12,\n            isDangerous: buttonType.isDangerous,\n            onPressed: onPressed,\n            lineHeight: 1.0,\n          ),\n        ),\n      ],\n    );\n  }\n\n  Color? fillColor(BuildContext context) {\n    if (buttonType.isPrimary) {\n      return Theme.of(context).colorScheme.primary;\n    }\n    return Colors.transparent;\n  }\n\n  Color? hoverColor(BuildContext context) {\n    if (buttonType.isDangerous) {\n      return Theme.of(context).colorScheme.error.withValues(alpha: 0.1);\n    }\n\n    if (buttonType.isPrimary) {\n      return Theme.of(context).colorScheme.primary.withValues(alpha: 0.9);\n    }\n\n    if (buttonType.isHighlight) {\n      return const Color(0xFF5C3699);\n    }\n    return null;\n  }\n\n  Color? fontColor(BuildContext context) {\n    if (buttonType.isDangerous) {\n      return Theme.of(context).colorScheme.error;\n    }\n\n    if (buttonType.isHighlight) {\n      return const Color(0xFF5C3699);\n    }\n\n    return Theme.of(context).colorScheme.onPrimary;\n  }\n\n  Color? fontHoverColor(BuildContext context) {\n    return Colors.white;\n  }\n\n  Color? borderColor(BuildContext context) {\n    if (buttonType.isHighlight) {\n      return const Color(0xFF5C3699);\n    }\n\n    return null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/_restart_app_button.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass RestartButton extends StatelessWidget {\n  const RestartButton({\n    super.key,\n    required this.showRestartHint,\n    required this.onClick,\n  });\n\n  final bool showRestartHint;\n  final VoidCallback onClick;\n\n  @override\n  Widget build(BuildContext context) {\n    final List<Widget> children = [_buildRestartButton(context)];\n    if (showRestartHint) {\n      children.add(\n        Padding(\n          padding: const EdgeInsets.only(top: 10),\n          child: FlowyText(\n            LocaleKeys.settings_menu_restartAppTip.tr(),\n            maxLines: null,\n          ),\n        ),\n      );\n    }\n\n    return Column(children: children);\n  }\n\n  Widget _buildRestartButton(BuildContext context) {\n    if (UniversalPlatform.isDesktopOrWeb) {\n      return Row(\n        children: [\n          SizedBox(\n            height: 42,\n            child: PrimaryRoundedButton(\n              text: LocaleKeys.settings_menu_restartApp.tr(),\n              margin: const EdgeInsets.symmetric(horizontal: 24),\n              fontWeight: FontWeight.w600,\n              radius: 12.0,\n              onTap: onClick,\n            ),\n          ),\n        ],\n      );\n      // Row(\n      //   children: [\n      //     FlowyButton(\n      //       isSelected: true,\n      //       useIntrinsicWidth: true,\n      //       margin: const EdgeInsets.symmetric(\n      //         horizontal: 30,\n      //         vertical: 10,\n      //       ),\n      //       text: FlowyText(\n      //         LocaleKeys.settings_menu_restartApp.tr(),\n      //       ),\n      //       onTap: onClick,\n      //     ),\n      //     const Spacer(),\n      //   ],\n      // );\n    } else {\n      return MobileLogoutButton(\n        text: LocaleKeys.settings_menu_restartApp.tr(),\n        onPressed: onClick,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/cancel_plan_survey_dialog.dart",
    "content": "import 'dart:convert';\n\nimport 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\n\nFuture<String?> showCancelSurveyDialog(BuildContext context) {\n  return showDialog<String?>(\n    context: context,\n    builder: (_) => const _Survey(),\n  );\n}\n\nclass _Survey extends StatefulWidget {\n  const _Survey();\n\n  @override\n  State<_Survey> createState() => _SurveyState();\n}\n\nclass _SurveyState extends State<_Survey> {\n  final PageController pageController = PageController();\n  final Map<String, String> answers = {};\n\n  @override\n  void dispose() {\n    pageController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Dialog(\n      shape: RoundedRectangleBorder(\n        borderRadius: BorderRadius.circular(12.0),\n      ),\n      child: SizedBox(\n        width: 674,\n        child: Padding(\n          padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),\n          child: Row(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              Expanded(\n                child: Column(\n                  mainAxisSize: MainAxisSize.min,\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    // Survey title\n                    Row(\n                      mainAxisSize: MainAxisSize.min,\n                      children: [\n                        Expanded(\n                          child: FlowyText(\n                            LocaleKeys.settings_cancelSurveyDialog_title.tr(),\n                            fontSize: 22.0,\n                            overflow: TextOverflow.ellipsis,\n                            color: AFThemeExtension.of(context).strongText,\n                          ),\n                        ),\n                        FlowyButton(\n                          useIntrinsicWidth: true,\n                          text: const FlowySvg(FlowySvgs.upgrade_close_s),\n                          onTap: () => Navigator.of(context).pop(),\n                        ),\n                      ],\n                    ),\n                    const VSpace(12),\n                    // Survey explanation\n                    FlowyText(\n                      LocaleKeys.settings_cancelSurveyDialog_description.tr(),\n                      maxLines: 3,\n                    ),\n                    const VSpace(8),\n                    const Divider(),\n                    const VSpace(8),\n                    // Question \"sheet\"\n                    SizedBox(\n                      height: 400,\n                      width: 650,\n                      child: PageView.builder(\n                        controller: pageController,\n                        itemCount: _questionsAndAnswers.length,\n                        itemBuilder: (context, index) => _QAPage(\n                          qa: _questionsAndAnswers[index],\n                          isFirstQuestion: index == 0,\n                          isFinalQuestion:\n                              index == _questionsAndAnswers.length - 1,\n                          selectedAnswer:\n                              answers[_questionsAndAnswers[index].question],\n                          onPrevious: () {\n                            if (index > 0) {\n                              pageController.animateToPage(\n                                index - 1,\n                                duration: const Duration(milliseconds: 300),\n                                curve: Curves.easeInOut,\n                              );\n                            }\n                          },\n                          onAnswerChanged: (answer) {\n                            answers[_questionsAndAnswers[index].question] =\n                                answer;\n                          },\n                          onAnswerSelected: (answer) {\n                            answers[_questionsAndAnswers[index].question] =\n                                answer;\n\n                            if (index == _questionsAndAnswers.length - 1) {\n                              Navigator.of(context).pop(jsonEncode(answers));\n                            } else {\n                              pageController.animateToPage(\n                                index + 1,\n                                duration: const Duration(milliseconds: 300),\n                                curve: Curves.easeInOut,\n                              );\n                            }\n                          },\n                        ),\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _QAPage extends StatefulWidget {\n  const _QAPage({\n    required this.qa,\n    required this.onAnswerSelected,\n    required this.onAnswerChanged,\n    required this.onPrevious,\n    this.selectedAnswer,\n    this.isFirstQuestion = false,\n    this.isFinalQuestion = false,\n  });\n\n  final _QA qa;\n  final String? selectedAnswer;\n\n  /// Called when \"Next\" is pressed\n  ///\n  final Function(String) onAnswerSelected;\n\n  /// Called whenever an answer is selected or changed\n  ///\n  final Function(String) onAnswerChanged;\n  final VoidCallback onPrevious;\n  final bool isFirstQuestion;\n  final bool isFinalQuestion;\n\n  @override\n  State<_QAPage> createState() => _QAPageState();\n}\n\nclass _QAPageState extends State<_QAPage> {\n  final otherController = TextEditingController();\n\n  int _selectedIndex = -1;\n  String? answer;\n\n  @override\n  void initState() {\n    super.initState();\n    if (widget.selectedAnswer != null) {\n      answer = widget.selectedAnswer;\n      _selectedIndex = widget.qa.answers.indexOf(widget.selectedAnswer!);\n      if (_selectedIndex == -1) {\n        // We assume the last question is \"Other\"\n        _selectedIndex = widget.qa.answers.length - 1;\n        otherController.text = widget.selectedAnswer!;\n      }\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        FlowyText(\n          widget.qa.question,\n          fontSize: 16.0,\n          color: AFThemeExtension.of(context).strongText,\n        ),\n        const VSpace(18),\n        SeparatedColumn(\n          separatorBuilder: () => const VSpace(6),\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: widget.qa.answers\n              .mapIndexed(\n                (index, option) => _AnswerOption(\n                  prefix: _indexToLetter(index),\n                  option: option,\n                  isSelected: _selectedIndex == index,\n                  onTap: () => setState(() {\n                    _selectedIndex = index;\n                    if (_selectedIndex == widget.qa.answers.length - 1 &&\n                        widget.qa.lastIsOther) {\n                      answer = otherController.text;\n                    } else {\n                      answer = option;\n                    }\n                    widget.onAnswerChanged(option);\n                  }),\n                ),\n              )\n              .toList(),\n        ),\n        if (widget.qa.lastIsOther &&\n            _selectedIndex == widget.qa.answers.length - 1) ...[\n          const VSpace(8),\n          FlowyTextField(\n            controller: otherController,\n            hintText: LocaleKeys.settings_cancelSurveyDialog_otherHint.tr(),\n            onChanged: (value) => setState(() {\n              answer = value;\n              widget.onAnswerChanged(value);\n            }),\n          ),\n        ],\n        const VSpace(20),\n        Row(\n          children: [\n            if (!widget.isFirstQuestion) ...[\n              DecoratedBox(\n                decoration: ShapeDecoration(\n                  shape: RoundedRectangleBorder(\n                    side: const BorderSide(color: Color(0x1E14171B)),\n                    borderRadius: BorderRadius.circular(8),\n                  ),\n                ),\n                child: FlowyButton(\n                  useIntrinsicWidth: true,\n                  margin: const EdgeInsets.symmetric(\n                    horizontal: 16.0,\n                    vertical: 9.0,\n                  ),\n                  text: FlowyText.regular(LocaleKeys.button_previous.tr()),\n                  onTap: widget.onPrevious,\n                ),\n              ),\n              const HSpace(12.0),\n            ],\n            DecoratedBox(\n              decoration: ShapeDecoration(\n                color: Theme.of(context).colorScheme.primary,\n                shape: RoundedRectangleBorder(\n                  borderRadius: BorderRadius.circular(8),\n                ),\n              ),\n              child: FlowyButton(\n                useIntrinsicWidth: true,\n                margin:\n                    const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0),\n                radius: BorderRadius.circular(8),\n                text: FlowyText.regular(\n                  widget.isFinalQuestion\n                      ? LocaleKeys.button_submit.tr()\n                      : LocaleKeys.button_next.tr(),\n                  color: Colors.white,\n                ),\n                disable: !canProceed(),\n                onTap: canProceed()\n                    ? () => widget.onAnswerSelected(\n                          answer ?? widget.qa.answers[_selectedIndex],\n                        )\n                    : null,\n              ),\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n\n  bool canProceed() {\n    if (_selectedIndex == widget.qa.answers.length - 1 &&\n        widget.qa.lastIsOther) {\n      return answer != null &&\n          answer!.isNotEmpty &&\n          answer != LocaleKeys.settings_cancelSurveyDialog_commonOther.tr();\n    }\n\n    return _selectedIndex != -1;\n  }\n}\n\nclass _AnswerOption extends StatelessWidget {\n  const _AnswerOption({\n    required this.prefix,\n    required this.option,\n    required this.onTap,\n    this.isSelected = false,\n  });\n\n  final String prefix;\n  final String option;\n  final VoidCallback onTap;\n  final bool isSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      cursor: SystemMouseCursors.click,\n      child: GestureDetector(\n        onTap: onTap,\n        child: Container(\n          padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),\n          decoration: BoxDecoration(\n            borderRadius: Corners.s8Border,\n            border: Border.all(\n              width: 2,\n              color: isSelected\n                  ? Theme.of(context).colorScheme.primary\n                  : Theme.of(context).dividerColor,\n            ),\n          ),\n          child: Row(\n            mainAxisSize: MainAxisSize.min,\n            children: [\n              const HSpace(2),\n              Container(\n                width: 24,\n                height: 24,\n                decoration: BoxDecoration(\n                  color: isSelected\n                      ? Theme.of(context).colorScheme.primary\n                      : Theme.of(context).dividerColor,\n                  borderRadius: Corners.s6Border,\n                ),\n                child: Center(\n                  child: FlowyText(\n                    prefix,\n                    color: isSelected ? Colors.white : null,\n                  ),\n                ),\n              ),\n              const HSpace(8),\n              FlowyText(\n                option,\n                fontWeight: FontWeight.w400,\n                fontSize: 16.0,\n                color: AFThemeExtension.of(context).strongText,\n              ),\n              const HSpace(6),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nfinal _questionsAndAnswers = [\n  _QA(\n    question: LocaleKeys.settings_cancelSurveyDialog_questionOne_question.tr(),\n    answers: [\n      LocaleKeys.settings_cancelSurveyDialog_questionOne_answerOne.tr(),\n      LocaleKeys.settings_cancelSurveyDialog_questionOne_answerTwo.tr(),\n      LocaleKeys.settings_cancelSurveyDialog_questionOne_answerThree.tr(),\n      LocaleKeys.settings_cancelSurveyDialog_questionOne_answerFour.tr(),\n      LocaleKeys.settings_cancelSurveyDialog_questionOne_answerFive.tr(),\n      LocaleKeys.settings_cancelSurveyDialog_commonOther.tr(),\n    ],\n    lastIsOther: true,\n  ),\n  _QA(\n    question: LocaleKeys.settings_cancelSurveyDialog_questionTwo_question.tr(),\n    answers: [\n      LocaleKeys.settings_cancelSurveyDialog_questionTwo_answerOne.tr(),\n      LocaleKeys.settings_cancelSurveyDialog_questionTwo_answerTwo.tr(),\n      LocaleKeys.settings_cancelSurveyDialog_questionTwo_answerThree.tr(),\n      LocaleKeys.settings_cancelSurveyDialog_questionTwo_answerFour.tr(),\n      LocaleKeys.settings_cancelSurveyDialog_questionTwo_answerFive.tr(),\n    ],\n  ),\n  _QA(\n    question:\n        LocaleKeys.settings_cancelSurveyDialog_questionThree_question.tr(),\n    answers: [\n      LocaleKeys.settings_cancelSurveyDialog_questionThree_answerOne.tr(),\n      LocaleKeys.settings_cancelSurveyDialog_questionThree_answerTwo.tr(),\n      LocaleKeys.settings_cancelSurveyDialog_questionThree_answerThree.tr(),\n      LocaleKeys.settings_cancelSurveyDialog_questionThree_answerFour.tr(),\n      LocaleKeys.settings_cancelSurveyDialog_commonOther.tr(),\n    ],\n    lastIsOther: true,\n  ),\n  _QA(\n    question: LocaleKeys.settings_cancelSurveyDialog_questionFour_question.tr(),\n    answers: [\n      LocaleKeys.settings_cancelSurveyDialog_questionFour_answerOne.tr(),\n      LocaleKeys.settings_cancelSurveyDialog_questionFour_answerTwo.tr(),\n      LocaleKeys.settings_cancelSurveyDialog_questionFour_answerThree.tr(),\n      LocaleKeys.settings_cancelSurveyDialog_questionFour_answerFour.tr(),\n      LocaleKeys.settings_cancelSurveyDialog_questionFour_answerFive.tr(),\n    ],\n  ),\n];\n\nclass _QA {\n  const _QA({\n    required this.question,\n    required this.answers,\n    this.lastIsOther = false,\n  });\n\n  final String question;\n  final List<String> answers;\n  final bool lastIsOther;\n}\n\n/// Returns the letter corresponding to the index.\n///\n/// Eg. 0 -> A, 1 -> B, 2 -> C, ..., and so forth.\n///\nString _indexToLetter(int index) {\n  return String.fromCharCode(65 + index);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart",
    "content": "export 'emoji_shortcut_event.dart';\nexport 'src/emji_picker_config.dart';\nexport 'src/emoji_picker.dart';\nexport 'src/emoji_picker_builder.dart';\nexport 'src/flowy_emoji_picker_config.dart';\nexport 'src/models/emoji_model.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart",
    "content": "import 'package:appflowy/plugins/emoji/emoji_actions_command.dart';\nimport 'package:appflowy/plugins/emoji/emoji_menu.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:flutter/material.dart';\n\nfinal CommandShortcutEvent emojiShortcutEvent = CommandShortcutEvent(\n  key: 'Ctrl + Alt + E to show emoji picker',\n  command: 'ctrl+alt+e',\n  macOSCommand: 'cmd+alt+e',\n  getDescription: () => 'Show an emoji picker',\n  handler: _emojiShortcutHandler,\n);\n\nCommandShortcutEventHandler _emojiShortcutHandler = (editorState) {\n  final selection = editorState.selection;\n  if (selection == null) {\n    return KeyEventResult.ignored;\n  }\n  final node = editorState.getNodeAtPath(selection.start.path);\n  final context = node?.context;\n  if (node == null ||\n      context == null ||\n      node.delta == null ||\n      node.type == CodeBlockKeys.type) {\n    return KeyEventResult.ignored;\n  }\n  final container = Overlay.of(context);\n  emojiMenuService = EmojiMenu(editorState: editorState, overlay: container);\n  emojiMenuService?.show('');\n  return KeyEventResult.handled;\n};\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/default_emoji_picker_view.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\n\nimport 'emoji_picker.dart';\nimport 'emoji_picker_builder.dart';\nimport 'models/emoji_category_models.dart';\nimport 'models/emoji_model.dart';\n\nclass DefaultEmojiPickerView extends EmojiPickerBuilder {\n  const DefaultEmojiPickerView(\n    super.config,\n    super.state, {\n    super.key,\n  });\n\n  @override\n  DefaultEmojiPickerViewState createState() => DefaultEmojiPickerViewState();\n}\n\nclass DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>\n    with TickerProviderStateMixin {\n  PageController? _pageController;\n  TabController? _tabController;\n  final TextEditingController _emojiController = TextEditingController();\n  final FocusNode _emojiFocusNode = FocusNode();\n  EmojiCategoryGroup searchEmojiList =\n      EmojiCategoryGroup(EmojiCategory.SEARCH, <Emoji>[]);\n  final scrollController = ScrollController();\n\n  @override\n  void initState() {\n    super.initState();\n\n    int initCategory = widget.state.emojiCategoryGroupList.indexWhere(\n      (el) => el.category == widget.config.initCategory,\n    );\n    if (initCategory == -1) {\n      initCategory = 0;\n    }\n    _tabController = TabController(\n      initialIndex: initCategory,\n      length: widget.state.emojiCategoryGroupList.length,\n      vsync: this,\n    );\n    _pageController = PageController(initialPage: initCategory);\n    _emojiFocusNode.requestFocus();\n    _emojiController.addListener(_onEmojiChanged);\n  }\n\n  @override\n  void dispose() {\n    _emojiController.removeListener(_onEmojiChanged);\n    _emojiController.dispose();\n    _emojiFocusNode.dispose();\n    _pageController?.dispose();\n    _tabController?.dispose();\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  void _onEmojiChanged() {\n    final String query = _emojiController.text.toLowerCase();\n    if (query.isEmpty) {\n      searchEmojiList.emoji.clear();\n      _pageController!.jumpToPage(_tabController!.index);\n    } else {\n      searchEmojiList.emoji.clear();\n      for (final element in widget.state.emojiCategoryGroupList) {\n        searchEmojiList.emoji.addAll(\n          element.emoji\n              .where((item) => item.name.toLowerCase().contains(query))\n              .toList(),\n        );\n      }\n    }\n    setState(() {});\n  }\n\n  Widget _buildBackspaceButton() {\n    if (widget.state.onBackspacePressed != null) {\n      return Material(\n        type: MaterialType.transparency,\n        child: IconButton(\n          padding: const EdgeInsets.only(bottom: 2),\n          icon: Icon(Icons.backspace, color: widget.config.backspaceColor),\n          onPressed: () => widget.state.onBackspacePressed!(),\n        ),\n      );\n    }\n\n    return const SizedBox.shrink();\n  }\n\n  bool isEmojiSearching() =>\n      searchEmojiList.emoji.isNotEmpty || _emojiController.text.isNotEmpty;\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(\n      builder: (context, constraints) {\n        final emojiSize = widget.config.getEmojiSize(constraints.maxWidth);\n        final style = Theme.of(context);\n\n        return Container(\n          color: widget.config.bgColor,\n          padding: const EdgeInsets.all(4),\n          child: Column(\n            children: [\n              const VSpace(4),\n              // search bar\n              SizedBox(\n                height: 32.0,\n                child: TextField(\n                  controller: _emojiController,\n                  focusNode: _emojiFocusNode,\n                  autofocus: true,\n                  style: style.textTheme.bodyMedium,\n                  cursorColor: style.textTheme.bodyMedium?.color,\n                  decoration: InputDecoration(\n                    contentPadding: const EdgeInsets.all(8),\n                    hintText: widget.config.searchHintText,\n                    hintStyle: widget.config.serachHintTextStyle,\n                    enabledBorder: widget.config.serachBarEnableBorder,\n                    focusedBorder: widget.config.serachBarFocusedBorder,\n                  ),\n                ),\n              ),\n              const VSpace(4),\n              Row(\n                children: [\n                  Expanded(\n                    child: TabBar(\n                      labelColor: widget.config.selectedCategoryIconColor,\n                      unselectedLabelColor: widget.config.categoryIconColor,\n                      controller: isEmojiSearching()\n                          ? TabController(length: 1, vsync: this)\n                          : _tabController,\n                      labelPadding: EdgeInsets.zero,\n                      indicatorColor:\n                          widget.config.selectedCategoryIconBackgroundColor,\n                      padding: const EdgeInsets.symmetric(vertical: 4.0),\n                      indicator: BoxDecoration(\n                        border: Border.all(color: Colors.transparent),\n                        borderRadius: BorderRadius.circular(4.0),\n                        color: style.colorScheme.secondary,\n                      ),\n                      onTap: (index) {\n                        _pageController!.animateToPage(\n                          index,\n                          duration: widget.config.tabIndicatorAnimDuration,\n                          curve: Curves.ease,\n                        );\n                      },\n                      tabs: isEmojiSearching()\n                          ? [_buildCategory(EmojiCategory.SEARCH, emojiSize)]\n                          : widget.state.emojiCategoryGroupList\n                              .asMap()\n                              .entries\n                              .map<Widget>(\n                                (item) => _buildCategory(\n                                  item.value.category,\n                                  emojiSize,\n                                ),\n                              )\n                              .toList(),\n                    ),\n                  ),\n                  _buildBackspaceButton(),\n                ],\n              ),\n              Flexible(\n                child: PageView.builder(\n                  itemCount: searchEmojiList.emoji.isNotEmpty\n                      ? 1\n                      : widget.state.emojiCategoryGroupList.length,\n                  controller: _pageController,\n                  physics: const NeverScrollableScrollPhysics(),\n                  itemBuilder: (context, index) {\n                    final EmojiCategoryGroup emojiCategoryGroup =\n                        isEmojiSearching()\n                            ? searchEmojiList\n                            : widget.state.emojiCategoryGroupList[index];\n                    return _buildPage(emojiSize, emojiCategoryGroup);\n                  },\n                ),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildCategory(EmojiCategory category, double categorySize) {\n    return Tab(\n      height: categorySize,\n      child: Icon(\n        widget.config.getIconForCategory(category),\n        size: categorySize / 1.3,\n      ),\n    );\n  }\n\n  Widget _buildButtonWidget({\n    required VoidCallback onPressed,\n    required Widget child,\n  }) {\n    if (widget.config.buttonMode == ButtonMode.MATERIAL) {\n      return InkWell(onTap: onPressed, child: child);\n    }\n    return GestureDetector(onTap: onPressed, child: child);\n  }\n\n  Widget _buildPage(double emojiSize, EmojiCategoryGroup emojiCategoryGroup) {\n    // Display notice if recent has no entries yet\n    if (emojiCategoryGroup.category == EmojiCategory.RECENT &&\n        emojiCategoryGroup.emoji.isEmpty) {\n      return _buildNoRecent();\n    } else if (emojiCategoryGroup.category == EmojiCategory.SEARCH &&\n        emojiCategoryGroup.emoji.isEmpty) {\n      return Center(child: Text(widget.config.noEmojiFoundText));\n    }\n    // Build page normally\n    return ScrollbarListStack(\n      axis: Axis.vertical,\n      controller: scrollController,\n      barSize: 4.0,\n      scrollbarPadding: const EdgeInsets.symmetric(horizontal: 4.0),\n      handleColor: widget.config.scrollBarHandleColor,\n      showTrack: true,\n      child: ScrollConfiguration(\n        behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),\n        child: GridView.builder(\n          controller: scrollController,\n          padding: const EdgeInsets.all(0),\n          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\n            crossAxisCount: widget.config.emojiNumberPerRow,\n            mainAxisSpacing: widget.config.verticalSpacing,\n            crossAxisSpacing: widget.config.horizontalSpacing,\n          ),\n          itemCount: emojiCategoryGroup.emoji.length,\n          itemBuilder: (context, index) {\n            final item = emojiCategoryGroup.emoji[index];\n            return _buildEmoji(emojiSize, emojiCategoryGroup, item);\n          },\n          cacheExtent: 10,\n        ),\n      ),\n    );\n  }\n\n  Widget _buildEmoji(\n    double emojiSize,\n    EmojiCategoryGroup emojiCategoryGroup,\n    Emoji emoji,\n  ) {\n    return _buildButtonWidget(\n      onPressed: () {\n        widget.state.onEmojiSelected(emojiCategoryGroup.category, emoji);\n      },\n      child: FlowyHover(\n        child: FittedBox(\n          child: Text(\n            emoji.emoji,\n            style: TextStyle(fontSize: emojiSize),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildNoRecent() {\n    return Center(\n      child: Text(\n        widget.config.noRecentsText,\n        style: widget.config.noRecentsStyle,\n        textAlign: TextAlign.center,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emji_picker_config.dart",
    "content": "import 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport 'emoji_picker.dart';\nimport 'models/emoji_category_models.dart';\n\npart 'emji_picker_config.freezed.dart';\n\n@freezed\nclass EmojiPickerConfig with _$EmojiPickerConfig {\n  // private empty constructor is used to make method work in freezed\n  // https://pub.dev/packages/freezed#adding-getters-and-methods-to-our-models\n  const EmojiPickerConfig._();\n  const factory EmojiPickerConfig({\n    @Default(7) int emojiNumberPerRow,\n    // The maximum size(width and height) of emoji\n    // It also depaneds on the screen size and emojiNumberPerRow\n    @Default(32) double emojiSizeMax,\n    // Vertical spacing between emojis\n    @Default(0) double verticalSpacing,\n    // Horizontal spacing between emojis\n    @Default(0) double horizontalSpacing,\n    // The initial [EmojiCategory] that will be selected\n    @Default(EmojiCategory.RECENT) EmojiCategory initCategory,\n    // The background color of the Widget\n    @Default(Color(0xFFEBEFF2)) Color? bgColor,\n    // The color of the category icons\n    @Default(Colors.grey) Color? categoryIconColor,\n    // The color of the category icon when selected\n    @Default(Colors.blue) Color? selectedCategoryIconColor,\n    // The color of the category indicator\n    @Default(Colors.blue) Color? selectedCategoryIconBackgroundColor,\n    // The color of the loading indicator during initialization\n    @Default(Colors.blue) Color? progressIndicatorColor,\n    // The color of the backspace icon button\n    @Default(Colors.blue) Color? backspaceColor,\n    // Show extra tab with recently used emoji\n    @Default(true) bool showRecentsTab,\n    // Limit of recently used emoji that will be saved\n    @Default(28) int recentsLimit,\n    @Default('Search emoji') String searchHintText,\n    TextStyle? serachHintTextStyle,\n    InputBorder? serachBarEnableBorder,\n    InputBorder? serachBarFocusedBorder,\n    // The text to be displayed if no recent emojis to display\n    @Default('No recent emoji') String noRecentsText,\n    TextStyle? noRecentsStyle,\n    // The text to be displayed if no emoji found\n    @Default('No emoji found') String noEmojiFoundText,\n    Color? scrollBarHandleColor,\n    // Duration of tab indicator to animate to next category\n    @Default(kTabScrollDuration) Duration tabIndicatorAnimDuration,\n    // Determines the icon to display for each [EmojiCategory]\n    @Default(EmojiCategoryIcons()) EmojiCategoryIcons emojiCategoryIcons,\n    // Change between Material and Cupertino button style\n    @Default(ButtonMode.MATERIAL) ButtonMode buttonMode,\n  }) = _EmojiPickerConfig;\n\n  /// Get Emoji size based on properties and screen width\n  double getEmojiSize(double width) {\n    final maxSize = width / emojiNumberPerRow;\n    return min(maxSize, emojiSizeMax);\n  }\n\n  /// Returns the icon for the category\n  IconData getIconForCategory(EmojiCategory category) {\n    switch (category) {\n      case EmojiCategory.RECENT:\n        return emojiCategoryIcons.recentIcon;\n      case EmojiCategory.SMILEYS:\n        return emojiCategoryIcons.smileyIcon;\n      case EmojiCategory.ANIMALS:\n        return emojiCategoryIcons.animalIcon;\n      case EmojiCategory.FOODS:\n        return emojiCategoryIcons.foodIcon;\n      case EmojiCategory.TRAVEL:\n        return emojiCategoryIcons.travelIcon;\n      case EmojiCategory.ACTIVITIES:\n        return emojiCategoryIcons.activityIcon;\n      case EmojiCategory.OBJECTS:\n        return emojiCategoryIcons.objectIcon;\n      case EmojiCategory.SYMBOLS:\n        return emojiCategoryIcons.symbolIcon;\n      case EmojiCategory.FLAGS:\n        return emojiCategoryIcons.flagIcon;\n      case EmojiCategory.SEARCH:\n        return emojiCategoryIcons.searchIcon;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_lists.dart",
    "content": "// Copyright information\n// File originally from https://github.com/JeffG05/emoji_picker\n\n// import 'emoji.dart';\n\n// final List<Map<String, String>> temp = [smileys, animals, foods, activities, travel, objects, symbols, flags];\n\n// final List<Emoji> emojiSearchList = temp\n//     .map((element) {\n//       return element.entries.map<Emoji>((entry) => Emoji(entry.key, entry.value)).toList();\n//     })\n//     .toList()\n//     .first;\n\n/// Map of all possible emojis along with their names in [Category.SMILEYS]\nfinal Map<String, String> smileys = Map.fromIterables([\n  'Grinning Face',\n  'Grinning Face With Big Eyes',\n  'Grinning Face With Smiling Eyes',\n  'Beaming Face With Smiling Eyes',\n  'Grinning Squinting Face',\n  'Grinning Face With Sweat',\n  'Rolling on the Floor Laughing',\n  'Face With Tears of Joy',\n  'Slightly Smiling Face',\n  'Upside-Down Face',\n  'Winking Face',\n  'Smiling Face With Smiling Eyes',\n  'Smiling Face With Halo',\n  'Smiling Face With Hearts',\n  'Smiling Face With Heart-Eyes',\n  'Star-Struck',\n  'Face Blowing a Kiss',\n  'Kissing Face',\n  'Smiling Face',\n  'Kissing Face With Closed Eyes',\n  'Kissing Face With Smiling Eyes',\n  'Face Savoring Food',\n  'Face With Tongue',\n  'Winking Face With Tongue',\n  'Zany Face',\n  'Squinting Face With Tongue',\n  'Money-Mouth Face',\n  'Hugging Face',\n  'Face With Hand Over Mouth',\n  'Shushing Face',\n  'Thinking Face',\n  'Zipper-Mouth Face',\n  'Face With Raised Eyebrow',\n  'Neutral Face',\n  'Expressionless Face',\n  'Face Without Mouth',\n  'Smirking Face',\n  'Unamused Face',\n  'Face With Rolling Eyes',\n  'Grimacing Face',\n  'Lying Face',\n  'Relieved Face',\n  'Pensive Face',\n  'Sleepy Face',\n  'Drooling Face',\n  'Sleeping Face',\n  'Face With Medical Mask',\n  'Face With Thermometer',\n  'Face With Head-Bandage',\n  'Nauseated Face',\n  'Face Vomiting',\n  'Sneezing Face',\n  'Hot Face',\n  'Cold Face',\n  'Woozy Face',\n  'Dizzy Face',\n  'Exploding Head',\n  'Cowboy Hat Face',\n  'Partying Face',\n  'Smiling Face With Sunglasses',\n  'Nerd Face',\n  'Face With Monocle',\n  'Confused Face',\n  'Worried Face',\n  'Slightly Frowning Face',\n  'Frowning Face',\n  'Face With Open Mouth',\n  'Hushed Face',\n  'Astonished Face',\n  'Flushed Face',\n  'Pleading Face',\n  'Frowning Face With Open Mouth',\n  'Anguished Face',\n  'Fearful Face',\n  'Anxious Face With Sweat',\n  'Sad but Relieved Face',\n  'Crying Face',\n  'Loudly Crying Face',\n  'Face Screaming in Fear',\n  'Confounded Face',\n  'Persevering Face',\n  'Disappointed Face',\n  'Downcast Face With Sweat',\n  'Weary Face',\n  'Tired Face',\n  'Face With Steam From Nose',\n  'Pouting Face',\n  'Angry Face',\n  'Face With Symbols on Mouth',\n  'Smiling Face With Horns',\n  'Angry Face With Horns',\n  'Skull',\n  'Skull and Crossbones',\n  'Pile of Poo',\n  'Clown Face',\n  'Ogre',\n  'Goblin',\n  'Ghost',\n  'Alien',\n  'Alien Monster',\n  'Robot Face',\n  'Grinning Cat Face',\n  'Grinning Cat Face With Smiling Eyes',\n  'Cat Face With Tears of Joy',\n  'Smiling Cat Face With Heart-Eyes',\n  'Cat Face With Wry Smile',\n  'Kissing Cat Face',\n  'Weary Cat Face',\n  'Crying Cat Face',\n  'Pouting Cat Face',\n  'Kiss Mark',\n  'Waving Hand',\n  'Raised Back of Hand',\n  'Hand With Fingers Splayed',\n  'Raised Hand',\n  'Vulcan Salute',\n  'OK Hand',\n  'Victory Hand',\n  'Crossed Fingers',\n  'Love-You Gesture',\n  'Sign of the Horns',\n  'Call Me Hand',\n  'Backhand Index Pointing Left',\n  'Backhand Index Pointing Right',\n  'Backhand Index Pointing Up',\n  'Middle Finger',\n  'Backhand Index Pointing Down',\n  'Index Pointing Up',\n  'Thumbs Up',\n  'Thumbs Down',\n  'Raised Fist',\n  'Oncoming Fist',\n  'Left-Facing Fist',\n  'Right-Facing Fist',\n  'Clapping Hands',\n  'Raising Hands',\n  'Open Hands',\n  'Palms Up Together',\n  'Handshake',\n  'Folded Hands',\n  'Writing Hand',\n  'Nail Polish',\n  'Selfie',\n  'Flexed Biceps',\n  'Leg',\n  'Foot',\n  'Ear',\n  'Nose',\n  'Brain',\n  'Tooth',\n  'Bone',\n  'Eyes',\n  'Eye',\n  'Tongue',\n  'Mouth',\n  'Baby',\n  'Child',\n  'Boy',\n  'Girl',\n  'Person',\n  'Man',\n  'Man: Beard',\n  'Man: Blond Hair',\n  'Man: Red Hair',\n  'Man: Curly Hair',\n  'Man: White Hair',\n  'Man: Bald',\n  'Woman',\n  'Woman: Blond Hair',\n  'Woman: Red Hair',\n  'Woman: Curly Hair',\n  'Woman: White Hair',\n  'Woman: Bald',\n  'Older Person',\n  'Old Man',\n  'Old Woman',\n  'Man Frowning',\n  'Woman Frowning',\n  'Man Pouting',\n  'Woman Pouting',\n  'Man Gesturing No',\n  'Woman Gesturing No',\n  'Man Gesturing OK',\n  'Woman Gesturing OK',\n  'Man Tipping Hand',\n  'Woman Tipping Hand',\n  'Man Raising Hand',\n  'Woman Raising Hand',\n  'Man Bowing',\n  'Woman Bowing',\n  'Man Facepalming',\n  'Woman Facepalming',\n  'Man Shrugging',\n  'Woman Shrugging',\n  'Man Health Worker',\n  'Woman Health Worker',\n  'Man Student',\n  'Woman Student',\n  'Man Teacher',\n  'Woman Teacher',\n  'Man Judge',\n  'Woman Judge',\n  'Man Farmer',\n  'Woman Farmer',\n  'Man Cook',\n  'Woman Cook',\n  'Man Mechanic',\n  'Woman Mechanic',\n  'Man Factory Worker',\n  'Woman Factory Worker',\n  'Man Office Worker',\n  'Woman Office Worker',\n  'Man Scientist',\n  'Woman Scientist',\n  'Man Technologist',\n  'Woman Technologist',\n  'Man Singer',\n  'Woman Singer',\n  'Man Artist',\n  'Woman Artist',\n  'Man Pilot',\n  'Woman Pilot',\n  'Man Astronaut',\n  'Woman Astronaut',\n  'Man Firefighter',\n  'Woman Firefighter',\n  'Man Police Officer',\n  'Woman Police Officer',\n  'Man Detective',\n  'Woman Detective',\n  'Man Guard',\n  'Woman Guard',\n  'Man Construction Worker',\n  'Woman Construction Worker',\n  'Prince',\n  'Princess',\n  'Man Wearing Turban',\n  'Woman Wearing Turban',\n  'Man With Chinese Cap',\n  'Woman With Headscarf',\n  'Man in Tuxedo',\n  'Bride With Veil',\n  'Pregnant Woman',\n  'Breast-Feeding',\n  'Baby Angel',\n  'Santa Claus',\n  'Mrs. Claus',\n  'Man Superhero',\n  'Woman Superhero',\n  'Man Supervillain',\n  'Woman Supervillain',\n  'Man Mage',\n  'Woman Mage',\n  'Man Fairy',\n  'Woman Fairy',\n  'Man Vampire',\n  'Woman Vampire',\n  'Merman',\n  'Mermaid',\n  'Man Elf',\n  'Woman Elf',\n  'Man Genie',\n  'Woman Genie',\n  'Man Zombie',\n  'Woman Zombie',\n  'Man Getting Massage',\n  'Woman Getting Massage',\n  'Man Getting Haircut',\n  'Woman Getting Haircut',\n  'Man Walking',\n  'Woman Walking',\n  'Man Running',\n  'Woman Running',\n  'Woman Dancing',\n  'Man Dancing',\n  'Man in Suit Levitating',\n  'Men With Bunny Ears',\n  'Women With Bunny Ears',\n  'Man in Steamy Room',\n  'Woman in Steamy Room',\n  'Person in Lotus Position',\n  'Women Holding Hands',\n  'Woman and Man Holding Hands',\n  'Men Holding Hands',\n  'Kiss',\n  'Kiss: Man, Man',\n  'Kiss: Woman, Woman',\n  'Couple With Heart',\n  'Couple With Heart: Man, Man',\n  'Couple With Heart: Woman, Woman',\n  'Family',\n  'Family: Man, Woman, Boy',\n  'Family: Man, Woman, Girl',\n  'Family: Man, Woman, Girl, Boy',\n  'Family: Man, Woman, Boy, Boy',\n  'Family: Man, Woman, Girl, Girl',\n  'Family: Man, Man, Boy',\n  'Family: Man, Man, Girl',\n  'Family: Man, Man, Girl, Boy',\n  'Family: Man, Man, Boy, Boy',\n  'Family: Man, Man, Girl, Girl',\n  'Family: Woman, Woman, Boy',\n  'Family: Woman, Woman, Girl',\n  'Family: Woman, Woman, Girl, Boy',\n  'Family: Woman, Woman, Boy, Boy',\n  'Family: Woman, Woman, Girl, Girl',\n  'Family: Man, Boy',\n  'Family: Man, Boy, Boy',\n  'Family: Man, Girl',\n  'Family: Man, Girl, Boy',\n  'Family: Man, Girl, Girl',\n  'Family: Woman, Boy',\n  'Family: Woman, Boy, Boy',\n  'Family: Woman, Girl',\n  'Family: Woman, Girl, Boy',\n  'Family: Woman, Girl, Girl',\n  'Speaking Head',\n  'Bust in Silhouette',\n  'Busts in Silhouette',\n  'Footprints',\n  'Luggage',\n  'Closed Umbrella',\n  'Umbrella',\n  'Thread',\n  'Yarn',\n  'Glasses',\n  'Sunglasses',\n  'Goggles',\n  'Lab Coat',\n  'Necktie',\n  'T-Shirt',\n  'Jeans',\n  'Scarf',\n  'Gloves',\n  'Coat',\n  'Socks',\n  'Dress',\n  'Kimono',\n  'Bikini',\n  'Woman’s Clothes',\n  'Purse',\n  'Handbag',\n  'Clutch Bag',\n  'Backpack',\n  'Man’s Shoe',\n  'Running Shoe',\n  'Hiking Boot',\n  'Flat Shoe',\n  'High-Heeled Shoe',\n  'Woman’s Sandal',\n  'Woman’s Boot',\n  'Crown',\n  'Woman’s Hat',\n  'Top Hat',\n  'Graduation Cap',\n  'Billed Cap',\n  'Rescue Worker’s Helmet',\n  'Lipstick',\n  'Ring',\n  'Briefcase',\n], [\n  '😀',\n  '😃',\n  '😄',\n  '😁',\n  '😆',\n  '😅',\n  '🤣',\n  '😂',\n  '🙂',\n  '🙃',\n  '😉',\n  '😊',\n  '😇',\n  '🥰',\n  '😍',\n  '🤩',\n  '😘',\n  '😗',\n  '☺',\n  '😚',\n  '😙',\n  '😋',\n  '😛',\n  '😜',\n  '🤪',\n  '😝',\n  '🤑',\n  '🤗',\n  '🤭',\n  '🤫',\n  '🤔',\n  '🤐',\n  '🤨',\n  '😐',\n  '😑',\n  '😶',\n  '😏',\n  '😒',\n  '🙄',\n  '😬',\n  '🤥',\n  '😌',\n  '😔',\n  '😪',\n  '🤤',\n  '😴',\n  '😷',\n  '🤒',\n  '🤕',\n  '🤢',\n  '🤮',\n  '🤧',\n  '🥵',\n  '🥶',\n  '🥴',\n  '😵',\n  '🤯',\n  '🤠',\n  '🥳',\n  '😎',\n  '🤓',\n  '🧐',\n  '😕',\n  '😟',\n  '🙁',\n  '☹️',\n  '😮',\n  '😯',\n  '😲',\n  '😳',\n  '🥺',\n  '😦',\n  '😧',\n  '😨',\n  '😰',\n  '😥',\n  '😢',\n  '😭',\n  '😱',\n  '😖',\n  '😣',\n  '😞',\n  '😓',\n  '😩',\n  '😫',\n  '😤',\n  '😡',\n  '😠',\n  '🤬',\n  '😈',\n  '👿',\n  '💀',\n  '☠',\n  '💩',\n  '🤡',\n  '👹',\n  '👺',\n  '👻',\n  '👽',\n  '👾',\n  '🤖',\n  '😺',\n  '😸',\n  '😹',\n  '😻',\n  '😼',\n  '😽',\n  '🙀',\n  '😿',\n  '😾',\n  '💋',\n  '👋',\n  '🤚',\n  '🖐',\n  '✋',\n  '🖖',\n  '👌',\n  '✌',\n  '🤞',\n  '🤟',\n  '🤘',\n  '🤙',\n  '👈',\n  '👉',\n  '👆',\n  '🖕',\n  '👇',\n  '☝',\n  '👍',\n  '👎',\n  '✊',\n  '👊',\n  '🤛',\n  '🤜',\n  '👏',\n  '🙌',\n  '👐',\n  '🤲',\n  '🤝',\n  '🙏',\n  '✍',\n  '💅',\n  '🤳',\n  '💪',\n  '🦵',\n  '🦶',\n  '👂',\n  '👃',\n  '🧠',\n  '🦷',\n  '🦴',\n  '👀',\n  '👁',\n  '👅',\n  '👄',\n  '👶',\n  '🧒',\n  '👦',\n  '👧',\n  '🧑',\n  '👨',\n  '🧔',\n  '👱',\n  '👨‍🦰',\n  '👨‍🦱',\n  '👨‍🦳',\n  '👨‍🦲',\n  '👩',\n  '👱',\n  '👩‍🦰',\n  '👩‍🦱',\n  '👩‍🦳',\n  '👩‍🦲',\n  '🧓',\n  '👴',\n  '👵',\n  '🙍',\n  '🙍',\n  '🙎',\n  '🙎',\n  '🙅',\n  '🙅',\n  '🙆',\n  '🙆',\n  '💁',\n  '💁',\n  '🙋',\n  '🙋',\n  '🙇',\n  '🙇',\n  '🤦',\n  '🤦',\n  '🤷',\n  '🤷',\n  '👨‍⚕️',\n  '👩‍⚕️',\n  '👨‍🎓',\n  '👩‍🎓',\n  '👨‍🏫',\n  '👩‍🏫',\n  '👨‍⚖️',\n  '👩‍⚖️',\n  '👨‍🌾',\n  '👩‍🌾',\n  '👨‍🍳',\n  '👩‍🍳',\n  '👨‍🔧',\n  '👩‍🔧',\n  '👨‍🏭',\n  '👩‍🏭',\n  '👨‍💼',\n  '👩‍💼',\n  '👨‍🔬',\n  '👩‍🔬',\n  '👨‍💻',\n  '👩‍💻',\n  '👨‍🎤',\n  '👩‍🎤',\n  '👨‍🎨',\n  '👩‍🎨',\n  '👨‍✈️',\n  '👩‍✈️',\n  '👨‍🚀',\n  '👩‍🚀',\n  '👨‍🚒',\n  '👩‍🚒',\n  '👮',\n  '👮',\n  '🕵️',\n  '🕵️',\n  '💂',\n  '💂',\n  '👷',\n  '👷',\n  '🤴',\n  '👸',\n  '👳',\n  '👳',\n  '👲',\n  '🧕',\n  '🤵',\n  '👰',\n  '🤰',\n  '🤱',\n  '👼',\n  '🎅',\n  '🤶',\n  '🦸',\n  '🦸',\n  '🦹',\n  '🦹',\n  '🧙',\n  '🧙',\n  '🧚',\n  '🧚',\n  '🧛',\n  '🧛',\n  '🧜',\n  '🧜',\n  '🧝',\n  '🧝',\n  '🧞',\n  '🧞',\n  '🧟',\n  '🧟',\n  '💆',\n  '💆',\n  '💇',\n  '💇',\n  '🚶',\n  '🚶',\n  '🏃',\n  '🏃',\n  '💃',\n  '🕺',\n  '🕴',\n  '👯',\n  '👯',\n  '🧖',\n  '🧖',\n  '🧘',\n  '👭',\n  '👫',\n  '👬',\n  '💏',\n  '👨‍❤️‍💋‍👨',\n  '👩‍❤️‍💋‍👩',\n  '💑',\n  '👨‍❤️‍👨',\n  '👩‍❤️‍👩',\n  '👪',\n  '👨‍👩‍👦',\n  '👨‍👩‍👧',\n  '👨‍👩‍👧‍👦',\n  '👨‍👩‍👦‍👦',\n  '👨‍👩‍👧‍👧',\n  '👨‍👨‍👦',\n  '👨‍👨‍👧',\n  '👨‍👨‍👧‍👦',\n  '👨‍👨‍👦‍👦',\n  '👨‍👨‍👧‍👧',\n  '👩‍👩‍👦',\n  '👩‍👩‍👧',\n  '👩‍👩‍👧‍👦',\n  '👩‍👩‍👦‍👦',\n  '👩‍👩‍👧‍👧',\n  '👨‍👦',\n  '👨‍👦‍👦',\n  '👨‍👧',\n  '👨‍👧‍👦',\n  '👨‍👧‍👧',\n  '👩‍👦',\n  '👩‍👦‍👦',\n  '👩‍👧',\n  '👩‍👧‍👦',\n  '👩‍👧‍👧',\n  '🗣',\n  '👤',\n  '👥',\n  '👣',\n  '🧳',\n  '🌂',\n  '☂',\n  '🧵',\n  '🧶',\n  '👓',\n  '🕶',\n  '🥽',\n  '🥼',\n  '👔',\n  '👕',\n  '👖',\n  '🧣',\n  '🧤',\n  '🧥',\n  '🧦',\n  '👗',\n  '👘',\n  '👙',\n  '👚',\n  '👛',\n  '👜',\n  '👝',\n  '🎒',\n  '👞',\n  '👟',\n  '🥾',\n  '🥿',\n  '👠',\n  '👡',\n  '👢',\n  '👑',\n  '👒',\n  '🎩',\n  '🎓',\n  '🧢',\n  '⛑',\n  '💄',\n  '💍',\n  '💼',\n]);\n\n/// Map of all possible emojis along with their names in [Category.ANIMALS]\nfinal Map<String, String> animals = Map.fromIterables([\n  'Dog Face',\n  'Cat Face',\n  'Mouse Face',\n  'Hamster Face',\n  'Rabbit Face',\n  'Fox Face',\n  'Bear Face',\n  'Panda Face',\n  'Koala Face',\n  'Tiger Face',\n  'Lion Face',\n  'Cow Face',\n  'Pig Face',\n  'Pig Nose',\n  'Frog Face',\n  'Monkey Face',\n  'See-No-Evil Monkey',\n  'Hear-No-Evil Monkey',\n  'Speak-No-Evil Monkey',\n  'Monkey',\n  'Collision',\n  'Dizzy',\n  'Sweat Droplets',\n  'Dashing Away',\n  'Gorilla',\n  'Dog',\n  'Poodle',\n  'Wolf Face',\n  'Raccoon',\n  'Cat',\n  'Tiger',\n  'Leopard',\n  'Horse Face',\n  'Horse',\n  'Unicorn Face',\n  'Zebra',\n  'Ox',\n  'Water Buffalo',\n  'Cow',\n  'Pig',\n  'Boar',\n  'Ram',\n  'Ewe',\n  'Goat',\n  'Camel',\n  'Two-Hump Camel',\n  'Llama',\n  'Giraffe',\n  'Elephant',\n  'Rhinoceros',\n  'Hippopotamus',\n  'Mouse',\n  'Rat',\n  'Rabbit',\n  'Chipmunk',\n  'Hedgehog',\n  'Bat',\n  'Kangaroo',\n  'Badger',\n  'Paw Prints',\n  'Turkey',\n  'Chicken',\n  'Rooster',\n  'Hatching Chick',\n  'Baby Chick',\n  'Front-Facing Baby Chick',\n  'Bird',\n  'Penguin',\n  'Dove',\n  'Eagle',\n  'Duck',\n  'Swan',\n  'Owl',\n  'Peacock',\n  'Parrot',\n  'Crocodile',\n  'Turtle',\n  'Lizard',\n  'Snake',\n  'Dragon Face',\n  'Dragon',\n  'Sauropod',\n  'T-Rex',\n  'Spouting Whale',\n  'Whale',\n  'Dolphin',\n  'Fish',\n  'Tropical Fish',\n  'Blowfish',\n  'Shark',\n  'Octopus',\n  'Spiral Shell',\n  'Snail',\n  'Butterfly',\n  'Bug',\n  'Ant',\n  'Honeybee',\n  'Lady Beetle',\n  'Cricket',\n  'Spider',\n  'Spider Web',\n  'Scorpion',\n  'Mosquito',\n  'Microbe',\n  'Bouquet',\n  'Cherry Blossom',\n  'White Flower',\n  'Rosette',\n  'Rose',\n  'Wilted Flower',\n  'Hibiscus',\n  'Sunflower',\n  'Blossom',\n  'Tulip',\n  'Seedling',\n  'Evergreen Tree',\n  'Deciduous Tree',\n  'Palm Tree',\n  'Cactus',\n  'Sheaf of Rice',\n  'Herb',\n  'Shamrock',\n  'Four Leaf Clover',\n  'Maple Leaf',\n  'Fallen Leaf',\n  'Leaf Fluttering in Wind',\n  'Mushroom',\n  'Chestnut',\n  'Crab',\n  'Lobster',\n  'Shrimp',\n  'Squid',\n  'Globe Showing Europe-Africa',\n  'Globe Showing Americas',\n  'Globe Showing Asia-Australia',\n  'Globe With Meridians',\n  'New Moon',\n  'Waxing Crescent Moon',\n  'First Quarter Moon',\n  'Waxing Gibbous Moon',\n  'Full Moon',\n  'Waning Gibbous Moon',\n  'Last Quarter Moon',\n  'Waning Crescent Moon',\n  'Crescent Moon',\n  'New Moon Face',\n  'First Quarter Moon Face',\n  'Last Quarter Moon Face',\n  'Sun',\n  'Full Moon Face',\n  'Sun With Face',\n  'Star',\n  'Glowing Star',\n  'Shooting Star',\n  'Cloud',\n  'Sun Behind Cloud',\n  'Cloud With Lightning and Rain',\n  'Sun Behind Small Cloud',\n  'Sun Behind Large Cloud',\n  'Sun Behind Rain Cloud',\n  'Cloud With Rain',\n  'Cloud With Snow',\n  'Cloud With Lightning',\n  'Tornado',\n  'Fog',\n  'Wind Face',\n  'Rainbow',\n  'Umbrella',\n  'Umbrella With Rain Drops',\n  'High Voltage',\n  'Snowflake',\n  'Snowman Without Snow',\n  'Snowman',\n  'Comet',\n  'Fire',\n  'Droplet',\n  'Water Wave',\n  'Christmas Tree',\n  'Sparkles',\n  'Tanabata Tree',\n  'Pine Decoration',\n], [\n  '🐶',\n  '🐱',\n  '🐭',\n  '🐹',\n  '🐰',\n  '🦊',\n  '🐻',\n  '🐼',\n  '🐨',\n  '🐯',\n  '🦁',\n  '🐮',\n  '🐷',\n  '🐽',\n  '🐸',\n  '🐵',\n  '🙈',\n  '🙉',\n  '🙊',\n  '🐒',\n  '💥',\n  '💫',\n  '💦',\n  '💨',\n  '🦍',\n  '🐕',\n  '🐩',\n  '🐺',\n  '🦝',\n  '🐈',\n  '🐅',\n  '🐆',\n  '🐴',\n  '🐎',\n  '🦄',\n  '🦓',\n  '🐂',\n  '🐃',\n  '🐄',\n  '🐖',\n  '🐗',\n  '🐏',\n  '🐑',\n  '🐐',\n  '🐪',\n  '🐫',\n  '🦙',\n  '🦒',\n  '🐘',\n  '🦏',\n  '🦛',\n  '🐁',\n  '🐀',\n  '🐇',\n  '🐿',\n  '🦔',\n  '🦇',\n  '🦘',\n  '🦡',\n  '🐾',\n  '🦃',\n  '🐔',\n  '🐓',\n  '🐣',\n  '🐤',\n  '🐥',\n  '🐦',\n  '🐧',\n  '🕊',\n  '🦅',\n  '🦆',\n  '🦢',\n  '🦉',\n  '🦚',\n  '🦜',\n  '🐊',\n  '🐢',\n  '🦎',\n  '🐍',\n  '🐲',\n  '🐉',\n  '🦕',\n  '🦖',\n  '🐳',\n  '🐋',\n  '🐬',\n  '🐟',\n  '🐠',\n  '🐡',\n  '🦈',\n  '🐙',\n  '🐚',\n  '🐌',\n  '🦋',\n  '🐛',\n  '🐜',\n  '🐝',\n  '🐞',\n  '🦗',\n  '🕷',\n  '🕸',\n  '🦂',\n  '🦟',\n  '🦠',\n  '💐',\n  '🌸',\n  '💮',\n  '🏵',\n  '🌹',\n  '🥀',\n  '🌺',\n  '🌻',\n  '🌼',\n  '🌷',\n  '🌱',\n  '🌲',\n  '🌳',\n  '🌴',\n  '🌵',\n  '🌾',\n  '🌿',\n  '☘',\n  '🍀',\n  '🍁',\n  '🍂',\n  '🍃',\n  '🍄',\n  '🌰',\n  '🦀',\n  '🦞',\n  '🦐',\n  '🦑',\n  '🌍',\n  '🌎',\n  '🌏',\n  '🌐',\n  '🌑',\n  '🌒',\n  '🌓',\n  '🌔',\n  '🌕',\n  '🌖',\n  '🌗',\n  '🌘',\n  '🌙',\n  '🌚',\n  '🌛',\n  '🌜',\n  '☀',\n  '🌝',\n  '🌞',\n  '⭐',\n  '🌟',\n  '🌠',\n  '☁',\n  '⛅',\n  '⛈',\n  '🌤',\n  '🌥',\n  '🌦',\n  '🌧',\n  '🌨',\n  '🌩',\n  '🌪',\n  '🌫',\n  '🌬',\n  '🌈',\n  '☂',\n  '☔',\n  '⚡',\n  '❄',\n  '☃',\n  '⛄',\n  '☄',\n  '🔥',\n  '💧',\n  '🌊',\n  '🎄',\n  '✨',\n  '🎋',\n  '🎍',\n]);\n\n/// Map of all possible emojis along with their names in [Category.FOODS]\nfinal Map<String, String> foods = Map.fromIterables([\n  'Grapes',\n  'Melon',\n  'Watermelon',\n  'Tangerine',\n  'Lemon',\n  'Banana',\n  'Pineapple',\n  'Mango',\n  'Red Apple',\n  'Green Apple',\n  'Pear',\n  'Peach',\n  'Cherries',\n  'Strawberry',\n  'Kiwi Fruit',\n  'Tomato',\n  'Coconut',\n  'Avocado',\n  'Eggplant',\n  'Potato',\n  'Carrot',\n  'Ear of Corn',\n  'Hot Pepper',\n  'Cucumber',\n  'Leafy Green',\n  'Broccoli',\n  'Mushroom',\n  'Peanuts',\n  'Chestnut',\n  'Bread',\n  'Croissant',\n  'Baguette Bread',\n  'Pretzel',\n  'Bagel',\n  'Pancakes',\n  'Cheese Wedge',\n  'Meat on Bone',\n  'Poultry Leg',\n  'Cut of Meat',\n  'Bacon',\n  'Hamburger',\n  'French Fries',\n  'Pizza',\n  'Hot Dog',\n  'Sandwich',\n  'Taco',\n  'Burrito',\n  'Stuffed Flatbread',\n  'Cooking',\n  'Shallow Pan of Food',\n  'Pot of Food',\n  'Bowl With Spoon',\n  'Green Salad',\n  'Popcorn',\n  'Salt',\n  'Canned Food',\n  'Bento Box',\n  'Rice Cracker',\n  'Rice Ball',\n  'Cooked Rice',\n  'Curry Rice',\n  'Steaming Bowl',\n  'Spaghetti',\n  'Roasted Sweet Potato',\n  'Oden',\n  'Sushi',\n  'Fried Shrimp',\n  'Fish Cake With Swirl',\n  'Moon Cake',\n  'Dango',\n  'Dumpling',\n  'Fortune Cookie',\n  'Takeout Box',\n  'Soft Ice Cream',\n  'Shaved Ice',\n  'Ice Cream',\n  'Doughnut',\n  'Cookie',\n  'Birthday Cake',\n  'Shortcake',\n  'Cupcake',\n  'Pie',\n  'Chocolate Bar',\n  'Candy',\n  'Lollipop',\n  'Custard',\n  'Honey Pot',\n  'Baby Bottle',\n  'Glass of Milk',\n  'Hot Beverage',\n  'Teacup Without Handle',\n  'Sake',\n  'Bottle With Popping Cork',\n  'Wine Glass',\n  'Cocktail Glass',\n  'Tropical Drink',\n  'Beer Mug',\n  'Clinking Beer Mugs',\n  'Clinking Glasses',\n  'Tumbler Glass',\n  'Cup With Straw',\n  'Chopsticks',\n  'Fork and Knife With Plate',\n  'Fork and Knife',\n  'Spoon',\n], [\n  '🍇',\n  '🍈',\n  '🍉',\n  '🍊',\n  '🍋',\n  '🍌',\n  '🍍',\n  '🥭',\n  '🍎',\n  '🍏',\n  '🍐',\n  '🍑',\n  '🍒',\n  '🍓',\n  '🥝',\n  '🍅',\n  '🥥',\n  '🥑',\n  '🍆',\n  '🥔',\n  '🥕',\n  '🌽',\n  '🌶',\n  '🥒',\n  '🥬',\n  '🥦',\n  '🍄',\n  '🥜',\n  '🌰',\n  '🍞',\n  '🥐',\n  '🥖',\n  '🥨',\n  '🥯',\n  '🥞',\n  '🧀',\n  '🍖',\n  '🍗',\n  '🥩',\n  '🥓',\n  '🍔',\n  '🍟',\n  '🍕',\n  '🌭',\n  '🥪',\n  '🌮',\n  '🌯',\n  '🥙',\n  '🍳',\n  '🥘',\n  '🍲',\n  '🥣',\n  '🥗',\n  '🍿',\n  '🧂',\n  '🥫',\n  '🍱',\n  '🍘',\n  '🍙',\n  '🍚',\n  '🍛',\n  '🍜',\n  '🍝',\n  '🍠',\n  '🍢',\n  '🍣',\n  '🍤',\n  '🍥',\n  '🥮',\n  '🍡',\n  '🥟',\n  '🥠',\n  '🥡',\n  '🍦',\n  '🍧',\n  '🍨',\n  '🍩',\n  '🍪',\n  '🎂',\n  '🍰',\n  '🧁',\n  '🥧',\n  '🍫',\n  '🍬',\n  '🍭',\n  '🍮',\n  '🍯',\n  '🍼',\n  '🥛',\n  '☕',\n  '🍵',\n  '🍶',\n  '🍾',\n  '🍷',\n  '🍸',\n  '🍹',\n  '🍺',\n  '🍻',\n  '🥂',\n  '🥃',\n  '🥤',\n  '🥢',\n  '🍽',\n  '🍴',\n  '🥄',\n]);\n\n/// Map of all possible emojis along with their names in [Category.TRAVEL]\nfinal Map<String, String> travel = Map.fromIterables([\n  'Person Rowing Boat',\n  'Map of Japan',\n  'Snow-Capped Mountain',\n  'Mountain',\n  'Volcano',\n  'Mount Fuji',\n  'Camping',\n  'Beach With Umbrella',\n  'Desert',\n  'Desert Island',\n  'National Park',\n  'Stadium',\n  'Classical Building',\n  'Building Construction',\n  'Houses',\n  'Derelict House',\n  'House',\n  'House With Garden',\n  'Office Building',\n  'Japanese Post Office',\n  'Post Office',\n  'Hospital',\n  'Bank',\n  'Hotel',\n  'Love Hotel',\n  'Convenience Store',\n  'School',\n  'Department Store',\n  'Factory',\n  'Japanese Castle',\n  'Castle',\n  'Wedding',\n  'Tokyo Tower',\n  'Statue of Liberty',\n  'Church',\n  'Mosque',\n  'Synagogue',\n  'Shinto Shrine',\n  'Kaaba',\n  'Fountain',\n  'Tent',\n  'Foggy',\n  'Night With Stars',\n  'Cityscape',\n  'Sunrise Over Mountains',\n  'Sunrise',\n  'Cityscape at Dusk',\n  'Sunset',\n  'Bridge at Night',\n  'Carousel Horse',\n  'Ferris Wheel',\n  'Roller Coaster',\n  'Locomotive',\n  'Railway Car',\n  'High-Speed Train',\n  'Bullet Train',\n  'Train',\n  'Metro',\n  'Light Rail',\n  'Station',\n  'Tram',\n  'Monorail',\n  'Mountain Railway',\n  'Tram Car',\n  'Bus',\n  'Oncoming Bus',\n  'Trolleybus',\n  'Minibus',\n  'Ambulance',\n  'Fire Engine',\n  'Police Car',\n  'Oncoming Police Car',\n  'Taxi',\n  'Oncoming Taxi',\n  'Automobile',\n  'Oncoming Automobile',\n  'Delivery Truck',\n  'Articulated Lorry',\n  'Tractor',\n  'Racing Car',\n  'Motorcycle',\n  'Motor Scooter',\n  'Bicycle',\n  'Kick Scooter',\n  'Bus Stop',\n  'Railway Track',\n  'Fuel Pump',\n  'Police Car Light',\n  'Horizontal Traffic Light',\n  'Vertical Traffic Light',\n  'Construction',\n  'Anchor',\n  'Sailboat',\n  'Speedboat',\n  'Passenger Ship',\n  'Ferry',\n  'Motor Boat',\n  'Ship',\n  'Airplane',\n  'Small Airplane',\n  'Airplane Departure',\n  'Airplane Arrival',\n  'Seat',\n  'Helicopter',\n  'Suspension Railway',\n  'Mountain Cableway',\n  'Aerial Tramway',\n  'Satellite',\n  'Rocket',\n  'Flying Saucer',\n  'Shooting Star',\n  'Milky Way',\n  'Umbrella on Ground',\n  'Fireworks',\n  'Sparkler',\n  'Moon Viewing Ceremony',\n  'Yen Banknote',\n  'Dollar Banknote',\n  'Euro Banknote',\n  'Pound Banknote',\n  'Moai',\n  'Passport Control',\n  'Customs',\n  'Baggage Claim',\n  'Left Luggage',\n], [\n  '🚣',\n  '🗾',\n  '🏔',\n  '⛰',\n  '🌋',\n  '🗻',\n  '🏕',\n  '🏖',\n  '🏜',\n  '🏝',\n  '🏞',\n  '🏟',\n  '🏛',\n  '🏗',\n  '🏘',\n  '🏚',\n  '🏠',\n  '🏡',\n  '🏢',\n  '🏣',\n  '🏤',\n  '🏥',\n  '🏦',\n  '🏨',\n  '🏩',\n  '🏪',\n  '🏫',\n  '🏬',\n  '🏭',\n  '🏯',\n  '🏰',\n  '💒',\n  '🗼',\n  '🗽',\n  '⛪',\n  '🕌',\n  '🕍',\n  '⛩',\n  '🕋',\n  '⛲',\n  '⛺',\n  '🌁',\n  '🌃',\n  '🏙',\n  '🌄',\n  '🌅',\n  '🌆',\n  '🌇',\n  '🌉',\n  '🎠',\n  '🎡',\n  '🎢',\n  '🚂',\n  '🚃',\n  '🚄',\n  '🚅',\n  '🚆',\n  '🚇',\n  '🚈',\n  '🚉',\n  '🚊',\n  '🚝',\n  '🚞',\n  '🚋',\n  '🚌',\n  '🚍',\n  '🚎',\n  '🚐',\n  '🚑',\n  '🚒',\n  '🚓',\n  '🚔',\n  '🚕',\n  '🚖',\n  '🚗',\n  '🚘',\n  '🚚',\n  '🚛',\n  '🚜',\n  '🏎',\n  '🏍',\n  '🛵',\n  '🚲',\n  '🛴',\n  '🚏',\n  '🛤',\n  '⛽',\n  '🚨',\n  '🚥',\n  '🚦',\n  '🚧',\n  '⚓',\n  '⛵',\n  '🚤',\n  '🛳',\n  '⛴',\n  '🛥',\n  '🚢',\n  '✈',\n  '🛩',\n  '🛫',\n  '🛬',\n  '💺',\n  '🚁',\n  '🚟',\n  '🚠',\n  '🚡',\n  '🛰',\n  '🚀',\n  '🛸',\n  '🌠',\n  '🌌',\n  '⛱',\n  '🎆',\n  '🎇',\n  '🎑',\n  '💴',\n  '💵',\n  '💶',\n  '💷',\n  '🗿',\n  '🛂',\n  '🛃',\n  '🛄',\n  '🛅',\n]);\n\n/// Map of all possible emojis along with their names in [Category.ACTIVITIES]\nfinal Map<String, String> activities = Map.fromIterables([\n  'Man in Suit Levitating',\n  'Man Climbing',\n  'Woman Climbing',\n  'Horse Racing',\n  'Skier',\n  'Snowboarder',\n  'Man Golfing',\n  'Woman Golfing',\n  'Man Surfing',\n  'Woman Surfing',\n  'Man Rowing Boat',\n  'Woman Rowing Boat',\n  'Man Swimming',\n  'Woman Swimming',\n  'Man Bouncing Ball',\n  'Woman Bouncing Ball',\n  'Man Lifting Weights',\n  'Woman Lifting Weights',\n  'Man Biking',\n  'Woman Biking',\n  'Man Mountain Biking',\n  'Woman Mountain Biking',\n  'Man Cartwheeling',\n  'Woman Cartwheeling',\n  'Men Wrestling',\n  'Women Wrestling',\n  'Man Playing Water Polo',\n  'Woman Playing Water Polo',\n  'Man Playing Handball',\n  'Woman Playing Handball',\n  'Man Juggling',\n  'Woman Juggling',\n  'Man in Lotus Position',\n  'Woman in Lotus Position',\n  'Circus Tent',\n  'Skateboard',\n  'Reminder Ribbon',\n  'Admission Tickets',\n  'Ticket',\n  'Military Medal',\n  'Trophy',\n  'Sports Medal',\n  '1st Place Medal',\n  '2nd Place Medal',\n  '3rd Place Medal',\n  'Soccer Ball',\n  'Baseball',\n  'Softball',\n  'Basketball',\n  'Volleyball',\n  'American Football',\n  'Rugby Football',\n  'Tennis',\n  'Flying Disc',\n  'Bowling',\n  'Cricket Game',\n  'FieldPB Hockey',\n  'Ice Hockey',\n  'Lacrosse',\n  'Ping Pong',\n  'Badminton',\n  'Boxing Glove',\n  'Martial Arts Uniform',\n  'Flag in Hole',\n  'Ice Skate',\n  'Fishing Pole',\n  'Running Shirt',\n  'Skis',\n  'Sled',\n  'Curling Stone',\n  'Direct Hit',\n  'Pool 8 Ball',\n  'Video Game',\n  'Slot Machine',\n  'Game Die',\n  'Jigsaw',\n  'Chess Pawn',\n  'Performing Arts',\n  'Artist Palette',\n  'Thread',\n  'Yarn',\n  'Musical Score',\n  'Microphone',\n  'Headphone',\n  'Saxophone',\n  'Guitar',\n  'Musical Keyboard',\n  'Trumpet',\n  'Violin',\n  'Drum',\n  'Clapper Board',\n  'Bow and Arrow',\n], [\n  '🕴',\n  '🧗',\n  '🧗',\n  '🏇',\n  '⛷',\n  '🏂',\n  '🏌️',\n  '🏌️',\n  '🏄',\n  '🏄',\n  '🚣',\n  '🚣',\n  '🏊',\n  '🏊',\n  '⛹️',\n  '⛹️',\n  '🏋️',\n  '🏋️',\n  '🚴',\n  '🚴',\n  '🚵',\n  '🚵',\n  '🤸',\n  '🤸',\n  '🤼',\n  '🤼',\n  '🤽',\n  '🤽',\n  '🤾',\n  '🤾',\n  '🤹',\n  '🤹',\n  '🧘🏻‍♂️',\n  '🧘🏻‍♀️',\n  '🎪',\n  '🛹',\n  '🎗',\n  '🎟',\n  '🎫',\n  '🎖',\n  '🏆',\n  '🏅',\n  '🥇',\n  '🥈',\n  '🥉',\n  '⚽',\n  '⚾',\n  '🥎',\n  '🏀',\n  '🏐',\n  '🏈',\n  '🏉',\n  '🎾',\n  '🥏',\n  '🎳',\n  '🏏',\n  '🏑',\n  '🏒',\n  '🥍',\n  '🏓',\n  '🏸',\n  '🥊',\n  '🥋',\n  '⛳',\n  '⛸',\n  '🎣',\n  '🎽',\n  '🎿',\n  '🛷',\n  '🥌',\n  '🎯',\n  '🎱',\n  '🎮',\n  '🎰',\n  '🎲',\n  '🧩',\n  '♟',\n  '🎭',\n  '🎨',\n  '🧵',\n  '🧶',\n  '🎼',\n  '🎤',\n  '🎧',\n  '🎷',\n  '🎸',\n  '🎹',\n  '🎺',\n  '🎻',\n  '🥁',\n  '🎬',\n  '🏹',\n]);\n\n/// Map of all possible emojis along with their names in [Category.OBJECTS]\nfinal Map<String, String> objects = Map.fromIterables([\n  'Love Letter',\n  'Hole',\n  'Bomb',\n  'Person Taking Bath',\n  'Person in Bed',\n  'Kitchen Knife',\n  'Amphora',\n  'World Map',\n  'Compass',\n  'Brick',\n  'Barber Pole',\n  'Oil Drum',\n  'Bellhop Bell',\n  'Luggage',\n  'Hourglass Done',\n  'Hourglass Not Done',\n  'Watch',\n  'Alarm Clock',\n  'Stopwatch',\n  'Timer Clock',\n  'Mantelpiece Clock',\n  'Thermometer',\n  'Umbrella on Ground',\n  'Firecracker',\n  'Balloon',\n  'Party Popper',\n  'Confetti Ball',\n  'Japanese Dolls',\n  'Carp Streamer',\n  'Wind Chime',\n  'Red Envelope',\n  'Ribbon',\n  'Wrapped Gift',\n  'Crystal Ball',\n  'Nazar Amulet',\n  'Joystick',\n  'Teddy Bear',\n  'Framed Picture',\n  'Thread',\n  'Yarn',\n  'Shopping Bags',\n  'Prayer Beads',\n  'Gem Stone',\n  'Postal Horn',\n  'Studio Microphone',\n  'Level Slider',\n  'Control Knobs',\n  'Radio',\n  'Mobile Phone',\n  'Mobile Phone With Arrow',\n  'Telephone',\n  'Telephone Receiver',\n  'Pager',\n  'Fax Machine',\n  'Battery',\n  'Electric Plug',\n  'Laptop Computer',\n  'Desktop Computer',\n  'Printer',\n  'Keyboard',\n  'Computer Mouse',\n  'Trackball',\n  'Computer Disk',\n  'Floppy Disk',\n  'Optical Disk',\n  'DVD',\n  'Abacus',\n  'Movie Camera',\n  'Film Frames',\n  'Film Projector',\n  'Television',\n  'Camera',\n  'Camera With Flash',\n  'Video Camera',\n  'Videocassette',\n  'Magnifying Glass Tilted Left',\n  'Magnifying Glass Tilted Right',\n  'Candle',\n  'Light Bulb',\n  'Flashlight',\n  'Red Paper Lantern',\n  'Notebook With Decorative Cover',\n  'Closed Book',\n  'Open Book',\n  'Green Book',\n  'Blue Book',\n  'Orange Book',\n  'Books',\n  'Notebook',\n  'Page With Curl',\n  'Scroll',\n  'Page Facing Up',\n  'Newspaper',\n  'Rolled-Up Newspaper',\n  'Bookmark Tabs',\n  'Bookmark',\n  'Label',\n  'Money Bag',\n  'Yen Banknote',\n  'Dollar Banknote',\n  'Euro Banknote',\n  'Pound Banknote',\n  'Money With Wings',\n  'Credit Card',\n  'Receipt',\n  'Envelope',\n  'E-Mail',\n  'Incoming Envelope',\n  'Envelope With Arrow',\n  'Outbox Tray',\n  'Inbox Tray',\n  'Package',\n  'Closed Mailbox With Raised Flag',\n  'Closed Mailbox With Lowered Flag',\n  'Open Mailbox With Raised Flag',\n  'Open Mailbox With Lowered Flag',\n  'Postbox',\n  'Ballot Box With Ballot',\n  'Pencil',\n  'Black Nib',\n  'Fountain Pen',\n  'Pen',\n  'Paintbrush',\n  'Crayon',\n  'Memo',\n  'File Folder',\n  'Open File Folder',\n  'Card Index Dividers',\n  'Calendar',\n  'Tear-Off Calendar',\n  'Spiral Notepad',\n  'Spiral Calendar',\n  'Card Index',\n  'Chart Increasing',\n  'Chart Decreasing',\n  'Bar Chart',\n  'Clipboard',\n  'Pushpin',\n  'Round Pushpin',\n  'Paperclip',\n  'Linked Paperclips',\n  'Straight Ruler',\n  'Triangular Ruler',\n  'Scissors',\n  'Card File Box',\n  'File Cabinet',\n  'Wastebasket',\n  'Locked',\n  'Unlocked',\n  'Locked With Pen',\n  'Locked With Key',\n  'Key',\n  'Old Key',\n  'Hammer',\n  'Pick',\n  'Hammer and Pick',\n  'Hammer and Wrench',\n  'Dagger',\n  'Crossed Swords',\n  'Pistol',\n  'Shield',\n  'Wrench',\n  'Nut and Bolt',\n  'Gear',\n  'Clamp',\n  'Balance Scale',\n  'Link',\n  'Chains',\n  'Toolbox',\n  'Magnet',\n  'Alembic',\n  'Test Tube',\n  'Petri Dish',\n  'DNA',\n  'Microscope',\n  'Telescope',\n  'Satellite Antenna',\n  'Syringe',\n  'Pill',\n  'Door',\n  'Bed',\n  'Couch and Lamp',\n  'Toilet',\n  'Shower',\n  'Bathtub',\n  'Lotion Bottle',\n  'Safety Pin',\n  'Broom',\n  'Basket',\n  'Roll of Paper',\n  'Soap',\n  'Sponge',\n  'Fire Extinguisher',\n  'Cigarette',\n  'Coffin',\n  'Funeral Urn',\n  'Moai',\n  'Potable Water',\n], [\n  '💌',\n  '🕳',\n  '💣',\n  '🛀',\n  '🛌',\n  '🔪',\n  '🏺',\n  '🗺',\n  '🧭',\n  '🧱',\n  '💈',\n  '🛢',\n  '🛎',\n  '🧳',\n  '⌛',\n  '⏳',\n  '⌚',\n  '⏰',\n  '⏱',\n  '⏲',\n  '🕰',\n  '🌡',\n  '⛱',\n  '🧨',\n  '🎈',\n  '🎉',\n  '🎊',\n  '🎎',\n  '🎏',\n  '🎐',\n  '🧧',\n  '🎀',\n  '🎁',\n  '🔮',\n  '🧿',\n  '🕹',\n  '🧸',\n  '🖼',\n  '🧵',\n  '🧶',\n  '🛍',\n  '📿',\n  '💎',\n  '📯',\n  '🎙',\n  '🎚',\n  '🎛',\n  '📻',\n  '📱',\n  '📲',\n  '☎',\n  '📞',\n  '📟',\n  '📠',\n  '🔋',\n  '🔌',\n  '💻',\n  '🖥',\n  '🖨',\n  '⌨',\n  '🖱',\n  '🖲',\n  '💽',\n  '💾',\n  '💿',\n  '📀',\n  '🧮',\n  '🎥',\n  '🎞',\n  '📽',\n  '📺',\n  '📷',\n  '📸',\n  '📹',\n  '📼',\n  '🔍',\n  '🔎',\n  '🕯',\n  '💡',\n  '🔦',\n  '🏮',\n  '📔',\n  '📕',\n  '📖',\n  '📗',\n  '📘',\n  '📙',\n  '📚',\n  '📓',\n  '📃',\n  '📜',\n  '📄',\n  '📰',\n  '🗞',\n  '📑',\n  '🔖',\n  '🏷',\n  '💰',\n  '💴',\n  '💵',\n  '💶',\n  '💷',\n  '💸',\n  '💳',\n  '🧾',\n  '✉',\n  '📧',\n  '📨',\n  '📩',\n  '📤',\n  '📥',\n  '📦',\n  '📫',\n  '📪',\n  '📬',\n  '📭',\n  '📮',\n  '🗳',\n  '✏',\n  '✒',\n  '🖋',\n  '🖊',\n  '🖌',\n  '🖍',\n  '📝',\n  '📁',\n  '📂',\n  '🗂',\n  '📅',\n  '📆',\n  '🗒',\n  '🗓',\n  '📇',\n  '📈',\n  '📉',\n  '📊',\n  '📋',\n  '📌',\n  '📍',\n  '📎',\n  '🖇',\n  '📏',\n  '📐',\n  '✂',\n  '🗃',\n  '🗄',\n  '🗑',\n  '🔒',\n  '🔓',\n  '🔏',\n  '🔐',\n  '🔑',\n  '🗝',\n  '🔨',\n  '⛏',\n  '⚒',\n  '🛠',\n  '🗡',\n  '⚔',\n  '🔫',\n  '🛡',\n  '🔧',\n  '🔩',\n  '⚙',\n  '🗜',\n  '⚖',\n  '🔗',\n  '⛓',\n  '🧰',\n  '🧲',\n  '⚗',\n  '🧪',\n  '🧫',\n  '🧬',\n  '🔬',\n  '🔭',\n  '📡',\n  '💉',\n  '💊',\n  '🚪',\n  '🛏',\n  '🛋',\n  '🚽',\n  '🚿',\n  '🛁',\n  '🧴',\n  '🧷',\n  '🧹',\n  '🧺',\n  '🧻',\n  '🧼',\n  '🧽',\n  '🧯',\n  '🚬',\n  '⚰',\n  '⚱',\n  '🗿',\n  '🚰',\n]);\n\n/// Map of all possible emojis along with their names in [Category.SYMBOLS]\nfinal Map<String, String> symbols = Map.fromIterables([\n  'Heart With Arrow',\n  'Heart With Ribbon',\n  'Sparkling Heart',\n  'Growing Heart',\n  'Beating Heart',\n  'Revolving Hearts',\n  'Two Hearts',\n  'Heart Decoration',\n  'Heavy Heart Exclamation',\n  'Broken Heart',\n  'Red Heart',\n  'Orange Heart',\n  'Yellow Heart',\n  'Green Heart',\n  'Blue Heart',\n  'Purple Heart',\n  'Black Heart',\n  'Hundred Points',\n  'Anger Symbol',\n  'Speech Balloon',\n  'Eye in Speech Bubble',\n  'Right Anger Bubble',\n  'Thought Balloon',\n  'Zzz',\n  'White Flower',\n  'Hot Springs',\n  'Barber Pole',\n  'Stop Sign',\n  'Twelve O’Clock',\n  'Twelve-Thirty',\n  'One O’Clock',\n  'One-Thirty',\n  'Two O’Clock',\n  'Two-Thirty',\n  'Three O’Clock',\n  'Three-Thirty',\n  'Four O’Clock',\n  'Four-Thirty',\n  'Five O’Clock',\n  'Five-Thirty',\n  'Six O’Clock',\n  'Six-Thirty',\n  'Seven O’Clock',\n  'Seven-Thirty',\n  'Eight O’Clock',\n  'Eight-Thirty',\n  'Nine O’Clock',\n  'Nine-Thirty',\n  'Ten O’Clock',\n  'Ten-Thirty',\n  'Eleven O’Clock',\n  'Eleven-Thirty',\n  'Cyclone',\n  'Spade Suit',\n  'Heart Suit',\n  'Diamond Suit',\n  'Club Suit',\n  'Joker',\n  'Mahjong Red Dragon',\n  'Flower Playing Cards',\n  'Muted Speaker',\n  'Speaker Low Volume',\n  'Speaker Medium Volume',\n  'Speaker High Volume',\n  'Loudspeaker',\n  'Megaphone',\n  'Postal Horn',\n  'Bell',\n  'Bell With Slash',\n  'Musical Note',\n  'Musical Notes',\n  'ATM Sign',\n  'Litter in Bin Sign',\n  'Potable Water',\n  'Wheelchair Symbol',\n  'Men’s Room',\n  'Women’s Room',\n  'Restroom',\n  'Baby Symbol',\n  'Water Closet',\n  'Warning',\n  'Children Crossing',\n  'No Entry',\n  'Prohibited',\n  'No Bicycles',\n  'No Smoking',\n  'No Littering',\n  'Non-Potable Water',\n  'No Pedestrians',\n  'No One Under Eighteen',\n  'Radioactive',\n  'Biohazard',\n  'Up Arrow',\n  'Up-Right Arrow',\n  'Right Arrow',\n  'Down-Right Arrow',\n  'Down Arrow',\n  'Down-Left Arrow',\n  'Left Arrow',\n  'Up-Left Arrow',\n  'Up-Down Arrow',\n  'Left-Right Arrow',\n  'Right Arrow Curving Left',\n  'Left Arrow Curving Right',\n  'Right Arrow Curving Up',\n  'Right Arrow Curving Down',\n  'Clockwise Vertical Arrows',\n  'Counterclockwise Arrows Button',\n  'Back Arrow',\n  'End Arrow',\n  'On! Arrow',\n  'Soon Arrow',\n  'Top Arrow',\n  'Place of Worship',\n  'Atom Symbol',\n  'Om',\n  'Star of David',\n  'Wheel of Dharma',\n  'Yin Yang',\n  'Latin Cross',\n  'Orthodox Cross',\n  'Star and Crescent',\n  'Peace Symbol',\n  'Menorah',\n  'Dotted Six-Pointed Star',\n  'Aries',\n  'Taurus',\n  'Gemini',\n  'Cancer',\n  'Leo',\n  'Virgo',\n  'Libra',\n  'Scorpio',\n  'Sagittarius',\n  'Capricorn',\n  'Aquarius',\n  'Pisces',\n  'Ophiuchus',\n  'Shuffle Tracks Button',\n  'Repeat Button',\n  'Repeat Single Button',\n  'Play Button',\n  'Fast-Forward Button',\n  'Reverse Button',\n  'Fast Reverse Button',\n  'Upwards Button',\n  'Fast Up Button',\n  'Downwards Button',\n  'Fast Down Button',\n  'Stop Button',\n  'Eject Button',\n  'Cinema',\n  'Dim Button',\n  'Bright Button',\n  'Antenna Bars',\n  'Vibration Mode',\n  'Mobile Phone Off',\n  'Infinity',\n  'Recycling Symbol',\n  'Trident Emblem',\n  'Name Badge',\n  'Japanese Symbol for Beginner',\n  'Heavy Large Circle',\n  'White Heavy Check Mark',\n  'Ballot Box With Check',\n  'Heavy Check Mark',\n  'Heavy Multiplication X',\n  'Cross Mark',\n  'Cross Mark Button',\n  'Heavy Plus Sign',\n  'Heavy Minus Sign',\n  'Heavy Division Sign',\n  'Curly Loop',\n  'Double Curly Loop',\n  'Part Alternation Mark',\n  'Eight-Spoked Asterisk',\n  'Eight-Pointed Star',\n  'Sparkle',\n  'Double Exclamation Mark',\n  'Exclamation Question Mark',\n  'Question Mark',\n  'White Question Mark',\n  'White Exclamation Mark',\n  'Exclamation Mark',\n  'Copyright',\n  'Registered',\n  'Trade Mark',\n  'Keycap Number Sign',\n  'Keycap Digit Zero',\n  'Keycap Digit One',\n  'Keycap Digit Two',\n  'Keycap Digit Three',\n  'Keycap Digit Four',\n  'Keycap Digit Five',\n  'Keycap Digit Six',\n  'Keycap Digit Seven',\n  'Keycap Digit Eight',\n  'Keycap Digit Nine',\n  'Keycap: 10',\n  'Input Latin Uppercase',\n  'Input Latin Lowercase',\n  'Input Numbers',\n  'Input Symbols',\n  'Input Latin Letters',\n  'A Button (Blood Type)',\n  'AB Button (Blood Type)',\n  'B Button (Blood Type)',\n  'CL Button',\n  'Cool Button',\n  'Free Button',\n  'Information',\n  'ID Button',\n  'Circled M',\n  'New Button',\n  'NG Button',\n  'O Button (Blood Type)',\n  'OK Button',\n  'P Button',\n  'SOS Button',\n  'Up! Button',\n  'Vs Button',\n  'Japanese “Here” Button',\n  'Japanese “Service Charge” Button',\n  'Japanese “Monthly Amount” Button',\n  'Japanese “Not Free of Charge” Button',\n  'Japanese “Reserved” Button',\n  'Japanese “Bargain” Button',\n  'Japanese “Discount” Button',\n  'Japanese “Free of Charge” Button',\n  'Japanese “Prohibited” Button',\n  'Japanese “Acceptable” Button',\n  'Japanese “Application” Button',\n  'Japanese “Passing Grade” Button',\n  'Japanese “Vacancy” Button',\n  'Japanese “Congratulations” Button',\n  'Japanese “Secret” Button',\n  'Japanese “Open for Business” Button',\n  'Japanese “No Vacancy” Button',\n  'Red Circle',\n  'Blue Circle',\n  'Black Circle',\n  'White Circle',\n  'Black Large Square',\n  'White Large Square',\n  'Black Medium Square',\n  'White Medium Square',\n  'Black Medium-Small Square',\n  'White Medium-Small Square',\n  'Black Small Square',\n  'White Small Square',\n  'Large Orange Diamond',\n  'Large Blue Diamond',\n  'Small Orange Diamond',\n  'Small Blue Diamond',\n  'Red Triangle Pointed Up',\n  'Red Triangle Pointed Down',\n  'Diamond With a Dot',\n  'White Square Button',\n  'Black Square Button',\n], [\n  '💘',\n  '💝',\n  '💖',\n  '💗',\n  '💓',\n  '💞',\n  '💕',\n  '💟',\n  '❣',\n  '💔',\n  '❤',\n  '🧡',\n  '💛',\n  '💚',\n  '💙',\n  '💜',\n  '🖤',\n  '💯',\n  '💢',\n  '💬',\n  '👁️‍🗨️',\n  '🗯',\n  '💭',\n  '💤',\n  '💮',\n  '♨',\n  '💈',\n  '🛑',\n  '🕛',\n  '🕧',\n  '🕐',\n  '🕜',\n  '🕑',\n  '🕝',\n  '🕒',\n  '🕞',\n  '🕓',\n  '🕟',\n  '🕔',\n  '🕠',\n  '🕕',\n  '🕡',\n  '🕖',\n  '🕢',\n  '🕗',\n  '🕣',\n  '🕘',\n  '🕤',\n  '🕙',\n  '🕥',\n  '🕚',\n  '🕦',\n  '🌀',\n  '♠',\n  '♥',\n  '♦',\n  '♣',\n  '🃏',\n  '🀄',\n  '🎴',\n  '🔇',\n  '🔈',\n  '🔉',\n  '🔊',\n  '📢',\n  '📣',\n  '📯',\n  '🔔',\n  '🔕',\n  '🎵',\n  '🎶',\n  '🏧',\n  '🚮',\n  '🚰',\n  '♿',\n  '🚹',\n  '🚺',\n  '🚻',\n  '🚼',\n  '🚾',\n  '⚠',\n  '🚸',\n  '⛔',\n  '🚫',\n  '🚳',\n  '🚭',\n  '🚯',\n  '🚱',\n  '🚷',\n  '🔞',\n  '☢',\n  '☣',\n  '⬆',\n  '↗',\n  '➡',\n  '↘',\n  '⬇',\n  '↙',\n  '⬅',\n  '↖',\n  '↕',\n  '↔',\n  '↩',\n  '↪',\n  '⤴',\n  '⤵',\n  '🔃',\n  '🔄',\n  '🔙',\n  '🔚',\n  '🔛',\n  '🔜',\n  '🔝',\n  '🛐',\n  '⚛',\n  '🕉',\n  '✡',\n  '☸',\n  '☯',\n  '✝',\n  '☦',\n  '☪',\n  '☮',\n  '🕎',\n  '🔯',\n  '♈',\n  '♉',\n  '♊',\n  '♋',\n  '♌',\n  '♍',\n  '♎',\n  '♏',\n  '♐',\n  '♑',\n  '♒',\n  '♓',\n  '⛎',\n  '🔀',\n  '🔁',\n  '🔂',\n  '▶',\n  '⏩',\n  '◀',\n  '⏪',\n  '🔼',\n  '⏫',\n  '🔽',\n  '⏬',\n  '⏹',\n  '⏏',\n  '🎦',\n  '🔅',\n  '🔆',\n  '📶',\n  '📳',\n  '📴',\n  '♾',\n  '♻',\n  '🔱',\n  '📛',\n  '🔰',\n  '⭕',\n  '✅',\n  '☑',\n  '✔',\n  '✖',\n  '❌',\n  '❎',\n  '➕',\n  '➖',\n  '➗',\n  '➰',\n  '➿',\n  '〽',\n  '✳',\n  '✴',\n  '❇',\n  '‼',\n  '⁉',\n  '❓',\n  '❔',\n  '❕',\n  '❗',\n  '©',\n  '®',\n  '™',\n  '#️⃣',\n  '0️⃣',\n  '1️⃣',\n  '2️⃣',\n  '3️⃣',\n  '4️⃣',\n  '5️⃣',\n  '6️⃣',\n  '7️⃣',\n  '8️⃣',\n  '9️⃣',\n  '🔟',\n  '🔠',\n  '🔡',\n  '🔢',\n  '🔣',\n  '🔤',\n  '🅰',\n  '🆎',\n  '🅱',\n  '🆑',\n  '🆒',\n  '🆓',\n  'ℹ',\n  '🆔',\n  'Ⓜ',\n  '🆕',\n  '🆖',\n  '🅾',\n  '🆗',\n  '🅿',\n  '🆘',\n  '🆙',\n  '🆚',\n  '🈁',\n  '🈂',\n  '🈷',\n  '🈶',\n  '🈯',\n  '🉐',\n  '🈹',\n  '🈚',\n  '🈲',\n  '🉑',\n  '🈸',\n  '🈴',\n  '🈳',\n  '㊗',\n  '㊙',\n  '🈺',\n  '🈵',\n  '🔴',\n  '🔵',\n  '⚫',\n  '⚪',\n  '⬛',\n  '⬜',\n  '◼',\n  '◻',\n  '◾',\n  '◽',\n  '▪',\n  '▫',\n  '🔶',\n  '🔷',\n  '🔸',\n  '🔹',\n  '🔺',\n  '🔻',\n  '💠',\n  '🔳',\n  '🔲',\n]);\n\n/// Map of all possible emojis along with their names in [Category.FLAGS]\nfinal Map<String, String> flags = Map.fromIterables([\n  'Chequered Flag',\n  'Triangular Flag',\n  'Crossed Flags',\n  'Black Flag',\n  'White Flag',\n  'Rainbow Flag',\n  'Pirate Flag',\n  'Flag: Ascension Island',\n  'Flag: Andorra',\n  'Flag: United Arab Emirates',\n  'Flag: Afghanistan',\n  'Flag: Antigua &amp; Barbuda',\n  'Flag: Anguilla',\n  'Flag: Albania',\n  'Flag: Armenia',\n  'Flag: Angola',\n  'Flag: Antarctica',\n  'Flag: argentina',\n  'Flag: American Samoa',\n  'Flag: Austria',\n  'Flag: Australia',\n  'Flag: Aruba',\n  'Flag: Åland Islands',\n  'Flag: Azerbaijan',\n  'Flag: Bosnia &amp; Herzegovina',\n  'Flag: Barbados',\n  'Flag: Bangladesh',\n  'Flag: Belgium',\n  'Flag: Burkina Faso',\n  'Flag: Bulgaria',\n  'Flag: Bahrain',\n  'Flag: Burundi',\n  'Flag: Benin',\n  'Flag: St. Barthélemy',\n  'Flag: Bermuda',\n  'Flag: Brunei',\n  'Flag: Bolivia',\n  'Flag: Caribbean Netherlands',\n  'Flag: Brazil',\n  'Flag: Bahamas',\n  'Flag: Bhutan',\n  'Flag: Bouvet Island',\n  'Flag: Botswana',\n  'Flag: Belarus',\n  'Flag: Belize',\n  'Flag: Canada',\n  'Flag: Cocos (Keeling) Islands',\n  'Flag: Congo - Kinshasa',\n  'Flag: Central African Republic',\n  'Flag: Congo - Brazzaville',\n  'Flag: Switzerland',\n  'Flag: Côte d’Ivoire',\n  'Flag: Cook Islands',\n  'Flag: Chile',\n  'Flag: Cameroon',\n  'Flag: China',\n  'Flag: Colombia',\n  'Flag: Clipperton Island',\n  'Flag: Costa Rica',\n  'Flag: Cuba',\n  'Flag: Cape Verde',\n  'Flag: Curaçao',\n  'Flag: Christmas Island',\n  'Flag: Cyprus',\n  'Flag: Czechia',\n  'Flag: Germany',\n  'Flag: Diego Garcia',\n  'Flag: Djibouti',\n  'Flag: Denmark',\n  'Flag: Dominica',\n  'Flag: Dominican Republic',\n  'Flag: Algeria',\n  'Flag: Ceuta &amp; Melilla',\n  'Flag: Ecuador',\n  'Flag: Estonia',\n  'Flag: Egypt',\n  'Flag: Western Sahara',\n  'Flag: Eritrea',\n  'Flag: Spain',\n  'Flag: Ethiopia',\n  'Flag: European Union',\n  'Flag: Finland',\n  'Flag: Fiji',\n  'Flag: Falkland Islands',\n  'Flag: Micronesia',\n  'Flag: Faroe Islands',\n  'Flag: france',\n  'Flag: Gabon',\n  'Flag: United Kingdom',\n  'Flag: Grenada',\n  'Flag: Georgia',\n  'Flag: French Guiana',\n  'Flag: Guernsey',\n  'Flag: Ghana',\n  'Flag: Gibraltar',\n  'Flag: Greenland',\n  'Flag: Gambia',\n  'Flag: Guinea',\n  'Flag: Guadeloupe',\n  'Flag: Equatorial Guinea',\n  'Flag: Greece',\n  'Flag: South Georgia &amp; South Sandwich Islands',\n  'Flag: Guatemala',\n  'Flag: Guam',\n  'Flag: Guinea-Bissau',\n  'Flag: Guyana',\n  'Flag: Hong Kong SAR China',\n  'Flag: Heard &amp; McDonald Islands',\n  'Flag: Honduras',\n  'Flag: Croatia',\n  'Flag: Haiti',\n  'Flag: Hungary',\n  'Flag: Canary Islands',\n  'Flag: Indonesia',\n  'Flag: Ireland',\n  'Flag: Israel',\n  'Flag: Isle of Man',\n  'Flag: India',\n  'Flag: British Indian Ocean Territory',\n  'Flag: Iraq',\n  'Flag: Iran',\n  'Flag: Iceland',\n  'Flag: Italy',\n  'Flag: Jersey',\n  'Flag: Jamaica',\n  'Flag: Jordan',\n  'Flag: Japan',\n  'Flag: Kenya',\n  'Flag: Kyrgyzstan',\n  'Flag: Cambodia',\n  'Flag: Kiribati',\n  'Flag: Comoros',\n  'Flag: St. Kitts &amp; Nevis',\n  'Flag: North Korea',\n  'Flag: South Korea',\n  'Flag: Kuwait',\n  'Flag: Cayman Islands',\n  'Flag: Kazakhstan',\n  'Flag: Laos',\n  'Flag: Lebanon',\n  'Flag: St. Lucia',\n  'Flag: Liechtenstein',\n  'Flag: Sri Lanka',\n  'Flag: Liberia',\n  'Flag: Lesotho',\n  'Flag: Lithuania',\n  'Flag: Luxembourg',\n  'Flag: Latvia',\n  'Flag: Libya',\n  'Flag: Morocco',\n  'Flag: Monaco',\n  'Flag: Moldova',\n  'Flag: Montenegro',\n  'Flag: St. Martin',\n  'Flag: Madagascar',\n  'Flag: Marshall Islands',\n  'Flag: North Macedonia',\n  'Flag: Mali',\n  'Flag: Myanmar (Burma)',\n  'Flag: Mongolia',\n  'Flag: Macau Sar China',\n  'Flag: Northern Mariana Islands',\n  'Flag: Martinique',\n  'Flag: Mauritania',\n  'Flag: Montserrat',\n  'Flag: Malta',\n  'Flag: Mauritius',\n  'Flag: Maldives',\n  'Flag: Malawi',\n  'Flag: Mexico',\n  'Flag: Malaysia',\n  'Flag: Mozambique',\n  'Flag: Namibia',\n  'Flag: New Caledonia',\n  'Flag: Niger',\n  'Flag: Norfolk Island',\n  'Flag: Nigeria',\n  'Flag: Nicaragua',\n  'Flag: Netherlands',\n  'Flag: Norway',\n  'Flag: Nepal',\n  'Flag: Nauru',\n  'Flag: Niue',\n  'Flag: New Zealand',\n  'Flag: Oman',\n  'Flag: Panama',\n  'Flag: Peru',\n  'Flag: French Polynesia',\n  'Flag: Papua New Guinea',\n  'Flag: Philippines',\n  'Flag: Pakistan',\n  'Flag: Poland',\n  'Flag: St. Pierre &amp; Miquelon',\n  'Flag: Pitcairn Islands',\n  'Flag: Puerto Rico',\n  'Flag: Palestinian Territories',\n  'Flag: Portugal',\n  'Flag: Palau',\n  'Flag: Paraguay',\n  'Flag: Qatar',\n  'Flag: Réunion',\n  'Flag: Romania',\n  'Flag: Serbia',\n  'Flag: Russia',\n  'Flag: Rwanda',\n  'Flag: Saudi Arabia',\n  'Flag: Solomon Islands',\n  'Flag: Seychelles',\n  'Flag: Sudan',\n  'Flag: Sweden',\n  'Flag: Singapore',\n  'Flag: St. Helena',\n  'Flag: Slovenia',\n  'Flag: Svalbard &amp; Jan Mayen',\n  'Flag: Slovakia',\n  'Flag: Sierra Leone',\n  'Flag: San Marino',\n  'Flag: Senegal',\n  'Flag: Somalia',\n  'Flag: Suriname',\n  'Flag: South Sudan',\n  'Flag: São Tomé &amp; Príncipe',\n  'Flag: El Salvador',\n  'Flag: Sint Maarten',\n  'Flag: Syria',\n  'Flag: Swaziland',\n  'Flag: Tristan Da Cunha',\n  'Flag: Turks &amp; Caicos Islands',\n  'Flag: Chad',\n  'Flag: French Southern Territories',\n  'Flag: Togo',\n  'Flag: Thailand',\n  'Flag: Tajikistan',\n  'Flag: Tokelau',\n  'Flag: Timor-Leste',\n  'Flag: Turkmenistan',\n  'Flag: Tunisia',\n  'Flag: Tonga',\n  'Flag: Turkey',\n  'Flag: Trinidad &amp; Tobago',\n  'Flag: Tuvalu',\n  'Flag: Taiwan',\n  'Flag: Tanzania',\n  'Flag: Ukraine',\n  'Flag: Uganda',\n  'Flag: U.S. Outlying Islands',\n  'Flag: United Nations',\n  'Flag: United States',\n  'Flag: Uruguay',\n  'Flag: Uzbekistan',\n  'Flag: Vatican City',\n  'Flag: St. Vincent &amp; Grenadines',\n  'Flag: Venezuela',\n  'Flag: British Virgin Islands',\n  'Flag: U.S. Virgin Islands',\n  'Flag: Vietnam',\n  'Flag: Vanuatu',\n  'Flag: Wallis &amp; Futuna',\n  'Flag: Samoa',\n  'Flag: Kosovo',\n  'Flag: Yemen',\n  'Flag: Mayotte',\n  'Flag: South Africa',\n  'Flag: Zambia',\n  'Flag: Zimbabwe',\n], [\n  '🏁',\n  '🚩',\n  '🎌',\n  '🏴',\n  '🏳',\n  '🏳️‍🌈',\n  '🏴‍☠️',\n  '🇦🇨',\n  '🇦🇩',\n  '🇦🇪',\n  '🇦🇫',\n  '🇦🇬',\n  '🇦🇮',\n  '🇦🇱',\n  '🇦🇲',\n  '🇦🇴',\n  '🇦🇶',\n  '🇦🇷',\n  '🇦🇸',\n  '🇦🇹',\n  '🇦🇺',\n  '🇦🇼',\n  '🇦🇽',\n  '🇦🇿',\n  '🇧🇦',\n  '🇧🇧',\n  '🇧🇩',\n  '🇧🇪',\n  '🇧🇫',\n  '🇧🇬',\n  '🇧🇭',\n  '🇧🇮',\n  '🇧🇯',\n  '🇧🇱',\n  '🇧🇲',\n  '🇧🇳',\n  '🇧🇴',\n  '🇧🇶',\n  '🇧🇷',\n  '🇧🇸',\n  '🇧🇹',\n  '🇧🇻',\n  '🇧🇼',\n  '🇧🇾',\n  '🇧🇿',\n  '🇨🇦',\n  '🇨🇨',\n  '🇨🇩',\n  '🇨🇫',\n  '🇨🇬',\n  '🇨🇭',\n  '🇨🇮',\n  '🇨🇰',\n  '🇨🇱',\n  '🇨🇲',\n  '🇨🇳',\n  '🇨🇴',\n  '🇨🇵',\n  '🇨🇷',\n  '🇨🇺',\n  '🇨🇻',\n  '🇨🇼',\n  '🇨🇽',\n  '🇨🇾',\n  '🇨🇿',\n  '🇩🇪',\n  '🇩🇬',\n  '🇩🇯',\n  '🇩🇰',\n  '🇩🇲',\n  '🇩🇴',\n  '🇩🇿',\n  '🇪🇦',\n  '🇪🇨',\n  '🇪🇪',\n  '🇪🇬',\n  '🇪🇭',\n  '🇪🇷',\n  '🇪🇸',\n  '🇪🇹',\n  '🇪🇺',\n  '🇫🇮',\n  '🇫🇯',\n  '🇫🇰',\n  '🇫🇲',\n  '🇫🇴',\n  '🇫🇷',\n  '🇬🇦',\n  '🇬🇧',\n  '🇬🇩',\n  '🇬🇪',\n  '🇬🇫',\n  '🇬🇬',\n  '🇬🇭',\n  '🇬🇮',\n  '🇬🇱',\n  '🇬🇲',\n  '🇬🇳',\n  '🇬🇵',\n  '🇬🇶',\n  '🇬🇷',\n  '🇬🇸',\n  '🇬🇹',\n  '🇬🇺',\n  '🇬🇼',\n  '🇬🇾',\n  '🇭🇰',\n  '🇭🇲',\n  '🇭🇳',\n  '🇭🇷',\n  '🇭🇹',\n  '🇭🇺',\n  '🇮🇨',\n  '🇮🇩',\n  '🇮🇪',\n  '🇮🇱',\n  '🇮🇲',\n  '🇮🇳',\n  '🇮🇴',\n  '🇮🇶',\n  '🇮🇷',\n  '🇮🇸',\n  '🇮🇹',\n  '🇯🇪',\n  '🇯🇲',\n  '🇯🇴',\n  '🇯🇵',\n  '🇰🇪',\n  '🇰🇬',\n  '🇰🇭',\n  '🇰🇮',\n  '🇰🇲',\n  '🇰🇳',\n  '🇰🇵',\n  '🇰🇷',\n  '🇰🇼',\n  '🇰🇾',\n  '🇰🇿',\n  '🇱🇦',\n  '🇱🇧',\n  '🇱🇨',\n  '🇱🇮',\n  '🇱🇰',\n  '🇱🇷',\n  '🇱🇸',\n  '🇱🇹',\n  '🇱🇺',\n  '🇱🇻',\n  '🇱🇾',\n  '🇲🇦',\n  '🇲🇨',\n  '🇲🇩',\n  '🇲🇪',\n  '🇲🇫',\n  '🇲🇬',\n  '🇲🇭',\n  '🇲🇰',\n  '🇲🇱',\n  '🇲🇲',\n  '🇲🇳',\n  '🇲🇴',\n  '🇲🇵',\n  '🇲🇶',\n  '🇲🇷',\n  '🇲🇸',\n  '🇲🇹',\n  '🇲🇺',\n  '🇲🇻',\n  '🇲🇼',\n  '🇲🇽',\n  '🇲🇾',\n  '🇲🇿',\n  '🇳🇦',\n  '🇳🇨',\n  '🇳🇪',\n  '🇳🇫',\n  '🇳🇬',\n  '🇳🇮',\n  '🇳🇱',\n  '🇳🇴',\n  '🇳🇵',\n  '🇳🇷',\n  '🇳🇺',\n  '🇳🇿',\n  '🇴🇲',\n  '🇵🇦',\n  '🇵🇪',\n  '🇵🇫',\n  '🇵🇬',\n  '🇵🇭',\n  '🇵🇰',\n  '🇵🇱',\n  '🇵🇲',\n  '🇵🇳',\n  '🇵🇷',\n  '🇵🇸',\n  '🇵🇹',\n  '🇵🇼',\n  '🇵🇾',\n  '🇶🇦',\n  '🇷🇪',\n  '🇷🇴',\n  '🇷🇸',\n  '🇷🇺',\n  '🇷🇼',\n  '🇸🇦',\n  '🇸🇧',\n  '🇸🇨',\n  '🇸🇩',\n  '🇸🇪',\n  '🇸🇬',\n  '🇸🇭',\n  '🇸🇮',\n  '🇸🇯',\n  '🇸🇰',\n  '🇸🇱',\n  '🇸🇲',\n  '🇸🇳',\n  '🇸🇴',\n  '🇸🇷',\n  '🇸🇸',\n  '🇸🇹',\n  '🇸🇻',\n  '🇸🇽',\n  '🇸🇾',\n  '🇸🇿',\n  '🇹🇦',\n  '🇹🇨',\n  '🇹🇩',\n  '🇹🇫',\n  '🇹🇬',\n  '🇹🇭',\n  '🇹🇯',\n  '🇹🇰',\n  '🇹🇱',\n  '🇹🇲',\n  '🇹🇳',\n  '🇹🇴',\n  '🇹🇷',\n  '🇹🇹',\n  '🇹🇻',\n  '🇹🇼',\n  '🇹🇿',\n  '🇺🇦',\n  '🇺🇬',\n  '🇺🇲',\n  '🇺🇳',\n  '🇺🇸',\n  '🇺🇾',\n  '🇺🇿',\n  '🇻🇦',\n  '🇻🇨',\n  '🇻🇪',\n  '🇻🇬',\n  '🇻🇮',\n  '🇻🇳',\n  '🇻🇺',\n  '🇼🇫',\n  '🇼🇸',\n  '🇽🇰',\n  '🇾🇪',\n  '🇾🇹',\n  '🇿🇦',\n  '🇿🇲',\n  '🇿🇼',\n]);\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_picker.dart",
    "content": "// ignore_for_file: constant_identifier_names\n\nimport 'dart:convert';\nimport 'dart:io';\nimport 'dart:math';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\nimport 'models/emoji_category_models.dart';\nimport 'emji_picker_config.dart';\nimport 'default_emoji_picker_view.dart';\nimport 'models/emoji_model.dart';\nimport 'emoji_lists.dart' as emoji_list;\nimport 'emoji_view_state.dart';\nimport 'models/recent_emoji_model.dart';\n\n/// The emoji category shown on the category tab\nenum EmojiCategory {\n  /// Searched emojis\n  SEARCH,\n\n  /// Recent emojis\n  RECENT,\n\n  /// Smiley emojis\n  SMILEYS,\n\n  /// Animal emojis\n  ANIMALS,\n\n  /// Food emojis\n  FOODS,\n\n  /// Activity emojis\n  ACTIVITIES,\n\n  /// Travel emojis\n  TRAVEL,\n\n  /// Objects emojis\n  OBJECTS,\n\n  /// Sumbol emojis\n  SYMBOLS,\n\n  /// Flag emojis\n  FLAGS,\n}\n\n/// Enum to alter the keyboard button style\nenum ButtonMode {\n  /// Android button style - gives the button a splash color with ripple effect\n  MATERIAL,\n\n  /// iOS button style - gives the button a fade out effect when pressed\n  CUPERTINO\n}\n\n/// Callback function for when emoji is selected\n///\n/// The function returns the selected [Emoji] as well\n/// as the [EmojiCategory] from which it originated\ntypedef OnEmojiSelected = void Function(EmojiCategory category, Emoji emoji);\n\n/// Callback function for backspace button\ntypedef OnBackspacePressed = void Function();\n\n/// Callback function for custom view\ntypedef EmojiViewBuilder = Widget Function(\n  EmojiPickerConfig config,\n  EmojiViewState state,\n);\n\n/// The Emoji Keyboard widget\n///\n/// This widget displays a grid of [Emoji] sorted by [EmojiCategory]\n/// which the user can horizontally scroll through.\n///\n/// There is also a bottombar which displays all the possible [EmojiCategory]\n/// and allow the user to quickly switch to that [EmojiCategory]\nclass EmojiPicker extends StatefulWidget {\n  /// EmojiPicker for flutter\n  const EmojiPicker({\n    super.key,\n    required this.onEmojiSelected,\n    this.onBackspacePressed,\n    this.config = const EmojiPickerConfig(),\n    this.customWidget,\n  });\n\n  /// Custom widget\n  final EmojiViewBuilder? customWidget;\n\n  /// The function called when the emoji is selected\n  final OnEmojiSelected onEmojiSelected;\n\n  /// The function called when backspace button is pressed\n  final OnBackspacePressed? onBackspacePressed;\n\n  /// Config for customizations\n  final EmojiPickerConfig config;\n\n  @override\n  EmojiPickerState createState() => EmojiPickerState();\n}\n\nclass EmojiPickerState extends State<EmojiPicker> {\n  static const platform = MethodChannel('emoji_picker_flutter');\n\n  List<EmojiCategoryGroup> emojiCategoryGroupList = List.empty(growable: true);\n  List<RecentEmoji> recentEmojiList = List.empty(growable: true);\n  late Future<void> updateEmojiFuture;\n\n  // Prevent emojis to be reloaded with every build\n  bool loaded = false;\n\n  @override\n  void initState() {\n    super.initState();\n    updateEmojiFuture = _updateEmojis();\n  }\n\n  @override\n  void didUpdateWidget(covariant EmojiPicker oldWidget) {\n    if (oldWidget.config != widget.config) {\n      // EmojiPickerConfig changed - rebuild EmojiPickerView completely\n      loaded = false;\n      updateEmojiFuture = _updateEmojis();\n    }\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (!loaded) {\n      // Load emojis\n      updateEmojiFuture.then(\n        (value) => WidgetsBinding.instance.addPostFrameCallback((_) {\n          if (!mounted) return;\n          setState(() {\n            loaded = true;\n          });\n        }),\n      );\n\n      // Show loading indicator\n      return const Center(child: CircularProgressIndicator());\n    }\n    if (widget.config.showRecentsTab) {\n      emojiCategoryGroupList[0].emoji =\n          recentEmojiList.map((e) => e.emoji).toList().cast<Emoji>();\n    }\n\n    final state = EmojiViewState(\n      emojiCategoryGroupList,\n      _getOnEmojiListener(),\n      widget.onBackspacePressed,\n    );\n\n    // Build\n    return widget.customWidget == null\n        ? DefaultEmojiPickerView(widget.config, state)\n        : widget.customWidget!(widget.config, state);\n  }\n\n  // Add recent emoji handling to tap listener\n  OnEmojiSelected _getOnEmojiListener() {\n    return (category, emoji) {\n      if (widget.config.showRecentsTab) {\n        _addEmojiToRecentlyUsed(emoji).then((value) {\n          if (category != EmojiCategory.RECENT && mounted) {\n            setState(() {\n              // rebuild to update recent emoji tab\n              // when it is not current tab\n            });\n          }\n        });\n      }\n      widget.onEmojiSelected(category, emoji);\n    };\n  }\n\n  // Initialize emoji data\n  Future<void> _updateEmojis() async {\n    emojiCategoryGroupList.clear();\n    if (widget.config.showRecentsTab) {\n      recentEmojiList = await _getRecentEmojis();\n      final List<Emoji> recentEmojiMap =\n          recentEmojiList.map((e) => e.emoji).toList().cast<Emoji>();\n      emojiCategoryGroupList\n          .add(EmojiCategoryGroup(EmojiCategory.RECENT, recentEmojiMap));\n    }\n    emojiCategoryGroupList.addAll([\n      EmojiCategoryGroup(\n        EmojiCategory.SMILEYS,\n        await _getAvailableEmojis(emoji_list.smileys, title: 'smileys'),\n      ),\n      EmojiCategoryGroup(\n        EmojiCategory.ANIMALS,\n        await _getAvailableEmojis(emoji_list.animals, title: 'animals'),\n      ),\n      EmojiCategoryGroup(\n        EmojiCategory.FOODS,\n        await _getAvailableEmojis(emoji_list.foods, title: 'foods'),\n      ),\n      EmojiCategoryGroup(\n        EmojiCategory.ACTIVITIES,\n        await _getAvailableEmojis(\n          emoji_list.activities,\n          title: 'activities',\n        ),\n      ),\n      EmojiCategoryGroup(\n        EmojiCategory.TRAVEL,\n        await _getAvailableEmojis(emoji_list.travel, title: 'travel'),\n      ),\n      EmojiCategoryGroup(\n        EmojiCategory.OBJECTS,\n        await _getAvailableEmojis(emoji_list.objects, title: 'objects'),\n      ),\n      EmojiCategoryGroup(\n        EmojiCategory.SYMBOLS,\n        await _getAvailableEmojis(emoji_list.symbols, title: 'symbols'),\n      ),\n      EmojiCategoryGroup(\n        EmojiCategory.FLAGS,\n        await _getAvailableEmojis(emoji_list.flags, title: 'flags'),\n      ),\n    ]);\n  }\n\n  // Get available emoji for given category title\n  Future<List<Emoji>> _getAvailableEmojis(\n    Map<String, String> map, {\n    required String title,\n  }) async {\n    Map<String, String>? newMap;\n\n    // Get Emojis cached locally if available\n    newMap = await _restoreFilteredEmojis(title);\n\n    if (newMap == null) {\n      // Check if emoji is available on this platform\n      newMap = await _getPlatformAvailableEmoji(map);\n      // Save available Emojis to local storage for faster loading next time\n      if (newMap != null) {\n        await _cacheFilteredEmojis(title, newMap);\n      }\n    }\n\n    // Map to Emoji Object\n    return newMap!.entries\n        .map<Emoji>((entry) => Emoji(entry.key, entry.value))\n        .toList();\n  }\n\n  // Check if emoji is available on current platform\n  Future<Map<String, String>?> _getPlatformAvailableEmoji(\n    Map<String, String> emoji,\n  ) async {\n    if (Platform.isAndroid) {\n      Map<String, String>? filtered = {};\n      const delimiter = '|';\n      try {\n        final entries = emoji.values.join(delimiter);\n        final keys = emoji.keys.join(delimiter);\n        final result = (await platform.invokeMethod<String>(\n          'checkAvailability',\n          {'emojiKeys': keys, 'emojiEntries': entries},\n        )) as String;\n        final resultKeys = result.split(delimiter);\n        for (var i = 0; i < resultKeys.length; i++) {\n          filtered[resultKeys[i]] = emoji[resultKeys[i]]!;\n        }\n      } on PlatformException catch (_) {\n        filtered = null;\n      }\n      return filtered;\n    } else {\n      return emoji;\n    }\n  }\n\n  // Restore locally cached emoji\n  Future<Map<String, String>?> _restoreFilteredEmojis(String title) async {\n    final prefs = await SharedPreferences.getInstance();\n    final emojiJson = prefs.getString(title);\n    if (emojiJson == null) {\n      return null;\n    }\n    final emojis =\n        Map<String, String>.from(jsonDecode(emojiJson) as Map<String, dynamic>);\n    return emojis;\n  }\n\n  // Stores filtered emoji locally for faster access next time\n  Future<void> _cacheFilteredEmojis(\n    String title,\n    Map<String, String> emojis,\n  ) async {\n    final prefs = await SharedPreferences.getInstance();\n    final emojiJson = jsonEncode(emojis);\n    await prefs.setString(title, emojiJson);\n  }\n\n  // Returns list of recently used emoji from cache\n  Future<List<RecentEmoji>> _getRecentEmojis() async {\n    final prefs = await SharedPreferences.getInstance();\n    final emojiJson = prefs.getString('recent');\n    if (emojiJson == null) {\n      return [];\n    }\n    final json = jsonDecode(emojiJson) as List<dynamic>;\n    return json.map<RecentEmoji>(RecentEmoji.fromJson).toList();\n  }\n\n  // Add an emoji to recently used list or increase its counter\n  Future<void> _addEmojiToRecentlyUsed(Emoji emoji) async {\n    final prefs = await SharedPreferences.getInstance();\n    final recentEmojiIndex = recentEmojiList\n        .indexWhere((element) => element.emoji.emoji == emoji.emoji);\n    if (recentEmojiIndex != -1) {\n      // Already exist in recent list\n      // Just update counter\n      recentEmojiList[recentEmojiIndex].counter++;\n    } else {\n      recentEmojiList.add(RecentEmoji(emoji, 1));\n    }\n    // Sort by counter desc\n    recentEmojiList.sort((a, b) => b.counter - a.counter);\n    // Limit entries to recentsLimit\n    recentEmojiList = recentEmojiList.sublist(\n      0,\n      min(widget.config.recentsLimit, recentEmojiList.length),\n    );\n    // save locally\n    await prefs.setString('recent', jsonEncode(recentEmojiList));\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_picker_builder.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'emji_picker_config.dart';\nimport 'emoji_view_state.dart';\n\n/// Template class for custom implementation\n/// Inherit this class to create your own EmojiPicker\nabstract class EmojiPickerBuilder extends StatefulWidget {\n  /// Constructor\n  const EmojiPickerBuilder(this.config, this.state, {super.key});\n\n  /// Config for customizations\n  final EmojiPickerConfig config;\n\n  /// State that holds current emoji data\n  final EmojiViewState state;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emoji_view_state.dart",
    "content": "import 'models/emoji_category_models.dart';\nimport 'emoji_picker.dart';\n\n/// State that holds current emoji data\nclass EmojiViewState {\n  /// Constructor\n  EmojiViewState(\n    this.emojiCategoryGroupList,\n    this.onEmojiSelected,\n    this.onBackspacePressed,\n  );\n\n  /// List of all categories including their emojis\n  final List<EmojiCategoryGroup> emojiCategoryGroupList;\n\n  /// Callback when pressed on emoji\n  final OnEmojiSelected onEmojiSelected;\n\n  /// Callback when pressed on backspace\n  final OnBackspacePressed? onBackspacePressed;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/flowy_emoji_picker_config.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/src/emji_picker_config.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nEmojiPickerConfig buildFlowyEmojiPickerConfig(BuildContext context) {\n  final style = Theme.of(context);\n  return EmojiPickerConfig(\n    bgColor: style.cardColor,\n    categoryIconColor: style.iconTheme.color,\n    selectedCategoryIconColor: style.colorScheme.onSurface,\n    selectedCategoryIconBackgroundColor: style.colorScheme.primary,\n    progressIndicatorColor: style.colorScheme.primary,\n    backspaceColor: style.colorScheme.primary,\n    searchHintText: LocaleKeys.emoji_search.tr(),\n    serachHintTextStyle: style.textTheme.bodyMedium?.copyWith(\n      color: style.hintColor,\n    ),\n    serachBarEnableBorder: OutlineInputBorder(\n      borderRadius: BorderRadius.circular(4),\n      borderSide: BorderSide(color: style.dividerColor),\n    ),\n    serachBarFocusedBorder: OutlineInputBorder(\n      borderRadius: BorderRadius.circular(4),\n      borderSide: BorderSide(\n        color: style.colorScheme.primary,\n      ),\n    ),\n    noRecentsText: LocaleKeys.emoji_noRecent.tr(),\n    noRecentsStyle: style.textTheme.bodyMedium,\n    noEmojiFoundText: LocaleKeys.emoji_noEmojiFound.tr(),\n    scrollBarHandleColor: style.colorScheme.onSurface,\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/models/emoji_category_models.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'emoji_model.dart';\nimport '../emoji_picker.dart';\n\n/// EmojiCategory with its emojis\nclass EmojiCategoryGroup {\n  EmojiCategoryGroup(this.category, this.emoji);\n\n  final EmojiCategory category;\n\n  /// List of emoji of this category\n  List<Emoji> emoji;\n\n  @override\n  String toString() {\n    return 'Name: $category, Emoji: $emoji';\n  }\n}\n\n/// Class that defines the icon representing a [EmojiCategory]\nclass EmojiCategoryIcon {\n  /// Icon of Category\n  const EmojiCategoryIcon({\n    required this.icon,\n    this.color = const Color(0xffd3d3d3),\n    this.selectedColor = const Color(0xffb2b2b2),\n  });\n\n  /// The icon to represent the category\n  final IconData icon;\n\n  /// The default color of the icon\n  final Color color;\n\n  /// The color of the icon once the category is selected\n  final Color selectedColor;\n}\n\n/// Class used to define all the [EmojiCategoryIcon] shown for each [EmojiCategory]\n///\n/// This allows the keyboard to be personalized by changing icons shown.\n/// If a [EmojiCategoryIcon] is set as null or not defined during initialization,\n/// the default icons will be used instead\nclass EmojiCategoryIcons {\n  /// Constructor\n  const EmojiCategoryIcons({\n    this.recentIcon = Icons.access_time,\n    this.smileyIcon = Icons.tag_faces,\n    this.animalIcon = Icons.pets,\n    this.foodIcon = Icons.fastfood,\n    this.activityIcon = Icons.directions_run,\n    this.travelIcon = Icons.location_city,\n    this.objectIcon = Icons.lightbulb_outline,\n    this.symbolIcon = Icons.emoji_symbols,\n    this.flagIcon = Icons.flag,\n    this.searchIcon = Icons.search,\n  });\n\n  /// Icon for [EmojiCategory.RECENT]\n  final IconData recentIcon;\n\n  /// Icon for [EmojiCategory.SMILEYS]\n  final IconData smileyIcon;\n\n  /// Icon for [EmojiCategory.ANIMALS]\n  final IconData animalIcon;\n\n  /// Icon for [EmojiCategory.FOODS]\n  final IconData foodIcon;\n\n  /// Icon for [EmojiCategory.ACTIVITIES]\n  final IconData activityIcon;\n\n  /// Icon for [EmojiCategory.TRAVEL]\n  final IconData travelIcon;\n\n  /// Icon for [EmojiCategory.OBJECTS]\n  final IconData objectIcon;\n\n  /// Icon for [EmojiCategory.SYMBOLS]\n  final IconData symbolIcon;\n\n  /// Icon for [EmojiCategory.FLAGS]\n  final IconData flagIcon;\n\n  /// Icon for [EmojiCategory.SEARCH]\n  final IconData searchIcon;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/models/emoji_model.dart",
    "content": "/// A class to store data for each individual emoji\nclass Emoji {\n  /// Emoji constructor\n  const Emoji(this.name, this.emoji);\n\n  /// The name or description for this emoji\n  final String name;\n\n  /// The unicode string for this emoji\n  ///\n  /// This is the string that should be displayed to view the emoji\n  final String emoji;\n\n  @override\n  String toString() {\n    // return 'Name: $name, Emoji: $emoji';\n    return name;\n  }\n\n  /// Parse Emoji from json\n  static Emoji fromJson(Map<String, dynamic> json) {\n    return Emoji(json['name'] as String, json['emoji'] as String);\n  }\n\n  ///  Encode Emoji to json\n  Map<String, dynamic> toJson() {\n    return {\n      'name': name,\n      'emoji': emoji,\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/models/recent_emoji_model.dart",
    "content": "import 'emoji_model.dart';\n\n/// Class that holds an recent emoji\n/// Recent Emoji has an instance of the emoji\n/// And a counter, which counts how often this emoji\n/// has been used before\nclass RecentEmoji {\n  /// Constructor\n  RecentEmoji(this.emoji, this.counter);\n\n  /// Emoji instance\n  final Emoji emoji;\n\n  /// Counter how often emoji has been used before\n  int counter = 0;\n\n  /// Parse RecentEmoji from json\n  static RecentEmoji fromJson(dynamic json) {\n    return RecentEmoji(\n      Emoji.fromJson(json['emoji'] as Map<String, dynamic>),\n      json['counter'] as int,\n    );\n  }\n\n  /// Encode RecentEmoji to json\n  Map<String, dynamic> toJson() => {\n        'emoji': emoji,\n        'counter': counter,\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\n\nclass FeatureFlagsPage extends StatelessWidget {\n  const FeatureFlagsPage({\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return SettingsBody(\n      title: 'Feature flags',\n      children: [\n        SeparatedColumn(\n          children: FeatureFlag.data.entries\n              .where((e) => e.key != FeatureFlag.unknown)\n              .map((e) => _FeatureFlagItem(featureFlag: e.key))\n              .toList(),\n        ),\n        FlowyTextButton(\n          'Restart the app to apply changes',\n          fontSize: 16.0,\n          fontColor: Colors.red,\n          padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),\n          onPressed: () async => runAppFlowy(),\n        ),\n      ],\n    );\n  }\n}\n\nclass _FeatureFlagItem extends StatefulWidget {\n  const _FeatureFlagItem({required this.featureFlag});\n\n  final FeatureFlag featureFlag;\n\n  @override\n  State<_FeatureFlagItem> createState() => _FeatureFlagItemState();\n}\n\nclass _FeatureFlagItemState extends State<_FeatureFlagItem> {\n  @override\n  Widget build(BuildContext context) {\n    return ListTile(\n      title: FlowyText(widget.featureFlag.name, fontSize: 16.0),\n      subtitle: FlowyText.small(widget.featureFlag.description, maxLines: 3),\n      trailing: Switch.adaptive(\n        value: widget.featureFlag.isOn,\n        onChanged: (value) async {\n          await widget.featureFlag.update(value);\n          setState(() {});\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart",
    "content": "import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart';\nimport 'package:flutter/material.dart';\n\nclass FeatureFlagScreen extends StatelessWidget {\n  const FeatureFlagScreen({\n    super.key,\n  });\n\n  static const routeName = '/feature_flag';\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('Feature Flags'),\n      ),\n      body: const FeatureFlagsPage(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:styled_widget/styled_widget.dart';\n\nimport '../../../../../generated/locale_keys.g.dart';\n\nclass SettingsExportFileWidget extends StatefulWidget {\n  const SettingsExportFileWidget({super.key});\n\n  @override\n  State<SettingsExportFileWidget> createState() =>\n      SettingsExportFileWidgetState();\n}\n\n@visibleForTesting\nclass SettingsExportFileWidgetState extends State<SettingsExportFileWidget> {\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        FlowyText.medium(\n          LocaleKeys.settings_files_exportData.tr(),\n          fontSize: 13,\n          overflow: TextOverflow.ellipsis,\n        ).padding(horizontal: 5.0),\n        const Spacer(),\n        _OpenExportedDirectoryButton(\n          onTap: () async {\n            await showDialog(\n              context: context,\n              builder: (context) {\n                return const FlowyDialog(\n                  child: Padding(\n                    padding: EdgeInsets.symmetric(\n                      horizontal: 16,\n                      vertical: 20,\n                    ),\n                    child: FileExporterWidget(),\n                  ),\n                );\n              },\n            );\n          },\n        ),\n      ],\n    );\n  }\n}\n\nclass _OpenExportedDirectoryButton extends StatelessWidget {\n  const _OpenExportedDirectoryButton({\n    required this.onTap,\n  });\n\n  final VoidCallback onTap;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyIconButton(\n      hoverColor: Theme.of(context).colorScheme.secondaryContainer,\n      tooltipText: LocaleKeys.settings_files_export.tr(),\n      icon: FlowySvg(\n        FlowySvgs.open_folder_lg,\n        color: Theme.of(context).iconTheme.color,\n      ),\n      onPressed: onTap,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart",
    "content": "import 'dart:io';\n\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/navigator_context_extension.dart';\nimport 'package:appflowy/workspace/application/export/document_exporter.dart';\nimport 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/share/export_service.dart';\nimport 'package:appflowy/workspace/presentation/home/toast.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/file_picker/file_picker_service.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:path/path.dart' as p;\n\nimport '../../../../../generated/locale_keys.g.dart';\n\nclass FileExporterWidget extends StatefulWidget {\n  const FileExporterWidget({super.key});\n\n  @override\n  State<FileExporterWidget> createState() => _FileExporterWidgetState();\n}\n\nclass _FileExporterWidgetState extends State<FileExporterWidget> {\n  // Map<String, List<String>> _selectedPages = {};\n\n  SettingsFileExporterCubit? cubit;\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder<FlowyResult<WorkspacePB, FlowyError>>(\n      future: FolderEventReadCurrentWorkspace().send(),\n      builder: (context, snapshot) {\n        if (snapshot.hasData &&\n            snapshot.connectionState == ConnectionState.done) {\n          final workspace = snapshot.data?.fold((s) => s, (e) => null);\n          if (workspace != null) {\n            final views = workspace.views;\n            cubit ??= SettingsFileExporterCubit(views: views);\n            return BlocProvider<SettingsFileExporterCubit>.value(\n              value: cubit!,\n              child: Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  Row(\n                    mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                    children: [\n                      FlowyText.medium(\n                        LocaleKeys.settings_files_selectFiles.tr(),\n                        fontSize: 16.0,\n                      ),\n                      BlocBuilder<SettingsFileExporterCubit,\n                          SettingsFileExportState>(\n                        builder: (context, state) => FlowyTextButton(\n                          state.selectedItems\n                                  .expand((element) => element)\n                                  .every((element) => element)\n                              ? LocaleKeys.settings_files_deselectAll.tr()\n                              : LocaleKeys.settings_files_selectAll.tr(),\n                          fontColor: AFThemeExtension.of(context).textColor,\n                          onPressed: () {\n                            context\n                                .read<SettingsFileExporterCubit>()\n                                .selectOrDeselectAllItems();\n                          },\n                        ),\n                      ),\n                    ],\n                  ),\n                  const VSpace(8),\n                  const Expanded(child: _ExpandedList()),\n                  const VSpace(8),\n                  _buildButtons(),\n                ],\n              ),\n            );\n          }\n        }\n        return const CircularProgressIndicator();\n      },\n    );\n  }\n\n  Widget _buildButtons() {\n    return Row(\n      children: [\n        const Spacer(),\n        FlowyTextButton(\n          LocaleKeys.button_cancel.tr(),\n          fontColor: AFThemeExtension.of(context).textColor,\n          onPressed: () => Navigator.of(context).pop(),\n        ),\n        const HSpace(8),\n        FlowyTextButton(\n          LocaleKeys.button_ok.tr(),\n          fontColor: AFThemeExtension.of(context).textColor,\n          onPressed: () async {\n            await getIt<FilePickerService>()\n                .getDirectoryPath()\n                .then((exportPath) async {\n              if (exportPath != null && cubit != null) {\n                final views = cubit!.state.selectedViews;\n                final result =\n                    await _AppFlowyFileExporter.exportToPath(exportPath, views);\n                if (mounted) {\n                  if (result.$1) {\n                    // success\n                    showSnackBarMessage(\n                      context,\n                      LocaleKeys.settings_files_exportFileSuccess.tr(),\n                    );\n                  } else {\n                    showSnackBarMessage(\n                      context,\n                      LocaleKeys.settings_files_exportFileFail.tr() +\n                          result.$2.join('\\n'),\n                    );\n                  }\n                }\n              } else if (mounted) {\n                showSnackBarMessage(\n                  context,\n                  LocaleKeys.settings_files_exportFileFail.tr(),\n                );\n              }\n              if (mounted) {\n                context.popToHome();\n              }\n            });\n          },\n        ),\n      ],\n    );\n  }\n}\n\nclass _ExpandedList extends StatefulWidget {\n  const _ExpandedList();\n\n  // final List<AppPB> apps;\n  // final void Function(Map<String, List<String>> selectedPages) onChanged;\n\n  @override\n  State<_ExpandedList> createState() => _ExpandedListState();\n}\n\nclass _ExpandedListState extends State<_ExpandedList> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<SettingsFileExporterCubit, SettingsFileExportState>(\n      builder: (context, state) {\n        return Material(\n          color: Colors.transparent,\n          child: SingleChildScrollView(\n            child: Column(\n              children: _buildChildren(context),\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  List<Widget> _buildChildren(BuildContext context) {\n    final apps = context.read<SettingsFileExporterCubit>().state.views;\n    final List<Widget> children = [];\n    for (var i = 0; i < apps.length; i++) {\n      children.add(_buildExpandedItem(context, i));\n    }\n    return children;\n  }\n\n  Widget _buildExpandedItem(BuildContext context, int index) {\n    final state = context.read<SettingsFileExporterCubit>().state;\n    final apps = state.views;\n    final expanded = state.expanded;\n    final selectedItems = state.selectedItems;\n    final isExpanded = expanded[index] == true;\n    final List<Widget> expandedChildren = [];\n    if (isExpanded) {\n      for (var i = 0; i < selectedItems[index].length; i++) {\n        final name = apps[index].childViews[i].name;\n        final checkbox = CheckboxListTile(\n          value: selectedItems[index][i],\n          onChanged: (value) {\n            // update selected item\n            context\n                .read<SettingsFileExporterCubit>()\n                .selectOrDeselectItem(index, i);\n          },\n          title: FlowyText.regular('  $name'),\n        );\n        expandedChildren.add(checkbox);\n      }\n    }\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        GestureDetector(\n          onTap: () => context\n              .read<SettingsFileExporterCubit>()\n              .expandOrUnexpandApp(index),\n          child: ListTile(\n            title: FlowyText.medium(apps[index].name),\n            trailing: Icon(\n              isExpanded\n                  ? Icons.arrow_drop_down_rounded\n                  : Icons.arrow_drop_up_rounded,\n            ),\n          ),\n        ),\n        ...expandedChildren,\n      ],\n    );\n  }\n}\n\nclass _AppFlowyFileExporter {\n  static Future<(bool result, List<String> failedNames)> exportToPath(\n    String path,\n    List<ViewPB> views,\n  ) async {\n    final failedFileNames = <String>[];\n    final Map<String, int> names = {};\n    for (final view in views) {\n      String? content;\n      String? fileExtension;\n      switch (view.layout) {\n        case ViewLayoutPB.Document:\n          final documentExporter = DocumentExporter(view);\n          final result = await documentExporter.export(\n            DocumentExportType.json,\n          );\n          result.fold(\n            (json) {\n              content = json;\n            },\n            (e) => Log.error(e),\n          );\n          fileExtension = 'afdocument';\n          break;\n        default:\n          final result =\n              await BackendExportService.exportDatabaseAsCSV(view.id);\n          result.fold(\n            (l) => content = l.data,\n            (r) => Log.error(r),\n          );\n          fileExtension = 'csv';\n          break;\n      }\n      if (content != null) {\n        final count = names.putIfAbsent(view.name, () => 0);\n        final name = count == 0 ? view.name : '${view.name}($count)';\n        final file = File(p.join(path, '$name.$fileExtension'));\n        await file.writeAsString(content!);\n        names[view.name] = count + 1;\n      } else {\n        failedFileNames.add(view.name);\n      }\n    }\n\n    return (failedFileNames.isEmpty, failedFileNames);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/invitation/invite_member_by_email.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:string_validator/string_validator.dart';\n\nclass InviteMemberByEmail extends StatefulWidget {\n  const InviteMemberByEmail({super.key});\n\n  @override\n  State<InviteMemberByEmail> createState() => _InviteMemberByEmailState();\n}\n\nclass _InviteMemberByEmailState extends State<InviteMemberByEmail> {\n  final _emailController = TextEditingController();\n\n  @override\n  void dispose() {\n    _emailController.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Text(\n          LocaleKeys.settings_appearance_members_inviteMemberByEmail.tr(),\n          style: theme.textStyle.body.enhanced(\n            color: theme.textColorScheme.primary,\n          ),\n        ),\n        VSpace(theme.spacing.m),\n        Row(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            Expanded(\n              child: AFTextField(\n                size: AFTextFieldSize.m,\n                controller: _emailController,\n                hintText:\n                    LocaleKeys.settings_appearance_members_inviteHint.tr(),\n                onSubmitted: (value) => _inviteMember(),\n              ),\n            ),\n            HSpace(theme.spacing.l),\n            AFFilledTextButton.primary(\n              text: LocaleKeys.settings_appearance_members_sendInvite.tr(),\n              onTap: _inviteMember,\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n\n  void _inviteMember() {\n    final email = _emailController.text;\n    if (!isEmail(email)) {\n      showToastNotification(\n        type: ToastificationType.error,\n        message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(),\n      );\n      return;\n    }\n\n    context\n        .read<WorkspaceMemberBloc>()\n        .add(WorkspaceMemberEvent.inviteWorkspaceMemberByEmail(email));\n    // clear the email field after inviting\n    _emailController.clear();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/invitation/invite_member_by_link.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pbenum.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nclass InviteMemberByLink extends StatelessWidget {\n  const InviteMemberByLink({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        Expanded(\n          child: Column(\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              _Title(),\n              _Description(),\n            ],\n          ),\n        ),\n        _CopyLinkButton(),\n      ],\n    );\n  }\n}\n\nclass _Title extends StatelessWidget {\n  const _Title();\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Text(\n      LocaleKeys.settings_appearance_members_inviteLinkToAddMember.tr(),\n      style: theme.textStyle.body.enhanced(\n        color: theme.textColorScheme.primary,\n      ),\n      maxLines: 1,\n      overflow: TextOverflow.ellipsis,\n    );\n  }\n}\n\nclass _Description extends StatelessWidget {\n  const _Description();\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Text.rich(\n      TextSpan(\n        children: [\n          TextSpan(\n            text: LocaleKeys.settings_appearance_members_clickToCopyLink.tr(),\n            style: theme.textStyle.caption.standard(\n              color: theme.textColorScheme.primary,\n            ),\n          ),\n          TextSpan(\n            text: ' ${LocaleKeys.settings_appearance_members_or.tr()} ',\n            style: theme.textStyle.caption.standard(\n              color: theme.textColorScheme.primary,\n            ),\n          ),\n          TextSpan(\n            text: LocaleKeys.settings_appearance_members_generateANewLink.tr(),\n            style: theme.textStyle.caption.standard(\n              color: theme.textColorScheme.action,\n            ),\n            mouseCursor: SystemMouseCursors.click,\n            recognizer: TapGestureRecognizer()\n              ..onTap = () => _onGenerateInviteLink(context),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Future<void> _onGenerateInviteLink(BuildContext context) async {\n    final state = context.read<WorkspaceMemberBloc>().state;\n    final subscriptionInfo = state.subscriptionInfo;\n    final inviteLink = state.inviteLink;\n\n    // check the current workspace member count, if it exceed the limit, show a upgrade dialog.\n    // prevent hard code here, because the member count may exceed the limit after the invite link is generated.\n    if (inviteLink == null &&\n        subscriptionInfo?.plan == WorkspacePlanPB.FreePlan &&\n        state.members.length >= 2) {\n      await showConfirmDialog(\n        context: context,\n        title:\n            LocaleKeys.settings_appearance_members_inviteFailedDialogTitle.tr(),\n        description:\n            LocaleKeys.settings_appearance_members_inviteFailedMemberLimit.tr(),\n        confirmLabel: LocaleKeys.upgradePlanModal_actionButton.tr(),\n        onConfirm: (_) => context\n            .read<WorkspaceMemberBloc>()\n            .add(const WorkspaceMemberEvent.upgradePlan()),\n      );\n      return;\n    }\n\n    if (inviteLink != null) {\n      // show a dialog to confirm if the user wants to copy the link to the clipboard\n      await showConfirmDialog(\n        context: context,\n        style: ConfirmPopupStyle.cancelAndOk,\n        title: LocaleKeys.settings_appearance_members_resetInviteLink.tr(),\n        description: LocaleKeys\n            .settings_appearance_members_resetInviteLinkDescription\n            .tr(),\n        confirmLabel: LocaleKeys.settings_appearance_members_reset.tr(),\n        onConfirm: (_) {\n          context.read<WorkspaceMemberBloc>().add(\n                const WorkspaceMemberEvent.generateInviteLink(),\n              );\n        },\n        confirmButtonBuilder: (_) => AFFilledTextButton.destructive(\n          text: LocaleKeys.settings_appearance_members_reset.tr(),\n          onTap: () {\n            context.read<WorkspaceMemberBloc>().add(\n                  const WorkspaceMemberEvent.generateInviteLink(),\n                );\n\n            Navigator.of(context).pop();\n          },\n        ),\n      );\n    } else {\n      context.read<WorkspaceMemberBloc>().add(\n            const WorkspaceMemberEvent.generateInviteLink(),\n          );\n    }\n  }\n}\n\nclass _CopyLinkButton extends StatefulWidget {\n  const _CopyLinkButton();\n\n  @override\n  State<_CopyLinkButton> createState() => _CopyLinkButtonState();\n}\n\nclass _CopyLinkButtonState extends State<_CopyLinkButton> {\n  ToastificationItem? toastificationItem;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return AFOutlinedTextButton.normal(\n      text: LocaleKeys.settings_appearance_members_copyLink.tr(),\n      textStyle: theme.textStyle.body.standard(\n        color: theme.textColorScheme.primary,\n      ),\n      padding: EdgeInsets.symmetric(\n        horizontal: theme.spacing.l,\n        vertical: theme.spacing.s,\n      ),\n      onTap: () async {\n        final state = context.read<WorkspaceMemberBloc>().state;\n        final subscriptionInfo = state.subscriptionInfo;\n        // check the current workspace member count, if it exceed the limit, show a upgrade dialog.\n        // prevent hard code here, because the member count may exceed the limit after the invite link is generated.\n        if (subscriptionInfo?.plan == WorkspacePlanPB.FreePlan &&\n            state.members.length >= 2) {\n          await showConfirmDialog(\n            context: context,\n            title: LocaleKeys\n                .settings_appearance_members_inviteFailedDialogTitle\n                .tr(),\n            description: LocaleKeys\n                .settings_appearance_members_inviteFailedMemberLimit\n                .tr(),\n            confirmLabel: LocaleKeys.upgradePlanModal_actionButton.tr(),\n            onConfirm: (_) => context\n                .read<WorkspaceMemberBloc>()\n                .add(const WorkspaceMemberEvent.upgradePlan()),\n          );\n          return;\n        }\n\n        final link = state.inviteLink;\n        if (link != null) {\n          await getIt<ClipboardService>().setData(\n            ClipboardServiceData(\n              plainText: link,\n            ),\n          );\n\n          if (toastificationItem != null) {\n            toastification.dismiss(toastificationItem!);\n          }\n\n          toastificationItem = showToastNotification(\n            message: LocaleKeys.shareAction_copyLinkSuccess.tr(),\n          );\n        } else {\n          showToastNotification(\n            message: LocaleKeys.settings_appearance_members_noInviteLink.tr(),\n            type: ToastificationType.error,\n          );\n        }\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/invitation/m_invite_member_by_email.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:string_validator/string_validator.dart';\n\nclass MInviteMemberByEmail extends StatefulWidget {\n  const MInviteMemberByEmail({super.key});\n\n  @override\n  State<MInviteMemberByEmail> createState() => _MInviteMemberByEmailState();\n}\n\nclass _MInviteMemberByEmailState extends State<MInviteMemberByEmail> {\n  final _emailController = TextEditingController();\n\n  bool _isInviteButtonEnabled = false;\n\n  @override\n  void initState() {\n    super.initState();\n\n    _emailController.addListener(_onEmailChanged);\n  }\n\n  @override\n  void dispose() {\n    _emailController.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        AFTextField(\n          autoFocus: true,\n          controller: _emailController,\n          hintText: LocaleKeys.settings_appearance_members_inviteHint.tr(),\n          onSubmitted: (value) => _inviteMember(),\n        ),\n        VSpace(theme.spacing.m),\n        _isInviteButtonEnabled\n            ? AFFilledTextButton.primary(\n                text: 'Send invite',\n                alignment: Alignment.center,\n                size: AFButtonSize.l,\n                textStyle: theme.textStyle.heading4.enhanced(\n                  color: theme.textColorScheme.onFill,\n                ),\n                onTap: _inviteMember,\n              )\n            : AFFilledTextButton.disabled(\n                text: 'Send invite',\n                alignment: Alignment.center,\n                size: AFButtonSize.l,\n                textStyle: theme.textStyle.heading4.enhanced(\n                  color: theme.textColorScheme.tertiary,\n                ),\n              ),\n      ],\n    );\n  }\n\n  void _inviteMember() {\n    final email = _emailController.text;\n    if (!isEmail(email)) {\n      showToastNotification(\n        type: ToastificationType.error,\n        message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(),\n      );\n      return;\n    }\n\n    context\n        .read<WorkspaceMemberBloc>()\n        .add(WorkspaceMemberEvent.inviteWorkspaceMemberByEmail(email));\n    // clear the email field after inviting\n    _emailController.clear();\n  }\n\n  void _onEmailChanged() {\n    setState(() {\n      _isInviteButtonEnabled = _emailController.text.isNotEmpty;\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/invitation/m_invite_member_by_link.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass MInviteMemberByLink extends StatelessWidget {\n  const MInviteMemberByLink({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        _Title(),\n        VSpace(theme.spacing.l),\n        _CopyLinkButton(),\n        VSpace(theme.spacing.l),\n        _Description(),\n      ],\n    );\n  }\n}\n\nclass _Title extends StatelessWidget {\n  const _Title();\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Text(\n      LocaleKeys.settings_appearance_members_inviteLinkToAddMember.tr(),\n      style: theme.textStyle.heading4.enhanced(\n        color: theme.textColorScheme.primary,\n      ),\n    );\n  }\n}\n\nclass _Description extends StatelessWidget {\n  const _Description();\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Text.rich(\n      TextSpan(\n        children: [\n          TextSpan(\n            text: LocaleKeys.settings_appearance_members_clickToCopyLink.tr(),\n            style: theme.textStyle.body.standard(\n              color: theme.textColorScheme.primary,\n            ),\n          ),\n          TextSpan(\n            text: ' ${LocaleKeys.settings_appearance_members_or.tr()} ',\n            style: theme.textStyle.body.standard(\n              color: theme.textColorScheme.primary,\n            ),\n          ),\n          TextSpan(\n            text: LocaleKeys.settings_appearance_members_generateANewLink.tr(),\n            style: theme.textStyle.body.standard(\n              color: theme.textColorScheme.action,\n            ),\n            mouseCursor: SystemMouseCursors.click,\n            recognizer: TapGestureRecognizer()\n              ..onTap = () => _onGenerateInviteLink(context),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Future<void> _onGenerateInviteLink(BuildContext context) async {\n    final inviteLink = context.read<WorkspaceMemberBloc>().state.inviteLink;\n    if (inviteLink != null) {\n      // show a dialog to confirm if the user wants to copy the link to the clipboard\n      await showConfirmDialog(\n        context: context,\n        style: ConfirmPopupStyle.cancelAndOk,\n        title: LocaleKeys.settings_appearance_members_resetInviteLink.tr(),\n        description: LocaleKeys\n            .settings_appearance_members_resetInviteLinkDescription\n            .tr(),\n        confirmLabel: LocaleKeys.settings_appearance_members_reset.tr(),\n        onConfirm: (_) {\n          context.read<WorkspaceMemberBloc>().add(\n                const WorkspaceMemberEvent.generateInviteLink(),\n              );\n        },\n        confirmButtonBuilder: (dialogContext) => AFFilledTextButton.destructive(\n          size: UniversalPlatform.isDesktop ? AFButtonSize.m : AFButtonSize.l,\n          text: LocaleKeys.settings_appearance_members_reset.tr(),\n          onTap: () {\n            context.read<WorkspaceMemberBloc>().add(\n                  const WorkspaceMemberEvent.generateInviteLink(),\n                );\n\n            Navigator.of(dialogContext).pop();\n          },\n        ),\n      );\n    } else {\n      context.read<WorkspaceMemberBloc>().add(\n            const WorkspaceMemberEvent.generateInviteLink(),\n          );\n    }\n  }\n}\n\nclass _CopyLinkButton extends StatelessWidget {\n  const _CopyLinkButton();\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return AFOutlinedTextButton.normal(\n      size: AFButtonSize.l,\n      alignment: Alignment.center,\n      text: LocaleKeys.button_copyLink.tr(),\n      textStyle: theme.textStyle.heading4.enhanced(\n        color: theme.textColorScheme.primary,\n      ),\n      onTap: () {\n        final link = context.read<WorkspaceMemberBloc>().state.inviteLink;\n        if (link != null) {\n          getIt<ClipboardService>().setData(\n            ClipboardServiceData(\n              plainText: link,\n            ),\n          );\n\n          showToastNotification(\n            message: LocaleKeys.shareAction_copyLinkSuccess.tr(),\n          );\n        } else {\n          showToastNotification(\n            message: LocaleKeys.settings_appearance_members_noInviteLink.tr(),\n            type: ToastificationType.error,\n          );\n        }\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/invitation/member_http_service.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:http/http.dart' as http;\n\nenum InviteCodeEndpoint {\n  getInviteCode,\n  deleteInviteCode,\n  generateInviteCode;\n\n  String get path {\n    switch (this) {\n      case InviteCodeEndpoint.getInviteCode:\n      case InviteCodeEndpoint.deleteInviteCode:\n      case InviteCodeEndpoint.generateInviteCode:\n        return '/api/workspace/{workspaceId}/invite-code';\n    }\n  }\n\n  String get method {\n    switch (this) {\n      case InviteCodeEndpoint.getInviteCode:\n        return 'GET';\n      case InviteCodeEndpoint.deleteInviteCode:\n        return 'DELETE';\n      case InviteCodeEndpoint.generateInviteCode:\n        return 'POST';\n    }\n  }\n\n  Uri uri(String baseUrl, String workspaceId) =>\n      Uri.parse(path.replaceAll('{workspaceId}', workspaceId)).replace(\n        scheme: Uri.parse(baseUrl).scheme,\n        host: Uri.parse(baseUrl).host,\n        port: Uri.parse(baseUrl).port,\n      );\n}\n\nclass MemberHttpService {\n  MemberHttpService({\n    required this.baseUrl,\n    required this.authToken,\n  });\n\n  final String baseUrl;\n  final String authToken;\n\n  final http.Client client = http.Client();\n\n  Map<String, String> get headers => {\n        'Content-Type': 'application/json',\n        'Authorization': 'Bearer $authToken',\n      };\n\n  /// Gets the invite code for a workspace\n  Future<FlowyResult<String, FlowyError>> getInviteCode({\n    required String workspaceId,\n  }) async {\n    final result = await _makeRequest(\n      endpoint: InviteCodeEndpoint.getInviteCode,\n      workspaceId: workspaceId,\n      errorMessage: 'Failed to get invite code',\n    );\n\n    try {\n      return result.fold(\n        (data) {\n          final code = data['data']['code'];\n          if (code is! String || code.isEmpty) {\n            return FlowyResult.failure(\n              FlowyError(msg: 'Failed to get invite code: $code'),\n            );\n          }\n          return FlowyResult.success(code);\n        },\n        (error) => FlowyResult.failure(error),\n      );\n    } catch (e) {\n      return FlowyResult.failure(\n        FlowyError(msg: 'Failed to get invite code: $e'),\n      );\n    }\n  }\n\n  /// Deletes the invite code for a workspace\n  Future<FlowyResult<bool, FlowyError>> deleteInviteCode({\n    required String workspaceId,\n  }) async {\n    final result = await _makeRequest(\n      endpoint: InviteCodeEndpoint.deleteInviteCode,\n      workspaceId: workspaceId,\n      errorMessage: 'Failed to delete invite code',\n    );\n\n    return result.fold(\n      (data) => FlowyResult.success(true),\n      (error) => FlowyResult.failure(error),\n    );\n  }\n\n  /// Generates a new invite code for a workspace\n  ///\n  /// [workspaceId] - The ID of the workspace\n  Future<FlowyResult<String, FlowyError>> generateInviteCode({\n    required String workspaceId,\n    int? validityPeriodHours,\n  }) async {\n    final result = await _makeRequest(\n      endpoint: InviteCodeEndpoint.generateInviteCode,\n      workspaceId: workspaceId,\n      errorMessage: 'Failed to generate invite code',\n      body: {\n        'validity_period_hours': validityPeriodHours,\n      },\n    );\n\n    try {\n      return result.fold(\n        (data) => FlowyResult.success(data['data']['code'].toString()),\n        (error) => FlowyResult.failure(error),\n      );\n    } catch (e) {\n      return FlowyResult.failure(\n        FlowyError(msg: 'Failed to generate invite code: $e'),\n      );\n    }\n  }\n\n  /// Makes a request to the specified endpoint\n  Future<FlowyResult<dynamic, FlowyError>> _makeRequest({\n    required InviteCodeEndpoint endpoint,\n    required String workspaceId,\n    Map<String, dynamic>? body,\n    String errorMessage = 'Request failed',\n  }) async {\n    try {\n      final uri = endpoint.uri(baseUrl, workspaceId);\n      http.Response response;\n\n      switch (endpoint.method) {\n        case 'GET':\n          response = await client.get(\n            uri,\n            headers: headers,\n          );\n          break;\n        case 'DELETE':\n          response = await client.delete(\n            uri,\n            headers: headers,\n          );\n          break;\n        case 'POST':\n          response = await client.post(\n            uri,\n            headers: headers,\n            body: body != null ? jsonEncode(body) : null,\n          );\n          break;\n        default:\n          return FlowyResult.failure(\n            FlowyError(msg: 'Invalid request method: ${endpoint.method}'),\n          );\n      }\n\n      if (response.statusCode == 200) {\n        if (response.body.isNotEmpty) {\n          return FlowyResult.success(jsonDecode(response.body));\n        }\n        return FlowyResult.success(true);\n      } else {\n        final errorBody =\n            response.body.isNotEmpty ? jsonDecode(response.body) : {};\n\n        Log.info(\n          '${endpoint.name} request failed: ${response.statusCode}, $errorBody',\n        );\n\n        return FlowyResult.failure(\n          FlowyError(\n            msg: errorBody['msg'] ?? errorMessage,\n          ),\n        );\n      }\n    } catch (e) {\n      return FlowyResult.failure(\n        FlowyError(msg: 'Network error: ${e.toString()}'),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/shared/af_role_pb_extension.dart';\nimport 'package:appflowy/shared/af_user_profile_extension.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/invitation/member_http_service.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\nimport 'package:protobuf/protobuf.dart';\n\npart 'workspace_member_bloc.freezed.dart';\n\n// 1. get the workspace members\n// 2. display the content based on the user role\n//  Owner:\n//   - invite member button\n//   - delete member button\n//   - member list\n//  Member:\n//  Guest:\n//   - member list\nclass WorkspaceMemberBloc\n    extends Bloc<WorkspaceMemberEvent, WorkspaceMemberState> {\n  WorkspaceMemberBloc({\n    required this.userProfile,\n    String? workspaceId,\n    this.workspace,\n  })  : _userBackendService = UserBackendService(userId: userProfile.id),\n        super(WorkspaceMemberState.initial()) {\n    on<WorkspaceMemberEvent>((event, emit) async {\n      await event.when(\n        initial: () async => _onInitial(emit, workspaceId),\n        getWorkspaceMembers: () async => _onGetWorkspaceMembers(emit),\n        addWorkspaceMember: (email) async => _onAddWorkspaceMember(emit, email),\n        inviteWorkspaceMemberByEmail: (email) async =>\n            _onInviteWorkspaceMemberByEmail(emit, email),\n        removeWorkspaceMemberByEmail: (email) async =>\n            _onRemoveWorkspaceMemberByEmail(emit, email),\n        inviteWorkspaceMemberByLink: (link) async =>\n            _onInviteWorkspaceMemberByLink(emit, link),\n        generateInviteLink: () async => _onGenerateInviteLink(emit),\n        updateWorkspaceMember: (email, role) async =>\n            _onUpdateWorkspaceMember(emit, email, role),\n        updateSubscriptionInfo: (info) async =>\n            _onUpdateSubscriptionInfo(emit, info),\n        upgradePlan: () async => _onUpgradePlan(),\n        getInviteCode: () async => _onGetInviteCode(emit),\n        updateInviteLink: (inviteLink) async => emit(\n          state.copyWith(\n            inviteLink: inviteLink,\n          ),\n        ),\n      );\n    });\n  }\n\n  @override\n  Future<void> close() async {\n    _workspaceId.dispose();\n\n    await super.close();\n  }\n\n  final UserProfilePB userProfile;\n  final UserWorkspacePB? workspace;\n  final UserBackendService _userBackendService;\n\n  final ValueNotifier<String?> _workspaceId = ValueNotifier<String?>(null);\n  MemberHttpService? _memberHttpService;\n\n  Future<void> _onInitial(\n    Emitter<WorkspaceMemberState> emit,\n    String? workspaceId,\n  ) async {\n    _workspaceId.addListener(() {\n      if (!isClosed) {\n        add(const WorkspaceMemberEvent.getInviteCode());\n      }\n    });\n\n    await _setCurrentWorkspaceId(workspaceId);\n\n    final currentWorkspaceId = _workspaceId.value;\n    if (currentWorkspaceId == null) {\n      Log.error('Failed to get workspace members: workspaceId is null');\n      return;\n    }\n\n    final result =\n        await _userBackendService.getWorkspaceMembers(currentWorkspaceId);\n    final members = result.fold<List<WorkspaceMemberPB>>(\n      (s) => s.items,\n      (e) => [],\n    );\n    final myRole = _getMyRole(members);\n\n    if (myRole.isOwner) {\n      unawaited(_fetchWorkspaceSubscriptionInfo());\n    }\n\n    emit(\n      state.copyWith(\n        members: members,\n        myRole: myRole,\n        isLoading: false,\n        actionResult: WorkspaceMemberActionResult(\n          actionType: WorkspaceMemberActionType.get,\n          result: result,\n        ),\n      ),\n    );\n  }\n\n  Future<void> _onGetWorkspaceMembers(\n    Emitter<WorkspaceMemberState> emit,\n  ) async {\n    final workspaceId = _workspaceId.value;\n    if (workspaceId == null) {\n      Log.error('Failed to get workspace members: workspaceId is null');\n      return;\n    }\n\n    final result = await _userBackendService.getWorkspaceMembers(workspaceId);\n    final members = result.fold<List<WorkspaceMemberPB>>(\n      (s) => s.items,\n      (e) => [],\n    );\n    final myRole = _getMyRole(members);\n    emit(\n      state.copyWith(\n        members: members,\n        myRole: myRole,\n        actionResult: WorkspaceMemberActionResult(\n          actionType: WorkspaceMemberActionType.get,\n          result: result,\n        ),\n      ),\n    );\n  }\n\n  Future<void> _onAddWorkspaceMember(\n    Emitter<WorkspaceMemberState> emit,\n    String email,\n  ) async {\n    final workspaceId = _workspaceId.value;\n    if (workspaceId == null) {\n      Log.error('Failed to add workspace member by email: workspaceId is null');\n      return;\n    }\n\n    final result = await _userBackendService.addWorkspaceMember(\n      workspaceId,\n      email,\n    );\n    emit(\n      state.copyWith(\n        actionResult: WorkspaceMemberActionResult(\n          actionType: WorkspaceMemberActionType.addByEmail,\n          result: result,\n        ),\n      ),\n    );\n    // the addWorkspaceMember doesn't return the updated members,\n    //  so we need to get the members again\n    result.onSuccess((s) {\n      add(const WorkspaceMemberEvent.getWorkspaceMembers());\n    });\n  }\n\n  Future<void> _onInviteWorkspaceMemberByEmail(\n    Emitter<WorkspaceMemberState> emit,\n    String email,\n  ) async {\n    final workspaceId = _workspaceId.value;\n    if (workspaceId == null) {\n      Log.error(\n        'Failed to invite workspace member by email: workspaceId is null',\n      );\n      return;\n    }\n\n    final result = await _userBackendService.inviteWorkspaceMember(\n      workspaceId,\n      email,\n      role: AFRolePB.Member,\n    );\n    emit(\n      state.copyWith(\n        actionResult: WorkspaceMemberActionResult(\n          actionType: WorkspaceMemberActionType.inviteByEmail,\n          result: result,\n        ),\n      ),\n    );\n  }\n\n  Future<void> _onRemoveWorkspaceMemberByEmail(\n    Emitter<WorkspaceMemberState> emit,\n    String email,\n  ) async {\n    final workspaceId = _workspaceId.value;\n    if (workspaceId == null) {\n      Log.error(\n        'Failed to remove workspace member by email: workspaceId is null',\n      );\n      return;\n    }\n\n    final result = await _userBackendService.removeWorkspaceMember(\n      workspaceId,\n      email,\n    );\n    final members = result.fold(\n      (s) => state.members.where((e) => e.email != email).toList(),\n      (e) => state.members,\n    );\n    emit(\n      state.copyWith(\n        members: members,\n        actionResult: WorkspaceMemberActionResult(\n          actionType: WorkspaceMemberActionType.removeByEmail,\n          result: result,\n        ),\n      ),\n    );\n  }\n\n  Future<void> _onInviteWorkspaceMemberByLink(\n    Emitter<WorkspaceMemberState> emit,\n    String link,\n  ) async {}\n\n  Future<void> _onGenerateInviteLink(\n    Emitter<WorkspaceMemberState> emit,\n  ) async {\n    final workspaceId = _workspaceId.value;\n    if (workspaceId == null) {\n      Log.error('Failed to generate invite link: workspaceId is null');\n      return;\n    }\n\n    final resetInviteLink = state.inviteLink != null;\n\n    final result = await _memberHttpService?.generateInviteCode(\n      workspaceId: workspaceId,\n    );\n\n    await result?.fold(\n      (s) async {\n        final inviteLink = await _buildInviteLink(inviteCode: s);\n        emit(\n          state.copyWith(\n            inviteLink: inviteLink,\n            actionResult: WorkspaceMemberActionResult(\n              actionType: resetInviteLink\n                  ? WorkspaceMemberActionType.resetInviteLink\n                  : WorkspaceMemberActionType.generateInviteLink,\n              result: result,\n            ),\n          ),\n        );\n      },\n      (e) async {\n        Log.error('Failed to generate invite link: ${e.msg}', e);\n        emit(\n          state.copyWith(\n            actionResult: WorkspaceMemberActionResult(\n              actionType: resetInviteLink\n                  ? WorkspaceMemberActionType.resetInviteLink\n                  : WorkspaceMemberActionType.generateInviteLink,\n              result: result,\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Future<void> _onUpdateWorkspaceMember(\n    Emitter<WorkspaceMemberState> emit,\n    String email,\n    AFRolePB role,\n  ) async {\n    final workspaceId = _workspaceId.value;\n    if (workspaceId == null) {\n      Log.error('Failed to update workspace member: workspaceId is null');\n      return;\n    }\n\n    final result = await _userBackendService.updateWorkspaceMember(\n      workspaceId,\n      email,\n      role,\n    );\n    final members = result.fold(\n      (s) => state.members.map((e) {\n        if (e.email == email) {\n          e.freeze();\n          return e.rebuild((p0) => p0.role = role);\n        }\n        return e;\n      }).toList(),\n      (e) => state.members,\n    );\n    emit(\n      state.copyWith(\n        members: members,\n        actionResult: WorkspaceMemberActionResult(\n          actionType: WorkspaceMemberActionType.updateRole,\n          result: result,\n        ),\n      ),\n    );\n  }\n\n  Future<void> _onUpdateSubscriptionInfo(\n    Emitter<WorkspaceMemberState> emit,\n    WorkspaceSubscriptionInfoPB info,\n  ) async {\n    emit(state.copyWith(subscriptionInfo: info));\n  }\n\n  Future<void> _onUpgradePlan() async {\n    final workspaceId = _workspaceId.value;\n    if (workspaceId == null) {\n      Log.error('Failed to upgrade plan: workspaceId is null');\n      return;\n    }\n\n    final plan = state.subscriptionInfo?.plan;\n    if (plan == null) {\n      return Log.error('Failed to upgrade plan: plan is null');\n    }\n\n    if (plan == WorkspacePlanPB.FreePlan) {\n      final checkoutLink = await _userBackendService.createSubscription(\n        workspaceId,\n        SubscriptionPlanPB.Pro,\n      );\n\n      checkoutLink.fold(\n        (pl) => afLaunchUrlString(pl.paymentLink),\n        (f) => Log.error('Failed to create subscription: ${f.msg}', f),\n      );\n    }\n  }\n\n  Future<void> _onGetInviteCode(Emitter<WorkspaceMemberState> emit) async {\n    final baseUrl = await getAppFlowyCloudUrl();\n    final authToken = userProfile.authToken;\n    final workspaceId = _workspaceId.value;\n    if (authToken != null && workspaceId != null) {\n      _memberHttpService = MemberHttpService(\n        baseUrl: baseUrl,\n        authToken: authToken,\n      );\n      unawaited(\n        _memberHttpService?.getInviteCode(workspaceId: workspaceId).fold(\n          (s) async {\n            final inviteLink = await _buildInviteLink(inviteCode: s);\n            if (!isClosed) {\n              add(WorkspaceMemberEvent.updateInviteLink(inviteLink));\n            }\n          },\n          (e) {},\n        ),\n      );\n    } else {\n      Log.error('Failed to get auth token');\n    }\n  }\n\n  AFRolePB _getMyRole(List<WorkspaceMemberPB> members) {\n    final role = members\n        .firstWhereOrNull(\n          (e) => e.email == userProfile.email,\n        )\n        ?.role;\n    if (role == null) {\n      Log.error('Failed to get my role');\n      return AFRolePB.Guest;\n    }\n    return role;\n  }\n\n  Future<void> _setCurrentWorkspaceId(String? workspaceId) async {\n    if (workspace != null) {\n      _workspaceId.value = workspace!.workspaceId;\n    } else if (workspaceId != null && workspaceId.isNotEmpty) {\n      _workspaceId.value = workspaceId;\n    } else {\n      final currentWorkspace = await FolderEventReadCurrentWorkspace().send();\n      currentWorkspace.fold((s) {\n        _workspaceId.value = s.id;\n      }, (e) {\n        assert(false, 'Failed to read current workspace: $e');\n        Log.error('Failed to read current workspace: $e');\n      });\n    }\n  }\n\n  Future<void> _fetchWorkspaceSubscriptionInfo() async {\n    final workspaceId = _workspaceId.value;\n    if (workspaceId == null) {\n      Log.error('Failed to fetch subscription info: workspaceId is null');\n      return;\n    }\n\n    final result = await UserBackendService.getWorkspaceSubscriptionInfo(\n      workspaceId,\n    );\n\n    result.fold(\n      (info) {\n        if (!isClosed) {\n          add(WorkspaceMemberEvent.updateSubscriptionInfo(info));\n        }\n      },\n      (f) => Log.error('Failed to fetch subscription info: ${f.msg}', f),\n    );\n  }\n\n  Future<String> _buildInviteLink({required String inviteCode}) async {\n    final baseUrl = await getAppFlowyShareDomain();\n    final authToken = userProfile.authToken;\n    if (authToken != null) {\n      return '$baseUrl/app/invited/$inviteCode';\n    }\n    return '';\n  }\n}\n\n@freezed\nclass WorkspaceMemberEvent with _$WorkspaceMemberEvent {\n  const factory WorkspaceMemberEvent.initial() = Initial;\n\n  // Members related events\n  const factory WorkspaceMemberEvent.getWorkspaceMembers() =\n      GetWorkspaceMembers;\n  const factory WorkspaceMemberEvent.addWorkspaceMember(String email) =\n      AddWorkspaceMember;\n  const factory WorkspaceMemberEvent.inviteWorkspaceMemberByEmail(\n    String email,\n  ) = InviteWorkspaceMemberByEmail;\n  const factory WorkspaceMemberEvent.removeWorkspaceMemberByEmail(\n    String email,\n  ) = RemoveWorkspaceMemberByEmail;\n  const factory WorkspaceMemberEvent.updateWorkspaceMember(\n    String email,\n    AFRolePB role,\n  ) = UpdateWorkspaceMember;\n\n  // Subscription related events\n  const factory WorkspaceMemberEvent.updateSubscriptionInfo(\n    WorkspaceSubscriptionInfoPB subscriptionInfo,\n  ) = UpdateSubscriptionInfo;\n  const factory WorkspaceMemberEvent.upgradePlan() = UpgradePlan;\n\n  // Invite link related events\n  const factory WorkspaceMemberEvent.inviteWorkspaceMemberByLink(\n    String link,\n  ) = InviteWorkspaceMemberByLink;\n  const factory WorkspaceMemberEvent.getInviteCode() = GetInviteCode;\n  const factory WorkspaceMemberEvent.generateInviteLink() = GenerateInviteLink;\n  const factory WorkspaceMemberEvent.updateInviteLink(String inviteLink) =\n      UpdateInviteLink;\n}\n\nenum WorkspaceMemberActionType {\n  none,\n  get,\n  // this event will send an invitation to the member\n  inviteByEmail,\n  inviteByLink,\n  generateInviteLink,\n  resetInviteLink,\n  // this event will add the member without sending an invitation\n  addByEmail,\n  removeByEmail,\n  updateRole,\n}\n\nclass WorkspaceMemberActionResult {\n  const WorkspaceMemberActionResult({\n    required this.actionType,\n    required this.result,\n  });\n\n  final WorkspaceMemberActionType actionType;\n  final FlowyResult<void, FlowyError> result;\n}\n\n@freezed\nclass WorkspaceMemberState with _$WorkspaceMemberState {\n  const WorkspaceMemberState._();\n\n  const factory WorkspaceMemberState({\n    @Default([]) List<WorkspaceMemberPB> members,\n    @Default(AFRolePB.Guest) AFRolePB myRole,\n    @Default(null) WorkspaceMemberActionResult? actionResult,\n    @Default(true) bool isLoading,\n    @Default(null) WorkspaceSubscriptionInfoPB? subscriptionInfo,\n    @Default(null) String? inviteLink,\n  }) = _WorkspaceMemberState;\n\n  factory WorkspaceMemberState.initial() => const WorkspaceMemberState();\n\n  @override\n  int get hashCode => runtimeType.hashCode;\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n\n    return other is WorkspaceMemberState &&\n        other.members == members &&\n        other.myRole == myRole &&\n        other.subscriptionInfo == subscriptionInfo &&\n        other.inviteLink == inviteLink &&\n        identical(other.actionResult, actionResult);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/shared/af_role_pb_extension.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/invitation/invite_member_by_email.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/invitation/invite_member_by_link.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass WorkspaceMembersPage extends StatelessWidget {\n  const WorkspaceMembersPage({\n    super.key,\n    required this.userProfile,\n    required this.workspaceId,\n  });\n\n  final UserProfilePB userProfile;\n  final String workspaceId;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider<WorkspaceMemberBloc>(\n      create: (context) => WorkspaceMemberBloc(userProfile: userProfile)\n        ..add(const WorkspaceMemberEvent.initial())\n        ..add(const WorkspaceMemberEvent.getInviteCode()),\n      child: BlocConsumer<WorkspaceMemberBloc, WorkspaceMemberState>(\n        listener: _showResultDialog,\n        builder: (context, state) {\n          return SettingsBody(\n            title: LocaleKeys.settings_appearance_members_title.tr(),\n            // Enable it when the backend support admin panel\n            // descriptionBuilder: _buildDescription,\n            autoSeparate: false,\n            children: [\n              if (state.myRole.canInvite) ...[\n                const InviteMemberByLink(),\n                const SettingsCategorySpacer(),\n                const InviteMemberByEmail(),\n                const SettingsCategorySpacer(\n                  bottomSpacing: 0,\n                ),\n              ],\n              if (state.members.isNotEmpty)\n                _MemberList(\n                  members: state.members,\n                  userProfile: userProfile,\n                  myRole: state.myRole,\n                ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  // Enable it when the backend support admin panel\n  // Widget _buildDescription(BuildContext context) {\n  //   final theme = AppFlowyTheme.of(context);\n  //   return Text.rich(\n  //     TextSpan(\n  //       children: [\n  //         TextSpan(\n  //           text:\n  //               '${LocaleKeys.settings_appearance_members_memberPageDescription1.tr()} ',\n  //           style: theme.textStyle.caption.standard(\n  //             color: theme.textColorScheme.secondary,\n  //           ),\n  //         ),\n  //         TextSpan(\n  //           text: LocaleKeys.settings_appearance_members_adminPanel.tr(),\n  //           style: theme.textStyle.caption.underline(\n  //             color: theme.textColorScheme.secondary,\n  //           ),\n  //           mouseCursor: SystemMouseCursors.click,\n  //           recognizer: TapGestureRecognizer()\n  //             ..onTap = () async {\n  //               final baseUrl = await getAppFlowyCloudUrl();\n  //               await afLaunchUrlString(baseUrl);\n  //             },\n  //         ),\n  //         TextSpan(\n  //           text:\n  //               ' ${LocaleKeys.settings_appearance_members_memberPageDescription2.tr()} ',\n  //           style: theme.textStyle.caption.standard(\n  //             color: theme.textColorScheme.secondary,\n  //           ),\n  //         ),\n  //       ],\n  //     ),\n  //   );\n  // }\n\n  // Widget _showMemberLimitWarning(\n  //   BuildContext context,\n  //   WorkspaceMemberState state,\n  // ) {\n  //   // We promise that state.actionResult != null before calling\n  //   // this method\n  //   final actionResult = state.actionResult!.result;\n  //   final actionType = state.actionResult!.actionType;\n\n  //   if (actionType == WorkspaceMemberActionType.inviteByEmail &&\n  //       actionResult.isFailure) {\n  //     final error = actionResult.getFailure().code;\n  //     if (error == ErrorCode.WorkspaceMemberLimitExceeded) {\n  //       return Row(\n  //         children: [\n  //           const FlowySvg(\n  //             FlowySvgs.warning_s,\n  //             blendMode: BlendMode.dst,\n  //             size: Size.square(20),\n  //           ),\n  //           const HSpace(12),\n  //           Expanded(\n  //             child: RichText(\n  //               text: TextSpan(\n  //                 children: [\n  //                   if (state.subscriptionInfo?.plan ==\n  //                       WorkspacePlanPB.ProPlan) ...[\n  //                     TextSpan(\n  //                       text: LocaleKeys\n  //                           .settings_appearance_members_memberLimitExceededPro\n  //                           .tr(),\n  //                       style: TextStyle(\n  //                         fontSize: 14,\n  //                         fontWeight: FontWeight.w400,\n  //                         color: AFThemeExtension.of(context).strongText,\n  //                       ),\n  //                     ),\n  //                     WidgetSpan(\n  //                       child: MouseRegion(\n  //                         cursor: SystemMouseCursors.click,\n  //                         child: GestureDetector(\n  //                           // Hardcoded support email, in the future we might\n  //                           // want to add this to an environment variable\n  //                           onTap: () async => afLaunchUrlString(\n  //                             'mailto:support@appflowy.io',\n  //                           ),\n  //                           child: FlowyText(\n  //                             LocaleKeys\n  //                                 .settings_appearance_members_memberLimitExceededProContact\n  //                                 .tr(),\n  //                             fontSize: 14,\n  //                             fontWeight: FontWeight.w400,\n  //                             color: Theme.of(context).colorScheme.primary,\n  //                           ),\n  //                         ),\n  //                       ),\n  //                     ),\n  //                   ] else ...[\n  //                     TextSpan(\n  //                       text: LocaleKeys\n  //                           .settings_appearance_members_memberLimitExceeded\n  //                           .tr(),\n  //                       style: TextStyle(\n  //                         fontSize: 14,\n  //                         fontWeight: FontWeight.w400,\n  //                         color: AFThemeExtension.of(context).strongText,\n  //                       ),\n  //                     ),\n  //                     WidgetSpan(\n  //                       child: MouseRegion(\n  //                         cursor: SystemMouseCursors.click,\n  //                         child: GestureDetector(\n  //                           onTap: () => context\n  //                               .read<WorkspaceMemberBloc>()\n  //                               .add(const WorkspaceMemberEvent.upgradePlan()),\n  //                           child: FlowyText(\n  //                             LocaleKeys\n  //                                 .settings_appearance_members_memberLimitExceededUpgrade\n  //                                 .tr(),\n  //                             fontSize: 14,\n  //                             fontWeight: FontWeight.w400,\n  //                             color: Theme.of(context).colorScheme.primary,\n  //                           ),\n  //                         ),\n  //                       ),\n  //                     ),\n  //                   ],\n  //                 ],\n  //               ),\n  //             ),\n  //           ),\n  //         ],\n  //       );\n  //     }\n  //   }\n\n  //   return const SizedBox.shrink();\n  // }\n\n  void _showResultDialog(BuildContext context, WorkspaceMemberState state) {\n    final actionResult = state.actionResult;\n    if (actionResult == null) {\n      return;\n    }\n\n    final actionType = actionResult.actionType;\n    final result = actionResult.result;\n\n    // only show the result dialog when the action is WorkspaceMemberActionType.add\n    if (actionType == WorkspaceMemberActionType.addByEmail) {\n      result.fold(\n        (s) {\n          showToastNotification(\n            message:\n                LocaleKeys.settings_appearance_members_addMemberSuccess.tr(),\n          );\n        },\n        (f) {\n          Log.error('add workspace member failed: $f');\n          final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded\n              ? LocaleKeys.settings_appearance_members_memberLimitExceeded.tr()\n              : LocaleKeys.settings_appearance_members_failedToAddMember.tr();\n          showDialog(\n            context: context,\n            builder: (context) => NavigatorOkCancelDialog(message: message),\n          );\n        },\n      );\n    } else if (actionType == WorkspaceMemberActionType.inviteByEmail) {\n      result.fold(\n        (s) {\n          showToastNotification(\n            message:\n                LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(),\n          );\n        },\n        (f) {\n          Log.error('invite workspace member failed: $f');\n          final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded\n              ? LocaleKeys.settings_appearance_members_inviteFailedMemberLimit\n                  .tr()\n              : LocaleKeys.settings_appearance_members_failedToInviteMember\n                  .tr();\n          showConfirmDialog(\n            context: context,\n            title: LocaleKeys\n                .settings_appearance_members_inviteFailedDialogTitle\n                .tr(),\n            description: message,\n            confirmLabel: LocaleKeys\n                .settings_appearance_members_memberLimitExceededUpgrade\n                .tr(),\n            onConfirm: (_) => context\n                .read<WorkspaceMemberBloc>()\n                .add(const WorkspaceMemberEvent.upgradePlan()),\n          );\n        },\n      );\n    } else if (actionType == WorkspaceMemberActionType.generateInviteLink) {\n      result.fold(\n        (s) async {\n          showToastNotification(\n            message: LocaleKeys\n                .settings_appearance_members_generatedLinkSuccessfully\n                .tr(),\n          );\n\n          // copy the invite link to the clipboard\n          final inviteLink = state.inviteLink;\n          if (inviteLink != null) {\n            await getIt<ClipboardService>().setPlainText(inviteLink);\n            Future.delayed(const Duration(milliseconds: 200), () {\n              showToastNotification(\n                message: LocaleKeys.shareAction_copyLinkSuccess.tr(),\n              );\n            });\n          }\n        },\n        (f) {\n          Log.error('generate invite link failed: $f');\n          showToastNotification(\n            type: ToastificationType.error,\n            message:\n                LocaleKeys.settings_appearance_members_generatedLinkFailed.tr(),\n          );\n        },\n      );\n    } else if (actionType == WorkspaceMemberActionType.resetInviteLink) {\n      result.fold(\n        (s) async {\n          showToastNotification(\n            message: LocaleKeys\n                .settings_appearance_members_resetLinkSuccessfully\n                .tr(),\n          );\n\n          // copy the invite link to the clipboard\n          final inviteLink = state.inviteLink;\n          if (inviteLink != null) {\n            await getIt<ClipboardService>().setPlainText(inviteLink);\n            Future.delayed(const Duration(milliseconds: 200), () {\n              showToastNotification(\n                message: LocaleKeys.shareAction_copyLinkSuccess.tr(),\n              );\n            });\n          }\n        },\n        (f) {\n          Log.error('generate invite link failed: $f');\n          showToastNotification(\n            type: ToastificationType.error,\n            message:\n                LocaleKeys.settings_appearance_members_resetLinkFailed.tr(),\n          );\n        },\n      );\n    }\n  }\n}\n\nclass _MemberList extends StatelessWidget {\n  const _MemberList({\n    required this.members,\n    required this.myRole,\n    required this.userProfile,\n  });\n\n  final List<WorkspaceMemberPB> members;\n  final AFRolePB myRole;\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return SeparatedColumn(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      separatorBuilder: () => Divider(\n        color: theme.borderColorScheme.primary,\n      ),\n      children: [\n        const _MemberListHeader(),\n        ...members.map(\n          (member) => _MemberItem(\n            member: member,\n            myRole: myRole,\n            userProfile: userProfile,\n          ),\n        ),\n      ],\n    );\n  }\n}\n\nclass _MemberListHeader extends StatelessWidget {\n  const _MemberListHeader();\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Row(\n      children: [\n        Expanded(\n          flex: 4,\n          child: Text(\n            LocaleKeys.settings_appearance_members_user.tr(),\n            style: theme.textStyle.body.standard(\n              color: theme.textColorScheme.secondary,\n            ),\n          ),\n        ),\n        Expanded(\n          flex: 2,\n          child: Text(\n            LocaleKeys.settings_appearance_members_role.tr(),\n            style: theme.textStyle.body.standard(\n              color: theme.textColorScheme.secondary,\n            ),\n          ),\n        ),\n        Expanded(\n          flex: 3,\n          child: Text(\n            LocaleKeys.settings_accountPage_email_title.tr(),\n            style: theme.textStyle.body.standard(\n              color: theme.textColorScheme.secondary,\n            ),\n          ),\n        ),\n        const HSpace(28.0),\n      ],\n    );\n  }\n}\n\nclass _MemberItem extends StatelessWidget {\n  const _MemberItem({\n    required this.member,\n    required this.myRole,\n    required this.userProfile,\n  });\n\n  final WorkspaceMemberPB member;\n  final AFRolePB myRole;\n  final UserProfilePB userProfile;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Row(\n      children: [\n        Expanded(\n          flex: 4,\n          child: Row(\n            children: [\n              UserAvatar(\n                iconUrl: member.avatarUrl,\n                name: member.name,\n                size: AFAvatarSize.s,\n              ),\n              HSpace(8),\n              Flexible(\n                child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    Text(\n                      member.name,\n                      style: theme.textStyle.body.enhanced(\n                        color: theme.textColorScheme.primary,\n                      ),\n                      maxLines: 1,\n                      overflow: TextOverflow.ellipsis,\n                    ),\n                    Text(\n                      _formatJoinedDate(member.joinedAt.toInt()),\n                      style: theme.textStyle.caption.standard(\n                        color: theme.textColorScheme.secondary,\n                      ),\n                      maxLines: 1,\n                      overflow: TextOverflow.ellipsis,\n                    ),\n                  ],\n                ),\n              ),\n              HSpace(8),\n            ],\n          ),\n        ),\n        Expanded(\n          flex: 2,\n          child: member.role.isOwner || !myRole.canUpdate\n              ? Text(\n                  member.role.description,\n                  style: theme.textStyle.body.standard(\n                    color: theme.textColorScheme.primary,\n                  ),\n                )\n              : _MemberRoleActionList(\n                  member: member,\n                ),\n        ),\n        Expanded(\n          flex: 3,\n          child: FlowyTooltip(\n            message: member.email,\n            child: Text(\n              member.email,\n              maxLines: 1,\n              overflow: TextOverflow.ellipsis,\n              style: theme.textStyle.body.standard(\n                color: theme.textColorScheme.primary,\n              ),\n            ),\n          ),\n        ),\n        myRole.canDelete &&\n                member.email != userProfile.email // can't delete self\n            ? _MemberMoreActionList(member: member)\n            : const HSpace(28.0),\n      ],\n    );\n  }\n\n  String _formatJoinedDate(int joinedAt) {\n    final date = DateTime.fromMillisecondsSinceEpoch(joinedAt * 1000);\n    return 'Joined on ${DateFormat('MMM d, y').format(date)}';\n  }\n}\n\nenum _MemberMoreAction {\n  delete,\n}\n\nclass _MemberMoreActionList extends StatelessWidget {\n  const _MemberMoreActionList({\n    required this.member,\n  });\n\n  final WorkspaceMemberPB member;\n\n  @override\n  Widget build(BuildContext context) {\n    return PopoverActionList<_MemberMoreActionWrapper>(\n      asBarrier: true,\n      direction: PopoverDirection.bottomWithCenterAligned,\n      actions: _MemberMoreAction.values\n          .map((e) => _MemberMoreActionWrapper(e, member))\n          .toList(),\n      buildChild: (controller) {\n        return FlowyButton(\n          useIntrinsicWidth: true,\n          text: const FlowySvg(\n            FlowySvgs.three_dots_s,\n          ),\n          onTap: () {\n            controller.show();\n          },\n        );\n      },\n      onSelected: (action, controller) {\n        switch (action.inner) {\n          case _MemberMoreAction.delete:\n            showCancelAndConfirmDialog(\n              context: context,\n              title: LocaleKeys.settings_appearance_members_removeMember.tr(),\n              description: LocaleKeys\n                  .settings_appearance_members_areYouSureToRemoveMember\n                  .tr(),\n              confirmLabel: LocaleKeys.button_yes.tr(),\n              onConfirm: (_) => context.read<WorkspaceMemberBloc>().add(\n                    WorkspaceMemberEvent.removeWorkspaceMemberByEmail(\n                      action.member.email,\n                    ),\n                  ),\n            );\n            break;\n        }\n        controller.close();\n      },\n    );\n  }\n}\n\nclass _MemberMoreActionWrapper extends ActionCell {\n  _MemberMoreActionWrapper(this.inner, this.member);\n\n  final _MemberMoreAction inner;\n  final WorkspaceMemberPB member;\n\n  @override\n  String get name {\n    switch (inner) {\n      case _MemberMoreAction.delete:\n        return LocaleKeys.settings_appearance_members_removeFromWorkspace.tr();\n    }\n  }\n}\n\nclass _MemberRoleActionList extends StatelessWidget {\n  const _MemberRoleActionList({\n    required this.member,\n  });\n\n  final WorkspaceMemberPB member;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Text(\n      member.role.description,\n      style: theme.textStyle.body.standard(\n        color: theme.textColorScheme.primary,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/env/env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/shared/share/constants.dart';\nimport 'package:appflowy/shared/error_page/error_page.dart';\nimport 'package:appflowy/workspace/application/settings/appflowy_cloud_setting_bloc.dart';\nimport 'package:appflowy/workspace/application/settings/appflowy_cloud_urls_bloc.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/_restart_app_button.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/web_url_hint_widget.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass AppFlowyCloudViewSetting extends StatelessWidget {\n  const AppFlowyCloudViewSetting({\n    super.key,\n    this.serverURL = kAppflowyCloudUrl,\n    this.authenticatorType = AuthenticatorType.appflowyCloud,\n    required this.restartAppFlowy,\n  });\n\n  final String serverURL;\n  final AuthenticatorType authenticatorType;\n  final VoidCallback restartAppFlowy;\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder<FlowyResult<CloudSettingPB, FlowyError>>(\n      future: UserEventGetCloudConfig().send(),\n      builder: (context, snapshot) {\n        if (snapshot.data != null &&\n            snapshot.connectionState == ConnectionState.done) {\n          return snapshot.data!.fold(\n            (setting) => _renderContent(context, setting),\n            (err) => FlowyErrorPage.message(err.toString(), howToFix: \"\"),\n          );\n        }\n\n        return const Center(\n          child: CircularProgressIndicator(),\n        );\n      },\n    );\n  }\n\n  BlocProvider<AppFlowyCloudSettingBloc> _renderContent(\n    BuildContext context,\n    CloudSettingPB setting,\n  ) {\n    return BlocProvider(\n      create: (context) => AppFlowyCloudSettingBloc(setting)\n        ..add(const AppFlowyCloudSettingEvent.initial()),\n      child: BlocBuilder<AppFlowyCloudSettingBloc, AppFlowyCloudSettingState>(\n        builder: (context, state) {\n          return Column(\n            children: [\n              const VSpace(8),\n              if (state.workspaceType == WorkspaceTypePB.ServerW)\n                const AppFlowyCloudEnableSync(),\n              const VSpace(6),\n              // const AppFlowyCloudSyncLogEnabled(),\n              const VSpace(12),\n              RestartButton(\n                onClick: () {\n                  NavigatorAlertDialog(\n                    title: LocaleKeys.settings_menu_restartAppTip.tr(),\n                    confirm: () async {\n                      await useBaseWebDomain(\n                        ShareConstants.defaultBaseWebDomain,\n                      );\n                      await useAppFlowyBetaCloudWithURL(\n                        serverURL,\n                        authenticatorType,\n                      );\n                      restartAppFlowy();\n                    },\n                  ).show(context);\n                },\n                showRestartHint: state.showRestartHint,\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass CustomAppFlowyCloudView extends StatelessWidget {\n  const CustomAppFlowyCloudView({required this.restartAppFlowy, super.key});\n\n  final VoidCallback restartAppFlowy;\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder<FlowyResult<CloudSettingPB, FlowyError>>(\n      future: UserEventGetCloudConfig().send(),\n      builder: (context, snapshot) {\n        if (snapshot.data != null &&\n            snapshot.connectionState == ConnectionState.done) {\n          return snapshot.data!.fold(\n            (setting) => _renderContent(setting),\n            (err) => FlowyErrorPage.message(err.toString(), howToFix: \"\"),\n          );\n        } else {\n          return const Center(\n            child: CircularProgressIndicator(),\n          );\n        }\n      },\n    );\n  }\n\n  BlocProvider<AppFlowyCloudSettingBloc> _renderContent(\n    CloudSettingPB setting,\n  ) {\n    final List<Widget> children = [];\n    children.addAll([\n      const AppFlowyCloudEnableSync(),\n      // const AppFlowyCloudSyncLogEnabled(),\n      const VSpace(40),\n    ]);\n\n    // If the enableCustomCloud flag is true, then the user can dynamically configure cloud settings. Otherwise, the user cannot dynamically configure cloud settings.\n    if (Env.enableCustomCloud) {\n      children.add(\n        AppFlowyCloudURLs(restartAppFlowy: () => restartAppFlowy()),\n      );\n    } else {\n      children.add(\n        Row(\n          children: [\n            FlowyText(LocaleKeys.settings_menu_cloudServerType.tr()),\n            const Spacer(),\n            const FlowyText(Env.afCloudUrl),\n          ],\n        ),\n      );\n    }\n    return BlocProvider(\n      create: (context) => AppFlowyCloudSettingBloc(setting)\n        ..add(const AppFlowyCloudSettingEvent.initial()),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: children,\n      ),\n    );\n  }\n}\n\nclass AppFlowyCloudURLs extends StatelessWidget {\n  const AppFlowyCloudURLs({super.key, required this.restartAppFlowy});\n\n  final VoidCallback restartAppFlowy;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) =>\n          AppFlowyCloudURLsBloc()..add(const AppFlowyCloudURLsEvent.initial()),\n      child: BlocListener<AppFlowyCloudURLsBloc, AppFlowyCloudURLsState>(\n        listener: (context, state) async {\n          if (state.restartApp) {\n            restartAppFlowy();\n          }\n        },\n        child: BlocBuilder<AppFlowyCloudURLsBloc, AppFlowyCloudURLsState>(\n          builder: (context, state) {\n            return Column(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                const AppFlowySelfHostTip(),\n                const VSpace(12),\n                CloudURLInput(\n                  title: LocaleKeys.settings_menu_cloudURL.tr(),\n                  url: state.config.base_url,\n                  hint: LocaleKeys.settings_menu_cloudURLHint.tr(),\n                  onChanged: (text) {\n                    context.read<AppFlowyCloudURLsBloc>().add(\n                          AppFlowyCloudURLsEvent.updateServerUrl(\n                            text,\n                          ),\n                        );\n                  },\n                ),\n                const VSpace(8),\n                CloudURLInput(\n                  title: LocaleKeys.settings_menu_webURL.tr(),\n                  url: state.config.base_web_domain,\n                  hint: LocaleKeys.settings_menu_webURLHint.tr(),\n                  hintBuilder: (context) => const WebUrlHintWidget(),\n                  onChanged: (text) {\n                    context.read<AppFlowyCloudURLsBloc>().add(\n                          AppFlowyCloudURLsEvent.updateBaseWebDomain(\n                            text,\n                          ),\n                        );\n                  },\n                ),\n                const VSpace(12),\n                RestartButton(\n                  onClick: () {\n                    NavigatorAlertDialog(\n                      title: LocaleKeys.settings_menu_restartAppTip.tr(),\n                      confirm: () {\n                        context.read<AppFlowyCloudURLsBloc>().add(\n                              const AppFlowyCloudURLsEvent.confirmUpdate(),\n                            );\n                      },\n                    ).show(context);\n                  },\n                  showRestartHint: state.showRestartHint,\n                ),\n              ],\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass AppFlowySelfHostTip extends StatelessWidget {\n  const AppFlowySelfHostTip({super.key});\n\n  final url =\n      \"https://docs.appflowy.io/docs/guides/appflowy/self-hosting-appflowy#build-appflowy-with-a-self-hosted-server\";\n\n  @override\n  Widget build(BuildContext context) {\n    return Opacity(\n      opacity: 0.6,\n      child: RichText(\n        text: TextSpan(\n          children: <TextSpan>[\n            TextSpan(\n              text: LocaleKeys.settings_menu_selfHostStart.tr(),\n              style: Theme.of(context).textTheme.bodySmall!,\n            ),\n            TextSpan(\n              text: \" ${LocaleKeys.settings_menu_selfHostContent.tr()} \",\n              style: Theme.of(context).textTheme.bodyMedium!.copyWith(\n                    fontSize: FontSizes.s14,\n                    color: Theme.of(context).colorScheme.primary,\n                    decoration: TextDecoration.underline,\n                  ),\n              recognizer: TapGestureRecognizer()\n                ..onTap = () => afLaunchUrlString(url),\n            ),\n            TextSpan(\n              text: LocaleKeys.settings_menu_selfHostEnd.tr(),\n              style: Theme.of(context).textTheme.bodySmall!,\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\n@visibleForTesting\nclass CloudURLInput extends StatefulWidget {\n  const CloudURLInput({\n    super.key,\n    required this.title,\n    required this.url,\n    required this.hint,\n    required this.onChanged,\n    this.hintBuilder,\n  });\n\n  final String title;\n  final String url;\n  final String hint;\n  final ValueChanged<String> onChanged;\n  final WidgetBuilder? hintBuilder;\n\n  @override\n  CloudURLInputState createState() => CloudURLInputState();\n}\n\nclass CloudURLInputState extends State<CloudURLInput> {\n  late TextEditingController _controller;\n\n  @override\n  void initState() {\n    super.initState();\n    _controller = TextEditingController(text: widget.url);\n  }\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      mainAxisSize: MainAxisSize.min,\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        _buildHint(context),\n        SizedBox(\n          height: 28,\n          child: TextField(\n            controller: _controller,\n            style: Theme.of(context).textTheme.titleMedium!.copyWith(\n                  fontSize: 14,\n                  fontWeight: FontWeight.w400,\n                ),\n            decoration: InputDecoration(\n              enabledBorder: UnderlineInputBorder(\n                borderSide: BorderSide(\n                  color: AFThemeExtension.of(context).onBackground,\n                ),\n              ),\n              focusedBorder: UnderlineInputBorder(\n                borderSide: BorderSide(\n                  color: Theme.of(context).colorScheme.primary,\n                ),\n              ),\n              hintText: widget.hint,\n              errorText: context.read<AppFlowyCloudURLsBloc>().state.urlError,\n            ),\n            onChanged: widget.onChanged,\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildHint(BuildContext context) {\n    final children = <Widget>[\n      FlowyText(\n        widget.title,\n        fontSize: 12,\n      ),\n    ];\n\n    if (widget.hintBuilder != null) {\n      children.add(widget.hintBuilder!(context));\n    }\n\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      children: children,\n    );\n  }\n}\n\nclass AppFlowyCloudEnableSync extends StatelessWidget {\n  const AppFlowyCloudEnableSync({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<AppFlowyCloudSettingBloc, AppFlowyCloudSettingState>(\n      builder: (context, state) {\n        return Row(\n          children: [\n            FlowyText.medium(LocaleKeys.settings_menu_enableSync.tr()),\n            const Spacer(),\n            Toggle(\n              value: state.setting.enableSync,\n              onChanged: (value) => context\n                  .read<AppFlowyCloudSettingBloc>()\n                  .add(AppFlowyCloudSettingEvent.enableSync(value)),\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n\nclass AppFlowyCloudSyncLogEnabled extends StatelessWidget {\n  const AppFlowyCloudSyncLogEnabled({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<AppFlowyCloudSettingBloc, AppFlowyCloudSettingState>(\n      builder: (context, state) {\n        return Row(\n          children: [\n            FlowyText.medium(LocaleKeys.settings_menu_enableSyncLog.tr()),\n            const Spacer(),\n            Toggle(\n              value: state.isSyncLogEnabled,\n              onChanged: (value) {\n                if (value) {\n                  showCancelAndConfirmDialog(\n                    context: context,\n                    title: LocaleKeys.settings_menu_enableSyncLog.tr(),\n                    description:\n                        LocaleKeys.settings_menu_enableSyncLogWarning.tr(),\n                    confirmLabel: LocaleKeys.button_confirm.tr(),\n                    onConfirm: (_) {\n                      context\n                          .read<AppFlowyCloudSettingBloc>()\n                          .add(AppFlowyCloudSettingEvent.enableSyncLog(value));\n                    },\n                  );\n                } else {\n                  context\n                      .read<AppFlowyCloudSettingBloc>()\n                      .add(AppFlowyCloudSettingEvent.enableSyncLog(value));\n                }\n              },\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n\nclass BillingGateGuard extends StatelessWidget {\n  const BillingGateGuard({required this.builder, super.key});\n\n  final Widget Function(BuildContext context) builder;\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: isBillingEnabled(),\n      builder: (context, snapshot) {\n        final isBillingEnabled = snapshot.data ?? false;\n        if (isBillingEnabled &&\n            snapshot.connectionState == ConnectionState.done) {\n          return builder(context);\n        }\n\n        // If the billing is not enabled, show nothing\n        return const SizedBox.shrink();\n      },\n    );\n  }\n}\n\nFuture<bool> isBillingEnabled() async {\n  final result = await UserEventGetCloudConfig().send();\n  return result.fold(\n    (cloudSetting) {\n      final whiteList = [\n        \"https://beta.appflowy.cloud\",\n        \"https://test.appflowy.cloud\",\n      ];\n      if (kDebugMode) {\n        whiteList.add(\"http://localhost:8000\");\n      }\n\n      final isWhiteListed = whiteList.contains(cloudSetting.serverUrl);\n      if (!isWhiteListed) {\n        Log.warn(\"Billing is not enabled for server ${cloudSetting.serverUrl}\");\n      }\n      return isWhiteListed;\n    },\n    (err) {\n      Log.error(\"Failed to get cloud config: $err\");\n      return false;\n    },\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/env/env.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/widgets.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/application/settings/cloud_setting_bloc.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/setting_local_cloud.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:go_router/go_router.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nimport 'setting_appflowy_cloud.dart';\n\nclass SettingCloud extends StatelessWidget {\n  const SettingCloud({\n    super.key,\n    required this.restartAppFlowy,\n  });\n\n  final VoidCallback restartAppFlowy;\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: getAuthenticatorType(),\n      builder:\n          (BuildContext context, AsyncSnapshot<AuthenticatorType> snapshot) {\n        if (snapshot.hasData) {\n          final cloudType = snapshot.data!;\n          return BlocProvider(\n            create: (context) => CloudSettingBloc(cloudType),\n            child: BlocBuilder<CloudSettingBloc, CloudSettingState>(\n              builder: (context, state) {\n                return SettingsBody(\n                  title: LocaleKeys.settings_menu_cloudSettings.tr(),\n                  autoSeparate: false,\n                  children: [\n                    if (Env.enableCustomCloud)\n                      _CloudServerSwitcher(cloudType: state.cloudType),\n                    _viewFromCloudType(state.cloudType),\n                  ],\n                );\n              },\n            ),\n          );\n        } else {\n          return const Center(child: CircularProgressIndicator());\n        }\n      },\n    );\n  }\n\n  Widget _viewFromCloudType(AuthenticatorType cloudType) {\n    switch (cloudType) {\n      case AuthenticatorType.local:\n        return SettingLocalCloud(restartAppFlowy: restartAppFlowy);\n      case AuthenticatorType.appflowyCloud:\n        return AppFlowyCloudViewSetting(restartAppFlowy: restartAppFlowy);\n      case AuthenticatorType.appflowyCloudSelfHost:\n        return CustomAppFlowyCloudView(restartAppFlowy: restartAppFlowy);\n      case AuthenticatorType.appflowyCloudDevelop:\n        return AppFlowyCloudViewSetting(\n          serverURL: \"http://localhost\",\n          authenticatorType: AuthenticatorType.appflowyCloudDevelop,\n          restartAppFlowy: restartAppFlowy,\n        );\n    }\n  }\n}\n\nclass CloudTypeSwitcher extends StatelessWidget {\n  const CloudTypeSwitcher({\n    super.key,\n    required this.cloudType,\n    required this.onSelected,\n  });\n\n  final AuthenticatorType cloudType;\n  final Function(AuthenticatorType) onSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    final isDevelopMode = integrationMode().isDevelop;\n    // Only show the appflowyCloudDevelop in develop mode\n    final values = AuthenticatorType.values.where((element) {\n      return isDevelopMode || element != AuthenticatorType.appflowyCloudDevelop;\n    }).toList();\n    return UniversalPlatform.isDesktopOrWeb\n        ? SettingsDropdown(\n            selectedOption: cloudType,\n            onChanged: (type) {\n              if (type != cloudType) {\n                NavigatorAlertDialog(\n                  title: LocaleKeys.settings_menu_changeServerTip.tr(),\n                  confirm: () async {\n                    onSelected(type);\n                  },\n                  hideCancelButton: true,\n                ).show(context);\n              }\n            },\n            options: values\n                .map(\n                  (type) => buildDropdownMenuEntry(\n                    context,\n                    value: type,\n                    label: titleFromCloudType(type),\n                  ),\n                )\n                .toList(),\n          )\n        : FlowyButton(\n            text: FlowyText(\n              titleFromCloudType(cloudType),\n            ),\n            useIntrinsicWidth: true,\n            rightIcon: const Icon(\n              Icons.chevron_right,\n            ),\n            onTap: () => showMobileBottomSheet(\n              context,\n              showHeader: true,\n              showDragHandle: true,\n              showDivider: false,\n              title: LocaleKeys.settings_menu_cloudServerType.tr(),\n              builder: (context) => Column(\n                children: values\n                    .mapIndexed(\n                      (i, e) => FlowyOptionTile.checkbox(\n                        text: titleFromCloudType(values[i]),\n                        isSelected: cloudType == values[i],\n                        onTap: () {\n                          onSelected(e);\n                          context.pop();\n                        },\n                        showBottomBorder: i == values.length - 1,\n                      ),\n                    )\n                    .toList(),\n              ),\n            ),\n          );\n  }\n}\n\nclass CloudTypeItem extends StatelessWidget {\n  const CloudTypeItem({\n    super.key,\n    required this.cloudType,\n    required this.currentCloudType,\n    required this.onSelected,\n  });\n\n  final AuthenticatorType cloudType;\n  final AuthenticatorType currentCloudType;\n  final Function(AuthenticatorType) onSelected;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 32,\n      child: FlowyButton(\n        text: FlowyText.medium(\n          titleFromCloudType(cloudType),\n        ),\n        rightIcon: currentCloudType == cloudType\n            ? const FlowySvg(FlowySvgs.check_s)\n            : null,\n        onTap: () {\n          if (currentCloudType != cloudType) {\n            NavigatorAlertDialog(\n              title: LocaleKeys.settings_menu_changeServerTip.tr(),\n              confirm: () async {\n                onSelected(cloudType);\n              },\n              hideCancelButton: true,\n            ).show(context);\n          }\n          PopoverContainer.of(context).close();\n        },\n      ),\n    );\n  }\n}\n\nclass _CloudServerSwitcher extends StatelessWidget {\n  const _CloudServerSwitcher({\n    required this.cloudType,\n  });\n\n  final AuthenticatorType cloudType;\n\n  @override\n  Widget build(BuildContext context) {\n    return UniversalPlatform.isDesktopOrWeb\n        ? Row(\n            children: [\n              Expanded(\n                child: FlowyText.medium(\n                  LocaleKeys.settings_menu_cloudServerType.tr(),\n                ),\n              ),\n              Flexible(\n                child: CloudTypeSwitcher(\n                  cloudType: cloudType,\n                  onSelected: (type) => context\n                      .read<CloudSettingBloc>()\n                      .add(CloudSettingEvent.updateCloudType(type)),\n                ),\n              ),\n            ],\n          )\n        : Row(\n            mainAxisAlignment: MainAxisAlignment.spaceBetween,\n            children: [\n              FlowyText.medium(\n                LocaleKeys.settings_menu_cloudServerType.tr(),\n              ),\n              CloudTypeSwitcher(\n                cloudType: cloudType,\n                onSelected: (type) => context\n                    .read<CloudSettingBloc>()\n                    .add(CloudSettingEvent.updateCloudType(type)),\n              ),\n            ],\n          );\n  }\n}\n\nString titleFromCloudType(AuthenticatorType cloudType) {\n  switch (cloudType) {\n    case AuthenticatorType.local:\n      return LocaleKeys.settings_menu_cloudLocal.tr();\n    case AuthenticatorType.appflowyCloud:\n      return LocaleKeys.settings_menu_cloudAppFlowy.tr();\n    case AuthenticatorType.appflowyCloudSelfHost:\n      return LocaleKeys.settings_menu_cloudAppFlowySelfHost.tr();\n    case AuthenticatorType.appflowyCloudDevelop:\n      return \"AppFlowyCloud Develop\";\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_local_cloud.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/_restart_app_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\nclass SettingLocalCloud extends StatelessWidget {\n  const SettingLocalCloud({super.key, required this.restartAppFlowy});\n\n  final VoidCallback restartAppFlowy;\n\n  @override\n  Widget build(BuildContext context) {\n    return RestartButton(\n      onClick: () => onPressed(context),\n      showRestartHint: true,\n    );\n  }\n\n  void onPressed(BuildContext context) {\n    NavigatorAlertDialog(\n      title: LocaleKeys.settings_menu_restartAppTip.tr(),\n      confirm: () async {\n        await useLocalServer();\n        restartAppFlowy();\n      },\n    ).show(context);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart",
    "content": "import 'package:appflowy/env/cloud_env.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/sign_in_bloc.dart';\nimport 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/snap_bar.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SettingThirdPartyLogin extends StatelessWidget {\n  const SettingThirdPartyLogin({\n    super.key,\n    required this.didLogin,\n  });\n\n  final VoidCallback didLogin;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocProvider(\n      create: (context) => getIt<SignInBloc>(),\n      child: BlocConsumer<SignInBloc, SignInState>(\n        listener: (context, state) {\n          final successOrFail = state.successOrFail;\n          if (successOrFail != null) {\n            _handleSuccessOrFail(successOrFail, context);\n          }\n        },\n        builder: (_, state) {\n          final indicator = state.isSubmitting\n              ? const LinearProgressIndicator(minHeight: 1)\n              : const SizedBox.shrink();\n\n          final promptMessage = state.isSubmitting\n              ? FlowyText.medium(\n                  LocaleKeys.signIn_syncPromptMessage.tr(),\n                  maxLines: null,\n                )\n              : const SizedBox.shrink();\n\n          return Column(\n            children: [\n              promptMessage,\n              const VSpace(6),\n              indicator,\n              const VSpace(6),\n              if (isAuthEnabled) const ThirdPartySignInButtons(),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  Future<void> _handleSuccessOrFail(\n    FlowyResult<UserProfilePB, FlowyError> result,\n    BuildContext context,\n  ) async {\n    result.fold(\n      (user) async {\n        didLogin();\n        await runAppFlowy();\n      },\n      (error) => showSnapBar(context, error.msg),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/feature_flags.dart';\nimport 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\n\nclass SettingsMenu extends StatelessWidget {\n  const SettingsMenu({\n    super.key,\n    required this.changeSelectedPage,\n    required this.currentPage,\n    required this.userProfile,\n    required this.isBillingEnabled,\n    required this.currentUserRole,\n  });\n\n  final Function changeSelectedPage;\n  final SettingsPage currentPage;\n  final UserProfilePB userProfile;\n  final bool isBillingEnabled;\n  final AFRolePB? currentUserRole;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return Container(\n      decoration: BoxDecoration(\n        color: theme.surfaceContainerColorScheme.layer01,\n        borderRadius: BorderRadiusDirectional.horizontal(\n          start: Radius.circular(theme.spacing.m),\n        ),\n      ),\n      height: double.infinity,\n      child: SingleChildScrollView(\n        padding: EdgeInsets.symmetric(\n          vertical: 24,\n          horizontal: theme.spacing.l,\n        ),\n        physics: const ClampingScrollPhysics(),\n        child: Column(\n          spacing: theme.spacing.xs,\n          children: [\n            SettingsMenuElement(\n              page: SettingsPage.account,\n              selectedPage: currentPage,\n              label: LocaleKeys.settings_accountPage_menuLabel.tr(),\n              icon: const FlowySvg(FlowySvgs.settings_page_user_m),\n              changeSelectedPage: changeSelectedPage,\n            ),\n            SettingsMenuElement(\n              page: SettingsPage.workspace,\n              selectedPage: currentPage,\n              label: LocaleKeys.settings_workspacePage_menuLabel.tr(),\n              icon: const FlowySvg(FlowySvgs.settings_page_workspace_m),\n              changeSelectedPage: changeSelectedPage,\n            ),\n            if (FeatureFlag.membersSettings.isOn &&\n                userProfile.workspaceType == WorkspaceTypePB.ServerW &&\n                currentUserRole != null &&\n                currentUserRole != AFRolePB.Guest)\n              SettingsMenuElement(\n                page: SettingsPage.member,\n                selectedPage: currentPage,\n                label: LocaleKeys.settings_appearance_members_label.tr(),\n                icon: const FlowySvg(FlowySvgs.settings_page_users_m),\n                changeSelectedPage: changeSelectedPage,\n              ),\n            SettingsMenuElement(\n              page: SettingsPage.manageData,\n              selectedPage: currentPage,\n              label: LocaleKeys.settings_manageDataPage_menuLabel.tr(),\n              icon: const FlowySvg(FlowySvgs.settings_page_database_m),\n              changeSelectedPage: changeSelectedPage,\n            ),\n            SettingsMenuElement(\n              page: SettingsPage.notifications,\n              selectedPage: currentPage,\n              label: LocaleKeys.settings_menu_notifications.tr(),\n              icon: const FlowySvg(FlowySvgs.settings_page_bell_m),\n              changeSelectedPage: changeSelectedPage,\n            ),\n            SettingsMenuElement(\n              page: SettingsPage.cloud,\n              selectedPage: currentPage,\n              label: LocaleKeys.settings_menu_cloudSettings.tr(),\n              icon: const FlowySvg(FlowySvgs.settings_page_cloud_m),\n              changeSelectedPage: changeSelectedPage,\n            ),\n            SettingsMenuElement(\n              page: SettingsPage.shortcuts,\n              selectedPage: currentPage,\n              label: LocaleKeys.settings_shortcutsPage_menuLabel.tr(),\n              icon: const FlowySvg(FlowySvgs.settings_page_keyboard_m),\n              changeSelectedPage: changeSelectedPage,\n            ),\n            SettingsMenuElement(\n              page: SettingsPage.ai,\n              selectedPage: currentPage,\n              label: LocaleKeys.settings_aiPage_menuLabel.tr(),\n              icon: const FlowySvg(\n                FlowySvgs.settings_page_ai_m,\n              ),\n              changeSelectedPage: changeSelectedPage,\n            ),\n            if (userProfile.workspaceType == WorkspaceTypePB.ServerW &&\n                currentUserRole != null &&\n                currentUserRole != AFRolePB.Guest)\n              SettingsMenuElement(\n                page: SettingsPage.sites,\n                selectedPage: currentPage,\n                label: LocaleKeys.settings_sites_title.tr(),\n                icon: const FlowySvg(FlowySvgs.settings_page_earth_m),\n                changeSelectedPage: changeSelectedPage,\n              ),\n            if (FeatureFlag.planBilling.isOn && isBillingEnabled) ...[\n              SettingsMenuElement(\n                page: SettingsPage.plan,\n                selectedPage: currentPage,\n                label: LocaleKeys.settings_planPage_menuLabel.tr(),\n                icon: const FlowySvg(FlowySvgs.settings_page_plan_m),\n                changeSelectedPage: changeSelectedPage,\n              ),\n              SettingsMenuElement(\n                page: SettingsPage.billing,\n                selectedPage: currentPage,\n                label: LocaleKeys.settings_billingPage_menuLabel.tr(),\n                icon: const FlowySvg(FlowySvgs.settings_page_credit_card_m),\n                changeSelectedPage: changeSelectedPage,\n              ),\n            ],\n            if (kDebugMode)\n              SettingsMenuElement(\n                // no need to translate this page\n                page: SettingsPage.featureFlags,\n                selectedPage: currentPage,\n                label: 'Feature Flags',\n                icon: const Icon(\n                  Icons.flag,\n                  size: 20,\n                ),\n                changeSelectedPage: changeSelectedPage,\n              ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass SimpleSettingsMenu extends StatelessWidget {\n  const SimpleSettingsMenu({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        Expanded(\n          child: Container(\n            padding: const EdgeInsets.symmetric(vertical: 8) +\n                const EdgeInsets.only(left: 8, right: 4),\n            decoration: BoxDecoration(\n              color: Theme.of(context).colorScheme.surfaceContainerHighest,\n              borderRadius: const BorderRadius.only(\n                topLeft: Radius.circular(8),\n                bottomLeft: Radius.circular(8),\n              ),\n            ),\n            child: SingleChildScrollView(\n              // Right padding is added to make the scrollbar centered\n              // in the space between the menu and the content\n              padding: const EdgeInsets.only(right: 4) +\n                  const EdgeInsets.symmetric(vertical: 16),\n              physics: const ClampingScrollPhysics(),\n              child: SeparatedColumn(\n                separatorBuilder: () => const VSpace(16),\n                children: [\n                  SettingsMenuElement(\n                    page: SettingsPage.cloud,\n                    selectedPage: SettingsPage.cloud,\n                    label: LocaleKeys.settings_menu_cloudSettings.tr(),\n                    icon: const Icon(Icons.sync),\n                    changeSelectedPage: () {},\n                  ),\n                  if (kDebugMode)\n                    SettingsMenuElement(\n                      // no need to translate this page\n                      page: SettingsPage.featureFlags,\n                      selectedPage: SettingsPage.cloud,\n                      label: 'Feature Flags',\n                      icon: const Icon(Icons.flag),\n                      changeSelectedPage: () {},\n                    ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu_element.dart",
    "content": "import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\n\nclass SettingsMenuElement extends StatelessWidget {\n  const SettingsMenuElement({\n    super.key,\n    required this.page,\n    required this.label,\n    required this.icon,\n    required this.changeSelectedPage,\n    required this.selectedPage,\n  });\n\n  final SettingsPage page;\n  final SettingsPage selectedPage;\n  final String label;\n  final Widget icon;\n  final Function changeSelectedPage;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return AFBaseButton(\n      onTap: () => changeSelectedPage(page),\n      padding: EdgeInsets.all(theme.spacing.m),\n      borderRadius: theme.borderRadius.m,\n      borderColor: (_, __, ___, ____) => Colors.transparent,\n      backgroundColor: (_, isHovering, __) {\n        if (isHovering) {\n          return theme.fillColorScheme.contentHover;\n        } else if (page == selectedPage) {\n          return theme.fillColorScheme.themeSelect;\n        }\n        return Colors.transparent;\n      },\n      builder: (_, __, ___) {\n        return Row(\n          children: [\n            icon,\n            HSpace(theme.spacing.m),\n            Text(\n              label,\n              style: theme.textStyle.body.standard(\n                color: theme.textColorScheme.primary,\n              ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  // return FlowyHover(\n  //   isSelected: () => page == selectedPage,\n  //   resetHoverOnRebuild: false,\n  //   style: HoverStyle(\n  //     hoverColor: AFThemeExtension.of(context).greyHover,\n  //     borderRadius: BorderRadius.circular(4),\n  //   ),\n  //   builder: (_, isHovering) => ListTile(\n  //     dense: true,\n  //     leading: iconWidget(\n  //       isHovering || page == selectedPage\n  //           ? Theme.of(context).colorScheme.onSurface\n  //           : AFThemeExtension.of(context).textColor,\n  //     ),\n  //     onTap: () => changeSelectedPage(page),\n  //     selected: page == selectedPage,\n  //     selectedColor: Theme.of(context).colorScheme.onSurface,\n  //     selectedTileColor: Theme.of(context).colorScheme.primary,\n  //     shape: RoundedRectangleBorder(\n  //       borderRadius: BorderRadius.circular(5),\n  //     ),\n  //     minLeadingWidth: 0,\n  //     title: FlowyText.medium(\n  //       label,\n  //       fontSize: FontSizes.s14,\n  //       overflow: TextOverflow.ellipsis,\n  //       color: page == selectedPage\n  //           ? Theme.of(context).colorScheme.onSurface\n  //           : null,\n  //     ),\n  //   ),\n  // );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SettingsNotificationsView extends StatelessWidget {\n  const SettingsNotificationsView({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<NotificationSettingsCubit, NotificationSettingsState>(\n      builder: (context, state) {\n        return SettingsBody(\n          title: LocaleKeys.settings_menu_notifications.tr(),\n          children: [\n            SettingListTile(\n              label: LocaleKeys.settings_notifications_enableNotifications_label\n                  .tr(),\n              hint: LocaleKeys.settings_notifications_enableNotifications_hint\n                  .tr(),\n              trailing: [\n                Toggle(\n                  value: state.isNotificationsEnabled,\n                  onChanged: (_) => context\n                      .read<NotificationSettingsCubit>()\n                      .toggleNotificationsEnabled(),\n                ),\n              ],\n            ),\n            SettingListTile(\n              label: LocaleKeys\n                  .settings_notifications_showNotificationsIcon_label\n                  .tr(),\n              hint: LocaleKeys.settings_notifications_showNotificationsIcon_hint\n                  .tr(),\n              trailing: [\n                Toggle(\n                  value: state.isShowNotificationsIconEnabled,\n                  onChanged: (_) => context\n                      .read<NotificationSettingsCubit>()\n                      .toggleShowNotificationIconEnabled(),\n                ),\n              ],\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_confirm_delete_dialog.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport 'theme_upload_view.dart';\n\nclass ThemeConfirmDeleteDialog extends StatelessWidget {\n  const ThemeConfirmDeleteDialog({\n    super.key,\n    required this.theme,\n  });\n\n  final AppTheme theme;\n\n  void onConfirm(BuildContext context) => Navigator.of(context).pop(true);\n  void onCancel(BuildContext context) => Navigator.of(context).pop(false);\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyDialog(\n      padding: EdgeInsets.zero,\n      constraints: const BoxConstraints.tightFor(\n        width: 300,\n        height: 100,\n      ),\n      title: FlowyText.regular(\n        LocaleKeys.document_plugins_cover_alertDialogConfirmation.tr(),\n        textAlign: TextAlign.center,\n      ),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.end,\n        children: [\n          SizedBox(\n            width: ThemeUploadWidget.buttonSize.width,\n            child: FlowyButton(\n              text: FlowyText.semibold(\n                LocaleKeys.button_ok.tr(),\n                fontSize: ThemeUploadWidget.buttonFontSize,\n              ),\n              onTap: () => onConfirm(context),\n            ),\n          ),\n          SizedBox(\n            width: ThemeUploadWidget.buttonSize.width,\n            child: FlowyButton(\n              text: FlowyText.semibold(\n                LocaleKeys.button_cancel.tr(),\n                fontSize: ThemeUploadWidget.buttonFontSize,\n              ),\n              onTap: () => onCancel(context),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload.dart",
    "content": "export 'theme_confirm_delete_dialog.dart';\nexport 'theme_upload_button.dart';\nexport 'theme_upload_learn_more_button.dart';\nexport 'theme_upload_decoration.dart';\nexport 'theme_upload_failure_widget.dart';\nexport 'theme_upload_loading_widget.dart';\nexport 'theme_upload_view.dart';\nexport 'upload_new_theme_widget.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_button.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart';\nimport 'package:flowy_infra/plugins/bloc/dynamic_plugin_event.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'theme_upload_view.dart';\n\nclass ThemeUploadButton extends StatelessWidget {\n  const ThemeUploadButton({super.key, this.color});\n\n  final Color? color;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox.fromSize(\n      size: ThemeUploadWidget.buttonSize,\n      child: FlowyButton(\n        decoration: BoxDecoration(\n          borderRadius: BorderRadius.circular(8),\n          color: color ?? Theme.of(context).colorScheme.primary,\n        ),\n        hoverColor: color,\n        text: Row(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            FlowyText.medium(\n              fontSize: ThemeUploadWidget.buttonFontSize,\n              color: Theme.of(context).colorScheme.onPrimary,\n              LocaleKeys.settings_appearance_themeUpload_button.tr(),\n            ),\n          ],\n        ),\n        onTap: () => BlocProvider.of<DynamicPluginBloc>(context)\n            .add(DynamicPluginEvent.addPlugin()),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart",
    "content": "import 'package:dotted_border/dotted_border.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\n\nimport 'theme_upload_view.dart';\n\nclass ThemeUploadDecoration extends StatelessWidget {\n  const ThemeUploadDecoration({super.key, required this.child});\n\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      decoration: BoxDecoration(\n        borderRadius: BorderRadius.circular(ThemeUploadWidget.borderRadius),\n        color: Theme.of(context).colorScheme.surface,\n        border: Border.all(\n          color: AFThemeExtension.of(context).onBackground.withValues(\n                alpha: ThemeUploadWidget.fadeOpacity,\n              ),\n        ),\n      ),\n      padding: ThemeUploadWidget.padding,\n      child: DottedBorder(\n        borderType: BorderType.RRect,\n        dashPattern: const [6, 6],\n        color: Theme.of(context)\n            .colorScheme\n            .onSurface\n            .withValues(alpha: ThemeUploadWidget.fadeOpacity),\n        radius: const Radius.circular(ThemeUploadWidget.borderRadius),\n        child: ClipRRect(\n          borderRadius: BorderRadius.circular(ThemeUploadWidget.borderRadius),\n          child: child,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\n\nclass ThemeUploadFailureWidget extends StatelessWidget {\n  const ThemeUploadFailureWidget({super.key, required this.errorMessage});\n\n  final String errorMessage;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      color: Theme.of(context)\n          .colorScheme\n          .error\n          .withValues(alpha: ThemeUploadWidget.fadeOpacity),\n      constraints: const BoxConstraints.expand(),\n      padding: ThemeUploadWidget.padding,\n      child: Column(\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          const Spacer(),\n          FlowySvg(\n            FlowySvgs.close_m,\n            size: ThemeUploadWidget.iconSize,\n            color: AFThemeExtension.of(context).onBackground,\n          ),\n          FlowyText.medium(\n            errorMessage,\n            overflow: TextOverflow.ellipsis,\n          ),\n          ThemeUploadWidget.elementSpacer,\n          const ThemeUploadLearnMoreButton(),\n          ThemeUploadWidget.elementSpacer,\n          ThemeUploadButton(color: Theme.of(context).colorScheme.error),\n          ThemeUploadWidget.elementSpacer,\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/error_page/error_page.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';\nimport 'package:flutter/material.dart';\n\nclass ThemeUploadLearnMoreButton extends StatelessWidget {\n  const ThemeUploadLearnMoreButton({super.key});\n\n  static const learnMoreURL =\n      'https://docs.appflowy.io/docs/appflowy/product/themes';\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: ThemeUploadWidget.buttonSize.height,\n      child: IntrinsicWidth(\n        child: SecondaryButton(\n          outlineColor: AFThemeExtension.of(context).onBackground,\n          child: Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 8),\n            child: FlowyText.medium(\n              fontSize: ThemeUploadWidget.buttonFontSize,\n              LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),\n            ),\n          ),\n          onPressed: () async {\n            final uri = Uri.parse(learnMoreURL);\n            await afLaunchUri(\n              uri,\n              context: context,\n              onFailure: (_) async {\n                if (context.mounted) {\n                  await Dialogs.show(\n                    context,\n                    child: FlowyDialog(\n                      child: FlowyErrorPage.message(\n                        LocaleKeys\n                            .settings_appearance_themeUpload_urlUploadFailure\n                            .tr()\n                            .replaceAll(\n                              '{}',\n                              uri.toString(),\n                            ),\n                        howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),\n                      ),\n                    ),\n                  );\n                }\n              },\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass ThemeUploadLoadingWidget extends StatelessWidget {\n  const ThemeUploadLoadingWidget({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: ThemeUploadWidget.padding,\n      color: Theme.of(context)\n          .colorScheme\n          .surface\n          .withValues(alpha: ThemeUploadWidget.fadeOpacity),\n      constraints: const BoxConstraints.expand(),\n      child: Column(\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          CircularProgressIndicator(\n            color: Theme.of(context).colorScheme.primary,\n          ),\n          ThemeUploadWidget.elementSpacer,\n          FlowyText.regular(\n            LocaleKeys.settings_appearance_themeUpload_loading.tr(),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart';\nimport 'package:flowy_infra/plugins/bloc/dynamic_plugin_state.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport 'theme_upload_decoration.dart';\nimport 'theme_upload_failure_widget.dart';\nimport 'theme_upload_loading_widget.dart';\nimport 'upload_new_theme_widget.dart';\n\nclass ThemeUploadWidget extends StatefulWidget {\n  const ThemeUploadWidget({super.key});\n\n  static const double borderRadius = 8;\n  static const double buttonFontSize = 14;\n  static const Size buttonSize = Size(100, 32);\n  static const EdgeInsets padding = EdgeInsets.all(12.0);\n  static const Size iconSize = Size.square(48);\n  static const Widget elementSpacer = SizedBox(height: 12);\n  static const double fadeOpacity = 0.5;\n  static const Duration fadeDuration = Duration(milliseconds: 750);\n\n  @override\n  State<ThemeUploadWidget> createState() => _ThemeUploadWidgetState();\n}\n\nclass _ThemeUploadWidgetState extends State<ThemeUploadWidget> {\n  void listen(BuildContext context, DynamicPluginState state) {\n    setState(() {\n      state.whenOrNull(\n        ready: (plugins) {\n          child =\n              const UploadNewThemeWidget(key: Key('upload_new_theme_widget'));\n        },\n        deletionSuccess: () {\n          child =\n              const UploadNewThemeWidget(key: Key('upload_new_theme_widget'));\n        },\n        processing: () {\n          child = const ThemeUploadLoadingWidget(\n            key: Key('upload_theme_loading_widget'),\n          );\n        },\n        compilationFailure: (errorMessage) {\n          child = ThemeUploadFailureWidget(\n            key: const Key('upload_theme_failure_widget'),\n            errorMessage: errorMessage,\n          );\n        },\n        compilationSuccess: () {\n          if (Navigator.of(context).canPop()) {\n            Navigator.of(context)\n                .pop(const DynamicPluginState.compilationSuccess());\n          }\n        },\n      );\n    });\n  }\n\n  Widget child = const UploadNewThemeWidget(\n    key: Key('upload_new_theme_widget'),\n  );\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocListener<DynamicPluginBloc, DynamicPluginState>(\n      listener: listen,\n      child: ThemeUploadDecoration(\n        child: Center(\n          child: AnimatedSwitcher(\n            duration: ThemeUploadWidget.fadeDuration,\n            switchInCurve: Curves.easeInOutCubicEmphasized,\n            child: child,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass UploadNewThemeWidget extends StatelessWidget {\n  const UploadNewThemeWidget({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      color: Theme.of(context)\n          .colorScheme\n          .surface\n          .withValues(alpha: ThemeUploadWidget.fadeOpacity),\n      padding: ThemeUploadWidget.padding,\n      child: Column(\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: [\n          const Spacer(),\n          FlowySvg(\n            FlowySvgs.folder_m,\n            size: ThemeUploadWidget.iconSize,\n            color: AFThemeExtension.of(context).onBackground,\n          ),\n          FlowyText.medium(\n            LocaleKeys.settings_appearance_themeUpload_description.tr(),\n            overflow: TextOverflow.ellipsis,\n          ),\n          ThemeUploadWidget.elementSpacer,\n          const ThemeUploadLearnMoreButton(),\n          ThemeUploadWidget.elementSpacer,\n          const Divider(),\n          ThemeUploadWidget.elementSpacer,\n          const ThemeUploadButton(),\n          ThemeUploadWidget.elementSpacer,\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/utils/form_factor.dart",
    "content": "enum FormFactor {\n  mobile._(600),\n  tablet._(840),\n  desktop._(1280);\n\n  const FormFactor._(this.width);\n\n  factory FormFactor.fromWidth(double width) {\n    if (width < FormFactor.mobile.width) {\n      return FormFactor.mobile;\n    } else if (width < FormFactor.tablet.width) {\n      return FormFactor.tablet;\n    } else {\n      return FormFactor.desktop;\n    }\n  }\n\n  final double width;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/utils/hex_opacity_string_extension.dart",
    "content": "extension HexOpacityExtension on String {\n  /// Only used in a valid color String like '0xff00bcf0'\n  String extractHex() {\n    return substring(4);\n  }\n\n  /// Only used in a valid color String like '0xff00bcf0'\n  String extractOpacity() {\n    final opacityString = substring(2, 4);\n    final opacityInt = int.parse(opacityString, radix: 16) / 2.55;\n    return opacityInt.toStringAsFixed(0);\n  }\n\n  /// Apply on the hex string like '00bcf0', with opacity like '100'\n  String combineHexWithOpacity(String opacity) {\n    final opacityInt = (int.parse(opacity) * 2.55).round().toRadixString(16);\n    return '0x$opacityInt$this';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/web_url_hint_widget.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/icon_button.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flutter/material.dart';\n\nclass WebUrlHintWidget extends StatelessWidget {\n  const WebUrlHintWidget({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.only(left: 2),\n      child: FlowyTooltip(\n        message: LocaleKeys.workspace_learnMore.tr(),\n        preferBelow: false,\n        child: FlowyIconButton(\n          width: 24,\n          height: 24,\n          icon: const FlowySvg(\n            FlowySvgs.information_s,\n          ),\n          onPressed: () {\n            afLaunchUrlString(\n              'https://appflowy.com/docs/self-host-appflowy-run-appflowy-web',\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter/widgets.dart';\n\nimport 'widgets/reminder_selector.dart';\n\ntypedef DaySelectedCallback = void Function(DateTime);\ntypedef RangeSelectedCallback = void Function(DateTime, DateTime);\ntypedef IsRangeChangedCallback = void Function(bool, DateTime?, DateTime?);\ntypedef IncludeTimeChangedCallback = void Function(bool, DateTime?, DateTime?);\n\nabstract class AppFlowyDatePicker extends StatefulWidget {\n  const AppFlowyDatePicker({\n    super.key,\n    required this.dateTime,\n    this.endDateTime,\n    required this.includeTime,\n    required this.isRange,\n    this.reminderOption = ReminderOption.none,\n    required this.dateFormat,\n    required this.timeFormat,\n    this.onDaySelected,\n    this.onRangeSelected,\n    this.onIncludeTimeChanged,\n    this.onIsRangeChanged,\n    this.onReminderSelected,\n  });\n\n  final DateTime? dateTime;\n  final DateTime? endDateTime;\n\n  final DateFormatPB dateFormat;\n  final TimeFormatPB timeFormat;\n\n  /// Called when the date is picked, whether by submitting a date from the top\n  /// or by selecting a date in the calendar. Will not be called if isRange is\n  /// true\n  final DaySelectedCallback? onDaySelected;\n\n  /// Called when a date range is picked. Will not be called if isRange is false\n  final RangeSelectedCallback? onRangeSelected;\n\n  /// Whether the date picker allows inputting a time in addition to the date\n  final bool includeTime;\n\n  /// Called when the include time value is changed. This callback has the side\n  /// effect of changing the dateTime values as well\n  final IncludeTimeChangedCallback? onIncludeTimeChanged;\n\n  // Whether the date picker supports date ranges\n  final bool isRange;\n\n  /// Called when the is range value is changed. This callback has the side\n  /// effect of changing the dateTime values as well\n  final IsRangeChangedCallback? onIsRangeChanged;\n\n  final ReminderOption reminderOption;\n  final OnReminderSelected? onReminderSelected;\n}\n\nabstract class AppFlowyDatePickerState<T extends AppFlowyDatePicker>\n    extends State<T> {\n  // store date values in the state and refresh the ui upon any changes made, instead of only updating them after receiving update from backend.\n  late DateTime? dateTime;\n  late DateTime? startDateTime;\n  late DateTime? endDateTime;\n  late bool includeTime;\n  late bool isRange;\n  late ReminderOption reminderOption;\n\n  late DateTime focusedDateTime;\n  PageController? pageController;\n\n  bool justChangedIsRange = false;\n\n  @override\n  void initState() {\n    super.initState();\n\n    dateTime = widget.dateTime;\n    startDateTime = widget.isRange ? widget.dateTime : null;\n    endDateTime = widget.isRange ? widget.endDateTime : null;\n    includeTime = widget.includeTime;\n    isRange = widget.isRange;\n    reminderOption = widget.reminderOption;\n\n    focusedDateTime = widget.dateTime ?? DateTime.now();\n  }\n\n  @override\n  void didUpdateWidget(covariant oldWidget) {\n    dateTime = widget.dateTime;\n    if (widget.isRange) {\n      startDateTime = widget.dateTime;\n      endDateTime = widget.endDateTime;\n    } else {\n      startDateTime = endDateTime = null;\n    }\n    includeTime = widget.includeTime;\n    isRange = widget.isRange;\n    if (oldWidget.reminderOption != widget.reminderOption) {\n      reminderOption = widget.reminderOption;\n    }\n    super.didUpdateWidget(oldWidget);\n  }\n\n  void onDateSelectedFromDatePicker(\n    DateTime? newStartDateTime,\n    DateTime? newEndDateTime,\n  ) {\n    if (newStartDateTime == null) {\n      return;\n    }\n    if (isRange) {\n      if (newEndDateTime == null) {\n        if (justChangedIsRange && dateTime != null) {\n          justChangedIsRange = false;\n          DateTime start = dateTime!;\n          DateTime end = combineDateTimes(\n            DateTime(\n              newStartDateTime.year,\n              newStartDateTime.month,\n              newStartDateTime.day,\n            ),\n            start,\n          );\n          if (end.isBefore(start)) {\n            (start, end) = (end, start);\n          }\n          widget.onRangeSelected?.call(start, end);\n          setState(() {\n            // 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.\n            dateTime = startDateTime = endDateTime = null;\n            focusedDateTime = getNewFocusedDay(newStartDateTime);\n          });\n        } else {\n          final combined = combineDateTimes(newStartDateTime, dateTime);\n          setState(() {\n            dateTime = combined;\n            startDateTime = combined;\n            endDateTime = null;\n            focusedDateTime = getNewFocusedDay(combined);\n          });\n        }\n      } else {\n        bool switched = false;\n        DateTime combinedDateTime =\n            combineDateTimes(newStartDateTime, dateTime);\n        DateTime combinedEndDateTime =\n            combineDateTimes(newEndDateTime, widget.endDateTime);\n\n        if (combinedEndDateTime.isBefore(combinedDateTime)) {\n          (combinedDateTime, combinedEndDateTime) =\n              (combinedEndDateTime, combinedDateTime);\n          switched = true;\n        }\n\n        widget.onRangeSelected?.call(combinedDateTime, combinedEndDateTime);\n\n        setState(() {\n          dateTime = switched ? combinedDateTime : combinedEndDateTime;\n          startDateTime = combinedDateTime;\n          endDateTime = combinedEndDateTime;\n          focusedDateTime = getNewFocusedDay(newEndDateTime);\n        });\n      }\n    } else {\n      final combinedDateTime = combineDateTimes(newStartDateTime, dateTime);\n      widget.onDaySelected?.call(combinedDateTime);\n\n      setState(() {\n        dateTime = combinedDateTime;\n        focusedDateTime = getNewFocusedDay(combinedDateTime);\n      });\n    }\n  }\n\n  DateTime combineDateTimes(DateTime date, DateTime? time) {\n    final timeComponent = time == null\n        ? Duration.zero\n        : Duration(hours: time.hour, minutes: time.minute);\n\n    return DateTime(date.year, date.month, date.day).add(timeComponent);\n  }\n\n  void onDateTimeInputSubmitted(DateTime value) {\n    if (isRange) {\n      DateTime end = endDateTime ?? value;\n      if (end.isBefore(value)) {\n        (value, end) = (end, value);\n      }\n\n      widget.onRangeSelected?.call(value, end);\n\n      setState(() {\n        dateTime = value;\n        startDateTime = value;\n        endDateTime = end;\n        focusedDateTime = getNewFocusedDay(value);\n      });\n    } else {\n      widget.onDaySelected?.call(value);\n\n      setState(() {\n        dateTime = value;\n        focusedDateTime = getNewFocusedDay(value);\n      });\n    }\n  }\n\n  void onEndDateTimeInputSubmitted(DateTime value) {\n    if (isRange) {\n      if (endDateTime == null) {\n        value = combineDateTimes(value, widget.endDateTime);\n      }\n      DateTime start = startDateTime ?? value;\n      if (value.isBefore(start)) {\n        (start, value) = (value, start);\n      }\n\n      widget.onRangeSelected?.call(start, value);\n\n      if (endDateTime == null) {\n        // 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.\n        setState(() {\n          dateTime = startDateTime = endDateTime = null;\n          focusedDateTime = getNewFocusedDay(value);\n        });\n      } else {\n        setState(() {\n          dateTime = start;\n          startDateTime = start;\n          endDateTime = value;\n          focusedDateTime = getNewFocusedDay(value);\n        });\n      }\n    } else {\n      widget.onDaySelected?.call(value);\n\n      setState(() {\n        dateTime = value;\n        focusedDateTime = getNewFocusedDay(value);\n      });\n    }\n  }\n\n  DateTime getNewFocusedDay(DateTime dateTime) {\n    if (focusedDateTime.year != dateTime.year ||\n        focusedDateTime.month != dateTime.month) {\n      return DateTime(dateTime.year, dateTime.month);\n    } else {\n      return focusedDateTime;\n    }\n  }\n\n  void onIsRangeChanged(bool value) {\n    if (value) {\n      justChangedIsRange = true;\n    }\n\n    final now = DateTime.now();\n    final fillerDate = includeTime\n        ? DateTime(now.year, now.month, now.day, now.hour, now.minute)\n        : DateTime(now.year, now.month, now.day);\n    final newDateTime = dateTime ?? fillerDate;\n\n    if (value) {\n      widget.onIsRangeChanged!.call(value, newDateTime, newDateTime);\n    } else {\n      widget.onIsRangeChanged!.call(value, null, null);\n    }\n\n    setState(() {\n      isRange = value;\n      dateTime = focusedDateTime = newDateTime;\n      if (value) {\n        startDateTime = endDateTime = newDateTime;\n      } else {\n        startDateTime = endDateTime = null;\n      }\n    });\n  }\n\n  void onIncludeTimeChanged(bool value) {\n    late final DateTime? newDateTime;\n    late final DateTime? newEndDateTime;\n\n    final now = DateTime.now();\n    final fillerDate = value\n        ? DateTime(now.year, now.month, now.day, now.hour, now.minute)\n        : DateTime(now.year, now.month, now.day);\n\n    if (value) {\n      // fill date if empty, add time component\n      newDateTime = dateTime == null\n          ? fillerDate\n          : combineDateTimes(dateTime!, fillerDate);\n      newEndDateTime = isRange\n          ? endDateTime == null\n              ? fillerDate\n              : combineDateTimes(endDateTime!, fillerDate)\n          : null;\n    } else {\n      // fill date if empty, remove time component\n      newDateTime = dateTime == null\n          ? fillerDate\n          : DateTime(\n              dateTime!.year,\n              dateTime!.month,\n              dateTime!.day,\n            );\n      newEndDateTime = isRange\n          ? endDateTime == null\n              ? fillerDate\n              : DateTime(\n                  endDateTime!.year,\n                  endDateTime!.month,\n                  endDateTime!.day,\n                )\n          : null;\n    }\n\n    widget.onIncludeTimeChanged!.call(value, newDateTime, newEndDateTime);\n\n    setState(() {\n      includeTime = value;\n      dateTime = newDateTime ?? dateTime;\n      if (isRange) {\n        startDateTime = newDateTime ?? dateTime;\n        endDateTime = newEndDateTime ?? endDateTime;\n      } else {\n        startDateTime = endDateTime = null;\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/desktop_date_picker.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart';\nimport 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\n\nimport 'appflowy_date_picker_base.dart';\nimport 'widgets/date_picker.dart';\nimport 'widgets/date_time_text_field.dart';\nimport 'widgets/end_time_button.dart';\nimport 'widgets/reminder_selector.dart';\n\nclass OptionGroup {\n  OptionGroup({required this.options});\n\n  final List<Widget> options;\n}\n\nclass DesktopAppFlowyDatePicker extends AppFlowyDatePicker {\n  const DesktopAppFlowyDatePicker({\n    super.key,\n    required super.dateTime,\n    super.endDateTime,\n    required super.includeTime,\n    required super.isRange,\n    super.reminderOption = ReminderOption.none,\n    required super.dateFormat,\n    required super.timeFormat,\n    super.onDaySelected,\n    super.onRangeSelected,\n    super.onIncludeTimeChanged,\n    super.onIsRangeChanged,\n    super.onReminderSelected,\n    this.popoverMutex,\n    this.options = const [],\n  });\n\n  final PopoverMutex? popoverMutex;\n\n  final List<OptionGroup> options;\n\n  @override\n  State<AppFlowyDatePicker> createState() => DesktopAppFlowyDatePickerState();\n}\n\n@visibleForTesting\nclass DesktopAppFlowyDatePickerState\n    extends AppFlowyDatePickerState<DesktopAppFlowyDatePicker> {\n  final isTabPressedNotifier = ValueNotifier<bool>(false);\n  final refreshStartTextFieldNotifier = RefreshDateTimeTextFieldController();\n  final refreshEndTextFieldNotifier = RefreshDateTimeTextFieldController();\n\n  @override\n  void dispose() {\n    isTabPressedNotifier.dispose();\n    refreshStartTextFieldNotifier.dispose();\n    refreshEndTextFieldNotifier.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    // GestureDetector is a workaround to stop popover from closing\n    // when clicking on the date picker.\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: () {},\n      child: Padding(\n        padding: const EdgeInsets.only(top: 18.0, bottom: 12.0),\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            DateTimeTextField(\n              key: const ValueKey('date_time_text_field'),\n              includeTime: includeTime,\n              dateTime: isRange ? startDateTime : dateTime,\n              dateFormat: widget.dateFormat,\n              timeFormat: widget.timeFormat,\n              popoverMutex: widget.popoverMutex,\n              isTabPressed: isTabPressedNotifier,\n              refreshTextController: refreshStartTextFieldNotifier,\n              onSubmitted: onDateTimeInputSubmitted,\n              showHint: true,\n            ),\n            if (isRange) ...[\n              const VSpace(8),\n              DateTimeTextField(\n                key: const ValueKey('end_date_time_text_field'),\n                includeTime: includeTime,\n                dateTime: endDateTime,\n                dateFormat: widget.dateFormat,\n                timeFormat: widget.timeFormat,\n                popoverMutex: widget.popoverMutex,\n                isTabPressed: isTabPressedNotifier,\n                refreshTextController: refreshEndTextFieldNotifier,\n                onSubmitted: onEndDateTimeInputSubmitted,\n                showHint: isRange && !(dateTime != null && endDateTime == null),\n              ),\n            ],\n            const VSpace(14),\n            Focus(\n              descendantsAreTraversable: false,\n              child: _buildDatePickerHeader(),\n            ),\n            const VSpace(14),\n            DatePicker(\n              isRange: isRange,\n              onDaySelected: (selectedDay, focusedDay) {\n                onDateSelectedFromDatePicker(selectedDay, null);\n              },\n              onRangeSelected: (start, end, focusedDay) {\n                onDateSelectedFromDatePicker(start, end);\n              },\n              selectedDay: dateTime,\n              startDay: isRange ? startDateTime : null,\n              endDay: isRange ? endDateTime : null,\n              focusedDay: focusedDateTime,\n              onCalendarCreated: (controller) {\n                pageController = controller;\n              },\n              onPageChanged: (focusedDay) {\n                setState(\n                  () => focusedDateTime = DateTime(\n                    focusedDay.year,\n                    focusedDay.month,\n                    focusedDay.day,\n                  ),\n                );\n              },\n            ),\n            if (widget.onIsRangeChanged != null ||\n                widget.onIncludeTimeChanged != null)\n              const TypeOptionSeparator(spacing: 12.0),\n            if (widget.onIsRangeChanged != null) ...[\n              EndTimeButton(\n                isRange: isRange,\n                onChanged: onIsRangeChanged,\n              ),\n              const VSpace(4.0),\n            ],\n            if (widget.onIncludeTimeChanged != null)\n              Padding(\n                padding: const EdgeInsets.symmetric(horizontal: 12.0),\n                child: IncludeTimeButton(\n                  includeTime: includeTime,\n                  onChanged: onIncludeTimeChanged,\n                ),\n              ),\n            if (widget.onReminderSelected != null) ...[\n              const _GroupSeparator(),\n              ReminderSelector(\n                mutex: widget.popoverMutex,\n                hasTime: includeTime,\n                timeFormat: widget.timeFormat,\n                selectedOption: reminderOption,\n                onOptionSelected: (option) {\n                  widget.onReminderSelected?.call(option);\n                  setState(() => reminderOption = option);\n                },\n              ),\n            ],\n            if (widget.options.isNotEmpty) ...[\n              const _GroupSeparator(),\n              ListView.separated(\n                shrinkWrap: true,\n                itemCount: widget.options.length,\n                physics: const NeverScrollableScrollPhysics(),\n                separatorBuilder: (_, __) => const _GroupSeparator(),\n                itemBuilder: (_, index) =>\n                    _renderGroupOptions(widget.options[index].options),\n              ),\n            ],\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildDatePickerHeader() {\n    return Padding(\n      padding: const EdgeInsetsDirectional.only(start: 22.0, end: 18.0),\n      child: Row(\n        children: [\n          Expanded(\n            child: FlowyText(\n              DateFormat.yMMMM().format(focusedDateTime),\n            ),\n          ),\n          FlowyIconButton(\n            width: 20,\n            icon: FlowySvg(\n              FlowySvgs.arrow_left_s,\n              color: Theme.of(context).iconTheme.color,\n              size: const Size.square(20.0),\n            ),\n            onPressed: () => pageController?.previousPage(\n              duration: const Duration(milliseconds: 300),\n              curve: Curves.easeOut,\n            ),\n          ),\n          const HSpace(4.0),\n          FlowyIconButton(\n            width: 20,\n            icon: FlowySvg(\n              FlowySvgs.arrow_right_s,\n              color: Theme.of(context).iconTheme.color,\n              size: const Size.square(20.0),\n            ),\n            onPressed: () {\n              pageController?.nextPage(\n                duration: const Duration(milliseconds: 300),\n                curve: Curves.easeOut,\n              );\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _renderGroupOptions(List<Widget> options) => ListView.separated(\n        shrinkWrap: true,\n        itemCount: options.length,\n        separatorBuilder: (_, __) => const VSpace(4),\n        itemBuilder: (_, index) => options[index],\n      );\n\n  @override\n  void onDateTimeInputSubmitted(DateTime value) {\n    if (isRange) {\n      DateTime end = endDateTime ?? value;\n      if (end.isBefore(value)) {\n        (value, end) = (end, value);\n        refreshStartTextFieldNotifier.refresh();\n      }\n\n      widget.onRangeSelected?.call(value, end);\n\n      setState(() {\n        dateTime = value;\n        startDateTime = value;\n        endDateTime = end;\n      });\n    } else {\n      widget.onDaySelected?.call(value);\n\n      setState(() {\n        dateTime = value;\n        focusedDateTime = getNewFocusedDay(value);\n      });\n    }\n  }\n\n  @override\n  void onEndDateTimeInputSubmitted(DateTime value) {\n    if (isRange) {\n      if (endDateTime == null) {\n        value = combineDateTimes(value, widget.endDateTime);\n      }\n      DateTime start = startDateTime ?? value;\n      if (value.isBefore(start)) {\n        (start, value) = (value, start);\n        refreshEndTextFieldNotifier.refresh();\n      }\n\n      widget.onRangeSelected?.call(start, value);\n\n      if (endDateTime == null) {\n        // 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.\n        setState(() {\n          dateTime = startDateTime = endDateTime = null;\n          focusedDateTime = getNewFocusedDay(value);\n        });\n      } else {\n        setState(() {\n          dateTime = start;\n          startDateTime = start;\n          endDateTime = value;\n          focusedDateTime = getNewFocusedDay(value);\n        });\n      }\n    } else {\n      widget.onDaySelected?.call(value);\n\n      setState(() {\n        dateTime = value;\n        focusedDateTime = getNewFocusedDay(value);\n      });\n    }\n  }\n}\n\nclass _GroupSeparator extends StatelessWidget {\n  const _GroupSeparator();\n\n  @override\n  Widget build(BuildContext context) => Padding(\n        padding: const EdgeInsets.symmetric(vertical: 8),\n        child: Container(color: Theme.of(context).dividerColor, height: 1.0),\n      );\n}\n\nclass RefreshDateTimeTextFieldController extends ChangeNotifier {\n  void refresh() => notifyListeners();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_date_picker.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';\nimport 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_mobile_option_decorate_box.dart';\nimport 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';\nimport 'package:appflowy/plugins/base/drag_handler.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_editor.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nimport 'appflowy_date_picker_base.dart';\n\nclass MobileAppFlowyDatePicker extends AppFlowyDatePicker {\n  const MobileAppFlowyDatePicker({\n    super.key,\n    required super.dateTime,\n    super.endDateTime,\n    required super.includeTime,\n    required super.isRange,\n    super.reminderOption = ReminderOption.none,\n    required super.dateFormat,\n    required super.timeFormat,\n    super.onDaySelected,\n    super.onRangeSelected,\n    super.onIncludeTimeChanged,\n    super.onIsRangeChanged,\n    super.onReminderSelected,\n    this.onClearDate,\n  });\n\n  final VoidCallback? onClearDate;\n\n  @override\n  State<MobileAppFlowyDatePicker> createState() =>\n      _MobileAppFlowyDatePickerState();\n}\n\nclass _MobileAppFlowyDatePickerState\n    extends AppFlowyDatePickerState<MobileAppFlowyDatePicker> {\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        FlowyOptionDecorateBox(\n          showTopBorder: false,\n          child: _TimePicker(\n            dateTime: isRange ? startDateTime : dateTime,\n            endDateTime: endDateTime,\n            includeTime: includeTime,\n            isRange: isRange,\n            dateFormat: widget.dateFormat,\n            timeFormat: widget.timeFormat,\n            onStartTimeChanged: onDateTimeInputSubmitted,\n            onEndTimeChanged: onEndDateTimeInputSubmitted,\n          ),\n        ),\n        const _Divider(),\n        FlowyOptionDecorateBox(\n          child: MobileDatePicker(\n            isRange: isRange,\n            selectedDay: dateTime,\n            startDay: isRange ? startDateTime : null,\n            endDay: isRange ? endDateTime : null,\n            focusedDay: focusedDateTime,\n            onDaySelected: (selectedDay) {\n              onDateSelectedFromDatePicker(selectedDay, null);\n            },\n            onRangeSelected: (start, end) {\n              onDateSelectedFromDatePicker(start, end);\n            },\n            onPageChanged: (focusedDay) {\n              setState(() => focusedDateTime = focusedDay);\n            },\n          ),\n        ),\n        const _Divider(),\n        if (widget.onIsRangeChanged != null)\n          _IsRangeSwitch(\n            isRange: widget.isRange,\n            onRangeChanged: onIsRangeChanged,\n          ),\n        if (widget.onIncludeTimeChanged != null)\n          _IncludeTimeSwitch(\n            showTopBorder: widget.onIsRangeChanged == null,\n            includeTime: includeTime,\n            onIncludeTimeChanged: onIncludeTimeChanged,\n          ),\n        if (widget.onReminderSelected != null) ...[\n          const _Divider(),\n          _ReminderSelector(\n            selectedReminderOption: reminderOption,\n            onReminderSelected: (option) {\n              widget.onReminderSelected!.call(option);\n              setState(() => reminderOption = option);\n            },\n            timeFormat: widget.timeFormat,\n            hasTime: widget.includeTime,\n          ),\n        ],\n        if (widget.onClearDate != null) ...[\n          const _Divider(),\n          _ClearDateButton(\n            onClearDate: () {\n              widget.onClearDate!.call();\n              Navigator.of(context).pop();\n            },\n          ),\n        ],\n        const _Divider(),\n      ],\n    );\n  }\n}\n\nclass _Divider extends StatelessWidget {\n  const _Divider();\n\n  @override\n  Widget build(BuildContext context) => const VSpace(20.0);\n}\n\nclass _ReminderSelector extends StatelessWidget {\n  const _ReminderSelector({\n    this.selectedReminderOption,\n    required this.onReminderSelected,\n    required this.timeFormat,\n    this.hasTime = false,\n  });\n\n  final ReminderOption? selectedReminderOption;\n  final OnReminderSelected onReminderSelected;\n  final TimeFormatPB timeFormat;\n  final bool hasTime;\n\n  @override\n  Widget build(BuildContext context) {\n    final option = selectedReminderOption ?? ReminderOption.none;\n\n    final availableOptions = [...ReminderOption.values];\n    if (option != ReminderOption.custom) {\n      availableOptions.remove(ReminderOption.custom);\n    }\n\n    availableOptions.removeWhere(\n      (o) => !o.timeExempt && (!hasTime ? !o.withoutTime : o.requiresNoTime),\n    );\n\n    return FlowyOptionTile.text(\n      text: LocaleKeys.datePicker_reminderLabel.tr(),\n      trailing: Row(\n        children: [\n          const HSpace(6.0),\n          FlowyText(\n            option.label,\n            color: Theme.of(context).hintColor,\n          ),\n          const HSpace(4.0),\n          FlowySvg(\n            FlowySvgs.arrow_right_s,\n            color: Theme.of(context).hintColor,\n            size: const Size.square(18.0),\n          ),\n        ],\n      ),\n      onTap: () => showMobileBottomSheet(\n        context,\n        builder: (_) => DraggableScrollableSheet(\n          expand: false,\n          snap: true,\n          initialChildSize: 0.7,\n          minChildSize: 0.7,\n          builder: (context, controller) => Column(\n            children: [\n              ColoredBox(\n                color: Theme.of(context).colorScheme.surface,\n                child: const Center(child: DragHandle()),\n              ),\n              const _ReminderSelectHeader(),\n              Flexible(\n                child: SingleChildScrollView(\n                  controller: controller,\n                  child: Column(\n                    children: availableOptions.map<Widget>(\n                      (o) {\n                        String label = o.label;\n                        if (o.withoutTime && !o.timeExempt) {\n                          const time = \"09:00\";\n                          final t = timeFormat == TimeFormatPB.TwelveHour\n                              ? \"$time AM\"\n                              : time;\n\n                          label = \"$label ($t)\";\n                        }\n\n                        return FlowyOptionTile.text(\n                          text: label,\n                          showTopBorder: o == ReminderOption.none,\n                          onTap: () {\n                            onReminderSelected(o);\n                            context.pop();\n                          },\n                        );\n                      },\n                    ).toList()\n                      ..insert(0, const _Divider()),\n                  ),\n                ),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _ReminderSelectHeader extends StatelessWidget {\n  const _ReminderSelectHeader();\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: 56,\n      decoration: BoxDecoration(\n        color: Theme.of(context).colorScheme.surface,\n        border: Border(\n          bottom: BorderSide(\n            color: Theme.of(context).dividerColor,\n          ),\n        ),\n      ),\n      child: Row(\n        mainAxisAlignment: MainAxisAlignment.spaceBetween,\n        children: [\n          SizedBox(\n            width: 120,\n            child: AppBarCancelButton(onTap: context.pop),\n          ),\n          FlowyText.medium(\n            LocaleKeys.datePicker_selectReminder.tr(),\n            fontSize: 17.0,\n          ),\n          const HSpace(120),\n        ],\n      ),\n    );\n  }\n}\n\nclass _TimePicker extends StatelessWidget {\n  const _TimePicker({\n    required this.dateTime,\n    required this.endDateTime,\n    required this.dateFormat,\n    required this.timeFormat,\n    required this.includeTime,\n    required this.isRange,\n    required this.onStartTimeChanged,\n    this.onEndTimeChanged,\n  });\n\n  final DateTime? dateTime;\n  final DateTime? endDateTime;\n\n  final bool includeTime;\n  final bool isRange;\n\n  final DateFormatPB dateFormat;\n  final TimeFormatPB timeFormat;\n\n  final void Function(DateTime time) onStartTimeChanged;\n  final void Function(DateTime time)? onEndTimeChanged;\n\n  @override\n  Widget build(BuildContext context) {\n    final dateStr = getDateStr(dateTime);\n    final timeStr = getTimeStr(dateTime);\n    final endDateStr = getDateStr(endDateTime);\n    final endTimeStr = getTimeStr(endDateTime);\n\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          _buildTime(\n            context,\n            dateStr,\n            timeStr,\n            includeTime,\n            true,\n          ),\n          if (isRange) ...[\n            VSpace(8.0, color: Theme.of(context).colorScheme.surface),\n            _buildTime(\n              context,\n              endDateStr,\n              endTimeStr,\n              includeTime,\n              false,\n            ),\n          ],\n        ],\n      ),\n    );\n  }\n\n  Widget _buildTime(\n    BuildContext context,\n    String dateStr,\n    String timeStr,\n    bool includeTime,\n    bool isStartDay,\n  ) {\n    final List<Widget> children = [];\n\n    final now = DateTime.now();\n    final hintDate = DateTime(now.year, now.month, 1, 9);\n\n    if (!includeTime) {\n      children.add(\n        Expanded(\n          child: GestureDetector(\n            behavior: HitTestBehavior.opaque,\n            onTap: () async {\n              final result = await _showDateTimePicker(\n                context,\n                isStartDay ? dateTime : endDateTime,\n                use24hFormat: timeFormat == TimeFormatPB.TwentyFourHour,\n                mode: CupertinoDatePickerMode.date,\n              );\n              handleDateTimePickerResult(result, isStartDay, true);\n            },\n            child: Padding(\n              padding: const EdgeInsets.symmetric(\n                horizontal: 12.0,\n                vertical: 8,\n              ),\n              child: FlowyText(\n                dateStr.isNotEmpty ? dateStr : getDateStr(hintDate),\n                color: dateStr.isEmpty ? Theme.of(context).hintColor : null,\n              ),\n            ),\n          ),\n        ),\n      );\n    } else {\n      children.addAll([\n        Expanded(\n          child: GestureDetector(\n            behavior: HitTestBehavior.opaque,\n            onTap: () async {\n              final result = await _showDateTimePicker(\n                context,\n                isStartDay ? dateTime : endDateTime,\n                use24hFormat: timeFormat == TimeFormatPB.TwentyFourHour,\n                mode: CupertinoDatePickerMode.date,\n              );\n              handleDateTimePickerResult(result, isStartDay, true);\n            },\n            child: Padding(\n              padding: const EdgeInsets.symmetric(vertical: 8.0),\n              child: FlowyText(\n                dateStr.isNotEmpty ? dateStr : \"\",\n                textAlign: TextAlign.center,\n              ),\n            ),\n          ),\n        ),\n        Container(\n          width: 1,\n          height: 16,\n          color: Theme.of(context).colorScheme.outline,\n        ),\n        Expanded(\n          child: GestureDetector(\n            behavior: HitTestBehavior.opaque,\n            onTap: () async {\n              final result = await _showDateTimePicker(\n                context,\n                isStartDay ? dateTime : endDateTime,\n                use24hFormat: timeFormat == TimeFormatPB.TwentyFourHour,\n                mode: CupertinoDatePickerMode.time,\n              );\n              handleDateTimePickerResult(result, isStartDay, false);\n            },\n            child: Padding(\n              padding: const EdgeInsets.symmetric(vertical: 8.0),\n              child: FlowyText(\n                timeStr.isNotEmpty ? timeStr : \"\",\n                textAlign: TextAlign.center,\n              ),\n            ),\n          ),\n        ),\n      ]);\n    }\n\n    return Container(\n      constraints: const BoxConstraints(minHeight: 36),\n      decoration: BoxDecoration(\n        borderRadius: BorderRadius.circular(6),\n        color: Theme.of(context).colorScheme.secondaryContainer,\n        border: Border.all(\n          color: Theme.of(context).colorScheme.outline,\n        ),\n      ),\n      child: Row(\n        children: children,\n      ),\n    );\n  }\n\n  Future<DateTime?> _showDateTimePicker(\n    BuildContext context,\n    DateTime? dateTime, {\n    required CupertinoDatePickerMode mode,\n    required bool use24hFormat,\n  }) async {\n    DateTime? result;\n\n    return showMobileBottomSheet(\n      context,\n      builder: (context) => Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          ConstrainedBox(\n            constraints: const BoxConstraints(maxHeight: 300),\n            child: CupertinoDatePicker(\n              mode: mode,\n              initialDateTime: dateTime,\n              use24hFormat: use24hFormat,\n              onDateTimeChanged: (dateTime) {\n                result = dateTime;\n              },\n            ),\n          ),\n          Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 36),\n            child: FlowyTextButton(\n              LocaleKeys.button_confirm.tr(),\n              constraints: const BoxConstraints.tightFor(height: 42),\n              mainAxisAlignment: MainAxisAlignment.center,\n              fontColor: Theme.of(context).colorScheme.onPrimary,\n              fillColor: Theme.of(context).primaryColor,\n              onPressed: () {\n                Navigator.of(context).pop(result);\n              },\n            ),\n          ),\n          const VSpace(18.0),\n        ],\n      ),\n    );\n  }\n\n  void handleDateTimePickerResult(\n    DateTime? result,\n    bool isStartDay,\n    bool isDate,\n  ) {\n    if (result == null) {\n      return;\n    }\n\n    if (isDate) {\n      final date = isStartDay ? dateTime : endDateTime;\n\n      if (date != null) {\n        final timeComponent = Duration(hours: date.hour, minutes: date.minute);\n        result =\n            DateTime(result.year, result.month, result.day).add(timeComponent);\n      }\n    }\n\n    if (isStartDay) {\n      onStartTimeChanged.call(result);\n    } else {\n      onEndTimeChanged?.call(result);\n    }\n  }\n\n  String getDateStr(DateTime? dateTime) {\n    if (dateTime == null) {\n      return \"\";\n    }\n    return DateFormat(dateFormat.pattern).format(dateTime);\n  }\n\n  String getTimeStr(DateTime? dateTime) {\n    if (dateTime == null || !includeTime) {\n      return \"\";\n    }\n    return DateFormat(timeFormat.pattern).format(dateTime);\n  }\n}\n\nclass _IsRangeSwitch extends StatelessWidget {\n  const _IsRangeSwitch({\n    required this.isRange,\n    required this.onRangeChanged,\n  });\n\n  final bool isRange;\n  final Function(bool) onRangeChanged;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyOptionTile.toggle(\n      text: LocaleKeys.grid_field_isRange.tr(),\n      isSelected: isRange,\n      onValueChanged: onRangeChanged,\n    );\n  }\n}\n\nclass _IncludeTimeSwitch extends StatelessWidget {\n  const _IncludeTimeSwitch({\n    this.showTopBorder = true,\n    required this.includeTime,\n    required this.onIncludeTimeChanged,\n  });\n\n  final bool showTopBorder;\n  final bool includeTime;\n  final Function(bool) onIncludeTimeChanged;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyOptionTile.toggle(\n      showTopBorder: showTopBorder,\n      text: LocaleKeys.grid_field_includeTime.tr(),\n      isSelected: includeTime,\n      onValueChanged: onIncludeTimeChanged,\n    );\n  }\n}\n\nclass _ClearDateButton extends StatelessWidget {\n  const _ClearDateButton({required this.onClearDate});\n\n  final VoidCallback onClearDate;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyOptionTile.text(\n      text: LocaleKeys.grid_field_clearDate.tr(),\n      onTap: onClearDate,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/date_time_format_ext.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\n\nextension ToDateFormat on UserDateFormatPB {\n  DateFormatPB get simplified => switch (this) {\n        UserDateFormatPB.DayMonthYear => DateFormatPB.DayMonthYear,\n        UserDateFormatPB.Friendly => DateFormatPB.Friendly,\n        UserDateFormatPB.ISO => DateFormatPB.ISO,\n        UserDateFormatPB.Locally => DateFormatPB.Local,\n        UserDateFormatPB.US => DateFormatPB.US,\n        _ => DateFormatPB.Friendly,\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/layout.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass DatePickerSize {\n  static double scale = 1;\n\n  static double get itemHeight => 26 * scale;\n  static double get seperatorHeight => 4 * scale;\n\n  static EdgeInsets get itemOptionInsets => const EdgeInsets.all(4);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/utils/user_time_format_ext.dart",
    "content": "import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\n\nextension ToTimeFormat on UserTimeFormatPB {\n  TimeFormatPB get simplified => switch (this) {\n        UserTimeFormatPB.TwelveHour => TimeFormatPB.TwelveHour,\n        UserTimeFormatPB.TwentyFourHour => TimeFormatPB.TwentyFourHour,\n        _ => TimeFormatPB.TwentyFourHour,\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart",
    "content": "import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/layout.dart';\nimport 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\n\nclass ClearDateButton extends StatelessWidget {\n  const ClearDateButton({\n    super.key,\n    required this.onClearDate,\n  });\n\n  final VoidCallback onClearDate;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 12.0),\n      child: SizedBox(\n        height: DatePickerSize.itemHeight,\n        child: FlowyButton(\n          text: FlowyText(LocaleKeys.datePicker_clearDate.tr()),\n          onTap: () {\n            onClearDate();\n            PopoverContainer.of(context).close();\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker.dart",
    "content": "import 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\nimport 'package:table_calendar/table_calendar.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nfinal kFirstDay = DateTime.utc(1970);\nfinal kLastDay = DateTime.utc(2100);\n\nclass DatePicker extends StatefulWidget {\n  const DatePicker({\n    super.key,\n    required this.isRange,\n    this.calendarFormat = CalendarFormat.month,\n    this.startDay,\n    this.endDay,\n    this.selectedDay,\n    required this.focusedDay,\n    this.onDaySelected,\n    this.onRangeSelected,\n    this.onCalendarCreated,\n    this.onPageChanged,\n  });\n\n  final bool isRange;\n  final CalendarFormat calendarFormat;\n\n  final DateTime? startDay;\n  final DateTime? endDay;\n  final DateTime? selectedDay;\n\n  final DateTime focusedDay;\n\n  final void Function(\n    DateTime selectedDay,\n    DateTime focusedDay,\n  )? onDaySelected;\n\n  final void Function(\n    DateTime? start,\n    DateTime? end,\n    DateTime focusedDay,\n  )? onRangeSelected;\n\n  final void Function(PageController pageController)? onCalendarCreated;\n\n  final void Function(DateTime focusedDay)? onPageChanged;\n\n  @override\n  State<DatePicker> createState() => _DatePickerState();\n}\n\nclass _DatePickerState extends State<DatePicker> {\n  late CalendarFormat _calendarFormat = widget.calendarFormat;\n\n  @override\n  Widget build(BuildContext context) {\n    final textStyle = Theme.of(context).textTheme.bodyMedium!;\n    final boxDecoration = BoxDecoration(\n      color: Theme.of(context).cardColor,\n      shape: BoxShape.circle,\n    );\n\n    final calendarStyle = UniversalPlatform.isMobile\n        ? _CalendarStyle.mobile(\n            dowTextStyle: textStyle.copyWith(\n              color: Theme.of(context).hintColor,\n              fontSize: 14.0,\n            ),\n          )\n        : _CalendarStyle.desktop(\n            dowTextStyle: AFThemeExtension.of(context).caption,\n            selectedColor: Theme.of(context).colorScheme.primary,\n          );\n\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 16.0),\n      child: TableCalendar(\n        firstDay: kFirstDay,\n        lastDay: kLastDay,\n        focusedDay: widget.focusedDay,\n        rowHeight: calendarStyle.rowHeight,\n        calendarFormat: _calendarFormat,\n        daysOfWeekHeight: calendarStyle.dowHeight,\n        rangeSelectionMode: widget.isRange\n            ? RangeSelectionMode.enforced\n            : RangeSelectionMode.disabled,\n        rangeStartDay: widget.isRange ? widget.startDay : null,\n        rangeEndDay: widget.isRange ? widget.endDay : null,\n        availableGestures: calendarStyle.availableGestures,\n        availableCalendarFormats: const {CalendarFormat.month: 'Month'},\n        onCalendarCreated: widget.onCalendarCreated,\n        headerVisible: calendarStyle.headerVisible,\n        headerStyle: calendarStyle.headerStyle,\n        calendarStyle: CalendarStyle(\n          cellMargin: const EdgeInsets.all(3.5),\n          defaultDecoration: boxDecoration,\n          selectedDecoration: boxDecoration.copyWith(\n            color: calendarStyle.selectedColor,\n          ),\n          todayDecoration: boxDecoration.copyWith(\n            color: Colors.transparent,\n            border: Border.all(color: calendarStyle.selectedColor),\n          ),\n          weekendDecoration: boxDecoration,\n          outsideDecoration: boxDecoration,\n          rangeStartDecoration: boxDecoration.copyWith(\n            color: calendarStyle.selectedColor,\n          ),\n          rangeEndDecoration: boxDecoration.copyWith(\n            color: calendarStyle.selectedColor,\n          ),\n          defaultTextStyle: textStyle,\n          weekendTextStyle: textStyle,\n          selectedTextStyle: textStyle.copyWith(\n            color: Theme.of(context).colorScheme.surface,\n          ),\n          rangeStartTextStyle: textStyle.copyWith(\n            color: Theme.of(context).colorScheme.surface,\n          ),\n          rangeEndTextStyle: textStyle.copyWith(\n            color: Theme.of(context).colorScheme.surface,\n          ),\n          todayTextStyle: textStyle,\n          outsideTextStyle: textStyle.copyWith(\n            color: Theme.of(context).disabledColor,\n          ),\n          rangeHighlightColor: Theme.of(context).colorScheme.secondaryContainer,\n        ),\n        calendarBuilders: CalendarBuilders(\n          dowBuilder: (context, day) {\n            final locale = context.locale.toLanguageTag();\n            final label = DateFormat.E(locale).format(day);\n            return Padding(\n              padding: const EdgeInsets.only(bottom: 8.0),\n              child: Center(\n                child: Text(label, style: calendarStyle.dowTextStyle),\n              ),\n            );\n          },\n        ),\n        selectedDayPredicate: (day) =>\n            widget.isRange ? false : isSameDay(widget.selectedDay, day),\n        onFormatChanged: (calendarFormat) =>\n            setState(() => _calendarFormat = calendarFormat),\n        onPageChanged: (focusedDay) {\n          widget.onPageChanged?.call(focusedDay);\n        },\n        onDaySelected: widget.onDaySelected,\n        onRangeSelected: widget.onRangeSelected,\n      ),\n    );\n  }\n}\n\nclass _CalendarStyle {\n  _CalendarStyle.desktop({\n    required this.selectedColor,\n    required this.dowTextStyle,\n  })  : rowHeight = 33,\n        dowHeight = 35,\n        headerVisible = false,\n        headerStyle = const HeaderStyle(),\n        availableGestures = AvailableGestures.horizontalSwipe;\n\n  _CalendarStyle.mobile({required this.dowTextStyle})\n      : rowHeight = 48,\n        dowHeight = 48,\n        headerVisible = false,\n        headerStyle = const HeaderStyle(),\n        selectedColor = const Color(0xFF00BCF0),\n        availableGestures = AvailableGestures.horizontalSwipe;\n\n  _CalendarStyle({\n    required this.rowHeight,\n    required this.dowHeight,\n    required this.headerVisible,\n    required this.headerStyle,\n    required this.dowTextStyle,\n    required this.selectedColor,\n    required this.availableGestures,\n  });\n\n  final double rowHeight;\n  final double dowHeight;\n  final bool headerVisible;\n  final HeaderStyle headerStyle;\n  final TextStyle dowTextStyle;\n  final Color selectedColor;\n  final AvailableGestures availableGestures;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart",
    "content": "import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/utils/date_time_format_ext.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/utils/user_time_format_ext.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flowy_infra_ui/style_widget/decoration.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\n/// Provides arguemnts for [AppFlowyDatePicker] when showing\n/// a [DatePickerMenu]\n///\nclass DatePickerOptions {\n  DatePickerOptions({\n    DateTime? focusedDay,\n    this.selectedDay,\n    this.includeTime = false,\n    this.isRange = false,\n    this.dateFormat = UserDateFormatPB.Friendly,\n    this.timeFormat = UserTimeFormatPB.TwentyFourHour,\n    this.selectedReminderOption,\n    this.onDaySelected,\n    this.onRangeSelected,\n    this.onIncludeTimeChanged,\n    this.onIsRangeChanged,\n    this.onReminderSelected,\n  }) : focusedDay = focusedDay ?? DateTime.now();\n\n  final DateTime focusedDay;\n  final DateTime? selectedDay;\n  final bool includeTime;\n  final bool isRange;\n  final UserDateFormatPB dateFormat;\n  final UserTimeFormatPB timeFormat;\n  final ReminderOption? selectedReminderOption;\n\n  final DaySelectedCallback? onDaySelected;\n  final RangeSelectedCallback? onRangeSelected;\n  final IncludeTimeChangedCallback? onIncludeTimeChanged;\n  final IsRangeChangedCallback? onIsRangeChanged;\n  final OnReminderSelected? onReminderSelected;\n\n  DatePickerOptions copyWith({\n    DateTime? focusedDay,\n    DateTime? selectedDay,\n    bool? includeTime,\n    bool? isRange,\n    UserDateFormatPB? dateFormat,\n    UserTimeFormatPB? timeFormat,\n    ReminderOption? selectedReminderOption,\n    DaySelectedCallback? onDaySelected,\n    RangeSelectedCallback? onRangeSelected,\n    IncludeTimeChangedCallback? onIncludeTimeChanged,\n    IsRangeChangedCallback? onIsRangeChanged,\n    OnReminderSelected? onReminderSelected,\n  }) {\n    return DatePickerOptions(\n      focusedDay: focusedDay ?? this.focusedDay,\n      selectedDay: selectedDay ?? this.selectedDay,\n      includeTime: includeTime ?? this.includeTime,\n      isRange: isRange ?? this.isRange,\n      dateFormat: dateFormat ?? this.dateFormat,\n      timeFormat: timeFormat ?? this.timeFormat,\n      selectedReminderOption:\n          selectedReminderOption ?? this.selectedReminderOption,\n      onDaySelected: onDaySelected ?? this.onDaySelected,\n      onRangeSelected: onRangeSelected ?? this.onRangeSelected,\n      onIncludeTimeChanged: onIncludeTimeChanged ?? this.onIncludeTimeChanged,\n      onIsRangeChanged: onIsRangeChanged ?? this.onIsRangeChanged,\n      onReminderSelected: onReminderSelected ?? this.onReminderSelected,\n    );\n  }\n}\n\nabstract class DatePickerService {\n  void show(Offset offset, {required DatePickerOptions options});\n\n  void dismiss();\n}\n\nconst double _datePickerWidth = 260;\nconst double _datePickerHeight = 404;\nconst double _ySpacing = 15;\n\nclass DatePickerMenu extends DatePickerService {\n  DatePickerMenu({required this.context, required this.editorState});\n\n  final BuildContext context;\n  final EditorState editorState;\n  PopoverMutex? popoverMutex;\n\n  OverlayEntry? _menuEntry;\n\n  @override\n  void dismiss() {\n    _menuEntry?.remove();\n    _menuEntry = null;\n    popoverMutex?.close();\n    popoverMutex?.dispose();\n    popoverMutex = null;\n  }\n\n  @override\n  void show(Offset offset, {required DatePickerOptions options}) =>\n      _show(offset, options: options);\n\n  void _show(Offset offset, {required DatePickerOptions options}) {\n    dismiss();\n\n    final editorSize = editorState.renderBox!.size;\n\n    double offsetX = offset.dx;\n    double offsetY = offset.dy;\n\n    final showRight = (offset.dx + _datePickerWidth) < editorSize.width;\n    if (!showRight) {\n      offsetX = offset.dx - _datePickerWidth;\n    }\n\n    final showBelow = (offset.dy + _datePickerHeight) < editorSize.height;\n    if (!showBelow) {\n      if ((offset.dy - _datePickerHeight) < 0) {\n        // Show dialog in the middle\n        offsetY = offset.dy - (_datePickerHeight / 3);\n      } else {\n        // Show above\n        offsetY = offset.dy - _datePickerHeight;\n      }\n    }\n\n    popoverMutex = PopoverMutex();\n    _menuEntry = OverlayEntry(\n      builder: (_) => Material(\n        type: MaterialType.transparency,\n        child: SizedBox(\n          height: editorSize.height,\n          width: editorSize.width,\n          child: KeyboardListener(\n            focusNode: FocusNode()..requestFocus(),\n            onKeyEvent: (event) {\n              if (event.logicalKey == LogicalKeyboardKey.escape) {\n                dismiss();\n              }\n            },\n            child: GestureDetector(\n              behavior: HitTestBehavior.opaque,\n              onTap: dismiss,\n              child: Stack(\n                children: [\n                  _AnimatedDatePicker(\n                    offset: Offset(offsetX, offsetY),\n                    showBelow: showBelow,\n                    options: options,\n                    popoverMutex: popoverMutex,\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n\n    Overlay.of(context).insert(_menuEntry!);\n  }\n}\n\nclass _AnimatedDatePicker extends StatefulWidget {\n  const _AnimatedDatePicker({\n    required this.offset,\n    required this.showBelow,\n    required this.options,\n    this.popoverMutex,\n  });\n\n  final Offset offset;\n  final bool showBelow;\n  final DatePickerOptions options;\n  final PopoverMutex? popoverMutex;\n\n  @override\n  State<_AnimatedDatePicker> createState() => _AnimatedDatePickerState();\n}\n\nclass _AnimatedDatePickerState extends State<_AnimatedDatePicker> {\n  late DatePickerOptions options = widget.options;\n\n  @override\n  Widget build(BuildContext context) {\n    final dy = widget.offset.dy + (widget.showBelow ? _ySpacing : -_ySpacing);\n\n    return AnimatedPositioned(\n      duration: const Duration(milliseconds: 200),\n      top: dy,\n      left: widget.offset.dx,\n      child: Container(\n        decoration: FlowyDecoration.decoration(\n          Theme.of(context).cardColor,\n          Theme.of(context).colorScheme.shadow,\n        ),\n        constraints: BoxConstraints.loose(const Size(_datePickerWidth, 465)),\n        child: DesktopAppFlowyDatePicker(\n          includeTime: options.includeTime,\n          isRange: options.isRange,\n          dateFormat: options.dateFormat.simplified,\n          timeFormat: options.timeFormat.simplified,\n          dateTime: options.selectedDay,\n          popoverMutex: widget.popoverMutex,\n          reminderOption: options.selectedReminderOption ?? ReminderOption.none,\n          onDaySelected: options.onDaySelected == null\n              ? null\n              : (d) {\n                  options.onDaySelected?.call(d);\n                  setState(() {\n                    options = options.copyWith(selectedDay: d);\n                  });\n                },\n          onIsRangeChanged: options.onIsRangeChanged == null\n              ? null\n              : (isRange, s, e) {\n                  options.onIsRangeChanged?.call(isRange, s, e);\n                },\n          onIncludeTimeChanged: options.onIncludeTimeChanged == null\n              ? null\n              : (include, s, e) {\n                  options.onIncludeTimeChanged?.call(include, s, e);\n                  setState(() {\n                    options =\n                        options.copyWith(includeTime: include, selectedDay: s);\n                  });\n                },\n          onRangeSelected: options.onRangeSelected == null\n              ? null\n              : (s, e) {\n                  options.onRangeSelected?.call(s, e);\n                },\n          onReminderSelected: options.onReminderSelected == null\n              ? null\n              : (o) {\n                  options.onReminderSelected?.call(o);\n                  setState(() {\n                    options = options.copyWith(selectedReminderOption: o);\n                  });\n                },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_settings.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/widgets.dart';\n\nimport 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/utils/layout.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\n\nclass DateTimeSetting extends StatefulWidget {\n  const DateTimeSetting({\n    super.key,\n    required this.dateFormat,\n    required this.timeFormat,\n    required this.onDateFormatChanged,\n    required this.onTimeFormatChanged,\n  });\n\n  final DateFormatPB dateFormat;\n  final TimeFormatPB timeFormat;\n  final Function(DateFormatPB) onDateFormatChanged;\n  final Function(TimeFormatPB) onTimeFormatChanged;\n\n  @override\n  State<DateTimeSetting> createState() => _DateTimeSettingState();\n}\n\nclass _DateTimeSettingState extends State<DateTimeSetting> {\n  final timeSettingPopoverMutex = PopoverMutex();\n  String? overlayIdentifier;\n\n  @override\n  void dispose() {\n    timeSettingPopoverMutex.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final List<Widget> children = [\n      AppFlowyPopover(\n        mutex: timeSettingPopoverMutex,\n        triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,\n        offset: const Offset(8, 0),\n        popupBuilder: (_) => DateFormatList(\n          selectedFormat: widget.dateFormat,\n          onSelected: _onDateFormatChanged,\n        ),\n        child: const Padding(\n          padding: EdgeInsets.symmetric(horizontal: 6.0),\n          child: DateFormatButton(),\n        ),\n      ),\n      AppFlowyPopover(\n        mutex: timeSettingPopoverMutex,\n        triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,\n        offset: const Offset(8, 0),\n        popupBuilder: (_) => TimeFormatList(\n          selectedFormat: widget.timeFormat,\n          onSelected: _onTimeFormatChanged,\n        ),\n        child: const Padding(\n          padding: EdgeInsets.symmetric(horizontal: 6.0),\n          child: TimeFormatButton(),\n        ),\n      ),\n    ];\n\n    return SizedBox(\n      width: 180,\n      child: ListView.separated(\n        shrinkWrap: true,\n        separatorBuilder: (_, __) => VSpace(DatePickerSize.seperatorHeight),\n        itemCount: children.length,\n        itemBuilder: (_, int index) => children[index],\n        padding: const EdgeInsets.symmetric(vertical: 6.0),\n      ),\n    );\n  }\n\n  void _onTimeFormatChanged(TimeFormatPB format) {\n    widget.onTimeFormatChanged(format);\n    timeSettingPopoverMutex.close();\n  }\n\n  void _onDateFormatChanged(DateFormatPB format) {\n    widget.onDateFormatChanged(format);\n    timeSettingPopoverMutex.close();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_text_field.dart",
    "content": "import 'package:any_date/any_date.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nimport '../desktop_date_picker.dart';\nimport 'date_picker.dart';\n\nclass DateTimeTextField extends StatefulWidget {\n  const DateTimeTextField({\n    super.key,\n    required this.dateTime,\n    required this.includeTime,\n    required this.dateFormat,\n    this.timeFormat,\n    this.onSubmitted,\n    this.popoverMutex,\n    this.isTabPressed,\n    this.refreshTextController,\n    required this.showHint,\n  }) : assert(includeTime && timeFormat != null || !includeTime);\n\n  final DateTime? dateTime;\n  final bool includeTime;\n  final void Function(DateTime dateTime)? onSubmitted;\n  final DateFormatPB dateFormat;\n  final TimeFormatPB? timeFormat;\n  final PopoverMutex? popoverMutex;\n  final ValueNotifier<bool>? isTabPressed;\n  final RefreshDateTimeTextFieldController? refreshTextController;\n  final bool showHint;\n\n  @override\n  State<DateTimeTextField> createState() => _DateTimeTextFieldState();\n}\n\nclass _DateTimeTextFieldState extends State<DateTimeTextField> {\n  late final FocusNode focusNode;\n  late final FocusNode dateFocusNode;\n  late final FocusNode timeFocusNode;\n\n  final dateTextController = TextEditingController();\n  final timeTextController = TextEditingController();\n\n  final statesController = WidgetStatesController();\n\n  bool justSubmitted = false;\n\n  DateFormat get dateFormat => DateFormat(widget.dateFormat.pattern);\n  DateFormat get timeFormat => DateFormat(widget.timeFormat?.pattern);\n\n  @override\n  void initState() {\n    super.initState();\n    updateTextControllers();\n\n    focusNode = FocusNode()..addListener(focusNodeListener);\n    dateFocusNode = FocusNode(onKeyEvent: textFieldOnKeyEvent)\n      ..addListener(dateFocusNodeListener);\n    timeFocusNode = FocusNode(onKeyEvent: textFieldOnKeyEvent)\n      ..addListener(timeFocusNodeListener);\n    widget.isTabPressed?.addListener(isTabPressedListener);\n    widget.refreshTextController?.addListener(updateTextControllers);\n    widget.popoverMutex?.addPopoverListener(popoverListener);\n  }\n\n  @override\n  void didUpdateWidget(covariant oldWidget) {\n    if (oldWidget.dateTime != widget.dateTime ||\n        oldWidget.dateFormat != widget.dateFormat ||\n        oldWidget.timeFormat != widget.timeFormat) {\n      statesController.update(WidgetState.error, false);\n      updateTextControllers();\n    }\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  void dispose() {\n    dateTextController.dispose();\n    timeTextController.dispose();\n    widget.popoverMutex?.removePopoverListener(popoverListener);\n    widget.isTabPressed?.removeListener(isTabPressedListener);\n    widget.refreshTextController?.removeListener(updateTextControllers);\n    dateFocusNode\n      ..removeListener(dateFocusNodeListener)\n      ..dispose();\n    timeFocusNode\n      ..removeListener(timeFocusNodeListener)\n      ..dispose();\n    focusNode\n      ..removeListener(focusNodeListener)\n      ..dispose();\n    statesController.dispose();\n    super.dispose();\n  }\n\n  void focusNodeListener() {\n    if (focusNode.hasFocus) {\n      statesController.update(WidgetState.focused, true);\n      widget.popoverMutex?.close();\n    } else {\n      statesController.update(WidgetState.focused, false);\n    }\n  }\n\n  void isTabPressedListener() {\n    if (!dateFocusNode.hasFocus && !timeFocusNode.hasFocus) {\n      return;\n    }\n    final controller =\n        dateFocusNode.hasFocus ? dateTextController : timeTextController;\n    if (widget.isTabPressed != null && widget.isTabPressed!.value) {\n      controller.selection = TextSelection(\n        baseOffset: 0,\n        extentOffset: controller.text.characters.length,\n      );\n      widget.isTabPressed?.value = false;\n    }\n  }\n\n  KeyEventResult textFieldOnKeyEvent(FocusNode node, KeyEvent event) {\n    if (event is KeyUpEvent && event.logicalKey == LogicalKeyboardKey.tab) {\n      widget.isTabPressed?.value = true;\n    }\n    return KeyEventResult.ignored;\n  }\n\n  void dateFocusNodeListener() {\n    if (dateFocusNode.hasFocus || justSubmitted) {\n      justSubmitted = true;\n      return;\n    }\n\n    final expected = widget.dateTime == null\n        ? \"\"\n        : DateFormat(widget.dateFormat.pattern).format(widget.dateTime!);\n    if (expected != dateTextController.text.trim()) {\n      onDateTextFieldSubmitted();\n    }\n  }\n\n  void timeFocusNodeListener() {\n    if (timeFocusNode.hasFocus || widget.timeFormat == null || justSubmitted) {\n      justSubmitted = true;\n      return;\n    }\n\n    final expected = widget.dateTime == null\n        ? \"\"\n        : DateFormat(widget.timeFormat!.pattern).format(widget.dateTime!);\n    if (expected != timeTextController.text.trim()) {\n      onTimeTextFieldSubmitted();\n    }\n  }\n\n  void popoverListener() {\n    if (focusNode.hasFocus) {\n      focusNode.unfocus();\n    }\n  }\n\n  void updateTextControllers() {\n    if (widget.dateTime == null) {\n      dateTextController.clear();\n      timeTextController.clear();\n      return;\n    }\n\n    dateTextController.text = dateFormat.format(widget.dateTime!);\n    timeTextController.text = timeFormat.format(widget.dateTime!);\n  }\n\n  void onDateTextFieldSubmitted() {\n    DateTime? dateTime = parseDateTimeStr(dateTextController.text.trim());\n    if (dateTime == null) {\n      statesController.update(WidgetState.error, true);\n      return;\n    }\n    statesController.update(WidgetState.error, false);\n    if (widget.dateTime != null) {\n      final timeComponent = Duration(\n        hours: widget.dateTime!.hour,\n        minutes: widget.dateTime!.minute,\n        seconds: widget.dateTime!.second,\n      );\n      dateTime = DateTime(\n        dateTime.year,\n        dateTime.month,\n        dateTime.day,\n      ).add(timeComponent);\n    }\n    widget.onSubmitted?.call(dateTime);\n  }\n\n  void onTimeTextFieldSubmitted() {\n    // this happens in the middle of a date range selection\n    if (widget.dateTime == null) {\n      widget.refreshTextController?.refresh();\n      statesController.update(WidgetState.error, true);\n      return;\n    }\n    final adjustedTimeStr =\n        \"${dateTextController.text} ${timeTextController.text.trim()}\";\n    final dateTime = parseDateTimeStr(adjustedTimeStr);\n\n    if (dateTime == null) {\n      statesController.update(WidgetState.error, true);\n      return;\n    }\n    statesController.update(WidgetState.error, false);\n    widget.onSubmitted?.call(dateTime);\n  }\n\n  DateTime? parseDateTimeStr(String string) {\n    final locale = context.locale.toLanguageTag();\n    final parser = AnyDate.fromLocale(locale);\n    final result = parser.tryParse(string);\n    if (result == null ||\n        result.isBefore(kFirstDay) ||\n        result.isAfter(kLastDay)) {\n      return null;\n    }\n    return result;\n  }\n\n  late final WidgetStateProperty<Color?> borderColor =\n      WidgetStateProperty.resolveWith(\n    (states) {\n      if (states.contains(WidgetState.error)) {\n        return Theme.of(context).colorScheme.errorContainer;\n      }\n      if (states.contains(WidgetState.focused)) {\n        return Theme.of(context).colorScheme.primary;\n      }\n      return Theme.of(context).colorScheme.outline;\n    },\n  );\n\n  @override\n  Widget build(BuildContext context) {\n    final now = DateTime.now();\n    final hintDate = DateTime(now.year, now.month, 1, 9);\n\n    return Focus(\n      focusNode: focusNode,\n      skipTraversal: true,\n      child: wrapWithGestures(\n        child: ListenableBuilder(\n          listenable: statesController,\n          builder: (context, child) {\n            final resolved = borderColor.resolve(statesController.value);\n            return Padding(\n              padding: const EdgeInsets.symmetric(horizontal: 18.0),\n              child: Container(\n                constraints: const BoxConstraints.tightFor(height: 32),\n                decoration: BoxDecoration(\n                  border: Border.fromBorderSide(\n                    BorderSide(\n                      color: resolved ?? Colors.transparent,\n                    ),\n                  ),\n                  borderRadius: Corners.s8Border,\n                ),\n                child: child,\n              ),\n            );\n          },\n          child: widget.includeTime\n              ? Row(\n                  children: [\n                    Expanded(\n                      child: TextField(\n                        key: const ValueKey('date_time_text_field_date'),\n                        focusNode: dateFocusNode,\n                        controller: dateTextController,\n                        style: Theme.of(context).textTheme.bodyMedium,\n                        decoration: getInputDecoration(\n                          const EdgeInsetsDirectional.fromSTEB(12, 6, 6, 6),\n                          dateFormat.format(hintDate),\n                        ),\n                        onSubmitted: (value) {\n                          justSubmitted = true;\n                          onDateTextFieldSubmitted();\n                        },\n                      ),\n                    ),\n                    VerticalDivider(\n                      indent: 4,\n                      endIndent: 4,\n                      width: 1,\n                      color: Theme.of(context).colorScheme.outline,\n                    ),\n                    Expanded(\n                      child: TextField(\n                        key: const ValueKey('date_time_text_field_time'),\n                        focusNode: timeFocusNode,\n                        controller: timeTextController,\n                        style: Theme.of(context).textTheme.bodyMedium,\n                        maxLength: widget.timeFormat == TimeFormatPB.TwelveHour\n                            ? 8 // 12:34 PM = 8 characters\n                            : 5, // 12:34 = 5 characters\n                        inputFormatters: [\n                          FilteringTextInputFormatter.allow(\n                            RegExp('[0-9:AaPpMm]'),\n                          ),\n                        ],\n                        decoration: getInputDecoration(\n                          const EdgeInsetsDirectional.fromSTEB(6, 6, 12, 6),\n                          timeFormat.format(hintDate),\n                        ),\n                        onSubmitted: (value) {\n                          justSubmitted = true;\n                          onTimeTextFieldSubmitted();\n                        },\n                      ),\n                    ),\n                  ],\n                )\n              : Center(\n                  child: TextField(\n                    key: const ValueKey('date_time_text_field_date'),\n                    focusNode: dateFocusNode,\n                    controller: dateTextController,\n                    style: Theme.of(context).textTheme.bodyMedium,\n                    decoration: getInputDecoration(\n                      const EdgeInsets.symmetric(horizontal: 12, vertical: 6),\n                      dateFormat.format(hintDate),\n                    ),\n                    onSubmitted: (value) {\n                      justSubmitted = true;\n                      onDateTextFieldSubmitted();\n                    },\n                  ),\n                ),\n        ),\n      ),\n    );\n  }\n\n  Widget wrapWithGestures({required Widget child}) {\n    return GestureDetector(\n      onTapDown: (_) {\n        statesController.update(WidgetState.pressed, true);\n      },\n      onTapCancel: () {\n        statesController.update(WidgetState.pressed, false);\n      },\n      onTap: () {\n        statesController.update(WidgetState.pressed, false);\n      },\n      child: child,\n    );\n  }\n\n  InputDecoration getInputDecoration(\n    EdgeInsetsGeometry padding,\n    String? hintText,\n  ) {\n    return InputDecoration(\n      border: InputBorder.none,\n      contentPadding: padding,\n      isCollapsed: true,\n      isDense: true,\n      hintText: widget.showHint ? hintText : null,\n      counterText: \"\",\n      hintStyle: Theme.of(context)\n          .textTheme\n          .bodyMedium\n          ?.copyWith(color: Theme.of(context).hintColor),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_time_settings.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\n\nclass DateTypeOptionButton extends StatelessWidget {\n  const DateTypeOptionButton({\n    super.key,\n    required this.dateFormat,\n    required this.timeFormat,\n    required this.onDateFormatChanged,\n    required this.onTimeFormatChanged,\n    required this.popoverMutex,\n  });\n\n  final DateFormatPB dateFormat;\n  final TimeFormatPB timeFormat;\n  final Function(DateFormatPB) onDateFormatChanged;\n  final Function(TimeFormatPB) onTimeFormatChanged;\n  final PopoverMutex? popoverMutex;\n\n  @override\n  Widget build(BuildContext context) {\n    final title =\n        \"${LocaleKeys.datePicker_dateFormat.tr()} & ${LocaleKeys.datePicker_timeFormat.tr()}\";\n    return AppFlowyPopover(\n      mutex: popoverMutex,\n      offset: const Offset(8, 0),\n      margin: EdgeInsets.zero,\n      constraints: BoxConstraints.loose(const Size(140, 100)),\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 12.0),\n        child: SizedBox(\n          height: GridSize.popoverItemHeight,\n          child: FlowyButton(\n            text: FlowyText(title),\n            rightIcon: const FlowySvg(FlowySvgs.more_s),\n          ),\n        ),\n      ),\n      popupBuilder: (_) => DateTimeSetting(\n        dateFormat: dateFormat,\n        timeFormat: timeFormat,\n        onDateFormatChanged: (format) {\n          onDateFormatChanged(format);\n          popoverMutex?.close();\n        },\n        onTimeFormatChanged: (format) {\n          onTimeFormatChanged(format);\n          popoverMutex?.close();\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\n\nclass EndTimeButton extends StatelessWidget {\n  const EndTimeButton({\n    super.key,\n    required this.isRange,\n    required this.onChanged,\n  });\n\n  final bool isRange;\n  final Function(bool value) onChanged;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 12.0),\n      child: SizedBox(\n        height: GridSize.popoverItemHeight,\n        child: Padding(\n          padding: GridSize.typeOptionContentInsets,\n          child: Row(\n            children: [\n              FlowySvg(\n                FlowySvgs.date_s,\n                color: Theme.of(context).iconTheme.color,\n              ),\n              const HSpace(6),\n              FlowyText(LocaleKeys.datePicker_isRange.tr()),\n              const Spacer(),\n              Toggle(\n                value: isRange,\n                onChanged: onChanged,\n                padding: EdgeInsets.zero,\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/mobile_date_editor.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\n\nclass MobileDatePicker extends StatefulWidget {\n  const MobileDatePicker({\n    super.key,\n    this.selectedDay,\n    this.startDay,\n    this.endDay,\n    required this.focusedDay,\n    required this.isRange,\n    this.onDaySelected,\n    this.onRangeSelected,\n    this.onPageChanged,\n  });\n\n  final DateTime? selectedDay;\n  final DateTime? startDay;\n  final DateTime? endDay;\n  final DateTime focusedDay;\n\n  final bool isRange;\n\n  final void Function(DateTime)? onDaySelected;\n  final void Function(DateTime?, DateTime?)? onRangeSelected;\n  final void Function(DateTime)? onPageChanged;\n\n  @override\n  State<MobileDatePicker> createState() => _MobileDatePickerState();\n}\n\nclass _MobileDatePickerState extends State<MobileDatePicker> {\n  PageController? pageController;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        const VSpace(8.0),\n        _buildHeader(context),\n        const VSpace(8.0),\n        _buildCalendar(context),\n        const VSpace(16.0),\n      ],\n    );\n  }\n\n  Widget _buildCalendar(BuildContext context) {\n    return DatePicker(\n      isRange: widget.isRange,\n      onDaySelected: (selectedDay, _) {\n        widget.onDaySelected?.call(selectedDay);\n      },\n      focusedDay: widget.focusedDay,\n      onRangeSelected: (start, end, focusedDay) {\n        widget.onRangeSelected?.call(start, end);\n      },\n      selectedDay: widget.selectedDay,\n      startDay: widget.startDay,\n      endDay: widget.endDay,\n      onCalendarCreated: (pageController) {\n        this.pageController = pageController;\n      },\n      onPageChanged: widget.onPageChanged,\n    );\n  }\n\n  Widget _buildHeader(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsetsDirectional.only(start: 16, end: 8),\n      child: Row(\n        children: [\n          Expanded(\n            child: FlowyText(\n              DateFormat.yMMMM().format(widget.focusedDay),\n            ),\n          ),\n          FlowyButton(\n            useIntrinsicWidth: true,\n            text: FlowySvg(\n              FlowySvgs.arrow_left_s,\n              color: Theme.of(context).iconTheme.color,\n              size: const Size.square(24.0),\n            ),\n            onTap: () {\n              pageController?.previousPage(\n                duration: const Duration(milliseconds: 300),\n                curve: Curves.easeOut,\n              );\n            },\n          ),\n          const HSpace(24.0),\n          FlowyButton(\n            useIntrinsicWidth: true,\n            text: FlowySvg(\n              FlowySvgs.arrow_right_s,\n              color: Theme.of(context).iconTheme.color,\n              size: const Size.square(24.0),\n            ),\n            onTap: () {\n              pageController?.nextPage(\n                duration: const Duration(milliseconds: 300),\n                curve: Curves.easeOut,\n              );\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/mobile_date_header.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\n\nconst _height = 44.0;\n\nclass MobileDateHeader extends StatelessWidget {\n  const MobileDateHeader({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      color: Theme.of(context).colorScheme.surface,\n      child: Stack(\n        children: [\n          const Align(\n            alignment: Alignment.centerLeft,\n            child: AppBarCloseButton(),\n          ),\n          Align(\n            child: FlowyText.medium(\n              LocaleKeys.grid_field_dateFieldName.tr(),\n              fontSize: 16,\n            ),\n          ),\n        ].map((e) => SizedBox(height: _height, child: e)).toList(),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/utils/layout.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';\nimport 'package:calendar_view/calendar_view.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\n\ntypedef OnReminderSelected = void Function(ReminderOption option);\n\nclass ReminderSelector extends StatelessWidget {\n  const ReminderSelector({\n    super.key,\n    required this.mutex,\n    required this.selectedOption,\n    required this.onOptionSelected,\n    required this.timeFormat,\n    this.hasTime = false,\n  });\n\n  final PopoverMutex? mutex;\n  final ReminderOption selectedOption;\n  final OnReminderSelected? onOptionSelected;\n  final TimeFormatPB timeFormat;\n  final bool hasTime;\n\n  @override\n  Widget build(BuildContext context) {\n    final options = ReminderOption.values.toList();\n    if (selectedOption != ReminderOption.custom) {\n      options.remove(ReminderOption.custom);\n    }\n\n    options.removeWhere(\n      (o) => !o.timeExempt && (!hasTime ? !o.withoutTime : o.requiresNoTime),\n    );\n\n    final optionWidgets = options.map(\n      (o) {\n        String label = o.label;\n        if (o.withoutTime && !o.timeExempt) {\n          const time = \"09:00\";\n          final t = timeFormat == TimeFormatPB.TwelveHour ? \"$time AM\" : time;\n\n          label = \"$label ($t)\";\n        }\n\n        return SizedBox(\n          height: DatePickerSize.itemHeight,\n          child: FlowyButton(\n            text: FlowyText(label),\n            rightIcon:\n                o == selectedOption ? const FlowySvg(FlowySvgs.check_s) : null,\n            onTap: () {\n              if (o != selectedOption) {\n                onOptionSelected?.call(o);\n                mutex?.close();\n              }\n            },\n          ),\n        );\n      },\n    ).toList();\n\n    return AppFlowyPopover(\n      mutex: mutex,\n      offset: const Offset(8, 0),\n      margin: EdgeInsets.zero,\n      constraints: const BoxConstraints(maxHeight: 400, maxWidth: 205),\n      popupBuilder: (_) => Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Padding(\n            padding: const EdgeInsets.all(6.0),\n            child: SeparatedColumn(\n              children: optionWidgets,\n              separatorBuilder: () => VSpace(DatePickerSize.seperatorHeight),\n            ),\n          ),\n        ],\n      ),\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 12.0),\n        child: SizedBox(\n          height: DatePickerSize.itemHeight,\n          child: FlowyButton(\n            text: FlowyText(LocaleKeys.datePicker_reminderLabel.tr()),\n            rightIcon: Row(\n              children: [\n                FlowyText.regular(selectedOption.label),\n                const FlowySvg(FlowySvgs.more_s),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nenum ReminderOption {\n  none(time: Duration()),\n  atTimeOfEvent(time: Duration()),\n  fiveMinsBefore(time: Duration(minutes: 5)),\n  tenMinsBefore(time: Duration(minutes: 10)),\n  fifteenMinsBefore(time: Duration(minutes: 15)),\n  thirtyMinsBefore(time: Duration(minutes: 30)),\n  oneHourBefore(time: Duration(hours: 1)),\n  twoHoursBefore(time: Duration(hours: 2)),\n  onDayOfEvent(\n    time: Duration(hours: 9),\n    withoutTime: true,\n    requiresNoTime: true,\n  ),\n  // 9:00 AM the day before (24-9)\n  oneDayBefore(time: Duration(hours: 15), withoutTime: true),\n  twoDaysBefore(time: Duration(days: 1, hours: 15), withoutTime: true),\n  oneWeekBefore(time: Duration(days: 6, hours: 15), withoutTime: true),\n  custom(time: Duration());\n\n  const ReminderOption({\n    required this.time,\n    this.withoutTime = false,\n    this.requiresNoTime = false,\n  }) : assert(!requiresNoTime || withoutTime);\n\n  final Duration time;\n\n  /// If true, don't consider the time component of the dateTime\n  final bool withoutTime;\n\n  /// If true, [withoutTime] must be true as well. Will add time instead of subtract to get notification time.\n  final bool requiresNoTime;\n\n  bool get timeExempt =>\n      [ReminderOption.none, ReminderOption.custom].contains(this);\n\n  String get label => switch (this) {\n        ReminderOption.none => LocaleKeys.datePicker_reminderOptions_none.tr(),\n        ReminderOption.atTimeOfEvent =>\n          LocaleKeys.datePicker_reminderOptions_atTimeOfEvent.tr(),\n        ReminderOption.fiveMinsBefore =>\n          LocaleKeys.datePicker_reminderOptions_fiveMinsBefore.tr(),\n        ReminderOption.tenMinsBefore =>\n          LocaleKeys.datePicker_reminderOptions_tenMinsBefore.tr(),\n        ReminderOption.fifteenMinsBefore =>\n          LocaleKeys.datePicker_reminderOptions_fifteenMinsBefore.tr(),\n        ReminderOption.thirtyMinsBefore =>\n          LocaleKeys.datePicker_reminderOptions_thirtyMinsBefore.tr(),\n        ReminderOption.oneHourBefore =>\n          LocaleKeys.datePicker_reminderOptions_oneHourBefore.tr(),\n        ReminderOption.twoHoursBefore =>\n          LocaleKeys.datePicker_reminderOptions_twoHoursBefore.tr(),\n        ReminderOption.onDayOfEvent =>\n          LocaleKeys.datePicker_reminderOptions_onDayOfEvent.tr(),\n        ReminderOption.oneDayBefore =>\n          LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(),\n        ReminderOption.twoDaysBefore =>\n          LocaleKeys.datePicker_reminderOptions_twoDaysBefore.tr(),\n        ReminderOption.oneWeekBefore =>\n          LocaleKeys.datePicker_reminderOptions_oneWeekBefore.tr(),\n        ReminderOption.custom =>\n          LocaleKeys.datePicker_reminderOptions_custom.tr(),\n      };\n\n  static ReminderOption fromDateDifference(\n    DateTime eventDate,\n    DateTime reminderDate,\n  ) {\n    final def = fromMinutes(eventDate.difference(reminderDate).inMinutes);\n    if (def != ReminderOption.custom) {\n      return def;\n    }\n\n    final diff = eventDate.withoutTime.difference(reminderDate).inMinutes;\n    return fromMinutes(diff);\n  }\n\n  static ReminderOption fromMinutes(int minutes) => switch (minutes) {\n        0 => ReminderOption.atTimeOfEvent,\n        5 => ReminderOption.fiveMinsBefore,\n        10 => ReminderOption.tenMinsBefore,\n        15 => ReminderOption.fifteenMinsBefore,\n        30 => ReminderOption.thirtyMinsBefore,\n        60 => ReminderOption.oneHourBefore,\n        120 => ReminderOption.twoHoursBefore,\n        // Negative because Event Day Today + 940 minutes\n        -540 => ReminderOption.onDayOfEvent,\n        900 => ReminderOption.oneDayBefore,\n        2340 => ReminderOption.twoDaysBefore,\n        9540 => ReminderOption.oneWeekBefore,\n        _ => ReminderOption.custom,\n      };\n\n  DateTime getNotificationDateTime(DateTime date) {\n    return withoutTime\n        ? requiresNoTime\n            ? date.withoutTime.add(time)\n            : date.withoutTime.subtract(time)\n        : date.subtract(time);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\n\ntypedef SimpleAFDialogAction = (String, void Function(BuildContext)?);\n\n/// A simple dialog with a title, content, and actions.\n///\n/// The primary button is a filled button and colored using theme or destructive\n/// color depending on the [isDestructive] parameter. The secondary button is an\n/// outlined button.\n///\nFuture<void> showSimpleAFDialog({\n  required BuildContext context,\n  required String title,\n  required String content,\n  bool isDestructive = false,\n  required SimpleAFDialogAction primaryAction,\n  SimpleAFDialogAction? secondaryAction,\n  bool barrierDismissible = true,\n}) {\n  final theme = AppFlowyTheme.of(context);\n\n  return showDialog(\n    context: context,\n    barrierColor: theme.surfaceColorScheme.overlay,\n    barrierDismissible: barrierDismissible,\n    builder: (_) {\n      return AFModal(\n        constraints: BoxConstraints(\n          maxWidth: AFModalDimension.S,\n        ),\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            AFModalHeader(\n              leading: Text(\n                title,\n              ),\n              trailing: [\n                AFGhostButton.normal(\n                  onTap: () => Navigator.of(context).pop(),\n                  padding: EdgeInsets.all(theme.spacing.xs),\n                  builder: (context, isHovering, disabled) {\n                    return FlowySvg(\n                      FlowySvgs.toast_close_s,\n                      size: Size.square(20),\n                    );\n                  },\n                ),\n              ],\n            ),\n            Flexible(\n              child: ConstrainedBox(\n                // AFModalDimension.dialogHeight - header - footer\n                constraints: BoxConstraints(minHeight: 108.0),\n                child: AFModalBody(\n                  child: Text(\n                    content,\n                    style: theme.textStyle.body.standard(\n                      color: theme.textColorScheme.primary,\n                    ),\n                  ),\n                ),\n              ),\n            ),\n            AFModalFooter(\n              trailing: [\n                if (secondaryAction != null)\n                  AFOutlinedTextButton.normal(\n                    text: secondaryAction.$1,\n                    onTap: () {\n                      secondaryAction.$2?.call(context);\n                      Navigator.of(context).pop();\n                    },\n                  ),\n                isDestructive\n                    ? AFFilledTextButton.destructive(\n                        text: primaryAction.$1,\n                        onTap: () {\n                          primaryAction.$2?.call(context);\n                          Navigator.of(context).pop();\n                        },\n                      )\n                    : AFFilledTextButton.primary(\n                        text: primaryAction.$1,\n                        onTap: () {\n                          primaryAction.$2?.call(context);\n                          Navigator.of(context).pop();\n                        },\n                      ),\n              ],\n            ),\n          ],\n        ),\n      );\n    },\n  );\n}\n\n/// Shows a dialog for renaming an item with a text field.\n/// The API is flexible: either provide a callback for confirmation or use the\n/// returned Future to get the new value.\n///\nFuture<String?> showAFTextFieldDialog({\n  required BuildContext context,\n  required String title,\n  required String initialValue,\n  void Function(String)? onConfirm,\n  bool barrierDismissible = true,\n  bool selectAll = true,\n  int? maxLength,\n  String? hintText,\n}) {\n  return showDialog<String?>(\n    context: context,\n    barrierColor: AppFlowyTheme.of(context).surfaceColorScheme.overlay,\n    barrierDismissible: barrierDismissible,\n    builder: (context) {\n      return AFTextFieldDialog(\n        title: title,\n        initialValue: initialValue,\n        onConfirm: onConfirm,\n        selectAll: selectAll,\n        maxLength: maxLength,\n        hintText: hintText,\n      );\n    },\n  );\n}\n\nclass AFTextFieldDialog extends StatefulWidget {\n  const AFTextFieldDialog({\n    super.key,\n    required this.title,\n    required this.initialValue,\n    this.onConfirm,\n    this.selectAll = true,\n    this.maxLength,\n    this.hintText,\n  });\n\n  final String title;\n  final String initialValue;\n  final void Function(String)? onConfirm;\n  final bool selectAll;\n  final int? maxLength;\n  final String? hintText;\n\n  @override\n  State<AFTextFieldDialog> createState() => _AFTextFieldDialogState();\n}\n\nclass _AFTextFieldDialogState extends State<AFTextFieldDialog> {\n  final textController = TextEditingController();\n\n  @override\n  void initState() {\n    super.initState();\n    textController.value = TextEditingValue(\n      text: widget.initialValue,\n      selection: widget.selectAll\n          ? TextSelection(\n              baseOffset: 0,\n              extentOffset: widget.initialValue.length,\n            )\n          : TextSelection.collapsed(\n              offset: widget.initialValue.length,\n            ),\n    );\n  }\n\n  @override\n  void dispose() {\n    textController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return AFModal(\n      constraints: BoxConstraints(\n        maxWidth: AFModalDimension.S,\n      ),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          AFModalHeader(\n            leading: Text(\n              widget.title,\n            ),\n            trailing: [\n              AFGhostButton.normal(\n                onTap: () => Navigator.of(context).pop(),\n                padding: EdgeInsets.all(theme.spacing.xs),\n                builder: (context, isHovering, disabled) {\n                  return FlowySvg(\n                    FlowySvgs.toast_close_s,\n                    size: Size.square(20),\n                  );\n                },\n              ),\n            ],\n          ),\n          Flexible(\n            child: AFModalBody(\n              child: AFTextField(\n                autoFocus: true,\n                size: AFTextFieldSize.m,\n                hintText: widget.hintText,\n                maxLength: widget.maxLength,\n                controller: textController,\n                onSubmitted: (_) {\n                  handleConfirm();\n                },\n              ),\n            ),\n          ),\n          AFModalFooter(\n            trailing: [\n              AFOutlinedTextButton.normal(\n                text: LocaleKeys.button_cancel.tr(),\n                onTap: () => Navigator.of(context).pop(),\n              ),\n              ValueListenableBuilder(\n                valueListenable: textController,\n                builder: (contex, value, child) {\n                  return AFFilledTextButton.primary(\n                    text: LocaleKeys.button_confirm.tr(),\n                    disabled: value.text.trim().isEmpty,\n                    onTap: handleConfirm,\n                  );\n                },\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n\n  void handleConfirm() {\n    final text = textController.text.trim();\n\n    if (text.isEmpty) {\n      return;\n    }\n\n    widget.onConfirm?.call(text);\n    Navigator.of(context).pop(text);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/buttons/primary_button.dart';\nimport 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';\nimport 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:toastification/toastification.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nexport 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';\nexport 'package:toastification/toastification.dart';\n\nclass NavigatorCustomDialog extends StatefulWidget {\n  const NavigatorCustomDialog({\n    super.key,\n    required this.child,\n    this.cancel,\n    this.confirm,\n    this.hideCancelButton = false,\n  });\n\n  final Widget child;\n  final void Function()? cancel;\n  final void Function()? confirm;\n  final bool hideCancelButton;\n\n  @override\n  State<NavigatorCustomDialog> createState() => _NavigatorCustomDialog();\n}\n\nclass _NavigatorCustomDialog extends State<NavigatorCustomDialog> {\n  @override\n  Widget build(BuildContext context) {\n    return StyledDialog(\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: <Widget>[\n          ...[\n            ConstrainedBox(\n              constraints: const BoxConstraints(\n                maxWidth: 400,\n                maxHeight: 260,\n              ),\n              child: widget.child,\n            ),\n          ],\n          if (widget.confirm != null) ...[\n            const VSpace(20),\n            OkCancelButton(\n              onOkPressed: () {\n                widget.confirm?.call();\n                Navigator.of(context).pop();\n              },\n              onCancelPressed: widget.hideCancelButton\n                  ? null\n                  : () {\n                      widget.cancel?.call();\n                      Navigator.of(context).pop();\n                    },\n            ),\n          ],\n        ],\n      ),\n    );\n  }\n}\n\nclass NavigatorAlertDialog extends StatefulWidget {\n  const NavigatorAlertDialog({\n    super.key,\n    required this.title,\n    this.cancel,\n    this.confirm,\n    this.hideCancelButton = false,\n    this.constraints,\n  });\n\n  final String title;\n  final void Function()? cancel;\n  final void Function()? confirm;\n  final bool hideCancelButton;\n  final BoxConstraints? constraints;\n\n  @override\n  State<NavigatorAlertDialog> createState() => _CreateFlowyAlertDialog();\n}\n\nclass _CreateFlowyAlertDialog extends State<NavigatorAlertDialog> {\n  @override\n  Widget build(BuildContext context) {\n    return StyledDialog(\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        mainAxisAlignment: MainAxisAlignment.center,\n        children: <Widget>[\n          ...[\n            ConstrainedBox(\n              constraints: widget.constraints ??\n                  const BoxConstraints(\n                    maxWidth: 400,\n                    maxHeight: 260,\n                  ),\n              child: FlowyText.medium(\n                widget.title,\n                fontSize: FontSizes.s16,\n                textAlign: TextAlign.center,\n                color: Theme.of(context).colorScheme.tertiary,\n                maxLines: null,\n              ),\n            ),\n          ],\n          if (widget.confirm != null) ...[\n            const VSpace(20),\n            OkCancelButton(\n              onOkPressed: () {\n                widget.confirm?.call();\n                Navigator.of(context).pop();\n              },\n              onCancelPressed: widget.hideCancelButton\n                  ? null\n                  : () {\n                      widget.cancel?.call();\n                      Navigator.of(context).pop();\n                    },\n            ),\n          ],\n        ],\n      ),\n    );\n  }\n}\n\nclass NavigatorOkCancelDialog extends StatelessWidget {\n  const NavigatorOkCancelDialog({\n    super.key,\n    this.onOkPressed,\n    this.onCancelPressed,\n    this.okTitle,\n    this.cancelTitle,\n    this.title,\n    this.message,\n    this.maxWidth,\n    this.titleUpperCase = true,\n    this.autoDismiss = true,\n  });\n\n  final VoidCallback? onOkPressed;\n  final VoidCallback? onCancelPressed;\n  final String? okTitle;\n  final String? cancelTitle;\n  final String? title;\n  final String? message;\n  final double? maxWidth;\n  final bool titleUpperCase;\n  final bool autoDismiss;\n\n  @override\n  Widget build(BuildContext context) {\n    final onCancel = onCancelPressed == null\n        ? null\n        : () {\n            onCancelPressed?.call();\n            if (autoDismiss) {\n              Navigator.of(context).pop();\n            }\n          };\n    return StyledDialog(\n      maxWidth: maxWidth ?? 500,\n      padding: EdgeInsets.symmetric(horizontal: Insets.xl, vertical: Insets.l),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: <Widget>[\n          if (title != null) ...[\n            FlowyText.medium(\n              titleUpperCase ? title!.toUpperCase() : title!,\n              fontSize: FontSizes.s16,\n              maxLines: 3,\n            ),\n            VSpace(Insets.sm * 1.5),\n            Container(\n              color: Theme.of(context).colorScheme.surfaceContainerHighest,\n              height: 1,\n            ),\n            VSpace(Insets.m * 1.5),\n          ],\n          if (message != null)\n            FlowyText.medium(\n              message!,\n              maxLines: 3,\n            ),\n          SizedBox(height: Insets.l),\n          OkCancelButton(\n            onOkPressed: () {\n              onOkPressed?.call();\n              if (autoDismiss) {\n                Navigator.of(context).pop();\n              }\n            },\n            onCancelPressed: onCancel,\n            okTitle: okTitle?.toUpperCase(),\n            cancelTitle: cancelTitle?.toUpperCase(),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass OkCancelButton extends StatelessWidget {\n  const OkCancelButton({\n    super.key,\n    this.onOkPressed,\n    this.onCancelPressed,\n    this.okTitle,\n    this.cancelTitle,\n    this.minHeight,\n    this.alignment = MainAxisAlignment.spaceAround,\n    this.mode = TextButtonMode.big,\n  });\n\n  final VoidCallback? onOkPressed;\n  final VoidCallback? onCancelPressed;\n  final String? okTitle;\n  final String? cancelTitle;\n  final double? minHeight;\n  final MainAxisAlignment alignment;\n  final TextButtonMode mode;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 48,\n      child: Row(\n        mainAxisAlignment: alignment,\n        children: <Widget>[\n          if (onCancelPressed != null)\n            SecondaryTextButton(\n              cancelTitle ?? LocaleKeys.button_cancel.tr(),\n              onPressed: onCancelPressed,\n              mode: mode,\n            ),\n          if (onCancelPressed != null) HSpace(Insets.m),\n          if (onOkPressed != null)\n            PrimaryTextButton(\n              okTitle ?? LocaleKeys.button_ok.tr(),\n              onPressed: onOkPressed,\n              mode: mode,\n            ),\n        ],\n      ),\n    );\n  }\n}\n\nToastificationItem showToastNotification({\n  BuildContext? context,\n  String? message,\n  TextSpan? richMessage,\n  String? description,\n  ToastificationType type = ToastificationType.success,\n  ToastificationCallbacks? callbacks,\n  bool showCloseButton = false,\n  double bottomPadding = 100,\n}) {\n  assert(\n    (message == null) != (richMessage == null),\n    \"Exactly one of message or richMessage must be non-null.\",\n  );\n  return toastification.showCustom(\n    context: context,\n    alignment: Alignment.bottomCenter,\n    autoCloseDuration: const Duration(milliseconds: 3000),\n    callbacks: callbacks ?? const ToastificationCallbacks(),\n    builder: (_, item) {\n      return UniversalPlatform.isMobile\n          ? _MobileToast(\n              message: message,\n              type: type,\n              bottomPadding: bottomPadding,\n              description: description,\n            )\n          : DesktopToast(\n              message: message,\n              richMessage: richMessage,\n              type: type,\n              onDismiss: () => toastification.dismiss(item),\n              showCloseButton: showCloseButton,\n            );\n    },\n  );\n}\n\nclass _MobileToast extends StatelessWidget {\n  const _MobileToast({\n    this.message,\n    this.type = ToastificationType.success,\n    this.bottomPadding = 100,\n    this.description,\n  });\n\n  final String? message;\n  final ToastificationType type;\n  final double bottomPadding;\n  final String? description;\n\n  @override\n  Widget build(BuildContext context) {\n    if (message == null) {\n      return const SizedBox.shrink();\n    }\n    final hintText = FlowyText.regular(\n      message!,\n      fontSize: 16.0,\n      figmaLineHeight: 18.0,\n      color: Colors.white,\n      maxLines: 10,\n    );\n    final descriptionText = description != null\n        ? FlowyText.regular(\n            description!,\n            fontSize: 12,\n            color: Colors.white,\n            maxLines: 10,\n          )\n        : null;\n    return Container(\n      alignment: Alignment.bottomCenter,\n      padding: EdgeInsets.only(\n        bottom: bottomPadding,\n        left: 16,\n        right: 16,\n      ),\n      child: Container(\n        padding: const EdgeInsets.symmetric(\n          horizontal: 12.0,\n          vertical: 13.0,\n        ),\n        decoration: BoxDecoration(\n          borderRadius: BorderRadius.circular(12.0),\n          color: const Color(0xE5171717),\n        ),\n        child: type == ToastificationType.success\n            ? Column(\n                mainAxisSize: MainAxisSize.min,\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  Row(\n                    mainAxisSize: MainAxisSize.min,\n                    children: [\n                      if (type == ToastificationType.success) ...[\n                        const FlowySvg(\n                          FlowySvgs.success_s,\n                          blendMode: null,\n                        ),\n                        const HSpace(8.0),\n                      ],\n                      Expanded(child: hintText),\n                    ],\n                  ),\n                  if (descriptionText != null) ...[\n                    const VSpace(4.0),\n                    descriptionText,\n                  ],\n                ],\n              )\n            : Column(\n                mainAxisSize: MainAxisSize.min,\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  hintText,\n                  if (descriptionText != null) ...[\n                    const VSpace(4.0),\n                    descriptionText,\n                  ],\n                ],\n              ),\n      ),\n    );\n  }\n}\n\n@visibleForTesting\nclass DesktopToast extends StatelessWidget {\n  const DesktopToast({\n    super.key,\n    this.message,\n    this.richMessage,\n    required this.type,\n    this.onDismiss,\n    this.showCloseButton = false,\n  });\n\n  final String? message;\n  final TextSpan? richMessage;\n  final ToastificationType type;\n  final void Function()? onDismiss;\n  final bool showCloseButton;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return Center(\n      child: Container(\n        constraints: const BoxConstraints(maxWidth: 360.0),\n        padding: EdgeInsets.symmetric(\n          horizontal: theme.spacing.xl,\n          vertical: theme.spacing.l,\n        ),\n        margin: const EdgeInsets.only(bottom: 32.0),\n        decoration: BoxDecoration(\n          color: theme.surfaceColorScheme.inverse,\n          borderRadius: BorderRadius.circular(theme.borderRadius.l),\n          boxShadow: theme.shadow.small,\n        ),\n        child: Row(\n          mainAxisSize: MainAxisSize.min,\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            // icon\n            FlowySvg(\n              switch (type) {\n                ToastificationType.warning => FlowySvgs.toast_warning_filled_s,\n                ToastificationType.success => FlowySvgs.toast_checked_filled_s,\n                ToastificationType.error => FlowySvgs.toast_error_filled_s,\n                _ => throw UnimplementedError(),\n              },\n              size: const Size.square(20.0),\n              blendMode: null,\n            ),\n            HSpace(\n              theme.spacing.m,\n            ),\n            // text\n            Flexible(\n              child: message != null\n                  ? Text(\n                      message!,\n                      maxLines: 2,\n                      overflow: TextOverflow.ellipsis,\n                      style: theme.textStyle.body.standard(\n                        color: theme.textColorScheme.onFill,\n                      ),\n                    )\n                  : RichText(\n                      text: richMessage!,\n                      maxLines: 2,\n                      overflow: TextOverflow.ellipsis,\n                    ),\n            ),\n            if (showCloseButton) ...[\n              HSpace(\n                theme.spacing.xl,\n              ),\n              // close\n              MouseRegion(\n                cursor: SystemMouseCursors.click,\n                child: GestureDetector(\n                  behavior: HitTestBehavior.opaque,\n                  onTap: onDismiss,\n                  child: const SizedBox.square(\n                    dimension: 24.0,\n                    child: Center(\n                      child: FlowySvg(\n                        FlowySvgs.toast_close_s,\n                        size: Size.square(20.0),\n                        color: Color(0xFFBDBDBD),\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n            ],\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nFuture<void> showConfirmDeletionDialog({\n  required BuildContext context,\n  required String name,\n  required String description,\n  required VoidCallback onConfirm,\n}) {\n  return showDialog(\n    context: context,\n    builder: (_) {\n      final title = LocaleKeys.space_deleteConfirmation.tr() + name;\n      return Dialog(\n        shape: RoundedRectangleBorder(\n          borderRadius: BorderRadius.circular(12.0),\n        ),\n        child: SizedBox(\n          width: 440,\n          child: ConfirmPopup(\n            title: title,\n            description: description,\n            onConfirm: (_) => onConfirm(),\n          ),\n        ),\n      );\n    },\n  );\n}\n\nFuture<void> showConfirmDialog({\n  required BuildContext context,\n  required String title,\n  required String description,\n  TextStyle? titleStyle,\n  TextStyle? descriptionStyle,\n  void Function(BuildContext context)? onConfirm,\n  VoidCallback? onCancel,\n  String? confirmLabel,\n  ConfirmPopupStyle style = ConfirmPopupStyle.onlyOk,\n  WidgetBuilder? confirmButtonBuilder,\n  Color? confirmButtonColor,\n}) {\n  return showDialog(\n    context: context,\n    builder: (context) {\n      return Dialog(\n        shape: RoundedRectangleBorder(\n          borderRadius: BorderRadius.circular(12.0),\n        ),\n        child: SizedBox(\n          width: 440,\n          child: ConfirmPopup(\n            title: title,\n            description: description,\n            titleStyle: titleStyle,\n            descriptionStyle: descriptionStyle,\n            confirmButtonBuilder: confirmButtonBuilder,\n            onConfirm: (_) => onConfirm?.call(context),\n            onCancel: () => onCancel?.call(),\n            confirmLabel: confirmLabel,\n            style: style,\n            confirmButtonColor: confirmButtonColor,\n          ),\n        ),\n      );\n    },\n  );\n}\n\nFuture<void> showCancelAndConfirmDialog({\n  required BuildContext context,\n  required String title,\n  required String description,\n  void Function(BuildContext context)? onConfirm,\n  VoidCallback? onCancel,\n  String? confirmLabel,\n}) {\n  return showDialog(\n    context: context,\n    builder: (_) {\n      return Dialog(\n        shape: RoundedRectangleBorder(\n          borderRadius: BorderRadius.circular(12.0),\n        ),\n        child: SizedBox(\n          width: 440,\n          child: ConfirmPopup(\n            title: title,\n            description: description,\n            onConfirm: (context) => onConfirm?.call(context),\n            confirmLabel: confirmLabel,\n            confirmButtonColor: Theme.of(context).colorScheme.primary,\n            onCancel: () => onCancel?.call(),\n          ),\n        ),\n      );\n    },\n  );\n}\n\nFuture<void> showCustomConfirmDialog({\n  required BuildContext context,\n  required String title,\n  required String description,\n  required Widget Function(BuildContext) builder,\n  VoidCallback? onConfirm,\n  VoidCallback? onCancel,\n  String? confirmLabel,\n  ConfirmPopupStyle style = ConfirmPopupStyle.onlyOk,\n  bool closeOnConfirm = true,\n  bool showCloseButton = true,\n  bool enableKeyboardListener = true,\n  bool barrierDismissible = true,\n}) {\n  return showDialog(\n    context: context,\n    barrierDismissible: barrierDismissible,\n    builder: (context) {\n      return Dialog(\n        shape: RoundedRectangleBorder(\n          borderRadius: BorderRadius.circular(12.0),\n        ),\n        child: SizedBox(\n          width: 440,\n          child: ConfirmPopup(\n            title: title,\n            description: description,\n            onConfirm: (_) => onConfirm?.call(),\n            onCancel: onCancel,\n            confirmLabel: confirmLabel,\n            confirmButtonColor: Theme.of(context).colorScheme.primary,\n            style: style,\n            closeOnAction: closeOnConfirm,\n            showCloseButton: showCloseButton,\n            enableKeyboardListener: enableKeyboardListener,\n            child: builder(context),\n          ),\n        ),\n      );\n    },\n  );\n}\n\nFuture<void> showCancelAndDeleteDialog({\n  required BuildContext context,\n  required String title,\n  required String description,\n  Widget Function(BuildContext)? builder,\n  VoidCallback? onDelete,\n  String? confirmLabel,\n  bool closeOnAction = false,\n}) {\n  return showDialog(\n    context: context,\n    builder: (_) {\n      return Dialog(\n        shape: RoundedRectangleBorder(\n          borderRadius: BorderRadius.circular(12.0),\n        ),\n        child: SizedBox(\n          width: 440,\n          child: ConfirmPopup(\n            title: title,\n            description: description,\n            onConfirm: (_) => onDelete?.call(),\n            closeOnAction: closeOnAction,\n            confirmLabel: confirmLabel,\n            confirmButtonColor: Theme.of(context).colorScheme.error,\n            child: builder?.call(context),\n          ),\n        ),\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/draggable_item/draggable_item.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\n/// This value is used to disable the auto scroll when dragging.\n///\n/// It is used to prevent the auto scroll when dragging a view item to a document.\nbool disableAutoScrollWhenDragging = false;\n\nclass DraggableItem<T extends Object> extends StatefulWidget {\n  const DraggableItem({\n    super.key,\n    required this.child,\n    required this.data,\n    this.feedback,\n    this.childWhenDragging,\n    this.onAcceptWithDetails,\n    this.onWillAcceptWithDetails,\n    this.onMove,\n    this.onLeave,\n    this.enableAutoScroll = true,\n    this.hitTestSize = const Size(100, 100),\n    this.onDragging,\n  });\n\n  final T data;\n\n  final Widget child;\n  final Widget? feedback;\n  final Widget? childWhenDragging;\n\n  final DragTargetAcceptWithDetails<T>? onAcceptWithDetails;\n  final DragTargetWillAcceptWithDetails<T>? onWillAcceptWithDetails;\n  final DragTargetMove<T>? onMove;\n  final DragTargetLeave<T>? onLeave;\n\n  /// Whether to enable auto scroll when dragging.\n  ///\n  /// If true, the draggable item must be wrapped inside a [Scrollable] widget.\n  final bool enableAutoScroll;\n  final Size hitTestSize;\n\n  final void Function(bool isDragging)? onDragging;\n\n  @override\n  State<DraggableItem<T>> createState() => _DraggableItemState<T>();\n}\n\nclass _DraggableItemState<T extends Object> extends State<DraggableItem<T>> {\n  ScrollableState? scrollable;\n  EdgeDraggingAutoScroller? autoScroller;\n  Rect? dragTarget;\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n\n    initAutoScrollerIfNeeded(context);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    initAutoScrollerIfNeeded(context);\n\n    return DragTarget(\n      onAcceptWithDetails: widget.onAcceptWithDetails,\n      onWillAcceptWithDetails: widget.onWillAcceptWithDetails,\n      onMove: widget.onMove,\n      onLeave: widget.onLeave,\n      builder: (_, __, ___) => _Draggable<T>(\n        data: widget.data,\n        feedback: widget.feedback ?? widget.child,\n        childWhenDragging: widget.childWhenDragging ?? widget.child,\n        child: widget.child,\n        onDragUpdate: (details) {\n          if (widget.enableAutoScroll && !disableAutoScrollWhenDragging) {\n            dragTarget = details.globalPosition & widget.hitTestSize;\n            autoScroller?.startAutoScrollIfNecessary(dragTarget!);\n          }\n          widget.onDragging?.call(true);\n        },\n        onDragEnd: (details) {\n          autoScroller?.stopAutoScroll();\n          dragTarget = null;\n          widget.onDragging?.call(false);\n        },\n        onDraggableCanceled: (_, __) {\n          autoScroller?.stopAutoScroll();\n          dragTarget = null;\n          widget.onDragging?.call(false);\n        },\n      ),\n    );\n  }\n\n  void initAutoScrollerIfNeeded(BuildContext context) {\n    if (!widget.enableAutoScroll || disableAutoScrollWhenDragging) {\n      return;\n    }\n\n    scrollable = Scrollable.of(context);\n    if (scrollable == null) {\n      throw FlutterError(\n        'DraggableItem must be wrapped inside a Scrollable widget '\n        'when enableAutoScroll is true.',\n      );\n    }\n\n    autoScroller?.stopAutoScroll();\n    autoScroller = EdgeDraggingAutoScroller(\n      scrollable!,\n      onScrollViewScrolled: () {\n        if (dragTarget != null && !disableAutoScrollWhenDragging) {\n          autoScroller!.startAutoScrollIfNecessary(dragTarget!);\n        }\n      },\n      velocityScalar: 20,\n    );\n  }\n}\n\nclass _Draggable<T extends Object> extends StatelessWidget {\n  const _Draggable({\n    required this.child,\n    required this.feedback,\n    this.data,\n    this.childWhenDragging,\n    this.onDragStarted,\n    this.onDragUpdate,\n    this.onDraggableCanceled,\n    this.onDragEnd,\n    this.onDragCompleted,\n  });\n\n  /// The data that will be dropped by this draggable.\n  final T? data;\n\n  final Widget child;\n\n  final Widget? childWhenDragging;\n  final Widget feedback;\n\n  /// Called when the draggable starts being dragged.\n  final VoidCallback? onDragStarted;\n\n  final DragUpdateCallback? onDragUpdate;\n\n  final DraggableCanceledCallback? onDraggableCanceled;\n\n  final VoidCallback? onDragCompleted;\n  final DragEndCallback? onDragEnd;\n\n  @override\n  Widget build(BuildContext context) {\n    return UniversalPlatform.isMobile\n        ? LongPressDraggable<T>(\n            data: data,\n            feedback: feedback,\n            childWhenDragging: childWhenDragging,\n            onDragUpdate: onDragUpdate,\n            onDragEnd: onDragEnd,\n            onDraggableCanceled: onDraggableCanceled,\n            child: child,\n          )\n        : Draggable<T>(\n            data: data,\n            feedback: feedback,\n            childWhenDragging: childWhenDragging,\n            onDragUpdate: onDragUpdate,\n            onDragEnd: onDragEnd,\n            onDraggableCanceled: onDraggableCanceled,\n            child: child,\n          );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/edit_panel/edit_panel.dart",
    "content": "import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart';\nimport 'package:appflowy/workspace/application/edit_panel/edit_context.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/workspace/presentation/home/home_sizes.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/bar_title.dart';\nimport 'package:flowy_infra_ui/style_widget/close_button.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\n\nclass EditPanel extends StatelessWidget {\n  const EditPanel({\n    super.key,\n    required this.panelContext,\n    required this.onEndEdit,\n  });\n\n  final EditPanelContext panelContext;\n  final VoidCallback onEndEdit;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      color: Theme.of(context).colorScheme.secondary,\n      child: BlocProvider(\n        create: (context) => getIt<EditPanelBloc>(),\n        child: BlocBuilder<EditPanelBloc, EditPanelState>(\n          builder: (context, state) {\n            return Column(\n              crossAxisAlignment: CrossAxisAlignment.stretch,\n              children: [\n                EditPanelTopBar(onClose: () => onEndEdit()),\n                Expanded(\n                  child: panelContext.child,\n                ),\n              ],\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass EditPanelTopBar extends StatelessWidget {\n  const EditPanelTopBar({super.key, required this.onClose});\n\n  final VoidCallback onClose;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: HomeSizes.editPanelTopBarHeight,\n      child: Padding(\n        padding: const EdgeInsets.all(8.0),\n        child: Row(\n          children: [\n            FlowyBarTitle(\n              title: LocaleKeys.title.tr(),\n            ),\n            const Spacer(),\n            FlowyCloseButton(onPressed: onClose),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/edit_panel/panel_animation.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass AnimatedPanel extends StatefulWidget {\n  const AnimatedPanel({\n    super.key,\n    this.isClosed = false,\n    this.closedX = 0.0,\n    this.closedY = 0.0,\n    this.duration = 0.0,\n    this.curve,\n    this.child,\n  });\n\n  final bool isClosed;\n  final double closedX;\n  final double closedY;\n  final double duration;\n  final Curve? curve;\n  final Widget? child;\n\n  @override\n  AnimatedPanelState createState() => AnimatedPanelState();\n}\n\nclass AnimatedPanelState extends State<AnimatedPanel> {\n  bool _isHidden = true;\n\n  @override\n  Widget build(BuildContext context) {\n    final Offset closePos = Offset(widget.closedX, widget.closedY);\n    final double duration = _isHidden && widget.isClosed ? 0 : widget.duration;\n    return TweenAnimationBuilder(\n      curve: widget.curve ?? Curves.easeOut,\n      tween: Tween<Offset>(\n        begin: !widget.isClosed ? Offset.zero : closePos,\n        end: !widget.isClosed ? Offset.zero : closePos,\n      ),\n      duration: Duration(milliseconds: (duration * 1000).round()),\n      builder: (_, Offset value, Widget? c) {\n        _isHidden =\n            widget.isClosed && value == Offset(widget.closedX, widget.closedY);\n        return _isHidden\n            ? const SizedBox.shrink()\n            : Transform.translate(offset: value, child: c);\n      },\n      child: widget.child,\n    );\n  }\n}\n\nextension AnimatedPanelExtensions on Widget {\n  Widget animatedPanelX({\n    double closeX = 0.0,\n    bool? isClosed,\n    double? duration,\n    Curve? curve,\n  }) =>\n      animatedPanel(\n        closePos: Offset(closeX, 0),\n        isClosed: isClosed,\n        curve: curve,\n        duration: duration,\n      );\n\n  Widget animatedPanelY({\n    double closeY = 0.0,\n    bool? isClosed,\n    double? duration,\n    Curve? curve,\n  }) =>\n      animatedPanel(\n        closePos: Offset(0, closeY),\n        isClosed: isClosed,\n        curve: curve,\n        duration: duration,\n      );\n\n  Widget animatedPanel({\n    required Offset closePos,\n    bool? isClosed,\n    double? duration,\n    Curve? curve,\n  }) {\n    return AnimatedPanel(\n      closedX: closePos.dx,\n      closedY: closePos.dy,\n      isClosed: isClosed ?? false,\n      duration: duration ?? .35,\n      curve: curve,\n      child: this,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/favorite_button.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass ViewFavoriteButton extends StatelessWidget {\n  const ViewFavoriteButton({\n    super.key,\n    required this.view,\n  });\n\n  final ViewPB view;\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<FavoriteBloc, FavoriteState>(\n      builder: (context, state) {\n        final isFavorite = state.views.any((v) => v.item.id == view.id);\n        return Listener(\n          onPointerDown: (_) =>\n              context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view)),\n          child: FlowyTooltip(\n            message: isFavorite\n                ? LocaleKeys.button_removeFromFavorites.tr()\n                : LocaleKeys.button_addToFavorites.tr(),\n            child: FlowyHover(\n              resetHoverOnRebuild: false,\n              child: Padding(\n                padding: const EdgeInsets.all(6),\n                child: FlowySvg(\n                  isFavorite ? FlowySvgs.favorited_s : FlowySvgs.favorite_s,\n                  size: const Size.square(18),\n                  blendMode: isFavorite ? null : BlendMode.srcIn,\n                ),\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/startup/tasks/rust_sdk.dart';\nimport 'package:appflowy/util/theme_extension.dart';\nimport 'package:appflowy/workspace/presentation/home/toast.dart';\nimport 'package:appflowy/workspace/presentation/widgets/float_bubble/social_media_section.dart';\nimport 'package:appflowy/workspace/presentation/widgets/float_bubble/version_section.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:device_info_plus/device_info_plus.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nclass QuestionBubble extends StatelessWidget {\n  const QuestionBubble({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return const SizedBox.square(\n      dimension: 32.0,\n      child: BubbleActionList(),\n    );\n  }\n}\n\nclass BubbleActionList extends StatefulWidget {\n  const BubbleActionList({super.key});\n\n  @override\n  State<BubbleActionList> createState() => _BubbleActionListState();\n}\n\nclass _BubbleActionListState extends State<BubbleActionList> {\n  bool isOpen = false;\n\n  Color get fontColor => isOpen\n      ? Theme.of(context).colorScheme.onPrimary\n      : Theme.of(context).colorScheme.tertiary;\n\n  Color get fillColor => isOpen\n      ? Theme.of(context).colorScheme.primary\n      : Theme.of(context).colorScheme.tertiaryContainer;\n\n  void toggle() {\n    setState(() {\n      isOpen = !isOpen;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final List<PopoverAction> actions = [];\n    actions.addAll(\n      BubbleAction.values.map((action) => BubbleActionWrapper(action)),\n    );\n\n    actions.add(SocialMediaSection());\n    actions.add(FlowyVersionSection());\n\n    final (color, borderColor, shadowColor, iconColor) =\n        Theme.of(context).isLightMode\n            ? (\n                Colors.white,\n                const Color(0x2D454849),\n                const Color(0x14000000),\n                Colors.black,\n              )\n            : (\n                const Color(0xFF242B37),\n                const Color(0x2DFFFFFF),\n                const Color(0x14000000),\n                Colors.white,\n              );\n\n    return PopoverActionList<PopoverAction>(\n      direction: PopoverDirection.topWithRightAligned,\n      actions: actions,\n      offset: const Offset(0, -8),\n      constraints: const BoxConstraints(\n        minWidth: 200,\n        maxWidth: 460,\n        maxHeight: 400,\n      ),\n      buildChild: (controller) {\n        return FlowyTooltip(\n          message: LocaleKeys.questionBubble_getSupport.tr(),\n          child: MouseRegion(\n            cursor: SystemMouseCursors.click,\n            child: GestureDetector(\n              child: Container(\n                padding: const EdgeInsets.all(8.0),\n                decoration: ShapeDecoration(\n                  color: color,\n                  shape: RoundedRectangleBorder(\n                    side: BorderSide(width: 0.50, color: borderColor),\n                    borderRadius: BorderRadius.circular(18),\n                  ),\n                  shadows: [\n                    BoxShadow(\n                      color: shadowColor,\n                      blurRadius: 20,\n                      offset: const Offset(0, 8),\n                    ),\n                  ],\n                ),\n                child: FlowySvg(\n                  FlowySvgs.help_center_s,\n                  color: iconColor,\n                ),\n              ),\n              onTap: () => controller.show(),\n            ),\n          ),\n        );\n      },\n      onClosed: toggle,\n      onSelected: (action, controller) {\n        if (action is BubbleActionWrapper) {\n          switch (action.inner) {\n            case BubbleAction.whatsNews:\n              afLaunchUrlString('https://www.appflowy.io/what-is-new');\n              break;\n            case BubbleAction.getSupport:\n              afLaunchUrlString('https://discord.gg/9Q2xaN37tV');\n              break;\n            case BubbleAction.debug:\n              _DebugToast().show();\n              break;\n            case BubbleAction.shortcuts:\n              afLaunchUrlString(\n                'https://docs.appflowy.io/docs/appflowy/product/shortcuts',\n              );\n              break;\n            case BubbleAction.markdown:\n              afLaunchUrlString(\n                'https://docs.appflowy.io/docs/appflowy/product/markdown',\n              );\n              break;\n            case BubbleAction.github:\n              afLaunchUrlString(\n                'https://github.com/AppFlowy-IO/AppFlowy/issues/new/choose',\n              );\n              break;\n            case BubbleAction.helpAndDocumentation:\n              afLaunchUrlString(\n                'https://appflowy.com/guide',\n              );\n              break;\n          }\n        }\n\n        controller.close();\n      },\n    );\n  }\n}\n\nclass _DebugToast {\n  void show() async {\n    String debugInfo = '';\n    debugInfo += await _getDeviceInfo();\n    debugInfo += await _getDocumentPath();\n    await Clipboard.setData(ClipboardData(text: debugInfo));\n\n    showMessageToast(LocaleKeys.questionBubble_debug_success.tr());\n  }\n\n  Future<String> _getDeviceInfo() async {\n    final deviceInfoPlugin = DeviceInfoPlugin();\n    final deviceInfo = await deviceInfoPlugin.deviceInfo;\n\n    return deviceInfo.data.entries\n        .fold('', (prev, el) => '$prev${el.key}: ${el.value}\\n');\n  }\n\n  Future<String> _getDocumentPath() async {\n    return appFlowyApplicationDataDirectory().then((directory) {\n      final path = directory.path.toString();\n      return 'Document: $path\\n';\n    });\n  }\n}\n\nenum BubbleAction {\n  whatsNews,\n  helpAndDocumentation,\n  getSupport,\n  debug,\n  shortcuts,\n  markdown,\n  github,\n}\n\nclass BubbleActionWrapper extends ActionCell {\n  BubbleActionWrapper(this.inner);\n\n  final BubbleAction inner;\n  @override\n  Widget? leftIcon(Color iconColor) => inner.icons;\n\n  @override\n  String get name => inner.name;\n}\n\nextension QuestionBubbleExtension on BubbleAction {\n  String get name {\n    switch (this) {\n      case BubbleAction.whatsNews:\n        return LocaleKeys.questionBubble_whatsNew.tr();\n      case BubbleAction.helpAndDocumentation:\n        return LocaleKeys.questionBubble_helpAndDocumentation.tr();\n      case BubbleAction.getSupport:\n        return LocaleKeys.questionBubble_getSupport.tr();\n      case BubbleAction.debug:\n        return LocaleKeys.questionBubble_debug_name.tr();\n      case BubbleAction.shortcuts:\n        return LocaleKeys.questionBubble_shortcuts.tr();\n      case BubbleAction.markdown:\n        return LocaleKeys.questionBubble_markdown.tr();\n      case BubbleAction.github:\n        return LocaleKeys.questionBubble_feedback.tr();\n    }\n  }\n\n  Widget? get icons {\n    switch (this) {\n      case BubbleAction.whatsNews:\n        return const FlowySvg(FlowySvgs.star_s);\n      case BubbleAction.helpAndDocumentation:\n        return const FlowySvg(\n          FlowySvgs.help_and_documentation_s,\n          size: Size.square(16.0),\n        );\n      case BubbleAction.getSupport:\n        return const FlowySvg(FlowySvgs.message_support_s);\n      case BubbleAction.debug:\n        return const FlowySvg(FlowySvgs.debug_s);\n      case BubbleAction.shortcuts:\n        return const FlowySvg(FlowySvgs.keyboard_s);\n      case BubbleAction.markdown:\n        return const FlowySvg(FlowySvgs.number_s);\n      case BubbleAction.github:\n        return const FlowySvg(FlowySvgs.share_feedback_s);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/social_media_section.dart",
    "content": "import 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\n\nclass SocialMediaSection extends CustomActionCell {\n  @override\n  Widget buildWithContext(\n    BuildContext context,\n    PopoverController controller,\n    PopoverMutex? mutex,\n  ) {\n    final List<Widget> children = [\n      Divider(\n        height: 1,\n        color: Theme.of(context).dividerColor,\n        thickness: 1.0,\n      ),\n    ];\n\n    children.addAll(\n      SocialMedia.values.map(\n        (social) {\n          return ActionCellWidget(\n            action: SocialMediaWrapper(social),\n            itemHeight: ActionListSizes.itemHeight,\n            onSelected: (action) {\n              final url = switch (action.inner) {\n                SocialMedia.reddit => 'https://www.reddit.com/r/AppFlowy/',\n                SocialMedia.twitter => 'https://x.com/appflowy',\n                SocialMedia.forum => 'https://forum.appflowy.com/',\n              };\n\n              afLaunchUrlString(url);\n            },\n          );\n        },\n      ),\n    );\n\n    return Padding(\n      padding: const EdgeInsets.symmetric(vertical: 6),\n      child: Column(\n        children: children,\n      ),\n    );\n  }\n}\n\nenum SocialMedia { forum, twitter, reddit }\n\nclass SocialMediaWrapper extends ActionCell {\n  SocialMediaWrapper(this.inner);\n\n  final SocialMedia inner;\n  @override\n  Widget? leftIcon(Color iconColor) => inner.icons;\n\n  @override\n  String get name => inner.name;\n\n  @override\n  Color? textColor(BuildContext context) => inner.textColor(context);\n}\n\nextension QuestionBubbleExtension on SocialMedia {\n  Color? textColor(BuildContext context) {\n    switch (this) {\n      case SocialMedia.reddit:\n        return Theme.of(context).hintColor;\n\n      case SocialMedia.twitter:\n        return Theme.of(context).hintColor;\n\n      case SocialMedia.forum:\n        return Theme.of(context).hintColor;\n    }\n  }\n\n  String get name {\n    switch (this) {\n      case SocialMedia.forum:\n        return 'Community Forum';\n      case SocialMedia.twitter:\n        return 'Twitter – @appflowy';\n      case SocialMedia.reddit:\n        return 'Reddit – r/appflowy';\n    }\n  }\n\n  Widget? get icons {\n    switch (this) {\n      case SocialMedia.reddit:\n        return null;\n      case SocialMedia.twitter:\n        return null;\n      case SocialMedia.forum:\n        return null;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/version_section.dart",
    "content": "import 'package:appflowy/env/env.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:package_info_plus/package_info_plus.dart';\nimport 'package:styled_widget/styled_widget.dart';\n\nclass FlowyVersionSection extends CustomActionCell {\n  @override\n  Widget buildWithContext(\n    BuildContext context,\n    PopoverController controller,\n    PopoverMutex? mutex,\n  ) {\n    return FutureBuilder(\n      future: PackageInfo.fromPlatform(),\n      builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {\n        if (snapshot.connectionState == ConnectionState.done) {\n          if (snapshot.hasError) {\n            return FlowyText(\n              \"Error: ${snapshot.error}\",\n              color: Theme.of(context).disabledColor,\n            );\n          }\n\n          final PackageInfo packageInfo = snapshot.data;\n          final String appName = packageInfo.appName;\n          final String version = packageInfo.version;\n\n          return SizedBox(\n            height: 30,\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Divider(\n                  height: 1,\n                  color: Theme.of(context).dividerColor,\n                  thickness: 1.0,\n                ),\n                const VSpace(6),\n                GestureDetector(\n                  behavior: HitTestBehavior.opaque,\n                  onDoubleTap: () {\n                    if (Env.internalBuild != '1' && !kDebugMode) {\n                      return;\n                    }\n                    enableDocumentInternalLog = !enableDocumentInternalLog;\n                    showToastNotification(\n                      message: enableDocumentInternalLog\n                          ? 'Enabled Internal Log'\n                          : 'Disabled Internal Log',\n                    );\n                  },\n                  child: FlowyText(\n                    '$appName $version',\n                    color: Theme.of(context).hintColor,\n                    fontSize: 12,\n                  ).padding(\n                    horizontal: ActionListSizes.itemHPadding,\n                  ),\n                ),\n              ],\n            ),\n          );\n        } else {\n          return const SizedBox(height: 30);\n        }\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/image_provider.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:flutter/widgets.dart';\n\n/// Abstract class for providing images to the [InteractiveImageViewer].\n///\nabstract class AFImageProvider {\n  const AFImageProvider({this.onDeleteImage});\n\n  /// Provide this callback if you want it to be possible to\n  /// delete the Image through the [InteractiveImageViewer].\n  ///\n  final Function(int index)? onDeleteImage;\n\n  int get imageCount;\n  int get initialIndex;\n\n  ImageBlockData getImage(int index);\n  Widget renderImage(\n    BuildContext context,\n    int index, [\n    UserProfilePB? userProfile,\n  ]);\n}\n\nclass AFBlockImageProvider implements AFImageProvider {\n  const AFBlockImageProvider({\n    required this.images,\n    this.initialIndex = 0,\n    this.onDeleteImage,\n  });\n\n  final List<ImageBlockData> images;\n\n  @override\n  final Function(int)? onDeleteImage;\n\n  @override\n  final int initialIndex;\n\n  @override\n  int get imageCount => images.length;\n\n  @override\n  ImageBlockData getImage(int index) => images[index];\n\n  @override\n  Widget renderImage(\n    BuildContext context,\n    int index, [\n    UserProfilePB? userProfile,\n  ]) {\n    final image = getImage(index);\n\n    if (image.type == CustomImageType.local &&\n        localPathRegex.hasMatch(image.url)) {\n      return Image(image: image.toImageProvider());\n    }\n\n    return FlowyNetworkImage(\n      url: image.url,\n      userProfilePB: userProfile,\n      fit: BoxFit.contain,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/interactive_image_toolbar.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:appflowy/core/helpers/url_launcher.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/file_picker/file_picker_impl.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flowy_infra_ui/style_widget/snap_bar.dart';\nimport 'package:flutter/material.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart';\nimport 'package:universal_platform/universal_platform.dart';\n\nclass InteractiveImageToolbar extends StatelessWidget {\n  const InteractiveImageToolbar({\n    super.key,\n    required this.currentImage,\n    required this.imageCount,\n    required this.isFirstIndex,\n    required this.isLastIndex,\n    required this.currentScale,\n    required this.onPrevious,\n    required this.onNext,\n    required this.onZoomIn,\n    required this.onZoomOut,\n    required this.onScaleChanged,\n    this.onDelete,\n    this.userProfile,\n  });\n\n  final ImageBlockData currentImage;\n  final int imageCount;\n  final bool isFirstIndex;\n  final bool isLastIndex;\n  final int currentScale;\n\n  final VoidCallback onPrevious;\n  final VoidCallback onNext;\n  final VoidCallback onZoomIn;\n  final VoidCallback onZoomOut;\n  final Function(double scale) onScaleChanged;\n  final UserProfilePB? userProfile;\n  final VoidCallback? onDelete;\n\n  @override\n  Widget build(BuildContext context) {\n    return Positioned(\n      bottom: 16,\n      left: 0,\n      right: 0,\n      child: Padding(\n        padding: const EdgeInsets.all(16.0),\n        child: SizedBox(\n          width: 200,\n          child: Row(\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              if (imageCount > 1)\n                _renderToolbarItems(\n                  children: [\n                    _ToolbarItem(\n                      isDisabled: isFirstIndex,\n                      tooltip: LocaleKeys\n                          .document_imageBlock_interactiveViewer_toolbar_previousImageTooltip\n                          .tr(),\n                      icon: FlowySvgs.arrow_left_s,\n                      onTap: () {\n                        if (!isFirstIndex) {\n                          onPrevious();\n                        }\n                      },\n                    ),\n                    _ToolbarItem(\n                      isDisabled: isLastIndex,\n                      tooltip: LocaleKeys\n                          .document_imageBlock_interactiveViewer_toolbar_nextImageTooltip\n                          .tr(),\n                      icon: FlowySvgs.arrow_right_s,\n                      onTap: () {\n                        if (!isLastIndex) {\n                          onNext();\n                        }\n                      },\n                    ),\n                  ],\n                ),\n              const HSpace(10),\n              _renderToolbarItems(\n                children: [\n                  _ToolbarItem(\n                    tooltip: LocaleKeys\n                        .document_imageBlock_interactiveViewer_toolbar_zoomOutTooltip\n                        .tr(),\n                    icon: FlowySvgs.minus_s,\n                    onTap: onZoomOut,\n                  ),\n                  AppFlowyPopover(\n                    offset: const Offset(0, -8),\n                    decorationColor: Colors.transparent,\n                    direction: PopoverDirection.topWithCenterAligned,\n                    constraints: const BoxConstraints(maxHeight: 50),\n                    popupBuilder: (context) => _renderToolbarItems(\n                      children: [\n                        _ScaleSlider(\n                          currentScale: currentScale,\n                          onScaleChanged: onScaleChanged,\n                        ),\n                      ],\n                    ),\n                    child: FlowyTooltip(\n                      message: LocaleKeys\n                          .document_imageBlock_interactiveViewer_toolbar_changeZoomLevelTooltip\n                          .tr(),\n                      child: FlowyHover(\n                        resetHoverOnRebuild: false,\n                        style: HoverStyle(\n                          hoverColor: Colors.white.withValues(alpha: 0.1),\n                          borderRadius: BorderRadius.circular(4),\n                        ),\n                        child: Padding(\n                          padding: const EdgeInsets.all(6),\n                          child: SizedBox(\n                            width: 40,\n                            child: Center(\n                              child: FlowyText(\n                                LocaleKeys\n                                    .document_imageBlock_interactiveViewer_toolbar_scalePercentage\n                                    .tr(args: [currentScale.toString()]),\n                                color: Colors.white,\n                              ),\n                            ),\n                          ),\n                        ),\n                      ),\n                    ),\n                  ),\n                  _ToolbarItem(\n                    tooltip: LocaleKeys\n                        .document_imageBlock_interactiveViewer_toolbar_zoomInTooltip\n                        .tr(),\n                    icon: FlowySvgs.add_s,\n                    onTap: onZoomIn,\n                  ),\n                ],\n              ),\n              const HSpace(10),\n              _renderToolbarItems(\n                children: [\n                  if (onDelete != null)\n                    _ToolbarItem(\n                      tooltip: LocaleKeys\n                          .document_imageBlock_interactiveViewer_toolbar_deleteImageTooltip\n                          .tr(),\n                      icon: FlowySvgs.delete_s,\n                      onTap: () {\n                        onDelete!();\n                        Navigator.of(context).pop();\n                      },\n                    ),\n                  if (!UniversalPlatform.isMobile) ...[\n                    _ToolbarItem(\n                      tooltip: currentImage.isNotInternal\n                          ? LocaleKeys\n                              .document_imageBlock_interactiveViewer_toolbar_openLocalImage\n                              .tr()\n                          : LocaleKeys\n                              .document_imageBlock_interactiveViewer_toolbar_downloadImage\n                              .tr(),\n                      icon: currentImage.isNotInternal\n                          ? currentImage.isLocal\n                              ? FlowySvgs.folder_m\n                              : FlowySvgs.m_aa_link_s\n                          : FlowySvgs.download_s,\n                      onTap: () => _locateOrDownloadImage(context),\n                    ),\n                  ],\n                ],\n              ),\n              const HSpace(10),\n              _renderToolbarItems(\n                children: [\n                  _ToolbarItem(\n                    tooltip: LocaleKeys\n                        .document_imageBlock_interactiveViewer_toolbar_closeViewer\n                        .tr(),\n                    icon: FlowySvgs.close_viewer_s,\n                    onTap: () => Navigator.of(context).pop(),\n                  ),\n                ],\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget _renderToolbarItems({required List<Widget> children}) {\n    return DecoratedBox(\n      decoration: BoxDecoration(\n        borderRadius: BorderRadius.circular(6),\n        color: Colors.black.withValues(alpha: 0.6),\n      ),\n      child: Padding(\n        padding: const EdgeInsets.all(4),\n        child: SeparatedRow(\n          mainAxisSize: MainAxisSize.min,\n          separatorBuilder: () => const HSpace(4),\n          children: children,\n        ),\n      ),\n    );\n  }\n\n  Future<void> _locateOrDownloadImage(BuildContext context) async {\n    if (currentImage.isLocal || currentImage.isNotInternal) {\n      /// If the image type is local, we simply open the image\n      ///\n      /// // In case of eg. Unsplash images (images without extension type in URL),\n      // we don't know their mimetype. In the future we can write a parser\n      // using the Mime package and read the image to get the proper extension.\n      await afLaunchUrlString(currentImage.url);\n    } else {\n      if (userProfile == null) {\n        return showSnapBar(\n          context,\n          LocaleKeys.document_plugins_image_imageDownloadFailedToken.tr(),\n        );\n      }\n\n      final uri = Uri.parse(currentImage.url);\n      final imgFile = File(uri.pathSegments.last);\n      final savePath = await FilePicker().saveFile(\n        fileName: basename(imgFile.path),\n      );\n\n      if (savePath != null) {\n        final uri = Uri.parse(currentImage.url);\n\n        final token = jsonDecode(userProfile!.token)['access_token'];\n        final response = await http.get(\n          uri,\n          headers: {'Authorization': 'Bearer $token'},\n        );\n        if (response.statusCode == 200) {\n          final imgFile = File(savePath);\n          await imgFile.writeAsBytes(response.bodyBytes);\n        } else if (context.mounted) {\n          showSnapBar(\n            context,\n            LocaleKeys.document_plugins_image_imageDownloadFailed.tr(),\n          );\n        }\n      }\n    }\n  }\n}\n\nclass _ToolbarItem extends StatelessWidget {\n  const _ToolbarItem({\n    required this.tooltip,\n    required this.icon,\n    required this.onTap,\n    this.isDisabled = false,\n  });\n\n  final String tooltip;\n  final FlowySvgData icon;\n  final VoidCallback onTap;\n  final bool isDisabled;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: onTap,\n      child: FlowyTooltip(\n        message: tooltip,\n        child: FlowyHover(\n          resetHoverOnRebuild: false,\n          style: HoverStyle(\n            hoverColor: isDisabled\n                ? Colors.transparent\n                : Colors.white.withValues(alpha: 0.1),\n            borderRadius: BorderRadius.circular(4),\n          ),\n          child: Container(\n            width: 32,\n            height: 32,\n            padding: const EdgeInsets.all(8),\n            child: FlowySvg(\n              icon,\n              color: isDisabled ? Colors.grey : Colors.white,\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass _ScaleSlider extends StatefulWidget {\n  const _ScaleSlider({\n    required this.currentScale,\n    required this.onScaleChanged,\n  });\n\n  final int currentScale;\n  final Function(double scale) onScaleChanged;\n\n  @override\n  State<_ScaleSlider> createState() => __ScaleSliderState();\n}\n\nclass __ScaleSliderState extends State<_ScaleSlider> {\n  late int _currentScale = widget.currentScale;\n\n  @override\n  Widget build(BuildContext context) {\n    return Slider(\n      max: 5.0,\n      min: 0.5,\n      value: _currentScale / 100,\n      onChanged: (scale) {\n        widget.onScaleChanged(scale);\n        setState(\n          () => _currentScale = (scale * 100).toInt(),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nimport 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';\nimport 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';\nimport 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_toolbar.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';\nimport 'package:provider/provider.dart';\n\nconst double _minScaleFactor = .5;\nconst double _maxScaleFactor = 5;\n\nclass InteractiveImageViewer extends StatefulWidget {\n  const InteractiveImageViewer({\n    super.key,\n    this.userProfile,\n    required this.imageProvider,\n  });\n\n  final UserProfilePB? userProfile;\n  final AFImageProvider imageProvider;\n\n  @override\n  State<InteractiveImageViewer> createState() => _InteractiveImageViewerState();\n}\n\nclass _InteractiveImageViewerState extends State<InteractiveImageViewer> {\n  final TransformationController controller = TransformationController();\n  final focusNode = FocusNode();\n\n  int currentScale = 100;\n  late int currentIndex = widget.imageProvider.initialIndex;\n\n  bool get isLastIndex => currentIndex == widget.imageProvider.imageCount - 1;\n  bool get isFirstIndex => currentIndex == 0;\n\n  late ImageBlockData currentImage;\n\n  UserProfilePB? userProfile;\n\n  @override\n  void initState() {\n    super.initState();\n    controller.addListener(_onControllerChanged);\n    currentImage = widget.imageProvider.getImage(currentIndex);\n    userProfile =\n        widget.userProfile ?? context.read<DocumentBloc>().state.userProfilePB;\n    focusNode.requestFocus();\n  }\n\n  void _onControllerChanged() {\n    final scale = controller.value.getMaxScaleOnAxis();\n    final percentage = (scale * 100).toInt();\n    setState(() => currentScale = percentage);\n  }\n\n  @override\n  void dispose() {\n    controller.removeListener(_onControllerChanged);\n    controller.dispose();\n    focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final size = MediaQuery.of(context).size;\n\n    return KeyboardListener(\n      focusNode: focusNode,\n      onKeyEvent: (event) {\n        if (event is! KeyDownEvent) {\n          return;\n        }\n\n        if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {\n          _move(-1);\n        } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {\n          _move(1);\n        } else if ([\n          LogicalKeyboardKey.add,\n          LogicalKeyboardKey.numpadAdd,\n        ].contains(event.logicalKey)) {\n          _zoom(1.1, size);\n        } else if ([\n          LogicalKeyboardKey.minus,\n          LogicalKeyboardKey.numpadSubtract,\n        ].contains(event.logicalKey)) {\n          _zoom(.9, size);\n        } else if ([\n          LogicalKeyboardKey.numpad0,\n          LogicalKeyboardKey.digit0,\n        ].contains(event.logicalKey)) {\n          controller.value = Matrix4.identity();\n          _onControllerChanged();\n        }\n      },\n      child: Stack(\n        fit: StackFit.expand,\n        children: [\n          SizedBox.expand(\n            child: InteractiveViewer(\n              boundaryMargin: const EdgeInsets.all(double.infinity),\n              transformationController: controller,\n              constrained: false,\n              minScale: _minScaleFactor,\n              maxScale: _maxScaleFactor,\n              scaleFactor: 500,\n              child: SizedBox(\n                height: size.height,\n                width: size.width,\n                child: GestureDetector(\n                  // We can consider adding zoom behavior instead in a later iteration\n                  onDoubleTap: () => Navigator.of(context).pop(),\n                  child: widget.imageProvider.renderImage(\n                    context,\n                    currentIndex,\n                    userProfile,\n                  ),\n                ),\n              ),\n            ),\n          ),\n          InteractiveImageToolbar(\n            currentImage: currentImage,\n            imageCount: widget.imageProvider.imageCount,\n            isFirstIndex: isFirstIndex,\n            isLastIndex: isLastIndex,\n            currentScale: currentScale,\n            userProfile: userProfile,\n            onPrevious: () => _move(-1),\n            onNext: () => _move(1),\n            onZoomIn: () => _zoom(1.1, size),\n            onZoomOut: () => _zoom(.9, size),\n            onScaleChanged: (scale) {\n              final currentScale = controller.value.getMaxScaleOnAxis();\n              final scaleStep = scale / currentScale;\n              _zoom(scaleStep, size);\n            },\n            onDelete: widget.imageProvider.onDeleteImage == null\n                ? null\n                : () => widget.imageProvider.onDeleteImage?.call(currentIndex),\n          ),\n        ],\n      ),\n    );\n  }\n\n  void _move(int steps) {\n    setState(() {\n      final index = currentIndex + steps;\n      currentIndex = index.clamp(0, widget.imageProvider.imageCount - 1);\n      currentImage = widget.imageProvider.getImage(currentIndex);\n    });\n  }\n\n  void _zoom(double scaleStep, Size size) {\n    final center = Offset(size.width / 2, size.height / 2);\n    final scenePointBefore = controller.toScene(center);\n    final currentScale = controller.value.getMaxScaleOnAxis();\n    final newScale = (currentScale * scaleStep).clamp(\n      _minScaleFactor,\n      _maxScaleFactor,\n    );\n\n    // Create a new transformation\n    final newMatrix = Matrix4.identity()\n      ..translate(scenePointBefore.dx, scenePointBefore.dy)\n      ..scale(newScale / currentScale)\n      ..translate(-scenePointBefore.dx, -scenePointBefore.dy);\n\n    // Apply the new transformation\n    controller.value = newMatrix * controller.value;\n\n    // Convert the center point to scene coordinates after scaling\n    final scenePointAfter = controller.toScene(center);\n\n    // Compute difference to keep the same center point\n    final dx = scenePointAfter.dx - scenePointBefore.dx;\n    final dy = scenePointAfter.dy - scenePointBefore.dy;\n\n    // Apply the translation\n    controller.value = Matrix4.identity()\n      ..translate(-dx, -dy)\n      ..multiply(controller.value);\n\n    _onControllerChanged();\n  }\n}\n\nvoid openInteractiveViewerFromFile(\n  BuildContext context,\n  MediaFilePB file, {\n  required void Function(int) onDeleteImage,\n  UserProfilePB? userProfile,\n}) =>\n    showDialog(\n      context: context,\n      builder: (_) => InteractiveImageViewer(\n        userProfile: userProfile,\n        imageProvider: AFBlockImageProvider(\n          images: [\n            ImageBlockData(\n              url: file.url,\n              type: file.uploadType.toCustomImageType(),\n            ),\n          ],\n          onDeleteImage: onDeleteImage,\n        ),\n      ),\n    );\n\nvoid openInteractiveViewerFromFiles(\n  BuildContext context,\n  List<MediaFilePB> files, {\n  required void Function(int) onDeleteImage,\n  int initialIndex = 0,\n  UserProfilePB? userProfile,\n}) =>\n    showDialog(\n      context: context,\n      builder: (_) => InteractiveImageViewer(\n        userProfile: userProfile,\n        imageProvider: AFBlockImageProvider(\n          initialIndex: initialIndex,\n          images: files\n              .map(\n                (f) => ImageBlockData(\n                  url: f.url,\n                  type: f.uploadType.toCustomImageType(),\n                ),\n              )\n              .toList(),\n          onDeleteImage: onDeleteImage,\n        ),\n      ),\n    );\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart",
    "content": "import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/features/share_tab/data/models/share_access_level.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';\nimport 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart';\nimport 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_action.dart';\nimport 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart';\nimport 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass MoreViewActions extends StatefulWidget {\n  const MoreViewActions({\n    super.key,\n    required this.view,\n    this.customActions = const [],\n  });\n\n  /// The view to show the actions for.\n  ///\n  final ViewPB view;\n\n  /// Custom actions to show in the popover, will be laid out at the top.\n  ///\n  final List<Widget> customActions;\n\n  @override\n  State<MoreViewActions> createState() => _MoreViewActionsState();\n}\n\nclass _MoreViewActionsState extends State<MoreViewActions> {\n  final popoverMutex = PopoverMutex();\n\n  @override\n  void dispose() {\n    popoverMutex.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<ViewInfoBloc, ViewInfoState>(\n      builder: (context, state) {\n        return AppFlowyPopover(\n          mutex: popoverMutex,\n          constraints: const BoxConstraints(maxWidth: 245),\n          direction: PopoverDirection.bottomWithRightAligned,\n          offset: const Offset(0, 12),\n          popupBuilder: (_) => _buildPopup(state),\n          child: const _ThreeDots(),\n        );\n      },\n    );\n  }\n\n  Widget _buildPopup(ViewInfoState viewInfoState) {\n    final userWorkspaceBloc = context.read<UserWorkspaceBloc>();\n    final userProfile = userWorkspaceBloc.state.userProfile;\n    final workspaceId =\n        userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';\n\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider(\n          create: (_) => ViewBloc(view: widget.view)\n            ..add(\n              const ViewEvent.initial(),\n            ),\n        ),\n        BlocProvider(\n          create: (context) => SpaceBloc(\n            userProfile: userProfile,\n            workspaceId: workspaceId,\n          )..add(\n              const SpaceEvent.initial(openFirstPage: false),\n            ),\n        ),\n        BlocProvider.value(\n          value: context.read<PageAccessLevelBloc>(),\n        ),\n      ],\n      child: BlocBuilder<ViewBloc, ViewState>(\n        builder: (context, viewState) {\n          return BlocBuilder<SpaceBloc, SpaceState>(\n            builder: (context, state) {\n              if (state.spaces.isEmpty &&\n                  userProfile.workspaceType == WorkspaceTypePB.ServerW) {\n                return const SizedBox.shrink();\n              }\n\n              final actions = _buildActions(\n                context,\n                viewInfoState,\n              );\n              return ListView.builder(\n                key: ValueKey(state.spaces.hashCode),\n                shrinkWrap: true,\n                padding: EdgeInsets.zero,\n                itemCount: actions.length,\n                physics: StyledScrollPhysics(),\n                itemBuilder: (_, index) => actions[index],\n              );\n            },\n          );\n        },\n      ),\n    );\n  }\n\n  List<Widget> _buildActions(BuildContext context, ViewInfoState state) {\n    final pageAccessLevelBloc = context.watch<PageAccessLevelBloc>();\n    final pageAccessLevelState = pageAccessLevelBloc.state;\n    final view = pageAccessLevelState.view;\n\n    final appearanceSettings = context.watch<AppearanceSettingsCubit>().state;\n    final dateFormat = appearanceSettings.dateFormat;\n    final timeFormat = appearanceSettings.timeFormat;\n\n    final viewMoreActionTypes = switch (pageAccessLevelState.accessLevel) {\n      ShareAccessLevel.readOnly => [],\n      _ => [\n          if (widget.view.layout != ViewLayoutPB.Chat)\n            ViewMoreActionType.duplicate,\n          ViewMoreActionType.moveTo,\n          ViewMoreActionType.delete,\n          ViewMoreActionType.divider,\n        ],\n    };\n\n    final actions = [\n      ...widget.customActions,\n      if (widget.view.isDocument) ...[\n        const FontSizeAction(),\n        ViewAction(\n          type: ViewMoreActionType.divider,\n          view: view,\n          mutex: popoverMutex,\n        ),\n      ],\n      if (state.workspaceType == WorkspaceTypePB.ServerW &&\n          (widget.view.isDocument || widget.view.isDatabase) &&\n          !pageAccessLevelState.isReadOnly) ...[\n        LockPageAction(\n          view: view,\n        ),\n        ViewAction(\n          type: ViewMoreActionType.divider,\n          view: view,\n          mutex: popoverMutex,\n        ),\n      ],\n      ...viewMoreActionTypes.map(\n        (type) => ViewAction(\n          type: type,\n          view: view,\n          mutex: popoverMutex,\n        ),\n      ),\n      if (state.documentCounters != null || state.createdAt != null) ...[\n        ViewMetaInfo(\n          dateFormat: dateFormat,\n          timeFormat: timeFormat,\n          documentCounters: state.documentCounters,\n          titleCounters: state.titleCounters,\n          createdAt: state.createdAt,\n        ),\n        const VSpace(4.0),\n      ],\n    ];\n    return actions;\n  }\n}\n\nclass _ThreeDots extends StatelessWidget {\n  const _ThreeDots();\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      message: LocaleKeys.moreAction_moreOptions.tr(),\n      child: FlowyHover(\n        style: HoverStyle(\n          foregroundColorOnHover: Theme.of(context).colorScheme.onPrimary,\n        ),\n        builder: (context, isHovering) => Padding(\n          padding: const EdgeInsets.all(6),\n          child: FlowySvg(\n            FlowySvgs.three_dots_s,\n            size: const Size.square(18),\n            color: isHovering\n                ? Theme.of(context).colorScheme.onSurface\n                : Theme.of(context).iconTheme.color,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass ViewAction extends StatelessWidget {\n  const ViewAction({\n    super.key,\n    required this.type,\n    required this.view,\n    this.mutex,\n  });\n\n  final ViewMoreActionType type;\n  final ViewPB view;\n  final PopoverMutex? mutex;\n\n  @override\n  Widget build(BuildContext context) {\n    final wrapper = ViewMoreActionTypeWrapper(\n      type,\n      view,\n      (controller, data) async {\n        await _onAction(context, data);\n        mutex?.close();\n      },\n      moveActionDirection: PopoverDirection.leftWithTopAligned,\n      moveActionOffset: const Offset(-10, 0),\n    );\n    return wrapper.buildWithContext(\n      context,\n      // this is a dummy controller, we don't need to control the popover here.\n      PopoverController(),\n      null,\n    );\n  }\n\n  Future<void> _onAction(\n    BuildContext context,\n    dynamic data,\n  ) async {\n    switch (type) {\n      case ViewMoreActionType.delete:\n        final (containPublishedPage, _) =\n            await ViewBackendService.containPublishedPage(view);\n\n        if (containPublishedPage && context.mounted) {\n          await showConfirmDeletionDialog(\n            context: context,\n            name: view.nameOrDefault,\n            description: LocaleKeys.publish_containsPublishedPage.tr(),\n            onConfirm: () {\n              context.read<ViewBloc>().add(const ViewEvent.delete());\n            },\n          );\n        } else if (context.mounted) {\n          context.read<ViewBloc>().add(const ViewEvent.delete());\n        }\n      case ViewMoreActionType.duplicate:\n        context.read<ViewBloc>().add(const ViewEvent.duplicate());\n      case ViewMoreActionType.moveTo:\n        final value = data;\n        if (value is! (ViewPB, ViewPB)) {\n          return;\n        }\n        final space = value.$1;\n        final target = value.$2;\n        final result = await ViewBackendService.getView(view.parentViewId);\n        result.fold(\n          (parentView) => moveViewCrossSpace(\n            context,\n            space,\n            view,\n            parentView,\n            FolderSpaceType.public,\n            view,\n            target.id,\n          ),\n          (f) => Log.error(f),\n        );\n\n        // the move action is handled in the button itself\n        break;\n      default:\n        throw UnimplementedError();\n    }\n  }\n}\n\nclass CustomViewAction extends StatelessWidget {\n  const CustomViewAction({\n    super.key,\n    required this.view,\n    required this.leftIcon,\n    required this.label,\n    this.tooltipMessage,\n    this.disabled = false,\n    this.onTap,\n    this.mutex,\n  });\n\n  final ViewPB view;\n  final FlowySvgData leftIcon;\n  final String label;\n  final bool disabled;\n  final String? tooltipMessage;\n  final VoidCallback? onTap;\n  final PopoverMutex? mutex;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      height: 34,\n      padding: const EdgeInsets.symmetric(vertical: 2.0),\n      child: FlowyTooltip(\n        message: tooltipMessage,\n        child: FlowyButton(\n          margin: const EdgeInsets.symmetric(horizontal: 6),\n          disable: disabled,\n          onTap: onTap,\n          leftIcon: FlowySvg(\n            leftIcon,\n            size: const Size.square(16.0),\n            color: disabled ? Theme.of(context).disabledColor : null,\n          ),\n          iconPadding: 10.0,\n          text: FlowyText(\n            label,\n            figmaLineHeight: 18.0,\n            color: disabled ? Theme.of(context).disabledColor : null,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/font_size_action.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass FontSizeAction extends StatelessWidget {\n  const FontSizeAction({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyPopover(\n      direction: PopoverDirection.leftWithCenterAligned,\n      constraints: const BoxConstraints(maxHeight: 40, maxWidth: 240),\n      offset: const Offset(-10, 0),\n      popupBuilder: (context) {\n        return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(\n          builder: (_, state) => FontSizeStepper(\n            minimumValue: 10,\n            maximumValue: 24,\n            value: state.fontSize,\n            divisions: 8,\n            onChanged: (newFontSize) => context\n                .read<DocumentAppearanceCubit>()\n                .syncFontSize(newFontSize),\n          ),\n        );\n      },\n      child: Container(\n        height: 34,\n        padding: const EdgeInsets.symmetric(vertical: 2.0),\n        child: FlowyButton(\n          text: FlowyText.regular(\n            LocaleKeys.moreAction_fontSize.tr(),\n            fontSize: 14.0,\n            lineHeight: 1.0,\n            figmaLineHeight: 18.0,\n            color: AFThemeExtension.of(context).textColor,\n          ),\n          leftIcon: Icon(\n            Icons.format_size_sharp,\n            color: Theme.of(context).iconTheme.color,\n            size: 18,\n          ),\n          leftIconSize: const Size(18, 18),\n          hoverColor: AFThemeExtension.of(context).lightGreyHover,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\n\nclass FontSizeStepper extends StatefulWidget {\n  const FontSizeStepper({\n    super.key,\n    required this.minimumValue,\n    required this.maximumValue,\n    required this.value,\n    required this.divisions,\n    required this.onChanged,\n  });\n\n  final double minimumValue;\n  final double maximumValue;\n  final double value;\n  final ValueChanged<double> onChanged;\n  final int divisions;\n\n  @override\n  State<FontSizeStepper> createState() => _FontSizeStepperState();\n}\n\nclass _FontSizeStepperState extends State<FontSizeStepper> {\n  late double _value = widget.value.clamp(\n    widget.minimumValue,\n    widget.maximumValue,\n  );\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 10),\n      child: Row(\n        children: [\n          const FlowyText('A', fontSize: 14),\n          const HSpace(6),\n          Expanded(\n            child: SliderTheme(\n              data: Theme.of(context).sliderTheme.copyWith(\n                    showValueIndicator: ShowValueIndicator.never,\n                    thumbShape: const RoundSliderThumbShape(\n                      enabledThumbRadius: 8,\n                    ),\n                    overlayShape: const RoundSliderOverlayShape(\n                      overlayRadius: 16,\n                    ),\n                  ),\n              child: Slider(\n                value: _value,\n                min: widget.minimumValue,\n                max: widget.maximumValue,\n                divisions: widget.divisions,\n                onChanged: (value) {\n                  setState(() => _value = value);\n                  widget.onChanged(value);\n                },\n              ),\n            ),\n          ),\n          const HSpace(6),\n          const FlowyText('A', fontSize: 20),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart",
    "content": "import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass LockPageAction extends StatefulWidget {\n  const LockPageAction({\n    super.key,\n    required this.view,\n  });\n\n  final ViewPB view;\n\n  @override\n  State<LockPageAction> createState() => _LockPageActionState();\n}\n\nclass _LockPageActionState extends State<LockPageAction> {\n  @override\n  Widget build(BuildContext context) {\n    return BlocBuilder<PageAccessLevelBloc, PageAccessLevelState>(\n      builder: (context, state) {\n        return _buildTextButton(context);\n      },\n    );\n  }\n\n  Widget _buildTextButton(\n    BuildContext context,\n  ) {\n    return Container(\n      height: 34,\n      padding: const EdgeInsets.symmetric(vertical: 2.0),\n      child: FlowyIconTextButton(\n        margin: const EdgeInsets.symmetric(horizontal: 6),\n        onTap: () => _toggle(context),\n        leftIconBuilder: (onHover) => FlowySvg(\n          FlowySvgs.lock_page_s,\n          size: const Size.square(16.0),\n        ),\n        iconPadding: 10.0,\n        textBuilder: (onHover) => FlowyText(\n          LocaleKeys.disclosureAction_lockPage.tr(),\n          figmaLineHeight: 18.0,\n        ),\n        rightIconBuilder: (_) => _buildSwitch(\n          context,\n        ),\n      ),\n    );\n  }\n\n  Widget _buildSwitch(BuildContext context) {\n    final lockState = context.read<PageAccessLevelBloc>().state;\n    if (lockState.isLoadingLockStatus) {\n      return SizedBox.shrink();\n    }\n\n    return Container(\n      width: 30,\n      height: 20,\n      margin: const EdgeInsets.only(right: 6),\n      child: FittedBox(\n        fit: BoxFit.fill,\n        child: CupertinoSwitch(\n          value: lockState.isLocked,\n          activeTrackColor: Theme.of(context).colorScheme.primary,\n          onChanged: (_) => _toggle(context),\n        ),\n      ),\n    );\n  }\n\n  Future<void> _toggle(BuildContext context) async {\n    final isLocked = context.read<PageAccessLevelBloc>().state.isLocked;\n\n    context.read<PageAccessLevelBloc>().add(\n          isLocked\n              ? PageAccessLevelEvent.unlock()\n              : PageAccessLevelEvent.lock(),\n        );\n\n    Log.info('update page(${widget.view.id}) lock status: $isLocked');\n  }\n}\n\nclass LockPageButtonWrapper extends StatelessWidget {\n  const LockPageButtonWrapper({\n    super.key,\n    required this.child,\n  });\n\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyTooltip(\n      message: LocaleKeys.lockPage_lockedOperationTooltip.tr(),\n      child: IgnorePointer(\n        child: child,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\n\nclass ViewMetaInfo extends StatelessWidget {\n  const ViewMetaInfo({\n    super.key,\n    required this.dateFormat,\n    required this.timeFormat,\n    this.documentCounters,\n    this.titleCounters,\n    this.createdAt,\n  });\n\n  final UserDateFormatPB dateFormat;\n  final UserTimeFormatPB timeFormat;\n  final Counters? documentCounters;\n  final Counters? titleCounters;\n  final DateTime? createdAt;\n\n  @override\n  Widget build(BuildContext context) {\n    final numberFormat = NumberFormat();\n\n    // If more info is added to this Widget, use a separated ListView\n    return Padding(\n      padding: const EdgeInsets.symmetric(horizontal: 6),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          if (documentCounters != null && titleCounters != null) ...[\n            FlowyText.regular(\n              LocaleKeys.moreAction_wordCount.tr(\n                args: [\n                  numberFormat\n                      .format(\n                        documentCounters!.wordCount + titleCounters!.wordCount,\n                      )\n                      .toString(),\n                ],\n              ),\n              fontSize: 12,\n              color: Theme.of(context).hintColor,\n            ),\n            const VSpace(2),\n            FlowyText.regular(\n              LocaleKeys.moreAction_charCount.tr(\n                args: [\n                  numberFormat\n                      .format(\n                        documentCounters!.charCount + titleCounters!.charCount,\n                      )\n                      .toString(),\n                ],\n              ),\n              fontSize: 12,\n              color: Theme.of(context).hintColor,\n            ),\n          ],\n          if (createdAt != null) ...[\n            if (documentCounters != null && titleCounters != null)\n              const VSpace(2),\n            FlowyText.regular(\n              LocaleKeys.moreAction_createdAt.tr(\n                args: [dateFormat.formatDate(createdAt!, true, timeFormat)],\n              ),\n              fontSize: 12,\n              maxLines: 2,\n              color: Theme.of(context).hintColor,\n            ),\n          ],\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart",
    "content": "import 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flutter/material.dart';\nimport 'package:styled_widget/styled_widget.dart';\n\nclass PopoverActionList<T extends PopoverAction> extends StatefulWidget {\n  const PopoverActionList({\n    super.key,\n    this.controller,\n    this.popoverMutex,\n    required this.actions,\n    required this.buildChild,\n    required this.onSelected,\n    this.mutex,\n    this.onClosed,\n    this.onPopupBuilder,\n    this.direction = PopoverDirection.rightWithTopAligned,\n    this.asBarrier = false,\n    this.offset = Offset.zero,\n    this.animationDuration = const Duration(),\n    this.slideDistance = 20,\n    this.beginScaleFactor = 0.9,\n    this.endScaleFactor = 1.0,\n    this.beginOpacity = 0.0,\n    this.endOpacity = 1.0,\n    this.constraints = const BoxConstraints(\n      minWidth: 120,\n      maxWidth: 460,\n      maxHeight: 300,\n    ),\n    this.showAtCursor = false,\n  });\n\n  final PopoverController? controller;\n  final PopoverMutex? popoverMutex;\n  final List<T> actions;\n  final Widget Function(PopoverController) buildChild;\n  final Function(T, PopoverController) onSelected;\n  final PopoverMutex? mutex;\n  final VoidCallback? onClosed;\n  final VoidCallback? onPopupBuilder;\n  final PopoverDirection direction;\n  final bool asBarrier;\n  final Offset offset;\n  final BoxConstraints constraints;\n  final Duration animationDuration;\n  final double slideDistance;\n  final double beginScaleFactor;\n  final double endScaleFactor;\n  final double beginOpacity;\n  final double endOpacity;\n  final bool showAtCursor;\n\n  @override\n  State<PopoverActionList<T>> createState() => _PopoverActionListState<T>();\n}\n\nclass _PopoverActionListState<T extends PopoverAction>\n    extends State<PopoverActionList<T>> {\n  late PopoverController popoverController =\n      widget.controller ?? PopoverController();\n\n  @override\n  void dispose() {\n    if (widget.controller == null) {\n      popoverController.close();\n    }\n    super.dispose();\n  }\n\n  @override\n  void didUpdateWidget(covariant PopoverActionList<T> oldWidget) {\n    if (widget.controller != oldWidget.controller) {\n      popoverController.close();\n      popoverController = widget.controller ?? PopoverController();\n    }\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final child = widget.buildChild(popoverController);\n    return AppFlowyPopover(\n      asBarrier: widget.asBarrier,\n      animationDuration: widget.animationDuration,\n      slideDistance: widget.slideDistance,\n      beginScaleFactor: widget.beginScaleFactor,\n      endScaleFactor: widget.endScaleFactor,\n      beginOpacity: widget.beginOpacity,\n      endOpacity: widget.endOpacity,\n      controller: popoverController,\n      constraints: widget.constraints,\n      direction: widget.direction,\n      mutex: widget.mutex,\n      offset: widget.offset,\n      triggerActions: PopoverTriggerFlags.none,\n      onClose: widget.onClosed,\n      showAtCursor: widget.showAtCursor,\n      popupBuilder: (_) {\n        widget.onPopupBuilder?.call();\n        final List<Widget> children = widget.actions.map((action) {\n          if (action is ActionCell) {\n            return ActionCellWidget<T>(\n              action: action,\n              itemHeight: ActionListSizes.itemHeight,\n              onSelected: (action) {\n                widget.onSelected(action, popoverController);\n              },\n            );\n          } else if (action is PopoverActionCell) {\n            return PopoverActionCellWidget<T>(\n              popoverMutex: widget.popoverMutex,\n              popoverController: popoverController,\n              action: action,\n              itemHeight: ActionListSizes.itemHeight,\n            );\n          } else {\n            final custom = action as CustomActionCell;\n            return custom.buildWithContext(\n              context,\n              popoverController,\n              widget.popoverMutex,\n            );\n          }\n        }).toList();\n\n        return IntrinsicHeight(\n          child: IntrinsicWidth(\n            child: Column(children: children),\n          ),\n        );\n      },\n      child: child,\n    );\n  }\n}\n\nabstract class ActionCell extends PopoverAction {\n  Widget? leftIcon(Color iconColor) => null;\n  Widget? rightIcon(Color iconColor) => null;\n  String get name;\n  Color? textColor(BuildContext context) {\n    return null;\n  }\n}\n\ntypedef PopoverActionCellBuilder = Widget Function(\n  BuildContext context,\n  PopoverController parentController,\n  PopoverController controller,\n);\n\nabstract class PopoverActionCell extends PopoverAction {\n  Widget? leftIcon(Color iconColor) => null;\n  Widget? rightIcon(Color iconColor) => null;\n  String get name;\n\n  PopoverActionCellBuilder get builder;\n}\n\nabstract class CustomActionCell extends PopoverAction {\n  Widget buildWithContext(\n    BuildContext context,\n    PopoverController controller,\n    PopoverMutex? mutex,\n  );\n}\n\nabstract class PopoverAction {}\n\nclass ActionListSizes {\n  static double itemHPadding = 10;\n  static double itemHeight = 20;\n  static double vPadding = 6;\n  static double hPadding = 10;\n}\n\nclass ActionCellWidget<T extends PopoverAction> extends StatelessWidget {\n  const ActionCellWidget({\n    super.key,\n    required this.action,\n    required this.onSelected,\n    required this.itemHeight,\n  });\n\n  final T action;\n  final Function(T) onSelected;\n  final double itemHeight;\n\n  @override\n  Widget build(BuildContext context) {\n    final actionCell = action as ActionCell;\n    final leftIcon =\n        actionCell.leftIcon(Theme.of(context).colorScheme.onSurface);\n\n    final rightIcon =\n        actionCell.rightIcon(Theme.of(context).colorScheme.onSurface);\n\n    return HoverButton(\n      itemHeight: itemHeight,\n      leftIcon: leftIcon,\n      rightIcon: rightIcon,\n      name: actionCell.name,\n      textColor: actionCell.textColor(context),\n      onTap: () => onSelected(action),\n    );\n  }\n}\n\nclass PopoverActionCellWidget<T extends PopoverAction> extends StatefulWidget {\n  const PopoverActionCellWidget({\n    super.key,\n    this.popoverMutex,\n    required this.popoverController,\n    required this.action,\n    required this.itemHeight,\n  });\n\n  final PopoverMutex? popoverMutex;\n  final T action;\n  final double itemHeight;\n\n  final PopoverController popoverController;\n\n  @override\n  State<PopoverActionCellWidget> createState() =>\n      _PopoverActionCellWidgetState();\n}\n\nclass _PopoverActionCellWidgetState<T extends PopoverAction>\n    extends State<PopoverActionCellWidget<T>> {\n  final popoverController = PopoverController();\n  @override\n  Widget build(BuildContext context) {\n    final actionCell = widget.action as PopoverActionCell;\n    final leftIcon =\n        actionCell.leftIcon(Theme.of(context).colorScheme.onSurface);\n    final rightIcon =\n        actionCell.rightIcon(Theme.of(context).colorScheme.onSurface);\n    return AppFlowyPopover(\n      mutex: widget.popoverMutex,\n      controller: popoverController,\n      asBarrier: true,\n      popupBuilder: (context) => actionCell.builder(\n        context,\n        widget.popoverController,\n        popoverController,\n      ),\n      child: HoverButton(\n        itemHeight: widget.itemHeight,\n        leftIcon: leftIcon,\n        rightIcon: rightIcon,\n        name: actionCell.name,\n        onTap: () => popoverController.show(),\n      ),\n    );\n  }\n}\n\nclass HoverButton extends StatelessWidget {\n  const HoverButton({\n    super.key,\n    required this.onTap,\n    required this.itemHeight,\n    this.leftIcon,\n    required this.name,\n    this.rightIcon,\n    this.textColor,\n  });\n\n  final VoidCallback onTap;\n  final double itemHeight;\n  final Widget? leftIcon;\n  final Widget? rightIcon;\n  final String name;\n  final Color? textColor;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyHover(\n      child: GestureDetector(\n        behavior: HitTestBehavior.opaque,\n        onTap: onTap,\n        child: SizedBox(\n          height: itemHeight,\n          child: Row(\n            children: [\n              if (leftIcon != null) ...[\n                leftIcon!,\n                HSpace(ActionListSizes.itemHPadding),\n              ],\n              Expanded(\n                child: FlowyText.regular(\n                  name,\n                  overflow: TextOverflow.visible,\n                  lineHeight: 1.15,\n                  color: textColor,\n                ),\n              ),\n              if (rightIcon != null) ...[\n                HSpace(ActionListSizes.itemHPadding),\n                rightIcon!,\n              ],\n            ],\n          ),\n        ).padding(\n          horizontal: ActionListSizes.hPadding,\n          vertical: ActionListSizes.vPadding,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flowy_infra_ui/style_widget/text_field.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/widgets.dart';\n\nimport '../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\n\nclass RenameViewPopover extends StatefulWidget {\n  const RenameViewPopover({\n    super.key,\n    required this.view,\n    required this.name,\n    required this.popoverController,\n    required this.emoji,\n    this.icon,\n    this.showIconChanger = true,\n    this.tabs = const [PickerTabType.emoji, PickerTabType.icon],\n  });\n\n  final ViewPB view;\n  final String name;\n  final PopoverController popoverController;\n  final EmojiIconData emoji;\n  final Widget? icon;\n  final bool showIconChanger;\n  final List<PickerTabType> tabs;\n\n  @override\n  State<RenameViewPopover> createState() => _RenameViewPopoverState();\n}\n\nclass _RenameViewPopoverState extends State<RenameViewPopover> {\n  final TextEditingController _controller = TextEditingController();\n\n  @override\n  void initState() {\n    super.initState();\n    _controller.text = widget.name;\n    _controller.selection =\n        TextSelection(baseOffset: 0, extentOffset: widget.name.length);\n  }\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      mainAxisSize: MainAxisSize.min,\n      children: [\n        if (widget.showIconChanger) ...[\n          SizedBox(\n            width: 30.0,\n            child: EmojiPickerButton(\n              emoji: widget.emoji,\n              defaultIcon: widget.icon,\n              direction: PopoverDirection.bottomWithCenterAligned,\n              offset: const Offset(0, 18),\n              onSubmitted: _updateViewIcon,\n              documentId: widget.view.id,\n              tabs: widget.tabs,\n            ),\n          ),\n          const HSpace(6),\n        ],\n        SizedBox(\n          height: 32.0,\n          width: 220,\n          child: FlowyTextField(\n            controller: _controller,\n            maxLength: 256,\n            onSubmitted: _updateViewName,\n            onCanceled: () => _updateViewName(_controller.text),\n            showCounter: false,\n          ),\n        ),\n      ],\n    );\n  }\n\n  Future<void> _updateViewName(String name) async {\n    if (name.isNotEmpty && name != widget.name) {\n      await ViewBackendService.updateView(\n        viewId: widget.view.id,\n        name: _controller.text,\n      );\n      widget.popoverController.close();\n    }\n  }\n\n  Future<void> _updateViewIcon(\n    SelectedEmojiIconResult r,\n    PopoverController? _,\n  ) async {\n    await ViewBackendService.updateViewIcon(\n      view: widget.view,\n      viewIcon: r.data,\n    );\n    if (!r.keepOpen) {\n      widget.popoverController.close();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/sidebar_resizer.dart",
    "content": "import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nclass SidebarResizer extends StatefulWidget {\n  const SidebarResizer({super.key});\n\n  @override\n  State<SidebarResizer> createState() => _SidebarResizerState();\n}\n\nclass _SidebarResizerState extends State<SidebarResizer> {\n  final ValueNotifier<bool> isHovered = ValueNotifier(false);\n  final ValueNotifier<bool> isDragging = ValueNotifier(false);\n\n  @override\n  void dispose() {\n    isHovered.dispose();\n    isDragging.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      cursor: SystemMouseCursors.resizeLeftRight,\n      onEnter: (_) => isHovered.value = true,\n      onExit: (_) => isHovered.value = false,\n      child: GestureDetector(\n        dragStartBehavior: DragStartBehavior.down,\n        behavior: HitTestBehavior.translucent,\n        onHorizontalDragStart: (details) {\n          isDragging.value = true;\n\n          context\n              .read<HomeSettingBloc>()\n              .add(const HomeSettingEvent.editPanelResizeStart());\n        },\n        onHorizontalDragUpdate: (details) {\n          isDragging.value = true;\n\n          context\n              .read<HomeSettingBloc>()\n              .add(HomeSettingEvent.editPanelResized(details.localPosition.dx));\n        },\n        onHorizontalDragEnd: (details) {\n          isDragging.value = false;\n\n          context\n              .read<HomeSettingBloc>()\n              .add(const HomeSettingEvent.editPanelResizeEnd());\n        },\n        onHorizontalDragCancel: () {\n          isDragging.value = false;\n\n          context\n              .read<HomeSettingBloc>()\n              .add(const HomeSettingEvent.editPanelResizeEnd());\n        },\n        child: ValueListenableBuilder(\n          valueListenable: isHovered,\n          builder: (context, isHovered, _) {\n            return ValueListenableBuilder(\n              valueListenable: isDragging,\n              builder: (context, isDragging, _) {\n                return Container(\n                  width: 2,\n                  // increase the width of the resizer to make it easier to drag\n                  margin: const EdgeInsets.only(right: 2.0),\n                  height: MediaQuery.of(context).size.height,\n                  color: isHovered || isDragging\n                      ? const Color(0xFF00B5FF)\n                      : Colors.transparent,\n                );\n              },\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/tab_bar_item.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view/view_listener.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass ViewTabBarItem extends StatefulWidget {\n  const ViewTabBarItem({\n    super.key,\n    required this.view,\n    this.shortForm = false,\n  });\n\n  final ViewPB view;\n  final bool shortForm;\n\n  @override\n  State<ViewTabBarItem> createState() => _ViewTabBarItemState();\n}\n\nclass _ViewTabBarItemState extends State<ViewTabBarItem> {\n  late final ViewListener _viewListener;\n  late ViewPB view;\n\n  @override\n  void initState() {\n    super.initState();\n    view = widget.view;\n    _viewListener = ViewListener(viewId: widget.view.id);\n    _viewListener.start(\n      onViewUpdated: (updatedView) {\n        if (mounted) {\n          setState(() => view = updatedView);\n        }\n      },\n    );\n  }\n\n  @override\n  void dispose() {\n    _viewListener.stop();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      mainAxisAlignment:\n          widget.shortForm ? MainAxisAlignment.center : MainAxisAlignment.start,\n      children: [\n        if (widget.view.icon.value.isNotEmpty)\n          RawEmojiIconWidget(\n            emoji: widget.view.icon.toEmojiIconData(),\n            emojiSize: 16,\n          ),\n        if (!widget.shortForm && view.icon.value.isNotEmpty) const HSpace(6),\n        if (!widget.shortForm || view.icon.value.isEmpty) ...[\n          Flexible(\n            child: FlowyText.medium(\n              view.nameOrDefault,\n              overflow: TextOverflow.ellipsis,\n            ),\n          ),\n        ],\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle.dart",
    "content": "import 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\n\nclass ToggleStyle {\n  const ToggleStyle({\n    required this.height,\n    required this.width,\n    required this.thumbRadius,\n  });\n\n  const ToggleStyle.big()\n      : height = 16,\n        width = 27,\n        thumbRadius = 14;\n\n  const ToggleStyle.small()\n      : height = 10,\n        width = 16,\n        thumbRadius = 8;\n\n  const ToggleStyle.mobile()\n      : height = 24,\n        width = 42,\n        thumbRadius = 18;\n\n  final double height;\n  final double width;\n  final double thumbRadius;\n}\n\nclass Toggle extends StatelessWidget {\n  const Toggle({\n    super.key,\n    required this.value,\n    required this.onChanged,\n    this.style = const ToggleStyle.big(),\n    this.thumbColor,\n    this.activeBackgroundColor,\n    this.inactiveBackgroundColor,\n    this.duration = const Duration(milliseconds: 150),\n    this.padding = const EdgeInsets.all(8.0),\n  });\n\n  final bool value;\n  final void Function(bool) onChanged;\n  final ToggleStyle style;\n  final Color? thumbColor;\n  final Color? activeBackgroundColor;\n  final Color? inactiveBackgroundColor;\n  final EdgeInsets padding;\n  final Duration duration;\n\n  @override\n  Widget build(BuildContext context) {\n    final backgroundColor = value\n        ? activeBackgroundColor ?? Theme.of(context).colorScheme.primary\n        : inactiveBackgroundColor ??\n            AFThemeExtension.of(context).toggleButtonBGColor;\n    return GestureDetector(\n      onTap: () => onChanged(!value),\n      child: Padding(\n        padding: padding,\n        child: Stack(\n          children: [\n            Container(\n              height: style.height,\n              width: style.width,\n              decoration: BoxDecoration(\n                color: backgroundColor,\n                borderRadius: BorderRadius.circular(style.height / 2),\n              ),\n            ),\n            AnimatedPositioned(\n              duration: duration,\n              top: (style.height - style.thumbRadius) / 2,\n              left: value ? style.width - style.thumbRadius - 1 : 1,\n              child: Container(\n                height: style.thumbRadius,\n                width: style.thumbRadius,\n                decoration: BoxDecoration(\n                  color: thumbColor ?? Colors.white,\n                  borderRadius: BorderRadius.circular(style.thumbRadius / 2),\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass UserAvatar extends StatelessWidget {\n  const UserAvatar({\n    super.key,\n    required this.iconUrl,\n    required this.name,\n    required this.size,\n    this.isHovering = false,\n    this.decoration,\n  });\n\n  final String iconUrl;\n  final String name;\n\n  final AFAvatarSize size;\n  final Decoration? decoration;\n\n  // If true, a border will be applied on top of the avatar\n  final bool isHovering;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return SizedBox.square(\n      dimension: size.size,\n      child: DecoratedBox(\n        decoration: decoration ??\n            BoxDecoration(\n              shape: BoxShape.circle,\n              border: isHovering\n                  ? Border.all(\n                      color: theme.iconColorScheme.primary,\n                      width: 4,\n                    )\n                  : null,\n            ),\n        child: AFAvatar(\n          url: iconUrl,\n          name: name,\n          size: size,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart",
    "content": "import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart';\nimport 'package:appflowy/features/share_tab/data/models/share_section_type.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/tab.dart';\nimport 'package:appflowy/startup/plugin/plugin.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/util/string_extension.dart';\nimport 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';\nimport 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/application/view_title/view_title_bar_bloc.dart';\nimport 'package:appflowy/workspace/application/view_title/view_title_bloc.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';\nimport 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'\n    hide AFRolePB;\nimport 'package:appflowy_backend/protobuf/flowy-user/workspace.pbenum.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:collection/collection.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\n\nimport '../../../plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';\n\n// space name > ... > view_title\nclass ViewTitleBar extends StatelessWidget {\n  const ViewTitleBar({\n    super.key,\n    required this.view,\n  });\n\n  final ViewPB view;\n\n  @override\n  Widget build(BuildContext context) {\n    return MultiBlocProvider(\n      providers: [\n        BlocProvider(\n          create: (_) => ViewTitleBarBloc(\n            view: view,\n          ),\n        ),\n      ],\n      child: BlocBuilder<PageAccessLevelBloc, PageAccessLevelState>(\n        buildWhen: (previous, current) =>\n            previous.isLoadingLockStatus != current.isLoadingLockStatus,\n        builder: (context, pageAccessLevelState) {\n          return BlocConsumer<ViewTitleBarBloc, ViewTitleBarState>(\n            listener: (context, state) {\n              // update the page section type when the space permission is changed\n              final spacePermission = state.ancestors\n                  .firstWhereOrNull(\n                    (ancestor) => ancestor.isSpace,\n                  )\n                  ?.spacePermission;\n              if (spacePermission == null) {\n                return;\n              }\n              final sectionType = switch (spacePermission) {\n                SpacePermission.publicToAll => SharedSectionType.public,\n                SpacePermission.private => SharedSectionType.private,\n              };\n              final bloc = context.read<PageAccessLevelBloc>();\n              if (!bloc.isClosed && !bloc.state.isShared) {\n                bloc.add(\n                  PageAccessLevelEvent.updateSectionType(sectionType),\n                );\n              }\n            },\n            builder: (context, state) {\n              final theme = AppFlowyTheme.of(context);\n              final ancestors = state.ancestors;\n              if (ancestors.isEmpty) {\n                return const SizedBox.shrink();\n              }\n              return SingleChildScrollView(\n                scrollDirection: Axis.horizontal,\n                child: SizedBox(\n                  height: 24,\n                  child: Row(\n                    children: [\n                      ..._buildViewTitles(\n                        context,\n                        ancestors,\n                        state.isDeleted,\n                        pageAccessLevelState.isEditable,\n                        pageAccessLevelState,\n                      ),\n                      HSpace(theme.spacing.m),\n                      _buildLockPageStatus(context),\n                    ],\n                  ),\n                ),\n              );\n            },\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildLockPageStatus(BuildContext context) {\n    return BlocConsumer<PageAccessLevelBloc, PageAccessLevelState>(\n      listenWhen: (previous, current) =>\n          previous.isLoadingLockStatus == current.isLoadingLockStatus &&\n          current.isLoadingLockStatus == false,\n      listener: (context, state) {\n        if (state.isLocked) {\n          showToastNotification(\n            message: LocaleKeys.lockPage_pageLockedToast.tr(),\n          );\n        }\n      },\n      builder: (context, state) {\n        if (state.isLocked) {\n          return LockedPageStatus();\n        } else if (!state.isLocked && state.lockCounter > 0) {\n          return ReLockedPageStatus();\n        }\n        return const SizedBox.shrink();\n      },\n    );\n  }\n\n  List<Widget> _buildViewTitles(\n    BuildContext context,\n    List<ViewPB> views,\n    bool isDeleted,\n    bool isEditable,\n    PageAccessLevelState pageAccessLevelState,\n  ) {\n    final theme = AppFlowyTheme.of(context);\n\n    if (isDeleted) {\n      return _buildDeletedTitle(context, views.last);\n    }\n\n    // if the level is too deep, only show the last two view, the first one view and the root view\n    // for example:\n    // if the views are [root, view1, view2, view3, view4, view5], only show [root, view1, ..., view4, view5]\n    // if the views are [root, view1, view2, view3], show [root, view1, view2, view3]\n    const lowerBound = 2;\n    final upperBound = views.length - 2;\n    bool hasAddedEllipsis = false;\n    final children = <Widget>[];\n\n    if (views.length <= 1) {\n      return [];\n    }\n\n    // remove the space from views if the current user role is a guest\n    final myRole =\n        context.read<UserWorkspaceBloc>().state.currentWorkspace?.role;\n    if (myRole == AFRolePB.Guest) {\n      views = views.where((view) => !view.isSpace).toList();\n    }\n\n    // ignore the workspace name, use section name instead in the future\n    // skip the workspace view\n    for (var i = 1; i < views.length; i++) {\n      final view = views[i];\n\n      if (i >= lowerBound && i < upperBound) {\n        if (!hasAddedEllipsis) {\n          hasAddedEllipsis = true;\n          children.addAll([\n            const FlowyText.regular(' ... '),\n            const FlowySvg(FlowySvgs.title_bar_divider_s),\n          ]);\n        }\n        continue;\n      }\n\n      final child = FlowyTooltip(\n        key: ValueKey(view.id),\n        message: view.name,\n        child: ViewTitle(\n          view: view,\n          behavior: i == views.length - 1 && !view.isLocked && isEditable\n              ? ViewTitleBehavior.editable // only the last one is editable\n              : ViewTitleBehavior.uneditable, // others are not editable\n          onUpdated: () {\n            if (context.mounted) {\n              context\n                  .read<ViewTitleBarBloc>()\n                  .add(const ViewTitleBarEvent.reload());\n            }\n          },\n        ),\n      );\n\n      children.add(child);\n\n      if (i != views.length - 1) {\n        // if not the last one, add a divider\n        children.add(const FlowySvg(FlowySvgs.title_bar_divider_s));\n      }\n    }\n\n    // add the section icon in the breadcrumb\n    children.addAll([\n      HSpace(theme.spacing.xs),\n      BlocBuilder<PageAccessLevelBloc, PageAccessLevelState>(\n        buildWhen: (previous, current) =>\n            previous.sectionType != current.sectionType,\n        builder: (context, state) {\n          return _buildSectionIcon(context, state);\n        },\n      ),\n    ]);\n\n    return children;\n  }\n\n  List<Widget> _buildDeletedTitle(BuildContext context, ViewPB view) {\n    return [\n      const TrashBreadcrumb(),\n      const FlowySvg(FlowySvgs.title_bar_divider_s),\n      FlowyTooltip(\n        key: ValueKey(view.id),\n        message: view.name,\n        child: ViewTitle(\n          view: view,\n          onUpdated: () => context\n              .read<ViewTitleBarBloc>()\n              .add(const ViewTitleBarEvent.reload()),\n        ),\n      ),\n    ];\n  }\n\n  Widget _buildSectionIcon(\n    BuildContext context,\n    PageAccessLevelState pageAccessLevelState,\n  ) {\n    final theme = AppFlowyTheme.of(context);\n    final state = context.read<UserWorkspaceBloc>().state;\n\n    if (state.currentWorkspace?.workspaceType == WorkspaceTypePB.LocalW) {\n      return const SizedBox.shrink();\n    }\n\n    final iconName = switch (pageAccessLevelState.sectionType) {\n      SharedSectionType.public => FlowySvgs.public_section_icon_m,\n      SharedSectionType.private => FlowySvgs.private_section_icon_m,\n      SharedSectionType.shared => FlowySvgs.shared_section_icon_m,\n      SharedSectionType.unknown =>\n        throw UnsupportedError('Unknown section type'),\n    };\n\n    final icon = FlowySvg(\n      iconName,\n      color: theme.iconColorScheme.tertiary,\n      size: Size.square(20),\n    );\n\n    final text = switch (pageAccessLevelState.sectionType) {\n      SharedSectionType.public => 'Team space',\n      SharedSectionType.private => 'Private',\n      SharedSectionType.shared => 'Shared',\n      SharedSectionType.unknown =>\n        throw UnsupportedError('Unknown section type'),\n    };\n\n    final workspaceName = state.currentWorkspace?.name;\n    final tooltipText = switch (pageAccessLevelState.sectionType) {\n      SharedSectionType.public => 'Everyone at $workspaceName has access',\n      SharedSectionType.private => 'Only you have access',\n      SharedSectionType.shared => '',\n      SharedSectionType.unknown =>\n        throw UnsupportedError('Unknown section type'),\n    };\n\n    return FlowyTooltip(\n      message: tooltipText,\n      child: Row(\n        textBaseline: TextBaseline.alphabetic,\n        crossAxisAlignment: CrossAxisAlignment.baseline,\n        children: [\n          HSpace(theme.spacing.xs),\n          icon,\n          const HSpace(4.0), // ask designer to provide the spacing\n          Text(\n            text,\n            style: theme.textStyle.caption\n                .enhanced(color: theme.textColorScheme.tertiary),\n          ),\n          HSpace(theme.spacing.xs),\n        ],\n      ),\n    );\n  }\n}\n\nclass TrashBreadcrumb extends StatelessWidget {\n  const TrashBreadcrumb({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      height: 32,\n      child: FlowyButton(\n        useIntrinsicWidth: true,\n        margin: const EdgeInsets.symmetric(horizontal: 6.0),\n        onTap: () {\n          getIt<MenuSharedState>().latestOpenView = null;\n          getIt<TabsBloc>().add(\n            TabsEvent.openPlugin(\n              plugin: makePlugin(pluginType: PluginType.trash),\n            ),\n          );\n        },\n        text: Row(\n          children: [\n            const FlowySvg(FlowySvgs.trash_s, size: Size.square(14)),\n            const HSpace(4.0),\n            FlowyText.regular(\n              LocaleKeys.trash_text.tr(),\n              fontSize: 14.0,\n              overflow: TextOverflow.ellipsis,\n              figmaLineHeight: 18.0,\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nenum ViewTitleBehavior {\n  editable,\n  uneditable,\n}\n\nclass ViewTitle extends StatefulWidget {\n  const ViewTitle({\n    super.key,\n    required this.view,\n    this.behavior = ViewTitleBehavior.editable,\n    required this.onUpdated,\n  });\n\n  final ViewPB view;\n  final ViewTitleBehavior behavior;\n  final VoidCallback onUpdated;\n\n  @override\n  State<ViewTitle> createState() => _ViewTitleState();\n}\n\nclass _ViewTitleState extends State<ViewTitle> {\n  final popoverController = PopoverController();\n  final textEditingController = TextEditingController();\n\n  @override\n  void dispose() {\n    textEditingController.dispose();\n    popoverController.close();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final isEditable = widget.behavior == ViewTitleBehavior.editable;\n\n    return BlocProvider(\n      create: (_) => ViewTitleBloc(view: widget.view)\n        ..add(\n          const ViewTitleEvent.initial(),\n        ),\n      child: BlocConsumer<ViewTitleBloc, ViewTitleState>(\n        listenWhen: (previous, current) {\n          if (previous.view == null || current.view == null) {\n            return false;\n          }\n\n          return previous.view != current.view;\n        },\n        listener: (_, state) {\n          _resetTextEditingController(state);\n          widget.onUpdated();\n        },\n        builder: (context, state) {\n          // root view\n          if (widget.view.parentViewId.isEmpty) {\n            return Row(\n              children: [\n                FlowyText.regular(state.name),\n                const HSpace(4.0),\n              ],\n            );\n          } else if (widget.view.isSpace) {\n            return _buildSpaceTitle(context, state);\n          } else if (isEditable) {\n            return _buildEditableViewTitle(context, state);\n          } else {\n            return _buildUnEditableViewTitle(context, state);\n          }\n        },\n      ),\n    );\n  }\n\n  Widget _buildSpaceTitle(BuildContext context, ViewTitleState state) {\n    return Container(\n      alignment: Alignment.center,\n      margin: const EdgeInsets.symmetric(horizontal: 6.0),\n      child: _buildIconAndName(context, state, false),\n    );\n  }\n\n  Widget _buildUnEditableViewTitle(BuildContext context, ViewTitleState state) {\n    return Listener(\n      onPointerDown: (_) => context.read<TabsBloc>().openPlugin(widget.view),\n      child: SizedBox(\n        height: 32.0,\n        child: FlowyButton(\n          useIntrinsicWidth: true,\n          margin: const EdgeInsets.symmetric(horizontal: 6.0),\n          text: _buildIconAndName(context, state, false),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildEditableViewTitle(BuildContext context, ViewTitleState state) {\n    return AppFlowyPopover(\n      constraints: const BoxConstraints(\n        maxWidth: 300,\n        maxHeight: 44,\n      ),\n      controller: popoverController,\n      direction: PopoverDirection.bottomWithLeftAligned,\n      offset: const Offset(0, 6),\n      popupBuilder: (context) {\n        // icon + textfield\n        _resetTextEditingController(state);\n        return RenameViewPopover(\n          view: widget.view,\n          name: widget.view.name,\n          popoverController: popoverController,\n          icon: widget.view.defaultIcon(),\n          emoji: state.icon,\n          tabs: const [\n            PickerTabType.emoji,\n            PickerTabType.icon,\n            PickerTabType.custom,\n          ],\n        );\n      },\n      child: SizedBox(\n        height: 32.0,\n        child: FlowyButton(\n          useIntrinsicWidth: true,\n          margin: const EdgeInsets.symmetric(horizontal: 6.0),\n          text: _buildIconAndName(context, state, true),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildIconAndName(\n    BuildContext context,\n    ViewTitleState state,\n    bool isEditable,\n  ) {\n    final view = state.view ?? widget.view;\n    final spaceIcon = view.buildSpaceIconSvg(context);\n    final icon =\n        state.icon.isNotEmpty ? state.icon : view.icon.toEmojiIconData();\n    final name = state.name.isEmpty ? widget.view.name : state.name;\n    return SingleChildScrollView(\n      child: Row(\n        children: [\n          if (icon.isNotEmpty) ...[\n            RawEmojiIconWidget(emoji: icon, emojiSize: 14.0),\n            const HSpace(4.0),\n          ],\n          if (view.isSpace && spaceIcon != null) ...[\n            SpaceIcon(\n              dimension: 14,\n              svgSize: 8.5,\n              space: view,\n              cornerRadius: 4,\n            ),\n            const HSpace(6.0),\n          ],\n          Opacity(\n            opacity: isEditable ? 1.0 : 0.5,\n            child: FlowyText.regular(\n              name.orDefault(LocaleKeys.menuAppHeader_defaultNewPageName.tr()),\n              fontSize: 14.0,\n              overflow: TextOverflow.ellipsis,\n              figmaLineHeight: 18.0,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  void _resetTextEditingController(ViewTitleState state) {\n    textEditingController\n      ..text = state.name\n      ..selection = TextSelection(\n        baseOffset: 0,\n        extentOffset: state.name.length,\n      );\n  }\n}\n\nclass LockedPageStatus extends StatelessWidget {\n  const LockedPageStatus({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final color = const Color(0xFFD95A0B);\n    return FlowyTooltip(\n      message: LocaleKeys.lockPage_lockTooltip.tr(),\n      child: DecoratedBox(\n        decoration: ShapeDecoration(\n          shape: RoundedRectangleBorder(\n            side: BorderSide(color: color),\n            borderRadius: BorderRadius.circular(6),\n          ),\n          color: context.lockedPageButtonBackground,\n        ),\n        child: FlowyButton(\n          useIntrinsicWidth: true,\n          margin: const EdgeInsets.symmetric(\n            horizontal: 4.0,\n            vertical: 4.0,\n          ),\n          iconPadding: 4.0,\n          text: FlowyText.regular(\n            LocaleKeys.lockPage_lockPage.tr(),\n            color: color,\n            fontSize: 12.0,\n          ),\n          hoverColor: color.withValues(alpha: 0.1),\n          leftIcon: FlowySvg(\n            FlowySvgs.lock_page_fill_s,\n            blendMode: null,\n          ),\n          onTap: () => context.read<PageAccessLevelBloc>().add(\n                const PageAccessLevelEvent.unlock(),\n              ),\n        ),\n      ),\n    );\n  }\n}\n\nclass ReLockedPageStatus extends StatelessWidget {\n  const ReLockedPageStatus({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final iconColor = const Color(0xFF8F959E);\n    return DecoratedBox(\n      decoration: ShapeDecoration(\n        shape: RoundedRectangleBorder(\n          side: BorderSide(color: iconColor),\n          borderRadius: BorderRadius.circular(6),\n        ),\n        color: context.lockedPageButtonBackground,\n      ),\n      child: FlowyButton(\n        useIntrinsicWidth: true,\n        margin: const EdgeInsets.symmetric(\n          horizontal: 4.0,\n          vertical: 4.0,\n        ),\n        iconPadding: 4.0,\n        text: FlowyText.regular(\n          LocaleKeys.lockPage_reLockPage.tr(),\n          fontSize: 12.0,\n        ),\n        leftIcon: FlowySvg(\n          FlowySvgs.unlock_page_s,\n          color: iconColor,\n          blendMode: null,\n        ),\n        onTap: () => context.read<PageAccessLevelBloc>().add(\n              const PageAccessLevelEvent.lock(),\n            ),\n      ),\n    );\n  }\n}\n\nextension on BuildContext {\n  Color get lockedPageButtonBackground {\n    if (Theme.of(this).brightness == Brightness.light) {\n      return Colors.white.withValues(alpha: 0.75);\n    }\n    return Color(0xB21B1A22);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/linux/.gitignore",
    "content": "flutter/ephemeral\n"
  },
  {
    "path": "frontend/appflowy_flutter/linux/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.10)\nproject(runner LANGUAGES CXX)\n\nset(BINARY_NAME \"AppFlowy\")\nset(APPLICATION_ID \"io.appflowy.appflowy\")\n\ncmake_policy(SET CMP0063 NEW)\n\nset(CMAKE_INSTALL_RPATH \"$ORIGIN/lib\")\n\n# Root filesystem for cross-building.\nif(FLUTTER_TARGET_PLATFORM_SYSROOT)\n  set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\n  set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\nendif()\n\n# Configure build options.\nif(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n  set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n    STRING \"Flutter build mode\" FORCE)\n  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n    \"Debug\" \"Profile\" \"Release\")\nendif()\n\n# Compilation settings that should be applied to most targets.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_14)\n  target_compile_options(${TARGET} PRIVATE -Wall -Werror)\n  target_compile_options(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:-O3>\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:NDEBUG>\")\nendfunction()\n\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\n\n# Flutter library and tool build rules.\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# System-level dependencies.\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)\n\nadd_definitions(-DAPPLICATION_ID=\"${APPLICATION_ID}\")\n\n# Application build\nadd_executable(${BINARY_NAME}\n  \"main.cc\"\n  \"my_application.cc\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n)\napply_standard_settings(${BINARY_NAME})\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter)\ntarget_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n\n# Only the install-generated bundle's copy of the executable will launch\n# correctly, since the resources must in the right relative locations. To avoid\n# people trying to run the unbundled copy, put it in a subdirectory instead of\n# the default top-level location.\nset_target_properties(${BINARY_NAME}\n  PROPERTIES\n  RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/intermediates_do_not_run\"\n)\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n# === Installation ===\n# By default, \"installing\" just makes a relocatable bundle in the build\n# directory.\nset(BUILD_BUNDLE_DIR \"${PROJECT_BINARY_DIR}/bundle\")\n\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\n# Start with a clean build bundle directory every time.\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${BUILD_BUNDLE_DIR}/\\\")\n  \" COMPONENT Runtime)\n\nset(DART_FFI_DIR \"${CMAKE_INSTALL_PREFIX}/lib\")\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}/lib\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${DART_FFI_DLL}\" DESTINATION \"${DART_FFI_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nif(PLUGIN_BUNDLED_LIBRARIES)\n  install(FILES \"${PLUGIN_BUNDLED_LIBRARIES}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n\n# Fully re-copy the assets directory on each build to avoid having stale files\n# from a previous install.\nset(FLUTTER_ASSET_DIR_NAME \"flutter_assets\")\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\\\")\n  \" COMPONENT Runtime)\ninstall(DIRECTORY \"${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}\"\n  DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\" COMPONENT Runtime)\n\n# Install the AOT library on non-Debug builds only.\nif(NOT CMAKE_BUILD_TYPE MATCHES \"Debug\")\n  install(FILES \"${AOT_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n"
  },
  {
    "path": "frontend/appflowy_flutter/linux/flutter/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.10)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\n\n# Serves the same purpose as list(TRANSFORM ... PREPEND ...),\n# which isn't available in 3.10.\nfunction(list_prepend LIST_NAME PREFIX)\n    set(NEW_LIST \"\")\n    foreach(element ${${LIST_NAME}})\n        list(APPEND NEW_LIST \"${PREFIX}${element}\")\n    endforeach(element)\n    set(${LIST_NAME} \"${NEW_LIST}\" PARENT_SCOPE)\nendfunction()\n\n# === Flutter Library ===\n# System-level dependencies.\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)\npkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)\npkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)\n\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/libflutter_linux_gtk.so\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(DART_FFI_DLL \"${CMAKE_CURRENT_SOURCE_DIR}/dart_ffi/libdart_ffi.so\" PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/lib/libapp.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"fl_basic_message_channel.h\"\n  \"fl_binary_codec.h\"\n  \"fl_binary_messenger.h\"\n  \"fl_dart_project.h\"\n  \"fl_engine.h\"\n  \"fl_json_message_codec.h\"\n  \"fl_json_method_codec.h\"\n  \"fl_message_codec.h\"\n  \"fl_method_call.h\"\n  \"fl_method_channel.h\"\n  \"fl_method_codec.h\"\n  \"fl_method_response.h\"\n  \"fl_plugin_registrar.h\"\n  \"fl_plugin_registry.h\"\n  \"fl_standard_message_codec.h\"\n  \"fl_standard_method_codec.h\"\n  \"fl_string_codec.h\"\n  \"fl_value.h\"\n  \"fl_view.h\"\n  \"flutter_linux.h\"\n)\nlist_prepend(FLUTTER_LIBRARY_HEADERS \"${EPHEMERAL_DIR}/flutter_linux/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}\")\ntarget_link_libraries(flutter INTERFACE\n  PkgConfig::GTK\n  PkgConfig::GLIB\n  PkgConfig::GIO\n)\nadd_dependencies(flutter flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CMAKE_CURRENT_BINARY_DIR}/_phony_\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh\"\n      ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n)\n"
  },
  {
    "path": "frontend/appflowy_flutter/linux/flutter/dart_ffi/binding.h",
    "content": "#include <stdarg.h>\n#include <stdbool.h>\n#include <stdint.h>\n#include <stdlib.h>\n\nint64_t init_sdk(int64_t port, char *data);\n\nvoid async_event(int64_t port, const uint8_t *input, uintptr_t len);\n\nconst uint8_t *sync_event(const uint8_t *input, uintptr_t len);\n\nint32_t set_stream_port(int64_t port);\n\nint32_t set_log_stream_port(int64_t port);\n\nvoid link_me_please(void);\n\nvoid rust_log(int64_t level, const char *data);\n\nvoid set_env(const char *data);\n"
  },
  {
    "path": "frontend/appflowy_flutter/linux/main.cc",
    "content": "#include \"my_application.h\"\n\nint main(int argc, char** argv) {\n  g_autoptr(MyApplication) app = my_application_new();\n  return g_application_run(G_APPLICATION(app), argc, argv);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/linux/my_application.cc",
    "content": "#include \"my_application.h\"\n\n#include <flutter_linux/flutter_linux.h>\n#ifdef GDK_WINDOWING_X11\n#include <gdk/gdkx.h>\n#endif\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nstruct _MyApplication\n{\n  GtkApplication parent_instance;\n  char **dart_entrypoint_arguments;\n};\n\nG_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)\n\n// Implements GApplication::activate.\nstatic void my_application_activate(GApplication *application)\n{\n  MyApplication *self = MY_APPLICATION(application);\n\n  GList* windows = gtk_application_get_windows(GTK_APPLICATION(application));\n  if (windows) {\n    gtk_window_present(GTK_WINDOW(windows->data));\n    return;\n  }\n\n  GtkWindow *window =\n      GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));\n  gtk_window_set_title(window, \"AppFlowy\");\n\n  gtk_window_set_default_size(window, 1280, 720);\n  gtk_widget_show(GTK_WIDGET(window));\n\n  g_autoptr(FlDartProject) project = fl_dart_project_new();\n  fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);\n\n  FlView *view = fl_view_new(project);\n  gtk_widget_show(GTK_WIDGET(view));\n  gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));\n\n  fl_register_plugins(FL_PLUGIN_REGISTRY(view));\n\n  gtk_widget_grab_focus(GTK_WIDGET(view));\n}\n\n// Implements GApplication::local_command_line.\nstatic gboolean my_application_local_command_line(GApplication *application, gchar ***arguments, int *exit_status)\n{\n  MyApplication *self = MY_APPLICATION(application);\n  // Strip out the first argument as it is the binary name.\n  self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);\n\n  g_autoptr(GError) error = nullptr;\n  if (!g_application_register(application, nullptr, &error))\n  {\n    g_warning(\"Failed to register: %s\", error->message);\n    *exit_status = 1;\n    return TRUE;\n  }\n\n  g_application_activate(application);\n  *exit_status = 0;\n\n  return FALSE;\n}\n\n// Implements GObject::dispose.\nstatic void my_application_dispose(GObject *object)\n{\n  MyApplication *self = MY_APPLICATION(object);\n  g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);\n  G_OBJECT_CLASS(my_application_parent_class)->dispose(object);\n}\n\nstatic void my_application_class_init(MyApplicationClass *klass)\n{\n  G_APPLICATION_CLASS(klass)->activate = my_application_activate;\n  G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;\n  G_OBJECT_CLASS(klass)->dispose = my_application_dispose;\n}\n\nstatic void my_application_init(MyApplication *self) {}\n\nMyApplication* my_application_new() {\n  return MY_APPLICATION(g_object_new(my_application_get_type(),\n                                     \"application-id\", APPLICATION_ID,\n                                     \"flags\", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN,\n                                     nullptr));\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/linux/my_application.h",
    "content": "#ifndef FLUTTER_MY_APPLICATION_H_\n#define FLUTTER_MY_APPLICATION_H_\n\n#include <gtk/gtk.h>\n\nG_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,\n                     GtkApplication)\n\n/**\n * my_application_new:\n *\n * Creates a new Flutter-based application.\n *\n * Returns: a new #MyApplication.\n */\nMyApplication* my_application_new();\n\n#endif  // FLUTTER_MY_APPLICATION_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/linux/packaging/deb/make_config.yaml",
    "content": "display_name: AppFlowy\npackage_name: appflowy\n\nmaintainer:\n  name: AppFlowy\n  email: support@appflowy.io\n\nkeywords:\n  - AppFlowy\n  - Office\n  - Document\n  - Database\n  - Note\n  - Kanban\n  - Note\n\ninstalled_size: 100000\nicon: linux/packaging/assets/logo.png\n\ngeneric_name: AppFlowy\n\ncategories:\n  - Office\n  - Productivity\n\nstartup_notify: true\nessential: false\n\nsection: x11\npriority: optional\n\nsupportedMimeType: x-scheme-handler/appflowy-flutter\n\ndependencies:\n  - libnotify-bin\n  - libkeybinder-3.0-0\n"
  },
  {
    "path": "frontend/appflowy_flutter/linux/packaging/rpm/make_config.yaml",
    "content": "display_name: AppFlowy\nicon: linux/packaging/assets/logo.png\ngroup: Applications/Office\nvendor: AppFlowy\npackager: AppFlowy\npackagerEmail: support@appflowy.io\nlicense: APGL-3.0\nurl: https://github.com/AppFlowy-IO/appflowy\n\nbuild_arch: x86_64\n\nkeywords:\n  - AppFlowy\n  - Office\n  - Document\n  - Database\n  - Note\n  - Kanban\n  - Note\n\ngeneric_name: AppFlowy\n\ncategories:\n  - Office\n  - Productivity\n\nstartup_notify: true\n\nsupportedMimeType: x-scheme-handler/appflowy-flutter\n\nrequires:\n  - libnotify\n  - keybinder\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/.gitignore",
    "content": "# Flutter-related\n**/Flutter/ephemeral/\n**/Pods/\n\n# Xcode-related\n**/xcuserdata/\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Flutter/Flutter-Debug.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"\n#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Flutter/Flutter-Release.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"\n#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Podfile",
    "content": "MINIMUM_MACOSX_DEPLOYMENT_TARGET_SUPPORTED = '10.14'\n\nplatform :osx, MINIMUM_MACOSX_DEPLOYMENT_TARGET_SUPPORTED\n\n# CocoaPods analytics sends network stats synchronously affecting flutter build latency.\nENV['COCOAPODS_DISABLE_STATS'] = 'true'\n\nproject 'Runner', {\n  'Debug' => :debug,\n  'Profile' => :release,\n  'Release' => :release,\n}\n\ndef flutter_root\n  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)\n  unless File.exist?(generated_xcode_build_settings_path)\n    raise \"#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \\\"flutter pub get\\\" is executed first\"\n  end\n\n  File.foreach(generated_xcode_build_settings_path) do |line|\n    matches = line.match(/FLUTTER_ROOT\\=(.*)/)\n    return matches[1].strip if matches\n  end\n  raise \"FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \\\"flutter pub get\\\"\"\nend\n\nrequire File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)\n\nflutter_macos_podfile_setup\n\ndef build_specify_archs_only\n  if ENV.has_key?('BUILD_ACTIVE_ARCHS_ONLY')\n    xcodeproj_path = File.dirname(__FILE__) + '/Runner.xcodeproj'\n    project = Xcodeproj::Project.open(xcodeproj_path)\n    project.targets.each do |target|\n      if target.name == 'Runner'\n        target.build_configurations.each do |config|\n          config.build_settings['ONLY_ACTIVE_ARCH'] = ENV['BUILD_ACTIVE_ARCHS_ONLY']\n        end\n      end\n    end\n    project.save()\n  end\n\n  if ENV.has_key?('BUILD_ARCHS')\n    xcodeproj_path = File.dirname(__FILE__) + '/Runner.xcodeproj'\n    project = Xcodeproj::Project.open(xcodeproj_path)\n    project.targets.each do |target|\n      if target.name == 'Runner'\n        target.build_configurations.each do |config|\n          config.build_settings['ARCHS'] = ENV['BUILD_ARCHS']\n        end\n      end\n    end\n    project.save()\n  end\nend\n\nbuild_specify_archs_only()\n\ntarget 'Runner' do\n  use_frameworks!\n  use_modular_headers!\n\n  flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))\nend\n\npost_install do |installer|\n  installer.pods_project.targets.each do |target|\n    flutter_additional_macos_build_settings(target)\n  end\n\n  installer.aggregate_targets.each do |target|\n    target.xcconfigs.each do |variant, xcconfig|\n      xcconfig_path = target.client_root + target.xcconfig_relative_path(variant)\n      IO.write(xcconfig_path, IO.read(xcconfig_path).gsub(\"DT_TOOLCHAIN_DIR\", \"TOOLCHAIN_DIR\"))\n    end\n  end\n\n  installer.pods_project.targets.each do |target|\n    target.build_configurations.each do |config|\n      if config.base_configuration_reference.is_a? Xcodeproj::Project::Object::PBXFileReference\n        xcconfig_path = config.base_configuration_reference.real_path\n        IO.write(xcconfig_path, IO.read(xcconfig_path).gsub(\"DT_TOOLCHAIN_DIR\", \"TOOLCHAIN_DIR\"))\n      end\n      config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = MINIMUM_MACOSX_DEPLOYMENT_TARGET_SUPPORTED\n    end\n  end\nend\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Runner/AppDelegate.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\n@main\nclass AppDelegate: FlutterAppDelegate {\n  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {\n    return false\n  }\n\n  override func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {\n    if !flag {\n        for window in sender.windows {\n            window.makeKeyAndOrderFront(self)\n        }\n    }\n\n    return true\n  }\n\n  override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {\n    return true\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"40.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"2x\",\n      \"size\" : \"20x20\"\n    },\n    {\n      \"filename\" : \"60.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"3x\",\n      \"size\" : \"20x20\"\n    },\n    {\n      \"filename\" : \"29.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"1x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"filename\" : \"58.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"2x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"filename\" : \"87.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"3x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"filename\" : \"80.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"2x\",\n      \"size\" : \"40x40\"\n    },\n    {\n      \"filename\" : \"120.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"3x\",\n      \"size\" : \"40x40\"\n    },\n    {\n      \"filename\" : \"57.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"1x\",\n      \"size\" : \"57x57\"\n    },\n    {\n      \"filename\" : \"114.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"2x\",\n      \"size\" : \"57x57\"\n    },\n    {\n      \"filename\" : \"120.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"2x\",\n      \"size\" : \"60x60\"\n    },\n    {\n      \"filename\" : \"180.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"3x\",\n      \"size\" : \"60x60\"\n    },\n    {\n      \"filename\" : \"20.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"1x\",\n      \"size\" : \"20x20\"\n    },\n    {\n      \"filename\" : \"40.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"2x\",\n      \"size\" : \"20x20\"\n    },\n    {\n      \"filename\" : \"29.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"1x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"filename\" : \"58.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"2x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"filename\" : \"40.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"1x\",\n      \"size\" : \"40x40\"\n    },\n    {\n      \"filename\" : \"80.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"2x\",\n      \"size\" : \"40x40\"\n    },\n    {\n      \"filename\" : \"50.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"1x\",\n      \"size\" : \"50x50\"\n    },\n    {\n      \"filename\" : \"100.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"2x\",\n      \"size\" : \"50x50\"\n    },\n    {\n      \"filename\" : \"72.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"1x\",\n      \"size\" : \"72x72\"\n    },\n    {\n      \"filename\" : \"144.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"2x\",\n      \"size\" : \"72x72\"\n    },\n    {\n      \"filename\" : \"76.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"1x\",\n      \"size\" : \"76x76\"\n    },\n    {\n      \"filename\" : \"152.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"2x\",\n      \"size\" : \"76x76\"\n    },\n    {\n      \"filename\" : \"167.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"2x\",\n      \"size\" : \"83.5x83.5\"\n    },\n    {\n      \"filename\" : \"1024.png\",\n      \"idiom\" : \"ios-marketing\",\n      \"scale\" : \"1x\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"filename\" : \"16.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"16x16\"\n    },\n    {\n      \"filename\" : \"32.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"16x16\"\n    },\n    {\n      \"filename\" : \"32.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"32x32\"\n    },\n    {\n      \"filename\" : \"64.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"32x32\"\n    },\n    {\n      \"filename\" : \"128.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"128x128\"\n    },\n    {\n      \"filename\" : \"256.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"128x128\"\n    },\n    {\n      \"filename\" : \"256.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"256x256\"\n    },\n    {\n      \"filename\" : \"512.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"256x256\"\n    },\n    {\n      \"filename\" : \"512.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"512x512\"\n    },\n    {\n      \"filename\" : \"1024.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"512x512\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Runner/Base.lproj/MainMenu.xib",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion=\"14490.70\" targetRuntime=\"MacOSX.Cocoa\" propertyAccessControl=\"none\" useAutolayout=\"YES\" customObjectInstantitationMethod=\"direct\">\n    <dependencies>\n        <deployment identifier=\"macosx\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.CocoaPlugin\" version=\"14490.70\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <objects>\n        <customObject id=\"-2\" userLabel=\"File's Owner\" customClass=\"NSApplication\">\n            <connections>\n                <outlet property=\"delegate\" destination=\"Voe-Tx-rLC\" id=\"GzC-gU-4Uq\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"-1\" userLabel=\"First Responder\" customClass=\"FirstResponder\"/>\n        <customObject id=\"-3\" userLabel=\"Application\" customClass=\"NSObject\"/>\n        <customObject id=\"Voe-Tx-rLC\" customClass=\"AppDelegate\" customModule=\"Runner\" customModuleProvider=\"target\">\n            <connections>\n                <outlet property=\"applicationMenu\" destination=\"uQy-DD-JDr\" id=\"XBo-yE-nKs\"/>\n                <outlet property=\"mainFlutterWindow\" destination=\"QvC-M9-y7g\" id=\"gIp-Ho-8D9\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"YLy-65-1bz\" customClass=\"NSFontManager\"/>\n        <menu title=\"Main Menu\" systemMenu=\"main\" id=\"AYu-sK-qS6\">\n            <items>\n                <menuItem title=\"APP_NAME\" id=\"1Xt-HY-uBw\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"APP_NAME\" systemMenu=\"apple\" id=\"uQy-DD-JDr\">\n                        <items>\n                            <menuItem title=\"About APP_NAME\" id=\"5kV-Vb-QxS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"orderFrontStandardAboutPanel:\" target=\"-1\" id=\"Exp-CZ-Vem\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"VOq-y0-SEH\"/>\n                            <menuItem title=\"Preferences…\" keyEquivalent=\",\" id=\"BOF-NM-1cW\"/>\n                            <menuItem isSeparatorItem=\"YES\" id=\"wFC-TO-SCJ\"/>\n                            <menuItem title=\"Services\" id=\"NMo-om-nkz\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Services\" systemMenu=\"services\" id=\"hz9-B4-Xy5\"/>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"4je-JR-u6R\"/>\n                            <menuItem title=\"Hide APP_NAME\" keyEquivalent=\"h\" id=\"Olw-nP-bQN\">\n                                <connections>\n                                    <action selector=\"hide:\" target=\"-1\" id=\"PnN-Uc-m68\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Hide Others\" keyEquivalent=\"h\" id=\"Vdr-fp-XzO\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"hideOtherApplications:\" target=\"-1\" id=\"VT4-aY-XCT\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Show All\" id=\"Kd2-mp-pUS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"unhideAllApplications:\" target=\"-1\" id=\"Dhg-Le-xox\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"kCx-OE-vgT\"/>\n                            <menuItem title=\"Quit APP_NAME\" keyEquivalent=\"q\" id=\"4sb-4s-VLi\">\n                                <connections>\n                                    <action selector=\"terminate:\" target=\"-1\" id=\"Te7-pn-YzF\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Edit\" id=\"5QF-Oa-p0T\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Edit\" id=\"W48-6f-4Dl\">\n                        <items>\n                            <menuItem title=\"Undo\" keyEquivalent=\"z\" id=\"dRJ-4n-Yzg\">\n                                <connections>\n                                    <action selector=\"undo:\" target=\"-1\" id=\"M6e-cu-g7V\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Redo\" keyEquivalent=\"Z\" id=\"6dh-zS-Vam\">\n                                <connections>\n                                    <action selector=\"redo:\" target=\"-1\" id=\"oIA-Rs-6OD\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"WRV-NI-Exz\"/>\n                            <menuItem title=\"Cut\" keyEquivalent=\"x\" id=\"uRl-iY-unG\">\n                                <connections>\n                                    <action selector=\"cut:\" target=\"-1\" id=\"YJe-68-I9s\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Copy\" keyEquivalent=\"c\" id=\"x3v-GG-iWU\">\n                                <connections>\n                                    <action selector=\"copy:\" target=\"-1\" id=\"G1f-GL-Joy\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Paste\" keyEquivalent=\"v\" id=\"gVA-U4-sdL\">\n                                <connections>\n                                    <action selector=\"paste:\" target=\"-1\" id=\"UvS-8e-Qdg\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Paste and Match Style\" keyEquivalent=\"V\" id=\"WeT-3V-zwk\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"pasteAsPlainText:\" target=\"-1\" id=\"cEh-KX-wJQ\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Delete\" id=\"pa3-QI-u2k\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"delete:\" target=\"-1\" id=\"0Mk-Ml-PaM\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Select All\" keyEquivalent=\"a\" id=\"Ruw-6m-B2m\">\n                                <connections>\n                                    <action selector=\"selectAll:\" target=\"-1\" id=\"VNm-Mi-diN\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"uyl-h8-XO2\"/>\n                            <menuItem title=\"Find\" id=\"4EN-yA-p0u\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Find\" id=\"1b7-l0-nxx\">\n                                    <items>\n                                        <menuItem title=\"Find…\" tag=\"1\" keyEquivalent=\"f\" id=\"Xz5-n4-O0W\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"cD7-Qs-BN4\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find and Replace…\" tag=\"12\" keyEquivalent=\"f\" id=\"YEy-JH-Tfz\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"WD3-Gg-5AJ\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find Next\" tag=\"2\" keyEquivalent=\"g\" id=\"q09-fT-Sye\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"NDo-RZ-v9R\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find Previous\" tag=\"3\" keyEquivalent=\"G\" id=\"OwM-mh-QMV\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"HOh-sY-3ay\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Use Selection for Find\" tag=\"7\" keyEquivalent=\"e\" id=\"buJ-ug-pKt\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"U76-nv-p5D\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Jump to Selection\" keyEquivalent=\"j\" id=\"S0p-oC-mLd\">\n                                            <connections>\n                                                <action selector=\"centerSelectionInVisibleArea:\" target=\"-1\" id=\"IOG-6D-g5B\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Spelling and Grammar\" id=\"Dv1-io-Yv7\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Spelling\" id=\"3IN-sU-3Bg\">\n                                    <items>\n                                        <menuItem title=\"Show Spelling and Grammar\" keyEquivalent=\":\" id=\"HFo-cy-zxI\">\n                                            <connections>\n                                                <action selector=\"showGuessPanel:\" target=\"-1\" id=\"vFj-Ks-hy3\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Check Document Now\" keyEquivalent=\";\" id=\"hz2-CU-CR7\">\n                                            <connections>\n                                                <action selector=\"checkSpelling:\" target=\"-1\" id=\"fz7-VC-reM\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"bNw-od-mp5\"/>\n                                        <menuItem title=\"Check Spelling While Typing\" id=\"rbD-Rh-wIN\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleContinuousSpellChecking:\" target=\"-1\" id=\"7w6-Qz-0kB\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Check Grammar With Spelling\" id=\"mK6-2p-4JG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleGrammarChecking:\" target=\"-1\" id=\"muD-Qn-j4w\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Correct Spelling Automatically\" id=\"78Y-hA-62v\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticSpellingCorrection:\" target=\"-1\" id=\"2lM-Qi-WAP\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Substitutions\" id=\"9ic-FL-obx\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Substitutions\" id=\"FeM-D8-WVr\">\n                                    <items>\n                                        <menuItem title=\"Show Substitutions\" id=\"z6F-FW-3nz\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"orderFrontSubstitutionsPanel:\" target=\"-1\" id=\"oku-mr-iSq\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"gPx-C9-uUO\"/>\n                                        <menuItem title=\"Smart Copy/Paste\" id=\"9yt-4B-nSM\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleSmartInsertDelete:\" target=\"-1\" id=\"3IJ-Se-DZD\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Quotes\" id=\"hQb-2v-fYv\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticQuoteSubstitution:\" target=\"-1\" id=\"ptq-xd-QOA\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Dashes\" id=\"rgM-f4-ycn\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticDashSubstitution:\" target=\"-1\" id=\"oCt-pO-9gS\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Links\" id=\"cwL-P1-jid\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticLinkDetection:\" target=\"-1\" id=\"Gip-E3-Fov\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Data Detectors\" id=\"tRr-pd-1PS\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticDataDetection:\" target=\"-1\" id=\"R1I-Nq-Kbl\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Text Replacement\" id=\"HFQ-gK-NFA\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticTextReplacement:\" target=\"-1\" id=\"DvP-Fe-Py6\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Transformations\" id=\"2oI-Rn-ZJC\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Transformations\" id=\"c8a-y6-VQd\">\n                                    <items>\n                                        <menuItem title=\"Make Upper Case\" id=\"vmV-6d-7jI\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"uppercaseWord:\" target=\"-1\" id=\"sPh-Tk-edu\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Make Lower Case\" id=\"d9M-CD-aMd\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"lowercaseWord:\" target=\"-1\" id=\"iUZ-b5-hil\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Capitalize\" id=\"UEZ-Bs-lqG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"capitalizeWord:\" target=\"-1\" id=\"26H-TL-nsh\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Speech\" id=\"xrE-MZ-jX0\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Speech\" id=\"3rS-ZA-NoH\">\n                                    <items>\n                                        <menuItem title=\"Start Speaking\" id=\"Ynk-f8-cLZ\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"startSpeaking:\" target=\"-1\" id=\"654-Ng-kyl\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Stop Speaking\" id=\"Oyz-dy-DGm\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"stopSpeaking:\" target=\"-1\" id=\"dX8-6p-jy9\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"View\" id=\"H8h-7b-M4v\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"View\" id=\"HyV-fh-RgO\">\n                        <items>\n                            <menuItem title=\"Enter Full Screen\" keyEquivalent=\"f\" id=\"4J7-dP-txa\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" control=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"toggleFullScreen:\" target=\"-1\" id=\"dU3-MA-1Rq\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Window\" id=\"aUF-d1-5bR\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Window\" systemMenu=\"window\" id=\"Td7-aD-5lo\">\n                        <items>\n                            <menuItem title=\"Minimize\" keyEquivalent=\"m\" id=\"OY7-WF-poV\">\n                                <connections>\n                                    <action selector=\"performMiniaturize:\" target=\"-1\" id=\"VwT-WD-YPe\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Zoom\" id=\"R4o-n2-Eq4\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"performZoom:\" target=\"-1\" id=\"DIl-cC-cCs\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"eu3-7i-yIM\"/>\n                            <menuItem title=\"Bring All to Front\" id=\"LE2-aR-0XJ\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"arrangeInFront:\" target=\"-1\" id=\"DRN-fu-gQh\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n            </items>\n            <point key=\"canvasLocation\" x=\"142\" y=\"-258\"/>\n        </menu>\n        <window title=\"APP_NAME\" allowsToolTipsWhenApplicationIsInactive=\"NO\" autorecalculatesKeyViewLoop=\"NO\" releasedWhenClosed=\"NO\" animationBehavior=\"default\" id=\"QvC-M9-y7g\" customClass=\"MainFlutterWindow\" customModule=\"Runner\" customModuleProvider=\"target\">\n            <windowStyleMask key=\"styleMask\" titled=\"YES\" closable=\"YES\" miniaturizable=\"YES\" resizable=\"YES\"/>\n            <rect key=\"contentRect\" x=\"335\" y=\"390\" width=\"800\" height=\"600\"/>\n            <rect key=\"screenRect\" x=\"0.0\" y=\"0.0\" width=\"2560\" height=\"1577\"/>\n            <view key=\"contentView\" wantsLayer=\"YES\" id=\"EiT-Mj-1SZ\">\n                <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"800\" height=\"600\"/>\n                <autoresizingMask key=\"autoresizingMask\"/>\n            </view>\n        </window>\n    </objects>\n</document>\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig",
    "content": "// Application-level settings for the Runner target.\n//\n// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the\n// future. If not, the values below would default to using the project name when this becomes a\n// 'flutter create' template.\n\n// The application's name. By default this is also the title of the Flutter window.\nPRODUCT_NAME = AppFlowy\n\n// The application's bundle identifier\nPRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy\n\n// The copyright displayed in application information\nPRODUCT_COPYRIGHT = Copyright © 2025 AppFlowy.IO. All rights reserved.\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Runner/Configs/Debug.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Debug.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Runner/Configs/Release.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Release.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Runner/Configs/Warnings.xcconfig",
    "content": "WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings\nGCC_WARN_UNDECLARED_SELECTOR = YES\nCLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES\nCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE\nCLANG_WARN__DUPLICATE_METHOD_MATCH = YES\nCLANG_WARN_PRAGMA_PACK = YES\nCLANG_WARN_STRICT_PROTOTYPES = YES\nCLANG_WARN_COMMA = YES\nGCC_WARN_STRICT_SELECTOR_MATCH = YES\nCLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES\nCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES\nGCC_WARN_SHADOW = YES\nCLANG_WARN_UNREACHABLE_CODE = YES\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Runner/DebugProfile.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n\t<dict>\n\t\t<key>com.apple.security.app-sandbox</key>\n\t\t<false />\n\t\t<key>com.apple.security.files.downloads.read-write</key>\n\t\t<true />\n\t\t<key>com.apple.security.files.user-selected.read-write</key>\n\t\t<true />\n\t\t<key>com.apple.security.network.client</key>\n\t\t<true />\n\t\t<key>com.apple.security.network.server</key>\n\t\t<true />\n\t\t<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>\n\t\t<array>\n\t\t\t<string>/</string>\n\t\t</array>\n\t</dict>\n</plist>"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n\t<dict>\n\t\t<key>LSApplicationCategoryType</key>\n\t\t<string>public.app-category.productivity</string>\n\t\t<key>CFBundleDevelopmentRegion</key>\n\t\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t\t<key>CFBundleExecutable</key>\n\t\t<string>$(EXECUTABLE_NAME)</string>\n\t\t<key>CFBundleIconFile</key>\n\t\t<string></string>\n\t\t<key>CFBundleIdentifier</key>\n\t\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t\t<key>CFBundleInfoDictionaryVersion</key>\n\t\t<string>6.0</string>\n\t\t<key>CFBundleLocalizations</key>\n\t\t<array>\n\t\t\t<string>en</string>\n\t\t\t<string>fr</string>\n\t\t\t<string>it</string>\n\t\t\t<string>zh</string>\n\t\t</array>\n\t\t<key>CFBundleName</key>\n\t\t<string>$(PRODUCT_NAME)</string>\n\t\t<key>CFBundlePackageType</key>\n\t\t<string>APPL</string>\n\t\t<key>CFBundleShortVersionString</key>\n\t\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t\t<key>CFBundleURLTypes</key>\n\t\t<array>\n\t\t\t<dict>\n\t\t\t\t<key>CFBundleURLName</key>\n\t\t\t\t<string></string>\n\t\t\t\t<key>CFBundleURLSchemes</key>\n\t\t\t\t<array>\n\t\t\t\t\t<string>appflowy-flutter</string>\n\t\t\t\t</array>\n\t\t\t</dict>\n\t\t</array>\n\t\t<key>CFBundleVersion</key>\n\t\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t\t<key>LSMinimumSystemVersion</key>\n\t\t<string>$(MACOSX_DEPLOYMENT_TARGET)</string>\n\t\t<key>NSAppTransportSecurity</key>\n\t\t<dict>\n\t\t\t<key>NSAllowsArbitraryLoads</key>\n\t\t\t<true />\n\t\t</dict>\n\t\t<key>NSHumanReadableCopyright</key>\n\t\t<string>$(PRODUCT_COPYRIGHT)</string>\n\t\t<key>NSMainNibFile</key>\n\t\t<string>MainMenu</string>\n\t\t<key>NSPrincipalClass</key>\n\t\t<string>NSApplication</string>\n\t\t<key>SUPublicEDKey</key>\n\t\t<string>Bs++IOmOwYmNTjMMC2jMqLNldP+mndDp/LwujCg2/kw=</string>\n\t\t<key>SUAllowsAutomaticUpdates</key>\n\t\t<false />\n\t</dict>\n</plist>"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\nprivate let kTrafficLightOffetTop = 14\n\nclass MainFlutterWindow: NSWindow {\n  func registerMethodChannel(flutterViewController: FlutterViewController) {\n    let cocoaWindowChannel = FlutterMethodChannel(name: \"flutter/cocoaWindow\", binaryMessenger: flutterViewController.engine.binaryMessenger)\n    cocoaWindowChannel.setMethodCallHandler({\n      (call: FlutterMethodCall, result: FlutterResult) -> Void in\n      if call.method == \"setWindowPosition\" {\n        guard let position = call.arguments as? NSArray else {\n          result(nil)\n          return\n        }\n        let nX = position[0] as! NSNumber\n        let nY = position[1] as! NSNumber\n        let x = nX.doubleValue\n        let y = nY.doubleValue\n\n        self.setFrameOrigin(NSPoint(x: x, y: y))\n        result(nil)\n        return\n      } else if call.method == \"getWindowPosition\" {\n        let frame = self.frame\n        result([frame.origin.x, frame.origin.y])\n        return\n      } else if call.method == \"zoom\" {\n        self.zoom(self)\n        result(nil)\n        return\n      }\n\n      result(FlutterMethodNotImplemented)\n    })\n  }\n\n  func layoutTrafficLightButton(titlebarView: NSView, button: NSButton, offsetTop: CGFloat, offsetLeft: CGFloat) {\n    button.translatesAutoresizingMaskIntoConstraints = false;\n    titlebarView.addConstraint(NSLayoutConstraint.init(\n      item: button,\n      attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: titlebarView, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: offsetTop))\n    titlebarView.addConstraint(NSLayoutConstraint.init(\n      item: button,\n      attribute: NSLayoutConstraint.Attribute.left, relatedBy: NSLayoutConstraint.Relation.equal, toItem: titlebarView, attribute: NSLayoutConstraint.Attribute.left, multiplier: 1, constant: offsetLeft))\n  }\n\n  func layoutTrafficLights() {\n    let closeButton = self.standardWindowButton(ButtonType.closeButton)!\n    let minButton = self.standardWindowButton(ButtonType.miniaturizeButton)!\n    let zoomButton = self.standardWindowButton(ButtonType.zoomButton)!\n    let titlebarView = closeButton.superview!\n\n    self.layoutTrafficLightButton(titlebarView: titlebarView, button: closeButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 12)\n    self.layoutTrafficLightButton(titlebarView: titlebarView, button: minButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 30)\n    self.layoutTrafficLightButton(titlebarView: titlebarView, button: zoomButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 48)\n\n    let customToolbar = NSTitlebarAccessoryViewController()\n    let newView = NSView()\n    newView.frame = NSRect(origin: CGPoint(), size: CGSize(width: 0, height: 40))  // only the height is cared\n    customToolbar.view = newView\n    self.addTitlebarAccessoryViewController(customToolbar)\n  }\n\n  override func awakeFromNib() {\n    let flutterViewController = FlutterViewController.init()\n    let windowFrame = self.frame\n    self.contentViewController = flutterViewController\n\n    self.registerMethodChannel(flutterViewController: flutterViewController)\n\n    self.setFrame(windowFrame, display: true)\n    self.titlebarAppearsTransparent = true\n    self.titleVisibility = .hidden\n    self.styleMask.insert(StyleMask.fullSizeContentView)\n    self.isMovableByWindowBackground = true\n\n    // For the macOS version 15 or higher, set it to true to enable the window tiling\n    if #available(macOS 15.0, *) {\n      self.isMovable = true\n    } else {\n      self.isMovable = false\n    }\n\n    self.layoutTrafficLights()\n\n    RegisterGeneratedPlugins(registry: flutterViewController)\n\n    super.awakeFromNib()\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Runner/Release.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n\t<dict>\n\t\t<key>com.apple.security.app-sandbox</key>\n\t\t<false />\n\t\t<key>com.apple.security.files.downloads.read-write</key>\n\t\t<true />\n\t\t<key>com.apple.security.files.user-selected.read-write</key>\n\t\t<true />\n\t\t<key>com.apple.security.network.client</key>\n\t\t<true />\n\t\t<key>com.apple.security.network.server</key>\n\t\t<true />\n\t\t<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>\n\t\t<array>\n\t\t\t<string>/</string>\n\t\t</array>\n\t</dict>\n</plist>"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXAggregateTarget section */\n\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {\n\t\t\tisa = PBXAggregateTarget;\n\t\t\tbuildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t33CC111E2044C6BF0003C045 /* ShellScript */,\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = \"Flutter Assemble\";\n\t\t\tproductName = FLX;\n\t\t};\n/* End PBXAggregateTarget section */\n\n/* Begin PBXBuildFile section */\n\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };\n\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };\n\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };\n\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };\n\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };\n\t\t706E045829F286F600B789F4 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 706E045729F286EC00B789F4 /* libc++.tbd */; };\n\t\tD7360C6D6177708F7B2D3C9D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CD81A6C7244B2318E0BA2E8 /* Pods_Runner.framework */; };\n\t\tFB4E0E4F2CC9F3F900C57E87 /* libbz2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = FB4E0E4E2CC9F3E900C57E87 /* libbz2.tbd */; };\n\t\tFB54062C2D22665000223D60 /* liblzma.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = FB54062B2D22664200223D60 /* liblzma.tbd */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 33CC10E52044A3C60003C045 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 33CC111A2044C6BA0003C045;\n\t\t\tremoteInfo = FLX;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t33CC110E2044A8840003C045 /* Bundle Framework */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Bundle Framework\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t1823EB6E74189944EAA69652 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.profile.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t1CD81A6C7244B2318E0BA2E8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = \"<group>\"; };\n\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = \"<group>\"; };\n\t\t33CC10ED2044A3C60003C045 /* AppFlowy.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AppFlowy.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = \"<group>\"; };\n\t\t33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = \"<group>\"; };\n\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = \"<group>\"; };\n\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = \"Flutter-Generated.xcconfig\"; path = \"ephemeral/Flutter-Generated.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = \"<group>\"; };\n\t\t33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = \"<group>\"; };\n\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = \"<group>\"; };\n\t\t6DEEC7DEFA746DDF1338FF4D /* 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 = \"<group>\"; };\n\t\t706E045729F286EC00B789F4 /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = \"sourcecode.text-based-dylib-definition\"; name = \"libc++.tbd\"; path = \"usr/lib/libc++.tbd\"; sourceTree = SDKROOT; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t7D41C30A3910C3A40B6085E3 /* 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 = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\tFB4E0E4E2CC9F3E900C57E87 /* libbz2.tbd */ = {isa = PBXFileReference; lastKnownFileType = \"sourcecode.text-based-dylib-definition\"; name = libbz2.tbd; path = usr/lib/libbz2.tbd; sourceTree = SDKROOT; };\n\t\tFB54062B2D22664200223D60 /* liblzma.tbd */ = {isa = PBXFileReference; lastKnownFileType = \"sourcecode.text-based-dylib-definition\"; name = liblzma.tbd; path = usr/lib/liblzma.tbd; sourceTree = SDKROOT; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t33CC10EA2044A3C60003C045 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tFB4E0E4F2CC9F3F900C57E87 /* libbz2.tbd in Frameworks */,\n\t\t\t\t706E045829F286F600B789F4 /* libc++.tbd in Frameworks */,\n\t\t\t\tFB54062C2D22665000223D60 /* liblzma.tbd in Frameworks */,\n\t\t\t\tD7360C6D6177708F7B2D3C9D /* Pods_Runner.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t33BA886A226E78AF003329D5 /* Configs */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */,\n\t\t\t);\n\t\t\tpath = Configs;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10E42044A3C60003C045 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33FAB671232836740065AC1E /* Runner */,\n\t\t\t\t33CEB47122A05771004F2AC0 /* Flutter */,\n\t\t\t\t33CC10EE2044A3C60003C045 /* Products */,\n\t\t\t\tD73912EC22F37F3D000D13A0 /* Frameworks */,\n\t\t\t\t6B44C542FA0845A8F2FA3624 /* Pods */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10EE2044A3C60003C045 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10ED2044A3C60003C045 /* AppFlowy.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC11242044D66E0003C045 /* Resources */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */,\n\t\t\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */,\n\t\t\t\t33CC10F72044A3C60003C045 /* Info.plist */,\n\t\t\t);\n\t\t\tname = Resources;\n\t\t\tpath = ..;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CEB47122A05771004F2AC0 /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,\n\t\t\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,\n\t\t\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,\n\t\t\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,\n\t\t\t);\n\t\t\tpath = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33FAB671232836740065AC1E /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */,\n\t\t\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,\n\t\t\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */,\n\t\t\t\t33E51914231749380026EE4D /* Release.entitlements */,\n\t\t\t\t33CC11242044D66E0003C045 /* Resources */,\n\t\t\t\t33BA886A226E78AF003329D5 /* Configs */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t6B44C542FA0845A8F2FA3624 /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t6DEEC7DEFA746DDF1338FF4D /* Pods-Runner.debug.xcconfig */,\n\t\t\t\t7D41C30A3910C3A40B6085E3 /* Pods-Runner.release.xcconfig */,\n\t\t\t\t1823EB6E74189944EAA69652 /* Pods-Runner.profile.xcconfig */,\n\t\t\t);\n\t\t\tpath = Pods;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD73912EC22F37F3D000D13A0 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tFB54062B2D22664200223D60 /* liblzma.tbd */,\n\t\t\t\tFB4E0E4E2CC9F3E900C57E87 /* libbz2.tbd */,\n\t\t\t\t706E045729F286EC00B789F4 /* libc++.tbd */,\n\t\t\t\t1CD81A6C7244B2318E0BA2E8 /* Pods_Runner.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t33CC10EC2044A3C60003C045 /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t611CF908D5E75C6DF581F81A /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t33CC10E92044A3C60003C045 /* Sources */,\n\t\t\t\t33CC10EA2044A3C60003C045 /* Frameworks */,\n\t\t\t\t33CC10EB2044A3C60003C045 /* Resources */,\n\t\t\t\t33CC110E2044A8840003C045 /* Bundle Framework */,\n\t\t\t\t3399D490228B24CF009A79C7 /* ShellScript */,\n\t\t\t\t4372B34726148A3BC297850A /* [CP] Embed Pods Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 33CC10ED2044A3C60003C045 /* AppFlowy.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t33CC10E52044A3C60003C045 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastSwiftUpdateCheck = 0920;\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t33CC10EC2044A3C60003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t\tProvisioningStyle = Automatic;\n\t\t\t\t\t\tSystemCapabilities = {\n\t\t\t\t\t\t\tcom.apple.Sandbox = {\n\t\t\t\t\t\t\t\tenabled = 1;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t\t33CC111A2044C6BA0003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tProvisioningStyle = Manual;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 33CC10E42044A3C60003C045;\n\t\t\tproductRefGroup = 33CC10EE2044A3C60003C045 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t33CC10EC2044A3C60003C045 /* Runner */,\n\t\t\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t33CC10EB2044A3C60003C045 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,\n\t\t\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t3399D490228B24CF009A79C7 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"echo \\\"$PRODUCT_NAME.app\\\" > \\\"$PROJECT_DIR\\\"/Flutter/ephemeral/.app_filename && \\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh embed\\n\";\n\t\t};\n\t\t33CC111E2044C6BF0003C045 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterInputs.xcfilelist,\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\tFlutter/ephemeral/tripwire,\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterOutputs.xcfilelist,\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire\";\n\t\t};\n\t\t4372B34726148A3BC297850A /* [CP] Embed Pods Frameworks */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist\",\n\t\t\t);\n\t\t\tname = \"[CP] Embed Pods Frameworks\";\n\t\t\toutputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t611CF908D5E75C6DF581F81A /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t33CC10E92044A3C60003C045 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,\n\t\t\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,\n\t\t\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;\n\t\t\ttargetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F52044A3C60003C045 /* Base */,\n\t\t\t);\n\t\t\tname = MainMenu.xib;\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t338D0CE9231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSTRIP_STYLE = \"non-global\";\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEA231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tARCHS = \"$(ARCHS_STANDARD)\";\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tENABLE_HARDENED_RUNTIME = NO;\n\t\t\t\tEXCLUDED_ARCHS = \"\";\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = AppFlowy;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.appflowy.appflowy.flutter;\n\t\t\t\tPRODUCT_NAME = AppFlowy;\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSTRIP_STYLE = \"non-global\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEB231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t33CC10F92044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSTRIP_STYLE = \"non-global\";\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FA2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSTRIP_STYLE = \"non-global\";\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC10FC2044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tARCHS = \"$(ARCHS_STANDARD)\";\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tENABLE_HARDENED_RUNTIME = NO;\n\t\t\t\tEXCLUDED_ARCHS = \"\";\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = AppFlowy;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.appflowy.appflowy.flutter;\n\t\t\t\tPRODUCT_NAME = AppFlowy;\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSTRIP_STYLE = \"non-global\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FD2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tARCHS = \"$(ARCHS_STANDARD)\";\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tENABLE_HARDENED_RUNTIME = YES;\n\t\t\t\tEXCLUDED_ARCHS = \"\";\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = AppFlowy;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.appflowy.appflowy.flutter;\n\t\t\t\tPRODUCT_NAME = AppFlowy;\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSTRIP_STYLE = \"non-global\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC111C2044C6BA0003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC111D2044C6BA0003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10F92044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FA2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CE9231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10FC2044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FD2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CEA231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC111C2044C6BA0003C045 /* Debug */,\n\t\t\t\t33CC111D2044C6BA0003C045 /* Release */,\n\t\t\t\t338D0CEB231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 33CC10E52044A3C60003C045 /* Project object */;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1510\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n               BuildableName = \"AppFlowy.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"AppFlowy.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <Testables>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"AppFlowy.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"AppFlowy.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n   <FileRef\n      location = \"group:Pods/Pods.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/macos/build/ios/XCBuildData/PIFCache/project/PROJECT@v11_mod=a7fbf46937053896f73cc7c7ec6baefb_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1-json",
    "content": "{\"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\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e983499ae993d0615bc4a25c6d23d299cd2\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/app_links-6.3.3/ios/Classes/AppLinksPlugin.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9878bdb70529628b051bfb14170b6ce281\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/app_links-6.3.3/ios/Classes/SwiftAppLinksPlugin.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98803a16064d6b26357b9fa2d9c81eb2c0\",\"name\":\"Classes\",\"path\":\"Classes\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e982bcf9ac9f04ccd93893e9ae54d152c11\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/app_links-6.3.3/ios/Resources/PrivacyInfo.xcprivacy\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980af336cd97bd48a5f76247c45af3ca5a\",\"name\":\"Resources\",\"path\":\"Resources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d4c688735e4b7c4c9af25f0254256c5a\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980f47fa79356e5b78e85637df4eab1e8f\",\"name\":\"app_links\",\"path\":\"app_links\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9852951db13a823ccb98a790f496fab4e3\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988786e23fb077ded3affc0f7a37fc275c\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9874ef26f64fe74d7c02d1a94bcde1184c\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989187ad07f57154eca7d638acb8377adb\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98dc7eee9f7cdf2e623ebc316ac5388a1e\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982c3c6bda68e418bfa0a7d818fbfb0037\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a15f02f386a528bc8eb605ae37642455\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98cbbda7998cd576c276e2258fb3bad5a5\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982b881f7fdcc02480bc57ddb51bd8d98f\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98aa491e8e0f7d7e2c84ecbe06c2f4df77\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ae796e67846c10ae19147ab24013260d\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ee7362a7cb62b913a6f351fa670031fb\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c70d2ff23f2582de73eade3b63ca6732\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98cfdf5bb4bb8df40c98fd16d64963a3be\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/app_links-6.3.3/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b1016bc412809827cc7f8ffd7be68d60\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/app_links-6.3.3/LICENSE\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988f23ea9e443884f6d6a834c279bb42c4\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e989d5bbbb2531f246d5e384ddd39c9ef94\",\"path\":\"app_links.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e985bbf5bd8a81b79ac8b94165aebec6742\",\"path\":\"app_links-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a0510b51afa43c66fac3cfa1b303b58d\",\"path\":\"app_links-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e988921fa59229e94f886b598374e0a0a6c\",\"path\":\"app_links-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e988a8ca854307b43eb2b99c9daa9a1cc96\",\"path\":\"app_links-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e986a31ee17a0bf46ce08a50fbab9d7e0d7\",\"path\":\"app_links.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e981b9d938e8177ba8be7381344ebbf3f72\",\"path\":\"app_links.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98faf0d598eef28587701cbceba0a82824\",\"path\":\"ResourceBundle-app_links_ios_privacy-app_links-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9834af3cd6a85915e92c7086a5cef194bd\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/app_links\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e985fe1a1ae80c7c29a049e0b0f5a968eb6\",\"name\":\"app_links\",\"path\":\"../.symlinks/plugins/app_links/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d2eb541b9cb3760f4d8a0de1aceac0cd\",\"path\":\"../../../../../../packages/appflowy_backend/ios/Classes/AppFlowyBackendPlugin.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ff9d3e6fbf7f3ebdf3b2d98a4d52480e\",\"path\":\"../../../../../../packages/appflowy_backend/ios/Classes/AppFlowyBackendPlugin.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98082f3bd8728bb955d3b231cc205df22a\",\"path\":\"../../../../../../packages/appflowy_backend/ios/Classes/binding.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e981cf16cdf61f891cb9befc3392b13ff5f\",\"name\":\"Classes\",\"path\":\"Classes\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980ba42de9b682ddbb7d9ecb3a91add63c\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ce8a30ee373383f69298444d24489306\",\"name\":\"appflowy_backend\",\"path\":\"appflowy_backend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c86a47e2309e0b005ed79419dd093f0d\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98503452eddbee5272ed788d0cc749960e\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98055cce552f828e105a78ac03f0bdd805\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ca2c764817f57c4676d84d72558b3899\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f5501b2ed32cd958d3563e60f001d703\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b9c0603d95a0886bda3e008ea3e3501a\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9877a3c2776a0dd48b2c60d087afb651a0\",\"name\":\"..\",\"path\":\"../../../../../packages/appflowy_backend/ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"archive.ar\",\"guid\":\"bfdfe7dc352907fc980b868725387e9836c8d2e47d81f8a6899c11848d60e876\",\"path\":\"../../../../../packages/appflowy_backend/ios/libdart_ffi.a\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d5592f7a99092768eeefc4cfc096cae8\",\"name\":\"Frameworks\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text.script.ruby\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c2aceac4d9f7a88a7f2aef9ddb559c68\",\"path\":\"../../../../../packages/appflowy_backend/ios/appflowy_backend.podspec\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e985e17269d373b81baa61bd43ba6a542e5\",\"path\":\"../../../../../packages/appflowy_backend/LICENSE\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9850866a0df8df8e8e5c4a2fae74fa1e1f\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e87c56b9467409c44163e34bc882ac5d\",\"path\":\"appflowy_backend.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e158e9b03f83f13bbdf8668ff066134b\",\"path\":\"appflowy_backend-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98eaf420deb60cd2d5a76ad867f2138dae\",\"path\":\"appflowy_backend-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9805ed6ca977fdc6df3bcf5eed84819eb9\",\"path\":\"appflowy_backend-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e983bb1fa0b0f13951c6fa653a914b3fa2c\",\"path\":\"appflowy_backend-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e986cd74f83369370fbe7e403ef8053e619\",\"path\":\"appflowy_backend.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98403ef0361dbe02708d9acfefd0464e16\",\"path\":\"appflowy_backend.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a1e14bed5ed1f1fb81e768bd14f444fe\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/appflowy_backend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9835d1d3e8ff3e04e32db8f29bce41a67d\",\"name\":\"appflowy_backend\",\"path\":\"../.symlinks/plugins/appflowy_backend/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e985824c2dcc3b51cb92cca4c0ee6b76711\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/ios/Classes/ConnectivityPlusPlugin.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e980ce7873e1e41d09d3e0e844e23814078\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/ios/Classes/ConnectivityProvider.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98880b04837ff2e7c369e7e0eb127f9146\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/ios/Classes/PathMonitorConnectivityProvider.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e986c4b5d0d184e7bdaae9f3f567209b6a8\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/ios/Classes/ReachabilityConnectivityProvider.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e982d898374f337072929ea1137cd9b0531\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/ios/Classes/SwiftConnectivityPlusPlugin.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a0e254b7ac4017377847f08d93859c98\",\"name\":\"Classes\",\"path\":\"Classes\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98403bc21ffba3f9ff7918b7c9d3ed9571\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f8845f64df2ea83ab8319d64b1bbadde\",\"name\":\"connectivity_plus\",\"path\":\"connectivity_plus\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989eb115a9dec07b1f45a9c5d5afae3331\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9811c71b5edc42403c378790dde71cd61e\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988ba6e533cf0750afe861cf587836d63c\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98337f70e0b3205d77252416fe9d53ade5\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d3ba4a1d262c8736c1a3dd2bbee95feb\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98935b95f888c988c238c3ba19998f568c\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9815214da764f00e2aace6e3402b97ff27\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c4748e70a6f1ff450d8ad89293faf1ad\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987ddf25614c5494e3e2e964d3362a7e13\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98652b1ddb86551418439feb3a3cde66c8\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98413da6f3e8f068df77eefa2354cc1260\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b18d54474e5f2acd3f7e93b85008ecaa\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ff9bc5085e73af685f00aee655d65cbd\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ec4569066db3377ae3957a0893989f6a\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e982050d0252ea66aa742807a457832653a\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/LICENSE\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986076caee38a9da764b3dfa1e4e1a5d41\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e986c1079ccb9ad2a0dd836bbde26dfebf0\",\"path\":\"connectivity_plus.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b6ddb885fad9a4542a6329a470fa2fe4\",\"path\":\"connectivity_plus-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e592f2c2e44ef3deac3cd02784cc784d\",\"path\":\"connectivity_plus-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d207fb51488c7ea7ac1275e911a94150\",\"path\":\"connectivity_plus-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9898468587a6cab520cdcb8933d0b368ca\",\"path\":\"connectivity_plus-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e9818c5888400978b2fa22d952059454061\",\"path\":\"connectivity_plus.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d82fe1b7b4db7bc85ab18441f1e2c0ce\",\"path\":\"connectivity_plus.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984b570fd876e00e3c1622ee7a13633dbe\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/connectivity_plus\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a5f07478f75bc3b752964b6b533c114d\",\"name\":\"connectivity_plus\",\"path\":\"../.symlinks/plugins/connectivity_plus/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9830307b7eb9685258ae248d413d28a51f\",\"name\":\"Classes\",\"path\":\"Classes\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b1c115a5b698e82de643427263904f6e\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b72e875a2313b2fd273ee6413267e0b7\",\"name\":\"device_info_plus\",\"path\":\"device_info_plus\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a016968d1537de5fee216f39b9b13a4b\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984163ac08562702d9111e13bc58eadee3\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9863be3299f9535e1f37edeca566c56956\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ec71f83543e78b02bb8878f2bdaa1770\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e983b57caee95bb0defd7fbea09e4630e95\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9866f39d80aa059eb566807578dd56d3e0\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d5774272732a1c387c19dcb20953a259\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9884b715eaf409b020791c09bd1a45ac40\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989064cf80f6cbf753a3f2f76769676869\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9853d1bd10368491358c120edf0879a1c1\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98cd41f321f927f1501da3ce4c3e1705d3\",\"name\":\"device_info_plus\",\"path\":\"device_info_plus\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c8a696a7575bdfd695b50552bda53e63\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986fad17d5ebb6a1b1e363579811099dac\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9896b7c1d7f56d6b335d559dde146c1187\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b7caa4a8157f551fd5157e38236212ff\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98eb0be9618fdb3c31fc76e6c391bb5a54\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d79e90f30e8071ea0755a235e7bbe47a\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f7e502581e9e563f0e716961fe919778\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ceb019bbffa37850d4328a3b5e80e961\",\"name\":\"dev\",\"path\":\"../dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d1e6ecf8a8f5e7866d4a3bf6a028da56\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9803607fd62a02c7946a1d3477b61e1422\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984cfdc50cf761e86a346d06e7bbf2d3a1\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e53458045fdd4d428512566882c92e7e\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a82b79305f187eb48310a393f4bfe813\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/device_info_plus-10.1.2/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e983862a4e9dba3759d8615b149cba8a0dc\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/device_info_plus-10.1.2/LICENSE\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982a7f5deb6a64ae5e5a8c21df8be555e2\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e9800ef6f27822088506da77df111adaf81\",\"path\":\"device_info_plus.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d3729b6804108439237d0ad0b05ddce2\",\"path\":\"device_info_plus-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e984decbb29f8ccfc95e475df5348ee3959\",\"path\":\"device_info_plus-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98051abdaf3129a419e9bb3b8c7a83e64b\",\"path\":\"device_info_plus-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e913211adfbc9d70bfc43b4439f4332c\",\"path\":\"device_info_plus-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e984bb4748a26339d307020f1110f05e895\",\"path\":\"device_info_plus.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e981f4d52f92d8b0f360ee8bef71882f85a\",\"path\":\"device_info_plus.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f354426d7c8f682caf04a755240d2fde\",\"path\":\"ResourceBundle-device_info_plus_privacy-device_info_plus-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98623cfe9c4e66d55a221902e7792e5d43\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/device_info_plus\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ba853655f8a54c79510e0b66012fac89\",\"name\":\"device_info_plus\",\"path\":\"../.symlinks/plugins/device_info_plus/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e16f08255892d0f684af6ee4b4380824\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/Classes/FileInfo.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f56ed42bf21b1a52ac8fcb9a82336a65\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/Classes/FilePickerPlugin.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e988e93662f755da0279387c09ed20fcb67\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/Classes/FilePickerPlugin.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e982542f63e48192694c2301b53dd8da392\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/Classes/FileUtils.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e987347d2eccffbba5b92a6737ac7c7f11f\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/Classes/FileUtils.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e984b1a288da1e6fe981b46f8315944e21d\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/Classes/ImageUtils.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e04840a4cc59ee085ade0b92a852884e\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/Classes/ImageUtils.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ed38e9ec1e8cd7f7427a8e3752fa5961\",\"name\":\"Classes\",\"path\":\"Classes\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e9830eb81718ca8a4637d18322144cb97c2\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/Resources/PrivacyInfo.xcprivacy\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980cdda343b1e3945e28451531e9c1a605\",\"name\":\"Resources\",\"path\":\"Resources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9845180a5dc5686acdc4e9429fa972713e\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9836c9feed71b513f9919701e702f5ef43\",\"name\":\"file_picker\",\"path\":\"file_picker\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e5fd1b4108172e277e33c8a87d3fcd70\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9880d01b963c8ef608a0ad04583a5fc312\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9805619a0f02043e6fdb7a2be76189cfff\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9849b67c3d88226e7c72c75fa15e3bbdc0\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e1d9d867fa533872b45a7339a773d9b3\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b0187227de80dc084fabcf302004870b\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9825d0e9fb8adf9bdf05bf7db5c5fc456d\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9800d40be9558dad2dacf5f93047196827\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98abd6b198ade79f4872ee96e030a87e36\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9862b34fd8ad582ae69aae1fce3d396520\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ffed80c94d5fea9d76f4fe35d4478984\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9852117d814a38fe3eba010475bafd632a\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e72e01936d9a14e0e9b869c88ad45424\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9875e49cbfeae37a120af486d9eb2d93c5\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e9808d1f81b0d4082e0417db2f18a2acfb6\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/LICENSE\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98cbd123e02025168eb151b7d71e556d22\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b0953817803e455c620122efd668eb5f\",\"path\":\"file_picker.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9837a1507700518fa7f49a60aac4b7767c\",\"path\":\"file_picker-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d8e8a876a8ca0ce915464fbc27689c44\",\"path\":\"file_picker-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98273092b724e2281270954671470d86c2\",\"path\":\"file_picker-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98330b1e95388e708f5ad38ed7ce90e28c\",\"path\":\"file_picker-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98893e38e9593d6a06c8522268bf23390a\",\"path\":\"file_picker.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e981a76198b195c337f9587084c94a43437\",\"path\":\"file_picker.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e982406cc43be2014b06d264ea164e97c61\",\"path\":\"ResourceBundle-file_picker_ios_privacy-file_picker-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a98dabe6e24ef65809527fcab892dd86\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/file_picker\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f62c63ccec2525926d94e2db6b738061\",\"name\":\"file_picker\",\"path\":\"../.symlinks/plugins/file_picker/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e982c61e60ddf03cc04542492a7725f11f3\",\"path\":\"../../../../../../packages/flowy_infra_ui/ios/Classes/FlowyInfraUIPlugin.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a2f3125bf6a20381c479cb67f97aa968\",\"path\":\"../../../../../../packages/flowy_infra_ui/ios/Classes/SwiftFlowyInfraUIPlugin.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"children\":[{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e984ba1576a81594fe8f17aea9519f8b1fd\",\"path\":\"../../../../../../../packages/flowy_infra_ui/ios/Classes/Event/KeyboardEventHandler.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d5424bf8b4f526e22486e2a27f3268a5\",\"name\":\"Event\",\"path\":\"Event\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988d8845aeb0c1ed01cfe07a5f22cc0ec9\",\"name\":\"Classes\",\"path\":\"Classes\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9803f83e7a1d900c14ec7e6905f0092826\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98bf611f0f52fc18d7226f7e7f09c38245\",\"name\":\"flowy_infra_ui\",\"path\":\"flowy_infra_ui\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e981fb46775118dd702b7316a553c95e90e\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98616f2715911b1a74d97ddaada4344fbc\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984cd1c794c9b9ad19f2e4f0ca3f026819\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b37b705e2925a02d2062aaafe5099089\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982add91273b39db2a9cf969fe86dca06a\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e983b3dd8aad5aabbc2496c663812f03cfd\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d656d9598f5d14273b8b8981109e8b9e\",\"name\":\"..\",\"path\":\"../../../../../packages/flowy_infra_ui/ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text.script.ruby\",\"guid\":\"bfdfe7dc352907fc980b868725387e989d6db7e7b482c3241d9a1acce4813ee8\",\"path\":\"../../../../../packages/flowy_infra_ui/ios/flowy_infra_ui.podspec\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e989fd87faba1a0f13d4d7494191f208760\",\"path\":\"../../../../../packages/flowy_infra_ui/LICENSE\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98145bf30753fca245e5d38777385d9388\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e9893b48c5d4d9ce2656928a0ee3ff7944f\",\"path\":\"flowy_infra_ui.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e985398a052a839596d179b48db347a52c5\",\"path\":\"flowy_infra_ui-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c1829b249c76d5ac78b6563b1ee7fde3\",\"path\":\"flowy_infra_ui-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e985b5e2f476270884826aa113914031988\",\"path\":\"flowy_infra_ui-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e983e584fb8f2aede8db256844ee817b410\",\"path\":\"flowy_infra_ui-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98892689e861aa6009551af6801e83c338\",\"path\":\"flowy_infra_ui.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f6af4fb94ad21206a16cbaf1e6453010\",\"path\":\"flowy_infra_ui.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982cc4e6d82b8278ce278988c26691765e\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/flowy_infra_ui\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f43591bd9ec58ad67ffe4453fafc4a1a\",\"name\":\"flowy_infra_ui\",\"path\":\"../.symlinks/plugins/flowy_infra_ui/ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"children\":[{\"fileType\":\"text.script.ruby\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fb18cc16183813fcd8641d3cadfad33b\",\"path\":\"Flutter.podspec\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d37e3d81bea52e1b4801db4886715c89\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e9812cdcc535f32a8a76cc3d8e883d2013f\",\"path\":\"Flutter.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98048531a74a78f7613c93ba91f15c7ef8\",\"path\":\"Flutter.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98152d9dfbdddbef1340635c08e31152c6\",\"name\":\"Support Files\",\"path\":\"../Pods/Target Support Files/Flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988b26d3d01bc6aaf3315f6d51c851186d\",\"name\":\"Flutter\",\"path\":\"../Flutter\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9877147b007839a2216fd57593e0f3ba5f\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/fluttertoast-8.2.10/ios/Classes/FluttertoastPlugin.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987efa3ca5a1d26929ed3b109a26806afa\",\"name\":\"Classes\",\"path\":\"Classes\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98792f001acf47168aaffa10afd959b5c4\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/fluttertoast-8.2.10/ios/Resources/PrivacyInfo.xcprivacy\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9812c06060cb56dec48c82c3b93bd2fdf2\",\"name\":\"Resources\",\"path\":\"Resources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989a85c4c34db915b09efc04ad74e71906\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98879342f1ae53be318ccc851c8fb5f741\",\"name\":\"fluttertoast\",\"path\":\"fluttertoast\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986e74552a0b437b7d398a0e712dfea078\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98cd6a60c3b14da76ce2370d2a8be6b094\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e29b7bd0bc91b1bd5e1a2b480473e1d7\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984dd71cdc0b7c0e396503163601d414ef\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9892ba1e9027f479ab7d1c9354ba3a2aee\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d4cad64b7e33d505b2ecf5d631c9d3e7\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987932e250d513fa720967e0dd955cb5c2\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9869aa178a8c06888c2f7f224a2918a492\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98316da1c1d27426250efb3febcdf6e95a\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989a6847ce3a370c3b3d860bf1b58a643e\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9898b42eaa404b9ed45fdaf7f308bef4aa\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b9d4812f1ec13b817bb1728f24b63ef3\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9850c5079f1f3a336ba56135604b1311cd\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980ef2416841219f34ad24eb0617d29faa\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/fluttertoast-8.2.10/ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text.script.ruby\",\"guid\":\"bfdfe7dc352907fc980b868725387e982ae0f7d0422d2f75f6cdde13de9e63ab\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/fluttertoast-8.2.10/ios/fluttertoast.podspec\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e983d3e3470809b19b23bd82e0a81446281\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/fluttertoast-8.2.10/LICENSE\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d1afec4a601b8594f84cb4f864f71af4\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e983d17b3300e70f7d2c18f92e0d276131b\",\"path\":\"fluttertoast.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e981f77f9386b6ff408a5830dc5ec967554\",\"path\":\"fluttertoast-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f000b535e938ba4dfcadc2d08b7f9dcb\",\"path\":\"fluttertoast-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980af04a50d29871944a910af21b08eb54\",\"path\":\"fluttertoast-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d7bc0cf610c976df935aac7605febe19\",\"path\":\"fluttertoast-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e984e7d38849f0179ae896bfe33295f4bcb\",\"path\":\"fluttertoast.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98bfeefd631cf811ef5ce08bdcc7b5e371\",\"path\":\"fluttertoast.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98740356b6d05058396c4af19281498c88\",\"path\":\"ResourceBundle-fluttertoast_privacy-fluttertoast-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98781d0486e386be35e6ed9b7fe0e393a9\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/fluttertoast\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988aa84318e78c044eca9d3adcb2083544\",\"name\":\"fluttertoast\",\"path\":\"../.symlinks/plugins/fluttertoast/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984a37fbaf4a9886ee0471de51a32ce11c\",\"name\":\"Resources\",\"path\":\"Resources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98593629a4be85977669ea4c059a1d10a3\",\"name\":\"image_picker_ios\",\"path\":\"image_picker_ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f1b4186d564806fb6d451c19a178c28c\",\"name\":\"Sources\",\"path\":\"Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d5d0ee78f468d5980cfa1ea4ad6dc829\",\"name\":\"image_picker_ios\",\"path\":\"image_picker_ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e985041391a222e0b4847d8b2a02427be61\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9875530726d790ea59a3b2315529b92cc3\",\"name\":\"image_picker_ios\",\"path\":\"image_picker_ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989ea0a989d0c2a93f9b189cf1469cea87\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98391dbed2b1e45fe5718c27685e1d7c67\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986721fa987b7c82532939c240860d8345\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98fb63c7bf2a24aed477a61c0970de0960\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b2bbb2e48ebc4b126f757c2703266204\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9817ce3811d81e530356a343c80efe8ad4\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9849f3c66c3da79f5b15d867daabb3187b\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d3fa3916bdb222eafea4d7820d6eb9e8\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98cd7d949543e498a3b27db6a1893718cd\",\"name\":\"..\",\"path\":\".\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982e942c8a9d3701363338a82b251071ac\",\"name\":\"image_picker_ios\",\"path\":\"image_picker_ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98502dd1df3bee9584d607bb1b1a902b26\",\"name\":\"include\",\"path\":\"include\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982056603c1983c7c0b15b4f9d4e839e1e\",\"name\":\"image_picker_ios\",\"path\":\"image_picker_ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9892285f318b670b27f7fbf2287b291843\",\"name\":\"Sources\",\"path\":\"Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986a27670c0d2f4e5b9f45ade587b13d2c\",\"name\":\"image_picker_ios\",\"path\":\"image_picker_ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987b8f4a03e01f25f540c609ad50dc4076\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d97a722bba91079df06c440102468bba\",\"name\":\"image_picker_ios\",\"path\":\"image_picker_ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98af8e2dbb1e8185946e7becdd47b97f9e\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989ae881df58521cd150fc286fffe35603\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9803a91fc4ede7e5b0c2afbd5fab18fee9\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e981a39b88b7383525d9d473bd93d1e2f0b\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9811f7fa0b42b0c8fb654345ec4ebb8e66\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98623219eb0a2c578df94d5883bc419325\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e981125f09590385ffd9ea5a9fe5aa60ba5\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d31ae9e961eabf22319ec6a1fca4f113\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9817eb7c23f31f300a1c251c4305fda8c1\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ec140a00b7550fc8d6fbc0aaceaa7a89\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98bfbf2e646d2c92073ff2e83bebd9af2d\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f2692e38d39bd674e71f6bb22b891e4a\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9829706d777976177d0bf097f898b5b58f\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e985a01f741d7f1ce665f3e325d6c6a57f8\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98646c57f09cb01b0c762a0d93ca3f7b78\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98221a992b68c7cffb1a8eb5cbfbb297e9\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e988990923df59cdef8e5b1e19216f2a97a\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/LICENSE\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98aa0c16b2297382848b598f9592b0c3f0\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a37a01b5033e0b3fe7a1dbb5d490ef35\",\"path\":\"image_picker_ios.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fd29ed6fd89520fddcfbd7124ff888d8\",\"path\":\"image_picker_ios-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e983d86dee1d916fb29b63d5b1bfcc7b5f9\",\"path\":\"image_picker_ios-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9803f06581ec423d44280cf7868eea5e93\",\"path\":\"image_picker_ios-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e9862b8fc1ccbc32fefecbae61cf5888907\",\"path\":\"image_picker_ios.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e985530ede90a4a8c2c7507499317499697\",\"path\":\"image_picker_ios.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98bd2861c7f94ca0a221c93e59dabe2757\",\"path\":\"ResourceBundle-image_picker_ios_privacy-image_picker_ios-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9859b1bb772d76fea02e84d0016d099c3a\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/image_picker_ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9830cadcc7a1263e98e67a0cf722e5d0ab\",\"name\":\"image_picker_ios\",\"path\":\"../.symlinks/plugins/image_picker_ios/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9823e5a3226da96ed3914b1c8fcf559df9\",\"name\":\"include\",\"path\":\"include\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98bde8b1c5794795e3b29ba817afae8d21\",\"name\":\"integration_test\",\"path\":\"integration_test\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b7f37a3e9ff7a757003a42461c25c365\",\"name\":\"Sources\",\"path\":\"Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ede6ae88c67694e327de9c425a7f6132\",\"name\":\"integration_test\",\"path\":\"integration_test\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987f156cd8efcf8b450a0acccba337affe\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989dcf9316567af671febf7c01cbe9f506\",\"name\":\"integration_test\",\"path\":\"integration_test\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987cf80e4fed23235d2406a799ca33eeca\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9847a104d2bb397b64bd133d45453210d2\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989392c96daffc1a0e321f346049c20e7e\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980176812f8f0fbbedf15c13f3c2ee1f26\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e985aa0f0e528439e82d21295d31624797f\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98db8efb64140c28021c5072d8734e4699\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986ba3dab5252afe70f25f2838a01bdd4b\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b67e71ad7e85fc1ac5853ac3428427de\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e985f23f974cdcdf9412df80483d5680940\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9873283974ea9ea8d983cfb681aeeee604\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98db229454a7e5041b4830de422e60e499\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989a2ced9ad917530dc3e50756255a1e21\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986b9379d3571941dbae7d47d880a7a296\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e985055bbeb70e29da915730a29ed24173f\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988070745f3c2f414eef05f58b72da6f5c\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9863c8c730e09674801ad705a3ac5f0bb5\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989899d04f24c9ff95f1314d4fabeea094\",\"name\":\"..\",\"path\":\"../../../../../../../../../../fvm/versions/3.27.4/packages/integration_test/ios/integration_test/Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text.script.ruby\",\"guid\":\"bfdfe7dc352907fc980b868725387e980c15437a062ac65671b286006afd1f2d\",\"path\":\"../../../../../../../../../../fvm/versions/3.27.4/packages/integration_test/ios/integration_test.podspec\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d94ca862be564f152946859a75277d34\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e98201d61b0d848a49c65589ceb7506e08d\",\"path\":\"integration_test.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98af0a4e5755a22cb43543a81c374a2d51\",\"path\":\"integration_test-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a8522fb10db2dfa210c208f905afd9f3\",\"path\":\"integration_test-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c0b24b129604eddd217c08a7d0c062db\",\"path\":\"integration_test-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9896f6fa393191b60c6487f245272c94e3\",\"path\":\"integration_test-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98105bcf1dafac8dc8d85eaff566c35643\",\"path\":\"integration_test.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e986a9f0cec960a86a1eea7dbdb4c49fcc4\",\"path\":\"integration_test.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e981a27bfd3655d560ebee82c3ab7c104f3\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/integration_test\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98051dc09e6cf1b3ea30ba9f39d11035d5\",\"name\":\"integration_test\",\"path\":\"../.symlinks/plugins/integration_test/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98fc9b7f6e025c71f553fe6444717223ed\",\"name\":\"Classes\",\"path\":\"Classes\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98db377227d8253ffe98324018634bc2ba\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986f2a88ed98a92034ce863ada1219ab5f\",\"name\":\"irondash_engine_context\",\"path\":\"irondash_engine_context\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98da61f0e74006e8fbd784bf4a3b970e5f\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98635b7210c5b62e20e037994cad1bf111\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c64150b3d0970cb99fece561f5a8799c\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d3fdbea6d8cb166625df79788fcad443\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98cbeb45be878891b685575f6b25a8528b\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9847ce44e75f1621ef4e0a6a5ae85f248d\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b2ae8d52765d0e5d9bea0d19cd7aebc9\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c84ed3eae59cea51b25d355ea7faec4c\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9899ac6782e67161e5aecf52fb2c668a4c\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98710388c0277b6c02dc2bfce47465f2f8\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c5e6f44e93118427f696de099ce587e1\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c97afc961fd21222514666e42cbbfd8f\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984e9cef67c7f53259a28a5a877e4a2131\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e985b651d4fcaab066c39a7ebe7cff5daa9\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/irondash_engine_context-0.5.4/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e9830fe1bed9eaa6250033f52ceaadf7d59\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/irondash_engine_context-0.5.4/LICENSE\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986fa867be18f4a0a3be7766d74146209f\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e98425d4148b72c406e4fd6672ecc362707\",\"path\":\"irondash_engine_context.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d84855c0549724120cd6c3172b83cfa2\",\"path\":\"irondash_engine_context-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98de048098637f376810dd3bb08c83895c\",\"path\":\"irondash_engine_context-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b78febae09cf0c8f6fc7540345bee7a3\",\"path\":\"irondash_engine_context-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9899f4dc54067ed61c6ad87cabdfa23817\",\"path\":\"irondash_engine_context-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e9859e866190126bed1c816e3db8cf733a3\",\"path\":\"irondash_engine_context.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e9837cfa75eec5f21f87364f26642dcde07\",\"path\":\"irondash_engine_context.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9832c8cd0ed43920be52e135a0de0ef859\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/irondash_engine_context\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98abf194361c2fa0e4ef4dbec1a81fd4cf\",\"name\":\"irondash_engine_context\",\"path\":\"../.symlinks/plugins/irondash_engine_context/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98bc635b84f7af43f9a693a5e68a4759e0\",\"name\":\"Classes\",\"path\":\"Classes\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f0eadcb14d473ce9c1a7c484bf311b2e\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98cf80142a4ef1966c0c5c5b502225cf5a\",\"name\":\"keyboard_height_plugin\",\"path\":\"keyboard_height_plugin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989d420609dd708f812028e4f49f0e9d67\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9895cace1167e8f8a1209977ad38dfccfb\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98422a86793790ea0d327a52c60169ac60\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9830a1c8fc44b202d4661674a7fad9873c\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e85d16fef38a2779c13639b43f1e3726\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9842be4aab321e08d3762331c112720d13\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d8f5a70339a345d74b94ca7b959328bb\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986d3eca9fadfaabd2c455a69807986068\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d98f50f9676e73dee28b8bb61da9f8ff\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9807172992c96c01947c7f63e7875b1880\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982737307bd1600ca8956d8f22af636541\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98854bc6907e9bd1273078ea0de43aa17e\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d53a1d0636736242ed1151c333b5fc9f\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980c1dd450aea5e1194ba1fff003416312\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/keyboard_height_plugin-0.1.5/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e985cbc6607d34630da0312e81033a0eac3\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/keyboard_height_plugin-0.1.5/LICENSE\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98fe47c46f6c2454354c85a993a081845d\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e98619ee239792a4f0a0de0a4a0b0a159ed\",\"path\":\"keyboard_height_plugin.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a419ee90c48f680450b1d3ead4ab82d1\",\"path\":\"keyboard_height_plugin-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b22dd52696b4067993e38f2551b63980\",\"path\":\"keyboard_height_plugin-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ac24e13278278e052df0627e6f2cfe76\",\"path\":\"keyboard_height_plugin-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e988d785c658ed39de0a4c38be45dfbc1bb\",\"path\":\"keyboard_height_plugin-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b6b8fa71d08d5539059bc6a5868a679d\",\"path\":\"keyboard_height_plugin.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e985de3091a8d3281d32c2891e218876b88\",\"path\":\"keyboard_height_plugin.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b6f518b56ca945c4e48ea2f80f2dde0e\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/keyboard_height_plugin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98de11b38cb40d0a466e962277af5c13cc\",\"name\":\"keyboard_height_plugin\",\"path\":\"../.symlinks/plugins/keyboard_height_plugin/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fda7caf052d83850aa64439013c284eb\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/open_filex-4.6.0/ios/Classes/OpenFilePlugin.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987c5f0895df45e85ab2470a8073a4c8f0\",\"name\":\"Classes\",\"path\":\"Classes\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98edc66a76af5791867252ee9a4fb21910\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9895e2c6870f605df8c86b47a953028581\",\"name\":\"open_filex\",\"path\":\"open_filex\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980fa51cd5f979e99f137ab48d9cfe538c\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989c915eac28a038451586120b5e42a4dd\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9844dffd7af5aba7efc4912e98f2c99fa8\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982963e1dbef544728f2fbcde398f7ce6b\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9820c60640726eda7bd6b2af35bd76df61\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98bb93350f484f56f7d755b99459ba03a1\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e5b26b9b30e5f21dd0e61b63275d1293\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986605830136cc6fec7889ae13c84cbe94\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986b903bd75146f71f5101abe9a847888e\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9835c6d6f45f08bbdd25355fae03109fc9\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989b22dd54825d4836f9aeb5366f4d1942\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98fc1763d383faa8bc59b0d7f9aeef3f0a\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98bc4e9c0a74a0677415efe8c07067e305\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9898e553f7a823f6dc70a807a2f2d230a6\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/open_filex-4.6.0/ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ee0863552468cb4b187a007dc6d9a9b2\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/open_filex-4.6.0/LICENSE\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.script.ruby\",\"guid\":\"bfdfe7dc352907fc980b868725387e9852adb17f9ae796569014283b63994f43\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/open_filex-4.6.0/ios/open_filex.podspec\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9881839ee4b95080a8084a269cdb03d9cf\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e985fad692e094e1d0680167d0e9b810fb5\",\"path\":\"open_filex.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9840ba9a41d5060da79af9271d16ff75b4\",\"path\":\"open_filex-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98481c5fe7d76a4040e0ad917154c7e4d5\",\"path\":\"open_filex-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e983eb20923999fb0272a7d7981812ac419\",\"path\":\"open_filex-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98045ac1ed0e248a91c7e040ef0eb573ac\",\"path\":\"open_filex-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d756e4a333f24f739d0261675ee1a4ba\",\"path\":\"open_filex.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e989220364d0a7adb8651f9b9241f5c7291\",\"path\":\"open_filex.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984048ca38c41c417f8623767606347b2e\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/open_filex\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d744b5aadcc662d9087e0ff8ecfa7db1\",\"name\":\"open_filex\",\"path\":\"../.symlinks/plugins/open_filex/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98434c664522ec8a949437da93121dc4ea\",\"name\":\"package_info_plus\",\"path\":\"package_info_plus\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98da4508512de7ed1cf62ba23f6e0a1782\",\"name\":\"include\",\"path\":\"include\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987977607c1ffbf525bd13f89c1d4d9af6\",\"name\":\"package_info_plus\",\"path\":\"package_info_plus\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a38ddce212e7e062f995ed0c81943891\",\"name\":\"Sources\",\"path\":\"Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98931f5d8098ad1aa7c93eec26372ba236\",\"name\":\"package_info_plus\",\"path\":\"package_info_plus\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98285660b922695a353a0d34c11f224681\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9850cc5da0429ea6a6d2b70a2656fcab30\",\"name\":\"package_info_plus\",\"path\":\"package_info_plus\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987ddc93fc8ade4d1b45b41d4e43f4567c\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98265c9ab7615abe783c697b6cd4393241\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d03678f78b9690ff2b539e1f76337f73\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980ad2375b9e2190a33ef70bfb739550e5\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b7ffa0b49f80dc8ef09e80c7b9ea10d9\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9870b15a53dd8bece6cba98fe4fe76a795\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c4af5d8efdfb0599410662c8d5fbef98\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9875f8bf110ce0646b5fb64007eacf9418\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c89cf11d4dd01c776076b9aba66f6de9\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9856bac1d729bbf1ed2a72d1315cebd361\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9891ba0e8cfe4277c3831c309656922ca3\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980531ebbd14b9b1db9cb595aee441e728\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989afbfb7501eabf645dfdd3b2d5371318\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987d5f78597ba1b1ca57b94d699cac9587\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e0fd6aabdc57252bbd2b8424bc907ff7\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9890c7045574c1805ae1374327a927bd10\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/package_info_plus-8.1.3/ios/package_info_plus/Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e987939da84758bc78ea493a94d1cea8578\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/package_info_plus-8.1.3/LICENSE\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a53ea27f341ed23389e7d2e47e722d47\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e989676689969e25b7e2b922fdcc545264e\",\"path\":\"package_info_plus.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e985313e6a90d7b731193c46689110da675\",\"path\":\"package_info_plus-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ecaad6d841c680a103b7c06250152c10\",\"path\":\"package_info_plus-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987e968239394f15ed507902e17a5a5d3b\",\"path\":\"package_info_plus-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e983b443c6a2f8b2d550ac2cdd9baa30e9c\",\"path\":\"package_info_plus-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e9833b0ea6cf67df0b17b6edee85d07e536\",\"path\":\"package_info_plus.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fe3643fe82f3241c56cd3e148f9bfcfb\",\"path\":\"package_info_plus.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e9bc7cd75e23035e788a0e7c25bf5266\",\"path\":\"ResourceBundle-package_info_plus_privacy-package_info_plus-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98597a17b9183cd7dd4bf1dc3afc5c61ea\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/package_info_plus\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98aacab2d91d3b0af7ec103ba0eb959971\",\"name\":\"package_info_plus\",\"path\":\"../.symlinks/plugins/package_info_plus/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b4c976140e1e1950199f10d209e74f5b\",\"name\":\"Resources\",\"path\":\"Resources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98eacafab16506b203843ef40c684430b1\",\"name\":\"path_provider_foundation\",\"path\":\"path_provider_foundation\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98afc251afea8ca3c7c50e9df03700de10\",\"name\":\"Sources\",\"path\":\"Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e09b0557ec981a89a5d3cdcc6842f0c7\",\"name\":\"path_provider_foundation\",\"path\":\"path_provider_foundation\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98848154feecc038860b30933f28fcd571\",\"name\":\"darwin\",\"path\":\"darwin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e4c4edafb8828d8853578114f39e78ab\",\"name\":\"path_provider_foundation\",\"path\":\"path_provider_foundation\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988219086c27550c56b47393a9c0e39e4c\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e704029916b2a9af5f095363d987f0fe\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98357eba85aa36d189cd46406d27f5211c\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987a6b9a2268f0d1806de5375f49c7ad34\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f47342e61ef54b290a2f053ba08d9dca\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d0b8c28e93e04c0badf7323d031eae30\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9837afaa61f9ce1c7f4fd458c2cf5adf98\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ca9305ea66f0600f4817de50cde8db74\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9808e63fd1bbc402ab215a50d1d16dcd32\",\"name\":\"..\",\"path\":\".\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f3577bef4fc38afcab726db64a001531\",\"name\":\"path_provider_foundation\",\"path\":\"path_provider_foundation\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982b0a750596bd71fbc8cfcf1a9c72ca42\",\"name\":\"Sources\",\"path\":\"Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c1402de7d04e960dca8c1c9a29277190\",\"name\":\"path_provider_foundation\",\"path\":\"path_provider_foundation\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b0db58f72b87ea1465b9bbf9fa254cd1\",\"name\":\"darwin\",\"path\":\"darwin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d11257d51b57c6c8f8403335be4b346e\",\"name\":\"path_provider_foundation\",\"path\":\"path_provider_foundation\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b2fe29e812c42cd3759a2d5cda315ac9\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9866a118eb431e0fc65900b0d1ed264fc9\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c5612f9fa3a0fea2a9cb1e98a04ed834\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987ba29a7b05ab240400ab764ea3778d63\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e6d60acafabcaba2c92afe3f89dccc00\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98957bcf5769cd6c1ddb7b1bacb4045c62\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9857a303219d5ef02905623f5642388e38\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d0517ff6d03f19c2b5d8f5ae77998f57\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c6054740313f5e60a651b62a3a9fddff\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98bb43e5bcfafff3075b881ff2a9506820\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98952ef0fb2ed4fabd37dfc72628c7e3ba\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c173f0cea5f6ab214f5630fe12869f9b\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b3e481ae0756598ee53e01ba3ade5dfc\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9815a65875b9784344b15a908a00046200\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9827c3714a1d7d43b116f6f2b348cb54b5\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98600a8661b70b3bcc185abb7a4e9008f4\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/darwin/path_provider_foundation/Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a8b04a5824c42b6f4f609567865faee6\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/LICENSE\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d9cf578a3f3bd7e82b6d5e62241a3287\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e9861842fd411d053b57aced9948ca56491\",\"path\":\"path_provider_foundation.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cc5a067b653131ca99fde3ba1debf062\",\"path\":\"path_provider_foundation-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e989dd8857730e4c3db203f2e8d33299b7f\",\"path\":\"path_provider_foundation-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980ba8eca3e056e4a5b65fa675f2756611\",\"path\":\"path_provider_foundation-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a687ca6ea8584ff3e8089580a0210540\",\"path\":\"path_provider_foundation-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e989a3ec1c60f383374577efaaaba58a590\",\"path\":\"path_provider_foundation.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c312fad1a73dfd52c70c16e3d25e37e6\",\"path\":\"path_provider_foundation.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e9d12e6cfc445534a2c16d64aa703cc3\",\"path\":\"ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f3324a2357137a370c65dc36aa758feb\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/path_provider_foundation\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986a2b2d2d35d31671cf8d3a967a7db258\",\"name\":\"path_provider_foundation\",\"path\":\"../.symlinks/plugins/path_provider_foundation/darwin\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98bb6db6682fe6132381afffdd0b60c949\",\"name\":\"strategies\",\"path\":\"strategies\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d5b3b31059ef8de5598d516260c77291\",\"name\":\"util\",\"path\":\"util\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b21a21f20e31c9dffd8a301b181e7499\",\"name\":\"Classes\",\"path\":\"Classes\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989ac6ab6d3c4d5bfd6ee635e5bfaf86a2\",\"name\":\"Resources\",\"path\":\"Resources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986adbf3486d93b28d124272c05d31c535\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9873343d816dbbcd1b184b6d68e52b8f8b\",\"name\":\"permission_handler_apple\",\"path\":\"permission_handler_apple\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e29a61315e2ef4299defb08c834d8658\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9813dc0e9e99378f2e9402361650d4f3c8\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e01ef4d5c3bed4f78810b1761f0bfd2b\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982268db3e1a4139e97ecd2061e336e5bb\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e985a0dfbbd565a647af8ec44544f29a21d\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98656f043d890499eb834029f3c937d260\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98de17afe181ca6321b521909130e6f662\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986adad874bc346fed5ccaed27e453b741\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980ff2305908cc93c4a0725655e88bdc78\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9857ae62fcfbdc453c8d905748f5c3feee\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a7884dba25b1f761bcc93d0154f12971\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9828b593dc8b9a43bd8236b7d7a05a125d\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9840d397c5e3d46385c36e81d47ddb4e1d\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b5d7000c1c54eb9b83d1cdaabf7aa111\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e98eedb8bb38abe4a753d3e5a4a50166ce0\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/LICENSE\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e981b1b5aec77e91493469b21f5697a5678\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e9895519dd776dc6b6b6a0d5a99939759a6\",\"path\":\"permission_handler_apple.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d3090640601ef537b4effff19f6956f9\",\"path\":\"permission_handler_apple-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98693e4bbf87f0173ce83ad9dad9c4ec64\",\"path\":\"permission_handler_apple-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98db4011adc2801eb0447df5d29a88d434\",\"path\":\"permission_handler_apple-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9872ca1772cd655bc190d399786a965f06\",\"path\":\"permission_handler_apple-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f0f251835fe2b85ba8e20304af40d5bf\",\"path\":\"permission_handler_apple.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98498a3b2ced2fb3f55d9af8975a87d769\",\"path\":\"permission_handler_apple.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e986ea3ec8648859708ff7049b01e77d7e6\",\"path\":\"ResourceBundle-permission_handler_apple_privacy-permission_handler_apple-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e981e5c39acd14afb0de574df88226e3506\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/permission_handler_apple\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98373a889c847b4c599507bdad83234fe7\",\"name\":\"permission_handler_apple\",\"path\":\"../.symlinks/plugins/permission_handler_apple/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98dcbc4e0c213bbd743d9703fa7e437dfc\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/saver_gallery-4.0.1/ios/Classes/SaverGalleryPlugin.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f7bbe1f859cede2224d42b729d4c355e\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/saver_gallery-4.0.1/ios/Classes/SwiftSaverGalleryPlugin.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d5b49ba1d17a7857dc59f1175110cc13\",\"name\":\"Classes\",\"path\":\"Classes\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c89b00fc416882d2d0d7c9b090fa7c9c\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/saver_gallery-4.0.1/ios/Resources/PrivacyInfo.xcprivacy\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9820ce2023299a06e421e4a78afc6b0b82\",\"name\":\"Resources\",\"path\":\"Resources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9865ece6bf5cc7c6cce51ccbe39d149b83\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98541499aef6d248a8301980d2821058d4\",\"name\":\"saver_gallery\",\"path\":\"saver_gallery\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9822ec8e7cdcda0404b791382d92111f01\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9864d2f6413ca98b754c7ee8a5171ad1fa\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98230a7213b882ddae22c49a7606136e55\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e985321a2bb5473af9a5a2bc5bc4f834a04\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98dc31a6400e820a903a3adf3e0472b97d\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986b76ca3af3e88ee3e30fcf7e21bc6d0e\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988541bb9a4c72550c30a45563b09a2e33\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e4d469fa76bd962f8bbbec14965af39f\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988ab99b3f5ad06e51078cf9e780a1d69a\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98362dc58617351a4e100aac833e0a9775\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980dde8a02284f890277e1c2802444a83b\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9863783b60069d614fac80a368d82f00e3\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989d4a0878fd359b487c9cea0fa7016714\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980cda4027ee69c106bb442ab08ef4d82b\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/saver_gallery-4.0.1/ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e98187e4b7080377e470ceccd8012e8f22b\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/saver_gallery-4.0.1/LICENSE\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.script.ruby\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f1b23c2ea102d15d2d3364c334ab1150\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/saver_gallery-4.0.1/ios/saver_gallery.podspec\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f1988bfa026ce78ad365f9598fb48ccf\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e980dce72be9e98a0e7938fea67d5dec3f0\",\"path\":\"ResourceBundle-saver_gallery-saver_gallery-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f8441040a6b86dbd9daae56dbd15f227\",\"path\":\"saver_gallery.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98bc00fdad72f5f3771416bb7da4f843ac\",\"path\":\"saver_gallery-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b3812fe968f19a56dff3b7b21717a930\",\"path\":\"saver_gallery-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9851858f7e87dff1cd2d14bcfec05bf9e7\",\"path\":\"saver_gallery-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fdb913e186672d6dd019c490a01906ce\",\"path\":\"saver_gallery-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e986375a6a5aae83e2825b2fa84412a7b38\",\"path\":\"saver_gallery.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e986cfaff9f48d79d9aac38b1a4200ab7b7\",\"path\":\"saver_gallery.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9870bd2de048ce790265ba57145fada872\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/saver_gallery\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e9f8053722083a22097cca53b68ac915\",\"name\":\"saver_gallery\",\"path\":\"../.symlinks/plugins/saver_gallery/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b3f62b7cd428533f32ccbe502d30579a\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/sentry_flutter-8.8.0/ios/Classes/SentryFlutterPlugin.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9826cd8809b1744007cf2cdc49b9ee183e\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/sentry_flutter-8.8.0/ios/Classes/SentryFlutterPlugin.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9814197e8a8c3142283755e74bb1e1a542\",\"path\":\"../../../../../../../../../../../.pub-cache/hosted/pub.dev/sentry_flutter-8.8.0/ios/Classes/SentryFlutterPluginApple.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ed1bfbd13db96f1a5a9a509f62b23475\",\"name\":\"Classes\",\"path\":\"Classes\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988a2e2c7191f3342f69288dc5b6f24eac\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986b57f5aa6fe19896cfd444c1d7e3374b\",\"name\":\"sentry_flutter\",\"path\":\"sentry_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e95123df781f7246754488867eecad12\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980c8dbba9160b89547d42fa282bc4d833\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98408185297f1419b37849ee4ef61dd14a\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98db140395c21d264a6950ef09ad273630\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a317c3cc4f246a4d86dee21027be9f4b\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982c4e624eda92287d712d01e09bae857c\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988c938458c24c5a8a3de86b13a240257d\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9816b3e0cb5684faafbde3cd04104a8bf1\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982a1a4fb826ee2604d98a1124cda9655e\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984814fee408387d1cb1ac9abf04007801\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98646b3421a4027d57df2335874d741e7a\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d7d7eb47d8b13b56642d25993bad53b7\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9834206fc222aad06497489cefdfcadd01\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98472caaca9c22e04c9586a3fde6ae90f3\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/sentry_flutter-8.8.0/ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e988942381687e6a83dfb7f7b51b7114eae\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/sentry_flutter-8.8.0/LICENSE\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.script.ruby\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d7a35162a09b1bafe73fd4acf4dcd74e\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/sentry_flutter-8.8.0/ios/sentry_flutter.podspec\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c631af1f252c1a6185a574de8e401916\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b2b1e766516fc1973912c9ed7d38fba5\",\"path\":\"sentry_flutter.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b20710f0afc9a65fd11ac053ab5224c4\",\"path\":\"sentry_flutter-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fb88983eae4d4fb5041df897b5eb48b7\",\"path\":\"sentry_flutter-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98140ec5a195fd3cd2e6672a247b3c50dc\",\"path\":\"sentry_flutter-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980060be8e96d53611f0f35fc6b03e1144\",\"path\":\"sentry_flutter-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98898ca6b789d6ebda968b7bf2c32c2927\",\"path\":\"sentry_flutter.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98eb2509e05e4701c0e7caf39cb9f1a7ad\",\"path\":\"sentry_flutter.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f6c519cd219bbe493b512e88f6691f63\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/sentry_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9890b2588f7d4f7005fbfcb636532e8266\",\"name\":\"sentry_flutter\",\"path\":\"../.symlinks/plugins/sentry_flutter/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988aa5f3177cec22ad24112deb2bd5df88\",\"name\":\"share_plus\",\"path\":\"share_plus\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989b5ccde2486e01b9f408c52039e15189\",\"name\":\"include\",\"path\":\"include\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98fb60a3fc698011e0520a294091444220\",\"name\":\"share_plus\",\"path\":\"share_plus\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c0adb9f898335bb561ebd6a92c8b5764\",\"name\":\"Sources\",\"path\":\"Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987c61ae0ab682495e830ebfec4bb2d992\",\"name\":\"share_plus\",\"path\":\"share_plus\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98fec752fcf63dc774ef6492dfb4088824\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98061408887ffc6320474b1b3b9e56e869\",\"name\":\"share_plus\",\"path\":\"share_plus\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e981ca0473827b2355cd2af25d5d7d10627\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984fc5d20b622f4b201b585186291a8f67\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c1ceacb5959c41905f6d4367a79a62d4\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988d04b2310b73a2f1377c4bab4917fbb0\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9858dedb5230d6424239e49823b8eaf146\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a939846af6f55181341850124da10438\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987236a4c37717175b816032b6e16c35db\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98eefdcc530f2e92f8f7f62696466d831b\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e999dc81b190f4772b868a75138aa75e\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989a1108d6d840a33f47b9c0fc3af3275d\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988dae09aab63bdbe088b9c18b957886b4\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98cdcb3d9a575e64374dc1a1db82409a54\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988352975c34e4d7ee8941ab48a3068611\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9864da257bade8dccae972a95edd8d4097\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e981dc40745a0624f3b853ce39f5bd094cd\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98253b8bf66bd8ccf4c977852ef0a7714b\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/share_plus-10.1.4/ios/share_plus/Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e981e6f67f07605eeaa639fc9e31c57db8a\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/share_plus-10.1.4/LICENSE\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.script.ruby\",\"guid\":\"bfdfe7dc352907fc980b868725387e98851d3db594f8658c9786f56478fbb6a5\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/share_plus-10.1.4/ios/share_plus.podspec\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e42d81eea266b46708846d237e6b9514\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e986288ba151af560df873b930937f73b07\",\"path\":\"ResourceBundle-share_plus_privacy-share_plus-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f12cd395a2605f620e5facab18ef7617\",\"path\":\"share_plus.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e982db5430b3897df3342086c41d0ab0cc2\",\"path\":\"share_plus-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98875a85a28fcc15b9fc0d81e86a96d256\",\"path\":\"share_plus-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e15cc77379f3a5d2d644bb59a33c227a\",\"path\":\"share_plus-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f3d67ef42ee4a797c3b450c5f684db3a\",\"path\":\"share_plus-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e9874f3142bf7e3eb1d60d86ef7b8277153\",\"path\":\"share_plus.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e986e07cf0736987a294d8f475ef4545d77\",\"path\":\"share_plus.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a569546e20b60ea744093ab0ba2e1905\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/share_plus\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988982bbfb1ebde9b684d127cb40ed59ae\",\"name\":\"share_plus\",\"path\":\"../.symlinks/plugins/share_plus/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980424689f89b333dcef6b831656c09796\",\"name\":\"Resources\",\"path\":\"Resources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d76e5d546741e7c1bff558bc1604fa29\",\"name\":\"shared_preferences_foundation\",\"path\":\"shared_preferences_foundation\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9833fd9e8a667e58c3eab9ad18ac331952\",\"name\":\"Sources\",\"path\":\"Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a2f6b7afaf2afa6bd09f4e0f94068d4d\",\"name\":\"shared_preferences_foundation\",\"path\":\"shared_preferences_foundation\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98fb40156ec424853fa943291f5b303d0f\",\"name\":\"darwin\",\"path\":\"darwin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b1e3c36a262a1ed5ad3b185a0227eb67\",\"name\":\"shared_preferences_foundation\",\"path\":\"shared_preferences_foundation\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9831ab42991c30549603d3d8ca536224cf\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98508ae9e3aa5011f9d4942b2f89441d82\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e985f334cf968485fb19584ceb692110236\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98138f3662de1a399453cb9534c664235e\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9826b71c8c69e21da06f07c82c6cf362d3\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9828e3c8782ecd686251f0f260cb28a2d7\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9806bae6027bc7a368204df9fc3614abdb\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e5fd31bd7e3de1ebf8947d9a8ac4c2ca\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d049289db0016cf5f53369d79d91af15\",\"name\":\"..\",\"path\":\".\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98939dcc4abff9273e20bbabe5b4fd6244\",\"name\":\"shared_preferences_foundation\",\"path\":\"shared_preferences_foundation\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98642d561628a9b022c32e461ac6978d8d\",\"name\":\"Sources\",\"path\":\"Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c2f343ab24d82d39b3b8df75288becb2\",\"name\":\"shared_preferences_foundation\",\"path\":\"shared_preferences_foundation\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988e8c1d85da5ff6697e97a0b62ee47e2a\",\"name\":\"darwin\",\"path\":\"darwin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984be81f35c0b61917ec47fbad62d32af3\",\"name\":\"shared_preferences_foundation\",\"path\":\"shared_preferences_foundation\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9803adc645aa9991244006b1d25477fdab\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9830ff183ca4264558be2d7888f36dfe2d\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b46089f714cf1702ed485a5fc0b2746c\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98dd0a1e90245c099ea267ffdb9fe7727f\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9806173dfed9c5271631519792136dcb97\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9896acf7fc50fc7d46cf4fd58ba1a01a8e\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9877cd8eaf1b44ad950ab2c1de574d545e\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a4a438f84238468ae34a00396733c267\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9826990e339d1e6bfb92c43f2df4021253\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9839d6e5e860b74845e418690de949729d\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980437a510b83f078e975ac6e022596220\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e985a9c0f13a0e24494a94657ac6f06a09e\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f45a8957e29b9d8d573538c7c2a6660e\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989bc0f1da02a3a34375b7c9fb790aa6de\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9873814c430069fbcad3cc2b2068a2f2c2\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982a9fea2f1146b6aa8643c58308630eb8\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/darwin/shared_preferences_foundation/Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e987d0661ac6c8236d9bcf05223d8b5321c\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/LICENSE\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98538b20e19bcb07bd154b1117ca2e7f62\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98bf7e5ed8e9ee51963dd29079b941aefc\",\"path\":\"ResourceBundle-shared_preferences_foundation_privacy-shared_preferences_foundation-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fe484d482b057065dd2bc365ef0b1cbd\",\"path\":\"shared_preferences_foundation.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e987cfa1a168d224a15e60944154b171acd\",\"path\":\"shared_preferences_foundation-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e9882ef96f96deea78c69f4201bad0a0f36\",\"path\":\"shared_preferences_foundation-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98321ebbc91006eb23c9f9908d066f0fb5\",\"path\":\"shared_preferences_foundation-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e988ce14ca997c1ee5ad8eed12f49b985ef\",\"path\":\"shared_preferences_foundation-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e9863033d3e663dd173908d510d04d9645f\",\"path\":\"shared_preferences_foundation.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e983dff215e39548f9188a7b8c8d4c24c32\",\"path\":\"shared_preferences_foundation.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9843a113f040e59beff6b2bad0d9500e2d\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/shared_preferences_foundation\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9815e66823fa48c84facba1c31c881a7f2\",\"name\":\"shared_preferences_foundation\",\"path\":\"../.symlinks/plugins/shared_preferences_foundation/darwin\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ff6e5bd448ece42e55969cd7ee2dcd22\",\"name\":\"Resources\",\"path\":\"Resources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986016d04cf905713b9d5b98ae9bb69fd9\",\"name\":\"sqflite_darwin\",\"path\":\"sqflite_darwin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982743918f80430a5f6e3fb467f2d4e58a\",\"name\":\"Sources\",\"path\":\"Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98db5cfaa6497e017576049d2d766fe43d\",\"name\":\"sqflite_darwin\",\"path\":\"sqflite_darwin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9887c4ce35bf05c88dedb678a8a05f7ddb\",\"name\":\"darwin\",\"path\":\"darwin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98fbfd6a91559f348dcd214ab1cffe04a8\",\"name\":\"sqflite_darwin\",\"path\":\"sqflite_darwin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a41b93d2bbcfa5fa03eef8097eb619a2\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9835810861c4e0c37a8ee85af809b7fae6\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e622054adbb0796c1efd972eb720d1ed\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982499ce39bf8d8ed82a87b79418b6050f\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984cfa4ad175bfd7a9f0e02542ff45ab26\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986069180160f76ffe39b94ecee9268997\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98adeb13adbc566e980f9d252124dcea3f\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9820d6e1cf31e5551a3d68c121e504d8d1\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e985f4b97fdfbc2f7c5f93d9c36aaac6a3a\",\"name\":\"..\",\"path\":\".\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9892e5814da7ab09018c72d77ff35a15cf\",\"name\":\"sqflite_darwin\",\"path\":\"sqflite_darwin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a8b633bfa4915a0040bf5c00fcd27ba1\",\"name\":\"include\",\"path\":\"include\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b7b37f7cc0762d9ca0708a155f22b8eb\",\"name\":\"sqflite_darwin\",\"path\":\"sqflite_darwin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989eba8369512e5541562107b2d8b760a0\",\"name\":\"Sources\",\"path\":\"Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e33f20f0d475d4d5413f81e2e96ca687\",\"name\":\"sqflite_darwin\",\"path\":\"sqflite_darwin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987eef1cebbe3bb46644378bc4631fe02d\",\"name\":\"darwin\",\"path\":\"darwin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ed219e0fa4be3681e4e695b0532eae32\",\"name\":\"sqflite_darwin\",\"path\":\"sqflite_darwin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988972fd127184a48f0dc3e6aee302404e\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9877fca5587d318fba37442797f6f705b8\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e983ae2fa1946bb36f5e91787fb46d589c5\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984f9f6b5fd902d714436e433e3f764dd9\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98dd7c7199347a79274529e6303ba01ef7\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ca3a121f05226c04281b755cc833b917\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9893f425a7aea6b4482e64d0a5bc657629\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e49b35d1a40e4dde1b2357ef1cb8d669\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987e5e76ea8d51c8b8d79c16a34746f33e\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9854e643717fa41262edb3c3f1d725d61a\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ed6f9da6b7cdf2cb6886e30e6cdfbc8c\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98de4bc8577c1a9bea6d84ede9bedadcbb\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987d9613248b511bc9325f7bcd3f3e4dd2\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988f60c32fce1cfc81f7648e0319f50cb0\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9897a924c3a55a4bbf49fa6473d66861c6\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c43b6316619d3de45f006103a22d7931\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e981d8665643615515f27809c8e1ce6963e\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/LICENSE\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"net.daringfireball.markdown\",\"guid\":\"bfdfe7dc352907fc980b868725387e9852fdd6f48c5deae81f51387413e1e002\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/README.md\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98fe65523c54c78b02ecd1fb77f3e46537\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f79a082bb47738f827607307a0e2452a\",\"path\":\"ResourceBundle-sqflite_darwin_privacy-sqflite_darwin-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c804c2a12db2f8031b9549edefec60e1\",\"path\":\"sqflite_darwin.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e987dd296579fd63723f2385fc3e309a485\",\"path\":\"sqflite_darwin-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98dd8f3f1e4afb05338b44768a2eea883a\",\"path\":\"sqflite_darwin-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9839943824b9f6edc4e040c5c8b2151f82\",\"path\":\"sqflite_darwin-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9867c38360f7a8c57f89677b1b8dd0f63b\",\"path\":\"sqflite_darwin-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e983c5c591d1e2590f943bcf841c7250c29\",\"path\":\"sqflite_darwin.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ecc2b7f452e75bd86ca68cc787ffd0e6\",\"path\":\"sqflite_darwin.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9863d40e6f29652cdc2796922cd0db2932\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/sqflite_darwin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984e01895d459ef3578b9180bd22234ef2\",\"name\":\"sqflite_darwin\",\"path\":\"../.symlinks/plugins/sqflite_darwin/darwin\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9812ef15f301a3279be9158ecc5e2ee578\",\"name\":\"Classes\",\"path\":\"Classes\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9821ac568b46c2e99c638c4468b42476d3\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98288b3abd52cc5524befa714a293f1d78\",\"name\":\"super_native_extensions\",\"path\":\"super_native_extensions\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9847c7f918c61997a2cee2375876483e92\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98038dcce1516b7703d28d4d5925b0e167\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e27492f09959f12010d60e1537d0abd4\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989b82d7b34f027e37120d28d733c9bc63\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984e2d0c4c003b18b59d642ab053cd2e85\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98dac882f2bd6878262999058971045bb1\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98906dd99ca335afe3dd9a7c6cc05a2048\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9895327431cbeda356cd4093b0b03dde53\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98b3779ad14c3af16a3d17c6cb4a8af745\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98413ff013f5792a21699c27aa303d4851\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98503a720642223ab224d97e9c5f9bbe9d\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9823726b2431178718aa367a59ee6a520c\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f023da7cab226ce319043a4e226d36dd\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e550084af2ca7acaf14461ed1f151852\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/super_native_extensions-0.8.24/ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e988cb8e2ab455bb20dc19f7a3a2246df30\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/super_native_extensions-0.8.24/LICENSE\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98eeb382127790a1793c33c9b12ba2e097\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a18f45e0ed141c453cff3a3c4527ecbd\",\"path\":\"super_native_extensions.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e985c10bd2540461e46ce7f11028e018d75\",\"path\":\"super_native_extensions-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fb6de20b66e6b8dbd451f64bf906989c\",\"path\":\"super_native_extensions-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fdcda392f627466e5edccd1508970413\",\"path\":\"super_native_extensions-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c698b70fb576aa4fe26ace0b9b8afcdd\",\"path\":\"super_native_extensions-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ecc61835f7006374d2df8ec0b73025d7\",\"path\":\"super_native_extensions.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98960e413e1bdfa694f48dc871e7ef827a\",\"path\":\"super_native_extensions.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f219566d0916d6a77685c33cd16653c8\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/super_native_extensions\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f569f35312eec5d67d042173116f99c8\",\"name\":\"super_native_extensions\",\"path\":\"../.symlinks/plugins/super_native_extensions/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98247df5322ea5db3a84214881c52ee25d\",\"name\":\"Resources\",\"path\":\"Resources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9887c8fcbc6272382fe1137437a8cfd2aa\",\"name\":\"url_launcher_ios\",\"path\":\"url_launcher_ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987ab2c88ca66fa6efb7003fcd0a5fecbd\",\"name\":\"Sources\",\"path\":\"Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98aca3ef13270c3be3fdb24dff41e0219a\",\"name\":\"url_launcher_ios\",\"path\":\"url_launcher_ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988826612973646a4053217e2324d0644f\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980d552426f9b9a7580f9c3386dc024b81\",\"name\":\"url_launcher_ios\",\"path\":\"url_launcher_ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987cd73363ca2db27832a5adec82341972\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98fefce717b106b030e9dc126dd095e251\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9899ed1fd19213f07006bd2b9430d7772b\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9863db1d0c1fc2b855158b6fa51dde21fd\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988e2efb7aff436f31626c71ead15daeef\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c34f07f7b163887aa57af2d6c5172187\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f1d44c68f08f6c906df9811d7caf2ef3\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9832553b1b2fc5c507feabcb35c959e868\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984fb77cebbaaabf4e807dd6ebc12b73f9\",\"name\":\"..\",\"path\":\".\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987ea293b0366abc71ebb96acf17297ef7\",\"name\":\"url_launcher_ios\",\"path\":\"url_launcher_ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9888d93807e2dc1f667f941643f134e44c\",\"name\":\"Sources\",\"path\":\"Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e981798185d018f93fe0298b8aba5d63560\",\"name\":\"url_launcher_ios\",\"path\":\"url_launcher_ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e981019612be8bea02a74431d38ce3b9cbd\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9880eb5ac131bdc80edb26f033f8a72fce\",\"name\":\"url_launcher_ios\",\"path\":\"url_launcher_ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9851161dc629e1fb4d2d3fd8bd10ee4ca2\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989fc545117a2c4837792115a214e84a3e\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98cae5b473355e5f3f56b441bc383d1a87\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98025d2d7ef7d681723bfdf777175c2ffd\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9834b9faca7fd3324a1c2164a4cf7e5313\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988303e4a8f1f82c7fb4ea2702bb5cb5dd\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984516cc538c760100f78348eb10326053\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98732e82463b828bce4818d57c1ea975ab\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9880fbcdef07ddfda2f3153be29f969c0f\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e981e967b61253c26da24779f0e1571787a\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e983de9bb36b851937d715c96cb7d26dc7a\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98611614762b8213684ec434869762ea22\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988cb0470925262a8de3be403c8ae31ff0\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9833562037aa4c75db5121f347c9e53091\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e70d73185437e27f145504e3dc9f1033\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98ed5927b53a4d1ccd1f9684043eea4526\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/ios/url_launcher_ios/Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a5500bec173c13a85e52bd5fff791d8f\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/LICENSE\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98cceb9263cc3f6dd5b9212b964cfa7054\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98604688695bcf046b591ad88eaebd26fc\",\"path\":\"ResourceBundle-url_launcher_ios_privacy-url_launcher_ios-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e987be471ca7fbb33b50a518eea5dcb87e8\",\"path\":\"url_launcher_ios.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9871fa081d7f580f4936897eb46aae0878\",\"path\":\"url_launcher_ios-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e986f18d2a83f59c045caefbc68524cb3d3\",\"path\":\"url_launcher_ios-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ad745689d7ee76ea50404b77fa12b6f0\",\"path\":\"url_launcher_ios-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98eec495e5daf9423acf950402315f3522\",\"path\":\"url_launcher_ios-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b3b3d080e11ff54026ba894393f62b3a\",\"path\":\"url_launcher_ios.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e980bab71536c4bda286b310a48eedac214\",\"path\":\"url_launcher_ios.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98938e093d7e3b952e819ce9ac1448e73b\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/url_launcher_ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9833186a359a29a050b940ca2dbdf952b9\",\"name\":\"url_launcher_ios\",\"path\":\"../.symlinks/plugins/url_launcher_ios/ios\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9877cdee8b856454ff8e4213d5b4b8e81e\",\"name\":\"Resources\",\"path\":\"Resources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982e8fed289e7a93127c4e71b0a59be8c1\",\"name\":\"webview_flutter_wkwebview\",\"path\":\"webview_flutter_wkwebview\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989c9e91c05d6fcd60cecbf709fcfae9f8\",\"name\":\"Sources\",\"path\":\"Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e5bb761b833aa1c82ed6c79ade71f8d2\",\"name\":\"webview_flutter_wkwebview\",\"path\":\"webview_flutter_wkwebview\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f9d8e7c800ff72bd4f22b25217d72bd2\",\"name\":\"darwin\",\"path\":\"darwin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c7188e90eaef7902fa09bbd3b6c34138\",\"name\":\"webview_flutter_wkwebview\",\"path\":\"webview_flutter_wkwebview\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980d69590ab194d9defd371868c962c163\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987e1eeba79f29d46cbd63b8f82bc2f113\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982e575334bd02bdf114450e05cddb31e6\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987403e6bff419acd09288ab9193a47d46\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e983974551ef8940d0a77ce7e5dba345c15\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984bffff6dbf90752f811d1b628435611f\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980016f1720faf508de4c79265c93531f1\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e985409f0ee94bca73500af103a1f36ec69\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98863ea06f124e08ce56a9dc6093f14a5e\",\"name\":\"..\",\"path\":\".\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d786586788316e81a204e06a860d9b2d\",\"name\":\"webview_flutter_wkwebview\",\"path\":\"webview_flutter_wkwebview\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986d305bdf63057a110d9a04a946a6a0b3\",\"name\":\"include\",\"path\":\"include\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e985af659c04f6cdf4a7c73cb8559d95a55\",\"name\":\"webview_flutter_wkwebview\",\"path\":\"webview_flutter_wkwebview\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98e1e70587e6aba44c0bf6b4fa49bed627\",\"name\":\"Sources\",\"path\":\"Sources\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e983b00ae6fdbc30a34dfacc53e5e678acb\",\"name\":\"webview_flutter_wkwebview\",\"path\":\"webview_flutter_wkwebview\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9865432e0f8cd5ef04acb4722323bfaef7\",\"name\":\"darwin\",\"path\":\"darwin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e981fe60fe58df7074f3f027dfb308c785b\",\"name\":\"webview_flutter_wkwebview\",\"path\":\"webview_flutter_wkwebview\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98299b41c4840e81d29fd9defe63f562c1\",\"name\":\"plugins\",\"path\":\"plugins\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a1a72cddbe31b7904461fb30eabe3f41\",\"name\":\".symlinks\",\"path\":\".symlinks\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9866cb0dd0dbb6d8ae7c42bf7e70385636\",\"name\":\"ios\",\"path\":\"ios\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982c9395928d2b28bc4832e7eaf6338061\",\"name\":\"appflowy_flutter\",\"path\":\"appflowy_flutter\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e981cf382f136f655ee4ef53b31836e9976\",\"name\":\"frontend\",\"path\":\"frontend\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982695540da12cb3688e9cd89de55b4c12\",\"name\":\"AppFlowy\",\"path\":\"AppFlowy\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989cea8ab7b2bc9aafea94b071fcb36fe9\",\"name\":\"samples\",\"path\":\"samples\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987cb1234016d9a45a8e6ebfa24a1ca6b6\",\"name\":\"dev\",\"path\":\"dev\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e981ba95ad9f21e528ba007f114552e8faa\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989c9004908f380362c00015c90fe116d5\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98fe5984c239273e4752feee9c1cd0d417\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98eb7ac77a35fceda15f40d2e6dfe455e1\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98481664ecd25a98fbf478061e4214182c\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984d34857cedf24e0ffba417d4ddb7a5a7\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f104ab4138053278b0c742e5da3a37b6\",\"name\":\"..\",\"path\":\"..\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986ba8636d82c00ef31adc8e2a02c78383\",\"name\":\"..\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f15216db275bd0a141b390a80247ad56\",\"path\":\"../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/LICENSE\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d1afcef86fa9f9ff1253aefecce2db52\",\"name\":\"Pod\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e980dc040649dc9e2eab53c3da3c40c35b0\",\"path\":\"ResourceBundle-webview_flutter_wkwebview_privacy-webview_flutter_wkwebview-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e989d9a8732910c4132ba5e9ee6c49a6fd0\",\"path\":\"webview_flutter_wkwebview.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e985debbc2cab2d792fa92242781697aa86\",\"path\":\"webview_flutter_wkwebview-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f10ce8a5ece3a15020d640ce3ed6466e\",\"path\":\"webview_flutter_wkwebview-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9818b0186c11c004e985a36edb09183861\",\"path\":\"webview_flutter_wkwebview-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e3c8f3c679654f0d8322c0bfe86cb48c\",\"path\":\"webview_flutter_wkwebview.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98de306c3f6c2c14f312be69bcd973767d\",\"path\":\"webview_flutter_wkwebview.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98fb0231873e76e3fdd25c08c4f7142e58\",\"name\":\"Support Files\",\"path\":\"../../../../Pods/Target Support Files/webview_flutter_wkwebview\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98daffe5a2e168f85d5660f57f2cc4f3d1\",\"name\":\"webview_flutter_wkwebview\",\"path\":\"../.symlinks/plugins/webview_flutter_wkwebview/darwin\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9880465010b585dd5bc5b7af0a27d58067\",\"name\":\"Development Pods\",\"path\":\"\",\"sourceTree\":\"<group>\",\"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\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986f57df5597ed36b645cb934c885be56d\",\"name\":\"Frameworks\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"children\":[{\"children\":[{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e980f256812a9858de27fb60bd1accdd32c\",\"path\":\"Sources/DKImagePickerController/View/Cell/DKAssetGroupCellItemProtocol.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9837ba02f2eab7de1730b72c879d53f332\",\"path\":\"Sources/DKImagePickerController/View/Cell/DKAssetGroupDetailBaseCell.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e981791c992ccf9564106a690e1d96420b8\",\"path\":\"Sources/DKImagePickerController/View/Cell/DKAssetGroupDetailCameraCell.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e981149a1d26875e8b68800278ef1ef0dd2\",\"path\":\"Sources/DKImagePickerController/View/Cell/DKAssetGroupDetailImageCell.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c8875336f053558b1f9ed7c43e262ddd\",\"path\":\"Sources/DKImagePickerController/View/DKAssetGroupDetailVC.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98164873936b2ccd15bdc608cc60c2cc65\",\"path\":\"Sources/DKImagePickerController/View/Cell/DKAssetGroupDetailVideoCell.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a7cecd4700762c7a109513bfb52989bb\",\"path\":\"Sources/DKImagePickerController/View/DKAssetGroupGridLayout.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e989c224ca64eb47c09a922a3d927cf7f10\",\"path\":\"Sources/DKImagePickerController/View/DKAssetGroupListVC.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e986c20dce043f12383acfeb98f96470e02\",\"path\":\"Sources/DKImagePickerController/DKImageAssetExporter.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9855672fea159780d4dd23a1f5a30a7837\",\"path\":\"Sources/DKImagePickerController/DKImageExtensionController.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98261f313efe36b40a087a9d35206c9607\",\"path\":\"Sources/DKImagePickerController/DKImagePickerController.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e982975067e23a41db0b8f6979b4a4a3b55\",\"path\":\"Sources/DKImagePickerController/DKImagePickerControllerBaseUIDelegate.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d331501c9a3bbd1cec21da6d1bed6a79\",\"path\":\"Sources/DKImagePickerController/View/DKPermissionView.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9851006020f4c7fc277370bdaceee539fd\",\"path\":\"Sources/DKImagePickerController/DKPopoverViewController.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98f1abbceda566cfe605dbb72a81ac8f8e\",\"name\":\"Core\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e10cad38a7b8b791b5523321d2112ef0\",\"path\":\"Sources/DKImageDataManager/Model/DKAsset.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e985cdfbee7f0fa63af790094c590a82f83\",\"path\":\"Sources/DKImageDataManager/Model/DKAsset+Export.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e982173071859389679a6c45171b90987da\",\"path\":\"Sources/DKImageDataManager/Model/DKAsset+Fetch.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a89a84fdf4f961d13d26db8520129232\",\"path\":\"Sources/DKImageDataManager/Model/DKAssetGroup.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e418cf226491ab81be949451bec00d12\",\"path\":\"Sources/DKImageDataManager/DKImageBaseManager.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9878488e740dd474df94f5eec1f4ebdef4\",\"path\":\"Sources/DKImageDataManager/DKImageDataManager.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e01ac6181d083bc327440f955529b52b\",\"path\":\"Sources/DKImageDataManager/DKImageGroupDataManager.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e981e366accc57fcc9cc1a70ecd199c50e7\",\"name\":\"ImageDataManager\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e982a05c3bf490f4a4639be6133ce46fc50\",\"path\":\"Sources/Extensions/DKImageExtensionGallery.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9878b271eaafac7e3a83dd4dcb67d7b63e\",\"name\":\"PhotoGallery\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9892de1f95603b3309408b855b150222e8\",\"path\":\"Sources/DKImagePickerController/Resource/DKImagePickerControllerResource.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"children\":[{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e982cb145326ee0fe9b023de780deed78c8\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/ar.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e98aa1b813ba9ab60abaff79984557a95ae\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/Base.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e980c9106c6deeaa489bece52872ddc8302\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/da.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e98638a8ead3c4921071ce058b31e0789b1\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/de.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e9808c6c81d6544958ee5022314e9257bea\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/en.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e989d1190414344f8164f52c639a0b3923e\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/es.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a3612de5bfe6103968b0ad0e24321c06\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/fr.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e9840393198352d2a493eca64a5e8366781\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/hu.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder.assetcatalog\",\"guid\":\"bfdfe7dc352907fc980b868725387e9866b7b2a063b357116fff6128faab6010\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/Images.xcassets\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e98389b54d175b1ebff57cec1e50f863fcc\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/it.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e98653c7ebf4d1cd10720ef59b098ac5602\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/ja.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e98104b68b4ff0a25a5296d0b83fcc56453\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/ko.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d31edcc28dfbbe3d8ce637b5afc30729\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/nb-NO.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e984482abef551eb7f8c2328ba24695640c\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/nl.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e987d2482c68c58caea8c964ddc88375bce\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/pt_BR.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e82421c9939f56bd2bac60c90c0972a4\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/ru.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e98641e96c7b9979000ba15756d8c4088c5\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/tr.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e987dbf20be783e65771c0b6b10146e0526\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/ur.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e98496e8767ebc06f02f3a190467b76c669\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/vi.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e9851d4a9e151fbaba3b43b0c1c328e7692\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/zh-Hans.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e989af334eda3f4b74b226c99b858e3a8ee\",\"path\":\"Sources/DKImagePickerController/Resource/Resources/zh-Hant.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9804dfbef9d9e61df9bb559e2872b5da24\",\"name\":\"Resources\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d02568085c3a0b85378f96f4eb289f1a\",\"name\":\"Resource\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e985e388c776c1b334623438d45d3541462\",\"path\":\"DKImagePickerController.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e982407d1e8008282cbe7def1a4ffcc63a5\",\"path\":\"DKImagePickerController-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ea7538dde57b4e0f1e4dd4e75d1c0d0c\",\"path\":\"DKImagePickerController-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b27014ca8a98af66a8fbde29e88273ac\",\"path\":\"DKImagePickerController-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e989c1f453bb5d9dee5b1b66f4e8eb4eb4d\",\"path\":\"DKImagePickerController-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e9806dc5717c6a31d76aad6bd6ece1d0e8a\",\"path\":\"DKImagePickerController.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e982c698840c90027623e3da75a6ca7c080\",\"path\":\"DKImagePickerController.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e987e62c3ecefb84e0040e723e52f3a31b8\",\"path\":\"ResourceBundle-DKImagePickerController-DKImagePickerController-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e983adef97a715cd560344faddd2136ca7c\",\"name\":\"Support Files\",\"path\":\"../Target Support Files/DKImagePickerController\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986a64e2ff11892e14ae7fcb90015c6a37\",\"name\":\"DKImagePickerController\",\"path\":\"DKImagePickerController\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"children\":[{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9863e802284c73bd79713e7ebbd9884a03\",\"path\":\"DKPhotoGallery/DKPhotoGallery.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e980341a413639d3324d70cd433b477ad56\",\"path\":\"DKPhotoGallery/DKPhotoGalleryContentVC.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e989de872349f4afb6a3ec10554d20a0824\",\"path\":\"DKPhotoGallery/Transition/DKPhotoGalleryInteractiveTransition.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9891931bbad7bcd3c15555c1ab3b5e9eda\",\"path\":\"DKPhotoGallery/DKPhotoGalleryScrollView.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e989304b9d8408d53cfd4dc626445b7aa78\",\"path\":\"DKPhotoGallery/Transition/DKPhotoGalleryTransitionController.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9851ce307e43c7abf9a92db60b62d92eed\",\"path\":\"DKPhotoGallery/Transition/DKPhotoGalleryTransitionDismiss.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98bb524791f17b91cf8d8e69a04b9d6241\",\"path\":\"DKPhotoGallery/Transition/DKPhotoGalleryTransitionPresent.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9817d140dcad4deb3eb9494e9b79f6a54f\",\"path\":\"DKPhotoGallery/DKPhotoIncrementalIndicator.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b08fe598889329654b388a853ab11345\",\"path\":\"DKPhotoGallery/DKPhotoPreviewFactory.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98abec4317337fcb2bdb4f60b9acc75558\",\"name\":\"Core\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e981fe52c027e9b931957c809c3731fe5e8\",\"path\":\"DKPhotoGallery/DKPhotoGalleryItem.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98790064f3a8ebed4c57f29d168a40864a\",\"name\":\"Model\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c8c44b9bad588a8cbce8ae16059cc044\",\"path\":\"DKPhotoGallery/Preview/PDFPreview/DKPDFView.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e989def67d6c082310eb82979d6e6048439\",\"path\":\"DKPhotoGallery/Preview/ImagePreview/DKPhotoBaseImagePreviewVC.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e64926604b2aa420c550979dce302c11\",\"path\":\"DKPhotoGallery/Preview/DKPhotoBasePreviewVC.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e985f43f00297e493a6c1ff3f01846dd6ab\",\"path\":\"DKPhotoGallery/Preview/DKPhotoContentAnimationView.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e982b1b24b667eeb0aa9df1641d1afbf02f\",\"path\":\"DKPhotoGallery/Preview/ImagePreview/DKPhotoImageDownloader.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b7c4392f573762b73d261ae2c1ad25f5\",\"path\":\"DKPhotoGallery/Preview/ImagePreview/DKPhotoImagePreviewVC.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98036bb4411bb8d063d059db4e041547ed\",\"path\":\"DKPhotoGallery/Preview/ImagePreview/DKPhotoImageUtility.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b73272e18580fd7065bff4ab280cf9d1\",\"path\":\"DKPhotoGallery/Preview/ImagePreview/DKPhotoImageView.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b16bf7005681538906dcee4437e1937d\",\"path\":\"DKPhotoGallery/Preview/PDFPreview/DKPhotoPDFPreviewVC.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e989a757ab8e8a9fc630d1a57794cb67b77\",\"path\":\"DKPhotoGallery/Preview/PlayerPreview/DKPhotoPlayerPreviewVC.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e988a849dc79b3d29aaf31294b33a51b25b\",\"path\":\"DKPhotoGallery/Preview/DKPhotoProgressIndicator.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a03d72595a015012babd53f62ce9fe7b\",\"path\":\"DKPhotoGallery/Preview/DKPhotoProgressIndicatorProtocol.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9895fb6b2018a434a37a846c5e29fe4c20\",\"path\":\"DKPhotoGallery/Preview/QRCode/DKPhotoQRCodeResultVC.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9806b510aa1c18127530918269f3b10ecd\",\"path\":\"DKPhotoGallery/Preview/QRCode/DKPhotoWebVC.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e988b5a7957a4c4d63e718fe2f2bfa1a142\",\"path\":\"DKPhotoGallery/Preview/PlayerPreview/DKPlayerView.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98a4f4a3f314a53e8278137c97d48a2a7d\",\"name\":\"Preview\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e982c920e4b6a548594014fc19d89752af0\",\"path\":\"DKPhotoGallery/Resource/DKPhotoGalleryResource.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"children\":[{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e98322362f55daedd42cf2628aab1e7ee5a\",\"path\":\"DKPhotoGallery/Resource/Resources/Base.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e98257fc87c20dddd559c3a65cb29ef2a8c\",\"path\":\"DKPhotoGallery/Resource/Resources/en.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder.assetcatalog\",\"guid\":\"bfdfe7dc352907fc980b868725387e98632a77eb6e8a5c194970ffbb837e3163\",\"path\":\"DKPhotoGallery/Resource/Resources/Images.xcassets\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"folder\",\"guid\":\"bfdfe7dc352907fc980b868725387e981cdbb40ea77c1fcafa54f43ddd5ead0e\",\"path\":\"DKPhotoGallery/Resource/Resources/zh-Hans.lproj\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9833f8b21cf1e493b32aa79c353bb36c59\",\"name\":\"Resources\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987e4621e932260350f9fe18d776062789\",\"name\":\"Resource\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e98669eec05ea75fd44fb0e2666721dbfac\",\"path\":\"DKPhotoGallery.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e986ea1aeb64b4ee3b54e278e3a7146573c\",\"path\":\"DKPhotoGallery-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e984490f039b0eb3468d94b0ab2e6a714c8\",\"path\":\"DKPhotoGallery-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9827c39759d14ae68710c6e6f3caaa10e9\",\"path\":\"DKPhotoGallery-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e986024dcc5015a387313d8adc4bce7b36a\",\"path\":\"DKPhotoGallery-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b1d81c0ffe868a64db997ed9da144cf0\",\"path\":\"DKPhotoGallery.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e988204bc98e0d67983074132a4f5665a6b\",\"path\":\"DKPhotoGallery.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e987ab489cc4f30435cd7ded82f0c6eae68\",\"path\":\"ResourceBundle-DKPhotoGallery-DKPhotoGallery-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987ec59054c088e47e413206915ef9028f\",\"name\":\"Support Files\",\"path\":\"../Target Support Files/DKPhotoGallery\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986c674d50d3b10e235f4f35f7a35d5ae8\",\"name\":\"DKPhotoGallery\",\"path\":\"DKPhotoGallery\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9896448ec1f9cbf14857b3fe6b95caa011\",\"path\":\"Sources/Reachability.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e989bacf7eba26d0bb486de3cbfe5ea13c2\",\"path\":\"ReachabilitySwift.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b6244bc1a6a40e6115703cb3d00b8446\",\"path\":\"ReachabilitySwift-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e987b7b6b5547766ec62d38737cd24e68cd\",\"path\":\"ReachabilitySwift-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98547e39e19bf28c52f14233768cc21bd9\",\"path\":\"ReachabilitySwift-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e988aeea5b451523377c4d21395c5903a7c\",\"path\":\"ReachabilitySwift-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98901baae58940cfd27e962f9d5011362f\",\"path\":\"ReachabilitySwift.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e989a874d2419abc53ad8b0cd3d2a5318ae\",\"path\":\"ReachabilitySwift.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9896d1894bae44836917da92d3aab54f93\",\"name\":\"Support Files\",\"path\":\"../Target Support Files/ReachabilitySwift\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98bebab75b96c37c53dc67122f6031b002\",\"name\":\"ReachabilitySwift\",\"path\":\"ReachabilitySwift\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"children\":[{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cbbfeea198fc4bce239c47fc57d8a961\",\"path\":\"SDWebImage/Private/NSBezierPath+SDRoundedCorners.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a4610931d9a303cf4a5413361d1ada97\",\"path\":\"SDWebImage/Private/NSBezierPath+SDRoundedCorners.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980e908f1ceaaa33a36d5403f550e8b9aa\",\"path\":\"SDWebImage/Core/NSButton+WebCache.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9817f5ea7520f6058c0504b71e2865c594\",\"path\":\"SDWebImage/Core/NSButton+WebCache.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e984f06f7f319f35e24d3947bb38a46b4b9\",\"path\":\"SDWebImage/Core/NSData+ImageContentType.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e987b53a8893f5f3cdd6faadb0d3bfc4d62\",\"path\":\"SDWebImage/Core/NSData+ImageContentType.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9890edfb9455d8d050d856d19de294b76c\",\"path\":\"SDWebImage/Core/NSImage+Compatibility.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e987853d28d16fa30841c1a55b61c447d01\",\"path\":\"SDWebImage/Core/NSImage+Compatibility.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c8d140d68f858c8de679df2d49e39ce1\",\"path\":\"SDWebImage/Core/SDAnimatedImage.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b0ebe90ca974f5cad05396d5c0c407cc\",\"path\":\"SDWebImage/Core/SDAnimatedImage.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9809df879cdb20a2d85e42e0ec9467d874\",\"path\":\"SDWebImage/Core/SDAnimatedImagePlayer.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e2ed6b92aae530671fb6a85f4968036a\",\"path\":\"SDWebImage/Core/SDAnimatedImagePlayer.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f116af502c254813038fb0fa9b6d7e43\",\"path\":\"SDWebImage/Core/SDAnimatedImageRep.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e987a40b9365faa88cb247c44544207a3ce\",\"path\":\"SDWebImage/Core/SDAnimatedImageRep.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e985026f7f77e36035ebbdce01168f1c703\",\"path\":\"SDWebImage/Core/SDAnimatedImageView.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d1c51ae1c8f49cab13fbed609b95490d\",\"path\":\"SDWebImage/Core/SDAnimatedImageView.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9853c881de2f36eb982e91b6111edd2159\",\"path\":\"SDWebImage/Core/SDAnimatedImageView+WebCache.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98229f3dff826db691d58cbf3df3ae4b2c\",\"path\":\"SDWebImage/Core/SDAnimatedImageView+WebCache.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a7fb3d4eb5f455ce911e0e84b77ee5df\",\"path\":\"SDWebImage/Private/SDAssociatedObject.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9889c0b922f6710e9bbaad47d90e458796\",\"path\":\"SDWebImage/Private/SDAssociatedObject.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9870a50afed1d5bb331637a22e1b2cdab6\",\"path\":\"SDWebImage/Private/SDAsyncBlockOperation.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e989a88b4352ddd2268b71af2cececb468f\",\"path\":\"SDWebImage/Private/SDAsyncBlockOperation.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9894060044116c365175a4d8c9d018b47b\",\"path\":\"SDWebImage/Private/SDDeviceHelper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98379407cdd5ec5f0842afe1dfb96cddaf\",\"path\":\"SDWebImage/Private/SDDeviceHelper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9820bbf68e6d4b7e185246c728464b69b4\",\"path\":\"SDWebImage/Core/SDDiskCache.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98556c8c3201769e199eb76c9fc9d5a0de\",\"path\":\"SDWebImage/Core/SDDiskCache.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9822703fb114a75f5c5f2f254bb3f6484c\",\"path\":\"SDWebImage/Private/SDDisplayLink.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e982e5995722797bf81f987a7b34e8d29db\",\"path\":\"SDWebImage/Private/SDDisplayLink.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98688ef71815752ca7f6167d44ae846d81\",\"path\":\"SDWebImage/Private/SDFileAttributeHelper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d4442f69c6514bc98fae925092574979\",\"path\":\"SDWebImage/Private/SDFileAttributeHelper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cc8572110877f8beb99b8973afcea556\",\"path\":\"SDWebImage/Core/SDGraphicsImageRenderer.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d990503ee20a63ae3a6ac274eb15e500\",\"path\":\"SDWebImage/Core/SDGraphicsImageRenderer.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a81bf725f34f12d4633981ff1a9be615\",\"path\":\"SDWebImage/Core/SDImageAPNGCoder.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a93d56dd2776df0d9323eaf235ac4573\",\"path\":\"SDWebImage/Core/SDImageAPNGCoder.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98063a7a83ef11a2830cff9aef0f246a46\",\"path\":\"SDWebImage/Private/SDImageAssetManager.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9878a035a3947c0bfb2c74de160efc90f8\",\"path\":\"SDWebImage/Private/SDImageAssetManager.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ba45c50b7e93c71a1094293aeb130683\",\"path\":\"SDWebImage/Core/SDImageAWebPCoder.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e988b1062c96ec96331da01cae06e95e9bb\",\"path\":\"SDWebImage/Core/SDImageAWebPCoder.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98adff88ced90105be82ec5c254de38c39\",\"path\":\"SDWebImage/Core/SDImageCache.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b59fb4d17416bbf73623c9ca148d4781\",\"path\":\"SDWebImage/Core/SDImageCache.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98da4185ba0b8e0c270b9045f1c0ea7f34\",\"path\":\"SDWebImage/Core/SDImageCacheConfig.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f690a5caa9ac98baa8bfaca0d8119a97\",\"path\":\"SDWebImage/Core/SDImageCacheConfig.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e984fc8806592e30baa20155c119711a319\",\"path\":\"SDWebImage/Core/SDImageCacheDefine.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98aa465b740f26e7c857d665d9e948f68f\",\"path\":\"SDWebImage/Core/SDImageCacheDefine.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98025e98798e009017d38c6b8f9f98dd3b\",\"path\":\"SDWebImage/Core/SDImageCachesManager.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e989babcbb0bec790231c9ea03d45485895\",\"path\":\"SDWebImage/Core/SDImageCachesManager.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9848a18db96f9cc4192f5855b2c6ec65fc\",\"path\":\"SDWebImage/Private/SDImageCachesManagerOperation.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e988c4bfa1a1a41475204073a9c37cc4138\",\"path\":\"SDWebImage/Private/SDImageCachesManagerOperation.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9878c179b604ae00d511bcfe7b1c26229a\",\"path\":\"SDWebImage/Core/SDImageCoder.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e79c00adee724aeb4ee07faf25c36816\",\"path\":\"SDWebImage/Core/SDImageCoder.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9899370db25d5ccec85c5bc3841d9150c3\",\"path\":\"SDWebImage/Core/SDImageCoderHelper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9864cb7481f2ac08dacad300433c1e0e01\",\"path\":\"SDWebImage/Core/SDImageCoderHelper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98466aa14d7abf59b152f093b8602bd34b\",\"path\":\"SDWebImage/Core/SDImageCodersManager.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a351c522c640e5c3836b0b3b0ff15805\",\"path\":\"SDWebImage/Core/SDImageCodersManager.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e989938d9775335a6ffdff595f50234b88b\",\"path\":\"SDWebImage/Core/SDImageFrame.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a8ca264bae9407c5ab570cb5c4eaa6e8\",\"path\":\"SDWebImage/Core/SDImageFrame.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e989a78ff5eec8840469dfa52c4df920d84\",\"path\":\"SDWebImage/Core/SDImageGIFCoder.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e984d22bab198a56a739526a9f9fac69088\",\"path\":\"SDWebImage/Core/SDImageGIFCoder.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b137bd091f2422235254ba87eeeebb86\",\"path\":\"SDWebImage/Core/SDImageGraphics.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e987ec2e6e40d48d5e1c573c77074e16edf\",\"path\":\"SDWebImage/Core/SDImageGraphics.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9817f302504ade0c532160f76082a200fc\",\"path\":\"SDWebImage/Core/SDImageHEICCoder.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cec361cb5581e3f861f0c9ebc7eb79b3\",\"path\":\"SDWebImage/Core/SDImageHEICCoder.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9863c1633045c8f4e6a7fefb974e14ca36\",\"path\":\"SDWebImage/Core/SDImageIOAnimatedCoder.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98728975404c77f683b561feae9c1d429f\",\"path\":\"SDWebImage/Core/SDImageIOAnimatedCoder.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9840158ea8f0a0e2c720c2f9a0c43bc29e\",\"path\":\"SDWebImage/Private/SDImageIOAnimatedCoderInternal.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e981a3f021a5e50a888a5ae1abc4e2e57fe\",\"path\":\"SDWebImage/Core/SDImageIOCoder.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9810ea71fbe3808e0193ef3ebfe97b71ed\",\"path\":\"SDWebImage/Core/SDImageIOCoder.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e3114679d79d3ddb667c3e6fb257d6fb\",\"path\":\"SDWebImage/Core/SDImageLoader.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d8bfeefcb91662f7491bdee227ce85b8\",\"path\":\"SDWebImage/Core/SDImageLoader.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987a235e20b10d513a95f2fe5a4faa9fb2\",\"path\":\"SDWebImage/Core/SDImageLoadersManager.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ae292dc3f6734ffa8a30d1eb750b312a\",\"path\":\"SDWebImage/Core/SDImageLoadersManager.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98555b647b7b68b3c59175202ef82bc955\",\"path\":\"SDWebImage/Core/SDImageTransformer.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98379f432747c66b556608cfcb163dd08a\",\"path\":\"SDWebImage/Core/SDImageTransformer.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e984b63b84d1f79f14a6dc3a12cac2809ed\",\"path\":\"SDWebImage/Private/SDInternalMacros.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98aad7fdfe00389f16c787afd556ef9f8e\",\"path\":\"SDWebImage/Private/SDInternalMacros.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98209eac57b1854edf591b16e3f80fec69\",\"path\":\"SDWebImage/Core/SDMemoryCache.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e982b6d0c6ce7e156d1ba21129855543471\",\"path\":\"SDWebImage/Core/SDMemoryCache.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9898c0fd4390df5133c07e348bdbd55d9c\",\"path\":\"SDWebImage/Private/SDmetamacros.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e988e65ef0d735983c43262cb848c637768\",\"path\":\"SDWebImage/Private/SDWeakProxy.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c1a6e6955d89f3463af21ab50142f735\",\"path\":\"SDWebImage/Private/SDWeakProxy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9802c2dd1c8e6745d6ad8dfa6ac15a50df\",\"path\":\"WebImage/SDWebImage.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98583254b6e10a610161162b2462c9a243\",\"path\":\"SDWebImage/Core/SDWebImageCacheKeyFilter.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d67745e1c95b9a3b0b6afc23ba0e53d1\",\"path\":\"SDWebImage/Core/SDWebImageCacheKeyFilter.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e6013c78eea89a230352d0a58977ee65\",\"path\":\"SDWebImage/Core/SDWebImageCacheSerializer.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c798c0650901d3f454e84b38b32b7c14\",\"path\":\"SDWebImage/Core/SDWebImageCacheSerializer.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987bcdc97ee957f7d3dcc46ccd3df087d6\",\"path\":\"SDWebImage/Core/SDWebImageCompat.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98175d683f5876f4bef28a5c568a03ee0e\",\"path\":\"SDWebImage/Core/SDWebImageCompat.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a1f302e052441cf2364ea9fdbb633665\",\"path\":\"SDWebImage/Core/SDWebImageDefine.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9801960d18f6952523241592bfcf8df74f\",\"path\":\"SDWebImage/Core/SDWebImageDefine.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98da783a4512ad557f5bed33b07819428b\",\"path\":\"SDWebImage/Core/SDWebImageDownloader.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e988bf2ba3046f2a1dc82c3ae3236bb5c29\",\"path\":\"SDWebImage/Core/SDWebImageDownloader.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987349aeb8db30aa64526eda7973f155b2\",\"path\":\"SDWebImage/Core/SDWebImageDownloaderConfig.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e987233e0726650facd69de48131fbfb887\",\"path\":\"SDWebImage/Core/SDWebImageDownloaderConfig.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e981f0f6fc0eca1c4df41989a0e01130390\",\"path\":\"SDWebImage/Core/SDWebImageDownloaderDecryptor.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b1e56d48b1b3c795cd043d008ac2d69f\",\"path\":\"SDWebImage/Core/SDWebImageDownloaderDecryptor.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e986735d6c802483fa77c3db72c66398b93\",\"path\":\"SDWebImage/Core/SDWebImageDownloaderOperation.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e986a5410248346eb3a5db888eeaf016ed3\",\"path\":\"SDWebImage/Core/SDWebImageDownloaderOperation.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980f2c0526259c08fb215f33283d062669\",\"path\":\"SDWebImage/Core/SDWebImageDownloaderRequestModifier.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e986771283dce17b4fb9bd4b5fe643ed3b9\",\"path\":\"SDWebImage/Core/SDWebImageDownloaderRequestModifier.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ae78c144f7dd1ffa4151c834e1856244\",\"path\":\"SDWebImage/Core/SDWebImageDownloaderResponseModifier.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cd959e9548caf1d3b10d572543028967\",\"path\":\"SDWebImage/Core/SDWebImageDownloaderResponseModifier.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e981f2f1b5fb7e7a0259d587693c70e17dc\",\"path\":\"SDWebImage/Core/SDWebImageError.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d7b5236ded30e2d2a5ff450a32f7bd62\",\"path\":\"SDWebImage/Core/SDWebImageError.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98db171e7f43254466428cd7d160d4bc66\",\"path\":\"SDWebImage/Core/SDWebImageIndicator.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e988059612109acc8adb78283f1ba5f6c8e\",\"path\":\"SDWebImage/Core/SDWebImageIndicator.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e989a4f6041648958d4aff810b46b83ec9d\",\"path\":\"SDWebImage/Core/SDWebImageManager.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e985ec38f9cb9f5d4978c0311d1008e04cc\",\"path\":\"SDWebImage/Core/SDWebImageManager.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e982c27b087e8cd840abc3ef59829b80efa\",\"path\":\"SDWebImage/Core/SDWebImageOperation.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e989ab8963206759569d84f30727fa4cba2\",\"path\":\"SDWebImage/Core/SDWebImageOperation.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b72cfadd9fa624c843ceaca86f14fba7\",\"path\":\"SDWebImage/Core/SDWebImageOptionsProcessor.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b5e7d24f1e11dbac304e87bddaff0bab\",\"path\":\"SDWebImage/Core/SDWebImageOptionsProcessor.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e982a13b238680429b9cb2b8bf0839d5f79\",\"path\":\"SDWebImage/Core/SDWebImagePrefetcher.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e985fdfabd035ec7a467b035a8ce350edd5\",\"path\":\"SDWebImage/Core/SDWebImagePrefetcher.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987fbe1b7021e7099d47e3d29bb0d246b0\",\"path\":\"SDWebImage/Core/SDWebImageTransition.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d898bb0c82f19d5c90770a13e1992bc4\",\"path\":\"SDWebImage/Core/SDWebImageTransition.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e46d3f1b4958c657b86076222797d146\",\"path\":\"SDWebImage/Private/SDWebImageTransitionInternal.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e989b6a95fea066801733ba0d9eb7781a8d\",\"path\":\"SDWebImage/Core/UIButton+WebCache.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a7d08c8d1537b994f19071149f068bb7\",\"path\":\"SDWebImage/Core/UIButton+WebCache.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9845d7911932c79e171dd089fd4628b5b8\",\"path\":\"SDWebImage/Private/UIColor+SDHexString.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98008deca9be811cf53f205b0ddcc4983a\",\"path\":\"SDWebImage/Private/UIColor+SDHexString.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e986bb62f370bbe234b6bd0d7c77da71782\",\"path\":\"SDWebImage/Core/UIImage+ExtendedCacheData.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98aa1127430a2307c0512ba13f3695fe9e\",\"path\":\"SDWebImage/Core/UIImage+ExtendedCacheData.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ccd7d7396bf3843012af71140da2a49c\",\"path\":\"SDWebImage/Core/UIImage+ForceDecode.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9863a4e0085b4eca2b418099edfec4340a\",\"path\":\"SDWebImage/Core/UIImage+ForceDecode.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b5c221940cf71976a9792c00ded2a77c\",\"path\":\"SDWebImage/Core/UIImage+GIF.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d05fd41d05faa2360aeaf46fb5065514\",\"path\":\"SDWebImage/Core/UIImage+GIF.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98662eda270b9081b770cb1f2a02e387b2\",\"path\":\"SDWebImage/Core/UIImage+MemoryCacheCost.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fd8dd526fbbf1c35d004173853ebc817\",\"path\":\"SDWebImage/Core/UIImage+MemoryCacheCost.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e986357ed01b566bb1d1be0954a632e35e0\",\"path\":\"SDWebImage/Core/UIImage+Metadata.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e986a263fb34dde4d01069f152b021094c7\",\"path\":\"SDWebImage/Core/UIImage+Metadata.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98deca6db8fdc22a81efbb6cd75f7c9144\",\"path\":\"SDWebImage/Core/UIImage+MultiFormat.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e981f8ce8389cf620585da7cd0d5ae68c6d\",\"path\":\"SDWebImage/Core/UIImage+MultiFormat.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f79a5a836cf1d081150335f28fe35761\",\"path\":\"SDWebImage/Core/UIImage+Transform.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c1367cac0329507a751b00959f8b1fe9\",\"path\":\"SDWebImage/Core/UIImage+Transform.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9836fad0c1818f3ebd7ea94ecdb2cda58b\",\"path\":\"SDWebImage/Core/UIImageView+HighlightedWebCache.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e869a02fe4a657b9ddb0f148af11b090\",\"path\":\"SDWebImage/Core/UIImageView+HighlightedWebCache.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fc1f6b2f5b528239f7eb374a16707dd2\",\"path\":\"SDWebImage/Core/UIImageView+WebCache.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e987a0efb32908f89fd6eaf93a60386fe37\",\"path\":\"SDWebImage/Core/UIImageView+WebCache.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d835b0cd54e8e5c3eefa756ca6a8cf4f\",\"path\":\"SDWebImage/Core/UIView+WebCache.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98da978efa2c20cf80e6a593952a64f214\",\"path\":\"SDWebImage/Core/UIView+WebCache.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e982999b5735c33706130ed303f0efe7372\",\"path\":\"SDWebImage/Core/UIView+WebCacheOperation.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e983963a49bc38ac28ca13543440440738c\",\"path\":\"SDWebImage/Core/UIView+WebCacheOperation.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986e6118052c889416524a66863865f2d2\",\"name\":\"Core\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b569f5963060c97f2f2ba38385fe0047\",\"path\":\"SDWebImage.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e582145da4c7bad45fd0fea722620980\",\"path\":\"SDWebImage-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f69556d85c791f9f7bb4864067304e9e\",\"path\":\"SDWebImage-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a5d7cf445f11b8a851603791bb80dccd\",\"path\":\"SDWebImage-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9849c916368f02890a1ae21d78a5082dce\",\"path\":\"SDWebImage-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cbf68d1f922bc4817004dd6a2700f369\",\"path\":\"SDWebImage.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e982a978c9504e7f820a9a62749b4639eb7\",\"path\":\"SDWebImage.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98c5c4f2160b2c64c56cc35e1563f69dfa\",\"name\":\"Support Files\",\"path\":\"../Target Support Files/SDWebImage\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e989ab633a406a0ba749c8183ca48f70ca1\",\"name\":\"SDWebImage\",\"path\":\"SDWebImage\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"children\":[{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98198cb7323cf38f49c099a19af424e38e\",\"path\":\"Sources/Swift/Metrics/BucketsMetricsAggregator.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9820b096789e7bd1bfa4b58fa13bc5b50f\",\"path\":\"Sources/Swift/Metrics/CounterMetric.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98853fa02d04d0430ad3f2d7b333874af4\",\"path\":\"Sources/Swift/Metrics/DistributionMetric.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ac0780da839edf981809953008673060\",\"path\":\"Sources/Swift/Metrics/EncodeMetrics.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e982bd4f7e001b4dd1cf079308ca514554b\",\"path\":\"Sources/Swift/Metrics/GaugeMetric.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9824d6ee3483a76d2eb18b2c33833c5c8a\",\"path\":\"Sources/Swift/Tools/HTTPHeaderSanitizer.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98382df2b05839d9ea3f03c148af3ae692\",\"path\":\"Sources/Swift/Metrics/LocalMetricsAggregator.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e981b4b3945f043de125e7b8c8aaeacc7c3\",\"path\":\"Sources/Swift/Metrics/Metric.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9830b09011eccca71f62b2f66238867f76\",\"path\":\"Sources/Swift/Metrics/MetricsAggregator.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98aa15c1b3d311498677dcd94b75280f47\",\"path\":\"Sources/Sentry/include/NSArray+SentrySanitize.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e982ad87d7afe24ffd2d6bd213296aea811\",\"path\":\"Sources/Sentry/NSArray+SentrySanitize.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f4b859c713f2344c6fc1477b46cbafeb\",\"path\":\"Sources/Sentry/include/NSLocale+Sentry.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e987f7b4c8e450d6bc934891b572509dd5c\",\"path\":\"Sources/Sentry/NSLocale+Sentry.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b9346543c1b98901f22f3c7e23aff838\",\"path\":\"Sources/Swift/Extensions/NSLock.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e989b00bcbca3893520fc4f39ff18d0b1f5\",\"path\":\"Sources/Sentry/include/NSMutableDictionary+Sentry.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98902ebd6036a41aaacac357ee5ccce1b9\",\"path\":\"Sources/Sentry/NSMutableDictionary+Sentry.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9885b82b46b9dd005b768f8ef3c751040f\",\"path\":\"Sources/Swift/Extensions/NumberExtensions.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a716e75ccb528744d3ffde348c74054d\",\"path\":\"Sources/Sentry/include/HybridPublic/PrivateSentrySDKOnly.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.objcpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e9810ac796ebca563e59fec2d96f029829c\",\"path\":\"Sources/Sentry/PrivateSentrySDKOnly.mm\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980ed4d648fc835701dd40045ac4f0eb42\",\"path\":\"Sources/Sentry/include/HybridPublic/PrivatesHeader.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9825d3d80b5d5f47d8c3577be50d208f17\",\"path\":\"Sources/Sentry/Public/Sentry.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9881e78b26fce5be69601e8f39debf1815\",\"path\":\"Sources/Sentry/include/SentryANRTracker.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e988933b4bc3332ec51f502704c510ed48f\",\"path\":\"Sources/Sentry/SentryANRTracker.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c4aec8c696347d8a5fd3da2c9ae3b34b\",\"path\":\"Sources/Sentry/include/SentryANRTrackerV2.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98143f06533f29ffe1f4e62e249ee7c61a\",\"path\":\"Sources/Sentry/SentryANRTrackerV2.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9805b65a9fd5a17f8804adb0010a2bb994\",\"path\":\"Sources/Swift/Integrations/ANR/SentryANRTrackerV2Delegate.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a9377495765c6b05f4938e987beaecc4\",\"path\":\"Sources/Sentry/include/SentryANRTrackingIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9803a9b0b910a8e69ce5d48707b2f7b0ff\",\"path\":\"Sources/Sentry/SentryANRTrackingIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9853bb97ef1af68a2e8a6cd65845220a21\",\"path\":\"Sources/Sentry/include/SentryANRTrackingIntegrationV2.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f85156689aeaf18e00084edae9890433\",\"path\":\"Sources/Sentry/SentryANRTrackingIntegrationV2.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e982f3eaf81b6a498c005395bd74ff9b40c\",\"path\":\"Sources/Sentry/include/HybridPublic/SentryAppStartMeasurement.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98573cf1fba6ec7158ac9e5d30872d3358\",\"path\":\"Sources/Sentry/SentryAppStartMeasurement.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98da6d3a4aa155bc2b99c1c5776fa1d7ab\",\"path\":\"Sources/Sentry/include/SentryAppStartTracker.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9858a59bcb56abe22c97af586957612f8a\",\"path\":\"Sources/Sentry/SentryAppStartTracker.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9897bfe385980cf04d6052b9a9a48626bd\",\"path\":\"Sources/Sentry/include/SentryAppStartTrackingIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e984c7e16723e7d6c5950d220f08a0f3a1b\",\"path\":\"Sources/Sentry/SentryAppStartTrackingIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98711c5fa9ee620eb44232227aea5b4980\",\"path\":\"Sources/Sentry/include/SentryAppState.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e102ca54129581ea6ac1fd19ca2f900d\",\"path\":\"Sources/Sentry/SentryAppState.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f4aff5850320b2892171d2f9050e54f0\",\"path\":\"Sources/Sentry/include/SentryAppStateManager.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98945bba539ff28910dddfef800d776212\",\"path\":\"Sources/Sentry/SentryAppStateManager.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c7abc40702780507f67178f200c91e33\",\"path\":\"Sources/Sentry/include/SentryAsynchronousOperation.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f2c93c0f5d5274544f463c3dd147b69e\",\"path\":\"Sources/Sentry/SentryAsynchronousOperation.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e9848c4b2580de96341fc0bfc5e6780e93a\",\"path\":\"Sources/Sentry/SentryAsyncSafeLog.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98503f56b704c70eeeaa87f393ca606f46\",\"path\":\"Sources/Sentry/SentryAsyncSafeLog.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9839b12ad72defb9e70cd9c490eab0f56b\",\"path\":\"Sources/Sentry/Public/SentryAttachment.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9817625a85b70840a0b0718c986516528e\",\"path\":\"Sources/Sentry/SentryAttachment.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98999febd8a8da745b5ea4843c406440a6\",\"path\":\"Sources/Sentry/include/SentryAttachment+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980c19c6b2f6ae9fa9cbfb42827276a528\",\"path\":\"Sources/Sentry/include/SentryAutoBreadcrumbTrackingIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e982375cf366eb9fa65b8bba986be274ede\",\"path\":\"Sources/Sentry/SentryAutoBreadcrumbTrackingIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e986e0e69f1089a15308ed825159b9e98f7\",\"path\":\"Sources/Sentry/include/SentryAutoSessionTrackingIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a4037e5e9e7feba04419a743f2be779b\",\"path\":\"Sources/Sentry/SentryAutoSessionTrackingIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.cpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e981de7f79c30c8772c5fa58d8ea6d1b1b6\",\"path\":\"Sources/Sentry/SentryBacktrace.cpp\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9891ba718e9d3cff3ab8d9bfee8bd0462b\",\"path\":\"Sources/Sentry/include/SentryBacktrace.hpp\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e989f61aa00b8a1516208dca7f64cd6a75c\",\"path\":\"Sources/Sentry/Public/SentryBaggage.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b20f7baef1201383ed42eb1c42b1ef2a\",\"path\":\"Sources/Sentry/SentryBaggage.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e983e4f391d969af2e5ce4e2e2dd4a41dfb\",\"path\":\"Sources/Swift/Helper/SentryBaggageSerialization.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e985a62059eff00244af5c87c6ddd480877\",\"path\":\"Sources/Sentry/include/SentryBaseIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e987a9f6073313425fbba5130148b9b3a65\",\"path\":\"Sources/Sentry/SentryBaseIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ccba6be3c8cd08597e295bd9856dd7e3\",\"path\":\"Sources/Sentry/include/HybridPublic/SentryBinaryImageCache.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98963cbdfa2428d743619bf4f2c4cdf206\",\"path\":\"Sources/Sentry/SentryBinaryImageCache.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98963224f46996165eb8c62129394cfe81\",\"path\":\"Sources/Sentry/Public/SentryBreadcrumb.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e6b4f1738c9e7b78c11959faf476bb67\",\"path\":\"Sources/Sentry/SentryBreadcrumb.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98647433b4e086798b973e947311127f7c\",\"path\":\"Sources/Sentry/include/HybridPublic/SentryBreadcrumb+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b337f47392c6e73cb95920efdda0b675\",\"path\":\"Sources/Sentry/include/SentryBreadcrumbDelegate.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fd297ac79b8c53f88805194c653eb324\",\"path\":\"Sources/Sentry/include/SentryBreadcrumbTracker.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98917b84e096fb7a3cee094b304c0d1f1e\",\"path\":\"Sources/Sentry/SentryBreadcrumbTracker.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987869ca5f51cd67f10fb06cf6d2a072e1\",\"path\":\"Sources/Sentry/include/SentryBuildAppStartSpans.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b16fdba02a928575106b9d1ac1686733\",\"path\":\"Sources/Sentry/SentryBuildAppStartSpans.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9899a99bf57fcbf0c333b505bdb2e414cc\",\"path\":\"Sources/Sentry/include/SentryByteCountFormatter.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e29dfdbdcbf120e148ffa7d4a4c248a4\",\"path\":\"Sources/Sentry/SentryByteCountFormatter.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98be06b9e44be8b3f68cdabea1eab410fb\",\"path\":\"Sources/Sentry/Public/SentryClient.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e981f87b3406b001d130316dbc8ada38a00\",\"path\":\"Sources/Sentry/SentryClient.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987b01f905126992881fc2dce8d8608866\",\"path\":\"Sources/Sentry/include/SentryClient+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987f499c7f3f52d80336b1b533a1394782\",\"path\":\"Sources/Sentry/include/SentryClientReport.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a4dc6d699fd7d93e90a761eda46c4f64\",\"path\":\"Sources/Sentry/SentryClientReport.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9830ba7417bb270438177deb73683c5536\",\"path\":\"Sources/Sentry/include/SentryCompiler.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e985f16ffcf4dfbc88b10ce30fd3e633708\",\"path\":\"Sources/Sentry/include/SentryConcurrentRateLimitsDictionary.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98869ae30e47af8e56237caf99320cf284\",\"path\":\"Sources/Sentry/SentryConcurrentRateLimitsDictionary.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f66f182cfe191dbc0994d7db36647922\",\"path\":\"Sources/Sentry/include/SentryContinuousProfiler.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.objcpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fe03e1800d75ed025acc7c9f643b8f04\",\"path\":\"Sources/Sentry/Profiling/SentryContinuousProfiler.mm\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9860162861b3210d454479d7e1ac71d658\",\"path\":\"Sources/Sentry/include/SentryCoreDataSwizzling.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9828efb6a05eb4ef1519bb559204168266\",\"path\":\"Sources/Sentry/SentryCoreDataSwizzling.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9813f1af6eada9adbb72a6632c8d2b2829\",\"path\":\"Sources/Sentry/include/SentryCoreDataTracker.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e980e9d7ced294ae24069ba05792c403f21\",\"path\":\"Sources/Sentry/SentryCoreDataTracker.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98373aa59d4a768c9ebed68430e50f9fd4\",\"path\":\"Sources/Sentry/include/SentryCoreDataTrackingIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d656120ee1d06ad362116f5cd2c4da69\",\"path\":\"Sources/Sentry/SentryCoreDataTrackingIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98971753585da08da03da7583249217f27\",\"path\":\"Sources/Sentry/include/SentryCPU.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e71ce65934ced440e8087b210d26e9dd\",\"path\":\"Sources/SentryCrash/Recording/SentryCrash.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9810ab6f5523c4427b61de387291c7a269\",\"path\":\"Sources/SentryCrash/Recording/SentryCrash.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e983e0adaf7be093e8acf73fd7ca3cd8020\",\"path\":\"Sources/SentryCrash/Recording/SentryCrashBinaryImageCache.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e986c59b016150adc6042a09462cf5f2949\",\"path\":\"Sources/SentryCrash/Recording/SentryCrashBinaryImageCache.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ba2cf898e20c56ff809039433c33cfd0\",\"path\":\"Sources/Sentry/include/SentryCrashBinaryImageProvider.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e982f6e693d7a2699f92a735b00a3b748ec\",\"path\":\"Sources/SentryCrash/Recording/SentryCrashC.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e988880cf5ad48383745a739a9820534574\",\"path\":\"Sources/SentryCrash/Recording/SentryCrashC.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e981c394d73a0449af8d0295e4f249392e4\",\"path\":\"Sources/SentryCrash/Recording/SentryCrashCachedData.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e984943986cd47c7fbf04e945d0f7ce23dc\",\"path\":\"Sources/SentryCrash/Recording/SentryCrashCachedData.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e985af7183f551d475ec21df7099e52ba19\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashCPU.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9819e75d68bf8cdd116d83607aa0c87f45\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashCPU.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a8b934d46e8ace1a43b18243a1180d46\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashCPU_Apple.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e986e6252e39b90f3ce76d83e5d6c3974b2\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashCPU_arm.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a418c37e4b8f0cae47ee4e0ef8a63b52\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashCPU_arm64.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e9854f64306da17dca29bd7ff5a1c475bbe\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashCPU_x86_32.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e9858b9351f994445a2cced9788b61b0857\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashCPU_x86_64.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e9815c5e7deb3496c383b03ad2cb8c63c54\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashDate.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98920a4695f60aba5f7a2d55e19953a7c3\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashDate.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e982e916154b2f4a8ee629c521943af2be5\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashDebug.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fa25002e2a2a304eb735465f026645f3\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashDebug.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9862a562ca672b78057b80f0d6b1381746\",\"path\":\"Sources/Sentry/include/SentryCrashDefaultBinaryImageProvider.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e986d3ae0f245877d1a34a61568db103431\",\"path\":\"Sources/Sentry/SentryCrashDefaultBinaryImageProvider.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e989f539a8dd7e053b0be13bac966b57726\",\"path\":\"Sources/Sentry/include/SentryCrashDefaultMachineContextWrapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98834119e584475e9bbe8dde34e6124eed\",\"path\":\"Sources/Sentry/SentryCrashDefaultMachineContextWrapper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987c323ed9eabfe9ca185b337c9308d491\",\"path\":\"Sources/SentryCrash/Recording/SentryCrashDoctor.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e86a5d681f8907d2cfc0b4615cae7aa6\",\"path\":\"Sources/SentryCrash/Recording/SentryCrashDoctor.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e98872202ab332a20c50d416271f9d2372a\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashDynamicLinker.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ba5fbfcc911db7d4ae22629456fd9144\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashDynamicLinker.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98328deee97d6c62d2e5b511634b98f69a\",\"path\":\"Sources/Sentry/Public/SentryCrashExceptionApplication.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ced46e42add5fe763f983e5bb41852eb\",\"path\":\"Sources/Sentry/SentryCrashExceptionApplication.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e9851df5069bd644d351f378d5a24e99531\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashFileUtils.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ee672a6996837477d96028b68c1168d1\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashFileUtils.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e9884856fe388c064570227a322157015ee\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashID.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9895df3868e47b3489c27cba402f91a418\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashID.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98eafd27653478c04bf42d5a09c9475684\",\"path\":\"Sources/SentryCrash/Installations/SentryCrashInstallation.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98785810b1ca7d160b372c70ed55188bdb\",\"path\":\"Sources/SentryCrash/Installations/SentryCrashInstallation.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98789ed62e82862651822eb9e968b4183b\",\"path\":\"Sources/SentryCrash/Installations/SentryCrashInstallation+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987b19143bc7df2526be6323a84f13d0cd\",\"path\":\"Sources/Sentry/include/SentryCrashInstallationReporter.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9843bd4e1a8c710f4388f1611823aa8703\",\"path\":\"Sources/Sentry/SentryCrashInstallationReporter.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fa237d860d80a5137ce637c70f1d45ae\",\"path\":\"Sources/Sentry/include/SentryCrashIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e984f9135a1bacbd9b7d700d830329cfa18\",\"path\":\"Sources/Sentry/SentryCrashIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ed40eb6a8415d9209bc656e3e9003d20\",\"path\":\"Sources/Sentry/include/SentryCrashIsAppImage.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e983ace9e6326466c6006d7903e835f2298\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e984db0ba7ef8f9c62a4768e1244e0175a5\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980949e62d558d1222c68ac80103de072a\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodecObjC.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fb551adfbd11861983f13d4ea4ac5672\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodecObjC.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ebe01a592e2d14b2852a1fb5fbd2aec6\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashMach.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e63f4dd5624ba45cbd9550973bc7a64b\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashMach.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ac0ec3dc837141d75865164b98357241\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashMachineContext.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e982d7b304e56d6074933295f9cbe624568\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashMachineContext.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b5dcb1a00343dc6a7015edc3f087c60a\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashMachineContext_Apple.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9890a37d86794ca14855675168cd3383ad\",\"path\":\"Sources/Sentry/include/SentryCrashMachineContextWrapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e982bc91af71f89cf0a1a1788830d8adeba\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashMemory.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98bad5c89792bfd17e382bb695ad8ed3f7\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashMemory.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cb3e637b4b9c0fd20f7ea80210d8b3c8\",\"path\":\"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f6864c5d37412b769927b5d5c74fe602\",\"path\":\"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e981c4aa5985300ab845f08441631c136e0\",\"path\":\"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_AppState.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e986db33830a858c9794d31e24b7b5cdb20\",\"path\":\"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_AppState.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.cpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e98928789958ea08294471edb78122163bf\",\"path\":\"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d88503a787c4bef8d3f64a16f8ac7431\",\"path\":\"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e985631641a5bf876e0a4464325cd89f9f0\",\"path\":\"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_MachException.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f9cccecbb59fac96ef00765633935089\",\"path\":\"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_MachException.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e981f0f66e36982c6f19b8196b5549c1151\",\"path\":\"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_NSException.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9854c3e488a79f2baa48bf1bde77c46cd4\",\"path\":\"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_NSException.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fc0ee9ce543fb5029fa8d77a33a2a927\",\"path\":\"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ef5045f7ebe869b4e0ded07a2e0b854f\",\"path\":\"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e986d521257b90b2beb37d3cbeb2b852f91\",\"path\":\"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_System.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e989b790d6fb8d01f7933ddbca6b6c2e119\",\"path\":\"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_System.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b15723d60c434030589a9c72cc466027\",\"path\":\"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitorContext.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e984023b76db5ad50719b293030bedfa158\",\"path\":\"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitorType.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e0d4db171ade52ab9810b83e9e46027a\",\"path\":\"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitorType.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9858f76db325762543559f151e9cde7092\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashNSErrorUtil.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98036351eb99bc61418fa7904beb886a4b\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashNSErrorUtil.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ec8643356b925433575de2c29645e403\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashObjC.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d43045f94dfb5f8084c33ff0795a4fb3\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashObjC.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e983ff24760fdf3ff3241a74acaab8abd66\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashObjCApple.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e52be506e681f88f6f6ca68d2f91cc4e\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashPlatformSpecificDefines.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ebb3d6360d98f69e909523c8039f5805\",\"path\":\"Sources/SentryCrash/Recording/SentryCrashReport.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9882eca2c2727b3da70dc9991523763b33\",\"path\":\"Sources/SentryCrash/Recording/SentryCrashReport.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9857e1affb989c53e044956e4452fdbcea\",\"path\":\"Sources/Sentry/include/SentryCrashReportConverter.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e982deeb46c4817dcdcd9ac3ff4d08481d4\",\"path\":\"Sources/Sentry/SentryCrashReportConverter.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e982aad48539d3cdacf06eba4d8e46a2584\",\"path\":\"Sources/SentryCrash/Recording/SentryCrashReportFields.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e981e9d85b65beaeba8501a1c62b8febe5b\",\"path\":\"Sources/SentryCrash/Reporting/Filters/SentryCrashReportFilter.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980d8271e53cbfcf85f6cd065e708f2de2\",\"path\":\"Sources/SentryCrash/Reporting/Filters/SentryCrashReportFilterBasic.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9840d849ddebfb811a2974576cbaeecf80\",\"path\":\"Sources/SentryCrash/Reporting/Filters/SentryCrashReportFilterBasic.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a9d5de5d3dfe92644037d71ee5190757\",\"path\":\"Sources/SentryCrash/Recording/SentryCrashReportFixer.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f69b87a8cdb3abcbac109f20e35935f7\",\"path\":\"Sources/SentryCrash/Recording/SentryCrashReportFixer.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9875fc97b0b53f66ab6b047f69319bae1f\",\"path\":\"Sources/Sentry/include/SentryCrashReportSink.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f0c689d1974688e39dbfb4912fa84a98\",\"path\":\"Sources/Sentry/SentryCrashReportSink.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e9896c2e55b5af11c135ee2e941bdd0d7f7\",\"path\":\"Sources/SentryCrash/Recording/SentryCrashReportStore.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9837c90d38984770cab39b73a4ada41ded\",\"path\":\"Sources/SentryCrash/Recording/SentryCrashReportStore.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f92815bb8d5e910d80c83c261ef2665a\",\"path\":\"Sources/SentryCrash/Recording/SentryCrashReportVersion.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e982b8d6627048f9dc97cf8b1ea4105f91b\",\"path\":\"Sources/SentryCrash/Recording/SentryCrashReportWriter.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d078af0694b1dfb7d9ccde999b44e4fb\",\"path\":\"Sources/Sentry/include/SentryCrashScopeObserver.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98078b4b6983c2612f332996e0802c4c78\",\"path\":\"Sources/Sentry/SentryCrashScopeObserver.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fc0f9f2e8fb1eb0f1596f10d50e88f2f\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashSignalInfo.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e983af198c0f2730501f9ce1047fe9febf5\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashSignalInfo.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e982f50e0ec35d87b0e7665d862c74ad244\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980ab8a94243d149d379bbd54e70a367d2\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e989255de6ce3a4e6431e337169ea12941c\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_Backtrace.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98227f0ab7c55a7b2a8c57667847371c5c\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_Backtrace.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e981ded049e9550dae6bfea340e4044795f\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_MachineContext.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e985481f9043d0f6eec88a8931509d27ca7\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_MachineContext.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98172b9a0d48d2f67d576708e2c15c5681\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_SelfThread.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e980331a56274b42b435802d99adc0f0030\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_SelfThread.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98616e193ad92d875887b6acd5f702b997\",\"path\":\"Sources/Sentry/include/SentryCrashStackEntryMapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fcbe81bbd35a2c2ad89d2e20c97dcc80\",\"path\":\"Sources/Sentry/SentryCrashStackEntryMapper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cac8c3e4cc76215e854ea8e9fd0b4fbd\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashString.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e982bc1b6754e56222800d995cd75f8c2bb\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashString.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e542917dc6ed22f0dc8928e137282679\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashSymbolicator.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9842aa558befabfc401dc6c66d86529e41\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashSymbolicator.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e9805e8aec9ac44b4f557bd1c2c0ed33d7e\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashSysCtl.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987d038ee2ee6d13d961dcc8f8cef0e8f9\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashSysCtl.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e9814ac97981888d650562ae1699fbed050\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashThread.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e983761e96361f55650fcff3ec415c306c0\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashThread.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cf8824f371b9de403b95fafde5e63a58\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashUUIDConversion.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9824542f73d2e98c60530e9c276356c21d\",\"path\":\"Sources/SentryCrash/Recording/Tools/SentryCrashUUIDConversion.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f8bc45a2584bffecf7c9393d70e3ac1b\",\"path\":\"Sources/SentryCrash/Reporting/Filters/Tools/SentryCrashVarArgs.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d8def6fa7b0dff52e0288684681cd235\",\"path\":\"Sources/Sentry/include/SentryCrashWrapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98844dfbd5d2a8a41ef38196b8c228710e\",\"path\":\"Sources/Sentry/SentryCrashWrapper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98beaf5cf161791d03e65fca4463133522\",\"path\":\"Sources/Swift/Helper/SentryCurrentDateProvider.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9820c8946792aadcff4c6b7911cd3509b1\",\"path\":\"Sources/Sentry/include/SentryDataCategory.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9897ecba504c62f71a990fd084fdd0aa08\",\"path\":\"Sources/Sentry/include/SentryDataCategoryMapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98993a135e0861037578f65e78f6f19fa5\",\"path\":\"Sources/Sentry/SentryDataCategoryMapper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fb2c2fb737cef61e3aa32bc173200734\",\"path\":\"Sources/Sentry/include/SentryDateUtil.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e980da3c3a8a5a8e75a2b495f25b864ffff\",\"path\":\"Sources/Sentry/SentryDateUtil.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e982cfb6141f563ef3b224e37c23dfd6ee7\",\"path\":\"Sources/Sentry/include/SentryDateUtils.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e981e501be4e9e4ce930febba0a67b92f83\",\"path\":\"Sources/Sentry/SentryDateUtils.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e986de643aa7e4d68dd257a682eb4ecf094\",\"path\":\"Sources/Sentry/Public/SentryDebugImageProvider.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e988714d874b4c7d300f8820a9568aac3b9\",\"path\":\"Sources/Sentry/SentryDebugImageProvider.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ccd67327f2cef046d47a060a50e2b033\",\"path\":\"Sources/Sentry/Public/SentryDebugMeta.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d62e6ab2a2d7467600f6feb7e2671a52\",\"path\":\"Sources/Sentry/SentryDebugMeta.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98057cb7e081eab9c13bc45a99e2046c64\",\"path\":\"Sources/Sentry/include/SentryDefaultObjCRuntimeWrapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98755befdde596ed98608c0f49e465f2e9\",\"path\":\"Sources/Sentry/SentryDefaultObjCRuntimeWrapper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987fb8b09d68a8675e669ccaa2b3d70f8b\",\"path\":\"Sources/Sentry/include/SentryDefaultRateLimits.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98284e598b5703764339c9c4dd2d39c047\",\"path\":\"Sources/Sentry/SentryDefaultRateLimits.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a8b66c1f8b8fe6afdb3bd5083ac798dd\",\"path\":\"Sources/Sentry/Public/SentryDefines.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e982b9ddd57b7d448702427527f15078729\",\"path\":\"Sources/Sentry/include/SentryDelayedFrame.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b80297ad046a9e2ee2fc01b38f886866\",\"path\":\"Sources/Sentry/SentryDelayedFrame.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c933c980d9fd2d6b18ab7660be826a32\",\"path\":\"Sources/Sentry/include/SentryDelayedFramesTracker.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9883f0c58fdda69214b888d7a89386438d\",\"path\":\"Sources/Sentry/SentryDelayedFramesTracker.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e988e3fca574186ef9a3fbf60c7936614dd\",\"path\":\"Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e988e075ecfc8e39865eebcf99287896107\",\"path\":\"Sources/Sentry/SentryDependencyContainer.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ab472310b921cb357b3403f16d6bcc91\",\"path\":\"Sources/Sentry/include/SentryDevice.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.objcpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ce8faf244cb4f73da113703dbb0fd493\",\"path\":\"Sources/Sentry/SentryDevice.mm\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f496c08b830fbf3baf100455494f579e\",\"path\":\"Sources/SentryCrash/Reporting/Filters/Tools/SentryDictionaryDeepSearch.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ed54e2f0091c3b6700b4882495308ca7\",\"path\":\"Sources/SentryCrash/Reporting/Filters/Tools/SentryDictionaryDeepSearch.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c50f7a6b19aff7f4ca9db7611b185ffc\",\"path\":\"Sources/Sentry/include/SentryDiscardedEvent.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e982b532fff8e4b1e30be209e64ad5f2328\",\"path\":\"Sources/Sentry/SentryDiscardedEvent.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e989c9e6c345c44f79bfb78251b2dbb8c7b\",\"path\":\"Sources/Sentry/include/SentryDiscardReason.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c183e6d53e8117f062c3f4b7cd156530\",\"path\":\"Sources/Sentry/include/SentryDiscardReasonMapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e986c8a3f333ed0b95751e7fbf78d7c98b9\",\"path\":\"Sources/Sentry/SentryDiscardReasonMapper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98bba374cd8c0dc6a0b88cd9da50d55613\",\"path\":\"Sources/Sentry/include/SentryDispatchFactory.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b61addc447754e7e5ec64e70a8a8d043\",\"path\":\"Sources/Sentry/SentryDispatchFactory.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98181ef37baebf7620fdb3ed512d9c5b2c\",\"path\":\"Sources/Sentry/include/SentryDispatchQueueWrapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fdfd3d660615791f5b19cb7a270b3d33\",\"path\":\"Sources/Sentry/SentryDispatchQueueWrapper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98edef8192fe71dc5c3aa5e7605d8bc231\",\"path\":\"Sources/Sentry/include/SentryDispatchSourceWrapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ad912bcb51aa320ac7d9ae0d6b1c29d0\",\"path\":\"Sources/Sentry/SentryDispatchSourceWrapper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e985d9e7a648cec7e7fc0ad3cf75672ea68\",\"path\":\"Sources/Sentry/include/SentryDisplayLinkWrapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cebef9f2b8583ad43ec280bc3f601765\",\"path\":\"Sources/Sentry/include/SentryDisplayLinkWrapper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98eb4e19f7af34fa2f92906307734634ab\",\"path\":\"Sources/Sentry/Public/SentryDsn.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98bc58b953a2bf19f0ba887c35a848c43d\",\"path\":\"Sources/Sentry/SentryDsn.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98629100bfbc75470514774b17e16c6ed2\",\"path\":\"Sources/Swift/Helper/SentryEnabledFeaturesBuilder.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f5d9dabdb733a787122fe8ba4ca78980\",\"path\":\"Sources/Sentry/include/HybridPublic/SentryEnvelope.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e989ef35f84679b5286380254cf3782d3fc\",\"path\":\"Sources/Sentry/SentryEnvelope.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e982792cab6ec953388de02a2ff6935ac57\",\"path\":\"Sources/Sentry/include/SentryEnvelope+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f6cedb8fc664cf2a66556a10e0939328\",\"path\":\"Sources/Sentry/include/SentryEnvelopeAttachmentHeader.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e987e289e99946da7963f37deadbe959fdd\",\"path\":\"Sources/Sentry/SentryEnvelopeAttachmentHeader.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98141370ca334e2965d382e9f5cee9eb40\",\"path\":\"Sources/Sentry/Public/SentryEnvelopeItemHeader.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e987942c3b2f4ab61de1e45f7316786b84c\",\"path\":\"Sources/Sentry/SentryEnvelopeItemHeader.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98781a30887bd009417ad1a4112c2fb3fe\",\"path\":\"Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9895aa5c265104e54b26110f7557073d7b\",\"path\":\"Sources/Sentry/include/SentryEnvelopeRateLimit.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d5a56c7cb96529d8791d765d11773537\",\"path\":\"Sources/Sentry/SentryEnvelopeRateLimit.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98bafc78f7024b690ecf9471384a9902b4\",\"path\":\"Sources/Sentry/Public/SentryError.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.objcpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ca1115e3b61c2229c1c80bd36f06de1b\",\"path\":\"Sources/Sentry/SentryError.mm\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98413720c3decab33490a7c5172007d190\",\"path\":\"Sources/Sentry/Public/SentryEvent.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e983743f3d27289e139c0b954aaa4e0628a\",\"path\":\"Sources/Sentry/SentryEvent.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a8f47a674006a0bd05b6739dc62b4f4f\",\"path\":\"Sources/Sentry/include/SentryEvent+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987159465a79e7a721e0255b49a250409e\",\"path\":\"Sources/Sentry/Public/SentryException.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9895825e81ff4e9350316aa59835550564\",\"path\":\"Sources/Sentry/SentryException.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9862683b73d213711a5794050fbedb940e\",\"path\":\"Sources/Swift/SentryExperimentalOptions.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a207c835059cd1e43b5179b933097323\",\"path\":\"Sources/Sentry/SentryExtraContextProvider.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e15159df27cf33be5cf5b699238e167f\",\"path\":\"Sources/Sentry/SentryExtraContextProvider.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e61558513e2bb03c67587242c1ae2320\",\"path\":\"Sources/Swift/Helper/SentryFileContents.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9891743b33ff8051d960b0188178ff8d18\",\"path\":\"Sources/Sentry/include/SentryFileIOTrackingIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9811f125cb4ea42b0ee92dca25f4b88671\",\"path\":\"Sources/Sentry/SentryFileIOTrackingIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c2c1ff1a63d933d631b4428ce2e38740\",\"path\":\"Sources/Sentry/include/SentryFileManager.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e987dc757a9b6a393b594b0e7f493805852\",\"path\":\"Sources/Sentry/SentryFileManager.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c2ef2bd076cd4dd059f7e298bf852014\",\"path\":\"Sources/Sentry/include/HybridPublic/SentryFormatter.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9852b362bca7cc0c9e4f0ac4357b8926c2\",\"path\":\"Sources/Sentry/Public/SentryFrame.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cb0c8f2e66fc06982ca30a2cf3eb9bfc\",\"path\":\"Sources/Sentry/SentryFrame.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e981a64e72c9b8c3c97fd22013edcdae5ca\",\"path\":\"Sources/Sentry/include/SentryFrameRemover.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9887e6e078e6301374b2fef621090a7c44\",\"path\":\"Sources/Sentry/SentryFrameRemover.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98dba5b9e2f053ff10581a7a52a1336881\",\"path\":\"Sources/Swift/Integrations/FramesTracking/SentryFramesDelayResult.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980aa82f1dc751a60b99dfdc678c1d417c\",\"path\":\"Sources/Sentry/include/HybridPublic/SentryFramesTracker.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9851e95210c8e69b336ec75546cb826a45\",\"path\":\"Sources/Sentry/SentryFramesTracker.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98efaa72ef40eaa86dff924769099fe63f\",\"path\":\"Sources/Sentry/include/SentryFramesTrackingIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98732b4c467888ee6e063ecfd31db00fc1\",\"path\":\"Sources/Sentry/SentryFramesTrackingIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9891ed9d2029d00cffa36a1f5fc06398a1\",\"path\":\"Sources/Sentry/Public/SentryGeo.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e988d6d449e12ef4dade75902629ee9c5e5\",\"path\":\"Sources/Sentry/SentryGeo.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ca8e4cce263f1f47c0f162e4c586c4f8\",\"path\":\"Sources/Sentry/include/SentryGlobalEventProcessor.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ed988e9fdf3783f895274a6a759ea0e0\",\"path\":\"Sources/Sentry/SentryGlobalEventProcessor.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e986e3e378fc6f7cc204b68b3caa7236f27\",\"path\":\"Sources/Sentry/include/SentryHttpDateParser.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a9f385e05164f46553c2ee2cf481b7d4\",\"path\":\"Sources/Sentry/SentryHttpDateParser.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c3bfc35223f1b5b6943049d2fb6c4389\",\"path\":\"Sources/Sentry/Public/SentryHttpStatusCodeRange.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98768627ada7e32a0631bc8af27cb1d1ff\",\"path\":\"Sources/Sentry/SentryHttpStatusCodeRange.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cca6fefe9cb49705f068665278f8ac02\",\"path\":\"Sources/Sentry/include/SentryHttpStatusCodeRange+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e989de0a39761e8a1050f2257cbe8713a04\",\"path\":\"Sources/Sentry/include/SentryHttpTransport.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e985ee2ceb1a7cef57f920d5af9053a5d61\",\"path\":\"Sources/Sentry/SentryHttpTransport.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98836f5531819312d8b361c85d8e8b18a4\",\"path\":\"Sources/Sentry/Public/SentryHub.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98252ac7b4e2a6feb7cd60fdd1a4f09dc4\",\"path\":\"Sources/Sentry/SentryHub.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98638a129b104a232f59a5551902b1f3f4\",\"path\":\"Sources/Sentry/include/SentryHub+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98118a67a7239494acc31abf04261df084\",\"path\":\"Sources/Swift/Protocol/SentryId.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98070046bf0c41b80e5921e2353abd1714\",\"path\":\"Sources/Sentry/include/SentryInAppLogic.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c06ead7e0dbabb3e7c310df0e456432b\",\"path\":\"Sources/Sentry/SentryInAppLogic.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e38ea514f750e6ef90ab207cb16028c2\",\"path\":\"Sources/Sentry/include/SentryInstallation.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e981b188529a60da3e88d8e87a3cef822ea\",\"path\":\"Sources/Sentry/SentryInstallation.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f1ecb074aa05d438550e8283fcf8840e\",\"path\":\"Sources/Swift/Protocol/SentryIntegrationProtocol.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9880707aac6a0b662d864e53db8ec3e873\",\"path\":\"Sources/Sentry/include/SentryInternalCDefines.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b48ba94d494b08c87d61b93a678e3dd2\",\"path\":\"Sources/Sentry/include/SentryInternalDefines.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9827d2c767a3050b20c39203308cb87f8d\",\"path\":\"Sources/Sentry/include/SentryInternalNotificationNames.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9849c7220f6bfe0eeb615b9a7accf8b533\",\"path\":\"Sources/Sentry/include/SentryInternalSerializable.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98def9cc150a565d2550979ec5b69998b1\",\"path\":\"Sources/Sentry/include/SentryLaunchProfiling.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98911fdc6061a2135c748153f2e4a05aa3\",\"path\":\"Sources/Sentry/Profiling/SentryLaunchProfiling.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e989df9dc056cea8fcf0f59af8f85cd8282\",\"path\":\"Sources/Swift/Helper/Log/SentryLevel.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e983c8f81c7f1106715ce45bc09d968a4cb\",\"path\":\"Sources/Sentry/include/SentryLevelHelper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9827103d0e236a2917fac90b75c47f4fec\",\"path\":\"Sources/Sentry/SentryLevelHelper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98844dd3ba156013068dbf7f35651a5957\",\"path\":\"Sources/Sentry/include/SentryLevelMapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e986b5d8de5042f9b6c6debab299d006332\",\"path\":\"Sources/Sentry/SentryLevelMapper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e988d673b72b0119aacb08d60c172d07c94\",\"path\":\"Sources/Sentry/include/SentryLog.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9828d4fd172c6650cc894dc077f2713654\",\"path\":\"Sources/Swift/Tools/SentryLog.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c9238e73552f603c4ec002a1af34b21c\",\"path\":\"Sources/Sentry/include/SentryLogC.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e980bbcb909a9d03a552f1277782e18761a\",\"path\":\"Sources/Sentry/SentryLogC.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e986b40c6c48b55db5046d6db511942fa38\",\"path\":\"Sources/Swift/Tools/SentryLogOutput.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.cpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e9849d90f9a6a3c8d0dad49a3899b83cec1\",\"path\":\"Sources/Sentry/SentryMachLogging.cpp\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9800ab9a3ecaf9fb561465a8384a1a4232\",\"path\":\"Sources/Sentry/include/SentryMachLogging.hpp\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e982d535deadc12989119b0615b9dd28f36\",\"path\":\"Sources/Sentry/Public/SentryMeasurementUnit.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d114189b8a76ae18f56bcfc4aaa7ea75\",\"path\":\"Sources/Sentry/SentryMeasurementUnit.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980ca01cfbb872cb4259cd1e35227961de\",\"path\":\"Sources/Sentry/include/SentryMeasurementValue.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e987666c44d8e4bd63cee7a88e33191a7e6\",\"path\":\"Sources/Sentry/SentryMeasurementValue.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9851202d61adfe91c437af0f1c67e03d04\",\"path\":\"Sources/Sentry/Public/SentryMechanism.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e451424b1cdbc8b40cd309ecd4bbbf52\",\"path\":\"Sources/Sentry/SentryMechanism.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98bab24acd4702a28a3c4c9063e4ef0dce\",\"path\":\"Sources/Sentry/Public/SentryMechanismMeta.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e982968cc739e708fdcb3738f171b8efd43\",\"path\":\"Sources/Sentry/SentryMechanismMeta.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9830744347f699402adbc18f50caf4e2d7\",\"path\":\"Sources/Sentry/Public/SentryMessage.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e981082286186bf8efb07f5f2ce82e71bfc\",\"path\":\"Sources/Sentry/SentryMessage.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9899ce478e31a9826614e13502b4400b26\",\"path\":\"Sources/Sentry/include/SentryMeta.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e980d00aea86836a212dca15301d0f6be80\",\"path\":\"Sources/Sentry/SentryMeta.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ead309d87cf3d8ca9c2bc918c7485105\",\"path\":\"Sources/Sentry/include/SentryMetricKitIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e982f522d156c7a74cefb6fc138d7ed9eea\",\"path\":\"Sources/Sentry/SentryMetricKitIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987cd2cb4adcd0f896a6a9aa250d475f99\",\"path\":\"Sources/Sentry/include/SentryMetricProfiler.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.objcpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e9869f27a6b12c3ecdb44b5cf7f02588341\",\"path\":\"Sources/Sentry/SentryMetricProfiler.mm\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e986ce9c2df801c62045926d8864e84a2a1\",\"path\":\"Sources/Swift/Metrics/SentryMetricsAPI.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e981e970cc41f9443b9aec9727b6001ea4c\",\"path\":\"Sources/Swift/Metrics/SentryMetricsClient.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e982f7d0a3333cb2dcfcbe9c5afd9ec20f5\",\"path\":\"Sources/Sentry/include/SentryMigrateSessionInit.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c0abc314e5dda9b6db74ec0670b9817b\",\"path\":\"Sources/Sentry/SentryMigrateSessionInit.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e984f295a6e7dc5aee192e374504ed7fe03\",\"path\":\"Sources/Sentry/include/SentryMsgPackSerializer.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9886c992067744c43e9998edd72ad5e859\",\"path\":\"Sources/Sentry/SentryMsgPackSerializer.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98571b885c8f8846fcc4fd4db493b7816d\",\"path\":\"Sources/Swift/MetricKit/SentryMXCallStackTree.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e987aff5927f8aad996cb87fb760614dbfd\",\"path\":\"Sources/Swift/MetricKit/SentryMXManager.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e6c11cbc922ee1e497e67191309e6292\",\"path\":\"Sources/Sentry/include/SentryNetworkTracker.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9845b3a6e7f24267ede88073dada9a10f1\",\"path\":\"Sources/Sentry/SentryNetworkTracker.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98940d6066dd7b5b655ab91100255b0082\",\"path\":\"Sources/Sentry/include/SentryNetworkTrackingIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e7ed27e60fec75e818894a08259699a4\",\"path\":\"Sources/Sentry/SentryNetworkTrackingIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e983581727c987fa4cca0954306818b12b7\",\"path\":\"Sources/Sentry/include/SentryNoOpSpan.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9805654afa1f82958dd293c561ac9ab271\",\"path\":\"Sources/Sentry/SentryNoOpSpan.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9833e522abc1d5b99c6068adbf830967c2\",\"path\":\"Sources/Sentry/include/SentryNSDataSwizzling.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9833178f1b00328efc29f49ca21f6fa919\",\"path\":\"Sources/Sentry/SentryNSDataSwizzling.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a32f1dc5a94f948c465e56c4010cf7e2\",\"path\":\"Sources/Sentry/include/SentryNSDataTracker.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e984d57188a0ae1e5a026a97e63d1e37dd4\",\"path\":\"Sources/Sentry/SentryNSDataTracker.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987e9ef0b68a6618bff633d8257bbe4807\",\"path\":\"Sources/Sentry/include/SentryNSDataUtils.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9846770a46bcf2b781d57ce3c925c8d341\",\"path\":\"Sources/Sentry/SentryNSDataUtils.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e981b65e1d892bcf2bcb3c5f20ec5972415\",\"path\":\"Sources/Sentry/include/SentryNSDictionarySanitize.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e986c8f20ef0729cf34cac9cba109e20051\",\"path\":\"Sources/Sentry/SentryNSDictionarySanitize.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e983337f11aeab3a289f5c8c19c3908b921\",\"path\":\"Sources/Sentry/Public/SentryNSError.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e984e9914a4b4293c692a90ba05b97965b5\",\"path\":\"Sources/Sentry/SentryNSError.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9880ca6abb4fff5d7995dc2123497283ad\",\"path\":\"Sources/Sentry/include/SentryNSNotificationCenterWrapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e986f2b065fb1bca703459a4977185b8d05\",\"path\":\"Sources/Sentry/SentryNSNotificationCenterWrapper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98663c96b18dbadd2be6f08f5b8ea31c5e\",\"path\":\"Sources/Sentry/include/SentryNSProcessInfoWrapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.objcpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e9894ae4b1a56d92b5ab6ed329af307db93\",\"path\":\"Sources/Sentry/SentryNSProcessInfoWrapper.mm\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9878f37430fba46c9dd937dbfac8cf691c\",\"path\":\"Sources/Sentry/include/SentryNSTimerFactory.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98692460fbec7a54719df44466b20bcd96\",\"path\":\"Sources/Sentry/SentryNSTimerFactory.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98680d2dc7ef64f30c303c18b4117568fc\",\"path\":\"Sources/Sentry/include/SentryNSURLRequest.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e288c4e7a426b08b04b780e40e1f743f\",\"path\":\"Sources/Sentry/SentryNSURLRequest.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c1a99b0cf864330bb01057e2bffc60fc\",\"path\":\"Sources/Sentry/include/SentryNSURLRequestBuilder.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d4217e23e64d40642d3a033d561f8d35\",\"path\":\"Sources/Sentry/SentryNSURLRequestBuilder.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e989354f19ee7a39a29a3b6520194111f66\",\"path\":\"Sources/Sentry/include/SentryNSURLSessionTaskSearch.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e985cf2ec8edccb99946f64c83878c3c803\",\"path\":\"Sources/Sentry/SentryNSURLSessionTaskSearch.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98537f34ec2613ecc57890ea95d1c7af35\",\"path\":\"Sources/Sentry/include/SentryObjCRuntimeWrapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e987bcb2b2868ee46b3d212b5e9a35106d4\",\"path\":\"Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e989e50a47e4c378c019942525106400715\",\"path\":\"Sources/Sentry/Public/SentryOptions.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98063d68f2c40f62f6810d0ed406bb382e\",\"path\":\"Sources/Sentry/SentryOptions.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98eeec8be2c9ebaa5285c5472c3b36bf2e\",\"path\":\"Sources/Sentry/include/HybridPublic/SentryOptions+HybridSDKs.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e984bba6bc40fc60e061c7795ed9d14710d\",\"path\":\"Sources/Sentry/include/SentryOptions+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e984efbed9b76890e16efc0580a6c7f5776\",\"path\":\"Sources/Sentry/include/SentryPerformanceTracker.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e989a15e09aff4d797481eb78d448d68934\",\"path\":\"Sources/Sentry/SentryPerformanceTracker.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e981abbc71e31b4628810dbd6cd1779a6ab\",\"path\":\"Sources/Sentry/include/SentryPerformanceTrackingIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d4b7a5dd39a2fec7b7ddc0ee48570ad5\",\"path\":\"Sources/Sentry/SentryPerformanceTrackingIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e988b84a4cbbf82db3ee0783089fc6f3bba\",\"path\":\"Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9855c159eb37097297bb381315fef4af83\",\"path\":\"Sources/Sentry/include/SentryPredicateDescriptor.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d4338a3e8437f3b1d1acce2c48ddc4fb\",\"path\":\"Sources/Sentry/SentryPredicateDescriptor.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cb932407441bd3d3ca9e06a6841edf04\",\"path\":\"Sources/Sentry/include/SentryPrivate.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a5d56cb9cee92ba68a856453c30f353d\",\"path\":\"Sources/Sentry/include/SentryProfiledTracerConcurrency.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.objcpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e987291ecb593ccdba0e5fc6a98cbf5fdae\",\"path\":\"Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.objcpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e989733858144eb7e903d25bb39b2aa1d69\",\"path\":\"Sources/Sentry/SentryProfiler.mm\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98023f426c062e3f41cac6b9ff2674eb7c\",\"path\":\"Sources/Sentry/include/SentryProfiler+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e981591d1e000f7e36a693757459cc651de\",\"path\":\"Sources/Sentry/Profiling/SentryProfilerDefines.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9834c19d0343e8b716ae4009d27cd5343f\",\"path\":\"Sources/Sentry/include/SentryProfilerSerialization.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.objcpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e9464d6a322ac39a174f4614fab171f4\",\"path\":\"Sources/Sentry/Profiling/SentryProfilerSerialization.mm\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e986d6c0901f1b81ea1582095356892c849\",\"path\":\"Sources/Sentry/Profiling/SentryProfilerSerialization+Test.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9849170326f9e7324f758fad5c8fda7c26\",\"path\":\"Sources/Sentry/include/SentryProfilerState.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.objcpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e988624d29c0763de3bcc218c7c4acf5c0e\",\"path\":\"Sources/Sentry/Profiling/SentryProfilerState.mm\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c97c721f87e21f23b35f49ab3480ddd9\",\"path\":\"Sources/Sentry/include/SentryProfilerState+ObjCpp.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9803b8fbdce4e2ed80e49b511afbfdf7ba\",\"path\":\"Sources/Sentry/include/SentryProfilerTestHelpers.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9857b858be807d990227d8cc8a467799eb\",\"path\":\"Sources/Sentry/Profiling/SentryProfilerTestHelpers.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d57d7ff8850a7f68bc5127a2de78e1c6\",\"path\":\"Sources/Sentry/include/SentryProfileTimeseries.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.objcpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cf09f39b101036d9039a5e9fdd5d4910\",\"path\":\"Sources/Sentry/SentryProfileTimeseries.mm\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980fa47d49e2bb65216878780f3a37d5f1\",\"path\":\"Sources/Sentry/Public/SentryProfilingConditionals.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98dbbe1e35eb18685486a5c8b6577bbbe9\",\"path\":\"Sources/Sentry/SentryPropagationContext.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9888a2658ee08672b053eb8cc3642a4e76\",\"path\":\"Sources/Sentry/SentryPropagationContext.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e989bdf26ad09027d76fb370df8816c6eb9\",\"path\":\"Sources/Sentry/include/SentryQueueableRequestManager.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c2d294613ba90e126869f812c1ab6325\",\"path\":\"Sources/Sentry/SentryQueueableRequestManager.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e983454e6f421a0345f33e633ea310f2406\",\"path\":\"Sources/Sentry/include/SentryRandom.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e980f3118093a755d081ed6850275016cf3\",\"path\":\"Sources/Sentry/SentryRandom.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a60f227949bd23fe3b07ff7b9601fc22\",\"path\":\"Sources/Sentry/include/SentryRateLimitParser.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fe5ac5ab93a3da03e0e408b697c6a079\",\"path\":\"Sources/Sentry/SentryRateLimitParser.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e989d3798a7794bb34165683bfa0463151d\",\"path\":\"Sources/Sentry/include/SentryRateLimits.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e986a01eacf4fd9c5cbbfa18985456424be\",\"path\":\"Sources/Sentry/include/SentryReachability.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d3daca73881c4a4b4874c14922476273\",\"path\":\"Sources/Sentry/SentryReachability.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b22a5ba1237fd87037d77d3641902363\",\"path\":\"Sources/Swift/Protocol/SentryRedactOptions.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9842a47bed079d580cd20989b9075294e3\",\"path\":\"Sources/Swift/Integrations/SessionReplay/SentryReplayEvent.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e985726026a1a1c31e50325233986a3b382\",\"path\":\"Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9805701d4fccb49684aa1b55a60f18fb3c\",\"path\":\"Sources/Swift/Integrations/SessionReplay/SentryReplayRecording.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e989cc077d015a9f88874f161283b85d1af\",\"path\":\"Sources/Swift/Integrations/SessionReplay/SentryReplayType.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9857bc555ad325fec19f683147a099c7e1\",\"path\":\"Sources/Swift/Integrations/SessionReplay/SentryReplayVideoMaker.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98de5de5142f58314b7d12a2e669c41266\",\"path\":\"Sources/Sentry/Public/SentryRequest.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9871b012a3b5f1462b9d197c7012741e34\",\"path\":\"Sources/Sentry/SentryRequest.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e985df69af6f60d3c7f31989a95e3f3a9e8\",\"path\":\"Sources/Sentry/include/SentryRequestManager.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987657341f336ac02acfcf382525f1509c\",\"path\":\"Sources/Sentry/include/SentryRequestOperation.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d263d257b6329d0a2dfee2e60a97fb5c\",\"path\":\"Sources/Sentry/SentryRequestOperation.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987942c8ac2f032c0940a6a38ab91bf734\",\"path\":\"Sources/Sentry/include/SentryRetryAfterHeaderParser.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ac9e44632d815a3c81252e6452f71035\",\"path\":\"Sources/Sentry/SentryRetryAfterHeaderParser.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f50457f417b0f79a0e1b0de834cf9aca\",\"path\":\"Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebBreadcrumbEvent.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cb2f125ee7e524b3c4b954ba30af5d66\",\"path\":\"Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebCustomEvent.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e988d52ddcc81c8bca32b6adcfe81c6b681\",\"path\":\"Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebEvent.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98078862f44140b3e99c42e7d1ab12699b\",\"path\":\"Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebMetaEvent.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9846929a8c54eb68f7ae47aba553065cd4\",\"path\":\"Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebSpanEvent.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fbbaa747f79a3331a947c4a688bc013a\",\"path\":\"Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebTouchEvent.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cb215688c9705038d49294cb6576f234\",\"path\":\"Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebVideoEvent.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cf48b342ad07ad2abb993872033b47d0\",\"path\":\"Sources/Sentry/include/SentrySample.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e983cab1af79491ca23a25ea7c74d671896\",\"path\":\"Sources/Sentry/Profiling/SentrySample.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e984d00b5598fb7c97a8153da8d65037dfa\",\"path\":\"Sources/Sentry/Public/SentrySampleDecision.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e986c69e66e55629c2c42ad5d27ff70b570\",\"path\":\"Sources/Sentry/SentrySampleDecision.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e981142043015590766f312d36cdd419e39\",\"path\":\"Sources/Sentry/include/SentrySampleDecision+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987eb65cfe85a754fabdf367fc17bec2d2\",\"path\":\"Sources/Sentry/include/SentrySamplerDecision.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9894d865a928ae3975ad2746f118b4468b\",\"path\":\"Sources/Sentry/SentrySamplerDecision.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ea213eb10687e172d1d3aded3bd41ae7\",\"path\":\"Sources/Sentry/include/SentrySampling.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98662bd827119a1617f1274adce996caf7\",\"path\":\"Sources/Sentry/SentrySampling.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987d47f3af45086a117a1cc24b2df9365a\",\"path\":\"Sources/Sentry/Public/SentrySamplingContext.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e989ff077c3356757e30d11f375da1ea1eb\",\"path\":\"Sources/Sentry/SentrySamplingContext.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.cpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e981a82de91e8f2e5e3932cf0c7756ee711\",\"path\":\"Sources/Sentry/SentrySamplingProfiler.cpp\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e985cd5b426c8a926a07fa8d251d5dab624\",\"path\":\"Sources/Sentry/include/SentrySamplingProfiler.hpp\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e988185ba6a88b996493cdf38ca379baabb\",\"path\":\"Sources/Sentry/Public/SentryScope.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a51dc90ee182f3f234e31a6c3f115461\",\"path\":\"Sources/Sentry/SentryScope.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98918a5c81a8e0b096accec29a8a6d8bb3\",\"path\":\"Sources/Sentry/include/SentryScope+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e441058a2a5fac727c3f9be0e75a7e8e\",\"path\":\"Sources/Sentry/include/SentryScopeObserver.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ff6e8cbde607cb070aaf521c95fb961a\",\"path\":\"Sources/Sentry/SentryScopeSyncC.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9881d0f7b202c7e8a98e9936b29655c95d\",\"path\":\"Sources/Sentry/include/SentryScopeSyncC.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e989a369f24a3d33c353bfec1e7f64b3ea6\",\"path\":\"Sources/Sentry/include/HybridPublic/SentryScreenFrames.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e46fe301c2548cc68a7e96ce8366b59a\",\"path\":\"Sources/Sentry/SentryScreenFrames.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d09812498914ddbf97a6798491555ab6\",\"path\":\"Sources/Sentry/include/SentryScreenshot.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e04f95ec1867f216e77231ddd1dda094\",\"path\":\"Sources/Sentry/SentryScreenshot.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b1ff561b93fc4c5e149438256f956387\",\"path\":\"Sources/Sentry/include/SentryScreenshotIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e989e9f3ce88297e823291af52c5ad53475\",\"path\":\"Sources/Sentry/SentryScreenshotIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9826acbc0b304b2a0ad2003d840178f0ff\",\"path\":\"Sources/Sentry/Public/SentrySDK.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e980e9d69924caebf6884c96f9ce74bc334\",\"path\":\"Sources/Sentry/SentrySDK.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c7d38ced97e72f78e89610670b1b3fb3\",\"path\":\"Sources/Sentry/include/SentrySDK+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e984d5ae9898138c992b2021f68ec8babf6\",\"path\":\"Sources/Sentry/include/SentrySdkInfo.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98faa847346efca28ffffb35376ea3a952\",\"path\":\"Sources/Sentry/SentrySdkInfo.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cf616d9f6cd818fe3348d29c0c916aa2\",\"path\":\"Sources/Sentry/Public/SentrySerializable.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e1dd28873beb5b2dcb1af5beb95d271b\",\"path\":\"Sources/Sentry/include/SentrySerialization.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e980d6f259ec16188737b09287bd283b367\",\"path\":\"Sources/Sentry/SentrySerialization.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f0fafebc44b49d7ff8414673d3cef1c9\",\"path\":\"Sources/Sentry/include/SentrySession.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e980b8b57442bb9cab2603b9a999081af38\",\"path\":\"Sources/Sentry/SentrySession.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e982116f2e7bb2c8e2ac20df990e0f81236\",\"path\":\"Sources/Sentry/include/SentrySession+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e983c8c985666c5352b6af3c7433239a020\",\"path\":\"Sources/Sentry/include/SentrySessionCrashedHandler.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b12e7dd603ab333ae8345872f96182a8\",\"path\":\"Sources/Sentry/SentrySessionCrashedHandler.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98928b60c2da7f7825875efa439d557e6b\",\"path\":\"Sources/Swift/Protocol/SentrySessionListener.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9828b59ccaf8ff87aa8c8c271b9b5bf184\",\"path\":\"Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98aed94b002bc11a182b8826fa79b3c5e5\",\"path\":\"Sources/Sentry/include/SentrySessionReplayIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fa40304186e2c99ee5ccbfc8d92800fc\",\"path\":\"Sources/Sentry/SentrySessionReplayIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e987cc66d7b02be2cb3d51c5934a525009a\",\"path\":\"Sources/Sentry/include/SentrySessionReplayIntegration+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ada9c823043bb19207c45e2346ff5a6a\",\"path\":\"Sources/Sentry/include/HybridPublic/SentrySessionReplayIntegration-Hybrid.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.c\",\"guid\":\"bfdfe7dc352907fc980b868725387e9894dee7e19fea06bda4dbaaf5c1d9d35b\",\"path\":\"Sources/Sentry/SentrySessionReplaySyncC.c\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9864e08ba6ae71bf1d1c7b269fd8a8fe0f\",\"path\":\"Sources/Sentry/include/SentrySessionReplaySyncC.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e28c0aa12b28173b3abf435b1fa19a34\",\"path\":\"Sources/Sentry/include/SentrySessionTracker.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c0c10fbaa1cb5e98abbbef20d36e7b41\",\"path\":\"Sources/Sentry/SentrySessionTracker.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c6bf18b7cbf57a94d6c0d96399d10e5c\",\"path\":\"Sources/Sentry/include/SentrySpan.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d09bbbc9887ced326bec2d6d7246dce1\",\"path\":\"Sources/Sentry/SentrySpan.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f21c40f0c349752b7f501e01c22fa48b\",\"path\":\"Sources/Sentry/include/SentrySpan+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ecbbe5eadcd9001b4c2fc21e078f887a\",\"path\":\"Sources/Sentry/Public/SentrySpanContext.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e980572088b35705ae125400e5eaf1ca5c0\",\"path\":\"Sources/Sentry/SentrySpanContext.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9853c456af0a27c58300581e4f45771983\",\"path\":\"Sources/Sentry/include/SentrySpanContext+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c593d7d98c929c6a2b0f7d4d0589a1d4\",\"path\":\"Sources/Sentry/Public/SentrySpanId.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9826f4fe2fca7fe75078c0e9e4e65a2d02\",\"path\":\"Sources/Sentry/SentrySpanId.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980a3fbb743bd3ac9e28a18b2bd756f025\",\"path\":\"Sources/Sentry/include/SentrySpanOperations.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9871bde8593bddf6bbbc9f5418fd2e33c4\",\"path\":\"Sources/Sentry/Public/SentrySpanProtocol.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9878c897661c68cc4ff7868fa62493291e\",\"path\":\"Sources/Sentry/Public/SentrySpanStatus.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c18eedf9441788560deccc91341f4ef1\",\"path\":\"Sources/Sentry/SentrySpanStatus.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98786a47c67a0dc6ec0050a48c5a5e4e3d\",\"path\":\"Sources/Sentry/include/SentrySpotlightTransport.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98571366b1f9a87519f69133aa94ea1592\",\"path\":\"Sources/Sentry/SentrySpotlightTransport.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e985987b898ae3639d21a5bfb374115d528\",\"path\":\"Sources/Swift/Integrations/SessionReplay/SentrySRDefaultBreadcrumbConverter.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a1f775d7fcfd5f7d435fe7abffb91fb3\",\"path\":\"Sources/Sentry/include/SentryStackBounds.hpp\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ea3cc80c8121c0454193a096db3b0641\",\"path\":\"Sources/Sentry/include/SentryStackFrame.hpp\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e988ebb06d6eb20ea09e0d5cce625106b6e\",\"path\":\"Sources/Sentry/Public/SentryStacktrace.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9803eaef84f8cf151765e8f2dacff23bae\",\"path\":\"Sources/Sentry/SentryStacktrace.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9863ffcb1f013b44034ba0bfda59953036\",\"path\":\"Sources/Sentry/include/SentryStacktraceBuilder.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e985614a6bc35ea979167d2101aaf82ca72\",\"path\":\"Sources/Sentry/SentryStacktraceBuilder.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e70d13d7772d677d98ce5f06ab6a2bc7\",\"path\":\"Sources/Sentry/include/SentryStatsdClient.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e988a1606defceb762419fa23b4bb125351\",\"path\":\"Sources/Sentry/SentryStatsdClient.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e05ba937b597322372e39fc0358edb5f\",\"path\":\"Sources/Sentry/include/SentrySubClassFinder.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e989c1b2a8ac0bcddb420b0941ef640d0eb\",\"path\":\"Sources/Sentry/SentrySubClassFinder.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e984d4ee0056f4fbde7672fb570961a8583\",\"path\":\"Sources/Sentry/include/SentrySwift.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980bdb04ca4987d86965932ea3417f118b\",\"path\":\"Sources/Sentry/include/SentrySwiftAsyncIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a09e471bf53c0e8d38c894fb2f23deda\",\"path\":\"Sources/Sentry/SentrySwiftAsyncIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9864929f6d55b4bf658809924b262e5263\",\"path\":\"Sources/Sentry/include/HybridPublic/SentrySwizzle.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d97ee0c09e9e900424b1603d10be7375\",\"path\":\"Sources/Sentry/SentrySwizzle.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9877c9f1b3633c0e198960f68aa0c0810f\",\"path\":\"Sources/Sentry/include/SentrySwizzleWrapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98711558a1dd411bb478cd26d8f3f745f2\",\"path\":\"Sources/Sentry/SentrySwizzleWrapper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98993f9b39cd9919f8ec83374ff13b1328\",\"path\":\"Sources/Sentry/include/SentrySysctl.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e985deb0edc70afd30a8360f3dc8e856686\",\"path\":\"Sources/Sentry/SentrySysctl.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ff36fbd967725834366ae2130fb2bed0\",\"path\":\"Sources/Sentry/include/SentrySystemEventBreadcrumbs.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a94a3a2ea869ff4381e8499b0d762c37\",\"path\":\"Sources/Sentry/SentrySystemEventBreadcrumbs.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e988821b58f8dee309405f48b809d77c389\",\"path\":\"Sources/Sentry/include/SentrySystemWrapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.objcpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e7b36496f0059bd09052fe0723a1482e\",\"path\":\"Sources/Sentry/SentrySystemWrapper.mm\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9846f8b71aa0d77e70aad6baf2780ede40\",\"path\":\"Sources/Sentry/Public/SentryThread.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98333f641f00d2f2094339ae17ba284755\",\"path\":\"Sources/Sentry/SentryThread.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.cpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e98f983405d6088b8487df98cbf1cf08289\",\"path\":\"Sources/Sentry/SentryThreadHandle.cpp\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9804a7db4ceccb6811912f7206f78e9447\",\"path\":\"Sources/Sentry/include/SentryThreadHandle.hpp\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9862bc8fb6ad96b53f2b26b863c155ac1e\",\"path\":\"Sources/Sentry/include/SentryThreadInspector.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9880761a78208e56b6a4707d7016a455cc\",\"path\":\"Sources/Sentry/SentryThreadInspector.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.cpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e98bebf1492e8f631b28d50d71933f1372b\",\"path\":\"Sources/Sentry/SentryThreadMetadataCache.cpp\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9826a9f2e728b361a2543a40d1abedebd0\",\"path\":\"Sources/Sentry/include/SentryThreadMetadataCache.hpp\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98706ef18fd5b8c82cc56ff430d4229cb2\",\"path\":\"Sources/Sentry/include/SentryThreadState.hpp\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980755b34b65dc8dfadeb1feee1a3cf958\",\"path\":\"Sources/Sentry/include/SentryThreadWrapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e980b6ae641edc0eb8755e4ed4a2488c062\",\"path\":\"Sources/Sentry/SentryThreadWrapper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98da71b3b7edd671b3d5c2cd01db469838\",\"path\":\"Sources/Sentry/include/SentryTime.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.objcpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e989800de374e76adb4a26b0bcddeecc2fd\",\"path\":\"Sources/Sentry/SentryTime.mm\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980cb9271b7f81c1a2d6b0f37452893839\",\"path\":\"Sources/Sentry/include/SentryTimeToDisplayTracker.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c7ad08268e0d328f29de3b10cd63ddc2\",\"path\":\"Sources/Sentry/SentryTimeToDisplayTracker.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e982a75db7baa0413a63ae20e85adb1d22b\",\"path\":\"Sources/Swift/Integrations/SessionReplay/SentryTouchTracker.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a2a1ef82a610bc27d79d0c54c67af7f0\",\"path\":\"Sources/Sentry/Public/SentryTraceContext.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9887cc63e57392bb0beca2e79621425813\",\"path\":\"Sources/Sentry/SentryTraceContext.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9892f3522efd5a3f19de8e39f523c688d6\",\"path\":\"Sources/Sentry/Public/SentryTraceHeader.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98df898a62101ed695fd27677ba8483559\",\"path\":\"Sources/Sentry/SentryTraceHeader.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c6f2a07c2d050533d09cf4c390de1f8c\",\"path\":\"Sources/Sentry/include/SentryTraceOrigins.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b7d38aba29d59ffea50820df6a97c3d1\",\"path\":\"Sources/Sentry/include/SentryTraceProfiler.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.objcpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d882e4234aaaf8e0069c816fdd59f579\",\"path\":\"Sources/Sentry/Profiling/SentryTraceProfiler.mm\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e981398c4eb84cdf497790fb1fe607e2e63\",\"path\":\"Sources/Sentry/include/SentryTracer.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e982bf9fe4d6f2326a7fccee18a75faa388\",\"path\":\"Sources/Sentry/SentryTracer.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98872ffad4b49352b651b261a73dad0060\",\"path\":\"Sources/Sentry/include/SentryTracer+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9810cd37e42899add31fcc33f4bb739aac\",\"path\":\"Sources/Sentry/include/SentryTracerConfiguration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e986e709c30b562ffcb2b1eda4a0ad426d4\",\"path\":\"Sources/Sentry/SentryTracerConfiguration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9866a291b84196da3f36541b841f97bd0e\",\"path\":\"Sources/Sentry/include/SentryTransaction.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98221eaf92859927540a5d4291e050542e\",\"path\":\"Sources/Sentry/SentryTransaction.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98da808eab441d9e75217ff3d80a5e32a4\",\"path\":\"Sources/Sentry/Public/SentryTransactionContext.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.cpp.objcpp\",\"guid\":\"bfdfe7dc352907fc980b868725387e9841ff7a58c398688fb8514f02d4c41c49\",\"path\":\"Sources/Sentry/SentryTransactionContext.mm\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cb1d9988a5b4400c251fe18f3b1dd801\",\"path\":\"Sources/Sentry/include/SentryTransactionContext+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9836194029038246f4ec8bf1410be089cc\",\"path\":\"Sources/Swift/Integrations/Performance/SentryTransactionNameSource.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e2f9ec322d655c0ea0950183b52221d8\",\"path\":\"Sources/Sentry/include/SentryTransport.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d6a32bf0e617b57a20d2c33c46eb7914\",\"path\":\"Sources/Sentry/include/SentryTransportAdapter.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e984be035c6e10d36e66be434b1c21a073b\",\"path\":\"Sources/Sentry/SentryTransportAdapter.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98954fa85d06420d5ec52380486fd5cb33\",\"path\":\"Sources/Sentry/include/SentryTransportFactory.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b676ca7f4497f4d1fe4138d1f5e18cf7\",\"path\":\"Sources/Sentry/SentryTransportFactory.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e986a376fd26c1eb7097e69832b6f02467c\",\"path\":\"Sources/Sentry/include/SentryUIApplication.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ad8cadda5d19b22d7d5e391f3f2e5773\",\"path\":\"Sources/Sentry/SentryUIApplication.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9894a25cc56626f69559a458a15ac02efd\",\"path\":\"Sources/Sentry/include/SentryUIDeviceWrapper.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e983ef575e6594a639f646ed7e15b622f05\",\"path\":\"Sources/Sentry/SentryUIDeviceWrapper.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d16e83a31b866dde2772c9ce54293036\",\"path\":\"Sources/Sentry/include/SentryUIEventTracker.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c7648303d42528f7384cd7e96635cb91\",\"path\":\"Sources/Sentry/SentryUIEventTracker.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98727154a288bb63516c37f191b7f989fc\",\"path\":\"Sources/Sentry/include/SentryUIEventTrackerMode.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ec06dab8b33576c79bb7f8e7c442836d\",\"path\":\"Sources/Sentry/include/SentryUIEventTrackerTransactionMode.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e4854f9e24894360225810635de170d4\",\"path\":\"Sources/Sentry/SentryUIEventTrackerTransactionMode.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98521d00d65862188e87f09af5fdd2b143\",\"path\":\"Sources/Sentry/include/SentryUIEventTrackingIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c9a689aa4dad67a6b1ca280f2f6ebea4\",\"path\":\"Sources/Sentry/SentryUIEventTrackingIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980e3a99429d2074eae3ed0f01f406b861\",\"path\":\"Sources/Sentry/include/SentryUIViewControllerPerformanceTracker.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e9845042217e88518e3e855ac6610a6bd28\",\"path\":\"Sources/Sentry/SentryUIViewControllerPerformanceTracker.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9889e3af4a95fcd5340aa42b761c61304c\",\"path\":\"Sources/Sentry/include/SentryUIViewControllerSwizzling.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e983176fb557a25d7b21f81988edd0f03bb\",\"path\":\"Sources/Sentry/SentryUIViewControllerSwizzling.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9893fe513507e3ed2aee8e215050e2e0d7\",\"path\":\"Sources/Sentry/Public/SentryUser.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e981599944e2dac3b6e693aef6d13475bc3\",\"path\":\"Sources/Sentry/SentryUser.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98306548f7ff648626ef748253421486b8\",\"path\":\"Sources/Sentry/include/HybridPublic/SentryUser+Private.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e983d67248f1449eda03bef4a9ab05fe6a4\",\"path\":\"Sources/Sentry/Public/SentryUserFeedback.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98aea6546754779f273d8124b565989345\",\"path\":\"Sources/Sentry/SentryUserFeedback.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9821d0b241774df222e3b7377348f85f63\",\"path\":\"Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980bb1557164f81c9e1bfbb7f8363a7867\",\"path\":\"Sources/Sentry/include/SentryViewHierarchy.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98299ac7ba68df1b240a6b268ff9a86b76\",\"path\":\"Sources/Sentry/SentryViewHierarchy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980afc7a0cee689b8418b92fe22b0c3615\",\"path\":\"Sources/Sentry/include/SentryViewHierarchyIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98b53e903d92672c16fb54d8ff0231cef9\",\"path\":\"Sources/Sentry/SentryViewHierarchyIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98817181aab5ecfaf90b5b85e17c49dec8\",\"path\":\"Sources/Swift/Tools/SentryViewPhotographer.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9889f837ea12a895bd661390ab3127aa6d\",\"path\":\"Sources/Swift/Tools/SentryViewScreenshotProvider.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98831b777b6c8ddefb76a396274e08cf65\",\"path\":\"Sources/Sentry/include/SentryWatchdogTerminationLogic.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98018b75d110252e6219e0ee545362ac37\",\"path\":\"Sources/Sentry/SentryWatchdogTerminationLogic.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e981b4d68ef1663be58c2c7d79a11afa4df\",\"path\":\"Sources/Sentry/include/SentryWatchdogTerminationScopeObserver.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98364193a20b48ba275110ac3d6d5e9cda\",\"path\":\"Sources/Sentry/SentryWatchdogTerminationScopeObserver.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fd01427898ca74956b565fd99d02dc1c\",\"path\":\"Sources/Sentry/include/SentryWatchdogTerminationTracker.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e987e91816c36c7362ce51491a1a85d80cd\",\"path\":\"Sources/Sentry/SentryWatchdogTerminationTracker.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98780c1b9a14bc63cecf76b3990a59877b\",\"path\":\"Sources/Sentry/include/SentryWatchdogTerminationTrackingIntegration.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98491b83da9ba737ee9f38229ad6e420d5\",\"path\":\"Sources/Sentry/SentryWatchdogTerminationTrackingIntegration.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ed78e714a1400e941f04e4a22e3d5b2e\",\"path\":\"Sources/Sentry/Public/SentryWithoutUIKit.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e981fe228c1e3eb9e98e34f508e02afeb4d\",\"path\":\"Sources/Swift/Metrics/SetMetric.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98fa7dc020d34d966016e6329a66dd8e9f\",\"path\":\"Sources/Swift/Extensions/StringExtensions.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d9c9b6d5fc746b0b6e694918a128dda3\",\"path\":\"Sources/Swift/SwiftDescriptor.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98cb607a81a62d906f3b39c57798758ac5\",\"path\":\"Sources/Swift/Tools/UIImageHelper.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e988bedd1aa102f8e0c2b086358fe8c4265\",\"path\":\"Sources/Swift/Tools/UIRedactBuilder.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98199af03d7bbe972473ba296e49e9e28f\",\"path\":\"Sources/Sentry/include/UIViewController+Sentry.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c67c9dd02046243d9509ed2cd4798160\",\"path\":\"Sources/Sentry/UIViewController+Sentry.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a9bdf6e3af1025f342b74ed83538e99f\",\"path\":\"Sources/Swift/Extensions/UIViewExtensions.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9847561447b26da1538d79830f9f25d534\",\"path\":\"Sources/Swift/Tools/UrlSanitized.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e0f7059daf81ae00dbaea4f94fd0940e\",\"path\":\"Sources/Swift/Tools/URLSessionTaskHelper.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"children\":[{\"fileType\":\"text.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98720140f27bb1633c64c1b1da36ff7150\",\"path\":\"Sources/Resources/PrivacyInfo.xcprivacy\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984e8e4b6b4cdf0c8e787fc62391dd92fc\",\"name\":\"Resources\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98fde7c7fa201b545fa7346c2dbc65d01e\",\"name\":\"HybridSDK\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ef1bcc008e87a9a25e802f910bd3b338\",\"path\":\"ResourceBundle-Sentry-Sentry-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e98379c05280a321bf688329fe3aef3105b\",\"path\":\"Sentry.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98743f2bd1841f8f09decb589fed2636c8\",\"path\":\"Sentry-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e983b019ef6e78b06a8a837bc6d70936ca6\",\"path\":\"Sentry-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98a244d1195fc83689d6ce3ffc5e36f6e2\",\"path\":\"Sentry-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e983938971700c25e06de2a31eeb4ea8038\",\"path\":\"Sentry-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98008eb440e134dedec2875572805c0b95\",\"path\":\"Sentry.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e9802292b204c9bad4ac7f8961219de2b07\",\"path\":\"Sentry.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e986fb9ef72a20518718c75331fd4883cdd\",\"name\":\"Support Files\",\"path\":\"../Target Support Files/Sentry\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e982359f49a15cf54f5a5642b64cc3eb2e3\",\"name\":\"Sentry\",\"path\":\"Sentry\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d16ccbe7206fb0e2ba6426ac4ec38dd9\",\"path\":\"SwiftyGif/NSImage+SwiftyGif.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9889573bb4021c2391fbff0a825ae4178b\",\"path\":\"SwiftyGif/NSImageView+SwiftyGif.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e9835fe91271911430f451a39e60aa1986c\",\"path\":\"SwiftyGif/ObjcAssociatedWeakObject.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ee06371f21d2935a5ee20d4d5bb4e44f\",\"path\":\"SwiftyGif/SwiftyGif.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e7350beaaae8ffbf946713feea0a4648\",\"path\":\"SwiftyGif/SwiftyGifManager.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98595dbe4b870633814d56f32dc4618746\",\"path\":\"SwiftyGif/UIImage+SwiftyGif.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.swift\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d770d4ce570ae939fbded8f93e00ff8e\",\"path\":\"SwiftyGif/UIImageView+SwiftyGif.swift\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e9875483e8f93a2b38f9b1fe471ba33c3a7\",\"path\":\"SwiftyGif.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c6490049da5fff353f3196233e5aa86b\",\"path\":\"SwiftyGif-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e988ad7a0fe1ae6306e9a821e4ed13fff44\",\"path\":\"SwiftyGif-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e981915be642f44f92bbb0c663859b70dfe\",\"path\":\"SwiftyGif-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e980626803cd8030a41b813ca8f28fffea0\",\"path\":\"SwiftyGif-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ed5df08130fde981e5066c74824ca0ad\",\"path\":\"SwiftyGif.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98be3854286ad572fdc485c3f4251ca09f\",\"path\":\"SwiftyGif.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98810193ceb3c555979788ed2a5c372602\",\"name\":\"Support Files\",\"path\":\"../Target Support Files/SwiftyGif\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e980ae2061b06cbc4928ef7854c13f38740\",\"name\":\"SwiftyGif\",\"path\":\"SwiftyGif\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98632304d423ab2bf91c956eac7a76a9c7\",\"path\":\"Toast-Framework/Toast.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9890a726412a53fe90ba2295a50dfd22a2\",\"path\":\"Toast/UIView+Toast.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e989752157b2cd16354797296f760ae1aef\",\"path\":\"Toast/UIView+Toast.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e989938462c628741b000a566d13b66d51d\",\"path\":\"Toast.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98e3d5685abb47d94d856240e992947bc4\",\"path\":\"Toast-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e989038dc03edeabc512af644ec374031bf\",\"path\":\"Toast-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e985032ba3070912dc548c2a9e4e7a902a9\",\"path\":\"Toast-prefix.pch\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c66dfb77b319009b7b9b119ff175df41\",\"path\":\"Toast-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e9863fe2c666a46f95fbbe4c0f7414d7d8b\",\"path\":\"Toast.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98ca2bbfca057495e362d7fe67afdfd6b9\",\"path\":\"Toast.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e987533233d249f8595cd310d0355eed423\",\"name\":\"Support Files\",\"path\":\"../Target Support Files/Toast\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e984ea7b243f231f45db36c593b6182c199\",\"name\":\"Toast\",\"path\":\"Toast\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e988cfe4b62ae2e73f7f7be4602344a0f59\",\"name\":\"Pods\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"guid\":\"bfdfe7dc352907fc980b868725387e9879aabb07c832697b70c102efdf5309db\",\"name\":\"Products\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"},{\"children\":[{\"children\":[{\"fileType\":\"sourcecode.module-map\",\"guid\":\"bfdfe7dc352907fc980b868725387e98bc9b33687cf974a825db433d5a4ce66f\",\"path\":\"Pods-Runner.modulemap\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text\",\"guid\":\"bfdfe7dc352907fc980b868725387e987edd064bed57826d8d017ae149a3d52e\",\"path\":\"Pods-Runner-acknowledgements.markdown\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e987fb378bbcd6c5ed58c790a44f8de1ad8\",\"path\":\"Pods-Runner-acknowledgements.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.objc\",\"guid\":\"bfdfe7dc352907fc980b868725387e98d314b0e0b0622c4b6805a741f2686226\",\"path\":\"Pods-Runner-dummy.m\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.script.sh\",\"guid\":\"bfdfe7dc352907fc980b868725387e98c868b1c7c8b6f1d4f98ebdaeb296a675\",\"path\":\"Pods-Runner-frameworks.sh\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.plist.xml\",\"guid\":\"bfdfe7dc352907fc980b868725387e986fd78883b66d5d9077a96975628773ce\",\"path\":\"Pods-Runner-Info.plist\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.script.sh\",\"guid\":\"bfdfe7dc352907fc980b868725387e983a42af4f91479a2ff6a94702f9452725\",\"path\":\"Pods-Runner-resources.sh\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"sourcecode.c.h\",\"guid\":\"bfdfe7dc352907fc980b868725387e9847c36cf1fe2165ccb62332bdeff26348\",\"path\":\"Pods-Runner-umbrella.h\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e985f6ec3891a0be6525111807007bbcf76\",\"path\":\"Pods-Runner.debug.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e988db807c74690161e03dc575ee7464945\",\"path\":\"Pods-Runner.profile.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"},{\"fileType\":\"text.xcconfig\",\"guid\":\"bfdfe7dc352907fc980b868725387e98778a51cd43710d278531fd4c4e5ed80f\",\"path\":\"Pods-Runner.release.xcconfig\",\"sourceTree\":\"<group>\",\"type\":\"file\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e9863ac39dbb3c5e6697e1e483c96fdcf12\",\"name\":\"Pods-Runner\",\"path\":\"Target Support Files/Pods-Runner\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98d87df373aa945c7cffc56572b5b5c1d0\",\"name\":\"Targets Support Files\",\"path\":\"\",\"sourceTree\":\"<group>\",\"type\":\"group\"}],\"guid\":\"bfdfe7dc352907fc980b868725387e98677e601b37074db53aff90e47c8f96d1\",\"name\":\"Pods\",\"path\":\"\",\"sourceTree\":\"<group>\",\"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\"]}"
  },
  {
    "path": "frontend/appflowy_flutter/macos/build/ios/XCBuildData/PIFCache/workspace/WORKSPACE@v11_hash=(null)_subobjects=9b6915bad2214bcc5eb58b855fe7b55a-json",
    "content": "{\"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\"]}"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\nbuild/\n\n# Android related\n**/android/**/gradle-wrapper.jar\n**/android/.gradle\n**/android/captures/\n**/android/gradlew\n**/android/gradlew.bat\n**/android/local.properties\n**/android/**/GeneratedPluginRegistrant.java\n\n# iOS/XCode related\n**/ios/**/*.mode1v3\n**/ios/**/*.mode2v3\n**/ios/**/*.moved-aside\n**/ios/**/*.pbxuser\n**/ios/**/*.perspectivev3\n**/ios/**/*sync/\n**/ios/**/.sconsign.dblite\n**/ios/**/.tags*\n**/ios/**/.vagrant/\n**/ios/**/DerivedData/\n**/ios/**/Icon?\n**/ios/**/Pods/\n**/ios/**/.symlinks/\n**/ios/**/profile\n**/ios/**/xcuserdata\n**/ios/.generated/\n**/ios/Flutter/App.framework\n**/ios/Flutter/Flutter.framework\n**/ios/Flutter/Flutter.podspec\n**/ios/Flutter/Generated.xcconfig\n**/ios/Flutter/app.flx\n**/ios/Flutter/app.zip\n**/ios/Flutter/flutter_assets/\n**/ios/Flutter/flutter_export_environment.sh\n**/ios/ServiceDefinitions.json\n**/ios/Runner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!**/ios/**/default.mode1v3\n!**/ios/**/default.mode2v3\n!**/ios/**/default.pbxuser\n!**/ios/**/default.perspectivev3\n\n**/*.dylib\n**/*.a\n**/*.lib\n**/*.dll\n**/*.so\n\nlib/protobuf\nlib/dispatch/dart_event"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/.metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled.\n\nversion:\n  revision: 135454af32477f815a7525073027a3ff9eff1bfd\n  channel: stable\n\nproject_type: plugin\n\n# Tracks metadata for the flutter migrate command\nmigration:\n  platforms:\n    - platform: root\n      create_revision: 135454af32477f815a7525073027a3ff9eff1bfd\n      base_revision: 135454af32477f815a7525073027a3ff9eff1bfd\n    - platform: android\n      create_revision: 135454af32477f815a7525073027a3ff9eff1bfd\n      base_revision: 135454af32477f815a7525073027a3ff9eff1bfd\n    - platform: ios\n      create_revision: 135454af32477f815a7525073027a3ff9eff1bfd\n      base_revision: 135454af32477f815a7525073027a3ff9eff1bfd\n    - platform: macos\n      create_revision: 135454af32477f815a7525073027a3ff9eff1bfd\n      base_revision: 135454af32477f815a7525073027a3ff9eff1bfd\n    - platform: windows\n      create_revision: 135454af32477f815a7525073027a3ff9eff1bfd\n      base_revision: 135454af32477f815a7525073027a3ff9eff1bfd\n\n  # User provided section\n\n  # List of Local paths (relative to this file) that should be\n  # ignored by the migrate tool.\n  #\n  # Files that are not part of the templates will be ignored by default.\n  unmanaged_files:\n    - 'lib/main.dart'\n    - 'ios/Runner.xcodeproj/project.pbxproj'\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/CHANGELOG.md",
    "content": "## 0.0.1\n\n* TODO: Describe initial release.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/LICENSE",
    "content": "TODO: Add your license here.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/README.md",
    "content": "# appflowy_backend\n\nA new flutter plugin project.\n\n## Getting Started\n\nThis project is a starting point for a Flutter\n[plug-in package](https://flutter.dev/developing-packages/),\na specialized package that includes platform-specific implementation code for\nAndroid and/or iOS.\n\nFor help getting started with Flutter, view our\n[online documentation](https://flutter.dev/docs), which offers tutorials,\nsamples, guidance on mobile development, and a full API reference.\n\nThe plugin project was generated without specifying the `--platforms` flag, no platforms are currently supported.\nTo add platforms, run `flutter create -t plugin --platforms <platforms> .` under the same\ndirectory. You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/analysis_options.yaml",
    "content": "analyzer:\n  exclude:\n    - \"**/*.g.dart\"\n    - \"**/*.pb.dart\"\n    - \"**/*.freezed.dart\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/android/.gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea/workspace.xml\n/.idea/libraries\n.DS_Store\n/build\n/captures\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/android/build.gradle",
    "content": "group 'com.plugin.appflowy_backend'\nversion '1.0-SNAPSHOT'\n\nbuildscript {\n    ext.kotlin_version = '1.8.0'\n    repositories {\n        google()\n        jcenter()\n    }\n\n    dependencies {\n        classpath 'com.android.tools.build:gradle:8.1.0'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n    }\n}\n\nrootProject.allprojects {\n    repositories {\n        google()\n        jcenter()\n    }\n}\n\napply plugin: 'com.android.library'\napply plugin: 'kotlin-android'\n\nandroid {\n    compileSdkVersion 33\n    namespace 'com.plugin.appflowy_backend'\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    kotlinOptions {\n        jvmTarget = '1.8'\n    }\n\n    sourceSets {\n        main.java.srcDirs += 'src/main/kotlin'\n    }\n    defaultConfig {\n        minSdkVersion 23\n    }\n}\n\ndependencies {\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\"\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/android/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.7-all.zip\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx1536M\nandroid.useAndroidX=true\nandroid.enableJetifier=true\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/android/settings.gradle",
    "content": "rootProject.name = 'appflowy_backend'\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/android/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  package=\"com.plugin.appflowy_backend\">\n</manifest>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/android/src/main/kotlin/com/plugin/appflowy_backend/AppFlowyBackendPlugin.kt",
    "content": "package com.plugin.appflowy_backend\n\nimport androidx.annotation.NonNull\n\nimport io.flutter.embedding.engine.plugins.FlutterPlugin\nimport io.flutter.plugin.common.MethodCall\nimport io.flutter.plugin.common.MethodChannel\nimport io.flutter.plugin.common.MethodChannel.MethodCallHandler\nimport io.flutter.plugin.common.MethodChannel.Result\nimport io.flutter.plugin.common.PluginRegistry.Registrar\n\n/** AppFlowyBackendPlugin */\nclass AppFlowyBackendPlugin: FlutterPlugin, MethodCallHandler {\n  /// The MethodChannel that will the communication between Flutter and native Android\n  ///\n  /// This local reference serves to register the plugin with the Flutter Engine and unregister it\n  /// when the Flutter Engine is detached from the Activity\n  private lateinit var channel : MethodChannel\n\n  override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {\n    channel = MethodChannel(flutterPluginBinding.binaryMessenger, \"appflowy_backend\")\n    channel.setMethodCallHandler(this)\n  }\n\n  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {\n    if (call.method == \"getPlatformVersion\") {\n      result.success(\"Android ${android.os.Build.VERSION.RELEASE}\")\n    } else {\n      result.notImplemented()\n    }\n  }\n\n  override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {\n    channel.setMethodCallHandler(null)\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\n/build/\n\n# Web related\nlib/generated_plugin_registrant.dart\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Android Studio will place build artifacts here\n/android/app/debug\n/android/app/profile\n/android/app/release\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/.metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: 63062a64432cce03315d6b5196fda7912866eb37\n  channel: dev\n\nproject_type: app\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/README.md",
    "content": "# flowy_sdk_example\n\nDemonstrates how to use the appflowy_backend plugin.\n\n## Getting Started\n\nThis project is a starting point for a Flutter application.\n\nA few resources to get you started if this is your first Flutter project:\n\n- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)\n- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)\n\nFor help getting started with Flutter, view our\n[online documentation](https://flutter.dev/docs), which offers tutorials,\nsamples, guidance on mobile development, and a full API reference.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/analysis_options.yaml",
    "content": "include: ../analysis_options.yaml\n\nanalyzer:\n  exclude:\n\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/android/.gitignore",
    "content": "gradle-wrapper.jar\n/.gradle\n/captures/\n/gradlew\n/gradlew.bat\n/local.properties\nGeneratedPluginRegistrant.java\n\n# Remember to never publicly share your keystore.\n# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app\nkey.properties\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/android/app/build.gradle",
    "content": "def localProperties = new Properties()\ndef localPropertiesFile = rootProject.file('local.properties')\nif (localPropertiesFile.exists()) {\n    localPropertiesFile.withReader('UTF-8') { reader ->\n        localProperties.load(reader)\n    }\n}\n\ndef flutterRoot = localProperties.getProperty('flutter.sdk')\nif (flutterRoot == null) {\n    throw new GradleException(\"Flutter SDK not found. Define location with flutter.sdk in the local.properties file.\")\n}\n\ndef flutterVersionCode = localProperties.getProperty('flutter.versionCode')\nif (flutterVersionCode == null) {\n    flutterVersionCode = '1'\n}\n\ndef flutterVersionName = localProperties.getProperty('flutter.versionName')\nif (flutterVersionName == null) {\n    flutterVersionName = '1.0'\n}\n\napply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply from: \"$flutterRoot/packages/flutter_tools/gradle/flutter.gradle\"\n\nandroid {\n    compileSdkVersion 30\n\n    sourceSets {\n        main.java.srcDirs += 'src/main/kotlin'\n    }\n\n    defaultConfig {\n        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).\n        applicationId \"com.plugin.flowy_sdk_example\"\n        minSdkVersion 33\n        targetSdkVersion 33\n        versionCode flutterVersionCode.toInteger()\n        versionName flutterVersionName\n    }\n\n    buildTypes {\n        release {\n            // TODO: Add your own signing config for the release build.\n            // Signing with the debug keys for now, so `flutter run --release` works.\n            signingConfig signingConfigs.debug\n        }\n    }\n}\n\nflutter {\n    source '../..'\n}\n\ndependencies {\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\"\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/android/app/src/debug/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.plugin.flowy_sdk_example\">\n    <!-- Flutter needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/android/app/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.plugin.flowy_sdk_example\">\n   <application\n        android:label=\"flowy_sdk_example\"\n        android:icon=\"@mipmap/ic_launcher\">\n        <activity\n            android:name=\".MainActivity\"\n            android:launchMode=\"singleTop\"\n            android:theme=\"@style/LaunchTheme\"\n            android:configChanges=\"orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode\"\n            android:hardwareAccelerated=\"true\"\n            android:windowSoftInputMode=\"adjustResize\">\n            <!-- Specifies an Android theme to apply to this Activity as soon as\n                 the Android process has started. This theme is visible to the user\n                 while the Flutter UI initializes. After that, this theme continues\n                 to determine the Window background behind the Flutter UI. -->\n            <meta-data\n              android:name=\"io.flutter.embedding.android.NormalTheme\"\n              android:resource=\"@style/NormalTheme\"\n              />\n            <!-- Displays an Android View that continues showing the launch screen\n                 Drawable until Flutter paints its first frame, then this splash\n                 screen fades out. A splash screen is useful to avoid any visual\n                 gap between the end of Android's launch screen and the painting of\n                 Flutter's first frame. -->\n            <meta-data\n              android:name=\"io.flutter.embedding.android.SplashScreenDrawable\"\n              android:resource=\"@drawable/launch_background\"\n              />\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\"/>\n                <category android:name=\"android.intent.category.LAUNCHER\"/>\n            </intent-filter>\n        </activity>\n        <!-- Don't delete the meta-data below.\n             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->\n        <meta-data\n            android:name=\"flutterEmbedding\"\n            android:value=\"2\" />\n    </application>\n</manifest>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/android/app/src/main/kotlin/com/plugin/flowy_sdk_example/MainActivity.kt",
    "content": "package com.plugin.flowy_sdk_example\n\nimport io.flutter.embedding.android.FlutterActivity\n\nclass MainActivity: FlutterActivity() {\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/android/app/src/main/res/drawable/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@android:color/white\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/android/app/src/main/res/drawable-v21/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"?android:colorBackground\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/android/app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             Flutter draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n         \n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/android/app/src/main/res/values-night/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             Flutter draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n         \n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/android/app/src/profile/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.plugin.flowy_sdk_example\">\n    <!-- Flutter needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/android/build.gradle",
    "content": "buildscript {\n    ext.kotlin_version = '1.6.10'\n    repositories {\n        google()\n        jcenter()\n    }\n\n    dependencies {\n        classpath 'com.android.tools.build:gradle:4.1.0'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        jcenter()\n    }\n}\n\nrootProject.buildDir = '../build'\nsubprojects {\n    project.buildDir = \"${rootProject.buildDir}/${project.name}\"\n}\nsubprojects {\n    project.evaluationDependsOn(':app')\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/android/gradle/wrapper/gradle-wrapper.properties",
    "content": "#Fri Jun 23 08:50:38 CEST 2017\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.7-all.zip\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx1536M\nandroid.useAndroidX=true\nandroid.enableJetifier=true\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/android/settings.gradle",
    "content": "include ':app'\n\ndef localPropertiesFile = new File(rootProject.projectDir, \"local.properties\")\ndef properties = new Properties()\n\nassert localPropertiesFile.exists()\nlocalPropertiesFile.withReader(\"UTF-8\") { reader -> properties.load(reader) }\n\ndef flutterSdkPath = properties.getProperty(\"flutter.sdk\")\nassert flutterSdkPath != null, \"flutter.sdk not set in local.properties\"\napply from: \"$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/integration_test/app_test.dart",
    "content": "// This is a basic Flutter integration test.\n//\n// To perform an interaction with a widget in your test, use the WidgetTester\n// utility that Flutter provides. For example, you can send tap and scroll\n// gestures. You can also use WidgetTester to find child widgets in the widget\n// tree, read text, and verify that the values of widget properties are correct.\n\n// import 'package:flutter/material.dart';\n// import 'package:flutter_test/flutter_test.dart';\n// import 'package:integration_test/integration_test.dart';\n\n// import 'package:flowy_sdk_example/main.dart' as app;\n\n// void main() => run(_testMain);\n\n// void _testMain() {\n//   testWidgets('Counter increments smoke test', (WidgetTester tester) async {\n//     // Build our app and trigger a frame.\n//     app.main();\n\n//     // Trigger a frame.\n//     await tester.pumpAndSettle();\n\n//     // Verify that platform version is retrieved.\n//     expect(\n//       find.byWidgetPredicate(\n//         (Widget widget) => widget is Text &&\n//                            widget.data.startsWith('Running on:'),\n//       ),\n//       findsOneWidget,\n//     );\n//   });\n// }\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/integration_test/driver.dart",
    "content": "// This file is provided as a convenience for running integration tests via the\n// flutter drive command.\n//\n// flutter drive --driver integration_test/driver.dart --target integration_test/app_test.dart\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/.gitignore",
    "content": "*.mode1v3\n*.mode2v3\n*.moved-aside\n*.pbxuser\n*.perspectivev3\n**/*sync/\n.sconsign.dblite\n.tags*\n**/.vagrant/\n**/DerivedData/\nIcon?\n**/Pods/\n**/.symlinks/\nprofile\nxcuserdata\n**/.generated/\nFlutter/App.framework\nFlutter/Flutter.framework\nFlutter/Flutter.podspec\nFlutter/Generated.xcconfig\nFlutter/app.flx\nFlutter/app.zip\nFlutter/flutter_assets/\nFlutter/flutter_export_environment.sh\nServiceDefinitions.json\nRunner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!default.mode1v3\n!default.mode2v3\n!default.pbxuser\n!default.perspectivev3\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Flutter/AppFrameworkInfo.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>CFBundleDevelopmentRegion</key>\n  <string>en</string>\n  <key>CFBundleExecutable</key>\n  <string>App</string>\n  <key>CFBundleIdentifier</key>\n  <string>io.flutter.flutter.app</string>\n  <key>CFBundleInfoDictionaryVersion</key>\n  <string>6.0</string>\n  <key>CFBundleName</key>\n  <string>App</string>\n  <key>CFBundlePackageType</key>\n  <string>FMWK</string>\n  <key>CFBundleShortVersionString</key>\n  <string>1.0</string>\n  <key>CFBundleSignature</key>\n  <string>????</string>\n  <key>CFBundleVersion</key>\n  <string>1.0</string>\n  <key>MinimumOSVersion</key>\n  <string>8.0</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Flutter/Debug.xcconfig",
    "content": "#include \"Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Flutter/Release.xcconfig",
    "content": "#include \"Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Podfile",
    "content": "# Uncomment this line to define a global platform for your project\n# platform :ios, '9.0'\n\n# CocoaPods analytics sends network stats synchronously affecting flutter build latency.\nENV['COCOAPODS_DISABLE_STATS'] = 'true'\n\nproject 'Runner', {\n  'Debug' => :debug,\n  'Profile' => :release,\n  'Release' => :release,\n}\n\ndef flutter_root\n  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)\n  unless File.exist?(generated_xcode_build_settings_path)\n    raise \"#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first\"\n  end\n\n  File.foreach(generated_xcode_build_settings_path) do |line|\n    matches = line.match(/FLUTTER_ROOT\\=(.*)/)\n    return matches[1].strip if matches\n  end\n  raise \"FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get\"\nend\n\nrequire File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)\n\nflutter_ios_podfile_setup\n\ntarget 'Runner' do\n  use_frameworks!\n  use_modular_headers!\n\n  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))\nend\n\npost_install do |installer|\n  installer.pods_project.targets.each do |target|\n    flutter_additional_ios_build_settings(target)\n  end\nend\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Runner/AppDelegate.swift",
    "content": "import UIKit\nimport Flutter\n\n@UIApplicationMain\n@objc class AppDelegate: FlutterAppDelegate {\n  override func application(\n    _ application: UIApplication,\n    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n  ) -> Bool {\n    GeneratedPluginRegistrant.register(with: self)\n    return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-40x40@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-40x40@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"60x60\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-60x60@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"60x60\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-60x60@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-20x20@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-29x29@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-29x29@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-40x40@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-40x40@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"76x76\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-76x76@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"76x76\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-76x76@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"83.5x83.5\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-83.5x83.5@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"1024x1024\",\n      \"idiom\" : \"ios-marketing\",\n      \"filename\" : \"Icon-App-1024x1024@1x.png\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@3x.png\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md",
    "content": "# Launch Screen Assets\n\nYou can customize the launch screen with your own desired assets by replacing the image files in this directory.\n\nYou can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images."
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Runner/Base.lproj/LaunchScreen.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"12121\" systemVersion=\"16G29\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" launchScreen=\"YES\" colorMatched=\"YES\" initialViewController=\"01J-lp-oVM\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"12089\"/>\n    </dependencies>\n    <scenes>\n        <!--View Controller-->\n        <scene sceneID=\"EHf-IW-A2E\">\n            <objects>\n                <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"Ydg-fD-yQy\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"xbc-2k-c8Z\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"Ze5-6b-2t3\">\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <imageView opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" image=\"LaunchImage\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"YRO-k0-Ey4\">\n                            </imageView>\n                        </subviews>\n                        <color key=\"backgroundColor\" red=\"1\" green=\"1\" blue=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n                        <constraints>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerX\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerX\" id=\"1a2-6s-vTC\"/>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerY\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerY\" id=\"4X2-HB-R7a\"/>\n                        </constraints>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"iYj-Kq-Ea1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"53\" y=\"375\"/>\n        </scene>\n    </scenes>\n    <resources>\n        <image name=\"LaunchImage\" width=\"168\" height=\"185\"/>\n    </resources>\n</document>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Runner/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"10117\" systemVersion=\"15F34\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" useTraitCollections=\"YES\" initialViewController=\"BYZ-38-t0r\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"10085\"/>\n    </dependencies>\n    <scenes>\n        <!--Flutter View Controller-->\n        <scene sceneID=\"tne-QT-ifu\">\n            <objects>\n                <viewController id=\"BYZ-38-t0r\" customClass=\"FlutterViewController\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"y3c-jy-aDJ\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"wfy-db-euE\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"8bC-Xf-vdC\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"600\" height=\"600\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" white=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"calibratedWhite\"/>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"dkx-z0-nzr\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>flowy_sdk_example</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>UILaunchStoryboardName</key>\n\t<string>LaunchScreen</string>\n\t<key>UIMainStoryboardFile</key>\n\t<string>Main</string>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UIViewControllerBasedStatusBarAppearance</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Runner/Runner-Bridging-Header.h",
    "content": "#import \"GeneratedPluginRegistrant.h\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 46;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };\n\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };\n\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };\n\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };\n\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };\n\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t9705A1C41CF9048500538489 /* Embed Frameworks */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Embed Frameworks\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = \"<group>\"; };\n\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = \"<group>\"; };\n\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = \"<group>\"; };\n\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = \"Runner-Bridging-Header.h\"; sourceTree = \"<group>\"; };\n\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = \"<group>\"; };\n\t\t97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\t97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t97C146EB1CF9000F007C117D /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t9740EEB11CF90186004384FC /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */,\n\t\t\t);\n\t\t\tname = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146E51CF9000F007C117D = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9740EEB11CF90186004384FC /* Flutter */,\n\t\t\t\t97C146F01CF9000F007C117D /* Runner */,\n\t\t\t\t97C146EF1CF9000F007C117D /* Products */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146EF1CF9000F007C117D /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146EE1CF9000F007C117D /* Runner.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146F01CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FA1CF9000F007C117D /* Main.storyboard */,\n\t\t\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */,\n\t\t\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,\n\t\t\t\t97C147021CF9000F007C117D /* Info.plist */,\n\t\t\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,\n\t\t\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,\n\t\t\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */,\n\t\t\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t97C146ED1CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t9740EEB61CF901F6004384FC /* Run Script */,\n\t\t\t\t97C146EA1CF9000F007C117D /* Sources */,\n\t\t\t\t97C146EB1CF9000F007C117D /* Frameworks */,\n\t\t\t\t97C146EC1CF9000F007C117D /* Resources */,\n\t\t\t\t9705A1C41CF9048500538489 /* Embed Frameworks */,\n\t\t\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 97C146EE1CF9000F007C117D /* Runner.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t97C146E61CF9000F007C117D /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastUpgradeCheck = 1020;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t97C146ED1CF9000F007C117D = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 7.3.1;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 97C146E51CF9000F007C117D;\n\t\t\tproductRefGroup = 97C146EF1CF9000F007C117D /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t97C146ED1CF9000F007C117D /* Runner */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t97C146EC1CF9000F007C117D /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,\n\t\t\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,\n\t\t\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,\n\t\t\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"Thin Binary\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" embed_and_thin\";\n\t\t};\n\t\t9740EEB61CF901F6004384FC /* Run Script */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"Run Script\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" build\";\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t97C146EA1CF9000F007C117D /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,\n\t\t\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXVariantGroup section */\n\t\t97C146FA1CF9000F007C117D /* Main.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FB1CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C147001CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = LaunchScreen.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t249021D3217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 9.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t249021D4217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = \"$(inherited) @executable_path/Frameworks\";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.plugin.flowySdkExample;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t97C147031CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 9.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147041CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 9.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Owholemodule\";\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t97C147061CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = \"$(inherited) @executable_path/Frameworks\";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.plugin.flowySdkExample;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147071CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = \"$(inherited) @executable_path/Frameworks\";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.plugin.flowySdkExample;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147031CF9000F007C117D /* Debug */,\n\t\t\t\t97C147041CF9000F007C117D /* Release */,\n\t\t\t\t249021D3217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147061CF9000F007C117D /* Debug */,\n\t\t\t\t97C147071CF9000F007C117D /* Release */,\n\t\t\t\t249021D4217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 97C146E61CF9000F007C117D /* Project object */;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1020\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n               BuildableName = \"Runner.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <Testables>\n      </Testables>\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <AdditionalOptions>\n      </AdditionalOptions>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n      <AdditionalOptions>\n      </AdditionalOptions>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/lib/main.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nimport 'package:appflowy_backend/appflowy_backend.dart';\n\nvoid main() {\n  runApp(MyApp());\n}\n\nclass MyApp extends StatefulWidget {\n  @override\n  _MyAppState createState() => _MyAppState();\n}\n\nclass _MyAppState extends State<MyApp> {\n  String _platformVersion = 'Unknown';\n\n  @override\n  void initState() {\n    super.initState();\n    initPlatformState();\n  }\n\n  // Platform messages are asynchronous, so we initialize in an async method.\n  Future<void> initPlatformState() async {\n    String platformVersion;\n    // Platform messages may fail, so we use a try/catch PlatformException.\n    try {\n      platformVersion = await FlowySDK.platformVersion;\n    } on PlatformException {\n      platformVersion = 'Failed to get platform version.';\n    }\n\n    // If the widget was removed from the tree while the asynchronous platform\n    // message was in flight, we want to discard the reply rather than calling\n    // setState to update our non-existent appearance.\n    if (!mounted) return;\n    setState(() => _platformVersion = platformVersion);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      home: Scaffold(\n        appBar: AppBar(title: const Text('Plugin example app')),\n        body: Center(child: Text('Running on: $_platformVersion\\n')),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/.gitignore",
    "content": "# Flutter-related\n**/Flutter/ephemeral/\n**/Pods/\n\n# Xcode-related\n**/xcuserdata/\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Flutter/Flutter-Debug.xcconfig",
    "content": "#include \"Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"\n#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Flutter/Flutter-Release.xcconfig",
    "content": "#include \"Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"\n#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Podfile",
    "content": "platform :osx, '10.14'\n\n# CocoaPods analytics sends network stats synchronously affecting flutter build latency.\nENV['COCOAPODS_DISABLE_STATS'] = 'true'\n\nproject 'Runner', {\n  'Debug' => :debug,\n  'Profile' => :release,\n  'Release' => :release,\n}\n\ndef parse_KV_file(file, separator='=')\n  file_abs_path = File.expand_path(file)\n  if !File.exists? file_abs_path\n    return [];\n  end\n  pods_ary = []\n  skip_line_start_symbols = [\"#\", \"/\"]\n  File.foreach(file_abs_path) { |line|\n      next if skip_line_start_symbols.any? { |symbol| line =~ /^\\s*#{symbol}/ }\n      plugin = line.split(pattern=separator)\n      if plugin.length == 2\n        podname = plugin[0].strip()\n        path = plugin[1].strip()\n        podpath = File.expand_path(\"#{path}\", file_abs_path)\n        pods_ary.push({:name => podname, :path => podpath});\n      else\n        puts \"Invalid plugin specification: #{line}\"\n      end\n  }\n  return pods_ary\nend\n\ndef pubspec_supports_macos(file)\n  file_abs_path = File.expand_path(file)\n  if !File.exists? file_abs_path\n    return false;\n  end\n  File.foreach(file_abs_path) { |line|\n    return true if line =~ /^\\s*macos:/\n  }\n  return false\nend\n\ntarget 'Runner' do\n  use_frameworks!\n  use_modular_headers!\n\n  # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock\n  # referring to absolute paths on developers' machines.\n  ephemeral_dir = File.join('Flutter', 'ephemeral')\n  symlink_dir = File.join(ephemeral_dir, '.symlinks')\n  symlink_plugins_dir = File.join(symlink_dir, 'plugins')\n  system(\"rm -rf #{symlink_dir}\")\n  system(\"mkdir -p #{symlink_plugins_dir}\")\n\n  # Flutter Pods\n  generated_xcconfig = parse_KV_file(File.join(ephemeral_dir, 'Flutter-Generated.xcconfig'))\n  if generated_xcconfig.empty?\n    puts \"Flutter-Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first.\"\n  end\n  generated_xcconfig.map { |p|\n    if p[:name] == 'FLUTTER_FRAMEWORK_DIR'\n      symlink = File.join(symlink_dir, 'flutter')\n      File.symlink(File.dirname(p[:path]), symlink)\n      pod 'FlutterMacOS', :path => File.join(symlink, File.basename(p[:path]))\n    end\n  }\n\n  # Plugin Pods\n  plugin_pods = parse_KV_file('../.flutter-plugins')\n  plugin_pods.map { |p|\n    symlink = File.join(symlink_plugins_dir, p[:name])\n    File.symlink(p[:path], symlink)\n    if pubspec_supports_macos(File.join(symlink, 'pubspec.yaml'))\n      pod p[:name], :path => File.join(symlink, 'macos')\n    end\n  }\nend\n\n# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system.\ninstall! 'cocoapods', :disable_input_output_paths => true\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/AppDelegate.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\n@main\nclass AppDelegate: FlutterAppDelegate {\n  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {\n    return true\n  }\n\n  override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {\n    return true\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_16.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_32.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"32x32\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_32.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"32x32\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_64.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"128x128\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_128.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"128x128\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_256.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"256x256\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_256.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"256x256\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_512.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"512x512\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_512.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"512x512\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_1024.png\",\n      \"scale\" : \"2x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/Base.lproj/MainMenu.xib",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion=\"14490.70\" targetRuntime=\"MacOSX.Cocoa\" propertyAccessControl=\"none\" useAutolayout=\"YES\" customObjectInstantitationMethod=\"direct\">\n    <dependencies>\n        <deployment identifier=\"macosx\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.CocoaPlugin\" version=\"14490.70\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <objects>\n        <customObject id=\"-2\" userLabel=\"File's Owner\" customClass=\"NSApplication\">\n            <connections>\n                <outlet property=\"delegate\" destination=\"Voe-Tx-rLC\" id=\"GzC-gU-4Uq\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"-1\" userLabel=\"First Responder\" customClass=\"FirstResponder\"/>\n        <customObject id=\"-3\" userLabel=\"Application\" customClass=\"NSObject\"/>\n        <customObject id=\"Voe-Tx-rLC\" customClass=\"AppDelegate\" customModule=\"Runner\" customModuleProvider=\"target\">\n            <connections>\n                <outlet property=\"applicationMenu\" destination=\"uQy-DD-JDr\" id=\"XBo-yE-nKs\"/>\n                <outlet property=\"mainFlutterWindow\" destination=\"QvC-M9-y7g\" id=\"gIp-Ho-8D9\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"YLy-65-1bz\" customClass=\"NSFontManager\"/>\n        <menu title=\"Main Menu\" systemMenu=\"main\" id=\"AYu-sK-qS6\">\n            <items>\n                <menuItem title=\"APP_NAME\" id=\"1Xt-HY-uBw\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"APP_NAME\" systemMenu=\"apple\" id=\"uQy-DD-JDr\">\n                        <items>\n                            <menuItem title=\"About APP_NAME\" id=\"5kV-Vb-QxS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"orderFrontStandardAboutPanel:\" target=\"-1\" id=\"Exp-CZ-Vem\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"VOq-y0-SEH\"/>\n                            <menuItem title=\"Preferences…\" keyEquivalent=\",\" id=\"BOF-NM-1cW\"/>\n                            <menuItem isSeparatorItem=\"YES\" id=\"wFC-TO-SCJ\"/>\n                            <menuItem title=\"Services\" id=\"NMo-om-nkz\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Services\" systemMenu=\"services\" id=\"hz9-B4-Xy5\"/>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"4je-JR-u6R\"/>\n                            <menuItem title=\"Hide APP_NAME\" keyEquivalent=\"h\" id=\"Olw-nP-bQN\">\n                                <connections>\n                                    <action selector=\"hide:\" target=\"-1\" id=\"PnN-Uc-m68\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Hide Others\" keyEquivalent=\"h\" id=\"Vdr-fp-XzO\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"hideOtherApplications:\" target=\"-1\" id=\"VT4-aY-XCT\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Show All\" id=\"Kd2-mp-pUS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"unhideAllApplications:\" target=\"-1\" id=\"Dhg-Le-xox\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"kCx-OE-vgT\"/>\n                            <menuItem title=\"Quit APP_NAME\" keyEquivalent=\"q\" id=\"4sb-4s-VLi\">\n                                <connections>\n                                    <action selector=\"terminate:\" target=\"-1\" id=\"Te7-pn-YzF\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Edit\" id=\"5QF-Oa-p0T\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Edit\" id=\"W48-6f-4Dl\">\n                        <items>\n                            <menuItem title=\"Undo\" keyEquivalent=\"z\" id=\"dRJ-4n-Yzg\">\n                                <connections>\n                                    <action selector=\"undo:\" target=\"-1\" id=\"M6e-cu-g7V\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Redo\" keyEquivalent=\"Z\" id=\"6dh-zS-Vam\">\n                                <connections>\n                                    <action selector=\"redo:\" target=\"-1\" id=\"oIA-Rs-6OD\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"WRV-NI-Exz\"/>\n                            <menuItem title=\"Cut\" keyEquivalent=\"x\" id=\"uRl-iY-unG\">\n                                <connections>\n                                    <action selector=\"cut:\" target=\"-1\" id=\"YJe-68-I9s\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Copy\" keyEquivalent=\"c\" id=\"x3v-GG-iWU\">\n                                <connections>\n                                    <action selector=\"copy:\" target=\"-1\" id=\"G1f-GL-Joy\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Paste\" keyEquivalent=\"v\" id=\"gVA-U4-sdL\">\n                                <connections>\n                                    <action selector=\"paste:\" target=\"-1\" id=\"UvS-8e-Qdg\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Paste and Match Style\" keyEquivalent=\"V\" id=\"WeT-3V-zwk\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"pasteAsPlainText:\" target=\"-1\" id=\"cEh-KX-wJQ\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Delete\" id=\"pa3-QI-u2k\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"delete:\" target=\"-1\" id=\"0Mk-Ml-PaM\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Select All\" keyEquivalent=\"a\" id=\"Ruw-6m-B2m\">\n                                <connections>\n                                    <action selector=\"selectAll:\" target=\"-1\" id=\"VNm-Mi-diN\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"uyl-h8-XO2\"/>\n                            <menuItem title=\"Find\" id=\"4EN-yA-p0u\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Find\" id=\"1b7-l0-nxx\">\n                                    <items>\n                                        <menuItem title=\"Find…\" tag=\"1\" keyEquivalent=\"f\" id=\"Xz5-n4-O0W\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"cD7-Qs-BN4\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find and Replace…\" tag=\"12\" keyEquivalent=\"f\" id=\"YEy-JH-Tfz\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"WD3-Gg-5AJ\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find Next\" tag=\"2\" keyEquivalent=\"g\" id=\"q09-fT-Sye\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"NDo-RZ-v9R\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find Previous\" tag=\"3\" keyEquivalent=\"G\" id=\"OwM-mh-QMV\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"HOh-sY-3ay\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Use Selection for Find\" tag=\"7\" keyEquivalent=\"e\" id=\"buJ-ug-pKt\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"U76-nv-p5D\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Jump to Selection\" keyEquivalent=\"j\" id=\"S0p-oC-mLd\">\n                                            <connections>\n                                                <action selector=\"centerSelectionInVisibleArea:\" target=\"-1\" id=\"IOG-6D-g5B\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Spelling and Grammar\" id=\"Dv1-io-Yv7\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Spelling\" id=\"3IN-sU-3Bg\">\n                                    <items>\n                                        <menuItem title=\"Show Spelling and Grammar\" keyEquivalent=\":\" id=\"HFo-cy-zxI\">\n                                            <connections>\n                                                <action selector=\"showGuessPanel:\" target=\"-1\" id=\"vFj-Ks-hy3\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Check Document Now\" keyEquivalent=\";\" id=\"hz2-CU-CR7\">\n                                            <connections>\n                                                <action selector=\"checkSpelling:\" target=\"-1\" id=\"fz7-VC-reM\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"bNw-od-mp5\"/>\n                                        <menuItem title=\"Check Spelling While Typing\" id=\"rbD-Rh-wIN\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleContinuousSpellChecking:\" target=\"-1\" id=\"7w6-Qz-0kB\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Check Grammar With Spelling\" id=\"mK6-2p-4JG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleGrammarChecking:\" target=\"-1\" id=\"muD-Qn-j4w\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Correct Spelling Automatically\" id=\"78Y-hA-62v\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticSpellingCorrection:\" target=\"-1\" id=\"2lM-Qi-WAP\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Substitutions\" id=\"9ic-FL-obx\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Substitutions\" id=\"FeM-D8-WVr\">\n                                    <items>\n                                        <menuItem title=\"Show Substitutions\" id=\"z6F-FW-3nz\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"orderFrontSubstitutionsPanel:\" target=\"-1\" id=\"oku-mr-iSq\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"gPx-C9-uUO\"/>\n                                        <menuItem title=\"Smart Copy/Paste\" id=\"9yt-4B-nSM\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleSmartInsertDelete:\" target=\"-1\" id=\"3IJ-Se-DZD\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Quotes\" id=\"hQb-2v-fYv\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticQuoteSubstitution:\" target=\"-1\" id=\"ptq-xd-QOA\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Dashes\" id=\"rgM-f4-ycn\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticDashSubstitution:\" target=\"-1\" id=\"oCt-pO-9gS\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Links\" id=\"cwL-P1-jid\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticLinkDetection:\" target=\"-1\" id=\"Gip-E3-Fov\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Data Detectors\" id=\"tRr-pd-1PS\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticDataDetection:\" target=\"-1\" id=\"R1I-Nq-Kbl\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Text Replacement\" id=\"HFQ-gK-NFA\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticTextReplacement:\" target=\"-1\" id=\"DvP-Fe-Py6\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Transformations\" id=\"2oI-Rn-ZJC\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Transformations\" id=\"c8a-y6-VQd\">\n                                    <items>\n                                        <menuItem title=\"Make Upper Case\" id=\"vmV-6d-7jI\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"uppercaseWord:\" target=\"-1\" id=\"sPh-Tk-edu\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Make Lower Case\" id=\"d9M-CD-aMd\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"lowercaseWord:\" target=\"-1\" id=\"iUZ-b5-hil\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Capitalize\" id=\"UEZ-Bs-lqG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"capitalizeWord:\" target=\"-1\" id=\"26H-TL-nsh\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Speech\" id=\"xrE-MZ-jX0\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Speech\" id=\"3rS-ZA-NoH\">\n                                    <items>\n                                        <menuItem title=\"Start Speaking\" id=\"Ynk-f8-cLZ\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"startSpeaking:\" target=\"-1\" id=\"654-Ng-kyl\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Stop Speaking\" id=\"Oyz-dy-DGm\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"stopSpeaking:\" target=\"-1\" id=\"dX8-6p-jy9\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"View\" id=\"H8h-7b-M4v\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"View\" id=\"HyV-fh-RgO\">\n                        <items>\n                            <menuItem title=\"Enter Full Screen\" keyEquivalent=\"f\" id=\"4J7-dP-txa\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" control=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"toggleFullScreen:\" target=\"-1\" id=\"dU3-MA-1Rq\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Window\" id=\"aUF-d1-5bR\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Window\" systemMenu=\"window\" id=\"Td7-aD-5lo\">\n                        <items>\n                            <menuItem title=\"Minimize\" keyEquivalent=\"m\" id=\"OY7-WF-poV\">\n                                <connections>\n                                    <action selector=\"performMiniaturize:\" target=\"-1\" id=\"VwT-WD-YPe\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Zoom\" id=\"R4o-n2-Eq4\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"performZoom:\" target=\"-1\" id=\"DIl-cC-cCs\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"eu3-7i-yIM\"/>\n                            <menuItem title=\"Bring All to Front\" id=\"LE2-aR-0XJ\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"arrangeInFront:\" target=\"-1\" id=\"DRN-fu-gQh\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n            </items>\n            <point key=\"canvasLocation\" x=\"142\" y=\"-258\"/>\n        </menu>\n        <window title=\"APP_NAME\" allowsToolTipsWhenApplicationIsInactive=\"NO\" autorecalculatesKeyViewLoop=\"NO\" releasedWhenClosed=\"NO\" animationBehavior=\"default\" id=\"QvC-M9-y7g\" customClass=\"MainFlutterWindow\" customModule=\"Runner\" customModuleProvider=\"target\">\n            <windowStyleMask key=\"styleMask\" titled=\"YES\" closable=\"YES\" miniaturizable=\"YES\" resizable=\"YES\"/>\n            <rect key=\"contentRect\" x=\"335\" y=\"390\" width=\"800\" height=\"600\"/>\n            <rect key=\"screenRect\" x=\"0.0\" y=\"0.0\" width=\"2560\" height=\"1577\"/>\n            <view key=\"contentView\" wantsLayer=\"YES\" id=\"EiT-Mj-1SZ\">\n                <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"800\" height=\"600\"/>\n                <autoresizingMask key=\"autoresizingMask\"/>\n            </view>\n        </window>\n    </objects>\n</document>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/Configs/AppInfo.xcconfig",
    "content": "// Application-level settings for the Runner target.\n//\n// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the\n// future. If not, the values below would default to using the project name when this becomes a\n// 'flutter create' template.\n\n// The application's name. By default this is also the title of the Flutter window.\nPRODUCT_NAME = flowy_sdk_example\n\n// The application's bundle identifier\nPRODUCT_BUNDLE_IDENTIFIER = com.plugin.flowySdkExample\n\n// The copyright displayed in application information\nPRODUCT_COPYRIGHT = Copyright © 2021 com.plugin. All rights reserved.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/Configs/Debug.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Debug.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/Configs/Release.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Release.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/Configs/Warnings.xcconfig",
    "content": "WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings\nGCC_WARN_UNDECLARED_SELECTOR = YES\nCLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES\nCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE\nCLANG_WARN__DUPLICATE_METHOD_MATCH = YES\nCLANG_WARN_PRAGMA_PACK = YES\nCLANG_WARN_STRICT_PROTOTYPES = YES\nCLANG_WARN_COMMA = YES\nGCC_WARN_STRICT_SELECTOR_MATCH = YES\nCLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES\nCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES\nGCC_WARN_SHADOW = YES\nCLANG_WARN_UNREACHABLE_CODE = YES\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/DebugProfile.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n\t<key>com.apple.security.cs.allow-jit</key>\n\t<true/>\n\t<key>com.apple.security.network.server</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIconFile</key>\n\t<string></string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSMinimumSystemVersion</key>\n\t<string>$(MACOSX_DEPLOYMENT_TARGET)</string>\n\t<key>NSHumanReadableCopyright</key>\n\t<string>$(PRODUCT_COPYRIGHT)</string>\n\t<key>NSMainNibFile</key>\n\t<string>MainMenu</string>\n\t<key>NSPrincipalClass</key>\n\t<string>NSApplication</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/MainFlutterWindow.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\nclass MainFlutterWindow: NSWindow {\n  override func awakeFromNib() {\n    let flutterViewController = FlutterViewController.init()\n    let windowFrame = self.frame\n    self.contentViewController = flutterViewController\n    self.setFrame(windowFrame, display: true)\n\n    RegisterGeneratedPlugins(registry: flutterViewController)\n\n    super.awakeFromNib()\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner/Release.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXAggregateTarget section */\n\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {\n\t\t\tisa = PBXAggregateTarget;\n\t\t\tbuildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t33CC111E2044C6BF0003C045 /* ShellScript */,\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = \"Flutter Assemble\";\n\t\t\tproductName = FLX;\n\t\t};\n/* End PBXAggregateTarget section */\n\n/* Begin PBXBuildFile section */\n\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };\n\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };\n\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };\n\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };\n\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };\n\t\tB7C2E82907836001B5A6F548 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 49D7864808B727FDFB82A4C2 /* Pods_Runner.framework */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 33CC10E52044A3C60003C045 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 33CC111A2044C6BA0003C045;\n\t\t\tremoteInfo = FLX;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t33CC110E2044A8840003C045 /* Bundle Framework */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Bundle Framework\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t0E4B7400A641C13F34C1A73C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.profile.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = \"<group>\"; };\n\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = \"<group>\"; };\n\t\t33CC10ED2044A3C60003C045 /* flowy_sdk_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flowy_sdk_example.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = \"<group>\"; };\n\t\t33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = \"<group>\"; };\n\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = \"<group>\"; };\n\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = \"Flutter-Generated.xcconfig\"; path = \"ephemeral/Flutter-Generated.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = \"<group>\"; };\n\t\t33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = \"<group>\"; };\n\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = \"<group>\"; };\n\t\t4959329ACC9EF83FB15AC0E5 /* 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 = \"<group>\"; };\n\t\t49D7864808B727FDFB82A4C2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\tA82DF8E6F43DF0AD4D0653DC /* 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 = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t33CC10EA2044A3C60003C045 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tB7C2E82907836001B5A6F548 /* Pods_Runner.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t33BA886A226E78AF003329D5 /* Configs */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */,\n\t\t\t);\n\t\t\tpath = Configs;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10E42044A3C60003C045 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33FAB671232836740065AC1E /* Runner */,\n\t\t\t\t33CEB47122A05771004F2AC0 /* Flutter */,\n\t\t\t\t33CC10EE2044A3C60003C045 /* Products */,\n\t\t\t\tD73912EC22F37F3D000D13A0 /* Frameworks */,\n\t\t\t\t426A48CDE70CED9D3E9185FC /* Pods */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10EE2044A3C60003C045 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10ED2044A3C60003C045 /* flowy_sdk_example.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC11242044D66E0003C045 /* Resources */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */,\n\t\t\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */,\n\t\t\t\t33CC10F72044A3C60003C045 /* Info.plist */,\n\t\t\t);\n\t\t\tname = Resources;\n\t\t\tpath = ..;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CEB47122A05771004F2AC0 /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,\n\t\t\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,\n\t\t\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,\n\t\t\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,\n\t\t\t);\n\t\t\tpath = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33FAB671232836740065AC1E /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */,\n\t\t\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,\n\t\t\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */,\n\t\t\t\t33E51914231749380026EE4D /* Release.entitlements */,\n\t\t\t\t33CC11242044D66E0003C045 /* Resources */,\n\t\t\t\t33BA886A226E78AF003329D5 /* Configs */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t426A48CDE70CED9D3E9185FC /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tA82DF8E6F43DF0AD4D0653DC /* Pods-Runner.debug.xcconfig */,\n\t\t\t\t4959329ACC9EF83FB15AC0E5 /* Pods-Runner.release.xcconfig */,\n\t\t\t\t0E4B7400A641C13F34C1A73C /* Pods-Runner.profile.xcconfig */,\n\t\t\t);\n\t\t\tname = Pods;\n\t\t\tpath = Pods;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD73912EC22F37F3D000D13A0 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t49D7864808B727FDFB82A4C2 /* Pods_Runner.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t33CC10EC2044A3C60003C045 /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t40E676EAFF7C261E0E226351 /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t33CC10E92044A3C60003C045 /* Sources */,\n\t\t\t\t33CC10EA2044A3C60003C045 /* Frameworks */,\n\t\t\t\t33CC10EB2044A3C60003C045 /* Resources */,\n\t\t\t\t33CC110E2044A8840003C045 /* Bundle Framework */,\n\t\t\t\t3399D490228B24CF009A79C7 /* ShellScript */,\n\t\t\t\t648FD3193605213263C327B2 /* [CP] Embed Pods Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 33CC10ED2044A3C60003C045 /* flowy_sdk_example.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t33CC10E52044A3C60003C045 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastSwiftUpdateCheck = 0920;\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t33CC10EC2044A3C60003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t\tProvisioningStyle = Automatic;\n\t\t\t\t\t\tSystemCapabilities = {\n\t\t\t\t\t\t\tcom.apple.Sandbox = {\n\t\t\t\t\t\t\t\tenabled = 1;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t\t33CC111A2044C6BA0003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tProvisioningStyle = Manual;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 33CC10E42044A3C60003C045;\n\t\t\tproductRefGroup = 33CC10EE2044A3C60003C045 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t33CC10EC2044A3C60003C045 /* Runner */,\n\t\t\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t33CC10EB2044A3C60003C045 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,\n\t\t\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t3399D490228B24CF009A79C7 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"echo \\\"$PRODUCT_NAME.app\\\" > \\\"$PROJECT_DIR\\\"/Flutter/ephemeral/.app_filename && \\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh embed\\n\";\n\t\t};\n\t\t33CC111E2044C6BF0003C045 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterInputs.xcfilelist,\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\tFlutter/ephemeral/tripwire,\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterOutputs.xcfilelist,\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire\";\n\t\t};\n\t\t40E676EAFF7C261E0E226351 /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t648FD3193605213263C327B2 /* [CP] Embed Pods Frameworks */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tname = \"[CP] Embed Pods Frameworks\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t33CC10E92044A3C60003C045 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,\n\t\t\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,\n\t\t\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;\n\t\t\ttargetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F52044A3C60003C045 /* Base */,\n\t\t\t);\n\t\t\tname = MainMenu.xib;\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t338D0CE9231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEA231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tFRAMEWORK_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"$(PROJECT_DIR)/Flutter/ephemeral\",\n\t\t\t\t);\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEB231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t33CC10F92044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FA2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC10FC2044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tFRAMEWORK_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"$(PROJECT_DIR)/Flutter/ephemeral\",\n\t\t\t\t);\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FD2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tFRAMEWORK_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"$(PROJECT_DIR)/Flutter/ephemeral\",\n\t\t\t\t);\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC111C2044C6BA0003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC111D2044C6BA0003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10F92044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FA2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CE9231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10FC2044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FD2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CEA231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC111C2044C6BA0003C045 /* Debug */,\n\t\t\t\t33CC111D2044C6BA0003C045 /* Release */,\n\t\t\t\t338D0CEB231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 33CC10E52044A3C60003C045 /* Project object */;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1510\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n               BuildableName = \"flowy_sdk_example.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"flowy_sdk_example.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <AdditionalOptions>\n      </AdditionalOptions>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"flowy_sdk_example.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n      <AdditionalOptions>\n      </AdditionalOptions>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"flowy_sdk_example.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n   <FileRef\n      location = \"group:Pods/Pods.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/pubspec.yaml",
    "content": "name: flowy_sdk_example\ndescription: Demonstrates how to use the appflowy_backend plugin.\n\n# The following line prevents the package from being accidentally published to\n# pub.dev using `pub publish`. This is preferred for private packages.\npublish_to: 'none' # Remove this line if you wish to publish to pub.dev\n\nenvironment:\n  sdk: \">=2.12.0 <3.0.0\"\n\ndependencies:\n  flutter:\n    sdk: flutter\n\n  appflowy_backend:\n    # When depending on this package from a real application you should use:\n    #   appflowy_backend: ^x.y.z\n    # See https://dart.dev/tools/pub/dependencies#version-constraints\n    # The example app is bundled with the plugin so we use a path dependency on\n    # the parent directory to use the current plugin's version.\n    path: ../\n\n  # The following adds the Cupertino Icons font to your application.\n  # Use with the CupertinoIcons class for iOS style icons.\n  cupertino_icons: ^1.0.1\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n  integration_test:\n    sdk: flutter\n  flutter_lints: ^3.0.1\n\n# For information on the generic Dart part of this file, see the\n# following page: https://dart.dev/tools/pub/pubspec\n\n# The following section is specific to Flutter.\nflutter:\n\n  # The following line ensures that the Material Icons font is\n  # included with your application, so that you can use the icons in\n  # the material Icons class.\n  uses-material-design: true\n\n  # To add assets to your application, add an assets section, like this:\n  # assets:\n  #   - images/a_dot_burr.jpeg\n  #   - images/a_dot_ham.jpeg\n\n  # An image asset can refer to one or more resolution-specific \"variants\", see\n  # https://flutter.dev/assets-and-images/#resolution-aware.\n\n  # For details regarding adding assets from package dependencies, see\n  # https://flutter.dev/assets-and-images/#from-packages\n\n  # To add custom fonts to your application, add a fonts section here,\n  # in this \"flutter\" section. Each entry in this list should have a\n  # \"family\" key with the font family name, and a \"fonts\" key with a\n  # list giving the asset and other descriptors for the font. For\n  # example:\n  # fonts:\n  #   - family: Schyler\n  #     fonts:\n  #       - asset: fonts/Schyler-Regular.ttf\n  #       - asset: fonts/Schyler-Italic.ttf\n  #         style: italic\n  #   - family: Trajan Pro\n  #     fonts:\n  #       - asset: fonts/TrajanPro.ttf\n  #       - asset: fonts/TrajanPro_Bold.ttf\n  #         weight: 700\n  #\n  # For details regarding fonts from package dependencies,\n  # see https://flutter.dev/custom-fonts/#from-packages\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/test/widget_test.dart",
    "content": "// This is a basic Flutter widget test.\n//\n// To perform an interaction with a widget in your test, use the WidgetTester\n// utility that Flutter provides. For example, you can send tap and scroll\n// gestures. You can also use WidgetTester to find child widgets in the widget\n// tree, read text, and verify that the values of widget properties are correct.\n\nvoid main() {}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/windows/.gitignore",
    "content": "flutter/ephemeral/\n\n# Visual Studio user-specific files.\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# Visual Studio build-related files.\nx64/\nx86/\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/windows/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.14)\nproject(flowy_sdk_example LANGUAGES CXX)\n\nset(BINARY_NAME \"flowy_sdk_example\")\n\ncmake_policy(SET CMP0063 NEW)\n\nset(CMAKE_INSTALL_RPATH \"$ORIGIN/lib\")\n\n# Configure build options.\nget_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)\nif(IS_MULTICONFIG)\n  set(CMAKE_CONFIGURATION_TYPES \"Debug;Profile;Release\"\n    CACHE STRING \"\" FORCE)\nelse()\n  if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n    set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n      STRING \"Flutter build mode\" FORCE)\n    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n      \"Debug\" \"Profile\" \"Release\")\n  endif()\nendif()\n\nset(CMAKE_EXE_LINKER_FLAGS_PROFILE \"${CMAKE_EXE_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_SHARED_LINKER_FLAGS_PROFILE \"${CMAKE_SHARED_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_C_FLAGS_PROFILE \"${CMAKE_C_FLAGS_RELEASE}\")\nset(CMAKE_CXX_FLAGS_PROFILE \"${CMAKE_CXX_FLAGS_RELEASE}\")\n\n# Use Unicode for all projects.\nadd_definitions(-DUNICODE -D_UNICODE)\n\n# Compilation settings that should be applied to most targets.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_17)\n  target_compile_options(${TARGET} PRIVATE /W4 /WX /wd\"4100\")\n  target_compile_options(${TARGET} PRIVATE /EHsc)\n  target_compile_definitions(${TARGET} PRIVATE \"_HAS_EXCEPTIONS=0\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<CONFIG:Debug>:_DEBUG>\")\nendfunction()\n\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\n\n# Flutter library and tool build rules.\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# Application build\nadd_subdirectory(\"runner\")\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n\n# === Installation ===\n# Support files are copied into place next to the executable, so that it can\n# run in place. This is done instead of making a separate bundle (as on Linux)\n# so that building and running from within Visual Studio will work.\nset(BUILD_BUNDLE_DIR \"$<TARGET_FILE_DIR:${BINARY_NAME}>\")\n# Make the \"install\" step default, as it's required to run.\nset(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nif(PLUGIN_BUNDLED_LIBRARIES)\n  install(FILES \"${PLUGIN_BUNDLED_LIBRARIES}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n\n# Fully re-copy the assets directory on each build to avoid having stale files\n# from a previous install.\nset(FLUTTER_ASSET_DIR_NAME \"flutter_assets\")\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\\\")\n  \" COMPONENT Runtime)\ninstall(DIRECTORY \"${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}\"\n  DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\" COMPONENT Runtime)\n\n# Install the AOT library on non-Debug builds only.\ninstall(FILES \"${AOT_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  CONFIGURATIONS Profile;Release\n  COMPONENT Runtime)\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/windows/flutter/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.14)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\nset(WRAPPER_ROOT \"${EPHEMERAL_DIR}/cpp_client_wrapper\")\n\n# === Flutter Library ===\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/flutter_windows.dll\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/windows/app.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"flutter_export.h\"\n  \"flutter_windows.h\"\n  \"flutter_messenger.h\"\n  \"flutter_plugin_registrar.h\"\n  \"flutter_texture_registrar.h\"\n)\nlist(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND \"${EPHEMERAL_DIR}/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}.lib\")\nadd_dependencies(flutter flutter_assemble)\n\n# === Wrapper ===\nlist(APPEND CPP_WRAPPER_SOURCES_CORE\n  \"core_implementations.cc\"\n  \"standard_codec.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_PLUGIN\n  \"plugin_registrar.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_APP\n  \"flutter_engine.cc\"\n  \"flutter_view_controller.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND \"${WRAPPER_ROOT}/\")\n\n# Wrapper sources needed for a plugin.\nadd_library(flutter_wrapper_plugin STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n)\napply_standard_settings(flutter_wrapper_plugin)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  POSITION_INDEPENDENT_CODE ON)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  CXX_VISIBILITY_PRESET hidden)\ntarget_link_libraries(flutter_wrapper_plugin PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_plugin PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_plugin flutter_assemble)\n\n# Wrapper sources needed for the runner.\nadd_library(flutter_wrapper_app STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\napply_standard_settings(flutter_wrapper_app)\ntarget_link_libraries(flutter_wrapper_app PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_app PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_app flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nset(PHONY_OUTPUT \"${CMAKE_CURRENT_BINARY_DIR}/_phony_\")\nset_source_files_properties(\"${PHONY_OUTPUT}\" PROPERTIES SYMBOLIC TRUE)\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}\n    ${CPP_WRAPPER_SOURCES_APP}\n    ${PHONY_OUTPUT}\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat\"\n      windows-x64 $<CONFIG>\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/windows/runner/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.14)\nproject(runner LANGUAGES CXX)\n\nadd_executable(${BINARY_NAME} WIN32\n  \"flutter_window.cpp\"\n  \"main.cpp\"\n  \"utils.cpp\"\n  \"win32_window.cpp\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n  \"Runner.rc\"\n  \"runner.exe.manifest\"\n)\napply_standard_settings(${BINARY_NAME})\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"NOMINMAX\")\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)\ntarget_include_directories(${BINARY_NAME} PRIVATE \"${CMAKE_SOURCE_DIR}\")\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/windows/runner/Runner.rc",
    "content": "// Microsoft Visual C++ generated resource script.\n//\n#pragma code_page(65001)\n#include \"resource.h\"\n\n#define APSTUDIO_READONLY_SYMBOLS\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 2 resource.\n//\n#include \"winres.h\"\n\n/////////////////////////////////////////////////////////////////////////////\n#undef APSTUDIO_READONLY_SYMBOLS\n\n/////////////////////////////////////////////////////////////////////////////\n// English (United States) resources\n\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\nLANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US\n\n#ifdef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// TEXTINCLUDE\n//\n\n1 TEXTINCLUDE\nBEGIN\n    \"resource.h\\0\"\nEND\n\n2 TEXTINCLUDE\nBEGIN\n    \"#include \"\"winres.h\"\"\\r\\n\"\n    \"\\0\"\nEND\n\n3 TEXTINCLUDE\nBEGIN\n    \"\\r\\n\"\n    \"\\0\"\nEND\n\n#endif    // APSTUDIO_INVOKED\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Icon\n//\n\n// Icon with lowest ID value placed first to ensure application icon\n// remains consistent on all systems.\nIDI_APP_ICON            ICON                    \"resources\\\\app_icon.ico\"\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Version\n//\n\n#ifdef FLUTTER_BUILD_NUMBER\n#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER\n#else\n#define VERSION_AS_NUMBER 1,0,0\n#endif\n\n#ifdef FLUTTER_BUILD_NAME\n#define VERSION_AS_STRING #FLUTTER_BUILD_NAME\n#else\n#define VERSION_AS_STRING \"1.0.0\"\n#endif\n\nVS_VERSION_INFO VERSIONINFO\n FILEVERSION VERSION_AS_NUMBER\n PRODUCTVERSION VERSION_AS_NUMBER\n FILEFLAGSMASK VS_FFI_FILEFLAGSMASK\n#ifdef _DEBUG\n FILEFLAGS VS_FF_DEBUG\n#else\n FILEFLAGS 0x0L\n#endif\n FILEOS VOS__WINDOWS32\n FILETYPE VFT_APP\n FILESUBTYPE 0x0L\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904e4\"\n        BEGIN\n            VALUE \"CompanyName\", \"com.plugin\" \"\\0\"\n            VALUE \"FileDescription\", \"Demonstrates how to use the appflowy_backend plugin.\" \"\\0\"\n            VALUE \"FileVersion\", VERSION_AS_STRING \"\\0\"\n            VALUE \"InternalName\", \"flowy_sdk_example\" \"\\0\"\n            VALUE \"LegalCopyright\", \"Copyright (C) 2021 com.plugin. All rights reserved.\" \"\\0\"\n            VALUE \"OriginalFilename\", \"flowy_sdk_example.exe\" \"\\0\"\n            VALUE \"ProductName\", \"flowy_sdk_example\" \"\\0\"\n            VALUE \"ProductVersion\", VERSION_AS_STRING \"\\0\"\n        END\n    END\n    BLOCK \"VarFileInfo\"\n    BEGIN\n        VALUE \"Translation\", 0x409, 1252\n    END\nEND\n\n#endif    // English (United States) resources\n/////////////////////////////////////////////////////////////////////////////\n\n\n\n#ifndef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 3 resource.\n//\n\n\n/////////////////////////////////////////////////////////////////////////////\n#endif    // not APSTUDIO_INVOKED\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/windows/runner/flutter_window.cpp",
    "content": "#include \"flutter_window.h\"\n\n#include <optional>\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nFlutterWindow::FlutterWindow(const flutter::DartProject& project)\n    : project_(project) {}\n\nFlutterWindow::~FlutterWindow() {}\n\nbool FlutterWindow::OnCreate() {\n  if (!Win32Window::OnCreate()) {\n    return false;\n  }\n\n  RECT frame = GetClientArea();\n\n  // The size here must match the window dimensions to avoid unnecessary surface\n  // creation / destruction in the startup path.\n  flutter_controller_ = std::make_unique<flutter::FlutterViewController>(\n      frame.right - frame.left, frame.bottom - frame.top, project_);\n  // Ensure that basic setup of the controller was successful.\n  if (!flutter_controller_->engine() || !flutter_controller_->view()) {\n    return false;\n  }\n  RegisterPlugins(flutter_controller_->engine());\n  SetChildContent(flutter_controller_->view()->GetNativeWindow());\n  return true;\n}\n\nvoid FlutterWindow::OnDestroy() {\n  if (flutter_controller_) {\n    flutter_controller_ = nullptr;\n  }\n\n  Win32Window::OnDestroy();\n}\n\nLRESULT\nFlutterWindow::MessageHandler(HWND hwnd, UINT const message,\n                              WPARAM const wparam,\n                              LPARAM const lparam) noexcept {\n  // Give Flutter, including plugins, an opportunity to handle window messages.\n  if (flutter_controller_) {\n    std::optional<LRESULT> result =\n        flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,\n                                                      lparam);\n    if (result) {\n      return *result;\n    }\n  }\n\n  switch (message) {\n    case WM_FONTCHANGE:\n      flutter_controller_->engine()->ReloadSystemFonts();\n      break;\n  }\n\n  return Win32Window::MessageHandler(hwnd, message, wparam, lparam);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/windows/runner/flutter_window.h",
    "content": "#ifndef RUNNER_FLUTTER_WINDOW_H_\n#define RUNNER_FLUTTER_WINDOW_H_\n\n#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n\n#include <memory>\n\n#include \"win32_window.h\"\n\n// A window that does nothing but host a Flutter view.\nclass FlutterWindow : public Win32Window {\n public:\n  // Creates a new FlutterWindow hosting a Flutter view running |project|.\n  explicit FlutterWindow(const flutter::DartProject& project);\n  virtual ~FlutterWindow();\n\n protected:\n  // Win32Window:\n  bool OnCreate() override;\n  void OnDestroy() override;\n  LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,\n                         LPARAM const lparam) noexcept override;\n\n private:\n  // The project to run.\n  flutter::DartProject project_;\n\n  // The Flutter instance hosted by this window.\n  std::unique_ptr<flutter::FlutterViewController> flutter_controller_;\n};\n\n#endif  // RUNNER_FLUTTER_WINDOW_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/windows/runner/main.cpp",
    "content": "#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n#include <windows.h>\n\n#include \"flutter_window.h\"\n#include \"utils.h\"\n\nint APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,\n                      _In_ wchar_t *command_line, _In_ int show_command) {\n  // Attach to console when present (e.g., 'flutter run') or create a\n  // new console when running with a debugger.\n  if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {\n    CreateAndAttachConsole();\n  }\n\n  // Initialize COM, so that it is available for use in the library and/or\n  // plugins.\n  ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);\n\n  flutter::DartProject project(L\"data\");\n\n  std::vector<std::string> command_line_arguments =\n      GetCommandLineArguments();\n\n  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));\n\n  FlutterWindow window(project);\n  Win32Window::Point origin(10, 10);\n  Win32Window::Size size(1280, 720);\n  if (!window.CreateAndShow(L\"flowy_sdk_example\", origin, size)) {\n    return EXIT_FAILURE;\n  }\n  window.SetQuitOnClose(true);\n\n  ::MSG msg;\n  while (::GetMessage(&msg, nullptr, 0, 0)) {\n    ::TranslateMessage(&msg);\n    ::DispatchMessage(&msg);\n  }\n\n  ::CoUninitialize();\n  return EXIT_SUCCESS;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/windows/runner/resource.h",
    "content": "//{{NO_DEPENDENCIES}}\n// Microsoft Visual C++ generated include file.\n// Used by Runner.rc\n//\n#define IDI_APP_ICON                    101\n\n// Next default values for new objects\n//\n#ifdef APSTUDIO_INVOKED\n#ifndef APSTUDIO_READONLY_SYMBOLS\n#define _APS_NEXT_RESOURCE_VALUE        102\n#define _APS_NEXT_COMMAND_VALUE         40001\n#define _APS_NEXT_CONTROL_VALUE         1001\n#define _APS_NEXT_SYMED_VALUE           101\n#endif\n#endif\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/windows/runner/runner.exe.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2</dpiAwareness>\n    </windowsSettings>\n  </application>\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- Windows 10 -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"/>\n      <!-- Windows 8.1 -->\n      <supportedOS Id=\"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\"/>\n      <!-- Windows 8 -->\n      <supportedOS Id=\"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\"/>\n      <!-- Windows 7 -->\n      <supportedOS Id=\"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\"/>\n    </application>\n  </compatibility>\n</assembly>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/windows/runner/utils.cpp",
    "content": "#include \"utils.h\"\n\n#include <flutter_windows.h>\n#include <io.h>\n#include <stdio.h>\n#include <windows.h>\n\n#include <iostream>\n\nvoid CreateAndAttachConsole() {\n  if (::AllocConsole()) {\n    FILE *unused;\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stdout)) {\n      _dup2(_fileno(stdout), 1);\n    }\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stderr)) {\n      _dup2(_fileno(stdout), 2);\n    }\n    std::ios::sync_with_stdio();\n    FlutterDesktopResyncOutputStreams();\n  }\n}\n\nstd::vector<std::string> GetCommandLineArguments() {\n  // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.\n  int argc;\n  wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);\n  if (argv == nullptr) {\n    return std::vector<std::string>();\n  }\n\n  std::vector<std::string> command_line_arguments;\n\n  // Skip the first argument as it's the binary name.\n  for (int i = 1; i < argc; i++) {\n    command_line_arguments.push_back(Utf8FromUtf16(argv[i]));\n  }\n\n  ::LocalFree(argv);\n\n  return command_line_arguments;\n}\n\nstd::string Utf8FromUtf16(const wchar_t* utf16_string) {\n  if (utf16_string == nullptr) {\n    return std::string();\n  }\n  int target_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      -1, nullptr, 0, nullptr, nullptr);\n  if (target_length == 0) {\n    return std::string();\n  }\n  std::string utf8_string;\n  utf8_string.resize(target_length);\n  int converted_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      -1, utf8_string.data(),\n      target_length, nullptr, nullptr);\n  if (converted_length == 0) {\n    return std::string();\n  }\n  return utf8_string;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/windows/runner/utils.h",
    "content": "#ifndef RUNNER_UTILS_H_\n#define RUNNER_UTILS_H_\n\n#include <string>\n#include <vector>\n\n// Creates a console for the process, and redirects stdout and stderr to\n// it for both the runner and the Flutter library.\nvoid CreateAndAttachConsole();\n\n// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string\n// encoded in UTF-8. Returns an empty std::string on failure.\nstd::string Utf8FromUtf16(const wchar_t* utf16_string);\n\n// Gets the command line arguments passed in as a std::vector<std::string>,\n// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.\nstd::vector<std::string> GetCommandLineArguments();\n\n#endif  // RUNNER_UTILS_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/windows/runner/win32_window.cpp",
    "content": "#include \"win32_window.h\"\n\n#include <flutter_windows.h>\n\n#include \"resource.h\"\n\nnamespace {\n\nconstexpr const wchar_t kWindowClassName[] = L\"FLUTTER_RUNNER_WIN32_WINDOW\";\n\n// The number of Win32Window objects that currently exist.\nstatic int g_active_window_count = 0;\n\nusing EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);\n\n// Scale helper to convert logical scaler values to physical using passed in\n// scale factor\nint Scale(int source, double scale_factor) {\n  return static_cast<int>(source * scale_factor);\n}\n\n// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.\n// This API is only needed for PerMonitor V1 awareness mode.\nvoid EnableFullDpiSupportIfAvailable(HWND hwnd) {\n  HMODULE user32_module = LoadLibraryA(\"User32.dll\");\n  if (!user32_module) {\n    return;\n  }\n  auto enable_non_client_dpi_scaling =\n      reinterpret_cast<EnableNonClientDpiScaling*>(\n          GetProcAddress(user32_module, \"EnableNonClientDpiScaling\"));\n  if (enable_non_client_dpi_scaling != nullptr) {\n    enable_non_client_dpi_scaling(hwnd);\n    FreeLibrary(user32_module);\n  }\n}\n\n}  // namespace\n\n// Manages the Win32Window's window class registration.\nclass WindowClassRegistrar {\n public:\n  ~WindowClassRegistrar() = default;\n\n  // Returns the singleton registar instance.\n  static WindowClassRegistrar* GetInstance() {\n    if (!instance_) {\n      instance_ = new WindowClassRegistrar();\n    }\n    return instance_;\n  }\n\n  // Returns the name of the window class, registering the class if it hasn't\n  // previously been registered.\n  const wchar_t* GetWindowClass();\n\n  // Unregisters the window class. Should only be called if there are no\n  // instances of the window.\n  void UnregisterWindowClass();\n\n private:\n  WindowClassRegistrar() = default;\n\n  static WindowClassRegistrar* instance_;\n\n  bool class_registered_ = false;\n};\n\nWindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;\n\nconst wchar_t* WindowClassRegistrar::GetWindowClass() {\n  if (!class_registered_) {\n    WNDCLASS window_class{};\n    window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);\n    window_class.lpszClassName = kWindowClassName;\n    window_class.style = CS_HREDRAW | CS_VREDRAW;\n    window_class.cbClsExtra = 0;\n    window_class.cbWndExtra = 0;\n    window_class.hInstance = GetModuleHandle(nullptr);\n    window_class.hIcon =\n        LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));\n    window_class.hbrBackground = 0;\n    window_class.lpszMenuName = nullptr;\n    window_class.lpfnWndProc = Win32Window::WndProc;\n    RegisterClass(&window_class);\n    class_registered_ = true;\n  }\n  return kWindowClassName;\n}\n\nvoid WindowClassRegistrar::UnregisterWindowClass() {\n  UnregisterClass(kWindowClassName, nullptr);\n  class_registered_ = false;\n}\n\nWin32Window::Win32Window() {\n  ++g_active_window_count;\n}\n\nWin32Window::~Win32Window() {\n  --g_active_window_count;\n  Destroy();\n}\n\nbool Win32Window::CreateAndShow(const std::wstring& title,\n                                const Point& origin,\n                                const Size& size) {\n  Destroy();\n\n  const wchar_t* window_class =\n      WindowClassRegistrar::GetInstance()->GetWindowClass();\n\n  const POINT target_point = {static_cast<LONG>(origin.x),\n                              static_cast<LONG>(origin.y)};\n  HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);\n  UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);\n  double scale_factor = dpi / 96.0;\n\n  HWND window = CreateWindow(\n      window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,\n      Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),\n      Scale(size.width, scale_factor), Scale(size.height, scale_factor),\n      nullptr, nullptr, GetModuleHandle(nullptr), this);\n\n  if (!window) {\n    return false;\n  }\n\n  return OnCreate();\n}\n\n// static\nLRESULT CALLBACK Win32Window::WndProc(HWND const window,\n                                      UINT const message,\n                                      WPARAM const wparam,\n                                      LPARAM const lparam) noexcept {\n  if (message == WM_NCCREATE) {\n    auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);\n    SetWindowLongPtr(window, GWLP_USERDATA,\n                     reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));\n\n    auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);\n    EnableFullDpiSupportIfAvailable(window);\n    that->window_handle_ = window;\n  } else if (Win32Window* that = GetThisFromHandle(window)) {\n    return that->MessageHandler(window, message, wparam, lparam);\n  }\n\n  return DefWindowProc(window, message, wparam, lparam);\n}\n\nLRESULT\nWin32Window::MessageHandler(HWND hwnd,\n                            UINT const message,\n                            WPARAM const wparam,\n                            LPARAM const lparam) noexcept {\n  switch (message) {\n    case WM_DESTROY:\n      window_handle_ = nullptr;\n      Destroy();\n      if (quit_on_close_) {\n        PostQuitMessage(0);\n      }\n      return 0;\n\n    case WM_DPICHANGED: {\n      auto newRectSize = reinterpret_cast<RECT*>(lparam);\n      LONG newWidth = newRectSize->right - newRectSize->left;\n      LONG newHeight = newRectSize->bottom - newRectSize->top;\n\n      SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,\n                   newHeight, SWP_NOZORDER | SWP_NOACTIVATE);\n\n      return 0;\n    }\n    case WM_SIZE: {\n      RECT rect = GetClientArea();\n      if (child_content_ != nullptr) {\n        // Size and position the child window.\n        MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,\n                   rect.bottom - rect.top, TRUE);\n      }\n      return 0;\n    }\n\n    case WM_ACTIVATE:\n      if (child_content_ != nullptr) {\n        SetFocus(child_content_);\n      }\n      return 0;\n  }\n\n  return DefWindowProc(window_handle_, message, wparam, lparam);\n}\n\nvoid Win32Window::Destroy() {\n  OnDestroy();\n\n  if (window_handle_) {\n    DestroyWindow(window_handle_);\n    window_handle_ = nullptr;\n  }\n  if (g_active_window_count == 0) {\n    WindowClassRegistrar::GetInstance()->UnregisterWindowClass();\n  }\n}\n\nWin32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {\n  return reinterpret_cast<Win32Window*>(\n      GetWindowLongPtr(window, GWLP_USERDATA));\n}\n\nvoid Win32Window::SetChildContent(HWND content) {\n  child_content_ = content;\n  SetParent(content, window_handle_);\n  RECT frame = GetClientArea();\n\n  MoveWindow(content, frame.left, frame.top, frame.right - frame.left,\n             frame.bottom - frame.top, true);\n\n  SetFocus(child_content_);\n}\n\nRECT Win32Window::GetClientArea() {\n  RECT frame;\n  GetClientRect(window_handle_, &frame);\n  return frame;\n}\n\nHWND Win32Window::GetHandle() {\n  return window_handle_;\n}\n\nvoid Win32Window::SetQuitOnClose(bool quit_on_close) {\n  quit_on_close_ = quit_on_close;\n}\n\nbool Win32Window::OnCreate() {\n  // No-op; provided for subclasses.\n  return true;\n}\n\nvoid Win32Window::OnDestroy() {\n  // No-op; provided for subclasses.\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/example/windows/runner/win32_window.h",
    "content": "#ifndef RUNNER_WIN32_WINDOW_H_\n#define RUNNER_WIN32_WINDOW_H_\n\n#include <windows.h>\n\n#include <functional>\n#include <memory>\n#include <string>\n\n// A class abstraction for a high DPI-aware Win32 Window. Intended to be\n// inherited from by classes that wish to specialize with custom\n// rendering and input handling\nclass Win32Window {\n public:\n  struct Point {\n    unsigned int x;\n    unsigned int y;\n    Point(unsigned int x, unsigned int y) : x(x), y(y) {}\n  };\n\n  struct Size {\n    unsigned int width;\n    unsigned int height;\n    Size(unsigned int width, unsigned int height)\n        : width(width), height(height) {}\n  };\n\n  Win32Window();\n  virtual ~Win32Window();\n\n  // Creates and shows a win32 window with |title| and position and size using\n  // |origin| and |size|. New windows are created on the default monitor. Window\n  // sizes are specified to the OS in physical pixels, hence to ensure a\n  // consistent size to will treat the width height passed in to this function\n  // as logical pixels and scale to appropriate for the default monitor. Returns\n  // true if the window was created successfully.\n  bool CreateAndShow(const std::wstring& title,\n                     const Point& origin,\n                     const Size& size);\n\n  // Release OS resources associated with window.\n  void Destroy();\n\n  // Inserts |content| into the window tree.\n  void SetChildContent(HWND content);\n\n  // Returns the backing Window handle to enable clients to set icon and other\n  // window properties. Returns nullptr if the window has been destroyed.\n  HWND GetHandle();\n\n  // If true, closing this window will quit the application.\n  void SetQuitOnClose(bool quit_on_close);\n\n  // Return a RECT representing the bounds of the current client area.\n  RECT GetClientArea();\n\n protected:\n  // Processes and route salient window messages for mouse handling,\n  // size change and DPI. Delegates handling of these to member overloads that\n  // inheriting classes can handle.\n  virtual LRESULT MessageHandler(HWND window,\n                                 UINT const message,\n                                 WPARAM const wparam,\n                                 LPARAM const lparam) noexcept;\n\n  // Called when CreateAndShow is called, allowing subclass window-related\n  // setup. Subclasses should return false if setup fails.\n  virtual bool OnCreate();\n\n  // Called when Destroy is called.\n  virtual void OnDestroy();\n\n private:\n  friend class WindowClassRegistrar;\n\n  // OS callback called by message pump. Handles the WM_NCCREATE message which\n  // is passed when the non-client area is being created and enables automatic\n  // non-client DPI scaling so that the non-client area automatically\n  // responsponds to changes in DPI. All other messages are handled by\n  // MessageHandler.\n  static LRESULT CALLBACK WndProc(HWND const window,\n                                  UINT const message,\n                                  WPARAM const wparam,\n                                  LPARAM const lparam) noexcept;\n\n  // Retrieves a class instance pointer for |window|\n  static Win32Window* GetThisFromHandle(HWND const window) noexcept;\n\n  bool quit_on_close_ = false;\n\n  // window handle for top level window.\n  HWND window_handle_ = nullptr;\n\n  // window handle for hosted content.\n  HWND child_content_ = nullptr;\n};\n\n#endif  // RUNNER_WIN32_WINDOW_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/ios/.gitignore",
    "content": ".idea/\n.vagrant/\n.sconsign.dblite\n.svn/\n\n.DS_Store\n*.swp\nprofile\n\nDerivedData/\nbuild/\nGeneratedPluginRegistrant.h\nGeneratedPluginRegistrant.m\n\n.generated/\n\n*.pbxuser\n*.mode1v3\n*.mode2v3\n*.perspectivev3\n\n!default.pbxuser\n!default.mode1v3\n!default.mode2v3\n!default.perspectivev3\n\nxcuserdata\n\n*.moved-aside\n\n*.pyc\n*sync/\nIcon?\n.tags*\n\n/Flutter/Generated.xcconfig\n/Flutter/flutter_export_environment.sh"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/ios/Assets/.gitkeep",
    "content": ""
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/ios/Classes/AppFlowyBackendPlugin.h",
    "content": "#import <Flutter/Flutter.h>\n\n@interface AppFlowyBackendPlugin : NSObject<FlutterPlugin>\n@end\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/ios/Classes/AppFlowyBackendPlugin.m",
    "content": "#import \"AppFlowyBackendPlugin.h\"\n#if __has_include(<appflowy_backend/appflowy_backend-Swift.h>)\n#import <appflowy_backend/appflowy_backend-Swift.h>\n#else\n// Support project import fallback if the generated compatibility header\n// is not copied when this plugin is created as a library.\n// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816\n#import \"appflowy_backend-Swift.h\"\n#endif\n\n@implementation AppFlowyBackendPlugin\n+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {\n  [SwiftAppFlowyBackendPlugin registerWithRegistrar:registrar];\n}\n@end\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/ios/Classes/AppFlowyBackendPlugin.swift",
    "content": "import Flutter\nimport UIKit\n\npublic class SwiftAppFlowyBackendPlugin: NSObject, FlutterPlugin {\n  public static func register(with registrar: FlutterPluginRegistrar) {\n    let channel = FlutterMethodChannel(name: \"appflowy_backend\", binaryMessenger: registrar.messenger())\n    let instance = SwiftAppFlowyBackendPlugin()\n    registrar.addMethodCallDelegate(instance, channel: channel)\n  }\n\n  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {\n    result(\"iOS \" + UIDevice.current.systemVersion)\n  }\n\n  public static func dummyMethodToEnforceBundling() {\n    link_me_please()\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/ios/Classes/binding.h",
    "content": "#include <stdarg.h>\n#include <stdbool.h>\n#include <stdint.h>\n#include <stdlib.h>\n\nint64_t init_sdk(int64_t port, char *data);\n\nvoid async_event(int64_t port, const uint8_t *input, uintptr_t len);\n\nconst uint8_t *sync_event(const uint8_t *input, uintptr_t len);\n\nint32_t set_stream_port(int64_t port);\n\nint32_t set_log_stream_port(int64_t port);\n\nvoid link_me_please(void);\n\nvoid rust_log(int64_t level, const char *data);\n\nvoid set_env(const char *data);\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/ios/appflowy_backend.podspec",
    "content": "#\n# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.\n# Run `pod lib lint appflowy_backend.podspec' to validate before publishing.\n#\nPod::Spec.new do |s|\n  s.name             = 'appflowy_backend'\n  s.version          = '0.0.1'\n  s.summary          = 'A new flutter plugin project.'\n  s.description      = <<-DESC\nA new flutter plugin project.\n                       DESC\n  s.homepage         = 'http://example.com'\n  s.license          = { :file => '../LICENSE' }\n  s.author           = { 'AppFlowy' => 'annie@appflowy.io' }\n  s.source           = { :path => '.' }\n  s.source_files = 'Classes/**/*'\n  s.dependency 'Flutter'\n  s.platform = :ios, '8.0'\n\n  # Flutter.framework does not contain a i386 slice.\n  s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }\n  s.swift_version = '5.0'\n  s.static_framework = true\n  s.vendored_libraries = \"libdart_ffi.a\"\n  s.library = \"c++\"\nend\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi';\nimport 'dart:io';\nimport 'dart:isolate';\n\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/rust_stream.dart';\nimport 'package:ffi/ffi.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\n\nimport 'ffi.dart' as ffi;\n\nexport 'package:async/async.dart';\n\nenum ExceptionType {\n  AppearanceSettingsIsEmpty,\n}\n\nclass FlowySDKException implements Exception {\n  ExceptionType type;\n  FlowySDKException(this.type);\n}\n\nclass FlowySDK {\n  static const MethodChannel _channel = MethodChannel('appflowy_backend');\n  static Future<String> get platformVersion async {\n    final String version = await _channel.invokeMethod('getPlatformVersion');\n    return version;\n  }\n\n  FlowySDK();\n\n  Future<void> dispose() async {}\n\n  Future<void> init(String configuration) async {\n    ffi.set_stream_port(RustStreamReceiver.shared.port);\n    ffi.store_dart_post_cobject(NativeApi.postCObject);\n\n    // On iOS, VSCode can't print logs from Rust, so we need to use a different method to print logs.\n    // 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.\n    if (Platform.isIOS && kDebugMode) {\n      ffi.set_log_stream_port(RustLogStreamReceiver.logShared.port);\n    }\n\n    // final completer = Completer<Uint8List>();\n    // // Create a SendPort that accepts only one message.\n    // final sendPort = singleCompletePort(completer);\n\n    final code = ffi.init_sdk(0, configuration.toNativeUtf8());\n    if (code != 0) {\n      throw Exception('Failed to initialize the SDK');\n    }\n    // return completer.future;\n  }\n}\n\nclass RustLogStreamReceiver {\n  static RustLogStreamReceiver logShared = RustLogStreamReceiver._internal();\n  late RawReceivePort _ffiPort;\n  late StreamController<Uint8List> _streamController;\n  late StreamSubscription<Uint8List> _subscription;\n  int get port => _ffiPort.sendPort.nativePort;\n\n  RustLogStreamReceiver._internal() {\n    _ffiPort = RawReceivePort();\n    _streamController = StreamController();\n    _ffiPort.handler = _streamController.add;\n\n    _subscription = _streamController.stream.listen((data) {\n      String decodedString = utf8.decode(data);\n      Log.info(decodedString);\n    });\n  }\n\n  factory RustLogStreamReceiver() {\n    return logShared;\n  }\n\n  Future<void> dispose() async {\n    await _streamController.close();\n    await _subscription.cancel();\n    _ffiPort.close();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend_method_channel.dart",
    "content": "import 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\n\nimport 'appflowy_backend_platform_interface.dart';\n\n/// An implementation of [AppFlowyBackendPlatform] that uses method channels.\nclass MethodChannelFlowySdk extends AppFlowyBackendPlatform {\n  /// The method channel used to interact with the native platform.\n  @visibleForTesting\n  final methodChannel = const MethodChannel('appflowy_backend');\n\n  @override\n  Future<String?> getPlatformVersion() async {\n    final version =\n        await methodChannel.invokeMethod<String>('getPlatformVersion');\n    return version;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend_platform_interface.dart",
    "content": "import 'package:plugin_platform_interface/plugin_platform_interface.dart';\n\nimport 'appflowy_backend_method_channel.dart';\n\nabstract class AppFlowyBackendPlatform extends PlatformInterface {\n  /// Constructs a FlowySdkPlatform.\n  AppFlowyBackendPlatform() : super(token: _token);\n\n  static final Object _token = Object();\n\n  static AppFlowyBackendPlatform _instance = MethodChannelFlowySdk();\n\n  /// The default instance of [AppFlowyBackendPlatform] to use.\n  ///\n  /// Defaults to [MethodChannelFlowySdk].\n  static AppFlowyBackendPlatform get instance => _instance;\n\n  /// Platform-specific implementations should set this with their own\n  /// platform-specific class that extends [AppFlowyBackendPlatform] when\n  /// they register themselves.\n  static set instance(AppFlowyBackendPlatform instance) {\n    PlatformInterface.verifyToken(instance, _token);\n    _instance = instance;\n  }\n\n  Future<String?> getPlatformVersion() {\n    throw UnimplementedError('platformVersion() has not been implemented.');\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/dispatch.dart",
    "content": "import 'dart:async';\nimport 'dart:convert' show utf8;\nimport 'dart:ffi';\nimport 'dart:typed_data';\n\nimport 'package:flutter/services.dart';\n\nimport 'package:appflowy_backend/ffi.dart' as ffi;\nimport 'package:appflowy_backend/log.dart';\n// ignore: unnecessary_import\nimport 'package:appflowy_backend/protobuf/dart-ffi/ffi_response.pb.dart';\nimport 'package:appflowy_backend/protobuf/dart-ffi/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-search/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-storage/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:ffi/ffi.dart';\nimport 'package:isolates/isolates.dart';\nimport 'package:isolates/ports.dart';\nimport 'package:protobuf/protobuf.dart';\n\nimport '../protobuf/flowy-date/entities.pb.dart';\nimport '../protobuf/flowy-date/event_map.pb.dart';\n\nimport 'error.dart';\n\npart 'dart_event/flowy-folder/dart_event.dart';\npart 'dart_event/flowy-user/dart_event.dart';\npart 'dart_event/flowy-database2/dart_event.dart';\npart 'dart_event/flowy-document/dart_event.dart';\npart 'dart_event/flowy-date/dart_event.dart';\npart 'dart_event/flowy-search/dart_event.dart';\npart 'dart_event/flowy-ai/dart_event.dart';\npart 'dart_event/flowy-storage/dart_event.dart';\n\nenum FFIException {\n  RequestIsEmpty,\n}\n\nclass DispatchException implements Exception {\n  FFIException type;\n  DispatchException(this.type);\n}\n\nclass Dispatch {\n  static bool enableTracing = false;\n\n  static Future<FlowyResult<Uint8List, Uint8List>> asyncRequest(\n    FFIRequest request,\n  ) async {\n    Future<FlowyResult<Uint8List, Uint8List>> _asyncRequest() async {\n      final bytesFuture = _sendToRust(request);\n      final response = await _extractResponse(bytesFuture);\n      final payload = _extractPayload(response);\n      return payload;\n    }\n\n    if (enableTracing) {\n      final start = DateTime.now();\n      final result = await _asyncRequest();\n      final duration = DateTime.now().difference(start);\n      Log.debug('Dispatch ${request.event} took ${duration.inMilliseconds}ms');\n      return result;\n    }\n\n    return _asyncRequest();\n  }\n}\n\nFlowyResult<Uint8List, Uint8List> _extractPayload(\n  FlowyResult<FFIResponse, FlowyInternalError> response,\n) {\n  return response.fold(\n    (response) {\n      switch (response.code) {\n        case FFIStatusCode.Ok:\n          return FlowySuccess(Uint8List.fromList(response.payload));\n        case FFIStatusCode.Err:\n          final errorBytes = Uint8List.fromList(response.payload);\n          GlobalErrorCodeNotifier.receiveErrorBytes(errorBytes);\n          return FlowyFailure(errorBytes);\n        case FFIStatusCode.Internal:\n          final error = utf8.decode(response.payload);\n          Log.error(\"Dispatch internal error: $error\");\n          return FlowyFailure(emptyBytes());\n        default:\n          Log.error(\"Impossible to here\");\n          return FlowyFailure(emptyBytes());\n      }\n    },\n    (error) {\n      Log.error(\"Response should not be empty $error\");\n      return FlowyFailure(emptyBytes());\n    },\n  );\n}\n\nFuture<FlowyResult<FFIResponse, FlowyInternalError>> _extractResponse(\n  Completer<Uint8List> bytesFuture,\n) async {\n  final bytes = await bytesFuture.future;\n  try {\n    final response = FFIResponse.fromBuffer(bytes);\n    return FlowySuccess(response);\n  } catch (e, s) {\n    final error = StackTraceError(e, s);\n    Log.error('Deserialize response failed. ${error.toString()}');\n    return FlowyFailure(error.asFlowyError());\n  }\n}\n\nCompleter<Uint8List> _sendToRust(FFIRequest request) {\n  Uint8List bytes = request.writeToBuffer();\n  assert(bytes.isEmpty == false);\n  if (bytes.isEmpty) {\n    throw DispatchException(FFIException.RequestIsEmpty);\n  }\n\n  final Pointer<Uint8> input = calloc.allocate<Uint8>(bytes.length);\n  final list = input.asTypedList(bytes.length);\n  list.setAll(0, bytes);\n\n  final completer = Completer<Uint8List>();\n  final port = singleCompletePort(completer);\n  ffi.async_event(port.nativePort, input, bytes.length);\n  calloc.free(input);\n\n  return completer;\n}\n\nUint8List requestToBytes<T extends GeneratedMessage>(T? message) {\n  try {\n    if (message != null) {\n      return message.writeToBuffer();\n    } else {\n      return emptyBytes();\n    }\n  } catch (e, s) {\n    final error = StackTraceError(e, s);\n    Log.error('Serial request failed. ${error.toString()}');\n    return emptyBytes();\n  }\n}\n\nUint8List emptyBytes() {\n  return Uint8List.fromList([]);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/error.dart",
    "content": "import 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_backend/protobuf/dart-ffi/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';\nimport 'package:flutter/foundation.dart';\n\nclass FlowyInternalError {\n  late FFIStatusCode _statusCode;\n  late String _error;\n\n  FFIStatusCode get statusCode {\n    return _statusCode;\n  }\n\n  String get error {\n    return _error;\n  }\n\n  bool get has_error {\n    return _statusCode != FFIStatusCode.Ok;\n  }\n\n  String toString() {\n    return \"$_statusCode: $_error\";\n  }\n\n  FlowyInternalError({\n    required FFIStatusCode statusCode,\n    required String error,\n  }) {\n    _statusCode = statusCode;\n    _error = error;\n  }\n}\n\nclass StackTraceError {\n  Object error;\n  StackTrace trace;\n  StackTraceError(\n    this.error,\n    this.trace,\n  );\n\n  FlowyInternalError asFlowyError() {\n    return FlowyInternalError(\n        statusCode: FFIStatusCode.Err, error: this.toString());\n  }\n\n  String toString() {\n    return '${error.runtimeType}. Stack trace: $trace';\n  }\n}\n\ntypedef void ErrorListener();\n\n/// Receive error when Rust backend send error message back to the flutter frontend\n///\nclass GlobalErrorCodeNotifier extends ChangeNotifier {\n  // Static instance with lazy initialization\n  static final GlobalErrorCodeNotifier _instance =\n      GlobalErrorCodeNotifier._internal();\n\n  FlowyError? _error;\n\n  // Private internal constructor\n  GlobalErrorCodeNotifier._internal();\n\n  // Factory constructor to return the same instance\n  factory GlobalErrorCodeNotifier() {\n    return _instance;\n  }\n\n  static void receiveError(FlowyError error) {\n    if (_instance._error?.code != error.code) {\n      _instance._error = error;\n      _instance.notifyListeners();\n    }\n  }\n\n  static void receiveErrorBytes(Uint8List bytes) {\n    try {\n      final error = FlowyError.fromBuffer(bytes);\n      if (_instance._error?.code != error.code) {\n        _instance._error = error;\n        _instance.notifyListeners();\n      }\n    } catch (e) {\n      Log.error(\"Can not parse error bytes: $e\");\n    }\n  }\n\n  static ErrorListener add({\n    required void Function(FlowyError error) onError,\n    bool Function(FlowyError code)? onErrorIf,\n  }) {\n    void listener() {\n      final error = _instance._error;\n      if (error != null) {\n        if (onErrorIf == null || onErrorIf(error)) {\n          onError(error);\n        }\n      }\n    }\n\n    _instance.addListener(listener);\n    return listener;\n  }\n\n  static void remove(ErrorListener listener) {\n    _instance.removeListener(listener);\n  }\n}\n\nextension FlowyErrorExtension on FlowyError {\n  bool get isAIResponseLimitExceeded =>\n      code == ErrorCode.AIResponseLimitExceeded;\n\n  bool get isStorageLimitExceeded => code == ErrorCode.FileStorageLimitExceeded;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/lib/ffi.dart",
    "content": "/// bindings for `libdart_ffi`\n\nimport 'dart:ffi';\nimport 'dart:io';\n\n// ignore: import_of_legacy_library_into_null_safe\nimport 'package:ffi/ffi.dart' as ffi;\nimport 'package:flutter/foundation.dart' as Foundation;\n\n// ignore_for_file: unused_import, camel_case_types, non_constant_identifier_names\nfinal DynamicLibrary _dart_ffi_lib = _open();\n\n/// Reference to the Dynamic Library, it should be only used for low-level access\nfinal DynamicLibrary dl = _dart_ffi_lib;\nDynamicLibrary _open() {\n  if (Platform.environment.containsKey('FLUTTER_TEST')) {\n    final prefix = \"${Directory.current.path}/.sandbox\";\n    if (Platform.isLinux)\n      return DynamicLibrary.open('${prefix}/libdart_ffi.so');\n    if (Platform.isAndroid)\n      return DynamicLibrary.open('${prefix}/libdart_ffi.so');\n    if (Platform.isMacOS)\n      return DynamicLibrary.open('${prefix}/libdart_ffi.dylib');\n    if (Platform.isIOS) return DynamicLibrary.open('${prefix}/libdart_ffi.a');\n    if (Platform.isWindows)\n      return DynamicLibrary.open('${prefix}/dart_ffi.dll');\n  } else {\n    if (Platform.isLinux) return DynamicLibrary.open('libdart_ffi.so');\n    if (Platform.isAndroid) return DynamicLibrary.open('libdart_ffi.so');\n    if (Platform.isMacOS) return DynamicLibrary.executable();\n    if (Platform.isIOS) return DynamicLibrary.executable();\n    if (Platform.isWindows) return DynamicLibrary.open('dart_ffi.dll');\n  }\n\n  throw UnsupportedError('This platform is not supported.');\n}\n\n/// C function `async_event`.\nvoid async_event(\n  int port,\n  Pointer<Uint8> input,\n  int len,\n) {\n  _invoke_async(port, input, len);\n}\n\nfinal _invoke_async_Dart _invoke_async = _dart_ffi_lib\n    .lookupFunction<_invoke_async_C, _invoke_async_Dart>('async_event');\ntypedef _invoke_async_C = Void Function(\n  Int64 port,\n  Pointer<Uint8> input,\n  Uint64 len,\n);\ntypedef _invoke_async_Dart = void Function(\n  int port,\n  Pointer<Uint8> input,\n  int len,\n);\n\n/// C function `sync_event`.\nPointer<Uint8> sync_event(\n  Pointer<Uint8> input,\n  int len,\n) {\n  return _invoke_sync(input, len);\n}\n\nfinal _invoke_sync_Dart _invoke_sync = _dart_ffi_lib\n    .lookupFunction<_invoke_sync_C, _invoke_sync_Dart>('sync_event');\ntypedef _invoke_sync_C = Pointer<Uint8> Function(\n  Pointer<Uint8> input,\n  Uint64 len,\n);\ntypedef _invoke_sync_Dart = Pointer<Uint8> Function(\n  Pointer<Uint8> input,\n  int len,\n);\n\n/// C function `init_sdk`.\nint init_sdk(\n  int port,\n  Pointer<ffi.Utf8> data,\n) {\n  return _init_sdk(port, data);\n}\n\nfinal _init_sdk_Dart _init_sdk =\n    _dart_ffi_lib.lookupFunction<_init_sdk_C, _init_sdk_Dart>('init_sdk');\ntypedef _init_sdk_C = Int64 Function(\n  Int64 port,\n  Pointer<ffi.Utf8> path,\n);\ntypedef _init_sdk_Dart = int Function(\n  int port,\n  Pointer<ffi.Utf8> path,\n);\n\n/// C function `init_stream`.\nint set_stream_port(int port) {\n  return _set_stream_port(port);\n}\n\nfinal _set_stream_port_Dart _set_stream_port =\n    _dart_ffi_lib.lookupFunction<_set_stream_port_C, _set_stream_port_Dart>(\n        'set_stream_port');\n\ntypedef _set_stream_port_C = Int32 Function(\n  Int64 port,\n);\ntypedef _set_stream_port_Dart = int Function(\n  int port,\n);\n\n/// C function `set log stream port`.\nint set_log_stream_port(int port) {\n  return _set_log_stream_port(port);\n}\n\nfinal _set_log_stream_port_Dart _set_log_stream_port = _dart_ffi_lib\n    .lookupFunction<_set_log_stream_port_C, _set_log_stream_port_Dart>(\n        'set_log_stream_port');\n\ntypedef _set_log_stream_port_C = Int32 Function(\n  Int64 port,\n);\ntypedef _set_log_stream_port_Dart = int Function(\n  int port,\n);\n\n/// C function `link_me_please`.\nvoid link_me_please() {\n  _link_me_please();\n}\n\nfinal _link_me_please_Dart _link_me_please = _dart_ffi_lib\n    .lookupFunction<_link_me_please_C, _link_me_please_Dart>('link_me_please');\ntypedef _link_me_please_C = Void Function();\ntypedef _link_me_please_Dart = void Function();\n\n/// Binding to `allo-isolate` crate\nvoid store_dart_post_cobject(\n  Pointer<NativeFunction<Int8 Function(Int64, Pointer<Dart_CObject>)>> ptr,\n) {\n  _store_dart_post_cobject(ptr);\n}\n\nfinal _store_dart_post_cobject_Dart _store_dart_post_cobject = _dart_ffi_lib\n    .lookupFunction<_store_dart_post_cobject_C, _store_dart_post_cobject_Dart>(\n        'store_dart_post_cobject');\ntypedef _store_dart_post_cobject_C = Void Function(\n  Pointer<NativeFunction<Int8 Function(Int64, Pointer<Dart_CObject>)>> ptr,\n);\ntypedef _store_dart_post_cobject_Dart = void Function(\n  Pointer<NativeFunction<Int8 Function(Int64, Pointer<Dart_CObject>)>> ptr,\n);\n\nvoid rust_log(\n  int level,\n  Pointer<ffi.Utf8> data,\n) {\n  _invoke_rust_log(level, data);\n}\n\nfinal _invoke_rust_log_Dart _invoke_rust_log = _dart_ffi_lib\n    .lookupFunction<_invoke_rust_log_C, _invoke_rust_log_Dart>('rust_log');\ntypedef _invoke_rust_log_C = Void Function(\n  Int64 level,\n  Pointer<ffi.Utf8> data,\n);\ntypedef _invoke_rust_log_Dart = void Function(\n  int level,\n  Pointer<ffi.Utf8>,\n);\n\n/// C function `set_env`.\nvoid set_env(\n  Pointer<ffi.Utf8> data,\n) {\n  _set_env(data);\n}\n\nfinal _set_env_Dart _set_env =\n    _dart_ffi_lib.lookupFunction<_set_env_C, _set_env_Dart>('set_env');\ntypedef _set_env_C = Void Function(\n  Pointer<ffi.Utf8> data,\n);\ntypedef _set_env_Dart = void Function(\n  Pointer<ffi.Utf8> data,\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart",
    "content": "// ignore: import_of_legacy_library_into_null_safe\nimport 'dart:ffi';\n\nimport 'package:ffi/ffi.dart' as ffi;\nimport 'package:flutter/foundation.dart';\nimport 'package:talker/talker.dart';\n\nimport 'ffi.dart';\n\nclass Log {\n  static final shared = Log();\n\n  late Talker _logger;\n\n  bool enableFlutterLog = true;\n\n  // used to disable log in tests\n  bool disableLog = false;\n\n  Log() {\n    _logger = Talker(\n      filter: LogLevelTalkerFilter(),\n    );\n  }\n\n  // Generic internal logging function to reduce code duplication\n  static void _log(\n    LogLevel level,\n    int rustLevel,\n    dynamic msg, [\n    dynamic error,\n    StackTrace? stackTrace,\n  ]) {\n    // only forward logs to flutter in debug mode, otherwise log to rust to\n    // persist logs in the file system\n    if (shared.enableFlutterLog && kDebugMode) {\n      shared._logger.log(msg, logLevel: level, stackTrace: stackTrace);\n    } else {\n      String formattedMessage = _formatMessageWithStackTrace(msg, stackTrace);\n      rust_log(rustLevel, toNativeUtf8(formattedMessage));\n    }\n  }\n\n  static void info(dynamic msg, [dynamic error, StackTrace? stackTrace]) {\n    if (shared.disableLog) {\n      return;\n    }\n\n    _log(LogLevel.info, 0, msg, error, stackTrace);\n  }\n\n  static void debug(dynamic msg, [dynamic error, StackTrace? stackTrace]) {\n    if (shared.disableLog) {\n      return;\n    }\n\n    _log(LogLevel.debug, 1, msg, error, stackTrace);\n  }\n\n  static void warn(dynamic msg, [dynamic error, StackTrace? stackTrace]) {\n    if (shared.disableLog) {\n      return;\n    }\n\n    _log(LogLevel.warning, 3, msg, error, stackTrace);\n  }\n\n  static void trace(dynamic msg, [dynamic error, StackTrace? stackTrace]) {\n    if (shared.disableLog) {\n      return;\n    }\n\n    _log(LogLevel.verbose, 2, msg, error, stackTrace);\n  }\n\n  static void error(dynamic msg, [dynamic error, StackTrace? stackTrace]) {\n    if (shared.disableLog) {\n      return;\n    }\n\n    _log(LogLevel.error, 4, msg, error, stackTrace);\n  }\n}\n\nbool isReleaseVersion() {\n  return kReleaseMode;\n}\n\n// Utility to convert a message to native Utf8 (used in rust_log)\nPointer<ffi.Utf8> toNativeUtf8(dynamic msg) {\n  return \"$msg\".toNativeUtf8();\n}\n\nString _formatMessageWithStackTrace(dynamic msg, StackTrace? stackTrace) {\n  if (stackTrace != null) {\n    return \"$msg\\nStackTrace:\\n$stackTrace\"; // Append the stack trace to the message\n  }\n  return msg.toString();\n}\n\nclass LogLevelTalkerFilter implements TalkerFilter {\n  @override\n  bool filter(TalkerData data) {\n    // filter out the debug logs in release mode\n    return kDebugMode ? true : data.logLevel != LogLevel.debug;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/lib/rust_stream.dart",
    "content": "import 'dart:async';\nimport 'dart:ffi';\nimport 'dart:isolate';\nimport 'dart:typed_data';\nimport 'package:appflowy_backend/log.dart';\n\nimport 'protobuf/flowy-notification/subject.pb.dart';\n\ntypedef ObserverCallback = void Function(SubscribeObject observable);\n\nclass RustStreamReceiver {\n  static RustStreamReceiver shared = RustStreamReceiver._internal();\n  late RawReceivePort _ffiPort;\n  late StreamController<Uint8List> _streamController;\n  late StreamController<SubscribeObject> _observableController;\n  late StreamSubscription<Uint8List> _ffiSubscription;\n\n  int get port => _ffiPort.sendPort.nativePort;\n  StreamController<SubscribeObject> get observable => _observableController;\n\n  RustStreamReceiver._internal() {\n    _ffiPort = RawReceivePort();\n    _streamController = StreamController();\n    _observableController = StreamController.broadcast();\n\n    _ffiPort.handler = _streamController.add;\n    _ffiSubscription = _streamController.stream.listen(_streamCallback);\n  }\n\n  factory RustStreamReceiver() {\n    return shared;\n  }\n\n  static StreamSubscription<SubscribeObject> listen(\n      void Function(SubscribeObject subject) callback) {\n    return RustStreamReceiver.shared.observable.stream.listen(callback);\n  }\n\n  void _streamCallback(Uint8List bytes) {\n    try {\n      final observable = SubscribeObject.fromBuffer(bytes);\n      _observableController.add(observable);\n    } catch (e, s) {\n      Log.error(\n          'RustStreamReceiver SubscribeObject deserialize error: ${e.runtimeType}');\n      Log.error('Stack trace \\n $s');\n      rethrow;\n    }\n  }\n\n  Future<void> dispose() async {\n    await _ffiSubscription.cancel();\n    await _streamController.close();\n    await _observableController.close();\n    _ffiPort.close();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/linux/Classes/binding.h",
    "content": "#include <stdarg.h>\n#include <stdbool.h>\n#include <stdint.h>\n#include <stdlib.h>\n\n\nint64_t init_sdk(char *path);\n\nvoid async_command(int64_t port, const uint8_t *input, uintptr_t len);\n\nconst uint8_t *sync_command(const uint8_t *input, uintptr_t len);\n\nint32_t set_stream_port(int64_t port);\n\nvoid link_me_please(void);\n\nvoid rust_log(int64_t level, const char *data);\n\nvoid set_env(const char *data);\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/macos/Classes/AppFlowyBackendPlugin.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\npublic class AppFlowyBackendPlugin: NSObject, FlutterPlugin {\n  public static func register(with registrar: FlutterPluginRegistrar) {\n    let channel = FlutterMethodChannel(name: \"appflowy_backend\", binaryMessenger: registrar.messenger)\n    let instance = AppFlowyBackendPlugin()\n    registrar.addMethodCallDelegate(instance, channel: channel)\n  }\n\n  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {\n    switch call.method {\n    case \"getPlatformVersion\":\n      result(\"macOS \" + ProcessInfo.processInfo.operatingSystemVersionString)\n    default:\n      result(FlutterMethodNotImplemented)\n    }\n  }\n\n  public static func dummyMethodToEnforceBundling() {\n    link_me_please()\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/macos/Classes/binding.h",
    "content": "#include <stdarg.h>\n#include <stdbool.h>\n#include <stdint.h>\n#include <stdlib.h>\n\nint64_t init_sdk(int64_t port, char *data);\n\nvoid async_event(int64_t port, const uint8_t *input, uintptr_t len);\n\nconst uint8_t *sync_event(const uint8_t *input, uintptr_t len);\n\nint32_t set_stream_port(int64_t port);\n\nint32_t set_log_stream_port(int64_t port);\n\nvoid link_me_please(void);\n\nvoid rust_log(int64_t level, const char *data);\n\nvoid set_env(const char *data);\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/macos/appflowy_backend.podspec",
    "content": "#\n# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.\n# Run `pod lib lint appflowy_backend.podspec' to validate before publishing.\n#\nPod::Spec.new do |s|\n  s.name             = 'appflowy_backend'\n  s.version          = '0.0.1'\n  s.summary          = 'A new flutter plugin project.'\n  s.description      = <<-DESC\nA new flutter plugin project.\n                       DESC\n  s.homepage         = 'http://example.com'\n  s.license          = { :file => '../LICENSE' }\n  s.author           = { 'AppFlowy' => 'annie@appflowy.io' }\n  s.source           = { :path => '.' }\n  s.source_files     = 'Classes/**/*'\n  s.public_header_files = 'Classes/**/*.h'\n  s.dependency 'FlutterMacOS'\n\n  s.platform = :osx, '10.13'\n  s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }\n  s.swift_version = '5.0'\n  s.static_framework = true\n  s.vendored_libraries = \"libdart_ffi.a\"\nend\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml",
    "content": "name: appflowy_backend\ndescription: A new flutter plugin project.\nversion: 0.0.1\nhomepage: https://appflowy.io\npublish_to: \"none\"\n\nenvironment:\n  sdk: \">=2.17.0-0 <3.0.0\"\n  flutter: \">=1.17.0\"\n\ndependencies:\n  flutter:\n    sdk: flutter\n  ffi: ^2.0.2\n  isolates: ^3.0.3+8\n  protobuf: ^3.1.0\n  talker: ^4.7.1\n  plugin_platform_interface: ^2.1.3\n  appflowy_result:\n    path: ../appflowy_result\n  fixnum: ^1.1.0\n  async: ^2.11.0\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n\n# For information on the generic Dart part of this file, see the\n# following page: https://dart.dev/tools/pub/pubspec\n\n# The following section is specific to Flutter.\nflutter:\n  # This section identifies this Flutter project as a plugin project.\n  # The 'pluginClass' and Android 'package' identifiers should not ordinarily\n  # be modified. They are used by the tooling to maintain consistency when\n  # adding or updating assets for this project.\n  plugin:\n    platforms:\n      android:\n        package: com.plugin.appflowy_backend\n        pluginClass: AppFlowyBackendPlugin\n      ios:\n        pluginClass: AppFlowyBackendPlugin\n      macos:\n        pluginClass: AppFlowyBackendPlugin\n      windows:\n        pluginClass: AppFlowyBackendPlugin\n\n  # To add assets to your plugin package, add an assets section, like this:\n  # assets:\n  #   - images/a_dot_burr.jpeg\n  #   - images/a_dot_ham.jpeg\n  #\n  # For details regarding assets in packages, see\n  # https://flutter.dev/assets-and-images/#from-packages\n  #\n  # An image asset can refer to one or more resolution-specific \"variants\", see\n  # https://flutter.dev/assets-and-images/#resolution-aware.\n\n  # To add custom fonts to your plugin package, add a fonts section here,\n  # in this \"flutter\" section. Each entry in this list should have a\n  # \"family\" key with the font family name, and a \"fonts\" key with a\n  # list giving the asset and other descriptors for the font. For\n  # example:\n  # fonts:\n  #   - family: Schyler\n  #     fonts:\n  #       - asset: fonts/Schyler-Regular.ttf\n  #       - asset: fonts/Schyler-Italic.ttf\n  #         style: italic\n  #   - family: Trajan Pro\n  #     fonts:\n  #       - asset: fonts/TrajanPro.ttf\n  #       - asset: fonts/TrajanPro_Bold.ttf\n  #         weight: 700\n  #\n  # For details regarding fonts in packages, see\n  # https://flutter.dev/custom-fonts/#from-packages\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/test/appflowy_backend_method_channel_test.dart",
    "content": "import 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:appflowy_backend/appflowy_backend_method_channel.dart';\n\nvoid main() {\n  MethodChannelFlowySdk platform = MethodChannelFlowySdk();\n  const MethodChannel channel = MethodChannel('appflowy_backend');\n\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  setUp(() {\n    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n        .setMockMethodCallHandler(\n      channel,\n      (MethodCall methodCall) async {\n        return '42';\n      },\n    );\n  });\n\n  tearDown(() {\n    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n        .setMockMethodCallHandler(\n      channel,\n      null,\n    );\n  });\n\n  test('getPlatformVersion', () async {\n    expect(await platform.getPlatformVersion(), '42');\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/test/appflowy_backend_test.dart",
    "content": "import 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:appflowy_backend/appflowy_backend.dart';\n\nvoid main() {\n  const MethodChannel channel = MethodChannel('appflowy_backend');\n\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  setUp(() {\n    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n        .setMockMethodCallHandler(channel, (MethodCall methodCall) async {\n      return '42';\n    });\n  });\n\n  tearDown(() {\n    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n        .setMockMethodCallHandler(\n      channel,\n      null,\n    );\n  });\n\n  test('getPlatformVersion', () async {\n    expect(await FlowySDK.platformVersion, '42');\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/windows/.gitignore",
    "content": "flutter/\n\n# Visual Studio user-specific files.\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# Visual Studio build-related files.\nx64/\nx86/\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/windows/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.14)\nset(PROJECT_NAME \"appflowy_backend\")\nproject(${PROJECT_NAME} LANGUAGES CXX)\n\n# This value is used when generating builds using this plugin, so it must\n# not be changed\nset(PLUGIN_NAME \"appflowy_backend_plugin\")\n\nadd_library(${PLUGIN_NAME} SHARED\n  \"appflowy_backend_plugin.cpp\"\n)\napply_standard_settings(${PLUGIN_NAME})\nset_target_properties(${PLUGIN_NAME} PROPERTIES\n  CXX_VISIBILITY_PRESET hidden)\ntarget_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)\ntarget_include_directories(${PLUGIN_NAME} INTERFACE\n  \"${CMAKE_CURRENT_SOURCE_DIR}/include\")\ntarget_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin)\n\n# List of absolute paths to libraries that should be bundled with the plugin\nset(appflowy_backend_bundled_libraries\n \"\"\n PARENT_SCOPE\n)\n\n\n\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/windows/app_flowy_backend_plugin.h",
    "content": "#ifndef FLUTTER_PLUGIN_FLOWY_SDK_PLUGIN_H_\n#define FLUTTER_PLUGIN_FLOWY_SDK_PLUGIN_H_\n\n#include <flutter/method_channel.h>\n#include <flutter/plugin_registrar_windows.h>\n\n#include <memory>\n\nnamespace appflowy_backend {\n\nclass AppFlowyBackendPlugin : public flutter::Plugin {\n public:\n  static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);\n\n  AppFlowyBackendPlugin();\n\n  virtual ~AppFlowyBackendPlugin();\n\n  // Disallow copy and assign.\n  AppFlowyBackendPlugin(const AppFlowyBackendPlugin&) = delete;\n  AppFlowyBackendPlugin& operator=(const AppFlowyBackendPlugin&) = delete;\n\n private:\n  // Called when a method is called on this plugin's channel from Dart.\n  void HandleMethodCall(\n      const flutter::MethodCall<flutter::EncodableValue> &method_call,\n      std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);\n};\n\n}  // namespace appflowy_backend\n\n#endif  // FLUTTER_PLUGIN_FLOWY_SDK_PLUGIN_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/windows/appflowy_backend_plugin.cpp",
    "content": "\n// This must be included before many other Windows headers.\n#include <windows.h>\n\n// For getPlatformVersion; remove unless needed for your plugin implementation.\n#include <VersionHelpers.h>\n\n#include <flutter/method_channel.h>\n#include <flutter/plugin_registrar_windows.h>\n#include <flutter/standard_method_codec.h>\n\n#include <map>\n#include <memory>\n#include <sstream>\n#include \"include/appflowy_backend/app_flowy_backend_plugin.h\"\n\nnamespace\n{\n\n  class AppFlowyBackendPlugin : public flutter::Plugin\n  {\n  public:\n    static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);\n\n    AppFlowyBackendPlugin();\n\n    virtual ~AppFlowyBackendPlugin();\n\n  private:\n    // Called when a method is called on this plugin's channel from Dart.\n    void HandleMethodCall(\n        const flutter::MethodCall<flutter::EncodableValue> &method_call,\n        std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);\n  };\n\n  // static\n  void AppFlowyBackendPlugin::RegisterWithRegistrar(\n      flutter::PluginRegistrarWindows *registrar)\n  {\n    auto channel =\n        std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(\n            registrar->messenger(), \"appflowy_backend\",\n            &flutter::StandardMethodCodec::GetInstance());\n\n    auto plugin = std::make_unique<AppFlowyBackendPlugin>();\n\n    channel->SetMethodCallHandler(\n        [plugin_pointer = plugin.get()](const auto &call, auto result)\n        {\n          plugin_pointer->HandleMethodCall(call, std::move(result));\n        });\n\n    registrar->AddPlugin(std::move(plugin));\n  }\n\n  AppFlowyBackendPlugin::AppFlowyBackendPlugin() {}\n\n  AppFlowyBackendPlugin::~AppFlowyBackendPlugin() {}\n\n  void AppFlowyBackendPlugin::HandleMethodCall(\n      const flutter::MethodCall<flutter::EncodableValue> &method_call,\n      std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)\n  {\n    if (method_call.method_name().compare(\"getPlatformVersion\") == 0)\n    {\n      std::ostringstream version_stream;\n      version_stream << \"Windows \";\n      if (IsWindows10OrGreater())\n      {\n        version_stream << \"10+\";\n      }\n      else if (IsWindows8OrGreater())\n      {\n        version_stream << \"8\";\n      }\n      else if (IsWindows7OrGreater())\n      {\n        version_stream << \"7\";\n      }\n      result->Success(flutter::EncodableValue(version_stream.str()));\n    }\n    else\n    {\n      result->NotImplemented();\n    }\n  }\n\n} // namespace\n\nvoid AppFlowyBackendPluginRegisterWithRegistrar(\n    FlutterDesktopPluginRegistrarRef registrar)\n{\n  AppFlowyBackendPlugin::RegisterWithRegistrar(\n      flutter::PluginRegistrarManager::GetInstance()\n          ->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));\n}"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/windows/appflowy_backend_plugin_c_api.cpp",
    "content": "#include \"include/appflowy_backend/appflowy_backend_plugin_c_api.h\"\n\n#include <flutter/plugin_registrar_windows.h>\n\n#include \"appflowy_flutter_backend_plugin.h\"\n\nvoid AppFlowyBackendPluginCApiRegisterWithRegistrar(\n    FlutterDesktopPluginRegistrarRef registrar)\n{\n    appflowy_backend::AppFlowyBackendPlugin::RegisterWithRegistrar(\n        flutter::PluginRegistrarManager::GetInstance()\n            ->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/windows/include/appflowy_backend/app_flowy_backend_plugin.h",
    "content": "#ifndef FLUTTER_PLUGIN_FLOWY_SDK_PLUGIN_H_\n#define FLUTTER_PLUGIN_FLOWY_SDK_PLUGIN_H_\n\n#include <flutter_plugin_registrar.h>\n\n#ifdef FLUTTER_PLUGIN_IMPL\n#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport)\n#else\n#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport)\n#endif\n\n#if defined(__cplusplus)\nextern \"C\" {\n#endif\n\nFLUTTER_PLUGIN_EXPORT void AppFlowyBackendPluginRegisterWithRegistrar(\n    FlutterDesktopPluginRegistrarRef registrar);\n\n#if defined(__cplusplus)\n}  // extern \"C\"\n#endif\n\n#endif  // FLUTTER_PLUGIN_FLOWY_SDK_PLUGIN_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_backend/windows/include/appflowy_backend/appflowy_backend_plugin_c_api.h",
    "content": "#ifndef FLUTTER_PLUGIN_appflowy_backend_plugin_c_api_H_\n#define FLUTTER_PLUGIN_appflowy_backend_plugin_c_api_H_\n\n#include <flutter_plugin_registrar.h>\n\n#ifdef FLUTTER_PLUGIN_IMPL\n#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport)\n#else\n#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport)\n#endif\n\n#if defined(__cplusplus)\nextern \"C\" {\n#endif\n\nFLUTTER_PLUGIN_EXPORT void AppFlowyBackendPluginCApiRegisterWithRegistrar(\n    FlutterDesktopPluginRegistrarRef registrar);\n\n#if defined(__cplusplus)\n}  // extern \"C\"\n#endif\n\n#endif  // FLUTTER_PLUGIN_appflowy_backend_plugin_c_api_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\nmigrate_working_dir/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.\n/pubspec.lock\n**/doc/api/\n.dart_tool/\n.packages\nbuild/\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/.metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: f1875d570e39de09040c8f79aa13cc56baab8db1\n  channel: stable\n\nproject_type: package\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/CHANGELOG.md",
    "content": "## 0.0.1\n\n* TODO: Describe initial release.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/LICENSE",
    "content": "TODO: Add your license here.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/README.md",
    "content": "# AppFlowy Popover\n\nA Popover can be used to display some content on top of another.\n\nIt can be used to display a dropdown menu.\n\n> A popover is a transient view that appears above other content onscreen when you tap a control or in an area. Typically, a popover includes an arrow pointing to the location from which it emerged. Popovers can be nonmodal or modal. A nonmodal popover is dismissed by tapping another part of the screen or a button on the popover. A modal popover is dismissed by tapping a Cancel or other button on the popover.\n\nSource: [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/ios/views/popovers/).\n\n## Features\n\n- Basic popover style\n- Follow the target automatically\n- Nested popover support\n- Exclusive API\n\n![](./screenshot.png)\n\n## Example\n\n```dart\nPopover(\n  // Define how to trigger the popover\n  triggerActions: PopoverTriggerActionFlags.click,\n  child: TextButton(child: Text(\"Popover\"), onPressed: () {}),\n  // Define the direction of the popover\n  direction: PopoverDirection.bottomWithLeftAligned,\n  popupBuilder(BuildContext context) {\n    return PopoverMenu();\n  },\n);\n```\n\n### Trigger the popover manually\n\nSometimes, if you want to trigger the popover manually, you can use a `PopoverController`.\n\n```dart\nclass MyWidgetState extends State<GridDateCell> {\n  late PopoverController _popover;\n\n  @override\n  void initState() {\n    _popover = PopoverController();\n    super.initState();\n  }\n\n  // triggered by another widget\n  _onClick() {\n    _popover.show();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Popover(\n      controller: _popover,\n      ...\n    )\n  }\n}\n```\n\n### Make several popovers exclusive\n\nThe popover has a mechanism to make sure there are only one popover is shown in a group of popovers.\nIt's called `PopoverMutex`.\n\nIf you pass the same mutex object to the popovers, there will be only one popover is triggered.\n\n```dart\nclass MyWidgetState extends State<GridDateCell> {\n  final _popoverMutex = PopoverMutex();\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        Popover(\n          mutex: _popoverMutex,\n          ...\n        ),\n        Popover(\n          mutex: _popoverMutex,\n          ...\n        ),\n        Popover(\n          mutex: _popoverMutex,\n          ...\n        ),\n      ]\n    )\n  }\n}\n```\n\n## API\n\n| Param          | Description                                                      | Type                                    |\n| -------------- | ---------------------------------------------------------------- | --------------------------------------- |\n| offset         | The offset between the popover and the child                     | `Offset`                                |\n| popupBuilder   | The function used to build the popover                           | `Widget Function(BuildContext context)` |\n| triggerActions | Define the actions about how to trigger the popover              | `int`                                   |\n| mutex          | If multiple popovers are exclusive, pass the same mutex to them. | `PopoverMutex`                          |\n| direction      | The direction where the popover should be placed                 | `PopoverDirection`                      |\n| onClose        | The callback will be called after the popover is closed          | `void Function()`                       |\n| child          | The child to trigger the popover                                 | `Widget`                                |\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/analysis_options.yaml",
    "content": "# This file configures the analyzer, which statically analyzes Dart code to\n# check for errors, warnings, and lints.\n#\n# The issues identified by the analyzer are surfaced in the UI of Dart-enabled\n# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be\n# invoked from the command line by running `flutter analyze`.\n\n# The following line activates a set of recommended lints for Flutter apps,\n# packages, and plugins designed to encourage good coding practices.\n\ninclude: package:flutter_lints/flutter.yaml\n\nanalyzer:\n  exclude:\n    - \"**/*.g.dart\"\n    - \"**/*.freezed.dart\"\n\nlinter:\n  # The lint rules applied to this project can be customized in the\n  # section below to disable rules from the `package:flutter_lints/flutter.yaml`\n  # included above or to enable additional rules. A list of all available lints\n  # and their documentation is published at\n  # https://dart-lang.github.io/linter/lints/index.html.\n  #\n  # Instead of disabling a lint rule for the entire project in the\n  # section below, it can also be suppressed for a single line of code\n  # or a specific dart file by using the `// ignore: name_of_lint` and\n  # `// ignore_for_file: name_of_lint` syntax on the line or in the file\n  # producing the lint.\n  rules:\n    - require_trailing_commas\n\n    - prefer_collection_literals\n    - prefer_final_fields\n    - prefer_final_in_for_each\n    - prefer_final_locals\n\n    - sized_box_for_whitespace\n    - use_decorated_box\n\n    - unnecessary_parenthesis\n    - unnecessary_await_in_return\n    - unnecessary_raw_strings\n\n    - avoid_unnecessary_containers\n    - avoid_redundant_argument_values\n    - avoid_unused_constructor_parameters\n\n    - always_declare_return_types\n\n    - sort_constructors_first\n    - unawaited_futures\n\n    - prefer_single_quotes\n\n# Additional information about this file can be found at\n# https://dart.dev/guides/language/analysis-options\n\nerrors:\n  invalid_annotation_target: ignore\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\nmigrate_working_dir/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\n/build/\n\n# Web related\nlib/generated_plugin_registrant.dart\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Android Studio will place build artifacts here\n/android/app/debug\n/android/app/profile\n/android/app/release\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/.metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled.\n\nversion:\n  revision: f1875d570e39de09040c8f79aa13cc56baab8db1\n  channel: stable\n\nproject_type: app\n\n# Tracks metadata for the flutter migrate command\nmigration:\n  platforms:\n    - platform: root\n      create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1\n      base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1\n    - platform: android\n      create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1\n      base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1\n    - platform: ios\n      create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1\n      base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1\n    - platform: linux\n      create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1\n      base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1\n    - platform: macos\n      create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1\n      base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1\n    - platform: web\n      create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1\n      base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1\n    - platform: windows\n      create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1\n      base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1\n\n  # User provided section\n\n  # List of Local paths (relative to this file) that should be\n  # ignored by the migrate tool.\n  #\n  # Files that are not part of the templates will be ignored by default.\n  unmanaged_files:\n    - 'lib/main.dart'\n    - 'ios/Runner.xcodeproj/project.pbxproj'\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/README.md",
    "content": "# example\n\nA new Flutter project.\n\n## Getting Started\n\nThis project is a starting point for a Flutter application.\n\nA few resources to get you started if this is your first Flutter project:\n\n- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)\n- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)\n\nFor help getting started with Flutter development, view the\n[online documentation](https://docs.flutter.dev/), which offers tutorials,\nsamples, guidance on mobile development, and a full API reference.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/analysis_options.yaml",
    "content": "# This file configures the analyzer, which statically analyzes Dart code to\n# check for errors, warnings, and lints.\n#\n# The issues identified by the analyzer are surfaced in the UI of Dart-enabled\n# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be\n# invoked from the command line by running `flutter analyze`.\n\n# The following line activates a set of recommended lints for Flutter apps,\n# packages, and plugins designed to encourage good coding practices.\n\ninclude: package:flutter_lints/flutter.yaml\n\nanalyzer:\n  exclude:\n    - \"**/*.g.dart\"\n    - \"**/*.freezed.dart\"\n\nlinter:\n  # The lint rules applied to this project can be customized in the\n  # section below to disable rules from the `package:flutter_lints/flutter.yaml`\n  # included above or to enable additional rules. A list of all available lints\n  # and their documentation is published at\n  # https://dart-lang.github.io/linter/lints/index.html.\n  #\n  # Instead of disabling a lint rule for the entire project in the\n  # section below, it can also be suppressed for a single line of code\n  # or a specific dart file by using the `// ignore: name_of_lint` and\n  # `// ignore_for_file: name_of_lint` syntax on the line or in the file\n  # producing the lint.\n  rules:\n    - require_trailing_commas\n\n    - prefer_collection_literals\n    - prefer_final_fields\n    - prefer_final_in_for_each\n    - prefer_final_locals\n\n    - sized_box_for_whitespace\n    - use_decorated_box\n\n    - unnecessary_parenthesis\n    - unnecessary_await_in_return\n    - unnecessary_raw_strings\n\n    - avoid_unnecessary_containers\n    - avoid_redundant_argument_values\n    - avoid_unused_constructor_parameters\n\n    - always_declare_return_types\n\n    - sort_constructors_first\n    - unawaited_futures\n\n    - prefer_single_quotes\n\n# Additional information about this file can be found at\n# https://dart.dev/guides/language/analysis-options\n\nerrors:\n  invalid_annotation_target: ignore\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/android/.gitignore",
    "content": "gradle-wrapper.jar\n/.gradle\n/captures/\n/gradlew\n/gradlew.bat\n/local.properties\nGeneratedPluginRegistrant.java\n\n# Remember to never publicly share your keystore.\n# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app\nkey.properties\n**/*.keystore\n**/*.jks\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/android/app/build.gradle",
    "content": "def localProperties = new Properties()\ndef localPropertiesFile = rootProject.file('local.properties')\nif (localPropertiesFile.exists()) {\n    localPropertiesFile.withReader('UTF-8') { reader ->\n        localProperties.load(reader)\n    }\n}\n\ndef flutterRoot = localProperties.getProperty('flutter.sdk')\nif (flutterRoot == null) {\n    throw new GradleException(\"Flutter SDK not found. Define location with flutter.sdk in the local.properties file.\")\n}\n\ndef flutterVersionCode = localProperties.getProperty('flutter.versionCode')\nif (flutterVersionCode == null) {\n    flutterVersionCode = '1'\n}\n\ndef flutterVersionName = localProperties.getProperty('flutter.versionName')\nif (flutterVersionName == null) {\n    flutterVersionName = '1.0'\n}\n\napply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply from: \"$flutterRoot/packages/flutter_tools/gradle/flutter.gradle\"\n\nandroid {\n    compileSdkVersion flutter.compileSdkVersion\n    ndkVersion flutter.ndkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    kotlinOptions {\n        jvmTarget = '1.8'\n    }\n\n    sourceSets {\n        main.java.srcDirs += 'src/main/kotlin'\n    }\n\n    defaultConfig {\n        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).\n        applicationId \"com.example.example\"\n        // You can update the following values to match your application needs.\n        // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.\n        minSdkVersion flutter.minSdkVersion\n        targetSdkVersion flutter.targetSdkVersion\n        versionCode flutterVersionCode.toInteger()\n        versionName flutterVersionName\n    }\n\n    buildTypes {\n        release {\n            // TODO: Add your own signing config for the release build.\n            // Signing with the debug keys for now, so `flutter run --release` works.\n            signingConfig signingConfigs.debug\n        }\n    }\n}\n\nflutter {\n    source '../..'\n}\n\ndependencies {\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\"\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/android/app/src/debug/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.example.example\">\n    <!-- The INTERNET permission is required for development. Specifically,\n         the Flutter tool needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/android/app/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.example.example\">\n   <application\n        android:label=\"example\"\n        android:name=\"${applicationName}\"\n        android:icon=\"@mipmap/ic_launcher\">\n        <activity\n            android:name=\".MainActivity\"\n            android:exported=\"true\"\n            android:launchMode=\"singleTop\"\n            android:theme=\"@style/LaunchTheme\"\n            android:configChanges=\"orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode\"\n            android:hardwareAccelerated=\"true\"\n            android:windowSoftInputMode=\"adjustResize\">\n            <!-- Specifies an Android theme to apply to this Activity as soon as\n                 the Android process has started. This theme is visible to the user\n                 while the Flutter UI initializes. After that, this theme continues\n                 to determine the Window background behind the Flutter UI. -->\n            <meta-data\n              android:name=\"io.flutter.embedding.android.NormalTheme\"\n              android:resource=\"@style/NormalTheme\"\n              />\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\"/>\n                <category android:name=\"android.intent.category.LAUNCHER\"/>\n            </intent-filter>\n        </activity>\n        <!-- Don't delete the meta-data below.\n             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->\n        <meta-data\n            android:name=\"flutterEmbedding\"\n            android:value=\"2\" />\n    </application>\n</manifest>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt",
    "content": "package com.example.example\n\nimport io.flutter.embedding.android.FlutterActivity\n\nclass MainActivity: FlutterActivity() {\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/android/app/src/main/res/drawable/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@android:color/white\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/android/app/src/main/res/drawable-v21/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"?android:colorBackground\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/android/app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             the Flutter engine draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n\n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/android/app/src/main/res/values-night/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             the Flutter engine draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n\n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/android/app/src/profile/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.example.example\">\n    <!-- The INTERNET permission is required for development. Specifically,\n         the Flutter tool needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/android/build.gradle",
    "content": "buildscript {\n    ext.kotlin_version = '1.6.10'\n    repositories {\n        google()\n        mavenCentral()\n    }\n\n    dependencies {\n        classpath 'com.android.tools.build:gradle:7.1.2'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\nrootProject.buildDir = '../build'\nsubprojects {\n    project.buildDir = \"${rootProject.buildDir}/${project.name}\"\n}\nsubprojects {\n    project.evaluationDependsOn(':app')\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/android/gradle/wrapper/gradle-wrapper.properties",
    "content": "#Fri Jun 23 08:50:38 CEST 2017\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-7.4-all.zip\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx1536M\nandroid.useAndroidX=true\nandroid.enableJetifier=true\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/android/settings.gradle",
    "content": "include ':app'\n\ndef localPropertiesFile = new File(rootProject.projectDir, \"local.properties\")\ndef properties = new Properties()\n\nassert localPropertiesFile.exists()\nlocalPropertiesFile.withReader(\"UTF-8\") { reader -> properties.load(reader) }\n\ndef flutterSdkPath = properties.getProperty(\"flutter.sdk\")\nassert flutterSdkPath != null, \"flutter.sdk not set in local.properties\"\napply from: \"$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/.gitignore",
    "content": "**/dgph\n*.mode1v3\n*.mode2v3\n*.moved-aside\n*.pbxuser\n*.perspectivev3\n**/*sync/\n.sconsign.dblite\n.tags*\n**/.vagrant/\n**/DerivedData/\nIcon?\n**/Pods/\n**/.symlinks/\nprofile\nxcuserdata\n**/.generated/\nFlutter/App.framework\nFlutter/Flutter.framework\nFlutter/Flutter.podspec\nFlutter/Generated.xcconfig\nFlutter/ephemeral/\nFlutter/app.flx\nFlutter/app.zip\nFlutter/flutter_assets/\nFlutter/flutter_export_environment.sh\nServiceDefinitions.json\nRunner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!default.mode1v3\n!default.mode2v3\n!default.pbxuser\n!default.perspectivev3\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Flutter/AppFrameworkInfo.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>CFBundleDevelopmentRegion</key>\n  <string>en</string>\n  <key>CFBundleExecutable</key>\n  <string>App</string>\n  <key>CFBundleIdentifier</key>\n  <string>io.flutter.flutter.app</string>\n  <key>CFBundleInfoDictionaryVersion</key>\n  <string>6.0</string>\n  <key>CFBundleName</key>\n  <string>App</string>\n  <key>CFBundlePackageType</key>\n  <string>FMWK</string>\n  <key>CFBundleShortVersionString</key>\n  <string>1.0</string>\n  <key>CFBundleSignature</key>\n  <string>????</string>\n  <key>CFBundleVersion</key>\n  <string>1.0</string>\n  <key>MinimumOSVersion</key>\n  <string>12.0</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Flutter/Debug.xcconfig",
    "content": "#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Flutter/Release.xcconfig",
    "content": "#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner/AppDelegate.swift",
    "content": "import UIKit\nimport Flutter\n\n@UIApplicationMain\n@objc class AppDelegate: FlutterAppDelegate {\n  override func application(\n    _ application: UIApplication,\n    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n  ) -> Bool {\n    GeneratedPluginRegistrant.register(with: self)\n    return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-40x40@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-40x40@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"60x60\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-60x60@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"60x60\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-60x60@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-20x20@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-29x29@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-29x29@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-40x40@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-40x40@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"76x76\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-76x76@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"76x76\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-76x76@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"83.5x83.5\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-83.5x83.5@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"1024x1024\",\n      \"idiom\" : \"ios-marketing\",\n      \"filename\" : \"Icon-App-1024x1024@1x.png\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@3x.png\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md",
    "content": "# Launch Screen Assets\n\nYou can customize the launch screen with your own desired assets by replacing the image files in this directory.\n\nYou can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images."
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner/Base.lproj/LaunchScreen.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"12121\" systemVersion=\"16G29\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" launchScreen=\"YES\" colorMatched=\"YES\" initialViewController=\"01J-lp-oVM\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"12089\"/>\n    </dependencies>\n    <scenes>\n        <!--View Controller-->\n        <scene sceneID=\"EHf-IW-A2E\">\n            <objects>\n                <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"Ydg-fD-yQy\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"xbc-2k-c8Z\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"Ze5-6b-2t3\">\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <imageView opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" image=\"LaunchImage\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"YRO-k0-Ey4\">\n                            </imageView>\n                        </subviews>\n                        <color key=\"backgroundColor\" red=\"1\" green=\"1\" blue=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n                        <constraints>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerX\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerX\" id=\"1a2-6s-vTC\"/>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerY\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerY\" id=\"4X2-HB-R7a\"/>\n                        </constraints>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"iYj-Kq-Ea1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"53\" y=\"375\"/>\n        </scene>\n    </scenes>\n    <resources>\n        <image name=\"LaunchImage\" width=\"168\" height=\"185\"/>\n    </resources>\n</document>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"10117\" systemVersion=\"15F34\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" useTraitCollections=\"YES\" initialViewController=\"BYZ-38-t0r\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"10085\"/>\n    </dependencies>\n    <scenes>\n        <!--Flutter View Controller-->\n        <scene sceneID=\"tne-QT-ifu\">\n            <objects>\n                <viewController id=\"BYZ-38-t0r\" customClass=\"FlutterViewController\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"y3c-jy-aDJ\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"wfy-db-euE\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"8bC-Xf-vdC\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"600\" height=\"600\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" white=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"calibratedWhite\"/>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"dkx-z0-nzr\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleDisplayName</key>\n\t<string>Example</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>example</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>UILaunchStoryboardName</key>\n\t<string>LaunchScreen</string>\n\t<key>UIMainStoryboardFile</key>\n\t<string>Main</string>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UIViewControllerBasedStatusBarAppearance</key>\n\t<false/>\n\t<key>CADisableMinimumFrameDurationOnPhone</key>\n\t<true/>\n\t<key>UIApplicationSupportsIndirectInputEvents</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner/Runner-Bridging-Header.h",
    "content": "#import \"GeneratedPluginRegistrant.h\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };\n\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };\n\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };\n\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };\n\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };\n\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t9705A1C41CF9048500538489 /* Embed Frameworks */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Embed Frameworks\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = \"<group>\"; };\n\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = \"<group>\"; };\n\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = \"<group>\"; };\n\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = \"Runner-Bridging-Header.h\"; sourceTree = \"<group>\"; };\n\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = \"<group>\"; };\n\t\t97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\t97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t97C146EB1CF9000F007C117D /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t9740EEB11CF90186004384FC /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */,\n\t\t\t);\n\t\t\tname = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146E51CF9000F007C117D = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9740EEB11CF90186004384FC /* Flutter */,\n\t\t\t\t97C146F01CF9000F007C117D /* Runner */,\n\t\t\t\t97C146EF1CF9000F007C117D /* Products */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146EF1CF9000F007C117D /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146EE1CF9000F007C117D /* Runner.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146F01CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FA1CF9000F007C117D /* Main.storyboard */,\n\t\t\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */,\n\t\t\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,\n\t\t\t\t97C147021CF9000F007C117D /* Info.plist */,\n\t\t\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,\n\t\t\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,\n\t\t\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */,\n\t\t\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t97C146ED1CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t9740EEB61CF901F6004384FC /* Run Script */,\n\t\t\t\t97C146EA1CF9000F007C117D /* Sources */,\n\t\t\t\t97C146EB1CF9000F007C117D /* Frameworks */,\n\t\t\t\t97C146EC1CF9000F007C117D /* Resources */,\n\t\t\t\t9705A1C41CF9048500538489 /* Embed Frameworks */,\n\t\t\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 97C146EE1CF9000F007C117D /* Runner.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t97C146E61CF9000F007C117D /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t97C146ED1CF9000F007C117D = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 7.3.1;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 97C146E51CF9000F007C117D;\n\t\t\tproductRefGroup = 97C146EF1CF9000F007C117D /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t97C146ED1CF9000F007C117D /* Runner */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t97C146EC1CF9000F007C117D /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,\n\t\t\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,\n\t\t\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,\n\t\t\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\",\n\t\t\t);\n\t\t\tname = \"Thin Binary\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" embed_and_thin\";\n\t\t};\n\t\t9740EEB61CF901F6004384FC /* Run Script */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"Run Script\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" build\";\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t97C146EA1CF9000F007C117D /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,\n\t\t\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXVariantGroup section */\n\t\t97C146FA1CF9000F007C117D /* Main.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FB1CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C147001CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = LaunchScreen.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t249021D3217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 12.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t249021D4217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.example;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t97C147031CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 12.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147041CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 12.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t97C147061CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.example;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147071CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.example;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147031CF9000F007C117D /* Debug */,\n\t\t\t\t97C147041CF9000F007C117D /* Release */,\n\t\t\t\t249021D3217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147061CF9000F007C117D /* Debug */,\n\t\t\t\t97C147071CF9000F007C117D /* Release */,\n\t\t\t\t249021D4217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 97C146E61CF9000F007C117D /* Project object */;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1510\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n               BuildableName = \"Runner.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <Testables>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/lib/example_button.dart",
    "content": "import 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\n\nclass PopoverMenu extends StatefulWidget {\n  const PopoverMenu({super.key});\n\n  @override\n  State<StatefulWidget> createState() => _PopoverMenuState();\n}\n\nclass _PopoverMenuState extends State<PopoverMenu> {\n  final PopoverMutex popOverMutex = PopoverMutex();\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      type: MaterialType.transparency,\n      child: Container(\n        width: 200,\n        height: 200,\n        decoration: BoxDecoration(\n          color: Colors.white,\n          borderRadius: const BorderRadius.all(Radius.circular(8)),\n          boxShadow: [\n            BoxShadow(\n              color: Colors.grey.withValues(alpha: 0.5),\n              spreadRadius: 5,\n              blurRadius: 7,\n              offset: const Offset(0, 3), // changes position of shadow\n            ),\n          ],\n        ),\n        child: ListView(\n          children: [\n            Container(\n              margin: const EdgeInsets.all(8),\n              child: const Text(\n                'Popover',\n                style: TextStyle(\n                  fontSize: 14,\n                  color: Colors.black,\n                ),\n              ),\n            ),\n            Popover(\n              triggerActions:\n                  PopoverTriggerFlags.hover | PopoverTriggerFlags.click,\n              mutex: popOverMutex,\n              offset: const Offset(10, 0),\n              asBarrier: true,\n              debugId: 'First',\n              popupBuilder: (BuildContext context) {\n                return const PopoverMenu();\n              },\n              child: TextButton(\n                onPressed: () {},\n                child: const Text('First'),\n              ),\n            ),\n            Popover(\n              triggerActions:\n                  PopoverTriggerFlags.hover | PopoverTriggerFlags.click,\n              mutex: popOverMutex,\n              asBarrier: true,\n              debugId: 'Second',\n              offset: const Offset(10, 0),\n              popupBuilder: (BuildContext context) {\n                return const PopoverMenu();\n              },\n              child: TextButton(\n                onPressed: () {},\n                child: const Text('Second'),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass ExampleButton extends StatelessWidget {\n  const ExampleButton({\n    super.key,\n    required this.label,\n    required this.direction,\n    this.offset = Offset.zero,\n  });\n\n  final String label;\n  final Offset? offset;\n  final PopoverDirection direction;\n\n  @override\n  Widget build(BuildContext context) {\n    return Popover(\n      triggerActions: PopoverTriggerFlags.click,\n      animationDuration: Durations.medium1,\n      offset: offset,\n      direction: direction,\n      debugId: label,\n      child: TextButton(\n        child: Text(label),\n        onPressed: () {},\n      ),\n      popupBuilder: (BuildContext context) {\n        return const PopoverMenu();\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/lib/main.dart",
    "content": "import 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\n\nimport './example_button.dart';\n\nvoid main() {\n  runApp(const MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      title: 'Flutter Demo',\n      theme: ThemeData(\n        primarySwatch: Colors.blue,\n      ),\n      home: const MyHomePage(title: 'AppFlowy Popover Example'),\n    );\n  }\n}\n\nclass MyHomePage extends StatefulWidget {\n  const MyHomePage({super.key, required this.title});\n\n  final String title;\n\n  @override\n  State<MyHomePage> createState() => _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(widget.title),\n      ),\n      body: const Padding(\n        padding: EdgeInsets.symmetric(horizontal: 48.0, vertical: 24.0),\n        child: Row(\n          mainAxisAlignment: MainAxisAlignment.spaceBetween,\n          children: [\n            Column(\n              mainAxisAlignment: MainAxisAlignment.spaceBetween,\n              children: [\n                ExampleButton(\n                  label: 'Left top',\n                  offset: Offset(0, 10),\n                  direction: PopoverDirection.bottomWithLeftAligned,\n                ),\n                ExampleButton(\n                  label: 'Left Center',\n                  offset: Offset(0, -10),\n                  direction: PopoverDirection.rightWithCenterAligned,\n                ),\n                ExampleButton(\n                  label: 'Left bottom',\n                  offset: Offset(0, -10),\n                  direction: PopoverDirection.topWithLeftAligned,\n                ),\n              ],\n            ),\n            Column(\n              mainAxisAlignment: MainAxisAlignment.spaceBetween,\n              children: [\n                ExampleButton(\n                  label: 'Top',\n                  offset: Offset(0, 10),\n                  direction: PopoverDirection.bottomWithCenterAligned,\n                ),\n                Column(\n                  mainAxisAlignment: MainAxisAlignment.center,\n                  children: [\n                    ExampleButton(\n                      label: 'Central',\n                      offset: Offset(0, 10),\n                      direction: PopoverDirection.bottomWithCenterAligned,\n                    ),\n                  ],\n                ),\n                ExampleButton(\n                  label: 'Bottom',\n                  offset: Offset(0, -10),\n                  direction: PopoverDirection.topWithCenterAligned,\n                ),\n              ],\n            ),\n            Column(\n              mainAxisAlignment: MainAxisAlignment.spaceBetween,\n              children: [\n                ExampleButton(\n                  label: 'Right top',\n                  offset: Offset(0, 10),\n                  direction: PopoverDirection.bottomWithRightAligned,\n                ),\n                ExampleButton(\n                  label: 'Right Center',\n                  offset: Offset(0, 10),\n                  direction: PopoverDirection.leftWithCenterAligned,\n                ),\n                ExampleButton(\n                  label: 'Right bottom',\n                  offset: Offset(0, -10),\n                  direction: PopoverDirection.topWithRightAligned,\n                ),\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/linux/.gitignore",
    "content": "flutter/ephemeral\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/linux/CMakeLists.txt",
    "content": "# Project-level configuration.\ncmake_minimum_required(VERSION 3.10)\nproject(runner LANGUAGES CXX)\n\n# The name of the executable created for the application. Change this to change\n# the on-disk name of your application.\nset(BINARY_NAME \"example\")\n# The unique GTK application identifier for this application. See:\n# https://wiki.gnome.org/HowDoI/ChooseApplicationID\nset(APPLICATION_ID \"com.example.example\")\n\n# Explicitly opt in to modern CMake behaviors to avoid warnings with recent\n# versions of CMake.\ncmake_policy(SET CMP0063 NEW)\n\n# Load bundled libraries from the lib/ directory relative to the binary.\nset(CMAKE_INSTALL_RPATH \"$ORIGIN/lib\")\n\n# Root filesystem for cross-building.\nif(FLUTTER_TARGET_PLATFORM_SYSROOT)\n  set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\n  set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\nendif()\n\n# Define build configuration options.\nif(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n  set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n    STRING \"Flutter build mode\" FORCE)\n  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n    \"Debug\" \"Profile\" \"Release\")\nendif()\n\n# Compilation settings that should be applied to most targets.\n#\n# Be cautious about adding new options here, as plugins use this function by\n# default. In most cases, you should add new options to specific targets instead\n# of modifying this function.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_14)\n  target_compile_options(${TARGET} PRIVATE -Wall -Werror)\n  target_compile_options(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:-O3>\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:NDEBUG>\")\nendfunction()\n\n# Flutter library and tool build rules.\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# System-level dependencies.\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)\n\nadd_definitions(-DAPPLICATION_ID=\"${APPLICATION_ID}\")\n\n# Define the application target. To change its name, change BINARY_NAME above,\n# not the value here, or `flutter run` will no longer work.\n#\n# Any new source files that you add to the application should be added here.\nadd_executable(${BINARY_NAME}\n  \"main.cc\"\n  \"my_application.cc\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n)\n\n# Apply the standard set of build settings. This can be removed for applications\n# that need different build settings.\napply_standard_settings(${BINARY_NAME})\n\n# Add dependency libraries. Add any application-specific dependencies here.\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter)\ntarget_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)\n\n# Run the Flutter tool portions of the build. This must not be removed.\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n\n# Only the install-generated bundle's copy of the executable will launch\n# correctly, since the resources must in the right relative locations. To avoid\n# people trying to run the unbundled copy, put it in a subdirectory instead of\n# the default top-level location.\nset_target_properties(${BINARY_NAME}\n  PROPERTIES\n  RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/intermediates_do_not_run\"\n)\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n\n# === Installation ===\n# By default, \"installing\" just makes a relocatable bundle in the build\n# directory.\nset(BUILD_BUNDLE_DIR \"${PROJECT_BINARY_DIR}/bundle\")\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\n# Start with a clean build bundle directory every time.\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${BUILD_BUNDLE_DIR}/\\\")\n  \" COMPONENT Runtime)\n\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}/lib\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nforeach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})\n  install(FILES \"${bundled_library}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendforeach(bundled_library)\n\n# Fully re-copy the assets directory on each build to avoid having stale files\n# from a previous install.\nset(FLUTTER_ASSET_DIR_NAME \"flutter_assets\")\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\\\")\n  \" COMPONENT Runtime)\ninstall(DIRECTORY \"${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}\"\n  DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\" COMPONENT Runtime)\n\n# Install the AOT library on non-Debug builds only.\nif(NOT CMAKE_BUILD_TYPE MATCHES \"Debug\")\n  install(FILES \"${AOT_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/linux/flutter/CMakeLists.txt",
    "content": "# This file controls Flutter-level build steps. It should not be edited.\ncmake_minimum_required(VERSION 3.10)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\n\n# Serves the same purpose as list(TRANSFORM ... PREPEND ...),\n# which isn't available in 3.10.\nfunction(list_prepend LIST_NAME PREFIX)\n    set(NEW_LIST \"\")\n    foreach(element ${${LIST_NAME}})\n        list(APPEND NEW_LIST \"${PREFIX}${element}\")\n    endforeach(element)\n    set(${LIST_NAME} \"${NEW_LIST}\" PARENT_SCOPE)\nendfunction()\n\n# === Flutter Library ===\n# System-level dependencies.\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)\npkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)\npkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)\n\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/libflutter_linux_gtk.so\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/lib/libapp.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"fl_basic_message_channel.h\"\n  \"fl_binary_codec.h\"\n  \"fl_binary_messenger.h\"\n  \"fl_dart_project.h\"\n  \"fl_engine.h\"\n  \"fl_json_message_codec.h\"\n  \"fl_json_method_codec.h\"\n  \"fl_message_codec.h\"\n  \"fl_method_call.h\"\n  \"fl_method_channel.h\"\n  \"fl_method_codec.h\"\n  \"fl_method_response.h\"\n  \"fl_plugin_registrar.h\"\n  \"fl_plugin_registry.h\"\n  \"fl_standard_message_codec.h\"\n  \"fl_standard_method_codec.h\"\n  \"fl_string_codec.h\"\n  \"fl_value.h\"\n  \"fl_view.h\"\n  \"flutter_linux.h\"\n)\nlist_prepend(FLUTTER_LIBRARY_HEADERS \"${EPHEMERAL_DIR}/flutter_linux/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}\")\ntarget_link_libraries(flutter INTERFACE\n  PkgConfig::GTK\n  PkgConfig::GLIB\n  PkgConfig::GIO\n)\nadd_dependencies(flutter flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CMAKE_CURRENT_BINARY_DIR}/_phony_\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh\"\n      ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n)\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/linux/main.cc",
    "content": "#include \"my_application.h\"\n\nint main(int argc, char** argv) {\n  g_autoptr(MyApplication) app = my_application_new();\n  return g_application_run(G_APPLICATION(app), argc, argv);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/linux/my_application.cc",
    "content": "#include \"my_application.h\"\n\n#include <flutter_linux/flutter_linux.h>\n#ifdef GDK_WINDOWING_X11\n#include <gdk/gdkx.h>\n#endif\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nstruct _MyApplication {\n  GtkApplication parent_instance;\n  char** dart_entrypoint_arguments;\n};\n\nG_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)\n\n// Implements GApplication::activate.\nstatic void my_application_activate(GApplication* application) {\n  MyApplication* self = MY_APPLICATION(application);\n  GtkWindow* window =\n      GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));\n\n  // Use a header bar when running in GNOME as this is the common style used\n  // by applications and is the setup most users will be using (e.g. Ubuntu\n  // desktop).\n  // If running on X and not using GNOME then just use a traditional title bar\n  // in case the window manager does more exotic layout, e.g. tiling.\n  // If running on Wayland assume the header bar will work (may need changing\n  // if future cases occur).\n  gboolean use_header_bar = TRUE;\n#ifdef GDK_WINDOWING_X11\n  GdkScreen* screen = gtk_window_get_screen(window);\n  if (GDK_IS_X11_SCREEN(screen)) {\n    const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);\n    if (g_strcmp0(wm_name, \"GNOME Shell\") != 0) {\n      use_header_bar = FALSE;\n    }\n  }\n#endif\n  if (use_header_bar) {\n    GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());\n    gtk_widget_show(GTK_WIDGET(header_bar));\n    gtk_header_bar_set_title(header_bar, \"example\");\n    gtk_header_bar_set_show_close_button(header_bar, TRUE);\n    gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));\n  } else {\n    gtk_window_set_title(window, \"example\");\n  }\n\n  gtk_window_set_default_size(window, 1280, 720);\n  gtk_widget_show(GTK_WIDGET(window));\n\n  g_autoptr(FlDartProject) project = fl_dart_project_new();\n  fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);\n\n  FlView* view = fl_view_new(project);\n  gtk_widget_show(GTK_WIDGET(view));\n  gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));\n\n  fl_register_plugins(FL_PLUGIN_REGISTRY(view));\n\n  gtk_widget_grab_focus(GTK_WIDGET(view));\n}\n\n// Implements GApplication::local_command_line.\nstatic gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {\n  MyApplication* self = MY_APPLICATION(application);\n  // Strip out the first argument as it is the binary name.\n  self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);\n\n  g_autoptr(GError) error = nullptr;\n  if (!g_application_register(application, nullptr, &error)) {\n     g_warning(\"Failed to register: %s\", error->message);\n     *exit_status = 1;\n     return TRUE;\n  }\n\n  g_application_activate(application);\n  *exit_status = 0;\n\n  return TRUE;\n}\n\n// Implements GObject::dispose.\nstatic void my_application_dispose(GObject* object) {\n  MyApplication* self = MY_APPLICATION(object);\n  g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);\n  G_OBJECT_CLASS(my_application_parent_class)->dispose(object);\n}\n\nstatic void my_application_class_init(MyApplicationClass* klass) {\n  G_APPLICATION_CLASS(klass)->activate = my_application_activate;\n  G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;\n  G_OBJECT_CLASS(klass)->dispose = my_application_dispose;\n}\n\nstatic void my_application_init(MyApplication* self) {}\n\nMyApplication* my_application_new() {\n  return MY_APPLICATION(g_object_new(my_application_get_type(),\n                                     \"application-id\", APPLICATION_ID,\n                                     \"flags\", G_APPLICATION_NON_UNIQUE,\n                                     nullptr));\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/linux/my_application.h",
    "content": "#ifndef FLUTTER_MY_APPLICATION_H_\n#define FLUTTER_MY_APPLICATION_H_\n\n#include <gtk/gtk.h>\n\nG_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,\n                     GtkApplication)\n\n/**\n * my_application_new:\n *\n * Creates a new Flutter-based application.\n *\n * Returns: a new #MyApplication.\n */\nMyApplication* my_application_new();\n\n#endif  // FLUTTER_MY_APPLICATION_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/.gitignore",
    "content": "# Flutter-related\n**/Flutter/ephemeral/\n**/Pods/\n\n# Xcode-related\n**/dgph\n**/xcuserdata/\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Flutter/Flutter-Debug.xcconfig",
    "content": "#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Flutter/Flutter-Release.xcconfig",
    "content": "#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner/AppDelegate.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\n@NSApplicationMain\nclass AppDelegate: FlutterAppDelegate {\n  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {\n    return true\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_16.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_32.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"32x32\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_32.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"32x32\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_64.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"128x128\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_128.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"128x128\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_256.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"256x256\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_256.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"256x256\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_512.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"512x512\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_512.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"512x512\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_1024.png\",\n      \"scale\" : \"2x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner/Base.lproj/MainMenu.xib",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion=\"14490.70\" targetRuntime=\"MacOSX.Cocoa\" propertyAccessControl=\"none\" useAutolayout=\"YES\" customObjectInstantitationMethod=\"direct\">\n    <dependencies>\n        <deployment identifier=\"macosx\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.CocoaPlugin\" version=\"14490.70\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <objects>\n        <customObject id=\"-2\" userLabel=\"File's Owner\" customClass=\"NSApplication\">\n            <connections>\n                <outlet property=\"delegate\" destination=\"Voe-Tx-rLC\" id=\"GzC-gU-4Uq\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"-1\" userLabel=\"First Responder\" customClass=\"FirstResponder\"/>\n        <customObject id=\"-3\" userLabel=\"Application\" customClass=\"NSObject\"/>\n        <customObject id=\"Voe-Tx-rLC\" customClass=\"AppDelegate\" customModule=\"Runner\" customModuleProvider=\"target\">\n            <connections>\n                <outlet property=\"applicationMenu\" destination=\"uQy-DD-JDr\" id=\"XBo-yE-nKs\"/>\n                <outlet property=\"mainFlutterWindow\" destination=\"QvC-M9-y7g\" id=\"gIp-Ho-8D9\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"YLy-65-1bz\" customClass=\"NSFontManager\"/>\n        <menu title=\"Main Menu\" systemMenu=\"main\" id=\"AYu-sK-qS6\">\n            <items>\n                <menuItem title=\"APP_NAME\" id=\"1Xt-HY-uBw\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"APP_NAME\" systemMenu=\"apple\" id=\"uQy-DD-JDr\">\n                        <items>\n                            <menuItem title=\"About APP_NAME\" id=\"5kV-Vb-QxS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"orderFrontStandardAboutPanel:\" target=\"-1\" id=\"Exp-CZ-Vem\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"VOq-y0-SEH\"/>\n                            <menuItem title=\"Preferences…\" keyEquivalent=\",\" id=\"BOF-NM-1cW\"/>\n                            <menuItem isSeparatorItem=\"YES\" id=\"wFC-TO-SCJ\"/>\n                            <menuItem title=\"Services\" id=\"NMo-om-nkz\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Services\" systemMenu=\"services\" id=\"hz9-B4-Xy5\"/>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"4je-JR-u6R\"/>\n                            <menuItem title=\"Hide APP_NAME\" keyEquivalent=\"h\" id=\"Olw-nP-bQN\">\n                                <connections>\n                                    <action selector=\"hide:\" target=\"-1\" id=\"PnN-Uc-m68\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Hide Others\" keyEquivalent=\"h\" id=\"Vdr-fp-XzO\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"hideOtherApplications:\" target=\"-1\" id=\"VT4-aY-XCT\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Show All\" id=\"Kd2-mp-pUS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"unhideAllApplications:\" target=\"-1\" id=\"Dhg-Le-xox\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"kCx-OE-vgT\"/>\n                            <menuItem title=\"Quit APP_NAME\" keyEquivalent=\"q\" id=\"4sb-4s-VLi\">\n                                <connections>\n                                    <action selector=\"terminate:\" target=\"-1\" id=\"Te7-pn-YzF\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Edit\" id=\"5QF-Oa-p0T\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Edit\" id=\"W48-6f-4Dl\">\n                        <items>\n                            <menuItem title=\"Undo\" keyEquivalent=\"z\" id=\"dRJ-4n-Yzg\">\n                                <connections>\n                                    <action selector=\"undo:\" target=\"-1\" id=\"M6e-cu-g7V\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Redo\" keyEquivalent=\"Z\" id=\"6dh-zS-Vam\">\n                                <connections>\n                                    <action selector=\"redo:\" target=\"-1\" id=\"oIA-Rs-6OD\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"WRV-NI-Exz\"/>\n                            <menuItem title=\"Cut\" keyEquivalent=\"x\" id=\"uRl-iY-unG\">\n                                <connections>\n                                    <action selector=\"cut:\" target=\"-1\" id=\"YJe-68-I9s\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Copy\" keyEquivalent=\"c\" id=\"x3v-GG-iWU\">\n                                <connections>\n                                    <action selector=\"copy:\" target=\"-1\" id=\"G1f-GL-Joy\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Paste\" keyEquivalent=\"v\" id=\"gVA-U4-sdL\">\n                                <connections>\n                                    <action selector=\"paste:\" target=\"-1\" id=\"UvS-8e-Qdg\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Paste and Match Style\" keyEquivalent=\"V\" id=\"WeT-3V-zwk\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"pasteAsPlainText:\" target=\"-1\" id=\"cEh-KX-wJQ\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Delete\" id=\"pa3-QI-u2k\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"delete:\" target=\"-1\" id=\"0Mk-Ml-PaM\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Select All\" keyEquivalent=\"a\" id=\"Ruw-6m-B2m\">\n                                <connections>\n                                    <action selector=\"selectAll:\" target=\"-1\" id=\"VNm-Mi-diN\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"uyl-h8-XO2\"/>\n                            <menuItem title=\"Find\" id=\"4EN-yA-p0u\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Find\" id=\"1b7-l0-nxx\">\n                                    <items>\n                                        <menuItem title=\"Find…\" tag=\"1\" keyEquivalent=\"f\" id=\"Xz5-n4-O0W\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"cD7-Qs-BN4\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find and Replace…\" tag=\"12\" keyEquivalent=\"f\" id=\"YEy-JH-Tfz\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"WD3-Gg-5AJ\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find Next\" tag=\"2\" keyEquivalent=\"g\" id=\"q09-fT-Sye\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"NDo-RZ-v9R\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find Previous\" tag=\"3\" keyEquivalent=\"G\" id=\"OwM-mh-QMV\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"HOh-sY-3ay\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Use Selection for Find\" tag=\"7\" keyEquivalent=\"e\" id=\"buJ-ug-pKt\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"U76-nv-p5D\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Jump to Selection\" keyEquivalent=\"j\" id=\"S0p-oC-mLd\">\n                                            <connections>\n                                                <action selector=\"centerSelectionInVisibleArea:\" target=\"-1\" id=\"IOG-6D-g5B\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Spelling and Grammar\" id=\"Dv1-io-Yv7\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Spelling\" id=\"3IN-sU-3Bg\">\n                                    <items>\n                                        <menuItem title=\"Show Spelling and Grammar\" keyEquivalent=\":\" id=\"HFo-cy-zxI\">\n                                            <connections>\n                                                <action selector=\"showGuessPanel:\" target=\"-1\" id=\"vFj-Ks-hy3\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Check Document Now\" keyEquivalent=\";\" id=\"hz2-CU-CR7\">\n                                            <connections>\n                                                <action selector=\"checkSpelling:\" target=\"-1\" id=\"fz7-VC-reM\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"bNw-od-mp5\"/>\n                                        <menuItem title=\"Check Spelling While Typing\" id=\"rbD-Rh-wIN\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleContinuousSpellChecking:\" target=\"-1\" id=\"7w6-Qz-0kB\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Check Grammar With Spelling\" id=\"mK6-2p-4JG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleGrammarChecking:\" target=\"-1\" id=\"muD-Qn-j4w\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Correct Spelling Automatically\" id=\"78Y-hA-62v\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticSpellingCorrection:\" target=\"-1\" id=\"2lM-Qi-WAP\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Substitutions\" id=\"9ic-FL-obx\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Substitutions\" id=\"FeM-D8-WVr\">\n                                    <items>\n                                        <menuItem title=\"Show Substitutions\" id=\"z6F-FW-3nz\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"orderFrontSubstitutionsPanel:\" target=\"-1\" id=\"oku-mr-iSq\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"gPx-C9-uUO\"/>\n                                        <menuItem title=\"Smart Copy/Paste\" id=\"9yt-4B-nSM\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleSmartInsertDelete:\" target=\"-1\" id=\"3IJ-Se-DZD\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Quotes\" id=\"hQb-2v-fYv\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticQuoteSubstitution:\" target=\"-1\" id=\"ptq-xd-QOA\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Dashes\" id=\"rgM-f4-ycn\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticDashSubstitution:\" target=\"-1\" id=\"oCt-pO-9gS\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Links\" id=\"cwL-P1-jid\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticLinkDetection:\" target=\"-1\" id=\"Gip-E3-Fov\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Data Detectors\" id=\"tRr-pd-1PS\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticDataDetection:\" target=\"-1\" id=\"R1I-Nq-Kbl\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Text Replacement\" id=\"HFQ-gK-NFA\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticTextReplacement:\" target=\"-1\" id=\"DvP-Fe-Py6\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Transformations\" id=\"2oI-Rn-ZJC\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Transformations\" id=\"c8a-y6-VQd\">\n                                    <items>\n                                        <menuItem title=\"Make Upper Case\" id=\"vmV-6d-7jI\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"uppercaseWord:\" target=\"-1\" id=\"sPh-Tk-edu\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Make Lower Case\" id=\"d9M-CD-aMd\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"lowercaseWord:\" target=\"-1\" id=\"iUZ-b5-hil\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Capitalize\" id=\"UEZ-Bs-lqG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"capitalizeWord:\" target=\"-1\" id=\"26H-TL-nsh\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Speech\" id=\"xrE-MZ-jX0\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Speech\" id=\"3rS-ZA-NoH\">\n                                    <items>\n                                        <menuItem title=\"Start Speaking\" id=\"Ynk-f8-cLZ\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"startSpeaking:\" target=\"-1\" id=\"654-Ng-kyl\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Stop Speaking\" id=\"Oyz-dy-DGm\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"stopSpeaking:\" target=\"-1\" id=\"dX8-6p-jy9\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"View\" id=\"H8h-7b-M4v\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"View\" id=\"HyV-fh-RgO\">\n                        <items>\n                            <menuItem title=\"Enter Full Screen\" keyEquivalent=\"f\" id=\"4J7-dP-txa\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" control=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"toggleFullScreen:\" target=\"-1\" id=\"dU3-MA-1Rq\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Window\" id=\"aUF-d1-5bR\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Window\" systemMenu=\"window\" id=\"Td7-aD-5lo\">\n                        <items>\n                            <menuItem title=\"Minimize\" keyEquivalent=\"m\" id=\"OY7-WF-poV\">\n                                <connections>\n                                    <action selector=\"performMiniaturize:\" target=\"-1\" id=\"VwT-WD-YPe\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Zoom\" id=\"R4o-n2-Eq4\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"performZoom:\" target=\"-1\" id=\"DIl-cC-cCs\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"eu3-7i-yIM\"/>\n                            <menuItem title=\"Bring All to Front\" id=\"LE2-aR-0XJ\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"arrangeInFront:\" target=\"-1\" id=\"DRN-fu-gQh\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Help\" id=\"EPT-qC-fAb\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Help\" systemMenu=\"help\" id=\"rJ0-wn-3NY\"/>\n                </menuItem>\n            </items>\n            <point key=\"canvasLocation\" x=\"142\" y=\"-258\"/>\n        </menu>\n        <window title=\"APP_NAME\" allowsToolTipsWhenApplicationIsInactive=\"NO\" autorecalculatesKeyViewLoop=\"NO\" releasedWhenClosed=\"NO\" animationBehavior=\"default\" id=\"QvC-M9-y7g\" customClass=\"MainFlutterWindow\" customModule=\"Runner\" customModuleProvider=\"target\">\n            <windowStyleMask key=\"styleMask\" titled=\"YES\" closable=\"YES\" miniaturizable=\"YES\" resizable=\"YES\"/>\n            <rect key=\"contentRect\" x=\"335\" y=\"390\" width=\"800\" height=\"600\"/>\n            <rect key=\"screenRect\" x=\"0.0\" y=\"0.0\" width=\"2560\" height=\"1577\"/>\n            <view key=\"contentView\" wantsLayer=\"YES\" id=\"EiT-Mj-1SZ\">\n                <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"800\" height=\"600\"/>\n                <autoresizingMask key=\"autoresizingMask\"/>\n            </view>\n        </window>\n    </objects>\n</document>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner/Configs/AppInfo.xcconfig",
    "content": "// Application-level settings for the Runner target.\n//\n// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the\n// future. If not, the values below would default to using the project name when this becomes a\n// 'flutter create' template.\n\n// The application's name. By default this is also the title of the Flutter window.\nPRODUCT_NAME = example\n\n// The application's bundle identifier\nPRODUCT_BUNDLE_IDENTIFIER = com.example.example\n\n// The copyright displayed in application information\nPRODUCT_COPYRIGHT = Copyright © 2022 com.example. All rights reserved.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner/Configs/Debug.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Debug.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner/Configs/Release.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Release.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner/Configs/Warnings.xcconfig",
    "content": "WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings\nGCC_WARN_UNDECLARED_SELECTOR = YES\nCLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES\nCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE\nCLANG_WARN__DUPLICATE_METHOD_MATCH = YES\nCLANG_WARN_PRAGMA_PACK = YES\nCLANG_WARN_STRICT_PROTOTYPES = YES\nCLANG_WARN_COMMA = YES\nGCC_WARN_STRICT_SELECTOR_MATCH = YES\nCLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES\nCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES\nGCC_WARN_SHADOW = YES\nCLANG_WARN_UNREACHABLE_CODE = YES\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner/DebugProfile.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n\t<key>com.apple.security.cs.allow-jit</key>\n\t<true/>\n\t<key>com.apple.security.network.server</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIconFile</key>\n\t<string></string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSMinimumSystemVersion</key>\n\t<string>$(MACOSX_DEPLOYMENT_TARGET)</string>\n\t<key>NSHumanReadableCopyright</key>\n\t<string>$(PRODUCT_COPYRIGHT)</string>\n\t<key>NSMainNibFile</key>\n\t<string>MainMenu</string>\n\t<key>NSPrincipalClass</key>\n\t<string>NSApplication</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner/MainFlutterWindow.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\nclass MainFlutterWindow: NSWindow {\n  override func awakeFromNib() {\n    let flutterViewController = FlutterViewController.init()\n    let windowFrame = self.frame\n    self.contentViewController = flutterViewController\n    self.setFrame(windowFrame, display: true)\n\n    RegisterGeneratedPlugins(registry: flutterViewController)\n\n    super.awakeFromNib()\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner/Release.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXAggregateTarget section */\n\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {\n\t\t\tisa = PBXAggregateTarget;\n\t\t\tbuildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t33CC111E2044C6BF0003C045 /* ShellScript */,\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = \"Flutter Assemble\";\n\t\t\tproductName = FLX;\n\t\t};\n/* End PBXAggregateTarget section */\n\n/* Begin PBXBuildFile section */\n\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };\n\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };\n\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };\n\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };\n\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 33CC10E52044A3C60003C045 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 33CC111A2044C6BA0003C045;\n\t\t\tremoteInfo = FLX;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t33CC110E2044A8840003C045 /* Bundle Framework */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Bundle Framework\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = \"<group>\"; };\n\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = \"<group>\"; };\n\t\t33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = \"example.app\"; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = \"<group>\"; };\n\t\t33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = \"<group>\"; };\n\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = \"<group>\"; };\n\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = \"Flutter-Generated.xcconfig\"; path = \"ephemeral/Flutter-Generated.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = \"<group>\"; };\n\t\t33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = \"<group>\"; };\n\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t33CC10EA2044A3C60003C045 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t33BA886A226E78AF003329D5 /* Configs */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */,\n\t\t\t);\n\t\t\tpath = Configs;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10E42044A3C60003C045 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33FAB671232836740065AC1E /* Runner */,\n\t\t\t\t33CEB47122A05771004F2AC0 /* Flutter */,\n\t\t\t\t33CC10EE2044A3C60003C045 /* Products */,\n\t\t\t\tD73912EC22F37F3D000D13A0 /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10EE2044A3C60003C045 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10ED2044A3C60003C045 /* example.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC11242044D66E0003C045 /* Resources */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */,\n\t\t\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */,\n\t\t\t\t33CC10F72044A3C60003C045 /* Info.plist */,\n\t\t\t);\n\t\t\tname = Resources;\n\t\t\tpath = ..;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CEB47122A05771004F2AC0 /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,\n\t\t\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,\n\t\t\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,\n\t\t\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,\n\t\t\t);\n\t\t\tpath = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33FAB671232836740065AC1E /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */,\n\t\t\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,\n\t\t\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */,\n\t\t\t\t33E51914231749380026EE4D /* Release.entitlements */,\n\t\t\t\t33CC11242044D66E0003C045 /* Resources */,\n\t\t\t\t33BA886A226E78AF003329D5 /* Configs */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD73912EC22F37F3D000D13A0 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t33CC10EC2044A3C60003C045 /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t33CC10E92044A3C60003C045 /* Sources */,\n\t\t\t\t33CC10EA2044A3C60003C045 /* Frameworks */,\n\t\t\t\t33CC10EB2044A3C60003C045 /* Resources */,\n\t\t\t\t33CC110E2044A8840003C045 /* Bundle Framework */,\n\t\t\t\t3399D490228B24CF009A79C7 /* ShellScript */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 33CC10ED2044A3C60003C045 /* example.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t33CC10E52044A3C60003C045 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastSwiftUpdateCheck = 0920;\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t33CC10EC2044A3C60003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t\tProvisioningStyle = Automatic;\n\t\t\t\t\t\tSystemCapabilities = {\n\t\t\t\t\t\t\tcom.apple.Sandbox = {\n\t\t\t\t\t\t\t\tenabled = 1;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t\t33CC111A2044C6BA0003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tProvisioningStyle = Manual;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 33CC10E42044A3C60003C045;\n\t\t\tproductRefGroup = 33CC10EE2044A3C60003C045 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t33CC10EC2044A3C60003C045 /* Runner */,\n\t\t\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t33CC10EB2044A3C60003C045 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,\n\t\t\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t3399D490228B24CF009A79C7 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"echo \\\"$PRODUCT_NAME.app\\\" > \\\"$PROJECT_DIR\\\"/Flutter/ephemeral/.app_filename && \\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh embed\\n\";\n\t\t};\n\t\t33CC111E2044C6BF0003C045 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterInputs.xcfilelist,\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\tFlutter/ephemeral/tripwire,\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterOutputs.xcfilelist,\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire\";\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t33CC10E92044A3C60003C045 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,\n\t\t\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,\n\t\t\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;\n\t\t\ttargetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F52044A3C60003C045 /* Base */,\n\t\t\t);\n\t\t\tname = MainMenu.xib;\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t338D0CE9231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEA231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEB231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t33CC10F92044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FA2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC10FC2044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FD2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC111C2044C6BA0003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC111D2044C6BA0003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10F92044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FA2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CE9231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10FC2044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FD2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CEA231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC111C2044C6BA0003C045 /* Debug */,\n\t\t\t\t33CC111D2044C6BA0003C045 /* Release */,\n\t\t\t\t338D0CEB231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 33CC10E52044A3C60003C045 /* Project object */;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1510\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n               BuildableName = \"example.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"example.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <Testables>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"example.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"example.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/pubspec.yaml",
    "content": "name: example\ndescription: A new Flutter project.\n\n# The following line prevents the package from being accidentally published to\n# pub.dev using `flutter pub publish`. This is preferred for private packages.\npublish_to: \"none\" # Remove this line if you wish to publish to pub.dev\n\n# The following defines the version and build number for your application.\n# A version number is three numbers separated by dots, like 1.2.43\n# followed by an optional build number separated by a +.\n# Both the version and the builder number may be overridden in flutter\n# build by specifying --build-name and --build-number, respectively.\n# In Android, build-name is used as versionName while build-number used as versionCode.\n# Read more about Android versioning at https://developer.android.com/studio/publish/versioning\n# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.\n# Read more about iOS versioning at\n# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html\nversion: 1.0.0+1\n\nenvironment:\n  sdk: \">=3.3.0 <4.0.0\"\n\n# Dependencies specify other packages that your package needs in order to work.\n# To automatically upgrade your package dependencies to the latest versions\n# consider running `flutter pub upgrade --major-versions`. Alternatively,\n# dependencies can be manually updated by changing the version numbers below to\n# the latest version available on pub.dev. To see which dependencies have newer\n# versions available, run `flutter pub outdated`.\ndependencies:\n  flutter:\n    sdk: flutter\n\n  # The following adds the Cupertino Icons font to your application.\n  # Use with the CupertinoIcons class for iOS style icons.\n  cupertino_icons: ^1.0.2\n  appflowy_popover:\n    path: ../\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n\n  # The \"flutter_lints\" package below contains a set of recommended lints to\n  # encourage good coding practices. The lint set provided by the package is\n  # activated in the `analysis_options.yaml` file located at the root of your\n  # package. See that file for information about deactivating specific lint\n  # rules and activating additional ones.\n  flutter_lints: ^3.0.1\n\n# For information on the generic Dart part of this file, see the\n# following page: https://dart.dev/tools/pub/pubspec\n\n# The following section is specific to Flutter packages.\nflutter:\n  # The following line ensures that the Material Icons font is\n  # included with your application, so that you can use the icons in\n  # the material Icons class.\n  uses-material-design: true\n\n  # To add assets to your application, add an assets section, like this:\n  # assets:\n  #   - images/a_dot_burr.jpeg\n  #   - images/a_dot_ham.jpeg\n\n  # An image asset can refer to one or more resolution-specific \"variants\", see\n  # https://flutter.dev/assets-and-images/#resolution-aware\n\n  # For details regarding adding assets from package dependencies, see\n  # https://flutter.dev/assets-and-images/#from-packages\n\n  # To add custom fonts to your application, add a fonts section here,\n  # in this \"flutter\" section. Each entry in this list should have a\n  # \"family\" key with the font family name, and a \"fonts\" key with a\n  # list giving the asset and other descriptors for the font. For\n  # example:\n  # fonts:\n  #   - family: Schyler\n  #     fonts:\n  #       - asset: fonts/Schyler-Regular.ttf\n  #       - asset: fonts/Schyler-Italic.ttf\n  #         style: italic\n  #   - family: Trajan Pro\n  #     fonts:\n  #       - asset: fonts/TrajanPro.ttf\n  #       - asset: fonts/TrajanPro_Bold.ttf\n  #         weight: 700\n  #\n  # For details regarding fonts from package dependencies,\n  # see https://flutter.dev/custom-fonts/#from-packages\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/web/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <!--\n    If you are serving your web app in a path other than the root, change the\n    href value below to reflect the base path you are serving from.\n\n    The path provided below has to start and end with a slash \"/\" in order for\n    it to work correctly.\n\n    For more details:\n    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base\n\n    This is a placeholder for base href that will be replaced by the value of\n    the `--base-href` argument provided to `flutter build`.\n  -->\n  <base href=\"$FLUTTER_BASE_HREF\">\n\n  <meta charset=\"UTF-8\">\n  <meta content=\"IE=Edge\" http-equiv=\"X-UA-Compatible\">\n  <meta name=\"description\" content=\"A new Flutter project.\">\n\n  <!-- iOS meta tags & icons -->\n  <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n  <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\">\n  <meta name=\"apple-mobile-web-app-title\" content=\"example\">\n  <link rel=\"apple-touch-icon\" href=\"icons/Icon-192.png\">\n\n  <!-- Favicon -->\n  <link rel=\"icon\" type=\"image/png\" href=\"favicon.png\"/>\n\n  <title>example</title>\n  <link rel=\"manifest\" href=\"manifest.json\">\n\n  <script>\n    // The value below is injected by flutter build, do not touch.\n    var serviceWorkerVersion = null;\n  </script>\n  <!-- This script adds the flutter initialization JS code -->\n  <script src=\"flutter.js\" defer></script>\n</head>\n<body>\n  <script>\n    window.addEventListener('load', function(ev) {\n      // Download main.dart.js\n      _flutter.loader.loadEntrypoint({\n        serviceWorker: {\n          serviceWorkerVersion: serviceWorkerVersion,\n        }\n      }).then(function(engineInitializer) {\n        return engineInitializer.initializeEngine();\n      }).then(function(appRunner) {\n        return appRunner.runApp();\n      });\n    });\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/web/manifest.json",
    "content": "{\n    \"name\": \"example\",\n    \"short_name\": \"example\",\n    \"start_url\": \".\",\n    \"display\": \"standalone\",\n    \"background_color\": \"#0175C2\",\n    \"theme_color\": \"#0175C2\",\n    \"description\": \"A new Flutter project.\",\n    \"orientation\": \"portrait-primary\",\n    \"prefer_related_applications\": false,\n    \"icons\": [\n        {\n            \"src\": \"icons/Icon-192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"icons/Icon-512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"icons/Icon-maskable-192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\",\n            \"purpose\": \"maskable\"\n        },\n        {\n            \"src\": \"icons/Icon-maskable-512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\",\n            \"purpose\": \"maskable\"\n        }\n    ]\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/windows/.gitignore",
    "content": "flutter/ephemeral/\n\n# Visual Studio user-specific files.\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# Visual Studio build-related files.\nx64/\nx86/\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/windows/CMakeLists.txt",
    "content": "# Project-level configuration.\ncmake_minimum_required(VERSION 3.14)\nproject(example LANGUAGES CXX)\n\n# The name of the executable created for the application. Change this to change\n# the on-disk name of your application.\nset(BINARY_NAME \"example\")\n\n# Explicitly opt in to modern CMake behaviors to avoid warnings with recent\n# versions of CMake.\ncmake_policy(SET CMP0063 NEW)\n\n# Define build configuration option.\nget_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)\nif(IS_MULTICONFIG)\n  set(CMAKE_CONFIGURATION_TYPES \"Debug;Profile;Release\"\n    CACHE STRING \"\" FORCE)\nelse()\n  if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n    set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n      STRING \"Flutter build mode\" FORCE)\n    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n      \"Debug\" \"Profile\" \"Release\")\n  endif()\nendif()\n# Define settings for the Profile build mode.\nset(CMAKE_EXE_LINKER_FLAGS_PROFILE \"${CMAKE_EXE_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_SHARED_LINKER_FLAGS_PROFILE \"${CMAKE_SHARED_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_C_FLAGS_PROFILE \"${CMAKE_C_FLAGS_RELEASE}\")\nset(CMAKE_CXX_FLAGS_PROFILE \"${CMAKE_CXX_FLAGS_RELEASE}\")\n\n# Use Unicode for all projects.\nadd_definitions(-DUNICODE -D_UNICODE)\n\n# Compilation settings that should be applied to most targets.\n#\n# Be cautious about adding new options here, as plugins use this function by\n# default. In most cases, you should add new options to specific targets instead\n# of modifying this function.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_17)\n  target_compile_options(${TARGET} PRIVATE /W4 /WX /wd\"4100\")\n  target_compile_options(${TARGET} PRIVATE /EHsc)\n  target_compile_definitions(${TARGET} PRIVATE \"_HAS_EXCEPTIONS=0\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<CONFIG:Debug>:_DEBUG>\")\nendfunction()\n\n# Flutter library and tool build rules.\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# Application build; see runner/CMakeLists.txt.\nadd_subdirectory(\"runner\")\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n\n# === Installation ===\n# Support files are copied into place next to the executable, so that it can\n# run in place. This is done instead of making a separate bundle (as on Linux)\n# so that building and running from within Visual Studio will work.\nset(BUILD_BUNDLE_DIR \"$<TARGET_FILE_DIR:${BINARY_NAME}>\")\n# Make the \"install\" step default, as it's required to run.\nset(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nif(PLUGIN_BUNDLED_LIBRARIES)\n  install(FILES \"${PLUGIN_BUNDLED_LIBRARIES}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n\n# Fully re-copy the assets directory on each build to avoid having stale files\n# from a previous install.\nset(FLUTTER_ASSET_DIR_NAME \"flutter_assets\")\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\\\")\n  \" COMPONENT Runtime)\ninstall(DIRECTORY \"${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}\"\n  DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\" COMPONENT Runtime)\n\n# Install the AOT library on non-Debug builds only.\ninstall(FILES \"${AOT_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  CONFIGURATIONS Profile;Release\n  COMPONENT Runtime)\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/windows/flutter/CMakeLists.txt",
    "content": "# This file controls Flutter-level build steps. It should not be edited.\ncmake_minimum_required(VERSION 3.14)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\nset(WRAPPER_ROOT \"${EPHEMERAL_DIR}/cpp_client_wrapper\")\n\n# === Flutter Library ===\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/flutter_windows.dll\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/windows/app.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"flutter_export.h\"\n  \"flutter_windows.h\"\n  \"flutter_messenger.h\"\n  \"flutter_plugin_registrar.h\"\n  \"flutter_texture_registrar.h\"\n)\nlist(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND \"${EPHEMERAL_DIR}/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}.lib\")\nadd_dependencies(flutter flutter_assemble)\n\n# === Wrapper ===\nlist(APPEND CPP_WRAPPER_SOURCES_CORE\n  \"core_implementations.cc\"\n  \"standard_codec.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_PLUGIN\n  \"plugin_registrar.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_APP\n  \"flutter_engine.cc\"\n  \"flutter_view_controller.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND \"${WRAPPER_ROOT}/\")\n\n# Wrapper sources needed for a plugin.\nadd_library(flutter_wrapper_plugin STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n)\napply_standard_settings(flutter_wrapper_plugin)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  POSITION_INDEPENDENT_CODE ON)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  CXX_VISIBILITY_PRESET hidden)\ntarget_link_libraries(flutter_wrapper_plugin PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_plugin PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_plugin flutter_assemble)\n\n# Wrapper sources needed for the runner.\nadd_library(flutter_wrapper_app STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\napply_standard_settings(flutter_wrapper_app)\ntarget_link_libraries(flutter_wrapper_app PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_app PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_app flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nset(PHONY_OUTPUT \"${CMAKE_CURRENT_BINARY_DIR}/_phony_\")\nset_source_files_properties(\"${PHONY_OUTPUT}\" PROPERTIES SYMBOLIC TRUE)\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}\n    ${CPP_WRAPPER_SOURCES_APP}\n    ${PHONY_OUTPUT}\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat\"\n      windows-x64 $<CONFIG>\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/windows/runner/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.14)\nproject(runner LANGUAGES CXX)\n\n# Define the application target. To change its name, change BINARY_NAME in the\n# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer\n# work.\n#\n# Any new source files that you add to the application should be added here.\nadd_executable(${BINARY_NAME} WIN32\n  \"flutter_window.cpp\"\n  \"main.cpp\"\n  \"utils.cpp\"\n  \"win32_window.cpp\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n  \"Runner.rc\"\n  \"runner.exe.manifest\"\n)\n\n# Apply the standard set of build settings. This can be removed for applications\n# that need different build settings.\napply_standard_settings(${BINARY_NAME})\n\n# Disable Windows macros that collide with C++ standard library functions.\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"NOMINMAX\")\n\n# Add dependency libraries and include directories. Add any application-specific\n# dependencies here.\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)\ntarget_include_directories(${BINARY_NAME} PRIVATE \"${CMAKE_SOURCE_DIR}\")\n\n# Run the Flutter tool portions of the build. This must not be removed.\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/windows/runner/Runner.rc",
    "content": "// Microsoft Visual C++ generated resource script.\n//\n#pragma code_page(65001)\n#include \"resource.h\"\n\n#define APSTUDIO_READONLY_SYMBOLS\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 2 resource.\n//\n#include \"winres.h\"\n\n/////////////////////////////////////////////////////////////////////////////\n#undef APSTUDIO_READONLY_SYMBOLS\n\n/////////////////////////////////////////////////////////////////////////////\n// English (United States) resources\n\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\nLANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US\n\n#ifdef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// TEXTINCLUDE\n//\n\n1 TEXTINCLUDE\nBEGIN\n    \"resource.h\\0\"\nEND\n\n2 TEXTINCLUDE\nBEGIN\n    \"#include \"\"winres.h\"\"\\r\\n\"\n    \"\\0\"\nEND\n\n3 TEXTINCLUDE\nBEGIN\n    \"\\r\\n\"\n    \"\\0\"\nEND\n\n#endif    // APSTUDIO_INVOKED\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Icon\n//\n\n// Icon with lowest ID value placed first to ensure application icon\n// remains consistent on all systems.\nIDI_APP_ICON            ICON                    \"resources\\\\app_icon.ico\"\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Version\n//\n\n#ifdef FLUTTER_BUILD_NUMBER\n#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER\n#else\n#define VERSION_AS_NUMBER 1,0,0\n#endif\n\n#ifdef FLUTTER_BUILD_NAME\n#define VERSION_AS_STRING #FLUTTER_BUILD_NAME\n#else\n#define VERSION_AS_STRING \"1.0.0\"\n#endif\n\nVS_VERSION_INFO VERSIONINFO\n FILEVERSION VERSION_AS_NUMBER\n PRODUCTVERSION VERSION_AS_NUMBER\n FILEFLAGSMASK VS_FFI_FILEFLAGSMASK\n#ifdef _DEBUG\n FILEFLAGS VS_FF_DEBUG\n#else\n FILEFLAGS 0x0L\n#endif\n FILEOS VOS__WINDOWS32\n FILETYPE VFT_APP\n FILESUBTYPE 0x0L\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904e4\"\n        BEGIN\n            VALUE \"CompanyName\", \"com.example\" \"\\0\"\n            VALUE \"FileDescription\", \"example\" \"\\0\"\n            VALUE \"FileVersion\", VERSION_AS_STRING \"\\0\"\n            VALUE \"InternalName\", \"example\" \"\\0\"\n            VALUE \"LegalCopyright\", \"Copyright (C) 2022 com.example. All rights reserved.\" \"\\0\"\n            VALUE \"OriginalFilename\", \"example.exe\" \"\\0\"\n            VALUE \"ProductName\", \"example\" \"\\0\"\n            VALUE \"ProductVersion\", VERSION_AS_STRING \"\\0\"\n        END\n    END\n    BLOCK \"VarFileInfo\"\n    BEGIN\n        VALUE \"Translation\", 0x409, 1252\n    END\nEND\n\n#endif    // English (United States) resources\n/////////////////////////////////////////////////////////////////////////////\n\n\n\n#ifndef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 3 resource.\n//\n\n\n/////////////////////////////////////////////////////////////////////////////\n#endif    // not APSTUDIO_INVOKED\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/windows/runner/flutter_window.cpp",
    "content": "#include \"flutter_window.h\"\n\n#include <optional>\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nFlutterWindow::FlutterWindow(const flutter::DartProject& project)\n    : project_(project) {}\n\nFlutterWindow::~FlutterWindow() {}\n\nbool FlutterWindow::OnCreate() {\n  if (!Win32Window::OnCreate()) {\n    return false;\n  }\n\n  RECT frame = GetClientArea();\n\n  // The size here must match the window dimensions to avoid unnecessary surface\n  // creation / destruction in the startup path.\n  flutter_controller_ = std::make_unique<flutter::FlutterViewController>(\n      frame.right - frame.left, frame.bottom - frame.top, project_);\n  // Ensure that basic setup of the controller was successful.\n  if (!flutter_controller_->engine() || !flutter_controller_->view()) {\n    return false;\n  }\n  RegisterPlugins(flutter_controller_->engine());\n  SetChildContent(flutter_controller_->view()->GetNativeWindow());\n  return true;\n}\n\nvoid FlutterWindow::OnDestroy() {\n  if (flutter_controller_) {\n    flutter_controller_ = nullptr;\n  }\n\n  Win32Window::OnDestroy();\n}\n\nLRESULT\nFlutterWindow::MessageHandler(HWND hwnd, UINT const message,\n                              WPARAM const wparam,\n                              LPARAM const lparam) noexcept {\n  // Give Flutter, including plugins, an opportunity to handle window messages.\n  if (flutter_controller_) {\n    std::optional<LRESULT> result =\n        flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,\n                                                      lparam);\n    if (result) {\n      return *result;\n    }\n  }\n\n  switch (message) {\n    case WM_FONTCHANGE:\n      flutter_controller_->engine()->ReloadSystemFonts();\n      break;\n  }\n\n  return Win32Window::MessageHandler(hwnd, message, wparam, lparam);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/windows/runner/flutter_window.h",
    "content": "#ifndef RUNNER_FLUTTER_WINDOW_H_\n#define RUNNER_FLUTTER_WINDOW_H_\n\n#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n\n#include <memory>\n\n#include \"win32_window.h\"\n\n// A window that does nothing but host a Flutter view.\nclass FlutterWindow : public Win32Window {\n public:\n  // Creates a new FlutterWindow hosting a Flutter view running |project|.\n  explicit FlutterWindow(const flutter::DartProject& project);\n  virtual ~FlutterWindow();\n\n protected:\n  // Win32Window:\n  bool OnCreate() override;\n  void OnDestroy() override;\n  LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,\n                         LPARAM const lparam) noexcept override;\n\n private:\n  // The project to run.\n  flutter::DartProject project_;\n\n  // The Flutter instance hosted by this window.\n  std::unique_ptr<flutter::FlutterViewController> flutter_controller_;\n};\n\n#endif  // RUNNER_FLUTTER_WINDOW_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/windows/runner/main.cpp",
    "content": "#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n#include <windows.h>\n\n#include \"flutter_window.h\"\n#include \"utils.h\"\n\nint APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,\n                      _In_ wchar_t *command_line, _In_ int show_command) {\n  // Attach to console when present (e.g., 'flutter run') or create a\n  // new console when running with a debugger.\n  if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {\n    CreateAndAttachConsole();\n  }\n\n  // Initialize COM, so that it is available for use in the library and/or\n  // plugins.\n  ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);\n\n  flutter::DartProject project(L\"data\");\n\n  std::vector<std::string> command_line_arguments =\n      GetCommandLineArguments();\n\n  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));\n\n  FlutterWindow window(project);\n  Win32Window::Point origin(10, 10);\n  Win32Window::Size size(1280, 720);\n  if (!window.CreateAndShow(L\"example\", origin, size)) {\n    return EXIT_FAILURE;\n  }\n  window.SetQuitOnClose(true);\n\n  ::MSG msg;\n  while (::GetMessage(&msg, nullptr, 0, 0)) {\n    ::TranslateMessage(&msg);\n    ::DispatchMessage(&msg);\n  }\n\n  ::CoUninitialize();\n  return EXIT_SUCCESS;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/windows/runner/resource.h",
    "content": "//{{NO_DEPENDENCIES}}\n// Microsoft Visual C++ generated include file.\n// Used by Runner.rc\n//\n#define IDI_APP_ICON                    101\n\n// Next default values for new objects\n//\n#ifdef APSTUDIO_INVOKED\n#ifndef APSTUDIO_READONLY_SYMBOLS\n#define _APS_NEXT_RESOURCE_VALUE        102\n#define _APS_NEXT_COMMAND_VALUE         40001\n#define _APS_NEXT_CONTROL_VALUE         1001\n#define _APS_NEXT_SYMED_VALUE           101\n#endif\n#endif\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/windows/runner/runner.exe.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2</dpiAwareness>\n    </windowsSettings>\n  </application>\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- Windows 10 -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"/>\n      <!-- Windows 8.1 -->\n      <supportedOS Id=\"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\"/>\n      <!-- Windows 8 -->\n      <supportedOS Id=\"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\"/>\n      <!-- Windows 7 -->\n      <supportedOS Id=\"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\"/>\n    </application>\n  </compatibility>\n</assembly>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/windows/runner/utils.cpp",
    "content": "#include \"utils.h\"\n\n#include <flutter_windows.h>\n#include <io.h>\n#include <stdio.h>\n#include <windows.h>\n\n#include <iostream>\n\nvoid CreateAndAttachConsole() {\n  if (::AllocConsole()) {\n    FILE *unused;\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stdout)) {\n      _dup2(_fileno(stdout), 1);\n    }\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stderr)) {\n      _dup2(_fileno(stdout), 2);\n    }\n    std::ios::sync_with_stdio();\n    FlutterDesktopResyncOutputStreams();\n  }\n}\n\nstd::vector<std::string> GetCommandLineArguments() {\n  // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.\n  int argc;\n  wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);\n  if (argv == nullptr) {\n    return std::vector<std::string>();\n  }\n\n  std::vector<std::string> command_line_arguments;\n\n  // Skip the first argument as it's the binary name.\n  for (int i = 1; i < argc; i++) {\n    command_line_arguments.push_back(Utf8FromUtf16(argv[i]));\n  }\n\n  ::LocalFree(argv);\n\n  return command_line_arguments;\n}\n\nstd::string Utf8FromUtf16(const wchar_t* utf16_string) {\n  if (utf16_string == nullptr) {\n    return std::string();\n  }\n  int target_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      -1, nullptr, 0, nullptr, nullptr);\n  std::string utf8_string;\n  if (target_length == 0 || target_length > utf8_string.max_size()) {\n    return utf8_string;\n  }\n  utf8_string.resize(target_length);\n  int converted_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      -1, utf8_string.data(),\n      target_length, nullptr, nullptr);\n  if (converted_length == 0) {\n    return std::string();\n  }\n  return utf8_string;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/windows/runner/utils.h",
    "content": "#ifndef RUNNER_UTILS_H_\n#define RUNNER_UTILS_H_\n\n#include <string>\n#include <vector>\n\n// Creates a console for the process, and redirects stdout and stderr to\n// it for both the runner and the Flutter library.\nvoid CreateAndAttachConsole();\n\n// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string\n// encoded in UTF-8. Returns an empty std::string on failure.\nstd::string Utf8FromUtf16(const wchar_t* utf16_string);\n\n// Gets the command line arguments passed in as a std::vector<std::string>,\n// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.\nstd::vector<std::string> GetCommandLineArguments();\n\n#endif  // RUNNER_UTILS_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/windows/runner/win32_window.cpp",
    "content": "#include \"win32_window.h\"\n\n#include <flutter_windows.h>\n\n#include \"resource.h\"\n\nnamespace {\n\nconstexpr const wchar_t kWindowClassName[] = L\"FLUTTER_RUNNER_WIN32_WINDOW\";\n\n// The number of Win32Window objects that currently exist.\nstatic int g_active_window_count = 0;\n\nusing EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);\n\n// Scale helper to convert logical scaler values to physical using passed in\n// scale factor\nint Scale(int source, double scale_factor) {\n  return static_cast<int>(source * scale_factor);\n}\n\n// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.\n// This API is only needed for PerMonitor V1 awareness mode.\nvoid EnableFullDpiSupportIfAvailable(HWND hwnd) {\n  HMODULE user32_module = LoadLibraryA(\"User32.dll\");\n  if (!user32_module) {\n    return;\n  }\n  auto enable_non_client_dpi_scaling =\n      reinterpret_cast<EnableNonClientDpiScaling*>(\n          GetProcAddress(user32_module, \"EnableNonClientDpiScaling\"));\n  if (enable_non_client_dpi_scaling != nullptr) {\n    enable_non_client_dpi_scaling(hwnd);\n    FreeLibrary(user32_module);\n  }\n}\n\n}  // namespace\n\n// Manages the Win32Window's window class registration.\nclass WindowClassRegistrar {\n public:\n  ~WindowClassRegistrar() = default;\n\n  // Returns the singleton registar instance.\n  static WindowClassRegistrar* GetInstance() {\n    if (!instance_) {\n      instance_ = new WindowClassRegistrar();\n    }\n    return instance_;\n  }\n\n  // Returns the name of the window class, registering the class if it hasn't\n  // previously been registered.\n  const wchar_t* GetWindowClass();\n\n  // Unregisters the window class. Should only be called if there are no\n  // instances of the window.\n  void UnregisterWindowClass();\n\n private:\n  WindowClassRegistrar() = default;\n\n  static WindowClassRegistrar* instance_;\n\n  bool class_registered_ = false;\n};\n\nWindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;\n\nconst wchar_t* WindowClassRegistrar::GetWindowClass() {\n  if (!class_registered_) {\n    WNDCLASS window_class{};\n    window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);\n    window_class.lpszClassName = kWindowClassName;\n    window_class.style = CS_HREDRAW | CS_VREDRAW;\n    window_class.cbClsExtra = 0;\n    window_class.cbWndExtra = 0;\n    window_class.hInstance = GetModuleHandle(nullptr);\n    window_class.hIcon =\n        LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));\n    window_class.hbrBackground = 0;\n    window_class.lpszMenuName = nullptr;\n    window_class.lpfnWndProc = Win32Window::WndProc;\n    RegisterClass(&window_class);\n    class_registered_ = true;\n  }\n  return kWindowClassName;\n}\n\nvoid WindowClassRegistrar::UnregisterWindowClass() {\n  UnregisterClass(kWindowClassName, nullptr);\n  class_registered_ = false;\n}\n\nWin32Window::Win32Window() {\n  ++g_active_window_count;\n}\n\nWin32Window::~Win32Window() {\n  --g_active_window_count;\n  Destroy();\n}\n\nbool Win32Window::CreateAndShow(const std::wstring& title,\n                                const Point& origin,\n                                const Size& size) {\n  Destroy();\n\n  const wchar_t* window_class =\n      WindowClassRegistrar::GetInstance()->GetWindowClass();\n\n  const POINT target_point = {static_cast<LONG>(origin.x),\n                              static_cast<LONG>(origin.y)};\n  HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);\n  UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);\n  double scale_factor = dpi / 96.0;\n\n  HWND window = CreateWindow(\n      window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,\n      Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),\n      Scale(size.width, scale_factor), Scale(size.height, scale_factor),\n      nullptr, nullptr, GetModuleHandle(nullptr), this);\n\n  if (!window) {\n    return false;\n  }\n\n  return OnCreate();\n}\n\n// static\nLRESULT CALLBACK Win32Window::WndProc(HWND const window,\n                                      UINT const message,\n                                      WPARAM const wparam,\n                                      LPARAM const lparam) noexcept {\n  if (message == WM_NCCREATE) {\n    auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);\n    SetWindowLongPtr(window, GWLP_USERDATA,\n                     reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));\n\n    auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);\n    EnableFullDpiSupportIfAvailable(window);\n    that->window_handle_ = window;\n  } else if (Win32Window* that = GetThisFromHandle(window)) {\n    return that->MessageHandler(window, message, wparam, lparam);\n  }\n\n  return DefWindowProc(window, message, wparam, lparam);\n}\n\nLRESULT\nWin32Window::MessageHandler(HWND hwnd,\n                            UINT const message,\n                            WPARAM const wparam,\n                            LPARAM const lparam) noexcept {\n  switch (message) {\n    case WM_DESTROY:\n      window_handle_ = nullptr;\n      Destroy();\n      if (quit_on_close_) {\n        PostQuitMessage(0);\n      }\n      return 0;\n\n    case WM_DPICHANGED: {\n      auto newRectSize = reinterpret_cast<RECT*>(lparam);\n      LONG newWidth = newRectSize->right - newRectSize->left;\n      LONG newHeight = newRectSize->bottom - newRectSize->top;\n\n      SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,\n                   newHeight, SWP_NOZORDER | SWP_NOACTIVATE);\n\n      return 0;\n    }\n    case WM_SIZE: {\n      RECT rect = GetClientArea();\n      if (child_content_ != nullptr) {\n        // Size and position the child window.\n        MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,\n                   rect.bottom - rect.top, TRUE);\n      }\n      return 0;\n    }\n\n    case WM_ACTIVATE:\n      if (child_content_ != nullptr) {\n        SetFocus(child_content_);\n      }\n      return 0;\n  }\n\n  return DefWindowProc(window_handle_, message, wparam, lparam);\n}\n\nvoid Win32Window::Destroy() {\n  OnDestroy();\n\n  if (window_handle_) {\n    DestroyWindow(window_handle_);\n    window_handle_ = nullptr;\n  }\n  if (g_active_window_count == 0) {\n    WindowClassRegistrar::GetInstance()->UnregisterWindowClass();\n  }\n}\n\nWin32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {\n  return reinterpret_cast<Win32Window*>(\n      GetWindowLongPtr(window, GWLP_USERDATA));\n}\n\nvoid Win32Window::SetChildContent(HWND content) {\n  child_content_ = content;\n  SetParent(content, window_handle_);\n  RECT frame = GetClientArea();\n\n  MoveWindow(content, frame.left, frame.top, frame.right - frame.left,\n             frame.bottom - frame.top, true);\n\n  SetFocus(child_content_);\n}\n\nRECT Win32Window::GetClientArea() {\n  RECT frame;\n  GetClientRect(window_handle_, &frame);\n  return frame;\n}\n\nHWND Win32Window::GetHandle() {\n  return window_handle_;\n}\n\nvoid Win32Window::SetQuitOnClose(bool quit_on_close) {\n  quit_on_close_ = quit_on_close;\n}\n\nbool Win32Window::OnCreate() {\n  // No-op; provided for subclasses.\n  return true;\n}\n\nvoid Win32Window::OnDestroy() {\n  // No-op; provided for subclasses.\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/example/windows/runner/win32_window.h",
    "content": "#ifndef RUNNER_WIN32_WINDOW_H_\n#define RUNNER_WIN32_WINDOW_H_\n\n#include <windows.h>\n\n#include <functional>\n#include <memory>\n#include <string>\n\n// A class abstraction for a high DPI-aware Win32 Window. Intended to be\n// inherited from by classes that wish to specialize with custom\n// rendering and input handling\nclass Win32Window {\n public:\n  struct Point {\n    unsigned int x;\n    unsigned int y;\n    Point(unsigned int x, unsigned int y) : x(x), y(y) {}\n  };\n\n  struct Size {\n    unsigned int width;\n    unsigned int height;\n    Size(unsigned int width, unsigned int height)\n        : width(width), height(height) {}\n  };\n\n  Win32Window();\n  virtual ~Win32Window();\n\n  // Creates and shows a win32 window with |title| and position and size using\n  // |origin| and |size|. New windows are created on the default monitor. Window\n  // sizes are specified to the OS in physical pixels, hence to ensure a\n  // consistent size to will treat the width height passed in to this function\n  // as logical pixels and scale to appropriate for the default monitor. Returns\n  // true if the window was created successfully.\n  bool CreateAndShow(const std::wstring& title,\n                     const Point& origin,\n                     const Size& size);\n\n  // Release OS resources associated with window.\n  void Destroy();\n\n  // Inserts |content| into the window tree.\n  void SetChildContent(HWND content);\n\n  // Returns the backing Window handle to enable clients to set icon and other\n  // window properties. Returns nullptr if the window has been destroyed.\n  HWND GetHandle();\n\n  // If true, closing this window will quit the application.\n  void SetQuitOnClose(bool quit_on_close);\n\n  // Return a RECT representing the bounds of the current client area.\n  RECT GetClientArea();\n\n protected:\n  // Processes and route salient window messages for mouse handling,\n  // size change and DPI. Delegates handling of these to member overloads that\n  // inheriting classes can handle.\n  virtual LRESULT MessageHandler(HWND window,\n                                 UINT const message,\n                                 WPARAM const wparam,\n                                 LPARAM const lparam) noexcept;\n\n  // Called when CreateAndShow is called, allowing subclass window-related\n  // setup. Subclasses should return false if setup fails.\n  virtual bool OnCreate();\n\n  // Called when Destroy is called.\n  virtual void OnDestroy();\n\n private:\n  friend class WindowClassRegistrar;\n\n  // OS callback called by message pump. Handles the WM_NCCREATE message which\n  // is passed when the non-client area is being created and enables automatic\n  // non-client DPI scaling so that the non-client area automatically\n  // responsponds to changes in DPI. All other messages are handled by\n  // MessageHandler.\n  static LRESULT CALLBACK WndProc(HWND const window,\n                                  UINT const message,\n                                  WPARAM const wparam,\n                                  LPARAM const lparam) noexcept;\n\n  // Retrieves a class instance pointer for |window|\n  static Win32Window* GetThisFromHandle(HWND const window) noexcept;\n\n  bool quit_on_close_ = false;\n\n  // window handle for top level window.\n  HWND window_handle_ = nullptr;\n\n  // window handle for hosted content.\n  HWND child_content_ = nullptr;\n};\n\n#endif  // RUNNER_WIN32_WINDOW_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/lib/appflowy_popover.dart",
    "content": "/// AppFlowyBoard library\nlibrary;\n\nexport 'src/mutex.dart';\nexport 'src/popover.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/lib/src/follower.dart",
    "content": "import 'package:flutter/rendering.dart';\nimport 'package:flutter/widgets.dart';\n\nclass PopoverCompositedTransformFollower extends CompositedTransformFollower {\n  const PopoverCompositedTransformFollower({\n    super.key,\n    required super.link,\n    super.showWhenUnlinked = true,\n    super.offset = Offset.zero,\n    super.targetAnchor = Alignment.topLeft,\n    super.followerAnchor = Alignment.topLeft,\n    super.child,\n  });\n\n  @override\n  PopoverRenderFollowerLayer createRenderObject(BuildContext context) {\n    final screenSize = MediaQuery.of(context).size;\n    return PopoverRenderFollowerLayer(\n      screenSize: screenSize,\n      link: link,\n      showWhenUnlinked: showWhenUnlinked,\n      offset: offset,\n      leaderAnchor: targetAnchor,\n      followerAnchor: followerAnchor,\n    );\n  }\n\n  @override\n  void updateRenderObject(\n    BuildContext context,\n    PopoverRenderFollowerLayer renderObject,\n  ) {\n    final screenSize = MediaQuery.of(context).size;\n    renderObject\n      ..screenSize = screenSize\n      ..link = link\n      ..showWhenUnlinked = showWhenUnlinked\n      ..offset = offset\n      ..leaderAnchor = targetAnchor\n      ..followerAnchor = followerAnchor;\n  }\n}\n\nclass PopoverRenderFollowerLayer extends RenderFollowerLayer {\n  PopoverRenderFollowerLayer({\n    required super.link,\n    super.showWhenUnlinked = true,\n    super.offset = Offset.zero,\n    super.leaderAnchor = Alignment.topLeft,\n    super.followerAnchor = Alignment.topLeft,\n    super.child,\n    required this.screenSize,\n  });\n\n  Size screenSize;\n\n  @override\n  void paint(PaintingContext context, Offset offset) {\n    super.paint(context, offset);\n\n    if (link.leader == null) {\n      return;\n    }\n  }\n}\n\nclass EdgeFollowerLayer extends FollowerLayer {\n  EdgeFollowerLayer({\n    required super.link,\n    super.showWhenUnlinked = true,\n    super.unlinkedOffset = Offset.zero,\n    super.linkedOffset = Offset.zero,\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/lib/src/layout.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/rendering.dart';\n\nimport './popover.dart';\n\nclass PopoverLayoutDelegate extends SingleChildLayoutDelegate {\n  PopoverLayoutDelegate({\n    required this.link,\n    required this.direction,\n    required this.offset,\n    required this.windowPadding,\n    this.position,\n    this.showAtCursor = false,\n  });\n\n  PopoverLink link;\n  PopoverDirection direction;\n  final Offset offset;\n  final EdgeInsets windowPadding;\n\n  /// Required when [showAtCursor] is true.\n  ///\n  final Offset? position;\n\n  /// If true, the popover will be shown at the cursor position.\n  /// This will ignore the [direction], and the child size.\n  ///\n  final bool showAtCursor;\n\n  @override\n  bool shouldRelayout(PopoverLayoutDelegate oldDelegate) {\n    if (direction != oldDelegate.direction) {\n      return true;\n    }\n\n    if (link != oldDelegate.link) {\n      return true;\n    }\n\n    if (link.leaderOffset != oldDelegate.link.leaderOffset) {\n      return true;\n    }\n\n    if (link.leaderSize != oldDelegate.link.leaderSize) {\n      return true;\n    }\n\n    return false;\n  }\n\n  @override\n  Size getSize(BoxConstraints constraints) {\n    return Size(\n      constraints.maxWidth - windowPadding.left - windowPadding.right,\n      constraints.maxHeight - windowPadding.top - windowPadding.bottom,\n    );\n  }\n\n  @override\n  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {\n    return BoxConstraints(\n      maxWidth: constraints.maxWidth - windowPadding.left - windowPadding.right,\n      maxHeight:\n          constraints.maxHeight - windowPadding.top - windowPadding.bottom,\n    );\n  }\n\n  @override\n  Offset getPositionForChild(Size size, Size childSize) {\n    final effectiveOffset = link.leaderOffset;\n    final leaderSize = link.leaderSize;\n\n    if (effectiveOffset == null || leaderSize == null) {\n      return Offset.zero;\n    }\n\n    Offset position;\n    if (showAtCursor && this.position != null) {\n      position = this.position! +\n          Offset(\n            effectiveOffset.dx + offset.dx,\n            effectiveOffset.dy + offset.dy,\n          );\n    } else {\n      final anchorRect = Rect.fromLTWH(\n        effectiveOffset.dx + offset.dx,\n        effectiveOffset.dy + offset.dy,\n        leaderSize.width,\n        leaderSize.height,\n      );\n\n      switch (direction) {\n        case PopoverDirection.topLeft:\n          position = Offset(\n            anchorRect.left - childSize.width,\n            anchorRect.top - childSize.height,\n          );\n          break;\n        case PopoverDirection.topRight:\n          position = Offset(\n            anchorRect.right,\n            anchorRect.top - childSize.height,\n          );\n          break;\n        case PopoverDirection.bottomLeft:\n          position = Offset(\n            anchorRect.left - childSize.width,\n            anchorRect.bottom,\n          );\n          break;\n        case PopoverDirection.bottomRight:\n          position = Offset(\n            anchorRect.right,\n            anchorRect.bottom,\n          );\n          break;\n        case PopoverDirection.center:\n          position = anchorRect.center;\n          break;\n        case PopoverDirection.topWithLeftAligned:\n          position = Offset(\n            anchorRect.left,\n            anchorRect.top - childSize.height,\n          );\n          break;\n        case PopoverDirection.topWithCenterAligned:\n          position = Offset(\n            anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0,\n            anchorRect.top - childSize.height,\n          );\n          break;\n        case PopoverDirection.topWithRightAligned:\n          position = Offset(\n            anchorRect.right - childSize.width,\n            anchorRect.top - childSize.height,\n          );\n          break;\n        case PopoverDirection.rightWithTopAligned:\n          position = Offset(anchorRect.right, anchorRect.top);\n          break;\n        case PopoverDirection.rightWithCenterAligned:\n          position = Offset(\n            anchorRect.right,\n            anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0,\n          );\n          break;\n        case PopoverDirection.rightWithBottomAligned:\n          position = Offset(\n            anchorRect.right,\n            anchorRect.bottom - childSize.height,\n          );\n          break;\n        case PopoverDirection.bottomWithLeftAligned:\n          position = Offset(\n            anchorRect.left,\n            anchorRect.bottom,\n          );\n          break;\n        case PopoverDirection.bottomWithCenterAligned:\n          position = Offset(\n            anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0,\n            anchorRect.bottom,\n          );\n          break;\n        case PopoverDirection.bottomWithRightAligned:\n          position = Offset(\n            anchorRect.right - childSize.width,\n            anchorRect.bottom,\n          );\n          break;\n        case PopoverDirection.leftWithTopAligned:\n          position = Offset(\n            anchorRect.left - childSize.width,\n            anchorRect.top,\n          );\n          break;\n        case PopoverDirection.leftWithCenterAligned:\n          position = Offset(\n            anchorRect.left - childSize.width,\n            anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0,\n          );\n          break;\n        case PopoverDirection.leftWithBottomAligned:\n          position = Offset(\n            anchorRect.left - childSize.width,\n            anchorRect.bottom - childSize.height,\n          );\n          break;\n        default:\n          throw UnimplementedError();\n      }\n    }\n\n    return Offset(\n      math.max(\n        windowPadding.left,\n        math.min(\n          windowPadding.left + size.width - childSize.width,\n          position.dx,\n        ),\n      ),\n      math.max(\n        windowPadding.top,\n        math.min(\n          windowPadding.top + size.height - childSize.height,\n          position.dy,\n        ),\n      ),\n    );\n  }\n\n  PopoverLayoutDelegate copyWith({\n    PopoverLink? link,\n    PopoverDirection? direction,\n    Offset? offset,\n    EdgeInsets? windowPadding,\n    Offset? position,\n    bool? showAtCursor,\n  }) {\n    return PopoverLayoutDelegate(\n      link: link ?? this.link,\n      direction: direction ?? this.direction,\n      offset: offset ?? this.offset,\n      windowPadding: windowPadding ?? this.windowPadding,\n      position: position ?? this.position,\n      showAtCursor: showAtCursor ?? this.showAtCursor,\n    );\n  }\n}\n\nclass PopoverTarget extends SingleChildRenderObjectWidget {\n  const PopoverTarget({\n    super.key,\n    super.child,\n    required this.link,\n  });\n\n  final PopoverLink link;\n\n  @override\n  PopoverTargetRenderBox createRenderObject(BuildContext context) {\n    return PopoverTargetRenderBox(\n      link: link,\n    );\n  }\n\n  @override\n  void updateRenderObject(\n    BuildContext context,\n    PopoverTargetRenderBox renderObject,\n  ) {\n    renderObject.link = link;\n  }\n}\n\nclass PopoverTargetRenderBox extends RenderProxyBox {\n  PopoverTargetRenderBox({\n    required this.link,\n    RenderBox? child,\n  }) : super(child);\n\n  PopoverLink link;\n\n  @override\n  bool get alwaysNeedsCompositing => true;\n\n  @override\n  void performLayout() {\n    super.performLayout();\n    link.leaderSize = size;\n  }\n\n  @override\n  void paint(PaintingContext context, Offset offset) {\n    link.leaderOffset = localToGlobal(Offset.zero);\n    super.paint(context, offset);\n  }\n\n  @override\n  void detach() {\n    super.detach();\n    link.leaderOffset = null;\n    link.leaderSize = null;\n  }\n\n  @override\n  void attach(covariant PipelineOwner owner) {\n    super.attach(owner);\n    if (hasSize) {\n      // The leaderSize was set after [performLayout], but was\n      // set to null when [detach] get called.\n      //\n      // set the leaderSize when attach get called\n      link.leaderSize = size;\n    }\n  }\n\n  @override\n  void debugFillProperties(DiagnosticPropertiesBuilder properties) {\n    super.debugFillProperties(properties);\n    properties.add(DiagnosticsProperty<PopoverLink>('link', link));\n  }\n}\n\nclass PopoverLink {\n  Offset? leaderOffset;\n  Size? leaderSize;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart",
    "content": "import 'dart:collection';\n\nimport 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flutter/material.dart';\n\ntypedef _EntryMap = LinkedHashMap<PopoverState, OverlayEntryContext>;\n\nclass RootOverlayEntry {\n  final _EntryMap _entries = _EntryMap();\n\n  bool contains(PopoverState state) => _entries.containsKey(state);\n\n  bool get isEmpty => _entries.isEmpty;\n  bool get isNotEmpty => _entries.isNotEmpty;\n\n  void addEntry(\n    BuildContext context,\n    String id,\n    PopoverState newState,\n    OverlayEntry entry,\n    bool asBarrier,\n    AnimationController animationController,\n  ) {\n    _entries[newState] = OverlayEntryContext(\n      id,\n      entry,\n      newState,\n      asBarrier,\n      animationController,\n    );\n    Overlay.of(context).insert(entry);\n  }\n\n  void removeEntry(PopoverState state) {\n    final removedEntry = _entries.remove(state);\n    removedEntry?.overlayEntry.remove();\n  }\n\n  OverlayEntryContext? popEntry() {\n    if (isEmpty) {\n      return null;\n    }\n\n    final lastEntry = _entries.values.last;\n    _entries.remove(lastEntry.popoverState);\n    lastEntry.animationController.reverse().then((_) {\n      lastEntry.overlayEntry.remove();\n      lastEntry.popoverState.widget.onClose?.call();\n    });\n\n    return lastEntry.asBarrier ? lastEntry : popEntry();\n  }\n\n  bool isLastEntryAsBarrier() {\n    if (isEmpty) {\n      return false;\n    }\n\n    return _entries.values.last.asBarrier;\n  }\n}\n\nclass OverlayEntryContext {\n  OverlayEntryContext(\n    this.id,\n    this.overlayEntry,\n    this.popoverState,\n    this.asBarrier,\n    this.animationController,\n  );\n\n  final String id;\n  final OverlayEntry overlayEntry;\n  final PopoverState popoverState;\n  final bool asBarrier;\n  final AnimationController animationController;\n\n  @override\n  String toString() {\n    return 'OverlayEntryContext(id: $id, asBarrier: $asBarrier, popoverState: ${popoverState.widget.debugId})';\n  }\n}\n\nclass PopoverMask extends StatelessWidget {\n  const PopoverMask({\n    super.key,\n    required this.onTap,\n    this.decoration,\n  });\n\n  final VoidCallback onTap;\n  final Decoration? decoration;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onTap: onTap,\n      child: Container(\n        decoration: decoration,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mutex.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'popover.dart';\n\n/// If multiple popovers are exclusive,\n/// pass the same mutex to them.\nclass PopoverMutex {\n  PopoverMutex();\n\n  final _PopoverStateNotifier _stateNotifier = _PopoverStateNotifier();\n\n  void addPopoverListener(VoidCallback listener) {\n    _stateNotifier.addListener(listener);\n  }\n\n  void removePopoverListener(VoidCallback listener) {\n    _stateNotifier.removeListener(listener);\n  }\n\n  void close() => _stateNotifier.state?.close();\n\n  PopoverState? get state => _stateNotifier.state;\n\n  set state(PopoverState? newState) => _stateNotifier.state = newState;\n\n  void removeState() {\n    _stateNotifier.state = null;\n  }\n\n  void dispose() {\n    _stateNotifier.dispose();\n  }\n}\n\nclass _PopoverStateNotifier extends ChangeNotifier {\n  PopoverState? _state;\n\n  PopoverState? get state => _state;\n\n  set state(PopoverState? newState) {\n    if (_state != null && _state != newState) {\n      _state?.close();\n    }\n    _state = newState;\n    notifyListeners();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart",
    "content": "import 'package:appflowy_popover/src/layout.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nimport 'mask.dart';\nimport 'mutex.dart';\n\nclass PopoverController {\n  PopoverState? _state;\n\n  void close() => _state?.close();\n  void show() => _state?.showOverlay();\n  void showAt(Offset position) => _state?.showOverlay(position);\n}\n\nclass PopoverTriggerFlags {\n  static const int none = 0x00;\n  static const int click = 0x01;\n  static const int hover = 0x02;\n  static const int secondaryClick = 0x04;\n}\n\nenum PopoverDirection {\n  // Corner aligned with a corner of the SourceWidget\n  topLeft,\n  topRight,\n  bottomLeft,\n  bottomRight,\n  center,\n\n  // Edge aligned with a edge of the SourceWidget\n  topWithLeftAligned,\n  topWithCenterAligned,\n  topWithRightAligned,\n  rightWithTopAligned,\n  rightWithCenterAligned,\n  rightWithBottomAligned,\n  bottomWithLeftAligned,\n  bottomWithCenterAligned,\n  bottomWithRightAligned,\n  leftWithTopAligned,\n  leftWithCenterAligned,\n  leftWithBottomAligned,\n\n  custom,\n}\n\nenum PopoverClickHandler {\n  listener,\n  gestureDetector,\n}\n\nclass Popover extends StatefulWidget {\n  const Popover({\n    super.key,\n    required this.child,\n    required this.popupBuilder,\n    this.controller,\n    this.offset,\n    this.triggerActions = 0,\n    this.direction = PopoverDirection.rightWithTopAligned,\n    this.mutex,\n    this.windowPadding,\n    this.onOpen,\n    this.onClose,\n    this.canClose,\n    this.asBarrier = false,\n    this.clickHandler = PopoverClickHandler.listener,\n    this.skipTraversal = false,\n    this.animationDuration = const Duration(milliseconds: 200),\n    this.beginOpacity = 0.0,\n    this.endOpacity = 1.0,\n    this.beginScaleFactor = 1.0,\n    this.endScaleFactor = 1.0,\n    this.slideDistance = 5.0,\n    this.debugId,\n    this.maskDecoration = const BoxDecoration(\n      color: Color.fromARGB(0, 244, 67, 54),\n    ),\n    this.showAtCursor = false,\n  });\n\n  final PopoverController? controller;\n\n  /// The offset from the [child] where the popover will be drawn\n  final Offset? offset;\n\n  /// Amount of padding between the edges of the window and the popover\n  final EdgeInsets? windowPadding;\n\n  final Decoration? maskDecoration;\n\n  /// The function used to build the popover.\n  final Widget? Function(BuildContext context) popupBuilder;\n\n  /// Specify how the popover can be triggered when interacting with the child\n  /// by supplying a bitwise-OR combination of one or more [PopoverTriggerFlags]\n  final int triggerActions;\n\n  /// If multiple popovers are exclusive,\n  /// pass the same mutex to them.\n  final PopoverMutex? mutex;\n\n  /// The direction of the popover\n  final PopoverDirection direction;\n\n  final VoidCallback? onOpen;\n  final VoidCallback? onClose;\n  final Future<bool> Function()? canClose;\n\n  final bool asBarrier;\n\n  /// The widget that will be used to trigger the popover.\n  ///\n  /// Why do we need this?\n  /// Because if the parent widget of the popover is GestureDetector,\n  ///  the conflict won't be resolve by using Listener, we want these two gestures exclusive.\n  final PopoverClickHandler clickHandler;\n\n  final bool skipTraversal;\n\n  /// Animation time of the popover.\n  final Duration animationDuration;\n\n  /// The distance of the popover's slide animation.\n  final double slideDistance;\n\n  /// The scale factor of the popover's scale animation.\n  final double beginScaleFactor;\n  final double endScaleFactor;\n\n  /// The opacity of the popover's fade animation.\n  final double beginOpacity;\n  final double endOpacity;\n\n  final String? debugId;\n\n  /// Whether the popover should be shown at the cursor position.\n  ///\n  /// This only works when using [PopoverClickHandler.listener] as the click handler.\n  ///\n  /// Alternatively for having a normal popover, and use the cursor position only on\n  /// secondary click, consider showing the popover programatically with [PopoverController.showAt].\n  ///\n  final bool showAtCursor;\n\n  /// The content area of the popover.\n  final Widget child;\n\n  @override\n  State<Popover> createState() => PopoverState();\n}\n\nclass PopoverState extends State<Popover> with SingleTickerProviderStateMixin {\n  static final RootOverlayEntry rootEntry = RootOverlayEntry();\n\n  final PopoverLink popoverLink = PopoverLink();\n  late PopoverLayoutDelegate layoutDelegate = PopoverLayoutDelegate(\n    direction: widget.direction,\n    link: popoverLink,\n    offset: widget.offset ?? Offset.zero,\n    windowPadding: widget.windowPadding ?? EdgeInsets.zero,\n  );\n\n  late AnimationController animationController;\n  late Animation<double> fadeAnimation;\n  late Animation<double> scaleAnimation;\n  late Animation<Offset> slideAnimation;\n\n  // If the widget is disposed, prevent the animation from being called.\n  bool isDisposed = false;\n\n  Offset? cursorPosition;\n\n  @override\n  void initState() {\n    super.initState();\n\n    widget.controller?._state = this;\n    _buildAnimations();\n  }\n\n  @override\n  void deactivate() {\n    close(notify: false);\n\n    super.deactivate();\n  }\n\n  @override\n  void dispose() {\n    isDisposed = true;\n    animationController.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return PopoverTarget(\n      link: popoverLink,\n      child: _buildChild(context),\n    );\n  }\n\n  @override\n  void reassemble() {\n    // clear the overlay\n    while (rootEntry.isNotEmpty) {\n      rootEntry.popEntry();\n    }\n\n    super.reassemble();\n  }\n\n  void showOverlay([Offset? position]) {\n    close(withAnimation: true);\n\n    if (widget.mutex != null) {\n      widget.mutex?.state = this;\n    }\n\n    if (position != null) {\n      final RenderBox? renderBox = context.findRenderObject() as RenderBox?;\n      final offset = renderBox?.globalToLocal(position);\n      layoutDelegate = layoutDelegate.copyWith(\n        position: offset ?? position,\n        windowPadding: EdgeInsets.zero,\n        showAtCursor: true,\n      );\n    }\n\n    final shouldAddMask = rootEntry.isEmpty;\n    rootEntry.addEntry(\n      context,\n      widget.debugId ?? '',\n      this,\n      OverlayEntry(builder: (_) => _buildOverlayContent(shouldAddMask)),\n      widget.asBarrier,\n      animationController,\n    );\n\n    if (widget.animationDuration != Duration.zero) {\n      animationController.forward();\n    }\n  }\n\n  void close({\n    bool notify = true,\n    bool withAnimation = false,\n  }) {\n    if (rootEntry.contains(this)) {\n      void callback() {\n        rootEntry.removeEntry(this);\n        if (notify) {\n          widget.onClose?.call();\n        }\n      }\n\n      if (isDisposed ||\n          !withAnimation ||\n          widget.animationDuration == Duration.zero) {\n        callback();\n      } else {\n        animationController.reverse().then((_) => callback());\n      }\n    }\n  }\n\n  void _removeRootOverlay() {\n    rootEntry.popEntry();\n\n    if (widget.mutex?.state == this) {\n      widget.mutex?.removeState();\n    }\n  }\n\n  Widget _buildChild(BuildContext context) {\n    Widget child = widget.child;\n\n    if (widget.triggerActions == 0) {\n      return child;\n    }\n\n    child = _buildClickHandler(\n      child,\n      () {\n        widget.onOpen?.call();\n        if (widget.triggerActions & PopoverTriggerFlags.none != 0) {\n          return;\n        }\n\n        showOverlay(cursorPosition);\n      },\n    );\n\n    if (widget.triggerActions & PopoverTriggerFlags.hover != 0) {\n      child = MouseRegion(\n        onEnter: (event) => showOverlay(),\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  Widget _buildClickHandler(Widget child, VoidCallback handler) {\n    return switch (widget.clickHandler) {\n      PopoverClickHandler.listener => Listener(\n          onPointerDown: (event) {\n            cursorPosition = widget.showAtCursor ? event.position : null;\n\n            if (event.buttons == kSecondaryMouseButton &&\n                widget.triggerActions & PopoverTriggerFlags.secondaryClick !=\n                    0) {\n              return _callHandler(handler);\n            }\n\n            if (event.buttons == kPrimaryMouseButton &&\n                widget.triggerActions & PopoverTriggerFlags.click != 0) {\n              return _callHandler(handler);\n            }\n          },\n          child: child,\n        ),\n      PopoverClickHandler.gestureDetector => GestureDetector(\n          onTap: () {\n            if (widget.triggerActions & PopoverTriggerFlags.click != 0) {\n              return _callHandler(handler);\n            }\n          },\n          onSecondaryTap: () {\n            if (widget.triggerActions & PopoverTriggerFlags.secondaryClick !=\n                0) {\n              return _callHandler(handler);\n            }\n          },\n          child: child,\n        ),\n    };\n  }\n\n  void _callHandler(VoidCallback handler) {\n    if (rootEntry.contains(this)) {\n      close();\n    } else {\n      handler();\n    }\n  }\n\n  Widget _buildOverlayContent(bool shouldAddMask) {\n    return CallbackShortcuts(\n      bindings: {\n        const SingleActivator(LogicalKeyboardKey.escape): _removeRootOverlay,\n      },\n      child: FocusScope(\n        child: Stack(\n          children: [\n            if (shouldAddMask) _buildMask(),\n            _buildPopoverContainer(),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget _buildMask() {\n    return PopoverMask(\n      decoration: widget.maskDecoration,\n      onTap: () async {\n        if (await widget.canClose?.call() ?? true) {\n          _removeRootOverlay();\n        }\n      },\n    );\n  }\n\n  Widget _buildPopoverContainer() {\n    Widget child = PopoverContainer(\n      delegate: layoutDelegate,\n      popupBuilder: widget.popupBuilder,\n      skipTraversal: widget.skipTraversal,\n      onClose: close,\n      onCloseAll: _removeRootOverlay,\n    );\n\n    if (widget.animationDuration != Duration.zero) {\n      child = AnimatedBuilder(\n        animation: animationController,\n        builder: (_, child) => Opacity(\n          opacity: fadeAnimation.value,\n          child: Transform.scale(\n            scale: scaleAnimation.value,\n            child: Transform.translate(\n              offset: slideAnimation.value,\n              child: child,\n            ),\n          ),\n        ),\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  void _buildAnimations() {\n    animationController = AnimationController(\n      duration: widget.animationDuration,\n      vsync: this,\n    );\n    fadeAnimation = _buildFadeAnimation();\n    scaleAnimation = _buildScaleAnimation();\n    slideAnimation = _buildSlideAnimation();\n  }\n\n  Animation<double> _buildFadeAnimation() {\n    return Tween<double>(\n      begin: widget.beginOpacity,\n      end: widget.endOpacity,\n    ).animate(\n      CurvedAnimation(\n        parent: animationController,\n        curve: Curves.easeInOut,\n      ),\n    );\n  }\n\n  Animation<double> _buildScaleAnimation() {\n    return Tween<double>(\n      begin: widget.beginScaleFactor,\n      end: widget.endScaleFactor,\n    ).animate(\n      CurvedAnimation(\n        parent: animationController,\n        curve: Curves.easeInOut,\n      ),\n    );\n  }\n\n  Animation<Offset> _buildSlideAnimation() {\n    final values = _getSlideAnimationValues();\n    return Tween<Offset>(\n      begin: values.$1,\n      end: values.$2,\n    ).animate(\n      CurvedAnimation(\n        parent: animationController,\n        curve: Curves.linear,\n      ),\n    );\n  }\n\n  (Offset, Offset) _getSlideAnimationValues() {\n    final slideDistance = widget.slideDistance;\n\n    switch (widget.direction) {\n      case PopoverDirection.bottomWithLeftAligned:\n        return (\n          Offset(-slideDistance, -slideDistance),\n          Offset.zero,\n        );\n      case PopoverDirection.bottomWithCenterAligned:\n        return (\n          Offset(0, -slideDistance),\n          Offset.zero,\n        );\n      case PopoverDirection.bottomWithRightAligned:\n        return (\n          Offset(slideDistance, -slideDistance),\n          Offset.zero,\n        );\n      case PopoverDirection.topWithLeftAligned:\n        return (\n          Offset(-slideDistance, slideDistance),\n          Offset.zero,\n        );\n      case PopoverDirection.topWithCenterAligned:\n        return (\n          Offset(0, slideDistance),\n          Offset.zero,\n        );\n      case PopoverDirection.topWithRightAligned:\n        return (\n          Offset(slideDistance, slideDistance),\n          Offset.zero,\n        );\n      case PopoverDirection.leftWithTopAligned:\n      case PopoverDirection.leftWithCenterAligned:\n      case PopoverDirection.leftWithBottomAligned:\n        return (\n          Offset(slideDistance, 0),\n          Offset.zero,\n        );\n      case PopoverDirection.rightWithTopAligned:\n      case PopoverDirection.rightWithCenterAligned:\n      case PopoverDirection.rightWithBottomAligned:\n        return (\n          Offset(-slideDistance, 0),\n          Offset.zero,\n        );\n      default:\n        return (Offset.zero, Offset.zero);\n    }\n  }\n}\n\nclass PopoverContainer extends StatefulWidget {\n  const PopoverContainer({\n    super.key,\n    required this.popupBuilder,\n    required this.delegate,\n    required this.onClose,\n    required this.onCloseAll,\n    required this.skipTraversal,\n  });\n\n  final Widget? Function(BuildContext context) popupBuilder;\n  final void Function() onClose;\n  final void Function() onCloseAll;\n  final bool skipTraversal;\n  final PopoverLayoutDelegate delegate;\n\n  @override\n  State<StatefulWidget> createState() => PopoverContainerState();\n\n  static PopoverContainerState of(BuildContext context) {\n    if (context is StatefulElement && context.state is PopoverContainerState) {\n      return context.state as PopoverContainerState;\n    }\n    return context.findAncestorStateOfType<PopoverContainerState>()!;\n  }\n\n  static PopoverContainerState? maybeOf(BuildContext context) {\n    if (context is StatefulElement && context.state is PopoverContainerState) {\n      return context.state as PopoverContainerState;\n    }\n    return context.findAncestorStateOfType<PopoverContainerState>();\n  }\n}\n\nclass PopoverContainerState extends State<PopoverContainer> {\n  @override\n  Widget build(BuildContext context) {\n    return Focus(\n      autofocus: true,\n      skipTraversal: widget.skipTraversal,\n      child: CustomSingleChildLayout(\n        delegate: widget.delegate,\n        child: widget.popupBuilder(context),\n      ),\n    );\n  }\n\n  void close() => widget.onClose();\n\n  void closeAll() => widget.onCloseAll();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/pubspec.yaml",
    "content": "name: appflowy_popover\ndescription: A new Flutter package project.\nversion: 0.0.1\nhomepage: https://appflowy.io\n\nenvironment:\n  flutter: \">=3.22.0\"\n  sdk: \">=3.3.0 <4.0.0\"\n\ndependencies:\n  flutter:\n    sdk: flutter\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n  flutter_lints: ^4.0.0\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_popover/test/popover_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  test('adds one to input values', () {\n    // final calculator = Calculator();\n    // expect(calculator.addOne(2), 3);\n    // expect(calculator.addOne(-7), -6);\n    // expect(calculator.addOne(0), 1);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_result/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\nmigrate_working_dir/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.\n/pubspec.lock\n**/doc/api/\n.dart_tool/\nbuild/\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_result/.metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: \"bae5e49bc2a867403c43b2aae2de8f8c33b037e4\"\n  channel: \"[user-branch]\"\n\nproject_type: package\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_result/CHANGELOG.md",
    "content": "## 0.0.1\n\n* TODO: Describe initial release.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_result/LICENSE",
    "content": "TODO: Add your license here.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_result/README.md",
    "content": "<!--\nThis README describes the package. If you publish this package to pub.dev,\nthis README's contents appear on the landing page for your package.\n\nFor information about how to write a good package README, see the guide for\n[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).\n\nFor general information about developing packages, see the Dart guide for\n[creating packages](https://dart.dev/guides/libraries/create-library-packages)\nand the Flutter guide for\n[developing packages and plugins](https://flutter.dev/developing-packages).\n-->\n\nTODO: Put a short description of the package here that helps potential users\nknow whether this package might be useful for them.\n\n## Features\n\nTODO: List what your package can do. Maybe include images, gifs, or videos.\n\n## Getting started\n\nTODO: List prerequisites and provide or point to information on how to\nstart using the package.\n\n## Usage\n\nTODO: Include short and useful examples for package users. Add longer examples\nto `/example` folder.\n\n```dart\nconst like = 'sample';\n```\n\n## Additional information\n\nTODO: Tell users more about the package: where to find more information, how to\ncontribute to the package, how to file issues, what response they can expect\nfrom the package authors, and more.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_result/analysis_options.yaml",
    "content": "include: package:flutter_lints/flutter.yaml\n\n# Additional information about this file can be found at\n# https://dart.dev/guides/language/analysis-options\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_result/lib/appflowy_result.dart",
    "content": "/// AppFlowyPopover library\nlibrary;\n\nexport 'src/async_result.dart';\nexport 'src/result.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart",
    "content": "import 'package:appflowy_result/appflowy_result.dart';\n\ntypedef FlowyAsyncResult<S, F extends Object> = Future<FlowyResult<S, F>>;\n\nextension FlowyAsyncResultExtension<S, F extends Object>\n    on FlowyAsyncResult<S, F> {\n  Future<S> getOrElse(S Function(F f) onFailure) {\n    return then((result) => result.getOrElse(onFailure));\n  }\n\n  Future<S?> toNullable() {\n    return then((result) => result.toNullable());\n  }\n\n  Future<S> getOrThrow() {\n    return then((result) => result.getOrThrow());\n  }\n\n  Future<W> fold<W>(\n    W Function(S s) onSuccess,\n    W Function(F f) onFailure,\n  ) {\n    return then<W>((result) => result.fold(onSuccess, onFailure));\n  }\n\n  Future<bool> isError() {\n    return then((result) => result.isFailure);\n  }\n\n  Future<bool> isSuccess() {\n    return then((result) => result.isSuccess);\n  }\n\n  FlowyAsyncResult<S, F> onFailure(void Function(F failure) onFailure) {\n    return then((result) => result..onFailure(onFailure));\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart",
    "content": "abstract class FlowyResult<S, F extends Object> {\n  const FlowyResult();\n\n  factory FlowyResult.success(S s) => FlowySuccess(s);\n\n  factory FlowyResult.failure(F f) => FlowyFailure(f);\n\n  T fold<T>(T Function(S s) onSuccess, T Function(F f) onFailure);\n\n  FlowyResult<T, F> map<T>(T Function(S success) fn);\n  FlowyResult<S, T> mapError<T extends Object>(T Function(F failure) fn);\n\n  bool get isSuccess;\n  bool get isFailure;\n\n  S? toNullable();\n\n  T? onSuccess<T>(T? Function(S s) onSuccess);\n  T? onFailure<T>(T? Function(F f) onFailure);\n\n  S getOrElse(S Function(F failure) onFailure);\n  S getOrThrow();\n\n  F getFailure();\n}\n\nclass FlowySuccess<S, F extends Object> implements FlowyResult<S, F> {\n  final S _value;\n\n  FlowySuccess(this._value);\n\n  S get value => _value;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is FlowySuccess &&\n          runtimeType == other.runtimeType &&\n          _value == other._value;\n\n  @override\n  int get hashCode => _value.hashCode;\n\n  @override\n  String toString() => 'Success(value: $_value)';\n\n  @override\n  T fold<T>(T Function(S s) onSuccess, T Function(F e) onFailure) =>\n      onSuccess(_value);\n\n  @override\n  map<T>(T Function(S success) fn) {\n    return FlowySuccess(fn(_value));\n  }\n\n  @override\n  FlowyResult<S, T> mapError<T extends Object>(T Function(F error) fn) {\n    return FlowySuccess(_value);\n  }\n\n  @override\n  bool get isSuccess => true;\n\n  @override\n  bool get isFailure => false;\n\n  @override\n  S? toNullable() {\n    return _value;\n  }\n\n  @override\n  T? onSuccess<T>(T? Function(S success) onSuccess) {\n    return onSuccess(_value);\n  }\n\n  @override\n  T? onFailure<T>(T? Function(F failure) onFailure) {\n    return null;\n  }\n\n  @override\n  S getOrElse(S Function(F failure) onFailure) {\n    return _value;\n  }\n\n  @override\n  S getOrThrow() {\n    return _value;\n  }\n\n  @override\n  F getFailure() {\n    throw UnimplementedError();\n  }\n}\n\nclass FlowyFailure<S, F extends Object> implements FlowyResult<S, F> {\n  final F _value;\n\n  FlowyFailure(this._value);\n\n  F get error => _value;\n\n  @override\n  bool operator ==(Object other) =>\n      identical(this, other) ||\n      other is FlowyFailure &&\n          runtimeType == other.runtimeType &&\n          _value == other._value;\n\n  @override\n  int get hashCode => _value.hashCode;\n\n  @override\n  String toString() => 'Failure(error: $_value)';\n\n  @override\n  T fold<T>(T Function(S s) onSuccess, T Function(F e) onFailure) =>\n      onFailure(_value);\n\n  @override\n  map<T>(T Function(S success) fn) {\n    return FlowyFailure(_value);\n  }\n\n  @override\n  FlowyResult<S, T> mapError<T extends Object>(T Function(F error) fn) {\n    return FlowyFailure(fn(_value));\n  }\n\n  @override\n  bool get isSuccess => false;\n\n  @override\n  bool get isFailure => true;\n\n  @override\n  S? toNullable() {\n    return null;\n  }\n\n  @override\n  T? onSuccess<T>(T? Function(S success) onSuccess) {\n    return null;\n  }\n\n  @override\n  T? onFailure<T>(T? Function(F failure) onFailure) {\n    return onFailure(_value);\n  }\n\n  @override\n  S getOrElse(S Function(F failure) onFailure) {\n    return onFailure(_value);\n  }\n\n  @override\n  S getOrThrow() {\n    throw _value;\n  }\n\n  @override\n  F getFailure() {\n    return _value;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_result/pubspec.yaml",
    "content": "name: appflowy_result\ndescription: \"A new Flutter package project.\"\nversion: 0.0.1\nhomepage: https://github.com/appflowy-io/appflowy\n\nenvironment:\n  sdk: \">=3.3.0 <4.0.0\"\n  flutter: \">=1.17.0\"\n\ndev_dependencies:\n  flutter_lints: ^3.0.0\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\nmigrate_working_dir/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n.vscode/\n\n# Flutter/Dart/Pub related\n# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.\n/pubspec.lock\n**/doc/api/\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\nbuild/\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/.metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: \"d8a9f9a52e5af486f80d932e838ee93861ffd863\"\n  channel: \"[user-branch]\"\n\nproject_type: package\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/CHANGELOG.md",
    "content": "## 0.0.1\n\n* TODO: Describe initial release.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/LICENSE",
    "content": "TODO: Add your license here.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/README.md",
    "content": "# AppFlowy UI\n\nAppFlowy 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.\n\n## Features\n\n- **Design System Components**: Buttons, text fields, and more UI components that follow the AppFlowy design system\n- **Theming**: Consistent theming across all components with light and dark mode support\n\n## Installation\n\nAdd the following to your `pubspec.yaml` file:\n\n```yaml\ndependencies:\n  appflowy_ui: ^1.0.0\n```\n\n## Supported components\n\n- [x] Button\n- [x] TextField\n- [ ] Avatar\n- [ ] Checkbox\n- [ ] Grid\n- [ ] Link\n- [ ] Loading & Progress Indicator\n- [ ] Menu\n- [ ] Message Box\n- [ ] Navigation Bar\n- [ ] Popover\n- [ ] Scroll Bar\n- [ ] Tab Bar\n- [ ] Toggle\n- [ ] Tooltip\n\n## Reference\n\nFigma: https://www.figma.com/design/aphWa2OgkqyIragpatdk7a/Design-System\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/analysis_options.yaml",
    "content": "include: package:flutter_lints/flutter.yaml\n\nlinter:\n  rules:\n    - require_trailing_commas\n\n    - prefer_collection_literals\n    - prefer_final_fields\n    - prefer_final_in_for_each\n    - prefer_final_locals\n\n    - sized_box_for_whitespace\n    - use_decorated_box\n\n    - unnecessary_parenthesis\n    - unnecessary_await_in_return\n    - unnecessary_raw_strings\n\n    - avoid_unnecessary_containers\n    - avoid_redundant_argument_values\n    - avoid_unused_constructor_parameters\n\n    - always_declare_return_types\n\n    - sort_constructors_first\n    - unawaited_futures\n\nerrors:\n  invalid_annotation_target: ignore\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.build/\n.buildlog/\n.history\n.svn/\n.swiftpm/\nmigrate_working_dir/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.pub-cache/\n.pub/\n/build/\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Android Studio will place build artifacts here\n/android/app/debug\n/android/app/profile\n/android/app/release\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/.metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: \"d8a9f9a52e5af486f80d932e838ee93861ffd863\"\n  channel: \"[user-branch]\"\n\nproject_type: app\n\n# Tracks metadata for the flutter migrate command\nmigration:\n  platforms:\n    - platform: root\n      create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863\n      base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863\n    - platform: macos\n      create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863\n      base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863\n\n  # User provided section\n\n  # List of Local paths (relative to this file) that should be\n  # ignored by the migrate tool.\n  #\n  # Files that are not part of the templates will be ignored by default.\n  unmanaged_files:\n    - 'lib/main.dart'\n    - 'ios/Runner.xcodeproj/project.pbxproj'\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/README.md",
    "content": "# AppFlowy UI Example\n\nThis example demonstrates how to use the `appflowy_ui` package in a Flutter application.\n\n## Getting Started\n\nTo run this example:\n\n1. Ensure you have Flutter installed and set up on your machine\n2. Clone this repository\n3. Navigate to the example directory:\n   ```bash\n   cd example\n   ```\n4. Get the dependencies:\n   ```bash\n   flutter pub get\n   ```\n5. Run the example:\n   ```bash\n   flutter run\n   ```\n\n## Features Demonstrated\n\n- Basic app structure using AppFlowy UI components\n- Material 3 design integration\n- Responsive layout\n\n## Project Structure\n\n- `lib/main.dart`: The main application file\n- `pubspec.yaml`: Project dependencies and configuration\n\n## Additional Resources\n\nFor more information about the AppFlowy UI package, please refer to:\n\n- The main package documentation\n- [AppFlowy Website](https://appflowy.io)\n- [AppFlowy GitHub Repository](https://github.com/AppFlowy-IO/AppFlowy)\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/analysis_options.yaml",
    "content": "# This file configures the analyzer, which statically analyzes Dart code to\n# check for errors, warnings, and lints.\n#\n# The issues identified by the analyzer are surfaced in the UI of Dart-enabled\n# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be\n# invoked from the command line by running `flutter analyze`.\n\n# The following line activates a set of recommended lints for Flutter apps,\n# packages, and plugins designed to encourage good coding practices.\ninclude: package:flutter_lints/flutter.yaml\n\nlinter:\n  # The lint rules applied to this project can be customized in the\n  # section below to disable rules from the `package:flutter_lints/flutter.yaml`\n  # included above or to enable additional rules. A list of all available lints\n  # and their documentation is published at https://dart.dev/lints.\n  #\n  # Instead of disabling a lint rule for the entire project in the\n  # section below, it can also be suppressed for a single line of code\n  # or a specific dart file by using the `// ignore: name_of_lint` and\n  # `// ignore_for_file: name_of_lint` syntax on the line or in the file\n  # producing the lint.\n  rules:\n    # avoid_print: false  # Uncomment to disable the `avoid_print` rule\n    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule\n\n# Additional information about this file can be found at\n# https://dart.dev/guides/language/analysis-options\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport 'src/avatar/avatar_page.dart';\nimport 'src/buttons/buttons_page.dart';\nimport 'src/dropdown_menu/dropdown_menu_page.dart';\nimport 'src/menu/menu_page.dart';\nimport 'src/modal/modal_page.dart';\nimport 'src/textfield/textfield_page.dart';\n\nenum ThemeMode {\n  light,\n  dark,\n}\n\nfinal themeMode = ValueNotifier(ThemeMode.light);\n\nvoid main() {\n  runApp(\n    const MyApp(),\n  );\n}\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return ValueListenableBuilder(\n      valueListenable: themeMode,\n      builder: (context, themeMode, child) {\n        final themeBuilder = AppFlowyDefaultTheme();\n        final themeData =\n            themeMode == ThemeMode.light ? ThemeData.light() : ThemeData.dark();\n\n        return AnimatedAppFlowyTheme(\n          data: themeMode == ThemeMode.light\n              ? themeBuilder.light()\n              : themeBuilder.dark(),\n          child: MaterialApp(\n            debugShowCheckedModeBanner: false,\n            title: 'AppFlowy UI Example',\n            theme: themeData.copyWith(\n              visualDensity: VisualDensity.standard,\n            ),\n            home: const MyHomePage(\n              title: 'AppFlowy UI',\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nclass MyHomePage extends StatefulWidget {\n  const MyHomePage({\n    super.key,\n    required this.title,\n  });\n\n  final String title;\n\n  @override\n  State<MyHomePage> createState() => _MyHomePageState();\n}\n\nclass _MyHomePageState extends State<MyHomePage> {\n  final tabs = [\n    Tab(text: 'Button'),\n    Tab(text: 'TextField'),\n    Tab(text: 'Modal'),\n    Tab(text: 'Avatar'),\n    Tab(text: 'Menu'),\n    Tab(text: 'Dropdown Menu'),\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return DefaultTabController(\n      length: tabs.length,\n      child: Scaffold(\n        appBar: AppBar(\n          backgroundColor: Theme.of(context).colorScheme.inversePrimary,\n          title: Text(\n            widget.title,\n            style: theme.textStyle.title.enhanced(\n              color: theme.textColorScheme.primary,\n            ),\n          ),\n          actions: [\n            IconButton(\n              icon: Icon(\n                Theme.of(context).brightness == Brightness.light\n                    ? Icons.dark_mode\n                    : Icons.light_mode,\n              ),\n              onPressed: _toggleTheme,\n              tooltip: 'Toggle theme',\n            ),\n          ],\n        ),\n        body: TabBarView(\n          children: [\n            ButtonsPage(),\n            TextFieldPage(),\n            ModalPage(),\n            AvatarPage(),\n            MenuPage(),\n            DropdownMenuPage(),\n          ],\n        ),\n        bottomNavigationBar: TabBar(\n          tabs: tabs,\n        ),\n        floatingActionButton: null,\n      ),\n    );\n  }\n\n  void _toggleTheme() {\n    themeMode.value =\n        themeMode.value == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/avatar/avatar_page.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass AvatarPage extends StatelessWidget {\n  const AvatarPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleChildScrollView(\n      padding: const EdgeInsets.all(24),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          _sectionTitle('Avatar with Name (Initials)'),\n          Wrap(\n            spacing: 16,\n            children: [\n              AFAvatar(name: 'Lucas', size: AFAvatarSize.xs),\n              AFAvatar(name: 'Vivian', size: AFAvatarSize.s),\n              AFAvatar(name: 'John', size: AFAvatarSize.m),\n              AFAvatar(name: 'Cindy', size: AFAvatarSize.l),\n              AFAvatar(name: 'Alex', size: AFAvatarSize.xl),\n            ],\n          ),\n          const SizedBox(height: 32),\n          _sectionTitle('Avatar with Image URL'),\n          Wrap(\n            spacing: 16,\n            children: [\n              AFAvatar(\n                url: 'https://avatar.iran.liara.run/public/35',\n                size: AFAvatarSize.xs,\n              ),\n              AFAvatar(\n                url: 'https://avatar.iran.liara.run/public/36',\n                size: AFAvatarSize.s,\n              ),\n              AFAvatar(\n                url: 'https://avatar.iran.liara.run/public/37',\n                size: AFAvatarSize.m,\n              ),\n              AFAvatar(\n                url: 'https://avatar.iran.liara.run/public/38',\n                size: AFAvatarSize.l,\n              ),\n              AFAvatar(\n                url: 'https://avatar.iran.liara.run/public/39',\n                size: AFAvatarSize.xl,\n              ),\n            ],\n          ),\n          const SizedBox(height: 32),\n          _sectionTitle('Custom Colors'),\n          Wrap(\n            spacing: 16,\n            children: [\n              AFAvatar(\n                name: 'Nina',\n                size: AFAvatarSize.l,\n                backgroundColor: Colors.deepPurple,\n                textColor: Colors.white,\n              ),\n              AFAvatar(\n                name: 'Lucas Xu',\n                size: AFAvatarSize.l,\n                backgroundColor: Colors.amber,\n                textColor: Colors.black,\n              ),\n              AFAvatar(\n                name: 'A',\n                size: AFAvatarSize.l,\n                backgroundColor: Colors.green,\n                textColor: Colors.white,\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _sectionTitle(String text) => Padding(\n        padding: const EdgeInsets.only(bottom: 12),\n        child: Text(\n          text,\n          style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),\n        ),\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/buttons/buttons_page.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass ButtonsPage extends StatelessWidget {\n  const ButtonsPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleChildScrollView(\n      padding: const EdgeInsets.all(16.0),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          _buildSection(\n            'Filled Text Buttons',\n            [\n              AFFilledTextButton.primary(\n                text: 'Primary Button',\n                onTap: () {},\n              ),\n              const SizedBox(width: 16),\n              AFFilledTextButton.destructive(\n                text: 'Destructive Button',\n                onTap: () {},\n              ),\n              const SizedBox(width: 16),\n              AFFilledTextButton.disabled(\n                text: 'Disabled Button',\n              ),\n            ],\n          ),\n          const SizedBox(height: 32),\n          _buildSection(\n            'Filled Icon Text Buttons',\n            [\n              AFFilledButton.primary(\n                onTap: () {},\n                builder: (context, isHovering, disabled) => Row(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    Icon(\n                      Icons.add,\n                      size: 20,\n                      color: AppFlowyTheme.of(context).textColorScheme.onFill,\n                    ),\n                    const SizedBox(width: 8),\n                    Text(\n                      'Primary Button',\n                      style: TextStyle(\n                        color: AppFlowyTheme.of(context).textColorScheme.onFill,\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n              const SizedBox(width: 16),\n              AFFilledButton.destructive(\n                onTap: () {},\n                builder: (context, isHovering, disabled) => Row(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    Icon(\n                      Icons.delete,\n                      size: 20,\n                      color: AppFlowyTheme.of(context).textColorScheme.onFill,\n                    ),\n                    const SizedBox(width: 8),\n                    Text(\n                      'Destructive Button',\n                      style: TextStyle(\n                        color: AppFlowyTheme.of(context).textColorScheme.onFill,\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n              const SizedBox(width: 16),\n              AFFilledButton.disabled(\n                builder: (context, isHovering, disabled) => Row(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    Icon(\n                      Icons.block,\n                      size: 20,\n                      color: AppFlowyTheme.of(context).textColorScheme.tertiary,\n                    ),\n                    const SizedBox(width: 8),\n                    Text(\n                      'Disabled Button',\n                      style: TextStyle(\n                        color:\n                            AppFlowyTheme.of(context).textColorScheme.tertiary,\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ],\n          ),\n          const SizedBox(height: 32),\n          _buildSection(\n            'Outlined Text Buttons',\n            [\n              AFOutlinedTextButton.normal(\n                text: 'Normal Button',\n                onTap: () {},\n              ),\n              const SizedBox(width: 16),\n              AFOutlinedTextButton.destructive(\n                text: 'Destructive Button',\n                onTap: () {},\n              ),\n              const SizedBox(width: 16),\n              AFOutlinedTextButton.disabled(\n                text: 'Disabled Button',\n              ),\n            ],\n          ),\n          const SizedBox(height: 32),\n          _buildSection(\n            'Outlined Icon Text Buttons',\n            [\n              AFOutlinedButton.normal(\n                onTap: () {},\n                builder: (context, isHovering, disabled) => Row(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    Icon(\n                      Icons.add,\n                      size: 20,\n                      color: AppFlowyTheme.of(context).textColorScheme.primary,\n                    ),\n                    const SizedBox(width: 8),\n                    Text(\n                      'Normal Button',\n                      style: TextStyle(\n                        color:\n                            AppFlowyTheme.of(context).textColorScheme.primary,\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n              const SizedBox(width: 16),\n              AFOutlinedButton.destructive(\n                onTap: () {},\n                builder: (context, isHovering, disabled) => Row(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    Icon(\n                      Icons.delete,\n                      size: 20,\n                      color: AppFlowyTheme.of(context).textColorScheme.error,\n                    ),\n                    const SizedBox(width: 8),\n                    Text(\n                      'Destructive Button',\n                      style: TextStyle(\n                        color: AppFlowyTheme.of(context).textColorScheme.error,\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n              const SizedBox(width: 16),\n              AFOutlinedButton.disabled(\n                builder: (context, isHovering, disabled) => Row(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    Icon(\n                      Icons.block,\n                      size: 20,\n                      color: AppFlowyTheme.of(context).textColorScheme.tertiary,\n                    ),\n                    const SizedBox(width: 8),\n                    Text(\n                      'Disabled Button',\n                      style: TextStyle(\n                        color:\n                            AppFlowyTheme.of(context).textColorScheme.tertiary,\n                      ),\n                    ),\n                  ],\n                ),\n              ),\n            ],\n          ),\n          const SizedBox(height: 32),\n          _buildSection(\n            'Ghost Buttons',\n            [\n              AFGhostTextButton.primary(\n                text: 'Primary Button',\n                onTap: () {},\n              ),\n              const SizedBox(width: 16),\n              AFGhostTextButton.disabled(\n                text: 'Disabled Button',\n              ),\n            ],\n          ),\n          const SizedBox(height: 32),\n          _buildSection(\n            'Button with alignment',\n            [\n              SizedBox(\n                width: 200,\n                child: AFFilledTextButton.primary(\n                  text: 'Left Button',\n                  onTap: () {},\n                  alignment: Alignment.centerLeft,\n                ),\n              ),\n              const SizedBox(width: 16),\n              SizedBox(\n                width: 200,\n                child: AFFilledTextButton.primary(\n                  text: 'Center Button',\n                  onTap: () {},\n                  alignment: Alignment.center,\n                ),\n              ),\n              const SizedBox(width: 16),\n              SizedBox(\n                width: 200,\n                child: AFFilledTextButton.primary(\n                  text: 'Right Button',\n                  onTap: () {},\n                  alignment: Alignment.centerRight,\n                ),\n              ),\n            ],\n          ),\n          const SizedBox(height: 32),\n          _buildSection(\n            'Button Sizes',\n            [\n              AFFilledTextButton.primary(\n                text: 'Small Button',\n                onTap: () {},\n                size: AFButtonSize.s,\n              ),\n              const SizedBox(width: 16),\n              AFFilledTextButton.primary(\n                text: 'Medium Button',\n                onTap: () {},\n              ),\n              const SizedBox(width: 16),\n              AFFilledTextButton.primary(\n                text: 'Large Button',\n                onTap: () {},\n                size: AFButtonSize.l,\n              ),\n              const SizedBox(width: 16),\n              AFFilledTextButton.primary(\n                text: 'Extra Large Button',\n                onTap: () {},\n                size: AFButtonSize.xl,\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildSection(String title, List<Widget> children) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Text(\n          title,\n          style: const TextStyle(\n            fontSize: 20,\n            fontWeight: FontWeight.bold,\n          ),\n        ),\n        const SizedBox(height: 16),\n        Wrap(\n          spacing: 8,\n          runSpacing: 8,\n          children: children,\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/dropdown_menu/dropdown_menu_page.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass AFDropDownMenuItem with AFDropDownMenuMixin {\n  const AFDropDownMenuItem({\n    required this.label,\n  });\n\n  @override\n  final String label;\n}\n\nclass DropdownMenuPage extends StatefulWidget {\n  const DropdownMenuPage({super.key});\n\n  @override\n  State<DropdownMenuPage> createState() => _DropdownMenuPageState();\n}\n\nclass _DropdownMenuPageState extends State<DropdownMenuPage> {\n  List<AFDropDownMenuItem> selectedItems = [];\n  bool isDisabled = false;\n  bool isMultiselect = false;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return Stack(\n      children: [\n        Positioned(\n          left: theme.spacing.xxl,\n          top: theme.spacing.xxl,\n          child: Container(\n            padding: EdgeInsets.all(\n              theme.spacing.m,\n            ),\n            decoration: BoxDecoration(\n              color: theme.backgroundColorScheme.primary,\n              borderRadius: BorderRadius.circular(theme.borderRadius.m),\n              boxShadow: theme.shadow.medium,\n            ),\n            child: Column(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                _buildOption(\n                  'is disabled',\n                  isDisabled,\n                  (value) {\n                    setState(() => isDisabled = value);\n                  },\n                ),\n                // _buildOption(\n                //   'multiselect',\n                //   isMultiselect,\n                //   (value) {\n                //     setState(() => isMultiselect = value);\n                //   },\n                // ),\n              ],\n            ),\n          ),\n        ),\n        Center(\n          child: SizedBox(\n            width: 240,\n            child: AFDropDownMenu(\n              items: items,\n              selectedItems: selectedItems,\n              isDisabled: isDisabled,\n              // isMultiselect: isMultiselect,\n              onSelected: (value) {\n                if (value != null) {\n                  setState(() {\n                    if (isMultiselect) {\n                      if (selectedItems.contains(value)) {\n                        selectedItems.remove(value);\n                      } else {\n                        selectedItems.add(value);\n                      }\n                    } else {\n                      selectedItems\n                        ..clear()\n                        ..add(value);\n                    }\n                  });\n                }\n              },\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildOption(\n    String label,\n    bool value,\n    void Function(bool) onChanged,\n  ) {\n    return Row(\n      children: [\n        SizedBox(\n          width: 200,\n          child: Text(label),\n        ),\n        Switch(\n          value: value,\n          onChanged: onChanged,\n        ),\n      ],\n    );\n  }\n\n  static const items = [\n    AFDropDownMenuItem(label: 'Item 1'),\n    AFDropDownMenuItem(label: 'Item 2'),\n    AFDropDownMenuItem(label: 'Item 3'),\n    AFDropDownMenuItem(label: 'Item 4'),\n    AFDropDownMenuItem(label: 'Item 5'),\n  ];\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/menu/menu_page.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_animate/flutter_animate.dart';\nimport 'package:flutter_svg/svg.dart';\n\n/// A showcase page for the AFMenu, AFMenuSection, and AFTextMenuItem components.\nclass MenuPage extends StatefulWidget {\n  const MenuPage({super.key});\n\n  @override\n  State<MenuPage> createState() => _MenuPageState();\n}\n\nclass _MenuPageState extends State<MenuPage> {\n  final popoverController = AFPopoverController();\n\n  @override\n  void dispose() {\n    popoverController.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final leading = SvgPicture.asset(\n      'assets/images/vector.svg',\n      colorFilter: ColorFilter.mode(\n        theme.textColorScheme.primary,\n        BlendMode.srcIn,\n      ),\n    );\n    final logo = Container(\n      decoration: BoxDecoration(\n        borderRadius: BorderRadius.circular(theme.borderRadius.m),\n        border: Border.all(\n          color: theme.borderColorScheme.primary,\n        ),\n      ),\n      padding: EdgeInsets.all(theme.spacing.xs),\n      child: const FlutterLogo(size: 18),\n    );\n    final arrowRight = SvgPicture.asset(\n      'assets/images/arrow_right.svg',\n      width: 20,\n      height: 20,\n    );\n    final animationDuration = const Duration(milliseconds: 120);\n\n    return Center(\n      child: SingleChildScrollView(\n        child: Wrap(\n          crossAxisAlignment: WrapCrossAlignment.start,\n          runAlignment: WrapAlignment.start,\n          runSpacing: 16,\n          spacing: 16,\n          children: [\n            AFMenu(\n              width: 240,\n              children: [\n                AFMenuSection(\n                  title: 'Section 1',\n                  children: [\n                    AFTextMenuItem(\n                      leading: leading,\n                      title: 'Menu Item 1',\n                      selected: true,\n                      onTap: () {},\n                    ),\n                    AFPopover(\n                      controller: popoverController,\n                      shadows: theme.shadow.medium,\n                      anchor: const AFAnchor(\n                        offset: Offset(0, -20),\n                        overlayAlignment: Alignment.centerRight,\n                      ),\n                      effects: [\n                        FadeEffect(duration: animationDuration),\n                        ScaleEffect(\n                          duration: animationDuration,\n                          begin: Offset(.95, .95),\n                          end: Offset(1, 1),\n                        ),\n                        MoveEffect(\n                          duration: animationDuration,\n                          begin: Offset(-10, 0),\n                          end: Offset(0, 0),\n                        ),\n                      ],\n                      popover: (context) {\n                        return AFMenu(\n                          children: [\n                            AFTextMenuItem(\n                              leading: leading,\n                              title: 'Menu Item 2-1',\n                              onTap: () {},\n                            ),\n                            AFTextMenuItem(\n                              leading: leading,\n                              title: 'Menu Item 2-2',\n                              onTap: () {},\n                            ),\n                            AFTextMenuItem(\n                              leading: leading,\n                              title: 'Menu Item 2-3',\n                              onTap: () {},\n                            ),\n                          ],\n                        );\n                      },\n                      child: AFTextMenuItem(\n                        leading: leading,\n                        title: 'Menu Item 2',\n                        onTap: () {\n                          popoverController.toggle();\n                        },\n                      ),\n                    ),\n                    AFTextMenuItem(\n                      leading: leading,\n                      title: 'Menu Item 3',\n                      onTap: () {},\n                    ),\n                  ],\n                ),\n                AFMenuSection(\n                  title: 'Section 2',\n                  children: [\n                    AFTextMenuItem(\n                      leading: logo,\n                      title: 'Menu Item 4',\n                      subtitle: 'Menu Item',\n                      trailing: const Icon(\n                        Icons.check,\n                        size: 18,\n                        color: Colors.blueAccent,\n                      ),\n                      onTap: () {},\n                    ),\n                    AFTextMenuItem(\n                      leading: logo,\n                      title: 'Menu Item 5',\n                      subtitle: 'Menu Item',\n                      onTap: () {},\n                    ),\n                    AFTextMenuItem(\n                      leading: logo,\n                      title: 'Menu Item 6',\n                      subtitle: 'Menu Item',\n                      onTap: () {},\n                    ),\n                  ],\n                ),\n                AFMenuSection(\n                  title: 'Section 3',\n                  children: [\n                    AFTextMenuItem(\n                      leading: leading,\n                      title: 'Menu Item 7',\n                      trailing: arrowRight,\n                      onTap: () {},\n                    ),\n                    AFTextMenuItem(\n                      leading: leading,\n                      title: 'Menu Item 8',\n                      trailing: arrowRight,\n                      onTap: () {},\n                    ),\n                  ],\n                ),\n              ],\n            ),\n            const SizedBox(height: 32),\n            // Example: Menu with search bar\n            AFMenu(\n              width: 240,\n              children: [\n                AFTextMenuItem(\n                  leading: leading,\n                  title: 'Menu Item 1',\n                  onTap: () {},\n                ),\n                AFTextMenuItem(\n                  leading: leading,\n                  title: 'Menu Item 2',\n                  onTap: () {},\n                ),\n                AFTextMenuItem(\n                  leading: leading,\n                  title: 'Menu Item 3',\n                  onTap: () {},\n                ),\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/modal/modal_page.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass ModalPage extends StatefulWidget {\n  const ModalPage({super.key});\n\n  @override\n  State<ModalPage> createState() => _ModalPageState();\n}\n\nclass _ModalPageState extends State<ModalPage> {\n  double width = AFModalDimension.M;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return Center(\n      child: Container(\n        constraints: BoxConstraints(maxWidth: 600),\n        padding: EdgeInsets.symmetric(horizontal: theme.spacing.xl),\n        child: Column(\n          spacing: theme.spacing.l,\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            Row(\n              spacing: theme.spacing.m,\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                AFGhostButton.normal(\n                  onTap: () => setState(() => width = AFModalDimension.S),\n                  builder: (context, isHovering, disabled) {\n                    return Text(\n                      'S',\n                      style: TextStyle(\n                        color: width == AFModalDimension.S\n                            ? theme.textColorScheme.action\n                            : theme.textColorScheme.primary,\n                      ),\n                    );\n                  },\n                ),\n                AFGhostButton.normal(\n                  onTap: () => setState(() => width = AFModalDimension.M),\n                  builder: (context, isHovering, disabled) {\n                    return Text(\n                      'M',\n                      style: TextStyle(\n                        color: width == AFModalDimension.M\n                            ? theme.textColorScheme.action\n                            : theme.textColorScheme.primary,\n                      ),\n                    );\n                  },\n                ),\n                AFGhostButton.normal(\n                  onTap: () => setState(() => width = AFModalDimension.L),\n                  builder: (context, isHovering, disabled) {\n                    return Text(\n                      'L',\n                      style: TextStyle(\n                        color: width == AFModalDimension.L\n                            ? theme.textColorScheme.action\n                            : theme.textColorScheme.primary,\n                      ),\n                    );\n                  },\n                ),\n              ],\n            ),\n            AFFilledButton.primary(\n              builder: (context, isHovering, disabled) {\n                return Text(\n                  'Show Modal',\n                  style: TextStyle(\n                    color: AppFlowyTheme.of(context).textColorScheme.onFill,\n                  ),\n                );\n              },\n              onTap: () {\n                showDialog(\n                  context: context,\n                  barrierColor: theme.surfaceColorScheme.overlay,\n                  builder: (context) {\n                    final theme = AppFlowyTheme.of(context);\n\n                    return Center(\n                      child: AFModal(\n                          constraints: BoxConstraints(\n                            maxWidth: width,\n                            maxHeight: AFModalDimension.dialogHeight,\n                          ),\n                          child: Column(\n                            mainAxisSize: MainAxisSize.min,\n                            children: [\n                              AFModalHeader(\n                                leading: Text(\n                                  'Header',\n                                  style: theme.textStyle.heading4.prominent(\n                                    color: theme.textColorScheme.primary,\n                                  ),\n                                ),\n                                trailing: [\n                                  AFGhostButton.normal(\n                                    onTap: () => Navigator.of(context).pop(),\n                                    builder: (context, isHovering, disabled) {\n                                      return const Icon(Icons.close);\n                                    },\n                                  )\n                                ],\n                              ),\n                              Expanded(\n                                child: AFModalBody(\n                                  child: Text(\n                                      'A dialog briefly presents information or requests confirmation, allowing users to continue their workflow after interaction.'),\n                                ),\n                              ),\n                              AFModalFooter(\n                                trailing: [\n                                  AFOutlinedButton.normal(\n                                    onTap: () => Navigator.of(context).pop(),\n                                    builder: (context, isHovering, disabled) {\n                                      return const Text('Cancel');\n                                    },\n                                  ),\n                                  AFFilledButton.primary(\n                                    onTap: () => Navigator.of(context).pop(),\n                                    builder: (context, isHovering, disabled) {\n                                      return Text(\n                                        'Apply',\n                                        style: TextStyle(\n                                          color: AppFlowyTheme.of(context)\n                                              .textColorScheme\n                                              .onFill,\n                                        ),\n                                      );\n                                    },\n                                  ),\n                                ],\n                              )\n                            ],\n                          )),\n                    );\n                  },\n                );\n              },\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass TextFieldPage extends StatelessWidget {\n  const TextFieldPage({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleChildScrollView(\n      padding: const EdgeInsets.all(16.0),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          _buildSection(\n            'TextField Sizes',\n            [\n              AFTextField(\n                hintText: 'Please enter your name',\n                size: AFTextFieldSize.m,\n              ),\n              AFTextField(\n                hintText: 'Please enter your name',\n              ),\n            ],\n          ),\n          const SizedBox(height: 32),\n          _buildSection(\n            'TextField with hint text',\n            [\n              AFTextField(\n                hintText: 'Please enter your name',\n              ),\n            ],\n          ),\n          const SizedBox(height: 32),\n          _buildSection(\n            'TextField with initial text',\n            [\n              AFTextField(\n                initialText: 'https://appflowy.com',\n              ),\n            ],\n          ),\n          const SizedBox(height: 32),\n          _buildSection(\n            'TextField with validator ',\n            [\n              AFTextField(\n                validator: (controller) {\n                  if (controller.text.isEmpty) {\n                    return (true, 'This field is required');\n                  }\n\n                  final emailRegex =\n                      RegExp(r'^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$');\n                  if (!emailRegex.hasMatch(controller.text)) {\n                    return (true, 'Please enter a valid email address');\n                  }\n\n                  return (false, '');\n                },\n              ),\n            ],\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildSection(String title, List<Widget> children) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Text(\n          title,\n          style: const TextStyle(\n            fontSize: 20,\n            fontWeight: FontWeight.bold,\n          ),\n        ),\n        const SizedBox(height: 16),\n        Wrap(\n          spacing: 8,\n          runSpacing: 8,\n          children: children,\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/.gitignore",
    "content": "# Flutter-related\n**/Flutter/ephemeral/\n**/Pods/\n\n# Xcode-related\n**/dgph\n**/xcuserdata/\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Debug.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"\n#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Release.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"\n#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Podfile",
    "content": "platform :osx, '10.14'\n\n# CocoaPods analytics sends network stats synchronously affecting flutter build latency.\nENV['COCOAPODS_DISABLE_STATS'] = 'true'\n\nproject 'Runner', {\n  'Debug' => :debug,\n  'Profile' => :release,\n  'Release' => :release,\n}\n\ndef flutter_root\n  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)\n  unless File.exist?(generated_xcode_build_settings_path)\n    raise \"#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \\\"flutter pub get\\\" is executed first\"\n  end\n\n  File.foreach(generated_xcode_build_settings_path) do |line|\n    matches = line.match(/FLUTTER_ROOT\\=(.*)/)\n    return matches[1].strip if matches\n  end\n  raise \"FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \\\"flutter pub get\\\"\"\nend\n\nrequire File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)\n\nflutter_macos_podfile_setup\n\ntarget 'Runner' do\n  use_frameworks!\n  use_modular_headers!\n\n  flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))\n  target 'RunnerTests' do\n    inherit! :search_paths\n  end\nend\n\npost_install do |installer|\n  installer.pods_project.targets.each do |target|\n    flutter_additional_macos_build_settings(target)\n  end\nend\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/AppDelegate.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\n@main\nclass AppDelegate: FlutterAppDelegate {\n  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {\n    return true\n  }\n\n  override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {\n    return true\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_16.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_32.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"32x32\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_32.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"32x32\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_64.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"128x128\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_128.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"128x128\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_256.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"256x256\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_256.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"256x256\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_512.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"512x512\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_512.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"512x512\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_1024.png\",\n      \"scale\" : \"2x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Base.lproj/MainMenu.xib",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion=\"14490.70\" targetRuntime=\"MacOSX.Cocoa\" propertyAccessControl=\"none\" useAutolayout=\"YES\" customObjectInstantitationMethod=\"direct\">\n    <dependencies>\n        <deployment identifier=\"macosx\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.CocoaPlugin\" version=\"14490.70\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <objects>\n        <customObject id=\"-2\" userLabel=\"File's Owner\" customClass=\"NSApplication\">\n            <connections>\n                <outlet property=\"delegate\" destination=\"Voe-Tx-rLC\" id=\"GzC-gU-4Uq\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"-1\" userLabel=\"First Responder\" customClass=\"FirstResponder\"/>\n        <customObject id=\"-3\" userLabel=\"Application\" customClass=\"NSObject\"/>\n        <customObject id=\"Voe-Tx-rLC\" customClass=\"AppDelegate\" customModule=\"Runner\" customModuleProvider=\"target\">\n            <connections>\n                <outlet property=\"applicationMenu\" destination=\"uQy-DD-JDr\" id=\"XBo-yE-nKs\"/>\n                <outlet property=\"mainFlutterWindow\" destination=\"QvC-M9-y7g\" id=\"gIp-Ho-8D9\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"YLy-65-1bz\" customClass=\"NSFontManager\"/>\n        <menu title=\"Main Menu\" systemMenu=\"main\" id=\"AYu-sK-qS6\">\n            <items>\n                <menuItem title=\"APP_NAME\" id=\"1Xt-HY-uBw\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"APP_NAME\" systemMenu=\"apple\" id=\"uQy-DD-JDr\">\n                        <items>\n                            <menuItem title=\"About APP_NAME\" id=\"5kV-Vb-QxS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"orderFrontStandardAboutPanel:\" target=\"-1\" id=\"Exp-CZ-Vem\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"VOq-y0-SEH\"/>\n                            <menuItem title=\"Preferences…\" keyEquivalent=\",\" id=\"BOF-NM-1cW\"/>\n                            <menuItem isSeparatorItem=\"YES\" id=\"wFC-TO-SCJ\"/>\n                            <menuItem title=\"Services\" id=\"NMo-om-nkz\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Services\" systemMenu=\"services\" id=\"hz9-B4-Xy5\"/>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"4je-JR-u6R\"/>\n                            <menuItem title=\"Hide APP_NAME\" keyEquivalent=\"h\" id=\"Olw-nP-bQN\">\n                                <connections>\n                                    <action selector=\"hide:\" target=\"-1\" id=\"PnN-Uc-m68\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Hide Others\" keyEquivalent=\"h\" id=\"Vdr-fp-XzO\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"hideOtherApplications:\" target=\"-1\" id=\"VT4-aY-XCT\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Show All\" id=\"Kd2-mp-pUS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"unhideAllApplications:\" target=\"-1\" id=\"Dhg-Le-xox\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"kCx-OE-vgT\"/>\n                            <menuItem title=\"Quit APP_NAME\" keyEquivalent=\"q\" id=\"4sb-4s-VLi\">\n                                <connections>\n                                    <action selector=\"terminate:\" target=\"-1\" id=\"Te7-pn-YzF\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Edit\" id=\"5QF-Oa-p0T\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Edit\" id=\"W48-6f-4Dl\">\n                        <items>\n                            <menuItem title=\"Undo\" keyEquivalent=\"z\" id=\"dRJ-4n-Yzg\">\n                                <connections>\n                                    <action selector=\"undo:\" target=\"-1\" id=\"M6e-cu-g7V\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Redo\" keyEquivalent=\"Z\" id=\"6dh-zS-Vam\">\n                                <connections>\n                                    <action selector=\"redo:\" target=\"-1\" id=\"oIA-Rs-6OD\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"WRV-NI-Exz\"/>\n                            <menuItem title=\"Cut\" keyEquivalent=\"x\" id=\"uRl-iY-unG\">\n                                <connections>\n                                    <action selector=\"cut:\" target=\"-1\" id=\"YJe-68-I9s\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Copy\" keyEquivalent=\"c\" id=\"x3v-GG-iWU\">\n                                <connections>\n                                    <action selector=\"copy:\" target=\"-1\" id=\"G1f-GL-Joy\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Paste\" keyEquivalent=\"v\" id=\"gVA-U4-sdL\">\n                                <connections>\n                                    <action selector=\"paste:\" target=\"-1\" id=\"UvS-8e-Qdg\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Paste and Match Style\" keyEquivalent=\"V\" id=\"WeT-3V-zwk\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"pasteAsPlainText:\" target=\"-1\" id=\"cEh-KX-wJQ\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Delete\" id=\"pa3-QI-u2k\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"delete:\" target=\"-1\" id=\"0Mk-Ml-PaM\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Select All\" keyEquivalent=\"a\" id=\"Ruw-6m-B2m\">\n                                <connections>\n                                    <action selector=\"selectAll:\" target=\"-1\" id=\"VNm-Mi-diN\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"uyl-h8-XO2\"/>\n                            <menuItem title=\"Find\" id=\"4EN-yA-p0u\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Find\" id=\"1b7-l0-nxx\">\n                                    <items>\n                                        <menuItem title=\"Find…\" tag=\"1\" keyEquivalent=\"f\" id=\"Xz5-n4-O0W\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"cD7-Qs-BN4\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find and Replace…\" tag=\"12\" keyEquivalent=\"f\" id=\"YEy-JH-Tfz\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"WD3-Gg-5AJ\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find Next\" tag=\"2\" keyEquivalent=\"g\" id=\"q09-fT-Sye\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"NDo-RZ-v9R\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find Previous\" tag=\"3\" keyEquivalent=\"G\" id=\"OwM-mh-QMV\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"HOh-sY-3ay\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Use Selection for Find\" tag=\"7\" keyEquivalent=\"e\" id=\"buJ-ug-pKt\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"U76-nv-p5D\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Jump to Selection\" keyEquivalent=\"j\" id=\"S0p-oC-mLd\">\n                                            <connections>\n                                                <action selector=\"centerSelectionInVisibleArea:\" target=\"-1\" id=\"IOG-6D-g5B\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Spelling and Grammar\" id=\"Dv1-io-Yv7\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Spelling\" id=\"3IN-sU-3Bg\">\n                                    <items>\n                                        <menuItem title=\"Show Spelling and Grammar\" keyEquivalent=\":\" id=\"HFo-cy-zxI\">\n                                            <connections>\n                                                <action selector=\"showGuessPanel:\" target=\"-1\" id=\"vFj-Ks-hy3\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Check Document Now\" keyEquivalent=\";\" id=\"hz2-CU-CR7\">\n                                            <connections>\n                                                <action selector=\"checkSpelling:\" target=\"-1\" id=\"fz7-VC-reM\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"bNw-od-mp5\"/>\n                                        <menuItem title=\"Check Spelling While Typing\" id=\"rbD-Rh-wIN\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleContinuousSpellChecking:\" target=\"-1\" id=\"7w6-Qz-0kB\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Check Grammar With Spelling\" id=\"mK6-2p-4JG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleGrammarChecking:\" target=\"-1\" id=\"muD-Qn-j4w\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Correct Spelling Automatically\" id=\"78Y-hA-62v\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticSpellingCorrection:\" target=\"-1\" id=\"2lM-Qi-WAP\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Substitutions\" id=\"9ic-FL-obx\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Substitutions\" id=\"FeM-D8-WVr\">\n                                    <items>\n                                        <menuItem title=\"Show Substitutions\" id=\"z6F-FW-3nz\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"orderFrontSubstitutionsPanel:\" target=\"-1\" id=\"oku-mr-iSq\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"gPx-C9-uUO\"/>\n                                        <menuItem title=\"Smart Copy/Paste\" id=\"9yt-4B-nSM\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleSmartInsertDelete:\" target=\"-1\" id=\"3IJ-Se-DZD\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Quotes\" id=\"hQb-2v-fYv\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticQuoteSubstitution:\" target=\"-1\" id=\"ptq-xd-QOA\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Dashes\" id=\"rgM-f4-ycn\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticDashSubstitution:\" target=\"-1\" id=\"oCt-pO-9gS\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Links\" id=\"cwL-P1-jid\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticLinkDetection:\" target=\"-1\" id=\"Gip-E3-Fov\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Data Detectors\" id=\"tRr-pd-1PS\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticDataDetection:\" target=\"-1\" id=\"R1I-Nq-Kbl\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Text Replacement\" id=\"HFQ-gK-NFA\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticTextReplacement:\" target=\"-1\" id=\"DvP-Fe-Py6\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Transformations\" id=\"2oI-Rn-ZJC\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Transformations\" id=\"c8a-y6-VQd\">\n                                    <items>\n                                        <menuItem title=\"Make Upper Case\" id=\"vmV-6d-7jI\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"uppercaseWord:\" target=\"-1\" id=\"sPh-Tk-edu\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Make Lower Case\" id=\"d9M-CD-aMd\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"lowercaseWord:\" target=\"-1\" id=\"iUZ-b5-hil\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Capitalize\" id=\"UEZ-Bs-lqG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"capitalizeWord:\" target=\"-1\" id=\"26H-TL-nsh\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Speech\" id=\"xrE-MZ-jX0\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Speech\" id=\"3rS-ZA-NoH\">\n                                    <items>\n                                        <menuItem title=\"Start Speaking\" id=\"Ynk-f8-cLZ\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"startSpeaking:\" target=\"-1\" id=\"654-Ng-kyl\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Stop Speaking\" id=\"Oyz-dy-DGm\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"stopSpeaking:\" target=\"-1\" id=\"dX8-6p-jy9\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"View\" id=\"H8h-7b-M4v\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"View\" id=\"HyV-fh-RgO\">\n                        <items>\n                            <menuItem title=\"Enter Full Screen\" keyEquivalent=\"f\" id=\"4J7-dP-txa\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" control=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"toggleFullScreen:\" target=\"-1\" id=\"dU3-MA-1Rq\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Window\" id=\"aUF-d1-5bR\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Window\" systemMenu=\"window\" id=\"Td7-aD-5lo\">\n                        <items>\n                            <menuItem title=\"Minimize\" keyEquivalent=\"m\" id=\"OY7-WF-poV\">\n                                <connections>\n                                    <action selector=\"performMiniaturize:\" target=\"-1\" id=\"VwT-WD-YPe\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Zoom\" id=\"R4o-n2-Eq4\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"performZoom:\" target=\"-1\" id=\"DIl-cC-cCs\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"eu3-7i-yIM\"/>\n                            <menuItem title=\"Bring All to Front\" id=\"LE2-aR-0XJ\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"arrangeInFront:\" target=\"-1\" id=\"DRN-fu-gQh\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Help\" id=\"EPT-qC-fAb\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Help\" systemMenu=\"help\" id=\"rJ0-wn-3NY\"/>\n                </menuItem>\n            </items>\n            <point key=\"canvasLocation\" x=\"142\" y=\"-258\"/>\n        </menu>\n        <window title=\"APP_NAME\" allowsToolTipsWhenApplicationIsInactive=\"NO\" autorecalculatesKeyViewLoop=\"NO\" releasedWhenClosed=\"NO\" animationBehavior=\"default\" id=\"QvC-M9-y7g\" customClass=\"MainFlutterWindow\" customModule=\"Runner\" customModuleProvider=\"target\">\n            <windowStyleMask key=\"styleMask\" titled=\"YES\" closable=\"YES\" miniaturizable=\"YES\" resizable=\"YES\"/>\n            <rect key=\"contentRect\" x=\"335\" y=\"390\" width=\"800\" height=\"600\"/>\n            <rect key=\"screenRect\" x=\"0.0\" y=\"0.0\" width=\"2560\" height=\"1577\"/>\n            <view key=\"contentView\" wantsLayer=\"YES\" id=\"EiT-Mj-1SZ\">\n                <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"800\" height=\"600\"/>\n                <autoresizingMask key=\"autoresizingMask\"/>\n            </view>\n        </window>\n    </objects>\n</document>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/AppInfo.xcconfig",
    "content": "// Application-level settings for the Runner target.\n//\n// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the\n// future. If not, the values below would default to using the project name when this becomes a\n// 'flutter create' template.\n\n// The application's name. By default this is also the title of the Flutter window.\nPRODUCT_NAME = appflowy_ui_example\n\n// The application's bundle identifier\nPRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample\n\n// The copyright displayed in application information\nPRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Debug.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Debug.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Release.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Release.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Warnings.xcconfig",
    "content": "WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings\nGCC_WARN_UNDECLARED_SELECTOR = YES\nCLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES\nCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE\nCLANG_WARN__DUPLICATE_METHOD_MATCH = YES\nCLANG_WARN_PRAGMA_PACK = YES\nCLANG_WARN_STRICT_PROTOTYPES = YES\nCLANG_WARN_COMMA = YES\nGCC_WARN_STRICT_SELECTOR_MATCH = YES\nCLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES\nCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES\nGCC_WARN_SHADOW = YES\nCLANG_WARN_UNREACHABLE_CODE = YES\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/DebugProfile.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n\t<key>com.apple.security.cs.allow-jit</key>\n\t<true/>\n\t<key>com.apple.security.network.server</key>\n\t<true/>\n  <key>com.apple.security.network.client</key>\n  <true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIconFile</key>\n\t<string></string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSMinimumSystemVersion</key>\n\t<string>$(MACOSX_DEPLOYMENT_TARGET)</string>\n\t<key>NSHumanReadableCopyright</key>\n\t<string>$(PRODUCT_COPYRIGHT)</string>\n\t<key>NSMainNibFile</key>\n\t<string>MainMenu</string>\n\t<key>NSPrincipalClass</key>\n\t<string>NSApplication</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/MainFlutterWindow.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\nclass MainFlutterWindow: NSWindow {\n  override func awakeFromNib() {\n    let flutterViewController = FlutterViewController()\n    let windowFrame = self.frame\n    self.contentViewController = flutterViewController\n    self.setFrame(windowFrame, display: true)\n\n    RegisterGeneratedPlugins(registry: flutterViewController)\n\n    super.awakeFromNib()\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Release.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXAggregateTarget section */\n\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {\n\t\t\tisa = PBXAggregateTarget;\n\t\t\tbuildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t33CC111E2044C6BF0003C045 /* ShellScript */,\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = \"Flutter Assemble\";\n\t\t\tproductName = FLX;\n\t\t};\n/* End PBXAggregateTarget section */\n\n/* Begin PBXBuildFile section */\n\t\t331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };\n\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };\n\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };\n\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };\n\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };\n\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };\n\t\tA8240F6CBA460C4ECE77B497 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 67F505219B8EAD64EC2D7058 /* Pods_Runner.framework */; };\n\t\tED985151D7C5493A133E2F4A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 99B39EDC5EC15A319D26431B /* Pods_RunnerTests.framework */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 33CC10E52044A3C60003C045 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 33CC10EC2044A3C60003C045;\n\t\t\tremoteInfo = Runner;\n\t\t};\n\t\t33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 33CC10E52044A3C60003C045 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 33CC111A2044C6BA0003C045;\n\t\t\tremoteInfo = FLX;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t33CC110E2044A8840003C045 /* Bundle Framework */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Bundle Framework\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t126A5ABCAF9A616F39D0C99A /* 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 = \"<group>\"; };\n\t\t294E5A6C2D9F40258167A63C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-RunnerTests.release.xcconfig\"; path = \"Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = \"<group>\"; };\n\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = \"<group>\"; };\n\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = \"<group>\"; };\n\t\t33CC10ED2044A3C60003C045 /* appflowy_ui_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = appflowy_ui_example.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = \"<group>\"; };\n\t\t33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = \"<group>\"; };\n\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = \"<group>\"; };\n\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = \"Flutter-Generated.xcconfig\"; path = \"ephemeral/Flutter-Generated.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = \"<group>\"; };\n\t\t33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = \"<group>\"; };\n\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = \"<group>\"; };\n\t\t67F505219B8EAD64EC2D7058 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t7323BEF11E938E0621A2FE27 /* 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 = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\t99B39EDC5EC15A319D26431B /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tC27947A571D4D6E82250D9D4 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-RunnerTests.profile.xcconfig\"; path = \"Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig\"; sourceTree = \"<group>\"; };\n\t\tDECDE2B61D56E070AC9CFE43 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.profile.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig\"; sourceTree = \"<group>\"; };\n\t\tE8C30E6DE13EF821101E690E /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-RunnerTests.debug.xcconfig\"; path = \"Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig\"; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t331C80D2294CF70F00263BE5 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tED985151D7C5493A133E2F4A /* Pods_RunnerTests.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t33CC10EA2044A3C60003C045 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tA8240F6CBA460C4ECE77B497 /* Pods_Runner.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t331C80D6294CF71000263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t331C80D7294CF71000263BE5 /* RunnerTests.swift */,\n\t\t\t);\n\t\t\tpath = RunnerTests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33BA886A226E78AF003329D5 /* Configs */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */,\n\t\t\t);\n\t\t\tpath = Configs;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10E42044A3C60003C045 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33FAB671232836740065AC1E /* Runner */,\n\t\t\t\t33CEB47122A05771004F2AC0 /* Flutter */,\n\t\t\t\t331C80D6294CF71000263BE5 /* RunnerTests */,\n\t\t\t\t33CC10EE2044A3C60003C045 /* Products */,\n\t\t\t\tD73912EC22F37F3D000D13A0 /* Frameworks */,\n\t\t\t\tD0B4F9D1672BB1F3A8ADB1D5 /* Pods */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10EE2044A3C60003C045 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10ED2044A3C60003C045 /* appflowy_ui_example.app */,\n\t\t\t\t331C80D5294CF71000263BE5 /* RunnerTests.xctest */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC11242044D66E0003C045 /* Resources */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */,\n\t\t\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */,\n\t\t\t\t33CC10F72044A3C60003C045 /* Info.plist */,\n\t\t\t);\n\t\t\tname = Resources;\n\t\t\tpath = ..;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CEB47122A05771004F2AC0 /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,\n\t\t\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,\n\t\t\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,\n\t\t\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,\n\t\t\t);\n\t\t\tpath = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33FAB671232836740065AC1E /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */,\n\t\t\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,\n\t\t\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */,\n\t\t\t\t33E51914231749380026EE4D /* Release.entitlements */,\n\t\t\t\t33CC11242044D66E0003C045 /* Resources */,\n\t\t\t\t33BA886A226E78AF003329D5 /* Configs */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD0B4F9D1672BB1F3A8ADB1D5 /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t126A5ABCAF9A616F39D0C99A /* Pods-Runner.debug.xcconfig */,\n\t\t\t\t7323BEF11E938E0621A2FE27 /* Pods-Runner.release.xcconfig */,\n\t\t\t\tDECDE2B61D56E070AC9CFE43 /* Pods-Runner.profile.xcconfig */,\n\t\t\t\tE8C30E6DE13EF821101E690E /* Pods-RunnerTests.debug.xcconfig */,\n\t\t\t\t294E5A6C2D9F40258167A63C /* Pods-RunnerTests.release.xcconfig */,\n\t\t\t\tC27947A571D4D6E82250D9D4 /* Pods-RunnerTests.profile.xcconfig */,\n\t\t\t);\n\t\t\tname = Pods;\n\t\t\tpath = Pods;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD73912EC22F37F3D000D13A0 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t67F505219B8EAD64EC2D7058 /* Pods_Runner.framework */,\n\t\t\t\t99B39EDC5EC15A319D26431B /* Pods_RunnerTests.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t331C80D4294CF70F00263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t259F838CE5DFD9B02C9072E2 /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t331C80D1294CF70F00263BE5 /* Sources */,\n\t\t\t\t331C80D2294CF70F00263BE5 /* Frameworks */,\n\t\t\t\t331C80D3294CF70F00263BE5 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t331C80DA294CF71000263BE5 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = RunnerTests;\n\t\t\tproductName = RunnerTests;\n\t\t\tproductReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t33CC10EC2044A3C60003C045 /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tB8FE1B340A42EC42E5F2652D /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t33CC10E92044A3C60003C045 /* Sources */,\n\t\t\t\t33CC10EA2044A3C60003C045 /* Frameworks */,\n\t\t\t\t33CC10EB2044A3C60003C045 /* Resources */,\n\t\t\t\t33CC110E2044A8840003C045 /* Bundle Framework */,\n\t\t\t\t3399D490228B24CF009A79C7 /* ShellScript */,\n\t\t\t\t3CE63926704EF25F1635BDFC /* [CP] Embed Pods Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 33CC10ED2044A3C60003C045 /* appflowy_ui_example.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t33CC10E52044A3C60003C045 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = YES;\n\t\t\t\tLastSwiftUpdateCheck = 0920;\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t331C80D4294CF70F00263BE5 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.0;\n\t\t\t\t\t\tTestTargetID = 33CC10EC2044A3C60003C045;\n\t\t\t\t\t};\n\t\t\t\t\t33CC10EC2044A3C60003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t\tProvisioningStyle = Automatic;\n\t\t\t\t\t\tSystemCapabilities = {\n\t\t\t\t\t\t\tcom.apple.Sandbox = {\n\t\t\t\t\t\t\t\tenabled = 1;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t\t33CC111A2044C6BA0003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tProvisioningStyle = Manual;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 33CC10E42044A3C60003C045;\n\t\t\tproductRefGroup = 33CC10EE2044A3C60003C045 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t33CC10EC2044A3C60003C045 /* Runner */,\n\t\t\t\t331C80D4294CF70F00263BE5 /* RunnerTests */,\n\t\t\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t331C80D3294CF70F00263BE5 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t33CC10EB2044A3C60003C045 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,\n\t\t\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t259F838CE5DFD9B02C9072E2 /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t3399D490228B24CF009A79C7 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"echo \\\"$PRODUCT_NAME.app\\\" > \\\"$PROJECT_DIR\\\"/Flutter/ephemeral/.app_filename && \\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh embed\\n\";\n\t\t};\n\t\t33CC111E2044C6BF0003C045 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterInputs.xcfilelist,\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\tFlutter/ephemeral/tripwire,\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterOutputs.xcfilelist,\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire\";\n\t\t};\n\t\t3CE63926704EF25F1635BDFC /* [CP] Embed Pods Frameworks */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist\",\n\t\t\t);\n\t\t\tname = \"[CP] Embed Pods Frameworks\";\n\t\t\toutputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\tB8FE1B340A42EC42E5F2652D /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t331C80D1294CF70F00263BE5 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t33CC10E92044A3C60003C045 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,\n\t\t\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,\n\t\t\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 33CC10EC2044A3C60003C045 /* Runner */;\n\t\t\ttargetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;\n\t\t};\n\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;\n\t\t\ttargetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F52044A3C60003C045 /* Base */,\n\t\t\t);\n\t\t\tname = MainMenu.xib;\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t331C80DB294CF71000263BE5 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = E8C30E6DE13EF821101E690E /* Pods-RunnerTests.debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/appflowy_ui_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/appflowy_ui_example\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t331C80DC294CF71000263BE5 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 294E5A6C2D9F40258167A63C /* Pods-RunnerTests.release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/appflowy_ui_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/appflowy_ui_example\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t331C80DD294CF71000263BE5 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = C27947A571D4D6E82250D9D4 /* Pods-RunnerTests.profile.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/appflowy_ui_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/appflowy_ui_example\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CE9231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEA231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=macosx*]\" = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tDEVELOPMENT_TEAM = VHB67HRSZG;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEB231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t33CC10F92044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FA2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC10FC2044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=macosx*]\" = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tDEVELOPMENT_TEAM = VHB67HRSZG;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FD2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=macosx*]\" = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tDEVELOPMENT_TEAM = VHB67HRSZG;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC111C2044C6BA0003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC111D2044C6BA0003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t331C80DB294CF71000263BE5 /* Debug */,\n\t\t\t\t331C80DC294CF71000263BE5 /* Release */,\n\t\t\t\t331C80DD294CF71000263BE5 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10F92044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FA2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CE9231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10FC2044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FD2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CEA231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC111C2044C6BA0003C045 /* Debug */,\n\t\t\t\t33CC111D2044C6BA0003C045 /* Release */,\n\t\t\t\t338D0CEB231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 33CC10E52044A3C60003C045 /* Project object */;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1510\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n               BuildableName = \"appflowy_ui_example.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"appflowy_ui_example.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <Testables>\n         <TestableReference\n            skipped = \"NO\"\n            parallelizable = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"331C80D4294CF70F00263BE5\"\n               BuildableName = \"RunnerTests.xctest\"\n               BlueprintName = \"RunnerTests\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"appflowy_ui_example.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"appflowy_ui_example.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n   <FileRef\n      location = \"group:Pods/Pods.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict/>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/macos/RunnerTests/RunnerTests.swift",
    "content": "import Cocoa\nimport FlutterMacOS\nimport XCTest\n\nclass RunnerTests: XCTestCase {\n\n  func testExample() {\n    // If you add code to the Runner application, consider adding tests here.\n    // See https://developer.apple.com/documentation/xctest for more information about using XCTest.\n  }\n\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/pubspec.yaml",
    "content": "name: appflowy_ui_example\ndescription: \"Example app showcasing AppFlowy UI components and widgets\"\npublish_to: \"none\"\n\nversion: 1.0.0+1\n\nenvironment:\n  flutter: \">=3.27.4\"\n  sdk: \">=3.3.0 <4.0.0\"\n\ndependencies:\n  flutter:\n    sdk: flutter\n  appflowy_ui:\n    path: ../\n  cupertino_icons: ^1.0.6\n  flutter_svg: ^2.1.0\n  flutter_animate: ^4.5.2\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n  flutter_lints: ^5.0.0\n\nflutter:\n  uses-material-design: true\n\n  assets:\n    - assets/images/vector.svg\n    - assets/images/arrow_right.svg\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/example/test/widget_test.dart",
    "content": "// This is a basic Flutter widget test.\n//\n// To perform an interaction with a widget in your test, use the WidgetTester\n// utility in the flutter_test package. For example, you can send tap and scroll\n// gestures. You can also use WidgetTester to find child widgets in the widget\n// tree, read text, and verify that the values of widget properties are correct.\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'package:appflowy_ui_example/main.dart';\n\nvoid main() {\n  testWidgets('Counter increments smoke test', (WidgetTester tester) async {\n    // Build our app and trigger a frame.\n    await tester.pumpWidget(const MyApp());\n\n    // Verify that our counter starts at 0.\n    expect(find.text('0'), findsOneWidget);\n    expect(find.text('1'), findsNothing);\n\n    // Tap the '+' icon and trigger a frame.\n    await tester.tap(find.byIcon(Icons.add));\n    await tester.pump();\n\n    // Verify that our counter has incremented.\n    expect(find.text('0'), findsNothing);\n    expect(find.text('1'), findsOneWidget);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/appflowy_ui.dart",
    "content": "export 'src/component/component.dart';\nexport 'src/theme/data/appflowy_default/primitive.dart';\nexport 'src/theme/theme.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/avatar/avatar.dart",
    "content": "import 'package:appflowy_ui/src/theme/appflowy_theme.dart';\nimport 'package:appflowy_ui/src/theme/definition/theme_data.dart';\nimport 'package:cached_network_image/cached_network_image.dart';\nimport 'package:flutter/material.dart';\n\n/// Avatar sizes in pixels\nenum AFAvatarSize {\n  xs,\n  s,\n  m,\n  l,\n  xl;\n\n  double get size {\n    switch (this) {\n      case AFAvatarSize.xs:\n        return 16.0;\n      case AFAvatarSize.s:\n        return 24.0;\n      case AFAvatarSize.m:\n        return 32.0;\n      case AFAvatarSize.l:\n        return 48.0;\n      case AFAvatarSize.xl:\n        return 64.0;\n    }\n  }\n\n  TextStyle buildTextStyle(AppFlowyThemeData theme, Color color) {\n    switch (this) {\n      case AFAvatarSize.xs:\n        return theme.textStyle.caption.standard(color: color);\n      case AFAvatarSize.s:\n        return theme.textStyle.body.standard(color: color);\n      case AFAvatarSize.m:\n        return theme.textStyle.heading4.standard(color: color);\n      case AFAvatarSize.l:\n        return theme.textStyle.heading3.standard(color: color);\n      case AFAvatarSize.xl:\n        return theme.textStyle.heading2.standard(color: color);\n    }\n  }\n}\n\n/// Avatar widget\nclass AFAvatar extends StatelessWidget {\n  /// Displays an avatar. Precedence: [child] > [url] > [name].\n  ///\n  /// If [child] is provided, it is shown. Otherwise, if [url] is provided and non-empty, the image is shown. Otherwise, initials from [name] are shown.\n  const AFAvatar({\n    super.key,\n    this.name,\n    this.url,\n    this.size = AFAvatarSize.m,\n    this.textColor,\n    this.backgroundColor,\n    this.child,\n    this.colorHash,\n  });\n\n  /// The name of the avatar. Used for initials if [child] and [url] are not provided.\n  final String? name;\n\n  /// The URL of the avatar image. Used if [child] is not provided.\n  final String? url;\n\n  /// Custom widget to display as the avatar. Takes highest precedence.\n  final Widget? child;\n\n  /// The size of the avatar.\n  final AFAvatarSize size;\n\n  /// The text color for initials. Only applies when showing initials.\n  /// If not provided, a matching thick color from badge color scheme will be used.\n  final Color? textColor;\n\n  /// The background color for initials. Only applies when showing initials.\n  /// If not provided, a light color from badge color scheme will be used.\n  final Color? backgroundColor;\n\n  /// The hash value used to pick the color. If it's not provided, the name hash will be used.\n  final String? colorHash;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final double avatarSize = size.size;\n\n    // Pick color index based on name hash (1-20)\n    final int colorIndex = _pickColorIndexFromName(colorHash ?? name);\n    final Color backgroundColor =\n        this.backgroundColor ?? _getBadgeBackgroundColor(theme, colorIndex);\n    final Color textColor =\n        this.textColor ?? _getBadgeTextColor(theme, colorIndex);\n\n    final TextStyle textStyle = size.buildTextStyle(theme, textColor);\n\n    final Widget avatarContent = _buildAvatarContent(\n      avatarSize: avatarSize,\n      bgColor: backgroundColor,\n      textStyle: textStyle,\n    );\n\n    return SizedBox(\n      width: avatarSize,\n      height: avatarSize,\n      child: avatarContent,\n    );\n  }\n\n  Widget _buildAvatarContent({\n    required double avatarSize,\n    required Color bgColor,\n    required TextStyle textStyle,\n  }) {\n    if (child != null) {\n      return ClipOval(\n        child: SizedBox(\n          width: avatarSize,\n          height: avatarSize,\n          child: child,\n        ),\n      );\n    } else if (url != null && url!.isNotEmpty) {\n      return ClipOval(\n        child: CachedNetworkImage(\n          imageUrl: url!,\n          width: avatarSize,\n          height: avatarSize,\n          fit: BoxFit.cover,\n          // fallback to initials if the image is not found\n          errorWidget: (context, error, stackTrace) => _buildInitialsCircle(\n            avatarSize,\n            bgColor,\n            textStyle,\n          ),\n        ),\n      );\n    } else {\n      return _buildInitialsCircle(\n        avatarSize,\n        bgColor,\n        textStyle,\n      );\n    }\n  }\n\n  Widget _buildInitialsCircle(double size, Color bgColor, TextStyle textStyle) {\n    final initial = _getInitials(name);\n    return Container(\n      decoration: BoxDecoration(\n        color: bgColor,\n        shape: BoxShape.circle,\n      ),\n      alignment: Alignment.center,\n      child: Text(\n        initial,\n        style: textStyle,\n        textAlign: TextAlign.center,\n      ),\n    );\n  }\n\n  String _getInitials(String? name) {\n    if (name == null || name.trim().isEmpty) return '';\n\n    // Always return just the first letter of the name\n    return name.trim()[0].toUpperCase();\n  }\n\n  /// Deterministically pick a color index (1-20) based on the user name\n  int _pickColorIndexFromName(String? name) {\n    if (name == null || name.isEmpty) return 1;\n    int hash = 0;\n    for (int i = 0; i < name.length; i++) {\n      hash = name.codeUnitAt(i) + ((hash << 5) - hash);\n    }\n    return (hash.abs() % 20) + 1;\n  }\n\n  /// Gets the background color from badge color scheme using a list\n  Color _getBadgeBackgroundColor(AppFlowyThemeData theme, int colorIndex) {\n    final List<Color> backgroundColors = [\n      theme.badgeColorScheme.color1Light2,\n      theme.badgeColorScheme.color2Light2,\n      theme.badgeColorScheme.color3Light2,\n      theme.badgeColorScheme.color4Light2,\n      theme.badgeColorScheme.color5Light2,\n      theme.badgeColorScheme.color6Light2,\n      theme.badgeColorScheme.color7Light2,\n      theme.badgeColorScheme.color8Light2,\n      theme.badgeColorScheme.color9Light2,\n      theme.badgeColorScheme.color10Light2,\n      theme.badgeColorScheme.color11Light2,\n      theme.badgeColorScheme.color12Light2,\n      theme.badgeColorScheme.color13Light2,\n      theme.badgeColorScheme.color14Light2,\n      theme.badgeColorScheme.color15Light2,\n      theme.badgeColorScheme.color16Light2,\n      theme.badgeColorScheme.color17Light2,\n      theme.badgeColorScheme.color18Light2,\n      theme.badgeColorScheme.color19Light2,\n      theme.badgeColorScheme.color20Light2,\n    ];\n    return backgroundColors[(colorIndex - 1).clamp(0, 19)];\n  }\n\n  /// Gets the text color from badge color scheme using a list\n  Color _getBadgeTextColor(AppFlowyThemeData theme, int colorIndex) {\n    final List<Color> textColors = [\n      theme.badgeColorScheme.color1Thick3,\n      theme.badgeColorScheme.color2Thick3,\n      theme.badgeColorScheme.color3Thick3,\n      theme.badgeColorScheme.color4Thick3,\n      theme.badgeColorScheme.color5Thick3,\n      theme.badgeColorScheme.color6Thick3,\n      theme.badgeColorScheme.color7Thick3,\n      theme.badgeColorScheme.color8Thick3,\n      theme.badgeColorScheme.color9Thick3,\n      theme.badgeColorScheme.color10Thick3,\n      theme.badgeColorScheme.color11Thick3,\n      theme.badgeColorScheme.color12Thick3,\n      theme.badgeColorScheme.color13Thick3,\n      theme.badgeColorScheme.color14Thick3,\n      theme.badgeColorScheme.color15Thick3,\n      theme.badgeColorScheme.color16Thick3,\n      theme.badgeColorScheme.color17Thick3,\n      theme.badgeColorScheme.color18Thick3,\n      theme.badgeColorScheme.color19Thick3,\n      theme.badgeColorScheme.color20Thick3,\n    ];\n    return textColors[(colorIndex - 1).clamp(0, 19)];\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart",
    "content": "import 'package:appflowy_ui/src/theme/appflowy_theme.dart';\nimport 'package:flutter/widgets.dart';\n\nenum AFButtonSize {\n  s,\n  m,\n  l,\n  xl;\n\n  TextStyle buildTextStyle(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return switch (this) {\n      AFButtonSize.s => theme.textStyle.body.enhanced(),\n      AFButtonSize.m => theme.textStyle.body.enhanced(),\n      AFButtonSize.l => theme.textStyle.body.enhanced(),\n      AFButtonSize.xl => theme.textStyle.title.enhanced(),\n    };\n  }\n\n  EdgeInsetsGeometry buildPadding(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return switch (this) {\n      AFButtonSize.s => EdgeInsets.symmetric(\n          horizontal: theme.spacing.l,\n          vertical: theme.spacing.xs,\n        ),\n      AFButtonSize.m => EdgeInsets.symmetric(\n          horizontal: theme.spacing.xl,\n          vertical: theme.spacing.s,\n        ),\n      AFButtonSize.l => EdgeInsets.symmetric(\n          horizontal: theme.spacing.xl,\n          vertical: 10, // why?\n        ),\n      AFButtonSize.xl => EdgeInsets.symmetric(\n          horizontal: theme.spacing.xl,\n          vertical: 14, // why?\n        ),\n    };\n  }\n\n  double buildBorderRadius(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return switch (this) {\n      AFButtonSize.s => theme.borderRadius.m,\n      AFButtonSize.m => theme.borderRadius.m,\n      AFButtonSize.l => 10, // why?\n      AFButtonSize.xl => theme.borderRadius.xl,\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart",
    "content": "import 'package:appflowy_ui/src/theme/appflowy_theme.dart';\nimport 'package:flutter/material.dart';\n\ntypedef AFBaseButtonColorBuilder = Color Function(\n  BuildContext context,\n  bool isHovering,\n  bool disabled,\n);\n\ntypedef AFBaseButtonBorderColorBuilder = Color Function(\n  BuildContext context,\n  bool isHovering,\n  bool disabled,\n  bool isFocused,\n);\n\nclass AFBaseButton extends StatefulWidget {\n  const AFBaseButton({\n    super.key,\n    required this.onTap,\n    required this.builder,\n    required this.padding,\n    required this.borderRadius,\n    this.borderColor,\n    this.backgroundColor,\n    this.ringColor,\n    this.disabled = false,\n    this.autofocus = false,\n    this.showFocusRing = true,\n  });\n\n  final VoidCallback? onTap;\n\n  final AFBaseButtonBorderColorBuilder? borderColor;\n  final AFBaseButtonBorderColorBuilder? ringColor;\n  final AFBaseButtonColorBuilder? backgroundColor;\n\n  final EdgeInsetsGeometry padding;\n  final double borderRadius;\n  final bool disabled;\n  final bool autofocus;\n  final bool showFocusRing;\n\n  final Widget Function(\n    BuildContext context,\n    bool isHovering,\n    bool disabled,\n  ) builder;\n\n  @override\n  State<AFBaseButton> createState() => _AFBaseButtonState();\n}\n\nclass _AFBaseButtonState extends State<AFBaseButton> {\n  final FocusNode focusNode = FocusNode();\n\n  bool isHovering = false;\n  bool isFocused = false;\n\n  @override\n  void dispose() {\n    focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final Color borderColor = _buildBorderColor(context);\n    final Color backgroundColor = _buildBackgroundColor(context);\n    final Color ringColor = _buildRingColor(context);\n\n    return Actions(\n      actions: {\n        ActivateIntent: CallbackAction<ActivateIntent>(\n          onInvoke: (_) {\n            if (!widget.disabled) {\n              widget.onTap?.call();\n            }\n            return;\n          },\n        ),\n      },\n      child: Focus(\n        focusNode: focusNode,\n        onFocusChange: (isFocused) {\n          setState(() => this.isFocused = isFocused);\n        },\n        autofocus: widget.autofocus,\n        child: MouseRegion(\n          cursor: widget.onTap == null\n              ? SystemMouseCursors.basic\n              : widget.disabled\n                  ? SystemMouseCursors.basic\n                  : SystemMouseCursors.click,\n          onEnter: (_) => setState(() => isHovering = true),\n          onExit: (_) => setState(() => isHovering = false),\n          child: GestureDetector(\n            onTap: widget.disabled ? null : widget.onTap,\n            child: DecoratedBox(\n              decoration: BoxDecoration(\n                borderRadius: BorderRadius.circular(widget.borderRadius),\n                border: isFocused && widget.showFocusRing\n                    ? Border.all(\n                        color: ringColor,\n                        width: 2,\n                        strokeAlign: BorderSide.strokeAlignOutside,\n                      )\n                    : null,\n              ),\n              child: DecoratedBox(\n                decoration: BoxDecoration(\n                  color: backgroundColor,\n                  border: Border.all(color: borderColor),\n                  borderRadius: BorderRadius.circular(widget.borderRadius),\n                ),\n                child: Padding(\n                  padding: widget.padding,\n                  child: widget.builder(\n                    context,\n                    isHovering,\n                    widget.disabled,\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Color _buildBorderColor(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return widget.borderColor\n            ?.call(context, isHovering, widget.disabled, isFocused) ??\n        theme.borderColorScheme.primary;\n  }\n\n  Color _buildBackgroundColor(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return widget.backgroundColor?.call(context, isHovering, widget.disabled) ??\n        theme.fillColorScheme.content;\n  }\n\n  Color _buildRingColor(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    if (widget.ringColor != null) {\n      return widget.ringColor!\n          .call(context, isHovering, widget.disabled, isFocused);\n    }\n\n    if (isFocused) {\n      return theme.borderColorScheme.themeThick.withAlpha(128);\n    }\n\n    return Colors.transparent;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass AFBaseTextButton extends StatelessWidget {\n  const AFBaseTextButton({\n    super.key,\n    required this.text,\n    required this.onTap,\n    this.disabled = false,\n    this.size = AFButtonSize.m,\n    this.padding,\n    this.borderRadius,\n    this.textColor,\n    this.backgroundColor,\n    this.alignment,\n    this.textStyle,\n  });\n\n  /// The text of the button.\n  final String text;\n\n  /// Whether the button is disabled.\n  final bool disabled;\n\n  /// The callback when the button is tapped.\n  final VoidCallback onTap;\n\n  /// The size of the button.\n  final AFButtonSize size;\n\n  /// The padding of the button.\n  final EdgeInsetsGeometry? padding;\n\n  /// The border radius of the button.\n  final double? borderRadius;\n\n  /// The text color of the button.\n  final AFBaseButtonColorBuilder? textColor;\n\n  /// The background color of the button.\n  final AFBaseButtonColorBuilder? backgroundColor;\n\n  /// The alignment of the button.\n  ///\n  /// If it's null, the button size will be the size of the text with padding.\n  final Alignment? alignment;\n\n  /// The text style of the button.\n  final TextStyle? textStyle;\n\n  @override\n  Widget build(BuildContext context) {\n    throw UnimplementedError();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/button.dart",
    "content": "// Base button\nexport 'base_button/base.dart';\nexport 'base_button/base_button.dart';\nexport 'base_button/base_text_button.dart';\n// Filled buttons\nexport 'filled_button/filled_button.dart';\nexport 'filled_button/filled_icon_text_button.dart';\nexport 'filled_button/filled_text_button.dart';\n// Ghost buttons\nexport 'ghost_button/ghost_button.dart';\nexport 'ghost_button/ghost_icon_text_button.dart';\nexport 'ghost_button/ghost_text_button.dart';\n// Outlined buttons\nexport 'outlined_button/outlined_button.dart';\nexport 'outlined_button/outlined_icon_text_button.dart';\nexport 'outlined_button/outlined_text_button.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart",
    "content": "import 'package:appflowy_ui/src/component/component.dart';\nimport 'package:appflowy_ui/src/theme/appflowy_theme.dart';\nimport 'package:flutter/material.dart';\n\ntypedef AFFilledButtonWidgetBuilder = Widget Function(\n  BuildContext context,\n  bool isHovering,\n  bool disabled,\n);\n\nclass AFFilledButton extends StatelessWidget {\n  const AFFilledButton._({\n    super.key,\n    required this.builder,\n    required this.onTap,\n    required this.backgroundColor,\n    this.size = AFButtonSize.m,\n    this.padding,\n    this.borderRadius,\n    this.disabled = false,\n  });\n\n  /// Primary text button.\n  factory AFFilledButton.primary({\n    Key? key,\n    required AFFilledButtonWidgetBuilder builder,\n    required VoidCallback onTap,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    bool disabled = false,\n  }) {\n    return AFFilledButton._(\n      key: key,\n      builder: builder,\n      onTap: onTap,\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: disabled,\n      backgroundColor: (context, isHovering, disabled) {\n        if (disabled) {\n          return AppFlowyTheme.of(context).fillColorScheme.contentHover;\n        }\n        if (isHovering) {\n          return AppFlowyTheme.of(context).fillColorScheme.themeThickHover;\n        }\n        return AppFlowyTheme.of(context).fillColorScheme.themeThick;\n      },\n    );\n  }\n\n  /// Destructive text button.\n  factory AFFilledButton.destructive({\n    Key? key,\n    required AFFilledButtonWidgetBuilder builder,\n    required VoidCallback onTap,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    bool disabled = false,\n  }) {\n    return AFFilledButton._(\n      key: key,\n      builder: builder,\n      onTap: onTap,\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: disabled,\n      backgroundColor: (context, isHovering, disabled) {\n        if (disabled) {\n          return AppFlowyTheme.of(context).fillColorScheme.contentHover;\n        }\n        if (isHovering) {\n          return AppFlowyTheme.of(context).fillColorScheme.errorThickHover;\n        }\n        return AppFlowyTheme.of(context).fillColorScheme.errorThick;\n      },\n    );\n  }\n\n  /// Disabled text button.\n  factory AFFilledButton.disabled({\n    Key? key,\n    required AFFilledButtonWidgetBuilder builder,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    Color? backgroundColor,\n  }) {\n    return AFFilledButton._(\n      key: key,\n      builder: builder,\n      onTap: () {},\n      size: size,\n      disabled: true,\n      padding: padding,\n      borderRadius: borderRadius,\n      backgroundColor: (context, isHovering, disabled) =>\n          backgroundColor ??\n          AppFlowyTheme.of(context).fillColorScheme.contentHover,\n    );\n  }\n\n  final VoidCallback onTap;\n  final bool disabled;\n  final AFButtonSize size;\n  final EdgeInsetsGeometry? padding;\n  final double? borderRadius;\n\n  final AFBaseButtonColorBuilder? backgroundColor;\n  final AFFilledButtonWidgetBuilder builder;\n\n  @override\n  Widget build(BuildContext context) {\n    return AFBaseButton(\n      disabled: disabled,\n      backgroundColor: backgroundColor,\n      borderColor: (_, __, ___, ____) => Colors.transparent,\n      padding: padding ?? size.buildPadding(context),\n      borderRadius: borderRadius ?? size.buildBorderRadius(context),\n      onTap: onTap,\n      builder: builder,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_icon_text_button.dart",
    "content": "import 'package:appflowy_ui/src/component/component.dart';\nimport 'package:appflowy_ui/src/theme/appflowy_theme.dart';\nimport 'package:flutter/material.dart';\n\ntypedef AFFilledIconBuilder = Widget Function(\n  BuildContext context,\n  bool isHovering,\n  bool disabled,\n);\n\nclass AFFilledIconTextButton extends StatelessWidget {\n  const AFFilledIconTextButton._({\n    super.key,\n    required this.text,\n    required this.onTap,\n    required this.iconBuilder,\n    this.textColor,\n    this.backgroundColor,\n    this.size = AFButtonSize.m,\n    this.padding,\n    this.borderRadius,\n  });\n\n  /// Primary filled text button.\n  factory AFFilledIconTextButton.primary({\n    Key? key,\n    required String text,\n    required VoidCallback onTap,\n    required AFFilledIconBuilder iconBuilder,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n  }) {\n    return AFFilledIconTextButton._(\n      key: key,\n      text: text,\n      onTap: onTap,\n      iconBuilder: iconBuilder,\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      backgroundColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.fillColorScheme.tertiary;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.themeThickHover;\n        }\n        return theme.fillColorScheme.themeThick;\n      },\n      textColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        return theme.textColorScheme.onFill;\n      },\n    );\n  }\n\n  /// Destructive filled text button.\n  factory AFFilledIconTextButton.destructive({\n    Key? key,\n    required String text,\n    required VoidCallback onTap,\n    required AFFilledIconBuilder iconBuilder,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n  }) {\n    return AFFilledIconTextButton._(\n      key: key,\n      text: text,\n      iconBuilder: iconBuilder,\n      onTap: onTap,\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      backgroundColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.fillColorScheme.tertiary;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.errorThickHover;\n        }\n        return theme.fillColorScheme.errorThick;\n      },\n      textColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        return theme.textColorScheme.onFill;\n      },\n    );\n  }\n\n  /// Disabled filled text button.\n  factory AFFilledIconTextButton.disabled({\n    Key? key,\n    required String text,\n    required AFFilledIconBuilder iconBuilder,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n  }) {\n    return AFFilledIconTextButton._(\n      key: key,\n      text: text,\n      iconBuilder: iconBuilder,\n      onTap: () {},\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      backgroundColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        return theme.fillColorScheme.tertiary;\n      },\n      textColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        return theme.textColorScheme.onFill;\n      },\n    );\n  }\n\n  /// Ghost filled text button with transparent background that shows color on hover.\n  factory AFFilledIconTextButton.ghost({\n    Key? key,\n    required String text,\n    required VoidCallback onTap,\n    required AFFilledIconBuilder iconBuilder,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n  }) {\n    return AFFilledIconTextButton._(\n      key: key,\n      text: text,\n      iconBuilder: iconBuilder,\n      onTap: onTap,\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      backgroundColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return Colors.transparent;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.themeThickHover;\n        }\n        return Colors.transparent;\n      },\n      textColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.textColorScheme.tertiary;\n        }\n        return theme.textColorScheme.primary;\n      },\n    );\n  }\n\n  final String text;\n  final VoidCallback onTap;\n  final AFButtonSize size;\n  final EdgeInsetsGeometry? padding;\n  final double? borderRadius;\n\n  final AFFilledIconBuilder iconBuilder;\n\n  final AFBaseButtonColorBuilder? textColor;\n  final AFBaseButtonColorBuilder? backgroundColor;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return AFBaseButton(\n      backgroundColor: backgroundColor,\n      padding: padding ?? size.buildPadding(context),\n      borderRadius: borderRadius ?? size.buildBorderRadius(context),\n      onTap: onTap,\n      builder: (context, isHovering, disabled) {\n        final textColor = this.textColor?.call(context, isHovering, disabled) ??\n            theme.textColorScheme.onFill;\n        return Row(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            iconBuilder(context, isHovering, disabled),\n            SizedBox(width: theme.spacing.s),\n            Text(\n              text,\n              style: size.buildTextStyle(context).copyWith(\n                    color: textColor,\n                  ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart",
    "content": "import 'package:appflowy_ui/src/component/component.dart';\nimport 'package:appflowy_ui/src/theme/appflowy_theme.dart';\nimport 'package:flutter/material.dart';\n\nclass AFFilledTextButton extends AFBaseTextButton {\n  const AFFilledTextButton({\n    super.key,\n    required super.text,\n    required super.onTap,\n    required super.backgroundColor,\n    required super.textColor,\n    super.size = AFButtonSize.m,\n    super.padding,\n    super.borderRadius,\n    super.disabled = false,\n    super.alignment,\n    super.textStyle,\n  });\n\n  /// Primary text button.\n  factory AFFilledTextButton.primary({\n    Key? key,\n    required String text,\n    required VoidCallback onTap,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    bool disabled = false,\n    Alignment? alignment,\n    TextStyle? textStyle,\n  }) {\n    return AFFilledTextButton(\n      key: key,\n      text: text,\n      onTap: onTap,\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: disabled,\n      alignment: alignment,\n      textStyle: textStyle,\n      textColor: (context, isHovering, disabled) {\n        if (disabled) {\n          return AppFlowyTheme.of(context).textColorScheme.tertiary;\n        }\n        return AppFlowyTheme.of(context).textColorScheme.onFill;\n      },\n      backgroundColor: (context, isHovering, disabled) {\n        if (disabled) {\n          return AppFlowyTheme.of(context).fillColorScheme.contentHover;\n        }\n        if (isHovering) {\n          return AppFlowyTheme.of(context).fillColorScheme.themeThickHover;\n        }\n        return AppFlowyTheme.of(context).fillColorScheme.themeThick;\n      },\n    );\n  }\n\n  /// Destructive text button.\n  factory AFFilledTextButton.destructive({\n    Key? key,\n    required String text,\n    required VoidCallback onTap,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    bool disabled = false,\n    Alignment? alignment,\n    TextStyle? textStyle,\n  }) {\n    return AFFilledTextButton(\n      key: key,\n      text: text,\n      onTap: onTap,\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: disabled,\n      alignment: alignment,\n      textStyle: textStyle,\n      textColor: (context, isHovering, disabled) {\n        if (disabled) {\n          return AppFlowyTheme.of(context).textColorScheme.tertiary;\n        }\n        return AppFlowyTheme.of(context).textColorScheme.onFill;\n      },\n      backgroundColor: (context, isHovering, disabled) {\n        if (disabled) {\n          return AppFlowyTheme.of(context).fillColorScheme.contentHover;\n        }\n        if (isHovering) {\n          return AppFlowyTheme.of(context).fillColorScheme.errorThickHover;\n        }\n        return AppFlowyTheme.of(context).fillColorScheme.errorThick;\n      },\n    );\n  }\n\n  /// Disabled text button.\n  factory AFFilledTextButton.disabled({\n    Key? key,\n    required String text,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    Alignment? alignment,\n    TextStyle? textStyle,\n  }) {\n    return AFFilledTextButton(\n      key: key,\n      text: text,\n      onTap: () {},\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: true,\n      alignment: alignment,\n      textStyle: textStyle,\n      textColor: (context, isHovering, disabled) =>\n          AppFlowyTheme.of(context).textColorScheme.tertiary,\n      backgroundColor: (context, isHovering, disabled) =>\n          AppFlowyTheme.of(context).fillColorScheme.contentHover,\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ConstrainedBox(\n      constraints: BoxConstraints(\n        minWidth: 76,\n      ),\n      child: AFBaseButton(\n        disabled: disabled,\n        backgroundColor: backgroundColor,\n        borderColor: (_, __, ___, ____) => Colors.transparent,\n        padding: padding ?? size.buildPadding(context),\n        borderRadius: borderRadius ?? size.buildBorderRadius(context),\n        onTap: onTap,\n        builder: (context, isHovering, disabled) {\n          final textColor =\n              this.textColor?.call(context, isHovering, disabled) ??\n                  AppFlowyTheme.of(context).textColorScheme.onFill;\n          Widget child = Text(\n            text,\n            style: textStyle ??\n                size.buildTextStyle(context).copyWith(color: textColor),\n            textAlign: TextAlign.center,\n          );\n\n          final alignment = this.alignment;\n          if (alignment != null) {\n            child = Align(\n              alignment: alignment,\n              child: child,\n            );\n          }\n\n          return child;\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart",
    "content": "import 'package:appflowy_ui/src/component/component.dart';\nimport 'package:appflowy_ui/src/theme/appflowy_theme.dart';\nimport 'package:flutter/material.dart';\n\ntypedef AFGhostButtonWidgetBuilder = Widget Function(\n  BuildContext context,\n  bool isHovering,\n  bool disabled,\n);\n\nclass AFGhostButton extends StatelessWidget {\n  const AFGhostButton._({\n    super.key,\n    required this.onTap,\n    required this.backgroundColor,\n    required this.builder,\n    this.size = AFButtonSize.m,\n    this.padding,\n    this.borderRadius,\n    this.disabled = false,\n  });\n\n  /// Normal ghost button.\n  factory AFGhostButton.normal({\n    Key? key,\n    required VoidCallback onTap,\n    required AFGhostButtonWidgetBuilder builder,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    bool disabled = false,\n  }) {\n    return AFGhostButton._(\n      key: key,\n      builder: builder,\n      onTap: onTap,\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: disabled,\n      backgroundColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.fillColorScheme.content;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.contentHover;\n        }\n        return theme.fillColorScheme.content;\n      },\n    );\n  }\n\n  /// Disabled ghost button.\n  factory AFGhostButton.disabled({\n    Key? key,\n    required AFGhostButtonWidgetBuilder builder,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n  }) {\n    return AFGhostButton._(\n      key: key,\n      builder: builder,\n      onTap: () {},\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: true,\n      backgroundColor: (context, isHovering, disabled) =>\n          AppFlowyTheme.of(context).fillColorScheme.content,\n    );\n  }\n\n  final VoidCallback onTap;\n  final bool disabled;\n  final AFButtonSize size;\n  final EdgeInsetsGeometry? padding;\n  final double? borderRadius;\n\n  final AFBaseButtonColorBuilder? backgroundColor;\n  final AFGhostButtonWidgetBuilder builder;\n\n  @override\n  Widget build(BuildContext context) {\n    return AFBaseButton(\n      disabled: disabled,\n      backgroundColor: backgroundColor,\n      borderColor: (_, __, ___, ____) => Colors.transparent,\n      padding: padding ?? size.buildPadding(context),\n      borderRadius: borderRadius ?? size.buildBorderRadius(context),\n      onTap: onTap,\n      builder: builder,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart",
    "content": "import 'package:appflowy_ui/src/component/component.dart';\nimport 'package:appflowy_ui/src/theme/appflowy_theme.dart';\nimport 'package:flutter/material.dart';\n\ntypedef AFGhostIconBuilder = Widget Function(\n  BuildContext context,\n  bool isHovering,\n  bool disabled,\n);\n\nclass AFGhostIconTextButton extends StatelessWidget {\n  const AFGhostIconTextButton({\n    super.key,\n    required this.text,\n    required this.onTap,\n    required this.iconBuilder,\n    this.textColor,\n    this.backgroundColor,\n    this.size = AFButtonSize.m,\n    this.padding,\n    this.borderRadius,\n    this.disabled = false,\n    this.mainAxisAlignment = MainAxisAlignment.center,\n  });\n\n  /// Primary ghost text button.\n  factory AFGhostIconTextButton.primary({\n    Key? key,\n    required String text,\n    required VoidCallback onTap,\n    required AFGhostIconBuilder iconBuilder,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    bool disabled = false,\n    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.center,\n  }) {\n    return AFGhostIconTextButton(\n      key: key,\n      text: text,\n      onTap: onTap,\n      iconBuilder: iconBuilder,\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: disabled,\n      mainAxisAlignment: mainAxisAlignment,\n      backgroundColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return Colors.transparent;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.contentHover;\n        }\n        return Colors.transparent;\n      },\n      textColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.textColorScheme.tertiary;\n        }\n        return theme.textColorScheme.primary;\n      },\n    );\n  }\n\n  /// Disabled ghost text button.\n  factory AFGhostIconTextButton.disabled({\n    Key? key,\n    required String text,\n    required AFGhostIconBuilder iconBuilder,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.center,\n  }) {\n    return AFGhostIconTextButton(\n      key: key,\n      text: text,\n      iconBuilder: iconBuilder,\n      onTap: () {},\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: true,\n      mainAxisAlignment: mainAxisAlignment,\n      backgroundColor: (context, isHovering, disabled) {\n        return Colors.transparent;\n      },\n      textColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        return theme.textColorScheme.tertiary;\n      },\n    );\n  }\n\n  final String text;\n  final bool disabled;\n  final VoidCallback onTap;\n  final AFButtonSize size;\n  final EdgeInsetsGeometry? padding;\n  final double? borderRadius;\n\n  final AFGhostIconBuilder iconBuilder;\n\n  final AFBaseButtonColorBuilder? textColor;\n  final AFBaseButtonColorBuilder? backgroundColor;\n\n  final MainAxisAlignment mainAxisAlignment;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return AFBaseButton(\n      disabled: disabled,\n      backgroundColor: backgroundColor,\n      borderColor: (context, isHovering, disabled, isFocused) {\n        return Colors.transparent;\n      },\n      padding: padding ?? size.buildPadding(context),\n      borderRadius: borderRadius ?? size.buildBorderRadius(context),\n      onTap: onTap,\n      builder: (context, isHovering, disabled) {\n        final textColor = this.textColor?.call(context, isHovering, disabled) ??\n            theme.textColorScheme.primary;\n        return Row(\n          mainAxisAlignment: mainAxisAlignment,\n          children: [\n            iconBuilder(\n              context,\n              isHovering,\n              disabled,\n            ),\n            SizedBox(width: theme.spacing.m),\n            Text(\n              text,\n              style: size.buildTextStyle(context).copyWith(\n                    color: textColor,\n                  ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart",
    "content": "import 'package:appflowy_ui/src/component/component.dart';\nimport 'package:appflowy_ui/src/theme/appflowy_theme.dart';\nimport 'package:flutter/material.dart';\n\nclass AFGhostTextButton extends AFBaseTextButton {\n  const AFGhostTextButton({\n    super.key,\n    required super.text,\n    required super.onTap,\n    super.textColor,\n    super.backgroundColor,\n    super.size = AFButtonSize.m,\n    super.padding,\n    super.borderRadius,\n    super.disabled = false,\n    super.alignment,\n    super.textStyle,\n  });\n\n  /// Normal ghost text button.\n  factory AFGhostTextButton.primary({\n    Key? key,\n    required String text,\n    required VoidCallback onTap,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    bool disabled = false,\n    Alignment? alignment,\n    TextStyle? textStyle,\n  }) {\n    return AFGhostTextButton(\n      key: key,\n      text: text,\n      onTap: onTap,\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: disabled,\n      alignment: alignment,\n      textStyle: textStyle,\n      backgroundColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (isHovering) {\n          return theme.fillColorScheme.contentHover;\n        }\n        return theme.fillColorScheme.content;\n      },\n      textColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.textColorScheme.tertiary;\n        }\n        if (isHovering) {\n          return theme.textColorScheme.primary;\n        }\n        return theme.textColorScheme.primary;\n      },\n    );\n  }\n\n  /// Disabled ghost text button.\n  factory AFGhostTextButton.disabled({\n    Key? key,\n    required String text,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    Alignment? alignment,\n    TextStyle? textStyle,\n  }) {\n    return AFGhostTextButton(\n      key: key,\n      text: text,\n      onTap: () {},\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: true,\n      alignment: alignment,\n      textStyle: textStyle,\n      textColor: (context, isHovering, disabled) =>\n          AppFlowyTheme.of(context).textColorScheme.tertiary,\n      backgroundColor: (context, isHovering, disabled) =>\n          AppFlowyTheme.of(context).fillColorScheme.content,\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return ConstrainedBox(\n      constraints: BoxConstraints(\n        minWidth: 76,\n      ),\n      child: AFBaseButton(\n        disabled: disabled,\n        backgroundColor: backgroundColor,\n        borderColor: (_, __, ___, ____) => Colors.transparent,\n        padding: padding ?? size.buildPadding(context),\n        borderRadius: borderRadius ?? size.buildBorderRadius(context),\n        onTap: onTap,\n        builder: (context, isHovering, disabled) {\n          final textColor =\n              this.textColor?.call(context, isHovering, disabled) ??\n                  theme.textColorScheme.primary;\n\n          Widget child = Text(\n            text,\n            style: textStyle ??\n                size.buildTextStyle(context).copyWith(color: textColor),\n            textAlign: TextAlign.center,\n          );\n\n          final alignment = this.alignment;\n          if (alignment != null) {\n            child = Align(\n              alignment: alignment,\n              child: child,\n            );\n          }\n\n          return child;\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart",
    "content": "import 'package:appflowy_ui/src/component/component.dart';\nimport 'package:appflowy_ui/src/theme/appflowy_theme.dart';\nimport 'package:flutter/material.dart';\n\ntypedef AFOutlinedButtonWidgetBuilder = Widget Function(\n  BuildContext context,\n  bool isHovering,\n  bool disabled,\n);\n\nclass AFOutlinedButton extends StatelessWidget {\n  const AFOutlinedButton._({\n    super.key,\n    required this.onTap,\n    required this.builder,\n    this.borderColor,\n    this.backgroundColor,\n    this.size = AFButtonSize.m,\n    this.padding,\n    this.borderRadius,\n    this.disabled = false,\n  });\n\n  /// Normal outlined button.\n  factory AFOutlinedButton.normal({\n    Key? key,\n    required AFOutlinedButtonWidgetBuilder builder,\n    required VoidCallback onTap,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    bool disabled = false,\n  }) {\n    return AFOutlinedButton._(\n      key: key,\n      onTap: onTap,\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: disabled,\n      borderColor: (context, isHovering, disabled, isFocused) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.borderColorScheme.primary;\n        }\n        if (isHovering) {\n          return theme.borderColorScheme.primaryHover;\n        }\n        return theme.borderColorScheme.primary;\n      },\n      backgroundColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.fillColorScheme.content;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.contentHover;\n        }\n        return theme.fillColorScheme.content;\n      },\n      builder: builder,\n    );\n  }\n\n  /// Destructive outlined button.\n  factory AFOutlinedButton.destructive({\n    Key? key,\n    required AFOutlinedButtonWidgetBuilder builder,\n    required VoidCallback onTap,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    bool disabled = false,\n  }) {\n    return AFOutlinedButton._(\n      key: key,\n      onTap: onTap,\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: disabled,\n      borderColor: (context, isHovering, disabled, isFocused) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.fillColorScheme.errorThick;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.errorThickHover;\n        }\n        return theme.fillColorScheme.errorThick;\n      },\n      backgroundColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.fillColorScheme.errorThick;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.errorSelect;\n        }\n        return theme.fillColorScheme.content;\n      },\n      builder: builder,\n    );\n  }\n\n  /// Disabled outlined text button.\n  factory AFOutlinedButton.disabled({\n    Key? key,\n    required AFOutlinedButtonWidgetBuilder builder,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n  }) {\n    return AFOutlinedButton._(\n      key: key,\n      onTap: () {},\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: true,\n      borderColor: (context, isHovering, disabled, isFocused) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.borderColorScheme.primary;\n        }\n        if (isHovering) {\n          return theme.borderColorScheme.primaryHover;\n        }\n        return theme.borderColorScheme.primary;\n      },\n      backgroundColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.fillColorScheme.content;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.contentHover;\n        }\n        return theme.fillColorScheme.content;\n      },\n      builder: builder,\n    );\n  }\n\n  final VoidCallback onTap;\n  final bool disabled;\n  final AFButtonSize size;\n  final EdgeInsetsGeometry? padding;\n  final double? borderRadius;\n\n  final AFBaseButtonBorderColorBuilder? borderColor;\n  final AFBaseButtonColorBuilder? backgroundColor;\n\n  final AFOutlinedButtonWidgetBuilder builder;\n\n  @override\n  Widget build(BuildContext context) {\n    return AFBaseButton(\n      disabled: disabled,\n      backgroundColor: backgroundColor,\n      borderColor: borderColor,\n      padding: padding ?? size.buildPadding(context),\n      borderRadius: borderRadius ?? size.buildBorderRadius(context),\n      onTap: onTap,\n      builder: builder,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart",
    "content": "import 'package:appflowy_ui/src/component/component.dart';\nimport 'package:appflowy_ui/src/theme/appflowy_theme.dart';\nimport 'package:flutter/material.dart';\n\ntypedef AFOutlinedIconBuilder = Widget Function(\n  BuildContext context,\n  bool isHovering,\n  bool disabled,\n);\n\nclass AFOutlinedIconTextButton extends StatelessWidget {\n  const AFOutlinedIconTextButton._({\n    super.key,\n    required this.text,\n    required this.onTap,\n    required this.iconBuilder,\n    this.borderColor,\n    this.textColor,\n    this.backgroundColor,\n    this.size = AFButtonSize.m,\n    this.padding,\n    this.borderRadius,\n    this.disabled = false,\n    this.alignment = MainAxisAlignment.center,\n  });\n\n  /// Normal outlined text button.\n  factory AFOutlinedIconTextButton.normal({\n    Key? key,\n    required String text,\n    required VoidCallback onTap,\n    required AFOutlinedIconBuilder iconBuilder,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    bool disabled = false,\n    MainAxisAlignment alignment = MainAxisAlignment.center,\n  }) {\n    return AFOutlinedIconTextButton._(\n      key: key,\n      text: text,\n      onTap: onTap,\n      iconBuilder: iconBuilder,\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: disabled,\n      alignment: alignment,\n      borderColor: (context, isHovering, disabled, isFocused) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.borderColorScheme.primary;\n        }\n        if (isHovering) {\n          return theme.borderColorScheme.primaryHover;\n        }\n        return theme.borderColorScheme.primary;\n      },\n      backgroundColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.fillColorScheme.content;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.contentHover;\n        }\n        return theme.fillColorScheme.content;\n      },\n      textColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.textColorScheme.tertiary;\n        }\n        if (isHovering) {\n          return theme.textColorScheme.primary;\n        }\n        return theme.textColorScheme.primary;\n      },\n    );\n  }\n\n  /// Destructive outlined text button.\n  factory AFOutlinedIconTextButton.destructive({\n    Key? key,\n    required String text,\n    required VoidCallback onTap,\n    required AFOutlinedIconBuilder iconBuilder,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    bool disabled = false,\n    MainAxisAlignment alignment = MainAxisAlignment.center,\n  }) {\n    return AFOutlinedIconTextButton._(\n      key: key,\n      text: text,\n      iconBuilder: iconBuilder,\n      onTap: onTap,\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: disabled,\n      alignment: alignment,\n      borderColor: (context, isHovering, disabled, isFocused) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.fillColorScheme.errorThick;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.errorThickHover;\n        }\n        return theme.fillColorScheme.errorThick;\n      },\n      backgroundColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.fillColorScheme.errorThick;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.errorThickHover;\n        }\n        return theme.fillColorScheme.errorThick;\n      },\n      textColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        return disabled\n            ? theme.textColorScheme.error\n            : theme.textColorScheme.error;\n      },\n    );\n  }\n\n  /// Disabled outlined text button.\n  factory AFOutlinedIconTextButton.disabled({\n    Key? key,\n    required String text,\n    required AFOutlinedIconBuilder iconBuilder,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    MainAxisAlignment alignment = MainAxisAlignment.center,\n  }) {\n    return AFOutlinedIconTextButton._(\n      key: key,\n      text: text,\n      iconBuilder: iconBuilder,\n      onTap: () {},\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: true,\n      alignment: alignment,\n      textColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        return disabled\n            ? theme.textColorScheme.tertiary\n            : theme.textColorScheme.primary;\n      },\n      borderColor: (context, isHovering, disabled, isFocused) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.borderColorScheme.primary;\n        }\n        if (isHovering) {\n          return theme.borderColorScheme.primaryHover;\n        }\n        return theme.borderColorScheme.primary;\n      },\n      backgroundColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.fillColorScheme.content;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.contentHover;\n        }\n        return theme.fillColorScheme.content;\n      },\n    );\n  }\n\n  final String text;\n  final bool disabled;\n  final VoidCallback onTap;\n  final AFButtonSize size;\n  final EdgeInsetsGeometry? padding;\n  final double? borderRadius;\n  final MainAxisAlignment alignment;\n\n  final AFOutlinedIconBuilder iconBuilder;\n\n  final AFBaseButtonColorBuilder? textColor;\n  final AFBaseButtonBorderColorBuilder? borderColor;\n  final AFBaseButtonColorBuilder? backgroundColor;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return AFBaseButton(\n      backgroundColor: backgroundColor,\n      borderColor: borderColor,\n      padding: padding ?? size.buildPadding(context),\n      borderRadius: borderRadius ?? size.buildBorderRadius(context),\n      onTap: onTap,\n      disabled: disabled,\n      builder: (context, isHovering, disabled) {\n        final textColor = this.textColor?.call(context, isHovering, disabled) ??\n            theme.textColorScheme.primary;\n        return Row(\n          mainAxisAlignment: alignment,\n          children: [\n            iconBuilder(context, isHovering, disabled),\n            SizedBox(width: theme.spacing.s),\n            Text(\n              text,\n              style: size.buildTextStyle(context).copyWith(\n                    color: textColor,\n                  ),\n            ),\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart",
    "content": "import 'package:appflowy_ui/src/component/component.dart';\nimport 'package:appflowy_ui/src/theme/appflowy_theme.dart';\nimport 'package:flutter/material.dart';\n\nclass AFOutlinedTextButton extends AFBaseTextButton {\n  const AFOutlinedTextButton._({\n    super.key,\n    required super.text,\n    required super.onTap,\n    this.borderColor,\n    super.textStyle,\n    super.textColor,\n    super.backgroundColor,\n    super.size = AFButtonSize.m,\n    super.padding,\n    super.borderRadius,\n    super.disabled = false,\n    super.alignment,\n  });\n\n  /// Normal outlined text button.\n  factory AFOutlinedTextButton.normal({\n    Key? key,\n    required String text,\n    required VoidCallback onTap,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    bool disabled = false,\n    Alignment? alignment,\n    TextStyle? textStyle,\n    AFBaseButtonColorBuilder? backgroundColor,\n  }) {\n    return AFOutlinedTextButton._(\n      key: key,\n      text: text,\n      onTap: onTap,\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: disabled,\n      alignment: alignment,\n      textStyle: textStyle,\n      borderColor: (context, isHovering, disabled, isFocused) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.borderColorScheme.primary;\n        }\n        if (isHovering) {\n          return theme.borderColorScheme.primaryHover;\n        }\n        return theme.borderColorScheme.primary;\n      },\n      backgroundColor: backgroundColor ??\n          (context, isHovering, disabled) {\n            final theme = AppFlowyTheme.of(context);\n            if (disabled) {\n              return theme.fillColorScheme.content;\n            }\n            if (isHovering) {\n              return theme.fillColorScheme.contentHover;\n            }\n            return theme.fillColorScheme.content;\n          },\n      textColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.textColorScheme.tertiary;\n        }\n        if (isHovering) {\n          return theme.textColorScheme.primary;\n        }\n        return theme.textColorScheme.primary;\n      },\n    );\n  }\n\n  /// Destructive outlined text button.\n  factory AFOutlinedTextButton.destructive({\n    Key? key,\n    required String text,\n    required VoidCallback onTap,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    bool disabled = false,\n    Alignment? alignment,\n    TextStyle? textStyle,\n  }) {\n    return AFOutlinedTextButton._(\n      key: key,\n      text: text,\n      onTap: onTap,\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: disabled,\n      alignment: alignment,\n      textStyle: textStyle,\n      borderColor: (context, isHovering, disabled, isFocused) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.fillColorScheme.errorThick;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.errorThickHover;\n        }\n        return theme.fillColorScheme.errorThick;\n      },\n      backgroundColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.fillColorScheme.errorThick;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.errorSelect;\n        }\n        return theme.fillColorScheme.content;\n      },\n      textColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        return disabled\n            ? theme.textColorScheme.error\n            : theme.textColorScheme.error;\n      },\n    );\n  }\n\n  /// Disabled outlined text button.\n  factory AFOutlinedTextButton.disabled({\n    Key? key,\n    required String text,\n    AFButtonSize size = AFButtonSize.m,\n    EdgeInsetsGeometry? padding,\n    double? borderRadius,\n    Alignment? alignment,\n    TextStyle? textStyle,\n  }) {\n    return AFOutlinedTextButton._(\n      key: key,\n      text: text,\n      onTap: () {},\n      size: size,\n      padding: padding,\n      borderRadius: borderRadius,\n      disabled: true,\n      alignment: alignment,\n      textStyle: textStyle,\n      textColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        return disabled\n            ? theme.textColorScheme.tertiary\n            : theme.textColorScheme.primary;\n      },\n      borderColor: (context, isHovering, disabled, isFocused) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.borderColorScheme.primary;\n        }\n        if (isHovering) {\n          return theme.borderColorScheme.primaryHover;\n        }\n        return theme.borderColorScheme.primary;\n      },\n      backgroundColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.fillColorScheme.content;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.contentHover;\n        }\n        return theme.fillColorScheme.content;\n      },\n    );\n  }\n\n  final AFBaseButtonBorderColorBuilder? borderColor;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return ConstrainedBox(\n      constraints: BoxConstraints(\n        minWidth: 76,\n      ),\n      child: AFBaseButton(\n        disabled: disabled,\n        backgroundColor: backgroundColor,\n        borderColor: borderColor,\n        padding: padding ?? size.buildPadding(context),\n        borderRadius: borderRadius ?? size.buildBorderRadius(context),\n        onTap: onTap,\n        builder: (context, isHovering, disabled) {\n          final textColor =\n              this.textColor?.call(context, isHovering, disabled) ??\n                  theme.textColorScheme.primary;\n\n          Widget child = Text(\n            text,\n            style: textStyle ??\n                size.buildTextStyle(context).copyWith(color: textColor),\n            textAlign: TextAlign.center,\n          );\n\n          final alignment = this.alignment;\n\n          if (alignment != null) {\n            child = Align(\n              alignment: alignment,\n              child: child,\n            );\n          }\n\n          return child;\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart",
    "content": "export 'button/button.dart';\nexport 'dropdown_menu/dropdown_menu.dart';\nexport 'separator/divider.dart';\nexport 'modal/modal.dart';\nexport 'textfield/textfield.dart';\nexport 'avatar/avatar.dart';\nexport 'menu/menu.dart';\nexport 'popover/popover.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/dropdown_menu/dropdown_menu.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nmixin AFDropDownMenuMixin {\n  String get label;\n}\n\nclass AFDropDownMenu<T extends AFDropDownMenuMixin> extends StatefulWidget {\n  const AFDropDownMenu({\n    super.key,\n    required this.items,\n    required this.selectedItems,\n    this.onSelected,\n    this.closeOnSelect,\n    this.controller,\n    this.isClearEnabled = true,\n    this.errorText,\n    this.isRequired = false,\n    this.isDisabled = false,\n    this.emptyLabel,\n    this.clearIcon,\n    this.dropdownIcon,\n    this.selectedIcon,\n  });\n\n  final List<T> items;\n  final List<T> selectedItems;\n  final void Function(T? value)? onSelected;\n  final bool? closeOnSelect;\n  final AFPopoverController? controller;\n  final String? errorText;\n  final bool isRequired;\n  final bool isDisabled;\n  final bool isMultiselect = false;\n  final bool isClearEnabled;\n  final String? emptyLabel;\n  final Widget? clearIcon;\n  final Widget? dropdownIcon;\n  final Widget? selectedIcon;\n\n  @override\n  State<AFDropDownMenu<T>> createState() => _AFDropDownMenuState<T>();\n}\n\nclass _AFDropDownMenuState<T extends AFDropDownMenuMixin>\n    extends State<AFDropDownMenu<T>> {\n  late final AFPopoverController controller;\n  bool isHovering = false;\n  bool isOpen = false;\n\n  @override\n  void initState() {\n    super.initState();\n    controller = widget.controller ?? AFPopoverController();\n    controller.addListener(popoverListener);\n  }\n\n  @override\n  void dispose() {\n    if (widget.controller == null) {\n      controller.dispose();\n    } else {\n      controller.removeListener(popoverListener);\n    }\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return LayoutBuilder(\n      builder: (context, constraints) {\n        return AFPopover(\n          controller: controller,\n          padding: EdgeInsets.zero,\n          anchor: AFAnchor(\n            childAlignment: Alignment.topCenter,\n            overlayAlignment: Alignment.bottomCenter,\n            offset: Offset(0, theme.spacing.xs),\n          ),\n          decoration: BoxDecoration(\n            color: theme.surfaceColorScheme.layer01,\n            borderRadius: BorderRadius.circular(theme.borderRadius.m),\n            boxShadow: theme.shadow.small,\n          ),\n          popover: (popoverContext) {\n            return ConstrainedBox(\n              constraints: BoxConstraints(\n                maxWidth: constraints.maxWidth,\n                maxHeight: 300,\n              ),\n              child: _DropdownPopoverContents(\n                items: widget.items,\n                onSelected: (item) {\n                  widget.onSelected?.call(item);\n                  if ((widget.closeOnSelect == null && !widget.isMultiselect) ||\n                      widget.closeOnSelect == true) {\n                    controller.hide();\n                  }\n                },\n                selectedItems: widget.selectedItems,\n                selectedIcon: widget.selectedIcon,\n                isMultiselect: widget.isMultiselect,\n              ),\n            );\n          },\n          child: MouseRegion(\n            onEnter: (_) => setState(() => isHovering = true),\n            onExit: (_) => setState(() => isHovering = false),\n            child: GestureDetector(\n              onTap: () {\n                if (widget.isDisabled) {\n                  return;\n                }\n                if (controller.isOpen) {\n                  controller.hide();\n                } else {\n                  controller.show();\n                }\n              },\n              child: Container(\n                constraints: const BoxConstraints.tightFor(height: 32),\n                decoration: BoxDecoration(\n                  border: Border.all(\n                    color: widget.isDisabled\n                        ? theme.borderColorScheme.primary\n                        : isOpen\n                            ? theme.borderColorScheme.themeThick\n                            : isHovering\n                                ? theme.borderColorScheme.primaryHover\n                                : theme.borderColorScheme.primary,\n                  ),\n                  color: widget.isDisabled\n                      ? theme.fillColorScheme.contentHover\n                      : null,\n                  borderRadius: BorderRadius.circular(theme.borderRadius.m),\n                ),\n                padding: EdgeInsets.symmetric(\n                  horizontal: theme.spacing.m,\n                  vertical: theme.spacing.xs,\n                ),\n                child: Row(\n                  spacing: theme.spacing.xs,\n                  children: [\n                    Expanded(\n                      child: _DropdownButtonContents(\n                        items: widget.selectedItems,\n                        isMultiselect: widget.isMultiselect,\n                        isDisabled: widget.isDisabled,\n                        emptyLabel: widget.emptyLabel,\n                      ),\n                    ),\n                    if (widget.isClearEnabled &&\n                        isOpen &&\n                        widget.clearIcon != null)\n                      widget.clearIcon!,\n                    widget.dropdownIcon ??\n                        SizedBox.square(\n                          dimension: 20,\n                          child: Icon(\n                            Icons.arrow_drop_down,\n                            size: 16,\n                          ),\n                        ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  void popoverListener() {\n    setState(() {\n      isOpen = controller.isOpen;\n    });\n  }\n}\n\nclass _DropdownButtonContents<T extends AFDropDownMenuMixin>\n    extends StatelessWidget {\n  const _DropdownButtonContents({\n    super.key,\n    required this.items,\n    this.isDisabled = false,\n    this.isMultiselect = false,\n    this.emptyLabel,\n  });\n\n  final List<T> items;\n  final bool isMultiselect;\n  final bool isDisabled;\n  final String? emptyLabel;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    if (isMultiselect) {\n      return SingleChildScrollView(\n        scrollDirection: Axis.horizontal,\n        padding: EdgeInsets.zero,\n        child: Row(\n          spacing: theme.spacing.xs,\n          children: [\n            ...items.map((item) {\n              return Container(\n                decoration: BoxDecoration(\n                  borderRadius: BorderRadius.circular(theme.spacing.s),\n                  color: theme.surfaceContainerColorScheme.layer02,\n                ),\n                padding: EdgeInsetsDirectional.fromSTEB(\n                  theme.spacing.m,\n                  1.0,\n                  theme.spacing.s,\n                  1.0,\n                ),\n                child: Row(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    Flexible(\n                      child: Text(\n                        item.label,\n                        style: theme.textStyle.body.standard(\n                          color: theme.textColorScheme.primary,\n                        ),\n                      ),\n                    ),\n                    Icon(\n                      Icons.cancel,\n                      size: 16,\n                      color: theme.iconColorScheme.tertiary,\n                    ),\n                  ],\n                ),\n              );\n            }),\n            TextField(\n              enabled: !isDisabled,\n              decoration: InputDecoration(\n                hintText: items.isEmpty ? emptyLabel ?? \"(optional)\" : null,\n                hintStyle: theme.textStyle.body.standard(\n                  color: theme.textColorScheme.tertiary,\n                ),\n                border: InputBorder.none,\n                constraints: const BoxConstraints(maxWidth: 120),\n                isCollapsed: true,\n                isDense: true,\n              ),\n              style: theme.textStyle.body.standard(\n                color: isDisabled\n                    ? theme.textColorScheme.tertiary\n                    : theme.textColorScheme.primary,\n              ),\n            ),\n          ],\n        ),\n      );\n    }\n\n    return Text(\n      items.isEmpty ? emptyLabel ?? \"(optional)\" : items.first.label,\n      style: theme.textStyle.body.standard(\n        color: isDisabled || items.isEmpty\n            ? theme.textColorScheme.tertiary\n            : theme.textColorScheme.primary,\n      ),\n      overflow: TextOverflow.ellipsis,\n      maxLines: 1,\n    );\n  }\n}\n\nclass _DropdownPopoverContents<T extends AFDropDownMenuMixin>\n    extends StatelessWidget {\n  const _DropdownPopoverContents({\n    super.key,\n    required this.items,\n    this.selectedItems = const [],\n    this.onSelected,\n    this.isMultiselect = false,\n    this.selectedIcon,\n  });\n\n  final List<T> items;\n  final List<T> selectedItems;\n  final void Function(T? value)? onSelected;\n  final bool isMultiselect;\n  final Widget? selectedIcon;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return FocusScope(\n      autofocus: true,\n      child: ListView.builder(\n        itemCount: items.length,\n        physics: const ClampingScrollPhysics(),\n        padding: EdgeInsets.all(theme.spacing.m),\n        shrinkWrap: true,\n        itemBuilder: itemBuilder,\n      ),\n    );\n  }\n\n  Widget itemBuilder(BuildContext context, int index) {\n    final theme = AppFlowyTheme.of(context);\n    final item = items[index];\n\n    return AFBaseButton(\n      padding: EdgeInsets.symmetric(\n        vertical: theme.spacing.s,\n        horizontal: theme.spacing.m,\n      ),\n      borderRadius: theme.borderRadius.m,\n      borderColor: (context, isHovering, disabled, isFocused) {\n        return Colors.transparent;\n      },\n      showFocusRing: false,\n      builder: (context, _, __) {\n        return Row(\n          spacing: theme.spacing.m,\n          children: [\n            Expanded(\n              child: Text(\n                item.label,\n                style: theme.textStyle.body\n                    .standard(color: theme.textColorScheme.primary)\n                    .copyWith(overflow: TextOverflow.ellipsis),\n              ),\n            ),\n            if (selectedItems.contains(item) && isMultiselect)\n              selectedIcon ??\n                  Icon(\n                    Icons.check,\n                    color: theme.fillColorScheme.themeThick,\n                    size: 20.0,\n                  ),\n          ],\n        );\n      },\n      backgroundColor: (context, isHovering, _) {\n        if (selectedItems.contains(item) && !isMultiselect) {\n          return theme.fillColorScheme.themeSelect;\n        }\n        if (isHovering) {\n          return theme.fillColorScheme.contentHover;\n        }\n        return Colors.transparent;\n      },\n      onTap: () {\n        onSelected?.call(item);\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/menu/menu.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nexport 'menu_item.dart';\nexport 'section.dart';\nexport 'text_menu_item.dart';\n\n/// The main menu container widget, supporting sections, menu items.\nclass AFMenu extends StatelessWidget {\n  const AFMenu({\n    super.key,\n    required this.children,\n    this.width,\n    this.backgroundColor,\n  });\n\n  /// The list of widgets to display in the menu (sections or menu items).\n  final List<Widget> children;\n\n  /// The width of the menu.\n  final double? width;\n\n  final Color? backgroundColor;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return Container(\n      decoration: BoxDecoration(\n        color: backgroundColor ?? theme.surfaceColorScheme.primary,\n        borderRadius: BorderRadius.circular(theme.borderRadius.l),\n        border: Border.all(\n          color: theme.borderColorScheme.primary,\n        ),\n        boxShadow: theme.shadow.medium,\n      ),\n      width: width,\n      padding: EdgeInsets.all(theme.spacing.m),\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: children,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/menu/menu_item.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\n/// Menu item widget\nclass AFMenuItem extends StatelessWidget {\n  /// Creates a menu item.\n  ///\n  /// [title] and [onTap] are required. Optionally provide [leading], [subtitle], [selected], and [trailing].\n  const AFMenuItem({\n    super.key,\n    required this.title,\n    this.onTap,\n    this.leading,\n    this.subtitle,\n    this.selected = false,\n    this.trailing,\n    this.padding,\n    this.showSelectedBackground = true,\n  });\n\n  /// Widget to display before the title (e.g., an icon or avatar).\n  final Widget? leading;\n\n  /// The main text of the menu item.\n  final Widget title;\n\n  /// Optional secondary text displayed below the title.\n  final Widget? subtitle;\n\n  /// Whether the menu item is selected.\n  final bool selected;\n\n  /// Whether to show the selected background color.\n  final bool showSelectedBackground;\n\n  /// Called when the menu item is tapped.\n  final VoidCallback? onTap;\n\n  /// Widget to display after the title (e.g., a trailing icon).\n  final Widget? trailing;\n\n  /// Padding of the menu item.\n  final EdgeInsets? padding;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    final effectivePadding = padding ??\n        EdgeInsets.symmetric(\n          horizontal: theme.spacing.m,\n          vertical: theme.spacing.s,\n        );\n\n    return AFBaseButton(\n      onTap: onTap,\n      padding: effectivePadding,\n      borderRadius: theme.borderRadius.m,\n      borderColor: (context, isHovering, disabled, isFocused) {\n        return Colors.transparent;\n      },\n      backgroundColor: (context, isHovering, disabled) {\n        final theme = AppFlowyTheme.of(context);\n        if (disabled) {\n          return theme.fillColorScheme.content;\n        }\n        if (selected && showSelectedBackground) {\n          return theme.fillColorScheme.themeSelect;\n        }\n        if (isHovering && onTap != null) {\n          return theme.fillColorScheme.contentHover;\n        }\n        return theme.fillColorScheme.content;\n      },\n      builder: (context, isHovering, disabled) {\n        return Row(\n          children: [\n            // Leading widget (icon/avatar), if provided\n            if (leading != null) ...[\n              leading!,\n              SizedBox(width: theme.spacing.m),\n            ],\n            // Main content: title and optional subtitle\n            Expanded(\n              child: Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  // Title text\n                  title,\n                  // Subtitle text, if provided\n                  if (subtitle != null) subtitle!,\n                ],\n              ),\n            ),\n            // Trailing widget (e.g., icon), if provided\n            if (trailing != null) trailing!,\n          ],\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/menu/section.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\n/// A section in the menu, optionally with a title and a list of children.\nclass AFMenuSection extends StatelessWidget {\n  const AFMenuSection({\n    super.key,\n    this.title,\n    required this.children,\n    this.padding,\n    this.constraints,\n  });\n\n  /// The title of the section (e.g., 'Section 1').\n  final String? title;\n\n  /// The widgets to display in this section (typically AFMenuItem widgets).\n  final List<Widget> children;\n\n  /// Section padding.\n  final EdgeInsets? padding;\n\n  /// The height of the section.\n  final BoxConstraints? constraints;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    final effectivePadding = padding ??\n        EdgeInsets.symmetric(\n          horizontal: theme.spacing.m,\n          vertical: theme.spacing.s,\n        );\n\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        if (title != null) ...[\n          Padding(\n            padding: effectivePadding,\n            child: Text(\n              title!,\n              style: theme.textStyle.caption.enhanced(\n                color: theme.textColorScheme.tertiary,\n              ),\n            ),\n          ),\n        ],\n        Container(\n          constraints: constraints,\n          child: SingleChildScrollView(\n            child: Column(\n              children: children,\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/menu/text_menu_item.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\n/// Text menu item widget\nclass AFTextMenuItem extends StatelessWidget {\n  /// Creates a text menu item.\n  ///\n  /// [title] and [onTap] are required. Optionally provide [leading], [subtitle], [selected], and [trailing].\n  const AFTextMenuItem({\n    super.key,\n    required this.title,\n    required this.onTap,\n    this.leading,\n    this.subtitle,\n    this.selected = false,\n    this.trailing,\n    this.titleColor,\n    this.subtitleColor,\n    this.showSelectedBackground = true,\n  });\n\n  /// Widget to display before the title (e.g., an icon or avatar).\n  final Widget? leading;\n\n  /// The main text of the menu item.\n  final String title;\n\n  /// The color of the title.\n  final Color? titleColor;\n\n  /// Optional secondary text displayed below the title.\n  final String? subtitle;\n\n  /// The color of the subtitle.\n  final Color? subtitleColor;\n\n  /// Whether the menu item is selected.\n  final bool selected;\n\n  /// Whether to show the selected background color.\n  final bool showSelectedBackground;\n\n  /// Called when the menu item is tapped.\n  final VoidCallback onTap;\n\n  /// Widget to display after the title (e.g., a trailing icon).\n  final Widget? trailing;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    return AFMenuItem(\n      title: Text(\n        title,\n        style: theme.textStyle.body.standard(\n          color: titleColor ?? theme.textColorScheme.primary,\n        ),\n      ),\n      subtitle: subtitle != null\n          ? Text(\n              subtitle!,\n              style: theme.textStyle.caption.standard(\n                color: subtitleColor ?? theme.textColorScheme.secondary,\n              ),\n            )\n          : null,\n      leading: leading,\n      trailing: trailing,\n      selected: selected,\n      showSelectedBackground: showSelectedBackground,\n      onTap: onTap,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/dimension.dart",
    "content": "class AFModalDimension {\n  const AFModalDimension._();\n\n  static const double S = 400.0;\n  static const double M = 560.0;\n  static const double L = 720.0;\n\n  static const double dialogHeight = 200.0;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/modal.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nexport 'dimension.dart';\n\nclass AFModal extends StatelessWidget {\n  const AFModal({\n    super.key,\n    this.constraints = const BoxConstraints(),\n    this.backgroundColor,\n    required this.child,\n  });\n\n  final BoxConstraints constraints;\n  final Color? backgroundColor;\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return Center(\n      child: Padding(\n        padding: EdgeInsets.all(theme.spacing.xl),\n        child: ConstrainedBox(\n          constraints: constraints,\n          child: DecoratedBox(\n            decoration: BoxDecoration(\n              boxShadow: theme.shadow.medium,\n              borderRadius: BorderRadius.circular(theme.borderRadius.xl),\n              color: backgroundColor ?? theme.surfaceColorScheme.primary,\n            ),\n            child: Material(\n              color: Colors.transparent,\n              child: child,\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass AFModalHeader extends StatelessWidget {\n  const AFModalHeader({\n    super.key,\n    required this.leading,\n    this.trailing = const [],\n  });\n\n  final Widget leading;\n  final List<Widget> trailing;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return Padding(\n      padding: EdgeInsets.only(\n        top: theme.spacing.xl,\n        left: theme.spacing.xxl,\n        right: theme.spacing.xxl,\n      ),\n      child: DefaultTextStyle(\n        style: theme.textStyle.heading4.prominent(\n          color: theme.textColorScheme.primary,\n        ),\n        child: Row(\n          spacing: theme.spacing.s,\n          children: [\n            Expanded(child: leading),\n            ...trailing,\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass AFModalFooter extends StatelessWidget {\n  const AFModalFooter({\n    super.key,\n    this.leading = const [],\n    this.trailing = const [],\n  });\n\n  final List<Widget> leading;\n  final List<Widget> trailing;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return Padding(\n      padding: EdgeInsets.only(\n        bottom: theme.spacing.xl,\n        left: theme.spacing.xxl,\n        right: theme.spacing.xxl,\n      ),\n      child: Row(\n        spacing: theme.spacing.l,\n        children: [\n          ...leading,\n          Spacer(),\n          ...trailing,\n        ],\n      ),\n    );\n  }\n}\n\nclass AFModalBody extends StatelessWidget {\n  const AFModalBody({\n    super.key,\n    required this.child,\n  });\n\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    return Padding(\n      padding: EdgeInsets.symmetric(\n        vertical: theme.spacing.l,\n        horizontal: theme.spacing.xxl,\n      ),\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/popover/anchor.dart",
    "content": "import 'package:appflowy_ui/src/component/popover/shadcn/_portal.dart';\nimport 'package:flutter/material.dart';\n\n/// Automatically infers the position of the [ShadPortal] in the global\n/// coordinate system adjusting according to the [offset],\n/// [followerAnchor] and [targetAnchor] properties.\n@immutable\nclass AFAnchorAuto extends ShadAnchorAuto {\n  const AFAnchorAuto({\n    super.offset,\n    super.followTargetOnResize,\n    super.followerAnchor,\n    super.targetAnchor,\n  });\n}\n\n/// Manually specifies the position of the [ShadPortal] in the global\n/// coordinate system.\n@immutable\nclass AFAnchor extends ShadAnchor {\n  const AFAnchor({\n    super.childAlignment,\n    super.overlayAlignment,\n    super.offset,\n  });\n}\n\n/// Manually specifies the position of the [ShadPortal] in the global\n/// coordinate system.\n@immutable\nclass AFGlobalAnchor extends ShadGlobalAnchor {\n  const AFGlobalAnchor(super.offset);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/popover/popover.dart",
    "content": "import 'dart:ui';\n\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:appflowy_ui/src/component/popover/shadcn/_mouse_area.dart';\nimport 'package:appflowy_ui/src/component/popover/shadcn/_portal.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_animate/flutter_animate.dart';\n\nexport 'anchor.dart';\n\n/// Notes: The implementation of this page is copied from [flutter_shadcn_ui](https://github.com/nank1ro/flutter-shadcn-ui).\n///\n/// Renaming is for the consistency of the AppFlowy UI.\n\n/// Controls the visibility of a [AFPopover].\nclass AFPopoverController extends ChangeNotifier {\n  AFPopoverController({bool isOpen = false}) : _isOpen = isOpen;\n\n  bool _isOpen = false;\n\n  /// Indicates if the popover is visible.\n  bool get isOpen => _isOpen;\n\n  /// Displays the popover.\n  void show() {\n    if (_isOpen) return;\n    _isOpen = true;\n    notifyListeners();\n  }\n\n  /// Hides the popover.\n  void hide() {\n    if (!_isOpen) return;\n    _isOpen = false;\n    notifyListeners();\n  }\n\n  void setOpen(bool open) {\n    if (_isOpen == open) return;\n    _isOpen = open;\n    notifyListeners();\n  }\n\n  /// Toggles the visibility of the popover.\n  void toggle() => _isOpen ? hide() : show();\n}\n\nclass AFPopover extends StatefulWidget {\n  const AFPopover({\n    super.key,\n    required this.child,\n    required this.popover,\n    this.controller,\n    this.visible,\n    this.closeOnTapOutside = true,\n    this.focusNode,\n    this.anchor,\n    this.effects,\n    this.shadows,\n    this.padding,\n    this.decoration,\n    this.filter,\n    this.groupId,\n    this.areaGroupId,\n    this.useSameGroupIdForChild = true,\n  }) : assert(\n          (controller != null) ^ (visible != null),\n          'Either controller or visible must be provided',\n        );\n\n  /// {@template ShadPopover.popover}\n  /// The widget displayed as a popover.\n  /// {@endtemplate}\n  final WidgetBuilder popover;\n\n  /// {@template ShadPopover.child}\n  /// The child widget.\n  /// {@endtemplate}\n  final Widget child;\n\n  /// {@template ShadPopover.controller}\n  /// The controller that controls the visibility of the [popover].\n  /// {@endtemplate}\n  final AFPopoverController? controller;\n\n  /// {@template ShadPopover.visible}\n  /// Indicates if the popover should be visible.\n  /// {@endtemplate}\n  final bool? visible;\n\n  /// {@template ShadPopover.closeOnTapOutside}\n  /// Closes the popover when the user taps outside, defaults to true.\n  /// {@endtemplate}\n  final bool closeOnTapOutside;\n\n  /// {@template ShadPopover.focusNode}\n  /// The focus node of the child, the [popover] will be shown when\n  /// focused.\n  /// {@endtemplate}\n  final FocusNode? focusNode;\n\n  ///{@template ShadPopover.anchor}\n  /// The position of the [popover] in the global coordinate system.\n  ///\n  /// Defaults to `ShadAnchorAuto()`.\n  /// {@endtemplate}\n  final ShadAnchorBase? anchor;\n\n  /// {@template ShadPopover.effects}\n  /// The animation effects applied to the [popover]. Defaults to\n  /// [FadeEffect(), ScaleEffect(begin: Offset(.95, .95), end: Offset(1, 1)),\n  /// MoveEffect(begin: Offset(0, 2), end: Offset(0, 0))].\n  /// {@endtemplate}\n  final List<Effect<dynamic>>? effects;\n\n  /// {@template ShadPopover.shadows}\n  /// The shadows applied to the [popover], defaults to\n  /// [ShadShadows.md].\n  /// {@endtemplate}\n  final List<BoxShadow>? shadows;\n\n  /// {@template ShadPopover.padding}\n  /// The padding of the [popover], defaults to\n  /// `EdgeInsets.symmetric(horizontal: 12, vertical: 6)`.\n  /// {@endtemplate}\n  final EdgeInsetsGeometry? padding;\n\n  /// {@template ShadPopover.decoration}\n  /// The decoration of the [popover].\n  /// {@endtemplate}\n  final BoxDecoration? decoration;\n\n  /// {@template ShadPopover.filter}\n  /// The filter of the [popover], defaults to `null`.\n  /// {@endtemplate}\n  final ImageFilter? filter;\n\n  /// {@template ShadPopover.groupId}\n  /// The group id of the [popover], defaults to `UniqueKey()`.\n  ///\n  /// Used to determine it the tap is inside the [popover] or not.\n  /// {@endtemplate}\n  final Object? groupId;\n\n  /// {@macro ShadMouseArea.groupId}\n  final Object? areaGroupId;\n\n  /// {@template ShadPopover.useSameGroupIdForChild}\n  /// Whether the [groupId] should be used for the child widget, defaults to\n  /// `true`. This teams that taps on the child widget will be handled as inside\n  /// the popover.\n  /// {@endtemplate}\n  final bool useSameGroupIdForChild;\n\n  @override\n  State<AFPopover> createState() => _AFPopoverState();\n}\n\nclass _AFPopoverState extends State<AFPopover> {\n  static final List<_AFPopoverState> _openPopovers = [];\n  static int? _lastPopoverClosedTimestamp;\n  static void _markPopoverClosedThisFrame() {\n    _lastPopoverClosedTimestamp = DateTime.now().microsecondsSinceEpoch;\n    WidgetsBinding.instance.addPostFrameCallback((_) {\n      _lastPopoverClosedTimestamp = null;\n    });\n  }\n\n  AFPopoverController? _controller;\n  AFPopoverController get controller => widget.controller ?? _controller!;\n  bool animating = false;\n\n  late final _popoverKey = UniqueKey();\n\n  Object get groupId => widget.groupId ?? _popoverKey;\n\n  bool get _isTopMostPopover =>\n      _openPopovers.isNotEmpty && _openPopovers.last == this;\n\n  @override\n  void initState() {\n    super.initState();\n    if (widget.controller == null) {\n      _controller = AFPopoverController();\n    }\n    controller.addListener(_onControllerChanged);\n    if (controller.isOpen) {\n      _registerPopover();\n    }\n  }\n\n  void _onControllerChanged() {\n    if (controller.isOpen) {\n      _registerPopover();\n    } else {\n      _unregisterPopover();\n    }\n  }\n\n  @override\n  void didUpdateWidget(covariant AFPopover oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    if (widget.visible != null) {\n      if (widget.visible! && !controller.isOpen) {\n        controller.show();\n      } else if (!widget.visible! && controller.isOpen) {\n        controller.hide();\n      }\n    }\n  }\n\n  @override\n  void dispose() {\n    controller.removeListener(_onControllerChanged);\n    _unregisterPopover();\n    _controller?.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n\n    final effectiveEffects = widget.effects ?? [];\n    final effectivePadding = widget.padding ??\n        EdgeInsets.symmetric(\n          horizontal: theme.spacing.m,\n          vertical: theme.spacing.l,\n        );\n\n    final effectiveAnchor = widget.anchor ?? const ShadAnchorAuto();\n    final effectiveDecoration = widget.decoration ??\n        BoxDecoration(\n          color: theme.surfaceColorScheme.layer01,\n          borderRadius: BorderRadius.circular(theme.borderRadius.m),\n          boxShadow: theme.shadow.medium,\n        );\n\n    final effectiveFilter = widget.filter;\n\n    Widget popover = ShadMouseArea(\n      groupId: widget.areaGroupId,\n      child: DecoratedBox(\n        decoration: effectiveDecoration,\n        child: Padding(\n          padding: effectivePadding,\n          child: DefaultTextStyle(\n            style: TextStyle(\n              color: theme.textColorScheme.primary,\n            ),\n            child: Builder(\n              builder: widget.popover,\n            ),\n          ),\n        ),\n      ),\n    );\n\n    if (effectiveFilter != null) {\n      popover = BackdropFilter(\n        filter: widget.filter!,\n        child: popover,\n      );\n    }\n\n    if (effectiveEffects.isNotEmpty) {\n      popover = Animate(\n        effects: effectiveEffects,\n        child: popover,\n      );\n    }\n\n    if (widget.closeOnTapOutside) {\n      popover = TapRegion(\n        groupId: groupId,\n        behavior: HitTestBehavior.opaque,\n        onTapOutside: (_) {\n          final now = DateTime.now().microsecondsSinceEpoch;\n          if (_isTopMostPopover &&\n              (_lastPopoverClosedTimestamp == null ||\n                  now - _lastPopoverClosedTimestamp! > 1000)) {\n            controller.hide();\n            _markPopoverClosedThisFrame();\n          }\n        },\n        child: popover,\n      );\n    }\n\n    Widget child = ListenableBuilder(\n      listenable: controller,\n      builder: (context, _) {\n        return CallbackShortcuts(\n          bindings: {\n            const SingleActivator(LogicalKeyboardKey.escape): () {\n              controller.hide();\n            },\n          },\n          child: ShadPortal(\n            portalBuilder: (_) => popover,\n            visible: controller.isOpen,\n            anchor: effectiveAnchor,\n            child: widget.child,\n          ),\n        );\n      },\n    );\n\n    if (widget.useSameGroupIdForChild) {\n      child = TapRegion(\n        groupId: groupId,\n        child: child,\n      );\n    }\n    return child;\n  }\n\n  void _registerPopover() {\n    if (!_openPopovers.contains(this)) {\n      _openPopovers.add(this);\n    }\n  }\n\n  void _unregisterPopover() {\n    _openPopovers.remove(this);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/popover/shadcn/_mouse_area.dart",
    "content": "import 'package:flutter/foundation.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter/widgets.dart';\n\n/// Notes: The implementation of this page is copied from [flutter_shadcn_ui](https://github.com/nank1ro/flutter-shadcn-ui).\n\nabstract class MouseAreaRegistry {\n  /// Register the given [ShadMouseAreaRenderBox] with the registry.\n  void registerMouseArea(ShadMouseAreaRenderBox region);\n\n  /// Unregister the given [ShadMouseAreaRenderBox] with the registry.\n  void unregisterMouseArea(ShadMouseAreaRenderBox region);\n\n  /// Allows finding of the nearest [MouseAreaRegistry], such as a\n  /// [MouseAreaSurfaceRenderBox].\n  static MouseAreaRegistry? maybeOf(BuildContext context) {\n    return context.findAncestorRenderObjectOfType<MouseAreaSurfaceRenderBox>();\n  }\n\n  /// Allows finding of the nearest [MouseAreaRegistry], such as a\n  /// [MouseAreaSurfaceRenderBox].\n  ///\n  /// Will throw if a [MouseAreaRegistry] isn't found.\n  static MouseAreaRegistry of(BuildContext context) {\n    final registry = maybeOf(context);\n    assert(() {\n      if (registry == null) {\n        throw FlutterError(\n          '''\nMouseRegionRegistry.of() was called with a context that does not contain a MouseRegionSurface widget.\\n\n  No MouseRegionSurface widget ancestor could be found starting from the context that was passed to\n  MouseRegionRegistry.of().\\n\n  The context used was:\\n\n    $context\n''',\n        );\n      }\n      return true;\n    }());\n    return registry!;\n  }\n}\n\nclass MouseAreaSurfaceRenderBox extends RenderProxyBoxWithHitTestBehavior\n    implements MouseAreaRegistry {\n  final Expando<BoxHitTestResult> _cachedResults = Expando<BoxHitTestResult>();\n  final Set<ShadMouseAreaRenderBox> _registeredRegions =\n      <ShadMouseAreaRenderBox>{};\n  final Map<Object?, Set<ShadMouseAreaRenderBox>> _groupIdToRegions =\n      <Object?, Set<ShadMouseAreaRenderBox>>{};\n\n  @override\n  void handleEvent(PointerEvent event, HitTestEntry entry) {\n    assert(debugHandleEvent(event, entry));\n    assert(\n      () {\n        for (final region in _registeredRegions) {\n          if (!region.enabled) {\n            return false;\n          }\n        }\n        return true;\n      }(),\n      'A MouseAreaRegion was registered when it was disabled.',\n    );\n\n    if (_registeredRegions.isEmpty) {\n      return;\n    }\n\n    final result = _cachedResults[entry];\n\n    if (result == null) {\n      return;\n    }\n\n    // A child was hit, so we need to call onExit for those regions or\n    // groups of regions that were not hit.\n    final hitRegions = _getRegionsHit(_registeredRegions, result.path)\n        .cast<ShadMouseAreaRenderBox>()\n        .toSet();\n\n    final insideRegions = <ShadMouseAreaRenderBox>{\n      for (final ShadMouseAreaRenderBox region in hitRegions)\n        if (region.groupId == null)\n          region\n        // Adding all grouped regions, so they act as a single region.\n        else\n          ..._groupIdToRegions[region.groupId]!,\n    };\n    // If they're not inside, then they're outside.\n    final outsideRegions = _registeredRegions.difference(insideRegions);\n    for (final region in outsideRegions) {\n      region.onExit?.call(\n        PointerExitEvent(\n          viewId: event.viewId,\n          timeStamp: event.timeStamp,\n          pointer: event.pointer,\n          device: event.device,\n          position: event.position,\n          delta: event.delta,\n          buttons: event.buttons,\n          obscured: event.obscured,\n          pressureMin: event.pressureMin,\n          pressureMax: event.pressureMax,\n          distance: event.distance,\n          distanceMax: event.distanceMax,\n          size: event.size,\n          radiusMajor: event.radiusMajor,\n          radiusMinor: event.radiusMinor,\n          radiusMin: event.radiusMin,\n          radiusMax: event.radiusMax,\n          orientation: event.orientation,\n          tilt: event.tilt,\n          down: event.down,\n          synthesized: event.synthesized,\n          embedderId: event.embedderId,\n        ),\n      );\n    }\n    for (final region in insideRegions) {\n      region.onEnter?.call(\n        PointerEnterEvent(\n          viewId: event.viewId,\n          timeStamp: event.timeStamp,\n          pointer: event.pointer,\n          device: event.device,\n          position: event.position,\n          delta: event.delta,\n          buttons: event.buttons,\n          obscured: event.obscured,\n          pressureMin: event.pressureMin,\n          pressureMax: event.pressureMax,\n          distance: event.distance,\n          distanceMax: event.distanceMax,\n          size: event.size,\n          radiusMajor: event.radiusMajor,\n          radiusMinor: event.radiusMinor,\n          radiusMin: event.radiusMin,\n          radiusMax: event.radiusMax,\n          orientation: event.orientation,\n          tilt: event.tilt,\n          down: event.down,\n          synthesized: event.synthesized,\n          embedderId: event.embedderId,\n        ),\n      );\n    }\n  }\n\n  @override\n  bool hitTest(BoxHitTestResult result, {required Offset position}) {\n    if (!size.contains(position)) {\n      return false;\n    }\n\n    final hitTarget =\n        hitTestChildren(result, position: position) || hitTestSelf(position);\n\n    if (hitTarget) {\n      final entry = BoxHitTestEntry(this, position);\n      _cachedResults[entry] = result;\n      result.add(entry);\n    }\n\n    return hitTarget;\n  }\n\n  @override\n  void registerMouseArea(ShadMouseAreaRenderBox region) {\n    assert(!_registeredRegions.contains(region));\n    _registeredRegions.add(region);\n    if (region.groupId != null) {\n      _groupIdToRegions[region.groupId] ??= <ShadMouseAreaRenderBox>{};\n      _groupIdToRegions[region.groupId]!.add(region);\n    }\n  }\n\n  @override\n  void unregisterMouseArea(ShadMouseAreaRenderBox region) {\n    assert(_registeredRegions.contains(region));\n    _registeredRegions.remove(region);\n    if (region.groupId != null) {\n      assert(_groupIdToRegions.containsKey(region.groupId));\n      _groupIdToRegions[region.groupId]!.remove(region);\n      if (_groupIdToRegions[region.groupId]!.isEmpty) {\n        _groupIdToRegions.remove(region.groupId);\n      }\n    }\n  }\n\n  // Returns the registered regions that are in the hit path.\n  Set<HitTestTarget> _getRegionsHit(\n    Set<ShadMouseAreaRenderBox> detectors,\n    Iterable<HitTestEntry> hitTestPath,\n  ) {\n    return <HitTestTarget>{\n      for (final HitTestEntry<HitTestTarget> entry in hitTestPath)\n        if (entry.target case final HitTestTarget target)\n          if (_registeredRegions.contains(target)) target,\n    };\n  }\n}\n\nclass ShadMouseArea extends SingleChildRenderObjectWidget {\n  /// Creates a const [ShadMouseArea].\n  ///\n  /// The [child] argument is required.\n  const ShadMouseArea({\n    super.key,\n    super.child,\n    this.enabled = true,\n    this.behavior = HitTestBehavior.deferToChild,\n    this.groupId,\n    this.onEnter,\n    this.onExit,\n    this.cursor = MouseCursor.defer,\n    String? debugLabel,\n  }) : debugLabel = kReleaseMode ? null : debugLabel;\n\n  /// Whether or not this [ShadMouseArea] is enabled as part of the composite\n  /// region.\n  final bool enabled;\n\n  /// How to behave during hit testing when deciding how the hit test propagates\n  /// to children and whether to consider targets behind this [ShadMouseArea].\n  ///\n  /// Defaults to [HitTestBehavior.deferToChild].\n  ///\n  /// See [HitTestBehavior] for the allowed values and their meanings.\n  final HitTestBehavior behavior;\n\n  /// {@template ShadMouseArea.groupId}\n  /// An optional group ID that groups [ShadMouseArea]s together so that they\n  /// operate as one region. If any member of a group is hit by a particular\n  /// hover, then all members will have their [onEnter] or [onExit] called.\n  ///\n  /// If the group id is null, then only this region is hit tested.\n  /// {@endtemplate}\n  final Object? groupId;\n\n  /// Triggered when a pointer enters the region.\n  final PointerEnterEventListener? onEnter;\n\n  /// Triggered when a pointer exits the region.\n  final PointerExitEventListener? onExit;\n\n  /// The mouse cursor for mouse pointers that are hovering over the region.\n  ///\n  /// When a mouse enters the region, its cursor will be changed to the [cursor]\n  /// When the mouse leaves the region, the cursor will be decided by the region\n  /// found at the new location.\n  ///\n  /// The [cursor] defaults to [MouseCursor.defer], deferring the choice of\n  /// cursor to the next region behind it in hit-test order.\n  final MouseCursor cursor;\n\n  /// An optional debug label to help with debugging in debug mode.\n  ///\n  /// Will be null in release mode.\n  final String? debugLabel;\n\n  @override\n  RenderObject createRenderObject(BuildContext context) {\n    return ShadMouseAreaRenderBox(\n      registry: MouseAreaRegistry.maybeOf(context),\n      enabled: enabled,\n      behavior: behavior,\n      groupId: groupId,\n      debugLabel: debugLabel,\n      onEnter: onEnter,\n      onExit: onExit,\n      cursor: cursor,\n    );\n  }\n\n  @override\n  void debugFillProperties(DiagnosticPropertiesBuilder properties) {\n    super.debugFillProperties(properties);\n    properties\n      ..add(\n        FlagProperty(\n          'enabled',\n          value: enabled,\n          ifFalse: 'DISABLED',\n          defaultValue: true,\n        ),\n      )\n      ..add(\n        DiagnosticsProperty<HitTestBehavior>(\n          'behavior',\n          behavior,\n          defaultValue: HitTestBehavior.deferToChild,\n        ),\n      )\n      ..add(\n        DiagnosticsProperty<Object?>(\n          'debugLabel',\n          debugLabel,\n          defaultValue: null,\n        ),\n      )\n      ..add(\n        DiagnosticsProperty<Object?>('groupId', groupId, defaultValue: null),\n      );\n  }\n\n  @override\n  void updateRenderObject(\n    BuildContext context,\n    covariant ShadMouseAreaRenderBox renderObject,\n  ) {\n    renderObject\n      ..registry = MouseAreaRegistry.maybeOf(context)\n      ..enabled = enabled\n      ..behavior = behavior\n      ..groupId = groupId\n      ..onEnter = onEnter\n      ..onExit = onExit;\n    if (!kReleaseMode) {\n      renderObject.debugLabel = debugLabel;\n    }\n  }\n}\n\nclass ShadMouseAreaRenderBox extends RenderProxyBoxWithHitTestBehavior {\n  /// Creates a [ShadMouseAreaRenderBox].\n  ShadMouseAreaRenderBox({\n    this.onEnter,\n    this.onExit,\n    MouseAreaRegistry? registry,\n    bool enabled = true,\n    super.behavior = HitTestBehavior.deferToChild,\n    bool validForMouseTracker = true,\n    Object? groupId,\n    String? debugLabel,\n    MouseCursor cursor = MouseCursor.defer,\n  })  : _registry = registry,\n        _cursor = cursor,\n        _validForMouseTracker = validForMouseTracker,\n        _enabled = enabled,\n        _groupId = groupId,\n        debugLabel = kReleaseMode ? null : debugLabel;\n\n  bool _isRegistered = false;\n\n  /// A label used in debug builds. Will be null in release builds.\n  String? debugLabel;\n\n  bool _enabled;\n\n  Object? _groupId;\n  MouseAreaRegistry? _registry;\n  bool _validForMouseTracker;\n\n  MouseCursor _cursor;\n  PointerEnterEventListener? onEnter;\n  PointerExitEventListener? onExit;\n\n  MouseCursor get cursor => _cursor;\n  set cursor(MouseCursor value) {\n    if (_cursor != value) {\n      _cursor = value;\n      // A repaint is needed in order to trigger a device update of\n      // [MouseTracker] so that this new value can be found.\n      markNeedsPaint();\n    }\n  }\n\n  /// Whether or not this region should participate in the composite region.\n  bool get enabled => _enabled;\n\n  set enabled(bool value) {\n    if (_enabled != value) {\n      _enabled = value;\n      markNeedsLayout();\n    }\n  }\n\n  /// An optional group ID that groups [ShadMouseAreaRenderBox]s together so\n  /// that they operate as one region. If any member of a group is hit by a\n  /// particular hover, then all members will have their\n  /// [onEnter] or [onExit] called.\n  ///\n  /// If the group id is null, then only this region is hit tested.\n  Object? get groupId => _groupId;\n\n  set groupId(Object? value) {\n    if (_groupId != value) {\n      // If the group changes, we need to unregister and re-register under the\n      // new group. The re-registration happens automatically in layout().\n      if (_isRegistered) {\n        _registry!.unregisterMouseArea(this);\n        _isRegistered = false;\n      }\n      _groupId = value;\n      markNeedsLayout();\n    }\n  }\n\n  /// The registry that this [ShadMouseAreaRenderBox] should register with.\n  ///\n  /// If the [registry] is null, then this region will not be registered\n  /// anywhere, and will not do any tap detection.\n  ///\n  /// A [MouseAreaSurfaceRenderBox] is a [MouseAreaRegistry].\n  MouseAreaRegistry? get registry => _registry;\n\n  set registry(MouseAreaRegistry? value) {\n    if (_registry != value) {\n      if (_isRegistered) {\n        _registry!.unregisterMouseArea(this);\n        _isRegistered = false;\n      }\n      _registry = value;\n      markNeedsLayout();\n    }\n  }\n\n  bool get validForMouseTracker => _validForMouseTracker;\n\n  @override\n  void attach(PipelineOwner owner) {\n    super.attach(owner);\n    _validForMouseTracker = true;\n  }\n\n  @override\n  Size computeSizeForNoChild(BoxConstraints constraints) {\n    return constraints.biggest;\n  }\n\n  @override\n  void debugFillProperties(DiagnosticPropertiesBuilder properties) {\n    super.debugFillProperties(properties);\n    properties\n      ..add(\n        DiagnosticsProperty<String?>(\n          'debugLabel',\n          debugLabel,\n          defaultValue: null,\n        ),\n      )\n      ..add(\n        DiagnosticsProperty<Object?>('groupId', groupId, defaultValue: null),\n      )\n      ..add(\n        FlagProperty(\n          'enabled',\n          value: enabled,\n          ifFalse: 'DISABLED',\n          defaultValue: true,\n        ),\n      );\n  }\n\n  @override\n  void detach() {\n    // It's possible that the renderObject be detached during mouse events\n    // dispatching, set the [MouseTrackerAnnotation.validForMouseTracker] false\n    // to prevent the callbacks from being called.\n    _validForMouseTracker = false;\n    super.detach();\n  }\n\n  @override\n  void dispose() {\n    if (_isRegistered) {\n      _registry!.unregisterMouseArea(this);\n    }\n    super.dispose();\n  }\n\n  @override\n  void layout(Constraints constraints, {bool parentUsesSize = false}) {\n    super.layout(constraints, parentUsesSize: parentUsesSize);\n    if (_registry == null) {\n      return;\n    }\n    if (_isRegistered) {\n      _registry!.unregisterMouseArea(this);\n    }\n    final shouldBeRegistered = _enabled && _registry != null;\n    if (shouldBeRegistered) {\n      _registry!.registerMouseArea(this);\n    }\n    _isRegistered = shouldBeRegistered;\n  }\n}\n\n/// A widget that provides notification of a hover inside or outside of a set of\n/// registered regions, grouped by [ShadMouseArea.groupId], without\n/// participating in the [gesture disambiguation](https://flutter.dev/to/gesture-disambiguation) system.\nclass ShadMouseAreaSurface extends SingleChildRenderObjectWidget {\n  /// Creates a const [RenderTapRegionSurface].\n  ///\n  /// The [child] attribute is required.\n  const ShadMouseAreaSurface({\n    super.key,\n    required Widget super.child,\n  });\n\n  @override\n  RenderObject createRenderObject(BuildContext context) {\n    return MouseAreaSurfaceRenderBox();\n  }\n\n  @override\n  void updateRenderObject(\n    BuildContext context,\n    RenderProxyBoxWithHitTestBehavior renderObject,\n  ) {}\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/popover/shadcn/_portal.dart",
    "content": "import 'package:flutter/material.dart';\n\n/// Notes: The implementation of this page is copied from [flutter_shadcn_ui](https://github.com/nank1ro/flutter-shadcn-ui).\n\n/// The position of the [ShadPortal] in the global coordinate system.\nsealed class ShadAnchorBase {\n  const ShadAnchorBase();\n}\n\n/// Automatically infers the position of the [ShadPortal] in the global\n/// coordinate system adjusting according to the [offset],\n/// [followerAnchor] and [targetAnchor] properties.\n@immutable\nclass ShadAnchorAuto extends ShadAnchorBase {\n  const ShadAnchorAuto({\n    this.offset = Offset.zero,\n    this.followTargetOnResize = true,\n    this.followerAnchor = Alignment.bottomCenter,\n    this.targetAnchor = Alignment.bottomCenter,\n  });\n\n  /// The offset of the overlay from the target widget.\n  final Offset offset;\n\n  /// Whether the overlay is automatically adjusted to follow the target\n  /// widget when the target widget moves dues to a window resize.\n  final bool followTargetOnResize;\n\n  /// The coordinates of the overlay from which the overlay starts, which\n  /// is calculated from the initial [targetAnchor].\n  final Alignment followerAnchor;\n\n  /// The coordinates of the target from which the overlay starts.\n  final Alignment targetAnchor;\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n\n    return other is ShadAnchorAuto &&\n        other.offset == offset &&\n        other.followTargetOnResize == followTargetOnResize &&\n        other.followerAnchor == followerAnchor &&\n        other.targetAnchor == targetAnchor;\n  }\n\n  @override\n  int get hashCode =>\n      offset.hashCode ^\n      followTargetOnResize.hashCode ^\n      followerAnchor.hashCode ^\n      targetAnchor.hashCode;\n}\n\n/// Manually specifies the position of the [ShadPortal] in the global\n/// coordinate system.\n@immutable\nclass ShadAnchor extends ShadAnchorBase {\n  const ShadAnchor({\n    this.childAlignment = Alignment.topLeft,\n    this.overlayAlignment = Alignment.bottomLeft,\n    this.offset = Offset.zero,\n  });\n\n  final Alignment childAlignment;\n  final Alignment overlayAlignment;\n  final Offset offset;\n\n  static const center = ShadAnchor(\n    childAlignment: Alignment.topCenter,\n    overlayAlignment: Alignment.bottomCenter,\n  );\n\n  ShadAnchor copyWith({\n    Alignment? childAlignment,\n    Alignment? overlayAlignment,\n    Offset? offset,\n  }) {\n    return ShadAnchor(\n      childAlignment: childAlignment ?? this.childAlignment,\n      overlayAlignment: overlayAlignment ?? this.overlayAlignment,\n      offset: offset ?? this.offset,\n    );\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n\n    return other is ShadAnchor &&\n        other.childAlignment == childAlignment &&\n        other.overlayAlignment == overlayAlignment &&\n        other.offset == offset;\n  }\n\n  @override\n  int get hashCode {\n    return childAlignment.hashCode ^\n        overlayAlignment.hashCode ^\n        offset.hashCode;\n  }\n}\n\n@immutable\nclass ShadGlobalAnchor extends ShadAnchorBase {\n  const ShadGlobalAnchor(this.offset);\n\n  /// The global offset where the overlay is positioned.\n  final Offset offset;\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n\n    return other is ShadGlobalAnchor && other.offset == offset;\n  }\n\n  @override\n  int get hashCode => offset.hashCode;\n}\n\nclass ShadPortal extends StatefulWidget {\n  const ShadPortal({\n    super.key,\n    required this.child,\n    required this.portalBuilder,\n    required this.visible,\n    required this.anchor,\n  });\n\n  final Widget child;\n  final WidgetBuilder portalBuilder;\n  final bool visible;\n  final ShadAnchorBase anchor;\n\n  @override\n  State<ShadPortal> createState() => _ShadPortalState();\n}\n\nclass _ShadPortalState extends State<ShadPortal> {\n  final layerLink = LayerLink();\n  final overlayPortalController = OverlayPortalController();\n  final overlayKey = GlobalKey();\n\n  @override\n  void initState() {\n    super.initState();\n    updateVisibility();\n  }\n\n  @override\n  void didUpdateWidget(covariant ShadPortal oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    updateVisibility();\n  }\n\n  @override\n  void dispose() {\n    hide();\n    super.dispose();\n  }\n\n  void updateVisibility() {\n    final shouldShow = widget.visible;\n\n    WidgetsBinding.instance.addPostFrameCallback((timer) {\n      shouldShow ? show() : hide();\n    });\n  }\n\n  void hide() {\n    if (overlayPortalController.isShowing) {\n      overlayPortalController.hide();\n    }\n  }\n\n  void show() {\n    if (!overlayPortalController.isShowing) {\n      overlayPortalController.show();\n    }\n  }\n\n  Widget buildAutoPosition(\n    BuildContext context,\n    ShadAnchorAuto anchor,\n  ) {\n    if (anchor.followTargetOnResize) {\n      MediaQuery.sizeOf(context);\n    }\n    final overlayState = Overlay.of(context, debugRequiredFor: widget);\n    final box = this.context.findRenderObject()! as RenderBox;\n    final overlayAncestor =\n        overlayState.context.findRenderObject()! as RenderBox;\n\n    final overlay = overlayKey.currentContext?.findRenderObject() as RenderBox?;\n    final overlaySize = overlay?.size ?? Size.zero;\n\n    final targetOffset = switch (anchor.targetAnchor) {\n      Alignment.topLeft => box.size.topLeft(Offset.zero),\n      Alignment.topCenter => box.size.topCenter(Offset.zero),\n      Alignment.topRight => box.size.topRight(Offset.zero),\n      Alignment.centerLeft => box.size.centerLeft(Offset.zero),\n      Alignment.center => box.size.center(Offset.zero),\n      Alignment.centerRight => box.size.centerRight(Offset.zero),\n      Alignment.bottomLeft => box.size.bottomLeft(Offset.zero),\n      Alignment.bottomCenter => box.size.bottomCenter(Offset.zero),\n      Alignment.bottomRight => box.size.bottomRight(Offset.zero),\n      final alignment => throw Exception(\n          \"\"\"ShadAnchorAuto doesn't support the alignment $alignment you provided\"\"\",\n        ),\n    };\n\n    var followerOffset = switch (anchor.followerAnchor) {\n      Alignment.topLeft => Offset(-overlaySize.width / 2, -overlaySize.height),\n      Alignment.topCenter => Offset(0, -overlaySize.height),\n      Alignment.topRight => Offset(overlaySize.width / 2, -overlaySize.height),\n      Alignment.centerLeft =>\n        Offset(-overlaySize.width / 2, -overlaySize.height / 2),\n      Alignment.center => Offset(0, -overlaySize.height / 2),\n      Alignment.centerRight =>\n        Offset(overlaySize.width / 2, -overlaySize.height / 2),\n      Alignment.bottomLeft => Offset(-overlaySize.width / 2, 0),\n      Alignment.bottomCenter => Offset.zero,\n      Alignment.bottomRight => Offset(overlaySize.width / 2, 0),\n      final alignment => throw Exception(\n          \"\"\"ShadAnchorAuto doesn't support the alignment $alignment you provided\"\"\",\n        ),\n    };\n\n    followerOffset += targetOffset + anchor.offset;\n\n    final target = box.localToGlobal(\n      followerOffset,\n      ancestor: overlayAncestor,\n    );\n\n    if (overlay == null) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        setState(() {});\n      });\n    }\n    return CustomSingleChildLayout(\n      delegate: ShadPositionDelegate(\n        target: target,\n        verticalOffset: 0,\n        preferBelow: true,\n      ),\n      child: KeyedSubtree(\n        key: overlayKey,\n        child: Visibility.maintain(\n          // The overlay layout details are available only after the view is\n          // rendered, in this way we can avoid the flickering effect.\n          visible: overlay != null,\n          child: IgnorePointer(\n            ignoring: overlay == null,\n            child: widget.portalBuilder(context),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget buildManualPosition(\n    BuildContext context,\n    ShadAnchor anchor,\n  ) {\n    return CompositedTransformFollower(\n      link: layerLink,\n      offset: anchor.offset,\n      followerAnchor: anchor.childAlignment,\n      targetAnchor: anchor.overlayAlignment,\n      child: widget.portalBuilder(context),\n    );\n  }\n\n  Widget buildGlobalPosition(\n    BuildContext context,\n    ShadGlobalAnchor anchor,\n  ) {\n    return CustomSingleChildLayout(\n      delegate: ShadPositionDelegate(\n        target: anchor.offset,\n        verticalOffset: 0,\n        preferBelow: true,\n      ),\n      child: widget.portalBuilder(context),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return CompositedTransformTarget(\n      link: layerLink,\n      child: OverlayPortal(\n        controller: overlayPortalController,\n        overlayChildBuilder: (context) {\n          return Material(\n            type: MaterialType.transparency,\n            child: Center(\n              widthFactor: 1,\n              heightFactor: 1,\n              child: switch (widget.anchor) {\n                final ShadAnchorAuto anchor =>\n                  buildAutoPosition(context, anchor),\n                final ShadAnchor anchor => buildManualPosition(context, anchor),\n                final ShadGlobalAnchor anchor =>\n                  buildGlobalPosition(context, anchor),\n              },\n            ),\n          );\n        },\n        child: widget.child,\n      ),\n    );\n  }\n}\n\n/// A delegate for computing the layout of an overlay to be displayed above or\n/// below a target specified in the global coordinate system.\nclass ShadPositionDelegate extends SingleChildLayoutDelegate {\n  /// Creates a delegate for computing the layout of an overlay.\n  ShadPositionDelegate({\n    required this.target,\n    required this.verticalOffset,\n    required this.preferBelow,\n  });\n\n  /// The offset of the target the overlay is positioned near in the global\n  /// coordinate system.\n  final Offset target;\n\n  /// The amount of vertical distance between the target and the displayed\n  /// overlay.\n  final double verticalOffset;\n\n  /// Whether the overlay is displayed below its widget by default.\n  ///\n  /// If there is insufficient space to display the tooltip in the preferred\n  /// direction, the tooltip will be displayed in the opposite direction.\n  final bool preferBelow;\n\n  @override\n  BoxConstraints getConstraintsForChild(BoxConstraints constraints) =>\n      constraints.loosen();\n\n  @override\n  Offset getPositionForChild(Size size, Size childSize) {\n    return positionDependentBox(\n      size: size,\n      childSize: childSize,\n      target: target,\n      verticalOffset: verticalOffset,\n      preferBelow: preferBelow,\n      margin: 0,\n    );\n  }\n\n  @override\n  bool shouldRelayout(ShadPositionDelegate oldDelegate) {\n    return target != oldDelegate.target ||\n        verticalOffset != oldDelegate.verticalOffset ||\n        preferBelow != oldDelegate.preferBelow;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/separator/divider.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/widgets.dart';\n\nclass AFDivider extends StatelessWidget {\n  const AFDivider({\n    super.key,\n    this.axis = Axis.horizontal,\n    this.color,\n    this.thickness = 1.0,\n    this.spacing = 0.0,\n    this.startIndent = 0.0,\n    this.endIndent = 0.0,\n  })  : assert(thickness > 0.0),\n        assert(spacing >= 0.0),\n        assert(startIndent >= 0.0),\n        assert(endIndent >= 0.0);\n\n  final Axis axis;\n  final double thickness;\n  final double spacing;\n  final double startIndent;\n  final double endIndent;\n  final Color? color;\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final color = this.color ?? theme.borderColorScheme.primary;\n\n    return switch (axis) {\n      Axis.horizontal => Container(\n          height: thickness,\n          color: color,\n          margin: EdgeInsetsDirectional.only(\n            start: startIndent,\n            end: endIndent,\n            top: spacing,\n            bottom: spacing,\n          ),\n        ),\n      Axis.vertical => Container(\n          width: thickness,\n          color: color,\n          margin: EdgeInsets.only(\n            left: spacing,\n            right: spacing,\n            top: startIndent,\n            bottom: endIndent,\n          ),\n        ),\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart",
    "content": "import 'package:appflowy_ui/src/theme/theme.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\ntypedef AFTextFieldValidator = (bool result, String errorText) Function(\n  TextEditingController controller,\n);\n\nabstract class AFTextFieldState extends State<AFTextField> {\n  // Error handler\n  void syncError({required String errorText}) {}\n  void clearError() {}\n\n  /// Obscure the text.\n  void syncObscured(bool isObscured) {}\n}\n\nclass AFTextField extends StatefulWidget {\n  const AFTextField({\n    super.key,\n    this.hintText,\n    this.initialText,\n    this.keyboardType,\n    this.validator,\n    this.controller,\n    this.onChanged,\n    this.onSubmitted,\n    this.autoFocus,\n    this.obscureText = false,\n    this.suffixIconBuilder,\n    this.suffixIconConstraints,\n    this.size = AFTextFieldSize.l,\n    this.groupId = EditableText,\n    this.focusNode,\n    this.readOnly = false,\n    this.maxLength,\n  });\n\n  /// The hint text to display when the text field is empty.\n  final String? hintText;\n\n  /// The initial text to display in the text field.\n  final String? initialText;\n\n  /// The type of keyboard to display.\n  final TextInputType? keyboardType;\n\n  /// The size variant of the text field.\n  final AFTextFieldSize size;\n\n  /// The validator to use for the text field.\n  final AFTextFieldValidator? validator;\n\n  /// The controller to use for the text field.\n  ///\n  /// If it's not provided, the text field will use a new controller.\n  final TextEditingController? controller;\n\n  /// The callback to call when the text field changes.\n  final void Function(String)? onChanged;\n\n  /// The callback to call when the text field is submitted.\n  final void Function(String)? onSubmitted;\n\n  /// Enable auto focus.\n  final bool? autoFocus;\n\n  /// Obscure the text.\n  final bool obscureText;\n\n  /// The trailing widget to display.\n  final Widget? Function(BuildContext context, bool isObscured)?\n      suffixIconBuilder;\n\n  /// The size of the suffix icon.\n  final BoxConstraints? suffixIconConstraints;\n\n  /// The group ID for the text field.\n  final Object groupId;\n\n  /// The focus node for the text field.\n  final FocusNode? focusNode;\n\n  /// Readonly.\n  final bool readOnly;\n\n  /// The maximum length of the text field.\n  final int? maxLength;\n\n  @override\n  State<AFTextField> createState() => _AFTextFieldState();\n}\n\nclass _AFTextFieldState extends AFTextFieldState {\n  late final TextEditingController effectiveController;\n\n  bool hasError = false;\n  String errorText = '';\n\n  bool isObscured = false;\n\n  @override\n  void initState() {\n    super.initState();\n\n    effectiveController = widget.controller ?? TextEditingController();\n\n    final initialText = widget.initialText;\n    if (initialText != null) {\n      effectiveController.text = initialText;\n    }\n\n    effectiveController.addListener(_validate);\n\n    isObscured = widget.obscureText;\n  }\n\n  @override\n  void dispose() {\n    effectiveController.removeListener(_validate);\n    if (widget.controller == null) {\n      effectiveController.dispose();\n    }\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = AppFlowyTheme.of(context);\n    final borderRadius = widget.size.borderRadius(theme);\n    final contentPadding = widget.size.contentPadding(theme);\n\n    final errorBorderColor = theme.borderColorScheme.errorThick;\n    final defaultBorderColor = theme.borderColorScheme.primary;\n\n    final border = OutlineInputBorder(\n      borderSide: BorderSide(\n        color: hasError ? errorBorderColor : defaultBorderColor,\n      ),\n      borderRadius: borderRadius,\n    );\n\n    final enabledBorder = OutlineInputBorder(\n      borderSide: BorderSide(\n        color: hasError ? errorBorderColor : defaultBorderColor,\n      ),\n      borderRadius: borderRadius,\n    );\n\n    final focusedBorder = OutlineInputBorder(\n      borderSide: BorderSide(\n        color: widget.readOnly\n            ? defaultBorderColor\n            : hasError\n                ? errorBorderColor\n                : theme.borderColorScheme.themeThick,\n      ),\n      borderRadius: borderRadius,\n    );\n\n    final errorBorder = OutlineInputBorder(\n      borderSide: BorderSide(\n        color: errorBorderColor,\n      ),\n      borderRadius: borderRadius,\n    );\n\n    final focusedErrorBorder = OutlineInputBorder(\n      borderSide: BorderSide(\n        color: errorBorderColor,\n      ),\n      borderRadius: borderRadius,\n    );\n\n    Widget child = TextField(\n      groupId: widget.groupId,\n      focusNode: widget.focusNode,\n      controller: effectiveController,\n      keyboardType: widget.keyboardType,\n      readOnly: widget.readOnly,\n      style: theme.textStyle.body.standard(\n        color: theme.textColorScheme.primary,\n      ),\n      obscureText: isObscured,\n      onChanged: widget.onChanged,\n      onSubmitted: widget.onSubmitted,\n      autofocus: widget.autoFocus ?? false,\n      maxLength: widget.maxLength,\n      maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,\n      decoration: InputDecoration(\n        hintText: widget.hintText,\n        hintStyle: theme.textStyle.body.standard(\n          color: theme.textColorScheme.tertiary,\n        ),\n        isDense: true,\n        constraints: BoxConstraints(),\n        contentPadding: contentPadding,\n        border: border,\n        enabledBorder: enabledBorder,\n        focusedBorder: focusedBorder,\n        errorBorder: errorBorder,\n        focusedErrorBorder: focusedErrorBorder,\n        hoverColor: theme.borderColorScheme.primaryHover,\n        suffixIcon: widget.suffixIconBuilder?.call(context, isObscured),\n        suffixIconConstraints: widget.suffixIconConstraints,\n      ),\n    );\n\n    if (hasError && errorText.isNotEmpty) {\n      child = Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          child,\n          SizedBox(height: theme.spacing.xs),\n          Text(\n            errorText,\n            style: theme.textStyle.caption.standard(\n              color: theme.textColorScheme.error,\n            ),\n          ),\n        ],\n      );\n    }\n\n    return child;\n  }\n\n  void _validate() {\n    final validator = widget.validator;\n    if (validator != null) {\n      final result = validator(effectiveController);\n      setState(() {\n        hasError = result.$1;\n        errorText = result.$2;\n      });\n    }\n  }\n\n  @override\n  void syncError({\n    required String errorText,\n  }) {\n    setState(() {\n      hasError = true;\n      this.errorText = errorText;\n    });\n  }\n\n  @override\n  void clearError() {\n    setState(() {\n      hasError = false;\n      errorText = '';\n    });\n  }\n\n  @override\n  void syncObscured(bool isObscured) {\n    setState(() {\n      this.isObscured = isObscured;\n    });\n  }\n}\n\nenum AFTextFieldSize {\n  m,\n  l;\n\n  EdgeInsetsGeometry contentPadding(AppFlowyThemeData theme) {\n    return EdgeInsets.symmetric(\n      vertical: switch (this) {\n        AFTextFieldSize.m => theme.spacing.s,\n        AFTextFieldSize.l => 10.0,\n      },\n      horizontal: theme.spacing.m,\n    );\n  }\n\n  BorderRadius borderRadius(AppFlowyThemeData theme) {\n    return BorderRadius.circular(\n      switch (this) {\n        AFTextFieldSize.m => theme.borderRadius.m,\n        AFTextFieldSize.l => 10.0,\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart",
    "content": "import 'package:appflowy_ui/src/theme/theme.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\n\nclass AppFlowyTheme extends StatelessWidget {\n  const AppFlowyTheme({\n    super.key,\n    required this.data,\n    required this.child,\n  });\n\n  final AppFlowyThemeData data;\n  final Widget child;\n\n  static AppFlowyThemeData of(BuildContext context, {bool listen = true}) {\n    final provider = maybeOf(context, listen: listen);\n    if (provider == null) {\n      throw FlutterError(\n        '''\n        AppFlowyTheme.of() called with a context that does not contain a AppFlowyTheme.\\n\n        No AppFlowyTheme ancestor could be found starting from the context that was passed to AppFlowyTheme.of().\n        This can happen because you do not have a AppFlowyTheme widget (which introduces a AppFlowyTheme),\n        or it can happen if the context you use comes from a widget above this widget.\\n\n        The context used was: $context''',\n      );\n    }\n    return provider;\n  }\n\n  static AppFlowyThemeData? maybeOf(\n    BuildContext context, {\n    bool listen = true,\n  }) {\n    if (listen) {\n      return context\n          .dependOnInheritedWidgetOfExactType<AppFlowyInheritedTheme>()\n          ?.themeData;\n    }\n    final provider = context\n        .getElementForInheritedWidgetOfExactType<AppFlowyInheritedTheme>()\n        ?.widget;\n\n    return (provider as AppFlowyInheritedTheme?)?.themeData;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyInheritedTheme(\n      themeData: data,\n      child: child,\n    );\n  }\n}\n\nclass AppFlowyInheritedTheme extends InheritedTheme {\n  const AppFlowyInheritedTheme({\n    super.key,\n    required this.themeData,\n    required super.child,\n  });\n\n  final AppFlowyThemeData themeData;\n\n  @override\n  Widget wrap(BuildContext context, Widget child) {\n    return AppFlowyTheme(data: themeData, child: child);\n  }\n\n  @override\n  bool updateShouldNotify(AppFlowyInheritedTheme oldWidget) =>\n      themeData != oldWidget.themeData;\n}\n\n/// An interpolation between two [AppFlowyThemeData]s.\n///\n/// This class specializes the interpolation of [Tween<AppFlowyThemeData>] to\n/// call the [AppFlowyThemeData.lerp] method.\n///\n/// See [Tween] for a discussion on how to use interpolation objects.\nclass AppFlowyThemeDataTween extends Tween<AppFlowyThemeData> {\n  /// Creates a [AppFlowyThemeData] tween.\n  ///\n  /// The [begin] and [end] properties must be non-null before the tween is\n  /// first used, but the arguments can be null if the values are going to be\n  /// filled in later.\n  AppFlowyThemeDataTween({super.begin, super.end});\n\n  @override\n  AppFlowyThemeData lerp(double t) => AppFlowyThemeData.lerp(begin!, end!, t);\n}\n\nclass AnimatedAppFlowyTheme extends ImplicitlyAnimatedWidget {\n  /// Creates an animated theme.\n  ///\n  /// By default, the theme transition uses a linear curve.\n  const AnimatedAppFlowyTheme({\n    super.key,\n    required this.data,\n    super.curve,\n    super.duration = kThemeAnimationDuration,\n    super.onEnd,\n    required this.child,\n  });\n\n  /// Specifies the color and typography values for descendant widgets.\n  final AppFlowyThemeData data;\n\n  /// The widget below this widget in the tree.\n  ///\n  /// {@macro flutter.widgets.ProxyWidget.child}\n  final Widget child;\n\n  @override\n  AnimatedWidgetBaseState<AnimatedAppFlowyTheme> createState() =>\n      _AnimatedThemeState();\n}\n\nclass _AnimatedThemeState\n    extends AnimatedWidgetBaseState<AnimatedAppFlowyTheme> {\n  AppFlowyThemeDataTween? data;\n\n  @override\n  void forEachTween(TweenVisitor<dynamic> visitor) {\n    data = visitor(\n      data,\n      widget.data,\n      (dynamic value) =>\n          AppFlowyThemeDataTween(begin: value as AppFlowyThemeData),\n    )! as AppFlowyThemeDataTween;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AppFlowyTheme(\n      data: data!.evaluate(animation),\n      child: widget.child,\n    );\n  }\n\n  @override\n  void debugFillProperties(DiagnosticPropertiesBuilder description) {\n    super.debugFillProperties(description);\n    description.add(\n      DiagnosticsProperty<AppFlowyThemeDataTween>(\n        'data',\n        data,\n        showName: false,\n        defaultValue: null,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart",
    "content": "// ignore_for_file: constant_identifier_names, non_constant_identifier_names\n//\n// AUTO-GENERATED - DO NOT EDIT DIRECTLY\n//\n// This file is auto-generated by the generate_theme.dart script\n// Generation time: 2025-05-15T16:35:45.557032\n//\n// To modify these colors, edit the source JSON files and run the script:\n//\n// dart run script/generate_theme.dart\n//\nimport 'package:flutter/material.dart';\n\nclass AppFlowyPrimitiveTokens {\n  AppFlowyPrimitiveTokens._();\n\n  /// #f8faff\n  static Color get neutral100 => Color(0xFFF8FAFF);\n\n  /// #e4e8f5\n  static Color get neutral200 => Color(0xFFE4E8F5);\n\n  /// #ced3e6\n  static Color get neutral300 => Color(0xFFCED3E6);\n\n  /// #b5bbd3\n  static Color get neutral400 => Color(0xFFB5BBD3);\n\n  /// #989eb7\n  static Color get neutral500 => Color(0xFF989EB7);\n\n  /// #6f748c\n  static Color get neutral600 => Color(0xFF6F748C);\n\n  /// #54596e\n  static Color get neutral700 => Color(0xFF54596E);\n\n  /// #3d404f\n  static Color get neutral800 => Color(0xFF3D404F);\n\n  /// #363845\n  static Color get neutral830 => Color(0xFF363845);\n\n  /// #32343f\n  static Color get neutral850 => Color(0xFF32343F);\n\n  /// #272930\n  static Color get neutral900 => Color(0xFF272930);\n\n  /// #21232a\n  static Color get neutral1000 => Color(0xFF21232A);\n\n  /// #000000\n  static Color get neutralBlack => Color(0xFF000000);\n\n  /// #00000099\n  static Color get neutralAlphaBlack60 => Color(0x99000000);\n\n  /// #ffffff\n  static Color get neutralWhite => Color(0xFFFFFFFF);\n\n  /// #ffffff00\n  static Color get neutralAlphaWhite0 => Color(0x00FFFFFF);\n\n  /// #f9fafd0d\n  static Color get neutralAlphaGrey10005 => Color(0x0DF9FAFD);\n\n  /// #f9fafd1a\n  static Color get neutralAlphaGrey10010 => Color(0x1AF9FAFD);\n\n  /// #1f23290d\n  static Color get neutralAlphaGrey100005 => Color(0x0D1F2329);\n\n  /// #1f23291a\n  static Color get neutralAlphaGrey100010 => Color(0x1A1F2329);\n\n  /// #e3f6ff\n  static Color get blue100 => Color(0xFFE3F6FF);\n\n  /// #a9e2ff\n  static Color get blue200 => Color(0xFFA9E2FF);\n\n  /// #80d2ff\n  static Color get blue300 => Color(0xFF80D2FF);\n\n  /// #4ec1ff\n  static Color get blue400 => Color(0xFF4EC1FF);\n\n  /// #00b5ff\n  static Color get blue500 => Color(0xFF00B5FF);\n\n  /// #0092d6\n  static Color get blue600 => Color(0xFF0092D6);\n\n  /// #0078c0\n  static Color get blue700 => Color(0xFF0078C0);\n\n  /// #0065a9\n  static Color get blue800 => Color(0xFF0065A9);\n\n  /// #00508f\n  static Color get blue900 => Color(0xFF00508F);\n\n  /// #003c77\n  static Color get blue1000 => Color(0xFF003C77);\n\n  /// #00b5ff26\n  static Color get blueAlphaBlue50015 => Color(0x2600B5FF);\n\n  /// #00b5ff33\n  static Color get blueAlphaBlue50020 => Color(0x3300B5FF);\n\n  /// #ecf9f5\n  static Color get green100 => Color(0xFFECF9F5);\n\n  /// #c3e5d8\n  static Color get green200 => Color(0xFFC3E5D8);\n\n  /// #9ad1bc\n  static Color get green300 => Color(0xFF9AD1BC);\n\n  /// #71bd9f\n  static Color get green400 => Color(0xFF71BD9F);\n\n  /// #48a982\n  static Color get green500 => Color(0xFF48A982);\n\n  /// #248569\n  static Color get green600 => Color(0xFF248569);\n\n  /// #29725d\n  static Color get green700 => Color(0xFF29725D);\n\n  /// #2e6050\n  static Color get green800 => Color(0xFF2E6050);\n\n  /// #305548\n  static Color get green900 => Color(0xFF305548);\n\n  /// #305244\n  static Color get green1000 => Color(0xFF305244);\n\n  /// #f1e0ff\n  static Color get purple100 => Color(0xFFF1E0FF);\n\n  /// #e1b3ff\n  static Color get purple200 => Color(0xFFE1B3FF);\n\n  /// #d185ff\n  static Color get purple300 => Color(0xFFD185FF);\n\n  /// #bc58ff\n  static Color get purple400 => Color(0xFFBC58FF);\n\n  /// #9327ff\n  static Color get purple500 => Color(0xFF9327FF);\n\n  /// #7a1dcc\n  static Color get purple600 => Color(0xFF7A1DCC);\n\n  /// #6617b3\n  static Color get purple700 => Color(0xFF6617B3);\n\n  /// #55138f\n  static Color get purple800 => Color(0xFF55138F);\n\n  /// #470c72\n  static Color get purple900 => Color(0xFF470C72);\n\n  /// #380758\n  static Color get purple1000 => Color(0xFF380758);\n\n  /// #ffe5ef\n  static Color get magenta100 => Color(0xFFFFE5EF);\n\n  /// #ffb8d1\n  static Color get magenta200 => Color(0xFFFFB8D1);\n\n  /// #ff8ab2\n  static Color get magenta300 => Color(0xFFFF8AB2);\n\n  /// #ff5c93\n  static Color get magenta400 => Color(0xFFFF5C93);\n\n  /// #fb006d\n  static Color get magenta500 => Color(0xFFFB006D);\n\n  /// #d2005f\n  static Color get magenta600 => Color(0xFFD2005F);\n\n  /// #d2005f\n  static Color get magenta700 => Color(0xFFD2005F);\n\n  /// #850040\n  static Color get magenta800 => Color(0xFF850040);\n\n  /// #610031\n  static Color get magenta900 => Color(0xFF610031);\n\n  /// #400022\n  static Color get magenta1000 => Color(0xFF400022);\n\n  /// #ffd2dd\n  static Color get red100 => Color(0xFFFFD2DD);\n\n  /// #ffa5b4\n  static Color get red200 => Color(0xFFFFA5B4);\n\n  /// #ff7d87\n  static Color get red300 => Color(0xFFFF7D87);\n\n  /// #ff5050\n  static Color get red400 => Color(0xFFFF5050);\n\n  /// #f33641\n  static Color get red500 => Color(0xFFF33641);\n\n  /// #e71d32\n  static Color get red600 => Color(0xFFE71D32);\n\n  /// #ad1625\n  static Color get red700 => Color(0xFFAD1625);\n\n  /// #8c101c\n  static Color get red800 => Color(0xFF8C101C);\n\n  /// #6e0a1e\n  static Color get red900 => Color(0xFF6E0A1E);\n\n  /// #4c0a17\n  static Color get red1000 => Color(0xFF4C0A17);\n\n  /// #f336411a\n  static Color get redAlphaRed50010 => Color(0x1AF33641);\n\n  /// #fff3d5\n  static Color get orange100 => Color(0xFFFFF3D5);\n\n  /// #ffe4ab\n  static Color get orange200 => Color(0xFFFFE4AB);\n\n  /// #ffd181\n  static Color get orange300 => Color(0xFFFFD181);\n\n  /// #ffbe62\n  static Color get orange400 => Color(0xFFFFBE62);\n\n  /// #ffa02e\n  static Color get orange500 => Color(0xFFFFA02E);\n\n  /// #db7e21\n  static Color get orange600 => Color(0xFFDB7E21);\n\n  /// #b75f17\n  static Color get orange700 => Color(0xFFB75F17);\n\n  /// #93450e\n  static Color get orange800 => Color(0xFF93450E);\n\n  /// #7a3108\n  static Color get orange900 => Color(0xFF7A3108);\n\n  /// #602706\n  static Color get orange1000 => Color(0xFF602706);\n\n  /// #fff9b2\n  static Color get yellow100 => Color(0xFFFFF9B2);\n\n  /// #ffec66\n  static Color get yellow200 => Color(0xFFFFEC66);\n\n  /// #ffdf1a\n  static Color get yellow300 => Color(0xFFFFDF1A);\n\n  /// #ffcc00\n  static Color get yellow400 => Color(0xFFFFCC00);\n\n  /// #ffce00\n  static Color get yellow500 => Color(0xFFFFCE00);\n\n  /// #e6b800\n  static Color get yellow600 => Color(0xFFE6B800);\n\n  /// #cc9f00\n  static Color get yellow700 => Color(0xFFCC9F00);\n\n  /// #b38a00\n  static Color get yellow800 => Color(0xFFB38A00);\n\n  /// #9a7500\n  static Color get yellow900 => Color(0xFF9A7500);\n\n  /// #7f6200\n  static Color get yellow1000 => Color(0xFF7F6200);\n\n  /// #fcf2f2\n  static Color get subtleColorRose100 => Color(0xFFFCF2F2);\n\n  /// #fae3e3\n  static Color get subtleColorRose200 => Color(0xFFFAE3E3);\n\n  /// #fad9d9\n  static Color get subtleColorRose300 => Color(0xFFFAD9D9);\n\n  /// #edadad\n  static Color get subtleColorRose400 => Color(0xFFEDADAD);\n\n  /// #cc4e4e\n  static Color get subtleColorRose500 => Color(0xFFCC4E4E);\n\n  /// #702828\n  static Color get subtleColorRose600 => Color(0xFF702828);\n\n  /// #fcf4f0\n  static Color get subtleColorPapaya100 => Color(0xFFFCF4F0);\n\n  /// #fae8de\n  static Color get subtleColorPapaya200 => Color(0xFFFAE8DE);\n\n  /// #fadfd2\n  static Color get subtleColorPapaya300 => Color(0xFFFADFD2);\n\n  /// #f0bda3\n  static Color get subtleColorPapaya400 => Color(0xFFF0BDA3);\n\n  /// #d67240\n  static Color get subtleColorPapaya500 => Color(0xFFD67240);\n\n  /// #6b3215\n  static Color get subtleColorPapaya600 => Color(0xFF6B3215);\n\n  /// #fff7ed\n  static Color get subtleColorTangerine100 => Color(0xFFFFF7ED);\n\n  /// #fcedd9\n  static Color get subtleColorTangerine200 => Color(0xFFFCEDD9);\n\n  /// #fae5ca\n  static Color get subtleColorTangerine300 => Color(0xFFFAE5CA);\n\n  /// #f2cb99\n  static Color get subtleColorTangerine400 => Color(0xFFF2CB99);\n\n  /// #db8f2c\n  static Color get subtleColorTangerine500 => Color(0xFFDB8F2C);\n\n  /// #613b0a\n  static Color get subtleColorTangerine600 => Color(0xFF613B0A);\n\n  /// #fff9ec\n  static Color get subtleColorMango100 => Color(0xFFFFF9EC);\n\n  /// #fcf1d7\n  static Color get subtleColorMango200 => Color(0xFFFCF1D7);\n\n  /// #fae9c3\n  static Color get subtleColorMango300 => Color(0xFFFAE9C3);\n\n  /// #f5d68e\n  static Color get subtleColorMango400 => Color(0xFFF5D68E);\n\n  /// #e0a416\n  static Color get subtleColorMango500 => Color(0xFFE0A416);\n\n  /// #5c4102\n  static Color get subtleColorMango600 => Color(0xFF5C4102);\n\n  /// #fffbe8\n  static Color get subtleColorLemon100 => Color(0xFFFFFBE8);\n\n  /// #fcf5cf\n  static Color get subtleColorLemon200 => Color(0xFFFCF5CF);\n\n  /// #faefb9\n  static Color get subtleColorLemon300 => Color(0xFFFAEFB9);\n\n  /// #f5e282\n  static Color get subtleColorLemon400 => Color(0xFFF5E282);\n\n  /// #e0bb00\n  static Color get subtleColorLemon500 => Color(0xFFE0BB00);\n\n  /// #574800\n  static Color get subtleColorLemon600 => Color(0xFF574800);\n\n  /// #f9fae6\n  static Color get subtleColorOlive100 => Color(0xFFF9FAE6);\n\n  /// #f6f7d0\n  static Color get subtleColorOlive200 => Color(0xFFF6F7D0);\n\n  /// #f0f2b3\n  static Color get subtleColorOlive300 => Color(0xFFF0F2B3);\n\n  /// #dbde83\n  static Color get subtleColorOlive400 => Color(0xFFDBDE83);\n\n  /// #adb204\n  static Color get subtleColorOlive500 => Color(0xFFADB204);\n\n  /// #4a4c03\n  static Color get subtleColorOlive600 => Color(0xFF4A4C03);\n\n  /// #f6f9e6\n  static Color get subtleColorLime100 => Color(0xFFF6F9E6);\n\n  /// #eef5ce\n  static Color get subtleColorLime200 => Color(0xFFEEF5CE);\n\n  /// #e7f0bb\n  static Color get subtleColorLime300 => Color(0xFFE7F0BB);\n\n  /// #cfdb91\n  static Color get subtleColorLime400 => Color(0xFFCFDB91);\n\n  /// #92a822\n  static Color get subtleColorLime500 => Color(0xFF92A822);\n\n  /// #414d05\n  static Color get subtleColorLime600 => Color(0xFF414D05);\n\n  /// #f4faeb\n  static Color get subtleColorGrass100 => Color(0xFFF4FAEB);\n\n  /// #e9f5d7\n  static Color get subtleColorGrass200 => Color(0xFFE9F5D7);\n\n  /// #def0c5\n  static Color get subtleColorGrass300 => Color(0xFFDEF0C5);\n\n  /// #bfd998\n  static Color get subtleColorGrass400 => Color(0xFFBFD998);\n\n  /// #75a828\n  static Color get subtleColorGrass500 => Color(0xFF75A828);\n\n  /// #334d0c\n  static Color get subtleColorGrass600 => Color(0xFF334D0C);\n\n  /// #f1faf0\n  static Color get subtleColorForest100 => Color(0xFFF1FAF0);\n\n  /// #e2f5df\n  static Color get subtleColorForest200 => Color(0xFFE2F5DF);\n\n  /// #d7f0d3\n  static Color get subtleColorForest300 => Color(0xFFD7F0D3);\n\n  /// #a8d6a1\n  static Color get subtleColorForest400 => Color(0xFFA8D6A1);\n\n  /// #49a33b\n  static Color get subtleColorForest500 => Color(0xFF49A33B);\n\n  /// #1e4f16\n  static Color get subtleColorForest600 => Color(0xFF1E4F16);\n\n  /// #f0faf6\n  static Color get subtleColorJade100 => Color(0xFFF0FAF6);\n\n  /// #dff5eb\n  static Color get subtleColorJade200 => Color(0xFFDFF5EB);\n\n  /// #cef0e1\n  static Color get subtleColorJade300 => Color(0xFFCEF0E1);\n\n  /// #90d1b5\n  static Color get subtleColorJade400 => Color(0xFF90D1B5);\n\n  /// #1c9963\n  static Color get subtleColorJade500 => Color(0xFF1C9963);\n\n  /// #075231\n  static Color get subtleColorJade600 => Color(0xFF075231);\n\n  /// #f0f9fa\n  static Color get subtleColorAqua100 => Color(0xFFF0F9FA);\n\n  /// #dff3f5\n  static Color get subtleColorAqua200 => Color(0xFFDFF3F5);\n\n  /// #ccecf0\n  static Color get subtleColorAqua300 => Color(0xFFCCECF0);\n\n  /// #83ccd4\n  static Color get subtleColorAqua400 => Color(0xFF83CCD4);\n\n  /// #008e9e\n  static Color get subtleColorAqua500 => Color(0xFF008E9E);\n\n  /// #004e57\n  static Color get subtleColorAqua600 => Color(0xFF004E57);\n\n  /// #f0f6fa\n  static Color get subtleColorAzure100 => Color(0xFFF0F6FA);\n\n  /// #e1eef7\n  static Color get subtleColorAzure200 => Color(0xFFE1EEF7);\n\n  /// #d3e6f5\n  static Color get subtleColorAzure300 => Color(0xFFD3E6F5);\n\n  /// #88c0eb\n  static Color get subtleColorAzure400 => Color(0xFF88C0EB);\n\n  /// #0877cc\n  static Color get subtleColorAzure500 => Color(0xFF0877CC);\n\n  /// #154469\n  static Color get subtleColorAzure600 => Color(0xFF154469);\n\n  /// #f0f3fa\n  static Color get subtleColorDenim100 => Color(0xFFF0F3FA);\n\n  /// #e3ebfa\n  static Color get subtleColorDenim200 => Color(0xFFE3EBFA);\n\n  /// #d7e2f7\n  static Color get subtleColorDenim300 => Color(0xFFD7E2F7);\n\n  /// #9ab6ed\n  static Color get subtleColorDenim400 => Color(0xFF9AB6ED);\n\n  /// #3267d1\n  static Color get subtleColorDenim500 => Color(0xFF3267D1);\n\n  /// #223c70\n  static Color get subtleColorDenim600 => Color(0xFF223C70);\n\n  /// #f2f2fc\n  static Color get subtleColorMauve100 => Color(0xFFF2F2FC);\n\n  /// #e6e6fa\n  static Color get subtleColorMauve200 => Color(0xFFE6E6FA);\n\n  /// #dcdcf7\n  static Color get subtleColorMauve300 => Color(0xFFDCDCF7);\n\n  /// #aeaef5\n  static Color get subtleColorMauve400 => Color(0xFFAEAEF5);\n\n  /// #5555e0\n  static Color get subtleColorMauve500 => Color(0xFF5555E0);\n\n  /// #36366b\n  static Color get subtleColorMauve600 => Color(0xFF36366B);\n\n  /// #f6f3fc\n  static Color get subtleColorLavender100 => Color(0xFFF6F3FC);\n\n  /// #ebe3fa\n  static Color get subtleColorLavender200 => Color(0xFFEBE3FA);\n\n  /// #e4daf7\n  static Color get subtleColorLavender300 => Color(0xFFE4DAF7);\n\n  /// #c1aaf0\n  static Color get subtleColorLavender400 => Color(0xFFC1AAF0);\n\n  /// #8153db\n  static Color get subtleColorLavender500 => Color(0xFF8153DB);\n\n  /// #462f75\n  static Color get subtleColorLavender600 => Color(0xFF462F75);\n\n  /// #f7f0fa\n  static Color get subtleColorLilac100 => Color(0xFFF7F0FA);\n\n  /// #f0e1f7\n  static Color get subtleColorLilac200 => Color(0xFFF0E1F7);\n\n  /// #edd7f7\n  static Color get subtleColorLilac300 => Color(0xFFEDD7F7);\n\n  /// #d3a9e8\n  static Color get subtleColorLilac400 => Color(0xFFD3A9E8);\n\n  /// #9e4cc7\n  static Color get subtleColorLilac500 => Color(0xFF9E4CC7);\n\n  /// #562d6b\n  static Color get subtleColorLilac600 => Color(0xFF562D6B);\n\n  /// #faf0fa\n  static Color get subtleColorMallow100 => Color(0xFFFAF0FA);\n\n  /// #f5e1f4\n  static Color get subtleColorMallow200 => Color(0xFFF5E1F4);\n\n  /// #f5d7f4\n  static Color get subtleColorMallow300 => Color(0xFFF5D7F4);\n\n  /// #dea4dc\n  static Color get subtleColorMallow400 => Color(0xFFDEA4DC);\n\n  /// #b240af\n  static Color get subtleColorMallow500 => Color(0xFFB240AF);\n\n  /// #632861\n  static Color get subtleColorMallow600 => Color(0xFF632861);\n\n  /// #f9eff3\n  static Color get subtleColorCamellia100 => Color(0xFFF9EFF3);\n\n  /// #f7e1eb\n  static Color get subtleColorCamellia200 => Color(0xFFF7E1EB);\n\n  /// #f7d7e5\n  static Color get subtleColorCamellia300 => Color(0xFFF7D7E5);\n\n  /// #e5a3c0\n  static Color get subtleColorCamellia400 => Color(0xFFE5A3C0);\n\n  /// #c24279\n  static Color get subtleColorCamellia500 => Color(0xFFC24279);\n\n  /// #6e2343\n  static Color get subtleColorCamellia600 => Color(0xFF6E2343);\n\n  /// #f5f5f5\n  static Color get subtleColorSmoke100 => Color(0xFFF5F5F5);\n\n  /// #e8e8e8\n  static Color get subtleColorSmoke200 => Color(0xFFE8E8E8);\n\n  /// #dedede\n  static Color get subtleColorSmoke300 => Color(0xFFDEDEDE);\n\n  /// #b8b8b8\n  static Color get subtleColorSmoke400 => Color(0xFFB8B8B8);\n\n  /// #6e6e6e\n  static Color get subtleColorSmoke500 => Color(0xFF6E6E6E);\n\n  /// #404040\n  static Color get subtleColorSmoke600 => Color(0xFF404040);\n\n  /// #f2f4f7\n  static Color get subtleColorIron100 => Color(0xFFF2F4F7);\n\n  /// #e6e9f0\n  static Color get subtleColorIron200 => Color(0xFFE6E9F0);\n\n  /// #dadee5\n  static Color get subtleColorIron300 => Color(0xFFDADEE5);\n\n  /// #b0b5bf\n  static Color get subtleColorIron400 => Color(0xFFB0B5BF);\n\n  /// #666f80\n  static Color get subtleColorIron500 => Color(0xFF666F80);\n\n  /// #394152\n  static Color get subtleColorIron600 => Color(0xFF394152);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart",
    "content": "// ignore_for_file: constant_identifier_names, non_constant_identifier_names\n//\n// AUTO-GENERATED - DO NOT EDIT DIRECTLY\n//\n// This file is auto-generated by the generate_theme.dart script\n// Generation time: 2025-05-15T16:35:45.572178\n//\n// To modify these colors, edit the source JSON files and run the script:\n//\n// dart run script/generate_theme.dart\n//\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport '../shared.dart';\n\nclass AppFlowyDefaultTheme implements AppFlowyThemeBuilder {\n  @override\n  AppFlowyThemeData light({\n    String? fontFamily,\n  }) {\n    final textStyle = AppFlowyBaseTextStyle.customFontFamily(fontFamily ?? '');\n    final borderRadius = AppFlowySharedTokens.buildBorderRadius();\n    final spacing = AppFlowySharedTokens.buildSpacing();\n    final shadow = AppFlowySharedTokens.buildShadow(Brightness.light);\n\n    final textColorScheme = AppFlowyTextColorScheme(\n      primary: AppFlowyPrimitiveTokens.neutral1000,\n      secondary: AppFlowyPrimitiveTokens.neutral600,\n      tertiary: AppFlowyPrimitiveTokens.neutral500,\n      quaternary: AppFlowyPrimitiveTokens.neutral200,\n      onFill: AppFlowyPrimitiveTokens.neutralWhite,\n      action: AppFlowyPrimitiveTokens.blue600,\n      actionHover: AppFlowyPrimitiveTokens.blue700,\n      info: AppFlowyPrimitiveTokens.blue600,\n      infoHover: AppFlowyPrimitiveTokens.blue700,\n      success: AppFlowyPrimitiveTokens.green600,\n      successHover: AppFlowyPrimitiveTokens.green700,\n      warning: AppFlowyPrimitiveTokens.orange600,\n      warningHover: AppFlowyPrimitiveTokens.orange700,\n      error: AppFlowyPrimitiveTokens.red600,\n      errorHover: AppFlowyPrimitiveTokens.red700,\n      featured: AppFlowyPrimitiveTokens.purple500,\n      featuredHover: AppFlowyPrimitiveTokens.purple600,\n    );\n\n    final iconColorScheme = AppFlowyIconColorScheme(\n      primary: AppFlowyPrimitiveTokens.neutral1000,\n      secondary: AppFlowyPrimitiveTokens.neutral600,\n      tertiary: AppFlowyPrimitiveTokens.neutral400,\n      quaternary: AppFlowyPrimitiveTokens.neutral200,\n      infoThick: AppFlowyPrimitiveTokens.blue600,\n      infoThickHover: AppFlowyPrimitiveTokens.blue700,\n      successThick: AppFlowyPrimitiveTokens.green600,\n      successThickHover: AppFlowyPrimitiveTokens.green700,\n      warningThick: AppFlowyPrimitiveTokens.orange600,\n      warningThickHover: AppFlowyPrimitiveTokens.orange700,\n      errorThick: AppFlowyPrimitiveTokens.red600,\n      errorThickHover: AppFlowyPrimitiveTokens.red700,\n      featuredThick: AppFlowyPrimitiveTokens.purple500,\n      featuredThickHover: AppFlowyPrimitiveTokens.purple600,\n      onFill: AppFlowyPrimitiveTokens.neutralWhite,\n    );\n\n    final borderColorScheme = AppFlowyBorderColorScheme(\n      primary: AppFlowyPrimitiveTokens.neutral200,\n      primaryHover: AppFlowyPrimitiveTokens.neutral300,\n      secondary: AppFlowyPrimitiveTokens.neutral800,\n      secondaryHover: AppFlowyPrimitiveTokens.neutral700,\n      tertiary: AppFlowyPrimitiveTokens.neutral1000,\n      tertiaryHover: AppFlowyPrimitiveTokens.neutral900,\n      themeThick: AppFlowyPrimitiveTokens.blue500,\n      themeThickHover: AppFlowyPrimitiveTokens.blue600,\n      infoThick: AppFlowyPrimitiveTokens.blue500,\n      infoThickHover: AppFlowyPrimitiveTokens.blue600,\n      successThick: AppFlowyPrimitiveTokens.green600,\n      successThickHover: AppFlowyPrimitiveTokens.green700,\n      warningThick: AppFlowyPrimitiveTokens.orange600,\n      warningThickHover: AppFlowyPrimitiveTokens.orange700,\n      errorThick: AppFlowyPrimitiveTokens.red600,\n      errorThickHover: AppFlowyPrimitiveTokens.red700,\n      featuredThick: AppFlowyPrimitiveTokens.purple500,\n      featuredThickHover: AppFlowyPrimitiveTokens.purple600,\n    );\n\n    final fillColorScheme = AppFlowyFillColorScheme(\n      primary: AppFlowyPrimitiveTokens.neutral100,\n      primaryHover: AppFlowyPrimitiveTokens.neutral200,\n      secondary: AppFlowyPrimitiveTokens.neutral300,\n      secondaryHover: AppFlowyPrimitiveTokens.neutral400,\n      tertiary: AppFlowyPrimitiveTokens.neutral600,\n      tertiaryHover: AppFlowyPrimitiveTokens.neutral500,\n      quaternary: AppFlowyPrimitiveTokens.neutral1000,\n      quaternaryHover: AppFlowyPrimitiveTokens.neutral900,\n      content: AppFlowyPrimitiveTokens.neutralAlphaWhite0,\n      contentHover: AppFlowyPrimitiveTokens.neutralAlphaGrey100005,\n      contentVisible: AppFlowyPrimitiveTokens.neutralAlphaGrey100005,\n      contentVisibleHover: AppFlowyPrimitiveTokens.neutralAlphaGrey100010,\n      themeThick: AppFlowyPrimitiveTokens.blue500,\n      themeThickHover: AppFlowyPrimitiveTokens.blue600,\n      themeSelect: AppFlowyPrimitiveTokens.blueAlphaBlue50015,\n      textSelect: AppFlowyPrimitiveTokens.blueAlphaBlue50020,\n      infoLight: AppFlowyPrimitiveTokens.blue100,\n      infoLightHover: AppFlowyPrimitiveTokens.blue200,\n      infoThick: AppFlowyPrimitiveTokens.blue500,\n      infoThickHover: AppFlowyPrimitiveTokens.blue600,\n      successLight: AppFlowyPrimitiveTokens.green100,\n      successLightHover: AppFlowyPrimitiveTokens.green200,\n      warningLight: AppFlowyPrimitiveTokens.orange100,\n      warningLightHover: AppFlowyPrimitiveTokens.orange200,\n      errorLight: AppFlowyPrimitiveTokens.red100,\n      errorLightHover: AppFlowyPrimitiveTokens.red200,\n      errorThick: AppFlowyPrimitiveTokens.red600,\n      errorThickHover: AppFlowyPrimitiveTokens.red700,\n      errorSelect: AppFlowyPrimitiveTokens.redAlphaRed50010,\n      featuredLight: AppFlowyPrimitiveTokens.purple100,\n      featuredLightHover: AppFlowyPrimitiveTokens.purple200,\n      featuredThick: AppFlowyPrimitiveTokens.purple500,\n      featuredThickHover: AppFlowyPrimitiveTokens.purple600,\n    );\n\n    final surfaceColorScheme = AppFlowySurfaceColorScheme(\n      primary: AppFlowyPrimitiveTokens.neutralWhite,\n      primaryHover: AppFlowyPrimitiveTokens.neutral100,\n      layer01: AppFlowyPrimitiveTokens.neutralWhite,\n      layer01Hover: AppFlowyPrimitiveTokens.neutral100,\n      layer02: AppFlowyPrimitiveTokens.neutralWhite,\n      layer02Hover: AppFlowyPrimitiveTokens.neutral100,\n      layer03: AppFlowyPrimitiveTokens.neutralWhite,\n      layer03Hover: AppFlowyPrimitiveTokens.neutral100,\n      layer04: AppFlowyPrimitiveTokens.neutralWhite,\n      layer04Hover: AppFlowyPrimitiveTokens.neutral100,\n      inverse: AppFlowyPrimitiveTokens.neutral1000,\n      secondary: AppFlowyPrimitiveTokens.neutral1000,\n      overlay: AppFlowyPrimitiveTokens.neutralAlphaBlack60,\n    );\n\n    final surfaceContainerColorScheme = AppFlowySurfaceContainerColorScheme(\n      layer01: AppFlowyPrimitiveTokens.neutral100,\n      layer02: AppFlowyPrimitiveTokens.neutral200,\n      layer03: AppFlowyPrimitiveTokens.neutral300,\n    );\n\n    final backgroundColorScheme = AppFlowyBackgroundColorScheme(\n      primary: AppFlowyPrimitiveTokens.neutralWhite,\n    );\n\n    final badgeColorScheme = AppFlowyBadgeColorScheme(\n      color1Light1: AppFlowyPrimitiveTokens.subtleColorRose100,\n      color1Light2: AppFlowyPrimitiveTokens.subtleColorRose200,\n      color1Light3: AppFlowyPrimitiveTokens.subtleColorRose300,\n      color1Thick1: AppFlowyPrimitiveTokens.subtleColorRose400,\n      color1Thick2: AppFlowyPrimitiveTokens.subtleColorRose500,\n      color1Thick3: AppFlowyPrimitiveTokens.subtleColorRose600,\n      color2Light1: AppFlowyPrimitiveTokens.subtleColorPapaya100,\n      color2Light2: AppFlowyPrimitiveTokens.subtleColorPapaya200,\n      color2Light3: AppFlowyPrimitiveTokens.subtleColorPapaya300,\n      color2Thick1: AppFlowyPrimitiveTokens.subtleColorPapaya400,\n      color2Thick2: AppFlowyPrimitiveTokens.subtleColorPapaya500,\n      color2Thick3: AppFlowyPrimitiveTokens.subtleColorPapaya600,\n      color3Light1: AppFlowyPrimitiveTokens.subtleColorTangerine100,\n      color3Light2: AppFlowyPrimitiveTokens.subtleColorTangerine200,\n      color3Light3: AppFlowyPrimitiveTokens.subtleColorTangerine300,\n      color3Thick1: AppFlowyPrimitiveTokens.subtleColorTangerine400,\n      color3Thick2: AppFlowyPrimitiveTokens.subtleColorTangerine500,\n      color3Thick3: AppFlowyPrimitiveTokens.subtleColorTangerine600,\n      color4Light1: AppFlowyPrimitiveTokens.subtleColorMango100,\n      color4Light2: AppFlowyPrimitiveTokens.subtleColorMango200,\n      color4Light3: AppFlowyPrimitiveTokens.subtleColorMango300,\n      color4Thick1: AppFlowyPrimitiveTokens.subtleColorMango400,\n      color4Thick2: AppFlowyPrimitiveTokens.subtleColorMango500,\n      color4Thick3: AppFlowyPrimitiveTokens.subtleColorMango600,\n      color5Light1: AppFlowyPrimitiveTokens.subtleColorLemon100,\n      color5Light2: AppFlowyPrimitiveTokens.subtleColorLemon200,\n      color5Light3: AppFlowyPrimitiveTokens.subtleColorLemon300,\n      color5Thick1: AppFlowyPrimitiveTokens.subtleColorLemon400,\n      color5Thick2: AppFlowyPrimitiveTokens.subtleColorLemon500,\n      color5Thick3: AppFlowyPrimitiveTokens.subtleColorLemon600,\n      color6Light1: AppFlowyPrimitiveTokens.subtleColorOlive100,\n      color6Light2: AppFlowyPrimitiveTokens.subtleColorOlive200,\n      color6Light3: AppFlowyPrimitiveTokens.subtleColorOlive300,\n      color6Thick1: AppFlowyPrimitiveTokens.subtleColorOlive400,\n      color6Thick2: AppFlowyPrimitiveTokens.subtleColorOlive500,\n      color6Thick3: AppFlowyPrimitiveTokens.subtleColorOlive600,\n      color7Light1: AppFlowyPrimitiveTokens.subtleColorLime100,\n      color7Light2: AppFlowyPrimitiveTokens.subtleColorLime200,\n      color7Light3: AppFlowyPrimitiveTokens.subtleColorLime300,\n      color7Thick1: AppFlowyPrimitiveTokens.subtleColorLime400,\n      color7Thick2: AppFlowyPrimitiveTokens.subtleColorLime500,\n      color7Thick3: AppFlowyPrimitiveTokens.subtleColorLime600,\n      color8Light1: AppFlowyPrimitiveTokens.subtleColorGrass100,\n      color8Light2: AppFlowyPrimitiveTokens.subtleColorGrass200,\n      color8Light3: AppFlowyPrimitiveTokens.subtleColorGrass300,\n      color8Thick1: AppFlowyPrimitiveTokens.subtleColorGrass400,\n      color8Thick2: AppFlowyPrimitiveTokens.subtleColorGrass500,\n      color8Thick3: AppFlowyPrimitiveTokens.subtleColorGrass600,\n      color9Light1: AppFlowyPrimitiveTokens.subtleColorForest100,\n      color9Light2: AppFlowyPrimitiveTokens.subtleColorForest200,\n      color9Light3: AppFlowyPrimitiveTokens.subtleColorForest300,\n      color9Thick1: AppFlowyPrimitiveTokens.subtleColorForest400,\n      color9Thick2: AppFlowyPrimitiveTokens.subtleColorForest500,\n      color9Thick3: AppFlowyPrimitiveTokens.subtleColorForest600,\n      color10Light1: AppFlowyPrimitiveTokens.subtleColorJade100,\n      color10Light2: AppFlowyPrimitiveTokens.subtleColorJade200,\n      color10Light3: AppFlowyPrimitiveTokens.subtleColorJade300,\n      color10Thick1: AppFlowyPrimitiveTokens.subtleColorJade400,\n      color10Thick2: AppFlowyPrimitiveTokens.subtleColorJade500,\n      color10Thick3: AppFlowyPrimitiveTokens.subtleColorJade600,\n      color11Light1: AppFlowyPrimitiveTokens.subtleColorAqua100,\n      color11Light2: AppFlowyPrimitiveTokens.subtleColorAqua200,\n      color11Light3: AppFlowyPrimitiveTokens.subtleColorAqua300,\n      color11Thick1: AppFlowyPrimitiveTokens.subtleColorAqua400,\n      color11Thick2: AppFlowyPrimitiveTokens.subtleColorAqua500,\n      color11Thick3: AppFlowyPrimitiveTokens.subtleColorAqua600,\n      color12Light1: AppFlowyPrimitiveTokens.subtleColorAzure100,\n      color12Light2: AppFlowyPrimitiveTokens.subtleColorAzure200,\n      color12Light3: AppFlowyPrimitiveTokens.subtleColorAzure300,\n      color12Thick1: AppFlowyPrimitiveTokens.subtleColorAzure400,\n      color12Thick2: AppFlowyPrimitiveTokens.subtleColorAzure500,\n      color12Thick3: AppFlowyPrimitiveTokens.subtleColorAzure600,\n      color13Light1: AppFlowyPrimitiveTokens.subtleColorDenim100,\n      color13Light2: AppFlowyPrimitiveTokens.subtleColorDenim200,\n      color13Light3: AppFlowyPrimitiveTokens.subtleColorDenim300,\n      color13Thick1: AppFlowyPrimitiveTokens.subtleColorDenim400,\n      color13Thick2: AppFlowyPrimitiveTokens.subtleColorDenim500,\n      color13Thick3: AppFlowyPrimitiveTokens.subtleColorDenim600,\n      color14Light1: AppFlowyPrimitiveTokens.subtleColorMauve100,\n      color14Light2: AppFlowyPrimitiveTokens.subtleColorMauve200,\n      color14Light3: AppFlowyPrimitiveTokens.subtleColorMauve300,\n      color14Thick1: AppFlowyPrimitiveTokens.subtleColorMauve400,\n      color14Thick2: AppFlowyPrimitiveTokens.subtleColorMauve500,\n      color14Thick3: AppFlowyPrimitiveTokens.subtleColorMauve600,\n      color15Light1: AppFlowyPrimitiveTokens.subtleColorLavender100,\n      color15Light2: AppFlowyPrimitiveTokens.subtleColorLavender200,\n      color15Light3: AppFlowyPrimitiveTokens.subtleColorLavender300,\n      color15Thick1: AppFlowyPrimitiveTokens.subtleColorLavender400,\n      color15Thick2: AppFlowyPrimitiveTokens.subtleColorLavender500,\n      color15Thick3: AppFlowyPrimitiveTokens.subtleColorLavender600,\n      color16Light1: AppFlowyPrimitiveTokens.subtleColorLilac100,\n      color16Light2: AppFlowyPrimitiveTokens.subtleColorLilac200,\n      color16Light3: AppFlowyPrimitiveTokens.subtleColorLilac300,\n      color16Thick1: AppFlowyPrimitiveTokens.subtleColorLilac400,\n      color16Thick2: AppFlowyPrimitiveTokens.subtleColorLilac500,\n      color16Thick3: AppFlowyPrimitiveTokens.subtleColorLilac600,\n      color17Light1: AppFlowyPrimitiveTokens.subtleColorMallow100,\n      color17Light2: AppFlowyPrimitiveTokens.subtleColorMallow200,\n      color17Light3: AppFlowyPrimitiveTokens.subtleColorMallow300,\n      color17Thick1: AppFlowyPrimitiveTokens.subtleColorMallow400,\n      color17Thick2: AppFlowyPrimitiveTokens.subtleColorMallow500,\n      color17Thick3: AppFlowyPrimitiveTokens.subtleColorMallow600,\n      color18Light1: AppFlowyPrimitiveTokens.subtleColorCamellia100,\n      color18Light2: AppFlowyPrimitiveTokens.subtleColorCamellia200,\n      color18Light3: AppFlowyPrimitiveTokens.subtleColorCamellia300,\n      color18Thick1: AppFlowyPrimitiveTokens.subtleColorCamellia400,\n      color18Thick2: AppFlowyPrimitiveTokens.subtleColorCamellia500,\n      color18Thick3: AppFlowyPrimitiveTokens.subtleColorCamellia600,\n      color19Light1: AppFlowyPrimitiveTokens.subtleColorSmoke100,\n      color19Light2: AppFlowyPrimitiveTokens.subtleColorSmoke200,\n      color19Light3: AppFlowyPrimitiveTokens.subtleColorSmoke300,\n      color19Thick1: AppFlowyPrimitiveTokens.subtleColorSmoke400,\n      color19Thick2: AppFlowyPrimitiveTokens.subtleColorSmoke500,\n      color19Thick3: AppFlowyPrimitiveTokens.subtleColorSmoke600,\n      color20Light1: AppFlowyPrimitiveTokens.subtleColorIron100,\n      color20Light2: AppFlowyPrimitiveTokens.subtleColorIron200,\n      color20Light3: AppFlowyPrimitiveTokens.subtleColorIron300,\n      color20Thick1: AppFlowyPrimitiveTokens.subtleColorIron400,\n      color20Thick2: AppFlowyPrimitiveTokens.subtleColorIron500,\n      color20Thick3: AppFlowyPrimitiveTokens.subtleColorIron600,\n    );\n\n    final brandColorScheme = AppFlowyBrandColorScheme(\n      skyline: Color(0xFF00B5FF),\n      aqua: Color(0xFF00C8FF),\n      violet: Color(0xFF9327FF),\n      amethyst: Color(0xFF8427E0),\n      berry: Color(0xFFE3006D),\n      coral: Color(0xFFFB006D),\n      golden: Color(0xFFF7931E),\n      amber: Color(0xFFFFBD00),\n      lemon: Color(0xFFFFCE00),\n    );\n\n    final otherColorsColorScheme = AppFlowyOtherColorsColorScheme(\n      textHighlight: AppFlowyPrimitiveTokens.blue200,\n    );\n\n    return AppFlowyThemeData(\n      textStyle: textStyle,\n      textColorScheme: textColorScheme,\n      borderColorScheme: borderColorScheme,\n      fillColorScheme: fillColorScheme,\n      surfaceColorScheme: surfaceColorScheme,\n      backgroundColorScheme: backgroundColorScheme,\n      iconColorScheme: iconColorScheme,\n      brandColorScheme: brandColorScheme,\n      otherColorsColorScheme: otherColorsColorScheme,\n      borderRadius: borderRadius,\n      surfaceContainerColorScheme: surfaceContainerColorScheme,\n      badgeColorScheme: badgeColorScheme,\n      spacing: spacing,\n      shadow: shadow,\n    );\n  }\n\n  @override\n  AppFlowyThemeData dark({\n    String? fontFamily,\n  }) {\n    final textStyle = AppFlowyBaseTextStyle.customFontFamily(fontFamily ?? '');\n    final borderRadius = AppFlowySharedTokens.buildBorderRadius();\n    final spacing = AppFlowySharedTokens.buildSpacing();\n    final shadow = AppFlowySharedTokens.buildShadow(Brightness.dark);\n\n    final textColorScheme = AppFlowyTextColorScheme(\n      primary: AppFlowyPrimitiveTokens.neutral200,\n      secondary: AppFlowyPrimitiveTokens.neutral500,\n      tertiary: AppFlowyPrimitiveTokens.neutral600,\n      quaternary: AppFlowyPrimitiveTokens.neutral1000,\n      onFill: AppFlowyPrimitiveTokens.neutralWhite,\n      action: AppFlowyPrimitiveTokens.blue500,\n      actionHover: AppFlowyPrimitiveTokens.blue400,\n      info: AppFlowyPrimitiveTokens.blue500,\n      infoHover: AppFlowyPrimitiveTokens.blue400,\n      success: AppFlowyPrimitiveTokens.green600,\n      successHover: AppFlowyPrimitiveTokens.green500,\n      warning: AppFlowyPrimitiveTokens.orange600,\n      warningHover: AppFlowyPrimitiveTokens.orange500,\n      error: AppFlowyPrimitiveTokens.red500,\n      errorHover: AppFlowyPrimitiveTokens.red400,\n      featured: AppFlowyPrimitiveTokens.purple500,\n      featuredHover: AppFlowyPrimitiveTokens.purple400,\n    );\n\n    final iconColorScheme = AppFlowyIconColorScheme(\n      primary: AppFlowyPrimitiveTokens.neutral200,\n      secondary: AppFlowyPrimitiveTokens.neutral400,\n      tertiary: AppFlowyPrimitiveTokens.neutral600,\n      quaternary: AppFlowyPrimitiveTokens.neutral1000,\n      infoThick: AppFlowyPrimitiveTokens.blue500,\n      infoThickHover: AppFlowyPrimitiveTokens.blue400,\n      successThick: AppFlowyPrimitiveTokens.green600,\n      successThickHover: AppFlowyPrimitiveTokens.green500,\n      warningThick: AppFlowyPrimitiveTokens.orange600,\n      warningThickHover: AppFlowyPrimitiveTokens.orange500,\n      errorThick: AppFlowyPrimitiveTokens.red500,\n      errorThickHover: AppFlowyPrimitiveTokens.red400,\n      featuredThick: AppFlowyPrimitiveTokens.purple500,\n      featuredThickHover: AppFlowyPrimitiveTokens.purple400,\n      onFill: AppFlowyPrimitiveTokens.neutralWhite,\n    );\n\n    final borderColorScheme = AppFlowyBorderColorScheme(\n      primary: AppFlowyPrimitiveTokens.neutral800,\n      primaryHover: AppFlowyPrimitiveTokens.neutral700,\n      secondary: AppFlowyPrimitiveTokens.neutral300,\n      secondaryHover: AppFlowyPrimitiveTokens.neutral200,\n      tertiary: AppFlowyPrimitiveTokens.neutral100,\n      tertiaryHover: AppFlowyPrimitiveTokens.neutralWhite,\n      themeThick: AppFlowyPrimitiveTokens.blue500,\n      themeThickHover: AppFlowyPrimitiveTokens.blue600,\n      infoThick: AppFlowyPrimitiveTokens.blue500,\n      infoThickHover: AppFlowyPrimitiveTokens.blue400,\n      successThick: AppFlowyPrimitiveTokens.green600,\n      successThickHover: AppFlowyPrimitiveTokens.green500,\n      warningThick: AppFlowyPrimitiveTokens.orange600,\n      warningThickHover: AppFlowyPrimitiveTokens.orange500,\n      errorThick: AppFlowyPrimitiveTokens.red500,\n      errorThickHover: AppFlowyPrimitiveTokens.red400,\n      featuredThick: AppFlowyPrimitiveTokens.purple500,\n      featuredThickHover: AppFlowyPrimitiveTokens.purple400,\n    );\n\n    final fillColorScheme = AppFlowyFillColorScheme(\n      primary: AppFlowyPrimitiveTokens.neutral900,\n      primaryHover: AppFlowyPrimitiveTokens.neutral800,\n      secondary: AppFlowyPrimitiveTokens.neutral600,\n      secondaryHover: AppFlowyPrimitiveTokens.neutral500,\n      tertiary: AppFlowyPrimitiveTokens.neutral300,\n      tertiaryHover: AppFlowyPrimitiveTokens.neutral200,\n      quaternary: AppFlowyPrimitiveTokens.neutral100,\n      quaternaryHover: AppFlowyPrimitiveTokens.neutralWhite,\n      content: AppFlowyPrimitiveTokens.neutralAlphaWhite0,\n      contentHover: AppFlowyPrimitiveTokens.neutralAlphaGrey10005,\n      contentVisible: AppFlowyPrimitiveTokens.neutralAlphaGrey10005,\n      contentVisibleHover: AppFlowyPrimitiveTokens.neutralAlphaGrey10010,\n      themeThick: AppFlowyPrimitiveTokens.blue500,\n      themeThickHover: AppFlowyPrimitiveTokens.blue600,\n      themeSelect: AppFlowyPrimitiveTokens.blueAlphaBlue50015,\n      textSelect: AppFlowyPrimitiveTokens.blueAlphaBlue50020,\n      infoLight: AppFlowyPrimitiveTokens.blue200,\n      infoLightHover: AppFlowyPrimitiveTokens.blue100,\n      infoThick: AppFlowyPrimitiveTokens.blue500,\n      infoThickHover: AppFlowyPrimitiveTokens.blue400,\n      successLight: AppFlowyPrimitiveTokens.green200,\n      successLightHover: AppFlowyPrimitiveTokens.green100,\n      warningLight: AppFlowyPrimitiveTokens.orange200,\n      warningLightHover: AppFlowyPrimitiveTokens.orange100,\n      errorLight: AppFlowyPrimitiveTokens.red200,\n      errorLightHover: AppFlowyPrimitiveTokens.red100,\n      errorThick: AppFlowyPrimitiveTokens.red500,\n      errorThickHover: AppFlowyPrimitiveTokens.red400,\n      errorSelect: AppFlowyPrimitiveTokens.redAlphaRed50010,\n      featuredLight: AppFlowyPrimitiveTokens.purple200,\n      featuredLightHover: AppFlowyPrimitiveTokens.purple100,\n      featuredThick: AppFlowyPrimitiveTokens.purple500,\n      featuredThickHover: AppFlowyPrimitiveTokens.purple400,\n    );\n\n    final surfaceColorScheme = AppFlowySurfaceColorScheme(\n      primary: AppFlowyPrimitiveTokens.neutral900,\n      primaryHover: AppFlowyPrimitiveTokens.neutral800,\n      layer01: AppFlowyPrimitiveTokens.neutral900,\n      layer01Hover: AppFlowyPrimitiveTokens.neutral800,\n      layer02: AppFlowyPrimitiveTokens.neutral850,\n      layer02Hover: AppFlowyPrimitiveTokens.neutral800,\n      layer03: AppFlowyPrimitiveTokens.neutral850,\n      layer03Hover: AppFlowyPrimitiveTokens.neutral800,\n      layer04: AppFlowyPrimitiveTokens.neutral830,\n      layer04Hover: AppFlowyPrimitiveTokens.neutral800,\n      inverse: AppFlowyPrimitiveTokens.neutral800,\n      secondary: AppFlowyPrimitiveTokens.neutral800,\n      overlay: AppFlowyPrimitiveTokens.neutralAlphaBlack60,\n    );\n\n    final surfaceContainerColorScheme = AppFlowySurfaceContainerColorScheme(\n      layer01: AppFlowyPrimitiveTokens.neutral900,\n      layer02: AppFlowyPrimitiveTokens.neutral800,\n      layer03: AppFlowyPrimitiveTokens.neutral700,\n    );\n\n    final backgroundColorScheme = AppFlowyBackgroundColorScheme(\n      primary: AppFlowyPrimitiveTokens.neutral1000,\n    );\n\n    final badgeColorScheme = AppFlowyBadgeColorScheme(\n      color1Light1: AppFlowyPrimitiveTokens.subtleColorRose100,\n      color1Light2: AppFlowyPrimitiveTokens.subtleColorRose200,\n      color1Light3: AppFlowyPrimitiveTokens.subtleColorRose300,\n      color1Thick1: AppFlowyPrimitiveTokens.subtleColorRose400,\n      color1Thick2: AppFlowyPrimitiveTokens.subtleColorRose500,\n      color1Thick3: AppFlowyPrimitiveTokens.subtleColorRose600,\n      color2Light1: AppFlowyPrimitiveTokens.subtleColorPapaya100,\n      color2Light2: AppFlowyPrimitiveTokens.subtleColorPapaya200,\n      color2Light3: AppFlowyPrimitiveTokens.subtleColorPapaya300,\n      color2Thick1: AppFlowyPrimitiveTokens.subtleColorPapaya400,\n      color2Thick2: AppFlowyPrimitiveTokens.subtleColorPapaya500,\n      color2Thick3: AppFlowyPrimitiveTokens.subtleColorPapaya600,\n      color3Light1: AppFlowyPrimitiveTokens.subtleColorTangerine100,\n      color3Light2: AppFlowyPrimitiveTokens.subtleColorTangerine200,\n      color3Light3: AppFlowyPrimitiveTokens.subtleColorTangerine300,\n      color3Thick1: AppFlowyPrimitiveTokens.subtleColorTangerine400,\n      color3Thick2: AppFlowyPrimitiveTokens.subtleColorTangerine500,\n      color3Thick3: AppFlowyPrimitiveTokens.subtleColorTangerine600,\n      color4Light1: AppFlowyPrimitiveTokens.subtleColorMango100,\n      color4Light2: AppFlowyPrimitiveTokens.subtleColorMango200,\n      color4Light3: AppFlowyPrimitiveTokens.subtleColorMango300,\n      color4Thick1: AppFlowyPrimitiveTokens.subtleColorMango400,\n      color4Thick2: AppFlowyPrimitiveTokens.subtleColorMango500,\n      color4Thick3: AppFlowyPrimitiveTokens.subtleColorMango600,\n      color5Light1: AppFlowyPrimitiveTokens.subtleColorLemon100,\n      color5Light2: AppFlowyPrimitiveTokens.subtleColorLemon200,\n      color5Light3: AppFlowyPrimitiveTokens.subtleColorLemon300,\n      color5Thick1: AppFlowyPrimitiveTokens.subtleColorLemon400,\n      color5Thick2: AppFlowyPrimitiveTokens.subtleColorLemon500,\n      color5Thick3: AppFlowyPrimitiveTokens.subtleColorLemon600,\n      color6Light1: AppFlowyPrimitiveTokens.subtleColorOlive100,\n      color6Light2: AppFlowyPrimitiveTokens.subtleColorOlive200,\n      color6Light3: AppFlowyPrimitiveTokens.subtleColorOlive300,\n      color6Thick1: AppFlowyPrimitiveTokens.subtleColorOlive400,\n      color6Thick2: AppFlowyPrimitiveTokens.subtleColorOlive500,\n      color6Thick3: AppFlowyPrimitiveTokens.subtleColorOlive600,\n      color7Light1: AppFlowyPrimitiveTokens.subtleColorLime100,\n      color7Light2: AppFlowyPrimitiveTokens.subtleColorLime200,\n      color7Light3: AppFlowyPrimitiveTokens.subtleColorLime300,\n      color7Thick1: AppFlowyPrimitiveTokens.subtleColorLime400,\n      color7Thick2: AppFlowyPrimitiveTokens.subtleColorLime500,\n      color7Thick3: AppFlowyPrimitiveTokens.subtleColorLime600,\n      color8Light1: AppFlowyPrimitiveTokens.subtleColorGrass100,\n      color8Light2: AppFlowyPrimitiveTokens.subtleColorGrass200,\n      color8Light3: AppFlowyPrimitiveTokens.subtleColorGrass300,\n      color8Thick1: AppFlowyPrimitiveTokens.subtleColorGrass400,\n      color8Thick2: AppFlowyPrimitiveTokens.subtleColorGrass500,\n      color8Thick3: AppFlowyPrimitiveTokens.subtleColorGrass600,\n      color9Light1: AppFlowyPrimitiveTokens.subtleColorForest100,\n      color9Light2: AppFlowyPrimitiveTokens.subtleColorForest200,\n      color9Light3: AppFlowyPrimitiveTokens.subtleColorForest300,\n      color9Thick1: AppFlowyPrimitiveTokens.subtleColorForest400,\n      color9Thick2: AppFlowyPrimitiveTokens.subtleColorForest500,\n      color9Thick3: AppFlowyPrimitiveTokens.subtleColorForest600,\n      color10Light1: AppFlowyPrimitiveTokens.subtleColorJade100,\n      color10Light2: AppFlowyPrimitiveTokens.subtleColorJade200,\n      color10Light3: AppFlowyPrimitiveTokens.subtleColorJade300,\n      color10Thick1: AppFlowyPrimitiveTokens.subtleColorJade400,\n      color10Thick2: AppFlowyPrimitiveTokens.subtleColorJade500,\n      color10Thick3: AppFlowyPrimitiveTokens.subtleColorJade600,\n      color11Light1: AppFlowyPrimitiveTokens.subtleColorAqua100,\n      color11Light2: AppFlowyPrimitiveTokens.subtleColorAqua200,\n      color11Light3: AppFlowyPrimitiveTokens.subtleColorAqua300,\n      color11Thick1: AppFlowyPrimitiveTokens.subtleColorAqua400,\n      color11Thick2: AppFlowyPrimitiveTokens.subtleColorAqua500,\n      color11Thick3: AppFlowyPrimitiveTokens.subtleColorAqua600,\n      color12Light1: AppFlowyPrimitiveTokens.subtleColorAzure100,\n      color12Light2: AppFlowyPrimitiveTokens.subtleColorAzure200,\n      color12Light3: AppFlowyPrimitiveTokens.subtleColorAzure300,\n      color12Thick1: AppFlowyPrimitiveTokens.subtleColorAzure400,\n      color12Thick2: AppFlowyPrimitiveTokens.subtleColorAzure500,\n      color12Thick3: AppFlowyPrimitiveTokens.subtleColorAzure600,\n      color13Light1: AppFlowyPrimitiveTokens.subtleColorDenim100,\n      color13Light2: AppFlowyPrimitiveTokens.subtleColorDenim200,\n      color13Light3: AppFlowyPrimitiveTokens.subtleColorDenim300,\n      color13Thick1: AppFlowyPrimitiveTokens.subtleColorDenim400,\n      color13Thick2: AppFlowyPrimitiveTokens.subtleColorDenim500,\n      color13Thick3: AppFlowyPrimitiveTokens.subtleColorDenim600,\n      color14Light1: AppFlowyPrimitiveTokens.subtleColorMauve100,\n      color14Light2: AppFlowyPrimitiveTokens.subtleColorMauve200,\n      color14Light3: AppFlowyPrimitiveTokens.subtleColorMauve300,\n      color14Thick1: AppFlowyPrimitiveTokens.subtleColorMauve400,\n      color14Thick2: AppFlowyPrimitiveTokens.subtleColorMauve500,\n      color14Thick3: AppFlowyPrimitiveTokens.subtleColorMauve600,\n      color15Light1: AppFlowyPrimitiveTokens.subtleColorLavender100,\n      color15Light2: AppFlowyPrimitiveTokens.subtleColorLavender200,\n      color15Light3: AppFlowyPrimitiveTokens.subtleColorLavender300,\n      color15Thick1: AppFlowyPrimitiveTokens.subtleColorLavender400,\n      color15Thick2: AppFlowyPrimitiveTokens.subtleColorLavender500,\n      color15Thick3: AppFlowyPrimitiveTokens.subtleColorLavender600,\n      color16Light1: AppFlowyPrimitiveTokens.subtleColorLilac100,\n      color16Light2: AppFlowyPrimitiveTokens.subtleColorLilac200,\n      color16Light3: AppFlowyPrimitiveTokens.subtleColorLilac300,\n      color16Thick1: AppFlowyPrimitiveTokens.subtleColorLilac400,\n      color16Thick2: AppFlowyPrimitiveTokens.subtleColorLilac500,\n      color16Thick3: AppFlowyPrimitiveTokens.subtleColorLilac600,\n      color17Light1: AppFlowyPrimitiveTokens.subtleColorMallow100,\n      color17Light2: AppFlowyPrimitiveTokens.subtleColorMallow200,\n      color17Light3: AppFlowyPrimitiveTokens.subtleColorMallow300,\n      color17Thick1: AppFlowyPrimitiveTokens.subtleColorMallow400,\n      color17Thick2: AppFlowyPrimitiveTokens.subtleColorMallow500,\n      color17Thick3: AppFlowyPrimitiveTokens.subtleColorMallow600,\n      color18Light1: AppFlowyPrimitiveTokens.subtleColorCamellia100,\n      color18Light2: AppFlowyPrimitiveTokens.subtleColorCamellia200,\n      color18Light3: AppFlowyPrimitiveTokens.subtleColorCamellia300,\n      color18Thick1: AppFlowyPrimitiveTokens.subtleColorCamellia400,\n      color18Thick2: AppFlowyPrimitiveTokens.subtleColorCamellia500,\n      color18Thick3: AppFlowyPrimitiveTokens.subtleColorCamellia600,\n      color19Light1: AppFlowyPrimitiveTokens.subtleColorSmoke100,\n      color19Light2: AppFlowyPrimitiveTokens.subtleColorSmoke200,\n      color19Light3: AppFlowyPrimitiveTokens.subtleColorSmoke300,\n      color19Thick1: AppFlowyPrimitiveTokens.subtleColorSmoke400,\n      color19Thick2: AppFlowyPrimitiveTokens.subtleColorSmoke500,\n      color19Thick3: AppFlowyPrimitiveTokens.subtleColorSmoke600,\n      color20Light1: AppFlowyPrimitiveTokens.subtleColorIron100,\n      color20Light2: AppFlowyPrimitiveTokens.subtleColorIron200,\n      color20Light3: AppFlowyPrimitiveTokens.subtleColorIron300,\n      color20Thick1: AppFlowyPrimitiveTokens.subtleColorIron400,\n      color20Thick2: AppFlowyPrimitiveTokens.subtleColorIron500,\n      color20Thick3: AppFlowyPrimitiveTokens.subtleColorIron600,\n    );\n\n    final brandColorScheme = AppFlowyBrandColorScheme(\n      skyline: Color(0xFF00B5FF),\n      aqua: Color(0xFF00C8FF),\n      violet: Color(0xFF9327FF),\n      amethyst: Color(0xFF8427E0),\n      berry: Color(0xFFE3006D),\n      coral: Color(0xFFFB006D),\n      golden: Color(0xFFF7931E),\n      amber: Color(0xFFFFBD00),\n      lemon: Color(0xFFFFCE00),\n    );\n\n    final otherColorsColorScheme = AppFlowyOtherColorsColorScheme(\n      textHighlight: AppFlowyPrimitiveTokens.blue200,\n    );\n\n    return AppFlowyThemeData(\n      textStyle: textStyle,\n      textColorScheme: textColorScheme,\n      borderColorScheme: borderColorScheme,\n      fillColorScheme: fillColorScheme,\n      surfaceColorScheme: surfaceColorScheme,\n      backgroundColorScheme: backgroundColorScheme,\n      iconColorScheme: iconColorScheme,\n      brandColorScheme: brandColorScheme,\n      otherColorsColorScheme: otherColorsColorScheme,\n      borderRadius: borderRadius,\n      surfaceContainerColorScheme: surfaceContainerColorScheme,\n      badgeColorScheme: badgeColorScheme,\n      spacing: spacing,\n      shadow: shadow,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/built_in_themes.dart",
    "content": "export 'appflowy_default/semantic.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart",
    "content": "import 'package:appflowy_ui/appflowy_ui.dart';\n\nclass CustomTheme implements AppFlowyThemeBuilder {\n  const CustomTheme({\n    required this.lightThemeJson,\n    required this.darkThemeJson,\n  });\n\n  final Map<String, dynamic> lightThemeJson;\n  final Map<String, dynamic> darkThemeJson;\n\n  @override\n  AppFlowyThemeData light({\n    String? fontFamily,\n  }) {\n    throw UnimplementedError();\n  }\n\n  @override\n  AppFlowyThemeData dark({\n    String? fontFamily,\n  }) {\n    throw UnimplementedError();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/shared.dart",
    "content": "import 'package:appflowy_ui/src/theme/definition/border_radius/border_radius.dart';\nimport 'package:appflowy_ui/src/theme/definition/shadow/shadow.dart';\nimport 'package:appflowy_ui/src/theme/definition/spacing/spacing.dart';\nimport 'package:flutter/material.dart';\n\nclass AppFlowySpacingConstant {\n  static const double spacing100 = 4;\n  static const double spacing200 = 6;\n  static const double spacing300 = 8;\n  static const double spacing400 = 12;\n  static const double spacing500 = 16;\n  static const double spacing600 = 20;\n}\n\nclass AppFlowyBorderRadiusConstant {\n  static const double radius100 = 4;\n  static const double radius200 = 6;\n  static const double radius300 = 8;\n  static const double radius400 = 12;\n  static const double radius500 = 16;\n  static const double radius600 = 20;\n}\n\nclass AppFlowySharedTokens {\n  const AppFlowySharedTokens();\n\n  static AppFlowyBorderRadius buildBorderRadius() {\n    return AppFlowyBorderRadius(\n      xs: AppFlowyBorderRadiusConstant.radius100,\n      s: AppFlowyBorderRadiusConstant.radius200,\n      m: AppFlowyBorderRadiusConstant.radius300,\n      l: AppFlowyBorderRadiusConstant.radius400,\n      xl: AppFlowyBorderRadiusConstant.radius500,\n      xxl: AppFlowyBorderRadiusConstant.radius600,\n    );\n  }\n\n  static AppFlowySpacing buildSpacing() {\n    return AppFlowySpacing(\n      xs: AppFlowySpacingConstant.spacing100,\n      s: AppFlowySpacingConstant.spacing200,\n      m: AppFlowySpacingConstant.spacing300,\n      l: AppFlowySpacingConstant.spacing400,\n      xl: AppFlowySpacingConstant.spacing500,\n      xxl: AppFlowySpacingConstant.spacing600,\n    );\n  }\n\n  static AppFlowyShadow buildShadow(\n    Brightness brightness,\n  ) {\n    return switch (brightness) {\n      Brightness.light => AppFlowyShadow(\n          small: [\n            BoxShadow(\n              offset: Offset(0, 2),\n              blurRadius: 16,\n              color: Color(0x1F000000),\n            ),\n          ],\n          medium: [\n            BoxShadow(\n              offset: Offset(0, 4),\n              blurRadius: 32,\n              color: Color(0x1F000000),\n            ),\n          ],\n        ),\n      Brightness.dark => AppFlowyShadow(\n          small: [\n            BoxShadow(\n              offset: Offset(0, 2),\n              blurRadius: 16,\n              color: Color(0x7A000000),\n            ),\n          ],\n          medium: [\n            BoxShadow(\n              offset: Offset(0, 4),\n              blurRadius: 32,\n              color: Color(0x7A000000),\n            ),\n          ],\n        ),\n    };\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/border_radius/border_radius.dart",
    "content": "class AppFlowyBorderRadius {\n  const AppFlowyBorderRadius({\n    required this.xs,\n    required this.s,\n    required this.m,\n    required this.l,\n    required this.xl,\n    required this.xxl,\n  });\n\n  final double xs;\n  final double s;\n  final double m;\n  final double l;\n  final double xl;\n  final double xxl;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass AppFlowyBackgroundColorScheme {\n  const AppFlowyBackgroundColorScheme({\n    required this.primary,\n  });\n\n  final Color primary;\n\n  AppFlowyBackgroundColorScheme lerp(\n    AppFlowyBackgroundColorScheme other,\n    double t,\n  ) {\n    return AppFlowyBackgroundColorScheme(\n      primary: Color.lerp(\n        primary,\n        other.primary,\n        t,\n      )!,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/badge_color_scheme.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass AppFlowyBadgeColorScheme {\n  AppFlowyBadgeColorScheme({\n    required this.color1Light1,\n    required this.color1Light2,\n    required this.color1Light3,\n    required this.color1Thick1,\n    required this.color1Thick2,\n    required this.color1Thick3,\n    required this.color2Light1,\n    required this.color2Light2,\n    required this.color2Light3,\n    required this.color2Thick1,\n    required this.color2Thick2,\n    required this.color2Thick3,\n    required this.color3Light1,\n    required this.color3Light2,\n    required this.color3Light3,\n    required this.color3Thick1,\n    required this.color3Thick2,\n    required this.color3Thick3,\n    required this.color4Light1,\n    required this.color4Light2,\n    required this.color4Light3,\n    required this.color4Thick1,\n    required this.color4Thick2,\n    required this.color4Thick3,\n    required this.color5Light1,\n    required this.color5Light2,\n    required this.color5Light3,\n    required this.color5Thick1,\n    required this.color5Thick2,\n    required this.color5Thick3,\n    required this.color6Light1,\n    required this.color6Light2,\n    required this.color6Light3,\n    required this.color6Thick1,\n    required this.color6Thick2,\n    required this.color6Thick3,\n    required this.color7Light1,\n    required this.color7Light2,\n    required this.color7Light3,\n    required this.color7Thick1,\n    required this.color7Thick2,\n    required this.color7Thick3,\n    required this.color8Light1,\n    required this.color8Light2,\n    required this.color8Light3,\n    required this.color8Thick1,\n    required this.color8Thick2,\n    required this.color8Thick3,\n    required this.color9Light1,\n    required this.color9Light2,\n    required this.color9Light3,\n    required this.color9Thick1,\n    required this.color9Thick2,\n    required this.color9Thick3,\n    required this.color10Light1,\n    required this.color10Light2,\n    required this.color10Light3,\n    required this.color10Thick1,\n    required this.color10Thick2,\n    required this.color10Thick3,\n    required this.color11Light1,\n    required this.color11Light2,\n    required this.color11Light3,\n    required this.color11Thick1,\n    required this.color11Thick2,\n    required this.color11Thick3,\n    required this.color12Light1,\n    required this.color12Light2,\n    required this.color12Light3,\n    required this.color12Thick1,\n    required this.color12Thick2,\n    required this.color12Thick3,\n    required this.color13Light1,\n    required this.color13Light2,\n    required this.color13Light3,\n    required this.color13Thick1,\n    required this.color13Thick2,\n    required this.color13Thick3,\n    required this.color14Light1,\n    required this.color14Light2,\n    required this.color14Light3,\n    required this.color14Thick1,\n    required this.color14Thick2,\n    required this.color14Thick3,\n    required this.color15Light1,\n    required this.color15Light2,\n    required this.color15Light3,\n    required this.color15Thick1,\n    required this.color15Thick2,\n    required this.color15Thick3,\n    required this.color16Light1,\n    required this.color16Light2,\n    required this.color16Light3,\n    required this.color16Thick1,\n    required this.color16Thick2,\n    required this.color16Thick3,\n    required this.color17Light1,\n    required this.color17Light2,\n    required this.color17Light3,\n    required this.color17Thick1,\n    required this.color17Thick2,\n    required this.color17Thick3,\n    required this.color18Light1,\n    required this.color18Light2,\n    required this.color18Light3,\n    required this.color18Thick1,\n    required this.color18Thick2,\n    required this.color18Thick3,\n    required this.color19Light1,\n    required this.color19Light2,\n    required this.color19Light3,\n    required this.color19Thick1,\n    required this.color19Thick2,\n    required this.color19Thick3,\n    required this.color20Light1,\n    required this.color20Light2,\n    required this.color20Light3,\n    required this.color20Thick1,\n    required this.color20Thick2,\n    required this.color20Thick3,\n  });\n\n  final Color color1Light1;\n  final Color color1Light2;\n  final Color color1Light3;\n  final Color color1Thick1;\n  final Color color1Thick2;\n  final Color color1Thick3;\n  final Color color2Light1;\n  final Color color2Light2;\n  final Color color2Light3;\n  final Color color2Thick1;\n  final Color color2Thick2;\n  final Color color2Thick3;\n  final Color color3Light1;\n  final Color color3Light2;\n  final Color color3Light3;\n  final Color color3Thick1;\n  final Color color3Thick2;\n  final Color color3Thick3;\n  final Color color4Light1;\n  final Color color4Light2;\n  final Color color4Light3;\n  final Color color4Thick1;\n  final Color color4Thick2;\n  final Color color4Thick3;\n  final Color color5Light1;\n  final Color color5Light2;\n  final Color color5Light3;\n  final Color color5Thick1;\n  final Color color5Thick2;\n  final Color color5Thick3;\n  final Color color6Light1;\n  final Color color6Light2;\n  final Color color6Light3;\n  final Color color6Thick1;\n  final Color color6Thick2;\n  final Color color6Thick3;\n  final Color color7Light1;\n  final Color color7Light2;\n  final Color color7Light3;\n  final Color color7Thick1;\n  final Color color7Thick2;\n  final Color color7Thick3;\n  final Color color8Light1;\n  final Color color8Light2;\n  final Color color8Light3;\n  final Color color8Thick1;\n  final Color color8Thick2;\n  final Color color8Thick3;\n  final Color color9Light1;\n  final Color color9Light2;\n  final Color color9Light3;\n  final Color color9Thick1;\n  final Color color9Thick2;\n  final Color color9Thick3;\n  final Color color10Light1;\n  final Color color10Light2;\n  final Color color10Light3;\n  final Color color10Thick1;\n  final Color color10Thick2;\n  final Color color10Thick3;\n  final Color color11Light1;\n  final Color color11Light2;\n  final Color color11Light3;\n  final Color color11Thick1;\n  final Color color11Thick2;\n  final Color color11Thick3;\n  final Color color12Light1;\n  final Color color12Light2;\n  final Color color12Light3;\n  final Color color12Thick1;\n  final Color color12Thick2;\n  final Color color12Thick3;\n  final Color color13Light1;\n  final Color color13Light2;\n  final Color color13Light3;\n  final Color color13Thick1;\n  final Color color13Thick2;\n  final Color color13Thick3;\n  final Color color14Light1;\n  final Color color14Light2;\n  final Color color14Light3;\n  final Color color14Thick1;\n  final Color color14Thick2;\n  final Color color14Thick3;\n  final Color color15Light1;\n  final Color color15Light2;\n  final Color color15Light3;\n  final Color color15Thick1;\n  final Color color15Thick2;\n  final Color color15Thick3;\n  final Color color16Light1;\n  final Color color16Light2;\n  final Color color16Light3;\n  final Color color16Thick1;\n  final Color color16Thick2;\n  final Color color16Thick3;\n  final Color color17Light1;\n  final Color color17Light2;\n  final Color color17Light3;\n  final Color color17Thick1;\n  final Color color17Thick2;\n  final Color color17Thick3;\n  final Color color18Light1;\n  final Color color18Light2;\n  final Color color18Light3;\n  final Color color18Thick1;\n  final Color color18Thick2;\n  final Color color18Thick3;\n  final Color color19Light1;\n  final Color color19Light2;\n  final Color color19Light3;\n  final Color color19Thick1;\n  final Color color19Thick2;\n  final Color color19Thick3;\n  final Color color20Light1;\n  final Color color20Light2;\n  final Color color20Light3;\n  final Color color20Thick1;\n  final Color color20Thick2;\n  final Color color20Thick3;\n\n  AppFlowyBadgeColorScheme lerp(\n    AppFlowyBadgeColorScheme other,\n    double t,\n  ) {\n    return AppFlowyBadgeColorScheme(\n      color1Light1: Color.lerp(\n        color1Light1,\n        other.color1Light1,\n        t,\n      )!,\n      color1Light2: Color.lerp(\n        color1Light2,\n        other.color1Light2,\n        t,\n      )!,\n      color1Light3: Color.lerp(\n        color1Light3,\n        other.color1Light3,\n        t,\n      )!,\n      color1Thick1: Color.lerp(\n        color1Thick1,\n        other.color1Thick1,\n        t,\n      )!,\n      color1Thick2: Color.lerp(\n        color1Thick2,\n        other.color1Thick2,\n        t,\n      )!,\n      color1Thick3: Color.lerp(\n        color1Thick3,\n        other.color1Thick3,\n        t,\n      )!,\n      color2Light1: Color.lerp(\n        color2Light1,\n        other.color2Light1,\n        t,\n      )!,\n      color2Light2: Color.lerp(\n        color2Light2,\n        other.color2Light2,\n        t,\n      )!,\n      color2Light3: Color.lerp(\n        color2Light3,\n        other.color2Light3,\n        t,\n      )!,\n      color2Thick1: Color.lerp(\n        color2Thick1,\n        other.color2Thick1,\n        t,\n      )!,\n      color2Thick2: Color.lerp(\n        color2Thick2,\n        other.color2Thick2,\n        t,\n      )!,\n      color2Thick3: Color.lerp(\n        color2Thick3,\n        other.color2Thick3,\n        t,\n      )!,\n      color3Light1: Color.lerp(\n        color3Light1,\n        other.color3Light1,\n        t,\n      )!,\n      color3Light2: Color.lerp(\n        color3Light2,\n        other.color3Light2,\n        t,\n      )!,\n      color3Light3: Color.lerp(\n        color3Light3,\n        other.color3Light3,\n        t,\n      )!,\n      color3Thick1: Color.lerp(\n        color3Thick1,\n        other.color3Thick1,\n        t,\n      )!,\n      color3Thick2: Color.lerp(\n        color3Thick2,\n        other.color3Thick2,\n        t,\n      )!,\n      color3Thick3: Color.lerp(\n        color3Thick3,\n        other.color3Thick3,\n        t,\n      )!,\n      color4Light1: Color.lerp(\n        color4Light1,\n        other.color4Light1,\n        t,\n      )!,\n      color4Light2: Color.lerp(\n        color4Light2,\n        other.color4Light2,\n        t,\n      )!,\n      color4Light3: Color.lerp(\n        color4Light3,\n        other.color4Light3,\n        t,\n      )!,\n      color4Thick1: Color.lerp(\n        color4Thick1,\n        other.color4Thick1,\n        t,\n      )!,\n      color4Thick2: Color.lerp(\n        color4Thick2,\n        other.color4Thick2,\n        t,\n      )!,\n      color4Thick3: Color.lerp(\n        color4Thick3,\n        other.color4Thick3,\n        t,\n      )!,\n      color5Light1: Color.lerp(\n        color5Light1,\n        other.color5Light1,\n        t,\n      )!,\n      color5Light2: Color.lerp(\n        color5Light2,\n        other.color5Light2,\n        t,\n      )!,\n      color5Light3: Color.lerp(\n        color5Light3,\n        other.color5Light3,\n        t,\n      )!,\n      color5Thick1: Color.lerp(\n        color5Thick1,\n        other.color5Thick1,\n        t,\n      )!,\n      color5Thick2: Color.lerp(\n        color5Thick2,\n        other.color5Thick2,\n        t,\n      )!,\n      color5Thick3: Color.lerp(\n        color5Thick3,\n        other.color5Thick3,\n        t,\n      )!,\n      color6Light1: Color.lerp(\n        color6Light1,\n        other.color6Light1,\n        t,\n      )!,\n      color6Light2: Color.lerp(\n        color6Light2,\n        other.color6Light2,\n        t,\n      )!,\n      color6Light3: Color.lerp(\n        color6Light3,\n        other.color6Light3,\n        t,\n      )!,\n      color6Thick1: Color.lerp(\n        color6Thick1,\n        other.color6Thick1,\n        t,\n      )!,\n      color6Thick2: Color.lerp(\n        color6Thick2,\n        other.color6Thick2,\n        t,\n      )!,\n      color6Thick3: Color.lerp(\n        color6Thick3,\n        other.color6Thick3,\n        t,\n      )!,\n      color7Light1: Color.lerp(\n        color7Light1,\n        other.color7Light1,\n        t,\n      )!,\n      color7Light2: Color.lerp(\n        color7Light2,\n        other.color7Light2,\n        t,\n      )!,\n      color7Light3: Color.lerp(\n        color7Light3,\n        other.color7Light3,\n        t,\n      )!,\n      color7Thick1: Color.lerp(\n        color7Thick1,\n        other.color7Thick1,\n        t,\n      )!,\n      color7Thick2: Color.lerp(\n        color7Thick2,\n        other.color7Thick2,\n        t,\n      )!,\n      color7Thick3: Color.lerp(\n        color7Thick3,\n        other.color7Thick3,\n        t,\n      )!,\n      color8Light1: Color.lerp(\n        color8Light1,\n        other.color8Light1,\n        t,\n      )!,\n      color8Light2: Color.lerp(\n        color8Light2,\n        other.color8Light2,\n        t,\n      )!,\n      color8Light3: Color.lerp(\n        color8Light3,\n        other.color8Light3,\n        t,\n      )!,\n      color8Thick1: Color.lerp(\n        color8Thick1,\n        other.color8Thick1,\n        t,\n      )!,\n      color8Thick2: Color.lerp(\n        color8Thick2,\n        other.color8Thick2,\n        t,\n      )!,\n      color8Thick3: Color.lerp(\n        color8Thick3,\n        other.color8Thick3,\n        t,\n      )!,\n      color9Light1: Color.lerp(\n        color9Light1,\n        other.color9Light1,\n        t,\n      )!,\n      color9Light2: Color.lerp(\n        color9Light2,\n        other.color9Light2,\n        t,\n      )!,\n      color9Light3: Color.lerp(\n        color9Light3,\n        other.color9Light3,\n        t,\n      )!,\n      color9Thick1: Color.lerp(\n        color9Thick1,\n        other.color9Thick1,\n        t,\n      )!,\n      color9Thick2: Color.lerp(\n        color9Thick2,\n        other.color9Thick2,\n        t,\n      )!,\n      color9Thick3: Color.lerp(\n        color9Thick3,\n        other.color9Thick3,\n        t,\n      )!,\n      color10Light1: Color.lerp(\n        color10Light1,\n        other.color10Light1,\n        t,\n      )!,\n      color10Light2: Color.lerp(\n        color10Light2,\n        other.color10Light2,\n        t,\n      )!,\n      color10Light3: Color.lerp(\n        color10Light3,\n        other.color10Light3,\n        t,\n      )!,\n      color10Thick1: Color.lerp(\n        color10Thick1,\n        other.color10Thick1,\n        t,\n      )!,\n      color10Thick2: Color.lerp(\n        color10Thick2,\n        other.color10Thick2,\n        t,\n      )!,\n      color10Thick3: Color.lerp(\n        color10Thick3,\n        other.color10Thick3,\n        t,\n      )!,\n      color11Light1: Color.lerp(\n        color11Light1,\n        other.color11Light1,\n        t,\n      )!,\n      color11Light2: Color.lerp(\n        color11Light2,\n        other.color11Light2,\n        t,\n      )!,\n      color11Light3: Color.lerp(\n        color11Light3,\n        other.color11Light3,\n        t,\n      )!,\n      color11Thick1: Color.lerp(\n        color11Thick1,\n        other.color11Thick1,\n        t,\n      )!,\n      color11Thick2: Color.lerp(\n        color11Thick2,\n        other.color11Thick2,\n        t,\n      )!,\n      color11Thick3: Color.lerp(\n        color11Thick3,\n        other.color11Thick3,\n        t,\n      )!,\n      color12Light1: Color.lerp(\n        color12Light1,\n        other.color12Light1,\n        t,\n      )!,\n      color12Light2: Color.lerp(\n        color12Light2,\n        other.color12Light2,\n        t,\n      )!,\n      color12Light3: Color.lerp(\n        color12Light3,\n        other.color12Light3,\n        t,\n      )!,\n      color12Thick1: Color.lerp(\n        color12Thick1,\n        other.color12Thick1,\n        t,\n      )!,\n      color12Thick2: Color.lerp(\n        color12Thick2,\n        other.color12Thick2,\n        t,\n      )!,\n      color12Thick3: Color.lerp(\n        color12Thick3,\n        other.color12Thick3,\n        t,\n      )!,\n      color13Light1: Color.lerp(\n        color13Light1,\n        other.color13Light1,\n        t,\n      )!,\n      color13Light2: Color.lerp(\n        color13Light2,\n        other.color13Light2,\n        t,\n      )!,\n      color13Light3: Color.lerp(\n        color13Light3,\n        other.color13Light3,\n        t,\n      )!,\n      color13Thick1: Color.lerp(\n        color13Thick1,\n        other.color13Thick1,\n        t,\n      )!,\n      color13Thick2: Color.lerp(\n        color13Thick2,\n        other.color13Thick2,\n        t,\n      )!,\n      color13Thick3: Color.lerp(\n        color13Thick3,\n        other.color13Thick3,\n        t,\n      )!,\n      color14Light1: Color.lerp(\n        color14Light1,\n        other.color14Light1,\n        t,\n      )!,\n      color14Light2: Color.lerp(\n        color14Light2,\n        other.color14Light2,\n        t,\n      )!,\n      color14Light3: Color.lerp(\n        color14Light3,\n        other.color14Light3,\n        t,\n      )!,\n      color14Thick1: Color.lerp(\n        color14Thick1,\n        other.color14Thick1,\n        t,\n      )!,\n      color14Thick2: Color.lerp(\n        color14Thick2,\n        other.color14Thick2,\n        t,\n      )!,\n      color14Thick3: Color.lerp(\n        color14Thick3,\n        other.color14Thick3,\n        t,\n      )!,\n      color15Light1: Color.lerp(\n        color15Light1,\n        other.color15Light1,\n        t,\n      )!,\n      color15Light2: Color.lerp(\n        color15Light2,\n        other.color15Light2,\n        t,\n      )!,\n      color15Light3: Color.lerp(\n        color15Light3,\n        other.color15Light3,\n        t,\n      )!,\n      color15Thick1: Color.lerp(\n        color15Thick1,\n        other.color15Thick1,\n        t,\n      )!,\n      color15Thick2: Color.lerp(\n        color15Thick2,\n        other.color15Thick2,\n        t,\n      )!,\n      color15Thick3: Color.lerp(\n        color15Thick3,\n        other.color15Thick3,\n        t,\n      )!,\n      color16Light1: Color.lerp(\n        color16Light1,\n        other.color16Light1,\n        t,\n      )!,\n      color16Light2: Color.lerp(\n        color16Light2,\n        other.color16Light2,\n        t,\n      )!,\n      color16Light3: Color.lerp(\n        color16Light3,\n        other.color16Light3,\n        t,\n      )!,\n      color16Thick1: Color.lerp(\n        color16Thick1,\n        other.color16Thick1,\n        t,\n      )!,\n      color16Thick2: Color.lerp(\n        color16Thick2,\n        other.color16Thick2,\n        t,\n      )!,\n      color16Thick3: Color.lerp(\n        color16Thick3,\n        other.color16Thick3,\n        t,\n      )!,\n      color17Light1: Color.lerp(\n        color17Light1,\n        other.color17Light1,\n        t,\n      )!,\n      color17Light2: Color.lerp(\n        color17Light2,\n        other.color17Light2,\n        t,\n      )!,\n      color17Light3: Color.lerp(\n        color17Light3,\n        other.color17Light3,\n        t,\n      )!,\n      color17Thick1: Color.lerp(\n        color17Thick1,\n        other.color17Thick1,\n        t,\n      )!,\n      color17Thick2: Color.lerp(\n        color17Thick2,\n        other.color17Thick2,\n        t,\n      )!,\n      color17Thick3: Color.lerp(\n        color17Thick3,\n        other.color17Thick3,\n        t,\n      )!,\n      color18Light1: Color.lerp(\n        color18Light1,\n        other.color18Light1,\n        t,\n      )!,\n      color18Light2: Color.lerp(\n        color18Light2,\n        other.color18Light2,\n        t,\n      )!,\n      color18Light3: Color.lerp(\n        color18Light3,\n        other.color18Light3,\n        t,\n      )!,\n      color18Thick1: Color.lerp(\n        color18Thick1,\n        other.color18Thick1,\n        t,\n      )!,\n      color18Thick2: Color.lerp(\n        color18Thick2,\n        other.color18Thick2,\n        t,\n      )!,\n      color18Thick3: Color.lerp(\n        color18Thick3,\n        other.color18Thick3,\n        t,\n      )!,\n      color19Light1: Color.lerp(\n        color19Light1,\n        other.color19Light1,\n        t,\n      )!,\n      color19Light2: Color.lerp(\n        color19Light2,\n        other.color19Light2,\n        t,\n      )!,\n      color19Light3: Color.lerp(\n        color19Light3,\n        other.color19Light3,\n        t,\n      )!,\n      color19Thick1: Color.lerp(\n        color19Thick1,\n        other.color19Thick1,\n        t,\n      )!,\n      color19Thick2: Color.lerp(\n        color19Thick2,\n        other.color19Thick2,\n        t,\n      )!,\n      color19Thick3: Color.lerp(\n        color19Thick3,\n        other.color19Thick3,\n        t,\n      )!,\n      color20Light1: Color.lerp(\n        color20Light1,\n        other.color20Light1,\n        t,\n      )!,\n      color20Light2: Color.lerp(\n        color20Light2,\n        other.color20Light2,\n        t,\n      )!,\n      color20Light3: Color.lerp(\n        color20Light3,\n        other.color20Light3,\n        t,\n      )!,\n      color20Thick1: Color.lerp(\n        color20Thick1,\n        other.color20Thick1,\n        t,\n      )!,\n      color20Thick2: Color.lerp(\n        color20Thick2,\n        other.color20Thick2,\n        t,\n      )!,\n      color20Thick3: Color.lerp(\n        color20Thick3,\n        other.color20Thick3,\n        t,\n      )!,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass AppFlowyBorderColorScheme {\n  AppFlowyBorderColorScheme({\n    required this.primary,\n    required this.primaryHover,\n    required this.secondary,\n    required this.secondaryHover,\n    required this.tertiary,\n    required this.tertiaryHover,\n    required this.themeThick,\n    required this.themeThickHover,\n    required this.infoThick,\n    required this.infoThickHover,\n    required this.successThick,\n    required this.successThickHover,\n    required this.warningThick,\n    required this.warningThickHover,\n    required this.errorThick,\n    required this.errorThickHover,\n    required this.featuredThick,\n    required this.featuredThickHover,\n  });\n\n  final Color primary;\n  final Color primaryHover;\n  final Color secondary;\n  final Color secondaryHover;\n  final Color tertiary;\n  final Color tertiaryHover;\n  final Color themeThick;\n  final Color themeThickHover;\n  final Color infoThick;\n  final Color infoThickHover;\n  final Color successThick;\n  final Color successThickHover;\n  final Color warningThick;\n  final Color warningThickHover;\n  final Color errorThick;\n  final Color errorThickHover;\n  final Color featuredThick;\n  final Color featuredThickHover;\n\n  AppFlowyBorderColorScheme lerp(\n    AppFlowyBorderColorScheme other,\n    double t,\n  ) {\n    return AppFlowyBorderColorScheme(\n      primary: Color.lerp(\n        primary,\n        other.primary,\n        t,\n      )!,\n      primaryHover: Color.lerp(\n        primaryHover,\n        other.primaryHover,\n        t,\n      )!,\n      secondary: Color.lerp(\n        secondary,\n        other.secondary,\n        t,\n      )!,\n      secondaryHover: Color.lerp(\n        secondaryHover,\n        other.secondaryHover,\n        t,\n      )!,\n      tertiary: Color.lerp(\n        tertiary,\n        other.tertiary,\n        t,\n      )!,\n      tertiaryHover: Color.lerp(\n        tertiaryHover,\n        other.tertiaryHover,\n        t,\n      )!,\n      themeThick: Color.lerp(\n        themeThick,\n        other.themeThick,\n        t,\n      )!,\n      themeThickHover: Color.lerp(\n        themeThickHover,\n        other.themeThickHover,\n        t,\n      )!,\n      infoThick: Color.lerp(\n        infoThick,\n        other.infoThick,\n        t,\n      )!,\n      infoThickHover: Color.lerp(\n        infoThickHover,\n        other.infoThickHover,\n        t,\n      )!,\n      successThick: Color.lerp(\n        successThick,\n        other.successThick,\n        t,\n      )!,\n      successThickHover: Color.lerp(\n        successThickHover,\n        other.successThickHover,\n        t,\n      )!,\n      warningThick: Color.lerp(\n        warningThick,\n        other.warningThick,\n        t,\n      )!,\n      warningThickHover: Color.lerp(\n        warningThickHover,\n        other.warningThickHover,\n        t,\n      )!,\n      errorThick: Color.lerp(\n        errorThick,\n        other.errorThick,\n        t,\n      )!,\n      errorThickHover: Color.lerp(\n        errorThickHover,\n        other.errorThickHover,\n        t,\n      )!,\n      featuredThick: Color.lerp(\n        featuredThick,\n        other.featuredThick,\n        t,\n      )!,\n      featuredThickHover: Color.lerp(\n        featuredThickHover,\n        other.featuredThickHover,\n        t,\n      )!,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass AppFlowyBrandColorScheme {\n  const AppFlowyBrandColorScheme({\n    required this.skyline,\n    required this.aqua,\n    required this.violet,\n    required this.amethyst,\n    required this.berry,\n    required this.coral,\n    required this.golden,\n    required this.amber,\n    required this.lemon,\n  });\n\n  final Color skyline;\n  final Color aqua;\n  final Color violet;\n  final Color amethyst;\n  final Color berry;\n  final Color coral;\n  final Color golden;\n  final Color amber;\n  final Color lemon;\n\n  AppFlowyBrandColorScheme lerp(\n    AppFlowyBrandColorScheme other,\n    double t,\n  ) {\n    return AppFlowyBrandColorScheme(\n      skyline: Color.lerp(skyline, other.skyline, t)!,\n      aqua: Color.lerp(aqua, other.aqua, t)!,\n      violet: Color.lerp(violet, other.violet, t)!,\n      amethyst: Color.lerp(amethyst, other.amethyst, t)!,\n      berry: Color.lerp(berry, other.berry, t)!,\n      coral: Color.lerp(coral, other.coral, t)!,\n      golden: Color.lerp(golden, other.golden, t)!,\n      amber: Color.lerp(amber, other.amber, t)!,\n      lemon: Color.lerp(lemon, other.lemon, t)!,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/color_scheme.dart",
    "content": "export 'background_color_scheme.dart';\nexport 'badge_color_scheme.dart';\nexport 'border_color_scheme.dart';\nexport 'brand_color_scheme.dart';\nexport 'fill_color_scheme.dart';\nexport 'icon_color_scheme.dart';\nexport 'other_color_scheme.dart';\nexport 'surface_color_scheme.dart';\nexport 'surface_container_color_scheme.dart';\nexport 'text_color_scheme.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass AppFlowyFillColorScheme {\n  const AppFlowyFillColorScheme({\n    required this.primary,\n    required this.primaryHover,\n    required this.secondary,\n    required this.secondaryHover,\n    required this.tertiary,\n    required this.tertiaryHover,\n    required this.quaternary,\n    required this.quaternaryHover,\n    required this.content,\n    required this.contentHover,\n    required this.contentVisible,\n    required this.contentVisibleHover,\n    required this.themeThick,\n    required this.themeThickHover,\n    required this.themeSelect,\n    required this.textSelect,\n    required this.infoLight,\n    required this.infoLightHover,\n    required this.infoThick,\n    required this.infoThickHover,\n    required this.successLight,\n    required this.successLightHover,\n    required this.warningLight,\n    required this.warningLightHover,\n    required this.errorLight,\n    required this.errorLightHover,\n    required this.errorThick,\n    required this.errorThickHover,\n    required this.errorSelect,\n    required this.featuredLight,\n    required this.featuredLightHover,\n    required this.featuredThick,\n    required this.featuredThickHover,\n  });\n\n  final Color primary;\n  final Color primaryHover;\n  final Color secondary;\n  final Color secondaryHover;\n  final Color tertiary;\n  final Color tertiaryHover;\n  final Color quaternary;\n  final Color quaternaryHover;\n  final Color content;\n  final Color contentHover;\n  final Color contentVisible;\n  final Color contentVisibleHover;\n  final Color themeThick;\n  final Color themeThickHover;\n  final Color themeSelect;\n  final Color textSelect;\n  final Color infoLight;\n  final Color infoLightHover;\n  final Color infoThick;\n  final Color infoThickHover;\n  final Color successLight;\n  final Color successLightHover;\n  final Color warningLight;\n  final Color warningLightHover;\n  final Color errorLight;\n  final Color errorLightHover;\n  final Color errorThick;\n  final Color errorThickHover;\n  final Color errorSelect;\n  final Color featuredLight;\n  final Color featuredLightHover;\n  final Color featuredThick;\n  final Color featuredThickHover;\n\n  AppFlowyFillColorScheme lerp(\n    AppFlowyFillColorScheme other,\n    double t,\n  ) {\n    return AppFlowyFillColorScheme(\n      primary: Color.lerp(\n        primary,\n        other.primary,\n        t,\n      )!,\n      primaryHover: Color.lerp(\n        primaryHover,\n        other.primaryHover,\n        t,\n      )!,\n      secondary: Color.lerp(\n        secondary,\n        other.secondary,\n        t,\n      )!,\n      secondaryHover: Color.lerp(\n        secondaryHover,\n        other.secondaryHover,\n        t,\n      )!,\n      tertiary: Color.lerp(\n        tertiary,\n        other.tertiary,\n        t,\n      )!,\n      tertiaryHover: Color.lerp(\n        tertiaryHover,\n        other.tertiaryHover,\n        t,\n      )!,\n      quaternary: Color.lerp(\n        quaternary,\n        other.quaternary,\n        t,\n      )!,\n      quaternaryHover: Color.lerp(\n        quaternaryHover,\n        other.quaternaryHover,\n        t,\n      )!,\n      content: Color.lerp(\n        content,\n        other.content,\n        t,\n      )!,\n      contentHover: Color.lerp(\n        contentHover,\n        other.contentHover,\n        t,\n      )!,\n      contentVisible: Color.lerp(\n        contentVisible,\n        other.contentVisible,\n        t,\n      )!,\n      contentVisibleHover: Color.lerp(\n        contentVisibleHover,\n        other.contentVisibleHover,\n        t,\n      )!,\n      themeThick: Color.lerp(\n        themeThick,\n        other.themeThick,\n        t,\n      )!,\n      themeThickHover: Color.lerp(\n        themeThickHover,\n        other.themeThickHover,\n        t,\n      )!,\n      themeSelect: Color.lerp(\n        themeSelect,\n        other.themeSelect,\n        t,\n      )!,\n      textSelect: Color.lerp(\n        textSelect,\n        other.textSelect,\n        t,\n      )!,\n      infoLight: Color.lerp(\n        infoLight,\n        other.infoLight,\n        t,\n      )!,\n      infoLightHover: Color.lerp(\n        infoLightHover,\n        other.infoLightHover,\n        t,\n      )!,\n      infoThick: Color.lerp(\n        infoThick,\n        other.infoThick,\n        t,\n      )!,\n      infoThickHover: Color.lerp(\n        infoThickHover,\n        other.infoThickHover,\n        t,\n      )!,\n      successLight: Color.lerp(\n        successLight,\n        other.successLight,\n        t,\n      )!,\n      successLightHover: Color.lerp(\n        successLightHover,\n        other.successLightHover,\n        t,\n      )!,\n      warningLight: Color.lerp(\n        warningLight,\n        other.warningLight,\n        t,\n      )!,\n      warningLightHover: Color.lerp(\n        warningLightHover,\n        other.warningLightHover,\n        t,\n      )!,\n      errorLight: Color.lerp(\n        errorLight,\n        other.errorLight,\n        t,\n      )!,\n      errorLightHover: Color.lerp(\n        errorLightHover,\n        other.errorLightHover,\n        t,\n      )!,\n      errorThick: Color.lerp(\n        errorThick,\n        other.errorThick,\n        t,\n      )!,\n      errorThickHover: Color.lerp(\n        errorThickHover,\n        other.errorThickHover,\n        t,\n      )!,\n      errorSelect: Color.lerp(\n        errorSelect,\n        other.errorSelect,\n        t,\n      )!,\n      featuredLight: Color.lerp(\n        featuredLight,\n        other.featuredLight,\n        t,\n      )!,\n      featuredLightHover: Color.lerp(\n        featuredLightHover,\n        other.featuredLightHover,\n        t,\n      )!,\n      featuredThick: Color.lerp(\n        featuredThick,\n        other.featuredThick,\n        t,\n      )!,\n      featuredThickHover: Color.lerp(\n        featuredThickHover,\n        other.featuredThickHover,\n        t,\n      )!,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass AppFlowyIconColorScheme {\n  const AppFlowyIconColorScheme({\n    required this.primary,\n    required this.secondary,\n    required this.tertiary,\n    required this.quaternary,\n    required this.onFill,\n    required this.featuredThick,\n    required this.featuredThickHover,\n    required this.infoThick,\n    required this.infoThickHover,\n    required this.successThick,\n    required this.successThickHover,\n    required this.warningThick,\n    required this.warningThickHover,\n    required this.errorThick,\n    required this.errorThickHover,\n  });\n\n  final Color primary;\n  final Color secondary;\n  final Color tertiary;\n  final Color quaternary;\n  final Color onFill;\n  final Color featuredThick;\n  final Color featuredThickHover;\n  final Color infoThick;\n  final Color infoThickHover;\n  final Color successThick;\n  final Color successThickHover;\n  final Color warningThick;\n  final Color warningThickHover;\n  final Color errorThick;\n  final Color errorThickHover;\n\n  AppFlowyIconColorScheme lerp(\n    AppFlowyIconColorScheme other,\n    double t,\n  ) {\n    return AppFlowyIconColorScheme(\n      primary: Color.lerp(\n        primary,\n        other.primary,\n        t,\n      )!,\n      secondary: Color.lerp(\n        secondary,\n        other.secondary,\n        t,\n      )!,\n      tertiary: Color.lerp(\n        tertiary,\n        other.tertiary,\n        t,\n      )!,\n      quaternary: Color.lerp(\n        quaternary,\n        other.quaternary,\n        t,\n      )!,\n      onFill: Color.lerp(\n        onFill,\n        other.onFill,\n        t,\n      )!,\n      featuredThick: Color.lerp(\n        featuredThick,\n        other.featuredThick,\n        t,\n      )!,\n      featuredThickHover: Color.lerp(\n        featuredThickHover,\n        other.featuredThickHover,\n        t,\n      )!,\n      infoThick: Color.lerp(\n        infoThick,\n        other.infoThick,\n        t,\n      )!,\n      infoThickHover: Color.lerp(\n        infoThickHover,\n        other.infoThickHover,\n        t,\n      )!,\n      successThick: Color.lerp(\n        successThick,\n        other.successThick,\n        t,\n      )!,\n      successThickHover: Color.lerp(\n        successThickHover,\n        other.successThickHover,\n        t,\n      )!,\n      warningThick: Color.lerp(\n        warningThick,\n        other.warningThick,\n        t,\n      )!,\n      warningThickHover: Color.lerp(\n        warningThickHover,\n        other.warningThickHover,\n        t,\n      )!,\n      errorThick: Color.lerp(\n        errorThick,\n        other.errorThick,\n        t,\n      )!,\n      errorThickHover: Color.lerp(\n        errorThickHover,\n        other.errorThickHover,\n        t,\n      )!,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart",
    "content": "import 'dart:ui';\n\nclass AppFlowyOtherColorsColorScheme {\n  const AppFlowyOtherColorsColorScheme({\n    required this.textHighlight,\n  });\n\n  final Color textHighlight;\n\n  AppFlowyOtherColorsColorScheme lerp(\n    AppFlowyOtherColorsColorScheme other,\n    double t,\n  ) {\n    return AppFlowyOtherColorsColorScheme(\n      textHighlight: Color.lerp(textHighlight, other.textHighlight, t)!,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass AppFlowySurfaceColorScheme {\n  const AppFlowySurfaceColorScheme({\n    required this.primary,\n    required this.primaryHover,\n    required this.layer01,\n    required this.layer01Hover,\n    required this.layer02,\n    required this.layer02Hover,\n    required this.layer03,\n    required this.layer03Hover,\n    required this.layer04,\n    required this.layer04Hover,\n    required this.inverse,\n    required this.secondary,\n    required this.overlay,\n  });\n\n  final Color primary;\n  final Color primaryHover;\n  final Color layer01;\n  final Color layer01Hover;\n  final Color layer02;\n  final Color layer02Hover;\n  final Color layer03;\n  final Color layer03Hover;\n  final Color layer04;\n  final Color layer04Hover;\n  final Color inverse;\n  final Color secondary;\n  final Color overlay;\n\n  AppFlowySurfaceColorScheme lerp(\n    AppFlowySurfaceColorScheme other,\n    double t,\n  ) {\n    return AppFlowySurfaceColorScheme(\n      primary: Color.lerp(\n        primary,\n        other.primary,\n        t,\n      )!,\n      primaryHover: Color.lerp(\n        primaryHover,\n        other.primaryHover,\n        t,\n      )!,\n      layer01: Color.lerp(\n        layer01,\n        other.layer01,\n        t,\n      )!,\n      layer01Hover: Color.lerp(\n        layer01Hover,\n        other.layer01Hover,\n        t,\n      )!,\n      layer02: Color.lerp(\n        layer02,\n        other.layer02,\n        t,\n      )!,\n      layer02Hover: Color.lerp(\n        layer02Hover,\n        other.layer02Hover,\n        t,\n      )!,\n      layer03: Color.lerp(\n        layer03,\n        other.layer03,\n        t,\n      )!,\n      layer03Hover: Color.lerp(\n        layer03Hover,\n        other.layer03Hover,\n        t,\n      )!,\n      layer04: Color.lerp(\n        layer04,\n        other.layer04,\n        t,\n      )!,\n      layer04Hover: Color.lerp(\n        layer04Hover,\n        other.layer04Hover,\n        t,\n      )!,\n      inverse: Color.lerp(\n        inverse,\n        other.inverse,\n        t,\n      )!,\n      secondary: Color.lerp(\n        secondary,\n        other.secondary,\n        t,\n      )!,\n      overlay: Color.lerp(\n        overlay,\n        other.overlay,\n        t,\n      )!,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_container_color_scheme.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass AppFlowySurfaceContainerColorScheme {\n  const AppFlowySurfaceContainerColorScheme({\n    required this.layer01,\n    required this.layer02,\n    required this.layer03,\n  });\n\n  final Color layer01;\n  final Color layer02;\n  final Color layer03;\n\n  AppFlowySurfaceContainerColorScheme lerp(\n    AppFlowySurfaceContainerColorScheme other,\n    double t,\n  ) {\n    return AppFlowySurfaceContainerColorScheme(\n      layer01: Color.lerp(\n        layer01,\n        other.layer01,\n        t,\n      )!,\n      layer02: Color.lerp(\n        layer02,\n        other.layer02,\n        t,\n      )!,\n      layer03: Color.lerp(\n        layer03,\n        other.layer03,\n        t,\n      )!,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass AppFlowyTextColorScheme {\n  const AppFlowyTextColorScheme({\n    required this.primary,\n    required this.secondary,\n    required this.tertiary,\n    required this.quaternary,\n    required this.onFill,\n    required this.action,\n    required this.actionHover,\n    required this.info,\n    required this.infoHover,\n    required this.success,\n    required this.successHover,\n    required this.warning,\n    required this.warningHover,\n    required this.error,\n    required this.errorHover,\n    required this.featured,\n    required this.featuredHover,\n  });\n\n  final Color primary;\n  final Color secondary;\n  final Color tertiary;\n  final Color quaternary;\n  final Color onFill;\n  final Color action;\n  final Color actionHover;\n  final Color info;\n  final Color infoHover;\n  final Color success;\n  final Color successHover;\n  final Color warning;\n  final Color warningHover;\n  final Color error;\n  final Color errorHover;\n  final Color featured;\n  final Color featuredHover;\n\n  AppFlowyTextColorScheme lerp(\n    AppFlowyTextColorScheme other,\n    double t,\n  ) {\n    return AppFlowyTextColorScheme(\n      primary: Color.lerp(\n        primary,\n        other.primary,\n        t,\n      )!,\n      secondary: Color.lerp(\n        secondary,\n        other.secondary,\n        t,\n      )!,\n      tertiary: Color.lerp(\n        tertiary,\n        other.tertiary,\n        t,\n      )!,\n      quaternary: Color.lerp(\n        quaternary,\n        other.quaternary,\n        t,\n      )!,\n      onFill: Color.lerp(\n        onFill,\n        other.onFill,\n        t,\n      )!,\n      action: Color.lerp(\n        action,\n        other.action,\n        t,\n      )!,\n      actionHover: Color.lerp(\n        actionHover,\n        other.actionHover,\n        t,\n      )!,\n      info: Color.lerp(\n        info,\n        other.info,\n        t,\n      )!,\n      infoHover: Color.lerp(\n        infoHover,\n        other.infoHover,\n        t,\n      )!,\n      success: Color.lerp(\n        success,\n        other.success,\n        t,\n      )!,\n      successHover: Color.lerp(\n        successHover,\n        other.successHover,\n        t,\n      )!,\n      warning: Color.lerp(\n        warning,\n        other.warning,\n        t,\n      )!,\n      warningHover: Color.lerp(\n        warningHover,\n        other.warningHover,\n        t,\n      )!,\n      error: Color.lerp(\n        error,\n        other.error,\n        t,\n      )!,\n      errorHover: Color.lerp(\n        errorHover,\n        other.errorHover,\n        t,\n      )!,\n      featured: Color.lerp(\n        featured,\n        other.featured,\n        t,\n      )!,\n      featuredHover: Color.lerp(\n        featuredHover,\n        other.featuredHover,\n        t,\n      )!,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/shadow/shadow.dart",
    "content": "import 'package:flutter/widgets.dart';\n\nclass AppFlowyShadow {\n  AppFlowyShadow({\n    required this.small,\n    required this.medium,\n  });\n\n  final List<BoxShadow> small;\n  final List<BoxShadow> medium;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/spacing/spacing.dart",
    "content": "class AppFlowySpacing {\n  const AppFlowySpacing({\n    required this.xs,\n    required this.s,\n    required this.m,\n    required this.l,\n    required this.xl,\n    required this.xxl,\n  });\n\n  final double xs;\n  final double s;\n  final double m;\n  final double l;\n  final double xl;\n  final double xxl;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/base/default_text_style.dart",
    "content": "import 'package:flutter/widgets.dart';\n\nabstract class TextThemeType {\n  const TextThemeType({\n    required this.fontFamily,\n  });\n\n  final String fontFamily;\n\n  TextStyle standard({\n    String? family,\n    Color? color,\n    FontWeight? weight,\n  });\n\n  TextStyle enhanced({\n    String? family,\n    Color? color,\n    FontWeight? weight,\n  });\n\n  TextStyle prominent({\n    String? family,\n    Color? color,\n    FontWeight? weight,\n  });\n\n  TextStyle underline({\n    String? family,\n    Color? color,\n    FontWeight? weight,\n  });\n}\n\nclass TextThemeHeading1 extends TextThemeType {\n  const TextThemeHeading1({\n    required super.fontFamily,\n  });\n\n  @override\n  TextStyle standard({\n    String? family,\n    Color? color,\n    FontWeight? weight,\n  }) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        fontSize: 36,\n        height: 40 / 36,\n        color: color,\n        weight: weight ?? FontWeight.w400,\n      );\n\n  @override\n  TextStyle enhanced({\n    String? family,\n    Color? color,\n    FontWeight? weight,\n  }) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        fontSize: 36,\n        height: 40 / 36,\n        color: color,\n        weight: weight ?? FontWeight.w600,\n      );\n\n  @override\n  TextStyle prominent({\n    String? family,\n    Color? color,\n    FontWeight? weight,\n  }) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        fontSize: 36,\n        height: 40 / 36,\n        color: color,\n        weight: weight ?? FontWeight.w700,\n      );\n\n  @override\n  TextStyle underline({\n    String? family,\n    Color? color,\n    FontWeight? weight,\n  }) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        fontSize: 36,\n        height: 40 / 36,\n        color: color,\n        weight: weight ?? FontWeight.bold,\n        decoration: TextDecoration.underline,\n      );\n\n  static TextStyle _defaultTextStyle({\n    required String family,\n    required double fontSize,\n    required double height,\n    TextDecoration decoration = TextDecoration.none,\n    Color? color,\n    FontWeight weight = FontWeight.bold,\n  }) =>\n      TextStyle(\n        inherit: false,\n        fontSize: fontSize,\n        decoration: decoration,\n        fontStyle: FontStyle.normal,\n        fontWeight: weight,\n        height: height,\n        fontFamily: family,\n        color: color,\n        textBaseline: TextBaseline.alphabetic,\n        leadingDistribution: TextLeadingDistribution.even,\n      );\n}\n\nclass TextThemeHeading2 extends TextThemeType {\n  const TextThemeHeading2({\n    required super.fontFamily,\n  });\n\n  @override\n  TextStyle standard({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.w400,\n      );\n\n  @override\n  TextStyle enhanced({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.w600,\n      );\n\n  @override\n  TextStyle prominent({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.w700,\n      );\n\n  @override\n  TextStyle underline({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.w400,\n        decoration: TextDecoration.underline,\n      );\n\n  static TextStyle _defaultTextStyle({\n    required String family,\n    double fontSize = 24,\n    double height = 32 / 24,\n    TextDecoration decoration = TextDecoration.none,\n    FontWeight weight = FontWeight.w400,\n    Color? color,\n  }) =>\n      TextStyle(\n        inherit: false,\n        fontSize: fontSize,\n        decoration: decoration,\n        fontStyle: FontStyle.normal,\n        fontWeight: weight,\n        height: height,\n        fontFamily: family,\n        color: color,\n        textBaseline: TextBaseline.alphabetic,\n        leadingDistribution: TextLeadingDistribution.even,\n      );\n}\n\nclass TextThemeHeading3 extends TextThemeType {\n  const TextThemeHeading3({\n    required super.fontFamily,\n  });\n\n  @override\n  TextStyle standard({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.w400,\n      );\n\n  @override\n  TextStyle enhanced({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.w600,\n      );\n\n  @override\n  TextStyle prominent({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.w700,\n      );\n\n  @override\n  TextStyle underline({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.w400,\n        decoration: TextDecoration.underline,\n      );\n\n  static TextStyle _defaultTextStyle({\n    required String family,\n    double fontSize = 20,\n    double height = 28 / 20,\n    TextDecoration decoration = TextDecoration.none,\n    FontWeight weight = FontWeight.w400,\n    Color? color,\n  }) =>\n      TextStyle(\n        inherit: false,\n        fontSize: fontSize,\n        decoration: decoration,\n        fontStyle: FontStyle.normal,\n        fontWeight: weight,\n        height: height,\n        fontFamily: family,\n        color: color,\n        textBaseline: TextBaseline.alphabetic,\n        leadingDistribution: TextLeadingDistribution.even,\n      );\n}\n\nclass TextThemeHeading4 extends TextThemeType {\n  const TextThemeHeading4({\n    required super.fontFamily,\n  });\n\n  @override\n  TextStyle standard({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.w400,\n      );\n\n  @override\n  TextStyle enhanced({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.w600,\n      );\n\n  @override\n  TextStyle prominent({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.w700,\n      );\n\n  @override\n  TextStyle underline({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.w400,\n        decoration: TextDecoration.underline,\n      );\n\n  static TextStyle _defaultTextStyle({\n    required String family,\n    double fontSize = 16,\n    double height = 22 / 16,\n    TextDecoration decoration = TextDecoration.none,\n    FontWeight weight = FontWeight.w400,\n    Color? color,\n  }) =>\n      TextStyle(\n        inherit: false,\n        fontSize: fontSize,\n        decoration: decoration,\n        fontStyle: FontStyle.normal,\n        fontWeight: weight,\n        height: height,\n        fontFamily: family,\n        color: color,\n        textBaseline: TextBaseline.alphabetic,\n        leadingDistribution: TextLeadingDistribution.even,\n      );\n}\n\nclass TextThemeHeadline extends TextThemeType {\n  const TextThemeHeadline({\n    required super.fontFamily,\n  });\n\n  @override\n  TextStyle standard({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.normal,\n      );\n\n  @override\n  TextStyle enhanced({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.w500,\n      );\n\n  @override\n  TextStyle prominent({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.bold,\n      );\n\n  @override\n  TextStyle underline({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.normal,\n        decoration: TextDecoration.underline,\n      );\n\n  static TextStyle _defaultTextStyle({\n    required String family,\n    double fontSize = 24,\n    double height = 36 / 24,\n    TextDecoration decoration = TextDecoration.none,\n    FontWeight weight = FontWeight.normal,\n    Color? color,\n  }) =>\n      TextStyle(\n        inherit: false,\n        fontSize: fontSize,\n        decoration: decoration,\n        fontStyle: FontStyle.normal,\n        fontWeight: weight,\n        height: height,\n        fontFamily: family,\n        textBaseline: TextBaseline.alphabetic,\n        leadingDistribution: TextLeadingDistribution.even,\n        color: color,\n      );\n}\n\nclass TextThemeTitle extends TextThemeType {\n  const TextThemeTitle({\n    required super.fontFamily,\n  });\n\n  @override\n  TextStyle standard({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.normal,\n      );\n\n  @override\n  TextStyle enhanced({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.w500,\n      );\n\n  @override\n  TextStyle prominent({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.bold,\n      );\n\n  @override\n  TextStyle underline({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.normal,\n        decoration: TextDecoration.underline,\n      );\n\n  static TextStyle _defaultTextStyle({\n    required String family,\n    double fontSize = 20,\n    double height = 28 / 20,\n    FontWeight weight = FontWeight.normal,\n    TextDecoration decoration = TextDecoration.none,\n    Color? color,\n  }) =>\n      TextStyle(\n        inherit: false,\n        fontSize: fontSize,\n        decoration: decoration,\n        fontStyle: FontStyle.normal,\n        fontWeight: weight,\n        height: height,\n        fontFamily: family,\n        textBaseline: TextBaseline.alphabetic,\n        leadingDistribution: TextLeadingDistribution.even,\n        color: color,\n      );\n}\n\nclass TextThemeBody extends TextThemeType {\n  const TextThemeBody({\n    required super.fontFamily,\n  });\n\n  @override\n  TextStyle standard({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.normal,\n      );\n\n  @override\n  TextStyle enhanced({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.w500,\n      );\n\n  @override\n  TextStyle prominent({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.bold,\n      );\n\n  @override\n  TextStyle underline({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.normal,\n        decoration: TextDecoration.underline,\n      );\n\n  static TextStyle _defaultTextStyle({\n    required String family,\n    double fontSize = 14,\n    double height = 20 / 14,\n    FontWeight weight = FontWeight.normal,\n    TextDecoration decoration = TextDecoration.none,\n    Color? color,\n  }) =>\n      TextStyle(\n        inherit: false,\n        fontSize: fontSize,\n        decoration: decoration,\n        fontStyle: FontStyle.normal,\n        fontWeight: weight,\n        height: height,\n        fontFamily: family,\n        textBaseline: TextBaseline.alphabetic,\n        leadingDistribution: TextLeadingDistribution.even,\n        color: color,\n      );\n}\n\nclass TextThemeCaption extends TextThemeType {\n  const TextThemeCaption({\n    required super.fontFamily,\n  });\n\n  @override\n  TextStyle standard({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.normal,\n      );\n\n  @override\n  TextStyle enhanced({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.w500,\n      );\n\n  @override\n  TextStyle prominent({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.bold,\n      );\n\n  @override\n  TextStyle underline({String? family, Color? color, FontWeight? weight}) =>\n      _defaultTextStyle(\n        family: family ?? super.fontFamily,\n        color: color,\n        weight: weight ?? FontWeight.normal,\n        decoration: TextDecoration.underline,\n      );\n\n  static TextStyle _defaultTextStyle({\n    required String family,\n    double fontSize = 12,\n    double height = 18 / 12,\n    FontWeight weight = FontWeight.normal,\n    TextDecoration decoration = TextDecoration.none,\n    Color? color,\n  }) =>\n      TextStyle(\n        inherit: false,\n        fontSize: fontSize,\n        decoration: decoration,\n        fontStyle: FontStyle.normal,\n        fontWeight: weight,\n        height: height,\n        fontFamily: family,\n        textBaseline: TextBaseline.alphabetic,\n        leadingDistribution: TextLeadingDistribution.even,\n        color: color,\n      );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart",
    "content": "import 'package:appflowy_ui/src/theme/definition/text_style/base/default_text_style.dart';\n\nclass AppFlowyBaseTextStyle {\n  factory AppFlowyBaseTextStyle.customFontFamily(String fontFamily) =>\n      AppFlowyBaseTextStyle(\n        heading1: TextThemeHeading1(fontFamily: fontFamily),\n        heading2: TextThemeHeading2(fontFamily: fontFamily),\n        heading3: TextThemeHeading3(fontFamily: fontFamily),\n        heading4: TextThemeHeading4(fontFamily: fontFamily),\n        headline: TextThemeHeadline(fontFamily: fontFamily),\n        title: TextThemeTitle(fontFamily: fontFamily),\n        body: TextThemeBody(fontFamily: fontFamily),\n        caption: TextThemeCaption(fontFamily: fontFamily),\n      );\n\n  const AppFlowyBaseTextStyle({\n    this.heading1 = const TextThemeHeading1(fontFamily: ''),\n    this.heading2 = const TextThemeHeading2(fontFamily: ''),\n    this.heading3 = const TextThemeHeading3(fontFamily: ''),\n    this.heading4 = const TextThemeHeading4(fontFamily: ''),\n    this.headline = const TextThemeHeadline(fontFamily: ''),\n    this.title = const TextThemeTitle(fontFamily: ''),\n    this.body = const TextThemeBody(fontFamily: ''),\n    this.caption = const TextThemeCaption(fontFamily: ''),\n  });\n\n  final TextThemeType heading1;\n  final TextThemeType heading2;\n  final TextThemeType heading3;\n  final TextThemeType heading4;\n  final TextThemeType headline;\n  final TextThemeType title;\n  final TextThemeType body;\n  final TextThemeType caption;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart",
    "content": "import 'border_radius/border_radius.dart';\nimport 'color_scheme/color_scheme.dart';\nimport 'shadow/shadow.dart';\nimport 'spacing/spacing.dart';\nimport 'text_style/text_style.dart';\n\n/// [AppFlowyThemeData] defines the structure of the design system, and contains\n/// the data that all child widgets will have access to.\nclass AppFlowyThemeData {\n  const AppFlowyThemeData({\n    required this.textColorScheme,\n    required this.textStyle,\n    required this.iconColorScheme,\n    required this.borderColorScheme,\n    required this.backgroundColorScheme,\n    required this.fillColorScheme,\n    required this.surfaceColorScheme,\n    required this.borderRadius,\n    required this.spacing,\n    required this.shadow,\n    required this.brandColorScheme,\n    required this.surfaceContainerColorScheme,\n    required this.badgeColorScheme,\n    required this.otherColorsColorScheme,\n  });\n\n  final AppFlowyTextColorScheme textColorScheme;\n\n  final AppFlowyBaseTextStyle textStyle;\n\n  final AppFlowyIconColorScheme iconColorScheme;\n\n  final AppFlowyBorderColorScheme borderColorScheme;\n\n  final AppFlowyBackgroundColorScheme backgroundColorScheme;\n\n  final AppFlowyFillColorScheme fillColorScheme;\n\n  final AppFlowySurfaceColorScheme surfaceColorScheme;\n\n  final AppFlowyBorderRadius borderRadius;\n\n  final AppFlowySpacing spacing;\n\n  final AppFlowyShadow shadow;\n\n  final AppFlowyBrandColorScheme brandColorScheme;\n\n  final AppFlowySurfaceContainerColorScheme surfaceContainerColorScheme;\n\n  final AppFlowyBadgeColorScheme badgeColorScheme;\n\n  final AppFlowyOtherColorsColorScheme otherColorsColorScheme;\n\n  static AppFlowyThemeData lerp(\n    AppFlowyThemeData begin,\n    AppFlowyThemeData end,\n    double t,\n  ) {\n    return AppFlowyThemeData(\n      textColorScheme: begin.textColorScheme.lerp(\n        end.textColorScheme,\n        t,\n      ),\n      textStyle: end.textStyle,\n      iconColorScheme: begin.iconColorScheme.lerp(\n        end.iconColorScheme,\n        t,\n      ),\n      borderColorScheme: begin.borderColorScheme.lerp(\n        end.borderColorScheme,\n        t,\n      ),\n      backgroundColorScheme: begin.backgroundColorScheme.lerp(\n        end.backgroundColorScheme,\n        t,\n      ),\n      fillColorScheme: begin.fillColorScheme.lerp(\n        end.fillColorScheme,\n        t,\n      ),\n      surfaceColorScheme: begin.surfaceColorScheme.lerp(\n        end.surfaceColorScheme,\n        t,\n      ),\n      borderRadius: end.borderRadius,\n      spacing: end.spacing,\n      shadow: end.shadow,\n      brandColorScheme: begin.brandColorScheme.lerp(\n        end.brandColorScheme,\n        t,\n      ),\n      otherColorsColorScheme: begin.otherColorsColorScheme.lerp(\n        end.otherColorsColorScheme,\n        t,\n      ),\n      surfaceContainerColorScheme: begin.surfaceContainerColorScheme.lerp(\n        end.surfaceContainerColorScheme,\n        t,\n      ),\n      badgeColorScheme: begin.badgeColorScheme.lerp(\n        end.badgeColorScheme,\n        t,\n      ),\n    );\n  }\n}\n\n/// [AppFlowyThemeBuilder] is used to build the light and dark themes. Extend\n/// this class to create a built-in theme, or use the [CustomTheme] class to\n/// create a custom theme from JSON data.\n///\n/// See also:\n///\n/// - [AppFlowyThemeData] for the main theme data class.\nabstract class AppFlowyThemeBuilder {\n  const AppFlowyThemeBuilder();\n\n  AppFlowyThemeData light({\n    String? fontFamily,\n  });\n\n  AppFlowyThemeData dark({\n    String? fontFamily,\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart",
    "content": "export 'appflowy_theme.dart';\nexport 'data/built_in_themes.dart';\nexport 'definition/border_radius/border_radius.dart';\nexport 'definition/color_scheme/color_scheme.dart';\nexport 'definition/theme_data.dart';\nexport 'definition/spacing/spacing.dart';\nexport 'definition/shadow/shadow.dart';\nexport 'definition/text_style/text_style.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/pubspec.yaml",
    "content": "name: appflowy_ui\ndescription: \"A Flutter package for AppFlowy UI components and widgets\"\nversion: 1.0.0\nhomepage: https://github.com/appflowy-io/appflowy\n\nenvironment:\n  sdk: ^3.6.2\n  flutter: \">=1.17.0\"\n\ndependencies:\n  cached_network_image: ^3.4.1\n  flutter:\n    sdk: flutter\n  flutter_animate: ^4.5.2\n  flutter_lints: ^5.0.0\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/script/Primitive.Mode 1.tokens.json",
    "content": "{\n  \"Neutral\": {\n    \"100\": {\n      \"$type\": \"color\",\n      \"$value\": \"#f8faff\"\n    },\n    \"200\": {\n      \"$type\": \"color\",\n      \"$value\": \"#e4e8f5\"\n    },\n    \"300\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ced3e6\"\n    },\n    \"400\": {\n      \"$type\": \"color\",\n      \"$value\": \"#b5bbd3\"\n    },\n    \"500\": {\n      \"$type\": \"color\",\n      \"$value\": \"#989eb7\"\n    },\n    \"600\": {\n      \"$type\": \"color\",\n      \"$value\": \"#6f748c\"\n    },\n    \"700\": {\n      \"$type\": \"color\",\n      \"$value\": \"#54596e\"\n    },\n    \"800\": {\n      \"$type\": \"color\",\n      \"$value\": \"#3d404f\"\n    },\n    \"830\": {\n      \"$type\": \"color\",\n      \"$value\": \"#363845\"\n    },\n    \"850\": {\n      \"$type\": \"color\",\n      \"$value\": \"#32343f\"\n    },\n    \"900\": {\n      \"$type\": \"color\",\n      \"$value\": \"#272930\"\n    },\n    \"1000\": {\n      \"$type\": \"color\",\n      \"$value\": \"#21232a\"\n    },\n    \"black\": {\n      \"$type\": \"color\",\n      \"$value\": \"#000000\"\n    },\n    \"alpha-black-60\": {\n      \"$type\": \"color\",\n      \"$value\": \"#00000099\"\n    },\n    \"white\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffffff\"\n    },\n    \"alpha-white-0\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffffff00\"\n    },\n    \"alpha-grey-100-05\": {\n      \"$type\": \"color\",\n      \"$value\": \"#f9fafd0d\"\n    },\n    \"alpha-grey-100-10\": {\n      \"$type\": \"color\",\n      \"$value\": \"#f9fafd1a\"\n    },\n    \"alpha-grey-1000-05\": {\n      \"$type\": \"color\",\n      \"$value\": \"#1f23290d\"\n    },\n    \"alpha-grey-1000-10\": {\n      \"$type\": \"color\",\n      \"$value\": \"#1f23291a\"\n    }\n  },\n  \"Blue\": {\n    \"100\": {\n      \"$type\": \"color\",\n      \"$value\": \"#e3f6ff\"\n    },\n    \"200\": {\n      \"$type\": \"color\",\n      \"$value\": \"#a9e2ff\"\n    },\n    \"300\": {\n      \"$type\": \"color\",\n      \"$value\": \"#80d2ff\"\n    },\n    \"400\": {\n      \"$type\": \"color\",\n      \"$value\": \"#4ec1ff\"\n    },\n    \"500\": {\n      \"$type\": \"color\",\n      \"$value\": \"#00b5ff\"\n    },\n    \"600\": {\n      \"$type\": \"color\",\n      \"$value\": \"#0092d6\"\n    },\n    \"700\": {\n      \"$type\": \"color\",\n      \"$value\": \"#0078c0\"\n    },\n    \"800\": {\n      \"$type\": \"color\",\n      \"$value\": \"#0065a9\"\n    },\n    \"900\": {\n      \"$type\": \"color\",\n      \"$value\": \"#00508f\"\n    },\n    \"1000\": {\n      \"$type\": \"color\",\n      \"$value\": \"#003c77\"\n    },\n    \"alpha-blue-500-15\": {\n      \"$type\": \"color\",\n      \"$value\": \"#00b5ff26\"\n    },\n    \"alpha-blue-500-20\": {\n      \"$type\": \"color\",\n      \"$value\": \"#00b5ff33\",\n      \"$description\": \"Text Selected Effect\"\n    }\n  },\n  \"Green\": {\n    \"100\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ecf9f5\"\n    },\n    \"200\": {\n      \"$type\": \"color\",\n      \"$value\": \"#c3e5d8\"\n    },\n    \"300\": {\n      \"$type\": \"color\",\n      \"$value\": \"#9ad1bc\"\n    },\n    \"400\": {\n      \"$type\": \"color\",\n      \"$value\": \"#71bd9f\"\n    },\n    \"500\": {\n      \"$type\": \"color\",\n      \"$value\": \"#48a982\"\n    },\n    \"600\": {\n      \"$type\": \"color\",\n      \"$value\": \"#248569\"\n    },\n    \"700\": {\n      \"$type\": \"color\",\n      \"$value\": \"#29725d\"\n    },\n    \"800\": {\n      \"$type\": \"color\",\n      \"$value\": \"#2e6050\"\n    },\n    \"900\": {\n      \"$type\": \"color\",\n      \"$value\": \"#305548\"\n    },\n    \"1000\": {\n      \"$type\": \"color\",\n      \"$value\": \"#305244\"\n    }\n  },\n  \"Purple\": {\n    \"100\": {\n      \"$type\": \"color\",\n      \"$value\": \"#f1e0ff\"\n    },\n    \"200\": {\n      \"$type\": \"color\",\n      \"$value\": \"#e1b3ff\"\n    },\n    \"300\": {\n      \"$type\": \"color\",\n      \"$value\": \"#d185ff\"\n    },\n    \"400\": {\n      \"$type\": \"color\",\n      \"$value\": \"#bc58ff\"\n    },\n    \"500\": {\n      \"$type\": \"color\",\n      \"$value\": \"#9327ff\"\n    },\n    \"600\": {\n      \"$type\": \"color\",\n      \"$value\": \"#7a1dcc\"\n    },\n    \"700\": {\n      \"$type\": \"color\",\n      \"$value\": \"#6617b3\"\n    },\n    \"800\": {\n      \"$type\": \"color\",\n      \"$value\": \"#55138f\"\n    },\n    \"900\": {\n      \"$type\": \"color\",\n      \"$value\": \"#470c72\"\n    },\n    \"1000\": {\n      \"$type\": \"color\",\n      \"$value\": \"#380758\"\n    }\n  },\n  \"Magenta\": {\n    \"100\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffe5ef\"\n    },\n    \"200\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffb8d1\"\n    },\n    \"300\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ff8ab2\"\n    },\n    \"400\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ff5c93\"\n    },\n    \"500\": {\n      \"$type\": \"color\",\n      \"$value\": \"#fb006d\"\n    },\n    \"600\": {\n      \"$type\": \"color\",\n      \"$value\": \"#d2005f\"\n    },\n    \"700\": {\n      \"$type\": \"color\",\n      \"$value\": \"#d2005f\"\n    },\n    \"800\": {\n      \"$type\": \"color\",\n      \"$value\": \"#850040\"\n    },\n    \"900\": {\n      \"$type\": \"color\",\n      \"$value\": \"#610031\"\n    },\n    \"1000\": {\n      \"$type\": \"color\",\n      \"$value\": \"#400022\"\n    }\n  },\n  \"Red\": {\n    \"100\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffd2dd\"\n    },\n    \"200\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffa5b4\"\n    },\n    \"300\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ff7d87\"\n    },\n    \"400\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ff5050\"\n    },\n    \"500\": {\n      \"$type\": \"color\",\n      \"$value\": \"#f33641\"\n    },\n    \"600\": {\n      \"$type\": \"color\",\n      \"$value\": \"#e71d32\"\n    },\n    \"700\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ad1625\"\n    },\n    \"800\": {\n      \"$type\": \"color\",\n      \"$value\": \"#8c101c\"\n    },\n    \"900\": {\n      \"$type\": \"color\",\n      \"$value\": \"#6e0a1e\"\n    },\n    \"1000\": {\n      \"$type\": \"color\",\n      \"$value\": \"#4c0a17\"\n    },\n    \"alpha-red-500-10\": {\n      \"$type\": \"color\",\n      \"$value\": \"#f336411a\"\n    }\n  },\n  \"Orange\": {\n    \"100\": {\n      \"$type\": \"color\",\n      \"$value\": \"#fff3d5\"\n    },\n    \"200\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffe4ab\"\n    },\n    \"300\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffd181\"\n    },\n    \"400\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffbe62\"\n    },\n    \"500\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffa02e\"\n    },\n    \"600\": {\n      \"$type\": \"color\",\n      \"$value\": \"#db7e21\"\n    },\n    \"700\": {\n      \"$type\": \"color\",\n      \"$value\": \"#b75f17\"\n    },\n    \"800\": {\n      \"$type\": \"color\",\n      \"$value\": \"#93450e\"\n    },\n    \"900\": {\n      \"$type\": \"color\",\n      \"$value\": \"#7a3108\"\n    },\n    \"1000\": {\n      \"$type\": \"color\",\n      \"$value\": \"#602706\"\n    }\n  },\n  \"Yellow\": {\n    \"100\": {\n      \"$type\": \"color\",\n      \"$value\": \"#fff9b2\"\n    },\n    \"200\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffec66\"\n    },\n    \"300\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffdf1a\"\n    },\n    \"400\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffcc00\"\n    },\n    \"500\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffce00\"\n    },\n    \"600\": {\n      \"$type\": \"color\",\n      \"$value\": \"#e6b800\"\n    },\n    \"700\": {\n      \"$type\": \"color\",\n      \"$value\": \"#cc9f00\"\n    },\n    \"800\": {\n      \"$type\": \"color\",\n      \"$value\": \"#b38a00\"\n    },\n    \"900\": {\n      \"$type\": \"color\",\n      \"$value\": \"#9a7500\"\n    },\n    \"1000\": {\n      \"$type\": \"color\",\n      \"$value\": \"#7f6200\"\n    }\n  },\n  \"Subtle_Color\": {\n    \"Rose\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#fcf2f2\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#fae3e3\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#fad9d9\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#edadad\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#cc4e4e\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#702828\"\n      }\n    },\n    \"Papaya\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#fcf4f0\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#fae8de\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#fadfd2\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f0bda3\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#d67240\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#6b3215\"\n      }\n    },\n    \"Tangerine\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#fff7ed\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#fcedd9\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#fae5ca\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f2cb99\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#db8f2c\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#613b0a\"\n      }\n    },\n    \"Mango\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#fff9ec\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#fcf1d7\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#fae9c3\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f5d68e\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#e0a416\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#5c4102\"\n      }\n    },\n    \"Lemon\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#fffbe8\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#fcf5cf\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#faefb9\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f5e282\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#e0bb00\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#574800\"\n      }\n    },\n    \"Olive\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f9fae6\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f6f7d0\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f0f2b3\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#dbde83\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#adb204\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#4a4c03\"\n      }\n    },\n    \"Lime\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f6f9e6\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#eef5ce\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#e7f0bb\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#cfdb91\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#92a822\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#414d05\"\n      }\n    },\n    \"Grass\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f4faeb\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#e9f5d7\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#def0c5\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#bfd998\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#75a828\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#334d0c\"\n      }\n    },\n    \"Forest\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f1faf0\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#e2f5df\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#d7f0d3\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#a8d6a1\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#49a33b\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#1e4f16\"\n      }\n    },\n    \"Jade\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f0faf6\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#dff5eb\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#cef0e1\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#90d1b5\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#1c9963\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#075231\"\n      }\n    },\n    \"Aqua\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f0f9fa\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#dff3f5\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#ccecf0\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#83ccd4\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#008e9e\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#004e57\"\n      }\n    },\n    \"Azure\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f0f6fa\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#e1eef7\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#d3e6f5\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#88c0eb\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#0877cc\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#154469\"\n      }\n    },\n    \"Denim\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f0f3fa\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#e3ebfa\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#d7e2f7\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#9ab6ed\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#3267d1\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#223c70\"\n      }\n    },\n    \"Mauve\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f2f2fc\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#e6e6fa\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#dcdcf7\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#aeaef5\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#5555e0\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#36366b\"\n      }\n    },\n    \"Lavender\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f6f3fc\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#ebe3fa\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#e4daf7\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#c1aaf0\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#8153db\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#462f75\"\n      }\n    },\n    \"Lilac\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f7f0fa\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f0e1f7\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#edd7f7\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#d3a9e8\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#9e4cc7\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#562d6b\"\n      }\n    },\n    \"Mallow\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#faf0fa\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f5e1f4\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f5d7f4\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#dea4dc\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#b240af\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#632861\"\n      }\n    },\n    \"Camellia\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f9eff3\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f7e1eb\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f7d7e5\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#e5a3c0\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#c24279\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#6e2343\"\n      }\n    },\n    \"Smoke\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f5f5f5\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#e8e8e8\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#dedede\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#b8b8b8\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#6e6e6e\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#404040\"\n      }\n    },\n    \"Iron\": {\n      \"100\": {\n        \"$type\": \"color\",\n        \"$value\": \"#f2f4f7\"\n      },\n      \"200\": {\n        \"$type\": \"color\",\n        \"$value\": \"#e6e9f0\"\n      },\n      \"300\": {\n        \"$type\": \"color\",\n        \"$value\": \"#dadee5\"\n      },\n      \"400\": {\n        \"$type\": \"color\",\n        \"$value\": \"#b0b5bf\"\n      },\n      \"500\": {\n        \"$type\": \"color\",\n        \"$value\": \"#666f80\"\n      },\n      \"600\": {\n        \"$type\": \"color\",\n        \"$value\": \"#394152\"\n      }\n    }\n  },\n  \"Spacing\": {\n    \"0\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"0px\"\n    },\n    \"100\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"4px\"\n    },\n    \"200\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"6px\"\n    },\n    \"300\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"8px\"\n    },\n    \"400\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"12px\"\n    },\n    \"500\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"16px\"\n    },\n    \"600\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"20px\"\n    },\n    \"1000\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"1000px\"\n    }\n  },\n  \"Border-Radius\": {\n    \"0\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"0px\"\n    },\n    \"100\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"4px\"\n    },\n    \"200\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"6px\"\n    },\n    \"300\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"8px\"\n    },\n    \"400\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"12px\"\n    },\n    \"500\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"16px\"\n    },\n    \"600\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"20px\"\n    },\n    \"1000\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"1000px\"\n    }\n  }\n}"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Dark Mode.tokens.json",
    "content": "{\n  \"Text\": {\n    \"primary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.200}\"\n    },\n    \"secondary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.500}\"\n    },\n    \"tertiary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.600}\"\n    },\n    \"quaternary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.1000}\"\n    },\n    \"on-fill\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.white}\"\n    },\n    \"action\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.500}\"\n    },\n    \"action-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.400}\"\n    },\n    \"info\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.500}\"\n    },\n    \"info-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.400}\"\n    },\n    \"success\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Green.600}\"\n    },\n    \"success-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Green.500}\"\n    },\n    \"warning\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Orange.600}\"\n    },\n    \"warning-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Orange.500}\"\n    },\n    \"error\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.500}\"\n    },\n    \"error-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.400}\"\n    },\n    \"featured\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.500}\"\n    },\n    \"featured-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.400}\"\n    }\n  },\n  \"Icon\": {\n    \"primary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.200}\"\n    },\n    \"secondary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.400}\"\n    },\n    \"tertiary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.600}\"\n    },\n    \"quaternary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.1000}\"\n    },\n    \"info-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.500}\"\n    },\n    \"info-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.400}\"\n    },\n    \"success-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Green.600}\"\n    },\n    \"success-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Green.500}\"\n    },\n    \"warning-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Orange.600}\"\n    },\n    \"warning-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Orange.500}\"\n    },\n    \"error-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.500}\"\n    },\n    \"error-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.400}\"\n    },\n    \"featured-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.500}\"\n    },\n    \"featured-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.400}\"\n    },\n    \"on-fill\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.white}\"\n    }\n  },\n  \"Border\": {\n    \"primary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.800}\"\n    },\n    \"primary-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.700}\"\n    },\n    \"secondary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.300}\"\n    },\n    \"secondary-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.200}\"\n    },\n    \"tertiary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.100}\"\n    },\n    \"tertiary-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.white}\"\n    },\n    \"theme-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.500}\"\n    },\n    \"theme-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.600}\"\n    },\n    \"info-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.500}\"\n    },\n    \"info-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.400}\"\n    },\n    \"success-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Green.600}\"\n    },\n    \"success-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Green.500}\"\n    },\n    \"warning-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Orange.600}\"\n    },\n    \"warning-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Orange.500}\"\n    },\n    \"error-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.500}\"\n    },\n    \"error-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.400}\"\n    },\n    \"featured-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.500}\"\n    },\n    \"featured-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.400}\"\n    }\n  },\n  \"Fill\": {\n    \"primary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.900}\",\n      \"$description\": \"No longer available\"\n    },\n    \"primary-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.800}\",\n      \"$description\": \"No longer available\"\n    },\n    \"secondary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.600}\",\n      \"$description\": \"No longer available\"\n    },\n    \"secondary-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.500}\",\n      \"$description\": \"No longer available\"\n    },\n    \"tertiary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.300}\",\n      \"$description\": \"No longer available\"\n    },\n    \"tertiary-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.200}\",\n      \"$description\": \"No longer available\"\n    },\n    \"quaternary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.100}\",\n      \"$description\": \"No longer available\"\n    },\n    \"quaternary-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.white}\",\n      \"$description\": \"No longer available\"\n    },\n    \"content\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.alpha-white-0}\"\n    },\n    \"content-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.alpha-grey-100-05}\",\n      \"$description\": \"Used for hover state, eg. button, navigation item, menu item and grid item.\"\n    },\n    \"content-visible\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.alpha-grey-100-05}\",\n      \"$description\": \"Used for hover state, eg. button, navigation item, menu item and grid item.\"\n    },\n    \"content-visible-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.alpha-grey-100-10}\"\n    },\n    \"theme-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.500}\"\n    },\n    \"theme-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.600}\"\n    },\n    \"theme-select\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.alpha-blue-500-15}\"\n    },\n    \"text-select\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.alpha-blue-500-20}\"\n    },\n    \"info-light\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.200}\"\n    },\n    \"info-light-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.100}\"\n    },\n    \"info-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.500}\"\n    },\n    \"info-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.400}\"\n    },\n    \"success-light\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Green.200}\"\n    },\n    \"success-light-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Green.100}\"\n    },\n    \"warning-light\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Orange.200}\"\n    },\n    \"warning-light-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Orange.100}\"\n    },\n    \"error-light\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.200}\"\n    },\n    \"error-light-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.100}\"\n    },\n    \"error-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.500}\"\n    },\n    \"error-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.400}\"\n    },\n    \"error-select\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.alpha-red-500-10}\"\n    },\n    \"featured-light\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.200}\"\n    },\n    \"featured-light-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.100}\"\n    },\n    \"featured-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.500}\"\n    },\n    \"featured-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.400}\"\n    }\n  },\n  \"Surface\": {\n    \"primary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.900}\"\n    },\n    \"primary-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.800}\"\n    },\n    \"layer-01\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.900}\",\n      \"$description\": \"Settings Window\"\n    },\n    \"layer-01-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.800}\"\n    },\n    \"layer-02\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.850}\",\n      \"$description\": \"Settings Side Panel, Modal Window\"\n    },\n    \"layer-02-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.800}\"\n    },\n    \"layer-03\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.850}\",\n      \"$description\": \"Dialog\"\n    },\n    \"layer-03-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.800}\"\n    },\n    \"layer-04\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.830}\",\n      \"$description\": \"Dropdown Menu\"\n    },\n    \"layer-04-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.800}\"\n    },\n    \"inverse\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.800}\"\n    },\n    \"secondary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.800}\"\n    },\n    \"overlay\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.alpha-black-60}\"\n    }\n  },\n  \"Surface_Container\": {\n    \"layer-01\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.900}\"\n    },\n    \"layer-02\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.800}\"\n    },\n    \"layer-03\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.700}\"\n    }\n  },\n  \"Background\": {\n    \"primary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.1000}\"\n    }\n  },\n  \"Badge\": {\n    \"color-1\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Rose.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Rose.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Rose.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Rose.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Rose.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Rose.600}\"\n      }\n    },\n    \"color-2\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Papaya.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Papaya.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Papaya.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Papaya.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Papaya.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Papaya.600}\"\n      }\n    },\n    \"color-3\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Tangerine.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Tangerine.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Tangerine.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Tangerine.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Tangerine.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Tangerine.600}\"\n      }\n    },\n    \"color-4\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mango.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mango.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mango.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mango.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mango.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mango.600}\"\n      }\n    },\n    \"color-5\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lemon.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lemon.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lemon.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lemon.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lemon.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lemon.600}\"\n      }\n    },\n    \"color-6\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Olive.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Olive.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Olive.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Olive.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Olive.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Olive.600}\"\n      }\n    },\n    \"color-7\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lime.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lime.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lime.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lime.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lime.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lime.600}\"\n      }\n    },\n    \"color-8\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Grass.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Grass.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Grass.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Grass.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Grass.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Grass.600}\"\n      }\n    },\n    \"color-9\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Forest.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Forest.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Forest.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Forest.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Forest.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Forest.600}\"\n      }\n    },\n    \"color-10\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Jade.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Jade.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Jade.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Jade.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Jade.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Jade.600}\"\n      }\n    },\n    \"color-11\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Aqua.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Aqua.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Aqua.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Aqua.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Aqua.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Aqua.600}\"\n      }\n    },\n    \"color-12\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Azure.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Azure.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Azure.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Azure.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Azure.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Azure.600}\"\n      }\n    },\n    \"color-13\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Denim.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Denim.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Denim.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Denim.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Denim.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Denim.600}\"\n      }\n    },\n    \"color-14\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mauve.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mauve.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mauve.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mauve.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mauve.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mauve.600}\"\n      }\n    },\n    \"color-15\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lavender.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lavender.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lavender.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lavender.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lavender.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lavender.600}\"\n      }\n    },\n    \"color-16\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lilac.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lilac.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lilac.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lilac.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lilac.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lilac.600}\"\n      }\n    },\n    \"color-17\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mallow.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mallow.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mallow.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mallow.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mallow.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mallow.600}\"\n      }\n    },\n    \"color-18\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Camellia.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Camellia.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Camellia.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Camellia.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Camellia.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Camellia.600}\"\n      }\n    },\n    \"color-19\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Smoke.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Smoke.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Smoke.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Smoke.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Smoke.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Smoke.600}\"\n      }\n    },\n    \"color-20\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Iron.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Iron.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Iron.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Iron.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Iron.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Iron.600}\"\n      }\n    }\n  },\n  \"Brand\": {\n    \"Skyline\": {\n      \"$type\": \"color\",\n      \"$value\": \"#00b5ff\"\n    },\n    \"Aqua\": {\n      \"$type\": \"color\",\n      \"$value\": \"#00c8ff\"\n    },\n    \"Violet\": {\n      \"$type\": \"color\",\n      \"$value\": \"#9327ff\"\n    },\n    \"Amethyst\": {\n      \"$type\": \"color\",\n      \"$value\": \"#8427e0\"\n    },\n    \"Berry\": {\n      \"$type\": \"color\",\n      \"$value\": \"#e3006d\"\n    },\n    \"Coral\": {\n      \"$type\": \"color\",\n      \"$value\": \"#fb006d\"\n    },\n    \"Golden\": {\n      \"$type\": \"color\",\n      \"$value\": \"#f7931e\"\n    },\n    \"Amber\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffbd00\"\n    },\n    \"Lemon\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffce00\"\n    }\n  },\n  \"Other_Colors\": {\n    \"text-highlight\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.200}\"\n    }\n  },\n  \"Spacing\": {\n    \"spacing-0\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Spacing.0}\"\n    },\n    \"spacing-xs\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Spacing.100}\"\n    },\n    \"spacing-s\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Spacing.200}\"\n    },\n    \"spacing-m\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Spacing.300}\"\n    },\n    \"spacing-l\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Spacing.400}\"\n    },\n    \"spacing-xl\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Spacing.500}\"\n    },\n    \"spacing-xxl\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Spacing.600}\"\n    },\n    \"spacing-full\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Spacing.1000}\"\n    }\n  },\n  \"Border_Radius\": {\n    \"border-radius-0\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Border-Radius.0}\"\n    },\n    \"border-radius-xs\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Border-Radius.100}\"\n    },\n    \"border-radius-s\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Border-Radius.200}\"\n    },\n    \"border-radius-m\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Border-Radius.300}\"\n    },\n    \"border-radius-l\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Border-Radius.400}\"\n    },\n    \"border-radius-xl\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Border-Radius.500}\"\n    },\n    \"border-radius-xxl\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Border-Radius.600}\"\n    },\n    \"border-radius-full\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Border-Radius.1000}\"\n    }\n  }\n}"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Light Mode.tokens.json",
    "content": "{\n  \"Text\": {\n    \"primary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.1000}\"\n    },\n    \"secondary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.600}\"\n    },\n    \"tertiary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.500}\"\n    },\n    \"quaternary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.200}\"\n    },\n    \"on-fill\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.white}\"\n    },\n    \"action\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.600}\"\n    },\n    \"action-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.700}\"\n    },\n    \"info\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.600}\"\n    },\n    \"info-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.700}\"\n    },\n    \"success\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Green.600}\"\n    },\n    \"success-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Green.700}\"\n    },\n    \"warning\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Orange.600}\"\n    },\n    \"warning-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Orange.700}\"\n    },\n    \"error\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.600}\"\n    },\n    \"error-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.700}\"\n    },\n    \"featured\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.500}\"\n    },\n    \"featured-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.600}\"\n    }\n  },\n  \"Icon\": {\n    \"primary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.1000}\"\n    },\n    \"secondary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.600}\"\n    },\n    \"tertiary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.400}\"\n    },\n    \"quaternary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.200}\"\n    },\n    \"info-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.600}\"\n    },\n    \"info-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.700}\"\n    },\n    \"success-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Green.600}\"\n    },\n    \"success-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Green.700}\"\n    },\n    \"warning-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Orange.600}\"\n    },\n    \"warning-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Orange.700}\"\n    },\n    \"error-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.600}\"\n    },\n    \"error-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.700}\"\n    },\n    \"featured-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.500}\"\n    },\n    \"featured-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.600}\"\n    },\n    \"on-fill\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.white}\"\n    }\n  },\n  \"Border\": {\n    \"primary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.200}\"\n    },\n    \"primary-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.300}\"\n    },\n    \"secondary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.800}\"\n    },\n    \"secondary-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.700}\"\n    },\n    \"tertiary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.1000}\"\n    },\n    \"tertiary-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.900}\"\n    },\n    \"theme-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.500}\"\n    },\n    \"theme-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.600}\"\n    },\n    \"info-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.500}\"\n    },\n    \"info-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.600}\"\n    },\n    \"success-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Green.600}\"\n    },\n    \"success-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Green.700}\"\n    },\n    \"warning-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Orange.600}\"\n    },\n    \"warning-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Orange.700}\"\n    },\n    \"error-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.600}\"\n    },\n    \"error-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.700}\"\n    },\n    \"featured-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.500}\"\n    },\n    \"featured-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.600}\"\n    }\n  },\n  \"Fill\": {\n    \"primary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.100}\",\n      \"$description\": \"No longer available\"\n    },\n    \"primary-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.200}\",\n      \"$description\": \"No longer available\"\n    },\n    \"secondary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.300}\",\n      \"$description\": \"No longer available\"\n    },\n    \"secondary-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.400}\",\n      \"$description\": \"No longer available\"\n    },\n    \"tertiary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.600}\",\n      \"$description\": \"No longer available\"\n    },\n    \"tertiary-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.500}\",\n      \"$description\": \"No longer available\"\n    },\n    \"quaternary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.1000}\",\n      \"$description\": \"No longer available\"\n    },\n    \"quaternary-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.900}\",\n      \"$description\": \"No longer available\"\n    },\n    \"content\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.alpha-white-0}\"\n    },\n    \"content-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.alpha-grey-1000-05}\",\n      \"$description\": \"Used for hover state, eg. button, navigation item, menu item and grid item.\"\n    },\n    \"content-visible\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.alpha-grey-1000-05}\",\n      \"$description\": \"Used for hover state, eg. button, navigation item, menu item and grid item.\"\n    },\n    \"content-visible-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.alpha-grey-1000-10}\"\n    },\n    \"theme-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.500}\"\n    },\n    \"theme-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.600}\"\n    },\n    \"theme-select\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.alpha-blue-500-15}\"\n    },\n    \"text-select\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.alpha-blue-500-20}\"\n    },\n    \"info-light\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.100}\"\n    },\n    \"info-light-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.200}\"\n    },\n    \"info-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.500}\"\n    },\n    \"info-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.600}\"\n    },\n    \"success-light\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Green.100}\"\n    },\n    \"success-light-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Green.200}\"\n    },\n    \"warning-light\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Orange.100}\"\n    },\n    \"warning-light-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Orange.200}\"\n    },\n    \"error-light\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.100}\"\n    },\n    \"error-light-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.200}\"\n    },\n    \"error-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.600}\"\n    },\n    \"error-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.700}\"\n    },\n    \"error-select\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Red.alpha-red-500-10}\"\n    },\n    \"featured-light\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.100}\"\n    },\n    \"featured-light-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.200}\"\n    },\n    \"featured-thick\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.500}\"\n    },\n    \"featured-thick-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Purple.600}\"\n    }\n  },\n  \"Surface\": {\n    \"primary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.white}\"\n    },\n    \"primary-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.100}\"\n    },\n    \"layer-01\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.white}\",\n      \"$description\": \"Settings Window\"\n    },\n    \"layer-01-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.100}\"\n    },\n    \"layer-02\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.white}\",\n      \"$description\": \"Settings Side Panel, Modal Window\"\n    },\n    \"layer-02-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.100}\"\n    },\n    \"layer-03\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.white}\",\n      \"$description\": \"Dialog\"\n    },\n    \"layer-03-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.100}\"\n    },\n    \"layer-04\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.white}\",\n      \"$description\": \"Dropdown Menu\"\n    },\n    \"layer-04-hover\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.100}\"\n    },\n    \"inverse\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.1000}\"\n    },\n    \"secondary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.1000}\"\n    },\n    \"overlay\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.alpha-black-60}\"\n    }\n  },\n  \"Surface_Container\": {\n    \"layer-01\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.100}\"\n    },\n    \"layer-02\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.200}\"\n    },\n    \"layer-03\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.300}\"\n    }\n  },\n  \"Background\": {\n    \"primary\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Neutral.white}\"\n    }\n  },\n  \"Badge\": {\n    \"color-1\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Rose.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Rose.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Rose.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Rose.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Rose.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Rose.600}\"\n      }\n    },\n    \"color-2\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Papaya.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Papaya.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Papaya.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Papaya.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Papaya.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Papaya.600}\"\n      }\n    },\n    \"color-3\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Tangerine.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Tangerine.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Tangerine.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Tangerine.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Tangerine.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Tangerine.600}\"\n      }\n    },\n    \"color-4\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mango.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mango.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mango.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mango.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mango.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mango.600}\"\n      }\n    },\n    \"color-5\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lemon.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lemon.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lemon.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lemon.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lemon.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lemon.600}\"\n      }\n    },\n    \"color-6\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Olive.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Olive.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Olive.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Olive.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Olive.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Olive.600}\"\n      }\n    },\n    \"color-7\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lime.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lime.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lime.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lime.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lime.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lime.600}\"\n      }\n    },\n    \"color-8\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Grass.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Grass.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Grass.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Grass.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Grass.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Grass.600}\"\n      }\n    },\n    \"color-9\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Forest.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Forest.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Forest.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Forest.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Forest.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Forest.600}\"\n      }\n    },\n    \"color-10\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Jade.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Jade.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Jade.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Jade.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Jade.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Jade.600}\"\n      }\n    },\n    \"color-11\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Aqua.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Aqua.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Aqua.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Aqua.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Aqua.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Aqua.600}\"\n      }\n    },\n    \"color-12\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Azure.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Azure.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Azure.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Azure.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Azure.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Azure.600}\"\n      }\n    },\n    \"color-13\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Denim.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Denim.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Denim.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Denim.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Denim.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Denim.600}\"\n      }\n    },\n    \"color-14\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mauve.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mauve.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mauve.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mauve.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mauve.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mauve.600}\"\n      }\n    },\n    \"color-15\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lavender.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lavender.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lavender.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lavender.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lavender.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lavender.600}\"\n      }\n    },\n    \"color-16\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lilac.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lilac.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lilac.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lilac.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lilac.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Lilac.600}\"\n      }\n    },\n    \"color-17\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mallow.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mallow.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mallow.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mallow.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mallow.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Mallow.600}\"\n      }\n    },\n    \"color-18\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Camellia.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Camellia.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Camellia.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Camellia.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Camellia.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Camellia.600}\"\n      }\n    },\n    \"color-19\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Smoke.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Smoke.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Smoke.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Smoke.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Smoke.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Smoke.600}\"\n      }\n    },\n    \"color-20\": {\n      \"light-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Iron.100}\"\n      },\n      \"light-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Iron.200}\"\n      },\n      \"light-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Iron.300}\"\n      },\n      \"thick-1\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Iron.400}\"\n      },\n      \"thick-2\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Iron.500}\"\n      },\n      \"thick-3\": {\n        \"$type\": \"color\",\n        \"$value\": \"{Subtle_Color.Iron.600}\"\n      }\n    }\n  },\n  \"Brand\": {\n    \"Skyline\": {\n      \"$type\": \"color\",\n      \"$value\": \"#00b5ff\"\n    },\n    \"Aqua\": {\n      \"$type\": \"color\",\n      \"$value\": \"#00c8ff\"\n    },\n    \"Violet\": {\n      \"$type\": \"color\",\n      \"$value\": \"#9327ff\"\n    },\n    \"Amethyst\": {\n      \"$type\": \"color\",\n      \"$value\": \"#8427e0\"\n    },\n    \"Berry\": {\n      \"$type\": \"color\",\n      \"$value\": \"#e3006d\"\n    },\n    \"Coral\": {\n      \"$type\": \"color\",\n      \"$value\": \"#fb006d\"\n    },\n    \"Golden\": {\n      \"$type\": \"color\",\n      \"$value\": \"#f7931e\"\n    },\n    \"Amber\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffbd00\"\n    },\n    \"Lemon\": {\n      \"$type\": \"color\",\n      \"$value\": \"#ffce00\"\n    }\n  },\n  \"Other_Colors\": {\n    \"text-highlight\": {\n      \"$type\": \"color\",\n      \"$value\": \"{Blue.200}\"\n    }\n  },\n  \"Spacing\": {\n    \"spacing-0\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Spacing.0}\"\n    },\n    \"spacing-xs\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Spacing.100}\"\n    },\n    \"spacing-s\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Spacing.200}\"\n    },\n    \"spacing-m\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Spacing.300}\"\n    },\n    \"spacing-l\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Spacing.400}\"\n    },\n    \"spacing-xl\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Spacing.500}\"\n    },\n    \"spacing-xxl\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Spacing.600}\"\n    },\n    \"spacing-full\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Spacing.1000}\"\n    }\n  },\n  \"Border_Radius\": {\n    \"border-radius-0\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Border-Radius.0}\"\n    },\n    \"border-radius-xs\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Border-Radius.100}\"\n    },\n    \"border-radius-s\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Border-Radius.200}\"\n    },\n    \"border-radius-m\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Border-Radius.300}\"\n    },\n    \"border-radius-l\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Border-Radius.400}\"\n    },\n    \"border-radius-xl\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Border-Radius.500}\"\n    },\n    \"border-radius-xxl\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Border-Radius.600}\"\n    },\n    \"border-radius-full\": {\n      \"$type\": \"dimension\",\n      \"$value\": \"{Border-Radius.1000}\"\n    }\n  }\n}"
  },
  {
    "path": "frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart",
    "content": "// ignore_for_file: avoid_print, depend_on_referenced_packages\n\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:collection/collection.dart';\n\nvoid main() {\n  generatePrimitive();\n  generateSemantic();\n}\n\nvoid generatePrimitive() {\n  // 1. Load the JSON file.\n  final jsonString =\n      File('script/Primitive.Mode 1.tokens.json').readAsStringSync();\n  final jsonData = jsonDecode(jsonString) as Map<String, dynamic>;\n\n  // 2. Prepare the output code.\n  final buffer = StringBuffer();\n\n  buffer.writeln('''\n// ignore_for_file: constant_identifier_names, non_constant_identifier_names\n//\n// AUTO-GENERATED - DO NOT EDIT DIRECTLY\n//\n// This file is auto-generated by the generate_theme.dart script\n// Generation time: ${DateTime.now().toIso8601String()}\n//\n// To modify these colors, edit the source JSON files and run the script:\n//\n// dart run script/generate_theme.dart\n//\nimport 'package:flutter/material.dart';\n\nclass AppFlowyPrimitiveTokens {\n  AppFlowyPrimitiveTokens._();''');\n\n  // 3. Process each color category.\n  jsonData.forEach((categoryName, categoryData) {\n    categoryData.forEach((tokenName, tokenData) {\n      processPrimitiveTokenData(\n        buffer,\n        tokenData,\n        '${categoryName}_$tokenName',\n      );\n    });\n  });\n\n  buffer.writeln('}');\n\n  // 4. Write the output to a Dart file.\n  final outputFile = File('lib/src/theme/data/appflowy_default/primitive.dart');\n  outputFile.writeAsStringSync(buffer.toString());\n\n  print('Successfully generated ${outputFile.path}');\n}\n\nvoid processPrimitiveTokenData(\n  StringBuffer buffer,\n  Map<String, dynamic> tokenData,\n  final String currentTokenName,\n) {\n  if (tokenData\n      case {\n        r'$type': 'color',\n        r'$value': final String colorValue,\n      }) {\n    final dartColorValue = convertColor(colorValue);\n    final dartTokenName = currentTokenName.replaceAll('-', '_').toCamelCase();\n\n    buffer.writeln('''\n\n  /// $colorValue\n  static Color get $dartTokenName => Color(0x$dartColorValue);''');\n  } else {\n    tokenData.forEach((key, value) {\n      if (value is Map<String, dynamic>) {\n        processPrimitiveTokenData(buffer, value, '${currentTokenName}_$key');\n      }\n    });\n  }\n}\n\nvoid generateSemantic() {\n  // 1. Load the JSON file.\n  final lightJsonString =\n      File('script/Semantic.Light Mode.tokens.json').readAsStringSync();\n  final darkJsonString =\n      File('script/Semantic.Dark Mode.tokens.json').readAsStringSync();\n  final lightJsonData = jsonDecode(lightJsonString) as Map<String, dynamic>;\n  final darkJsonData = jsonDecode(darkJsonString) as Map<String, dynamic>;\n\n  // 2. Prepare the output code.\n  final buffer = StringBuffer();\n\n  buffer.writeln('''\n// ignore_for_file: constant_identifier_names, non_constant_identifier_names\n//\n// AUTO-GENERATED - DO NOT EDIT DIRECTLY\n//\n// This file is auto-generated by the generate_theme.dart script\n// Generation time: ${DateTime.now().toIso8601String()}\n//\n// To modify these colors, edit the source JSON files and run the script:\n//\n// dart run script/generate_theme.dart\n//\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nimport '../shared.dart';\n\nclass AppFlowyDefaultTheme implements AppFlowyThemeBuilder {''');\n\n  // 3. Process light mode semantic tokens\n  buffer.writeln('''\n  @override\n  AppFlowyThemeData light({\n    String? fontFamily,\n  }) {\n    final textStyle = AppFlowyBaseTextStyle.customFontFamily(fontFamily ?? '');\n    final borderRadius = AppFlowySharedTokens.buildBorderRadius();\n    final spacing = AppFlowySharedTokens.buildSpacing();\n    final shadow = AppFlowySharedTokens.buildShadow(Brightness.light);''');\n\n  lightJsonData.forEach((categoryName, categoryData) {\n    if ([\n      'Spacing',\n      'Border_Radius',\n      'Shadow',\n      'Badge_Color',\n    ].contains(categoryName)) {\n      return;\n    }\n\n    final fullCategoryName = \"${categoryName}_color_scheme\".toCamelCase();\n    final className = 'AppFlowy${fullCategoryName.toCapitalize()}';\n\n    buffer\n      ..writeln()\n      ..writeln('    final $fullCategoryName = $className(');\n\n    categoryData.forEach((tokenName, tokenData) {\n      processSemanticTokenData(buffer, tokenData, tokenName);\n    });\n    buffer.writeln('    );');\n  });\n\n  buffer.writeln();\n  buffer.writeln('''\n    return AppFlowyThemeData(\n      textStyle: textStyle,\n      textColorScheme: textColorScheme,\n      borderColorScheme: borderColorScheme,\n      fillColorScheme: fillColorScheme,\n      surfaceColorScheme: surfaceColorScheme,\n      backgroundColorScheme: backgroundColorScheme,\n      iconColorScheme: iconColorScheme,\n      brandColorScheme: brandColorScheme,\n      otherColorsColorScheme: otherColorsColorScheme,\n      borderRadius: borderRadius,\n      surfaceContainerColorScheme: surfaceContainerColorScheme,\n      badgeColorScheme: badgeColorScheme,\n      spacing: spacing,\n      shadow: shadow,\n    );\n  }''');\n\n  buffer.writeln();\n\n  buffer.writeln('''\n  @override\n  AppFlowyThemeData dark({\n    String? fontFamily,\n  }) {\n    final textStyle = AppFlowyBaseTextStyle.customFontFamily(fontFamily ?? '');\n    final borderRadius = AppFlowySharedTokens.buildBorderRadius();\n    final spacing = AppFlowySharedTokens.buildSpacing();\n    final shadow = AppFlowySharedTokens.buildShadow(Brightness.dark);''');\n\n  darkJsonData.forEach((categoryName, categoryData) {\n    if ([\n      'Spacing',\n      'Border_Radius',\n      'Shadow',\n      'Badge_Color',\n    ].contains(categoryName)) {\n      return;\n    }\n\n    final fullCategoryName = \"${categoryName}_color_scheme\".toCamelCase();\n    final className = 'AppFlowy${fullCategoryName.toCapitalize()}';\n\n    buffer\n      ..writeln()\n      ..writeln('    final $fullCategoryName = $className(');\n\n    categoryData.forEach((tokenName, tokenData) {\n      if (tokenData is Map<String, dynamic>) {\n        processSemanticTokenData(buffer, tokenData, tokenName);\n      }\n    });\n    buffer.writeln('    );');\n  });\n\n  buffer.writeln();\n\n  buffer.writeln('''\n    return AppFlowyThemeData(\n      textStyle: textStyle,\n      textColorScheme: textColorScheme,\n      borderColorScheme: borderColorScheme,\n      fillColorScheme: fillColorScheme,\n      surfaceColorScheme: surfaceColorScheme,\n      backgroundColorScheme: backgroundColorScheme,\n      iconColorScheme: iconColorScheme,\n      brandColorScheme: brandColorScheme,\n      otherColorsColorScheme: otherColorsColorScheme,\n      borderRadius: borderRadius,\n      surfaceContainerColorScheme: surfaceContainerColorScheme,\n      badgeColorScheme: badgeColorScheme,\n      spacing: spacing,\n      shadow: shadow,\n    );\n  }''');\n\n  buffer.writeln('}');\n\n  // 4. Write the output to a Dart file.\n  final outputFile = File('lib/src/theme/data/appflowy_default/semantic.dart');\n  outputFile.writeAsStringSync(buffer.toString());\n\n  print('Successfully generated ${outputFile.path}');\n}\n\nvoid processSemanticTokenData(\n  StringBuffer buffer,\n  Map<String, dynamic> json,\n  final String currentTokenName,\n) {\n  if (json\n      case {\n        r'$type': 'color',\n        r'$value': final String value,\n      }) {\n    final semanticTokenName =\n        currentTokenName.replaceAll('-', '_').toCamelCase();\n\n    final String colorValueOrPrimitiveToken;\n    if (value.isColor) {\n      colorValueOrPrimitiveToken = 'Color(0x${convertColor(value)})';\n    } else {\n      final primitiveToken = value\n          .replaceAll(RegExp(r'\\{|\\}'), '')\n          .replaceAll(RegExp(r'\\.|-'), '_')\n          .toCamelCase();\n      colorValueOrPrimitiveToken = 'AppFlowyPrimitiveTokens.$primitiveToken';\n    }\n\n    buffer.writeln('      $semanticTokenName: $colorValueOrPrimitiveToken,');\n  } else {\n    json.forEach((key, value) {\n      if (value is Map<String, dynamic>) {\n        processSemanticTokenData(\n          buffer,\n          value,\n          '${currentTokenName}_$key',\n        );\n      }\n    });\n  }\n}\n\nString convertColor(String hexColor) {\n  String color = hexColor.toUpperCase().replaceAll('#', '');\n  if (color.length == 6) {\n    color = 'FF$color'; // Add missing alpha channel\n  } else if (color.length == 8) {\n    color = color.substring(6) + color.substring(0, 6); // Rearrange to ARGB\n  }\n  return color;\n}\n\nextension on String {\n  String toCamelCase() {\n    return split('_').mapIndexed((index, part) {\n      if (index == 0) {\n        return part.toLowerCase();\n      } else {\n        return part[0].toUpperCase() + part.substring(1).toLowerCase();\n      }\n    }).join();\n  }\n\n  String toCapitalize() {\n    if (isEmpty) {\n      return this;\n    }\n    return '${this[0].toUpperCase()}${substring(1)}';\n  }\n\n  bool get isColor =>\n      startsWith('#') ||\n      (startsWith('0x') && length == 10) ||\n      (startsWith('0xFF') && length == 12);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\nbuild/\n\n# Android related\n**/android/**/gradle-wrapper.jar\n**/android/.gradle\n**/android/captures/\n**/android/gradlew\n**/android/gradlew.bat\n**/android/local.properties\n**/android/**/GeneratedPluginRegistrant.java\n\n# iOS/XCode related\n**/ios/**/*.mode1v3\n**/ios/**/*.mode2v3\n**/ios/**/*.moved-aside\n**/ios/**/*.pbxuser\n**/ios/**/*.perspectivev3\n**/ios/**/*sync/\n**/ios/**/.sconsign.dblite\n**/ios/**/.tags*\n**/ios/**/.vagrant/\n**/ios/**/DerivedData/\n**/ios/**/Icon?\n**/ios/**/Pods/\n**/ios/**/.symlinks/\n**/ios/**/profile\n**/ios/**/xcuserdata\n**/ios/.generated/\n**/ios/Flutter/App.framework\n**/ios/Flutter/Flutter.framework\n**/ios/Flutter/Flutter.podspec\n**/ios/Flutter/Generated.xcconfig\n**/ios/Flutter/ephemeral\n**/ios/Flutter/app.flx\n**/ios/Flutter/app.zip\n**/ios/Flutter/flutter_assets/\n**/ios/Flutter/flutter_export_environment.sh\n**/ios/ServiceDefinitions.json\n**/ios/Runner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!**/ios/**/default.mode1v3\n!**/ios/**/default.mode2v3\n!**/ios/**/default.pbxuser\n!**/ios/**/default.perspectivev3\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/.metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: 96bbcd006fafade4ad7a4abde77cec32df6846ea\n  channel: dev\n\nproject_type: package\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/LICENSE",
    "content": "TODO: Add your license here.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/analysis_options.yaml",
    "content": "include: package:flutter_lints/flutter.yaml\n\n# Additional information about this file can be found at\n# https://dart.dev/guides/language/analysis-options\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart",
    "content": "import 'package:flowy_infra/theme.dart';\nimport 'package:flowy_infra/utils/color_converter.dart';\nimport 'package:flutter/material.dart';\nimport 'package:freezed_annotation/freezed_annotation.dart';\n\nimport 'dandelion.dart';\nimport 'default_colorscheme.dart';\nimport 'lavender.dart';\nimport 'lemonade.dart';\n\npart 'colorscheme.g.dart';\n\n/// A map of all the built-in themes.\n///\n/// The key is the theme name, and the value is a list of two color schemes:\n/// the first is for light mode, and the second is for dark mode.\nconst Map<String, List<FlowyColorScheme>> themeMap = {\n  BuiltInTheme.defaultTheme: [\n    DefaultColorScheme.light(),\n    DefaultColorScheme.dark(),\n  ],\n  BuiltInTheme.dandelion: [\n    DandelionColorScheme.light(),\n    DandelionColorScheme.dark(),\n  ],\n  BuiltInTheme.lemonade: [\n    LemonadeColorScheme.light(),\n    LemonadeColorScheme.dark(),\n  ],\n  BuiltInTheme.lavender: [\n    LavenderColorScheme.light(),\n    LavenderColorScheme.dark(),\n  ],\n};\n\n@JsonSerializable(converters: [ColorConverter()])\nclass FlowyColorScheme {\n  const FlowyColorScheme({\n    required this.surface,\n    required this.hover,\n    required this.selector,\n    required this.red,\n    required this.yellow,\n    required this.green,\n    required this.shader1,\n    required this.shader2,\n    required this.shader3,\n    required this.shader4,\n    required this.shader5,\n    required this.shader6,\n    required this.shader7,\n    required this.bg1,\n    required this.bg2,\n    required this.bg3,\n    required this.bg4,\n    required this.tint1,\n    required this.tint2,\n    required this.tint3,\n    required this.tint4,\n    required this.tint5,\n    required this.tint6,\n    required this.tint7,\n    required this.tint8,\n    required this.tint9,\n    required this.main1,\n    required this.main2,\n    required this.shadow,\n    required this.sidebarBg,\n    required this.divider,\n    required this.topbarBg,\n    required this.icon,\n    required this.text,\n    required this.secondaryText,\n    required this.strongText,\n    required this.input,\n    required this.hint,\n    required this.primary,\n    required this.onPrimary,\n    required this.hoverBG1,\n    required this.hoverBG2,\n    required this.hoverBG3,\n    required this.hoverFG,\n    required this.questionBubbleBG,\n    required this.progressBarBGColor,\n    required this.toolbarColor,\n    required this.toggleButtonBGColor,\n    required this.calendarWeekendBGColor,\n    required this.gridRowCountColor,\n    required this.borderColor,\n    required this.scrollbarColor,\n    required this.scrollbarHoverColor,\n    required this.lightIconColor,\n    required this.toolbarHoverColor,\n  });\n\n  final Color surface;\n  final Color hover;\n  final Color selector;\n  final Color red;\n  final Color yellow;\n  final Color green;\n  final Color shader1;\n  final Color shader2;\n  final Color shader3;\n  final Color shader4;\n  final Color shader5;\n  final Color shader6;\n  final Color shader7;\n  final Color bg1;\n  final Color bg2;\n  final Color bg3;\n  final Color bg4;\n  final Color tint1;\n  final Color tint2;\n  final Color tint3;\n  final Color tint4;\n  final Color tint5;\n  final Color tint6;\n  final Color tint7;\n  final Color tint8;\n  final Color tint9;\n  final Color main1;\n  final Color main2;\n  final Color shadow;\n  final Color sidebarBg;\n  final Color divider;\n  final Color topbarBg;\n  final Color icon;\n  final Color text;\n  final Color secondaryText;\n  final Color strongText;\n  final Color input;\n  final Color hint;\n  final Color primary;\n  final Color onPrimary;\n  //page title hover effect\n  final Color hoverBG1;\n  //action item hover effect\n  final Color hoverBG2;\n  final Color hoverBG3;\n  //the text color when it is hovered\n  final Color hoverFG;\n  final Color questionBubbleBG;\n  final Color progressBarBGColor;\n  //editor toolbar BG color\n  final Color toolbarColor;\n  final Color toggleButtonBGColor;\n  final Color calendarWeekendBGColor;\n  //grid bottom count color\n  final Color gridRowCountColor;\n\n  final Color borderColor;\n\n  final Color scrollbarColor;\n  final Color scrollbarHoverColor;\n\n  final Color lightIconColor;\n  final Color toolbarHoverColor;\n\n  factory FlowyColorScheme.fromJson(Map<String, dynamic> json) =>\n      _$FlowyColorSchemeFromJson(json);\n\n  Map<String, dynamic> toJson() => _$FlowyColorSchemeToJson(this);\n\n  /// Merges the given [json] with the default color scheme\n  /// based on the given [brightness].\n  ///\n  factory FlowyColorScheme.fromJsonSoft(\n    Map<String, dynamic> json, [\n    Brightness brightness = Brightness.light,\n  ]) {\n    final colorScheme = brightness == Brightness.light\n        ? const DefaultColorScheme.light()\n        : const DefaultColorScheme.dark();\n    final defaultMap = colorScheme.toJson();\n    final mergedMap = Map<String, dynamic>.from(defaultMap)..addAll(json);\n\n    return FlowyColorScheme.fromJson(mergedMap);\n  }\n\n  /// Useful in validating that a teheme adheres to the default color scheme.\n  /// Returns the keys that are missing from the [json].\n  ///\n  /// We use this for testing and debugging, and we might make it possible for users to\n  /// check their themes for missing keys in the future.\n  ///\n  /// Sample usage:\n  /// ```dart\n  ///  final lightJson = await jsonDecode(await light.readAsString());\n  ///  final lightMissingKeys = FlowyColorScheme.getMissingKeys(lightJson);\n  /// ```\n  ///\n  static List<String> getMissingKeys(Map<String, dynamic> json) {\n    final defaultKeys = const DefaultColorScheme.light().toJson().keys;\n    final jsonKeys = json.keys;\n\n    return defaultKeys.where((key) => !jsonKeys.contains(key)).toList();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart",
    "content": "import 'package:flowy_infra/colorscheme/default_colorscheme.dart';\nimport 'package:flutter/material.dart';\n\nimport 'colorscheme.dart';\n\nconst _black = Color(0xff000000);\nconst _white = Color(0xFFFFFFFF);\nconst _lightBg1 = Color(0xFFFFD13E);\nconst _lightShader1 = Color(0xff333333);\nconst _lightShader3 = Color(0xff828282);\nconst _lightShader5 = Color(0xffe0e0e0);\nconst _lightShader6 = Color(0xfff2f2f2);\nconst _lightDandelionYellow = Color(0xffffcb00);\nconst _lightDandelionLightYellow = Color(0xffffdf66);\nconst _lightDandelionGreen = Color(0xff9bc53d);\nconst _lightTint9 = Color(0xffe1fbff);\n\nconst _darkShader1 = Color(0xff131720);\nconst _darkShader2 = Color(0xff1A202C);\nconst _darkShader3 = Color(0xff363D49);\nconst _darkShader5 = Color(0xffBBC3CD);\nconst _darkShader6 = Color(0xffF2F2F2);\nconst _darkMain1 = Color(0xffffcb00);\nconst _darkInput = Color(0xff282E3A);\n\nclass DandelionColorScheme extends FlowyColorScheme {\n  const DandelionColorScheme.light()\n      : super(\n          surface: Colors.white,\n          hover: const Color(0xFFe0f8ff),\n          // hover effect on setting value\n          selector: _lightDandelionLightYellow,\n          red: const Color(0xfffb006d),\n          yellow: const Color(0xffffd667),\n          green: const Color(0xff66cf80),\n          shader1: const Color(0xff333333),\n          shader2: const Color(0xff4f4f4f),\n          shader3: const Color(0xff828282),\n          // disable text color\n          shader4: const Color(0xffbdbdbd),\n          shader5: _lightShader5,\n          shader6: const Color(0xfff2f2f2),\n          shader7: _black,\n          bg1: _lightBg1,\n          bg2: const Color(0xffedeef2),\n          // Hover color on trash button\n          bg3: _lightDandelionYellow,\n          bg4: const Color(0xff2c144b),\n          tint1: const Color(0xffe8e0ff),\n          tint2: const Color(0xffffe7fd),\n          tint3: const Color(0xffffe7ee),\n          tint4: const Color(0xffffefe3),\n          tint5: const Color(0xfffff2cd),\n          tint6: const Color(0xfff5ffdc),\n          tint7: const Color(0xffddffd6),\n          tint8: const Color(0xffdefff1),\n          tint9: _lightTint9,\n          main1: _lightDandelionYellow,\n          // cursor color\n          main2: _lightDandelionYellow,\n          shadow: const Color.fromRGBO(0, 0, 0, 0.15),\n          sidebarBg: _lightDandelionGreen,\n          divider: _lightShader6,\n          topbarBg: _white,\n          icon: _lightShader1,\n          text: _lightShader1,\n          secondaryText: _lightShader1,\n          strongText: Colors.black,\n          input: _white,\n          hint: _lightShader3,\n          primary: _lightDandelionYellow,\n          onPrimary: _lightShader1,\n          // hover color in sidebar\n          hoverBG1: _lightDandelionYellow,\n          // tool bar hover color\n          hoverBG2: _lightDandelionLightYellow,\n          hoverBG3: _lightShader6,\n          hoverFG: _lightShader1,\n          questionBubbleBG: _lightDandelionLightYellow,\n          progressBarBGColor: _lightTint9,\n          toolbarColor: _lightShader1,\n          toggleButtonBGColor: _lightDandelionYellow,\n          calendarWeekendBGColor: const Color(0xFFFBFBFC),\n          gridRowCountColor: _black,\n          borderColor: ColorSchemeConstants.lightBorderColor,\n          scrollbarColor: const Color(0x3F171717),\n          scrollbarHoverColor: const Color(0x7F171717),\n          lightIconColor: const Color(0xFF8F959E),\n          toolbarHoverColor: const Color(0xFFF2F4F7),\n        );\n\n  const DandelionColorScheme.dark()\n      : super(\n          surface: const Color(0xff292929),\n          hover: const Color(0xff1f1f1f),\n          selector: _darkShader2,\n          red: const Color(0xfffb006d),\n          yellow: const Color(0xffffd667),\n          green: const Color(0xff66cf80),\n          shader1: _white,\n          shader2: _darkShader2,\n          shader3: const Color(0xff828282),\n          shader4: const Color(0xffbdbdbd),\n          shader5: _darkShader5,\n          shader6: _darkShader6,\n          shader7: _white,\n          bg1: const Color(0xFFD5A200),\n          bg2: _black,\n          bg3: _darkMain1,\n          bg4: const Color(0xff2c144b),\n          tint1: const Color(0x4d9327FF),\n          tint2: const Color(0x66FC0088),\n          tint3: const Color(0x4dFC00E2),\n          tint4: const Color(0x80BE5B00),\n          tint5: const Color(0x33F8EE00),\n          tint6: const Color(0x4d6DC300),\n          tint7: const Color(0x5900BD2A),\n          tint8: const Color(0x80008890),\n          tint9: const Color(0x4d0029FF),\n          main1: _darkMain1,\n          main2: _darkMain1,\n          shadow: const Color(0xff0F131C),\n          sidebarBg: const Color(0xff25300e),\n          divider: _darkShader3,\n          topbarBg: _darkShader1,\n          icon: _darkShader5,\n          text: _darkShader5,\n          secondaryText: _darkShader5,\n          strongText: Colors.white,\n          input: _darkInput,\n          hint: _darkShader5,\n          primary: _darkMain1,\n          onPrimary: _darkShader1,\n          hoverBG1: _darkMain1,\n          hoverBG2: _darkMain1,\n          hoverBG3: _darkShader3,\n          hoverFG: _darkShader1,\n          questionBubbleBG: _darkShader3,\n          progressBarBGColor: _darkShader3,\n          toolbarColor: _darkInput,\n          toggleButtonBGColor: _darkShader1,\n          calendarWeekendBGColor: const Color(0xff121212),\n          gridRowCountColor: _darkMain1,\n          borderColor: ColorSchemeConstants.darkBorderColor,\n          scrollbarColor: const Color(0x40FFFFFF),\n          scrollbarHoverColor: const Color(0x80FFFFFF),\n          lightIconColor: const Color(0xFF8F959E),\n          toolbarHoverColor: _lightShader6,\n        );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'colorscheme.dart';\n\nclass ColorSchemeConstants {\n  static const white = Color(0xFFFFFFFF);\n  static const lightHover = Color(0xFFe0f8FF);\n  static const lightSelector = Color(0xFFf2fcFF);\n  static const lightBg1 = Color(0xFFf7f8fc);\n  static const lightBg2 = Color(0x0F1F2329);\n  static const lightShader1 = Color(0xFF333333);\n  static const lightShader3 = Color(0xFF828282);\n  static const lightShader5 = Color(0xFFe0e0e0);\n  static const lightShader6 = Color(0xFFf2f2f2);\n  static const lightMain1 = Color(0xFF00bcf0);\n  static const lightTint9 = Color(0xFFe1fbFF);\n  static const darkShader1 = Color(0xFF131720);\n  static const darkShader2 = Color(0xFF1A202C);\n  static const darkShader3 = Color(0xFF363D49);\n  static const darkShader5 = Color(0xFFBBC3CD);\n  static const darkShader6 = Color(0xFFF2F2F2);\n  static const darkMain1 = Color(0xFF00BCF0);\n  static const darkMain2 = Color(0xFF00BCF0);\n  static const darkInput = Color(0xFF282E3A);\n  static const lightBorderColor = Color(0xFFEDEDEE);\n  static const darkBorderColor = Color(0xFF3A3F49);\n}\n\nclass DefaultColorScheme extends FlowyColorScheme {\n  const DefaultColorScheme.light()\n      : super(\n          surface: ColorSchemeConstants.white,\n          hover: ColorSchemeConstants.lightHover,\n          selector: ColorSchemeConstants.lightSelector,\n          red: const Color(0xFFfb006d),\n          yellow: const Color(0xFFFFd667),\n          green: const Color(0xFF66cf80),\n          shader1: ColorSchemeConstants.lightShader1,\n          shader2: const Color(0xFF4f4f4f),\n          shader3: ColorSchemeConstants.lightShader3,\n          shader4: const Color(0xFFbdbdbd),\n          shader5: ColorSchemeConstants.lightShader5,\n          shader6: ColorSchemeConstants.lightShader6,\n          shader7: ColorSchemeConstants.lightShader1,\n          bg1: ColorSchemeConstants.lightBg1,\n          bg2: ColorSchemeConstants.lightBg2,\n          bg3: const Color(0xFFe2e4eb),\n          bg4: const Color(0xFF2c144b),\n          tint1: const Color(0xFFe8e0FF),\n          tint2: const Color(0xFFFFe7fd),\n          tint3: const Color(0xFFFFe7ee),\n          tint4: const Color(0xFFFFefe3),\n          tint5: const Color(0xFFFFf2cd),\n          tint6: const Color(0xFFf5FFdc),\n          tint7: const Color(0xFFddFFd6),\n          tint8: const Color(0xFFdeFFf1),\n          tint9: ColorSchemeConstants.lightTint9,\n          main1: ColorSchemeConstants.lightMain1,\n          main2: const Color(0xFF00b7ea),\n          shadow: const Color.fromRGBO(0, 0, 0, 0.15),\n          sidebarBg: ColorSchemeConstants.lightBg1,\n          divider: ColorSchemeConstants.lightShader6,\n          topbarBg: ColorSchemeConstants.white,\n          icon: ColorSchemeConstants.lightShader1,\n          text: ColorSchemeConstants.lightShader1,\n          secondaryText: const Color(0xFF4f4f4f),\n          strongText: Colors.black,\n          input: ColorSchemeConstants.white,\n          hint: ColorSchemeConstants.lightShader3,\n          primary: ColorSchemeConstants.lightMain1,\n          onPrimary: ColorSchemeConstants.white,\n          hoverBG1: ColorSchemeConstants.lightBg2,\n          hoverBG2: ColorSchemeConstants.lightHover,\n          hoverBG3: ColorSchemeConstants.lightShader6,\n          hoverFG: ColorSchemeConstants.lightShader1,\n          questionBubbleBG: ColorSchemeConstants.lightSelector,\n          progressBarBGColor: ColorSchemeConstants.lightTint9,\n          toolbarColor: ColorSchemeConstants.lightShader1,\n          toggleButtonBGColor: ColorSchemeConstants.lightShader5,\n          calendarWeekendBGColor: const Color(0xFFFBFBFC),\n          gridRowCountColor: ColorSchemeConstants.lightShader1,\n          borderColor: ColorSchemeConstants.lightBorderColor,\n          scrollbarColor: const Color(0x3F171717),\n          scrollbarHoverColor: const Color(0x7F171717),\n          lightIconColor: const Color(0xFF8F959E),\n          toolbarHoverColor: const Color(0xFFF2F4F7),\n        );\n\n  const DefaultColorScheme.dark()\n      : super(\n          surface: ColorSchemeConstants.darkShader2,\n          hover: ColorSchemeConstants.darkMain1,\n          selector: ColorSchemeConstants.darkShader2,\n          red: const Color(0xFFfb006d),\n          yellow: const Color(0xFFF7CF46),\n          green: const Color(0xFF66CF80),\n          shader1: ColorSchemeConstants.darkShader1,\n          shader2: ColorSchemeConstants.darkShader2,\n          shader3: ColorSchemeConstants.darkShader3,\n          shader4: const Color(0xFF505469),\n          shader5: ColorSchemeConstants.darkShader5,\n          shader6: ColorSchemeConstants.darkShader6,\n          shader7: ColorSchemeConstants.white,\n          bg1: const Color(0xFF1A202C),\n          bg2: const Color(0xFFEDEEF2),\n          bg3: ColorSchemeConstants.darkMain1,\n          bg4: const Color(0xFF2C144B),\n          tint1: const Color(0x4D502FD6),\n          tint2: const Color(0x4DBF1CC0),\n          tint3: const Color(0x4DC42A53),\n          tint4: const Color(0x4DD77922),\n          tint5: const Color(0x4DC59A1A),\n          tint6: const Color(0x4DA4C824),\n          tint7: const Color(0x4D23CA2E),\n          tint8: const Color(0x4D19CCAC),\n          tint9: const Color(0x4D04A9D7),\n          main1: ColorSchemeConstants.darkMain2,\n          main2: const Color(0xFF00B7EA),\n          shadow: const Color(0xFF0F131C),\n          sidebarBg: const Color(0xFF232B38),\n          divider: ColorSchemeConstants.darkShader3,\n          topbarBg: ColorSchemeConstants.darkShader1,\n          icon: ColorSchemeConstants.darkShader5,\n          text: ColorSchemeConstants.darkShader5,\n          secondaryText: ColorSchemeConstants.darkShader5,\n          strongText: Colors.white,\n          input: ColorSchemeConstants.darkInput,\n          hint: const Color(0xFF59647a),\n          primary: ColorSchemeConstants.darkMain2,\n          onPrimary: ColorSchemeConstants.darkShader1,\n          hoverBG1: const Color(0x1AFFFFFF),\n          hoverBG2: ColorSchemeConstants.darkMain1,\n          hoverBG3: ColorSchemeConstants.darkShader3,\n          hoverFG: const Color(0xE5FFFFFF),\n          questionBubbleBG: ColorSchemeConstants.darkShader3,\n          progressBarBGColor: ColorSchemeConstants.darkShader3,\n          toolbarColor: ColorSchemeConstants.darkInput,\n          toggleButtonBGColor: const Color(0xFF828282),\n          calendarWeekendBGColor: ColorSchemeConstants.darkShader1,\n          gridRowCountColor: ColorSchemeConstants.darkShader5,\n          borderColor: ColorSchemeConstants.darkBorderColor,\n          scrollbarColor: const Color(0x40FFFFFF),\n          scrollbarHoverColor: const Color(0x80FFFFFF),\n          lightIconColor: const Color(0xFF8F959E),\n          toolbarHoverColor: ColorSchemeConstants.lightShader6,\n        );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart",
    "content": "import 'package:flowy_infra/colorscheme/default_colorscheme.dart';\nimport 'package:flutter/material.dart';\n\nimport 'colorscheme.dart';\n\nconst _black = Color(0xff000000);\nconst _white = Color(0xFFFFFFFF);\n\nconst _lightHover = Color(0xffd8d6fc);\nconst _lightSelector = Color(0xffe5e3f9);\nconst _lightBg1 = Color(0xfff2f0f6);\nconst _lightBg2 = Color(0xffd8d6fc);\nconst _lightShader1 = Color(0xff333333);\nconst _lightShader3 = Color(0xff828282);\nconst _lightShader5 = Color(0xffe0e0e0);\nconst _lightShader6 = Color(0xffd8d6fc);\nconst _lightMain1 = Color(0xffaba9e7);\nconst _lightTint9 = Color(0xffe1fbff);\n\nconst _darkShader1 = Color(0xff131720);\nconst _darkShader2 = Color(0xff1A202C);\nconst _darkShader3 = Color(0xff363D49);\nconst _darkShader5 = Color(0xffBBC3CD);\nconst _darkShader6 = Color(0xffF2F2F2);\nconst _darkMain1 = Color(0xffab00ff);\nconst _darkInput = Color(0xff282E3A);\n\nclass LavenderColorScheme extends FlowyColorScheme {\n  const LavenderColorScheme.light()\n      : super(\n          surface: Colors.white,\n          hover: _lightHover,\n          selector: _lightSelector,\n          red: const Color(0xfffb006d),\n          yellow: const Color(0xffffd667),\n          green: const Color(0xff66cf80),\n          shader1: const Color(0xff333333),\n          shader2: const Color(0xff4f4f4f),\n          shader3: const Color(0xff828282),\n          shader4: const Color(0xffbdbdbd),\n          shader5: _lightShader5,\n          shader6: const Color(0xfff2f2f2),\n          shader7: _black,\n          bg1: const Color(0xffAC59FF),\n          bg2: const Color(0xffedeef2),\n          bg3: _lightHover,\n          bg4: const Color(0xff2c144b),\n          tint1: const Color(0xffe8e0ff),\n          tint2: const Color(0xffffe7fd),\n          tint3: const Color(0xffffe7ee),\n          tint4: const Color(0xffffefe3),\n          tint5: const Color(0xfffff2cd),\n          tint6: const Color(0xfff5ffdc),\n          tint7: const Color(0xffddffd6),\n          tint8: const Color(0xffdefff1),\n          tint9: _lightMain1,\n          main1: _lightMain1,\n          main2: _lightMain1,\n          shadow: const Color.fromRGBO(0, 0, 0, 0.15),\n          sidebarBg: _lightBg1,\n          divider: _lightShader6,\n          topbarBg: _white,\n          icon: _lightShader1,\n          text: _lightShader1,\n          secondaryText: _lightShader1,\n          strongText: Colors.black,\n          input: _white,\n          hint: _lightShader3,\n          primary: _lightMain1,\n          onPrimary: _lightShader1,\n          hoverBG1: _lightBg2,\n          hoverBG2: _lightHover,\n          hoverBG3: _lightShader6,\n          hoverFG: _lightShader1,\n          questionBubbleBG: _lightSelector,\n          progressBarBGColor: _lightTint9,\n          toolbarColor: _lightShader1,\n          toggleButtonBGColor: _lightSelector,\n          calendarWeekendBGColor: const Color(0xFFFBFBFC),\n          gridRowCountColor: _black,\n          borderColor: ColorSchemeConstants.lightBorderColor,\n          scrollbarColor: const Color(0x3F171717),\n          scrollbarHoverColor: const Color(0x7F171717),\n          lightIconColor: const Color(0xFF8F959E),\n          toolbarHoverColor: const Color(0xFFF2F4F7),\n        );\n\n  const LavenderColorScheme.dark()\n      : super(\n          surface: const Color(0xFF1B1A1D),\n          hover: _darkMain1,\n          selector: _darkShader2,\n          red: const Color(0xfffb006d),\n          yellow: const Color(0xffffd667),\n          green: const Color(0xff66cf80),\n          shader1: _white,\n          shader2: _darkShader2,\n          shader3: const Color(0xff828282),\n          shader4: const Color(0xffbdbdbd),\n          shader5: _white,\n          shader6: _darkShader6,\n          shader7: _white,\n          bg1: const Color(0xff8C23F6),\n          bg2: _black,\n          bg3: _darkMain1,\n          bg4: const Color(0xff2c144b),\n          tint1: const Color(0x4d9327FF),\n          tint2: const Color(0x66FC0088),\n          tint3: const Color(0x4dFC00E2),\n          tint4: const Color(0x80BE5B00),\n          tint5: const Color(0x33F8EE00),\n          tint6: const Color(0x4d6DC300),\n          tint7: const Color(0x5900BD2A),\n          tint8: const Color(0x80008890),\n          tint9: const Color(0x4d0029FF),\n          main1: _darkMain1,\n          main2: _darkMain1,\n          shadow: const Color(0xff0F131C),\n          sidebarBg: const Color(0xff2D223B),\n          divider: _darkShader3,\n          topbarBg: _darkShader1,\n          icon: _darkShader5,\n          text: _darkShader5,\n          secondaryText: _darkShader5,\n          strongText: Colors.white,\n          input: _darkInput,\n          hint: _darkShader5,\n          primary: _darkMain1,\n          onPrimary: _darkShader1,\n          hoverBG1: _darkMain1,\n          hoverBG2: _darkMain1,\n          hoverBG3: _darkShader3,\n          hoverFG: _darkShader1,\n          questionBubbleBG: _darkShader3,\n          progressBarBGColor: _darkShader3,\n          toolbarColor: _darkInput,\n          toggleButtonBGColor: _darkShader1,\n          calendarWeekendBGColor: const Color(0xff121212),\n          gridRowCountColor: _darkMain1,\n          borderColor: ColorSchemeConstants.darkBorderColor,\n          scrollbarColor: const Color(0x40FFFFFF),\n          scrollbarHoverColor: const Color(0x80FFFFFF),\n          lightIconColor: const Color(0xFF8F959E),\n          toolbarHoverColor: _lightShader6,\n        );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart",
    "content": "import 'package:flowy_infra/colorscheme/default_colorscheme.dart';\nimport 'package:flutter/material.dart';\n\nimport 'colorscheme.dart';\n\nconst _black = Color(0xff000000);\nconst _white = Color(0xFFFFFFFF);\nconst _lightBg1 = Color(0xFFFFD13E);\nconst _lightShader1 = Color(0xff333333);\nconst _lightShader3 = Color(0xff828282);\nconst _lightShader5 = Color(0xffe0e0e0);\nconst _lightShader6 = Color(0xfff2f2f2);\nconst _lightDandelionYellow = Color(0xffffcb00);\nconst _lightDandelionLightYellow = Color(0xffffdf66);\nconst _lightTint9 = Color(0xffe1fbff);\n\nconst _darkShader1 = Color(0xff131720);\nconst _darkShader2 = Color(0xff1A202C);\nconst _darkShader3 = Color(0xff363D49);\nconst _darkShader5 = Color(0xffBBC3CD);\nconst _darkShader6 = Color(0xffF2F2F2);\nconst _darkMain1 = Color(0xffffcb00);\nconst _darkInput = Color(0xff282E3A);\n\n// Derive from [DandelionColorScheme]\n// Use a light yellow color in the sidebar intead of a green color in Dandelion\n// Some field name are still included 'Dandelion' to indicate they are the same color as the one in Dandelion\nclass LemonadeColorScheme extends FlowyColorScheme {\n  const LemonadeColorScheme.light()\n      : super(\n          surface: Colors.white,\n          hover: const Color(0xFFe0f8ff),\n          // hover effect on setting value\n          selector: _lightDandelionLightYellow,\n          red: const Color(0xfffb006d),\n          yellow: const Color(0xffffd667),\n          green: const Color(0xff66cf80),\n          shader1: const Color(0xff333333),\n          shader2: const Color(0xff4f4f4f),\n          shader3: const Color(0xff828282),\n          // disable text color\n          shader4: const Color(0xffbdbdbd),\n          shader5: _lightShader5,\n          shader6: const Color(0xfff2f2f2),\n          shader7: _black,\n          bg1: _lightBg1,\n          bg2: const Color(0xffedeef2),\n          // Hover color on trash button\n          bg3: _lightDandelionYellow,\n          bg4: const Color(0xff2c144b),\n          tint1: const Color(0xffe8e0ff),\n          tint2: const Color(0xffffe7fd),\n          tint3: const Color(0xffffe7ee),\n          tint4: const Color(0xffffefe3),\n          tint5: const Color(0xfffff2cd),\n          tint6: const Color(0xfff5ffdc),\n          tint7: const Color(0xffddffd6),\n          tint8: const Color(0xffdefff1),\n          tint9: _lightTint9,\n          main1: _lightDandelionYellow,\n          // cursor color\n          main2: _lightDandelionYellow,\n          shadow: const Color.fromRGBO(0, 0, 0, 0.15),\n          sidebarBg: const Color(0xfffaf0c8),\n          divider: _lightShader6,\n          topbarBg: _white,\n          icon: _lightShader1,\n          text: _lightShader1,\n          secondaryText: _lightShader1,\n          strongText: Colors.black,\n          input: _white,\n          hint: _lightShader3,\n          primary: _lightDandelionYellow,\n          onPrimary: _lightShader1,\n          // hover color in sidebar\n          hoverBG1: _lightDandelionYellow,\n          // tool bar hover color\n          hoverBG2: _lightDandelionLightYellow,\n          hoverBG3: _lightShader6,\n          hoverFG: _lightShader1,\n          questionBubbleBG: _lightDandelionLightYellow,\n          progressBarBGColor: _lightTint9,\n          toolbarColor: _lightShader1,\n          toggleButtonBGColor: _lightDandelionYellow,\n          calendarWeekendBGColor: const Color(0xFFFBFBFC),\n          gridRowCountColor: _black,\n          borderColor: ColorSchemeConstants.lightBorderColor,\n          scrollbarColor: const Color(0x3F171717),\n          scrollbarHoverColor: const Color(0x7F171717),\n          lightIconColor: const Color(0xFF8F959E),\n          toolbarHoverColor: const Color(0xFFF2F4F7),\n        );\n\n  const LemonadeColorScheme.dark()\n      : super(\n            surface: const Color(0xff292929),\n            hover: const Color(0xff1f1f1f),\n            selector: _darkShader2,\n            red: const Color(0xfffb006d),\n            yellow: const Color(0xffffd667),\n            green: const Color(0xff66cf80),\n            shader1: _white,\n            shader2: _darkShader2,\n            shader3: const Color(0xff828282),\n            shader4: const Color(0xffbdbdbd),\n            shader5: _darkShader5,\n            shader6: _darkShader6,\n            shader7: _white,\n            bg1: const Color(0xFFD5A200),\n            bg2: _black,\n            bg3: _darkMain1,\n            bg4: const Color(0xff2c144b),\n            tint1: const Color(0x4d9327FF),\n            tint2: const Color(0x66FC0088),\n            tint3: const Color(0x4dFC00E2),\n            tint4: const Color(0x80BE5B00),\n            tint5: const Color(0x33F8EE00),\n            tint6: const Color(0x4d6DC300),\n            tint7: const Color(0x5900BD2A),\n            tint8: const Color(0x80008890),\n            tint9: const Color(0x4d0029FF),\n            main1: _darkMain1,\n            main2: _darkMain1,\n            shadow: _black,\n            sidebarBg: const Color(0xff232B38),\n            divider: _darkShader3,\n            topbarBg: _darkShader1,\n            icon: _darkShader5,\n            text: _darkShader5,\n            secondaryText: _darkShader5,\n            strongText: Colors.white,\n            input: _darkInput,\n            hint: _darkShader5,\n            primary: _darkMain1,\n            onPrimary: _darkShader1,\n            hoverBG1: _darkMain1,\n            hoverBG2: _darkMain1,\n            hoverBG3: _darkShader3,\n            hoverFG: _darkShader1,\n            questionBubbleBG: _darkShader3,\n            progressBarBGColor: _darkShader3,\n            toolbarColor: _darkInput,\n            toggleButtonBGColor: _darkShader1,\n            calendarWeekendBGColor: const Color(0xff121212),\n            gridRowCountColor: _darkMain1,\n            borderColor: ColorSchemeConstants.darkBorderColor,\n            scrollbarColor: const Color(0x40FFFFFF),\n            scrollbarHoverColor: const Color(0x80FFFFFF),\n            lightIconColor: const Color(0xFF8F959E),\n            toolbarHoverColor: _lightShader6);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_impl.dart",
    "content": "import 'package:flutter/services.dart';\n\nimport 'package:file_picker/file_picker.dart' as fp;\nimport 'package:flowy_infra/file_picker/file_picker_service.dart';\n\nclass FilePicker implements FilePickerService {\n  @override\n  Future<String?> getDirectoryPath({String? title}) {\n    return fp.FilePicker.platform.getDirectoryPath();\n  }\n\n  @override\n  Future<FilePickerResult?> pickFiles({\n    String? dialogTitle,\n    String? initialDirectory,\n    fp.FileType type = fp.FileType.any,\n    List<String>? allowedExtensions,\n    Function(fp.FilePickerStatus p1)? onFileLoading,\n    bool allowCompression = true,\n    bool allowMultiple = false,\n    bool withData = false,\n    bool withReadStream = false,\n    bool lockParentWindow = false,\n  }) async {\n    final result = await fp.FilePicker.platform.pickFiles(\n      dialogTitle: dialogTitle,\n      initialDirectory: initialDirectory,\n      type: type,\n      allowedExtensions: allowedExtensions,\n      onFileLoading: onFileLoading,\n      allowCompression: allowCompression,\n      allowMultiple: allowMultiple,\n      withData: withData,\n      withReadStream: withReadStream,\n      lockParentWindow: lockParentWindow,\n    );\n    return FilePickerResult(result?.files ?? []);\n  }\n\n  /// On Desktop it will return the path to which the file should be saved.\n  ///\n  /// On Mobile it will return the path to where the file has been saved, and will\n  /// automatically save it. The [bytes] parameter is required on Mobile.\n  ///\n  @override\n  Future<String?> saveFile({\n    String? dialogTitle,\n    String? fileName,\n    String? initialDirectory,\n    FileType type = FileType.any,\n    List<String>? allowedExtensions,\n    bool lockParentWindow = false,\n    Uint8List? bytes,\n  }) async {\n    final result = await fp.FilePicker.platform.saveFile(\n      dialogTitle: dialogTitle,\n      fileName: fileName,\n      initialDirectory: initialDirectory,\n      type: type,\n      allowedExtensions: allowedExtensions,\n      lockParentWindow: lockParentWindow,\n      bytes: bytes,\n    );\n\n    return result;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_service.dart",
    "content": "import 'package:file_picker/file_picker.dart';\n\nexport 'package:file_picker/file_picker.dart'\n    show FileType, FilePickerStatus, PlatformFile;\n\nclass FilePickerResult {\n  const FilePickerResult(this.files);\n\n  /// Picked files.\n  final List<PlatformFile> files;\n}\n\n/// Abstract file picker as a service to implement dependency injection.\nabstract class FilePickerService {\n  Future<String?> getDirectoryPath({\n    String? title,\n  }) async =>\n      throw UnimplementedError('getDirectoryPath() has not been implemented.');\n\n  Future<FilePickerResult?> pickFiles({\n    String? dialogTitle,\n    String? initialDirectory,\n    FileType type = FileType.any,\n    List<String>? allowedExtensions,\n    Function(FilePickerStatus)? onFileLoading,\n    bool allowCompression = true,\n    bool allowMultiple = false,\n    bool withData = false,\n    bool withReadStream = false,\n    bool lockParentWindow = false,\n  }) async =>\n      throw UnimplementedError('pickFiles() has not been implemented.');\n\n  Future<String?> saveFile({\n    String? dialogTitle,\n    String? fileName,\n    String? initialDirectory,\n    FileType type = FileType.any,\n    List<String>? allowedExtensions,\n    bool lockParentWindow = false,\n  }) async =>\n      throw UnimplementedError('saveFile() has not been implemented.');\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/icon_data.dart",
    "content": "/// Flutter icons FlowyIconData\n/// Copyright (C) 2021 by original authors @ fluttericon.com, fontello.com\n/// This font was generated by FlutterIcon.com, which is derived from Fontello.\n///\n/// To use this font, place it in your fonts/ directory and include the\n/// following in your pubspec.yaml\n///\n/// flutter:\n///   fonts:\n///    - family:  FlowyIconData\n///      fonts:\n///       - asset: fonts/FlowyIconData.ttf\n///\n///\n///\nlibrary;\n// ignore_for_file: constant_identifier_names\n\nimport 'package:flutter/widgets.dart';\n\nclass FlowyIconData {\n  FlowyIconData._();\n\n  static const _kFontFam = 'FlowyIconData';\n  static const String? _kFontPkg = null;\n\n  static const IconData drop_down_hide =\n      IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg);\n  static const IconData drop_down_show =\n      IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart",
    "content": "import 'package:flutter/material.dart';\n\nString languageFromLocale(Locale locale) {\n  switch (locale.languageCode) {\n    // Most often used languages\n    case \"en\":\n      switch (locale.countryCode) {\n        case \"GB\":\n          return \"English (GB)\";\n        case \"US\":\n          return \"English (US)\";\n        default:\n          return \"English (US)\";\n      }\n    case \"zh\":\n      switch (locale.countryCode) {\n        case \"CN\":\n          return \"简体中文\";\n        case \"TW\":\n          return \"繁體中文\";\n        default:\n          return locale.languageCode;\n      }\n\n    // Then in alphabetical order\n    case \"am\":\n      return \"አማርኛ\";\n    case \"ar\":\n      return \"العربية\";\n    case \"ca\":\n      return \"Català\";\n    case \"cs\":\n      return \"Čeština\";\n    case \"ckb\":\n      switch (locale.countryCode) {\n        case \"KU\":\n          return \"کوردی سۆرانی\";\n        default:\n          return locale.languageCode;\n      }\n    case \"de\":\n      return \"Deutsch\";\n    case \"es\":\n      return \"Español\";\n    case \"eu\":\n      return \"Euskera\";\n    case \"el\":\n      return \"Ελληνικά\";\n    case \"fr\":\n      switch (locale.countryCode) {\n        case \"CA\":\n          return \"Français (CA)\";\n        case \"FR\":\n          return \"Français (FR)\";\n        default:\n          return locale.languageCode;\n      }\n    case \"mr\":\n      return \"मराठी\";\n    case \"he\":\n      return \"עברית\";\n    case \"hu\":\n      return \"Magyar\";\n    case \"id\":\n      return \"Bahasa Indonesia\";\n    case \"it\":\n      return \"Italiano\";\n    case \"ja\":\n      return \"日本語\";\n    case \"ko\":\n      return \"한국어\";\n    case \"pl\":\n      return \"Polski\";\n    case \"pt\":\n      return \"Português\";\n    case \"ru\":\n      return \"русский\";\n    case \"sv\":\n      return \"Svenska\";\n    case \"th\":\n      return \"ไทย\";\n    case \"tr\":\n      return \"Türkçe\";\n    case \"fa\":\n      return \"فارسی\";\n    case \"uk\":\n      return \"українська\";\n    case \"ur\":\n      return \"اردو\";\n    case \"hin\":\n      return \"हिन्दी\";\n  }\n  // If not found then the language code will be displayed\n  return locale.languageCode;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/notifier.dart",
    "content": "import 'package:flutter/material.dart';\n\nabstract class Comparable<T> {\n  bool compare(T? previous, T? current);\n}\n\nclass ObjectComparable<T> extends Comparable<T> {\n  @override\n  bool compare(T? previous, T? current) {\n    return previous == current;\n  }\n}\n\nclass PublishNotifier<T> extends ChangeNotifier {\n  T? _value;\n  Comparable<T>? comparable = ObjectComparable();\n\n  PublishNotifier({this.comparable});\n\n  set value(T newValue) {\n    if (comparable != null) {\n      if (comparable!.compare(_value, newValue)) {\n        _value = newValue;\n        notifyListeners();\n      }\n    } else {\n      _value = newValue;\n      notifyListeners();\n    }\n  }\n\n  T? get currentValue => _value;\n\n  void addPublishListener(void Function(T) callback,\n      {bool Function()? listenWhen}) {\n    super.addListener(\n      () {\n        if (_value == null) {\n          return;\n        } else {}\n\n        if (listenWhen != null && listenWhen() == false) {\n          return;\n        }\n\n        callback(_value as T);\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/platform_extension.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/foundation.dart';\n\nextension PlatformExtension on Platform {\n  /// Returns true if the operating system is macOS and not running on Web platform.\n  static bool get isMacOS {\n    if (kIsWeb) {\n      return false;\n    }\n    return Platform.isMacOS;\n  }\n\n  /// Returns true if the operating system is Windows and not running on Web platform.\n  static bool get isWindows {\n    if (kIsWeb) {\n      return false;\n    }\n    return Platform.isWindows;\n  }\n\n  /// Returns true if the operating system is Linux and not running on Web platform.\n  static bool get isLinux {\n    if (kIsWeb) {\n      return false;\n    }\n    return Platform.isLinux;\n  }\n\n  static bool get isDesktopOrWeb {\n    if (kIsWeb) {\n      return true;\n    }\n    return isDesktop;\n  }\n\n  static bool get isDesktop {\n    if (kIsWeb) {\n      return false;\n    }\n    return Platform.isWindows || Platform.isLinux || Platform.isMacOS;\n  }\n\n  static bool get isMobile {\n    if (kIsWeb) {\n      return false;\n    }\n    return Platform.isAndroid || Platform.isIOS;\n  }\n\n  static bool get isNotMobile {\n    if (kIsWeb) {\n      return false;\n    }\n    return !isMobile;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_bloc.dart",
    "content": "import 'package:bloc/bloc.dart';\nimport 'package:flowy_infra/plugins/service/models/exceptions.dart';\nimport 'package:flowy_infra/plugins/service/plugin_service.dart';\n\nimport '../../file_picker/file_picker_impl.dart';\n\nimport 'dynamic_plugin_event.dart';\nimport 'dynamic_plugin_state.dart';\n\nclass DynamicPluginBloc extends Bloc<DynamicPluginEvent, DynamicPluginState> {\n  DynamicPluginBloc({FilePicker? filePicker})\n      : super(const DynamicPluginState.uninitialized()) {\n    on<DynamicPluginEvent>(dispatch);\n    add(DynamicPluginEvent.load());\n  }\n\n  Future<void> dispatch(\n      DynamicPluginEvent event, Emitter<DynamicPluginState> emit) async {\n    await event.when(\n      addPlugin: () => addPlugin(emit),\n      removePlugin: (name) => removePlugin(emit, name),\n      load: () => onLoadRequested(emit),\n    );\n  }\n\n  Future<void> onLoadRequested(Emitter<DynamicPluginState> emit) async {\n    emit(\n      DynamicPluginState.ready(\n        plugins: await FlowyPluginService.instance.plugins,\n      ),\n    );\n  }\n\n  Future<void> addPlugin(Emitter<DynamicPluginState> emit) async {\n    emit(const DynamicPluginState.processing());\n    try {\n      final plugin = await FlowyPluginService.pick();\n      if (plugin == null) {\n        return emit(\n          DynamicPluginState.ready(\n            plugins: await FlowyPluginService.instance.plugins,\n          ),\n        );\n      }\n      await FlowyPluginService.instance.addPlugin(plugin);\n    } on PluginCompilationException catch (exception) {\n      return emit(\n        DynamicPluginState.compilationFailure(errorMessage: exception.message),\n      );\n    }\n\n    emit(const DynamicPluginState.compilationSuccess());\n    emit(\n      DynamicPluginState.ready(\n        plugins: await FlowyPluginService.instance.plugins,\n      ),\n    );\n  }\n\n  Future<void> removePlugin(\n    Emitter<DynamicPluginState> emit,\n    String name,\n  ) async {\n    emit(const DynamicPluginState.processing());\n\n    final plugin = await FlowyPluginService.instance.lookup(name: name);\n\n    if (plugin == null) {\n      return emit(\n        DynamicPluginState.ready(\n          plugins: await FlowyPluginService.instance.plugins,\n        ),\n      );\n    }\n\n    await FlowyPluginService.removePlugin(plugin);\n\n    emit(const DynamicPluginState.deletionSuccess());\n    emit(\n      DynamicPluginState.ready(\n        plugins: await FlowyPluginService.instance.plugins,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_event.dart",
    "content": "import 'package:freezed_annotation/freezed_annotation.dart';\n\npart 'dynamic_plugin_event.freezed.dart';\n\n@freezed\nclass DynamicPluginEvent with _$DynamicPluginEvent {\n  factory DynamicPluginEvent.addPlugin() = _AddPlugin;\n  factory DynamicPluginEvent.removePlugin({required String name}) =\n      _RemovePlugin;\n  factory DynamicPluginEvent.load() = _Load;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_state.dart",
    "content": "import 'package:freezed_annotation/freezed_annotation.dart';\n\nimport '../service/models/flowy_dynamic_plugin.dart';\n\npart 'dynamic_plugin_state.freezed.dart';\n\n@freezed\nclass DynamicPluginState with _$DynamicPluginState {\n  const factory DynamicPluginState.uninitialized() = _Uninitialized;\n  const factory DynamicPluginState.ready({\n    required Iterable<FlowyDynamicPlugin> plugins,\n  }) = Ready;\n  const factory DynamicPluginState.processing() = _Processing;\n  const factory DynamicPluginState.compilationFailure(\n      {required String errorMessage}) = _CompilationFailure;\n  const factory DynamicPluginState.deletionFailure({\n    required String path,\n  }) = _DeletionFailure;\n  const factory DynamicPluginState.deletionSuccess() = _DeletionSuccess;\n  const factory DynamicPluginState.compilationSuccess() = _CompilationSuccess;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/location_service.dart",
    "content": "import 'dart:io';\n\nclass PluginLocationService {\n  const PluginLocationService({\n    required Future<Directory> fallback,\n  }) : _fallback = fallback;\n\n  final Future<Directory> _fallback;\n\n  Future<Directory> get fallback async => _fallback;\n\n  Future<Directory> get location async => fallback;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/exceptions.dart",
    "content": "class PluginCompilationException implements Exception {\n  final String message;\n\n  PluginCompilationException(this.message);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/flowy_dynamic_plugin.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\n\nimport 'package:file/memory.dart';\nimport 'package:flowy_infra/colorscheme/colorscheme.dart';\nimport 'package:flowy_infra/plugins/service/models/exceptions.dart';\nimport 'package:flowy_infra/theme.dart';\nimport 'package:path/path.dart' as p;\n\nimport 'plugin_type.dart';\n\ntypedef DynamicPluginLibrary = Iterable<FlowyDynamicPlugin>;\n\n/// A class that encapsulates dynamically loaded plugins for AppFlowy.\n///\n/// This class can be modified to support loading node widget builders and other\n/// plugins that are dynamically loaded at runtime for the editor. For now,\n/// it only supports loading app themes.\nclass FlowyDynamicPlugin {\n  FlowyDynamicPlugin._({\n    required String name,\n    required String path,\n    this.theme,\n  })  : _name = name,\n        _path = path;\n\n  /// The plugins should be loaded into a folder with the extension `.flowy_plugin`.\n  static bool isPlugin(FileSystemEntity entity) =>\n      entity is Directory && p.extension(entity.path).contains(ext);\n\n  /// The extension for the plugin folder.\n  static const String ext = 'flowy_plugin';\n  static String get lightExtension => ['light', 'json'].join('.');\n  static String get darkExtension => ['dark', 'json'].join('.');\n\n  String get name => _name;\n  late final String _name;\n\n  String get _fsPluginName => [name, ext].join('.');\n\n  final AppTheme? theme;\n  final String _path;\n\n  Directory get source {\n    return Directory(_path);\n  }\n\n  /// Loads and \"compiles\" loaded plugins.\n  ///\n  /// If the plugin loaded does not contain the `.flowy_plugin` extension, this\n  /// this method will throw an error. Likewise, if the plugin does not follow\n  /// the expected format, this method will throw an error.\n  static Future<FlowyDynamicPlugin> decode({required Directory src}) async {\n    // throw an error if the plugin does not follow the proper format.\n    if (!isPlugin(src)) {\n      throw PluginCompilationException(\n        'The plugin directory must have the extension `.flowy_plugin`.',\n      );\n    }\n\n    // throws an error if the plugin does not follow the proper format.\n    final type = PluginType.from(src: src);\n\n    switch (type) {\n      case PluginType.theme:\n        return _theme(src: src);\n    }\n  }\n\n  /// Encodes the plugin in memory. The Directory given is not the actual\n  /// directory on the file system, but rather a virtual directory in memory.\n  ///\n  /// Instances of this class should always have a path on disk, otherwise a\n  /// compilation error will be thrown during the construction of this object.\n  Future<Directory> encode() async {\n    final fs = MemoryFileSystem();\n    final directory = fs.directory(_fsPluginName)..createSync();\n\n    final lightThemeFileName = '$name.$lightExtension';\n    directory.childFile(lightThemeFileName).createSync();\n    directory\n        .childFile(lightThemeFileName)\n        .writeAsStringSync(jsonEncode(theme!.lightTheme.toJson()));\n\n    final darkThemeFileName = '$name.$darkExtension';\n    directory.childFile(darkThemeFileName).createSync();\n    directory\n        .childFile(darkThemeFileName)\n        .writeAsStringSync(jsonEncode(theme!.darkTheme.toJson()));\n\n    return directory;\n  }\n\n  /// Theme plugins should have the following format.\n  /// > directory.flowy_plugin // plugin root\n  /// >   - theme.light.json   // the light theme\n  /// >   - theme.dark.json    // the dark theme\n  ///\n  /// If the theme does not adhere to that format, it is considered an error.\n  static Future<FlowyDynamicPlugin> _theme({required Directory src}) async {\n    late final String name;\n    try {\n      name = p.basenameWithoutExtension(src.path).split('.').first;\n    } catch (e) {\n      throw PluginCompilationException(\n        'The theme plugin does not adhere to the following format: `<plugin_name>.flowy_plugin`.',\n      );\n    }\n\n    final light = src\n        .listSync()\n        .where((event) =>\n            event is File && p.basename(event.path).contains(lightExtension))\n        .first as File;\n\n    final dark = src\n        .listSync()\n        .where((event) =>\n            event is File && p.basename(event.path).contains(darkExtension))\n        .first as File;\n\n    late final FlowyColorScheme lightTheme;\n    late final FlowyColorScheme darkTheme;\n\n    try {\n      lightTheme = FlowyColorScheme.fromJsonSoft(\n        await jsonDecode(await light.readAsString()),\n      );\n    } catch (e) {\n      throw PluginCompilationException(\n        'The light theme json file is not valid.',\n      );\n    }\n\n    try {\n      darkTheme = FlowyColorScheme.fromJsonSoft(\n        await jsonDecode(await dark.readAsString()),\n        Brightness.dark,\n      );\n    } catch (e) {\n      throw PluginCompilationException(\n        'The dark theme json file is not valid.',\n      );\n    }\n\n    final theme = AppTheme(\n      themeName: name,\n      builtIn: false,\n      lightTheme: lightTheme,\n      darkTheme: darkTheme,\n    );\n\n    return FlowyDynamicPlugin._(\n      name: theme.themeName,\n      path: src.path,\n      theme: theme,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/plugin_type.dart",
    "content": "import 'dart:io';\n\nimport 'package:flowy_infra/plugins/service/models/exceptions.dart';\nimport 'package:flowy_infra/plugins/service/models/flowy_dynamic_plugin.dart';\nimport 'package:path/path.dart' as p;\n\nenum PluginType {\n  theme._();\n\n  const PluginType._();\n\n  factory PluginType.from({required Directory src}) {\n    if (_isTheme(src)) {\n      return PluginType.theme;\n    }\n    throw PluginCompilationException(\n        'Could not determine the plugin type from source `$src`.');\n  }\n\n  static bool _isTheme(Directory plugin) {\n    final files = plugin.listSync();\n    return files.any((entity) =>\n            entity is File &&\n            p\n                .basename(entity.path)\n                .endsWith(FlowyDynamicPlugin.lightExtension)) &&\n        files.any((entity) =>\n            entity is File &&\n            p.basename(entity.path).endsWith(FlowyDynamicPlugin.darkExtension));\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/plugin_service.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:flowy_infra/file_picker/file_picker_impl.dart';\n\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\nimport 'location_service.dart';\nimport 'models/flowy_dynamic_plugin.dart';\n\n/// A service to maintain the state of the plugins for AppFlowy.\nclass FlowyPluginService {\n  FlowyPluginService._();\n  static final FlowyPluginService _instance = FlowyPluginService._();\n  static FlowyPluginService get instance => _instance;\n\n  PluginLocationService _locationService = PluginLocationService(\n    fallback: getApplicationDocumentsDirectory(),\n  );\n\n  void setLocation(PluginLocationService locationService) =>\n      _locationService = locationService;\n\n  Future<Iterable<Directory>> get _targets async {\n    final location = await _locationService.location;\n    final targets = location.listSync().where(FlowyDynamicPlugin.isPlugin);\n    return targets.map<Directory>((entity) => entity as Directory).toList();\n  }\n\n  /// Searches the [PluginLocationService.location] for plugins and compiles them.\n  Future<DynamicPluginLibrary> get plugins async {\n    final List<FlowyDynamicPlugin> compiled = [];\n    for (final src in await _targets) {\n      final plugin = await FlowyDynamicPlugin.decode(src: src);\n      compiled.add(plugin);\n    }\n    return compiled;\n  }\n\n  /// Chooses a plugin from the file system using FilePickerService and tries to compile it.\n  ///\n  /// If the operation is cancelled or the plugin is invalid, this method will return null.\n  static Future<FlowyDynamicPlugin?> pick({FilePicker? service}) async {\n    service ??= FilePicker();\n\n    final result = await service.getDirectoryPath();\n\n    if (result == null) {\n      return null;\n    }\n\n    final directory = Directory(result);\n    return FlowyDynamicPlugin.decode(src: directory);\n  }\n\n  /// Searches the plugin registry for a plugin with the given name.\n  Future<FlowyDynamicPlugin?> lookup({required String name}) async {\n    final library = await plugins;\n    return library\n        // cast to nullable type to allow return of null if not found.\n        .cast<FlowyDynamicPlugin?>()\n        // null assert is fine here because the original list was non-nullable\n        .firstWhere((plugin) => plugin!.name == name, orElse: () => null);\n  }\n\n  /// Adds a plugin to the registry. To construct a [FlowyDynamicPlugin]\n  /// use [FlowyDynamicPlugin.encode()]\n  Future<void> addPlugin(FlowyDynamicPlugin plugin) async {\n    // try to compile the plugin before we add it to the registry.\n    final source = await plugin.encode();\n    // add the plugin to the registry\n    final destionation = [\n      (await _locationService.location).path,\n      p.basename(source.path),\n    ].join(Platform.pathSeparator);\n\n    _copyDirectorySync(source, Directory(destionation));\n  }\n\n  /// Removes a plugin from the registry.\n  static Future<void> removePlugin(FlowyDynamicPlugin plugin) async {\n    final target = plugin.source;\n    await target.delete(recursive: true);\n  }\n\n  static void _copyDirectorySync(Directory source, Directory destination) {\n    if (!destination.existsSync()) {\n      destination.createSync(recursive: true);\n    }\n\n    for (final child in source.listSync(recursive: false)) {\n      final newPath = p.join(destination.path, p.basename(child.path));\n      if (child is File) {\n        File(newPath)\n          ..createSync(recursive: true)\n          ..writeAsStringSync(child.readAsStringSync());\n      } else if (child is Directory) {\n        _copyDirectorySync(child, Directory(newPath));\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/size.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass PageBreaks {\n  static double get largePhone => 550;\n\n  static double get tabletPortrait => 768;\n\n  static double get tabletLandscape => 1024;\n\n  static double get desktop => 1440;\n}\n\nclass Insets {\n  /// Dynamic insets, may get scaled with the device size\n  static double scale = 1;\n\n  static double get xs => 2 * scale;\n\n  static double get sm => 6 * scale;\n\n  static double get m => 12 * scale;\n\n  static double get l => 24 * scale;\n\n  static double get xl => 36 * scale;\n\n  static double get xxl => 64 * scale;\n\n  static double get xxxl => 80 * scale;\n}\n\nclass FontSizes {\n  static double get scale => 1;\n\n  static double get s11 => 11 * scale;\n\n  static double get s12 => 12 * scale;\n\n  static double get s14 => 14 * scale;\n\n  static double get s16 => 16 * scale;\n\n  static double get s18 => 18 * scale;\n\n  static double get s20 => 20 * scale;\n\n  static double get s24 => 24 * scale;\n\n  static double get s32 => 32 * scale;\n\n  static double get s44 => 44 * scale;\n}\n\nclass Sizes {\n  static double hitScale = 1;\n\n  static double get hit => 40 * hitScale;\n\n  static double get iconMed => 20;\n}\n\nclass Corners {\n  static const BorderRadius s3Border = BorderRadius.all(s3Radius);\n  static const Radius s3Radius = Radius.circular(3);\n\n  static const BorderRadius s4Border = BorderRadius.all(s4Radius);\n  static const Radius s4Radius = Radius.circular(4);\n\n  static const BorderRadius s5Border = BorderRadius.all(s5Radius);\n  static const Radius s5Radius = Radius.circular(5);\n\n  static const BorderRadius s6Border = BorderRadius.all(s6Radius);\n  static const Radius s6Radius = Radius.circular(6);\n\n  static const BorderRadius s8Border = BorderRadius.all(s8Radius);\n  static const Radius s8Radius = Radius.circular(8);\n\n  static const BorderRadius s10Border = BorderRadius.all(s10Radius);\n  static const Radius s10Radius = Radius.circular(10);\n\n  static const BorderRadius s12Border = BorderRadius.all(s12Radius);\n  static const Radius s12Radius = Radius.circular(12);\n\n  static const BorderRadius s16Border = BorderRadius.all(s16Radius);\n  static const Radius s16Radius = Radius.circular(16);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/theme.dart",
    "content": "import 'package:flowy_infra/colorscheme/colorscheme.dart';\nimport 'package:flowy_infra/colorscheme/default_colorscheme.dart';\nimport 'plugins/service/plugin_service.dart';\n\nclass BuiltInTheme {\n  static const String defaultTheme = 'Default';\n  static const String dandelion = 'Dandelion';\n  static const String lemonade = 'Lemonade';\n  static const String lavender = 'Lavender';\n}\n\nclass AppTheme {\n  // metadata member\n  final bool builtIn;\n  final String themeName;\n  final FlowyColorScheme lightTheme;\n  final FlowyColorScheme darkTheme;\n  // static final Map<String, dynamic> _cachedJsonData = {};\n\n  const AppTheme({\n    required this.builtIn,\n    required this.themeName,\n    required this.lightTheme,\n    required this.darkTheme,\n  });\n\n  static const AppTheme fallback = AppTheme(\n    builtIn: true,\n    themeName: BuiltInTheme.defaultTheme,\n    lightTheme: DefaultColorScheme.light(),\n    darkTheme: DefaultColorScheme.dark(),\n  );\n\n  static Future<Iterable<AppTheme>> _plugins(FlowyPluginService service) async {\n    final plugins = await service.plugins;\n    return plugins.map((plugin) => plugin.theme).whereType<AppTheme>();\n  }\n\n  static Iterable<AppTheme> get builtins => themeMap.entries\n      .map(\n        (entry) => AppTheme(\n          builtIn: true,\n          themeName: entry.key,\n          lightTheme: entry.value[0],\n          darkTheme: entry.value[1],\n        ),\n      )\n      .toList();\n\n  static Future<Iterable<AppTheme>> themes(FlowyPluginService service) async =>\n      [\n        ...builtins,\n        ...(await _plugins(service)),\n      ];\n\n  static Future<AppTheme> fromName(\n    String themeName, {\n    FlowyPluginService? pluginService,\n  }) async {\n    pluginService ??= FlowyPluginService.instance;\n    for (final theme in await themes(pluginService)) {\n      if (theme.themeName == themeName) {\n        return theme;\n      }\n    }\n    throw ArgumentError('The theme $themeName does not exist.');\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart",
    "content": "import 'package:flutter/material.dart';\n\n@immutable\nclass AFThemeExtension extends ThemeExtension<AFThemeExtension> {\n  static AFThemeExtension of(BuildContext context) =>\n      Theme.of(context).extension<AFThemeExtension>()!;\n\n  static AFThemeExtension? maybeOf(BuildContext context) =>\n      Theme.of(context).extension<AFThemeExtension>();\n\n  const AFThemeExtension({\n    required this.warning,\n    required this.success,\n    required this.tint1,\n    required this.tint2,\n    required this.tint3,\n    required this.tint4,\n    required this.tint5,\n    required this.tint6,\n    required this.tint7,\n    required this.tint8,\n    required this.tint9,\n    required this.greyHover,\n    required this.greySelect,\n    required this.lightGreyHover,\n    required this.toggleOffFill,\n    required this.textColor,\n    required this.secondaryTextColor,\n    required this.strongText,\n    required this.calloutBGColor,\n    required this.tableCellBGColor,\n    required this.calendarWeekendBGColor,\n    required this.code,\n    required this.callout,\n    required this.caption,\n    required this.progressBarBGColor,\n    required this.toggleButtonBGColor,\n    required this.gridRowCountColor,\n    required this.background,\n    required this.onBackground,\n    required this.borderColor,\n    required this.scrollbarColor,\n    required this.scrollbarHoverColor,\n    required this.toolbarHoverColor,\n    required this.lightIconColor,\n  });\n\n  final Color? warning;\n  final Color? success;\n\n  final Color tint1;\n  final Color tint2;\n  final Color tint3;\n  final Color tint4;\n  final Color tint5;\n  final Color tint6;\n  final Color tint7;\n  final Color tint8;\n  final Color tint9;\n\n  final Color textColor;\n  final Color secondaryTextColor;\n  final Color strongText;\n  final Color greyHover;\n  final Color greySelect;\n  final Color lightGreyHover;\n  final Color toggleOffFill;\n  final Color progressBarBGColor;\n  final Color toggleButtonBGColor;\n  final Color calloutBGColor;\n  final Color tableCellBGColor;\n  final Color calendarWeekendBGColor;\n  final Color gridRowCountColor;\n\n  final TextStyle code;\n  final TextStyle callout;\n  final TextStyle caption;\n\n  final Color background;\n  final Color onBackground;\n\n  /// The color of the border of the widget.\n  ///\n  /// This is used in the divider, outline border, etc.\n  final Color borderColor;\n\n  final Color scrollbarColor;\n  final Color scrollbarHoverColor;\n\n  final Color toolbarHoverColor;\n  final Color lightIconColor;\n\n  @override\n  AFThemeExtension copyWith({\n    Color? warning,\n    Color? success,\n    Color? tint1,\n    Color? tint2,\n    Color? tint3,\n    Color? tint4,\n    Color? tint5,\n    Color? tint6,\n    Color? tint7,\n    Color? tint8,\n    Color? tint9,\n    Color? textColor,\n    Color? secondaryTextColor,\n    Color? strongText,\n    Color? calloutBGColor,\n    Color? tableCellBGColor,\n    Color? greyHover,\n    Color? greySelect,\n    Color? lightGreyHover,\n    Color? toggleOffFill,\n    Color? progressBarBGColor,\n    Color? toggleButtonBGColor,\n    Color? calendarWeekendBGColor,\n    Color? gridRowCountColor,\n    TextStyle? code,\n    TextStyle? callout,\n    TextStyle? caption,\n    Color? background,\n    Color? onBackground,\n    Color? borderColor,\n    Color? scrollbarColor,\n    Color? scrollbarHoverColor,\n    Color? lightIconColor,\n    Color? toolbarHoverColor,\n  }) =>\n      AFThemeExtension(\n        warning: warning ?? this.warning,\n        success: success ?? this.success,\n        tint1: tint1 ?? this.tint1,\n        tint2: tint2 ?? this.tint2,\n        tint3: tint3 ?? this.tint3,\n        tint4: tint4 ?? this.tint4,\n        tint5: tint5 ?? this.tint5,\n        tint6: tint6 ?? this.tint6,\n        tint7: tint7 ?? this.tint7,\n        tint8: tint8 ?? this.tint8,\n        tint9: tint9 ?? this.tint9,\n        textColor: textColor ?? this.textColor,\n        secondaryTextColor: secondaryTextColor ?? this.secondaryTextColor,\n        strongText: strongText ?? this.strongText,\n        calloutBGColor: calloutBGColor ?? this.calloutBGColor,\n        tableCellBGColor: tableCellBGColor ?? this.tableCellBGColor,\n        greyHover: greyHover ?? this.greyHover,\n        greySelect: greySelect ?? this.greySelect,\n        lightGreyHover: lightGreyHover ?? this.lightGreyHover,\n        toggleOffFill: toggleOffFill ?? this.toggleOffFill,\n        progressBarBGColor: progressBarBGColor ?? this.progressBarBGColor,\n        toggleButtonBGColor: toggleButtonBGColor ?? this.toggleButtonBGColor,\n        calendarWeekendBGColor:\n            calendarWeekendBGColor ?? this.calendarWeekendBGColor,\n        gridRowCountColor: gridRowCountColor ?? this.gridRowCountColor,\n        code: code ?? this.code,\n        callout: callout ?? this.callout,\n        caption: caption ?? this.caption,\n        onBackground: onBackground ?? this.onBackground,\n        background: background ?? this.background,\n        borderColor: borderColor ?? this.borderColor,\n        scrollbarColor: scrollbarColor ?? this.scrollbarColor,\n        scrollbarHoverColor: scrollbarHoverColor ?? this.scrollbarHoverColor,\n        lightIconColor: lightIconColor ?? this.lightIconColor,\n        toolbarHoverColor: toolbarHoverColor ?? this.toolbarHoverColor,\n      );\n\n  @override\n  ThemeExtension<AFThemeExtension> lerp(\n      ThemeExtension<AFThemeExtension>? other, double t) {\n    if (other is! AFThemeExtension) {\n      return this;\n    }\n    return AFThemeExtension(\n      warning: Color.lerp(warning, other.warning, t),\n      success: Color.lerp(success, other.success, t),\n      tint1: Color.lerp(tint1, other.tint1, t)!,\n      tint2: Color.lerp(tint2, other.tint2, t)!,\n      tint3: Color.lerp(tint3, other.tint3, t)!,\n      tint4: Color.lerp(tint4, other.tint4, t)!,\n      tint5: Color.lerp(tint5, other.tint5, t)!,\n      tint6: Color.lerp(tint6, other.tint6, t)!,\n      tint7: Color.lerp(tint7, other.tint7, t)!,\n      tint8: Color.lerp(tint8, other.tint8, t)!,\n      tint9: Color.lerp(tint9, other.tint9, t)!,\n      textColor: Color.lerp(textColor, other.textColor, t)!,\n      secondaryTextColor: Color.lerp(\n        secondaryTextColor,\n        other.secondaryTextColor,\n        t,\n      )!,\n      strongText: Color.lerp(\n        strongText,\n        other.strongText,\n        t,\n      )!,\n      calloutBGColor: Color.lerp(calloutBGColor, other.calloutBGColor, t)!,\n      tableCellBGColor:\n          Color.lerp(tableCellBGColor, other.tableCellBGColor, t)!,\n      greyHover: Color.lerp(greyHover, other.greyHover, t)!,\n      greySelect: Color.lerp(greySelect, other.greySelect, t)!,\n      lightGreyHover: Color.lerp(lightGreyHover, other.lightGreyHover, t)!,\n      toggleOffFill: Color.lerp(toggleOffFill, other.toggleOffFill, t)!,\n      progressBarBGColor:\n          Color.lerp(progressBarBGColor, other.progressBarBGColor, t)!,\n      toggleButtonBGColor:\n          Color.lerp(toggleButtonBGColor, other.toggleButtonBGColor, t)!,\n      calendarWeekendBGColor:\n          Color.lerp(calendarWeekendBGColor, other.calendarWeekendBGColor, t)!,\n      gridRowCountColor:\n          Color.lerp(gridRowCountColor, other.gridRowCountColor, t)!,\n      code: other.code,\n      callout: other.callout,\n      caption: other.caption,\n      onBackground: Color.lerp(onBackground, other.onBackground, t)!,\n      background: Color.lerp(background, other.background, t)!,\n      borderColor: Color.lerp(borderColor, other.borderColor, t)!,\n      scrollbarColor: Color.lerp(scrollbarColor, other.scrollbarColor, t)!,\n      scrollbarHoverColor:\n          Color.lerp(scrollbarHoverColor, other.scrollbarHoverColor, t)!,\n      lightIconColor: Color.lerp(lightIconColor, other.lightIconColor, t)!,\n      toolbarHoverColor:\n          Color.lerp(toolbarHoverColor, other.toolbarHoverColor, t)!,\n    );\n  }\n}\n\nenum FlowyTint {\n  tint1,\n  tint2,\n  tint3,\n  tint4,\n  tint5,\n  tint6,\n  tint7,\n  tint8,\n  tint9;\n\n  String toJson() => name;\n  static FlowyTint fromJson(String json) {\n    try {\n      return FlowyTint.values.byName(json);\n    } catch (_) {\n      return FlowyTint.tint1;\n    }\n  }\n\n  static FlowyTint? fromId(String id) {\n    for (final value in FlowyTint.values) {\n      if (value.id == id) {\n        return value;\n      }\n    }\n    return null;\n  }\n\n  Color color(BuildContext context, {AFThemeExtension? theme}) =>\n      switch (this) {\n        FlowyTint.tint1 => theme?.tint1 ?? AFThemeExtension.of(context).tint1,\n        FlowyTint.tint2 => theme?.tint2 ?? AFThemeExtension.of(context).tint2,\n        FlowyTint.tint3 => theme?.tint3 ?? AFThemeExtension.of(context).tint3,\n        FlowyTint.tint4 => theme?.tint4 ?? AFThemeExtension.of(context).tint4,\n        FlowyTint.tint5 => theme?.tint5 ?? AFThemeExtension.of(context).tint5,\n        FlowyTint.tint6 => theme?.tint6 ?? AFThemeExtension.of(context).tint6,\n        FlowyTint.tint7 => theme?.tint7 ?? AFThemeExtension.of(context).tint7,\n        FlowyTint.tint8 => theme?.tint8 ?? AFThemeExtension.of(context).tint8,\n        FlowyTint.tint9 => theme?.tint9 ?? AFThemeExtension.of(context).tint9,\n      };\n\n  String get id => switch (this) {\n        // DON'T change this name because it's saved in the database!\n        FlowyTint.tint1 => 'appflowy_them_color_tint1',\n        FlowyTint.tint2 => 'appflowy_them_color_tint2',\n        FlowyTint.tint3 => 'appflowy_them_color_tint3',\n        FlowyTint.tint4 => 'appflowy_them_color_tint4',\n        FlowyTint.tint5 => 'appflowy_them_color_tint5',\n        FlowyTint.tint6 => 'appflowy_them_color_tint6',\n        FlowyTint.tint7 => 'appflowy_them_color_tint7',\n        FlowyTint.tint8 => 'appflowy_them_color_tint8',\n        FlowyTint.tint9 => 'appflowy_them_color_tint9',\n      };\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/time/duration.dart",
    "content": "import 'package:time/time.dart';\n\nexport 'package:time/time.dart';\n\nclass FlowyDurations {\n  static Duration get fastest => .15.seconds;\n\n  static Duration get fast => .25.seconds;\n\n  static Duration get medium => .35.seconds;\n\n  static Duration get slow => .7.seconds;\n}\n\nclass RouteDurations {\n  static Duration get slow => .7.seconds;\n  static Duration get medium => .35.seconds;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/time/prelude.dart",
    "content": "export \"duration.dart\";\nexport 'package:time/time.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/utils/color_converter.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:json_annotation/json_annotation.dart';\n\nclass ColorConverter implements JsonConverter<Color, String> {\n  const ColorConverter();\n\n  static const Color fallback = Colors.transparent;\n\n  @override\n  Color fromJson(String radixString) {\n    final int? color = int.tryParse(radixString);\n    return color == null ? fallback : Color(color);\n  }\n\n  @override\n  String toJson(Color color) {\n    final alpha = (color.a * 255).toInt().toRadixString(16).padLeft(2, '0');\n    final red = (color.r * 255).toInt().toRadixString(16).padLeft(2, '0');\n    final green = (color.g * 255).toInt().toRadixString(16).padLeft(2, '0');\n    final blue = (color.b * 255).toInt().toRadixString(16).padLeft(2, '0');\n\n    return '0x$alpha$red$green$blue'.toLowerCase();\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/lib/uuid.dart",
    "content": "import 'package:uuid/data.dart';\nimport 'package:uuid/rng.dart';\nimport 'package:uuid/uuid.dart';\n\nconst _uuid = Uuid();\n\nString uuid() {\n  return _uuid.v4();\n}\n\nString fixedUuid(int seed, UuidType type) {\n  return _uuid.v4(config: V4Options(null, MathRNG(seed: seed + type.index)));\n}\n\nenum UuidType {\n  // 0.6.0\n  publicSpace,\n  privateSpace,\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml",
    "content": "name: flowy_infra\ndescription: AppFlowy Infra.\nversion: 0.0.1\nhomepage: https://appflowy.io\n\nenvironment:\n  sdk: \">=3.0.0 <4.0.0\"\n  flutter: \">=3.10.1\"\n\ndependencies:\n  flutter:\n    sdk: flutter\n  json_annotation: ^4.7.0\n  path_provider: ^2.0.15\n  path: ^1.8.2\n  time: \">=2.0.0\"\n  uuid: \">=2.2.2\"\n  bloc: ^9.0.0\n  freezed_annotation: ^2.1.0\n  file_picker: ^8.0.2\n  file: ^7.0.0\n  analyzer: 6.11.0\n\ndev_dependencies:\n  build_runner: ^2.4.9\n  flutter_lints: ^3.0.1\n  freezed: ^2.4.7\n  json_serializable: ^6.5.4\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/.gitignore",
    "content": ".DS_Store\n.dart_tool/\n\n.packages\n.pub/\n\nbuild/\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/.metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: fa5883b78e566877613ad1ccb48dd92075cb5c23\n  channel: dev\n\nproject_type: plugin\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/CHANGELOG.md",
    "content": "## 0.0.1\n\n* TODO: Describe initial release.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/LICENSE",
    "content": "TODO: Add your license here.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/README.md",
    "content": "# flowy_infra_ui\n\nA new flutter plugin project.\n\n## Getting Started\n\nThis project is a starting point for a Flutter\n[plug-in package](https://flutter.dev/developing-packages/),\na specialized package that includes platform-specific implementation code for\nAndroid and/or iOS.\n\nFor help getting started with Flutter, view our\n[online documentation](https://flutter.dev/docs), which offers tutorials,\nsamples, guidance on mobile development, and a full API reference.\n\nThe plugin project was generated without specifying the `--platforms` flag, no platforms are currently supported.\nTo add platforms, run `flutter create -t plugin --platforms <platforms> .` under the same\ndirectory. You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/analysis_options.yaml",
    "content": "include: package:flutter_lints/flutter.yaml\n\n# Additional information about this file can be found at\n# https://dart.dev/guides/language/analysis-options\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/android/.classpath",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<classpath>\n\t<classpathentry kind=\"con\" path=\"org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/\"/>\n\t<classpathentry kind=\"con\" path=\"org.eclipse.buildship.core.gradleclasspathcontainer\"/>\n\t<classpathentry kind=\"output\" path=\"bin/default\"/>\n</classpath>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/android/.gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea/workspace.xml\n/.idea/libraries\n.DS_Store\n/build\n/captures\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/android/.project",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<projectDescription>\n\t<name>flowy_infra_ui</name>\n\t<comment>Project flowy_infra_ui created by Buildship.</comment>\n\t<projects>\n\t</projects>\n\t<buildSpec>\n\t\t<buildCommand>\n\t\t\t<name>org.eclipse.jdt.core.javabuilder</name>\n\t\t\t<arguments>\n\t\t\t</arguments>\n\t\t</buildCommand>\n\t\t<buildCommand>\n\t\t\t<name>org.eclipse.buildship.core.gradleprojectbuilder</name>\n\t\t\t<arguments>\n\t\t\t</arguments>\n\t\t</buildCommand>\n\t</buildSpec>\n\t<natures>\n\t\t<nature>org.eclipse.jdt.core.javanature</nature>\n\t\t<nature>org.eclipse.buildship.core.gradleprojectnature</nature>\n\t</natures>\n\t<filteredResources>\n\t\t<filter>\n\t\t\t<id>1693395487121</id>\n\t\t\t<name></name>\n\t\t\t<type>30</type>\n\t\t\t<matcher>\n\t\t\t\t<id>org.eclipse.core.resources.regexFilterMatcher</id>\n\t\t\t\t<arguments>node_modules|\\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>\n\t\t\t</matcher>\n\t\t</filter>\n\t</filteredResources>\n</projectDescription>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/android/.settings/org.eclipse.buildship.core.prefs",
    "content": "arguments=--init-script /var/folders/th/tfqrqcp12kvgzs3c3z0xqxlc0000gn/T/d146c9752a26f79b52047fb6dc6ed385d064e120494f96f08ca63a317c41f94c.gradle --init-script /var/folders/th/tfqrqcp12kvgzs3c3z0xqxlc0000gn/T/52cde0cfcf3e28b8b7510e992210d9614505e0911af0c190bd590d7158574963.gradle\nauto.sync=false\nbuild.scans.enabled=false\nconnection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(7.4.2))\nconnection.project.dir=\neclipse.preferences.version=1\ngradle.user.home=\njava.home=/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home\njvm.arguments=\noffline.mode=false\noverride.workspace.settings=true\nshow.console.view=true\nshow.executions.view=true\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/android/build.gradle",
    "content": "group 'com.example.flowy_infra_ui'\nversion '1.0'\n\nbuildscript {\n    ext.kotlin_version  = '1.8.0'\n    repositories {\n        google()\n        mavenCentral()\n    }\n\n    dependencies {\n        classpath 'com.android.tools.build:gradle:7.4.2'\n    }\n}\n\nrootProject.allprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\napply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion 33\n    namespace 'com.example.flowy_infra_ui'\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n    dependencies {\n        implementation \"androidx.core:core:1.5.0-rc01\"\n    }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/android/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.7-all.zip\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx1536M\nandroid.useAndroidX=true\nandroid.enableJetifier=true\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/android/settings.gradle",
    "content": "rootProject.name = 'flowy_infra_ui'\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/android/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  package=\"com.example.flowy_infra_ui\">\n</manifest>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/android/src/main/java/com/example/flowy_infra_ui/FlowyInfraUIPlugin.java",
    "content": "package com.example.flowy_infra_ui;\n\nimport android.app.Activity;\n\nimport androidx.annotation.NonNull;\n\nimport com.example.flowy_infra_ui.event.KeyboardEventHandler;\n\nimport io.flutter.embedding.engine.plugins.FlutterPlugin;\nimport io.flutter.embedding.engine.plugins.activity.ActivityAware;\nimport io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;\nimport io.flutter.plugin.common.EventChannel;\nimport io.flutter.plugin.common.MethodCall;\nimport io.flutter.plugin.common.MethodChannel;\nimport io.flutter.plugin.common.MethodChannel.MethodCallHandler;\nimport io.flutter.plugin.common.MethodChannel.Result;\n\n/** FlowyInfraUIPlugin */\npublic class FlowyInfraUIPlugin implements FlutterPlugin, ActivityAware, MethodCallHandler {\n\n  // MARK: - Constant\n  public static final String INFRA_UI_METHOD_CHANNEL_NAME = \"flowy_infra_ui_method\";\n  public static final String INFRA_UI_KEYBOARD_EVENT_CHANNEL_NAME = \"flowy_infra_ui_event/keyboard\";\n\n  public static final String INFRA_UI_METHOD_GET_PLATFORM_VERSION = \"getPlatformVersion\";\n\n  // Method Channel\n  private MethodChannel methodChannel;\n  // Event Channel\n  private KeyboardEventHandler keyboardEventHandler = new KeyboardEventHandler();\n\n  @Override\n  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {\n    methodChannel = new MethodChannel(\n            flutterPluginBinding.getBinaryMessenger(),\n            INFRA_UI_METHOD_CHANNEL_NAME);\n    methodChannel.setMethodCallHandler(this);\n\n    final EventChannel keyboardEventChannel = new EventChannel(\n            flutterPluginBinding.getBinaryMessenger(),\n            INFRA_UI_KEYBOARD_EVENT_CHANNEL_NAME);\n    keyboardEventChannel.setStreamHandler(keyboardEventHandler);\n  }\n\n  @Override\n  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {\n    methodChannel.setMethodCallHandler(null);\n    keyboardEventHandler.cancelObserveKeyboardAction();\n  }\n\n  @Override\n  public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {\n    keyboardEventHandler.observeKeyboardAction(binding.getActivity());\n  }\n\n  @Override\n  public void onDetachedFromActivityForConfigChanges() {\n    keyboardEventHandler.cancelObserveKeyboardAction();\n  }\n\n  @Override\n  public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {\n    keyboardEventHandler.observeKeyboardAction(binding.getActivity());\n  }\n\n  @Override\n  public void onDetachedFromActivity() {\n    keyboardEventHandler.cancelObserveKeyboardAction();\n  }\n\n  // MARK: - Method Channel\n\n  @Override\n  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {\n    if (call.method.equals(INFRA_UI_METHOD_GET_PLATFORM_VERSION)) {\n      result.success(\"Android \" + android.os.Build.VERSION.RELEASE);\n    } else {\n      result.notImplemented();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/android/src/main/java/com/example/flowy_infra_ui/event/KeyboardEventHandler.java",
    "content": "package com.example.flowy_infra_ui.event;\n\nimport android.app.Activity;\nimport android.os.Build;\nimport android.view.View;\n\nimport androidx.annotation.RequiresApi;\nimport androidx.core.view.OnApplyWindowInsetsListener;\nimport androidx.core.view.ViewCompat;\nimport androidx.core.view.WindowInsetsCompat;\n\nimport io.flutter.plugin.common.EventChannel;\n\npublic class KeyboardEventHandler implements EventChannel.StreamHandler {\n    private EventChannel.EventSink eventSink;\n    private View rootView;\n    private boolean isKeyboardShow = false;\n\n    @Override\n    public void onListen(Object arguments, EventChannel.EventSink events) {\n        eventSink = events;\n    }\n\n    @Override\n    public void onCancel(Object arguments) {\n        eventSink = null;\n    }\n\n    // MARK: - Helper\n\n    @RequiresApi(Build.VERSION_CODES.R)\n    public void observeKeyboardAction(Activity activity) {\n        rootView = activity.findViewById(android.R.id.content);\n\n        ViewCompat.setOnApplyWindowInsetsListener(rootView, new OnApplyWindowInsetsListener() {\n            @Override\n            public WindowInsetsCompat onApplyWindowInsets(View view, WindowInsetsCompat insets) {\n                isKeyboardShow = insets.isVisible(WindowInsetsCompat.Type.ime());\n                if (eventSink != null) {\n                    eventSink.success(isKeyboardShow);\n                }\n                return insets;\n            }\n        });\n    }\n\n    public void cancelObserveKeyboardAction() {\n        if (rootView != null) {\n            ViewCompat.setOnApplyWindowInsetsListener(rootView, null);\n            rootView = null;\n        }\n    }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/android/src/main/kotlin/com/example/flowy_infra_ui/FlowyInfraUiPlugin.kt",
    "content": "package com.example.flowy_infra_ui\n\nimport androidx.annotation.NonNull\n\nimport io.flutter.embedding.engine.plugins.FlutterPlugin\nimport io.flutter.plugin.common.MethodCall\nimport io.flutter.plugin.common.MethodChannel\nimport io.flutter.plugin.common.MethodChannel.MethodCallHandler\nimport io.flutter.plugin.common.MethodChannel.Result\n\n/** FlowyInfraUIPlugin */\nclass FlowyInfraUIPlugin: FlutterPlugin, MethodCallHandler {\n  /// The MethodChannel that will the communication between Flutter and native Android\n  ///\n  /// This local reference serves to register the plugin with the Flutter Engine and unregister it\n  /// when the Flutter Engine is detached from the Activity\n  private lateinit var channel : MethodChannel\n\n  override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {\n    channel = MethodChannel(flutterPluginBinding.binaryMessenger, \"flowy_infra_ui\")\n    channel.setMethodCallHandler(this)\n  }\n\n  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {\n    if (call.method == \"getPlatformVersion\") {\n      result.success(\"Android ${android.os.Build.VERSION.RELEASE}\")\n    } else {\n      result.notImplemented()\n    }\n  }\n\n  override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {\n    channel.setMethodCallHandler(null)\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\n/build/\n\n# Web related\nlib/generated_plugin_registrant.dart\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Android Studio will place build artifacts here\n/android/app/debug\n/android/app/profile\n/android/app/release\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/.metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: fa5883b78e566877613ad1ccb48dd92075cb5c23\n  channel: dev\n\nproject_type: app\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/README.md",
    "content": "# flowy_infra_ui_example\n\nDemonstrates how to use the flowy_infra_ui plugin.\n\n## Getting Started\n\nThis project is a starting point for a Flutter application.\n\nA few resources to get you started if this is your first Flutter project:\n\n- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)\n- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)\n\nFor help getting started with Flutter, view our\n[online documentation](https://flutter.dev/docs), which offers tutorials,\nsamples, guidance on mobile development, and a full API reference.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/analysis_options.yaml",
    "content": "# This file configures the analyzer, which statically analyzes Dart code to\n# check for errors, warnings, and lints.\n#\n# The issues identified by the analyzer are surfaced in the UI of Dart-enabled\n# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be\n# invoked from the command line by running `flutter analyze`.\n\n# The following line activates a set of recommended lints for Flutter apps,\n# packages, and plugins designed to encourage good coding practices.\ninclude: package:flutter_lints/flutter.yaml\n\nlinter:\n  # The lint rules applied to this project can be customized in the\n  # section below to disable rules from the `package:flutter_lints/flutter.yaml`\n  # included above or to enable additional rules. A list of all available lints\n  # and their documentation is published at\n  # https://dart-lang.github.io/linter/lints/index.html.\n  #\n  # Instead of disabling a lint rule for the entire project in the\n  # section below, it can also be suppressed for a single line of code\n  # or a specific dart file by using the `// ignore: name_of_lint` and\n  # `// ignore_for_file: name_of_lint` syntax on the line or in the file\n  # producing the lint.\n  rules:\n    # avoid_print: false  # Uncomment to disable the `avoid_print` rule\n    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule\n\n# Additional information about this file can be found at\n# https://dart.dev/guides/language/analysis-options\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/.gitignore",
    "content": "gradle-wrapper.jar\n/.gradle\n/captures/\n/gradlew\n/gradlew.bat\n/local.properties\nGeneratedPluginRegistrant.java\n\n# Remember to never publicly share your keystore.\n# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app\nkey.properties\n**/*.keystore\n**/*.jks\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/.project",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<projectDescription>\n\t<name>android</name>\n\t<comment>Project android created by Buildship.</comment>\n\t<projects>\n\t</projects>\n\t<buildSpec>\n\t\t<buildCommand>\n\t\t\t<name>org.eclipse.buildship.core.gradleprojectbuilder</name>\n\t\t\t<arguments>\n\t\t\t</arguments>\n\t\t</buildCommand>\n\t</buildSpec>\n\t<natures>\n\t\t<nature>org.eclipse.buildship.core.gradleprojectnature</nature>\n\t</natures>\n\t<filteredResources>\n\t\t<filter>\n\t\t\t<id>1626576261654</id>\n\t\t\t<name></name>\n\t\t\t<type>30</type>\n\t\t\t<matcher>\n\t\t\t\t<id>org.eclipse.core.resources.regexFilterMatcher</id>\n\t\t\t\t<arguments>node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>\n\t\t\t</matcher>\n\t\t</filter>\n\t</filteredResources>\n</projectDescription>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/.settings/org.eclipse.buildship.core.prefs",
    "content": "arguments=\nauto.sync=false\nbuild.scans.enabled=false\nconnection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)\nconnection.project.dir=\neclipse.preferences.version=1\ngradle.user.home=\njava.home=/Library/Java/JavaVirtualMachines/jdk11.0.5-zulu.jdk/Contents/Home\njvm.arguments=\noffline.mode=false\noverride.workspace.settings=true\nshow.console.view=true\nshow.executions.view=true\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/app/.classpath",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<classpath>\n\t<classpathentry kind=\"con\" path=\"org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/\"/>\n\t<classpathentry kind=\"con\" path=\"org.eclipse.buildship.core.gradleclasspathcontainer\"/>\n\t<classpathentry kind=\"output\" path=\"bin/default\"/>\n</classpath>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/app/.project",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<projectDescription>\n\t<name>app</name>\n\t<comment>Project app created by Buildship.</comment>\n\t<projects>\n\t</projects>\n\t<buildSpec>\n\t\t<buildCommand>\n\t\t\t<name>org.eclipse.jdt.core.javabuilder</name>\n\t\t\t<arguments>\n\t\t\t</arguments>\n\t\t</buildCommand>\n\t\t<buildCommand>\n\t\t\t<name>org.eclipse.buildship.core.gradleprojectbuilder</name>\n\t\t\t<arguments>\n\t\t\t</arguments>\n\t\t</buildCommand>\n\t</buildSpec>\n\t<natures>\n\t\t<nature>org.eclipse.jdt.core.javanature</nature>\n\t\t<nature>org.eclipse.buildship.core.gradleprojectnature</nature>\n\t</natures>\n\t<filteredResources>\n\t\t<filter>\n\t\t\t<id>1626576261660</id>\n\t\t\t<name></name>\n\t\t\t<type>30</type>\n\t\t\t<matcher>\n\t\t\t\t<id>org.eclipse.core.resources.regexFilterMatcher</id>\n\t\t\t\t<arguments>node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>\n\t\t\t</matcher>\n\t\t</filter>\n\t</filteredResources>\n</projectDescription>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/app/.settings/org.eclipse.buildship.core.prefs",
    "content": "arguments=\nauto.sync=false\nbuild.scans.enabled=false\nconnection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)\nconnection.project.dir=\neclipse.preferences.version=1\ngradle.user.home=\njava.home=/Library/Java/JavaVirtualMachines/jdk11.0.5-zulu.jdk/Contents/Home\njvm.arguments=\noffline.mode=false\noverride.workspace.settings=true\nshow.console.view=true\nshow.executions.view=true\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/app/build.gradle",
    "content": "def localProperties = new Properties()\ndef localPropertiesFile = rootProject.file('local.properties')\nif (localPropertiesFile.exists()) {\n    localPropertiesFile.withReader('UTF-8') { reader ->\n        localProperties.load(reader)\n    }\n}\n\ndef flutterRoot = localProperties.getProperty('flutter.sdk')\nif (flutterRoot == null) {\n    throw new GradleException(\"Flutter SDK not found. Define location with flutter.sdk in the local.properties file.\")\n}\n\ndef flutterVersionCode = localProperties.getProperty('flutter.versionCode')\nif (flutterVersionCode == null) {\n    flutterVersionCode = '1'\n}\n\ndef flutterVersionName = localProperties.getProperty('flutter.versionName')\nif (flutterVersionName == null) {\n    flutterVersionName = '1.0'\n}\n\napply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply from: \"$flutterRoot/packages/flutter_tools/gradle/flutter.gradle\"\n//apply plugin: 'kotlin-android-extensions'\n\n\n//androidExtensions {\n//    experimental = true\n//}\n\nandroid {\n    compileSdkVersion flutter.compileSdkVersion\n    ndkVersion flutter.ndkVersion\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    kotlinOptions {\n        jvmTarget = '1.8'\n    }\n\n    sourceSets {\n        main.java.srcDirs += 'src/main/kotlin'\n    }\n\n    defaultConfig {\n        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).\n        applicationId \"com.example.flowy_infra_ui_example\"\n        minSdkVersion flutter.minSdkVersion\n        targetSdkVersion flutter.targetSdkVersion\n        versionCode flutterVersionCode.toInteger()\n        versionName flutterVersionName\n        multiDexEnabled true\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled true\n            shrinkResources true\n            // TODO: Add your own signing config for the release build.\n            // Signing with the debug keys for now, so `flutter run --release` works.\n            signingConfig signingConfigs.debug\n        }\n    }\n\n}\n\nflutter {\n    source '../..'\n}\n\ndependencies {\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\"\n    implementation 'com.android.support:multidex:2.0.1'\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/app/src/debug/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.example.flowy_infra_ui_example\">\n    <!-- Flutter needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/app/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.example.flowy_infra_ui_example\">\n   <application\n        android:label=\"flowy_infra_ui_example\"\n        android:icon=\"@mipmap/ic_launcher\">\n        <activity\n            android:name=\".MainActivity\"\n            android:launchMode=\"singleTop\"\n            android:theme=\"@style/LaunchTheme\"\n            android:configChanges=\"orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode\"\n            android:hardwareAccelerated=\"true\"\n            android:windowSoftInputMode=\"adjustResize\">\n            <!-- Specifies an Android theme to apply to this Activity as soon as\n                 the Android process has started. This theme is visible to the user\n                 while the Flutter UI initializes. After that, this theme continues\n                 to determine the Window background behind the Flutter UI. -->\n            <meta-data\n              android:name=\"io.flutter.embedding.android.NormalTheme\"\n              android:resource=\"@style/NormalTheme\"\n              />\n            <!-- Displays an Android View that continues showing the launch screen\n                 Drawable until Flutter paints its first frame, then this splash\n                 screen fades out. A splash screen is useful to avoid any visual\n                 gap between the end of Android's launch screen and the painting of\n                 Flutter's first frame. -->\n            <meta-data\n              android:name=\"io.flutter.embedding.android.SplashScreenDrawable\"\n              android:resource=\"@drawable/launch_background\"\n              />\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\"/>\n                <category android:name=\"android.intent.category.LAUNCHER\"/>\n            </intent-filter>\n        </activity>\n        <!-- Don't delete the meta-data below.\n             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->\n        <meta-data\n            android:name=\"flutterEmbedding\"\n            android:value=\"2\" />\n    </application>\n</manifest>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt",
    "content": "package com.example.example\n\nimport io.flutter.embedding.android.FlutterActivity\n\nclass MainActivity: FlutterActivity() {\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/app/src/main/kotlin/com/example/flowy_infra_ui_example/MainActivity.kt",
    "content": "package com.example.flowy_infra_ui_example\n\nimport io.flutter.embedding.android.FlutterActivity\n\nclass MainActivity: FlutterActivity() {\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/app/src/main/res/drawable/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@android:color/white\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/app/src/main/res/drawable-v21/launch_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"?android:colorBackground\" />\n\n    <!-- You can insert your own image assets here -->\n    <!-- <item>\n        <bitmap\n            android:gravity=\"center\"\n            android:src=\"@mipmap/launch_image\" />\n    </item> -->\n</layer-list>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             Flutter draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n         \n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/app/src/main/res/values-night/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             Flutter draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n         \n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/app/src/profile/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.example.flowy_infra_ui_example\">\n    <!-- Flutter needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/build.gradle",
    "content": "buildscript {\n    ext.kotlin_version = '1.6.10'\n    repositories {\n        google()\n        mavenCentral()\n    }\n\n    dependencies {\n        classpath 'com.android.tools.build:gradle:4.1.1'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\nrootProject.buildDir = '../build'\nsubprojects {\n    project.buildDir = \"${rootProject.buildDir}/${project.name}\"\n    project.evaluationDependsOn(':app')\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/gradle/wrapper/gradle-wrapper.properties",
    "content": "#Sat Jul 17 23:27:26 CST 2021\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-7.1.1-bin.zip\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx1536M\nandroid.useAndroidX=true\nandroid.enableJetifier=true\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/android/settings.gradle",
    "content": "include ':app'\n\ndef localPropertiesFile = new File(rootProject.projectDir, \"local.properties\")\ndef properties = new Properties()\n\nassert localPropertiesFile.exists()\nlocalPropertiesFile.withReader(\"UTF-8\") { reader -> properties.load(reader) }\n\ndef flutterSdkPath = properties.getProperty(\"flutter.sdk\")\nassert flutterSdkPath != null, \"flutter.sdk not set in local.properties\"\napply from: \"$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle\"\n\n\n\ndef flutterProjectRoot = rootProject.projectDir.parentFile.toPath()\n\ndef plugins = new Properties()\ndef pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')\nif (pluginsFile.exists()) {\n    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }\n}\n\nplugins.each { name, path ->\n    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()\n    include \":$name\"\n    project(\":$name\").projectDir = pluginDirectory\n}"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/example/android/app/src/main/java/com/example/flowy_infra_ui_example/FlutterActivity.java",
    "content": "package example.android.app.src.main.java.com.example.flowy_infra_ui_example;\n\npublic class FlutterActivity {\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/.gitignore",
    "content": "*.mode1v3\n*.mode2v3\n*.moved-aside\n*.pbxuser\n*.perspectivev3\n**/*sync/\n.sconsign.dblite\n.tags*\n**/.vagrant/\n**/DerivedData/\nIcon?\n**/Pods/\n**/.symlinks/\nprofile\nxcuserdata\n**/.generated/\nFlutter/App.framework\nFlutter/Flutter.framework\nFlutter/Flutter.podspec\nFlutter/Generated.xcconfig\nFlutter/ephemeral/\nFlutter/app.flx\nFlutter/app.zip\nFlutter/flutter_assets/\nFlutter/flutter_export_environment.sh\nServiceDefinitions.json\nRunner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!default.mode1v3\n!default.mode2v3\n!default.pbxuser\n!default.perspectivev3\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Flutter/AppFrameworkInfo.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n  <key>CFBundleDevelopmentRegion</key>\n  <string>en</string>\n  <key>CFBundleExecutable</key>\n  <string>App</string>\n  <key>CFBundleIdentifier</key>\n  <string>io.flutter.flutter.app</string>\n  <key>CFBundleInfoDictionaryVersion</key>\n  <string>6.0</string>\n  <key>CFBundleName</key>\n  <string>App</string>\n  <key>CFBundlePackageType</key>\n  <string>FMWK</string>\n  <key>CFBundleShortVersionString</key>\n  <string>1.0</string>\n  <key>CFBundleSignature</key>\n  <string>????</string>\n  <key>CFBundleVersion</key>\n  <string>1.0</string>\n  <key>MinimumOSVersion</key>\n  <string>8.0</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Flutter/Debug.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Flutter/Release.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Podfile",
    "content": "# Uncomment this line to define a global platform for your project\n# platform :ios, '9.0'\n\n# CocoaPods analytics sends network stats synchronously affecting flutter build latency.\nENV['COCOAPODS_DISABLE_STATS'] = 'true'\n\nproject 'Runner', {\n  'Debug' => :debug,\n  'Profile' => :release,\n  'Release' => :release,\n}\n\ndef flutter_root\n  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)\n  unless File.exist?(generated_xcode_build_settings_path)\n    raise \"#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first\"\n  end\n\n  File.foreach(generated_xcode_build_settings_path) do |line|\n    matches = line.match(/FLUTTER_ROOT\\=(.*)/)\n    return matches[1].strip if matches\n  end\n  raise \"FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get\"\nend\n\nrequire File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)\n\nflutter_ios_podfile_setup\n\ntarget 'Runner' do\n  use_frameworks!\n  use_modular_headers!\n\n  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))\nend\n\npost_install do |installer|\n  installer.pods_project.targets.each do |target|\n    flutter_additional_ios_build_settings(target)\n  end\nend\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Runner/AppDelegate.swift",
    "content": "import UIKit\nimport Flutter\n\n@UIApplicationMain\n@objc class AppDelegate: FlutterAppDelegate {\n  override func application(\n    _ application: UIApplication,\n    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n  ) -> Bool {\n    GeneratedPluginRegistrant.register(with: self)\n    return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-40x40@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-40x40@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"60x60\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-60x60@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"60x60\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-60x60@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-20x20@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-29x29@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-29x29@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-40x40@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-40x40@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"76x76\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-76x76@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"76x76\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-76x76@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"83.5x83.5\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-83.5x83.5@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"1024x1024\",\n      \"idiom\" : \"ios-marketing\",\n      \"filename\" : \"Icon-App-1024x1024@1x.png\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@3x.png\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md",
    "content": "# Launch Screen Assets\n\nYou can customize the launch screen with your own desired assets by replacing the image files in this directory.\n\nYou can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images."
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Runner/Base.lproj/LaunchScreen.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"12121\" systemVersion=\"16G29\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" launchScreen=\"YES\" colorMatched=\"YES\" initialViewController=\"01J-lp-oVM\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"12089\"/>\n    </dependencies>\n    <scenes>\n        <!--View Controller-->\n        <scene sceneID=\"EHf-IW-A2E\">\n            <objects>\n                <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"Ydg-fD-yQy\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"xbc-2k-c8Z\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"Ze5-6b-2t3\">\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <imageView opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" image=\"LaunchImage\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"YRO-k0-Ey4\">\n                            </imageView>\n                        </subviews>\n                        <color key=\"backgroundColor\" red=\"1\" green=\"1\" blue=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n                        <constraints>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerX\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerX\" id=\"1a2-6s-vTC\"/>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerY\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerY\" id=\"4X2-HB-R7a\"/>\n                        </constraints>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"iYj-Kq-Ea1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"53\" y=\"375\"/>\n        </scene>\n    </scenes>\n    <resources>\n        <image name=\"LaunchImage\" width=\"168\" height=\"185\"/>\n    </resources>\n</document>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Runner/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"10117\" systemVersion=\"15F34\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" useTraitCollections=\"YES\" initialViewController=\"BYZ-38-t0r\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"10085\"/>\n    </dependencies>\n    <scenes>\n        <!--Flutter View Controller-->\n        <scene sceneID=\"tne-QT-ifu\">\n            <objects>\n                <viewController id=\"BYZ-38-t0r\" customClass=\"FlutterViewController\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"y3c-jy-aDJ\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"wfy-db-euE\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"8bC-Xf-vdC\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"600\" height=\"600\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" white=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"calibratedWhite\"/>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"dkx-z0-nzr\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>flowy_infra_ui_example</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>UILaunchStoryboardName</key>\n\t<string>LaunchScreen</string>\n\t<key>UIMainStoryboardFile</key>\n\t<string>Main</string>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UIViewControllerBasedStatusBarAppearance</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Runner/Runner-Bridging-Header.h",
    "content": "#import \"GeneratedPluginRegistrant.h\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 46;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };\n\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };\n\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };\n\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };\n\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };\n\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };\n\t\tB061A1718EA00FC8FD116CB3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 609D91DEF4C1832DC41DF975 /* Pods_Runner.framework */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t9705A1C41CF9048500538489 /* Embed Frameworks */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Embed Frameworks\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = \"<group>\"; };\n\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = \"<group>\"; };\n\t\t330E5DF8FFAD644160722BE7 /* 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 = \"<group>\"; };\n\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = \"<group>\"; };\n\t\t609D91DEF4C1832DC41DF975 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = \"Runner-Bridging-Header.h\"; sourceTree = \"<group>\"; };\n\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = \"<group>\"; };\n\t\t97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\t97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\tE433772F3E94B24F3479C2F9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.profile.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig\"; sourceTree = \"<group>\"; };\n\t\tF09BA83EC3ECFDE285785DB2 /* 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 = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t97C146EB1CF9000F007C117D /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tB061A1718EA00FC8FD116CB3 /* Pods_Runner.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t35285CDC74580BB1F9C79F75 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t609D91DEF4C1832DC41DF975 /* Pods_Runner.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t6F690F158B0591DDAF4BC371 /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t330E5DF8FFAD644160722BE7 /* Pods-Runner.debug.xcconfig */,\n\t\t\t\tF09BA83EC3ECFDE285785DB2 /* Pods-Runner.release.xcconfig */,\n\t\t\t\tE433772F3E94B24F3479C2F9 /* Pods-Runner.profile.xcconfig */,\n\t\t\t);\n\t\t\tname = Pods;\n\t\t\tpath = Pods;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9740EEB11CF90186004384FC /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */,\n\t\t\t);\n\t\t\tname = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146E51CF9000F007C117D = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9740EEB11CF90186004384FC /* Flutter */,\n\t\t\t\t97C146F01CF9000F007C117D /* Runner */,\n\t\t\t\t97C146EF1CF9000F007C117D /* Products */,\n\t\t\t\t6F690F158B0591DDAF4BC371 /* Pods */,\n\t\t\t\t35285CDC74580BB1F9C79F75 /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146EF1CF9000F007C117D /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146EE1CF9000F007C117D /* Runner.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146F01CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FA1CF9000F007C117D /* Main.storyboard */,\n\t\t\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */,\n\t\t\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,\n\t\t\t\t97C147021CF9000F007C117D /* Info.plist */,\n\t\t\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,\n\t\t\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,\n\t\t\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */,\n\t\t\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t97C146ED1CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tDA35FF5721D34BC0BE4E1DD5 /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t9740EEB61CF901F6004384FC /* Run Script */,\n\t\t\t\t97C146EA1CF9000F007C117D /* Sources */,\n\t\t\t\t97C146EB1CF9000F007C117D /* Frameworks */,\n\t\t\t\t97C146EC1CF9000F007C117D /* Resources */,\n\t\t\t\t9705A1C41CF9048500538489 /* Embed Frameworks */,\n\t\t\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */,\n\t\t\t\t8A9E1CD9725728F74665B246 /* [CP] Embed Pods Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 97C146EE1CF9000F007C117D /* Runner.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t97C146E61CF9000F007C117D /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastUpgradeCheck = 1020;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t97C146ED1CF9000F007C117D = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 7.3.1;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 97C146E51CF9000F007C117D;\n\t\t\tproductRefGroup = 97C146EF1CF9000F007C117D /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t97C146ED1CF9000F007C117D /* Runner */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t97C146EC1CF9000F007C117D /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,\n\t\t\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,\n\t\t\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,\n\t\t\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"Thin Binary\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" embed_and_thin\";\n\t\t};\n\t\t8A9E1CD9725728F74665B246 /* [CP] Embed Pods Frameworks */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\",\n\t\t\t\t\"${BUILT_PRODUCTS_DIR}/flowy_infra_ui/flowy_infra_ui.framework\",\n\t\t\t);\n\t\t\tname = \"[CP] Embed Pods Frameworks\";\n\t\t\toutputPaths = (\n\t\t\t\t\"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flowy_infra_ui.framework\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t9740EEB61CF901F6004384FC /* Run Script */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"Run Script\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" build\";\n\t\t};\n\t\tDA35FF5721D34BC0BE4E1DD5 /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t97C146EA1CF9000F007C117D /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,\n\t\t\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXVariantGroup section */\n\t\t97C146FA1CF9000F007C117D /* Main.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FB1CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C147001CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = LaunchScreen.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t249021D3217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 9.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t249021D4217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = \"$(inherited) @executable_path/Frameworks\";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.flowyInfraUiExample;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t97C147031CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 9.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147041CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 9.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Owholemodule\";\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t97C147061CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = \"$(inherited) @executable_path/Frameworks\";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.flowyInfraUiExample;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147071CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = \"$(inherited) @executable_path/Frameworks\";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.example.flowyInfraUiExample;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147031CF9000F007C117D /* Debug */,\n\t\t\t\t97C147041CF9000F007C117D /* Release */,\n\t\t\t\t249021D3217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147061CF9000F007C117D /* Debug */,\n\t\t\t\t97C147071CF9000F007C117D /* Release */,\n\t\t\t\t249021D4217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 97C146E61CF9000F007C117D /* Project object */;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1020\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n               BuildableName = \"Runner.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <Testables>\n      </Testables>\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <AdditionalOptions>\n      </AdditionalOptions>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n      <AdditionalOptions>\n      </AdditionalOptions>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n   <FileRef\n      location = \"group:Pods/Pods.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/home/demo_item.dart",
    "content": "import 'package:flutter/material.dart';\n\nabstract class ListItem {}\n\nabstract class DemoItem extends ListItem {\n  String buildTitle();\n\n  void handleTap(BuildContext context);\n}\n\nclass SectionHeaderItem extends ListItem {\n  SectionHeaderItem(this.title);\n\n  final String title;\n\n  Widget buildWidget(BuildContext context) => Text(title);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/home/home_screen.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport '../overlay/overlay_screen.dart';\nimport '../keyboard/keyboard_screen.dart';\nimport 'demo_item.dart';\n\nclass HomeScreen extends StatelessWidget {\n  const HomeScreen({super.key});\n\n  static List<ListItem> items = [\n    SectionHeaderItem('Widget Demos'),\n    KeyboardItem(),\n    OverlayItem(),\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('Demos'),\n      ),\n      body: ListView.builder(\n        itemCount: items.length,\n        itemBuilder: (context, index) {\n          final item = items[index];\n          if (item is SectionHeaderItem) {\n            return Container(\n              constraints: const BoxConstraints(maxHeight: 48.0),\n              color: Colors.grey[300],\n              alignment: Alignment.center,\n              child: ListTile(\n                title: Text(item.title),\n              ),\n            );\n          } else if (item is DemoItem) {\n            return ListTile(\n              title: Text(item.buildTitle()),\n              onTap: () => item.handleTap(context),\n            );\n          }\n          return const ListTile(\n            title: Text('Unknow.'),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/keyboard/keyboard_screen.dart",
    "content": "import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';\nimport 'package:flutter/material.dart';\nimport '../home/demo_item.dart';\n\nclass KeyboardItem extends DemoItem {\n  @override\n  String buildTitle() => 'Keyboard Listener';\n\n  @override\n  void handleTap(BuildContext context) {\n    Navigator.of(context).push(\n      MaterialPageRoute(\n        builder: (context) {\n          return const KeyboardScreen();\n        },\n      ),\n    );\n  }\n}\n\nclass KeyboardScreen extends StatefulWidget {\n  const KeyboardScreen({super.key});\n\n  @override\n  State<KeyboardScreen> createState() => _KeyboardScreenState();\n}\n\nclass _KeyboardScreenState extends State<KeyboardScreen> {\n  bool _isKeyboardVisible = false;\n  final TextEditingController _controller =\n      TextEditingController(text: 'Hello Flowy');\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('Keyboard Visibility Demo'),\n      ),\n      body: KeyboardVisibilityDetector(\n        onKeyboardVisibilityChange: (isKeyboardVisible) {\n          setState(() => _isKeyboardVisible = isKeyboardVisible);\n        },\n        child: GestureDetector(\n          onTap: () => _dismissKeyboard(context),\n          behavior: HitTestBehavior.translucent,\n          child: Padding(\n            padding: const EdgeInsets.symmetric(horizontal: 36),\n            child: Center(\n              child: Column(\n                mainAxisAlignment: MainAxisAlignment.center,\n                children: [\n                  Padding(\n                    padding: const EdgeInsets.symmetric(vertical: 12.0),\n                    child: Text(\n                      'Keyboard Visible: $_isKeyboardVisible',\n                      style: const TextStyle(fontSize: 24.0),\n                    ),\n                  ),\n                  TextField(\n                    style: const TextStyle(fontSize: 20),\n                    controller: _controller,\n                  ),\n                ],\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  void _dismissKeyboard(BuildContext context) {\n    final currentFocus = FocusScope.of(context);\n\n    if (!currentFocus.hasPrimaryFocus && currentFocus.hasFocus) {\n      FocusManager.instance.primaryFocus?.unfocus();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/main.dart",
    "content": "import 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui_web.dart';\nimport 'package:flutter/material.dart';\n\nimport 'home/home_screen.dart';\n\nvoid main() {\n  runApp(const ExampleApp());\n}\n\nclass ExampleApp extends StatelessWidget {\n  const ExampleApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return const MaterialApp(\n      builder: overlayManagerBuilder,\n      title: \"Flowy Infra Title\",\n      home: HomeScreen(),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart",
    "content": "import 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui_web.dart';\nimport 'package:flutter/material.dart';\nimport 'package:provider/provider.dart';\n\nimport '../home/demo_item.dart';\n\nclass OverlayItem extends DemoItem {\n  @override\n  String buildTitle() => 'Overlay';\n\n  @override\n  void handleTap(BuildContext context) {\n    Navigator.of(context).push(\n      MaterialPageRoute(\n        builder: (context) {\n          return const OverlayScreen();\n        },\n      ),\n    );\n  }\n}\n\nclass OverlayDemoConfiguration extends ChangeNotifier {\n  OverlayDemoConfiguration(this._anchorDirection, this._overlapBehaviour);\n\n  AnchorDirection _anchorDirection;\n\n  AnchorDirection get anchorDirection => _anchorDirection;\n\n  set anchorDirection(AnchorDirection value) {\n    _anchorDirection = value;\n    notifyListeners();\n  }\n\n  OverlapBehaviour _overlapBehaviour;\n\n  OverlapBehaviour get overlapBehaviour => _overlapBehaviour;\n\n  set overlapBehaviour(OverlapBehaviour value) {\n    _overlapBehaviour = value;\n    notifyListeners();\n  }\n}\n\nclass OverlayScreen extends StatelessWidget {\n  const OverlayScreen({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('Overlay Demo'),\n      ),\n      body: ChangeNotifierProvider(\n        create: (context) => OverlayDemoConfiguration(\n            AnchorDirection.rightWithTopAligned, OverlapBehaviour.stretch),\n        child: Builder(builder: (providerContext) {\n          return Center(\n            child: ConstrainedBox(\n              constraints: const BoxConstraints.tightFor(width: 500),\n              child: Column(\n                crossAxisAlignment: CrossAxisAlignment.center,\n                children: [\n                  const SizedBox(height: 48.0),\n                  ElevatedButton(\n                    onPressed: () {\n                      final windowSize = MediaQuery.of(context).size;\n                      FlowyOverlay.of(context).insertCustom(\n                        widget: Positioned(\n                          left: windowSize.width / 2.0 - 100,\n                          top: 200,\n                          child: SizedBox(\n                            width: 200,\n                            height: 100,\n                            child: Card(\n                              color: Colors.green[200],\n                              child: GestureDetector(\n                                // ignore: avoid_print\n                                onTapDown: (_) => print('Hello Flutter'),\n                                child:\n                                    const Center(child: FlutterLogo(size: 100)),\n                              ),\n                            ),\n                          ),\n                        ),\n                        identifier: 'overlay_flutter_logo',\n                        delegate: null,\n                      );\n                    },\n                    child: const Text('Show Overlay'),\n                  ),\n                  const SizedBox(height: 24.0),\n                  DropdownButton<AnchorDirection>(\n                    value: providerContext\n                        .watch<OverlayDemoConfiguration>()\n                        .anchorDirection,\n                    onChanged: (AnchorDirection? newValue) {\n                      if (newValue != null) {\n                        providerContext\n                            .read<OverlayDemoConfiguration>()\n                            .anchorDirection = newValue;\n                      }\n                    },\n                    items:\n                        AnchorDirection.values.map((AnchorDirection classType) {\n                      return DropdownMenuItem<AnchorDirection>(\n                          value: classType, child: Text(classType.toString()));\n                    }).toList(),\n                  ),\n                  const SizedBox(height: 24.0),\n                  DropdownButton<OverlapBehaviour>(\n                    value: providerContext\n                        .watch<OverlayDemoConfiguration>()\n                        .overlapBehaviour,\n                    onChanged: (OverlapBehaviour? newValue) {\n                      if (newValue != null) {\n                        providerContext\n                            .read<OverlayDemoConfiguration>()\n                            .overlapBehaviour = newValue;\n                      }\n                    },\n                    items: OverlapBehaviour.values\n                        .map((OverlapBehaviour classType) {\n                      return DropdownMenuItem<OverlapBehaviour>(\n                          value: classType, child: Text(classType.toString()));\n                    }).toList(),\n                  ),\n                  const SizedBox(height: 24.0),\n                  Builder(builder: (buttonContext) {\n                    return SizedBox(\n                      height: 100,\n                      child: ElevatedButton(\n                        onPressed: () {\n                          FlowyOverlay.of(context).insertWithAnchor(\n                            widget: SizedBox(\n                              width: 300,\n                              height: 50,\n                              child: Card(\n                                color: Colors.grey[200],\n                                child: GestureDetector(\n                                  // ignore: avoid_print\n                                  onTapDown: (_) => print('Hello Flutter'),\n                                  child: const Center(\n                                      child: FlutterLogo(size: 50)),\n                                ),\n                              ),\n                            ),\n                            identifier: 'overlay_anchored_card',\n                            delegate: null,\n                            anchorContext: buttonContext,\n                            anchorDirection: providerContext\n                                .read<OverlayDemoConfiguration>()\n                                .anchorDirection,\n                            overlapBehaviour: providerContext\n                                .read<OverlayDemoConfiguration>()\n                                .overlapBehaviour,\n                          );\n                        },\n                        child: const Text('Show Anchored Overlay'),\n                      ),\n                    );\n                  }),\n                  const SizedBox(height: 24.0),\n                  ElevatedButton(\n                    onPressed: () {\n                      final windowSize = MediaQuery.of(context).size;\n                      FlowyOverlay.of(context).insertWithRect(\n                        widget: SizedBox(\n                          width: 200,\n                          height: 100,\n                          child: Card(\n                            color: Colors.orange[200],\n                            child: GestureDetector(\n                              // ignore: avoid_print\n                              onTapDown: (_) => debugPrint('Hello Flutter'),\n                              child:\n                                  const Center(child: FlutterLogo(size: 100)),\n                            ),\n                          ),\n                        ),\n                        identifier: 'overlay_positioned_card',\n                        delegate: null,\n                        anchorPosition: Offset(0, windowSize.height - 200),\n                        anchorSize: Size.zero,\n                        anchorDirection: providerContext\n                            .read<OverlayDemoConfiguration>()\n                            .anchorDirection,\n                        overlapBehaviour: providerContext\n                            .read<OverlayDemoConfiguration>()\n                            .overlapBehaviour,\n                      );\n                    },\n                    child: const Text('Show Positioned Overlay'),\n                  ),\n                  const SizedBox(height: 24.0),\n                  Builder(builder: (buttonContext) {\n                    return ElevatedButton(\n                      onPressed: () {\n                        ListOverlay.showWithAnchor(\n                          context,\n                          itemBuilder: (_, index) => Card(\n                            margin: const EdgeInsets.symmetric(\n                                vertical: 8.0, horizontal: 12.0),\n                            elevation: 0,\n                            child: Text(\n                              'Option $index',\n                              style: const TextStyle(\n                                  fontSize: 20.0, color: Colors.black),\n                            ),\n                          ),\n                          itemCount: 10,\n                          identifier: 'overlay_list_menu',\n                          anchorContext: buttonContext,\n                          anchorDirection: providerContext\n                              .read<OverlayDemoConfiguration>()\n                              .anchorDirection,\n                          overlapBehaviour: providerContext\n                              .read<OverlayDemoConfiguration>()\n                              .overlapBehaviour,\n                          constraints:\n                              BoxConstraints.tight(const Size(200, 200)),\n                        );\n                      },\n                      child: const Text('Show List Overlay'),\n                    );\n                  }),\n                  const SizedBox(height: 24.0),\n                  Builder(builder: (buttonContext) {\n                    return ElevatedButton(\n                      onPressed: () {\n                        OptionOverlay.showWithAnchor(\n                          context,\n                          items: <String>[\n                            'Alpha',\n                            'Beta',\n                            'Charlie',\n                            'Delta',\n                            'Echo',\n                            'Foxtrot',\n                            'Golf',\n                            'Hotel'\n                          ],\n                          onHover: (value, index) => debugPrint(\n                              'Did hover option $index, value $value'),\n                          onTap: (value, index) =>\n                              debugPrint('Did tap option $index, value $value'),\n                          identifier: 'overlay_options',\n                          anchorContext: buttonContext,\n                          anchorDirection: providerContext\n                              .read<OverlayDemoConfiguration>()\n                              .anchorDirection,\n                          overlapBehaviour: providerContext\n                              .read<OverlayDemoConfiguration>()\n                              .overlapBehaviour,\n                        );\n                      },\n                      child: const Text('Show Options Overlay'),\n                    );\n                  }),\n                ],\n              ),\n            ),\n          );\n        }),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/linux/.gitignore",
    "content": "flutter/ephemeral\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/linux/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.10)\nproject(runner LANGUAGES CXX)\n\nset(BINARY_NAME \"flowy_infra_ui_example\")\nset(APPLICATION_ID \"com.example.flowy_infra_ui\")\n\ncmake_policy(SET CMP0063 NEW)\n\nset(CMAKE_INSTALL_RPATH \"$ORIGIN/lib\")\n\n# Root filesystem for cross-building.\nif(FLUTTER_TARGET_PLATFORM_SYSROOT)\n  set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\n  set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\nendif()\n\n# Configure build options.\nif(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n  set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n    STRING \"Flutter build mode\" FORCE)\n  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n    \"Debug\" \"Profile\" \"Release\")\nendif()\n\n# Compilation settings that should be applied to most targets.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_14)\n  target_compile_options(${TARGET} PRIVATE -Wall -Werror)\n  target_compile_options(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:-O3>\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:NDEBUG>\")\nendfunction()\n\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\n\n# Flutter library and tool build rules.\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# System-level dependencies.\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)\n\nadd_definitions(-DAPPLICATION_ID=\"${APPLICATION_ID}\")\n\n# Application build\nadd_executable(${BINARY_NAME}\n  \"main.cc\"\n  \"my_application.cc\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n)\napply_standard_settings(${BINARY_NAME})\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter)\ntarget_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n# Only the install-generated bundle's copy of the executable will launch\n# correctly, since the resources must in the right relative locations. To avoid\n# people trying to run the unbundled copy, put it in a subdirectory instead of\n# the default top-level location.\nset_target_properties(${BINARY_NAME}\n  PROPERTIES\n  RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/intermediates_do_not_run\"\n)\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n\n# === Installation ===\n# By default, \"installing\" just makes a relocatable bundle in the build\n# directory.\nset(BUILD_BUNDLE_DIR \"${PROJECT_BINARY_DIR}/bundle\")\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\n# Start with a clean build bundle directory every time.\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${BUILD_BUNDLE_DIR}/\\\")\n  \" COMPONENT Runtime)\n\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}/lib\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nif(PLUGIN_BUNDLED_LIBRARIES)\n  install(FILES \"${PLUGIN_BUNDLED_LIBRARIES}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n\n# Fully re-copy the assets directory on each build to avoid having stale files\n# from a previous install.\nset(FLUTTER_ASSET_DIR_NAME \"flutter_assets\")\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\\\")\n  \" COMPONENT Runtime)\ninstall(DIRECTORY \"${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}\"\n  DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\" COMPONENT Runtime)\n\n# Install the AOT library on non-Debug builds only.\nif(NOT CMAKE_BUILD_TYPE MATCHES \"Debug\")\n  install(FILES \"${AOT_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/linux/flutter/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.10)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\n\n# Serves the same purpose as list(TRANSFORM ... PREPEND ...),\n# which isn't available in 3.10.\nfunction(list_prepend LIST_NAME PREFIX)\n    set(NEW_LIST \"\")\n    foreach(element ${${LIST_NAME}})\n        list(APPEND NEW_LIST \"${PREFIX}${element}\")\n    endforeach(element)\n    set(${LIST_NAME} \"${NEW_LIST}\" PARENT_SCOPE)\nendfunction()\n\n# === Flutter Library ===\n# System-level dependencies.\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)\npkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)\npkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)\n\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/libflutter_linux_gtk.so\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/lib/libapp.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"fl_basic_message_channel.h\"\n  \"fl_binary_codec.h\"\n  \"fl_binary_messenger.h\"\n  \"fl_dart_project.h\"\n  \"fl_engine.h\"\n  \"fl_json_message_codec.h\"\n  \"fl_json_method_codec.h\"\n  \"fl_message_codec.h\"\n  \"fl_method_call.h\"\n  \"fl_method_channel.h\"\n  \"fl_method_codec.h\"\n  \"fl_method_response.h\"\n  \"fl_plugin_registrar.h\"\n  \"fl_plugin_registry.h\"\n  \"fl_standard_message_codec.h\"\n  \"fl_standard_method_codec.h\"\n  \"fl_string_codec.h\"\n  \"fl_value.h\"\n  \"fl_view.h\"\n  \"flutter_linux.h\"\n)\nlist_prepend(FLUTTER_LIBRARY_HEADERS \"${EPHEMERAL_DIR}/flutter_linux/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}\")\ntarget_link_libraries(flutter INTERFACE\n  PkgConfig::GTK\n  PkgConfig::GLIB\n  PkgConfig::GIO\n)\nadd_dependencies(flutter flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CMAKE_CURRENT_BINARY_DIR}/_phony_\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh\"\n      ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n)\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/linux/main.cc",
    "content": "#include \"my_application.h\"\n\nint main(int argc, char** argv) {\n  g_autoptr(MyApplication) app = my_application_new();\n  return g_application_run(G_APPLICATION(app), argc, argv);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/linux/my_application.cc",
    "content": "#include \"my_application.h\"\n\n#include <flutter_linux/flutter_linux.h>\n#ifdef GDK_WINDOWING_X11\n#include <gdk/gdkx.h>\n#endif\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nstruct _MyApplication {\n  GtkApplication parent_instance;\n  char** dart_entrypoint_arguments;\n};\n\nG_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)\n\n// Implements GApplication::activate.\nstatic void my_application_activate(GApplication* application) {\n  MyApplication* self = MY_APPLICATION(application);\n  GtkWindow* window =\n      GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));\n\n  // Use a header bar when running in GNOME as this is the common style used\n  // by applications and is the setup most users will be using (e.g. Ubuntu\n  // desktop).\n  // If running on X and not using GNOME then just use a traditional title bar\n  // in case the window manager does more exotic layout, e.g. tiling.\n  // If running on Wayland assume the header bar will work (may need changing\n  // if future cases occur).\n  gboolean use_header_bar = TRUE;\n#ifdef GDK_WINDOWING_X11\n  GdkScreen* screen = gtk_window_get_screen(window);\n  if (GDK_IS_X11_SCREEN(screen)) {\n    const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);\n    if (g_strcmp0(wm_name, \"GNOME Shell\") != 0) {\n      use_header_bar = FALSE;\n    }\n  }\n#endif\n  if (use_header_bar) {\n    GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());\n    gtk_widget_show(GTK_WIDGET(header_bar));\n    gtk_header_bar_set_title(header_bar, \"flowy_infra_ui_example\");\n    gtk_header_bar_set_show_close_button(header_bar, TRUE);\n    gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));\n  } else {\n    gtk_window_set_title(window, \"flowy_infra_ui_example\");\n  }\n\n  gtk_window_set_default_size(window, 1280, 720);\n  gtk_widget_show(GTK_WIDGET(window));\n\n  g_autoptr(FlDartProject) project = fl_dart_project_new();\n  fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);\n\n  FlView* view = fl_view_new(project);\n  gtk_widget_show(GTK_WIDGET(view));\n  gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));\n\n  fl_register_plugins(FL_PLUGIN_REGISTRY(view));\n\n  gtk_widget_grab_focus(GTK_WIDGET(view));\n}\n\n// Implements GApplication::local_command_line.\nstatic gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {\n  MyApplication* self = MY_APPLICATION(application);\n  // Strip out the first argument as it is the binary name.\n  self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);\n\n  g_autoptr(GError) error = nullptr;\n  if (!g_application_register(application, nullptr, &error)) {\n     g_warning(\"Failed to register: %s\", error->message);\n     *exit_status = 1;\n     return TRUE;\n  }\n\n  g_application_activate(application);\n  *exit_status = 0;\n\n  return TRUE;\n}\n\n// Implements GObject::dispose.\nstatic void my_application_dispose(GObject* object) {\n  MyApplication* self = MY_APPLICATION(object);\n  g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);\n  G_OBJECT_CLASS(my_application_parent_class)->dispose(object);\n}\n\nstatic void my_application_class_init(MyApplicationClass* klass) {\n  G_APPLICATION_CLASS(klass)->activate = my_application_activate;\n  G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;\n  G_OBJECT_CLASS(klass)->dispose = my_application_dispose;\n}\n\nstatic void my_application_init(MyApplication* self) {}\n\nMyApplication* my_application_new() {\n  return MY_APPLICATION(g_object_new(my_application_get_type(),\n                                     \"application-id\", APPLICATION_ID,\n                                     \"flags\", G_APPLICATION_NON_UNIQUE,\n                                     nullptr));\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/linux/my_application.h",
    "content": "#ifndef FLUTTER_MY_APPLICATION_H_\n#define FLUTTER_MY_APPLICATION_H_\n\n#include <gtk/gtk.h>\n\nG_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,\n                     GtkApplication)\n\n/**\n * my_application_new:\n *\n * Creates a new Flutter-based application.\n *\n * Returns: a new #MyApplication.\n */\nMyApplication* my_application_new();\n\n#endif  // FLUTTER_MY_APPLICATION_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/.gitignore",
    "content": "# Flutter-related\n**/Flutter/ephemeral/\n**/Pods/\n\n# Xcode-related\n**/xcuserdata/\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Flutter/Flutter-Debug.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"\n#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Flutter/Flutter-Release.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"\n#include \"ephemeral/Flutter-Generated.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Podfile",
    "content": "platform :osx, '10.13'\n\n# CocoaPods analytics sends network stats synchronously affecting flutter build latency.\nENV['COCOAPODS_DISABLE_STATS'] = 'true'\n\nproject 'Runner', {\n  'Debug' => :debug,\n  'Profile' => :release,\n  'Release' => :release,\n}\n\ndef flutter_root\n  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)\n  unless File.exist?(generated_xcode_build_settings_path)\n    raise \"#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \\\"flutter pub get\\\" is executed first\"\n  end\n\n  File.foreach(generated_xcode_build_settings_path) do |line|\n    matches = line.match(/FLUTTER_ROOT\\=(.*)/)\n    return matches[1].strip if matches\n  end\n  raise \"FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \\\"flutter pub get\\\"\"\nend\n\nrequire File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)\n\nflutter_macos_podfile_setup\n\ntarget 'Runner' do\n  use_frameworks!\n  use_modular_headers!\n\n  flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))\nend\n\npost_install do |installer|\n  installer.pods_project.targets.each do |target|\n    flutter_additional_macos_build_settings(target)\n  end\nend\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Runner/AppDelegate.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\n@NSApplicationMain\nclass AppDelegate: FlutterAppDelegate {\n  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {\n    return true\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_16.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_32.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"32x32\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_32.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"32x32\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_64.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"128x128\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_128.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"128x128\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_256.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"256x256\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_256.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"256x256\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_512.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"512x512\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_512.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"512x512\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_1024.png\",\n      \"scale\" : \"2x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Runner/Base.lproj/MainMenu.xib",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion=\"14490.70\" targetRuntime=\"MacOSX.Cocoa\" propertyAccessControl=\"none\" useAutolayout=\"YES\" customObjectInstantitationMethod=\"direct\">\n    <dependencies>\n        <deployment identifier=\"macosx\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.CocoaPlugin\" version=\"14490.70\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <objects>\n        <customObject id=\"-2\" userLabel=\"File's Owner\" customClass=\"NSApplication\">\n            <connections>\n                <outlet property=\"delegate\" destination=\"Voe-Tx-rLC\" id=\"GzC-gU-4Uq\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"-1\" userLabel=\"First Responder\" customClass=\"FirstResponder\"/>\n        <customObject id=\"-3\" userLabel=\"Application\" customClass=\"NSObject\"/>\n        <customObject id=\"Voe-Tx-rLC\" customClass=\"AppDelegate\" customModule=\"Runner\" customModuleProvider=\"target\">\n            <connections>\n                <outlet property=\"applicationMenu\" destination=\"uQy-DD-JDr\" id=\"XBo-yE-nKs\"/>\n                <outlet property=\"mainFlutterWindow\" destination=\"QvC-M9-y7g\" id=\"gIp-Ho-8D9\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"YLy-65-1bz\" customClass=\"NSFontManager\"/>\n        <menu title=\"Main Menu\" systemMenu=\"main\" id=\"AYu-sK-qS6\">\n            <items>\n                <menuItem title=\"APP_NAME\" id=\"1Xt-HY-uBw\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"APP_NAME\" systemMenu=\"apple\" id=\"uQy-DD-JDr\">\n                        <items>\n                            <menuItem title=\"About APP_NAME\" id=\"5kV-Vb-QxS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"orderFrontStandardAboutPanel:\" target=\"-1\" id=\"Exp-CZ-Vem\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"VOq-y0-SEH\"/>\n                            <menuItem title=\"Preferences…\" keyEquivalent=\",\" id=\"BOF-NM-1cW\"/>\n                            <menuItem isSeparatorItem=\"YES\" id=\"wFC-TO-SCJ\"/>\n                            <menuItem title=\"Services\" id=\"NMo-om-nkz\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Services\" systemMenu=\"services\" id=\"hz9-B4-Xy5\"/>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"4je-JR-u6R\"/>\n                            <menuItem title=\"Hide APP_NAME\" keyEquivalent=\"h\" id=\"Olw-nP-bQN\">\n                                <connections>\n                                    <action selector=\"hide:\" target=\"-1\" id=\"PnN-Uc-m68\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Hide Others\" keyEquivalent=\"h\" id=\"Vdr-fp-XzO\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"hideOtherApplications:\" target=\"-1\" id=\"VT4-aY-XCT\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Show All\" id=\"Kd2-mp-pUS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"unhideAllApplications:\" target=\"-1\" id=\"Dhg-Le-xox\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"kCx-OE-vgT\"/>\n                            <menuItem title=\"Quit APP_NAME\" keyEquivalent=\"q\" id=\"4sb-4s-VLi\">\n                                <connections>\n                                    <action selector=\"terminate:\" target=\"-1\" id=\"Te7-pn-YzF\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Edit\" id=\"5QF-Oa-p0T\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Edit\" id=\"W48-6f-4Dl\">\n                        <items>\n                            <menuItem title=\"Undo\" keyEquivalent=\"z\" id=\"dRJ-4n-Yzg\">\n                                <connections>\n                                    <action selector=\"undo:\" target=\"-1\" id=\"M6e-cu-g7V\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Redo\" keyEquivalent=\"Z\" id=\"6dh-zS-Vam\">\n                                <connections>\n                                    <action selector=\"redo:\" target=\"-1\" id=\"oIA-Rs-6OD\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"WRV-NI-Exz\"/>\n                            <menuItem title=\"Cut\" keyEquivalent=\"x\" id=\"uRl-iY-unG\">\n                                <connections>\n                                    <action selector=\"cut:\" target=\"-1\" id=\"YJe-68-I9s\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Copy\" keyEquivalent=\"c\" id=\"x3v-GG-iWU\">\n                                <connections>\n                                    <action selector=\"copy:\" target=\"-1\" id=\"G1f-GL-Joy\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Paste\" keyEquivalent=\"v\" id=\"gVA-U4-sdL\">\n                                <connections>\n                                    <action selector=\"paste:\" target=\"-1\" id=\"UvS-8e-Qdg\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Paste and Match Style\" keyEquivalent=\"V\" id=\"WeT-3V-zwk\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"pasteAsPlainText:\" target=\"-1\" id=\"cEh-KX-wJQ\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Delete\" id=\"pa3-QI-u2k\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"delete:\" target=\"-1\" id=\"0Mk-Ml-PaM\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Select All\" keyEquivalent=\"a\" id=\"Ruw-6m-B2m\">\n                                <connections>\n                                    <action selector=\"selectAll:\" target=\"-1\" id=\"VNm-Mi-diN\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"uyl-h8-XO2\"/>\n                            <menuItem title=\"Find\" id=\"4EN-yA-p0u\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Find\" id=\"1b7-l0-nxx\">\n                                    <items>\n                                        <menuItem title=\"Find…\" tag=\"1\" keyEquivalent=\"f\" id=\"Xz5-n4-O0W\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"cD7-Qs-BN4\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find and Replace…\" tag=\"12\" keyEquivalent=\"f\" id=\"YEy-JH-Tfz\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"WD3-Gg-5AJ\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find Next\" tag=\"2\" keyEquivalent=\"g\" id=\"q09-fT-Sye\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"NDo-RZ-v9R\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find Previous\" tag=\"3\" keyEquivalent=\"G\" id=\"OwM-mh-QMV\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"HOh-sY-3ay\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Use Selection for Find\" tag=\"7\" keyEquivalent=\"e\" id=\"buJ-ug-pKt\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"U76-nv-p5D\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Jump to Selection\" keyEquivalent=\"j\" id=\"S0p-oC-mLd\">\n                                            <connections>\n                                                <action selector=\"centerSelectionInVisibleArea:\" target=\"-1\" id=\"IOG-6D-g5B\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Spelling and Grammar\" id=\"Dv1-io-Yv7\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Spelling\" id=\"3IN-sU-3Bg\">\n                                    <items>\n                                        <menuItem title=\"Show Spelling and Grammar\" keyEquivalent=\":\" id=\"HFo-cy-zxI\">\n                                            <connections>\n                                                <action selector=\"showGuessPanel:\" target=\"-1\" id=\"vFj-Ks-hy3\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Check Document Now\" keyEquivalent=\";\" id=\"hz2-CU-CR7\">\n                                            <connections>\n                                                <action selector=\"checkSpelling:\" target=\"-1\" id=\"fz7-VC-reM\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"bNw-od-mp5\"/>\n                                        <menuItem title=\"Check Spelling While Typing\" id=\"rbD-Rh-wIN\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleContinuousSpellChecking:\" target=\"-1\" id=\"7w6-Qz-0kB\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Check Grammar With Spelling\" id=\"mK6-2p-4JG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleGrammarChecking:\" target=\"-1\" id=\"muD-Qn-j4w\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Correct Spelling Automatically\" id=\"78Y-hA-62v\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticSpellingCorrection:\" target=\"-1\" id=\"2lM-Qi-WAP\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Substitutions\" id=\"9ic-FL-obx\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Substitutions\" id=\"FeM-D8-WVr\">\n                                    <items>\n                                        <menuItem title=\"Show Substitutions\" id=\"z6F-FW-3nz\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"orderFrontSubstitutionsPanel:\" target=\"-1\" id=\"oku-mr-iSq\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"gPx-C9-uUO\"/>\n                                        <menuItem title=\"Smart Copy/Paste\" id=\"9yt-4B-nSM\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleSmartInsertDelete:\" target=\"-1\" id=\"3IJ-Se-DZD\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Quotes\" id=\"hQb-2v-fYv\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticQuoteSubstitution:\" target=\"-1\" id=\"ptq-xd-QOA\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Dashes\" id=\"rgM-f4-ycn\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticDashSubstitution:\" target=\"-1\" id=\"oCt-pO-9gS\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Links\" id=\"cwL-P1-jid\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticLinkDetection:\" target=\"-1\" id=\"Gip-E3-Fov\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Data Detectors\" id=\"tRr-pd-1PS\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticDataDetection:\" target=\"-1\" id=\"R1I-Nq-Kbl\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Text Replacement\" id=\"HFQ-gK-NFA\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticTextReplacement:\" target=\"-1\" id=\"DvP-Fe-Py6\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Transformations\" id=\"2oI-Rn-ZJC\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Transformations\" id=\"c8a-y6-VQd\">\n                                    <items>\n                                        <menuItem title=\"Make Upper Case\" id=\"vmV-6d-7jI\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"uppercaseWord:\" target=\"-1\" id=\"sPh-Tk-edu\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Make Lower Case\" id=\"d9M-CD-aMd\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"lowercaseWord:\" target=\"-1\" id=\"iUZ-b5-hil\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Capitalize\" id=\"UEZ-Bs-lqG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"capitalizeWord:\" target=\"-1\" id=\"26H-TL-nsh\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Speech\" id=\"xrE-MZ-jX0\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Speech\" id=\"3rS-ZA-NoH\">\n                                    <items>\n                                        <menuItem title=\"Start Speaking\" id=\"Ynk-f8-cLZ\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"startSpeaking:\" target=\"-1\" id=\"654-Ng-kyl\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Stop Speaking\" id=\"Oyz-dy-DGm\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"stopSpeaking:\" target=\"-1\" id=\"dX8-6p-jy9\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"View\" id=\"H8h-7b-M4v\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"View\" id=\"HyV-fh-RgO\">\n                        <items>\n                            <menuItem title=\"Enter Full Screen\" keyEquivalent=\"f\" id=\"4J7-dP-txa\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" control=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"toggleFullScreen:\" target=\"-1\" id=\"dU3-MA-1Rq\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Window\" id=\"aUF-d1-5bR\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Window\" systemMenu=\"window\" id=\"Td7-aD-5lo\">\n                        <items>\n                            <menuItem title=\"Minimize\" keyEquivalent=\"m\" id=\"OY7-WF-poV\">\n                                <connections>\n                                    <action selector=\"performMiniaturize:\" target=\"-1\" id=\"VwT-WD-YPe\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Zoom\" id=\"R4o-n2-Eq4\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"performZoom:\" target=\"-1\" id=\"DIl-cC-cCs\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"eu3-7i-yIM\"/>\n                            <menuItem title=\"Bring All to Front\" id=\"LE2-aR-0XJ\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"arrangeInFront:\" target=\"-1\" id=\"DRN-fu-gQh\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n            </items>\n            <point key=\"canvasLocation\" x=\"142\" y=\"-258\"/>\n        </menu>\n        <window title=\"APP_NAME\" allowsToolTipsWhenApplicationIsInactive=\"NO\" autorecalculatesKeyViewLoop=\"NO\" releasedWhenClosed=\"NO\" animationBehavior=\"default\" id=\"QvC-M9-y7g\" customClass=\"MainFlutterWindow\" customModule=\"Runner\" customModuleProvider=\"target\">\n            <windowStyleMask key=\"styleMask\" titled=\"YES\" closable=\"YES\" miniaturizable=\"YES\" resizable=\"YES\"/>\n            <rect key=\"contentRect\" x=\"335\" y=\"390\" width=\"800\" height=\"600\"/>\n            <rect key=\"screenRect\" x=\"0.0\" y=\"0.0\" width=\"2560\" height=\"1577\"/>\n            <view key=\"contentView\" wantsLayer=\"YES\" id=\"EiT-Mj-1SZ\">\n                <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"800\" height=\"600\"/>\n                <autoresizingMask key=\"autoresizingMask\"/>\n            </view>\n        </window>\n    </objects>\n</document>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Runner/Configs/AppInfo.xcconfig",
    "content": "// Application-level settings for the Runner target.\n//\n// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the\n// future. If not, the values below would default to using the project name when this becomes a\n// 'flutter create' template.\n\n// The application's name. By default this is also the title of the Flutter window.\nPRODUCT_NAME = flowy_infra_ui_example\n\n// The application's bundle identifier\nPRODUCT_BUNDLE_IDENTIFIER = com.example.flowyInfraUiExample\n\n// The copyright displayed in application information\nPRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Runner/Configs/Debug.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Debug.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Runner/Configs/Release.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Release.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Runner/Configs/Warnings.xcconfig",
    "content": "WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings\nGCC_WARN_UNDECLARED_SELECTOR = YES\nCLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES\nCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE\nCLANG_WARN__DUPLICATE_METHOD_MATCH = YES\nCLANG_WARN_PRAGMA_PACK = YES\nCLANG_WARN_STRICT_PROTOTYPES = YES\nCLANG_WARN_COMMA = YES\nGCC_WARN_STRICT_SELECTOR_MATCH = YES\nCLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES\nCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES\nGCC_WARN_SHADOW = YES\nCLANG_WARN_UNREACHABLE_CODE = YES\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Runner/DebugProfile.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n\t<key>com.apple.security.cs.allow-jit</key>\n\t<true/>\n\t<key>com.apple.security.network.server</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIconFile</key>\n\t<string></string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSMinimumSystemVersion</key>\n\t<string>$(MACOSX_DEPLOYMENT_TARGET)</string>\n\t<key>NSHumanReadableCopyright</key>\n\t<string>$(PRODUCT_COPYRIGHT)</string>\n\t<key>NSMainNibFile</key>\n\t<string>MainMenu</string>\n\t<key>NSPrincipalClass</key>\n\t<string>NSApplication</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Runner/MainFlutterWindow.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\nclass MainFlutterWindow: NSWindow {\n  override func awakeFromNib() {\n    let flutterViewController = FlutterViewController.init()\n    let windowFrame = self.frame\n    self.contentViewController = flutterViewController\n    self.setFrame(windowFrame, display: true)\n\n    RegisterGeneratedPlugins(registry: flutterViewController)\n\n    super.awakeFromNib()\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Runner/Release.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 51;\n\tobjects = {\n\n/* Begin PBXAggregateTarget section */\n\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {\n\t\t\tisa = PBXAggregateTarget;\n\t\t\tbuildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t33CC111E2044C6BF0003C045 /* ShellScript */,\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = \"Flutter Assemble\";\n\t\t\tproductName = FLX;\n\t\t};\n/* End PBXAggregateTarget section */\n\n/* Begin PBXBuildFile section */\n\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };\n\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };\n\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };\n\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };\n\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };\n\t\t7912A075158F80106DD95645 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0464B205D770E7A68F28B6D /* Pods_Runner.framework */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 33CC10E52044A3C60003C045 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 33CC111A2044C6BA0003C045;\n\t\t\tremoteInfo = FLX;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t33CC110E2044A8840003C045 /* Bundle Framework */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Bundle Framework\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t2D5900421C18B1A15A65A9EC /* 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 = \"<group>\"; };\n\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = \"<group>\"; };\n\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = \"<group>\"; };\n\t\t33CC10ED2044A3C60003C045 /* flowy_infra_ui_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flowy_infra_ui_example.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = \"<group>\"; };\n\t\t33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = \"<group>\"; };\n\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = \"<group>\"; };\n\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = \"Flutter-Generated.xcconfig\"; path = \"ephemeral/Flutter-Generated.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = \"<group>\"; };\n\t\t33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = \"<group>\"; };\n\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\t97A4E031C17F0C7BEEE33734 /* 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 = \"<group>\"; };\n\t\tAB3F9417FEFE6929B49F80FE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.profile.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig\"; sourceTree = \"<group>\"; };\n\t\tC0464B205D770E7A68F28B6D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t33CC10EA2044A3C60003C045 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t7912A075158F80106DD95645 /* Pods_Runner.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t33BA886A226E78AF003329D5 /* Configs */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */,\n\t\t\t);\n\t\t\tpath = Configs;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10E42044A3C60003C045 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33FAB671232836740065AC1E /* Runner */,\n\t\t\t\t33CEB47122A05771004F2AC0 /* Flutter */,\n\t\t\t\t33CC10EE2044A3C60003C045 /* Products */,\n\t\t\t\tD73912EC22F37F3D000D13A0 /* Frameworks */,\n\t\t\t\tA750A856115646FFDA1D1478 /* Pods */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10EE2044A3C60003C045 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10ED2044A3C60003C045 /* flowy_infra_ui_example.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC11242044D66E0003C045 /* Resources */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */,\n\t\t\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */,\n\t\t\t\t33CC10F72044A3C60003C045 /* Info.plist */,\n\t\t\t);\n\t\t\tname = Resources;\n\t\t\tpath = ..;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CEB47122A05771004F2AC0 /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,\n\t\t\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,\n\t\t\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,\n\t\t\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,\n\t\t\t);\n\t\t\tpath = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33FAB671232836740065AC1E /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */,\n\t\t\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,\n\t\t\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */,\n\t\t\t\t33E51914231749380026EE4D /* Release.entitlements */,\n\t\t\t\t33CC11242044D66E0003C045 /* Resources */,\n\t\t\t\t33BA886A226E78AF003329D5 /* Configs */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tA750A856115646FFDA1D1478 /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97A4E031C17F0C7BEEE33734 /* Pods-Runner.debug.xcconfig */,\n\t\t\t\t2D5900421C18B1A15A65A9EC /* Pods-Runner.release.xcconfig */,\n\t\t\t\tAB3F9417FEFE6929B49F80FE /* Pods-Runner.profile.xcconfig */,\n\t\t\t);\n\t\t\tname = Pods;\n\t\t\tpath = Pods;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD73912EC22F37F3D000D13A0 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tC0464B205D770E7A68F28B6D /* Pods_Runner.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t33CC10EC2044A3C60003C045 /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t2FBE5781370B1EF78F66CD14 /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t33CC10E92044A3C60003C045 /* Sources */,\n\t\t\t\t33CC10EA2044A3C60003C045 /* Frameworks */,\n\t\t\t\t33CC10EB2044A3C60003C045 /* Resources */,\n\t\t\t\t33CC110E2044A8840003C045 /* Bundle Framework */,\n\t\t\t\t3399D490228B24CF009A79C7 /* ShellScript */,\n\t\t\t\t196EE237C3BE7811FE50A841 /* [CP] Embed Pods Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 33CC10ED2044A3C60003C045 /* flowy_infra_ui_example.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t33CC10E52044A3C60003C045 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastSwiftUpdateCheck = 0920;\n\t\t\t\tLastUpgradeCheck = 0930;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t33CC10EC2044A3C60003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t\tProvisioningStyle = Automatic;\n\t\t\t\t\t\tSystemCapabilities = {\n\t\t\t\t\t\t\tcom.apple.Sandbox = {\n\t\t\t\t\t\t\t\tenabled = 1;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t\t33CC111A2044C6BA0003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tProvisioningStyle = Manual;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 33CC10E42044A3C60003C045;\n\t\t\tproductRefGroup = 33CC10EE2044A3C60003C045 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t33CC10EC2044A3C60003C045 /* Runner */,\n\t\t\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t33CC10EB2044A3C60003C045 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,\n\t\t\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t196EE237C3BE7811FE50A841 /* [CP] Embed Pods Frameworks */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist\",\n\t\t\t);\n\t\t\tname = \"[CP] Embed Pods Frameworks\";\n\t\t\toutputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t2FBE5781370B1EF78F66CD14 /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t3399D490228B24CF009A79C7 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"echo \\\"$PRODUCT_NAME.app\\\" > \\\"$PROJECT_DIR\\\"/Flutter/ephemeral/.app_filename && \\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh embed\\n\";\n\t\t};\n\t\t33CC111E2044C6BF0003C045 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterInputs.xcfilelist,\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\tFlutter/ephemeral/tripwire,\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterOutputs.xcfilelist,\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire\";\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t33CC10E92044A3C60003C045 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,\n\t\t\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,\n\t\t\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;\n\t\t\ttargetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F52044A3C60003C045 /* Base */,\n\t\t\t);\n\t\t\tname = MainMenu.xib;\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t338D0CE9231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.11;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEA231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEB231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t33CC10F92044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.11;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FA2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.11;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC10FC2044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FD2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC111C2044C6BA0003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC111D2044C6BA0003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10F92044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FA2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CE9231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10FC2044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FD2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CEA231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC111C2044C6BA0003C045 /* Debug */,\n\t\t\t\t33CC111D2044C6BA0003C045 /* Release */,\n\t\t\t\t338D0CEB231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 33CC10E52044A3C60003C045 /* Project object */;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1000\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n               BuildableName = \"flowy_infra_ui_example.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"flowy_infra_ui_example.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <AdditionalOptions>\n      </AdditionalOptions>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"flowy_infra_ui_example.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n      <AdditionalOptions>\n      </AdditionalOptions>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"flowy_infra_ui_example.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n   <FileRef\n      location = \"group:Pods/Pods.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/pubspec.yaml",
    "content": "name: flowy_infra_ui_example\ndescription: Demonstrates how to use the flowy_infra_ui plugin.\n\npublish_to: 'none' # Remove this line if you wish to publish to pub.dev\n\nenvironment:\n  flutter: \">=3.22.0\"\n  sdk: \">=3.1.5 <4.0.0\"\n\ndependencies:\n  flutter:\n    sdk: flutter\n\n  flowy_infra_ui:\n    path: ../\n\n  cupertino_icons: ^1.0.2\n  provider:\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n\n  flutter_lints: ^3.0.1\n\nflutter:\n  uses-material-design: true\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/test/widget_test.dart",
    "content": "// This is a basic Flutter widget test.\n//\n// To perform an interaction with a widget in your test, use the WidgetTester\n// utility that Flutter provides. For example, you can send tap and scroll\n// gestures. You can also use WidgetTester to find child widgets in the widget\n// tree, read text, and verify that the values of widget properties are correct.\n\nvoid main() {}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/web/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <!--\n    If you are serving your web app in a path other than the root, change the\n    href value below to reflect the base path you are serving from.\n\n    The path provided below has to start and end with a slash \"/\" in order for\n    it to work correctly.\n\n    For more details:\n    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base\n\n    This is a placeholder for base href that will be replaced by the value of\n    the `--base-href` argument provided to `flutter build`.\n  -->\n  <base href=\"$FLUTTER_BASE_HREF\">\n\n  <meta charset=\"UTF-8\">\n  <meta content=\"IE=Edge\" http-equiv=\"X-UA-Compatible\">\n  <meta name=\"description\" content=\"Demonstrates how to use the flowy_infra_ui plugin.\">\n\n  <!-- iOS meta tags & icons -->\n  <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n  <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\">\n  <meta name=\"apple-mobile-web-app-title\" content=\"flowy_infra_ui_example\">\n  <link rel=\"apple-touch-icon\" href=\"icons/Icon-192.png\">\n\n  <title>flowy_infra_ui_example</title>\n  <link rel=\"manifest\" href=\"manifest.json\">\n</head>\n<body>\n  <!-- This script installs service_worker.js to provide PWA functionality to\n       application. For more information, see:\n       https://developers.google.com/web/fundamentals/primers/service-workers -->\n  <script>\n    var serviceWorkerVersion = null;\n    var scriptLoaded = false;\n    function loadMainDartJs() {\n      if (scriptLoaded) {\n        return;\n      }\n      scriptLoaded = true;\n      var scriptTag = document.createElement('script');\n      scriptTag.src = 'main.dart.js';\n      scriptTag.type = 'application/javascript';\n      document.body.append(scriptTag);\n    }\n\n    if ('serviceWorker' in navigator) {\n      // Service workers are supported. Use them.\n      window.addEventListener('load', function () {\n        // Wait for registration to finish before dropping the <script> tag.\n        // Otherwise, the browser will load the script multiple times,\n        // potentially different versions.\n        var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;\n        navigator.serviceWorker.register(serviceWorkerUrl)\n          .then((reg) => {\n            function waitForActivation(serviceWorker) {\n              serviceWorker.addEventListener('statechange', () => {\n                if (serviceWorker.state == 'activated') {\n                  console.log('Installed new service worker.');\n                  loadMainDartJs();\n                }\n              });\n            }\n            if (!reg.active && (reg.installing || reg.waiting)) {\n              // No active web worker and we have installed or are installing\n              // one for the first time. Simply wait for it to activate.\n              waitForActivation(reg.installing ?? reg.waiting);\n            } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {\n              // When the app updates the serviceWorkerVersion changes, so we\n              // need to ask the service worker to update.\n              console.log('New service worker available.');\n              reg.update();\n              waitForActivation(reg.installing);\n            } else {\n              // Existing service worker is still good.\n              console.log('Loading app from service worker.');\n              loadMainDartJs();\n            }\n          });\n\n        // If service worker doesn't succeed in a reasonable amount of time,\n        // fallback to plaint <script> tag.\n        setTimeout(() => {\n          if (!scriptLoaded) {\n            console.warn(\n              'Failed to load app from service worker. Falling back to plain <script> tag.',\n            );\n            loadMainDartJs();\n          }\n        }, 4000);\n      });\n    } else {\n      // Service workers not supported. Just drop the <script> tag.\n      loadMainDartJs();\n    }\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/web/manifest.json",
    "content": "{\n    \"name\": \"flowy_infra_ui_example\",\n    \"short_name\": \"flowy_infra_ui_example\",\n    \"start_url\": \".\",\n    \"display\": \"standalone\",\n    \"background_color\": \"#0175C2\",\n    \"theme_color\": \"#0175C2\",\n    \"description\": \"Demonstrates how to use the flowy_infra_ui plugin.\",\n    \"orientation\": \"portrait-primary\",\n    \"prefer_related_applications\": false,\n    \"icons\": [\n        {\n            \"src\": \"icons/Icon-192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"icons/Icon-512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\"\n        }\n    ]\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/windows/.gitignore",
    "content": "flutter/ephemeral/\n\n# Visual Studio user-specific files.\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# Visual Studio build-related files.\nx64/\nx86/\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/windows/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.15)\nproject(flowy_infra_ui_example LANGUAGES CXX)\n\nset(BINARY_NAME \"flowy_infra_ui_example\")\n\ncmake_policy(SET CMP0063 NEW)\n\nset(CMAKE_INSTALL_RPATH \"$ORIGIN/lib\")\n\n# Configure build options.\nget_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)\nif(IS_MULTICONFIG)\n  set(CMAKE_CONFIGURATION_TYPES \"Debug;Profile;Release\"\n    CACHE STRING \"\" FORCE)\nelse()\n  if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n    set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n      STRING \"Flutter build mode\" FORCE)\n    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n      \"Debug\" \"Profile\" \"Release\")\n  endif()\nendif()\n\nset(CMAKE_EXE_LINKER_FLAGS_PROFILE \"${CMAKE_EXE_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_SHARED_LINKER_FLAGS_PROFILE \"${CMAKE_SHARED_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_C_FLAGS_PROFILE \"${CMAKE_C_FLAGS_RELEASE}\")\nset(CMAKE_CXX_FLAGS_PROFILE \"${CMAKE_CXX_FLAGS_RELEASE}\")\n\n# Use Unicode for all projects.\nadd_definitions(-DUNICODE -D_UNICODE)\n\n# Compilation settings that should be applied to most targets.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_17)\n  target_compile_options(${TARGET} PRIVATE /W4 /WX /wd\"4100\")\n  target_compile_options(${TARGET} PRIVATE /EHsc)\n  target_compile_definitions(${TARGET} PRIVATE \"_HAS_EXCEPTIONS=0\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<CONFIG:Debug>:_DEBUG>\")\nendfunction()\n\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\n\n# Flutter library and tool build rules.\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# Application build\nadd_subdirectory(\"runner\")\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n\n# === Installation ===\n# Support files are copied into place next to the executable, so that it can\n# run in place. This is done instead of making a separate bundle (as on Linux)\n# so that building and running from within Visual Studio will work.\nset(BUILD_BUNDLE_DIR \"$<TARGET_FILE_DIR:${BINARY_NAME}>\")\n# Make the \"install\" step default, as it's required to run.\nset(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nif(PLUGIN_BUNDLED_LIBRARIES)\n  install(FILES \"${PLUGIN_BUNDLED_LIBRARIES}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n\n# Fully re-copy the assets directory on each build to avoid having stale files\n# from a previous install.\nset(FLUTTER_ASSET_DIR_NAME \"flutter_assets\")\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\\\")\n  \" COMPONENT Runtime)\ninstall(DIRECTORY \"${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}\"\n  DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\" COMPONENT Runtime)\n\n# Install the AOT library on non-Debug builds only.\ninstall(FILES \"${AOT_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  CONFIGURATIONS Profile;Release\n  COMPONENT Runtime)\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/windows/flutter/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.15)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\nset(WRAPPER_ROOT \"${EPHEMERAL_DIR}/cpp_client_wrapper\")\n\n# === Flutter Library ===\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/flutter_windows.dll\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/windows/app.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"flutter_export.h\"\n  \"flutter_windows.h\"\n  \"flutter_messenger.h\"\n  \"flutter_plugin_registrar.h\"\n  \"flutter_texture_registrar.h\"\n)\nlist(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND \"${EPHEMERAL_DIR}/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}.lib\")\nadd_dependencies(flutter flutter_assemble)\n\n# === Wrapper ===\nlist(APPEND CPP_WRAPPER_SOURCES_CORE\n  \"core_implementations.cc\"\n  \"standard_codec.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_PLUGIN\n  \"plugin_registrar.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_APP\n  \"flutter_engine.cc\"\n  \"flutter_view_controller.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND \"${WRAPPER_ROOT}/\")\n\n# Wrapper sources needed for a plugin.\nadd_library(flutter_wrapper_plugin STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n)\napply_standard_settings(flutter_wrapper_plugin)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  POSITION_INDEPENDENT_CODE ON)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  CXX_VISIBILITY_PRESET hidden)\ntarget_link_libraries(flutter_wrapper_plugin PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_plugin PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_plugin flutter_assemble)\n\n# Wrapper sources needed for the runner.\nadd_library(flutter_wrapper_app STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\napply_standard_settings(flutter_wrapper_app)\ntarget_link_libraries(flutter_wrapper_app PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_app PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_app flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nset(PHONY_OUTPUT \"${CMAKE_CURRENT_BINARY_DIR}/_phony_\")\nset_source_files_properties(\"${PHONY_OUTPUT}\" PROPERTIES SYMBOLIC TRUE)\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}\n    ${CPP_WRAPPER_SOURCES_APP}\n    ${PHONY_OUTPUT}\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat\"\n      windows-x64 $<CONFIG>\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/windows/runner/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.15)\nproject(runner LANGUAGES CXX)\n\nadd_executable(${BINARY_NAME} WIN32\n  \"flutter_window.cpp\"\n  \"main.cpp\"\n  \"utils.cpp\"\n  \"win32_window.cpp\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n  \"Runner.rc\"\n  \"runner.exe.manifest\"\n)\napply_standard_settings(${BINARY_NAME})\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"NOMINMAX\")\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)\ntarget_include_directories(${BINARY_NAME} PRIVATE \"${CMAKE_SOURCE_DIR}\")\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/windows/runner/Runner.rc",
    "content": "// Microsoft Visual C++ generated resource script.\n//\n#pragma code_page(65001)\n#include \"resource.h\"\n\n#define APSTUDIO_READONLY_SYMBOLS\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 2 resource.\n//\n#include \"winres.h\"\n\n/////////////////////////////////////////////////////////////////////////////\n#undef APSTUDIO_READONLY_SYMBOLS\n\n/////////////////////////////////////////////////////////////////////////////\n// English (United States) resources\n\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\nLANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US\n\n#ifdef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// TEXTINCLUDE\n//\n\n1 TEXTINCLUDE\nBEGIN\n    \"resource.h\\0\"\nEND\n\n2 TEXTINCLUDE\nBEGIN\n    \"#include \"\"winres.h\"\"\\r\\n\"\n    \"\\0\"\nEND\n\n3 TEXTINCLUDE\nBEGIN\n    \"\\r\\n\"\n    \"\\0\"\nEND\n\n#endif    // APSTUDIO_INVOKED\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Icon\n//\n\n// Icon with lowest ID value placed first to ensure application icon\n// remains consistent on all systems.\nIDI_APP_ICON            ICON                    \"resources\\\\app_icon.ico\"\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Version\n//\n\n#ifdef FLUTTER_BUILD_NUMBER\n#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER\n#else\n#define VERSION_AS_NUMBER 1,0,0\n#endif\n\n#ifdef FLUTTER_BUILD_NAME\n#define VERSION_AS_STRING #FLUTTER_BUILD_NAME\n#else\n#define VERSION_AS_STRING \"1.0.0\"\n#endif\n\nVS_VERSION_INFO VERSIONINFO\n FILEVERSION VERSION_AS_NUMBER\n PRODUCTVERSION VERSION_AS_NUMBER\n FILEFLAGSMASK VS_FFI_FILEFLAGSMASK\n#ifdef _DEBUG\n FILEFLAGS VS_FF_DEBUG\n#else\n FILEFLAGS 0x0L\n#endif\n FILEOS VOS__WINDOWS32\n FILETYPE VFT_APP\n FILESUBTYPE 0x0L\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904e4\"\n        BEGIN\n            VALUE \"CompanyName\", \"com.example\" \"\\0\"\n            VALUE \"FileDescription\", \"Demonstrates how to use the flowy_infra_ui plugin.\" \"\\0\"\n            VALUE \"FileVersion\", VERSION_AS_STRING \"\\0\"\n            VALUE \"InternalName\", \"flowy_infra_ui_example\" \"\\0\"\n            VALUE \"LegalCopyright\", \"Copyright (C) 2021 com.example. All rights reserved.\" \"\\0\"\n            VALUE \"OriginalFilename\", \"flowy_infra_ui_example.exe\" \"\\0\"\n            VALUE \"ProductName\", \"flowy_infra_ui_example\" \"\\0\"\n            VALUE \"ProductVersion\", VERSION_AS_STRING \"\\0\"\n        END\n    END\n    BLOCK \"VarFileInfo\"\n    BEGIN\n        VALUE \"Translation\", 0x409, 1252\n    END\nEND\n\n#endif    // English (United States) resources\n/////////////////////////////////////////////////////////////////////////////\n\n\n\n#ifndef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 3 resource.\n//\n\n\n/////////////////////////////////////////////////////////////////////////////\n#endif    // not APSTUDIO_INVOKED\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/windows/runner/flutter_window.cpp",
    "content": "#include \"flutter_window.h\"\n\n#include <optional>\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nFlutterWindow::FlutterWindow(const flutter::DartProject& project)\n    : project_(project) {}\n\nFlutterWindow::~FlutterWindow() {}\n\nbool FlutterWindow::OnCreate() {\n  if (!Win32Window::OnCreate()) {\n    return false;\n  }\n\n  RECT frame = GetClientArea();\n\n  // The size here must match the window dimensions to avoid unnecessary surface\n  // creation / destruction in the startup path.\n  flutter_controller_ = std::make_unique<flutter::FlutterViewController>(\n      frame.right - frame.left, frame.bottom - frame.top, project_);\n  // Ensure that basic setup of the controller was successful.\n  if (!flutter_controller_->engine() || !flutter_controller_->view()) {\n    return false;\n  }\n  RegisterPlugins(flutter_controller_->engine());\n  SetChildContent(flutter_controller_->view()->GetNativeWindow());\n  return true;\n}\n\nvoid FlutterWindow::OnDestroy() {\n  if (flutter_controller_) {\n    flutter_controller_ = nullptr;\n  }\n\n  Win32Window::OnDestroy();\n}\n\nLRESULT\nFlutterWindow::MessageHandler(HWND hwnd, UINT const message,\n                              WPARAM const wparam,\n                              LPARAM const lparam) noexcept {\n  // Give Flutter, including plugins, an opportunity to handle window messages.\n  if (flutter_controller_) {\n    std::optional<LRESULT> result =\n        flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,\n                                                      lparam);\n    if (result) {\n      return *result;\n    }\n  }\n\n  switch (message) {\n    case WM_FONTCHANGE:\n      flutter_controller_->engine()->ReloadSystemFonts();\n      break;\n  }\n\n  return Win32Window::MessageHandler(hwnd, message, wparam, lparam);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/windows/runner/flutter_window.h",
    "content": "#ifndef RUNNER_FLUTTER_WINDOW_H_\n#define RUNNER_FLUTTER_WINDOW_H_\n\n#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n\n#include <memory>\n\n#include \"win32_window.h\"\n\n// A window that does nothing but host a Flutter view.\nclass FlutterWindow : public Win32Window {\n public:\n  // Creates a new FlutterWindow hosting a Flutter view running |project|.\n  explicit FlutterWindow(const flutter::DartProject& project);\n  virtual ~FlutterWindow();\n\n protected:\n  // Win32Window:\n  bool OnCreate() override;\n  void OnDestroy() override;\n  LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,\n                         LPARAM const lparam) noexcept override;\n\n private:\n  // The project to run.\n  flutter::DartProject project_;\n\n  // The Flutter instance hosted by this window.\n  std::unique_ptr<flutter::FlutterViewController> flutter_controller_;\n};\n\n#endif  // RUNNER_FLUTTER_WINDOW_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/windows/runner/main.cpp",
    "content": "#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n#include <windows.h>\n\n#include \"flutter_window.h\"\n#include \"utils.h\"\n\nint APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,\n                      _In_ wchar_t *command_line, _In_ int show_command) {\n  // Attach to console when present (e.g., 'flutter run') or create a\n  // new console when running with a debugger.\n  if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {\n    CreateAndAttachConsole();\n  }\n\n  // Initialize COM, so that it is available for use in the library and/or\n  // plugins.\n  ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);\n\n  flutter::DartProject project(L\"data\");\n\n  std::vector<std::string> command_line_arguments =\n      GetCommandLineArguments();\n\n  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));\n\n  FlutterWindow window(project);\n  Win32Window::Point origin(10, 10);\n  Win32Window::Size size(1280, 720);\n  if (!window.CreateAndShow(L\"flowy_infra_ui_example\", origin, size)) {\n    return EXIT_FAILURE;\n  }\n  window.SetQuitOnClose(true);\n\n  ::MSG msg;\n  while (::GetMessage(&msg, nullptr, 0, 0)) {\n    ::TranslateMessage(&msg);\n    ::DispatchMessage(&msg);\n  }\n\n  ::CoUninitialize();\n  return EXIT_SUCCESS;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/windows/runner/resource.h",
    "content": "//{{NO_DEPENDENCIES}}\n// Microsoft Visual C++ generated include file.\n// Used by Runner.rc\n//\n#define IDI_APP_ICON                    101\n\n// Next default values for new objects\n//\n#ifdef APSTUDIO_INVOKED\n#ifndef APSTUDIO_READONLY_SYMBOLS\n#define _APS_NEXT_RESOURCE_VALUE        102\n#define _APS_NEXT_COMMAND_VALUE         40001\n#define _APS_NEXT_CONTROL_VALUE         1001\n#define _APS_NEXT_SYMED_VALUE           101\n#endif\n#endif\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/windows/runner/runner.exe.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2</dpiAwareness>\n    </windowsSettings>\n  </application>\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- Windows 10 -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"/>\n      <!-- Windows 8.1 -->\n      <supportedOS Id=\"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\"/>\n      <!-- Windows 8 -->\n      <supportedOS Id=\"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\"/>\n      <!-- Windows 7 -->\n      <supportedOS Id=\"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\"/>\n    </application>\n  </compatibility>\n</assembly>\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/windows/runner/utils.cpp",
    "content": "#include \"utils.h\"\n\n#include <flutter_windows.h>\n#include <io.h>\n#include <stdio.h>\n#include <windows.h>\n\n#include <iostream>\n\nvoid CreateAndAttachConsole() {\n  if (::AllocConsole()) {\n    FILE *unused;\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stdout)) {\n      _dup2(_fileno(stdout), 1);\n    }\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stderr)) {\n      _dup2(_fileno(stdout), 2);\n    }\n    std::ios::sync_with_stdio();\n    FlutterDesktopResyncOutputStreams();\n  }\n}\n\nstd::vector<std::string> GetCommandLineArguments() {\n  // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.\n  int argc;\n  wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);\n  if (argv == nullptr) {\n    return std::vector<std::string>();\n  }\n\n  std::vector<std::string> command_line_arguments;\n\n  // Skip the first argument as it's the binary name.\n  for (int i = 1; i < argc; i++) {\n    command_line_arguments.push_back(Utf8FromUtf16(argv[i]));\n  }\n\n  ::LocalFree(argv);\n\n  return command_line_arguments;\n}\n\nstd::string Utf8FromUtf16(const wchar_t* utf16_string) {\n  if (utf16_string == nullptr) {\n    return std::string();\n  }\n  int target_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      -1, nullptr, 0, nullptr, nullptr);\n  if (target_length == 0) {\n    return std::string();\n  }\n  std::string utf8_string;\n  utf8_string.resize(target_length);\n  int converted_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      -1, utf8_string.data(),\n      target_length, nullptr, nullptr);\n  if (converted_length == 0) {\n    return std::string();\n  }\n  return utf8_string;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/windows/runner/utils.h",
    "content": "#ifndef RUNNER_UTILS_H_\n#define RUNNER_UTILS_H_\n\n#include <string>\n#include <vector>\n\n// Creates a console for the process, and redirects stdout and stderr to\n// it for both the runner and the Flutter library.\nvoid CreateAndAttachConsole();\n\n// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string\n// encoded in UTF-8. Returns an empty std::string on failure.\nstd::string Utf8FromUtf16(const wchar_t* utf16_string);\n\n// Gets the command line arguments passed in as a std::vector<std::string>,\n// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.\nstd::vector<std::string> GetCommandLineArguments();\n\n#endif  // RUNNER_UTILS_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/windows/runner/win32_window.cpp",
    "content": "#include \"win32_window.h\"\n\n#include <flutter_windows.h>\n\n#include \"resource.h\"\n\nnamespace {\n\nconstexpr const wchar_t kWindowClassName[] = L\"FLUTTER_RUNNER_WIN32_WINDOW\";\n\n// The number of Win32Window objects that currently exist.\nstatic int g_active_window_count = 0;\n\nusing EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);\n\n// Scale helper to convert logical scaler values to physical using passed in\n// scale factor\nint Scale(int source, double scale_factor) {\n  return static_cast<int>(source * scale_factor);\n}\n\n// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.\n// This API is only needed for PerMonitor V1 awareness mode.\nvoid EnableFullDpiSupportIfAvailable(HWND hwnd) {\n  HMODULE user32_module = LoadLibraryA(\"User32.dll\");\n  if (!user32_module) {\n    return;\n  }\n  auto enable_non_client_dpi_scaling =\n      reinterpret_cast<EnableNonClientDpiScaling*>(\n          GetProcAddress(user32_module, \"EnableNonClientDpiScaling\"));\n  if (enable_non_client_dpi_scaling != nullptr) {\n    enable_non_client_dpi_scaling(hwnd);\n    FreeLibrary(user32_module);\n  }\n}\n\n}  // namespace\n\n// Manages the Win32Window's window class registration.\nclass WindowClassRegistrar {\n public:\n  ~WindowClassRegistrar() = default;\n\n  // Returns the singleton registar instance.\n  static WindowClassRegistrar* GetInstance() {\n    if (!instance_) {\n      instance_ = new WindowClassRegistrar();\n    }\n    return instance_;\n  }\n\n  // Returns the name of the window class, registering the class if it hasn't\n  // previously been registered.\n  const wchar_t* GetWindowClass();\n\n  // Unregisters the window class. Should only be called if there are no\n  // instances of the window.\n  void UnregisterWindowClass();\n\n private:\n  WindowClassRegistrar() = default;\n\n  static WindowClassRegistrar* instance_;\n\n  bool class_registered_ = false;\n};\n\nWindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;\n\nconst wchar_t* WindowClassRegistrar::GetWindowClass() {\n  if (!class_registered_) {\n    WNDCLASS window_class{};\n    window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);\n    window_class.lpszClassName = kWindowClassName;\n    window_class.style = CS_HREDRAW | CS_VREDRAW;\n    window_class.cbClsExtra = 0;\n    window_class.cbWndExtra = 0;\n    window_class.hInstance = GetModuleHandle(nullptr);\n    window_class.hIcon =\n        LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));\n    window_class.hbrBackground = 0;\n    window_class.lpszMenuName = nullptr;\n    window_class.lpfnWndProc = Win32Window::WndProc;\n    RegisterClass(&window_class);\n    class_registered_ = true;\n  }\n  return kWindowClassName;\n}\n\nvoid WindowClassRegistrar::UnregisterWindowClass() {\n  UnregisterClass(kWindowClassName, nullptr);\n  class_registered_ = false;\n}\n\nWin32Window::Win32Window() {\n  ++g_active_window_count;\n}\n\nWin32Window::~Win32Window() {\n  --g_active_window_count;\n  Destroy();\n}\n\nbool Win32Window::CreateAndShow(const std::wstring& title,\n                                const Point& origin,\n                                const Size& size) {\n  Destroy();\n\n  const wchar_t* window_class =\n      WindowClassRegistrar::GetInstance()->GetWindowClass();\n\n  const POINT target_point = {static_cast<LONG>(origin.x),\n                              static_cast<LONG>(origin.y)};\n  HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);\n  UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);\n  double scale_factor = dpi / 96.0;\n\n  HWND window = CreateWindow(\n      window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,\n      Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),\n      Scale(size.width, scale_factor), Scale(size.height, scale_factor),\n      nullptr, nullptr, GetModuleHandle(nullptr), this);\n\n  if (!window) {\n    return false;\n  }\n\n  return OnCreate();\n}\n\n// static\nLRESULT CALLBACK Win32Window::WndProc(HWND const window,\n                                      UINT const message,\n                                      WPARAM const wparam,\n                                      LPARAM const lparam) noexcept {\n  if (message == WM_NCCREATE) {\n    auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);\n    SetWindowLongPtr(window, GWLP_USERDATA,\n                     reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));\n\n    auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);\n    EnableFullDpiSupportIfAvailable(window);\n    that->window_handle_ = window;\n  } else if (Win32Window* that = GetThisFromHandle(window)) {\n    return that->MessageHandler(window, message, wparam, lparam);\n  }\n\n  return DefWindowProc(window, message, wparam, lparam);\n}\n\nLRESULT\nWin32Window::MessageHandler(HWND hwnd,\n                            UINT const message,\n                            WPARAM const wparam,\n                            LPARAM const lparam) noexcept {\n  switch (message) {\n    case WM_DESTROY:\n      window_handle_ = nullptr;\n      Destroy();\n      if (quit_on_close_) {\n        PostQuitMessage(0);\n      }\n      return 0;\n\n    case WM_DPICHANGED: {\n      auto newRectSize = reinterpret_cast<RECT*>(lparam);\n      LONG newWidth = newRectSize->right - newRectSize->left;\n      LONG newHeight = newRectSize->bottom - newRectSize->top;\n\n      SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,\n                   newHeight, SWP_NOZORDER | SWP_NOACTIVATE);\n\n      return 0;\n    }\n    case WM_SIZE: {\n      RECT rect = GetClientArea();\n      if (child_content_ != nullptr) {\n        // Size and position the child window.\n        MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,\n                   rect.bottom - rect.top, TRUE);\n      }\n      return 0;\n    }\n\n    case WM_ACTIVATE:\n      if (child_content_ != nullptr) {\n        SetFocus(child_content_);\n      }\n      return 0;\n  }\n\n  return DefWindowProc(window_handle_, message, wparam, lparam);\n}\n\nvoid Win32Window::Destroy() {\n  OnDestroy();\n\n  if (window_handle_) {\n    DestroyWindow(window_handle_);\n    window_handle_ = nullptr;\n  }\n  if (g_active_window_count == 0) {\n    WindowClassRegistrar::GetInstance()->UnregisterWindowClass();\n  }\n}\n\nWin32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {\n  return reinterpret_cast<Win32Window*>(\n      GetWindowLongPtr(window, GWLP_USERDATA));\n}\n\nvoid Win32Window::SetChildContent(HWND content) {\n  child_content_ = content;\n  SetParent(content, window_handle_);\n  RECT frame = GetClientArea();\n\n  MoveWindow(content, frame.left, frame.top, frame.right - frame.left,\n             frame.bottom - frame.top, true);\n\n  SetFocus(child_content_);\n}\n\nRECT Win32Window::GetClientArea() {\n  RECT frame;\n  GetClientRect(window_handle_, &frame);\n  return frame;\n}\n\nHWND Win32Window::GetHandle() {\n  return window_handle_;\n}\n\nvoid Win32Window::SetQuitOnClose(bool quit_on_close) {\n  quit_on_close_ = quit_on_close;\n}\n\nbool Win32Window::OnCreate() {\n  // No-op; provided for subclasses.\n  return true;\n}\n\nvoid Win32Window::OnDestroy() {\n  // No-op; provided for subclasses.\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/example/windows/runner/win32_window.h",
    "content": "#ifndef RUNNER_WIN32_WINDOW_H_\n#define RUNNER_WIN32_WINDOW_H_\n\n#include <windows.h>\n\n#include <functional>\n#include <memory>\n#include <string>\n\n// A class abstraction for a high DPI-aware Win32 Window. Intended to be\n// inherited from by classes that wish to specialize with custom\n// rendering and input handling\nclass Win32Window {\n public:\n  struct Point {\n    unsigned int x;\n    unsigned int y;\n    Point(unsigned int x, unsigned int y) : x(x), y(y) {}\n  };\n\n  struct Size {\n    unsigned int width;\n    unsigned int height;\n    Size(unsigned int width, unsigned int height)\n        : width(width), height(height) {}\n  };\n\n  Win32Window();\n  virtual ~Win32Window();\n\n  // Creates and shows a win32 window with |title| and position and size using\n  // |origin| and |size|. New windows are created on the default monitor. Window\n  // sizes are specified to the OS in physical pixels, hence to ensure a\n  // consistent size to will treat the width height passed in to this function\n  // as logical pixels and scale to appropriate for the default monitor. Returns\n  // true if the window was created successfully.\n  bool CreateAndShow(const std::wstring& title,\n                     const Point& origin,\n                     const Size& size);\n\n  // Release OS resources associated with window.\n  void Destroy();\n\n  // Inserts |content| into the window tree.\n  void SetChildContent(HWND content);\n\n  // Returns the backing Window handle to enable clients to set icon and other\n  // window properties. Returns nullptr if the window has been destroyed.\n  HWND GetHandle();\n\n  // If true, closing this window will quit the application.\n  void SetQuitOnClose(bool quit_on_close);\n\n  // Return a RECT representing the bounds of the current client area.\n  RECT GetClientArea();\n\n protected:\n  // Processes and route salient window messages for mouse handling,\n  // size change and DPI. Delegates handling of these to member overloads that\n  // inheriting classes can handle.\n  virtual LRESULT MessageHandler(HWND window,\n                                 UINT const message,\n                                 WPARAM const wparam,\n                                 LPARAM const lparam) noexcept;\n\n  // Called when CreateAndShow is called, allowing subclass window-related\n  // setup. Subclasses should return false if setup fails.\n  virtual bool OnCreate();\n\n  // Called when Destroy is called.\n  virtual void OnDestroy();\n\n private:\n  friend class WindowClassRegistrar;\n\n  // OS callback called by message pump. Handles the WM_NCCREATE message which\n  // is passed when the non-client area is being created and enables automatic\n  // non-client DPI scaling so that the non-client area automatically\n  // responsponds to changes in DPI. All other messages are handled by\n  // MessageHandler.\n  static LRESULT CALLBACK WndProc(HWND const window,\n                                  UINT const message,\n                                  WPARAM const wparam,\n                                  LPARAM const lparam) noexcept;\n\n  // Retrieves a class instance pointer for |window|\n  static Win32Window* GetThisFromHandle(HWND const window) noexcept;\n\n  bool quit_on_close_ = false;\n\n  // window handle for top level window.\n  HWND window_handle_ = nullptr;\n\n  // window handle for hosted content.\n  HWND child_content_ = nullptr;\n};\n\n#endif  // RUNNER_WIN32_WINDOW_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\nbuild/\n\n# Android related\n**/android/**/gradle-wrapper.jar\n**/android/.gradle\n**/android/captures/\n**/android/gradlew\n**/android/gradlew.bat\n**/android/local.properties\n**/android/**/GeneratedPluginRegistrant.java\n\n# iOS/XCode related\n**/ios/**/*.mode1v3\n**/ios/**/*.mode2v3\n**/ios/**/*.moved-aside\n**/ios/**/*.pbxuser\n**/ios/**/*.perspectivev3\n**/ios/**/*sync/\n**/ios/**/.sconsign.dblite\n**/ios/**/.tags*\n**/ios/**/.vagrant/\n**/ios/**/DerivedData/\n**/ios/**/Icon?\n**/ios/**/Pods/\n**/ios/**/.symlinks/\n**/ios/**/profile\n**/ios/**/xcuserdata\n**/ios/.generated/\n**/ios/Flutter/App.framework\n**/ios/Flutter/Flutter.framework\n**/ios/Flutter/Flutter.podspec\n**/ios/Flutter/Generated.xcconfig\n**/ios/Flutter/ephemeral\n**/ios/Flutter/app.flx\n**/ios/Flutter/app.zip\n**/ios/Flutter/flutter_assets/\n**/ios/Flutter/flutter_export_environment.sh\n**/ios/ServiceDefinitions.json\n**/ios/Runner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!**/ios/**/default.mode1v3\n!**/ios/**/default.mode2v3\n!**/ios/**/default.pbxuser\n!**/ios/**/default.perspectivev3\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/.metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: fa5883b78e566877613ad1ccb48dd92075cb5c23\n  channel: dev\n\nproject_type: package\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/CHANGELOG.md",
    "content": "## 0.0.1\n\n* TODO: Describe initial release.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/LICENSE",
    "content": "TODO: Add your license here.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/README.md",
    "content": "# flowy_infra_ui_platform_interface\n\nA new Flutter package project.\n\n## Getting Started\n\nThis project is a starting point for a Dart\n[package](https://flutter.dev/developing-packages/),\na library module containing code that can be shared easily across\nmultiple Flutter or Dart projects.\n\nFor help getting started with Flutter, view our \n[online documentation](https://flutter.dev/docs), which offers tutorials, \nsamples, guidance on mobile development, and a full API reference.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/analysis_options.yaml",
    "content": "include: package:flutter_lints/flutter.yaml\n\n# Additional information about this file can be found at\n# https://dart.dev/guides/language/analysis-options\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/lib/flowy_infra_ui_platform_interface.dart",
    "content": "library flowy_infra_ui_platform_interface;\n\nimport 'package:plugin_platform_interface/plugin_platform_interface.dart';\nimport 'src/method_channel_flowy_infra_ui.dart';\n\nabstract class FlowyInfraUIPlatform extends PlatformInterface {\n  FlowyInfraUIPlatform() : super(token: _token);\n\n  static final Object _token = Object();\n\n  static FlowyInfraUIPlatform _instance = MethodChannelFlowyInfraUI();\n\n  static FlowyInfraUIPlatform get instance => _instance;\n\n  static set instance(FlowyInfraUIPlatform instance) {\n    PlatformInterface.verifyToken(instance, _token);\n    _instance = instance;\n  }\n\n  Stream<bool> get onKeyboardVisibilityChange {\n    throw UnimplementedError(\n        '`onKeyboardChange` should be overridden by subclass.');\n  }\n\n  Future<String?> getPlatformVersion() {\n    throw UnimplementedError(\n        '`getPlatformVersion` should be overridden by subclass.');\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/lib/src/method_channel_flowy_infra_ui.dart",
    "content": "import 'package:flowy_infra_ui_platform_interface/flowy_infra_ui_platform_interface.dart';\nimport 'package:flutter/services.dart';\n\n// ignore_for_file: constant_identifier_names\nconst INFRA_UI_METHOD_CHANNEL_NAME = 'flowy_infra_ui_method';\nconst INFRA_UI_KEYBOARD_EVENT_CHANNEL_NAME = 'flowy_infra_ui_event/keyboard';\nconst INFRA_UI_METHOD_GET_PLATFORM_VERSION = 'getPlatformVersion';\n\nclass MethodChannelFlowyInfraUI extends FlowyInfraUIPlatform {\n  final MethodChannel _methodChannel =\n      const MethodChannel(INFRA_UI_METHOD_CHANNEL_NAME);\n  final EventChannel _keyboardChannel =\n      const EventChannel(INFRA_UI_KEYBOARD_EVENT_CHANNEL_NAME);\n\n  late final Stream<bool> _onKeyboardVisibilityChange =\n      _keyboardChannel.receiveBroadcastStream().map((event) => event as bool);\n\n  @override\n  Stream<bool> get onKeyboardVisibilityChange => _onKeyboardVisibilityChange;\n\n  @override\n  Future<String> getPlatformVersion() async {\n    String? version = await _methodChannel\n        .invokeMethod<String>(INFRA_UI_METHOD_GET_PLATFORM_VERSION);\n    return version ?? 'unknow';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml",
    "content": "name: flowy_infra_ui_platform_interface\ndescription: A new Flutter package project.\nversion: 0.0.1\nhomepage: https://github.com/appflowy-io/appflowy\n\nenvironment:\n  sdk: \">=2.12.0 <3.0.0\"\n  flutter: \">=1.17.0\"\n\ndependencies:\n  flutter:\n    sdk: flutter\n\n  plugin_platform_interface: ^2.0.0\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n  flutter_lints: ^3.0.1\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/test/flowy_infra_ui_platform_interface_test.dart",
    "content": "void main() {}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\nbuild/\n\n# Android related\n**/android/**/gradle-wrapper.jar\n**/android/.gradle\n**/android/captures/\n**/android/gradlew\n**/android/gradlew.bat\n**/android/local.properties\n**/android/**/GeneratedPluginRegistrant.java\n\n# iOS/XCode related\n**/ios/**/*.mode1v3\n**/ios/**/*.mode2v3\n**/ios/**/*.moved-aside\n**/ios/**/*.pbxuser\n**/ios/**/*.perspectivev3\n**/ios/**/*sync/\n**/ios/**/.sconsign.dblite\n**/ios/**/.tags*\n**/ios/**/.vagrant/\n**/ios/**/DerivedData/\n**/ios/**/Icon?\n**/ios/**/Pods/\n**/ios/**/.symlinks/\n**/ios/**/profile\n**/ios/**/xcuserdata\n**/ios/.generated/\n**/ios/Flutter/App.framework\n**/ios/Flutter/Flutter.framework\n**/ios/Flutter/Flutter.podspec\n**/ios/Flutter/Generated.xcconfig\n**/ios/Flutter/ephemeral\n**/ios/Flutter/app.flx\n**/ios/Flutter/app.zip\n**/ios/Flutter/flutter_assets/\n**/ios/Flutter/flutter_export_environment.sh\n**/ios/ServiceDefinitions.json\n**/ios/Runner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!**/ios/**/default.mode1v3\n!**/ios/**/default.mode2v3\n!**/ios/**/default.pbxuser\n!**/ios/**/default.perspectivev3\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/.metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: fa5883b78e566877613ad1ccb48dd92075cb5c23\n  channel: dev\n\nproject_type: package\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/CHANGELOG.md",
    "content": "## 0.0.1\n\n* TODO: Describe initial release.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/LICENSE",
    "content": "TODO: Add your license here.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/README.md",
    "content": "# flowy_infra_ui_web\n\nA new Flutter package project.\n\n## Getting Started\n\nThis project is a starting point for a Dart\n[package](https://flutter.dev/developing-packages/),\na library module containing code that can be shared easily across\nmultiple Flutter or Dart projects.\n\nFor help getting started with Flutter, view our \n[online documentation](https://flutter.dev/docs), which offers tutorials, \nsamples, guidance on mobile development, and a full API reference.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/analysis_options.yaml",
    "content": "include: package:flutter_lints/flutter.yaml\n\n# Additional information about this file can be found at\n# https://dart.dev/guides/language/analysis-options\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/lib/flowy_infra_ui_web.dart",
    "content": "library flowy_infra_ui_web;\n\nimport 'dart:html' as html show window;\nimport 'package:flowy_infra_ui_platform_interface/flowy_infra_ui_platform_interface.dart';\nimport 'package:flutter_web_plugins/flutter_web_plugins.dart';\n\nclass FlowyInfraUIPlugin extends FlowyInfraUIPlatform {\n  static void registerWith(Registrar registrar) {\n    FlowyInfraUIPlatform.instance = FlowyInfraUIPlugin();\n  }\n\n  // MARK: - Keyboard\n\n  @override\n  Stream<bool> get onKeyboardVisibilityChange async* {\n    // suppose that keyboard won't show in web side\n    yield false;\n  }\n\n  @override\n  Future<String?> getPlatformVersion() async {\n    final version = html.window.navigator.userAgent;\n    return Future.value(version);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml",
    "content": "name: flowy_infra_ui_web\ndescription: A new Flutter package project.\nversion: 0.0.1\nhomepage: https://github.com/appflowy-io/appflowy\npublish_to: none\n\nenvironment:\n  sdk: \">=2.12.0 <3.0.0\"\n  flutter: \">=1.17.0\"\n\ndependencies:\n  flutter_web_plugins:\n    sdk: flutter\n\n  flowy_infra_ui_platform_interface:\n    path: ../flowy_infra_ui_platform_interface\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n  flutter_lints: ^3.0.1\n\nflutter:\n  plugin:\n    platforms:\n      web:\n        pluginClass: FlowyInfraUIPlugin\n        fileName: flowy_infra_ui_web.dart\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/test/flowy_infra_ui_web_test.dart",
    "content": "void main() {}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/ios/.gitignore",
    "content": ".idea/\n.vagrant/\n.sconsign.dblite\n.svn/\n\n.DS_Store\n*.swp\nprofile\n\nDerivedData/\nbuild/\nGeneratedPluginRegistrant.h\nGeneratedPluginRegistrant.m\n\n.generated/\n\n*.pbxuser\n*.mode1v3\n*.mode2v3\n*.perspectivev3\n\n!default.pbxuser\n!default.mode1v3\n!default.mode2v3\n!default.perspectivev3\n\nxcuserdata\n\n*.moved-aside\n\n*.pyc\n*sync/\nIcon?\n.tags*\n\n/Flutter/Generated.xcconfig\n/Flutter/ephemeral/\n/Flutter/flutter_export_environment.sh"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Assets/.gitkeep",
    "content": ""
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/Event/KeyboardEventHandler.swift",
    "content": "//\n//  KeyboardEventHandler.swift\n//  flowy_infra_ui\n//\n//  Created by Jaylen Bian on 7/17/21.\n//\n\nclass KeyboardEventHandler: NSObject, FlutterStreamHandler {\n\n    var isKeyboardShow: Bool = false\n    var eventSink: FlutterEventSink?\n\n    override init() {\n        super.init()\n\n        NotificationCenter.default.addObserver(\n            self,\n            selector: #selector(handleKeyboardWillShow),\n            name: UIApplication.keyboardWillShowNotification,\n            object: nil)\n        NotificationCenter.default.addObserver(\n            self,\n            selector: #selector(handleKeyboardDidShow),\n            name: UIApplication.keyboardDidShowNotification,\n            object: nil)\n        NotificationCenter.default.addObserver(\n            self,\n            selector: #selector(handleKeyboardWillHide),\n            name: UIApplication.keyboardWillHideNotification,\n            object: nil)\n        NotificationCenter.default.addObserver(\n            self,\n            selector: #selector(handleKeyboardDidHide),\n            name: UIApplication.keyboardDidHideNotification,\n            object: nil)\n    }\n\n    func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {\n        eventSink = events\n        return nil\n    }\n\n    func onCancel(withArguments arguments: Any?) -> FlutterError? {\n        eventSink = nil\n        return nil\n    }\n\n    // MARK: Helper\n\n    @objc\n    private func handleKeyboardWillShow() {\n        guard !isKeyboardShow else {\n            return\n        }\n        isKeyboardShow = true\n        eventSink?(NSNumber(booleanLiteral: true))\n    }\n\n    @objc\n    private func handleKeyboardDidShow() {\n        guard !isKeyboardShow else {\n            return\n        }\n        isKeyboardShow = true\n        eventSink?(NSNumber(booleanLiteral: true))\n    }\n\n    @objc\n    private func handleKeyboardWillHide() {\n        guard isKeyboardShow else {\n            return\n        }\n        isKeyboardShow = false\n        eventSink?(NSNumber(booleanLiteral: false))\n    }\n\n    @objc\n    private func handleKeyboardDidHide() {\n        guard isKeyboardShow else {\n            return\n        }\n        isKeyboardShow = false\n        eventSink?(NSNumber(booleanLiteral: false))\n    }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/FlowyInfraUIPlugin.h",
    "content": "#import <Flutter/Flutter.h>\n\n@interface FlowyInfraUIPlugin : NSObject <FlutterPlugin>\n@end\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/FlowyInfraUIPlugin.m",
    "content": "#import \"FlowyInfraUIPlugin.h\"\n#if __has_include(<flowy_infra_ui/flowy_infra_ui-Swift.h>)\n#import <flowy_infra_ui/flowy_infra_ui-Swift.h>\n#else\n// Support project import fallback if the generated compatibility header\n// is not copied when this plugin is created as a library.\n// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816\n#import \"flowy_infra_ui-Swift.h\"\n#endif\n\n@implementation FlowyInfraUIPlugin\n+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {\n  [SwiftFlowyInfraUIPlugin registerWithRegistrar:registrar];\n}\n@end\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/ios/Classes/SwiftFlowyInfraUIPlugin.swift",
    "content": "import Flutter\nimport UIKit\n\npublic class SwiftFlowyInfraUIPlugin: NSObject, FlutterPlugin {\n  public static func register(with registrar: FlutterPluginRegistrar) {\n    let channel = FlutterMethodChannel(name: \"flowy_infra_ui\", binaryMessenger: registrar.messenger())\n    let instance = SwiftFlowyInfraUIPlugin()\n    registrar.addMethodCallDelegate(instance, channel: channel)\n  }\n\n  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {\n    result(\"iOS \" + UIDevice.current.systemVersion)\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/ios/flowy_infra_ui.podspec",
    "content": "#\n# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.\n# Run `pod lib lint flowy_infra_ui.podspec` to validate before publishing.\n#\nPod::Spec.new do |s|\n  s.name             = 'flowy_infra_ui'\n  s.version          = '0.0.1'\n  s.summary          = 'A new flutter plugin project.'\n  s.description      = <<-DESC\nA new flutter plugin project.\n                       DESC\n  s.homepage         = 'http://example.com'\n  s.license          = { :file => '../LICENSE' }\n  s.author           = { 'AppFlowy' => 'annie@appflowy.io' }\n  s.source           = { :path => '.' }\n  s.source_files = 'Classes/**/*'\n  s.dependency 'Flutter'\n  s.platform = :ios, '8.0'\n\n  # Flutter.framework does not contain a i386 slice.\n  s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }\n  s.swift_version = '5.0'\nend\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/basis.dart",
    "content": "// MARK: - Shared Builder\ntypedef IndexedCallback = void Function(int index);\ntypedef IndexedValueCallback<T> = void Function(T value, int index);\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/flowy_infra_ui.dart",
    "content": "// Basis\nexport '/widget/flowy_tooltip.dart';\nexport '/widget/separated_flex.dart';\nexport '/widget/spacing.dart';\nexport 'basis.dart';\nexport 'src/flowy_overlay/appflowy_popover.dart';\nexport 'src/flowy_overlay/flowy_dialog.dart';\n// Overlay\nexport 'src/flowy_overlay/flowy_overlay.dart';\nexport 'src/flowy_overlay/list_overlay.dart';\nexport 'src/flowy_overlay/option_overlay.dart';\n// Keyboard\nexport 'src/keyboard/keyboard_visibility_detector.dart';\nexport 'style_widget/button.dart';\nexport 'style_widget/color_picker.dart';\nexport 'style_widget/divider.dart';\nexport 'style_widget/icon_button.dart';\nexport 'style_widget/primary_rounded_button.dart';\nexport 'style_widget/scrollbar.dart';\nexport 'style_widget/scrolling/styled_list.dart';\nexport 'style_widget/scrolling/styled_scroll_bar.dart';\nexport 'style_widget/text.dart';\nexport 'style_widget/text_field.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/flowy_infra_ui_web.dart",
    "content": "// Basis\nexport 'basis.dart';\n\n// Keyboard\nexport 'src/keyboard/keyboard_visibility_detector.dart';\n\n// Overlay\nexport 'src/flowy_overlay/flowy_overlay.dart';\nexport 'src/flowy_overlay/list_overlay.dart';\nexport 'src/flowy_overlay/option_overlay.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart",
    "content": "import 'package:appflowy_popover/appflowy_popover.dart';\nimport 'package:flowy_infra/colorscheme/default_colorscheme.dart';\nimport 'package:flutter/material.dart';\n\nexport 'package:appflowy_popover/appflowy_popover.dart';\n\nclass ShadowConstants {\n  ShadowConstants._();\n\n  static const List<BoxShadow> lightSmall = [\n    BoxShadow(offset: Offset(0, 4), blurRadius: 20, color: Color(0x1A1F2329)),\n  ];\n  static const List<BoxShadow> lightMedium = [\n    BoxShadow(offset: Offset(0, 4), blurRadius: 32, color: Color(0x121F2225)),\n  ];\n  static const List<BoxShadow> darkSmall = [\n    BoxShadow(offset: Offset(0, 2), blurRadius: 16, color: Color(0x7A000000)),\n  ];\n  static const List<BoxShadow> darkMedium = [\n    BoxShadow(offset: Offset(0, 4), blurRadius: 32, color: Color(0x7A000000)),\n  ];\n}\n\nclass AppFlowyPopover extends StatelessWidget {\n  const AppFlowyPopover({\n    super.key,\n    required this.child,\n    required this.popupBuilder,\n    this.direction = PopoverDirection.rightWithTopAligned,\n    this.onOpen,\n    this.onClose,\n    this.canClose,\n    this.constraints = const BoxConstraints(maxWidth: 240, maxHeight: 600),\n    this.mutex,\n    this.triggerActions = PopoverTriggerFlags.click,\n    this.offset,\n    this.controller,\n    this.asBarrier = false,\n    this.margin = const EdgeInsets.all(6),\n    this.windowPadding = const EdgeInsets.all(8.0),\n    this.clickHandler = PopoverClickHandler.listener,\n    this.skipTraversal = false,\n    this.decorationColor,\n    this.borderRadius,\n    this.popoverDecoration,\n    this.animationDuration = const Duration(),\n    this.slideDistance = 5.0,\n    this.beginScaleFactor = 0.9,\n    this.endScaleFactor = 1.0,\n    this.beginOpacity = 0.0,\n    this.endOpacity = 1.0,\n    this.showAtCursor = false,\n  });\n\n  final Widget child;\n  final PopoverController? controller;\n  final Widget Function(BuildContext context) popupBuilder;\n  final PopoverDirection direction;\n  final int triggerActions;\n  final BoxConstraints constraints;\n  final VoidCallback? onOpen;\n  final VoidCallback? onClose;\n  final Future<bool> Function()? canClose;\n  final PopoverMutex? mutex;\n  final Offset? offset;\n  final bool asBarrier;\n  final EdgeInsets margin;\n  final EdgeInsets windowPadding;\n  final Color? decorationColor;\n  final BorderRadius? borderRadius;\n  final Duration animationDuration;\n  final double slideDistance;\n  final double beginScaleFactor;\n  final double endScaleFactor;\n  final double beginOpacity;\n  final double endOpacity;\n  final Decoration? popoverDecoration;\n\n  /// The widget that will be used to trigger the popover.\n  ///\n  /// Why do we need this?\n  /// Because if the parent widget of the popover is GestureDetector,\n  ///  the conflict won't be resolve by using Listener, we want these two gestures exclusive.\n  final PopoverClickHandler clickHandler;\n\n  /// If true the popover will not participate in focus traversal.\n  ///\n  final bool skipTraversal;\n\n  /// Whether the popover should be shown at the cursor position.\n  /// If true, the [offset] will be ignored.\n  ///\n  /// This only works when using [PopoverClickHandler.listener] as the click handler.\n  ///\n  /// Alternatively for having a normal popover, and use the cursor position only on\n  /// secondary click, consider showing the popover programatically with [PopoverController.showAt].\n  ///\n  final bool showAtCursor;\n\n  @override\n  Widget build(BuildContext context) {\n    return Popover(\n      controller: controller,\n      animationDuration: animationDuration,\n      slideDistance: slideDistance,\n      beginScaleFactor: beginScaleFactor,\n      endScaleFactor: endScaleFactor,\n      beginOpacity: beginOpacity,\n      endOpacity: endOpacity,\n      onOpen: onOpen,\n      onClose: onClose,\n      canClose: canClose,\n      direction: direction,\n      mutex: mutex,\n      asBarrier: asBarrier,\n      triggerActions: triggerActions,\n      windowPadding: windowPadding,\n      offset: offset,\n      clickHandler: clickHandler,\n      skipTraversal: skipTraversal,\n      popupBuilder: (context) => _PopoverContainer(\n        constraints: constraints,\n        margin: margin,\n        decoration: popoverDecoration,\n        decorationColor: decorationColor,\n        borderRadius: borderRadius,\n        child: popupBuilder(context),\n      ),\n      showAtCursor: showAtCursor,\n      child: child,\n    );\n  }\n}\n\nclass _PopoverContainer extends StatelessWidget {\n  const _PopoverContainer({\n    this.decorationColor,\n    this.borderRadius,\n    this.decoration,\n    required this.child,\n    required this.margin,\n    required this.constraints,\n  });\n\n  final Widget child;\n  final BoxConstraints constraints;\n  final EdgeInsets margin;\n  final Color? decorationColor;\n  final BorderRadius? borderRadius;\n  final Decoration? decoration;\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      type: MaterialType.transparency,\n      child: Container(\n        padding: margin,\n        decoration: decoration ??\n            context.getPopoverDecoration(\n              color: decorationColor,\n              borderRadius: borderRadius,\n            ),\n        constraints: constraints,\n        child: child,\n      ),\n    );\n  }\n}\n\nextension PopoverDecoration on BuildContext {\n  /// The decoration of the popover.\n  ///\n  /// Don't customize the entire decoration of the popover,\n  ///   use the built-in popoverDecoration instead and ask the designer before changing it.\n  ShapeDecoration getPopoverDecoration({\n    Color? color,\n    BorderRadius? borderRadius,\n  }) {\n    final borderColor = Theme.of(this).brightness == Brightness.light\n        ? ColorSchemeConstants.lightBorderColor\n        : ColorSchemeConstants.darkBorderColor;\n    final shadows = Theme.of(this).brightness == Brightness.light\n        ? ShadowConstants.lightSmall\n        : ShadowConstants.darkSmall;\n    return ShapeDecoration(\n      color: color ?? Theme.of(this).cardColor,\n      shape: RoundedRectangleBorder(\n        side: BorderSide(\n          width: 1,\n          strokeAlign: BorderSide.strokeAlignOutside,\n          color: color != Colors.transparent ? borderColor : color!,\n        ),\n        borderRadius: borderRadius ?? BorderRadius.circular(10),\n      ),\n      shadows: shadows,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_dialog.dart",
    "content": "import 'package:flutter/material.dart';\n\nconst _overlayContainerPadding = EdgeInsets.symmetric(vertical: 12);\nconst overlayContainerMaxWidth = 760.0;\nconst overlayContainerMinWidth = 320.0;\nconst _defaultInsetPadding =\n    EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0);\n\nclass FlowyDialog extends StatelessWidget {\n  const FlowyDialog({\n    super.key,\n    required this.child,\n    this.title,\n    this.shape,\n    this.constraints,\n    this.padding = _overlayContainerPadding,\n    this.backgroundColor,\n    this.expandHeight = true,\n    this.alignment,\n    this.insetPadding,\n    this.width,\n  });\n\n  final Widget? title;\n  final ShapeBorder? shape;\n  final Widget child;\n  final BoxConstraints? constraints;\n  final EdgeInsets padding;\n  final Color? backgroundColor;\n  final bool expandHeight;\n\n  // Position of the Dialog\n  final Alignment? alignment;\n\n  // Inset of the Dialog\n  final EdgeInsets? insetPadding;\n\n  final double? width;\n\n  @override\n  Widget build(BuildContext context) {\n    final windowSize = MediaQuery.of(context).size;\n    final size = windowSize * 0.7;\n\n    return SimpleDialog(\n      alignment: alignment,\n      insetPadding: insetPadding ?? _defaultInsetPadding,\n      contentPadding: EdgeInsets.zero,\n      backgroundColor: backgroundColor ?? Theme.of(context).cardColor,\n      title: title,\n      shape: shape ??\n          RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),\n      clipBehavior: Clip.antiAliasWithSaveLayer,\n      children: [\n        Material(\n          type: MaterialType.transparency,\n          child: Container(\n            height: expandHeight ? size.height : null,\n            width: width ?? size.width,\n            constraints: constraints,\n            child: child,\n          ),\n        )\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart",
    "content": "// ignore_for_file: unused_element\n\nimport 'dart:ui';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nimport 'package:flowy_infra_ui/src/flowy_overlay/layout.dart';\n\n/// Specifies how overlay are anchored to the SourceWidget\nenum AnchorDirection {\n  // Corner aligned with a corner of the SourceWidget\n  topLeft,\n  topRight,\n  bottomLeft,\n  bottomRight,\n  center,\n\n  // Edge aligned with a edge of the SourceWidget\n  topWithLeftAligned,\n  topWithCenterAligned,\n  topWithRightAligned,\n  rightWithTopAligned,\n  rightWithCenterAligned,\n  rightWithBottomAligned,\n  bottomWithLeftAligned,\n  bottomWithCenterAligned,\n  bottomWithRightAligned,\n  leftWithTopAligned,\n  leftWithCenterAligned,\n  leftWithBottomAligned,\n\n  // Custom position\n  custom,\n}\n\n/// The behaviour of overlay when overlap with anchor widget\nenum OverlapBehaviour {\n  /// Maintain overlay size, which may cover the anchor widget.\n  none,\n\n  /// Resize overlay to avoid overlapping the anchor widget.\n  stretch,\n}\n\nenum OnBackBehavior {\n  /// Won't handle the back action\n  none,\n\n  /// Animate to get the user's attention\n  alert,\n\n  /// Intercept the back action and abort directly\n  abort,\n\n  /// Intercept the back action and dismiss overlay\n  dismiss,\n}\n\nclass FlowyOverlayStyle {\n  final Color barrierColor;\n  bool blur;\n\n  FlowyOverlayStyle(\n      {this.barrierColor = Colors.transparent, this.blur = false});\n}\n\nfinal GlobalKey<FlowyOverlayState> _key = GlobalKey<FlowyOverlayState>();\n\n/// Invoke this method in app generation process\nWidget overlayManagerBuilder(BuildContext context, Widget? child) {\n  assert(child != null, 'Child can\\'t be null.');\n  return FlowyOverlay(key: _key, child: child!);\n}\n\nabstract mixin class FlowyOverlayDelegate {\n  bool asBarrier() => false;\n  void didRemove() => {};\n}\n\nclass FlowyOverlay extends StatefulWidget {\n  const FlowyOverlay({super.key, required this.child});\n\n  final Widget child;\n\n  static FlowyOverlayState of(\n    BuildContext context, {\n    bool rootOverlay = false,\n  }) {\n    FlowyOverlayState? state = maybeOf(context, rootOverlay: rootOverlay);\n    assert(() {\n      if (state == null) {\n        throw FlutterError(\n          'Can\\'t find overlay manager in current context, please check if already wrapped by overlay manager.',\n        );\n      }\n      return true;\n    }());\n    return state!;\n  }\n\n  static FlowyOverlayState? maybeOf(\n    BuildContext context, {\n    bool rootOverlay = false,\n  }) {\n    FlowyOverlayState? state;\n    if (rootOverlay) {\n      state = context.findRootAncestorStateOfType<FlowyOverlayState>();\n    } else {\n      state = context.findAncestorStateOfType<FlowyOverlayState>();\n    }\n    return state;\n  }\n\n  static Future<void> show({\n    required BuildContext context,\n    required WidgetBuilder builder,\n  }) async {\n    await showDialog(\n      context: context,\n      builder: builder,\n    );\n  }\n\n  static void pop(BuildContext context) {\n    Navigator.of(context).pop();\n  }\n\n  @override\n  FlowyOverlayState createState() => FlowyOverlayState();\n}\n\nclass OverlayItem {\n  Widget widget;\n  String identifier;\n  FlowyOverlayDelegate? delegate;\n  FocusNode focusNode;\n\n  OverlayItem({\n    required this.widget,\n    required this.identifier,\n    required this.focusNode,\n    this.delegate,\n  });\n\n  void dispose() {\n    focusNode.dispose();\n  }\n}\n\nclass FlowyOverlayState extends State<FlowyOverlay> {\n  final List<OverlayItem> _overlayList = [];\n  FlowyOverlayStyle style = FlowyOverlayStyle();\n\n  final Map<ShortcutActivator, void Function(String)>\n      _keyboardShortcutBindings = {};\n\n  /// Insert a overlay widget which frame is set by the widget, not the component.\n  /// Be sure to specify the offset and size using a anchorable widget (like `Position`, `CompositedTransformFollower`)\n  void insertCustom({\n    required Widget widget,\n    required String identifier,\n    FlowyOverlayDelegate? delegate,\n  }) {\n    _showOverlay(\n      widget: widget,\n      identifier: identifier,\n      shouldAnchor: false,\n      delegate: delegate,\n    );\n  }\n\n  void insertWithRect({\n    required Widget widget,\n    required String identifier,\n    required Offset anchorPosition,\n    required Size anchorSize,\n    AnchorDirection? anchorDirection,\n    FlowyOverlayDelegate? delegate,\n    OverlapBehaviour? overlapBehaviour,\n    FlowyOverlayStyle? style,\n  }) {\n    if (style != null) {\n      this.style = style;\n    }\n\n    _showOverlay(\n      widget: widget,\n      identifier: identifier,\n      shouldAnchor: true,\n      delegate: delegate,\n      anchorPosition: anchorPosition,\n      anchorSize: anchorSize,\n      anchorDirection: anchorDirection,\n      overlapBehaviour: overlapBehaviour,\n    );\n  }\n\n  void insertWithAnchor({\n    required Widget widget,\n    required String identifier,\n    required BuildContext anchorContext,\n    AnchorDirection? anchorDirection,\n    FlowyOverlayDelegate? delegate,\n    OverlapBehaviour? overlapBehaviour,\n    FlowyOverlayStyle? style,\n    Offset? anchorOffset,\n  }) {\n    this.style = style ?? FlowyOverlayStyle();\n\n    _showOverlay(\n      widget: widget,\n      identifier: identifier,\n      shouldAnchor: true,\n      delegate: delegate,\n      anchorContext: anchorContext,\n      anchorDirection: anchorDirection,\n      overlapBehaviour: overlapBehaviour,\n      anchorOffset: anchorOffset,\n    );\n  }\n\n  void remove(String identifier) {\n    setState(() {\n      final index =\n          _overlayList.indexWhere((item) => item.identifier == identifier);\n      if (index != -1) {\n        final OverlayItem item = _overlayList.removeAt(index);\n        item.delegate?.didRemove();\n        item.dispose();\n      }\n    });\n  }\n\n  void removeAll() {\n    setState(() {\n      if (_overlayList.isEmpty) {\n        return;\n      }\n\n      final reveredList = _overlayList.reversed.toList();\n      final firstItem = reveredList.removeAt(0);\n      _overlayList.remove(firstItem);\n      if (firstItem.delegate != null) {\n        firstItem.delegate!.didRemove();\n        firstItem.dispose();\n        if (firstItem.delegate!.asBarrier()) {\n          return;\n        }\n      }\n\n      for (final element in reveredList) {\n        if (element.delegate?.asBarrier() ?? false) {\n          return;\n        } else {\n          element.delegate?.didRemove();\n          element.dispose();\n          _overlayList.remove(element);\n        }\n      }\n    });\n  }\n\n  void _markDirty() {\n    if (mounted) {\n      setState(() {});\n    }\n  }\n\n  void _showOverlay({\n    required Widget widget,\n    required String identifier,\n    required bool shouldAnchor,\n    Offset? anchorPosition,\n    Size? anchorSize,\n    AnchorDirection? anchorDirection,\n    BuildContext? anchorContext,\n    Offset? anchorOffset,\n    OverlapBehaviour? overlapBehaviour,\n    FlowyOverlayDelegate? delegate,\n  }) {\n    Widget overlay = widget;\n    final offset = anchorOffset ?? Offset.zero;\n    final focusNode = FocusNode();\n    if (shouldAnchor) {\n      assert(\n        anchorPosition != null || anchorContext != null,\n        'Must provide `anchorPosition` or `anchorContext` to locating overlay.',\n      );\n      Offset targetAnchorPosition = anchorPosition ?? Offset.zero;\n      Size targetAnchorSize = anchorSize ?? Size.zero;\n      if (anchorContext != null) {\n        RenderObject renderObject = anchorContext.findRenderObject()!;\n        assert(\n          renderObject is RenderBox,\n          'Unexpecteded non-RenderBox render object caught.',\n        );\n        final renderBox = renderObject as RenderBox;\n        targetAnchorPosition = renderBox.localToGlobal(Offset.zero);\n        targetAnchorSize = renderBox.size;\n      }\n      final anchorRect = Rect.fromLTWH(\n        targetAnchorPosition.dx + offset.dx,\n        targetAnchorPosition.dy + offset.dy,\n        targetAnchorSize.width,\n        targetAnchorSize.height,\n      );\n\n      overlay = CustomSingleChildLayout(\n        delegate: OverlayLayoutDelegate(\n          anchorRect: anchorRect,\n          anchorDirection:\n              anchorDirection ?? AnchorDirection.rightWithTopAligned,\n          overlapBehaviour: overlapBehaviour ?? OverlapBehaviour.stretch,\n        ),\n        child: Focus(\n            focusNode: focusNode,\n            onKeyEvent: (node, event) {\n              KeyEventResult result = KeyEventResult.ignored;\n              for (final ShortcutActivator activator\n                  in _keyboardShortcutBindings.keys) {\n                if (activator.accepts(event, HardwareKeyboard.instance)) {\n                  _keyboardShortcutBindings[activator]!.call(identifier);\n                  result = KeyEventResult.handled;\n                }\n              }\n              return result;\n            },\n            child: widget),\n      );\n    }\n\n    setState(() {\n      _overlayList.add(OverlayItem(\n        widget: overlay,\n        identifier: identifier,\n        focusNode: focusNode,\n        delegate: delegate,\n      ));\n    });\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _keyboardShortcutBindings.addAll({\n      LogicalKeySet(LogicalKeyboardKey.escape): (identifier) =>\n          remove(identifier),\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final overlays = _overlayList.map((item) {\n      Widget widget = item.widget;\n\n      // requestFocus will cause the children weird focus behaviors.\n      // item.focusNode.requestFocus();\n      if (item.delegate?.asBarrier() ?? false) {\n        widget = Container(\n          color: style.barrierColor,\n          child: GestureDetector(\n            behavior: HitTestBehavior.opaque,\n            onTap: _handleTapOnBackground,\n            child: widget,\n          ),\n        );\n      }\n      return widget;\n    }).toList();\n\n    List<Widget> children = <Widget>[widget.child];\n    Widget? child = _renderBackground(overlays);\n    if (child != null) {\n      children.add(child);\n    }\n\n    // Try to fix there is no overlay for editabletext widget. e.g. TextField.\n    // // Check out the TextSelectionOverlay class in text_selection.dart.\n    // // ...\n    // //  final OverlayState? overlay = Overlay.of(context, rootOverlay: true);\n    // // assert(\n    // //   overlay != null,\n    // //   'No Overlay widget exists above $context.\\n'\n    // //   'Usually the Navigator created by WidgetsApp provides the overlay. Perhaps your '\n    // //   'app content was created above the Navigator with the WidgetsApp builder parameter.',\n    // // );\n    // // ...\n\n    return MaterialApp(\n      theme: Theme.of(context),\n      debugShowCheckedModeBanner: false,\n      home: Stack(children: children..addAll(overlays)),\n    );\n  }\n\n  void _handleTapOnBackground() => removeAll();\n\n  Widget? _renderBackground(List<Widget> overlays) {\n    Widget? child;\n    if (overlays.isNotEmpty) {\n      child = Container(\n        color: style.barrierColor,\n        child: GestureDetector(\n          behavior: HitTestBehavior.opaque,\n          onTap: _handleTapOnBackground,\n        ),\n      );\n\n      if (style.blur) {\n        child = BackdropFilter(\n          filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),\n          child: child,\n        );\n      }\n    }\n    return child;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_popover_layout.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:flutter/material.dart';\n\nimport 'flowy_overlay.dart';\n\nclass PopoverLayoutDelegate extends SingleChildLayoutDelegate {\n  PopoverLayoutDelegate({\n    required this.anchorRect,\n    required this.anchorDirection,\n    required this.overlapBehaviour,\n  });\n\n  final Rect anchorRect;\n  final AnchorDirection anchorDirection;\n  final OverlapBehaviour overlapBehaviour;\n\n  @override\n  bool shouldRelayout(PopoverLayoutDelegate oldDelegate) {\n    return anchorRect != oldDelegate.anchorRect ||\n        anchorDirection != oldDelegate.anchorDirection ||\n        overlapBehaviour != oldDelegate.overlapBehaviour;\n  }\n\n  @override\n  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {\n    switch (overlapBehaviour) {\n      case OverlapBehaviour.none:\n        return constraints.loosen();\n      case OverlapBehaviour.stretch:\n        BoxConstraints childConstraints;\n        switch (anchorDirection) {\n          case AnchorDirection.topLeft:\n            childConstraints = BoxConstraints.loose(Size(\n              anchorRect.left,\n              anchorRect.top,\n            ));\n            break;\n          case AnchorDirection.topRight:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth - anchorRect.right,\n              anchorRect.top,\n            ));\n            break;\n          case AnchorDirection.bottomLeft:\n            childConstraints = BoxConstraints.loose(Size(\n              anchorRect.left,\n              constraints.maxHeight - anchorRect.bottom,\n            ));\n            break;\n          case AnchorDirection.bottomRight:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth - anchorRect.right,\n              constraints.maxHeight - anchorRect.bottom,\n            ));\n            break;\n          case AnchorDirection.center:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth,\n              constraints.maxHeight,\n            ));\n            break;\n          case AnchorDirection.topWithLeftAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth - anchorRect.left,\n              anchorRect.top,\n            ));\n            break;\n          case AnchorDirection.topWithCenterAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth,\n              anchorRect.top,\n            ));\n            break;\n          case AnchorDirection.topWithRightAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              anchorRect.right,\n              anchorRect.top,\n            ));\n            break;\n          case AnchorDirection.rightWithTopAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth - anchorRect.right,\n              constraints.maxHeight - anchorRect.top,\n            ));\n            break;\n          case AnchorDirection.rightWithCenterAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth - anchorRect.right,\n              constraints.maxHeight,\n            ));\n            break;\n          case AnchorDirection.rightWithBottomAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth - anchorRect.right,\n              anchorRect.bottom,\n            ));\n            break;\n          case AnchorDirection.bottomWithLeftAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              anchorRect.left,\n              constraints.maxHeight - anchorRect.bottom,\n            ));\n            break;\n          case AnchorDirection.bottomWithCenterAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth,\n              constraints.maxHeight - anchorRect.bottom,\n            ));\n            break;\n          case AnchorDirection.bottomWithRightAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              anchorRect.right,\n              constraints.maxHeight - anchorRect.bottom,\n            ));\n            break;\n          case AnchorDirection.leftWithTopAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              anchorRect.left,\n              constraints.maxHeight - anchorRect.top,\n            ));\n            break;\n          case AnchorDirection.leftWithCenterAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              anchorRect.left,\n              constraints.maxHeight,\n            ));\n            break;\n          case AnchorDirection.leftWithBottomAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              anchorRect.left,\n              anchorRect.bottom,\n            ));\n            break;\n          case AnchorDirection.custom:\n            childConstraints = constraints.loosen();\n            break;\n        }\n        return childConstraints;\n    }\n  }\n\n  @override\n  Offset getPositionForChild(Size size, Size childSize) {\n    Offset position;\n    switch (anchorDirection) {\n      case AnchorDirection.topLeft:\n        position = Offset(\n          anchorRect.left - childSize.width,\n          anchorRect.top - childSize.height,\n        );\n        break;\n      case AnchorDirection.topRight:\n        position = Offset(\n          anchorRect.right,\n          anchorRect.top - childSize.height,\n        );\n        break;\n      case AnchorDirection.bottomLeft:\n        position = Offset(\n          anchorRect.left - childSize.width,\n          anchorRect.bottom,\n        );\n        break;\n      case AnchorDirection.bottomRight:\n        position = Offset(\n          anchorRect.right,\n          anchorRect.bottom,\n        );\n        break;\n      case AnchorDirection.center:\n        position = anchorRect.center;\n        break;\n      case AnchorDirection.topWithLeftAligned:\n        position = Offset(\n          anchorRect.left,\n          anchorRect.top - childSize.height,\n        );\n        break;\n      case AnchorDirection.topWithCenterAligned:\n        position = Offset(\n          anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0,\n          anchorRect.top - childSize.height,\n        );\n        break;\n      case AnchorDirection.topWithRightAligned:\n        position = Offset(\n          anchorRect.right - childSize.width,\n          anchorRect.top - childSize.height,\n        );\n        break;\n      case AnchorDirection.rightWithTopAligned:\n        position = Offset(anchorRect.right, anchorRect.top);\n        break;\n      case AnchorDirection.rightWithCenterAligned:\n        position = Offset(\n          anchorRect.right,\n          anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0,\n        );\n        break;\n      case AnchorDirection.rightWithBottomAligned:\n        position = Offset(\n          anchorRect.right,\n          anchorRect.bottom - childSize.height,\n        );\n        break;\n      case AnchorDirection.bottomWithLeftAligned:\n        position = Offset(\n          anchorRect.left,\n          anchorRect.bottom,\n        );\n        break;\n      case AnchorDirection.bottomWithCenterAligned:\n        position = Offset(\n          anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0,\n          anchorRect.bottom,\n        );\n        break;\n      case AnchorDirection.bottomWithRightAligned:\n        position = Offset(\n          anchorRect.right - childSize.width,\n          anchorRect.bottom,\n        );\n        break;\n      case AnchorDirection.leftWithTopAligned:\n        position = Offset(\n          anchorRect.left - childSize.width,\n          anchorRect.top,\n        );\n        break;\n      case AnchorDirection.leftWithCenterAligned:\n        position = Offset(\n          anchorRect.left - childSize.width,\n          anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0,\n        );\n        break;\n      case AnchorDirection.leftWithBottomAligned:\n        position = Offset(\n          anchorRect.left - childSize.width,\n          anchorRect.bottom - childSize.height,\n        );\n        break;\n      default:\n        throw UnimplementedError();\n    }\n    return Offset(\n      math.max(0.0, math.min(size.width - childSize.width, position.dx)),\n      math.max(0.0, math.min(size.height - childSize.height, position.dy)),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/layout.dart",
    "content": "import 'dart:math' as math;\n\nimport 'package:flutter/material.dart';\n\nimport 'flowy_overlay.dart';\n\nclass OverlayLayoutDelegate extends SingleChildLayoutDelegate {\n  OverlayLayoutDelegate({\n    required this.anchorRect,\n    required this.anchorDirection,\n    required this.overlapBehaviour,\n  });\n\n  final Rect anchorRect;\n  final AnchorDirection anchorDirection;\n  final OverlapBehaviour overlapBehaviour;\n\n  @override\n  bool shouldRelayout(OverlayLayoutDelegate oldDelegate) {\n    return anchorRect != oldDelegate.anchorRect ||\n        anchorDirection != oldDelegate.anchorDirection ||\n        overlapBehaviour != oldDelegate.overlapBehaviour;\n  }\n\n  @override\n  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {\n    switch (overlapBehaviour) {\n      case OverlapBehaviour.none:\n        return constraints.loosen();\n      case OverlapBehaviour.stretch:\n        BoxConstraints childConstraints;\n        switch (anchorDirection) {\n          case AnchorDirection.topLeft:\n            childConstraints = BoxConstraints.loose(Size(\n              anchorRect.left,\n              anchorRect.top,\n            ));\n            break;\n          case AnchorDirection.topRight:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth - anchorRect.right,\n              anchorRect.top,\n            ));\n            break;\n          case AnchorDirection.bottomLeft:\n            childConstraints = BoxConstraints.loose(Size(\n              anchorRect.left,\n              constraints.maxHeight - anchorRect.bottom,\n            ));\n            break;\n          case AnchorDirection.bottomRight:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth - anchorRect.right,\n              constraints.maxHeight - anchorRect.bottom,\n            ));\n            break;\n          case AnchorDirection.center:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth,\n              constraints.maxHeight,\n            ));\n            break;\n          case AnchorDirection.topWithLeftAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth - anchorRect.left,\n              anchorRect.top,\n            ));\n            break;\n          case AnchorDirection.topWithCenterAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth,\n              anchorRect.top,\n            ));\n            break;\n          case AnchorDirection.topWithRightAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              anchorRect.right,\n              anchorRect.top,\n            ));\n            break;\n          case AnchorDirection.rightWithTopAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth - anchorRect.right,\n              constraints.maxHeight - anchorRect.top,\n            ));\n            break;\n          case AnchorDirection.rightWithCenterAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth - anchorRect.right,\n              constraints.maxHeight,\n            ));\n            break;\n          case AnchorDirection.rightWithBottomAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth - anchorRect.right,\n              anchorRect.bottom,\n            ));\n            break;\n          case AnchorDirection.bottomWithLeftAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              anchorRect.left,\n              constraints.maxHeight - anchorRect.bottom,\n            ));\n            break;\n          case AnchorDirection.bottomWithCenterAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              constraints.maxWidth,\n              constraints.maxHeight - anchorRect.bottom,\n            ));\n            break;\n          case AnchorDirection.bottomWithRightAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              anchorRect.right,\n              constraints.maxHeight - anchorRect.bottom,\n            ));\n            break;\n          case AnchorDirection.leftWithTopAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              anchorRect.left,\n              constraints.maxHeight - anchorRect.top,\n            ));\n            break;\n          case AnchorDirection.leftWithCenterAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              anchorRect.left,\n              constraints.maxHeight,\n            ));\n            break;\n          case AnchorDirection.leftWithBottomAligned:\n            childConstraints = BoxConstraints.loose(Size(\n              anchorRect.left,\n              anchorRect.bottom,\n            ));\n            break;\n          case AnchorDirection.custom:\n            childConstraints = constraints.loosen();\n            break;\n        }\n        return childConstraints;\n    }\n  }\n\n  @override\n  Offset getPositionForChild(Size size, Size childSize) {\n    Offset position;\n    switch (anchorDirection) {\n      case AnchorDirection.topLeft:\n        position = Offset(\n          anchorRect.left - childSize.width,\n          anchorRect.top - childSize.height,\n        );\n        break;\n      case AnchorDirection.topRight:\n        position = Offset(\n          anchorRect.right,\n          anchorRect.top - childSize.height,\n        );\n        break;\n      case AnchorDirection.bottomLeft:\n        position = Offset(\n          anchorRect.left - childSize.width,\n          anchorRect.bottom,\n        );\n        break;\n      case AnchorDirection.bottomRight:\n        position = Offset(\n          anchorRect.right,\n          anchorRect.bottom,\n        );\n        break;\n      case AnchorDirection.center:\n        position = anchorRect.center;\n        break;\n      case AnchorDirection.topWithLeftAligned:\n        position = Offset(\n          anchorRect.left,\n          anchorRect.top - childSize.height,\n        );\n        break;\n      case AnchorDirection.topWithCenterAligned:\n        position = Offset(\n          anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0,\n          anchorRect.top - childSize.height,\n        );\n        break;\n      case AnchorDirection.topWithRightAligned:\n        position = Offset(\n          anchorRect.right - childSize.width,\n          anchorRect.top - childSize.height,\n        );\n        break;\n      case AnchorDirection.rightWithTopAligned:\n        position = Offset(anchorRect.right, anchorRect.top);\n        break;\n      case AnchorDirection.rightWithCenterAligned:\n        position = Offset(\n          anchorRect.right,\n          anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0,\n        );\n        break;\n      case AnchorDirection.rightWithBottomAligned:\n        position = Offset(\n          anchorRect.right,\n          anchorRect.bottom - childSize.height,\n        );\n        break;\n      case AnchorDirection.bottomWithLeftAligned:\n        position = Offset(\n          anchorRect.left,\n          anchorRect.bottom,\n        );\n        break;\n      case AnchorDirection.bottomWithCenterAligned:\n        position = Offset(\n          anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0,\n          anchorRect.bottom,\n        );\n        break;\n      case AnchorDirection.bottomWithRightAligned:\n        position = Offset(\n          anchorRect.right - childSize.width,\n          anchorRect.bottom,\n        );\n        break;\n      case AnchorDirection.leftWithTopAligned:\n        position = Offset(\n          anchorRect.left - childSize.width,\n          anchorRect.top,\n        );\n        break;\n      case AnchorDirection.leftWithCenterAligned:\n        position = Offset(\n          anchorRect.left - childSize.width,\n          anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0,\n        );\n        break;\n      case AnchorDirection.leftWithBottomAligned:\n        position = Offset(\n          anchorRect.left - childSize.width,\n          anchorRect.bottom - childSize.height,\n        );\n        break;\n      default:\n        throw UnimplementedError();\n    }\n    return Offset(\n      math.max(0.0, math.min(size.width - childSize.width, position.dx)),\n      math.max(0.0, math.min(size.height - childSize.height, position.dy)),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart",
    "content": "import 'dart:math';\n\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flowy_infra_ui/style_widget/decoration.dart';\n\nclass ListOverlayFooter {\n  Widget widget;\n  double height;\n  EdgeInsets padding;\n  ListOverlayFooter({\n    required this.widget,\n    required this.height,\n    this.padding = EdgeInsets.zero,\n  });\n}\n\nclass ListOverlay extends StatelessWidget {\n  const ListOverlay({\n    super.key,\n    required this.itemBuilder,\n    this.itemCount = 0,\n    this.controller,\n    this.constraints = const BoxConstraints(),\n    this.footer,\n  });\n\n  final IndexedWidgetBuilder itemBuilder;\n  final int itemCount;\n  final ScrollController? controller;\n  final BoxConstraints constraints;\n  final ListOverlayFooter? footer;\n\n  @override\n  Widget build(BuildContext context) {\n    const padding = EdgeInsets.symmetric(horizontal: 6, vertical: 6);\n    double totalHeight = constraints.minHeight + padding.vertical;\n    if (footer != null) {\n      totalHeight = totalHeight + footer!.height + footer!.padding.vertical;\n    }\n\n    final innerConstraints = BoxConstraints(\n      minHeight: totalHeight,\n      maxHeight: max(constraints.maxHeight, totalHeight),\n      minWidth: constraints.minWidth,\n      maxWidth: constraints.maxWidth,\n    );\n\n    List<Widget> children = [];\n    for (var i = 0; i < itemCount; i++) {\n      children.add(itemBuilder(context, i));\n    }\n\n    return OverlayContainer(\n      constraints: innerConstraints,\n      padding: padding,\n      child: SingleChildScrollView(\n        scrollDirection: Axis.horizontal,\n        child: IntrinsicWidth(\n          child: Column(\n            mainAxisSize: MainAxisSize.max,\n            children: [\n              ...children,\n              if (footer != null)\n                Padding(\n                  padding: footer!.padding,\n                  child: footer!.widget,\n                ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  static void showWithAnchor(\n    BuildContext context, {\n    required String identifier,\n    required IndexedWidgetBuilder itemBuilder,\n    int itemCount = 0,\n    ScrollController? controller,\n    BoxConstraints constraints = const BoxConstraints(),\n    required BuildContext anchorContext,\n    AnchorDirection? anchorDirection,\n    FlowyOverlayDelegate? delegate,\n    OverlapBehaviour? overlapBehaviour,\n    FlowyOverlayStyle? style,\n    Offset? anchorOffset,\n    ListOverlayFooter? footer,\n  }) {\n    FlowyOverlay.of(context).insertWithAnchor(\n      widget: ListOverlay(\n        itemBuilder: itemBuilder,\n        itemCount: itemCount,\n        controller: controller,\n        constraints: constraints,\n        footer: footer,\n      ),\n      identifier: identifier,\n      anchorContext: anchorContext,\n      anchorDirection: anchorDirection,\n      delegate: delegate,\n      overlapBehaviour: overlapBehaviour,\n      anchorOffset: anchorOffset,\n      style: style,\n    );\n  }\n}\n\nconst overlayContainerPadding = EdgeInsets.all(12);\n\nclass OverlayContainer extends StatelessWidget {\n  final Widget child;\n  final BoxConstraints? constraints;\n  final EdgeInsets padding;\n  const OverlayContainer({\n    required this.child,\n    this.constraints,\n    this.padding = overlayContainerPadding,\n    super.key,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Material(\n      type: MaterialType.transparency,\n      child: Container(\n        padding: padding,\n        decoration: FlowyDecoration.decoration(\n          Theme.of(context).colorScheme.surface,\n          Theme.of(context).colorScheme.shadow.withValues(alpha: 0.15),\n        ),\n        constraints: constraints,\n        child: child,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/option_overlay.dart",
    "content": "import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';\nimport 'package:flutter/material.dart';\n\nclass OptionItem {\n  const OptionItem(this.icon, this.title);\n\n  final Icon? icon;\n  final String title;\n}\n\nclass OptionOverlay<T> extends StatelessWidget {\n  const OptionOverlay({\n    super.key,\n    required this.items,\n    this.onHover,\n    this.onTap,\n  });\n\n  final List<T> items;\n  final IndexedValueCallback<T>? onHover;\n  final IndexedValueCallback<T>? onTap;\n\n  static void showWithAnchor<T>(\n    BuildContext context, {\n    required List<T> items,\n    required String identifier,\n    required BuildContext anchorContext,\n    IndexedValueCallback<T>? onHover,\n    IndexedValueCallback<T>? onTap,\n    AnchorDirection? anchorDirection,\n    OverlapBehaviour? overlapBehaviour,\n    FlowyOverlayDelegate? delegate,\n  }) {\n    FlowyOverlay.of(context).insertWithAnchor(\n      widget: OptionOverlay(\n        items: items,\n        onHover: onHover,\n        onTap: onTap,\n      ),\n      identifier: identifier,\n      anchorContext: anchorContext,\n      anchorDirection: anchorDirection,\n      delegate: delegate,\n      overlapBehaviour: overlapBehaviour,\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final List<_OptionListItem> listItems =\n        items.map((e) => _OptionListItem(e)).toList();\n    return ListOverlay(\n      itemBuilder: (context, index) {\n        return MouseRegion(\n          cursor: SystemMouseCursors.click,\n          onHover:\n              onHover != null ? (_) => onHover!(items[index], index) : null,\n          child: GestureDetector(\n            onTap: onTap != null ? () => onTap!(items[index], index) : null,\n            child: listItems[index],\n          ),\n        );\n      },\n      itemCount: listItems.length,\n    );\n  }\n}\n\nclass _OptionListItem<T> extends StatelessWidget {\n  const _OptionListItem(\n    this.value, {\n    super.key,\n  });\n\n  final T value;\n\n  @override\n  Widget build(BuildContext context) {\n    if (T == String || T == OptionItem) {\n      var children = <Widget>[];\n      if (value is String) {\n        children = [\n          Text(value as String),\n        ];\n      } else if (value is OptionItem) {\n        final optionItem = value as OptionItem;\n        children = [\n          if (optionItem.icon != null) optionItem.icon!,\n          Text(optionItem.title),\n        ];\n      }\n      return Column(\n        mainAxisSize: MainAxisSize.min,\n        children: children,\n      );\n    }\n    throw UnimplementedError('The type $T is not supported by option list.');\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/focus/auto_unfocus_overlay.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass AutoUnfocus extends StatelessWidget {\n  const AutoUnfocus({\n    super.key,\n    required this.child,\n  });\n\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return GestureDetector(\n      onTap: () => _unfocusWidget(context),\n      child: child,\n    );\n  }\n\n  void _unfocusWidget(BuildContext context) {\n    final focusing = FocusScope.of(context);\n\n    if (!focusing.hasPrimaryFocus && focusing.hasFocus) {\n      FocusManager.instance.primaryFocus?.unfocus();\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/keyboard/keyboard_visibility_detector.dart",
    "content": "import 'dart:async';\n\nimport 'package:flowy_infra_ui_platform_interface/flowy_infra_ui_platform_interface.dart';\nimport 'package:flutter/material.dart';\n\nclass KeyboardVisibilityDetector extends StatefulWidget {\n  const KeyboardVisibilityDetector({\n    super.key,\n    required this.child,\n    this.onKeyboardVisibilityChange,\n  });\n\n  final Widget child;\n  final void Function(bool)? onKeyboardVisibilityChange;\n\n  @override\n  State<KeyboardVisibilityDetector> createState() =>\n      _KeyboardVisibilityDetectorState();\n}\n\nclass _KeyboardVisibilityDetectorState\n    extends State<KeyboardVisibilityDetector> {\n  FlowyInfraUIPlatform get _platform => FlowyInfraUIPlatform.instance;\n\n  bool isObserving = false;\n  bool isKeyboardVisible = false;\n  late StreamSubscription _keyboardSubscription;\n\n  @override\n  void initState() {\n    super.initState();\n    _keyboardSubscription =\n        _platform.onKeyboardVisibilityChange.listen((newValue) {\n      setState(() {\n        isKeyboardVisible = newValue;\n        if (widget.onKeyboardVisibilityChange != null) {\n          widget.onKeyboardVisibilityChange!(newValue);\n        }\n      });\n    });\n  }\n\n  @override\n  void dispose() {\n    _keyboardSubscription.cancel();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return _KeyboardVisibilityDetectorInheritedWidget(\n      isKeyboardVisible: isKeyboardVisible,\n      child: widget.child,\n    );\n  }\n}\n\nclass _KeyboardVisibilityDetectorInheritedWidget extends InheritedWidget {\n  const _KeyboardVisibilityDetectorInheritedWidget({\n    required this.isKeyboardVisible,\n    required super.child,\n  });\n\n  final bool isKeyboardVisible;\n\n  @override\n  bool updateShouldNotify(\n      _KeyboardVisibilityDetectorInheritedWidget oldWidget) {\n    return isKeyboardVisible != oldWidget.isKeyboardVisible;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/bar_title.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass FlowyBarTitle extends StatelessWidget {\n  final String title;\n\n  const FlowyBarTitle({\n    super.key,\n    required this.title,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return Text(\n      title,\n      style: const TextStyle(fontSize: 24),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart",
    "content": "import 'dart:io';\n\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';\nimport 'package:flowy_infra_ui/widget/spacing.dart';\nimport 'package:flutter/material.dart';\n\nclass FlowyIconTextButton extends StatelessWidget {\n  final Widget Function(bool onHover) textBuilder;\n  final VoidCallback? onTap;\n  final VoidCallback? onSecondaryTap;\n  final void Function(bool)? onHover;\n  final EdgeInsets? margin;\n  final Widget? Function(bool onHover)? leftIconBuilder;\n  final Widget? Function(bool onHover)? rightIconBuilder;\n  final Color? hoverColor;\n  final bool isSelected;\n  final BorderRadius? radius;\n  final BoxDecoration? decoration;\n  final bool useIntrinsicWidth;\n  final bool disable;\n  final double disableOpacity;\n  final Size? leftIconSize;\n  final bool expandText;\n  final MainAxisAlignment mainAxisAlignment;\n  final bool showDefaultBoxDecorationOnMobile;\n  final double iconPadding;\n  final bool expand;\n  final Color? borderColor;\n  final bool resetHoverOnRebuild;\n\n  const FlowyIconTextButton({\n    super.key,\n    required this.textBuilder,\n    this.onTap,\n    this.onSecondaryTap,\n    this.onHover,\n    this.margin,\n    this.leftIconBuilder,\n    this.rightIconBuilder,\n    this.hoverColor,\n    this.isSelected = false,\n    this.radius,\n    this.decoration,\n    this.useIntrinsicWidth = false,\n    this.disable = false,\n    this.disableOpacity = 0.5,\n    this.leftIconSize = const Size.square(16),\n    this.expandText = true,\n    this.mainAxisAlignment = MainAxisAlignment.center,\n    this.showDefaultBoxDecorationOnMobile = false,\n    this.iconPadding = 6,\n    this.expand = false,\n    this.borderColor,\n    this.resetHoverOnRebuild = true,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final color = hoverColor ?? Theme.of(context).colorScheme.secondary;\n\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: disable ? null : onTap,\n      onSecondaryTap: disable ? null : onSecondaryTap,\n      child: FlowyHover(\n        resetHoverOnRebuild: resetHoverOnRebuild,\n        cursor:\n            disable ? SystemMouseCursors.forbidden : SystemMouseCursors.click,\n        style: HoverStyle(\n          borderRadius: radius ?? Corners.s6Border,\n          hoverColor: color,\n          border: borderColor == null ? null : Border.all(color: borderColor!),\n        ),\n        onHover: disable ? null : onHover,\n        isSelected: () => isSelected,\n        builder: (context, onHover) => _render(context, onHover),\n      ),\n    );\n  }\n\n  Widget _render(BuildContext context, bool onHover) {\n    final List<Widget> children = [];\n\n    final Widget? leftIcon = leftIconBuilder?.call(onHover);\n    if (leftIcon != null) {\n      children.add(\n        SizedBox.fromSize(\n          size: leftIconSize,\n          child: leftIcon,\n        ),\n      );\n      children.add(HSpace(iconPadding));\n    }\n\n    if (expandText) {\n      children.add(Expanded(child: textBuilder(onHover)));\n    } else {\n      children.add(textBuilder(onHover));\n    }\n\n    final Widget? rightIcon = rightIconBuilder?.call(onHover);\n    if (rightIcon != null) {\n      children.add(HSpace(iconPadding));\n      // No need to define the size of rightIcon. Just use its intrinsic width\n      children.add(rightIcon);\n    }\n\n    Widget child = Row(\n      mainAxisAlignment: mainAxisAlignment,\n      crossAxisAlignment: CrossAxisAlignment.center,\n      mainAxisSize: expand ? MainAxisSize.max : MainAxisSize.min,\n      children: children,\n    );\n\n    if (useIntrinsicWidth) {\n      child = IntrinsicWidth(child: child);\n    }\n\n    final decoration = this.decoration ??\n        (showDefaultBoxDecorationOnMobile &&\n                (Platform.isIOS || Platform.isAndroid)\n            ? BoxDecoration(\n                border: Border.all(\n                color: borderColor ??\n                    Theme.of(context).colorScheme.surfaceContainerHighest,\n                width: 1.0,\n              ))\n            : null);\n\n    return Container(\n      decoration: decoration,\n      child: Padding(\n        padding:\n            margin ?? const EdgeInsets.symmetric(horizontal: 6, vertical: 4),\n        child: child,\n      ),\n    );\n  }\n}\n\nclass FlowyButton extends StatelessWidget {\n  final Widget text;\n  final VoidCallback? onTap;\n  final VoidCallback? onSecondaryTap;\n  final void Function(bool)? onHover;\n  final EdgeInsetsGeometry? margin;\n  final Widget? leftIcon;\n  final Widget? rightIcon;\n  final Color? hoverColor;\n  final bool isSelected;\n  final BorderRadius? radius;\n  final BoxDecoration? decoration;\n  final bool useIntrinsicWidth;\n  final bool disable;\n  final double disableOpacity;\n  final Size? leftIconSize;\n  final bool expandText;\n  final MainAxisAlignment mainAxisAlignment;\n  final bool showDefaultBoxDecorationOnMobile;\n  final double iconPadding;\n  final bool expand;\n  final Color? borderColor;\n  final Color? backgroundColor;\n  final bool resetHoverOnRebuild;\n\n  const FlowyButton({\n    super.key,\n    required this.text,\n    this.onTap,\n    this.onSecondaryTap,\n    this.onHover,\n    this.margin,\n    this.leftIcon,\n    this.rightIcon,\n    this.hoverColor,\n    this.isSelected = false,\n    this.radius,\n    this.decoration,\n    this.useIntrinsicWidth = false,\n    this.disable = false,\n    this.disableOpacity = 0.5,\n    this.leftIconSize = const Size.square(16),\n    this.expandText = true,\n    this.mainAxisAlignment = MainAxisAlignment.center,\n    this.showDefaultBoxDecorationOnMobile = false,\n    this.iconPadding = 6,\n    this.expand = false,\n    this.borderColor,\n    this.backgroundColor,\n    this.resetHoverOnRebuild = true,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final color = hoverColor ?? Theme.of(context).colorScheme.secondary;\n    final alpha = (255 * disableOpacity).toInt();\n    color.withAlpha(alpha);\n\n    if (Platform.isIOS || Platform.isAndroid) {\n      return InkWell(\n        splashFactory: Platform.isIOS ? NoSplash.splashFactory : null,\n        onTap: disable ? null : onTap,\n        onSecondaryTap: disable ? null : onSecondaryTap,\n        borderRadius: radius ?? Corners.s6Border,\n        child: _render(context),\n      );\n    }\n\n    return GestureDetector(\n      behavior: HitTestBehavior.opaque,\n      onTap: disable ? null : onTap,\n      onSecondaryTap: disable ? null : onSecondaryTap,\n      child: FlowyHover(\n        resetHoverOnRebuild: resetHoverOnRebuild,\n        cursor:\n            disable ? SystemMouseCursors.forbidden : SystemMouseCursors.click,\n        style: HoverStyle(\n          borderRadius: radius ?? Corners.s6Border,\n          hoverColor: color,\n          border: borderColor == null ? null : Border.all(color: borderColor!),\n          backgroundColor: backgroundColor ?? Colors.transparent,\n        ),\n        onHover: disable ? null : onHover,\n        isSelected: () => isSelected,\n        builder: (context, onHover) => _render(context),\n      ),\n    );\n  }\n\n  Widget _render(BuildContext context) {\n    final List<Widget> children = [];\n\n    if (leftIcon != null) {\n      children.add(\n        SizedBox.fromSize(\n          size: leftIconSize,\n          child: leftIcon!,\n        ),\n      );\n      children.add(HSpace(iconPadding));\n    }\n\n    if (expandText) {\n      children.add(Expanded(child: text));\n    } else {\n      children.add(text);\n    }\n\n    if (rightIcon != null) {\n      children.add(HSpace(iconPadding));\n      // No need to define the size of rightIcon. Just use its intrinsic width\n      children.add(rightIcon!);\n    }\n\n    Widget child = Row(\n      mainAxisAlignment: mainAxisAlignment,\n      crossAxisAlignment: CrossAxisAlignment.center,\n      mainAxisSize: expand ? MainAxisSize.max : MainAxisSize.min,\n      children: children,\n    );\n\n    if (useIntrinsicWidth) {\n      child = IntrinsicWidth(child: child);\n    }\n\n    var decoration = this.decoration;\n\n    if (decoration == null &&\n        (showDefaultBoxDecorationOnMobile &&\n            (Platform.isIOS || Platform.isAndroid))) {\n      decoration = BoxDecoration(\n        color: backgroundColor ?? Theme.of(context).colorScheme.surface,\n      );\n    }\n\n    if (decoration == null && (Platform.isIOS || Platform.isAndroid)) {\n      if (showDefaultBoxDecorationOnMobile) {\n        decoration = BoxDecoration(\n          border: Border.all(\n            color: borderColor ?? Theme.of(context).colorScheme.outline,\n            width: 1.0,\n          ),\n          borderRadius: radius,\n        );\n      } else if (backgroundColor != null) {\n        decoration = BoxDecoration(\n          color: backgroundColor,\n          borderRadius: radius,\n        );\n      }\n    }\n\n    return Container(\n      decoration: decoration,\n      child: Padding(\n        padding: margin ??\n            const EdgeInsets.symmetric(\n              horizontal: 6,\n              vertical: 4,\n            ),\n        child: child,\n      ),\n    );\n  }\n}\n\nclass FlowyTextButton extends StatelessWidget {\n  const FlowyTextButton(\n    this.text, {\n    super.key,\n    this.onPressed,\n    this.fontSize,\n    this.fontColor,\n    this.fontHoverColor,\n    this.overflow = TextOverflow.ellipsis,\n    this.fontWeight,\n    this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6),\n    this.hoverColor,\n    this.fillColor,\n    this.heading,\n    this.radius,\n    this.mainAxisAlignment = MainAxisAlignment.start,\n    this.tooltip,\n    this.constraints = const BoxConstraints(minWidth: 0.0, minHeight: 0.0),\n    this.decoration,\n    this.fontFamily,\n    this.isDangerous = false,\n    this.borderColor,\n    this.lineHeight,\n  });\n\n  factory FlowyTextButton.primary({\n    required BuildContext context,\n    required String text,\n    VoidCallback? onPressed,\n  }) =>\n      FlowyTextButton(\n        text,\n        constraints: const BoxConstraints(minHeight: 32),\n        fillColor: Theme.of(context).colorScheme.primary,\n        hoverColor: const Color(0xFF005483),\n        fontColor: Theme.of(context).colorScheme.onPrimary,\n        fontHoverColor: Colors.white,\n        onPressed: onPressed,\n      );\n\n  factory FlowyTextButton.secondary({\n    required BuildContext context,\n    required String text,\n    VoidCallback? onPressed,\n  }) =>\n      FlowyTextButton(\n        text,\n        constraints: const BoxConstraints(minHeight: 32),\n        fillColor: Colors.transparent,\n        hoverColor: Theme.of(context).colorScheme.primary,\n        fontColor: Theme.of(context).colorScheme.primary,\n        borderColor: Theme.of(context).colorScheme.primary,\n        fontHoverColor: Colors.white,\n        onPressed: onPressed,\n      );\n\n  final String text;\n  final FontWeight? fontWeight;\n  final Color? fontColor;\n  final Color? fontHoverColor;\n  final double? fontSize;\n  final TextOverflow overflow;\n\n  final VoidCallback? onPressed;\n  final EdgeInsets padding;\n  final Widget? heading;\n  final Color? hoverColor;\n  final Color? fillColor;\n  final BorderRadius? radius;\n  final MainAxisAlignment mainAxisAlignment;\n  final String? tooltip;\n  final BoxConstraints constraints;\n\n  final TextDecoration? decoration;\n\n  final String? fontFamily;\n  final bool isDangerous;\n  final Color? borderColor;\n  final double? lineHeight;\n\n  @override\n  Widget build(BuildContext context) {\n    List<Widget> children = [];\n    if (heading != null) {\n      children.add(heading!);\n      children.add(const HSpace(8));\n    }\n    children.add(Text(\n      text,\n      overflow: overflow,\n      textAlign: TextAlign.center,\n    ));\n\n    Widget child = Row(\n      crossAxisAlignment: CrossAxisAlignment.center,\n      mainAxisAlignment: mainAxisAlignment,\n      children: children,\n    );\n\n    child = ConstrainedBox(\n      constraints: constraints,\n      child: TextButton(\n        onPressed: onPressed,\n        focusNode: FocusNode(skipTraversal: onPressed == null),\n        style: ButtonStyle(\n          overlayColor: const WidgetStatePropertyAll(Colors.transparent),\n          splashFactory: NoSplash.splashFactory,\n          tapTargetSize: MaterialTapTargetSize.shrinkWrap,\n          padding: WidgetStateProperty.all(padding),\n          elevation: WidgetStateProperty.all(0),\n          shape: WidgetStateProperty.all(\n            RoundedRectangleBorder(\n              side: BorderSide(\n                color: borderColor ??\n                    (isDangerous\n                        ? Theme.of(context).colorScheme.error\n                        : Colors.transparent),\n              ),\n              borderRadius: radius ?? Corners.s6Border,\n            ),\n          ),\n          textStyle: WidgetStateProperty.all(\n            Theme.of(context).textTheme.bodyMedium?.copyWith(\n                  fontWeight: fontWeight ?? FontWeight.w500,\n                  fontSize: fontSize,\n                  color: fontColor ?? Theme.of(context).colorScheme.onPrimary,\n                  decoration: decoration,\n                  fontFamily: fontFamily,\n                  height: lineHeight ?? 1.1,\n                ),\n          ),\n          backgroundColor: WidgetStateProperty.resolveWith(\n            (states) {\n              if (states.contains(WidgetState.hovered)) {\n                return hoverColor ??\n                    (isDangerous\n                        ? Theme.of(context).colorScheme.error\n                        : Theme.of(context).colorScheme.secondary);\n              }\n\n              return fillColor ??\n                  (isDangerous\n                      ? Colors.transparent\n                      : Theme.of(context).colorScheme.secondaryContainer);\n            },\n          ),\n          foregroundColor: WidgetStateProperty.resolveWith(\n            (states) {\n              if (states.contains(WidgetState.hovered)) {\n                return fontHoverColor ??\n                    (fontColor ?? Theme.of(context).colorScheme.onSurface);\n              }\n\n              return fontColor ?? Theme.of(context).colorScheme.onSurface;\n            },\n          ),\n        ),\n        child: child,\n      ),\n    );\n\n    if (tooltip != null) {\n      child = FlowyTooltip(message: tooltip!, child: child);\n    }\n\n    if (onPressed == null) {\n      child = ExcludeFocus(child: child);\n    }\n\n    return child;\n  }\n}\n\nclass FlowyRichTextButton extends StatelessWidget {\n  const FlowyRichTextButton(\n    this.text, {\n    super.key,\n    this.onPressed,\n    this.overflow = TextOverflow.ellipsis,\n    this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6),\n    this.hoverColor,\n    this.fillColor,\n    this.heading,\n    this.radius,\n    this.mainAxisAlignment = MainAxisAlignment.start,\n    this.tooltip,\n    this.constraints = const BoxConstraints(minWidth: 58.0, minHeight: 30.0),\n    this.decoration,\n  });\n\n  final InlineSpan text;\n  final TextOverflow overflow;\n\n  final VoidCallback? onPressed;\n  final EdgeInsets padding;\n  final Widget? heading;\n  final Color? hoverColor;\n  final Color? fillColor;\n  final BorderRadius? radius;\n  final MainAxisAlignment mainAxisAlignment;\n  final String? tooltip;\n  final BoxConstraints constraints;\n\n  final TextDecoration? decoration;\n\n  @override\n  Widget build(BuildContext context) {\n    List<Widget> children = [];\n    if (heading != null) {\n      children.add(heading!);\n      children.add(const HSpace(6));\n    }\n    children.add(\n      RichText(text: text, overflow: overflow, textAlign: TextAlign.center),\n    );\n\n    Widget child = Padding(\n      padding: padding,\n      child: Row(\n        crossAxisAlignment: CrossAxisAlignment.center,\n        mainAxisAlignment: mainAxisAlignment,\n        children: children,\n      ),\n    );\n\n    child = RawMaterialButton(\n      hoverElevation: 0,\n      highlightElevation: 0,\n      shape: RoundedRectangleBorder(borderRadius: radius ?? Corners.s6Border),\n      fillColor: fillColor ?? Theme.of(context).colorScheme.secondaryContainer,\n      hoverColor: hoverColor ?? Theme.of(context).colorScheme.secondary,\n      focusColor: Colors.transparent,\n      splashColor: Colors.transparent,\n      highlightColor: Colors.transparent,\n      elevation: 0,\n      constraints: constraints,\n      onPressed: () {},\n      child: child,\n    );\n\n    child = IgnoreParentGestureWidget(onPress: onPressed, child: child);\n\n    if (tooltip != null) {\n      child = FlowyTooltip(message: tooltip!, child: child);\n    }\n\n    return child;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/close_button.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass FlowyCloseButton extends StatelessWidget {\n  final VoidCallback? onPressed;\n\n  const FlowyCloseButton({\n    super.key,\n    this.onPressed,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return TextButton(onPressed: onPressed, child: const Icon(Icons.close));\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/color_picker.dart",
    "content": "import 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flowy_svg/flowy_svg.dart';\nimport 'package:flutter/material.dart';\n\nclass FlowyColorOption {\n  const FlowyColorOption({\n    required this.color,\n    required this.i18n,\n    required this.id,\n  });\n\n  final Color color;\n  final String i18n;\n  final String id;\n}\n\nclass FlowyColorPicker extends StatelessWidget {\n  final List<FlowyColorOption> colors;\n  final Color? selected;\n  final Function(FlowyColorOption option, int index)? onTap;\n  final double separatorSize;\n  final double iconSize;\n  final double itemHeight;\n  final Border? border;\n\n  const FlowyColorPicker({\n    super.key,\n    required this.colors,\n    this.selected,\n    this.onTap,\n    this.separatorSize = 4,\n    this.iconSize = 16,\n    this.itemHeight = 32,\n    this.border,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return ListView.separated(\n      shrinkWrap: true,\n      separatorBuilder: (context, index) {\n        return VSpace(separatorSize);\n      },\n      itemCount: colors.length,\n      physics: StyledScrollPhysics(),\n      itemBuilder: (BuildContext context, int index) {\n        return _buildColorOption(colors[index], index);\n      },\n    );\n  }\n\n  Widget _buildColorOption(\n    FlowyColorOption option,\n    int i,\n  ) {\n    Widget? checkmark;\n    if (selected == option.color) {\n      checkmark = const FlowySvg(FlowySvgData(\"grid/checkmark\"));\n    }\n\n    final colorIcon = ColorOptionIcon(\n      color: option.color,\n      iconSize: iconSize,\n    );\n\n    return SizedBox(\n      height: itemHeight,\n      child: FlowyButton(\n        text: FlowyText(option.i18n),\n        leftIcon: colorIcon,\n        rightIcon: checkmark,\n        iconPadding: 10,\n        onTap: () {\n          onTap?.call(option, i);\n        },\n      ),\n    );\n  }\n}\n\nclass ColorOptionIcon extends StatelessWidget {\n  const ColorOptionIcon({\n    super.key,\n    required this.color,\n    this.iconSize = 16.0,\n  });\n\n  final Color color;\n  final double iconSize;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox.square(\n      dimension: iconSize,\n      child: DecoratedBox(\n        decoration: BoxDecoration(\n          color: color,\n          shape: BoxShape.circle,\n          border: color == Colors.transparent\n              ? Border.all(color: const Color(0xFFCFD3D9))\n              : null,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/container.dart",
    "content": "import 'package:flowy_infra/time/duration.dart';\nimport 'package:flutter/material.dart';\n\nclass FlowyContainer extends StatelessWidget {\n  final Color color;\n  final BorderRadiusGeometry? borderRadius;\n  final List<BoxShadow>? shadows;\n  final Widget? child;\n  final double? width;\n  final double? height;\n  final Alignment? align;\n  final EdgeInsets? margin;\n  final Duration? duration;\n  final BoxBorder? border;\n\n  const FlowyContainer(this.color,\n      {super.key,\n      this.borderRadius,\n      this.shadows,\n      this.child,\n      this.width,\n      this.height,\n      this.align,\n      this.margin,\n      this.duration,\n      this.border});\n\n  @override\n  Widget build(BuildContext context) {\n    return AnimatedContainer(\n        width: width,\n        height: height,\n        margin: margin,\n        alignment: align,\n        duration: duration ?? FlowyDurations.medium,\n        decoration: BoxDecoration(\n            color: color,\n            borderRadius: borderRadius,\n            boxShadow: shadows,\n            border: border),\n        child: child);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/decoration.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass FlowyDecoration {\n  static Decoration decoration(\n    Color boxColor,\n    Color boxShadow, {\n    double spreadRadius = 0,\n    double blurRadius = 20,\n    Offset offset = Offset.zero,\n    double borderRadius = 6,\n    BoxBorder? border,\n  }) {\n    return BoxDecoration(\n      color: boxColor,\n      borderRadius: BorderRadius.all(Radius.circular(borderRadius)),\n      boxShadow: [\n        BoxShadow(\n          color: boxShadow,\n          spreadRadius: spreadRadius,\n          blurRadius: blurRadius,\n          offset: offset,\n        ),\n      ],\n      border: border,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/divider.dart",
    "content": "import 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\n\nclass FlowyDivider extends StatelessWidget {\n  const FlowyDivider({\n    super.key,\n    this.padding,\n  });\n\n  final EdgeInsets? padding;\n\n  @override\n  Widget build(BuildContext context) {\n    return Padding(\n      padding: padding ?? EdgeInsets.zero,\n      child: Divider(\n        height: 1.0,\n        thickness: 1.0,\n        color: AFThemeExtension.of(context).borderColor,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/extension.dart",
    "content": "import 'package:flutter/material.dart';\nexport 'package:styled_widget/styled_widget.dart';\n\nclass TopBorder extends StatelessWidget {\n  const TopBorder({\n    super.key,\n    this.width = 1.0,\n    this.color = Colors.grey,\n    required this.child,\n  });\n\n  final Widget child;\n  final double width;\n  final Color color;\n\n  @override\n  Widget build(BuildContext context) {\n    return DecoratedBox(\n      decoration: BoxDecoration(\n        border: Border(\n          top: BorderSide(width: width, color: color),\n        ),\n      ),\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/hover.dart",
    "content": "import 'package:flutter/material.dart';\n\ntypedef HoverBuilder = Widget Function(BuildContext context, bool onHover);\n\nclass FlowyHover extends StatefulWidget {\n  final HoverStyle? style;\n  final HoverBuilder? builder;\n  final Widget? child;\n\n  final bool Function()? isSelected;\n  final void Function(bool)? onHover;\n  final MouseCursor? cursor;\n\n  /// Reset the hover state when the parent widget get rebuild.\n  /// Default to true.\n  final bool resetHoverOnRebuild;\n\n  /// Determined whether the [builder] should get called when onEnter/onExit\n  /// happened\n  ///\n  /// [FlowyHover] show hover when [MouseRegion]'s onEnter get called\n  /// [FlowyHover] hide hover when [MouseRegion]'s onExit get called\n  ///\n  final bool Function()? buildWhenOnHover;\n\n  const FlowyHover({\n    super.key,\n    this.builder,\n    this.child,\n    this.style,\n    this.isSelected,\n    this.onHover,\n    this.cursor,\n    this.resetHoverOnRebuild = true,\n    this.buildWhenOnHover,\n  });\n\n  @override\n  State<FlowyHover> createState() => _FlowyHoverState();\n}\n\nclass _FlowyHoverState extends State<FlowyHover> {\n  bool _onHover = false;\n\n  @override\n  void didUpdateWidget(covariant FlowyHover oldWidget) {\n    if (widget.resetHoverOnRebuild) {\n      // Reset the _onHover to false when the parent widget get rebuild.\n      _onHover = false;\n    }\n\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      cursor: widget.cursor != null ? widget.cursor! : SystemMouseCursors.click,\n      opaque: false,\n      onHover: (_) => _setOnHover(true),\n      onEnter: (_) => _setOnHover(true),\n      onExit: (_) => _setOnHover(false),\n      child: FlowyHoverContainer(\n        style: widget.style ??\n            HoverStyle(hoverColor: Theme.of(context).colorScheme.secondary),\n        applyStyle: _onHover || (widget.isSelected?.call() ?? false),\n        child: widget.child ?? widget.builder!(context, _onHover),\n      ),\n    );\n  }\n\n  void _setOnHover(bool isHovering) {\n    if (isHovering == _onHover) return;\n\n    if (widget.buildWhenOnHover?.call() ?? true) {\n      setState(() => _onHover = isHovering);\n      if (widget.onHover != null) {\n        widget.onHover!(isHovering);\n      }\n    }\n  }\n}\n\nclass HoverStyle {\n  final BoxBorder? border;\n  final Color? hoverColor;\n  final Color? foregroundColorOnHover;\n  final BorderRadius borderRadius;\n  final EdgeInsets contentMargin;\n  final Color backgroundColor;\n\n  const HoverStyle({\n    this.border,\n    this.borderRadius = const BorderRadius.all(Radius.circular(6)),\n    this.contentMargin = EdgeInsets.zero,\n    this.backgroundColor = Colors.transparent,\n    this.hoverColor,\n    this.foregroundColorOnHover,\n  });\n\n  const HoverStyle.transparent({\n    this.borderRadius = const BorderRadius.all(Radius.circular(6)),\n    this.contentMargin = EdgeInsets.zero,\n    this.backgroundColor = Colors.transparent,\n    this.foregroundColorOnHover,\n  })  : hoverColor = Colors.transparent,\n        border = null;\n}\n\nclass FlowyHoverContainer extends StatelessWidget {\n  final HoverStyle style;\n  final Widget child;\n  final bool applyStyle;\n\n  const FlowyHoverContainer({\n    super.key,\n    required this.child,\n    required this.style,\n    this.applyStyle = false,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = Theme.of(context);\n    final textTheme = theme.textTheme;\n    final iconTheme = theme.iconTheme;\n    // override text's theme with foregroundColorOnHover when it is hovered\n    final hoverTheme = theme.copyWith(\n      textTheme: textTheme.copyWith(\n        bodyMedium: textTheme.bodyMedium?.copyWith(\n          color: style.foregroundColorOnHover ?? theme.colorScheme.onSurface,\n        ),\n      ),\n      iconTheme: iconTheme.copyWith(\n        color: style.foregroundColorOnHover ?? theme.colorScheme.onSurface,\n      ),\n    );\n\n    return Container(\n      margin: style.contentMargin,\n      decoration: BoxDecoration(\n        border: style.border,\n        color: applyStyle\n            ? style.hoverColor ?? Theme.of(context).colorScheme.secondary\n            : style.backgroundColor,\n        borderRadius: style.borderRadius,\n      ),\n      child: Theme(\n        data: applyStyle ? hoverTheme : Theme.of(context),\n        child: child,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\n\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/style_widget/hover.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flowy_svg/flowy_svg.dart';\n\nclass FlowyIconButton extends StatelessWidget {\n  final double width;\n  final double? height;\n  final Widget icon;\n  final VoidCallback? onPressed;\n  final Color? fillColor;\n  final Color? hoverColor;\n  final Color? iconColorOnHover;\n  final EdgeInsets iconPadding;\n  final BorderRadius? radius;\n  final String? tooltipText;\n  final InlineSpan? richTooltipText;\n  final bool preferBelow;\n  final BoxDecoration? decoration;\n  final bool? isSelected;\n\n  const FlowyIconButton({\n    super.key,\n    this.width = 30,\n    this.height,\n    this.onPressed,\n    this.fillColor = Colors.transparent,\n    this.hoverColor,\n    this.iconColorOnHover,\n    this.iconPadding = EdgeInsets.zero,\n    this.radius,\n    this.decoration,\n    this.tooltipText,\n    this.richTooltipText,\n    this.preferBelow = true,\n    this.isSelected,\n    required this.icon,\n  }) : assert((richTooltipText != null && tooltipText == null) ||\n            (richTooltipText == null && tooltipText != null) ||\n            (richTooltipText == null && tooltipText == null));\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child = icon;\n    final size = Size(width, height ?? width);\n\n    final tooltipMessage =\n        tooltipText == null && richTooltipText == null ? '' : tooltipText;\n\n    assert(size.width > iconPadding.horizontal);\n    assert(size.height > iconPadding.vertical);\n\n    child = Padding(\n      padding: iconPadding,\n      child: Center(child: child),\n    );\n\n    if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {\n      child = FlowyHover(\n        isSelected: isSelected != null ? () => isSelected! : null,\n        style: HoverStyle(\n            hoverColor: hoverColor,\n            foregroundColorOnHover:\n                iconColorOnHover ?? Theme.of(context).iconTheme.color,\n            borderRadius: radius ?? Corners.s6Border\n            //Do not set background here. Use [fillColor] instead.\n            ),\n        resetHoverOnRebuild: false,\n        child: child,\n      );\n    }\n\n    return Container(\n      constraints: BoxConstraints.tightFor(\n        width: size.width,\n        height: size.height,\n      ),\n      decoration: decoration,\n      child: FlowyTooltip(\n        preferBelow: preferBelow,\n        message: tooltipMessage,\n        richMessage: richTooltipText,\n        child: RawMaterialButton(\n          clipBehavior: Clip.antiAlias,\n          hoverElevation: 0,\n          highlightElevation: 0,\n          shape:\n              RoundedRectangleBorder(borderRadius: radius ?? Corners.s6Border),\n          fillColor: fillColor,\n          hoverColor: Colors.transparent,\n          focusColor: Colors.transparent,\n          splashColor: Colors.transparent,\n          highlightColor: Colors.transparent,\n          elevation: 0,\n          onPressed: onPressed,\n          child: child,\n        ),\n      ),\n    );\n  }\n}\n\nclass FlowyDropdownButton extends StatelessWidget {\n  const FlowyDropdownButton({super.key, this.onPressed});\n\n  final VoidCallback? onPressed;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyIconButton(\n      width: 16,\n      onPressed: onPressed,\n      icon: const FlowySvg(FlowySvgData(\"home/drop_down_show\")),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/image_icon.dart",
    "content": "import 'package:flowy_infra/size.dart';\nimport 'package:flutter/material.dart';\n\nclass FlowyImageIcon extends StatelessWidget {\n  final AssetImage image;\n  final Color? color;\n  final double? size;\n\n  const FlowyImageIcon(this.image, {super.key, this.color, this.size});\n\n  @override\n  Widget build(BuildContext context) {\n    return ImageIcon(image,\n        size: size ?? Sizes.iconMed, color: color ?? Colors.white);\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/primary_rounded_button.dart",
    "content": "import 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\n\nclass PrimaryRoundedButton extends StatelessWidget {\n  const PrimaryRoundedButton({\n    super.key,\n    required this.text,\n    this.fontSize,\n    this.fontWeight,\n    this.color,\n    this.radius,\n    this.margin,\n    this.onTap,\n    this.hoverColor,\n    this.backgroundColor,\n    this.useIntrinsicWidth = true,\n    this.lineHeight,\n    this.figmaLineHeight,\n    this.leftIcon,\n    this.textColor,\n  });\n\n  final String text;\n  final double? fontSize;\n  final FontWeight? fontWeight;\n  final Color? color;\n  final double? radius;\n  final EdgeInsets? margin;\n  final VoidCallback? onTap;\n  final Color? hoverColor;\n  final Color? backgroundColor;\n  final bool useIntrinsicWidth;\n  final double? lineHeight;\n  final double? figmaLineHeight;\n  final Widget? leftIcon;\n  final Color? textColor;\n\n  @override\n  Widget build(BuildContext context) {\n    return FlowyButton(\n      useIntrinsicWidth: useIntrinsicWidth,\n      leftIcon: leftIcon,\n      text: FlowyText(\n        text,\n        fontSize: fontSize ?? 14.0,\n        fontWeight: fontWeight ?? FontWeight.w500,\n        lineHeight: lineHeight ?? 1.0,\n        figmaLineHeight: figmaLineHeight,\n        color: textColor ?? Theme.of(context).colorScheme.onPrimary,\n        textAlign: TextAlign.center,\n        overflow: TextOverflow.ellipsis,\n      ),\n      margin: margin ?? const EdgeInsets.symmetric(horizontal: 14.0),\n      backgroundColor: backgroundColor ?? Theme.of(context).colorScheme.primary,\n      hoverColor: hoverColor ??\n          Theme.of(context).colorScheme.primary.withValues(alpha: 0.9),\n      radius: BorderRadius.circular(radius ?? 10.0),\n      onTap: onTap,\n    );\n  }\n}\n\nclass OutlinedRoundedButton extends StatelessWidget {\n  const OutlinedRoundedButton({\n    super.key,\n    required this.text,\n    this.onTap,\n    this.margin,\n    this.radius,\n  });\n\n  final String text;\n  final VoidCallback? onTap;\n  final EdgeInsets? margin;\n  final double? radius;\n\n  @override\n  Widget build(BuildContext context) {\n    return DecoratedBox(\n      decoration: ShapeDecoration(\n        shape: RoundedRectangleBorder(\n          side: Theme.of(context).brightness == Brightness.light\n              ? const BorderSide(color: Color(0x1E14171B))\n              : const BorderSide(color: Colors.white10),\n          borderRadius: BorderRadius.circular(radius ?? 8),\n        ),\n      ),\n      child: FlowyButton(\n        useIntrinsicWidth: true,\n        margin: margin ??\n            const EdgeInsets.symmetric(\n              horizontal: 16.0,\n              vertical: 9.0,\n            ),\n        radius: BorderRadius.circular(radius ?? 8),\n        text: FlowyText.regular(\n          text,\n          lineHeight: 1.0,\n          textAlign: TextAlign.center,\n        ),\n        onTap: onTap,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/progress_indicator.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:loading_indicator/loading_indicator.dart';\n\nList<Color> _kDefaultRainbowColors = const [\n  Colors.red,\n  Colors.orange,\n  Colors.yellow,\n  Colors.green,\n  Colors.blue,\n  Colors.indigo,\n  Colors.purple,\n];\n\n// CircularProgressIndicator()\nclass FlowyProgressIndicator extends StatelessWidget {\n  const FlowyProgressIndicator({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox.expand(\n      child: Center(\n        child: SizedBox(\n          width: 60,\n          child: LoadingIndicator(\n            indicatorType: Indicator.pacman,\n            colors: _kDefaultRainbowColors,\n            strokeWidth: 4.0,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrollbar.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass FlowyScrollbar extends StatefulWidget {\n  const FlowyScrollbar({\n    super.key,\n    this.controller,\n    this.thumbVisibility = true,\n    required this.child,\n  });\n\n  final ScrollController? controller;\n  final Widget child;\n  final bool thumbVisibility;\n\n  @override\n  State<FlowyScrollbar> createState() => _FlowyScrollbarState();\n}\n\nclass _FlowyScrollbarState extends State<FlowyScrollbar> {\n  final ValueNotifier<bool> isHovered = ValueNotifier(false);\n\n  @override\n  void dispose() {\n    isHovered.dispose();\n\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      onEnter: (_) => isHovered.value = true,\n      onExit: (_) => isHovered.value = false,\n      child: ValueListenableBuilder(\n        valueListenable: isHovered,\n        builder: (context, isHovered, child) {\n          return Scrollbar(\n            thumbVisibility: isHovered && widget.thumbVisibility,\n            // the radius should be fixed to 12\n            radius: const Radius.circular(12),\n            controller: widget.controller,\n            child: ScrollConfiguration(\n              behavior: ScrollConfiguration.of(context).copyWith(\n                scrollbars: false,\n              ),\n              child: child!,\n            ),\n          );\n        },\n        child: widget.child,\n      ),\n    );\n  }\n}\n\nclass ScrollControllerBuilder extends StatefulWidget {\n  const ScrollControllerBuilder({super.key, required this.builder});\n  final ScrollControllerWidgetBuilder builder;\n\n  @override\n  State<ScrollControllerBuilder> createState() =>\n      _ScrollControllerBuilderState();\n}\n\nclass _ScrollControllerBuilderState extends State<ScrollControllerBuilder> {\n  final ScrollController controller = ScrollController();\n\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return widget.builder(context, controller);\n  }\n}\n\ntypedef ScrollControllerWidgetBuilder = Widget Function(\n  BuildContext context,\n  ScrollController controller,\n);\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_list.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'styled_scroll_bar.dart';\n\nclass StyledScrollPhysics extends AlwaysScrollableScrollPhysics {}\n\n/// Core ListView for the app.\n/// Wraps a [ScrollbarListStack] + [ListView.builder] and assigns the 'Styled' scroll physics for the app\n/// Exposes a controller so other widgets can manipulate the list\nclass StyledListView extends StatefulWidget {\n  final double? itemExtent;\n  final int? itemCount;\n  final Axis axis;\n  final EdgeInsets? padding;\n  final EdgeInsets? scrollbarPadding;\n  final double? barSize;\n\n  final IndexedWidgetBuilder itemBuilder;\n\n  StyledListView({\n    super.key,\n    required this.itemBuilder,\n    required this.itemCount,\n    this.itemExtent,\n    this.axis = Axis.vertical,\n    this.padding,\n    this.barSize,\n    this.scrollbarPadding,\n  }) {\n    assert(itemExtent != 0, 'Item extent should never be 0, null is ok.');\n  }\n\n  @override\n  StyledListViewState createState() => StyledListViewState();\n}\n\n/// State is public so this can easily be controlled externally\nclass StyledListViewState extends State<StyledListView> {\n  final scrollController = ScrollController();\n\n  @override\n  void dispose() {\n    scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final contentSize = (widget.itemCount ?? 0.0) * (widget.itemExtent ?? 00.0);\n    Widget listContent = ScrollbarListStack(\n      contentSize: contentSize,\n      axis: widget.axis,\n      controller: scrollController,\n      barSize: widget.barSize ?? 8,\n      scrollbarPadding: widget.scrollbarPadding,\n      child: ListView.builder(\n        padding: widget.padding,\n        scrollDirection: widget.axis,\n        physics: StyledScrollPhysics(),\n        controller: scrollController,\n        itemExtent: widget.itemExtent,\n        itemCount: widget.itemCount,\n        itemBuilder: widget.itemBuilder,\n      ),\n    );\n    return listContent;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scroll_bar.dart",
    "content": "import 'dart:async';\nimport 'dart:math';\n\nimport 'package:async/async.dart';\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flowy_infra_ui/widget/mouse_hover_builder.dart';\nimport 'package:flutter/material.dart';\nimport 'package:styled_widget/styled_widget.dart';\n\nclass StyledScrollbar extends StatefulWidget {\n  const StyledScrollbar({\n    super.key,\n    this.size,\n    required this.axis,\n    required this.controller,\n    this.onDrag,\n    this.contentSize,\n    this.showTrack = false,\n    this.autoHideScrollbar = true,\n    this.handleColor,\n    this.trackColor,\n  });\n\n  final double? size;\n  final Axis axis;\n  final ScrollController controller;\n  final Function(double)? onDrag;\n  final bool showTrack;\n  final bool autoHideScrollbar;\n  final Color? handleColor;\n  final Color? trackColor;\n\n  // ignore: todo\n  // TODO: Remove contentHeight if we can fix this issue\n  // https://stackoverflow.com/questions/60855712/flutter-how-to-force-scrollcontroller-to-recalculate-position-maxextents\n  final double? contentSize;\n\n  @override\n  ScrollbarState createState() => ScrollbarState();\n}\n\nclass ScrollbarState extends State<StyledScrollbar> {\n  double _viewExtent = 100;\n  CancelableOperation? _hideScrollbarOperation;\n  bool hideHandler = false;\n\n  @override\n  void initState() {\n    super.initState();\n    widget.controller.addListener(_onScrollChanged);\n    widget.controller.position.isScrollingNotifier\n        .addListener(_hideScrollbarInTime);\n  }\n\n  @override\n  void dispose() {\n    if (widget.controller.hasClients) {\n      widget.controller.removeListener(_onScrollChanged);\n      widget.controller.position.isScrollingNotifier\n          .removeListener(_hideScrollbarInTime);\n    }\n    super.dispose();\n  }\n\n  void _onScrollChanged() => setState(() {});\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(\n      builder: (_, BoxConstraints constraints) {\n        double maxExtent;\n        final double? contentSize = widget.contentSize;\n\n        switch (widget.axis) {\n          case Axis.vertical:\n            // Use supplied contentSize if we have it, otherwise just fallback to maxScrollExtents\n            if (contentSize != null && contentSize > 0) {\n              maxExtent = contentSize - constraints.maxHeight;\n            } else {\n              maxExtent = widget.controller.position.maxScrollExtent;\n            }\n\n            _viewExtent = constraints.maxHeight;\n\n            break;\n          case Axis.horizontal:\n            // Use supplied contentSize if we have it, otherwise just fallback to maxScrollExtents\n            if (contentSize != null && contentSize > 0) {\n              maxExtent = contentSize - constraints.maxWidth;\n            } else {\n              maxExtent = widget.controller.position.maxScrollExtent;\n            }\n            _viewExtent = constraints.maxWidth;\n\n            break;\n        }\n\n        final contentExtent = maxExtent + _viewExtent;\n        // Calculate the alignment for the handle, this is a value between 0 and 1,\n        // it automatically takes the handle size into acct\n        // ignore: omit_local_variable_types\n        double handleAlignment =\n            maxExtent == 0 ? 0 : widget.controller.offset / maxExtent;\n\n        // Convert handle alignment from [0, 1] to [-1, 1]\n        handleAlignment *= 2.0;\n        handleAlignment -= 1.0;\n\n        // Calculate handleSize by comparing the total content size to our viewport\n        double handleExtent = _viewExtent;\n        if (contentExtent > _viewExtent) {\n          // Make sure handle is never small than the minSize\n          handleExtent = max(60, _viewExtent * _viewExtent / contentExtent);\n        }\n\n        // Hide the handle if content is < the viewExtent\n        var showHandle = hideHandler\n            ? false\n            : contentExtent > _viewExtent && contentExtent > 0;\n\n        // Track color\n        var trackColor = widget.trackColor ??\n            (Theme.of(context).brightness == Brightness.dark\n                ? AFThemeExtension.of(context).lightGreyHover\n                : AFThemeExtension.of(context).greyHover);\n\n        // Layout the stack, it just contains a child, and\n        return Stack(\n          children: [\n            /// TRACK, thin strip, aligned along the end of the parent\n            if (widget.showTrack)\n              Align(\n                alignment: const Alignment(1, 1),\n                child: Container(\n                  color: trackColor,\n                  width: widget.axis == Axis.vertical\n                      ? widget.size\n                      : double.infinity,\n                  height: widget.axis == Axis.horizontal\n                      ? widget.size\n                      : double.infinity,\n                ),\n              ),\n\n            /// HANDLE - Clickable shape that changes scrollController when dragged\n            Align(\n              // Use calculated alignment to position handle from -1 to 1, let Alignment do the rest of the work\n              alignment: Alignment(\n                widget.axis == Axis.vertical ? 1 : handleAlignment,\n                widget.axis == Axis.horizontal ? 1 : handleAlignment,\n              ),\n              child: GestureDetector(\n                onVerticalDragUpdate: _handleVerticalDrag,\n                onHorizontalDragUpdate: _handleHorizontalDrag,\n                // HANDLE SHAPE\n                child: MouseHoverBuilder(\n                  builder: (_, isHovered) {\n                    final handleColor =\n                        Theme.of(context).scrollbarTheme.thumbColor?.resolve(\n                              isHovered ? {WidgetState.dragged} : {},\n                            );\n                    return Container(\n                      width: widget.axis == Axis.vertical\n                          ? widget.size\n                          : handleExtent,\n                      height: widget.axis == Axis.horizontal\n                          ? widget.size\n                          : handleExtent,\n                      decoration: BoxDecoration(\n                        color: handleColor,\n                        borderRadius: Corners.s3Border,\n                      ),\n                    );\n                  },\n                ),\n              ),\n            )\n          ],\n        ).opacity(showHandle ? 1.0 : 0.0, animate: true);\n      },\n    );\n  }\n\n  void _hideScrollbarInTime() {\n    if (!mounted || !widget.autoHideScrollbar) return;\n\n    _hideScrollbarOperation?.cancel();\n\n    if (!widget.controller.position.isScrollingNotifier.value) {\n      _hideScrollbarOperation = CancelableOperation.fromFuture(\n        Future.delayed(const Duration(seconds: 2)),\n      ).then((_) {\n        hideHandler = true;\n        if (mounted) {\n          setState(() {});\n        }\n      });\n    } else {\n      hideHandler = false;\n    }\n  }\n\n  void _handleHorizontalDrag(DragUpdateDetails details) {\n    var pos = widget.controller.offset;\n    var pxRatio = (widget.controller.position.maxScrollExtent + _viewExtent) /\n        _viewExtent;\n    widget.controller.jumpTo((pos + details.delta.dx * pxRatio)\n        .clamp(0.0, widget.controller.position.maxScrollExtent));\n    widget.onDrag?.call(details.delta.dx);\n  }\n\n  void _handleVerticalDrag(DragUpdateDetails details) {\n    var pos = widget.controller.offset;\n    var pxRatio = (widget.controller.position.maxScrollExtent + _viewExtent) /\n        _viewExtent;\n    widget.controller.jumpTo((pos + details.delta.dy * pxRatio)\n        .clamp(0.0, widget.controller.position.maxScrollExtent));\n    widget.onDrag?.call(details.delta.dy);\n  }\n}\n\nclass ScrollbarListStack extends StatelessWidget {\n  const ScrollbarListStack({\n    super.key,\n    required this.barSize,\n    required this.axis,\n    required this.child,\n    required this.controller,\n    this.contentSize,\n    this.scrollbarPadding,\n    this.handleColor,\n    this.autoHideScrollbar = true,\n    this.trackColor,\n    this.showTrack = false,\n    this.includeInsets = true,\n  });\n\n  final double barSize;\n  final Axis axis;\n  final Widget child;\n  final ScrollController controller;\n  final double? contentSize;\n  final EdgeInsets? scrollbarPadding;\n  final Color? handleColor;\n  final Color? trackColor;\n  final bool showTrack;\n  final bool autoHideScrollbar;\n  final bool includeInsets;\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        /// Wrap with a bit of padding on the right or bottom to make room for the scrollbar\n        Padding(\n          padding: !includeInsets\n              ? EdgeInsets.zero\n              : EdgeInsets.only(\n                  right: axis == Axis.vertical ? barSize + Insets.m : 0,\n                  bottom: axis == Axis.horizontal ? barSize + Insets.m : 0,\n                ),\n          child: child,\n        ),\n\n        /// Display the scrollbar\n        Padding(\n          padding: scrollbarPadding ?? EdgeInsets.zero,\n          child: StyledScrollbar(\n            size: barSize,\n            axis: axis,\n            controller: controller,\n            contentSize: contentSize,\n            trackColor: trackColor,\n            handleColor: handleColor,\n            autoHideScrollbar: autoHideScrollbar,\n            showTrack: showTrack,\n          ),\n        )\n            // The animate will be used by the children that are using styled_widget.\n            .animate(const Duration(milliseconds: 250), Curves.easeOut),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'styled_list.dart';\nimport 'styled_scroll_bar.dart';\n\nclass StyledSingleChildScrollView extends StatefulWidget {\n  const StyledSingleChildScrollView({\n    super.key,\n    required this.child,\n    this.contentSize,\n    this.axis = Axis.vertical,\n    this.trackColor,\n    this.handleColor,\n    this.controller,\n    this.scrollbarPadding,\n    this.barSize = 8,\n    this.autoHideScrollbar = true,\n    this.includeInsets = true,\n  });\n\n  final Widget? child;\n  final double? contentSize;\n  final Axis axis;\n  final Color? trackColor;\n  final Color? handleColor;\n  final ScrollController? controller;\n  final EdgeInsets? scrollbarPadding;\n  final double barSize;\n  final bool autoHideScrollbar;\n  final bool includeInsets;\n\n  @override\n  State<StyledSingleChildScrollView> createState() =>\n      StyledSingleChildScrollViewState();\n}\n\nclass StyledSingleChildScrollViewState\n    extends State<StyledSingleChildScrollView> {\n  late final ScrollController scrollController =\n      widget.controller ?? ScrollController();\n\n  @override\n  void dispose() {\n    if (widget.controller == null) {\n      scrollController.dispose();\n    }\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ScrollbarListStack(\n      autoHideScrollbar: widget.autoHideScrollbar,\n      contentSize: widget.contentSize,\n      axis: widget.axis,\n      controller: scrollController,\n      scrollbarPadding: widget.scrollbarPadding,\n      barSize: widget.barSize,\n      trackColor: widget.trackColor,\n      handleColor: widget.handleColor,\n      includeInsets: widget.includeInsets,\n      child: SingleChildScrollView(\n        scrollDirection: widget.axis,\n        physics: StyledScrollPhysics(),\n        controller: scrollController,\n        child: widget.child,\n      ),\n    );\n  }\n}\n\nclass StyledCustomScrollView extends StatefulWidget {\n  const StyledCustomScrollView({\n    super.key,\n    this.axis = Axis.vertical,\n    this.trackColor,\n    this.handleColor,\n    this.verticalController,\n    this.slivers = const <Widget>[],\n    this.barSize = 8,\n  });\n\n  final Axis axis;\n  final Color? trackColor;\n  final Color? handleColor;\n  final ScrollController? verticalController;\n  final List<Widget> slivers;\n  final double barSize;\n\n  @override\n  StyledCustomScrollViewState createState() => StyledCustomScrollViewState();\n}\n\nclass StyledCustomScrollViewState extends State<StyledCustomScrollView> {\n  late final ScrollController controller =\n      widget.verticalController ?? ScrollController();\n\n  @override\n  Widget build(BuildContext context) {\n    var child = ScrollConfiguration(\n      behavior: const ScrollBehavior().copyWith(scrollbars: false),\n      child: CustomScrollView(\n        scrollDirection: widget.axis,\n        physics: StyledScrollPhysics(),\n        controller: controller,\n        slivers: widget.slivers,\n      ),\n    );\n\n    return ScrollbarListStack(\n      axis: widget.axis,\n      controller: controller,\n      barSize: widget.barSize,\n      trackColor: widget.trackColor,\n      handleColor: widget.handleColor,\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart",
    "content": "import 'dart:io';\n\nimport 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\n\nvoid showSnapBar(BuildContext context, String title, {VoidCallback? onClosed}) {\n  ScaffoldMessenger.of(context).clearSnackBars();\n\n  ScaffoldMessenger.of(context)\n      .showSnackBar(\n        SnackBar(\n          backgroundColor:\n              Theme.of(context).colorScheme.surfaceContainerHighest,\n          duration: const Duration(milliseconds: 8000),\n          content: FlowyText(\n            title,\n            maxLines: 2,\n            fontSize:\n                (Platform.isLinux || Platform.isWindows || Platform.isMacOS)\n                    ? 14\n                    : 12,\n          ),\n        ),\n      )\n      .closed\n      .then((value) => onClosed?.call());\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart",
    "content": "import 'dart:io';\n\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flutter/material.dart';\nimport 'package:google_fonts/google_fonts.dart';\n\nclass FlowyText extends StatelessWidget {\n  final String text;\n  final TextOverflow? overflow;\n  final double? fontSize;\n  final FontWeight? fontWeight;\n  final TextAlign? textAlign;\n  final int? maxLines;\n  final Color? color;\n  final TextDecoration? decoration;\n  final Color? decorationColor;\n  final double? decorationThickness;\n  final String? fontFamily;\n  final List<String>? fallbackFontFamily;\n  final bool withTooltip;\n  final StrutStyle? strutStyle;\n  final bool isEmoji;\n\n  /// this is used to control the line height in Flutter.\n  final double? lineHeight;\n\n  /// this is used to control the line height from Figma.\n  final double? figmaLineHeight;\n\n  final bool optimizeEmojiAlign;\n\n  const FlowyText(\n    this.text, {\n    super.key,\n    this.overflow = TextOverflow.clip,\n    this.fontSize,\n    this.fontWeight,\n    this.textAlign,\n    this.color,\n    this.maxLines = 1,\n    this.decoration,\n    this.decorationColor,\n    this.fontFamily,\n    this.fallbackFontFamily,\n    // // https://api.flutter.dev/flutter/painting/TextStyle/height.html\n    this.lineHeight,\n    this.figmaLineHeight,\n    this.withTooltip = false,\n    this.isEmoji = false,\n    this.strutStyle,\n    this.optimizeEmojiAlign = false,\n    this.decorationThickness,\n  });\n\n  FlowyText.small(\n    this.text, {\n    super.key,\n    this.overflow,\n    this.color,\n    this.textAlign,\n    this.maxLines = 1,\n    this.decoration,\n    this.decorationColor,\n    this.fontFamily,\n    this.fallbackFontFamily,\n    this.lineHeight,\n    this.withTooltip = false,\n    this.isEmoji = false,\n    this.strutStyle,\n    this.figmaLineHeight,\n    this.optimizeEmojiAlign = false,\n    this.decorationThickness,\n  })  : fontWeight = FontWeight.w400,\n        fontSize = (Platform.isIOS || Platform.isAndroid) ? 14 : 12;\n\n  const FlowyText.regular(\n    this.text, {\n    super.key,\n    this.fontSize,\n    this.overflow,\n    this.color,\n    this.textAlign,\n    this.maxLines = 1,\n    this.decoration,\n    this.decorationColor,\n    this.fontFamily,\n    this.fallbackFontFamily,\n    this.lineHeight,\n    this.withTooltip = false,\n    this.isEmoji = false,\n    this.strutStyle,\n    this.figmaLineHeight,\n    this.optimizeEmojiAlign = false,\n    this.decorationThickness,\n  }) : fontWeight = FontWeight.w400;\n\n  const FlowyText.medium(\n    this.text, {\n    super.key,\n    this.fontSize,\n    this.overflow,\n    this.color,\n    this.textAlign,\n    this.maxLines = 1,\n    this.decoration,\n    this.decorationColor,\n    this.fontFamily,\n    this.fallbackFontFamily,\n    this.lineHeight,\n    this.withTooltip = false,\n    this.isEmoji = false,\n    this.strutStyle,\n    this.figmaLineHeight,\n    this.optimizeEmojiAlign = false,\n    this.decorationThickness,\n  }) : fontWeight = FontWeight.w500;\n\n  const FlowyText.semibold(\n    this.text, {\n    super.key,\n    this.fontSize,\n    this.overflow,\n    this.color,\n    this.textAlign,\n    this.maxLines = 1,\n    this.decoration,\n    this.decorationColor,\n    this.fontFamily,\n    this.fallbackFontFamily,\n    this.lineHeight,\n    this.withTooltip = false,\n    this.isEmoji = false,\n    this.strutStyle,\n    this.figmaLineHeight,\n    this.optimizeEmojiAlign = false,\n    this.decorationThickness,\n  }) : fontWeight = FontWeight.w600;\n\n  // Some emojis are not supported on Linux and Android, fallback to noto color emoji\n  const FlowyText.emoji(\n    this.text, {\n    super.key,\n    this.fontSize,\n    this.overflow,\n    this.color,\n    this.textAlign = TextAlign.center,\n    this.maxLines = 1,\n    this.decoration,\n    this.decorationColor,\n    this.lineHeight,\n    this.withTooltip = false,\n    this.strutStyle = const StrutStyle(forceStrutHeight: true),\n    this.isEmoji = true,\n    this.fontFamily,\n    this.figmaLineHeight,\n    this.optimizeEmojiAlign = false,\n    this.decorationThickness,\n  })  : fontWeight = FontWeight.w400,\n        fallbackFontFamily = null;\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child;\n\n    var fontFamily = this.fontFamily;\n    var fallbackFontFamily = this.fallbackFontFamily;\n    var fontSize =\n        this.fontSize ?? Theme.of(context).textTheme.bodyMedium!.fontSize!;\n    if (isEmoji && _useNotoColorEmoji) {\n      fontFamily = _loadEmojiFontFamilyIfNeeded();\n      if (fontFamily != null && fallbackFontFamily == null) {\n        fallbackFontFamily = [fontFamily];\n      }\n    }\n\n    double? lineHeight;\n    // use figma line height as first priority\n    if (figmaLineHeight != null) {\n      lineHeight = figmaLineHeight! / fontSize;\n    } else if (this.lineHeight != null) {\n      lineHeight = this.lineHeight!;\n    }\n\n    if (isEmoji && (_useNotoColorEmoji || Platform.isWindows)) {\n      const scaleFactor = 0.9;\n      fontSize *= scaleFactor;\n      if (lineHeight != null) {\n        lineHeight /= scaleFactor;\n      }\n    }\n\n    final textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(\n          fontSize: fontSize,\n          fontWeight: fontWeight,\n          color: color,\n          decoration: decoration,\n          decorationColor: decorationColor,\n          decorationThickness: decorationThickness,\n          fontFamily: fontFamily,\n          fontFamilyFallback: fallbackFontFamily,\n          height: lineHeight,\n          leadingDistribution: isEmoji && optimizeEmojiAlign\n              ? TextLeadingDistribution.even\n              : null,\n        );\n\n    child = Text(\n      text,\n      maxLines: maxLines,\n      textAlign: textAlign,\n      overflow: overflow ?? TextOverflow.clip,\n      style: textStyle,\n      strutStyle: !isEmoji || (isEmoji && optimizeEmojiAlign)\n          ? StrutStyle.fromTextStyle(\n              textStyle,\n              forceStrutHeight: true,\n              leadingDistribution: TextLeadingDistribution.even,\n              height: lineHeight,\n            )\n          : null,\n    );\n\n    if (withTooltip) {\n      child = FlowyTooltip(\n        message: text,\n        child: child,\n      );\n    }\n\n    return child;\n  }\n\n  String? _loadEmojiFontFamilyIfNeeded() {\n    if (_useNotoColorEmoji) {\n      return GoogleFonts.notoColorEmoji().fontFamily;\n    }\n\n    return null;\n  }\n\n  bool get _useNotoColorEmoji => Platform.isLinux;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart",
    "content": "import 'dart:async';\n\nimport 'package:flowy_infra/size.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nclass FlowyTextField extends StatefulWidget {\n  final String? hintText;\n  final String? text;\n  final TextStyle? textStyle;\n  final void Function(String)? onChanged;\n  final void Function()? onEditingComplete;\n  final void Function(String)? onSubmitted;\n  final void Function()? onCanceled;\n  final FocusNode? focusNode;\n  final bool autoFocus;\n  final int? maxLength;\n  final TextEditingController? controller;\n  final bool autoClearWhenDone;\n  final bool submitOnLeave;\n  final Duration? debounceDuration;\n  final String? errorText;\n  final Widget? error;\n  final int? maxLines;\n  final bool showCounter;\n  final Widget? prefixIcon;\n  final Widget? suffixIcon;\n  final BoxConstraints? prefixIconConstraints;\n  final BoxConstraints? suffixIconConstraints;\n  final BoxConstraints? hintTextConstraints;\n  final TextStyle? hintStyle;\n  final InputDecoration? decoration;\n  final TextAlignVertical? textAlignVertical;\n  final TextInputAction? textInputAction;\n  final TextInputType? keyboardType;\n  final List<TextInputFormatter>? inputFormatters;\n  final bool obscureText;\n  final bool isDense;\n  final bool readOnly;\n  final Color? enableBorderColor;\n  final double? cursorHeight;\n  final BorderRadius? borderRadius;\n  final void Function()? onTap;\n  final Function(PointerDownEvent)? onTapOutside;\n\n  const FlowyTextField({\n    super.key,\n    this.hintText = \"\",\n    this.text,\n    this.textStyle,\n    this.onChanged,\n    this.onEditingComplete,\n    this.onSubmitted,\n    this.onCanceled,\n    this.focusNode,\n    this.autoFocus = true,\n    this.maxLength,\n    this.controller,\n    this.autoClearWhenDone = false,\n    this.submitOnLeave = false,\n    this.debounceDuration,\n    this.errorText,\n    this.error,\n    this.maxLines = 1,\n    this.showCounter = true,\n    this.prefixIcon,\n    this.suffixIcon,\n    this.prefixIconConstraints,\n    this.suffixIconConstraints,\n    this.hintTextConstraints,\n    this.hintStyle,\n    this.decoration,\n    this.textAlignVertical,\n    this.textInputAction,\n    this.keyboardType = TextInputType.multiline,\n    this.inputFormatters,\n    this.obscureText = false,\n    this.isDense = true,\n    this.readOnly = false,\n    this.enableBorderColor,\n    this.borderRadius,\n    this.onTap,\n    this.onTapOutside,\n    this.cursorHeight,\n  });\n\n  @override\n  State<FlowyTextField> createState() => FlowyTextFieldState();\n}\n\nclass FlowyTextFieldState extends State<FlowyTextField> {\n  late FocusNode focusNode;\n  late TextEditingController controller;\n  Timer? _debounceOnChanged;\n\n  @override\n  void initState() {\n    super.initState();\n\n    focusNode = widget.focusNode ?? FocusNode();\n    focusNode.addListener(notifyDidEndEditing);\n\n    controller = widget.controller ?? TextEditingController();\n\n    if (widget.text != null) {\n      controller.text = widget.text!;\n    }\n\n    if (widget.autoFocus) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        focusNode.requestFocus();\n        if (widget.controller == null) {\n          controller.selection = TextSelection.fromPosition(\n            TextPosition(offset: controller.text.length),\n          );\n        }\n      });\n    }\n  }\n\n  @override\n  void dispose() {\n    focusNode.removeListener(notifyDidEndEditing);\n    if (widget.focusNode == null) {\n      focusNode.dispose();\n    }\n    if (widget.controller == null) {\n      controller.dispose();\n    }\n    _debounceOnChanged?.cancel();\n    super.dispose();\n  }\n\n  void _debounceOnChangedText(Duration duration, String text) {\n    _debounceOnChanged?.cancel();\n    _debounceOnChanged = Timer(duration, () async {\n      if (mounted) {\n        _onChanged(text);\n      }\n    });\n  }\n\n  void _onChanged(String text) {\n    widget.onChanged?.call(text);\n    setState(() {});\n  }\n\n  void _onSubmitted(String text) {\n    widget.onSubmitted?.call(text);\n    if (widget.autoClearWhenDone) {\n      controller.clear();\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return TextField(\n      cursorHeight: widget.cursorHeight,\n      readOnly: widget.readOnly,\n      controller: controller,\n      focusNode: focusNode,\n      onChanged: (text) {\n        if (widget.debounceDuration != null) {\n          _debounceOnChangedText(widget.debounceDuration!, text);\n        } else {\n          _onChanged(text);\n        }\n      },\n      onSubmitted: _onSubmitted,\n      onEditingComplete: widget.onEditingComplete,\n      onTap: widget.onTap,\n      onTapOutside: widget.onTapOutside,\n      minLines: 1,\n      maxLines: widget.maxLines,\n      maxLength: widget.maxLength,\n      maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,\n      style: widget.textStyle ?? Theme.of(context).textTheme.bodySmall,\n      textAlignVertical: widget.textAlignVertical ?? TextAlignVertical.center,\n      keyboardType: widget.keyboardType,\n      inputFormatters: widget.inputFormatters,\n      obscureText: widget.obscureText,\n      decoration: widget.decoration ??\n          InputDecoration(\n            constraints: widget.hintTextConstraints ??\n                BoxConstraints(\n                  maxHeight: widget.errorText?.isEmpty ?? true ? 32 : 58,\n                ),\n            contentPadding: EdgeInsets.symmetric(\n              horizontal: widget.isDense ? 12 : 18,\n              vertical:\n                  (widget.maxLines == null || widget.maxLines! > 1) ? 12 : 0,\n            ),\n            enabledBorder: OutlineInputBorder(\n              borderRadius: widget.borderRadius ?? Corners.s8Border,\n              borderSide: BorderSide(\n                color: widget.enableBorderColor ??\n                    Theme.of(context).colorScheme.outline,\n              ),\n            ),\n            isDense: false,\n            hintText: widget.hintText,\n            errorText: widget.errorText,\n            error: widget.error,\n            errorStyle: Theme.of(context)\n                .textTheme\n                .bodySmall!\n                .copyWith(color: Theme.of(context).colorScheme.error),\n            hintStyle: widget.hintStyle ??\n                Theme.of(context)\n                    .textTheme\n                    .bodySmall!\n                    .copyWith(color: Theme.of(context).hintColor),\n            suffixText: widget.showCounter ? _suffixText() : \"\",\n            counterText: \"\",\n            focusedBorder: OutlineInputBorder(\n              borderRadius: widget.borderRadius ?? Corners.s8Border,\n              borderSide: BorderSide(\n                color: widget.readOnly\n                    ? widget.enableBorderColor ??\n                        Theme.of(context).colorScheme.outline\n                    : Theme.of(context).colorScheme.primary,\n              ),\n            ),\n            errorBorder: OutlineInputBorder(\n              borderSide: BorderSide(\n                color: Theme.of(context).colorScheme.error,\n              ),\n              borderRadius: widget.borderRadius ?? Corners.s8Border,\n            ),\n            focusedErrorBorder: OutlineInputBorder(\n              borderSide: BorderSide(\n                color: Theme.of(context).colorScheme.error,\n              ),\n              borderRadius: widget.borderRadius ?? Corners.s8Border,\n            ),\n            prefixIcon: widget.prefixIcon,\n            suffixIcon: widget.suffixIcon,\n            prefixIconConstraints: widget.prefixIconConstraints,\n            suffixIconConstraints: widget.suffixIconConstraints,\n          ),\n    );\n  }\n\n  void notifyDidEndEditing() {\n    if (!focusNode.hasFocus) {\n      if (controller.text.isNotEmpty && widget.submitOnLeave) {\n        widget.onSubmitted?.call(controller.text);\n      } else {\n        widget.onCanceled?.call();\n      }\n    }\n  }\n\n  String? _suffixText() {\n    if (widget.maxLength != null) {\n      return ' ${controller.text.length}/${widget.maxLength}';\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_input.dart",
    "content": "import 'dart:async';\nimport 'dart:math' as math;\n\nimport 'package:flowy_infra/size.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nclass FlowyFormTextInput extends StatelessWidget {\n  static EdgeInsets kDefaultTextInputPadding = const EdgeInsets.only(bottom: 2);\n\n  final String? label;\n  final bool? autoFocus;\n  final String? initialValue;\n  final String? hintText;\n  final EdgeInsets? contentPadding;\n  final TextStyle? textStyle;\n  final TextAlign textAlign;\n  final int? maxLines;\n  final int? maxLength;\n  final bool showCounter;\n  final TextEditingController? controller;\n  final TextCapitalization? capitalization;\n  final Function(String)? onChanged;\n  final Function()? onEditingComplete;\n  final Function(bool)? onFocusChanged;\n  final Function(FocusNode)? onFocusCreated;\n\n  const FlowyFormTextInput({\n    super.key,\n    this.label,\n    this.autoFocus,\n    this.initialValue,\n    this.onChanged,\n    this.onEditingComplete,\n    this.hintText,\n    this.onFocusChanged,\n    this.onFocusCreated,\n    this.controller,\n    this.contentPadding,\n    this.capitalization,\n    this.textStyle,\n    this.textAlign = TextAlign.center,\n    this.maxLines,\n    this.maxLength,\n    this.showCounter = true,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return StyledSearchTextInput(\n      capitalization: capitalization,\n      label: label,\n      autoFocus: autoFocus,\n      initialValue: initialValue,\n      onChanged: onChanged,\n      onFocusCreated: onFocusCreated,\n      style: textStyle ?? Theme.of(context).textTheme.bodyMedium,\n      textAlign: textAlign,\n      onEditingComplete: onEditingComplete,\n      onFocusChanged: onFocusChanged,\n      controller: controller,\n      maxLines: maxLines,\n      maxLength: maxLength,\n      showCounter: showCounter,\n      contentPadding: contentPadding ?? kDefaultTextInputPadding,\n      hintText: hintText,\n      hintStyle: Theme.of(context)\n          .textTheme\n          .bodyMedium!\n          .copyWith(color: Theme.of(context).hintColor.withValues(alpha: 0.7)),\n      isDense: true,\n      inputBorder: const ThinUnderlineBorder(\n        borderSide: BorderSide(width: 5, color: Colors.red),\n      ),\n    );\n  }\n}\n\nclass StyledSearchTextInput extends StatefulWidget {\n  final String? label;\n  final TextStyle? style;\n  final TextAlign textAlign;\n  final EdgeInsets? contentPadding;\n  final bool? autoFocus;\n  final bool? obscureText;\n  final IconData? icon;\n  final String? initialValue;\n  final int? maxLines;\n  final int? maxLength;\n  final bool showCounter;\n  final TextEditingController? controller;\n  final TextCapitalization? capitalization;\n  final TextInputType? type;\n  final bool? enabled;\n  final bool? autoValidate;\n  final bool? enableSuggestions;\n  final bool? autoCorrect;\n  final bool isDense;\n  final String? errorText;\n  final String? hintText;\n  final TextStyle? hintStyle;\n  final Widget? prefixIcon;\n  final Widget? suffixIcon;\n  final InputDecoration? inputDecoration;\n  final InputBorder? inputBorder;\n\n  final Function(String)? onChanged;\n  final Function()? onEditingComplete;\n  final Function()? onEditingCancel;\n  final Function(bool)? onFocusChanged;\n  final Function(FocusNode)? onFocusCreated;\n  final Function(String)? onFieldSubmitted;\n  final Function(String?)? onSaved;\n  final VoidCallback? onTap;\n\n  const StyledSearchTextInput({\n    super.key,\n    this.label,\n    this.autoFocus = false,\n    this.obscureText = false,\n    this.type = TextInputType.text,\n    this.textAlign = TextAlign.center,\n    this.icon,\n    this.initialValue = '',\n    this.controller,\n    this.enabled,\n    this.autoValidate = false,\n    this.enableSuggestions = true,\n    this.autoCorrect = true,\n    this.isDense = false,\n    this.errorText,\n    this.style,\n    this.contentPadding,\n    this.prefixIcon,\n    this.suffixIcon,\n    this.inputDecoration,\n    this.onChanged,\n    this.onEditingComplete,\n    this.onEditingCancel,\n    this.onFocusChanged,\n    this.onFocusCreated,\n    this.onFieldSubmitted,\n    this.onSaved,\n    this.onTap,\n    this.hintText,\n    this.hintStyle,\n    this.capitalization,\n    this.maxLines,\n    this.maxLength,\n    this.showCounter = false,\n    this.inputBorder,\n  });\n\n  @override\n  StyledSearchTextInputState createState() => StyledSearchTextInputState();\n}\n\nclass StyledSearchTextInputState extends State<StyledSearchTextInput> {\n  late TextEditingController _controller;\n  late FocusNode _focusNode;\n\n  @override\n  void initState() {\n    super.initState();\n    _controller =\n        widget.controller ?? TextEditingController(text: widget.initialValue);\n    _focusNode = FocusNode(\n      debugLabel: widget.label,\n      canRequestFocus: true,\n      onKeyEvent: (node, event) {\n        if (event.logicalKey == LogicalKeyboardKey.escape) {\n          widget.onEditingCancel?.call();\n          return KeyEventResult.handled;\n        }\n        return KeyEventResult.ignored;\n      },\n    );\n    // Listen for focus out events\n    _focusNode.addListener(_onFocusChanged);\n    widget.onFocusCreated?.call(_focusNode);\n    if (widget.autoFocus ?? false) {\n      scheduleMicrotask(() => _focusNode.requestFocus());\n    }\n  }\n\n  void _onFocusChanged() => widget.onFocusChanged?.call(_focusNode.hasFocus);\n\n  @override\n  void dispose() {\n    if (widget.controller == null) {\n      _controller.dispose();\n    }\n    _focusNode.removeListener(_onFocusChanged);\n    _focusNode.dispose();\n    super.dispose();\n  }\n\n  void clear() => _controller.clear();\n\n  String get text => _controller.text;\n\n  set text(String value) => _controller.text = value;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: EdgeInsets.symmetric(vertical: Insets.sm),\n      child: TextFormField(\n        onChanged: widget.onChanged,\n        onEditingComplete: widget.onEditingComplete,\n        onFieldSubmitted: widget.onFieldSubmitted,\n        onSaved: widget.onSaved,\n        onTap: widget.onTap,\n        autofocus: widget.autoFocus ?? false,\n        focusNode: _focusNode,\n        keyboardType: widget.type,\n        obscureText: widget.obscureText ?? false,\n        autocorrect: widget.autoCorrect ?? false,\n        enableSuggestions: widget.enableSuggestions ?? false,\n        style: widget.style ?? Theme.of(context).textTheme.bodyMedium,\n        cursorColor: Theme.of(context).colorScheme.primary,\n        controller: _controller,\n        showCursor: true,\n        enabled: widget.enabled,\n        maxLines: widget.maxLines,\n        maxLength: widget.maxLength,\n        textCapitalization: widget.capitalization ?? TextCapitalization.none,\n        textAlign: widget.textAlign,\n        decoration: widget.inputDecoration ??\n            InputDecoration(\n              prefixIcon: widget.prefixIcon,\n              suffixIcon: widget.suffixIcon,\n              counterText: \"\",\n              suffixText: widget.showCounter ? _suffixText() : \"\",\n              contentPadding: widget.contentPadding ?? EdgeInsets.all(Insets.m),\n              border: widget.inputBorder ??\n                  const OutlineInputBorder(borderSide: BorderSide.none),\n              isDense: widget.isDense,\n              icon: widget.icon == null ? null : Icon(widget.icon),\n              errorText: widget.errorText,\n              errorMaxLines: 2,\n              hintText: widget.hintText,\n              hintStyle: widget.hintStyle ??\n                  Theme.of(context)\n                      .textTheme\n                      .bodyMedium!\n                      .copyWith(color: Theme.of(context).hintColor),\n              labelText: widget.label,\n            ),\n      ),\n    );\n  }\n\n  String? _suffixText() {\n    if (widget.controller != null && widget.maxLength != null) {\n      return ' ${widget.controller!.text.length}/${widget.maxLength}';\n    }\n    return null;\n  }\n}\n\nclass ThinUnderlineBorder extends InputBorder {\n  /// Creates an underline border for an [InputDecorator].\n  ///\n  /// The [borderSide] parameter defaults to [BorderSide.none] (it must not be\n  /// null). Applications typically do not specify a [borderSide] parameter\n  /// because the input decorator substitutes its own, using [copyWith], based\n  /// on the current theme and [InputDecorator.isFocused].\n  ///\n  /// The [borderRadius] parameter defaults to a value where the top left\n  /// and right corners have a circular radius of 4.0. The [borderRadius]\n  /// parameter must not be null.\n  const ThinUnderlineBorder({\n    super.borderSide = const BorderSide(),\n    this.borderRadius = const BorderRadius.only(\n      topLeft: Radius.circular(4.0),\n      topRight: Radius.circular(4.0),\n    ),\n  });\n\n  /// The radii of the border's rounded rectangle corners.\n  ///\n  /// When this border is used with a filled input decorator, see\n  /// [InputDecoration.filled], the border radius defines the shape\n  /// of the background fill as well as the bottom left and right\n  /// edges of the underline itself.\n  ///\n  /// By default the top right and top left corners have a circular radius\n  /// of 4.0.\n  final BorderRadius borderRadius;\n\n  @override\n  bool get isOutline => false;\n\n  @override\n  UnderlineInputBorder copyWith({\n    BorderSide? borderSide,\n    BorderRadius? borderRadius,\n  }) {\n    return UnderlineInputBorder(\n      borderSide: borderSide ?? this.borderSide,\n      borderRadius: borderRadius ?? this.borderRadius,\n    );\n  }\n\n  @override\n  EdgeInsetsGeometry get dimensions =>\n      EdgeInsets.only(bottom: borderSide.width);\n\n  @override\n  UnderlineInputBorder scale(double t) =>\n      UnderlineInputBorder(borderSide: borderSide.scale(t));\n\n  @override\n  Path getInnerPath(Rect rect, {TextDirection? textDirection}) {\n    return Path()\n      ..addRect(Rect.fromLTWH(rect.left, rect.top, rect.width,\n          math.max(0.0, rect.height - borderSide.width)));\n  }\n\n  @override\n  Path getOuterPath(Rect rect, {TextDirection? textDirection}) {\n    return Path()..addRRect(borderRadius.resolve(textDirection).toRRect(rect));\n  }\n\n  @override\n  ShapeBorder? lerpFrom(ShapeBorder? a, double t) {\n    if (a is UnderlineInputBorder) {\n      final newBorderRadius =\n          BorderRadius.lerp(a.borderRadius, borderRadius, t);\n\n      if (newBorderRadius != null) {\n        return UnderlineInputBorder(\n          borderSide: BorderSide.lerp(a.borderSide, borderSide, t),\n          borderRadius: newBorderRadius,\n        );\n      }\n    }\n    return super.lerpFrom(a, t);\n  }\n\n  @override\n  ShapeBorder? lerpTo(ShapeBorder? b, double t) {\n    if (b is UnderlineInputBorder) {\n      final newBorderRadius =\n          BorderRadius.lerp(b.borderRadius, borderRadius, t);\n      if (newBorderRadius != null) {\n        return UnderlineInputBorder(\n          borderSide: BorderSide.lerp(borderSide, b.borderSide, t),\n          borderRadius: newBorderRadius,\n        );\n      }\n    }\n    return super.lerpTo(b, t);\n  }\n\n  /// Draw a horizontal line at the bottom of [rect].\n  ///\n  /// The [borderSide] defines the line's color and weight. The `textDirection`\n  /// `gap` and `textDirection` parameters are ignored.\n  /// @override\n\n  @override\n  void paint(\n    Canvas canvas,\n    Rect rect, {\n    double? gapStart,\n    double gapExtent = 0.0,\n    double gapPercentage = 0.0,\n    TextDirection? textDirection,\n  }) {\n    if (borderRadius.bottomLeft != Radius.zero ||\n        borderRadius.bottomRight != Radius.zero) {\n      canvas.clipPath(getOuterPath(rect, textDirection: textDirection));\n    }\n    canvas.drawLine(rect.bottomLeft, rect.bottomRight, borderSide.toPaint());\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (identical(this, other)) return true;\n    if (other.runtimeType != runtimeType) return false;\n    return other is InputBorder && other.borderSide == borderSide;\n  }\n\n  @override\n  int get hashCode => borderSide.hashCode;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/toolbar_button.dart",
    "content": "import 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/widget/flowy_tooltip.dart';\nimport 'package:flutter/material.dart';\n\nclass FlowyToolbarButton extends StatelessWidget {\n  final Widget child;\n  final VoidCallback? onPressed;\n  final EdgeInsets padding;\n  final String? tooltip;\n\n  const FlowyToolbarButton({\n    super.key,\n    this.onPressed,\n    this.tooltip,\n    this.padding = const EdgeInsets.symmetric(vertical: 8, horizontal: 6),\n    required this.child,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    final tooltipMessage = tooltip ?? '';\n\n    return FlowyTooltip(\n      message: tooltipMessage,\n      padding: EdgeInsets.zero,\n      child: RawMaterialButton(\n        clipBehavior: Clip.antiAlias,\n        constraints: const BoxConstraints(minWidth: 36, minHeight: 32),\n        hoverElevation: 0,\n        highlightElevation: 0,\n        padding: EdgeInsets.zero,\n        shape: const RoundedRectangleBorder(borderRadius: Corners.s6Border),\n        hoverColor: Colors.transparent,\n        focusColor: Colors.transparent,\n        splashColor: Colors.transparent,\n        highlightColor: Colors.transparent,\n        elevation: 0,\n        onPressed: onPressed,\n        child: child,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/base_styled_button.dart",
    "content": "import 'package:flowy_infra/size.dart';\nimport 'package:flutter/material.dart';\n\nclass BaseStyledButton extends StatefulWidget {\n  final Widget child;\n  final VoidCallback? onPressed;\n  final Function(bool)? onFocusChanged;\n  final Function(bool)? onHighlightChanged;\n  final Color? bgColor;\n  final Color? focusColor;\n  final Color? hoverColor;\n  final Color? highlightColor;\n  final EdgeInsets? contentPadding;\n  final double? minWidth;\n  final double? minHeight;\n  final BorderRadius? borderRadius;\n  final bool useBtnText;\n  final bool autoFocus;\n\n  final ShapeBorder? shape;\n\n  final Color outlineColor;\n\n  const BaseStyledButton({\n    super.key,\n    required this.child,\n    this.onPressed,\n    this.onFocusChanged,\n    this.onHighlightChanged,\n    this.bgColor,\n    this.focusColor,\n    this.contentPadding,\n    this.minWidth,\n    this.minHeight,\n    this.borderRadius,\n    this.hoverColor,\n    this.highlightColor,\n    this.shape,\n    this.useBtnText = true,\n    this.autoFocus = false,\n    this.outlineColor = Colors.transparent,\n  });\n\n  @override\n  State<BaseStyledButton> createState() => BaseStyledBtnState();\n}\n\nclass BaseStyledBtnState extends State<BaseStyledButton> {\n  late FocusNode _focusNode;\n  bool _isFocused = false;\n\n  @override\n  void initState() {\n    super.initState();\n    _focusNode = FocusNode(debugLabel: '', canRequestFocus: true);\n    _focusNode.addListener(_onFocusChanged);\n  }\n\n  void _onFocusChanged() {\n    if (_focusNode.hasFocus != _isFocused) {\n      setState(() => _isFocused = _focusNode.hasFocus);\n      widget.onFocusChanged?.call(_isFocused);\n    }\n  }\n\n  @override\n  void dispose() {\n    _focusNode.removeListener(_onFocusChanged);\n    _focusNode.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      decoration: BoxDecoration(\n        color: widget.bgColor ?? Theme.of(context).colorScheme.surface,\n        borderRadius: widget.borderRadius ?? Corners.s10Border,\n        boxShadow: _isFocused\n            ? [\n                BoxShadow(\n                  color: Theme.of(context).colorScheme.shadow,\n                  offset: Offset.zero,\n                  blurRadius: 8.0,\n                  spreadRadius: 0.0,\n                ),\n                BoxShadow(\n                  color:\n                      widget.bgColor ?? Theme.of(context).colorScheme.surface,\n                  offset: Offset.zero,\n                  blurRadius: 8.0,\n                  spreadRadius: -4.0,\n                ),\n              ]\n            : [],\n      ),\n      foregroundDecoration: _isFocused\n          ? ShapeDecoration(\n              shape: RoundedRectangleBorder(\n                side: BorderSide(\n                  width: 1.8,\n                  color: Theme.of(context).colorScheme.outline,\n                ),\n                borderRadius: widget.borderRadius ?? Corners.s10Border,\n              ),\n            )\n          : null,\n      child: RawMaterialButton(\n        focusNode: _focusNode,\n        autofocus: widget.autoFocus,\n        textStyle:\n            widget.useBtnText ? Theme.of(context).textTheme.bodyMedium : null,\n        materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,\n        // visualDensity: VisualDensity.compact,\n        splashColor: Colors.transparent,\n        mouseCursor: SystemMouseCursors.click,\n        elevation: 0,\n        hoverElevation: 0,\n        highlightElevation: 0,\n        focusElevation: 0,\n        fillColor: Colors.transparent,\n        hoverColor: widget.hoverColor ?? Colors.transparent,\n        highlightColor: widget.highlightColor ?? Colors.transparent,\n        focusColor: widget.focusColor ?? Colors.grey.withValues(alpha: 0.35),\n        constraints: BoxConstraints(\n            minHeight: widget.minHeight ?? 0, minWidth: widget.minWidth ?? 0),\n        onPressed: widget.onPressed,\n        shape: widget.shape ??\n            RoundedRectangleBorder(\n              side: BorderSide(color: widget.outlineColor, width: 1.5),\n              borderRadius: widget.borderRadius ?? Corners.s10Border,\n            ),\n        child: Opacity(\n          opacity: widget.onPressed != null ? 1 : .7,\n          child: Padding(\n            padding: widget.contentPadding ?? EdgeInsets.all(Insets.m),\n            child: widget.child,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart",
    "content": "import 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\n\nimport 'base_styled_button.dart';\nimport 'secondary_button.dart';\n\nclass PrimaryTextButton extends StatelessWidget {\n  final String label;\n  final VoidCallback? onPressed;\n  final TextButtonMode mode;\n\n  const PrimaryTextButton(this.label,\n      {super.key, this.onPressed, this.mode = TextButtonMode.big});\n\n  @override\n  Widget build(BuildContext context) {\n    return PrimaryButton(\n      mode: mode,\n      onPressed: onPressed,\n      child: FlowyText.regular(\n        label,\n        color: Theme.of(context).colorScheme.onPrimary,\n      ),\n    );\n  }\n}\n\nclass PrimaryButton extends StatelessWidget {\n  const PrimaryButton({\n    super.key,\n    required this.child,\n    this.onPressed,\n    this.mode = TextButtonMode.big,\n    this.backgroundColor,\n  });\n\n  final Widget child;\n  final VoidCallback? onPressed;\n  final TextButtonMode mode;\n  final Color? backgroundColor;\n\n  @override\n  Widget build(BuildContext context) {\n    return BaseStyledButton(\n      minWidth: mode.size.width,\n      minHeight: mode.size.height,\n      contentPadding: const EdgeInsets.symmetric(horizontal: 6),\n      bgColor: backgroundColor ?? Theme.of(context).colorScheme.primary,\n      hoverColor: Theme.of(context).colorScheme.primaryContainer,\n      borderRadius: mode.borderRadius,\n      onPressed: onPressed,\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/secondary_button.dart",
    "content": "import 'package:flowy_infra_ui/style_widget/text.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flowy_infra/size.dart';\n\nimport 'base_styled_button.dart';\n\nenum TextButtonMode {\n  normal,\n  big,\n  small;\n\n  Size get size {\n    switch (this) {\n      case TextButtonMode.normal:\n        return const Size(80, 32);\n      case TextButtonMode.big:\n        return const Size(100, 40);\n      case TextButtonMode.small:\n        return const Size(100, 30);\n    }\n  }\n\n  BorderRadius get borderRadius {\n    switch (this) {\n      case TextButtonMode.normal:\n        return Corners.s8Border;\n      case TextButtonMode.big:\n        return Corners.s12Border;\n      case TextButtonMode.small:\n        return Corners.s6Border;\n    }\n  }\n}\n\nclass SecondaryTextButton extends StatelessWidget {\n  const SecondaryTextButton(\n    this.label, {\n    super.key,\n    this.onPressed,\n    this.textColor,\n    this.outlineColor,\n    this.mode = TextButtonMode.normal,\n  });\n\n  final String label;\n  final VoidCallback? onPressed;\n  final TextButtonMode mode;\n  final Color? textColor;\n  final Color? outlineColor;\n\n  @override\n  Widget build(BuildContext context) {\n    return SecondaryButton(\n      mode: mode,\n      onPressed: onPressed,\n      outlineColor: outlineColor,\n      child: FlowyText.regular(\n        label,\n        color: textColor ?? Theme.of(context).colorScheme.primary,\n      ),\n    );\n  }\n}\n\nclass SecondaryButton extends StatelessWidget {\n  const SecondaryButton({\n    super.key,\n    required this.child,\n    this.onPressed,\n    this.outlineColor,\n    this.mode = TextButtonMode.normal,\n  });\n\n  final Widget child;\n  final VoidCallback? onPressed;\n  final TextButtonMode mode;\n  final Color? outlineColor;\n\n  @override\n  Widget build(BuildContext context) {\n    final size = mode.size;\n    return BaseStyledButton(\n      minWidth: size.width,\n      minHeight: size.height,\n      contentPadding: EdgeInsets.zero,\n      bgColor: Colors.transparent,\n      outlineColor: outlineColor ?? Theme.of(context).colorScheme.primary,\n      borderRadius: mode.borderRadius,\n      onPressed: onPressed,\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/constraint_flex_view.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass ConstrainedFlexView extends StatelessWidget {\n  final Widget child;\n  final double minSize;\n  final Axis axis;\n  final EdgeInsets scrollPadding;\n\n  const ConstrainedFlexView(this.minSize,\n      {super.key,\n      required this.child,\n      this.axis = Axis.horizontal,\n      this.scrollPadding = EdgeInsets.zero});\n\n  bool get isHz => axis == Axis.horizontal;\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(\n      builder: (_, constraints) {\n        final viewSize = isHz ? constraints.maxWidth : constraints.maxHeight;\n        if (viewSize > minSize) return child;\n        return Padding(\n          padding: scrollPadding,\n          child: SingleChildScrollView(\n            child: ConstrainedBox(\n              constraints: BoxConstraints(\n                  maxHeight: isHz ? double.infinity : minSize,\n                  maxWidth: isHz ? minSize : double.infinity),\n              child: child,\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/dialog_size.dart",
    "content": "class DialogSize {\n  static double get minDialogWidth => 400;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart",
    "content": "import 'dart:ui';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';\nimport 'package:flowy_infra_ui/widget/dialog/dialog_size.dart';\n\nextension IntoDialog on Widget {\n  Future<dynamic> show(BuildContext context) async {\n    FocusNode dialogFocusNode = FocusNode();\n    await Dialogs.show(\n      child: KeyboardListener(\n        focusNode: dialogFocusNode,\n        onKeyEvent: (event) {\n          if (event.logicalKey == LogicalKeyboardKey.escape) {\n            Navigator.of(context).pop();\n          }\n        },\n        child: this,\n      ),\n      context,\n    );\n    dialogFocusNode.dispose();\n  }\n}\n\nclass StyledDialog extends StatelessWidget {\n  final Widget child;\n  final double? maxWidth;\n  final double? maxHeight;\n  final EdgeInsets? padding;\n  final EdgeInsets? margin;\n  final BorderRadius? borderRadius;\n  final Color? bgColor;\n  final bool shrinkWrap;\n\n  const StyledDialog({\n    super.key,\n    required this.child,\n    this.maxWidth,\n    this.maxHeight,\n    this.padding,\n    this.margin,\n    this.bgColor,\n    this.borderRadius = const BorderRadius.all(Radius.circular(6)),\n    this.shrinkWrap = true,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    Widget innerContent = Container(\n      padding: padding ??\n          EdgeInsets.symmetric(horizontal: Insets.xxl, vertical: Insets.xl),\n      color: bgColor ?? Theme.of(context).colorScheme.surface,\n      child: child,\n    );\n\n    if (shrinkWrap) {\n      innerContent = IntrinsicWidth(\n          child: IntrinsicHeight(\n        child: innerContent,\n      ));\n    }\n\n    return FocusTraversalGroup(\n      child: Container(\n        margin: margin ?? EdgeInsets.all(Insets.sm * 2),\n        alignment: Alignment.center,\n        child: ConstrainedBox(\n          constraints: BoxConstraints(\n            minWidth: DialogSize.minDialogWidth,\n            maxHeight: maxHeight ?? double.infinity,\n            maxWidth: maxWidth ?? double.infinity,\n          ),\n          child: ClipRRect(\n            borderRadius: borderRadius ?? BorderRadius.zero,\n            child: SingleChildScrollView(\n              physics: StyledScrollPhysics(),\n              //https://medium.com/saugo360/https-medium-com-saugo360-flutter-using-overlay-to-display-floating-widgets-2e6d0e8decb9\n              child: Material(\n                type: MaterialType.transparency,\n                child: innerContent,\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass Dialogs {\n  static Future<dynamic> show(BuildContext context,\n      {required Widget child}) async {\n    return await Navigator.of(context).push(\n      StyledDialogRoute(\n        barrier: DialogBarrier(color: Colors.black.withValues(alpha: 0.4)),\n        pageBuilder: (BuildContext buildContext, Animation<double> animation,\n            Animation<double> secondaryAnimation) {\n          return SafeArea(child: child);\n        },\n      ),\n    );\n  }\n}\n\nclass DialogBarrier {\n  String label;\n  Color color;\n  bool dismissible;\n  ImageFilter? filter;\n\n  DialogBarrier({\n    this.dismissible = true,\n    this.color = Colors.transparent,\n    this.label = '',\n    this.filter,\n  });\n}\n\nclass StyledDialogRoute<T> extends PopupRoute<T> {\n  final RoutePageBuilder _pageBuilder;\n  final DialogBarrier barrier;\n\n  StyledDialogRoute({\n    required RoutePageBuilder pageBuilder,\n    required this.barrier,\n    Duration transitionDuration = const Duration(milliseconds: 300),\n    RouteTransitionsBuilder? transitionBuilder,\n    super.settings,\n  })  : _pageBuilder = pageBuilder,\n        _transitionDuration = transitionDuration,\n        _transitionBuilder = transitionBuilder,\n        super(filter: barrier.filter);\n\n  @override\n  bool get barrierDismissible {\n    return barrier.dismissible;\n  }\n\n  @override\n  String get barrierLabel => barrier.label;\n\n  @override\n  Color get barrierColor => barrier.color;\n\n  @override\n  Duration get transitionDuration => _transitionDuration;\n  final Duration _transitionDuration;\n\n  final RouteTransitionsBuilder? _transitionBuilder;\n\n  @override\n  Widget buildPage(BuildContext context, Animation<double> animation,\n      Animation<double> secondaryAnimation) {\n    return Semantics(\n      scopesRoute: true,\n      explicitChildNodes: true,\n      child: _pageBuilder(context, animation, secondaryAnimation),\n    );\n  }\n\n  @override\n  Widget buildTransitions(BuildContext context, Animation<double> animation,\n      Animation<double> secondaryAnimation, Widget child) {\n    if (_transitionBuilder == null) {\n      return FadeTransition(\n          opacity: CurvedAnimation(parent: animation, curve: Curves.easeInOut),\n          child: child);\n    } else {\n      return _transitionBuilder!(context, animation, secondaryAnimation, child);\n    } // Some default transition\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart",
    "content": "import 'package:flutter/material.dart';\n\nconst _tooltipWaitDuration = Duration(milliseconds: 300);\n\nclass FlowyTooltip extends StatelessWidget {\n  const FlowyTooltip({\n    super.key,\n    this.message,\n    this.richMessage,\n    this.preferBelow,\n    this.margin,\n    this.verticalOffset,\n    this.padding,\n    this.child,\n  });\n\n  final String? message;\n  final InlineSpan? richMessage;\n  final bool? preferBelow;\n  final EdgeInsetsGeometry? margin;\n  final Widget? child;\n  final double? verticalOffset;\n  final EdgeInsets? padding;\n\n  @override\n  Widget build(BuildContext context) {\n    if (message == null && richMessage == null) {\n      return child ?? const SizedBox.shrink();\n    }\n\n    return Tooltip(\n      margin: margin,\n      verticalOffset: verticalOffset ?? 16.0,\n      padding: padding ??\n          const EdgeInsets.symmetric(\n            horizontal: 12.0,\n            vertical: 8.0,\n          ),\n      decoration: BoxDecoration(\n        color: context.tooltipBackgroundColor(),\n        borderRadius: BorderRadius.circular(10.0),\n      ),\n      waitDuration: _tooltipWaitDuration,\n      message: message,\n      textStyle: message != null ? context.tooltipTextStyle() : null,\n      richMessage: richMessage,\n      preferBelow: preferBelow,\n      child: child,\n    );\n  }\n}\n\nclass ManualTooltip extends StatefulWidget {\n  const ManualTooltip({\n    super.key,\n    this.message,\n    this.richMessage,\n    this.preferBelow,\n    this.margin,\n    this.verticalOffset,\n    this.padding,\n    this.showAutomaticlly = false,\n    this.child,\n  });\n\n  final String? message;\n  final InlineSpan? richMessage;\n  final bool? preferBelow;\n  final EdgeInsetsGeometry? margin;\n  final Widget? child;\n  final double? verticalOffset;\n  final EdgeInsets? padding;\n  final bool showAutomaticlly;\n\n  @override\n  State<ManualTooltip> createState() => _ManualTooltipState();\n}\n\nclass _ManualTooltipState extends State<ManualTooltip> {\n  final key = GlobalKey<TooltipState>();\n\n  @override\n  void initState() {\n    if (widget.showAutomaticlly) {\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        if (mounted) key.currentState?.ensureTooltipVisible();\n      });\n    }\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Tooltip(\n      key: key,\n      margin: widget.margin,\n      verticalOffset: widget.verticalOffset ?? 16.0,\n      triggerMode: widget.showAutomaticlly ? TooltipTriggerMode.manual : null,\n      padding: widget.padding ??\n          const EdgeInsets.symmetric(\n            horizontal: 12.0,\n            vertical: 8.0,\n          ),\n      decoration: BoxDecoration(\n        color: context.tooltipBackgroundColor(),\n        borderRadius: BorderRadius.circular(10.0),\n      ),\n      waitDuration: _tooltipWaitDuration,\n      message: widget.message,\n      textStyle: widget.message != null ? context.tooltipTextStyle() : null,\n      richMessage: widget.richMessage,\n      preferBelow: widget.preferBelow,\n      child: widget.child,\n    );\n  }\n}\n\nextension FlowyToolTipExtension on BuildContext {\n  double tooltipFontSize() => 14.0;\n\n  double tooltipHeight({double? fontSize}) =>\n      20.0 / (fontSize ?? tooltipFontSize());\n\n  Color tooltipFontColor() => Theme.of(this).brightness == Brightness.light\n      ? Colors.white\n      : Colors.black;\n\n  TextStyle? tooltipTextStyle({Color? fontColor, double? fontSize}) {\n    return Theme.of(this).textTheme.bodyMedium?.copyWith(\n          color: fontColor ?? tooltipFontColor(),\n          fontSize: fontSize ?? tooltipFontSize(),\n          fontWeight: FontWeight.w400,\n          height: tooltipHeight(fontSize: fontSize),\n          leadingDistribution: TextLeadingDistribution.even,\n        );\n  }\n\n  TextStyle? tooltipHintTextStyle({double? fontSize}) => tooltipTextStyle(\n        fontColor: tooltipFontColor().withValues(alpha: 0.7),\n        fontSize: fontSize,\n      );\n\n  Color tooltipBackgroundColor() =>\n      Theme.of(this).brightness == Brightness.light\n          ? const Color(0xFF1D2129)\n          : const Color(0xE5E5E5E5);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/ignore_parent_gesture.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass IgnoreParentGestureWidget extends StatelessWidget {\n  const IgnoreParentGestureWidget({\n    super.key,\n    required this.child,\n    this.onPress,\n  });\n\n  final Widget child;\n  final VoidCallback? onPress;\n\n  @override\n  Widget build(BuildContext context) {\n    // https://docs.flutter.dev/development/ui/advanced/gestures#gesture-disambiguation\n    // https://github.com/AppFlowy-IO/AppFlowy/issues/1290\n    return Listener(\n      onPointerDown: (event) {\n        onPress?.call();\n      },\n      onPointerSignal: (event) {},\n      onPointerMove: (event) {},\n      onPointerUp: (event) {},\n      onPointerHover: (event) {},\n      onPointerPanZoomStart: (event) {},\n      onPointerPanZoomUpdate: (event) {},\n      onPointerPanZoomEnd: (event) {},\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/mouse_hover_builder.dart",
    "content": "import 'package:flutter/material.dart';\n\ntypedef HoverBuilder = Widget Function(BuildContext context, bool onHover);\n\nclass MouseHoverBuilder extends StatefulWidget {\n  final bool isClickable;\n\n  const MouseHoverBuilder(\n      {super.key, required this.builder, this.isClickable = false});\n\n  final HoverBuilder builder;\n\n  @override\n  State<MouseHoverBuilder> createState() => _MouseHoverBuilderState();\n}\n\nclass _MouseHoverBuilderState extends State<MouseHoverBuilder> {\n  bool _onHover = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      cursor: widget.isClickable\n          ? SystemMouseCursors.click\n          : SystemMouseCursors.basic,\n      onEnter: (p) => setOnHover(true),\n      onExit: (p) => setOnHover(false),\n      child: widget.builder(context, _onHover),\n    );\n  }\n\n  void setOnHover(bool value) => setState(() => _onHover = value);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart",
    "content": "import 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra_ui/style_widget/button.dart';\nimport 'package:flutter/material.dart';\n\nclass RoundedTextButton extends StatelessWidget {\n  final VoidCallback? onPressed;\n  final String? title;\n  final double? width;\n  final double? height;\n  final BorderRadius? borderRadius;\n  final Color borderColor;\n  final Color? fillColor;\n  final Color? hoverColor;\n  final Color? textColor;\n  final double? fontSize;\n  final FontWeight? fontWeight;\n  final EdgeInsets padding;\n\n  const RoundedTextButton({\n    super.key,\n    this.onPressed,\n    this.title,\n    this.width,\n    this.height,\n    this.borderRadius,\n    this.borderColor = Colors.transparent,\n    this.fillColor,\n    this.hoverColor,\n    this.textColor,\n    this.fontSize,\n    this.fontWeight,\n    this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6),\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return ConstrainedBox(\n      constraints: BoxConstraints(\n        minWidth: 10,\n        maxWidth: width ?? double.infinity,\n        minHeight: 10,\n        maxHeight: height ?? 60,\n      ),\n      child: SizedBox.expand(\n        child: FlowyTextButton(\n          title ?? '',\n          fontWeight: fontWeight,\n          onPressed: onPressed,\n          fontSize: fontSize,\n          mainAxisAlignment: MainAxisAlignment.center,\n          radius: borderRadius ?? Corners.s6Border,\n          fontColor: textColor ?? Theme.of(context).colorScheme.onPrimary,\n          fillColor: fillColor ?? Theme.of(context).colorScheme.primary,\n          hoverColor:\n              hoverColor ?? Theme.of(context).colorScheme.primaryContainer,\n          padding: padding,\n        ),\n      ),\n    );\n  }\n}\n\nclass RoundedImageButton extends StatelessWidget {\n  final VoidCallback? press;\n  final double size;\n  final BorderRadius borderRadius;\n  final Color borderColor;\n  final Color color;\n  final Widget child;\n\n  const RoundedImageButton({\n    super.key,\n    this.press,\n    required this.size,\n    this.borderRadius = BorderRadius.zero,\n    this.borderColor = Colors.transparent,\n    this.color = Colors.transparent,\n    required this.child,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: size,\n      height: size,\n      child: TextButton(\n        onPressed: press,\n        style: ButtonStyle(\n            shape: WidgetStateProperty.all<RoundedRectangleBorder>(\n                RoundedRectangleBorder(borderRadius: borderRadius))),\n        child: child,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nimport 'package:flowy_infra/size.dart';\nimport 'package:flowy_infra/time/duration.dart';\nimport 'package:flowy_infra_ui/widget/rounded_button.dart';\n\nclass RoundedInputField extends StatefulWidget {\n  final String? hintText;\n  final bool obscureText;\n  final Widget? obscureIcon;\n  final Widget? obscureHideIcon;\n  final Color? normalBorderColor;\n  final Color? errorBorderColor;\n  final Color? cursorColor;\n  final Color? focusBorderColor;\n  final String errorText;\n  final TextStyle? style;\n  final ValueChanged<String>? onChanged;\n  final Function(String)? onEditingComplete;\n  final String? initialValue;\n  final EdgeInsets margin;\n  final EdgeInsets padding;\n  final EdgeInsets contentPadding;\n  final double height;\n  final FocusNode? focusNode;\n  final TextEditingController? controller;\n  final bool autoFocus;\n  final int? maxLength;\n  final Function(String)? onFieldSubmitted;\n\n  const RoundedInputField({\n    super.key,\n    this.hintText,\n    this.errorText = \"\",\n    this.initialValue,\n    this.obscureText = false,\n    this.obscureIcon,\n    this.obscureHideIcon,\n    this.onChanged,\n    this.onEditingComplete,\n    this.normalBorderColor,\n    this.errorBorderColor,\n    this.focusBorderColor,\n    this.cursorColor,\n    this.style,\n    this.margin = EdgeInsets.zero,\n    this.padding = EdgeInsets.zero,\n    this.contentPadding = const EdgeInsets.symmetric(horizontal: 10),\n    this.height = 48,\n    this.focusNode,\n    this.controller,\n    this.autoFocus = false,\n    this.maxLength,\n    this.onFieldSubmitted,\n  });\n\n  @override\n  State<RoundedInputField> createState() => _RoundedInputFieldState();\n}\n\nclass _RoundedInputFieldState extends State<RoundedInputField> {\n  String inputText = \"\";\n  bool obscureText = false;\n\n  @override\n  void initState() {\n    super.initState();\n    obscureText = widget.obscureText;\n    inputText = widget.controller != null\n        ? widget.controller!.text\n        : widget.initialValue ?? \"\";\n  }\n\n  String? _suffixText() => widget.maxLength != null\n      ? ' ${widget.controller!.text.length}/${widget.maxLength}'\n      : null;\n\n  @override\n  Widget build(BuildContext context) {\n    Color borderColor =\n        widget.normalBorderColor ?? Theme.of(context).colorScheme.outline;\n    Color focusBorderColor =\n        widget.focusBorderColor ?? Theme.of(context).colorScheme.primary;\n\n    if (widget.errorText.isNotEmpty) {\n      borderColor = Theme.of(context).colorScheme.error;\n      focusBorderColor = borderColor;\n    }\n\n    List<Widget> children = [\n      Container(\n        margin: widget.margin,\n        padding: widget.padding,\n        height: widget.height,\n        child: TextFormField(\n          controller: widget.controller,\n          initialValue: widget.initialValue,\n          focusNode: widget.focusNode,\n          autofocus: widget.autoFocus,\n          maxLength: widget.maxLength,\n          maxLengthEnforcement:\n              MaxLengthEnforcement.truncateAfterCompositionEnds,\n          onFieldSubmitted: widget.onFieldSubmitted,\n          onChanged: (value) {\n            inputText = value;\n            if (widget.onChanged != null) {\n              widget.onChanged!(value);\n            }\n            setState(() {});\n          },\n          onEditingComplete: () {\n            if (widget.onEditingComplete != null) {\n              widget.onEditingComplete!(inputText);\n            }\n          },\n          cursorColor:\n              widget.cursorColor ?? Theme.of(context).colorScheme.primary,\n          obscureText: obscureText,\n          style: widget.style ?? Theme.of(context).textTheme.bodyMedium,\n          decoration: InputDecoration(\n            contentPadding: widget.contentPadding,\n            hintText: widget.hintText,\n            hintStyle: Theme.of(context)\n                .textTheme\n                .bodySmall!\n                .copyWith(color: Theme.of(context).hintColor),\n            suffixText: _suffixText(),\n            counterText: \"\",\n            enabledBorder: OutlineInputBorder(\n              borderSide: BorderSide(color: borderColor, width: 1.0),\n              borderRadius: Corners.s10Border,\n            ),\n            focusedBorder: OutlineInputBorder(\n              borderSide: BorderSide(color: focusBorderColor, width: 1.0),\n              borderRadius: Corners.s10Border,\n            ),\n            suffixIcon: obscureIcon(),\n          ),\n        ),\n      ),\n    ];\n\n    if (widget.errorText.isNotEmpty) {\n      children.add(\n        Align(\n          alignment: Alignment.centerLeft,\n          child: Padding(\n            padding: const EdgeInsets.symmetric(vertical: 4),\n            child: Text(\n              widget.errorText,\n              style: widget.style,\n            ),\n          ),\n        ),\n      );\n    }\n\n    return AnimatedSize(\n      duration: .4.seconds,\n      curve: Curves.easeInOut,\n      child: Column(children: children),\n    );\n  }\n\n  Widget? obscureIcon() {\n    if (widget.obscureText == false) {\n      return null;\n    }\n\n    const double iconWidth = 16;\n    if (inputText.isEmpty) {\n      return SizedBox.fromSize(size: const Size.square(iconWidth));\n    }\n\n    assert(widget.obscureIcon != null && widget.obscureHideIcon != null);\n    final icon = obscureText ? widget.obscureIcon! : widget.obscureHideIcon!;\n\n    return RoundedImageButton(\n      size: iconWidth,\n      press: () => setState(() => obscureText = !obscureText),\n      child: icon,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/route/animation.dart",
    "content": "import 'package:animations/animations.dart';\nimport 'package:flutter/material.dart';\n\ntypedef PageBuilder = Widget Function();\n\nclass PageRoutes {\n  static const double kDefaultDuration = .35;\n  static const Curve kDefaultEaseFwd = Curves.easeOut;\n  static const Curve kDefaultEaseReverse = Curves.easeOut;\n\n  static Route<T> fade<T>(PageBuilder pageBuilder, RouteSettings? settings,\n      [double duration = kDefaultDuration]) {\n    return PageRouteBuilder<T>(\n      settings: settings,\n      transitionDuration: Duration(milliseconds: (duration * 1000).round()),\n      pageBuilder: (context, animation, secondaryAnimation) => pageBuilder(),\n      transitionsBuilder: (context, animation, secondaryAnimation, child) {\n        return FadeTransition(opacity: animation, child: child);\n      },\n    );\n  }\n\n  static Route<T> fadeThrough<T>(PageBuilder pageBuilder,\n      [double duration = kDefaultDuration]) {\n    return PageRouteBuilder<T>(\n      transitionDuration: Duration(milliseconds: (duration * 1000).round()),\n      pageBuilder: (context, animation, secondaryAnimation) => pageBuilder(),\n      transitionsBuilder: (context, animation, secondaryAnimation, child) {\n        return FadeThroughTransition(\n            animation: animation,\n            secondaryAnimation: secondaryAnimation,\n            child: child);\n      },\n    );\n  }\n\n  static Route<T> fadeScale<T>(PageBuilder pageBuilder,\n      [double duration = kDefaultDuration]) {\n    return PageRouteBuilder<T>(\n      transitionDuration: Duration(milliseconds: (duration * 1000).round()),\n      pageBuilder: (context, animation, secondaryAnimation) => pageBuilder(),\n      transitionsBuilder: (context, animation, secondaryAnimation, child) {\n        return FadeScaleTransition(animation: animation, child: child);\n      },\n    );\n  }\n\n  static Route<T> sharedAxis<T>(PageBuilder pageBuilder,\n      [SharedAxisTransitionType type = SharedAxisTransitionType.scaled,\n      double duration = kDefaultDuration]) {\n    return PageRouteBuilder<T>(\n      transitionDuration: Duration(milliseconds: (duration * 1000).round()),\n      pageBuilder: (context, animation, secondaryAnimation) => pageBuilder(),\n      transitionsBuilder: (context, animation, secondaryAnimation, child) {\n        return SharedAxisTransition(\n          animation: animation,\n          secondaryAnimation: secondaryAnimation,\n          transitionType: type,\n          child: child,\n        );\n      },\n    );\n  }\n\n  static Route<T> slide<T>(PageBuilder pageBuilder,\n      {double duration = kDefaultDuration,\n      Offset startOffset = const Offset(1, 0),\n      Curve easeFwd = kDefaultEaseFwd,\n      Curve easeReverse = kDefaultEaseReverse}) {\n    return PageRouteBuilder<T>(\n      transitionDuration: Duration(milliseconds: (duration * 1000).round()),\n      pageBuilder: (context, animation, secondaryAnimation) => pageBuilder(),\n      transitionsBuilder: (context, animation, secondaryAnimation, child) {\n        bool reverse = animation.status == AnimationStatus.reverse;\n        return SlideTransition(\n          position: Tween<Offset>(begin: startOffset, end: const Offset(0, 0))\n              .animate(CurvedAnimation(\n                  parent: animation, curve: reverse ? easeReverse : easeFwd)),\n          child: child,\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/separated_flex.dart",
    "content": "import 'package:flutter/material.dart';\n\ntypedef SeparatorBuilder = Widget Function();\n\nWidget _defaultColumnSeparatorBuilder() => const Divider();\nWidget _defaultRowSeparatorBuilder() => const VerticalDivider();\n\nclass SeparatedColumn extends Column {\n  SeparatedColumn({\n    super.key,\n    super.mainAxisAlignment,\n    super.crossAxisAlignment,\n    super.mainAxisSize,\n    super.textBaseline,\n    super.textDirection,\n    super.verticalDirection,\n    SeparatorBuilder separatorBuilder = _defaultColumnSeparatorBuilder,\n    required List<Widget> children,\n  }) : super(children: _insertSeparators(children, separatorBuilder));\n}\n\nclass SeparatedRow extends Row {\n  SeparatedRow({\n    super.key,\n    super.mainAxisAlignment,\n    super.crossAxisAlignment,\n    super.mainAxisSize,\n    super.textBaseline,\n    super.textDirection,\n    super.verticalDirection,\n    SeparatorBuilder separatorBuilder = _defaultRowSeparatorBuilder,\n    required List<Widget> children,\n  }) : super(children: _insertSeparators(children, separatorBuilder));\n}\n\nList<Widget> _insertSeparators(\n  List<Widget> children,\n  SeparatorBuilder separatorBuilder,\n) {\n  if (children.length < 2) {\n    return children;\n  }\n\n  List<Widget> newChildren = [];\n  for (int i = 0; i < children.length - 1; i++) {\n    newChildren.add(children[i]);\n    newChildren.add(separatorBuilder());\n  }\n  return newChildren..add(children.last);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/spacing.dart",
    "content": "import 'package:flutter/cupertino.dart';\n\nclass Space extends StatelessWidget {\n  final double width;\n  final double height;\n\n  const Space(this.width, this.height, {super.key});\n\n  @override\n  Widget build(BuildContext context) => SizedBox(width: width, height: height);\n}\n\nclass VSpace extends StatelessWidget {\n  const VSpace(\n    this.size, {\n    super.key,\n    this.color,\n  });\n\n  final double size;\n  final Color? color;\n\n  @override\n  Widget build(BuildContext context) {\n    if (color != null) {\n      return SizedBox(\n        height: size,\n        width: double.infinity,\n        child: ColoredBox(\n          color: color!,\n        ),\n      );\n    } else {\n      return Space(0, size);\n    }\n  }\n}\n\nclass HSpace extends StatelessWidget {\n  const HSpace(\n    this.size, {\n    super.key,\n    this.color,\n  });\n\n  final double size;\n  final Color? color;\n\n  @override\n  Widget build(BuildContext context) {\n    if (color != null) {\n      return SizedBox(\n        height: double.infinity,\n        width: size,\n        child: ColoredBox(\n          color: color!,\n        ),\n      );\n    } else {\n      return Space(size, 0);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/linux/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.10)\nset(PROJECT_NAME \"flowy_infra_ui\")\nproject(${PROJECT_NAME} LANGUAGES CXX)\n\n# This value is used when generating builds using this plugin, so it must\n# not be changed\nset(PLUGIN_NAME \"flowy_infra_ui_plugin\")\n\nadd_library(${PLUGIN_NAME} SHARED\n  \"flowy_infra_ui_plugin.cc\"\n  \"flowy_infra_u_i_plugin.cc\"\n)\napply_standard_settings(${PLUGIN_NAME})\nset_target_properties(${PLUGIN_NAME} PROPERTIES\n  CXX_VISIBILITY_PRESET hidden)\ntarget_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)\ntarget_include_directories(${PLUGIN_NAME} INTERFACE\n  \"${CMAKE_CURRENT_SOURCE_DIR}/include\")\ntarget_link_libraries(${PLUGIN_NAME} PRIVATE flutter)\ntarget_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK)\n\n# List of absolute paths to libraries that should be bundled with the plugin\nset(flowy_infra_ui_bundled_libraries\n  \"\"\n  PARENT_SCOPE\n)\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/linux/flowy_infra_u_i_plugin.cc",
    "content": "#include \"include/flowy_infra_ui/flowy_infra_u_i_plugin.h\"\n\n#include <flutter_linux/flutter_linux.h>\n#include <gtk/gtk.h>\n#include <sys/utsname.h>\n\n#include <cstring>\n\n#define FLOWY_INFRA_U_I_PLUGIN(obj) \\\n  (G_TYPE_CHECK_INSTANCE_CAST((obj), flowy_infra_u_i_plugin_get_type(), \\\n                              FlowyInfraUiPlugin))\n\nstruct _FlowyInfraUiPlugin {\n  GObject parent_instance;\n};\n\nG_DEFINE_TYPE(FlowyInfraUiPlugin, flowy_infra_u_i_plugin, g_object_get_type())\n\n// Called when a method call is received from Flutter.\nstatic void flowy_infra_u_i_plugin_handle_method_call(\n    FlowyInfraUiPlugin* self,\n    FlMethodCall* method_call) {\n  g_autoptr(FlMethodResponse) response = nullptr;\n\n  const gchar* method = fl_method_call_get_name(method_call);\n\n  if (strcmp(method, \"getPlatformVersion\") == 0) {\n    struct utsname uname_data = {};\n    uname(&uname_data);\n    g_autofree gchar *version = g_strdup_printf(\"Linux %s\", uname_data.version);\n    g_autoptr(FlValue) result = fl_value_new_string(version);\n    response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));\n  } else {\n    response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());\n  }\n\n  fl_method_call_respond(method_call, response, nullptr);\n}\n\nstatic void flowy_infra_u_i_plugin_dispose(GObject* object) {\n  G_OBJECT_CLASS(flowy_infra_u_i_plugin_parent_class)->dispose(object);\n}\n\nstatic void flowy_infra_u_i_plugin_class_init(FlowyInfraUiPluginClass* klass) {\n  G_OBJECT_CLASS(klass)->dispose = flowy_infra_u_i_plugin_dispose;\n}\n\nstatic void flowy_infra_u_i_plugin_init(FlowyInfraUiPlugin* self) {}\n\nstatic void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call,\n                           gpointer user_data) {\n  FlowyInfraUiPlugin* plugin = FLOWY_INFRA_U_I_PLUGIN(user_data);\n  flowy_infra_u_i_plugin_handle_method_call(plugin, method_call);\n}\n\nvoid flowy_infra_u_i_plugin_register_with_registrar(FlPluginRegistrar* registrar) {\n  FlowyInfraUiPlugin* plugin = FLOWY_INFRA_U_I_PLUGIN(\n      g_object_new(flowy_infra_u_i_plugin_get_type(), nullptr));\n\n  g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();\n  g_autoptr(FlMethodChannel) channel =\n      fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar),\n                            \"flowy_infra_u_i\",\n                            FL_METHOD_CODEC(codec));\n  fl_method_channel_set_method_call_handler(channel, method_call_cb,\n                                            g_object_ref(plugin),\n                                            g_object_unref);\n\n  g_object_unref(plugin);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/linux/flowy_infra_ui_plugin.cc",
    "content": "#include \"include/flowy_infra_ui/flowy_infra_ui_plugin.h\"\n\n#include <flutter_linux/flutter_linux.h>\n#include <gtk/gtk.h>\n#include <sys/utsname.h>\n\n#include <cstring>\n\n#define FLOWY_INFRA_UI_PLUGIN(obj) \\\n  (G_TYPE_CHECK_INSTANCE_CAST((obj), flowy_infra_ui_plugin_get_type(), \\\n                              FlowyInfraUiPlugin))\n\nstruct _FlowyInfraUiPlugin {\n  GObject parent_instance;\n};\n\nG_DEFINE_TYPE(FlowyInfraUiPlugin, flowy_infra_ui_plugin, g_object_get_type())\n\n// Called when a method call is received from Flutter.\nstatic void flowy_infra_ui_plugin_handle_method_call(\n    FlowyInfraUiPlugin* self,\n    FlMethodCall* method_call) {\n  g_autoptr(FlMethodResponse) response = nullptr;\n\n  const gchar* method = fl_method_call_get_name(method_call);\n\n  if (strcmp(method, \"getPlatformVersion\") == 0) {\n    struct utsname uname_data = {};\n    uname(&uname_data);\n    g_autofree gchar *version = g_strdup_printf(\"Linux %s\", uname_data.version);\n    g_autoptr(FlValue) result = fl_value_new_string(version);\n    response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));\n  } else {\n    response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());\n  }\n\n  fl_method_call_respond(method_call, response, nullptr);\n}\n\nstatic void flowy_infra_ui_plugin_dispose(GObject* object) {\n  G_OBJECT_CLASS(flowy_infra_ui_plugin_parent_class)->dispose(object);\n}\n\nstatic void flowy_infra_ui_plugin_class_init(FlowyInfraUiPluginClass* klass) {\n  G_OBJECT_CLASS(klass)->dispose = flowy_infra_ui_plugin_dispose;\n}\n\nstatic void flowy_infra_ui_plugin_init(FlowyInfraUiPlugin* self) {}\n\nstatic void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call,\n                           gpointer user_data) {\n  FlowyInfraUiPlugin* plugin = FLOWY_INFRA_UI_PLUGIN(user_data);\n  flowy_infra_ui_plugin_handle_method_call(plugin, method_call);\n}\n\nvoid flowy_infra_ui_plugin_register_with_registrar(FlPluginRegistrar* registrar) {\n  FlowyInfraUiPlugin* plugin = FLOWY_INFRA_UI_PLUGIN(\n      g_object_new(flowy_infra_ui_plugin_get_type(), nullptr));\n\n  g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();\n  g_autoptr(FlMethodChannel) channel =\n      fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar),\n                            \"flowy_infra_ui\",\n                            FL_METHOD_CODEC(codec));\n  fl_method_channel_set_method_call_handler(channel, method_call_cb,\n                                            g_object_ref(plugin),\n                                            g_object_unref);\n\n  g_object_unref(plugin);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/linux/include/flowy_infra_ui/flowy_infra_u_i_plugin.h",
    "content": "#ifndef FLUTTER_PLUGIN_FLOWY_INFRA_UI_PLUGIN_H_\n#define FLUTTER_PLUGIN_FLOWY_INFRA_UI_PLUGIN_H_\n\n#include <flutter_linux/flutter_linux.h>\n\nG_BEGIN_DECLS\n\n#ifdef FLUTTER_PLUGIN_IMPL\n#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility(\"default\")))\n#else\n#define FLUTTER_PLUGIN_EXPORT\n#endif\n\ntypedef struct _FlowyInfraUiPlugin FlowyInfraUiPlugin;\ntypedef struct {\n  GObjectClass parent_class;\n} FlowyInfraUiPluginClass;\n\nFLUTTER_PLUGIN_EXPORT GType flowy_infra_u_i_plugin_get_type();\n\nFLUTTER_PLUGIN_EXPORT void flowy_infra_u_i_plugin_register_with_registrar(\n    FlPluginRegistrar* registrar);\n\nG_END_DECLS\n\n#endif  // FLUTTER_PLUGIN_FLOWY_INFRA_UI_PLUGIN_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/linux/include/flowy_infra_ui/flowy_infra_ui_plugin.h",
    "content": "#ifndef FLUTTER_PLUGIN_FLOWY_INFRA_UI_PLUGIN_H_\n#define FLUTTER_PLUGIN_FLOWY_INFRA_UI_PLUGIN_H_\n\n#include <flutter_linux/flutter_linux.h>\n\nG_BEGIN_DECLS\n\n#ifdef FLUTTER_PLUGIN_IMPL\n#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility(\"default\")))\n#else\n#define FLUTTER_PLUGIN_EXPORT\n#endif\n\ntypedef struct _FlowyInfraUiPlugin FlowyInfraUiPlugin;\ntypedef struct {\n  GObjectClass parent_class;\n} FlowyInfraUiPluginClass;\n\nFLUTTER_PLUGIN_EXPORT GType flowy_infra_ui_plugin_get_type();\n\nFLUTTER_PLUGIN_EXPORT void flowy_infra_ui_plugin_register_with_registrar(\n    FlPluginRegistrar* registrar);\n\nG_END_DECLS\n\n#endif  // FLUTTER_PLUGIN_FLOWY_INFRA_UI_PLUGIN_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/macos/Classes/FlowyInfraUiPlugin.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\npublic class FlowyInfraUIPlugin: NSObject, FlutterPlugin {\n  public static func register(with registrar: FlutterPluginRegistrar) {\n    let channel = FlutterMethodChannel(name: \"flowy_infra_ui\", binaryMessenger: registrar.messenger)\n    let instance = FlowyInfraUIPlugin()\n    registrar.addMethodCallDelegate(instance, channel: channel)\n  }\n\n  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {\n    switch call.method {\n    case \"getPlatformVersion\":\n      result(\"macOS \" + ProcessInfo.processInfo.operatingSystemVersionString)\n    default:\n      result(FlutterMethodNotImplemented)\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/macos/flowy_infra_ui.podspec",
    "content": "#\n# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.\n# Run `pod lib lint flowy_infra_ui.podspec` to validate before publishing.\n#\nPod::Spec.new do |s|\n  s.name             = 'flowy_infra_ui'\n  s.version          = '0.0.1'\n  s.summary          = 'A new flutter plugin project.'\n  s.description      = <<-DESC\nA new flutter plugin project.\n                       DESC\n  s.homepage         = 'http://example.com'\n  s.license          = { :file => '../LICENSE' }\n  s.author           = { 'AppFlowy' => 'annie@appflowy.io' }\n  s.source           = { :path => '.' }\n  s.source_files     = 'Classes/**/*'\n  s.dependency 'FlutterMacOS'\n\n  s.platform = :osx, '10.13'\n  s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }\n  s.swift_version = '5.0'\nend\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml",
    "content": "name: flowy_infra_ui\ndescription: A new flutter plugin project.\nversion: 0.0.1\nhomepage: https://appflowy.io\npublish_to: \"none\"\n\nenvironment:\n  sdk: \">=3.0.0 <4.0.0\"\n  flutter: \">=3.10.1\"\n\ndependencies:\n  flutter:\n    sdk: flutter\n\n  # Thirdparty packages\n\n  styled_widget: ^0.4.1\n  animations: ^2.0.7\n  loading_indicator: ^3.1.0\n  async:\n  url_launcher: ^6.1.11\n  google_fonts: ^6.1.0\n\n  # Federated Platform Interface\n  flowy_infra_ui_platform_interface:\n    path: flowy_infra_ui_platform_interface\n  appflowy_popover:\n    path: ../appflowy_popover\n  flowy_infra:\n    path: ../flowy_infra\n  flowy_svg:\n    path: ../flowy_svg\n\n  analyzer: 6.11.0\n\ndev_dependencies:\n  build_runner: ^2.4.9\n  provider: ^6.0.5\n  flutter_test:\n    sdk: flutter\n  flutter_lints: ^3.0.1\n\nflutter:\n  plugin:\n    platforms:\n      # TODO: uncomment android part will fail the Linux build process, will resolve later\n      # android:\n      #   package: com.example.flowy_infra_ui\n      #   pluginClass: FlowyInfraUIPlugin\n      ios:\n        pluginClass: FlowyInfraUIPlugin\n      macos:\n        pluginClass: FlowyInfraUIPlugin\n      windows:\n        pluginClass: FlowyInfraUIPlugin\n      linux:\n        pluginClass: FlowyInfraUIPlugin\n      web:\n        default_package: flowy_infra_ui_web\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/test/flowy_infra_ui_test.dart",
    "content": "import 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  const MethodChannel channel = MethodChannel('flowy_infra_ui');\n\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  setUp(() {\n    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n        .setMockMethodCallHandler(channel, (MethodCall methodCall) async {\n      return '42';\n    });\n  });\n\n  tearDown(() {\n    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n        .setMockMethodCallHandler(\n      channel,\n      null,\n    );\n  });\n\n  test('getPlatformVersion', () async {});\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/windows/.gitignore",
    "content": "flutter/\n\n# Visual Studio user-specific files.\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# Visual Studio build-related files.\nx64/\nx86/\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/windows/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.15)\nset(PROJECT_NAME \"flowy_infra_ui\")\nproject(${PROJECT_NAME} LANGUAGES CXX)\n\n# This value is used when generating builds using this plugin, so it must\n# not be changed\nset(PLUGIN_NAME \"flowy_infra_ui_plugin\")\n\nadd_library(${PLUGIN_NAME} SHARED\n  \"flowy_infra_ui_plugin.cpp\"\n)\napply_standard_settings(${PLUGIN_NAME})\nset_target_properties(${PLUGIN_NAME} PROPERTIES\n  CXX_VISIBILITY_PRESET hidden)\ntarget_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)\ntarget_include_directories(${PLUGIN_NAME} INTERFACE\n  \"${CMAKE_CURRENT_SOURCE_DIR}/include\")\ntarget_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin)\n\n# List of absolute paths to libraries that should be bundled with the plugin\nset(flowy_infra_ui_bundled_libraries\n  \"\"\n  PARENT_SCOPE\n)\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/windows/flowy_infra_ui_plugin.cpp",
    "content": "#include \"include/flowy_infra_ui/flowy_infra_ui_plugin.h\"\n\n// This must be included before many other Windows headers.\n#include <windows.h>\n\n// For getPlatformVersion; remove unless needed for your plugin implementation.\n#include <VersionHelpers.h>\n\n#include <flutter/method_channel.h>\n#include <flutter/plugin_registrar_windows.h>\n#include <flutter/standard_method_codec.h>\n\n#include <map>\n#include <memory>\n#include <sstream>\n\nnamespace {\n\nclass FlowyInfraUIPlugin : public flutter::Plugin {\n public:\n  static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);\n\n  FlowyInfraUIPlugin();\n\n  virtual ~FlowyInfraUIPlugin();\n\n private:\n  // Called when a method is called on this plugin's channel from Dart.\n  void HandleMethodCall(\n      const flutter::MethodCall<flutter::EncodableValue> &method_call,\n      std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);\n};\n\n// static\nvoid FlowyInfraUIPlugin::RegisterWithRegistrar(\n    flutter::PluginRegistrarWindows *registrar) {\n  auto channel =\n      std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(\n          registrar->messenger(), \"flowy_infra_ui\",\n          &flutter::StandardMethodCodec::GetInstance());\n\n  auto plugin = std::make_unique<FlowyInfraUIPlugin>();\n\n  channel->SetMethodCallHandler(\n      [plugin_pointer = plugin.get()](const auto &call, auto result) {\n        plugin_pointer->HandleMethodCall(call, std::move(result));\n      });\n\n  registrar->AddPlugin(std::move(plugin));\n}\n\nFlowyInfraUIPlugin::FlowyInfraUIPlugin() {}\n\nFlowyInfraUIPlugin::~FlowyInfraUIPlugin() {}\n\nvoid FlowyInfraUIPlugin::HandleMethodCall(\n    const flutter::MethodCall<flutter::EncodableValue> &method_call,\n    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {\n  if (method_call.method_name().compare(\"getPlatformVersion\") == 0) {\n    std::ostringstream version_stream;\n    version_stream << \"Windows \";\n    if (IsWindows10OrGreater()) {\n      version_stream << \"10+\";\n    } else if (IsWindows8OrGreater()) {\n      version_stream << \"8\";\n    } else if (IsWindows7OrGreater()) {\n      version_stream << \"7\";\n    }\n    result->Success(flutter::EncodableValue(version_stream.str()));\n  } else {\n    result->NotImplemented();\n  }\n}\n\n}  // namespace\n\nvoid FlowyInfraUIPluginRegisterWithRegistrar(\n    FlutterDesktopPluginRegistrarRef registrar) {\n  FlowyInfraUIPlugin::RegisterWithRegistrar(\n      flutter::PluginRegistrarManager::GetInstance()\n          ->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/windows/include/flowy_infra_ui/flowy_infra_u_i_plugin.h",
    "content": "#ifndef FLUTTER_PLUGIN_FLOWY_INFRA_UI_PLUGIN_H_\n#define FLUTTER_PLUGIN_FLOWY_INFRA_UI_PLUGIN_H_\n\n#include <flutter_plugin_registrar.h>\n\n#ifdef FLUTTER_PLUGIN_IMPL\n#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport)\n#else\n#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport)\n#endif\n\n#if defined(__cplusplus)\nextern \"C\" {\n#endif\n\nFLUTTER_PLUGIN_EXPORT void FlowyInfraUIPluginRegisterWithRegistrar(\n    FlutterDesktopPluginRegistrarRef registrar);\n\n#if defined(__cplusplus)\n}  // extern \"C\"\n#endif\n\n#endif  // FLUTTER_PLUGIN_FLOWY_INFRA_UI_PLUGIN_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_infra_ui/windows/include/flowy_infra_ui/flowy_infra_ui_plugin.h",
    "content": "#ifndef FLUTTER_PLUGIN_FLOWY_INFRA_UI_PLUGIN_H_\n#define FLUTTER_PLUGIN_FLOWY_INFRA_UI_PLUGIN_H_\n\n#include <flutter_plugin_registrar.h>\n\n#ifdef FLUTTER_PLUGIN_IMPL\n#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport)\n#else\n#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport)\n#endif\n\n#if defined(__cplusplus)\nextern \"C\" {\n#endif\n\nFLUTTER_PLUGIN_EXPORT void FlowyInfraUIPluginRegisterWithRegistrar(\n    FlutterDesktopPluginRegistrarRef registrar);\n\n#if defined(__cplusplus)\n}  // extern \"C\"\n#endif\n\n#endif  // FLUTTER_PLUGIN_FLOWY_INFRA_UI_PLUGIN_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug Report\nabout: Create a report to help us improve\ntitle: \"fix: \"\nlabels: bug\n---\n\n**Description**\n\nA clear and concise description of what the bug is.\n\n**Steps To Reproduce**\n\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected Behavior**\n\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\n\nIf applicable, add screenshots to help explain your problem.\n\n**Additional Context**\n\nAdd any other context about the problem here.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/build.md",
    "content": "---\nname: Build System\nabout: Changes that affect the build system or external dependencies\ntitle: \"build: \"\nlabels: build\n---\n\n**Description**\n\nDescribe what changes need to be done to the build system and why.\n\n**Requirements**\n\n- [ ] The build system is passing\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/chore.md",
    "content": "---\nname: Chore\nabout: Other changes that don't modify src or test files\ntitle: \"chore: \"\nlabels: chore\n---\n\n**Description**\n\nClearly describe what change is needed and why. If this changes code then please use another issue type.\n\n**Requirements**\n\n- [ ] No functional changes to the code\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/ci.md",
    "content": "---\nname: Continuous Integration\nabout: Changes to the CI configuration files and scripts\ntitle: \"ci: \"\nlabels: ci\n---\n\n**Description**\n\nDescribe what changes need to be done to the ci/cd system and why.\n\n**Requirements**\n\n- [ ] The ci system is passing\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/documentation.md",
    "content": "---\nname: Documentation\nabout: Improve the documentation so all collaborators have a common understanding\ntitle: \"docs: \"\nlabels: documentation\n---\n\n**Description**\n\nClearly describe what documentation you are looking to add or improve.\n\n**Requirements**\n\n- [ ] Requirements go here\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature Request\nabout: A new feature to be added to the project\ntitle: \"feat: \"\nlabels: feature\n---\n\n**Description**\n\nClearly describe what you are looking to add. The more context the better.\n\n**Requirements**\n\n- [ ] Checklist of requirements to be fulfilled\n\n**Additional Context**\n\nAdd any other context or screenshots about the feature request go here.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/performance.md",
    "content": "---\nname: Performance Update\nabout: A code change that improves performance\ntitle: \"perf: \"\nlabels: performance\n---\n\n**Description**\n\nClearly 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.\n\n**Requirements**\n\n- [ ] There is no drop in test coverage.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/refactor.md",
    "content": "---\nname: Refactor\nabout: A code change that neither fixes a bug nor adds a feature\ntitle: \"refactor: \"\nlabels: refactor\n---\n\n**Description**\n\nClearly describe what needs to be refactored and why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize.\n\n**Requirements**\n\n- [ ] There is no drop in test coverage.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/revert.md",
    "content": "---\nname: Revert Commit\nabout: Reverts a previous commit\ntitle: \"revert: \"\nlabels: revert\n---\n\n**Description**\n\nProvide a link to a PR/Commit that you are looking to revert and why.\n\n**Requirements**\n\n- [ ] Change has been reverted\n- [ ] No change in test coverage has happened\n- [ ] A new ticket is created for any follow on work that needs to happen\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/style.md",
    "content": "---\nname: Style Changes\nabout: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)\ntitle: \"style: \"\nlabels: style\n---\n\n**Description**\n\nClearly describe what you are looking to change and why.\n\n**Requirements**\n\n- [ ] There is no drop in test coverage.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/.github/ISSUE_TEMPLATE/test.md",
    "content": "---\nname: Test\nabout: Adding missing tests or correcting existing tests\ntitle: \"test: \"\nlabels: test\n---\n\n**Description**\n\nList 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.\n\n**Requirements**\n\n- [ ] There is no drop in test coverage.\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/.github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--\n  Thanks for contributing!\n\n  Provide a description of your changes below and a general summary in the title\n\n  Please look at the following checklist to ensure that your PR can be accepted quickly:\n-->\n\n## Status\n\n**READY/IN DEVELOPMENT/HOLD**\n\n## Description\n\n<!--- Describe your changes in detail -->\n\n## Type of Change\n\n<!--- Put an `x` in all the boxes that apply: -->\n\n- [ ] ✨ New feature (non-breaking change which adds functionality)\n- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)\n- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)\n- [ ] 🧹 Code refactor\n- [ ] ✅ Build configuration change\n- [ ] 📝 Documentation\n- [ ] 🗑️ Chore\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/.github/cspell.json",
    "content": "{\n  \"version\": \"0.2\",\n  \"$schema\": \"https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json\",\n  \"dictionaries\": [\"vgv_allowed\", \"vgv_forbidden\"],\n  \"dictionaryDefinitions\": [\n    {\n      \"name\": \"vgv_allowed\",\n      \"path\": \"https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/allowed.txt\",\n      \"description\": \"Allowed VGV Spellings\"\n    },\n    {\n      \"name\": \"vgv_forbidden\",\n      \"path\": \"https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/forbidden.txt\",\n      \"description\": \"Forbidden VGV Spellings\"\n    }\n  ],\n  \"useGitignore\": true,\n  \"words\": [\n    \"flowy_svg\"\n  ]\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/.github/dependabot.yaml",
    "content": "version: 2\nenable-beta-ecosystems: true\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n  - package-ecosystem: \"pub\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/.github/workflows/main.yaml",
    "content": "name: ci\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\non:\n  pull_request:\n    branches:\n      - main\n\njobs:\n  semantic_pull_request:\n    uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1\n\n  spell-check:\n    uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/spell_check.yml@v1\n    with:\n      includes: \"**/*.md\"\n      modified_files_only: false\n\n  build:\n    uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1\n    with:\n      flutter_channel: stable\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/.gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# VSCode related\n.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\n/build/\npubspec.lock\n\n# Web related\nlib/generated_plugin_registrant.dart\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Test related\ncoverage"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/analysis_options.yaml",
    "content": "linter:\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/bin/flowy_svg.dart",
    "content": "import 'dart:async';\nimport 'dart:developer';\nimport 'dart:io';\n\nimport 'package:args/args.dart';\nimport 'package:path/path.dart' as path;\n\nimport 'options.dart';\n\nconst languageKeywords = [\n  'abstract',\n  'else',\n  'import',\n  'show',\n  'as',\n  'enum',\n  'static',\n  'assert',\n  'export',\n  'interface',\n  'super',\n  'async',\n  'extends',\n  'is',\n  'switch',\n  'await',\n  'extension',\n  'late',\n  'sync',\n  'base',\n  'external',\n  'library',\n  'this',\n  'break',\n  'factory',\n  'mixin',\n  'throw',\n  'case',\n  'false',\n  'new',\n  'true',\n  'catch',\n  'final',\n  'variable',\n  'null',\n  'try',\n  'class',\n  'final',\n  'class',\n  'on',\n  'typedef',\n  'const',\n  'finally',\n  'operator',\n  'var',\n  'continue',\n  'for',\n  'part',\n  'void',\n  'covariant',\n  'Function',\n  'required',\n  'when',\n  'default',\n  'get',\n  'rethrow',\n  'while',\n  'deferred',\n  'hide',\n  'return',\n  'with',\n  'do',\n  'if',\n  'sealed',\n  'yield',\n  'dynamic',\n  'implements',\n  'set',\n];\n\nvoid main(List<String> args) {\n  if (_isHelpCommand(args)) {\n    _printHelperDisplay();\n  } else {\n    generateSvgData(_generateOption(args));\n  }\n}\n\nbool _isHelpCommand(List<String> args) {\n  return args.length == 1 && (args[0] == '--help' || args[0] == '-h');\n}\n\nvoid _printHelperDisplay() {\n  final parser = _generateArgParser(null);\n  log(parser.usage);\n}\n\nOptions _generateOption(List<String> args) {\n  final generateOptions = Options();\n  _generateArgParser(generateOptions).parse(args);\n  return generateOptions;\n}\n\nArgParser _generateArgParser(Options? generateOptions) {\n  final parser = ArgParser()\n    ..addOption(\n      'source-dir',\n      abbr: 'S',\n      defaultsTo: '/assets/flowy_icons',\n      callback: (String? x) => generateOptions!.sourceDir = x,\n      help: 'Folder containing localization files',\n    )\n    ..addOption(\n      'output-dir',\n      abbr: 'O',\n      defaultsTo: '/lib/generated',\n      callback: (String? x) => generateOptions!.outputDir = x,\n      help: 'Output folder stores for the generated file',\n    )\n    ..addOption(\n      'name',\n      abbr: 'N',\n      defaultsTo: 'flowy_svgs.g.dart',\n      callback: (String? x) => generateOptions!.outputFile = x,\n      help: 'The name of the output file that this tool will generate',\n    );\n\n  return parser;\n}\n\nDirectory source(Options options) => Directory(\n      [\n        Directory.current.path,\n        Directory.fromUri(\n          Uri.file(\n            options.sourceDir!,\n            windows: Platform.isWindows,\n          ),\n        ).path,\n      ].join(),\n    );\n\nFile output(Options options) => File(\n      [\n        Directory.current.path,\n        Directory.fromUri(\n          Uri.file(options.outputDir!, windows: Platform.isWindows),\n        ).path,\n        Platform.pathSeparator,\n        File.fromUri(\n          Uri.file(\n            options.outputFile!,\n            windows: Platform.isWindows,\n          ),\n        ).path,\n      ].join(),\n    );\n\n/// generates the svg data\nFuture<void> generateSvgData(Options options) async {\n  // the source directory that this is targeting\n  final src = source(options);\n\n  // the output directory that this is targeting\n  final out = output(options);\n\n  var files = await dirContents(src);\n  files = files.where((f) => f.path.contains('.svg')).toList();\n\n  await generate(files, out, options);\n}\n\n/// List the contents of the directory\nFuture<List<FileSystemEntity>> dirContents(Directory dir) {\n  final files = <FileSystemEntity>[];\n  final completer = Completer<List<FileSystemEntity>>();\n\n  dir.list(recursive: true).listen(\n        files.add,\n        onDone: () => completer.complete(files),\n      );\n  return completer.future;\n}\n\n/// Generate the abstract class for the FlowySvg data.\nFuture<void> generate(\n  List<FileSystemEntity> files,\n  File output,\n  Options options,\n) async {\n  final generated = File(output.path);\n\n  // create the output file if it doesn't exist\n  if (!generated.existsSync()) {\n    generated.createSync(recursive: true);\n  }\n\n  // content of the generated file\n  final builder = StringBuffer()..writeln(prelude);\n  files.whereType<File>().forEach(\n        (element) => builder.writeln(lineFor(element, options)),\n      );\n  builder.writeln(postlude);\n\n  generated.writeAsStringSync(builder.toString());\n}\n\nString lineFor(File file, Options options) {\n  final name = varNameFor(file, options);\n  return \"  static const $name = FlowySvgData('${pathFor(file)}');\";\n}\n\nString pathFor(File file) {\n  final relative = path.relative(file.path, from: Directory.current.path);\n  final uri = Uri.file(relative);\n  return uri.toFilePath(windows: false);\n}\n\nString varNameFor(File file, Options options) {\n  final from = source(options).path;\n\n  final relative = Uri.file(path.relative(file.path, from: from));\n\n  final parts = relative.pathSegments;\n\n  final cleaned = parts.map(clean).toList();\n\n  var simplified = cleaned.reversed\n      // join all cleaned path segments with an underscore\n      .join('_')\n      // there are some cases where the segment contains a dart reserved keyword\n      // in this case, the path will be suffixed with an underscore which means\n      // there will be a double underscore, so we have to replace the double\n      // underscore with one underscore\n      .replaceAll(RegExp('_+'), '_');\n\n  // rename icon based on relative path folder name (16x, 24x, etc.)\n  for (final key in sizeMap.keys) {\n    simplified = simplified.replaceAll(key, sizeMap[key]!);\n  }\n\n  return simplified;\n}\n\nconst sizeMap = {\n  r'$16x': 's',\n  r'$20x': 'm',\n  r'$24x': 'm',\n  r'$32x': 'lg',\n  r'$40x': 'xl'\n};\n\n/// cleans the path segment before rejoining the path into a variable name\nString clean(String segment) {\n  final cleaned = segment\n      // replace all dashes with underscores (dash is invalid in\n      // a variable name)\n      .replaceAll('-', '_')\n      // replace all spaces with an underscore\n      .replaceAll(RegExp(r'\\s+'), '_')\n      // replace all file extensions with an empty string\n      .replaceAll(RegExp(r'\\.[^.]*$'), '')\n      // convert everything to lower case\n      .toLowerCase();\n\n  if (languageKeywords.contains(cleaned)) {\n    return '${cleaned}_';\n  } else if (cleaned.startsWith(RegExp('[0-9]'))) {\n    return '\\$$cleaned';\n  }\n  return cleaned;\n}\n\n/// The prelude for the generated file\nconst prelude = '''\n// DO NOT EDIT. This code is generated by the flowy_svg script\n\n// import the widget with from this package\nimport 'package:flowy_svg/flowy_svg.dart';\n\n// export as convenience to the programmer\nexport 'package:flowy_svg/flowy_svg.dart';\n\n/// A class to easily list all the svgs in the app\nclass FlowySvgs {''';\n\n/// The postlude for the generated file\nconst postlude = '''\n}\n''';\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/bin/options.dart",
    "content": "/// The options for the command line tool\nclass Options {\n  /// The source directory which the tool will use to generate the output file\n  String? sourceDir;\n\n  /// The output directory which the tool will use to output the file(s)\n  String? outputDir;\n\n  /// The name of the file that will be generated\n  String? outputFile;\n\n  @override\n  String toString() {\n    return '''\nOptions:\n  sourceDir: $sourceDir\n  outputDir: $outputDir\n  name: $outputFile\n''';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/lib/flowy_svg.dart",
    "content": "/// A Flutter package to generate Dart code for SVG files.\nlibrary flowy_svg;\n\nexport 'src/flowy_svg.dart';\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_svg/flutter_svg.dart';\n\nexport 'package:flutter_svg/flutter_svg.dart';\n\n/// The class for FlowySvgData that the code generator will implement\nclass FlowySvgData {\n  /// The svg data\n  const FlowySvgData(\n    this.path,\n  );\n\n  /// The path to the svg data in appflowy assets/images\n  final String path;\n}\n\n/// For icon that needs to change color when it is on hovered\n///\n/// Get the hover color from ThemeData\nclass FlowySvg extends StatelessWidget {\n  /// Construct a FlowySvg Widget\n  const FlowySvg(\n    this.svg, {\n    super.key,\n    this.size,\n    this.color,\n    this.blendMode = BlendMode.srcIn,\n    this.opacity,\n    this.svgString,\n  });\n\n  /// Construct a FlowySvg Widget from a string\n  factory FlowySvg.string(\n    String svgString, {\n    Key? key,\n    Size? size,\n    Color? color,\n    BlendMode? blendMode = BlendMode.srcIn,\n    double? opacity,\n  }) {\n    return FlowySvg(\n      const FlowySvgData(''),\n      key: key,\n      size: size,\n      color: color,\n      blendMode: blendMode,\n      opacity: opacity,\n      svgString: svgString,\n    );\n  }\n\n  /// The data for the flowy svg. Will be generated by the generator in this\n  /// package within bin/flowy_svg.dart\n  final FlowySvgData svg;\n\n  /// The size of the svg\n  final Size? size;\n\n  /// The svg string\n  final String? svgString;\n\n  /// The color of the svg.\n  ///\n  /// This property will not be applied to the underlying svg widget if the\n  /// blend mode is null, but the blend mode defaults to [BlendMode.srcIn]\n  /// if it is not explicitly set to null.\n  final Color? color;\n\n  /// The blend mode applied to the svg.\n  ///\n  /// If the blend mode is null then the icon color will not be applied.\n  /// Set both the icon color and blendMode in order to apply color to the\n  /// svg widget.\n  final BlendMode? blendMode;\n\n  /// The opacity of the svg\n  ///\n  /// if null then use the opacity of the iconColor\n  final double? opacity;\n\n  @override\n  Widget build(BuildContext context) {\n    Color? iconColor = color ?? Theme.of(context).iconTheme.color;\n    if (opacity != null) {\n      iconColor = iconColor?.withValues(alpha: opacity!);\n    }\n\n    final textScaleFactor = MediaQuery.textScalerOf(context).scale(1);\n\n    final Widget svg;\n\n    if (svgString != null) {\n      svg = SvgPicture.string(\n        svgString!,\n        width: size?.width,\n        height: size?.height,\n        colorFilter: iconColor != null && blendMode != null\n            ? ColorFilter.mode(\n                iconColor,\n                blendMode!,\n              )\n            : null,\n      );\n    } else {\n      svg = SvgPicture.asset(\n        _normalized(),\n        width: size?.width,\n        height: size?.height,\n        colorFilter: iconColor != null && blendMode != null\n            ? ColorFilter.mode(\n                iconColor,\n                blendMode!,\n              )\n            : null,\n      );\n    }\n\n    return Transform.scale(\n      scale: textScaleFactor,\n      child: SizedBox(\n        width: size?.width,\n        height: size?.height,\n        child: svg,\n      ),\n    );\n  }\n\n  /// If the SVG's path does not start with `assets/`, it is\n  /// normalized and directed to `assets/images/`\n  ///\n  /// If the SVG does not end with `.svg`, then we append the file extension\n  ///\n  String _normalized() {\n    var path = svg.path;\n\n    if (!path.toLowerCase().startsWith('assets/')) {\n      path = 'assets/images/$path';\n    }\n\n    if (!path.toLowerCase().endsWith('.svg')) {\n      path = '$path.svg';\n    }\n\n    return path;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/packages/flowy_svg/pubspec.yaml",
    "content": "name: flowy_svg\ndescription: AppFlowy Svgs\nversion: 0.1.0+1\npublish_to: none\n\nenvironment:\n  sdk: \">=3.0.0 <4.0.0\"\n  flutter: 3.10.0\n\ndependencies:\n  args: ^2.4.2\n  flutter:\n    sdk: flutter\n  flutter_svg: ^2.0.7\n  path: ^1.8.3\n\ndev_dependencies:\n  very_good_analysis: ^5.0.0\n"
  },
  {
    "path": "frontend/appflowy_flutter/pubspec.lock",
    "content": "# Generated by pub\n# See https://dart.dev/tools/pub/glossary#lockfile\npackages:\n  _fe_analyzer_shared:\n    dependency: transitive\n    description:\n      name: _fe_analyzer_shared\n      sha256: \"16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"76.0.0\"\n  _macros:\n    dependency: transitive\n    description: dart\n    source: sdk\n    version: \"0.3.3\"\n  analyzer:\n    dependency: \"direct main\"\n    description:\n      name: analyzer\n      sha256: \"1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"6.11.0\"\n  animations:\n    dependency: transitive\n    description:\n      name: animations\n      sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.0.11\"\n  ansicolor:\n    dependency: transitive\n    description:\n      name: ansicolor\n      sha256: \"50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.0.3\"\n  any_date:\n    dependency: \"direct main\"\n    description:\n      name: any_date\n      sha256: e9ed245ba44ccebf3c2d6daa3592213f409821128593d448b219a1f8e9bd17a1\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.1.1\"\n  app_links:\n    dependency: \"direct main\"\n    description:\n      name: app_links\n      sha256: \"433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"6.3.3\"\n  app_links_linux:\n    dependency: transitive\n    description:\n      name: app_links_linux\n      sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.0.3\"\n  app_links_platform_interface:\n    dependency: transitive\n    description:\n      name: app_links_platform_interface\n      sha256: \"05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.0.2\"\n  app_links_web:\n    dependency: transitive\n    description:\n      name: app_links_web\n      sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.0.4\"\n  appflowy_backend:\n    dependency: \"direct main\"\n    description:\n      path: \"packages/appflowy_backend\"\n      relative: true\n    source: path\n    version: \"0.0.1\"\n  appflowy_board:\n    dependency: \"direct main\"\n    description:\n      path: \".\"\n      ref: e8317c0d1af8d23dc5707b02ea43864536b6de91\n      resolved-ref: e8317c0d1af8d23dc5707b02ea43864536b6de91\n      url: \"https://github.com/AppFlowy-IO/appflowy-board.git\"\n    source: git\n    version: \"0.1.2\"\n  appflowy_editor:\n    dependency: \"direct main\"\n    description:\n      path: \".\"\n      ref: \"470c4e7\"\n      resolved-ref: \"470c4e77c71b63f693ce0923a927afcd667d6f3b\"\n      url: \"https://github.com/AppFlowy-IO/appflowy-editor.git\"\n    source: git\n    version: \"5.2.0\"\n  appflowy_editor_plugins:\n    dependency: \"direct main\"\n    description:\n      path: \"packages/appflowy_editor_plugins\"\n      ref: \"4efcff7\"\n      resolved-ref: \"4efcff720ed01dd4d0f5f88a9f1ff6f79f423caa\"\n      url: \"https://github.com/AppFlowy-IO/AppFlowy-plugins.git\"\n    source: git\n    version: \"0.0.6\"\n  appflowy_popover:\n    dependency: \"direct main\"\n    description:\n      path: \"packages/appflowy_popover\"\n      relative: true\n    source: path\n    version: \"0.0.1\"\n  appflowy_result:\n    dependency: \"direct main\"\n    description:\n      path: \"packages/appflowy_result\"\n      relative: true\n    source: path\n    version: \"0.0.1\"\n  appflowy_ui:\n    dependency: \"direct main\"\n    description:\n      path: \"packages/appflowy_ui\"\n      relative: true\n    source: path\n    version: \"1.0.0\"\n  archive:\n    dependency: \"direct main\"\n    description:\n      name: archive\n      sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.6.1\"\n  args:\n    dependency: transitive\n    description:\n      name: args\n      sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.6.0\"\n  async:\n    dependency: transitive\n    description:\n      name: async\n      sha256: \"947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.11.0\"\n  auto_size_text_field:\n    dependency: \"direct main\"\n    description:\n      name: auto_size_text_field\n      sha256: \"41c90b2270e38edc6ce5c02e5a17737a863e65e246bdfc94565a38f3ec399144\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.2.4\"\n  auto_updater:\n    dependency: \"direct main\"\n    description:\n      path: \"packages/auto_updater\"\n      ref: \"1d81a824f3633f1d0200ba51b78fe0f9ce429458\"\n      resolved-ref: \"1d81a824f3633f1d0200ba51b78fe0f9ce429458\"\n      url: \"https://github.com/LucasXu0/auto_updater.git\"\n    source: git\n    version: \"1.0.0\"\n  auto_updater_macos:\n    dependency: \"direct overridden\"\n    description:\n      path: \"packages/auto_updater_macos\"\n      ref: \"1d81a824f3633f1d0200ba51b78fe0f9ce429458\"\n      resolved-ref: \"1d81a824f3633f1d0200ba51b78fe0f9ce429458\"\n      url: \"https://github.com/LucasXu0/auto_updater.git\"\n    source: git\n    version: \"1.0.0\"\n  auto_updater_platform_interface:\n    dependency: \"direct overridden\"\n    description:\n      path: \"packages/auto_updater_platform_interface\"\n      ref: \"1d81a824f3633f1d0200ba51b78fe0f9ce429458\"\n      resolved-ref: \"1d81a824f3633f1d0200ba51b78fe0f9ce429458\"\n      url: \"https://github.com/LucasXu0/auto_updater.git\"\n    source: git\n    version: \"1.0.0\"\n  auto_updater_windows:\n    dependency: transitive\n    description:\n      name: auto_updater_windows\n      sha256: \"2bba20a71eee072f49b7267fedd5c4f1406c4b1b1e5b83932c634dbab75b80c9\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.0.0\"\n  avatar_stack:\n    dependency: \"direct main\"\n    description:\n      name: avatar_stack\n      sha256: \"354527ba139956fd6439e2c49199d8298d72afdaa6c4cd6f37f26b97faf21f7e\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.0.0\"\n  barcode:\n    dependency: transitive\n    description:\n      name: barcode\n      sha256: \"7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.2.9\"\n  bidi:\n    dependency: transitive\n    description:\n      name: bidi\n      sha256: \"77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.0.13\"\n  bitsdojo_window:\n    dependency: \"direct main\"\n    description:\n      name: bitsdojo_window\n      sha256: \"88ef7765dafe52d97d7a3684960fb5d003e3151e662c18645c1641c22b873195\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.1.6\"\n  bitsdojo_window_linux:\n    dependency: transitive\n    description:\n      name: bitsdojo_window_linux\n      sha256: \"9519c0614f98be733e0b1b7cb15b827007886f6fe36a4fb62cf3d35b9dd578ab\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.1.4\"\n  bitsdojo_window_macos:\n    dependency: transitive\n    description:\n      name: bitsdojo_window_macos\n      sha256: f7c5be82e74568c68c5b8449e2c5d8fd12ec195ecd70745a7b9c0f802bb0268f\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.1.4\"\n  bitsdojo_window_platform_interface:\n    dependency: transitive\n    description:\n      name: bitsdojo_window_platform_interface\n      sha256: \"65daa015a0c6dba749bdd35a0f092e7a8ba8b0766aa0480eb3ef808086f6e27c\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.1.2\"\n  bitsdojo_window_windows:\n    dependency: transitive\n    description:\n      name: bitsdojo_window_windows\n      sha256: fa982cf61ede53f483e50b257344a1c250af231a3cdc93a7064dd6dc0d720b68\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.1.6\"\n  bloc:\n    dependency: \"direct main\"\n    description:\n      name: bloc\n      sha256: \"52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"9.0.0\"\n  bloc_test:\n    dependency: \"direct dev\"\n    description:\n      name: bloc_test\n      sha256: \"1dd549e58be35148bc22a9135962106aa29334bc1e3f285994946a1057b29d7b\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"10.0.0\"\n  boolean_selector:\n    dependency: transitive\n    description:\n      name: boolean_selector\n      sha256: \"6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.1.1\"\n  build:\n    dependency: transitive\n    description:\n      name: build\n      sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.4.2\"\n  build_config:\n    dependency: transitive\n    description:\n      name: build_config\n      sha256: \"4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.1.2\"\n  build_daemon:\n    dependency: transitive\n    description:\n      name: build_daemon\n      sha256: \"294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.0.3\"\n  build_resolvers:\n    dependency: transitive\n    description:\n      name: build_resolvers\n      sha256: \"99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.4.3\"\n  build_runner:\n    dependency: \"direct dev\"\n    description:\n      name: build_runner\n      sha256: \"74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.4.14\"\n  build_runner_core:\n    dependency: transitive\n    description:\n      name: build_runner_core\n      sha256: \"22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"8.0.0\"\n  built_collection:\n    dependency: transitive\n    description:\n      name: built_collection\n      sha256: \"376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"5.1.1\"\n  built_value:\n    dependency: transitive\n    description:\n      name: built_value\n      sha256: \"28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"8.9.3\"\n  cached_network_image:\n    dependency: \"direct main\"\n    description:\n      name: cached_network_image\n      sha256: \"7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.4.1\"\n  cached_network_image_platform_interface:\n    dependency: transitive\n    description:\n      name: cached_network_image_platform_interface\n      sha256: \"35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.1.1\"\n  cached_network_image_web:\n    dependency: transitive\n    description:\n      name: cached_network_image_web\n      sha256: \"980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.3.1\"\n  calendar_view:\n    dependency: \"direct main\"\n    description:\n      path: \".\"\n      ref: \"6fe0c98\"\n      resolved-ref: \"6fe0c989289b077569858d5472f3f7ec05b7746f\"\n      url: \"https://github.com/Xazin/flutter_calendar_view\"\n    source: git\n    version: \"1.0.5\"\n  characters:\n    dependency: transitive\n    description:\n      name: characters\n      sha256: \"04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.3.0\"\n  charcode:\n    dependency: transitive\n    description:\n      name: charcode\n      sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.4.0\"\n  checked_yaml:\n    dependency: transitive\n    description:\n      name: checked_yaml\n      sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.0.3\"\n  clock:\n    dependency: transitive\n    description:\n      name: clock\n      sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.1.1\"\n  code_builder:\n    dependency: transitive\n    description:\n      name: code_builder\n      sha256: \"0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.10.1\"\n  collection:\n    dependency: \"direct main\"\n    description:\n      name: collection\n      sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.19.0\"\n  connectivity_plus:\n    dependency: \"direct main\"\n    description:\n      name: connectivity_plus\n      sha256: \"224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"5.0.2\"\n  connectivity_plus_platform_interface:\n    dependency: transitive\n    description:\n      name: connectivity_plus_platform_interface\n      sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.2.4\"\n  convert:\n    dependency: transitive\n    description:\n      name: convert\n      sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.1.2\"\n  coverage:\n    dependency: transitive\n    description:\n      name: coverage\n      sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.11.1\"\n  cross_cache:\n    dependency: transitive\n    description:\n      name: cross_cache\n      sha256: \"80329477264c73f09945ee780ccdc84df9231f878dc7227d132d301e34ff310b\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.0.4\"\n  cross_file:\n    dependency: \"direct main\"\n    description:\n      name: cross_file\n      sha256: \"7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.3.4+2\"\n  crypto:\n    dependency: transitive\n    description:\n      name: crypto\n      sha256: \"1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.0.6\"\n  csslib:\n    dependency: transitive\n    description:\n      name: csslib\n      sha256: \"09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.0.2\"\n  dart_style:\n    dependency: transitive\n    description:\n      name: dart_style\n      sha256: \"7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.3.7\"\n  dbus:\n    dependency: transitive\n    description:\n      name: dbus\n      sha256: \"365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.7.10\"\n  defer_pointer:\n    dependency: \"direct main\"\n    description:\n      name: defer_pointer\n      sha256: d69e6f8c1d0f052d2616cc1db3782e0ea73f42e4c6f6122fd1a548dfe79faf02\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.0.2\"\n  desktop_drop:\n    dependency: \"direct main\"\n    description:\n      name: desktop_drop\n      sha256: \"03abf1c0443afdd1d65cf8fa589a2f01c67a11da56bbb06f6ea1de79d5628e94\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.5.0\"\n  device_info_plus:\n    dependency: \"direct main\"\n    description:\n      name: device_info_plus\n      sha256: \"72d146c6d7098689ff5c5f66bcf593ac11efc530095385356e131070333e64da\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"11.3.0\"\n  device_info_plus_platform_interface:\n    dependency: transitive\n    description:\n      name: device_info_plus_platform_interface\n      sha256: \"0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"7.0.2\"\n  diff_match_patch:\n    dependency: transitive\n    description:\n      name: diff_match_patch\n      sha256: \"2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.4.1\"\n  diffutil_dart:\n    dependency: \"direct main\"\n    description:\n      name: diffutil_dart\n      sha256: \"5e74883aedf87f3b703cb85e815bdc1ed9208b33501556e4a8a5572af9845c81\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.0.1\"\n  dio:\n    dependency: transitive\n    description:\n      name: dio\n      sha256: \"5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"5.7.0\"\n  dio_web_adapter:\n    dependency: transitive\n    description:\n      name: dio_web_adapter\n      sha256: \"33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.0.0\"\n  dotted_border:\n    dependency: \"direct main\"\n    description:\n      name: dotted_border\n      sha256: \"108837e11848ca776c53b30bc870086f84b62ed6e01c503ed976e8f8c7df9c04\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.1.0\"\n  easy_debounce:\n    dependency: transitive\n    description:\n      name: easy_debounce\n      sha256: f082609cfb8f37defb9e37fc28bc978c6712dedf08d4c5a26f820fa10165a236\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.0.3\"\n  easy_localization:\n    dependency: \"direct main\"\n    description:\n      name: easy_localization\n      sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.0.7\"\n  easy_logger:\n    dependency: transitive\n    description:\n      name: easy_logger\n      sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.0.2\"\n  envied:\n    dependency: \"direct main\"\n    description:\n      name: envied\n      sha256: \"08a9012e5d93e1a816919a52e37c7b8367e73ebb8d52d1ca7dd6fcd875a2cd2c\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.0.1\"\n  envied_generator:\n    dependency: \"direct dev\"\n    description:\n      name: envied_generator\n      sha256: \"9a49ca9f3744069661c4f2c06993647699fae2773bca10b593fbb3228d081027\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.0.1\"\n  equatable:\n    dependency: \"direct main\"\n    description:\n      name: equatable\n      sha256: \"567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.0.7\"\n  event_bus:\n    dependency: \"direct main\"\n    description:\n      name: event_bus\n      sha256: \"1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.0.1\"\n  expandable:\n    dependency: \"direct main\"\n    description:\n      name: expandable\n      sha256: \"9604d612d4d1146dafa96c6d8eec9c2ff0994658d6d09fed720ab788c7f5afc2\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"5.0.1\"\n  extended_text_field:\n    dependency: \"direct main\"\n    description:\n      name: extended_text_field\n      sha256: \"3996195c117c6beb734026a7bc0ba80d7e4e84e4edd4728caa544d8209ab4d7d\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"16.0.2\"\n  extended_text_library:\n    dependency: \"direct main\"\n    description:\n      name: extended_text_library\n      sha256: \"13d99f8a10ead472d5e2cf4770d3d047203fe5054b152e9eb5dc692a71befbba\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"12.0.1\"\n  fake_async:\n    dependency: transitive\n    description:\n      name: fake_async\n      sha256: \"511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.3.1\"\n  ffi:\n    dependency: transitive\n    description:\n      name: ffi\n      sha256: \"16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.1.3\"\n  file:\n    dependency: \"direct main\"\n    description:\n      name: file\n      sha256: \"5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"7.0.0\"\n  file_picker:\n    dependency: \"direct overridden\"\n    description:\n      name: file_picker\n      sha256: \"16dc141db5a2ccc6520ebb6a2eb5945b1b09e95085c021d9f914f8ded7f1465c\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"8.1.4\"\n  file_selector_linux:\n    dependency: transitive\n    description:\n      name: file_selector_linux\n      sha256: \"54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.9.3+2\"\n  file_selector_macos:\n    dependency: transitive\n    description:\n      name: file_selector_macos\n      sha256: \"271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.9.4+2\"\n  file_selector_platform_interface:\n    dependency: transitive\n    description:\n      name: file_selector_platform_interface\n      sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.6.2\"\n  file_selector_windows:\n    dependency: transitive\n    description:\n      name: file_selector_windows\n      sha256: \"8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.9.3+3\"\n  fixnum:\n    dependency: \"direct main\"\n    description:\n      name: fixnum\n      sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.1.1\"\n  flex_color_picker:\n    dependency: \"direct main\"\n    description:\n      name: flex_color_picker\n      sha256: c083b79f1c57eaeed9f464368be376951230b3cb1876323b784626152a86e480\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.7.0\"\n  flex_seed_scheme:\n    dependency: transitive\n    description:\n      name: flex_seed_scheme\n      sha256: d3ba3c5c92d2d79d45e94b4c6c71d01fac3c15017da1545880c53864da5dfeb0\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.5.0\"\n  flowy_infra:\n    dependency: \"direct main\"\n    description:\n      path: \"packages/flowy_infra\"\n      relative: true\n    source: path\n    version: \"0.0.1\"\n  flowy_infra_ui:\n    dependency: \"direct main\"\n    description:\n      path: \"packages/flowy_infra_ui\"\n      relative: true\n    source: path\n    version: \"0.0.1\"\n  flowy_infra_ui_platform_interface:\n    dependency: transitive\n    description:\n      path: \"packages/flowy_infra_ui/flowy_infra_ui_platform_interface\"\n      relative: true\n    source: path\n    version: \"0.0.1\"\n  flowy_svg:\n    dependency: \"direct main\"\n    description:\n      path: \"packages/flowy_svg\"\n      relative: true\n    source: path\n    version: \"0.1.0+1\"\n  flutter:\n    dependency: \"direct main\"\n    description: flutter\n    source: sdk\n    version: \"0.0.0\"\n  flutter_animate:\n    dependency: \"direct main\"\n    description:\n      name: flutter_animate\n      sha256: \"7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.5.2\"\n  flutter_bloc:\n    dependency: \"direct main\"\n    description:\n      name: flutter_bloc\n      sha256: \"1046d719fbdf230330d3443187cc33cc11963d15c9089f6cc56faa42a4c5f0cc\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"9.1.0\"\n  flutter_cache_manager:\n    dependency: \"direct main\"\n    description:\n      path: flutter_cache_manager\n      ref: HEAD\n      resolved-ref: fbab857b1b1d209240a146d32f496379b9f62276\n      url: \"https://github.com/LucasXu0/flutter_cache_manager.git\"\n    source: git\n    version: \"3.3.1\"\n  flutter_chat_core:\n    dependency: \"direct main\"\n    description:\n      name: flutter_chat_core\n      sha256: \"14557aaac7c71b80c279eca41781d214853940cf01727934c742b5845c42dd1e\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.0.2\"\n  flutter_chat_types:\n    dependency: transitive\n    description:\n      name: flutter_chat_types\n      sha256: e285b588f6d19d907feb1f6d912deaf22e223656769c34093b64e1c59b094fb9\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.6.2\"\n  flutter_chat_ui:\n    dependency: \"direct main\"\n    description:\n      name: flutter_chat_ui\n      sha256: \"2afd22eaebaf0f6ec8425048921479c3dd1a229604015dca05b174c6e8e44292\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.0.0-dev.1\"\n  flutter_driver:\n    dependency: transitive\n    description: flutter\n    source: sdk\n    version: \"0.0.0\"\n  flutter_emoji_mart:\n    dependency: \"direct main\"\n    description:\n      path: \".\"\n      ref: \"355aa56\"\n      resolved-ref: \"355aa56e9c74a91e00370a882739e0bb98c30bd8\"\n      url: \"https://github.com/LucasXu0/emoji_mart.git\"\n    source: git\n    version: \"1.0.2\"\n  flutter_highlight:\n    dependency: transitive\n    description:\n      name: flutter_highlight\n      sha256: \"7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.7.0\"\n  flutter_link_previewer:\n    dependency: transitive\n    description:\n      name: flutter_link_previewer\n      sha256: \"007069e60f42419fb59872beb7a3cc3ea21e9f1bdff5d40239f376fa62ca9f20\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.2.2\"\n  flutter_linkify:\n    dependency: transitive\n    description:\n      name: flutter_linkify\n      sha256: \"74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"6.0.0\"\n  flutter_lints:\n    dependency: \"direct dev\"\n    description:\n      name: flutter_lints\n      sha256: \"5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"5.0.0\"\n  flutter_localizations:\n    dependency: transitive\n    description: flutter\n    source: sdk\n    version: \"0.0.0\"\n  flutter_math_fork:\n    dependency: \"direct main\"\n    description:\n      name: flutter_math_fork\n      sha256: \"284bab89b2fbf1bc3a0baf13d011c1dd324d004e35d177626b77f2fc056366ac\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.7.3\"\n  flutter_plugin_android_lifecycle:\n    dependency: transitive\n    description:\n      name: flutter_plugin_android_lifecycle\n      sha256: \"615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.0.24\"\n  flutter_shaders:\n    dependency: transitive\n    description:\n      name: flutter_shaders\n      sha256: \"34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.1.3\"\n  flutter_slidable:\n    dependency: \"direct main\"\n    description:\n      name: flutter_slidable\n      sha256: a857de7ea701f276fd6a6c4c67ae885b60729a3449e42766bb0e655171042801\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.1.2\"\n  flutter_staggered_grid_view:\n    dependency: \"direct main\"\n    description:\n      name: flutter_staggered_grid_view\n      sha256: \"19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.7.0\"\n  flutter_sticky_header:\n    dependency: \"direct overridden\"\n    description:\n      name: flutter_sticky_header\n      sha256: \"7f76d24d119424ca0c95c146b8627a457e8de8169b0d584f766c2c545db8f8be\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.7.0\"\n  flutter_svg:\n    dependency: transitive\n    description:\n      name: flutter_svg\n      sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.0.17\"\n  flutter_test:\n    dependency: \"direct dev\"\n    description: flutter\n    source: sdk\n    version: \"0.0.0\"\n  flutter_tex:\n    dependency: \"direct main\"\n    description:\n      name: flutter_tex\n      sha256: ef7896946052e150514a2afe10f6e33e4fe0e7e4fc51195b65da811cb33c59ab\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.0.13\"\n  flutter_web_plugins:\n    dependency: transitive\n    description: flutter\n    source: sdk\n    version: \"0.0.0\"\n  fluttertoast:\n    dependency: \"direct main\"\n    description:\n      name: fluttertoast\n      sha256: \"24467dc20bbe49fd63e57d8e190798c4d22cbbdac30e54209d153a15273721d1\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"8.2.10\"\n  freezed:\n    dependency: \"direct dev\"\n    description:\n      name: freezed\n      sha256: \"44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.5.7\"\n  freezed_annotation:\n    dependency: \"direct main\"\n    description:\n      name: freezed_annotation\n      sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.4.4\"\n  frontend_server_client:\n    dependency: transitive\n    description:\n      name: frontend_server_client\n      sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.0.0\"\n  fuchsia_remote_debug_protocol:\n    dependency: transitive\n    description: flutter\n    source: sdk\n    version: \"0.0.0\"\n  get_it:\n    dependency: \"direct main\"\n    description:\n      name: get_it\n      sha256: f126a3e286b7f5b578bf436d5592968706c4c1de28a228b870ce375d9f743103\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"8.0.3\"\n  glob:\n    dependency: transitive\n    description:\n      name: glob\n      sha256: \"0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.1.2\"\n  go_router:\n    dependency: \"direct main\"\n    description:\n      name: go_router\n      sha256: \"7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"14.6.3\"\n  google_fonts:\n    dependency: \"direct main\"\n    description:\n      name: google_fonts\n      sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"6.2.1\"\n  graphs:\n    dependency: transitive\n    description:\n      name: graphs\n      sha256: \"741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.3.2\"\n  gtk:\n    dependency: transitive\n    description:\n      name: gtk\n      sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.1.0\"\n  highlight:\n    dependency: \"direct main\"\n    description:\n      name: highlight\n      sha256: \"5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.7.0\"\n  hive:\n    dependency: transitive\n    description:\n      name: hive\n      sha256: \"8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.2.3\"\n  hive_flutter:\n    dependency: \"direct main\"\n    description:\n      name: hive_flutter\n      sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.1.0\"\n  hotkey_manager:\n    dependency: \"direct main\"\n    description:\n      name: hotkey_manager\n      sha256: \"8aaa0aeaca7015b8c561a58d02eb7ebba95e93357fc9540398c5751ee24afd7c\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.1.8\"\n  html:\n    dependency: transitive\n    description:\n      name: html\n      sha256: \"1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.15.5\"\n  html2md:\n    dependency: \"direct main\"\n    description:\n      name: html2md\n      sha256: \"465cf8ffa1b510fe0e97941579bf5b22e2d575f2cecb500a9c0254efe33a8036\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.3.2\"\n  http:\n    dependency: \"direct main\"\n    description:\n      name: http\n      sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.2.2\"\n  http_multi_server:\n    dependency: transitive\n    description:\n      name: http_multi_server\n      sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.2.2\"\n  http_parser:\n    dependency: transitive\n    description:\n      name: http_parser\n      sha256: \"178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.1.2\"\n  iconsax_flutter:\n    dependency: transitive\n    description:\n      name: iconsax_flutter\n      sha256: \"95b65699da8ea98f87c5d232f06b0debaaf1ec1332b697e4d90969ec9a93037d\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.0.0\"\n  image:\n    dependency: transitive\n    description:\n      name: image\n      sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.3.0\"\n  image_picker:\n    dependency: \"direct main\"\n    description:\n      name: image_picker\n      sha256: \"021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.1.2\"\n  image_picker_android:\n    dependency: transitive\n    description:\n      name: image_picker_android\n      sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.8.12+20\"\n  image_picker_for_web:\n    dependency: transitive\n    description:\n      name: image_picker_for_web\n      sha256: \"717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.0.6\"\n  image_picker_ios:\n    dependency: transitive\n    description:\n      name: image_picker_ios\n      sha256: \"05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.8.12+2\"\n  image_picker_linux:\n    dependency: transitive\n    description:\n      name: image_picker_linux\n      sha256: \"4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.2.1+1\"\n  image_picker_macos:\n    dependency: transitive\n    description:\n      name: image_picker_macos\n      sha256: \"3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.2.1+1\"\n  image_picker_platform_interface:\n    dependency: transitive\n    description:\n      name: image_picker_platform_interface\n      sha256: \"886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.10.1\"\n  image_picker_windows:\n    dependency: transitive\n    description:\n      name: image_picker_windows\n      sha256: \"6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.2.1+1\"\n  integration_test:\n    dependency: \"direct dev\"\n    description: flutter\n    source: sdk\n    version: \"0.0.0\"\n  intl:\n    dependency: \"direct main\"\n    description:\n      name: intl\n      sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.19.0\"\n  io:\n    dependency: transitive\n    description:\n      name: io\n      sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.0.5\"\n  irondash_engine_context:\n    dependency: transitive\n    description:\n      name: irondash_engine_context\n      sha256: cd7b769db11a2b5243b037c8a9b1ecaef02e1ae27a2d909ffa78c1dad747bb10\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.5.4\"\n  irondash_message_channel:\n    dependency: transitive\n    description:\n      name: irondash_message_channel\n      sha256: b4101669776509c76133b8917ab8cfc704d3ad92a8c450b92934dd8884a2f060\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.7.0\"\n  isolates:\n    dependency: transitive\n    description:\n      name: isolates\n      sha256: ce89e4141b27b877326d3715be2dceac7a7ba89f3229785816d2d318a75ddf28\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.0.3+8\"\n  js:\n    dependency: transitive\n    description:\n      name: js\n      sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.6.7\"\n  json_annotation:\n    dependency: \"direct main\"\n    description:\n      name: json_annotation\n      sha256: \"1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.9.0\"\n  json_serializable:\n    dependency: \"direct dev\"\n    description:\n      name: json_serializable\n      sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"6.9.0\"\n  keyboard_height_plugin:\n    dependency: \"direct main\"\n    description:\n      name: keyboard_height_plugin\n      sha256: \"3a51c8ebb43465ebe0b3bad17f3b6d945421e58011f3f5a08134afe69a3d775f\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.1.5\"\n  leak_tracker:\n    dependency: \"direct main\"\n    description:\n      name: leak_tracker\n      sha256: \"7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"10.0.7\"\n  leak_tracker_flutter_testing:\n    dependency: transitive\n    description:\n      name: leak_tracker_flutter_testing\n      sha256: \"9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.0.8\"\n  leak_tracker_testing:\n    dependency: transitive\n    description:\n      name: leak_tracker_testing\n      sha256: \"6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.0.1\"\n  linked_scroll_controller:\n    dependency: \"direct main\"\n    description:\n      name: linked_scroll_controller\n      sha256: e6020062bcf4ffc907ee7fd090fa971e65d8dfaac3c62baf601a3ced0b37986a\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.2.0\"\n  linkify:\n    dependency: transitive\n    description:\n      name: linkify\n      sha256: \"4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"5.0.0\"\n  lint:\n    dependency: transitive\n    description:\n      name: lint\n      sha256: \"4a539aa34ec5721a2c7574ae2ca0336738ea4adc2a34887d54b7596310b33c85\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.10.0\"\n  lints:\n    dependency: transitive\n    description:\n      name: lints\n      sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"5.1.1\"\n  loading_indicator:\n    dependency: transitive\n    description:\n      name: loading_indicator\n      sha256: a101ffb2aa3e646137d7810bfa90b50525dd3f72c01235b6df7491cf6af6f284\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.1.1\"\n  local_notifier:\n    dependency: \"direct main\"\n    description:\n      name: local_notifier\n      sha256: f6cfc933c6fbc961f4e52b5c880f68e41b2d3cd29aad557cc654fd211093a025\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.1.6\"\n  logging:\n    dependency: transitive\n    description:\n      name: logging\n      sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.3.0\"\n  macros:\n    dependency: transitive\n    description:\n      name: macros\n      sha256: \"1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.1.3-main.0\"\n  markdown:\n    dependency: \"direct main\"\n    description:\n      name: markdown\n      sha256: \"935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"7.3.0\"\n  markdown_widget:\n    dependency: \"direct main\"\n    description:\n      name: markdown_widget\n      sha256: \"216dced98962d7699a265344624bc280489d739654585ee881c95563a3252fac\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.3.2+6\"\n  matcher:\n    dependency: transitive\n    description:\n      name: matcher\n      sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.12.16+1\"\n  material_color_utilities:\n    dependency: transitive\n    description:\n      name: material_color_utilities\n      sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.11.1\"\n  meta:\n    dependency: transitive\n    description:\n      name: meta\n      sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.15.0\"\n  mime:\n    dependency: \"direct main\"\n    description:\n      name: mime\n      sha256: \"41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.0.0\"\n  mockito:\n    dependency: transitive\n    description:\n      name: mockito\n      sha256: f99d8d072e249f719a5531735d146d8cf04c580d93920b04de75bef6dfb2daf6\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"5.4.5\"\n  mocktail:\n    dependency: \"direct dev\"\n    description:\n      name: mocktail\n      sha256: \"890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.0.4\"\n  nanoid:\n    dependency: \"direct main\"\n    description:\n      name: nanoid\n      sha256: be3f8752d9046c825df2f3914195151eb876f3ad64b9d833dd0b799b77b8759e\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.0.0\"\n  nested:\n    dependency: transitive\n    description:\n      name: nested\n      sha256: \"03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.0.0\"\n  nm:\n    dependency: transitive\n    description:\n      name: nm\n      sha256: \"2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.5.0\"\n  node_preamble:\n    dependency: transitive\n    description:\n      name: node_preamble\n      sha256: \"6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.0.2\"\n  numerus:\n    dependency: \"direct main\"\n    description:\n      name: numerus\n      sha256: a17a3f34527497e89378696a76f382b40dc534c4a57b3778de246ebc1ce2ca99\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.3.0\"\n  octo_image:\n    dependency: transitive\n    description:\n      name: octo_image\n      sha256: \"34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.1.0\"\n  open_filex:\n    dependency: \"direct main\"\n    description:\n      name: open_filex\n      sha256: dcb7bd3d32db8db5260253a62f1564c02c2c8df64bc0187cd213f65f827519bd\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.6.0\"\n  package_config:\n    dependency: transitive\n    description:\n      name: package_config\n      sha256: \"92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.1.1\"\n  package_info_plus:\n    dependency: \"direct main\"\n    description:\n      name: package_info_plus\n      sha256: \"739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"8.1.3\"\n  package_info_plus_platform_interface:\n    dependency: transitive\n    description:\n      name: package_info_plus_platform_interface\n      sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.0.2\"\n  path:\n    dependency: \"direct main\"\n    description:\n      name: path\n      sha256: \"087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.9.0\"\n  path_drawing:\n    dependency: transitive\n    description:\n      name: path_drawing\n      sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.0.1\"\n  path_parsing:\n    dependency: transitive\n    description:\n      name: path_parsing\n      sha256: \"883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.1.0\"\n  path_provider:\n    dependency: \"direct main\"\n    description:\n      name: path_provider\n      sha256: \"50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.1.5\"\n  path_provider_android:\n    dependency: transitive\n    description:\n      name: path_provider_android\n      sha256: \"4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.2.15\"\n  path_provider_foundation:\n    dependency: transitive\n    description:\n      name: path_provider_foundation\n      sha256: \"4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.4.1\"\n  path_provider_linux:\n    dependency: transitive\n    description:\n      name: path_provider_linux\n      sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.2.1\"\n  path_provider_platform_interface:\n    dependency: transitive\n    description:\n      name: path_provider_platform_interface\n      sha256: \"88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.1.2\"\n  path_provider_windows:\n    dependency: transitive\n    description:\n      name: path_provider_windows\n      sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.3.0\"\n  pausable_timer:\n    dependency: transitive\n    description:\n      name: pausable_timer\n      sha256: \"6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.1.0+3\"\n  pdf:\n    dependency: transitive\n    description:\n      name: pdf\n      sha256: \"28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.11.3\"\n  percent_indicator:\n    dependency: \"direct main\"\n    description:\n      name: percent_indicator\n      sha256: c37099ad833a883c9d71782321cb65c3a848c21b6939b6185f0ff6640d05814c\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.2.3\"\n  permission_handler:\n    dependency: \"direct main\"\n    description:\n      path: permission_handler\n      ref: faef1c9\n      resolved-ref: faef1c97970de29995642bfae61b884591798684\n      url: \"https://github.com/LucasXu0/flutter-permission-handler.git\"\n    source: git\n    version: \"12.0.1\"\n  permission_handler_android:\n    dependency: transitive\n    description:\n      name: permission_handler_android\n      sha256: \"1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"13.0.1\"\n  permission_handler_apple:\n    dependency: transitive\n    description:\n      name: permission_handler_apple\n      sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"9.4.7\"\n  permission_handler_html:\n    dependency: transitive\n    description:\n      name: permission_handler_html\n      sha256: \"38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.1.3+5\"\n  permission_handler_platform_interface:\n    dependency: transitive\n    description:\n      name: permission_handler_platform_interface\n      sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.3.0\"\n  permission_handler_windows:\n    dependency: transitive\n    description:\n      name: permission_handler_windows\n      sha256: \"1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.2.1\"\n  petitparser:\n    dependency: transitive\n    description:\n      name: petitparser\n      sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"6.0.2\"\n  pixel_snap:\n    dependency: transitive\n    description:\n      name: pixel_snap\n      sha256: \"677410ea37b07cd37ecb6d5e6c0d8d7615a7cf3bd92ba406fd1ac57e937d1fb0\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.1.5\"\n  platform:\n    dependency: transitive\n    description:\n      name: platform\n      sha256: \"9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.1.5\"\n  plugin_platform_interface:\n    dependency: \"direct dev\"\n    description:\n      name: plugin_platform_interface\n      sha256: \"4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.1.8\"\n  pool:\n    dependency: transitive\n    description:\n      name: pool\n      sha256: \"20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.5.1\"\n  process:\n    dependency: transitive\n    description:\n      name: process\n      sha256: \"21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"5.0.2\"\n  protobuf:\n    dependency: \"direct main\"\n    description:\n      name: protobuf\n      sha256: \"68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.1.0\"\n  provider:\n    dependency: \"direct main\"\n    description:\n      name: provider\n      sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"6.1.2\"\n  pub_semver:\n    dependency: transitive\n    description:\n      name: pub_semver\n      sha256: \"7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.1.5\"\n  pubspec_parse:\n    dependency: transitive\n    description:\n      name: pubspec_parse\n      sha256: \"0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.5.0\"\n  qr:\n    dependency: transitive\n    description:\n      name: qr\n      sha256: \"5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.0.2\"\n  recase:\n    dependency: transitive\n    description:\n      name: recase\n      sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.1.0\"\n  reorderable_tabbar:\n    dependency: \"direct main\"\n    description:\n      path: \".\"\n      ref: \"93c4977\"\n      resolved-ref: \"93c4977ffab68906694cdeaea262be6045543c94\"\n      url: \"https://github.com/LucasXu0/reorderable_tabbar\"\n    source: git\n    version: \"1.0.6\"\n  reorderables:\n    dependency: \"direct main\"\n    description:\n      name: reorderables\n      sha256: \"004a886e4878df1ee27321831c838bc1c976311f4ca6a74ce7d561e506540a77\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.6.0\"\n  run_with_network_images:\n    dependency: \"direct dev\"\n    description:\n      name: run_with_network_images\n      sha256: \"8bf2de4e5120ab24037eda09596408938aa8f5b09f6afabd49683bd01c7baa36\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.0.1\"\n  rxdart:\n    dependency: transitive\n    description:\n      name: rxdart\n      sha256: \"0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.27.7\"\n  saver_gallery:\n    dependency: \"direct main\"\n    description:\n      name: saver_gallery\n      sha256: bf59475e50b73d666630bed7a5fdb621fed92d637f64e3c61ce81653ec6a833c\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.0.1\"\n  scaled_app:\n    dependency: \"direct main\"\n    description:\n      name: scaled_app\n      sha256: a2ad9f22cf2200a5ce455b59c5ea7bfb09a84acfc52452d1db54f4958c99d76a\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.3.0\"\n  screen_retriever:\n    dependency: transitive\n    description:\n      name: screen_retriever\n      sha256: \"570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.2.0\"\n  screen_retriever_linux:\n    dependency: transitive\n    description:\n      name: screen_retriever_linux\n      sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.2.0\"\n  screen_retriever_macos:\n    dependency: transitive\n    description:\n      name: screen_retriever_macos\n      sha256: \"71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.2.0\"\n  screen_retriever_platform_interface:\n    dependency: transitive\n    description:\n      name: screen_retriever_platform_interface\n      sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.2.0\"\n  screen_retriever_windows:\n    dependency: transitive\n    description:\n      name: screen_retriever_windows\n      sha256: \"449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.2.0\"\n  scroll_to_index:\n    dependency: \"direct main\"\n    description:\n      name: scroll_to_index\n      sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.0.1\"\n  scrollable_positioned_list:\n    dependency: \"direct main\"\n    description:\n      name: scrollable_positioned_list\n      sha256: \"1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.3.8\"\n  share_plus:\n    dependency: \"direct main\"\n    description:\n      name: share_plus\n      sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"10.1.4\"\n  share_plus_platform_interface:\n    dependency: transitive\n    description:\n      name: share_plus_platform_interface\n      sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"5.0.2\"\n  shared_preferences:\n    dependency: \"direct main\"\n    description:\n      name: shared_preferences\n      sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.3.5\"\n  shared_preferences_android:\n    dependency: transitive\n    description:\n      name: shared_preferences_android\n      sha256: bf808be89fe9dc467475e982c1db6c2faf3d2acf54d526cd5ec37d86c99dbd84\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.4.1\"\n  shared_preferences_foundation:\n    dependency: transitive\n    description:\n      name: shared_preferences_foundation\n      sha256: \"6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.5.4\"\n  shared_preferences_linux:\n    dependency: transitive\n    description:\n      name: shared_preferences_linux\n      sha256: \"580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.4.1\"\n  shared_preferences_platform_interface:\n    dependency: transitive\n    description:\n      name: shared_preferences_platform_interface\n      sha256: \"57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.4.1\"\n  shared_preferences_web:\n    dependency: transitive\n    description:\n      name: shared_preferences_web\n      sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.4.2\"\n  shared_preferences_windows:\n    dependency: transitive\n    description:\n      name: shared_preferences_windows\n      sha256: \"94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.4.1\"\n  sheet:\n    dependency: \"direct main\"\n    description:\n      path: sheet\n      ref: e44458d\n      resolved-ref: e44458d2359565324e117bb3d41da04f5e60362e\n      url: \"https://github.com/jamesblasco/modal_bottom_sheet\"\n    source: git\n    version: \"1.0.0\"\n  shelf:\n    dependency: transitive\n    description:\n      name: shelf\n      sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.4.2\"\n  shelf_packages_handler:\n    dependency: transitive\n    description:\n      name: shelf_packages_handler\n      sha256: \"89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.0.2\"\n  shelf_static:\n    dependency: transitive\n    description:\n      name: shelf_static\n      sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.1.3\"\n  shelf_web_socket:\n    dependency: transitive\n    description:\n      name: shelf_web_socket\n      sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.0.1\"\n  simple_gesture_detector:\n    dependency: transitive\n    description:\n      name: simple_gesture_detector\n      sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.2.1\"\n  sized_context:\n    dependency: \"direct main\"\n    description:\n      name: sized_context\n      sha256: \"9921e6c09e018132c3e1c6a18e14febbc1cc5c87a200d64ff7578cb49991f6e7\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.0.0+4\"\n  sky_engine:\n    dependency: transitive\n    description: flutter\n    source: sdk\n    version: \"0.0.0\"\n  sliver_tools:\n    dependency: transitive\n    description:\n      name: sliver_tools\n      sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.2.12\"\n  source_gen:\n    dependency: transitive\n    description:\n      name: source_gen\n      sha256: \"14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.5.0\"\n  source_helper:\n    dependency: transitive\n    description:\n      name: source_helper\n      sha256: \"86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.3.5\"\n  source_map_stack_trace:\n    dependency: transitive\n    description:\n      name: source_map_stack_trace\n      sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.1.2\"\n  source_maps:\n    dependency: transitive\n    description:\n      name: source_maps\n      sha256: \"190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.10.13\"\n  source_span:\n    dependency: transitive\n    description:\n      name: source_span\n      sha256: \"53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.10.0\"\n  sprintf:\n    dependency: transitive\n    description:\n      name: sprintf\n      sha256: \"1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"7.0.0\"\n  sqflite:\n    dependency: transitive\n    description:\n      name: sqflite\n      sha256: \"2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.4.1\"\n  sqflite_android:\n    dependency: transitive\n    description:\n      name: sqflite_android\n      sha256: \"78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.4.0\"\n  sqflite_common:\n    dependency: transitive\n    description:\n      name: sqflite_common\n      sha256: \"761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.5.4+6\"\n  sqflite_darwin:\n    dependency: transitive\n    description:\n      name: sqflite_darwin\n      sha256: \"22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.4.1+1\"\n  sqflite_platform_interface:\n    dependency: transitive\n    description:\n      name: sqflite_platform_interface\n      sha256: \"8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.4.0\"\n  stack_trace:\n    dependency: transitive\n    description:\n      name: stack_trace\n      sha256: \"9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.12.0\"\n  stream_channel:\n    dependency: transitive\n    description:\n      name: stream_channel\n      sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.1.2\"\n  stream_transform:\n    dependency: transitive\n    description:\n      name: stream_transform\n      sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.1.1\"\n  string_scanner:\n    dependency: transitive\n    description:\n      name: string_scanner\n      sha256: \"688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.3.0\"\n  string_validator:\n    dependency: \"direct main\"\n    description:\n      name: string_validator\n      sha256: a278d038104aa2df15d0e09c47cb39a49f907260732067d0034dc2f2e4e2ac94\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.1.0\"\n  styled_widget:\n    dependency: \"direct main\"\n    description:\n      name: styled_widget\n      sha256: \"4d439802919b6ccf10d1488798656da8804633b03012682dd1c8ca70a084aa84\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.4.1\"\n  super_clipboard:\n    dependency: \"direct main\"\n    description:\n      name: super_clipboard\n      sha256: \"4a6ae6dfaa282ec1f2bff750976f535517ed8ca842d5deae13985eb11c00ac1f\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.8.24\"\n  super_native_extensions:\n    dependency: transitive\n    description:\n      name: super_native_extensions\n      sha256: a433bba8186cd6b707560c42535bf284804665231c00bca86faf1aa4968b7637\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.8.24\"\n  sync_http:\n    dependency: transitive\n    description:\n      name: sync_http\n      sha256: \"7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.3.1\"\n  synchronized:\n    dependency: \"direct main\"\n    description:\n      name: synchronized\n      sha256: \"69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.3.0+3\"\n  tab_indicator_styler:\n    dependency: transitive\n    description:\n      name: tab_indicator_styler\n      sha256: \"9e7e90367e20f71f3882fc6578fdcced35ab1c66ab20fcb623cdcc20d2796c76\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.0.0\"\n  table_calendar:\n    dependency: \"direct main\"\n    description:\n      name: table_calendar\n      sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.1.3\"\n  talker:\n    dependency: \"direct main\"\n    description:\n      name: talker\n      sha256: f4b3f6110b03f78ef314f897322e47c6be2b0fbe6fafa62e4041ac5321e88620\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.8.1\"\n  talker_bloc_logger:\n    dependency: \"direct main\"\n    description:\n      name: talker_bloc_logger\n      sha256: \"2f3ccf88c473105b7fecc4a81289f4345ee5aa652dd2f43108a60dded08afc4a\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.8.1\"\n  talker_logger:\n    dependency: transitive\n    description:\n      name: talker_logger\n      sha256: \"4e526350aa917d8c68eeded19604ce82ffe68ceeb9fd803225d30a12924ca506\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.8.1\"\n  term_glyph:\n    dependency: transitive\n    description:\n      name: term_glyph\n      sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.2.1\"\n  test:\n    dependency: transitive\n    description:\n      name: test\n      sha256: \"713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.25.8\"\n  test_api:\n    dependency: transitive\n    description:\n      name: test_api\n      sha256: \"664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.7.3\"\n  test_core:\n    dependency: transitive\n    description:\n      name: test_core\n      sha256: \"12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.6.5\"\n  time:\n    dependency: \"direct main\"\n    description:\n      name: time\n      sha256: \"370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.1.5\"\n  timing:\n    dependency: transitive\n    description:\n      name: timing\n      sha256: \"62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.0.2\"\n  toastification:\n    dependency: \"direct main\"\n    description:\n      name: toastification\n      sha256: \"4d97fbfa463dfe83691044cba9f37cb185a79bb9205cfecb655fa1f6be126a13\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.3.0\"\n  tuple:\n    dependency: transitive\n    description:\n      name: tuple\n      sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.0.2\"\n  typed_data:\n    dependency: transitive\n    description:\n      name: typed_data\n      sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.4.0\"\n  universal_html:\n    dependency: transitive\n    description:\n      name: universal_html\n      sha256: \"56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.2.4\"\n  universal_io:\n    dependency: transitive\n    description:\n      name: universal_io\n      sha256: \"1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.2.2\"\n  universal_platform:\n    dependency: \"direct main\"\n    description:\n      name: universal_platform\n      sha256: \"64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.1.0\"\n  unsplash_client:\n    dependency: \"direct main\"\n    description:\n      path: \".\"\n      ref: a8411fc\n      resolved-ref: a8411fcead178834d1f4572f64dc78b059ffa221\n      url: \"https://github.com/LucasXu0/unsplash_client.git\"\n    source: git\n    version: \"2.2.0\"\n  url_launcher:\n    dependency: \"direct main\"\n    description:\n      name: url_launcher\n      sha256: \"9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"6.3.1\"\n  url_launcher_android:\n    dependency: transitive\n    description:\n      name: url_launcher_android\n      sha256: \"6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"6.3.14\"\n  url_launcher_ios:\n    dependency: transitive\n    description:\n      name: url_launcher_ios\n      sha256: \"16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"6.3.2\"\n  url_launcher_linux:\n    dependency: transitive\n    description:\n      name: url_launcher_linux\n      sha256: \"4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.2.1\"\n  url_launcher_macos:\n    dependency: transitive\n    description:\n      name: url_launcher_macos\n      sha256: \"17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.2.2\"\n  url_launcher_platform_interface:\n    dependency: \"direct dev\"\n    description:\n      name: url_launcher_platform_interface\n      sha256: \"552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.3.2\"\n  url_launcher_web:\n    dependency: transitive\n    description:\n      name: url_launcher_web\n      sha256: \"3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.4.0\"\n  url_launcher_windows:\n    dependency: transitive\n    description:\n      name: url_launcher_windows\n      sha256: \"3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.1.4\"\n  url_protocol:\n    dependency: \"direct main\"\n    description:\n      path: \".\"\n      ref: HEAD\n      resolved-ref: \"77a84201ed8ca50082f4248f3a373d053b1c0462\"\n      url: \"https://github.com/LucasXu0/flutter_url_protocol.git\"\n    source: git\n    version: \"1.0.0\"\n  uuid:\n    dependency: \"direct overridden\"\n    description:\n      name: uuid\n      sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.5.1\"\n  value_layout_builder:\n    dependency: transitive\n    description:\n      name: value_layout_builder\n      sha256: c02511ea91ca5c643b514a33a38fa52536f74aa939ec367d02938b5ede6807fa\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.4.0\"\n  vector_graphics:\n    dependency: transitive\n    description:\n      name: vector_graphics\n      sha256: \"27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.1.15\"\n  vector_graphics_codec:\n    dependency: transitive\n    description:\n      name: vector_graphics_codec\n      sha256: \"99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.1.13\"\n  vector_graphics_compiler:\n    dependency: transitive\n    description:\n      name: vector_graphics_compiler\n      sha256: \"1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.1.16\"\n  vector_math:\n    dependency: transitive\n    description:\n      name: vector_math\n      sha256: \"80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.1.4\"\n  version:\n    dependency: \"direct main\"\n    description:\n      name: version\n      sha256: \"3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.0.2\"\n  visibility_detector:\n    dependency: transitive\n    description:\n      name: visibility_detector\n      sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.4.0+2\"\n  vm_service:\n    dependency: transitive\n    description:\n      name: vm_service\n      sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"14.3.0\"\n  watcher:\n    dependency: transitive\n    description:\n      name: watcher\n      sha256: \"69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.1.1\"\n  web:\n    dependency: transitive\n    description:\n      name: web\n      sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.1.0\"\n  web_socket:\n    dependency: transitive\n    description:\n      name: web_socket\n      sha256: \"3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.1.6\"\n  web_socket_channel:\n    dependency: transitive\n    description:\n      name: web_socket_channel\n      sha256: \"9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.0.1\"\n  webdriver:\n    dependency: transitive\n    description:\n      name: webdriver\n      sha256: \"3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.0.4\"\n  webkit_inspection_protocol:\n    dependency: transitive\n    description:\n      name: webkit_inspection_protocol\n      sha256: \"87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.2.1\"\n  webview_flutter:\n    dependency: transitive\n    description:\n      name: webview_flutter\n      sha256: \"889a0a678e7c793c308c68739996227c9661590605e70b1f6cf6b9a6634f7aec\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.10.0\"\n  webview_flutter_android:\n    dependency: transitive\n    description:\n      name: webview_flutter_android\n      sha256: d1ee28f44894cbabb1d94cc42f9980297f689ff844d067ec50ff88d86e27d63f\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"4.3.0\"\n  webview_flutter_platform_interface:\n    dependency: transitive\n    description:\n      name: webview_flutter_platform_interface\n      sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"2.10.0\"\n  webview_flutter_plus:\n    dependency: transitive\n    description:\n      name: webview_flutter_plus\n      sha256: f883dfc94d03b1a2a17441c8e8a8e1941558ed3322f2b586cd06486114e18048\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"0.4.10\"\n  webview_flutter_wkwebview:\n    dependency: transitive\n    description:\n      name: webview_flutter_wkwebview\n      sha256: \"4adc14ea9a770cc9e2c8f1ac734536bd40e82615bd0fa6b94be10982de656cc7\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.17.0\"\n  win32:\n    dependency: transitive\n    description:\n      name: win32\n      sha256: \"154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"5.10.0\"\n  win32_registry:\n    dependency: transitive\n    description:\n      name: win32_registry\n      sha256: \"21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.1.5\"\n  window_manager:\n    dependency: \"direct main\"\n    description:\n      path: \"packages/window_manager\"\n      ref: \"5a43aed\"\n      resolved-ref: \"5a43aed33ffd9f86e1bac2fc9f4931383cc2ee2b\"\n      url: \"https://github.com/leanflutter/window_manager.git\"\n    source: git\n    version: \"0.4.3\"\n  xdg_directories:\n    dependency: transitive\n    description:\n      name: xdg_directories\n      sha256: \"7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15\"\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"1.1.0\"\n  xml:\n    dependency: \"direct main\"\n    description:\n      name: xml\n      sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"6.5.0\"\n  yaml:\n    dependency: transitive\n    description:\n      name: yaml\n      sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce\n      url: \"https://pub.dev\"\n    source: hosted\n    version: \"3.1.3\"\nsdks:\n  dart: \">=3.6.2 <4.0.0\"\n  flutter: \">=3.27.4\"\n"
  },
  {
    "path": "frontend/appflowy_flutter/pubspec.yaml",
    "content": "name: appflowy\ndescription: Bring projects, wikis, and teams together with AI. AppFlowy is an\n  AI collaborative workspace where you achieve more without losing control of\n  your data. The best open source alternative to Notion.\npublish_to: \"none\"\n\nversion: 0.9.9\n\nenvironment:\n  flutter: \">=3.27.4\"\n  sdk: \">=3.3.0 <4.0.0\"\n\ndependencies:\n  any_date: ^1.0.4\n  app_links: ^6.3.3\n  appflowy_backend:\n    path: packages/appflowy_backend\n  appflowy_board:\n    git:\n      url: https://github.com/AppFlowy-IO/appflowy-board.git\n      ref: e8317c0d1af8d23dc5707b02ea43864536b6de91\n  appflowy_editor:\n  appflowy_editor_plugins:\n  appflowy_popover:\n    path: packages/appflowy_popover\n  appflowy_result:\n    path: packages/appflowy_result\n  appflowy_ui:\n    path: packages/appflowy_ui\n  archive: ^3.4.10\n  auto_size_text_field: ^2.2.3\n  auto_updater: ^1.0.0\n  avatar_stack: ^3.0.0\n\n  # BitsDojo Window for Windows\n  bitsdojo_window: ^0.1.6\n  bloc: ^9.0.0\n  cached_network_image: ^3.3.0\n  calendar_view:\n    git:\n      url: https://github.com/Xazin/flutter_calendar_view\n      ref: \"6fe0c98\"\n  collection: ^1.17.1\n  connectivity_plus: ^5.0.2\n  cross_file: ^0.3.4+1\n\n  # Desktop Drop uses Cross File (XFile) data type\n  defer_pointer: ^0.0.2\n  desktop_drop: ^0.5.0\n  device_info_plus:\n  diffutil_dart: ^4.0.1\n  dotted_border: ^2.0.0+3\n  easy_localization: ^3.0.2\n  envied: ^1.0.1\n  equatable: ^2.0.5\n  expandable: ^5.0.1\n  extended_text_field: ^16.0.2\n  extended_text_library: ^12.0.0\n  file: ^7.0.0\n  fixnum: ^1.1.0\n  flex_color_picker: ^3.5.1\n  flowy_infra:\n    path: packages/flowy_infra\n  flowy_infra_ui:\n    path: packages/flowy_infra_ui\n  flowy_svg:\n    path: packages/flowy_svg\n  flutter:\n    sdk: flutter\n  flutter_animate: ^4.5.0\n  flutter_bloc: ^9.1.0\n  flutter_cache_manager: ^3.3.1\n  flutter_chat_core: 0.0.2\n  flutter_chat_ui: ^2.0.0-dev.1\n  flutter_emoji_mart:\n    git:\n      url: https://github.com/LucasXu0/emoji_mart.git\n      ref: \"355aa56\"\n  flutter_math_fork: ^0.7.3\n  flutter_slidable: ^3.0.0\n\n  flutter_staggered_grid_view: ^0.7.0\n  flutter_tex: ^4.0.9\n  fluttertoast: ^8.2.6\n  freezed_annotation: ^2.2.0\n  get_it: ^8.0.3\n  go_router: ^14.2.0\n  google_fonts: ^6.1.0\n  highlight: ^0.7.0\n  hive_flutter: ^1.1.0\n  hotkey_manager: ^0.1.7\n  html2md: ^1.3.2\n  http: ^1.0.0\n  image_picker: ^1.0.4\n\n  #  third party packages\n  intl: ^0.19.0\n  json_annotation: ^4.8.1\n  keyboard_height_plugin: ^0.1.5\n  leak_tracker: ^10.0.0\n  linked_scroll_controller: ^0.2.0\n\n  # Notifications\n  # TODO: Consider implementing custom package\n  # to gather notification handling for all platforms\n  local_notifier: ^0.1.5\n  markdown:\n  markdown_widget: ^2.3.2+6\n  mime: ^2.0.0\n  nanoid: ^1.0.0\n  numerus: ^2.1.2\n  # Used to open local files on Mobile\n  open_filex: ^4.5.0\n  package_info_plus: ^8.0.2\n  path: ^1.8.3\n  path_provider: ^2.0.15\n  percent_indicator: 4.2.3\n  permission_handler: ^11.3.1\n  protobuf: ^3.1.0\n  provider: ^6.0.5\n  reorderable_tabbar: ^1.0.6\n  reorderables: ^0.6.0\n  scaled_app: ^2.3.0\n  scroll_to_index: ^3.0.1\n  scrollable_positioned_list: ^0.3.8\n  share_plus: ^10.0.2\n  shared_preferences: ^2.2.2\n  sheet:\n  sized_context: ^1.0.0+4\n  string_validator: ^1.0.0\n  styled_widget: ^0.4.1\n  super_clipboard: ^0.8.24\n  synchronized: ^3.1.0+1\n  table_calendar: ^3.0.9\n  time: ^2.1.3\n  event_bus: ^2.0.1\n\n  toastification: ^2.0.0\n  universal_platform: ^1.1.0\n  unsplash_client: ^2.1.1\n  url_launcher: ^6.1.11\n  url_protocol:\n\n  # Window Manager for MacOS and Linux\n  version: ^3.0.2\n  xml: ^6.5.0\n  window_manager: ^0.4.3\n  saver_gallery: ^4.0.1\n  talker_bloc_logger: ^4.8.1\n  talker: ^4.7.1\n\n  analyzer: 6.11.0\n\ndev_dependencies:\n  # Introduce talker to log the bloc events, and only log the events in the development mode\n\n  bloc_test: ^10.0.0\n  build_runner: ^2.4.9\n  envied_generator: ^1.0.1\n  flutter_lints: ^5.0.0\n\n  flutter_test:\n    sdk: flutter\n  freezed: ^2.4.7\n  integration_test:\n    sdk: flutter\n  json_serializable: ^6.7.1\n\n  mocktail: ^1.0.1\n\n  plugin_platform_interface: any\n  run_with_network_images: ^0.0.1\n  url_launcher_platform_interface: any\n\ndependency_overrides:\n  http: ^1.0.0\n  device_info_plus: ^11.2.2\n\n  url_protocol:\n    git:\n      url: https://github.com/LucasXu0/flutter_url_protocol.git\n      commit: 737681d\n\n  appflowy_editor:\n    git:\n      url: https://github.com/AppFlowy-IO/appflowy-editor.git\n      ref: \"470c4e7\"\n\n  appflowy_editor_plugins:\n    git:\n      url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git\n      path: \"packages/appflowy_editor_plugins\"\n      ref: \"4efcff7\"\n\n  sheet:\n    git:\n      url: https://github.com/jamesblasco/modal_bottom_sheet\n      ref: e44458d\n      path: sheet\n\n  window_manager:\n    git:\n      url: https://github.com/leanflutter/window_manager.git\n      ref: \"5a43aed\"\n      path: packages/window_manager\n\n  uuid: ^4.4.0\n\n  flutter_cache_manager:\n    git:\n      url: https://github.com/LucasXu0/flutter_cache_manager.git\n      commit: fbab857b1b1d209240a146d32f496379b9f62276\n      path: flutter_cache_manager\n\n  flutter_sticky_header: ^0.7.0\n\n  reorderable_tabbar:\n    git:\n      url: https://github.com/LucasXu0/reorderable_tabbar\n      ref: 93c4977\n  # Don't upgrade file_picker until the issue is fixed\n  # https://github.com/miguelpruivo/flutter_file_picker/issues/1652\n  file_picker: 8.1.4\n\n  auto_updater:\n    git:\n      url: https://github.com/LucasXu0/auto_updater.git\n      path: packages/auto_updater\n      ref: 1d81a824f3633f1d0200ba51b78fe0f9ce429458\n\n  auto_updater_macos:\n    git:\n      url: https://github.com/LucasXu0/auto_updater.git\n      path: packages/auto_updater_macos\n      ref: 1d81a824f3633f1d0200ba51b78fe0f9ce429458\n\n  auto_updater_platform_interface:\n    git:\n      url: https://github.com/LucasXu0/auto_updater.git\n      path: packages/auto_updater_platform_interface\n      ref: 1d81a824f3633f1d0200ba51b78fe0f9ce429458\n\n  unsplash_client:\n    git:\n      url: https://github.com/LucasXu0/unsplash_client.git\n      ref: a8411fc\n\n  # https://github.com/LucasXu0/flutter-permission-handler/commit/faef1c97970de29995642bfae61b884591798684\n  # Prevent the location from being accessed continuously on Windows\n  permission_handler:\n    git:\n      url: https://github.com/LucasXu0/flutter-permission-handler.git\n      path: permission_handler\n      ref: faef1c9\n\nflutter:\n  generate: true\n  uses-material-design: true\n\n  fonts:\n    - family: Poppins\n      fonts:\n        - asset: assets/google_fonts/Poppins/Poppins-ExtraLight.ttf\n          weight: 100\n        - asset: assets/google_fonts/Poppins/Poppins-Thin.ttf\n          weight: 200\n        - asset: assets/google_fonts/Poppins/Poppins-Light.ttf\n          weight: 300\n        - asset: assets/google_fonts/Poppins/Poppins-Regular.ttf\n          weight: 400\n        - asset: assets/google_fonts/Poppins/Poppins-Medium.ttf\n          weight: 500\n        - asset: assets/google_fonts/Poppins/Poppins-SemiBold.ttf\n          weight: 600\n        - asset: assets/google_fonts/Poppins/Poppins-Bold.ttf\n          weight: 700\n        - asset: assets/google_fonts/Poppins/Poppins-Black.ttf\n          weight: 800\n        - asset: assets/google_fonts/Poppins/Poppins-ExtraBold.ttf\n          weight: 900\n    - family: RobotoMono\n      fonts:\n        - asset: assets/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf\n        - asset: assets/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf\n          style: italic\n    # White-label font configuration will be added here\n    # BEGIN: WHITE_LABEL_FONT\n    # END: WHITE_LABEL_FONT\n\n  # To add assets to your application, add an assets section, like this:\n  assets:\n    - assets/images/\n    - assets/images/appearance/\n    - assets/images/built_in_cover_images/\n    - assets/flowy_icons/\n    - assets/flowy_icons/16x/\n    - assets/flowy_icons/20x/\n    - assets/flowy_icons/24x/\n    - assets/flowy_icons/32x/\n    - assets/flowy_icons/40x/\n    - assets/images/emoji/\n    - assets/images/login/\n    - assets/translations/\n    - assets/icons/icons.json\n    - assets/fonts/\n    - assets/built_in_prompts.json\n\n    # The following assets will be excluded in release.\n    # BEGIN: EXCLUDE_IN_RELEASE\n    - assets/test/workspaces/\n    - assets/test/images/\n    - assets/template/\n    - assets/test/workspaces/markdowns/\n    - assets/test/workspaces/database/\n    # END: EXCLUDE_IN_RELEASE\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/ai/ai.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:bloc_test/bloc_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nimport '../../util.dart';\n\nconst _aiResponse = 'UPDATED:';\n\nclass _MockCompletionStream extends Mock implements CompletionStream {}\n\nclass _MockAIRepository extends Mock implements AppFlowyAIService {\n  @override\n  Future<(String, CompletionStream)?> streamCompletion({\n    String? objectId,\n    required String text,\n    PredefinedFormat? format,\n    String? promptId,\n    List<String> sourceIds = const [],\n    List<AiWriterRecord> history = const [],\n    required CompletionTypePB completionType,\n    required Future<void> Function() onStart,\n    required Future<void> Function(String text) processMessage,\n    required Future<void> Function(String text) processAssistMessage,\n    required Future<void> Function() onEnd,\n    required void Function(AIError error) onError,\n    required void Function(LocalAIStreamingState state)\n        onLocalAIStreamingStateChange,\n  }) async {\n    final stream = _MockCompletionStream();\n    unawaited(\n      Future(() async {\n        await onStart();\n        final lines = text.split('\\n');\n        for (final line in lines) {\n          if (line.isNotEmpty) {\n            await processMessage('$_aiResponse $line\\n\\n');\n          }\n        }\n        await onEnd();\n      }),\n    );\n    return ('mock_id', stream);\n  }\n}\n\nclass _MockAIRepositoryLess extends Mock implements AppFlowyAIService {\n  @override\n  Future<(String, CompletionStream)?> streamCompletion({\n    String? objectId,\n    required String text,\n    PredefinedFormat? format,\n    String? promptId,\n    List<String> sourceIds = const [],\n    List<AiWriterRecord> history = const [],\n    required CompletionTypePB completionType,\n    required Future<void> Function() onStart,\n    required Future<void> Function(String text) processMessage,\n    required Future<void> Function(String text) processAssistMessage,\n    required Future<void> Function() onEnd,\n    required void Function(AIError error) onError,\n    required void Function(LocalAIStreamingState state)\n        onLocalAIStreamingStateChange,\n  }) async {\n    final stream = _MockCompletionStream();\n    unawaited(\n      Future(() async {\n        await onStart();\n        // only return 1 line.\n        await processMessage('Hello World');\n        await onEnd();\n      }),\n    );\n    return ('mock_id', stream);\n  }\n}\n\nclass _MockAIRepositoryMore extends Mock implements AppFlowyAIService {\n  @override\n  Future<(String, CompletionStream)?> streamCompletion({\n    String? objectId,\n    required String text,\n    PredefinedFormat? format,\n    String? promptId,\n    List<String> sourceIds = const [],\n    List<AiWriterRecord> history = const [],\n    required CompletionTypePB completionType,\n    required Future<void> Function() onStart,\n    required Future<void> Function(String text) processMessage,\n    required Future<void> Function(String text) processAssistMessage,\n    required Future<void> Function() onEnd,\n    required void Function(AIError error) onError,\n    required void Function(LocalAIStreamingState state)\n        onLocalAIStreamingStateChange,\n  }) async {\n    final stream = _MockCompletionStream();\n    unawaited(\n      Future(() async {\n        await onStart();\n        // return 10 lines\n        for (var i = 0; i < 10; i++) {\n          await processMessage('Hello World\\n\\n');\n        }\n        await onEnd();\n      }),\n    );\n    return ('mock_id', stream);\n  }\n}\n\nclass _MockErrorRepository extends Mock implements AppFlowyAIService {\n  @override\n  Future<(String, CompletionStream)?> streamCompletion({\n    String? objectId,\n    required String text,\n    PredefinedFormat? format,\n    String? promptId,\n    List<String> sourceIds = const [],\n    List<AiWriterRecord> history = const [],\n    required CompletionTypePB completionType,\n    required Future<void> Function() onStart,\n    required Future<void> Function(String text) processMessage,\n    required Future<void> Function(String text) processAssistMessage,\n    required Future<void> Function() onEnd,\n    required void Function(AIError error) onError,\n    required void Function(LocalAIStreamingState state)\n        onLocalAIStreamingStateChange,\n  }) async {\n    final stream = _MockCompletionStream();\n    unawaited(\n      Future(() async {\n        await onStart();\n        onError(\n          const AIError(\n            message: 'Error',\n            code: AIErrorCode.aiResponseLimitExceeded,\n          ),\n        );\n      }),\n    );\n    return ('mock_id', stream);\n  }\n}\n\nvoid registerMockRepository(AppFlowyAIService mock) {\n  if (getIt.isRegistered<AIRepository>()) {\n    getIt.unregister<AIRepository>();\n  }\n  getIt.registerFactory<AIRepository>(() => mock);\n}\n\nvoid main() {\n  group('AIWriterCubit:', () {\n    const text1 = '1. Select text to style using the toolbar menu.';\n    const text2 = '2. Discover more styling options in Aa.';\n    const text3 =\n        '3. AppFlowy empowers you to beautifully and effortlessly style your content.';\n\n    setUp(() {\n      TestWidgetsFlutterBinding.ensureInitialized();\n    });\n\n    blocTest<AiWriterCubit, AiWriterState>(\n      'send request before the bloc is initialized',\n      build: () {\n        final document = Document(\n          root: pageNode(\n            children: [\n              paragraphNode(text: text1),\n              paragraphNode(text: text2),\n              paragraphNode(text: text3),\n            ],\n          ),\n        );\n        final selection = Selection(\n          start: Position(path: [0]),\n          end: Position(path: [2], offset: text3.length),\n        );\n        final editorState = EditorState(document: document)\n          ..selection = selection;\n        registerMockRepository(_MockAIRepository());\n        return AiWriterCubit(\n          documentId: '',\n          editorState: editorState,\n        );\n      },\n      act: (bloc) => bloc.register(\n        aiWriterNode(\n          command: AiWriterCommand.explain,\n          selection: Selection(\n            start: Position(path: [0]),\n            end: Position(path: [2], offset: text3.length),\n          ),\n        ),\n      ),\n      wait: Duration(seconds: 1),\n      expect: () => [\n        isA<GeneratingAiWriterState>()\n            .having((s) => s.markdownText, 'result', isEmpty),\n        isA<GeneratingAiWriterState>()\n            .having((s) => s.markdownText, 'result', isNotEmpty)\n            .having((s) => s.markdownText, 'result', contains('UPDATED:')),\n        isA<GeneratingAiWriterState>()\n            .having((s) => s.markdownText, 'result', isNotEmpty)\n            .having((s) => s.markdownText, 'result', contains('UPDATED:')),\n        isA<GeneratingAiWriterState>()\n            .having((s) => s.markdownText, 'result', isNotEmpty)\n            .having((s) => s.markdownText, 'result', contains('UPDATED:')),\n        isA<ReadyAiWriterState>()\n            .having((s) => s.markdownText, 'result', isNotEmpty)\n            .having((s) => s.markdownText, 'result', contains('UPDATED:')),\n      ],\n    );\n\n    blocTest<AiWriterCubit, AiWriterState>(\n      'exceed the ai response limit',\n      build: () {\n        const text1 = '1. Select text to style using the toolbar menu.';\n        const text2 = '2. Discover more styling options in Aa.';\n        const text3 =\n            '3. AppFlowy empowers you to beautifully and effortlessly style your content.';\n        final document = Document(\n          root: pageNode(\n            children: [\n              paragraphNode(text: text1),\n              paragraphNode(text: text2),\n              paragraphNode(text: text3),\n            ],\n          ),\n        );\n        final selection = Selection(\n          start: Position(path: [0]),\n          end: Position(path: [2], offset: text3.length),\n        );\n        final editorState = EditorState(document: document)\n          ..selection = selection;\n        registerMockRepository(_MockErrorRepository());\n        return AiWriterCubit(\n          documentId: '',\n          editorState: editorState,\n        );\n      },\n      act: (bloc) => bloc.register(\n        aiWriterNode(\n          command: AiWriterCommand.explain,\n          selection: Selection(\n            start: Position(path: [0]),\n            end: Position(path: [2], offset: text3.length),\n          ),\n        ),\n      ),\n      wait: Duration(seconds: 1),\n      expect: () => [\n        isA<GeneratingAiWriterState>()\n            .having((s) => s.markdownText, 'result', isEmpty),\n        isA<ErrorAiWriterState>().having(\n          (s) => s.error.code,\n          'error code',\n          AIErrorCode.aiResponseLimitExceeded,\n        ),\n      ],\n    );\n\n    test('improve writing - the result contains the same number of paragraphs',\n        () async {\n      final selection = Selection(\n        start: Position(path: [0]),\n        end: Position(path: [2], offset: text3.length),\n      );\n      final document = Document(\n        root: pageNode(\n          children: [\n            paragraphNode(text: text1),\n            paragraphNode(text: text2),\n            paragraphNode(text: text3),\n            aiWriterNode(\n              command: AiWriterCommand.improveWriting,\n              selection: selection,\n            ),\n          ],\n        ),\n      );\n      final editorState = EditorState(document: document)\n        ..selection = selection;\n      final aiNode = editorState.getNodeAtPath([3])!;\n      registerMockRepository(_MockAIRepository());\n      final bloc = AiWriterCubit(\n        documentId: '',\n        editorState: editorState,\n      );\n      bloc.register(aiNode);\n      await blocResponseFuture();\n      bloc.runResponseAction(SuggestionAction.accept);\n      await blocResponseFuture();\n      expect(\n        editorState.document.root.children.length,\n        3,\n      );\n      expect(\n        editorState.getNodeAtPath([0])!.delta!.toPlainText(),\n        '$_aiResponse $text1',\n      );\n      expect(\n        editorState.getNodeAtPath([1])!.delta!.toPlainText(),\n        '$_aiResponse $text2',\n      );\n      expect(\n        editorState.getNodeAtPath([2])!.delta!.toPlainText(),\n        '$_aiResponse $text3',\n      );\n    });\n\n    test('improve writing - discard', () async {\n      final selection = Selection(\n        start: Position(path: [0]),\n        end: Position(path: [2], offset: text3.length),\n      );\n      final document = Document(\n        root: pageNode(\n          children: [\n            paragraphNode(text: text1),\n            paragraphNode(text: text2),\n            paragraphNode(text: text3),\n            aiWriterNode(\n              command: AiWriterCommand.improveWriting,\n              selection: selection,\n            ),\n          ],\n        ),\n      );\n      final editorState = EditorState(document: document)\n        ..selection = selection;\n      final aiNode = editorState.getNodeAtPath([3])!;\n      registerMockRepository(_MockAIRepository());\n      final bloc = AiWriterCubit(\n        documentId: '',\n        editorState: editorState,\n      );\n      bloc.register(aiNode);\n      await blocResponseFuture();\n      bloc.runResponseAction(SuggestionAction.discard);\n      await blocResponseFuture();\n      expect(\n        editorState.document.root.children.length,\n        3,\n      );\n      expect(editorState.getNodeAtPath([0])!.delta!.toPlainText(), text1);\n      expect(editorState.getNodeAtPath([1])!.delta!.toPlainText(), text2);\n      expect(editorState.getNodeAtPath([2])!.delta!.toPlainText(), text3);\n    });\n\n    test('improve writing - the result less than the original text', () async {\n      final selection = Selection(\n        start: Position(path: [0]),\n        end: Position(path: [2], offset: text3.length),\n      );\n      final document = Document(\n        root: pageNode(\n          children: [\n            paragraphNode(text: text1),\n            paragraphNode(text: text2),\n            paragraphNode(text: text3),\n            aiWriterNode(\n              command: AiWriterCommand.improveWriting,\n              selection: selection,\n            ),\n          ],\n        ),\n      );\n      final editorState = EditorState(document: document)\n        ..selection = selection;\n      final aiNode = editorState.getNodeAtPath([3])!;\n      registerMockRepository(_MockAIRepositoryLess());\n      final bloc = AiWriterCubit(\n        documentId: '',\n        editorState: editorState,\n      );\n      bloc.register(aiNode);\n      await blocResponseFuture();\n      bloc.runResponseAction(SuggestionAction.accept);\n      await blocResponseFuture();\n      expect(editorState.document.root.children.length, 2);\n      expect(\n        editorState.getNodeAtPath([0])!.delta!.toPlainText(),\n        'Hello World',\n      );\n    });\n\n    test('improve writing - the result more than the original text', () async {\n      final selection = Selection(\n        start: Position(path: [0]),\n        end: Position(path: [2], offset: text3.length),\n      );\n      final document = Document(\n        root: pageNode(\n          children: [\n            paragraphNode(text: text1),\n            paragraphNode(text: text2),\n            paragraphNode(text: text3),\n            aiWriterNode(\n              command: AiWriterCommand.improveWriting,\n              selection: selection,\n            ),\n          ],\n        ),\n      );\n      final editorState = EditorState(document: document)\n        ..selection = selection;\n      final aiNode = editorState.getNodeAtPath([3])!;\n      registerMockRepository(_MockAIRepositoryMore());\n      final bloc = AiWriterCubit(\n        documentId: '',\n        editorState: editorState,\n      );\n      bloc.register(aiNode);\n      await blocResponseFuture();\n      bloc.runResponseAction(SuggestionAction.accept);\n      await blocResponseFuture();\n      expect(editorState.document.root.children.length, 10);\n      for (var i = 0; i < 10; i++) {\n        expect(\n          editorState.getNodeAtPath([i])!.delta!.toPlainText(),\n          'Hello World',\n        );\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/app_setting_test/appearance_test.dart",
    "content": "import 'package:appflowy/user/application/user_settings_service.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';\nimport 'package:bloc_test/bloc_test.dart';\nimport 'package:flowy_infra/theme.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../util.dart';\n\nvoid main() {\n  // ignore: unused_local_variable\n  late AppFlowyUnitTest context;\n  setUpAll(() async {\n    context = await AppFlowyUnitTest.ensureInitialized();\n  });\n\n  group('$AppearanceSettingsCubit', () {\n    late AppearanceSettingsPB appearanceSetting;\n    late DateTimeSettingsPB dateTimeSettings;\n\n    setUp(() async {\n      appearanceSetting =\n          await UserSettingsBackendService().getAppearanceSetting();\n      dateTimeSettings =\n          await UserSettingsBackendService().getDateTimeSettings();\n      await blocResponseFuture();\n    });\n\n    blocTest<AppearanceSettingsCubit, AppearanceSettingsState>(\n      'default theme',\n      build: () => AppearanceSettingsCubit(\n        appearanceSetting,\n        dateTimeSettings,\n        AppTheme.fallback,\n      ),\n      verify: (bloc) {\n        expect(bloc.state.font, defaultFontFamily);\n        expect(bloc.state.themeMode, ThemeMode.system);\n      },\n    );\n\n    blocTest<AppearanceSettingsCubit, AppearanceSettingsState>(\n      'save key/value',\n      build: () => AppearanceSettingsCubit(\n        appearanceSetting,\n        dateTimeSettings,\n        AppTheme.fallback,\n      ),\n      act: (bloc) {\n        bloc.setKeyValue(\"123\", \"456\");\n      },\n      verify: (bloc) {\n        expect(bloc.getValue(\"123\"), \"456\");\n      },\n    );\n\n    blocTest<AppearanceSettingsCubit, AppearanceSettingsState>(\n      'remove key/value',\n      build: () => AppearanceSettingsCubit(\n        appearanceSetting,\n        dateTimeSettings,\n        AppTheme.fallback,\n      ),\n      act: (bloc) {\n        bloc.setKeyValue(\"123\", null);\n      },\n      verify: (bloc) {\n        expect(bloc.getValue(\"123\"), null);\n      },\n    );\n\n    blocTest<AppearanceSettingsCubit, AppearanceSettingsState>(\n      'initial state uses fallback theme',\n      build: () => AppearanceSettingsCubit(\n        appearanceSetting,\n        dateTimeSettings,\n        AppTheme.fallback,\n      ),\n      verify: (bloc) {\n        expect(bloc.state.appTheme.themeName, AppTheme.fallback.themeName);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/app_setting_test/document_appearance_test.dart",
    "content": "import 'package:appflowy/core/config/kv_keys.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nvoid main() {\n  WidgetsFlutterBinding.ensureInitialized();\n  group('DocumentAppearanceCubit', () {\n    late SharedPreferences preferences;\n    late DocumentAppearanceCubit cubit;\n\n    setUpAll(() async {\n      SharedPreferences.setMockInitialValues({});\n    });\n\n    setUp(() async {\n      preferences = await SharedPreferences.getInstance();\n      cubit = DocumentAppearanceCubit();\n    });\n\n    tearDown(() async {\n      await preferences.clear();\n      await cubit.close();\n    });\n\n    test('Initial state', () {\n      expect(cubit.state.fontSize, 16.0);\n      expect(cubit.state.fontFamily, defaultFontFamily);\n    });\n\n    test('Fetch document appearance from SharedPreferences', () async {\n      await preferences.setDouble(KVKeys.kDocumentAppearanceFontSize, 18.0);\n      await preferences.setString(\n        KVKeys.kDocumentAppearanceFontFamily,\n        'Arial',\n      );\n\n      await cubit.fetch();\n\n      expect(cubit.state.fontSize, 18.0);\n      expect(cubit.state.fontFamily, 'Arial');\n    });\n\n    test('Sync font size to SharedPreferences', () async {\n      await cubit.syncFontSize(20.0);\n\n      final fontSize =\n          preferences.getDouble(KVKeys.kDocumentAppearanceFontSize);\n      expect(fontSize, 20.0);\n      expect(cubit.state.fontSize, 20.0);\n    });\n\n    test('Sync font family to SharedPreferences', () async {\n      await cubit.syncFontFamily('Helvetica');\n\n      final fontFamily =\n          preferences.getString(KVKeys.kDocumentAppearanceFontFamily);\n      expect(fontFamily, 'Helvetica');\n      expect(cubit.state.fontFamily, 'Helvetica');\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/board_test/create_card_test.dart",
    "content": "import 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/board/application/board_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'util.dart';\n\nvoid main() {\n  late AppFlowyBoardTest boardTest;\n\n  setUpAll(() async {\n    boardTest = await AppFlowyBoardTest.ensureInitialized();\n  });\n\n  test('create kanban baord card', () async {\n    final context = await boardTest.createTestBoard();\n    final databaseController = DatabaseController(view: context.gridView);\n    final boardBloc = BoardBloc(\n      databaseController: databaseController,\n    )..add(const BoardEvent.initial());\n    await boardResponseFuture();\n\n    List<String> groupIds = boardBloc.state.maybeMap(\n      orElse: () => const [],\n      ready: (value) => value.groupIds,\n    );\n    String lastGroupId = groupIds.last;\n\n    // the group at index 3 is the 'No status' group;\n    assert(boardBloc.groupControllers[lastGroupId]!.group.rows.isEmpty);\n    assert(\n      groupIds.length == 4,\n      'but receive ${groupIds.length}',\n    );\n\n    boardBloc.add(\n      BoardEvent.createRow(\n        groupIds[3],\n        OrderObjectPositionTypePB.End,\n        null,\n        null,\n      ),\n    );\n    await boardResponseFuture();\n\n    groupIds = boardBloc.state.maybeMap(\n      orElse: () => [],\n      ready: (value) => value.groupIds,\n    );\n    lastGroupId = groupIds.last;\n\n    assert(\n      boardBloc.groupControllers[lastGroupId]!.group.rows.length == 1,\n      'but receive ${boardBloc.groupControllers[lastGroupId]!.group.rows.length}',\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart",
    "content": "import 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/board/application/board_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'util.dart';\n\nvoid main() {\n  late AppFlowyBoardTest boardTest;\n\n  setUpAll(() async {\n    boardTest = await AppFlowyBoardTest.ensureInitialized();\n  });\n\n  test('create build-in kanban board test', () async {\n    final context = await boardTest.createTestBoard();\n    final boardBloc = BoardBloc(\n      databaseController: DatabaseController(view: context.gridView),\n    )..add(const BoardEvent.initial());\n    await boardResponseFuture();\n\n    assert(boardBloc.groupControllers.values.length == 4);\n    assert(context.fieldContexts.length == 2);\n  });\n\n  test('edit kanban board field name test', () async {\n    final context = await boardTest.createTestBoard();\n    final boardBloc = BoardBloc(\n      databaseController: DatabaseController(view: context.gridView),\n    )..add(const BoardEvent.initial());\n    await boardResponseFuture();\n\n    final fieldInfo = context.singleSelectFieldContext();\n\n    final editorBloc = FieldEditorBloc(\n      viewId: context.gridView.id,\n      fieldInfo: fieldInfo,\n      fieldController: context.fieldController,\n      isNew: false,\n    );\n    await boardResponseFuture();\n\n    editorBloc.add(const FieldEditorEvent.renameField('Hello world'));\n    await boardResponseFuture();\n\n    // assert the groups were not changed\n    assert(\n      boardBloc.groupControllers.values.length == 4,\n      \"Expected 4, but receive ${boardBloc.groupControllers.values.length}\",\n    );\n\n    assert(\n      context.fieldContexts.length == 2,\n      \"Expected 2, but receive ${context.fieldContexts.length}\",\n    );\n  });\n\n  test('create a new field in kanban board test', () async {\n    final context = await boardTest.createTestBoard();\n    final boardBloc = BoardBloc(\n      databaseController: DatabaseController(view: context.gridView),\n    )..add(const BoardEvent.initial());\n    await boardResponseFuture();\n\n    await context.createField(FieldType.Checkbox);\n    await boardResponseFuture();\n    final checkboxField = context.fieldContexts.last.field;\n    assert(checkboxField.fieldType == FieldType.Checkbox);\n\n    assert(\n      boardBloc.groupControllers.values.length == 4,\n      \"Expected 4, but receive ${boardBloc.groupControllers.values.length}\",\n    );\n\n    assert(\n      context.fieldContexts.length == 3,\n      \"Expected 3, but receive ${context.fieldContexts.length}\",\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/board_test/group_by_checkbox_field_test.dart",
    "content": "import 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/setting/group_bloc.dart';\nimport 'package:appflowy/plugins/database/board/application/board_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'util.dart';\n\nvoid main() {\n  late AppFlowyBoardTest boardTest;\n\n  setUpAll(() async {\n    boardTest = await AppFlowyBoardTest.ensureInitialized();\n  });\n\n  // Group by checkbox field\n  test('group by checkbox field test', () async {\n    final context = await boardTest.createTestBoard();\n    final boardBloc = BoardBloc(\n      databaseController: DatabaseController(view: context.gridView),\n    )..add(const BoardEvent.initial());\n    await boardResponseFuture();\n\n    // assert the initial values\n    assert(boardBloc.groupControllers.values.length == 4);\n    assert(context.fieldContexts.length == 2);\n\n    // create checkbox field\n    await context.createField(FieldType.Checkbox);\n    await boardResponseFuture();\n    assert(context.fieldContexts.length == 3);\n\n    // set group by checkbox\n    final checkboxField = context.fieldContexts.last.field;\n    final gridGroupBloc = DatabaseGroupBloc(\n      viewId: context.gridView.id,\n      databaseController: context.databaseController,\n    )..add(const DatabaseGroupEvent.initial());\n    gridGroupBloc.add(\n      DatabaseGroupEvent.setGroupByField(\n        checkboxField.id,\n        checkboxField.fieldType,\n      ),\n    );\n    await boardResponseFuture();\n\n    assert(boardBloc.groupControllers.values.length == 2);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/setting/group_bloc.dart';\nimport 'package:appflowy/plugins/database/board/application/board_bloc.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nimport 'util.dart';\n\nvoid main() {\n  late AppFlowyBoardTest boardTest;\n\n  setUpAll(() async {\n    boardTest = await AppFlowyBoardTest.ensureInitialized();\n  });\n\n  test('group by date field test', () async {\n    final context = await boardTest.createTestBoard();\n    final boardBloc = BoardBloc(\n      databaseController: DatabaseController(view: context.gridView),\n    )..add(const BoardEvent.initial());\n    await boardResponseFuture();\n\n    // assert the initial values\n    assert(boardBloc.groupControllers.values.length == 4);\n    assert(context.fieldContexts.length == 2);\n\n    await context.createField(FieldType.DateTime);\n    await boardResponseFuture();\n    assert(context.fieldContexts.length == 3);\n\n    final dateField = context.fieldContexts.last.field;\n    final cellController = context.makeCellControllerFromFieldId(dateField.id)\n        as DateCellController;\n    final bloc = DateCellEditorBloc(\n      cellController: cellController,\n      reminderBloc: getIt<ReminderBloc>(),\n    );\n    await boardResponseFuture();\n\n    bloc.add(DateCellEditorEvent.updateDateTime(DateTime.now()));\n    await boardResponseFuture();\n\n    final gridGroupBloc = DatabaseGroupBloc(\n      viewId: context.gridView.id,\n      databaseController: context.databaseController,\n    )..add(const DatabaseGroupEvent.initial());\n    gridGroupBloc.add(\n      DatabaseGroupEvent.setGroupByField(\n        dateField.id,\n        dateField.fieldType,\n      ),\n    );\n    await boardResponseFuture();\n\n    assert(boardBloc.groupControllers.values.length == 2);\n    assert(\n      boardBloc.boardController.groupDatas.last.headerData.groupName ==\n          LocaleKeys.board_dateCondition_today.tr(),\n    );\n  });\n\n  test('group by date field with condition', () async {\n    final context = await boardTest.createTestBoard();\n    final boardBloc = BoardBloc(\n      databaseController: DatabaseController(view: context.gridView),\n    )..add(const BoardEvent.initial());\n    await boardResponseFuture();\n\n    // assert the initial values\n    assert(boardBloc.groupControllers.values.length == 4);\n    assert(context.fieldContexts.length == 2);\n\n    await context.createField(FieldType.DateTime);\n    await boardResponseFuture();\n    assert(context.fieldContexts.length == 3);\n\n    final dateField = context.fieldContexts.last.field;\n    final cellController = context.makeCellControllerFromFieldId(dateField.id)\n        as DateCellController;\n    final bloc = DateCellEditorBloc(\n      cellController: cellController,\n      reminderBloc: getIt<ReminderBloc>(),\n    );\n    await boardResponseFuture();\n\n    bloc.add(DateCellEditorEvent.updateDateTime(DateTime.now()));\n    await boardResponseFuture();\n\n    final gridGroupBloc = DatabaseGroupBloc(\n      viewId: context.gridView.id,\n      databaseController: context.databaseController,\n    )..add(const DatabaseGroupEvent.initial());\n    final settingContent = DateGroupConfigurationPB()\n      ..condition = DateConditionPB.Year;\n    gridGroupBloc.add(\n      DatabaseGroupEvent.setGroupByField(\n        dateField.id,\n        dateField.fieldType,\n        settingContent.writeToBuffer(),\n      ),\n    );\n    await boardResponseFuture();\n\n    assert(boardBloc.groupControllers.values.length == 2);\n    assert(\n      boardBloc.boardController.groupDatas.last.headerData.groupName ==\n          DateTime.now().year.toString(),\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/setting/group_bloc.dart';\nimport 'package:appflowy/plugins/database/board/application/board_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'util.dart';\n\nvoid main() {\n  late AppFlowyBoardTest boardTest;\n\n  setUpAll(() async {\n    boardTest = await AppFlowyBoardTest.ensureInitialized();\n  });\n\n  test('no status group name test', () async {\n    final context = await boardTest.createTestBoard();\n\n    // create multi-select field\n    await context.createField(FieldType.MultiSelect);\n    await boardResponseFuture();\n    assert(context.fieldContexts.length == 3);\n    final multiSelectField = context.fieldContexts.last.field;\n\n    // set grouped by the new multi-select field\"\n    final gridGroupBloc = DatabaseGroupBloc(\n      viewId: context.gridView.id,\n      databaseController: context.databaseController,\n    )..add(const DatabaseGroupEvent.initial());\n    await boardResponseFuture();\n\n    gridGroupBloc.add(\n      DatabaseGroupEvent.setGroupByField(\n        multiSelectField.id,\n        multiSelectField.fieldType,\n      ),\n    );\n    await boardResponseFuture();\n\n    // assert only have the 'No status' group\n    final boardBloc = BoardBloc(\n      databaseController: DatabaseController(view: context.gridView),\n    )..add(const BoardEvent.initial());\n    await boardResponseFuture();\n    assert(\n      boardBloc.groupControllers.values.length == 1,\n      \"Expected 1, but receive ${boardBloc.groupControllers.values.length}\",\n    );\n  });\n\n  test('group by multi select with no options test', () async {\n    final context = await boardTest.createTestBoard();\n\n    // create multi-select field\n    await context.createField(FieldType.MultiSelect);\n    await boardResponseFuture();\n    assert(context.fieldContexts.length == 3);\n    final multiSelectField = context.fieldContexts.last.field;\n\n    // Create options\n    final cellController =\n        context.makeCellControllerFromFieldId(multiSelectField.id)\n            as SelectOptionCellController;\n\n    final bloc = SelectOptionCellEditorBloc(cellController: cellController);\n    await boardResponseFuture();\n    bloc.add(const SelectOptionCellEditorEvent.filterOption(\"A\"));\n    bloc.add(const SelectOptionCellEditorEvent.createOption());\n    await boardResponseFuture();\n    bloc.add(const SelectOptionCellEditorEvent.filterOption(\"B\"));\n    bloc.add(const SelectOptionCellEditorEvent.createOption());\n    await boardResponseFuture();\n\n    // set grouped by the new multi-select field\"\n    final gridGroupBloc = DatabaseGroupBloc(\n      viewId: context.gridView.id,\n      databaseController: context.databaseController,\n    )..add(const DatabaseGroupEvent.initial());\n    await boardResponseFuture();\n\n    gridGroupBloc.add(\n      DatabaseGroupEvent.setGroupByField(\n        multiSelectField.id,\n        multiSelectField.fieldType,\n      ),\n    );\n    await boardResponseFuture();\n\n    // assert there are only three group\n    final boardBloc = BoardBloc(\n      databaseController: DatabaseController(view: context.gridView),\n    )..add(const BoardEvent.initial());\n    await boardResponseFuture();\n    assert(\n      boardBloc.groupControllers.values.length == 3,\n      \"Expected 3, but receive ${boardBloc.groupControllers.values.length}\",\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/board_test/group_by_unsupport_field_test.dart",
    "content": "import 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/board/application/board_bloc.dart';\nimport 'package:bloc_test/bloc_test.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'util.dart';\n\nvoid main() {\n  late AppFlowyBoardTest boardTest;\n  late FieldEditorBloc editorBloc;\n  late BoardTestContext context;\n\n  setUpAll(() async {\n    boardTest = await AppFlowyBoardTest.ensureInitialized();\n    context = await boardTest.createTestBoard();\n    final fieldInfo = context.singleSelectFieldContext();\n    editorBloc = context.makeFieldEditor(\n      fieldInfo: fieldInfo,\n    );\n\n    await boardResponseFuture();\n  });\n\n  group('Group with not support grouping field', () {\n    blocTest<FieldEditorBloc, FieldEditorState>(\n      \"switch to text field\",\n      build: () => editorBloc,\n      wait: boardResponseDuration(),\n      act: (bloc) async {\n        bloc.add(const FieldEditorEvent.switchFieldType(FieldType.RichText));\n      },\n      verify: (bloc) {\n        assert(bloc.state.field.fieldType == FieldType.RichText);\n      },\n    );\n    blocTest<BoardBloc, BoardState>(\n      'assert the number of groups is 1',\n      build: () => BoardBloc(\n        databaseController: DatabaseController(view: context.gridView),\n      )..add(\n          const BoardEvent.initial(),\n        ),\n      wait: boardResponseDuration(),\n      verify: (bloc) {\n        assert(\n          bloc.groupControllers.values.length == 1,\n          \"Expected 1, but receive ${bloc.groupControllers.values.length}\",\n        );\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/board_test/util.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/row/row_cache.dart';\nimport 'package:appflowy/plugins/database/board/board.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\n\nimport '../../util.dart';\nimport '../grid_test/util.dart';\n\nclass AppFlowyBoardTest {\n  AppFlowyBoardTest({required this.unitTest});\n\n  final AppFlowyUnitTest unitTest;\n\n  static Future<AppFlowyBoardTest> ensureInitialized() async {\n    final inner = await AppFlowyUnitTest.ensureInitialized();\n    return AppFlowyBoardTest(unitTest: inner);\n  }\n\n  Future<BoardTestContext> createTestBoard() async {\n    final app = await unitTest.createWorkspace();\n    final builder = BoardPluginBuilder();\n    return ViewBackendService.createView(\n      parentViewId: app.id,\n      name: \"Test Board\",\n      layoutType: builder.layoutType,\n      openAfterCreate: true,\n    ).then((result) {\n      return result.fold(\n        (view) async {\n          final context = BoardTestContext(\n            view,\n            DatabaseController(view: view),\n          );\n          final result = await context._boardDataController.open();\n          result.fold((l) => null, (r) => throw Exception(r));\n          return context;\n        },\n        (error) {\n          throw Exception();\n        },\n      );\n    });\n  }\n}\n\nFuture<void> boardResponseFuture() {\n  return Future.delayed(boardResponseDuration());\n}\n\nDuration boardResponseDuration({int milliseconds = 2000}) {\n  return Duration(milliseconds: milliseconds);\n}\n\nclass BoardTestContext {\n  BoardTestContext(this.gridView, this._boardDataController);\n\n  final ViewPB gridView;\n  final DatabaseController _boardDataController;\n\n  List<RowInfo> get rowInfos {\n    return _boardDataController.rowCache.rowInfos;\n  }\n\n  List<FieldInfo> get fieldContexts => fieldController.fieldInfos;\n\n  FieldController get fieldController {\n    return _boardDataController.fieldController;\n  }\n\n  DatabaseController get databaseController => _boardDataController;\n\n  FieldEditorBloc makeFieldEditor({\n    required FieldInfo fieldInfo,\n  }) =>\n      FieldEditorBloc(\n        viewId: databaseController.viewId,\n        fieldController: fieldController,\n        fieldInfo: fieldInfo,\n        isNew: false,\n      );\n\n  CellController makeCellControllerFromFieldId(String fieldId) {\n    return makeCellController(\n      _boardDataController,\n      CellContext(fieldId: fieldId, rowId: rowInfos.last.rowId),\n    );\n  }\n\n  Future<FieldEditorBloc> createField(FieldType fieldType) async {\n    final editorBloc =\n        await createFieldEditor(databaseController: _boardDataController);\n    await gridResponseFuture();\n    editorBloc.add(FieldEditorEvent.switchFieldType(fieldType));\n    await gridResponseFuture();\n    return Future(() => editorBloc);\n  }\n\n  FieldInfo singleSelectFieldContext() {\n    final fieldInfo = fieldContexts\n        .firstWhere((element) => element.fieldType == FieldType.SingleSelect);\n    return fieldInfo;\n  }\n\n  FieldInfo textFieldContext() {\n    final fieldInfo = fieldContexts\n        .firstWhere((element) => element.fieldType == FieldType.RichText);\n    return fieldInfo;\n  }\n\n  FieldInfo checkboxFieldContext() {\n    final fieldInfo = fieldContexts\n        .firstWhere((element) => element.fieldType == FieldType.Checkbox);\n    return fieldInfo;\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/chat_test/chat_load_message_test.dart",
    "content": "import 'package:flutter_test/flutter_test.dart';\n\nimport 'util.dart';\n\nvoid main() {\n  // ignore: unused_local_variable\n  late AppFlowyChatTest chatTest;\n\n  setUpAll(() async {\n    chatTest = await AppFlowyChatTest.ensureInitialized();\n  });\n\n  test('send message', () async {\n    // final context = await chatTest.createChat();\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/chat_test/util.dart",
    "content": "import 'package:appflowy/plugins/ai_chat/chat.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\n\nimport '../../util.dart';\n\nclass AppFlowyChatTest {\n  AppFlowyChatTest({required this.unitTest});\n\n  final AppFlowyUnitTest unitTest;\n\n  static Future<AppFlowyChatTest> ensureInitialized() async {\n    final inner = await AppFlowyUnitTest.ensureInitialized();\n    return AppFlowyChatTest(unitTest: inner);\n  }\n\n  Future<ViewPB> createChat() async {\n    final app = await unitTest.createWorkspace();\n    final builder = AIChatPluginBuilder();\n    return ViewBackendService.createView(\n      parentViewId: app.id,\n      name: \"Test Chat\",\n      layoutType: builder.layoutType,\n      openAfterCreate: true,\n    ).then((result) {\n      return result.fold(\n        (view) async {\n          return view;\n        },\n        (error) {\n          throw Exception();\n        },\n      );\n    });\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/grid_test/cell/checklist_cell_bloc_test.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../util.dart';\n\nvoid main() {\n  late AppFlowyGridTest cellTest;\n\n  setUpAll(() async {\n    cellTest = await AppFlowyGridTest.ensureInitialized();\n  });\n\n  group('checklist cell bloc:', () {\n    late GridTestContext context;\n    late ChecklistCellController cellController;\n\n    setUp(() async {\n      context = await cellTest.makeDefaultTestGrid();\n      await FieldBackendService.createField(\n        viewId: context.viewId,\n        fieldType: FieldType.Checklist,\n      );\n      await gridResponseFuture();\n      final fieldIndex = context.fieldController.fieldInfos\n          .indexWhere((field) => field.fieldType == FieldType.Checklist);\n      cellController = context.makeGridCellController(fieldIndex, 0).as();\n    });\n\n    test('create tasks', () async {\n      final bloc = ChecklistCellBloc(cellController: cellController);\n      await gridResponseFuture();\n\n      expect(bloc.state.tasks.length, 0);\n\n      bloc.add(const ChecklistCellEvent.createNewTask(\"B\"));\n      await gridResponseFuture();\n\n      expect(bloc.state.tasks.length, 1);\n\n      bloc.add(const ChecklistCellEvent.createNewTask(\"A\", index: 0));\n      await gridResponseFuture();\n\n      expect(bloc.state.tasks.length, 2);\n      expect(bloc.state.tasks.first.data.name, \"A\");\n      expect(bloc.state.tasks.last.data.name, \"B\");\n    });\n\n    test('rename task', () async {\n      final bloc = ChecklistCellBloc(cellController: cellController);\n      await gridResponseFuture();\n\n      expect(bloc.state.tasks.length, 0);\n\n      bloc.add(const ChecklistCellEvent.createNewTask(\"B\"));\n      await gridResponseFuture();\n\n      expect(bloc.state.tasks.length, 1);\n      expect(bloc.state.tasks.first.data.name, \"B\");\n\n      bloc.add(\n        ChecklistCellEvent.updateTaskName(bloc.state.tasks.first.data, \"A\"),\n      );\n      await gridResponseFuture();\n\n      expect(bloc.state.tasks.length, 1);\n      expect(bloc.state.tasks.first.data.name, \"A\");\n    });\n\n    test('select task', () async {\n      final bloc = ChecklistCellBloc(cellController: cellController);\n      await gridResponseFuture();\n\n      bloc.add(const ChecklistCellEvent.createNewTask(\"A\"));\n      await gridResponseFuture();\n\n      expect(bloc.state.tasks.length, 1);\n      expect(bloc.state.tasks.first.isSelected, false);\n\n      bloc.add(const ChecklistCellEvent.selectTask('A'));\n      await gridResponseFuture();\n\n      expect(bloc.state.tasks.first.isSelected, false);\n\n      bloc.add(\n        ChecklistCellEvent.selectTask(bloc.state.tasks.first.data.id),\n      );\n      await gridResponseFuture();\n\n      expect(bloc.state.tasks.first.isSelected, true);\n    });\n\n    test('delete task', () async {\n      final bloc = ChecklistCellBloc(cellController: cellController);\n      await gridResponseFuture();\n\n      expect(bloc.state.tasks.length, 0);\n\n      bloc.add(const ChecklistCellEvent.createNewTask(\"A\"));\n      await gridResponseFuture();\n\n      expect(bloc.state.tasks.length, 1);\n      expect(bloc.state.tasks.first.isSelected, false);\n\n      bloc.add(const ChecklistCellEvent.deleteTask('A'));\n      await gridResponseFuture();\n\n      expect(bloc.state.tasks.length, 1);\n\n      bloc.add(\n        ChecklistCellEvent.deleteTask(bloc.state.tasks.first.data.id),\n      );\n      await gridResponseFuture();\n\n      expect(bloc.state.tasks.length, 0);\n    });\n\n    test('reorder task', () async {\n      final bloc = ChecklistCellBloc(cellController: cellController);\n      await gridResponseFuture();\n\n      bloc.add(const ChecklistCellEvent.createNewTask(\"A\"));\n      await gridResponseFuture();\n      bloc.add(const ChecklistCellEvent.createNewTask(\"B\"));\n      await gridResponseFuture();\n      bloc.add(const ChecklistCellEvent.createNewTask(\"C\"));\n      await gridResponseFuture();\n      bloc.add(const ChecklistCellEvent.createNewTask(\"D\"));\n      await gridResponseFuture();\n\n      expect(bloc.state.tasks.length, 4);\n\n      bloc.add(const ChecklistCellEvent.reorderTask(0, 2));\n      await gridResponseFuture();\n\n      expect(bloc.state.tasks.length, 4);\n      expect(bloc.state.tasks[0].data.name, \"B\");\n      expect(bloc.state.tasks[1].data.name, \"A\");\n      expect(bloc.state.tasks[2].data.name, \"C\");\n      expect(bloc.state.tasks[3].data.name, \"D\");\n\n      bloc.add(const ChecklistCellEvent.reorderTask(3, 1));\n      await gridResponseFuture();\n\n      expect(bloc.state.tasks.length, 4);\n      expect(bloc.state.tasks[0].data.name, \"B\");\n      expect(bloc.state.tasks[1].data.name, \"D\");\n      expect(bloc.state.tasks[2].data.name, \"A\");\n      expect(bloc.state.tasks[3].data.name, \"C\");\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/grid_test/cell/date_cell_bloc_test.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:time/time.dart';\n\nimport '../util.dart';\n\nvoid main() {\n  late AppFlowyGridTest cellTest;\n\n  setUpAll(() async {\n    cellTest = await AppFlowyGridTest.ensureInitialized();\n  });\n\n  group('date time cell bloc:', () {\n    late GridTestContext context;\n    late DateCellController cellController;\n\n    setUp(() async {\n      context = await cellTest.makeDefaultTestGrid();\n      await FieldBackendService.createField(\n        viewId: context.viewId,\n        fieldType: FieldType.DateTime,\n      );\n      await gridResponseFuture();\n      final fieldIndex = context.fieldController.fieldInfos\n          .indexWhere((field) => field.fieldType == FieldType.DateTime);\n      cellController = context.makeGridCellController(fieldIndex, 0).as();\n    });\n\n    test('select date', () async {\n      final reminderBloc = ReminderBloc();\n      final bloc = DateCellEditorBloc(\n        cellController: cellController,\n        reminderBloc: reminderBloc,\n      );\n      await gridResponseFuture();\n\n      expect(bloc.state.dateTime, null);\n      expect(bloc.state.endDateTime, null);\n      expect(bloc.state.includeTime, false);\n      expect(bloc.state.isRange, false);\n\n      final now = DateTime.now();\n      bloc.add(DateCellEditorEvent.updateDateTime(now));\n      await gridResponseFuture();\n\n      expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true);\n    });\n\n    test('include time', () async {\n      final reminderBloc = ReminderBloc();\n      final bloc = DateCellEditorBloc(\n        cellController: cellController,\n        reminderBloc: reminderBloc,\n      );\n      await gridResponseFuture();\n\n      final now = DateTime.now();\n      bloc.add(DateCellEditorEvent.setIncludeTime(true, now, null));\n      await gridResponseFuture();\n\n      expect(bloc.state.includeTime, true);\n      expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true);\n      expect(bloc.state.endDateTime, null);\n\n      bloc.add(const DateCellEditorEvent.setIncludeTime(false, null, null));\n      await gridResponseFuture();\n\n      expect(bloc.state.includeTime, false);\n      expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true);\n      expect(bloc.state.endDateTime, null);\n    });\n\n    test('end time basic', () async {\n      final reminderBloc = ReminderBloc();\n      final bloc = DateCellEditorBloc(\n        cellController: cellController,\n        reminderBloc: reminderBloc,\n      );\n      await gridResponseFuture();\n\n      expect(bloc.state.isRange, false);\n      expect(bloc.state.dateTime, null);\n      expect(bloc.state.endDateTime, null);\n\n      final now = DateTime.now();\n      bloc.add(DateCellEditorEvent.updateDateTime(now));\n      await gridResponseFuture();\n\n      expect(bloc.state.isRange, false);\n      expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true);\n      expect(bloc.state.endDateTime, null);\n\n      bloc.add(const DateCellEditorEvent.setIsRange(true, null, null));\n      await gridResponseFuture();\n\n      expect(bloc.state.isRange, true);\n      expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true);\n      expect(bloc.state.endDateTime!.isAtSameMinuteAs(now), true);\n\n      bloc.add(const DateCellEditorEvent.setIsRange(false, null, null));\n      await gridResponseFuture();\n\n      expect(bloc.state.isRange, false);\n      expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true);\n      expect(bloc.state.endDateTime, null);\n    });\n\n    test('end time from empty', () async {\n      final reminderBloc = ReminderBloc();\n      final bloc = DateCellEditorBloc(\n        cellController: cellController,\n        reminderBloc: reminderBloc,\n      );\n      await gridResponseFuture();\n\n      expect(bloc.state.isRange, false);\n      expect(bloc.state.dateTime, null);\n      expect(bloc.state.endDateTime, null);\n\n      final now = DateTime.now();\n      bloc.add(DateCellEditorEvent.setIsRange(true, now, now));\n      await gridResponseFuture();\n\n      expect(bloc.state.isRange, true);\n      expect(bloc.state.dateTime!.isAtSameDayAs(now), true);\n      expect(bloc.state.endDateTime!.isAtSameDayAs(now), true);\n\n      bloc.add(const DateCellEditorEvent.setIsRange(false, null, null));\n      await gridResponseFuture();\n\n      expect(bloc.state.isRange, false);\n      expect(bloc.state.dateTime!.isAtSameDayAs(now), true);\n      expect(bloc.state.endDateTime, null);\n    });\n\n    test('end time unexpected null', () async {\n      final reminderBloc = ReminderBloc();\n      final bloc = DateCellEditorBloc(\n        cellController: cellController,\n        reminderBloc: reminderBloc,\n      );\n      await gridResponseFuture();\n\n      expect(bloc.state.isRange, false);\n      expect(bloc.state.dateTime, null);\n      expect(bloc.state.endDateTime, null);\n\n      final now = DateTime.now();\n      // pass in unexpected null as end date time\n      bloc.add(DateCellEditorEvent.setIsRange(true, now, null));\n      await gridResponseFuture();\n\n      // no changes\n      expect(bloc.state.isRange, false);\n      expect(bloc.state.dateTime, null);\n      expect(bloc.state.endDateTime, null);\n    });\n\n    test('end time unexpected end', () async {\n      final reminderBloc = ReminderBloc();\n      final bloc = DateCellEditorBloc(\n        cellController: cellController,\n        reminderBloc: reminderBloc,\n      );\n      await gridResponseFuture();\n\n      expect(bloc.state.isRange, false);\n      expect(bloc.state.dateTime, null);\n      expect(bloc.state.endDateTime, null);\n\n      final now = DateTime.now();\n      bloc.add(DateCellEditorEvent.setIsRange(true, now, now));\n      await gridResponseFuture();\n\n      bloc.add(DateCellEditorEvent.setIsRange(false, now, now));\n      await gridResponseFuture();\n\n      // no change\n      expect(bloc.state.isRange, true);\n      expect(bloc.state.dateTime!.isAtSameDayAs(now), true);\n      expect(bloc.state.endDateTime!.isAtSameDayAs(now), true);\n    });\n\n    test('clear date', () async {\n      final reminderBloc = ReminderBloc();\n      final bloc = DateCellEditorBloc(\n        cellController: cellController,\n        reminderBloc: reminderBloc,\n      );\n      await gridResponseFuture();\n\n      final now = DateTime.now();\n      bloc.add(DateCellEditorEvent.setIsRange(true, now, now));\n      await gridResponseFuture();\n      bloc.add(DateCellEditorEvent.setIncludeTime(true, now, now));\n      await gridResponseFuture();\n\n      expect(bloc.state.isRange, true);\n      expect(bloc.state.includeTime, true);\n      expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true);\n      expect(bloc.state.endDateTime!.isAtSameMinuteAs(now), true);\n\n      bloc.add(const DateCellEditorEvent.clearDate());\n      await gridResponseFuture();\n\n      expect(bloc.state.dateTime, null);\n      expect(bloc.state.endDateTime, null);\n      expect(bloc.state.includeTime, false);\n      expect(bloc.state.isRange, false);\n    });\n\n    test('set date format', () async {\n      final reminderBloc = ReminderBloc();\n      final bloc = DateCellEditorBloc(\n        cellController: cellController,\n        reminderBloc: reminderBloc,\n      );\n      await gridResponseFuture();\n\n      expect(\n        bloc.state.dateTypeOptionPB.dateFormat,\n        DateFormatPB.Friendly,\n      );\n      expect(\n        bloc.state.dateTypeOptionPB.timeFormat,\n        TimeFormatPB.TwentyFourHour,\n      );\n\n      bloc.add(\n        const DateCellEditorEvent.setDateFormat(DateFormatPB.ISO),\n      );\n      await gridResponseFuture();\n      expect(\n        bloc.state.dateTypeOptionPB.dateFormat,\n        DateFormatPB.ISO,\n      );\n\n      bloc.add(\n        const DateCellEditorEvent.setTimeFormat(TimeFormatPB.TwelveHour),\n      );\n      await gridResponseFuture();\n      expect(\n        bloc.state.dateTypeOptionPB.timeFormat,\n        TimeFormatPB.TwelveHour,\n      );\n    });\n\n    test('set reminder option', () async {\n      final reminderBloc = ReminderBloc();\n      final bloc = DateCellEditorBloc(\n        cellController: cellController,\n        reminderBloc: reminderBloc,\n      );\n      await gridResponseFuture();\n\n      expect(reminderBloc.state.reminders.length, 0);\n\n      final now = DateTime.now();\n      final yesterday = DateTime(now.year, now.month, now.day - 1);\n\n      bloc.add(DateCellEditorEvent.updateDateTime(yesterday));\n      await gridResponseFuture();\n      bloc.add(\n        const DateCellEditorEvent.setReminderOption(\n          ReminderOption.onDayOfEvent,\n        ),\n      );\n      await gridResponseFuture();\n\n      expect(reminderBloc.state.reminders.length, 1);\n      expect(\n        reminderBloc.state.reminders.first.scheduledAt,\n        Int64(\n          yesterday.add(const Duration(hours: 9)).millisecondsSinceEpoch ~/\n              1000,\n        ),\n      );\n\n      bloc.add(\n        const DateCellEditorEvent.setReminderOption(ReminderOption.none),\n      );\n      await gridResponseFuture();\n      expect(reminderBloc.state.reminders.length, 0);\n      reminderBloc.add(const ReminderEvent.refresh());\n      await gridResponseFuture();\n      expect(reminderBloc.state.reminders.length, 0);\n    });\n\n    test('set reminder option with later time', () async {\n      final reminderBloc = ReminderBloc();\n      final bloc = DateCellEditorBloc(\n        cellController: cellController,\n        reminderBloc: reminderBloc,\n      );\n      await gridResponseFuture();\n\n      expect(reminderBloc.state.reminders.length, 0);\n\n      final now = DateTime.now();\n      final threeDaysFromToday = DateTime(now.year, now.month, now.day + 3);\n\n      bloc.add(DateCellEditorEvent.updateDateTime(threeDaysFromToday));\n      await gridResponseFuture();\n      bloc.add(\n        const DateCellEditorEvent.setReminderOption(\n          ReminderOption.onDayOfEvent,\n        ),\n      );\n      await gridResponseFuture();\n\n      expect(reminderBloc.state.reminders.length, 0);\n    });\n\n    test('set reminder option from empty', () async {\n      final reminderBloc = ReminderBloc();\n      final bloc = DateCellEditorBloc(\n        cellController: cellController,\n        reminderBloc: reminderBloc,\n      );\n      await gridResponseFuture();\n\n      final now = DateTime.now();\n      final yesterday = DateTime(now.year, now.month, now.day - 1);\n      bloc.add(DateCellEditorEvent.updateDateTime(yesterday));\n      await gridResponseFuture();\n      bloc.add(\n        const DateCellEditorEvent.setReminderOption(\n          ReminderOption.onDayOfEvent,\n        ),\n      );\n      await gridResponseFuture();\n\n      expect(bloc.state.dateTime, yesterday);\n      expect(reminderBloc.state.reminders.length, 1);\n      expect(\n        reminderBloc.state.reminders.first.scheduledAt,\n        Int64(\n          yesterday.add(const Duration(hours: 9)).millisecondsSinceEpoch ~/\n              1000,\n        ),\n      );\n\n      bloc.add(const DateCellEditorEvent.clearDate());\n      await gridResponseFuture();\n      expect(reminderBloc.state.reminders.length, 0);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/grid_test/cell/select_option_cell_test.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../util.dart';\n\nvoid main() {\n  late AppFlowyGridTest cellTest;\n\n  setUpAll(() async {\n    cellTest = await AppFlowyGridTest.ensureInitialized();\n  });\n\n  group('select cell bloc:', () {\n    late GridTestContext context;\n    late SelectOptionCellController cellController;\n\n    setUp(() async {\n      context = await cellTest.makeDefaultTestGrid();\n      await RowBackendService.createRow(viewId: context.viewId);\n      final fieldIndex = context.fieldController.fieldInfos\n          .indexWhere((field) => field.fieldType == FieldType.SingleSelect);\n      cellController = context.makeGridCellController(fieldIndex, 0).as();\n    });\n\n    test('create options', () async {\n      final bloc = SelectOptionCellEditorBloc(cellController: cellController);\n      await gridResponseFuture();\n\n      bloc.add(const SelectOptionCellEditorEvent.filterOption(\"A\"));\n      bloc.add(const SelectOptionCellEditorEvent.createOption());\n      await gridResponseFuture();\n\n      expect(bloc.state.options.length, 1);\n      expect(bloc.state.options[0].name, \"A\");\n    });\n\n    test('update options', () async {\n      final bloc = SelectOptionCellEditorBloc(cellController: cellController);\n      await gridResponseFuture();\n\n      bloc.add(const SelectOptionCellEditorEvent.filterOption(\"A\"));\n      bloc.add(const SelectOptionCellEditorEvent.createOption());\n      await gridResponseFuture();\n\n      final SelectOptionPB optionUpdate = bloc.state.options[0]\n        ..color = SelectOptionColorPB.Aqua\n        ..name = \"B\";\n      bloc.add(SelectOptionCellEditorEvent.updateOption(optionUpdate));\n\n      expect(bloc.state.options.length, 1);\n      expect(bloc.state.options[0].name, \"B\");\n      expect(bloc.state.options[0].color, SelectOptionColorPB.Aqua);\n    });\n\n    test('delete options', () async {\n      final bloc = SelectOptionCellEditorBloc(cellController: cellController);\n      await gridResponseFuture();\n\n      bloc.add(const SelectOptionCellEditorEvent.filterOption(\"A\"));\n      bloc.add(const SelectOptionCellEditorEvent.createOption());\n      await gridResponseFuture();\n      assert(\n        bloc.state.options.length == 1,\n        \"Expect 1 but receive ${bloc.state.options.length}, Options: ${bloc.state.options}\",\n      );\n\n      bloc.add(const SelectOptionCellEditorEvent.filterOption(\"B\"));\n      bloc.add(const SelectOptionCellEditorEvent.createOption());\n      await gridResponseFuture();\n      assert(\n        bloc.state.options.length == 2,\n        \"Expect 2 but receive ${bloc.state.options.length}, Options: ${bloc.state.options}\",\n      );\n\n      bloc.add(const SelectOptionCellEditorEvent.filterOption(\"C\"));\n      bloc.add(const SelectOptionCellEditorEvent.createOption());\n      await gridResponseFuture();\n      assert(\n        bloc.state.options.length == 3,\n        \"Expect 3 but receive ${bloc.state.options.length}. Options: ${bloc.state.options}\",\n      );\n\n      bloc.add(SelectOptionCellEditorEvent.deleteOption(bloc.state.options[0]));\n      await gridResponseFuture();\n      assert(\n        bloc.state.options.length == 2,\n        \"Expect 2 but receive ${bloc.state.options.length}. Options: ${bloc.state.options}\",\n      );\n\n      bloc.add(const SelectOptionCellEditorEvent.deleteAllOptions());\n      await gridResponseFuture();\n\n      assert(\n        bloc.state.options.isEmpty,\n        \"Expect empty but receive ${bloc.state.options.length}. Options: ${bloc.state.options}\",\n      );\n    });\n\n    test('select/unselect option', () async {\n      final bloc = SelectOptionCellEditorBloc(cellController: cellController);\n      await gridResponseFuture();\n\n      bloc.add(const SelectOptionCellEditorEvent.filterOption(\"A\"));\n      bloc.add(const SelectOptionCellEditorEvent.createOption());\n      await gridResponseFuture();\n\n      final optionId = bloc.state.options[0].id;\n      bloc.add(SelectOptionCellEditorEvent.unselectOption(optionId));\n      await gridResponseFuture();\n      assert(bloc.state.selectedOptions.isEmpty);\n\n      bloc.add(SelectOptionCellEditorEvent.selectOption(optionId));\n      await gridResponseFuture();\n\n      assert(bloc.state.selectedOptions.length == 1);\n      expect(bloc.state.selectedOptions[0].name, \"A\");\n    });\n\n    test('select an option or create one', () async {\n      final bloc = SelectOptionCellEditorBloc(cellController: cellController);\n      await gridResponseFuture();\n\n      bloc.add(const SelectOptionCellEditorEvent.filterOption(\"A\"));\n      bloc.add(const SelectOptionCellEditorEvent.createOption());\n      await gridResponseFuture();\n\n      bloc.add(const SelectOptionCellEditorEvent.filterOption(\"B\"));\n      bloc.add(const SelectOptionCellEditorEvent.submitTextField());\n      await gridResponseFuture();\n\n      bloc.add(const SelectOptionCellEditorEvent.filterOption(\"A\"));\n      bloc.add(const SelectOptionCellEditorEvent.submitTextField());\n      await gridResponseFuture();\n\n      expect(bloc.state.selectedOptions.length, 1);\n      expect(bloc.state.options.length, 1);\n      expect(bloc.state.selectedOptions[0].name, \"A\");\n    });\n\n    test('select multiple options', () async {\n      final bloc = SelectOptionCellEditorBloc(cellController: cellController);\n      await gridResponseFuture();\n\n      bloc.add(const SelectOptionCellEditorEvent.filterOption(\"A\"));\n      bloc.add(const SelectOptionCellEditorEvent.createOption());\n      await gridResponseFuture();\n\n      bloc.add(const SelectOptionCellEditorEvent.filterOption(\"B\"));\n      bloc.add(const SelectOptionCellEditorEvent.createOption());\n      await gridResponseFuture();\n\n      bloc.add(\n        const SelectOptionCellEditorEvent.selectMultipleOptions(\n          [\"A\", \"B\", \"C\"],\n          \"x\",\n        ),\n      );\n      await gridResponseFuture();\n\n      assert(bloc.state.selectedOptions.length == 1);\n      expect(bloc.state.selectedOptions[0].name, \"A\");\n      expect(bloc.filter, \"x\");\n    });\n\n    test('filter options', () async {\n      final bloc = SelectOptionCellEditorBloc(cellController: cellController);\n      await gridResponseFuture();\n\n      bloc.add(const SelectOptionCellEditorEvent.filterOption(\"abcd\"));\n      bloc.add(const SelectOptionCellEditorEvent.createOption());\n      await gridResponseFuture();\n      expect(\n        bloc.state.options.length,\n        1,\n        reason: \"Options: ${bloc.state.options}\",\n      );\n\n      bloc.add(const SelectOptionCellEditorEvent.filterOption(\"aaaa\"));\n      bloc.add(const SelectOptionCellEditorEvent.createOption());\n      await gridResponseFuture();\n      expect(\n        bloc.state.options.length,\n        2,\n        reason: \"Options: ${bloc.state.options}\",\n      );\n\n      bloc.add(const SelectOptionCellEditorEvent.filterOption(\"defg\"));\n      bloc.add(const SelectOptionCellEditorEvent.createOption());\n      await gridResponseFuture();\n      expect(\n        bloc.state.options.length,\n        3,\n        reason: \"Options: ${bloc.state.options}\",\n      );\n\n      bloc.add(const SelectOptionCellEditorEvent.filterOption(\"a\"));\n      await gridResponseFuture();\n\n      expect(\n        bloc.state.options.length,\n        2,\n        reason: \"Options: ${bloc.state.options}\",\n      );\n      expect(\n        bloc.allOptions.length,\n        3,\n        reason: \"Options: ${bloc.state.options}\",\n      );\n      expect(bloc.state.createSelectOptionSuggestion!.name, \"a\");\n      expect(bloc.filter, \"a\");\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/grid_test/cell/text_cell_bloc_test.dart",
    "content": "import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/row/row_service.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/plugins/database/domain/field_settings_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../util.dart';\n\nvoid main() {\n  late AppFlowyGridTest cellTest;\n\n  setUpAll(() async {\n    cellTest = await AppFlowyGridTest.ensureInitialized();\n  });\n\n  group('text cell bloc:', () {\n    late GridTestContext context;\n    late TextCellController cellController;\n\n    setUp(() async {\n      context = await cellTest.makeDefaultTestGrid();\n      await RowBackendService.createRow(viewId: context.viewId);\n      final fieldIndex = context.fieldController.fieldInfos\n          .indexWhere((field) => field.fieldType == FieldType.RichText);\n      cellController = context.makeGridCellController(fieldIndex, 0).as();\n    });\n\n    test('update text', () async {\n      final bloc = TextCellBloc(cellController: cellController);\n      await gridResponseFuture();\n\n      expect(bloc.state.content, \"\");\n\n      bloc.add(const TextCellEvent.updateText(\"A\"));\n      await gridResponseFuture(milliseconds: 600);\n\n      expect(bloc.state.content, \"A\");\n    });\n\n    test('non-primary text field emoji and hasDocument', () async {\n      final primaryBloc = TextCellBloc(cellController: cellController);\n      expect(primaryBloc.state.emoji == null, false);\n      expect(primaryBloc.state.hasDocument == null, false);\n\n      await primaryBloc.close();\n\n      await FieldBackendService.createField(\n        viewId: context.viewId,\n        fieldName: \"Second\",\n      );\n      await gridResponseFuture();\n      final fieldIndex = context.fieldController.fieldInfos.indexWhere(\n        (field) => field.fieldType == FieldType.RichText && !field.isPrimary,\n      );\n      cellController = context.makeGridCellController(fieldIndex, 0).as();\n      final nonPrimaryBloc = TextCellBloc(cellController: cellController);\n      await gridResponseFuture();\n\n      expect(nonPrimaryBloc.state.emoji == null, true);\n      expect(nonPrimaryBloc.state.hasDocument == null, true);\n    });\n\n    test('update wrap cell content', () async {\n      final bloc = TextCellBloc(cellController: cellController);\n      await gridResponseFuture();\n\n      expect(bloc.state.wrap, true);\n\n      await FieldSettingsBackendService(\n        viewId: context.viewId,\n      ).updateFieldSettings(\n        fieldId: cellController.fieldId,\n        wrapCellContent: false,\n      );\n      await gridResponseFuture();\n\n      expect(bloc.state.wrap, false);\n    });\n\n    test('update emoji', () async {\n      final bloc = TextCellBloc(cellController: cellController);\n      await gridResponseFuture();\n\n      expect(bloc.state.emoji!.value, \"\");\n\n      await RowBackendService(viewId: context.viewId)\n          .updateMeta(rowId: cellController.rowId, iconURL: \"dummy\");\n      await gridResponseFuture();\n\n      expect(bloc.state.emoji!.value, \"dummy\");\n    });\n\n    test('update document data', () async {\n      // This is so fake?\n      final bloc = TextCellBloc(cellController: cellController);\n      await gridResponseFuture();\n\n      expect(bloc.state.hasDocument!.value, false);\n\n      await RowBackendService(viewId: context.viewId)\n          .updateMeta(rowId: cellController.rowId, isDocumentEmpty: false);\n      await gridResponseFuture();\n\n      expect(bloc.state.hasDocument!.value, true);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_cell_bloc_test.dart",
    "content": "import 'package:appflowy/plugins/database/application/field/field_cell_bloc.dart';\nimport 'package:bloc_test/bloc_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../util.dart';\n\nvoid main() {\n  late AppFlowyGridTest gridTest;\n\n  setUpAll(() async {\n    gridTest = await AppFlowyGridTest.ensureInitialized();\n  });\n\n  group('field cell bloc:', () {\n    late GridTestContext context;\n    late double width;\n\n    setUp(() async {\n      context = await gridTest.makeDefaultTestGrid();\n    });\n\n    blocTest(\n      'update field width',\n      build: () => FieldCellBloc(\n        fieldInfo: context.fieldController.fieldInfos[0],\n        viewId: context.viewId,\n      ),\n      act: (bloc) {\n        width = bloc.state.width;\n        bloc.add(const FieldCellEvent.onResizeStart());\n        bloc.add(const FieldCellEvent.startUpdateWidth(100));\n        bloc.add(const FieldCellEvent.endUpdateWidth());\n      },\n      verify: (bloc) {\n        expect(bloc.state.width, width + 100);\n      },\n    );\n\n    blocTest(\n      'field width should not be less than 50px',\n      build: () => FieldCellBloc(\n        viewId: context.viewId,\n        fieldInfo: context.fieldController.fieldInfos[0],\n      ),\n      act: (bloc) {\n        bloc.add(const FieldCellEvent.onResizeStart());\n        bloc.add(const FieldCellEvent.startUpdateWidth(-110));\n        bloc.add(const FieldCellEvent.endUpdateWidth());\n      },\n      verify: (bloc) {\n        expect(bloc.state.width, 50);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_editor_bloc_test.dart",
    "content": "import 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:nanoid/nanoid.dart';\n\nimport '../util.dart';\n\nvoid main() {\n  late AppFlowyGridTest gridTest;\n\n  setUpAll(() async {\n    gridTest = await AppFlowyGridTest.ensureInitialized();\n  });\n\n  group('field editor bloc:', () {\n    late GridTestContext context;\n    late FieldEditorBloc editorBloc;\n\n    setUp(() async {\n      context = await gridTest.makeDefaultTestGrid();\n      final fieldInfo = context.fieldController.fieldInfos\n          .firstWhere((field) => field.fieldType == FieldType.SingleSelect);\n      editorBloc = FieldEditorBloc(\n        viewId: context.viewId,\n        fieldController: context.fieldController,\n        fieldInfo: fieldInfo,\n        isNew: false,\n      );\n    });\n\n    test('rename field', () async {\n      expect(editorBloc.state.field.name, equals(\"Type\"));\n\n      editorBloc.add(const FieldEditorEvent.renameField('Hello world'));\n\n      await gridResponseFuture();\n      expect(editorBloc.state.field.name, equals(\"Hello world\"));\n    });\n\n    test('edit icon', () async {\n      expect(editorBloc.state.field.icon, equals(\"\"));\n\n      editorBloc.add(const FieldEditorEvent.updateIcon('emoji/smiley-face'));\n\n      await gridResponseFuture();\n      expect(editorBloc.state.field.icon, equals(\"emoji/smiley-face\"));\n\n      editorBloc.add(const FieldEditorEvent.updateIcon(\"\"));\n\n      await gridResponseFuture();\n      expect(editorBloc.state.field.icon, equals(\"\"));\n    });\n\n    test('switch to text field', () async {\n      expect(editorBloc.state.field.fieldType, equals(FieldType.SingleSelect));\n\n      editorBloc.add(\n        const FieldEditorEvent.switchFieldType(FieldType.RichText),\n      );\n      await gridResponseFuture();\n\n      expect(editorBloc.state.field.fieldType, equals(FieldType.RichText));\n    });\n\n    test('update field type option', () async {\n      final selectOption = SelectOptionPB()\n        ..id = nanoid(4)\n        ..color = SelectOptionColorPB.Lime\n        ..name = \"New option\";\n      final typeOptionData = SingleSelectTypeOptionPB()\n        ..options.addAll([selectOption]);\n\n      editorBloc.add(\n        FieldEditorEvent.updateTypeOption(typeOptionData.writeToBuffer()),\n      );\n      await gridResponseFuture();\n\n      final actual = SingleSelectTypeOptionDataParser()\n          .fromBuffer(editorBloc.state.field.field.typeOptionData);\n\n      expect(actual, equals(typeOptionData));\n    });\n\n    test('update visibility', () async {\n      expect(\n        editorBloc.state.field.visibility,\n        equals(FieldVisibility.AlwaysShown),\n      );\n\n      editorBloc.add(const FieldEditorEvent.toggleFieldVisibility());\n      await gridResponseFuture();\n\n      expect(\n        editorBloc.state.field.visibility,\n        equals(FieldVisibility.AlwaysHidden),\n      );\n    });\n\n    test('update wrap cell', () async {\n      expect(\n        editorBloc.state.field.wrapCellContent,\n        equals(true),\n      );\n\n      editorBloc.add(const FieldEditorEvent.toggleWrapCellContent());\n      await gridResponseFuture();\n\n      expect(\n        editorBloc.state.field.wrapCellContent,\n        equals(false),\n      );\n    });\n\n    test('insert left and right', () async {\n      expect(\n        context.fieldController.fieldInfos.length,\n        equals(3),\n      );\n\n      editorBloc.add(const FieldEditorEvent.insertLeft());\n      await gridResponseFuture();\n      editorBloc.add(const FieldEditorEvent.insertRight());\n      await gridResponseFuture();\n\n      expect(\n        context.fieldController.fieldInfos.length,\n        equals(5),\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_editor_bloc_test.dart",
    "content": "import 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/plugins/database/domain/filter_service.dart';\nimport 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../util.dart';\n\nvoid main() {\n  late AppFlowyGridTest gridTest;\n\n  setUpAll(() async {\n    gridTest = await AppFlowyGridTest.ensureInitialized();\n  });\n\n  group('filter editor bloc:', () {\n    late GridTestContext context;\n    late FilterEditorBloc filterBloc;\n\n    setUp(() async {\n      context = await gridTest.makeDefaultTestGrid();\n      filterBloc = FilterEditorBloc(\n        viewId: context.viewId,\n        fieldController: context.fieldController,\n      );\n    });\n\n    FieldInfo getFirstFieldByType(FieldType fieldType) {\n      return context.fieldController.fieldInfos\n          .firstWhere((field) => field.fieldType == fieldType);\n    }\n\n    test('create filter', () async {\n      expect(filterBloc.state.filters.length, equals(0));\n      expect(filterBloc.state.fields.length, equals(3));\n\n      // through domain directly\n      final textField = getFirstFieldByType(FieldType.RichText);\n      final service = FilterBackendService(viewId: context.viewId);\n      await service.insertTextFilter(\n        fieldId: textField.id,\n        condition: TextFilterConditionPB.TextIsEmpty,\n        content: \"\",\n      );\n      await gridResponseFuture();\n      expect(filterBloc.state.filters.length, equals(1));\n      expect(filterBloc.state.fields.length, equals(3));\n\n      // through bloc event\n      final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);\n      filterBloc.add(FilterEditorEvent.createFilter(selectOptionField));\n      await gridResponseFuture();\n      expect(filterBloc.state.filters.length, equals(2));\n      expect(filterBloc.state.filters.first.fieldId, equals(textField.id));\n      expect(filterBloc.state.filters[1].fieldId, equals(selectOptionField.id));\n\n      final filter = filterBloc.state.filters.first as TextFilter;\n      expect(filter.condition, equals(TextFilterConditionPB.TextIsEmpty));\n      expect(filter.content, equals(\"\"));\n      final filter2 = filterBloc.state.filters[1] as SelectOptionFilter;\n      expect(filter2.condition, equals(SelectOptionFilterConditionPB.OptionIs));\n      expect(filter2.optionIds.length, equals(0));\n      expect(filterBloc.state.fields.length, equals(3));\n    });\n\n    test('change filtering field', () async {\n      final textField = getFirstFieldByType(FieldType.RichText);\n      final selectField = getFirstFieldByType(FieldType.Checkbox);\n      filterBloc.add(FilterEditorEvent.createFilter(textField));\n      await gridResponseFuture();\n      expect(filterBloc.state.filters.length, equals(1));\n      expect(filterBloc.state.fields.length, equals(3));\n      expect(\n        filterBloc.state.filters.first.fieldType,\n        equals(FieldType.RichText),\n      );\n\n      final filter = filterBloc.state.filters.first;\n      filterBloc.add(\n        FilterEditorEvent.changeFilteringField(filter.filterId, selectField),\n      );\n      await gridResponseFuture();\n      expect(filterBloc.state.filters.length, equals(1));\n      expect(\n        filterBloc.state.filters.first.fieldType,\n        equals(FieldType.Checkbox),\n      );\n      expect(filterBloc.state.fields.length, equals(3));\n    });\n\n    test('delete filter', () async {\n      final textField = getFirstFieldByType(FieldType.RichText);\n      filterBloc.add(FilterEditorEvent.createFilter(textField));\n      await gridResponseFuture();\n      expect(filterBloc.state.filters.length, equals(1));\n      expect(filterBloc.state.fields.length, equals(3));\n\n      final filter = filterBloc.state.filters.first;\n      filterBloc.add(FilterEditorEvent.deleteFilter(filter.filterId));\n      await gridResponseFuture();\n      expect(filterBloc.state.filters.length, equals(0));\n      expect(filterBloc.state.fields.length, equals(3));\n    });\n\n    test('update filter', () async {\n      final service = FilterBackendService(viewId: context.viewId);\n      final textField = getFirstFieldByType(FieldType.RichText);\n\n      // Create filter\n      await service.insertTextFilter(\n        fieldId: textField.id,\n        condition: TextFilterConditionPB.TextIsEmpty,\n        content: \"\",\n      );\n      await gridResponseFuture();\n      TextFilter filter = filterBloc.state.filters.first as TextFilter;\n      expect(filter.condition, equals(TextFilterConditionPB.TextIsEmpty));\n\n      final textFilter = context.fieldController.filters.first;\n\n      // Update the existing filter\n      await service.insertTextFilter(\n        fieldId: textField.id,\n        filterId: textFilter.filterId,\n        condition: TextFilterConditionPB.TextIs,\n        content: \"ABC\",\n      );\n      await gridResponseFuture();\n      filter = filterBloc.state.filters.first as TextFilter;\n      expect(filter.condition, equals(TextFilterConditionPB.TextIs));\n      expect(filter.content, equals(\"ABC\"));\n    });\n\n    test('update filtering field\\'s name', () async {\n      final textField = getFirstFieldByType(FieldType.RichText);\n      filterBloc.add(FilterEditorEvent.createFilter(textField));\n      await gridResponseFuture();\n      expect(filterBloc.state.filters.length, equals(1));\n\n      expect(filterBloc.state.fields.length, equals(3));\n      expect(filterBloc.state.fields.first.name, equals(\"Name\"));\n\n      // edit field\n      await FieldBackendService(\n        viewId: context.viewId,\n        fieldId: textField.id,\n      ).updateField(name: \"New Name\");\n      await gridResponseFuture();\n      expect(filterBloc.state.fields.length, equals(3));\n      expect(filterBloc.state.fields.first.name, equals(\"New Name\"));\n    });\n\n    test('update field type', () async {\n      final checkboxField = getFirstFieldByType(FieldType.Checkbox);\n      filterBloc.add(FilterEditorEvent.createFilter(checkboxField));\n      await gridResponseFuture();\n      expect(filterBloc.state.filters.length, equals(1));\n\n      // edit field\n      await FieldBackendService(\n        viewId: context.viewId,\n        fieldId: checkboxField.id,\n      ).updateType(fieldType: FieldType.DateTime);\n      await gridResponseFuture();\n\n      // filter is removed\n      expect(filterBloc.state.filters.length, equals(0));\n      expect(filterBloc.state.fields.length, equals(3));\n      expect(filterBloc.state.fields[2].fieldType, FieldType.DateTime);\n    });\n\n    test('update filter field', () async {\n      final checkboxField = getFirstFieldByType(FieldType.Checkbox);\n      filterBloc.add(FilterEditorEvent.createFilter(checkboxField));\n      await gridResponseFuture();\n      expect(filterBloc.state.filters.length, equals(1));\n\n      // edit field\n      await FieldBackendService(\n        viewId: context.viewId,\n        fieldId: checkboxField.id,\n      ).updateField(name: \"HERRO\");\n      await gridResponseFuture();\n\n      expect(filterBloc.state.filters.length, equals(1));\n      expect(filterBloc.state.fields.length, equals(3));\n      expect(filterBloc.state.fields[2].name, \"HERRO\");\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_entities_test.dart",
    "content": "import 'dart:typed_data';\n\nimport 'package:appflowy/plugins/database/application/field/filter_entities.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:fixnum/fixnum.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('parsing filter entities:', () {\n    FilterPB createFilterPB(\n      FieldType fieldType,\n      Uint8List data,\n    ) {\n      return FilterPB(\n        id: \"FT\",\n        filterType: FilterType.Data,\n        data: FilterDataPB(\n          fieldId: \"FD\",\n          fieldType: fieldType,\n          data: data,\n        ),\n      );\n    }\n\n    test('text', () async {\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.RichText,\n            TextFilterPB(\n              condition: TextFilterConditionPB.TextContains,\n              content: \"c\",\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          TextFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.RichText,\n            condition: TextFilterConditionPB.TextContains,\n            content: \"c\",\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.RichText,\n            TextFilterPB(\n              condition: TextFilterConditionPB.TextContains,\n              content: \"\",\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          TextFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.RichText,\n            condition: TextFilterConditionPB.TextContains,\n            content: \"\",\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.RichText,\n            TextFilterPB(\n              condition: TextFilterConditionPB.TextIsEmpty,\n              content: \"\",\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          TextFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.RichText,\n            condition: TextFilterConditionPB.TextIsEmpty,\n            content: \"\",\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.RichText,\n            TextFilterPB(\n              condition: TextFilterConditionPB.TextIsEmpty,\n              content: \"\",\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          TextFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.RichText,\n            condition: TextFilterConditionPB.TextIsEmpty,\n            content: \"c\",\n          ),\n        ),\n      );\n    });\n\n    test('number', () async {\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.Number,\n            NumberFilterPB(\n              condition: NumberFilterConditionPB.GreaterThan,\n              content: \"\",\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          NumberFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.Number,\n            condition: NumberFilterConditionPB.GreaterThan,\n            content: \"\",\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.Number,\n            NumberFilterPB(\n              condition: NumberFilterConditionPB.GreaterThan,\n              content: \"123\",\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          NumberFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.Number,\n            condition: NumberFilterConditionPB.GreaterThan,\n            content: \"123\",\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.Number,\n            NumberFilterPB(\n              condition: NumberFilterConditionPB.NumberIsEmpty,\n              content: \"\",\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          NumberFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.Number,\n            condition: NumberFilterConditionPB.NumberIsEmpty,\n            content: \"\",\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.Number,\n            NumberFilterPB(\n              condition: NumberFilterConditionPB.NumberIsEmpty,\n              content: \"\",\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          NumberFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.Number,\n            condition: NumberFilterConditionPB.NumberIsEmpty,\n            content: \"123\",\n          ),\n        ),\n      );\n    });\n\n    test('checkbox', () async {\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.Checkbox,\n            CheckboxFilterPB(\n              condition: CheckboxFilterConditionPB.IsChecked,\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          const CheckboxFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.Checkbox,\n            condition: CheckboxFilterConditionPB.IsChecked,\n          ),\n        ),\n      );\n    });\n\n    test('checklist', () async {\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.Checklist,\n            ChecklistFilterPB(\n              condition: ChecklistFilterConditionPB.IsComplete,\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          const ChecklistFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.Checklist,\n            condition: ChecklistFilterConditionPB.IsComplete,\n          ),\n        ),\n      );\n    });\n\n    test('single select option', () async {\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.SingleSelect,\n            SelectOptionFilterPB(\n              condition: SelectOptionFilterConditionPB.OptionIs,\n              optionIds: [],\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          SelectOptionFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.SingleSelect,\n            condition: SelectOptionFilterConditionPB.OptionIs,\n            optionIds: const [],\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.SingleSelect,\n            SelectOptionFilterPB(\n              condition: SelectOptionFilterConditionPB.OptionIs,\n              optionIds: ['a'],\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          SelectOptionFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.SingleSelect,\n            condition: SelectOptionFilterConditionPB.OptionIs,\n            optionIds: const ['a'],\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.SingleSelect,\n            SelectOptionFilterPB(\n              condition: SelectOptionFilterConditionPB.OptionIs,\n              optionIds: ['a', 'b'],\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          SelectOptionFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.SingleSelect,\n            condition: SelectOptionFilterConditionPB.OptionIs,\n            optionIds: const ['a', 'b'],\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.SingleSelect,\n            SelectOptionFilterPB(\n              condition: SelectOptionFilterConditionPB.OptionIsEmpty,\n              optionIds: [],\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          SelectOptionFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.SingleSelect,\n            condition: SelectOptionFilterConditionPB.OptionIsEmpty,\n            optionIds: const [],\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.SingleSelect,\n            SelectOptionFilterPB(\n              condition: SelectOptionFilterConditionPB.OptionIsEmpty,\n              optionIds: ['a'],\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          SelectOptionFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.SingleSelect,\n            condition: SelectOptionFilterConditionPB.OptionIsEmpty,\n            optionIds: const [],\n          ),\n        ),\n      );\n    });\n\n    test('multi select option', () async {\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.MultiSelect,\n            SelectOptionFilterPB(\n              condition: SelectOptionFilterConditionPB.OptionContains,\n              optionIds: [],\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          SelectOptionFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.MultiSelect,\n            condition: SelectOptionFilterConditionPB.OptionContains,\n            optionIds: const [],\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.MultiSelect,\n            SelectOptionFilterPB(\n              condition: SelectOptionFilterConditionPB.OptionContains,\n              optionIds: ['a'],\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          SelectOptionFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.MultiSelect,\n            condition: SelectOptionFilterConditionPB.OptionContains,\n            optionIds: const ['a'],\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.MultiSelect,\n            SelectOptionFilterPB(\n              condition: SelectOptionFilterConditionPB.OptionContains,\n              optionIds: ['a', 'b'],\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          SelectOptionFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.MultiSelect,\n            condition: SelectOptionFilterConditionPB.OptionContains,\n            optionIds: const ['a', 'b'],\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.MultiSelect,\n            SelectOptionFilterPB(\n              condition: SelectOptionFilterConditionPB.OptionIs,\n              optionIds: ['a', 'b'],\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          SelectOptionFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.MultiSelect,\n            condition: SelectOptionFilterConditionPB.OptionIs,\n            optionIds: const ['a', 'b'],\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.MultiSelect,\n            SelectOptionFilterPB(\n              condition: SelectOptionFilterConditionPB.OptionIsEmpty,\n              optionIds: [],\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          SelectOptionFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.MultiSelect,\n            condition: SelectOptionFilterConditionPB.OptionIsEmpty,\n            optionIds: const [],\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.MultiSelect,\n            SelectOptionFilterPB(\n              condition: SelectOptionFilterConditionPB.OptionIsEmpty,\n              optionIds: ['a'],\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          SelectOptionFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.MultiSelect,\n            condition: SelectOptionFilterConditionPB.OptionIsEmpty,\n            optionIds: const [],\n          ),\n        ),\n      );\n    });\n\n    test('date time', () {\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.DateTime,\n            DateFilterPB(\n              condition: DateFilterConditionPB.DateStartsOn,\n              timestamp: Int64(5),\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          DateTimeFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.DateTime,\n            condition: DateFilterConditionPB.DateStartsOn,\n            timestamp: DateTime.fromMillisecondsSinceEpoch(5 * 1000),\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.DateTime,\n            DateFilterPB(\n              condition: DateFilterConditionPB.DateStartsOn,\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          DateTimeFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.DateTime,\n            condition: DateFilterConditionPB.DateStartsOn,\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.DateTime,\n            DateFilterPB(\n              condition: DateFilterConditionPB.DateStartsOn,\n              start: Int64(5),\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          DateTimeFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.DateTime,\n            condition: DateFilterConditionPB.DateStartsOn,\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.DateTime,\n            DateFilterPB(\n              condition: DateFilterConditionPB.DateEndsBetween,\n              start: Int64(5),\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          DateTimeFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.DateTime,\n            condition: DateFilterConditionPB.DateEndsBetween,\n            start: DateTime.fromMillisecondsSinceEpoch(5 * 1000),\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.DateTime,\n            DateFilterPB(\n              condition: DateFilterConditionPB.DateEndIsNotEmpty,\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          DateTimeFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.DateTime,\n            condition: DateFilterConditionPB.DateEndIsNotEmpty,\n          ),\n        ),\n      );\n\n      expect(\n        DatabaseFilter.fromPB(\n          createFilterPB(\n            FieldType.DateTime,\n            DateFilterPB(\n              condition: DateFilterConditionPB.DateEndIsNotEmpty,\n              start: Int64(5),\n              end: Int64(5),\n              timestamp: Int64(5),\n            ).writeToBuffer(),\n          ),\n        ),\n        equals(\n          DateTimeFilter(\n            filterId: \"FT\",\n            fieldId: \"FD\",\n            fieldType: FieldType.DateTime,\n            condition: DateFilterConditionPB.DateEndIsNotEmpty,\n          ),\n        ),\n      );\n    });\n  });\n\n  // group('write to buffer', () {\n  //   test('text', () {});\n  // });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart",
    "content": "import 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';\nimport 'package:bloc_test/bloc_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'util.dart';\n\nvoid main() {\n  late AppFlowyGridTest gridTest;\n\n  setUpAll(() async {\n    gridTest = await AppFlowyGridTest.ensureInitialized();\n  });\n\n  group('Edit Grid:', () {\n    late GridTestContext context;\n\n    setUp(() async {\n      context = await gridTest.makeDefaultTestGrid();\n    });\n\n    // The initial number of rows is 3 for each grid\n    // We create one row so we expect 4 rows\n    blocTest<GridBloc, GridState>(\n      \"create a row\",\n      build: () => GridBloc(\n        view: context.view,\n        databaseController: DatabaseController(view: context.view),\n      )..add(const GridEvent.initial()),\n      act: (bloc) => bloc.add(const GridEvent.createRow()),\n      wait: gridResponseDuration(),\n      verify: (bloc) {\n        expect(bloc.state.rowInfos.length, equals(4));\n      },\n    );\n\n    blocTest<GridBloc, GridState>(\n      \"delete the last row\",\n      build: () => GridBloc(\n        view: context.view,\n        databaseController: DatabaseController(view: context.view),\n      )..add(const GridEvent.initial()),\n      act: (bloc) async {\n        await gridResponseFuture();\n        bloc.add(GridEvent.deleteRow(bloc.state.rowInfos.last));\n      },\n      wait: gridResponseDuration(),\n      verify: (bloc) {\n        expect(bloc.state.rowInfos.length, equals(2));\n      },\n    );\n\n    String? firstId;\n    String? secondId;\n    String? thirdId;\n\n    blocTest(\n      'reorder rows',\n      build: () => GridBloc(\n        view: context.view,\n        databaseController: DatabaseController(view: context.view),\n      )..add(const GridEvent.initial()),\n      act: (bloc) async {\n        await gridResponseFuture();\n\n        firstId = bloc.state.rowInfos[0].rowId;\n        secondId = bloc.state.rowInfos[1].rowId;\n        thirdId = bloc.state.rowInfos[2].rowId;\n\n        bloc.add(const GridEvent.moveRow(0, 2));\n      },\n      wait: gridResponseDuration(),\n      verify: (bloc) {\n        expect(secondId, equals(bloc.state.rowInfos[0].rowId));\n        expect(thirdId, equals(bloc.state.rowInfos[1].rowId));\n        expect(firstId, equals(bloc.state.rowInfos[2].rowId));\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/grid_test/sort/sort_editor_bloc_test.dart",
    "content": "import 'package:appflowy/plugins/database/application/field/field_info.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../util.dart';\n\nvoid main() {\n  late AppFlowyGridTest gridTest;\n\n  setUpAll(() async {\n    gridTest = await AppFlowyGridTest.ensureInitialized();\n  });\n\n  group('sort editor bloc:', () {\n    late GridTestContext context;\n    late SortEditorBloc sortBloc;\n\n    setUp(() async {\n      context = await gridTest.makeDefaultTestGrid();\n      sortBloc = SortEditorBloc(\n        viewId: context.viewId,\n        fieldController: context.fieldController,\n      );\n    });\n\n    FieldInfo getFirstFieldByType(FieldType fieldType) {\n      return context.fieldController.fieldInfos\n          .firstWhere((field) => field.fieldType == fieldType);\n    }\n\n    test('create sort', () async {\n      expect(sortBloc.state.sorts.length, equals(0));\n      expect(sortBloc.state.creatableFields.length, equals(3));\n      expect(sortBloc.state.allFields.length, equals(3));\n\n      final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);\n      sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id));\n      await gridResponseFuture();\n      expect(sortBloc.state.sorts.length, 1);\n      expect(sortBloc.state.sorts.first.fieldId, selectOptionField.id);\n      expect(\n        sortBloc.state.sorts.first.condition,\n        SortConditionPB.Ascending,\n      );\n      expect(sortBloc.state.creatableFields.length, equals(2));\n      expect(sortBloc.state.allFields.length, equals(3));\n    });\n\n    test('change sort field', () async {\n      final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);\n      sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id));\n      await gridResponseFuture();\n\n      expect(\n        sortBloc.state.creatableFields\n            .map((e) => e.id)\n            .contains(selectOptionField.id),\n        false,\n      );\n\n      final checkboxField = getFirstFieldByType(FieldType.Checkbox);\n      sortBloc.add(\n        SortEditorEvent.editSort(\n          sortId: sortBloc.state.sorts.first.sortId,\n          fieldId: checkboxField.id,\n        ),\n      );\n      await gridResponseFuture();\n\n      expect(sortBloc.state.creatableFields.length, equals(2));\n      expect(\n        sortBloc.state.creatableFields\n            .map((e) => e.id)\n            .contains(checkboxField.id),\n        false,\n      );\n    });\n\n    test('update sort direction', () async {\n      final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);\n      sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id));\n      await gridResponseFuture();\n\n      expect(\n        sortBloc.state.sorts.first.condition,\n        SortConditionPB.Ascending,\n      );\n\n      sortBloc.add(\n        SortEditorEvent.editSort(\n          sortId: sortBloc.state.sorts.first.sortId,\n          condition: SortConditionPB.Descending,\n        ),\n      );\n      await gridResponseFuture();\n\n      expect(\n        sortBloc.state.sorts.first.condition,\n        SortConditionPB.Descending,\n      );\n    });\n\n    test('reorder sorts', () async {\n      final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);\n      final checkboxField = getFirstFieldByType(FieldType.Checkbox);\n      sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id));\n      await gridResponseFuture();\n      sortBloc.add(SortEditorEvent.createSort(fieldId: checkboxField.id));\n      await gridResponseFuture();\n\n      expect(sortBloc.state.sorts[0].fieldId, selectOptionField.id);\n      expect(sortBloc.state.sorts[1].fieldId, checkboxField.id);\n      expect(sortBloc.state.creatableFields.length, equals(1));\n      expect(sortBloc.state.allFields.length, equals(3));\n\n      sortBloc.add(\n        const SortEditorEvent.reorderSort(0, 2),\n      );\n      await gridResponseFuture();\n\n      expect(sortBloc.state.sorts[0].fieldId, checkboxField.id);\n      expect(sortBloc.state.sorts[1].fieldId, selectOptionField.id);\n      expect(sortBloc.state.creatableFields.length, equals(1));\n      expect(sortBloc.state.allFields.length, equals(3));\n    });\n\n    test('delete sort', () async {\n      final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);\n      sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id));\n      await gridResponseFuture();\n\n      expect(sortBloc.state.sorts.length, 1);\n\n      sortBloc.add(\n        SortEditorEvent.deleteSort(sortBloc.state.sorts.first.sortId),\n      );\n      await gridResponseFuture();\n\n      expect(sortBloc.state.sorts.length, 0);\n      expect(sortBloc.state.creatableFields.length, equals(3));\n      expect(sortBloc.state.allFields.length, equals(3));\n    });\n\n    test('delete all sorts', () async {\n      final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);\n      final checkboxField = getFirstFieldByType(FieldType.Checkbox);\n      sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id));\n      await gridResponseFuture();\n      sortBloc.add(SortEditorEvent.createSort(fieldId: checkboxField.id));\n      await gridResponseFuture();\n\n      expect(sortBloc.state.sorts.length, 2);\n\n      sortBloc.add(const SortEditorEvent.deleteAllSorts());\n      await gridResponseFuture();\n\n      expect(sortBloc.state.sorts.length, 0);\n      expect(sortBloc.state.creatableFields.length, equals(3));\n      expect(sortBloc.state.allFields.length, equals(3));\n    });\n\n    test('update sort field', () async {\n      final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);\n      sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id));\n      await gridResponseFuture();\n\n      expect(sortBloc.state.sorts.length, equals(1));\n\n      // edit field\n      await FieldBackendService(\n        viewId: context.viewId,\n        fieldId: selectOptionField.id,\n      ).updateField(name: \"HERRO\");\n      await gridResponseFuture();\n\n      expect(sortBloc.state.sorts.length, equals(1));\n      expect(sortBloc.state.allFields[1].name, \"HERRO\");\n\n      expect(sortBloc.state.creatableFields.length, equals(2));\n      expect(sortBloc.state.allFields.length, equals(3));\n    });\n\n    test('delete sorting field', () async {\n      final selectOptionField = getFirstFieldByType(FieldType.SingleSelect);\n      sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id));\n      await gridResponseFuture();\n\n      expect(sortBloc.state.sorts.length, equals(1));\n\n      // edit field\n      await FieldBackendService(\n        viewId: context.viewId,\n        fieldId: selectOptionField.id,\n      ).delete();\n      await gridResponseFuture();\n\n      expect(sortBloc.state.sorts.length, equals(0));\n      expect(sortBloc.state.creatableFields.length, equals(2));\n      expect(sortBloc.state.allFields.length, equals(2));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/plugins/database/application/cell/cell_controller.dart';\nimport 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';\nimport 'package:appflowy/plugins/database/application/field/field_controller.dart';\nimport 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart';\nimport 'package:appflowy/plugins/database/domain/field_service.dart';\nimport 'package:appflowy/plugins/database/application/row/row_cache.dart';\nimport 'package:appflowy/plugins/database/application/database_controller.dart';\nimport 'package:appflowy/workspace/application/settings/share/import_service.dart';\nimport 'package:appflowy/workspace/application/view/view_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:flutter/services.dart';\n\nimport '../../util.dart';\n\nconst v020GridFileName = \"v020.afdb\";\nconst v069GridFileName = \"v069.afdb\";\n\nclass GridTestContext {\n  GridTestContext(this.view, this.databaseController);\n\n  final ViewPB view;\n  final DatabaseController databaseController;\n\n  String get viewId => view.id;\n\n  List<RowInfo> get rowInfos {\n    return databaseController.rowCache.rowInfos;\n  }\n\n  FieldController get fieldController => databaseController.fieldController;\n\n  Future<FieldEditorBloc> createField(FieldType fieldType) async {\n    final editorBloc =\n        await createFieldEditor(databaseController: databaseController);\n    await gridResponseFuture();\n    editorBloc.add(FieldEditorEvent.switchFieldType(fieldType));\n    await gridResponseFuture();\n    return editorBloc;\n  }\n\n  CellController makeGridCellController(int fieldIndex, int rowIndex) {\n    return makeCellController(\n      databaseController,\n      CellContext(\n        fieldId: fieldController.fieldInfos[fieldIndex].id,\n        rowId: rowInfos[rowIndex].rowId,\n      ),\n    ).as();\n  }\n}\n\nFuture<FieldEditorBloc> createFieldEditor({\n  required DatabaseController databaseController,\n}) async {\n  final result = await FieldBackendService.createField(\n    viewId: databaseController.viewId,\n  );\n  await gridResponseFuture();\n  return result.fold(\n    (field) {\n      return FieldEditorBloc(\n        viewId: databaseController.viewId,\n        fieldController: databaseController.fieldController,\n        fieldInfo: databaseController.fieldController.getField(field.id)!,\n        isNew: true,\n      );\n    },\n    (err) => throw Exception(err),\n  );\n}\n\n/// Create a empty Grid for test\nclass AppFlowyGridTest {\n  AppFlowyGridTest({required this.unitTest});\n\n  final AppFlowyUnitTest unitTest;\n\n  static Future<AppFlowyGridTest> ensureInitialized() async {\n    final inner = await AppFlowyUnitTest.ensureInitialized();\n    return AppFlowyGridTest(unitTest: inner);\n  }\n\n  Future<GridTestContext> makeDefaultTestGrid() async {\n    final workspace = await unitTest.createWorkspace();\n    final context = await ViewBackendService.createView(\n      parentViewId: workspace.id,\n      name: \"Test Grid\",\n      layoutType: ViewLayoutPB.Grid,\n      openAfterCreate: true,\n    ).fold(\n      (view) async {\n        final databaseController = DatabaseController(view: view);\n        await databaseController\n            .open()\n            .fold((l) => null, (r) => throw Exception(r));\n        return GridTestContext(\n          view,\n          databaseController,\n        );\n      },\n      (error) => throw Exception(),\n    );\n\n    return context;\n  }\n\n  Future<GridTestContext> makeTestGridFromImportedData(\n    String fileName,\n  ) async {\n    final workspace = await unitTest.createWorkspace();\n\n    // Don't use the p.join to build the path that used in loadString. It\n    // is not working on windows.\n    final data = await rootBundle\n        .loadString(\"assets/test/workspaces/database/$fileName\");\n\n    final context = await ImportBackendService.importPages(\n      workspace.id,\n      [\n        ImportItemPayloadPB()\n          ..name = fileName\n          ..data = utf8.encode(data)\n          ..viewLayout = ViewLayoutPB.Grid\n          ..importType = ImportTypePB.AFDatabase,\n      ],\n    ).fold(\n      (views) async {\n        final view = views.items.first;\n        final databaseController = DatabaseController(view: view);\n        await databaseController\n            .open()\n            .fold((l) => null, (r) => throw Exception(r));\n        return GridTestContext(\n          view,\n          databaseController,\n        );\n      },\n      (err) => throw Exception(),\n    );\n\n    return context;\n  }\n}\n\nFuture<void> gridResponseFuture({int milliseconds = 300}) {\n  return Future.delayed(\n    gridResponseDuration(milliseconds: milliseconds),\n  );\n}\n\nDuration gridResponseDuration({int milliseconds = 300}) {\n  return Duration(milliseconds: milliseconds);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart",
    "content": "import 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/workspace/application/home/home_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../util.dart';\n\nvoid main() {\n  late AppFlowyUnitTest testContext;\n  setUpAll(() async {\n    testContext = await AppFlowyUnitTest.ensureInitialized();\n  });\n\n  test('init home screen', () async {\n    final workspaceSetting = await FolderEventGetCurrentWorkspaceSetting()\n        .send()\n        .then((result) => result.fold((l) => l, (r) => throw Exception()));\n    await blocResponseFuture();\n\n    final homeBloc = HomeBloc(workspaceSetting)..add(const HomeEvent.initial());\n    await blocResponseFuture();\n\n    assert(homeBloc.state.workspaceSetting.hasLatestView());\n  });\n\n  test('open the document', () async {\n    final workspaceSetting = await FolderEventGetCurrentWorkspaceSetting()\n        .send()\n        .then((result) => result.fold((l) => l, (r) => throw Exception()));\n    await blocResponseFuture();\n\n    final homeBloc = HomeBloc(workspaceSetting)..add(const HomeEvent.initial());\n    await blocResponseFuture();\n\n    final app = await testContext.createWorkspace();\n    final appBloc = ViewBloc(view: app)..add(const ViewEvent.initial());\n    assert(appBloc.state.lastCreatedView == null);\n\n    appBloc.add(\n      const ViewEvent.createView(\n        \"New document\",\n        ViewLayoutPB.Document,\n        section: ViewSectionPB.Public,\n      ),\n    );\n    await blocResponseFuture();\n\n    assert(appBloc.state.lastCreatedView != null);\n    final latestView = appBloc.state.lastCreatedView!;\n    final _ = DocumentBloc(documentId: latestView.id)\n      ..add(const DocumentEvent.initial());\n\n    await FolderEventSetLatestView(ViewIdPB(value: latestView.id)).send();\n    await blocResponseFuture();\n\n    final actual = homeBloc.state.workspaceSetting.latestView.id;\n    assert(actual == latestView.id);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/home_test/sidebar_section_bloc_test.dart",
    "content": "import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../util.dart';\n\nvoid main() {\n  late AppFlowyUnitTest testContext;\n  setUpAll(() async {\n    testContext = await AppFlowyUnitTest.ensureInitialized();\n  });\n\n  test('assert initial apps is the build-in app', () async {\n    final menuBloc = SidebarSectionsBloc()\n      ..add(\n        SidebarSectionsEvent.initial(\n          testContext.userProfile,\n          testContext.currentWorkspace.id,\n        ),\n      );\n\n    await blocResponseFuture();\n\n    assert(menuBloc.state.section.publicViews.length == 1);\n    assert(menuBloc.state.section.privateViews.isEmpty);\n  });\n\n  test('create views', () async {\n    final menuBloc = SidebarSectionsBloc()\n      ..add(\n        SidebarSectionsEvent.initial(\n          testContext.userProfile,\n          testContext.currentWorkspace.id,\n        ),\n      );\n    await blocResponseFuture();\n\n    final names = ['View 1', 'View 2', 'View 3'];\n    for (final name in names) {\n      menuBloc.add(\n        SidebarSectionsEvent.createRootViewInSection(\n          name: name,\n          index: 0,\n          viewSection: ViewSectionPB.Public,\n        ),\n      );\n      await blocResponseFuture();\n    }\n\n    final reversedNames = names.reversed.toList();\n    for (var i = 0; i < names.length; i++) {\n      assert(\n        menuBloc.state.section.publicViews[i].name == reversedNames[i],\n      );\n    }\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart",
    "content": "import 'package:appflowy/plugins/trash/application/trash_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../util.dart';\n\nclass TrashTestContext {\n  TrashTestContext(this.unitTest);\n\n  late ViewPB view;\n  late ViewBloc viewBloc;\n  late List<ViewPB> allViews;\n  final AppFlowyUnitTest unitTest;\n\n  Future<void> initialize() async {\n    view = await unitTest.createWorkspace();\n    viewBloc = ViewBloc(view: view)..add(const ViewEvent.initial());\n    await blocResponseFuture();\n\n    viewBloc.add(\n      const ViewEvent.createView(\n        \"Document 1\",\n        ViewLayoutPB.Document,\n        section: ViewSectionPB.Public,\n      ),\n    );\n    await blocResponseFuture(millisecond: 300);\n\n    viewBloc.add(\n      const ViewEvent.createView(\n        \"Document 2\",\n        ViewLayoutPB.Document,\n        section: ViewSectionPB.Public,\n      ),\n    );\n    await blocResponseFuture(millisecond: 300);\n\n    viewBloc.add(\n      const ViewEvent.createView(\n        \"Document 3\",\n        ViewLayoutPB.Document,\n        section: ViewSectionPB.Public,\n      ),\n    );\n    await blocResponseFuture(millisecond: 300);\n\n    allViews = [...viewBloc.state.view.childViews];\n    assert(allViews.length == 3, 'but receive ${allViews.length}');\n  }\n}\n\nvoid main() {\n  late AppFlowyUnitTest unitTest;\n  setUpAll(() async {\n    unitTest = await AppFlowyUnitTest.ensureInitialized();\n  });\n\n  // 1. Create three views\n  // 2. Delete a view and check the state\n  // 3. Delete all views and check the state\n  // 4. Put back a view\n  // 5. Put back all views\n\n  group('trash test: ', () {\n    test('delete a view', () async {\n      final context = TrashTestContext(unitTest);\n      await context.initialize();\n      final trashBloc = TrashBloc()..add(const TrashEvent.initial());\n      await blocResponseFuture();\n\n      // delete a view\n      final deletedView = context.viewBloc.state.view.childViews[0];\n      final deleteViewBloc = ViewBloc(view: deletedView)\n        ..add(const ViewEvent.initial());\n      await blocResponseFuture();\n      deleteViewBloc.add(const ViewEvent.delete());\n      await blocResponseFuture(millisecond: 1000);\n      assert(context.viewBloc.state.view.childViews.length == 2);\n      assert(trashBloc.state.objects.length == 1);\n      assert(trashBloc.state.objects.first.id == deletedView.id);\n\n      // put back\n      trashBloc.add(TrashEvent.putback(deletedView.id));\n      await blocResponseFuture(millisecond: 1000);\n      assert(context.viewBloc.state.view.childViews.length == 3);\n      assert(trashBloc.state.objects.isEmpty);\n\n      // delete all views\n      for (final view in context.allViews) {\n        final deleteViewBloc = ViewBloc(view: view)\n          ..add(const ViewEvent.initial());\n        await blocResponseFuture();\n        deleteViewBloc.add(const ViewEvent.delete());\n        await blocResponseFuture(millisecond: 1000);\n      }\n      expect(trashBloc.state.objects[0].id, context.allViews[0].id);\n      expect(trashBloc.state.objects[1].id, context.allViews[1].id);\n      expect(trashBloc.state.objects[2].id, context.allViews[2].id);\n\n      // delete a view permanently\n      trashBloc.add(TrashEvent.delete(trashBloc.state.objects[0]));\n      await blocResponseFuture(millisecond: 1000);\n      expect(trashBloc.state.objects.length, 2);\n\n      // delete all view permanently\n      trashBloc.add(const TrashEvent.deleteAll());\n      await blocResponseFuture(millisecond: 1000);\n      assert(\n        trashBloc.state.objects.isEmpty,\n        \"but receive ${trashBloc.state.objects.length}\",\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart",
    "content": "import 'package:appflowy/plugins/document/application/document_bloc.dart';\nimport 'package:appflowy/workspace/application/view/view_bloc.dart';\nimport 'package:appflowy_backend/dispatch/dispatch.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../util.dart';\n\nvoid main() {\n  const name = 'Hello world';\n\n  late AppFlowyUnitTest testContext;\n\n  setUpAll(() async {\n    testContext = await AppFlowyUnitTest.ensureInitialized();\n  });\n\n  Future<ViewBloc> createTestViewBloc() async {\n    final view = await testContext.createWorkspace();\n    final viewBloc = ViewBloc(view: view)\n      ..add(\n        const ViewEvent.initial(),\n      );\n    await blocResponseFuture();\n    return viewBloc;\n  }\n\n  test('rename view test', () async {\n    final viewBloc = await createTestViewBloc();\n    viewBloc.add(const ViewEvent.rename(name));\n    await blocResponseFuture();\n    expect(viewBloc.state.view.name, name);\n  });\n\n  test('duplicate view test', () async {\n    final viewBloc = await createTestViewBloc();\n    // create a nested view\n    viewBloc.add(\n      const ViewEvent.createView(\n        name,\n        ViewLayoutPB.Document,\n        section: ViewSectionPB.Public,\n      ),\n    );\n    await blocResponseFuture();\n    expect(viewBloc.state.view.childViews.length, 1);\n    final childViewBloc = ViewBloc(view: viewBloc.state.view.childViews.first)\n      ..add(\n        const ViewEvent.initial(),\n      );\n    childViewBloc.add(const ViewEvent.duplicate());\n    await blocResponseFuture(millisecond: 1000);\n    expect(viewBloc.state.view.childViews.length, 2);\n  });\n\n  test('delete view test', () async {\n    final viewBloc = await createTestViewBloc();\n    viewBloc.add(\n      const ViewEvent.createView(\n        name,\n        ViewLayoutPB.Document,\n        section: ViewSectionPB.Public,\n      ),\n    );\n    await blocResponseFuture();\n    expect(viewBloc.state.view.childViews.length, 1);\n    final childViewBloc = ViewBloc(view: viewBloc.state.view.childViews.first)\n      ..add(\n        const ViewEvent.initial(),\n      );\n    await blocResponseFuture();\n    childViewBloc.add(const ViewEvent.delete());\n    await blocResponseFuture();\n    assert(viewBloc.state.view.childViews.isEmpty);\n  });\n\n  test('create nested view test', () async {\n    final viewBloc = await createTestViewBloc();\n    viewBloc.add(\n      const ViewEvent.createView(\n        'Document 1',\n        ViewLayoutPB.Document,\n        section: ViewSectionPB.Public,\n      ),\n    );\n    await blocResponseFuture();\n    final document1Bloc = ViewBloc(view: viewBloc.state.view.childViews.first)\n      ..add(\n        const ViewEvent.initial(),\n      );\n    await blocResponseFuture();\n    const name = 'Document 1 - 1';\n    document1Bloc.add(\n      const ViewEvent.createView(\n        'Document 1 - 1',\n        ViewLayoutPB.Document,\n        section: ViewSectionPB.Public,\n      ),\n    );\n    await blocResponseFuture();\n    expect(document1Bloc.state.view.childViews.length, 1);\n    expect(document1Bloc.state.view.childViews.first.name, name);\n  });\n\n  test('create documents in order', () async {\n    final viewBloc = await createTestViewBloc();\n    final names = ['1', '2', '3'];\n    for (final name in names) {\n      viewBloc.add(\n        ViewEvent.createView(\n          name,\n          ViewLayoutPB.Document,\n          section: ViewSectionPB.Public,\n        ),\n      );\n      await blocResponseFuture(millisecond: 400);\n    }\n\n    expect(viewBloc.state.view.childViews.length, 3);\n    for (var i = 0; i < names.length; i++) {\n      expect(viewBloc.state.view.childViews[i].name, names[i]);\n    }\n  });\n\n  test('open latest view test', () async {\n    final viewBloc = await createTestViewBloc();\n    expect(viewBloc.state.lastCreatedView, isNull);\n\n    viewBloc.add(\n      const ViewEvent.createView(\n        '1',\n        ViewLayoutPB.Document,\n        section: ViewSectionPB.Public,\n      ),\n    );\n    await blocResponseFuture();\n    expect(\n      viewBloc.state.lastCreatedView!.id,\n      viewBloc.state.view.childViews.last.id,\n    );\n    expect(\n      viewBloc.state.lastCreatedView!.name,\n      '1',\n    );\n\n    viewBloc.add(\n      const ViewEvent.createView(\n        '2',\n        ViewLayoutPB.Document,\n        section: ViewSectionPB.Public,\n      ),\n    );\n    await blocResponseFuture();\n    expect(\n      viewBloc.state.lastCreatedView!.name,\n      '2',\n    );\n  });\n\n  test('open latest document test', () async {\n    const name1 = 'document';\n    final viewBloc = await createTestViewBloc();\n    viewBloc.add(\n      const ViewEvent.createView(\n        name1,\n        ViewLayoutPB.Document,\n        section: ViewSectionPB.Public,\n      ),\n    );\n    await blocResponseFuture();\n    final document = viewBloc.state.lastCreatedView!;\n    assert(document.name == name1);\n\n    const gird = 'grid';\n    viewBloc.add(\n      const ViewEvent.createView(\n        gird,\n        ViewLayoutPB.Document,\n        section: ViewSectionPB.Public,\n      ),\n    );\n    await blocResponseFuture();\n    assert(viewBloc.state.lastCreatedView!.name == gird);\n\n    var workspaceLatest =\n        await FolderEventGetCurrentWorkspaceSetting().send().then(\n              (result) => result.fold(\n                (l) => l,\n                (r) => throw Exception(),\n              ),\n            );\n    workspaceLatest.latestView.id == viewBloc.state.lastCreatedView!.id;\n\n    // ignore: unused_local_variable\n    final documentBloc = DocumentBloc(documentId: document.id)\n      ..add(\n        const DocumentEvent.initial(),\n      );\n    await blocResponseFuture();\n\n    workspaceLatest = await FolderEventGetCurrentWorkspaceSetting().send().then(\n          (result) => result.fold(\n            (l) => l,\n            (r) => throw Exception(),\n          ),\n        );\n    workspaceLatest.latestView.id == document.id;\n  });\n\n  test('create views', () async {\n    final viewBloc = await createTestViewBloc();\n    const layouts = ViewLayoutPB.values;\n    for (var i = 0; i < layouts.length; i++) {\n      final layout = layouts[i];\n      if (layout == ViewLayoutPB.Chat) {\n        continue;\n      }\n      viewBloc.add(\n        ViewEvent.createView(\n          'Test $layout',\n          layout,\n          section: ViewSectionPB.Public,\n        ),\n      );\n      await blocResponseFuture(millisecond: 1000);\n      expect(viewBloc.state.view.childViews.length, i + 1);\n      expect(viewBloc.state.view.childViews.last.name, 'Test $layout');\n      expect(viewBloc.state.view.childViews.last.layout, layout);\n    }\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/lib/features/settings/data_location_bloc_test.dart",
    "content": "import 'package:appflowy/features/settings/settings.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc_test/bloc_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass MockSettingsRepository extends Mock implements SettingsRepository {}\n\nvoid main() {\n  late MockSettingsRepository repository;\n  late DataLocationBloc bloc;\n\n  const defaultPath = '/default/path';\n  const customPath = '/custom/path';\n\n  setUp(() {\n    repository = MockSettingsRepository();\n    when(() => repository.getUserDataLocation()).thenAnswer(\n      (_) async => FlowyResult.success(\n        UserDataLocation(path: defaultPath, isCustom: false),\n      ),\n    );\n    bloc = DataLocationBloc(repository: repository)\n      ..add(DataLocationEvent.initial());\n  });\n\n  tearDown(() async {\n    await bloc.close();\n  });\n\n  blocTest<DataLocationBloc, DataLocationState>(\n    'emits updated state when resetting to default',\n    build: () => bloc,\n    setUp: () {\n      when(() => repository.resetUserDataLocation()).thenAnswer(\n        (_) async => FlowyResult.success(\n          UserDataLocation(path: defaultPath, isCustom: false),\n        ),\n      );\n    },\n    act: (bloc) => bloc.add(DataLocationEvent.resetToDefault()),\n    wait: const Duration(milliseconds: 100),\n    expect: () => [\n      DataLocationState(\n        userDataLocation: UserDataLocation(path: defaultPath, isCustom: false),\n        didResetToDefault: true,\n      ),\n    ],\n  );\n\n  blocTest<DataLocationBloc, DataLocationState>(\n    'emits updated state when setting custom path',\n    build: () => bloc,\n    setUp: () {\n      when(() => repository.setCustomLocation(customPath)).thenAnswer(\n        (_) async => FlowyResult.success(\n          UserDataLocation(path: customPath, isCustom: true),\n        ),\n      );\n    },\n    act: (bloc) => bloc.add(DataLocationEvent.setCustomPath(customPath)),\n    wait: const Duration(milliseconds: 100),\n    expect: () => [\n      DataLocationState(\n        userDataLocation: UserDataLocation(path: customPath, isCustom: true),\n        didResetToDefault: false,\n      ),\n    ],\n  );\n\n  blocTest<DataLocationBloc, DataLocationState>(\n    'emits state with cleared reset flag',\n    build: () => bloc,\n    seed: () => DataLocationState(\n      userDataLocation: UserDataLocation(path: defaultPath, isCustom: false),\n      didResetToDefault: true,\n    ),\n    act: (bloc) => bloc.add(DataLocationEvent.clearState()),\n    wait: const Duration(milliseconds: 100),\n    expect: () => [\n      DataLocationState(\n        userDataLocation: UserDataLocation(\n          path: defaultPath,\n          isCustom: false,\n        ),\n        didResetToDefault: false,\n      ),\n    ],\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/lib/features/share_section/shared_section_bloc_test.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/share_access_level.dart';\nimport 'package:appflowy/features/shared_section/data/repositories/shared_pages_repository.dart';\nimport 'package:appflowy/features/shared_section/logic/shared_section_bloc.dart';\nimport 'package:appflowy/features/shared_section/models/shared_page.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc_test/bloc_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass MockSharePagesRepository extends Mock implements SharedPagesRepository {}\n\nvoid main() {\n  late MockSharePagesRepository repository;\n  late SharedSectionBloc bloc;\n  const workspaceId = 'workspace-id';\n  final initialPages = <SharedPage>[];\n  final updatedPages = <SharedPage>[\n    SharedPage(\n      view: ViewPB(\n        id: '1',\n        name: 'Page 1',\n      ),\n      accessLevel: ShareAccessLevel.readOnly,\n    ),\n  ];\n\n  setUp(() {\n    repository = MockSharePagesRepository();\n    when(() => repository.getSharedPages())\n        .thenAnswer((_) async => FlowyResult.success(initialPages));\n    bloc = SharedSectionBloc(\n      workspaceId: workspaceId,\n      repository: repository,\n    )..add(const SharedSectionEvent.init());\n  });\n\n  tearDown(() async {\n    await bloc.close();\n  });\n\n  blocTest<SharedSectionBloc, SharedSectionState>(\n    'emits updated sharedPages on updateSharedPages',\n    build: () => bloc,\n    act: (bloc) => bloc.add(\n      SharedSectionEvent.updateSharedPages(\n        sharedPages: updatedPages,\n      ),\n    ),\n    wait: const Duration(milliseconds: 100),\n    expect: () => [\n      SharedSectionState.initial().copyWith(\n        sharedPages: updatedPages,\n      ),\n    ],\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/lib/features/share_tab/share_tab_bloc_test.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/features/share_tab/data/repositories/local_share_with_user_repository_impl.dart';\nimport 'package:appflowy/features/share_tab/logic/share_tab_bloc.dart';\nimport 'package:bloc_test/bloc_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  const pageId = 'test_page_id';\n  const workspaceId = 'test_workspace_id';\n  late LocalShareWithUserRepositoryImpl repository;\n  late ShareTabBloc bloc;\n\n  setUp(() {\n    repository = LocalShareWithUserRepositoryImpl();\n    bloc = ShareTabBloc(\n      repository: repository,\n      pageId: pageId,\n      workspaceId: workspaceId,\n    );\n  });\n\n  tearDown(() async {\n    await bloc.close();\n  });\n\n  const email = 'lucas.xu@appflowy.io';\n\n  group('ShareTabBloc', () {\n    blocTest<ShareTabBloc, ShareTabState>(\n      'shares page with user',\n      build: () => bloc,\n      act: (bloc) => bloc.add(\n        ShareTabEvent.inviteUsers(\n          emails: [email],\n          accessLevel: ShareAccessLevel.readOnly,\n        ),\n      ),\n      wait: const Duration(milliseconds: 100),\n      expect: () => [\n        // First state: shareResult is null\n        isA<ShareTabState>().having(\n          (s) => s.shareResult,\n          'shareResult',\n          isNull,\n        ),\n        // Second state: shareResult is Success and users updated\n        isA<ShareTabState>()\n            .having((s) => s.shareResult, 'shareResult', isNotNull)\n            .having(\n              (s) => s.users.any((u) => u.email == email),\n              'users contains new user',\n              isTrue,\n            ),\n      ],\n    );\n\n    blocTest<ShareTabBloc, ShareTabState>(\n      'removes user from page',\n      build: () => bloc,\n      act: (bloc) => bloc.add(\n        ShareTabEvent.removeUsers(\n          emails: [email],\n        ),\n      ),\n      wait: const Duration(milliseconds: 100),\n      expect: () => [\n        // First state: removeResult is null\n        isA<ShareTabState>()\n            .having((s) => s.removeResult, 'removeResult', isNull),\n        // Second state: removeResult is Success and users updated\n        isA<ShareTabState>()\n            .having((s) => s.removeResult, 'removeResult', isNotNull)\n            .having(\n              (s) => s.users.any((u) => u.email == email),\n              'users contains removed user',\n              isFalse,\n            ),\n      ],\n    );\n\n    blocTest<ShareTabBloc, ShareTabState>(\n      'updates access level for user',\n      build: () => bloc,\n      act: (bloc) => bloc.add(\n        ShareTabEvent.updateUserAccessLevel(\n          email: email,\n          accessLevel: ShareAccessLevel.fullAccess,\n        ),\n      ),\n      wait: const Duration(milliseconds: 100),\n      expect: () => [\n        // First state: updateAccessLevelResult is null\n        isA<ShareTabState>().having(\n          (s) => s.updateAccessLevelResult,\n          'updateAccessLevelResult',\n          isNull,\n        ),\n        // Second state: updateAccessLevelResult is Success and users updated\n        isA<ShareTabState>()\n            .having(\n              (s) => s.updateAccessLevelResult,\n              'updateAccessLevelResult',\n              isNotNull,\n            )\n            .having(\n              (s) => s.users.firstWhere((u) => u.email == email).accessLevel,\n              'vivian accessLevel',\n              ShareAccessLevel.fullAccess,\n            ),\n      ],\n    );\n\n    final guestEmail = 'guest@appflowy.io';\n    blocTest<ShareTabBloc, ShareTabState>(\n      'turns user into member',\n      build: () => bloc,\n      act: (bloc) => bloc\n        ..add(\n          ShareTabEvent.inviteUsers(\n            emails: [guestEmail],\n            accessLevel: ShareAccessLevel.readOnly,\n          ),\n        )\n        ..add(\n          ShareTabEvent.convertToMember(\n            email: guestEmail,\n          ),\n        ),\n      wait: const Duration(milliseconds: 100),\n      expect: () => [\n        // First state: shareResult is null\n        isA<ShareTabState>().having(\n          (s) => s.shareResult,\n          'shareResult',\n          isNull,\n        ),\n        // Second state: shareResult is Success and users updated\n        isA<ShareTabState>()\n            .having(\n              (s) => s.shareResult,\n              'shareResult',\n              isNotNull,\n            )\n            .having(\n              (s) => s.users.any((u) => u.email == guestEmail),\n              'users contains guest@appflowy.io',\n              isTrue,\n            ),\n        // Third state: turnIntoMemberResult is Success and users updated\n        isA<ShareTabState>()\n            .having(\n              (s) => s.turnIntoMemberResult,\n              'turnIntoMemberResult',\n              isNotNull,\n            )\n            .having(\n              (s) => s.users.firstWhere((u) => u.email == guestEmail).role,\n              'guest@appflowy.io role',\n              ShareRole.member,\n            ),\n      ],\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/shortcuts_test/shortcuts_cubit_test.dart",
    "content": "import 'dart:ffi';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';\nimport 'package:bloc_test/bloc_test.dart';\nimport 'package:flutter_test/flutter_test.dart';\n// ignore: depend_on_referenced_packages\nimport 'package:mocktail/mocktail.dart';\n\nclass MockSettingsShortcutService extends Mock\n    implements SettingsShortcutService {}\n\nvoid main() {\n  group(\"ShortcutsCubit\", () {\n    late SettingsShortcutService service;\n    late ShortcutsCubit shortcutsCubit;\n\n    setUp(() async {\n      service = MockSettingsShortcutService();\n      when(\n        () => service.saveAllShortcuts(any()),\n      ).thenAnswer((_) async => true);\n      when(\n        () => service.getCustomizeShortcuts(),\n      ).thenAnswer((_) async => []);\n      when(\n        () => service.updateCommandShortcuts(any(), any()),\n      ).thenAnswer((_) async => Void);\n\n      shortcutsCubit = ShortcutsCubit(service);\n    });\n\n    test('initial state is correct', () {\n      final shortcutsCubit = ShortcutsCubit(service);\n      expect(shortcutsCubit.state, const ShortcutsState());\n    });\n\n    group('fetchShortcuts', () {\n      blocTest<ShortcutsCubit, ShortcutsState>(\n        'calls getCustomizeShortcuts() once',\n        build: () => shortcutsCubit,\n        act: (cubit) => cubit.fetchShortcuts(),\n        verify: (_) {\n          verify(() => service.getCustomizeShortcuts()).called(1);\n        },\n      );\n\n      blocTest<ShortcutsCubit, ShortcutsState>(\n        'emits [updating, failure] when getCustomizeShortcuts() throws',\n        setUp: () {\n          when(\n            () => service.getCustomizeShortcuts(),\n          ).thenThrow(Exception('oops'));\n        },\n        build: () => shortcutsCubit,\n        act: (cubit) => cubit.fetchShortcuts(),\n        expect: () => <dynamic>[\n          const ShortcutsState(status: ShortcutsStatus.updating),\n          isA<ShortcutsState>()\n              .having((w) => w.status, 'status', ShortcutsStatus.failure),\n        ],\n      );\n\n      blocTest<ShortcutsCubit, ShortcutsState>(\n        'emits [updating, success] when getCustomizeShortcuts() returns shortcuts',\n        build: () => shortcutsCubit,\n        act: (cubit) => cubit.fetchShortcuts(),\n        expect: () => <dynamic>[\n          const ShortcutsState(status: ShortcutsStatus.updating),\n          isA<ShortcutsState>()\n              .having((w) => w.status, 'status', ShortcutsStatus.success)\n              .having(\n                (w) => w.commandShortcutEvents,\n                'shortcuts',\n                commandShortcutEvents,\n              ),\n        ],\n      );\n    });\n\n    group('updateShortcut', () {\n      blocTest<ShortcutsCubit, ShortcutsState>(\n        'calls saveAllShortcuts() once',\n        build: () => shortcutsCubit,\n        act: (cubit) => cubit.updateAllShortcuts(),\n        verify: (_) {\n          verify(() => service.saveAllShortcuts(any())).called(1);\n        },\n      );\n\n      blocTest<ShortcutsCubit, ShortcutsState>(\n        'emits [updating, failure] when saveAllShortcuts() throws',\n        setUp: () {\n          when(\n            () => service.saveAllShortcuts(any()),\n          ).thenThrow(Exception('oops'));\n        },\n        build: () => shortcutsCubit,\n        act: (cubit) => cubit.updateAllShortcuts(),\n        expect: () => <dynamic>[\n          const ShortcutsState(status: ShortcutsStatus.updating),\n          isA<ShortcutsState>()\n              .having((w) => w.status, 'status', ShortcutsStatus.failure),\n        ],\n      );\n\n      blocTest<ShortcutsCubit, ShortcutsState>(\n        'emits [updating, success] when saveAllShortcuts() is successful',\n        build: () => shortcutsCubit,\n        act: (cubit) => cubit.updateAllShortcuts(),\n        expect: () => <dynamic>[\n          const ShortcutsState(status: ShortcutsStatus.updating),\n          isA<ShortcutsState>()\n              .having((w) => w.status, 'status', ShortcutsStatus.success),\n        ],\n      );\n    });\n\n    group('resetToDefault', () {\n      blocTest<ShortcutsCubit, ShortcutsState>(\n        'calls saveAllShortcuts() once',\n        build: () => shortcutsCubit,\n        act: (cubit) => cubit.resetToDefault(),\n        verify: (_) {\n          verify(() => service.saveAllShortcuts(any())).called(1);\n          verify(() => service.getCustomizeShortcuts()).called(1);\n        },\n      );\n\n      blocTest<ShortcutsCubit, ShortcutsState>(\n        'emits [updating, failure] when saveAllShortcuts() throws',\n        setUp: () {\n          when(\n            () => service.saveAllShortcuts(any()),\n          ).thenThrow(Exception('oops'));\n        },\n        build: () => shortcutsCubit,\n        act: (cubit) => cubit.resetToDefault(),\n        expect: () => <dynamic>[\n          const ShortcutsState(status: ShortcutsStatus.updating),\n          isA<ShortcutsState>()\n              .having((w) => w.status, 'status', ShortcutsStatus.failure),\n        ],\n      );\n\n      blocTest<ShortcutsCubit, ShortcutsState>(\n        'emits [updating, success] when getCustomizeShortcuts() returns shortcuts',\n        build: () => shortcutsCubit,\n        act: (cubit) => cubit.resetToDefault(),\n        expect: () => <dynamic>[\n          const ShortcutsState(status: ShortcutsStatus.updating),\n          isA<ShortcutsState>()\n              .having((w) => w.status, 'status', ShortcutsStatus.success)\n              .having(\n                (w) => w.commandShortcutEvents,\n                'shortcuts',\n                commandShortcutEvents,\n              ),\n        ],\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/view_selector_test.dart",
    "content": "import 'package:appflowy/ai/service/view_selector_cubit.dart';\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:bloc_test/bloc_test.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  ViewPB testView(\n    String id,\n    String name,\n    ViewLayoutPB layout, [\n    bool isSpace = false,\n    List<ViewPB> children = const [],\n  ]) {\n    return ViewPB()\n      ..id = id\n      ..name = name\n      ..layout = layout\n      ..isSpace = isSpace\n      ..childViews.addAll(children);\n  }\n\n  List<ViewPB> createTestViews() {\n    return [\n      testView('1', 'View 1', ViewLayoutPB.Document, true, [\n        testView('1-1', 'View 1-1', ViewLayoutPB.Document),\n        testView('1-2', 'View 1-2', ViewLayoutPB.Document),\n      ]),\n      testView('2', 'View 2', ViewLayoutPB.Document, false, [\n        testView('2-1', 'View 2-1', ViewLayoutPB.Document),\n        testView('2-2', 'View 2-2', ViewLayoutPB.Grid),\n        testView('2-3', 'View 2-3', ViewLayoutPB.Document, false, [\n          testView('2-3-1', 'View 2-3-1', ViewLayoutPB.Document),\n        ]),\n      ]),\n      testView('3', 'View 3', ViewLayoutPB.Document, true, [\n        testView('3-1', 'View 3-1', ViewLayoutPB.Grid, false, [\n          testView('3-1-1', 'View 3-1-1', ViewLayoutPB.Board),\n        ]),\n      ]),\n      testView('4', 'View 4', ViewLayoutPB.Document, true, [\n        testView('4-1', 'View 4-1', ViewLayoutPB.Chat),\n        testView('4-2', 'View 4-2', ViewLayoutPB.Document, false, [\n          testView('4-2-1', 'View 4-2-1', ViewLayoutPB.Document),\n          testView('4-2-2', 'View 4-2-2', ViewLayoutPB.Document),\n        ]),\n      ]),\n    ];\n  }\n\n  Map<String, ViewSelectedStatus> getSelectedStatus(\n    List<ViewSelectorItem> items,\n  ) {\n    return {\n      for (final item in items) item.view.id: item.selectedStatus,\n      for (final item in items)\n        if (item.children.isNotEmpty) ...getSelectedStatus(item.children),\n    };\n  }\n\n  group('ViewSelectorCubit test:', () {\n    blocTest<ViewSelectorCubit, ViewSelectorState>(\n      'initial state',\n      build: () => ViewSelectorCubit(\n        getIgnoreViewType: (_) => IgnoreViewType.none,\n        maxSelectedParentPageCount: 1,\n      ),\n      act: (_) {},\n      verify: (cubit) {\n        final s = cubit.state;\n        expect(s.visibleSources, isEmpty);\n        expect(s.selectedSources, isEmpty);\n      },\n    );\n\n    blocTest<ViewSelectorCubit, ViewSelectorState>(\n      'update sources',\n      build: () => ViewSelectorCubit(\n        getIgnoreViewType: (_) => IgnoreViewType.none,\n        maxSelectedParentPageCount: 1,\n      ),\n      act: (cubit) async {\n        final views = createTestViews();\n        await cubit.refreshSources(views, views.first);\n      },\n      verify: (cubit) {\n        final s = cubit.state;\n        expect(s.visibleSources.length, 4);\n        expect(s.visibleSources[0].isExpanded, isTrue);\n        expect(s.visibleSources[0].children.length, 2);\n        expect(s.visibleSources[1].children.length, 3);\n        expect(s.visibleSources[2].children.length, 1);\n        expect(s.visibleSources[3].children.length, 2);\n        expect(s.selectedSources.isEmpty, isTrue);\n      },\n    );\n\n    blocTest<ViewSelectorCubit, ViewSelectorState>(\n      'update sources multiple times',\n      build: () => ViewSelectorCubit(\n        getIgnoreViewType: (_) => IgnoreViewType.none,\n        maxSelectedParentPageCount: 1,\n      ),\n      act: (cubit) async {\n        final views = createTestViews();\n        await cubit.refreshSources([], null);\n        await cubit.refreshSources(views, null);\n        await cubit.refreshSources([], null);\n      },\n      expect: () => [\n        predicate<ViewSelectorState>(\n          (s) => s.visibleSources.isEmpty && s.selectedSources.isEmpty,\n        ),\n        predicate<ViewSelectorState>(\n          (s) => s.visibleSources.isNotEmpty && s.selectedSources.isEmpty,\n        ),\n        predicate<ViewSelectorState>(\n          (s) => s.visibleSources.isEmpty && s.selectedSources.isEmpty,\n        ),\n      ],\n    );\n\n    blocTest<ViewSelectorCubit, ViewSelectorState>(\n      'update selected sources',\n      build: () => ViewSelectorCubit(\n        getIgnoreViewType: (_) => IgnoreViewType.none,\n        maxSelectedParentPageCount: 100,\n      ),\n      act: (cubit) async {\n        final views = createTestViews();\n        cubit.updateSelectedSources([\n          '2-3-1',\n          '3-1',\n          '3-1-1',\n          '4-2',\n          '4-2-1',\n        ]);\n        await cubit.refreshSources(views, null);\n      },\n      skip: 1,\n      expect: () => [\n        predicate<ViewSelectorState>((s) {\n          final lengthCheck =\n              s.visibleSources.length == 4 && s.selectedSources.length == 3;\n\n          final expected = {\n            '1': ViewSelectedStatus.unselected,\n            '1-1': ViewSelectedStatus.unselected,\n            '1-2': ViewSelectedStatus.unselected,\n            '2': ViewSelectedStatus.partiallySelected,\n            '2-1': ViewSelectedStatus.unselected,\n            '2-2': ViewSelectedStatus.unselected,\n            '2-3': ViewSelectedStatus.partiallySelected,\n            '2-3-1': ViewSelectedStatus.selected,\n            '3': ViewSelectedStatus.partiallySelected,\n            '3-1': ViewSelectedStatus.selected,\n            '3-1-1': ViewSelectedStatus.selected,\n            '4': ViewSelectedStatus.partiallySelected,\n            '4-1': ViewSelectedStatus.unselected,\n            '4-2': ViewSelectedStatus.partiallySelected,\n            '4-2-1': ViewSelectedStatus.selected,\n            '4-2-2': ViewSelectedStatus.unselected,\n          };\n\n          final actual = getSelectedStatus(s.visibleSources);\n\n          return lengthCheck && mapEquals(expected, actual);\n        }),\n      ],\n    );\n\n    blocTest<ViewSelectorCubit, ViewSelectorState>(\n      'select a source 1',\n      build: () => ViewSelectorCubit(\n        getIgnoreViewType: (_) => IgnoreViewType.none,\n        maxSelectedParentPageCount: 100,\n      ),\n      act: (cubit) async {\n        final views = createTestViews();\n        await cubit.refreshSources(views, null);\n        cubit.toggleSelectedStatus(\n          cubit.state.visibleSources[1].children[2].children[0], // '2-3-1',\n          false,\n        );\n      },\n      skip: 1,\n      expect: () => [\n        predicate<ViewSelectorState>((s) {\n          return getSelectedStatus(s.visibleSources)\n              .values\n              .every((value) => value == ViewSelectedStatus.unselected);\n        }),\n        predicate<ViewSelectorState>((s) {\n          final selectedStatusMap = getSelectedStatus(s.visibleSources);\n          return selectedStatusMap['2-3'] ==\n                  ViewSelectedStatus.partiallySelected &&\n              selectedStatusMap['2-3-1'] == ViewSelectedStatus.selected;\n        }),\n      ],\n    );\n\n    blocTest<ViewSelectorCubit, ViewSelectorState>(\n      'select a source 2',\n      build: () => ViewSelectorCubit(\n        getIgnoreViewType: (_) => IgnoreViewType.none,\n        maxSelectedParentPageCount: 100,\n      ),\n      act: (cubit) async {\n        final views = createTestViews();\n        await cubit.refreshSources(views, null);\n        cubit.toggleSelectedStatus(\n          cubit.state.visibleSources[1].children[2], // '2-3',\n          false,\n        );\n        cubit.toggleSelectedStatus(\n          cubit.state.visibleSources[1].children[2].children[0], // '2-3-1',\n          false,\n        );\n      },\n      skip: 2,\n      expect: () => [\n        predicate<ViewSelectorState>((s) {\n          final selectedStatusMap = getSelectedStatus(s.visibleSources);\n          return selectedStatusMap['2-3'] == ViewSelectedStatus.selected &&\n              selectedStatusMap['2-3-1'] == ViewSelectedStatus.selected;\n        }),\n        predicate<ViewSelectorState>((s) {\n          final selectedStatusMap = getSelectedStatus(s.visibleSources);\n          return selectedStatusMap['2-3'] ==\n                  ViewSelectedStatus.partiallySelected &&\n              selectedStatusMap['2-3-1'] == ViewSelectedStatus.unselected;\n        }),\n      ],\n    );\n\n    blocTest<ViewSelectorCubit, ViewSelectorState>(\n      'select a source 3',\n      build: () => ViewSelectorCubit(\n        getIgnoreViewType: (_) => IgnoreViewType.none,\n        maxSelectedParentPageCount: 100,\n      ),\n      act: (cubit) async {\n        final views = createTestViews();\n        cubit.updateSelectedSources(['2-3', '2-3-1']);\n        await cubit.refreshSources(views, null);\n        cubit.toggleSelectedStatus(\n          cubit.state.visibleSources[1].children[2], // '2-3',\n          false,\n        );\n      },\n      skip: 1,\n      expect: () => [\n        predicate<ViewSelectorState>((s) {\n          final selectedStatusMap = getSelectedStatus(s.visibleSources);\n          return selectedStatusMap['2-3'] == ViewSelectedStatus.selected &&\n              selectedStatusMap['2-3-1'] == ViewSelectedStatus.selected;\n        }),\n        predicate<ViewSelectorState>((s) {\n          final selectedStatusMap = getSelectedStatus(s.visibleSources);\n          return selectedStatusMap['2-3'] == ViewSelectedStatus.unselected &&\n              selectedStatusMap['2-3-1'] == ViewSelectedStatus.unselected;\n        }),\n      ],\n    );\n\n    blocTest<ViewSelectorCubit, ViewSelectorState>(\n      'select a source 4',\n      build: () => ViewSelectorCubit(\n        getIgnoreViewType: (_) => IgnoreViewType.none,\n        maxSelectedParentPageCount: 100,\n      ),\n      act: (cubit) async {\n        final views = createTestViews();\n        cubit.updateSelectedSources(['4-2', '4-2-1']);\n        await cubit.refreshSources(views, null);\n        cubit.toggleSelectedStatus(\n          cubit.state.visibleSources[3].children[1].children[1], // 4-2-2,\n          false,\n        );\n      },\n      skip: 1,\n      expect: () => [\n        predicate<ViewSelectorState>((s) {\n          final selectedStatusMap = getSelectedStatus(s.visibleSources);\n          return selectedStatusMap['4-2'] ==\n                  ViewSelectedStatus.partiallySelected &&\n              selectedStatusMap['4-2-1'] == ViewSelectedStatus.selected &&\n              selectedStatusMap['4-2-2'] == ViewSelectedStatus.unselected;\n        }),\n        predicate<ViewSelectorState>((s) {\n          final selectedStatusMap = getSelectedStatus(s.visibleSources);\n          return selectedStatusMap['4-2'] == ViewSelectedStatus.selected &&\n              selectedStatusMap['4-2-1'] == ViewSelectedStatus.selected &&\n              selectedStatusMap['4-2-2'] == ViewSelectedStatus.selected;\n        }),\n      ],\n    );\n\n    blocTest<ViewSelectorCubit, ViewSelectorState>(\n      'select a source 5',\n      build: () => ViewSelectorCubit(\n        getIgnoreViewType: (_) => IgnoreViewType.none,\n        maxSelectedParentPageCount: 100,\n      ),\n      act: (cubit) async {\n        final views = createTestViews();\n        cubit.updateSelectedSources(['4-2', '4-2-1']);\n        await cubit.refreshSources(views, null);\n        cubit.toggleSelectedStatus(\n          cubit.state.visibleSources[3].children[1], // 4-2\n          false,\n        );\n      },\n      verify: (cubit) {\n        final selectedStatusMap = getSelectedStatus(cubit.state.visibleSources);\n        expect(selectedStatusMap['4-2'], ViewSelectedStatus.unselected);\n        expect(selectedStatusMap['4-2-1'], ViewSelectedStatus.unselected);\n        expect(selectedStatusMap['4-2-2'], ViewSelectedStatus.unselected);\n      },\n    );\n\n    blocTest<ViewSelectorCubit, ViewSelectorState>(\n      'cannot select more than maximum selection limit',\n      build: () => ViewSelectorCubit(\n        getIgnoreViewType: (_) => IgnoreViewType.none,\n        maxSelectedParentPageCount: 2,\n      ),\n      act: (cubit) async {\n        final views = createTestViews();\n        cubit.updateSelectedSources(['1-1', '2-1']);\n        await cubit.refreshSources(views, null);\n      },\n      verify: (cubit) {\n        final s = cubit.state;\n        expect(s.visibleSources[0].children[0].isDisabled, isFalse);\n        expect(s.visibleSources[0].children[1].isDisabled, isFalse);\n        expect(s.visibleSources[1].children[0].isDisabled, isFalse);\n        expect(s.visibleSources[1].children[1].isDisabled, isFalse);\n        expect(s.visibleSources[2].children[0].isDisabled, isTrue);\n      },\n    );\n\n    blocTest<ViewSelectorCubit, ViewSelectorState>(\n      'filter sources correctly',\n      build: () => ViewSelectorCubit(\n        getIgnoreViewType: (_) => IgnoreViewType.none,\n        maxSelectedParentPageCount: 1,\n      ),\n      act: (cubit) async {\n        final views = createTestViews();\n        await cubit.refreshSources(views, null);\n        cubit.filterTextController.text = 'View 1';\n      },\n      verify: (cubit) {\n        final s = cubit.state;\n        expect(s.visibleSources.length, 1);\n        expect(s.visibleSources[0].children.length, 2);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/bloc_test/workspace_test/workspace_bloc_test.dart",
    "content": "import 'package:appflowy/features/workspace/data/repositories/workspace_repository.dart';\nimport 'package:appflowy/features/workspace/logic/workspace_bloc.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/reminder/reminder_bloc.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';\nimport 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:appflowy_result/appflowy_result.dart';\nimport 'package:bloc_test/bloc_test.dart';\nimport 'package:fixnum/fixnum.dart' as fixnum;\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass MockWorkspaceRepository extends Mock implements WorkspaceRepository {}\n\nclass MockReminderBloc extends Mock implements ReminderBloc {}\n\nclass FakeWorkspaceTypePB extends Fake implements WorkspaceTypePB {}\n\nclass FakeReminderEvent extends Fake implements ReminderEvent {}\n\nFuture<bool> mockIsBillingEnabled() async => false;\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  setUpAll(() {\n    registerFallbackValue(FakeWorkspaceTypePB());\n    registerFallbackValue(FakeReminderEvent());\n  });\n\n  group('UserWorkspaceBloc', () {\n    late MockWorkspaceRepository mockRepository;\n    late MockReminderBloc mockReminderBloc;\n    late UserProfilePB userProfile;\n    UserWorkspaceBloc? bloc;\n\n    setUp(() {\n      mockRepository = MockWorkspaceRepository();\n      mockReminderBloc = MockReminderBloc();\n      userProfile = UserProfilePB()\n        ..id = fixnum.Int64(123)\n        ..name = 'Test User'\n        ..email = 'test@example.com'\n        ..userAuthType = AuthTypePB.Local;\n\n      getIt.registerLazySingleton<ReminderBloc>(() => mockReminderBloc);\n\n      when(() => mockReminderBloc.add(any())).thenReturn(null);\n\n      when(() => mockRepository.getCurrentWorkspace()).thenAnswer(\n        (_) async => FlowyResult.failure(\n          FlowyError()..code = ErrorCode.Internal,\n        ),\n      );\n      when(() => mockRepository.getWorkspaces()).thenAnswer(\n        (_) async => FlowyResult.success(<UserWorkspacePB>[]),\n      );\n      when(\n        () => mockRepository.openWorkspace(\n          workspaceId: any(named: 'workspaceId'),\n          workspaceType: any(named: 'workspaceType'),\n        ),\n      ).thenAnswer(\n        (_) async => FlowyResult.success(null),\n      );\n      when(\n        () => mockRepository.getWorkspaceSubscriptionInfo(\n          workspaceId: any(named: 'workspaceId'),\n        ),\n      ).thenAnswer(\n        (_) async => FlowyResult.success(WorkspaceSubscriptionInfoPB()),\n      );\n    });\n\n    tearDown(() {\n      if (bloc != null && !bloc!.isClosed) {\n        bloc!.close();\n      }\n\n      if (getIt.isRegistered<ReminderBloc>()) {\n        getIt.unregister<ReminderBloc>();\n      }\n    });\n\n    UserWorkspacePB createTestWorkspace({\n      required String id,\n      required String name,\n      String icon = '',\n      WorkspaceTypePB workspaceType = WorkspaceTypePB.LocalW,\n      int createdAt = 1000,\n    }) {\n      return UserWorkspacePB()\n        ..workspaceId = id\n        ..name = name\n        ..icon = icon\n        ..workspaceType = workspaceType\n        ..createdAtTimestamp = fixnum.Int64(createdAt);\n    }\n\n    WorkspacePB createCurrentWorkspace({\n      required String id,\n      required String name,\n      int createdAt = 1000,\n    }) {\n      return WorkspacePB()\n        ..id = id\n        ..name = name\n        ..createTime = fixnum.Int64(createdAt);\n    }\n\n    group('initial state', () {\n      test('should have correct initial state', () {\n        bloc = UserWorkspaceBloc(\n          repository: mockRepository,\n          userProfile: userProfile,\n        );\n\n        expect(bloc!.state.userProfile, equals(userProfile));\n        expect(bloc!.state.workspaces, isEmpty);\n        expect(bloc!.state.currentWorkspace, isNull);\n        expect(bloc!.state.actionResult, isNull);\n        expect(bloc!.state.isCollabWorkspaceOn, isFalse);\n        expect(bloc!.state.workspaceSubscriptionInfo, isNull);\n      });\n    });\n\n    group('fetchWorkspaces', () {\n      blocTest<UserWorkspaceBloc, UserWorkspaceState>(\n        'should fetch workspaces successfully when current workspace exists in list',\n        setUp: () {\n          final currentWorkspace = createCurrentWorkspace(\n            id: 'workspace-1',\n            name: 'Workspace 1',\n          );\n          final workspaces = [\n            createTestWorkspace(id: 'workspace-1', name: 'Workspace 1'),\n            createTestWorkspace(id: 'workspace-2', name: 'Workspace 2'),\n          ];\n\n          when(() => mockRepository.getCurrentWorkspace()).thenAnswer(\n            (_) async => FlowyResult.success(currentWorkspace),\n          );\n          when(() => mockRepository.getWorkspaces()).thenAnswer(\n            (_) async => FlowyResult.success(workspaces),\n          );\n          when(() => mockRepository.isBillingEnabled()).thenAnswer(\n            (_) async => true,\n          );\n        },\n        build: () => UserWorkspaceBloc(\n          repository: mockRepository,\n          userProfile: userProfile,\n        ),\n        act: (bloc) => bloc.add(UserWorkspaceEvent.fetchWorkspaces()),\n        expect: () => [\n          // First: workspaces are loaded\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.workspaces.length == 2 && state.currentWorkspace == null,\n          ),\n          // Second: opening workspace action starts\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.workspaces.length == 2 &&\n                state.actionResult?.actionType == WorkspaceActionType.open &&\n                state.actionResult?.isLoading == true,\n          ),\n          // Third: opening workspace action completes and currentWorkspace is set\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.workspaces.length == 2 &&\n                state.currentWorkspace != null &&\n                state.currentWorkspace?.workspaceId == 'workspace-1' &&\n                state.actionResult?.isLoading == false,\n          ),\n          // Fourth: subscription info is fetched\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.workspaces.length == 2 &&\n                state.currentWorkspace?.workspaceId == 'workspace-1' &&\n                state.workspaceSubscriptionInfo != null,\n          ),\n        ],\n        verify: (bloc) {\n          expect(bloc.state.workspaces.length, equals(2));\n          expect(\n            bloc.state.workspaces.first.workspaceId,\n            equals('workspace-1'),\n          );\n          expect(bloc.state.workspaces.last.workspaceId, equals('workspace-2'));\n          expect(\n            bloc.state.currentWorkspace?.workspaceId,\n            equals('workspace-1'),\n          );\n        },\n      );\n\n      blocTest<UserWorkspaceBloc, UserWorkspaceState>(\n        'should handle error when fetching current workspace fails but workspaces succeed',\n        setUp: () {\n          final workspaces = [\n            createTestWorkspace(id: 'workspace-1', name: 'Workspace 1'),\n            createTestWorkspace(id: 'workspace-2', name: 'Workspace 2'),\n          ];\n\n          when(() => mockRepository.getCurrentWorkspace()).thenAnswer(\n            (_) async => FlowyResult.failure(\n              FlowyError()..code = ErrorCode.Internal,\n            ),\n          );\n          when(() => mockRepository.getWorkspaces()).thenAnswer(\n            (_) async => FlowyResult.success(workspaces),\n          );\n          when(() => mockRepository.isBillingEnabled()).thenAnswer(\n            (_) async => true,\n          );\n        },\n        build: () => UserWorkspaceBloc(\n          repository: mockRepository,\n          userProfile: userProfile,\n        ),\n        act: (bloc) => bloc.add(UserWorkspaceEvent.fetchWorkspaces()),\n        expect: () => [\n          // First: workspaces are loaded, first workspace becomes current\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.workspaces.length == 2 && state.currentWorkspace == null,\n          ),\n          // Second: opening workspace action starts\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.workspaces.length == 2 &&\n                state.actionResult?.actionType == WorkspaceActionType.open &&\n                state.actionResult?.isLoading == true,\n          ),\n          // Third: opening workspace action completes and currentWorkspace is set\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.workspaces.length == 2 &&\n                state.currentWorkspace != null &&\n                state.currentWorkspace?.workspaceId == 'workspace-1' &&\n                state.actionResult?.isLoading == false,\n          ),\n          // Fourth: subscription info is fetched\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.workspaces.length == 2 &&\n                state.currentWorkspace?.workspaceId == 'workspace-1' &&\n                state.workspaceSubscriptionInfo != null,\n          ),\n        ],\n        verify: (bloc) {\n          expect(bloc.state.workspaces.length, equals(2));\n          expect(\n            bloc.state.currentWorkspace?.workspaceId,\n            equals('workspace-1'),\n          );\n        },\n      );\n\n      blocTest<UserWorkspaceBloc, UserWorkspaceState>(\n        'should handle error when fetching workspaces fails',\n        setUp: () {\n          when(() => mockRepository.getCurrentWorkspace()).thenAnswer(\n            (_) async => FlowyResult.failure(\n              FlowyError()..code = ErrorCode.Internal,\n            ),\n          );\n          when(() => mockRepository.getWorkspaces()).thenAnswer(\n            (_) async => FlowyResult.failure(\n              FlowyError()..code = ErrorCode.Internal,\n            ),\n          );\n        },\n        build: () => UserWorkspaceBloc(\n          repository: mockRepository,\n          userProfile: userProfile,\n        ),\n        act: (bloc) => bloc.add(UserWorkspaceEvent.fetchWorkspaces()),\n        verify: (bloc) {\n          expect(bloc.state.workspaces, isEmpty);\n          expect(bloc.state.currentWorkspace, isNull);\n\n          verifyNever(() => mockReminderBloc.add(any()));\n        },\n      );\n    });\n\n    group('createWorkspace', () {\n      blocTest<UserWorkspaceBloc, UserWorkspaceState>(\n        'should create workspace successfully',\n        setUp: () {\n          final newWorkspace = createTestWorkspace(\n            id: 'new-workspace',\n            name: 'New Workspace',\n          );\n\n          when(\n            () => mockRepository.createWorkspace(\n              name: 'New Workspace',\n              workspaceType: WorkspaceTypePB.LocalW,\n            ),\n          ).thenAnswer(\n            (_) async => FlowyResult.success(newWorkspace),\n          );\n\n          when(() => mockRepository.isBillingEnabled()).thenAnswer(\n            (_) async => true,\n          );\n        },\n        build: () => UserWorkspaceBloc(\n          repository: mockRepository,\n          userProfile: userProfile,\n        ),\n        act: (bloc) => bloc.add(\n          UserWorkspaceEvent.createWorkspace(\n            name: 'New Workspace',\n            workspaceType: WorkspaceTypePB.LocalW,\n          ),\n        ),\n        expect: () => [\n          // First: create workspace action starts\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.actionResult?.actionType == WorkspaceActionType.create &&\n                state.actionResult?.isLoading == true,\n          ),\n          // Second: create workspace action completes, workspace is added\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.actionResult?.actionType == WorkspaceActionType.create &&\n                state.actionResult?.isLoading == false &&\n                state.actionResult?.result?.isSuccess == true &&\n                state.workspaces.isNotEmpty,\n          ),\n          // Third: opening workspace action starts\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.actionResult?.actionType == WorkspaceActionType.open &&\n                state.actionResult?.isLoading == true &&\n                state.workspaces.isNotEmpty,\n          ),\n          // Fourth: opening workspace action completes, currentWorkspace is set\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.actionResult?.actionType == WorkspaceActionType.open &&\n                state.actionResult?.isLoading == false &&\n                state.currentWorkspace != null &&\n                state.currentWorkspace?.workspaceId == 'new-workspace',\n          ),\n          // Fifth: subscription info is fetched\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.currentWorkspace?.workspaceId == 'new-workspace' &&\n                state.workspaceSubscriptionInfo != null,\n          ),\n        ],\n        verify: (bloc) {\n          expect(\n            bloc.state.workspaces.any((w) => w.workspaceId == 'new-workspace'),\n            isTrue,\n          );\n        },\n      );\n\n      blocTest<UserWorkspaceBloc, UserWorkspaceState>(\n        'should handle error when creating workspace fails',\n        setUp: () {\n          when(\n            () => mockRepository.createWorkspace(\n              name: any(named: 'name'),\n              workspaceType: any(named: 'workspaceType'),\n            ),\n          ).thenAnswer(\n            (_) async => FlowyResult.failure(\n              FlowyError()..code = ErrorCode.Internal,\n            ),\n          );\n        },\n        build: () => UserWorkspaceBloc(\n          repository: mockRepository,\n          userProfile: userProfile,\n        ),\n        act: (bloc) => bloc.add(\n          UserWorkspaceEvent.createWorkspace(\n            name: 'New Workspace',\n            workspaceType: WorkspaceTypePB.LocalW,\n          ),\n        ),\n        expect: () => [\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.actionResult?.actionType == WorkspaceActionType.create &&\n                state.actionResult?.isLoading == true,\n          ),\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.actionResult?.actionType == WorkspaceActionType.create &&\n                state.actionResult?.isLoading == false &&\n                state.actionResult?.result?.isFailure == true,\n          ),\n        ],\n        verify: (bloc) {\n          verifyNever(() => mockReminderBloc.add(any()));\n        },\n      );\n    });\n\n    group('deleteWorkspace', () {\n      blocTest<UserWorkspaceBloc, UserWorkspaceState>(\n        'should prevent deleting the only workspace',\n        setUp: () {\n          when(() => mockRepository.getCurrentWorkspace()).thenAnswer(\n            (_) async => FlowyResult.success(\n              createCurrentWorkspace(id: 'workspace-1', name: 'Workspace 1'),\n            ),\n          );\n\n          when(() => mockRepository.getWorkspaces()).thenAnswer(\n            (_) async => FlowyResult.success([\n              createTestWorkspace(id: 'workspace-1', name: 'Workspace 1'),\n            ]),\n          );\n        },\n        build: () {\n          final bloc = UserWorkspaceBloc(\n            repository: mockRepository,\n            userProfile: userProfile,\n          );\n\n          bloc.emit(\n            bloc.state.copyWith(\n              workspaces: [\n                createTestWorkspace(id: 'workspace-1', name: 'Workspace 1'),\n              ],\n            ),\n          );\n          return bloc;\n        },\n        act: (bloc) => bloc.add(\n          UserWorkspaceEvent.deleteWorkspace(workspaceId: 'workspace-1'),\n        ),\n        expect: () => [\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.actionResult?.actionType == WorkspaceActionType.delete &&\n                state.actionResult?.isLoading == true,\n          ),\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.actionResult?.actionType == WorkspaceActionType.delete &&\n                state.actionResult?.isLoading == false &&\n                state.actionResult?.result?.isFailure == true,\n          ),\n        ],\n      );\n\n      blocTest<UserWorkspaceBloc, UserWorkspaceState>(\n        'should delete workspace successfully when more than one workspace exists',\n        setUp: () {\n          // Create a sequence of responses for getWorkspaces calls\n          var callCount = 0;\n          when(\n            () => mockRepository.deleteWorkspace(\n              workspaceId: any(named: 'workspaceId'),\n            ),\n          ).thenAnswer(\n            (_) async => FlowyResult.success(null),\n          );\n\n          when(() => mockRepository.getCurrentWorkspace()).thenAnswer(\n            (_) async => FlowyResult.success(\n              createCurrentWorkspace(id: 'workspace-1', name: 'Workspace 1'),\n            ),\n          );\n\n          // Return 2 workspaces on first call (for deletion validation)\n          // Return 1 workspace on second call (after deletion)\n          when(() => mockRepository.getWorkspaces()).thenAnswer(\n            (_) async {\n              callCount++;\n              if (callCount == 1) {\n                return FlowyResult.success([\n                  createTestWorkspace(id: 'workspace-1', name: 'Workspace 1'),\n                  createTestWorkspace(id: 'workspace-2', name: 'Workspace 2'),\n                ]);\n              } else {\n                return FlowyResult.success([\n                  createTestWorkspace(id: 'workspace-1', name: 'Workspace 1'),\n                ]);\n              }\n            },\n          );\n\n          when(() => mockRepository.isBillingEnabled()).thenAnswer(\n            (_) async => true,\n          );\n        },\n        build: () {\n          final bloc = UserWorkspaceBloc(\n            repository: mockRepository,\n            userProfile: userProfile,\n          );\n\n          bloc.emit(\n            bloc.state.copyWith(\n              workspaces: [\n                createTestWorkspace(id: 'workspace-1', name: 'Workspace 1'),\n                createTestWorkspace(id: 'workspace-2', name: 'Workspace 2'),\n              ],\n              currentWorkspace: createTestWorkspace(\n                id: 'workspace-1',\n                name: 'Workspace 1',\n              ),\n            ),\n          );\n          return bloc;\n        },\n        act: (bloc) => bloc.add(\n          UserWorkspaceEvent.deleteWorkspace(workspaceId: 'workspace-2'),\n        ),\n        expect: () => [\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.actionResult?.actionType == WorkspaceActionType.delete &&\n                state.actionResult?.isLoading == true,\n          ),\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.actionResult?.actionType == WorkspaceActionType.delete &&\n                state.actionResult?.isLoading == false &&\n                state.actionResult?.result?.isSuccess == true,\n          ),\n        ],\n        verify: (bloc) {\n          expect(bloc.state.workspaces.length, equals(1));\n          expect(\n            bloc.state.workspaces.any((w) => w.workspaceId == 'workspace-2'),\n            isFalse,\n          );\n        },\n      );\n    });\n\n    group('renameWorkspace', () {\n      blocTest<UserWorkspaceBloc, UserWorkspaceState>(\n        'should rename workspace successfully',\n        setUp: () {\n          when(\n            () => mockRepository.renameWorkspace(\n              workspaceId: 'workspace-1',\n              name: 'Renamed Workspace',\n            ),\n          ).thenAnswer(\n            (_) async => FlowyResult.success(null),\n          );\n        },\n        build: () {\n          final bloc = UserWorkspaceBloc(\n            repository: mockRepository,\n            userProfile: userProfile,\n          );\n\n          bloc.emit(\n            bloc.state.copyWith(\n              workspaces: [\n                createTestWorkspace(id: 'workspace-1', name: 'Original Name'),\n              ],\n              currentWorkspace: createTestWorkspace(\n                id: 'workspace-1',\n                name: 'Original Name',\n              ),\n            ),\n          );\n          return bloc;\n        },\n        act: (bloc) => bloc.add(\n          UserWorkspaceEvent.renameWorkspace(\n            workspaceId: 'workspace-1',\n            name: 'Renamed Workspace',\n          ),\n        ),\n        expect: () => [\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.actionResult?.actionType == WorkspaceActionType.rename &&\n                state.actionResult?.isLoading == false &&\n                state.workspaces.first.name == 'Renamed Workspace' &&\n                state.currentWorkspace?.name == 'Renamed Workspace',\n          ),\n        ],\n      );\n\n      blocTest<UserWorkspaceBloc, UserWorkspaceState>(\n        'should handle error when renaming workspace fails',\n        setUp: () {\n          when(\n            () => mockRepository.renameWorkspace(\n              workspaceId: 'workspace-1',\n              name: 'Renamed Workspace',\n            ),\n          ).thenAnswer(\n            (_) async => FlowyResult.failure(\n              FlowyError()..code = ErrorCode.Internal,\n            ),\n          );\n        },\n        build: () {\n          final bloc = UserWorkspaceBloc(\n            repository: mockRepository,\n            userProfile: userProfile,\n          );\n\n          bloc.emit(\n            bloc.state.copyWith(\n              workspaces: [\n                createTestWorkspace(id: 'workspace-1', name: 'Original Name'),\n              ],\n            ),\n          );\n          return bloc;\n        },\n        act: (bloc) => bloc.add(\n          UserWorkspaceEvent.renameWorkspace(\n            workspaceId: 'workspace-1',\n            name: 'Renamed Workspace',\n          ),\n        ),\n        expect: () => [\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.actionResult?.actionType == WorkspaceActionType.rename &&\n                state.actionResult?.isLoading == false &&\n                state.actionResult?.result?.isFailure == true &&\n                state.workspaces.first.name == 'Original Name',\n          ),\n        ],\n      );\n    });\n\n    group('updateWorkspaceIcon', () {\n      blocTest<UserWorkspaceBloc, UserWorkspaceState>(\n        'should update workspace icon successfully',\n        setUp: () {\n          when(\n            () => mockRepository.updateWorkspaceIcon(\n              workspaceId: 'workspace-1',\n              icon: '🚀',\n            ),\n          ).thenAnswer(\n            (_) async => FlowyResult.success(null),\n          );\n        },\n        build: () {\n          final bloc = UserWorkspaceBloc(\n            repository: mockRepository,\n            userProfile: userProfile,\n          );\n\n          bloc.emit(\n            bloc.state.copyWith(\n              workspaces: [\n                createTestWorkspace(id: 'workspace-1', name: 'Workspace 1'),\n              ],\n              currentWorkspace: createTestWorkspace(\n                id: 'workspace-1',\n                name: 'Workspace 1',\n              ),\n            ),\n          );\n          return bloc;\n        },\n        act: (bloc) => bloc.add(\n          UserWorkspaceEvent.updateWorkspaceIcon(\n            workspaceId: 'workspace-1',\n            icon: '🚀',\n          ),\n        ),\n        expect: () => [\n          predicate<UserWorkspaceState>(\n            (state) =>\n                state.actionResult?.actionType ==\n                    WorkspaceActionType.updateIcon &&\n                state.actionResult?.isLoading == false &&\n                state.workspaces.first.icon == '🚀' &&\n                state.currentWorkspace?.icon == '🚀',\n          ),\n        ],\n      );\n\n      blocTest<UserWorkspaceBloc, UserWorkspaceState>(\n        'should ignore updating to same icon',\n        build: () {\n          final bloc = UserWorkspaceBloc(\n            repository: mockRepository,\n            userProfile: userProfile,\n          );\n\n          bloc.emit(\n            bloc.state.copyWith(\n              workspaces: [\n                createTestWorkspace(\n                  id: 'workspace-1',\n                  name: 'Workspace 1',\n                  icon: '🚀',\n                ),\n              ],\n            ),\n          );\n          return bloc;\n        },\n        act: (bloc) => bloc.add(\n          UserWorkspaceEvent.updateWorkspaceIcon(\n            workspaceId: 'workspace-1',\n            icon: '🚀',\n          ),\n        ),\n        expect: () => [],\n      );\n    });\n\n    group('updateWorkspaceSubscriptionInfo', () {\n      blocTest<UserWorkspaceBloc, UserWorkspaceState>(\n        'should update subscription info',\n        build: () => UserWorkspaceBloc(\n          repository: mockRepository,\n          userProfile: userProfile,\n        ),\n        act: (bloc) {\n          final subscriptionInfo = WorkspaceSubscriptionInfoPB();\n\n          bloc.add(\n            UserWorkspaceEvent.updateWorkspaceSubscriptionInfo(\n              workspaceId: 'workspace-1',\n              subscriptionInfo: subscriptionInfo,\n            ),\n          );\n        },\n        expect: () => [\n          predicate<UserWorkspaceState>(\n            (state) => state.workspaceSubscriptionInfo != null,\n          ),\n        ],\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/algorithm/levenshtein_test.dart",
    "content": "import 'package:appflowy/util/levenshtein.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  test('Levenshtein distance between identical strings', () {\n    final distance = levenshtein('abc', 'abc');\n    expect(distance, 0);\n  });\n\n  test('Levenshtein distance between strings of different lengths', () {\n    final distance = levenshtein('kitten', 'sitting');\n    expect(distance, 3);\n  });\n\n  test('Levenshtein distance between case-insensitive strings', () {\n    final distance = levenshtein('Hello', 'hello', caseSensitive: false);\n    expect(distance, 0);\n  });\n\n  test('Levenshtein distance between strings with substitutions', () {\n    final distance = levenshtein('kitten', 'smtten');\n    expect(distance, 2);\n  });\n\n  test('Levenshtein distance between strings with deletions', () {\n    final distance = levenshtein('kitten', 'kiten');\n    expect(distance, 1);\n  });\n\n  test('Levenshtein distance between strings with insertions', () {\n    final distance = levenshtein('kitten', 'kitxten');\n    expect(distance, 1);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/deeplink/deeplink_test.dart",
    "content": "import 'package:appflowy/startup/tasks/deeplink/deeplink_handler.dart';\nimport 'package:appflowy/startup/tasks/deeplink/invitation_deeplink_handler.dart';\nimport 'package:appflowy/startup/tasks/deeplink/login_deeplink_handler.dart';\nimport 'package:appflowy/startup/tasks/deeplink/open_app_deeplink_handler.dart';\nimport 'package:appflowy/startup/tasks/deeplink/payment_deeplink_handler.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('deep link handler: ', () {\n    final deepLinkHandlerRegistry = DeepLinkHandlerRegistry.instance\n      ..register(LoginDeepLinkHandler())\n      ..register(PaymentDeepLinkHandler())\n      ..register(InvitationDeepLinkHandler())\n      ..register(OpenAppDeepLinkHandler());\n\n    test('invitation deep link handler', () {\n      final uri = Uri.parse(\n        'appflowy-flutter://invitation-callback?email=lucas@appflowy.com&workspace_id=123',\n      );\n      deepLinkHandlerRegistry.processDeepLink(\n        uri: uri,\n        onStateChange: (handler, state) {\n          expect(handler, isA<InvitationDeepLinkHandler>());\n        },\n        onResult: (handler, result) {\n          expect(handler, isA<InvitationDeepLinkHandler>());\n          expect(result.isSuccess, true);\n        },\n        onError: (error) {\n          expect(error, isNull);\n        },\n      );\n    });\n\n    test('login deep link handler', () {\n      final uri =\n          Uri.parse('appflowy-flutter://login-callback#access_token=123');\n      expect(LoginDeepLinkHandler().canHandle(uri), true);\n    });\n\n    test('payment deep link handler', () {\n      final uri = Uri.parse('appflowy-flutter://payment-success');\n      expect(PaymentDeepLinkHandler().canHandle(uri), true);\n    });\n\n    test('unknown deep link handler', () {\n      final uri =\n          Uri.parse('appflowy-flutter://unknown-callback?workspace_id=123');\n      deepLinkHandlerRegistry.processDeepLink(\n        uri: uri,\n        onStateChange: (handler, state) {},\n        onResult: (handler, result) {},\n        onError: (error) {\n          expect(error, isNotNull);\n        },\n      );\n    });\n\n    test('open app deep link handler', () {\n      final uri = Uri.parse('appflowy-flutter://');\n      expect(OpenAppDeepLinkHandler().canHandle(uri), true);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/document/document_diff/document_diff_test.dart",
    "content": "import 'package:appflowy/plugins/document/application/document_diff.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('document diff:', () {\n    setUpAll(() {\n      Log.shared.disableLog = true;\n    });\n\n    tearDownAll(() {\n      Log.shared.disableLog = false;\n    });\n\n    const diff = DocumentDiff();\n\n    Node createNodeWithId({required String id, required String text}) {\n      return Node(\n        id: id,\n        type: ParagraphBlockKeys.type,\n        attributes: {\n          ParagraphBlockKeys.delta: (Delta()..insert(text)).toJson(),\n        },\n      );\n    }\n\n    Future<void> applyOperationAndVerifyDocument(\n      Document before,\n      Document after,\n      List<Operation> operations,\n    ) async {\n      final expected = after.toJson();\n      final editorState = EditorState(document: before);\n      final transaction = editorState.transaction;\n      for (final operation in operations) {\n        transaction.add(operation);\n      }\n      await editorState.apply(transaction);\n      expect(editorState.document.toJson(), expected);\n    }\n\n    test('no diff when the document is the same', () async {\n      // create two nodes with the same id and texts\n      final node1 = createNodeWithId(id: '1', text: 'Hello AppFlowy');\n      final node2 = createNodeWithId(id: '1', text: 'Hello AppFlowy');\n\n      final previous = Document.blank()..insert([0], [node1]);\n      final next = Document.blank()..insert([0], [node2]);\n      final operations = diff.diffDocument(previous, next);\n\n      expect(operations, isEmpty);\n\n      await applyOperationAndVerifyDocument(previous, next, operations);\n    });\n\n    test('update text diff with the same id', () async {\n      final node1 = createNodeWithId(id: '1', text: 'Hello AppFlowy');\n      final node2 = createNodeWithId(id: '1', text: 'Hello AppFlowy 2');\n\n      final previous = Document.blank()..insert([0], [node1]);\n      final next = Document.blank()..insert([0], [node2]);\n      final operations = diff.diffDocument(previous, next);\n\n      expect(operations.length, 1);\n      expect(operations[0], isA<UpdateOperation>());\n\n      await applyOperationAndVerifyDocument(previous, next, operations);\n    });\n\n    test('delete and insert text diff with different id', () async {\n      final node1 = createNodeWithId(id: '1', text: 'Hello AppFlowy');\n      final node2 = createNodeWithId(id: '2', text: 'Hello AppFlowy 2');\n\n      final previous = Document.blank()..insert([0], [node1]);\n      final next = Document.blank()..insert([0], [node2]);\n\n      final operations = diff.diffDocument(previous, next);\n\n      expect(operations.length, 2);\n      expect(operations[0], isA<InsertOperation>());\n      expect(operations[1], isA<DeleteOperation>());\n\n      await applyOperationAndVerifyDocument(previous, next, operations);\n    });\n\n    test('insert single text diff', () async {\n      final node1 = createNodeWithId(\n        id: '1',\n        text: 'Hello AppFlowy - First line',\n      );\n      final node21 = createNodeWithId(\n        id: '1',\n        text: 'Hello AppFlowy - First line',\n      );\n      final node22 = createNodeWithId(\n        id: '2',\n        text: 'Hello AppFlowy - Second line',\n      );\n\n      final previous = Document.blank()..insert([0], [node1]);\n      final next = Document.blank()..insert([0], [node21, node22]);\n\n      final operations = diff.diffDocument(previous, next);\n\n      expect(operations.length, 1);\n      expect(operations[0], isA<InsertOperation>());\n\n      await applyOperationAndVerifyDocument(previous, next, operations);\n    });\n\n    test('delete single text diff', () async {\n      final node11 = createNodeWithId(\n        id: '1',\n        text: 'Hello AppFlowy - First line',\n      );\n      final node12 = createNodeWithId(\n        id: '2',\n        text: 'Hello AppFlowy - Second line',\n      );\n      final node21 = createNodeWithId(\n        id: '1',\n        text: 'Hello AppFlowy - First line',\n      );\n\n      final previous = Document.blank()..insert([0], [node11, node12]);\n      final next = Document.blank()..insert([0], [node21]);\n\n      final operations = diff.diffDocument(previous, next);\n\n      expect(operations.length, 1);\n      expect(operations[0], isA<DeleteOperation>());\n\n      await applyOperationAndVerifyDocument(previous, next, operations);\n    });\n\n    test('insert multiple texts diff', () async {\n      final node11 = createNodeWithId(\n        id: '1',\n        text: 'Hello AppFlowy - First line',\n      );\n      final node15 = createNodeWithId(\n        id: '5',\n        text: 'Hello AppFlowy - Fifth line',\n      );\n      final node21 = createNodeWithId(\n        id: '1',\n        text: 'Hello AppFlowy - First line',\n      );\n      final node22 = createNodeWithId(\n        id: '2',\n        text: 'Hello AppFlowy - Second line',\n      );\n      final node23 = createNodeWithId(\n        id: '3',\n        text: 'Hello AppFlowy - Third line',\n      );\n      final node24 = createNodeWithId(\n        id: '4',\n        text: 'Hello AppFlowy - Fourth line',\n      );\n      final node25 = createNodeWithId(\n        id: '5',\n        text: 'Hello AppFlowy - Fifth line',\n      );\n\n      final previous = Document.blank()\n        ..insert(\n          [0],\n          [\n            node11,\n            node15,\n          ],\n        );\n      final next = Document.blank()\n        ..insert(\n          [0],\n          [\n            node21,\n            node22,\n            node23,\n            node24,\n            node25,\n          ],\n        );\n\n      final operations = diff.diffDocument(previous, next);\n\n      expect(operations.length, 1);\n\n      final op = operations[0] as InsertOperation;\n      expect(op.path, [1]);\n      expect(op.nodes, [node22, node23, node24]);\n\n      await applyOperationAndVerifyDocument(previous, next, operations);\n    });\n\n    test('delete multiple texts diff', () async {\n      final node11 = createNodeWithId(\n        id: '1',\n        text: 'Hello AppFlowy - First line',\n      );\n      final node12 = createNodeWithId(\n        id: '2',\n        text: 'Hello AppFlowy - Second line',\n      );\n      final node13 = createNodeWithId(\n        id: '3',\n        text: 'Hello AppFlowy - Third line',\n      );\n      final node14 = createNodeWithId(\n        id: '4',\n        text: 'Hello AppFlowy - Fourth line',\n      );\n      final node15 = createNodeWithId(\n        id: '5',\n        text: 'Hello AppFlowy - Fifth line',\n      );\n\n      final node21 = createNodeWithId(\n        id: '1',\n        text: 'Hello AppFlowy - First line',\n      );\n      final node25 = createNodeWithId(\n        id: '5',\n        text: 'Hello AppFlowy - Fifth line',\n      );\n\n      final previous = Document.blank()\n        ..insert(\n          [0],\n          [\n            node11,\n            node12,\n            node13,\n            node14,\n            node15,\n          ],\n        );\n      final next = Document.blank()\n        ..insert(\n          [0],\n          [\n            node21,\n            node25,\n          ],\n        );\n\n      final operations = diff.diffDocument(previous, next);\n\n      expect(operations.length, 1);\n\n      final op = operations[0] as DeleteOperation;\n      expect(op.path, [1]);\n      expect(op.nodes, [node12, node13, node14]);\n\n      await applyOperationAndVerifyDocument(previous, next, operations);\n    });\n\n    test('multiple delete and update diff', () async {\n      final node11 = createNodeWithId(\n        id: '1',\n        text: 'Hello AppFlowy - First line',\n      );\n      final node12 = createNodeWithId(\n        id: '2',\n        text: 'Hello AppFlowy - Second line',\n      );\n      final node13 = createNodeWithId(\n        id: '3',\n        text: 'Hello AppFlowy - Third line',\n      );\n      final node14 = createNodeWithId(\n        id: '4',\n        text: 'Hello AppFlowy - Fourth line',\n      );\n      final node15 = createNodeWithId(\n        id: '5',\n        text: 'Hello AppFlowy - Fifth line',\n      );\n\n      final node21 = createNodeWithId(\n        id: '1',\n        text: 'Hello AppFlowy - First line',\n      );\n      final node22 = createNodeWithId(\n        id: '2',\n        text: '',\n      );\n      final node25 = createNodeWithId(\n        id: '5',\n        text: 'Hello AppFlowy - Fifth line',\n      );\n\n      final previous = Document.blank()\n        ..insert(\n          [0],\n          [\n            node11,\n            node12,\n            node13,\n            node14,\n            node15,\n          ],\n        );\n      final next = Document.blank()\n        ..insert(\n          [0],\n          [\n            node21,\n            node22,\n            node25,\n          ],\n        );\n\n      final operations = diff.diffDocument(previous, next);\n\n      expect(operations.length, 2);\n      final op1 = operations[0] as UpdateOperation;\n      expect(op1.path, [1]);\n      expect(op1.attributes, node22.attributes);\n\n      final op2 = operations[1] as DeleteOperation;\n      expect(op2.path, [2]);\n      expect(op2.nodes, [node13, node14]);\n\n      await applyOperationAndVerifyDocument(previous, next, operations);\n    });\n\n    test('multiple insert and update diff', () async {\n      final node11 = createNodeWithId(\n        id: '1',\n        text: 'Hello AppFlowy - First line',\n      );\n      final node12 = createNodeWithId(\n        id: '2',\n        text: 'Hello AppFlowy - Second line',\n      );\n      final node13 = createNodeWithId(\n        id: '3',\n        text: 'Hello AppFlowy - Third line',\n      );\n      final node21 = createNodeWithId(\n        id: '1',\n        text: 'Hello AppFlowy - First line',\n      );\n      final node22 = createNodeWithId(\n        id: '2',\n        text: 'Hello AppFlowy - Second line - Updated',\n      );\n      final node23 = createNodeWithId(\n        id: '3',\n        text: 'Hello AppFlowy - Third line - Updated',\n      );\n      final node24 = createNodeWithId(\n        id: '4',\n        text: 'Hello AppFlowy - Fourth line - Updated',\n      );\n      final node25 = createNodeWithId(\n        id: '5',\n        text: 'Hello AppFlowy - Fifth line - Updated',\n      );\n\n      final previous = Document.blank()\n        ..insert(\n          [0],\n          [\n            node11,\n            node12,\n            node13,\n          ],\n        );\n      final next = Document.blank()\n        ..insert(\n          [0],\n          [\n            node21,\n            node22,\n            node23,\n            node24,\n            node25,\n          ],\n        );\n\n      final operations = diff.diffDocument(previous, next);\n\n      expect(operations.length, 3);\n      final op1 = operations[0] as InsertOperation;\n      expect(op1.path, [3]);\n      expect(op1.nodes, [node24, node25]);\n\n      final op2 = operations[1] as UpdateOperation;\n      expect(op2.path, [1]);\n      expect(op2.attributes, node22.attributes);\n\n      final op3 = operations[2] as UpdateOperation;\n      expect(op3.path, [2]);\n      expect(op3.attributes, node23.attributes);\n\n      await applyOperationAndVerifyDocument(previous, next, operations);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/document/html/_html_samples.dart",
    "content": "// | Month | Savings |\n// | -------- | ------- |\n// | January | $250 |\n// | February | $80 |\n// | March | $420 |\nconst tableFromNotion = '''\n<meta charset=\"utf-8\" />\n<table id=\"18196b61-6923-80d7-a184-fbc0352dabc3\" class=\"simple-table\">\n    <tbody>\n        <tr id=\"18196b61-6923-80a7-b70a-cbae038e1472\">\n            <td id=\"Wi`b\" class=\"\">Month</td>\n            <td id=\"|EyR\" class=\"\">Savings</td>\n        </tr>\n        <tr id=\"18196b61-6923-804a-914e-e45f6086a714\">\n            <td id=\"Wi`b\" class=\"\">January</td>\n            <td id=\"|EyR\" class=\"\">\\$250</td>\n        </tr>\n        <tr id=\"18196b61-6923-80b1-bef5-e15e1d302dfd\">\n            <td id=\"Wi`b\" class=\"\">February</td>\n            <td id=\"|EyR\" class=\"\">\\$80</td>\n        </tr>\n        <tr id=\"18196b61-6923-8079-aefa-d96c17230695\">\n            <td id=\"Wi`b\" class=\"\">March</td>\n            <td id=\"|EyR\" class=\"\">\\$420</td>\n        </tr>\n    </tbody>\n</table>\n''';\n\n// | Month | Savings |\n// | -------- | ------- |\n// | January | $250 |\n// | February | $80 |\n// | March | $420 |\nconst tableFromGoogleDocs = '''\n<meta charset=\"utf-8\" /><meta charset=\"utf-8\" />\n<b style=\"font-weight: normal;\" id=\"docs-internal-guid-c1bfdd24-7fff-ad47-657e-45aa52e41e4c\">\n    <div dir=\"ltr\" style=\"margin-left: 0pt;\" align=\"left\">\n        <table style=\"border: none; border-collapse: collapse; table-layout: fixed; width: 451.27559055118115pt;\">\n            <colgroup>\n                <col />\n                <col />\n            </colgroup>\n            <tbody>\n                <tr style=\"height: 0pt;\">\n                    <td\n                        style=\"\n                            border-left: solid #000000 1pt;\n                            border-right: solid #000000 1pt;\n                            border-bottom: solid #000000 1pt;\n                            border-top: solid #000000 1pt;\n                            vertical-align: top;\n                            padding: 5pt 5pt 5pt 5pt;\n                            overflow: hidden;\n                            overflow-wrap: break-word;\n                        \"\n                    >\n                        <p dir=\"ltr\" style=\"line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;\">\n                            <span\n                                style=\"\n                                    font-size: 11pt;\n                                    font-family: Arial, sans-serif;\n                                    color: #000000;\n                                    background-color: transparent;\n                                    font-weight: 400;\n                                    font-style: normal;\n                                    font-variant: normal;\n                                    text-decoration: none;\n                                    vertical-align: baseline;\n                                    white-space: pre;\n                                    white-space: pre-wrap;\n                                \"\n                            >\n                                Month\n                            </span>\n                        </p>\n                    </td>\n                    <td\n                        style=\"\n                            border-left: solid #000000 1pt;\n                            border-right: solid #000000 1pt;\n                            border-bottom: solid #000000 1pt;\n                            border-top: solid #000000 1pt;\n                            vertical-align: top;\n                            padding: 5pt 5pt 5pt 5pt;\n                            overflow: hidden;\n                            overflow-wrap: break-word;\n                        \"\n                    >\n                        <p dir=\"ltr\" style=\"line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;\">\n                            <span\n                                style=\"\n                                    font-size: 11pt;\n                                    font-family: Arial, sans-serif;\n                                    color: #000000;\n                                    background-color: transparent;\n                                    font-weight: 400;\n                                    font-style: normal;\n                                    font-variant: normal;\n                                    text-decoration: none;\n                                    vertical-align: baseline;\n                                    white-space: pre;\n                                    white-space: pre-wrap;\n                                \"\n                            >\n                                Savings\n                            </span>\n                        </p>\n                    </td>\n                </tr>\n                <tr style=\"height: 0pt;\">\n                    <td\n                        style=\"\n                            border-left: solid #000000 1pt;\n                            border-right: solid #000000 1pt;\n                            border-bottom: solid #000000 1pt;\n                            border-top: solid #000000 1pt;\n                            vertical-align: top;\n                            padding: 5pt 5pt 5pt 5pt;\n                            overflow: hidden;\n                            overflow-wrap: break-word;\n                        \"\n                    >\n                        <p dir=\"ltr\" style=\"line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;\">\n                            <span\n                                style=\"\n                                    font-size: 11pt;\n                                    font-family: Arial, sans-serif;\n                                    color: #000000;\n                                    background-color: transparent;\n                                    font-weight: 400;\n                                    font-style: normal;\n                                    font-variant: normal;\n                                    text-decoration: none;\n                                    vertical-align: baseline;\n                                    white-space: pre;\n                                    white-space: pre-wrap;\n                                \"\n                            >\n                                January\n                            </span>\n                        </p>\n                    </td>\n                    <td\n                        style=\"\n                            border-left: solid #000000 1pt;\n                            border-right: solid #000000 1pt;\n                            border-bottom: solid #000000 1pt;\n                            border-top: solid #000000 1pt;\n                            vertical-align: top;\n                            padding: 5pt 5pt 5pt 5pt;\n                            overflow: hidden;\n                            overflow-wrap: break-word;\n                        \"\n                    >\n                        <p dir=\"ltr\" style=\"line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;\">\n                            <span\n                                style=\"\n                                    font-size: 11pt;\n                                    font-family: Arial, sans-serif;\n                                    color: #000000;\n                                    background-color: transparent;\n                                    font-weight: 400;\n                                    font-style: normal;\n                                    font-variant: normal;\n                                    text-decoration: none;\n                                    vertical-align: baseline;\n                                    white-space: pre;\n                                    white-space: pre-wrap;\n                                \"\n                            >\n                                \\$250\n                            </span>\n                        </p>\n                    </td>\n                </tr>\n                <tr style=\"height: 0pt;\">\n                    <td\n                        style=\"\n                            border-left: solid #000000 1pt;\n                            border-right: solid #000000 1pt;\n                            border-bottom: solid #000000 1pt;\n                            border-top: solid #000000 1pt;\n                            vertical-align: top;\n                            padding: 5pt 5pt 5pt 5pt;\n                            overflow: hidden;\n                            overflow-wrap: break-word;\n                        \"\n                    >\n                        <p dir=\"ltr\" style=\"line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;\">\n                            <span\n                                style=\"\n                                    font-size: 11pt;\n                                    font-family: Arial, sans-serif;\n                                    color: #000000;\n                                    background-color: transparent;\n                                    font-weight: 400;\n                                    font-style: normal;\n                                    font-variant: normal;\n                                    text-decoration: none;\n                                    vertical-align: baseline;\n                                    white-space: pre;\n                                    white-space: pre-wrap;\n                                \"\n                            >\n                                February\n                            </span>\n                        </p>\n                    </td>\n                    <td\n                        style=\"\n                            border-left: solid #000000 1pt;\n                            border-right: solid #000000 1pt;\n                            border-bottom: solid #000000 1pt;\n                            border-top: solid #000000 1pt;\n                            vertical-align: top;\n                            padding: 5pt 5pt 5pt 5pt;\n                            overflow: hidden;\n                            overflow-wrap: break-word;\n                        \"\n                    >\n                        <p dir=\"ltr\" style=\"line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;\">\n                            <span\n                                style=\"\n                                    font-size: 11pt;\n                                    font-family: Arial, sans-serif;\n                                    color: #000000;\n                                    background-color: transparent;\n                                    font-weight: 400;\n                                    font-style: normal;\n                                    font-variant: normal;\n                                    text-decoration: none;\n                                    vertical-align: baseline;\n                                    white-space: pre;\n                                    white-space: pre-wrap;\n                                \"\n                            >\n                                \\$80\n                            </span>\n                        </p>\n                    </td>\n                </tr>\n                <tr style=\"height: 0pt;\">\n                    <td\n                        style=\"\n                            border-left: solid #000000 1pt;\n                            border-right: solid #000000 1pt;\n                            border-bottom: solid #000000 1pt;\n                            border-top: solid #000000 1pt;\n                            vertical-align: top;\n                            padding: 5pt 5pt 5pt 5pt;\n                            overflow: hidden;\n                            overflow-wrap: break-word;\n                        \"\n                    >\n                        <p dir=\"ltr\" style=\"line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;\">\n                            <span\n                                style=\"\n                                    font-size: 11pt;\n                                    font-family: Arial, sans-serif;\n                                    color: #000000;\n                                    background-color: transparent;\n                                    font-weight: 400;\n                                    font-style: normal;\n                                    font-variant: normal;\n                                    text-decoration: none;\n                                    vertical-align: baseline;\n                                    white-space: pre;\n                                    white-space: pre-wrap;\n                                \"\n                            >\n                                March\n                            </span>\n                        </p>\n                    </td>\n                    <td\n                        style=\"\n                            border-left: solid #000000 1pt;\n                            border-right: solid #000000 1pt;\n                            border-bottom: solid #000000 1pt;\n                            border-top: solid #000000 1pt;\n                            vertical-align: top;\n                            padding: 5pt 5pt 5pt 5pt;\n                            overflow: hidden;\n                            overflow-wrap: break-word;\n                        \"\n                    >\n                        <p dir=\"ltr\" style=\"line-height: 1.2; margin-top: 0pt; margin-bottom: 0pt;\">\n                            <span\n                                style=\"\n                                    font-size: 11pt;\n                                    font-family: Arial, sans-serif;\n                                    color: #000000;\n                                    background-color: transparent;\n                                    font-weight: 400;\n                                    font-style: normal;\n                                    font-variant: normal;\n                                    text-decoration: none;\n                                    vertical-align: baseline;\n                                    white-space: pre;\n                                    white-space: pre-wrap;\n                                \"\n                            >\n                                \\$420\n                            </span>\n                        </p>\n                    </td>\n                </tr>\n            </tbody>\n        </table>\n    </div>\n</b>\n''';\n\n// | Month | Savings |\n// | -------- | ------- |\n// | January | $250 |\n// | February | $80 |\n// | March | $420 |\nconst tableFromGoogleSheets = '''\n<meta charset=\"utf-8\" />\n<google-sheets-html-origin>\n    <style type=\"text/css\">\n        <!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}-->\n    </style>\n    <table\n        xmlns=\"http://www.w3.org/1999/xhtml\"\n        cellspacing=\"0\"\n        cellpadding=\"0\"\n        dir=\"ltr\"\n        border=\"1\"\n        style=\"table-layout: fixed; font-size: 10pt; font-family: Arial; width: 0px; border-collapse: collapse; border: none;\"\n        data-sheets-root=\"1\"\n        data-sheets-baot=\"1\"\n    >\n        <colgroup>\n            <col width=\"100\" />\n            <col width=\"100\" />\n        </colgroup>\n        <tbody>\n            <tr style=\"height: 21px;\">\n                <td style=\"overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;\">Month</td>\n                <td style=\"overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;\">Savings</td>\n            </tr>\n            <tr style=\"height: 21px;\">\n                <td style=\"overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;\">January</td>\n                <td style=\"overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;\">\\$250</td>\n            </tr>\n            <tr style=\"height: 21px;\">\n                <td style=\"overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;\">February</td>\n                <td style=\"overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;\">\\$80</td>\n            </tr>\n            <tr style=\"height: 21px;\">\n                <td style=\"overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;\">March</td>\n                <td style=\"overflow: hidden; padding: 2px 3px 2px 3px; vertical-align: bottom;\">\\$420</td>\n            </tr>\n        </tbody>\n    </table>\n</google-sheets-html-origin>\n\n''';\n\n// # The Benefits of a Balanced Diet\n// 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.\n// ---\n// ## Key Components of a Balanced Diet\n// A balanced diet consists of various food groups, each providing essential nutrients. The main components include:\n// 1.  **Carbohydrates** – Provide energy for daily activities.\n// 1.  **Proteins** – Support growth, muscle repair, and immune function.\n// 1.  **Fats** – Aid in cell function and energy storage.\n// 1.  **Vitamins and Minerals** – Essential for immune function, bone health, and overall bodily processes.\n// 1.  **Fiber** – Promotes healthy digestion and reduces the risk of chronic diseases.\n// 1.  **Water** – Vital for hydration and proper bodily functions.\n// ---\n// ## Health Benefits of a Balanced Diet\n// Maintaining a balanced diet can have profound effects on your health. Below are some of the most significant benefits:\n// ---\n// ### 1. **Improved Heart Health**\n// A balanced diet rich in fruits, vegetables, and healthy fats helps lower cholesterol levels, reduce inflammation, and maintain a healthy blood pressure.\n// ### 2. **Better Weight Management**\n// By consuming nutrient-dense foods and avoiding overeating, you can achieve and maintain a healthy weight.\n// ### 3. **Enhanced Mental Health**\n// Proper nutrition supports brain function, which can improve mood, cognitive performance, and mental well-being.\n// ### 4. **Stronger Immune System**\n// A diet full of vitamins and minerals strengthens the immune system and helps the body fight off infections.\n// ---\n// ## Recommended Daily Nutrient Intake\n// Below is a table that outlines the recommended daily intake for adults based on the different food groups:\n// |Nutrient|Recommended Daily Intake|Example Foods|\n// |---|---|---|\n// |**Carbohydrates**|45-65% of total calories|Whole grains, fruits, vegetables|\n// |**Proteins**|10-35% of total calories|Lean meats, beans, legumes, nuts, dairy|\n// |**Fats**|20-35% of total calories|Olive oil, avocado, nuts, fatty fish|\n// |**Fiber**|25-30 grams|Whole grains, fruits, vegetables, legumes|\n// |**Vitamins & Minerals**|Varies (See below)|Fruits, vegetables, dairy, fortified cereals|\n// |**Water**|2-3 liters/day|Water, herbal teas, soups|\n// ---\n// ## Conclusion\n// 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.\nconst tableFromChatGPT = '''\n<meta charset=\"utf-8\" />\n<h1>The Benefits of a Balanced Diet</h1>\n<p>\n    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,\n    we will explore the key benefits of a balanced diet and how it can improve your life.\n</p>\n<hr />\n<h2>Key Components of a Balanced Diet</h2>\n<p>A balanced diet consists of various food groups, each providing essential nutrients. The main components include:</p>\n<ol>\n    <li><strong>Carbohydrates</strong> – Provide energy for daily activities.</li>\n    <li><strong>Proteins</strong> – Support growth, muscle repair, and immune function.</li>\n    <li><strong>Fats</strong> – Aid in cell function and energy storage.</li>\n    <li><strong>Vitamins and Minerals</strong> – Essential for immune function, bone health, and overall bodily processes.</li>\n    <li><strong>Fiber</strong> – Promotes healthy digestion and reduces the risk of chronic diseases.</li>\n    <li><strong>Water</strong> – Vital for hydration and proper bodily functions.</li>\n</ol>\n<hr />\n<h2>Health Benefits of a Balanced Diet</h2>\n<p>Maintaining a balanced diet can have profound effects on your health. Below are some of the most significant benefits:</p>\n<hr />\n<h3>1. <strong>Improved Heart Health</strong></h3>\n<p>A balanced diet rich in fruits, vegetables, and healthy fats helps lower cholesterol levels, reduce inflammation, and maintain a healthy blood pressure.</p>\n<h3>2. <strong>Better Weight Management</strong></h3>\n<p>By consuming nutrient-dense foods and avoiding overeating, you can achieve and maintain a healthy weight.</p>\n<h3>3. <strong>Enhanced Mental Health</strong></h3>\n<p>Proper nutrition supports brain function, which can improve mood, cognitive performance, and mental well-being.</p>\n<h3>4. <strong>Stronger Immune System</strong></h3>\n<p>A diet full of vitamins and minerals strengthens the immune system and helps the body fight off infections.</p>\n<hr />\n<h2>Recommended Daily Nutrient Intake</h2>\n<p>Below is a table that outlines the recommended daily intake for adults based on the different food groups:</p>\n<table>\n    <thead>\n        <tr>\n            <th>Nutrient</th>\n            <th>Recommended Daily Intake</th>\n            <th>Example Foods</th>\n        </tr>\n    </thead>\n    <tbody>\n        <tr>\n            <td><strong>Carbohydrates</strong></td>\n            <td>45-65% of total calories</td>\n            <td>Whole grains, fruits, vegetables</td>\n        </tr>\n        <tr>\n            <td><strong>Proteins</strong></td>\n            <td>10-35% of total calories</td>\n            <td>Lean meats, beans, legumes, nuts, dairy</td>\n        </tr>\n        <tr>\n            <td><strong>Fats</strong></td>\n            <td>20-35% of total calories</td>\n            <td>Olive oil, avocado, nuts, fatty fish</td>\n        </tr>\n        <tr>\n            <td><strong>Fiber</strong></td>\n            <td>25-30 grams</td>\n            <td>Whole grains, fruits, vegetables, legumes</td>\n        </tr>\n        <tr>\n            <td><strong>Vitamins &amp; Minerals</strong></td>\n            <td>Varies (See below)</td>\n            <td>Fruits, vegetables, dairy, fortified cereals</td>\n        </tr>\n        <tr>\n            <td><strong>Water</strong></td>\n            <td>2-3 liters/day</td>\n            <td>Water, herbal teas, soups</td>\n        </tr>\n    </tbody>\n</table>\n<hr />\n<h2>Conclusion</h2>\n<p>\n    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.\n    By following the guidelines above, you can work toward achieving a healthier and happier life.\n</p>\n''';\n\n// | Month | Savings |\n// | -------- | ------- |\n// | January | $250 |\n// | February | $80 |\n// | March | $420 |\nconst tableFromAppleNotes = '''\n<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n<meta http-equiv=\"Content-Style-Type\" content=\"text/css\">\n<title></title>\n<meta name=\"Generator\" content=\"Cocoa HTML Writer\">\n<meta name=\"CocoaVersion\" content=\"2566\">\n<style type=\"text/css\">\np.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 13.0px '.AppleSystemUIFont'}\ntable.t1 {border-collapse: collapse}\ntd.td1 {border-style: solid; border-width: 1.0px 1.0px 1.0px 1.0px; border-color: #9a9a9a #9a9a9a #9a9a9a #9a9a9a; padding: 1.0px 5.0px 1.0px 5.0px}\n</style>\n</head>\n<body>\n<table cellspacing=\"0\" cellpadding=\"0\" class=\"t1\">\n<tbody>\n<tr>\n<td valign=\"top\" class=\"td1\">\n<p class=\"p1\">Month</p>\n</td>\n<td valign=\"top\" class=\"td1\">\n<p class=\"p1\">Savings</p>\n</td>\n</tr>\n<tr>\n<td valign=\"top\" class=\"td1\">\n<p class=\"p1\">January</p>\n</td>\n<td valign=\"top\" class=\"td1\">\n<p class=\"p1\">\\$250</p>\n</td>\n</tr>\n<tr>\n<td valign=\"top\" class=\"td1\">\n<p class=\"p1\">February</p>\n</td>\n<td valign=\"top\" class=\"td1\">\n<p class=\"p1\">\\$80</p>\n</td>\n</tr>\n<tr>\n<td valign=\"top\" class=\"td1\">\n<p class=\"p1\">March</p>\n</td>\n<td valign=\"top\" class=\"td1\">\n<p class=\"p1\">\\$420</p>\n</td>\n</tr>\n</tbody>\n</table>\n</body>\n</html>\n''';\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/document/html/paste_from_html_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '_html_samples.dart';\n\nvoid main() {\n  group('paste from html:', () {\n    void checkTable(String html) {\n      final nodes = EditorState.blank().convertHtmlToNodes(html);\n      expect(nodes.length, 1);\n      final table = nodes.first;\n      expect(table.type, SimpleTableBlockKeys.type);\n      expect(table.getCellText(0, 0), 'Month');\n      expect(table.getCellText(0, 1), 'Savings');\n      expect(table.getCellText(1, 0), 'January');\n      expect(table.getCellText(1, 1), '\\$250');\n      expect(table.getCellText(2, 0), 'February');\n      expect(table.getCellText(2, 1), '\\$80');\n      expect(table.getCellText(3, 0), 'March');\n      expect(table.getCellText(3, 1), '\\$420');\n    }\n\n    test('sample 1 - paste table from Notion', () {\n      checkTable(tableFromNotion);\n    });\n\n    test('sample 2 - paste table from Google Docs', () {\n      checkTable(tableFromGoogleDocs);\n    });\n\n    test('sample 3 - paste table from Google Sheets', () {\n      checkTable(tableFromGoogleSheets);\n    });\n\n    test('sample 4 - paste table from ChatGPT', () {\n      final nodes = EditorState.blank().convertHtmlToNodes(tableFromChatGPT);\n      final table =\n          nodes.where((node) => node.type == SimpleTableBlockKeys.type).first;\n\n      expect(table.columnLength, 3);\n      expect(table.rowLength, 7);\n\n      final dividers =\n          nodes.where((node) => node.type == DividerBlockKeys.type);\n      expect(dividers.length, 5);\n    });\n\n    test('sample 5 - paste table from Apple Notes', () {\n      checkTable(tableFromAppleNotes);\n    });\n  });\n}\n\nextension on Node {\n  String getCellText(\n    int row,\n    int column, {\n    int index = 0,\n  }) {\n    return children[row]\n            .children[column]\n            .children[index]\n            .delta\n            ?.toPlainText() ??\n        '';\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/document/option_menu/block_action_option_cubit_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('block action option cubit:', () {\n    setUpAll(() {\n      Log.shared.disableLog = true;\n    });\n\n    tearDownAll(() {\n      Log.shared.disableLog = false;\n    });\n\n    test('delete blocks', () async {\n      const text = 'paragraph';\n      final document = Document.blank()\n        ..insert([\n          0,\n        ], [\n          paragraphNode(text: text),\n          paragraphNode(text: text),\n          paragraphNode(text: text),\n        ]);\n\n      final editorState = EditorState(document: document);\n      final cubit = BlockActionOptionCubit(\n        editorState: editorState,\n        blockComponentBuilder: {},\n      );\n\n      editorState.selection = Selection(\n        start: Position(path: [0]),\n        end: Position(path: [2], offset: text.length),\n      );\n      editorState.selectionType = SelectionType.block;\n\n      await cubit.handleAction(OptionAction.delete, document.nodeAtPath([0])!);\n\n      // all the nodes should be deleted\n      expect(document.root.children, isEmpty);\n\n      editorState.dispose();\n    });\n\n    test('duplicate blocks', () async {\n      const text = 'paragraph';\n      final document = Document.blank()\n        ..insert([\n          0,\n        ], [\n          paragraphNode(text: text),\n          paragraphNode(text: text),\n          paragraphNode(text: text),\n        ]);\n\n      final editorState = EditorState(document: document);\n      final cubit = BlockActionOptionCubit(\n        editorState: editorState,\n        blockComponentBuilder: {},\n      );\n\n      editorState.selection = Selection(\n        start: Position(path: [0]),\n        end: Position(path: [2], offset: text.length),\n      );\n      editorState.selectionType = SelectionType.block;\n\n      await cubit.handleAction(\n        OptionAction.duplicate,\n        document.nodeAtPath([0])!,\n      );\n\n      expect(document.root.children, hasLength(6));\n\n      editorState.dispose();\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/document/shortcuts/format_shortcut_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/base/format_arrow_character.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('format shortcut:', () {\n    setUpAll(() {\n      Log.shared.disableLog = true;\n    });\n\n    tearDownAll(() {\n      Log.shared.disableLog = false;\n    });\n\n    test('turn = + > into ⇒', () async {\n      final document = Document.blank()\n        ..insert([\n          0,\n        ], [\n          paragraphNode(text: '='),\n        ]);\n\n      final editorState = EditorState(document: document);\n      editorState.selection = Selection.collapsed(\n        Position(path: [0], offset: 1),\n      );\n\n      final result = await customFormatGreaterEqual.execute(editorState);\n      expect(result, true);\n\n      expect(editorState.document.root.children.length, 1);\n      final node = editorState.document.root.children[0];\n      expect(node.delta!.toPlainText(), '⇒');\n\n      // use undo to revert the change\n      undoCommand.execute(editorState);\n      expect(editorState.document.root.children.length, 1);\n      final nodeAfterUndo = editorState.document.root.children[0];\n      expect(nodeAfterUndo.delta!.toPlainText(), '=>');\n\n      editorState.dispose();\n    });\n\n    test('turn - + > into →', () async {\n      final document = Document.blank()\n        ..insert([\n          0,\n        ], [\n          paragraphNode(text: '-'),\n        ]);\n\n      final editorState = EditorState(document: document);\n      editorState.selection = Selection.collapsed(\n        Position(path: [0], offset: 1),\n      );\n\n      final result = await customFormatDashGreater.execute(editorState);\n      expect(result, true);\n\n      expect(editorState.document.root.children.length, 1);\n      final node = editorState.document.root.children[0];\n      expect(node.delta!.toPlainText(), '→');\n\n      // use undo to revert the change\n      undoCommand.execute(editorState);\n      expect(editorState.document.root.children.length, 1);\n      final nodeAfterUndo = editorState.document.root.children[0];\n      expect(nodeAfterUndo.delta!.toPlainText(), '->');\n\n      editorState.dispose();\n    });\n\n    test('turn -- into —', () async {\n      final document = Document.blank()\n        ..insert([\n          0,\n        ], [\n          paragraphNode(text: '-'),\n        ]);\n\n      final editorState = EditorState(document: document);\n      editorState.selection = Selection.collapsed(\n        Position(path: [0], offset: 1),\n      );\n\n      final result = await customFormatDoubleHyphenEmDash.execute(editorState);\n      expect(result, true);\n\n      expect(editorState.document.root.children.length, 1);\n      final node = editorState.document.root.children[0];\n      expect(node.delta!.toPlainText(), '—');\n\n      // use undo to revert the change\n      undoCommand.execute(editorState);\n      expect(editorState.document.root.children.length, 1);\n      final nodeAfterUndo = editorState.document.root.children[0];\n      expect(nodeAfterUndo.delta!.toPlainText(), '--');\n\n      editorState.dispose();\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/document/shortcuts/toggle_list_shortcut_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('toggle list shortcut:', () {\n    Document createDocument(List<Node> nodes) {\n      final document = Document.blank();\n      document.insert([0], nodes);\n      return document;\n    }\n\n    setUpAll(() {\n      Log.shared.disableLog = true;\n    });\n\n    tearDownAll(() {\n      Log.shared.disableLog = false;\n    });\n\n    testWidgets('> + #', (tester) async {\n      const heading1 = '>Heading 1';\n      const paragraph1 = 'paragraph 1';\n      const paragraph2 = 'paragraph 2';\n\n      final document = createDocument([\n        headingNode(level: 1, text: heading1),\n        paragraphNode(text: paragraph1),\n        paragraphNode(text: paragraph2),\n      ]);\n\n      final editorState = EditorState(document: document);\n      editorState.selection = Selection.collapsed(\n        Position(path: [0], offset: 1),\n      );\n\n      final result = await formatGreaterToToggleList.execute(editorState);\n      expect(result, true);\n\n      expect(editorState.document.root.children.length, 1);\n      final node = editorState.document.root.children[0];\n      expect(node.type, ToggleListBlockKeys.type);\n      expect(node.attributes[ToggleListBlockKeys.level], 1);\n      expect(node.delta!.toPlainText(), 'Heading 1');\n      expect(node.children.length, 2);\n      expect(node.children[0].delta!.toPlainText(), paragraph1);\n      expect(node.children[1].delta!.toPlainText(), paragraph2);\n\n      editorState.dispose();\n    });\n\n    testWidgets('convert block contains children to toggle list',\n        (tester) async {\n      const paragraph1 = '>paragraph 1';\n      const paragraph1_1 = 'paragraph 1.1';\n      const paragraph1_2 = 'paragraph 1.2';\n\n      final document = createDocument([\n        paragraphNode(\n          text: paragraph1,\n          children: [\n            paragraphNode(text: paragraph1_1),\n            paragraphNode(text: paragraph1_2),\n          ],\n        ),\n      ]);\n\n      final editorState = EditorState(document: document);\n      editorState.selection = Selection.collapsed(\n        Position(path: [0], offset: 1),\n      );\n\n      final result = await formatGreaterToToggleList.execute(editorState);\n      expect(result, true);\n\n      expect(editorState.document.root.children.length, 1);\n      final node = editorState.document.root.children[0];\n      expect(node.type, ToggleListBlockKeys.type);\n      expect(node.delta!.toPlainText(), 'paragraph 1');\n      expect(node.children.length, 2);\n      expect(node.children[0].delta!.toPlainText(), paragraph1_1);\n      expect(node.children[1].delta!.toPlainText(), paragraph1_2);\n\n      editorState.dispose();\n    });\n\n    testWidgets('press the enter key in empty toggle list', (tester) async {\n      const text = 'AppFlowy';\n\n      final document = createDocument([\n        toggleListBlockNode(text: text, collapsed: true),\n      ]);\n\n      final editorState = EditorState(document: document);\n      editorState.selection = Selection.collapsed(\n        Position(path: [0], offset: text.length),\n      );\n\n      // simulate the enter key press\n      final result = await insertChildNodeInsideToggleList.execute(editorState);\n      expect(result, true);\n\n      final nodes = editorState.document.root.children;\n      expect(nodes.length, 2);\n      for (var i = 0; i < nodes.length; i++) {\n        final node = nodes[i];\n        expect(node.type, ToggleListBlockKeys.type);\n      }\n\n      editorState.dispose();\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('markdown text robot:', () {\n    setUpAll(() {\n      Log.shared.disableLog = true;\n    });\n\n    tearDownAll(() {\n      Log.shared.disableLog = false;\n    });\n\n    Future<void> testLiveRefresh(\n      List<String> texts, {\n      required void Function(EditorState) expect,\n    }) async {\n      final editorState = EditorState.blank();\n      editorState.selection = Selection.collapsed(Position(path: [0]));\n      final markdownTextRobot = MarkdownTextRobot(\n        editorState: editorState,\n      );\n      markdownTextRobot.start();\n      for (final text in texts) {\n        await markdownTextRobot.appendMarkdownText(text);\n        // mock the delay of the text robot\n        await Future.delayed(const Duration(milliseconds: 10));\n      }\n      await markdownTextRobot.persist();\n\n      expect(editorState);\n    }\n\n    test('parse markdown text (1)', () async {\n      final editorState = EditorState.blank();\n      editorState.selection = Selection.collapsed(Position(path: [0]));\n      final markdownTextRobot = MarkdownTextRobot(\n        editorState: editorState,\n      );\n\n      markdownTextRobot.start();\n      await markdownTextRobot.appendMarkdownText(_sample1);\n      await markdownTextRobot.persist();\n\n      final nodes = editorState.document.root.children;\n      expect(nodes.length, 4);\n\n      final n1 = nodes[0];\n      expect(n1.delta!.toPlainText(), 'The Curious Cat');\n      expect(n1.type, HeadingBlockKeys.type);\n\n      final n2 = nodes[1];\n      expect(n2.type, ParagraphBlockKeys.type);\n      expect(n2.delta!.toJson(), [\n        {'insert': 'Once upon a time in a '},\n        {\n          'insert': 'quiet village',\n          'attributes': {'bold': true},\n        },\n        {'insert': ', there lived a curious cat named '},\n        {\n          'insert': 'Whiskers',\n          'attributes': {'italic': true},\n        },\n        {'insert': '. Unlike other cats, Whiskers had a passion for '},\n        {\n          'insert': 'exploration',\n          'attributes': {'bold': true},\n        },\n        {\n          'insert':\n              '. Every day, he\\'d wander through the village, discovering hidden spots and making new friends with the local animals.',\n        },\n      ]);\n\n      final n3 = nodes[2];\n      expect(n3.type, ParagraphBlockKeys.type);\n      expect(n3.delta!.toJson(), [\n        {'insert': 'One sunny morning, Whiskers stumbled upon a mysterious '},\n        {\n          'insert': 'wooden box',\n          'attributes': {'bold': true},\n        },\n        {'insert': ' behind the old barn. It was covered in '},\n        {\n          'insert': 'vines and dust',\n          'attributes': {'italic': true},\n        },\n        {\n          'insert':\n              '. Intrigued, he nudged it open with his paw and found a collection of ancient maps. These maps led to secret trails around the village.',\n        },\n      ]);\n\n      final n4 = nodes[3];\n      expect(n4.type, ParagraphBlockKeys.type);\n      expect(n4.delta!.toJson(), [\n        {\n          'insert':\n              'Whiskers became the village\\'s hero, guiding everyone on exciting adventures.',\n        },\n      ]);\n    });\n\n    // Live refresh - Partial sample\n    // ## The Decision\n    // - Aria found an ancient map in her grandmother's attic.\n    // - The map hinted at a mystical place known as the Enchanted Forest.\n    // - Legends spoke of the forest as a realm where dreams came to life.\n    test('live refresh (2)', () async {\n      await testLiveRefresh(\n        _liveRefreshSample2,\n        expect: (editorState) {\n          final nodes = editorState.document.root.children;\n          expect(nodes.length, 4);\n\n          final n1 = nodes[0];\n          expect(n1.type, HeadingBlockKeys.type);\n          expect(n1.delta!.toPlainText(), 'The Decision');\n\n          final n2 = nodes[1];\n          expect(n2.type, BulletedListBlockKeys.type);\n          expect(\n            n2.delta!.toPlainText(),\n            'Aria found an ancient map in her grandmother\\'s attic.',\n          );\n\n          final n3 = nodes[2];\n          expect(n3.type, BulletedListBlockKeys.type);\n          expect(\n            n3.delta!.toPlainText(),\n            'The map hinted at a mystical place known as the Enchanted Forest.',\n          );\n\n          final n4 = nodes[3];\n          expect(n4.type, BulletedListBlockKeys.type);\n          expect(\n            n4.delta!.toPlainText(),\n            'Legends spoke of the forest as a realm where dreams came to life.',\n          );\n        },\n      );\n    });\n\n    // Partial sample\n    // ## The Preparation\n    // Before embarking on her journey, Aria prepared meticulously:\n    // 1. Gather Supplies\n    //   - A sturdy backpack\n    //   - A compass and a map\n    //   - Provisions for the week\n    // 2. Seek Guidance\n    //   - Visited the village elder for advice\n    //   - Listened to tales of past adventurers\n    // 3. Sharpen Skills\n    //   - Practiced archery and swordsmanship\n    //   - Enhanced survival skills\n    test('live refresh (3)', () async {\n      await testLiveRefresh(\n        _liveRefreshSample3,\n        expect: (editorState) {\n          final nodes = editorState.document.root.children;\n          expect(nodes.length, 5);\n\n          final n1 = nodes[0];\n          expect(n1.type, HeadingBlockKeys.type);\n          expect(n1.delta!.toPlainText(), 'The Preparation');\n\n          final n2 = nodes[1];\n          expect(n2.type, ParagraphBlockKeys.type);\n          expect(\n            n2.delta!.toPlainText(),\n            'Before embarking on her journey, Aria prepared meticulously:',\n          );\n\n          final n3 = nodes[2];\n          expect(n3.type, NumberedListBlockKeys.type);\n          expect(\n            n3.delta!.toPlainText(),\n            'Gather Supplies',\n          );\n\n          final n3c1 = n3.children[0];\n          expect(n3c1.type, BulletedListBlockKeys.type);\n          expect(n3c1.delta!.toPlainText(), 'A sturdy backpack');\n\n          final n3c2 = n3.children[1];\n          expect(n3c2.type, BulletedListBlockKeys.type);\n          expect(n3c2.delta!.toPlainText(), 'A compass and a map');\n\n          final n3c3 = n3.children[2];\n          expect(n3c3.type, BulletedListBlockKeys.type);\n          expect(n3c3.delta!.toPlainText(), 'Provisions for the week');\n\n          final n4 = nodes[3];\n          expect(n4.type, NumberedListBlockKeys.type);\n          expect(n4.delta!.toPlainText(), 'Seek Guidance');\n\n          final n4c1 = n4.children[0];\n          expect(n4c1.type, BulletedListBlockKeys.type);\n          expect(\n            n4c1.delta!.toPlainText(),\n            'Visited the village elder for advice',\n          );\n\n          final n4c2 = n4.children[1];\n          expect(n4c2.type, BulletedListBlockKeys.type);\n          expect(\n            n4c2.delta!.toPlainText(),\n            'Listened to tales of past adventurers',\n          );\n\n          final n5 = nodes[4];\n          expect(n5.type, NumberedListBlockKeys.type);\n          expect(\n            n5.delta!.toPlainText(),\n            'Sharpen Skills',\n          );\n\n          final n5c1 = n5.children[0];\n          expect(n5c1.type, BulletedListBlockKeys.type);\n          expect(\n            n5c1.delta!.toPlainText(),\n            'Practiced archery and swordsmanship',\n          );\n\n          final n5c2 = n5.children[1];\n          expect(n5c2.type, BulletedListBlockKeys.type);\n          expect(\n            n5c2.delta!.toPlainText(),\n            'Enhanced survival skills',\n          );\n        },\n      );\n    });\n\n    // Partial sample\n    // 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    // ```rust\n    // fn two_sum(nums: &[i32], target: i32) -> 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().enumerate() {\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    test('live refresh (4)', () async {\n      await testLiveRefresh(\n        _liveRefreshSample4,\n        expect: (editorState) {\n          final nodes = editorState.document.root.children;\n          expect(nodes.length, 2);\n\n          final n1 = nodes[0];\n          expect(n1.type, ParagraphBlockKeys.type);\n          expect(\n            n1.delta!.toPlainText(),\n            '''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\n          final n2 = nodes[1];\n          expect(n2.type, CodeBlockKeys.type);\n          expect(\n            n2.delta!.toPlainText(),\n            isNotEmpty,\n          );\n          expect(n2.attributes[CodeBlockKeys.language], 'rust');\n        },\n      );\n    });\n  });\n\n  group('markdown text robot - replace in same line:', () {\n    final text1 =\n        '''The introduction of the World Wide Web in the early 1990s marked a turning point. ''';\n    final text2 =\n        '''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption. ''';\n    final text3 =\n        '''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.''';\n\n    Document buildTestDocument() {\n      return Document(\n        root: pageNode(\n          children: [\n            paragraphNode(delta: Delta()..insert(text1 + text2 + text3)),\n          ],\n        ),\n      );\n    }\n\n    // 1. create a document with a paragraph node\n    // 2. use the text robot to replace the selected content in the same line\n    // 3. check the document\n    test('the selection is in the middle of the text', () async {\n      final document = buildTestDocument();\n      final editorState = EditorState(document: document);\n\n      editorState.selection = Selection(\n        start: Position(\n          path: [0],\n          offset: text1.length,\n        ),\n        end: Position(\n          path: [0],\n          offset: text1.length + text2.length,\n        ),\n      );\n\n      final markdownText =\n          '''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.''';\n      final markdownTextRobot = MarkdownTextRobot(\n        editorState: editorState,\n      );\n      await markdownTextRobot.replace(\n        selection: editorState.selection!,\n        markdownText: markdownText,\n      );\n\n      final afterDelta = editorState.document.root.children[0].delta!.toList();\n      expect(afterDelta.length, 5);\n\n      final d1 = afterDelta[0] as TextInsert;\n      expect(d1.text, '${text1}Tim Berners-Lee\\'s invention of the ');\n      expect(d1.attributes, null);\n\n      final d2 = afterDelta[1] as TextInsert;\n      expect(d2.text, 'World Wide Web');\n      expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});\n\n      final d3 = afterDelta[2] as TextInsert;\n      expect(d3.text, ' transformed the internet, making it accessible to ');\n      expect(d3.attributes, null);\n\n      final d4 = afterDelta[3] as TextInsert;\n      expect(d4.text, 'non-technical users');\n      expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});\n\n      final d5 = afterDelta[4] as TextInsert;\n      expect(\n        d5.text,\n        ' and opening the floodgates for global mass adoption.$text3',\n      );\n      expect(d5.attributes, null);\n    });\n\n    test('replace markdown text with selection from start to middle', () async {\n      final document = buildTestDocument();\n      final editorState = EditorState(document: document);\n\n      editorState.selection = Selection(\n        start: Position(\n          path: [0],\n        ),\n        end: Position(\n          path: [0],\n          offset: text1.length,\n        ),\n      );\n\n      final markdownText =\n          '''The **invention** of the _World Wide Web_ by Tim Berners-Lee transformed how we access information.''';\n      final markdownTextRobot = MarkdownTextRobot(\n        editorState: editorState,\n      );\n      await markdownTextRobot.replace(\n        selection: editorState.selection!,\n        markdownText: markdownText,\n      );\n\n      final afterDelta = editorState.document.root.children[0].delta!.toList();\n      expect(afterDelta.length, 5);\n\n      final d1 = afterDelta[0] as TextInsert;\n      expect(d1.text, 'The ');\n      expect(d1.attributes, null);\n\n      final d2 = afterDelta[1] as TextInsert;\n      expect(d2.text, 'invention');\n      expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});\n\n      final d3 = afterDelta[2] as TextInsert;\n      expect(d3.text, ' of the ');\n      expect(d3.attributes, null);\n\n      final d4 = afterDelta[3] as TextInsert;\n      expect(d4.text, 'World Wide Web');\n      expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});\n\n      final d5 = afterDelta[4] as TextInsert;\n      expect(\n        d5.text,\n        ' by Tim Berners-Lee transformed how we access information.$text2$text3',\n      );\n      expect(d5.attributes, null);\n    });\n\n    test('replace markdown text with selection from middle to end', () async {\n      final document = buildTestDocument();\n      final editorState = EditorState(document: document);\n\n      editorState.selection = Selection(\n        start: Position(\n          path: [0],\n          offset: text1.length + text2.length,\n        ),\n        end: Position(\n          path: [0],\n          offset: text1.length + text2.length + text3.length,\n        ),\n      );\n\n      final markdownText =\n          '''**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.''';\n      final markdownTextRobot = MarkdownTextRobot(\n        editorState: editorState,\n      );\n      await markdownTextRobot.replace(\n        selection: editorState.selection!,\n        markdownText: markdownText,\n      );\n\n      final afterDelta = editorState.document.root.children[0].delta!.toList();\n      expect(afterDelta.length, 7);\n\n      final d1 = afterDelta[0] as TextInsert;\n      expect(\n        d1.text,\n        text1 + text2,\n      );\n      expect(d1.attributes, null);\n\n      final d2 = afterDelta[1] as TextInsert;\n      expect(d2.text, 'Email');\n      expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});\n\n      final d3 = afterDelta[2] as TextInsert;\n      expect(\n        d3.text,\n        ' became widespread, and instant messaging services like ',\n      );\n      expect(d3.attributes, null);\n\n      final d4 = afterDelta[3] as TextInsert;\n      expect(d4.text, 'ICQ');\n      expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});\n\n      final d5 = afterDelta[4] as TextInsert;\n      expect(d5.text, ' and ');\n      expect(d5.attributes, null);\n\n      final d6 = afterDelta[5] as TextInsert;\n      expect(\n        d6.text,\n        'AOL Instant Messenger',\n      );\n      expect(d6.attributes, {AppFlowyRichTextKeys.bold: true});\n\n      final d7 = afterDelta[6] as TextInsert;\n      expect(\n        d7.text,\n        ' gained tremendous popularity, allowing for seamless real-time text communication across the globe.',\n      );\n      expect(d7.attributes, null);\n    });\n\n    test('replace markdown text with selection from start to end', () async {\n      final text1 =\n          '''The introduction of the World Wide Web in the early 1990s marked a turning point.''';\n      final text2 =\n          '''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption.''';\n      final text3 =\n          '''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.''';\n\n      final document = Document(\n        root: pageNode(\n          children: [\n            paragraphNode(delta: Delta()..insert(text1)),\n            paragraphNode(delta: Delta()..insert(text2)),\n            paragraphNode(delta: Delta()..insert(text3)),\n          ],\n        ),\n      );\n      final editorState = EditorState(document: document);\n\n      editorState.selection = Selection(\n        start: Position(path: [0]),\n        end: Position(path: [0], offset: text1.length),\n      );\n\n      final markdownText = '''1. $text1\n\n2. $text1\n\n3. $text1''';\n      final markdownTextRobot = MarkdownTextRobot(\n        editorState: editorState,\n      );\n      await markdownTextRobot.replace(\n        selection: editorState.selection!,\n        markdownText: markdownText,\n      );\n\n      final nodes = editorState.document.root.children;\n      expect(nodes.length, 5);\n\n      final d1 = nodes[0].delta!.toList()[0] as TextInsert;\n      expect(d1.text, text1);\n      expect(d1.attributes, null);\n      expect(nodes[0].type, NumberedListBlockKeys.type);\n\n      final d2 = nodes[1].delta!.toList()[0] as TextInsert;\n      expect(d2.text, text1);\n      expect(d2.attributes, null);\n      expect(nodes[1].type, NumberedListBlockKeys.type);\n\n      final d3 = nodes[2].delta!.toList()[0] as TextInsert;\n      expect(d3.text, text1);\n      expect(d3.attributes, null);\n      expect(nodes[2].type, NumberedListBlockKeys.type);\n\n      final d4 = nodes[3].delta!.toList()[0] as TextInsert;\n      expect(d4.text, text2);\n      expect(d4.attributes, null);\n\n      final d5 = nodes[4].delta!.toList()[0] as TextInsert;\n      expect(d5.text, text3);\n      expect(d5.attributes, null);\n    });\n  });\n\n  group('markdown text robot - replace in multiple lines:', () {\n    final text1 =\n        '''The introduction of the World Wide Web in the early 1990s marked a turning point. ''';\n    final text2 =\n        '''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption. ''';\n    final text3 =\n        '''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.''';\n\n    Document buildTestDocument() {\n      return Document(\n        root: pageNode(\n          children: [\n            paragraphNode(delta: Delta()..insert(text1)),\n            paragraphNode(delta: Delta()..insert(text2)),\n            paragraphNode(delta: Delta()..insert(text3)),\n          ],\n        ),\n      );\n    }\n\n    // 1. create a document with 3 paragraph nodes\n    // 2. use the text robot to replace the selected content in the multiple lines\n    // 3. check the document\n    test(\n        'the selection starts with the first paragraph and ends with the middle of second paragraph',\n        () async {\n      final document = buildTestDocument();\n      final editorState = EditorState(document: document);\n\n      editorState.selection = Selection(\n        start: Position(\n          path: [0],\n        ),\n        end: Position(\n          path: [1],\n          offset: text2.length -\n              ', opening the floodgates for mass adoption. '.length,\n        ),\n      );\n\n      final markdownText =\n          '''The **introduction** of the World Wide Web in the *early 1990s* marked a significant turning point.\n\nTim Berners-Lee's **revolutionary invention** made the internet accessible to non-technical users''';\n      final markdownTextRobot = MarkdownTextRobot(\n        editorState: editorState,\n      );\n      await markdownTextRobot.replace(\n        selection: editorState.selection!,\n        markdownText: markdownText,\n      );\n\n      final afterNodes = editorState.document.root.children;\n      expect(afterNodes.length, 3);\n\n      {\n        // first paragraph\n        final delta1 = afterNodes[0].delta!.toList();\n        expect(delta1.length, 5);\n\n        final d1 = delta1[0] as TextInsert;\n        expect(d1.text, 'The ');\n        expect(d1.attributes, null);\n\n        final d2 = delta1[1] as TextInsert;\n        expect(d2.text, 'introduction');\n        expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});\n\n        final d3 = delta1[2] as TextInsert;\n        expect(d3.text, ' of the World Wide Web in the ');\n        expect(d3.attributes, null);\n\n        final d4 = delta1[3] as TextInsert;\n        expect(d4.text, 'early 1990s');\n        expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});\n\n        final d5 = delta1[4] as TextInsert;\n        expect(d5.text, ' marked a significant turning point.');\n        expect(d5.attributes, null);\n      }\n\n      {\n        // second paragraph\n        final delta2 = afterNodes[1].delta!.toList();\n        expect(delta2.length, 3);\n\n        final d1 = delta2[0] as TextInsert;\n        expect(d1.text, \"Tim Berners-Lee's \");\n        expect(d1.attributes, null);\n\n        final d2 = delta2[1] as TextInsert;\n        expect(d2.text, \"revolutionary invention\");\n        expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});\n\n        final d3 = delta2[2] as TextInsert;\n        expect(\n          d3.text,\n          \" made the internet accessible to non-technical users, opening the floodgates for mass adoption. \",\n        );\n        expect(d3.attributes, null);\n      }\n\n      {\n        // third paragraph\n        final delta3 = afterNodes[2].delta!.toList();\n        expect(delta3.length, 1);\n\n        final d1 = delta3[0] as TextInsert;\n        expect(d1.text, text3);\n        expect(d1.attributes, null);\n      }\n    });\n\n    test(\n        'the selection starts with the middle of the first paragraph and ends with the middle of last paragraph',\n        () async {\n      final document = buildTestDocument();\n      final editorState = EditorState(document: document);\n\n      editorState.selection = Selection(\n        start: Position(\n          path: [0],\n          offset: 'The introduction of the World Wide Web'.length,\n        ),\n        end: Position(\n          path: [2],\n          offset:\n              'Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity'\n                  .length,\n        ),\n      );\n\n      final markdownText =\n          ''' in the **early 1990s** marked a *significant turning point* in technological history.\n\nTim Berners-Lee's **revolutionary invention** made the internet accessible to non-technical users, opening the floodgates for *unprecedented mass adoption*.\n\nEmail became **widely prevalent**, and instant messaging services like *ICQ* and *AOL Instant Messenger* gained tremendous popularity\n          ''';\n      final markdownTextRobot = MarkdownTextRobot(\n        editorState: editorState,\n      );\n      await markdownTextRobot.replace(\n        selection: editorState.selection!,\n        markdownText: markdownText,\n      );\n\n      final afterNodes = editorState.document.root.children;\n      expect(afterNodes.length, 3);\n\n      {\n        // first paragraph\n        final delta1 = afterNodes[0].delta!.toList();\n        expect(delta1.length, 5);\n\n        final d1 = delta1[0] as TextInsert;\n        expect(d1.text, 'The introduction of the World Wide Web in the ');\n        expect(d1.attributes, null);\n\n        final d2 = delta1[1] as TextInsert;\n        expect(d2.text, 'early 1990s');\n        expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});\n\n        final d3 = delta1[2] as TextInsert;\n        expect(d3.text, ' marked a ');\n        expect(d3.attributes, null);\n\n        final d4 = delta1[3] as TextInsert;\n        expect(d4.text, 'significant turning point');\n        expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});\n\n        final d5 = delta1[4] as TextInsert;\n        expect(d5.text, ' in technological history.');\n        expect(d5.attributes, null);\n      }\n\n      {\n        // second paragraph\n        final delta2 = afterNodes[1].delta!.toList();\n        expect(delta2.length, 5);\n\n        final d1 = delta2[0] as TextInsert;\n        expect(d1.text, \"Tim Berners-Lee's \");\n        expect(d1.attributes, null);\n\n        final d2 = delta2[1] as TextInsert;\n        expect(d2.text, \"revolutionary invention\");\n        expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});\n\n        final d3 = delta2[2] as TextInsert;\n        expect(\n          d3.text,\n          \" made the internet accessible to non-technical users, opening the floodgates for \",\n        );\n        expect(d3.attributes, null);\n\n        final d4 = delta2[3] as TextInsert;\n        expect(d4.text, \"unprecedented mass adoption\");\n        expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});\n\n        final d5 = delta2[4] as TextInsert;\n        expect(d5.text, \".\");\n        expect(d5.attributes, null);\n      }\n\n      {\n        // third paragraph\n        // third paragraph\n        final delta3 = afterNodes[2].delta!.toList();\n        expect(delta3.length, 7);\n\n        final d1 = delta3[0] as TextInsert;\n        expect(d1.text, \"Email became \");\n        expect(d1.attributes, null);\n\n        final d2 = delta3[1] as TextInsert;\n        expect(d2.text, \"widely prevalent\");\n        expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});\n\n        final d3 = delta3[2] as TextInsert;\n        expect(d3.text, \", and instant messaging services like \");\n        expect(d3.attributes, null);\n\n        final d4 = delta3[3] as TextInsert;\n        expect(d4.text, \"ICQ\");\n        expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});\n\n        final d5 = delta3[4] as TextInsert;\n        expect(d5.text, \" and \");\n        expect(d5.attributes, null);\n\n        final d6 = delta3[5] as TextInsert;\n        expect(d6.text, \"AOL Instant Messenger\");\n        expect(d6.attributes, {AppFlowyRichTextKeys.italic: true});\n\n        final d7 = delta3[6] as TextInsert;\n        expect(\n          d7.text,\n          \" gained tremendous popularity, allowing for real-time text communication.\",\n        );\n        expect(d7.attributes, null);\n      }\n    });\n\n    test(\n        'the length of the returned response less than the length of the selected text',\n        () async {\n      final document = buildTestDocument();\n      final editorState = EditorState(document: document);\n\n      editorState.selection = Selection(\n        start: Position(\n          path: [0],\n          offset: 'The introduction of the World Wide Web'.length,\n        ),\n        end: Position(\n          path: [2],\n          offset:\n              'Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity'\n                  .length,\n        ),\n      );\n\n      final markdownText =\n          ''' in the **early 1990s** marked a *significant turning point* in technological history.''';\n      final markdownTextRobot = MarkdownTextRobot(\n        editorState: editorState,\n      );\n      await markdownTextRobot.replace(\n        selection: editorState.selection!,\n        markdownText: markdownText,\n      );\n\n      final afterNodes = editorState.document.root.children;\n      expect(afterNodes.length, 2);\n\n      {\n        // first paragraph\n        final delta1 = afterNodes[0].delta!.toList();\n        expect(delta1.length, 5);\n\n        final d1 = delta1[0] as TextInsert;\n        expect(d1.text, \"The introduction of the World Wide Web in the \");\n        expect(d1.attributes, null);\n\n        final d2 = delta1[1] as TextInsert;\n        expect(d2.text, \"early 1990s\");\n        expect(d2.attributes, {AppFlowyRichTextKeys.bold: true});\n\n        final d3 = delta1[2] as TextInsert;\n        expect(d3.text, \" marked a \");\n        expect(d3.attributes, null);\n\n        final d4 = delta1[3] as TextInsert;\n        expect(d4.text, \"significant turning point\");\n        expect(d4.attributes, {AppFlowyRichTextKeys.italic: true});\n\n        final d5 = delta1[4] as TextInsert;\n        expect(d5.text, \" in technological history.\");\n        expect(d5.attributes, null);\n      }\n\n      {\n        // second paragraph\n        final delta2 = afterNodes[1].delta!.toList();\n        expect(delta2.length, 1);\n\n        final d1 = delta2[0] as TextInsert;\n        expect(d1.text, \", allowing for real-time text communication.\");\n        expect(d1.attributes, null);\n      }\n    });\n  });\n}\n\nconst _sample1 = '''# The Curious Cat\n\nOnce 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.\n\nOne 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.\n\nWhiskers became the village's hero, guiding everyone on exciting adventures.''';\n\nconst _liveRefreshSample2 = [\n  \"##\",\n  \" The\",\n  \" Decision\",\n  \"\\n\\n\",\n  \"-\",\n  \" Ar\",\n  \"ia\",\n  \" found\",\n  \" an\",\n  \" ancient map\",\n  \" in her grandmother\",\n  \"'s attic\",\n  \".\\n\",\n  \"-\",\n  \" The map\",\n  \" hinted at\",\n  \" a\",\n  \" mystical\",\n  \" place\",\n  \" known\",\n  \" as\",\n  \" the\",\n  \" En\",\n  \"ch\",\n  \"anted\",\n  \" Forest\",\n  \".\\n\",\n  \"-\",\n  \" Legends\",\n  \" spoke\",\n  \" of\",\n  \" the\",\n  \" forest\",\n  \" as\",\n  \" a realm\",\n  \" where dreams\",\n  \" came\",\n  \" to\",\n  \" life\",\n  \".\\n\\n\",\n];\n\nconst _liveRefreshSample3 = [\n  \"##\",\n  \" The\",\n  \" Preparation\\n\\n\",\n  \"Before\",\n  \" embarking\",\n  \" on\",\n  \" her\",\n  \" journey\",\n  \", Aria prepared\",\n  \" meticulously:\\n\\n\",\n  \"1\",\n  \".\",\n  \" **\",\n  \"Gather\",\n  \" Supplies**\",\n  \"  \\n\",\n  \"  \",\n  \" -\",\n  \" A\",\n  \" sturdy\",\n  \" backpack\",\n  \"\\n\",\n  \"  \",\n  \" -\",\n  \" A\",\n  \" compass\",\n  \" and\",\n  \" a map\",\n  \"\\n  \",\n  \" -\",\n  \" Pro\",\n  \"visions\",\n  \" for\",\n  \" the\",\n  \" week\",\n  \"\\n\\n\",\n  \"2\",\n  \".\",\n  \" **\",\n  \"Seek\",\n  \" Guidance\",\n  \"**\",\n  \"  \\n\",\n  \"  \",\n  \" -\",\n  \" Vis\",\n  \"ited\",\n  \" the\",\n  \" village\",\n  \" elder for advice\",\n  \"\\n\",\n  \"   -\",\n  \" List\",\n  \"ened\",\n  \" to\",\n  \" tales\",\n  \" of past\",\n  \" advent\",\n  \"urers\",\n  \"\\n\\n\",\n  \"3\",\n  \".\",\n  \" **\",\n  \"Shar\",\n  \"pen\",\n  \" Skills\",\n  \"**\",\n  \"  \\n\",\n  \"  \",\n  \" -\",\n  \" Pract\",\n  \"iced\",\n  \" arch\",\n  \"ery\",\n  \" and\",\n  \" swordsmanship\",\n  \"\\n  \",\n  \" -\",\n  \" Enhanced\",\n  \" survival skills\",\n];\n\nconst _liveRefreshSample4 = [\n  \"Sure\",\n  \", let's\",\n  \" provide an\",\n  \" alternative Rust\",\n  \" implementation for the Two\",\n  \" Sum\",\n  \" problem\",\n  \",\",\n  \" focusing\",\n  \" on\",\n  \" clarity\",\n  \" and efficiency\",\n  \" but with\",\n  \" a slightly\",\n  \" different approach\",\n  \":\\n\\n\",\n  \"```\",\n  \"rust\",\n  \"\\nfn two\",\n  \"_sum\",\n  \"(nums\",\n  \": &[\",\n  \"i\",\n  \"32\",\n  \"],\",\n  \" target\",\n  \":\",\n  \" i\",\n  \"32\",\n  \")\",\n  \" ->\",\n  \" Vec\",\n  \"<(usize\",\n  \", usize\",\n  \")>\",\n  \" {\\n\",\n  \"   \",\n  \" let\",\n  \" mut results\",\n  \" = Vec::\",\n  \"new\",\n  \"();\\n\",\n  \"   \",\n  \" let mut\",\n  \" map\",\n  \" =\",\n  \" std::collections\",\n  \"::\",\n  \"HashMap\",\n  \"::\",\n  \"new\",\n  \"();\\n\\n   \",\n  \" for (\",\n  \"i,\",\n  \" &num\",\n  \") in\",\n  \" nums.iter\",\n  \"().enumer\",\n  \"ate()\",\n  \" {\\n        let\",\n  \" complement\",\n  \" = target\",\n  \" - num\",\n  \";\\n\",\n  \"       \",\n  \" if\",\n  \" let\",\n  \" Some(&\",\n  \"j)\",\n  \" =\",\n  \" map\",\n  \".get(&\",\n  \"complement\",\n  \") {\\n\",\n  \"            results\",\n  \".push((\",\n  \"j\",\n  \",\",\n  \" i));\\n        }\\n\",\n  \"       \",\n  \" map\",\n  \".insert\",\n  \"(num\",\n  \", i\",\n  \");\\n\",\n  \"   \",\n  \" }\\n\\n   \",\n  \" results\\n\",\n  \"}\\n\\n\",\n  \"fn\",\n  \" main()\",\n  \" {\\n\",\n  \"   \",\n  \" let\",\n  \" nums\",\n  \" =\",\n  \" vec![2, \",\n  \"7\",\n  \",\",\n  \" 11, 15];\\n\",\n  \"    let\",\n  \" target\",\n  \" =\",\n  \" \",\n  \"9\",\n  \";\\n\\n\",\n  \"   \",\n  \" let\",\n  \" pairs\",\n  \" = two\",\n  \"_sum\",\n  \"(&\",\n  \"nums\",\n  \",\",\n  \" target);\\n\",\n  \"   \",\n  \" if\",\n  \" pairs\",\n  \".is\",\n  \"_empty()\",\n  \" {\\n       \",\n  \" println\",\n  \"!(\\\"\",\n  \"No\",\n  \" two\",\n  \" sum solution\",\n  \" found\\\");\\n\",\n  \"   \",\n  \" }\",\n  \" else {\\n        for\",\n  \" (\",\n  \"i\",\n  \", j\",\n  \") in\",\n  \" pairs {\\n\",\n  \"            println\",\n  \"!(\\\"Indices\",\n  \":\",\n  \" {},\",\n  \" {}\\\",\",\n  \" i\",\n  \",\",\n  \" j\",\n  \");\\n       \",\n  \" }\\n   \",\n  \" }\\n}\\n\",\n  \"```\\n\\n\",\n];\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/document/text_robot/text_robot_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/base/text_robot.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('text robot:', () {\n    setUpAll(() {\n      Log.shared.disableLog = true;\n    });\n\n    tearDownAll(() {\n      Log.shared.disableLog = false;\n    });\n\n    test('auto insert text with sentence mode (1)', () async {\n      final editorState = EditorState.blank();\n      editorState.selection = Selection.collapsed(Position(path: [0]));\n      final textRobot = TextRobot(\n        editorState: editorState,\n      );\n      for (final text in _sample1) {\n        await textRobot.autoInsertTextSync(\n          text,\n          separator: r'\\n\\n',\n          inputType: TextRobotInputType.sentence,\n          delay: Duration.zero,\n        );\n      }\n\n      final p1 = editorState.document.nodeAtPath([0])!.delta!.toPlainText();\n      final p2 = editorState.document.nodeAtPath([1])!.delta!.toPlainText();\n      final p3 = editorState.document.nodeAtPath([2])!.delta!.toPlainText();\n\n      expect(\n        p1,\n        '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. ',\n      );\n      expect(\n        p2,\n        '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. ',\n      );\n      expect(\n        p3,\n        '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.',\n      );\n    });\n\n    test('auto insert text with sentence mode (2)', () async {\n      final editorState = EditorState.blank();\n      editorState.selection = Selection.collapsed(Position(path: [0]));\n      final textRobot = TextRobot(\n        editorState: editorState,\n      );\n\n      var breakCount = 0;\n      for (final text in _sample2) {\n        if (text.contains('\\n\\n')) {\n          breakCount++;\n        }\n        await textRobot.autoInsertTextSync(\n          text,\n          separator: r'\\n\\n',\n          inputType: TextRobotInputType.sentence,\n          delay: Duration.zero,\n        );\n      }\n\n      final len = editorState.document.root.children.length;\n      expect(len, breakCount + 1);\n      expect(len, 7);\n\n      final p1 = editorState.document.nodeAtPath([0])!.delta!.toPlainText();\n      final p2 = editorState.document.nodeAtPath([1])!.delta!.toPlainText();\n      final p3 = editorState.document.nodeAtPath([2])!.delta!.toPlainText();\n      final p4 = editorState.document.nodeAtPath([3])!.delta!.toPlainText();\n      final p5 = editorState.document.nodeAtPath([4])!.delta!.toPlainText();\n      final p6 = editorState.document.nodeAtPath([5])!.delta!.toPlainText();\n      final p7 = editorState.document.nodeAtPath([6])!.delta!.toPlainText();\n\n      expect(\n        p1,\n        '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.',\n      );\n      expect(\n        p2,\n        '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.',\n      );\n      expect(\n        p3,\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 owls to playful otters. Together, they shared stories, solved problems, and looked out for one another.',\n      );\n      expect(\n        p4,\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. Elara, determined to help, turned to her animal friends for guidance.',\n      );\n      expect(\n        p5,\n        '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.',\n      );\n      expect(\n        p6,\n        '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.',\n      );\n      expect(\n        p7,\n        '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.',\n      );\n    });\n  });\n}\n\nfinal _sample1 = [\n  \"In\",\n  \" a quaint\",\n  \" village\",\n  \" nestled\",\n  \" between\",\n  \" rolling\",\n  \" hills\",\n  \",\",\n  \" a\",\n  \" young\",\n  \" girl\",\n  \" named\",\n  \" El\",\n  \"ara discovered\",\n  \" a hidden\",\n  \" garden\",\n  \".\",\n  \" She stumbled\",\n  \" upon\",\n  \" it\",\n  \" while\",\n  \" chasing\",\n  \" a\",\n  \" misch\",\n  \"iev\",\n  \"ous rabbit\",\n  \" through\",\n  \" a\",\n  \" narrow,\",\n  \" winding path\",\n  \".\",\n  \" \\n\\n\",\n  \"The\",\n  \" garden\",\n  \" was\",\n  \" a\",\n  \" vibrant\",\n  \" oasis\",\n  \",\",\n  \" br\",\n  \"imming with\",\n  \" colorful\",\n  \" flowers\",\n  \" and whisper\",\n  \"ing\",\n  \" trees\",\n  \".\",\n  \" El\",\n  \"ara\",\n  \" felt\",\n  \" an inexp\",\n  \"licable\",\n  \" connection\",\n  \" to\",\n  \" the\",\n  \" place,\",\n  \" as\",\n  \" if\",\n  \" it held\",\n  \" secrets\",\n  \" from\",\n  \" a\",\n  \" forgotten\",\n  \" time\",\n  \".\",\n  \" \\n\\n\",\n  \"Determ\",\n  \"ined to\",\n  \" uncover\",\n  \" its\",\n  \" mysteries\",\n  \",\",\n  \" she\",\n  \" visited\",\n  \" daily,\",\n  \" unravel\",\n  \"ing\",\n  \" tales\",\n  \" of\",\n  \" ancient\",\n  \" magic\",\n  \" and\",\n  \" wisdom\",\n  \".\",\n  \" The\",\n  \" garden transformed\",\n  \" her\",\n  \" spirit\",\n  \", teaching\",\n  \" her the\",\n  \" importance of harmony and\",\n  \" the\",\n  \" beauty\",\n  \" of\",\n  \" nature\",\n  \"'s wonders.\",\n];\n\nfinal _sample2 = [\n  \"Once\",\n  \" upon\",\n  \" a\",\n  \" time\",\n  \" in\",\n  \" the small\",\n  \",\",\n  \" whimsical\",\n  \" village\",\n  \" of\",\n  \" Green\",\n  \"h\",\n  \"ollow\",\n  \",\",\n  \" nestled\",\n  \" between\",\n  \" rolling hills\",\n  \" and\",\n  \" lush\",\n  \" forests\",\n  \",\",\n  \" there\",\n  \" lived\",\n  \" a young\",\n  \" girl\",\n  \" named\",\n  \" Elara.\",\n  \" Unlike the\",\n  \" other\",\n  \" villagers\",\n  \",\",\n  \" El\",\n  \"ara\",\n  \" had\",\n  \" a unique\",\n  \" gift\",\n  \":\",\n  \" she could\",\n  \" communicate\",\n  \" with\",\n  \" animals\",\n  \".\",\n  \" This\",\n  \" extraordinary\",\n  \" ability\",\n  \" made\",\n  \" her both a\",\n  \" beloved\",\n  \" and\",\n  \" mysterious\",\n  \" figure\",\n  \" in\",\n  \" Green\",\n  \"h\",\n  \"ollow\",\n  \".\\n\\n\",\n  \"One\",\n  \" crisp\",\n  \" autumn\",\n  \" morning,\",\n  \" as\",\n  \" golden\",\n  \" leaves\",\n  \" danced\",\n  \" in\",\n  \" the\",\n  \" breeze\",\n  \", El\",\n  \"ara heard\",\n  \" a distressed\",\n  \" call\",\n  \" from\",\n  \" the\",\n  \" forest\",\n  \".\",\n  \" Following\",\n  \" the\",\n  \" sound\",\n  \",\",\n  \" she\",\n  \" discovered\",\n  \" a\",\n  \" young\",\n  \" fox\",\n  \" trapped\",\n  \" in\",\n  \" a\",\n  \" hunter's\",\n  \" snare\",\n  \".\",\n  \" With\",\n  \" gentle\",\n  \" hands\",\n  \" and\",\n  \" a\",\n  \" calming\",\n  \" voice\",\n  \",\",\n  \" she\",\n  \" freed\",\n  \" the\",\n  \" frightened\",\n  \" creature\",\n  \", who\",\n  \" introduced\",\n  \" himself\",\n  \" as Ruf\",\n  \"us.\",\n  \" Gr\",\n  \"ateful\",\n  \" for\",\n  \" her\",\n  \" help\",\n  \",\",\n  \" Rufus promised\",\n  \" to assist\",\n  \" Elara\",\n  \" whenever\",\n  \" she\",\n  \" needed.\\n\\n\",\n  \"Word\",\n  \" of\",\n  \" Elara\",\n  \"'s kindness\",\n  \" spread among\",\n  \" the forest\",\n  \" animals\",\n  \",\",\n  \" and soon\",\n  \" she\",\n  \" found\",\n  \" herself\",\n  \" surrounded\",\n  \" by\",\n  \" a\",\n  \" diverse\",\n  \" group\",\n  \" of\",\n  \" animal\",\n  \" friends\",\n  \",\",\n  \" from\",\n  \" wise\",\n  \" old ow\",\n  \"ls to playful\",\n  \" ot\",\n  \"ters.\",\n  \" Together,\",\n  \" they\",\n  \" shared stories\",\n  \",\",\n  \" solved problems\",\n  \",\",\n  \" and\",\n  \" looked\",\n  \" out\",\n  \" for\",\n  \" one\",\n  \" another\",\n  \".\\n\\n\",\n  \"One\",\n  \" day\",\n  \", the village faced\",\n  \" an unexpected\",\n  \" threat\",\n  \":\",\n  \" a\",\n  \" severe\",\n  \" drought\",\n  \" that\",\n  \" threatened\",\n  \" their\",\n  \" crops\",\n  \" and\",\n  \" water supply\",\n  \".\",\n  \" The\",\n  \" villagers\",\n  \" grew\",\n  \" anxious\",\n  \",\",\n  \" unsure\",\n  \" of\",\n  \" how to\",\n  \" cope\",\n  \" with\",\n  \" the\",\n  \" impending\",\n  \" scarcity\",\n  \".\",\n  \" El\",\n  \"ara\",\n  \",\",\n  \" determined\",\n  \" to\",\n  \" help\",\n  \",\",\n  \" turned\",\n  \" to her\",\n  \" animal friends\",\n  \" for\",\n  \" guidance\",\n  \".\\n\\nThe\",\n  \" animals\",\n  \" led\",\n  \" El\",\n  \"ara\",\n  \" to\",\n  \" a\",\n  \" hidden\",\n  \" spring\",\n  \" deep\",\n  \" within\",\n  \" the forest,\",\n  \" a source\",\n  \" of\",\n  \" fresh\",\n  \" water unknown\",\n  \" to the\",\n  \" villagers\",\n  \".\",\n  \" With\",\n  \" Ruf\",\n  \"us's\",\n  \" clever planning\",\n  \" and the\",\n  \" ot\",\n  \"ters\",\n  \"'\",\n  \" help\",\n  \" in directing\",\n  \" the\",\n  \" flow\",\n  \",\",\n  \" they\",\n  \" managed\",\n  \" to\",\n  \" channel the\",\n  \" spring\",\n  \" water\",\n  \" to\",\n  \" the\",\n  \" village,\",\n  \" saving the\",\n  \" crops\",\n  \" and\",\n  \" quenching\",\n  \" the\",\n  \" villagers\",\n  \"'\",\n  \" thirst\",\n  \".\\n\\n\",\n  \"Gr\",\n  \"ateful and\",\n  \" amazed,\",\n  \" the\",\n  \" villagers\",\n  \" hailed El\",\n  \"ara as\",\n  \" a\",\n  \" hero\",\n  \".\",\n  \" They\",\n  \" came\",\n  \" to\",\n  \" understand the\",\n  \" importance\",\n  \" of living\",\n  \" harmon\",\n  \"iously\",\n  \" with\",\n  \" nature\",\n  \" and\",\n  \" the\",\n  \" wonders\",\n  \" that\",\n  \" could\",\n  \" be\",\n  \" achieved\",\n  \" through kindness\",\n  \" and cooperation\",\n  \".\\n\\nFrom\",\n  \" that day\",\n  \" on\",\n  \",\",\n  \" Greenh\",\n  \"ollow\",\n  \" thr\",\n  \"ived\",\n  \" as\",\n  \" a\",\n  \" community\",\n  \" where\",\n  \" humans\",\n  \" and\",\n  \" animals\",\n  \" lived together\",\n  \" in\",\n  \" harmony\",\n  \",\",\n  \" cher\",\n  \"ishing\",\n  \" the\",\n  \" bonds that\",\n  \" El\",\n  \"ara\",\n  \" had\",\n  \" helped\",\n  \" forge\",\n  \".\",\n  \" And whenever\",\n  \" challenges arose\",\n  \", the\",\n  \" villagers\",\n  \" knew\",\n  \" they\",\n  \" could\",\n  \" rely on\",\n  \" El\",\n  \"ara and\",\n  \" her\",\n  \" extraordinary\",\n  \" friends\",\n  \" to\",\n  \" guide them\",\n  \" through\",\n  \",\",\n  \" ensuring\",\n  \" that\",\n  \" the\",\n  \" spirit\",\n  \" of\",\n  \" unity\",\n  \" and\",\n  \" compassion\",\n  \" always prevailed.\",\n];\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart'\n    hide quoteNode, QuoteBlockKeys;\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('turn into:', () {\n    Document createDocument(List<Node> nodes) {\n      final document = Document.blank();\n      document.insert([0], nodes);\n      return document;\n    }\n\n    Future<void> checkTurnInto(\n      Document document,\n      String originalType,\n      String originalText, {\n      Selection? selection,\n      String? toType,\n      int? level,\n      void Function(EditorState editorState, Node node)? afterTurnInto,\n    }) async {\n      final editorState = EditorState(document: document);\n      final types = toType == null\n          ? EditorOptionActionType.turnInto.supportTypes\n          : [toType];\n      for (final type in types) {\n        if (type == originalType || type == SubPageBlockKeys.type) {\n          continue;\n        }\n\n        editorState.selectionType = SelectionType.block;\n        editorState.selection =\n            selection ?? Selection.collapsed(Position(path: [0]));\n\n        final node = editorState.getNodeAtPath([0])!;\n        expect(node.type, originalType);\n        final result = await BlockActionOptionCubit.turnIntoBlock(\n          type,\n          node,\n          editorState,\n          level: level,\n        );\n        expect(result, true);\n        final newNode = editorState.getNodeAtPath([0])!;\n        expect(newNode.type, type);\n        expect(newNode.delta!.toPlainText(), originalText);\n        afterTurnInto?.call(\n          editorState,\n          newNode,\n        );\n\n        // turn it back the originalType for the next test\n        editorState.selectionType = SelectionType.block;\n        editorState.selection = selection ??\n            Selection.collapsed(\n              Position(path: [0]),\n            );\n        await BlockActionOptionCubit.turnIntoBlock(\n          originalType,\n          newNode,\n          editorState,\n        );\n        expect(result, true);\n      }\n    }\n\n    setUpAll(() {\n      Log.shared.disableLog = true;\n    });\n\n    tearDownAll(() {\n      Log.shared.disableLog = false;\n    });\n\n    test('from heading to another blocks', () async {\n      const text = 'Heading 1';\n      final document = createDocument([headingNode(level: 1, text: text)]);\n      await checkTurnInto(document, HeadingBlockKeys.type, text);\n    });\n\n    test('from paragraph to another blocks', () async {\n      const text = 'Paragraph';\n      final document = createDocument([paragraphNode(text: text)]);\n      await checkTurnInto(\n        document,\n        ParagraphBlockKeys.type,\n        text,\n      );\n    });\n\n    test('from quote list to another blocks', () async {\n      const text = 'Quote';\n      final document = createDocument([\n        quoteNode(\n          delta: Delta()..insert(text),\n        ),\n      ]);\n      await checkTurnInto(\n        document,\n        QuoteBlockKeys.type,\n        text,\n      );\n    });\n\n    test('from todo list to another blocks', () async {\n      const text = 'Todo';\n      final document = createDocument([\n        todoListNode(\n          checked: false,\n          text: text,\n        ),\n      ]);\n      await checkTurnInto(\n        document,\n        TodoListBlockKeys.type,\n        text,\n      );\n    });\n\n    test('from bulleted list to another blocks', () async {\n      const text = 'bulleted list';\n      final document = createDocument([\n        bulletedListNode(\n          text: text,\n        ),\n      ]);\n      await checkTurnInto(\n        document,\n        BulletedListBlockKeys.type,\n        text,\n      );\n    });\n\n    test('from numbered list to another blocks', () async {\n      const text = 'numbered list';\n      final document = createDocument([\n        numberedListNode(\n          delta: Delta()..insert(text),\n        ),\n      ]);\n      await checkTurnInto(\n        document,\n        NumberedListBlockKeys.type,\n        text,\n      );\n    });\n\n    test('from callout to another blocks', () async {\n      const text = 'callout';\n      final document = createDocument([\n        calloutNode(\n          delta: Delta()..insert(text),\n        ),\n      ]);\n      await checkTurnInto(\n        document,\n        CalloutBlockKeys.type,\n        text,\n      );\n    });\n\n    for (final type in [\n      HeadingBlockKeys.type,\n    ]) {\n      test('from nested bulleted list to $type', () async {\n        const text = 'bulleted list';\n        const nestedText1 = 'nested bulleted list 1';\n        const nestedText2 = 'nested bulleted list 2';\n        const nestedText3 = 'nested bulleted list 3';\n        final document = createDocument([\n          bulletedListNode(\n            text: text,\n            children: [\n              bulletedListNode(\n                text: nestedText1,\n              ),\n              bulletedListNode(\n                text: nestedText2,\n              ),\n              bulletedListNode(\n                text: nestedText3,\n              ),\n            ],\n          ),\n        ]);\n        await checkTurnInto(\n          document,\n          BulletedListBlockKeys.type,\n          text,\n          toType: type,\n          afterTurnInto: (editorState, node) {\n            expect(node.type, type);\n            expect(node.children.length, 0);\n            expect(node.delta!.toPlainText(), text);\n\n            expect(editorState.document.root.children.length, 4);\n            expect(\n              editorState.document.root.children[1].type,\n              BulletedListBlockKeys.type,\n            );\n            expect(\n              editorState.document.root.children[1].delta!.toPlainText(),\n              nestedText1,\n            );\n            expect(\n              editorState.document.root.children[2].type,\n              BulletedListBlockKeys.type,\n            );\n            expect(\n              editorState.document.root.children[2].delta!.toPlainText(),\n              nestedText2,\n            );\n            expect(\n              editorState.document.root.children[3].type,\n              BulletedListBlockKeys.type,\n            );\n            expect(\n              editorState.document.root.children[3].delta!.toPlainText(),\n              nestedText3,\n            );\n          },\n        );\n      });\n    }\n\n    for (final type in [\n      HeadingBlockKeys.type,\n    ]) {\n      test('from nested numbered list to $type', () async {\n        const text = 'numbered list';\n        const nestedText1 = 'nested numbered list 1';\n        const nestedText2 = 'nested numbered list 2';\n        const nestedText3 = 'nested numbered list 3';\n        final document = createDocument([\n          numberedListNode(\n            delta: Delta()..insert(text),\n            children: [\n              numberedListNode(\n                delta: Delta()..insert(nestedText1),\n              ),\n              numberedListNode(\n                delta: Delta()..insert(nestedText2),\n              ),\n              numberedListNode(\n                delta: Delta()..insert(nestedText3),\n              ),\n            ],\n          ),\n        ]);\n        await checkTurnInto(\n          document,\n          NumberedListBlockKeys.type,\n          text,\n          toType: type,\n          afterTurnInto: (editorState, node) {\n            expect(node.type, type);\n            expect(node.children.length, 0);\n            expect(node.delta!.toPlainText(), text);\n\n            expect(editorState.document.root.children.length, 4);\n            expect(\n              editorState.document.root.children[1].type,\n              NumberedListBlockKeys.type,\n            );\n            expect(\n              editorState.document.root.children[1].delta!.toPlainText(),\n              nestedText1,\n            );\n            expect(\n              editorState.document.root.children[2].type,\n              NumberedListBlockKeys.type,\n            );\n            expect(\n              editorState.document.root.children[2].delta!.toPlainText(),\n              nestedText2,\n            );\n            expect(\n              editorState.document.root.children[3].type,\n              NumberedListBlockKeys.type,\n            );\n            expect(\n              editorState.document.root.children[3].delta!.toPlainText(),\n              nestedText3,\n            );\n          },\n        );\n      });\n    }\n\n    for (final type in [\n      HeadingBlockKeys.type,\n    ]) {\n      // numbered list, bulleted list, todo list\n      // before\n      // - numbered list 1\n      //   - nested list 1\n      // - bulleted list 2\n      //   - nested list 2\n      // - todo list 3\n      //   - nested list 3\n      // after\n      // - heading 1\n      // - nested list 1\n      // - heading 2\n      // - nested list 2\n      // - heading 3\n      // - nested list 3\n      test('from nested mixed list to $type', () async {\n        const text1 = 'numbered list 1';\n        const text2 = 'bulleted list 2';\n        const text3 = 'todo list 3';\n        const nestedText1 = 'nested list 1';\n        const nestedText2 = 'nested list 2';\n        const nestedText3 = 'nested list 3';\n        final document = createDocument([\n          numberedListNode(\n            delta: Delta()..insert(text1),\n            children: [\n              numberedListNode(\n                delta: Delta()..insert(nestedText1),\n              ),\n            ],\n          ),\n          bulletedListNode(\n            delta: Delta()..insert(text2),\n            children: [\n              bulletedListNode(\n                delta: Delta()..insert(nestedText2),\n              ),\n            ],\n          ),\n          todoListNode(\n            checked: false,\n            text: text3,\n            children: [\n              todoListNode(\n                checked: false,\n                text: nestedText3,\n              ),\n            ],\n          ),\n        ]);\n        await checkTurnInto(\n          document,\n          NumberedListBlockKeys.type,\n          text1,\n          toType: type,\n          selection: Selection(\n            start: Position(path: [0]),\n            end: Position(path: [2]),\n          ),\n          afterTurnInto: (editorState, node) {\n            final nodes = editorState.document.root.children;\n            expect(nodes.length, 6);\n            final texts = [\n              text1,\n              nestedText1,\n              text2,\n              nestedText2,\n              text3,\n              nestedText3,\n            ];\n            final types = [\n              type,\n              NumberedListBlockKeys.type,\n              type,\n              BulletedListBlockKeys.type,\n              type,\n              TodoListBlockKeys.type,\n            ];\n            for (var i = 0; i < 6; i++) {\n              expect(nodes[i].type, types[i]);\n              expect(nodes[i].children.length, 0);\n              expect(nodes[i].delta!.toPlainText(), texts[i]);\n            }\n          },\n        );\n      });\n    }\n\n    for (final type in [\n      ParagraphBlockKeys.type,\n      BulletedListBlockKeys.type,\n      NumberedListBlockKeys.type,\n      TodoListBlockKeys.type,\n      QuoteBlockKeys.type,\n      CalloutBlockKeys.type,\n    ]) {\n      // numbered list, bulleted list, todo list\n      // before\n      // - numbered list 1\n      //   - nested list 1\n      // - bulleted list 2\n      //   - nested list 2\n      // - todo list 3\n      //   - nested list 3\n      // after\n      // - new_list_type\n      //  - nested list 1\n      // - new_list_type\n      //  - nested list 2\n      // - new_list_type\n      //  - nested list 3\n      test('from nested mixed list to $type', () async {\n        const text1 = 'numbered list 1';\n        const text2 = 'bulleted list 2';\n        const text3 = 'todo list 3';\n        const nestedText1 = 'nested list 1';\n        const nestedText2 = 'nested list 2';\n        const nestedText3 = 'nested list 3';\n        final document = createDocument([\n          numberedListNode(\n            delta: Delta()..insert(text1),\n            children: [\n              numberedListNode(\n                delta: Delta()..insert(nestedText1),\n              ),\n            ],\n          ),\n          bulletedListNode(\n            delta: Delta()..insert(text2),\n            children: [\n              bulletedListNode(\n                delta: Delta()..insert(nestedText2),\n              ),\n            ],\n          ),\n          todoListNode(\n            checked: false,\n            text: text3,\n            children: [\n              todoListNode(\n                checked: false,\n                text: nestedText3,\n              ),\n            ],\n          ),\n        ]);\n        await checkTurnInto(\n          document,\n          NumberedListBlockKeys.type,\n          text1,\n          toType: type,\n          selection: Selection(\n            start: Position(path: [0]),\n            end: Position(path: [2]),\n          ),\n          afterTurnInto: (editorState, node) {\n            final nodes = editorState.document.root.children;\n            expect(nodes.length, 3);\n            final texts = [\n              text1,\n              text2,\n              text3,\n            ];\n            final nestedTexts = [\n              nestedText1,\n              nestedText2,\n              nestedText3,\n            ];\n            final types = [\n              NumberedListBlockKeys.type,\n              BulletedListBlockKeys.type,\n              TodoListBlockKeys.type,\n            ];\n            for (var i = 0; i < 3; i++) {\n              expect(nodes[i].type, type);\n              expect(nodes[i].children.length, 1);\n              expect(nodes[i].delta!.toPlainText(), texts[i]);\n              expect(nodes[i].children[0].type, types[i]);\n              expect(nodes[i].children[0].delta!.toPlainText(), nestedTexts[i]);\n            }\n          },\n        );\n      });\n    }\n\n    test('undo, redo', () async {\n      const text1 = 'numbered list 1';\n      const nestedText1 = 'nested list 1';\n      final document = createDocument([\n        numberedListNode(\n          delta: Delta()..insert(text1),\n          children: [\n            numberedListNode(\n              delta: Delta()..insert(nestedText1),\n            ),\n          ],\n        ),\n      ]);\n      await checkTurnInto(\n        document,\n        NumberedListBlockKeys.type,\n        text1,\n        toType: HeadingBlockKeys.type,\n        afterTurnInto: (editorState, node) {\n          expect(editorState.document.root.children.length, 2);\n          editorState.selection = Selection.collapsed(\n            Position(path: [0]),\n          );\n          KeyEventResult result = undoCommand.execute(editorState);\n          expect(result, KeyEventResult.handled);\n          expect(editorState.document.root.children.length, 1);\n          editorState.selection = Selection.collapsed(\n            Position(path: [0]),\n          );\n          result = redoCommand.execute(editorState);\n          expect(result, KeyEventResult.handled);\n          expect(editorState.document.root.children.length, 2);\n        },\n      );\n    });\n\n    test('calculate selection when turn into', () {\n      // Example:\n      // - bulleted list item 1\n      //   - bulleted list item 1-1\n      //   - bulleted list item 1-2\n      // - bulleted list item 2\n      //   - bulleted list item 2-1\n      //   - bulleted list item 2-2\n      // - bulleted list item 3\n      //   - bulleted list item 3-1\n      //   - bulleted list item 3-2\n      const text = 'bulleted list';\n      const nestedText = 'nested bulleted list';\n      final document = createDocument([\n        bulletedListNode(\n          text: '$text 1',\n          children: [\n            bulletedListNode(text: '$nestedText 1-1'),\n            bulletedListNode(text: '$nestedText 1-2'),\n          ],\n        ),\n        bulletedListNode(\n          text: '$text 2',\n          children: [\n            bulletedListNode(text: '$nestedText 2-1'),\n            bulletedListNode(text: '$nestedText 2-2'),\n          ],\n        ),\n        bulletedListNode(\n          text: '$text 3',\n          children: [\n            bulletedListNode(text: '$nestedText 3-1'),\n            bulletedListNode(text: '$nestedText 3-2'),\n          ],\n        ),\n      ]);\n      final editorState = EditorState(document: document);\n      final cubit = BlockActionOptionCubit(\n        editorState: editorState,\n        blockComponentBuilder: {},\n      );\n\n      // case 1: collapsed selection and the selection is in the top level\n      // and tap the turn into button at the [0]\n      final selection1 = Selection.collapsed(\n        Position(path: [0], offset: 1),\n      );\n      expect(\n        cubit.calculateTurnIntoSelection(\n          editorState.getNodeAtPath([0])!,\n          selection1,\n        ),\n        selection1,\n      );\n\n      // case 2: collapsed selection and the selection is in the nested level\n      // and tap the turn into button at the [0]\n      final selection2 = Selection.collapsed(\n        Position(path: [0, 0], offset: 1),\n      );\n      expect(\n        cubit.calculateTurnIntoSelection(\n          editorState.getNodeAtPath([0])!,\n          selection2,\n        ),\n        Selection.collapsed(Position(path: [0])),\n      );\n\n      // case 3, collapsed selection and the selection is in the nested level\n      // and tap the turn into button at the [0, 0]\n      final selection3 = Selection.collapsed(\n        Position(path: [0, 0], offset: 1),\n      );\n      expect(\n        cubit.calculateTurnIntoSelection(\n          editorState.getNodeAtPath([0, 0])!,\n          selection3,\n        ),\n        selection3,\n      );\n\n      // case 4, not collapsed selection and the selection is in the top level\n      // and tap the turn into button at the [0]\n      final selection4 = Selection(\n        start: Position(path: [0], offset: 1),\n        end: Position(path: [1], offset: 1),\n      );\n      expect(\n        cubit.calculateTurnIntoSelection(\n          editorState.getNodeAtPath([0])!,\n          selection4,\n        ),\n        selection4,\n      );\n\n      // case 5, not collapsed selection and the selection is in the nested level\n      // and tap the turn into button at the [0]\n      final selection5 = Selection(\n        start: Position(path: [0, 0], offset: 1),\n        end: Position(path: [0, 1], offset: 1),\n      );\n      expect(\n        cubit.calculateTurnIntoSelection(\n          editorState.getNodeAtPath([0])!,\n          selection5,\n        ),\n        Selection.collapsed(Position(path: [0])),\n      );\n\n      // case 6, not collapsed selection and the selection is in the nested level\n      // and tap the turn into button at the [0, 0]\n      final selection6 = Selection(\n        start: Position(path: [0, 0], offset: 1),\n        end: Position(path: [0, 1], offset: 1),\n      );\n      expect(\n        cubit.calculateTurnIntoSelection(\n          editorState.getNodeAtPath([0])!,\n          selection6,\n        ),\n        Selection.collapsed(Position(path: [0])),\n      );\n\n      // case 7, multiple blocks selection, and tap the turn into button of one of the selected nodes\n      final selection7 = Selection(\n        start: Position(path: [0], offset: 1),\n        end: Position(path: [2], offset: 1),\n      );\n      expect(\n        cubit.calculateTurnIntoSelection(\n          editorState.getNodeAtPath([1])!,\n          selection7,\n        ),\n        selection7,\n      );\n\n      // case 8, multiple blocks selection, and tap the turn into button of one of the non-selected nodes\n      final selection8 = Selection(\n        start: Position(path: [0], offset: 1),\n        end: Position(path: [1], offset: 1),\n      );\n      expect(\n        cubit.calculateTurnIntoSelection(\n          editorState.getNodeAtPath([2])!,\n          selection8,\n        ),\n        Selection.collapsed(Position(path: [2])),\n      );\n    });\n\n    group('turn into toggle list', () {\n      const heading1 = 'heading 1';\n      const heading2 = 'heading 2';\n      const heading3 = 'heading 3';\n      const paragraph1 = 'paragraph 1';\n      const paragraph2 = 'paragraph 2';\n      const paragraph3 = 'paragraph 3';\n\n      test('turn heading 1 block to toggle heading 1 block', () async {\n        // before\n        // # Heading 1\n        // paragraph 1\n        // paragraph 2\n        // paragraph 3\n\n        // after\n        // > # Heading 1\n        //   paragraph 1\n        //   paragraph 2\n        //   paragraph 3\n        final document = createDocument([\n          headingNode(level: 1, text: heading1),\n          paragraphNode(text: paragraph1),\n          paragraphNode(text: paragraph2),\n          paragraphNode(text: paragraph3),\n        ]);\n\n        await checkTurnInto(\n          document,\n          HeadingBlockKeys.type,\n          heading1,\n          selection: Selection.collapsed(Position(path: [0])),\n          toType: ToggleListBlockKeys.type,\n          level: 1,\n          afterTurnInto: (editorState, node) {\n            expect(editorState.document.root.children.length, 1);\n            expect(node.type, ToggleListBlockKeys.type);\n            expect(node.attributes[ToggleListBlockKeys.level], 1);\n            expect(node.children.length, 3);\n            for (var i = 0; i < 3; i++) {\n              expect(node.children[i].type, ParagraphBlockKeys.type);\n              expect(\n                node.children[i].delta!.toPlainText(),\n                [paragraph1, paragraph2, paragraph3][i],\n              );\n            }\n\n            // test undo together\n            final result = undoCommand.execute(editorState);\n            expect(result, KeyEventResult.handled);\n            expect(editorState.document.root.children.length, 4);\n          },\n        );\n      });\n\n      test('turn toggle heading 1 block to heading 1 block', () async {\n        // before\n        // > # Heading 1\n        //   paragraph 1\n        //   paragraph 2\n        //   paragraph 3\n\n        // after\n        // # Heading 1\n        // paragraph 1\n        // paragraph 2\n        // paragraph 3\n        final document = createDocument([\n          toggleHeadingNode(\n            text: heading1,\n            children: [\n              paragraphNode(text: paragraph1),\n              paragraphNode(text: paragraph2),\n              paragraphNode(text: paragraph3),\n            ],\n          ),\n        ]);\n\n        await checkTurnInto(\n          document,\n          ToggleListBlockKeys.type,\n          heading1,\n          selection: Selection.collapsed(\n            Position(path: [0]),\n          ),\n          toType: HeadingBlockKeys.type,\n          level: 1,\n          afterTurnInto: (editorState, node) {\n            expect(editorState.document.root.children.length, 4);\n            expect(node.type, HeadingBlockKeys.type);\n            expect(node.attributes[HeadingBlockKeys.level], 1);\n            expect(node.children.length, 0);\n            for (var i = 1; i <= 3; i++) {\n              final node = editorState.getNodeAtPath([i])!;\n              expect(node.type, ParagraphBlockKeys.type);\n              expect(\n                node.delta!.toPlainText(),\n                [paragraph1, paragraph2, paragraph3][i - 1],\n              );\n            }\n\n            // test undo together\n            editorState.selection = Selection.collapsed(\n              Position(path: [0]),\n            );\n            final result = undoCommand.execute(editorState);\n            expect(result, KeyEventResult.handled);\n            expect(editorState.document.root.children.length, 1);\n            final afterNode = editorState.getNodeAtPath([0])!;\n            expect(afterNode.type, ToggleListBlockKeys.type);\n            expect(afterNode.attributes[ToggleListBlockKeys.level], 1);\n            expect(afterNode.children.length, 3);\n          },\n        );\n      });\n\n      test('turn heading 2 block to toggle heading 2 block - case 1', () async {\n        // before\n        // ## Heading 2\n        // paragraph 1\n        // ### Heading 3\n        // paragraph 2\n        // # Heading 1 <- the heading 1 block will not be converted\n        // paragraph 3\n\n        // after\n        // > ## Heading 2\n        //   paragraph 1\n        //   ## Heading 2\n        //   paragraph 2\n        // # Heading 1\n        // paragraph 3\n        final document = createDocument([\n          headingNode(level: 2, text: heading2),\n          paragraphNode(text: paragraph1),\n          headingNode(level: 3, text: heading3),\n          paragraphNode(text: paragraph2),\n          headingNode(level: 1, text: heading1),\n          paragraphNode(text: paragraph3),\n        ]);\n\n        await checkTurnInto(\n          document,\n          HeadingBlockKeys.type,\n          heading2,\n          selection: Selection.collapsed(\n            Position(path: [0]),\n          ),\n          toType: ToggleListBlockKeys.type,\n          level: 2,\n          afterTurnInto: (editorState, node) {\n            expect(editorState.document.root.children.length, 3);\n            expect(node.type, ToggleListBlockKeys.type);\n            expect(node.attributes[ToggleListBlockKeys.level], 2);\n            expect(node.children.length, 3);\n            expect(node.children[0].delta!.toPlainText(), paragraph1);\n            expect(node.children[1].delta!.toPlainText(), heading3);\n            expect(node.children[2].delta!.toPlainText(), paragraph2);\n\n            // the heading 1 block will not be converted\n            final heading1Node = editorState.getNodeAtPath([1])!;\n            expect(heading1Node.type, HeadingBlockKeys.type);\n            expect(heading1Node.attributes[HeadingBlockKeys.level], 1);\n            expect(heading1Node.delta!.toPlainText(), heading1);\n\n            final paragraph3Node = editorState.getNodeAtPath([2])!;\n            expect(paragraph3Node.type, ParagraphBlockKeys.type);\n            expect(paragraph3Node.delta!.toPlainText(), paragraph3);\n\n            // test undo together\n            final result = undoCommand.execute(editorState);\n            expect(result, KeyEventResult.handled);\n            expect(editorState.document.root.children.length, 6);\n          },\n        );\n      });\n\n      test('turn heading 2 block to toggle heading 2 block - case 2', () async {\n        // before\n        // ## Heading 2\n        // paragraph 1\n        // ## Heading 2 <- the heading 2 block will not be converted\n        // paragraph 2\n        // # Heading 1 <- the heading 1 block will not be converted\n        // paragraph 3\n\n        // after\n        // > ## Heading 2\n        //   paragraph 1\n        // ## Heading 2\n        //   paragraph 2\n        // # Heading 1\n        // paragraph 3\n        final document = createDocument([\n          headingNode(level: 2, text: heading2),\n          paragraphNode(text: paragraph1),\n          headingNode(level: 2, text: heading2),\n          paragraphNode(text: paragraph2),\n          headingNode(level: 1, text: heading1),\n          paragraphNode(text: paragraph3),\n        ]);\n\n        await checkTurnInto(\n          document,\n          HeadingBlockKeys.type,\n          heading2,\n          selection: Selection.collapsed(\n            Position(path: [0]),\n          ),\n          toType: ToggleListBlockKeys.type,\n          level: 2,\n          afterTurnInto: (editorState, node) {\n            expect(editorState.document.root.children.length, 5);\n            expect(node.type, ToggleListBlockKeys.type);\n            expect(node.attributes[ToggleListBlockKeys.level], 2);\n            expect(node.children.length, 1);\n            expect(node.children[0].delta!.toPlainText(), paragraph1);\n\n            final heading2Node = editorState.getNodeAtPath([1])!;\n            expect(heading2Node.type, HeadingBlockKeys.type);\n            expect(heading2Node.attributes[HeadingBlockKeys.level], 2);\n            expect(heading2Node.delta!.toPlainText(), heading2);\n\n            final paragraph2Node = editorState.getNodeAtPath([2])!;\n            expect(paragraph2Node.type, ParagraphBlockKeys.type);\n            expect(paragraph2Node.delta!.toPlainText(), paragraph2);\n\n            // the heading 1 block will not be converted\n            final heading1Node = editorState.getNodeAtPath([3])!;\n            expect(heading1Node.type, HeadingBlockKeys.type);\n            expect(heading1Node.attributes[HeadingBlockKeys.level], 1);\n            expect(heading1Node.delta!.toPlainText(), heading1);\n\n            final paragraph3Node = editorState.getNodeAtPath([4])!;\n            expect(paragraph3Node.type, ParagraphBlockKeys.type);\n            expect(paragraph3Node.delta!.toPlainText(), paragraph3);\n\n            // test undo together\n            final result = undoCommand.execute(editorState);\n            expect(result, KeyEventResult.handled);\n            expect(editorState.document.root.children.length, 6);\n          },\n        );\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/editor/editor_drop_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_file.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:cross_file/cross_file.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport '../../util.dart';\n\nvoid main() {\n  setUpAll(() async {\n    await AppFlowyUnitTest.ensureInitialized();\n  });\n\n  group('drop images and files in EditorState', () {\n    test('dropImages on same path as paragraph node ', () async {\n      final editorState = EditorState(\n        document: Document.blank(withInitialText: true),\n      );\n\n      expect(editorState.getNodeAtPath([0])!.type, ParagraphBlockKeys.type);\n\n      const dropPath = <int>[0];\n\n      const imagePath = 'assets/test/images/sample.jpeg';\n      final imageFile = XFile(imagePath);\n      await editorState.dropImages(dropPath, [imageFile], 'documentId', true);\n\n      final node = editorState.getNodeAtPath(dropPath);\n      expect(node, isNotNull);\n      expect(node!.type, CustomImageBlockKeys.type);\n      expect(editorState.getNodeAtPath([1])!.type, ParagraphBlockKeys.type);\n    });\n\n    test('dropImages should insert image node on empty path', () async {\n      final editorState = EditorState(\n        document: Document.blank(withInitialText: true),\n      );\n\n      expect(editorState.getNodeAtPath([0])!.type, ParagraphBlockKeys.type);\n\n      const dropPath = <int>[1];\n\n      const imagePath = 'assets/test/images/sample.jpeg';\n      final imageFile = XFile(imagePath);\n      await editorState.dropImages(dropPath, [imageFile], 'documentId', true);\n\n      final node = editorState.getNodeAtPath(dropPath);\n      expect(node, isNotNull);\n      expect(node!.type, CustomImageBlockKeys.type);\n      expect(editorState.getNodeAtPath([0])!.type, ParagraphBlockKeys.type);\n      expect(editorState.getNodeAtPath([2]), null);\n    });\n\n    test('dropFiles on same path as paragraph node ', () async {\n      final editorState = EditorState(\n        document: Document.blank(withInitialText: true),\n      );\n\n      expect(editorState.getNodeAtPath([0])!.type, ParagraphBlockKeys.type);\n\n      const dropPath = <int>[0];\n\n      const filePath = 'assets/test/images/sample.jpeg';\n      final file = XFile(filePath);\n      await editorState.dropFiles(dropPath, [file], 'documentId', true);\n\n      final node = editorState.getNodeAtPath(dropPath);\n      expect(node, isNotNull);\n      expect(node!.type, FileBlockKeys.type);\n      expect(editorState.getNodeAtPath([1])!.type, ParagraphBlockKeys.type);\n    });\n\n    test('dropFiles should insert file node on empty path', () async {\n      final editorState = EditorState(\n        document: Document.blank(withInitialText: true),\n      );\n      const dropPath = <int>[1];\n\n      const filePath = 'assets/test/images/sample.jpeg';\n      final file = XFile(filePath);\n      await editorState.dropFiles(dropPath, [file], 'documentId', true);\n\n      final node = editorState.getNodeAtPath(dropPath);\n      expect(node, isNotNull);\n      expect(node!.type, FileBlockKeys.type);\n      expect(editorState.getNodeAtPath([0])!.type, ParagraphBlockKeys.type);\n      expect(editorState.getNodeAtPath([2]), null);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/editor/editor_migration_test.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  group('editor migration, from v0.1.x to 0.2', () {\n    test('migrate readme', () async {\n      final readme = await rootBundle.loadString('assets/template/readme.json');\n      final oldDocument = DocumentV0.fromJson(json.decode(readme));\n      final document = EditorMigration.migrateDocument(readme);\n      expect(document.root.type, 'page');\n      expect(oldDocument.root.children.length, document.root.children.length);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/editor/editor_style_test.dart",
    "content": "import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_style.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass MockDocumentAppearanceCubit extends Mock\n    implements DocumentAppearanceCubit {}\n\nclass MockBuildContext extends Mock implements BuildContext {}\n\nvoid main() {\n  WidgetsFlutterBinding.ensureInitialized();\n  group('EditorStyleCustomizer', () {\n    late EditorStyleCustomizer editorStyleCustomizer;\n    late MockBuildContext mockBuildContext;\n\n    setUp(() {\n      mockBuildContext = MockBuildContext();\n      editorStyleCustomizer = EditorStyleCustomizer(\n        context: mockBuildContext,\n        padding: EdgeInsets.zero,\n      );\n    });\n\n    test('baseTextStyle should return the expected TextStyle', () {\n      const fontFamily = 'Roboto';\n      final result = editorStyleCustomizer.baseTextStyle(fontFamily);\n      expect(result, isA<TextStyle>());\n      expect(result.fontFamily, 'Roboto_regular');\n    });\n\n    test(\n        'baseTextStyle should return the null TextStyle when an exception occurs',\n        () {\n      const garbage = 'Garbage';\n      final result = editorStyleCustomizer.baseTextStyle(garbage);\n      expect(result, isA<TextStyle>());\n      expect(\n        result.fontFamily,\n        null,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/editor/file_block_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('FileBlock:', () {\n    test('insert file block in non-empty paragraph', () async {\n      final document = Document.blank()\n        ..insert(\n          [0],\n          [paragraphNode(text: 'Hello World')],\n        );\n      final editorState = EditorState(document: document);\n      editorState.selection = Selection.collapsed(Position(path: [0]));\n\n      // insert file block after the first line\n      await editorState.insertEmptyFileBlock(GlobalKey());\n\n      final afterDocument = editorState.document;\n      expect(afterDocument.root.children.length, 2);\n      expect(afterDocument.root.children[1].type, FileBlockKeys.type);\n      expect(afterDocument.root.children[0].type, ParagraphBlockKeys.type);\n      expect(\n        afterDocument.root.children[0].delta!.toPlainText(),\n        'Hello World',\n      );\n    });\n\n    test('insert file block in empty paragraph', () async {\n      final document = Document.blank()\n        ..insert(\n          [0],\n          [paragraphNode(text: '')],\n        );\n      final editorState = EditorState(document: document);\n      editorState.selection = Selection.collapsed(Position(path: [0]));\n\n      await editorState.insertEmptyFileBlock(GlobalKey());\n\n      final afterDocument = editorState.document;\n      expect(afterDocument.root.children.length, 1);\n      expect(afterDocument.root.children[0].type, FileBlockKeys.type);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/editor/share_markdown_test.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('share markdown', () {\n    test('math equation', () {\n      const text = '''\n{\n    \"document\":{\n        \"type\":\"page\",\n        \"children\":[\n            {\n                \"type\":\"math_equation\",\n                \"data\":{\n                    \"formula\":\"E = MC^2\"\n                }\n            }\n        ]\n    }\n}\n''';\n      final document = Document.fromJson(\n        Map<String, Object>.from(json.decode(text)),\n      );\n      final result = documentToMarkdown(\n        document,\n        customParsers: [\n          const MathEquationNodeParser(),\n        ],\n      );\n      expect(result, r'$$E = MC^2$$');\n    });\n\n    test('code block', () {\n      const text = '''\n{\n    \"document\":{\n        \"type\":\"page\",\n        \"children\":[\n            {\n                \"type\":\"code\",\n                \"data\":{\n                      \"delta\": [\n                        {\n                            \"insert\": \"Some Code\"\n                        }\n                    ]\n                }\n            }\n        ]\n    }\n}\n''';\n      final document = Document.fromJson(\n        Map<String, Object>.from(json.decode(text)),\n      );\n      final result = documentToMarkdown(\n        document,\n        customParsers: [\n          const CodeBlockNodeParser(),\n        ],\n      );\n      expect(result, '```\\nSome Code\\n```');\n    });\n\n    test('divider', () {\n      const text = '''\n{\n    \"document\":{\n        \"type\":\"page\",\n        \"children\":[\n            {\n                \"type\":\"divider\"\n            }\n        ]\n    }\n}\n''';\n      final document = Document.fromJson(\n        Map<String, Object>.from(json.decode(text)),\n      );\n      final result = documentToMarkdown(\n        document,\n        customParsers: [\n          const DividerNodeParser(),\n        ],\n      );\n      expect(result, '---\\n');\n    });\n\n    test('callout', () {\n      const text = '''\n{\n    \"document\":{\n        \"type\":\"page\",\n        \"children\":[\n            {\n                \"type\":\"callout\",\n                \"data\":{\n                    \"icon\": \"😁\",\n                    \"delta\": [\n                        {\n                            \"insert\": \"Callout\"\n                        }\n                    ]\n                }\n            }\n        ]\n    }\n}\n''';\n      final document = Document.fromJson(\n        Map<String, Object>.from(json.decode(text)),\n      );\n      final result = documentToMarkdown(\n        document,\n        customParsers: [\n          const CalloutNodeParser(),\n        ],\n      );\n      expect(result, '''> 😁\n> Callout\n\n''');\n    });\n\n    test('toggle list', () {\n      const text = '''\n{\n    \"document\":{\n        \"type\":\"page\",\n        \"children\":[\n            {\n                \"type\":\"toggle_list\",\n                \"data\":{\n                    \"delta\": [\n                        {\n                            \"insert\": \"Toggle list\"\n                        }\n                    ]\n                }\n            }\n        ]\n    }\n}\n''';\n      final document = Document.fromJson(\n        Map<String, Object>.from(json.decode(text)),\n      );\n      final result = documentToMarkdown(\n        document,\n        customParsers: [\n          const ToggleListNodeParser(),\n        ],\n      );\n      expect(result, '- Toggle list\\n');\n    });\n\n    test('custom image', () {\n      const image =\n          'https://images.unsplash.com/photo-1694984121999-36d30b67f391?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxlZGl0b3JpYWwtZmVlZHwzfHx8ZW58MHx8fHx8&auto=format&fit=crop&w=800&q=60';\n      const text = '''\n{\n    \"document\":{\n        \"type\":\"page\",\n        \"children\":[\n            {\n                \"type\":\"image\",\n                \"data\":{\n                    \"url\": \"$image\"\n                }\n            }\n        ]\n    }\n}\n''';\n      final document = Document.fromJson(\n        Map<String, Object>.from(json.decode(text)),\n      );\n      final result = documentToMarkdown(\n        document,\n        customParsers: [\n          const CustomImageNodeParser(),\n        ],\n      );\n      expect(\n        result,\n        '![]($image)\\n',\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart",
    "content": "import 'dart:async';\n\nimport 'package:appflowy/plugins/document/application/document_service.dart';\nimport 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';\nimport 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('TransactionAdapter:', () {\n    test('toBlockAction insert node with children operation', () {\n      final editorState = EditorState.blank();\n\n      final transaction = editorState.transaction;\n      transaction.insertNode(\n        [0],\n        paragraphNode(\n          children: [\n            paragraphNode(text: '1', children: [paragraphNode(text: '1.1')]),\n            paragraphNode(text: '2'),\n            paragraphNode(text: '3', children: [paragraphNode(text: '3.1')]),\n            paragraphNode(text: '4'),\n          ],\n        ),\n      );\n\n      expect(transaction.operations.length, 1);\n      expect(transaction.operations[0] is InsertOperation, true);\n\n      final actions = transaction.operations[0].toBlockAction(editorState, '');\n\n      expect(actions.length, 7);\n      for (final action in actions) {\n        expect(action.blockActionPB.action, BlockActionTypePB.Insert);\n      }\n\n      expect(\n        actions[0].blockActionPB.payload.parentId,\n        editorState.document.root.id,\n        reason: '0 - parent id',\n      );\n      expect(\n        actions[0].blockActionPB.payload.prevId,\n        '',\n        reason: '0 - prev id',\n      );\n      expect(\n        actions[1].blockActionPB.payload.parentId,\n        actions[0].blockActionPB.payload.block.id,\n        reason: '1 - parent id',\n      );\n      expect(\n        actions[1].blockActionPB.payload.prevId,\n        '',\n        reason: '1 - prev id',\n      );\n      expect(\n        actions[2].blockActionPB.payload.parentId,\n        actions[1].blockActionPB.payload.block.id,\n        reason: '2 - parent id',\n      );\n      expect(\n        actions[2].blockActionPB.payload.prevId,\n        '',\n        reason: '2 - prev id',\n      );\n      expect(\n        actions[3].blockActionPB.payload.parentId,\n        actions[0].blockActionPB.payload.block.id,\n        reason: '3 - parent id',\n      );\n      expect(\n        actions[3].blockActionPB.payload.prevId,\n        actions[1].blockActionPB.payload.block.id,\n        reason: '3 - prev id',\n      );\n      expect(\n        actions[4].blockActionPB.payload.parentId,\n        actions[0].blockActionPB.payload.block.id,\n        reason: '4 - parent id',\n      );\n      expect(\n        actions[4].blockActionPB.payload.prevId,\n        actions[3].blockActionPB.payload.block.id,\n        reason: '4 - prev id',\n      );\n      expect(\n        actions[5].blockActionPB.payload.parentId,\n        actions[4].blockActionPB.payload.block.id,\n        reason: '5 - parent id',\n      );\n      expect(\n        actions[5].blockActionPB.payload.prevId,\n        '',\n        reason: '5 - prev id',\n      );\n      expect(\n        actions[6].blockActionPB.payload.parentId,\n        actions[0].blockActionPB.payload.block.id,\n        reason: '6 - parent id',\n      );\n      expect(\n        actions[6].blockActionPB.payload.prevId,\n        actions[4].blockActionPB.payload.block.id,\n        reason: '6 - prev id',\n      );\n    });\n\n    test('toBlockAction insert node before all children nodes', () {\n      final document = Document(\n        root: Node(\n          type: 'page',\n          children: [\n            paragraphNode(children: [paragraphNode(text: '1')]),\n          ],\n        ),\n      );\n      final editorState = EditorState(document: document);\n\n      final transaction = editorState.transaction;\n      transaction.insertNodes([0, 0], [paragraphNode(), paragraphNode()]);\n\n      expect(transaction.operations.length, 1);\n      expect(transaction.operations[0] is InsertOperation, true);\n\n      final actions = transaction.operations[0].toBlockAction(editorState, '');\n\n      expect(actions.length, 2);\n      for (final action in actions) {\n        expect(action.blockActionPB.action, BlockActionTypePB.Insert);\n      }\n\n      expect(\n        actions[0].blockActionPB.payload.parentId,\n        editorState.document.root.children.first.id,\n        reason: '0 - parent id',\n      );\n      expect(\n        actions[0].blockActionPB.payload.prevId,\n        '',\n        reason: '0 - prev id',\n      );\n      expect(\n        actions[1].blockActionPB.payload.parentId,\n        editorState.document.root.children.first.id,\n        reason: '1 - parent id',\n      );\n      expect(\n        actions[1].blockActionPB.payload.prevId,\n        actions[0].blockActionPB.payload.block.id,\n        reason: '1 - prev id',\n      );\n    });\n\n    test('update the external id and external type', () async {\n      // create a node without external id and external type\n      // the editing this node, the adapter should generate a new action\n      //  to assign a new external id and external type.\n      final node = bulletedListNode(text: 'Hello');\n      final document = Document(\n        root: pageNode(\n          children: [\n            node,\n          ],\n        ),\n      );\n\n      final transactionAdapter = TransactionAdapter(\n        documentId: '',\n        documentService: DocumentService(),\n      );\n\n      final editorState = EditorState(\n        document: document,\n      );\n\n      final completer = Completer();\n      editorState.transactionStream.listen((event) {\n        final time = event.$1;\n        if (time == TransactionTime.before) {\n          final actions = transactionAdapter.transactionToBlockActions(\n            event.$2,\n            editorState,\n          );\n          final textActions =\n              transactionAdapter.filterTextDeltaActions(actions);\n          final blockActions = transactionAdapter.filterBlockActions(actions);\n          expect(textActions.length, 1);\n          expect(blockActions.length, 1);\n\n          // check text operation\n          final textAction = textActions.first;\n          final textId = textAction.textDeltaPayloadPB?.textId;\n          {\n            expect(textAction.textDeltaType, TextDeltaType.create);\n\n            expect(textId, isNotEmpty);\n            final delta = textAction.textDeltaPayloadPB?.delta;\n            expect(delta, equals('[{\"insert\":\"HelloWorld\"}]'));\n          }\n\n          // check block operation\n          {\n            final blockAction = blockActions.first;\n            expect(blockAction.action, BlockActionTypePB.Update);\n            expect(blockAction.payload.block.id, node.id);\n            expect(\n              blockAction.payload.block.externalId,\n              textId,\n            );\n            expect(blockAction.payload.block.externalType, kExternalTextType);\n          }\n        } else if (time == TransactionTime.after) {\n          completer.complete();\n        }\n      });\n\n      await editorState.insertText(\n        5,\n        'World',\n        node: node,\n      );\n      await completer.future;\n    });\n\n    test('use delta from prev attributes if current delta is null', () async {\n      final node = todoListNode(\n        checked: false,\n        delta: Delta()..insert('AppFlowy'),\n      );\n      final document = Document(\n        root: pageNode(\n          children: [\n            node,\n          ],\n        ),\n      );\n      final transactionAdapter = TransactionAdapter(\n        documentId: '',\n        documentService: DocumentService(),\n      );\n\n      final editorState = EditorState(\n        document: document,\n      );\n\n      final completer = Completer();\n      editorState.transactionStream.listen((event) {\n        final time = event.$1;\n        if (time == TransactionTime.before) {\n          final actions = transactionAdapter.transactionToBlockActions(\n            event.$2,\n            editorState,\n          );\n          final textActions =\n              transactionAdapter.filterTextDeltaActions(actions);\n          final blockActions = transactionAdapter.filterBlockActions(actions);\n          expect(textActions.length, 1);\n          expect(blockActions.length, 1);\n\n          // check text operation\n          final textAction = textActions.first;\n          final textId = textAction.textDeltaPayloadPB?.textId;\n          {\n            expect(textAction.textDeltaType, TextDeltaType.create);\n\n            expect(textId, isNotEmpty);\n            final delta = textAction.textDeltaPayloadPB?.delta;\n            expect(delta, equals('[{\"insert\":\"AppFlowy\"}]'));\n          }\n\n          // check block operation\n          {\n            final blockAction = blockActions.first;\n            expect(blockAction.action, BlockActionTypePB.Update);\n            expect(blockAction.payload.block.id, node.id);\n            expect(\n              blockAction.payload.block.externalId,\n              textId,\n            );\n            expect(blockAction.payload.block.externalType, kExternalTextType);\n          }\n        } else if (time == TransactionTime.after) {\n          completer.complete();\n        }\n      });\n\n      final transaction = editorState.transaction;\n      transaction.updateNode(node, {TodoListBlockKeys.checked: true});\n      await editorState.apply(transaction);\n      await completer.future;\n    });\n\n    test('text retain with attributes that are false', () async {\n      final node = paragraphNode(\n        delta: Delta()\n          ..insert(\n            'Hello AppFlowy',\n            attributes: {\n              'bold': true,\n            },\n          ),\n      );\n      final document = Document(\n        root: pageNode(\n          children: [\n            node,\n          ],\n        ),\n      );\n      final transactionAdapter = TransactionAdapter(\n        documentId: '',\n        documentService: DocumentService(),\n      );\n\n      final editorState = EditorState(\n        document: document,\n      );\n\n      int counter = 0;\n      final completer = Completer();\n      editorState.transactionStream.listen((event) {\n        final time = event.$1;\n        if (time == TransactionTime.before) {\n          final actions = transactionAdapter.transactionToBlockActions(\n            event.$2,\n            editorState,\n          );\n          final textActions =\n              transactionAdapter.filterTextDeltaActions(actions);\n          final blockActions = transactionAdapter.filterBlockActions(actions);\n          expect(textActions.length, 1);\n          expect(blockActions.length, 1);\n          if (counter == 1) {\n            // check text operation\n            final textAction = textActions.first;\n            final textId = textAction.textDeltaPayloadPB?.textId;\n            {\n              expect(textAction.textDeltaType, TextDeltaType.create);\n\n              expect(textId, isNotEmpty);\n              final delta = textAction.textDeltaPayloadPB?.delta;\n              expect(\n                delta,\n                equals(\n                  '[{\"insert\":\"Hello\",\"attributes\":{\"bold\":null}},{\"insert\":\" AppFlowy\",\"attributes\":{\"bold\":true}}]',\n                ),\n              );\n            }\n          } else if (counter == 3) {\n            final textAction = textActions.first;\n            final textId = textAction.textDeltaPayloadPB?.textId;\n            {\n              expect(textAction.textDeltaType, TextDeltaType.update);\n\n              expect(textId, isNotEmpty);\n              final delta = textAction.textDeltaPayloadPB?.delta;\n              expect(\n                delta,\n                equals(\n                  '[{\"retain\":5,\"attributes\":{\"bold\":null}}]',\n                ),\n              );\n            }\n          }\n        } else if (time == TransactionTime.after && counter == 3) {\n          completer.complete();\n        }\n      });\n\n      counter = 1;\n      final insertTransaction = editorState.transaction;\n      insertTransaction.formatText(node, 0, 5, {\n        'bold': false,\n      });\n\n      await editorState.apply(insertTransaction);\n\n      counter = 2;\n      final updateTransaction = editorState.transaction;\n      updateTransaction.formatText(node, 0, 5, {\n        'bold': true,\n      });\n      await editorState.apply(updateTransaction);\n\n      counter = 3;\n      final formatTransaction = editorState.transaction;\n      formatTransaction.formatText(node, 0, 5, {\n        'bold': false,\n      });\n      await editorState.apply(formatTransaction);\n\n      await completer.future;\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/image/appflowy_network_image_test.dart",
    "content": "import 'package:appflowy/shared/appflowy_network_image.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('AppFlowy Network Image:', () {\n    setUpAll(() {\n      Log.shared.disableLog = true;\n    });\n\n    tearDownAll(() {\n      Log.shared.disableLog = false;\n    });\n\n    test(\n      'retry count should be clear if the value exceeds max retries',\n      () async {\n        const maxRetries = 5;\n        const fakeUrl = 'https://plus.unsplash.com/premium_photo-1731948132439';\n        final retryCounter = FlowyNetworkRetryCounter();\n        final tag = retryCounter.add(fakeUrl);\n        for (var i = 0; i < maxRetries; i++) {\n          retryCounter.increment(fakeUrl);\n          expect(retryCounter.getRetryCount(fakeUrl), i + 1);\n        }\n        retryCounter.clear(\n          tag: tag,\n          url: fakeUrl,\n          maxRetries: maxRetries,\n        );\n        expect(retryCounter.getRetryCount(fakeUrl), 0);\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/link_preview/link_preview_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() async {\n  test(\n    'description',\n    () async {\n      final links = [\n        'https://www.baidu.com/',\n        'https://appflowy.io/',\n        'https://github.com/AppFlowy-IO/AppFlowy',\n        'https://github.com/',\n        'https://www.figma.com/design/3K0ai4FhDOJ3Lts8G3KOVP/Page?node-id=7282-4007&p=f&t=rpfvEvh9K9J9WkIo-0',\n        'https://www.figma.com/files/drafts',\n        'https://www.youtube.com/watch?v=LyY5Rh9qBvA',\n        'https://www.youtube.com/',\n        'https://www.youtube.com/watch?v=a6GDT7',\n        'http://www.test.com/',\n        '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',\n        'https://www.google.com/',\n        'https://www.google.com.hk/search?q=test&oq=test&gs_lcrp=EgZjaHJvbWUyCQgAEEUYORiABDIHCAEQABiABDIHCAIQABiABDIHCAMQABiABDIHCAQQABiABDIHCAUQABiABDIHCAYQABiABDIHCAcQABiABDIHCAgQLhiABDIHCAkQABiABNIBCTE4MDJqMGoxNagCCLACAfEFAQs7K9PprSfxBQELOyvT6a0n&sourceid=chrome&ie=UTF-8',\n        'www.baidu.com',\n        'baidu.com',\n        'com',\n        'https://www.baidu.com',\n        'https://github.com/AppFlowy-IO/AppFlowy',\n        'https://appflowy.com/app/c29fafc4-b7c0-4549-8702-71339b0fd9ea/59f36be8-9b2f-4d3e-b6a1-816c6c2043e5?blockId=GCY_T4',\n      ];\n\n      final parser = DefaultParser();\n      int i = 1;\n      for (final link in links) {\n        final formatLink = LinkInfoParser.formatUrl(link);\n        final siteInfo = await parser\n            .parse(Uri.tryParse(formatLink) ?? Uri.parse(formatLink));\n        if (siteInfo?.isEmpty() ?? true) {\n          debugPrint('$i : $formatLink ---- empty \\n');\n        } else {\n          debugPrint('$i : $formatLink ---- \\n$siteInfo \\n');\n        }\n        i++;\n      }\n    },\n    timeout: const Timeout(Duration(seconds: 120)),\n  );\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/markdown/markdown_parser_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/shared/markdown_to_document.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('export markdown to document', () {\n    test('file block', () async {\n      final document = Document.blank()\n        ..insert(\n          [0],\n          [\n            fileNode(\n              name: 'file.txt',\n              url: 'https://file.com',\n            ),\n          ],\n        );\n      final markdown = await customDocumentToMarkdown(document);\n      expect(markdown, '[file.txt](https://file.com)\\n');\n    });\n\n    test('link preview', () async {\n      final document = Document.blank()\n        ..insert(\n          [0],\n          [linkPreviewNode(url: 'https://www.link_preview.com')],\n        );\n      final markdown = await customDocumentToMarkdown(document);\n      expect(\n        markdown,\n        '[https://www.link_preview.com](https://www.link_preview.com)\\n',\n      );\n    });\n\n    test('multiple images', () async {\n      const png1 = 'https://www.appflowy.png',\n          png2 = 'https://www.appflowy2.png';\n      final document = Document.blank()\n        ..insert(\n          [0],\n          [\n            multiImageNode(\n              images: [\n                ImageBlockData(\n                  url: png1,\n                  type: CustomImageType.external,\n                ),\n                ImageBlockData(\n                  url: png2,\n                  type: CustomImageType.external,\n                ),\n              ],\n            ),\n          ],\n        );\n      final markdown = await customDocumentToMarkdown(document);\n      expect(\n        markdown,\n        '![]($png1)\\n![]($png2)',\n      );\n    });\n\n    test('subpage block', () async {\n      const testSubpageId = 'testSubpageId';\n      final subpageNode = pageMentionNode(testSubpageId);\n      final document = Document.blank()\n        ..insert(\n          [0],\n          [subpageNode],\n        );\n      final markdown = await customDocumentToMarkdown(document);\n      expect(\n        markdown,\n        '[]($testSubpageId)\\n',\n      );\n    });\n\n    test('date or reminder', () async {\n      final dateTime = DateTime.now();\n      final document = Document.blank()\n        ..insert(\n          [0],\n          [dateMentionNode()],\n        );\n      final markdown = await customDocumentToMarkdown(document);\n      expect(\n        markdown,\n        '${DateFormat.yMMMd().format(dateTime)}\\n',\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/search/split_search_test.dart",
    "content": "import 'package:appflowy/mobile/presentation/search/mobile_search_cell.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Test for searching text with split query', () {\n    int checkLength(String query, List<String> contents) {\n      int i = 0;\n      for (final content in contents) {\n        if (content.toLowerCase() == query.toLowerCase()) {\n          i++;\n        }\n      }\n      return i;\n    }\n\n    test('split with space', () {\n      final content = 'Hello HELLO hello HeLLo';\n      final query = 'Hello';\n      final contents = content\n          .splitIncludeSeparator(query)\n          .where((e) => e.isNotEmpty)\n          .toList();\n      assert(contents.join() == content);\n\n      assert(checkLength(query, contents) == 4);\n    });\n\n    test('split without space', () {\n      final content = 'HelloHELLOhelloHeLLo';\n      final query = 'Hello';\n      final contents = content\n          .splitIncludeSeparator(query)\n          .where((e) => e.isNotEmpty)\n          .toList();\n      assert(contents.join() == content);\n      assert(checkLength(query, contents) == 4);\n    });\n\n    test('split without space and with error content', () {\n      final content = 'HellHELLOhelloeLLo';\n      final query = 'Hello';\n      final contents = content\n          .splitIncludeSeparator(query)\n          .where((e) => e.isNotEmpty)\n          .toList();\n      assert(contents.join() == content);\n      assert(checkLength(query, contents) == 2);\n    });\n\n    test('split with space and with error content', () {\n      final content = 'Hell HELLOhello eLLo';\n      final query = 'Hello';\n      final contents = content\n          .splitIncludeSeparator(query)\n          .where((e) => e.isNotEmpty)\n          .toList();\n      assert(contents.join() == content);\n      assert(checkLength(query, contents) == 2);\n    });\n\n    test('split without longer query', () {\n      final content = 'Hello';\n      final query = 'HelloHelloHelloHello';\n      final contents = content\n          .splitIncludeSeparator(query)\n          .where((e) => e.isNotEmpty)\n          .toList();\n      assert(contents.join() == content);\n      assert(checkLength(query, contents) == 0);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/select_option_split_text_input.dart",
    "content": "import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  const textSeparators = [','];\n\n  group('split input unit test', () {\n    test('empty input', () {\n      var (submitted, remainder) = splitInput(' ', textSeparators);\n      expect(submitted, []);\n      expect(remainder, '');\n\n      (submitted, remainder) = splitInput(', , , ', textSeparators);\n      expect(submitted, []);\n      expect(remainder, '');\n    });\n\n    test('simple input', () {\n      var (submitted, remainder) = splitInput('exampleTag', textSeparators);\n      expect(submitted, []);\n      expect(remainder, 'exampleTag');\n\n      (submitted, remainder) =\n          splitInput('tag with longer name', textSeparators);\n      expect(submitted, []);\n      expect(remainder, 'tag with longer name');\n\n      (submitted, remainder) = splitInput('trailing space ', textSeparators);\n      expect(submitted, []);\n      expect(remainder, 'trailing space ');\n    });\n\n    test('input with commas', () {\n      var (submitted, remainder) = splitInput('a, b, c', textSeparators);\n      expect(submitted, ['a', 'b']);\n      expect(remainder, 'c');\n\n      (submitted, remainder) = splitInput('a, b, c, ', textSeparators);\n      expect(submitted, ['a', 'b', 'c']);\n      expect(remainder, '');\n\n      (submitted, remainder) =\n          splitInput(',tag 1 ,2nd tag, third tag ', textSeparators);\n      expect(submitted, ['tag 1', '2nd tag']);\n      expect(remainder, 'third tag ');\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/settings/shortcuts/settings_shortcut_service_test.dart",
    "content": "import 'dart:convert';\nimport 'dart:io' show File;\n\nimport 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';\nimport 'package:appflowy/workspace/application/settings/shortcuts/shortcuts_model.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n// ignore: depend_on_referenced_packages\nimport 'package:file/memory.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  late SettingsShortcutService service;\n  late File mockFile;\n  String shortcutsJson = '';\n\n  setUp(() async {\n    final MemoryFileSystem fileSystem = MemoryFileSystem.test();\n    mockFile = await fileSystem.file(\"shortcuts.json\").create(recursive: true);\n    service = SettingsShortcutService(file: mockFile);\n    shortcutsJson = \"\"\"{\n   \"commandShortcuts\":[\n      {\n         \"key\":\"move the cursor upward\",\n         \"command\":\"alt+arrow up\"\n      },\n      {\n         \"key\":\"move the cursor backward one character\",\n         \"command\":\"alt+arrow left\"\n      },\n      {\n         \"key\":\"move the cursor downward\",\n         \"command\":\"alt+arrow down\"\n      }\n   ]\n}\"\"\";\n  });\n\n  group(\"Settings Shortcut Service\", () {\n    test(\n      \"returns default standard shortcuts if file is empty\",\n      () async {\n        expect(await service.getCustomizeShortcuts(), []);\n      },\n    );\n\n    test('returns updated shortcut event list from json', () {\n      final commandShortcuts = service.getShortcutsFromJson(shortcutsJson);\n\n      final cursorUpShortcut = commandShortcuts\n          .firstWhere((el) => el.key == \"move the cursor upward\");\n\n      final cursorDownShortcut = commandShortcuts\n          .firstWhere((el) => el.key == \"move the cursor downward\");\n\n      expect(\n        commandShortcuts.length,\n        3,\n      );\n      expect(cursorUpShortcut.command, \"alt+arrow up\");\n      expect(cursorDownShortcut.command, \"alt+arrow down\");\n    });\n\n    test(\n      \"saveAllShortcuts saves shortcuts\",\n      () async {\n        //updating one of standard command shortcut events.\n        final currentCommandShortcuts = standardCommandShortcutEvents;\n        const kKey = \"scroll one page down\";\n        const oldCommand = \"page down\";\n        const newCommand = \"alt+page down\";\n        final commandShortcutEvent = currentCommandShortcuts\n            .firstWhere((element) => element.key == kKey);\n\n        expect(commandShortcutEvent.command, oldCommand);\n\n        //updating the command.\n        commandShortcutEvent.updateCommand(\n          command: newCommand,\n        );\n\n        //saving the updated shortcuts\n        await service.saveAllShortcuts(currentCommandShortcuts);\n\n        //reading from the mock file the saved shortcut list.\n        final savedDataInFile = await mockFile.readAsString();\n\n        //Check if the lists where properly converted to JSON and saved.\n        final shortcuts = EditorShortcuts(\n          commandShortcuts:\n              currentCommandShortcuts.toCommandShortcutModelList(),\n        );\n\n        expect(jsonEncode(shortcuts.toJson()), savedDataInFile);\n\n        //now checking if the modified command of \"move the cursor upward\" is \"arrow up\"\n        final newCommandShortcuts =\n            service.getShortcutsFromJson(savedDataInFile);\n\n        final updatedCommandEvent =\n            newCommandShortcuts.firstWhere((el) => el.key == kKey);\n\n        expect(updatedCommandEvent.command, newCommand);\n      },\n    );\n\n    test('load shortcuts from file', () async {\n      //updating one of standard command shortcut event.\n      const kKey = \"scroll one page up\";\n      const oldCommand = \"page up\";\n      const newCommand = \"alt+page up\";\n      final currentCommandShortcuts = standardCommandShortcutEvents;\n      final commandShortcutEvent =\n          currentCommandShortcuts.firstWhere((element) => element.key == kKey);\n\n      expect(commandShortcutEvent.command, oldCommand);\n\n      //updating the command.\n      commandShortcutEvent.updateCommand(command: newCommand);\n\n      //saving the updated shortcuts\n      await service.saveAllShortcuts(currentCommandShortcuts);\n\n      //now directly fetching the shortcuts from loadShortcuts\n      final commandShortcuts = await service.getCustomizeShortcuts();\n      expect(\n        commandShortcuts,\n        currentCommandShortcuts.toCommandShortcutModelList(),\n      );\n\n      final updatedCommandEvent =\n          commandShortcuts.firstWhere((el) => el.key == kKey);\n\n      expect(updatedCommandEvent.command, newCommand);\n    });\n\n    test('updateCommandShortcuts works properly', () async {\n      //updating one of standard command shortcut event.\n      const kKey = \"move the cursor backward one character\";\n      const oldCommand = \"arrow left\";\n      const newCommand = \"alt+arrow left\";\n      final currentCommandShortcuts = standardCommandShortcutEvents;\n\n      //check if the current shortcut event's key is set to old command.\n      final currentCommandEvent =\n          currentCommandShortcuts.firstWhere((el) => el.key == kKey);\n\n      expect(currentCommandEvent.command, oldCommand);\n\n      final commandShortcutModelList =\n          EditorShortcuts.fromJson(jsonDecode(shortcutsJson)).commandShortcuts;\n\n      //now calling the updateCommandShortcuts method\n      await service.updateCommandShortcuts(\n        currentCommandShortcuts,\n        commandShortcutModelList,\n      );\n\n      //check if the shortcut event's key is updated.\n      final updatedCommandEvent =\n          currentCommandShortcuts.firstWhere((el) => el.key == kKey);\n\n      expect(updatedCommandEvent.command, newCommand);\n    });\n  });\n}\n\nextension on List<CommandShortcutEvent> {\n  List<CommandShortcutModel> toCommandShortcutModelList() =>\n      map((e) => CommandShortcutModel.fromCommandEvent(e)).toList();\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/settings/theme_missing_keys_test.dart",
    "content": "import 'package:flowy_infra/colorscheme/colorscheme.dart';\nimport 'package:flowy_infra/colorscheme/default_colorscheme.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Theme missing keys', () {\n    test('no missing keys', () {\n      const colorScheme = DefaultColorScheme.light();\n      final toJson = colorScheme.toJson();\n\n      expect(toJson.containsKey('surface'), true);\n\n      final missingKeys = FlowyColorScheme.getMissingKeys(toJson);\n      expect(missingKeys.isEmpty, true);\n    });\n\n    test('missing surface and bg2', () {\n      const colorScheme = DefaultColorScheme.light();\n      final toJson = colorScheme.toJson()\n        ..remove('surface')\n        ..remove('bg2');\n\n      expect(toJson.containsKey('surface'), false);\n      expect(toJson.containsKey('bg2'), false);\n\n      final missingKeys = FlowyColorScheme.getMissingKeys(toJson);\n      expect(missingKeys.length, 2);\n      expect(missingKeys.contains('surface'), true);\n      expect(missingKeys.contains('bg2'), true);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_contente_operation_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'simple_table_test_helper.dart';\n\nvoid main() {\n  group('Simple table content operation:', () {\n    void setupDependencyInjection() {\n      getIt.registerSingleton<ClipboardService>(ClipboardService());\n    }\n\n    setUpAll(() {\n      Log.shared.disableLog = true;\n\n      setupDependencyInjection();\n    });\n\n    tearDownAll(() {\n      Log.shared.disableLog = false;\n    });\n\n    test('clear content at row 1', () async {\n      const defaultContent = 'default content';\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n        defaultContent: defaultContent,\n      );\n      await editorState.clearContentAtRowIndex(\n        tableNode: tableNode,\n        rowIndex: 0,\n      );\n      for (var i = 0; i < tableNode.rowLength; i++) {\n        for (var j = 0; j < tableNode.columnLength; j++) {\n          expect(\n            tableNode\n                .getTableCellNode(rowIndex: i, columnIndex: j)\n                ?.children\n                .first\n                .delta\n                ?.toPlainText(),\n            i == 0 ? '' : defaultContent,\n          );\n        }\n      }\n    });\n\n    test('clear content at row 3', () async {\n      const defaultContent = 'default content';\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n        defaultContent: defaultContent,\n      );\n      await editorState.clearContentAtRowIndex(\n        tableNode: tableNode,\n        rowIndex: 2,\n      );\n      for (var i = 0; i < tableNode.rowLength; i++) {\n        for (var j = 0; j < tableNode.columnLength; j++) {\n          expect(\n            tableNode\n                .getTableCellNode(rowIndex: i, columnIndex: j)\n                ?.children\n                .first\n                .delta\n                ?.toPlainText(),\n            i == 2 ? '' : defaultContent,\n          );\n        }\n      }\n    });\n\n    test('clear content at column 1', () async {\n      const defaultContent = 'default content';\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n        defaultContent: defaultContent,\n      );\n      await editorState.clearContentAtColumnIndex(\n        tableNode: tableNode,\n        columnIndex: 0,\n      );\n      for (var i = 0; i < tableNode.rowLength; i++) {\n        for (var j = 0; j < tableNode.columnLength; j++) {\n          expect(\n            tableNode\n                .getTableCellNode(rowIndex: i, columnIndex: j)\n                ?.children\n                .first\n                .delta\n                ?.toPlainText(),\n            j == 0 ? '' : defaultContent,\n          );\n        }\n      }\n    });\n\n    test('clear content at column 4', () async {\n      const defaultContent = 'default content';\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n        defaultContent: defaultContent,\n      );\n      await editorState.clearContentAtColumnIndex(\n        tableNode: tableNode,\n        columnIndex: 3,\n      );\n      for (var i = 0; i < tableNode.rowLength; i++) {\n        for (var j = 0; j < tableNode.columnLength; j++) {\n          expect(\n            tableNode\n                .getTableCellNode(rowIndex: i, columnIndex: j)\n                ?.children\n                .first\n                .delta\n                ?.toPlainText(),\n            j == 3 ? '' : defaultContent,\n          );\n        }\n      }\n    });\n\n    test('copy row 1-2', () async {\n      const rowCount = 2;\n      const columnCount = 3;\n\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: rowCount,\n        columnCount: columnCount,\n        contentBuilder: (rowIndex, columnIndex) =>\n            'row $rowIndex, column $columnIndex',\n      );\n\n      for (var rowIndex = 0; rowIndex < rowCount; rowIndex++) {\n        final data = await editorState.copyRow(\n          tableNode: tableNode,\n          rowIndex: rowIndex,\n        );\n        expect(data, isNotNull);\n        expect(\n          data?.plainText,\n          'row $rowIndex, column 0\\nrow $rowIndex, column 1\\nrow $rowIndex, column 2',\n        );\n      }\n    });\n\n    test('copy column 1-2', () async {\n      const rowCount = 2;\n      const columnCount = 3;\n\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: rowCount,\n        columnCount: columnCount,\n        contentBuilder: (rowIndex, columnIndex) =>\n            'row $rowIndex, column $columnIndex',\n      );\n\n      for (var columnIndex = 0; columnIndex < columnCount; columnIndex++) {\n        final data = await editorState.copyColumn(\n          tableNode: tableNode,\n          columnIndex: columnIndex,\n        );\n        expect(data, isNotNull);\n        expect(\n          data?.plainText,\n          'row 0, column $columnIndex\\nrow 1, column $columnIndex',\n        );\n      }\n    });\n\n    test('cut row 1-2', () async {\n      const rowCount = 2;\n      const columnCount = 3;\n\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: rowCount,\n        columnCount: columnCount,\n        contentBuilder: (rowIndex, columnIndex) =>\n            'row $rowIndex, column $columnIndex',\n      );\n\n      for (var rowIndex = 0; rowIndex < rowCount; rowIndex++) {\n        final data = await editorState.copyRow(\n          tableNode: tableNode,\n          rowIndex: rowIndex,\n          clearContent: true,\n        );\n        expect(data, isNotNull);\n        expect(\n          data?.plainText,\n          'row $rowIndex, column 0\\nrow $rowIndex, column 1\\nrow $rowIndex, column 2',\n        );\n      }\n\n      for (var rowIndex = 0; rowIndex < rowCount; rowIndex++) {\n        for (var columnIndex = 0; columnIndex < columnCount; columnIndex++) {\n          expect(\n            tableNode\n                .getTableCellNode(rowIndex: rowIndex, columnIndex: columnIndex)\n                ?.children\n                .first\n                .delta\n                ?.toPlainText(),\n            '',\n          );\n        }\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_delete_operation_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'simple_table_test_helper.dart';\n\nvoid main() {\n  group('Simple table delete operation:', () {\n    setUpAll(() {\n      Log.shared.disableLog = true;\n    });\n\n    tearDownAll(() {\n      Log.shared.disableLog = false;\n    });\n\n    test('delete 2 rows in table', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n      );\n      await editorState.deleteRowInTable(tableNode, 0);\n      await editorState.deleteRowInTable(tableNode, 0);\n      expect(tableNode.rowLength, 1);\n      expect(tableNode.columnLength, 4);\n    });\n\n    test('delete 2 columns in table', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n      );\n      await editorState.deleteColumnInTable(tableNode, 0);\n      await editorState.deleteColumnInTable(tableNode, 0);\n      expect(tableNode.rowLength, 3);\n      expect(tableNode.columnLength, 2);\n    });\n\n    test('delete a row and a column in table', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n      );\n      await editorState.deleteColumnInTable(tableNode, 0);\n      await editorState.deleteRowInTable(tableNode, 0);\n      expect(tableNode.rowLength, 2);\n      expect(tableNode.columnLength, 3);\n    });\n\n    test('delete a row with background and align (1)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n      );\n      // delete the row 1\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 1, columnIndex: 0);\n      await editorState.updateRowBackgroundColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      expect(tableCellNode.rowColors, {\n        '1': '0xFF0000FF',\n      });\n      await editorState.updateRowAlign(\n        tableCellNode: tableCellNode,\n        align: TableAlign.center,\n      );\n      expect(tableNode.rowAligns, {\n        '1': TableAlign.center.key,\n      });\n      await editorState.deleteRowInTable(tableNode, 1);\n      expect(tableNode.rowLength, 2);\n      expect(tableNode.columnLength, 4);\n      expect(tableCellNode.rowColors, {});\n      expect(tableNode.rowAligns, {});\n    });\n\n    test('delete a row with background and align (2)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n      );\n      // delete the row 1\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 1, columnIndex: 0);\n      await editorState.updateRowBackgroundColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      expect(tableCellNode.rowColors, {\n        '1': '0xFF0000FF',\n      });\n      await editorState.updateRowAlign(\n        tableCellNode: tableCellNode,\n        align: TableAlign.center,\n      );\n      expect(tableNode.rowAligns, {\n        '1': TableAlign.center.key,\n      });\n      await editorState.deleteRowInTable(tableNode, 0);\n      expect(tableNode.rowLength, 2);\n      expect(tableNode.columnLength, 4);\n      expect(tableCellNode.rowColors, {\n        '0': '0xFF0000FF',\n      });\n      expect(tableNode.rowAligns, {\n        '0': TableAlign.center.key,\n      });\n    });\n\n    test('delete a column with background and align (1)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n      );\n      // delete the column 1\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);\n      await editorState.updateColumnBackgroundColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      expect(tableCellNode.columnColors, {\n        '1': '0xFF0000FF',\n      });\n      await editorState.updateColumnAlign(\n        tableCellNode: tableCellNode,\n        align: TableAlign.center,\n      );\n      expect(tableNode.columnAligns, {\n        '1': TableAlign.center.key,\n      });\n      await editorState.deleteColumnInTable(tableNode, 1);\n      expect(tableNode.rowLength, 3);\n      expect(tableNode.columnLength, 3);\n      expect(tableCellNode.columnColors, {});\n      expect(tableNode.columnAligns, {});\n    });\n\n    test('delete a column with background (2)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n      );\n      // delete the column 1\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);\n      await editorState.updateColumnBackgroundColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      expect(tableCellNode.columnColors, {\n        '1': '0xFF0000FF',\n      });\n      await editorState.updateColumnAlign(\n        tableCellNode: tableCellNode,\n        align: TableAlign.center,\n      );\n      expect(tableNode.columnAligns, {\n        '1': TableAlign.center.key,\n      });\n      await editorState.deleteColumnInTable(tableNode, 0);\n      expect(tableNode.rowLength, 3);\n      expect(tableNode.columnLength, 3);\n      expect(tableCellNode.columnColors, {\n        '0': '0xFF0000FF',\n      });\n      expect(tableNode.columnAligns, {\n        '0': TableAlign.center.key,\n      });\n    });\n\n    test('delete a column with text color & bold style (1)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n      );\n      // delete the column 1\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);\n      await editorState.updateColumnTextColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      await editorState.toggleColumnBoldAttribute(\n        tableCellNode: tableCellNode,\n        isBold: true,\n      );\n      expect(tableNode.columnTextColors, {\n        '1': '0xFF0000FF',\n      });\n      expect(tableNode.columnBoldAttributes, {\n        '1': true,\n      });\n      await editorState.deleteColumnInTable(tableNode, 0);\n      expect(tableNode.columnTextColors, {\n        '0': '0xFF0000FF',\n      });\n      expect(tableNode.columnBoldAttributes, {\n        '0': true,\n      });\n      expect(tableNode.rowLength, 3);\n      expect(tableNode.columnLength, 3);\n    });\n\n    test('delete a column with text color & bold style (2)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n      );\n      // delete the column 1\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);\n      await editorState.updateColumnTextColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      await editorState.toggleColumnBoldAttribute(\n        tableCellNode: tableCellNode,\n        isBold: true,\n      );\n      expect(tableNode.columnTextColors, {\n        '1': '0xFF0000FF',\n      });\n      expect(tableNode.columnBoldAttributes, {\n        '1': true,\n      });\n      await editorState.deleteColumnInTable(tableNode, 1);\n      expect(tableNode.columnTextColors, {});\n      expect(tableNode.columnBoldAttributes, {});\n      expect(tableNode.rowLength, 3);\n      expect(tableNode.columnLength, 3);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_duplicate_operation_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'simple_table_test_helper.dart';\n\nvoid main() {\n  group('Simple table delete operation:', () {\n    setUpAll(() {\n      Log.shared.disableLog = true;\n    });\n\n    tearDownAll(() {\n      Log.shared.disableLog = false;\n    });\n\n    test('duplicate a row', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n      );\n      await editorState.duplicateRowInTable(tableNode, 0);\n      expect(tableNode.rowLength, 4);\n      expect(tableNode.columnLength, 4);\n    });\n\n    test('duplicate a column', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n      );\n      await editorState.duplicateColumnInTable(tableNode, 0);\n      expect(tableNode.rowLength, 3);\n      expect(tableNode.columnLength, 5);\n    });\n\n    test('duplicate a row with background and align (1)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n      );\n      // duplicate the row 1\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 1, columnIndex: 0);\n      await editorState.updateRowBackgroundColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      expect(tableCellNode.rowColors, {\n        '1': '0xFF0000FF',\n      });\n      await editorState.updateRowAlign(\n        tableCellNode: tableCellNode,\n        align: TableAlign.center,\n      );\n      expect(tableNode.rowAligns, {\n        '1': TableAlign.center.key,\n      });\n      await editorState.duplicateRowInTable(tableNode, 1);\n      expect(tableCellNode.rowColors, {\n        '1': '0xFF0000FF',\n        '2': '0xFF0000FF',\n      });\n      expect(tableNode.rowAligns, {\n        '1': TableAlign.center.key,\n        '2': TableAlign.center.key,\n      });\n    });\n\n    test('duplicate a row with background and align (2)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n      );\n      // duplicate the row 1\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 1, columnIndex: 0);\n      await editorState.updateRowBackgroundColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      expect(tableCellNode.rowColors, {\n        '1': '0xFF0000FF',\n      });\n      await editorState.updateRowAlign(\n        tableCellNode: tableCellNode,\n        align: TableAlign.center,\n      );\n      expect(tableNode.rowAligns, {\n        '1': TableAlign.center.key,\n      });\n      await editorState.duplicateRowInTable(tableNode, 2);\n      expect(tableCellNode.rowColors, {\n        '1': '0xFF0000FF',\n      });\n      expect(tableNode.rowAligns, {\n        '1': TableAlign.center.key,\n      });\n    });\n\n    test('duplicate a column with background and align (1)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n      );\n      // duplicate the column 1\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);\n      await editorState.updateColumnBackgroundColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      await editorState.updateColumnAlign(\n        tableCellNode: tableCellNode,\n        align: TableAlign.center,\n      );\n      expect(tableNode.columnColors, {\n        '1': '0xFF0000FF',\n      });\n      expect(tableNode.columnAligns, {\n        '1': TableAlign.center.key,\n      });\n      await editorState.duplicateColumnInTable(tableNode, 1);\n      expect(tableCellNode.columnColors, {\n        '1': '0xFF0000FF',\n        '2': '0xFF0000FF',\n      });\n      expect(tableNode.columnAligns, {\n        '1': TableAlign.center.key,\n        '2': TableAlign.center.key,\n      });\n    });\n\n    test('duplicate a column with background and align (2)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n      );\n      // duplicate the column 1\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);\n      await editorState.updateColumnBackgroundColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      await editorState.updateColumnAlign(\n        tableCellNode: tableCellNode,\n        align: TableAlign.center,\n      );\n      expect(tableNode.columnColors, {\n        '1': '0xFF0000FF',\n      });\n      expect(tableNode.columnAligns, {\n        '1': TableAlign.center.key,\n      });\n      await editorState.duplicateColumnInTable(tableNode, 2);\n      expect(tableCellNode.columnColors, {\n        '1': '0xFF0000FF',\n      });\n      expect(tableNode.columnAligns, {\n        '1': TableAlign.center.key,\n      });\n    });\n\n    test('duplicate a column with text color & bold style (1)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n      );\n      // duplicate the column 1\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);\n      await editorState.updateColumnTextColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      await editorState.toggleColumnBoldAttribute(\n        tableCellNode: tableCellNode,\n        isBold: true,\n      );\n      expect(tableNode.columnTextColors, {\n        '1': '0xFF0000FF',\n      });\n      expect(tableNode.columnBoldAttributes, {\n        '1': true,\n      });\n      await editorState.duplicateColumnInTable(tableNode, 1);\n      expect(tableNode.columnTextColors, {\n        '1': '0xFF0000FF',\n        '2': '0xFF0000FF',\n      });\n      expect(tableNode.columnBoldAttributes, {\n        '1': true,\n        '2': true,\n      });\n    });\n\n    test('duplicate a column with text color & bold style (2)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 3,\n        columnCount: 4,\n      );\n      // duplicate the column 1\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1);\n      await editorState.updateColumnTextColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      await editorState.toggleColumnBoldAttribute(\n        tableCellNode: tableCellNode,\n        isBold: true,\n      );\n      expect(tableNode.columnTextColors, {\n        '1': '0xFF0000FF',\n      });\n      expect(tableNode.columnBoldAttributes, {\n        '1': true,\n      });\n      await editorState.duplicateColumnInTable(tableNode, 0);\n      expect(tableNode.columnTextColors, {\n        '2': '0xFF0000FF',\n      });\n      expect(tableNode.columnBoldAttributes, {\n        '2': true,\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_header_operation_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'simple_table_test_helper.dart';\n\nvoid main() {\n  group('Simple table header operation:', () {\n    setUpAll(() {\n      Log.shared.disableLog = true;\n    });\n\n    tearDownAll(() {\n      Log.shared.disableLog = false;\n    });\n\n    test('enable header column in table', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n      // default is not header column\n      expect(tableNode.isHeaderColumnEnabled, false);\n      await editorState.toggleEnableHeaderColumn(\n        tableNode: tableNode,\n        enable: true,\n      );\n      expect(tableNode.isHeaderColumnEnabled, true);\n      await editorState.toggleEnableHeaderColumn(\n        tableNode: tableNode,\n        enable: false,\n      );\n      expect(tableNode.isHeaderColumnEnabled, false);\n      expect(tableNode.rowLength, 2);\n      expect(tableNode.columnLength, 3);\n    });\n\n    test('enable header row in table', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n      // default is not header row\n      expect(tableNode.isHeaderRowEnabled, false);\n      await editorState.toggleEnableHeaderRow(\n        tableNode: tableNode,\n        enable: true,\n      );\n      expect(tableNode.isHeaderRowEnabled, true);\n      await editorState.toggleEnableHeaderRow(\n        tableNode: tableNode,\n        enable: false,\n      );\n      expect(tableNode.isHeaderRowEnabled, false);\n      expect(tableNode.rowLength, 2);\n      expect(tableNode.columnLength, 3);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_insert_operation_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'simple_table_test_helper.dart';\n\nvoid main() {\n  group('Simple table insert operation:', () {\n    setUpAll(() {\n      Log.shared.disableLog = true;\n    });\n\n    tearDownAll(() {\n      Log.shared.disableLog = false;\n    });\n\n    test('add 2 rows in table', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n      await editorState.addRowInTable(tableNode);\n      await editorState.addRowInTable(tableNode);\n      expect(tableNode.rowLength, 4);\n      expect(tableNode.columnLength, 3);\n    });\n\n    test('add 2 columns in table', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n      await editorState.addColumnInTable(tableNode);\n      await editorState.addColumnInTable(tableNode);\n      expect(tableNode.rowLength, 2);\n      expect(tableNode.columnLength, 5);\n    });\n\n    test('add 2 rows and 2 columns in table', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n      await editorState.addColumnAndRowInTable(tableNode);\n      await editorState.addColumnAndRowInTable(tableNode);\n      expect(tableNode.rowLength, 4);\n      expect(tableNode.columnLength, 5);\n    });\n\n    test('insert a row at the first position in table', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n      await editorState.insertRowInTable(tableNode, 0);\n      expect(tableNode.rowLength, 3);\n      expect(tableNode.columnLength, 3);\n    });\n\n    test('insert a column at the first position in table', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n      await editorState.insertColumnInTable(tableNode, 0);\n      expect(tableNode.columnLength, 4);\n      expect(tableNode.rowLength, 2);\n    });\n\n    test('insert a row with background and align (1)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n      // insert the row at the first position\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0);\n      await editorState.updateRowBackgroundColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      expect(tableNode.rowColors, {\n        '0': '0xFF0000FF',\n      });\n      await editorState.updateRowAlign(\n        tableCellNode: tableCellNode,\n        align: TableAlign.center,\n      );\n      expect(tableNode.rowAligns, {\n        '0': TableAlign.center.key,\n      });\n      await editorState.insertRowInTable(tableNode, 0);\n      expect(tableNode.rowColors, {\n        '1': '0xFF0000FF',\n      });\n      expect(tableNode.rowAligns, {\n        '1': TableAlign.center.key,\n      });\n    });\n\n    test('insert a row with background and align (2)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n      // insert the row at the first position\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0);\n      await editorState.updateRowBackgroundColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      expect(tableNode.rowColors, {\n        '0': '0xFF0000FF',\n      });\n      await editorState.updateRowAlign(\n        tableCellNode: tableCellNode,\n        align: TableAlign.center,\n      );\n      expect(tableNode.rowAligns, {\n        '0': TableAlign.center.key,\n      });\n      await editorState.insertRowInTable(tableNode, 1);\n      expect(tableNode.rowColors, {\n        '0': '0xFF0000FF',\n      });\n      expect(tableNode.rowAligns, {\n        '0': TableAlign.center.key,\n      });\n    });\n\n    test('insert a column with background and align (1)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n      // insert the column at the first position\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0);\n      await editorState.updateColumnBackgroundColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      await editorState.updateColumnAlign(\n        tableCellNode: tableCellNode,\n        align: TableAlign.center,\n      );\n      expect(tableNode.columnColors, {\n        '0': '0xFF0000FF',\n      });\n      expect(tableNode.columnAligns, {\n        '0': TableAlign.center.key,\n      });\n      await editorState.insertColumnInTable(tableNode, 0);\n      expect(tableNode.columnColors, {\n        '1': '0xFF0000FF',\n      });\n      expect(tableNode.columnAligns, {\n        '1': TableAlign.center.key,\n      });\n    });\n\n    test('insert a column with background and align (1)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n      // insert the column at the first position\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0);\n      await editorState.updateColumnBackgroundColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      await editorState.updateColumnAlign(\n        tableCellNode: tableCellNode,\n        align: TableAlign.center,\n      );\n      expect(tableNode.columnColors, {\n        '0': '0xFF0000FF',\n      });\n      expect(tableNode.columnAligns, {\n        '0': TableAlign.center.key,\n      });\n      await editorState.insertColumnInTable(tableNode, 1);\n      expect(tableNode.columnColors, {\n        '0': '0xFF0000FF',\n      });\n      expect(tableNode.columnAligns, {\n        '0': TableAlign.center.key,\n      });\n    });\n\n    test('insert a column with text color & bold style (1)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n      // insert the column at the first position\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0);\n      await editorState.updateColumnTextColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      await editorState.toggleColumnBoldAttribute(\n        tableCellNode: tableCellNode,\n        isBold: true,\n      );\n      expect(tableNode.columnTextColors, {\n        '0': '0xFF0000FF',\n      });\n      expect(tableNode.columnBoldAttributes, {\n        '0': true,\n      });\n      await editorState.insertColumnInTable(tableNode, 0);\n      expect(tableNode.columnTextColors, {\n        '1': '0xFF0000FF',\n      });\n      expect(tableNode.columnBoldAttributes, {\n        '1': true,\n      });\n    });\n\n    test('insert a column with text color & bold style (2)', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n      // insert the column at the first position\n      final tableCellNode =\n          tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0);\n      await editorState.updateColumnTextColor(\n        tableCellNode: tableCellNode!,\n        color: '0xFF0000FF',\n      );\n      await editorState.toggleColumnBoldAttribute(\n        tableCellNode: tableCellNode,\n        isBold: true,\n      );\n      expect(tableNode.columnTextColors, {\n        '0': '0xFF0000FF',\n      });\n      expect(tableNode.columnBoldAttributes, {\n        '0': true,\n      });\n      await editorState.insertColumnInTable(tableNode, 1);\n      expect(tableNode.columnTextColors, {\n        '0': '0xFF0000FF',\n      });\n      expect(tableNode.columnBoldAttributes, {\n        '0': true,\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_markdown_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';\nimport 'package:appflowy/shared/markdown_to_document.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('Simple table markdown:', () {\n    setUpAll(() {\n      Log.shared.disableLog = true;\n    });\n\n    tearDownAll(() {\n      Log.shared.disableLog = false;\n    });\n\n    test('convert simple table to markdown (1)', () async {\n      final tableNode = createSimpleTableBlockNode(\n        columnCount: 7,\n        rowCount: 11,\n        contentBuilder: (rowIndex, columnIndex) =>\n            _sampleContents[rowIndex][columnIndex],\n      );\n      final markdown = const SimpleTableNodeParser().transform(\n        tableNode,\n        null,\n      );\n      expect(markdown,\n          '''|Index|Customer Id|First Name|Last Name|Company|City|Country|\n|---|---|---|---|---|---|---|\n|1|DD37Cf93aecA6Dc|Sheryl|Baxter|Rasmussen Group|East Leonard|Chile|\n|2|1Ef7b82A4CAAD10|Preston|Lozano|Vega-Gentry|East Jimmychester|Djibouti|\n|3|6F94879bDAfE5a6|Roy|Berry|Murillo-Perry|Isabelborough|Antigua and Barbuda|\n|4|5Cef8BFA16c5e3c|Linda|Olsen|Dominguez, Mcmillan and Donovan|Bensonview|Dominican Republic|\n|5|053d585Ab6b3159|Joanna|Bender|Martin, Lang and Andrade|West Priscilla|Slovakia (Slovak Republic)|\n|6|2d08FB17EE273F4|Aimee|Downs|Steele Group|Chavezborough|Bosnia and Herzegovina|\n|7|EAd384DfDbBf77|Darren|Peck|Lester, Woodard and Mitchell|Lake Ana|Pitcairn Islands|\n|8|0e04AFde9f225dE|Brett|Mullen|Sanford, Davenport and Giles|Kimport|Bulgaria|\n|9|C2dE4dEEc489ae0|Sheryl|Meyers|Browning-Simon|Robersonstad|Cyprus|\n|10|8C2811a503C7c5a|Michelle|Gallagher|Beck-Hendrix|Elaineberg|Timor-Leste|\n''');\n    });\n\n    test('convert markdown to simple table (1)', () async {\n      final document = customMarkdownToDocument(_sampleMarkdown1);\n      expect(document, isNotNull);\n      final tableNode = document.nodeAtPath([0])!;\n      expect(tableNode, isNotNull);\n      expect(tableNode.type, equals(SimpleTableBlockKeys.type));\n      expect(tableNode.rowLength, equals(4));\n      expect(tableNode.columnLength, equals(4));\n    });\n\n    test('convert markdown to simple table (2)', () async {\n      final document = customMarkdownToDocument(\n        _sampleMarkdown1,\n        tableWidth: 200,\n      );\n      expect(document, isNotNull);\n      final tableNode = document.nodeAtPath([0])!;\n      expect(tableNode, isNotNull);\n      expect(tableNode.type, equals(SimpleTableBlockKeys.type));\n      expect(tableNode.columnWidths.length, 4);\n      for (final entry in tableNode.columnWidths.entries) {\n        expect(entry.value, equals(200));\n      }\n    });\n  });\n}\n\nconst _sampleContents = <List<String>>[\n  [\n    \"Index\",\n    \"Customer Id\",\n    \"First Name\",\n    \"Last Name\",\n    \"Company\",\n    \"City\",\n    \"Country\",\n  ],\n  [\n    \"1\",\n    \"DD37Cf93aecA6Dc\",\n    \"Sheryl\",\n    \"Baxter\",\n    \"Rasmussen Group\",\n    \"East Leonard\",\n    \"Chile\",\n  ],\n  [\n    \"2\",\n    \"1Ef7b82A4CAAD10\",\n    \"Preston\",\n    \"Lozano\",\n    \"Vega-Gentry\",\n    \"East Jimmychester\",\n    \"Djibouti\",\n  ],\n  [\n    \"3\",\n    \"6F94879bDAfE5a6\",\n    \"Roy\",\n    \"Berry\",\n    \"Murillo-Perry\",\n    \"Isabelborough\",\n    \"Antigua and Barbuda\",\n  ],\n  [\n    \"4\",\n    \"5Cef8BFA16c5e3c\",\n    \"Linda\",\n    \"Olsen\",\n    \"Dominguez, Mcmillan and Donovan\",\n    \"Bensonview\",\n    \"Dominican Republic\",\n  ],\n  [\n    \"5\",\n    \"053d585Ab6b3159\",\n    \"Joanna\",\n    \"Bender\",\n    \"Martin, Lang and Andrade\",\n    \"West Priscilla\",\n    \"Slovakia (Slovak Republic)\",\n  ],\n  [\n    \"6\",\n    \"2d08FB17EE273F4\",\n    \"Aimee\",\n    \"Downs\",\n    \"Steele Group\",\n    \"Chavezborough\",\n    \"Bosnia and Herzegovina\",\n  ],\n  [\n    \"7\",\n    \"EAd384DfDbBf77\",\n    \"Darren\",\n    \"Peck\",\n    \"Lester, Woodard and Mitchell\",\n    \"Lake Ana\",\n    \"Pitcairn Islands\",\n  ],\n  [\n    \"8\",\n    \"0e04AFde9f225dE\",\n    \"Brett\",\n    \"Mullen\",\n    \"Sanford, Davenport and Giles\",\n    \"Kimport\",\n    \"Bulgaria\",\n  ],\n  [\n    \"9\",\n    \"C2dE4dEEc489ae0\",\n    \"Sheryl\",\n    \"Meyers\",\n    \"Browning-Simon\",\n    \"Robersonstad\",\n    \"Cyprus\",\n  ],\n  [\n    \"10\",\n    \"8C2811a503C7c5a\",\n    \"Michelle\",\n    \"Gallagher\",\n    \"Beck-Hendrix\",\n    \"Elaineberg\",\n    \"Timor-Leste\",\n  ],\n];\n\nconst _sampleMarkdown1 = '''|A|B|C||\n|---|---|---|---|\n|D|E|F||\n|1|2|3||\n|||||\n''';\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_reorder_operation_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'simple_table_test_helper.dart';\n\nvoid main() {\n  group('Simple table reorder operation:', () {\n    setUpAll(() {\n      Log.shared.disableLog = true;\n    });\n\n    tearDownAll(() {\n      Log.shared.disableLog = false;\n    });\n\n    group('reorder column', () {\n      test('reorder column from index 1 to index 2', () async {\n        final (editorState, tableNode) = createEditorStateAndTable(\n          rowCount: 4,\n          columnCount: 3,\n          contentBuilder: (rowIndex, columnIndex) =>\n              'cell $rowIndex-$columnIndex',\n        );\n        await editorState.reorderColumn(tableNode, fromIndex: 1, toIndex: 2);\n        expect(tableNode.columnLength, 3);\n        expect(tableNode.rowLength, 4);\n        expect(\n          tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0),\n          'cell 0-0',\n        );\n        expect(\n          tableNode.getTableCellContent(rowIndex: 0, columnIndex: 1),\n          'cell 0-2',\n        );\n        expect(\n          tableNode.getTableCellContent(rowIndex: 0, columnIndex: 2),\n          'cell 0-1',\n        );\n      });\n\n      test('reorder column from index 2 to index 0', () async {\n        final (editorState, tableNode) = createEditorStateAndTable(\n          rowCount: 4,\n          columnCount: 3,\n          contentBuilder: (rowIndex, columnIndex) =>\n              'cell $rowIndex-$columnIndex',\n        );\n        await editorState.reorderColumn(tableNode, fromIndex: 2, toIndex: 0);\n        expect(tableNode.columnLength, 3);\n        expect(tableNode.rowLength, 4);\n        expect(\n          tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0),\n          'cell 0-2',\n        );\n        expect(\n          tableNode.getTableCellContent(rowIndex: 0, columnIndex: 1),\n          'cell 0-0',\n        );\n        expect(\n          tableNode.getTableCellContent(rowIndex: 0, columnIndex: 2),\n          'cell 0-1',\n        );\n      });\n\n      test('reorder column with same index', () async {\n        final (editorState, tableNode) = createEditorStateAndTable(\n          rowCount: 4,\n          columnCount: 3,\n          contentBuilder: (rowIndex, columnIndex) =>\n              'cell $rowIndex-$columnIndex',\n        );\n        await editorState.reorderColumn(tableNode, fromIndex: 1, toIndex: 1);\n        expect(tableNode.columnLength, 3);\n        expect(tableNode.rowLength, 4);\n        expect(\n          tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0),\n          'cell 0-0',\n        );\n        expect(\n          tableNode.getTableCellContent(rowIndex: 0, columnIndex: 1),\n          'cell 0-1',\n        );\n        expect(\n          tableNode.getTableCellContent(rowIndex: 0, columnIndex: 2),\n          'cell 0-2',\n        );\n      });\n\n      test(\n          'reorder column from index 0 to index 2 with align/color/width attributes (1)',\n          () async {\n        final (editorState, tableNode) = createEditorStateAndTable(\n          rowCount: 4,\n          columnCount: 3,\n          contentBuilder: (rowIndex, columnIndex) =>\n              'cell $rowIndex-$columnIndex',\n        );\n\n        // before reorder\n        // Column 0: align: right, color: 0xFF0000, width: 100\n        // Column 1: align: center, color: 0x00FF00, width: 150\n        // Column 2: align: left, color: 0x0000FF, width: 200\n        await updateTableColumnAttributes(\n          editorState,\n          tableNode,\n          columnIndex: 0,\n          align: TableAlign.right,\n          color: '#FF0000',\n          width: 100,\n        );\n        await updateTableColumnAttributes(\n          editorState,\n          tableNode,\n          columnIndex: 1,\n          align: TableAlign.center,\n          color: '#00FF00',\n          width: 150,\n        );\n        await updateTableColumnAttributes(\n          editorState,\n          tableNode,\n          columnIndex: 2,\n          align: TableAlign.left,\n          color: '#0000FF',\n          width: 200,\n        );\n\n        // after reorder\n        // Column 0: align: center, color: 0x00FF00, width: 150\n        // Column 1: align: left, color: 0x0000FF, width: 200\n        // Column 2: align: right, color: 0xFF0000, width: 100\n        await editorState.reorderColumn(tableNode, fromIndex: 0, toIndex: 2);\n        expect(tableNode.columnLength, 3);\n        expect(tableNode.rowLength, 4);\n\n        expect(tableNode.columnAligns, {\n          \"0\": TableAlign.center.key,\n          \"1\": TableAlign.left.key,\n          \"2\": TableAlign.right.key,\n        });\n        expect(tableNode.columnColors, {\n          \"0\": '#00FF00',\n          \"1\": '#0000FF',\n          \"2\": '#FF0000',\n        });\n        expect(tableNode.columnWidths, {\n          \"0\": 150,\n          \"1\": 200,\n          \"2\": 100,\n        });\n      });\n\n      test(\n          'reorder column from index 0 to index 2 and reorder it back to index 0',\n          () async {\n        final (editorState, tableNode) = createEditorStateAndTable(\n          rowCount: 2,\n          columnCount: 3,\n          contentBuilder: (rowIndex, columnIndex) =>\n              'cell $rowIndex-$columnIndex',\n        );\n\n        // before reorder\n        // Column 0: null\n        // Column 1: align: center, color: 0x0000FF, width: 200\n        // Column 2: align: right, color: 0x0000FF, width: 250\n        await updateTableColumnAttributes(\n          editorState,\n          tableNode,\n          columnIndex: 1,\n          align: TableAlign.center,\n          color: '#FF0000',\n          width: 200,\n        );\n        await updateTableColumnAttributes(\n          editorState,\n          tableNode,\n          columnIndex: 2,\n          align: TableAlign.right,\n          color: '#0000FF',\n          width: 250,\n        );\n\n        // move column from index 0 to index 2\n        await editorState.reorderColumn(tableNode, fromIndex: 0, toIndex: 2);\n        // move column from index 2 to index 0\n        await editorState.reorderColumn(tableNode, fromIndex: 2, toIndex: 0);\n        expect(tableNode.columnLength, 3);\n        expect(tableNode.rowLength, 2);\n\n        expect(tableNode.columnAligns, {\n          \"1\": TableAlign.center.key,\n          \"2\": TableAlign.right.key,\n        });\n        expect(tableNode.columnColors, {\n          \"1\": '#FF0000',\n          \"2\": '#0000FF',\n        });\n        expect(tableNode.columnWidths, {\n          \"1\": 200,\n          \"2\": 250,\n        });\n      });\n    });\n\n    group('reorder row', () {\n      test('reorder row from index 1 to index 2', () async {\n        final (editorState, tableNode) = createEditorStateAndTable(\n          rowCount: 3,\n          columnCount: 2,\n          contentBuilder: (rowIndex, columnIndex) =>\n              'cell $rowIndex-$columnIndex',\n        );\n        await editorState.reorderRow(tableNode, fromIndex: 1, toIndex: 2);\n        expect(tableNode.columnLength, 2);\n        expect(tableNode.rowLength, 3);\n        expect(\n          tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0),\n          'cell 0-0',\n        );\n        expect(\n          tableNode.getTableCellContent(rowIndex: 1, columnIndex: 0),\n          'cell 2-0',\n        );\n        expect(\n          tableNode.getTableCellContent(rowIndex: 2, columnIndex: 0),\n          'cell 1-0',\n        );\n      });\n\n      test('reorder row from index 2 to index 0', () async {\n        final (editorState, tableNode) = createEditorStateAndTable(\n          rowCount: 3,\n          columnCount: 2,\n          contentBuilder: (rowIndex, columnIndex) =>\n              'cell $rowIndex-$columnIndex',\n        );\n        await editorState.reorderRow(tableNode, fromIndex: 2, toIndex: 0);\n        expect(tableNode.columnLength, 2);\n        expect(tableNode.rowLength, 3);\n        expect(\n          tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0),\n          'cell 2-0',\n        );\n        expect(\n          tableNode.getTableCellContent(rowIndex: 1, columnIndex: 0),\n          'cell 0-0',\n        );\n        expect(\n          tableNode.getTableCellContent(rowIndex: 2, columnIndex: 0),\n          'cell 1-0',\n        );\n      });\n\n      test('reorder row with same', () async {\n        final (editorState, tableNode) = createEditorStateAndTable(\n          rowCount: 3,\n          columnCount: 2,\n          contentBuilder: (rowIndex, columnIndex) =>\n              'cell $rowIndex-$columnIndex',\n        );\n        await editorState.reorderRow(tableNode, fromIndex: 1, toIndex: 1);\n        expect(tableNode.columnLength, 2);\n        expect(tableNode.rowLength, 3);\n        expect(\n          tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0),\n          'cell 0-0',\n        );\n        expect(\n          tableNode.getTableCellContent(rowIndex: 1, columnIndex: 0),\n          'cell 1-0',\n        );\n        expect(\n          tableNode.getTableCellContent(rowIndex: 2, columnIndex: 0),\n          'cell 2-0',\n        );\n      });\n\n      test('reorder row from index 0 to index 2 with align/color attributes',\n          () async {\n        final (editorState, tableNode) = createEditorStateAndTable(\n          rowCount: 3,\n          columnCount: 2,\n          contentBuilder: (rowIndex, columnIndex) =>\n              'cell $rowIndex-$columnIndex',\n        );\n\n        // before reorder\n        // Row 0: align: right, color: 0xFF0000\n        // Row 1: align: center, color: 0x00FF00\n        // Row 2: align: left, color: 0x0000FF\n        await updateTableRowAttributes(\n          editorState,\n          tableNode,\n          rowIndex: 0,\n          align: TableAlign.right,\n          color: '#FF0000',\n        );\n        await updateTableRowAttributes(\n          editorState,\n          tableNode,\n          rowIndex: 1,\n          align: TableAlign.center,\n          color: '#00FF00',\n        );\n        await updateTableRowAttributes(\n          editorState,\n          tableNode,\n          rowIndex: 2,\n          align: TableAlign.left,\n          color: '#0000FF',\n        );\n\n        // after reorder\n        // Row 0: align: center, color: 0x00FF00\n        // Row 1: align: left, color: 0x0000FF\n        // Row 2: align: right, color: 0xFF0000\n        await editorState.reorderRow(tableNode, fromIndex: 0, toIndex: 2);\n        expect(tableNode.columnLength, 2);\n        expect(tableNode.rowLength, 3);\n        expect(tableNode.rowAligns, {\n          \"0\": TableAlign.center.key,\n          \"1\": TableAlign.left.key,\n          \"2\": TableAlign.right.key,\n        });\n        expect(tableNode.rowColors, {\n          \"0\": '#00FF00',\n          \"1\": '#0000FF',\n          \"2\": '#FF0000',\n        });\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_style_operation_test.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'simple_table_test_helper.dart';\n\nvoid main() {\n  group('Simple table style operation:', () {\n    setUpAll(() {\n      Log.shared.disableLog = true;\n    });\n\n    tearDownAll(() {\n      Log.shared.disableLog = false;\n    });\n\n    test('update column width in memory', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n      // check the default column width\n      expect(tableNode.columnWidths, isEmpty);\n      final tableCellNode = tableNode.getTableCellNode(\n        rowIndex: 0,\n        columnIndex: 0,\n      );\n      await editorState.updateColumnWidthInMemory(\n        tableCellNode: tableCellNode!,\n        deltaX: 100,\n      );\n      expect(tableNode.columnWidths, {\n        '0': SimpleTableConstants.defaultColumnWidth + 100,\n      });\n\n      // set the width less than the minimum column width\n      await editorState.updateColumnWidthInMemory(\n        tableCellNode: tableCellNode,\n        deltaX: -1000,\n      );\n      expect(tableNode.columnWidths, {\n        '0': SimpleTableConstants.minimumColumnWidth,\n      });\n    });\n\n    test('update column width', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n      expect(tableNode.columnWidths, isEmpty);\n\n      for (var i = 0; i < tableNode.columnLength; i++) {\n        final tableCellNode = tableNode.getTableCellNode(\n          rowIndex: 0,\n          columnIndex: i,\n        );\n        await editorState.updateColumnWidth(\n          tableCellNode: tableCellNode!,\n          width: 100,\n        );\n      }\n      expect(tableNode.columnWidths, {\n        '0': 100,\n        '1': 100,\n        '2': 100,\n      });\n\n      // set the width less than the minimum column width\n      for (var i = 0; i < tableNode.columnLength; i++) {\n        final tableCellNode = tableNode.getTableCellNode(\n          rowIndex: 0,\n          columnIndex: i,\n        );\n        await editorState.updateColumnWidth(\n          tableCellNode: tableCellNode!,\n          width: -1000,\n        );\n      }\n      expect(tableNode.columnWidths, {\n        '0': SimpleTableConstants.minimumColumnWidth,\n        '1': SimpleTableConstants.minimumColumnWidth,\n        '2': SimpleTableConstants.minimumColumnWidth,\n      });\n    });\n\n    test('update column align', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n      for (var i = 0; i < tableNode.columnLength; i++) {\n        final tableCellNode = tableNode.getTableCellNode(\n          rowIndex: 0,\n          columnIndex: i,\n        );\n        await editorState.updateColumnAlign(\n          tableCellNode: tableCellNode!,\n          align: TableAlign.center,\n        );\n      }\n      expect(tableNode.columnAligns, {\n        '0': TableAlign.center.key,\n        '1': TableAlign.center.key,\n        '2': TableAlign.center.key,\n      });\n    });\n\n    test('update row align', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n\n      for (var i = 0; i < tableNode.rowLength; i++) {\n        final tableCellNode = tableNode.getTableCellNode(\n          rowIndex: i,\n          columnIndex: 0,\n        );\n        await editorState.updateRowAlign(\n          tableCellNode: tableCellNode!,\n          align: TableAlign.center,\n        );\n      }\n\n      expect(tableNode.rowAligns, {\n        '0': TableAlign.center.key,\n        '1': TableAlign.center.key,\n      });\n    });\n\n    test('update column background color', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n\n      for (var i = 0; i < tableNode.columnLength; i++) {\n        final tableCellNode = tableNode.getTableCellNode(\n          rowIndex: 0,\n          columnIndex: i,\n        );\n        await editorState.updateColumnBackgroundColor(\n          tableCellNode: tableCellNode!,\n          color: '0xFF0000FF',\n        );\n      }\n      expect(tableNode.columnColors, {\n        '0': '0xFF0000FF',\n        '1': '0xFF0000FF',\n        '2': '0xFF0000FF',\n      });\n    });\n\n    test('update row background color', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n\n      for (var i = 0; i < tableNode.rowLength; i++) {\n        final tableCellNode = tableNode.getTableCellNode(\n          rowIndex: i,\n          columnIndex: 0,\n        );\n        await editorState.updateRowBackgroundColor(\n          tableCellNode: tableCellNode!,\n          color: '0xFF0000FF',\n        );\n      }\n\n      expect(tableNode.rowColors, {\n        '0': '0xFF0000FF',\n        '1': '0xFF0000FF',\n      });\n    });\n\n    test('update table align', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n\n      for (final align in [\n        TableAlign.center,\n        TableAlign.right,\n        TableAlign.left,\n      ]) {\n        await editorState.updateTableAlign(\n          tableNode: tableNode,\n          align: align,\n        );\n        expect(tableNode.tableAlign, align);\n      }\n    });\n\n    test('clear the existing align of the column before updating', () async {\n      final (editorState, tableNode) = createEditorStateAndTable(\n        rowCount: 2,\n        columnCount: 3,\n      );\n\n      final firstCellNode = tableNode.getTableCellNode(\n        rowIndex: 0,\n        columnIndex: 0,\n      );\n\n      Node firstParagraphNode = firstCellNode!.children.first;\n\n      // format the first paragraph to center align\n      final transaction = editorState.transaction;\n      transaction.updateNode(\n        firstParagraphNode,\n        {\n          blockComponentAlign: TableAlign.right.key,\n        },\n      );\n      await editorState.apply(transaction);\n\n      firstParagraphNode = editorState.getNodeAtPath([0, 0, 0, 0])!;\n      expect(\n        firstParagraphNode.attributes[blockComponentAlign],\n        TableAlign.right.key,\n      );\n\n      await editorState.updateColumnAlign(\n        tableCellNode: firstCellNode,\n        align: TableAlign.center,\n      );\n\n      expect(\n        firstParagraphNode.attributes[blockComponentAlign],\n        null,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_test_helper.dart",
    "content": "import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';\nimport 'package:appflowy_editor/appflowy_editor.dart';\n\n(EditorState editorState, Node tableNode) createEditorStateAndTable({\n  required int rowCount,\n  required int columnCount,\n  String? defaultContent,\n  String Function(int rowIndex, int columnIndex)? contentBuilder,\n}) {\n  final document = Document.blank()\n    ..insert(\n      [0],\n      [\n        createSimpleTableBlockNode(\n          columnCount: columnCount,\n          rowCount: rowCount,\n          defaultContent: defaultContent,\n          contentBuilder: contentBuilder,\n        ),\n      ],\n    );\n  final editorState = EditorState(document: document);\n  return (editorState, document.nodeAtPath([0])!);\n}\n\nFuture<void> updateTableColumnAttributes(\n  EditorState editorState,\n  Node tableNode, {\n  required int columnIndex,\n  TableAlign? align,\n  String? color,\n  double? width,\n}) async {\n  final cell = tableNode.getTableCellNode(\n    rowIndex: 0,\n    columnIndex: columnIndex,\n  )!;\n\n  if (align != null) {\n    await editorState.updateColumnAlign(\n      tableCellNode: cell,\n      align: align,\n    );\n  }\n\n  if (color != null) {\n    await editorState.updateColumnBackgroundColor(\n      tableCellNode: cell,\n      color: color,\n    );\n  }\n\n  if (width != null) {\n    await editorState.updateColumnWidth(\n      tableCellNode: cell,\n      width: width,\n    );\n  }\n}\n\nFuture<void> updateTableRowAttributes(\n  EditorState editorState,\n  Node tableNode, {\n  required int rowIndex,\n  TableAlign? align,\n  String? color,\n}) async {\n  final cell = tableNode.getTableCellNode(\n    rowIndex: rowIndex,\n    columnIndex: 0,\n  )!;\n\n  if (align != null) {\n    await editorState.updateRowAlign(\n      tableCellNode: cell,\n      align: align,\n    );\n  }\n\n  if (color != null) {\n    await editorState.updateRowBackgroundColor(\n      tableCellNode: cell,\n      color: color,\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/theme/theme_test.dart",
    "content": "import 'package:flowy_infra/colorscheme/colorscheme.dart';\nimport 'package:flowy_infra/plugins/service/location_service.dart';\nimport 'package:flowy_infra/plugins/service/models/flowy_dynamic_plugin.dart';\nimport 'package:flowy_infra/plugins/service/plugin_service.dart';\nimport 'package:flowy_infra/theme.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nclass MockPluginService implements FlowyPluginService {\n  @override\n  Future<void> addPlugin(FlowyDynamicPlugin plugin) =>\n      throw UnimplementedError();\n\n  @override\n  Future<FlowyDynamicPlugin?> lookup({required String name}) =>\n      throw UnimplementedError();\n\n  @override\n  Future<DynamicPluginLibrary> get plugins async => const Iterable.empty();\n\n  @override\n  void setLocation(PluginLocationService locationService) =>\n      throw UnimplementedError();\n}\n\nvoid main() {\n  WidgetsFlutterBinding.ensureInitialized();\n  group('AppTheme', () {\n    test('fallback theme', () {\n      const theme = AppTheme.fallback;\n\n      expect(theme.builtIn, true);\n      expect(theme.themeName, BuiltInTheme.defaultTheme);\n      expect(theme.lightTheme, isA<FlowyColorScheme>());\n      expect(theme.darkTheme, isA<FlowyColorScheme>());\n    });\n\n    test('built-in themes', () {\n      final themes = AppTheme.builtins;\n\n      expect(themes, isNotEmpty);\n      for (final theme in themes) {\n        expect(theme.builtIn, true);\n        expect(\n          theme.themeName,\n          anyOf([\n            BuiltInTheme.defaultTheme,\n            BuiltInTheme.dandelion,\n            BuiltInTheme.lavender,\n            BuiltInTheme.lemonade,\n          ]),\n        );\n        expect(theme.lightTheme, isA<FlowyColorScheme>());\n        expect(theme.darkTheme, isA<FlowyColorScheme>());\n      }\n    });\n\n    test('fromName returns existing theme', () async {\n      final theme = await AppTheme.fromName(\n        BuiltInTheme.defaultTheme,\n        pluginService: MockPluginService(),\n      );\n\n      expect(theme, isNotNull);\n      expect(theme.builtIn, true);\n      expect(theme.themeName, BuiltInTheme.defaultTheme);\n      expect(theme.lightTheme, isA<FlowyColorScheme>());\n      expect(theme.darkTheme, isA<FlowyColorScheme>());\n    });\n\n    test('fromName throws error for non-existent theme', () async {\n      expect(\n        () async => AppTheme.fromName(\n          'bogus',\n          pluginService: MockPluginService(),\n        ),\n        throwsArgumentError,\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/url_launcher/url_launcher_test.dart",
    "content": "import 'package:appflowy/shared/patterns/common_patterns.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('url launcher unit test', () {\n    test('launch local uri', () async {\n      const localUris = [\n        'file://path/to/file.txt',\n        '/path/to/file.txt',\n        'C:\\\\path\\\\to\\\\file.txt',\n        '../path/to/file.txt',\n      ];\n      for (final uri in localUris) {\n        final result = localPathRegex.hasMatch(uri);\n        expect(result, true);\n      }\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/util/recent_icons_test.dart",
    "content": "import 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';\nimport 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy_backend/log.dart';\nimport 'package:collection/collection.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nvoid main() {\n  setUpAll(() {\n    TestWidgetsFlutterBinding.ensureInitialized();\n    SharedPreferences.setMockInitialValues({});\n    getIt.registerFactory<KeyValueStorage>(() => DartKeyValue());\n    Log.shared.disableLog = true;\n  });\n\n  bool equalIcon(RecentIcon a, RecentIcon b) =>\n      a.groupName == b.groupName &&\n      a.name == b.name &&\n      a.keywords.equals(b.keywords) &&\n      a.content == b.content;\n\n  test('putEmoji', () async {\n    List<String> emojiIds = await RecentIcons.getEmojiIds();\n    assert(emojiIds.isEmpty);\n\n    await RecentIcons.putEmoji('1');\n    emojiIds = await RecentIcons.getEmojiIds();\n    assert(emojiIds.equals(['1']));\n\n    await RecentIcons.putEmoji('2');\n    assert(emojiIds.equals(['2', '1']));\n\n    await RecentIcons.putEmoji('1');\n    emojiIds = await RecentIcons.getEmojiIds();\n    assert(emojiIds.equals(['1', '2']));\n\n    for (var i = 0; i < RecentIcons.maxLength; ++i) {\n      await RecentIcons.putEmoji('${i + 100}');\n    }\n    emojiIds = await RecentIcons.getEmojiIds();\n    assert(emojiIds.length == RecentIcons.maxLength);\n    assert(\n      emojiIds.equals(\n        List.generate(RecentIcons.maxLength, (i) => '${i + 100}')\n            .reversed\n            .toList(),\n      ),\n    );\n  });\n\n  test('putIcons', () async {\n    List<RecentIcon> icons = await RecentIcons.getIcons();\n    assert(icons.isEmpty);\n    await loadIconGroups();\n    final groups = kIconGroups!;\n    final List<RecentIcon> localIcons = [];\n    for (final e in groups) {\n      localIcons.addAll(e.icons.map((e) => RecentIcon(e, e.name)).toList());\n    }\n\n    await RecentIcons.putIcon(localIcons.first);\n    icons = await RecentIcons.getIcons();\n    assert(icons.length == 1);\n    assert(equalIcon(icons.first, localIcons.first));\n\n    await RecentIcons.putIcon(localIcons[1]);\n    icons = await RecentIcons.getIcons();\n    assert(icons.length == 2);\n    assert(equalIcon(icons[0], localIcons[1]));\n    assert(equalIcon(icons[1], localIcons[0]));\n\n    await RecentIcons.putIcon(localIcons.first);\n    icons = await RecentIcons.getIcons();\n    assert(icons.length == 2);\n    assert(equalIcon(icons[1], localIcons[1]));\n    assert(equalIcon(icons[0], localIcons[0]));\n\n    for (var i = 0; i < RecentIcons.maxLength; ++i) {\n      await RecentIcons.putIcon(localIcons[10 + i]);\n    }\n\n    icons = await RecentIcons.getIcons();\n    assert(icons.length == RecentIcons.maxLength);\n\n    for (var i = 0; i < RecentIcons.maxLength; ++i) {\n      assert(\n        equalIcon(icons[RecentIcons.maxLength - i - 1], localIcons[10 + i]),\n      );\n    }\n  });\n\n  test('put without group name', () async {\n    RecentIcons.clear();\n    List<RecentIcon> icons = await RecentIcons.getIcons();\n    assert(icons.isEmpty);\n    await loadIconGroups();\n    final groups = kIconGroups!;\n    final List<RecentIcon> localIcons = [];\n    for (final e in groups) {\n      localIcons.addAll(e.icons.map((e) => RecentIcon(e, e.name)).toList());\n    }\n\n    await RecentIcons.putIcon(RecentIcon(localIcons.first.icon, ''));\n    icons = await RecentIcons.getIcons();\n    assert(icons.isEmpty);\n\n    await RecentIcons.putIcon(\n      RecentIcon(localIcons.first.icon, 'Test group name'),\n    );\n    icons = await RecentIcons.getIcons();\n    assert(icons.isNotEmpty);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/unit_test/util/time.dart",
    "content": "import 'package:appflowy/util/time.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  test('parseTime should parse time string to minutes', () {\n    expect(parseTime('10'), 10);\n    expect(parseTime('70m'), 70);\n    expect(parseTime('4h 20m'), 260);\n    expect(parseTime('1h 80m'), 140);\n    expect(parseTime('asffsa2h3m'), null);\n    expect(parseTime('2h3m'), null);\n    expect(parseTime('blah'), null);\n    expect(parseTime('10a'), null);\n    expect(parseTime('2h'), 120);\n  });\n\n  test('formatTime should format time minutes to formatted string', () {\n    expect(formatTime(5), \"5m\");\n    expect(formatTime(75), \"1h 15m\");\n    expect(formatTime(120), \"2h\");\n    expect(formatTime(-50), \"\");\n    expect(formatTime(0), \"0m\");\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/util.dart",
    "content": "import 'package:appflowy/startup/launch_configuration.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:appflowy/user/application/auth/auth_service.dart';\nimport 'package:appflowy/user/application/user_service.dart';\nimport 'package:appflowy/workspace/application/workspace/workspace_service.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';\nimport 'package:flowy_infra/uuid.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nclass AppFlowyUnitTest {\n  late UserProfilePB userProfile;\n  late UserBackendService userService;\n  late WorkspaceService workspaceService;\n  late WorkspacePB workspace;\n\n  static Future<AppFlowyUnitTest> ensureInitialized() async {\n    TestWidgetsFlutterBinding.ensureInitialized();\n    SharedPreferences.setMockInitialValues({});\n    _pathProviderInitialized();\n\n    await FlowyRunner.run(\n      AppFlowyApplicationUnitTest(),\n      IntegrationMode.unitTest,\n    );\n\n    final test = AppFlowyUnitTest();\n    await test._signIn();\n    await test._loadWorkspace();\n\n    await test._initialServices();\n    return test;\n  }\n\n  Future<void> _signIn() async {\n    final authService = getIt<AuthService>();\n    const password = \"AppFlowy123@\";\n    final uid = uuid();\n    final userEmail = \"$uid@appflowy.io\";\n    final result = await authService.signUp(\n      name: \"TestUser\",\n      password: password,\n      email: userEmail,\n    );\n    result.fold(\n      (user) {\n        userProfile = user;\n        userService = UserBackendService(userId: userProfile.id);\n      },\n      (error) {\n        assert(false, 'Error: $error');\n      },\n    );\n  }\n\n  WorkspacePB get currentWorkspace => workspace;\n  Future<void> _loadWorkspace() async {\n    final result = await UserBackendService.getCurrentWorkspace();\n    result.fold(\n      (value) => workspace = value,\n      (error) {\n        throw Exception(error);\n      },\n    );\n  }\n\n  Future<void> _initialServices() async {\n    workspaceService = WorkspaceService(\n      workspaceId: currentWorkspace.id,\n      userId: userProfile.id,\n    );\n  }\n\n  Future<ViewPB> createWorkspace() async {\n    final result = await workspaceService.createView(\n      name: \"Test App\",\n      viewSection: ViewSectionPB.Public,\n    );\n    return result.fold(\n      (app) => app,\n      (error) => throw Exception(error),\n    );\n  }\n}\n\nvoid _pathProviderInitialized() {\n  const MethodChannel channel =\n      MethodChannel('plugins.flutter.io/path_provider');\n  TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger\n      .setMockMethodCallHandler(channel, (MethodCall methodCall) async {\n    return '.';\n  });\n}\n\nclass AppFlowyApplicationUnitTest implements EntryPoint {\n  @override\n  Widget create(LaunchConfiguration config) {\n    return const SizedBox.shrink();\n  }\n}\n\nFuture<void> blocResponseFuture({int millisecond = 200}) {\n  return Future.delayed(Duration(milliseconds: millisecond));\n}\n\nDuration blocResponseDuration({int milliseconds = 200}) {\n  return Duration(milliseconds: milliseconds);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/confirm_dialog_test.dart",
    "content": "import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\n\nimport '../../integration_test/shared/util.dart';\nimport 'test_material_app.dart';\n\nclass _ConfirmPopupMock extends Mock {\n  void confirm();\n}\n\nvoid main() {\n  setUpAll(() async {\n    SharedPreferences.setMockInitialValues({});\n    EasyLocalization.logger.enableLevels = [];\n    await EasyLocalization.ensureInitialized();\n  });\n\n  Widget buildDialog(VoidCallback onConfirm) {\n    return Builder(\n      builder: (context) {\n        return TextButton(\n          child: const Text(\"\"),\n          onPressed: () {\n            showDialog(\n              context: context,\n              builder: (_) {\n                return AppFlowyTheme(\n                  data: AppFlowyDefaultTheme().light(),\n                  child: Dialog(\n                    shape: RoundedRectangleBorder(\n                      borderRadius: BorderRadius.circular(12.0),\n                    ),\n                    child: ConfirmPopup(\n                      description: \"desc\",\n                      title: \"title\",\n                      onConfirm: (_) => onConfirm(),\n                    ),\n                  ),\n                );\n              },\n            );\n          },\n        );\n      },\n    );\n  }\n\n  testWidgets('confirm dialog shortcut events', (tester) async {\n    final callback = _ConfirmPopupMock();\n\n    // escape\n    await tester.pumpWidget(\n      WidgetTestApp(\n        child: buildDialog(callback.confirm),\n      ),\n    );\n    await tester.pumpAndSettle();\n\n    await tester.tap(find.byType(TextButton));\n    await tester.pumpAndSettle();\n\n    expect(find.byType(ConfirmPopup), findsOneWidget);\n\n    await tester.simulateKeyEvent(LogicalKeyboardKey.escape);\n    verifyNever(() => callback.confirm());\n\n    verifyNever(() => callback.confirm());\n    expect(find.byType(ConfirmPopup), findsNothing);\n\n    // enter\n    await tester.pumpWidget(\n      WidgetTestApp(\n        child: buildDialog(callback.confirm),\n      ),\n    );\n    await tester.pumpAndSettle();\n\n    await tester.tap(find.byType(TextButton));\n    await tester.pumpAndSettle();\n\n    expect(find.byType(ConfirmPopup), findsOneWidget);\n\n    await tester.simulateKeyEvent(LogicalKeyboardKey.enter);\n    verify(() => callback.confirm()).called(1);\n\n    verifyNever(() => callback.confirm());\n    expect(find.byType(ConfirmPopup), findsNothing);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/date_picker_test.dart",
    "content": "import 'package:appflowy/generated/flowy_svgs.g.dart';\nimport 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';\nimport 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_time_text_field.dart';\nimport 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart';\nimport 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:shared_preferences/shared_preferences.dart';\nimport 'package:table_calendar/table_calendar.dart';\nimport 'package:time/time.dart';\n\nimport '../../integration_test/shared/util.dart';\nimport 'test_material_app.dart';\n\nconst _mockDatePickerDelay = Duration(milliseconds: 200);\n\nclass _DatePickerDataStub {\n  _DatePickerDataStub({\n    required this.dateTime,\n    required this.endDateTime,\n    required this.includeTime,\n    required this.isRange,\n  });\n\n  _DatePickerDataStub.empty()\n      : dateTime = null,\n        endDateTime = null,\n        includeTime = false,\n        isRange = false;\n\n  DateTime? dateTime;\n  DateTime? endDateTime;\n  bool includeTime;\n  bool isRange;\n}\n\nclass _MockDatePicker extends StatefulWidget {\n  const _MockDatePicker({\n    this.data,\n    this.dateFormat,\n    this.timeFormat,\n  });\n\n  final _DatePickerDataStub? data;\n  final DateFormatPB? dateFormat;\n  final TimeFormatPB? timeFormat;\n\n  @override\n  State<_MockDatePicker> createState() => _MockDatePickerState();\n}\n\nclass _MockDatePickerState extends State<_MockDatePicker> {\n  late final _DatePickerDataStub data;\n  late DateFormatPB dateFormat;\n  late TimeFormatPB timeFormat;\n\n  @override\n  void initState() {\n    super.initState();\n    data = widget.data ?? _DatePickerDataStub.empty();\n    dateFormat = widget.dateFormat ?? DateFormatPB.Friendly;\n    timeFormat = widget.timeFormat ?? TimeFormatPB.TwelveHour;\n  }\n\n  void updateDateFormat(DateFormatPB dateFormat) async {\n    setState(() {\n      this.dateFormat = dateFormat;\n    });\n  }\n\n  void updateTimeFormat(TimeFormatPB timeFormat) async {\n    setState(() {\n      this.timeFormat = timeFormat;\n    });\n  }\n\n  void updateDateCellData({\n    required DateTime? dateTime,\n    required DateTime? endDateTime,\n    required bool isRange,\n    required bool includeTime,\n  }) {\n    setState(() {\n      data.dateTime = dateTime;\n      data.endDateTime = endDateTime;\n      data.includeTime = includeTime;\n      data.isRange = isRange;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return DesktopAppFlowyDatePicker(\n      dateTime: data.dateTime,\n      endDateTime: data.endDateTime,\n      includeTime: data.includeTime,\n      isRange: data.isRange,\n      dateFormat: dateFormat,\n      timeFormat: timeFormat,\n      onDaySelected: (date) async {\n        await Future.delayed(_mockDatePickerDelay);\n        setState(() {\n          data.dateTime = date;\n        });\n      },\n      onRangeSelected: (start, end) async {\n        await Future.delayed(_mockDatePickerDelay);\n        setState(() {\n          data.dateTime = start;\n          data.endDateTime = end;\n        });\n      },\n      onIncludeTimeChanged: (value, dateTime, endDateTime) async {\n        await Future.delayed(_mockDatePickerDelay);\n        setState(() {\n          data.includeTime = value;\n          if (dateTime != null) {\n            data.dateTime = dateTime;\n          }\n          if (endDateTime != null) {\n            data.endDateTime = endDateTime;\n          }\n        });\n      },\n      onIsRangeChanged: (value, dateTime, endDateTime) async {\n        await Future.delayed(_mockDatePickerDelay);\n        setState(() {\n          data.isRange = value;\n          if (dateTime != null) {\n            data.dateTime = dateTime;\n          }\n          if (endDateTime != null) {\n            data.endDateTime = endDateTime;\n          }\n        });\n      },\n    );\n  }\n}\n\nvoid main() {\n  setUpAll(() async {\n    SharedPreferences.setMockInitialValues({});\n    EasyLocalization.logger.enableLevels = [];\n    await EasyLocalization.ensureInitialized();\n  });\n\n  Finder dayInDatePicker(int day) {\n    final findCalendar = find.byType(TableCalendar);\n    final findDay = find.text(day.toString());\n\n    return find.descendant(\n      of: findCalendar,\n      matching: findDay,\n    );\n  }\n\n  DateTime getLastMonth(DateTime date) {\n    if (date.month == 1) {\n      return DateTime(date.year - 1, 12);\n    } else {\n      return DateTime(date.year, date.month - 1);\n    }\n  }\n\n  _MockDatePickerState getMockState(WidgetTester tester) =>\n      tester.state<_MockDatePickerState>(find.byType(_MockDatePicker));\n\n  AppFlowyDatePickerState getAfState(WidgetTester tester) =>\n      tester.state<DesktopAppFlowyDatePickerState>(\n        find.byType(DesktopAppFlowyDatePicker),\n      );\n\n  group('AppFlowy date picker:', () {\n    testWidgets('default state', (tester) async {\n      await tester.pumpWidget(\n        const WidgetTestApp(\n          child: _MockDatePicker(),\n        ),\n      );\n\n      await tester.pumpAndSettle();\n\n      expect(find.byType(DesktopAppFlowyDatePicker), findsOneWidget);\n      expect(\n        find.byWidgetPredicate(\n          (w) => w is DateTimeTextField && w.dateTime == null,\n        ),\n        findsOneWidget,\n      );\n      expect(\n        find.byWidgetPredicate((w) => w is DatePicker && w.selectedDay == null),\n        findsOneWidget,\n      );\n      expect(\n        find.byWidgetPredicate((w) => w is IncludeTimeButton && !w.includeTime),\n        findsOneWidget,\n      );\n      expect(\n        find.byWidgetPredicate((w) => w is EndTimeButton && !w.isRange),\n        findsOneWidget,\n      );\n    });\n\n    testWidgets('passed in state', (tester) async {\n      await tester.pumpWidget(\n        WidgetTestApp(\n          child: _MockDatePicker(\n            data: _DatePickerDataStub(\n              dateTime: DateTime(2024, 10, 12, 13),\n              endDateTime: DateTime(2024, 10, 14, 5),\n              includeTime: true,\n              isRange: true,\n            ),\n          ),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(find.byType(DesktopAppFlowyDatePicker), findsOneWidget);\n      expect(find.byType(DateTimeTextField), findsNWidgets(2));\n      expect(find.byType(DatePicker), findsOneWidget);\n      expect(\n        find.byWidgetPredicate((w) => w is IncludeTimeButton && w.includeTime),\n        findsOneWidget,\n      );\n      expect(\n        find.byWidgetPredicate((w) => w is EndTimeButton && w.isRange),\n        findsOneWidget,\n      );\n      final afState = getAfState(tester);\n      expect(afState.focusedDateTime, DateTime(2024, 10, 12, 13));\n    });\n\n    testWidgets('date and time formats', (tester) async {\n      final date = DateTime(2024, 10, 12, 13);\n      await tester.pumpWidget(\n        WidgetTestApp(\n          child: _MockDatePicker(\n            dateFormat: DateFormatPB.Friendly,\n            timeFormat: TimeFormatPB.TwelveHour,\n            data: _DatePickerDataStub(\n              dateTime: date,\n              endDateTime: null,\n              includeTime: true,\n              isRange: false,\n            ),\n          ),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      final dateText = find.descendant(\n        of: find.byKey(const ValueKey('date_time_text_field_date')),\n        matching:\n            find.text(DateFormat(DateFormatPB.Friendly.pattern).format(date)),\n      );\n      expect(dateText, findsOneWidget);\n\n      final timeText = find.descendant(\n        of: find.byKey(const ValueKey('date_time_text_field_time')),\n        matching:\n            find.text(DateFormat(TimeFormatPB.TwelveHour.pattern).format(date)),\n      );\n      expect(timeText, findsOneWidget);\n\n      _MockDatePickerState mockState = getMockState(tester);\n      mockState.updateDateFormat(DateFormatPB.US);\n      await tester.pumpAndSettle();\n      final dateText2 = find.descendant(\n        of: find.byKey(const ValueKey('date_time_text_field_date')),\n        matching: find.text(DateFormat(DateFormatPB.US.pattern).format(date)),\n      );\n      expect(dateText2, findsOneWidget);\n\n      mockState = getMockState(tester);\n      mockState.updateTimeFormat(TimeFormatPB.TwentyFourHour);\n      await tester.pumpAndSettle();\n      final timeText2 = find.descendant(\n        of: find.byKey(const ValueKey('date_time_text_field_time')),\n        matching: find\n            .text(DateFormat(TimeFormatPB.TwentyFourHour.pattern).format(date)),\n      );\n      expect(timeText2, findsOneWidget);\n    });\n\n    testWidgets('page turn buttons', (tester) async {\n      await tester.pumpWidget(\n        const WidgetTestApp(\n          child: _MockDatePicker(),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      final now = DateTime.now();\n      expect(\n        find.text(DateFormat.yMMMM().format(now)),\n        findsOneWidget,\n      );\n\n      final lastMonth = getLastMonth(now);\n      await tester.tap(find.byFlowySvg(FlowySvgs.arrow_left_s));\n      await tester.pumpAndSettle();\n      expect(\n        find.text(DateFormat.yMMMM().format(lastMonth)),\n        findsOneWidget,\n      );\n\n      await tester.tap(find.byFlowySvg(FlowySvgs.arrow_right_s));\n      await tester.pumpAndSettle();\n      expect(\n        find.text(DateFormat.yMMMM().format(now)),\n        findsOneWidget,\n      );\n    });\n\n    testWidgets('select date', (tester) async {\n      await tester.pumpWidget(\n        const WidgetTestApp(\n          child: _MockDatePicker(),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      final now = DateTime.now();\n      final third = dayInDatePicker(3).first;\n      await tester.tap(third);\n      await tester.pump();\n\n      DateTime expected = DateTime(now.year, now.month, 3);\n\n      AppFlowyDatePickerState afState = getAfState(tester);\n      _MockDatePickerState mockState = getMockState(tester);\n      expect(afState.dateTime, expected);\n      expect(mockState.data.dateTime, null);\n\n      await tester.pumpAndSettle();\n      mockState = getMockState(tester);\n      expect(mockState.data.dateTime, expected);\n\n      final firstOfNextMonth = dayInDatePicker(1);\n\n      // for certain months, the first of next month isn't shown\n      if (firstOfNextMonth.allCandidates.length == 2) {\n        await tester.tap(firstOfNextMonth);\n        await tester.pumpAndSettle();\n\n        expected = DateTime(now.year, now.month + 1);\n        afState = getAfState(tester);\n        expect(afState.dateTime, expected);\n        expect(afState.focusedDateTime, expected);\n      }\n    });\n\n    testWidgets('select date range', (tester) async {\n      await tester.pumpWidget(\n        WidgetTestApp(\n          child: _MockDatePicker(\n            data: _DatePickerDataStub(\n              dateTime: null,\n              endDateTime: null,\n              includeTime: false,\n              isRange: true,\n            ),\n          ),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      AppFlowyDatePickerState afState = getAfState(tester);\n      _MockDatePickerState mockState = getMockState(tester);\n      expect(afState.startDateTime, null);\n      expect(afState.endDateTime, null);\n      expect(mockState.data.dateTime, null);\n      expect(mockState.data.endDateTime, null);\n\n      // 3-10\n      final now = DateTime.now();\n      final third = dayInDatePicker(3).first;\n      await tester.tap(third);\n      await tester.pumpAndSettle();\n\n      final expectedStart = DateTime(now.year, now.month, 3);\n      afState = getAfState(tester);\n      mockState = getMockState(tester);\n      expect(afState.startDateTime, expectedStart);\n      expect(afState.endDateTime, null);\n      expect(mockState.data.dateTime, null);\n      expect(mockState.data.endDateTime, null);\n\n      final tenth = dayInDatePicker(10).first;\n      await tester.tap(tenth);\n      await tester.pump();\n\n      final expectedEnd = DateTime(now.year, now.month, 10);\n      afState = getAfState(tester);\n      mockState = getMockState(tester);\n      expect(afState.startDateTime, expectedStart);\n      expect(afState.endDateTime, expectedEnd);\n      expect(mockState.data.dateTime, null);\n      expect(mockState.data.endDateTime, null);\n\n      await tester.pumpAndSettle();\n      afState = getAfState(tester);\n      mockState = getMockState(tester);\n      expect(afState.startDateTime, expectedStart);\n      expect(afState.endDateTime, expectedEnd);\n      expect(mockState.data.dateTime, expectedStart);\n      expect(mockState.data.endDateTime, expectedEnd);\n\n      // 7-18, backwards\n      final eighteenth = dayInDatePicker(18).first;\n      await tester.tap(eighteenth);\n      await tester.pumpAndSettle();\n\n      final expectedEnd2 = DateTime(now.year, now.month, 18);\n      afState = getAfState(tester);\n      mockState = getMockState(tester);\n      expect(afState.startDateTime, expectedEnd2);\n      expect(afState.endDateTime, null);\n      expect(mockState.data.dateTime, expectedStart);\n      expect(mockState.data.endDateTime, expectedEnd);\n\n      final seventh = dayInDatePicker(7).first;\n      await tester.tap(seventh);\n      await tester.pump();\n\n      final expectedStart2 = DateTime(now.year, now.month, 7);\n      afState = getAfState(tester);\n      mockState = getMockState(tester);\n      expect(afState.startDateTime, expectedStart2);\n      expect(afState.endDateTime, expectedEnd2);\n      expect(mockState.data.dateTime, expectedStart);\n      expect(mockState.data.endDateTime, expectedEnd);\n\n      await tester.pumpAndSettle();\n      afState = getAfState(tester);\n      mockState = getMockState(tester);\n      expect(afState.startDateTime, expectedStart2);\n      expect(afState.endDateTime, expectedEnd2);\n      expect(mockState.data.dateTime, expectedStart2);\n      expect(mockState.data.endDateTime, expectedEnd2);\n    });\n\n    testWidgets('select date range after toggling is range', (tester) async {\n      final now = DateTime.now();\n      final fourteenthDateTime = DateTime(now.year, now.month, 14);\n\n      await tester.pumpWidget(\n        WidgetTestApp(\n          child: _MockDatePicker(\n            data: _DatePickerDataStub(\n              dateTime: fourteenthDateTime,\n              endDateTime: null,\n              includeTime: false,\n              isRange: false,\n            ),\n          ),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      AppFlowyDatePickerState afState = getAfState(tester);\n      _MockDatePickerState mockState = getMockState(tester);\n      expect(afState.dateTime, fourteenthDateTime);\n      expect(afState.startDateTime, null);\n      expect(afState.endDateTime, null);\n      expect(afState.justChangedIsRange, false);\n\n      await tester.tap(\n        find.descendant(\n          of: find.byType(EndTimeButton),\n          matching: find.byType(Toggle),\n        ),\n      );\n      await tester.pump();\n\n      afState = getAfState(tester);\n      mockState = getMockState(tester);\n      expect(afState.isRange, true);\n      expect(afState.dateTime, fourteenthDateTime);\n      expect(afState.startDateTime, fourteenthDateTime);\n      expect(afState.endDateTime, fourteenthDateTime);\n      expect(afState.justChangedIsRange, true);\n      expect(mockState.data.isRange, false);\n      expect(mockState.data.dateTime, fourteenthDateTime);\n      expect(mockState.data.endDateTime, null);\n\n      await tester.pumpAndSettle();\n\n      afState = getAfState(tester);\n      mockState = getMockState(tester);\n      expect(afState.isRange, true);\n      expect(afState.dateTime, fourteenthDateTime);\n      expect(afState.startDateTime, fourteenthDateTime);\n      expect(afState.endDateTime, fourteenthDateTime);\n      expect(afState.justChangedIsRange, true);\n      expect(mockState.data.isRange, true);\n      expect(mockState.data.dateTime, fourteenthDateTime);\n      expect(mockState.data.endDateTime, fourteenthDateTime);\n\n      final twentyFirst = dayInDatePicker(21).first;\n      await tester.tap(twentyFirst);\n      await tester.pumpAndSettle();\n\n      final expected = DateTime(now.year, now.month, 21);\n\n      afState = getAfState(tester);\n      mockState = getMockState(tester);\n      expect(afState.dateTime, fourteenthDateTime);\n      expect(afState.startDateTime, fourteenthDateTime);\n      expect(afState.endDateTime, expected);\n      expect(afState.justChangedIsRange, false);\n      expect(mockState.data.dateTime, fourteenthDateTime);\n      expect(mockState.data.endDateTime, expected);\n      expect(mockState.data.isRange, true);\n    });\n\n    testWidgets('include time and modify', (tester) async {\n      final now = DateTime.now();\n      final fourteenthDateTime = now.copyWith(day: 14);\n\n      await tester.pumpWidget(\n        WidgetTestApp(\n          child: _MockDatePicker(\n            data: _DatePickerDataStub(\n              dateTime: DateTime(\n                fourteenthDateTime.year,\n                fourteenthDateTime.month,\n                fourteenthDateTime.day,\n              ),\n              endDateTime: null,\n              includeTime: false,\n              isRange: false,\n            ),\n          ),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      AppFlowyDatePickerState afState = getAfState(tester);\n      _MockDatePickerState mockState = getMockState(tester);\n      expect(afState.dateTime!.isAtSameDayAs(fourteenthDateTime), true);\n      expect(afState.dateTime!.isAtSameMinuteAs(fourteenthDateTime), false);\n      expect(afState.startDateTime, null);\n      expect(afState.endDateTime, null);\n      expect(afState.includeTime, false);\n\n      await tester.tap(\n        find.descendant(\n          of: find.byType(IncludeTimeButton),\n          matching: find.byType(Toggle),\n        ),\n      );\n      await tester.pump();\n\n      afState = getAfState(tester);\n      mockState = getMockState(tester);\n      expect(afState.dateTime!.isAtSameMinuteAs(fourteenthDateTime), true);\n      expect(afState.includeTime, true);\n      expect(\n        mockState.data.dateTime!.isAtSameDayAs(fourteenthDateTime),\n        true,\n      );\n      expect(\n        mockState.data.dateTime!.isAtSameMinuteAs(fourteenthDateTime),\n        false,\n      );\n      expect(mockState.data.includeTime, false);\n\n      await tester.pumpAndSettle(300.milliseconds);\n      mockState = getMockState(tester);\n      expect(\n        mockState.data.dateTime!.isAtSameMinuteAs(fourteenthDateTime),\n        true,\n      );\n      expect(mockState.data.includeTime, true);\n\n      final timeField = find.byKey(const ValueKey('date_time_text_field_time'));\n      await tester.enterText(timeField, \"1\");\n      await tester.testTextInput.receiveAction(TextInputAction.done);\n      await tester.pumpAndSettle(300.milliseconds);\n\n      DateTime expected = DateTime(\n        fourteenthDateTime.year,\n        fourteenthDateTime.month,\n        fourteenthDateTime.day,\n        1,\n      );\n\n      afState = getAfState(tester);\n      mockState = getMockState(tester);\n      expect(afState.dateTime, expected);\n      expect(mockState.data.dateTime, expected);\n\n      final dateText = find.descendant(\n        of: find.byKey(const ValueKey('date_time_text_field_date')),\n        matching: find\n            .text(DateFormat(DateFormatPB.Friendly.pattern).format(expected)),\n      );\n      expect(dateText, findsOneWidget);\n      final timeText = find.descendant(\n        of: find.byKey(const ValueKey('date_time_text_field_time')),\n        matching: find\n            .text(DateFormat(TimeFormatPB.TwelveHour.pattern).format(expected)),\n      );\n      expect(timeText, findsOneWidget);\n\n      final third = dayInDatePicker(3).first;\n      await tester.tap(third);\n      await tester.pumpAndSettle();\n\n      expected = DateTime(\n        fourteenthDateTime.year,\n        fourteenthDateTime.month,\n        3,\n        1,\n      );\n\n      afState = getAfState(tester);\n      mockState = getMockState(tester);\n      expect(afState.dateTime, expected);\n      expect(mockState.data.dateTime, expected);\n    });\n\n    testWidgets(\n      'turn on include time, turn on end date, then select date range',\n      (tester) async {\n        final fourteenth = DateTime(2024, 10, 14);\n\n        await tester.pumpWidget(\n          WidgetTestApp(\n            child: _MockDatePicker(\n              data: _DatePickerDataStub(\n                dateTime: fourteenth,\n                endDateTime: null,\n                includeTime: false,\n                isRange: false,\n              ),\n            ),\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        await tester.tap(\n          find.descendant(\n            of: find.byType(EndTimeButton),\n            matching: find.byType(Toggle),\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        final now = DateTime.now();\n        await tester.tap(\n          find.descendant(\n            of: find.byType(IncludeTimeButton),\n            matching: find.byType(Toggle),\n          ),\n        );\n        await tester.pumpAndSettle();\n\n        final third = dayInDatePicker(21).first;\n        await tester.tap(third);\n        await tester.pumpAndSettle();\n\n        final afState = getAfState(tester);\n        final mockState = getMockState(tester);\n        final expectedTime = Duration(hours: now.hour, minutes: now.minute);\n        final expectedStart = fourteenth.add(expectedTime);\n        final expectedEnd = fourteenth.copyWith(day: 21).add(expectedTime);\n        expect(afState.justChangedIsRange, false);\n        expect(afState.includeTime, true);\n        expect(afState.isRange, true);\n        expect(afState.dateTime, expectedStart);\n        expect(afState.startDateTime, expectedStart);\n        expect(afState.endDateTime, expectedEnd);\n        expect(mockState.data.dateTime, expectedStart);\n        expect(mockState.data.endDateTime, expectedEnd);\n        expect(mockState.data.isRange, true);\n      },\n    );\n\n    testWidgets('edit text field causes start and end to get swapped',\n        (tester) async {\n      final fourteenth = DateTime(2024, 10, 14, 1);\n\n      await tester.pumpWidget(\n        WidgetTestApp(\n          child: _MockDatePicker(\n            data: _DatePickerDataStub(\n              dateTime: fourteenth,\n              endDateTime: fourteenth,\n              includeTime: true,\n              isRange: true,\n            ),\n          ),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      expect(\n        find.text(\n          DateFormat(DateFormatPB.Friendly.pattern).format(fourteenth),\n        ),\n        findsNWidgets(2),\n      );\n\n      final dateTextField = find.descendant(\n        of: find.byKey(const ValueKey('date_time_text_field')),\n        matching: find.byKey(const ValueKey('date_time_text_field_date')),\n      );\n      expect(dateTextField, findsOneWidget);\n      await tester.enterText(dateTextField, \"Nov 30, 2024\");\n      await tester.testTextInput.receiveAction(TextInputAction.done);\n      await tester.pumpAndSettle();\n      await tester.pumpAndSettle();\n\n      final day = DateTime(2024, 11, 30, 1);\n\n      expect(\n        find.descendant(\n          of: find.byKey(const ValueKey('date_time_text_field')),\n          matching: find.text(\n            DateFormat(DateFormatPB.Friendly.pattern).format(fourteenth),\n          ),\n        ),\n        findsOneWidget,\n      );\n\n      expect(\n        find.descendant(\n          of: find.byKey(const ValueKey('end_date_time_text_field')),\n          matching: find.text(\n            DateFormat(DateFormatPB.Friendly.pattern).format(day),\n          ),\n        ),\n        findsOneWidget,\n      );\n\n      final mockState = getMockState(tester);\n      expect(mockState.data.dateTime, fourteenth);\n      expect(mockState.data.endDateTime, day);\n    });\n\n    testWidgets(\n        'select start date with calendar and then enter end date with keyboard',\n        (tester) async {\n      final fourteenth = DateTime(2024, 10, 14, 1);\n\n      await tester.pumpWidget(\n        WidgetTestApp(\n          child: _MockDatePicker(\n            data: _DatePickerDataStub(\n              dateTime: fourteenth,\n              endDateTime: fourteenth,\n              includeTime: true,\n              isRange: true,\n            ),\n          ),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      final third = dayInDatePicker(3).first;\n      await tester.tap(third);\n      await tester.pumpAndSettle();\n\n      final start = DateTime(2024, 10, 3, 1);\n\n      AppFlowyDatePickerState afState = getAfState(tester);\n      _MockDatePickerState mockState = getMockState(tester);\n      expect(afState.dateTime, start);\n      expect(afState.startDateTime, start);\n      expect(afState.endDateTime, null);\n      expect(mockState.data.dateTime, fourteenth);\n      expect(mockState.data.endDateTime, fourteenth);\n      expect(mockState.data.isRange, true);\n\n      final dateTextField = find.descendant(\n        of: find.byKey(const ValueKey('end_date_time_text_field')),\n        matching: find.byKey(const ValueKey('date_time_text_field_date')),\n      );\n      expect(dateTextField, findsOneWidget);\n      await tester.enterText(dateTextField, \"Oct 18, 2024\");\n      await tester.testTextInput.receiveAction(TextInputAction.done);\n      await tester.pumpAndSettle();\n      await tester.pumpAndSettle();\n\n      final end = DateTime(2024, 10, 18, 1);\n\n      expect(\n        find.descendant(\n          of: find.byKey(const ValueKey('date_time_text_field')),\n          matching: find.text(\n            DateFormat(DateFormatPB.Friendly.pattern).format(start),\n          ),\n        ),\n        findsOneWidget,\n      );\n\n      expect(\n        find.descendant(\n          of: find.byKey(const ValueKey('end_date_time_text_field')),\n          matching: find.text(\n            DateFormat(DateFormatPB.Friendly.pattern).format(end),\n          ),\n        ),\n        findsOneWidget,\n      );\n\n      afState = getAfState(tester);\n      mockState = getMockState(tester);\n      expect(afState.dateTime, start);\n      expect(afState.startDateTime, start);\n      expect(afState.endDateTime, end);\n      expect(mockState.data.dateTime, start);\n      expect(mockState.data.endDateTime, end);\n\n      // make sure click counter was reset\n      final twentyFifth = dayInDatePicker(25).first;\n      final expected = DateTime(2024, 10, 25, 1);\n      await tester.tap(twentyFifth);\n      await tester.pumpAndSettle();\n      afState = getAfState(tester);\n      mockState = getMockState(tester);\n      expect(afState.dateTime, expected);\n      expect(afState.startDateTime, expected);\n      expect(afState.endDateTime, null);\n      expect(mockState.data.dateTime, start);\n      expect(mockState.data.endDateTime, end);\n    });\n\n    testWidgets('same as above but enter time', (tester) async {\n      final fourteenth = DateTime(2024, 10, 14, 1);\n\n      await tester.pumpWidget(\n        WidgetTestApp(\n          child: _MockDatePicker(\n            data: _DatePickerDataStub(\n              dateTime: fourteenth,\n              endDateTime: fourteenth,\n              includeTime: true,\n              isRange: true,\n            ),\n          ),\n        ),\n      );\n      await tester.pumpAndSettle();\n\n      final third = dayInDatePicker(3).first;\n      await tester.tap(third);\n      await tester.pumpAndSettle();\n\n      final start = DateTime(2024, 10, 3, 1);\n\n      final dateTextField = find.descendant(\n        of: find.byKey(const ValueKey('end_date_time_text_field')),\n        matching: find.byKey(const ValueKey('date_time_text_field_time')),\n      );\n      expect(dateTextField, findsOneWidget);\n      await tester.enterText(dateTextField, \"15:00\");\n      await tester.testTextInput.receiveAction(TextInputAction.done);\n      await tester.pumpAndSettle();\n      await tester.pumpAndSettle();\n\n      expect(\n        find.descendant(\n          of: find.byKey(const ValueKey('date_time_text_field')),\n          matching: find.text(\n            DateFormat(DateFormatPB.Friendly.pattern).format(start),\n          ),\n        ),\n        findsOneWidget,\n      );\n\n      expect(\n        find.descendant(\n          of: find.byKey(const ValueKey('end_date_time_text_field')),\n          matching: find.text(\"15:00\"),\n        ),\n        findsNothing,\n      );\n\n      AppFlowyDatePickerState afState = getAfState(tester);\n      _MockDatePickerState mockState = getMockState(tester);\n      expect(afState.dateTime, start);\n      expect(afState.startDateTime, start);\n      expect(afState.endDateTime, null);\n      expect(mockState.data.dateTime, fourteenth);\n      expect(mockState.data.endDateTime, fourteenth);\n\n      // select for real now\n      final twentyFifth = dayInDatePicker(25).first;\n      final expected = DateTime(2024, 10, 25, 1);\n      await tester.tap(twentyFifth);\n      await tester.pumpAndSettle();\n      await tester.pumpAndSettle();\n      afState = getAfState(tester);\n      mockState = getMockState(tester);\n      expect(afState.dateTime, start);\n      expect(afState.startDateTime, start);\n      expect(afState.endDateTime, expected);\n      expect(mockState.data.dateTime, start);\n      expect(mockState.data.endDateTime, expected);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/direction_setting_test.dart",
    "content": "import 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/user/application/user_settings_service.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';\nimport 'package:appflowy/workspace/presentation/settings/shared/settings_radio_select.dart';\nimport 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';\nimport 'package:bloc_test/bloc_test.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nimport '../util.dart';\n\nclass MockAppearanceSettingsBloc\n    extends MockBloc<AppearanceSettingsCubit, AppearanceSettingsState>\n    implements AppearanceSettingsCubit {}\n\nclass MockDocumentAppearanceCubit extends Mock\n    implements DocumentAppearanceCubit {}\n\nclass MockDocumentAppearance extends Mock implements DocumentAppearance {}\n\nvoid main() {\n  late AppearanceSettingsPB appearanceSettings;\n  late DateTimeSettingsPB dateTimeSettings;\n\n  setUp(() async {\n    await AppFlowyUnitTest.ensureInitialized();\n    appearanceSettings =\n        await UserSettingsBackendService().getAppearanceSetting();\n    dateTimeSettings = await UserSettingsBackendService().getDateTimeSettings();\n    registerFallbackValue(AppFlowyTextDirection.ltr);\n  });\n\n  testWidgets('TextDirectionSelect update default text direction setting',\n      (WidgetTester tester) async {\n    final appearanceSettingsState = AppearanceSettingsState.initial(\n      AppTheme.fallback,\n      appearanceSettings.themeMode,\n      appearanceSettings.font,\n      appearanceSettings.layoutDirection,\n      appearanceSettings.textDirection,\n      appearanceSettings.enableRtlToolbarItems,\n      appearanceSettings.locale,\n      appearanceSettings.isMenuCollapsed,\n      appearanceSettings.menuOffset,\n      dateTimeSettings.dateFormat,\n      dateTimeSettings.timeFormat,\n      dateTimeSettings.timezoneId,\n      appearanceSettings.documentSetting.cursorColor.isEmpty\n          ? null\n          : Color(\n              int.parse(appearanceSettings.documentSetting.cursorColor),\n            ),\n      appearanceSettings.documentSetting.selectionColor.isEmpty\n          ? null\n          : Color(\n              int.parse(\n                appearanceSettings.documentSetting.selectionColor,\n              ),\n            ),\n      1.0,\n    );\n    final mockAppearanceSettingsBloc = MockAppearanceSettingsBloc();\n    when(() => mockAppearanceSettingsBloc.state).thenReturn(\n      appearanceSettingsState,\n    );\n\n    final mockDocumentAppearanceCubit = MockDocumentAppearanceCubit();\n    when(() => mockDocumentAppearanceCubit.stream).thenAnswer(\n      (_) => Stream.fromIterable([MockDocumentAppearance()]),\n    );\n\n    await tester.pumpWidget(\n      MultiBlocProvider(\n        providers: [\n          BlocProvider<AppearanceSettingsCubit>.value(\n            value: mockAppearanceSettingsBloc,\n          ),\n          BlocProvider<DocumentAppearanceCubit>.value(\n            value: mockDocumentAppearanceCubit,\n          ),\n        ],\n        child: MaterialApp(\n          theme: appearanceSettingsState.lightTheme,\n          home: MultiBlocProvider(\n            providers: [\n              BlocProvider<AppearanceSettingsCubit>.value(\n                value: mockAppearanceSettingsBloc,\n              ),\n              BlocProvider<DocumentAppearanceCubit>.value(\n                value: mockDocumentAppearanceCubit,\n              ),\n            ],\n            child: const Scaffold(\n              body: TextDirectionSelect(),\n            ),\n          ),\n        ),\n      ),\n    );\n    await tester.pumpAndSettle();\n\n    expect(\n      find.text(\n        LocaleKeys.settings_workspacePage_textDirection_leftToRight.tr(),\n      ),\n      findsOne,\n    );\n    expect(\n      find.text(\n        LocaleKeys.settings_workspacePage_textDirection_rightToLeft.tr(),\n      ),\n      findsOne,\n    );\n    expect(\n      find.text(\n        LocaleKeys.settings_workspacePage_textDirection_auto.tr(),\n      ),\n      findsOne,\n    );\n\n    final radioSelectFinder =\n        find.byType(SettingsRadioSelect<AppFlowyTextDirection>);\n    expect(radioSelectFinder, findsOne);\n\n    when(\n      () => mockAppearanceSettingsBloc.setTextDirection(\n        any<AppFlowyTextDirection>(),\n      ),\n    ).thenAnswer((_) async => {});\n    when(\n      () => mockDocumentAppearanceCubit.syncDefaultTextDirection(\n        any<String?>(),\n      ),\n    ).thenAnswer((_) async {});\n\n    final radioSelect = tester.widget(radioSelectFinder)\n        as SettingsRadioSelect<AppFlowyTextDirection>;\n    final rtlSelect = radioSelect.items\n        .firstWhere((select) => select.value == AppFlowyTextDirection.rtl);\n    radioSelect.onChanged(rtlSelect);\n\n    verify(\n      () => mockAppearanceSettingsBloc.setTextDirection(\n        any<AppFlowyTextDirection>(),\n      ),\n    ).called(1);\n    verify(\n      () => mockDocumentAppearanceCubit.syncDefaultTextDirection(\n        any<String?>(),\n      ),\n    ).called(1);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/lib/features/share_section/refresh_button_test.dart",
    "content": "import 'package:appflowy/features/shared_section/presentation/widgets/refresh_button.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../../widget_test_wrapper.dart';\n\nvoid main() {\n  group('refresh_button.dart: ', () {\n    testWidgets('shows refresh icon and triggers callback',\n        (WidgetTester tester) async {\n      bool pressed = false;\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: RefreshSharedSectionButton(\n            onTap: () => pressed = true,\n          ),\n        ),\n      );\n      expect(find.byIcon(Icons.refresh), findsOneWidget);\n      await tester.tap(find.byIcon(Icons.refresh));\n      await tester.pumpAndSettle();\n      expect(pressed, isTrue);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/lib/features/share_section/shared_page_actions_button_test.dart",
    "content": "import 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/features/share_tab/data/models/share_access_level.dart';\nimport 'package:appflowy/features/shared_section/presentation/widgets/shared_page_actions_button.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:get_it/get_it.dart';\nimport 'package:mocktail/mocktail.dart';\n\nimport '../../../widget_test_wrapper.dart';\n\nvoid main() {\n  setUp(() {\n    final mockStorage = MockKeyValueStorage();\n    // Stub methods to return appropriate Future values\n    when(() => mockStorage.get(any())).thenAnswer((_) => Future.value());\n    when(() => mockStorage.set(any(), any())).thenAnswer((_) => Future.value());\n    when(() => mockStorage.remove(any())).thenAnswer((_) => Future.value());\n    when(() => mockStorage.clear()).thenAnswer((_) => Future.value());\n\n    GetIt.I.registerSingleton<KeyValueStorage>(mockStorage);\n    GetIt.I.registerSingleton<MenuSharedState>(MenuSharedState());\n  });\n\n  tearDown(() {\n    GetIt.I.reset();\n  });\n\n  group('SharedPageActionsButton: ', () {\n    late ViewPB testView;\n    late List<ViewMoreActionType> capturedActions;\n    late List<bool> capturedEditingStates;\n\n    setUp(() {\n      testView = ViewPB()\n        ..id = 'test_view_id'\n        ..name = 'Test View'\n        ..layout = ViewLayoutPB.Document\n        ..isFavorite = false;\n      capturedActions = [];\n      capturedEditingStates = [];\n    });\n\n    Widget buildTestWidget({\n      required ShareAccessLevel accessLevel,\n      ViewPB? view,\n    }) {\n      return WidgetTestWrapper(\n        child: Scaffold(\n          body: SharedPageActionsButton(\n            view: view ?? testView,\n            accessLevel: accessLevel,\n            onAction: (type, view, data) {\n              capturedActions.add(type);\n            },\n            onSetEditing: (context, value) {\n              capturedEditingStates.add(value);\n            },\n            buildChild: (controller) => ElevatedButton(\n              onPressed: () => controller.show(),\n              child: const Text('Actions'),\n            ),\n          ),\n        ),\n      );\n    }\n\n    testWidgets('renders action button correctly', (WidgetTester tester) async {\n      await tester.pumpWidget(\n        buildTestWidget(accessLevel: ShareAccessLevel.readOnly),\n      );\n\n      expect(find.text('Actions'), findsOneWidget);\n      expect(find.byType(SharedPageActionsButton), findsOneWidget);\n    });\n\n    testWidgets('shows popover when button is tapped',\n        (WidgetTester tester) async {\n      await tester.pumpWidget(\n        buildTestWidget(accessLevel: ShareAccessLevel.readOnly),\n      );\n\n      // Tap the button to show popover\n      await tester.tap(find.text('Actions'));\n      await tester.pumpAndSettle();\n\n      // Should show the AFMenu popover\n      expect(find.byType(AFMenu), findsOneWidget);\n    });\n\n    testWidgets('shows correct menu items for read-only access',\n        (WidgetTester tester) async {\n      await tester.pumpWidget(\n        buildTestWidget(accessLevel: ShareAccessLevel.readOnly),\n      );\n\n      await tester.tap(find.text('Actions'));\n      await tester.pumpAndSettle();\n\n      // For read-only access, should only show favorite, leave shared page and open in new tab\n      expect(find.byType(AFTextMenuItem), findsNWidgets(3));\n\n      // Should find favorite action (since view is not favorited)\n      expect(find.text(ViewMoreActionType.favorite.name), findsOneWidget);\n      expect(\n        find.text(ViewMoreActionType.leaveSharedPage.name),\n        findsOneWidget,\n      );\n\n      // Should find open in new tab action\n      expect(find.text(ViewMoreActionType.openInNewTab.name), findsOneWidget);\n\n      // Should NOT find editable actions\n      expect(find.text(ViewMoreActionType.rename.name), findsNothing);\n      expect(find.text(ViewMoreActionType.delete.name), findsNothing);\n    });\n\n    testWidgets('shows correct menu items for edit access',\n        (WidgetTester tester) async {\n      await tester.pumpWidget(\n        buildTestWidget(accessLevel: ShareAccessLevel.readAndWrite),\n      );\n\n      await tester.tap(find.text('Actions'));\n      await tester.pumpAndSettle();\n\n      // Should show favorite, rename, change icon, and open in new tab\n      expect(find.text(ViewMoreActionType.favorite.name), findsOneWidget);\n      expect(find.text(ViewMoreActionType.rename.name), findsOneWidget);\n      expect(find.text(ViewMoreActionType.changeIcon.name), findsOneWidget);\n      expect(find.text(ViewMoreActionType.openInNewTab.name), findsOneWidget);\n\n      // Should NOT show delete for edit access\n      expect(find.text(ViewMoreActionType.delete.name), findsNothing);\n    });\n\n    testWidgets('shows correct menu items for full access',\n        (WidgetTester tester) async {\n      await tester.pumpWidget(\n        buildTestWidget(accessLevel: ShareAccessLevel.fullAccess),\n      );\n\n      await tester.tap(find.text('Actions'));\n      await tester.pumpAndSettle();\n\n      // Should show all actions including delete\n      expect(find.text(ViewMoreActionType.favorite.name), findsOneWidget);\n      expect(find.text(ViewMoreActionType.rename.name), findsOneWidget);\n      expect(find.text(ViewMoreActionType.changeIcon.name), findsOneWidget);\n      expect(find.text(ViewMoreActionType.delete.name), findsOneWidget);\n      expect(find.text(ViewMoreActionType.openInNewTab.name), findsOneWidget);\n    });\n\n    testWidgets('shows unfavorite when view is favorited',\n        (WidgetTester tester) async {\n      final favoritedView = ViewPB()\n        ..id = 'test_view_id'\n        ..name = 'Test View'\n        ..layout = ViewLayoutPB.Document\n        ..isFavorite = true;\n\n      await tester.pumpWidget(\n        buildTestWidget(\n          accessLevel: ShareAccessLevel.readOnly,\n          view: favoritedView,\n        ),\n      );\n\n      await tester.tap(find.text('Actions'));\n      await tester.pumpAndSettle();\n\n      expect(find.text(ViewMoreActionType.unFavorite.name), findsOneWidget);\n      expect(find.text(ViewMoreActionType.favorite.name), findsNothing);\n    });\n\n    testWidgets('does not show change icon for chat layout',\n        (WidgetTester tester) async {\n      final chatView = ViewPB()\n        ..id = 'test_view_id'\n        ..name = 'Test Chat'\n        ..layout = ViewLayoutPB.Chat\n        ..isFavorite = false;\n\n      await tester.pumpWidget(\n        buildTestWidget(\n          accessLevel: ShareAccessLevel.readAndWrite,\n          view: chatView,\n        ),\n      );\n\n      await tester.tap(find.text('Actions'));\n      await tester.pumpAndSettle();\n\n      // Should show rename but not change icon for chat\n      expect(find.text(ViewMoreActionType.rename.name), findsOneWidget);\n      expect(find.text(ViewMoreActionType.changeIcon.name), findsNothing);\n    });\n\n    testWidgets('triggers onAction callback when menu item is tapped',\n        (WidgetTester tester) async {\n      await tester.pumpWidget(\n        buildTestWidget(accessLevel: ShareAccessLevel.fullAccess),\n      );\n\n      await tester.tap(find.text('Actions'));\n      await tester.pumpAndSettle();\n\n      // Tap on the favorite action\n      await tester.tap(find.text(ViewMoreActionType.favorite.name));\n      await tester.pumpAndSettle();\n\n      expect(capturedActions, contains(ViewMoreActionType.favorite));\n    });\n\n    testWidgets('shows dividers between action groups',\n        (WidgetTester tester) async {\n      await tester.pumpWidget(\n        buildTestWidget(accessLevel: ShareAccessLevel.fullAccess),\n      );\n\n      await tester.tap(find.text('Actions'));\n      await tester.pumpAndSettle();\n\n      // Should have dividers separating action groups\n      expect(find.byType(AFDivider), findsAtLeastNWidgets(1));\n    });\n\n    testWidgets('delete action shows error color', (WidgetTester tester) async {\n      await tester.pumpWidget(\n        buildTestWidget(accessLevel: ShareAccessLevel.fullAccess),\n      );\n\n      await tester.tap(find.text('Actions'));\n      await tester.pumpAndSettle();\n\n      // Find the delete menu item\n      final deleteMenuItem = find.ancestor(\n        of: find.text(ViewMoreActionType.delete.name),\n        matching: find.byType(AFTextMenuItem),\n      );\n\n      expect(deleteMenuItem, findsOneWidget);\n\n      // The delete action should be present\n      expect(find.text(ViewMoreActionType.delete.name), findsOneWidget);\n    });\n\n    testWidgets('popover hides when menu item is selected',\n        (WidgetTester tester) async {\n      await tester.pumpWidget(\n        buildTestWidget(accessLevel: ShareAccessLevel.readOnly),\n      );\n\n      await tester.tap(find.text('Actions'));\n      await tester.pumpAndSettle();\n\n      // Popover should be visible\n      expect(find.byType(AFMenu), findsOneWidget);\n\n      // Tap on favorite action\n      await tester.tap(find.text(ViewMoreActionType.favorite.name));\n      await tester.pumpAndSettle();\n\n      // Popover should be hidden\n      expect(find.byType(AFMenu), findsNothing);\n    });\n  });\n}\n\nclass MockKeyValueStorage extends Mock implements KeyValueStorage {}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/lib/features/share_section/shared_pages_list_test.dart",
    "content": "import 'package:appflowy/core/config/kv.dart';\nimport 'package:appflowy/features/share_tab/data/models/share_access_level.dart';\nimport 'package:appflowy/features/shared_section/models/shared_page.dart';\nimport 'package:appflowy/features/shared_section/presentation/widgets/shared_page_list.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:get_it/get_it.dart';\nimport 'package:mocktail/mocktail.dart';\n\nimport '../../../widget_test_wrapper.dart';\n\nvoid main() {\n  setUp(() {\n    final mockStorage = MockKeyValueStorage();\n    // Stub methods to return appropriate Future values\n    when(() => mockStorage.get(any())).thenAnswer((_) => Future.value());\n    when(() => mockStorage.set(any(), any())).thenAnswer((_) => Future.value());\n    when(() => mockStorage.remove(any())).thenAnswer((_) => Future.value());\n    when(() => mockStorage.clear()).thenAnswer((_) => Future.value());\n\n    GetIt.I.registerSingleton<KeyValueStorage>(mockStorage);\n    GetIt.I.registerSingleton<MenuSharedState>(MenuSharedState());\n  });\n\n  tearDown(() {\n    GetIt.I.reset();\n  });\n\n  group('shared_pages_list.dart: ', () {\n    testWidgets('shows list of shared pages', (WidgetTester tester) async {\n      final sharedPages = [\n        SharedPage(\n          view: ViewPB()\n            ..id = '1'\n            ..name = 'Page 1',\n          accessLevel: ShareAccessLevel.readOnly,\n        ),\n        SharedPage(\n          view: ViewPB()\n            ..id = '2'\n            ..name = 'Page 2',\n          accessLevel: ShareAccessLevel.readOnly,\n        ),\n      ];\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: SingleChildScrollView(\n            child: SharedPageList(\n              sharedPages: sharedPages,\n              onAction: (action, view, data) {},\n              onSelected: (context, view) {},\n              onTertiarySelected: (context, view) {},\n              onSetEditing: (context, value) {},\n            ),\n          ),\n        ),\n      );\n      expect(find.text('Page 1'), findsOneWidget);\n      expect(find.text('Page 2'), findsOneWidget);\n      expect(find.byType(SharedPageList), findsOneWidget);\n    });\n  });\n}\n\nclass MockKeyValueStorage extends Mock implements KeyValueStorage {}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/lib/features/share_section/shared_section_error_test.dart",
    "content": "import 'package:appflowy/features/shared_section/presentation/widgets/shared_section_error.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../../widget_test_wrapper.dart';\n\nvoid main() {\n  group('shared_section_error.dart: ', () {\n    testWidgets('shows error message', (WidgetTester tester) async {\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: SharedSectionError(errorMessage: 'An error occurred'),\n        ),\n      );\n      expect(find.text('An error occurred'), findsOneWidget);\n      expect(find.byType(SharedSectionError), findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/lib/features/share_section/shared_section_header_test.dart",
    "content": "import 'package:appflowy/features/shared_section/presentation/widgets/shared_section_header.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../../widget_test_wrapper.dart';\n\nvoid main() {\n  group('shared_section_header.dart: ', () {\n    testWidgets('shows header title', (WidgetTester tester) async {\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: SharedSectionHeader(\n            onTap: () {},\n          ),\n        ),\n      );\n      expect(find.text(LocaleKeys.shareSection_shared.tr()), findsOneWidget);\n      expect(find.byType(SharedSectionHeader), findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/lib/features/share_section/shared_section_loading_test.dart",
    "content": "import 'package:appflowy/features/shared_section/presentation/widgets/shared_section_loading.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../../widget_test_wrapper.dart';\n\nvoid main() {\n  group('shared_section_loading.dart: ', () {\n    testWidgets('shows loading indicator', (WidgetTester tester) async {\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: SharedSectionLoading(),\n        ),\n      );\n      expect(find.byType(SharedSectionLoading), findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/access_level_list_widget_test.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/access_level_list_widget.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../../widget_test_wrapper.dart';\n\nvoid main() {\n  group('access_level_list_widget.dart: ', () {\n    testWidgets('shows all access levels and highlights selected',\n        (WidgetTester tester) async {\n      // Track callback invocations\n      ShareAccessLevel? selectedLevel;\n      bool turnedIntoMember = false;\n      bool removedAccess = false;\n\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: AccessLevelListWidget(\n            selectedAccessLevel: ShareAccessLevel.readAndWrite,\n            supportedAccessLevels: ShareAccessLevel.values,\n            additionalUserManagementOptions:\n                AdditionalUserManagementOptions.values,\n            callbacks: AccessLevelListCallbacks(\n              onSelectAccessLevel: (level) => selectedLevel = level,\n              onTurnIntoMember: () => turnedIntoMember = true,\n              onRemoveAccess: () => removedAccess = true,\n            ),\n          ),\n        ),\n      );\n\n      // Check all access level options are present\n      expect(find.text(ShareAccessLevel.fullAccess.title), findsOneWidget);\n      expect(find.text(ShareAccessLevel.readAndWrite.title), findsOneWidget);\n      expect(find.text(ShareAccessLevel.readAndComment.title), findsOneWidget);\n      expect(find.text(ShareAccessLevel.readOnly.title), findsOneWidget);\n\n      // Check that the selected access level is visually marked\n      final selectedTile = tester\n          .widgetList<AFTextMenuItem>(find.byType(AFTextMenuItem))\n          .where((item) => item.selected);\n      expect(selectedTile.length, 1);\n      expect(selectedTile.first.title, ShareAccessLevel.readAndWrite.title);\n\n      // Tap on another access level\n      await tester.tap(find.text(ShareAccessLevel.readOnly.title));\n      await tester.pumpAndSettle();\n      expect(selectedLevel, ShareAccessLevel.readOnly);\n\n      // Tap on Turn into Member\n      await tester.tap(find.text(LocaleKeys.shareTab_turnIntoMember.tr()));\n      await tester.pumpAndSettle();\n      expect(turnedIntoMember, isTrue);\n\n      // Tap on Remove access\n      await tester.tap(find.text(LocaleKeys.shareTab_removeAccess.tr()));\n      await tester.pumpAndSettle();\n      expect(removedAccess, isTrue);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/copy_link_widget_test.dart",
    "content": "import 'package:appflowy/features/share_tab/data/repositories/local_share_with_user_repository_impl.dart';\nimport 'package:appflowy/features/share_tab/logic/share_tab_bloc.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/copy_link_widget.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';\nimport 'package:appflowy/startup/startup.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nimport '../../../widget_test_wrapper.dart';\n\nvoid main() {\n  setUpAll(() {\n    registerFallbackValue(const ClipboardServiceData());\n  });\n\n  setUp(() {\n    if (getIt.isRegistered<ClipboardService>()) {\n      getIt.unregister<ClipboardService>();\n    }\n    getIt.registerSingleton<ClipboardService>(_MockClipboardService());\n  });\n\n  group('copy_link_widget.dart: ', () {\n    testWidgets('shows the share link and copy button, triggers callback',\n        (WidgetTester tester) async {\n      final mockClipboard = getIt<ClipboardService>() as _MockClipboardService;\n      when(() => mockClipboard.setData(any())).thenAnswer((_) async {});\n      final bloc = ShareTabBloc(\n        repository: LocalShareWithUserRepositoryImpl(),\n        pageId: 'pageId',\n        workspaceId: 'workspaceId',\n      );\n      const testLink = 'https://test.link';\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: BlocProvider<ShareTabBloc>.value(\n            value: bloc,\n            child: CopyLinkWidget(shareLink: testLink),\n          ),\n        ),\n      );\n\n      expect(find.text(LocaleKeys.shareTab_copyLink.tr()), findsOneWidget);\n      await tester.tap(find.text(LocaleKeys.shareTab_copyLink.tr()));\n      await tester.pumpAndSettle();\n      await tester.pump(const Duration(seconds: 4));\n\n      verify(() => mockClipboard.setData(any())).called(1);\n    });\n  });\n}\n\nclass _MockClipboardService extends Mock implements ClipboardService {}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/edit_access_level_widget_test.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/access_level_list_widget.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/edit_access_level_widget.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../../widget_test_wrapper.dart';\n\nvoid main() {\n  group('edit_access_level_widget.dart: ', () {\n    testWidgets('shows selected access level and opens popover',\n        (WidgetTester tester) async {\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: EditAccessLevelWidget(\n            selectedAccessLevel: ShareAccessLevel.readOnly,\n            supportedAccessLevels: ShareAccessLevel.values,\n            additionalUserManagementOptions:\n                AdditionalUserManagementOptions.values,\n            callbacks: AccessLevelListCallbacks(\n              onSelectAccessLevel: (level) {},\n              onTurnIntoMember: () {},\n              onRemoveAccess: () {},\n            ),\n          ),\n        ),\n      );\n      // Check selected access level is shown\n      expect(find.text(ShareAccessLevel.readOnly.title), findsOneWidget);\n      // Tap to open popover\n      await tester.tap(find.text(ShareAccessLevel.readOnly.title));\n      await tester.pumpAndSettle();\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/general_access_section_test.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/shared_group.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/general_access_section.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../../widget_test_wrapper.dart';\n\nvoid main() {\n  group('general_access_section.dart: ', () {\n    testWidgets('shows section title and SharedGroupWidget',\n        (WidgetTester tester) async {\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: GeneralAccessSection(\n            group: SharedGroup(\n              id: '1',\n              name: 'Group 1',\n              icon: '👥',\n            ),\n          ),\n        ),\n      );\n      expect(find.text(LocaleKeys.shareTab_generalAccess.tr()), findsOneWidget);\n      expect(find.byType(GeneralAccessSection), findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/people_with_access_section_test.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/people_with_access_section.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../../widget_test_wrapper.dart';\n\nvoid main() {\n  group('people_with_access_section.dart: ', () {\n    testWidgets('shows section title and user widgets, triggers callbacks',\n        (WidgetTester tester) async {\n      final user = SharedUser(\n        name: 'Test User',\n        email: 'test@user.com',\n        accessLevel: ShareAccessLevel.readOnly,\n        role: ShareRole.member,\n      );\n\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: PeopleWithAccessSection(\n            isInPublicPage: true,\n            currentUserEmail: user.email,\n            users: [user],\n            callbacks: PeopleWithAccessSectionCallbacks(\n              onRemoveAccess: (_) {},\n              onTurnIntoMember: (_) {},\n              onSelectAccessLevel: (_, level) {},\n            ),\n          ),\n        ),\n      );\n      expect(find.text('People with access'), findsOneWidget);\n      expect(find.text('Test User'), findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/share_with_user_widget_test.dart",
    "content": "import 'package:appflowy/features/share_tab/presentation/widgets/share_with_user_widget.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../../widget_test_wrapper.dart';\n\nvoid main() {\n  group('share_with_user_widget.dart: ', () {\n    testWidgets('shows input and button, triggers callback on valid email',\n        (WidgetTester tester) async {\n      List<String>? invited;\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: ShareWithUserWidget(\n            onInvite: (emails) => invited = emails,\n          ),\n        ),\n      );\n      expect(find.byType(TextField), findsOneWidget);\n      expect(find.text(LocaleKeys.shareTab_invite.tr()), findsOneWidget);\n      await tester.enterText(find.byType(TextField), 'test@user.com');\n      await tester.pumpAndSettle();\n      await tester.tap(find.text(LocaleKeys.shareTab_invite.tr()));\n      await tester.pumpAndSettle();\n      expect(invited, isNotNull);\n      expect(invited, contains('test@user.com'));\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/shared_group_widget_test.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/shared_group.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/shared_group_widget.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../../widget_test_wrapper.dart';\n\nvoid main() {\n  group('shared_group_widget.dart: ', () {\n    testWidgets('shows group name, description, and trailing widget',\n        (WidgetTester tester) async {\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: SharedGroupWidget(\n            group: SharedGroup(\n              id: '1',\n              name: 'Group 1',\n              icon: '👥',\n            ),\n          ),\n        ),\n      );\n      expect(\n        find.text(LocaleKeys.shareTab_anyoneAtWorkspace.tr()),\n        findsOneWidget,\n      );\n      expect(\n        find.text(LocaleKeys.shareTab_anyoneInGroupWithLinkCanEdit.tr()),\n        findsOneWidget,\n      );\n      // Trailing widget: EditAccessLevelWidget (disabled)\n      expect(find.byType(SharedGroupWidget), findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/shared_user_widget_test.dart",
    "content": "import 'package:appflowy/features/share_tab/data/models/models.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/access_level_list_widget.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/edit_access_level_widget.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/shared_user_widget.dart';\nimport 'package:appflowy/features/share_tab/presentation/widgets/turn_into_member_widget.dart';\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../../../widget_test_wrapper.dart';\n\nvoid main() {\n  group('shared_user_widget.dart: ', () {\n    testWidgets('shows user name, email, and role',\n        (WidgetTester tester) async {\n      final user = SharedUser(\n        name: 'Test User',\n        email: 'test@user.com',\n        accessLevel: ShareAccessLevel.readOnly,\n        role: ShareRole.member,\n      );\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: SharedUserWidget(\n            isInPublicPage: true,\n            user: user,\n            currentUser: user,\n          ),\n        ),\n      );\n      expect(find.text('Test User'), findsOneWidget);\n      expect(find.text('test@user.com'), findsOneWidget);\n      expect(find.text(LocaleKeys.shareTab_you.tr()), findsOneWidget);\n    });\n\n    testWidgets('shows Guest label for guest user',\n        (WidgetTester tester) async {\n      final user = SharedUser(\n        name: 'Guest User',\n        email: 'guest@user.com',\n        accessLevel: ShareAccessLevel.readOnly,\n        role: ShareRole.guest,\n      );\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: IntrinsicWidth(\n            child: SharedUserWidget(\n              isInPublicPage: true,\n              user: user,\n              currentUser: user,\n            ),\n          ),\n        ),\n      );\n      expect(find.text(LocaleKeys.shareTab_guest.tr()), findsOneWidget);\n    });\n\n    testWidgets('readonly user can only see remove self action in menu',\n        (WidgetTester tester) async {\n      final user = SharedUser(\n        name: 'Readonly User',\n        email: 'readonly@user.com',\n        accessLevel: ShareAccessLevel.readOnly,\n        role: ShareRole.member,\n      );\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: SharedUserWidget(\n            isInPublicPage: false,\n            user: user,\n            currentUser: user.copyWith(accessLevel: ShareAccessLevel.readOnly),\n          ),\n        ),\n      );\n      // Tap the EditAccessLevelWidget to open the menu\n      await tester.tap(find.byType(EditAccessLevelWidget));\n      await tester.pumpAndSettle();\n      // Only remove access should be visible as an actionable item\n      expect(find.text(LocaleKeys.shareTab_removeAccess.tr()), findsOneWidget);\n    });\n\n    testWidgets('edit user can only see remove self action in menu',\n        (WidgetTester tester) async {\n      final user = SharedUser(\n        name: 'Edit User',\n        email: 'edit@user.com',\n        accessLevel: ShareAccessLevel.readAndWrite,\n        role: ShareRole.member,\n      );\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: SharedUserWidget(\n            isInPublicPage: false,\n            user: user,\n            currentUser: user.copyWith(\n              accessLevel: ShareAccessLevel.readAndWrite,\n            ),\n          ),\n        ),\n      );\n      // Tap the EditAccessLevelWidget to open the menu\n      await tester.tap(find.byType(EditAccessLevelWidget));\n      await tester.pumpAndSettle();\n      // Only remove access should be visible as an actionable item\n      expect(find.text(LocaleKeys.shareTab_removeAccess.tr()), findsOneWidget);\n    });\n\n    testWidgets('full access user can change another people permission',\n        (WidgetTester tester) async {\n      final user = SharedUser(\n        name: 'Other User',\n        email: 'other@user.com',\n        accessLevel: ShareAccessLevel.readOnly,\n        role: ShareRole.member,\n      );\n      final currentUser = SharedUser(\n        name: 'Full Access User',\n        email: 'full@user.com',\n        accessLevel: ShareAccessLevel.fullAccess,\n        role: ShareRole.member,\n      );\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: SharedUserWidget(\n            isInPublicPage: false,\n            user: user,\n            currentUser: currentUser,\n          ),\n        ),\n      );\n      // Tap the EditAccessLevelWidget to open the menu\n      await tester.tap(find.byType(EditAccessLevelWidget));\n      await tester.pumpAndSettle();\n      // Permission change options should be visible\n      expect(find.text(ShareAccessLevel.readOnly.title), findsWidgets);\n      expect(find.text(ShareAccessLevel.readAndWrite.title), findsWidgets);\n      expect(find.text(LocaleKeys.shareTab_removeAccess.tr()), findsOneWidget);\n    });\n\n    testWidgets('full access user can turn a guest into member',\n        (WidgetTester tester) async {\n      bool turnedIntoMember = false;\n      final guestUser = SharedUser(\n        name: 'Guest User',\n        email: 'guest@user.com',\n        accessLevel: ShareAccessLevel.readOnly,\n        role: ShareRole.guest,\n      );\n      final currentUser = SharedUser(\n        name: 'Full Access User',\n        email: 'full@user.com',\n        accessLevel: ShareAccessLevel.fullAccess,\n        role: ShareRole.member,\n      );\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: SharedUserWidget(\n            isInPublicPage: true,\n            user: guestUser,\n            currentUser: currentUser,\n            callbacks: AccessLevelListCallbacks(\n              onSelectAccessLevel: (_) {},\n              onTurnIntoMember: () {\n                turnedIntoMember = true;\n              },\n              onRemoveAccess: () {},\n            ),\n          ),\n        ),\n      );\n      // The TurnIntoMemberWidget should be present\n      expect(find.byType(TurnIntoMemberWidget), findsOneWidget);\n      // Tap the button (AFGhostButton inside TurnIntoMemberWidget)\n      await tester.tap(find.byType(TurnIntoMemberWidget));\n      await tester.pumpAndSettle();\n      expect(turnedIntoMember, isTrue);\n    });\n\n    // Additional tests for more coverage\n    testWidgets('public page: member/owner always gets disabled button',\n        (WidgetTester tester) async {\n      final user = SharedUser(\n        name: 'Member User',\n        email: 'member@user.com',\n        accessLevel: ShareAccessLevel.readAndWrite,\n        role: ShareRole.member,\n      );\n      final currentUser =\n          user.copyWith(accessLevel: ShareAccessLevel.fullAccess);\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: SharedUserWidget(\n            isInPublicPage: true,\n            user: user,\n            currentUser: currentUser,\n          ),\n        ),\n      );\n      expect(find.byType(AFGhostTextButton), findsOneWidget);\n      expect(find.byType(EditAccessLevelWidget), findsNothing);\n    });\n\n    testWidgets('private page: full access user can manage others',\n        (WidgetTester tester) async {\n      final user = SharedUser(\n        name: 'Other User',\n        email: 'other@user.com',\n        accessLevel: ShareAccessLevel.readOnly,\n        role: ShareRole.member,\n      );\n      final currentUser = SharedUser(\n        name: 'Full Access User',\n        email: 'full@user.com',\n        accessLevel: ShareAccessLevel.fullAccess,\n        role: ShareRole.member,\n      );\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: SharedUserWidget(\n            isInPublicPage: false,\n            user: user,\n            currentUser: currentUser,\n          ),\n        ),\n      );\n      expect(find.byType(EditAccessLevelWidget), findsOneWidget);\n    });\n\n    testWidgets('private page: readonly user sees disabled button for others',\n        (WidgetTester tester) async {\n      final user = SharedUser(\n        name: 'Other User',\n        email: 'other@user.com',\n        accessLevel: ShareAccessLevel.readOnly,\n        role: ShareRole.member,\n      );\n      final currentUser = SharedUser(\n        name: 'Readonly User',\n        email: 'readonly@user.com',\n        accessLevel: ShareAccessLevel.readOnly,\n        role: ShareRole.member,\n      );\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: SharedUserWidget(\n            isInPublicPage: false,\n            user: user,\n            currentUser: currentUser,\n          ),\n        ),\n      );\n      expect(find.byType(AFGhostTextButton), findsOneWidget);\n      expect(find.byType(EditAccessLevelWidget), findsNothing);\n    });\n\n    testWidgets('self: full access user cannot change own access',\n        (WidgetTester tester) async {\n      final user = SharedUser(\n        name: 'Full Access User',\n        email: 'full@user.com',\n        accessLevel: ShareAccessLevel.fullAccess,\n        role: ShareRole.member,\n      );\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: SharedUserWidget(\n            isInPublicPage: false,\n            user: user,\n            currentUser: user,\n          ),\n        ),\n      );\n      expect(find.byType(AFGhostTextButton), findsOneWidget);\n      expect(find.byType(EditAccessLevelWidget), findsNothing);\n    });\n\n    testWidgets('self: readonly user can only remove self',\n        (WidgetTester tester) async {\n      final user = SharedUser(\n        name: 'Readonly User',\n        email: 'readonly@user.com',\n        accessLevel: ShareAccessLevel.readOnly,\n        role: ShareRole.member,\n      );\n      await tester.pumpWidget(\n        WidgetTestWrapper(\n          child: SharedUserWidget(\n            isInPublicPage: false,\n            user: user,\n            currentUser: user,\n          ),\n        ),\n      );\n      expect(find.byType(EditAccessLevelWidget), findsOneWidget);\n      // Open the menu and check only remove access is present\n      await tester.tap(find.byType(EditAccessLevelWidget));\n      await tester.pumpAndSettle();\n      expect(find.text(LocaleKeys.shareTab_removeAccess.tr()), findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/select_option_text_field_test.dart",
    "content": "import 'dart:collection';\n\nimport 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.dart';\nimport 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport '../bloc_test/grid_test/util.dart';\n\nvoid main() {\n  setUpAll(() {\n    AppFlowyGridTest.ensureInitialized();\n  });\n\n  group('text_field.dart', () {\n    String submit = '';\n    String remainder = '';\n    List<String> select = [];\n\n    final textController = TextEditingController();\n\n    final textField = SelectOptionTextField(\n      options: const [],\n      selectedOptionMap: LinkedHashMap<String, SelectOptionPB>(),\n      distanceToText: 0.0,\n      onSubmitted: () => submit = textController.text,\n      onPaste: (options, remaining) {\n        remainder = remaining;\n        select = options;\n      },\n      onRemove: (_) {},\n      newText: (text) => remainder = text,\n      textSeparators: const [','],\n      textController: textController,\n      focusNode: FocusNode(),\n    );\n\n    testWidgets('SelectOptionTextField callback outputs',\n        (WidgetTester tester) async {\n      await tester.pumpWidget(\n        MaterialApp(\n          home: Material(\n            child: textField,\n          ),\n        ),\n      );\n\n      // test that the input field exists\n      expect(find.byType(TextField), findsOneWidget);\n\n      // simulate normal input\n      await tester.enterText(find.byType(TextField), 'abcd');\n      expect(remainder, 'abcd');\n\n      await tester.enterText(find.byType(TextField), ' ');\n      expect(remainder, '');\n\n      // test submit functionality (aka pressing enter)\n      await tester.enterText(find.byType(TextField), 'an option');\n      await tester.testTextInput.receiveAction(TextInputAction.done);\n      expect(submit, 'an option');\n\n      // test inputs containing commas\n      await tester.enterText(find.byType(TextField), 'a a, bbbb , c');\n      expect(remainder, 'c');\n      expect(select, ['a a', 'bbbb']);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/spae_cion_test.dart",
    "content": "import 'dart:convert';\n\nimport 'package:appflowy/workspace/application/view/view_ext.dart';\nimport 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';\nimport 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nvoid main() {\n  group('space_icon.dart', () {\n    testWidgets('space icon is empty', (WidgetTester tester) async {\n      final emptySpaceIcon = {\n        ViewExtKeys.spaceIconKey: '',\n        ViewExtKeys.spaceIconColorKey: '',\n      };\n      final space = ViewPB(\n        name: 'test',\n        extra: jsonEncode(emptySpaceIcon),\n      );\n      await tester.pumpWidget(\n        MaterialApp(\n          home: Material(\n            child: SpaceIcon(dimension: 22, space: space),\n          ),\n        ),\n      );\n\n      // test that the input field exists\n      expect(find.byType(SpaceIcon), findsOneWidget);\n\n      // use the first character of page name as icon\n      expect(find.text('T'), findsOneWidget);\n    });\n\n    testWidgets('space icon is null', (WidgetTester tester) async {\n      final emptySpaceIcon = {\n        ViewExtKeys.spaceIconKey: null,\n        ViewExtKeys.spaceIconColorKey: null,\n      };\n      final space = ViewPB(\n        name: 'test',\n        extra: jsonEncode(emptySpaceIcon),\n      );\n      await tester.pumpWidget(\n        MaterialApp(\n          home: Material(\n            child: SpaceIcon(dimension: 22, space: space),\n          ),\n        ),\n      );\n\n      expect(find.byType(SpaceIcon), findsOneWidget);\n\n      // use the first character of page name as icon\n      expect(find.text('T'), findsOneWidget);\n    });\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/test_asset_bundle.dart",
    "content": "import 'dart:convert';\nimport 'dart:ui';\n\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\n\n/// TestAssetBundle is required in order to avoid issues with large assets\n///\n/// ref: https://medium.com/@sardox/flutter-test-and-randomly-missing-assets-in-goldens-ea959cdd336a\n///\n/// \"If your AssetManifest.json file exceeds 10kb, it will be\n///  loaded with isolate that (most likely) will cause your\n///  test to finish before assets are loaded so goldens will\n///  get empty assets.\"\n///\nclass TestAssetBundle extends CachingAssetBundle {\n  @override\n  Future<String> loadString(String key, {bool cache = true}) async {\n    // overriding this method to avoid limit of 10KB per asset\n    try {\n      final data = await load(key);\n      return utf8.decode(data.buffer.asUint8List());\n    } catch (err) {\n      throw FlutterError('Unable to load asset: $key');\n    }\n  }\n\n  @override\n  Future<ByteData> load(String key) async => rootBundle.load(key);\n}\n\nfinal testAssetBundle = TestAssetBundle();\n\n/// Loads from our custom asset bundle\nclass TestBundleAssetLoader extends AssetLoader {\n  const TestBundleAssetLoader();\n\n  String getLocalePath(String basePath, Locale locale) {\n    return '$basePath/${locale.toStringWithSeparator(separator: \"-\")}.json';\n  }\n\n  @override\n  Future<Map<String, dynamic>> load(String path, Locale locale) async {\n    final localePath = getLocalePath(path, locale);\n    return json.decode(await testAssetBundle.loadString(localePath));\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/test_material_app.dart",
    "content": "import 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra/theme_extension.dart';\nimport 'package:flutter/material.dart';\n\nimport 'test_asset_bundle.dart';\n\nclass WidgetTestApp extends StatelessWidget {\n  const WidgetTestApp({\n    super.key,\n    required this.child,\n  });\n\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return EasyLocalization(\n      supportedLocales: const [Locale('en', 'US')],\n      path: 'assets/translations',\n      fallbackLocale: const Locale('en', 'US'),\n      useFallbackTranslations: true,\n      saveLocale: false,\n      assetLoader: const TestBundleAssetLoader(),\n      child: Builder(\n        builder: (context) => MaterialApp(\n          locale: const Locale('en', 'US'),\n          localizationsDelegates: context.localizationDelegates,\n          theme: ThemeData.light().copyWith(\n            extensions: const [\n              AFThemeExtension(\n                warning: Colors.transparent,\n                success: Colors.transparent,\n                tint1: Colors.transparent,\n                tint2: Colors.transparent,\n                tint3: Colors.transparent,\n                tint4: Colors.transparent,\n                tint5: Colors.transparent,\n                tint6: Colors.transparent,\n                tint7: Colors.transparent,\n                tint8: Colors.transparent,\n                tint9: Colors.transparent,\n                textColor: Colors.transparent,\n                secondaryTextColor: Colors.transparent,\n                strongText: Colors.transparent,\n                greyHover: Colors.transparent,\n                greySelect: Colors.transparent,\n                lightGreyHover: Colors.transparent,\n                toggleOffFill: Colors.transparent,\n                progressBarBGColor: Colors.transparent,\n                toggleButtonBGColor: Colors.transparent,\n                calendarWeekendBGColor: Colors.transparent,\n                gridRowCountColor: Colors.transparent,\n                code: TextStyle(),\n                callout: TextStyle(),\n                calloutBGColor: Colors.transparent,\n                tableCellBGColor: Colors.transparent,\n                caption: TextStyle(),\n                onBackground: Colors.transparent,\n                background: Colors.transparent,\n                borderColor: Colors.transparent,\n                scrollbarColor: Colors.transparent,\n                scrollbarHoverColor: Colors.transparent,\n                lightIconColor: Colors.transparent,\n                toolbarHoverColor: Colors.transparent,\n              ),\n            ],\n          ),\n          home: Scaffold(\n            body: child,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/theme_font_family_setting_test.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport 'package:appflowy/generated/locale_keys.g.dart';\nimport 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';\nimport 'package:appflowy/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';\nimport 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'package:flowy_infra_ui/flowy_infra_ui.dart';\nimport 'package:flutter_bloc/flutter_bloc.dart';\nimport 'package:flutter_test/flutter_test.dart';\nimport 'package:mocktail/mocktail.dart';\n\nclass MockAppearanceSettingsCubit extends Mock\n    implements AppearanceSettingsCubit {}\n\nclass MockDocumentAppearanceCubit extends Mock\n    implements DocumentAppearanceCubit {}\n\nclass MockAppearanceSettingsState extends Mock\n    implements AppearanceSettingsState {}\n\nclass MockDocumentAppearance extends Mock implements DocumentAppearance {}\n\nvoid main() {\n  late MockAppearanceSettingsCubit appearanceSettingsCubit;\n  late MockDocumentAppearanceCubit documentAppearanceCubit;\n\n  setUp(() {\n    appearanceSettingsCubit = MockAppearanceSettingsCubit();\n    when(() => appearanceSettingsCubit.stream).thenAnswer(\n      (_) => Stream.fromIterable([MockAppearanceSettingsState()]),\n    );\n    documentAppearanceCubit = MockDocumentAppearanceCubit();\n    when(() => documentAppearanceCubit.stream).thenAnswer(\n      (_) => Stream.fromIterable([MockDocumentAppearance()]),\n    );\n  });\n\n  testWidgets('ThemeFontFamilySetting updates font family on selection',\n      (WidgetTester tester) async {\n    await tester.pumpWidget(\n      MultiBlocProvider(\n        providers: [\n          BlocProvider<AppearanceSettingsCubit>.value(\n            value: appearanceSettingsCubit,\n          ),\n          BlocProvider<DocumentAppearanceCubit>.value(\n            value: documentAppearanceCubit,\n          ),\n        ],\n        child: MaterialApp(\n          home: MultiBlocProvider(\n            providers: [\n              BlocProvider<AppearanceSettingsCubit>.value(\n                value: appearanceSettingsCubit,\n              ),\n              BlocProvider<DocumentAppearanceCubit>.value(\n                value: documentAppearanceCubit,\n              ),\n            ],\n            child: const Scaffold(\n              body: ThemeFontFamilySetting(\n                currentFontFamily: defaultFontFamily,\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n\n    final popover = find.byType(AppFlowyPopover);\n    await tester.tap(popover);\n    await tester.pumpAndSettle();\n\n    // Verify the initial font family\n    expect(\n      find.text(LocaleKeys.settings_appearance_fontFamily_defaultFont.tr()),\n      findsAtLeastNWidgets(1),\n    );\n    when(() => appearanceSettingsCubit.setFontFamily(any<String>()))\n        .thenAnswer((_) async {});\n    verifyNever(() => appearanceSettingsCubit.setFontFamily(any<String>()));\n    when(() => documentAppearanceCubit.syncFontFamily(any<String>()))\n        .thenAnswer((_) async {});\n    verifyNever(() => documentAppearanceCubit.syncFontFamily(any<String>()));\n\n    // Tap on a different font family\n    final abel = find.textContaining('Abel');\n    await tester.tap(abel);\n    await tester.pumpAndSettle();\n\n    // Verify that the font family is updated\n    verify(() => appearanceSettingsCubit.setFontFamily(any<String>()))\n        .called(1);\n    verify(() => documentAppearanceCubit.syncFontFamily(any<String>()))\n        .called(1);\n  });\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/test/widget_test/widget_test_wrapper.dart",
    "content": "import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';\nimport 'package:appflowy_ui/appflowy_ui.dart';\nimport 'package:flutter/material.dart';\n\nclass WidgetTestWrapper extends StatelessWidget {\n  const WidgetTestWrapper({super.key, required this.child});\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    final brightness = Theme.of(context).brightness;\n    final themeBuilder = AppFlowyDefaultTheme();\n    return ToastificationWrapper(\n      child: MaterialApp(\n        home: Material(\n          child: AppFlowyTheme(\n            data: brightness == Brightness.light\n                ? themeBuilder.light()\n                : themeBuilder.dark(),\n            child: child,\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/web/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <!--\n    If you are serving your web app in a path other than the root, change the\n    href value below to reflect the base path you are serving from.\n\n    The path provided below has to start and end with a slash \"/\" in order for\n    it to work correctly.\n\n    For more details:\n    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base\n\n    This is a placeholder for base href that will be replaced by the value of\n    the `--base-href` argument provided to `flutter build`.\n  -->\n  <base href=\"$FLUTTER_BASE_HREF\">\n\n  <meta charset=\"UTF-8\">\n  <meta content=\"IE=Edge\" http-equiv=\"X-UA-Compatible\">\n  <meta name=\"description\" content=\"A new Flutter project.\">\n\n  <!-- iOS meta tags & icons -->\n  <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n  <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\">\n  <meta name=\"apple-mobile-web-app-title\" content=\"appflowy_flutter\">\n  <link rel=\"apple-touch-icon\" href=\"icons/Icon-192.png\">\n\n  <title>appflowy_flutter</title>\n  <link rel=\"manifest\" href=\"manifest.json\">\n</head>\n<body>\n  <!-- This script installs service_worker.js to provide PWA functionality to\n       application. For more information, see:\n       https://developers.google.com/web/fundamentals/primers/service-workers -->\n  <script>\n    var serviceWorkerVersion = null;\n    var scriptLoaded = false;\n    function loadMainDartJs() {\n      if (scriptLoaded) {\n        return;\n      }\n      scriptLoaded = true;\n      var scriptTag = document.createElement('script');\n      scriptTag.src = 'main.dart.js';\n      scriptTag.type = 'application/javascript';\n      document.body.append(scriptTag);\n    }\n\n    if ('serviceWorker' in navigator) {\n      // Service workers are supported. Use them.\n      window.addEventListener('load', function () {\n        // Wait for registration to finish before dropping the <script> tag.\n        // Otherwise, the browser will load the script multiple times,\n        // potentially different versions.\n        var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;\n        navigator.serviceWorker.register(serviceWorkerUrl)\n          .then((reg) => {\n            function waitForActivation(serviceWorker) {\n              serviceWorker.addEventListener('statechange', () => {\n                if (serviceWorker.state == 'activated') {\n                  console.log('Installed new service worker.');\n                  loadMainDartJs();\n                }\n              });\n            }\n            if (!reg.active && (reg.installing || reg.waiting)) {\n              // No active web worker and we have installed or are installing\n              // one for the first time. Simply wait for it to activate.\n              waitForActivation(reg.installing ?? reg.waiting);\n            } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {\n              // When the app updates the serviceWorkerVersion changes, so we\n              // need to ask the service worker to update.\n              console.log('New service worker available.');\n              reg.update();\n              waitForActivation(reg.installing);\n            } else {\n              // Existing service worker is still good.\n              console.log('Loading app from service worker.');\n              loadMainDartJs();\n            }\n          });\n\n        // If service worker doesn't succeed in a reasonable amount of time,\n        // fallback to plain <script> tag.\n        setTimeout(() => {\n          if (!scriptLoaded) {\n            console.warn(\n              'Failed to load app from service worker. Falling back to plain <script> tag.',\n            );\n            loadMainDartJs();\n          }\n        }, 4000);\n      });\n    } else {\n      // Service workers not supported. Just drop the <script> tag.\n      loadMainDartJs();\n    }\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "frontend/appflowy_flutter/web/manifest.json",
    "content": "{\n    \"name\": \"appflowy_flutter\",\n    \"short_name\": \"appflowy_flutter\",\n    \"start_url\": \".\",\n    \"display\": \"standalone\",\n    \"background_color\": \"#0175C2\",\n    \"theme_color\": \"#0175C2\",\n    \"description\": \"A new Flutter project.\",\n    \"orientation\": \"portrait-primary\",\n    \"prefer_related_applications\": false,\n    \"icons\": [\n        {\n            \"src\": \"icons/Icon-192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"icons/Icon-512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\"\n        }\n    ]\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/windows/.gitignore",
    "content": "flutter/ephemeral/\n\n# Visual Studio user-specific files.\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# Visual Studio build-related files.\nx64/\nx86/\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n\nflutter/generated_plugin_registrant.cc\nflutter/generated_plugin_registrant.h\nflutter/generated_plugins.cmake"
  },
  {
    "path": "frontend/appflowy_flutter/windows/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.14)\nproject(appflowy_flutter LANGUAGES CXX)\n\nset(BINARY_NAME \"AppFlowy\")\n\ncmake_policy(SET CMP0063 NEW)\n\nset(CMAKE_INSTALL_RPATH \"$ORIGIN/lib\")\n\n# Configure build options.\nget_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)\n\nif(IS_MULTICONFIG)\n  set(CMAKE_CONFIGURATION_TYPES \"Debug;Profile;Release\"\n    CACHE STRING \"\" FORCE)\nelse()\n  if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n    set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n      STRING \"Flutter build mode\" FORCE)\n    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n      \"Debug\" \"Profile\" \"Release\")\n  endif()\nendif()\n\nset(CMAKE_EXE_LINKER_FLAGS_PROFILE \"${CMAKE_EXE_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_SHARED_LINKER_FLAGS_PROFILE \"${CMAKE_SHARED_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_C_FLAGS_PROFILE \"${CMAKE_C_FLAGS_RELEASE}\")\nset(CMAKE_CXX_FLAGS_PROFILE \"${CMAKE_CXX_FLAGS_RELEASE}\")\n\n# Use Unicode for all projects.\nadd_definitions(-DUNICODE -D_UNICODE)\n\n# Compilation settings that should be applied to most targets.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_17)\n  target_compile_options(${TARGET} PRIVATE /W4 /WX /wd\"4100\")\n  target_compile_options(${TARGET} PRIVATE /EHsc)\n  target_compile_definitions(${TARGET} PRIVATE \"_HAS_EXCEPTIONS=0\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<CONFIG:Debug>:_DEBUG>\")\nendfunction()\n\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\n\n# Flutter library and tool build rules.\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# Application build\nadd_subdirectory(\"runner\")\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n# === Installation ===\n# Support files are copied into place next to the executable, so that it can\n# run in place. This is done instead of making a separate bundle (as on Linux)\n# so that building and running from within Visual Studio will work.\nset(BUILD_BUNDLE_DIR \"$<TARGET_FILE_DIR:${BINARY_NAME}>\")\n\n# Make the \"install\" step default, as it's required to run.\nset(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)\n\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\nset(DART_FFI_DIR \"${CMAKE_INSTALL_PREFIX}\")\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${DART_FFI_DLL}\" DESTINATION \"${DART_FFI_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nif(PLUGIN_BUNDLED_LIBRARIES)\n  install(FILES \"${PLUGIN_BUNDLED_LIBRARIES}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n\n# Fully re-copy the assets directory on each build to avoid having stale files\n# from a previous install.\nset(FLUTTER_ASSET_DIR_NAME \"flutter_assets\")\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\\\")\n  \" COMPONENT Runtime)\ninstall(DIRECTORY \"${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}\"\n  DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\" COMPONENT Runtime)\n\n# Install the AOT library on non-Debug builds only.\ninstall(FILES \"${AOT_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  CONFIGURATIONS Profile;Release\n  COMPONENT Runtime)\n"
  },
  {
    "path": "frontend/appflowy_flutter/windows/flutter/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.14)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\nset(WRAPPER_ROOT \"${EPHEMERAL_DIR}/cpp_client_wrapper\")\n\n# Set fallback configurations for older versions of the flutter tool.\nif (NOT DEFINED FLUTTER_TARGET_PLATFORM)\n  set(FLUTTER_TARGET_PLATFORM \"windows-x64\")\nendif()\n\n# === Flutter Library ===\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/flutter_windows.dll\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(DART_FFI_DLL \"${CMAKE_CURRENT_SOURCE_DIR}/dart_ffi/dart_ffi.dll\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/windows/app.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"flutter_export.h\"\n  \"flutter_windows.h\"\n  \"flutter_messenger.h\"\n  \"flutter_plugin_registrar.h\"\n  \"flutter_texture_registrar.h\"\n)\nlist(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND \"${EPHEMERAL_DIR}/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}.lib\")\nadd_dependencies(flutter flutter_assemble)\n\n# === Wrapper ===\nlist(APPEND CPP_WRAPPER_SOURCES_CORE\n  \"core_implementations.cc\"\n  \"standard_codec.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_PLUGIN\n  \"plugin_registrar.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_APP\n  \"flutter_engine.cc\"\n  \"flutter_view_controller.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND \"${WRAPPER_ROOT}/\")\n\n# Wrapper sources needed for a plugin.\nadd_library(flutter_wrapper_plugin STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n)\napply_standard_settings(flutter_wrapper_plugin)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  POSITION_INDEPENDENT_CODE ON)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  CXX_VISIBILITY_PRESET hidden)\ntarget_link_libraries(flutter_wrapper_plugin PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_plugin PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_plugin flutter_assemble)\n\n# Wrapper sources needed for the runner.\nadd_library(flutter_wrapper_app STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\napply_standard_settings(flutter_wrapper_app)\ntarget_link_libraries(flutter_wrapper_app PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_app PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_app flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nset(PHONY_OUTPUT \"${CMAKE_CURRENT_BINARY_DIR}/_phony_\")\nset_source_files_properties(\"${PHONY_OUTPUT}\" PROPERTIES SYMBOLIC TRUE)\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}\n    ${CPP_WRAPPER_SOURCES_APP}\n    ${PHONY_OUTPUT}\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat\"\n      ${FLUTTER_TARGET_PLATFORM} $<CONFIG>\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\n"
  },
  {
    "path": "frontend/appflowy_flutter/windows/runner/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.14)\nproject(runner LANGUAGES CXX)\n\n# Define the application target. To change its name, change BINARY_NAME in the\n# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer\n# work.\n#\n# Any new source files that you add to the application should be added here.\nadd_executable(${BINARY_NAME} WIN32\n  \"flutter_window.cpp\"\n  \"main.cpp\"\n  \"utils.cpp\"\n  \"win32_window.cpp\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n  \"Runner.rc\"\n  \"runner.exe.manifest\"\n)\n\n# Apply the standard set of build settings. This can be removed for applications\n# that need different build settings.\napply_standard_settings(${BINARY_NAME})\n\n# Add preprocessor definitions for the build version.\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION=\\\"${FLUTTER_VERSION}\\\"\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}\")\n\n# Disable Windows macros that collide with C++ standard library functions.\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"NOMINMAX\")\n\n# Add dependency libraries and include directories. Add any application-specific\n# dependencies here.\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)\ntarget_include_directories(${BINARY_NAME} PRIVATE \"${CMAKE_SOURCE_DIR}\")\n\n# Run the Flutter tool portions of the build. This must not be removed.\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n"
  },
  {
    "path": "frontend/appflowy_flutter/windows/runner/Runner.rc",
    "content": "// Microsoft Visual C++ generated resource script.\n//\n#pragma code_page(65001)\n#include \"resource.h\"\n\n#define APSTUDIO_READONLY_SYMBOLS\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 2 resource.\n//\n#include \"winres.h\"\n\n/////////////////////////////////////////////////////////////////////////////\n#undef APSTUDIO_READONLY_SYMBOLS\n\n/////////////////////////////////////////////////////////////////////////////\n// English (United States) resources\n\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\nLANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US\n\n#ifdef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// TEXTINCLUDE\n//\n\n1 TEXTINCLUDE\nBEGIN\n    \"resource.h\\0\"\nEND\n\n2 TEXTINCLUDE\nBEGIN\n    \"#include \"\"winres.h\"\"\\r\\n\"\n    \"\\0\"\nEND\n\n3 TEXTINCLUDE\nBEGIN\n    \"\\r\\n\"\n    \"\\0\"\nEND\n\n#endif    // APSTUDIO_INVOKED\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Icon\n//\n\n// Icon with lowest ID value placed first to ensure application icon\n// remains consistent on all systems.\nIDI_APP_ICON            ICON                    \"resources\\\\app_icon.ico\"\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Version\n//\n\n#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)\n#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD\n#else\n#define VERSION_AS_NUMBER 1,0,0,0\n#endif\n\n#if defined(FLUTTER_VERSION)\n#define VERSION_AS_STRING FLUTTER_VERSION\n#else\n#define VERSION_AS_STRING \"1.0.0\"\n#endif\n\nVS_VERSION_INFO VERSIONINFO\n FILEVERSION VERSION_AS_NUMBER\n PRODUCTVERSION VERSION_AS_NUMBER\n FILEFLAGSMASK VS_FFI_FILEFLAGSMASK\n#ifdef _DEBUG\n FILEFLAGS VS_FF_DEBUG\n#else\n FILEFLAGS 0x0L\n#endif\n FILEOS VOS__WINDOWS32\n FILETYPE VFT_APP\n FILESUBTYPE 0x0L\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904e4\"\n        BEGIN\n            VALUE \"CompanyName\", \"io.appflowy\" \"\\0\"\n            VALUE \"FileDescription\", \"AppFlowy\" \"\\0\"\n            VALUE \"FileVersion\", VERSION_AS_STRING \"\\0\"\n            VALUE \"InternalName\", \"AppFlowy\" \"\\0\"\n            VALUE \"LegalCopyright\", \"Copyright (C) 2024 io.appflowy. All rights reserved.\" \"\\0\"\n            VALUE \"OriginalFilename\", \"AppFlowy.exe\" \"\\0\"\n            VALUE \"ProductName\", \"AppFlowy\" \"\\0\"\n            VALUE \"ProductVersion\", VERSION_AS_STRING \"\\0\"\n        END\n    END\n    BLOCK \"VarFileInfo\"\n    BEGIN\n        VALUE \"Translation\", 0x409, 1252\n    END\nEND\n\n#endif    // English (United States) resources\n/////////////////////////////////////////////////////////////////////////////\n\n\n\n#ifndef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 3 resource.\n//\n\n\n/////////////////////////////////////////////////////////////////////////////\n#endif    // not APSTUDIO_INVOKED\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// WinSparkle\n//\n\n// And verify signature using DSA public key:\nDSAPub      DSAPEM      \"../../dsa_pub.pem\""
  },
  {
    "path": "frontend/appflowy_flutter/windows/runner/flutter_window.cpp",
    "content": "#include \"flutter_window.h\"\n\n#include <optional>\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nFlutterWindow::FlutterWindow(const flutter::DartProject& project)\n    : project_(project) {}\n\nFlutterWindow::~FlutterWindow() {}\n\nbool FlutterWindow::OnCreate() {\n  if (!Win32Window::OnCreate()) {\n    return false;\n  }\n\n  RECT frame = GetClientArea();\n\n  // The size here must match the window dimensions to avoid unnecessary surface\n  // creation / destruction in the startup path.\n  flutter_controller_ = std::make_unique<flutter::FlutterViewController>(\n      frame.right - frame.left, frame.bottom - frame.top, project_);\n  // Ensure that basic setup of the controller was successful.\n  if (!flutter_controller_->engine() || !flutter_controller_->view()) {\n    return false;\n  }\n  RegisterPlugins(flutter_controller_->engine());\n  SetChildContent(flutter_controller_->view()->GetNativeWindow());\n\n  flutter_controller_->engine()->SetNextFrameCallback([&]() {\n    // https://pub.dev/packages/window_manager#windows\n    // this->Show();\n  });\n\n  // Flutter can complete the first frame before the \"show window\" callback is\n  // registered. The following call ensures a frame is pending to ensure the\n  // window is shown. It is a no-op if the first frame hasn't completed yet.\n  flutter_controller_->ForceRedraw();\n\n  return true;\n}\n\nvoid FlutterWindow::OnDestroy() {\n  if (flutter_controller_) {\n    flutter_controller_ = nullptr;\n  }\n\n  Win32Window::OnDestroy();\n}\n\nLRESULT\nFlutterWindow::MessageHandler(HWND hwnd, UINT const message,\n                              WPARAM const wparam,\n                              LPARAM const lparam) noexcept {\n  // Give Flutter, including plugins, an opportunity to handle window messages.\n  if (flutter_controller_) {\n    std::optional<LRESULT> result =\n        flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,\n                                                      lparam);\n    if (result) {\n      return *result;\n    }\n  }\n\n  switch (message) {\n    case WM_FONTCHANGE:\n      flutter_controller_->engine()->ReloadSystemFonts();\n      break;\n  }\n\n  return Win32Window::MessageHandler(hwnd, message, wparam, lparam);\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/windows/runner/flutter_window.h",
    "content": "#ifndef RUNNER_FLUTTER_WINDOW_H_\n#define RUNNER_FLUTTER_WINDOW_H_\n\n#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n\n#include <memory>\n\n#include \"win32_window.h\"\n\n// A window that does nothing but host a Flutter view.\nclass FlutterWindow : public Win32Window {\n public:\n  // Creates a new FlutterWindow hosting a Flutter view running |project|.\n  explicit FlutterWindow(const flutter::DartProject& project);\n  virtual ~FlutterWindow();\n\n protected:\n  // Win32Window:\n  bool OnCreate() override;\n  void OnDestroy() override;\n  LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,\n                         LPARAM const lparam) noexcept override;\n\n private:\n  // The project to run.\n  flutter::DartProject project_;\n\n  // The Flutter instance hosted by this window.\n  std::unique_ptr<flutter::FlutterViewController> flutter_controller_;\n};\n\n#endif  // RUNNER_FLUTTER_WINDOW_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/windows/runner/main.cpp",
    "content": "#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n#include <windows.h>\n\n#include \"flutter_window.h\"\n#include \"utils.h\"\n\nint APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,\n                      _In_ wchar_t *command_line, _In_ int show_command)\n{\n  HANDLE hMutexInstance = CreateMutex(NULL, TRUE, L\"AppFlowyMutex\");\n  HWND handle = FindWindowA(NULL, \"AppFlowy\");\n\n  if (GetLastError() == ERROR_ALREADY_EXISTS)\n  {\n    flutter::DartProject project(L\"data\");\n    std::vector<std::string> command_line_arguments = GetCommandLineArguments();\n    project.set_dart_entrypoint_arguments(std::move(command_line_arguments));\n    FlutterWindow window(project);\n    if (window.SendAppLinkToInstance(L\"AppFlowy\"))\n    {\n      return false;\n    }\n\n    WINDOWPLACEMENT place = {sizeof(WINDOWPLACEMENT)};\n    GetWindowPlacement(handle, &place);\n    ShowWindow(handle, SW_NORMAL);\n    return 0;\n  }\n\n  // Attach to console when present (e.g., 'flutter run') or create a\n  // new console when running with a debugger.\n  if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent())\n  {\n    CreateAndAttachConsole();\n  }\n\n  // Initialize COM, so that it is available for use in the library and/or\n  // plugins.\n  ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);\n\n  flutter::DartProject project(L\"data\");\n\n  std::vector<std::string> command_line_arguments = GetCommandLineArguments();\n\n  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));\n\n  FlutterWindow window(project);\n  Win32Window::Point origin(10, 10);\n  Win32Window::Size size(1280, 720);\n\n  if (!window.Create(L\"AppFlowy\", origin, size))\n  {\n    return EXIT_FAILURE;\n  }\n\n  window.Show();\n  window.SetQuitOnClose(true);\n\n  ::MSG msg;\n  while (::GetMessage(&msg, nullptr, 0, 0))\n  {\n    ::TranslateMessage(&msg);\n    ::DispatchMessage(&msg);\n  }\n\n  ::CoUninitialize();\n  ReleaseMutex(hMutexInstance);\n  return EXIT_SUCCESS;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/windows/runner/resource.h",
    "content": "//{{NO_DEPENDENCIES}}\n// Microsoft Visual C++ generated include file.\n// Used by Runner.rc\n//\n#define IDI_APP_ICON                    101\n\n// Next default values for new objects\n//\n#ifdef APSTUDIO_INVOKED\n#ifndef APSTUDIO_READONLY_SYMBOLS\n#define _APS_NEXT_RESOURCE_VALUE        102\n#define _APS_NEXT_COMMAND_VALUE         40001\n#define _APS_NEXT_CONTROL_VALUE         1001\n#define _APS_NEXT_SYMED_VALUE           101\n#endif\n#endif\n"
  },
  {
    "path": "frontend/appflowy_flutter/windows/runner/runner.exe.manifest",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\">\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2</dpiAwareness>\n    </windowsSettings>\n  </application>\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- Windows 10 -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"/>\n      <!-- Windows 8.1 -->\n      <supportedOS Id=\"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\"/>\n      <!-- Windows 8 -->\n      <supportedOS Id=\"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\"/>\n      <!-- Windows 7 -->\n      <supportedOS Id=\"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\"/>\n    </application>\n  </compatibility>\n</assembly>\n"
  },
  {
    "path": "frontend/appflowy_flutter/windows/runner/utils.cpp",
    "content": "#include \"utils.h\"\n\n#include <flutter_windows.h>\n#include <io.h>\n#include <stdio.h>\n#include <windows.h>\n\n#include <iostream>\n\nvoid CreateAndAttachConsole() {\n  if (::AllocConsole()) {\n    FILE *unused;\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stdout)) {\n      _dup2(_fileno(stdout), 1);\n    }\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stderr)) {\n      _dup2(_fileno(stdout), 2);\n    }\n    std::ios::sync_with_stdio();\n    FlutterDesktopResyncOutputStreams();\n  }\n}\n\nstd::vector<std::string> GetCommandLineArguments() {\n  // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.\n  int argc;\n  wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);\n  if (argv == nullptr) {\n    return std::vector<std::string>();\n  }\n\n  std::vector<std::string> command_line_arguments;\n\n  // Skip the first argument as it's the binary name.\n  for (int i = 1; i < argc; i++) {\n    command_line_arguments.push_back(Utf8FromUtf16(argv[i]));\n  }\n\n  ::LocalFree(argv);\n\n  return command_line_arguments;\n}\n\nstd::string Utf8FromUtf16(const wchar_t* utf16_string) {\n  if (utf16_string == nullptr) {\n    return std::string();\n  }\n  int target_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      -1, nullptr, 0, nullptr, nullptr);\n  if (target_length == 0) {\n    return std::string();\n  }\n  std::string utf8_string;\n  utf8_string.resize(target_length);\n  int converted_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      -1, utf8_string.data(),\n      target_length, nullptr, nullptr);\n  if (converted_length == 0) {\n    return std::string();\n  }\n  return utf8_string;\n}\n"
  },
  {
    "path": "frontend/appflowy_flutter/windows/runner/utils.h",
    "content": "#ifndef RUNNER_UTILS_H_\n#define RUNNER_UTILS_H_\n\n#include <string>\n#include <vector>\n\n// Creates a console for the process, and redirects stdout and stderr to\n// it for both the runner and the Flutter library.\nvoid CreateAndAttachConsole();\n\n// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string\n// encoded in UTF-8. Returns an empty std::string on failure.\nstd::string Utf8FromUtf16(const wchar_t* utf16_string);\n\n// Gets the command line arguments passed in as a std::vector<std::string>,\n// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.\nstd::vector<std::string> GetCommandLineArguments();\n\n#endif  // RUNNER_UTILS_H_\n"
  },
  {
    "path": "frontend/appflowy_flutter/windows/runner/win32_window.cpp",
    "content": "#include \"win32_window.h\"\n\n#include <dwmapi.h>\n#include <flutter_windows.h>\n\n#include \"resource.h\"\n\n#include \"app_links/app_links_plugin_c_api.h\"\n\nnamespace {\n\n/// Window attribute that enables dark mode window decorations.\n///\n/// Redefined in case the developer's machine has a Windows SDK older than\n/// version 10.0.22000.0.\n/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute\n#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE\n#define DWMWA_USE_IMMERSIVE_DARK_MODE 20\n#endif\n\nconstexpr const wchar_t kWindowClassName[] = L\"FLUTTER_RUNNER_WIN32_WINDOW\";\n\n/// Registry key for app theme preference.\n///\n/// A value of 0 indicates apps should use dark mode. A non-zero or missing\n/// value indicates apps should use light mode.\nconstexpr const wchar_t kGetPreferredBrightnessRegKey[] =\n  L\"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Themes\\\\Personalize\";\nconstexpr const wchar_t kGetPreferredBrightnessRegValue[] = L\"AppsUseLightTheme\";\n\n// The number of Win32Window objects that currently exist.\nstatic int g_active_window_count = 0;\n\nusing EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);\n\n// Scale helper to convert logical scaler values to physical using passed in\n// scale factor\nint Scale(int source, double scale_factor) {\n  return static_cast<int>(source * scale_factor);\n}\n\n// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.\n// This API is only needed for PerMonitor V1 awareness mode.\nvoid EnableFullDpiSupportIfAvailable(HWND hwnd) {\n  HMODULE user32_module = LoadLibraryA(\"User32.dll\");\n  if (!user32_module) {\n    return;\n  }\n  auto enable_non_client_dpi_scaling =\n      reinterpret_cast<EnableNonClientDpiScaling*>(\n          GetProcAddress(user32_module, \"EnableNonClientDpiScaling\"));\n  if (enable_non_client_dpi_scaling != nullptr) {\n    enable_non_client_dpi_scaling(hwnd);\n  }\n  FreeLibrary(user32_module);\n}\n\n}  // namespace\n\n// Manages the Win32Window's window class registration.\nclass WindowClassRegistrar {\n public:\n  ~WindowClassRegistrar() = default;\n\n  // Returns the singleton registrar instance.\n  static WindowClassRegistrar* GetInstance() {\n    if (!instance_) {\n      instance_ = new WindowClassRegistrar();\n    }\n    return instance_;\n  }\n\n  // Returns the name of the window class, registering the class if it hasn't\n  // previously been registered.\n  const wchar_t* GetWindowClass();\n\n  // Unregisters the window class. Should only be called if there are no\n  // instances of the window.\n  void UnregisterWindowClass();\n\n private:\n  WindowClassRegistrar() = default;\n\n  static WindowClassRegistrar* instance_;\n\n  bool class_registered_ = false;\n};\n\nWindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;\n\nconst wchar_t* WindowClassRegistrar::GetWindowClass() {\n  if (!class_registered_) {\n    WNDCLASS window_class{};\n    window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);\n    window_class.lpszClassName = kWindowClassName;\n    window_class.style = CS_HREDRAW | CS_VREDRAW;\n    window_class.cbClsExtra = 0;\n    window_class.cbWndExtra = 0;\n    window_class.hInstance = GetModuleHandle(nullptr);\n    window_class.hIcon =\n        LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));\n    window_class.hbrBackground = 0;\n    window_class.lpszMenuName = nullptr;\n    window_class.lpfnWndProc = Win32Window::WndProc;\n    RegisterClass(&window_class);\n    class_registered_ = true;\n  }\n  return kWindowClassName;\n}\n\nvoid WindowClassRegistrar::UnregisterWindowClass() {\n  UnregisterClass(kWindowClassName, nullptr);\n  class_registered_ = false;\n}\n\nWin32Window::Win32Window() {\n  ++g_active_window_count;\n}\n\nWin32Window::~Win32Window() {\n  --g_active_window_count;\n  Destroy();\n}\n\nbool Win32Window::Create(const std::wstring& title,\n                         const Point& origin,\n                         const Size& size) {\n  Destroy();\n\n  if (SendAppLinkToInstance(title))\n  {\n    return false;\n  }\n\n  const wchar_t* window_class =\n      WindowClassRegistrar::GetInstance()->GetWindowClass();\n\n  const POINT target_point = {static_cast<LONG>(origin.x),\n                              static_cast<LONG>(origin.y)};\n  HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);\n  UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);\n  double scale_factor = dpi / 96.0;\n\n  HWND window = CreateWindow(\n      window_class, title.c_str(), WS_OVERLAPPEDWINDOW,\n      Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),\n      Scale(size.width, scale_factor), Scale(size.height, scale_factor),\n      nullptr, nullptr, GetModuleHandle(nullptr), this);\n\n  if (!window) {\n    return false;\n  }\n\n  UpdateTheme(window);\n\n  return OnCreate();\n}\n\nbool Win32Window::Show() {\n  return ShowWindow(window_handle_, SW_SHOWNORMAL);\n}\n\n// static\nLRESULT CALLBACK Win32Window::WndProc(HWND const window,\n                                      UINT const message,\n                                      WPARAM const wparam,\n                                      LPARAM const lparam) noexcept {\n  if (message == WM_NCCREATE) {\n    auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);\n    SetWindowLongPtr(window, GWLP_USERDATA,\n                     reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));\n\n    auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);\n    EnableFullDpiSupportIfAvailable(window);\n    that->window_handle_ = window;\n  } else if (Win32Window* that = GetThisFromHandle(window)) {\n    return that->MessageHandler(window, message, wparam, lparam);\n  }\n\n  return DefWindowProc(window, message, wparam, lparam);\n}\n\nLRESULT\nWin32Window::MessageHandler(HWND hwnd,\n                            UINT const message,\n                            WPARAM const wparam,\n                            LPARAM const lparam) noexcept {\n  switch (message) {\n    case WM_DESTROY:\n      window_handle_ = nullptr;\n      Destroy();\n      if (quit_on_close_) {\n        PostQuitMessage(0);\n      }\n      return 0;\n\n    case WM_DPICHANGED: {\n      auto newRectSize = reinterpret_cast<RECT*>(lparam);\n      LONG newWidth = newRectSize->right - newRectSize->left;\n      LONG newHeight = newRectSize->bottom - newRectSize->top;\n\n      SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,\n                   newHeight, SWP_NOZORDER | SWP_NOACTIVATE);\n\n      return 0;\n    }\n    case WM_SIZE: {\n      RECT rect = GetClientArea();\n      if (child_content_ != nullptr) {\n        // Size and position the child window.\n        MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,\n                   rect.bottom - rect.top, TRUE);\n      }\n      return 0;\n    }\n\n    case WM_ACTIVATE:\n      if (child_content_ != nullptr) {\n        SetFocus(child_content_);\n      }\n      return 0;\n\n    case WM_DWMCOLORIZATIONCOLORCHANGED:\n      UpdateTheme(hwnd);\n      return 0;\n  }\n\n  return DefWindowProc(window_handle_, message, wparam, lparam);\n}\n\nvoid Win32Window::Destroy() {\n  OnDestroy();\n\n  if (window_handle_) {\n    DestroyWindow(window_handle_);\n    window_handle_ = nullptr;\n  }\n  if (g_active_window_count == 0) {\n    WindowClassRegistrar::GetInstance()->UnregisterWindowClass();\n  }\n}\n\nWin32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {\n  return reinterpret_cast<Win32Window*>(\n      GetWindowLongPtr(window, GWLP_USERDATA));\n}\n\nvoid Win32Window::SetChildContent(HWND content) {\n  child_content_ = content;\n  SetParent(content, window_handle_);\n  RECT frame = GetClientArea();\n\n  MoveWindow(content, frame.left, frame.top, frame.right - frame.left,\n             frame.bottom - frame.top, true);\n\n  SetFocus(child_content_);\n}\n\nRECT Win32Window::GetClientArea() {\n  RECT frame;\n  GetClientRect(window_handle_, &frame);\n  return frame;\n}\n\nHWND Win32Window::GetHandle() {\n  return window_handle_;\n}\n\nvoid Win32Window::SetQuitOnClose(bool quit_on_close) {\n  quit_on_close_ = quit_on_close;\n}\n\nbool Win32Window::OnCreate() {\n  // No-op; provided for subclasses.\n  return true;\n}\n\nvoid Win32Window::OnDestroy() {\n  // No-op; provided for subclasses.\n}\n\nvoid Win32Window::UpdateTheme(HWND const window) {\n  DWORD light_mode;\n  DWORD light_mode_size = sizeof(light_mode);\n  LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,\n                               kGetPreferredBrightnessRegValue,\n                               RRF_RT_REG_DWORD, nullptr, &light_mode,\n                               &light_mode_size);\n\n  if (result == ERROR_SUCCESS) {\n    BOOL enable_dark_mode = light_mode == 0;\n    DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,\n                          &enable_dark_mode, sizeof(enable_dark_mode));\n  }\n}\n\nbool Win32Window::SendAppLinkToInstance(const std::wstring &title)\n{\n  // Find our exact window\n  HWND hwnd = ::FindWindow(kWindowClassName, title.c_str());\n\n  if (hwnd)\n  {\n    // Dispatch new link to current window\n    SendAppLink(hwnd);\n\n    // (Optional) Restore our window to front in same state\n    WINDOWPLACEMENT place = {sizeof(WINDOWPLACEMENT)};\n    GetWindowPlacement(hwnd, &place);\n\n    switch (place.showCmd)\n    {\n    case SW_SHOWMAXIMIZED:\n      ShowWindow(hwnd, SW_SHOWMAXIMIZED);\n      break;\n    case SW_SHOWMINIMIZED:\n      ShowWindow(hwnd, SW_RESTORE);\n      break;\n    default:\n      ShowWindow(hwnd, SW_NORMAL);\n      break;\n    }\n\n    SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE);\n    SetForegroundWindow(hwnd);\n\n    // Window has been found, don't create another one.\n    return true;\n  }\n\n  return false;\n}"
  },
  {
    "path": "frontend/appflowy_flutter/windows/runner/win32_window.h",
    "content": "#ifndef RUNNER_WIN32_WINDOW_H_\n#define RUNNER_WIN32_WINDOW_H_\n\n#include <windows.h>\n\n#include <functional>\n#include <memory>\n#include <string>\n\n// A class abstraction for a high DPI-aware Win32 Window. Intended to be\n// inherited from by classes that wish to specialize with custom\n// rendering and input handling\nclass Win32Window {\n public:\n  struct Point {\n    unsigned int x;\n    unsigned int y;\n    Point(unsigned int x, unsigned int y) : x(x), y(y) {}\n  };\n\n  struct Size {\n    unsigned int width;\n    unsigned int height;\n    Size(unsigned int width, unsigned int height)\n        : width(width), height(height) {}\n  };\n\n  Win32Window();\n  virtual ~Win32Window();\n\n  // Creates a win32 window with |title| that is positioned and sized using\n  // |origin| and |size|. New windows are created on the default monitor. Window\n  // sizes are specified to the OS in physical pixels, hence to ensure a\n  // consistent size this function will scale the inputted width and height as\n  // as appropriate for the default monitor. The window is invisible until\n  // |Show| is called. Returns true if the window was created successfully.\n  bool Create(const std::wstring& title, const Point& origin, const Size& size);\n\n  // Show the current window. Returns true if the window was successfully shown.\n  bool Show();\n\n  // Release OS resources associated with window.\n  void Destroy();\n\n  // Inserts |content| into the window tree.\n  void SetChildContent(HWND content);\n\n  // Returns the backing Window handle to enable clients to set icon and other\n  // window properties. Returns nullptr if the window has been destroyed.\n  HWND GetHandle();\n\n  // If true, closing this window will quit the application.\n  void SetQuitOnClose(bool quit_on_close);\n\n  // Return a RECT representing the bounds of the current client area.\n  RECT GetClientArea();\n\n  // Dispatches link if any.\n  // This method enables our app to be with a single instance too.\n  bool SendAppLinkToInstance(const std::wstring &title);\n\n protected:\n  // Processes and route salient window messages for mouse handling,\n  // size change and DPI. Delegates handling of these to member overloads that\n  // inheriting classes can handle.\n  virtual LRESULT MessageHandler(HWND window,\n                                 UINT const message,\n                                 WPARAM const wparam,\n                                 LPARAM const lparam) noexcept;\n\n  // Called when CreateAndShow is called, allowing subclass window-related\n  // setup. Subclasses should return false if setup fails.\n  virtual bool OnCreate();\n\n  // Called when Destroy is called.\n  virtual void OnDestroy();\n\n private:\n  friend class WindowClassRegistrar;\n\n  // OS callback called by message pump. Handles the WM_NCCREATE message which\n  // is passed when the non-client area is being created and enables automatic\n  // non-client DPI scaling so that the non-client area automatically\n  // responds to changes in DPI. All other messages are handled by\n  // MessageHandler.\n  static LRESULT CALLBACK WndProc(HWND const window,\n                                  UINT const message,\n                                  WPARAM const wparam,\n                                  LPARAM const lparam) noexcept;\n\n  // Retrieves a class instance pointer for |window|\n  static Win32Window* GetThisFromHandle(HWND const window) noexcept;\n\n  // Update the window frame's theme to match the system theme.\n  static void UpdateTheme(HWND const window);\n\n  bool quit_on_close_ = false;\n\n  // window handle for top level window.\n  HWND window_handle_ = nullptr;\n\n  // window handle for hosted content.\n  HWND child_content_ = nullptr;\n};\n\n#endif  // RUNNER_WIN32_WINDOW_H_\n"
  },
  {
    "path": "frontend/resources/translations/am-ET.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Me\",\n  \"welcomeText\": \"እንኳን በደህና መጡ @:appName መተግበሪያ ስም\",\n  \"githubStarText\": \"በ Github ላይ ኮከብ\",\n  \"subscribeNewsletterText\": \"ለዜና ጽሑፍ ይመዝገቡ\",\n  \"letsGoButtonText\": \"ፈጣን ጅምር\",\n  \"title\": \"ርዕስ\",\n  \"youCanAlso\": \"እርስዎም ይችላሉ\",\n  \"and\": \"እና\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"ከዚህ በታች ለመጨመር ጠቅ ያድርጉ\",\n    \"addAboveCmd\": \"Alt+click\",\n    \"addAboveMacCmd\": \"Option+click\",\n    \"addAboveTooltip\": \"ከላይ ለመጨመር\",\n    \"dragTooltip\": \"ለመንቀሳቀስ ጎትት\",\n    \"openMenuTooltip\": \"ምናሌን ለመክፈት ጠቅ ያድርጉ\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"ተመዝገቢ\",\n    \"title\": \"ይመዝገቡ @:appName\",\n    \"getStartedText\": \"እንጀምር\",\n    \"emptyPasswordError\": \"የይለፍ ቃል ባዶ ሊሆን አይችልም\",\n    \"repeatPasswordEmptyError\": \"ይድገሙ የይለፍ ቃል ባዶ ሊሆን አይችልም\",\n    \"unmatchedPasswordError\": \"ይድገሙ የይለፍ ቃል እንደ የይለፍ ቃል አንድ አይነት አይደለም\",\n    \"alreadyHaveAnAccount\": \"ቀድሞውኑ መለያ አለዎት?\",\n    \"emailHint\": \"ኢሜል\",\n    \"passwordHint\": \"የይለፍ ቃል\",\n    \"repeatPasswordHint\": \"የማለፊያ ቃልዎን ይድገሙ\",\n    \"signUpWith\": \"ይመዝገቡ\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"ወደ @:appName ይግቡ\",\n    \"loginButtonText\": \"ግባ\",\n    \"loginStartWithAnonymous\": \"ስም-አልባ ክፍለ ጊዜ ይጀምሩ\",\n    \"continueAnonymousUser\": \"ስም-አልባ ክፍለ ጊዜን ይቀጥሉ\",\n    \"buttonText\": \"ግባ\",\n    \"forgotPassword\": \"የይለፍ ቃሉን ረሳኽው?\",\n    \"emailHint\": \"ኢሜል\",\n    \"passwordHint\": \"የይለፍ ቃል\",\n    \"dontHaveAnAccount\": \"መለያ የለዎትም?\",\n    \"repeatPasswordEmptyError\": \"ይድገሙ የይለፍ ቃል ባዶ ሊሆን አይችልም\",\n    \"unmatchedPasswordError\": \"ይድገሙ የይለፍ ቃል እንደ የይለፍ ቃል አንድ አይነት አይደለም\",\n    \"syncPromptMessage\": \"ውሂቡን ማመሳሰል የተወሰነ ጊዜ ሊወስድ ይችላል.እባክዎን ይህንን ገጽ አይዝጉ\",\n    \"or\": \"ወይም\",\n    \"LogInWithGoogle\": \"በ Google ይግቡ\",\n    \"LogInWithGithub\": \"በ Github ይግቡ\",\n    \"LogInWithDiscord\": \"በ Discord ይግቡ\",\n    \"signInWith\": \"በመለያ ይግቡ:\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"የስራ ቦታዎን ይምረጡ\",\n    \"create\": \"የስራ ቦታ ፍጠር\",\n    \"reset\": \"የስራ ቦታን ዳግም ያስጀምሩ\",\n    \"resetWorkspacePrompt\": \"የስራ ቦታን ዳግም ማስጀመር በውስጡ ያሉትን ገጾች እና መረጃዎችን ያጠፋል። እርግጠኛ ነዎት የስራ ቦታውን እንደገና ማስጀመር ይፈልጋሉ? ወይም የስራ ቦታውን ለማደስ የድጋፍ ቡድኑን ማነጋገር ይችላሉ\",\n    \"hint\": \"የስራ ቦታ\",\n    \"notFoundError\": \"የስራ ቦታ አልተገኘም\",\n    \"failedToLoad\": \"የሆነ ስህተት ተከስቷል! የስራ ቦታውን መጫን አልተሳካም። ማንኛውንም የተከፈተ የአፕሊኬሽንን ምሳሌ ለመዝጋት ይሞክሩ ከዛ እንደገና ይሞክሩ።\",\n    \"errorActions\": {\n      \"reportIssue\": \"ችግር ሪፖርት ያድርጉ\",\n      \"reachOut\": \"Discord ላይ ያግኙን\"\n    }\n  },\n  \"shareAction\": {\n    \"buttonText\": \"አጋራ\",\n    \"workInProgress\": \"በቅርብ ቀን\",\n    \"markdown\": \"Markdown\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"አገናኝ ቅዳ\"\n  },\n  \"moreAction\": {\n    \"small\": \"ትንሽ\",\n    \"medium\": \"መካከለኛ\",\n    \"large\": \"ትልቅ\",\n    \"fontSize\": \"የቅርጸ-ቁምፊ መጠን\",\n    \"import\": \"ማስመጣት\",\n    \"moreOptions\": \"ተጨማሪ አማራጮች\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"ጽሑፍ እና Markdown\",\n    \"documentFromV010\": \"ሰነድ ከ v0.1.0\",\n    \"databaseFromV010\": \"የመረጃ ቋት ከ v0.1.0\",\n    \"csv\": \"CSV\",\n    \"database\": \"የመረጃ ቋት\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"እንደገና ይሰይሙ\",\n    \"delete\": \"ሰርዝ\",\n    \"duplicate\": \"አባዛ\",\n    \"unfavorite\": \"ከተወዳጅዎች ያስወግዱ\",\n    \"favorite\": \"ወደ ተወዳጆች ያክሉ\",\n    \"openNewTab\": \"በአዲስ ትር ውስጥ ይክፈቱ\",\n    \"moveTo\": \"ወደ ይሂዱ\",\n    \"addToFavorites\": \"ወደ ተወዳጆች ያክሉ\",\n    \"copyLink\": \"አገናኝ ቅዳ\"\n  },\n  \"blankPageTitle\": \"ባዶ ገጽ\",\n  \"newPageText\": \"አዲስ ገጽ\",\n  \"newDocumentText\": \"አዲስ ሰነድ\",\n  \"newGridText\": \"አዲስ ፍርግርግ\",\n  \"newCalendarText\": \"አዲስ የቀን መቁጠሪያ\",\n  \"newBoardText\": \"አዲስ ቦርድ\",\n  \"trash\": {\n    \"text\": \"መጣያ\",\n    \"restoreAll\": \"ሁሉንም ወደነበረበት መመለስ\",\n    \"deleteAll\": \"ሁሉንም ሰርዝ\",\n    \"pageHeader\": {\n      \"fileName\": \"የመዝገብ ስም\",\n      \"lastModified\": \"ለመጨረሻ ጊዜ የተሻሻለው\",\n      \"created\": \"ተፈጠረ\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"ሁሉም ገጾችን በቆሻሻ መጣያ ውስጥ ለመሰረዝ እርግጠኛ ነዎት?\",\n      \"caption\": \"ይህ እርምጃ ሊቀለበስ አይችልም።\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"ሁሉንም ገጾች በቆሻሻ መጣያ ውስጥ እንደገና መመልስዎን እርግጠኛ ነዎት?\",\n      \"caption\": \"ይህ እርምጃ ሊቀለበስ አይችልም።\"\n    }\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"ይህ ገጽ በቆሻሻ መጣያ ውስጥ ነው\",\n    \"restore\": \"መልሶ መመለስ\",\n    \"deletePermanent\": \"በቋሚነት ሰርዝ\"\n  },\n  \"dialogCreatePageNameHint\": \"ገጽ ስም\",\n  \"questionBubble\": {\n    \"shortcuts\": \"አቋራጮች\",\n    \"whatsNew\": \"ምን አዲስ ነገር አለ?\",\n    \"help\": \"እገዛ እና ድጋፍ\",\n    \"markdown\": \"ምልክት ተደርጎበታል\",\n    \"debug\": {\n      \"name\": \"ማረም መረጃ\",\n      \"success\": \"የተገለበጠ የአድራሻ መረጃ ወደ ቅንጥብ ሰሌዳ!\",\n      \"fail\": \"የማረም መረጃ ወደ ቅንጥብ ሰሌዳ መቅዳት አልተቻለም\"\n    },\n    \"feedback\": \"ግብረመልስ\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"ያስወግዱ፣ እንደገና ይሰይሙ እና ተጨማሪ ...\",\n    \"addPageTooltip\": \"በፍጥነት አንድ ገጽ ውስጥ ያክሉ\",\n    \"defaultNewPageName\": \"ላልተሰራ\",\n    \"renameDialog\": \"እንደገና ይሰይሙ\"\n  },\n  \"toolbar\": {\n    \"undo\": \"መቀልበስ\",\n    \"redo\": \"ድጋሚ\",\n    \"bold\": \"ደፋር\",\n    \"italic\": \"ጣዕሙ\",\n    \"underline\": \"ከመስመር ውጭ\",\n    \"strike\": \"ቀሚስ\",\n    \"numList\": \"ቁጥሩ ዝርዝር\",\n    \"bulletList\": \"ጉልበተኛ ዝርዝር\",\n    \"checkList\": \"ዝርዝርን ይመልከቱ\",\n    \"inlineCode\": \"የውስጥ ኮድ\",\n    \"quote\": \"ጥቅስ\",\n    \"header\": \"አርዕስት\",\n    \"highlight\": \"ያድጉ\",\n    \"color\": \"ቀለም\",\n    \"addLink\": \"አገናኝን ያክሉ\",\n    \"link\": \"አገናኝ\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"ወደ ቀላል ሁኔታ ቀይር\",\n    \"darkMode\": \"ወደ ጨለማ ሁኔታ ቀይር\",\n    \"openAsPage\": \"እንደ ገጽ ይክፈቱ\",\n    \"addNewRow\": \"አዲስ ረድፍ ያክሉ\",\n    \"openMenu\": \"ምናሌን ለመክፈት ጠቅ ያድርጉ\",\n    \"dragRow\": \"ረድፉን እንደገና ለማዳበር ረዥም ፕሬስ\",\n    \"viewDataBase\": \"የመረጃ ቋት ይመልከቱ\",\n    \"referencePage\": \"ይህ {name} የተጠቀሰ ነው\",\n    \"addBlockBelow\": \"ከዚህ በታች የሆነ አግድ ያክሉ\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"የጎን አሞሌ ይዝጉ\",\n    \"openSidebar\": \"የተከፈተ የጎን አሞሌ\",\n    \"personal\": \"የግል\",\n    \"favorites\": \"ተወዳጆች\",\n    \"clickToHidePersonal\": \"የግል ክፍልን ለመደበቅ ጠቅ ያድርጉ\",\n    \"clickToHideFavorites\": \"የሚወዱትን ክፍል ለመደበቅ ጠቅ ያድርጉ\",\n    \"addAPage\": \"አንድ ገጽ ያክሉ\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"ለመላክ የተላከው ማስታወሻ\",\n      \"path\": \"Documents/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"እውቂያዎች\",\n    \"whatsHappening\": \"በዚህ ሳምንት ምን እየሆነ ነው?\",\n    \"addContact\": \"እውቂያ ይጨምሩ\",\n    \"editContact\": \"እውቂያ ያርትዑ\"\n  },\n  \"button\": {\n    \"ok\": \"እሺ\",\n    \"cancel\": \"ይቅር\",\n    \"signIn\": \"ይግቡ\",\n    \"signOut\": \"ዘግተው ውጣ\",\n    \"complete\": \"ተጠናቀቀ\",\n    \"save\": \"አስቀምጥ\",\n    \"generate\": \"ማመንጨት\",\n    \"esc\": \"Esc\",\n    \"keep\": \"ጠብቅ\",\n    \"tryAgain\": \"እንደገና ሞክር\",\n    \"discard\": \"መጣል\",\n    \"replace\": \"ይተኩ\",\n    \"insertBelow\": \"ከዚህ በታች ያስገቡ\",\n    \"upload\": \"ይስቀሉ\",\n    \"edit\": \"አርትዕ\",\n    \"delete\": \"ሰርዝ\",\n    \"duplicate\": \"የተባዛ\",\n    \"done\": \"ተከናውኗል\",\n    \"putback\": \"ይቅር\"\n  },\n  \"label\": {\n    \"welcome\": \"እንኳን ደና መጡ!\",\n    \"firstName\": \"የመጀመሪያ ስም\",\n    \"middleName\": \"የአባት ስም\",\n    \"lastName\": \"የአያት ስም\",\n    \"stepX\": \"ደረጃ {x}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"ከመለያዎ ጋር መገናኘት አልተቻለም።\",\n      \"failedMsg\": \"እባክዎን በአሳሽዎ ውስጥ የመግቢያ ሂደቱን ማጠናቀቁዎን ያረጋግጡ።\"\n    },\n    \"google\": {\n      \"title\": \"ጉግል መግቢያ\",\n      \"instruction1\": \"የ Google እውቂያዎችዎን ለማስመጣት ይህንን መተግበሪያ የድር አሳሽንዎን በመጠቀም ይህንን መተግበሪያ መፍቀድ ያስፈልግዎታል።\",\n      \"instruction2\": \"ጽሑፉን አዶን ጠቅ በማድረግ ወይም መምረጥ ወይም መምረጥ ይህንን ኮድ ወደ ቅንጥብ ሰሌዳዎ ይቅዱ።\",\n      \"instruction3\": \"በድር አሳሽዎ ውስጥ ወደሚከተለው አገናኝ ይሂዱ እና ከላይ ያለውን ኮድ ያስገቡ\",\n      \"instruction4\": \"ምዝገባ ሲያጠናቅቁ ከዚህ በታች ያለውን ቁልፍ ተጫን\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"ቅንብሮች\",\n    \"menu\": {\n      \"appearance\": \"መልክ\",\n      \"language\": \"ቋንቋ\",\n      \"user\": \"ተጠቃሚ\",\n      \"files\": \"ፋይሎች\",\n      \"notifications\": \"ማሳወቂያዎች\",\n      \"open\": \"ክፍት ቅንብሮች\",\n      \"logout\": \"ውጣ\",\n      \"logoutPrompt\": \"እርግጠኛ ነዎት ረጃጁን ማውጣት ይፈልጋሉ?\",\n      \"selfEncryptionLogoutPrompt\": \"እርግጠኛ ነዎት ዘግተው መውጣት ይፈልጋሉ? እባክዎን የምስጢር ምስጢሩን እንደገለበጡ ያረጋግጡ\",\n      \"syncSetting\": \"ቅንብሮችን አመሳስል\",\n      \"enableSync\": \"ማመሳሰልን አንቃ\",\n      \"enableEncrypt\": \"ውሂብን ኢንክሪፕት ማድረግ\",\n      \"enableEncryptPrompt\": \"በዚህ ምስጢር ውሂብዎን ለማስጠበቅ ምስጠራን ያግብሩ። በደህና ያከማቹ; አንዴ ከነቃ, ሊጠፋ አይችልም። ከጠፋ, የእርስዎ ውሂብ እየተስተካከለ ይሆናል። ለመቅዳት ጠቅ ያድርጉ\",\n      \"inputEncryptPrompt\": \"እባክዎ የምስጠራ ምስጢርዎን ያስገቡ ለ\",\n      \"clickToCopySecret\": \"ምስጢር ለመቅዳት ጠቅ ያድርጉ\",\n      \"inputTextFieldHint\": \"ምስጢርዎ\",\n      \"historicalUserList\": \"የተጠቃሚ የመግቢያ ታሪክ\",\n      \"historicalUserListTooltip\": \"ይህ ዝርዝር ስም-አልባ መለያዎችዎን ያሳያል። ዝርዝሩን ለማየት መለያ ላይ ጠቅ ማድረግ ይችላሉ። ያልታወቁ መለያዎች 'የተጀመሩ' ቁልፍን ጠቅ በማድረግ የተፈጠሩ ናቸው\",\n      \"openHistoricalUser\": \"ማንነቱ ያልታሰበ መለያ ለመክፈት ጠቅ ያድርጉ\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"ማሳወቂያዎችን አንቃ\",\n        \"hint\": \"የአካባቢያዊ ማሳወቂያዎችን ከማየት ለማቆም ያጥፉ.\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"ይህንን ቅንብር እንደገና ያስጀምሩ\",\n      \"fontFamily\": {\n        \"label\": \"የቅርጸ-ቁምፊ ቤተሰብ\",\n        \"search\": \"ፍለጋ\"\n      },\n      \"themeMode\": {\n        \"label\": \"ጭብጥ ሁኔታ\",\n        \"light\": \"ቀላል ሁኔታ\",\n        \"dark\": \"ጨለማ ሁነታን\",\n        \"system\": \"ከስርዓት ጋር መላመድ\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"አቀማመጥ አቅጣጫ\",\n        \"hint\": \"ከግራ ወደ ቀኝ ወይም ወደ ግራ ወይም ወደ ቀኝ በማያ ገጽዎ ላይ ያለውን የይዘት ፍሰት ይቆጣጠሩ።\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\"\n      },\n      \"textDirection\": {\n        \"label\": \"ነባሪ የጽሑፍ አቅጣጫ\",\n        \"hint\": \"ጽሑፉ ከግራ ወይም ከቀኝ እንደ ነባሪው መጀመር እንዳለበት ይግለጹ።\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\",\n        \"auto\": \"ራስ-ሰር\",\n        \"fallback\": \"እንደ አቀማመጥ አቅጣጫ ተመሳሳይ ነው\"\n      },\n      \"themeUpload\": {\n        \"button\": \"ይስቀሉ\",\n        \"uploadTheme\": \"ጭብጥ ይስቀሉ\",\n        \"description\": \"ከዚህ በታች ያለውን አዝራር በመጠቀም የራስዎን የአፕልሎሎሪያ ገጽ ጭብጥ ይስቀሉ።\",\n        \"failure\": \"የተሰቀሉት ጭብጥ ልክ ያልሆነ ቅርጸት ነበረው።\",\n        \"loading\": \"እባክዎ ጭብጥዎን በማረጋገጥ እና ሲሰቅል እባክዎ ይጠብቁ ...\",\n        \"uploadSuccess\": \"ጭብጥዎ በተሳካ ሁኔታ ተሰቅሏል\",\n        \"deletionFailure\": \"ጭብጡን መሰረዝ አልተቻለም.እሱን እራስዎ ለመሰረዝ ይሞክሩ።\",\n        \"filePickerDialogTitle\": \"የ .flowy_plugin ፋይል ይምረጡ\",\n        \"urlUploadFailure\": \"URL ን መክፈት አልተሳካም: {}\"\n      },\n      \"theme\": \"ጭብጥ\",\n      \"builtInsLabel\": \"የተገነቡ ገጽታዎች\",\n      \"pluginsLabel\": \"ተሰኪዎች\",\n      \"dateFormat\": {\n        \"label\": \"የቀን ቅርጸት\",\n        \"local\": \"አካባቢያዊ\",\n        \"us\": \"US\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"ተስማሚ\",\n        \"dmy\": \"D/M/Y\"\n      },\n      \"timeFormat\": {\n        \"label\": \"የጊዜ ቅርጸት\",\n        \"twelveHour\": \"አሥራ ሁለት ሰዓት\",\n        \"twentyFourHour\": \"ሀያ አራት ሰዓት\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"አንድን ገጽ በሚፈጥርበት ጊዜ የ ስምምነቱን ንግግር ያሳዩ\"\n    },\n    \"files\": {\n      \"copy\": \"ቅጂ\",\n      \"defaultLocation\": \"ፋይሎችን እና የውሂብ ማከማቻ ቦታን ያንብቡ\",\n      \"exportData\": \"ውሂብዎን ወደ ውጭ ይላኩ\",\n      \"doubleTapToCopy\": \"መንገዱን ለመቅዳት ሁለቴ መታ ያድርጉ\",\n      \"restoreLocation\": \"ወደ APPFOFLAY ነባሪ ጎዳና ወደነበረበት መመለስ\",\n      \"customizeLocation\": \"ሌላ አቃፊ ይክፈቱ\",\n      \"restartApp\": \"ተግባራዊ ለሆኑ ለውጦች መተግበሪያን እንደገና ያስጀምሩ።\",\n      \"exportDatabase\": \"የመረጃ ቋት የመረጃ ቋት\",\n      \"selectFiles\": \"ወደ ውጭ ለመላክ የሚያስፈልጉትን ፋይሎች ይምረጡ\",\n      \"selectAll\": \"ሁሉንም ምረጥ\",\n      \"deselectAll\": \"ሁሉንም አይምረጡ\",\n      \"createNewFolder\": \"አዲስ አቃፊ ይፍጠሩ\",\n      \"createNewFolderDesc\": \"ውሂብዎን ማከማቸት እንደሚፈልጉ ይንገሩን\",\n      \"defineWhereYourDataIsStored\": \"ውሂብዎ የት እንደተከማቸ ይግለጹ\",\n      \"open\": \"ክፈት\",\n      \"openFolder\": \"አሁን ያለውን አቃፊ ይክፈቱ\",\n      \"openFolderDesc\": \"አሁን ባለው የ Appflowy አቃፊዎ ውስጥ ያንብቡ እና ይፃፉ\",\n      \"folderHintText\": \"አቃፊ ስም\",\n      \"location\": \"አዲስ አቃፊ መፍጠር\",\n      \"locationDesc\": \"ለ Appflowy ውሂብ አቃፊዎ ስም ይምረጡ\",\n      \"browser\": \"ያስሱ\",\n      \"create\": \"ፍጠር\",\n      \"set\": \"አዘጋጅ\",\n      \"folderPath\": \"አቃፊዎን ለማከማቸት መንገድ\",\n      \"locationCannotBeEmpty\": \"መንገድ ባዶ ሊሆን አይችልም\",\n      \"pathCopiedSnackbar\": \"የፋይል ማከማቻ መንገድ ወደ ቅንጥብ ሰሌዳው ተቀብሏል!\",\n      \"changeLocationTooltips\": \"የውሂብ ማውጫውን ይለውጡ\",\n      \"change\": \"ለውጥ\",\n      \"openLocationTooltips\": \"ሌላ የውሂብ ማውጫ ይክፈቱ\",\n      \"openCurrentDataFolder\": \"የአሁኑን የውሂብ ማውጫ ይክፈቱ\",\n      \"recoverLocationTooltips\": \"ወደ Appflowy ነባሪ የውሂብ ማውጫ ዳግም ያስጀምሩ\",\n      \"exportFileSuccess\": \"ወደ ውጭ የመላክ ፋይል በተሳካ ሁኔታ!\",\n      \"exportFileFail\": \"ወደ ውጭ የመላክ ፋይል አልተሳካም!\",\n      \"export\": \"ወደ ውጭ ይላኩ\"\n    },\n    \"user\": {\n      \"name\": \"ስም\",\n      \"email\": \"ኢሜል\",\n      \"tooltipSelectIcon\": \"አዶን ይምረጡ\",\n      \"selectAnIcon\": \"አዶን ይምረጡ\",\n      \"pleaseInputYourOpenAIKey\": \"እባክዎን AI ቁልፍዎን ያስገቡ\",\n      \"pleaseInputYourStabilityAIKey\": \"እባክዎ Stability AI ቁልፍን ያስገቡ\",\n      \"clickToLogout\": \"የአሁኑን ተጠቃሚ ለመግባት ጠቅ ያድርጉ\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"አቋራጮች\",\n      \"command\": \"ትእዛዝ\",\n      \"keyBinding\": \"የቁልፍ ሰሌዳ\",\n      \"addNewCommand\": \"አዲስ ትዕዛዝ ያክሉ\",\n      \"updateShortcutStep\": \"የሚፈለገውን ቁልፍ ጥምረት እና አስገባን ይጫኑ\",\n      \"shortcutIsAlreadyUsed\": \"ይህ አቋራጭ አስቀድሞ ጥቅም ላይ ውሏል- {conflict}\",\n      \"resetToDefault\": \"ወደ ነባሪ የቁልፍ ማያያዣዎች ዳግም ያስጀምሩ\",\n      \"couldNotLoadErrorMsg\": \"አቋራጮችን መጫን አልተቻለም, እንደገና ይሞክሩ\",\n      \"couldNotSaveErrorMsg\": \"አቋራጮችን ማስቀመጥ አልተቻለም, እንደገና ይሞክሩ\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"እርግጠኛ ነዎት ይህንን አመለካከት መሰረዝ ይፈልጋሉ?\",\n    \"createView\": \"አዲስ\",\n    \"title\": {\n      \"placeholder\": \"ላልተሰራ\"\n    },\n    \"settings\": {\n      \"filter\": \"ማጣሪያ\",\n      \"sort\": \"ደርድር\",\n      \"sortBy\": \"ቅደምተከተሉ የተስተካከለው\",\n      \"properties\": \"ንብረቶች\",\n      \"reorderPropertiesTooltip\": \"ለመደናቀፊያዎች ንድፍ ለመጎተት ይጎትቱ\",\n      \"group\": \"ቡድን\",\n      \"addFilter\": \"ማጣሪያ ያክሉ\",\n      \"deleteFilter\": \"ማጣሪያ ሰርዝ\",\n      \"filterBy\": \"ማጣሪያ በ ...\",\n      \"typeAValue\": \"እሴት ይተይቡ ...\",\n      \"layout\": \"አቀማመጥ\",\n      \"databaseLayout\": \"አቀማመጥ\"\n    },\n    \"textFilter\": {\n      \"contains\": \"ይይዛል\",\n      \"doesNotContain\": \"አይይዝም\",\n      \"endsWith\": \"ከ ጋር ያበቃል\",\n      \"startWith\": \"ይጀምራል\",\n      \"is\": \"ነው\",\n      \"isNot\": \"አይደለም\",\n      \"isEmpty\": \"ባዶ ነው\",\n      \"isNotEmpty\": \"ባዶ አይደለም\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"አይደለም\",\n        \"startWith\": \"ይጀምራል\",\n        \"endWith\": \"ከ ጋር ያበቃል\",\n        \"isEmpty\": \"ይጀምራል\",\n        \"isNotEmpty\": \"ነው\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"አይደለም\",\n      \"isUnchecked\": \"ባዶ ነው\",\n      \"choicechipPrefix\": {\n        \"is\": \"ባዶ አይደለም\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"አይደለም\",\n      \"isIncomplted\": \"ባዶ ነው\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"ነው\",\n      \"isNot\": \"አይደለም\",\n      \"contains\": \"ይይዛል\",\n      \"doesNotContain\": \"አይይዝም\",\n      \"isEmpty\": \"ባዶ ነው\",\n      \"isNotEmpty\": \"ባዶ አይደለም\"\n    },\n    \"field\": {\n      \"hide\": \"አይደለም\",\n      \"show\": \"ባዶ ነው\",\n      \"insertLeft\": \"ባዶ አይደለም\",\n      \"insertRight\": \"ምልክት ተደርጎበታል\",\n      \"duplicate\": \"ቁጥጥር ካልተደረገበት\",\n      \"delete\": \"ነው\",\n      \"textFieldName\": \"ተጠናቅቋል\",\n      \"checkboxFieldName\": \"ያልተሟላ ነው\",\n      \"dateFieldName\": \"ደብቅ\",\n      \"updatedAtFieldName\": \"አሳይ\",\n      \"createdAtFieldName\": \"ግራ ያስገቡ\",\n      \"numberFieldName\": \"ቀኝ ያስገቡ\",\n      \"singleSelectFieldName\": \"የተባዛ\",\n      \"multiSelectFieldName\": \"ሰርዝ\",\n      \"urlFieldName\": \"ጽሑፍ\",\n      \"checklistFieldName\": \"አመልካች ሳጥን\",\n      \"numberFormat\": \"ቀን\",\n      \"dateFormat\": \"ለመጨረሻ ጊዜ የተስተካከለ ጊዜ\",\n      \"includeTime\": \"የተፈጠረ ጊዜ\",\n      \"isRange\": \"ቁጥሮች\",\n      \"dateFormatFriendly\": \"ይምረጡ\",\n      \"dateFormatISO\": \"Year-Month-Day\",\n      \"dateFormatLocal\": \"Month/Day/Year\",\n      \"dateFormatUS\": \"Year/Month/Day\",\n      \"dateFormatDayMonthYear\": \"Day/Month/Year\",\n      \"timeFormat\": \"ልዩ መልሶች\",\n      \"invalidTimeFormat\": \"ዩ አር ኤል\",\n      \"timeFormatTwelveHour\": \"የማረጋገጫ ዝርዝር\",\n      \"timeFormatTwentyFourHour\": \"የቁጥር ቅርጸት\",\n      \"clearDate\": \"የቀን ቅርጸት\",\n      \"addSelectOption\": \"ጊዜን ያካትቱ\",\n      \"optionTitle\": \"ቀኑ ቀኑ\",\n      \"addOption\": \"ወር ቀን, ዓመት\",\n      \"editProperty\": \"የጊዜ ቅርጸት\",\n      \"newProperty\": \"የተሳሳተ ቅርጸት\",\n      \"deleteFieldPromptMessage\": \"12 ሰዓት\",\n      \"newColumn\": \"24 ሰዓት\"\n    },\n    \"rowPage\": {\n      \"newField\": \"ግልጽ ቀን\",\n      \"fieldDragElementTooltip\": \"አንድ አማራጭ ያክሉ\",\n      \"showHiddenFields\": {\n        \"one\": \"የተደበቁ {} ፊልዶችን ያሳዩ\",\n        \"many\": \"የተደበቁ {} ፊልዶችን ያሳዩ\",\n        \"other\": \"የተደበቁ {} ፊልዶችን ያሳዩ\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"የተደበቁ {} ፊልዶችን ይደብቁ\",\n        \"many\": \"የተደበቁ {} ፊልዶችን ይደብቁ\",\n        \"other\": \"የተደበቁ {} ፊልዶችን ይደብቁ\"\n      }\n    },\n    \"sort\": {\n      \"ascending\": \"ኧረይህ ንብረት ይሰረዛል\",\n      \"descending\": \"አዲስ አምድ\",\n      \"deleteAllSorts\": \"አዲስ መስክ ያክሉ\",\n      \"addSort\": \"ምናሌን ለመክፈት ጠቅ ያድርጉ\"\n    },\n    \"row\": {\n      \"duplicate\": \"ቁጥጥር ካልተደረገበት\",\n      \"delete\": \"ነው\",\n      \"titlePlaceholder\": \"ላልተሰራ\",\n      \"textPlaceholder\": \"ተጠናቅቋል\",\n      \"copyProperty\": \"ያልተሟላ ነው\",\n      \"count\": \"ደብቅ\",\n      \"newRow\": \"አሳይ\",\n      \"action\": \"ግራ ያስገቡ\",\n      \"add\": \"ቀኝ ያስገቡ\",\n      \"drag\": \"የተባዛ\"\n    },\n    \"selectOption\": {\n      \"create\": \"ሰርዝ\",\n      \"purpleColor\": \"ጽሑፍ\",\n      \"pinkColor\": \"አመልካች ሳጥን\",\n      \"lightPinkColor\": \"ቀን\",\n      \"orangeColor\": \"ለመጨረሻ ጊዜ የተስተካከለ ጊዜ\",\n      \"yellowColor\": \"የተፈጠረ ጊዜ\",\n      \"limeColor\": \"ቁጥሮች\",\n      \"greenColor\": \"ይምረጡ\",\n      \"aquaColor\": \"ልዩ መልሶች\",\n      \"blueColor\": \"ዩ አር ኤል\",\n      \"deleteTag\": \"የማረጋገጫ ዝርዝር\",\n      \"colorPanelTitle\": \"የቁጥር ቅርጸት\",\n      \"panelTitle\": \"የቀን ቅርጸት\",\n      \"searchOption\": \"ጊዜን ያካትቱ\",\n      \"searchOrCreateOption\": \"ቀኑ ቀኑ\",\n      \"createNew\": \"ወር ቀን, ዓመት\",\n      \"orSelectOne\": \"የጊዜ ቅርጸት\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"የተሳሳተ ቅርጸት\",\n      \"addNew\": \"12 ሰዓት\",\n      \"submitNewTask\": \"ሰርዝ\"\n    },\n    \"menuName\": \"ጽሑፍ\",\n    \"referencedGridPrefix\": \"አመልካች ሳጥን\"\n  },\n  \"document\": {\n    \"menuName\": \"ሰነድ\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"ለማገናኘት ቦርድ ይምረጡ\",\n        \"createANewBoard\": \"አዲስ ቦርድ ይፍጠሩ\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"ለማገናኘት ፍርግርግ ይምረጡ\",\n        \"createANewGrid\": \"አዲስ ፍርግርግ ይፍጠሩ\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"ለማገናኘት የቀን መቁጠሪያ ይምረጡ\",\n        \"createANewCalendar\": \"አዲስ የቀን መቁጠሪያ ይፍጠሩ\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"ዝርዝር\",\n      \"codeBlock\": \"ኮድ ብሎክ\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"ማጣቀሻ ቦርድ\",\n      \"referencedGrid\": \"ማጣቀሻ ፍርግርግ\",\n      \"referencedCalendar\": \"የቀን ቀን መቁጠሪያ\",\n      \"autoGeneratorMenuItemName\": \"AI ጸሐፊ\",\n      \"autoGeneratorTitleName\": \"AI ማንኛውንም ነገር እንዲጽፉ ይጠይቁ ...\",\n      \"autoGeneratorLearnMore\": \"ተጨማሪ እወቅ\",\n      \"autoGeneratorGenerate\": \"ማመንጨት\",\n      \"autoGeneratorHintText\": \"AI ይጠይቁ ...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"የ AI ቁልፍ ማግኘት አልተቻለም\",\n      \"autoGeneratorRewrite\": \"እንደገና ይፃፉ\",\n      \"smartEdit\": \"ረዳቶች\",\n      \"openAI\": \"ኦፔና\",\n      \"smartEditFixSpelling\": \"የፊደል አጻጻፍ\",\n      \"warning\": \"⚠️ የAI ምላሾች ትክክል ያልሆኑ ወይም አሳሳች ሊሆኑ ይችላሉ.\",\n      \"smartEditSummarize\": \"ማጠቃለል\",\n      \"smartEditImproveWriting\": \"ጽሑፍን ማሻሻል\",\n      \"smartEditMakeLonger\": \"ረዘም ላለ ጊዜ ያድርጉ\",\n      \"smartEditCouldNotFetchResult\": \"ከOpenAI ውጤት ማምለጥ አልተቻለም\",\n      \"smartEditCouldNotFetchKey\": \"ኦፕናይ ቁልፍን ማጣት አልተቻለም\",\n      \"smartEditDisabled\": \"በቅንብሮች ውስጥ AI ያገናኙ\",\n      \"discardResponse\": \"የ AI ምላሾችን መጣል ይፈልጋሉ?\",\n      \"createInlineMathEquation\": \"እኩልነት ይፍጠሩ\",\n      \"toggleList\": \"የተስተካከለ ዝርዝር\",\n      \"cover\": {\n        \"changeCover\": \"ሽፋን\",\n        \"colors\": \"ቀለሞች\",\n        \"images\": \"ምስሎች\",\n        \"clearAll\": \"ሁሉንም ያፅዱ\",\n        \"abstract\": \"ረቂቅ\",\n        \"addCover\": \"ሽፋን ጨምር\",\n        \"addLocalImage\": \"የአከባቢ ምስል ያክሉ\",\n        \"invalidImageUrl\": \"ልክ ያልሆነ ምስል ዩአርኤል\",\n        \"failedToAddImageToGallery\": \"ወደ ማዕከለ-ስዕላት ምስልን ማከል አልተሳካም\",\n        \"enterImageUrl\": \"የምስል ዩአርኤል ያስገቡ\",\n        \"add\": \"ጨምር\",\n        \"back\": \"ተመለስ\",\n        \"saveToGallery\": \"ወደ ማዕከለ-ስዕላት ይቆጥቡ\",\n        \"removeIcon\": \"አዶ ያስወግዱ\",\n        \"pasteImageUrl\": \"የመለጠጥ ምስል ዩ አር ኤል\",\n        \"or\": \"ወይም\",\n        \"pickFromFiles\": \"ከፋይሎች ይምረጡ\",\n        \"couldNotFetchImage\": \"ምስልን ማምጣት አልተቻለም\",\n        \"imageSavingFailed\": \"ምስል ቁጠባ አልተሳካም\",\n        \"addIcon\": \"አዶን ያክሉ\",\n        \"coverRemoveAlert\": \"ከተሰረዘ በኋላ ከሽፋን ይወገዳል።\",\n        \"alertDialogConfirmation\": \"እርግጠኛ ነዎት ለመቀጠል ይፈልጋሉ?\"\n      },\n      \"mathEquation\": {\n        \"addMathEquation\": \"የሂሳብ እኩልታን ያክሉ\",\n        \"editMathEquation\": \"የሂሳብ እኩልትን ያርትዑ\"\n      },\n      \"optionAction\": {\n        \"click\": \"ጠቅ ያድርጉ\",\n        \"toOpenMenu\": \" ምናሌን ለመክፈት\",\n        \"delete\": \"ሰርዝ\",\n        \"duplicate\": \"የተባዛ\",\n        \"turnInto\": \"መለወጥ\",\n        \"moveUp\": \"ተንቀሳቀሱ\",\n        \"moveDown\": \"ውረድ\",\n        \"color\": \"ቀለም\",\n        \"align\": \"ስልጣን\",\n        \"left\": \"ግራ\",\n        \"center\": \"ማዕከል\",\n        \"right\": \"ቀኝ\",\n        \"defaultColor\": \"ነባሪ\"\n      },\n      \"image\": {\n        \"copiedToPasteBoard\": \"የምስል አገናኝ ወደ ቅንጥብ ሰሌዳው ተቀድቷል\",\n        \"addAnImage\": \"ምስል ያክሉ\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"የርዕስ ማውጫ ለመፍጠር የርዕስ ማውጫዎችን ለማክበር ያክሉ.\"\n      },\n      \"table\": {\n        \"addAfter\": \"በኋላ ያክሉ\",\n        \"addBefore\": \"ከዚህ በፊት ያክሉ\",\n        \"delete\": \"ሰርዝ\",\n        \"clear\": \"የተባዛ\",\n        \"duplicate\": \"የተባዛ\",\n        \"bgColor\": \"መለወጥ\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"ተንቀሳቀሱ\",\n        \"cut\": \"ውረድ\",\n        \"paste\": \"ቀለም\"\n      }\n    },\n    \"textBlock\": {\n      \"placeholder\": \"ለትዛዞች '/' ይጻፉ\"\n    },\n    \"title\": {\n      \"placeholder\": \"ያልተሰየመ\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"ምስልን ለማከል ጠቅ ያድርጉ\",\n      \"upload\": {\n        \"label\": \"ይስቀሉ\",\n        \"placeholder\": \"ምስልን ለመስቀል ጠቅ ያድርጉ\"\n      },\n      \"url\": {\n        \"label\": \"የምስል ዩአርኤል\",\n        \"placeholder\": \"የምስል ዩአርኤል ያስገቡ\"\n      },\n      \"ai\": {\n        \"label\": \"ምስል AI ውስጥ ምስልን ማመንጨት\",\n        \"placeholder\": \"ምስልን ለማመንጨት እባክዎን ለ AI ይጠይቁ\"\n      },\n      \"stability_ai\": {\n        \"label\": \"ምስልን Stability AI ያመነጫል\",\n        \"placeholder\": \"እባክዎን ምስሉን ለማመንጨት እባክዎ Stability AI አጥነት ያስገቡ\"\n      },\n      \"support\": \"የምስል መጠን ወሰን 5 ሜባ ነው.የሚደገፉ ቅርፀቶች: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"ልክ ያልሆነ ምስል\",\n        \"invalidImageSize\": \"የምስል መጠን ከ 5 ሜባ በታች መሆን አለበት\",\n        \"invalidImageFormat\": \"የምስል ቅርጸት አይደገፍም.የሚደገፉ ቅርፀቶች: JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"ልክ ያልሆነ ምስል ዩአርኤል\"\n      },\n      \"embedLink\": {\n        \"label\": \"አገናኝ\",\n        \"placeholder\": \"የመለጠጥ ወይም የምስል አገናኝን ይለዩ\"\n      },\n      \"searchForAnImage\": \"ምስልን ይፈልጉ\",\n      \"pleaseInputYourOpenAIKey\": \"እባክዎ በቅንብሮች ገጽ ውስጥ የኦፕሬና ቁልፍዎን ያስገቡ\",\n      \"pleaseInputYourStabilityAIKey\": \"በቅንብሮች ገጽ ውስጥ የመረጋጋት Stability AI ቁልፍን ያስገቡ\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"ቋንቋ\",\n        \"placeholder\": \"ቋንቋ ይምረጡ\"\n      }\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"መለጠፍ ወይም አገናኝ ይተይቡ\",\n      \"openInNewTab\": \"በአዲስ ትር ክፈት\",\n      \"copyLink\": \"አገናኝ ቅዳ\",\n      \"removeLink\": \"አገናኝን ያስወግዱ\",\n      \"url\": {\n        \"label\": \"አገናኝ ዩአርኤል\",\n        \"placeholder\": \"አገናኝ ዩአርኤል ያስገቡ\"\n      },\n      \"title\": {\n        \"label\": \"አገናኝ ርዕስ\",\n        \"placeholder\": \"የአገናኝ ርዕስ ያስገቡ\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"አንድን ሰው ወይም አንድ ገጽ ወይም ቀን ጥቀስ ...\",\n      \"page\": {\n        \"label\": \"ወደ ገጽ አገናኝ\",\n        \"tooltip\": \"ገጽ ለመክፈት ጠቅ ያድርጉ\"\n      }\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"ወደ ነባሪ ዳግም ያስጀምሩ\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"የአሁኑ ስሪት ይህንን ብሎክ አይደግፍም።\",\n      \"blockContentHasBeenCopied\": \"ብሎክ ይዘቱ ተቀብሏል።\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"አዲስ\"\n    },\n    \"menuName\": \"ቦርድ\",\n    \"referencedBoardPrefix\": \"እይታ\"\n  },\n  \"calendar\": {\n    \"menuName\": \"የቀን መቁጠሪያ\",\n    \"defaultNewCalendarTitle\": \"ላልተሰራ\",\n    \"newEventButtonTooltip\": \"አዲስ ክስተት ያክሉ\",\n    \"navigation\": {\n      \"today\": \"ዛሬ\",\n      \"jumpToday\": \"እስከ ዛሬ ድረስ ዝለል\",\n      \"previousMonth\": \"ያለፈው ወር\",\n      \"nextMonth\": \"በሚቀጥለው ወር\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"የሳምንቱ ቁጥሮች ያሳዩ\",\n      \"showWeekends\": \"ቅዳሜና እሁድን ያሳዩ\",\n      \"firstDayOfWeek\": \"ሳምንት ይጀምሩ\",\n      \"layoutDateField\": \"የቀን ቀን መቁጠሪያ በ\",\n      \"noDateTitle\": \"ቀን የለም\",\n      \"noDateHint\": \"ያልተስተካከሉ ክስተቶች እዚህ ይታያሉ\",\n      \"clickToAdd\": \"ወደ የቀን መቁጠሪያው ለማከል ጠቅ ያድርጉ\",\n      \"name\": \"የቀን መቁጠሪያ አቀማመጥ\"\n    },\n    \"referencedCalendarPrefix\": \"View of\"\n  },\n  \"errorDialog\": {\n    \"title\": \"Appflowy ስህተት\",\n    \"howToFixFallback\": \"ለተፈጠረው ችግር ይቅርታ እንጠይቃለን!ስህተትዎን በሚገልፅ የእኛ የ Gitubub ገጽ ላይ አንድ ጉዳይ ያስገቡ.\",\n    \"github\": \"በ Github ላይ ይመልከቱ\"\n  },\n  \"search\": {\n    \"label\": \"ፍለጋ\",\n    \"placeholder\": {\n      \"actions\": \"የፍለጋ እርምጃዎች ...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Copied!\",\n      \"fail\": \"መቅዳት አልተቻለም\"\n    }\n  },\n  \"unSupportBlock\": \"የአሁኑ ስሪት ይህንን ብሎክ አይደግፍም።\",\n  \"views\": {\n    \"deleteContentTitle\": \"እርግጠኛ ነዎት {pageType} መሰረዝ ይፈልጋሉ?\",\n    \"deleteContentCaption\": \"ይህን {pageType} ከሆነ, ከቆሻሻ መጣያ መመለስ ይችላሉ።\"\n  },\n  \"colors\": {\n    \"custom\": \"ብጁ\",\n    \"default\": \"ነባሪ\",\n    \"red\": \"ቀይ\",\n    \"orange\": \"ብርቱካናማ\",\n    \"yellow\": \"ቢጫ\",\n    \"green\": \"አረንጓዴ\",\n    \"blue\": \"ሰማያዊ\",\n    \"purple\": \"ሐምራዊ\",\n    \"pink\": \"ሐምራዊ\",\n    \"brown\": \"ብናማ\",\n    \"gray\": \"ግራጫ\"\n  },\n  \"emoji\": {\n    \"search\": \"ፈልግ ኢሞጂዲ\",\n    \"noRecent\": \"ምንም የቅርብ ጊዜ ኢሞጂ የለም\",\n    \"noEmojiFound\": \"ምንም ኢሞጂ አልተገኘም\",\n    \"filter\": \"ማጣሪያ\",\n    \"random\": \"የዘፈቀደ\",\n    \"selectSkinTone\": \"የቆዳ ድምጽ ይምረጡ\",\n    \"remove\": \"ኢሞጂን ያስወግዱ\",\n    \"categories\": {\n      \"smileys\": \"ፈገግታ እና ስሜቶች\",\n      \"people\": \"ሰዎች እና አካል\",\n      \"animals\": \"እንስሳት እና ተፈጥሮ\",\n      \"food\": \"ምግብ እና መጠጥ\",\n      \"activities\": \"እንቅስቃሴዎች\",\n      \"places\": \"ጉዞ እና ቦታዎች\",\n      \"objects\": \"ነገሮች\",\n      \"symbols\": \"ምልክቶች\",\n      \"flags\": \"ባንዲራዎች\",\n      \"nature\": \"ተፈጥሮ\",\n      \"frequentlyUsed\": \"ብዙ ጊዜ ጥቅም ላይ ውሏል\"\n    }\n  },\n  \"inlineActions\": {\n    \"noResults\": \"ምንም ውጤቶች የሉም\",\n    \"pageReference\": \"ገጽ ማጣቀሻ\",\n    \"date\": \"ቀን\",\n    \"reminder\": {\n      \"groupTitle\": \"አስታዋሽ\",\n      \"shortKeyword\": \"አስታውስ\"\n    }\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"በቅንብሮች ውስጥ ያለውን ቀን እና የጊዜ ቅርጸት ይለውጡ\"\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"ትናንት\",\n    \"today\": \"ዛሬ\",\n    \"tomorrow\": \"ነገ\",\n    \"oneWeek\": \"1 ሳምንት\"\n  },\n  \"notificationHub\": {\n    \"title\": \"ማሳወቂያዎች\",\n    \"emptyTitle\": \"ሁሉም ተነሱ!\",\n    \"emptyBody\": \"ምንም የማሳወቂያዎች ወይም እርምጃዎች የለም.በተረጋጋና ይደሰቱ.\",\n    \"tabs\": {\n      \"inbox\": \"የገቢ መልእክት ሳጥን\",\n      \"upcoming\": \"መጪ\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"ሁሉንም እንደ ንባብ ምልክት ያድርጉበት\",\n      \"showAll\": \"ሁሉም\",\n      \"showUnreads\": \"ያልተነበበ\"\n    },\n    \"filters\": {\n      \"ascending\": \"መውጣት\",\n      \"descending\": \"መውረድ\",\n      \"groupByDate\": \"ቀን\",\n      \"showUnreadsOnly\": \"ያልተነበቡ ቃላቶችን ብቻ ያሳዩ\",\n      \"resetToDefault\": \"ወደ ነባሪ ዳግም ያስጀምሩ\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"አስታዋሽ\",\n    \"message\": \"አስታውስ\",\n    \"tooltipDelete\": \"በቅንብሮች ውስጥ ያለውን ቀን እና የጊዜ ቅርጸት ይለውጡ\",\n    \"tooltipMarkRead\": \"ትናንት\",\n    \"tooltipMarkUnread\": \"ዛሬ\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"ነገ\",\n    \"previousMatch\": \"1 ሳምንት\",\n    \"nextMatch\": \"ማሳወቂያዎች\",\n    \"close\": \"ሁሉም ተነሱ!\",\n    \"replace\": \"ምንም የማሳወቂያዎች ወይም እርምጃዎች የለም.በተረጋጋና ይደሰቱ.\",\n    \"replaceAll\": \"የገቢ መልእክት ሳጥን\",\n    \"noResult\": \"ምንም ውጤቶች የሉም\",\n    \"caseSensitive\": \"መጪ\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/ar-SA.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"أنا\",\n  \"welcomeText\": \"مرحبًا بك في @: appName\",\n  \"welcomeTo\": \"مرحبا بكم في\",\n  \"githubStarText\": \"نجمة على GitHub\",\n  \"subscribeNewsletterText\": \"اشترك في النشرة الإخبارية\",\n  \"letsGoButtonText\": \"بداية سريعة\",\n  \"title\": \"عنوان\",\n  \"youCanAlso\": \"بامكانك ايضا\",\n  \"and\": \"و\",\n  \"failedToOpenUrl\": \"فشل في فتح الرابط: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"انقر للإضافة أدناه\",\n    \"addAboveCmd\": \"Alt + انقر\",\n    \"addAboveMacCmd\": \"Option + انقر\",\n    \"addAboveTooltip\": \"لإضافة أعلاه\",\n    \"dragTooltip\": \"اسحب للتحريك\",\n    \"openMenuTooltip\": \"انقر لفتح القائمة\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"اشتراك\",\n    \"title\": \"قم بالتسجيل في @: appName\",\n    \"getStartedText\": \"البدء\",\n    \"emptyPasswordError\": \"لا يمكن أن تكون كلمة المرور فارغة\",\n    \"repeatPasswordEmptyError\": \"إعادة كلمة المرور لا يمكن أن تكون فارغة\",\n    \"unmatchedPasswordError\": \"تكرار كلمة المرور ليس هو نفسه كلمة المرور\",\n    \"alreadyHaveAnAccount\": \"هل لديك حساب؟\",\n    \"emailHint\": \"بريد إلكتروني\",\n    \"passwordHint\": \"كلمة المرور\",\n    \"repeatPasswordHint\": \"اعد كلمة السر\",\n    \"signUpWith\": \"انشئ حساب باستخدام:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"تسجيل الدخول إلى @: appName\",\n    \"loginButtonText\": \"تسجيل الدخول\",\n    \"loginStartWithAnonymous\": \"ابدأ بجلسة خفية\",\n    \"continueAnonymousUser\": \"استمر بجلسة خفية\",\n    \"continueWithLocalModel\": \"الاستمرار بالنموذج المحلي\",\n    \"switchToAppFlowyCloud\": \"AppFlowy السحابي\",\n    \"anonymousMode\": \"الوضع المجهول\",\n    \"buttonText\": \"تسجيل الدخول\",\n    \"signingInText\": \"جاري تسجيل الدخول...\",\n    \"forgotPassword\": \"هل نسيت كلمة السر؟\",\n    \"emailHint\": \"بريد إلكتروني\",\n    \"passwordHint\": \"كلمة المرور\",\n    \"dontHaveAnAccount\": \"ليس لديك حساب؟\",\n    \"createAccount\": \"إنشاء حساب\",\n    \"repeatPasswordEmptyError\": \"إعادة كلمة المرور لا يمكن أن تكون فارغة\",\n    \"unmatchedPasswordError\": \"تكرار كلمة المرور ليس هو نفسه كلمة المرور\",\n    \"passwordMustContain\": \"يجب أن تحتوي كلمة المرور على حرف واحد ورقم واحد ورمز واحد على الأقل.\",\n    \"syncPromptMessage\": \"قد تستغرق مزامنة البيانات بعض الوقت. من فضلك لا تغلق هذه الصفحة\",\n    \"or\": \"أو\",\n    \"signInWithGoogle\": \"استكمال باستخدام Google\",\n    \"signInWithGithub\": \"استكمال باستخدام Github\",\n    \"signInWithDiscord\": \"استكمال باستخدام Discord\",\n    \"signInWithApple\": \"استكمال باستخدام Apple\",\n    \"continueAnotherWay\": \"استكمال بطريقة أخرى\",\n    \"signUpWithGoogle\": \"سجل باستخدام Google\",\n    \"signUpWithGithub\": \"سجل باستخدام Github\",\n    \"signUpWithDiscord\": \"سجل باستخدام Discord\",\n    \"signInWith\": \"تسجيل الدخول ب:\",\n    \"signInWithEmail\": \"متابعة باستخدام البريد الإلكتروني\",\n    \"signInWithMagicLink\": \"استكمال\",\n    \"signUpWithMagicLink\": \"سجل باستخدام Magic Link\",\n    \"pleaseInputYourEmail\": \"الرجاء إدخال عنوان بريدك الإلكتروني\",\n    \"settings\": \"إعدادات\",\n    \"magicLinkSent\": \"تم إرسال Magic Link!\",\n    \"invalidEmail\": \"يرجى إدخال عنوان بريد إلكتروني صالح\",\n    \"alreadyHaveAnAccount\": \"هل لديك حساب؟\",\n    \"logIn\": \"تسجيل الدخول\",\n    \"generalError\": \"حدث خطأ ما. يرجى المحاولة مرة أخرى لاحقًا\",\n    \"limitRateError\": \"لأسباب أمنية، يمكنك طلب Magic Link  كل 60 ثانية فقط\",\n    \"magicLinkSentDescription\": \"تم إرسال Magic Link إلى بريدك الإلكتروني. انقر على الرابط لإكمال تسجيل الدخول. ستنتهي صلاحية الرابط بعد 5 دقائق.\",\n    \"tokenHasExpiredOrInvalid\": \"الرمز منتهي الصلاحية أو غير صالح. يُرجى المحاولة مرة أخرى.\",\n    \"signingIn\": \"جاري تسجيل الدخول...\",\n    \"checkYourEmail\": \"تحقق من بريدك الإلكتروني\",\n    \"temporaryVerificationLinkSent\": \"تم إرسال رابط التحقق المؤقت.\\nيرجى التحقق من صندوق الوارد الخاص بك على\",\n    \"temporaryVerificationCodeSent\": \"لقد تم إرسال رمز التحقق المؤقت.\\nيرجى التحقق من صندوق الوارد الخاص بك على\",\n    \"continueToSignIn\": \"متابعة تسجيل الدخول\",\n    \"continueWithLoginCode\": \"متابعة مع رمز تسجيل الدخول\",\n    \"backToLogin\": \"العودة إلى تسجيل الدخول\",\n    \"enterCode\": \"أدخل الرمز\",\n    \"enterCodeManually\": \"أدخل الرمز يدويًا\",\n    \"continueWithEmail\": \"متابعة مع البريد الإلكتروني\",\n    \"enterPassword\": \"أدخل كلمة المرور\",\n    \"loginAs\": \"تسجيل الدخول باسم\",\n    \"invalidVerificationCode\": \"الرجاء إدخال رمز التحقق الصحيح\",\n    \"tooFrequentVerificationCodeRequest\": \"لقد قدمت طلبات كثيرة. يُرجى المحاولة لاحقًا.\",\n    \"invalidLoginCredentials\": \"كلمة المرور الخاصة بك غير صحيحة، يرجى المحاولة مرة أخرى\",\n    \"resetPassword\": \"إعادة ضبط كلمة المرور\",\n    \"resetPasswordDescription\": \"أدخل بريدك الإلكتروني لإعادة ضبط كلمة المرور الخاصة بك\",\n    \"continueToResetPassword\": \"متابعة إعادة ضبط كلمة المرور\",\n    \"resetPasswordSuccess\": \"تم إعادة ضبط كلمة المرور بنجاح\",\n    \"resetPasswordFailed\": \"فشل إعادة ضبط كلمة المرور\",\n    \"resetPasswordLinkSent\": \"تم إرسال رابط إعادة ضبط كلمة المرور إلى بريدك الإلكتروني. يُرجى مراجعة بريدك الوارد على\",\n    \"resetPasswordLinkExpired\": \"انتهت صلاحية رابط إعادة ضبط كلمة المرور. يُرجى طلب رابط جديد.\",\n    \"resetPasswordLinkInvalid\": \"رابط إعادة ضبط كلمة المرور غير صالح. يُرجى طلب رابط جديد.\",\n    \"enterNewPasswordFor\": \"أدخل كلمة المرور الجديدة لـ \",\n    \"newPassword\": \"كلمة المرور الجديدة\",\n    \"enterNewPassword\": \"أدخل كلمة المرور الجديدة\",\n    \"confirmPassword\": \"تأكيد كلمة المرور\",\n    \"confirmNewPassword\": \"أدخل كلمة المرور الجديدة\",\n    \"newPasswordCannotBeEmpty\": \"لا يمكن أن تكون كلمة المرور الجديدة فارغة\",\n    \"confirmPasswordCannotBeEmpty\": \"تأكيد كلمة المرور لا يمكن أن يكون فارغا\",\n    \"passwordsDoNotMatch\": \"كلمات المرور غير متطابقة\",\n    \"verifying\": \"جاري التحقق...\",\n    \"continueWithPassword\": \"متابعة مع كلمة المرور\",\n    \"youAreInLocalMode\": \"أنت في الوضع المحلي.\",\n    \"loginToAppFlowyCloud\": \"تسجيل الدخول إلى AppFlowy Cloud\",\n    \"anonymous\": \"مجهول\",\n    \"LogInWithGoogle\": \"تسجيل الدخول عبر جوجل\",\n    \"LogInWithGithub\": \"تسجيل الدخول مع جيثب\",\n    \"LogInWithDiscord\": \"تسجيل الدخول مع ديسكورد\",\n    \"loginAsGuestButtonText\": \"تسجيل دخول كضيف\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"اختر مساحة العمل الخاصة بك\",\n    \"defaultName\": \"مساحة العمل الخاصة بي\",\n    \"create\": \"قم بإنشاء مساحة عمل\",\n    \"new\": \"مساحة عمل جديدة\",\n    \"importFromNotion\": \"الاستيراد من Notion\",\n    \"learnMore\": \"تعلم المزيد\",\n    \"reset\": \"إعادة تعيين مساحة العمل\",\n    \"renameWorkspace\": \"إعادة تسمية مساحة العمل\",\n    \"workspaceNameCannotBeEmpty\": \"لا يمكن أن يكون اسم مساحة العمل فارغًا\",\n    \"resetWorkspacePrompt\": \"ستؤدي إعادة تعيين مساحة العمل إلى حذف جميع الصفحات والبيانات الموجودة بداخلها. هل أنت متأكد أنك تريد إعادة تعيين مساحة العمل؟ وبدلاً من ذلك، يمكنك الاتصال بفريق الدعم لاستعادة مساحة العمل\",\n    \"hint\": \"مساحة العمل\",\n    \"notFoundError\": \"مساحة العمل غير موجودة\",\n    \"failedToLoad\": \"هناك خطأ ما! فشل تحميل مساحة العمل. حاول إغلاق أي مثيل مفتوح لـ @:appName وحاول مرة أخرى.\",\n    \"errorActions\": {\n      \"reportIssue\": \"بلغ عن خطأ\",\n      \"reportIssueOnGithub\": \"الإبلاغ عن مشكلة على Github\",\n      \"exportLogFiles\": \"تصدير ملفات السجل\",\n      \"reachOut\": \"تواصل مع ديسكورد\"\n    },\n    \"menuTitle\": \"مساحات العمل\",\n    \"deleteWorkspaceHintText\": \" هل أنت متأكد من أنك تريد حذف مساحة العمل؟ لا يمكن التراجع عن هذا الإجراء، وستختفي أي صفحات قمت بنشرها سابقاً.\",\n    \"createSuccess\": \"تم إنشاء مساحة العمل بنجاح\",\n    \"createFailed\": \"فشل في إنشاء مساحة العمل\",\n    \"createLimitExceeded\": \"لقد وصلت إلى الحد الأقصى لمساحة العمل المسموح بها لحسابك. إذا كنت بحاجة إلى مساحات عمل إضافية لمواصلة عملك، فيرجى تقديم طلب على Github\",\n    \"deleteSuccess\": \"تم حذف مساحة العمل بنجاح\",\n    \"deleteFailed\": \"فشل في حذف مساحة العمل\",\n    \"openSuccess\": \"تم فتح مساحة العمل بنجاح\",\n    \"openFailed\": \"فشل في فتح مساحة العمل\",\n    \"renameSuccess\": \"تم إعادة تسمية مساحة العمل بنجاح\",\n    \"renameFailed\": \"فشل في إعادة تسمية مساحة العمل\",\n    \"updateIconSuccess\": \"تم تحديث أيقونة مساحة العمل بنجاح\",\n    \"updateIconFailed\": \"فشل تحديث أيقونة مساحة العمل\",\n    \"cannotDeleteTheOnlyWorkspace\": \"لا يمكن حذف مساحة العمل الوحيدة\",\n    \"fetchWorkspacesFailed\": \"فشل في الوصول لمساحات العمل\",\n    \"leaveCurrentWorkspace\": \"مغادرة مساحة العمل\",\n    \"leaveCurrentWorkspacePrompt\": \"هل أنت متأكد أنك تريد مغادرة مساحة العمل الحالية؟\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"مشاركه\",\n    \"workInProgress\": \"قريباً\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"نسخ إلى الحافظة\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"نسخ الرابط\",\n    \"publishToTheWeb\": \"نشر على الويب\",\n    \"publishToTheWebHint\": \"إنشاء موقع ويب مع AppFlowy\",\n    \"publish\": \"نشر\",\n    \"unPublish\": \"التراجع عن النشر\",\n    \"visitSite\": \"زيارة الموقع\",\n    \"exportAsTab\": \"تصدير كـ\",\n    \"publishTab\": \"نشر\",\n    \"shareTab\": \"مشاركة\",\n    \"publishOnAppFlowy\": \"نشر على AppFlowy\",\n    \"shareTabTitle\": \"دعوة للتعاون\",\n    \"shareTabDescription\": \"من أجل التعاون السهل مع أي شخص\",\n    \"copyLinkSuccess\": \"تم نسخ الرابط إلى الحافظة\",\n    \"copyShareLink\": \"نسخ رابط المشاركة\",\n    \"copyLinkFailed\": \"فشل في نسخ الرابط إلى الحافظة\",\n    \"copyLinkToBlockSuccess\": \"تم نسخ رابط الكتلة إلى الحافظة\",\n    \"copyLinkToBlockFailed\": \"فشل في نسخ رابط الكتلة إلى الحافظة\",\n    \"manageAllSites\": \"إدارة كافة المواقع\",\n    \"updatePathName\": \"تحديث اسم المسار\"\n  },\n  \"moreAction\": {\n    \"small\": \"صغير\",\n    \"medium\": \"متوسط\",\n    \"large\": \"كبير\",\n    \"fontSize\": \"حجم الخط\",\n    \"import\": \"استيراد\",\n    \"moreOptions\": \"المزيد من الخيارات\",\n    \"wordCount\": \"عدد الكلمات: {}\",\n    \"charCount\": \"عدد الأحرف: {}\",\n    \"createdAt\": \"منشأ: {}\",\n    \"deleteView\": \"يمسح\",\n    \"duplicateView\": \"تكرار\",\n    \"wordCountLabel\": \"عدد الكلمات: \",\n    \"charCountLabel\": \"عدد الأحرف: \",\n    \"createdAtLabel\": \"تم إنشاؤه: \",\n    \"syncedAtLabel\": \"تم المزامنة: \",\n    \"saveAsNewPage\": \"حفظ الرسائل في الصفحة\",\n    \"saveAsNewPageDisabled\": \"لا توجد رسائل متاحة\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"نص و Markdown\",\n    \"documentFromV010\": \"مستند من الإصدار 0.1.0\",\n    \"databaseFromV010\": \"قاعدة بيانات من الإصدار 0.1.0\",\n    \"notionZip\": \"ملف Zip المُصدَّر من Notion\",\n    \"csv\": \"CSV\",\n    \"database\": \"قاعدة البيانات\"\n  },\n  \"emojiIconPicker\": {\n    \"iconUploader\": {\n      \"placeholderLeft\": \"اسحب وأفلِت ملفًا، وانقر فوقه \",\n      \"placeholderUpload\": \"رفع\",\n      \"placeholderRight\": \"أو قم بلصق رابط الصورة.\",\n      \"dropToUpload\": \"إفلات ملف لتحميله\",\n      \"change\": \"تغير\"\n    }\n  },\n  \"disclosureAction\": {\n    \"rename\": \"إعادة تسمية\",\n    \"delete\": \"يمسح\",\n    \"duplicate\": \"كرر\",\n    \"unfavorite\": \"إزالة من المفضلة\",\n    \"favorite\": \"اضافة الى المفضلة\",\n    \"openNewTab\": \"افتح في علامة تبويب جديدة\",\n    \"moveTo\": \"نقل إلى\",\n    \"addToFavorites\": \"اضافة الى المفضلة\",\n    \"copyLink\": \"نسخ الرابط\",\n    \"changeIcon\": \"تغيير الأيقونة\",\n    \"collapseAllPages\": \"طي جميع الصفحات الفرعية\",\n    \"movePageTo\": \"تحريك الصفحة إلى\",\n    \"move\": \"تحريك\",\n    \"lockPage\": \"إلغاء تأمين الصفحة\"\n  },\n  \"blankPageTitle\": \"صفحة فارغة\",\n  \"newPageText\": \"صفحة جديدة\",\n  \"newDocumentText\": \"مستند جديد\",\n  \"newGridText\": \"شبكة جديدة\",\n  \"newCalendarText\": \"تقويم جديد\",\n  \"newBoardText\": \"سبورة جديدة\",\n  \"chat\": {\n    \"newChat\": \"الدردشة بالذكاء الاصطناعي\",\n    \"inputMessageHint\": \"اسأل @:appName AI\",\n    \"inputLocalAIMessageHint\": \"اسأل @:appName Local AI\",\n    \"unsupportedCloudPrompt\": \"هذه الميزة متوفرة فقط عند استخدام @:appName Cloud\",\n    \"relatedQuestion\": \"ذات صلة\",\n    \"serverUnavailable\": \"الخدمة غير متاحة مؤقتًا. يرجى المحاولة مرة أخرى لاحقًا.\",\n    \"aiServerUnavailable\": \"تم فقد الاتصال. يرجى التحقق من اتصالك بالإنترنت\",\n    \"retry\": \"إعادة المحاولة\",\n    \"clickToRetry\": \"انقر لإعادة المحاولة\",\n    \"regenerateAnswer\": \"إعادة توليد\",\n    \"question1\": \"كيفية استخدام كانبان لإدارة المهام\",\n    \"question2\": \"شرح طريقة GTD\",\n    \"question3\": \"لماذا تستخدم Rust\",\n    \"question4\": \"وصفة بما في مطبخي\",\n    \"question5\": \"إنشاء رسم توضيحي لصفحتي\",\n    \"question6\": \"قم بإعداد قائمة بالمهام التي يجب القيام بها خلال الأسبوع القادم\",\n    \"aiMistakePrompt\": \"الذكاء الاصطناعي قد يرتكب أخطاء. تحقق من المعلومات المهمة.\",\n    \"chatWithFilePrompt\": \"هل تريد الدردشة مع الملف؟\",\n    \"indexFileSuccess\": \"تم فهرسة الملف بنجاح\",\n    \"inputActionNoPages\": \"لا توجد نتائج للصفحة\",\n    \"referenceSource\": {\n      \"zero\": \"تم العثور على 0 من مصادر\",\n      \"one\": \"تم العثور على {count} مصدر\",\n      \"other\": \"تم العثور على {count} مصادر\"\n    },\n    \"clickToMention\": \"اذكر صفحة\",\n    \"uploadFile\": \"إرفاق ملفات PDF أو ملفات نصية أو ملفات Markdown\",\n    \"questionDetail\": \"مرحبًا {}! كيف يمكنني مساعدتك اليوم؟\",\n    \"indexingFile\": \"الفهرسة {}\",\n    \"generatingResponse\": \"توليد الاستجابة\",\n    \"selectSources\": \"اختر المصادر\",\n    \"currentPage\": \"الصفحة الحالية\",\n    \"sourcesLimitReached\": \"يمكنك فقط تحديد ما يصل إلى 3 مستندات من المستوى العلوي ومستنداتها الفرعية\",\n    \"sourceUnsupported\": \"نحن لا ندعم الدردشة مع قواعد البيانات في الوقت الحالي\",\n    \"regenerate\": \"حاول ثانية\",\n    \"addToPageButton\": \"أضف إلى الصفحة\",\n    \"addToPageTitle\": \"أضف رسالة إلى...\",\n    \"addToNewPage\": \"أضف إلى صفحة جديدة\",\n    \"addToNewPageName\": \"الرسائل المستخرجة من \\\"{}\\\"\",\n    \"addToNewPageSuccessToast\": \"تمت إضافة الرسالة إلى\",\n    \"openPagePreviewFailedToast\": \"فشل في فتح الصفحة\",\n    \"changeFormat\": {\n      \"actionButton\": \"تغيير التنسيق\",\n      \"confirmButton\": \"إعادة توليد مع هذا التنسيق\",\n      \"textOnly\": \"النص\",\n      \"imageOnly\": \"الصورة فقط\",\n      \"textAndImage\": \"النص والصورة\",\n      \"text\": \"الفقرة\",\n      \"bullet\": \"قائمة نقطية\",\n      \"number\": \"قائمة مرقمة\",\n      \"table\": \"الجدول\",\n      \"blankDescription\": \"تنسيق الاستجابة\",\n      \"defaultDescription\": \"الوضع التلقائي\",\n      \"textWithImageDescription\": \"@:chat.changeFormat.text مع الصورة\",\n      \"numberWithImageDescription\": \"@:chat.changeFormat.number مع الصورة\",\n      \"bulletWithImageDescription\": \"@:chat.changeFormat.bullet مع الصورة\",\n      \"tableWithImageDescription\": \"@:chat.changeFormat.table مع الصورة\"\n    },\n    \"switchModel\": {\n      \"label\": \"تبديل النموذج\",\n      \"localModel\": \"النموذج المحلي\",\n      \"cloudModel\": \"نموذج السحابة\",\n      \"autoModel\": \"آلي\"\n    },\n    \"selectBanner\": {\n      \"saveButton\": \"أضف إلى...\",\n      \"selectMessages\": \"حدد الرسائل\",\n      \"nSelected\": \"{} تم التحديد\",\n      \"allSelected\": \"جميعها محددة\"\n    },\n    \"stopTooltip\": \"توقف عن التوليد\"\n  },\n  \"trash\": {\n    \"text\": \"المهملات\",\n    \"restoreAll\": \"استعادة الكل\",\n    \"restore\": \"إسترجاع\",\n    \"deleteAll\": \"حذف الكل\",\n    \"pageHeader\": {\n      \"fileName\": \"اسم الملف\",\n      \"lastModified\": \"آخر تعديل\",\n      \"created\": \"تم انشاؤها\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"هل أنت متأكد من حذف جميع الصفحات في المهملات؟\",\n      \"caption\": \"لا يمكن التراجع عن هذا الإجراء.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"هل أنت متأكد من استعادة جميع الصفحات في المهملات؟\",\n      \"caption\": \"لا يمكن التراجع عن هذا الإجراء.\"\n    },\n    \"restorePage\": {\n      \"title\": \"إسترجاع: {}\",\n      \"caption\": \"هل أنت متأكد أنك تريد استعادة هذه الصفحة؟\"\n    },\n    \"mobile\": {\n      \"actions\": \"إجراءات سلة المهملات\",\n      \"empty\": \"سلة المهملات فارغة\",\n      \"emptyDescription\": \"ليس لديك أي ملفات محذوفة\",\n      \"isDeleted\": \"محذوف\",\n      \"isRestored\": \"تمت استعادته\"\n    },\n    \"confirmDeleteTitle\": \"هل أنت متأكد أنك تريد حذف هذه الصفحة نهائياً؟\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"هذه الصفحة في المهملات\",\n    \"restore\": \"استعادة الصفحة\",\n    \"deletePermanent\": \"الحذف بشكل نهائي\",\n    \"deletePermanentDescription\": \"هل أنت متأكد أنك تريد حذف هذه الصفحة بشكل دائم؟ هذا لا يمكن التراجع عنه.\"\n  },\n  \"dialogCreatePageNameHint\": \"اسم الصفحة\",\n  \"questionBubble\": {\n    \"shortcuts\": \"الاختصارات\",\n    \"whatsNew\": \"ما هو الجديد؟\",\n    \"helpAndDocumentation\": \"المساعدة والتوثيق\",\n    \"getSupport\": \"احصل على الدعم\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"معلومات التصحيح\",\n      \"success\": \"تم نسخ معلومات التصحيح إلى الحافظة!\",\n      \"fail\": \"تعذر نسخ معلومات التصحيح إلى الحافظة\"\n    },\n    \"feedback\": \"تعليق\",\n    \"help\": \"المساعدة والدعم\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"إزالة وإعادة تسمية والمزيد...\",\n    \"addPageTooltip\": \"أضف صفحة في الداخل بسرعة\",\n    \"defaultNewPageName\": \"بدون عنوان\",\n    \"renameDialog\": \"إعادة تسمية\",\n    \"pageNameSuffix\": \"نسخ\"\n  },\n  \"noPagesInside\": \"لا توجد صفحات في الداخل\",\n  \"toolbar\": {\n    \"undo\": \"الغاء التحميل\",\n    \"redo\": \"إعادة\",\n    \"bold\": \"عريض\",\n    \"italic\": \"مائل\",\n    \"underline\": \"تسطير\",\n    \"strike\": \"يتوسطه خط\",\n    \"numList\": \"قائمة مرقمة\",\n    \"bulletList\": \"قائمة نقطية\",\n    \"checkList\": \"قائمة تدقيق\",\n    \"inlineCode\": \"رمز مضمّن\",\n    \"quote\": \"كتلة اقتباس\",\n    \"header\": \"رأس\",\n    \"highlight\": \"تسليط الضوء\",\n    \"color\": \"لون\",\n    \"addLink\": \"إضافة رابط\",\n    \"link\": \"وصلة\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"قم بالتبديل إلى وضع الإضاءة\",\n    \"darkMode\": \"قم بالتبديل إلى الوضع الداكن\",\n    \"openAsPage\": \"فتح كصفحة\",\n    \"addNewRow\": \"أضف صفًا جديدًا\",\n    \"openMenu\": \"انقر لفتح القائمة\",\n    \"dragRow\": \"اضغط مطولاً لإعادة ترتيب الصف\",\n    \"viewDataBase\": \"عرض قاعدة البيانات\",\n    \"referencePage\": \"تمت الإشارة إلى هذا {name}\",\n    \"addBlockBelow\": \"إضافة كتلة أدناه\",\n    \"aiGenerate\": \"توليد\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"إغلاق الشريط الجانبي\",\n    \"openSidebar\": \"فتح الشريط الجانبي\",\n    \"expandSidebar\": \"توسيع كصفحة كاملة\",\n    \"personal\": \"شخصي\",\n    \"private\": \"خاص\",\n    \"workspace\": \"مساحة العمل\",\n    \"favorites\": \"المفضلة\",\n    \"clickToHidePrivate\": \"انقر لإخفاء المساحة الخاصة\\nالصفحات التي قمت بإنشائها هنا مرئية لك فقط\",\n    \"clickToHideWorkspace\": \"انقر لإخفاء مساحة العمل\\nالصفحات التي قمت بإنشائها هنا تكون مرئية لكل الأعضاء\",\n    \"clickToHidePersonal\": \"انقر لإخفاء القسم الشخصي\",\n    \"clickToHideFavorites\": \"انقر لإخفاء القسم المفضل\",\n    \"addAPage\": \"أضف صفحة\",\n    \"addAPageToPrivate\": \"أضف صفحة إلى مساحة خاصة\",\n    \"addAPageToWorkspace\": \"إضافة صفحة إلى مساحة العمل\",\n    \"recent\": \"مؤخرًا\",\n    \"today\": \"اليوم\",\n    \"thisWeek\": \"هذا الأسبوع\",\n    \"others\": \"المفضلات السابقة\",\n    \"earlier\": \"سابقًا\",\n    \"justNow\": \"الآن\",\n    \"minutesAgo\": \"منذ {count} دقيقة\",\n    \"lastViewed\": \"آخر مشاهدة\",\n    \"favoriteAt\": \"المفضلة\",\n    \"emptyRecent\": \"لا توجد صفحات حديثة\",\n    \"emptyRecentDescription\": \"عند عرض الصفحات، ستظهر هنا لسهولة استرجاعها.\",\n    \"emptyFavorite\": \"لا توجد صفحات مفضلة\",\n    \"emptyFavoriteDescription\": \"قم بوضع علامة على الصفحات كمفضلة - سيتم إدراجها هنا للوصول السريع!\",\n    \"removePageFromRecent\": \"هل تريد إزالة هذه الصفحة من القائمة الأخيرة؟\",\n    \"removeSuccess\": \"تمت الإزالة بنجاح\",\n    \"favoriteSpace\": \"المفضلة\",\n    \"RecentSpace\": \"مؤخرًا\",\n    \"Spaces\": \"المساحات\",\n    \"upgradeToPro\": \"الترقية إلى الإصدار الاحترافي\",\n    \"upgradeToAIMax\": \"إلغاء تأمين الذكاء الاصطناعي غير المحدود\",\n    \"storageLimitDialogTitle\": \"لقد نفدت مساحة التخزين المجانية لديك. قم بالترقية لإلغاء تأمين مساحة تخزين غير محدودة\",\n    \"storageLimitDialogTitleIOS\": \"لقد نفدت مساحة التخزين المجانية.\",\n    \"aiResponseLimitTitle\": \"لقد نفدت منك استجابات الذكاء الاصطناعي المجانية. قم بالترقية إلى الخطة الاحترافية أو قم بشراء إضافة الذكاء الاصطناعي لفتح عدد غير محدود من الاستجابات\",\n    \"aiResponseLimitDialogTitle\": \"تم الوصول إلى الحد الأقصى لاستجابات الذكاء الاصطناعي\",\n    \"aiResponseLimit\": \"لقد نفدت استجابات الذكاء الاصطناعي المجانية.<inlang-LineFeed>\\nانتقل إلى الإعدادات -&gt; الخطة -&gt; انقر فوق AI Max أو Pro Plan للحصول على المزيد من استجابات الذكاء الاصطناعي\",\n    \"askOwnerToUpgradeToPro\": \"مساحة العمل الخاصة بك نفدت من مساحة التخزين المجانية. يرجى مطالبة مالك مساحة العمل الخاصة بك بالترقية إلى الخطة الاحترافية\",\n    \"askOwnerToUpgradeToProIOS\": \"مساحة العمل الخاصة بك على وشك النفاد من مساحة التخزين المجانية.\",\n    \"askOwnerToUpgradeToAIMax\": \"لقد نفدت الاستجابات المجانية للذكاء الاصطناعي من مساحة العمل الخاصة بك. يرجى مطالبة مالك مساحة العمل الخاصة بك بترقية الخطة أو شراء إضافات الذكاء الاصطناعي\",\n    \"askOwnerToUpgradeToAIMaxIOS\": \"مساحة العمل الخاصة بك تفتقر إلى الاستجابات المجانية للذكاء الاصطناعي.\",\n    \"purchaseAIMax\": \"لقد نفدت استجابات الصور بالذكاء الاصطناعي من مساحة العمل الخاصة بك. يرجى مطالبة مالك مساحة العمل الخاصة بك بشراء AI Max\",\n    \"aiImageResponseLimit\": \"لقد نفدت استجابات الصور الخاصة بالذكاء الاصطناعي.<inlang-LineFeed>\\nانتقل إلى الإعدادات -&gt; الخطة -&gt; انقر فوق AI Max للحصول على المزيد من استجابات صور AI\",\n    \"purchaseStorageSpace\": \"شراء مساحة تخزين\",\n    \"singleFileProPlanLimitationDescription\": \"لقد تجاوزت الحد الأقصى لحجم تحميل الملف المسموح به في الخطة المجانية. يرجى الترقية إلى الخطة الاحترافية لتحميل ملفات أكبر حجمًا\",\n    \"purchaseAIResponse\": \"شراء\",\n    \"askOwnerToUpgradeToLocalAI\": \"اطلب من مالك مساحة العمل تمكين الذكاء الاصطناعي على الجهاز\",\n    \"upgradeToAILocal\": \"قم بتشغيل النماذج المحلية على جهازك لتحقيق أقصى قدر من الخصوصية\",\n    \"upgradeToAILocalDesc\": \"الدردشة باستخدام ملفات PDF، وتحسين كتابتك، وملء الجداول تلقائيًا باستخدام الذكاء الاصطناعي المحلي\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"تم تصدير ملاحظة إلى Markdown\",\n      \"path\": \"Documents/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"جهات الاتصال\",\n    \"whatsHappening\": \"ماذا يحدث هذا الاسبوع؟\",\n    \"addContact\": \"إضافة جهة اتصال\",\n    \"editContact\": \"تحرير جهة الاتصال\"\n  },\n  \"button\": {\n    \"ok\": \"حسنا\",\n    \"confirm\": \"تأكيد\",\n    \"done\": \"منتهي\",\n    \"cancel\": \"الغاء\",\n    \"signIn\": \"تسجيل الدخول\",\n    \"signOut\": \"خروج\",\n    \"complete\": \"مكتمل\",\n    \"save\": \"حفظ\",\n    \"generate\": \"يولد\",\n    \"esc\": \"خروج\",\n    \"keep\": \"ابقاء\",\n    \"tryAgain\": \"حاول ثانية\",\n    \"discard\": \"تجاهل\",\n    \"replace\": \"يستبدل\",\n    \"insertBelow\": \"إدراج أدناه\",\n    \"insertAbove\": \"أدخل أعلاه\",\n    \"upload\": \"رفع\",\n    \"edit\": \"يحرر\",\n    \"delete\": \"يمسح\",\n    \"copy\": \"النسخ\",\n    \"duplicate\": \"ينسخ\",\n    \"putback\": \"ضعها بالخلف\",\n    \"update\": \"تحديث\",\n    \"share\": \"مشاركة\",\n    \"removeFromFavorites\": \"إزالة من المفضلة\",\n    \"removeFromRecent\": \"إزالة من الأحدث\",\n    \"addToFavorites\": \"اضافة الى المفضلة\",\n    \"favoriteSuccessfully\": \"النجاح المفضل\",\n    \"unfavoriteSuccessfully\": \"النجاح غير المفضل\",\n    \"duplicateSuccessfully\": \"تم التكرار بنجاح\",\n    \"rename\": \"إعادة تسمية\",\n    \"helpCenter\": \"مركز المساعدة\",\n    \"add\": \"اضافة\",\n    \"yes\": \"نعم\",\n    \"no\": \"لا\",\n    \"clear\": \"مسح\",\n    \"remove\": \"حذف\",\n    \"dontRemove\": \"لا تقم بالإزالة\",\n    \"copyLink\": \"نسخ الرابط\",\n    \"align\": \"المحاذاة\",\n    \"login\": \"تسجيل الدخول\",\n    \"logout\": \"تسجيل الخروج\",\n    \"deleteAccount\": \"حذف الحساب\",\n    \"back\": \"خلف\",\n    \"signInGoogle\": \"متابعة مع جوجل\",\n    \"signInGithub\": \"متابعة مع GitHub\",\n    \"signInDiscord\": \"متابعة مع Discord\",\n    \"more\": \"أكثر\",\n    \"create\": \"إنشاء\",\n    \"close\": \"إغلاق\",\n    \"next\": \"التالي\",\n    \"previous\": \"السابق\",\n    \"submit\": \"إرسال\",\n    \"download\": \"تحميل\",\n    \"backToHome\": \"العودة إلى الصفحة الرئيسية\",\n    \"viewing\": \"عرض\",\n    \"editing\": \"تحرير\",\n    \"gotIt\": \"فهمتها\",\n    \"retry\": \"إعادة المحاولة\",\n    \"uploadFailed\": \"فشل الرفع.\",\n    \"copyLinkOriginal\": \"نسخ الرابط إلى الأصل\",\n    \"tryAGain\": \"حاول ثانية\"\n  },\n  \"label\": {\n    \"welcome\": \"مرحباً!\",\n    \"firstName\": \"الاسم الأول\",\n    \"middleName\": \"الاسم الأوسط\",\n    \"lastName\": \"اسم العائلة\",\n    \"stepX\": \"الخطوة {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"غير قادر على الاتصال بحسابك.\",\n      \"failedMsg\": \"يرجى التأكد من إكمال عملية تسجيل الدخول في متصفحك.\"\n    },\n    \"google\": {\n      \"title\": \"تسجيل الدخول إلى GOOGLE\",\n      \"instruction1\": \"لاستيراد جهات اتصال Google الخاصة بك ، ستحتاج إلى ترخيص هذا التطبيق باستخدام متصفح الويب الخاص بك.\",\n      \"instruction2\": \"انسخ هذا الرمز إلى الحافظة الخاصة بك عن طريق النقر فوق الرمز أو تحديد النص:\",\n      \"instruction3\": \"انتقل إلى الرابط التالي في متصفح الويب الخاص بك ، وأدخل الرمز أعلاه:\",\n      \"instruction4\": \"اضغط على الزر أدناه عند الانتهاء من التسجيل:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"إعدادات\",\n    \"popupMenuItem\": {\n      \"settings\": \"إعدادات\",\n      \"members\": \"الأعضاء\",\n      \"trash\": \"سلة المحذوفات\",\n      \"helpAndDocumentation\": \"المساعدة والتوثيق\",\n      \"getSupport\": \"احصل على الدعم\",\n      \"helpAndSupport\": \"المساعدة والدعم\"\n    },\n    \"sites\": {\n      \"title\": \"المواقع\",\n      \"namespaceTitle\": \"مساحة الاسم\",\n      \"namespaceDescription\": \"إدارة مساحة الاسم والصفحة الرئيسية الخاصة بك\",\n      \"namespaceHeader\": \"مساحة الاسم\",\n      \"homepageHeader\": \"الصفحة الرئيسية\",\n      \"updateNamespace\": \"تحديث مساحة الاسم\",\n      \"removeHomepage\": \"إزالة الصفحة الرئيسية\",\n      \"selectHomePage\": \"حدد الصفحة\",\n      \"clearHomePage\": \"مسح الصفحة الرئيسية لهذه المساحة الاسمية\",\n      \"customUrl\": \"عنوان URL مخصص\",\n      \"homePage\": {\n        \"upgradeToPro\": \"قم بالترقية إلى الخطة الاحترافية لتعيين الصفحة الرئيسية\"\n      },\n      \"namespace\": {\n        \"description\": \"سيتم تطبيق هذا التغيير على جميع الصفحات المنشورة الموجودة على مساحة الاسم هذه بشكل فوري\",\n        \"tooltip\": \"نحن نحتفظ بالحق في إزالة أي مساحات أسماء غير مناسبة\",\n        \"updateExistingNamespace\": \"تحديث مساحة الاسم الحالية\",\n        \"upgradeToPro\": \"قم بالترقية إلى الخطة الاحترافية لتعيين الصفحة الرئيسية\",\n        \"redirectToPayment\": \"إعادة التوجيه إلى صفحة الدفع...\",\n        \"onlyWorkspaceOwnerCanSetHomePage\": \"يمكن فقط لمالك مساحة العمل تعيين الصفحة الرئيسية\",\n        \"pleaseAskOwnerToSetHomePage\": \"يرجى طلب الترقية إلى الخطة الاحترافية من مالك مساحة العمل\"\n      },\n      \"publishedPage\": {\n        \"title\": \"جميع الصفحات المنشورة\",\n        \"description\": \"إدارة صفحاتك المنشورة\",\n        \"page\": \"صفحة\",\n        \"pathName\": \"اسم المسار\",\n        \"date\": \"تاريخ النشر\",\n        \"emptyHinText\": \"ليس لديك صفحات منشورة في مساحة العمل هذه\",\n        \"noPublishedPages\": \"لا توجد صفحات منشورة\",\n        \"settings\": \"نشر الإعدادات\",\n        \"clickToOpenPageInApp\": \"فتح الصفحة في التطبيق\",\n        \"clickToOpenPageInBrowser\": \"فتح الصفحة في المتصفح\"\n      },\n      \"error\": {\n        \"failedToGeneratePaymentLink\": \"فشل في إنشاء رابط الدفع لخطة الاحترافية\",\n        \"failedToUpdateNamespace\": \"فشل في تحديث مساحة الاسم\",\n        \"proPlanLimitation\": \"يجب عليك الترقية إلى الخطة الاحترافية لتحديث مساحة الاسم\",\n        \"namespaceAlreadyInUse\": \"تم أخذ مساحة الاسم بالفعل، يرجى تجربة مساحة اسم أخرى\",\n        \"invalidNamespace\": \"مساحة اسم غير صالحة، يرجى تجربة مساحة اسم أخرى\",\n        \"namespaceLengthAtLeast2Characters\": \"يجب أن تكون مساحة الاسم بطول حرفين على الأقل\",\n        \"onlyWorkspaceOwnerCanUpdateNamespace\": \"يمكن فقط لمالك مساحة العمل تحديث مساحة الاسم\",\n        \"onlyWorkspaceOwnerCanRemoveHomepage\": \"لا يمكن إلا لمالك مساحة العمل إزالة الصفحة الرئيسية\",\n        \"setHomepageFailed\": \"فشل في تعيين الصفحة الرئيسية\",\n        \"namespaceTooLong\": \"مساحة الاسم طويلة جدًا، يرجى تجربة مساحة اسم أخرى\",\n        \"namespaceTooShort\": \"مساحة الاسم قصيرة جدًا، يرجى تجربة مساحة اسم أخرى\",\n        \"namespaceIsReserved\": \"تم حجز مساحة الاسم، يرجى تجربة مساحة اسم أخرى\",\n        \"updatePathNameFailed\": \"فشل في تحديث اسم المسار\",\n        \"removeHomePageFailed\": \"فشل في إزالة الصفحة الرئيسية\",\n        \"publishNameContainsInvalidCharacters\": \"يحتوي اسم المسار على أحرف غير صالحة، يرجى تجربة اسم مسار آخر\",\n        \"publishNameTooShort\": \"اسم المسار قصير جدًا، يرجى تجربة اسم مسار آخر\",\n        \"publishNameTooLong\": \"اسم المسار طويل جدًا، يرجى تجربة اسم مسار آخر\",\n        \"publishNameAlreadyInUse\": \"اسم المسار قيد الاستخدام بالفعل، يرجى تجربة اسم مسار آخر\",\n        \"namespaceContainsInvalidCharacters\": \"تحتوي مساحة الاسم على أحرف غير صالحة، يرجى تجربة مساحة اسم أخرى\",\n        \"publishPermissionDenied\": \"يمكن فقط لمالك مساحة العمل أو ناشر الصفحة إدارة إعدادات النشر\",\n        \"publishNameCannotBeEmpty\": \"لا يمكن أن يكون اسم المسار فارغًا، يرجى تجربة اسم مسار آخر\"\n      },\n      \"success\": {\n        \"namespaceUpdated\": \"تم تحديث مساحة الاسم بنجاح\",\n        \"setHomepageSuccess\": \"تم تعيين الصفحة الرئيسية بنجاح\",\n        \"updatePathNameSuccess\": \"تم تحديث اسم المسار بنجاح\",\n        \"removeHomePageSuccess\": \"تم إزالة الصفحة الرئيسية بنجاح\"\n      }\n    },\n    \"accountPage\": {\n      \"menuLabel\": \"حسابي\",\n      \"title\": \"حسابي\",\n      \"general\": {\n        \"title\": \"اسم الحساب وصورة الملف الشخصي\",\n        \"changeProfilePicture\": \"تغيير صورة الملف الشخصي\"\n      },\n      \"email\": {\n        \"title\": \"بريد إلكتروني\",\n        \"actions\": {\n          \"change\": \"تغيير البريد الإلكتروني\"\n        }\n      },\n      \"login\": {\n        \"title\": \"تسجيل الدخول إلى الحساب\",\n        \"loginLabel\": \"تسجيل الدخول\",\n        \"logoutLabel\": \"تسجيل الخروج\"\n      },\n      \"isUpToDate\": \"تم تحديث @:appName !\",\n      \"officialVersion\": \"الإصدار {version} (الإصدار الرسمي)\"\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"مساحة العمل\",\n      \"title\": \"مساحة العمل\",\n      \"description\": \"قم بتخصيص مظهر مساحة العمل الخاصة بك، والسمة، والخط، وتخطيط النص، وتنسيق التاريخ/الوقت، واللغة.\",\n      \"workspaceName\": {\n        \"title\": \"اسم مساحة العمل\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"أيقونة مساحة العمل\",\n        \"description\": \"قم بتحميل صورة أو استخدم رمزًا تعبيريًا لمساحة عملك. ستظهر الأيقونة في الشريط الجانبي والإشعارات.\"\n      },\n      \"appearance\": {\n        \"title\": \"المظهر\",\n        \"description\": \"قم بتخصيص مظهر مساحة العمل الخاصة بك والموضوع والخط وتخطيط النص والتاريخ والوقت واللغة.\",\n        \"options\": {\n          \"system\": \"آلي\",\n          \"light\": \"فاتح\",\n          \"dark\": \"داكن\"\n        }\n      },\n      \"resetCursorColor\": {\n        \"title\": \"إعادة تعيين لون مؤشر المستند\",\n        \"description\": \"هل أنت متأكد أنك تريد إعادة تعيين لون المؤشر؟\"\n      },\n      \"resetSelectionColor\": {\n        \"title\": \"إعادة تعيين لون اختيار المستند\",\n        \"description\": \"هل أنت متأكد أنك تريد إعادة تعيين لون الإختيار؟\"\n      },\n      \"resetWidth\": {\n        \"resetSuccess\": \"تم إعادة تعيين عرض المستند بنجاح\"\n      },\n      \"theme\": {\n        \"title\": \"السمة\",\n        \"description\": \"حدد السمة المحددة مسبقًا، أو قم برفع موضوعك المخصص.\",\n        \"uploadCustomThemeTooltip\": \"رفع السمة المخصصة\",\n        \"failedToLoadThemes\": \"فشل تحميل السمات، يرجى التحقق من إعدادات الأذونات في إعدادات النظام &lt; الخصوصية والأمان &lt;الملفات والمجلدات &lt; @:appName\"\n      },\n      \"workspaceFont\": {\n        \"title\": \"خط مساحة العمل\",\n        \"noFontHint\": \"لم يتم العثور على الخط، حاول مصطلحًا آخر.\"\n      },\n      \"textDirection\": {\n        \"title\": \"اتجاه النص\",\n        \"leftToRight\": \"من اليسار إلى اليمين\",\n        \"rightToLeft\": \"من اليمين إلى اليسار\",\n        \"auto\": \"آلي\",\n        \"enableRTLItems\": \"تمكين عناصر شريط أدوات RTL\"\n      },\n      \"layoutDirection\": {\n        \"title\": \"اتجاه التخطيط\",\n        \"leftToRight\": \"من اليسار إلى اليمين\",\n        \"rightToLeft\": \"من اليمين إلى اليسار\"\n      },\n      \"dateTime\": {\n        \"title\": \"التاريخ والوقت\",\n        \"example\": \"{} في {} ({})\",\n        \"24HourTime\": \"الوقت بنظام 24 ساعة\",\n        \"dateFormat\": {\n          \"label\": \"تنسيق التاريخ\",\n          \"local\": \"محلي\",\n          \"us\": \"US أميركي\",\n          \"iso\": \"ايزو\",\n          \"friendly\": \"ودي\",\n          \"dmy\": \"يوم/شهر/سنة\"\n        }\n      },\n      \"language\": {\n        \"title\": \"اللغة\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"حذف مساحة العمل\",\n        \"content\": \"هل أنت متأكد من أنك تريد حذف مساحة العمل هذه؟ لا يمكن التراجع عن هذا الإجراء، وسيتم إلغاء نشر أي صفحات قمت بنشرها.\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"مغادرة مساحة العمل\",\n        \"content\": \"هل أنت متأكد من أنك تريد مغادرة مساحة العمل هذه؟ ستفقد إمكانية الوصول إلى جميع الصفحات والبيانات الموجودة فيها.\",\n        \"success\": \"لقد غادرت مساحة العمل بنجاح.\",\n        \"fail\": \"فشل في مغادرة مساحة العمل.\"\n      },\n      \"manageWorkspace\": {\n        \"title\": \"إدارة مساحة العمل\",\n        \"leaveWorkspace\": \"مغادرة مساحة العمل\",\n        \"deleteWorkspace\": \"حذف مساحة العمل\"\n      }\n    },\n    \"manageDataPage\": {\n      \"menuLabel\": \"إدارة البيانات\",\n      \"title\": \"إدارة البيانات\",\n      \"description\": \"إدارة تخزين البيانات محليًا أو استيراد البيانات الموجودة لديك إلى @:appName .\",\n      \"dataStorage\": {\n        \"title\": \"موقع تخزين الملفات\",\n        \"tooltip\": \"الموقع الذي يتم تخزين ملفاتك فيه\",\n        \"actions\": {\n          \"change\": \"تغيير المسار\",\n          \"open\": \"افتح المجلد\",\n          \"openTooltip\": \"فتح موقع مجلد البيانات الحالي\",\n          \"copy\": \"نسخ المسار\",\n          \"copiedHint\": \"تم نسخ المسار!\",\n          \"resetTooltip\": \"إعادة التعيين إلى الموقع الافتراضي\"\n        },\n        \"resetDialog\": {\n          \"title\": \"هل أنت متأكد؟\",\n          \"description\": \"لن يؤدي إعادة تعيين المسار إلى موقع البيانات الافتراضي إلى حذف بياناتك. إذا كنت تريد إعادة استيراد بياناتك الحالية، فيجب عليك نسخ مسار موقعك الحالي أولاً.\"\n        }\n      },\n      \"importData\": {\n        \"title\": \"استيراد البيانات\",\n        \"tooltip\": \"استيراد البيانات من مجلدات النسخ الاحتياطية/البيانات @:appName\",\n        \"description\": \"نسخ البيانات من مجلد بيانات خارجي @:appName\",\n        \"action\": \"تصفح الملف\"\n      },\n      \"encryption\": {\n        \"title\": \"التشفير\",\n        \"tooltip\": \"إدارة كيفية تخزين بياناتك وتشفيرها\",\n        \"descriptionNoEncryption\": \"سيؤدي تشغيل التشفير إلى تشفير كافة البيانات. ولا يمكن التراجع عن هذا الإجراء.\",\n        \"descriptionEncrypted\": \"بياناتك مشفرة.\",\n        \"action\": \"تشفير البيانات\",\n        \"dialog\": {\n          \"title\": \"هل تريد تشفير كافة بياناتك؟\",\n          \"description\": \"سيؤدي تشفير جميع بياناتك إلى الحفاظ على بياناتك آمنة. لا يمكن التراجع عن هذا الإجراء. هل أنت متأكد من أنك تريد المتابعة؟\"\n        }\n      },\n      \"cache\": {\n        \"title\": \"مسح ذاكرة التخزين المؤقت\",\n        \"description\": \"ساعد في حل مشكلات مثل عدم تحميل الصور، أو عدم ظهور الصفحات في مساحة، أو عدم تحميل الخطوط. لن يؤثر هذا على بياناتك.\",\n        \"dialog\": {\n          \"title\": \"مسح ذاكرة التخزين المؤقت\",\n          \"description\": \"ساعد في حل مشكلات مثل عدم تحميل الصور، أو عدم ظهور الصفحات في مساحة، أو عدم تحميل الخطوط. لن يؤثر هذا على بياناتك.\",\n          \"successHint\": \"تم مسح ذاكرة التخزين المؤقت!\"\n        }\n      },\n      \"data\": {\n        \"fixYourData\": \"إصلاح بياناتك\",\n        \"fixButton\": \"إصلاح\",\n        \"fixYourDataDescription\": \"إذا كنت تواجه مشكلات مع بياناتك، فيمكنك محاولة إصلاحها هنا.\"\n      }\n    },\n    \"shortcutsPage\": {\n      \"menuLabel\": \"اختصارات\",\n      \"title\": \"اختصارات\",\n      \"editBindingHint\": \"إدخال ربط جديد\",\n      \"searchHint\": \"بحث\",\n      \"actions\": {\n        \"resetDefault\": \"إعادة ضبط الافتراضي\"\n      },\n      \"errorPage\": {\n        \"message\": \"فشل تحميل الاختصارات: {}\",\n        \"howToFix\": \"يرجى المحاولة مرة أخرى، إذا استمرت المشكلة، فيرجى التواصل معنا عبر GitHub.\"\n      },\n      \"resetDialog\": {\n        \"title\": \"إعادة ضبط الاختصارات\",\n        \"description\": \"سيؤدي هذا إلى إعادة ضبط كافة ارتباطات المفاتيح الخاصة بك إلى الوضع الافتراضي، ولا يمكنك التراجع عن هذا لاحقًا، هل أنت متأكد من أنك تريد المتابعة؟\",\n        \"buttonLabel\": \"إعادة ضبط\"\n      },\n      \"conflictDialog\": {\n        \"title\": \"{} قيد الاستخدام حاليًا\",\n        \"descriptionPrefix\": \"يتم استخدام ارتباطات المفاتيح هذه حاليًا بواسطة \",\n        \"descriptionSuffix\": \"إذا قمت باستبدال ارتباط المفتاح هذا، فسيتم إزالته من {}.\",\n        \"confirmLabel\": \"متابعة\"\n      },\n      \"editTooltip\": \"اضغط لبدء تحرير ربط المفتاح\",\n      \"keybindings\": {\n        \"toggleToDoList\": \"التبديل إلى قائمة المهام\",\n        \"insertNewParagraphInCodeblock\": \"إدراج فقرة جديدة\",\n        \"pasteInCodeblock\": \"لصق في كتلة الكود\",\n        \"selectAllCodeblock\": \"حدد الكل\",\n        \"indentLineCodeblock\": \"أدخل فراغين فاصلين في بداية السطر\",\n        \"outdentLineCodeblock\": \"حذف فراغين فاصلين في بداية السطر\",\n        \"twoSpacesCursorCodeblock\": \"إدراج فراغين فاصلين عند المؤشر\",\n        \"copy\": \"اختيار النسخ\",\n        \"paste\": \"لصق في المحتوى\",\n        \"cut\": \"اختيار القص\",\n        \"alignLeft\": \"محاذاة النص إلى اليسار\",\n        \"alignCenter\": \"محاذاة النص إلى الوسط\",\n        \"alignRight\": \"محاذاة النص إلى اليمين\",\n        \"insertInlineMathEquation\": \"إدراج معادلة رياضية مضمنة\",\n        \"undo\": \"التراجع\",\n        \"redo\": \"الإعادة\",\n        \"convertToParagraph\": \"تحويل الكتلة إلى فقرة\",\n        \"backspace\": \"الحذف\",\n        \"deleteLeftWord\": \"حذف الكلمة اليسرى\",\n        \"deleteLeftSentence\": \"حذف الجملة اليسرى\",\n        \"delete\": \"حذف الحرف الأيمن\",\n        \"deleteMacOS\": \"حذف الحرف الأيسر\",\n        \"deleteRightWord\": \"حذف الكلمة اليمنى\",\n        \"moveCursorLeft\": \"حرك المؤشر إلى اليسار\",\n        \"moveCursorBeginning\": \"حرك المؤشر إلى البداية\",\n        \"moveCursorLeftWord\": \"حرك المؤشر إلى اليسار كلمة واحدة\",\n        \"moveCursorLeftSelect\": \"حدد وحرك المؤشر إلى اليسار\",\n        \"moveCursorBeginSelect\": \"حدد وحرك المؤشر إلى البداية\",\n        \"moveCursorLeftWordSelect\": \"حدد وحرك المؤشر إلى اليسار كلمة واحدة\",\n        \"moveCursorRight\": \"حرك المؤشر إلى اليمين\",\n        \"moveCursorEnd\": \"حرك المؤشر إلى النهاية\",\n        \"moveCursorRightWord\": \"حرك المؤشر إلى اليمين كلمة واحدة\",\n        \"moveCursorRightSelect\": \"حدد المؤشر وحركه إلى اليمين\",\n        \"moveCursorEndSelect\": \"حدد وحرك المؤشر إلى النهاية\",\n        \"moveCursorRightWordSelect\": \"حدد وحرك المؤشر إلى اليمين كلمة واحدة\",\n        \"moveCursorUp\": \"حرك المؤشر لأعلى\",\n        \"moveCursorTopSelect\": \"حدد وحرك المؤشر إلى الأعلى\",\n        \"moveCursorTop\": \"حرك المؤشر إلى الأعلى\",\n        \"moveCursorUpSelect\": \"حدد وحرك المؤشر لأعلى\",\n        \"moveCursorBottomSelect\": \"حدد وحرك المؤشر إلى الأسفل\",\n        \"moveCursorBottom\": \"حرك المؤشر إلى الأسفل\",\n        \"moveCursorDown\": \"حرك المؤشر إلى الأسفل\",\n        \"moveCursorDownSelect\": \"حدد وحرك المؤشر لأسفل\",\n        \"home\": \"تمرير إلى الأعلى\",\n        \"end\": \"تمرير إلى الأسفل\",\n        \"toggleBold\": \"تبديل الخط الغامق\",\n        \"toggleItalic\": \"تبديل الخط المائل\",\n        \"toggleUnderline\": \"تبديل التسطير\",\n        \"toggleStrikethrough\": \"تبديل الشطب\",\n        \"toggleCode\": \"تبديل الكود المضمن\",\n        \"toggleHighlight\": \"تبديل التمييز\",\n        \"showLinkMenu\": \"عرض قائمة الروابط\",\n        \"openInlineLink\": \"افتح الرابط المضمن\",\n        \"openLinks\": \"فتح جميع الروابط المحددة\",\n        \"indent\": \"المسافة البادئة\",\n        \"outdent\": \"مسافة بادئة سلبية\",\n        \"exit\": \"الخروج من التحرير\",\n        \"pageUp\": \"قم بالتمرير صفحة واحدة لأعلى\",\n        \"pageDown\": \"قم بالتمرير صفحة واحدة لأسفل\",\n        \"selectAll\": \"حدد الكل\",\n        \"pasteWithoutFormatting\": \"لصق المحتوى بدون تنسيق\",\n        \"showEmojiPicker\": \"عرض أداة اختيار الرموز التعبيرية\",\n        \"enterInTableCell\": \"إضافة فاصل الأسطر في الجدول\",\n        \"leftInTableCell\": \"حرك خلية واحدة إلى اليسار في الجدول\",\n        \"rightInTableCell\": \"حرك خلية واحدة إلى اليمين في الجدول\",\n        \"upInTableCell\": \"التحرك لأعلى خلية واحدة في الجدول\",\n        \"downInTableCell\": \"التحرك لأسفل خلية واحدة في الجدول\",\n        \"tabInTableCell\": \"انتقل إلى الخلية التالية المتوفرة في الجدول\",\n        \"shiftTabInTableCell\": \"انتقل إلى الخلية المتوفرة سابقًا في الجدول\",\n        \"backSpaceInTableCell\": \"توقف في بداية الخلية\"\n      },\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"إدراج فقرة جديدة بجوار كتلة الكود\",\n        \"codeBlockIndentLines\": \"أدخل مسافتين في بداية السطر في كتلة الكود\",\n        \"codeBlockOutdentLines\": \"حذف مسافتين في بداية السطر في كتلة الكود\",\n        \"codeBlockAddTwoSpaces\": \"أدخل مسافتين في موضع المؤشر في كتلة الكود\",\n        \"codeBlockSelectAll\": \"تحديد كل المحتوى داخل كتلة الكود\",\n        \"codeBlockPasteText\": \"لصق النص في كتلة الكود\",\n        \"textAlignLeft\": \"محاذاة النص إلى اليسار\",\n        \"textAlignCenter\": \"محاذاة النص إلى الوسط\",\n        \"textAlignRight\": \"محاذاة النص إلى اليمين\"\n      },\n      \"couldNotLoadErrorMsg\": \"لم نتمكن من تحميل الاختصارات، حاول مرة أخرى\",\n      \"couldNotSaveErrorMsg\": \"لم نتمكن من حفظ الاختصارات، حاول مرة أخرى\"\n    },\n    \"aiPage\": {\n      \"title\": \"إعدادات الذكاء الاصطناعي\",\n      \"menuLabel\": \"إعدادات الذكاء الاصطناعي\",\n      \"keys\": {\n        \"enableAISearchTitle\": \"بحث الذكاء الاصطناعي\",\n        \"aiSettingsDescription\": \"اختر النموذج المفضل لديك لتشغيل AppFlowy AI. يتضمن الآن GPT 4-o وClaude 3,5 وLlama 3.1 وMistral 7B\",\n        \"loginToEnableAIFeature\": \"لا يتم تمكين ميزات الذكاء الاصطناعي إلا بعد تسجيل الدخول باستخدام @:appName Cloud. إذا لم يكن لديك حساب @:appName ، فانتقل إلى \\\"حسابي\\\" للتسجيل\",\n        \"llmModel\": \"نموذج اللغة\",\n        \"globalLLMModel\": \"نموذج اللغة العام\",\n        \"readOnlyField\": \"هذا الحقل للقراءة فقط\",\n        \"llmModelType\": \"نوع نموذج اللغة\",\n        \"downloadLLMPrompt\": \"تنزيل {}\",\n        \"downloadAppFlowyOfflineAI\": \"سيؤدي تنزيل حزمة AI دون اتصال بالإنترنت إلى تفعيل تشغيل AI على جهازك. هل تريد الاستمرار؟\",\n        \"downloadLLMPromptDetail\": \" تنزيل النموذج المحلي {} سيستخدم ما يصل إلى {} من مساحة التخزين. هل تريد الاستمرار؟\",\n        \"downloadBigFilePrompt\": \"قد يستغرق الأمر حوالي 10 دقائق لإكمال التنزيل\",\n        \"downloadAIModelButton\": \"تنزيل\",\n        \"downloadingModel\": \"جاري التنزيل\",\n        \"localAILoaded\": \"تمت إضافة نموذج الذكاء الاصطناعي المحلي بنجاح وهو جاهز للاستخدام\",\n        \"localAIStart\": \"بدأت الدردشة المحلية بالذكاء الاصطناعي...\",\n        \"localAILoading\": \"جاري تحميل نموذج الدردشة المحلية للذكاء الاصطناعي...\",\n        \"localAIStopped\": \"تم إيقاف الذكاء الاصطناعي المحلي\",\n        \"localAIRunning\": \"الذكاء الاصطناعي المحلي قيد التشغيل\",\n        \"localAINotReadyRetryLater\": \"جاري تهيئة الذكاء الاصطناعي المحلي، يرجى المحاولة مرة أخرى لاحقًا\",\n        \"localAIDisabled\": \"أنت تستخدم الذكاء الاصطناعي المحلي، ولكنه مُعطّل. يُرجى الانتقال إلى الإعدادات لتفعيله أو تجربة نموذج آخر.\",\n        \"localAIInitializing\": \"يتم تهيئة الذكاء الاصطناعي المحلي وقد يستغرق الأمر بضع دقائق، حسب جهازك\",\n        \"localAINotReadyTextFieldPrompt\": \"لا يمكنك التحرير أثناء تحميل الذكاء الاصطناعي المحلي\",\n        \"localAIDisabledTextFieldPrompt\": \"لا يمكنك التحرير أثناء تعطيل الذكاء الاصطناعي المحلي\",\n        \"failToLoadLocalAI\": \"فشل في بدء تشغيل الذكاء الاصطناعي المحلي\",\n        \"restartLocalAI\": \"إعادة تشغيل الذكاء الاصطناعي المحلي\",\n        \"disableLocalAITitle\": \"تعطيل الذكاء الاصطناعي المحلي\",\n        \"disableLocalAIDescription\": \"هل تريد تعطيل الذكاء الاصطناعي المحلي؟\",\n        \"localAIToggleTitle\": \"التبديل لتفعيل أو تعطيل الذكاء الاصطناعي المحلي\",\n        \"localAIToggleSubTitle\": \"قم بتشغيل نماذج الذكاء الاصطناعي المحلية الأكثر تقدمًا داخل AppFlowy للحصول على أقصى درجات الخصوصية والأمان\",\n        \"offlineAIInstruction1\": \"اتبع\",\n        \"offlineAIInstruction2\": \"تعليمات\",\n        \"offlineAIInstruction3\": \"لتفعيل الذكاء الاصطناعي دون اتصال بالإنترنت.\",\n        \"offlineAIDownload1\": \"إذا لم تقم بتنزيل AppFlowy AI، فيرجى\",\n        \"offlineAIDownload2\": \"التنزيل\",\n        \"offlineAIDownload3\": \"إنه أولا\",\n        \"activeOfflineAI\": \"نشط\",\n        \"downloadOfflineAI\": \"التنزيل\",\n        \"openModelDirectory\": \"افتح المجلد\",\n        \"laiNotReady\": \"لم يتم تثبيت تطبيق الذكاء الاصطناعي المحلي بشكل صحيح.\",\n        \"ollamaNotReady\": \"خادم Ollama غير جاهز.\",\n        \"pleaseFollowThese\": \"اتبع هؤلاء\",\n        \"instructions\": \"التعليمات\",\n        \"installOllamaLai\": \"لإعداد Ollama وAppFlowy Local AI. تخطَّ هذه الخطوة إذا كنت قد قمت بإعدادها بالفعل\",\n        \"modelsMissing\": \"لم يتم العثور على النماذج المطلوبة.\",\n        \"downloadModel\": \"لتنزيلها.\",\n        \"startLocalAI\": \"قد يستغرق الأمر بضع ثوانٍ لبدء تشغيل الذكاء الاصطناعي المحلي\"\n      }\n    },\n    \"planPage\": {\n      \"menuLabel\": \"الخطة\",\n      \"title\": \"خطة التسعير\",\n      \"planUsage\": {\n        \"title\": \"ملخص استخدام الخطة\",\n        \"storageLabel\": \"تخزين\",\n        \"storageUsage\": \"{} من {} جيجا بايت\",\n        \"unlimitedStorageLabel\": \"تخزين غير محدود\",\n        \"collaboratorsLabel\": \"أعضاء\",\n        \"collaboratorsUsage\": \"{} ل {}\",\n        \"aiResponseLabel\": \"استجابات الذكاء الاصطناعي\",\n        \"aiResponseUsage\": \"{} ل {}\",\n        \"unlimitedAILabel\": \"استجابات غير محدودة\",\n        \"proBadge\": \"احترافية\",\n        \"aiMaxBadge\": \"الذكاء الاصطناعي ماكس\",\n        \"aiOnDeviceBadge\": \"الذكاء الاصطناعي على الجهاز لنظام التشغيل Mac\",\n        \"memberProToggle\": \"مزيد من الأعضاء وذكاء اصطناعي غير محدود\",\n        \"aiMaxToggle\": \"ذكاء اصطناعي غير محدود وإمكانية الوصول إلى نماذج متقدمة\",\n        \"aiOnDeviceToggle\": \"الذكاء الاصطناعي المحلي لتحقيق الخصوصية القصوى\",\n        \"aiCredit\": {\n          \"title\": \"أضف رصيد الذكاء الاصطناعي @:appName\",\n          \"price\": \"{}\",\n          \"priceDescription\": \"مقابل 1000 نقطة\",\n          \"purchase\": \"شراء الذكاء الاصطناعي\",\n          \"info\": \"أضف 1000 أرصدة الذكاء الاصطناعي لكل مساحة عمل وقم بدمج الذكاء الاصطناعي القابل للتخصيص بسلاسة في سير عملك للحصول على نتائج أذكى وأسرع مع ما يصل إلى:\",\n          \"infoItemOne\": \"10000 استجابة لكل قاعدة بيانات\",\n          \"infoItemTwo\": \"1000 استجابة لكل مساحة عمل\"\n        },\n        \"currentPlan\": {\n          \"bannerLabel\": \"الخطة الحالية\",\n          \"freeTitle\": \"مجاني\",\n          \"proTitle\": \"محترف\",\n          \"teamTitle\": \"فريق\",\n          \"freeInfo\": \"مثالي للأفراد حتى عضوين لتنظيم كل شيء\",\n          \"proInfo\": \"مثالي للفرق الصغيرة والمتوسطة التي يصل عددها إلى 10 أعضاء.\",\n          \"teamInfo\": \"مثالي لجميع الفرق المنتجة والمنظمة جيدًا.\",\n          \"upgrade\": \"تغيير الخطة\",\n          \"canceledInfo\": \"لقد تم إلغاء خطتك، وسيتم تخفيض مستوى خطتك إلى الخطة المجانية في {}.\"\n        },\n        \"addons\": {\n          \"title\": \"مَرافِق\",\n          \"addLabel\": \"إضافة\",\n          \"activeLabel\": \"تمت الإضافة\",\n          \"aiMax\": {\n            \"title\": \"الحد الأقصى للذكاء الاصطناعي\",\n            \"description\": \"استجابات الذكاء الاصطناعي غير المحدودة مدعومة بـ GPT-4o وClaude 3.5 Sonnet والمزيد\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"لكل مستخدم شهريًا يتم تحصيل الرسوم سنويًا\"\n          },\n          \"aiOnDevice\": {\n            \"title\": \"الذكاء الاصطناعي على الجهاز لنظام التشغيل Mac\",\n            \"description\": \"قم بتشغيل Mistral 7B وLLAMA 3 والمزيد من النماذج المحلية على جهازك\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"لكل مستخدم شهريًا يتم تحصيل الرسوم سنويًا\",\n            \"recommend\": \"يوصى باستخدام M1 أو أحدث\"\n          }\n        },\n        \"deal\": {\n          \"bannerLabel\": \"صفقة العام الجديد!\",\n          \"title\": \"تنمية فريقك!\",\n          \"info\": \"قم بالترقية واحصل على خصم 10% على خطط Pro وTeam! عزز إنتاجية مساحة العمل لديك من خلال ميزات جديدة قوية بما في ذلك الذكاء الاصطناعي @:appName .\",\n          \"viewPlans\": \"عرض الخطط\"\n        }\n      }\n    },\n    \"billingPage\": {\n      \"menuLabel\": \"الفوترة\",\n      \"title\": \"الفوترة\",\n      \"plan\": {\n        \"title\": \"الخطة\",\n        \"freeLabel\": \"مجاني\",\n        \"proLabel\": \"مجاني\",\n        \"planButtonLabel\": \"تغيير الخطة\",\n        \"billingPeriod\": \"فترة الفوترة\",\n        \"periodButtonLabel\": \"تعديل الفترة\"\n      },\n      \"paymentDetails\": {\n        \"title\": \"تفاصيل الدفع\",\n        \"methodLabel\": \"طريقة الدفع\",\n        \"methodButtonLabel\": \"طريقة التعديل\"\n      },\n      \"addons\": {\n        \"title\": \"مَرافِق\",\n        \"addLabel\": \"إضافة\",\n        \"removeLabel\": \"إزالة\",\n        \"renewLabel\": \"تجديد\",\n        \"aiMax\": {\n          \"label\": \"الحد الأقصى للذكاء الاصطناعي\",\n          \"description\": \"إلغاء تأمين عدد غير محدود من الذكاء الاصطناعي والنماذج المتقدمة\",\n          \"activeDescription\": \"الفاتورة القادمة مستحقة في {}\",\n          \"canceledDescription\": \"سيكون AI Max متوفرا حتى {}\"\n        },\n        \"aiOnDevice\": {\n          \"label\": \"الذكاء الاصطناعي على الجهاز لنظام التشغيل Mac\",\n          \"description\": \"إلغاء تأمين الذكاء الاصطناعي غير المحدود على جهازك\",\n          \"activeDescription\": \"الفاتورة القادمة مستحقة في {}\",\n          \"canceledDescription\": \"ستتوفر ميزة AI On-device for Mac حتى {}\"\n        },\n        \"removeDialog\": {\n          \"title\": \"إزالة {}\",\n          \"description\": \"هل أنت متأكد من أنك تريد إزالة {plan}؟ ستفقد الوصول إلى ميزات وفوائد {plan} على الفور.\"\n        }\n      },\n      \"currentPeriodBadge\": \"الحالي\",\n      \"changePeriod\": \"فترة التغيير\",\n      \"planPeriod\": \"{} فترة\",\n      \"monthlyInterval\": \"شهريا\",\n      \"monthlyPriceInfo\": \"لكل مقعد يتم دفع الفاتورة شهريًا\",\n      \"annualInterval\": \"سنويا\",\n      \"annualPriceInfo\": \"لكل مقعد يتم دفع الفاتورة سنويًا\"\n    },\n    \"comparePlanDialog\": {\n      \"title\": \"قارن واختر الخطة\",\n      \"planFeatures\": \" الخطة\\nميزات\",\n      \"current\": \"الحالي\",\n      \"actions\": {\n        \"upgrade\": \"ترقية\",\n        \"downgrade\": \"يرجع إلى إصدار أقدم\",\n        \"current\": \"الحالي\"\n      },\n      \"freePlan\": {\n        \"title\": \"مجاني\",\n        \"description\": \"للأفراد حتى عضوين لتنظيم كل شيء\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"مجاني للأبد\"\n      },\n      \"proPlan\": {\n        \"title\": \"احترافي\",\n        \"description\": \"للفرق الصغيرة لإدارة المشاريع ومعرفة الفريق\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"لكل مستخدم شهريا\\nيتم فوترتها سنويا\\n\\n{} يتم الفوترتها شهريًا\"\n      },\n      \"planLabels\": {\n        \"itemOne\": \"مساحات العمل\",\n        \"itemTwo\": \"الأعضاء\",\n        \"itemThree\": \"التخزين\",\n        \"itemFour\": \"التعاون في الزمن الحقيقي\",\n        \"itemFive\": \"تطبيق الجوال\",\n        \"itemSix\": \"استجابات الذكاء الاصطناعي\",\n        \"itemSeven\": \"صور الذكاء الاصطناعي\",\n        \"itemFileUpload\": \"رفع الملفات\",\n        \"customNamespace\": \"مساحة اسم مخصصة\",\n        \"tooltipFive\": \"تعاون في صفحات محددة مع غير الأعضاء\",\n        \"tooltipSix\": \"تعني مدة الحياة أن عدد الاستجابات لا يتم إعادة ضبطه أبدًا\",\n        \"intelligentSearch\": \"البحث الذكي\",\n        \"tooltipSeven\": \"يتيح لك تخصيص جزء من عنوان URL لمساحة العمل الخاصة بك\",\n        \"customNamespaceTooltip\": \"عنوان URL للموقع المنشور المخصص\"\n      },\n      \"freeLabels\": {\n        \"itemOne\": \"يتم فرض رسوم على كل مساحة عمل\",\n        \"itemTwo\": \"حتى 2\",\n        \"itemThree\": \"5 جيجا بايت\",\n        \"itemFour\": \"نعم\",\n        \"itemFive\": \"نعم\",\n        \"itemSix\": \"10 مدى الحياة\",\n        \"itemSeven\": \"2 مدى الحياة\",\n        \"itemFileUpload\": \"حتى 7 ميجا بايت\",\n        \"intelligentSearch\": \"البحث الذكي\"\n      },\n      \"proLabels\": {\n        \"itemOne\": \"تتم الفوترة على كل مساحة عمل\",\n        \"itemTwo\": \"حتى 10\",\n        \"itemThree\": \"غير محدود\",\n        \"itemFour\": \"نعم\",\n        \"itemFive\": \"نعم\",\n        \"itemSix\": \"غير محدود\",\n        \"itemSeven\": \"10 صور شهريا\",\n        \"itemFileUpload\": \"غير محدود\",\n        \"intelligentSearch\": \"البحث الذكي\"\n      },\n      \"paymentSuccess\": {\n        \"title\": \"أنت الآن على خطة {}!\",\n        \"description\": \"لقد تمت معالجة الدفع بنجاح وتم ترقية خطتك إلى @:appName {}. يمكنك عرض تفاصيل خطتك على صفحة الخطة\"\n      },\n      \"downgradeDialog\": {\n        \"title\": \"هل أنت متأكد أنك تريد تخفيض خطتك؟\",\n        \"description\": \"سيؤدي تخفيض مستوى خطتك إلى إعادتك إلى الخطة المجانية. قد يفقد الأعضاء إمكانية الوصول إلى مساحة العمل هذه وقد تحتاج إلى تحرير مساحة لتلبية حدود التخزين للخطة المجانية.\",\n        \"downgradeLabel\": \" تخفيض الخطة\"\n      }\n    },\n    \"cancelSurveyDialog\": {\n      \"title\": \"نحن نأسف لرحيلك\",\n      \"description\": \"نحن نأسف لرحيلك. يسعدنا سماع تعليقاتك لمساعدتنا على تحسين @:appName . يُرجى تخصيص بعض الوقت للإجابة على بعض الأسئلة.\",\n      \"commonOther\": \"آخر\",\n      \"otherHint\": \"اكتب إجابتك هنا\",\n      \"questionOne\": {\n        \"question\": \"ما الذي دفعك إلى إلغاء اشتراكك @:appName Pro؟\",\n        \"answerOne\": \"التكلفة مرتفعة للغاية\",\n        \"answerTwo\": \"الميزات لم تلبي التوقعات\",\n        \"answerThree\": \"وجدت بديلا أفضل\",\n        \"answerFour\": \"لم أستخدمه بشكل كافي لتبرير التكلفة\",\n        \"answerFive\": \"مشكلة في الخدمة أو صعوبات فنية\"\n      },\n      \"questionTwo\": {\n        \"question\": \"ما مدى احتمالية أن تفكر في إعادة الاشتراك في @:appName Pro في المستقبل؟\",\n        \"answerOne\": \"من المرجح جدًا\",\n        \"answerTwo\": \"من المحتمل إلى حد ما\",\n        \"answerThree\": \"غير متأكد\",\n        \"answerFour\": \"من غير المحتمل\",\n        \"answerFive\": \"من غير المحتمل جدًا\"\n      },\n      \"questionThree\": {\n        \"question\": \"ما هي الميزة الاحترافية التي تقدرها أكثر أثناء اشتراكك؟\",\n        \"answerOne\": \"التعاون بين المستخدمين المتعددين\",\n        \"answerTwo\": \"تاريخ الإصدار الأطول\",\n        \"answerThree\": \"استجابات الذكاء الاصطناعي غير المحدودة\",\n        \"answerFour\": \"الوصول إلى نماذج الذكاء الاصطناعي المحلية\"\n      },\n      \"questionFour\": {\n        \"question\": \"كيف تصف تجربتك العامة مع @:appName ؟\",\n        \"answerOne\": \"عظيم\",\n        \"answerTwo\": \"جيد\",\n        \"answerThree\": \"متوسط\",\n        \"answerFour\": \"أقل من المتوسط\",\n        \"answerFive\": \"غير راضٍ\"\n      }\n    },\n    \"common\": {\n      \"uploadingFile\": \"جاري رفع الملف. الرجاء عدم الخروج من التطبيق\",\n      \"uploadNotionSuccess\": \"تم تحميل ملف Notion zip الخاص بك بنجاح. بمجرد اكتمال عملية الاستيراد، ستتلقى رسالة تأكيد عبر البريد الإلكتروني\",\n      \"reset\": \"إعادة ضبط\"\n    },\n    \"menu\": {\n      \"appearance\": \"مظهر\",\n      \"language\": \"لغة\",\n      \"user\": \"مستخدم\",\n      \"files\": \"الملفات\",\n      \"notifications\": \"إشعارات\",\n      \"open\": \"أفتح الإعدادات\",\n      \"logout\": \"تسجيل خروج\",\n      \"logoutPrompt\": \"هل أنت متأكد من تسجيل الخروج؟\",\n      \"selfEncryptionLogoutPrompt\": \"هل أنت متأكد أنك تريد تسجيل الخروج؟ يرجى التأكد من قيامك بنسخ رمز التشفير\",\n      \"syncSetting\": \"إعدادات المزامنة\",\n      \"cloudSettings\": \"إعدادات السحابة\",\n      \"enableSync\": \"تفعيل المزامنة\",\n      \"enableSyncLog\": \"تفعيل تسجيل المزامنة\",\n      \"enableSyncLogWarning\": \"شكرًا لك على مساعدتك في تشخيص مشكلات المزامنة. سيؤدي هذا إلى تسجيل تعديلات المستندات الخاصة بك في ملف محلي. يرجى الخروج من التطبيق وإعادة فتحه بعد تفعيله\",\n      \"enableEncrypt\": \"تشفير البيانات\",\n      \"cloudURL\": \"الرابط الأساسي\",\n      \"webURL\": \"عنوان الويب\",\n      \"invalidCloudURLScheme\": \"مخطط غير صالح\",\n      \"cloudServerType\": \"خادم سحابي\",\n      \"cloudServerTypeTip\": \"يرجى ملاحظة أنه قد يقوم بتسجيل الخروج من حسابك الحالي بعد تبديل الخادم السحابي\",\n      \"cloudLocal\": \"محلي\",\n      \"cloudAppFlowy\": \"سحابة @:appName\",\n      \"cloudAppFlowySelfHost\": \"@:appName استضافة ذاتية على السحابة\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"لا يمكن أن يكون عنوان URL السحابي فارغًا\",\n      \"clickToCopy\": \"انقر للنسخ\",\n      \"selfHostStart\": \"إذا لم يكن لديك خادم، يرجى الرجوع إلى\",\n      \"selfHostContent\": \"مستند\",\n      \"selfHostEnd\": \"للحصول على إرشادات حول كيفية استضافة الخادم الخاص بك ذاتيًا\",\n      \"pleaseInputValidURL\": \"الرجاء إدخال عنوان URL صالح\",\n      \"changeUrl\": \"تغيير عنوان URL المستضاف ذاتيًا إلى {}\",\n      \"cloudURLHint\": \"أدخل الرابط الأساسي لخادمك\",\n      \"webURLHint\": \"أدخل عنوان URL الأساسي لخادم الويب الخاص بك\",\n      \"cloudWSURL\": \"عنوان Websockey\",\n      \"cloudWSURLHint\": \"أدخل عنوان websocket لخادمك\",\n      \"restartApp\": \"إعادة تشغيل\",\n      \"restartAppTip\": \"أعد تشغيل التطبيق لتصبح التغييرات سارية المفعول. يرجى ملاحظة أن هذا قد يؤدي إلى تسجيل الخروج من حسابك الحالي\",\n      \"changeServerTip\": \"بعد تغيير الخادم يجب عليك الضغط على زر إعادة التشغيل حتى تسري التغييرات\",\n      \"enableEncryptPrompt\": \"قم بتنشيط التشفير لتأمين بياناتك بهذا السر. قم بتخزينها بأمان؛ بمجرد تمكينه، لا يمكن إيقاف تشغيله. في حالة فقدانها، تصبح بياناتك غير قابلة للاسترداد. انقر للنسخ\",\n      \"inputEncryptPrompt\": \"الرجاء إدخال سر التشفير الخاص بك ل\",\n      \"clickToCopySecret\": \"انقر لنسخ السر\",\n      \"configServerSetting\": \"قم بتكوين إعدادات الخادم الخاص بك\",\n      \"configServerGuide\": \"بعد تحديد \\\"البدء السريع\\\"، انتقل إلى \\\"الإعدادات\\\" ثم \\\"إعدادات السحابة\\\" لتهيئة خادمك المستضاف ذاتيًا.\",\n      \"inputTextFieldHint\": \"السر الخاصة بك\",\n      \"historicalUserList\": \"سجل تسجيل دخول المستخدم\",\n      \"historicalUserListTooltip\": \"تعرض هذه القائمة حساباتك المجهولة. يمكنك النقر على الحساب لعرض تفاصيله. يتم إنشاء الحسابات المجهولة بالنقر فوق الزر \\\"البدء\\\".\",\n      \"openHistoricalUser\": \"انقر لفتح الحساب الخفي\",\n      \"customPathPrompt\": \"قد يؤدي تخزين مجلد بيانات @:appName في مجلد متزامن على السحابة مثل Google Drive إلى مخاطر. إذا تم الوصول إلى قاعدة البيانات الموجودة في هذا المجلد أو تعديلها من مواقع متعددة في نفس الوقت، فقد يؤدي ذلك إلى حدوث تعارضات في المزامنة وتلف محتمل للبيانات\",\n      \"importAppFlowyData\": \"استيراد البيانات من مجلد خارجي @:appName\",\n      \"importingAppFlowyDataTip\": \"جاري استيراد البيانات. يرجى عدم إغلاق التطبيق\",\n      \"importAppFlowyDataDescription\": \"انسخ البيانات من مجلد بيانات خارجي @:appName واستوردها إلى مجلد بيانات AppFlowy الحالي\",\n      \"importSuccess\": \"تم استيراد مجلد البيانات @:appName بنجاح\",\n      \"importFailed\": \"فشل استيراد مجلد البيانات @:appName\",\n      \"importGuide\": \"لمزيد من التفاصيل، يرجى مراجعة الوثيقة المشار إليها\",\n      \"cloudSetting\": \"إعداد السحابة\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"تمكين الإشعارات\",\n        \"hint\": \"قم بإيقاف تشغيله لمنع ظهور الإشعارات المحلية.\"\n      },\n      \"showNotificationsIcon\": {\n        \"label\": \"إظهار أيقونة الإشعارات\",\n        \"hint\": \"قم بإيقاف تشغيله لإخفاء أيقونة الإشعار في الشريط الجانبي.\"\n      },\n      \"archiveNotifications\": {\n        \"allSuccess\": \"تم أرشفة جميع الإشعارات بنجاح\",\n        \"success\": \"تم أرشفة الإشعار بنجاح\"\n      },\n      \"markAsReadNotifications\": {\n        \"allSuccess\": \"تم وضع علامة على كل شيء كمقروء بنجاح\",\n        \"success\": \"تم وضع علامة على القراءة بنجاح\"\n      },\n      \"action\": {\n        \"markAsRead\": \"وضع علامة كمقروءة\",\n        \"multipleChoice\": \"إختر المزيد\",\n        \"archive\": \"أرشيف\"\n      },\n      \"settings\": {\n        \"settings\": \"إعدادات\",\n        \"markAllAsRead\": \"وضع علامة على الكل كمقروء\",\n        \"archiveAll\": \"أرشفة الكل\"\n      },\n      \"emptyInbox\": {\n        \"title\": \"صندوق الوارد صفر!\",\n        \"description\": \"قم بتعيين التذكيرات لتلقي الإشعارات هنا.\"\n      },\n      \"emptyUnread\": {\n        \"title\": \"لا توجد إشعارات غير مقروءة\",\n        \"description\": \"لقد تم قراءة جميع الرسائل.\"\n      },\n      \"emptyArchived\": {\n        \"title\": \"غير مؤرشفة\",\n        \"description\": \"ستظهر الإشعارات المؤرشفة هنا.\"\n      },\n      \"tabs\": {\n        \"inbox\": \"صندوق الوارد\",\n        \"unread\": \"غير مقروء\",\n        \"archived\": \"مؤرشفة\"\n      },\n      \"refreshSuccess\": \"تم تحديث الإشعارات بنجاح\",\n      \"titles\": {\n        \"notifications\": \"إشعارات\",\n        \"reminder\": \"تذكير\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"إعادة ضبط هذا الإعداد\",\n      \"fontFamily\": {\n        \"label\": \"الخط\",\n        \"search\": \"يبحث\",\n        \"defaultFont\": \"نظام\"\n      },\n      \"themeMode\": {\n        \"label\": \"مظهر السمة\",\n        \"light\": \" المظهر الفاتح\",\n        \"dark\": \"المظهر الداكن\",\n        \"system\": \"التكيف مع النظام\"\n      },\n      \"fontScaleFactor\": \"عامل مقياس الخط\",\n      \"displaySize\": \"حجم العرض\",\n      \"documentSettings\": {\n        \"cursorColor\": \"لون مؤشر المستند\",\n        \"selectionColor\": \"لون اختيار المستند\",\n        \"width\": \"عرض المستند\",\n        \"changeWidth\": \"تغير\",\n        \"pickColor\": \"حدد اللون\",\n        \"colorShade\": \"ظل اللون\",\n        \"opacity\": \"الشفافية\",\n        \"hexEmptyError\": \"لا يمكن أن يكون اللون السادسة عشرية فارغًا\",\n        \"hexLengthError\": \"يجب أن تكون القيمة السداسية عشرية مكونة من 6 أرقام\",\n        \"hexInvalidError\": \"القيمة السادسة العشرية غير صالحة\",\n        \"opacityEmptyError\": \"لا يمكن أن تكون الشفافية فارغة\",\n        \"opacityRangeError\": \"يجب أن تكون الشفافية بين 1 و 100\",\n        \"app\": \"App\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"تطبيق\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"اتجاه التخطيط\",\n        \"hint\": \"التحكم في تدفق المحتوى على شاشتك، من اليسار إلى اليمين أو من اليمين إلى اليسار.\",\n        \"ltr\": \"من اليسار الى اليمين\",\n        \"rtl\": \"من اليمين الى اليسار\"\n      },\n      \"textDirection\": {\n        \"label\": \"اتجاه النص الافتراضي\",\n        \"hint\": \"حدد ما إذا كان النص يجب أن يبدأ من اليسار أو اليمين كإعداد افتراضي.\",\n        \"ltr\": \"من اليسار الى اليمين\",\n        \"rtl\": \"من اليمين الى اليسار\",\n        \"auto\": \"آلي\",\n        \"fallback\": \"نفس اتجاه التخطيط\"\n      },\n      \"themeUpload\": {\n        \"button\": \"رفع\",\n        \"uploadTheme\": \"رفع السمة\",\n        \"description\": \"قم برفع السمة @:appName الخاص بك باستخدام الزر أدناه.\",\n        \"loading\": \"يرجى الانتظار بينما نقوم بالتحقق من صحة السمة الخاصة بك وترفعها ...\",\n        \"uploadSuccess\": \"تم رفع سمتك بنجاح\",\n        \"deletionFailure\": \"فشل حذف الموضوع. حاول حذفه يدويًا.\",\n        \"filePickerDialogTitle\": \"اختر ملف .flowy_plugin\",\n        \"urlUploadFailure\": \"فشل فتح عنوان url: {}\",\n        \"failure\": \"النسق الذي تم تحميله به تنسيق غير صالح.\"\n      },\n      \"theme\": \"سمة\",\n      \"builtInsLabel\": \"ثيمات مدمجة\",\n      \"pluginsLabel\": \"الإضافات\",\n      \"dateFormat\": {\n        \"label\": \"صيغة التاريخ\",\n        \"local\": \"محلي\",\n        \"us\": \"الولايات المتحدة\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"سهل الاستخدام\",\n        \"dmy\": \"ي/ش/س\"\n      },\n      \"timeFormat\": {\n        \"label\": \"تنسيق الوقت\",\n        \"twelveHour\": \" نظام اثنتي عشرة ساعة\",\n        \"twentyFourHour\": \"نظام أربع وعشرون ساعة\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"إظهار مربع حوار التسمية عند إنشاء الصفحة\",\n      \"enableRTLToolbarItems\": \"تمكين عناصر شريط أدوات RTL\",\n      \"members\": {\n        \"title\": \"إعدادات الأعضاء\",\n        \"inviteMembers\": \"دعوة الأعضاء\",\n        \"inviteHint\": \"دعوة عبر البريد الإلكتروني\",\n        \"sendInvite\": \"إرسال دعوة\",\n        \"copyInviteLink\": \"نسخ رابط الدعوة\",\n        \"label\": \"الأعضاء\",\n        \"user\": \"المستخدم\",\n        \"role\": \"الدور\",\n        \"removeFromWorkspace\": \"الإزالة من مساحة العمل\",\n        \"removeFromWorkspaceSuccess\": \"تمت الإزالة من مساحة العمل بنجاح\",\n        \"removeFromWorkspaceFailed\": \"فشلت عملية الإزالة من مساحة العمل\",\n        \"owner\": \"المالك\",\n        \"guest\": \"الضيف\",\n        \"member\": \"العضو\",\n        \"memberHintText\": \"يمكن للعضو قراءة الصفحات وتحريرها\",\n        \"guestHintText\": \"يمكن للضيف القراءة والرد والتعليق وتحرير صفحات معينة بإذن.\",\n        \"emailInvalidError\": \"بريد إلكتروني غير صالح، يرجى التحقق والمحاولة مرة أخرى\",\n        \"emailSent\": \"تم إرسال البريد الإلكتروني، يرجى التحقق من صندوق الوارد\",\n        \"members\": \"الأعضاء\",\n        \"membersCount\": {\n          \"zero\": \"{} الأعضاء\",\n          \"one\": \"{} العضو\",\n          \"other\": \"{} الأعضاء\"\n        },\n        \"inviteFailedDialogTitle\": \"فشل في إرسال الدعوة\",\n        \"inviteFailedMemberLimit\": \"لقد تم الوصول إلى الحد الأقصى للأعضاء، يرجى الترقية لدعوة المزيد من الأعضاء.\",\n        \"inviteFailedMemberLimitMobile\": \"لقد وصلت مساحة العمل الخاصة بك إلى الحد الأقصى للأعضاء.\",\n        \"memberLimitExceeded\": \"تم الوصول إلى الحد الأقصى للأعضاء، لدعوة المزيد من الأعضاء، يرجى \",\n        \"memberLimitExceededUpgrade\": \"ترقية\",\n        \"memberLimitExceededPro\": \"تم الوصول إلى الحد الأقصى للأعضاء، إذا كنت بحاجة إلى المزيد من الأعضاء، فاتصل بنا \",\n        \"memberLimitExceededProContact\": \"support@appflowy.io\",\n        \"failedToAddMember\": \"فشل في إضافة العضو\",\n        \"addMemberSuccess\": \"تمت إضافة العضو بنجاح\",\n        \"removeMember\": \"إزالة العضو\",\n        \"areYouSureToRemoveMember\": \"هل أنت متأكد أنك تريد إزالة هذا العضو؟\",\n        \"inviteMemberSuccess\": \"لقد تم ارسال الدعوة بنجاح\",\n        \"failedToInviteMember\": \"فشل في دعوة العضو\",\n        \"workspaceMembersError\": \"عفواً، حدث خطأ ما\",\n        \"workspaceMembersErrorDescription\": \"لم نتمكن من تحميل قائمة الأعضاء في هذا الوقت. يرجى المحاولة مرة أخرى لاحقًا\",\n        \"inviteLinkToAddMember\": \"رابط الدعوة لإضافة عضو\",\n        \"clickToCopyLink\": \"انقر هنا لنسخ الرابط\",\n        \"or\": \"أو\",\n        \"generateANewLink\": \"إنشاء رابط جديد\",\n        \"inviteMemberByEmail\": \"دعوة الأعضاء عبر البريد الإلكتروني\",\n        \"inviteMemberHintText\": \"دعوة عبر البريد الإلكتروني\",\n        \"resetInviteLink\": \"إعادة ضبط رابط الدعوة؟\",\n        \"resetInviteLinkDescription\": \"ستؤدي إعادة الضبط إلى إلغاء تفعيل الرابط الحالي لجميع أعضاء المساحة وإنشاء رابط جديد. لن يعود الرابط القديم صالحًا.\",\n        \"adminPanel\": \"لوحة الإدارة\",\n        \"reset\": \"إعادة ضبط\",\n        \"resetInviteLinkSuccess\": \"تمت إعادة ضبط رابط الدعوة بنجاح\",\n        \"resetInviteLinkFailed\": \"فشل إعادة ضبط رابط الدعوة\",\n        \"resetInviteLinkFailedDescription\": \"الرجاء المحاولة مرة أخرى لاحقًا\",\n        \"memberPageDescription1\": \"الوصول إلى\",\n        \"memberPageDescription2\": \"لإدارة الضيوف والمستخدمين المتقدمين.\",\n        \"noInviteLink\": \"لم تقم بإنشاء رابط دعوة بعد.\",\n        \"copyLink\": \"انسخ الرابط\",\n        \"generatedLinkSuccessfully\": \"تم إنشاء الرابط بنجاح\",\n        \"generatedLinkFailed\": \"فشل في إنشاء الرابط\",\n        \"resetLinkSuccessfully\": \"تم إعادة ضبط الرابط بنجاح\",\n        \"resetLinkFailed\": \"فشل إعادة ضبط الرابط\"\n      },\n      \"lightLabel\": \"فاتح\",\n      \"darkLabel\": \"غامق\"\n    },\n    \"files\": {\n      \"copy\": \"انسخ\",\n      \"defaultLocation\": \"أين يتم تخزين بياناتك الآن\",\n      \"exportData\": \"قم بتصدير بياناتك\",\n      \"doubleTapToCopy\": \"انقر نقرًا مزدوجًا لنسخ المسار\",\n      \"restoreLocation\": \"استعادة المسار الافتراضي @:appName\",\n      \"customizeLocation\": \"افتح مجلدًا آخر\",\n      \"restartApp\": \"يرجى إعادة تشغيل التطبيق لتصبح التغييرات سارية المفعول.\",\n      \"exportDatabase\": \"تصدير قاعدة البيانات\",\n      \"selectFiles\": \"حدد الملفات التي تريد تصديرها\",\n      \"selectAll\": \"اختر الكل\",\n      \"deselectAll\": \"الغاء تحديد الكل\",\n      \"createNewFolder\": \"انشاء مجلد جديد\",\n      \"createNewFolderDesc\": \"أخبرنا بالمكان الذي تريد تخزين بياناتك فيه\",\n      \"defineWhereYourDataIsStored\": \"حدد مكان تخزين بياناتك\",\n      \"open\": \"يفتح\",\n      \"openFolder\": \"افتح مجلدًا موجودًا\",\n      \"openFolderDesc\": \"اقرأها واكتبها في مجلد @:appName الموجود لديك\",\n      \"folderHintText\": \"إسم الملف\",\n      \"location\": \"إنشاء مجلد جديد\",\n      \"locationDesc\": \"اختر اسمًا لمجلد بيانات @:appName\",\n      \"browser\": \"تصفح\",\n      \"create\": \"يخلق\",\n      \"set\": \"تعيين\",\n      \"folderPath\": \"مسار لتخزين المجلد الخاص بك\",\n      \"locationCannotBeEmpty\": \"لا يمكن أن يكون المسار فارغًا\",\n      \"pathCopiedSnackbar\": \"تم نسخ مسار تخزين الملفات إلى الحافظة!\",\n      \"changeLocationTooltips\": \"قم بتغيير دليل البيانات\",\n      \"change\": \"يتغير\",\n      \"openLocationTooltips\": \"افتح دليل بيانات آخر\",\n      \"openCurrentDataFolder\": \"افتح دليل البيانات الحالي\",\n      \"recoverLocationTooltips\": \"إعادة التعيين إلى دليل البيانات الافتراضي لـ @:appName\",\n      \"exportFileSuccess\": \"تم تصدير الملف بنجاح!\",\n      \"exportFileFail\": \"فشل تصدير الملف!\",\n      \"export\": \"يصدّر\",\n      \"clearCache\": \"مسح ذاكرة التخزين المؤقت\",\n      \"clearCacheDesc\": \"إذا واجهت مشكلات تتعلق بعدم تحميل الصور أو عدم عرض الخطوط بشكل صحيح، فحاول مسح ذاكرة التخزين المؤقت. لن يؤدي هذا الإجراء إلى إزالة بيانات المستخدم الخاصة بك.\",\n      \"areYouSureToClearCache\": \"هل أنت متأكد من مسح ذاكرة التخزين المؤقت؟\",\n      \"clearCacheSuccess\": \"تم مسح ذاكرة التخزين المؤقت بنجاح!\"\n    },\n    \"user\": {\n      \"name\": \"اسم\",\n      \"email\": \"بريد إلكتروني\",\n      \"tooltipSelectIcon\": \"حدد أيقونة\",\n      \"selectAnIcon\": \"حدد أيقونة\",\n      \"pleaseInputYourOpenAIKey\": \"الرجاء إدخال مفتاح AI الخاص بك\",\n      \"clickToLogout\": \"انقر لتسجيل خروج المستخدم الحالي\",\n      \"pleaseInputYourStabilityAIKey\": \"يرجى إدخال رمز Stability AI الخاص بك\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"معلومات شخصية\",\n      \"username\": \"اسم المستخدم\",\n      \"usernameEmptyError\": \"لا يمكن أن يكون اسم المستخدم فارغا\",\n      \"about\": \"حول\",\n      \"pushNotifications\": \"اشعارات لحظية\",\n      \"support\": \"دعم\",\n      \"joinDiscord\": \"انضم إلينا على ديسكورد\",\n      \"privacyPolicy\": \"سياسة الخصوصية\",\n      \"userAgreement\": \"اتفاقية المستخدم\",\n      \"termsAndConditions\": \"الشروط والأحكام\",\n      \"userprofileError\": \"فشل تحميل ملف تعريف المستخدم\",\n      \"userprofileErrorDescription\": \"يرجى محاولة تسجيل الخروج وتسجيل الدخول مرة أخرى للتحقق مما إذا كانت المشكلة لا تزال قائمة.\",\n      \"selectLayout\": \"حدد الشكل\",\n      \"selectStartingDay\": \"اختر يوم البدء\",\n      \"version\": \"النسخة\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"الاختصارات\",\n      \"command\": \"امر\",\n      \"keyBinding\": \"ربط المفاتيح\",\n      \"addNewCommand\": \"إضافة أمر جديد\",\n      \"updateShortcutStep\": \"اضغط على مجموعة المفاتيح المطلوبة ثم اضغط على ENTER\",\n      \"shortcutIsAlreadyUsed\": \"هذا الاختصار مستخدم بالفعل لـ: {conflict}\",\n      \"resetToDefault\": \"إعادة التعيين إلى روابط المفاتيح الافتراضية\",\n      \"couldNotLoadErrorMsg\": \"تعذر تحميل الاختصارات، حاول مرة أخرى\",\n      \"couldNotSaveErrorMsg\": \"تعذر حفظ الاختصارات، حاول مرة أخرى\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"هل أنت متأكد من حذف هذه الواجهة؟\",\n    \"createView\": \"جديد\",\n    \"title\": {\n      \"placeholder\": \"بدون عنوان\"\n    },\n    \"settings\": {\n      \"filter\": \"تنقية\",\n      \"sort\": \"ترتيب\",\n      \"sortBy\": \"ترتيب حسب\",\n      \"properties\": \"ملكيات\",\n      \"reorderPropertiesTooltip\": \"اسحب لإعادة ترتيب الخصائص\",\n      \"group\": \"مجموعة\",\n      \"addFilter\": \"أضف عامل تصفية\",\n      \"deleteFilter\": \"حذف عامل التصفية\",\n      \"filterBy\": \"مصنف بواسطة...\",\n      \"typeAValue\": \"اكتب قيمة ...\",\n      \"layout\": \"تَخطِيط\",\n      \"compactMode\": \"الوضع المضغوط\",\n      \"databaseLayout\": \"تَخطِيط\",\n      \"viewList\": {\n        \"zero\": \"0 مشاهدات\",\n        \"one\": \"{count} مشاهدة\",\n        \"other\": \"{count} المشاهدات\"\n      },\n      \"editView\": \"تعديل العرض\",\n      \"boardSettings\": \"إعدادات اللوحة\",\n      \"calendarSettings\": \"إعدادات التقويم\",\n      \"createView\": \"عرض جديد\",\n      \"duplicateView\": \"عرض مكرر\",\n      \"deleteView\": \"حذف العرض\",\n      \"numberOfVisibleFields\": \"{} تم عرضه\",\n      \"Properties\": \"ملكيات\"\n    },\n    \"filter\": {\n      \"empty\": \"لا توجد عوامل تصفية نشطة\",\n      \"addFilter\": \"أضف عامل التصفية\",\n      \"cannotFindCreatableField\": \"لا يمكن العثور على حقل مناسب للتصفية حسبه\",\n      \"conditon\": \"حالة\",\n      \"where\": \"أين\"\n    },\n    \"textFilter\": {\n      \"contains\": \"يتضمن\",\n      \"doesNotContain\": \"لا يحتوي\",\n      \"endsWith\": \"ينتهي بـ\",\n      \"startWith\": \"ابدا ب\",\n      \"is\": \"يكون\",\n      \"isNot\": \"ليس\",\n      \"isEmpty\": \"فارغ\",\n      \"isNotEmpty\": \"ليس فارغا\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"لا\",\n        \"startWith\": \"ابدا ب\",\n        \"endWith\": \"ينتهي بـ\",\n        \"isEmpty\": \"فارغ\",\n        \"isNotEmpty\": \"ليس فارغا\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"التحقق\",\n      \"isUnchecked\": \"لم يتم التحقق منه\",\n      \"choicechipPrefix\": {\n        \"is\": \"يكون\",\n        \"da\": \"بادئة خانة الاختيار\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"كاملة\",\n      \"isIncomplted\": \"غير مكتمل\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"يكون\",\n      \"isNot\": \"ليس\",\n      \"contains\": \"يتضمن\",\n      \"doesNotContain\": \"لا يحتوي\",\n      \"isEmpty\": \"فارغ\",\n      \"isNotEmpty\": \"ليس فارغا\"\n    },\n    \"dateFilter\": {\n      \"is\": \"يكون\",\n      \"before\": \"يكون قبل\",\n      \"after\": \"يكون بعد\",\n      \"onOrBefore\": \"يكون في او قبل\",\n      \"onOrAfter\": \"يكون في او بعد\",\n      \"between\": \"يتراوح ما بين\",\n      \"empty\": \"فارغ\",\n      \"notEmpty\": \"ليس فارغا\",\n      \"startDate\": \"تاريخ البدء\",\n      \"endDate\": \"تاريخ النهاية\",\n      \"choicechipPrefix\": {\n        \"before\": \"قبل\",\n        \"after\": \"بعد\",\n        \"between\": \"بين\",\n        \"onOrBefore\": \"في أو قبل\",\n        \"onOrAfter\": \"في أو بعد\",\n        \"isEmpty\": \"هو فارغ\",\n        \"isNotEmpty\": \"ليس فارغا\"\n      }\n    },\n    \"numberFilter\": {\n      \"equal\": \"يساوي\",\n      \"notEqual\": \"لا يساوي\",\n      \"lessThan\": \"أقل من\",\n      \"greaterThan\": \"أكبر من\",\n      \"lessThanOrEqualTo\": \"أقل من أو يساوي\",\n      \"greaterThanOrEqualTo\": \"أكبر من أو يساوي\",\n      \"isEmpty\": \"هو فارغ\",\n      \"isNotEmpty\": \"ليس فارغا\"\n    },\n    \"field\": {\n      \"label\": \"خاصية\",\n      \"hide\": \"يخفي\",\n      \"show\": \"عرض\",\n      \"insertLeft\": \"أدخل اليسار\",\n      \"insertRight\": \"أدخل اليمين\",\n      \"duplicate\": \"ينسخ\",\n      \"delete\": \"يمسح\",\n      \"wrapCellContent\": \"لف النص\",\n      \"clear\": \"مسح الخلايا\",\n      \"switchPrimaryFieldTooltip\": \"لا يمكن تغيير نوع الحقل للحقل الأساسي\",\n      \"textFieldName\": \"نص\",\n      \"checkboxFieldName\": \"خانة اختيار\",\n      \"dateFieldName\": \"تاريخ\",\n      \"updatedAtFieldName\": \"وقت آخر تعديل\",\n      \"createdAtFieldName\": \"وقت الإنشاء\",\n      \"numberFieldName\": \"أعداد\",\n      \"singleSelectFieldName\": \"يختار\",\n      \"multiSelectFieldName\": \"تحديد متعدد\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"قائمة تدقيق\",\n      \"relationFieldName\": \"العلاقة\",\n      \"summaryFieldName\": \"ملخص الذكاء الاصطناعي\",\n      \"timeFieldName\": \"وقت\",\n      \"mediaFieldName\": \"الملفات والوسائط\",\n      \"translateFieldName\": \"الترجمة بالذكاء الاصطناعي\",\n      \"translateTo\": \"ترجم إلى\",\n      \"numberFormat\": \"تنسيق الأرقام\",\n      \"dateFormat\": \"صيغة التاريخ\",\n      \"includeTime\": \"أضف الوقت\",\n      \"isRange\": \"تاريخ الانتهاء\",\n      \"dateFormatFriendly\": \"شهر يوم سنه\",\n      \"dateFormatISO\": \"سنة شهر يوم\",\n      \"dateFormatLocal\": \"شهر يوم سنه\",\n      \"dateFormatUS\": \"سنة شهر يوم\",\n      \"dateFormatDayMonthYear\": \"يوم شهر سنة\",\n      \"timeFormat\": \"تنسيق الوقت\",\n      \"invalidTimeFormat\": \"تنسيق غير صالح\",\n      \"timeFormatTwelveHour\": \"12 ساعة\",\n      \"timeFormatTwentyFourHour\": \"24 ساعة\",\n      \"clearDate\": \"مسح التاريخ\",\n      \"dateTime\": \"الوقت و التاريخ\",\n      \"startDateTime\": \"وقت تاريخ البدء\",\n      \"endDateTime\": \"وقت تاريخ الانتهاء\",\n      \"failedToLoadDate\": \"فشل تحميل قيمة التاريخ\",\n      \"selectTime\": \"حدد الوقت\",\n      \"selectDate\": \"حدد تاريخ\",\n      \"visibility\": \"الظهور\",\n      \"propertyType\": \"نوع الملكية\",\n      \"addSelectOption\": \"أضف خيارًا\",\n      \"typeANewOption\": \"اكتب خيارًا جديدًا\",\n      \"optionTitle\": \"خيارات\",\n      \"addOption\": \"إضافة خيار\",\n      \"editProperty\": \"تحرير الملكية\",\n      \"newProperty\": \"خاصية جديدة\",\n      \"openRowDocument\": \"افتح كصفحة\",\n      \"deleteFieldPromptMessage\": \"هل أنت متأكد؟ سيتم حذف هذه الخاصية\",\n      \"clearFieldPromptMessage\": \"هل أنت متأكد؟ سيتم إفراغ جميع الخلايا في هذا العمود\",\n      \"newColumn\": \"عمود جديد\",\n      \"format\": \"شكل\",\n      \"reminderOnDateTooltip\": \"تحتوي هذه الخلية على تذكير مجدول\",\n      \"optionAlreadyExist\": \"الخيار موجود بالفعل\"\n    },\n    \"rowPage\": {\n      \"newField\": \"إضافة حقل جديد\",\n      \"fieldDragElementTooltip\": \"انقر لفتح القائمة\",\n      \"showHiddenFields\": {\n        \"one\": \"إظهار {count} حقل\",\n        \"many\": \"إظهار {count} الحقول المخفية\",\n        \"other\": \"إظهار {count} الحقول المخفية\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"إخفاء {count} الحقل المخفي\",\n        \"many\": \"إخفاء {count} الحقول المخفية\",\n        \"other\": \"إخفاء {count} الحقول المخفية\"\n      },\n      \"openAsFullPage\": \"افتح كصفحة كاملة\",\n      \"viewDatabase\": \"عرض قاعدة البيانات الأصلية\",\n      \"moreRowActions\": \"مزيد من إجراءات الصف\"\n    },\n    \"sort\": {\n      \"ascending\": \"تصاعدي\",\n      \"descending\": \"تنازلي\",\n      \"by\": \"بواسطة\",\n      \"empty\": \"لا توجد تصنيفات نشطة\",\n      \"cannotFindCreatableField\": \"لا يمكن العثور على حقل مناسب للتصنيف حسبه\",\n      \"deleteAllSorts\": \"حذف جميع التراتيب\",\n      \"addSort\": \"أضف نوعًا\",\n      \"sortsActive\": \"لا يمكن {intention} أثناء التصنيف\",\n      \"removeSorting\": \"هل ترغب في إزالة كافة التصنيفات في هذا العرض والمتابعة؟\",\n      \"fieldInUse\": \"أنت تقوم بالفعل بالتصنيف حسب هذا الحقل\",\n      \"deleteSort\": \"حذف الفرز\"\n    },\n    \"row\": {\n      \"label\": \"صف\",\n      \"duplicate\": \"مكرره\",\n      \"delete\": \"يمسح\",\n      \"titlePlaceholder\": \"بدون عنوان\",\n      \"textPlaceholder\": \"فارغ\",\n      \"copyProperty\": \"نسخ الممتلكات إلى الحافظة\",\n      \"count\": \"عدد\",\n      \"newRow\": \"صف جديد\",\n      \"loadMore\": \"تحميل المزيد\",\n      \"action\": \"فعل\",\n      \"add\": \"انقر فوق إضافة إلى أدناه\",\n      \"drag\": \"اسحب للتحريك\",\n      \"deleteRowPrompt\": \"هل أنت متأكد من أنك تريد حذف هذا الصف؟ لا يمكن التراجع عن هذا الإجراء.\",\n      \"deleteCardPrompt\": \"هل أنت متأكد من أنك تريد حذف هذه البطاقة؟ لا يمكن التراجع عن هذا الإجراء.\",\n      \"dragAndClick\": \"اسحب للتحريك، انقر لفتح القائمة\",\n      \"insertRecordAbove\": \"أدخل السجل أعلاه\",\n      \"insertRecordBelow\": \"أدخل السجل أدناه\",\n      \"noContent\": \"لا يوجد محتوى\",\n      \"reorderRowDescription\": \"إعادة ترتيب الصف\",\n      \"createRowAboveDescription\": \"إنشاء صف أعلى\",\n      \"createRowBelowDescription\": \"أدخل صفًا أدناه\"\n    },\n    \"selectOption\": {\n      \"create\": \"يخلق\",\n      \"purpleColor\": \"أرجواني\",\n      \"pinkColor\": \"لون القرنفل\",\n      \"lightPinkColor\": \"وردي فاتح\",\n      \"orangeColor\": \"البرتقالي\",\n      \"yellowColor\": \"أصفر\",\n      \"limeColor\": \"جير\",\n      \"greenColor\": \"أخضر\",\n      \"aquaColor\": \"أكوا\",\n      \"blueColor\": \"أزرق\",\n      \"deleteTag\": \"حذف العلامة\",\n      \"colorPanelTitle\": \"الألوان\",\n      \"panelTitle\": \"حدد خيارًا أو أنشئ خيارًا\",\n      \"searchOption\": \"ابحث عن خيار\",\n      \"searchOrCreateOption\": \"بحث أو إنشاء خيار...\",\n      \"createNew\": \"إنشاء جديد\",\n      \"orSelectOne\": \"أو حدد خيارًا\",\n      \"typeANewOption\": \"اكتب خيارًا جديدًا\",\n      \"tagName\": \"اسم العلامة\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"وصف المهمة\",\n      \"addNew\": \"أضف عنصرًا\",\n      \"submitNewTask\": \"انشئ\",\n      \"hideComplete\": \"إخفاء المهام المكتملة\",\n      \"showComplete\": \"إظهار كافة المهام\"\n    },\n    \"url\": {\n      \"launch\": \"فتح في المتصفح\",\n      \"copy\": \"إنسخ الرابط\",\n      \"textFieldHint\": \"أدخل عنوان URL\"\n    },\n    \"relation\": {\n      \"relatedDatabasePlaceLabel\": \"قاعدة البيانات ذات الصلة\",\n      \"relatedDatabasePlaceholder\": \"لا أحد\",\n      \"inRelatedDatabase\": \"في\",\n      \"rowSearchTextFieldPlaceholder\": \"بحث\",\n      \"noDatabaseSelected\": \"لم يتم تحديد قاعدة بيانات، الرجاء تحديد قاعدة بيانات واحدة أولاً من القائمة أدناه:\",\n      \"emptySearchResult\": \"لم يتم العثور على أي سجلات\",\n      \"linkedRowListLabel\": \"{count} صفوف مرتبطة\",\n      \"unlinkedRowListLabel\": \"ربط صف آخر\"\n    },\n    \"menuName\": \"شبكة\",\n    \"referencedGridPrefix\": \"نظرا ل\",\n    \"calculate\": \"احسب\",\n    \"calculationTypeLabel\": {\n      \"none\": \"لا أحد\",\n      \"average\": \"المعدل\",\n      \"max\": \"الحد الأقصى\",\n      \"median\": \"المتوسط\",\n      \"min\": \"الحد الأدنى\",\n      \"sum\": \"المجموع\",\n      \"count\": \"العدد\",\n      \"countEmpty\": \"عدد فارغ\",\n      \"countEmptyShort\": \"فارغ\",\n      \"countNonEmpty\": \"العدد ليس فارغا\",\n      \"countNonEmptyShort\": \"مملوء\"\n    },\n    \"media\": {\n      \"rename\": \"إعادة تسمية\",\n      \"download\": \"التنزيل\",\n      \"expand\": \"توسيع\",\n      \"delete\": \"الحذف\",\n      \"moreFilesHint\": \"+{}\",\n      \"addFileOrImage\": \"إضافة ملف أو رابط\",\n      \"attachmentsHint\": \"{}\",\n      \"addFileMobile\": \"إضافة ملف\",\n      \"extraCount\": \"+{}\",\n      \"deleteFileDescription\": \"هل أنت متأكد من أنك تريد حذف هذا الملف؟ هذا الإجراء لا رجعة فيه.\",\n      \"showFileNames\": \"إظهار اسم الملف\",\n      \"downloadSuccess\": \"تم تنزيل الملف\",\n      \"downloadFailedToken\": \"فشل تنزيل الملف، الرمز المميز للمستخدم غير متوفر\",\n      \"setAsCover\": \"تعيين كغلاف\",\n      \"openInBrowser\": \"فتح في المتصفح\",\n      \"embedLink\": \"تضمين رابط الملف\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"وثيقة\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 مساءً\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"creating\": \"جارٍ الإنشاء...\",\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"حدد لوحة للارتباط بها\",\n        \"createANewBoard\": \"قم بإنشاء لوحة جديدة\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"حدد الشبكة للارتباط بها\",\n        \"createANewGrid\": \"قم بإنشاء شبكة جديدة\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"حدد تقويمًا للارتباط به\",\n        \"createANewCalendar\": \"قم بإنشاء تقويم جديد\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"حدد مستندًا للارتباط به\"\n      },\n      \"name\": {\n        \"textStyle\": \"نمط النص\",\n        \"list\": \"قائمة\",\n        \"toggle\": \"تبديل\",\n        \"fileAndMedia\": \"الملفات والوسائط\",\n        \"simpleTable\": \"جدول بسيط\",\n        \"visuals\": \"المرئيات\",\n        \"document\": \"وثيقة\",\n        \"advanced\": \"متقدم\",\n        \"text\": \"نص\",\n        \"heading1\": \"العنوان 1\",\n        \"heading2\": \"العنوان 2\",\n        \"heading3\": \"العنوان 3\",\n        \"image\": \"صورة\",\n        \"bulletedList\": \"قائمة نقطية\",\n        \"numberedList\": \"قائمة مرقمة\",\n        \"todoList\": \"قائمة المهام\",\n        \"doc\": \"وثيقة\",\n        \"linkedDoc\": \"رابط الصفحة\",\n        \"grid\": \"شبكة\",\n        \"linkedGrid\": \"الشبكة المرتبطة\",\n        \"kanban\": \"كانبان\",\n        \"linkedKanban\": \"كانبان مرتبط\",\n        \"calendar\": \"تقويم\",\n        \"linkedCalendar\": \"التقويم المرتبط\",\n        \"quote\": \"اقتباس\",\n        \"divider\": \"فاصل\",\n        \"table\": \"الجدول\",\n        \"callout\": \"وسيلة الشرح\",\n        \"outline\": \"مخطط تفصيلي\",\n        \"mathEquation\": \"معادلة الرياضيات\",\n        \"code\": \"الكود\",\n        \"toggleList\": \"قائمة التبديل\",\n        \"toggleHeading1\": \"تبديل العنوان 1\",\n        \"toggleHeading2\": \"تبديل العنوان 2\",\n        \"toggleHeading3\": \"تبديل العنوان 3\",\n        \"emoji\": \"الرموز التعبيرية\",\n        \"aiWriter\": \"كاتب الذكاء الاصطناعي\",\n        \"dateOrReminder\": \"التاريخ أو التذكير\",\n        \"photoGallery\": \"معرض الصور\",\n        \"file\": \"الملف\",\n        \"twoColumns\": \"عمودين\",\n        \"threeColumns\": \"3 أعمدة\",\n        \"fourColumns\": \"4 أعمدة\"\n      },\n      \"subPage\": {\n        \"name\": \"المستند\",\n        \"keyword1\": \"الصفحة الفرعية\",\n        \"keyword2\": \"الصفحة\",\n        \"keyword3\": \"الصفحة الطفل\",\n        \"keyword4\": \"ادراج الصفحة\",\n        \"keyword5\": \"تضمين الصفحة\",\n        \"keyword6\": \"صفحة جديدة\",\n        \"keyword7\": \"إنشاء صفحة\",\n        \"keyword8\": \"المستند\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"الخطوط العريضة\",\n      \"codeBlock\": \"كتلة الكود\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"المجلس المشار إليه\",\n      \"referencedGrid\": \"الشبكة المشار إليها\",\n      \"referencedCalendar\": \"التقويم المشار إليه\",\n      \"referencedDocument\": \"الوثيقة المشار إليها\",\n      \"aiWriter\": {\n        \"userQuestion\": \"اسأل الذكاء الاصطناعي عن أي شيء\",\n        \"continueWriting\": \"استمر في الكتابة\",\n        \"fixSpelling\": \"تصحيح الأخطاء الإملائية والنحوية\",\n        \"improveWriting\": \"تحسين الكتابة\",\n        \"summarize\": \"تلخيص\",\n        \"explain\": \"اشرح\",\n        \"makeShorter\": \"اجعلها أقصر\",\n        \"makeLonger\": \"اجعلها أطول\"\n      },\n      \"autoGeneratorMenuItemName\": \"كاتب AI\",\n      \"autoGeneratorTitleName\": \"AI: اطلب من الذكاء الاصطناعي كتابة أي شيء ...\",\n      \"autoGeneratorLearnMore\": \"يتعلم أكثر\",\n      \"autoGeneratorGenerate\": \"يولد\",\n      \"autoGeneratorHintText\": \"اسأل AI ...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"لا يمكن الحصول على مفتاح AI\",\n      \"autoGeneratorRewrite\": \"اعادة كتابة\",\n      \"smartEdit\": \"مساعدي الذكاء الاصطناعي\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"أصلح التهجئة\",\n      \"warning\": \"⚠️ يمكن أن تكون استجابات الذكاء الاصطناعي غير دقيقة أو مضللة.\",\n      \"smartEditSummarize\": \"لخص\",\n      \"smartEditImproveWriting\": \"تحسين الكتابة\",\n      \"smartEditMakeLonger\": \"اجعله أطول\",\n      \"smartEditCouldNotFetchResult\": \"تعذر جلب النتيجة من AI\",\n      \"smartEditCouldNotFetchKey\": \"تعذر جلب مفتاح AI\",\n      \"smartEditDisabled\": \"قم بتوصيل AI في الإعدادات\",\n      \"appflowyAIEditDisabled\": \"تسجيل الدخول لتمكين ميزات الذكاء الاصطناعي\",\n      \"discardResponse\": \"هل تريد تجاهل استجابات الذكاء الاصطناعي؟\",\n      \"createInlineMathEquation\": \"اصنع معادلة\",\n      \"fonts\": \"الخطوط\",\n      \"insertDate\": \"أدخل التاريخ\",\n      \"emoji\": \"الرموز التعبيرية\",\n      \"toggleList\": \"تبديل القائمة\",\n      \"emptyToggleHeading\": \"تبديل فارغ h{}. انقر لإضافة محتوى.\",\n      \"emptyToggleList\": \"قائمة تبديل فارغة. انقر لإضافة المحتوى.\",\n      \"emptyToggleHeadingWeb\": \"تبديل فارغ h{level}. انقر لإضافة محتوى\",\n      \"quoteList\": \"قائمة الاقتباس\",\n      \"numberedList\": \"قائمة مرقمة\",\n      \"bulletedList\": \"قائمة نقطية\",\n      \"todoList\": \"قائمة مهام\",\n      \"callout\": \"استدعاء\",\n      \"simpleTable\": {\n        \"moreActions\": {\n          \"color\": \"اللون\",\n          \"align\": \"المحاذاة\",\n          \"delete\": \"حذف\",\n          \"duplicate\": \"تكرار\",\n          \"insertLeft\": \"أدخل اليسار\",\n          \"insertRight\": \"إدراج إلى اليمين\",\n          \"insertAbove\": \"إدراج أعلاه\",\n          \"insertBelow\": \"إدراج أدناه\",\n          \"headerColumn\": \"عمود رأس الصفحة\",\n          \"headerRow\": \"صف رأس الصفحة\",\n          \"clearContents\": \"مسح المحتويات\",\n          \"setToPageWidth\": \"اضبط على عرض الصفحة\",\n          \"distributeColumnsWidth\": \"توزيع الأعمدة بالتساوي\",\n          \"duplicateRow\": \"صف مكرر\",\n          \"duplicateColumn\": \"عمود مكرر\",\n          \"textColor\": \"لون النص\",\n          \"cellBackgroundColor\": \"لون خلفية الخلية\",\n          \"duplicateTable\": \"جدول مكرر\"\n        },\n        \"clickToAddNewRow\": \"انقر لإضافة صف جديد\",\n        \"clickToAddNewColumn\": \"انقر لإضافة عمود جديد\",\n        \"clickToAddNewRowAndColumn\": \"انقر لإضافة صف وعمود جديدين\",\n        \"headerName\": {\n          \"table\": \"الجدول\",\n          \"alignText\": \"محاذاة النص\"\n        }\n      },\n      \"cover\": {\n        \"changeCover\": \"تبديل الغطاء\",\n        \"colors\": \"الألوان\",\n        \"images\": \"الصور\",\n        \"clearAll\": \"امسح الكل\",\n        \"abstract\": \"خلاصة\",\n        \"addCover\": \"أضف الغلاف\",\n        \"addLocalImage\": \"أضف الصورة المحلية\",\n        \"invalidImageUrl\": \"عنوان URL للصورة غير صالح\",\n        \"failedToAddImageToGallery\": \"فشل في إضافة الصورة إلى المعرض\",\n        \"enterImageUrl\": \"أدخل عنوان URL للصورة\",\n        \"add\": \"يضيف\",\n        \"back\": \"خلف\",\n        \"saveToGallery\": \"حفظ في المعرض\",\n        \"removeIcon\": \"إزالة الرمز\",\n        \"removeCover\": \"إزالة الغلاف\",\n        \"pasteImageUrl\": \"لصق عنوان URL للصورة\",\n        \"or\": \"أو\",\n        \"pickFromFiles\": \"اختر من الملفات\",\n        \"couldNotFetchImage\": \"تعذر جلب الصورة\",\n        \"imageSavingFailed\": \"فشل حفظ الصورة\",\n        \"addIcon\": \"إضافة أيقونة\",\n        \"changeIcon\": \"تغيير الايقونة\",\n        \"coverRemoveAlert\": \"ستتم إزالته من الغلاف بعد حذفه.\",\n        \"alertDialogConfirmation\": \"هل أنت متأكد أنك تريد الاستمرار؟\"\n      },\n      \"mathEquation\": {\n        \"name\": \"معادلة رياضية\",\n        \"addMathEquation\": \"أضف معادلة رياضية\",\n        \"editMathEquation\": \"تحرير المعادلة الرياضية\"\n      },\n      \"optionAction\": {\n        \"click\": \"انقر\",\n        \"toOpenMenu\": \" لفتح القائمة\",\n        \"drag\": \"سحب\",\n        \"toMove\": \" حرك\",\n        \"delete\": \"يمسح\",\n        \"duplicate\": \"ينسخ\",\n        \"turnInto\": \"تحول إلى\",\n        \"moveUp\": \"تحرك\",\n        \"moveDown\": \"تحرك لأسفل\",\n        \"color\": \"لون\",\n        \"align\": \"محاذاة\",\n        \"left\": \"غادر\",\n        \"center\": \"مركز\",\n        \"right\": \"يمين\",\n        \"defaultColor\": \"تقصير\",\n        \"depth\": \"عمق\",\n        \"copyLinkToBlock\": \"نسخ الرابط إلى الكتلة\"\n      },\n      \"image\": {\n        \"addAnImage\": \"أضف صورة\",\n        \"copiedToPasteBoard\": \"تم نسخ رابط الصورة إلى الحافظة\",\n        \"addAnImageDesktop\": \"أضف صورة\",\n        \"addAnImageMobile\": \"انقر لإضافة صورة واحدة أو أكثر\",\n        \"dropImageToInsert\": \"إسقاط الصور لإدراجها\",\n        \"imageUploadFailed\": \"فشل رفع الصورة\",\n        \"imageDownloadFailed\": \"فشل تنزيل الصورة، يرجى المحاولة مرة أخرى\",\n        \"imageDownloadFailedToken\": \"فشل تنزيل الصورة بسبب عدم وجود رمز المستخدم، يرجى المحاولة مرة أخرى\",\n        \"errorCode\": \"كود الخطأ\"\n      },\n      \"photoGallery\": {\n        \"name\": \"معرض الصور\",\n        \"imageKeyword\": \"صورة\",\n        \"imageGalleryKeyword\": \"معرض الصور\",\n        \"photoKeyword\": \"صورة\",\n        \"photoBrowserKeyword\": \"متصفح الصور\",\n        \"galleryKeyword\": \"معرض الصور\",\n        \"addImageTooltip\": \"أضف صورة\",\n        \"changeLayoutTooltip\": \"تغيير التخطيط\",\n        \"browserLayout\": \"المتصفح\",\n        \"gridLayout\": \"شبكة\",\n        \"deleteBlockTooltip\": \"حذف المعرض بأكمله\"\n      },\n      \"math\": {\n        \"copiedToPasteBoard\": \"تم نسخ المعادلة الرياضية إلى الحافظة\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"تم نسخ الرابط إلى الحافظة\",\n        \"convertToLink\": \"تحويل إلى رابط التضمين\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"أضف عناوين لإنشاء جدول محتويات.\",\n        \"noMatchHeadings\": \"لم يتم العثور على عناوين مطابقة.\"\n      },\n      \"table\": {\n        \"addAfter\": \"أضف بعد\",\n        \"addBefore\": \"أضف قبل\",\n        \"delete\": \"حذف\",\n        \"clear\": \"مسح المحتوى\",\n        \"duplicate\": \"نسخة طبق الاصل\",\n        \"bgColor\": \"لون الخلفية\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"نسخ\",\n        \"cut\": \"قطع\",\n        \"paste\": \"لصق\",\n        \"pasteAsPlainText\": \"لصق كنص عادي\"\n      },\n      \"action\": \"أجراءات\",\n      \"database\": {\n        \"selectDataSource\": \"حدد مصدر البيانات\",\n        \"noDataSource\": \"لا يوجد مصدر للبيانات\",\n        \"selectADataSource\": \"حدد مصدر البيانات\",\n        \"toContinue\": \"للمتابعة\",\n        \"newDatabase\": \"قاعدة بيانات جديدة\",\n        \"linkToDatabase\": \"رابط لقاعدة البيانات\"\n      },\n      \"date\": \"تاريخ\",\n      \"video\": {\n        \"label\": \"فيديو\",\n        \"emptyLabel\": \"أضف فيديو\",\n        \"placeholder\": \"ألصق رابط الفيديو\",\n        \"copiedToPasteBoard\": \"تم نسخ رابط الفيديو إلى الحافظة\",\n        \"insertVideo\": \"إضافة الفيديو\",\n        \"invalidVideoUrl\": \"لم يتم دعم عنوان URL المصدر بعد.\",\n        \"invalidVideoUrlYouTube\": \"لم يتم دعم YouTube بعد.\",\n        \"supportedFormats\": \"التنسيقات المدعومة: MP4، WebM، MOV، AVI، FLV، MPEG/M4V، H.264\"\n      },\n      \"file\": {\n        \"name\": \"ملف\",\n        \"uploadTab\": \"رفع\",\n        \"uploadMobile\": \"اختر ملف\",\n        \"uploadMobileGallery\": \"من معرض الصور\",\n        \"networkTab\": \"تضمين الرابط\",\n        \"placeholderText\": \"رفع أو تضمين ملف\",\n        \"placeholderDragging\": \"إفلات الملف للرفع\",\n        \"dropFileToUpload\": \"إفلات ملف لتحميله\",\n        \"fileUploadHint\": \"اسحب وأفلِت ملفًا أو انقر فوقه \",\n        \"fileUploadHintSuffix\": \"تصفح\",\n        \"networkHint\": \"لصق رابط الملف\",\n        \"networkUrlInvalid\": \"عنوان URL غير صالح. تحقق من عنوان URL وحاول مرة أخرى.\",\n        \"networkAction\": \"تضمين\",\n        \"fileTooBigError\": \"حجم الملف كبير جدًا، يرجى رفع ملف بحجم أقل من 10 ميجا بايت\",\n        \"renameFile\": {\n          \"title\": \"إعادة تسمية الملف\",\n          \"description\": \"أدخل الاسم الجديد لهذا الملف\",\n          \"nameEmptyError\": \"لا يمكن ترك اسم الملف فارغًا.\"\n        },\n        \"uploadedAt\": \"تم الرفع في {}\",\n        \"linkedAt\": \"تمت إضافة الرابط في {}\",\n        \"failedToOpenMsg\": \"فشل في الفتح، لم يتم العثور على الملف\"\n      },\n      \"subPage\": {\n        \"handlingPasteHint\": \" - (التعامل مع اللصق)\",\n        \"errors\": {\n          \"failedDeletePage\": \"فشل في حذف الصفحة\",\n          \"failedCreatePage\": \"فشل في إنشاء الصفحة\",\n          \"failedMovePage\": \"فشل نقل الصفحة إلى هذا المستند\",\n          \"failedDuplicatePage\": \"فشل في تكرار الصفحة\",\n          \"failedDuplicateFindView\": \"فشل في تكرار الصفحة - لم يتم العثور على العرض الأصلي\"\n        }\n      },\n      \"cannotMoveToItsChildren\": \"لا يمكن الانتقال إلى أطفاله\",\n      \"linkPreview\": {\n        \"typeSelection\": {\n          \"pasteAs\": \"لصق كـ\",\n          \"mention\": \"ذِكْر\",\n          \"URL\": \"عنوان URL\",\n          \"bookmark\": \"إشارة مرجعية\",\n          \"embed\": \"تضمين\"\n        },\n        \"linkPreviewMenu\": {\n          \"toMetion\": \"تحويل إلى ذكر\",\n          \"toUrl\": \"تحويل إلى عنوان URL\",\n          \"toEmbed\": \"تحويل إلى تضمين\",\n          \"toBookmark\": \"تحويل إلى إشارة مرجعية\",\n          \"copyLink\": \"نسخ الرابط\",\n          \"replace\": \"استبدال\",\n          \"reload\": \"إعادة التحميل\",\n          \"removeLink\": \"إزالة الرابط\",\n          \"pasteHint\": \"ألصق في https://...\",\n          \"unableToDisplay\": \"غير قادر على العرض\"\n        }\n      },\n      \"autoCompletionMenuItemName\": \"عنصر قائمة الإكمال التلقائي\"\n    },\n    \"outlineBlock\": {\n      \"placeholder\": \"جدول المحتويات\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"اكتب \\\"/\\\" للأوامر\"\n    },\n    \"title\": {\n      \"placeholder\": \"بدون عنوان\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"انقر لإضافة الصورة\",\n      \"upload\": {\n        \"label\": \"رفع\",\n        \"placeholder\": \"انقر لتحميل الصورة\"\n      },\n      \"url\": {\n        \"label\": \"رابط الصورة\",\n        \"placeholder\": \"أدخل عنوان URL للصورة\"\n      },\n      \"ai\": {\n        \"label\": \"إنشاء صورة من AI\",\n        \"placeholder\": \"يرجى إدخال الامر الواصف لـ AI لإنشاء الصورة\"\n      },\n      \"stability_ai\": {\n        \"label\": \"إنشاء صورة من Stability AI\",\n        \"placeholder\": \"يرجى إدخال المطالبة الخاصة بـ Stability AI لإنشاء الصورة\"\n      },\n      \"support\": \"الحد الأقصى لحجم الصورة هو 5 ميغا بايت. التنسيقات المدعومة: JPEG ، PNG ، GIF ، SVG\",\n      \"error\": {\n        \"invalidImage\": \"صورة غير صالحة\",\n        \"invalidImageSize\": \"يجب أن يكون حجم الصورة أقل من 5 ميغا بايت\",\n        \"invalidImageFormat\": \"تنسيق الصورة غير مدعوم. التنسيقات المدعومة: JPEG ، PNG ، GIF ، SVG\",\n        \"invalidImageUrl\": \"عنوان URL للصورة غير صالح\",\n        \"noImage\": \"لا يوجد مثل هذا الملف أو الدليل\",\n        \"multipleImagesFailed\": \"فشلت عملية رفع صورة واحدة أو أكثر، يرجى المحاولة مرة أخرى\"\n      },\n      \"embedLink\": {\n        \"label\": \"رابط متضمن\",\n        \"placeholder\": \"الصق أو اكتب رابط الصورة\"\n      },\n      \"unsplash\": {\n        \"label\": \"Unsplash\"\n      },\n      \"searchForAnImage\": \"ابحث عن صورة\",\n      \"pleaseInputYourOpenAIKey\": \"يرجى إدخال مفتاح AI الخاص بك في صفحة الإعدادات\",\n      \"saveImageToGallery\": \"احفظ الصورة\",\n      \"failedToAddImageToGallery\": \"فشلت إضافة الصورة إلى المعرض\",\n      \"successToAddImageToGallery\": \"تمت إضافة الصورة إلى المعرض بنجاح\",\n      \"unableToLoadImage\": \"غير قادر على تحميل الصورة\",\n      \"maximumImageSize\": \"الحد الأقصى لحجم الصورة المسموح برفعها هو 10 ميجا بايت\",\n      \"uploadImageErrorImageSizeTooBig\": \"يجب أن يكون حجم الصورة أقل من 10 ميجا بايت\",\n      \"imageIsUploading\": \"جاري رفع الصورة\",\n      \"openFullScreen\": \"افتح في الشاشة الكاملة\",\n      \"interactiveViewer\": {\n        \"toolbar\": {\n          \"previousImageTooltip\": \"الصورة السابقة\",\n          \"nextImageTooltip\": \"الصورة التالية\",\n          \"zoomOutTooltip\": \"تصغير\",\n          \"zoomInTooltip\": \"تكبير\",\n          \"changeZoomLevelTooltip\": \"تغيير مستوى التكبير\",\n          \"openLocalImage\": \"افتح الصورة\",\n          \"downloadImage\": \"تنزيل الصورة\",\n          \"closeViewer\": \"إغلاق العارض التفاعلي\",\n          \"scalePercentage\": \"{}%\",\n          \"deleteImageTooltip\": \"حذف الصورة\"\n        }\n      },\n      \"pleaseInputYourStabilityAIKey\": \"يرجى إدخال مفتاح Stability AI الخاص بك في صفحة الإعدادات\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"لغة\",\n        \"placeholder\": \"اختار اللغة\",\n        \"auto\": \"آلي\"\n      },\n      \"copyTooltip\": \"نسخ\",\n      \"searchLanguageHint\": \"ابحث عن لغة\",\n      \"codeCopiedSnackbar\": \"تم نسخ الكود إلى الحافظة!\"\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"الصق أو اكتب ارتباطًا\",\n      \"openInNewTab\": \"فتح في علامة تبويب جديدة\",\n      \"copyLink\": \"نسخ الرابط\",\n      \"removeLink\": \"إزالة الرابط\",\n      \"url\": {\n        \"label\": \"URL رابط\",\n        \"placeholder\": \"أدخل رابط\"\n      },\n      \"title\": {\n        \"label\": \"عنوان الارتباط\",\n        \"placeholder\": \"أدخل عنوان الرابط\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"اذكر شخص أو صفحة أو تاريخ...\",\n      \"page\": {\n        \"label\": \"رابط إلى الصفحة\",\n        \"tooltip\": \"انقر لفتح الصفحة\"\n      },\n      \"deleted\": \"تم الحذف\",\n      \"deletedContent\": \"هذا المحتوى غير موجود أو تم حذفه\",\n      \"noAccess\": \"لا يوجد وصول\",\n      \"deletedPage\": \"الصفحة المحذوفة\",\n      \"trashHint\": \" - في سلة المهملات\",\n      \"morePages\": \"المزيد من الصفحات\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"إعادة تعيين إلى الافتراضي\",\n      \"textSize\": \"حجم النص\",\n      \"textColor\": \"لون النص\",\n      \"h1\": \"العنوان 1\",\n      \"h2\": \"العنوان 2\",\n      \"h3\": \"العنوان 3\",\n      \"alignLeft\": \"محاذاة إلى اليسار\",\n      \"alignRight\": \"محاذاة إلى اليمين\",\n      \"alignCenter\": \"محاذاة إلى الوسط\",\n      \"link\": \"وصلة\",\n      \"textAlign\": \"محاذاة النص\",\n      \"moreOptions\": \"المزيد من الخيارات\",\n      \"font\": \"الخط\",\n      \"inlineCode\": \"الكود المضمن\",\n      \"suggestions\": \"اقتراحات\",\n      \"turnInto\": \"تحول إلى\",\n      \"equation\": \"معادلة\",\n      \"insert\": \"إدراج\",\n      \"linkInputHint\": \"لصق الرابط أو البحث عن الصفحات\",\n      \"pageOrURL\": \"الصفحة أو عنوان URL\",\n      \"linkName\": \"اسم الرابط\",\n      \"linkNameHint\": \"اسم رابط الإدخال\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"الإصدار الحالي لا يدعم هذا الحقل.\",\n      \"clickToCopyTheBlockContent\": \"انقر هنا لنسخ محتوى الكتلة\",\n      \"blockContentHasBeenCopied\": \"تم نسخ محتوى الحقل.\",\n      \"parseError\": \"حدث خطأ أثناء تحليل الكتلة {}.\",\n      \"copyBlockContent\": \"نسخ محتوى الكتلة\"\n    },\n    \"mobilePageSelector\": {\n      \"title\": \"حدد الصفحة\",\n      \"failedToLoad\": \"فشل تحميل قائمة الصفحات\",\n      \"noPagesFound\": \"لم يتم العثور على صفحات\"\n    },\n    \"attachmentMenu\": {\n      \"choosePhoto\": \"اختر الصورة\",\n      \"takePicture\": \"التقط صورة\",\n      \"chooseFile\": \"اختر الملف\"\n    },\n    \"data\": {\n      \"timeHintTextInTwelveHour\": \"اثنا عشر ساعة\",\n      \"timeHintTextInTwentyFourHour\": \"أربع و عشرون ساعة\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"label\": \"عمود\",\n      \"createNewCard\": \"جديد\",\n      \"renameGroupTooltip\": \"اضغط لإعادة تسمية المجموعة\",\n      \"createNewColumn\": \"أضف مجموعة جديدة\",\n      \"addToColumnTopTooltip\": \"أضف بطاقة جديدة في الأعلى\",\n      \"addToColumnBottomTooltip\": \"أضف بطاقة جديدة في الأسفل\",\n      \"renameColumn\": \"إعادة تسمية\",\n      \"hideColumn\": \"اخفاء\",\n      \"newGroup\": \"مجموعة جديدة\",\n      \"deleteColumn\": \"مسح\",\n      \"deleteColumnConfirmation\": \"سيؤدي هذا إلى حذف هذه المجموعة وجميع البطاقات الموجودة فيها. هل أنت متأكد من الاستمرار؟\",\n      \"groupActions\": \"إجراءات المجموعة\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"المجموعات المخفية\",\n      \"collapseTooltip\": \"إخفاء المجموعات المخفية\",\n      \"expandTooltip\": \"عرض المجموعات المخفية\"\n    },\n    \"cardDetail\": \"تفاصيل البطاقة\",\n    \"cardActions\": \"إجراءات البطاقة\",\n    \"cardDuplicated\": \"تم تكرار البطاقة\",\n    \"cardDeleted\": \"تم حذف البطاقة\",\n    \"showOnCard\": \"اظهار التفاصيل على البطاقة\",\n    \"setting\": \"اعداد\",\n    \"propertyName\": \"اسم الخاصية\",\n    \"menuName\": \"سبورة\",\n    \"showUngrouped\": \"إظهار العناصر غير المجمعة\",\n    \"ungroupedButtonText\": \"غير مجمعة\",\n    \"ungroupedButtonTooltip\": \"تحتوي على بطاقات لا تنتمي إلى أي مجموعة\",\n    \"ungroupedItemsTitle\": \"انقر للإضافة إلى السبورة\",\n    \"groupBy\": \"مجموعة من\",\n    \"groupCondition\": \"حالة المجموعة\",\n    \"referencedBoardPrefix\": \"نظرا ل\",\n    \"notesTooltip\": \"ملاحظات بالداخل\",\n    \"mobile\": {\n      \"editURL\": \"تعديل الرابط\",\n      \"showGroup\": \"إظهار المجموعة\",\n      \"showGroupContent\": \"هل أنت متأكد من إظهار هذه المجموعة على السبورة؟\",\n      \"failedToLoad\": \"فشل تحميل عرض السبورة\"\n    },\n    \"dateCondition\": {\n      \"weekOf\": \"أسبوع {} - {}\",\n      \"today\": \"اليوم\",\n      \"yesterday\": \"أمس\",\n      \"tomorrow\": \"غداً\",\n      \"lastSevenDays\": \"آخر 7 أيام\",\n      \"nextSevenDays\": \"الأيام 7 القادمة\",\n      \"lastThirtyDays\": \"آخر 30 يوما\",\n      \"nextThirtyDays\": \"30 يوما القادم\"\n    },\n    \"noGroup\": \"لا توجد مجموعة حسب الخاصية\",\n    \"noGroupDesc\": \"تتطلب وجهات نظر اللوحة خاصية للتجميع من أجل العرض\",\n    \"media\": {\n      \"cardText\": \"{} {}\",\n      \"fallbackName\": \"الملفات\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"تقويم\",\n    \"defaultNewCalendarTitle\": \"بدون عنوان\",\n    \"newEventButtonTooltip\": \"إضافة حدث جديد\",\n    \"navigation\": {\n      \"today\": \"اليوم\",\n      \"jumpToday\": \"انتقل إلى اليوم\",\n      \"previousMonth\": \"الشهر الماضى\",\n      \"nextMonth\": \"الشهر القادم\",\n      \"views\": {\n        \"day\": \"يوم\",\n        \"week\": \"أسبوع\",\n        \"month\": \"شهر\",\n        \"year\": \"سنة\"\n      }\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"لا يوجد أحداث حتى الآن\",\n      \"emptyBody\": \"اضغط على زر الإضافة \\\"+\\\" لإنشاء حدث في هذا اليوم.\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"إظهار أرقام الأسبوع\",\n      \"showWeekends\": \"عرض عطلات نهاية الأسبوع\",\n      \"firstDayOfWeek\": \"اليوم الأول من الأسبوع\",\n      \"layoutDateField\": \"تقويم التخطيط بواسطة\",\n      \"changeLayoutDateField\": \"تغيير حقل التخطيط\",\n      \"noDateTitle\": \"بدون تاريخ\",\n      \"unscheduledEventsTitle\": \"الأحداث غير المجدولة\",\n      \"clickToAdd\": \"انقر للإضافة إلى التقويم\",\n      \"name\": \"تخطيط التقويم\",\n      \"clickToOpen\": \"انقر لفتح السجل\",\n      \"noDateHint\": \"ستظهر الأحداث غير المجدولة هنا\"\n    },\n    \"referencedCalendarPrefix\": \"نظرا ل\",\n    \"quickJumpYear\": \"انتقل إلى\",\n    \"duplicateEvent\": \"حدث مكرر\"\n  },\n  \"errorDialog\": {\n    \"title\": \"خطأ @:appName\",\n    \"howToFixFallback\": \"نأسف للإزعاج! قم بإرسال مشكلة على صفحة GitHub الخاصة بنا والتي تصف الخطأ الخاص بك.\",\n    \"howToFixFallbackHint1\": \"نحن نأسف للإزعاج! أرسل مشكلة على \",\n    \"howToFixFallbackHint2\": \" الصفحة التي تصف الخطأ الخاص بك.\",\n    \"github\": \"عرض على جيثب\"\n  },\n  \"search\": {\n    \"label\": \"يبحث\",\n    \"sidebarSearchIcon\": \"ابحث وانتقل بسرعة إلى الصفحة\",\n    \"searchOrAskAI\": \"ابحث أو اسأل الذكاء الاصطناعي\",\n    \"askAIAnything\": \"اسأل الذكاء الاصطناعي عن أي شيء\",\n    \"askAIFor\": \"اسأل الذكاء الاصطناعي\",\n    \"noResultForSearching\": \"لم يتم العثور على أي تطابقات\",\n    \"noResultForSearchingHint\": \"حاول استخدام أسئلة أو كلمات رئيسية مختلفة.\\nقد تكون بعض الصفحات في سلة المهملات.\",\n    \"bestMatch\": \"أفضل تطابق\",\n    \"showMore\": \"إظهار المزيد\",\n    \"somethingWentWrong\": \"لقد حدث خطأ ما\",\n    \"pageNotExist\": \"هذه الصفحة غير موجودة\",\n    \"tryAgainOrLater\": \"الرجاء المحاولة مرة أخرى لاحقًا\",\n    \"placeholder\": {\n      \"actions\": \"إجراءات البحث ...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"نسخ!\",\n      \"fail\": \"غير قادر على النسخ\"\n    }\n  },\n  \"unSupportBlock\": \"الإصدار الحالي لا يدعم هذه الكتلة.\",\n  \"views\": {\n    \"deleteContentTitle\": \"هل أنت متأكد من أنك تريد حذف {pageType}؟\",\n    \"deleteContentCaption\": \"إذا قمت بحذف {pageType} هذه ، فيمكنك استعادتها من سلة المهملات.\"\n  },\n  \"colors\": {\n    \"custom\": \"مخصص\",\n    \"default\": \"اساسي\",\n    \"red\": \"أحمر\",\n    \"orange\": \"برتقالي\",\n    \"yellow\": \"أصفر\",\n    \"green\": \"أخضر\",\n    \"blue\": \"أزرق\",\n    \"purple\": \"بنفسجي\",\n    \"pink\": \"وردي\",\n    \"brown\": \"بني\",\n    \"gray\": \"رمادي\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"رمز تعبيري\",\n    \"search\": \"ابحث عن الرموز التعبيرية\",\n    \"noRecent\": \"لا توجد رموز تعبيرية حديثة\",\n    \"noEmojiFound\": \"لم يتم العثور على رموز تعبيرية\",\n    \"filter\": \"منقي\",\n    \"random\": \"عشوائي\",\n    \"selectSkinTone\": \"حدد لون البشرة\",\n    \"remove\": \"إزالة الرموز التعبيرية\",\n    \"categories\": {\n      \"smileys\": \"الوجوه الضاحكة والعاطفة\",\n      \"people\": \"الناس والجسم\",\n      \"animals\": \"الحيوانات والطبيعة\",\n      \"food\": \"طعام شراب\",\n      \"activities\": \"أنشطة\",\n      \"places\": \"السفر والأماكن\",\n      \"objects\": \"أشياء\",\n      \"symbols\": \"حرف او رمز\",\n      \"flags\": \"أعلام\",\n      \"nature\": \"طبيعة\",\n      \"frequentlyUsed\": \"تستخدم بشكل متكرر\"\n    },\n    \"skinTone\": {\n      \"default\": \"اساسي\",\n      \"light\": \"فاتح\",\n      \"mediumLight\": \"فاتح قليلا\",\n      \"medium\": \"متوسط\",\n      \"mediumDark\": \"متوسطة الظلمة\",\n      \"dark\": \"مظلم\"\n    },\n    \"openSourceIconsFrom\": \"أيقونات مفتوحة المصدر من\"\n  },\n  \"inlineActions\": {\n    \"noResults\": \"لا نتائج\",\n    \"recentPages\": \"الصفحات الأخيرة\",\n    \"pageReference\": \"مرجع الصفحة\",\n    \"docReference\": \"مرجع المستند\",\n    \"boardReference\": \"مرجع اللوحة\",\n    \"calReference\": \"مرجع التقويم\",\n    \"gridReference\": \"مرجع الشبكة\",\n    \"date\": \"تاريخ\",\n    \"reminder\": {\n      \"groupTitle\": \"تذكير\",\n      \"shortKeyword\": \"يذكر\"\n    },\n    \"createPage\": \"إنشاء صفحة فرعية \\\"{}\\\"\"\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"تغيير تنسيق التاريخ والوقت في الإعدادات\",\n    \"dateFormat\": \"تنسيق التاريخ\",\n    \"includeTime\": \"تضمين الوقت\",\n    \"isRange\": \"تاريخ النهاية\",\n    \"timeFormat\": \"تنسيق الوقت\",\n    \"clearDate\": \"مسح التاريخ\",\n    \"reminderLabel\": \"تذكير\",\n    \"selectReminder\": \"حدد التذكير\",\n    \"reminderOptions\": {\n      \"none\": \"لا شيء\",\n      \"atTimeOfEvent\": \"وقت الحدث\",\n      \"fiveMinsBefore\": \"قبل 5 دقائق\",\n      \"tenMinsBefore\": \"قبل 10 دقائق\",\n      \"fifteenMinsBefore\": \"قبل 15 دقيقة\",\n      \"thirtyMinsBefore\": \"قبل 30 دقيقة\",\n      \"oneHourBefore\": \"قبل ساعة واحدة\",\n      \"twoHoursBefore\": \"قبل ساعتين\",\n      \"onDayOfEvent\": \"في يوم الحدث\",\n      \"oneDayBefore\": \"قبل يوم واحد\",\n      \"twoDaysBefore\": \"قبل يومين\",\n      \"oneWeekBefore\": \"قبل اسبوع واحد\",\n      \"custom\": \"مخصص\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"أمس\",\n    \"today\": \"اليوم\",\n    \"tomorrow\": \"غداً\",\n    \"oneWeek\": \"أسبوع\"\n  },\n  \"notificationHub\": {\n    \"title\": \"إشعارات\",\n    \"closeNotification\": \"إغلاق الإشعار\",\n    \"viewNotifications\": \"عرض الإشعارات\",\n    \"noNotifications\": \"لا يوجد إشعارات حتى الآن\",\n    \"mentionedYou\": \"ذكرتك\",\n    \"archivedTooltip\": \"أرشفة هذا الإشعار\",\n    \"unarchiveTooltip\": \"إلغاء أرشفة هذا الإشعار\",\n    \"markAsReadTooltip\": \"قم بتمييز هذا الإشعار كمقروء\",\n    \"markAsArchivedSucceedToast\": \"تم الأرشفة بنجاح\",\n    \"markAllAsArchivedSucceedToast\": \"تم أرشفة كل شيء بنجاح\",\n    \"markAsReadSucceedToast\": \"وضع علامة كمقروءة بنجاح\",\n    \"markAllAsReadSucceedToast\": \"تم وضع علامة على كل شيء كمقروء بنجاح\",\n    \"today\": \"اليوم\",\n    \"older\": \"أقدم\",\n    \"mobile\": {\n      \"title\": \"تحديثات\"\n    },\n    \"emptyTitle\": \"انت على اخر اصدار!\",\n    \"emptyBody\": \"لا توجد إخطارات أو إجراءات معلقة. استمتع بالهدوء.\",\n    \"tabs\": {\n      \"inbox\": \"صندوق الوارد\",\n      \"upcoming\": \"القادمة\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"اشر عليها بانها قرات\",\n      \"showAll\": \"الجميع\",\n      \"showUnreads\": \"غير مقروءة\"\n    },\n    \"filters\": {\n      \"ascending\": \"تصاعدي\",\n      \"descending\": \"تنازلي\",\n      \"groupByDate\": \"ترتيب حسب التاريخ\",\n      \"showUnreadsOnly\": \"إظهار الرسائل غير المقروءة فقط\",\n      \"resetToDefault\": \"إعادة تعيين إلى الافتراضي\"\n    },\n    \"empty\": \"فارغ\"\n  },\n  \"reminderNotification\": {\n    \"title\": \"تذكير\",\n    \"message\": \"تذكر أن تتحقق من هذا قبل أن تنسى!\",\n    \"tooltipDelete\": \"مسح\",\n    \"tooltipMarkRead\": \"ضع إشارة مقروء\",\n    \"tooltipMarkUnread\": \"وضع علامة كغير مقروءة\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"البحث\",\n    \"previousMatch\": \"المباراة السابقة\",\n    \"nextMatch\": \"نتيجة البحث القادمة\",\n    \"close\": \"اغلق\",\n    \"replace\": \"استبدل\",\n    \"replaceAll\": \"استبدال الكل\",\n    \"noResult\": \"لا نتائج\",\n    \"caseSensitive\": \"دقة الحروف\",\n    \"searchMore\": \"ابحث للعثور على المزيد من النتائج\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"آسفون\",\n    \"loadingViewError\": \"نواجه مشكلة في تحميل هذا العرض. يرجى التحقق من اتصالك بالإنترنت، وتحديث التطبيق، ولا تتردد في التواصل مع الفريق إذا استمرت المشكلة.\",\n    \"syncError\": \"لم تتم مزامنة البيانات من جهاز آخر\",\n    \"syncErrorHint\": \"يرجى إعادة فتح هذه الصفحة على الجهاز الذي تم تحريرها عليه آخر مرة، ثم فتحها مرة أخرى على الجهاز الحالي.\",\n    \"clickToCopy\": \"انقر هنا لنسخ كود الخطأ\"\n  },\n  \"editor\": {\n    \"bold\": \"عريض\",\n    \"bulletedList\": \"قائمة نقطية\",\n    \"bulletedListShortForm\": \"نقطية\",\n    \"checkbox\": \"خانة الاختيار\",\n    \"embedCode\": \"كود متضمن\",\n    \"heading1\": \"رأسية اولى\",\n    \"heading2\": \"رأسية ثانية\",\n    \"heading3\": \"رأسية ثالثة\",\n    \"highlight\": \"ابراز\",\n    \"color\": \"لون\",\n    \"image\": \"صورة\",\n    \"date\": \"تاريخ\",\n    \"page\": \"صفحة\",\n    \"italic\": \"مائل\",\n    \"link\": \"رابط\",\n    \"numberedList\": \"قائمة مرقمة\",\n    \"numberedListShortForm\": \"مرقمة\",\n    \"toggleHeading1ShortForm\": \"تبديل h1\",\n    \"toggleHeading2ShortForm\": \"تبديل h2\",\n    \"toggleHeading3ShortForm\": \"تبديل h3\",\n    \"quote\": \"اقتباس\",\n    \"strikethrough\": \"يتوسطه خط\",\n    \"text\": \"نص\",\n    \"underline\": \"تسطير\",\n    \"fontColorDefault\": \"اساسي\",\n    \"fontColorGray\": \"رمادي\",\n    \"fontColorBrown\": \"بني\",\n    \"fontColorOrange\": \"برتقالي\",\n    \"fontColorYellow\": \"أصفر\",\n    \"fontColorGreen\": \"أخضر\",\n    \"fontColorBlue\": \"أزرق\",\n    \"fontColorPurple\": \"بنفسجي\",\n    \"fontColorPink\": \"وردي\",\n    \"fontColorRed\": \"أحمر\",\n    \"backgroundColorDefault\": \"الخلفية الافتراضية\",\n    \"backgroundColorGray\": \"خلفية رمادية\",\n    \"backgroundColorBrown\": \"خلفية بنية\",\n    \"backgroundColorOrange\": \"خلفية برتقالية\",\n    \"backgroundColorYellow\": \"خلفية صفراء\",\n    \"backgroundColorGreen\": \"خلفية خضراء\",\n    \"backgroundColorBlue\": \"الخلفية الزرقاء\",\n    \"backgroundColorPurple\": \"خلفية بنفسجية\",\n    \"backgroundColorPink\": \"خلفية وردية\",\n    \"backgroundColorRed\": \"خلفية حمراء\",\n    \"backgroundColorLime\": \"خلفية ليمونية\",\n    \"backgroundColorAqua\": \"خلفية مائية\",\n    \"done\": \"تم\",\n    \"cancel\": \"الغاء\",\n    \"tint1\": \"صبغة 1\",\n    \"tint2\": \"صبغة 2\",\n    \"tint3\": \"صبغة 3\",\n    \"tint4\": \"صبغة 4\",\n    \"tint5\": \"صبغة 5\",\n    \"tint6\": \"صبغة 6\",\n    \"tint7\": \"صبغة 7\",\n    \"tint8\": \"صبغة 8\",\n    \"tint9\": \"صبغة 9\",\n    \"lightLightTint1\": \"بنفسجي\",\n    \"lightLightTint2\": \"وردي\",\n    \"lightLightTint3\": \"وردي فاتح\",\n    \"lightLightTint4\": \"برتقالي\",\n    \"lightLightTint5\": \"أصفر\",\n    \"lightLightTint6\": \"ليموني\",\n    \"lightLightTint7\": \"أخضر\",\n    \"lightLightTint8\": \"أكوا\",\n    \"lightLightTint9\": \"أزرق\",\n    \"urlHint\": \"رابط\",\n    \"mobileHeading1\": \"عنوان 1\",\n    \"mobileHeading2\": \"العنوان 2\",\n    \"mobileHeading3\": \"العنوان 3\",\n    \"mobileHeading4\": \"العنوان 4\",\n    \"mobileHeading5\": \"العنوان 5\",\n    \"mobileHeading6\": \"العنوان 6\",\n    \"textColor\": \"لون الخط\",\n    \"backgroundColor\": \"لون الخلفية\",\n    \"addYourLink\": \"أضف الرابط الخاص بك\",\n    \"openLink\": \"افتح الرابط\",\n    \"copyLink\": \"انسخ الرابط\",\n    \"removeLink\": \"إزالة الرابط\",\n    \"editLink\": \"تعديل الرابط\",\n    \"convertTo\": \"تحويل إلى\",\n    \"linkText\": \"نص\",\n    \"linkTextHint\": \"الرجاء إدخال النص\",\n    \"linkAddressHint\": \"الرجاء إدخال الرابط\",\n    \"highlightColor\": \"تسليط الضوء على اللون\",\n    \"clearHighlightColor\": \"مسح لون التمييز\",\n    \"customColor\": \"لون مخصص\",\n    \"hexValue\": \"قيمة سداسية\",\n    \"opacity\": \"العتامة\",\n    \"resetToDefaultColor\": \"إعادة التعيين إلى اللون الافتراضي\",\n    \"ltr\": \"من اليسار الى اليمين\",\n    \"rtl\": \"من اليمين الى اليسار\",\n    \"auto\": \"آلي\",\n    \"cut\": \"قص\",\n    \"copy\": \"نسخ\",\n    \"paste\": \"لصق\",\n    \"find\": \"البحث\",\n    \"select\": \"حدد\",\n    \"selectAll\": \"حدد الكل\",\n    \"previousMatch\": \"نتيجة البحث السابقة\",\n    \"nextMatch\": \"نتيجة البحث التالية\",\n    \"closeFind\": \"اغلاق\",\n    \"replace\": \"يستبدل\",\n    \"replaceAll\": \"استبدال الكل\",\n    \"regex\": \"التعبير العادي\",\n    \"caseSensitive\": \"دقة الحروف\",\n    \"uploadImage\": \"تحميل الصور\",\n    \"urlImage\": \"رابط صورة \",\n    \"incorrectLink\": \"رابط غير صحيح\",\n    \"upload\": \"رفع\",\n    \"chooseImage\": \"اختر صورة\",\n    \"loading\": \"تحميل\",\n    \"imageLoadFailed\": \"لا يمكن تحميل الصورة\",\n    \"divider\": \"مقسم\",\n    \"table\": \"جدول\",\n    \"colAddBefore\": \"إضافة قبل\",\n    \"rowAddBefore\": \"إضافة قبل\",\n    \"colAddAfter\": \"إضافة بعد\",\n    \"rowAddAfter\": \"إضافة بعد\",\n    \"colRemove\": \"ازالة\",\n    \"rowRemove\": \"ازالة\",\n    \"colDuplicate\": \"نسخة طبق الاصل\",\n    \"rowDuplicate\": \"نسخة طبق الاصل\",\n    \"colClear\": \"مسح المحتوى\",\n    \"rowClear\": \"مسح المحتوى\",\n    \"slashPlaceHolder\": \"اكتب \\\"/\\\" لإدراج كتلة، أو ابدأ الكتابة\",\n    \"typeSomething\": \"اكتب شيئا ما...\",\n    \"toggleListShortForm\": \"تبديل\",\n    \"quoteListShortForm\": \"إقتباس\",\n    \"mathEquationShortForm\": \"صيغة\",\n    \"codeBlockShortForm\": \"الكود\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"لا توجد صفحة مفضلة\",\n    \"noFavoriteHintText\": \"اسحب الصفحة إلى اليسار لإضافتها إلى المفضلة لديك\",\n    \"removeFromSidebar\": \"إزالة من الشريط الجانبي\",\n    \"addToSidebar\": \"تثبيت على الشريط الجانبي\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"أدخل / لإدراج كتلة، أو ابدأ في الكتابة\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"مهام\",\n    \"bulletList\": \"قائمة\",\n    \"numberList\": \"قائمة\",\n    \"quote\": \"اقتباس\",\n    \"heading\": \"العنوان {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"رمز الصفحة\",\n    \"language\": \"لغة\",\n    \"font\": \"الخط\",\n    \"actions\": \"أجراءات\",\n    \"date\": \"تاريخ\",\n    \"addField\": \"إضافة حقل\",\n    \"userIcon\": \"رمز المستخدم\"\n  },\n  \"noLogFiles\": \"لا توجد ملفات السجل\",\n  \"newSettings\": {\n    \"myAccount\": {\n      \"title\": \"حسابي\",\n      \"subtitle\": \"قم بتخصيص ملفك الشخصي، وإدارة أمان الحساب، وفتح مفاتيح الذكاء الاصطناعي، أو تسجيل الدخول إلى حسابك.\",\n      \"profileLabel\": \"اسم الحساب وصورة الملف الشخصي\",\n      \"profileNamePlaceholder\": \"أدخل اسمك\",\n      \"accountSecurity\": \"أمان الحساب\",\n      \"2FA\": \"المصادقة بخطوتين\",\n      \"aiKeys\": \"مفاتيح الذكاء الاصطناعي\",\n      \"accountLogin\": \"تسجيل الدخول إلى الحساب\",\n      \"updateNameError\": \"فشل في تحديث الاسم\",\n      \"updateIconError\": \"فشل في تحديث الأيقونة\",\n      \"aboutAppFlowy\": \"حول appName\",\n      \"deleteAccount\": {\n        \"title\": \"حذف الحساب\",\n        \"subtitle\": \"احذف حسابك وجميع بياناتك بشكل دائم.\",\n        \"description\": \"احذف حسابك نهائيًا وأزل إمكانية الوصول إلى جميع مساحات العمل.\",\n        \"deleteMyAccount\": \"حذف حسابي\",\n        \"dialogTitle\": \"حذف الحساب\",\n        \"dialogContent1\": \"هل أنت متأكد أنك تريد حذف حسابك نهائياً؟\",\n        \"dialogContent2\": \"لا يمكن التراجع عن هذا الإجراء، وسوف يؤدي إلى إزالة الوصول من جميع مساحات العمل، ومسح حسابك بالكامل، بما في ذلك مساحات العمل الخاصة، وإزالتك من جميع مساحات العمل المشتركة.\",\n        \"confirmHint1\": \"من فضلك اكتب \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\" للتأكيد.\",\n        \"confirmHint2\": \"أفهم أن هذا الإجراء لا رجعة فيه وسيؤدي إلى حذف حسابي وجميع البيانات المرتبطة به بشكل دائم.\",\n        \"confirmHint3\": \"حذف حسابي\",\n        \"checkToConfirmError\": \"يجب عليك تحديد المربع لتأكيد الحذف\",\n        \"failedToGetCurrentUser\": \"فشل في الحصول على البريد الإلكتروني الحالي للمستخدم\",\n        \"confirmTextValidationFailed\": \"نص التأكيد الخاص بك لا يتطابق مع \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\"\",\n        \"deleteAccountSuccess\": \"تم حذف الحساب بنجاح\"\n      },\n      \"password\": {\n        \"title\": \"كلمة المرور\",\n        \"confirmPassword\": \"تأكيد كلمة المرور\",\n        \"changePassword\": \"تغيير كلمة المرور\",\n        \"currentPassword\": \"كلمة المرور الحالية\",\n        \"newPassword\": \"كلمة المرور الجديدة\",\n        \"confirmNewPassword\": \"تأكيد كلمة المرور الجديدة\",\n        \"setupPassword\": \"إعداد كلمة المرور\",\n        \"error\": {\n          \"currentPasswordIsRequired\": \"كلمة المرور الحالية مطلوبة\",\n          \"newPasswordIsRequired\": \"مطلوب كلمة مرور جديدة\",\n          \"confirmPasswordIsRequired\": \"تأكيد كلمة المرور مطلوب\",\n          \"passwordsDoNotMatch\": \"كلمات المرور غير متطابقة\",\n          \"newPasswordIsSameAsCurrent\": \"كلمة المرور الجديدة هي نفس كلمة المرور الحالية\",\n          \"currentPasswordIsIncorrect\": \"كلمة المرور الحالية غير صحيحة\",\n          \"passwordShouldBeAtLeast6Characters\": \"يجب أن تتكون كلمة المرور من {min} مِحْرَف على الأقل\",\n          \"passwordCannotBeLongerThan72Characters\": \"لا يمكن أن تكون كلمة المرور أطول من {max} مِحْرَف\"\n        },\n        \"toast\": {\n          \"passwordUpdatedSuccessfully\": \"تم تحديث كلمة المرور بنجاح\",\n          \"passwordUpdatedFailed\": \"فشل في تحديث كلمة المرور\",\n          \"passwordSetupSuccessfully\": \"تم إعداد كلمة المرور بنجاح\",\n          \"passwordSetupFailed\": \"فشل في إعداد كلمة المرور\"\n        },\n        \"hint\": {\n          \"enterYourPassword\": \"أدخل كلمة المرور الخاصة بك\",\n          \"confirmYourPassword\": \"تأكيد كلمة المرور الخاصة بك\",\n          \"enterYourCurrentPassword\": \"أدخل كلمة المرور الحالية الخاصة بك\",\n          \"enterYourNewPassword\": \"أدخل كلمة المرور الجديدة\",\n          \"confirmYourNewPassword\": \"تأكيد كلمة المرور الجديدة\"\n        }\n      },\n      \"myAccount\": \"حسابي\",\n      \"myProfile\": \"ملفي الشخصي\"\n    },\n    \"workplace\": {\n      \"name\": \"مكان العمل\",\n      \"title\": \"إعدادات مكان العمل\",\n      \"subtitle\": \"قم بتخصيص مظهر مساحة العمل الخاصة بك والسمة والخط وتخطيط النص والتاريخ والوقت واللغة.\",\n      \"workplaceName\": \"اسم مكان العمل\",\n      \"workplaceNamePlaceholder\": \"أدخل اسم مكان العمل\",\n      \"workplaceIcon\": \"أيقونة مكان العمل\",\n      \"workplaceIconSubtitle\": \"قم بتحميل صورة أو استخدم رمزًا تعبيريًا لمساحة عملك. سيظهر الرمز في الشريط الجانبي والإشعارات.\",\n      \"renameError\": \"فشل في إعادة تسمية مكان العمل\",\n      \"updateIconError\": \"فشل في تحديث الأيقونة\",\n      \"chooseAnIcon\": \"اختر أيقونة\",\n      \"appearance\": {\n        \"name\": \"المظهر\",\n        \"themeMode\": {\n          \"auto\": \"آلي\",\n          \"light\": \"فاتح\",\n          \"dark\": \"داكن\"\n        },\n        \"language\": \"لغة\"\n      }\n    },\n    \"syncState\": {\n      \"syncing\": \"المزامنة\",\n      \"synced\": \"متزامنة\",\n      \"noNetworkConnected\": \"لا يوجد شبكة متصلة\"\n    }\n  },\n  \"pageStyle\": {\n    \"title\": \"نمط الصفحة\",\n    \"layout\": \"تَخطِيط\",\n    \"coverImage\": \"صورة الغلاف\",\n    \"pageIcon\": \"أيقونة الصفحة\",\n    \"colors\": \"الألوان\",\n    \"gradient\": \"متدرج\",\n    \"backgroundImage\": \"صورة الخلفية\",\n    \"presets\": \"الإعدادات المسبقة\",\n    \"photo\": \"صورة\",\n    \"unsplash\": \"Unsplash\",\n    \"pageCover\": \"غلاف الصفحة\",\n    \"none\": \"لا شيء\",\n    \"openSettings\": \"افتح الإعدادات\",\n    \"photoPermissionTitle\": \"يرغب @:appName في الوصول إلى مكتبة الصور الخاصة بك\",\n    \"photoPermissionDescription\": \"يحتاج @:appName إلى الوصول إلى صورك للسماح لك بإضافة صور إلى مستنداتك\",\n    \"cameraPermissionTitle\": \"يريد @:appName الوصول إلى الكاميرا الخاصة بك\",\n    \"cameraPermissionDescription\": \"يحتاج @:appName إلى الوصول إلى الكاميرا للسماح لك بإضافة صور إلى مستنداتك من الكاميرا\",\n    \"doNotAllow\": \"لا تسمح\",\n    \"image\": \"صورة\"\n  },\n  \"commandPalette\": {\n    \"placeholder\": \"ابحث أو اطرح سؤالا...\",\n    \"bestMatches\": \"أفضل المطابقات\",\n    \"aiOverview\": \"نظرة عامة على الذكاء الاصطناعي\",\n    \"aiOverviewSource\": \"مصادر مرجعية\",\n    \"aiOverviewMoreDetails\": \"مزيد من التفاصيل\",\n    \"pagePreview\": \"معاينة المحتوى\",\n    \"clickToOpenPage\": \"انقر لفتح الصفحة\",\n    \"recentHistory\": \"التاريخ الحديث\",\n    \"navigateHint\": \"للتنقل\",\n    \"loadingTooltip\": \"نحن نبحث عن نتائج...\",\n    \"betaLabel\": \"النسخة التجريبية\",\n    \"betaTooltip\": \"نحن ندعم حاليًا البحث عن الصفحات والمحتوى في المستندات فقط\",\n    \"fromTrashHint\": \"من سلة المحذوفات\",\n    \"noResultsHint\": \"لم نعثر على ما تبحث عنه، حاول البحث عن مصطلح آخر.\",\n    \"clearSearchTooltip\": \"مسح حقل البحث\",\n    \"location\": \"موقع\",\n    \"created\": \"تم الإنشاء\",\n    \"edited\": \"تم التعديل\"\n  },\n  \"space\": {\n    \"delete\": \"حذف\",\n    \"deleteConfirmation\": \"حذف: \",\n    \"deleteConfirmationDescription\": \"سيتم حذف جميع الصفحات الموجودة ضمن هذه المساحة ونقلها إلى سلة المحذوفات، وسيتم إلغاء نشر أي صفحات منشورة.\",\n    \"rename\": \"إعادة تسمية المساحة\",\n    \"changeIcon\": \"تغيير الأيقونة\",\n    \"manage\": \"إدارة المساحة\",\n    \"addNewSpace\": \"إنشاء مساحة\",\n    \"collapseAllSubPages\": \"طي كل الصفحات الفرعية\",\n    \"createNewSpace\": \"إنشاء مساحة جديدة\",\n    \"createSpaceDescription\": \"إنشاء مساحات عامة وخاصة متعددة لتنظيم عملك بشكل أفضل.\",\n    \"spaceName\": \"اسم المساحة\",\n    \"spaceNamePlaceholder\": \"على سبيل المثال التسويق والهندسة والموارد البشرية\",\n    \"permission\": \"إذن المساحة\",\n    \"publicPermission\": \"عام\",\n    \"publicPermissionDescription\": \"جميع أعضاء مساحة العمل لديهم إمكانية الوصول الكامل\",\n    \"privatePermission\": \"خاص\",\n    \"privatePermissionDescription\": \"أنت فقط من يمكنه الوصول إلى هذه المساحة\",\n    \"spaceIconBackground\": \"لون الخلفية\",\n    \"spaceIcon\": \"أيقونة\",\n    \"dangerZone\": \"منطقة الخطر\",\n    \"unableToDeleteLastSpace\": \"غير قادر على حذف المساحة الأخيرة\",\n    \"unableToDeleteSpaceNotCreatedByYou\": \"غير قادر على حذف المساحات التي أنشأها الآخرون\",\n    \"enableSpacesForYourWorkspace\": \"تمكين المساحات لمساحة العمل الخاصة بك\",\n    \"title\": \"المساحات\",\n    \"defaultSpaceName\": \"عام\",\n    \"upgradeSpaceTitle\": \"تمكين المساحات\",\n    \"upgradeSpaceDescription\": \"قم بإنشاء مساحات عامة وخاصة متعددة لتنظيم مساحة عملك بشكل أفضل.\",\n    \"upgrade\": \"تحديث\",\n    \"upgradeYourSpace\": \"إنشاء مساحات متعددة\",\n    \"quicklySwitch\": \"انتقل بسرعة إلى المساحة التالية\",\n    \"duplicate\": \"مساحة مكررة\",\n    \"movePageToSpace\": \"نقل الصفحة إلى المساحة\",\n    \"cannotMovePageToDatabase\": \"لا يمكن نقل الصفحة إلى قاعدة البيانات\",\n    \"switchSpace\": \"تبديل المساحة\",\n    \"spaceNameCannotBeEmpty\": \"لا يمكن أن يكون اسم المساحة فارغًا\",\n    \"success\": {\n      \"deleteSpace\": \"تم حذف المساحة بنجاح\",\n      \"renameSpace\": \"تم إعادة تسمية المساحة بنجاح\",\n      \"duplicateSpace\": \"تم تكرار المساحة بنجاح\",\n      \"updateSpace\": \"تم تحديث المساحة بنجاح\"\n    },\n    \"error\": {\n      \"deleteSpace\": \"فشل في حذف المساحة\",\n      \"renameSpace\": \"فشل في إعادة تسمية المساحة\",\n      \"duplicateSpace\": \"فشل في تكرار المساحة\",\n      \"updateSpace\": \"فشل في تحديث المساحة\"\n    },\n    \"createSpace\": \"إنشاء مساحة\",\n    \"manageSpace\": \"إدارة المساحة\",\n    \"renameSpace\": \"إعادة تسمية المساحة\",\n    \"mSpaceIconColor\": \"لون أيقونة المساحة\",\n    \"mSpaceIcon\": \"أيقونة المساحة\"\n  },\n  \"publish\": {\n    \"hasNotBeenPublished\": \"لم يتم نشر هذه الصفحة بعد\",\n    \"spaceHasNotBeenPublished\": \"لم يتم دعم نشر المساحة بعد\",\n    \"reportPage\": \"صفحة التقرير\",\n    \"databaseHasNotBeenPublished\": \"لم يتم دعم نشر قاعدة البيانات بعد.\",\n    \"createdWith\": \"تم إنشاؤه باستخدام\",\n    \"downloadApp\": \"تنزيل AppFlowy\",\n    \"copy\": {\n      \"codeBlock\": \"تم نسخ محتوى كتلة الكود إلى الحافظة\",\n      \"imageBlock\": \"تم نسخ رابط الصورة إلى الحافظة\",\n      \"mathBlock\": \"تم نسخ المعادلة الرياضية إلى الحافظة\",\n      \"fileBlock\": \"تم نسخ رابط الملف إلى الحافظة\"\n    },\n    \"containsPublishedPage\": \"تحتوي هذه الصفحة على صفحة واحدة أو أكثر منشورة. إذا تابعت، فسيتم إلغاء نشرها. هل تريد متابعة الحذف؟\",\n    \"publishSuccessfully\": \"تم النشر بنجاح\",\n    \"unpublishSuccessfully\": \"تم إلغاء النشر بنجاح\",\n    \"publishFailed\": \"فشل في النشر\",\n    \"unpublishFailed\": \"فشل في إلغاء النشر\",\n    \"noAccessToVisit\": \"لا يمكن الوصول إلى هذه الصفحة...\",\n    \"createWithAppFlowy\": \"إنشاء موقع ويب مع AppFlowy\",\n    \"fastWithAI\": \"سريع وسهل مع الذكاء الاصطناعي.\",\n    \"tryItNow\": \"جربها الآن\",\n    \"onlyGridViewCanBePublished\": \"لا يمكن نشر سوى عرض الشبكة\",\n    \"database\": {\n      \"zero\": \"نشر {} عرض محدد\",\n      \"one\": \"نشر {} عروض محددة\",\n      \"many\": \"نشر {} عروض محددة\",\n      \"other\": \"نشر {} عروض محددة\"\n    },\n    \"mustSelectPrimaryDatabase\": \"يجب تحديد العرض الأساسي\",\n    \"noDatabaseSelected\": \"لم يتم تحديد أي قاعدة بيانات، يرجى تحديد قاعدة بيانات واحدة على الأقل.\",\n    \"unableToDeselectPrimaryDatabase\": \"غير قادر على إلغاء تحديد قاعدة البيانات الأساسية\",\n    \"saveThisPage\": \"ابدأ بهذا القالب\",\n    \"duplicateTitle\": \"أين تريد أن تضيف\",\n    \"selectWorkspace\": \"حدد مساحة العمل\",\n    \"addTo\": \"أضف إلى\",\n    \"duplicateSuccessfully\": \"تمت إضافته إلى مساحة العمل الخاصة بك\",\n    \"duplicateSuccessfullyDescription\": \"لم تقم بتثبيت AppFlowy؟ سيبدأ التنزيل تلقائيًا بعد النقر فوق \\\"تنزيل\\\".\",\n    \"downloadIt\": \"تنزيل\",\n    \"openApp\": \"افتح في التطبيق\",\n    \"duplicateFailed\": \"مكررة فشلت\",\n    \"membersCount\": {\n      \"zero\": \"لا يوجد أعضاء\",\n      \"one\": \"1 عضو\",\n      \"many\": \"{count} عضوا\",\n      \"other\": \"{count} عضوا\"\n    },\n    \"useThisTemplate\": \"استخدم القالب\"\n  },\n  \"web\": {\n    \"continue\": \"متابعة\",\n    \"or\": \"أو\",\n    \"continueWithGoogle\": \"متابعة مع جوجل\",\n    \"continueWithGithub\": \"متابعة مع GitHub\",\n    \"continueWithDiscord\": \"متابعة مع Discord\",\n    \"continueWithApple\": \"متابعة مع Apple \",\n    \"moreOptions\": \"المزيد من الخيارات\",\n    \"collapse\": \"طي\",\n    \"signInAgreement\": \"بالنقر فوق \\\"متابعة\\\" أعلاه، فإنك توافق على شروط استخدام AppFlowy\",\n    \"signInLocalAgreement\": \"من خلال النقر على \\\"البدء\\\" أعلاه، فإنك توافق على شروط وأحكام AppFlowy\",\n    \"and\": \"و\",\n    \"termOfUse\": \"شروط\",\n    \"privacyPolicy\": \"سياسة الخصوصية\",\n    \"signInError\": \"خطأ في تسجيل الدخول\",\n    \"login\": \"سجل أو قم بتسجيل الدخول\",\n    \"fileBlock\": {\n      \"uploadedAt\": \"تم الرفع في {time}\",\n      \"linkedAt\": \"تمت إضافة الرابط في {time}\",\n      \"empty\": \"رفع أو تضمين ملف\",\n      \"uploadFailed\": \"فشل الرفع، يرجى المحاولة مرة أخرى\",\n      \"retry\": \"إعادة المحاولة\"\n    },\n    \"importNotion\": \"الاستيراد من Notion\",\n    \"import\": \"الاستيراد\",\n    \"importSuccess\": \"تم الرفع بنجاح\",\n    \"importSuccessMessage\": \"سنخطرك عند اكتمال عملية الاستيراد. بعد ذلك، يمكنك عرض الصفحات المستوردة في الشريط الجانبي.\",\n    \"importFailed\": \"فشل الاستيراد، يرجى التحقق من تنسيق الملف\",\n    \"dropNotionFile\": \"قم بإسقاط ملف Notion zip الخاص بك هنا للرفع، أو انقر للاستعراض\",\n    \"error\": {\n      \"pageNameIsEmpty\": \"اسم الصفحة فارغ، الرجاء تجربة صفحة أخرى\"\n    }\n  },\n  \"globalComment\": {\n    \"comments\": \"تعليقات\",\n    \"addComment\": \"أضف تعليق\",\n    \"reactedBy\": \"تفاعل من قبل\",\n    \"addReaction\": \"إضافة تفاعل\",\n    \"reactedByMore\": \"و {count} آخرين\",\n    \"showSeconds\": {\n      \"one\": \"منذ ثانية واحدة\",\n      \"other\": \"منذ {count} ثانية\",\n      \"zero\": \"الآن\",\n      \"many\": \"منذ {count} ثانية\"\n    },\n    \"showMinutes\": {\n      \"one\": \"منذ دقيقة واحدة\",\n      \"other\": \"منذ {count} دقيقة\",\n      \"many\": \"منذ {count} دقيقة\"\n    },\n    \"showHours\": {\n      \"one\": \"منذ ساعة واحدة\",\n      \"other\": \"منذ {count} ساعة\",\n      \"many\": \"منذ {count} ساعة\"\n    },\n    \"showDays\": {\n      \"one\": \"منذ يوم واحد\",\n      \"other\": \"منذ {count} يوم\",\n      \"many\": \"منذ {count} يوم\"\n    },\n    \"showMonths\": {\n      \"one\": \"منذ شهر واحد\",\n      \"other\": \"منذ {count} شهر\",\n      \"many\": \"منذ {count} شهر\"\n    },\n    \"showYears\": {\n      \"one\": \"منذ سنة واحدة\",\n      \"other\": \"منذ {count} سنة\",\n      \"many\": \"منذ {count} سنة\"\n    },\n    \"reply\": \"رد\",\n    \"deleteComment\": \"حذف التعليق\",\n    \"youAreNotOwner\": \"أنت لست صاحب هذا التعليق\",\n    \"confirmDeleteDescription\": \"هل أنت متأكد أنك تريد حذف هذا التعليق؟\",\n    \"hasBeenDeleted\": \"تم الحذف\",\n    \"replyingTo\": \"الرد على\",\n    \"noAccessDeleteComment\": \"لا يجوز لك حذف هذا التعليق\",\n    \"collapse\": \"طي\",\n    \"readMore\": \"اقرأ المزيد\",\n    \"failedToAddComment\": \"فشل في إضافة التعليق\",\n    \"commentAddedSuccessfully\": \"تمت إضافة التعليق بنجاح.\",\n    \"commentAddedSuccessTip\": \"لقد قمت للتو بإضافة تعليق أو الرد عليه. هل ترغب في الانتقال إلى الأعلى لمشاهدة أحدث التعليقات؟\"\n  },\n  \"template\": {\n    \"asTemplate\": \"حفظ كقالب\",\n    \"name\": \"اسم القالب\",\n    \"description\": \"وصف القالب\",\n    \"about\": \"قالب حول\",\n    \"deleteFromTemplate\": \"حذف من القوالب\",\n    \"preview\": \"معاينة القالب\",\n    \"categories\": \"فئات القوالب\",\n    \"isNewTemplate\": \"PIN إلى قالب جديد\",\n    \"featured\": \"PIN إلى المميز\",\n    \"relatedTemplates\": \"القوالب ذات الصلة\",\n    \"requiredField\": \"{field} مطلوب\",\n    \"addCategory\": \"أضف \\\"{category}\\\"\",\n    \"addNewCategory\": \"إضافة فئة جديدة\",\n    \"addNewCreator\": \"إضافة مبدع جديد\",\n    \"deleteCategory\": \"حذف الفئة\",\n    \"editCategory\": \"تعديل الفئة\",\n    \"editCreator\": \"مبدع التعديل\",\n    \"category\": {\n      \"name\": \"اسم الفئة\",\n      \"icon\": \"أيقونة الفئة\",\n      \"bgColor\": \"لون خلفية الفئة\",\n      \"priority\": \"أولوية الفئة\",\n      \"desc\": \"وصف الفئة\",\n      \"type\": \"نوع الفئة\",\n      \"icons\": \"أيقونات الفئة\",\n      \"colors\": \"فئة الألوان\",\n      \"byUseCase\": \"حسب حالة الاستخدام\",\n      \"byFeature\": \"حسب الميزة\",\n      \"deleteCategory\": \"حذف الفئة\",\n      \"deleteCategoryDescription\": \"هل أنت متأكد أنك تريد حذف هذه الفئة؟\",\n      \"typeToSearch\": \"اكتب للبحث عن الفئات...\"\n    },\n    \"creator\": {\n      \"label\": \"مبدع القالب\",\n      \"name\": \"اسم المنشئ\",\n      \"avatar\": \"الصورة الرمزية للمبدع\",\n      \"accountLinks\": \"روابط حسابات المبدع\",\n      \"uploadAvatar\": \"انقر هنا لتحميل الصورة الرمزية\",\n      \"deleteCreator\": \"حذف المبدع\",\n      \"deleteCreatorDescription\": \"هل أنت متأكد أنك تريد حذف هذا المبدع؟\",\n      \"typeToSearch\": \"اكتب للبحث عن المبدعين...\"\n    },\n    \"uploadSuccess\": \"تم تحميل القالب بنجاح\",\n    \"uploadSuccessDescription\": \"لقد تم رفع القالب الخاص بك بنجاح. يمكنك الآن عرضه في معرض القوالب.\",\n    \"viewTemplate\": \"عرض القالب\",\n    \"deleteTemplate\": \"حذف القالب\",\n    \"deleteSuccess\": \"تم حذف القالب بنجاح\",\n    \"deleteTemplateDescription\": \"لن يؤثر هذا على الصفحة الحالية أو حالة النشر. هل أنت متأكد من أنك تريد حذف هذا القالب؟\",\n    \"addRelatedTemplate\": \"إضافة قالب ذو صلة\",\n    \"removeRelatedTemplate\": \"إزالة القالب ذو الصلة\",\n    \"uploadAvatar\": \"رفع الصورة الرمزية\",\n    \"searchInCategory\": \"البحث في {category}\",\n    \"label\": \"القوالب\"\n  },\n  \"fileDropzone\": {\n    \"dropFile\": \"انقر أو اسحب الملف إلى هذه المنطقة لتحميله\",\n    \"uploading\": \"جاري الرفع...\",\n    \"uploadFailed\": \"فشل الرفع\",\n    \"uploadSuccess\": \"تم الرفع بنجاح\",\n    \"uploadSuccessDescription\": \"تم رفع الملف بنجاح\",\n    \"uploadFailedDescription\": \"فشل رفع الملف\",\n    \"uploadingDescription\": \"جاري رفع الملف\"\n  },\n  \"gallery\": {\n    \"preview\": \"افتح في شاشة كاملة\",\n    \"copy\": \"نسخ\",\n    \"download\": \"تنزيل\",\n    \"prev\": \"السابق\",\n    \"next\": \"التالي\",\n    \"resetZoom\": \"إعادة ضبط التكبير\",\n    \"zoomIn\": \"التكبير \",\n    \"zoomOut\": \"التصغير\"\n  },\n  \"invitation\": {\n    \"join\": \"الانضمام\",\n    \"on\": \"على\",\n    \"invitedBy\": \"بدعوة من\",\n    \"membersCount\": {\n      \"zero\": \"{count} عضوا\",\n      \"one\": \"{count} عضو\",\n      \"many\": \"{count} عضوا\",\n      \"other\": \"{count} عضوا\"\n    },\n    \"tip\": \"لقد تمت دعوتك للانضمام إلى مساحة العمل هذه باستخدام معلومات الاتصال أدناه. إذا كانت هذه المعلومات غير صحيحة، فاتصل بالمسؤول لإعادة إرسال الدعوة.\",\n    \"joinWorkspace\": \"انضم إلى مساحة العمل\",\n    \"success\": \"لقد انضممت بنجاح إلى مساحة العمل\",\n    \"successMessage\": \"يمكنك الآن الوصول إلى كافة الصفحات ومساحات العمل الموجودة بداخله.\",\n    \"openWorkspace\": \"افتح AppFlowy\",\n    \"alreadyAccepted\": \"لقد قبلت الدعوة بالفعل\",\n    \"errorModal\": {\n      \"title\": \"لقد حدث خطأ ما\",\n      \"description\": \"قد لا يكون لحسابك الحالي {email} حق الوصول إلى مساحة العمل هذه. يرجى تسجيل الدخول بالحساب الصحيح أو الاتصال بمالك مساحة العمل للحصول على المساعدة.\",\n      \"contactOwner\": \"اتصل بالمالك\",\n      \"close\": \"العودة إلى الرئيسية\",\n      \"changeAccount\": \"تغيير الحساب\"\n    }\n  },\n  \"requestAccess\": {\n    \"title\": \"لا يمكن الوصول إلى هذه الصفحة\",\n    \"subtitle\": \"يمكنك طلب الوصول من مالك هذه الصفحة. بمجرد الموافقة، يمكنك عرض الصفحة.\",\n    \"requestAccess\": \"طلب الوصول\",\n    \"backToHome\": \"العودة إلى الرئيسية\",\n    \"tip\": \"لقد قمت بتسجيل الدخول حاليًا باسم<link/> .\",\n    \"mightBe\": \"قد تحتاج إلى<login/> مع حساب مختلف.\",\n    \"successful\": \"تم إرسال الطلب بنجاح\",\n    \"successfulMessage\": \"سيتم إعلامك بمجرد موافقة المالك على طلبك.\",\n    \"requestError\": \"فشل في طلب الوصول\",\n    \"repeatRequestError\": \"لقد طلبت بالفعل الوصول إلى هذه الصفحة\"\n  },\n  \"approveAccess\": {\n    \"title\": \"الموافقة على طلب الانضمام إلى مساحة العمل\",\n    \"requestSummary\": \"<user/>طلبات الانضمام<workspace/> والوصول<page/>\",\n    \"upgrade\": \"ترقية\",\n    \"downloadApp\": \"تنزيل AppFlowy\",\n    \"approveButton\": \"موافقة\",\n    \"approveSuccess\": \"تمت الموافقة بنجاح\",\n    \"approveError\": \"فشل في الموافقة، تأكد من عدم تجاوز حد خطة مساحة العمل\",\n    \"getRequestInfoError\": \"فشل في الحصول على معلومات الطلب\",\n    \"memberCount\": {\n      \"zero\": \"لا يوجد أعضاء\",\n      \"one\": \"عضو واحد\",\n      \"many\": \"{count} عضوا\",\n      \"other\": \"{count} عضوا\"\n    },\n    \"alreadyProTitle\": \"لقد وصلت إلى الحد الأقصى لخطة مساحة العمل\",\n    \"alreadyProMessage\": \"اطلب منهم الاتصال<email/> لإلغاء تأمين المزيد من الأعضاء\",\n    \"repeatApproveError\": \"لقد وافقت بالفعل على هذا الطلب\",\n    \"ensurePlanLimit\": \"تأكد من عدم تجاوز حد خطة مساحة العمل. إذا تم تجاوز الحد، ففكر في<upgrade/> خطة مساحة العمل أو<download/> .\",\n    \"requestToJoin\": \"طلب الانضمام\",\n    \"asMember\": \"كعضو\"\n  },\n  \"upgradePlanModal\": {\n    \"title\": \"الترقية إلى الإصدار الاحترافي\",\n    \"message\": \"وصل {name} إلى الحد الأقصى للعضوية المجانية. قم بالترقية إلى الخطة الاحترافية لدعوة المزيد من الأعضاء.\",\n    \"upgradeSteps\": \"كيفية ترقية خطتك على AppFlowy:\",\n    \"step1\": \"1. انتقل إلى الإعدادات\",\n    \"step2\": \"2. انقر فوق \\\"الخطة\\\"\",\n    \"step3\": \"3. حدد \\\"تغيير الخطة\\\"\",\n    \"appNote\": \"ملحوظة: \",\n    \"actionButton\": \"ترقية\",\n    \"downloadLink\": \"تنزيل التطبيق\",\n    \"laterButton\": \"لاحقاً\",\n    \"refreshNote\": \"بعد الترقية الناجحة، انقر فوق<refresh/> لتفعيل ميزاتك الجديدة.\",\n    \"refresh\": \"هنا\"\n  },\n  \"breadcrumbs\": {\n    \"label\": \"مسارات التنقل\"\n  },\n  \"time\": {\n    \"justNow\": \"الآن\",\n    \"seconds\": {\n      \"one\": \"ثانية واحدة\",\n      \"other\": \"{count} من الثواني\"\n    },\n    \"minutes\": {\n      \"one\": \"دقيقة واحدة\",\n      \"other\": \"{count} من الدقائق\"\n    },\n    \"hours\": {\n      \"one\": \"ساعة واحدة\",\n      \"other\": \"{count} من الساعات\"\n    },\n    \"days\": {\n      \"one\": \"يوم واحد\",\n      \"other\": \"{count} من الأيام\"\n    },\n    \"weeks\": {\n      \"one\": \"اسبوع واحد\",\n      \"other\": \"{count} من الأسابيع\"\n    },\n    \"months\": {\n      \"one\": \"شهر واحد\",\n      \"other\": \"{count} من الاشهر\"\n    },\n    \"years\": {\n      \"one\": \"سنة واحدة\",\n      \"other\": \"{count} من السنوات\"\n    },\n    \"ago\": \"منذ\",\n    \"yesterday\": \"أمس\",\n    \"today\": \"اليوم\"\n  },\n  \"members\": {\n    \"zero\": \"لا يوجد أعضاء\",\n    \"one\": \"عضو واحد\",\n    \"many\": \"{count} من الأعضاء\",\n    \"other\": \"{count} من الأعضاء\"\n  },\n  \"tabMenu\": {\n    \"close\": \"إغلاق\",\n    \"closeDisabledHint\": \"لا يمكن إغلاق علامة تبويب مثبتة، يرجى إلغاء التثبيت أولاً\",\n    \"closeOthers\": \"إغلاق علامات التبويب الأخرى\",\n    \"closeOthersHint\": \"سيؤدي هذا إلى إغلاق جميع علامات التبويب غير المثبتة باستثناء هذه العلامة\",\n    \"closeOthersDisabledHint\": \"تم تثبيت جميع علامات التبويب، ولا يمكن العثور على أي علامات تبويب لإغلاقها\",\n    \"favorite\": \"مفضل\",\n    \"unfavorite\": \"غير مفضل\",\n    \"favoriteDisabledHint\": \"لا يمكن إضافة هذا العرض إلى المفضلة\",\n    \"pinTab\": \"تثبيت\",\n    \"unpinTab\": \"إزالة التثبيت\"\n  },\n  \"openFileMessage\": {\n    \"success\": \"تم فتح الملف بنجاح\",\n    \"fileNotFound\": \"لم يتم العثور على الملف\",\n    \"noAppToOpenFile\": \"لا يوجد تطبيق لفتح هذا الملف\",\n    \"permissionDenied\": \"لا يوجد إذن لفتح هذا الملف\",\n    \"unknownError\": \"فشل فتح الملف\"\n  },\n  \"inviteMember\": {\n    \"requestInviteMembers\": \"دعوة إلى مكان مساحة عملك\",\n    \"inviteFailedMemberLimit\": \"لقد تم الوصول إلى الحد الأقصى للأعضاء، يرجى \",\n    \"upgrade\": \"ترقية\",\n    \"addEmail\": \"email@example.com، email2@example.com...\",\n    \"requestInvites\": \"إرسال الدعوات\",\n    \"inviteAlready\": \"لقد قمت بالفعل بدعوة هذا البريد الإلكتروني: {email}\",\n    \"inviteSuccess\": \"تم إرسال الدعوة بنجاح\",\n    \"description\": \"أدخل رسائل البريد الإلكتروني أدناه مع وضع فاصلة بينها. تعتمد الرسوم على عدد الأعضاء.\",\n    \"emails\": \"بريد إلكتروني\"\n  },\n  \"quickNote\": {\n    \"label\": \"ملاحظة سريعة\",\n    \"quickNotes\": \"ملاحظات سريعة\",\n    \"search\": \"بحث ملاحظات سريعة\",\n    \"collapseFullView\": \"طي العرض الكامل\",\n    \"expandFullView\": \"توسيع العرض الكامل\",\n    \"createFailed\": \"فشل في إنشاء ملاحظة سريعة\",\n    \"quickNotesEmpty\": \"لا توجد ملاحظات سريعة\",\n    \"emptyNote\": \"ملاحظة فارغة\",\n    \"deleteNotePrompt\": \"سيتم حذف الملاحظة المحددة بشكل دائم. هل أنت متأكد من أنك تريد حذفها؟\",\n    \"addNote\": \"ملاحظة جديدة\",\n    \"noAdditionalText\": \"لا يوجد نص إضافي\"\n  },\n  \"subscribe\": {\n    \"upgradePlanTitle\": \"قارن واختر الخطة\",\n    \"yearly\": \"سنوي\",\n    \"save\": \"وفر {discount}%\",\n    \"monthly\": \"شهريا\",\n    \"priceIn\": \"السعر في \",\n    \"free\": \"مجاني\",\n    \"pro\": \"احترافي\",\n    \"freeDescription\": \"للأفراد حتى عضوين لتنظيم كل شيء\",\n    \"proDescription\": \"للفرق الصغيرة لإدارة المشاريع ومعرفة الفريق\",\n    \"proDuration\": {\n      \"monthly\": \"لكل عضو شهريا\\nيتم دفع الفاتورة شهريا\",\n      \"yearly\": \"لكل عضو شهريا\\nيتم دفعها سنويا\"\n    },\n    \"cancel\": \"يرجع إلى إصدار أقدم\",\n    \"changePlan\": \"الترقية إلى الخطة الاحترافية\",\n    \"everythingInFree\": \"كل شيء مجاني +\",\n    \"currentPlan\": \"الحالي\",\n    \"freeDuration\": \"للأبد\",\n    \"freePoints\": {\n      \"first\": \"مساحة عمل تعاونية واحدة تتسع لعضوين كحد أقصى\",\n      \"second\": \"صفحات وكتل غير محدودة\",\n      \"three\": \"سعة تخزين 5 جيجا بايت\",\n      \"four\": \"البحث الذكي\",\n      \"five\": \"20 استجابة الذكاء الاصطناعي\",\n      \"six\": \"تطبيق الجوال\",\n      \"seven\": \"التعاون في الوقت الحقيقي\"\n    },\n    \"proPoints\": {\n      \"first\": \"تخزين غير محدود\",\n      \"second\": \"ما يصل إلى 10 أعضاء في مساحة العمل\",\n      \"three\": \"استجابات الذكاء الاصطناعي غير المحدودة\",\n      \"four\": \"رفع ملفات غير محدودة\",\n      \"five\": \"مساحة اسم مخصصة\"\n    },\n    \"cancelPlan\": {\n      \"title\": \"نأسف على مغادرتك\",\n      \"success\": \"لقد تم إلغاء اشتراكك بنجاح\",\n      \"description\": \"يؤسفنا رحيلك. يسعدنا سماع تعليقاتك لمساعدتنا على تحسين AppFlowy. يُرجى تخصيص بعض الوقت للإجابة على بعض الأسئلة.\",\n      \"commonOther\": \"آخر\",\n      \"otherHint\": \"اكتب إجابتك هنا\",\n      \"questionOne\": {\n        \"question\": \"ما الذي دفعك إلى إلغاء اشتراكك في AppFlowy Pro؟\",\n        \"answerOne\": \"التكلفة مرتفعة للغاية\",\n        \"answerTwo\": \"الميزات لم ترق إلى مستوى التوقعات\",\n        \"answerThree\": \"وجدت بديلا أفضل\",\n        \"answerFour\": \"لم أستخدمه بشكل كافي لتبرير التكلفة\",\n        \"answerFive\": \"مشكلة في الخدمة أو صعوبات فنية\"\n      },\n      \"questionTwo\": {\n        \"question\": \"ما مدى احتمالية تفكيرك في إعادة الاشتراك في AppFlowy Pro في المستقبل؟\",\n        \"answerOne\": \"من المرجح جدًا\",\n        \"answerTwo\": \"من المحتمل إلى حد ما\",\n        \"answerThree\": \"غير متأكد\",\n        \"answerFour\": \"من غير المحتمل\",\n        \"answerFive\": \"من غير المحتمل جدًا\"\n      },\n      \"questionThree\": {\n        \"question\": \"ما هي الميزة الاحترافية التي تقدرها أكثر أثناء اشتراكك؟\",\n        \"answerOne\": \"التعاون بين المستخدمين المتعددين\",\n        \"answerTwo\": \"سجل تاريخ الإصدارات لفترة أطول\",\n        \"answerThree\": \"استجابات الذكاء الاصطناعي غير المحدودة\",\n        \"answerFour\": \"الوصول إلى نماذج الذكاء الاصطناعي المحلية\"\n      },\n      \"questionFour\": {\n        \"question\": \"كيف تصف تجربتك الشاملة مع AppFlowy؟\",\n        \"answerOne\": \"عظيمة\",\n        \"answerTwo\": \"جيدة\",\n        \"answerThree\": \"متوسطة\",\n        \"answerFour\": \"أقل من المتوسط\",\n        \"answerFive\": \"غير راضٍ\"\n      }\n    }\n  },\n  \"ai\": {\n    \"contentPolicyViolation\": \"فشل إنشاء الصورة بسبب محتوى حساس. يرجى إعادة صياغة إدخالك والمحاولة مرة أخرى\",\n    \"textLimitReachedDescription\": \"لقد نفدت الاستجابات المجانية للذكاء الاصطناعي من مساحة عملك. قم بالترقية إلى الخطة الاحترافية أو قم بشراء إضافة للذكاء الاصطناعي لفتح عدد غير محدود من الاستجابات\",\n    \"imageLimitReachedDescription\": \"لقد استنفدت حصتك المجانية من صور الذكاء الاصطناعي. يُرجى الترقية إلى الخطة الاحترافية أو شراء إضافة الذكاء الاصطناعي لفتح عدد غير محدود من الاستجابات\",\n    \"limitReachedAction\": {\n      \"textDescription\": \"لقد نفدت الاستجابات المجانية للذكاء الاصطناعي من مساحة عملك. للحصول على المزيد من الاستجابات، يرجى\",\n      \"imageDescription\": \"لقد استنفدت حصتك المجانية من صور الذكاء الاصطناعي. يرجى\",\n      \"upgrade\": \"ترقية\",\n      \"toThe\": \"الى\",\n      \"proPlan\": \"الخطة الاحترافية\",\n      \"orPurchaseAn\": \"أو شراء\",\n      \"aiAddon\": \"مَرافِق الذكاء الاصطناعي\"\n    },\n    \"editing\": \"تحرير\",\n    \"analyzing\": \"تحليل\",\n    \"continueWritingEmptyDocumentTitle\": \"استمر في كتابة الخطأ\",\n    \"continueWritingEmptyDocumentDescription\": \"نواجه مشكلة في توسيع نطاق المحتوى في مستندك. اكتب مقدمة قصيرة وسنتولى الأمر من هناك!\",\n    \"more\": \"أكثر\",\n    \"customPrompt\": {\n      \"browsePrompts\": \"تصفح المطالبات\",\n      \"usePrompt\": \"استخدم المطالبة\",\n      \"featured\": \"مميز\",\n      \"example\": \"مثال المطالبة\",\n      \"all\": \"الجميع\",\n      \"development\": \"التطوير\",\n      \"writing\": \"الكتابة\",\n      \"healthAndFitness\": \"الصحة واللياقة البدنية\",\n      \"business\": \"العمل\",\n      \"marketing\": \"التسويق\",\n      \"travel\": \"السفر\",\n      \"others\": \"آخر\",\n      \"prompt\": \"المطالبة\",\n      \"sampleOutput\": \"عينة الإخراج\",\n      \"contentSeo\": \"المحتوى/ت.م.ب\",\n      \"emailMarketing\": \"التسويق عبر البريد الإلكتروني\",\n      \"paidAds\": \"الإعلانات المدفوعة\",\n      \"prCommunication\": \"العلاقات العامة/الاتصالات\",\n      \"recruiting\": \"التوظيف\",\n      \"sales\": \"مبيعات\",\n      \"socialMedia\": \"وسائل التواصل الاجتماعي\",\n      \"strategy\": \"الاستراتيجية\",\n      \"caseStudies\": \"دراسات الحالة\",\n      \"salesCopy\": \"النص البيعي\",\n      \"education\": \"التعليم\",\n      \"work\": \"العمل\",\n      \"podcastProduction\": \"إنتاج البودكاست\",\n      \"copyWriting\": \"كتابة التسويق\",\n      \"customerSuccess\": \"نجاح العملاء\"\n    }\n  },\n  \"autoUpdate\": {\n    \"criticalUpdateTitle\": \"التحديث ضروري للمتابعة\",\n    \"criticalUpdateDescription\": \"لقد أجرينا تحسينات لتحسين تجربتك! يُرجى التحديث من {currentVersion} إلى {newVersion} لمواصلة استخدام التطبيق.\",\n    \"criticalUpdateButton\": \"تحديث\",\n    \"bannerUpdateTitle\": \"النسخة الجديدة متاحة!\",\n    \"bannerUpdateDescription\": \"احصل على أحدث الميزات والإصلاحات. انقر على \\\"تحديث\\\" للتثبيت الآن.\",\n    \"bannerUpdateButton\": \"تحديث\",\n    \"settingsUpdateTitle\": \"الإصدار الجديد ({newVersion}) متاح!\",\n    \"settingsUpdateDescription\": \"الإصدار الحالي: {currentVersion} (الإصدار الرسمي) → {newVersion}\",\n    \"settingsUpdateButton\": \"تحديث\",\n    \"settingsUpdateWhatsNew\": \"ما الجديد\"\n  },\n  \"lockPage\": {\n    \"lockPage\": \"مقفل\",\n    \"reLockPage\": \"إعادة القفل\",\n    \"lockTooltip\": \"تم قفل الصفحة لمنع التعديل غير المقصود. انقر لفتح القفل.\",\n    \"pageLockedToast\": \"الصفحة مقفلة. التعديل معطل حتى يفتحها أحد.\",\n    \"lockedOperationTooltip\": \"تم قفل الصفحة لمنع التعديل غير المقصود.\"\n  },\n  \"suggestion\": {\n    \"accept\": \"يقبل\",\n    \"keep\": \"يحفظ\",\n    \"discard\": \"تجاهل\",\n    \"close\": \"يغلق\",\n    \"tryAgain\": \"حاول ثانية\",\n    \"rewrite\": \"إعادة كتابة\",\n    \"insertBelow\": \"أدخل أدناه\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/ca-ES.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Jo\",\n  \"welcomeText\": \"Benvingut a @:appName\",\n  \"welcomeTo\": \"Benvingut a\",\n  \"githubStarText\": \"Preferit a Github\",\n  \"subscribeNewsletterText\": \"Subscriu-me al butlletí\",\n  \"letsGoButtonText\": \"Endavant\",\n  \"title\": \"Títol\",\n  \"youCanAlso\": \"Tu pots també\",\n  \"and\": \"i\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Feu clic per afegir a continuació\",\n    \"addAboveCmd\": \"Alt+clic\",\n    \"addAboveMacCmd\": \"Opció+clic\",\n    \"addAboveTooltip\": \"afegir a dalt\",\n    \"dragTooltip\": \"Arrossegueu per moure's\",\n    \"openMenuTooltip\": \"Feu clic per obrir el menú\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Registra't\",\n    \"title\": \"Registra't a @:appName\",\n    \"getStartedText\": \"Comencem\",\n    \"emptyPasswordError\": \"La contrasenya no pot ser buida\",\n    \"repeatPasswordEmptyError\": \"La contrasenya repetida no pot ser buida\",\n    \"unmatchedPasswordError\": \"Les contrasenyes no concorden\",\n    \"alreadyHaveAnAccount\": \"Ja tens un compte?\",\n    \"emailHint\": \"Correu electrònic\",\n    \"passwordHint\": \"Contrasenya\",\n    \"repeatPasswordHint\": \"Repeteix la contrasenya\",\n    \"signUpWith\": \"Registra't amb:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Inicia sessió a @:appName\",\n    \"loginButtonText\": \"Inicia sessió\",\n    \"loginStartWithAnonymous\": \"Comenceu amb una sessió anònima\",\n    \"continueAnonymousUser\": \"Continueu amb una sessió anònima\",\n    \"buttonText\": \"Inicia sessió\",\n    \"signingInText\": \"S'està iniciant la sessió...\",\n    \"forgotPassword\": \"Has oblidat la contrasenya?\",\n    \"emailHint\": \"Correu electrònic\",\n    \"passwordHint\": \"Contrasenya\",\n    \"dontHaveAnAccount\": \"No tens un compte?\",\n    \"repeatPasswordEmptyError\": \"La contrasenya repetida no pot ser buida\",\n    \"unmatchedPasswordError\": \"Les contrasenyes no concorden\",\n    \"or\": \"O\",\n    \"signInWith\": \"Inicia sessió amb:\",\n    \"LogInWithGoogle\": \"Inicieu sessió amb Google\",\n    \"LogInWithGithub\": \"Inicieu sessió amb Github\",\n    \"LogInWithDiscord\": \"Inicieu sessió amb Discord\",\n    \"loginAsGuestButtonText\": \"Començar\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Tria el teu espai de treball\",\n    \"create\": \"Crear un espai de treball\",\n    \"reset\": \"Restableix l'espai de treball\",\n    \"hint\": \"espai de treball\",\n    \"notFoundError\": \"No s'ha trobat l'espai de treball\",\n    \"errorActions\": {\n      \"reportIssue\": \"Informar d'un problema\",\n      \"reachOut\": \"Posa't en contacte amb Discord\"\n    }\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Compartir\",\n    \"workInProgress\": \"Pròximament\",\n    \"markdown\": \"Markdown\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Copiar l'enllaç\"\n  },\n  \"moreAction\": {\n    \"small\": \"petit\",\n    \"medium\": \"mitjà\",\n    \"large\": \"gran\",\n    \"fontSize\": \"Mida de la font\",\n    \"import\": \"Importar\",\n    \"moreOptions\": \"Més opcions\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Text i rebaixa\",\n    \"documentFromV010\": \"Document de la v0.1.0\",\n    \"databaseFromV010\": \"Base de dades de la v0.1.0\",\n    \"csv\": \"CSV\",\n    \"database\": \"Base de dades\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Canviar el nom\",\n    \"delete\": \"Eliminar\",\n    \"duplicate\": \"Duplicar\",\n    \"unfavorite\": \"Elimina dels preferits\",\n    \"favorite\": \"Afegir a preferits\",\n    \"openNewTab\": \"Obre en una pestanya nova\",\n    \"moveTo\": \"Moure's cap a\",\n    \"addToFavorites\": \"Afegir a preferits\",\n    \"copyLink\": \"Copia l'enllaç\"\n  },\n  \"blankPageTitle\": \"Pàgina en blanc\",\n  \"newPageText\": \"Nova pàgina\",\n  \"newDocumentText\": \"Nou document\",\n  \"newCalendarText\": \"Nou calendari\",\n  \"newBoardText\": \"Nou tauler\",\n  \"trash\": {\n    \"text\": \"Paperera\",\n    \"restoreAll\": \"Recuperar-ho tot\",\n    \"deleteAll\": \"Eliminar-ho tot\",\n    \"pageHeader\": {\n      \"fileName\": \"Nom del fitxer\",\n      \"lastModified\": \"Última modificació\",\n      \"created\": \"Creat\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Esteu segur que suprimiu totes les pàgines de la paperera?\",\n      \"caption\": \"Aquesta acció no es pot desfer.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Esteu segur de restaurar totes les pàgines a la paperera?\",\n      \"caption\": \"Aquesta acció no es pot desfer.\"\n    },\n    \"mobile\": {\n      \"actions\": \"Accions de paperera\",\n      \"empty\": \"La paperera està buida\",\n      \"emptyDescription\": \"No tens cap fitxer suprimit\",\n      \"isDeleted\": \"está eliminat\",\n      \"isRestored\": \"es restaura\"\n    }\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Aquest pàgina es troba a la paperera\",\n    \"restore\": \"Recuperar-la\",\n    \"deletePermanent\": \"Elimina-la\"\n  },\n  \"dialogCreatePageNameHint\": \"Nom de la pàgina\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Dreceres\",\n    \"whatsNew\": \"Què hi ha de nou?\",\n    \"markdown\": \"Reducció\",\n    \"debug\": {\n      \"name\": \"Informació de depuració\",\n      \"success\": \"S'ha copiat la informació de depuració!\",\n      \"fail\": \"No es pot copiar la informació de depuració\"\n    },\n    \"feedback\": \"Feedback\",\n    \"help\": \"Ajuda i Suport\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Suprimeix, canvia el nom i més...\",\n    \"addPageTooltip\": \"Afegeix ràpidament una pàgina dins\",\n    \"defaultNewPageName\": \"Sense títol\",\n    \"renameDialog\": \"Canviar el nom\"\n  },\n  \"noPagesInside\": \"No hi ha pàgines dins\",\n  \"toolbar\": {\n    \"undo\": \"Desfer\",\n    \"redo\": \"Refer\",\n    \"bold\": \"Negreta\",\n    \"italic\": \"Cursiva\",\n    \"underline\": \"Text subratllar\",\n    \"strike\": \"Ratllat\",\n    \"numList\": \"Llista numerada\",\n    \"bulletList\": \"Llista de punts\",\n    \"checkList\": \"Llista de comprovació\",\n    \"inlineCode\": \"Inserir codi\",\n    \"quote\": \"Bloc citat\",\n    \"header\": \"Capçalera\",\n    \"highlight\": \"Subratllar\",\n    \"color\": \"Color\",\n    \"addLink\": \"Afegeix un enllaç\",\n    \"link\": \"Enllaç\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Canviar a mode clar\",\n    \"darkMode\": \"Canviar a mode fosc\",\n    \"openAsPage\": \"Obre com a pàgina\",\n    \"addNewRow\": \"Afegeix una fila nova\",\n    \"openMenu\": \"Feu clic per obrir el menú\",\n    \"dragRow\": \"Premeu llargament per reordenar la fila\",\n    \"viewDataBase\": \"Veure base de dades\",\n    \"referencePage\": \"Es fa referència a aquest {nom}\",\n    \"addBlockBelow\": \"Afegeix un bloc a continuació\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Close sidebar\",\n    \"openSidebar\": \"Open sidebar\",\n    \"personal\": \"Personal\",\n    \"favorites\": \"Preferits\",\n    \"clickToHidePersonal\": \"Feu clic per amagar la secció personal\",\n    \"clickToHideFavorites\": \"Feu clic per amagar la secció preferida\",\n    \"addAPage\": \"Afegeix una pàgina\",\n    \"recent\": \"Recent\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Nota exportada a Markdown\",\n      \"path\": \"Documents/fluides\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Contactes\",\n    \"whatsHappening\": \"Que passa aquesta setmana?\",\n    \"addContact\": \"Afegir un contacte\",\n    \"editContact\": \"Editar un contacte\"\n  },\n  \"button\": {\n    \"ok\": \"D'acord\",\n    \"done\": \"Fet\",\n    \"cancel\": \"Cancel · lar\",\n    \"signIn\": \"Iniciar sessió\",\n    \"signOut\": \"Tancar sessió\",\n    \"complete\": \"Completar\",\n    \"save\": \"Guardar\",\n    \"generate\": \"Generar\",\n    \"esc\": \"ESC\",\n    \"keep\": \"Mantenir\",\n    \"tryAgain\": \"Torna-ho a provar\",\n    \"discard\": \"Descartar\",\n    \"replace\": \"Substitueix\",\n    \"insertBelow\": \"Insereix a continuació\",\n    \"insertAbove\": \"Insereix a dalt\",\n    \"upload\": \"Carrega\",\n    \"edit\": \"Edita\",\n    \"delete\": \"Suprimeix\",\n    \"duplicate\": \"Duplicat\",\n    \"putback\": \"Posar enrere\",\n    \"update\": \"Actualització\",\n    \"share\": \"Compartir\",\n    \"removeFromFavorites\": \"Elimina dels preferits\",\n    \"addToFavorites\": \"Afegir a preferits\",\n    \"rename\": \"Renombrar\",\n    \"helpCenter\": \"Centre d'ajuda\",\n    \"add\": \"Afegir\",\n    \"yes\": \"Sí\"\n  },\n  \"label\": {\n    \"welcome\": \"Benvingut!\",\n    \"firstName\": \"Nom\",\n    \"middleName\": \"Segon Nom\",\n    \"lastName\": \"Cognom\",\n    \"stepX\": \"Pas {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"No s'ha pogut connectar al teu compte.\",\n      \"failedMsg\": \"Assegureu-vos que heu completat el procés d'inici de sessió al vostre navegador.\"\n    },\n    \"google\": {\n      \"title\": \"Iniciar sessió amb Google\",\n      \"instruction1\": \"Per importar els vostres contactes de Google, haureu d'autoritzar aquesta aplicació mitjançant el vostre navegador web.\",\n      \"instruction2\": \"Copia aquest codi clicant la icona o seleccionant el text:\",\n      \"instruction3\": \"Navega al següent enllaç amb el teu navegador i insereix el codi anterior:\",\n      \"instruction4\": \"Pressiona el botó d'avall una vegada hagis completat el registre:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Configuració\",\n    \"menu\": {\n      \"appearance\": \"Aparença\",\n      \"language\": \"Idioma\",\n      \"user\": \"Usuari\",\n      \"files\": \"Fitxers\",\n      \"notifications\": \"Notificacions\",\n      \"open\": \"Obrir la configuració\",\n      \"logout\": \"Tancar sessió\",\n      \"logoutPrompt\": \"Esteu segur de tancar la sessió?\",\n      \"selfEncryptionLogoutPrompt\": \"Esteu segur que voleu tancar la sessió? Assegureu-vos d'haver copiat el secret de xifratge\",\n      \"syncSetting\": \"Configuració de sincronització\",\n      \"cloudSettings\": \"Configuració del núvol\",\n      \"enableSync\": \"Activa la sincronització\",\n      \"enableEncrypt\": \"Xifrar dades\",\n      \"cloudURL\": \"URL base\",\n      \"invalidCloudURLScheme\": \"Esquema no vàlid\",\n      \"cloudServerType\": \"Servidor al núvol\",\n      \"cloudLocal\": \"Local\",\n      \"cloudAppFlowy\": \"@:appName Cloud Beta\",\n      \"clickToCopy\": \"Feu clic per copiar\",\n      \"selfHostContent\": \"document\",\n      \"cloudURLHint\": \"Introduïu l'URL base del vostre servidor\",\n      \"restartApp\": \"Reinicia\",\n      \"inputEncryptPrompt\": \"Introduïu el vostre secret de xifratge per a\",\n      \"clickToCopySecret\": \"Feu clic per copiar el secret\",\n      \"inputTextFieldHint\": \"El teu secret\",\n      \"importSuccess\": \"S'ha importat correctament la carpeta de dades d'@:appName\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Activa les notificacions\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"Restableix\",\n      \"fontFamily\": {\n        \"label\": \"Família de lletres\",\n        \"search\": \"Cerca\"\n      },\n      \"themeMode\": {\n        \"label\": \"Theme Mode\",\n        \"light\": \"Mode Clar\",\n        \"dark\": \"Mode Fosc\",\n        \"system\": \"Adapt to System\"\n      },\n      \"documentSettings\": {\n        \"cursorColor\": \"Color del cursor del document\",\n        \"selectionColor\": \"Color de selecció del document\",\n        \"hexEmptyError\": \"El color hexadecimal no pot estar buit\",\n        \"hexLengthError\": \"El valor hexadecimal ha de tenir 6 dígits\",\n        \"hexInvalidError\": \"Valor hexadecimal no vàlid\",\n        \"opacityEmptyError\": \"L'opacitat no pot estar buida\",\n        \"opacityRangeError\": \"L'opacitat ha d'estar entre 1 i 100\",\n        \"app\": \"App\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"Aplicar\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Direcció de maquetació\"\n      },\n      \"textDirection\": {\n        \"auto\": \"AUTO\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Carrega\",\n        \"uploadTheme\": \"Carrega el tema\",\n        \"description\": \"Carregueu el vostre propi tema @:appName amb el botó següent.\",\n        \"loading\": \"Si us plau, espereu mentre validem i carreguem el vostre tema...\",\n        \"uploadSuccess\": \"El teu tema s'ha penjat correctament\",\n        \"deletionFailure\": \"No s'ha pogut suprimir el tema. Intenta esborrar-lo manualment.\",\n        \"filePickerDialogTitle\": \"Trieu un fitxer .flowy_plugin\",\n        \"urlUploadFailure\": \"No s'ha pogut obrir l'URL: {}\",\n        \"failure\": \"El tema que s'ha penjat tenia un format no vàlid.\"\n      },\n      \"theme\": \"Tema\",\n      \"builtInsLabel\": \"Temes integrats\",\n      \"pluginsLabel\": \"Connectors\",\n      \"dateFormat\": {\n        \"label\": \"Format de data\",\n        \"local\": \"Local\",\n        \"iso\": \"ISO\",\n        \"dmy\": \"D/M/A\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Format horari\",\n        \"twelveHour\": \"Dotze hores\",\n        \"twentyFourHour\": \"Vint-i-quatre hores\"\n      }\n    },\n    \"files\": {\n      \"copy\": \"Còpia\",\n      \"defaultLocation\": \"Llegir fitxers i ubicació d'emmagatzematge de dades\",\n      \"exportData\": \"Exporteu les vostres dades\",\n      \"doubleTapToCopy\": \"Fes doble toc per copiar el camí\",\n      \"restoreLocation\": \"Restaura al camí predeterminat d'@:appName\",\n      \"customizeLocation\": \"Obriu una altra carpeta\",\n      \"restartApp\": \"Si us plau, reinicieu l'aplicació perquè els canvis tinguin efecte.\",\n      \"exportDatabase\": \"Exportar la base de dades\",\n      \"selectFiles\": \"Seleccioneu els fitxers que cal exportar\",\n      \"selectAll\": \"Seleccionar tot\",\n      \"deselectAll\": \"Deseleccionar tot\",\n      \"createNewFolder\": \"Creeu una carpeta nova\",\n      \"createNewFolderDesc\": \"Digueu-nos on voleu emmagatzemar les vostres dades\",\n      \"defineWhereYourDataIsStored\": \"Definiu on s'emmagatzemen les vostres dades\",\n      \"open\": \"Obert\",\n      \"openFolder\": \"Obre una carpeta existent\",\n      \"openFolderDesc\": \"Llegiu-lo i escriviu-lo a la vostra carpeta @:appName existent\",\n      \"folderHintText\": \"nom de la carpeta\",\n      \"location\": \"Creació d'una carpeta nova\",\n      \"locationDesc\": \"Trieu un nom per a la vostra carpeta de dades d'@:appName\",\n      \"browser\": \"Navega\",\n      \"create\": \"Crear\",\n      \"set\": \"Conjunt\",\n      \"folderPath\": \"Camí per emmagatzemar la vostra carpeta\",\n      \"locationCannotBeEmpty\": \"El camí no pot estar buit\",\n      \"pathCopiedSnackbar\": \"S'ha copiat el camí d'emmagatzematge del fitxer al porta-retalls!\",\n      \"changeLocationTooltips\": \"Canvia el directori de dades\",\n      \"change\": \"Canviar\",\n      \"openLocationTooltips\": \"Obriu un altre directori de dades\",\n      \"openCurrentDataFolder\": \"Obre el directori de dades actual\",\n      \"recoverLocationTooltips\": \"Restableix al directori de dades predeterminat d'@:appName\",\n      \"exportFileSuccess\": \"Exporta el fitxer correctament!\",\n      \"exportFileFail\": \"Ha fallat l'exportació del fitxer!\",\n      \"export\": \"Exporta\"\n    },\n    \"user\": {\n      \"name\": \"Nom\",\n      \"email\": \"Correu electrònic\",\n      \"tooltipSelectIcon\": \"Seleccioneu la icona\",\n      \"selectAnIcon\": \"Seleccioneu una icona\",\n      \"pleaseInputYourOpenAIKey\": \"si us plau, introduïu la vostra clau AI\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"Informació personal\",\n      \"username\": \"Nom d'usuari\",\n      \"about\": \"Sobre\",\n      \"pushNotifications\": \"Notificacions push\",\n      \"support\": \"Suport\",\n      \"joinDiscord\": \"Uneix-te a nosaltres a Discord\",\n      \"privacyPolicy\": \"Política de privacitat\",\n      \"userAgreement\": \"Acord d'usuari\",\n      \"termsAndConditions\": \"Termes i condicions\",\n      \"userprofileError\": \"No s'ha pogut carregar el perfil d'usuari\",\n      \"selectStartingDay\": \"Seleccioneu el dia d'inici\",\n      \"version\": \"Versió\"\n    },\n    \"shortcuts\": {\n      \"command\": \"Comandament\",\n      \"addNewCommand\": \"Afegeix una comanda nova\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Esteu segur que voleu suprimir aquesta vista?\",\n    \"createView\": \"Nou\",\n    \"title\": {\n      \"placeholder\": \"Sense títol\"\n    },\n    \"settings\": {\n      \"filter\": \"Filtre\",\n      \"sort\": \"Ordena\",\n      \"sortBy\": \"Ordenar per\",\n      \"properties\": \"Propietats\",\n      \"reorderPropertiesTooltip\": \"Arrossegueu per reordenar les propietats\",\n      \"group\": \"Grup\",\n      \"addFilter\": \"Afegeix un filtre\",\n      \"deleteFilter\": \"Suprimeix el filtre\",\n      \"filterBy\": \"Filtra per...\",\n      \"typeAValue\": \"Escriu un valor...\",\n      \"layout\": \"Disseny\",\n      \"databaseLayout\": \"Disseny\",\n      \"editView\": \"Edita la vista\",\n      \"boardSettings\": \"Configuració del tauler\",\n      \"calendarSettings\": \"Configuració del calendari\",\n      \"createView\": \"Vista nova\",\n      \"duplicateView\": \"Vista duplicada\",\n      \"deleteView\": \"Suprimeix la vista\",\n      \"viewList\": \"Visualitzacions de la base de dades\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Conté\",\n      \"doesNotContain\": \"No conté\",\n      \"endsWith\": \"Acaba amb\",\n      \"startWith\": \"Comença amb\",\n      \"is\": \"És\",\n      \"isNot\": \"No és\",\n      \"isEmpty\": \"Està buit\",\n      \"isNotEmpty\": \"No està buit\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"No\",\n        \"startWith\": \"Comença amb\",\n        \"endWith\": \"Acaba amb\",\n        \"isEmpty\": \"està buit\",\n        \"isNotEmpty\": \"no està buida\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Comprovat\",\n      \"isUnchecked\": \"Sense marcar\",\n      \"choicechipPrefix\": {\n        \"is\": \"és\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"està completa\",\n      \"isIncomplted\": \"és incompleta\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"És\",\n      \"isNot\": \"No és\",\n      \"contains\": \"Conté\",\n      \"doesNotContain\": \"No conté\",\n      \"isEmpty\": \"Està buit\",\n      \"isNotEmpty\": \"No està buit\"\n    },\n    \"dateFilter\": {\n      \"before\": \"És abans\",\n      \"after\": \"És després\",\n      \"onOrBefore\": \"Està activat o abans\",\n      \"onOrAfter\": \"Està activat o després\",\n      \"between\": \"Està entre\",\n      \"empty\": \"Està buit\",\n      \"notEmpty\": \"No està buit\"\n    },\n    \"field\": {\n      \"hide\": \"Amaga\",\n      \"show\": \"Espectacle\",\n      \"insertLeft\": \"Insereix a l'esquerra\",\n      \"insertRight\": \"Insereix a la dreta\",\n      \"duplicate\": \"Duplicat\",\n      \"delete\": \"Suprimeix\",\n      \"textFieldName\": \"Text\",\n      \"checkboxFieldName\": \"casella de selecció\",\n      \"dateFieldName\": \"Data\",\n      \"updatedAtFieldName\": \"Hora de l'última modificació\",\n      \"createdAtFieldName\": \"Temps creat\",\n      \"numberFieldName\": \"Nombres\",\n      \"singleSelectFieldName\": \"Seleccioneu\",\n      \"multiSelectFieldName\": \"Selecció múltiple\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Llista de verificació\",\n      \"numberFormat\": \"Format numèric\",\n      \"dateFormat\": \"Format de data\",\n      \"includeTime\": \"Inclou el temps\",\n      \"isRange\": \"Data de finalització\",\n      \"dateFormatFriendly\": \"Mes Dia, Any\",\n      \"dateFormatISO\": \"Any-Mes-Dia\",\n      \"dateFormatLocal\": \"Mes/Dia/Any\",\n      \"dateFormatUS\": \"Any/Mes/Dia\",\n      \"dateFormatDayMonthYear\": \"Dia/Mes/Any\",\n      \"timeFormat\": \"Format horari\",\n      \"invalidTimeFormat\": \"Format no vàlid\",\n      \"timeFormatTwelveHour\": \"12 hores\",\n      \"timeFormatTwentyFourHour\": \"24 hores\",\n      \"clearDate\": \"Data clara\",\n      \"dateTime\": \"Data i hora\",\n      \"startDateTime\": \"Data d'inici hora\",\n      \"endDateTime\": \"Data de finalització hora\",\n      \"failedToLoadDate\": \"No s'ha pogut carregar el valor de la data\",\n      \"selectTime\": \"Seleccioneu l'hora\",\n      \"selectDate\": \"Seleccioneu la data\",\n      \"visibility\": \"Visibilitat\",\n      \"propertyType\": \"Tipus de propietat\",\n      \"addSelectOption\": \"Afegeix una opció\",\n      \"typeANewOption\": \"Escriviu una nova opció\",\n      \"optionTitle\": \"Opcions\",\n      \"addOption\": \"Afegeix opció\",\n      \"editProperty\": \"Edita la propietat\",\n      \"newProperty\": \"Nova propietat\",\n      \"deleteFieldPromptMessage\": \"Estàs segur? Aquesta propietat se suprimirà\",\n      \"newColumn\": \"Nova columna\",\n      \"format\": \"Format\",\n      \"reminderOnDateTooltip\": \"Aquesta cel·la té un recordatori programat\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Afegeix un camp nou\",\n      \"fieldDragElementTooltip\": \"Feu clic per obrir el menú\"\n    },\n    \"sort\": {\n      \"ascending\": \"Ascendent\",\n      \"descending\": \"Descens\",\n      \"addSort\": \"Afegeix ordenació\",\n      \"deleteSort\": \"Suprimeix l'ordenació\"\n    },\n    \"row\": {\n      \"duplicate\": \"Duplicat\",\n      \"delete\": \"Suprimeix\",\n      \"titlePlaceholder\": \"Sense títol\",\n      \"textPlaceholder\": \"Buit\",\n      \"copyProperty\": \"S'ha copiat la propietat al porta-retalls\",\n      \"count\": \"Compte\",\n      \"newRow\": \"Nova fila\",\n      \"action\": \"Acció\",\n      \"drag\": \"Arrossegueu per moure's\"\n    },\n    \"selectOption\": {\n      \"create\": \"Crear\",\n      \"purpleColor\": \"Porpra\",\n      \"pinkColor\": \"Rosa\",\n      \"lightPinkColor\": \"Rosa clar\",\n      \"orangeColor\": \"taronja\",\n      \"yellowColor\": \"Groc\",\n      \"limeColor\": \"Lima\",\n      \"greenColor\": \"Verd\",\n      \"aquaColor\": \"Aqua\",\n      \"blueColor\": \"Blau\",\n      \"deleteTag\": \"Suprimeix l'etiqueta\",\n      \"colorPanelTitle\": \"Colors\",\n      \"panelTitle\": \"Seleccioneu una opció o creeu-ne una\",\n      \"searchOption\": \"Cerca una opció\"\n    },\n    \"checklist\": {\n      \"addNew\": \"Afegeix un element\",\n      \"submitNewTask\": \"Crear\"\n    },\n    \"url\": {\n      \"launch\": \"Oberta al navegador\",\n      \"copy\": \"Copia l'URL\"\n    },\n    \"menuName\": \"Quadrícula\",\n    \"referencedGridPrefix\": \"Vista de\"\n  },\n  \"document\": {\n    \"menuName\": \"Document\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Seleccioneu un tauler per enllaçar\",\n        \"createANewBoard\": \"Crea una nova Junta\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Seleccioneu una quadrícula per enllaçar\",\n        \"createANewGrid\": \"Creeu una nova quadrícula\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Seleccioneu un calendari al qual voleu enllaçar\",\n        \"createANewCalendar\": \"Crea un calendari nou\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Esquema\",\n      \"codeBlock\": \"Bloc de codi\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Junta de referència\",\n      \"referencedGrid\": \"Quadrícula de referència\",\n      \"referencedCalendar\": \"Calendari de referència\",\n      \"autoGeneratorMenuItemName\": \"AI Writer\",\n      \"autoGeneratorTitleName\": \"AI: Demana a AI que escrigui qualsevol cosa...\",\n      \"autoGeneratorLearnMore\": \"Aprèn més\",\n      \"autoGeneratorGenerate\": \"Generar\",\n      \"autoGeneratorHintText\": \"Pregunta a AI...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"No es pot obtenir la clau AI\",\n      \"autoGeneratorRewrite\": \"Reescriure\",\n      \"smartEdit\": \"Assistents d'IA\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"Corregir l'ortografia\",\n      \"warning\": \"⚠️ Les respostes de la IA poden ser inexactes o enganyoses.\",\n      \"smartEditSummarize\": \"Resumir\",\n      \"smartEditImproveWriting\": \"Millorar l'escriptura\",\n      \"smartEditMakeLonger\": \"Fer més llarg\",\n      \"smartEditCouldNotFetchResult\": \"No s'ha pogut obtenir el resultat d'AI\",\n      \"smartEditCouldNotFetchKey\": \"No s'ha pogut obtenir la clau AI\",\n      \"smartEditDisabled\": \"Connecteu AI a Configuració\",\n      \"discardResponse\": \"Voleu descartar les respostes d'IA?\",\n      \"createInlineMathEquation\": \"Crea una equació\",\n      \"fonts\": \"Fonts\",\n      \"toggleList\": \"Commuta la llista\",\n      \"bulletedList\": \"Llista amb pics\",\n      \"todoList\": \"Llista de tasques\",\n      \"cover\": {\n        \"changeCover\": \"Canvi de coberta\",\n        \"colors\": \"Colors\",\n        \"images\": \"Imatges\",\n        \"clearAll\": \"Esborra-ho tot\",\n        \"abstract\": \"Resum\",\n        \"addCover\": \"Afegeix coberta\",\n        \"addLocalImage\": \"Afegeix una imatge local\",\n        \"invalidImageUrl\": \"URL de la imatge no vàlid\",\n        \"failedToAddImageToGallery\": \"No s'ha pogut afegir la imatge a la galeria\",\n        \"enterImageUrl\": \"Introduïu l'URL de la imatge\",\n        \"add\": \"Afegeix\",\n        \"back\": \"esquena\",\n        \"saveToGallery\": \"Desa a la galeria\",\n        \"removeIcon\": \"Elimina la icona\",\n        \"pasteImageUrl\": \"Enganxa l'URL de la imatge\",\n        \"or\": \"O\",\n        \"pickFromFiles\": \"Trieu entre fitxers\",\n        \"couldNotFetchImage\": \"No s'ha pogut recuperar la imatge\",\n        \"imageSavingFailed\": \"No s'ha pogut desar la imatge\",\n        \"addIcon\": \"Afegeix una icona\",\n        \"changeIcon\": \"Canvia la icona\",\n        \"coverRemoveAlert\": \"S'eliminarà de la coberta després de suprimir-lo.\",\n        \"alertDialogConfirmation\": \"N'estàs segur, vols continuar?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"Equació matemàtica\",\n        \"addMathEquation\": \"Afegeix una equació matemàtica\",\n        \"editMathEquation\": \"Edita l'equació matemàtica\"\n      },\n      \"optionAction\": {\n        \"click\": \"Feu clic\",\n        \"toOpenMenu\": \" per obrir el menú\",\n        \"delete\": \"Suprimeix\",\n        \"duplicate\": \"Duplicat\",\n        \"turnInto\": \"Converteix-te en\",\n        \"moveUp\": \"Mou-te\",\n        \"moveDown\": \"Moure cap avall\",\n        \"color\": \"Color\",\n        \"align\": \"Alinear\",\n        \"left\": \"Esquerra\",\n        \"center\": \"Centre\",\n        \"right\": \"Dret\",\n        \"defaultColor\": \"Per defecte\"\n      },\n      \"image\": {\n        \"addAnImage\": \"Afegeix una imatge\",\n        \"copiedToPasteBoard\": \"L'enllaç de la imatge s'ha copiat al porta-retalls\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Afegiu títols per crear una taula de continguts.\"\n      },\n      \"table\": {\n        \"addAfter\": \"Afegeix després\",\n        \"addBefore\": \"Afegeix abans\",\n        \"delete\": \"Suprimeix\",\n        \"duplicate\": \"Duplicar\",\n        \"bgColor\": \"Color de fon\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Còpia\",\n        \"cut\": \"Talla\"\n      },\n      \"action\": \"Accions\",\n      \"database\": {\n        \"selectDataSource\": \"Seleccioneu la font de dades\",\n        \"noDataSource\": \"Sense font de dades\",\n        \"toContinue\": \"per continuar\",\n        \"newDatabase\": \"Nova base de dades\",\n        \"linkToDatabase\": \"Enllaç a la base de dades\"\n      }\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Escriviu '/' per a les ordres\"\n    },\n    \"title\": {\n      \"placeholder\": \"Sense títol\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Feu clic per afegir imatge\",\n      \"upload\": {\n        \"label\": \"Carrega\",\n        \"placeholder\": \"Feu clic per pujar la imatge\"\n      },\n      \"url\": {\n        \"label\": \"URL de la imatge\",\n        \"placeholder\": \"Introduïu l'URL de la imatge\"\n      },\n      \"ai\": {\n        \"label\": \"Generar imatge des d'AI\"\n      },\n      \"support\": \"El límit de mida de la imatge és de 5 MB. Formats admesos: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Imatge no vàlida\",\n        \"invalidImageSize\": \"La mida de la imatge ha de ser inferior a 5 MB\",\n        \"invalidImageFormat\": \"El format d'imatge no és compatible. Formats admesos: JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"URL de la imatge no vàlid\"\n      },\n      \"embedLink\": {\n        \"label\": \"Insereix l'enllaç\",\n        \"placeholder\": \"Enganxeu o escriviu un enllaç d'imatge\"\n      },\n      \"searchForAnImage\": \"Cerca una imatge\",\n      \"saveImageToGallery\": \"Guardar imatge\",\n      \"unableToLoadImage\": \"No es pot carregar la imatge\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Llenguatge\",\n        \"placeholder\": \"Escolliu l'idioma\"\n      }\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Enganxeu o escriviu un enllaç\",\n      \"openInNewTab\": \"Obre en una pestanya nova\",\n      \"copyLink\": \"Copia l'enllaç\",\n      \"removeLink\": \"Elimina l'enllaç\",\n      \"url\": {\n        \"label\": \"URL de l'enllaç\",\n        \"placeholder\": \"Introduïu l'URL de l'enllaç\"\n      },\n      \"title\": {\n        \"label\": \"Títol de l'enllaç\",\n        \"placeholder\": \"Introduïu el títol de l'enllaç\"\n      }\n    },\n    \"mention\": {\n      \"page\": {\n        \"label\": \"Enllaç a la pàgina\"\n      }\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"Nou\"\n    },\n    \"menuName\": \"Pissarra\",\n    \"referencedBoardPrefix\": \"Vista de\",\n    \"mobile\": {\n      \"showGroup\": \"Mostra grup\",\n      \"showGroupContent\": \"Esteu segur que voleu mostrar aquest grup al tauler?\",\n      \"failedToLoad\": \"No s'ha pogut carregar la vista del tauler\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Calendari\",\n    \"defaultNewCalendarTitle\": \"Sense títol\",\n    \"navigation\": {\n      \"today\": \"Avui\",\n      \"jumpToday\": \"Salta a Avui\",\n      \"previousMonth\": \"Mes anterior\",\n      \"nextMonth\": \"El mes que ve\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Mostra els números de la setmana\",\n      \"showWeekends\": \"Mostra els caps de setmana\",\n      \"firstDayOfWeek\": \"Comença la setmana\",\n      \"layoutDateField\": \"Disseny del calendari per\",\n      \"noDateTitle\": \"Sense data\",\n      \"clickToAdd\": \"Feu clic per afegir al calendari\",\n      \"name\": \"Disseny del calendari\",\n      \"noDateHint\": \"Els esdeveniments no programats es mostraran aquí\"\n    },\n    \"referencedCalendarPrefix\": \"Vista de\"\n  },\n  \"errorDialog\": {\n    \"title\": \"Error d'@:appName\",\n    \"howToFixFallback\": \"Lamentem les molèsties! Envieu un problema a la nostra pàgina de GitHub que descrigui el vostre error.\",\n    \"github\": \"Veure a GitHub\"\n  },\n  \"search\": {\n    \"label\": \"Cerca\",\n    \"placeholder\": {\n      \"actions\": \"Cerca accions...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Copiat!\",\n      \"fail\": \"No es pot copiar\"\n    }\n  },\n  \"unSupportBlock\": \"La versió actual no admet aquest bloc.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Esteu segur que voleu suprimir {pageType}?\",\n    \"deleteContentCaption\": \"si suprimiu aquest {pageType}, podeu restaurar-lo des de la paperera.\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/ckb-KU.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"من\",\n  \"welcomeText\": \"@:appName بەخێربێن بۆ\",\n  \"welcomeTo\": \"بەخێربێن بۆ\",\n  \"githubStarText\": \"بە گیتهابەکەمان ئەستێرە بدەن\",\n  \"subscribeNewsletterText\": \"سەبسکرایبی هەواڵنامە بکە\",\n  \"letsGoButtonText\": \"دەست پێ بکە\",\n  \"title\": \"سه‌ردێڕ\",\n  \"youCanAlso\": \"هەروەها ئەتوانی\",\n  \"and\": \"وە\",\n  \"failedToOpenUrl\": \"شکستی هێنا لە کردنەوەی url: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"بۆ زیادکردن لە خوارەوە کلیک بکە\",\n    \"addAboveCmd\": \"Alt+click\",\n    \"addAboveMacCmd\": \"Option+click\",\n    \"addAboveTooltip\": \"بۆ زیادکردنی سەرەوە\",\n    \"dragTooltip\": \"ڕاکێشان بۆ جوڵە\",\n    \"openMenuTooltip\": \"کلیک کردن بۆ کردنەوەی مینیوەکە\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"ناو نووسین\",\n    \"title\": \" ناو نووسین لە @:appName\",\n    \"getStartedText\": \"دەست پێ بکە\",\n    \"emptyPasswordError\": \"ناتوانرێت تێپه‌ڕه‌وشه بەتاڵ بێت\",\n    \"repeatPasswordEmptyError\": \"تێپه‌ڕه‌وشەی دووبارەکراو ناتوانرێت بەتاڵ بێت\",\n    \"unmatchedPasswordError\": \"دووبارەکراوەی تێپه‌ڕه‌وشه هەمان تێپه‌ڕه‌وشه نییە\",\n    \"alreadyHaveAnAccount\": \"لە پێشتر هه‌ژمارت هەیە؟\",\n    \"emailHint\": \"ئیمەیڵ\",\n    \"passwordHint\": \"تێپه‌ڕه‌وشه\",\n    \"repeatPasswordHint\": \"دووبارە کردنی تێپه‌ڕه‌وشه\",\n    \"signUpWith\": \"ناونووسین بە:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"چوونه‌ژووره‌وه‌ بە @:appName\",\n    \"loginButtonText\": \"چوونه‌ژووره‌وه‌\",\n    \"loginStartWithAnonymous\": \"بە دانیشتنێکی بێناو دەست پێ بکە\",\n    \"continueAnonymousUser\": \"وەک بەکارهێنەری میوان بەردەوام بە\",\n    \"buttonText\": \"چوونه‌ژووره‌وه‌\",\n    \"signingInText\": \"چوونە ناوەوە...\",\n    \"forgotPassword\": \"تێپه‌ڕه‌وشەت لەبیر كردووە ؟\",\n    \"emailHint\": \"ئیمەیڵ\",\n    \"passwordHint\": \"تێپه‌ڕه‌وشه\",\n    \"dontHaveAnAccount\": \"هه‌ژمارت نییە؟\",\n    \"repeatPasswordEmptyError\": \"ناتوانرێت تێپه‌ڕه‌وشه بەتاڵ بێت\",\n    \"unmatchedPasswordError\": \"دووبارەکراوەی تێپه‌ڕه‌وشه هەمان تێپه‌ڕه‌وشه نییە\",\n    \"syncPromptMessage\": \"ڕەنگە هاوکاتکردنی داتاکان ماوەیەکی پێبچێت. تکایە ئەم پەیجە دامەخە\",\n    \"or\": \"یان\",\n    \"signInWith\": \"ناونووسین وە:\",\n    \"signInWithEmail\": \"لە ڕێگەی ئیمەیڵەوە بچۆرە ژوورەوە\",\n    \"pleaseInputYourEmail\": \"تکایە ئیمەیڵەکەت بنووسە\",\n    \"magicLinkSent\": \"مەجیک لینک بۆ ئیمەیڵەکەت نێردراوە، تکایە ئیمەیڵەکەت بپشکنە\",\n    \"invalidEmail\": \"تکایە ئیمەیڵێکی دروست دابنێ\",\n    \"LogInWithGoogle\": \"چوونە ژوورەوە لە ڕێگەی گووگڵەوە\",\n    \"LogInWithGithub\": \"چوونە ژوورەوە لە ڕێگەی گیتهاب\",\n    \"LogInWithDiscord\": \"چوونە ژوورەوە لە ڕێگەی دیسکۆرد\",\n    \"loginAsGuestButtonText\": \"دەست پێ بکە\",\n    \"logInWithMagicLink\": \"بە مەجیک لینک بچۆرە ژوورەوە\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"هەڵبژاردنی شوێنی کارەکەت\",\n    \"create\": \"دروستکردنی شوێنی کارکردن\",\n    \"reset\": \"شوێنی کار ڕێست بکەرەوە\",\n    \"resetWorkspacePrompt\": \"ڕێستکردنی شوێنی کارەکە هەموو لاپەڕە و داتاکانی ناوی دەسڕێتەوە. ئایا دڵنیای کە دەتەوێت شوێنی کارەکە ڕێست بکەیتەوە؟ یان دەتوانیت پەیوەندی بە تیمی پشتگیرییەوە بکەیت بۆ گەڕاندنەوەی شوێنی کارەکە\",\n    \"hint\": \"شوێنی کارکردن\",\n    \"notFoundError\": \"هیچ شوێنێکی کار نەدۆزراوە\",\n    \"failedToLoad\": \"هەندێ شت بە هەڵە ڕۆیشت! شکستی هێنا لە بارکردنی شوێنی کارکردن. هەوڵبدە هەر نموونەیەکی کراوەی @:appName دابخەیت و دووبارە هەوڵبدەرەوە.\",\n    \"errorActions\": {\n      \"reportIssue\": \"ڕاپۆرت کردنی کێشەیەک\",\n      \"reportIssueOnGithub\": \"ڕاپۆرت کردنی کێشەیەک لەسەر گیتهابەوە \",\n      \"exportLogFiles\": \"هەناردەکردنی فایلەکانی گوزارش\",\n      \"reachOut\": \"پەیوەندی لەگەڵ دیسکۆرد\"\n    },\n    \"menuTitle\": \"شوێنی کارکردن\",\n    \"deleteWorkspaceHintText\": \"ئایا دڵنیای کە دەتەوێت شوێنی کارەکە بسڕیتەوە؟ ئەم کارە ناتوانرێت پووچەڵ بکرێتەوە.\",\n    \"createSuccess\": \"شوێنی کارکردن بە سەرکەوتوویی دروستکرا\",\n    \"createFailed\": \"شکستی هێنا لە دروستکردنی شوێنی کارکردن\",\n    \"createLimitExceeded\": \"تۆ گەیشتووی بە زۆرترین سنووری شوێنی کارکردن کە ڕێگەپێدراوە بۆ ئەکاونتەکەت. ئەگەر پێویستت بە شوێنی کاری زیاترە بۆ بەردەوامبوون لە کارەکانت، تکایە لە Github داوای بکە\",\n    \"deleteSuccess\": \"شوێنی کارکردن بە سەرکەوتوویی سڕاوەتەوە\",\n    \"deleteFailed\": \"شکستی هێنا لە سڕینەوەی شوێنی کارکردن\",\n    \"openSuccess\": \"بە سەرکەوتوویی شوێنی کارکردن بکەرەوە\",\n    \"openFailed\": \"شکستی هێنا لە کردنەوەی شوێنی کارکردن\",\n    \"renameSuccess\": \"ناوی شوێنی کار بە سەرکەوتوویی گۆڕدرا\",\n    \"renameFailed\": \"شکستی هێنا لە گۆڕینی ناوی شوێنی کار\",\n    \"updateIconSuccess\": \"ئایکۆنی شوێنی کار بە سەرکەوتوویی نوێکرایەوە\",\n    \"updateIconFailed\": \"لە نوێکردنی ئایکۆنی شوێنی کار شکستی هێنا\",\n    \"cannotDeleteTheOnlyWorkspace\": \"ناتوانرێت تاکە شوێنی کارکردن بسڕدرێتەوە\",\n    \"fetchWorkspacesFailed\": \"شکستی هێنا لە هێنانی شوێنەکانی کار\",\n    \"leaveCurrentWorkspace\": \"شوێنی کارکردن بەجێبهێڵە\",\n    \"leaveCurrentWorkspacePrompt\": \"ئایا دڵنیای کە دەتەوێت شوێنی کارکردنی ئێستا بەجێبهێڵیت؟\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"هاوبەشکردن\",\n    \"workInProgress\": \"بەم زووانە\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"کۆپی بکە بۆ کلیپبۆرد\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"کۆپی کردنی لینک\"\n  },\n  \"moreAction\": {\n    \"small\": \"بچووک\",\n    \"medium\": \"ناوەند\",\n    \"large\": \"گەورە\",\n    \"fontSize\": \"قەبارەی قەڵەم\",\n    \"import\": \"زیادکردن\",\n    \"moreOptions\": \"بژاردەی زیاتر\",\n    \"wordCount\": \"ژمارەی ووشە: {}\",\n    \"charCount\": \"ژمارەی پیتەکان: {}\",\n    \"createdAt\": \"دروستکراوە: {}\",\n    \"deleteView\": \"سڕینەوە\",\n    \"duplicateView\": \"دووبارە کردنەوە\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Text & Markdown\",\n    \"documentFromV010\": \"به‌ڵگه‌نامه لە وەشانی 0.1.0\",\n    \"databaseFromV010\": \"داتابەیس  لە وەشانی 0.1.0\",\n    \"csv\": \"CSV\",\n    \"database\": \"بنکەدراوە\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"گۆڕینی ناو\",\n    \"delete\": \"سڕینەوە\",\n    \"duplicate\": \"دووبارەکردنەوە\",\n    \"unfavorite\": \"سڕینەوە لە دڵخوازەکان\",\n    \"favorite\": \"خستنە لیستی هەڵبژاردەکان\",\n    \"openNewTab\": \"لە تابێکی نوێدا بکرێتەوە\",\n    \"moveTo\": \"گواستنەوە بۆ\",\n    \"addToFavorites\": \"خستنە لیستی هەڵبژاردەکان\",\n    \"copyLink\": \"کۆپی کردنی لینک\"\n  },\n  \"blankPageTitle\": \"لاپەڕەی بەتاڵ\",\n  \"newPageText\": \"لاپەڕەی نوێ\",\n  \"newDocumentText\": \"بەڵگەنامەی نوێ\",\n  \"newGridText\": \"تۆڕی نوێ\",\n  \"newCalendarText\": \"ڕۆژژمێری نوێ\",\n  \"newBoardText\": \"تەختەی نوێ\",\n  \"trash\": {\n    \"text\": \"زبڵدان\",\n    \"restoreAll\": \"گەڕاندنەوەی هەموو\",\n    \"deleteAll\": \"هەمووی بسڕەوە\",\n    \"pageHeader\": {\n      \"fileName\": \"ناوی پەڕگە\",\n      \"lastModified\": \"دوایین پیاچوونەوە\",\n      \"created\": \"دروستکراوە\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"دەتەوێت هەموو لاپەڕەکانی ناو زبڵدان بسڕیتەوە؟\",\n      \"caption\": \"ئەم کارە پێچەوانە نابێتەوە.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"ئایا دەتەوێت هەموو لاپەڕەکانی ناو زبڵدانەکە بگەڕێنێتەوە؟\",\n      \"caption\": \"ئەم کارە پێچەوانە نابێتەوە.\"\n    },\n    \"mobile\": {\n      \"actions\": \"کردەکانی زبڵدان\",\n      \"empty\": \"تەنەکەی زبڵدان بەتاڵە\",\n      \"emptyDescription\": \"هیچ فایلێکی سڕاوەت نییە\",\n      \"isDeleted\": \"دەسڕدرێتەوە\",\n      \"isRestored\": \"دەگەڕێتەوە\"\n    },\n    \"confirmDeleteTitle\": \"دڵنیای کە دەتەوێت ئەم لاپەڕەیە بۆ هەمیشە بسڕیتەوە؟\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"ئەم لاپەڕەیە لە زبڵداندایە\",\n    \"restore\": \"گەڕانەوەی لاپەڕە\",\n    \"deletePermanent\": \"سڕینەوەی هەمیشەیی\"\n  },\n  \"dialogCreatePageNameHint\": \"ناوی لاپەڕە\",\n  \"questionBubble\": {\n    \"shortcuts\": \"کورتە ڕێگاکان\",\n    \"whatsNew\": \"نوێترین\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"زانیاری دیباگ\",\n      \"success\": \"زانیارییەکانی دیباگ کۆپی کراون بۆ کلیپبۆرد!\",\n      \"fail\": \"ناتوانرێت زانیارییەکانی دیباگ کۆپی بکات بۆ کلیپبۆرد\"\n    },\n    \"feedback\": \"فیدباک\",\n    \"help\": \"پشتیوانی و یارمەتی\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"سڕینەوە، گۆڕینی ناو، و زۆر شتی تر...\",\n    \"addPageTooltip\": \"لاپەڕەیەک لە ناوەوە زیاد بکە\",\n    \"defaultNewPageName\": \"بێ ناونیشان\",\n    \"renameDialog\": \"گۆڕینی ناو\"\n  },\n  \"noPagesInside\": \"لە ناوەوە هیچ لاپەڕەیەک نییە\",\n  \"toolbar\": {\n    \"undo\": \"پاشەکشە\",\n    \"redo\": \"Redo\",\n    \"bold\": \"تۆخ\",\n    \"italic\": \"لار\",\n    \"underline\": \"هێڵ بەژێرداهێنان\",\n    \"strike\": \"لەگەڵ هێڵ لە ناوەڕاستدا\",\n    \"numList\": \"لیستی ژمارەدار\",\n    \"bulletList\": \"بووڵت لیست\",\n    \"checkList\": \"لیستی پیاچوونه‌وه‌\",\n    \"inlineCode\": \"کۆدی ناو هێڵ\",\n    \"quote\": \"ده‌ق\",\n    \"header\": \"سه‌رپه‌ڕه‌\",\n    \"highlight\": \"بەرجەستەکردن\",\n    \"color\": \"ڕەنگ\",\n    \"addLink\": \"زیادکردنی لینک\",\n    \"link\": \"بەستەر\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"دۆخی ڕووناک\",\n    \"darkMode\": \"دۆخی تاریک\",\n    \"openAsPage\": \"کردنەوە وەک لاپەڕە\",\n    \"addNewRow\": \"زیادکردنی ڕیزێکی نوێ\",\n    \"openMenu\": \"Menu کردنەوەی\",\n    \"dragRow\": \"بۆ ڕێکخستنەوەی ڕیزەکە فشارێکی درێژ بکە\",\n    \"viewDataBase\": \"بینینی بنکەدراوە\",\n    \"referencePage\": \"ئەم {name} ڕەوانە کراوە\",\n    \"addBlockBelow\": \"لە خوارەوە بلۆکێک زیاد بکە\",\n    \"urlLaunchAccessory\": \"کردنەوە لە وێبگەڕ\",\n    \"urlCopyAccessory\": \"کۆپی کردنی URL\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"داخستنی سایدبار\",\n    \"openSidebar\": \"کردنەوەی سایدبار\",\n    \"personal\": \"کەسی\",\n    \"private\": \"تایبه‌تی\",\n    \"workspace\": \"شوێنی کارکردن\",\n    \"favorites\": \"دڵخوازەکان\",\n    \"clickToHidePrivate\": \"بۆ شاردنەوەی شوێنی تایبەت کلیک بکە\\nئەو لاپەڕانەی لێرە دروستت کردووە تەنها بۆ تۆ دیارە\",\n    \"clickToHideWorkspace\": \"بۆ شاردنەوەی شوێنی کارکردن کلیک بکە\\nئەو پەیجانەی لێرە دروستت کردووە بۆ هەموو ئەندامێک دیارە\",\n    \"clickToHidePersonal\": \"بۆ شاردنەوەی بەشی کەسی کلیک بکە\",\n    \"clickToHideFavorites\": \"بۆ شاردنەوەی بەشی دڵخوازەکان کلیک بکە\",\n    \"addAPage\": \"زیاد کردنی لاپەڕەیەک\",\n    \"addAPageToPrivate\": \"لاپەڕەیەک زیاد بکە بۆ شوێنی تایبەت\",\n    \"addAPageToWorkspace\": \"لاپەڕەیەک زیاد بکە بۆ شوێنی کارکردن\",\n    \"recent\": \"نوێ\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"گۆڕینی دەق بۆ تێبینی\",\n      \"path\": \"Documents/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"بەردەنگەکان\",\n    \"whatsHappening\": \"چی ڕوودەدات\",\n    \"addContact\": \"زیادکردنی پەیوەندی\",\n    \"editContact\": \"دەستکاریکردنی پەیوەندی\"\n  },\n  \"button\": {\n    \"ok\": \"باشە\",\n    \"done\": \"ئەنجامدرا\",\n    \"cancel\": \"ڕەتکردنەوە\",\n    \"signIn\": \"چوونە ژوورەوە\",\n    \"signOut\": \"دەرچوون\",\n    \"complete\": \"تەواوە\",\n    \"save\": \"پاشەکەوتکردن\",\n    \"generate\": \"دروستکردن\",\n    \"esc\": \"ESC\",\n    \"keep\": \"پاراستن\",\n    \"tryAgain\": \"جارێکی تر هەوڵبدەرەوە\",\n    \"discard\": \"ڕەتکردنەوە\",\n    \"replace\": \"شوێن گرتنەوە\",\n    \"insertBelow\": \"insert لە خوارەوە\",\n    \"insertAbove\": \"لە سەرەوە دابنێ\",\n    \"upload\": \"بارکردن...\",\n    \"edit\": \"بژارکردن\",\n    \"delete\": \"سڕینەوە\",\n    \"duplicate\": \"هاوشێوە کردن\",\n    \"putback\": \"بیخەرەوە بۆ دواوە\",\n    \"update\": \"نوێکردنەوە\",\n    \"share\": \"هاوبەشکردن\",\n    \"removeFromFavorites\": \"سڕینەوە لە دڵخوازەکان\",\n    \"addToFavorites\": \"خستنە لیستی دڵخوازەکان\",\n    \"rename\": \"گۆڕینی ناو\",\n    \"helpCenter\": \"ناوەندی یارمەتی\",\n    \"add\": \"زیادکردن\",\n    \"yes\": \"بەڵێ\",\n    \"clear\": \"سڕدن\",\n    \"remove\": \"لابردن\",\n    \"dontRemove\": \"لامەبە\",\n    \"copyLink\": \"کۆپی بەستەر\",\n    \"align\": \"لاڕێككردن\",\n    \"login\": \"چوونه‌ژووره‌وه‌\",\n    \"logout\": \"دەرچوون\",\n    \"deleteAccount\": \"سڕینەوەی هەژمار\",\n    \"back\": \"پاش\",\n    \"signInGoogle\": \"لە ڕێگەی گووگڵەوە بچۆرە ژوورەوە\",\n    \"signInGithub\": \"لە ڕێگەی گیتهاب بچۆرە ژوورەوە\",\n    \"signInDiscord\": \"لە ڕێگەی دیسکۆرد بچۆرە ژوورەوە\",\n    \"Done\": \"تەواوه\",\n    \"Cancel\": \"ڕەتکردن\",\n    \"OK\": \"ئۆکەی\"\n  },\n  \"label\": {\n    \"welcome\": \"بەخێربێن!\",\n    \"firstName\": \"ناو\",\n    \"middleName\": \"ناوی ناوەڕاست\",\n    \"lastName\": \"ناوی خێزانی\",\n    \"stepX\": \"هەنگاو {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"ناتوانرێت پەیوەندی بە ئەکاونتەکەتەوە بکرێت\",\n      \"failedMsg\": \"تکایە دڵنیابە لە تەواوکردنی پرۆسەی چوونەژوورەوە لە وێبگەڕەکەتدا.\"\n    },\n    \"google\": {\n      \"title\": \"چوونە ژوورەوە بە ئەکاونتی گووگڵ\",\n      \"instruction1\": \"بۆ دەستگەیشتن بە کانتەکتەکان لە گووگڵ، پێویستە لە ڕێگەی وێبگەڕەکەتەوە بچیتە ناو ئەم بەرنامەیە.\",\n      \"instruction2\": \"ئەم کۆدە بە کرتەکردن لەسەر ئایکۆن یان دەق هەڵبژێرە کۆپی بکە بۆ کلیپبۆردەکەت:\",\n      \"instruction3\": \"لە وێبگەڕەکەتدا بڕۆ بۆ ئەم بەستەرەی خوارەوە و ئەو کۆدەی سەرەوە دابنێ:\",\n      \"instruction4\": \"دوای تەواوکردنی ناو نووسین، کرتە بکە سەر ئەم دوگمەیەی خوارەوە\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"ڕێکخستنەکان\",\n    \"menu\": {\n      \"appearance\": \"ڕووکار\",\n      \"language\": \"زمانەکان\",\n      \"user\": \"بەکارهێنەر\",\n      \"files\": \"فایلەکان\",\n      \"notifications\": \"ئاگادارکردنەوەکان\",\n      \"open\": \"کردنەوەی ڕێکخستنەکان\",\n      \"logout\": \"دەرچوون\",\n      \"logoutPrompt\": \"دڵنیای کە دەتەوێت بچیتە دەرەوە؟\",\n      \"selfEncryptionLogoutPrompt\": \"دڵنیای کە دەتەوێت بچیتە دەرەوە؟ تکایە دڵنیابە کە نهێنی کۆدکردنەکەت کۆپی کردووە\",\n      \"syncSetting\": \"ڕێکخستنەکانی هاوکاتکردن\",\n      \"cloudSettings\": \"ڕێکخستنەکانی کڵاود\",\n      \"enableSync\": \"چالاک کردنی هاوکاتکردن\",\n      \"enableEncrypt\": \"کۆدکردنی داتاکان\",\n      \"cloudURL\": \"بەستەری سەرەکی\",\n      \"invalidCloudURLScheme\": \"پلانی نادروست\",\n      \"cloudServerType\": \"ڕاژەکاری کڵاود\",\n      \"cloudServerTypeTip\": \"تکایە ئاگاداربە کە لەوانەیە دوای گۆڕینی ڕاژەکاری کڵاودکە لە ئەکاونتی ئێستات دەربچێت\",\n      \"cloudLocal\": \"خۆماڵی\",\n      \"cloudAppFlowy\": \"ئەپفلۆوی کلاود بێتا\",\n      \"cloudAppFlowySelfHost\": \"ئەپفلۆوی کلاود بە هۆستی خۆیی\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"url ی هەور ناتوانێت بەتاڵ بێت\",\n      \"clickToCopy\": \"کرتە بۆ کۆپی کردن\",\n      \"selfHostStart\": \"ئەگەر ڕاژه‌كارت نییە، تکایە سەردانی بکە...\",\n      \"selfHostContent\": \"به‌ڵگه‌نامه\",\n      \"selfHostEnd\": \"بۆ ڕێنمایی لەسەر چۆنیەتی خۆهۆستکردنی ڕاژەکاری خۆت\",\n      \"cloudURLHint\": \"URL ی بنەڕەتی ڕاژەکارەکەت بنووسە\",\n      \"cloudWSURL\": \"URL ی وێبسۆکێت\",\n      \"cloudWSURLHint\": \"ناونیشانی وێبسۆکێتی ڕاژەکارەکەت دابنێ\",\n      \"restartApp\": \"دووبارە دەستپێکردنەوە\",\n      \"restartAppTip\": \"بەرنامەکە دووبارە دەستپێبکەرەوە بۆ ئەوەی گۆڕانکارییەکان کاریگەرییان هەبێت. تکایە ئاگاداربە کە ئەمە ڕەنگە ئەکاونتی ئێستات دەربچێت\",\n      \"changeServerTip\": \"دوای گۆڕینی ڕاژەکارەکە، پێویستە کلیک لەسەر دوگمەی دووبارە دەستپێکردنەوە بکەیت بۆ ئەوەی گۆڕانکارییەکان کاریگەرییان هەبێت\",\n      \"enableEncryptPrompt\": \"کۆدکردن چالاک بکە بۆ پاراستنی داتاکانت بەم نهێنییە. بە سەلامەتی هەڵیبگرە؛ کاتێک چالاک کرا، ناتوانرێت بکوژێنرێتەوە. ئەگەر لەدەستچوو، داتاکانت دەبنە شتێکی وەرنەگیراو. بۆ کۆپیکردن کلیک بکە\",\n      \"inputEncryptPrompt\": \"تکایە نهێنی کۆدکردنەکەت بنووسە بۆ...\",\n      \"clickToCopySecret\": \"بۆ کۆپیکردنی نهێنی کلیک بکە\",\n      \"configServerSetting\": \"ڕێکخستنەکانی ڕاژەکارەکەت ڕێکبخە\",\n      \"configServerGuide\": \"دوای هەڵبژاردنی `دەستپێکردنی خێرا`، بچۆ بۆ `ڕێکخستنەکان` و پاشان \\\"ڕێکخستنەکانی کڵاود\\\" بۆ ڕێکخستنی سێرڤەری خۆهۆستکراوەکەت.\",\n      \"inputTextFieldHint\": \"نهێنی تۆ\",\n      \"historicalUserList\": \"مێژووی چوونەژوورەوەی بەکارهێنەر\",\n      \"historicalUserListTooltip\": \"ئەم لیستە ئەکاونتە بێناوەکانت پیشان دەدات. دەتوانیت کلیک لەسەر ئەکاونتێک بکەیت بۆ بینینی وردەکارییەکانی. ئەکاونتی بێناو بە کلیک کردن لەسەر دوگمەی دەستپێکردن دروست دەکرێت\",\n      \"openHistoricalUser\": \"بۆ کردنەوەی ئەکاونتی بێناو کلیک بکە\",\n      \"customPathPrompt\": \"هەڵگرتنی فۆڵدەری داتاکانی @:appName لە فۆڵدەرێکی هاوکاتی کڵاود وەک گووگڵ درایڤ دەتوانێت مەترسی دروست بکات. ئەگەر بنکەدراوەی ناو ئەم فۆڵدەرە لە یەک کاتدا لە چەندین شوێنەوە دەستی پێ بگات یان دەستکاری بکرێت، لەوانەیە ببێتە هۆی ناکۆکی هاوکاتکردن و ئەگەری تێکچوونی داتاکان\",\n      \"importAppFlowyData\": \"هێنانی داتا لە فۆڵدەری دەرەکی @:appName\",\n      \"importingAppFlowyDataTip\": \"هێنانی داتا لە قۆناغی جێبەجێکردندایە. تکایە ئەپەکە دامەخە\",\n      \"importAppFlowyDataDescription\": \"داتا لە فۆڵدەری داتای دەرەکی @:appName کۆپی بکە و هاوردە بکە بۆ ناو فۆڵدەری داتاکانی @:appName ی ئێستا\",\n      \"importSuccess\": \"بە سەرکەوتوویی فۆڵدەری داتاکانی @:appName هاوردە کرد\",\n      \"importFailed\": \"هاوردەکردنی فۆڵدەری داتاکانی @:appName شکستی هێنا\",\n      \"importGuide\": \"بۆ زانیاری زیاتر، تکایە بەڵگەنامەی ئاماژەپێکراو بپشکنە\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"چالاک کردنی ئاگادارکردنەوەکان\",\n        \"hint\": \"کوژاندنەوە بۆ وەستاندنی دەرکەوتنی ئاگادارکردنەوە ناوخۆییەکان\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"ڕێکخستن لە سفرەوە\",\n      \"fontFamily\": {\n        \"label\": \"فۆنتفامیلی\",\n        \"search\": \"گەڕان\"\n      },\n      \"themeMode\": {\n        \"label\": \"مۆدی تێم\",\n        \"light\": \"مۆدی ڕوناک\",\n        \"dark\": \"مۆدی تاریک\",\n        \"system\": \"خۆگونجاندن لەگەڵ سیستەمدا\"\n      },\n      \"documentSettings\": {\n        \"cursorColor\": \"ڕەنگی جێنیشانده‌ری بەڵگەنامە\",\n        \"selectionColor\": \"ڕەنگی \\\"دیاریكراو\\\" بەڵگەنامە\",\n        \"hexEmptyError\": \"ڕەنگی هێکس ناتوانێت بەتاڵ بێت\",\n        \"hexLengthError\": \"بەهای هێکس دەبێت درێژییەکەی ٦ ژمارە بێت\",\n        \"hexInvalidError\": \"بەهای هێکسی نادروست\",\n        \"opacityEmptyError\": \"لێڵی ناتوانێت بەتاڵ بێت\",\n        \"opacityRangeError\": \"لێڵی دەبێت لە نێوان 1 بۆ 100 بێت\",\n        \"app\": \"App\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"به‌کاربردن\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"ئاراستەی داڕشتن\",\n        \"hint\": \"کۆنتڕۆڵی ڕۆیشتنی ناوەڕۆک لەسەر شاشەکەت بکە، لە چەپەوە بۆ ڕاست یان ڕاست بۆ چەپ.\",\n        \"ltr\": \" چەپ بۆ ڕاست\",\n        \"rtl\": \"ڕاست بۆ چەپ\"\n      },\n      \"textDirection\": {\n        \"label\": \"ئاراستەی دەقی پێشوەختە\",\n        \"hint\": \"دیاری بکە کە ئایا دەق دەبێت لە چەپەوە دەستپێبکات یان ڕاست وەکو پێشوەختە.\",\n        \"ltr\": \" چەپ بۆ ڕاست\",\n        \"rtl\": \"ڕاست بۆ چەپ\",\n        \"auto\": \"خۆکار\",\n        \"fallback\": \"هەمان شێوەی ئاراستەی نەخشە\"\n      },\n      \"themeUpload\": {\n        \"button\": \"بارکردن\",\n        \"uploadTheme\": \"بارکردنی تێم\",\n        \"description\": \"بە بەکارهێنانی دوگمەی خوارەوە تێمی @:appName ـەکەت باربکە.\",\n        \"loading\": \"تکایە چاوەڕوان بن تا ئێمە تێمی قاڵبەکەت پشتڕاست دەکەینەوە و بار دەکەین...\",\n        \"uploadSuccess\": \"تێمی قاڵبەکەت بە سەرکەوتوویی بارکرا\",\n        \"deletionFailure\": \"تێمەکە نەسڕدرایەوە. هەوڵبدە بە دەستی لابەریت.\",\n        \"filePickerDialogTitle\": \"پەڕگەیەکی .flowy_plugin هەڵبژێرە\",\n        \"urlUploadFailure\": \"ناتوانرێت URL بکرێتەوە: {}\",\n        \"failure\": \"تێمی قاڵبی بارکراو نادروستە.\"\n      },\n      \"theme\": \"تێم و ڕووکار\",\n      \"builtInsLabel\": \"قاڵبی پێش دروستکراو\",\n      \"pluginsLabel\": \"پێوەکراوەکان\",\n      \"dateFormat\": {\n        \"label\": \"فۆرماتی بەروار\",\n        \"local\": \"ناوخۆیی\",\n        \"us\": \"US\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"بەکارهێنانی ئاسانە\",\n        \"dmy\": \"ڕ/م/س\"\n      },\n      \"timeFormat\": {\n        \"label\": \"فۆرماتی کات\",\n        \"twelveHour\": \"دوانزە کاتژمێر\",\n        \"twentyFourHour\": \"بیست و چوار کاتژمێر\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"پیشاندانی دیالۆگی ناونان لە کاتی دروستکردنی لاپەڕەیەکدا\"\n    },\n    \"files\": {\n      \"copy\": \"کۆپی\",\n      \"defaultLocation\": \"خوێندنەوەی پەڕگەکان و شوێنی هەڵگرتنی داتاکان\",\n      \"exportData\": \"دەرچوون لە داتاکانتەوە بەدەست بهێنە\",\n      \"doubleTapToCopy\": \"بۆ کۆپیکردن دووجار کلیک بکە\",\n      \"restoreLocation\": \"گەڕاندنەوە بۆ ڕێڕەوی پێشوەختەی @:appName\",\n      \"customizeLocation\": \"فۆڵدەرێکی دیکە بکەرەوە\",\n      \"restartApp\": \"تکایە ئەپەکە دابخە و بیکەرەوە بۆ ئەوەی گۆڕانکارییەکان جێبەجێ بکرێن.\",\n      \"exportDatabase\": \"هەناردە کردنی بنکەدراوە\",\n      \"selectFiles\": \"پەڕگەکان هەڵبژێرە بۆ هەناردە کردن\",\n      \"selectAll\": \"هەڵبژاردنی هەموویان\",\n      \"deselectAll\": \"هەڵبژاردەی هەموو هەڵبگرە\",\n      \"createNewFolder\": \"درووست کردنی فۆڵدەری نوێ\",\n      \"createNewFolderDesc\": \"پێمان بڵێ دەتەوێت داتاکانت لە کوێ هەڵبگریت\",\n      \"defineWhereYourDataIsStored\": \"پێناسە بکە کە داتاکانت لە کوێ هەڵدەگیرێن\",\n      \"open\": \"کردنەوە\",\n      \"openFolder\": \"فۆڵدەرێکی هەبوو بکەرەوە\",\n      \"openFolderDesc\": \"خوێندن و نووسین بۆ فۆڵدەری @:appName ی ئێستات\",\n      \"folderHintText\": \"ناوی فۆڵدەر\",\n      \"location\": \"دروستکردنی فۆڵدەرێکی نوێ\",\n      \"locationDesc\": \"ناوێک بۆ فۆڵدەری داتاکانی @:appName هەڵبژێرە\",\n      \"browser\": \"وێبگەڕ\",\n      \"create\": \"دروستکردن\",\n      \"set\": \"دانان\",\n      \"folderPath\": \"ڕێڕەوی پاشەکەوتکردنی فۆڵدەر\",\n      \"locationCannotBeEmpty\": \"ڕێڕەو ناتوانرێت بەتاڵ بێت\",\n      \"pathCopiedSnackbar\": \"ڕێڕەوی پاشەکەوتکردنی فایلەکە کۆپی کرا بۆ کلیپبۆرد!\",\n      \"changeLocationTooltips\": \"گۆڕینی دایرێکتۆری داتاکان\",\n      \"change\": \"گوڕین\",\n      \"openLocationTooltips\": \"دایرێکتۆرێکی تری داتا بکەرەوە\",\n      \"openCurrentDataFolder\": \"کردنەووەی دایرێکتۆری ئێستای داتا\",\n      \"recoverLocationTooltips\": \"گەڕاندنەوە بۆ دایرێکتۆری پێشووی داتاکان\",\n      \"exportFileSuccess\": \"هەناردەکردنی فایل بە سەرکەوتوویی!\",\n      \"exportFileFail\": \"هەناردەکردنی فایلەکە شکستی هێنا!\",\n      \"export\": \"هەناردەکردن\"\n    },\n    \"user\": {\n      \"name\": \"ناو\",\n      \"email\": \"ئیمەیڵ\",\n      \"tooltipSelectIcon\": \"هەڵبژاەدنی وێنۆچكه‌\",\n      \"selectAnIcon\": \"هەڵبژاردنی وێنۆچكه‌\",\n      \"pleaseInputYourOpenAIKey\": \"تکایە کلیلی AI ـەکەت بنووسە\",\n      \"clickToLogout\": \"بۆ دەرچوون لە بەکارهێنەری ئێستا کلیک بکە\",\n      \"pleaseInputYourStabilityAIKey\": \"تکایە جێگیری کلیلی  AI ـەکەت بنووسە\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"زانیاری کەسی\",\n      \"username\": \"ناوی بەکارهێنەر\",\n      \"usernameEmptyError\": \"ناوی بەکارهێنەر ناتوانێت بەتاڵ بێت\",\n      \"about\": \"لەربارەی\",\n      \"pushNotifications\": \"ئاگادارکردنەوەکانی خستنه‌سه‌ر\",\n      \"support\": \"پشتیوانی\",\n      \"joinDiscord\": \"لە دیسکۆرد لەگەڵمان بن\",\n      \"privacyPolicy\": \"سیاسەتی پاراستنی نهێنی\",\n      \"userAgreement\": \"ڕێککەوتنی بەکارهێنەر\",\n      \"termsAndConditions\": \"بار و دۆخ و مەرجەکان\",\n      \"userprofileError\": \"شکستی هێنا لە بارکردنی پڕۆفایلی بەکارهێنەر\",\n      \"userprofileErrorDescription\": \"تکایە هەوڵبدە بچیتە دەرەوە و بچۆرەوە ژوورەوە بۆ ئەوەی بزانیت ئایا کێشەکە هێشتا بەردەوامە یان نا.\",\n      \"selectLayout\": \"نەخشە هەڵبژێرە\",\n      \"selectStartingDay\": \"ڕۆژی دەستپێکردنەکەت هەڵبژێرە\",\n      \"version\": \"وەشان\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"کورتە ڕێگاکان\",\n      \"command\": \"فەرمان\",\n      \"keyBinding\": \"کورتکراوەکانی تەختەکلیل\",\n      \"addNewCommand\": \"زیاد کردنی فەرمانێکی نوێ\",\n      \"updateShortcutStep\": \"تێکەڵەی کلیلی دڵخواز داگرە و ENTER داگرە\",\n      \"shortcutIsAlreadyUsed\": \"ئەم کورتە ڕێگایە پێشتر بۆ: {conflict} بەکارهاتووە.\",\n      \"resetToDefault\": \"گەڕاندنەوە بۆ کلیلەکانی بنه‌ڕه‌ت\",\n      \"couldNotLoadErrorMsg\": \"کورتە ڕێگاکان نەتوانرا باربکرێن، تکایە دووبارە هەوڵبدەرەوە\",\n      \"couldNotSaveErrorMsg\": \"کورتە ڕێگاکان نەتوانرا پاشەکەوت بکرێن، تکایە دووبارە هەوڵبدەرەوە\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"ئایا دڵنیای کە دەتەوێت ئەم دیمەنە بسڕیتەوە؟\",\n    \"createView\": \"نوێ\",\n    \"title\": {\n      \"placeholder\": \"بێ ناونیشان\"\n    },\n    \"settings\": {\n      \"filter\": \"فیلتێر\",\n      \"sort\": \"پۆلێن کردن\",\n      \"sortBy\": \"ڕیزکردن بەپێی\",\n      \"properties\": \"تایبەتمەندیەکان\",\n      \"reorderPropertiesTooltip\": \"بۆ ڕێکخستنەوەی تایبەتمەندییەکان ڕابکێشە\",\n      \"group\": \"ده‌سته‌\",\n      \"addFilter\": \"زیادکردنی فیلتێر\",\n      \"deleteFilter\": \"سڕینەوەی فیلتێر\",\n      \"filterBy\": \"فیلتێر بەپێی...\",\n      \"typeAValue\": \"بەهایەک بنووسە...\",\n      \"layout\": \"گه‌ڵاڵه‌به‌ندی\",\n      \"databaseLayout\": \"گه‌ڵاڵه‌به‌ندی\",\n      \"viewList\": {\n        \"zero\": \"0 بینین\",\n        \"one\": \"{count} بینین\",\n        \"other\": \"{count} بینینەکان\"\n      },\n      \"editView\": \"دەستکاری دیمەن\",\n      \"boardSettings\": \"ڕێکخستنەکانی تەختە\",\n      \"calendarSettings\": \"ڕێکخستنەکانی ساڵنامە\",\n      \"numberOfVisibleFields\": \"{} نیشان دراوە\"\n    },\n    \"textFilter\": {\n      \"contains\": \"لەخۆ دەگرێت\",\n      \"doesNotContain\": \"لەخۆناگرێت\",\n      \"endsWith\": \"کۆتایی دێت بە\",\n      \"startWith\": \"دەسپێکردن بە\",\n      \"is\": \"هەیە\",\n      \"isNot\": \"نییە\",\n      \"isEmpty\": \"به‌تاڵه‌\",\n      \"isNotEmpty\": \"بەتاڵ نییە\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"لەدژی\",\n        \"startWith\": \"دەسپێکردن بە\",\n        \"endWith\": \"کۆتایی بە\",\n        \"isEmpty\": \"به‌تاڵه‌\",\n        \"isNotEmpty\": \"بەتاڵ نییە\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"پشکنین کراوە\",\n      \"isUnchecked\": \"پشکنین نەکراوە\",\n      \"choicechipPrefix\": {\n        \"is\": \"هەیە\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"تەواوە\",\n      \"isIncomplted\": \"ناتەواوە\"\n    },\n    \"dateFilter\": {\n      \"is\": \"هەیە\",\n      \"before\": \"پێشترە\",\n      \"after\": \"دوای ئەبێت\",\n      \"between\": \"لە نێواندایە\",\n      \"empty\": \"به‌تاڵه‌\",\n      \"notEmpty\": \"بەتاڵ نییە\"\n    },\n    \"field\": {\n      \"hide\": \"شاردنەوە\",\n      \"show\": \"نیشاندان\",\n      \"insertLeft\": \"جێگیرکردن لە چەپ\",\n      \"insertRight\": \"جێگیرکردن لە ڕاست\",\n      \"duplicate\": \"دووبارەکردنەوە\",\n      \"delete\": \"سڕینەوە\",\n      \"textFieldName\": \"دەق\",\n      \"checkboxFieldName\": \"بابەتە هەڵبژێردراوەکان\",\n      \"dateFieldName\": \"ڕێکەوت\",\n      \"updatedAtFieldName\": \"دوایین گۆڕانکاری\",\n      \"createdAtFieldName\": \"دروستکراوە لە...\",\n      \"numberFieldName\": \"ژمارەکان\",\n      \"singleSelectFieldName\": \"هەڵبژاردن\",\n      \"multiSelectFieldName\": \"فرە هەڵبژاردن\",\n      \"urlFieldName\": \"ناونیشانی ئینتەرنێتی\",\n      \"checklistFieldName\": \"لیستی پشکنین\",\n      \"numberFormat\": \"فۆرمات ژمارە\",\n      \"dateFormat\": \"فۆرمات ڕێکەوت\",\n      \"includeTime\": \"کات لەخۆ بگرێت\",\n      \"isRange\": \"ڕۆژی کۆتایی\",\n      \"dateFormatFriendly\": \"Mang Roj, Sall\",\n      \"dateFormatISO\": \"Sall-Mang-Roj\",\n      \"dateFormatLocal\": \"Mang/Roj/Sall\",\n      \"dateFormatUS\": \"Sall/Mang/Roj\",\n      \"dateFormatDayMonthYear\": \"Roj/Mang/Sall\",\n      \"timeFormat\": \"فۆرماتی کات\",\n      \"invalidTimeFormat\": \"فۆرماتێکی نادروست\",\n      \"timeFormatTwelveHour\": \"دوانزە کاتژمێر\",\n      \"timeFormatTwentyFourHour\": \"بیست و چوار کاتژمێر\",\n      \"clearDate\": \"سڕینەوەی ڕێکەوت\",\n      \"dateTime\": \"کاتی بەروار\",\n      \"startDateTime\": \"کاتی بەرواری دەستپێک\",\n      \"endDateTime\": \"کاتی بەرواری کۆتایی\",\n      \"failedToLoadDate\": \"شکستی هێنا لە بارکردنی بەهای بەروار\",\n      \"selectTime\": \"کات هەڵبژێرە\",\n      \"selectDate\": \"بەروار هەڵبژێرە\",\n      \"visibility\": \"پلەی بینین\",\n      \"propertyType\": \"جۆری تایبه‌تمه‌ندی\",\n      \"addSelectOption\": \"زیادکردنی بژاردەیەک\",\n      \"typeANewOption\": \"بژاردەیەکی نوێ بنووسە\",\n      \"optionTitle\": \"بژاردەکان\",\n      \"addOption\": \"زیادکردنی بژاردە\",\n      \"editProperty\": \"دەستکاریکردنی تایبەتمەندی\",\n      \"newProperty\": \"تایبەتمەندی نوێ\",\n      \"deleteFieldPromptMessage\": \"ئایا دڵنیایت لە سڕدنەوەی ئەم تایبەتمەندییە؟\",\n      \"newColumn\": \"ستوونی نوێ\",\n      \"format\": \"فۆرمات\",\n      \"reminderOnDateTooltip\": \"ئەم خانەیە بیرخستنەوەیەکی بەرنامە بۆ داڕێژراوی هەیە\"\n    },\n    \"rowPage\": {\n      \"newField\": \"خانەیێکی نوێ زیاد بکە\",\n      \"fieldDragElementTooltip\": \"بۆ کردنەوەی مێنۆ کرتە بکە\"\n    },\n    \"sort\": {\n      \"ascending\": \"هەڵکشاو\",\n      \"descending\": \"بەرەو خوار\",\n      \"deleteAllSorts\": \"هەموو ڕیزکردنەکان لاببە\",\n      \"addSort\": \"زیادکردنی ڕیزکردن\"\n    },\n    \"row\": {\n      \"duplicate\": \"دووبارە کردنەوە\",\n      \"delete\": \"سڕینەوە\",\n      \"titlePlaceholder\": \"بێ ناونیشان\",\n      \"textPlaceholder\": \"بەتاڵ\",\n      \"copyProperty\": \"تایبەتمەندی کۆپی کرا بۆ کلیپبۆرد\",\n      \"count\": \"سەرژمێرکردن\",\n      \"newRow\": \"ڕیزی نوێ\",\n      \"action\": \"کردەوە\",\n      \"drag\": \"ڕاکێشان بۆ جوڵە\",\n      \"dragAndClick\": \"بۆ جوڵاندن ڕابکێشە، کلیک بکە بۆ کردنەوەی مێنۆ\"\n    },\n    \"selectOption\": {\n      \"create\": \"دروستکردن\",\n      \"purpleColor\": \"مۆر\",\n      \"pinkColor\": \"پەمەیی\",\n      \"lightPinkColor\": \"پەمەیی کاڵ\",\n      \"orangeColor\": \"پرتەقاڵی\",\n      \"yellowColor\": \"زەرد\",\n      \"limeColor\": \"لیمۆیی\",\n      \"greenColor\": \"سەوز\",\n      \"aquaColor\": \"ئاکوا\",\n      \"blueColor\": \"شین\",\n      \"deleteTag\": \"سڕینەوە تاگ\",\n      \"colorPanelTitle\": \"ڕەنگەکان\",\n      \"panelTitle\": \"بژاردەیەک زیاد یان دروستی بکە.\",\n      \"searchOption\": \"گەڕان بەدوای بژاردەیەکدا\",\n      \"searchOrCreateOption\": \"گەڕان یان دروستکردنی بژاردەیەک...\",\n      \"createNew\": \"دروستکردنی نوێ\",\n      \"orSelectOne\": \"یان بژاردەیەک هەڵبژێرە\",\n      \"typeANewOption\": \"بژاردەیەکی نوێ بنووسە\",\n      \"tagName\": \"ناوی تاگ\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"وەسفکردنی ئەرک\",\n      \"addNew\": \"شتێک زیاد بکە\",\n      \"submitNewTask\": \"ئافراندن\",\n      \"hideComplete\": \"شاردنەوەی ئەرکە تەواوکراوەکان\",\n      \"showComplete\": \"هەموو ئەرکەکان پیشان بدە\"\n    },\n    \"menuName\": \"تۆڕ\",\n    \"referencedGridPrefix\": \"نواندن\",\n    \"calculate\": \"حیساب بکە\",\n    \"calculationTypeLabel\": {\n      \"none\": \"هیچ\",\n      \"average\": \"تێکڕا و ڕێژە\",\n      \"max\": \"زۆر\",\n      \"median\": \"ناوەند\",\n      \"min\": \"کەم\",\n      \"sum\": \"کۆ\"\n    },\n    \"singleSelectOptionFilter\": {\n      \"is\": \"هەیە\",\n      \"isNot\": \"نییە\",\n      \"isEmpty\": \"به‌تاڵه‌\",\n      \"isNotEmpty\": \"بەتاڵ نییە\"\n    },\n    \"multiSelectOptionFilter\": {\n      \"contains\": \"لەخۆ دەگرێت\",\n      \"doesNotContain\": \"لەخۆناگرێت\",\n      \"isEmpty\": \"به‌تاڵه‌\",\n      \"isNotEmpty\": \"بەتاڵ نییە\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"بەڵگەنامە\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"بۆردێک هەڵبژێرە بۆ ئەوەی لینکی بۆ بدەیت\",\n        \"createANewBoard\": \"بۆردێکی نوێ دروست بکە\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Gridێک هەڵبژێرە بۆ ئەوەی لینکی بۆ بکەیت\",\n        \"createANewGrid\": \"دروستکردنی تۆڕێکی نوێی پیشاندانی\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"ساڵنامەیەک هەڵبژێرە بۆ ئەوەی لینکی بۆ بکەیت.\",\n        \"createANewCalendar\": \"ساڵنامەیەکی نوێ دروست بکە\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"بەڵگەنامەیەک هەڵبژێرە کە بەستەرەکەی بۆ دابنێیت\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"گەڵاڵە\",\n      \"codeBlock\": \"بلۆکی کۆد\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"بۆردی چاوگ\",\n      \"referencedGrid\": \"تۆڕی ئاماژەپێکراو\",\n      \"referencedCalendar\": \"ساڵنامەی ئاماژەپێکراو\",\n      \"referencedDocument\": \"بەڵگەنامەی ئاماژەپێکراو\",\n      \"autoGeneratorMenuItemName\": \"AI نووسەری\",\n      \"autoGeneratorTitleName\": \"داوا لە AI بکە هەر شتێک بنووسێت...\",\n      \"autoGeneratorLearnMore\": \"زیاتر زانین\",\n      \"autoGeneratorGenerate\": \"بنووسە\",\n      \"autoGeneratorHintText\": \"لە AI پرسیار بکە...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"نەتوانرا کلیلی AI بەدەست بهێنرێت\",\n      \"autoGeneratorRewrite\": \"دووبارە نووسینەوە\",\n      \"smartEdit\": \"یاریدەدەری زیرەک\",\n      \"smartEditFixSpelling\": \"ڕاستکردنەوەی نووسین\",\n      \"warning\": \"⚠️ وەڵامەکانی AI دەتوانن هەڵە یان چەواشەکارانە بن\",\n      \"smartEditSummarize\": \"کورتەنووسی\",\n      \"smartEditImproveWriting\": \"پێشخستن نوووسین\",\n      \"smartEditMakeLonger\": \"درێژتری بکەرەوە\",\n      \"smartEditCouldNotFetchResult\": \"هیچ ئەنجامێک لە AI وەرنەگیرا\",\n      \"smartEditCouldNotFetchKey\": \"نەتوانرا کلیلی AI بهێنێتە ئاراوە\",\n      \"smartEditDisabled\": \"لە ڕێکخستنەکاندا پەیوەندی بە AI بکە\",\n      \"discardResponse\": \"ئایا دەتەوێت وەڵامەکانی AI بسڕیتەوە؟\",\n      \"createInlineMathEquation\": \"درووست کردنی هاوکێشە\",\n      \"fonts\": \"فۆنتەکان\",\n      \"toggleList\": \"Toggle لیستی\",\n      \"quoteList\": \"لیستی وەرگرتە\",\n      \"numberedList\": \"لیستی ژمارەدار\",\n      \"cover\": {\n        \"changeCover\": \"گۆڕینی بەرگ\",\n        \"colors\": \"ڕەنگەکان\",\n        \"images\": \"وێنەکان\",\n        \"clearAll\": \"سڕینەوەی هەموو\",\n        \"abstract\": \"کورتە\",\n        \"addCover\": \"زیاد کردنی بەرگ\",\n        \"addLocalImage\": \"خستنه‌سه‌ری وێنەی خۆماڵی\",\n        \"invalidImageUrl\": \"ڕێڕەوی وێنەکە نادروستە\",\n        \"failedToAddImageToGallery\": \"نەمتوانی وێنەکەت بخەمە پێشانگا😔️\",\n        \"enterImageUrl\": \"ڕێڕەوی وێنەکە بنووسە\",\n        \"add\": \"زیادکردن\",\n        \"back\": \"چوونە دووا\",\n        \"saveToGallery\": \"پاشەکەوت لە پێشانگا\",\n        \"removeIcon\": \"سڕینەوەی وێنۆچكه‌\",\n        \"pasteImageUrl\": \"نووسینی ڕێڕەوی وێنە\",\n        \"or\": \"یان\",\n        \"pickFromFiles\": \"لە نێوان په‌ڕگەکاندا هەڵبژێرە\",\n        \"couldNotFetchImage\": \"نەمتوانی وێنەکەت بهێنم\",\n        \"imageSavingFailed\": \"پاشەکەوتکردنی وێنە شکستی هێنا\",\n        \"addIcon\": \"زیاد کردنی وێنۆچكه‌\",\n        \"coverRemoveAlert\": \"دوای لابردنی، لە بەرگەکە دەسڕدرێتەوە!\",\n        \"alertDialogConfirmation\": \"دڵنیای کە دەتەوێت بەردەوام بیت؟🤨️\"\n      },\n      \"mathEquation\": {\n        \"addMathEquation\": \"هاوکێشەی بیرکاری زیاد بکە\",\n        \"editMathEquation\": \"ده‌ستكاری هاوکێشەی بیرکاری\"\n      },\n      \"optionAction\": {\n        \"click\": \"کرتە بکە\",\n        \"toOpenMenu\": \"بۆ کردنەوەی پێڕست\",\n        \"delete\": \"سڕینەوە\",\n        \"duplicate\": \"هاوشێوە کردن\",\n        \"turnInto\": \"گۆڕینی بۆ...\",\n        \"moveUp\": \"بەرزکردنەوە\",\n        \"moveDown\": \"دابەزاندن\",\n        \"color\": \"ڕەنگ\",\n        \"align\": \"ڕیزبەندی\",\n        \"left\": \"چەپ\",\n        \"center\": \"ناوەند\",\n        \"right\": \"ڕاست\",\n        \"defaultColor\": \"ڕەنگی بنەڕەت\"\n      },\n      \"image\": {\n        \"copiedToPasteBoard\": \"بەستەری وێنەکە کۆپی کرا بۆ کلیپبۆرد\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"بۆ دروستکردنی خشتەی ناوەڕۆک سەردێڕەکان داخڵ بکە\"\n      },\n      \"openAI\": \"AI ژیری دەستکرد\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"بۆ فەرمانەکان '/' بنووسە\"\n    },\n    \"title\": {\n      \"placeholder\": \"بێ ناونیشان\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"بۆ زیادکردنی وێنە کلیک بکە\",\n      \"upload\": {\n        \"label\": \"بەرزکردنەوە\",\n        \"placeholder\": \"بۆ بارکردنی وێنە کلیک بکە\"\n      },\n      \"url\": {\n        \"label\": \"بەستەری وێنە\",\n        \"placeholder\": \"بەستەری وێنەکە بنووسە\"\n      },\n      \"support\": \"سنووری قەبارەی وێنە 5 مێگابایت. فۆرماتەکەنی پشتیوانی کراو: JPEG، PNG، GIF، SVG\",\n      \"error\": {\n        \"invalidImage\": \"وێنەیەکی نادروست\",\n        \"invalidImageSize\": \"قەبارەی وێنەکە دەبێت لە 5 مێگابایت کەمتر بێت\",\n        \"invalidImageFormat\": \"فۆرماتی وێنەکە پشتیوانی ناکرێ. فۆرماتەکەنی پشتیوانی کراو: JPEG، PNG، GIF، SVG\",\n        \"invalidImageUrl\": \"Urlی وێنەکە نادروستە\"\n      }\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"زمان\",\n        \"placeholder\": \"هەڵبژاردنی زمان\"\n      }\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"ڕێڕەوەکە بلکێنە یان بینووسە\",\n      \"openInNewTab\": \"کردنەوە لە تابێکی نوێدا\",\n      \"copyLink\": \"کۆپی کردنی بەستەر\",\n      \"removeLink\": \"لابردنی بەستەر\",\n      \"url\": {\n        \"label\": \"بەستەر\",\n        \"placeholder\": \"بەستەرەکە بنووسە\"\n      },\n      \"title\": {\n        \"label\": \"سه‌ردێڕی بەستەرەکە\",\n        \"placeholder\": \"سه‌ردێڕی بەستەرەکە بنووسە\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"باسی کەسێک یان لاپەڕەیەک یان بەروارێک یان سرووشتێک بکە...\",\n      \"page\": {\n        \"label\": \"لینکی پەیج\",\n        \"tooltip\": \"کلیک بکە بۆ کردنەوەی لاپەڕەکە\"\n      }\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"دروست کردنی نوێ\"\n    },\n    \"menuName\": \"تەختە\",\n    \"referencedBoardPrefix\": \"دیمەنی...\",\n    \"mobile\": {\n      \"showGroup\": \"نیشاندانی گروپ\",\n      \"showGroupContent\": \"ئایا دڵنیایت دەتەوێت ئەم گروپە لەسەر تەختە نیشان بدەیت؟\",\n      \"failedToLoad\": \"بارکردنی بینینی تەختە سەرکەوتوو نەبوو\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"ساڵنامە\",\n    \"defaultNewCalendarTitle\": \"بێ ناونیشان\",\n    \"navigation\": {\n      \"today\": \"ئەمڕۆ\",\n      \"jumpToday\": \"باز بدە بۆ ئەمڕۆ\",\n      \"previousMonth\": \"مانگی پێشوو\",\n      \"nextMonth\": \"مانگی داهاتوو\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"پیشاندانی ژمارەکانی هەفتە\",\n      \"showWeekends\": \"پیشاندانی کۆتایی هەفتە\",\n      \"firstDayOfWeek\": \"سەرەتای هەفتە لە\",\n      \"layoutDateField\": \"ڕیزبەستی ساڵنامە بە\",\n      \"noDateTitle\": \"بە بێ بەروارێک\",\n      \"clickToAdd\": \"بۆ زیادکردن بۆ ساڵنامە کرتە بکە\",\n      \"name\": \"شێواز و گه‌ڵاڵه‌به‌ندی ڕۆژژمێر\",\n      \"noDateHint\": \"ڕووداوە بێ بەرنامە داڕێژراوەکان لێرەدا نیشان دەدرێن\"\n    },\n    \"referencedCalendarPrefix\": \"دیمەنی...\"\n  },\n  \"errorDialog\": {\n    \"title\": \"هەڵەی⛔️ @:appName\",\n    \"howToFixFallback\": \"ببورن بۆ کێشەکە🥺️! پرسەکە و وەسفەکەی لە لاپەڕەی GitHub ـمان بنێرن.\",\n    \"github\": \"بینین لە GitHub\"\n  },\n  \"search\": {\n    \"label\": \"گەڕان\",\n    \"placeholder\": {\n      \"actions\": \"کردەوەکانی گەڕان...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"ڕوونووس کرا!\",\n      \"fail\": \"ناتوانێت ڕوونووس بکرێت\"\n    }\n  },\n  \"unSupportBlock\": \"وەشانی ئێستا پشتگیری ئەم بلۆکە ناکات.\",\n  \"views\": {\n    \"deleteContentTitle\": \"ئایا دڵنیای کە دەتەوێت {pageType} بسڕیتەوە؟\",\n    \"deleteContentCaption\": \"ئەگەر ئەم {pageType} بسڕیتەوە، دەتوانیت لە سەتڵی زبڵ وەریبگریتەوە.\"\n  },\n  \"colors\": {\n    \"custom\": \"ڕاسپێراو-دڵخواز\",\n    \"default\": \"بنەڕەتی\",\n    \"red\": \"سوور\",\n    \"orange\": \"پرتەقاڵی\",\n    \"yellow\": \"زەرد\",\n    \"green\": \"سەوز\",\n    \"blue\": \"شین\",\n    \"purple\": \"مۆر\",\n    \"pink\": \"پەمەیی\",\n    \"brown\": \"قاوەیی\",\n    \"gray\": \"خۆڵەمێشی\"\n  },\n  \"emoji\": {\n    \"filter\": \"پاڵێو\",\n    \"random\": \"هەڕەمەکی\",\n    \"selectSkinTone\": \"هەڵبژاردنی ڕەنگی پێست\",\n    \"remove\": \"لابردنی ئیمۆجی\",\n    \"categories\": {\n      \"smileys\": \"پێکەنینەکان\",\n      \"people\": \"خەڵک و جەستە\",\n      \"animals\": \"ئاژەڵ و سروشت\",\n      \"food\": \"چێشت و خواردنەوە\",\n      \"activities\": \"چالاکیەکان\",\n      \"places\": \"گەشت و گوزار و شوێنەکان\",\n      \"objects\": \"شتەکان\",\n      \"symbols\": \"هێماکان\",\n      \"flags\": \"ئاڵاکان\",\n      \"nature\": \"سروشت\",\n      \"frequentlyUsed\": \"زۆرجار بەکارت هێناوە\"\n    }\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/cs-CZ.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Já\",\n  \"welcomeText\": \"Vítej\",\n  \"welcomeTo\": \"Vítejte v\",\n  \"githubStarText\": \"Ohvě\",\n  \"subscribeNewsletterText\": \"Přihlásit k odběru newsletteru\",\n  \"letsGoButtonText\": \"Rychlá\",\n  \"title\": \"Název\",\n  \"youCanAlso\": \"Můžete také\",\n  \"and\": \"a\",\n  \"failedToOpenUrl\": \"Nepodařilo se otevřít URL: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Kliknutím přidat pod\",\n    \"addAboveCmd\": \"Alt+kliknutí\",\n    \"addAboveMacCmd\": \"Option+kliknutí\",\n    \"addAboveTooltip\": \"Přidat \",\n    \"dragTooltip\": \"Posouvejte přetažením\",\n    \"openMenuTooltip\": \"Kliknutím otevřete menu\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Regi\",\n    \"title\": \"Zaregistrujte se do @:appName\",\n    \"getStartedText\": \"Začínáme\",\n    \"emptyPasswordError\": \"Heslo nesmí být prázdné\",\n    \"repeatPasswordEmptyError\": \"Heslo pro potvrzení nesmí být prázdné\",\n    \"unmatchedPasswordError\": \"Hesla se neshodují\",\n    \"alreadyHaveAnAccount\": \"Už máte účet?\",\n    \"emailHint\": \"E-mail\",\n    \"passwordHint\": \"Heslo\",\n    \"repeatPasswordHint\": \"Heslo znovu\",\n    \"signUpWith\": \"Zaregistrovat přes:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Přihlásit do @:appName\",\n    \"loginButtonText\": \"Přihlásit\",\n    \"loginStartWithAnonymous\": \"Začít anonymní sezení\",\n    \"continueAnonymousUser\": \"Pokra\",\n    \"continueWithLocalModel\": \"Pokračovat s lokálním modelem\",\n    \"switchToAppFlowyCloud\": \"AppFlowy Cloud\",\n    \"anonymousMode\": \"Anonymní režim\",\n    \"buttonText\": \"Přihlásit se\",\n    \"signingInText\": \"Přihlašování...\",\n    \"forgotPassword\": \"Zapomenuté heslo?\",\n    \"emailHint\": \"E-mail\",\n    \"passwordHint\": \"Heslo\",\n    \"dontHaveAnAccount\": \"Nemáte účet?\",\n    \"createAccount\": \"Vytvořit účet\",\n    \"repeatPasswordEmptyError\": \"Heslo nesmí být prázné\",\n    \"unmatchedPasswordError\": \"Hesla se neshodují\",\n    \"passwordMustContain\": \"Heslo musí obsahovat alespoň jedno písmeno, jedno číslo a jeden symbol.\",\n    \"syncPromptMessage\": \"Synchronizace dat může chvilku trvat. Nezavírejte prosím tuto stránku.\",\n    \"or\": \"NEBO\",\n    \"signInWithGoogle\": \"Pokračovat s Googlem\",\n    \"signInWithGithub\": \"Pokračovat s GitHubem\",\n    \"signInWithDiscord\": \"Pokračovat s Discordem\",\n    \"signInWithApple\": \"Pokračujte s Applem\",\n    \"continueAnotherWay\": \"Pokračovat jinak\",\n    \"signUpWithGoogle\": \"Zaregistrujte se přes Googlu\",\n    \"signUpWithGithub\": \"Zaregistrujte se přes Github\",\n    \"signUpWithDiscord\": \"Zaregistrujte se přes Discord\",\n    \"signInWith\": \"Přihlásit přes:\",\n    \"signInWithEmail\": \"Pokračovat s e-mailem\",\n    \"signInWithMagicLink\": \"Pokračovat\",\n    \"signUpWithMagicLink\": \"Zaregistrujte se přes Magic Link\",\n    \"pleaseInputYourEmail\": \"Zadejte prosím svou e-mailovou adresu\",\n    \"settings\": \"Nastavení\",\n    \"magicLinkSent\": \"Magic Link odeslán!\",\n    \"invalidEmail\": \"Zadejte prosím platnou e-mailovou adresu\",\n    \"alreadyHaveAnAccount\": \"Již máte účet?\",\n    \"logIn\": \"Přihlásit se\",\n    \"generalError\": \"Něco se pokazilo. Zkuste to prosím znovu později.\",\n    \"limitRateError\": \"Z bezpečnostních důvodů můžete požádat o magic link pouze každých 60 sekund.\",\n    \"magicLinkSentDescription\": \"Na váš e-mail byl odeslán magic link. Kliknutím na odkaz dokončíte přihlášení. Platnost odkazu vyprší po 5 minutách.\",\n    \"tokenHasExpiredOrInvalid\": \"Platnost kódu vypršela nebo je kód neplatný. Zkuste to prosím znovu.\",\n    \"signingIn\": \"Přihlašování...\",\n    \"checkYourEmail\": \"Zkontrolujte si e-mail\",\n    \"temporaryVerificationLinkSent\": \"Byl odeslán dočasný ověřovací odkaz.\\nZkontrolujte si prosím svou doručenou schránku\",\n    \"temporaryVerificationCodeSent\": \"Byl odeslán dočasný ověřovací kód.\\nZkontrolujte si prosím doručenou poštu na adrese\",\n    \"continueToSignIn\": \"Pokračovat k přihlášení\",\n    \"continueWithLoginCode\": \"Pokračovat s přihlašovacím kódem\",\n    \"backToLogin\": \"Zpět k přihlášení\",\n    \"enterCode\": \"Zadejte kód\",\n    \"enterCodeManually\": \"Zadejte kód ručně\",\n    \"continueWithEmail\": \"Pokračovat s e-mailem\",\n    \"enterPassword\": \"Zadejte heslo\",\n    \"loginAs\": \"Přihlásit se jako\",\n    \"invalidVerificationCode\": \"Zadejte prosím platný ověřovací kód\",\n    \"tooFrequentVerificationCodeRequest\": \"Odeslali jste příliš mnoho požadavků. Zkuste to prosím znovu později.\",\n    \"invalidLoginCredentials\": \"Vaše heslo je nesprávné, zkuste to prosím znovu.\",\n    \"resetPassword\": \"Obnovit heslo\",\n    \"resetPasswordDescription\": \"Zadejte svůj e-mail pro obnovení hesla\",\n    \"continueToResetPassword\": \"Pokračovat k resetování hesla\",\n    \"resetPasswordSuccess\": \"Úspěšné obnovení hesla\",\n    \"resetPasswordFailed\": \"Obnovení hesla se nezdařilo\",\n    \"resetPasswordLinkSent\": \"Odkaz pro obnovení hesla byl odeslán na váš e-mail. Zkontrolujte si prosím doručenou poštu na adrese\",\n    \"resetPasswordLinkExpired\": \"Platnost odkazu pro obnovení hesla vypršela. Požádejte o nový odkaz.\",\n    \"resetPasswordLinkInvalid\": \"Odkaz pro resetování hesla je neplatný. Požádejte o nový odkaz.\",\n    \"enterNewPasswordFor\": \"Zadejte nové heslo pro \",\n    \"newPassword\": \"Nové heslo\",\n    \"enterNewPassword\": \"Zadejte nové heslo\",\n    \"confirmPassword\": \"Potvrzení hesla\",\n    \"confirmNewPassword\": \"Zadejte nové heslo\",\n    \"newPasswordCannotBeEmpty\": \"Nové heslo nemůže být prázdné\",\n    \"confirmPasswordCannotBeEmpty\": \"Potvrzení hesla nemůže být prázdné\",\n    \"passwordsDoNotMatch\": \"Hesla se neshodují\",\n    \"verifying\": \"Ověřování...\",\n    \"continueWithPassword\": \"Pokračovat s heslem\",\n    \"youAreInLocalMode\": \"Jste v lokálním režimu.\",\n    \"loginToAppFlowyCloud\": \"Přihlášení do AppFlowy Cloud\",\n    \"LogInWithGoogle\": \"Přihlásit přes Google\",\n    \"LogInWithGithub\": \"Přihlásit přes GitHub\",\n    \"LogInWithDiscord\": \"Přihlásit přes Discord\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Vyberte pracovní ůplochu\",\n    \"defaultName\": \"Můj pracovní prostor\",\n    \"create\": \"Vytvořit pracovní prostor\",\n    \"new\": \"Nový pracovní prostor\",\n    \"importFromNotion\": \"Importovat z Notionu\",\n    \"learnMore\": \"Zjistěte více\",\n    \"reset\": \"Resetovat plochu\",\n    \"renameWorkspace\": \"Přejmenovat pracovní prostor\",\n    \"workspaceNameCannotBeEmpty\": \"Název pracovního prostoru nemůže být prázdný\",\n    \"resetWorkspacePrompt\": \"Obnovením pracovního prostoru smažete všechny stránky a data v nich. Opravdu chcete obnovit pracovní prostor? Pro obnovení pracovního prostoru můžete kontaktovat podporu.\",\n    \"hint\": \"pracovní plocha\",\n    \"notFoundError\": \"Pracovní prostor nenalezen\",\n    \"failedToLoad\": \"Něco se pokazilo! Nepodařilo se načíst pracovní prostor. Zkuste zavřít a znovu otevřít @:appName a zkuste to znovu.\",\n    \"errorActions\": {\n      \"reportIssue\": \"Nahlásit problém\",\n      \"reportIssueOnGithub\": \"Nahlásit problém na Githubu\",\n      \"exportLogFiles\": \"Exportovat soubory protokolu\",\n      \"reachOut\": \"Ozvat se na Discordu\"\n    },\n    \"menuTitle\": \"Pracovní prostory\",\n    \"deleteWorkspaceHintText\": \"Opravdu chcete smazat pracovní prostor? Tuto akci nelze vrátit zpět a všechny zveřejněné stránky přestanou být dostupné.\",\n    \"createSuccess\": \"Pracovní prostor byl úspěšně vytvořen\",\n    \"createFailed\": \"Vytvoření pracovního prostoru se nezdařilo\",\n    \"createLimitExceeded\": \"Dosáhli jste maximálního povoleného počtu pracovních prostorů pro váš účet. Pokud potřebujete další pracovní prostory pro pokračování v práci, požádejte o ně na Githubu\",\n    \"deleteSuccess\": \"Pracovní prostor byl úspěšně smazán\",\n    \"deleteFailed\": \"Nepodařilo se smazat pracovní prostor\",\n    \"openSuccess\": \"Pracovní prostor bylo úspěšně otevřeno\",\n    \"openFailed\": \"Nepodařilo se otevřít pracovní prostor\",\n    \"renameSuccess\": \"Pracovní prostor byl úspěšně přejmenován\",\n    \"renameFailed\": \"Přejmenování pracovního prostoru se nezdařilo\",\n    \"updateIconSuccess\": \"Ikona pracovního prostoru byla úspěšně aktualizována\",\n    \"updateIconFailed\": \"Aktualizace ikony pracovního prostoru se nezdařilo\",\n    \"cannotDeleteTheOnlyWorkspace\": \"Nelze smazat jediný pracovní prostor\",\n    \"fetchWorkspacesFailed\": \"Nepodařilo se načíst pracovní prostory\",\n    \"leaveCurrentWorkspace\": \"Opustit pracovní prostor\",\n    \"leaveCurrentWorkspacePrompt\": \"Opravdu chcete opustit aktuální pracovní prostor?\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Sdílet\",\n    \"workInProgress\": \"Ji brzy\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"Kopírovat do schránky\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Kopírovat odkaz\",\n    \"publishToTheWeb\": \"Publikovat na webu\",\n    \"publishToTheWebHint\": \"Vytvořte si webové stránky s AppFlowy\",\n    \"publish\": \"Zveřejnit\",\n    \"unPublish\": \"Zrušit zveřejnění\",\n    \"visitSite\": \"Navštívit stránku\",\n    \"exportAsTab\": \"Exportovat jako\",\n    \"publishTab\": \"Zveřejnit\",\n    \"shareTab\": \"Sdílet\",\n    \"publishOnAppFlowy\": \"Zveřejnit na AppFlowy\",\n    \"shareTabTitle\": \"Pozvat ke spolupráci\",\n    \"shareTabDescription\": \"Pro snadnou spolupráci s kýmkoli\",\n    \"copyLinkSuccess\": \"Odkaz zkopírován do schránky\",\n    \"copyShareLink\": \"Kopírovat odkaz pro sdílení\",\n    \"copyLinkFailed\": \"Nepodařilo se zkopírovat odkaz do schránky\",\n    \"copyLinkToBlockSuccess\": \"Odkaz na blok zkopírován do schránky\",\n    \"copyLinkToBlockFailed\": \"Nepodařilo se zkopírovat odkaz bloku do schránky\",\n    \"manageAllSites\": \"Spravovat všechny stránky\",\n    \"updatePathName\": \"Aktualizovat název cesty\"\n  },\n  \"moreAction\": {\n    \"small\": \"malé\",\n    \"medium\": \"střední\",\n    \"large\": \"velké\",\n    \"fontSize\": \"Velikost písma\",\n    \"import\": \"Importovat\",\n    \"moreOptions\": \"Ví\",\n    \"wordCount\": \"Počet slov: {}\",\n    \"charCount\": \"Počet znaků: {}\",\n    \"createdAt\": \"Vytvořeno: {}\",\n    \"deleteView\": \"Smazat\",\n    \"duplicateView\": \"Duplikovat\",\n    \"wordCountLabel\": \"Počet slov: \",\n    \"charCountLabel\": \"Počet znaků: \",\n    \"createdAtLabel\": \"Vytvořeno: \",\n    \"syncedAtLabel\": \"Synchronizováno: \",\n    \"saveAsNewPage\": \"Přidat zprávy na stránku\",\n    \"saveAsNewPageDisabled\": \"Žádné zprávy k dispozici\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Text a Markdown\",\n    \"documentFromV010\": \"Dokument z v0.1.0\",\n    \"databaseFromV010\": \"Databázi z v0.1.0\",\n    \"notionZip\": \"Exportovaný soubor ZIP z Notion\",\n    \"csv\": \"CSV\",\n    \"database\": \"Databáze\"\n  },\n  \"emojiIconPicker\": {\n    \"iconUploader\": {\n      \"placeholderLeft\": \"Přetáhněte soubor, klikněte na \",\n      \"placeholderUpload\": \"Nahrát\",\n      \"placeholderRight\": \"nebo vložte odkaz obrázku.\",\n      \"dropToUpload\": \"Přetáhněte soubor k nahrání\",\n      \"change\": \"Změnit\"\n    }\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Přejmenovat\",\n    \"delete\": \"Smazat\",\n    \"duplicate\": \"Duplikovat\",\n    \"unfavorite\": \"Odebrat z oblíbených\",\n    \"favorite\": \"Přidat do oblíbených\",\n    \"openNewTab\": \"Otevřít v novém panelu\",\n    \"moveTo\": \"Přesunout do\",\n    \"addToFavorites\": \"Přidat do oblíbených\",\n    \"copyLink\": \"Kopírovat odkaz\",\n    \"changeIcon\": \"Změnit ikonu\",\n    \"collapseAllPages\": \"Sbalit všechny podstránky\",\n    \"movePageTo\": \"Přesunout stránku na\",\n    \"move\": \"Přesunout\",\n    \"lockPage\": \"Zamknout stránku\"\n  },\n  \"blankPageTitle\": \"Prázdná stránka\",\n  \"newPageText\": \"Nová stránka\",\n  \"newDocumentText\": \"Nový dokument\",\n  \"newGridText\": \"Nová mřížka\",\n  \"newCalendarText\": \"Nový kalendář\",\n  \"newBoardText\": \"Nová nástěnka\",\n  \"chat\": {\n    \"newChat\": \"AI Chat\",\n    \"inputMessageHint\": \"Zeptejte se @:appName AI\",\n    \"inputLocalAIMessageHint\": \"Zeptejte se @:appName Lokální AI\",\n    \"unsupportedCloudPrompt\": \"Tato funkce je k dispozici pouze při použití @:appName Cloud\",\n    \"relatedQuestion\": \"Navrhované\",\n    \"serverUnavailable\": \"Připojení ztraceno. Zkontrolujte prosím připojení k internetu a\",\n    \"aiServerUnavailable\": \"AI je dočasně nedostupná. Zkuste to prosím znovu později.\",\n    \"retry\": \"Zkusit znovu\",\n    \"clickToRetry\": \"Kliknutím zkusíte znovu\",\n    \"regenerateAnswer\": \"Znovu generovat\",\n    \"question1\": \"Jak používat Kanban ke správě úkolů\",\n    \"question2\": \"Vysvětli metodu GTD\",\n    \"question3\": \"Proč používat Rust\",\n    \"question4\": \"Recept s tím, co mám v kuchyni\",\n    \"question5\": \"Vytvoř ilustraci pro mou stránku\",\n    \"question6\": \"Napsat si seznam úkolů na nadcházející týden\",\n    \"aiMistakePrompt\": \"Umělá inteligence může dělat chyby. Zkontrolujte důležité informace.\",\n    \"chatWithFilePrompt\": \"Chcete si s tím souborem popovídat?\",\n    \"indexFileSuccess\": \"Indexování souboru úspěšně\",\n    \"inputActionNoPages\": \"Žádné výsledky na stránce\",\n    \"referenceSource\": {\n      \"zero\": \"Nalezeno 0 zdrojů\",\n      \"one\": \"{count} nalezený zdroj\",\n      \"other\": \"{count} nalezených zdrojů\"\n    },\n    \"clickToMention\": \"Zmínit stránku\",\n    \"uploadFile\": \"Připojte PDF, textové soubory nebo soubory MarkDown\",\n    \"questionDetail\": \"Ahoj {}! Jak ti dnes můžu pomoct?\",\n    \"indexingFile\": \"Indexování {}\",\n    \"generatingResponse\": \"Generování odpovědi\",\n    \"selectSources\": \"Vyberte zdroje\",\n    \"currentPage\": \"Aktuální stránka\",\n    \"sourcesLimitReached\": \"Můžete vybrat maximálně 3 dokumenty nejvyšší úrovně a jejich podřízené dokumenty\",\n    \"sourceUnsupported\": \"V současné době nepodporujeme chatování s databázemi.\",\n    \"regenerate\": \"Zkuste to znovu\",\n    \"addToPageButton\": \"Přidat zprávu na stránku\",\n    \"addToPageTitle\": \"Přidat zprávu do...\",\n    \"addToNewPage\": \"Vytvořit novou stránku\",\n    \"addToNewPageName\": \"Zprávy extrahované z „{}“\",\n    \"addToNewPageSuccessToast\": \"Zpráva přidána do\",\n    \"openPagePreviewFailedToast\": \"Nepodařilo se otevřít stránku\",\n    \"changeFormat\": {\n      \"actionButton\": \"Změnit formát\",\n      \"confirmButton\": \"Znovu generovat s tímto formátem\",\n      \"textOnly\": \"Text\",\n      \"imageOnly\": \"Pouze obrázek\",\n      \"textAndImage\": \"Text a obrázek\",\n      \"text\": \"Odstavec\",\n      \"bullet\": \"Seznam s odrážkami\",\n      \"number\": \"Číslovaný seznam\",\n      \"table\": \"Tabulka\",\n      \"blankDescription\": \"Formátovat odpověď\",\n      \"defaultDescription\": \"Formát automatické odpovědi\",\n      \"textWithImageDescription\": \"@:chat.changeFormat.text s obrázkem\"\n    },\n    \"switchModel\": {\n      \"label\": \"Změnit model\",\n      \"localModel\": \"Lokální model\",\n      \"cloudModel\": \"Cloudový model\",\n      \"autoModel\": \"Auto\"\n    },\n    \"selectBanner\": {\n      \"saveButton\": \"Přidat k …\",\n      \"selectMessages\": \"Vyberte zprávy\",\n      \"nSelected\": \"{} vybráno\",\n      \"allSelected\": \"Vše vybráno\"\n    },\n    \"stopTooltip\": \"Zastavit generování\"\n  },\n  \"trash\": {\n    \"text\": \"Koš\",\n    \"restoreAll\": \"Obnovit vše\",\n    \"restore\": \"Obnovit\",\n    \"deleteAll\": \"Smazat vše\",\n    \"pageHeader\": {\n      \"fileName\": \"Název souboru\",\n      \"lastModified\": \"Změněno\",\n      \"created\": \"Vytvořeno\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Opravdu chcete smazat všechny stránky v Koši?\",\n      \"caption\": \"Tento krok nelze vrátit.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Opravdu chcete obnovit všechny stránky z Koše?\",\n      \"caption\": \"Tuto kci ne\"\n    },\n    \"restorePage\": {\n      \"title\": \"Obnovit: {}\",\n      \"caption\": \"Jste si jisti, že chcete tuto stránku obnovit?\"\n    },\n    \"mobile\": {\n      \"actions\": \"Koš - akce\",\n      \"empty\": \"Koš je prázdný\",\n      \"emptyDescription\": \"Nemáte žádný smazaný soubor\",\n      \"isDeleted\": \"je smazaný\",\n      \"isRestored\": \"je \"\n    },\n    \"confirmDeleteTitle\": \"Opravdu chcete tuto stránku trvale smazat?\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Tato stránka je v Koši\",\n    \"restore\": \"Obnovit stránku\",\n    \"deletePermanent\": \"Trvale smazat\",\n    \"deletePermanentDescription\": \"Jste si jisti, že chcete tuto stránku trvale smazat? Toto je nevratné.\"\n  },\n  \"dialogCreatePageNameHint\": \"Název stránky\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Klávesové zkratky\",\n    \"whatsNew\": \"Co je nového?\",\n    \"helpAndDocumentation\": \"Nápověda a dokumentace\",\n    \"getSupport\": \"Získejte podporu\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"Debug informace\",\n      \"success\": \"Debug informace zkopírovány do schránky!\",\n      \"fail\": \"Nepodařilo se zkopáí\"\n    },\n    \"feedback\": \"Zpětná vazba\",\n    \"help\": \"Pomoc a podpora\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Smazat, přejmenovat, a další...\",\n    \"addPageTooltip\": \"Rychle přidat podstránku\",\n    \"defaultNewPageName\": \"Nepojmenovaný\",\n    \"renameDialog\": \"Přejmenovat\",\n    \"pageNameSuffix\": \"Kopírovat\"\n  },\n  \"noPagesInside\": \"Neobsahuje žádné stránky\",\n  \"toolbar\": {\n    \"undo\": \"Zpět\",\n    \"redo\": \"Znovu\",\n    \"bold\": \"Tučné\",\n    \"italic\": \"Kurzíva\",\n    \"underline\": \"Podtřžení\",\n    \"strike\": \"Přeškrtnutí\",\n    \"numList\": \"Číslovaný seznam\",\n    \"bulletList\": \"Seznam s odrážkami\",\n    \"checkList\": \"Zaškrtávací seznam\",\n    \"inlineCode\": \"Vložený kód\",\n    \"quote\": \"Blok s citací\",\n    \"header\": \"Nadpis\",\n    \"highlight\": \"Zv\",\n    \"color\": \"Barva\",\n    \"addLink\": \"Přidat odkaz\",\n    \"link\": \"Odkaz\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Přepnout na světlý režim\",\n    \"darkMode\": \"Přepnout na tmavý režim\",\n    \"openAsPage\": \"Otevřít jako stránku\",\n    \"addNewRow\": \"Přidat nový řádek\",\n    \"openMenu\": \"Kliknutím otevřete menu\",\n    \"dragRow\": \"Dlou\",\n    \"viewDataBase\": \"Zobrazit databázi\",\n    \"referencePage\": \"Zdroj\",\n    \"addBlockBelow\": \"Přidat blok pod\",\n    \"aiGenerate\": \"Generovat\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Zavřít postranní panel\",\n    \"openSidebar\": \"Otevřít postranní panel\",\n    \"expandSidebar\": \"Rozbalit na celou stránku\",\n    \"personal\": \"Osobní\",\n    \"private\": \"Soukromé\",\n    \"workspace\": \"Pracovní prostor\",\n    \"favorites\": \"Oblíbené\",\n    \"clickToHidePrivate\": \"Kliknutím skryjete soukromý prostor\\nStránky, které jste zde vytvořili, jsou viditelné pouze pro vás\",\n    \"clickToHideWorkspace\": \"Kliknutím skryjete pracovní plochu\\nStránky, které jste zde vytvořili, jsou viditelné pro všechny členy\",\n    \"clickToHidePersonal\": \"Kliknutím schováte sekci Osobní\",\n    \"clickToHideFavorites\": \"Kliknutím schováte sekci Oblíbené\",\n    \"addAPage\": \"Přidat stránku\",\n    \"addAPageToPrivate\": \"Přidat stránku do soukromého prostoru\",\n    \"addAPageToWorkspace\": \"Přidat stránku do pracovního prostoru\",\n    \"recent\": \"Nedávné\",\n    \"today\": \"Dnes\",\n    \"thisWeek\": \"Tento týden\",\n    \"others\": \"Dřívější oblíbené\",\n    \"earlier\": \"Dříve\",\n    \"justNow\": \"právě teď\",\n    \"minutesAgo\": \"před {count} minutami\",\n    \"lastViewed\": \"Naposledy zobrazené\",\n    \"favoriteAt\": \"Přidáno do oblíbených\",\n    \"emptyRecent\": \"Žádné nedávné stránky\",\n    \"emptyRecentDescription\": \"Jakmile si zobrazíte stránky, objeví se zde pro snadné opětovné zobrazení.\",\n    \"emptyFavorite\": \"Žádné oblíbené stránky\",\n    \"emptyFavoriteDescription\": \"Označte si stránky jako oblíbené – budou zde uvedeny pro rychlý přístup!\",\n    \"removePageFromRecent\": \"Odebrat tuto stránku ze složky Nedávné?\",\n    \"removeSuccess\": \"Úspěšně odstraněno\",\n    \"favoriteSpace\": \"Oblíbené\",\n    \"RecentSpace\": \"Nedávné\",\n    \"Spaces\": \"Prostory\",\n    \"upgradeToPro\": \"Upgradujte na Pro verzi\",\n    \"upgradeToAIMax\": \"Odemkněte neomezenou AI\",\n    \"storageLimitDialogTitle\": \"Došlo vám bezplatné úložiště. Upgradujte a odemkněte si neomezené úložiště.\",\n    \"storageLimitDialogTitleIOS\": \"Došlo vám bezplatné úložiště.\",\n    \"aiResponseLimitTitle\": \"Došly vám bezplatné odpovědi s AI. Přejděte na tarif Pro nebo si zakupte doplněk s AI a odemkněte si neomezený počet odpovědí.\",\n    \"aiResponseLimitDialogTitle\": \"Dosažen limit odpovědí umělé inteligence\",\n    \"aiResponseLimit\": \"Došly vám bezplatné odpovědi AI.\\nPřejděte do Nastavení -> Plán -> Klikněte na AI Max nebo Pro Plan a získejte více odpovědí AI\",\n    \"askOwnerToUpgradeToPro\": \"Vašemu pracovnímu prostoru dochází bezplatné úložiště. Požádejte prosím vlastníka pracovního prostoru o upgrade na tarif Pro.\",\n    \"askOwnerToUpgradeToProIOS\": \"Vašemu pracovnímu prostoru dochází bezplatné úložiště.\",\n    \"askOwnerToUpgradeToAIMax\": \"Ve vašem pracovním prostoru došly bezplatné odpovědi AI. Požádejte vlastníka pracovního prostoru o upgrade tarifu nebo o zakoupení doplňků AI.\",\n    \"askOwnerToUpgradeToAIMaxIOS\": \"Vašemu pracovnímu prostoru docházejí bezplatné odpovědi umělé inteligence.\",\n    \"purchaseAIMax\": \"Ve vašem pracovním prostoru došly odpovědi AI Image. Požádejte prosím vlastníka pracovního prostoru o zakoupení AI Max.\",\n    \"aiImageResponseLimit\": \"Došly vám obrazové odpovědi umělé inteligence.<inlang-LineFeed>\\nPřejděte do Nastavení -> Plán -> Klikněte na AI Max pro získání dalších obrázkových odpovědí s umělou inteligencí.\",\n    \"purchaseStorageSpace\": \"Zakoupit úložný prostor\",\n    \"singleFileProPlanLimitationDescription\": \"Překročili jste maximální povolenou velikost nahrávaného souboru v bezplatném tarifu. Pro nahrávání větších souborů prosím upgradujte na tarif Pro.\",\n    \"purchaseAIResponse\": \"Nákup \",\n    \"askOwnerToUpgradeToLocalAI\": \"Požádejte vlastníka pracovního prostoru o povolení AI na zařízení\",\n    \"upgradeToAILocal\": \"Spouštějte lokální modely na svém zařízení pro maximální soukromí\",\n    \"upgradeToAILocalDesc\": \"Chatujte s PDF, vylepšete si psaní a automaticky vyplňujte tabulky pomocí lokální umělé inteligence\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Poznámka exportována do Markdownu\",\n      \"path\": \"Dokumenty/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"KontaktyCo se \",\n    \"whatsHappening\": \"Co se děje v tomto týdnu?\",\n    \"addContact\": \"Přidat kontakt\",\n    \"editContact\": \"Upravit kontakt\"\n  },\n  \"button\": {\n    \"ok\": \"OK\",\n    \"confirm\": \"Potvrdit\",\n    \"done\": \"Hotovo\",\n    \"cancel\": \"Zrušit\",\n    \"signIn\": \"Přihlásit se\",\n    \"signOut\": \"Odhlásit se\",\n    \"complete\": \"Dokončit\",\n    \"change\": \"Změnit\",\n    \"save\": \"Uložit\",\n    \"generate\": \"Vygenerovat\",\n    \"esc\": \"ESC\",\n    \"keep\": \"Z\",\n    \"tryAgain\": \"Zkusit znovu\",\n    \"discard\": \"Zahodit\",\n    \"replace\": \"Nahradit\",\n    \"insertBelow\": \"Vložit pod\",\n    \"insertAbove\": \"Vložit nad\",\n    \"upload\": \"Nahrát\",\n    \"edit\": \"Upravit\",\n    \"delete\": \"Smazat\",\n    \"copy\": \"Kopírovat\",\n    \"duplicate\": \"Duplikovat\",\n    \"putback\": \"Odložit\",\n    \"update\": \"Aktualizovat\",\n    \"share\": \"Sdílet\",\n    \"removeFromFavorites\": \"Odstranit z oblíbených\",\n    \"removeFromRecent\": \"Odebrat z nedávných\",\n    \"addToFavorites\": \"Přidat do oblíbených\",\n    \"favoriteSuccessfully\": \"Úspěšně přidáno do oblíbených\",\n    \"unfavoriteSuccessfully\": \"Úspěšně odebráno z oblíbených\",\n    \"duplicateSuccessfully\": \"Duplikování proběhlo úspěšné\",\n    \"rename\": \"Přejmenovat\",\n    \"helpCenter\": \"Centrum pomoci\",\n    \"add\": \"Přidat\",\n    \"yes\": \"Ano\",\n    \"no\": \"Ne\",\n    \"clear\": \"Smazat\",\n    \"remove\": \"Odebrat\",\n    \"dontRemove\": \"Neodebírat\",\n    \"copyLink\": \"Kopírovat odkaz\",\n    \"align\": \"Zarovnat\",\n    \"login\": \"Přihlášení\",\n    \"logout\": \"Odhlásit se\",\n    \"deleteAccount\": \"Smazat účet\",\n    \"back\": \"Zpět\",\n    \"signInGoogle\": \"Pokračovat s Googlem\",\n    \"signInGithub\": \"Pokračovat s GitHubem\",\n    \"signInDiscord\": \"Pokračovat s Discordem\",\n    \"more\": \"Více\",\n    \"create\": \"Vytvořit\",\n    \"close\": \"Zavřít\",\n    \"next\": \"Další\",\n    \"previous\": \"Předchozí\",\n    \"submit\": \"Odeslat\",\n    \"download\": \"Stáhnout\",\n    \"backToHome\": \"Zpět na domovskou stránku\",\n    \"viewing\": \"Prohlížení\",\n    \"editing\": \"Úprava\",\n    \"gotIt\": \"Rozumím\",\n    \"retry\": \"Zkusit znovu\",\n    \"uploadFailed\": \"Nahrávání se nezdařilo.\",\n    \"copyLinkOriginal\": \"Kopírovat odkaz na originál\"\n  },\n  \"label\": {\n    \"welcome\": \"Vítejte!\",\n    \"firstName\": \"Křestní jméno\",\n    \"middleName\": \"Prostřední jméno\",\n    \"lastName\": \"Příjmení\",\n    \"stepX\": \"Krok {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Nepodařilo se připojit k Vašemu účtu.\",\n      \"failedMsg\": \"Ujistěte se prosím, že jste se v prohlížeči přihlásili.\"\n    },\n    \"google\": {\n      \"title\": \"Goohle přihlašování\",\n      \"instruction1\": \"Tuto aplikaci musíte autorizovat, aby mohla importovat Vaše kontakty z Google Contacts.\",\n      \"instruction2\": \"Zkopírujte tento k=od do schránky kliknutím na ikonku nebo označením textu\",\n      \"instruction3\": \"Přejděte na tento odkaz kam vložte kód:\",\n      \"instruction4\": \"Po dokončení registrace stiskněte tlačítko níže:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Nastavení\",\n    \"popupMenuItem\": {\n      \"settings\": \"Nastavení\",\n      \"members\": \"Členové\",\n      \"trash\": \"Koš\",\n      \"helpAndDocumentation\": \"Nápověda a dokumentace\",\n      \"getSupport\": \"Získejte podporu\"\n    },\n    \"sites\": {\n      \"title\": \"Stránky\",\n      \"namespaceTitle\": \"Jmenný prostor\",\n      \"namespaceDescription\": \"Spravujte svůj jmenný prostor a domovskou stránku\",\n      \"namespaceHeader\": \"Jmenný prostor\",\n      \"homepageHeader\": \"Domovská stránka\",\n      \"updateNamespace\": \"Aktualizovat jmenný prostor\",\n      \"removeHomepage\": \"Odebrat domovskou stránku\",\n      \"selectHomePage\": \"Vyberte stránku\",\n      \"clearHomePage\": \"Vymazat domovskou stránku pro tento jmenný prostor\",\n      \"customUrl\": \"Vlastní URL\",\n      \"homePage\": {\n        \"upgradeToPro\": \"Upgradujte na Pro Plan a nastavte si domovskou stránku\"\n      },\n      \"namespace\": {\n        \"description\": \"Tato změna se vztahuje na všechny publikované stránky v tomto jmenném prostoru.\",\n        \"tooltip\": \"Vyhrazujeme si právo odstranit jakékoli nevhodné jmenné prostory.\",\n        \"updateExistingNamespace\": \"Aktualizovat existující jmenný prostor\",\n        \"upgradeToPro\": \"Přejděte na tarif Pro a získejte vlastní jmenný prostor.\",\n        \"redirectToPayment\": \"Přesměrování na platební stránku...\",\n        \"onlyWorkspaceOwnerCanSetHomePage\": \"Domovskou stránku může nastavit pouze vlastník pracovního prostoru\",\n        \"pleaseAskOwnerToSetHomePage\": \"Požádejte prosím vlastníka pracovního prostoru o upgrade na tarif Pro.\"\n      },\n      \"publishedPage\": {\n        \"title\": \"Všechny publikované stránky\",\n        \"description\": \"Spravujte své publikované stránky\",\n        \"page\": \"Strana\",\n        \"pathName\": \"Název cesty\",\n        \"date\": \"Datum zveřejnění\",\n        \"emptyHinText\": \"V tomto pracovním prostoru nemáte žádné zveřejněné stránky.\",\n        \"noPublishedPages\": \"Žádné zveřejněné stránky\",\n        \"settings\": \"Nastavení zveřejnění\",\n        \"clickToOpenPageInApp\": \"Otevřít stránku v aplikaci\",\n        \"clickToOpenPageInBrowser\": \"Otevřít stránku v prohlížeči\"\n      },\n      \"error\": {\n        \"failedToGeneratePaymentLink\": \"Nepodařilo se vygenerovat platební odkaz pro tarif Pro.\",\n        \"failedToUpdateNamespace\": \"Nepodařilo se aktualizovat jmenný prostor.\",\n        \"proPlanLimitation\": \"Pro aktualizaci jmenného prostoru je nutné upgradovat na tarif Pro.\",\n        \"namespaceAlreadyInUse\": \"Jmenný prostor je již obsazen, zkuste prosím jiný\",\n        \"invalidNamespace\": \"Neplatný jmenný prostor, zkuste prosím jiný\",\n        \"namespaceLengthAtLeast2Characters\": \"Jmenný prostor musí mít délku alespoň 2 znaky\",\n        \"onlyWorkspaceOwnerCanUpdateNamespace\": \"Pouze vlastník pracovního prostoru může aktualizovat jmenný prostor\",\n        \"onlyWorkspaceOwnerCanRemoveHomepage\": \"Domovskou stránku může odstranit pouze vlastník pracovního prostoru.\",\n        \"setHomepageFailed\": \"Nastavení domovské stránky se nezdařilo\",\n        \"namespaceTooLong\": \"Jmenný prostor je příliš dlouhý, zkuste prosím jiný\",\n        \"namespaceTooShort\": \"Jmenný prostor je příliš krátký, zkuste prosím jiný\",\n        \"namespaceIsReserved\": \"Jmenný prostor je rezervovaný, zkuste prosím jiný\",\n        \"updatePathNameFailed\": \"Nepodařilo se aktualizovat název cesty\",\n        \"removeHomePageFailed\": \"Odstranění domovské stránky se nezdařilo\",\n        \"publishNameContainsInvalidCharacters\": \"Název cesty obsahuje neplatné znaky, zkuste prosím jiný.\",\n        \"publishNameTooShort\": \"Název cesty je příliš krátký, zkuste prosím jiný\",\n        \"publishNameTooLong\": \"Název cesty je příliš dlouhý, zkuste prosím jiný\",\n        \"publishNameAlreadyInUse\": \"Název cesty se již používá, zkuste prosím jiný\"\n      }\n    },\n    \"accountPage\": {\n      \"login\": {\n        \"title\": \"Přihlášení k účtu\",\n        \"loginLabel\": \"Přihlásit se\",\n        \"logoutLabel\": \"Odhlásit se\"\n      }\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"Pracovní prostor\",\n      \"title\": \"Pracovní prostor\",\n      \"workspaceName\": {\n        \"title\": \"Název pracovního prostoru\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"Ikona pracovního prostoru\",\n        \"description\": \"Nahrajte obrázek nebo použijte emoji pro svůj pracovní prostor. Ikona se zobrazí v postranním panelu a v oznámeních.\"\n      },\n      \"appearance\": {\n        \"options\": {\n          \"light\": \"Světlý\",\n          \"dark\": \"Tmavý\"\n        }\n      },\n      \"textDirection\": {\n        \"leftToRight\": \"Zleva doprava\",\n        \"rightToLeft\": \"Zprava doleva\"\n      },\n      \"layoutDirection\": {\n        \"leftToRight\": \"Zleva doprava\",\n        \"rightToLeft\": \"Zprava doleva\"\n      },\n      \"dateTime\": {\n        \"title\": \"Datum a čas\",\n        \"24HourTime\": \"24 hodinový čas\",\n        \"dateFormat\": {\n          \"label\": \"Formát datumu\",\n          \"local\": \"Místní\",\n          \"us\": \"US\"\n        }\n      },\n      \"language\": {\n        \"title\": \"Jazyk\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"Smazat pracovní prostor\",\n        \"content\": \"Opravdu chcete smazat tento pracovní prostor? Tuto akci nelze vrátit zpět a všechny zveřejněné stránky budou zrušeny.\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"Opustit pracovní prostor\",\n        \"fail\": \"Nepodařilo se opustit pracovní prostor.\"\n      }\n    },\n    \"menu\": {\n      \"appearance\": \"Vzhled\",\n      \"language\": \"Jazyk\",\n      \"user\": \"Uživatel\",\n      \"files\": \"Soubory\",\n      \"notifications\": \"Upozornění\",\n      \"open\": \"Otevřít nastavení\",\n      \"logout\": \"Odhlásit se\",\n      \"logoutPrompt\": \"Opravdu se chcete odhlásit?\",\n      \"selfEncryptionLogoutPrompt\": \"Opravdu se chcete odhlásit? Ujistěte se prosím, že jste si zkopírovali šifrovací klíč\",\n      \"syncSetting\": \"Synchronizovat nastavení\",\n      \"enableSync\": \"Zapnout synchronizaci\",\n      \"enableEncrypt\": \"Šifrovat data\",\n      \"cloudURL\": \"URL adresa serveru\",\n      \"cloudAppFlowy\": \"@:appName Cloud Beta\",\n      \"enableEncryptPrompt\": \"Zapněte šifrování a zabezpečte svá \",\n      \"inputEncryptPrompt\": \"Vložte prosím Váš šifrovací klíč k\",\n      \"clickToCopySecret\": \"Kliknutím zkopírujete šifrovací klíč\",\n      \"inputTextFieldHint\": \"Váš klíč\",\n      \"historicalUserList\": \"Historie přihlášení uživatele\",\n      \"historicalUserListTooltip\": \"V tomto seznamu vidíte anonymní účty. Kliknutím na účet zobrazíte jeho detaily. Anonymní účty vznikají kliknutím na tlačítko \\\"Začínáme\\\"\",\n      \"openHistoricalUser\": \"Kliknutím založíte anonymní účet\",\n      \"customPathPrompt\": \"Uložením složky s daty @:appName ve složce synchronizovanéí jako např. Google Drive může nést rizika. Pokud se databáze v  složce navštíví nebo změní \",\n      \"cloudSetting\": \"Nastavení cloudu\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Po\",\n        \"hint\": \"Vypn\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"Obnovit tato nastavení\",\n      \"fontFamily\": {\n        \"label\": \"Písmo\",\n        \"search\": \"Vyhledávání\"\n      },\n      \"themeMode\": {\n        \"label\": \"Téma vzhledu\",\n        \"light\": \"Světlý vzhled\",\n        \"dark\": \"Tmavý vzhled\",\n        \"system\": \"Přizpůsobit systému\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Směr zobrazení\",\n        \"hint\": \"Ovládejte tok obsahu na \",\n        \"ltr\": \"Zleva doprava\",\n        \"rtl\": \"Zprava doleva\"\n      },\n      \"textDirection\": {\n        \"label\": \"Výchozí směr textu\",\n        \"hint\": \"Vyberte, jestli má text ve výchozím nastavení začínat zprava nebo zleva.\",\n        \"ltr\": \"Zleva doprava\",\n        \"rtl\": \"Zprava doleva\",\n        \"auto\": \"Automaticky\",\n        \"fallback\": \"Stejné jako směr rozvržení\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Nahrát\",\n        \"uploadTheme\": \"Nahrát motiv vzhledu\",\n        \"description\": \"Nahrajte vlastní motiv vzhledu pro @:appName stisknutím tlačítka níže.\",\n        \"loading\": \"Prosím počkejte dokud nedokončíme kontrolu a nahrávání vašeho motivu vzhledu...\",\n        \"uploadSuccess\": \"Váš motiv vzhledu byl úspěšně nahrán\",\n        \"deletionFailure\": \"Nepodařilo se smazat motiv vzhledu. Zkuste ho smazat ručně.\",\n        \"filePickerDialogTitle\": \"Vyberte soubor typu .flowy_plugin\",\n        \"urlUploadFailure\": \"Nepodařilo se otevřít URL adresu: {}\",\n        \"failure\": \"Nahrané téma vzhledu má neplatný formát.\"\n      },\n      \"theme\": \"Motiv vzhledu\",\n      \"builtInsLabel\": \"Vestavěné motivy vzhledu\",\n      \"pluginsLabel\": \"Dopl\",\n      \"dateFormat\": {\n        \"label\": \"Formát data\",\n        \"local\": \"Místní\",\n        \"us\": \"US\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"Přátelský\",\n        \"dmy\": \"D/M/Y\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Formát času\",\n        \"twelveHour\": \"12hodinový\",\n        \"twentyFourHour\": \"24hodinový\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"Zobrazit d\"\n    },\n    \"files\": {\n      \"copy\": \"Kopíá\",\n      \"defaultLocation\": \"Umístění pro čtení a ukládání dat\",\n      \"exportData\": \"Exportovat data\",\n      \"doubleTapToCopy\": \"Dvojitým klepnutím zkopírujete cestu\",\n      \"restoreLocation\": \"Obnovit výchozí @:appName cestu\",\n      \"customizeLocation\": \"OtevřítProsím tre další složku\",\n      \"restartApp\": \"Aby se projevily změny, restartujte prosím aplikaci.\",\n      \"exportDatabase\": \"Exportovat databázi\",\n      \"selectFiles\": \"Vyberte soubory k exportování\",\n      \"selectAll\": \"Označit vše\",\n      \"deselectAll\": \"Odznačit vše\",\n      \"createNewFolder\": \"V\",\n      \"createNewFolderDesc\": \"Řekněte nám, kam uložit Vaše data\",\n      \"defineWhereYourDataIsStored\": \"Vyberte kde jsou ukládána Vaše data\",\n      \"open\": \"Otevřít\",\n      \"openFolder\": \"Otevřít existující složku\",\n      \"openFolderDesc\": \"Číst a zapisovat do existující @:appName složky\",\n      \"folderHintText\": \"název složky\",\n      \"location\": \"Vytváření nové složky\",\n      \"locationDesc\": \"Vyberte název pro složku, kam bude @:appName ukládat Vaše data\",\n      \"browser\": \"Procházet\",\n      \"create\": \"Vytvořit\",\n      \"set\": \"Nastavit\",\n      \"folderPath\": \"Umístění složkyU\",\n      \"locationCannotBeEmpty\": \"Umístění nesmí být prázdné\",\n      \"pathCopiedSnackbar\": \"Umístění souboru zkopírováno do schránky\",\n      \"changeLocationTooltips\": \"Změnit složku pro ukládání dat\",\n      \"change\": \"Změnit\",\n      \"openLocationTooltips\": \"Otevřít jinou složku pro ukládání dat\",\n      \"openCurrentDataFolder\": \"Otevřít současnou složku pro ukládání dat\",\n      \"recoverLocationTooltips\": \"Obnovit výchozí složku s daty \",\n      \"exportFileSuccess\": \"Soubor byl úspěšně exportován!\",\n      \"exportFileFail\": \"Soubor se nepodařilo exportovat!\",\n      \"export\": \"Exportovat\"\n    },\n    \"user\": {\n      \"name\": \"Jméno\",\n      \"email\": \"E-mail\",\n      \"tooltipSelectIcon\": \"Vyberte ikonu\",\n      \"selectAnIcon\": \"Vyberte ikonu\",\n      \"pleaseInputYourOpenAIKey\": \"Prosím vložte svůj AI klíč\",\n      \"clickToLogout\": \"Klin\",\n      \"pleaseInputYourStabilityAIKey\": \"Prosím vložte svůj Stability AI klíč\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"Osobní informace\",\n      \"username\": \"Uživatelské jméno\",\n      \"usernameEmptyError\": \"Uživatelské jméno nesmí být prázdné\",\n      \"about\": \"O aplikaci\",\n      \"pushNotifications\": \"Push upozornění\",\n      \"support\": \"Podpora\",\n      \"joinDiscord\": \"Přidejte se k nám na Discordu\",\n      \"privacyPolicy\": \"Podmínky použití osobních údajů\",\n      \"userAgreement\": \"Uživatels\",\n      \"userprofileError\": \"Nepodařilo se načíst uživatelský profil\",\n      \"userprofileErrorDescription\": \"Prosím zkuste se odhlásit a znovu přihlásit a zkontrolujte, zda problém přetrvává\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"Klávesové zkratky\",\n      \"command\": \"Příkaz\",\n      \"keyBinding\": \"Přiřazená klávesa\",\n      \"addNewCommand\": \"Přidat nový příkaz\",\n      \"updateShortcutStep\": \"Stiskněte požadovanou kombinaci kláves a stiskněte ENTER\",\n      \"shortcutIsAlreadyUsed\": \"Tato zkratka je již použita pro: @@\",\n      \"resetToDefault\": \"Obnovit výchozí klávesové zkratky\",\n      \"couldNotLoadErrorMsg\": \"Nepodařilo se načíst klávesové zkratky, zkuste to znovu\",\n      \"couldNotSaveErrorMsg\": \"Nepodařilo seuložit klávesové zkta\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Opravdu chcete tento pohled odstranit?\",\n    \"createView\": \"Nový\",\n    \"title\": {\n      \"placeholder\": \"Bez nýzvu\"\n    },\n    \"settings\": {\n      \"filter\": \"Filtr\",\n      \"sort\": \"Řadit\",\n      \"sortBy\": \"Řadit podle\",\n      \"properties\": \"Vlastnosti\",\n      \"reorderPropertiesTooltip\": \"Přetažením uspořádáte vlastnosti\",\n      \"group\": \"Skupiny\",\n      \"addFilter\": \"Přidat filtr\",\n      \"deleteFilter\": \"Smazat filtr\",\n      \"filterBy\": \"Filtrovat podle...\",\n      \"typeAValue\": \"Napište hodnotu...\",\n      \"layout\": \"Rozložení\",\n      \"databaseLayout\": \"Rozložení\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Obsahuje\",\n      \"doesNotContain\": \"Neobsahuje\",\n      \"endsWith\": \"Končí\",\n      \"startWith\": \"Začíná\",\n      \"is\": \"Je\",\n      \"isNot\": \"Není\",\n      \"isEmpty\": \"Je pr\",\n      \"isNotEmpty\": \"Není prázdné\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Kromě\",\n        \"startWith\": \"Začíná\",\n        \"endWith\": \"Končí\",\n        \"isEmpty\": \"je prázdné\",\n        \"isNotEmpty\": \"není prázdné\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Zaškrtnuto\",\n      \"isUnchecked\": \"Nezaškrtnuto\",\n      \"choicechipPrefix\": {\n        \"is\": \"je\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"je hotový\",\n      \"isIncomplted\": \"není hotový\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"Je\",\n      \"isNot\": \"Není\",\n      \"contains\": \"Obsahuje\",\n      \"doesNotContain\": \"Neobsahuje\",\n      \"isEmpty\": \"Je prázdné\",\n      \"isNotEmpty\": \"Není prázdný\"\n    },\n    \"field\": {\n      \"hide\": \"Schovat\",\n      \"show\": \"Ukázat\",\n      \"insertLeft\": \"Vložit vlevo\",\n      \"insertRight\": \"Vložit vpravo\",\n      \"duplicate\": \"Duplikovat\",\n      \"delete\": \"Smazat\",\n      \"textFieldName\": \"Text\",\n      \"checkboxFieldName\": \"Zaškrtávací políčko\",\n      \"dateFieldName\": \"Datum\",\n      \"updatedAtFieldName\": \"Datum poslední úpravy\",\n      \"createdAtFieldName\": \"Datum vytvoření\",\n      \"numberFieldName\": \"Čísla\",\n      \"singleSelectFieldName\": \"Výběr\",\n      \"multiSelectFieldName\": \"Multivýběr\",\n      \"urlFieldName\": \"URL adresa\",\n      \"checklistFieldName\": \"Zaškrtávací seznam\",\n      \"numberFormat\": \"Formát čásel\",\n      \"dateFormat\": \"Formát data\",\n      \"includeTime\": \"Včetně času\",\n      \"isRange\": \"Datum kon\",\n      \"dateFormatFriendly\": \"Měsíc Den, Rok\",\n      \"dateFormatISO\": \"Rok-Měsíc-Den\",\n      \"dateFormatLocal\": \"Měsíc/Den/Rok\",\n      \"dateFormatUS\": \"Rok/Měsíc/Day\",\n      \"dateFormatDayMonthYear\": \"Den/Měsíc/Rok\",\n      \"timeFormat\": \"Formát času\",\n      \"invalidTimeFormat\": \"Neplatný formát\",\n      \"timeFormatTwelveHour\": \"12hodinový\",\n      \"timeFormatTwentyFourHour\": \"24hodinový\",\n      \"clearDate\": \"Vyčistit datum\",\n      \"dateTime\": \"Vyčistit čas\",\n      \"startDateTime\": \"Datum a čas začátku\",\n      \"endDateTime\": \"Datum a čas konce\",\n      \"failedToLoadDate\": \"Nepodařilo načíst datum\",\n      \"selectTime\": \"Vyberte čas\",\n      \"selectDate\": \"Vyberte datum\",\n      \"visibility\": \"Viditelnost\",\n      \"propertyType\": \"Typ vlastnosti\",\n      \"addSelectOption\": \"Přidat možnost\",\n      \"optionTitle\": \"Možnosti\",\n      \"addOption\": \"Přidat možnost\",\n      \"editProperty\": \"Upravit vlastnost\",\n      \"newProperty\": \"Nová vlastnost\",\n      \"deleteFieldPromptMessage\": \"Jste si jistí? Tato vlastnost bude smazána\",\n      \"newColumn\": \"Nový sloupeček\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Přidat nové pole\",\n      \"fieldDragElementTooltip\": \"Kli\",\n      \"showHiddenFields\": {\n        \"one\": \"Zobrazit {} skryté pole\",\n        \"many\": \"Zobrazit {} skrytá pole\",\n        \"other\": \"Zobrazit {} skrytá pole\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"Skrýt {} skryté pole\",\n        \"many\": \"Skrýt {} skrytá pole\",\n        \"other\": \"Skrýt {} skrytá pole\"\n      }\n    },\n    \"sort\": {\n      \"ascending\": \"Vzestupně\",\n      \"descending\": \"Sestupně\",\n      \"deleteAllSorts\": \"Smazat všechna řazení\",\n      \"addSort\": \"Přidat řazení\"\n    },\n    \"row\": {\n      \"duplicate\": \"Duplikovat\",\n      \"delete\": \"Smazat\",\n      \"titlePlaceholder\": \"Bez názvu\",\n      \"textPlaceholder\": \"Prázdné\",\n      \"copyProperty\": \"Vlastnost zkopírována do schránky\",\n      \"count\": \"Počet\",\n      \"newRow\": \"Nový řádek\",\n      \"action\": \"Akce\",\n      \"add\": \"Kliknutím přidáte pod\",\n      \"drag\": \"Přetáhnout podržením\",\n      \"dragAndClick\": \"Přetáhnout podržením, kliknutí otevírá menu\",\n      \"insertRecordAbove\": \"Vložte záznam nad\",\n      \"insertRecordBelow\": \"Vložte záznam pod\"\n    },\n    \"selectOption\": {\n      \"create\": \"Vytvořit\",\n      \"purpleColor\": \"Fialová\",\n      \"pinkColor\": \"Růžová\",\n      \"lightPinkColor\": \"Světle růžová\",\n      \"orangeColor\": \"Oranžová\",\n      \"yellowColor\": \"Žlutá\",\n      \"limeColor\": \"Limetková\",\n      \"greenColor\": \"Zelená\",\n      \"aquaColor\": \"Akvamarínová\",\n      \"blueColor\": \"Modrá\",\n      \"deleteTag\": \"Smazat štítek\",\n      \"colorPanelTitle\": \"Barvy\",\n      \"panelTitle\": \"Vyberte nebo vytvořte možnost\",\n      \"searchOption\": \"Hledat možnost\",\n      \"searchOrCreateOption\": \"Hledat nebo vytvořit možnost...\",\n      \"createNew\": \"Vytvořit novou\",\n      \"orSelectOne\": \"Nebo vybrat možnost\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"Popis úkolu\",\n      \"addNew\": \"Přidat úkol\",\n      \"submitNewTask\": \"Vytvořit\",\n      \"hideComplete\": \"Skrýt dokončené úkoly\",\n      \"showComplete\": \"Zobrazit všechny úkoly\"\n    },\n    \"menuName\": \"Mřížka\",\n    \"referencedGridPrefix\": \"Pohled na\"\n  },\n  \"document\": {\n    \"menuName\": \"Dokument\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 Odpoledne\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Propojit s nástěnkou\",\n        \"createANewBoard\": \"Vytvořit novou nástěnku\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Propojit s mřížkou\",\n        \"createANewGrid\": \"Vytvořit mřížku\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Propojit s kalendářem\",\n        \"createANewCalendar\": \"Vyto\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"Propojit s dokumentem\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Obrys\",\n      \"codeBlock\": \"Blok kódu\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Odkazovaná nástěnka\",\n      \"referencedGrid\": \"Odkazovaná mřížka\",\n      \"referencedCalendar\": \"Odkazovaný kalendář\",\n      \"referencedDocument\": \"Odkazovaný dokument\",\n      \"autoGeneratorMenuItemName\": \"AI Writer\",\n      \"autoGeneratorTitleName\": \"AI: Zeptej se AI na cokoliv...\",\n      \"autoGeneratorLearnMore\": \"Zjistit více\",\n      \"autoGeneratorGenerate\": \"Vygenerovat\",\n      \"autoGeneratorHintText\": \"Zeptat se AI...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Nepodařilo se získat klíč AI\",\n      \"autoGeneratorRewrite\": \"Přepsat\",\n      \"smartEdit\": \"AI asistenti\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"Opravit pravopis\",\n      \"warning\": \"⚠️ odpovědi AI mohou být nepřesné nebo zavádějící.\",\n      \"smartEditSummarize\": \"Shrnout\",\n      \"smartEditImproveWriting\": \"Vylepšit styl psaní\",\n      \"smartEditMakeLonger\": \"Prodloužit\",\n      \"smartEditCouldNotFetchResult\": \"Nepodařilo se stáhnout výsledek z AI\",\n      \"smartEditCouldNotFetchKey\": \"Nepodařilo se stáhnout klíč AI\",\n      \"smartEditDisabled\": \"Propojit s AI v Nastavení\",\n      \"discardResponse\": \"Opravdu chcete zahodit odpovědi od AI?\",\n      \"createInlineMathEquation\": \"Vytvořit rovnici\",\n      \"fonts\": \"Písma\",\n      \"toggleList\": \"Rozbalovací seznam\",\n      \"quoteList\": \"Seznam citátů\",\n      \"numberedList\": \"Číslovaný seznam\",\n      \"bulletedList\": \"Odrážkový seznam\",\n      \"todoList\": \"Úkolníček\",\n      \"callout\": \"Vyhlásit\",\n      \"cover\": {\n        \"changeCover\": \"Změnit přebal\",\n        \"colors\": \"Barvy\",\n        \"images\": \"Obrázky\",\n        \"clearAll\": \"Vyčistit vše\",\n        \"abstract\": \"Abstraktní\",\n        \"addCover\": \"Přidat přebal\",\n        \"addLocalImage\": \"Přidat obrázek z lokálního úložiště\",\n        \"invalidImageUrl\": \"URL adresa obrázku je neplatná\",\n        \"failedToAddImageToGallery\": \"Nepodařilo se přidat obrázek do galerie\",\n        \"enterImageUrl\": \"Zadejte URL adresu obrázku\",\n        \"add\": \"Přidat\",\n        \"back\": \"Zpět\",\n        \"saveToGallery\": \"Uložit do galerie\",\n        \"removeIcon\": \"Smazat ikonu\",\n        \"pasteImageUrl\": \"Vložit URL adresu obrázku\",\n        \"or\": \"NEBO\",\n        \"pickFromFiles\": \"Vyberte ze souborů\",\n        \"couldNotFetchImage\": \"Nepodařilo se načíst obrázky\",\n        \"imageSavingFailed\": \"Ukládání obrázku se nezdařilo\",\n        \"addIcon\": \"Přidat ikonu\",\n        \"changeIcon\": \"Změnit ikonu\",\n        \"coverRemoveAlert\": \"Po smazání bude odstraněno také z přebalu.\",\n        \"alertDialogConfirmation\": \"Jste si jistí, že chcete pokračovat?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"Matematick\",\n        \"addMathEquation\": \"Přidat TeX \",\n        \"editMathEquation\": \"Upravit matematickou rovnici\"\n      },\n      \"optionAction\": {\n        \"click\": \"Kliknutím\",\n        \"toOpenMenu\": \" otevřete menu\",\n        \"delete\": \"Smazat\",\n        \"duplicate\": \"Duplikovat\",\n        \"turnInto\": \"Změnit na\",\n        \"moveUp\": \"Posunout nahoru\",\n        \"moveDown\": \"Posunout dolů\",\n        \"color\": \"Barva\",\n        \"align\": \"Zarovnání\",\n        \"left\": \"Vlevo\",\n        \"center\": \"Doprostřed\",\n        \"right\": \"Vpravo\",\n        \"defaultColor\": \"Výchozí\"\n      },\n      \"image\": {\n        \"addAnImage\": \"Přidat obrázek\",\n        \"copiedToPasteBoard\": \"Odkaz na obrázek byl zkopírován do schránky\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Přidáním nadpisů vytvoříte obsah dokumentu\"\n      },\n      \"table\": {\n        \"addAfter\": \"Přidat za\",\n        \"addBefore\": \"Přidat před\",\n        \"delete\": \"Smazat\",\n        \"clear\": \"Vyčistit obsah\",\n        \"duplicate\": \"Duplikovat\",\n        \"bgColor\": \"Barva pozadí\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Kopírovat\",\n        \"cut\": \"Vyjmout\",\n        \"paste\": \"Vložit\"\n      },\n      \"action\": \"Příkazy\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Napište \\\"/\\\" pro zadání příkazu\"\n    },\n    \"title\": {\n      \"placeholder\": \"Bez názvu\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Kliknutím přidáte obrázek\",\n      \"upload\": {\n        \"label\": \"Nahrát\",\n        \"placeholder\": \"Kliknutím nahrajete obrázek\"\n      },\n      \"url\": {\n        \"label\": \"URL adresa obrázku\",\n        \"placeholder\": \"Vlože URL adresu obrázku\"\n      },\n      \"ai\": {\n        \"label\": \"Vygenerujte obrázek pomocí AI\",\n        \"placeholder\": \"Prosím vlo\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Generovat obrázek ze Stability AI\",\n        \"placeholder\": \"Zadejte prosím prompt pro generování obrázku pomocí Stability AI\"\n      },\n      \"support\": \"Maximální velikost obrázku je 5MB. Podporované formáty: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Neplatný obrázek\",\n        \"invalidImageSize\": \"Velikost obrázku musí být menší než 5MB\",\n        \"invalidImageFormat\": \"Formát obrázku není podporovaný. Podporované formáty: JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"Neplatná URL adresa obrázku\"\n      },\n      \"embedLink\": {\n        \"label\": \"Vložit odkaz (embed)\",\n        \"placeholder\": \"Vložte nebo napište odkaz na obrázek\"\n      },\n      \"searchForAnImage\": \"Hledat obrázek\",\n      \"pleaseInputYourOpenAIKey\": \"zadejte prosím svůj AI klíč v Nastavení\",\n      \"saveImageToGallery\": \"Uložit obrázek\",\n      \"failedToAddImageToGallery\": \"Nepodařilo se přidat obrázek do galerie\",\n      \"successToAddImageToGallery\": \"Obrázek byl úspěšně přidán do galerie\",\n      \"unableToLoadImage\": \"Nepodařilo se nahrát obrázek\",\n      \"pleaseInputYourStabilityAIKey\": \"prosím vložte svůjStability AI klíč v Nastavení\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Jazyk\",\n        \"placeholder\": \"Vyberte jazyk\"\n      }\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Vložte nebo napište odkaz\",\n      \"openInNewTab\": \"Otevřít v novém panelu\",\n      \"copyLink\": \"Kopírovat odkaz\",\n      \"removeLink\": \"Odstranit odkaz\",\n      \"url\": {\n        \"label\": \"URL adresa odkazu\",\n        \"placeholder\": \"Zadejte URL adresu odkazu\"\n      },\n      \"title\": {\n        \"label\": \"Název odkazu\",\n        \"placeholder\": \"Zadejte název odkazu\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"Označit člověka, stránku nebo datum...\",\n      \"page\": {\n        \"label\": \"Odkaz na stránku\",\n        \"tooltip\": \"Kliknutím otevřete stránku\"\n      }\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"Obnovit výchozí\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"Aktuální verze tento blok nepodporuje.\",\n      \"blockContentHasBeenCopied\": \"Obsah bloku byl zkopírován.\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"Nová\",\n      \"renameGroupTooltip\": \"Zmáčknutím přejmenujete skupinu\",\n      \"createNewColumn\": \"Přidat novou skupinu\",\n      \"addToColumnTopTooltip\": \"Přidá novou kartičku nahoru\",\n      \"renameColumn\": \"Přejmenovat\",\n      \"hideColumn\": \"Skrýt\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"Skryté skupiny\",\n      \"collapseTooltip\": \"Skrýt skryté skupiny\",\n      \"expandTooltip\": \"Zobrazit skryté skupiny\"\n    },\n    \"cardDetail\": \"Detail kartičky\",\n    \"cardActions\": \"Kartička - příkazy\",\n    \"cardDuplicated\": \"Kartička byla duplikována\",\n    \"cardDeleted\": \"Kartička smazána\",\n    \"menuName\": \"Nástěnka\",\n    \"showUngrouped\": \"Zobrazit položky bez skupiny\",\n    \"ungroupedButtonText\": \"Bez skupiny\",\n    \"ungroupedButtonTooltip\": \"Obsahuje karty, které \",\n    \"ungroupedItemsTitle\": \"Kliknutím přidáte na nástěnku\",\n    \"groupBy\": \"Seskupit podle\",\n    \"referencedBoardPrefix\": \"Pohled\",\n    \"mobile\": {\n      \"editURL\": \"Upravit URL adresu\",\n      \"showGroup\": \"Zobrazit skupinu\",\n      \"showGroupContent\": \"Opravdu chcete tuto skupinu zobrazit na nástěnce?\",\n      \"failedToLoad\": \"Zobrazení desky se nepodařilo načíst\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Kalendář\",\n    \"defaultNewCalendarTitle\": \"Nepojmenovaný\",\n    \"newEventButtonTooltip\": \"Přidat novou událost\",\n    \"navigation\": {\n      \"today\": \"Dnes\",\n      \"jumpToday\": \"Přejít na dnešek\",\n      \"previousMonth\": \"Předchozí měsíc\",\n      \"nextMonth\": \"Následující měsíc\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Zobrazit číslo týdne\",\n      \"showWeekends\": \"Zobrazit víkendy\",\n      \"firstDayOfWeek\": \"Počátek týdne\",\n      \"layoutDateField\": \"Rozložit kalendář podle\",\n      \"noDateTitle\": \"Žádné datum\",\n      \"noDateHint\": {\n        \"zero\": \"Zde uvidíte nenaplánované události\",\n        \"one\": \"{} nenaplánovaná událost\",\n        \"other\": \"{} nenaplánovaných událostí\"\n      },\n      \"clickToAdd\": \"Přidat do kalendáře\",\n      \"name\": \"Rozložení kalendáře\"\n    },\n    \"referencedCalendarPrefix\": \"Pohled na\"\n  },\n  \"errorDialog\": {\n    \"title\": \"Chyba @:appName\",\n    \"howToFixFallback\": \"Omlouváme se za nepříjemnost! Pošlete hlášení na náš GitHub, kde popíšete chybu na kterou jste narazili.\",\n    \"github\": \"Zobrazit na GitHubu\"\n  },\n  \"search\": {\n    \"label\": \"Hledat\",\n    \"placeholder\": {\n      \"actions\": \"Hledat - příkazy...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Zkopírováno!\",\n      \"fail\": \"Nepodařilo se zkopírovat\"\n    }\n  },\n  \"unSupportBlock\": \"Aktuální verze tento blok nepodporuje.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Opravdu chcete smazat {pageType}?\",\n    \"deleteContentCaption\": \"pokud \"\n  },\n  \"colors\": {\n    \"custom\": \"Vlastní\",\n    \"default\": \"Výchozí\",\n    \"red\": \"Červená\",\n    \"orange\": \"Oranžová\",\n    \"yellow\": \"Žlutá\",\n    \"green\": \"Zelená\",\n    \"blue\": \"Modrá\",\n    \"purple\": \"Fialová\",\n    \"pink\": \"Růžová\",\n    \"brown\": \"Hnědá\",\n    \"gray\": \"Šedá\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"Emoji\",\n    \"search\": \"Hledat emoji\",\n    \"noRecent\": \"Žádné nedávné emoji\",\n    \"noEmojiFound\": \"Nenalezeno žádné emoji\",\n    \"filter\": \"Filtr\",\n    \"random\": \"Náhodný\",\n    \"selectSkinTone\": \"Vybrat tón pleti\",\n    \"remove\": \"Smazat emoji\",\n    \"categories\": {\n      \"smileys\": \"Smajlíci a emoce\",\n      \"people\": \"Lidé a tělo\",\n      \"animals\": \"Zvířata a příroda\",\n      \"food\": \"Jídlo a pití\",\n      \"activities\": \"Aktivity\",\n      \"places\": \"Cestování a místa\",\n      \"objects\": \"Věci\",\n      \"symbols\": \"Symboly\",\n      \"flags\": \"Vlajky\",\n      \"nature\": \"Příroda\",\n      \"frequentlyUsed\": \"Často používané\"\n    },\n    \"skinTone\": {\n      \"default\": \"Výchozí\",\n      \"light\": \"Světlý\",\n      \"mediumLight\": \"Středně světlý\",\n      \"medium\": \"Střední\",\n      \"mediumDark\": \"Středně tmavý\",\n      \"dark\": \"Tmavý\"\n    }\n  },\n  \"inlineActions\": {\n    \"noResults\": \"Žádné výsledky\",\n    \"pageReference\": \"Odkazovaná stránka\",\n    \"date\": \"Datum\",\n    \"reminder\": {\n      \"groupTitle\": \"Připomenutí\",\n      \"shortKeyword\": \"připomenout\"\n    }\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"Formát data a času změníte v Nastavení\"\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"Včera\",\n    \"today\": \"Dnes\",\n    \"tomorrow\": \"Zítra\",\n    \"oneWeek\": \"1 týden\"\n  },\n  \"notificationHub\": {\n    \"title\": \"Upozornění\",\n    \"emptyTitle\": \"Nic Vám neuteklo!\",\n    \"emptyBody\": \"Žádné nevyřízené akce nebo upozornění. Užijte si klid.\",\n    \"tabs\": {\n      \"inbox\": \"Schránka\",\n      \"upcoming\": \"Nadcházející\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"Označit vše jako přečtené\",\n      \"showAll\": \"Vše\",\n      \"showUnreads\": \"Nepřečtené\"\n    },\n    \"filters\": {\n      \"ascending\": \"Vzestupně\",\n      \"descending\": \"Sestupně\",\n      \"groupByDate\": \"Seskupit podle data\",\n      \"showUnreadsOnly\": \"Zobrazit pouze nepřečtené\",\n      \"resetToDefault\": \"Obnovit výchozí\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"Upomínka\",\n    \"message\": \"Nezapomeňte to zkontrolovat než zapomenete!\",\n    \"tooltipDelete\": \"Smazat\",\n    \"tooltipMarkRead\": \"Označit jako přečtené\",\n    \"tooltipMarkUnread\": \"Označit jako nepřečtené\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"Najít\",\n    \"previousMatch\": \"Předchozí shoda\",\n    \"nextMatch\": \"Další shoda\",\n    \"close\": \"Zavřít\",\n    \"replace\": \"Nahradit\",\n    \"replaceAll\": \"Nahradit vše\",\n    \"noResult\": \"Žádné výsledky\",\n    \"caseSensitive\": \"Citlivý na malá/velká písmena\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"Omluváme se\",\n    \"loadingViewError\": \"Nedaří se nám načíst tento pohled. Zkontrolujte prosím Vaše internetové připojení, obnovte aplikaci a neváhejte nás kontaktovat pokud problém přetrvává.\"\n  },\n  \"editor\": {\n    \"bold\": \"Tučné\",\n    \"bulletedList\": \"Odrážkový seznam\",\n    \"checkbox\": \"Zaškrtávací políčko\",\n    \"embedCode\": \"Vložit kód (embed)\",\n    \"heading1\": \"Nadpis 1\",\n    \"heading2\": \"Nadpis 2\",\n    \"heading3\": \"Nadpis 3\",\n    \"highlight\": \"Zvýrazenění\",\n    \"color\": \"Barva\",\n    \"image\": \"Obrázek\",\n    \"italic\": \"Kurzíva\",\n    \"link\": \"Odkaz\",\n    \"numberedList\": \"Číslovaný seznam\",\n    \"quote\": \"Citace\",\n    \"strikethrough\": \"Přeškrtnutí\",\n    \"text\": \"Text\",\n    \"underline\": \"Podtržení\",\n    \"fontColorDefault\": \"Výchozí\",\n    \"fontColorGray\": \"Šedá\",\n    \"fontColorBrown\": \"Hnědá\",\n    \"fontColorOrange\": \"Oranžová\",\n    \"fontColorYellow\": \"Žlutá\",\n    \"fontColorGreen\": \"Zelená\",\n    \"fontColorBlue\": \"Modrá\",\n    \"fontColorPurple\": \"Fialová\",\n    \"fontColorPink\": \"Růžová\",\n    \"fontColorRed\": \"Červená\",\n    \"backgroundColorDefault\": \"Výchozí pozadí\",\n    \"backgroundColorGray\": \"Šedé pozadí\",\n    \"backgroundColorBrown\": \"Hnědé pozadí\",\n    \"backgroundColorOrange\": \"Oranžové pozadí\",\n    \"backgroundColorYellow\": \"Žluté pozadí\",\n    \"backgroundColorGreen\": \"Zelené pozadí\",\n    \"backgroundColorBlue\": \"Modré pozadí\",\n    \"backgroundColorPurple\": \"Fialové pozadí\",\n    \"backgroundColorPink\": \"Růžové pozadí\",\n    \"backgroundColorRed\": \"Červené pozadí\",\n    \"done\": \"Hotovo\",\n    \"cancel\": \"Zrušit\",\n    \"tint1\": \"Odstín 1\",\n    \"tint2\": \"Odstín 2\",\n    \"tint3\": \"Odstín 3\",\n    \"tint4\": \"Odstín 4\",\n    \"tint5\": \"Odstín 5\",\n    \"tint6\": \"Odstín 6\",\n    \"tint7\": \"Odstín 7\",\n    \"tint8\": \"Odstín 8\",\n    \"tint9\": \"Odstín 9\",\n    \"lightLightTint1\": \"Fialová\",\n    \"lightLightTint2\": \"Růžová\",\n    \"lightLightTint3\": \"Lehce růžová\",\n    \"lightLightTint4\": \"Oranžová\",\n    \"lightLightTint5\": \"Žlutá\",\n    \"lightLightTint6\": \"Limetková\",\n    \"lightLightTint7\": \"Zelená\",\n    \"lightLightTint8\": \"Akvamarínová\",\n    \"lightLightTint9\": \"Modrá\",\n    \"urlHint\": \"URL adresa\",\n    \"mobileHeading1\": \"Nadpis 1\",\n    \"mobileHeading2\": \"Nadpis 2\",\n    \"mobileHeading3\": \"Nadpis 3\",\n    \"textColor\": \"Barva textu\",\n    \"backgroundColor\": \"Barva pozadí\",\n    \"addYourLink\": \"Přidejte odkaz\",\n    \"openLink\": \"Otevřít odkaz\",\n    \"copyLink\": \"Kopírovat odkaz\",\n    \"removeLink\": \"Smazat odkaz\",\n    \"editLink\": \"Upravit odkaz\",\n    \"linkText\": \"Text\",\n    \"linkTextHint\": \"Zadejte prosím text\",\n    \"linkAddressHint\": \"Zadejte prosím URL adresu\",\n    \"highlightColor\": \"Barva zvýraznění\",\n    \"clearHighlightColor\": \"Obnovit barvu zvýraznění\",\n    \"customColor\": \"Vlastní barva\",\n    \"hexValue\": \"Hex hodnota\",\n    \"opacity\": \"Průhlednost\",\n    \"resetToDefaultColor\": \"Obnovit výchozí barvu\",\n    \"ltr\": \"Zleva doprava\",\n    \"rtl\": \"Zprava doleva\",\n    \"auto\": \"Automaticky\",\n    \"cut\": \"Vyjmout\",\n    \"copy\": \"Kopírovat\",\n    \"paste\": \"Vložit\",\n    \"find\": \"Najít\",\n    \"previousMatch\": \"Předchozí shoda\",\n    \"nextMatch\": \"Další shoda\",\n    \"closeFind\": \"Zavřít\",\n    \"replace\": \"Nahradit\",\n    \"replaceAll\": \"Nahradit vše\",\n    \"regex\": \"Regulární výraz\",\n    \"caseSensitive\": \"Citlivý na malá/velká písmena\",\n    \"uploadImage\": \"Nahrát obrázek\",\n    \"urlImage\": \"URL adresa obrázku\",\n    \"incorrectLink\": \"Nesprávný odkaz\",\n    \"upload\": \"Nahrát\",\n    \"chooseImage\": \"Vyberte obrázek\",\n    \"loading\": \"Načítání\",\n    \"imageLoadFailed\": \"Nepodařilo se načíst obrázek\",\n    \"divider\": \"Oddělovač\",\n    \"table\": \"Tabulka\",\n    \"colAddBefore\": \"Přidat před\",\n    \"rowAddBefore\": \"Přidat za\",\n    \"colAddAfter\": \"Přidat po\",\n    \"rowAddAfter\": \"Přidat po\",\n    \"colRemove\": \"Odstranit\",\n    \"rowRemove\": \"Odstranit\",\n    \"colDuplicate\": \"Duplikovat\",\n    \"rowDuplicate\": \"Duplikovat\",\n    \"colClear\": \"Vyčistit obsah\",\n    \"rowClear\": \"Vyčistit obsah\",\n    \"slashPlaceHolder\": \"Zadejte \\\"/\\\" pro vložení bloku, nebo začněte psát\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"Žádné oblíbené stránky\",\n    \"noFavoriteHintText\": \"Swipnutím doleva přidáte stránku do oblíbených\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"Napište / k \"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"Úkolníček\",\n    \"bulletList\": \"Seznam\",\n    \"numberList\": \"Seznam\",\n    \"quote\": \"Citace\",\n    \"heading\": \"Nadpis {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"Ikona stránky\",\n    \"language\": \"Jazyk\",\n    \"font\": \"Písmo\",\n    \"actions\": \"Příkazy\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/de-DE.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Ich\",\n  \"welcomeText\": \"Willkommen bei @:appName\",\n  \"welcomeTo\": \"Willkommen zu\",\n  \"githubStarText\": \"Mit einem Stern auf GitHub markieren\",\n  \"subscribeNewsletterText\": \"Abonniere den Newsletter\",\n  \"letsGoButtonText\": \"Los geht's\",\n  \"title\": \"Titel\",\n  \"youCanAlso\": \"Du kannst auch\",\n  \"and\": \"und\",\n  \"failedToOpenUrl\": \"URL konnte nicht geöffnet werden: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Unten klicken um etwas hinzuzufügen\",\n    \"addAboveCmd\": \"Alt+Klick\",\n    \"addAboveMacCmd\": \"Option+Klick\",\n    \"addAboveTooltip\": \"oben hinzufügen\",\n    \"dragTooltip\": \"Drag to Drop\",\n    \"openMenuTooltip\": \"Klicken, um das Menü zu öffnen\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Registrieren\",\n    \"title\": \"Registriere dich bei @:appName\",\n    \"getStartedText\": \"Erste Schritte\",\n    \"emptyPasswordError\": \"Passwort darf nicht leer sein\",\n    \"repeatPasswordEmptyError\": \"Passwortwiederholung darf nicht leer sein\",\n    \"unmatchedPasswordError\": \"Passwörter stimmen nicht überein\",\n    \"alreadyHaveAnAccount\": \"Hast du schon ein Account?\",\n    \"emailHint\": \"E-Mail\",\n    \"passwordHint\": \"Passwort\",\n    \"repeatPasswordHint\": \"Passwort wiederholen\",\n    \"signUpWith\": \"Anmelden mit:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Bei @:appName einloggen\",\n    \"loginButtonText\": \"Anmelden\",\n    \"loginStartWithAnonymous\": \"Anonyme Sitzung starten\",\n    \"continueAnonymousUser\": \"in anonymer Sitzung fortfahren\",\n    \"buttonText\": \"Anmelden\",\n    \"signingInText\": \"Anmelden...\",\n    \"forgotPassword\": \"Passwort vergessen?\",\n    \"emailHint\": \"E-Mail\",\n    \"passwordHint\": \"Passwort\",\n    \"dontHaveAnAccount\": \"Noch kein Konto?\",\n    \"createAccount\": \"Benutzerkonto erstellen\",\n    \"repeatPasswordEmptyError\": \"Passwortwiederholung darf nicht leer sein\",\n    \"unmatchedPasswordError\": \"Passwörter stimmen nicht überein\",\n    \"syncPromptMessage\": \"Synchronisation kann ein paar Minuten dauern. Diese Seite bitte nicht schließen\",\n    \"or\": \"ODER\",\n    \"signInWithGoogle\": \"Mit Google anmelden\",\n    \"signInWithGithub\": \"Mit Github anmelden\",\n    \"signInWithDiscord\": \"Mit Discord anmelden\",\n    \"signInWithApple\": \"Weiter mit Apple\",\n    \"continueAnotherWay\": \"Anders fortfahren\",\n    \"signUpWithGoogle\": \"Mit Google registrieren\",\n    \"signUpWithGithub\": \"Mit Github registrieren\",\n    \"signUpWithDiscord\": \"Mit Discord registrieren\",\n    \"signInWith\": \"Anmeldeoptionen:\",\n    \"signInWithEmail\": \"Mit E-Mail anmelden\",\n    \"signInWithMagicLink\": \"Mit Authentifizierungslink anmelden\",\n    \"signUpWithMagicLink\": \"Mit Authentifizierungslink registrieren\",\n    \"pleaseInputYourEmail\": \"Gib bitte deine E-Mail-Adresse ein\",\n    \"settings\": \"Einstellungen\",\n    \"magicLinkSent\": \"Wir haben dir einen Authentifizierungslink per E-Mail geschickt. Klicke auf den Link, um dich anzumelden.\",\n    \"invalidEmail\": \"Bitte gib eine gültige E-Mail-Adresse ein\",\n    \"alreadyHaveAnAccount\": \"Du hast bereits ein Konto?\",\n    \"logIn\": \"Anmeldung\",\n    \"generalError\": \"Etwas ist schiefgelaufen. Bitte versuche es später noch einmal\",\n    \"limitRateError\": \"Aus Sicherheitsgründen kannst du nur alle 60 Sekunden einen Authentifizierungslink anfordern\",\n    \"magicLinkSentDescription\": \"Ein Magic Link wurde an deine E-Mail-Adresse gesendet. Klicke auf den Link, um deine Anmeldung abzuschließen. Der Link läuft nach 5 Minuten ab.\",\n    \"anonymous\": \"Anonym\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Arbeitsbereich wählen\",\n    \"defaultName\": \"Mein Arbeitsbereich\",\n    \"create\": \"Arbeitsbereich erstellen\",\n    \"importFromNotion\": \"Von Notion importieren\",\n    \"learnMore\": \"Mehr erfahren\",\n    \"reset\": \"Arbeitsbereich zurücksetzen\",\n    \"renameWorkspace\": \"Arbeitsbereich umbenennen\",\n    \"workspaceNameCannotBeEmpty\": \"Arbeitsbereichname darf nicht leer sein\",\n    \"resetWorkspacePrompt\": \"Das Zurücksetzen des Arbeitsbereiches löscht alle enthaltenen Seiten und Daten. Bist du sicher, dass du den Arbeitsbereich zurücksetzen möchtest? \",\n    \"hint\": \"Arbeitsbereich\",\n    \"notFoundError\": \"Arbeitsbereich nicht gefunden\",\n    \"failedToLoad\": \"Etwas ist schief gelaufen! Der Arbeitsbereich konnte nicht geladen werden. Versuche, alle @:appName Instanzen zu schließen und versuche es erneut.\",\n    \"errorActions\": {\n      \"reportIssue\": \"Problem melden\",\n      \"reportIssueOnGithub\": \"Melde ein Problem auf GitHub\",\n      \"exportLogFiles\": \"Exportiere Log-Dateien\",\n      \"reachOut\": \"Kontaktiere uns auf Discord\"\n    },\n    \"menuTitle\": \"Arbeitsbereiche\",\n    \"deleteWorkspaceHintText\": \"Sicher, dass du deinen Arbeitsbereich löschen möchtest? Dies kann nicht mehr Rückgängig gemacht werden.\",\n    \"createSuccess\": \"Arbeitsbereich erfolgreich erstellt\",\n    \"createFailed\": \"Der Arbeitsbereich konnte nicht erstellt werden\",\n    \"createLimitExceeded\": \"Du hast die für dein Benutzerkonto maximal zulässige Anzahl an Arbeitsbereichen erreicht. Benötigst du zum fortsetzen deiner Arbeit noch weitere Arbeitsbereiche, erstelle auf GitHub bitte eine entsprechende Anfrage.\",\n    \"deleteSuccess\": \"Arbeitsbereich erfolgreich gelöscht\",\n    \"deleteFailed\": \"Der Arbeitsbereich konnte nicht gelöscht werden\",\n    \"openSuccess\": \"Arbeitsbereich erfolgreich geöffnet\",\n    \"openFailed\": \"Der Arbeitsbereich konnte nicht geöffnet werden\",\n    \"renameSuccess\": \"Arbeitsbereich erfolgreich umbenannt\",\n    \"renameFailed\": \"Der Arbeitsbereich konnte nicht umbenannt werden\",\n    \"updateIconSuccess\": \"Arbeitsbereich erfolgreich zurückgesetzt\",\n    \"updateIconFailed\": \"Der Arbeitsbereich konnte nicht zurückgesetzt werden\",\n    \"cannotDeleteTheOnlyWorkspace\": \"Der einzig vorhandene Arbeitsbereich kann nicht gelöscht werden\",\n    \"fetchWorkspacesFailed\": \"Arbeitsbereiche konnten nicht abgerufen werden!\",\n    \"leaveCurrentWorkspace\": \"Arbeitsbereich verlassen\",\n    \"leaveCurrentWorkspacePrompt\": \"Möchtest du den aktuellen Arbeitsbereich wirklich verlassen?\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Teilen\",\n    \"workInProgress\": \"Demnächst verfügbar\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"In die Zwischenablage kopieren\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Link kopieren\",\n    \"publishToTheWeb\": \"Im Web veröffentlichen\",\n    \"publishToTheWebHint\": \"Erstelle eine Website mit AppFlowy\",\n    \"publish\": \"Veröffentlichen\",\n    \"unPublish\": \"Veröffentlichung aufheben\",\n    \"visitSite\": \"Seite aufrufen\",\n    \"exportAsTab\": \"Exportieren als\",\n    \"publishTab\": \"Veröffentlichen\",\n    \"shareTab\": \"Teilen\",\n    \"publishOnAppFlowy\": \"Auf AppFlowy veröffentlichen\",\n    \"shareTabTitle\": \"Zum Mitmachen einladen\",\n    \"shareTabDescription\": \"Für eine einfache Zusammenarbeit mit allen\",\n    \"copyLinkSuccess\": \"Link in die Zwischenablage kopiert\",\n    \"copyShareLink\": \"Link zum Teilen kopieren\",\n    \"copyLinkFailed\": \"Link konnte nicht in die Zwischenablage kopiert werden\",\n    \"copyLinkToBlockSuccess\": \"Blocklink in die Zwischenablage kopiert\",\n    \"copyLinkToBlockFailed\": \"Blocklink konnte nicht in die Zwischenablage kopiert werden\",\n    \"manageAllSites\": \"Alle Seiten verwalten\",\n    \"updatePathName\": \"Pfadnamen aktualisieren\"\n  },\n  \"moreAction\": {\n    \"small\": \"klein\",\n    \"medium\": \"mittel\",\n    \"large\": \"groß\",\n    \"fontSize\": \"Schriftgröße\",\n    \"import\": \"Importieren\",\n    \"moreOptions\": \"Weitere Optionen\",\n    \"wordCount\": \"Wortanzahl: {}\",\n    \"charCount\": \"Zeichenanzahl: {}\",\n    \"createdAt\": \"Erstellt am: {}\",\n    \"deleteView\": \"Löschen\",\n    \"duplicateView\": \"Duplizieren\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Text & Markdown\",\n    \"documentFromV010\": \"Dokument ab v0.1.0\",\n    \"databaseFromV010\": \"Datenbank ab v0.1.0\",\n    \"notionZip\": \"von Notion exportierte Zip-Datei\",\n    \"csv\": \"CSV\",\n    \"database\": \"Datenbank\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Umbenennen\",\n    \"delete\": \"Löschen\",\n    \"duplicate\": \"Duplizieren\",\n    \"unfavorite\": \"Aus Favoriten entfernen\",\n    \"favorite\": \"Zu Favoriten hinzufügen\",\n    \"openNewTab\": \"In einem neuen Tab öffnen\",\n    \"moveTo\": \"Verschieben nach\",\n    \"addToFavorites\": \"Zu Favoriten hinzufügen\",\n    \"copyLink\": \"Link kopieren\",\n    \"changeIcon\": \"Symbol ändern\",\n    \"collapseAllPages\": \"Alle Seiten einklappen\"\n  },\n  \"blankPageTitle\": \"Leere Seite\",\n  \"newPageText\": \"Neue Seite\",\n  \"newDocumentText\": \"Neues Dokument\",\n  \"newGridText\": \"Neue Datentabelle\",\n  \"newCalendarText\": \"Neuer Kalender\",\n  \"newBoardText\": \"Neues Board\",\n  \"chat\": {\n    \"newChat\": \"Neuer Chat\",\n    \"inputMessageHint\": \"Nachricht an @:appName AI\",\n    \"inputLocalAIMessageHint\": \"Nachricht an @:appName Lokale KI\",\n    \"unsupportedCloudPrompt\": \"Diese Funktion ist nur bei Verwendung der @:appName Cloud verfügbar\",\n    \"relatedQuestion\": \"Verwandt\",\n    \"serverUnavailable\": \"Dienst vorübergehend nicht verfügbar. Bitte versuche es später erneut.\",\n    \"aiServerUnavailable\": \"Beim Generieren einer Antwort ist ein Fehler aufgetreten.\",\n    \"retry\": \"Wiederholen\",\n    \"clickToRetry\": \"Erneut versuchen\",\n    \"regenerateAnswer\": \"Regenerieren\",\n    \"question1\": \"Wie verwendet man Kanban zur Aufgabenverwaltung?\",\n    \"question2\": \"Erkläre mir die GTD-Methode\",\n    \"question3\": \"Warum sollte ich Rust verwenden?\",\n    \"question4\": \"Gebe mir ein Rezept mit dem, was in meiner Küche ist\",\n    \"question5\": \"Eine Illustration für meine Seite erstellen\",\n    \"question6\": \"Erstelle eine To-Do-Liste für meine kommende Woche\",\n    \"aiMistakePrompt\": \"KI kann Fehler machen. Überprüfe wichtige Informationen.\",\n    \"chatWithFilePrompt\": \"Möchtest du mit der Datei chatten?\",\n    \"indexFileSuccess\": \"Datei erfolgreich indiziert\",\n    \"inputActionNoPages\": \"Keine Seitenergebnisse\",\n    \"referenceSource\": {\n      \"zero\": \"0 Quellen gefunden\",\n      \"one\": \"{count} Quelle gefunden\",\n      \"other\": \"{count} Quellen gefunden\"\n    },\n    \"clickToMention\": \"Klicke hier, um eine Seite zu erwähnen\",\n    \"uploadFile\": \"Lade PDF-, md- oder txt-Dateien in den Chat hoch\",\n    \"questionDetail\": \"Hallo {}! Wie kann ich dir heute helfen?\",\n    \"indexingFile\": \"Indizierung {}\",\n    \"generatingResponse\": \"Antwort generieren\",\n    \"selectSources\": \"Quellen auswählen\",\n    \"regenerate\": \"Versuchen Sie es erneut\",\n    \"addToPageButton\": \"Zur Seite hinzufügen\",\n    \"addToPageTitle\": \"Nachricht hinzufügen an...\",\n    \"addToNewPage\": \"Zu einer neuen Seite hinzufügen\"\n  },\n  \"trash\": {\n    \"text\": \"Papierkorb\",\n    \"restoreAll\": \"Alles wiederherstellen\",\n    \"restore\": \"Wiederherstellen\",\n    \"deleteAll\": \"Alles löschen\",\n    \"pageHeader\": {\n      \"fileName\": \"Dateiname\",\n      \"lastModified\": \"Letzte Änderung\",\n      \"created\": \"Erstellt\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Bist du dir sicher? Das löscht alle Seiten in den Papierkorb.\",\n      \"caption\": \"Diese Aktion kann nicht rückgängig gemacht werden.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Möchtest du wirklich alle Seiten aus dem Papierkorb wiederherstellen?\",\n      \"caption\": \"Diese Aktion kann nicht rückgängig gemacht werden.\"\n    },\n    \"restorePage\": {\n      \"title\": \"Wiederherstellen: {}\",\n      \"caption\": \"Möchten Sie diese Seite wirklich wiederherstellen?\"\n    },\n    \"mobile\": {\n      \"actions\": \"Papierkorb-Einstellungen\",\n      \"empty\": \"Der Papierkorb ist leer.\",\n      \"emptyDescription\": \"Es sind keine gelöschten Dateien vorhanden.\",\n      \"isDeleted\": \"wurde gelöscht\",\n      \"isRestored\": \"wurde wiederhergestellt\"\n    },\n    \"confirmDeleteTitle\": \"Bist du dir sicher, dass du diese Seite unwiderruflich löschen möchtest?\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Diese Seite befindet sich im Papierkorb\",\n    \"restore\": \"Seite wiederherstellen\",\n    \"deletePermanent\": \"Dauerhaft löschen\",\n    \"deletePermanentDescription\": \"Möchten Sie diese Seite wirklich dauerhaft löschen? Dies kann nicht rückgängig gemacht werden.\"\n  },\n  \"dialogCreatePageNameHint\": \"Seitenname\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Tastenkürzel\",\n    \"whatsNew\": \"Was gibt es Neues?\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"Debug-Informationen\",\n      \"success\": \"Debug-Informationen in die Zwischenablage kopiert!\",\n      \"fail\": \"Debug-Informationen konnten nicht in die Zwischenablage kopiert werden\"\n    },\n    \"feedback\": \"Feedback\",\n    \"help\": \"Hilfe & Support\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Entfernen, umbenennen und mehr...\",\n    \"addPageTooltip\": \"Schnell eine Seite hineinfügen\",\n    \"defaultNewPageName\": \"Unbenannt\",\n    \"renameDialog\": \"Umbenennen\"\n  },\n  \"noPagesInside\": \"Keine Unterseiten\",\n  \"toolbar\": {\n    \"undo\": \"Rückgängig\",\n    \"redo\": \"Wiederherstellen\",\n    \"bold\": \"Fett\",\n    \"italic\": \"Kursiv\",\n    \"underline\": \"Unterstrichen\",\n    \"strike\": \"Durchstrichen\",\n    \"numList\": \"Nummerierte Liste\",\n    \"bulletList\": \"Aufzählung\",\n    \"checkList\": \"Checkliste\",\n    \"inlineCode\": \"Inline-Code\",\n    \"quote\": \"Zitat\",\n    \"header\": \"Überschrift\",\n    \"highlight\": \"Hervorhebung\",\n    \"color\": \"Farbe\",\n    \"addLink\": \"Link hinzufügen\",\n    \"link\": \"Link\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"In den hellen Modus wechseln\",\n    \"darkMode\": \"In den dunklen Modus wechseln\",\n    \"openAsPage\": \"Als Seite öffnen\",\n    \"addNewRow\": \"Neue Zeile hinzufügen\",\n    \"openMenu\": \"Menü öffnen\",\n    \"dragRow\": \"Gedrückt halten, um die Zeile neu anzuordnen\",\n    \"viewDataBase\": \"Datenbank ansehen\",\n    \"referencePage\": \"Auf diesen {Name} wird verwiesen\",\n    \"addBlockBelow\": \"Einen Block hinzufügen\",\n    \"aiGenerate\": \"Erzeugen\",\n    \"urlLaunchAccessory\": \"Im Browser öffnen\",\n    \"urlCopyAccessory\": \"Webadresse kopieren.\",\n    \"genSummary\": \"Zusammenfassung generieren\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Seitenleiste schließen\",\n    \"openSidebar\": \"Seitenleiste öffnen\",\n    \"personal\": \"Persönlich\",\n    \"private\": \"Privat\",\n    \"workspace\": \"Arbeitsbereich\",\n    \"favorites\": \"Favoriten\",\n    \"clickToHidePrivate\": \"Hier klicken, um den privaten Bereich auszublenden.\\nVon dir hier erstellte Seiten sind nur für dich sichtbar.\",\n    \"clickToHideWorkspace\": \"Klicken, um den Arbeitsbereich auszublenden\\nDie hier erstellten Seiten sind für jedes Mitglied sichtbar\",\n    \"clickToHidePersonal\": \"Klicken, um den persönlichen Abschnitt zu verbergen\",\n    \"clickToHideFavorites\": \"Klicken, um Favoriten zu verbergen\",\n    \"addAPage\": \"Seite hinzufügen\",\n    \"addAPageToPrivate\": \"Eine Seite zum privaten Bereich hinzufügen.\",\n    \"addAPageToWorkspace\": \"Eine Seite zum Arbeitsbereich hinzufügen\",\n    \"recent\": \"Kürzlich\",\n    \"today\": \"Heute\",\n    \"thisWeek\": \"Diese Woche\",\n    \"others\": \"Andere\",\n    \"earlier\": \"Früher\",\n    \"justNow\": \"soeben\",\n    \"minutesAgo\": \"vor {count} Minuten\",\n    \"lastViewed\": \"Zuletzt angesehen\",\n    \"favoriteAt\": \"Zu Favoriten hinzugefügt bei\",\n    \"emptyRecent\": \"Keine aktuellen Dokumente\",\n    \"emptyRecentDescription\": \"Kürzlich aufgerufene Dokumente werden hier, zur vereinfachten Auffindbarkeit, angezeigt.\",\n    \"emptyFavorite\": \"Keine favorisierten Dokumente\",\n    \"emptyFavoriteDescription\": \"Beginne mit der Erkundung und markiere Dokumente als Favoriten. Diese werden hier für den schnellen Zugriff aufgelistet!\",\n    \"removePageFromRecent\": \"Diese Seite aus „Kürzlich“ entfernen?\",\n    \"removeSuccess\": \"Erfolgreich entfernt\",\n    \"favoriteSpace\": \"Favoriten\",\n    \"RecentSpace\": \"Kürzlich\",\n    \"Spaces\": \"Bereiche\",\n    \"upgradeToPro\": \"Upgrade auf Pro\",\n    \"upgradeToAIMax\": \"Schalte unbegrenzte KI frei\",\n    \"storageLimitDialogTitle\": \"Dein freier Speicherplatz ist aufgebraucht. Upgrade deinen Plan, um unbegrenzten Speicherplatz freizuschalten.\",\n    \"storageLimitDialogTitleIOS\": \"Ihr freier Speicherplatz ist aufgebraucht.\",\n    \"aiResponseLimitTitle\": \"Du hast keine kostenlosen KI-Antworten mehr. Upgrade auf den Pro-Plan oder kaufe ein KI-Add-on, um unbegrenzte Antworten freizuschalten\",\n    \"aiResponseLimitDialogTitle\": \"Limit für KI-Antworten erreicht\",\n    \"aiResponseLimit\": \"Du hast keine kostenlosen KI-Antworten mehr zur Verfügung.\\n\\nGehe zu Einstellungen -> Plan -> Klicke auf KI Max oder Pro Plan, um mehr KI-Antworten zu erhalten\",\n    \"askOwnerToUpgradeToPro\": \"Dein Arbeitsbereich hat nicht mehr genügend freien Speicherplatz. Bitte den Eigentümer deines Arbeitsbereichs, auf den Pro-Plan hochzustufen.\",\n    \"askOwnerToUpgradeToProIOS\": \"In Ihrem Arbeitsbereich ist nicht mehr genügend freier Speicherplatz verfügbar.\",\n    \"askOwnerToUpgradeToAIMax\": \"In deinem Arbeitsbereich sind die kostenlosen KI-Antworten aufgebraucht. Bitte den Eigentümer deines Arbeitsbereichs, den Plan zu wechseln oder KI-Add-ons zu erwerben.\",\n    \"askOwnerToUpgradeToAIMaxIOS\": \"In Ihrem Arbeitsbereich gehen die kostenlosen KI-Antworten aus.\",\n    \"purchaseStorageSpace\": \"Speicherplatz kaufen\",\n    \"singleFileProPlanLimitationDescription\": \"Sie haben die maximal zulässige Datei-Uploadgröße im kostenlosen Plan überschritten. Bitte aktualisieren Sie auf den Pro-Plan, um größere Dateien hochzuladen\",\n    \"purchaseAIResponse\": \"Kaufen \",\n    \"askOwnerToUpgradeToLocalAI\": \"Bitte den Arbeitsbereichsbesitzer, KI auf dem Gerät zu aktivieren.\",\n    \"upgradeToAILocal\": \"KI offline auf Ihrem Gerät\",\n    \"upgradeToAILocalDesc\": \"Chatte mit PDFs, verbessere deine Schreibfähigkeiten und fülle Tabellen automatisch mithilfe lokaler KI aus.\",\n    \"public\": \"Öffentlich\",\n    \"clickToHidePublic\": \"Hier klicken, um den öffentlichen Bereich auszublenden.\\nHier erstellte Seiten sind für jedes Mitglied sichtbar.\",\n    \"addAPageToPublic\": \"Eine Seite zur öffentlichen Domäne hinzufügen.\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Notiz nach Markdown exportiert\",\n      \"path\": \"Dokumente/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Kontakte\",\n    \"whatsHappening\": \"Was passiert diese Woche?\",\n    \"addContact\": \"Kontakte hinzufügen\",\n    \"editContact\": \"Kontakte bearbeiten\"\n  },\n  \"button\": {\n    \"ok\": \"OK\",\n    \"confirm\": \"Bestätigen\",\n    \"done\": \"Erledigt!\",\n    \"cancel\": \"Abbrechen\",\n    \"signIn\": \"Anmelden\",\n    \"signOut\": \"Abmelden\",\n    \"complete\": \"Fertig\",\n    \"save\": \"Speichern\",\n    \"generate\": \"Erstellen\",\n    \"esc\": \"ESC\",\n    \"keep\": \"behalten\",\n    \"tryAgain\": \"Nochmal versuchen\",\n    \"discard\": \"Verwerfen\",\n    \"replace\": \"Ersetzen\",\n    \"insertBelow\": \"Unten einfügen\",\n    \"insertAbove\": \"Oben einfügen\",\n    \"upload\": \"Hochladen\",\n    \"edit\": \"Bearbeiten\",\n    \"delete\": \"Löschen\",\n    \"copy\": \"kopieren\",\n    \"duplicate\": \"Duplikat\",\n    \"putback\": \"wieder zurückgeben\",\n    \"update\": \"Update\",\n    \"share\": \"Teilen\",\n    \"removeFromFavorites\": \"Aus den Favoriten entfernen\",\n    \"removeFromRecent\": \"Aus „Kürzlich“ entfernen\",\n    \"addToFavorites\": \"Zu den Favoriten hinzufügen\",\n    \"favoriteSuccessfully\": \"Erfolgreich favorisiert\",\n    \"unfavoriteSuccessfully\": \"Erfolgreich entfavorisiert\",\n    \"duplicateSuccessfully\": \"Erfolgreich dupliziert\",\n    \"rename\": \"Umbenennen\",\n    \"helpCenter\": \"Hilfe Center\",\n    \"add\": \"Hinzufügen\",\n    \"yes\": \"Ja\",\n    \"no\": \"Nein\",\n    \"clear\": \"Leeren\",\n    \"remove\": \"Entfernen\",\n    \"dontRemove\": \"Nicht entfernen\",\n    \"copyLink\": \"Link kopieren\",\n    \"align\": \"zentrieren\",\n    \"login\": \"Anmelden\",\n    \"logout\": \"Abmelden\",\n    \"deleteAccount\": \"Benutzerkonto löschen\",\n    \"back\": \"Zurück\",\n    \"signInGoogle\": \"Mit einem Google Benutzerkonto anmelden\",\n    \"signInGithub\": \"Mit einem Github Benutzerkonto anmelden\",\n    \"signInDiscord\": \"Mit einem Discord Benutzerkonto anmelden\",\n    \"more\": \"Mehr\",\n    \"create\": \"Erstellen\",\n    \"close\": \"Schließen\",\n    \"next\": \"Weiter\",\n    \"previous\": \"Zurück\",\n    \"submit\": \"Einreichen\",\n    \"download\": \"Herunterladen\",\n    \"backToHome\": \"Zurück zur Startseite\",\n    \"viewing\": \"anschauen\",\n    \"editing\": \"Bearbeiten\",\n    \"gotIt\": \"Verstanden\"\n  },\n  \"label\": {\n    \"welcome\": \"Willkommen!\",\n    \"firstName\": \"Vorname\",\n    \"middleName\": \"Zweiter Vorname\",\n    \"lastName\": \"Nachname\",\n    \"stepX\": \"Schritt {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Keine Verbindung zum Konto möglich.\",\n      \"failedMsg\": \"Prüfe, ob der Anmeldevorgang im Browser abgeschlossen wurde.\"\n    },\n    \"google\": {\n      \"title\": \"Google Sign-In\",\n      \"instruction1\": \"Um die Google-Kontakte zu importieren, muss die Anwendung über den Webbrowser autorisiert werden.\",\n      \"instruction2\": \"Kopiere den Code in die Zwischenablage, über das Symbol oder indem du den Text auswählst:\",\n      \"instruction3\": \"Rufe den folgenden Link im Webbrowser auf und gebe den Code ein:\",\n      \"instruction4\": \"Klicke unten auf die Schaltfläche, wenn die Anmeldung abgeschlossen ist:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Einstellungen\",\n    \"popupMenuItem\": {\n      \"settings\": \"Einstellungen\",\n      \"members\": \"Mitglieder\",\n      \"trash\": \"Müll\",\n      \"helpAndSupport\": \"Hilfe & Unterstützung\"\n    },\n    \"sites\": {\n      \"title\": \"Seiten\",\n      \"namespaceTitle\": \"Namensraum\",\n      \"namespaceDescription\": \"Verwalten Sie Ihren Namespace und Ihre Startseite\",\n      \"namespaceHeader\": \"Namensraum\",\n      \"homepageHeader\": \"Startseite\",\n      \"updateNamespace\": \"Namespace aktualisieren\",\n      \"removeHomepage\": \"Startseite entfernen\",\n      \"selectHomePage\": \"Seite auswählen\",\n      \"clearHomePage\": \"Löschen Sie die Startseite für diesen Namensraum\",\n      \"customUrl\": \"Benutzerdefinierte URL\",\n      \"namespace\": {\n        \"description\": \"Diese Änderung gilt für alle veröffentlichten Seiten in diesem Namespace.\",\n        \"tooltip\": \"Wir behalten uns das Recht vor, unangemessene Namespaces zu entfernen\",\n        \"updateExistingNamespace\": \"Vorhandenen Namensraum aktualisieren\",\n        \"upgradeToPro\": \"Aktualisieren Sie auf den Pro-Plan, um eine Startseite einzurichten\",\n        \"redirectToPayment\": \"Weiterleitung zur Zahlungsseite ...\",\n        \"onlyWorkspaceOwnerCanSetHomePage\": \"Nur der Arbeitsbereichsbesitzer kann eine Startseite festlegen\",\n        \"pleaseAskOwnerToSetHomePage\": \"Bitten Sie den Arbeitsbereichsbesitzer, auf den Pro-Plan zu aktualisieren\"\n      },\n      \"publishedPage\": {\n        \"title\": \"Alle veröffentlichten Seiten\",\n        \"description\": \"Verwalten Sie Ihre veröffentlichten Seiten\",\n        \"page\": \"Seite\",\n        \"pathName\": \"Pfadname\",\n        \"date\": \"Veröffentlichungsdatum\",\n        \"emptyHinText\": \"Sie haben keine veröffentlichten Seiten in diesem Arbeitsbereich\",\n        \"noPublishedPages\": \"Keine veröffentlichten Seiten\",\n        \"settings\": \"Veröffentlichungseinstellungen\",\n        \"clickToOpenPageInApp\": \"Seite in App öffnen\",\n        \"clickToOpenPageInBrowser\": \"Seite im Browser öffnen\"\n      },\n      \"error\": {\n        \"failedToGeneratePaymentLink\": \"Zahlungslink für Pro Plan konnte nicht generiert werden\",\n        \"failedToUpdateNamespace\": \"Namensraum konnte nicht aktualisiert werden\",\n        \"proPlanLimitation\": \"Sie müssen auf den Pro-Plan upgraden, um den Namespace zu aktualisieren\",\n        \"namespaceAlreadyInUse\": \"Der Namespace ist bereits vergeben, bitte versuchen Sie es mit einem anderen\",\n        \"invalidNamespace\": \"Ungültiger Namensraum, bitte versuchen Sie einen anderen\",\n        \"namespaceLengthAtLeast2Characters\": \"Der Namensraum muss mindestens 2 Zeichen lang sein\",\n        \"onlyWorkspaceOwnerCanUpdateNamespace\": \"Nur der Arbeitsbereichsbesitzer kann den Namespace aktualisieren\",\n        \"onlyWorkspaceOwnerCanRemoveHomepage\": \"Nur der Arbeitsbereichsbesitzer kann die Homepage entfernen\",\n        \"setHomepageFailed\": \"Startseite konnte nicht eingerichtet werden\",\n        \"namespaceTooLong\": \"Der Namensraum ist zu lang. Bitte versuchen Sie es mit einem anderen.\",\n        \"namespaceTooShort\": \"Der Namensraum ist zu kurz, bitte versuchen Sie es mit einem anderen\",\n        \"namespaceIsReserved\": \"Der Namensraum ist reserviert, bitte versuchen Sie es mit einem anderen\",\n        \"updatePathNameFailed\": \"Pfadname konnte nicht aktualisiert werden\",\n        \"removeHomePageFailed\": \"Startseite konnte nicht entfernt werden\",\n        \"publishNameContainsInvalidCharacters\": \"Der Pfadname enthält ungültige Zeichen. Bitte versuchen Sie es mit einem anderen.\",\n        \"publishNameTooShort\": \"Der Pfadname ist zu kurz, bitte versuchen Sie es mit einem anderen\",\n        \"publishNameTooLong\": \"Der Pfadname ist zu lang, bitte versuchen Sie es mit einem anderen\",\n        \"publishNameAlreadyInUse\": \"Der Pfadname wird bereits verwendet. Bitte versuchen Sie einen anderen.\",\n        \"namespaceContainsInvalidCharacters\": \"Der Namespace enthält ungültige Zeichen. Bitte versuchen Sie es mit einem anderen.\",\n        \"publishPermissionDenied\": \"Nur der Arbeitsbereichsbesitzer oder Seitenherausgeber kann die Veröffentlichungseinstellungen verwalten\",\n        \"publishNameCannotBeEmpty\": \"Der Pfadname darf nicht leer sein. Bitte versuchen Sie es mit einem anderen.\"\n      },\n      \"success\": {\n        \"namespaceUpdated\": \"Namensraum erfolgreich aktualisiert\",\n        \"setHomepageSuccess\": \"Startseite erfolgreich eingerichtet\",\n        \"updatePathNameSuccess\": \"Pfadname erfolgreich aktualisiert\",\n        \"removeHomePageSuccess\": \"Startseite erfolgreich entfernt\"\n      }\n    },\n    \"accountPage\": {\n      \"menuLabel\": \"Mein Konto\",\n      \"title\": \"Mein Konto\",\n      \"general\": {\n        \"title\": \"Kontoname und Profilbild\",\n        \"changeProfilePicture\": \"Profilbild ändern\"\n      },\n      \"email\": {\n        \"title\": \"E-Mail\",\n        \"actions\": {\n          \"change\": \"E-Mail ändern\"\n        }\n      },\n      \"login\": {\n        \"title\": \"Kontoanmeldung\",\n        \"loginLabel\": \"Anmeldung\",\n        \"logoutLabel\": \"Ausloggen\"\n      },\n      \"description\": \"Passe dein Profil an, verwalte deine Sicherheitseinstellungen und KI API-Schlüssel oder melde dich bei deinem Konto an.\"\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"Arbeitsbereich\",\n      \"title\": \"Arbeitsbereich\",\n      \"description\": \"Passe das Erscheinungsbild, das Design, die Schriftart, das Textlayout, das Datums-/Zeitformat und die Sprache deines Arbeitsbereiches an.\",\n      \"workspaceName\": {\n        \"title\": \"Name des Arbeitsbereiches\",\n        \"savedMessage\": \"Name des Arbeitsbereiches gespeichert\",\n        \"editTooltip\": \"Name des Arbeitsbereiches ändern\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"Symbol\",\n        \"description\": \"Lade ein Bild hoch oder verwende ein Emoji für deinen Arbeitsbereich. Das Symbol wird in deiner Seitenleiste und in deinen Benachrichtigungen angezeigt.\"\n      },\n      \"appearance\": {\n        \"title\": \"Aussehen\",\n        \"description\": \"Passe das Erscheinungsbild, das Design, die Schriftart, das Textlayout, das Datums-/Zeitformat und die Sprache deines Arbeitsbereiches an.\",\n        \"options\": {\n          \"system\": \"Auto\",\n          \"light\": \"Hell\",\n          \"dark\": \"Dunkel\"\n        }\n      },\n      \"resetCursorColor\": {\n        \"title\": \"Farbe des Dokumentcursors zurücksetzen\",\n        \"description\": \"Möchtest du die Cursorfarbe wirklich zurücksetzen?\"\n      },\n      \"resetSelectionColor\": {\n        \"title\": \"Dokumentauswahlfarbe zurücksetzen\",\n        \"description\": \"Möchtest du die Auswahlfarbe wirklich zurücksetzen?\"\n      },\n      \"resetWidth\": {\n        \"resetSuccess\": \"Dokumentbreite erfolgreich zurückgesetzt\"\n      },\n      \"theme\": {\n        \"title\": \"Design\",\n        \"description\": \"Wähle ein voreingestelltes Design aus oder lade dein eigenes benutzerdefiniertes Design hoch.\",\n        \"uploadCustomThemeTooltip\": \"Ein benutzerdefiniertes Theme hochladen\"\n      },\n      \"workspaceFont\": {\n        \"title\": \"Schriftart\",\n        \"noFontHint\": \"Keine Schriftart gefunden, versuchen Sie einen anderen Begriff.\"\n      },\n      \"textDirection\": {\n        \"title\": \"Textrichtung\",\n        \"leftToRight\": \"Links nach rechts\",\n        \"rightToLeft\": \"Rechts nach links\",\n        \"auto\": \"Auto\",\n        \"enableRTLItems\": \"RTL-Symbolleistenelemente aktivieren\"\n      },\n      \"layoutDirection\": {\n        \"title\": \"Layoutrichtung\",\n        \"leftToRight\": \"Links nach rechts\",\n        \"rightToLeft\": \"Rechts nach links\"\n      },\n      \"dateTime\": {\n        \"title\": \"Datum & Zeit\",\n        \"example\": \"{} um {} ({})\",\n        \"24HourTime\": \"24-Stunden-Zeit\",\n        \"dateFormat\": {\n          \"label\": \"Datumsformat\",\n          \"local\": \"Lokal\",\n          \"us\": \"US\",\n          \"iso\": \"ISO\",\n          \"friendly\": \"Leserlich\",\n          \"dmy\": \"T/M/J\"\n        }\n      },\n      \"language\": {\n        \"title\": \"Sprache\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"Arbeitsbereich löschen\",\n        \"content\": \"Möchtest du diesen Arbeitsbereich wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"Arbeitsbereich verlassen\",\n        \"content\": \"Möchtest du diesen Arbeitsbereich wirklich verlassen? Du verlierst den Zugriff auf alle darin enthaltenen Seiten und Daten.\",\n        \"success\": \"Sie haben den Arbeitsbereich erfolgreich verlassen.\",\n        \"fail\": \"Das Verlassen des Arbeitsbereichs ist fehlgeschlagen.\"\n      },\n      \"manageWorkspace\": {\n        \"title\": \"Arbeitsbereich verwalten\",\n        \"leaveWorkspace\": \"Arbeitsbereich verlassen\",\n        \"deleteWorkspace\": \"Arbeitsbereich löschen\"\n      }\n    },\n    \"manageDataPage\": {\n      \"menuLabel\": \"Daten verwalten\",\n      \"title\": \"Daten verwalten\",\n      \"description\": \"Verwalte den lokalen Datenspeicher oder importiere deine vorhandenen Daten in @:appName. Du kannst deine Daten mit Ende-zu-Ende-Verschlüsselung absichern.\",\n      \"dataStorage\": {\n        \"title\": \"Speicherort\",\n        \"tooltip\": \"Das Verzeichnis, in dem deine Dateien gespeichert sind\",\n        \"actions\": {\n          \"change\": \"Pfad ändern\",\n          \"open\": \"Ordner öffnen\",\n          \"openTooltip\": \"Aktuellen Speicherort des Datenordners öffnen\",\n          \"copy\": \"Pfad kopieren\",\n          \"copiedHint\": \"Link kopiert!\",\n          \"resetTooltip\": \"Auf Standardspeicherort zurücksetzen\"\n        },\n        \"resetDialog\": {\n          \"title\": \"Bist du sicher?\",\n          \"description\": \"Durch das Zurücksetzen des Pfads auf das Standardverzeichnis werden deine Daten nicht gelöscht. Wenn du deine aktuellen Daten erneut importieren möchtest, solltest du zuerst den Pfad deines aktuellen Speicherorts kopieren.\"\n        }\n      },\n      \"importData\": {\n        \"title\": \"Daten importieren\",\n        \"tooltip\": \"Daten aus @:appName Backups-/Datenordnern importieren\",\n        \"description\": \"Daten aus einem externen @:appName Datenordner kopieren und in den aktuellen @:appName Datenordner importieren\",\n        \"action\": \"Ordner durchsuchen\"\n      },\n      \"encryption\": {\n        \"title\": \"Verschlüsselung\",\n        \"tooltip\": \"Verwalte, wie deine Daten gespeichert und verschlüsselt werden\",\n        \"descriptionNoEncryption\": \"Durch das Einschalten der Verschlüsselung werden alle Daten verschlüsselt. Dieser Vorgang kann nicht rückgängig gemacht werden.\",\n        \"descriptionEncrypted\": \"Deine Daten sind verschlüsselt.\",\n        \"action\": \"Daten verschlüsseln\",\n        \"dialog\": {\n          \"title\": \"Alle deine Daten verschlüsseln?\",\n          \"description\": \"Durch die Verschlüsselung all deiner Daten bleiben diese sicher und geschützt. Diese Aktion kann NICHT rückgängig gemacht werden. Möchtest du wirklich fortfahren?\"\n        }\n      },\n      \"cache\": {\n        \"title\": \"Cache leeren\",\n        \"description\": \"Wenn Bilder nicht geladen werden oder Schriftarten nicht richtig angezeigt werden, versuche den Cache zu leeren. Deine Benutzerdaten werden dadurch nicht gelöscht.\",\n        \"dialog\": {\n          \"title\": \"Bist du sicher?\",\n          \"description\": \"Durch das Leeren des Caches werden Bilder und Schriftarten beim Laden erneut heruntergeladen. Deine Daten werden durch diese Aktion weder entfernt noch geändert.\",\n          \"successHint\": \"Cache geleert!\"\n        }\n      },\n      \"data\": {\n        \"fixYourData\": \"Deine Daten korrigieren\",\n        \"fixButton\": \"Korrigieren\",\n        \"fixYourDataDescription\": \"Wenn du Probleme mit deinen Daten hast, kannst du hier versuchen, diese zu beheben.\"\n      }\n    },\n    \"shortcutsPage\": {\n      \"menuLabel\": \"Tastenkombinationen\",\n      \"title\": \"Shortcuts\",\n      \"editBindingHint\": \"Neue Verknüpfung eingeben\",\n      \"searchHint\": \"Suchen\",\n      \"actions\": {\n        \"resetDefault\": \"Standardeinstellung zurücksetzen\"\n      },\n      \"errorPage\": {\n        \"message\": \"Shortcuts konnten nicht geladen werden: {}\",\n        \"howToFix\": \"Bitte versuche es erneut. Wenn das Problem weiterhin besteht, melde es bitte auf GitHub.\"\n      },\n      \"resetDialog\": {\n        \"title\": \"Shortcuts zurücksetzen\",\n        \"description\": \"Dies wird alle deine Shortcuts auf die Standardeinstellungen zurücksetzen, dies kann nicht rückgängig gemacht werden. Bist du dir sicher, dass du fortfahren möchtest?\",\n        \"buttonLabel\": \"Zurücksetzen\"\n      },\n      \"conflictDialog\": {\n        \"title\": \"{} ist derzeit in Verwendung\",\n        \"descriptionPrefix\": \"Diese Tastenkombination wird derzeit verwendet von \",\n        \"descriptionSuffix\": \". Wenn du diese Tastaturbelegung ersetzt, wird sie aus {} entfernt.\",\n        \"confirmLabel\": \"Weiter\"\n      },\n      \"editTooltip\": \"Zum Starten der Bearbeitung der Tastaturbelegung drücken.\",\n      \"keybindings\": {\n        \"toggleToDoList\": \"Aufgabenliste ein-/ausblenden\",\n        \"insertNewParagraphInCodeblock\": \"Neuen Absatz einfügen\",\n        \"pasteInCodeblock\": \"In Codeblock einfügen\",\n        \"selectAllCodeblock\": \"Alles auswählen\",\n        \"indentLineCodeblock\": \"Zwei Leerzeichen am Zeilenanfang einfügen\",\n        \"outdentLineCodeblock\": \"Zwei Leerzeichen am Zeilenanfang löschen\",\n        \"twoSpacesCursorCodeblock\": \"Zwei Leerzeichen am Cursor einfügen\",\n        \"copy\": \"Auswahl kopieren\",\n        \"paste\": \"Inhalt einfügen\",\n        \"cut\": \"Auswahl ausschneiden\",\n        \"alignLeft\": \"Text links ausrichten\",\n        \"alignCenter\": \"Text zentriert ausrichten\",\n        \"alignRight\": \"Text rechts ausrichten\",\n        \"undo\": \"Undo\",\n        \"redo\": \"Redo\",\n        \"convertToParagraph\": \"Block in Absatz umwandeln\",\n        \"backspace\": \"Löschen\",\n        \"deleteLeftWord\": \"Linkes Wort löschen\",\n        \"deleteLeftSentence\": \"Linken Satz löschen\",\n        \"delete\": \"Rechtes Zeichen löschen\",\n        \"deleteMacOS\": \"Linkes Zeichen löschen\",\n        \"deleteRightWord\": \"Rechtes Wort löschen\",\n        \"moveCursorLeft\": \"Cursor nach links bewegen\",\n        \"moveCursorBeginning\": \"Cursor an den Zeilenanfang bewegen\",\n        \"moveCursorLeftWord\": \"Cursor ein Wort nach links bewegen\",\n        \"moveCursorLeftSelect\": \"Auswählen und Cursor nach links bewegen\",\n        \"moveCursorBeginSelect\": \"Auswählen und Cursor an den Zeilenanfang bewegen\",\n        \"moveCursorLeftWordSelect\": \"Auswählen und Cursor ein Wort nach links bewegen\",\n        \"moveCursorRight\": \"Cursor nach rechts bewegen\",\n        \"moveCursorEnd\": \"Cursor an das Zeilenende bewegen\",\n        \"moveCursorRightWord\": \"Cursor ein Wort nach rechts bewegen\",\n        \"moveCursorRightSelect\": \"Auswählen und Cursor nach rechts bewegen\",\n        \"moveCursorEndSelect\": \"Auswählen und Cursor an das Zeilenende bewegen\",\n        \"moveCursorRightWordSelect\": \"Markiere das Wort und bewege den Cursor ein Wort nach rechts\",\n        \"moveCursorUp\": \"Cursor nach oben bewegen\",\n        \"moveCursorTopSelect\": \"Auswählen und Cursor zum Anfang bewegen\",\n        \"moveCursorTop\": \"Cursor zum Anfang bewegen\",\n        \"moveCursorUpSelect\": \"Auswählen und Cursor nach oben bewegen\",\n        \"moveCursorBottomSelect\": \"Auswählen und Cursor ans Ende bewegen\",\n        \"moveCursorBottom\": \"Cursor ans Ende bewegen\",\n        \"moveCursorDown\": \"Cursor nach unten bewegen\",\n        \"moveCursorDownSelect\": \"Auswählen und Cursor nach unten bewegen\",\n        \"home\": \"Zum Anfang scrollen\",\n        \"end\": \"Zum Ende scrollen\",\n        \"toggleBold\": \"Fett ein-/ausschalten\",\n        \"toggleItalic\": \"Kursivschrift ein-/ausschalten\",\n        \"toggleUnderline\": \"Unterstreichung ein-/ausschalten\",\n        \"toggleStrikethrough\": \"Durchgestrichen ein-/ausschalten\",\n        \"toggleCode\": \"Inline-Code ein-/ausschalten\",\n        \"toggleHighlight\": \"Hervorhebung ein-/ausschalten\",\n        \"showLinkMenu\": \"Linkmenü anzeigen\",\n        \"openInlineLink\": \"Inline-Link öffnen\",\n        \"openLinks\": \"Alle ausgewählten Links öffnen\",\n        \"indent\": \"Einzug\",\n        \"outdent\": \"Ausrücken\",\n        \"exit\": \"Bearbeitung beenden\",\n        \"pageUp\": \"Eine Seite nach oben scrollen\",\n        \"pageDown\": \"Eine Seite nach unten scrollen\",\n        \"selectAll\": \"Alles auswählen\",\n        \"pasteWithoutFormatting\": \"Inhalt ohne Formatierung einfügen\",\n        \"showEmojiPicker\": \"Emoji-Auswahl anzeigen\",\n        \"enterInTableCell\": \"Zeilenumbruch in Tabelle hinzufügen\",\n        \"leftInTableCell\": \"In der Tabelle eine Zelle nach links verschieben\",\n        \"rightInTableCell\": \"In der Tabelle eine Zelle nach rechts verschieben\",\n        \"upInTableCell\": \"In der Tabelle eine Zelle nach oben verschieben\",\n        \"downInTableCell\": \"In der Tabelle eine Zelle nach unten verschieben\",\n        \"tabInTableCell\": \"Zur nächsten verfügbaren Zelle in der Tabelle gehen\",\n        \"shiftTabInTableCell\": \"Zur zuvor verfügbaren Zelle in der Tabelle gehen\",\n        \"backSpaceInTableCell\": \"Am Anfang der Zelle anhalten\"\n      },\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"Füge einen neuen Absatz neben dem Codeblock ein\",\n        \"codeBlockIndentLines\": \"Füge am Zeilenanfang im Codeblock zwei Leerzeichen ein\",\n        \"codeBlockOutdentLines\": \"Lösche zwei Leerzeichen am Zeilenanfang im Codeblock\",\n        \"codeBlockAddTwoSpaces\": \"Einfügen von zwei Leerzeichen an der Cursorposition im Codeblock\",\n        \"codeBlockSelectAll\": \"Wähle den gesamten Inhalt innerhalb eines Codeblocks aus\",\n        \"codeBlockPasteText\": \"Text in Codeblock einfügen\",\n        \"textAlignLeft\": \"Text nach links ausrichten\",\n        \"textAlignCenter\": \"Text nach rechts ausrichten\",\n        \"textAlignRight\": \"Text rechtsbündig ausrichten\"\n      },\n      \"couldNotLoadErrorMsg\": \"Konnte keine Shortcuts laden, versuche es erneut\",\n      \"couldNotSaveErrorMsg\": \"Shortcuts konnten nicht gespeichert werden, versuche es erneut\"\n    },\n    \"aiPage\": {\n      \"title\": \"KI-Einstellungen\",\n      \"menuLabel\": \"KI-Einstellungen\",\n      \"keys\": {\n        \"enableAISearchTitle\": \"KI-Suche\",\n        \"aiSettingsDescription\": \"Wähle oder konfiguriere KI-Modelle, die in @:appName verwendet werden. Für eine optimale Leistung empfehlen wir die Verwendung der Standardmodelloptionen\",\n        \"loginToEnableAIFeature\": \"KI-Funktionen werden erst nach der Anmeldung bei @:appName Cloud aktiviert. Wenn du kein @:appName-Konto hast, gehe zu „Mein Konto“, um dich zu registrieren\",\n        \"llmModel\": \"Sprachmodell\",\n        \"llmModelType\": \"Sprachmodelltyp\",\n        \"downloadLLMPrompt\": \"Herunterladen {}\",\n        \"downloadAppFlowyOfflineAI\": \"Durch das Herunterladen des KI-Offlinepakets kann KI auf deinem Gerät ausgeführt werden. Möchtest du fortfahren?\",\n        \"downloadLLMPromptDetail\": \"Das Herunterladen des lokalen Modells {} beansprucht bis zu {} Speicherplatz. Möchtest du fortfahren?\",\n        \"downloadBigFilePrompt\": \"Der Download kann etwa 10 Minuten dauern\",\n        \"downloadAIModelButton\": \"KI-Modell herunterladen\",\n        \"downloadingModel\": \"wird heruntergeladen\",\n        \"localAILoaded\": \"Lokales KI-Modell erfolgreich hinzugefügt und einsatzbereit\",\n        \"localAIStart\": \"Der lokale KI-Chat beginnt …\",\n        \"localAILoading\": \"Das lokale KI-Chat-Modell wird geladen …\",\n        \"localAIStopped\": \"Lokale KI wurde gestoppt\",\n        \"failToLoadLocalAI\": \"Lokale KI konnte nicht gestartet werden\",\n        \"restartLocalAI\": \"Lokale KI neustarten\",\n        \"disableLocalAITitle\": \"Lokale KI deaktivieren\",\n        \"disableLocalAIDescription\": \"Möchtest du die lokale KI deaktivieren?\",\n        \"localAIToggleTitle\": \"Umschalten zum Aktivieren oder Deaktivieren der lokalen KI\",\n        \"offlineAIInstruction1\": \"Folge der\",\n        \"offlineAIInstruction2\": \"Anweisung\",\n        \"offlineAIInstruction3\": \"um Offline-KI zu aktivieren.\",\n        \"offlineAIDownload1\": \"Wenn du die AppFlowy KI noch nicht heruntergeladen hast,\",\n        \"offlineAIDownload2\": \"lade\",\n        \"offlineAIDownload3\": \"sie zuerst herunter\",\n        \"activeOfflineAI\": \"Aktiv\",\n        \"downloadOfflineAI\": \"Herunterladen\",\n        \"openModelDirectory\": \"Ordner öffnen\",\n        \"title\": \"KI-API-Schlüssel\"\n      }\n    },\n    \"planPage\": {\n      \"menuLabel\": \"Plan\",\n      \"title\": \"Tarifplan\",\n      \"planUsage\": {\n        \"title\": \"Zusammenfassung der Plannutzung\",\n        \"storageLabel\": \"Speicher\",\n        \"storageUsage\": \"{} von {} GB\",\n        \"unlimitedStorageLabel\": \"Unbegrenzter Speicherplatz\",\n        \"collaboratorsLabel\": \"Gastmitarbeiter\",\n        \"collaboratorsUsage\": \"{} von {}\",\n        \"aiResponseLabel\": \"KI-Antworten\",\n        \"aiResponseUsage\": \"{} von {}\",\n        \"unlimitedAILabel\": \"Unbegrenzte Antworten\",\n        \"proBadge\": \"Pro\",\n        \"aiMaxBadge\": \"KI Max\",\n        \"aiOnDeviceBadge\": \"KI On-Device\",\n        \"memberProToggle\": \"Unbegrenzte Mitgliederzahl\",\n        \"aiMaxToggle\": \"Unbegrenzte KI-Antworten\",\n        \"aiOnDeviceToggle\": \"KI auf dem Gerät für ultimative Privatsphäre\",\n        \"aiCredit\": {\n          \"title\": \"@:appName KI-Guthaben hinzufügen\",\n          \"price\": \"{}\",\n          \"priceDescription\": \"für 1.000 Credits\",\n          \"purchase\": \"Kauf von KI\",\n          \"info\": \"Füge 1.000 KI-Credits pro Arbeitsbereich hinzu und integriere anpassbare KI nahtlos in deinen Arbeitsablauf für intelligentere, schnellere Ergebnisse mit bis zu:\",\n          \"infoItemOne\": \"10.000 Antworten pro Datenbank\",\n          \"infoItemTwo\": \"1.000 Antworten pro Arbeitsbereich\"\n        },\n        \"currentPlan\": {\n          \"bannerLabel\": \"Derzeitiger Plan\",\n          \"freeTitle\": \"Kostenfrei\",\n          \"proTitle\": \"Pro\",\n          \"teamTitle\": \"Team\",\n          \"freeInfo\": \"Perfekt für Einzelpersonen oder kleine Teams mit bis zu 2 Mitgliedern.\",\n          \"proInfo\": \"Perfekt für kleine und mittlere Teams mit bis zu 10 Mitgliedern.\",\n          \"teamInfo\": \"Perfekt für alle produktiven und gut organisierten Teams.\",\n          \"upgrade\": \"Vergleichen &\\n Upgraden\",\n          \"canceledInfo\": \"Dein Plan wurde gekündigt und du wirst am {} auf den kostenlosen Plan herabgestuft.\",\n          \"freeProOne\": \"Gemeinsamer Arbeitsbereich\",\n          \"freeProTwo\": \"Bis zu 2 Mitglieder (inkl. Eigentümer)\",\n          \"freeProThree\": \"Unbegrenzte Anzahl an Gästen (nur anzeigen)\",\n          \"freeProFour\": \"Speicher 5 GB\",\n          \"freeProFive\": \"30 Tage Änderungshistorie\",\n          \"freeConOne\": \"Gastmitarbeiter (Bearbeitungszugriff)\",\n          \"freeConTwo\": \"Unbegrenzter Speicherplatz\",\n          \"freeConThree\": \"6 Monate Änderungshistorie\",\n          \"professionalProOne\": \"Gemeinsamer Arbeitsbereich\",\n          \"professionalProTwo\": \"Unbegrenzte Mitgliederzahl\",\n          \"professionalProThree\": \"Unbegrenzte Anzahl an Gästen (nur anzeigen)\",\n          \"professionalProFour\": \"Unbegrenzter Speicherplatz\",\n          \"professionalProFive\": \"6 Monate Änderungshistorie\",\n          \"professionalConOne\": \"Unbegrenzte Anzahl an Gastmitarbeitern (Bearbeitungszugriff)\",\n          \"professionalConTwo\": \"Unbegrenzte KI-Antworten\",\n          \"professionalConThree\": \"1 Jahr Änderungshistorie\"\n        },\n        \"addons\": {\n          \"title\": \"Add-ons\",\n          \"addLabel\": \"Hinzufügen\",\n          \"activeLabel\": \"Hinzugefügt\",\n          \"aiMax\": {\n            \"title\": \"KI Max\",\n            \"description\": \"Schalte unbegrenzte KI frei\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"pro Benutzer und Monat\",\n            \"billingInfo\": \"jährliche Abrechnung oder {} bei monatlicher Abrechnung\"\n          },\n          \"aiOnDevice\": {\n            \"title\": \"KI On-Device\",\n            \"description\": \"KI offline auf deinem Gerät\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"pro Benutzer und Monat\",\n            \"recommend\": \"Empfohlen wird M1 oder neuer\",\n            \"billingInfo\": \"jährliche Abrechnung oder {} bei monatlicher Abrechnung\"\n          }\n        },\n        \"deal\": {\n          \"bannerLabel\": \"Neujahrsangebot!\",\n          \"title\": \"Erweiter dein Team!\",\n          \"info\": \"Upgraden und 10 % auf Pro- und Team-Pläne sparen! Steiger die Produktivität deines Arbeitsplatzes mit leistungsstarken neuen Funktionen, einschließlich @:appName KI.\",\n          \"viewPlans\": \"Pläne anzeigen\"\n        },\n        \"guestCollabToggle\": \"10 Gastmitarbeiter\",\n        \"storageUnlimited\": \"Unbegrenzter Speicherplatz mit deinem Pro-Plan\"\n      }\n    },\n    \"billingPage\": {\n      \"menuLabel\": \"Abrechnung\",\n      \"title\": \"Abrechnung\",\n      \"plan\": {\n        \"title\": \"Plan\",\n        \"freeLabel\": \"Kostenfrei\",\n        \"proLabel\": \"Pro\",\n        \"planButtonLabel\": \"Plan ändern\",\n        \"billingPeriod\": \"Abrechnungszeitraum\",\n        \"periodButtonLabel\": \"Zeitraum bearbeiten\"\n      },\n      \"paymentDetails\": {\n        \"title\": \"Zahlungsdetails\",\n        \"methodLabel\": \"Zahlungsmethode\",\n        \"methodButtonLabel\": \"Zahlungsmethode bearbeiten\"\n      },\n      \"addons\": {\n        \"title\": \"Add-ons\",\n        \"addLabel\": \"Hinzufügen\",\n        \"removeLabel\": \"Entfernen\",\n        \"renewLabel\": \"Erneuern\",\n        \"aiMax\": {\n          \"label\": \"KI Max\",\n          \"description\": \"Schalte unbegrenzte KI Antworten und erweiterte Modelle frei\",\n          \"activeDescription\": \"Nächste Rechnung fällig am {}\",\n          \"canceledDescription\": \"KI Max ist verfügbar bis {}\"\n        },\n        \"aiOnDevice\": {\n          \"label\": \"KI On-Device\",\n          \"description\": \"Schalte unbegrenzte KI Antworten offline auf deinem Gerät frei\",\n          \"activeDescription\": \"Nächste Rechnung fällig am {}\",\n          \"canceledDescription\": \"KI On-Device ist verfügbar bis {}\"\n        },\n        \"removeDialog\": {\n          \"title\": \"Entfernen {}\",\n          \"description\": \"Möchtest du den {plan} wirklich entfernen? Du verlierst dann sofort den Zugriff auf die Funktionen und Vorteile des {plan}.\"\n        }\n      },\n      \"currentPeriodBadge\": \"AKTUELL\",\n      \"changePeriod\": \"Zeitraum ändern\",\n      \"planPeriod\": \"{} Zeitraum\",\n      \"monthlyInterval\": \"Monatlich\",\n      \"monthlyPriceInfo\": \"pro Sitzplatz, monatliche Abrechnung\",\n      \"annualInterval\": \"Jährlich\",\n      \"annualPriceInfo\": \"pro Sitzplatz, jährliche Abrechnung\"\n    },\n    \"comparePlanDialog\": {\n      \"title\": \"Plan vergleichen & auswählen\",\n      \"planFeatures\": \"Plan\\nFeatures\",\n      \"current\": \"Aktuell\",\n      \"actions\": {\n        \"upgrade\": \"Upgrade\",\n        \"downgrade\": \"Downgrade\",\n        \"current\": \"Aktuell\",\n        \"downgradeDisabledTooltip\": \"Du wirst am Ende des Abrechnungszeitraums automatisch herabgestuft\"\n      },\n      \"freePlan\": {\n        \"title\": \"Kostenlos\",\n        \"description\": \"Für die Organisation jeder Ecke Ihres Lebens und Ihrer Arbeit.\",\n        \"price\": \"0€\",\n        \"priceInfo\": \"free forever\"\n      },\n      \"proPlan\": {\n        \"title\": \"Professionell\",\n        \"description\": \"Ein Ort für kleine Gruppen zum Planen und Organisieren.\",\n        \"price\": \"{} /Monat\",\n        \"priceInfo\": \"jährlich abgerechnet\"\n      },\n      \"planLabels\": {\n        \"itemOne\": \"Arbeitsbereiche\",\n        \"itemTwo\": \"Mitglieder\",\n        \"itemThree\": \"Gäste\",\n        \"itemFour\": \"Gäste\",\n        \"itemFive\": \"Speicher\",\n        \"itemSix\": \"Zusammenarbeit in Echtzeit\",\n        \"itemSeven\": \"Mobile App\",\n        \"itemFileUpload\": \"Datei-Uploads\",\n        \"customNamespace\": \"Benutzerdefinierter Namensraum\",\n        \"tooltipSix\": \"Lebenslang bedeutet, dass die Anzahl der Antworten nie zurückgesetzt wird\",\n        \"intelligentSearch\": \"Intelligente Suche\",\n        \"tooltipSeven\": \"Ermöglicht dir, einen Teil der URL für deinen Arbeitsbereich anzupassen\",\n        \"customNamespaceTooltip\": \"Benutzerdefinierte veröffentlichte Seiten-URL\",\n        \"tooltipThree\": \"Gäste haben nur Leserechte für die speziell freigegebenen Inhalte\",\n        \"tooltipFour\": \"Gäste werden als ein Sitzplatz abgerechnet\",\n        \"itemEight\": \"AI-Antworten\",\n        \"tooltipEight\": \"Lebenslang bedeutet, dass die Anzahl der Antworten nie zurückgesetzt wird.\"\n      },\n      \"freeLabels\": {\n        \"itemOne\": \"pro Arbeitsbereich berechnet\",\n        \"itemTwo\": \"3\",\n        \"itemThree\": \"5 GB\",\n        \"itemFour\": \"0\",\n        \"itemFive\": \"5 GB\",\n        \"itemSix\": \"10 Lebenszeiten\",\n        \"itemSeven\": \"ja\",\n        \"itemFileUpload\": \"Bis zu 7 MB\",\n        \"intelligentSearch\": \"Intelligente Suche\",\n        \"itemEight\": \"1.000 lebenslang\"\n      },\n      \"proLabels\": {\n        \"itemOne\": \"pro Arbeitsbereich berechnet\",\n        \"itemTwo\": \"bis zu 10\",\n        \"itemThree\": \"unbegrenzt\",\n        \"itemFour\": \"10 Gäste werden als ein Sitzplatz berechnet\",\n        \"itemFive\": \"unbegrenzt\",\n        \"itemSix\": \"ja\",\n        \"itemSeven\": \"ja\",\n        \"itemFileUpload\": \"Unbegrenzt\",\n        \"intelligentSearch\": \"Intelligente Suche\",\n        \"itemEight\": \"10.000 monatlich\"\n      },\n      \"paymentSuccess\": {\n        \"title\": \"Du bist jetzt im {} Plan!\",\n        \"description\": \"Deine Zahlung wurde erfolgreich verarbeitet und dein Plan wurde auf @:appName {} aktualisiert. Du kannst die Details deines Plans auf der Seite \\\"Plan\\\" einsehen\"\n      },\n      \"downgradeDialog\": {\n        \"title\": \"Bist du sicher, dass du deinen Plan herabstufen willst?\",\n        \"description\": \"Wenn du deinen Plan herabstufst, kehrst du zum kostenfreien Plan zurück. Mitglieder können den Zugang zu Arbeitsbereichen verlieren und du musst möglicherweise Speicherplatz freigeben, um die Speichergrenzen des kostenfreien Tarifs einzuhalten.\",\n        \"downgradeLabel\": \"Downgrade Plan\"\n      }\n    },\n    \"cancelSurveyDialog\": {\n      \"title\": \"Schade dich gehen zu sehen\",\n      \"description\": \"Wir bedauern, dass du gehst. Wir würden uns über dein Feedback freuen, das uns hilft @:appName zu verbessern. Bitte nehme dir einen Moment Zeit, um ein paar Fragen zu beantworten.\",\n      \"commonOther\": \"Andere\",\n      \"otherHint\": \"Schreibe deine Antwort hier\",\n      \"questionOne\": {\n        \"question\": \"Was hat dich dazu veranlasst, dein @:appName Pro-Abonnement zu kündigen?\",\n        \"answerOne\": \"Kosten zu hoch\",\n        \"answerTwo\": \"Die Funktionen entsprachen nicht den Erwartungen\",\n        \"answerThree\": \"Habe eine bessere Alternative gefunden\",\n        \"answerFour\": \"Habe es nicht oft genug genutzt, um die Kosten zu rechtfertigen\",\n        \"answerFive\": \"Serviceproblem oder technische Schwierigkeiten\"\n      },\n      \"questionTwo\": {\n        \"question\": \"Wie wahrscheinlich ist es, dass du in Zukunft ein erneutes Abonnement von @:appName Pro in Betracht ziehst?\",\n        \"answerOne\": \"Sehr wahrscheinlich\",\n        \"answerTwo\": \"Ziemlich wahrscheinlich\",\n        \"answerThree\": \"Nicht sicher\",\n        \"answerFour\": \"Unwahrscheinlich\",\n        \"answerFive\": \"Sehr unwahrscheinlich\"\n      },\n      \"questionThree\": {\n        \"question\": \"Welche Pro-Funktion hast du während deines Abonnements am meisten geschätzt?\",\n        \"answerOne\": \"Zusammenarbeit mehrerer Benutzer\",\n        \"answerTwo\": \"Längerer Versionsverlauf\",\n        \"answerThree\": \"Unbegrenzte KI-Antworten\",\n        \"answerFour\": \"Zugriff auf lokale KI-Modelle\"\n      },\n      \"questionFour\": {\n        \"question\": \"Wie würdest du deine allgemeine Erfahrung mit @:appName beschreiben?\",\n        \"answerOne\": \"Großartig\",\n        \"answerTwo\": \"Gut\",\n        \"answerThree\": \"Durchschnitt\",\n        \"answerFour\": \"Unterdurchschnittlich\",\n        \"answerFive\": \"Nicht zufrieden\"\n      }\n    },\n    \"common\": {\n      \"uploadingFile\": \"Datei wird hochgeladen. Bitte beenden Sie die App nicht.\",\n      \"uploadNotionSuccess\": \"Ihre Notion-ZIP-Datei wurde erfolgreich hochgeladen. Sobald der Import abgeschlossen ist, erhalten Sie eine Bestätigungs-E-Mail\",\n      \"reset\": \"Zurücksetzen\"\n    },\n    \"menu\": {\n      \"appearance\": \"Oberfläche\",\n      \"language\": \"Sprache\",\n      \"user\": \"Nutzer\",\n      \"files\": \"Dateien\",\n      \"notifications\": \"Benachrichtigungen\",\n      \"open\": \"Einstellungen öffnen\",\n      \"logout\": \"Abmelden\",\n      \"logoutPrompt\": \"Willst du dich wirklich abmelden?\",\n      \"selfEncryptionLogoutPrompt\": \"Willst du dich wirklich Abmelden? Bitte stelle sicher, dass der Encryption Secret Code kopiert wurde.\",\n      \"syncSetting\": \"Synchronisations-Einstellung\",\n      \"cloudSettings\": \"Cloud Einstellungen\",\n      \"enableSync\": \"Synchronisation aktivieren\",\n      \"enableSyncLog\": \"Synchronisation der Protokolldateien aktivieren\",\n      \"enableSyncLogWarning\": \"Vielen Dank für Ihre Hilfe bei der Diagnose von Synchronisierungsproblemen. Dadurch werden Ihre Dokumentänderungen in einer lokalen Datei protokolliert. Bitte beenden Sie die App und öffnen Sie sie erneut, nachdem Sie sie aktiviert haben.\",\n      \"enableEncrypt\": \"Daten verschlüsseln\",\n      \"cloudURL\": \"Basis URL\",\n      \"invalidCloudURLScheme\": \"Ungültiges Format\",\n      \"cloudServerType\": \"Cloud Server\",\n      \"cloudServerTypeTip\": \"Bitte beachte, dass der aktuelle Benutzer ausgeloggt wird beim wechsel des Cloud-Servers\",\n      \"cloudLocal\": \"Lokal\",\n      \"cloudAppFlowy\": \"@:appName Cloud Beta\",\n      \"cloudAppFlowySelfHost\": \"@:appName Cloud Self-hosted\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"Die Cloud-URL darf nicht leer sein\",\n      \"clickToCopy\": \"Klicken, um zu kopieren\",\n      \"selfHostStart\": \"Falls du keinen Server hast, konsultiere bitte\",\n      \"selfHostContent\": \"Dokument\",\n      \"selfHostEnd\": \"um einen einen eigenen Server aufzusetzen\",\n      \"pleaseInputValidURL\": \"Bitte geben Sie eine gültige URL ein\",\n      \"changeUrl\": \"Ändern Sie die selbst gehostete URL in {}\",\n      \"cloudURLHint\": \"Eingabe der Basis- URL Ihres Servers\",\n      \"cloudWSURL\": \"Websocket URL\",\n      \"cloudWSURLHint\": \"Eingbe der Websocket Adresse Ihres Servers\",\n      \"restartApp\": \"Neustart\",\n      \"restartAppTip\": \"Programm neustarten, um die Änderungen zu übernehmen. Bitte beachten, dass der aktuelle Account eventuell ausgeloggt wird.\",\n      \"changeServerTip\": \"Nach dem Wechsel des Servers muss auf die Schaltfläche „Neustart“ geklickt werden, damit die Änderungen wirksam werden\",\n      \"enableEncryptPrompt\": \"Verschlüsselung aktivieren, um deine Daten mit dem Secret Key zu verschlüsseln. Verwahre den Schlüssel sicher! \\nEinmal aktiviert kann es nicht mehr rückgängig gemacht werden.\\nFalls der Schlüssel verloren geht sind die Daten unwiderbringlich verloren.\\nKlicken, um zu kopieren.\",\n      \"inputEncryptPrompt\": \"Bitte den Encryption Secret Code eingeben\",\n      \"clickToCopySecret\": \"Klicken, um den Secret Code zu kopieren\",\n      \"configServerSetting\": \"Deine Servereinstellungen anpassen\",\n      \"configServerGuide\": \"`Schnellstart/Quick Start` auswählen, dann zu den `Einstellungen/Settings` wechseln und dann die Cloud-Einstellungen \\\"Cloud Settings\\\" auswählen, um deinen Server zu konfigurieren.\",\n      \"inputTextFieldHint\": \"Dein Secret-Code\",\n      \"historicalUserList\": \"Anmeldeverlauf\",\n      \"historicalUserListTooltip\": \"Diese Liste zeigt deine anonymen Accounts. Du kannst einen Account anklicken, um mehr Informationen zu sehen.\\nAnonyme Accounts werden über den 'Erste Schritte' Button erstellt.\",\n      \"openHistoricalUser\": \"Klicken, um einen anonymen Account zu öffnen\",\n      \"customPathPrompt\": \"Den @:appName Daten-Ordner in einem mit der Cloud synchronisierten Ordner (z.B. Google Drive) zu speichern, könnte Risiken bergen. Falls die Datenbank innerhalb dieses Ordners gleichzeitig von mehreren Orten zugegriffen oder verändert wird könnte dies zu Synchronisationskonflikten und potentiellen Daten-Beschädigungen führen\",\n      \"importAppFlowyData\": \"Daten von einem externen @:appName Ordner importieren.\",\n      \"importingAppFlowyDataTip\": \"Der Datenimport läuft. Bitte die App nicht schließen oder in den Hintergrund setzten\",\n      \"importAppFlowyDataDescription\": \"Daten von einem externen @:appName Ordner kopieren und in den aktuellen @:appName Datenordner importieren.\",\n      \"importSuccess\": \"Der @:appName Dateienordner wurde erfolgreich importiert\",\n      \"importFailed\": \"Der @:appName Dateienordner-Import ist fehlgeschlagen\",\n      \"importGuide\": \"Für weitere Details, bitte das verlinkte Dokument prüfen\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Benachrichtigungen aktivieren\",\n        \"hint\": \"Wenn diese Funktion ausgeschaltet ist, werden keine lokalen Benachrichtigungen mehr angezeigt.\"\n      },\n      \"showNotificationsIcon\": {\n        \"label\": \"Benachrichtigungssymbol anzeigen\",\n        \"hint\": \"Deaktiviere diese Option, um das Benachrichtigungssymbol in der Seitenleiste auszublenden.\"\n      },\n      \"archiveNotifications\": {\n        \"allSuccess\": \"Alle Benachrichtigungen erfolgreich archiviert\",\n        \"success\": \"Benachrichtigung erfolgreich archiviert\"\n      },\n      \"markAsReadNotifications\": {\n        \"allSuccess\": \"Alle erfolgreich als gelesen markiert\",\n        \"success\": \"Erfolgreich als gelesen markiert\"\n      },\n      \"action\": {\n        \"markAsRead\": \"Als gelesen markieren\",\n        \"multipleChoice\": \"Mehrfachauswahl\",\n        \"archive\": \"Archiv\"\n      },\n      \"settings\": {\n        \"settings\": \"Einstellungen\",\n        \"markAllAsRead\": \"Alles als gelesen markieren\",\n        \"archiveAll\": \"Alles archivieren\"\n      },\n      \"emptyInbox\": {\n        \"title\": \"Noch keine Benachrichtigungen\",\n        \"description\": \"Du wirst hier über @Erwähnungen benachrichtigt\"\n      },\n      \"emptyUnread\": {\n        \"title\": \"Keine ungelesenen Benachrichtigungen\",\n        \"description\": \"Du bist auf dem Laufenden!\"\n      },\n      \"emptyArchived\": {\n        \"title\": \"Keine archivierten Benachrichtigungen\",\n        \"description\": \"Du hast noch keine Benachrichtigungen archiviert\"\n      },\n      \"tabs\": {\n        \"inbox\": \"Posteingang\",\n        \"unread\": \"Ungelesen\",\n        \"archived\": \"Archiviert\"\n      },\n      \"refreshSuccess\": \"Benachrichtigungen erfolgreich aktualisiert\",\n      \"titles\": {\n        \"notifications\": \"Benachrichtigungen\",\n        \"reminder\": \"Erinnerung\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"Zurücksetzen\",\n      \"fontFamily\": {\n        \"label\": \"Schriftfamilie\",\n        \"search\": \"Suchen\",\n        \"defaultFont\": \"Standardschriftart\"\n      },\n      \"themeMode\": {\n        \"label\": \"Design\",\n        \"light\": \"Helles Design\",\n        \"dark\": \"Dunkles Design\",\n        \"system\": \"Wie Betriebssystem\"\n      },\n      \"fontScaleFactor\": \"Schriftgröße\",\n      \"documentSettings\": {\n        \"cursorColor\": \"Cursor-Farbe\",\n        \"selectionColor\": \"Auswahl-Farbe\",\n        \"width\": \"Dokumentbreite\",\n        \"changeWidth\": \"Ändern\",\n        \"pickColor\": \"Wähle eine Farbe\",\n        \"colorShade\": \"Farbschattierung\",\n        \"opacity\": \"Opazität\",\n        \"hexEmptyError\": \"Hex-Farbe darf nicht leer sein\",\n        \"hexLengthError\": \"Hex-Wert muss 6 Zeichen lang sein\",\n        \"hexInvalidError\": \"Ungültiger Hex-Wert\",\n        \"opacityEmptyError\": \"Transparenz darf nicht leer sein\",\n        \"opacityRangeError\": \"Transparenz ist ein Wert zwischen 1 und 100\",\n        \"app\": \"App\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"Verwenden\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Layoutrichtung\",\n        \"hint\": \"Steuere den Umlauf der Inhalte auf deinem Bildschirm: Von Links nach Rechts oder von Rechts nach Links.\",\n        \"ltr\": \"Links nach Rechts\",\n        \"rtl\": \"Rechts nach Links\"\n      },\n      \"textDirection\": {\n        \"label\": \"Textrichtung\",\n        \"hint\": \"Wie soll der Text laufen: von Links nach Rechts oder von Rechts nach Links?\",\n        \"ltr\": \"Links nach Rechts\",\n        \"rtl\": \"Rechts nach Links\",\n        \"auto\": \"AUTO\",\n        \"fallback\": \"Wie Layoutrichtung\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Hochladen\",\n        \"uploadTheme\": \"Theme hochladen\",\n        \"description\": \"Lade eigenes @:appName-Theme über die untere Schaltfläche hoch.\",\n        \"loading\": \"Bitte warte einen Moment . . .\\nWir validieren gerade dein Theme und laden es hoch.\",\n        \"uploadSuccess\": \"Das Theme wurde erfolgreich hochgeladen\",\n        \"deletionFailure\": \"Das Theme konnte nicht gelöscht werden. Versuche, es manuell zu löschen.\",\n        \"filePickerDialogTitle\": \"Wähle eine .flowy_plugin-Datei\",\n        \"urlUploadFailure\": \"URL konnte nicht geöffnet werden: {}\",\n        \"failure\": \"Das hochgeladene Theme hat ein ungültiges Format.\"\n      },\n      \"theme\": \"Theme\",\n      \"builtInsLabel\": \"Integrierte Theme\",\n      \"pluginsLabel\": \"Plugins\",\n      \"dateFormat\": {\n        \"label\": \"Datumsformat\",\n        \"local\": \"Lokal\",\n        \"us\": \"US\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"Leserlich\",\n        \"dmy\": \"TT/MM/JJJJ\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Zeitformat\",\n        \"twelveHour\": \"12 Stunden\",\n        \"twentyFourHour\": \"24 Stunden\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"Zeige Bennenungsfenster, wenn eine neue Seite erstellt wird\",\n      \"enableRTLToolbarItems\": \"RTL-Symbolleistenelemente aktivieren\",\n      \"members\": {\n        \"title\": \"Mitglieder-Einstellungen\",\n        \"inviteMembers\": \"Mitglieder einladen\",\n        \"inviteHint\": \"Per E-Mail einladen\",\n        \"sendInvite\": \"Einladung senden\",\n        \"copyInviteLink\": \"Kopiere Einladungslink\",\n        \"label\": \"Mitglieder\",\n        \"user\": \"Nutzer\",\n        \"role\": \"Rolle\",\n        \"removeFromWorkspace\": \"Vom Arbeitsbereich entfernen\",\n        \"removeFromWorkspaceSuccess\": \"Erfolgreich aus dem Arbeitsbereich entfernt\",\n        \"removeFromWorkspaceFailed\": \"Entfernen aus Arbeitsbereich fehlgeschlagen\",\n        \"owner\": \"Besitzer\",\n        \"guest\": \"Gast\",\n        \"member\": \"Mitglied\",\n        \"memberHintText\": \"Ein Mitglied kann Seiten lesen, kommentieren und bearbeiten, sowie Einladungen an Mitglieder & Gäste versenden.\",\n        \"guestHintText\": \"Ein Gast kann mit Erlaubnis bestimmte Seiten lesen, reagieren, kommentieren und bearbeiten.\",\n        \"emailInvalidError\": \"Ungültige E-Mail. Bitte prüfe die E-Mail und versuche es erneut.\",\n        \"emailSent\": \"E-Mail gesendet. Prüfe den Posteingang.\",\n        \"members\": \"Mitglieder\",\n        \"membersCount\": {\n          \"zero\": \"{} Mitglieder\",\n          \"one\": \"{} Mitglied\",\n          \"other\": \"{} Mitglieder\"\n        },\n        \"inviteFailedDialogTitle\": \"Einladung konnte nicht gesendet werden\",\n        \"inviteFailedMemberLimit\": \"Das Mitgliederlimit wurde erreicht. Bitte führe ein Upgrade durch, um weitere Mitglieder einzuladen.\",\n        \"inviteFailedMemberLimitMobile\": \"Ihr Arbeitsbereich hat das Mitgliederlimit erreicht.\",\n        \"memberLimitExceeded\": \"Du hast die maximal zulässige Mitgliederzahl für dein Benutzerkonto erreicht. Benötigst du weitere Mitglieder, um deine Arbeit fortsetzen zu können, erstelle auf Github bitte eine entsprechende Anfrage.\",\n        \"memberLimitExceededUpgrade\": \"upgrade\",\n        \"memberLimitExceededPro\": \"Mitgliederlimit erreicht. Wenn du mehr Mitglieder benötigst, kontaktiere\",\n        \"memberLimitExceededProContact\": \"support@appflowy.io\",\n        \"failedToAddMember\": \"Mitglied konnte nicht hinzugefügt werden!\",\n        \"addMemberSuccess\": \"Mitglied erfolgreich hinzugefügt\",\n        \"removeMember\": \"Mitglied entfernen\",\n        \"areYouSureToRemoveMember\": \"Möchtest du dieses Mitglied wirklich entfernen?\",\n        \"inviteMemberSuccess\": \"Die Einladung wurde erfolgreich versendet\",\n        \"failedToInviteMember\": \"Das Einladen des Mitglieds ist fehlgeschlagen\",\n        \"workspaceMembersError\": \"Hoppla, da ist etwas schiefgelaufen\",\n        \"workspaceMembersErrorDescription\": \"Wir konnten die Mitgliederliste derzeit nicht laden. Bitte versuchen Sie es später noch einmal.\"\n      }\n    },\n    \"files\": {\n      \"copy\": \"Kopieren\",\n      \"defaultLocation\": \"@:appName Datenverzeichnis\",\n      \"exportData\": \"Daten exportieren\",\n      \"doubleTapToCopy\": \"Zweimal tippen, um den Pfad zu kopieren\",\n      \"restoreLocation\": \"@:appName-Standardpfad wiederherstellen\",\n      \"customizeLocation\": \"Einen anderen Ordner öffnen\",\n      \"restartApp\": \"Bitte starte die App neu, damit die Änderungen wirksam werden.\",\n      \"exportDatabase\": \"Datenbank exportieren\",\n      \"selectFiles\": \"Dateien auswählen, die exportiert werden sollen\",\n      \"selectAll\": \"Alle auswählen\",\n      \"deselectAll\": \"Alle abwählen\",\n      \"createNewFolder\": \"Einen neuen Ordner erstellen\",\n      \"createNewFolderDesc\": \"Wo sollen die Daten gespeichert werden?\",\n      \"defineWhereYourDataIsStored\": \"Wo sind die Daten gespeichert?\",\n      \"open\": \"Offen\",\n      \"openFolder\": \"Einen vorhandenen Ordner öffnen\",\n      \"openFolderDesc\": \"Öffnen und speichern im vorhandenen @:appName-Ordner\",\n      \"folderHintText\": \"Ordnernamen\",\n      \"location\": \"Ein neuen Ordner erstellen\",\n      \"locationDesc\": \"Einen Namen für den @:appName Datenordner festlegen\",\n      \"browser\": \"Durchsuchen\",\n      \"create\": \"Erstellen\",\n      \"set\": \"Festlegen\",\n      \"folderPath\": \"Pfad zum Speichern des Ordners\",\n      \"locationCannotBeEmpty\": \"Der Pfad darf nicht leer sein\",\n      \"pathCopiedSnackbar\": \"Dateispeicherpfad in Zwischenablage kopiert!\",\n      \"changeLocationTooltips\": \"Datenverzeichnis ändern\",\n      \"change\": \"Ändern\",\n      \"openLocationTooltips\": \"Win anderes Datenverzeichnis öffnen\",\n      \"openCurrentDataFolder\": \"Aktuelles Datenverzeichnis öffnen\",\n      \"recoverLocationTooltips\": \"Zurücksetzen auf das Standarddatenverzeichnis von @:appName\",\n      \"exportFileSuccess\": \"Datei erfolgreich exportiert!\",\n      \"exportFileFail\": \"Datei-Export fehlgeschlagen!\",\n      \"export\": \"Export\",\n      \"clearCache\": \"Cache leeren\",\n      \"clearCacheDesc\": \"Wenn Probleme auftreten, dass Bilder nicht geladen werden oder Schriftarten nicht richtig angezeigt werden, versuche, den Cache zu leeren. Durch diese Aktion werden die Benutzerdaten nicht entfernt.\",\n      \"areYouSureToClearCache\": \"Möchtest du den Cache wirklich leeren?\",\n      \"clearCacheSuccess\": \"Cache erfolgreich geleert!\"\n    },\n    \"user\": {\n      \"name\": \"Name\",\n      \"email\": \"E-Mail\",\n      \"tooltipSelectIcon\": \"Symbol auswählen\",\n      \"selectAnIcon\": \"Ein Symbol auswählen\",\n      \"pleaseInputYourOpenAIKey\": \"Bitte gebe den AI-Schlüssel ein\",\n      \"clickToLogout\": \"Klicken, um den aktuellen Nutzer auszulogen\",\n      \"pleaseInputYourStabilityAIKey\": \"Bitte gebe den Stability AI Schlüssel ein\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"Persönliche Informationen\",\n      \"username\": \"Nutzername\",\n      \"usernameEmptyError\": \"Der Nutzername darf nicht leer sein\",\n      \"about\": \"Über\",\n      \"pushNotifications\": \"Push Benachrichtigungen\",\n      \"support\": \"Support\",\n      \"joinDiscord\": \"Komm zu uns auf Discord\",\n      \"privacyPolicy\": \"Datenschutz\",\n      \"userAgreement\": \"Nutzungsbedingungen\",\n      \"termsAndConditions\": \"Geschäftsbedingungen\",\n      \"userprofileError\": \"Das Nutzerprofil konnte nicht geladen werden\",\n      \"userprofileErrorDescription\": \"Bitte abmelden und wieder anmelden, um zu prüfen ob das Problem weiterhin bestehen bleibt.\",\n      \"selectLayout\": \"Layout auswählen\",\n      \"selectStartingDay\": \"Ersten Tag auswählen\",\n      \"version\": \"Version\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"Tastenkürzel\",\n      \"command\": \"Befehl\",\n      \"keyBinding\": \"Tastenkombination\",\n      \"addNewCommand\": \"Neuen Befehl hinzufügen\",\n      \"updateShortcutStep\": \"Die gewünschte Tastenkombination drücken und mit ENTER bestätigen\",\n      \"shortcutIsAlreadyUsed\": \"Diese Verknüpfung wird bereits verwendet für: {conflict}\",\n      \"resetToDefault\": \"Zurücksetzen\",\n      \"couldNotLoadErrorMsg\": \"Tastenkürzel konnten nicht geladen werden. Bitte nochmal versuchen.\",\n      \"couldNotSaveErrorMsg\": \"Tastenkürzel konnten nicht gespeichert werden. Bitte nochmal versuchen.\",\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"Einen neuen Absatz neben dem Codeblock einfügen\",\n        \"codeBlockIndentLines\": \"Zwei Leerzeichen am Zeilenanfang im Codeblock einfügen\",\n        \"codeBlockOutdentLines\": \"Zwei Leerzeichen am Zeilenanfang im Codeblock löschen\",\n        \"codeBlockAddTwoSpaces\": \"Zwei Leerzeichen am Zeilenanfang im Codeblock einfügen\",\n        \"codeBlockSelectAll\": \"Gesamten Inhalt innerhalb eines Codeblocks auswählen\",\n        \"codeBlockPasteText\": \"Text in Codeblock einfügen\",\n        \"textAlignLeft\": \"Text linksbündig ausrichten\",\n        \"textAlignCenter\": \"Text zentriert ausrichten\",\n        \"textAlignRight\": \"Text rechtsbündig ausrichten\"\n      }\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Möchtest du diese Ansicht wirklich löschen?\",\n    \"createView\": \"Neu\",\n    \"title\": {\n      \"placeholder\": \"Unbenannt\"\n    },\n    \"settings\": {\n      \"filter\": \"Filter\",\n      \"sort\": \"Sortieren\",\n      \"sortBy\": \"Sortiere nach\",\n      \"properties\": \"Eigenschaften\",\n      \"reorderPropertiesTooltip\": \"Ziehen, um die Eigenschaften neu anzuordnen\",\n      \"group\": \"Gruppe\",\n      \"addFilter\": \"Filter hinzufügen\",\n      \"deleteFilter\": \"Filter löschen\",\n      \"filterBy\": \"Filtern nach...\",\n      \"typeAValue\": \"Einen Wert eingeben...\",\n      \"layout\": \"Layout\",\n      \"databaseLayout\": \"Layout\",\n      \"viewList\": {\n        \"zero\": \"0 Aufrufe\",\n        \"one\": \"{count} Aufruf\",\n        \"other\": \"{count} Aufrufe\"\n      },\n      \"editView\": \"Ansicht editieren\",\n      \"boardSettings\": \"Board-Einstellungen\",\n      \"calendarSettings\": \"Kalender-Einstellungen\",\n      \"createView\": \"New Ansicht\",\n      \"duplicateView\": \"Ansicht duplizieren\",\n      \"deleteView\": \"Anslicht löschen\",\n      \"numberOfVisibleFields\": \"{} angezeigt\"\n    },\n    \"filter\": {\n      \"empty\": \"Keine aktiven Filter\",\n      \"addFilter\": \"Filter hinzufügen\",\n      \"cannotFindCreatableField\": \"Es wurde kein geeignetes Feld zum Filtern gefunden.\",\n      \"conditon\": \"Bedingung\",\n      \"where\": \"Wo\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Enthält\",\n      \"doesNotContain\": \"Beinhaltet nicht\",\n      \"endsWith\": \"Endet mit\",\n      \"startWith\": \"Beginnt mit\",\n      \"is\": \"Ist\",\n      \"isNot\": \"Ist nicht\",\n      \"isEmpty\": \"Ist leer\",\n      \"isNotEmpty\": \"Ist nicht leer\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Nicht\",\n        \"startWith\": \"Beginnt mit\",\n        \"endWith\": \"Endet mit\",\n        \"isEmpty\": \"ist leer\",\n        \"isNotEmpty\": \"ist nicht leer\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Geprüft\",\n      \"isUnchecked\": \"Ungeprüft\",\n      \"choicechipPrefix\": {\n        \"is\": \"Ist\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"ist komplett\",\n      \"isIncomplted\": \"ist unvollständig\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"Ist\",\n      \"isNot\": \"Ist nicht\",\n      \"contains\": \"Enthält\",\n      \"doesNotContain\": \"Beinhaltet nicht\",\n      \"isEmpty\": \"Ist leer\",\n      \"isNotEmpty\": \"Ist nicht leer\"\n    },\n    \"dateFilter\": {\n      \"is\": \"Ist\",\n      \"before\": \"Ist bevor\",\n      \"after\": \"Ist nach\",\n      \"onOrBefore\": \"Ist am oder vor\",\n      \"onOrAfter\": \"Ist am oder nach\",\n      \"between\": \"Ist zwischen\",\n      \"empty\": \"Ist leer\",\n      \"notEmpty\": \"Ist nicht leer\",\n      \"startDate\": \"Startdatum\",\n      \"endDate\": \"Enddatum\",\n      \"choicechipPrefix\": {\n        \"before\": \"Vorher\",\n        \"after\": \"Danach\",\n        \"between\": \"Zwischen\",\n        \"onOrBefore\": \"Am oder davor\",\n        \"onOrAfter\": \"Während oder danach\",\n        \"isEmpty\": \"leer\",\n        \"isNotEmpty\": \"nicht leer\"\n      }\n    },\n    \"numberFilter\": {\n      \"equal\": \"gleich\",\n      \"notEqual\": \"ungleich\",\n      \"lessThan\": \"weniger als\",\n      \"greaterThan\": \"größer als\",\n      \"lessThanOrEqualTo\": \"weniger als oder gleich wie\",\n      \"greaterThanOrEqualTo\": \"größer als oder gleich wie\",\n      \"isEmpty\": \"leer\",\n      \"isNotEmpty\": \"nicht leer\"\n    },\n    \"field\": {\n      \"label\": \"Eigenschaft\",\n      \"hide\": \"Verstecken\",\n      \"show\": \"Anzeigen\",\n      \"insertLeft\": \"Links einfügen\",\n      \"insertRight\": \"Rechts einfügen\",\n      \"duplicate\": \"Duplikat\",\n      \"delete\": \"Löschen\",\n      \"wrapCellContent\": \"Zeilenumbruch\",\n      \"clear\": \" Zelleninhalte löschen\",\n      \"switchPrimaryFieldTooltip\": \"Feldtyp des Primärfelds kann nicht geändert werden\",\n      \"textFieldName\": \"Text\",\n      \"checkboxFieldName\": \"Kontrollkästchen\",\n      \"dateFieldName\": \"Datum\",\n      \"updatedAtFieldName\": \"Letzte Änderungszeit\",\n      \"createdAtFieldName\": \"Erstellungsdatum\",\n      \"numberFieldName\": \"Zahlen\",\n      \"singleSelectFieldName\": \"Wählen\",\n      \"multiSelectFieldName\": \"Mehrfachauswahl\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Checkliste\",\n      \"relationFieldName\": \"Beziehung\",\n      \"summaryFieldName\": \"KI-Zusammenfassung\",\n      \"timeFieldName\": \"Zeit\",\n      \"mediaFieldName\": \"Dateien und Medien\",\n      \"translateFieldName\": \"AI-Übersetzen\",\n      \"translateTo\": \"Übersetzen in\",\n      \"numberFormat\": \"Zahlenformat\",\n      \"dateFormat\": \"Datumsformat\",\n      \"includeTime\": \"Zeitangabe\",\n      \"isRange\": \"Enddatum\",\n      \"dateFormatFriendly\": \"Monat Tag, Jahr\",\n      \"dateFormatISO\": \"Jahr-Monat-Tag\",\n      \"dateFormatLocal\": \"Monat/Tag/Jahr\",\n      \"dateFormatUS\": \"Jahr/Monat/Tag\",\n      \"dateFormatDayMonthYear\": \"Tag/Monat/Jahr\",\n      \"timeFormat\": \"Zeitformat\",\n      \"invalidTimeFormat\": \"Ungültiges Format\",\n      \"timeFormatTwelveHour\": \"12 Stunden\",\n      \"timeFormatTwentyFourHour\": \"24 Stunden\",\n      \"clearDate\": \"Datum löschen\",\n      \"dateTime\": \"Datum und Zeit\",\n      \"startDateTime\": \"Beginn Datum und Zeit\",\n      \"endDateTime\": \"Ende Datum und Zeit\",\n      \"failedToLoadDate\": \"Laden des Datums ist fehlgeschlagen\",\n      \"selectTime\": \"Auswahl Zeit\",\n      \"selectDate\": \"Auswahl Datum\",\n      \"visibility\": \"Sichtbarkeit\",\n      \"propertyType\": \"Eigenschaftstyp\",\n      \"addSelectOption\": \"Füge Option hinzu\",\n      \"typeANewOption\": \"Eine neue Option eingeben\",\n      \"optionTitle\": \"Optionen\",\n      \"addOption\": \"Option hinzufügen\",\n      \"editProperty\": \"Eigenschaft bearbeiten\",\n      \"newProperty\": \"Neue Eigenschaft\",\n      \"openRowDocument\": \"Als Seite öffnen\",\n      \"deleteFieldPromptMessage\": \"Sicher? Diese Eigenschaft wird gelöscht\",\n      \"clearFieldPromptMessage\": \"Bist du dir sicher? Alle Zelleninhalte in dieser Spalte werden gelöscht!\",\n      \"newColumn\": \"Neue Spalte\",\n      \"format\": \"Format\",\n      \"reminderOnDateTooltip\": \"Diese Zeile hat eine terminierte Erinnerung\",\n      \"optionAlreadyExist\": \"Einstellung existiert bereits\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Ein neues Feld hinzufügen\",\n      \"fieldDragElementTooltip\": \"Klicken, um das Menü zu öffnen\",\n      \"showHiddenFields\": {\n        \"one\": \"Zeige {count} verstecktes Feld\",\n        \"many\": \"Zeige {count} versteckte Felder\",\n        \"other\": \"Zeige {count} versteckte Felder\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"Blende {count} verstecktes Feld aus\",\n        \"many\": \"Blende {count} versteckte Felder aus\",\n        \"other\": \"Blende {count} versteckte Felder aus\"\n      },\n      \"openAsFullPage\": \"Als ganze Seite öffnen\",\n      \"moreRowActions\": \"Weitere Zeilenaktionen\"\n    },\n    \"sort\": {\n      \"ascending\": \"Aufsteigend\",\n      \"descending\": \"Absteigend\",\n      \"by\": \"von\",\n      \"empty\": \"Keine Sortierung\",\n      \"cannotFindCreatableField\": \"Es konnte kein geeignetes Feld zum Sortieren gefunden werden\",\n      \"deleteAllSorts\": \"Alle Sortierungen entfernen\",\n      \"addSort\": \"Sortierung hinzufügen\",\n      \"removeSorting\": \"Möchtest du die Sortierung entfernen?\",\n      \"fieldInUse\": \"Du sortierst bereits nach diesem Feld\"\n    },\n    \"row\": {\n      \"label\": \"Reihe\",\n      \"duplicate\": \"Duplikat\",\n      \"delete\": \"Löschen\",\n      \"titlePlaceholder\": \"Unbenannt\",\n      \"textPlaceholder\": \"Leer\",\n      \"copyProperty\": \"Eigenschaft in die Zwischenablage kopiert\",\n      \"count\": \"Zählen\",\n      \"newRow\": \"Neue Zeile\",\n      \"loadMore\": \"Mehr laden\",\n      \"action\": \"Aktion\",\n      \"add\": \"Klicken, um unten hinzuzufügen\",\n      \"drag\": \"Ziehen, um zu verschieben\",\n      \"deleteRowPrompt\": \"Bist du sicher, dass du diese Zeile löschen willst? Diese Aktion kann nicht rückgängig gemacht werden\",\n      \"deleteCardPrompt\": \"Bist du sicher, dass du diese Karte löschen willst? Diese Aktion kann nicht rückgängig gemacht werden\",\n      \"dragAndClick\": \"Ziehen, um zu verschieben. Klicke, um das Menü zu öffnen\",\n      \"insertRecordAbove\": \"Füge Datensatz oben ein\",\n      \"insertRecordBelow\": \"Füge Datensatz unten ein\",\n      \"noContent\": \"Kein Inhalt\",\n      \"reorderRowDescription\": \"Zeile neu anordnen\",\n      \"createRowAboveDescription\": \"Erstelle oben eine Zeile\",\n      \"createRowBelowDescription\": \"Unten eine Zeile einfügen\"\n    },\n    \"selectOption\": {\n      \"create\": \"Erstellen\",\n      \"purpleColor\": \"Lila\",\n      \"pinkColor\": \"Pink\",\n      \"lightPinkColor\": \"Hell-Pink\",\n      \"orangeColor\": \"Orange\",\n      \"yellowColor\": \"Gelb\",\n      \"limeColor\": \"Kalk\",\n      \"greenColor\": \"Grün\",\n      \"aquaColor\": \"Aqua\",\n      \"blueColor\": \"Blau\",\n      \"deleteTag\": \"Tag löschen\",\n      \"colorPanelTitle\": \"Farbe\",\n      \"panelTitle\": \"Eine Option auswählen oder erstellen\",\n      \"searchOption\": \"Nach einer Option suchen\",\n      \"searchOrCreateOption\": \"Eine Option suchen oder erstellen...\",\n      \"createNew\": \"Neu erstellen\",\n      \"orSelectOne\": \"Oder eine Option auswählen\",\n      \"typeANewOption\": \"Tippe eine neue Option\",\n      \"tagName\": \"Tag-Name\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"Aufgabenbeschreibung\",\n      \"addNew\": \"Füge eine Aufgabe hinzu\",\n      \"submitNewTask\": \"Erstellen\",\n      \"hideComplete\": \"Blende abgeschlossene Aufgaben aus\",\n      \"showComplete\": \"Zeige alle Aufgaben\"\n    },\n    \"url\": {\n      \"launch\": \"Im Browser öffnen\",\n      \"copy\": \"Webadresse kopieren\",\n      \"textFieldHint\": \"Gebe eine URL ein\"\n    },\n    \"relation\": {\n      \"relatedDatabasePlaceLabel\": \"Verwandte Datenbank\",\n      \"relatedDatabasePlaceholder\": \"Nichts\",\n      \"inRelatedDatabase\": \"in\",\n      \"rowSearchTextFieldPlaceholder\": \"Suchen\",\n      \"noDatabaseSelected\": \"Keine Datenbank ausgewählt! Bitte wähle zuerst eine Datenbank aus der nachfolgenden Liste aus:\",\n      \"emptySearchResult\": \"Nichts gefunden\",\n      \"linkedRowListLabel\": \"{count} verknüpfte Zeilen\",\n      \"unlinkedRowListLabel\": \"Eine weitere Zeile verknüpfen\"\n    },\n    \"menuName\": \"Datentabelle\",\n    \"referencedGridPrefix\": \"Sicht von\",\n    \"calculate\": \"berechnet\",\n    \"calculationTypeLabel\": {\n      \"none\": \"nichts\",\n      \"average\": \"Durchschnitt\",\n      \"max\": \"Max\",\n      \"median\": \"Mittelwert\",\n      \"min\": \"Min\",\n      \"sum\": \"Ergebnis\",\n      \"count\": \"Zahl\",\n      \"countEmpty\": \"Zahl leer\",\n      \"countEmptyShort\": \"leer\",\n      \"countNonEmpty\": \"Zahl nicht leer\",\n      \"countNonEmptyShort\": \"nicht leer\"\n    },\n    \"media\": {\n      \"rename\": \"Umbenennen\",\n      \"download\": \"Herunterladen\",\n      \"expand\": \"Erweitern\",\n      \"delete\": \"Löschen\",\n      \"moreFilesHint\": \"+{}\",\n      \"addFileOrImage\": \"Datei oder Link hinzufügen\",\n      \"attachmentsHint\": \"{}\",\n      \"addFileMobile\": \"Datei hinzufügen\",\n      \"extraCount\": \"+{}\",\n      \"deleteFileDescription\": \"Möchten Sie diese Datei wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.\",\n      \"showFileNames\": \"Dateinamen anzeigen\",\n      \"downloadSuccess\": \"Datei heruntergeladen\",\n      \"downloadFailedToken\": \"Datei konnte nicht heruntergeladen werden, Benutzertoken nicht verfügbar\",\n      \"setAsCover\": \"Als Cover festlegen\",\n      \"openInBrowser\": \"Im Browser öffnen\",\n      \"embedLink\": \"Dateilink einbetten\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"Dokument\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"13:00 Uhr\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Ein Board zum Verknüpfen auswählen\",\n        \"createANewBoard\": \"Ein neues Board erstellen\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Eine Datentabelle zum Verknüpfen auswählen\",\n        \"createANewGrid\": \"Eine neue Datentabelle erstellen\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Einen Kalender zum Verknüpfen auswählen\",\n        \"createANewCalendar\": \"Einen neuen Kalender erstellen\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"Eine Datentabelle zum Verknüpfen auswählen\"\n      },\n      \"name\": {\n        \"text\": \"Text\",\n        \"heading1\": \"Überschrift 1\",\n        \"heading2\": \"Überschrift 2\",\n        \"heading3\": \"Überschrift 3\",\n        \"image\": \"Bild\",\n        \"bulletedList\": \"Aufzählungsliste\",\n        \"numberedList\": \"Nummerierte Liste\",\n        \"todoList\": \"Aufgabenliste\",\n        \"doc\": \"Dokument\",\n        \"linkedDoc\": \"Link zur Seite\",\n        \"grid\": \"Raster\",\n        \"linkedGrid\": \"Verknüpftes Raster\",\n        \"kanban\": \"Kanban\",\n        \"linkedKanban\": \"Verknüpftes Kanban\",\n        \"calendar\": \"Kalender\",\n        \"linkedCalendar\": \"Verknüpfter Kalender\",\n        \"quote\": \"Zitat\",\n        \"divider\": \"Trenner\",\n        \"table\": \"Tabelle\",\n        \"outline\": \"Gliederung\",\n        \"mathEquation\": \"Mathematische Gleichung\",\n        \"code\": \"Code\",\n        \"toggleList\": \"Liste ein-/ausblenden\",\n        \"emoji\": \"Emoji\",\n        \"aiWriter\": \"KI-Autor\",\n        \"dateOrReminder\": \"Datum oder Erinnerung\",\n        \"photoGallery\": \"Fotogalerie\",\n        \"file\": \"Datei\"\n      },\n      \"subPage\": {\n        \"name\": \"Dokument\",\n        \"keyword1\": \"Unterseite\",\n        \"keyword2\": \"Seite\",\n        \"keyword3\": \"untergeordnete Seite\",\n        \"keyword4\": \"Seite einfügen\",\n        \"keyword6\": \"neue Seite\",\n        \"keyword7\": \"Seite erstellen\",\n        \"keyword8\": \"Dokument\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Gliederung\",\n      \"codeBlock\": \"Code Block\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Referenziertes Board\",\n      \"referencedGrid\": \"Referenzierte Datentabelle\",\n      \"referencedCalendar\": \"Referenzierter Kalender\",\n      \"referencedDocument\": \"Referenziertes Dokument\",\n      \"autoGeneratorMenuItemName\": \"AI-Autor\",\n      \"autoGeneratorTitleName\": \"AI: Die KI bitten, etwas zu schreiben ...\",\n      \"autoGeneratorLearnMore\": \"Mehr erfahren\",\n      \"autoGeneratorGenerate\": \"Erstellen\",\n      \"autoGeneratorHintText\": \"AI fragen ...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Der AI-Schlüssel kann nicht abgerufen werden\",\n      \"autoGeneratorRewrite\": \"Umschreiben\",\n      \"smartEdit\": \"KI-Assistenten\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"Korrigiere Rechtschreibung\",\n      \"warning\": \"⚠️ KI-Antworten können ungenau oder irreführend sein.\",\n      \"smartEditSummarize\": \"Zusammenfassen\",\n      \"smartEditImproveWriting\": \"Das Geschriebene verbessern\",\n      \"smartEditMakeLonger\": \"Länger machen\",\n      \"smartEditCouldNotFetchResult\": \"Das Ergebnis konnte nicht von AI abgerufen werden\",\n      \"smartEditCouldNotFetchKey\": \"Der AI-Schlüssel konnte nicht abgerufen werden\",\n      \"smartEditDisabled\": \"AI in den Einstellungen verbinden\",\n      \"appflowyAIEditDisabled\": \"Melde dich an, um KI-Funktionen zu aktivieren\",\n      \"discardResponse\": \"Möchtest du die KI-Antworten verwerfen?\",\n      \"createInlineMathEquation\": \"Formel erstellen\",\n      \"fonts\": \"Schriftarten\",\n      \"insertDate\": \"Datum einfügen\",\n      \"emoji\": \"Emoji\",\n      \"toggleList\": \"Liste umschalten\",\n      \"quoteList\": \"Kursblatt\",\n      \"numberedList\": \"Nummerierte Liste\",\n      \"bulletedList\": \"Stichpunktliste\",\n      \"todoList\": \"Offene Aufgaben Liste\",\n      \"callout\": \"Hervorhebung\",\n      \"simpleTable\": {\n        \"moreActions\": {\n          \"color\": \"Farbe\",\n          \"align\": \"Ausrichten\",\n          \"delete\": \"Löschen\",\n          \"duplicate\": \"duplizieren\",\n          \"insertLeft\": \"Links einfügen\",\n          \"insertRight\": \"Rechts einfügen\",\n          \"insertAbove\": \"Oben einfügen\",\n          \"insertBelow\": \"Unten einfügen\",\n          \"headerColumn\": \"Kopfspalte\",\n          \"headerRow\": \"Kopfzeile\",\n          \"clearContents\": \"Klarer Inhalt\",\n          \"setToPageWidth\": \"Auf Seitenbreite einstellen\",\n          \"distributeColumnsWidth\": \"Spalten gleichmäßig verteilen\",\n          \"duplicateRow\": \"Zeile duplizieren\",\n          \"duplicateColumn\": \"Spalte duplizieren\",\n          \"textColor\": \"Textfarbe\",\n          \"cellBackgroundColor\": \"Zellen-Hintergrundfarbe\",\n          \"duplicateTable\": \"Tabelle duplizieren\"\n        },\n        \"clickToAddNewRow\": \"Klicken Sie hier, um eine neue Zeile hinzuzufügen\",\n        \"clickToAddNewColumn\": \"Klicken Sie hier, um eine neue Spalte hinzuzufügen\",\n        \"clickToAddNewRowAndColumn\": \"Klicken Sie hier, um eine neue Zeile und Spalte hinzuzufügen\",\n        \"headerName\": {\n          \"table\": \"Tabelle\",\n          \"alignText\": \"Text ausrichten\"\n        }\n      },\n      \"cover\": {\n        \"changeCover\": \"Titelbild wechseln\",\n        \"colors\": \"Farben\",\n        \"images\": \"Bilder\",\n        \"clearAll\": \"Alles löschen\",\n        \"abstract\": \"Abstrakt\",\n        \"addCover\": \"Titelbild hinzufügen\",\n        \"addLocalImage\": \"Lokales Bild hinzufügen\",\n        \"invalidImageUrl\": \"Ungültige Bild-URL\",\n        \"failedToAddImageToGallery\": \"Das Bild konnte nicht zur Galerie hinzugefügt werden\",\n        \"enterImageUrl\": \"Bild-URL eingeben\",\n        \"add\": \"Hinzufügen\",\n        \"back\": \"Zurück\",\n        \"saveToGallery\": \"In der Galerie speichern\",\n        \"removeIcon\": \"Symbol entfernen\",\n        \"removeCover\": \"Cover entfernen\",\n        \"pasteImageUrl\": \"Bild-URL einfügen\",\n        \"or\": \"ODER\",\n        \"pickFromFiles\": \"Dateien auswählen\",\n        \"couldNotFetchImage\": \"Bild konnte nicht abgerufen werden\",\n        \"imageSavingFailed\": \"Bildspeicherung fehlgeschlagen\",\n        \"addIcon\": \"Symbol hinzufügen\",\n        \"changeIcon\": \"Symbol wechseln\",\n        \"coverRemoveAlert\": \"Nach dem Löschen wird es aus dem Titelbild entfernt.\",\n        \"alertDialogConfirmation\": \"Sicher, dass du weitermachen willst?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"Mathematische Formel\",\n        \"addMathEquation\": \"Mathematische Formel hinzufügen\",\n        \"editMathEquation\": \"Mathematische Formel bearbeiten\"\n      },\n      \"optionAction\": {\n        \"click\": \"Klicken\",\n        \"toOpenMenu\": \" um das Menü zu öffnen\",\n        \"drag\": \"Ziehen\",\n        \"toMove\": \" bewegen\",\n        \"delete\": \"Löschen\",\n        \"duplicate\": \"Duplikat\",\n        \"turnInto\": \"Umwandeln in\",\n        \"moveUp\": \"Nach oben verschieben\",\n        \"moveDown\": \"Nach unten verschieben\",\n        \"color\": \"Farbe\",\n        \"align\": \"Ausrichten\",\n        \"left\": \"Links\",\n        \"center\": \"Zentriert\",\n        \"right\": \"Rechts\",\n        \"defaultColor\": \"Standard\",\n        \"depth\": \"Tiefe\",\n        \"copyLinkToBlock\": \"Link zum Block kopieren\"\n      },\n      \"image\": {\n        \"addAnImage\": \"Ein Bild hinzufügen\",\n        \"copiedToPasteBoard\": \"Der Bildlink wurde in die Zwischenablage kopiert\",\n        \"addAnImageDesktop\": \"Bild(er) hier ablegen oder klicken, um Bild(er) hinzuzufügen\",\n        \"addAnImageMobile\": \"Klicke, um ein oder mehrere Bilder hinzuzufügen\",\n        \"dropImageToInsert\": \"Zum Einfügen Bilder hier ablegen\",\n        \"imageUploadFailed\": \"Bild hochladen gescheitert\",\n        \"imageDownloadFailed\": \"Das Hochladen des Bilds ist fehlgeschlagen. Bitte versuche es erneut.\",\n        \"imageDownloadFailedToken\": \"Das Hochladen des Bilds ist aufgrund eines fehlenden Benutzertokens fehlgeschlagen. Bitte versuche es erneut.\",\n        \"errorCode\": \"Fehlercode\"\n      },\n      \"photoGallery\": {\n        \"name\": \"Fotogallerie\",\n        \"imageKeyword\": \"Bild\",\n        \"imageGalleryKeyword\": \"Bildergalerie\",\n        \"photoKeyword\": \"Foto\",\n        \"photoBrowserKeyword\": \"Fotobrowser\",\n        \"galleryKeyword\": \"Galerie\",\n        \"addImageTooltip\": \"Bild hinzufügen\",\n        \"changeLayoutTooltip\": \"Layout ändern\",\n        \"browserLayout\": \"Browser\",\n        \"gridLayout\": \"Datentabelle\",\n        \"deleteBlockTooltip\": \"Ganze Galerie löschen\"\n      },\n      \"math\": {\n        \"copiedToPasteBoard\": \"Die mathematische Gleichung wurde in die Zwischenablage kopiert\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"Der Link wurde in die Zwischenablage kopiert\",\n        \"convertToLink\": \"Konvertieren zum eingebetteten Link\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Füge Überschriften hinzu, um ein Inhaltsverzeichnis zu erstellen.\",\n        \"noMatchHeadings\": \"Keine passenden Überschriften gefunden.\"\n      },\n      \"table\": {\n        \"addAfter\": \"Danach einfügen\",\n        \"addBefore\": \"Davor einfügen\",\n        \"delete\": \"Löschen\",\n        \"clear\": \"Inhalt löschen\",\n        \"duplicate\": \"Duplikat\",\n        \"bgColor\": \"Hintergrundfarbe\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Kopieren\",\n        \"cut\": \"Ausschneiden\",\n        \"paste\": \"Einfügen\",\n        \"pasteAsPlainText\": \"Als einfachen Text einfügen\"\n      },\n      \"action\": \"Aktionen\",\n      \"database\": {\n        \"selectDataSource\": \"Datenquelle auswählen\",\n        \"noDataSource\": \"Keine Datenquelle\",\n        \"selectADataSource\": \"Eine Datenquelle auswählen\",\n        \"toContinue\": \"fortfahren\",\n        \"newDatabase\": \"Neue Datenbank\",\n        \"linkToDatabase\": \"Verknüpfung zur Datenbank\"\n      },\n      \"date\": \"Datum\",\n      \"video\": {\n        \"label\": \"Video\",\n        \"emptyLabel\": \"Video hinzufügen\",\n        \"placeholder\": \"Videolink einfügen\",\n        \"copiedToPasteBoard\": \"Der Videolink wurde in die Zwischenablage kopiert\",\n        \"insertVideo\": \"Video hinzufügen\",\n        \"invalidVideoUrl\": \"Die Quell-URL wird noch nicht unterstützt.\",\n        \"invalidVideoUrlYouTube\": \"YouTube wird noch nicht unterstützt.\",\n        \"supportedFormats\": \"Unterstützte Formate: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264\"\n      },\n      \"file\": {\n        \"name\": \"Datei\",\n        \"uploadTab\": \"Hochladen\",\n        \"uploadMobile\": \"Wählen Sie eine Datei\",\n        \"uploadMobileGallery\": \"Aus der Fotogalerie\",\n        \"networkTab\": \"Link integrieren\",\n        \"placeholderText\": \"Klicke oder ziehe eine Datei hoch, um sie hochzuladen\",\n        \"placeholderDragging\": \"Lege die hochzuladende Datei hier ab\",\n        \"dropFileToUpload\": \"Lege die hochzuladende Datei hier ab\",\n        \"fileUploadHint\": \"Lege die hochzuladende Datei hier ab\\noder klicke, um eine Datei auszuwählen.\",\n        \"fileUploadHintSuffix\": \"Durchsuchen\",\n        \"networkHint\": \"Gebe einen Link zu einer Datei ein\",\n        \"networkUrlInvalid\": \"Ungültige URL. Bitte korrigiere die URL und versuche es erneut.\",\n        \"networkAction\": \"Dateilink einbetten\",\n        \"fileTooBigError\": \"Die Dateigröße ist zu groß. Bitte lade eine Datei mit einer Größe von weniger als 10 MB hoch.\",\n        \"renameFile\": {\n          \"title\": \"Datei umbenennen\",\n          \"description\": \"Gebe den neuen Namen für diese Datei ein\",\n          \"nameEmptyError\": \"Der Dateiname darf nicht leer bleiben.\"\n        },\n        \"uploadedAt\": \"Hochgeladen am {}\",\n        \"linkedAt\": \"Link hinzugefügt am {}\",\n        \"failedToOpenMsg\": \"Öffnen fehlgeschlagen, Datei nicht gefunden\"\n      },\n      \"subPage\": {\n        \"errors\": {\n          \"failedDeletePage\": \"Seite konnte nicht gelöscht werden\",\n          \"failedCreatePage\": \"Seite konnte nicht erstellt werden\",\n          \"failedMovePage\": \"Seite konnte nicht in dieses Dokument verschoben werden\",\n          \"failedDuplicatePage\": \"Seite konnte nicht dupliziert werden\",\n          \"failedDuplicateFindView\": \"Seite konnte nicht dupliziert werden - Originalansicht nicht gefunden\"\n        }\n      }\n    },\n    \"outlineBlock\": {\n      \"placeholder\": \"Inhaltsverzeichnis\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Gebe „/“ für Inhaltsblöcke ein\"\n    },\n    \"title\": {\n      \"placeholder\": \"Ohne Titel\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Klicken, um ein Bild hinzuzufügen\",\n      \"upload\": {\n        \"label\": \"Hochladen\",\n        \"placeholder\": \"Klicken, um das Bild hochzuladen\"\n      },\n      \"url\": {\n        \"label\": \"Bild URL\",\n        \"placeholder\": \"Bild-URL eingeben\"\n      },\n      \"ai\": {\n        \"label\": \"Bild mit AI erstellen\",\n        \"placeholder\": \"Bitte den Prompt für AI eingeben, um ein Bild zu erstellen\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Bild mit Stability AI erstellen\",\n        \"placeholder\": \"Bitte den Prompt für Stability AI eingeben, um ein Bild zu erstellen\"\n      },\n      \"support\": \"Die Bildgrößenbeschränkung beträgt 5 MB. Unterstützte Formate: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Ungültiges Bild\",\n        \"invalidImageSize\": \"Die Bildgröße muss kleiner als 5 MB sein\",\n        \"invalidImageFormat\": \"Das Bildformat wird nicht unterstützt. Unterstützte Formate: JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"Ungültige Bild-URL\",\n        \"noImage\": \"Keine Datei oder Verzeichnis\",\n        \"multipleImagesFailed\": \"Ein oder mehrere Bilder konnten nicht hochgeladen werden. Bitte versuche es erneut.\"\n      },\n      \"embedLink\": {\n        \"label\": \"Eingebetteter Link\",\n        \"placeholder\": \"Bild-Link einfügen oder eintippen\"\n      },\n      \"unsplash\": {\n        \"label\": \"Unsplash\"\n      },\n      \"searchForAnImage\": \"Nach einem Bild suchen\",\n      \"pleaseInputYourOpenAIKey\": \"biitte den AI Schlüssel in der Einstellungsseite eingeben\",\n      \"saveImageToGallery\": \"Bild speichern\",\n      \"failedToAddImageToGallery\": \"Das Bild konnte nicht zur Galerie hinzugefügt werden\",\n      \"successToAddImageToGallery\": \"Das Bild wurde zur Galerie hinzugefügt werden\",\n      \"unableToLoadImage\": \"Das Bild konnte nicht geladen werden\",\n      \"maximumImageSize\": \"Die maximal unterstützte Upload-Bildgröße beträgt 10 MB\",\n      \"uploadImageErrorImageSizeTooBig\": \"Die Bildgröße muss weniger als 10 MB betragen\",\n      \"imageIsUploading\": \"Bild wird hochgeladen\",\n      \"openFullScreen\": \"Im Vollbildmodus öffnen\",\n      \"interactiveViewer\": {\n        \"toolbar\": {\n          \"previousImageTooltip\": \"Vorheriges Bild\",\n          \"nextImageTooltip\": \"Nächstes Bild\",\n          \"zoomOutTooltip\": \"Rauszoomen\",\n          \"zoomInTooltip\": \"Hineinzoomen\",\n          \"changeZoomLevelTooltip\": \"Zoomstufe ändern\",\n          \"openLocalImage\": \"Bild öffnen\",\n          \"downloadImage\": \"Bild herunterladen\",\n          \"closeViewer\": \"Interaktive Anzeige schließen\",\n          \"scalePercentage\": \"{}%\",\n          \"deleteImageTooltip\": \"Bild löschen\"\n        }\n      },\n      \"pleaseInputYourStabilityAIKey\": \"biitte den Stability AI Schlüssel in der Einstellungsseite eingeben\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Sprache\",\n        \"placeholder\": \"Sprache auswählen\",\n        \"auto\": \"Auto\"\n      },\n      \"copyTooltip\": \"Inhalt des Codeblocks kopieren\",\n      \"searchLanguageHint\": \"Nach einer Sprache suchen\",\n      \"codeCopiedSnackbar\": \"Code in die Zwischenablage kopiert!\"\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Link einfügen oder eintippen\",\n      \"openInNewTab\": \"Im neuen Tab öffnen\",\n      \"copyLink\": \"Link kopieren\",\n      \"removeLink\": \"Link entfernen\",\n      \"url\": {\n        \"label\": \"URL verknüpfen\",\n        \"placeholder\": \"Link-URL eingeben\"\n      },\n      \"title\": {\n        \"label\": \"Linktitel\",\n        \"placeholder\": \"Linktitel eingeben\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"Eine Person, Seite oder Datum erwähnen...\",\n      \"page\": {\n        \"label\": \"Link zur Seite\",\n        \"tooltip\": \"Klicken, um die Seite zu öffnen\"\n      },\n      \"deleted\": \"gelöscht\",\n      \"deletedContent\": \"Dieser Inhalt existiert nicht oder wurde gelöscht\",\n      \"noAccess\": \"Kein Zugriff\",\n      \"deletedPage\": \"Gelöschte Seite\",\n      \"trashHint\": \" - im Papierkorb\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"Auf den Standard zurücksetzen\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"Die aktuelle Version unterstützt diesen Block nicht.\",\n      \"clickToCopyTheBlockContent\": \"Hier klicken, um den Blockinhalt zu kopieren\",\n      \"blockContentHasBeenCopied\": \"Der Blockinhalt wurde kopiert.\",\n      \"copyBlockContent\": \"Blockinhalt kopieren\"\n    },\n    \"mobilePageSelector\": {\n      \"title\": \"Seite auswählen\",\n      \"failedToLoad\": \"Seitenliste konnte nicht geladen werden\",\n      \"noPagesFound\": \"Keine Seiten gefunden\"\n    },\n    \"attachmentMenu\": {\n      \"choosePhoto\": \"Foto auswählen\",\n      \"takePicture\": \"Ein Foto machen\",\n      \"chooseFile\": \"Datei auswählen\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"label\": \"Spalte\",\n      \"createNewCard\": \"Neu\",\n      \"renameGroupTooltip\": \"Drücken, um die Gruppe umzubenennen\",\n      \"createNewColumn\": \"Eine neue Gruppe hinzufügen\",\n      \"addToColumnTopTooltip\": \"Eine neue Karte am oberen Ende hinzufügen\",\n      \"addToColumnBottomTooltip\": \"Eine neue Karte am unteren Ende hinzufügen\",\n      \"renameColumn\": \"Umbenennen\",\n      \"hideColumn\": \"Verstecken\",\n      \"newGroup\": \"Neue Gruppe\",\n      \"deleteColumn\": \"Löschen\",\n      \"deleteColumnConfirmation\": \"Das wird diese Gruppe und alle enthaltenen Karten löschen.\\nSicher, dass du fortsetzen möchtest?\",\n      \"groupActions\": \"Gruppenaktion\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"Versteckte Gruppen\",\n      \"collapseTooltip\": \"Verstecke die versteckten Gruppen\",\n      \"expandTooltip\": \"Zeige die versteckten Gruppen\"\n    },\n    \"cardDetail\": \"Kartendetail\",\n    \"cardActions\": \"Kartenaktionen\",\n    \"cardDuplicated\": \"Karte wurde dupliziert\",\n    \"cardDeleted\": \"Karte wurde gelöscht\",\n    \"showOnCard\": \"Zeige das Kartendetail\",\n    \"setting\": \"Einstellung\",\n    \"propertyName\": \"Eigenschaftname\",\n    \"menuName\": \"Board\",\n    \"showUngrouped\": \"Zeige nichtgruppierte Elemente\",\n    \"ungroupedButtonText\": \"Nichtgruppiert\",\n    \"ungroupedButtonTooltip\": \"Enthält Karten, die keiner Gruppe zugeordnet sind\",\n    \"ungroupedItemsTitle\": \"Klicke, um dem Board hinzuzufügen\",\n    \"groupBy\": \"Gruppiert nach\",\n    \"groupCondition\": \"Gruppenbedingung\",\n    \"referencedBoardPrefix\": \"Sicht von\",\n    \"notesTooltip\": \"Notizen vorhanden\",\n    \"mobile\": {\n      \"editURL\": \"Bearbeite URL\",\n      \"showGroup\": \"Zeige die Gruppe\",\n      \"showGroupContent\": \"Sicher, dass diese Gruppe auf dem Board angezeigt werden soll?\",\n      \"failedToLoad\": \"Boardansicht konnte nicht geladen werden\"\n    },\n    \"dateCondition\": {\n      \"weekOf\": \"Woche von {} - {}\",\n      \"today\": \"Heute\",\n      \"yesterday\": \"Gestern\",\n      \"tomorrow\": \"Morgen\",\n      \"lastSevenDays\": \"Letzte 7 Tage\",\n      \"nextSevenDays\": \"Nächste 7 Tage\",\n      \"lastThirtyDays\": \"Letzte 30 Tage\",\n      \"nextThirtyDays\": \"Nächste 30 Tage\"\n    },\n    \"noGroup\": \"Keine Gruppierung nach Eigenschaft\",\n    \"noGroupDesc\": \"Board-Ansichten benötigen eine Eigenschaft zum Gruppieren, um angezeigt zu werden\",\n    \"media\": {\n      \"cardText\": \"{} {}\",\n      \"fallbackName\": \"Dateien\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Kalender\",\n    \"defaultNewCalendarTitle\": \"Ohne Titel\",\n    \"newEventButtonTooltip\": \"Einen neues Ereignis eintragen\",\n    \"navigation\": {\n      \"today\": \"Heute\",\n      \"jumpToday\": \"Springe zu Heute\",\n      \"previousMonth\": \"Vorheriger Monat\",\n      \"nextMonth\": \"Nächster Monat\",\n      \"views\": {\n        \"day\": \"Tag\",\n        \"week\": \"Woche\",\n        \"month\": \"Monat\",\n        \"year\": \"Jahr\"\n      }\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"Noch keine Events\",\n      \"emptyBody\": \"Drücke die Plus-Taste, um für heute ein Ereignis zu erstellen.\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Wochennummern anzeigen\",\n      \"showWeekends\": \"Wochenenden anzeigen\",\n      \"firstDayOfWeek\": \"Wochenstart\",\n      \"layoutDateField\": \"Layoutkalender von\",\n      \"changeLayoutDateField\": \"Layoutfeld ändern\",\n      \"noDateTitle\": \"Kein Datum\",\n      \"noDateHint\": {\n        \"zero\": \"Ereignisse ohne Datum werden hier angezeigt\",\n        \"one\": \"{count} Ereignisse ohne Datum\",\n        \"other\": \"{count} Ereignisse ohne Datum\"\n      },\n      \"unscheduledEventsTitle\": \"Ungeplante Events\",\n      \"clickToAdd\": \"Klicken zum hinzufügen im Kalender\",\n      \"name\": \"Kalendereinstellungen\",\n      \"clickToOpen\": \"Hier klicken, um den Eintrag zu öffnen\"\n    },\n    \"referencedCalendarPrefix\": \"Sicht von\",\n    \"quickJumpYear\": \"Spring zu\",\n    \"duplicateEvent\": \"Doppeltes Ereignis\"\n  },\n  \"errorDialog\": {\n    \"title\": \"@:appName-Fehler\",\n    \"howToFixFallback\": \"Wir entschuldigen uns für die Unannehmlichkeiten! Reiche auf unserer GitHub-Seite ein Problem ein, das deinen Fehler beschreibt.\",\n    \"howToFixFallbackHint1\": \"Wir entschuldigen uns für die Unannehmlichkeiten! Melden Sie ein Problem auf unserer \",\n    \"howToFixFallbackHint2\": \" Seite, die Ihren Fehler beschreibt.\",\n    \"github\": \"Auf GitHub ansehen\"\n  },\n  \"search\": {\n    \"label\": \"Suchen\",\n    \"sidebarSearchIcon\": \"Suchen und schnell zu einer Seite springen\",\n    \"placeholder\": {\n      \"actions\": \"Suchaktionen...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Kopiert!\",\n      \"fail\": \"Kopieren nicht möglich\"\n    }\n  },\n  \"unSupportBlock\": \"Die aktuelle Version unterstützt diesen Block nicht.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Möchtest du den {pageType} wirklich löschen?\",\n    \"deleteContentCaption\": \"Wenn du diesen {pageType} löschst, kannst du ihn aus dem Papierkorb wiederherstellen.\"\n  },\n  \"colors\": {\n    \"custom\": \"Individuell\",\n    \"default\": \"Standard\",\n    \"red\": \"Rot\",\n    \"orange\": \"Orange\",\n    \"yellow\": \"Gelb\",\n    \"green\": \"Grün\",\n    \"blue\": \"Blau\",\n    \"purple\": \"Lila\",\n    \"pink\": \"Pink\",\n    \"brown\": \"Braun\",\n    \"gray\": \"Grau\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"Emoji\",\n    \"search\": \"Emoji suchen\",\n    \"noRecent\": \"Kein neuer Emoji\",\n    \"noEmojiFound\": \"Kein Emoji gefunden\",\n    \"filter\": \"Filter\",\n    \"random\": \"Zufällig\",\n    \"selectSkinTone\": \"Hautfarbe auswählen\",\n    \"remove\": \"Emoji entfernen\",\n    \"categories\": {\n      \"smileys\": \"Smileys & Emotionen\",\n      \"people\": \"Menschen & Körper\",\n      \"animals\": \"Tiere & Natur\",\n      \"food\": \"Essen & Trinken\",\n      \"activities\": \"Aktivitäten\",\n      \"places\": \"Reisen & Orte\",\n      \"objects\": \"Objekte\",\n      \"symbols\": \"Symbole\",\n      \"flags\": \"Flaggen\",\n      \"nature\": \"Natur\",\n      \"frequentlyUsed\": \"Häufig verwendet\"\n    },\n    \"skinTone\": {\n      \"default\": \"Standard\",\n      \"light\": \"Hell\",\n      \"mediumLight\": \"Mittelhell\",\n      \"medium\": \"Mittel\",\n      \"mediumDark\": \"Mitteldunkel\",\n      \"dark\": \"Dunkel\"\n    },\n    \"openSourceIconsFrom\": \"Open-Source-Icons von\"\n  },\n  \"inlineActions\": {\n    \"noResults\": \"Keine Ergebnisse\",\n    \"recentPages\": \"Kürzliche Seiten\",\n    \"pageReference\": \"Seitenreferenz\",\n    \"docReference\": \"Dokumentverweis\",\n    \"boardReference\": \"Board-Referenz\",\n    \"calReference\": \"Kalenderreferenz\",\n    \"gridReference\": \"Gitter Referenz\",\n    \"date\": \"Datum\",\n    \"reminder\": {\n      \"groupTitle\": \"Erinnerung\",\n      \"shortKeyword\": \"erinnern\"\n    }\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"Das Datums- und Zeitformat in den Einstellungen anpassen\",\n    \"dateFormat\": \"Datumsformat\",\n    \"includeTime\": \"Inkl. Zeit\",\n    \"isRange\": \"Enddatum\",\n    \"timeFormat\": \"Zeitformat\",\n    \"clearDate\": \"Datum löschen\",\n    \"reminderLabel\": \"Erinnerung\",\n    \"selectReminder\": \"Erinnerung auswählen\",\n    \"reminderOptions\": {\n      \"none\": \"nichts\",\n      \"atTimeOfEvent\": \"Uhrzeit des Events\",\n      \"fiveMinsBefore\": \"5Min. vorher\",\n      \"tenMinsBefore\": \"10Min. vorher\",\n      \"fifteenMinsBefore\": \"15Min. vorher\",\n      \"thirtyMinsBefore\": \"30Min. vorher\",\n      \"oneHourBefore\": \"1Std. vorher\",\n      \"twoHoursBefore\": \"2Std. vorher\",\n      \"onDayOfEvent\": \"Am Tag des Events\",\n      \"oneDayBefore\": \"1Tag vorher\",\n      \"twoDaysBefore\": \"2Tage vorher\",\n      \"oneWeekBefore\": \"1Woche vorher\",\n      \"custom\": \"Benutzerdefiniert\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"Gestern\",\n    \"today\": \"Heute\",\n    \"tomorrow\": \"Morgen\",\n    \"oneWeek\": \"1 Woche\"\n  },\n  \"notificationHub\": {\n    \"title\": \"Benachrichtigungen\",\n    \"mobile\": {\n      \"title\": \"Neuigkeiten\"\n    },\n    \"emptyTitle\": \"Leer\",\n    \"emptyBody\": \"Keine offenen Benachrichtigungen oder Aktionen. Genieße die Ruhe.\",\n    \"tabs\": {\n      \"inbox\": \"Eingang\",\n      \"upcoming\": \"Demnächst\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"Alle als gelesen markieren\",\n      \"showAll\": \"Alle\",\n      \"showUnreads\": \"Ungelesen\"\n    },\n    \"filters\": {\n      \"ascending\": \"Aufsteigend\",\n      \"descending\": \"Absteigend\",\n      \"groupByDate\": \"Nach Datum groupieren\",\n      \"showUnreadsOnly\": \"Nur ungelesen zeigen\",\n      \"resetToDefault\": \"Zurücksetzen\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"Erinnerung\",\n    \"message\": \"Bitte denke daran, dass hier zu prüfen bevor du es vergisst.\",\n    \"tooltipDelete\": \"Löschen\",\n    \"tooltipMarkRead\": \"Als gelesen markieren\",\n    \"tooltipMarkUnread\": \"Als ungelesen markieren\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"Finden\",\n    \"previousMatch\": \"Vorheriger Treffer\",\n    \"nextMatch\": \"Nächster Treffer\",\n    \"close\": \"Schließen\",\n    \"replace\": \"Ersetzen\",\n    \"replaceAll\": \"Alle ersetzen\",\n    \"noResult\": \"Keine Ergebnisse\",\n    \"caseSensitive\": \"Groß-/Kleinschreibung beachten\",\n    \"searchMore\": \"Suche für mehr Ergebnisse\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"Das tut uns leid\",\n    \"loadingViewError\": \"Wir haben Schwierigkeiten diese Ansicht zu laden. Bitte prüfe die Internetverbindung, lade die App neu und zögere nicht, das Team zu kontaktieren, falls das Problem weiterhin besteht.\",\n    \"syncError\": \"Daten werden nicht von einem anderen Gerät synchronisiert\",\n    \"syncErrorHint\": \"Bitte öffnen Sie diese Seite erneut auf dem Gerät, auf dem sie zuletzt bearbeitet wurde, und öffnen Sie die Seite dann erneut  auf dem aktuellen Gerät.\",\n    \"clickToCopy\": \"Klicken Sie hier, um den Fehlercode zu kopieren\"\n  },\n  \"editor\": {\n    \"bold\": \"Fett\",\n    \"bulletedList\": \"Stichpunktliste\",\n    \"bulletedListShortForm\": \"Mit Aufzählungszeichen\",\n    \"checkbox\": \"Checkbox\",\n    \"embedCode\": \"Eingebetteter Code\",\n    \"heading1\": \"Überschrift 1\",\n    \"heading2\": \"Überschrift 2\",\n    \"heading3\": \"Überschrift 3\",\n    \"highlight\": \"Hervorhebung\",\n    \"color\": \"Farbe\",\n    \"image\": \"Bild\",\n    \"date\": \"Datum\",\n    \"page\": \"Seite\",\n    \"italic\": \"Kursiv\",\n    \"link\": \"Link\",\n    \"numberedList\": \"Nummerierte Liste\",\n    \"numberedListShortForm\": \"Nummeriert\",\n    \"quote\": \"Zitat\",\n    \"strikethrough\": \"Durgestrichen\",\n    \"text\": \"Text\",\n    \"underline\": \"Unterstrichen\",\n    \"fontColorDefault\": \"Standard\",\n    \"fontColorGray\": \"Grau\",\n    \"fontColorBrown\": \"Braun\",\n    \"fontColorOrange\": \"Orange\",\n    \"fontColorYellow\": \"Gelb\",\n    \"fontColorGreen\": \"Grün\",\n    \"fontColorBlue\": \"Blau\",\n    \"fontColorPurple\": \"Lila\",\n    \"fontColorPink\": \"Pink\",\n    \"fontColorRed\": \"Rot\",\n    \"backgroundColorDefault\": \"Standardhintergrund\",\n    \"backgroundColorGray\": \"Grauer Hintergrund\",\n    \"backgroundColorBrown\": \"Brauner Hintergrund\",\n    \"backgroundColorOrange\": \"Oranger Hintergrund\",\n    \"backgroundColorYellow\": \"Gelber Hintergrund\",\n    \"backgroundColorGreen\": \"Grüner Hintergrund\",\n    \"backgroundColorBlue\": \"Blauer Hintergrund\",\n    \"backgroundColorPurple\": \"Lila Hintergrund\",\n    \"backgroundColorPink\": \"Pinker Hintergrund\",\n    \"backgroundColorRed\": \"Roter Hintergrund\",\n    \"backgroundColorLime\": \"Lime-Hintergrund\",\n    \"backgroundColorAqua\": \"Aqua-Hintergrund\",\n    \"done\": \"Erledigt\",\n    \"cancel\": \"Abbrechen\",\n    \"tint1\": \"Farbton 1\",\n    \"tint2\": \"Farbton 2\",\n    \"tint3\": \"Farbton 3\",\n    \"tint4\": \"Farbton 4\",\n    \"tint5\": \"Farbton 5\",\n    \"tint6\": \"Farbton 6\",\n    \"tint7\": \"Farbton 7\",\n    \"tint8\": \"Farbton 8\",\n    \"tint9\": \"Farbton 9\",\n    \"lightLightTint1\": \"Lila\",\n    \"lightLightTint2\": \"Pink\",\n    \"lightLightTint3\": \"Helles Pink\",\n    \"lightLightTint4\": \"Orange\",\n    \"lightLightTint5\": \"Gelb\",\n    \"lightLightTint6\": \"Hellgrün\",\n    \"lightLightTint7\": \"Grün\",\n    \"lightLightTint8\": \"Aqua\",\n    \"lightLightTint9\": \"Blau\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"Überschrift 1\",\n    \"mobileHeading2\": \"Überschrift 2\",\n    \"mobileHeading3\": \"Überschrift 3\",\n    \"textColor\": \"Textfarbe\",\n    \"backgroundColor\": \"Hintergrundfarbe\",\n    \"addYourLink\": \"Link hinzufügen\",\n    \"openLink\": \"Link öffnen\",\n    \"copyLink\": \"Link kopieren\",\n    \"removeLink\": \"Link entfernen\",\n    \"editLink\": \"Link bearbeiten\",\n    \"linkText\": \"Text\",\n    \"linkTextHint\": \"Bitte Text eingeben\",\n    \"linkAddressHint\": \"Bitte URL eingeben\",\n    \"highlightColor\": \"Hervorhebungsfarbe\",\n    \"clearHighlightColor\": \"Hervorhebungsfarbe löschen\",\n    \"customColor\": \"Eigene Farbe\",\n    \"hexValue\": \"Hex-Wert\",\n    \"opacity\": \"Transparenz\",\n    \"resetToDefaultColor\": \"Auf Standardfarben zurücksetzen\",\n    \"ltr\": \"LTR (Links nach rechts)\",\n    \"rtl\": \"RTL (rechts nach links)\",\n    \"auto\": \"Auto\",\n    \"cut\": \"Ausschneiden\",\n    \"copy\": \"Kopieren\",\n    \"paste\": \"Einfügen\",\n    \"find\": \"Finden\",\n    \"select\": \"Auswählen\",\n    \"selectAll\": \"Alle auswählen\",\n    \"previousMatch\": \"Vorheriger Treffer\",\n    \"nextMatch\": \"Nächster Treffer\",\n    \"closeFind\": \"Schließen\",\n    \"replace\": \"Ersetzen\",\n    \"replaceAll\": \"Alle ersetzen\",\n    \"regex\": \"Regulärer Ausdruck (Regex)\",\n    \"caseSensitive\": \"Groß-/Kleinschreibung beachten\",\n    \"uploadImage\": \"Bild hochladen\",\n    \"urlImage\": \"Bild-URL\",\n    \"incorrectLink\": \"Defekter Link\",\n    \"upload\": \"Hochladen\",\n    \"chooseImage\": \"Bild auswählen\",\n    \"loading\": \"Lädt\",\n    \"imageLoadFailed\": \"Bild konnte nicht geladen werden\",\n    \"divider\": \"Trenner\",\n    \"table\": \"Tabelle\",\n    \"colAddBefore\": \"Davor einfügen\",\n    \"rowAddBefore\": \"Davor einfügen\",\n    \"colAddAfter\": \"Danach einfügen\",\n    \"rowAddAfter\": \"Danach einfügen\",\n    \"colRemove\": \"Enternen\",\n    \"rowRemove\": \"Entfernen\",\n    \"colDuplicate\": \"Duplikat\",\n    \"rowDuplicate\": \"Duplikat\",\n    \"colClear\": \"Inhalt löschen\",\n    \"rowClear\": \"Inhalt löschen\",\n    \"slashPlaceHolder\": \"'/'-Taste, um einen Block einzufügen oder Text eingeben\",\n    \"typeSomething\": \"Etwas eingeben...\",\n    \"toggleListShortForm\": \"Umschalten\",\n    \"quoteListShortForm\": \"Zitat\",\n    \"mathEquationShortForm\": \"Formel\",\n    \"codeBlockShortForm\": \"Code\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"Leere Favoritenseite\",\n    \"noFavoriteHintText\": \"Nach links wischen, um es den Favoriten hinzuzufügen\",\n    \"removeFromSidebar\": \"Aus der Seitenleiste entfernen\",\n    \"addToSidebar\": \"An Seitenleiste anheften\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"'/'-Taste, um einen Block einzufügen oder Text eingeben\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"To-Do\",\n    \"bulletList\": \"Liste\",\n    \"numberList\": \"Liste\",\n    \"quote\": \"Zitat\",\n    \"heading\": \"Überschrift {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"Seitensymbol\",\n    \"language\": \"Sprache\",\n    \"font\": \"Schrift\",\n    \"actions\": \"Aktionen\",\n    \"date\": \"Datum\",\n    \"addField\": \"Ein Feld hinzufügen\",\n    \"userIcon\": \"Nutzerbild\"\n  },\n  \"noLogFiles\": \"Hier gibt es kein Log-File\",\n  \"newSettings\": {\n    \"myAccount\": {\n      \"title\": \"Mein Benutzerkonto\",\n      \"subtitle\": \"Passe dein Profil an, verwalte Einstellungen zur Sicherheit deines Benutzerkontos, öffne AI-Schlüssel oder melde dich bei deinem Konto an.\",\n      \"profileLabel\": \"Kontoname und Profilbild\",\n      \"profileNamePlaceholder\": \"Gib deinen Namen ein\",\n      \"accountSecurity\": \"Konto Sicherheit\",\n      \"2FA\": \"Authentifizierung in zwei Schritten\",\n      \"aiKeys\": \"AI-Schlüssel\",\n      \"accountLogin\": \"Benutzerkonto Login\",\n      \"updateNameError\": \"Namensaktualisierung fehlgeschlagen!\",\n      \"updateIconError\": \"Symbol konnte nicht aktualisiert werden!\",\n      \"deleteAccount\": {\n        \"title\": \"Benutzerkonto löschen\",\n        \"subtitle\": \"Benutzerkonto inkl. deiner persönlicher Daten unwiderruflich löschen.\",\n        \"description\": \"Löschen Sie Ihr Konto dauerhaft und entfernen Sie den Zugriff auf alle Arbeitsbereiche.\",\n        \"deleteMyAccount\": \"Mein Benutzerkonto löschen\",\n        \"dialogTitle\": \"Benutzerkonto löschen\",\n        \"dialogContent1\": \"Bist du sicher, dass du dein Benutzerkonto unwiderruflich löschen möchtest?\",\n        \"dialogContent2\": \"Diese Aktion kann nicht rückgängig gemacht werden und führt dazu, dass der Zugriff auf alle Teambereiche aufgehoben wird, dein gesamtes Benutzerkonto, einschließlich privater Arbeitsbereiche, gelöscht wird und du aus allen freigegebenen Arbeitsbereichen entfernt wirst.\",\n        \"confirmHint1\": \"Geben Sie zur Bestätigung bitte „@:newSettings.myAccount.deleteAccount.confirmHint3“ ein.\",\n        \"confirmHint3\": \"MEIN KONTO LÖSCHEN\",\n        \"checkToConfirmError\": \"Sie müssen das Kontrollkästchen aktivieren, um das Löschen zu bestätigen\",\n        \"failedToGetCurrentUser\": \"Aktuelle Benutzer-E-Mail konnte nicht abgerufen werden.\",\n        \"confirmTextValidationFailed\": \"Ihr Bestätigungstext stimmt nicht mit „@:newSettings.myAccount.deleteAccount.confirmHint3“ überein.\",\n        \"deleteAccountSuccess\": \"Konto erfolgreich gelöscht\"\n      }\n    },\n    \"workplace\": {\n      \"name\": \"Arbeitsbereich\",\n      \"title\": \"Arbeitsbereichseinstellungen\",\n      \"subtitle\": \"Passe das Erscheinungsbild, das Design, die Schriftart, das Textlayout, das Datum, die Uhrzeit und die Sprache deines Arbeitsbereiches an.\",\n      \"workplaceName\": \"Name des Arbeitsbereiches\",\n      \"workplaceNamePlaceholder\": \"Gib den Namen des Arbeitsbereiches ein\",\n      \"workplaceIcon\": \"Symbol\",\n      \"workplaceIconSubtitle\": \"Lade ein Bild hoch oder verwende ein Emoji für deinen Arbeitsbereich. Das Symbol wird in deiner Seitenleiste und in deinen Benachrichtigungen angezeigt.\",\n      \"renameError\": \"Umbenennen des Arbeitsbereiches fehlgeschlagen\",\n      \"updateIconError\": \"Symbol konnte nicht aktualisiert werden\",\n      \"chooseAnIcon\": \"Symbol auswählen\",\n      \"appearance\": {\n        \"name\": \"Aussehen\",\n        \"themeMode\": {\n          \"auto\": \"Auto\",\n          \"light\": \"Hell\",\n          \"dark\": \"Dunkel\"\n        },\n        \"language\": \"Sprache\"\n      }\n    },\n    \"syncState\": {\n      \"syncing\": \"Synchronisierung\",\n      \"synced\": \"Synchronisiert\",\n      \"noNetworkConnected\": \"Kein Netzwerk verbunden\"\n    }\n  },\n  \"pageStyle\": {\n    \"title\": \"Seitenformatierung\",\n    \"layout\": \"Layout\",\n    \"coverImage\": \"Titelbild\",\n    \"pageIcon\": \"Seitensymbol\",\n    \"colors\": \"Farben\",\n    \"gradient\": \"Gradient\",\n    \"backgroundImage\": \"Hintergrundbild\",\n    \"presets\": \"Voreinstellungen\",\n    \"photo\": \"Foto\",\n    \"unsplash\": \"Unsplash\",\n    \"pageCover\": \"Deckblatt\",\n    \"none\": \"Keines\",\n    \"openSettings\": \"Einstellungen öffnen\",\n    \"photoPermissionTitle\": \"@:appName möchte auf deine Fotobibliothek zugreifen\",\n    \"photoPermissionDescription\": \"Erlaube den Zugriff auf die Fotobibliothek zum Hochladen von Bildern.\",\n    \"cameraPermissionTitle\": \"@:appName möchte auf Ihre Kamera zugreifen\",\n    \"cameraPermissionDescription\": \"@:appName benötigt Zugriff auf Ihre Kamera, damit Sie Bilder von der Kamera zu Ihren Dokumenten hinzufügen können.\",\n    \"doNotAllow\": \"Nicht zulassen\",\n    \"image\": \"Bild\"\n  },\n  \"commandPalette\": {\n    \"placeholder\": \"Tippe, um nach Ansichten zu suchen...\",\n    \"bestMatches\": \"Beste Ergebnisse\",\n    \"recentHistory\": \"Aktuelle Historie\",\n    \"navigateHint\": \"navigieren\",\n    \"loadingTooltip\": \"Wir suchen nach Ergebnissen ...\",\n    \"betaLabel\": \"BETA\",\n    \"betaTooltip\": \"Wir unterstützen derzeit nur die Suche nach Seiten\",\n    \"fromTrashHint\": \"Aus dem Mülleimer\",\n    \"noResultsHint\": \"Wir haben nicht gefunden, wonach du gesucht hast. Versuche nach einem anderen Begriff zu suchen.\",\n    \"clearSearchTooltip\": \"Suchfeld löschen\"\n  },\n  \"space\": {\n    \"delete\": \"Löschen\",\n    \"deleteConfirmation\": \"Löschen:\",\n    \"deleteConfirmationDescription\": \"Alle Seiten innerhalb dieses Space werden gelöscht und in den Papierkorb verschoben.\",\n    \"rename\": \"Domäne umbennen\",\n    \"changeIcon\": \"Symbol ändern\",\n    \"manage\": \"Domäne verwalten\",\n    \"addNewSpace\": \"Domäne erstellen\",\n    \"collapseAllSubPages\": \"Alle Unterseiten einklappen\",\n    \"createNewSpace\": \"Eine neue Domäne erstellen\",\n    \"createSpaceDescription\": \"Erstellen mehrere öffentliche und private Domänen, um deine Arbeit besser zu organisieren.\",\n    \"spaceName\": \"Name der Domäne\",\n    \"spaceNamePlaceholder\": \"z.B. Marketing, Entwicklung, Personalabteilung\",\n    \"permission\": \"Berechtigung\",\n    \"publicPermission\": \"Öffentlich\",\n    \"publicPermissionDescription\": \"Alle Mitglieder des Arbeitsbereichs mit Vollzugriff\",\n    \"privatePermission\": \"Privat\",\n    \"privatePermissionDescription\": \"Nur du hast Zugang zu dieser Domäne\",\n    \"spaceIconBackground\": \"Hintergrundfarbe\",\n    \"spaceIcon\": \"Symbol\",\n    \"dangerZone\": \"Gefahrenzone\",\n    \"unableToDeleteLastSpace\": \"Die letzte Domäne kann nicht gelöscht werden\",\n    \"unableToDeleteSpaceNotCreatedByYou\": \"Von anderen erstellte Domänen können nicht gelöscht werden\",\n    \"enableSpacesForYourWorkspace\": \"Domänen für deinen Arbeitsbereich aktivieren\",\n    \"title\": \"Domänen\",\n    \"defaultSpaceName\": \"Allgemein\",\n    \"upgradeSpaceTitle\": \"Domänen aktivieren\",\n    \"upgradeSpaceDescription\": \"Erstelle mehrere öffentliche und private Domänen, um deinen Arbeitsbereich besser zu organisieren.\",\n    \"upgrade\": \"Update\",\n    \"upgradeYourSpace\": \"Mehrere Domänen erstellen\",\n    \"quicklySwitch\": \"Schnell zur nächsten Domäne wechseln\",\n    \"duplicate\": \"Domäne duplizieren\",\n    \"movePageToSpace\": \"Seite in die Domäne verschieben\",\n    \"cannotMovePageToDatabase\": \"Seite kann nicht in die Datenbank verschoben werden\",\n    \"switchSpace\": \"Domäne wechseln\",\n    \"spaceNameCannotBeEmpty\": \"Der Space-Name darf nicht leer sein\",\n    \"success\": {\n      \"deleteSpace\": \"Domäne erfolgreich gelöscht\",\n      \"renameSpace\": \"Domäne erfolgreich umbenannt\",\n      \"duplicateSpace\": \"Domäne erfolgreich dupliziert\",\n      \"updateSpace\": \"Domäne erfolgreich angepasst\"\n    },\n    \"error\": {\n      \"deleteSpace\": \"Löschung der Domäne fehlgeschlagen\",\n      \"renameSpace\": \"Umbenennung der Domäne fehlgeschlagen\",\n      \"duplicateSpace\": \"Duplizierung der Domäne fehlgeschlagen\",\n      \"updateSpace\": \"Anpassung der Domäne fehlgeschlagen\"\n    },\n    \"createSpace\": \"Erstelle Domäne\",\n    \"manageSpace\": \"Verwalte Domäne\",\n    \"renameSpace\": \"Domäne umbenennen\",\n    \"mSpaceIconColor\": \"Domänen-Iconfarbe\",\n    \"mSpaceIcon\": \"Domänen-Icon\"\n  },\n  \"publish\": {\n    \"hasNotBeenPublished\": \"Diese Seite wurde noch nicht veröffentlicht\",\n    \"reportPage\": \"Berichtsseite\",\n    \"databaseHasNotBeenPublished\": \"Das Veröffentlichen einer Datenbank wird noch nicht unterstützt.\",\n    \"createdWith\": \"Erstellt mit\",\n    \"downloadApp\": \"AppFlowy herunterladen\",\n    \"copy\": {\n      \"codeBlock\": \"Der Inhalt des Codeblocks wurde in die Zwischenablage kopiert\",\n      \"imageBlock\": \"Der Bildlink wurde in die Zwischenablage kopiert\",\n      \"mathBlock\": \"Die mathematische Gleichung wurde in die Zwischenablage kopiert\"\n    },\n    \"containsPublishedPage\": \"Diese Seite enthält eine oder mehrere veröffentlichte Seiten. Wenn du fortfährst, werden sie nicht mehr veröffentlicht. Möchtest du mit dem Löschen fortfahren?\",\n    \"publishSuccessfully\": \"Erfolgreich veröffentlicht\",\n    \"unpublishSuccessfully\": \"Veröffentlichung erfolgreich aufgehoben\",\n    \"publishFailed\": \"Veröffentlichung fehlgeschlagen\",\n    \"unpublishFailed\": \"Die Veröffentlichung konnte nicht rückgängig gemacht werden.\",\n    \"noAccessToVisit\": \"Kein Zugriff auf diese Seite...\",\n    \"createWithAppFlowy\": \"Erstelle eine Website mit AppFlowy\",\n    \"fastWithAI\": \"Schnell und einfach mit KI.\",\n    \"tryItNow\": \"Probiere es jetzt\",\n    \"onlyGridViewCanBePublished\": \"Nur die Datentabellenansicht kann veröffentlicht werden\",\n    \"database\": {\n      \"zero\": \"{} ausgewählte Ansichten veröffentlichen\",\n      \"one\": \"{} ausgewählte Ansicht veröffentlichen\",\n      \"many\": \"{} ausgewählte Ansichten veröffentlichen\",\n      \"other\": \"{} ausgewählte Ansichten veröffentlichen\"\n    },\n    \"mustSelectPrimaryDatabase\": \"Die primäre Ansicht muss ausgewählt sein\",\n    \"noDatabaseSelected\": \"Keine Datenbank ausgewählt, bitte wähle mindestens eine Datenbank aus.\",\n    \"unableToDeselectPrimaryDatabase\": \"Die Auswahl der primären Datenbank kann nicht aufgehoben werden.\",\n    \"saveThisPage\": \"Diese Seite speichern\",\n    \"duplicateTitle\": \"Wo möchten Sie hinzufügen\",\n    \"selectWorkspace\": \"Einen Arbeitsbereich auswählen\",\n    \"addTo\": \"Hinzufügen zu\",\n    \"duplicateSuccessfully\": \"Duplizierung erfolgreich. Möchtest du die Dokumente anzeigen?\",\n    \"duplicateSuccessfullyDescription\": \"Du hast die App nicht? Dein Download beginnt automatisch, nachdem du auf „Herunterladen“ geklickt hast.\",\n    \"downloadIt\": \"Herunterladen\",\n    \"openApp\": \"In App öffnen\",\n    \"duplicateFailed\": \"Duplizierung fehlgeschlagen\",\n    \"membersCount\": {\n      \"zero\": \"Keine Mitglieder\",\n      \"one\": \"1 Mitglied\",\n      \"many\": \"{count} Mitglieder\",\n      \"other\": \"{count} Mitglieder\"\n    },\n    \"useThisTemplate\": \"Verwenden Sie die Vorlage\"\n  },\n  \"web\": {\n    \"continue\": \"Weiter\",\n    \"or\": \"oder\",\n    \"continueWithGoogle\": \"Weiter mit Google\",\n    \"continueWithGithub\": \"Weiter mit GitHub\",\n    \"continueWithDiscord\": \"Weiter mit Discord\",\n    \"continueWithApple\": \"Weiter mit Apple \",\n    \"moreOptions\": \"Weitere Optionen\",\n    \"signInAgreement\": \"Wenn du oben auf \\\"Weiter\\\" klickst, bestätigst du, dass\\ndu folgende Dokumente gelesen, verstanden und akzeptiert hast:\\nAppFlowys\",\n    \"and\": \"und\",\n    \"termOfUse\": \"Bedingungen\",\n    \"privacyPolicy\": \"Datenschutzrichtlinie\",\n    \"signInError\": \"Anmeldefehler\",\n    \"login\": \"Registrieren oder anmelden\",\n    \"fileBlock\": {\n      \"uploadedAt\": \"Hochgeladen am {Zeit}\",\n      \"linkedAt\": \"Link hinzugefügt am {Zeit}\",\n      \"empty\": \"Hochladen oder Einbetten einer Datei\"\n    },\n    \"importNotion\": \"Importieren aus Notion\",\n    \"import\": \"Import\",\n    \"importSuccess\": \"Erfolgreich hochgeladen\",\n    \"importFailed\": \"Import fehlgeschlagen, bitte überprüfen Sie das Dateiformat\"\n  },\n  \"globalComment\": {\n    \"comments\": \"Kommentare\",\n    \"addComment\": \"Einen Kommentar hinzufügen\",\n    \"reactedBy\": \"Reaktion von\",\n    \"addReaction\": \"Reaktion hinzufügen\",\n    \"reactedByMore\": \"und {count} andere\",\n    \"showSeconds\": {\n      \"one\": \"vor 1 Sekunde\",\n      \"other\": \"vor {count} Sekunden\",\n      \"zero\": \"Soeben\",\n      \"many\": \"vor {count} Sekunden\"\n    },\n    \"showMinutes\": {\n      \"one\": \"vor 1 Minute\",\n      \"other\": \"vor {count} Minuten\",\n      \"many\": \"vor {count} Minuten\"\n    },\n    \"showHours\": {\n      \"one\": \"vor 1 Stunde\",\n      \"other\": \"vor {count} Stunden\",\n      \"many\": \"vor {count} Stunden\"\n    },\n    \"showDays\": {\n      \"one\": \"vor 1 Tag\",\n      \"other\": \"vor {count} Tagen\",\n      \"many\": \"vor {count} Tagen\"\n    },\n    \"showMonths\": {\n      \"one\": \"vor 1 Monat\",\n      \"other\": \"vor {count} Monaten\",\n      \"many\": \"vor {count} Monaten\"\n    },\n    \"showYears\": {\n      \"one\": \"vor 1 Jahr\",\n      \"other\": \"vor {count} Jahren\",\n      \"many\": \"vor {count} Jahren\"\n    },\n    \"reply\": \"Antworten\",\n    \"deleteComment\": \"Kommentar löschen\",\n    \"youAreNotOwner\": \"Du bist nicht der Verfasser dieses Kommentars\",\n    \"confirmDeleteDescription\": \"Möchtest du diesen Kommentar wirklich löschen?\",\n    \"hasBeenDeleted\": \"Gelöscht\",\n    \"replyingTo\": \"Antwort auf\",\n    \"noAccessDeleteComment\": \"Du bist nicht berechtigt, diesen Kommentar zu löschen\",\n    \"collapse\": \"Zusammenklappen\",\n    \"readMore\": \"Mehr lesen\",\n    \"failedToAddComment\": \"Kommentar konnte nicht hinzugefügt werden\",\n    \"commentAddedSuccessfully\": \"Kommentar erfolgreich hinzugefügt.\",\n    \"commentAddedSuccessTip\": \"Du hast gerade einen Kommentar hinzugefügt oder darauf geantwortet. Möchtest du nach oben springen, um die neuesten Kommentare zu sehen?\"\n  },\n  \"template\": {\n    \"asTemplate\": \"Als Vorlage speichern\",\n    \"name\": \"Vorlagenname\",\n    \"description\": \"Vorlagenbeschreibung\",\n    \"requiredField\": \"{field} ist erforderlich\",\n    \"addCategory\": \"\\\"{category}\\\" hinzufügen\",\n    \"addNewCategory\": \"Neue Kategorie hinzufügen\",\n    \"deleteCategory\": \"Lösche Kategorie\",\n    \"editCategory\": \"Bearbeite Kategorie\",\n    \"category\": {\n      \"name\": \"Kategoriename\",\n      \"icon\": \"Kategorie-Icon\",\n      \"bgColor\": \"Kategorie-Hintergrundfarbe\",\n      \"priority\": \"Kategoriepriorität\",\n      \"desc\": \"Kategoriebeschreibung\",\n      \"type\": \"Kategorie-Typ\",\n      \"icons\": \"Kategorie-Icons\",\n      \"colors\": \"Kategoriefarbe\",\n      \"deleteCategory\": \"Lösche Kategorie\",\n      \"deleteCategoryDescription\": \"Möchten Sie diese Kategorie wirklich löschen?\",\n      \"typeToSearch\": \"Tippen, um nach Kategorien zu suchen...\"\n    }\n  },\n  \"fileDropzone\": {\n    \"dropFile\": \"Klicken oder ziehen Sie die Datei zum Hochladen in diesen Bereich\",\n    \"uploading\": \"Hochladen...\",\n    \"uploadFailed\": \"Hochladen fehlgeschlagen\",\n    \"uploadSuccess\": \"Hochladen erfolgreich\",\n    \"uploadSuccessDescription\": \"Die Datei wurde erfolgreich hochgeladen\",\n    \"uploadFailedDescription\": \"Das Hochladen der Datei ist fehlgeschlagen\",\n    \"uploadingDescription\": \"Die Datei wird hochgeladen\"\n  },\n  \"gallery\": {\n    \"preview\": \"Im Vollbildmodus öffnen\",\n    \"copy\": \"Kopiere\",\n    \"prev\": \"Vorherige\",\n    \"next\": \"Nächste\",\n    \"resetZoom\": \"Setze Zoom zurück\",\n    \"zoomIn\": \"Vergrößern\",\n    \"zoomOut\": \"Verkleinern\"\n  },\n  \"invitation\": {\n    \"joinWorkspace\": \"Arbeitsbereich beitreten\",\n    \"success\": \"Sie sind dem Arbeitsbereich erfolgreich beigetreten\",\n    \"successMessage\": \"Sie können nun auf alle darin enthaltenen Seiten und Arbeitsbereiche zugreifen.\",\n    \"openWorkspace\": \"Öffne AppFlowy\",\n    \"errorModal\": {\n      \"description\": \"Ihr aktuelles Konto {email} hat möglicherweise keinen Zugriff auf diesen Arbeitsbereich. Bitte melden Sie sich mit dem richtigen Konto an oder wenden Sie sich an den Eigentümer des Arbeitsbereichs, um Hilfe zu erhalten.\"\n    }\n  },\n  \"approveAccess\": {\n    \"title\": \"Anfrage zum Beitritt zum Arbeitsbereich genehmigen\",\n    \"requestSummary\": \"<user/> fragt den Beitritt zu <workspace/> und den Zugriff auf <page/> an.\"\n  },\n  \"time\": {\n    \"justNow\": \"Soeben\",\n    \"seconds\": {\n      \"one\": \"1 Sekunde\",\n      \"other\": \"{count} Sekunden\"\n    },\n    \"minutes\": {\n      \"one\": \"1 Minute\",\n      \"other\": \"{count} Minuten\"\n    },\n    \"hours\": {\n      \"one\": \"1 Stunde\",\n      \"other\": \"{count} Stunden\"\n    },\n    \"days\": {\n      \"one\": \"1 Tag\",\n      \"other\": \"{count} Tage\"\n    },\n    \"weeks\": {\n      \"one\": \"1 Woche\",\n      \"other\": \"{count} Wochen\"\n    },\n    \"months\": {\n      \"one\": \"1 Monat\",\n      \"other\": \"{count} Monate\"\n    },\n    \"years\": {\n      \"one\": \"1 Jahr\",\n      \"other\": \"{count} Jahre\"\n    },\n    \"ago\": \"vor\",\n    \"yesterday\": \"Gestern\",\n    \"today\": \"Heute\"\n  },\n  \"members\": {\n    \"zero\": \"Keine Mitglieder\",\n    \"one\": \"1 Mitglied\",\n    \"many\": \"{count} Mitglieder\",\n    \"other\": \"{count} Mitglieder\"\n  },\n  \"tabMenu\": {\n    \"close\": \"Schließen\",\n    \"closeOthers\": \"Schließe andere Tabs\",\n    \"favoriteDisabledHint\": \"Diese Ansicht kann nicht als Favorit markiert werden\"\n  },\n  \"openFileMessage\": {\n    \"success\": \"Datei erfolgreich geöffnet\",\n    \"fileNotFound\": \"Datei nicht gefunden\",\n    \"permissionDenied\": \"Keine Berechtigung zum Öffnen dieser Datei\",\n    \"unknownError\": \"Öffnen der Datei fehlgeschlagen\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/el-GR.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Me\",\n  \"welcomeText\": \"Καλωσορίσατε στο @:appName\",\n  \"welcomeTo\": \"Καλωσορίσατε στο\",\n  \"githubStarText\": \"Star on GitHub\",\n  \"subscribeNewsletterText\": \"Εγγραφείτε στο Newsletter\",\n  \"letsGoButtonText\": \"Γρήγορη Εκκίνηση\",\n  \"title\": \"Τίτλος\",\n  \"youCanAlso\": \"Μπορείτε επίσης\",\n  \"and\": \"και\",\n  \"failedToOpenUrl\": \"Αποτυχία ανοίγματος διεύθυνσης url: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Κάντε κλικ για να προσθέσετε παρακάτω\",\n    \"addAboveCmd\": \"Alt+click\",\n    \"addAboveMacCmd\": \"Option+click\",\n    \"addAboveTooltip\": \"για να προσθέσετε παραπάνω\",\n    \"dragTooltip\": \"Σύρετε για μετακίνηση\",\n    \"openMenuTooltip\": \"Κάντε κλικ για άνοιγμα μενού\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Εγγραφή\",\n    \"title\": \"Εγγραφείτε στο @:appName\",\n    \"getStartedText\": \"Ξεκινήστε\",\n    \"emptyPasswordError\": \"Ο κωδικός πρόσβασης δεν μπορεί να είναι κενός\",\n    \"repeatPasswordEmptyError\": \"Η επανάληψη κωδικού πρόσβασης δεν μπορεί να είναι κενή\",\n    \"unmatchedPasswordError\": \"Η επανάληψη κωδικού πρόσβασης δεν είναι ίδια με τον κωδικό πρόσβασης\",\n    \"alreadyHaveAnAccount\": \"Έχετε ήδη λογαριασμό;\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Κωδικός\",\n    \"repeatPasswordHint\": \"Επαναλάβετε τον κωδικό πρόσβασης\",\n    \"signUpWith\": \"Εγγραφή με:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Συνδεθείτε στο @:appName\",\n    \"loginButtonText\": \"Σύνδεση\",\n    \"loginStartWithAnonymous\": \"Έναρξη με ανώνυμη συνεδρία\",\n    \"continueAnonymousUser\": \"Συνέχεια με ανώνυμη συνεδρία\",\n    \"buttonText\": \"Είσοδος\",\n    \"signingInText\": \"Πραγματοποιείται σύνδεση...\",\n    \"forgotPassword\": \"Ξεχάσατε το κωδικό;\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Κωδικός\",\n    \"dontHaveAnAccount\": \"Δεν έχετε λογαριασμό;\",\n    \"repeatPasswordEmptyError\": \"Η επανάληψη κωδικού πρόσβασης δεν μπορεί να είναι κενή\",\n    \"unmatchedPasswordError\": \"Η επανάληψη κωδικού πρόσβασης δεν είναι ίδια με τον κωδικό πρόσβασης\",\n    \"syncPromptMessage\": \"Ο συγχρονισμός των δεδομένων μπορεί να διαρκέσει λίγο. Παρακαλώ μην κλείσετε αυτήν τη σελίδα\",\n    \"or\": \"- Ή -\",\n    \"LogInWithGoogle\": \"Σύνδεση μέσω Google\",\n    \"LogInWithGithub\": \"Σύνδεση μέσω Github\",\n    \"LogInWithDiscord\": \"Σύνδεση μέσω Discord\",\n    \"signInWith\": \"Συνδεθείτε με:\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Επιλέξτε το χώρο εργασίας σας\",\n    \"create\": \"Δημιουργία χώρου εργασίας\",\n    \"reset\": \"Επαναφορά χώρου εργασίας\",\n    \"resetWorkspacePrompt\": \"Η επαναφορά του χώρου εργασίας θα διαγράψει όλες τις σελίδες και τα δεδομένα μέσα σε αυτό. Είστε βέβαιοι ότι θέλετε να επαναφέρετε το χώρο εργασίας? Εναλλακτικά, μπορείτε να επικοινωνήσετε με την ομάδα υποστήριξης για να επαναφέρετε το χώρο εργασίας\",\n    \"hint\": \"workspace\",\n    \"notFoundError\": \"Workspace not found\",\n    \"failedToLoad\": \"Something went wrong! Failed to load the workspace. Try to close any open instance of AppFlowy and try again.\",\n    \"errorActions\": {\n      \"reportIssue\": \"Report an issue\",\n      \"reportIssueOnGithub\": \"Report an issue on Github\",\n      \"exportLogFiles\": \"Export log files\",\n      \"reachOut\": \"Reach out on Discord\"\n    },\n    \"deleteWorkspaceHintText\": \"Are you sure you want to delete the workspace? This action cannot be undone.\",\n    \"createSuccess\": \"Workspace created successfully\",\n    \"createFailed\": \"Failed to create workspace\",\n    \"deleteSuccess\": \"Workspace deleted successfully\",\n    \"deleteFailed\": \"Failed to delete workspace\",\n    \"openSuccess\": \"Open workspace successfully\",\n    \"openFailed\": \"Failed to open workspace\",\n    \"renameSuccess\": \"Workspace renamed successfully\",\n    \"renameFailed\": \"Failed to rename workspace\",\n    \"updateIconSuccess\": \"Updated workspace icon successfully\",\n    \"updateIconFailed\": \"Updated workspace icon failed\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Share\",\n    \"workInProgress\": \"Coming soon\",\n    \"markdown\": \"Markdown\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Copy Link\"\n  },\n  \"moreAction\": {\n    \"small\": \"small\",\n    \"medium\": \"medium\",\n    \"large\": \"large\",\n    \"fontSize\": \"Font size\",\n    \"import\": \"Import\",\n    \"moreOptions\": \"More options\",\n    \"wordCount\": \"Word count: {}\",\n    \"charCount\": \"Character count: {}\",\n    \"createdAt\": \"Created: {}\",\n    \"deleteView\": \"Delete\",\n    \"duplicateView\": \"Duplicate\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Text & Markdown\",\n    \"documentFromV010\": \"Document from v0.1.0\",\n    \"databaseFromV010\": \"Database from v0.1.0\",\n    \"csv\": \"CSV\",\n    \"database\": \"Database\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Rename\",\n    \"delete\": \"Delete\",\n    \"duplicate\": \"Duplicate\",\n    \"unfavorite\": \"Remove from favorites\",\n    \"favorite\": \"Προσθήκη στα αγαπημένα\",\n    \"openNewTab\": \"Άνοιγμα σε νέα καρτέλα\",\n    \"moveTo\": \"Μετακίνηση στο\",\n    \"addToFavorites\": \"Προσθήκη στα Αγαπημένα\",\n    \"copyLink\": \"Αντιγραφή Συνδέσμου\"\n  },\n  \"blankPageTitle\": \"Κενή σελίδα\",\n  \"newPageText\": \"Νέα σελίδα\",\n  \"newDocumentText\": \"Νέο έγγραφο\",\n  \"newGridText\": \"Νέο πλέγμα\",\n  \"newCalendarText\": \"Νέο ημερολόγιο\",\n  \"newBoardText\": \"Νέος πίνακας\",\n  \"trash\": {\n    \"text\": \"Κάδος απορριμμάτων\",\n    \"restoreAll\": \"Επαναφορά Όλων\",\n    \"deleteAll\": \"Διαγραφή Όλων\",\n    \"pageHeader\": {\n      \"fileName\": \"Όνομα αρχείου\",\n      \"lastModified\": \"Τελευταία Τροποποίηση\",\n      \"created\": \"Δημιουργήθηκε\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Είστε βέβαιοι οτι θέλετε να διαγράψετε όλες τις σελίδες στον κάδο απορριμμάτων;\",\n      \"caption\": \"Αυτή η ενέργεια δεν μπορεί να ανεραιθεί.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Είστε βέβαιοι οτι θέλετε να επαναφέρετε όλες τις σελίδες στον κάδο απορριμμάτων;\",\n      \"caption\": \"Αυτή η ενέργεια δεν μπορεί να ανεραιθεί.\"\n    },\n    \"mobile\": {\n      \"actions\": \"Ενέργειες Απορριμμάτων\",\n      \"empty\": \"Ο κάδος απορριμμάτων είναι άδειος\",\n      \"emptyDescription\": \"Δεν έχετε διαγράψει κανένα αρχείο\",\n      \"isDeleted\": \"έχει διαγραφεί\",\n      \"isRestored\": \"έγινε επαναφορά\"\n    }\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Αυτή η σελίδα βρίσκεται στον κάδο απορριμμάτων\",\n    \"restore\": \"Επαναφορά σελίδας\",\n    \"deletePermanent\": \"Οριστική διαγραφή\"\n  },\n  \"dialogCreatePageNameHint\": \"Όνομα σελίδας\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Συντομεύσεις\",\n    \"whatsNew\": \"Τι νέο υπάρχει;\",\n    \"help\": \"Βοήθεια & Υποστήριξη\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"Debug Info\",\n      \"success\": \"Copied debug info to clipboard!\",\n      \"fail\": \"Unable to copy debug info to clipboard\"\n    },\n    \"feedback\": \"Σχόλια\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Αφαίρεση, μετονομασία και άλλα...\",\n    \"addPageTooltip\": \"Γρήγορη προσθήκη σελίδας\",\n    \"defaultNewPageName\": \"Χωρίς τίτλο\",\n    \"renameDialog\": \"Μετονομασία\"\n  },\n  \"noPagesInside\": \"Δεν υπάρχουν σελίδες\",\n  \"toolbar\": {\n    \"undo\": \"Αναίρεση\",\n    \"redo\": \"Επαναφορά\",\n    \"bold\": \"Έντονo\",\n    \"italic\": \"Πλάγια\",\n    \"underline\": \"Υπογράμμιση\",\n    \"strike\": \"Διακριτή διαγραφή\",\n    \"numList\": \"Αριθμημένη λίστα\",\n    \"bulletList\": \"Bulleted list\",\n    \"checkList\": \"Check List\",\n    \"inlineCode\": \"Inline Code\",\n    \"quote\": \"Quote Block\",\n    \"header\": \"Header\",\n    \"highlight\": \"Highlight\",\n    \"color\": \"Color\",\n    \"addLink\": \"Add Link\",\n    \"link\": \"Link\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Switch to Light mode\",\n    \"darkMode\": \"Switch to Dark mode\",\n    \"openAsPage\": \"Open as a Page\",\n    \"addNewRow\": \"Add a new row\",\n    \"openMenu\": \"Click to open menu\",\n    \"dragRow\": \"Long press to reorder the row\",\n    \"viewDataBase\": \"View database\",\n    \"referencePage\": \"This {name} is referenced\",\n    \"addBlockBelow\": \"Add a block below\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Close sidebar\",\n    \"openSidebar\": \"Open sidebar\",\n    \"personal\": \"Personal\",\n    \"favorites\": \"Favorites\",\n    \"clickToHidePersonal\": \"Click to hide personal section\",\n    \"clickToHideFavorites\": \"Click to hide favorite section\",\n    \"addAPage\": \"Add a page\",\n    \"recent\": \"Recent\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Exported Note To Markdown\",\n      \"path\": \"Documents/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Contacts\",\n    \"whatsHappening\": \"What's happening this week?\",\n    \"addContact\": \"Add Contact\",\n    \"editContact\": \"Edit Contact\"\n  },\n  \"button\": {\n    \"ok\": \"OK\",\n    \"done\": \"Done\",\n    \"cancel\": \"Cancel\",\n    \"signIn\": \"Sign In\",\n    \"signOut\": \"Sign Out\",\n    \"complete\": \"Complete\",\n    \"save\": \"Save\",\n    \"generate\": \"Generate\",\n    \"esc\": \"ESC\",\n    \"keep\": \"Keep\",\n    \"tryAgain\": \"Try again\",\n    \"discard\": \"Discard\",\n    \"replace\": \"Replace\",\n    \"insertBelow\": \"Insert below\",\n    \"insertAbove\": \"Εισαγωγή από επάνω\",\n    \"upload\": \"Μεταφόρτωση\",\n    \"edit\": \"Επεξεργασία\",\n    \"delete\": \"Διαγραφή\",\n    \"duplicate\": \"Δημιουργία διπλότυπου\",\n    \"putback\": \"Βάλτε Πίσω\",\n    \"update\": \"Ενημέρωση\",\n    \"share\": \"Κοινοποίηση\",\n    \"removeFromFavorites\": \"Κατάργηση από τα αγαπημένα\",\n    \"addToFavorites\": \"Προσθήκη στα αγαπημένα\",\n    \"rename\": \"Μετονομασία\",\n    \"helpCenter\": \"Κέντρο Βοήθειας\",\n    \"add\": \"Προσθήκη\",\n    \"yes\": \"Ναι\",\n    \"clear\": \"Καθαρισμός\",\n    \"remove\": \"Αφαίρεση\",\n    \"dontRemove\": \"Να μην αφαιρεθεί\",\n    \"copyLink\": \"Αντιγραφή Συνδέσμου\",\n    \"align\": \"Στοίχιση\",\n    \"login\": \"Σύνδεση\",\n    \"logout\": \"Αποσύνδεση\",\n    \"deleteAccount\": \"Διαγραφή λογαριασμού\",\n    \"back\": \"Πίσω\",\n    \"signInGoogle\": \"Συνδεθείτε μέσω λογαριασμού Google\",\n    \"signInGithub\": \"Συνδεθείτε μέσω λογαριασμού Github\",\n    \"signInDiscord\": \"Συνδεθείτε μέσω λογαριασμού Discord\"\n  },\n  \"label\": {\n    \"welcome\": \"Καλώς ήρθατε!\",\n    \"firstName\": \"Όνομα\",\n    \"middleName\": \"Μεσαίο όνομα\",\n    \"lastName\": \"Επώνυμο\",\n    \"stepX\": \"Step {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Αδυναμία σύνδεσης στο λογαριασμό σας.\",\n      \"failedMsg\": \"Παρακαλούμε βεβαιωθείτε ότι έχετε ολοκληρώσει τη διαδικασία εισόδου στο πρόγραμμα περιήγησης.\"\n    },\n    \"google\": {\n      \"title\": \"GOOGLE SIGN-IN\",\n      \"instruction1\": \"Για να εισαγάγετε τις Επαφές Google σας, θα πρέπει να εξουσιοδοτήσετε αυτήν την εφαρμογή χρησιμοποιώντας το πρόγραμμα περιήγησής σας.\",\n      \"instruction2\": \"Αντιγράψτε αυτόν τον κώδικα στο πρόχειρο κάνοντας κλικ στο εικονίδιο ή επιλέγοντας το κείμενο:\",\n      \"instruction3\": \"Μεταβείτε στον ακόλουθο σύνδεσμο στο πρόγραμμα περιήγησής σας και πληκτρολογήστε τον παραπάνω κωδικό:\",\n      \"instruction4\": \"Πατήστε το κουμπί παρακάτω όταν ολοκληρώσετε την εγγραφή:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Ρυθμίσεις\",\n    \"menu\": {\n      \"appearance\": \"Εμφάνιση\",\n      \"language\": \"Γλώσσα\",\n      \"user\": \"Χρήστης\",\n      \"files\": \"Αρχεία\",\n      \"notifications\": \"Ειδοποιήσεις\",\n      \"open\": \"Άνοιγμα Ρυθμίσεων\",\n      \"logout\": \"Αποσυνδέση\",\n      \"logoutPrompt\": \"Είστε σίγουροι ότι θέλετε να αποσυνδεθείτε;\",\n      \"selfEncryptionLogoutPrompt\": \"Είστε σίγουροι ότι θέλετε να αποσυνδεθείτε; Παρακαλούμε βεβαιωθείτε ότι έχετε αντιγράψει το κρυπτογραφημένο μυστικό\",\n      \"syncSetting\": \"Ρυθμίσεις συγχρονισμού\",\n      \"cloudSettings\": \"Ρυθμίσεις Cloud\",\n      \"enableSync\": \"Enable sync\",\n      \"enableEncrypt\": \"Encrypt data\",\n      \"cloudURL\": \"Base URL\",\n      \"invalidCloudURLScheme\": \"Invalid Scheme\",\n      \"cloudServerType\": \"Cloud server\",\n      \"cloudServerTypeTip\": \"Please note that it might log out your current account after switching the cloud server\",\n      \"cloudLocal\": \"Local\",\n      \"cloudAppFlowy\": \"AppFlowy Cloud\",\n      \"cloudAppFlowySelfHost\": \"AppFlowy Cloud Self-hosted\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"The cloud url can't be empty\",\n      \"clickToCopy\": \"Click to copy\",\n      \"selfHostStart\": \"If you don't have a server, please refer to the\",\n      \"selfHostContent\": \"document\",\n      \"selfHostEnd\": \"for guidance on how to self-host your own server\",\n      \"cloudURLHint\": \"Input the base URL of your server\",\n      \"cloudWSURL\": \"Websocket URL\",\n      \"cloudWSURLHint\": \"Input the websocket address of your server\",\n      \"restartApp\": \"Restart\",\n      \"restartAppTip\": \"Restart the application for the changes to take effect. Please note that this might log out your current account\",\n      \"changeServerTip\": \"After changing the server, you must click the restart button for the changes to take effect\",\n      \"enableEncryptPrompt\": \"Activate encryption to secure your data with this secret. Store it safely; once enabled, it can't be turned off. If lost, your data becomes irretrievable. Click to copy\",\n      \"inputEncryptPrompt\": \"Please enter your encryption secret for\",\n      \"clickToCopySecret\": \"Click to copy secret\",\n      \"configServerSetting\": \"Configurate your server settings\",\n      \"configServerGuide\": \"After selecting `Quick Start`, navigate to `Settings` and then \\\"Cloud Settings\\\" to configure your self-hosted server.\",\n      \"inputTextFieldHint\": \"Your secret\",\n      \"historicalUserList\": \"User login history\",\n      \"historicalUserListTooltip\": \"This list displays your anonymous accounts. You can click on an account to view its details. Anonymous accounts are created by clicking the 'Get Started' button\",\n      \"openHistoricalUser\": \"Click to open the anonymous account\",\n      \"customPathPrompt\": \"Storing the AppFlowy data folder in a cloud-synced folder such as Google Drive can pose risks. If the database within this folder is accessed or modified from multiple locations at the same time, it may result in synchronization conflicts and potential data corruption\",\n      \"importAppFlowyData\": \"Import Data from External AppFlowy Folder\",\n      \"importingAppFlowyDataTip\": \"Data import is in progress. Please do not close the app\",\n      \"importAppFlowyDataDescription\": \"Copy data from an external AppFlowy data folder and import it into the current AppFlowy data folder\",\n      \"importSuccess\": \"Successfully imported the AppFlowy data folder\",\n      \"importFailed\": \"Importing the AppFlowy data folder failed\",\n      \"importGuide\": \"For further details, please check the referenced document\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Enable notifications\",\n        \"hint\": \"Turn off to stop local notifications from appearing.\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"Reset\",\n      \"fontFamily\": {\n        \"label\": \"Font Family\",\n        \"search\": \"Search\"\n      },\n      \"themeMode\": {\n        \"label\": \"Theme Mode\",\n        \"light\": \"Light Mode\",\n        \"dark\": \"Σκοτεινό Θέμα\",\n        \"system\": \"Προσαρμογή στο σύστημα\"\n      },\n      \"fontScaleFactor\": \"Font Scale Factor\",\n      \"documentSettings\": {\n        \"cursorColor\": \"Χρώμα κέρσορα εγγράφου\",\n        \"selectionColor\": \"Χρώμα επιλογής κειμένου\",\n        \"hexEmptyError\": \"Το χρώμα σε δεκαεξαδική μορφή δεν μπορεί να είναι κενό\",\n        \"hexLengthError\": \"Η τιμή δεκαεξαδικού πρέπει να είναι 6 ψηφία\",\n        \"hexInvalidError\": \"Μη έγκυρη τιμή δεκαεξαδικού\",\n        \"opacityEmptyError\": \"Η διαφάνεια δεν μπορεί να είναι κενή\",\n        \"opacityRangeError\": \"Η διαφάνεια πρέπει να είναι μεταξύ 1 και 100\",\n        \"app\": \"Εφαρμογή\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"Apply\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Κατεύθυνση Διάταξης\",\n        \"hint\": \"Ελέγξτε τη ροή του περιεχομένου στην οθόνη σας, από αριστερά προς τα δεξιά ή δεξιά προς τα αριστερά.\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\"\n      },\n      \"textDirection\": {\n        \"label\": \"Προεπιλεγμένη κατεύθυνση κειμένου\",\n        \"hint\": \"Καθορίστε αν το κείμενο θα ξεκινά από αριστερά ή δεξιά ως προεπιλογή.\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\",\n        \"auto\": \"AUTO\",\n        \"fallback\": \"Ίδια με την κατεύθυνση διάταξης\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Μεταφόρτωση\",\n        \"uploadTheme\": \"Μεταφόρτωση θέματος\",\n        \"description\": \"Ανεβάστε το δικό σας θέμα για το AppFlowy χρησιμοποιώντας το παρακάτω κουμπί.\",\n        \"loading\": \"Παρακαλώ περιμένετε ενώ επικυρώνουμε και ανεβάζουμε το θέμα σας...\",\n        \"uploadSuccess\": \"Το θέμα σας μεταφορτώθηκε με επιτυχία\",\n        \"deletionFailure\": \"Αποτυχία διαγραφής του θέματος. Προσπαθήστε να το διαγράψετε χειροκίνητα.\",\n        \"filePickerDialogTitle\": \"Επιλέξτε ένα αρχείο .flowy_plugin\",\n        \"urlUploadFailure\": \"Αποτυχία ανοίγματος url: {}\"\n      },\n      \"theme\": \"Θέμα\",\n      \"builtInsLabel\": \"Ενσωματωμένα Θέματα\",\n      \"pluginsLabel\": \"Πρόσθετα\",\n      \"dateFormat\": {\n        \"label\": \"Μορφή ημερομηνίας\",\n        \"local\": \"Τοπική\",\n        \"us\": \"US\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"Friendly\",\n        \"dmy\": \"D/M/Y\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Μορφή ώρας\",\n        \"twelveHour\": \"12 ώρες\",\n        \"twentyFourHour\": \"24 ώρες\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"Εμφάνιση διαλόγου ονομασίας κατά τη δημιουργία μιας σελίδας\",\n      \"enableRTLToolbarItems\": \"Enable RTL toolbar items\",\n      \"members\": {\n        \"title\": \"Members Settings\",\n        \"inviteMembers\": \"Πρόσκληση Μέλους\",\n        \"sendInvite\": \"Αποστολή Πρόσκλησης\",\n        \"copyInviteLink\": \"Αντιγραφή Συνδέσμου Πρόσκλησης\",\n        \"label\": \"Μέλη\",\n        \"user\": \"User\",\n        \"role\": \"Role\",\n        \"removeFromWorkspace\": \"Remove from Workspace\",\n        \"owner\": \"Owner\",\n        \"guest\": \"Guest\",\n        \"member\": \"Member\",\n        \"memberHintText\": \"A member can read, comment, and edit pages. Invite members and guests.\",\n        \"guestHintText\": \"A Guest can read, react, comment, and can edit certain pages with permission.\",\n        \"emailInvalidError\": \"Invalid email, please check and try again\",\n        \"emailSent\": \"Email sent, please check the inbox\",\n        \"members\": \"members\"\n      }\n    },\n    \"files\": {\n      \"copy\": \"Copy\",\n      \"defaultLocation\": \"Read files and data storage location\",\n      \"exportData\": \"Export your data\",\n      \"doubleTapToCopy\": \"Double tap to copy the path\",\n      \"restoreLocation\": \"Restore to AppFlowy default path\",\n      \"customizeLocation\": \"Open another folder\",\n      \"restartApp\": \"Please restart app for the changes to take effect.\",\n      \"exportDatabase\": \"Export database\",\n      \"selectFiles\": \"Select the files that need to be export\",\n      \"selectAll\": \"Select all\",\n      \"deselectAll\": \"Deselect all\",\n      \"createNewFolder\": \"Create a new folder\",\n      \"createNewFolderDesc\": \"Tell us where you want to store your data\",\n      \"defineWhereYourDataIsStored\": \"Define where your data is stored\",\n      \"open\": \"Open\",\n      \"openFolder\": \"Open an existing folder\",\n      \"openFolderDesc\": \"Read and write it to your existing AppFlowy folder\",\n      \"folderHintText\": \"folder name\",\n      \"location\": \"Creating a new folder\",\n      \"locationDesc\": \"Pick a name for your AppFlowy data folder\",\n      \"browser\": \"Browse\",\n      \"create\": \"Create\",\n      \"set\": \"Set\",\n      \"folderPath\": \"Path to store your folder\",\n      \"locationCannotBeEmpty\": \"Path cannot be empty\",\n      \"pathCopiedSnackbar\": \"File storage path copied to clipboard!\",\n      \"changeLocationTooltips\": \"Change the data directory\",\n      \"change\": \"Αλλαγή\",\n      \"openLocationTooltips\": \"Open another data directory\",\n      \"openCurrentDataFolder\": \"Άνοιγμα του τρέχοντος φακέλου\",\n      \"recoverLocationTooltips\": \"Reset to AppFlowy's default data directory\",\n      \"exportFileSuccess\": \"Επιτυχής εξαγωγή αρχείου!\",\n      \"exportFileFail\": \"Η εξαγωγή αρχείου απέτυχε!\",\n      \"export\": \"Εξαγωγή\",\n      \"clearCache\": \"Εκκαθάριση προσωρινής μνήμης\",\n      \"clearCacheDesc\": \"Αν αντιμετωπίζετε προβλήματα με εικόνες που δεν φορτώνουν ή γραμματοσειρές που δεν εμφανίζονται σωστά, δοκιμάστε να καθαρίσετε την προσωρινή μνήμη. Αυτή η ενέργεια δεν θα διαγράψει τα δεδομένα χρήστη σας.\",\n      \"areYouSureToClearCache\": \"Σίγουρα θέλετε να καθαρίσετε την προσωρινή μνήμη;\",\n      \"clearCacheSuccess\": \"Επιτυχής εκκαθάριση προσωρινής μνήμης!\"\n    },\n    \"user\": {\n      \"name\": \"Όνομα\",\n      \"email\": \"Email\",\n      \"tooltipSelectIcon\": \"Select icon\",\n      \"selectAnIcon\": \"Select an icon\",\n      \"pleaseInputYourOpenAIKey\": \"παρακαλώ εισάγετε το AI κλειδί σας\",\n      \"pleaseInputYourStabilityAIKey\": \"παρακαλώ εισάγετε το Stability AI κλειδί σας\",\n      \"clickToLogout\": \"Κάντε κλικ για αποσύνδεση του τρέχοντος χρήστη\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"Συντομεύσεις\",\n      \"command\": \"Command\",\n      \"keyBinding\": \"Keybinding\",\n      \"addNewCommand\": \"Προσθήκη Νέας Εντολής\",\n      \"updateShortcutStep\": \"Πατήστε τον επιθυμητό συνδυασμό πλήκτρων και πατήστε ENTER\",\n      \"shortcutIsAlreadyUsed\": \"Αυτή η συντόμευση χρησιμοποιείται ήδη για: {conflict}\",\n      \"resetToDefault\": \"Επαναφορά προεπιλεγμένων συντομεύσεων πληκτρολογίου\",\n      \"couldNotLoadErrorMsg\": \"Αδυναμία φόρτωσης συντομεύσεων, Προσπαθήστε ξανά\",\n      \"couldNotSaveErrorMsg\": \"Δεν ήταν δυνατή η αποθήκευση συντομεύσεων, Προσπαθήστε ξανά\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"Προσωπικά Στοιχεία\",\n      \"username\": \"Όνομα Χρήστη\",\n      \"usernameEmptyError\": \"Το όνομα χρήστη δεν μπορεί να είναι κενό\",\n      \"about\": \"Σχετικά\",\n      \"pushNotifications\": \"Ειδοποιήσεις Push\",\n      \"support\": \"Υποστήριξη\",\n      \"joinDiscord\": \"Ελάτε μαζί μας στο Discord\",\n      \"privacyPolicy\": \"Πολιτική Απορρήτου\",\n      \"userAgreement\": \"Όροι Χρήσης\",\n      \"termsAndConditions\": \"Όροι και Προϋποθέσεις\",\n      \"userprofileError\": \"Αποτυχία φόρτωσης προφίλ χρήστη\",\n      \"userprofileErrorDescription\": \"Παρακαλώ προσπαθήστε να αποσυνδεθείτε και να συνδεθείτε ξανά για να ελέγξετε αν το πρόβλημα εξακολουθεί να υπάρχει.\",\n      \"selectLayout\": \"Επιλέξτε διάταξη\",\n      \"selectStartingDay\": \"Επιλέξτε ημέρα έναρξης\",\n      \"version\": \"Έκδοση\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν τη προβολή;\",\n    \"createView\": \"Νέο\",\n    \"title\": {\n      \"placeholder\": \"Χωρίς τίτλο\"\n    },\n    \"settings\": {\n      \"filter\": \"Φίλτρο\",\n      \"sort\": \"Ταξινόμηση\",\n      \"sortBy\": \"Ταξινόμηση κατά\",\n      \"properties\": \"Properties\",\n      \"reorderPropertiesTooltip\": \"Drag to reorder properties\",\n      \"group\": \"Group\",\n      \"addFilter\": \"Add Filter\",\n      \"deleteFilter\": \"Delete filter\",\n      \"filterBy\": \"Filter by...\",\n      \"typeAValue\": \"Type a value...\",\n      \"layout\": \"Layout\",\n      \"databaseLayout\": \"Layout\",\n      \"viewList\": {\n        \"zero\": \"0 views\",\n        \"one\": \"{count} view\",\n        \"other\": \"{count} views\"\n      },\n      \"editView\": \"Edit View\",\n      \"boardSettings\": \"Board settings\",\n      \"calendarSettings\": \"Calendar settings\",\n      \"createView\": \"New view\",\n      \"duplicateView\": \"Duplicate view\",\n      \"deleteView\": \"Delete view\",\n      \"numberOfVisibleFields\": \"{} shown\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Contains\",\n      \"doesNotContain\": \"Does not contain\",\n      \"endsWith\": \"Ends with\",\n      \"startWith\": \"Starts with\",\n      \"is\": \"Is\",\n      \"isNot\": \"Is not\",\n      \"isEmpty\": \"Is empty\",\n      \"isNotEmpty\": \"Is not empty\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Not\",\n        \"startWith\": \"Starts with\",\n        \"endWith\": \"Ends with\",\n        \"isEmpty\": \"is empty\",\n        \"isNotEmpty\": \"is not empty\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Checked\",\n      \"isUnchecked\": \"Unchecked\",\n      \"choicechipPrefix\": {\n        \"is\": \"is\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"is complete\",\n      \"isIncomplted\": \"is incomplete\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"Is\",\n      \"isNot\": \"Is not\",\n      \"contains\": \"Contains\",\n      \"doesNotContain\": \"Does not contain\",\n      \"isEmpty\": \"Είναι κενό\",\n      \"isNotEmpty\": \"Δεν είναι κενό\"\n    },\n    \"dateFilter\": {\n      \"is\": \"Is\",\n      \"before\": \"Is before\",\n      \"after\": \"Is after\",\n      \"onOrBefore\": \"Is on or before\",\n      \"onOrAfter\": \"Is on or after\",\n      \"between\": \"Is between\",\n      \"empty\": \"Είναι κενό\",\n      \"notEmpty\": \"Δεν είναι κενό\",\n      \"choicechipPrefix\": {\n        \"before\": \"Before\",\n        \"after\": \"After\",\n        \"onOrBefore\": \"On or before\",\n        \"onOrAfter\": \"On or after\",\n        \"isEmpty\": \"Is empty\",\n        \"isNotEmpty\": \"Is not empty\"\n      }\n    },\n    \"numberFilter\": {\n      \"equal\": \"Equals\",\n      \"notEqual\": \"Δεν ισούται με\",\n      \"lessThan\": \"Είναι μικρότερο από\",\n      \"greaterThan\": \"Είναι μεγαλύτερο από\",\n      \"lessThanOrEqualTo\": \"Είναι μικρότερο από ή ίσο με\",\n      \"greaterThanOrEqualTo\": \"Είναι μεγαλύτερο από ή ίσο με\",\n      \"isEmpty\": \"Είναι κενό\",\n      \"isNotEmpty\": \"Δεν είναι κενό\"\n    },\n    \"field\": {\n      \"hide\": \"Απόκρυψη\",\n      \"show\": \"Εμφάνιση\",\n      \"insertLeft\": \"Εισαγωγή από αριστερά\",\n      \"insertRight\": \"Εισαγωγή από δεξιά\",\n      \"duplicate\": \"Διπλότυπο\",\n      \"delete\": \"Διαγραφή\",\n      \"textFieldName\": \"Κείμενο\",\n      \"checkboxFieldName\": \"Checkbox\",\n      \"dateFieldName\": \"Date\",\n      \"updatedAtFieldName\": \"Τελευταία τροποποίηση\",\n      \"createdAtFieldName\": \"Δημιουργήθηκε στις\",\n      \"numberFieldName\": \"Numbers\",\n      \"singleSelectFieldName\": \"Επιλογή\",\n      \"multiSelectFieldName\": \"Multiselect\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Checklist\",\n      \"relationFieldName\": \"Relation\",\n      \"numberFormat\": \"Μορφή αριθμού\",\n      \"dateFormat\": \"Μορφή ημερομηνίας\",\n      \"includeTime\": \"Περιλαμβάνει χρόνο\",\n      \"isRange\": \"End date\",\n      \"dateFormatFriendly\": \"Μήνας Ημέρα, Έτος\",\n      \"dateFormatISO\": \"Έτος-Μήνας-Ημέρα\",\n      \"dateFormatLocal\": \"Μήνας/Ημέρα/Έτος\",\n      \"dateFormatUS\": \"Έτος/Μήνας/Ημέρα\",\n      \"dateFormatDayMonthYear\": \"Ημέρα/Μήνας/Έτος\",\n      \"timeFormat\": \"Time format\",\n      \"invalidTimeFormat\": \"Invalid format\",\n      \"timeFormatTwelveHour\": \"12 hour\",\n      \"timeFormatTwentyFourHour\": \"24 hour\",\n      \"clearDate\": \"Clear date\",\n      \"dateTime\": \"Date time\",\n      \"startDateTime\": \"Start date time\",\n      \"endDateTime\": \"End date time\",\n      \"failedToLoadDate\": \"Failed to load date value\",\n      \"selectTime\": \"Select time\",\n      \"selectDate\": \"Select date\",\n      \"visibility\": \"Visibility\",\n      \"propertyType\": \"Property type\",\n      \"addSelectOption\": \"Add an option\",\n      \"typeANewOption\": \"Type a new option\",\n      \"optionTitle\": \"Options\",\n      \"addOption\": \"Add option\",\n      \"editProperty\": \"Edit property\",\n      \"newProperty\": \"New property\",\n      \"deleteFieldPromptMessage\": \"Are you sure? This property will be deleted\",\n      \"newColumn\": \"New Column\",\n      \"format\": \"Format\",\n      \"reminderOnDateTooltip\": \"This cell has a scheduled reminder\",\n      \"optionAlreadyExist\": \"Option already exists\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Add a new field\",\n      \"fieldDragElementTooltip\": \"Click to open menu\",\n      \"showHiddenFields\": {\n        \"one\": \"Show {count} hidden field\",\n        \"many\": \"Show {count} hidden fields\",\n        \"other\": \"Show {count} hidden fields\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"Απόκρυψη {count} κρυφού πεδίου\",\n        \"many\": \"Απόκρυψη {count} κρυφών πεδίων\",\n        \"other\": \"Απόκρυψη {count} κρυφών πεδίων\"\n      }\n    },\n    \"sort\": {\n      \"ascending\": \"Αύξουσα\",\n      \"descending\": \"Φθίνουσα\",\n      \"by\": \"By\",\n      \"empty\": \"No active sorts\",\n      \"cannotFindCreatableField\": \"Αδυναμία εύρεσης κατάλληλου πεδίου για ταξινόμηση\",\n      \"deleteAllSorts\": \"Delete all sorts\",\n      \"addSort\": \"Add new sort\",\n      \"removeSorting\": \"Θα θέλατε να αφαιρέσετε τη ταξινόμηση;\",\n      \"fieldInUse\": \"You are already sorting by this field\"\n    },\n    \"row\": {\n      \"duplicate\": \"Duplicate\",\n      \"delete\": \"Διαγραφή στήλης\",\n      \"titlePlaceholder\": \"Χωρίς τίτλο\",\n      \"textPlaceholder\": \"Άδειο\",\n      \"copyProperty\": \"Copied property to clipboard\",\n      \"count\": \"Count\",\n      \"newRow\": \"Νέα γραμμή\",\n      \"action\": \"Action\",\n      \"add\": \"Click add to below\",\n      \"drag\": \"Σύρετε για μετακίνηση\",\n      \"dragAndClick\": \"Drag to move, click to open menu\",\n      \"insertRecordAbove\": \"Εισαγωγή εγγραφής επάνω\",\n      \"insertRecordBelow\": \"Εισαγωγή εγγραφής κάτω\"\n    },\n    \"selectOption\": {\n      \"create\": \"Δημιουργία\",\n      \"purpleColor\": \"Μωβ\",\n      \"pinkColor\": \"Ροζ\",\n      \"lightPinkColor\": \"Απαλό ροζ\",\n      \"orangeColor\": \"Πορτοκαλί\",\n      \"yellowColor\": \"Κίτρινο\",\n      \"limeColor\": \"Λάιμ\",\n      \"greenColor\": \"Πράσινο\",\n      \"aquaColor\": \"Θαλασσί\",\n      \"blueColor\": \"Μπλέ\",\n      \"deleteTag\": \"Διαγραφή ετικέτας\",\n      \"colorPanelTitle\": \"Χρώμα\",\n      \"panelTitle\": \"Select an option or create one\",\n      \"searchOption\": \"Search for an option\",\n      \"searchOrCreateOption\": \"Search or create an option...\",\n      \"createNew\": \"Δημιουργία νέας\",\n      \"orSelectOne\": \"Or select an option\",\n      \"typeANewOption\": \"Type a new option\",\n      \"tagName\": \"Όνομα ετικέτας\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"Περιγραφή εργασίας\",\n      \"addNew\": \"Προσθήκη νέας εργασίας\",\n      \"submitNewTask\": \"Δημιουργία\",\n      \"hideComplete\": \"Απόκρυψη ολοκληρωμένων εργασιών\",\n      \"showComplete\": \"Εμφάνιση όλων των εργασιών\"\n    },\n    \"url\": {\n      \"launch\": \"Άνοιγμα συνδέσμου στο πρόγραμμα περιήγησης\",\n      \"copy\": \"Copy link to clipboard\",\n      \"textFieldHint\": \"Enter a URL\"\n    },\n    \"relation\": {\n      \"relatedDatabasePlaceLabel\": \"Related Database\",\n      \"relatedDatabasePlaceholder\": \"None\",\n      \"inRelatedDatabase\": \"In\",\n      \"rowSearchTextFieldPlaceholder\": \"Search\",\n      \"noDatabaseSelected\": \"No database selected, please select one first from the list below:\",\n      \"emptySearchResult\": \"No records found\"\n    },\n    \"menuName\": \"Grid\",\n    \"referencedGridPrefix\": \"View of\",\n    \"calculate\": \"Calculate\",\n    \"calculationTypeLabel\": {\n      \"none\": \"None\",\n      \"average\": \"Average\",\n      \"max\": \"Max\",\n      \"median\": \"Median\",\n      \"min\": \"Min\",\n      \"sum\": \"Sum\",\n      \"count\": \"Count\",\n      \"countEmpty\": \"Count empty\",\n      \"countEmptyShort\": \"EMPTY\",\n      \"countNonEmpty\": \"Count not empty\",\n      \"countNonEmptyShort\": \"FILLED\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"Document\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Select a Board to link to\",\n        \"createANewBoard\": \"Create a new Board\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Select a Grid to link to\",\n        \"createANewGrid\": \"Create a new Grid\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Select a Calendar to link to\",\n        \"createANewCalendar\": \"Create a new Calendar\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"Select a Document to link to\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Outline\",\n      \"codeBlock\": \"Code Block\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Referenced Board\",\n      \"referencedGrid\": \"Referenced Grid\",\n      \"referencedCalendar\": \"Referenced Calendar\",\n      \"referencedDocument\": \"Referenced Document\",\n      \"autoGeneratorMenuItemName\": \"AI Writer\",\n      \"autoGeneratorTitleName\": \"AI: Ask AI to write anything...\",\n      \"autoGeneratorLearnMore\": \"Μάθετε περισσότερα\",\n      \"autoGeneratorGenerate\": \"Generate\",\n      \"autoGeneratorHintText\": \"Ρωτήστε Το AI ...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Αδυναμία λήψης κλειδιού AI\",\n      \"autoGeneratorRewrite\": \"Rewrite\",\n      \"smartEdit\": \"AI Assistants\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"Διόρθωση ορθογραφίας\",\n      \"warning\": \"⚠️ Οι απαντήσεις AI μπορεί να είναι ανακριβείς ή παραπλανητικές.\",\n      \"smartEditSummarize\": \"Summarize\",\n      \"smartEditImproveWriting\": \"Improve writing\",\n      \"smartEditMakeLonger\": \"Make longer\",\n      \"smartEditCouldNotFetchResult\": \"Could not fetch result from AI\",\n      \"smartEditCouldNotFetchKey\": \"Could not fetch AI key\",\n      \"smartEditDisabled\": \"Connect AI in Settings\",\n      \"discardResponse\": \"Do you want to discard the AI responses?\",\n      \"createInlineMathEquation\": \"Create equation\",\n      \"fonts\": \"Γραμματοσειρές\",\n      \"toggleList\": \"Toggle list\",\n      \"quoteList\": \"Quote list\",\n      \"numberedList\": \"Αριθμημένη λίστα\",\n      \"bulletedList\": \"Bulleted list\",\n      \"todoList\": \"Todo list\",\n      \"callout\": \"Callout\",\n      \"cover\": {\n        \"changeCover\": \"Change Cover\",\n        \"colors\": \"Χρώματα\",\n        \"images\": \"Εικόνες\",\n        \"clearAll\": \"Εκκαθάριση όλων\",\n        \"abstract\": \"Abstract\",\n        \"addCover\": \"Προσθέστε ένα εξώφυλλο\",\n        \"addLocalImage\": \"Add local image\",\n        \"invalidImageUrl\": \"Μη έγκυρο URL εικόνας\",\n        \"failedToAddImageToGallery\": \"Failed to add image to gallery\",\n        \"enterImageUrl\": \"Enter image URL\",\n        \"add\": \"Add\",\n        \"back\": \"Back\",\n        \"saveToGallery\": \"Save to gallery\",\n        \"removeIcon\": \"Remove icon\",\n        \"pasteImageUrl\": \"Paste image URL\",\n        \"or\": \"OR\",\n        \"pickFromFiles\": \"Pick from files\",\n        \"couldNotFetchImage\": \"Could not fetch image\",\n        \"imageSavingFailed\": \"Image Saving Failed\",\n        \"addIcon\": \"Add icon\",\n        \"changeIcon\": \"Change icon\",\n        \"coverRemoveAlert\": \"It will be removed from cover after it is deleted.\",\n        \"alertDialogConfirmation\": \"Are you sure, you want to continue?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"Math Equation\",\n        \"addMathEquation\": \"Add a TeX equation\",\n        \"editMathEquation\": \"Edit Math Equation\"\n      },\n      \"optionAction\": {\n        \"click\": \"Click\",\n        \"toOpenMenu\": \" to open menu\",\n        \"delete\": \"Delete\",\n        \"duplicate\": \"Duplicate\",\n        \"turnInto\": \"Turn into\",\n        \"moveUp\": \"Move up\",\n        \"moveDown\": \"Move down\",\n        \"color\": \"Color\",\n        \"align\": \"Align\",\n        \"left\": \"Left\",\n        \"center\": \"Center\",\n        \"right\": \"Right\",\n        \"defaultColor\": \"Default\",\n        \"depth\": \"Depth\"\n      },\n      \"image\": {\n        \"copiedToPasteBoard\": \"The image link has been copied to the clipboard\",\n        \"addAnImage\": \"Add an image\",\n        \"imageUploadFailed\": \"Image upload failed\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"The link has been copied to the clipboard\",\n        \"convertToLink\": \"Convert to embed link\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Add headings to create a table of contents.\",\n        \"noMatchHeadings\": \"No matching headings found.\"\n      },\n      \"table\": {\n        \"addAfter\": \"Add after\",\n        \"addBefore\": \"Add before\",\n        \"delete\": \"Delete\",\n        \"clear\": \"Clear content\",\n        \"duplicate\": \"Duplicate\",\n        \"bgColor\": \"Background color\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Copy\",\n        \"cut\": \"Cut\",\n        \"paste\": \"Paste\"\n      },\n      \"action\": \"Actions\",\n      \"database\": {\n        \"selectDataSource\": \"Select data source\",\n        \"noDataSource\": \"No data source\",\n        \"selectADataSource\": \"Select a data source\",\n        \"toContinue\": \"to continue\",\n        \"newDatabase\": \"New Database\",\n        \"linkToDatabase\": \"Link to Database\"\n      },\n      \"date\": \"Date\",\n      \"emoji\": \"Emoji\"\n    },\n    \"outlineBlock\": {\n      \"placeholder\": \"Table of Contents\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Type '/' for commands\"\n    },\n    \"title\": {\n      \"placeholder\": \"Untitled\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Click to add image\",\n      \"upload\": {\n        \"label\": \"Upload\",\n        \"placeholder\": \"Click to upload image\"\n      },\n      \"url\": {\n        \"label\": \"Image URL\",\n        \"placeholder\": \"Enter image URL\"\n      },\n      \"ai\": {\n        \"label\": \"Generate image from AI\",\n        \"placeholder\": \"Please input the prompt for AI to generate image\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Generate image from Stability AI\",\n        \"placeholder\": \"Please input the prompt for Stability AI to generate image\"\n      },\n      \"support\": \"Image size limit is 5MB. Supported formats: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Invalid image\",\n        \"invalidImageSize\": \"Image size must be less than 5MB\",\n        \"invalidImageFormat\": \"Image format is not supported. Supported formats: JPEG, PNG, JPG, GIF, SVG, WEBP\",\n        \"invalidImageUrl\": \"Invalid image URL\",\n        \"noImage\": \"No such file or directory\"\n      },\n      \"embedLink\": {\n        \"label\": \"Embed link\",\n        \"placeholder\": \"Paste or type an image link\"\n      },\n      \"unsplash\": {\n        \"label\": \"Unsplash\"\n      },\n      \"searchForAnImage\": \"Search for an image\",\n      \"pleaseInputYourOpenAIKey\": \"please input your AI key in Settings page\",\n      \"pleaseInputYourStabilityAIKey\": \"please input your Stability AI key in Settings page\",\n      \"saveImageToGallery\": \"Save image\",\n      \"failedToAddImageToGallery\": \"Failed to add image to gallery\",\n      \"successToAddImageToGallery\": \"Image added to gallery successfully\",\n      \"unableToLoadImage\": \"Unable to load image\",\n      \"maximumImageSize\": \"Maximum supported upload image size is 10MB\",\n      \"uploadImageErrorImageSizeTooBig\": \"Image size must be less than 10MB\",\n      \"imageIsUploading\": \"Image is uploading\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Language\",\n        \"placeholder\": \"Select language\"\n      }\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Paste or type a link\",\n      \"openInNewTab\": \"Open in new tab\",\n      \"copyLink\": \"Copy link\",\n      \"removeLink\": \"Remove link\",\n      \"url\": {\n        \"label\": \"Link URL\",\n        \"placeholder\": \"Enter link URL\"\n      },\n      \"title\": {\n        \"label\": \"Link Title\",\n        \"placeholder\": \"Enter link title\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"Mention a person or a page or date...\",\n      \"page\": {\n        \"label\": \"Link to page\",\n        \"tooltip\": \"Click to open page\"\n      },\n      \"deleted\": \"Deleted\",\n      \"deletedContent\": \"This content does not exist or has been deleted\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"Reset to default\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"The current version does not support this block.\",\n      \"blockContentHasBeenCopied\": \"The block content has been copied.\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"New\",\n      \"renameGroupTooltip\": \"Press to rename group\",\n      \"createNewColumn\": \"Add a new group\",\n      \"addToColumnTopTooltip\": \"Add a new card at the top\",\n      \"addToColumnBottomTooltip\": \"Add a new card at the bottom\",\n      \"renameColumn\": \"Rename\",\n      \"hideColumn\": \"Hide\",\n      \"newGroup\": \"New Group\",\n      \"deleteColumn\": \"Delete\",\n      \"deleteColumnConfirmation\": \"This will delete this group and all the cards in it.\\nAre you sure you want to continue?\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"Hidden Groups\",\n      \"collapseTooltip\": \"Hide the hidden groups\",\n      \"expandTooltip\": \"View the hidden groups\"\n    },\n    \"cardDetail\": \"Card Detail\",\n    \"cardActions\": \"Card Actions\",\n    \"cardDuplicated\": \"Card has been duplicated\",\n    \"cardDeleted\": \"Card has been deleted\",\n    \"showOnCard\": \"Show on card detail\",\n    \"setting\": \"Setting\",\n    \"propertyName\": \"Property name\",\n    \"menuName\": \"Board\",\n    \"showUngrouped\": \"Show ungrouped items\",\n    \"ungroupedButtonText\": \"Ungrouped\",\n    \"ungroupedButtonTooltip\": \"Contains cards that don't belong in any group\",\n    \"ungroupedItemsTitle\": \"Click to add to the board\",\n    \"groupBy\": \"Group by\",\n    \"referencedBoardPrefix\": \"View of\",\n    \"notesTooltip\": \"Notes inside\",\n    \"mobile\": {\n      \"editURL\": \"Edit URL\",\n      \"showGroup\": \"Show group\",\n      \"showGroupContent\": \"Are you sure you want to show this group on the board?\",\n      \"failedToLoad\": \"Failed to load board view\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Calendar\",\n    \"defaultNewCalendarTitle\": \"Untitled\",\n    \"newEventButtonTooltip\": \"Add a new event\",\n    \"navigation\": {\n      \"today\": \"Today\",\n      \"jumpToday\": \"Jump to Today\",\n      \"previousMonth\": \"Previous Month\",\n      \"nextMonth\": \"Next Month\"\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"No events yet\",\n      \"emptyBody\": \"Press the plus button to create an event on this day.\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Show week numbers\",\n      \"showWeekends\": \"Show weekends\",\n      \"firstDayOfWeek\": \"Start week on\",\n      \"layoutDateField\": \"Layout calendar by\",\n      \"changeLayoutDateField\": \"Change layout field\",\n      \"noDateTitle\": \"No Date\",\n      \"noDateHint\": {\n        \"zero\": \"Unscheduled events will show up here\",\n        \"one\": \"{count} unscheduled event\",\n        \"other\": \"{count} unscheduled events\"\n      },\n      \"unscheduledEventsTitle\": \"Unscheduled events\",\n      \"clickToAdd\": \"Click to add to the calendar\",\n      \"name\": \"Calendar settings\"\n    },\n    \"referencedCalendarPrefix\": \"View of\",\n    \"quickJumpYear\": \"Jump to\",\n    \"duplicateEvent\": \"Duplicate event\"\n  },\n  \"errorDialog\": {\n    \"title\": \"AppFlowy Error\",\n    \"howToFixFallback\": \"We're sorry for the inconvenience! Submit an issue on our GitHub page that describes your error.\",\n    \"github\": \"View on GitHub\"\n  },\n  \"search\": {\n    \"label\": \"Search\",\n    \"placeholder\": {\n      \"actions\": \"Search actions...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Copied!\",\n      \"fail\": \"Unable to copy\"\n    }\n  },\n  \"unSupportBlock\": \"The current version does not support this Block.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Are you sure want to delete the {pageType}?\",\n    \"deleteContentCaption\": \"if you delete this {pageType}, you can restore it from the trash.\"\n  },\n  \"colors\": {\n    \"custom\": \"Custom\",\n    \"default\": \"Default\",\n    \"red\": \"Red\",\n    \"orange\": \"Orange\",\n    \"yellow\": \"Yellow\",\n    \"green\": \"Green\",\n    \"blue\": \"Blue\",\n    \"purple\": \"Purple\",\n    \"pink\": \"Pink\",\n    \"brown\": \"Brown\",\n    \"gray\": \"Gray\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"Emoji\",\n    \"search\": \"Search emoji\",\n    \"noRecent\": \"No recent emoji\",\n    \"noEmojiFound\": \"No emoji found\",\n    \"filter\": \"Filter\",\n    \"random\": \"Random\",\n    \"selectSkinTone\": \"Select skin tone\",\n    \"remove\": \"Remove emoji\",\n    \"categories\": {\n      \"smileys\": \"Smileys & Emotion\",\n      \"people\": \"People & Body\",\n      \"animals\": \"Animals & Nature\",\n      \"food\": \"Food & Drink\",\n      \"activities\": \"Activities\",\n      \"places\": \"Travel & Places\",\n      \"objects\": \"Objects\",\n      \"symbols\": \"Symbols\",\n      \"flags\": \"Flags\",\n      \"nature\": \"Nature\",\n      \"frequentlyUsed\": \"Frequently Used\"\n    },\n    \"skinTone\": {\n      \"default\": \"Default\",\n      \"light\": \"Light\",\n      \"mediumLight\": \"Medium-Light\",\n      \"medium\": \"Medium\",\n      \"mediumDark\": \"Medium-Dark\",\n      \"dark\": \"Dark\"\n    }\n  },\n  \"inlineActions\": {\n    \"noResults\": \"No results\",\n    \"pageReference\": \"Page reference\",\n    \"docReference\": \"Document reference\",\n    \"boardReference\": \"Board reference\",\n    \"calReference\": \"Calendar reference\",\n    \"gridReference\": \"Grid reference\",\n    \"date\": \"Date\",\n    \"reminder\": {\n      \"groupTitle\": \"Reminder\",\n      \"shortKeyword\": \"remind\"\n    }\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"Change the date and time format in settings\",\n    \"dateFormat\": \"Date format\",\n    \"includeTime\": \"Include time\",\n    \"isRange\": \"End date\",\n    \"timeFormat\": \"Time format\",\n    \"clearDate\": \"Clear date\",\n    \"reminderLabel\": \"Reminder\",\n    \"selectReminder\": \"Select reminder\",\n    \"reminderOptions\": {\n      \"none\": \"None\",\n      \"atTimeOfEvent\": \"Time of event\",\n      \"fiveMinsBefore\": \"5 mins before\",\n      \"tenMinsBefore\": \"10 mins before\",\n      \"fifteenMinsBefore\": \"15 mins before\",\n      \"thirtyMinsBefore\": \"30 mins before\",\n      \"oneHourBefore\": \"1 hour before\",\n      \"twoHoursBefore\": \"2 hours before\",\n      \"onDayOfEvent\": \"On day of event\",\n      \"oneDayBefore\": \"1 day before\",\n      \"twoDaysBefore\": \"2 days before\",\n      \"oneWeekBefore\": \"1 week before\",\n      \"custom\": \"Custom\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"Yesterday\",\n    \"today\": \"Today\",\n    \"tomorrow\": \"Tomorrow\",\n    \"oneWeek\": \"1 week\"\n  },\n  \"notificationHub\": {\n    \"title\": \"Notifications\",\n    \"mobile\": {\n      \"title\": \"Updates\"\n    },\n    \"emptyTitle\": \"All caught up!\",\n    \"emptyBody\": \"No pending notifications or actions. Enjoy the calm.\",\n    \"tabs\": {\n      \"inbox\": \"Inbox\",\n      \"upcoming\": \"Upcoming\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"Mark all as read\",\n      \"showAll\": \"All\",\n      \"showUnreads\": \"Unread\"\n    },\n    \"filters\": {\n      \"ascending\": \"Ascending\",\n      \"descending\": \"Descending\",\n      \"groupByDate\": \"Group by date\",\n      \"showUnreadsOnly\": \"Show unreads only\",\n      \"resetToDefault\": \"Reset to default\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"Reminder\",\n    \"message\": \"Remember to check this before you forget!\",\n    \"tooltipDelete\": \"Delete\",\n    \"tooltipMarkRead\": \"Mark as read\",\n    \"tooltipMarkUnread\": \"Mark as unread\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"Find\",\n    \"previousMatch\": \"Previous match\",\n    \"nextMatch\": \"Next match\",\n    \"close\": \"Close\",\n    \"replace\": \"Replace\",\n    \"replaceAll\": \"Replace all\",\n    \"noResult\": \"No results\",\n    \"caseSensitive\": \"Case sensitive\",\n    \"searchMore\": \"Search to find more results\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"We're sorry\",\n    \"loadingViewError\": \"We're having trouble loading this view. Please check your internet connection, refresh the app, and do not hesitate to reach out to the team if the issue continues.\"\n  },\n  \"editor\": {\n    \"bold\": \"Bold\",\n    \"bulletedList\": \"Bulleted list\",\n    \"bulletedListShortForm\": \"Bulleted\",\n    \"checkbox\": \"Checkbox\",\n    \"embedCode\": \"Embed Code\",\n    \"heading1\": \"H1\",\n    \"heading2\": \"H2\",\n    \"heading3\": \"H3\",\n    \"highlight\": \"Highlight\",\n    \"color\": \"Color\",\n    \"image\": \"Image\",\n    \"date\": \"Date\",\n    \"italic\": \"Italic\",\n    \"link\": \"Link\",\n    \"numberedList\": \"Numbered list\",\n    \"numberedListShortForm\": \"Numbered\",\n    \"quote\": \"Quote\",\n    \"strikethrough\": \"Strikethrough\",\n    \"text\": \"Text\",\n    \"underline\": \"Underline\",\n    \"fontColorDefault\": \"Default\",\n    \"fontColorGray\": \"Gray\",\n    \"fontColorBrown\": \"Brown\",\n    \"fontColorOrange\": \"Orange\",\n    \"fontColorYellow\": \"Yellow\",\n    \"fontColorGreen\": \"Green\",\n    \"fontColorBlue\": \"Blue\",\n    \"fontColorPurple\": \"Purple\",\n    \"fontColorPink\": \"Pink\",\n    \"fontColorRed\": \"Red\",\n    \"backgroundColorDefault\": \"Default background\",\n    \"backgroundColorGray\": \"Gray background\",\n    \"backgroundColorBrown\": \"Brown background\",\n    \"backgroundColorOrange\": \"Orange background\",\n    \"backgroundColorYellow\": \"Yellow background\",\n    \"backgroundColorGreen\": \"Green background\",\n    \"backgroundColorBlue\": \"Blue background\",\n    \"backgroundColorPurple\": \"Purple background\",\n    \"backgroundColorPink\": \"Pink background\",\n    \"backgroundColorRed\": \"Red background\",\n    \"backgroundColorLime\": \"Lime background\",\n    \"backgroundColorAqua\": \"Aqua background\",\n    \"done\": \"Done\",\n    \"cancel\": \"Cancel\",\n    \"tint1\": \"Tint 1\",\n    \"tint2\": \"Tint 2\",\n    \"tint3\": \"Tint 3\",\n    \"tint4\": \"Tint 4\",\n    \"tint5\": \"Tint 5\",\n    \"tint6\": \"Tint 6\",\n    \"tint7\": \"Tint 7\",\n    \"tint8\": \"Tint 8\",\n    \"tint9\": \"Tint 9\",\n    \"lightLightTint1\": \"Purple\",\n    \"lightLightTint2\": \"Pink\",\n    \"lightLightTint3\": \"Light Pink\",\n    \"lightLightTint4\": \"Orange\",\n    \"lightLightTint5\": \"Yellow\",\n    \"lightLightTint6\": \"Lime\",\n    \"lightLightTint7\": \"Green\",\n    \"lightLightTint8\": \"Aqua\",\n    \"lightLightTint9\": \"Blue\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"Heading 1\",\n    \"mobileHeading2\": \"Heading 2\",\n    \"mobileHeading3\": \"Heading 3\",\n    \"textColor\": \"Text Color\",\n    \"backgroundColor\": \"Background Color\",\n    \"addYourLink\": \"Add your link\",\n    \"openLink\": \"Open link\",\n    \"copyLink\": \"Copy link\",\n    \"removeLink\": \"Remove link\",\n    \"editLink\": \"Edit link\",\n    \"linkText\": \"Text\",\n    \"linkTextHint\": \"Please enter text\",\n    \"linkAddressHint\": \"Please enter URL\",\n    \"highlightColor\": \"Highlight color\",\n    \"clearHighlightColor\": \"Clear highlight color\",\n    \"customColor\": \"Custom color\",\n    \"hexValue\": \"Hex value\",\n    \"opacity\": \"Opacity\",\n    \"resetToDefaultColor\": \"Reset to default color\",\n    \"ltr\": \"LTR\",\n    \"rtl\": \"RTL\",\n    \"auto\": \"Auto\",\n    \"cut\": \"Cut\",\n    \"copy\": \"Copy\",\n    \"paste\": \"Paste\",\n    \"find\": \"Find\",\n    \"previousMatch\": \"Previous match\",\n    \"nextMatch\": \"Next match\",\n    \"closeFind\": \"Close\",\n    \"replace\": \"Replace\",\n    \"replaceAll\": \"Replace all\",\n    \"regex\": \"Regex\",\n    \"caseSensitive\": \"Case sensitive\",\n    \"uploadImage\": \"Upload Image\",\n    \"urlImage\": \"URL Image\",\n    \"incorrectLink\": \"Incorrect Link\",\n    \"upload\": \"Upload\",\n    \"chooseImage\": \"Choose an image\",\n    \"loading\": \"Loading\",\n    \"imageLoadFailed\": \"Could not load the image\",\n    \"divider\": \"Divider\",\n    \"table\": \"Table\",\n    \"colAddBefore\": \"Add before\",\n    \"rowAddBefore\": \"Add before\",\n    \"colAddAfter\": \"Add after\",\n    \"rowAddAfter\": \"Add after\",\n    \"colRemove\": \"Remove\",\n    \"rowRemove\": \"Remove\",\n    \"colDuplicate\": \"Duplicate\",\n    \"rowDuplicate\": \"Duplicate\",\n    \"colClear\": \"Clear Content\",\n    \"rowClear\": \"Clear Content\",\n    \"slashPlaceHolder\": \"Type '/' to insert a block, or start typing\",\n    \"typeSomething\": \"Type something...\",\n    \"toggleListShortForm\": \"Toggle\",\n    \"quoteListShortForm\": \"Quote\",\n    \"mathEquationShortForm\": \"Formula\",\n    \"codeBlockShortForm\": \"Code\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"No favorite page\",\n    \"noFavoriteHintText\": \"Swipe the page to the left to add it to your favorites\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"Enter a / to insert a block, or start typing\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"To-do\",\n    \"bulletList\": \"List\",\n    \"numberList\": \"List\",\n    \"quote\": \"Quote\",\n    \"heading\": \"Heading {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"Page icon\",\n    \"language\": \"Language\",\n    \"font\": \"Font\",\n    \"actions\": \"Actions\",\n    \"date\": \"Date\",\n    \"addField\": \"Add field\",\n    \"userIcon\": \"User icon\"\n  },\n  \"noLogFiles\": \"There're no log files\",\n  \"newSettings\": {\n    \"myAccount\": {\n      \"title\": \"My account\",\n      \"subtitle\": \"Customize your profile, manage account security, open AI keys, or login into your account.\",\n      \"profileLabel\": \"Account name & Profile image\",\n      \"profileNamePlaceholder\": \"Enter your name\",\n      \"accountSecurity\": \"Account security\",\n      \"2FA\": \"2-Step Authentication\",\n      \"aiKeys\": \"AI keys\",\n      \"accountLogin\": \"Account Login\",\n      \"updateNameError\": \"Failed to update name\",\n      \"updateIconError\": \"Failed to update icon\",\n      \"deleteAccount\": {\n        \"title\": \"Delete Account\",\n        \"subtitle\": \"Permanently delete your account and all of your data.\",\n        \"deleteMyAccount\": \"Delete my account\",\n        \"dialogTitle\": \"Delete account\",\n        \"dialogContent1\": \"Are you sure you want to permanently delete your account?\",\n        \"dialogContent2\": \"This action cannot be undone, and will remove access from all teamspaces, erasing your entire account, including private workspaces, and removing you from all shared workspaces.\"\n      }\n    },\n    \"workplace\": {\n      \"name\": \"Workplace\",\n      \"title\": \"Workplace Settings\",\n      \"subtitle\": \"Customize your workspace appearance, theme, font, text layout, date, time, and language.\",\n      \"workplaceName\": \"Workplace name\",\n      \"workplaceNamePlaceholder\": \"Enter workplace name\",\n      \"workplaceIcon\": \"Workplace icon\",\n      \"workplaceIconSubtitle\": \"Upload an image or use an emoji for your workspace. Icon will show in your sidebar and notifications\",\n      \"renameError\": \"Failed to rename workplace\",\n      \"updateIconError\": \"Failed to update icon\",\n      \"appearance\": {\n        \"name\": \"Appearance\",\n        \"themeMode\": {\n          \"auto\": \"Auto\",\n          \"light\": \"Light\",\n          \"dark\": \"Dark\"\n        },\n        \"language\": \"Language\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/resources/translations/en-GB.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Me\",\n  \"welcomeText\": \"Welcome to @:appName\",\n  \"welcomeTo\": \"Welcome to\",\n  \"githubStarText\": \"Star on GitHub\",\n  \"subscribeNewsletterText\": \"Subscribe to Newsletter\",\n  \"letsGoButtonText\": \"Quick Start\",\n  \"title\": \"Title\",\n  \"youCanAlso\": \"You can also\",\n  \"and\": \"and\",\n  \"failedToOpenUrl\": \"Failed to open url: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Click to add below\",\n    \"addAboveCmd\": \"Alt+click\",\n    \"addAboveMacCmd\": \"Option+click\",\n    \"addAboveTooltip\": \"to add above\",\n    \"dragTooltip\": \"Drag to move\",\n    \"openMenuTooltip\": \"Click to open menu\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Sign Up\",\n    \"title\": \"Sign Up to @:appName\",\n    \"getStartedText\": \"Get Started\",\n    \"emptyPasswordError\": \"Password can't be empty\",\n    \"repeatPasswordEmptyError\": \"Repeat password can't be empty\",\n    \"unmatchedPasswordError\": \"Repeat password is not the same as password\",\n    \"alreadyHaveAnAccount\": \"Already have an account?\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Password\",\n    \"repeatPasswordHint\": \"Repeat password\",\n    \"signUpWith\": \"Sign up with:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Login to @:appName\",\n    \"loginButtonText\": \"Login\",\n    \"loginStartWithAnonymous\": \"Continue with an anonymous session\",\n    \"continueAnonymousUser\": \"Continue with an anonymous session\",\n    \"buttonText\": \"Sign In\",\n    \"signingInText\": \"Signing in...\",\n    \"forgotPassword\": \"Forgot Password?\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Password\",\n    \"dontHaveAnAccount\": \"Don't have an account?\",\n    \"createAccount\": \"Create account\",\n    \"repeatPasswordEmptyError\": \"Repeat password can't be empty\",\n    \"unmatchedPasswordError\": \"Repeat password is not the same as password\",\n    \"syncPromptMessage\": \"Syncing the data might take a while. Please don't close this page\",\n    \"or\": \"OR\",\n    \"signInWithGoogle\": \"Continue with Google\",\n    \"signInWithGithub\": \"Continue with GitHub\",\n    \"signInWithDiscord\": \"Continue with Discord\",\n    \"signInWithApple\": \"Continue with Apple\",\n    \"continueAnotherWay\": \"Continue another way\",\n    \"signUpWithGoogle\": \"Sign up with Google\",\n    \"signUpWithGithub\": \"Sign up with Github\",\n    \"signUpWithDiscord\": \"Sign up with Discord\",\n    \"signInWith\": \"Continue with:\",\n    \"signInWithEmail\": \"Continue with Email\",\n    \"signInWithMagicLink\": \"Continue\",\n    \"signUpWithMagicLink\": \"Sign up with Magic Link\",\n    \"pleaseInputYourEmail\": \"Please enter your email address\",\n    \"settings\": \"Settings\",\n    \"magicLinkSent\": \"Magic Link sent!\",\n    \"invalidEmail\": \"Please enter a valid email address\",\n    \"alreadyHaveAnAccount\": \"Already have an account?\",\n    \"logIn\": \"Log in\",\n    \"generalError\": \"Something went wrong. Please try again later\",\n    \"limitRateError\": \"For security reasons, you can only request a magic link every 60 seconds\",\n    \"magicLinkSentDescription\": \"A Magic Link was sent to your email. Click the link to complete your login. The link will expire after 5 minutes.\",\n    \"anonymous\": \"Anonymous\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Choose your workspace\",\n    \"defaultName\": \"My Workspace\",\n    \"create\": \"Create workspace\",\n    \"new\": \"New workspace\",\n    \"importFromNotion\": \"Import from Notion\",\n    \"learnMore\": \"Learn more\",\n    \"reset\": \"Reset workspace\",\n    \"renameWorkspace\": \"Rename workspace\",\n    \"workspaceNameCannotBeEmpty\": \"Workspace name cannot be empty\",\n    \"resetWorkspacePrompt\": \"Resetting the workspace will delete all pages and data within it. Are you sure you want to reset the workspace? Alternatively, you can contact the support team to restore the workspace\",\n    \"hint\": \"workspace\",\n    \"notFoundError\": \"Workspace not found\",\n    \"failedToLoad\": \"Something went wrong! Failed to load the workspace. Try to close any open instance of @:appName and try again.\",\n    \"errorActions\": {\n      \"reportIssue\": \"Report an issue\",\n      \"reportIssueOnGithub\": \"Report an issue on Github\",\n      \"exportLogFiles\": \"Export log files\",\n      \"reachOut\": \"Reach out on Discord\"\n    },\n    \"menuTitle\": \"Workspaces\",\n    \"deleteWorkspaceHintText\": \"Are you sure you want to delete the workspace? This action cannot be undone, and any pages you have published will be unpublished.\",\n    \"createSuccess\": \"Workspace created successfully\",\n    \"createFailed\": \"Failed to create workspace\",\n    \"createLimitExceeded\": \"You've reached the maximum workspace limit allowed for your account. If you need additional workspaces to continue your work, please request on Github\",\n    \"deleteSuccess\": \"Workspace deleted successfully\",\n    \"deleteFailed\": \"Failed to delete workspace\",\n    \"openSuccess\": \"Open workspace successfully\",\n    \"openFailed\": \"Failed to open workspace\",\n    \"renameSuccess\": \"Workspace renamed successfully\",\n    \"renameFailed\": \"Failed to rename workspace\",\n    \"updateIconSuccess\": \"Updated workspace icon successfully\",\n    \"updateIconFailed\": \"Updated workspace icon failed\",\n    \"cannotDeleteTheOnlyWorkspace\": \"Cannot delete the only workspace\",\n    \"fetchWorkspacesFailed\": \"Failed to fetch workspaces\",\n    \"leaveCurrentWorkspace\": \"Leave workspace\",\n    \"leaveCurrentWorkspacePrompt\": \"Are you sure you want to leave the current workspace?\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Share\",\n    \"workInProgress\": \"Coming soon\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"Copy to clipboard\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Copy link\",\n    \"publishToTheWeb\": \"Publish to Web\",\n    \"publishToTheWebHint\": \"Create a website with AppFlowy\",\n    \"publish\": \"Publish\",\n    \"unPublish\": \"Unpublish\",\n    \"visitSite\": \"Visit site\",\n    \"exportAsTab\": \"Export as\",\n    \"publishTab\": \"Publish\",\n    \"shareTab\": \"Share\",\n    \"publishOnAppFlowy\": \"Publish on AppFlowy\",\n    \"shareTabTitle\": \"Invite to collaborate\",\n    \"shareTabDescription\": \"For easy collaboration with anyone\",\n    \"copyLinkSuccess\": \"Copied link to clipboard\",\n    \"copyShareLink\": \"Copy share link\",\n    \"copyLinkFailed\": \"Failed to copy link to clipboard\",\n    \"copyLinkToBlockSuccess\": \"Copied block link to clipboard\",\n    \"copyLinkToBlockFailed\": \"Failed to copy block link to clipboard\",\n    \"manageAllSites\": \"Manage all sites\",\n    \"updatePathName\": \"Update path name\"\n  },\n  \"moreAction\": {\n    \"small\": \"small\",\n    \"medium\": \"medium\",\n    \"large\": \"large\",\n    \"fontSize\": \"Font size\",\n    \"import\": \"Import\",\n    \"moreOptions\": \"More options\",\n    \"wordCount\": \"Word count: {}\",\n    \"charCount\": \"Character count: {}\",\n    \"createdAt\": \"Created: {}\",\n    \"deleteView\": \"Delete\",\n    \"duplicateView\": \"Duplicate\",\n    \"wordCountLabel\": \"Word count: \",\n    \"charCountLabel\": \"Character count: \",\n    \"createdAtLabel\": \"Created: \",\n    \"syncedAtLabel\": \"Synced: \",\n    \"saveAsNewPage\": \"Add messages to page\",\n    \"saveAsNewPageDisabled\": \"No messages available\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Text & Markdown\",\n    \"documentFromV010\": \"Document from v0.1.0\",\n    \"databaseFromV010\": \"Database from v0.1.0\",\n    \"notionZip\": \"Notion Exported Zip File\",\n    \"csv\": \"CSV\",\n    \"database\": \"Database\"\n  },\n  \"emojiIconPicker\": {\n    \"iconUploader\": {\n      \"placeholderLeft\": \"Drag & drop a file, click to \",\n      \"placeholderUpload\": \"Upload\",\n      \"placeholderRight\": \", or paste an image link.\",\n      \"dropToUpload\": \"Drop a file to upload\",\n      \"change\": \"Change\"\n    }\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Rename\",\n    \"delete\": \"Delete\",\n    \"duplicate\": \"Duplicate\",\n    \"unfavorite\": \"Remove from Favourites\",\n    \"favorite\": \"Add to Favourites\",\n    \"openNewTab\": \"Open in a new tab\",\n    \"moveTo\": \"Move to\",\n    \"addToFavorites\": \"Add to Favourites\",\n    \"copyLink\": \"Copy link\",\n    \"changeIcon\": \"Change icon\",\n    \"collapseAllPages\": \"Collapse all subpages\",\n    \"movePageTo\": \"Move page to\",\n    \"move\": \"Move\",\n    \"lockPage\": \"Lock page\"\n  },\n  \"blankPageTitle\": \"Blank page\",\n  \"newPageText\": \"New page\",\n  \"newDocumentText\": \"New document\",\n  \"newGridText\": \"New grid\",\n  \"newCalendarText\": \"New calendar\",\n  \"newBoardText\": \"New board\",\n  \"chat\": {\n    \"newChat\": \"AI Chat\",\n    \"inputMessageHint\": \"Ask @:appName AI\",\n    \"inputLocalAIMessageHint\": \"Ask @:appName Local AI\",\n    \"unsupportedCloudPrompt\": \"This feature is only available when using @:appName Cloud\",\n    \"relatedQuestion\": \"Suggested\",\n    \"serverUnavailable\": \"Connection lost. Please check your internet and\",\n    \"aiServerUnavailable\": \"The AI service is temporarily unavailable. Please try again later.\",\n    \"retry\": \"Retry\",\n    \"clickToRetry\": \"Click to retry\",\n    \"regenerateAnswer\": \"Regenerate\",\n    \"question1\": \"How to use Kanban to manage tasks\",\n    \"question2\": \"Explain the GTD method\",\n    \"question3\": \"Why use Rust\",\n    \"question4\": \"Recipe with what's in my kitchen\",\n    \"question5\": \"Create an illustration for my page\",\n    \"question6\": \"Draw up a to-do list for my upcoming week\",\n    \"aiMistakePrompt\": \"AI can make mistakes. Check important info.\",\n    \"chatWithFilePrompt\": \"Do you want to chat with the file?\",\n    \"indexFileSuccess\": \"Indexing file successfully\",\n    \"inputActionNoPages\": \"No page results\",\n    \"referenceSource\": {\n      \"zero\": \"0 sources found\",\n      \"one\": \"{count} source found\",\n      \"other\": \"{count} sources found\"\n    },\n    \"clickToMention\": \"Mention a page\",\n    \"uploadFile\": \"Attach PDFs, text or markdown files\",\n    \"questionDetail\": \"Hi {}! How can I help you today?\",\n    \"indexingFile\": \"Indexing {}\",\n    \"generatingResponse\": \"Generating response\",\n    \"selectSources\": \"Select Sources\",\n    \"currentPage\": \"Current page\",\n    \"sourcesLimitReached\": \"You can only select up to 3 top-level documents and its children\",\n    \"sourceUnsupported\": \"We don't support chatting with databases at this time\",\n    \"regenerate\": \"Try again\",\n    \"addToPageButton\": \"Add message to page\",\n    \"addToPageTitle\": \"Add message to...\",\n    \"addToNewPage\": \"Create new page\",\n    \"addToNewPageName\": \"Messages extracted from \\\"{}\\\"\",\n    \"addToNewPageSuccessToast\": \"Message added to\",\n    \"openPagePreviewFailedToast\": \"Failed to open page\",\n    \"changeFormat\": {\n      \"actionButton\": \"Change format\",\n      \"confirmButton\": \"Regenerate with this format\",\n      \"textOnly\": \"Text\",\n      \"imageOnly\": \"Image only\",\n      \"textAndImage\": \"Text and Image\",\n      \"text\": \"Paragraph\",\n      \"bullet\": \"Bullet list\",\n      \"number\": \"Numbered list\",\n      \"table\": \"Table\",\n      \"blankDescription\": \"Format response\",\n      \"defaultDescription\": \"Auto response format\",\n      \"textWithImageDescription\": \"@:chat.changeFormat.text with image\",\n      \"numberWithImageDescription\": \"@:chat.changeFormat.number with image\",\n      \"bulletWithImageDescription\": \"@:chat.changeFormat.bullet with image\",\n      \"tableWithImageDescription\": \"@:chat.changeFormat.table with image\"\n    },\n    \"switchModel\": {\n      \"label\": \"Switch model\",\n      \"localModel\": \"Local Model\",\n      \"cloudModel\": \"Cloud Model\",\n      \"autoModel\": \"Auto\"\n    },\n    \"selectBanner\": {\n      \"saveButton\": \"Add to …\",\n      \"selectMessages\": \"Select messages\",\n      \"nSelected\": \"{} selected\",\n      \"allSelected\": \"All selected\"\n    },\n    \"stopTooltip\": \"Stop generating\"\n  },\n  \"trash\": {\n    \"text\": \"Bin\",\n    \"restoreAll\": \"Restore All\",\n    \"restore\": \"Restore\",\n    \"deleteAll\": \"Delete All\",\n    \"pageHeader\": {\n      \"fileName\": \"File name\",\n      \"lastModified\": \"Last Modified\",\n      \"created\": \"Created\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"All pages in bin\",\n      \"caption\": \"Are you sure you want to delete everything in Bin? This action cannot be undone.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Restore all pages in bin\",\n      \"caption\": \"This action cannot be undone.\"\n    },\n    \"restorePage\": {\n      \"title\": \"Restore: {}\",\n      \"caption\": \"Are you sure you want to restore this page?\"\n    },\n    \"mobile\": {\n      \"actions\": \"Bin Actions\",\n      \"empty\": \"No pages or spaces in Bin\",\n      \"emptyDescription\": \"Move things you don't need to the Bin.\",\n      \"isDeleted\": \"is deleted\",\n      \"isRestored\": \"is restored\"\n    },\n    \"confirmDeleteTitle\": \"Are you sure you want to delete this page permanently?\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"This page is in Bin\",\n    \"restore\": \"Restore page\",\n    \"deletePermanent\": \"Delete permanently\",\n    \"deletePermanentDescription\": \"Are you sure you want to delete this page permanently? This is irreversible.\"\n  },\n  \"dialogCreatePageNameHint\": \"Page name\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Shortcuts\",\n    \"whatsNew\": \"What's new?\",\n    \"helpAndDocumentation\": \"Help & documentation\",\n    \"getSupport\": \"Get support\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"Debug Info\",\n      \"success\": \"Copied debug info to clipboard!\",\n      \"fail\": \"Unable to copy debug info to clipboard\"\n    },\n    \"feedback\": \"Feedback\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Remove, rename, and more...\",\n    \"addPageTooltip\": \"Quickly add a page inside\",\n    \"defaultNewPageName\": \"Untitled\",\n    \"renameDialog\": \"Rename\",\n    \"pageNameSuffix\": \"Copy\"\n  },\n  \"noPagesInside\": \"No pages inside\",\n  \"toolbar\": {\n    \"undo\": \"Undo\",\n    \"redo\": \"Redo\",\n    \"bold\": \"Bold\",\n    \"italic\": \"Italic\",\n    \"underline\": \"Underline\",\n    \"strike\": \"Strikethrough\",\n    \"numList\": \"Numbered list\",\n    \"bulletList\": \"Bulleted list\",\n    \"checkList\": \"Checklist\",\n    \"inlineCode\": \"Inline Code\",\n    \"quote\": \"Quote Block\",\n    \"header\": \"Header\",\n    \"highlight\": \"Highlight\",\n    \"color\": \"Colour\",\n    \"addLink\": \"Add Link\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Switch to Light mode\",\n    \"darkMode\": \"Switch to Dark mode\",\n    \"openAsPage\": \"Open as a Page\",\n    \"addNewRow\": \"Add a new row\",\n    \"openMenu\": \"Click to open menu\",\n    \"dragRow\": \"Drag to reorder the row\",\n    \"viewDataBase\": \"View database\",\n    \"referencePage\": \"This {name} is referenced\",\n    \"addBlockBelow\": \"Add a block below\",\n    \"aiGenerate\": \"Generate\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Close sidebar\",\n    \"openSidebar\": \"Open sidebar\",\n    \"expandSidebar\": \"Expand as full page\",\n    \"personal\": \"Personal\",\n    \"private\": \"Private\",\n    \"workspace\": \"Workspace\",\n    \"favorites\": \"Favourites\",\n    \"clickToHidePrivate\": \"Click to hide private space\\nPages you created here are only visible to you\",\n    \"clickToHideWorkspace\": \"Click to hide workspace\\nPages you created here are visible to every member\",\n    \"clickToHidePersonal\": \"Click to hide personal space\",\n    \"clickToHideFavorites\": \"Click to hide favourite space\",\n    \"addAPage\": \"Add a new page\",\n    \"addAPageToPrivate\": \"Add a page to private space\",\n    \"addAPageToWorkspace\": \"Add a page to workspace\",\n    \"recent\": \"Recent\",\n    \"today\": \"Today\",\n    \"thisWeek\": \"This week\",\n    \"others\": \"Earlier favourites\",\n    \"earlier\": \"Earlier\",\n    \"justNow\": \"just now\",\n    \"minutesAgo\": \"{count} minutes ago\",\n    \"lastViewed\": \"Last viewed\",\n    \"favoriteAt\": \"Favourited\",\n    \"emptyRecent\": \"No Recent Pages\",\n    \"emptyRecentDescription\": \"As you view pages, they will appear here for easy retrieval.\",\n    \"emptyFavorite\": \"No Favourite Pages\",\n    \"emptyFavoriteDescription\": \"Mark pages as favourites—they'll be listed here for quick access!\",\n    \"removePageFromRecent\": \"Remove this page from the Recent?\",\n    \"removeSuccess\": \"Removed successfully\",\n    \"favoriteSpace\": \"Favourites\",\n    \"RecentSpace\": \"Recent\",\n    \"Spaces\": \"Spaces\",\n    \"upgradeToPro\": \"Upgrade to Pro\",\n    \"upgradeToAIMax\": \"Unlock unlimited AI\",\n    \"storageLimitDialogTitle\": \"You have run out of free storage. Upgrade to unlock unlimited storage\",\n    \"storageLimitDialogTitleIOS\": \"You have run out of free storage.\",\n    \"aiResponseLimitTitle\": \"You have run out of free AI responses. Upgrade to the Pro Plan or purchase an AI add-on to unlock unlimited responses\",\n    \"aiResponseLimitDialogTitle\": \"AI Responses limit reached\",\n    \"aiResponseLimit\": \"You have run out of free AI responses.\\n\\nGo to Settings -> Plan -> Click AI Max or Pro Plan to get more AI responses\",\n    \"askOwnerToUpgradeToPro\": \"Your workspace is running out of free storage. Please ask your workspace owner to upgrade to the Pro Plan\",\n    \"askOwnerToUpgradeToProIOS\": \"Your workspace is running out of free storage.\",\n    \"askOwnerToUpgradeToAIMax\": \"Your workspace has ran out of free AI responses. Please ask your workspace owner to upgrade the plan or purchase AI add-ons\",\n    \"askOwnerToUpgradeToAIMaxIOS\": \"Your workspace is running out of free AI responses.\",\n    \"purchaseAIMax\": \"Your workspace has ran out of AI Image responses. Please ask your workspace owner to purchase AI Max\",\n    \"aiImageResponseLimit\": \"You have run out of AI image responses.\\n\\nGo to Settings -> Plan -> Click AI Max to get more AI image responses\",\n    \"purchaseStorageSpace\": \"Purchase Storage Space\",\n    \"singleFileProPlanLimitationDescription\": \"You have exceeded the maximum file upload size allowed in the free plan. Please upgrade to the Pro Plan to upload larger files\",\n    \"purchaseAIResponse\": \"Purchase \",\n    \"askOwnerToUpgradeToLocalAI\": \"Ask workspace owner to enable AI On-device\",\n    \"upgradeToAILocal\": \"Run local models on your device for ultimate privacy\",\n    \"upgradeToAILocalDesc\": \"Chat with PDFs, improve your writing, and auto-fill tables using local AI\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Exported Note To Markdown\",\n      \"path\": \"Documents/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Contacts\",\n    \"whatsHappening\": \"What's happening this week?\",\n    \"addContact\": \"Add Contact\",\n    \"editContact\": \"Edit Contact\"\n  },\n  \"button\": {\n    \"ok\": \"Ok\",\n    \"confirm\": \"Confirm\",\n    \"done\": \"Done\",\n    \"cancel\": \"Cancel\",\n    \"signIn\": \"Sign In\",\n    \"signOut\": \"Sign Out\",\n    \"complete\": \"Complete\",\n    \"save\": \"Save\",\n    \"generate\": \"Generate\",\n    \"esc\": \"ESC\",\n    \"keep\": \"Keep\",\n    \"tryAgain\": \"Try again\",\n    \"discard\": \"Discard\",\n    \"replace\": \"Replace\",\n    \"insertBelow\": \"Insert below\",\n    \"insertAbove\": \"Insert above\",\n    \"upload\": \"Upload\",\n    \"edit\": \"Edit\",\n    \"delete\": \"Delete\",\n    \"copy\": \"Copy\",\n    \"duplicate\": \"Duplicate\",\n    \"putback\": \"Put Back\",\n    \"update\": \"Update\",\n    \"share\": \"Share\",\n    \"removeFromFavorites\": \"Remove from Favourites\",\n    \"removeFromRecent\": \"Remove from Recent\",\n    \"addToFavorites\": \"Add to Favourites\",\n    \"favoriteSuccessfully\": \"Favourited success\",\n    \"unfavoriteSuccessfully\": \"Unfavourited success\",\n    \"duplicateSuccessfully\": \"Duplicated successfully\",\n    \"rename\": \"Rename\",\n    \"helpCenter\": \"Help Centre\",\n    \"add\": \"Add\",\n    \"yes\": \"Yes\",\n    \"no\": \"No\",\n    \"clear\": \"Clear\",\n    \"remove\": \"Remove\",\n    \"dontRemove\": \"Don't remove\",\n    \"copyLink\": \"Copy Link\",\n    \"align\": \"Align\",\n    \"login\": \"Login\",\n    \"logout\": \"Log out\",\n    \"deleteAccount\": \"Delete account\",\n    \"back\": \"Back\",\n    \"signInGoogle\": \"Continue with Google\",\n    \"signInGithub\": \"Continue with GitHub\",\n    \"signInDiscord\": \"Continue with Discord\",\n    \"more\": \"More\",\n    \"create\": \"Create\",\n    \"close\": \"Close\",\n    \"next\": \"Next\",\n    \"previous\": \"Previous\",\n    \"submit\": \"Submit\",\n    \"download\": \"Download\",\n    \"backToHome\": \"Back to Home\",\n    \"viewing\": \"Viewing\",\n    \"editing\": \"Editing\",\n    \"gotIt\": \"Got it\",\n    \"retry\": \"Retry\",\n    \"uploadFailed\": \"Upload failed.\",\n    \"copyLinkOriginal\": \"Copy link to original\"\n  },\n  \"label\": {\n    \"welcome\": \"Welcome!\",\n    \"firstName\": \"First Name\",\n    \"middleName\": \"Middle Name\",\n    \"lastName\": \"Last Name\",\n    \"stepX\": \"Step {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Unable to connect to your account.\",\n      \"failedMsg\": \"Please make sure you've completed the sign-in process in your browser.\"\n    },\n    \"google\": {\n      \"title\": \"GOOGLE SIGN-IN\",\n      \"instruction1\": \"In order to import your Google Contacts, you'll need to authorise this application using your web browser.\",\n      \"instruction2\": \"Copy this code to your clipboard by clicking the icon or selecting the text:\",\n      \"instruction3\": \"Navigate to the following link in your web browser, and enter the above code:\",\n      \"instruction4\": \"Press the button below when you've completed signup:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Settings\",\n    \"popupMenuItem\": {\n      \"settings\": \"Settings\",\n      \"members\": \"Members\",\n      \"trash\": \"Bin\",\n      \"helpAndDocumentation\": \"Help & documentation\",\n      \"getSupport\": \"Get Support\"\n    },\n    \"sites\": {\n      \"title\": \"Sites\",\n      \"namespaceTitle\": \"Namespace\",\n      \"namespaceDescription\": \"Manage your namespace and homepage\",\n      \"namespaceHeader\": \"Namespace\",\n      \"homepageHeader\": \"Homepage\",\n      \"updateNamespace\": \"Update namespace\",\n      \"removeHomepage\": \"Remove homepage\",\n      \"selectHomePage\": \"Select a page\",\n      \"clearHomePage\": \"Clear the home page for this namespace\",\n      \"customUrl\": \"Custom URL\",\n      \"namespace\": {\n        \"description\": \"This change will apply to all the published pages live on this namespace\",\n        \"tooltip\": \"We reserve the rights to remove any inappropriate namespaces\",\n        \"updateExistingNamespace\": \"Update existing namespace\",\n        \"upgradeToPro\": \"Upgrade to Pro Plan to set a homepage\",\n        \"redirectToPayment\": \"Redirecting to payment page...\",\n        \"onlyWorkspaceOwnerCanSetHomePage\": \"Only the workspace owner can set a homepage\",\n        \"pleaseAskOwnerToSetHomePage\": \"Please ask the workspace owner to upgrade to Pro Plan\"\n      },\n      \"publishedPage\": {\n        \"title\": \"All published pages\",\n        \"description\": \"Manage your published pages\",\n        \"page\": \"Page\",\n        \"pathName\": \"Path name\",\n        \"date\": \"Published date\",\n        \"emptyHinText\": \"You have no published pages in this workspace\",\n        \"noPublishedPages\": \"No published pages\",\n        \"settings\": \"Publish settings\",\n        \"clickToOpenPageInApp\": \"Open page in app\",\n        \"clickToOpenPageInBrowser\": \"Open page in browser\"\n      },\n      \"error\": {\n        \"failedToGeneratePaymentLink\": \"Failed to generate payment link for Pro Plan\",\n        \"failedToUpdateNamespace\": \"Failed to update namespace\",\n        \"proPlanLimitation\": \"You need to upgrade to Pro Plan to update the namespace\",\n        \"namespaceAlreadyInUse\": \"The namespace is already taken, please try another one\",\n        \"invalidNamespace\": \"Invalid namespace, please try another one\",\n        \"namespaceLengthAtLeast2Characters\": \"The namespace must be at least 2 characters long\",\n        \"onlyWorkspaceOwnerCanUpdateNamespace\": \"Only workspace owner can update the namespace\",\n        \"onlyWorkspaceOwnerCanRemoveHomepage\": \"Only workspace owner can remove the homepage\",\n        \"setHomepageFailed\": \"Failed to set homepage\",\n        \"namespaceTooLong\": \"The namespace is too long, please try another one\",\n        \"namespaceTooShort\": \"The namespace is too short, please try another one\",\n        \"namespaceIsReserved\": \"The namespace is reserved, please try another one\",\n        \"updatePathNameFailed\": \"Failed to update path name\",\n        \"removeHomePageFailed\": \"Failed to remove homepage\",\n        \"publishNameContainsInvalidCharacters\": \"The path name contains invalid character(s), please try another one\",\n        \"publishNameTooShort\": \"The path name is too short, please try another one\",\n        \"publishNameTooLong\": \"The path name is too long, please try another one\",\n        \"publishNameAlreadyInUse\": \"The path name is already in use, please try another one\",\n        \"namespaceContainsInvalidCharacters\": \"The namespace contains invalid character(s), please try another one\",\n        \"publishPermissionDenied\": \"Only the workspace owner or page publisher can manage the publish settings\",\n        \"publishNameCannotBeEmpty\": \"The path name cannot be empty, please try another one\"\n      },\n      \"success\": {\n        \"namespaceUpdated\": \"Updated namespace successfully\",\n        \"setHomepageSuccess\": \"Set homepage successfully\",\n        \"updatePathNameSuccess\": \"Updated path name successfully\",\n        \"removeHomePageSuccess\": \"Remove homepage successfully\"\n      }\n    },\n    \"accountPage\": {\n      \"menuLabel\": \"Account & App\",\n      \"title\": \"My account\",\n      \"general\": {\n        \"title\": \"Account name & profile image\",\n        \"changeProfilePicture\": \"Change profile picture\"\n      },\n      \"email\": {\n        \"title\": \"Email\",\n        \"actions\": {\n          \"change\": \"Change email\"\n        }\n      },\n      \"login\": {\n        \"title\": \"Account login\",\n        \"loginLabel\": \"Log in\",\n        \"logoutLabel\": \"Log out\"\n      },\n      \"isUpToDate\": \"@:appName is up to date!\",\n      \"officialVersion\": \"Version {version} (Official build)\"\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"Workspace\",\n      \"title\": \"Workspace\",\n      \"description\": \"Customise your workspace appearance, theme, font, text layout, date-/time-format, and language.\",\n      \"workspaceName\": {\n        \"title\": \"Workspace name\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"Workspace icon\",\n        \"description\": \"Upload an image or use an emoji for your workspace. Icon will show in your sidebar and notifications.\"\n      },\n      \"appearance\": {\n        \"title\": \"Appearance\",\n        \"description\": \"Customise your workspace appearance, theme, font, text layout, date, time, and language.\",\n        \"options\": {\n          \"system\": \"Auto\",\n          \"light\": \"Light\",\n          \"dark\": \"Dark\"\n        }\n      },\n      \"resetCursorColor\": {\n        \"title\": \"Reset document cursor colour\",\n        \"description\": \"Are you sure you want to reset the cursor colour?\"\n      },\n      \"resetSelectionColor\": {\n        \"title\": \"Reset document selection colour\",\n        \"description\": \"Are you sure you want to reset the selection colour?\"\n      },\n      \"resetWidth\": {\n        \"resetSuccess\": \"Reset document width successfully\"\n      },\n      \"theme\": {\n        \"title\": \"Theme\",\n        \"description\": \"Select a preset theme, or upload your own custom theme.\",\n        \"uploadCustomThemeTooltip\": \"Upload a custom theme\"\n      },\n      \"workspaceFont\": {\n        \"title\": \"Workspace font\",\n        \"noFontHint\": \"No font found, try another term.\"\n      },\n      \"textDirection\": {\n        \"title\": \"Text direction\",\n        \"leftToRight\": \"Left to right\",\n        \"rightToLeft\": \"Right to left\",\n        \"auto\": \"Auto\",\n        \"enableRTLItems\": \"Enable RTL toolbar items\"\n      },\n      \"layoutDirection\": {\n        \"title\": \"Layout direction\",\n        \"leftToRight\": \"Left to right\",\n        \"rightToLeft\": \"Right to left\"\n      },\n      \"dateTime\": {\n        \"title\": \"Date & time\",\n        \"example\": \"{} at {} ({})\",\n        \"24HourTime\": \"24-hour time\",\n        \"dateFormat\": {\n          \"label\": \"Date format\",\n          \"local\": \"Local\",\n          \"us\": \"US\",\n          \"iso\": \"ISO\",\n          \"friendly\": \"Friendly\",\n          \"dmy\": \"D/M/Y\"\n        }\n      },\n      \"language\": {\n        \"title\": \"Language\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"Delete workspace\",\n        \"content\": \"Are you sure you want to delete this workspace? This action cannot be undone, and any pages you have published will be unpublished.\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"Leave workspace\",\n        \"content\": \"Are you sure you want to leave this workspace? You will lose access to all pages and data within it.\",\n        \"success\": \"You have left the workspace successfully.\",\n        \"fail\": \"Failed to leave the workspace.\"\n      },\n      \"manageWorkspace\": {\n        \"title\": \"Manage workspace\",\n        \"leaveWorkspace\": \"Leave workspace\",\n        \"deleteWorkspace\": \"Delete workspace\"\n      }\n    },\n    \"manageDataPage\": {\n      \"menuLabel\": \"Manage data\",\n      \"title\": \"Manage data\",\n      \"description\": \"Manage data local storage or Import your existing data into @:appName.\",\n      \"dataStorage\": {\n        \"title\": \"File storage location\",\n        \"tooltip\": \"The location where your files are stored\",\n        \"actions\": {\n          \"change\": \"Change path\",\n          \"open\": \"Open folder\",\n          \"openTooltip\": \"Open current data folder location\",\n          \"copy\": \"Copy path\",\n          \"copiedHint\": \"Path copied!\",\n          \"resetTooltip\": \"Reset to default location\"\n        },\n        \"resetDialog\": {\n          \"title\": \"Are you sure?\",\n          \"description\": \"Resetting the path to the default data location will not delete your data. If you want to re-import your current data, you should copy the path of your current location first.\"\n        }\n      },\n      \"importData\": {\n        \"title\": \"Import data\",\n        \"tooltip\": \"Import data from @:appName backups/data folders\",\n        \"description\": \"Copy data from an external @:appName data folder\",\n        \"action\": \"Browse file\"\n      },\n      \"encryption\": {\n        \"title\": \"Encryption\",\n        \"tooltip\": \"Manage how your data is stored and encrypted\",\n        \"descriptionNoEncryption\": \"Turning on encryption will encrypt all data. This can not be undone.\",\n        \"descriptionEncrypted\": \"Your data is encrypted.\",\n        \"action\": \"Encrypt data\",\n        \"dialog\": {\n          \"title\": \"Encrypt all your data?\",\n          \"description\": \"Encrypting all your data will keep your data safe and secure. This action can NOT be undone. Are you sure you want to continue?\"\n        }\n      },\n      \"cache\": {\n        \"title\": \"Clear cache\",\n        \"description\": \"Help resolve issues like image not loading, missing pages in a space, and fonts not loading. This will not affect your data.\",\n        \"dialog\": {\n          \"title\": \"Clear cache\",\n          \"description\": \"Help resolve issues like image not loading, missing pages in a space, and fonts not loading. This will not affect your data.\",\n          \"successHint\": \"Cache cleared!\"\n        }\n      },\n      \"data\": {\n        \"fixYourData\": \"Fix your data\",\n        \"fixButton\": \"Fix\",\n        \"fixYourDataDescription\": \"If you're experiencing issues with your data, you can try to fix it here.\"\n      }\n    },\n    \"shortcutsPage\": {\n      \"menuLabel\": \"Shortcuts\",\n      \"title\": \"Shortcuts\",\n      \"editBindingHint\": \"Input new binding\",\n      \"searchHint\": \"Search\",\n      \"actions\": {\n        \"resetDefault\": \"Reset default\"\n      },\n      \"errorPage\": {\n        \"message\": \"Failed to load shortcuts: {}\",\n        \"howToFix\": \"Please try again, if the issue persists please reach out on GitHub.\"\n      },\n      \"resetDialog\": {\n        \"title\": \"Reset shortcuts\",\n        \"description\": \"This will reset all of your keybindings to the default, you cannot undo this later, are you sure you want to proceed?\",\n        \"buttonLabel\": \"Reset\"\n      },\n      \"conflictDialog\": {\n        \"title\": \"{} is currently in use\",\n        \"descriptionPrefix\": \"This keybinding is currently being used by \",\n        \"descriptionSuffix\": \". If you replace this keybinding, it will be removed from {}.\",\n        \"confirmLabel\": \"Continue\"\n      },\n      \"editTooltip\": \"Press to start editing the keybinding\",\n      \"keybindings\": {\n        \"toggleToDoList\": \"Toggle to do list\",\n        \"insertNewParagraphInCodeblock\": \"Insert new paragraph\",\n        \"pasteInCodeblock\": \"Paste in codeblock\",\n        \"selectAllCodeblock\": \"Select all\",\n        \"indentLineCodeblock\": \"Insert two spaces at line start\",\n        \"outdentLineCodeblock\": \"Delete two spaces at line start\",\n        \"twoSpacesCursorCodeblock\": \"Insert two spaces at cursor\",\n        \"copy\": \"Copy selection\",\n        \"paste\": \"Paste in content\",\n        \"cut\": \"Cut selection\",\n        \"alignLeft\": \"Align text left\",\n        \"alignCenter\": \"Align text centre\",\n        \"alignRight\": \"Align text right\",\n        \"insertInlineMathEquation\": \"Insert inline maths equation\",\n        \"undo\": \"Undo\",\n        \"redo\": \"Redo\",\n        \"convertToParagraph\": \"Convert block to paragraph\",\n        \"backspace\": \"Delete\",\n        \"deleteLeftWord\": \"Delete left word\",\n        \"deleteLeftSentence\": \"Delete left sentence\",\n        \"delete\": \"Delete right character\",\n        \"deleteMacOS\": \"Delete left character\",\n        \"deleteRightWord\": \"Delete right word\",\n        \"moveCursorLeft\": \"Move cursor left\",\n        \"moveCursorBeginning\": \"Move cursor to the beginning\",\n        \"moveCursorLeftWord\": \"Move cursor left one word\",\n        \"moveCursorLeftSelect\": \"Select and move cursor left\",\n        \"moveCursorBeginSelect\": \"Select and move cursor to the beginning\",\n        \"moveCursorLeftWordSelect\": \"Select and move cursor left one word\",\n        \"moveCursorRight\": \"Move cursor right\",\n        \"moveCursorEnd\": \"Move cursor to the end\",\n        \"moveCursorRightWord\": \"Move cursor right one word\",\n        \"moveCursorRightSelect\": \"Select and move cursor right one\",\n        \"moveCursorEndSelect\": \"Select and move cursor to the end\",\n        \"moveCursorRightWordSelect\": \"Select and move cursor to the right one word\",\n        \"moveCursorUp\": \"Move cursor up\",\n        \"moveCursorTopSelect\": \"Select and move cursor to the top\",\n        \"moveCursorTop\": \"Move cursor to the top\",\n        \"moveCursorUpSelect\": \"Select and move cursor up\",\n        \"moveCursorBottomSelect\": \"Select and move cursor to the bottom\",\n        \"moveCursorBottom\": \"Move cursor to the bottom\",\n        \"moveCursorDown\": \"Move cursor down\",\n        \"moveCursorDownSelect\": \"Select and move cursor down\",\n        \"home\": \"Scroll to the top\",\n        \"end\": \"Scroll to the bottom\",\n        \"toggleBold\": \"Toggle bold\",\n        \"toggleItalic\": \"Toggle italic\",\n        \"toggleUnderline\": \"Toggle underline\",\n        \"toggleStrikethrough\": \"Toggle strikethrough\",\n        \"toggleCode\": \"Toggle in-line code\",\n        \"toggleHighlight\": \"Toggle highlight\",\n        \"showLinkMenu\": \"Show link menu\",\n        \"openInlineLink\": \"Open in-line link\",\n        \"openLinks\": \"Open all selected links\",\n        \"indent\": \"Indent\",\n        \"outdent\": \"Outdent\",\n        \"exit\": \"Exit editing\",\n        \"pageUp\": \"Scroll one page up\",\n        \"pageDown\": \"Scroll one page down\",\n        \"selectAll\": \"Select all\",\n        \"pasteWithoutFormatting\": \"Paste content without formatting\",\n        \"showEmojiPicker\": \"Show emoji picker\",\n        \"enterInTableCell\": \"Add linebreak in table\",\n        \"leftInTableCell\": \"Move left one cell in table\",\n        \"rightInTableCell\": \"Move right one cell in table\",\n        \"upInTableCell\": \"Move up one cell in table\",\n        \"downInTableCell\": \"Move down one cell in table\",\n        \"tabInTableCell\": \"Go to next available cell in table\",\n        \"shiftTabInTableCell\": \"Go to previously available cell in table\",\n        \"backSpaceInTableCell\": \"Stop at the beginning of the cell\"\n      },\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"Insert a new paragraph next to the code block\",\n        \"codeBlockIndentLines\": \"Insert two spaces at the line start in code block\",\n        \"codeBlockOutdentLines\": \"Delete two spaces at the line start in code block\",\n        \"codeBlockAddTwoSpaces\": \"Insert two spaces at the cursor position in code block\",\n        \"codeBlockSelectAll\": \"Select all content inside a code block\",\n        \"codeBlockPasteText\": \"Paste text in codeblock\",\n        \"textAlignLeft\": \"Align text to the left\",\n        \"textAlignCenter\": \"Align text to the centre\",\n        \"textAlignRight\": \"Align text to the right\"\n      },\n      \"couldNotLoadErrorMsg\": \"Could not load shortcuts, Try again\",\n      \"couldNotSaveErrorMsg\": \"Could not save shortcuts, Try again\"\n    },\n    \"aiPage\": {\n      \"title\": \"AI Settings\",\n      \"menuLabel\": \"AI Settings\",\n      \"keys\": {\n        \"enableAISearchTitle\": \"AI Search\",\n        \"aiSettingsDescription\": \"Choose your preferred model to power AppFlowy AI. Now includes GPT-4o, GPT-o3-mini, DeepSeek R1, Claude 3.5 Sonnet, and models available in Ollama\",\n        \"loginToEnableAIFeature\": \"AI features are only enabled after logging in with @:appName Cloud. If you don't have an @:appName account, go to 'My Account' to sign up\",\n        \"llmModel\": \"Language Model\",\n        \"llmModelType\": \"Language Model Type\",\n        \"downloadLLMPrompt\": \"Download {}\",\n        \"downloadAppFlowyOfflineAI\": \"Downloading AI offline package will enable AI to run on your device. Do you want to continue?\",\n        \"downloadLLMPromptDetail\": \"Downloading {} local model will take up to {} of storage. Do you want to continue?\",\n        \"downloadBigFilePrompt\": \"It may take around 10 minutes to complete the download\",\n        \"downloadAIModelButton\": \"Download\",\n        \"downloadingModel\": \"Downloading\",\n        \"localAILoaded\": \"Local AI Model successfully added and ready to use\",\n        \"localAIStart\": \"Local AI is starting. If it's slow, try toggling it off and on\",\n        \"localAILoading\": \"Local AI Chat Model is loading...\",\n        \"localAIStopped\": \"Local AI stopped\",\n        \"localAIRunning\": \"Local AI is running\",\n        \"localAINotReadyRetryLater\": \"Local AI is initialising, please retry later\",\n        \"localAIDisabled\": \"You are using local AI, but it is disabled. Please go to settings to enable it or try different model\",\n        \"localAIInitializing\": \"Local AI is loading. This may take a few seconds depending on your device\",\n        \"localAINotReadyTextFieldPrompt\": \"You can not edit while Local AI is loading\",\n        \"failToLoadLocalAI\": \"Failed to start local AI.\",\n        \"restartLocalAI\": \"Restart\",\n        \"disableLocalAITitle\": \"Disable local AI\",\n        \"disableLocalAIDescription\": \"Do you want to disable local AI?\",\n        \"localAIToggleTitle\": \"AppFlowy Local AI (LAI)\",\n        \"localAIToggleSubTitle\": \"Run the most advanced local AI models within AppFlowy for ultimate privacy and security\",\n        \"offlineAIInstruction1\": \"Follow the\",\n        \"offlineAIInstruction2\": \"instruction\",\n        \"offlineAIInstruction3\": \"to enable offline AI.\",\n        \"offlineAIDownload1\": \"If you have not downloaded the AppFlowy AI, please\",\n        \"offlineAIDownload2\": \"download\",\n        \"offlineAIDownload3\": \"it first\",\n        \"activeOfflineAI\": \"Active\",\n        \"downloadOfflineAI\": \"Download\",\n        \"openModelDirectory\": \"Open folder\",\n        \"laiNotReady\": \"The Local AI app was not installed correctly.\",\n        \"ollamaNotReady\": \"The Ollama server is not ready.\",\n        \"pleaseFollowThese\": \"Please follow these\",\n        \"instructions\": \"instructions\",\n        \"installOllamaLai\": \"to set up Ollama and AppFlowy Local AI.\",\n        \"modelsMissing\": \"Cannot find the required models.\",\n        \"downloadModel\": \"to download them.\"\n      }\n    },\n    \"planPage\": {\n      \"menuLabel\": \"Plan\",\n      \"title\": \"Pricing plan\",\n      \"planUsage\": {\n        \"title\": \"Plan usage summary\",\n        \"storageLabel\": \"Storage\",\n        \"storageUsage\": \"{} of {} GB\",\n        \"unlimitedStorageLabel\": \"Unlimited storage\",\n        \"collaboratorsLabel\": \"Members\",\n        \"collaboratorsUsage\": \"{} of {}\",\n        \"aiResponseLabel\": \"AI Responses\",\n        \"aiResponseUsage\": \"{} of {}\",\n        \"unlimitedAILabel\": \"Unlimited responses\",\n        \"proBadge\": \"Pro\",\n        \"aiMaxBadge\": \"AI Max\",\n        \"aiOnDeviceBadge\": \"AI On-device for Mac\",\n        \"memberProToggle\": \"More members & unlimited AI\",\n        \"aiMaxToggle\": \"Unlimited AI and access to advanced models\",\n        \"aiOnDeviceToggle\": \"Local AI for ultimate privacy\",\n        \"aiCredit\": {\n          \"title\": \"Add @:appName AI Credit\",\n          \"price\": \"{}\",\n          \"priceDescription\": \"for 1,000 credits\",\n          \"purchase\": \"Purchase AI\",\n          \"info\": \"Add 1,000 Ai credits per workspace and seamlessly integrate customisable AI into your workflow for smarter, faster results with up to:\",\n          \"infoItemOne\": \"10,000 responses per database\",\n          \"infoItemTwo\": \"1,000 responses per workspace\"\n        },\n        \"currentPlan\": {\n          \"bannerLabel\": \"Current plan\",\n          \"freeTitle\": \"Free\",\n          \"proTitle\": \"Pro\",\n          \"teamTitle\": \"Team\",\n          \"freeInfo\": \"Perfect for individuals up to 2 members to organise everything\",\n          \"proInfo\": \"Perfect for small and medium teams up to 10 members.\",\n          \"teamInfo\": \"Perfect for all productive and well-organised teams..\",\n          \"upgrade\": \"Change plan\",\n          \"canceledInfo\": \"Your plan is cancelled, you will be downgraded to the Free plan on {}.\"\n        },\n        \"addons\": {\n          \"title\": \"Add-ons\",\n          \"addLabel\": \"Add\",\n          \"activeLabel\": \"Added\",\n          \"aiMax\": {\n            \"title\": \"AI Max\",\n            \"description\": \"Unlimited AI responses powered by advanced AI models, and 50 AI images per month\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"Per user per month billed annually\"\n          },\n          \"aiOnDevice\": {\n            \"title\": \"AI On-device for Mac\",\n            \"description\": \"Run Mistral 7B, LLAMA 3, and more local models on your machine\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"Per user per month billed annually\",\n            \"recommend\": \"Recommend M1 or newer\"\n          }\n        },\n        \"deal\": {\n          \"bannerLabel\": \"New year deal!\",\n          \"title\": \"Grow your team!\",\n          \"info\": \"Upgrade and save 10% off Pro and Team plans! Boost your workspace productivity with powerful new features including @:appName AI.\",\n          \"viewPlans\": \"View plans\"\n        }\n      }\n    },\n    \"billingPage\": {\n      \"menuLabel\": \"Billing\",\n      \"title\": \"Billing\",\n      \"plan\": {\n        \"title\": \"Plan\",\n        \"freeLabel\": \"Free\",\n        \"proLabel\": \"Pro\",\n        \"planButtonLabel\": \"Change plan\",\n        \"billingPeriod\": \"Billing period\",\n        \"periodButtonLabel\": \"Edit period\"\n      },\n      \"paymentDetails\": {\n        \"title\": \"Payment details\",\n        \"methodLabel\": \"Payment method\",\n        \"methodButtonLabel\": \"Edit method\"\n      },\n      \"addons\": {\n        \"title\": \"Add-ons\",\n        \"addLabel\": \"Add\",\n        \"removeLabel\": \"Remove\",\n        \"renewLabel\": \"Renew\",\n        \"aiMax\": {\n          \"label\": \"AI Max\",\n          \"description\": \"Unlock unlimited AI and advanced models\",\n          \"activeDescription\": \"Next invoice due on {}\",\n          \"canceledDescription\": \"AI Max will be available until {}\"\n        },\n        \"aiOnDevice\": {\n          \"label\": \"AI On-device for Mac\",\n          \"description\": \"Unlock unlimited AI On-device on your device\",\n          \"activeDescription\": \"Next invoice due on {}\",\n          \"canceledDescription\": \"AI On-device for Mac will be available until {}\"\n        },\n        \"removeDialog\": {\n          \"title\": \"Remove {}\",\n          \"description\": \"Are you sure you want to remove {plan}? You will lose access to the features and benefits of {plan} immediately.\"\n        }\n      },\n      \"currentPeriodBadge\": \"CURRENT\",\n      \"changePeriod\": \"Change period\",\n      \"planPeriod\": \"{} period\",\n      \"monthlyInterval\": \"Monthly\",\n      \"monthlyPriceInfo\": \"per seat billed monthly\",\n      \"annualInterval\": \"Annually\",\n      \"annualPriceInfo\": \"per seat billed annually\"\n    },\n    \"comparePlanDialog\": {\n      \"title\": \"Compare & select plan\",\n      \"planFeatures\": \"Plan\\nFeatures\",\n      \"current\": \"Current\",\n      \"actions\": {\n        \"upgrade\": \"Upgrade\",\n        \"downgrade\": \"Downgrade\",\n        \"current\": \"Current\"\n      },\n      \"freePlan\": {\n        \"title\": \"Free\",\n        \"description\": \"For individuals up to 2 members to organise everything\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"Free forever\"\n      },\n      \"proPlan\": {\n        \"title\": \"Pro\",\n        \"description\": \"For small teams to manage projects and team knowledge\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"Per user per month \\nbilled annually\\n\\n{} billed monthly\"\n      },\n      \"planLabels\": {\n        \"itemOne\": \"Workspaces\",\n        \"itemTwo\": \"Members\",\n        \"itemThree\": \"Storage\",\n        \"itemFour\": \"Real-time collaboration\",\n        \"itemFive\": \"Mobile app\",\n        \"itemSix\": \"AI Responses\",\n        \"itemSeven\": \"AI Images\",\n        \"itemFileUpload\": \"File uploads\",\n        \"customNamespace\": \"Custom namespace\",\n        \"tooltipSix\": \"Lifetime means the number of responses never reset\",\n        \"intelligentSearch\": \"Intelligent search\",\n        \"tooltipSeven\": \"Allows you to customise part of the URL for your workspace\",\n        \"customNamespaceTooltip\": \"Custom published site URL\"\n      },\n      \"freeLabels\": {\n        \"itemOne\": \"Charged per workspace\",\n        \"itemTwo\": \"Up to 2\",\n        \"itemThree\": \"5 GB\",\n        \"itemFour\": \"yes\",\n        \"itemFive\": \"yes\",\n        \"itemSix\": \"10 lifetime\",\n        \"itemSeven\": \"2 lifetime\",\n        \"itemFileUpload\": \"Up to 7 MB\",\n        \"intelligentSearch\": \"Intelligent search\"\n      },\n      \"proLabels\": {\n        \"itemOne\": \"Charged per workspace\",\n        \"itemTwo\": \"Up to 10\",\n        \"itemThree\": \"Unlimited\",\n        \"itemFour\": \"yes\",\n        \"itemFive\": \"yes\",\n        \"itemSix\": \"Unlimited\",\n        \"itemSeven\": \"10 images per month\",\n        \"itemFileUpload\": \"Unlimited\",\n        \"intelligentSearch\": \"Intelligent search\"\n      },\n      \"paymentSuccess\": {\n        \"title\": \"You are now on the {} plan!\",\n        \"description\": \"Your payment has been successfully processed and your plan is upgraded to @:appName {}. You can view your plan details on the Plan page\"\n      },\n      \"downgradeDialog\": {\n        \"title\": \"Are you sure you want to downgrade your plan?\",\n        \"description\": \"Downgrading your plan will revert you back to the Free plan. Members may lose access to this workspace and you may need to free up space to meet the storage limits of the Free plan.\",\n        \"downgradeLabel\": \"Downgrade plan\"\n      }\n    },\n    \"cancelSurveyDialog\": {\n      \"title\": \"Sorry to see you go\",\n      \"description\": \"We're sorry to see you go. We'd love to hear your feedback to help us improve @:appName. Please take a moment to answer a few questions.\",\n      \"commonOther\": \"Other\",\n      \"otherHint\": \"Write your answer here\",\n      \"questionOne\": {\n        \"question\": \"What prompted you to cancel your @:appName Pro subscription?\",\n        \"answerOne\": \"Cost too high\",\n        \"answerTwo\": \"Features did not meet expectations\",\n        \"answerThree\": \"Found a better alternative\",\n        \"answerFour\": \"Did not use it enough to justify the expense\",\n        \"answerFive\": \"Service issue or technical difficulties\"\n      },\n      \"questionTwo\": {\n        \"question\": \"How likely are you to consider re-subscribing to @:appName Pro in the future?\",\n        \"answerOne\": \"Very likely\",\n        \"answerTwo\": \"Somewhat likely\",\n        \"answerThree\": \"Not sure\",\n        \"answerFour\": \"Unlikely\",\n        \"answerFive\": \"Very unlikely\"\n      },\n      \"questionThree\": {\n        \"question\": \"Which Pro feature did you value the most during your subscription?\",\n        \"answerOne\": \"Multi-user collaboration\",\n        \"answerTwo\": \"Longer time version history\",\n        \"answerThree\": \"Unlimited AI responses\",\n        \"answerFour\": \"Access to local AI models\"\n      },\n      \"questionFour\": {\n        \"question\": \"How would you describe your overall experience with @:appName?\",\n        \"answerOne\": \"Great\",\n        \"answerTwo\": \"Good\",\n        \"answerThree\": \"Average\",\n        \"answerFour\": \"Below average\",\n        \"answerFive\": \"Unsatisfied\"\n      }\n    },\n    \"common\": {\n      \"uploadingFile\": \"File is uploading. Please do not quit the app\",\n      \"uploadNotionSuccess\": \"Your Notion zip file has been uploaded successfully. Once the import is complete, you will receive a confirmation email\",\n      \"reset\": \"Reset\"\n    },\n    \"menu\": {\n      \"appearance\": \"Appearance\",\n      \"language\": \"Language\",\n      \"user\": \"User\",\n      \"files\": \"Files\",\n      \"notifications\": \"Notifications\",\n      \"open\": \"Open Settings\",\n      \"logout\": \"Logout\",\n      \"logoutPrompt\": \"Are you sure you want to logout?\",\n      \"selfEncryptionLogoutPrompt\": \"Are you sure you want to log out? Please ensure you have copied the encryption secret\",\n      \"syncSetting\": \"Sync Setting\",\n      \"cloudSettings\": \"Cloud Settings\",\n      \"enableSync\": \"Enable sync\",\n      \"enableSyncLog\": \"Enable sync logging\",\n      \"enableSyncLogWarning\": \"Thank you for helping diagnose sync issues. This will log your document edits to a local file. Please quit and reopen the app after enabling\",\n      \"enableEncrypt\": \"Encrypt data\",\n      \"cloudURL\": \"Base URL\",\n      \"webURL\": \"Web URL\",\n      \"invalidCloudURLScheme\": \"Invalid Scheme\",\n      \"cloudServerType\": \"Cloud server\",\n      \"cloudServerTypeTip\": \"Please note that it might log out your current account after switching the cloud server\",\n      \"cloudLocal\": \"Local\",\n      \"cloudAppFlowy\": \"@:appName Cloud\",\n      \"cloudAppFlowySelfHost\": \"@:appName Cloud Self-hosted\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"The cloud url can't be empty\",\n      \"clickToCopy\": \"Copy to clipboard\",\n      \"selfHostStart\": \"If you don't have a server, please refer to the\",\n      \"selfHostContent\": \"document\",\n      \"selfHostEnd\": \"for guidance on how to self-host your own server\",\n      \"pleaseInputValidURL\": \"Please input a valid URL\",\n      \"changeUrl\": \"Change self-hosted url to {}\",\n      \"cloudURLHint\": \"Input the base URL of your server\",\n      \"webURLHint\": \"Input the base URL of your web server\",\n      \"cloudWSURL\": \"Websocket URL\",\n      \"cloudWSURLHint\": \"Input the websocket address of your server\",\n      \"restartApp\": \"Restart\",\n      \"restartAppTip\": \"Restart the application for the changes to take effect. Please note that this might log out your current account.\",\n      \"changeServerTip\": \"After changing the server, you must click the restart button for the changes to take effect\",\n      \"enableEncryptPrompt\": \"Activate encryption to secure your data with this secret. Store it safely; once enabled, it can't be turned off. If lost, your data becomes irretrievable. Click to copy\",\n      \"inputEncryptPrompt\": \"Please enter your encryption secret for\",\n      \"clickToCopySecret\": \"Click to copy secret\",\n      \"configServerSetting\": \"Configurate your server settings\",\n      \"configServerGuide\": \"After selecting `Quick Start`, navigate to `Settings` and then \\\"Cloud Settings\\\" to configure your self-hosted server.\",\n      \"inputTextFieldHint\": \"Your secret\",\n      \"historicalUserList\": \"User login history\",\n      \"historicalUserListTooltip\": \"This list displays your anonymous accounts. You can click on an account to view its details. Anonymous accounts are created by clicking the 'Get Started' button\",\n      \"openHistoricalUser\": \"Click to open the anonymous account\",\n      \"customPathPrompt\": \"Storing the @:appName data folder in a cloud-synced folder such as Google Drive can pose risks. If the database within this folder is accessed or modified from multiple locations at the same time, it may result in synchronisation conflicts and potential data corruption\",\n      \"importAppFlowyData\": \"Import Data from External @:appName Folder\",\n      \"importingAppFlowyDataTip\": \"Data import is in progress. Please do not close the app\",\n      \"importAppFlowyDataDescription\": \"Copy data from an external @:appName data folder and import it into the current AppFlowy data folder\",\n      \"importSuccess\": \"Successfully imported the @:appName data folder\",\n      \"importFailed\": \"Importing the @:appName data folder failed\",\n      \"importGuide\": \"For further details, please check the referenced document\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Enable notifications\",\n        \"hint\": \"Turn off to stop local notifications from appearing.\"\n      },\n      \"showNotificationsIcon\": {\n        \"label\": \"Show notifications icon\",\n        \"hint\": \"Toggle off to hide the notification icon in the sidebar.\"\n      },\n      \"archiveNotifications\": {\n        \"allSuccess\": \"Archived all notifications successfully\",\n        \"success\": \"Archived notification successfully\"\n      },\n      \"markAsReadNotifications\": {\n        \"allSuccess\": \"Marked all as read successfully\",\n        \"success\": \"Marked as read successfully\"\n      },\n      \"action\": {\n        \"markAsRead\": \"Mark as read\",\n        \"multipleChoice\": \"Select more\",\n        \"archive\": \"Archive\"\n      },\n      \"settings\": {\n        \"settings\": \"Settings\",\n        \"markAllAsRead\": \"Mark all as read\",\n        \"archiveAll\": \"Archive all\"\n      },\n      \"emptyInbox\": {\n        \"title\": \"Inbox Zero!\",\n        \"description\": \"Set reminders to receive notifications here.\"\n      },\n      \"emptyUnread\": {\n        \"title\": \"No unread notifications\",\n        \"description\": \"You're all caught up!\"\n      },\n      \"emptyArchived\": {\n        \"title\": \"No archived\",\n        \"description\": \"Archived notifications will appear here.\"\n      },\n      \"tabs\": {\n        \"inbox\": \"Inbox\",\n        \"unread\": \"Unread\",\n        \"archived\": \"Archived\"\n      },\n      \"refreshSuccess\": \"Notifications refreshed successfully\",\n      \"titles\": {\n        \"notifications\": \"Notifications\",\n        \"reminder\": \"Reminder\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"Reset\",\n      \"fontFamily\": {\n        \"label\": \"Font Family\",\n        \"search\": \"Search\",\n        \"defaultFont\": \"System\"\n      },\n      \"themeMode\": {\n        \"label\": \"Theme Mode\",\n        \"light\": \"Light Mode\",\n        \"dark\": \"Dark Mode\",\n        \"system\": \"Adapt to System\"\n      },\n      \"fontScaleFactor\": \"Font Scale Factor\",\n      \"displaySize\": \"Display Size\",\n      \"documentSettings\": {\n        \"cursorColor\": \"Document cursor colour\",\n        \"selectionColor\": \"Document selection colour\",\n        \"width\": \"Document width\",\n        \"changeWidth\": \"Change\",\n        \"pickColor\": \"Select a colour\",\n        \"colorShade\": \"Colour shade\",\n        \"opacity\": \"Opacity\",\n        \"hexEmptyError\": \"Hex colour cannot be empty\",\n        \"hexLengthError\": \"Hex value must be 6 digits long\",\n        \"hexInvalidError\": \"Invalid hex value\",\n        \"opacityEmptyError\": \"Opacity cannot be empty\",\n        \"opacityRangeError\": \"Opacity must be between 1 and 100\",\n        \"app\": \"App\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"Apply\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Layout Direction\",\n        \"hint\": \"Control the flow of content on your screen, from left to right or right to left.\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\"\n      },\n      \"textDirection\": {\n        \"label\": \"Default Text Direction\",\n        \"hint\": \"Specify whether text should start from left or right as the default.\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\",\n        \"auto\": \"AUTO\",\n        \"fallback\": \"Same as layout direction\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Upload\",\n        \"uploadTheme\": \"Upload theme\",\n        \"description\": \"Upload your own @:appName theme using the button below.\",\n        \"loading\": \"Please wait while we validate and upload your theme...\",\n        \"uploadSuccess\": \"Your theme was uploaded successfully\",\n        \"deletionFailure\": \"Failed to delete the theme. Try to delete it manually.\",\n        \"filePickerDialogTitle\": \"Choose a .flowy_plugin file\",\n        \"urlUploadFailure\": \"Failed to open url: {}\"\n      },\n      \"theme\": \"Theme\",\n      \"builtInsLabel\": \"Built-in Themes\",\n      \"pluginsLabel\": \"Plugins\",\n      \"dateFormat\": {\n        \"label\": \"Date format\",\n        \"local\": \"Local\",\n        \"us\": \"US\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"Friendly\",\n        \"dmy\": \"D/M/Y\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Time format\",\n        \"twelveHour\": \"Twelve hour\",\n        \"twentyFourHour\": \"Twenty four hour\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"Show naming dialog when creating a page\",\n      \"enableRTLToolbarItems\": \"Enable RTL toolbar items\",\n      \"members\": {\n        \"title\": \"Members settings\",\n        \"inviteMembers\": \"Invite members\",\n        \"inviteHint\": \"Invite by email\",\n        \"sendInvite\": \"Send invite\",\n        \"copyInviteLink\": \"Copy invite link\",\n        \"label\": \"Members\",\n        \"user\": \"User\",\n        \"role\": \"Role\",\n        \"removeFromWorkspace\": \"Remove from Workspace\",\n        \"removeFromWorkspaceSuccess\": \"Remove from workspace successfully\",\n        \"removeFromWorkspaceFailed\": \"Remove from workspace failed\",\n        \"owner\": \"Owner\",\n        \"guest\": \"Guest\",\n        \"member\": \"Member\",\n        \"memberHintText\": \"A member can read and edit pages\",\n        \"guestHintText\": \"A Guest can read, react, comment, and can edit certain pages with permission.\",\n        \"emailInvalidError\": \"Invalid email, please check and try again\",\n        \"emailSent\": \"Email sent, please check the inbox\",\n        \"members\": \"members\",\n        \"membersCount\": {\n          \"zero\": \"{} members\",\n          \"one\": \"{} member\",\n          \"other\": \"{} members\"\n        },\n        \"inviteFailedDialogTitle\": \"Failed to send invite\",\n        \"inviteFailedMemberLimit\": \"Member limit has been reached, please upgrade to invite more members.\",\n        \"inviteFailedMemberLimitMobile\": \"Your workspace has reached the member limit.\",\n        \"memberLimitExceeded\": \"Member limit reached, to invite more members, please \",\n        \"memberLimitExceededUpgrade\": \"upgrade\",\n        \"memberLimitExceededPro\": \"Member limit reached, if you require more members contact \",\n        \"memberLimitExceededProContact\": \"support@appflowy.io\",\n        \"failedToAddMember\": \"Failed to add member\",\n        \"addMemberSuccess\": \"Member added successfully\",\n        \"removeMember\": \"Remove Member\",\n        \"areYouSureToRemoveMember\": \"Are you sure you want to remove this member?\",\n        \"inviteMemberSuccess\": \"The invitation has been sent successfully\",\n        \"failedToInviteMember\": \"Failed to invite member\",\n        \"workspaceMembersError\": \"Oops, something went wrong\",\n        \"workspaceMembersErrorDescription\": \"We couldn't load the member list at this time. Please try again later\"\n      }\n    },\n    \"files\": {\n      \"copy\": \"Copy\",\n      \"defaultLocation\": \"Read files and data storage location\",\n      \"exportData\": \"Export your data\",\n      \"doubleTapToCopy\": \"Double tap to copy the path\",\n      \"restoreLocation\": \"Restore to @:appName default path\",\n      \"customizeLocation\": \"Open another folder\",\n      \"restartApp\": \"Please restart app for the changes to take effect.\",\n      \"exportDatabase\": \"Export database\",\n      \"selectFiles\": \"Select the files that need to be export\",\n      \"selectAll\": \"Select all\",\n      \"deselectAll\": \"Deselect all\",\n      \"createNewFolder\": \"Create a new folder\",\n      \"createNewFolderDesc\": \"Tell us where you want to store your data\",\n      \"defineWhereYourDataIsStored\": \"Define where your data is stored\",\n      \"open\": \"Open\",\n      \"openFolder\": \"Open an existing folder\",\n      \"openFolderDesc\": \"Read and write it to your existing @:appName folder\",\n      \"folderHintText\": \"folder name\",\n      \"location\": \"Creating a new folder\",\n      \"locationDesc\": \"Pick a name for your @:appName data folder\",\n      \"browser\": \"Browse\",\n      \"create\": \"Create\",\n      \"set\": \"Set\",\n      \"folderPath\": \"Path to store your folder\",\n      \"locationCannotBeEmpty\": \"Path cannot be empty\",\n      \"pathCopiedSnackbar\": \"File storage path copied to clipboard!\",\n      \"changeLocationTooltips\": \"Change the data directory\",\n      \"change\": \"Change\",\n      \"openLocationTooltips\": \"Open another data directory\",\n      \"openCurrentDataFolder\": \"Open current data directory\",\n      \"recoverLocationTooltips\": \"Reset to @:appName's default data directory\",\n      \"exportFileSuccess\": \"Export file successfully!\",\n      \"exportFileFail\": \"Export file failed!\",\n      \"export\": \"Export\",\n      \"clearCache\": \"Clear cache\",\n      \"clearCacheDesc\": \"If you encounter issues with images not loading or fonts not displaying correctly, try clearing your cache. This action will not remove your user data.\",\n      \"areYouSureToClearCache\": \"Are you sure to clear the cache?\",\n      \"clearCacheSuccess\": \"Cache cleared successfully!\"\n    },\n    \"user\": {\n      \"name\": \"Name\",\n      \"email\": \"Email\",\n      \"tooltipSelectIcon\": \"Select icon\",\n      \"selectAnIcon\": \"Select an icon\",\n      \"pleaseInputYourOpenAIKey\": \"please input your AI key\",\n      \"clickToLogout\": \"Click to logout the current user\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"Personal Information\",\n      \"username\": \"User Name\",\n      \"usernameEmptyError\": \"User name cannot be empty\",\n      \"about\": \"About\",\n      \"pushNotifications\": \"Push Notifications\",\n      \"support\": \"Support\",\n      \"joinDiscord\": \"Join us in Discord\",\n      \"privacyPolicy\": \"Privacy Policy\",\n      \"userAgreement\": \"User Agreement\",\n      \"termsAndConditions\": \"Terms and Conditions\",\n      \"userprofileError\": \"Failed to load user profile\",\n      \"userprofileErrorDescription\": \"Please try to log out and log back in to check if the issue still persists.\",\n      \"selectLayout\": \"Select layout\",\n      \"selectStartingDay\": \"Select starting day\",\n      \"version\": \"Version\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Are you sure you want to delete this view?\",\n    \"createView\": \"New\",\n    \"title\": {\n      \"placeholder\": \"Untitled\"\n    },\n    \"settings\": {\n      \"filter\": \"Filter\",\n      \"sort\": \"Sort\",\n      \"sortBy\": \"Sort by\",\n      \"properties\": \"Properties\",\n      \"reorderPropertiesTooltip\": \"Drag to reorder properties\",\n      \"group\": \"Group\",\n      \"addFilter\": \"Add Filter\",\n      \"deleteFilter\": \"Delete filter\",\n      \"filterBy\": \"Filter by\",\n      \"typeAValue\": \"Type a value...\",\n      \"layout\": \"Layout\",\n      \"compactMode\": \"Compact mode\",\n      \"databaseLayout\": \"Layout\",\n      \"viewList\": {\n        \"zero\": \"0 views\",\n        \"one\": \"{count} view\",\n        \"other\": \"{count} views\"\n      },\n      \"editView\": \"Edit View\",\n      \"boardSettings\": \"Board settings\",\n      \"calendarSettings\": \"Calendar settings\",\n      \"createView\": \"New view\",\n      \"duplicateView\": \"Duplicate view\",\n      \"deleteView\": \"Delete view\",\n      \"numberOfVisibleFields\": \"{} shown\"\n    },\n    \"filter\": {\n      \"empty\": \"No active filters\",\n      \"addFilter\": \"Add filter\",\n      \"cannotFindCreatableField\": \"Cannot find a suitable field to filter by\",\n      \"conditon\": \"Condition\",\n      \"where\": \"Where\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Contains\",\n      \"doesNotContain\": \"Does not contain\",\n      \"endsWith\": \"Ends with\",\n      \"startWith\": \"Starts with\",\n      \"is\": \"Is\",\n      \"isNot\": \"Is not\",\n      \"isEmpty\": \"Is empty\",\n      \"isNotEmpty\": \"Is not empty\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Not\",\n        \"startWith\": \"Starts with\",\n        \"endWith\": \"Ends with\",\n        \"isEmpty\": \"is empty\",\n        \"isNotEmpty\": \"is not empty\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Ticked\",\n      \"isUnchecked\": \"Unticked\",\n      \"choicechipPrefix\": {\n        \"is\": \"is\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"Is complete\",\n      \"isIncomplted\": \"Is incomplete\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"Is\",\n      \"isNot\": \"Is not\",\n      \"contains\": \"Contains\",\n      \"doesNotContain\": \"Does not contain\",\n      \"isEmpty\": \"Is empty\",\n      \"isNotEmpty\": \"Is not empty\"\n    },\n    \"dateFilter\": {\n      \"is\": \"Is on\",\n      \"before\": \"Is before\",\n      \"after\": \"Is after\",\n      \"onOrBefore\": \"Is on or before\",\n      \"onOrAfter\": \"Is on or after\",\n      \"between\": \"Is between\",\n      \"empty\": \"Is empty\",\n      \"notEmpty\": \"Is not empty\",\n      \"startDate\": \"Start date\",\n      \"endDate\": \"End date\",\n      \"choicechipPrefix\": {\n        \"before\": \"Before\",\n        \"after\": \"After\",\n        \"between\": \"Between\",\n        \"onOrBefore\": \"On or before\",\n        \"onOrAfter\": \"On or after\",\n        \"isEmpty\": \"Is empty\",\n        \"isNotEmpty\": \"Is not empty\"\n      }\n    },\n    \"numberFilter\": {\n      \"equal\": \"Equals\",\n      \"notEqual\": \"Does not equal\",\n      \"lessThan\": \"Is less than\",\n      \"greaterThan\": \"Is greater than\",\n      \"lessThanOrEqualTo\": \"Is less than or equal to\",\n      \"greaterThanOrEqualTo\": \"Is greater than or equal to\",\n      \"isEmpty\": \"Is empty\",\n      \"isNotEmpty\": \"Is not empty\"\n    },\n    \"field\": {\n      \"label\": \"Property\",\n      \"hide\": \"Hide property\",\n      \"show\": \"Show property\",\n      \"insertLeft\": \"Insert left\",\n      \"insertRight\": \"Insert right\",\n      \"duplicate\": \"Duplicate\",\n      \"delete\": \"Delete\",\n      \"wrapCellContent\": \"Wrap text\",\n      \"clear\": \"Clear cells\",\n      \"switchPrimaryFieldTooltip\": \"Cannot change field type of primary field\",\n      \"textFieldName\": \"Text\",\n      \"checkboxFieldName\": \"Tick box\",\n      \"dateFieldName\": \"Date\",\n      \"updatedAtFieldName\": \"Last modified\",\n      \"createdAtFieldName\": \"Created at\",\n      \"numberFieldName\": \"Numbers\",\n      \"singleSelectFieldName\": \"Select\",\n      \"multiSelectFieldName\": \"Multiselect\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Checklist\",\n      \"relationFieldName\": \"Relation\",\n      \"summaryFieldName\": \"AI Summary\",\n      \"timeFieldName\": \"Time\",\n      \"mediaFieldName\": \"Files & media\",\n      \"translateFieldName\": \"AI Translate\",\n      \"translateTo\": \"Translate to\",\n      \"numberFormat\": \"Number format\",\n      \"dateFormat\": \"Date format\",\n      \"includeTime\": \"Include time\",\n      \"isRange\": \"End date\",\n      \"dateFormatFriendly\": \"Month Day, Year\",\n      \"dateFormatISO\": \"Year-Month-Day\",\n      \"dateFormatLocal\": \"Month/Day/Year\",\n      \"dateFormatUS\": \"Year/Month/Day\",\n      \"dateFormatDayMonthYear\": \"Day/Month/Year\",\n      \"timeFormat\": \"Time format\",\n      \"invalidTimeFormat\": \"Invalid format\",\n      \"timeFormatTwelveHour\": \"12 hour\",\n      \"timeFormatTwentyFourHour\": \"24 hour\",\n      \"clearDate\": \"Clear date\",\n      \"dateTime\": \"Date time\",\n      \"startDateTime\": \"Start date time\",\n      \"endDateTime\": \"End date time\",\n      \"failedToLoadDate\": \"Failed to load date value\",\n      \"selectTime\": \"Select time\",\n      \"selectDate\": \"Select date\",\n      \"visibility\": \"Visibility\",\n      \"propertyType\": \"Property type\",\n      \"addSelectOption\": \"Add an option\",\n      \"typeANewOption\": \"Type a new option\",\n      \"optionTitle\": \"Options\",\n      \"addOption\": \"Add option\",\n      \"editProperty\": \"Edit property\",\n      \"newProperty\": \"New property\",\n      \"openRowDocument\": \"Open as a page\",\n      \"deleteFieldPromptMessage\": \"Are you sure? This property and all its data will be deleted\",\n      \"clearFieldPromptMessage\": \"Are you sure? All cells in this column will be emptied\",\n      \"newColumn\": \"New column\",\n      \"format\": \"Format\",\n      \"reminderOnDateTooltip\": \"This cell has a scheduled reminder\",\n      \"optionAlreadyExist\": \"Option already exists\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Add a new field\",\n      \"fieldDragElementTooltip\": \"Click to open menu\",\n      \"showHiddenFields\": {\n        \"one\": \"Show {count} hidden field\",\n        \"many\": \"Show {count} hidden fields\",\n        \"other\": \"Show {count} hidden fields\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"Hide {count} hidden field\",\n        \"many\": \"Hide {count} hidden fields\",\n        \"other\": \"Hide {count} hidden fields\"\n      },\n      \"openAsFullPage\": \"Open as full page\",\n      \"moreRowActions\": \"More row actions\"\n    },\n    \"sort\": {\n      \"ascending\": \"Ascending\",\n      \"descending\": \"Descending\",\n      \"by\": \"By\",\n      \"empty\": \"No active sorts\",\n      \"cannotFindCreatableField\": \"Cannot find a suitable field to sort by\",\n      \"deleteAllSorts\": \"Delete all sorts\",\n      \"addSort\": \"Add sort\",\n      \"sortsActive\": \"Cannot {intention} while sorting\",\n      \"removeSorting\": \"Would you like to remove all the sorts in this view and continue?\",\n      \"fieldInUse\": \"You are already sorting by this field\"\n    },\n    \"row\": {\n      \"label\": \"Row\",\n      \"duplicate\": \"Duplicate\",\n      \"delete\": \"Delete\",\n      \"titlePlaceholder\": \"Untitled\",\n      \"textPlaceholder\": \"Empty\",\n      \"copyProperty\": \"Copied property to clipboard\",\n      \"count\": \"Count\",\n      \"newRow\": \"New row\",\n      \"loadMore\": \"Load more\",\n      \"action\": \"Action\",\n      \"add\": \"Click add to below\",\n      \"drag\": \"Drag to move\",\n      \"deleteRowPrompt\": \"Are you sure you want to delete this row? This action cannot be undone.\",\n      \"deleteCardPrompt\": \"Are you sure you want to delete this card? This action cannot be undone.\",\n      \"dragAndClick\": \"Drag to move, click to open menu\",\n      \"insertRecordAbove\": \"Insert record above\",\n      \"insertRecordBelow\": \"Insert record below\",\n      \"noContent\": \"No content\",\n      \"reorderRowDescription\": \"reorder row\",\n      \"createRowAboveDescription\": \"create a row above\",\n      \"createRowBelowDescription\": \"insert a row below\"\n    },\n    \"selectOption\": {\n      \"create\": \"Create\",\n      \"purpleColor\": \"Purple\",\n      \"pinkColor\": \"Pink\",\n      \"lightPinkColor\": \"Light Pink\",\n      \"orangeColor\": \"Orange\",\n      \"yellowColor\": \"Yellow\",\n      \"limeColor\": \"Lime\",\n      \"greenColor\": \"Green\",\n      \"aquaColor\": \"Aqua\",\n      \"blueColor\": \"Blue\",\n      \"deleteTag\": \"Delete tag\",\n      \"colorPanelTitle\": \"Colour\",\n      \"panelTitle\": \"Select an option or create one\",\n      \"searchOption\": \"Search for an option\",\n      \"searchOrCreateOption\": \"Search for an option or create one\",\n      \"createNew\": \"Create a new\",\n      \"orSelectOne\": \"Or select an option\",\n      \"typeANewOption\": \"Type a new option\",\n      \"tagName\": \"Tag name\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"Task description\",\n      \"addNew\": \"Add a new task\",\n      \"submitNewTask\": \"Create\",\n      \"hideComplete\": \"Hide completed tasks\",\n      \"showComplete\": \"Show all tasks\"\n    },\n    \"url\": {\n      \"launch\": \"Open link in browser\",\n      \"copy\": \"Copied link to clipboard\",\n      \"textFieldHint\": \"Enter a URL\",\n      \"copiedNotification\": \"Copied to clipboard!\"\n    },\n    \"relation\": {\n      \"relatedDatabasePlaceLabel\": \"Related Database\",\n      \"relatedDatabasePlaceholder\": \"None\",\n      \"inRelatedDatabase\": \"In\",\n      \"rowSearchTextFieldPlaceholder\": \"Search\",\n      \"noDatabaseSelected\": \"No database selected, please select one first from the list below:\",\n      \"emptySearchResult\": \"No records found\",\n      \"linkedRowListLabel\": \"{count} linked rows\",\n      \"unlinkedRowListLabel\": \"Link another row\"\n    },\n    \"menuName\": \"Grid\",\n    \"referencedGridPrefix\": \"View of\",\n    \"calculate\": \"Calculate\",\n    \"calculationTypeLabel\": {\n      \"none\": \"None\",\n      \"average\": \"Average\",\n      \"max\": \"Max\",\n      \"median\": \"Median\",\n      \"min\": \"Min\",\n      \"sum\": \"Sum\",\n      \"count\": \"Count\",\n      \"countEmpty\": \"Count empty\",\n      \"countEmptyShort\": \"EMPTY\",\n      \"countNonEmpty\": \"Count not empty\",\n      \"countNonEmptyShort\": \"FILLED\"\n    },\n    \"media\": {\n      \"rename\": \"Rename\",\n      \"download\": \"Download\",\n      \"expand\": \"Expand\",\n      \"delete\": \"Delete\",\n      \"moreFilesHint\": \"+{}\",\n      \"addFileOrImage\": \"Add file or link\",\n      \"attachmentsHint\": \"{}\",\n      \"addFileMobile\": \"Add file\",\n      \"extraCount\": \"+{}\",\n      \"deleteFileDescription\": \"Are you sure you want to delete this file? This action is irreversible.\",\n      \"showFileNames\": \"Show file name\",\n      \"downloadSuccess\": \"File downloaded\",\n      \"downloadFailedToken\": \"Failed to download file, user token unavailable\",\n      \"setAsCover\": \"Set as cover\",\n      \"openInBrowser\": \"Open in browser\",\n      \"embedLink\": \"Embed file link\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"Document\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"creating\": \"Creating...\",\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Select a Board to link to\",\n        \"createANewBoard\": \"Create a new Board\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Select a Grid to link to\",\n        \"createANewGrid\": \"Create a new Grid\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Select a Calendar to link to\",\n        \"createANewCalendar\": \"Create a new Calendar\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"Select a Document to link to\"\n      },\n      \"name\": {\n        \"textStyle\": \"Text Style\",\n        \"list\": \"List\",\n        \"toggle\": \"Toggle\",\n        \"fileAndMedia\": \"File & Media\",\n        \"simpleTable\": \"Simple Table\",\n        \"visuals\": \"Visuals\",\n        \"document\": \"Document\",\n        \"advanced\": \"Advanced\",\n        \"text\": \"Text\",\n        \"heading1\": \"Heading 1\",\n        \"heading2\": \"Heading 2\",\n        \"heading3\": \"Heading 3\",\n        \"image\": \"Image\",\n        \"bulletedList\": \"Bulleted list\",\n        \"numberedList\": \"Numbered list\",\n        \"todoList\": \"To-do list\",\n        \"doc\": \"Doc\",\n        \"linkedDoc\": \"Link to page\",\n        \"grid\": \"Grid\",\n        \"linkedGrid\": \"Linked Grid\",\n        \"kanban\": \"Kanban\",\n        \"linkedKanban\": \"Linked Kanban\",\n        \"calendar\": \"Calendar\",\n        \"linkedCalendar\": \"Linked Calendar\",\n        \"quote\": \"Quote\",\n        \"divider\": \"Divider\",\n        \"table\": \"Table\",\n        \"callout\": \"Callout\",\n        \"outline\": \"Outline\",\n        \"mathEquation\": \"Maths Equation\",\n        \"code\": \"Code\",\n        \"toggleList\": \"Toggle list\",\n        \"toggleHeading1\": \"Toggle heading 1\",\n        \"toggleHeading2\": \"Toggle heading 2\",\n        \"toggleHeading3\": \"Toggle heading 3\",\n        \"emoji\": \"Emoji\",\n        \"aiWriter\": \"Ask AI Anything\",\n        \"dateOrReminder\": \"Date or Reminder\",\n        \"photoGallery\": \"Photo Gallery\",\n        \"file\": \"File\",\n        \"twoColumns\": \"2 Columns\",\n        \"threeColumns\": \"3 Columns\",\n        \"fourColumns\": \"4 Columns\"\n      },\n      \"subPage\": {\n        \"name\": \"Document\",\n        \"keyword1\": \"sub page\",\n        \"keyword2\": \"page\",\n        \"keyword3\": \"child page\",\n        \"keyword4\": \"insert page\",\n        \"keyword5\": \"embed page\",\n        \"keyword6\": \"new page\",\n        \"keyword7\": \"create page\",\n        \"keyword8\": \"document\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Outline\",\n      \"codeBlock\": \"Code Block\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Referenced Board\",\n      \"referencedGrid\": \"Referenced Grid\",\n      \"referencedCalendar\": \"Referenced Calendar\",\n      \"referencedDocument\": \"Referenced Document\",\n      \"aiWriter\": {\n        \"userQuestion\": \"Ask AI anything\",\n        \"continueWriting\": \"Continue writing\",\n        \"fixSpelling\": \"Fix spelling & grammar\",\n        \"improveWriting\": \"Improve writing\",\n        \"summarize\": \"Summarise\",\n        \"explain\": \"Explain\",\n        \"makeShorter\": \"Make shorter\",\n        \"makeLonger\": \"Make longer\"\n      },\n      \"autoGeneratorMenuItemName\": \"AI Writer\",\n      \"autoGeneratorTitleName\": \"AI: Ask AI to write anything...\",\n      \"autoGeneratorLearnMore\": \"Learn more\",\n      \"autoGeneratorGenerate\": \"Generate\",\n      \"autoGeneratorHintText\": \"Ask AI ...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Can't get AI key\",\n      \"autoGeneratorRewrite\": \"Rewrite\",\n      \"smartEdit\": \"Ask AI\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"Fix spelling & grammar\",\n      \"warning\": \"⚠️ AI responses can be inaccurate or misleading.\",\n      \"smartEditSummarize\": \"Summarise\",\n      \"smartEditImproveWriting\": \"Improve writing\",\n      \"smartEditMakeLonger\": \"Make longer\",\n      \"smartEditCouldNotFetchResult\": \"Could not fetch result from AI\",\n      \"smartEditCouldNotFetchKey\": \"Could not fetch AI key\",\n      \"smartEditDisabled\": \"Connect AI in Settings\",\n      \"appflowyAIEditDisabled\": \"Sign in to enable AI features\",\n      \"discardResponse\": \"Are you sure you want to discard the AI response?\",\n      \"createInlineMathEquation\": \"Create equation\",\n      \"fonts\": \"Fonts\",\n      \"insertDate\": \"Insert date\",\n      \"emoji\": \"Emoji\",\n      \"toggleList\": \"Toggle list\",\n      \"emptyToggleHeading\": \"Empty toggle h{}. Click to add content.\",\n      \"emptyToggleList\": \"Empty toggle list. Click to add content.\",\n      \"emptyToggleHeadingWeb\": \"Empty toggle h{level}. Click to add content\",\n      \"quoteList\": \"Quote list\",\n      \"numberedList\": \"Numbered list\",\n      \"bulletedList\": \"Bulleted list\",\n      \"todoList\": \"Todo list\",\n      \"callout\": \"Callout\",\n      \"simpleTable\": {\n        \"moreActions\": {\n          \"color\": \"Colour\",\n          \"align\": \"Align\",\n          \"delete\": \"Delete\",\n          \"duplicate\": \"Duplicate\",\n          \"insertLeft\": \"Insert left\",\n          \"insertRight\": \"Insert right\",\n          \"insertAbove\": \"Insert above\",\n          \"insertBelow\": \"Insert below\",\n          \"headerColumn\": \"Header column\",\n          \"headerRow\": \"Header row\",\n          \"clearContents\": \"Clear contents\",\n          \"setToPageWidth\": \"Set to page width\",\n          \"distributeColumnsWidth\": \"Distribute columns evenly\",\n          \"duplicateRow\": \"Duplicate row\",\n          \"duplicateColumn\": \"Duplicate column\",\n          \"textColor\": \"Text colour\",\n          \"cellBackgroundColor\": \"Cell background colour\",\n          \"duplicateTable\": \"Duplicate table\"\n        },\n        \"clickToAddNewRow\": \"Click to add a new row\",\n        \"clickToAddNewColumn\": \"Click to add a new column\",\n        \"clickToAddNewRowAndColumn\": \"Click to add a new row and column\",\n        \"headerName\": {\n          \"table\": \"Table\",\n          \"alignText\": \"Align text\"\n        }\n      },\n      \"cover\": {\n        \"changeCover\": \"Change Cover\",\n        \"colors\": \"Colours\",\n        \"images\": \"Images\",\n        \"clearAll\": \"Clear All\",\n        \"abstract\": \"Abstract\",\n        \"addCover\": \"Add Cover\",\n        \"addLocalImage\": \"Add local image\",\n        \"invalidImageUrl\": \"Invalid image URL\",\n        \"failedToAddImageToGallery\": \"Failed to add image to gallery\",\n        \"enterImageUrl\": \"Enter image URL\",\n        \"add\": \"Add\",\n        \"back\": \"Back\",\n        \"saveToGallery\": \"Save to gallery\",\n        \"removeIcon\": \"Remove icon\",\n        \"removeCover\": \"Remove cover\",\n        \"pasteImageUrl\": \"Paste image URL\",\n        \"or\": \"OR\",\n        \"pickFromFiles\": \"Pick from files\",\n        \"couldNotFetchImage\": \"Could not fetch image\",\n        \"imageSavingFailed\": \"Image Saving Failed\",\n        \"addIcon\": \"Add icon\",\n        \"changeIcon\": \"Change icon\",\n        \"coverRemoveAlert\": \"It will be removed from cover after it is deleted.\",\n        \"alertDialogConfirmation\": \"Are you sure, you want to continue?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"Maths Equation\",\n        \"addMathEquation\": \"Add a TeX equation\",\n        \"editMathEquation\": \"Edit Maths Equation\"\n      },\n      \"optionAction\": {\n        \"click\": \"Click\",\n        \"toOpenMenu\": \" to open menu\",\n        \"drag\": \"Drag\",\n        \"toMove\": \" to move\",\n        \"delete\": \"Delete\",\n        \"duplicate\": \"Duplicate\",\n        \"turnInto\": \"Turn into\",\n        \"moveUp\": \"Move up\",\n        \"moveDown\": \"Move down\",\n        \"color\": \"Colour\",\n        \"align\": \"Align\",\n        \"left\": \"Left\",\n        \"center\": \"Centre\",\n        \"right\": \"Right\",\n        \"defaultColor\": \"Default\",\n        \"depth\": \"Depth\",\n        \"copyLinkToBlock\": \"Copy link to block\"\n      },\n      \"image\": {\n        \"addAnImage\": \"Add images\",\n        \"copiedToPasteBoard\": \"The image link has been copied to the clipboard\",\n        \"addAnImageDesktop\": \"Add an image\",\n        \"addAnImageMobile\": \"Click to add one or more images\",\n        \"dropImageToInsert\": \"Drop images to insert\",\n        \"imageUploadFailed\": \"Image upload failed\",\n        \"imageDownloadFailed\": \"Image download failed, please try again\",\n        \"imageDownloadFailedToken\": \"Image download failed due to missing user token, please try again\",\n        \"errorCode\": \"Error code\"\n      },\n      \"photoGallery\": {\n        \"name\": \"Photo gallery\",\n        \"imageKeyword\": \"image\",\n        \"imageGalleryKeyword\": \"image gallery\",\n        \"photoKeyword\": \"photo\",\n        \"photoBrowserKeyword\": \"photo browser\",\n        \"galleryKeyword\": \"gallery\",\n        \"addImageTooltip\": \"Add image\",\n        \"changeLayoutTooltip\": \"Change layout\",\n        \"browserLayout\": \"Browser\",\n        \"gridLayout\": \"Grid\",\n        \"deleteBlockTooltip\": \"Delete whole gallery\"\n      },\n      \"math\": {\n        \"copiedToPasteBoard\": \"The maths equation has been copied to the clipboard\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"The link has been copied to the clipboard\",\n        \"convertToLink\": \"Convert to embed link\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Add headings to create a table of contents.\",\n        \"noMatchHeadings\": \"No matching headings found.\"\n      },\n      \"table\": {\n        \"addAfter\": \"Add after\",\n        \"addBefore\": \"Add before\",\n        \"delete\": \"Delete\",\n        \"clear\": \"Clear content\",\n        \"duplicate\": \"Duplicate\",\n        \"bgColor\": \"Background colour\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Copy\",\n        \"cut\": \"Cut\",\n        \"paste\": \"Paste\",\n        \"pasteAsPlainText\": \"Paste as plain text\"\n      },\n      \"action\": \"Actions\",\n      \"database\": {\n        \"selectDataSource\": \"Select data source\",\n        \"noDataSource\": \"No data source\",\n        \"selectADataSource\": \"Select a data source\",\n        \"toContinue\": \"to continue\",\n        \"newDatabase\": \"New Database\",\n        \"linkToDatabase\": \"Link to Database\"\n      },\n      \"date\": \"Date\",\n      \"video\": {\n        \"label\": \"Video\",\n        \"emptyLabel\": \"Add a video\",\n        \"placeholder\": \"Paste the video link\",\n        \"copiedToPasteBoard\": \"The video link has been copied to the clipboard\",\n        \"insertVideo\": \"Add video\",\n        \"invalidVideoUrl\": \"The source URL is not supported yet.\",\n        \"invalidVideoUrlYouTube\": \"YouTube is not supported yet.\",\n        \"supportedFormats\": \"Supported formats: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264\"\n      },\n      \"file\": {\n        \"name\": \"File\",\n        \"uploadTab\": \"Upload\",\n        \"uploadMobile\": \"Choose a file\",\n        \"uploadMobileGallery\": \"From Photo Gallery\",\n        \"networkTab\": \"Embed link\",\n        \"placeholderText\": \"Upload or embed a file\",\n        \"placeholderDragging\": \"Drop the file to upload\",\n        \"dropFileToUpload\": \"Drop a file to upload\",\n        \"fileUploadHint\": \"Drag & drop a file or click to \",\n        \"fileUploadHintSuffix\": \"Browse\",\n        \"networkHint\": \"Paste a file link\",\n        \"networkUrlInvalid\": \"Invalid URL. Check the URL and try again.\",\n        \"networkAction\": \"Embed\",\n        \"fileTooBigError\": \"File size is too big, please upload a file with size less than 10MB\",\n        \"renameFile\": {\n          \"title\": \"Rename file\",\n          \"description\": \"Enter the new name for this file\",\n          \"nameEmptyError\": \"File name cannot be left empty.\"\n        },\n        \"uploadedAt\": \"Uploaded on {}\",\n        \"linkedAt\": \"Link added on {}\",\n        \"failedToOpenMsg\": \"Failed to open, file not found\"\n      },\n      \"subPage\": {\n        \"handlingPasteHint\": \" - (handling paste)\",\n        \"errors\": {\n          \"failedDeletePage\": \"Failed to delete page\",\n          \"failedCreatePage\": \"Failed to create page\",\n          \"failedMovePage\": \"Failed to move page to this document\",\n          \"failedDuplicatePage\": \"Failed to duplicate page\",\n          \"failedDuplicateFindView\": \"Failed to duplicate page - original view not found\"\n        }\n      },\n      \"cannotMoveToItsChildren\": \"Cannot move to its children\"\n    },\n    \"outlineBlock\": {\n      \"placeholder\": \"Table of Contents\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Type '/' for commands\"\n    },\n    \"title\": {\n      \"placeholder\": \"Untitled\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Click to add image(s)\",\n      \"upload\": {\n        \"label\": \"Upload\",\n        \"placeholder\": \"Click to upload image\"\n      },\n      \"url\": {\n        \"label\": \"Image URL\",\n        \"placeholder\": \"Enter image URL\"\n      },\n      \"ai\": {\n        \"label\": \"Generate image from AI\",\n        \"placeholder\": \"Please input the prompt for AI to generate image\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Generate image from Stability AI\",\n        \"placeholder\": \"Please input the prompt for Stability AI to generate image\"\n      },\n      \"support\": \"Image size limit is 5MB. Supported formats: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Invalid image\",\n        \"invalidImageSize\": \"Image size must be less than 5MB\",\n        \"invalidImageFormat\": \"Image format is not supported. Supported formats: JPEG, PNG, JPG, GIF, SVG, WEBP\",\n        \"invalidImageUrl\": \"Invalid image URL\",\n        \"noImage\": \"No such file or directory\",\n        \"multipleImagesFailed\": \"One or more images failed to upload, please try again\"\n      },\n      \"embedLink\": {\n        \"label\": \"Embed link\",\n        \"placeholder\": \"Paste or type an image link\"\n      },\n      \"unsplash\": {\n        \"label\": \"Unsplash\"\n      },\n      \"searchForAnImage\": \"Search for an image\",\n      \"pleaseInputYourOpenAIKey\": \"please input your AI key in Settings page\",\n      \"saveImageToGallery\": \"Save image\",\n      \"failedToAddImageToGallery\": \"Failed to save image\",\n      \"successToAddImageToGallery\": \"Saved image to Photos\",\n      \"unableToLoadImage\": \"Unable to load image\",\n      \"maximumImageSize\": \"Maximum supported upload image size is 10MB\",\n      \"uploadImageErrorImageSizeTooBig\": \"Image size must be less than 10MB\",\n      \"imageIsUploading\": \"Image is uploading\",\n      \"openFullScreen\": \"Open in full screen\",\n      \"interactiveViewer\": {\n        \"toolbar\": {\n          \"previousImageTooltip\": \"Previous image\",\n          \"nextImageTooltip\": \"Next image\",\n          \"zoomOutTooltip\": \"Zoom out\",\n          \"zoomInTooltip\": \"Zoom in\",\n          \"changeZoomLevelTooltip\": \"Change zoom level\",\n          \"openLocalImage\": \"Open image\",\n          \"downloadImage\": \"Download image\",\n          \"closeViewer\": \"Close interactive viewer\",\n          \"scalePercentage\": \"{}%\",\n          \"deleteImageTooltip\": \"Delete image\"\n        }\n      }\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Language\",\n        \"placeholder\": \"Select language\",\n        \"auto\": \"Auto\"\n      },\n      \"copyTooltip\": \"Copy\",\n      \"searchLanguageHint\": \"Search for a language\",\n      \"codeCopiedSnackbar\": \"Code copied to clipboard!\"\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Paste or type a link\",\n      \"openInNewTab\": \"Open in new tab\",\n      \"copyLink\": \"Copy link\",\n      \"removeLink\": \"Remove link\",\n      \"url\": {\n        \"label\": \"Link URL\",\n        \"placeholder\": \"Enter link URL\"\n      },\n      \"title\": {\n        \"label\": \"Link Title\",\n        \"placeholder\": \"Enter link title\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"Mention a person or a page or date...\",\n      \"page\": {\n        \"label\": \"Link to page\",\n        \"tooltip\": \"Click to open page\"\n      },\n      \"deleted\": \"Deleted\",\n      \"deletedContent\": \"This content does not exist or has been deleted\",\n      \"noAccess\": \"No Access\",\n      \"deletedPage\": \"Deleted page\",\n      \"trashHint\": \" - in bin\",\n      \"morePages\": \"more pages\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"Reset to default\",\n      \"textSize\": \"Text size\",\n      \"textColor\": \"Text colour\",\n      \"h1\": \"Heading 1\",\n      \"h2\": \"Heading 2\",\n      \"h3\": \"Heading 3\",\n      \"alignLeft\": \"Align left\",\n      \"alignRight\": \"Align right\",\n      \"alignCenter\": \"Align centre\",\n      \"link\": \"Link\",\n      \"textAlign\": \"Text align\",\n      \"moreOptions\": \"More options\",\n      \"font\": \"Font\",\n      \"inlineCode\": \"Inline code\",\n      \"suggestions\": \"Suggestions\",\n      \"turnInto\": \"Turn into\",\n      \"equation\": \"Equation\",\n      \"insert\": \"Insert\",\n      \"linkInputHint\": \"Paste link or search pages\",\n      \"pageOrURL\": \"Page or URL\",\n      \"linkName\": \"Link Name\",\n      \"linkNameHint\": \"Input link name\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"Unable to parse the block content\",\n      \"clickToCopyTheBlockContent\": \"Click to copy the block content\",\n      \"blockContentHasBeenCopied\": \"The block content has been copied.\",\n      \"parseError\": \"An error occurred while parsing the {} block.\",\n      \"copyBlockContent\": \"Copy block content\"\n    },\n    \"mobilePageSelector\": {\n      \"title\": \"Select page\",\n      \"failedToLoad\": \"Failed to load page list\",\n      \"noPagesFound\": \"No pages found\"\n    },\n    \"attachmentMenu\": {\n      \"choosePhoto\": \"Choose photo\",\n      \"takePicture\": \"Take a picture\",\n      \"chooseFile\": \"Choose file\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"label\": \"Column\",\n      \"createNewCard\": \"New\",\n      \"renameGroupTooltip\": \"Press to rename group\",\n      \"createNewColumn\": \"Add a new group\",\n      \"addToColumnTopTooltip\": \"Add a new card at the top\",\n      \"addToColumnBottomTooltip\": \"Add a new card at the bottom\",\n      \"renameColumn\": \"Rename\",\n      \"hideColumn\": \"Hide\",\n      \"newGroup\": \"New group\",\n      \"deleteColumn\": \"Delete\",\n      \"deleteColumnConfirmation\": \"This will delete this group and all the cards in it. Are you sure you want to continue?\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"Hidden Groups\",\n      \"collapseTooltip\": \"Hide the hidden groups\",\n      \"expandTooltip\": \"View the hidden groups\"\n    },\n    \"cardDetail\": \"Card Detail\",\n    \"cardActions\": \"Card Actions\",\n    \"cardDuplicated\": \"Card has been duplicated\",\n    \"cardDeleted\": \"Card has been deleted\",\n    \"showOnCard\": \"Show on card detail\",\n    \"setting\": \"Setting\",\n    \"propertyName\": \"Property name\",\n    \"menuName\": \"Board\",\n    \"showUngrouped\": \"Show ungrouped items\",\n    \"ungroupedButtonText\": \"Ungrouped\",\n    \"ungroupedButtonTooltip\": \"Contains cards that don't belong in any group\",\n    \"ungroupedItemsTitle\": \"Click to add to the board\",\n    \"groupBy\": \"Group by\",\n    \"groupCondition\": \"Group condition\",\n    \"referencedBoardPrefix\": \"View of\",\n    \"notesTooltip\": \"Notes inside\",\n    \"mobile\": {\n      \"editURL\": \"Edit URL\",\n      \"showGroup\": \"Show group\",\n      \"showGroupContent\": \"Are you sure you want to show this group on the board?\",\n      \"failedToLoad\": \"Failed to load board view\"\n    },\n    \"dateCondition\": {\n      \"weekOf\": \"Week of {} - {}\",\n      \"today\": \"Today\",\n      \"yesterday\": \"Yesterday\",\n      \"tomorrow\": \"Tomorrow\",\n      \"lastSevenDays\": \"Last 7 days\",\n      \"nextSevenDays\": \"Next 7 days\",\n      \"lastThirtyDays\": \"Last 30 days\",\n      \"nextThirtyDays\": \"Next 30 days\"\n    },\n    \"noGroup\": \"No group by property\",\n    \"noGroupDesc\": \"Board views require a property to group by in order to display\",\n    \"media\": {\n      \"cardText\": \"{} {}\",\n      \"fallbackName\": \"files\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Calendar\",\n    \"defaultNewCalendarTitle\": \"Untitled\",\n    \"newEventButtonTooltip\": \"Add a new event\",\n    \"navigation\": {\n      \"today\": \"Today\",\n      \"jumpToday\": \"Jump to Today\",\n      \"previousMonth\": \"Previous Month\",\n      \"nextMonth\": \"Next Month\",\n      \"views\": {\n        \"day\": \"Day\",\n        \"week\": \"Week\",\n        \"month\": \"Month\",\n        \"year\": \"Year\"\n      }\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"No events yet\",\n      \"emptyBody\": \"Press the plus button to create an event on this day.\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Show week numbers\",\n      \"showWeekends\": \"Show weekends\",\n      \"firstDayOfWeek\": \"Start week on\",\n      \"layoutDateField\": \"Layout calendar by\",\n      \"changeLayoutDateField\": \"Change layout field\",\n      \"noDateTitle\": \"No Date\",\n      \"noDateHint\": {\n        \"zero\": \"Unscheduled events will show up here\",\n        \"one\": \"{count} unscheduled event\",\n        \"other\": \"{count} unscheduled events\"\n      },\n      \"unscheduledEventsTitle\": \"Unscheduled events\",\n      \"clickToAdd\": \"Click to add to the calendar\",\n      \"name\": \"Calendar settings\",\n      \"clickToOpen\": \"Click to open the record\"\n    },\n    \"referencedCalendarPrefix\": \"View of\",\n    \"quickJumpYear\": \"Jump to\",\n    \"duplicateEvent\": \"Duplicate event\"\n  },\n  \"errorDialog\": {\n    \"title\": \"@:appName Error\",\n    \"howToFixFallback\": \"We're sorry for the inconvenience! Submit an issue on our GitHub page that describes your error.\",\n    \"howToFixFallbackHint1\": \"We're sorry for the inconvenience! Submit an issue on our \",\n    \"howToFixFallbackHint2\": \" page that describes your error.\",\n    \"github\": \"View on GitHub\"\n  },\n  \"search\": {\n    \"label\": \"Search\",\n    \"sidebarSearchIcon\": \"Search and quickly jump to a page\",\n    \"placeholder\": {\n      \"actions\": \"Search actions...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Copied!\",\n      \"fail\": \"Unable to copy\"\n    }\n  },\n  \"unSupportBlock\": \"The current version does not support this Block.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Are you sure want to delete the {pageType}?\",\n    \"deleteContentCaption\": \"if you delete this {pageType}, you can restore it from the bin.\"\n  },\n  \"colors\": {\n    \"custom\": \"Custom\",\n    \"default\": \"Default\",\n    \"red\": \"Red\",\n    \"orange\": \"Orange\",\n    \"yellow\": \"Yellow\",\n    \"green\": \"Green\",\n    \"blue\": \"Blue\",\n    \"purple\": \"Purple\",\n    \"pink\": \"Pink\",\n    \"brown\": \"Brown\",\n    \"gray\": \"Gray\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"Emoji\",\n    \"search\": \"Search emoji\",\n    \"noRecent\": \"No recent emoji\",\n    \"noEmojiFound\": \"No emoji found\",\n    \"filter\": \"Filter\",\n    \"random\": \"Random\",\n    \"selectSkinTone\": \"Select skin tone\",\n    \"remove\": \"Remove emoji\",\n    \"categories\": {\n      \"smileys\": \"Smileys & Emotion\",\n      \"people\": \"people\",\n      \"animals\": \"nature\",\n      \"food\": \"foods\",\n      \"activities\": \"activities\",\n      \"places\": \"places\",\n      \"objects\": \"objects\",\n      \"symbols\": \"symbols\",\n      \"flags\": \"flags\",\n      \"nature\": \"nature\",\n      \"frequentlyUsed\": \"frequently Used\"\n    },\n    \"skinTone\": {\n      \"default\": \"Default\",\n      \"light\": \"Light\",\n      \"mediumLight\": \"Medium-Light\",\n      \"medium\": \"Medium\",\n      \"mediumDark\": \"Medium-Dark\",\n      \"dark\": \"Dark\"\n    },\n    \"openSourceIconsFrom\": \"Open source icons from\"\n  },\n  \"inlineActions\": {\n    \"noResults\": \"No results\",\n    \"recentPages\": \"Recent pages\",\n    \"pageReference\": \"Page reference\",\n    \"docReference\": \"Document reference\",\n    \"boardReference\": \"Board reference\",\n    \"calReference\": \"Calendar reference\",\n    \"gridReference\": \"Grid reference\",\n    \"date\": \"Date\",\n    \"reminder\": {\n      \"groupTitle\": \"Reminder\",\n      \"shortKeyword\": \"remind\"\n    },\n    \"createPage\": \"Create \\\"{}\\\" sub-page\"\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"Change the date and time format in settings\",\n    \"dateFormat\": \"Date format\",\n    \"includeTime\": \"Include time\",\n    \"isRange\": \"End date\",\n    \"timeFormat\": \"Time format\",\n    \"clearDate\": \"Clear date\",\n    \"reminderLabel\": \"Reminder\",\n    \"selectReminder\": \"Select reminder\",\n    \"reminderOptions\": {\n      \"none\": \"None\",\n      \"atTimeOfEvent\": \"Time of event\",\n      \"fiveMinsBefore\": \"5 mins before\",\n      \"tenMinsBefore\": \"10 mins before\",\n      \"fifteenMinsBefore\": \"15 mins before\",\n      \"thirtyMinsBefore\": \"30 mins before\",\n      \"oneHourBefore\": \"1 hour before\",\n      \"twoHoursBefore\": \"2 hours before\",\n      \"onDayOfEvent\": \"On day of event\",\n      \"oneDayBefore\": \"1 day before\",\n      \"twoDaysBefore\": \"2 days before\",\n      \"oneWeekBefore\": \"1 week before\",\n      \"custom\": \"Custom\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"Yesterday\",\n    \"today\": \"Today\",\n    \"tomorrow\": \"Tomorrow\",\n    \"oneWeek\": \"1 week\"\n  },\n  \"notificationHub\": {\n    \"title\": \"Notifications\",\n    \"mobile\": {\n      \"title\": \"Updates\"\n    },\n    \"emptyTitle\": \"All caught up!\",\n    \"emptyBody\": \"No pending notifications or actions. Enjoy the calm.\",\n    \"tabs\": {\n      \"inbox\": \"Inbox\",\n      \"upcoming\": \"Upcoming\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"Mark all as read\",\n      \"showAll\": \"All\",\n      \"showUnreads\": \"Unread\"\n    },\n    \"filters\": {\n      \"ascending\": \"Ascending\",\n      \"descending\": \"Descending\",\n      \"groupByDate\": \"Group by date\",\n      \"showUnreadsOnly\": \"Show unreads only\",\n      \"resetToDefault\": \"Reset to default\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"Reminder\",\n    \"message\": \"Remember to check this before you forget!\",\n    \"tooltipDelete\": \"Delete\",\n    \"tooltipMarkRead\": \"Mark as read\",\n    \"tooltipMarkUnread\": \"Mark as unread\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"Find\",\n    \"previousMatch\": \"Previous match\",\n    \"nextMatch\": \"Next match\",\n    \"close\": \"Close\",\n    \"replace\": \"Replace\",\n    \"replaceAll\": \"Replace all\",\n    \"noResult\": \"No results\",\n    \"caseSensitive\": \"Case sensitive\",\n    \"searchMore\": \"Search to find more results\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"We're sorry\",\n    \"loadingViewError\": \"We're having trouble loading this view. Please check your internet connection, refresh the app, and do not hesitate to reach out to the team if the issue continues.\",\n    \"syncError\": \"Data is not synced from another device\",\n    \"syncErrorHint\": \"Please reopen this page on the device where it was last edited, then open it again on the current device.\",\n    \"clickToCopy\": \"Click to copy error code\"\n  },\n  \"editor\": {\n    \"bold\": \"Bold\",\n    \"bulletedList\": \"Bulleted list\",\n    \"bulletedListShortForm\": \"Bulleted\",\n    \"checkbox\": \"Tick box\",\n    \"embedCode\": \"Embed Code\",\n    \"heading1\": \"H1\",\n    \"heading2\": \"H2\",\n    \"heading3\": \"H3\",\n    \"highlight\": \"Highlight\",\n    \"color\": \"Colour\",\n    \"image\": \"Image\",\n    \"date\": \"Date\",\n    \"page\": \"Page\",\n    \"italic\": \"Italic\",\n    \"link\": \"Link\",\n    \"numberedList\": \"Numbered list\",\n    \"numberedListShortForm\": \"Numbered\",\n    \"toggleHeading1ShortForm\": \"Toggle H1\",\n    \"toggleHeading2ShortForm\": \"Toggle H2\",\n    \"toggleHeading3ShortForm\": \"Toggle H3\",\n    \"quote\": \"Quote\",\n    \"strikethrough\": \"Strikethrough\",\n    \"text\": \"Text\",\n    \"underline\": \"Underline\",\n    \"fontColorDefault\": \"Default\",\n    \"fontColorGray\": \"Gray\",\n    \"fontColorBrown\": \"Brown\",\n    \"fontColorOrange\": \"Orange\",\n    \"fontColorYellow\": \"Yellow\",\n    \"fontColorGreen\": \"Green\",\n    \"fontColorBlue\": \"Blue\",\n    \"fontColorPurple\": \"Purple\",\n    \"fontColorPink\": \"Pink\",\n    \"fontColorRed\": \"Red\",\n    \"backgroundColorDefault\": \"Default background\",\n    \"backgroundColorGray\": \"Gray background\",\n    \"backgroundColorBrown\": \"Brown background\",\n    \"backgroundColorOrange\": \"Orange background\",\n    \"backgroundColorYellow\": \"Yellow background\",\n    \"backgroundColorGreen\": \"Green background\",\n    \"backgroundColorBlue\": \"Blue background\",\n    \"backgroundColorPurple\": \"Purple background\",\n    \"backgroundColorPink\": \"Pink background\",\n    \"backgroundColorRed\": \"Red background\",\n    \"backgroundColorLime\": \"Lime background\",\n    \"backgroundColorAqua\": \"Aqua background\",\n    \"done\": \"Done\",\n    \"cancel\": \"Cancel\",\n    \"tint1\": \"Tint 1\",\n    \"tint2\": \"Tint 2\",\n    \"tint3\": \"Tint 3\",\n    \"tint4\": \"Tint 4\",\n    \"tint5\": \"Tint 5\",\n    \"tint6\": \"Tint 6\",\n    \"tint7\": \"Tint 7\",\n    \"tint8\": \"Tint 8\",\n    \"tint9\": \"Tint 9\",\n    \"lightLightTint1\": \"Purple\",\n    \"lightLightTint2\": \"Pink\",\n    \"lightLightTint3\": \"Light Pink\",\n    \"lightLightTint4\": \"Orange\",\n    \"lightLightTint5\": \"Yellow\",\n    \"lightLightTint6\": \"Lime\",\n    \"lightLightTint7\": \"Green\",\n    \"lightLightTint8\": \"Aqua\",\n    \"lightLightTint9\": \"Blue\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"Heading 1\",\n    \"mobileHeading2\": \"Heading 2\",\n    \"mobileHeading3\": \"Heading 3\",\n    \"mobileHeading4\": \"Heading 4\",\n    \"mobileHeading5\": \"Heading 5\",\n    \"mobileHeading6\": \"Heading 6\",\n    \"textColor\": \"Text Colour\",\n    \"backgroundColor\": \"Background Colour\",\n    \"addYourLink\": \"Add your link\",\n    \"openLink\": \"Open link\",\n    \"copyLink\": \"Copy link\",\n    \"removeLink\": \"Remove link\",\n    \"editLink\": \"Edit link\",\n    \"linkText\": \"Text\",\n    \"linkTextHint\": \"Please enter text\",\n    \"linkAddressHint\": \"Please enter URL\",\n    \"highlightColor\": \"Highlight colour\",\n    \"clearHighlightColor\": \"Clear highlight colour\",\n    \"customColor\": \"Custom colour\",\n    \"hexValue\": \"Hex value\",\n    \"opacity\": \"Opacity\",\n    \"resetToDefaultColor\": \"Reset to default colour\",\n    \"ltr\": \"LTR\",\n    \"rtl\": \"RTL\",\n    \"auto\": \"Auto\",\n    \"cut\": \"Cut\",\n    \"copy\": \"Copy\",\n    \"paste\": \"Paste\",\n    \"find\": \"Find\",\n    \"select\": \"Select\",\n    \"selectAll\": \"Select all\",\n    \"previousMatch\": \"Previous match\",\n    \"nextMatch\": \"Next match\",\n    \"closeFind\": \"Close\",\n    \"replace\": \"Replace\",\n    \"replaceAll\": \"Replace all\",\n    \"regex\": \"Regex\",\n    \"caseSensitive\": \"Case sensitive\",\n    \"uploadImage\": \"Upload Image\",\n    \"urlImage\": \"URL Image\",\n    \"incorrectLink\": \"Incorrect Link\",\n    \"upload\": \"Upload\",\n    \"chooseImage\": \"Choose an image\",\n    \"loading\": \"Loading\",\n    \"imageLoadFailed\": \"Image load failed\",\n    \"divider\": \"Divider\",\n    \"table\": \"Table\",\n    \"colAddBefore\": \"Add before\",\n    \"rowAddBefore\": \"Add before\",\n    \"colAddAfter\": \"Add after\",\n    \"rowAddAfter\": \"Add after\",\n    \"colRemove\": \"Remove\",\n    \"rowRemove\": \"Remove\",\n    \"colDuplicate\": \"Duplicate\",\n    \"rowDuplicate\": \"Duplicate\",\n    \"colClear\": \"Clear Content\",\n    \"rowClear\": \"Clear Content\",\n    \"slashPlaceHolder\": \"Type '/' to insert a block, or start typing\",\n    \"typeSomething\": \"Type something...\",\n    \"toggleListShortForm\": \"Toggle\",\n    \"quoteListShortForm\": \"Quote\",\n    \"mathEquationShortForm\": \"Formula\",\n    \"codeBlockShortForm\": \"Code\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"No favourite page\",\n    \"noFavoriteHintText\": \"Swipe the page to the left to add it to your favourites\",\n    \"removeFromSidebar\": \"Remove from sidebar\",\n    \"addToSidebar\": \"Pin to sidebar\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"Enter a / to insert a block, or start typing\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"To-do\",\n    \"bulletList\": \"List\",\n    \"numberList\": \"List\",\n    \"quote\": \"Quote\",\n    \"heading\": \"Heading {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"Page icon\",\n    \"language\": \"Language\",\n    \"font\": \"Font\",\n    \"actions\": \"Actions\",\n    \"date\": \"Date\",\n    \"addField\": \"Add field\",\n    \"userIcon\": \"User icon\"\n  },\n  \"noLogFiles\": \"There're no log files\",\n  \"newSettings\": {\n    \"myAccount\": {\n      \"title\": \"My account\",\n      \"subtitle\": \"Customise your profile, manage account security, open AI keys, or login into your account.\",\n      \"profileLabel\": \"Account name & Profile image\",\n      \"profileNamePlaceholder\": \"Enter your name\",\n      \"accountSecurity\": \"Account security\",\n      \"2FA\": \"2-Step Authentication\",\n      \"aiKeys\": \"AI keys\",\n      \"accountLogin\": \"Account Login\",\n      \"updateNameError\": \"Failed to update name\",\n      \"updateIconError\": \"Failed to update icon\",\n      \"aboutAppFlowy\": \"About @:appName\",\n      \"deleteAccount\": {\n        \"title\": \"Delete Account\",\n        \"subtitle\": \"Permanently delete your account and all of your data.\",\n        \"description\": \"Permanently delete your account and remove access from all workspaces.\",\n        \"deleteMyAccount\": \"Delete my account\",\n        \"dialogTitle\": \"Delete account\",\n        \"dialogContent1\": \"Are you sure you want to permanently delete your account?\",\n        \"dialogContent2\": \"This action cannot be undone, and will remove access from all workspaces, erasing your entire account, including private workspaces, and removing you from all shared workspaces.\",\n        \"confirmHint1\": \"Please type \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\" to confirm.\",\n        \"confirmHint2\": \"I understand that this action is irreversible and will permanently delete my account and all associated data.\",\n        \"confirmHint3\": \"DELETE MY ACCOUNT\",\n        \"checkToConfirmError\": \"You must tick the box to confirm deletion\",\n        \"failedToGetCurrentUser\": \"Failed to get current user email\",\n        \"confirmTextValidationFailed\": \"Your confirmation text does not match \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\"\",\n        \"deleteAccountSuccess\": \"Account deleted successfully\"\n      }\n    },\n    \"workplace\": {\n      \"name\": \"Workplace\",\n      \"title\": \"Workplace Settings\",\n      \"subtitle\": \"Customise your workspace appearance, theme, font, text layout, date, time, and language.\",\n      \"workplaceName\": \"Workplace name\",\n      \"workplaceNamePlaceholder\": \"Enter workplace name\",\n      \"workplaceIcon\": \"Workplace icon\",\n      \"workplaceIconSubtitle\": \"Upload an image or use an emoji for your workspace. Icon will show in your sidebar and notifications.\",\n      \"renameError\": \"Failed to rename workplace\",\n      \"updateIconError\": \"Failed to update icon\",\n      \"chooseAnIcon\": \"Choose an icon\",\n      \"appearance\": {\n        \"name\": \"Appearance\",\n        \"themeMode\": {\n          \"auto\": \"Auto\",\n          \"light\": \"Light\",\n          \"dark\": \"Dark\"\n        },\n        \"language\": \"Language\"\n      }\n    },\n    \"syncState\": {\n      \"syncing\": \"Syncing\",\n      \"synced\": \"Synced\",\n      \"noNetworkConnected\": \"No network connected\"\n    }\n  },\n  \"pageStyle\": {\n    \"title\": \"Page style\",\n    \"layout\": \"Layout\",\n    \"coverImage\": \"Cover image\",\n    \"pageIcon\": \"Page icon\",\n    \"colors\": \"Colours\",\n    \"gradient\": \"Gradient\",\n    \"backgroundImage\": \"Background image\",\n    \"presets\": \"Presets\",\n    \"photo\": \"Photo\",\n    \"unsplash\": \"Unsplash\",\n    \"pageCover\": \"Page cover\",\n    \"none\": \"None\",\n    \"openSettings\": \"Open Settings\",\n    \"photoPermissionTitle\": \"@:appName would like to access your photo library\",\n    \"photoPermissionDescription\": \"@:appName needs access to your photos to let you add images to your documents\",\n    \"cameraPermissionTitle\": \"@:appName would like to access your camera\",\n    \"cameraPermissionDescription\": \"@:appName needs access to your camera to let you add images to your documents from the camera\",\n    \"doNotAllow\": \"Don't Allow\",\n    \"image\": \"Image\"\n  },\n  \"commandPalette\": {\n    \"placeholder\": \"Search or ask a question...\",\n    \"bestMatches\": \"Best matches\",\n    \"recentHistory\": \"Recent history\",\n    \"navigateHint\": \"to navigate\",\n    \"loadingTooltip\": \"We are looking for results...\",\n    \"betaLabel\": \"BETA\",\n    \"betaTooltip\": \"We currently only support searching for pages and content in documents\",\n    \"fromTrashHint\": \"From bin\",\n    \"noResultsHint\": \"We didn't find what you're looking for, try searching for another term.\",\n    \"clearSearchTooltip\": \"Clear search field\"\n  },\n  \"space\": {\n    \"delete\": \"Delete\",\n    \"deleteConfirmation\": \"Delete: \",\n    \"deleteConfirmationDescription\": \"All pages within this Space will be deleted and moved to the Bin, and any published pages will be unpublished.\",\n    \"rename\": \"Rename Space\",\n    \"changeIcon\": \"Change icon\",\n    \"manage\": \"Manage Space\",\n    \"addNewSpace\": \"Create Space\",\n    \"collapseAllSubPages\": \"Collapse all subpages\",\n    \"createNewSpace\": \"Create a new space\",\n    \"createSpaceDescription\": \"Create multiple public and private spaces to better organise your work.\",\n    \"spaceName\": \"Space name\",\n    \"spaceNamePlaceholder\": \"e.g. Marketing, Engineering, HR\",\n    \"permission\": \"Space permission\",\n    \"publicPermission\": \"Public\",\n    \"publicPermissionDescription\": \"All workspace members with full access\",\n    \"privatePermission\": \"Private\",\n    \"privatePermissionDescription\": \"Only you can access this space\",\n    \"spaceIconBackground\": \"Background colour\",\n    \"spaceIcon\": \"Icon\",\n    \"dangerZone\": \"Danger Zone\",\n    \"unableToDeleteLastSpace\": \"Unable to delete the last Space\",\n    \"unableToDeleteSpaceNotCreatedByYou\": \"Unable to delete spaces created by others\",\n    \"enableSpacesForYourWorkspace\": \"Enable Spaces for your workspace\",\n    \"title\": \"Spaces\",\n    \"defaultSpaceName\": \"General\",\n    \"upgradeSpaceTitle\": \"Enable Spaces\",\n    \"upgradeSpaceDescription\": \"Create multiple public and private Spaces to better organise your workspace.\",\n    \"upgrade\": \"Update\",\n    \"upgradeYourSpace\": \"Create multiple Spaces\",\n    \"quicklySwitch\": \"Quickly switch to the next space\",\n    \"duplicate\": \"Duplicate Space\",\n    \"movePageToSpace\": \"Move page to space\",\n    \"cannotMovePageToDatabase\": \"Cannot move page to database\",\n    \"switchSpace\": \"Switch space\",\n    \"spaceNameCannotBeEmpty\": \"Space name cannot be empty\",\n    \"success\": {\n      \"deleteSpace\": \"Space deleted successfully\",\n      \"renameSpace\": \"Space renamed successfully\",\n      \"duplicateSpace\": \"Space duplicated successfully\",\n      \"updateSpace\": \"Space updated successfully\"\n    },\n    \"error\": {\n      \"deleteSpace\": \"Failed to delete space\",\n      \"renameSpace\": \"Failed to rename space\",\n      \"duplicateSpace\": \"Failed to duplicate space\",\n      \"updateSpace\": \"Failed to update space\"\n    },\n    \"createSpace\": \"Create space\",\n    \"manageSpace\": \"Manage space\",\n    \"renameSpace\": \"Rename space\",\n    \"mSpaceIconColor\": \"Space icon colour\",\n    \"mSpaceIcon\": \"Space icon\"\n  },\n  \"publish\": {\n    \"hasNotBeenPublished\": \"This page hasn't been published yet\",\n    \"spaceHasNotBeenPublished\": \"Haven't supported publishing a space yet\",\n    \"reportPage\": \"Report page\",\n    \"databaseHasNotBeenPublished\": \"Publishing a database is not supported yet.\",\n    \"createdWith\": \"Created with\",\n    \"downloadApp\": \"Download AppFlowy\",\n    \"copy\": {\n      \"codeBlock\": \"The content of code block has been copied to the clipboard\",\n      \"imageBlock\": \"The image link has been copied to the clipboard\",\n      \"mathBlock\": \"The maths equation has been copied to the clipboard\",\n      \"fileBlock\": \"The file link has been copied to the clipboard\"\n    },\n    \"containsPublishedPage\": \"This page contains one or more published pages. If you continue, they will be unpublished. Do you want to proceed with deletion?\",\n    \"publishSuccessfully\": \"Published successfully\",\n    \"unpublishSuccessfully\": \"Unpublished successfully\",\n    \"publishFailed\": \"Failed to publish\",\n    \"unpublishFailed\": \"Failed to unpublish\",\n    \"noAccessToVisit\": \"No access to this page...\",\n    \"createWithAppFlowy\": \"Create a website with AppFlowy\",\n    \"fastWithAI\": \"Fast and easy with AI.\",\n    \"tryItNow\": \"Try it now\",\n    \"onlyGridViewCanBePublished\": \"Only Grid view can be published\",\n    \"database\": {\n      \"zero\": \"Publish {} selected view\",\n      \"one\": \"Publish {} selected views\",\n      \"many\": \"Publish {} selected views\",\n      \"other\": \"Publish {} selected views\"\n    },\n    \"mustSelectPrimaryDatabase\": \"The primary view must be selected\",\n    \"noDatabaseSelected\": \"No database selected, please select at least one database.\",\n    \"unableToDeselectPrimaryDatabase\": \"Unable to deselect primary database\",\n    \"saveThisPage\": \"Start with this template\",\n    \"duplicateTitle\": \"Where would you like to add\",\n    \"selectWorkspace\": \"Select a workspace\",\n    \"addTo\": \"Add to\",\n    \"duplicateSuccessfully\": \"Added to your workspace\",\n    \"duplicateSuccessfullyDescription\": \"Don't have AppFlowy installed? The download will start automatically after you click 'Download'.\",\n    \"downloadIt\": \"Download\",\n    \"openApp\": \"Open in app\",\n    \"duplicateFailed\": \"Duplicated failed\",\n    \"membersCount\": {\n      \"zero\": \"No members\",\n      \"one\": \"1 member\",\n      \"many\": \"{count} members\",\n      \"other\": \"{count} members\"\n    },\n    \"useThisTemplate\": \"Use the template\"\n  },\n  \"web\": {\n    \"continue\": \"Continue\",\n    \"or\": \"or\",\n    \"continueWithGoogle\": \"Continue with Google\",\n    \"continueWithGithub\": \"Continue with GitHub\",\n    \"continueWithDiscord\": \"Continue with Discord\",\n    \"continueWithApple\": \"Continue with Apple \",\n    \"moreOptions\": \"More options\",\n    \"collapse\": \"Collapse\",\n    \"signInAgreement\": \"By clicking \\\"Continue\\\" above, you agreed to AppFlowy's\",\n    \"signInLocalAgreement\": \"By clicking \\\"Get Started\\\" above, you agreed to AppFlowy's\",\n    \"and\": \"and\",\n    \"termOfUse\": \"Terms\",\n    \"privacyPolicy\": \"Privacy Policy\",\n    \"signInError\": \"Sign in error\",\n    \"login\": \"Sign up or log in\",\n    \"fileBlock\": {\n      \"uploadedAt\": \"Uploaded on {time}\",\n      \"linkedAt\": \"Link added on {time}\",\n      \"empty\": \"Upload or embed a file\",\n      \"uploadFailed\": \"Upload failed, please try again\",\n      \"retry\": \"Retry\"\n    },\n    \"importNotion\": \"Import from Notion\",\n    \"import\": \"Import\",\n    \"importSuccess\": \"Uploaded successfully\",\n    \"importSuccessMessage\": \"We'll notify you when the import is complete. After that, you can view your imported pages in the sidebar.\",\n    \"importFailed\": \"Import failed, please check the file format\",\n    \"dropNotionFile\": \"Drop your Notion zip file here to upload, or click to browse\",\n    \"error\": {\n      \"pageNameIsEmpty\": \"The page name is empty, please try another one\"\n    }\n  },\n  \"globalComment\": {\n    \"comments\": \"Comments\",\n    \"addComment\": \"Add a comment\",\n    \"reactedBy\": \"reacted by\",\n    \"addReaction\": \"Add reaction\",\n    \"reactedByMore\": \"and {count} others\",\n    \"showSeconds\": {\n      \"one\": \"1 second ago\",\n      \"other\": \"{count} seconds ago\",\n      \"zero\": \"Just now\",\n      \"many\": \"{count} seconds ago\"\n    },\n    \"showMinutes\": {\n      \"one\": \"1 minute ago\",\n      \"other\": \"{count} minutes ago\",\n      \"many\": \"{count} minutes ago\"\n    },\n    \"showHours\": {\n      \"one\": \"1 hour ago\",\n      \"other\": \"{count} hours ago\",\n      \"many\": \"{count} hours ago\"\n    },\n    \"showDays\": {\n      \"one\": \"1 day ago\",\n      \"other\": \"{count} days ago\",\n      \"many\": \"{count} days ago\"\n    },\n    \"showMonths\": {\n      \"one\": \"1 month ago\",\n      \"other\": \"{count} months ago\",\n      \"many\": \"{count} months ago\"\n    },\n    \"showYears\": {\n      \"one\": \"1 year ago\",\n      \"other\": \"{count} years ago\",\n      \"many\": \"{count} years ago\"\n    },\n    \"reply\": \"Reply\",\n    \"deleteComment\": \"Delete comment\",\n    \"youAreNotOwner\": \"You are not the owner of this comment\",\n    \"confirmDeleteDescription\": \"Are you sure you want to delete this comment?\",\n    \"hasBeenDeleted\": \"Deleted\",\n    \"replyingTo\": \"Replying to\",\n    \"noAccessDeleteComment\": \"You're not allowed to delete this comment\",\n    \"collapse\": \"Collapse\",\n    \"readMore\": \"Read more\",\n    \"failedToAddComment\": \"Failed to add comment\",\n    \"commentAddedSuccessfully\": \"Comment added successfully.\",\n    \"commentAddedSuccessTip\": \"You've just added or replied to a comment. Would you like to jump to the top to see the latest comments?\"\n  },\n  \"template\": {\n    \"asTemplate\": \"Save as template\",\n    \"name\": \"Template name\",\n    \"description\": \"Template Description\",\n    \"about\": \"Template About\",\n    \"deleteFromTemplate\": \"Delete from templates\",\n    \"preview\": \"Template Preview\",\n    \"categories\": \"Template Categories\",\n    \"isNewTemplate\": \"PIN to New template\",\n    \"featured\": \"PIN to Featured\",\n    \"relatedTemplates\": \"Related Templates\",\n    \"requiredField\": \"{field} is required\",\n    \"addCategory\": \"Add \\\"{category}\\\"\",\n    \"addNewCategory\": \"Add new category\",\n    \"addNewCreator\": \"Add new creator\",\n    \"deleteCategory\": \"Delete category\",\n    \"editCategory\": \"Edit category\",\n    \"editCreator\": \"Edit creator\",\n    \"category\": {\n      \"name\": \"Category name\",\n      \"icon\": \"Category icon\",\n      \"bgColor\": \"Category background colour\",\n      \"priority\": \"Category priority\",\n      \"desc\": \"Category description\",\n      \"type\": \"Category type\",\n      \"icons\": \"Category Icons\",\n      \"colors\": \"Category Colours\",\n      \"byUseCase\": \"By Use Case\",\n      \"byFeature\": \"By Feature\",\n      \"deleteCategory\": \"Delete category\",\n      \"deleteCategoryDescription\": \"Are you sure you want to delete this category?\",\n      \"typeToSearch\": \"Type to search categories...\"\n    },\n    \"creator\": {\n      \"label\": \"Template Creator\",\n      \"name\": \"Creator name\",\n      \"avatar\": \"Creator avatar\",\n      \"accountLinks\": \"Creator account links\",\n      \"uploadAvatar\": \"Click to upload avatar\",\n      \"deleteCreator\": \"Delete creator\",\n      \"deleteCreatorDescription\": \"Are you sure you want to delete this creator?\",\n      \"typeToSearch\": \"Type to search creators...\"\n    },\n    \"uploadSuccess\": \"Template uploaded successfully\",\n    \"uploadSuccessDescription\": \"Your template has been uploaded successfully. You can now view it in the template gallery.\",\n    \"viewTemplate\": \"View template\",\n    \"deleteTemplate\": \"Delete template\",\n    \"deleteSuccess\": \"Template deleted successfully\",\n    \"deleteTemplateDescription\": \"This won't affect the current page or published status. Are you sure you want to delete this template?\",\n    \"addRelatedTemplate\": \"Add related template\",\n    \"removeRelatedTemplate\": \"Remove related template\",\n    \"uploadAvatar\": \"Upload avatar\",\n    \"searchInCategory\": \"Search in {category}\",\n    \"label\": \"Templates\"\n  },\n  \"fileDropzone\": {\n    \"dropFile\": \"Click or drag file to this area to upload\",\n    \"uploading\": \"Uploading...\",\n    \"uploadFailed\": \"Upload failed\",\n    \"uploadSuccess\": \"Upload success\",\n    \"uploadSuccessDescription\": \"The file has been uploaded successfully\",\n    \"uploadFailedDescription\": \"The file upload failed\",\n    \"uploadingDescription\": \"The file is being uploaded\"\n  },\n  \"gallery\": {\n    \"preview\": \"Open in full screen\",\n    \"copy\": \"Copy\",\n    \"download\": \"Download\",\n    \"prev\": \"Previous\",\n    \"next\": \"Next\",\n    \"resetZoom\": \"Reset zoom\",\n    \"zoomIn\": \"Zoom in\",\n    \"zoomOut\": \"Zoom out\"\n  },\n  \"invitation\": {\n    \"join\": \"Join\",\n    \"on\": \"on\",\n    \"invitedBy\": \"Invited by\",\n    \"membersCount\": {\n      \"zero\": \"{count} members\",\n      \"one\": \"{count} member\",\n      \"many\": \"{count} members\",\n      \"other\": \"{count} members\"\n    },\n    \"tip\": \"You’ve been invited to Join this workspace with the contact information below. If this is incorrect, contact your administrator to resend the invite.\",\n    \"joinWorkspace\": \"Join workspace\",\n    \"success\": \"You've successfully joined the workspace\",\n    \"successMessage\": \"You can now access all the pages and workspaces within it.\",\n    \"openWorkspace\": \"Open AppFlowy\",\n    \"alreadyAccepted\": \"You've already accepted the invitation\",\n    \"errorModal\": {\n      \"title\": \"Something went wrong\",\n      \"description\": \"Your current account {email} may not have access to this workspace. Please log in with the correct account or contact the workspace owner for help.\",\n      \"contactOwner\": \"Contact owner\",\n      \"close\": \"Back to home\",\n      \"changeAccount\": \"Change account\"\n    }\n  },\n  \"requestAccess\": {\n    \"title\": \"No access to this page\",\n    \"subtitle\": \"You can request access from the owner of this page. Once approved, you can view the page.\",\n    \"requestAccess\": \"Request access\",\n    \"backToHome\": \"Back to home\",\n    \"tip\": \"You're currently logged in as <link/>.\",\n    \"mightBe\": \"You might need to <login/> with a different account.\",\n    \"successful\": \"Request sent successfully\",\n    \"successfulMessage\": \"You will be notified once the owner approves your request.\",\n    \"requestError\": \"Failed to request access\",\n    \"repeatRequestError\": \"You've already requested access to this page\"\n  },\n  \"approveAccess\": {\n    \"title\": \"Approve Workspace Join Request\",\n    \"requestSummary\": \"<user/> requests to join <workspace/> and access <page/>\",\n    \"upgrade\": \"upgrade\",\n    \"downloadApp\": \"Download AppFlowy\",\n    \"approveButton\": \"Approve\",\n    \"approveSuccess\": \"Approved successfully\",\n    \"approveError\": \"Failed to approve, ensure the workspace plan limit is not exceeded\",\n    \"getRequestInfoError\": \"Failed to get request info\",\n    \"memberCount\": {\n      \"zero\": \"No members\",\n      \"one\": \"1 member\",\n      \"many\": \"{count} members\",\n      \"other\": \"{count} members\"\n    },\n    \"alreadyProTitle\": \"You've reached the workspace plan limit\",\n    \"alreadyProMessage\": \"Ask them to contact <email/> to unlock more members\",\n    \"repeatApproveError\": \"You've already approved this request\",\n    \"ensurePlanLimit\": \"Ensure the workspace plan limit is not exceeded. If the limit is exceeded, consider <upgrade/> the workspace plan or <download/>.\",\n    \"requestToJoin\": \"requested to join\",\n    \"asMember\": \"as a member\"\n  },\n  \"upgradePlanModal\": {\n    \"title\": \"Upgrade to Pro\",\n    \"message\": \"{name} has reached the free member limit. Upgrade to the Pro Plan to invite more members.\",\n    \"upgradeSteps\": \"How to upgrade your plan on AppFlowy:\",\n    \"step1\": \"1. Go to Settings\",\n    \"step2\": \"2. Click on 'Plan'\",\n    \"step3\": \"3. Select 'Change Plan'\",\n    \"appNote\": \"Note: \",\n    \"actionButton\": \"Upgrade\",\n    \"downloadLink\": \"Download App\",\n    \"laterButton\": \"Later\",\n    \"refreshNote\": \"After successful upgrade, click <refresh/> to activate your new features.\",\n    \"refresh\": \"here\"\n  },\n  \"breadcrumbs\": {\n    \"label\": \"Breadcrumbs\"\n  },\n  \"time\": {\n    \"justNow\": \"Just now\",\n    \"seconds\": {\n      \"one\": \"1 second\",\n      \"other\": \"{count} seconds\"\n    },\n    \"minutes\": {\n      \"one\": \"1 minute\",\n      \"other\": \"{count} minutes\"\n    },\n    \"hours\": {\n      \"one\": \"1 hour\",\n      \"other\": \"{count} hours\"\n    },\n    \"days\": {\n      \"one\": \"1 day\",\n      \"other\": \"{count} days\"\n    },\n    \"weeks\": {\n      \"one\": \"1 week\",\n      \"other\": \"{count} weeks\"\n    },\n    \"months\": {\n      \"one\": \"1 month\",\n      \"other\": \"{count} months\"\n    },\n    \"years\": {\n      \"one\": \"1 year\",\n      \"other\": \"{count} years\"\n    },\n    \"ago\": \"ago\",\n    \"yesterday\": \"Yesterday\",\n    \"today\": \"Today\"\n  },\n  \"members\": {\n    \"zero\": \"No members\",\n    \"one\": \"1 member\",\n    \"many\": \"{count} members\",\n    \"other\": \"{count} members\"\n  },\n  \"tabMenu\": {\n    \"close\": \"Close\",\n    \"closeDisabledHint\": \"Cannot close a pinned tab, please unpin first\",\n    \"closeOthers\": \"Close other tabs\",\n    \"closeOthersHint\": \"This will close all unpinned tabs except this one\",\n    \"closeOthersDisabledHint\": \"All tabs are pinned, cannot find any tabs to close\",\n    \"favorite\": \"Favourite\",\n    \"unfavorite\": \"Unfavourite\",\n    \"favoriteDisabledHint\": \"Cannot favourite this view\",\n    \"pinTab\": \"Pin\",\n    \"unpinTab\": \"Unpin\"\n  },\n  \"openFileMessage\": {\n    \"success\": \"File opened successfully\",\n    \"fileNotFound\": \"File not found\",\n    \"noAppToOpenFile\": \"No app to open this file\",\n    \"permissionDenied\": \"No permission to open this file\",\n    \"unknownError\": \"File open failed\"\n  },\n  \"inviteMember\": {\n    \"requestInviteMembers\": \"Invite to your workspace\",\n    \"inviteFailedMemberLimit\": \"Member limit has been reached, please \",\n    \"upgrade\": \"upgrade\",\n    \"addEmail\": \"email@example.com, email2@example.com...\",\n    \"requestInvites\": \"Send invites\",\n    \"inviteAlready\": \"You've already invited this email: {email}\",\n    \"inviteSuccess\": \"Invitation sent successfully\",\n    \"description\": \"Input emails below with commas between them. Charges are based on member count.\",\n    \"emails\": \"Email\"\n  },\n  \"quickNote\": {\n    \"label\": \"Quick Note\",\n    \"quickNotes\": \"Quick Notes\",\n    \"search\": \"Search Quick Notes\",\n    \"collapseFullView\": \"Collapse full view\",\n    \"expandFullView\": \"Expand full view\",\n    \"createFailed\": \"Failed to create Quick Note\",\n    \"quickNotesEmpty\": \"No Quick Notes\",\n    \"emptyNote\": \"Empty note\",\n    \"deleteNotePrompt\": \"The selected note will be deleted permanently. Are you sure you want to delete it?\",\n    \"addNote\": \"New Note\",\n    \"noAdditionalText\": \"No additional text\"\n  },\n  \"subscribe\": {\n    \"upgradePlanTitle\": \"Compare & select plan\",\n    \"yearly\": \"Yearly\",\n    \"save\": \"Save {discount}%\",\n    \"monthly\": \"Monthly\",\n    \"priceIn\": \"Price in \",\n    \"free\": \"Free\",\n    \"pro\": \"Pro\",\n    \"freeDescription\": \"For individuals up to 2 members to organise everything\",\n    \"proDescription\": \"For small teams to manage projects and team knowledge\",\n    \"proDuration\": {\n      \"monthly\": \"per member per month\\nbilled monthly\",\n      \"yearly\": \"per member per month\\nbilled annually\"\n    },\n    \"cancel\": \"Downgrade\",\n    \"changePlan\": \"Upgrade to Pro Plan\",\n    \"everythingInFree\": \"Everything in Free +\",\n    \"currentPlan\": \"Current\",\n    \"freeDuration\": \"forever\",\n    \"freePoints\": {\n      \"first\": \"1 collaborative workspace up to 2 members\",\n      \"second\": \"Unlimited pages & blocks\",\n      \"three\": \"5 GB storage\",\n      \"four\": \"Intelligent search\",\n      \"five\": \"20 AI responses\",\n      \"six\": \"Mobile app\",\n      \"seven\": \"Real-time collaboration\"\n    },\n    \"proPoints\": {\n      \"first\": \"Unlimited storage\",\n      \"second\": \"Up to 10 workspace members\",\n      \"three\": \"Unlimited AI responses\",\n      \"four\": \"Unlimited file uploads\",\n      \"five\": \"Custom namespace\"\n    },\n    \"cancelPlan\": {\n      \"title\": \"Sorry to see you go\",\n      \"success\": \"Your subscription has been canceled successfully\",\n      \"description\": \"We're sorry to see you go. We'd love to hear your feedback to help us improve AppFlowy. Please take a moment to answer a few questions.\",\n      \"commonOther\": \"Other\",\n      \"otherHint\": \"Write your answer here\",\n      \"questionOne\": {\n        \"question\": \"What prompted you to cancel your AppFlowy Pro subscription?\",\n        \"answerOne\": \"Cost too high\",\n        \"answerTwo\": \"Features did not meet expectations\",\n        \"answerThree\": \"Found a better alternative\",\n        \"answerFour\": \"Did not use it enough to justify the expense\",\n        \"answerFive\": \"Service issue or technical difficulties\"\n      },\n      \"questionTwo\": {\n        \"question\": \"How likely are you to consider re-subscribing to AppFlowy Pro in the future?\",\n        \"answerOne\": \"Very likely\",\n        \"answerTwo\": \"Somewhat likely\",\n        \"answerThree\": \"Not sure\",\n        \"answerFour\": \"Unlikely\",\n        \"answerFive\": \"Very unlikely\"\n      },\n      \"questionThree\": {\n        \"question\": \"Which Pro feature did you value the most during your subscription?\",\n        \"answerOne\": \"Multi-user collaboration\",\n        \"answerTwo\": \"Longer time version history\",\n        \"answerThree\": \"Unlimited AI responses\",\n        \"answerFour\": \"Access to local AI models\"\n      },\n      \"questionFour\": {\n        \"question\": \"How would you describe your overall experience with AppFlowy?\",\n        \"answerOne\": \"Great\",\n        \"answerTwo\": \"Good\",\n        \"answerThree\": \"Average\",\n        \"answerFour\": \"Below average\",\n        \"answerFive\": \"Unsatisfied\"\n      }\n    }\n  },\n  \"ai\": {\n    \"contentPolicyViolation\": \"Image generation failed due to sensitive content. Please rephrase your input and try again\",\n    \"textLimitReachedDescription\": \"Your workspace has run out of free AI responses. Upgrade to the Pro Plan or purchase an AI add-on to unlock unlimited responses\",\n    \"imageLimitReachedDescription\": \"You've used up your free AI image quota. Please upgrade to the Pro Plan or purchase an AI add-on to unlock unlimited responses\",\n    \"limitReachedAction\": {\n      \"textDescription\": \"Your workspace has run out of free AI responses. To get more responses, please\",\n      \"imageDescription\": \"You've used up your free AI image quota. Please\",\n      \"upgrade\": \"upgrade\",\n      \"toThe\": \"to the\",\n      \"proPlan\": \"Pro Plan\",\n      \"orPurchaseAn\": \"or purchase an\",\n      \"aiAddon\": \"AI add-on\"\n    },\n    \"editing\": \"Editing\",\n    \"analyzing\": \"Analysing\",\n    \"continueWritingEmptyDocumentTitle\": \"Continue writing error\",\n    \"continueWritingEmptyDocumentDescription\": \"We are having trouble expanding on the content in your document. Write a small intro and we can take it from there!\",\n    \"more\": \"More\"\n  },\n  \"autoUpdate\": {\n    \"criticalUpdateTitle\": \"Update required to continue\",\n    \"criticalUpdateDescription\": \"We've made improvements to enhance your experience! Please update from {currentVersion} to {newVersion} to keep using the app.\",\n    \"criticalUpdateButton\": \"Update\",\n    \"bannerUpdateTitle\": \"New Version Available!\",\n    \"bannerUpdateDescription\": \"Get the latest features and fixes. Click \\\"Update\\\" to install now\",\n    \"bannerUpdateButton\": \"Update\",\n    \"settingsUpdateTitle\": \"New Version ({newVersion}) Available!\",\n    \"settingsUpdateDescription\": \"Current version: {currentVersion} (Official build) → {newVersion}\",\n    \"settingsUpdateButton\": \"Update\",\n    \"settingsUpdateWhatsNew\": \"What's new\"\n  },\n  \"lockPage\": {\n    \"lockPage\": \"Locked\",\n    \"reLockPage\": \"Re-lock\",\n    \"lockTooltip\": \"Page locked to prevent accidental editing. Click to unlock.\",\n    \"pageLockedToast\": \"Page locked. Editing is disabled until someone unlocks it.\",\n    \"lockedOperationTooltip\": \"Page locked to prevent accidental editing.\"\n  },\n  \"suggestion\": {\n    \"accept\": \"Accept\",\n    \"keep\": \"Keep\",\n    \"discard\": \"Discard\",\n    \"close\": \"Close\",\n    \"tryAgain\": \"Try again\",\n    \"rewrite\": \"Rewrite\",\n    \"insertBelow\": \"Insert below\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/en-US.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Me\",\n  \"welcomeText\": \"Welcome to @:appName\",\n  \"welcomeTo\": \"Welcome to\",\n  \"githubStarText\": \"Star on GitHub\",\n  \"subscribeNewsletterText\": \"Subscribe to Newsletter\",\n  \"letsGoButtonText\": \"Quick Start\",\n  \"title\": \"Title\",\n  \"youCanAlso\": \"You can also\",\n  \"and\": \"and\",\n  \"failedToOpenUrl\": \"Failed to open url: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Click to add below\",\n    \"addAboveCmd\": \"Alt+click\",\n    \"addAboveMacCmd\": \"Option+click\",\n    \"addAboveTooltip\": \"to add above\",\n    \"dragTooltip\": \"Drag to move\",\n    \"openMenuTooltip\": \"Click to open menu\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Sign Up\",\n    \"title\": \"Sign Up to @:appName\",\n    \"getStartedText\": \"Get Started\",\n    \"emptyPasswordError\": \"Password can't be empty\",\n    \"repeatPasswordEmptyError\": \"Repeat password can't be empty\",\n    \"unmatchedPasswordError\": \"Repeat password is not the same as password\",\n    \"alreadyHaveAnAccount\": \"Already have an account?\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Password\",\n    \"repeatPasswordHint\": \"Repeat password\",\n    \"signUpWith\": \"Sign up with:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Login to @:appName\",\n    \"loginButtonText\": \"Login\",\n    \"loginStartWithAnonymous\": \"Continue with an anonymous session\",\n    \"continueAnonymousUser\": \"Continue with an anonymous session\",\n    \"continueWithLocalModel\": \"Continue with local model\",\n    \"switchToAppFlowyCloud\": \"AppFlowy Cloud\",\n    \"anonymousMode\": \"Anonymous mode\",\n    \"buttonText\": \"Sign In\",\n    \"signingInText\": \"Signing in...\",\n    \"forgotPassword\": \"Forgot Password?\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Password\",\n    \"dontHaveAnAccount\": \"Don't have an account?\",\n    \"createAccount\": \"Create account\",\n    \"repeatPasswordEmptyError\": \"Repeat password can't be empty\",\n    \"unmatchedPasswordError\": \"Repeat password is not the same as password\",\n    \"passwordMustContain\": \"Password must contain at least one letter, one number, and one symbol.\",\n    \"syncPromptMessage\": \"Syncing the data might take a while. Please don't close this page\",\n    \"or\": \"or\",\n    \"signInWithGoogle\": \"Continue with Google\",\n    \"signInWithGithub\": \"Continue with GitHub\",\n    \"signInWithDiscord\": \"Continue with Discord\",\n    \"signInWithApple\": \"Continue with Apple\",\n    \"continueAnotherWay\": \"Continue another way\",\n    \"signUpWithGoogle\": \"Sign up with Google\",\n    \"signUpWithGithub\": \"Sign up with Github\",\n    \"signUpWithDiscord\": \"Sign up with Discord\",\n    \"signInWith\": \"Continue with:\",\n    \"signInWithEmail\": \"Continue with Email\",\n    \"signInWithMagicLink\": \"Continue\",\n    \"signUpWithMagicLink\": \"Sign up with Magic Link\",\n    \"pleaseInputYourEmail\": \"Please enter your email address\",\n    \"settings\": \"Settings\",\n    \"magicLinkSent\": \"Magic Link sent!\",\n    \"invalidEmail\": \"Please enter a valid email address\",\n    \"alreadyHaveAnAccount\": \"Already have an account?\",\n    \"logIn\": \"Log in\",\n    \"generalError\": \"Something went wrong. Please try again later\",\n    \"limitRateError\": \"For security reasons, you can only request a magic link every 60 seconds\",\n    \"magicLinkSentDescription\": \"A Magic Link was sent to your email. Click the link to complete your login. The link will expire after 5 minutes.\",\n    \"tokenHasExpiredOrInvalid\": \"The code has expired or is invalid. Please try again.\",\n    \"signingIn\": \"Signing in...\",\n    \"checkYourEmail\": \"Check your email\",\n    \"temporaryVerificationLinkSent\": \"A temporary verification link has been sent.\\nPlease check your inbox at\",\n    \"temporaryVerificationCodeSent\": \"A temporary verification code has been sent.\\nPlease check your inbox at\",\n    \"continueToSignIn\": \"Continue to sign in\",\n    \"continueWithLoginCode\": \"Continue with login code\",\n    \"backToLogin\": \"Back to login\",\n    \"enterCode\": \"Enter code\",\n    \"enterCodeManually\": \"Enter code manually\",\n    \"continueWithEmail\": \"Continue with email\",\n    \"enterPassword\": \"Enter password\",\n    \"loginAs\": \"Login as\",\n    \"invalidVerificationCode\": \"Please enter a valid verification code\",\n    \"tooFrequentVerificationCodeRequest\": \"You have made too many requests. Please try again later.\",\n    \"invalidLoginCredentials\": \"Your password is incorrect, please try again\",\n    \"resetPassword\": \"Reset password\",\n    \"resetPasswordDescription\": \"Enter your email to reset your password\",\n    \"continueToResetPassword\": \"Continue to reset password\",\n    \"resetPasswordSuccess\": \"Password reset successfully\",\n    \"resetPasswordFailed\": \"Failed to reset password\",\n    \"resetPasswordLinkSent\": \"A password reset link has been sent to your email. Please check your inbox at\",\n    \"resetPasswordLinkExpired\": \"The password reset link has expired. Please request a new link.\",\n    \"resetPasswordLinkInvalid\": \"The password reset link is invalid. Please request a new link.\",\n    \"enterNewPasswordFor\": \"Enter new password for \",\n    \"newPassword\": \"New password\",\n    \"enterNewPassword\": \"Enter new password\",\n    \"confirmPassword\": \"Confirm password\",\n    \"confirmNewPassword\": \"Enter new password\",\n    \"newPasswordCannotBeEmpty\": \"New password cannot be empty\",\n    \"confirmPasswordCannotBeEmpty\": \"Confirm password cannot be empty\",\n    \"passwordsDoNotMatch\": \"Passwords do not match\",\n    \"verifying\": \"Verifying...\",\n    \"continueWithPassword\": \"Continue with password\",\n    \"youAreInLocalMode\": \"You're in local mode.\",\n    \"loginToAppFlowyCloud\": \"Login to AppFlowy Cloud\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Choose your workspace\",\n    \"defaultName\": \"My Workspace\",\n    \"create\": \"Create workspace\",\n    \"new\": \"New workspace\",\n    \"importFromNotion\": \"Import from Notion\",\n    \"learnMore\": \"Learn more\",\n    \"reset\": \"Reset workspace\",\n    \"renameWorkspace\": \"Rename workspace\",\n    \"workspaceNameCannotBeEmpty\": \"Workspace name cannot be empty\",\n    \"resetWorkspacePrompt\": \"Resetting the workspace will delete all pages and data within it. Are you sure you want to reset the workspace? Alternatively, you can contact the support team to restore the workspace\",\n    \"hint\": \"workspace\",\n    \"notFoundError\": \"Workspace not found\",\n    \"failedToLoad\": \"Something went wrong! Failed to load the workspace. Try to close any open instance of @:appName and try again.\",\n    \"errorActions\": {\n      \"reportIssue\": \"Report an issue\",\n      \"reportIssueOnGithub\": \"Report an issue on Github\",\n      \"exportLogFiles\": \"Export log files\",\n      \"reachOut\": \"Reach out on Discord\"\n    },\n    \"menuTitle\": \"Workspaces\",\n    \"deleteWorkspaceHintText\": \"Are you sure you want to delete the workspace? This action cannot be undone, and any pages you have published will be unpublished.\",\n    \"createSuccess\": \"Workspace created successfully\",\n    \"createFailed\": \"Failed to create workspace\",\n    \"createLimitExceeded\": \"You've reached the maximum workspace limit allowed for your account. If you need additional workspaces to continue your work, please request on Github\",\n    \"deleteSuccess\": \"Workspace deleted successfully\",\n    \"deleteFailed\": \"Failed to delete workspace\",\n    \"openSuccess\": \"Opened workspace successfully\",\n    \"openFailed\": \"Failed to open workspace\",\n    \"renameSuccess\": \"Workspace renamed successfully\",\n    \"renameFailed\": \"Failed to rename workspace\",\n    \"updateIconSuccess\": \"Updated workspace icon successfully\",\n    \"updateIconFailed\": \"Updated workspace icon failed\",\n    \"cannotDeleteTheOnlyWorkspace\": \"Cannot delete the only workspace\",\n    \"fetchWorkspacesFailed\": \"Failed to fetch workspaces\",\n    \"leaveCurrentWorkspace\": \"Leave workspace\",\n    \"leaveCurrentWorkspacePrompt\": \"Are you sure you want to leave the current workspace?\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Share\",\n    \"workInProgress\": \"Coming soon\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"Copy to clipboard\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Copy link\",\n    \"publishToTheWeb\": \"Publish to Web\",\n    \"publishToTheWebHint\": \"Create a website with AppFlowy\",\n    \"publish\": \"Publish\",\n    \"unPublish\": \"Unpublish\",\n    \"visitSite\": \"Visit site\",\n    \"exportAsTab\": \"Export as\",\n    \"publishTab\": \"Publish\",\n    \"shareTab\": \"Share\",\n    \"publishOnAppFlowy\": \"Publish on AppFlowy\",\n    \"shareTabTitle\": \"Invite to collaborate\",\n    \"shareTabDescription\": \"For easy collaboration with anyone\",\n    \"copyLinkSuccess\": \"Copied link to clipboard\",\n    \"copyShareLink\": \"Copy share link\",\n    \"copyLinkFailed\": \"Failed to copy link to clipboard\",\n    \"copyLinkToBlockSuccess\": \"Copied block link to clipboard\",\n    \"copyLinkToBlockFailed\": \"Failed to copy block link to clipboard\",\n    \"manageAllSites\": \"Manage all sites\",\n    \"updatePathName\": \"Update path name\"\n  },\n  \"moreAction\": {\n    \"small\": \"small\",\n    \"medium\": \"medium\",\n    \"large\": \"large\",\n    \"fontSize\": \"Font size\",\n    \"import\": \"Import\",\n    \"moreOptions\": \"More options\",\n    \"wordCount\": \"Word count: {}\",\n    \"charCount\": \"Character count: {}\",\n    \"createdAt\": \"Created: {}\",\n    \"deleteView\": \"Delete\",\n    \"duplicateView\": \"Duplicate\",\n    \"wordCountLabel\": \"Word count: \",\n    \"charCountLabel\": \"Character count: \",\n    \"createdAtLabel\": \"Created: \",\n    \"syncedAtLabel\": \"Synced: \",\n    \"saveAsNewPage\": \"Add messages to page\",\n    \"saveAsNewPageDisabled\": \"No messages available\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Text & Markdown\",\n    \"documentFromV010\": \"Document from v0.1.0\",\n    \"databaseFromV010\": \"Database from v0.1.0\",\n    \"notionZip\": \"Notion Exported Zip File\",\n    \"csv\": \"CSV\",\n    \"database\": \"Database\"\n  },\n  \"emojiIconPicker\": {\n    \"iconUploader\": {\n      \"placeholderLeft\": \"Drag & drop a file, click to \",\n      \"placeholderUpload\": \"Upload\",\n      \"placeholderRight\": \", or paste an image link.\",\n      \"dropToUpload\": \"Drop a file to upload\",\n      \"change\": \"Change\"\n    }\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Rename\",\n    \"delete\": \"Delete\",\n    \"duplicate\": \"Duplicate\",\n    \"unfavorite\": \"Remove from Favorites\",\n    \"favorite\": \"Add to Favorites\",\n    \"openNewTab\": \"Open in a new tab\",\n    \"moveTo\": \"Move to\",\n    \"addToFavorites\": \"Add to Favorites\",\n    \"copyLink\": \"Copy link\",\n    \"changeIcon\": \"Change icon\",\n    \"collapseAllPages\": \"Collapse all subpages\",\n    \"movePageTo\": \"Move page to\",\n    \"move\": \"Move\",\n    \"lockPage\": \"Lock page\"\n  },\n  \"blankPageTitle\": \"Blank page\",\n  \"newPageText\": \"New page\",\n  \"newDocumentText\": \"New document\",\n  \"newGridText\": \"New grid\",\n  \"newCalendarText\": \"New calendar\",\n  \"newBoardText\": \"New board\",\n  \"chat\": {\n    \"newChat\": \"AI Chat\",\n    \"inputMessageHint\": \"Ask @:appName AI\",\n    \"inputLocalAIMessageHint\": \"Ask @:appName Local AI\",\n    \"unsupportedCloudPrompt\": \"This feature is only available when using @:appName Cloud\",\n    \"relatedQuestion\": \"Suggested\",\n    \"serverUnavailable\": \"Connection lost. Please check your internet and\",\n    \"aiServerUnavailable\": \"The AI service is temporarily unavailable. Please try again later.\",\n    \"retry\": \"Retry\",\n    \"clickToRetry\": \"Click to retry\",\n    \"regenerateAnswer\": \"Regenerate\",\n    \"question1\": \"How to use Kanban to manage tasks\",\n    \"question2\": \"Explain the GTD method\",\n    \"question3\": \"Why use Rust\",\n    \"question4\": \"Recipe with what's in my kitchen\",\n    \"question5\": \"Create an illustration for my page\",\n    \"question6\": \"Draw up a to-do list for my upcoming week\",\n    \"aiMistakePrompt\": \"AI can make mistakes. Check important info.\",\n    \"chatWithFilePrompt\": \"Do you want to chat with the file?\",\n    \"indexFileSuccess\": \"Indexing file successfully\",\n    \"inputActionNoPages\": \"No page results\",\n    \"referenceSource\": {\n      \"zero\": \"0 sources found\",\n      \"one\": \"{count} source found\",\n      \"other\": \"{count} sources found\"\n    },\n    \"clickToMention\": \"Mention a page\",\n    \"uploadFile\": \"Attach PDFs, text or markdown files\",\n    \"questionDetail\": \"Hi {}! How can I help you today?\",\n    \"indexingFile\": \"Indexing {}\",\n    \"generatingResponse\": \"Generating response\",\n    \"selectSources\": \"Select Sources\",\n    \"currentPage\": \"Current page\",\n    \"sourcesLimitReached\": \"You can only select up to 3 top-level documents and its children\",\n    \"sourceUnsupported\": \"We don't support chatting with databases at this time\",\n    \"regenerate\": \"Try again\",\n    \"addToPageButton\": \"Add message to page\",\n    \"addToPageTitle\": \"Add message to...\",\n    \"addToNewPage\": \"Create new page\",\n    \"addToNewPageName\": \"Messages extracted from \\\"{}\\\"\",\n    \"addToNewPageSuccessToast\": \"Message added to\",\n    \"openPagePreviewFailedToast\": \"Failed to open page\",\n    \"changeFormat\": {\n      \"actionButton\": \"Change format\",\n      \"confirmButton\": \"Regenerate with this format\",\n      \"textOnly\": \"Text\",\n      \"imageOnly\": \"Image only\",\n      \"textAndImage\": \"Text and Image\",\n      \"text\": \"Paragraph\",\n      \"bullet\": \"Bullet list\",\n      \"number\": \"Numbered list\",\n      \"table\": \"Table\",\n      \"blankDescription\": \"Format response\",\n      \"defaultDescription\": \"Auto response format\",\n      \"textWithImageDescription\": \"@:chat.changeFormat.text with image\",\n      \"numberWithImageDescription\": \"@:chat.changeFormat.number with image\",\n      \"bulletWithImageDescription\": \"@:chat.changeFormat.bullet with image\",\n      \"tableWithImageDescription\": \"@:chat.changeFormat.table with image\"\n    },\n    \"switchModel\": {\n      \"label\": \"Switch model\",\n      \"localModel\": \"Local Model\",\n      \"cloudModel\": \"Cloud Model\",\n      \"autoModel\": \"Auto\"\n    },\n    \"selectBanner\": {\n      \"saveButton\": \"Add to …\",\n      \"selectMessages\": \"Select messages\",\n      \"nSelected\": \"{} selected\",\n      \"allSelected\": \"All selected\"\n    },\n    \"stopTooltip\": \"Stop generating\"\n  },\n  \"trash\": {\n    \"text\": \"Trash\",\n    \"restoreAll\": \"Restore All\",\n    \"restore\": \"Restore\",\n    \"deleteAll\": \"Delete All\",\n    \"pageHeader\": {\n      \"fileName\": \"File name\",\n      \"lastModified\": \"Last Modified\",\n      \"created\": \"Created\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"All pages in trash\",\n      \"caption\": \"Are you sure you want to delete everything in Trash? This action cannot be undone.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Restore all pages in trash\",\n      \"caption\": \"This action cannot be undone.\"\n    },\n    \"restorePage\": {\n      \"title\": \"Restore: {}\",\n      \"caption\": \"Are you sure you want to restore this page?\"\n    },\n    \"mobile\": {\n      \"actions\": \"Trash Actions\",\n      \"empty\": \"No pages or spaces in Trash\",\n      \"emptyDescription\": \"Move things you don't need to the Trash.\",\n      \"isDeleted\": \"is deleted\",\n      \"isRestored\": \"is restored\"\n    },\n    \"confirmDeleteTitle\": \"Are you sure you want to delete this page permanently?\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"This page is in Trash\",\n    \"restore\": \"Restore page\",\n    \"deletePermanent\": \"Delete permanently\",\n    \"deletePermanentDescription\": \"Are you sure you want to delete this page permanently? This is irreversible.\"\n  },\n  \"dialogCreatePageNameHint\": \"Page name\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Shortcuts\",\n    \"whatsNew\": \"What's new?\",\n    \"helpAndDocumentation\": \"Help & documentation\",\n    \"getSupport\": \"Get support\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"Debug Info\",\n      \"success\": \"Copied debug info to clipboard!\",\n      \"fail\": \"Unable to copy debug info to clipboard\"\n    },\n    \"feedback\": \"Feedback\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Remove, rename, and more...\",\n    \"addPageTooltip\": \"Quickly add a page inside\",\n    \"defaultNewPageName\": \"Untitled\",\n    \"renameDialog\": \"Rename\",\n    \"pageNameSuffix\": \"Copy\"\n  },\n  \"noPagesInside\": \"No pages inside\",\n  \"toolbar\": {\n    \"undo\": \"Undo\",\n    \"redo\": \"Redo\",\n    \"bold\": \"Bold\",\n    \"italic\": \"Italic\",\n    \"underline\": \"Underline\",\n    \"strike\": \"Strikethrough\",\n    \"numList\": \"Numbered list\",\n    \"bulletList\": \"Bulleted list\",\n    \"checkList\": \"Check List\",\n    \"inlineCode\": \"Inline Code\",\n    \"quote\": \"Quote Block\",\n    \"header\": \"Header\",\n    \"highlight\": \"Highlight\",\n    \"color\": \"Color\",\n    \"addLink\": \"Add Link\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Switch to Light mode\",\n    \"darkMode\": \"Switch to Dark mode\",\n    \"openAsPage\": \"Open as a Page\",\n    \"addNewRow\": \"Add a new row\",\n    \"openMenu\": \"Click to open menu\",\n    \"dragRow\": \"Drag to reorder the row\",\n    \"viewDataBase\": \"View database\",\n    \"referencePage\": \"This {name} is referenced\",\n    \"addBlockBelow\": \"Add a block below\",\n    \"aiGenerate\": \"Generate\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Close sidebar\",\n    \"openSidebar\": \"Open sidebar\",\n    \"expandSidebar\": \"Expand as full page\",\n    \"personal\": \"Personal\",\n    \"private\": \"Private\",\n    \"workspace\": \"Workspace\",\n    \"favorites\": \"Favorites\",\n    \"clickToHidePrivate\": \"Click to hide private space\\nPages you created here are only visible to you\",\n    \"clickToHideWorkspace\": \"Click to hide workspace\\nPages you created here are visible to every member\",\n    \"clickToHidePersonal\": \"Click to hide personal space\",\n    \"clickToHideFavorites\": \"Click to hide favorite space\",\n    \"addAPage\": \"Add a new page\",\n    \"addAPageToPrivate\": \"Add a page to private space\",\n    \"addAPageToWorkspace\": \"Add a page to workspace\",\n    \"recent\": \"Recent\",\n    \"today\": \"Today\",\n    \"thisWeek\": \"This week\",\n    \"others\": \"Earlier favorites\",\n    \"earlier\": \"Earlier\",\n    \"justNow\": \"just now\",\n    \"minutesAgo\": \"{count} minutes ago\",\n    \"lastViewed\": \"Last viewed\",\n    \"favoriteAt\": \"Favorited\",\n    \"emptyRecent\": \"No Recent Pages\",\n    \"emptyRecentDescription\": \"As you view pages, they will appear here for easy retrieval.\",\n    \"emptyFavorite\": \"No Favorite Pages\",\n    \"emptyFavoriteDescription\": \"Mark pages as favorites—they'll be listed here for quick access!\",\n    \"removePageFromRecent\": \"Remove this page from the Recent?\",\n    \"removeSuccess\": \"Removed successfully\",\n    \"favoriteSpace\": \"Favorites\",\n    \"RecentSpace\": \"Recent\",\n    \"Spaces\": \"Spaces\",\n    \"upgradeToPro\": \"Upgrade to Pro\",\n    \"upgradeToAIMax\": \"Unlock unlimited AI\",\n    \"storageLimitDialogTitle\": \"You have run out of free storage. Upgrade to unlock unlimited storage\",\n    \"storageLimitDialogTitleIOS\": \"You have run out of free storage.\",\n    \"aiResponseLimitTitle\": \"You have run out of free AI responses. Upgrade to the Pro Plan or purchase an AI add-on to unlock unlimited responses\",\n    \"aiResponseLimitDialogTitle\": \"AI Responses limit reached\",\n    \"aiResponseLimit\": \"You have run out of free AI responses.\\n\\nGo to Settings -> Plan -> Click AI Max or Pro Plan to get more AI responses\",\n    \"askOwnerToUpgradeToPro\": \"Your workspace is running out of free storage. Please ask your workspace owner to upgrade to the Pro Plan\",\n    \"askOwnerToUpgradeToProIOS\": \"Your workspace is running out of free storage.\",\n    \"askOwnerToUpgradeToAIMax\": \"Your workspace has ran out of free AI responses. Please ask your workspace owner to upgrade the plan or purchase AI add-ons\",\n    \"askOwnerToUpgradeToAIMaxIOS\": \"Your workspace is running out of free AI responses.\",\n    \"purchaseAIMax\": \"Your workspace has ran out of AI Image responses. Please ask your workspace owner to purchase AI Max\",\n    \"aiImageResponseLimit\": \"You have run out of AI image responses.\\n\\nGo to Settings -> Plan -> Click AI Max to get more AI image responses\",\n    \"purchaseStorageSpace\": \"Purchase Storage Space\",\n    \"singleFileProPlanLimitationDescription\": \"You have exceeded the maximum file upload size allowed in the free plan. Please upgrade to the Pro Plan to upload larger files\",\n    \"purchaseAIResponse\": \"Purchase \",\n    \"askOwnerToUpgradeToLocalAI\": \"Ask workspace owner to enable AI On-device\",\n    \"upgradeToAILocal\": \"Run local models on your device for ultimate privacy\",\n    \"upgradeToAILocalDesc\": \"Chat with PDFs, improve your writing, and auto-fill tables using local AI\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Exported Note To Markdown\",\n      \"path\": \"Documents/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Contacts\",\n    \"whatsHappening\": \"What's happening this week?\",\n    \"addContact\": \"Add Contact\",\n    \"editContact\": \"Edit Contact\"\n  },\n  \"button\": {\n    \"ok\": \"Ok\",\n    \"confirm\": \"Confirm\",\n    \"done\": \"Done\",\n    \"cancel\": \"Cancel\",\n    \"signIn\": \"Sign In\",\n    \"signOut\": \"Sign Out\",\n    \"complete\": \"Complete\",\n    \"change\": \"Change\",\n    \"save\": \"Save\",\n    \"generate\": \"Generate\",\n    \"esc\": \"ESC\",\n    \"keep\": \"Keep\",\n    \"tryAgain\": \"Try again\",\n    \"discard\": \"Discard\",\n    \"replace\": \"Replace\",\n    \"insertBelow\": \"Insert below\",\n    \"insertAbove\": \"Insert above\",\n    \"upload\": \"Upload\",\n    \"edit\": \"Edit\",\n    \"delete\": \"Delete\",\n    \"copy\": \"Copy\",\n    \"duplicate\": \"Duplicate\",\n    \"putback\": \"Put Back\",\n    \"update\": \"Update\",\n    \"share\": \"Share\",\n    \"removeFromFavorites\": \"Remove from Favorites\",\n    \"removeFromRecent\": \"Remove from Recent\",\n    \"addToFavorites\": \"Add to Favorites\",\n    \"favoriteSuccessfully\": \"Favorited success\",\n    \"unfavoriteSuccessfully\": \"Unfavorited success\",\n    \"duplicateSuccessfully\": \"Duplicated successfully\",\n    \"rename\": \"Rename\",\n    \"helpCenter\": \"Help Center\",\n    \"add\": \"Add\",\n    \"yes\": \"Yes\",\n    \"no\": \"No\",\n    \"clear\": \"Clear\",\n    \"remove\": \"Remove\",\n    \"dontRemove\": \"Don't remove\",\n    \"copyLink\": \"Copy Link\",\n    \"align\": \"Align\",\n    \"login\": \"Login\",\n    \"logout\": \"Log out\",\n    \"deleteAccount\": \"Delete account\",\n    \"back\": \"Back\",\n    \"signInGoogle\": \"Continue with Google\",\n    \"signInGithub\": \"Continue with GitHub\",\n    \"signInDiscord\": \"Continue with Discord\",\n    \"more\": \"More\",\n    \"create\": \"Create\",\n    \"close\": \"Close\",\n    \"next\": \"Next\",\n    \"previous\": \"Previous\",\n    \"submit\": \"Submit\",\n    \"download\": \"Download\",\n    \"backToHome\": \"Back to Home\",\n    \"viewing\": \"Viewing\",\n    \"editing\": \"Editing\",\n    \"gotIt\": \"Got it\",\n    \"retry\": \"Retry\",\n    \"uploadFailed\": \"Upload failed.\",\n    \"copyLinkOriginal\": \"Copy link to original\"\n  },\n  \"label\": {\n    \"welcome\": \"Welcome!\",\n    \"firstName\": \"First Name\",\n    \"middleName\": \"Middle Name\",\n    \"lastName\": \"Last Name\",\n    \"stepX\": \"Step {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Unable to connect to your account.\",\n      \"failedMsg\": \"Please make sure you've completed the sign-in process in your browser.\"\n    },\n    \"google\": {\n      \"title\": \"GOOGLE SIGN-IN\",\n      \"instruction1\": \"In order to import your Google Contacts, you'll need to authorize this application using your web browser.\",\n      \"instruction2\": \"Copy this code to your clipboard by clicking the icon or selecting the text:\",\n      \"instruction3\": \"Navigate to the following link in your web browser, and enter the above code:\",\n      \"instruction4\": \"Press the button below when you've completed signup:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Settings\",\n    \"popupMenuItem\": {\n      \"settings\": \"Settings\",\n      \"members\": \"Members\",\n      \"trash\": \"Trash\",\n      \"helpAndDocumentation\": \"Help & documentation\",\n      \"getSupport\": \"Get Support\"\n    },\n    \"sites\": {\n      \"title\": \"Sites\",\n      \"namespaceTitle\": \"Namespace\",\n      \"namespaceDescription\": \"Manage your namespace and homepage\",\n      \"namespaceHeader\": \"Namespace\",\n      \"homepageHeader\": \"Homepage\",\n      \"updateNamespace\": \"Update namespace\",\n      \"removeHomepage\": \"Remove homepage\",\n      \"selectHomePage\": \"Select a page\",\n      \"clearHomePage\": \"Clear the home page for this namespace\",\n      \"customUrl\": \"Custom URL\",\n      \"homePage\": {\n        \"upgradeToPro\": \"Upgrade to Pro Plan to set a homepage\"\n      },\n      \"namespace\": {\n        \"description\": \"This change will apply to all the published pages live on this namespace\",\n        \"tooltip\": \"We reserve the rights to remove any inappropriate namespaces\",\n        \"updateExistingNamespace\": \"Update existing namespace\",\n        \"upgradeToPro\": \"Upgrade to Pro Plan to claim a custom namespace\",\n        \"redirectToPayment\": \"Redirecting to payment page...\",\n        \"onlyWorkspaceOwnerCanSetHomePage\": \"Only the workspace owner can set a homepage\",\n        \"pleaseAskOwnerToSetHomePage\": \"Please ask the workspace owner to upgrade to Pro Plan\"\n      },\n      \"publishedPage\": {\n        \"title\": \"All published pages\",\n        \"description\": \"Manage your published pages\",\n        \"page\": \"Page\",\n        \"pathName\": \"Path name\",\n        \"date\": \"Published date\",\n        \"emptyHinText\": \"You have no published pages in this workspace\",\n        \"noPublishedPages\": \"No published pages\",\n        \"settings\": \"Publish settings\",\n        \"clickToOpenPageInApp\": \"Open page in app\",\n        \"clickToOpenPageInBrowser\": \"Open page in browser\"\n      },\n      \"error\": {\n        \"failedToGeneratePaymentLink\": \"Failed to generate payment link for Pro Plan\",\n        \"failedToUpdateNamespace\": \"Failed to update namespace\",\n        \"proPlanLimitation\": \"You need to upgrade to Pro Plan to update the namespace\",\n        \"namespaceAlreadyInUse\": \"The namespace is already taken, please try another one\",\n        \"invalidNamespace\": \"Invalid namespace, please try another one\",\n        \"namespaceLengthAtLeast2Characters\": \"The namespace must be at least 2 characters long\",\n        \"onlyWorkspaceOwnerCanUpdateNamespace\": \"Only workspace owner can update the namespace\",\n        \"onlyWorkspaceOwnerCanRemoveHomepage\": \"Only workspace owner can remove the homepage\",\n        \"setHomepageFailed\": \"Failed to set homepage\",\n        \"namespaceTooLong\": \"The namespace is too long, please try another one\",\n        \"namespaceTooShort\": \"The namespace is too short, please try another one\",\n        \"namespaceIsReserved\": \"The namespace is reserved, please try another one\",\n        \"updatePathNameFailed\": \"Failed to update path name\",\n        \"removeHomePageFailed\": \"Failed to remove homepage\",\n        \"publishNameContainsInvalidCharacters\": \"The path name contains invalid character(s), please try another one\",\n        \"publishNameTooShort\": \"The path name is too short, please try another one\",\n        \"publishNameTooLong\": \"The path name is too long, please try another one\",\n        \"publishNameAlreadyInUse\": \"The path name is already in use, please try another one\",\n        \"namespaceContainsInvalidCharacters\": \"The namespace contains invalid character(s), please try another one\",\n        \"publishPermissionDenied\": \"Only the workspace owner or page publisher can manage the publish settings\",\n        \"publishNameCannotBeEmpty\": \"The path name cannot be empty, please try another one\"\n      },\n      \"success\": {\n        \"namespaceUpdated\": \"Updated namespace successfully\",\n        \"setHomepageSuccess\": \"Set homepage successfully\",\n        \"updatePathNameSuccess\": \"Updated path name successfully\",\n        \"removeHomePageSuccess\": \"Remove homepage successfully\"\n      }\n    },\n    \"accountPage\": {\n      \"menuLabel\": \"Account & App\",\n      \"title\": \"My account\",\n      \"general\": {\n        \"title\": \"Account name & profile image\",\n        \"changeProfilePicture\": \"Change profile picture\"\n      },\n      \"email\": {\n        \"title\": \"Email\",\n        \"actions\": {\n          \"change\": \"Change email\"\n        }\n      },\n      \"login\": {\n        \"title\": \"Account login\",\n        \"loginLabel\": \"Log in\",\n        \"logoutLabel\": \"Log out\"\n      },\n      \"isUpToDate\": \"@:appName is up to date!\",\n      \"officialVersion\": \"Version {version} (官方構建)\"\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"Workspace\",\n      \"title\": \"Workspace\",\n      \"description\": \"Customize your workspace appearance, theme, font, text layout, date-/time-format, and language.\",\n      \"workspaceName\": {\n        \"title\": \"Workspace name\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"Workspace icon\",\n        \"description\": \"Upload an image or use an emoji for your workspace. Icon will show in your sidebar and notifications.\"\n      },\n      \"appearance\": {\n        \"title\": \"Appearance\",\n        \"description\": \"Customize your workspace appearance, theme, font, text layout, date, time, and language.\",\n        \"options\": {\n          \"system\": \"Auto\",\n          \"light\": \"Light\",\n          \"dark\": \"Dark\"\n        }\n      },\n      \"resetCursorColor\": {\n        \"title\": \"Reset document cursor color\",\n        \"description\": \"Are you sure you want to reset the cursor color?\"\n      },\n      \"resetSelectionColor\": {\n        \"title\": \"Reset document selection color\",\n        \"description\": \"Are you sure you want to reset the selection color?\"\n      },\n      \"resetWidth\": {\n        \"resetSuccess\": \"Reset document width successfully\"\n      },\n      \"theme\": {\n        \"title\": \"Theme\",\n        \"description\": \"Select a preset theme, or upload your own custom theme.\",\n        \"uploadCustomThemeTooltip\": \"Upload a custom theme\",\n        \"failedToLoadThemes\": \"Failed to load themes, please check your permission settings in System Settings > Privacy and Security > Files and Folders > @:appName\"\n      },\n      \"workspaceFont\": {\n        \"title\": \"Workspace font\",\n        \"noFontHint\": \"No font found, try another term.\"\n      },\n      \"textDirection\": {\n        \"title\": \"Text direction\",\n        \"leftToRight\": \"Left to right\",\n        \"rightToLeft\": \"Right to left\",\n        \"auto\": \"Auto\",\n        \"enableRTLItems\": \"Enable RTL toolbar items\"\n      },\n      \"layoutDirection\": {\n        \"title\": \"Layout direction\",\n        \"leftToRight\": \"Left to right\",\n        \"rightToLeft\": \"Right to left\"\n      },\n      \"dateTime\": {\n        \"title\": \"Date & time\",\n        \"example\": \"{} at {} ({})\",\n        \"24HourTime\": \"24-hour time\",\n        \"dateFormat\": {\n          \"label\": \"Date format\",\n          \"local\": \"Local\",\n          \"us\": \"US\",\n          \"iso\": \"ISO\",\n          \"friendly\": \"Friendly\",\n          \"dmy\": \"D/M/Y\"\n        }\n      },\n      \"language\": {\n        \"title\": \"Language\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"Delete workspace\",\n        \"content\": \"Are you sure you want to delete this workspace? This action cannot be undone, and any pages you have published will be unpublished.\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"Leave workspace\",\n        \"content\": \"Are you sure you want to leave this workspace? You will lose access to all pages and data within it.\",\n        \"success\": \"You have left the workspace successfully.\",\n        \"fail\": \"Failed to leave the workspace.\"\n      },\n      \"manageWorkspace\": {\n        \"title\": \"Manage workspace\",\n        \"leaveWorkspace\": \"Leave workspace\",\n        \"deleteWorkspace\": \"Delete workspace\"\n      }\n    },\n    \"manageDataPage\": {\n      \"menuLabel\": \"Manage data\",\n      \"title\": \"Manage data\",\n      \"description\": \"Manage data local storage or Import your existing data into @:appName.\",\n      \"dataStorage\": {\n        \"title\": \"File storage location\",\n        \"tooltip\": \"The location where your files are stored\",\n        \"actions\": {\n          \"change\": \"Change path\",\n          \"open\": \"Open folder\",\n          \"openTooltip\": \"Open current data folder location\",\n          \"copy\": \"Copy path\",\n          \"copiedHint\": \"Path copied!\",\n          \"resetTooltip\": \"Reset to default location\"\n        },\n        \"resetDialog\": {\n          \"title\": \"Are you sure?\",\n          \"description\": \"Resetting the path to the default data location will not delete your data. If you want to re-import your current data, you should copy the path of your current location first.\"\n        }\n      },\n      \"importData\": {\n        \"title\": \"Import data\",\n        \"tooltip\": \"Import data from @:appName backups/data folders\",\n        \"description\": \"Copy data from an external @:appName data folder\",\n        \"action\": \"Browse file\"\n      },\n      \"encryption\": {\n        \"title\": \"Encryption\",\n        \"tooltip\": \"Manage how your data is stored and encrypted\",\n        \"descriptionNoEncryption\": \"Turning on encryption will encrypt all data. This can not be undone.\",\n        \"descriptionEncrypted\": \"Your data is encrypted.\",\n        \"action\": \"Encrypt data\",\n        \"dialog\": {\n          \"title\": \"Encrypt all your data?\",\n          \"description\": \"Encrypting all your data will keep your data safe and secure. This action can NOT be undone. Are you sure you want to continue?\"\n        }\n      },\n      \"cache\": {\n        \"title\": \"Clear cache\",\n        \"description\": \"Help resolve issues like image not loading, missing pages in a space, and fonts not loading. This will not affect your data.\",\n        \"dialog\": {\n          \"title\": \"Clear cache\",\n          \"description\": \"Help resolve issues like image not loading, missing pages in a space, and fonts not loading. This will not affect your data.\",\n          \"successHint\": \"Cache cleared!\"\n        }\n      },\n      \"data\": {\n        \"fixYourData\": \"Fix your data\",\n        \"fixButton\": \"Fix\",\n        \"fixYourDataDescription\": \"If you're experiencing issues with your data, you can try to fix it here.\"\n      }\n    },\n    \"shortcutsPage\": {\n      \"menuLabel\": \"Shortcuts\",\n      \"title\": \"Shortcuts\",\n      \"editBindingHint\": \"Input new binding\",\n      \"searchHint\": \"Search\",\n      \"actions\": {\n        \"resetDefault\": \"Reset default\"\n      },\n      \"errorPage\": {\n        \"message\": \"Failed to load shortcuts: {}\",\n        \"howToFix\": \"Please try again, if the issue persists please reach out on GitHub.\"\n      },\n      \"resetDialog\": {\n        \"title\": \"Reset shortcuts\",\n        \"description\": \"This will reset all of your keybindings to the default, you cannot undo this later, are you sure you want to proceed?\",\n        \"buttonLabel\": \"Reset\"\n      },\n      \"conflictDialog\": {\n        \"title\": \"{} is currently in use\",\n        \"descriptionPrefix\": \"This keybinding is currently being used by \",\n        \"descriptionSuffix\": \". If you replace this keybinding, it will be removed from {}.\",\n        \"confirmLabel\": \"Continue\"\n      },\n      \"editTooltip\": \"Press to start editing the keybinding\",\n      \"keybindings\": {\n        \"toggleToDoList\": \"Toggle to do list\",\n        \"insertNewParagraphInCodeblock\": \"Insert new paragraph\",\n        \"pasteInCodeblock\": \"Paste in codeblock\",\n        \"selectAllCodeblock\": \"Select all\",\n        \"indentLineCodeblock\": \"Insert two spaces at line start\",\n        \"outdentLineCodeblock\": \"Delete two spaces at line start\",\n        \"twoSpacesCursorCodeblock\": \"Insert two spaces at cursor\",\n        \"copy\": \"Copy selection\",\n        \"paste\": \"Paste in content\",\n        \"cut\": \"Cut selection\",\n        \"alignLeft\": \"Align text left\",\n        \"alignCenter\": \"Align text center\",\n        \"alignRight\": \"Align text right\",\n        \"insertInlineMathEquation\": \"Insert inline math eqaution\",\n        \"undo\": \"Undo\",\n        \"redo\": \"Redo\",\n        \"convertToParagraph\": \"Convert block to paragraph\",\n        \"backspace\": \"Delete\",\n        \"deleteLeftWord\": \"Delete left word\",\n        \"deleteLeftSentence\": \"Delete left sentence\",\n        \"delete\": \"Delete right character\",\n        \"deleteMacOS\": \"Delete left character\",\n        \"deleteRightWord\": \"Delete right word\",\n        \"moveCursorLeft\": \"Move cursor left\",\n        \"moveCursorBeginning\": \"Move cursor to the beginning\",\n        \"moveCursorLeftWord\": \"Move cursor left one word\",\n        \"moveCursorLeftSelect\": \"Select and move cursor left\",\n        \"moveCursorBeginSelect\": \"Select and move cursor to the beginning\",\n        \"moveCursorLeftWordSelect\": \"Select and move cursor left one word\",\n        \"moveCursorRight\": \"Move cursor right\",\n        \"moveCursorEnd\": \"Move cursor to the end\",\n        \"moveCursorRightWord\": \"Move cursor right one word\",\n        \"moveCursorRightSelect\": \"Select and move cursor right one\",\n        \"moveCursorEndSelect\": \"Select and move cursor to the end\",\n        \"moveCursorRightWordSelect\": \"Select and move cursor to the right one word\",\n        \"moveCursorUp\": \"Move cursor up\",\n        \"moveCursorTopSelect\": \"Select and move cursor to the top\",\n        \"moveCursorTop\": \"Move cursor to the top\",\n        \"moveCursorUpSelect\": \"Select and move cursor up\",\n        \"moveCursorBottomSelect\": \"Select and move cursor to the bottom\",\n        \"moveCursorBottom\": \"Move cursor to the bottom\",\n        \"moveCursorDown\": \"Move cursor down\",\n        \"moveCursorDownSelect\": \"Select and move cursor down\",\n        \"home\": \"Scroll to the top\",\n        \"end\": \"Scroll to the bottom\",\n        \"toggleBold\": \"Toggle bold\",\n        \"toggleItalic\": \"Toggle italic\",\n        \"toggleUnderline\": \"Toggle underline\",\n        \"toggleStrikethrough\": \"Toggle strikethrough\",\n        \"toggleCode\": \"Toggle in-line code\",\n        \"toggleHighlight\": \"Toggle highlight\",\n        \"showLinkMenu\": \"Show link menu\",\n        \"openInlineLink\": \"Open in-line link\",\n        \"openLinks\": \"Open all selected links\",\n        \"indent\": \"Indent\",\n        \"outdent\": \"Outdent\",\n        \"exit\": \"Exit editing\",\n        \"pageUp\": \"Scroll one page up\",\n        \"pageDown\": \"Scroll one page down\",\n        \"selectAll\": \"Select all\",\n        \"pasteWithoutFormatting\": \"Paste content without formatting\",\n        \"showEmojiPicker\": \"Show emoji picker\",\n        \"enterInTableCell\": \"Add linebreak in table\",\n        \"leftInTableCell\": \"Move left one cell in table\",\n        \"rightInTableCell\": \"Move right one cell in table\",\n        \"upInTableCell\": \"Move up one cell in table\",\n        \"downInTableCell\": \"Move down one cell in table\",\n        \"tabInTableCell\": \"Go to next available cell in table\",\n        \"shiftTabInTableCell\": \"Go to previously available cell in table\",\n        \"backSpaceInTableCell\": \"Stop at the beginning of the cell\"\n      },\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"Insert a new paragraph next to the code block\",\n        \"codeBlockIndentLines\": \"Insert two spaces at the line start in code block\",\n        \"codeBlockOutdentLines\": \"Delete two spaces at the line start in code block\",\n        \"codeBlockAddTwoSpaces\": \"Insert two spaces at the cursor position in code block\",\n        \"codeBlockSelectAll\": \"Select all content inside a code block\",\n        \"codeBlockPasteText\": \"Paste text in codeblock\",\n        \"textAlignLeft\": \"Align text to the left\",\n        \"textAlignCenter\": \"Align text to the center\",\n        \"textAlignRight\": \"Align text to the right\"\n      },\n      \"couldNotLoadErrorMsg\": \"Could not load shortcuts, Try again\",\n      \"couldNotSaveErrorMsg\": \"Could not save shortcuts, Try again\"\n    },\n    \"aiPage\": {\n      \"title\": \"AI Settings\",\n      \"menuLabel\": \"AI Settings\",\n      \"keys\": {\n        \"enableAISearchTitle\": \"AI Search\",\n        \"aiSettingsDescription\": \"Choose your preferred model to power AppFlowy AI. Now includes GPT-4o, GPT-o3-mini, DeepSeek R1, Claude 3.5 Sonnet, and models available in Ollama\",\n        \"loginToEnableAIFeature\": \"AI features are only enabled after logging in with @:appName Cloud. If you don't have an @:appName account, go to 'My Account' to sign up\",\n        \"llmModel\": \"Language Model\",\n        \"globalLLMModel\": \"Global Language Model\",\n        \"readOnlyField\": \"This field is read-only\",\n        \"llmModelType\": \"Language Model Type\",\n        \"downloadLLMPrompt\": \"Download {}\",\n        \"downloadAppFlowyOfflineAI\": \"Downloading AI offline package will enable AI to run on your device. Do you want to continue?\",\n        \"downloadLLMPromptDetail\": \"Downloading {} local model will take up to {} of storage. Do you want to continue?\",\n        \"downloadBigFilePrompt\": \"It may take around 10 minutes to complete the download\",\n        \"downloadAIModelButton\": \"Download\",\n        \"downloadingModel\": \"Downloading\",\n        \"localAILoaded\": \"Local AI Model successfully added and ready to use\",\n        \"localAIStart\": \"Local AI is starting. If it's slow, try toggling it off and on\",\n        \"localAILoading\": \"Local AI Chat Model is loading...\",\n        \"localAIStopped\": \"Local AI stopped\",\n        \"localAIRunning\": \"Local AI is running\",\n        \"localAINotReadyRetryLater\": \"Local AI is initializing, please retry later\",\n        \"localAIDisabled\": \"You are using local AI, but it is disabled. Please go to settings to enable it or try different model\",\n        \"localAIInitializing\": \"Local AI is loading. This may take a few seconds depending on your device\",\n        \"localAINotReadyTextFieldPrompt\": \"You can not edit while Local AI is loading\",\n        \"localAIDisabledTextFieldPrompt\": \"You can not edit while Local AI is disabled\",\n        \"failToLoadLocalAI\": \"Failed to start local AI.\",\n        \"restartLocalAI\": \"Restart\",\n        \"disableLocalAITitle\": \"Disable local AI\",\n        \"disableLocalAIDescription\": \"Do you want to disable local AI?\",\n        \"localAIToggleTitle\": \"AppFlowy Local AI (LAI)\",\n        \"localAIToggleSubTitle\": \"Run the most advanced local AI models within AppFlowy for ultimate privacy and security\",\n        \"offlineAIInstruction1\": \"Follow the\",\n        \"offlineAIInstruction2\": \"instruction\",\n        \"offlineAIInstruction3\": \"to enable offline AI.\",\n        \"offlineAIDownload1\": \"If you have not downloaded the AppFlowy AI, please\",\n        \"offlineAIDownload2\": \"download\",\n        \"offlineAIDownload3\": \"it first\",\n        \"activeOfflineAI\": \"Active\",\n        \"downloadOfflineAI\": \"Download\",\n        \"openModelDirectory\": \"Open folder\",\n        \"laiNotReady\": \"The Local AI app was not installed correctly.\",\n        \"ollamaNotReady\": \"The Ollama server is not ready.\",\n        \"pleaseFollowThese\": \"Please follow these\",\n        \"instructions\": \"instructions\",\n        \"installOllamaLai\": \"to set up Ollama and AppFlowy Local AI.\",\n        \"modelsMissing\": \"Cannot find the required models: \",\n        \"downloadModel\": \"to download them.\"\n      }\n    },\n    \"planPage\": {\n      \"menuLabel\": \"Plan\",\n      \"title\": \"Pricing plan\",\n      \"planUsage\": {\n        \"title\": \"Plan usage summary\",\n        \"storageLabel\": \"Storage\",\n        \"storageUsage\": \"{} of {} GB\",\n        \"unlimitedStorageLabel\": \"Unlimited storage\",\n        \"collaboratorsLabel\": \"Members\",\n        \"collaboratorsUsage\": \"{} of {}\",\n        \"aiResponseLabel\": \"AI Responses\",\n        \"aiResponseUsage\": \"{} of {}\",\n        \"unlimitedAILabel\": \"Unlimited responses\",\n        \"proBadge\": \"Pro\",\n        \"aiMaxBadge\": \"AI Max\",\n        \"aiOnDeviceBadge\": \"AI On-device for Mac\",\n        \"memberProToggle\": \"More members & unlimited AI & guest access\",\n        \"aiMaxToggle\": \"Unlimited AI and access to advanced models\",\n        \"aiOnDeviceToggle\": \"Local AI for ultimate privacy\",\n        \"aiCredit\": {\n          \"title\": \"Add @:appName AI Credit\",\n          \"price\": \"{}\",\n          \"priceDescription\": \"for 1,000 credits\",\n          \"purchase\": \"Purchase AI\",\n          \"info\": \"Add 1,000 Ai credits per workspace and seamlessly integrate customizable AI into your workflow for smarter, faster results with up to:\",\n          \"infoItemOne\": \"10,000 responses per database\",\n          \"infoItemTwo\": \"1,000 responses per workspace\"\n        },\n        \"currentPlan\": {\n          \"bannerLabel\": \"Current plan\",\n          \"freeTitle\": \"Free\",\n          \"proTitle\": \"Pro\",\n          \"teamTitle\": \"Team\",\n          \"freeInfo\": \"Perfect for individuals up to 2 members to organize everything\",\n          \"proInfo\": \"Perfect for small and medium teams up to 10 members.\",\n          \"teamInfo\": \"Perfect for all productive and well-organized teams..\",\n          \"upgrade\": \"Change plan\",\n          \"canceledInfo\": \"Your plan is cancelled, you will be downgraded to the Free plan on {}.\"\n        },\n        \"addons\": {\n          \"title\": \"Add-ons\",\n          \"addLabel\": \"Add\",\n          \"activeLabel\": \"Added\",\n          \"aiMax\": {\n            \"title\": \"AI Max\",\n            \"description\": \"Unlimited AI responses powered by advanced AI models, and 50 AI images per month\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"Per user per month billed annually\"\n          },\n          \"aiOnDevice\": {\n            \"title\": \"AI On-device for Mac\",\n            \"description\": \"Run Mistral 7B, LLAMA 3, and more local models on your machine\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"Per user per month billed annually\",\n            \"recommend\": \"Recommend M1 or newer\"\n          }\n        },\n        \"deal\": {\n          \"bannerLabel\": \"New year deal!\",\n          \"title\": \"Grow your team!\",\n          \"info\": \"Upgrade and save 10% off Pro and Team plans! Boost your workspace productivity with powerful new features including @:appName AI.\",\n          \"viewPlans\": \"View plans\"\n        }\n      }\n    },\n    \"billingPage\": {\n      \"menuLabel\": \"Billing\",\n      \"title\": \"Billing\",\n      \"plan\": {\n        \"title\": \"Plan\",\n        \"freeLabel\": \"Free\",\n        \"proLabel\": \"Pro\",\n        \"planButtonLabel\": \"Change plan\",\n        \"billingPeriod\": \"Billing period\",\n        \"periodButtonLabel\": \"Edit period\"\n      },\n      \"paymentDetails\": {\n        \"title\": \"Payment details\",\n        \"methodLabel\": \"Payment method\",\n        \"methodButtonLabel\": \"Edit method\"\n      },\n      \"addons\": {\n        \"title\": \"Add-ons\",\n        \"addLabel\": \"Add\",\n        \"removeLabel\": \"Remove\",\n        \"renewLabel\": \"Renew\",\n        \"aiMax\": {\n          \"label\": \"AI Max\",\n          \"description\": \"Unlock unlimited AI and advanced models\",\n          \"activeDescription\": \"Next invoice due on {}\",\n          \"canceledDescription\": \"AI Max will be available until {}\"\n        },\n        \"aiOnDevice\": {\n          \"label\": \"AI On-device for Mac\",\n          \"description\": \"Unlock unlimited AI On-device on your device\",\n          \"activeDescription\": \"Next invoice due on {}\",\n          \"canceledDescription\": \"AI On-device for Mac will be available until {}\"\n        },\n        \"removeDialog\": {\n          \"title\": \"Remove {}\",\n          \"description\": \"Are you sure you want to remove {plan}? You will lose access to the features and benefits of {plan} immediately.\"\n        }\n      },\n      \"currentPeriodBadge\": \"CURRENT\",\n      \"changePeriod\": \"Change period\",\n      \"planPeriod\": \"{} period\",\n      \"monthlyInterval\": \"Monthly\",\n      \"monthlyPriceInfo\": \"per seat billed monthly\",\n      \"annualInterval\": \"Annually\",\n      \"annualPriceInfo\": \"per seat billed annually\"\n    },\n    \"comparePlanDialog\": {\n      \"title\": \"Compare & select plan\",\n      \"planFeatures\": \"Plan\\nFeatures\",\n      \"current\": \"Current\",\n      \"actions\": {\n        \"upgrade\": \"Upgrade\",\n        \"downgrade\": \"Downgrade\",\n        \"current\": \"Current\"\n      },\n      \"freePlan\": {\n        \"title\": \"Free\",\n        \"description\": \"For individuals up to 2 members to organize everything\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"Free forever\"\n      },\n      \"proPlan\": {\n        \"title\": \"Pro\",\n        \"description\": \"For small teams to manage projects and team knowledge\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"Per user per month \\nbilled annually\\n\\n{} billed monthly\"\n      },\n      \"planLabels\": {\n        \"itemOne\": \"Workspaces\",\n        \"itemTwo\": \"Members\",\n        \"itemThree\": \"Storage\",\n        \"itemFour\": \"Real-time collaboration\",\n        \"itemFive\": \"Guest editors\",\n        \"itemSix\": \"AI Responses\",\n        \"itemSeven\": \"AI Images\",\n        \"itemFileUpload\": \"File uploads\",\n        \"customNamespace\": \"Custom namespace\",\n        \"tooltipFive\": \"Collaborate on specific pages with non-members\",\n        \"tooltipSix\": \"Lifetime means the number of responses never reset\",\n        \"intelligentSearch\": \"Intelligent search\",\n        \"tooltipSeven\": \"Allows you to customize part of the URL for your workspace\",\n        \"customNamespaceTooltip\": \"Custom published site URL\"\n      },\n      \"freeLabels\": {\n        \"itemOne\": \"Charged per workspace\",\n        \"itemTwo\": \"Up to 2\",\n        \"itemThree\": \"5 GB\",\n        \"itemFour\": \"yes\",\n        \"itemFive\": \"yes\",\n        \"itemSix\": \"10 lifetime\",\n        \"itemSeven\": \"2 lifetime\",\n        \"itemFileUpload\": \"Up to 7 MB\",\n        \"intelligentSearch\": \"Intelligent search\"\n      },\n      \"proLabels\": {\n        \"itemOne\": \"Charged per workspace\",\n        \"itemTwo\": \"Up to 10\",\n        \"itemThree\": \"Unlimited\",\n        \"itemFour\": \"yes\",\n        \"itemFive\": \"Up to 100\",\n        \"itemSix\": \"Unlimited\",\n        \"itemSeven\": \"50 images per month\",\n        \"itemFileUpload\": \"Unlimited\",\n        \"intelligentSearch\": \"Intelligent search\"\n      },\n      \"paymentSuccess\": {\n        \"title\": \"You are now on the {} plan!\",\n        \"description\": \"Your payment has been successfully processed and your plan is upgraded to @:appName {}. You can view your plan details on the Plan page\"\n      },\n      \"downgradeDialog\": {\n        \"title\": \"Are you sure you want to downgrade your plan?\",\n        \"description\": \"Downgrading your plan will revert you back to the Free plan. Members may lose access to this workspace and you may need to free up space to meet the storage limits of the Free plan.\",\n        \"downgradeLabel\": \"Downgrade plan\"\n      }\n    },\n    \"cancelSurveyDialog\": {\n      \"title\": \"Sorry to see you go\",\n      \"description\": \"We're sorry to see you go. We'd love to hear your feedback to help us improve @:appName. Please take a moment to answer a few questions.\",\n      \"commonOther\": \"Other\",\n      \"otherHint\": \"Write your answer here\",\n      \"questionOne\": {\n        \"question\": \"What prompted you to cancel your @:appName Pro subscription?\",\n        \"answerOne\": \"Cost too high\",\n        \"answerTwo\": \"Features did not meet expectations\",\n        \"answerThree\": \"Found a better alternative\",\n        \"answerFour\": \"Did not use it enough to justify the expense\",\n        \"answerFive\": \"Service issue or technical difficulties\"\n      },\n      \"questionTwo\": {\n        \"question\": \"How likely are you to consider re-subscribing to @:appName Pro in the future?\",\n        \"answerOne\": \"Very likely\",\n        \"answerTwo\": \"Somewhat likely\",\n        \"answerThree\": \"Not sure\",\n        \"answerFour\": \"Unlikely\",\n        \"answerFive\": \"Very unlikely\"\n      },\n      \"questionThree\": {\n        \"question\": \"Which Pro feature did you value the most during your subscription?\",\n        \"answerOne\": \"Multi-user collaboration\",\n        \"answerTwo\": \"Longer time version history\",\n        \"answerThree\": \"Unlimited AI responses\",\n        \"answerFour\": \"Access to local AI models\"\n      },\n      \"questionFour\": {\n        \"question\": \"How would you describe your overall experience with @:appName?\",\n        \"answerOne\": \"Great\",\n        \"answerTwo\": \"Good\",\n        \"answerThree\": \"Average\",\n        \"answerFour\": \"Below average\",\n        \"answerFive\": \"Unsatisfied\"\n      }\n    },\n    \"common\": {\n      \"uploadingFile\": \"File is uploading. Please do not quit the app\",\n      \"uploadNotionSuccess\": \"Your Notion zip file has been uploaded successfully. Once the import is complete, you will receive a confirmation email\",\n      \"reset\": \"Reset\"\n    },\n    \"menu\": {\n      \"appearance\": \"Appearance\",\n      \"language\": \"Language\",\n      \"user\": \"User\",\n      \"files\": \"Files\",\n      \"notifications\": \"Notifications\",\n      \"open\": \"Open Settings\",\n      \"logout\": \"Log out\",\n      \"logoutPrompt\": \"Are you sure you want to log out?\",\n      \"selfEncryptionLogoutPrompt\": \"Are you sure you want to log out? Please ensure you have copied the encryption secret\",\n      \"syncSetting\": \"Sync Setting\",\n      \"cloudSettings\": \"Cloud Settings\",\n      \"enableSync\": \"Enable sync\",\n      \"enableSyncLog\": \"Enable sync logging\",\n      \"enableSyncLogWarning\": \"Thank you for helping diagnose sync issues. This will log your document edits to a local file. Please quit and reopen the app after enabling\",\n      \"enableEncrypt\": \"Encrypt data\",\n      \"cloudURL\": \"Base URL\",\n      \"webURL\": \"Web URL\",\n      \"invalidCloudURLScheme\": \"Invalid Scheme\",\n      \"cloudServerType\": \"Cloud server\",\n      \"cloudServerTypeTip\": \"Please note that it might log out your current account after switching the cloud server\",\n      \"cloudLocal\": \"Local\",\n      \"cloudAppFlowy\": \"@:appName Cloud\",\n      \"cloudAppFlowySelfHost\": \"@:appName Cloud Self-hosted\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"The cloud url can't be empty\",\n      \"clickToCopy\": \"Copy to clipboard\",\n      \"selfHostStart\": \"If you don't have a server, please refer to the\",\n      \"selfHostContent\": \"document\",\n      \"selfHostEnd\": \"for guidance on how to self-host your own server\",\n      \"pleaseInputValidURL\": \"Please input a valid URL\",\n      \"changeUrl\": \"Change self-hosted url to {}\",\n      \"cloudURLHint\": \"Input the base URL of your server\",\n      \"webURLHint\": \"Input the base URL of your web server\",\n      \"cloudWSURL\": \"Websocket URL\",\n      \"cloudWSURLHint\": \"Input the websocket address of your server\",\n      \"restartApp\": \"Restart\",\n      \"restartAppTip\": \"Restart the application for the changes to take effect. Please note that this might log out your current account.\",\n      \"changeServerTip\": \"After changing the server, you must click the restart button for the changes to take effect\",\n      \"enableEncryptPrompt\": \"Activate encryption to secure your data with this secret. Store it safely; once enabled, it can't be turned off. If lost, your data becomes irretrievable. Click to copy\",\n      \"inputEncryptPrompt\": \"Please enter your encryption secret for\",\n      \"clickToCopySecret\": \"Click to copy secret\",\n      \"configServerSetting\": \"Configurate your server settings\",\n      \"configServerGuide\": \"After selecting `Quick Start`, navigate to `Settings` and then \\\"Cloud Settings\\\" to configure your self-hosted server.\",\n      \"inputTextFieldHint\": \"Your secret\",\n      \"historicalUserList\": \"User login history\",\n      \"historicalUserListTooltip\": \"This list displays your anonymous accounts. You can click on an account to view its details. Anonymous accounts are created by clicking the 'Get Started' button\",\n      \"openHistoricalUser\": \"Click to open the anonymous account\",\n      \"customPathPrompt\": \"Storing the @:appName data folder in a cloud-synced folder such as Google Drive can pose risks. If the database within this folder is accessed or modified from multiple locations at the same time, it may result in synchronization conflicts and potential data corruption\",\n      \"importAppFlowyData\": \"Import Data from External @:appName Folder\",\n      \"importingAppFlowyDataTip\": \"Data import is in progress. Please do not close the app\",\n      \"importAppFlowyDataDescription\": \"Copy data from an external @:appName data folder and import it into the current AppFlowy data folder\",\n      \"importSuccess\": \"Successfully imported the @:appName data folder\",\n      \"importFailed\": \"Importing the @:appName data folder failed\",\n      \"importGuide\": \"For further details, please check the referenced document\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Enable notifications\",\n        \"hint\": \"Turn off to stop local notifications from appearing.\"\n      },\n      \"showNotificationsIcon\": {\n        \"label\": \"Show notifications icon\",\n        \"hint\": \"Toggle off to hide the notification icon in the sidebar.\"\n      },\n      \"archiveNotifications\": {\n        \"allSuccess\": \"Archived all notifications successfully\",\n        \"success\": \"Archived notification successfully\"\n      },\n      \"markAsReadNotifications\": {\n        \"allSuccess\": \"Marked all as read successfully\",\n        \"success\": \"Marked as read successfully\"\n      },\n      \"action\": {\n        \"markAsRead\": \"Mark as read\",\n        \"multipleChoice\": \"Select more\",\n        \"archive\": \"Archive\"\n      },\n      \"settings\": {\n        \"settings\": \"Settings\",\n        \"markAllAsRead\": \"Mark all as read\",\n        \"archiveAll\": \"Archive all\"\n      },\n      \"emptyInbox\": {\n        \"title\": \"Inbox Zero!\",\n        \"description\": \"Set reminders to receive notifications here.\"\n      },\n      \"emptyUnread\": {\n        \"title\": \"No unread notifications\",\n        \"description\": \"You're all caught up!\"\n      },\n      \"emptyArchived\": {\n        \"title\": \"No archived\",\n        \"description\": \"Archived notifications will appear here.\"\n      },\n      \"tabs\": {\n        \"inbox\": \"Inbox\",\n        \"unread\": \"Unread\",\n        \"archived\": \"Archived\"\n      },\n      \"refreshSuccess\": \"Notifications refreshed successfully\",\n      \"titles\": {\n        \"notifications\": \"Notifications\",\n        \"reminder\": \"Reminder\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"Reset\",\n      \"fontFamily\": {\n        \"label\": \"Font Family\",\n        \"search\": \"Search\",\n        \"defaultFont\": \"System\"\n      },\n      \"themeMode\": {\n        \"label\": \"Theme Mode\",\n        \"light\": \"Light Mode\",\n        \"dark\": \"Dark Mode\",\n        \"system\": \"Adapt to System\"\n      },\n      \"fontScaleFactor\": \"Font Scale Factor\",\n      \"displaySize\": \"Display Size\",\n      \"documentSettings\": {\n        \"cursorColor\": \"Document cursor color\",\n        \"selectionColor\": \"Document selection color\",\n        \"width\": \"Document width\",\n        \"changeWidth\": \"Change\",\n        \"pickColor\": \"Select a color\",\n        \"colorShade\": \"Color shade\",\n        \"opacity\": \"Opacity\",\n        \"hexEmptyError\": \"Hex color cannot be empty\",\n        \"hexLengthError\": \"Hex value must be 6 digits long\",\n        \"hexInvalidError\": \"Invalid hex value\",\n        \"opacityEmptyError\": \"Opacity cannot be empty\",\n        \"opacityRangeError\": \"Opacity must be between 1 and 100\",\n        \"app\": \"App\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"Apply\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Layout Direction\",\n        \"hint\": \"Control the flow of content on your screen, from left to right or right to left.\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\"\n      },\n      \"textDirection\": {\n        \"label\": \"Default Text Direction\",\n        \"hint\": \"Specify whether text should start from left or right as the default.\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\",\n        \"auto\": \"AUTO\",\n        \"fallback\": \"Same as layout direction\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Upload\",\n        \"uploadTheme\": \"Upload theme\",\n        \"description\": \"Upload your own @:appName theme using the button below.\",\n        \"loading\": \"Please wait while we validate and upload your theme...\",\n        \"uploadSuccess\": \"Your theme was uploaded successfully\",\n        \"deletionFailure\": \"Failed to delete the theme. Try to delete it manually.\",\n        \"filePickerDialogTitle\": \"Choose a .flowy_plugin file\",\n        \"urlUploadFailure\": \"Failed to open url: {}\"\n      },\n      \"theme\": \"Theme\",\n      \"builtInsLabel\": \"Built-in Themes\",\n      \"pluginsLabel\": \"Plugins\",\n      \"dateFormat\": {\n        \"label\": \"Date format\",\n        \"local\": \"Local\",\n        \"us\": \"US\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"Friendly\",\n        \"dmy\": \"D/M/Y\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Time format\",\n        \"twelveHour\": \"Twelve hour\",\n        \"twentyFourHour\": \"Twenty four hour\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"Show naming dialog when creating a page\",\n      \"enableRTLToolbarItems\": \"Enable RTL toolbar items\",\n      \"members\": {\n        \"title\": \"Members\",\n        \"inviteMembers\": \"Invite members\",\n        \"inviteHint\": \"Invite by email\",\n        \"sendInvite\": \"Invite\",\n        \"copyInviteLink\": \"Copy invite link\",\n        \"label\": \"Members\",\n        \"user\": \"User\",\n        \"role\": \"Role\",\n        \"removeFromWorkspace\": \"Remove from Workspace\",\n        \"removeFromWorkspaceSuccess\": \"Remove from workspace successfully\",\n        \"removeFromWorkspaceFailed\": \"Remove from workspace failed\",\n        \"owner\": \"Owner\",\n        \"guest\": \"Guest\",\n        \"member\": \"Member\",\n        \"memberHintText\": \"A member can read and edit pages\",\n        \"guestHintText\": \"A Guest can read, react, comment, and can edit certain pages with permission.\",\n        \"emailInvalidError\": \"Invalid email, please check and try again\",\n        \"emailSent\": \"Email sent, please check the inbox\",\n        \"members\": \"members\",\n        \"membersCount\": {\n          \"zero\": \"{} members\",\n          \"one\": \"{} member\",\n          \"other\": \"{} members\"\n        },\n        \"inviteFailedDialogTitle\": \"Upgrade to Pro Plan\",\n        \"inviteFailedMemberLimit\": \"This workspace has reached the free limit. Please upgrade to Pro to unlock more members.\",\n        \"inviteFailedMemberLimitMobile\": \"Your workspace has reached the member limit.\",\n        \"memberLimitExceeded\": \"Member limit reached, to invite more members, please \",\n        \"memberLimitExceededUpgrade\": \"upgrade\",\n        \"memberLimitExceededPro\": \"Member limit reached, if you require more members contact \",\n        \"memberLimitExceededProContact\": \"support@appflowy.io\",\n        \"failedToAddMember\": \"Failed to add member\",\n        \"addMemberSuccess\": \"Member added successfully\",\n        \"removeMember\": \"Remove Member\",\n        \"areYouSureToRemoveMember\": \"Are you sure you want to remove this member?\",\n        \"inviteMemberSuccess\": \"The invitation has been sent successfully\",\n        \"failedToInviteMember\": \"Failed to invite member\",\n        \"workspaceMembersError\": \"Oops, something went wrong\",\n        \"workspaceMembersErrorDescription\": \"We couldn't load the member list at this time. Please try again later\",\n        \"inviteLinkToAddMember\": \"Invite link to add member\",\n        \"clickToCopyLink\": \"Click to copy link\",\n        \"or\": \"or\",\n        \"generateANewLink\": \"generate a new link\",\n        \"inviteMemberByEmail\": \"Invite member by email\",\n        \"inviteMemberHintText\": \"Invite by email\",\n        \"resetInviteLink\": \"Reset the invite link?\",\n        \"resetInviteLinkDescription\": \"Resetting will deactivate the current link for all space members and generate a new one. The old link will no longer valid.\",\n        \"adminPanel\": \"Admin Panel\",\n        \"reset\": \"Reset\",\n        \"resetInviteLinkSuccess\": \"Invite link reset successfully\",\n        \"resetInviteLinkFailed\": \"Failed to reset the invite link\",\n        \"resetInviteLinkFailedDescription\": \"Please try again later\",\n        \"memberPageDescription1\": \"Access the\",\n        \"memberPageDescription2\": \"for guest and advanced user management.\",\n        \"noInviteLink\": \"You haven't generated an invite link yet.\",\n        \"copyLink\": \"Copy link\",\n        \"generatedLinkSuccessfully\": \"Generated link successfully\",\n        \"generatedLinkFailed\": \"Failed to generate link\",\n        \"resetLinkSuccessfully\": \"Reset link successfully\",\n        \"resetLinkFailed\": \"Failed to reset link\"\n      }\n    },\n    \"files\": {\n      \"copy\": \"Copy\",\n      \"defaultLocation\": \"Read files and data storage location\",\n      \"exportData\": \"Export your data\",\n      \"doubleTapToCopy\": \"Double tap to copy the path\",\n      \"restoreLocation\": \"Restore to @:appName default path\",\n      \"customizeLocation\": \"Open another folder\",\n      \"restartApp\": \"Please restart app for the changes to take effect.\",\n      \"exportDatabase\": \"Export database\",\n      \"selectFiles\": \"Select the files that need to be export\",\n      \"selectAll\": \"Select all\",\n      \"deselectAll\": \"Deselect all\",\n      \"createNewFolder\": \"Create a new folder\",\n      \"createNewFolderDesc\": \"Tell us where you want to store your data\",\n      \"defineWhereYourDataIsStored\": \"Define where your data is stored\",\n      \"open\": \"Open\",\n      \"openFolder\": \"Open an existing folder\",\n      \"openFolderDesc\": \"Read and write it to your existing @:appName folder\",\n      \"folderHintText\": \"folder name\",\n      \"location\": \"Creating a new folder\",\n      \"locationDesc\": \"Pick a name for your @:appName data folder\",\n      \"browser\": \"Browse\",\n      \"create\": \"Create\",\n      \"set\": \"Set\",\n      \"folderPath\": \"Path to store your folder\",\n      \"locationCannotBeEmpty\": \"Path cannot be empty\",\n      \"pathCopiedSnackbar\": \"File storage path copied to clipboard!\",\n      \"changeLocationTooltips\": \"Change the data directory\",\n      \"change\": \"Change\",\n      \"openLocationTooltips\": \"Open another data directory\",\n      \"openCurrentDataFolder\": \"Open current data directory\",\n      \"recoverLocationTooltips\": \"Reset to @:appName's default data directory\",\n      \"exportFileSuccess\": \"Export file successfully!\",\n      \"exportFileFail\": \"Export file failed!\",\n      \"export\": \"Export\",\n      \"clearCache\": \"Clear cache\",\n      \"clearCacheDesc\": \"If you encounter issues with images not loading or fonts not displaying correctly, try clearing your cache. This action will not remove your user data.\",\n      \"areYouSureToClearCache\": \"Are you sure to clear the cache?\",\n      \"clearCacheSuccess\": \"Cache cleared successfully!\"\n    },\n    \"user\": {\n      \"name\": \"Name\",\n      \"email\": \"Email\",\n      \"tooltipSelectIcon\": \"Select icon\",\n      \"selectAnIcon\": \"Select an icon\",\n      \"pleaseInputYourOpenAIKey\": \"please input your AI key\",\n      \"clickToLogout\": \"Click to log out the current user\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"Personal Information\",\n      \"username\": \"User Name\",\n      \"usernameEmptyError\": \"User name cannot be empty\",\n      \"about\": \"About\",\n      \"pushNotifications\": \"Push Notifications\",\n      \"support\": \"Support\",\n      \"joinDiscord\": \"Join us in Discord\",\n      \"privacyPolicy\": \"Privacy Policy\",\n      \"userAgreement\": \"User Agreement\",\n      \"termsAndConditions\": \"Terms and Conditions\",\n      \"userprofileError\": \"Failed to load user profile\",\n      \"userprofileErrorDescription\": \"Please try to log out and log back in to check if the issue still persists.\",\n      \"selectLayout\": \"Select layout\",\n      \"selectStartingDay\": \"Select starting day\",\n      \"version\": \"Version\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Are you sure you want to delete this view?\",\n    \"createView\": \"New\",\n    \"title\": {\n      \"placeholder\": \"Untitled\"\n    },\n    \"settings\": {\n      \"filter\": \"Filter\",\n      \"sort\": \"Sort\",\n      \"sortBy\": \"Sort by\",\n      \"properties\": \"Properties\",\n      \"reorderPropertiesTooltip\": \"Drag to reorder properties\",\n      \"group\": \"Group\",\n      \"addFilter\": \"Add Filter\",\n      \"deleteFilter\": \"Delete filter\",\n      \"filterBy\": \"Filter by\",\n      \"typeAValue\": \"Type a value...\",\n      \"layout\": \"Layout\",\n      \"compactMode\": \"Compact mode\",\n      \"databaseLayout\": \"Layout\",\n      \"viewList\": {\n        \"zero\": \"0 views\",\n        \"one\": \"{count} view\",\n        \"other\": \"{count} views\"\n      },\n      \"editView\": \"Edit View\",\n      \"boardSettings\": \"Board settings\",\n      \"calendarSettings\": \"Calendar settings\",\n      \"createView\": \"New view\",\n      \"duplicateView\": \"Duplicate view\",\n      \"deleteView\": \"Delete view\",\n      \"numberOfVisibleFields\": \"{} shown\"\n    },\n    \"filter\": {\n      \"empty\": \"No active filters\",\n      \"addFilter\": \"Add filter\",\n      \"cannotFindCreatableField\": \"Cannot find a suitable field to filter by\",\n      \"conditon\": \"Condition\",\n      \"where\": \"Where\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Contains\",\n      \"doesNotContain\": \"Does not contain\",\n      \"endsWith\": \"Ends with\",\n      \"startWith\": \"Starts with\",\n      \"is\": \"Is\",\n      \"isNot\": \"Is not\",\n      \"isEmpty\": \"Is empty\",\n      \"isNotEmpty\": \"Is not empty\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Not\",\n        \"startWith\": \"Starts with\",\n        \"endWith\": \"Ends with\",\n        \"isEmpty\": \"is empty\",\n        \"isNotEmpty\": \"is not empty\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Checked\",\n      \"isUnchecked\": \"Unchecked\",\n      \"choicechipPrefix\": {\n        \"is\": \"is\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"Is complete\",\n      \"isIncomplted\": \"Is incomplete\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"Is\",\n      \"isNot\": \"Is not\",\n      \"contains\": \"Contains\",\n      \"doesNotContain\": \"Does not contain\",\n      \"isEmpty\": \"Is empty\",\n      \"isNotEmpty\": \"Is not empty\"\n    },\n    \"dateFilter\": {\n      \"is\": \"Is on\",\n      \"before\": \"Is before\",\n      \"after\": \"Is after\",\n      \"onOrBefore\": \"Is on or before\",\n      \"onOrAfter\": \"Is on or after\",\n      \"between\": \"Is between\",\n      \"empty\": \"Is empty\",\n      \"notEmpty\": \"Is not empty\",\n      \"startDate\": \"Start date\",\n      \"endDate\": \"End date\",\n      \"choicechipPrefix\": {\n        \"before\": \"Before\",\n        \"after\": \"After\",\n        \"between\": \"Between\",\n        \"onOrBefore\": \"On or before\",\n        \"onOrAfter\": \"On or after\",\n        \"isEmpty\": \"Is empty\",\n        \"isNotEmpty\": \"Is not empty\"\n      }\n    },\n    \"numberFilter\": {\n      \"equal\": \"Equals\",\n      \"notEqual\": \"Does not equal\",\n      \"lessThan\": \"Is less than\",\n      \"greaterThan\": \"Is greater than\",\n      \"lessThanOrEqualTo\": \"Is less than or equal to\",\n      \"greaterThanOrEqualTo\": \"Is greater than or equal to\",\n      \"isEmpty\": \"Is empty\",\n      \"isNotEmpty\": \"Is not empty\"\n    },\n    \"field\": {\n      \"label\": \"Property\",\n      \"hide\": \"Hide property\",\n      \"show\": \"Show property\",\n      \"insertLeft\": \"Insert left\",\n      \"insertRight\": \"Insert right\",\n      \"duplicate\": \"Duplicate\",\n      \"delete\": \"Delete\",\n      \"wrapCellContent\": \"Wrap text\",\n      \"clear\": \"Clear cells\",\n      \"switchPrimaryFieldTooltip\": \"Cannot change field type of primary field\",\n      \"textFieldName\": \"Text\",\n      \"checkboxFieldName\": \"Checkbox\",\n      \"dateFieldName\": \"Date\",\n      \"updatedAtFieldName\": \"Last modified\",\n      \"createdAtFieldName\": \"Created at\",\n      \"numberFieldName\": \"Numbers\",\n      \"singleSelectFieldName\": \"Select\",\n      \"multiSelectFieldName\": \"Multiselect\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Checklist\",\n      \"relationFieldName\": \"Relation\",\n      \"summaryFieldName\": \"AI Summary\",\n      \"timeFieldName\": \"Time\",\n      \"mediaFieldName\": \"Files & media\",\n      \"translateFieldName\": \"AI Translate\",\n      \"translateTo\": \"Translate to\",\n      \"numberFormat\": \"Number format\",\n      \"dateFormat\": \"Date format\",\n      \"includeTime\": \"Include time\",\n      \"isRange\": \"End date\",\n      \"dateFormatFriendly\": \"Month Day, Year\",\n      \"dateFormatISO\": \"Year-Month-Day\",\n      \"dateFormatLocal\": \"Month/Day/Year\",\n      \"dateFormatUS\": \"Year/Month/Day\",\n      \"dateFormatDayMonthYear\": \"Day/Month/Year\",\n      \"timeFormat\": \"Time format\",\n      \"invalidTimeFormat\": \"Invalid format\",\n      \"timeFormatTwelveHour\": \"12 hour\",\n      \"timeFormatTwentyFourHour\": \"24 hour\",\n      \"clearDate\": \"Clear date\",\n      \"dateTime\": \"Date time\",\n      \"startDateTime\": \"Start date time\",\n      \"endDateTime\": \"End date time\",\n      \"failedToLoadDate\": \"Failed to load date value\",\n      \"selectTime\": \"Select time\",\n      \"selectDate\": \"Select date\",\n      \"visibility\": \"Visibility\",\n      \"propertyType\": \"Property type\",\n      \"addSelectOption\": \"Add an option\",\n      \"typeANewOption\": \"Type a new option\",\n      \"optionTitle\": \"Options\",\n      \"addOption\": \"Add option\",\n      \"editProperty\": \"Edit property\",\n      \"newProperty\": \"New property\",\n      \"openRowDocument\": \"Open as a page\",\n      \"deleteFieldPromptMessage\": \"Are you sure? This property and all its data will be deleted\",\n      \"clearFieldPromptMessage\": \"Are you sure? All cells in this column will be emptied\",\n      \"newColumn\": \"New column\",\n      \"format\": \"Format\",\n      \"reminderOnDateTooltip\": \"This cell has a scheduled reminder\",\n      \"optionAlreadyExist\": \"Option already exists\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Add a new field\",\n      \"fieldDragElementTooltip\": \"Click to open menu\",\n      \"showHiddenFields\": {\n        \"one\": \"Show {count} hidden field\",\n        \"many\": \"Show {count} hidden fields\",\n        \"other\": \"Show {count} hidden fields\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"Hide {count} hidden field\",\n        \"many\": \"Hide {count} hidden fields\",\n        \"other\": \"Hide {count} hidden fields\"\n      },\n      \"openAsFullPage\": \"Open as full page\",\n      \"viewDatabase\": \"View the original Database\",\n      \"moreRowActions\": \"More row actions\"\n    },\n    \"sort\": {\n      \"ascending\": \"Ascending\",\n      \"descending\": \"Descending\",\n      \"by\": \"By\",\n      \"empty\": \"No active sorts\",\n      \"cannotFindCreatableField\": \"Cannot find a suitable field to sort by\",\n      \"deleteAllSorts\": \"Delete all sorts\",\n      \"addSort\": \"Add sort\",\n      \"sortsActive\": \"Cannot {intention} while sorting\",\n      \"removeSorting\": \"Would you like to remove all the sorts in this view and continue?\",\n      \"fieldInUse\": \"You are already sorting by this field\"\n    },\n    \"row\": {\n      \"label\": \"Row\",\n      \"duplicate\": \"Duplicate\",\n      \"delete\": \"Delete\",\n      \"titlePlaceholder\": \"Untitled\",\n      \"textPlaceholder\": \"Empty\",\n      \"copyProperty\": \"Copied property to clipboard\",\n      \"count\": \"Count\",\n      \"newRow\": \"New row\",\n      \"loadMore\": \"Load more\",\n      \"action\": \"Action\",\n      \"add\": \"Click add to below\",\n      \"drag\": \"Drag to move\",\n      \"deleteRowPrompt\": \"Are you sure you want to delete this row? This action cannot be undone.\",\n      \"deleteCardPrompt\": \"Are you sure you want to delete this card? This action cannot be undone.\",\n      \"dragAndClick\": \"Drag to move, click to open menu\",\n      \"insertRecordAbove\": \"Insert record above\",\n      \"insertRecordBelow\": \"Insert record below\",\n      \"noContent\": \"No content\",\n      \"reorderRowDescription\": \"reorder row\",\n      \"createRowAboveDescription\": \"create a row above\",\n      \"createRowBelowDescription\": \"insert a row below\"\n    },\n    \"selectOption\": {\n      \"create\": \"Create\",\n      \"purpleColor\": \"Purple\",\n      \"pinkColor\": \"Pink\",\n      \"lightPinkColor\": \"Light Pink\",\n      \"orangeColor\": \"Orange\",\n      \"yellowColor\": \"Yellow\",\n      \"limeColor\": \"Lime\",\n      \"greenColor\": \"Green\",\n      \"aquaColor\": \"Aqua\",\n      \"blueColor\": \"Blue\",\n      \"deleteTag\": \"Delete tag\",\n      \"colorPanelTitle\": \"Color\",\n      \"panelTitle\": \"Select an option or create one\",\n      \"searchOption\": \"Search for an option\",\n      \"searchOrCreateOption\": \"Search for an option or create one\",\n      \"createNew\": \"Create a new\",\n      \"orSelectOne\": \"Or select an option\",\n      \"typeANewOption\": \"Type a new option\",\n      \"tagName\": \"Tag name\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"Task description\",\n      \"addNew\": \"Add a new task\",\n      \"submitNewTask\": \"Create\",\n      \"hideComplete\": \"Hide completed tasks\",\n      \"showComplete\": \"Show all tasks\"\n    },\n    \"url\": {\n      \"launch\": \"Open link in browser\",\n      \"copy\": \"Copy link to clipboard\",\n      \"textFieldHint\": \"Enter a URL\"\n    },\n    \"relation\": {\n      \"relatedDatabasePlaceLabel\": \"Related Database\",\n      \"relatedDatabasePlaceholder\": \"None\",\n      \"inRelatedDatabase\": \"In\",\n      \"rowSearchTextFieldPlaceholder\": \"Search\",\n      \"noDatabaseSelected\": \"No database selected, please select one first from the list below:\",\n      \"emptySearchResult\": \"No records found\",\n      \"linkedRowListLabel\": \"{count} linked rows\",\n      \"unlinkedRowListLabel\": \"Link another row\"\n    },\n    \"menuName\": \"Grid\",\n    \"referencedGridPrefix\": \"View of\",\n    \"calculate\": \"Calculate\",\n    \"calculationTypeLabel\": {\n      \"none\": \"None\",\n      \"average\": \"Average\",\n      \"max\": \"Max\",\n      \"median\": \"Median\",\n      \"min\": \"Min\",\n      \"sum\": \"Sum\",\n      \"count\": \"Count\",\n      \"countEmpty\": \"Count empty\",\n      \"countEmptyShort\": \"EMPTY\",\n      \"countNonEmpty\": \"Count not empty\",\n      \"countNonEmptyShort\": \"FILLED\"\n    },\n    \"media\": {\n      \"rename\": \"Rename\",\n      \"download\": \"Download\",\n      \"expand\": \"Expand\",\n      \"delete\": \"Delete\",\n      \"moreFilesHint\": \"+{}\",\n      \"addFileOrImage\": \"Add file or link\",\n      \"attachmentsHint\": \"{}\",\n      \"addFileMobile\": \"Add file\",\n      \"extraCount\": \"+{}\",\n      \"deleteFileDescription\": \"Are you sure you want to delete this file? This action is irreversible.\",\n      \"showFileNames\": \"Show file name\",\n      \"downloadSuccess\": \"File downloaded\",\n      \"downloadFailedToken\": \"Failed to download file, user token unavailable\",\n      \"setAsCover\": \"Set as cover\",\n      \"openInBrowser\": \"Open in browser\",\n      \"embedLink\": \"Embed file link\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"Document\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"creating\": \"Creating...\",\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Select a Board to link to\",\n        \"createANewBoard\": \"Create a new Board\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Select a Grid to link to\",\n        \"createANewGrid\": \"Create a new Grid\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Select a Calendar to link to\",\n        \"createANewCalendar\": \"Create a new Calendar\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"Select a Document to link to\"\n      },\n      \"name\": {\n        \"textStyle\": \"Text Style\",\n        \"list\": \"List\",\n        \"toggle\": \"Toggle\",\n        \"fileAndMedia\": \"File & Media\",\n        \"simpleTable\": \"Simple Table\",\n        \"visuals\": \"Visuals\",\n        \"document\": \"Document\",\n        \"advanced\": \"Advanced\",\n        \"text\": \"Text\",\n        \"heading1\": \"Heading 1\",\n        \"heading2\": \"Heading 2\",\n        \"heading3\": \"Heading 3\",\n        \"image\": \"Image\",\n        \"bulletedList\": \"Bulleted list\",\n        \"numberedList\": \"Numbered list\",\n        \"todoList\": \"To-do list\",\n        \"doc\": \"Doc\",\n        \"linkedDoc\": \"Link to page\",\n        \"grid\": \"Grid\",\n        \"linkedGrid\": \"Linked Grid\",\n        \"kanban\": \"Kanban\",\n        \"linkedKanban\": \"Linked Kanban\",\n        \"calendar\": \"Calendar\",\n        \"linkedCalendar\": \"Linked Calendar\",\n        \"quote\": \"Quote\",\n        \"divider\": \"Divider\",\n        \"table\": \"Table\",\n        \"callout\": \"Callout\",\n        \"outline\": \"Outline\",\n        \"mathEquation\": \"Math Equation\",\n        \"code\": \"Code\",\n        \"toggleList\": \"Toggle list\",\n        \"toggleHeading1\": \"Toggle heading 1\",\n        \"toggleHeading2\": \"Toggle heading 2\",\n        \"toggleHeading3\": \"Toggle heading 3\",\n        \"emoji\": \"Emoji\",\n        \"aiWriter\": \"Ask AI Anything\",\n        \"dateOrReminder\": \"Date or Reminder\",\n        \"photoGallery\": \"Photo Gallery\",\n        \"file\": \"File\",\n        \"twoColumns\": \"2 Columns\",\n        \"threeColumns\": \"3 Columns\",\n        \"fourColumns\": \"4 Columns\"\n      },\n      \"subPage\": {\n        \"name\": \"Document\",\n        \"keyword1\": \"sub page\",\n        \"keyword2\": \"page\",\n        \"keyword3\": \"child page\",\n        \"keyword4\": \"insert page\",\n        \"keyword5\": \"embed page\",\n        \"keyword6\": \"new page\",\n        \"keyword7\": \"create page\",\n        \"keyword8\": \"document\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Outline\",\n      \"codeBlock\": \"Code Block\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Referenced Board\",\n      \"referencedGrid\": \"Referenced Grid\",\n      \"referencedCalendar\": \"Referenced Calendar\",\n      \"referencedDocument\": \"Referenced Document\",\n      \"aiWriter\": {\n        \"userQuestion\": \"Ask AI anything\",\n        \"continueWriting\": \"Continue writing\",\n        \"fixSpelling\": \"Fix spelling & grammar\",\n        \"improveWriting\": \"Improve writing\",\n        \"summarize\": \"Summarize\",\n        \"explain\": \"Explain\",\n        \"makeShorter\": \"Make shorter\",\n        \"makeLonger\": \"Make longer\"\n      },\n      \"autoGeneratorMenuItemName\": \"AI Writer\",\n      \"autoGeneratorTitleName\": \"AI: Ask AI to write anything...\",\n      \"autoGeneratorLearnMore\": \"Learn more\",\n      \"autoGeneratorGenerate\": \"Generate\",\n      \"autoGeneratorHintText\": \"Ask AI ...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Can't get AI key\",\n      \"autoGeneratorRewrite\": \"Rewrite\",\n      \"smartEdit\": \"Ask AI\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"Fix spelling & grammar\",\n      \"warning\": \"⚠️ AI responses can be inaccurate or misleading.\",\n      \"smartEditSummarize\": \"Summarize\",\n      \"smartEditImproveWriting\": \"Improve writing\",\n      \"smartEditMakeLonger\": \"Make longer\",\n      \"smartEditCouldNotFetchResult\": \"Could not fetch result from AI\",\n      \"smartEditCouldNotFetchKey\": \"Could not fetch AI key\",\n      \"smartEditDisabled\": \"Connect AI in Settings\",\n      \"appflowyAIEditDisabled\": \"Sign in to enable AI features\",\n      \"discardResponse\": \"Are you sure you want to discard the AI response?\",\n      \"createInlineMathEquation\": \"Create equation\",\n      \"fonts\": \"Fonts\",\n      \"insertDate\": \"Insert date\",\n      \"emoji\": \"Emoji\",\n      \"toggleList\": \"Toggle list\",\n      \"emptyToggleHeading\": \"Empty toggle h{}. Click to add content.\",\n      \"emptyToggleList\": \"Empty toggle list. Click to add content.\",\n      \"emptyToggleHeadingWeb\": \"Empty toggle h{level}. Click to add content\",\n      \"quoteList\": \"Quote list\",\n      \"numberedList\": \"Numbered list\",\n      \"bulletedList\": \"Bulleted list\",\n      \"todoList\": \"Todo list\",\n      \"callout\": \"Callout\",\n      \"simpleTable\": {\n        \"moreActions\": {\n          \"color\": \"Color\",\n          \"align\": \"Align\",\n          \"delete\": \"Delete\",\n          \"duplicate\": \"Duplicate\",\n          \"insertLeft\": \"Insert left\",\n          \"insertRight\": \"Insert right\",\n          \"insertAbove\": \"Insert above\",\n          \"insertBelow\": \"Insert below\",\n          \"headerColumn\": \"Header column\",\n          \"headerRow\": \"Header row\",\n          \"clearContents\": \"Clear contents\",\n          \"setToPageWidth\": \"Set to page width\",\n          \"distributeColumnsWidth\": \"Distribute columns evenly\",\n          \"duplicateRow\": \"Duplicate row\",\n          \"duplicateColumn\": \"Duplicate column\",\n          \"textColor\": \"Text color\",\n          \"cellBackgroundColor\": \"Cell background color\",\n          \"duplicateTable\": \"Duplicate table\"\n        },\n        \"clickToAddNewRow\": \"Click to add a new row\",\n        \"clickToAddNewColumn\": \"Click to add a new column\",\n        \"clickToAddNewRowAndColumn\": \"Click to add a new row and column\",\n        \"headerName\": {\n          \"table\": \"Table\",\n          \"alignText\": \"Align text\"\n        }\n      },\n      \"cover\": {\n        \"changeCover\": \"Change Cover\",\n        \"colors\": \"Colors\",\n        \"images\": \"Images\",\n        \"clearAll\": \"Clear All\",\n        \"abstract\": \"Abstract\",\n        \"addCover\": \"Add Cover\",\n        \"addLocalImage\": \"Add local image\",\n        \"invalidImageUrl\": \"Invalid image URL\",\n        \"failedToAddImageToGallery\": \"Failed to add image to gallery\",\n        \"enterImageUrl\": \"Enter image URL\",\n        \"add\": \"Add\",\n        \"back\": \"Back\",\n        \"saveToGallery\": \"Save to gallery\",\n        \"removeIcon\": \"Remove icon\",\n        \"removeCover\": \"Remove cover\",\n        \"pasteImageUrl\": \"Paste image URL\",\n        \"or\": \"OR\",\n        \"pickFromFiles\": \"Pick from files\",\n        \"couldNotFetchImage\": \"Could not fetch image\",\n        \"imageSavingFailed\": \"Image Saving Failed\",\n        \"addIcon\": \"Add icon\",\n        \"changeIcon\": \"Change icon\",\n        \"coverRemoveAlert\": \"It will be removed from cover after it is deleted.\",\n        \"alertDialogConfirmation\": \"Are you sure, you want to continue?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"Math Equation\",\n        \"addMathEquation\": \"Add a TeX equation\",\n        \"editMathEquation\": \"Edit Math Equation\"\n      },\n      \"optionAction\": {\n        \"click\": \"Click\",\n        \"toOpenMenu\": \" to open menu\",\n        \"drag\": \"Drag\",\n        \"toMove\": \" to move\",\n        \"delete\": \"Delete\",\n        \"duplicate\": \"Duplicate\",\n        \"turnInto\": \"Turn into\",\n        \"moveUp\": \"Move up\",\n        \"moveDown\": \"Move down\",\n        \"color\": \"Color\",\n        \"align\": \"Align\",\n        \"left\": \"Left\",\n        \"center\": \"Center\",\n        \"right\": \"Right\",\n        \"defaultColor\": \"Default\",\n        \"depth\": \"Depth\",\n        \"copyLinkToBlock\": \"Copy link to block\"\n      },\n      \"image\": {\n        \"addAnImage\": \"Add images\",\n        \"copiedToPasteBoard\": \"The image link has been copied to the clipboard\",\n        \"addAnImageDesktop\": \"Add an image\",\n        \"addAnImageMobile\": \"Click to add one or more images\",\n        \"dropImageToInsert\": \"Drop images to insert\",\n        \"imageUploadFailed\": \"Image upload failed\",\n        \"imageDownloadFailed\": \"Image download failed, please try again\",\n        \"imageDownloadFailedToken\": \"Image download failed due to missing user token, please try again\",\n        \"errorCode\": \"Error code\"\n      },\n      \"photoGallery\": {\n        \"name\": \"Photo gallery\",\n        \"imageKeyword\": \"image\",\n        \"imageGalleryKeyword\": \"image gallery\",\n        \"photoKeyword\": \"photo\",\n        \"photoBrowserKeyword\": \"photo browser\",\n        \"galleryKeyword\": \"gallery\",\n        \"addImageTooltip\": \"Add image\",\n        \"changeLayoutTooltip\": \"Change layout\",\n        \"browserLayout\": \"Browser\",\n        \"gridLayout\": \"Grid\",\n        \"deleteBlockTooltip\": \"Delete whole gallery\"\n      },\n      \"math\": {\n        \"copiedToPasteBoard\": \"The math equation has been copied to the clipboard\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"The link has been copied to the clipboard\",\n        \"convertToLink\": \"Convert to embed link\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Add headings to create a table of contents.\",\n        \"noMatchHeadings\": \"No matching headings found.\"\n      },\n      \"table\": {\n        \"addAfter\": \"Add after\",\n        \"addBefore\": \"Add before\",\n        \"delete\": \"Delete\",\n        \"clear\": \"Clear content\",\n        \"duplicate\": \"Duplicate\",\n        \"bgColor\": \"Background color\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Copy\",\n        \"cut\": \"Cut\",\n        \"paste\": \"Paste\",\n        \"pasteAsPlainText\": \"Paste as plain text\"\n      },\n      \"action\": \"Actions\",\n      \"database\": {\n        \"selectDataSource\": \"Select data source\",\n        \"noDataSource\": \"No data source\",\n        \"selectADataSource\": \"Select a data source\",\n        \"toContinue\": \"to continue\",\n        \"newDatabase\": \"New Database\",\n        \"linkToDatabase\": \"Link to Database\"\n      },\n      \"date\": \"Date\",\n      \"video\": {\n        \"label\": \"Video\",\n        \"emptyLabel\": \"Add a video\",\n        \"placeholder\": \"Paste the video link\",\n        \"copiedToPasteBoard\": \"The video link has been copied to the clipboard\",\n        \"insertVideo\": \"Add video\",\n        \"invalidVideoUrl\": \"The source URL is not supported yet.\",\n        \"invalidVideoUrlYouTube\": \"YouTube is not supported yet.\",\n        \"supportedFormats\": \"Supported formats: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264\"\n      },\n      \"file\": {\n        \"name\": \"File\",\n        \"uploadTab\": \"Upload\",\n        \"uploadMobile\": \"Choose a file\",\n        \"uploadMobileGallery\": \"From Photo Gallery\",\n        \"networkTab\": \"Embed link\",\n        \"placeholderText\": \"Upload or embed a file\",\n        \"placeholderDragging\": \"Drop the file to upload\",\n        \"dropFileToUpload\": \"Drop a file to upload\",\n        \"fileUploadHint\": \"Drag & drop a file or click to \",\n        \"fileUploadHintSuffix\": \"Browse\",\n        \"networkHint\": \"Paste a file link\",\n        \"networkUrlInvalid\": \"Invalid URL. Check the URL and try again.\",\n        \"networkAction\": \"Embed\",\n        \"fileTooBigError\": \"File size is too big, please upload a file with size less than 10MB\",\n        \"renameFile\": {\n          \"title\": \"Rename file\",\n          \"description\": \"Enter the new name for this file\",\n          \"nameEmptyError\": \"File name cannot be left empty.\"\n        },\n        \"uploadedAt\": \"Uploaded on {}\",\n        \"linkedAt\": \"Link added on {}\",\n        \"failedToOpenMsg\": \"Failed to open, file not found\"\n      },\n      \"subPage\": {\n        \"handlingPasteHint\": \" - (handling paste)\",\n        \"errors\": {\n          \"failedDeletePage\": \"Failed to delete page\",\n          \"failedCreatePage\": \"Failed to create page\",\n          \"failedMovePage\": \"Failed to move page to this document\",\n          \"failedDuplicatePage\": \"Failed to duplicate page\",\n          \"failedDuplicateFindView\": \"Failed to duplicate page - original view not found\"\n        }\n      },\n      \"cannotMoveToItsChildren\": \"Cannot move to its children\",\n      \"linkPreview\": {\n        \"typeSelection\": {\n          \"pasteAs\": \"Paste as\",\n          \"mention\": \"Mention\",\n          \"URL\": \"URL\",\n          \"bookmark\": \"Bookmark\",\n          \"embed\": \"Embed\"\n        },\n        \"linkPreviewMenu\": {\n          \"toMetion\": \"Convert to Mention\",\n          \"toUrl\": \"Convert to URL\",\n          \"toEmbed\": \"Convert to Embed\",\n          \"toBookmark\": \"Convert to Bookmark\",\n          \"copyLink\": \"Copy Link\",\n          \"replace\": \"Replace\",\n          \"reload\": \"Reload\",\n          \"removeLink\": \"Remove Link\",\n          \"pasteHint\": \"Paste in https://...\",\n          \"unableToDisplay\": \"unable to display\"\n        }\n      }\n    },\n    \"outlineBlock\": {\n      \"placeholder\": \"Table of Contents\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Type '/' for commands\"\n    },\n    \"title\": {\n      \"placeholder\": \"Untitled\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Click to add image(s)\",\n      \"upload\": {\n        \"label\": \"Upload\",\n        \"placeholder\": \"Click to upload image\"\n      },\n      \"url\": {\n        \"label\": \"Image URL\",\n        \"placeholder\": \"Enter image URL\"\n      },\n      \"ai\": {\n        \"label\": \"Generate image from AI\",\n        \"placeholder\": \"Please input the prompt for AI to generate image\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Generate image from Stability AI\",\n        \"placeholder\": \"Please input the prompt for Stability AI to generate image\"\n      },\n      \"support\": \"Image size limit is 5MB. Supported formats: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Invalid image\",\n        \"invalidImageSize\": \"Image size must be less than 5MB\",\n        \"invalidImageFormat\": \"Image format is not supported. Supported formats: JPEG, PNG, JPG, GIF, SVG, WEBP\",\n        \"invalidImageUrl\": \"Invalid image URL\",\n        \"noImage\": \"No such file or directory\",\n        \"multipleImagesFailed\": \"One or more images failed to upload, please try again\"\n      },\n      \"embedLink\": {\n        \"label\": \"Embed link\",\n        \"placeholder\": \"Paste or type an image link\"\n      },\n      \"unsplash\": {\n        \"label\": \"Unsplash\"\n      },\n      \"searchForAnImage\": \"Search for an image\",\n      \"pleaseInputYourOpenAIKey\": \"please input your AI key in Settings page\",\n      \"saveImageToGallery\": \"Save image\",\n      \"failedToAddImageToGallery\": \"Failed to save image\",\n      \"successToAddImageToGallery\": \"Saved image to Photos\",\n      \"unableToLoadImage\": \"Unable to load image\",\n      \"maximumImageSize\": \"Maximum supported upload image size is 10MB\",\n      \"uploadImageErrorImageSizeTooBig\": \"Image size must be less than 10MB\",\n      \"imageIsUploading\": \"Image is uploading\",\n      \"openFullScreen\": \"Open in full screen\",\n      \"interactiveViewer\": {\n        \"toolbar\": {\n          \"previousImageTooltip\": \"Previous image\",\n          \"nextImageTooltip\": \"Next image\",\n          \"zoomOutTooltip\": \"Zoom out\",\n          \"zoomInTooltip\": \"Zoom in\",\n          \"changeZoomLevelTooltip\": \"Change zoom level\",\n          \"openLocalImage\": \"Open image\",\n          \"downloadImage\": \"Download image\",\n          \"closeViewer\": \"Close interactive viewer\",\n          \"scalePercentage\": \"{}%\",\n          \"deleteImageTooltip\": \"Delete image\"\n        }\n      }\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Language\",\n        \"placeholder\": \"Select language\",\n        \"auto\": \"Auto\"\n      },\n      \"copyTooltip\": \"Copy\",\n      \"searchLanguageHint\": \"Search for a language\",\n      \"codeCopiedSnackbar\": \"Code copied to clipboard!\"\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Paste or type a link\",\n      \"openInNewTab\": \"Open in new tab\",\n      \"copyLink\": \"Copy link\",\n      \"removeLink\": \"Remove link\",\n      \"url\": {\n        \"label\": \"Link URL\",\n        \"placeholder\": \"Enter link URL\"\n      },\n      \"title\": {\n        \"label\": \"Link Title\",\n        \"placeholder\": \"Enter link title\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"Mention a person or a page or date...\",\n      \"page\": {\n        \"label\": \"Link to page\",\n        \"tooltip\": \"Click to open page\"\n      },\n      \"deleted\": \"Deleted\",\n      \"deletedContent\": \"This content does not exist or has been deleted\",\n      \"noAccess\": \"No Access\",\n      \"deletedPage\": \"Deleted page\",\n      \"trashHint\": \" - in trash\",\n      \"morePages\": \"more pages\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"Reset to default\",\n      \"textSize\": \"Text size\",\n      \"textColor\": \"Text color\",\n      \"h1\": \"Heading 1\",\n      \"h2\": \"Heading 2\",\n      \"h3\": \"Heading 3\",\n      \"alignLeft\": \"Align left\",\n      \"alignRight\": \"Align right\",\n      \"alignCenter\": \"Align center\",\n      \"link\": \"Link\",\n      \"textAlign\": \"Text align\",\n      \"moreOptions\": \"More options\",\n      \"font\": \"Font\",\n      \"inlineCode\": \"Inline code\",\n      \"suggestions\": \"Suggestions\",\n      \"turnInto\": \"Turn into\",\n      \"equation\": \"Equation\",\n      \"insert\": \"Insert\",\n      \"linkInputHint\": \"Paste link or search pages\",\n      \"pageOrURL\": \"Page or URL\",\n      \"linkName\": \"Link Name\",\n      \"linkNameHint\": \"Input link name\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"Unable to parse the block content\",\n      \"clickToCopyTheBlockContent\": \"Click to copy the block content\",\n      \"blockContentHasBeenCopied\": \"The block content has been copied.\",\n      \"parseError\": \"An error occurred while parsing the {} block.\",\n      \"copyBlockContent\": \"Copy block content\"\n    },\n    \"mobilePageSelector\": {\n      \"title\": \"Select page\",\n      \"failedToLoad\": \"Failed to load page list\",\n      \"noPagesFound\": \"No pages found\"\n    },\n    \"attachmentMenu\": {\n      \"choosePhoto\": \"Choose photo\",\n      \"takePicture\": \"Take a picture\",\n      \"chooseFile\": \"Choose file\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"label\": \"Column\",\n      \"createNewCard\": \"New\",\n      \"renameGroupTooltip\": \"Press to rename group\",\n      \"createNewColumn\": \"Add a new group\",\n      \"addToColumnTopTooltip\": \"Add a new card at the top\",\n      \"addToColumnBottomTooltip\": \"Add a new card at the bottom\",\n      \"renameColumn\": \"Rename\",\n      \"hideColumn\": \"Hide\",\n      \"newGroup\": \"New group\",\n      \"deleteColumn\": \"Delete\",\n      \"deleteColumnConfirmation\": \"This will delete this group and all the cards in it. Are you sure you want to continue?\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"Hidden Groups\",\n      \"collapseTooltip\": \"Hide the hidden groups\",\n      \"expandTooltip\": \"View the hidden groups\"\n    },\n    \"cardDetail\": \"Card Detail\",\n    \"cardActions\": \"Card Actions\",\n    \"cardDuplicated\": \"Card has been duplicated\",\n    \"cardDeleted\": \"Card has been deleted\",\n    \"showOnCard\": \"Show on card detail\",\n    \"setting\": \"Setting\",\n    \"propertyName\": \"Property name\",\n    \"menuName\": \"Board\",\n    \"showUngrouped\": \"Show ungrouped items\",\n    \"ungroupedButtonText\": \"Ungrouped\",\n    \"ungroupedButtonTooltip\": \"Contains cards that don't belong in any group\",\n    \"ungroupedItemsTitle\": \"Click to add to the board\",\n    \"groupBy\": \"Group by\",\n    \"groupCondition\": \"Group condition\",\n    \"referencedBoardPrefix\": \"View of\",\n    \"notesTooltip\": \"Notes inside\",\n    \"mobile\": {\n      \"editURL\": \"Edit URL\",\n      \"showGroup\": \"Show group\",\n      \"showGroupContent\": \"Are you sure you want to show this group on the board?\",\n      \"failedToLoad\": \"Failed to load board view\"\n    },\n    \"dateCondition\": {\n      \"weekOf\": \"Week of {} - {}\",\n      \"today\": \"Today\",\n      \"yesterday\": \"Yesterday\",\n      \"tomorrow\": \"Tomorrow\",\n      \"lastSevenDays\": \"Last 7 days\",\n      \"nextSevenDays\": \"Next 7 days\",\n      \"lastThirtyDays\": \"Last 30 days\",\n      \"nextThirtyDays\": \"Next 30 days\"\n    },\n    \"noGroup\": \"No group by property\",\n    \"noGroupDesc\": \"Board views require a property to group by in order to display\",\n    \"media\": {\n      \"cardText\": \"{} {}\",\n      \"fallbackName\": \"files\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Calendar\",\n    \"defaultNewCalendarTitle\": \"Untitled\",\n    \"newEventButtonTooltip\": \"Add a new event\",\n    \"navigation\": {\n      \"today\": \"Today\",\n      \"jumpToday\": \"Jump to Today\",\n      \"previousMonth\": \"Previous Month\",\n      \"nextMonth\": \"Next Month\",\n      \"views\": {\n        \"day\": \"Day\",\n        \"week\": \"Week\",\n        \"month\": \"Month\",\n        \"year\": \"Year\"\n      }\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"No events yet\",\n      \"emptyBody\": \"Press the plus button to create an event on this day.\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Show week numbers\",\n      \"showWeekends\": \"Show weekends\",\n      \"firstDayOfWeek\": \"Start week on\",\n      \"layoutDateField\": \"Layout calendar by\",\n      \"changeLayoutDateField\": \"Change layout field\",\n      \"noDateTitle\": \"No Date\",\n      \"noDateHint\": {\n        \"zero\": \"Unscheduled events will show up here\",\n        \"one\": \"{count} unscheduled event\",\n        \"other\": \"{count} unscheduled events\"\n      },\n      \"unscheduledEventsTitle\": \"Unscheduled events\",\n      \"clickToAdd\": \"Click to add to the calendar\",\n      \"name\": \"Calendar settings\",\n      \"clickToOpen\": \"Click to open the record\"\n    },\n    \"referencedCalendarPrefix\": \"View of\",\n    \"quickJumpYear\": \"Jump to\",\n    \"duplicateEvent\": \"Duplicate event\"\n  },\n  \"errorDialog\": {\n    \"title\": \"@:appName Error\",\n    \"howToFixFallback\": \"We're sorry for the inconvenience! Submit an issue on our GitHub page that describes your error.\",\n    \"howToFixFallbackHint1\": \"We're sorry for the inconvenience! Submit an issue on our \",\n    \"howToFixFallbackHint2\": \" page that describes your error.\",\n    \"github\": \"View on GitHub\"\n  },\n  \"search\": {\n    \"label\": \"Search\",\n    \"sidebarSearchIcon\": \"Search and quickly jump to a page\",\n    \"searchOrAskAI\": \"Search or ask AI\",\n    \"searchFieldHint\": \"Search or ask a question in {}...\",\n    \"askAIAnything\": \"Ask AI anything\",\n    \"askAIFor\": \"Ask AI\",\n    \"searching\": \"Searching...\",\n    \"noResultForSearching\": \"No matches found\",\n    \"noResultForSearchingHint\": \"Try different questions or keywords.\\n Some pages may be in the Trash.\",\n    \"noResultForSearchingHintWithoutTrash\": \"Try different questions or keywords\\n Some pages may be in the \",\n    \"bestMatch\": \"Best match\",\n    \"seeMore\": \"See more\",\n    \"showMore\": \"Show more\",\n    \"somethingWentWrong\": \"Something went wrong\",\n    \"pageNotExist\": \"This page doesn't exist\",\n    \"tryAgainOrLater\": \"Please try again later\",\n    \"placeholder\": {\n      \"actions\": \"Search actions...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Copied to clipboard\",\n      \"fail\": \"Unable to copy\"\n    }\n  },\n  \"unSupportBlock\": \"The current version does not support this Block.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Are you sure want to delete the {pageType}?\",\n    \"deleteContentCaption\": \"if you delete this {pageType}, you can restore it from the trash.\"\n  },\n  \"colors\": {\n    \"custom\": \"Custom\",\n    \"default\": \"Default\",\n    \"red\": \"Red\",\n    \"orange\": \"Orange\",\n    \"yellow\": \"Yellow\",\n    \"green\": \"Green\",\n    \"blue\": \"Blue\",\n    \"purple\": \"Purple\",\n    \"pink\": \"Pink\",\n    \"brown\": \"Brown\",\n    \"gray\": \"Gray\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"Emoji\",\n    \"search\": \"Search emoji\",\n    \"noRecent\": \"No recent emoji\",\n    \"noEmojiFound\": \"No emoji found\",\n    \"filter\": \"Filter\",\n    \"random\": \"Random\",\n    \"selectSkinTone\": \"Select skin tone\",\n    \"remove\": \"Remove emoji\",\n    \"categories\": {\n      \"smileys\": \"Smileys & Emotion\",\n      \"people\": \"people\",\n      \"animals\": \"nature\",\n      \"food\": \"foods\",\n      \"activities\": \"activities\",\n      \"places\": \"places\",\n      \"objects\": \"objects\",\n      \"symbols\": \"symbols\",\n      \"flags\": \"flags\",\n      \"nature\": \"nature\",\n      \"frequentlyUsed\": \"frequently Used\"\n    },\n    \"skinTone\": {\n      \"default\": \"Default\",\n      \"light\": \"Light\",\n      \"mediumLight\": \"Medium-Light\",\n      \"medium\": \"Medium\",\n      \"mediumDark\": \"Medium-Dark\",\n      \"dark\": \"Dark\"\n    },\n    \"openSourceIconsFrom\": \"Open source icons from\"\n  },\n  \"inlineActions\": {\n    \"noResults\": \"No results\",\n    \"recentPages\": \"Recent pages\",\n    \"pageReference\": \"Page reference\",\n    \"docReference\": \"Document reference\",\n    \"boardReference\": \"Board reference\",\n    \"calReference\": \"Calendar reference\",\n    \"gridReference\": \"Grid reference\",\n    \"date\": \"Date\",\n    \"reminder\": {\n      \"groupTitle\": \"Reminder\",\n      \"shortKeyword\": \"remind\"\n    },\n    \"createPage\": \"Create \\\"{}\\\" sub-page\"\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"Change the date and time format in settings\",\n    \"dateFormat\": \"Date format\",\n    \"includeTime\": \"Include time\",\n    \"isRange\": \"End date\",\n    \"timeFormat\": \"Time format\",\n    \"clearDate\": \"Clear date\",\n    \"reminderLabel\": \"Reminder\",\n    \"selectReminder\": \"Select reminder\",\n    \"reminderOptions\": {\n      \"none\": \"None\",\n      \"atTimeOfEvent\": \"Time of event\",\n      \"fiveMinsBefore\": \"5 mins before\",\n      \"tenMinsBefore\": \"10 mins before\",\n      \"fifteenMinsBefore\": \"15 mins before\",\n      \"thirtyMinsBefore\": \"30 mins before\",\n      \"oneHourBefore\": \"1 hour before\",\n      \"twoHoursBefore\": \"2 hours before\",\n      \"onDayOfEvent\": \"On day of event\",\n      \"oneDayBefore\": \"1 day before\",\n      \"twoDaysBefore\": \"2 days before\",\n      \"oneWeekBefore\": \"1 week before\",\n      \"custom\": \"Custom\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"Yesterday\",\n    \"today\": \"Today\",\n    \"tomorrow\": \"Tomorrow\",\n    \"oneWeek\": \"1 week\"\n  },\n  \"notificationHub\": {\n    \"title\": \"Notifications\",\n    \"closeNotification\": \"Close notification\",\n    \"viewNotifications\": \"View notifications\",\n    \"noNotifications\": \"No notifications yet\",\n    \"mentionedYou\": \"Mentioned you\",\n    \"archivedTooltip\": \"Archive this notification\",\n    \"unarchiveTooltip\": \"Unarchive this notification\",\n    \"markAsReadTooltip\": \"Mark this notification as read\",\n    \"markAsArchivedSucceedToast\": \"Archived successfully\",\n    \"markAllAsArchivedSucceedToast\": \"Archived all successfully\",\n    \"markAsReadSucceedToast\": \"Mark as read successfully\",\n    \"markAllAsReadSucceedToast\": \"Mark all as read successfully\",\n    \"today\": \"Today\",\n    \"older\": \"Older\",\n    \"mobile\": {\n      \"title\": \"Updates\"\n    },\n    \"emptyTitle\": \"All caught up!\",\n    \"emptyBody\": \"No pending notifications or actions. Enjoy the calm.\",\n    \"tabs\": {\n      \"inbox\": \"Inbox\",\n      \"upcoming\": \"Upcoming\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"Mark all as read\",\n      \"showAll\": \"All\",\n      \"showUnreads\": \"Unread\"\n    },\n    \"filters\": {\n      \"ascending\": \"Ascending\",\n      \"descending\": \"Descending\",\n      \"groupByDate\": \"Group by date\",\n      \"showUnreadsOnly\": \"Show unreads only\",\n      \"resetToDefault\": \"Reset to default\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"Reminder\",\n    \"message\": \"Remember to check this before you forget!\",\n    \"tooltipDelete\": \"Delete\",\n    \"tooltipMarkRead\": \"Mark as read\",\n    \"tooltipMarkUnread\": \"Mark as unread\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"Find\",\n    \"previousMatch\": \"Previous match\",\n    \"nextMatch\": \"Next match\",\n    \"close\": \"Close\",\n    \"replace\": \"Replace\",\n    \"replaceAll\": \"Replace all\",\n    \"noResult\": \"No results\",\n    \"caseSensitive\": \"Case sensitive\",\n    \"searchMore\": \"Search to find more results\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"We're sorry\",\n    \"loadingViewError\": \"We're having trouble loading this view. Please check your internet connection, refresh the app, and do not hesitate to reach out to the team if the issue continues.\",\n    \"syncError\": \"Data is not synced from another device\",\n    \"syncErrorHint\": \"Please reopen this page on the device where it was last edited, then open it again on the current device.\",\n    \"clickToCopy\": \"Click to copy error code\"\n  },\n  \"editor\": {\n    \"bold\": \"Bold\",\n    \"bulletedList\": \"Bulleted list\",\n    \"bulletedListShortForm\": \"Bulleted\",\n    \"checkbox\": \"Checkbox\",\n    \"embedCode\": \"Embed Code\",\n    \"heading1\": \"H1\",\n    \"heading2\": \"H2\",\n    \"heading3\": \"H3\",\n    \"highlight\": \"Highlight\",\n    \"color\": \"Color\",\n    \"image\": \"Image\",\n    \"date\": \"Date\",\n    \"page\": \"Page\",\n    \"italic\": \"Italic\",\n    \"link\": \"Link\",\n    \"numberedList\": \"Numbered list\",\n    \"numberedListShortForm\": \"Numbered\",\n    \"toggleHeading1ShortForm\": \"Toggle H1\",\n    \"toggleHeading2ShortForm\": \"Toggle H2\",\n    \"toggleHeading3ShortForm\": \"Toggle H3\",\n    \"quote\": \"Quote\",\n    \"strikethrough\": \"Strikethrough\",\n    \"text\": \"Text\",\n    \"underline\": \"Underline\",\n    \"fontColorDefault\": \"Default\",\n    \"fontColorGray\": \"Gray\",\n    \"fontColorBrown\": \"Brown\",\n    \"fontColorOrange\": \"Orange\",\n    \"fontColorYellow\": \"Yellow\",\n    \"fontColorGreen\": \"Green\",\n    \"fontColorBlue\": \"Blue\",\n    \"fontColorPurple\": \"Purple\",\n    \"fontColorPink\": \"Pink\",\n    \"fontColorRed\": \"Red\",\n    \"backgroundColorDefault\": \"Default background\",\n    \"backgroundColorGray\": \"Gray background\",\n    \"backgroundColorBrown\": \"Brown background\",\n    \"backgroundColorOrange\": \"Orange background\",\n    \"backgroundColorYellow\": \"Yellow background\",\n    \"backgroundColorGreen\": \"Green background\",\n    \"backgroundColorBlue\": \"Blue background\",\n    \"backgroundColorPurple\": \"Purple background\",\n    \"backgroundColorPink\": \"Pink background\",\n    \"backgroundColorRed\": \"Red background\",\n    \"backgroundColorLime\": \"Lime background\",\n    \"backgroundColorAqua\": \"Aqua background\",\n    \"done\": \"Done\",\n    \"cancel\": \"Cancel\",\n    \"tint1\": \"Tint 1\",\n    \"tint2\": \"Tint 2\",\n    \"tint3\": \"Tint 3\",\n    \"tint4\": \"Tint 4\",\n    \"tint5\": \"Tint 5\",\n    \"tint6\": \"Tint 6\",\n    \"tint7\": \"Tint 7\",\n    \"tint8\": \"Tint 8\",\n    \"tint9\": \"Tint 9\",\n    \"lightLightTint1\": \"Purple\",\n    \"lightLightTint2\": \"Pink\",\n    \"lightLightTint3\": \"Light Pink\",\n    \"lightLightTint4\": \"Orange\",\n    \"lightLightTint5\": \"Yellow\",\n    \"lightLightTint6\": \"Lime\",\n    \"lightLightTint7\": \"Green\",\n    \"lightLightTint8\": \"Aqua\",\n    \"lightLightTint9\": \"Blue\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"Heading 1\",\n    \"mobileHeading2\": \"Heading 2\",\n    \"mobileHeading3\": \"Heading 3\",\n    \"mobileHeading4\": \"Heading 4\",\n    \"mobileHeading5\": \"Heading 5\",\n    \"mobileHeading6\": \"Heading 6\",\n    \"textColor\": \"Text Color\",\n    \"backgroundColor\": \"Background Color\",\n    \"addYourLink\": \"Add your link\",\n    \"openLink\": \"Open link\",\n    \"copyLink\": \"Copy link\",\n    \"removeLink\": \"Remove link\",\n    \"editLink\": \"Edit link\",\n    \"convertTo\": \"Convert to\",\n    \"linkText\": \"Text\",\n    \"linkTextHint\": \"Please enter text\",\n    \"linkAddressHint\": \"Please enter URL\",\n    \"highlightColor\": \"Highlight color\",\n    \"clearHighlightColor\": \"Clear highlight color\",\n    \"customColor\": \"Custom color\",\n    \"hexValue\": \"Hex value\",\n    \"opacity\": \"Opacity\",\n    \"resetToDefaultColor\": \"Reset to default color\",\n    \"ltr\": \"LTR\",\n    \"rtl\": \"RTL\",\n    \"auto\": \"Auto\",\n    \"cut\": \"Cut\",\n    \"copy\": \"Copy\",\n    \"paste\": \"Paste\",\n    \"find\": \"Find\",\n    \"select\": \"Select\",\n    \"selectAll\": \"Select all\",\n    \"previousMatch\": \"Previous match\",\n    \"nextMatch\": \"Next match\",\n    \"closeFind\": \"Close\",\n    \"replace\": \"Replace\",\n    \"replaceAll\": \"Replace all\",\n    \"regex\": \"Regex\",\n    \"caseSensitive\": \"Case sensitive\",\n    \"uploadImage\": \"Upload Image\",\n    \"urlImage\": \"URL Image\",\n    \"incorrectLink\": \"Incorrect Link\",\n    \"upload\": \"Upload\",\n    \"chooseImage\": \"Choose an image\",\n    \"loading\": \"Loading\",\n    \"imageLoadFailed\": \"Image load failed\",\n    \"divider\": \"Divider\",\n    \"table\": \"Table\",\n    \"colAddBefore\": \"Add before\",\n    \"rowAddBefore\": \"Add before\",\n    \"colAddAfter\": \"Add after\",\n    \"rowAddAfter\": \"Add after\",\n    \"colRemove\": \"Remove\",\n    \"rowRemove\": \"Remove\",\n    \"colDuplicate\": \"Duplicate\",\n    \"rowDuplicate\": \"Duplicate\",\n    \"colClear\": \"Clear Content\",\n    \"rowClear\": \"Clear Content\",\n    \"slashPlaceHolder\": \"Type '/' to insert a block, or start typing\",\n    \"typeSomething\": \"Type something...\",\n    \"toggleListShortForm\": \"Toggle\",\n    \"quoteListShortForm\": \"Quote\",\n    \"mathEquationShortForm\": \"Formula\",\n    \"codeBlockShortForm\": \"Code\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"No favorite page\",\n    \"noFavoriteHintText\": \"Swipe the page to the left to add it to your favorites\",\n    \"removeFromSidebar\": \"Remove from sidebar\",\n    \"addToSidebar\": \"Pin to sidebar\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"Enter a / to insert a block, or start typing\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"To-do\",\n    \"bulletList\": \"List\",\n    \"numberList\": \"List\",\n    \"quote\": \"Quote\",\n    \"heading\": \"Heading {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"Page icon\",\n    \"language\": \"Language\",\n    \"font\": \"Font\",\n    \"actions\": \"Actions\",\n    \"date\": \"Date\",\n    \"addField\": \"Add field\",\n    \"userIcon\": \"User icon\"\n  },\n  \"noLogFiles\": \"There're no log files\",\n  \"newSettings\": {\n    \"myAccount\": {\n      \"title\": \"Account & App\",\n      \"subtitle\": \"Customize your profile, manage account security, open AI keys, or login into your account.\",\n      \"profileLabel\": \"Account name & Profile image\",\n      \"profileNamePlaceholder\": \"Enter your name\",\n      \"accountSecurity\": \"Account security\",\n      \"2FA\": \"2-Step Authentication\",\n      \"aiKeys\": \"AI keys\",\n      \"accountLogin\": \"Account Login\",\n      \"updateNameError\": \"Failed to update name\",\n      \"updateIconError\": \"Failed to update icon\",\n      \"aboutAppFlowy\": \"About @:appName\",\n      \"deleteAccount\": {\n        \"title\": \"Delete Account\",\n        \"subtitle\": \"Permanently delete your account and all of your data.\",\n        \"description\": \"Permanently delete your account and remove access from all workspaces.\",\n        \"deleteMyAccount\": \"Delete my account\",\n        \"dialogTitle\": \"Delete account\",\n        \"dialogContent1\": \"Are you sure you want to permanently delete your account?\",\n        \"dialogContent2\": \"This action cannot be undone, and will remove access from all workspaces, erasing your entire account, including private workspaces, and removing you from all shared workspaces.\",\n        \"confirmHint1\": \"Please type \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\" to confirm.\",\n        \"confirmHint2\": \"I understand that this action is irreversible and will permanently delete my account and all associated data.\",\n        \"confirmHint3\": \"DELETE MY ACCOUNT\",\n        \"checkToConfirmError\": \"You must check the box to confirm deletion\",\n        \"failedToGetCurrentUser\": \"Failed to get current user email\",\n        \"confirmTextValidationFailed\": \"Your confirmation text does not match \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\"\",\n        \"deleteAccountSuccess\": \"Account deleted successfully\"\n      },\n      \"password\": {\n        \"title\": \"Password\",\n        \"confirmPassword\": \"Confirm password\",\n        \"changePassword\": \"Change password\",\n        \"currentPassword\": \"Current password\",\n        \"newPassword\": \"New password\",\n        \"confirmNewPassword\": \"Confirm new password\",\n        \"setupPassword\": \"Setup password\",\n        \"error\": {\n          \"currentPasswordIsRequired\": \"Current password is required\",\n          \"newPasswordIsRequired\": \"New password is required\",\n          \"confirmPasswordIsRequired\": \"Confirm password is required\",\n          \"passwordsDoNotMatch\": \"Passwords do not match\",\n          \"newPasswordIsSameAsCurrent\": \"New password is same as current password\",\n          \"currentPasswordIsIncorrect\": \"Current password is incorrect\",\n          \"passwordShouldBeAtLeast6Characters\": \"Password should be at least {min} characters\",\n          \"passwordCannotBeLongerThan72Characters\": \"Password cannot be longer than {max} characters\"\n        },\n        \"toast\": {\n          \"passwordUpdatedSuccessfully\": \"Password updated successfully\",\n          \"passwordUpdatedFailed\": \"Failed to update password\",\n          \"passwordSetupSuccessfully\": \"Password setup successfully\",\n          \"passwordSetupFailed\": \"Failed to setup password\"\n        },\n        \"hint\": {\n          \"enterYourPassword\": \"Enter your password\",\n          \"confirmYourPassword\": \"Confirm your password\",\n          \"enterYourCurrentPassword\": \"Enter your current password\",\n          \"enterYourNewPassword\": \"Enter your new password\",\n          \"confirmYourNewPassword\": \"Confirm your new password\"\n        }\n      },\n      \"myAccount\": \"My Account\",\n      \"myProfile\": \"My Profile\"\n    },\n    \"workplace\": {\n      \"name\": \"Workplace\",\n      \"title\": \"Workplace Settings\",\n      \"subtitle\": \"Customize your workspace appearance, theme, font, text layout, date, time, and language.\",\n      \"workplaceName\": \"Workplace name\",\n      \"workplaceNamePlaceholder\": \"Enter workplace name\",\n      \"workplaceIcon\": \"Workplace icon\",\n      \"workplaceIconSubtitle\": \"Upload an image or use an emoji for your workspace. Icon will show in your sidebar and notifications.\",\n      \"renameError\": \"Failed to rename workplace\",\n      \"updateIconError\": \"Failed to update icon\",\n      \"chooseAnIcon\": \"Choose an icon\",\n      \"appearance\": {\n        \"name\": \"Appearance\",\n        \"themeMode\": {\n          \"auto\": \"Auto\",\n          \"light\": \"Light\",\n          \"dark\": \"Dark\"\n        },\n        \"language\": \"Language\"\n      }\n    },\n    \"syncState\": {\n      \"syncing\": \"Syncing\",\n      \"synced\": \"Synced\",\n      \"noNetworkConnected\": \"No network connected\"\n    }\n  },\n  \"pageStyle\": {\n    \"title\": \"Page style\",\n    \"layout\": \"Layout\",\n    \"coverImage\": \"Cover image\",\n    \"pageIcon\": \"Page icon\",\n    \"colors\": \"Colors\",\n    \"gradient\": \"Gradient\",\n    \"backgroundImage\": \"Background image\",\n    \"presets\": \"Presets\",\n    \"photo\": \"Photo\",\n    \"unsplash\": \"Unsplash\",\n    \"pageCover\": \"Page cover\",\n    \"none\": \"None\",\n    \"openSettings\": \"Open Settings\",\n    \"photoPermissionTitle\": \"@:appName would like to access your photo library\",\n    \"photoPermissionDescription\": \"@:appName needs access to your photos to let you add images to your documents\",\n    \"cameraPermissionTitle\": \"@:appName would like to access your camera\",\n    \"cameraPermissionDescription\": \"@:appName needs access to your camera to let you add images to your documents from the camera\",\n    \"doNotAllow\": \"Don't Allow\",\n    \"image\": \"Image\"\n  },\n  \"commandPalette\": {\n    \"placeholder\": \"Search or ask a question...\",\n    \"bestMatches\": \"Best matches\",\n    \"aiOverview\": \"AI overview\",\n    \"aiOverviewSource\": \"Reference sources\",\n    \"aiOverviewMoreDetails\": \"More details\",\n    \"aiAskFollowUp\": \"Ask follow-up\",\n    \"pagePreview\": \"Content preview\",\n    \"clickToOpenPage\": \"Click to open page\",\n    \"recentHistory\": \"Recent history\",\n    \"navigateHint\": \"to navigate\",\n    \"loadingTooltip\": \"We are looking for results...\",\n    \"betaLabel\": \"BETA\",\n    \"betaTooltip\": \"We currently only support searching for pages and content in documents\",\n    \"fromTrashHint\": \"From trash\",\n    \"noResultsHint\": \"We didn't find what you're looking for, try searching for another term.\",\n    \"clearSearchTooltip\": \"Clear input\",\n    \"location\": \"Location\",\n    \"created\": \"Created\",\n    \"edited\": \"Edited\"\n  },\n  \"space\": {\n    \"delete\": \"Delete\",\n    \"deleteConfirmation\": \"Delete: \",\n    \"deleteConfirmationDescription\": \"All pages within this Space will be deleted and moved to the Trash, and any published pages will be unpublished.\",\n    \"rename\": \"Rename Space\",\n    \"changeIcon\": \"Change icon\",\n    \"manage\": \"Manage Space\",\n    \"addNewSpace\": \"Create Space\",\n    \"collapseAllSubPages\": \"Collapse all subpages\",\n    \"createNewSpace\": \"Create a new space\",\n    \"createSpaceDescription\": \"Create multiple public and private spaces to better organize your work.\",\n    \"spaceName\": \"Space name\",\n    \"spaceNamePlaceholder\": \"e.g. Marketing, Engineering, HR\",\n    \"permission\": \"Space permission\",\n    \"publicPermission\": \"Public\",\n    \"publicPermissionDescription\": \"All workspace members with full access\",\n    \"privatePermission\": \"Private\",\n    \"privatePermissionDescription\": \"Only you can access this space\",\n    \"spaceIconBackground\": \"Background color\",\n    \"spaceIcon\": \"Icon\",\n    \"dangerZone\": \"Danger Zone\",\n    \"unableToDeleteLastSpace\": \"Unable to delete the last Space\",\n    \"unableToDeleteSpaceNotCreatedByYou\": \"Unable to delete spaces created by others\",\n    \"enableSpacesForYourWorkspace\": \"Enable Spaces for your workspace\",\n    \"title\": \"Spaces\",\n    \"defaultSpaceName\": \"General\",\n    \"upgradeSpaceTitle\": \"Enable Spaces\",\n    \"upgradeSpaceDescription\": \"Create multiple public and private Spaces to better organize your workspace.\",\n    \"upgrade\": \"Update\",\n    \"upgradeYourSpace\": \"Create multiple Spaces\",\n    \"quicklySwitch\": \"Quickly switch to the next space\",\n    \"duplicate\": \"Duplicate Space\",\n    \"movePageToSpace\": \"Move page to space\",\n    \"cannotMovePageToDatabase\": \"Cannot move page to database\",\n    \"switchSpace\": \"Switch space\",\n    \"spaceNameCannotBeEmpty\": \"Space name cannot be empty\",\n    \"success\": {\n      \"deleteSpace\": \"Space deleted successfully\",\n      \"renameSpace\": \"Space renamed successfully\",\n      \"duplicateSpace\": \"Space duplicated successfully\",\n      \"updateSpace\": \"Space updated successfully\"\n    },\n    \"error\": {\n      \"deleteSpace\": \"Failed to delete space\",\n      \"renameSpace\": \"Failed to rename space\",\n      \"duplicateSpace\": \"Failed to duplicate space\",\n      \"updateSpace\": \"Failed to update space\"\n    },\n    \"createSpace\": \"Create space\",\n    \"manageSpace\": \"Manage space\",\n    \"renameSpace\": \"Rename space\",\n    \"mSpaceIconColor\": \"Space icon color\",\n    \"mSpaceIcon\": \"Space icon\"\n  },\n  \"publish\": {\n    \"hasNotBeenPublished\": \"This page hasn't been published yet\",\n    \"spaceHasNotBeenPublished\": \"Haven't supported publishing a space yet\",\n    \"reportPage\": \"Report page\",\n    \"databaseHasNotBeenPublished\": \"Publishing a database is not supported yet.\",\n    \"createdWith\": \"Created with\",\n    \"downloadApp\": \"Download AppFlowy\",\n    \"copy\": {\n      \"codeBlock\": \"The content of code block has been copied to the clipboard\",\n      \"imageBlock\": \"The image link has been copied to the clipboard\",\n      \"mathBlock\": \"The math equation has been copied to the clipboard\",\n      \"fileBlock\": \"The file link has been copied to the clipboard\"\n    },\n    \"containsPublishedPage\": \"This page contains one or more published pages. If you continue, they will be unpublished. Do you want to proceed with deletion?\",\n    \"publishSuccessfully\": \"Published successfully\",\n    \"unpublishSuccessfully\": \"Unpublished successfully\",\n    \"publishFailed\": \"Failed to publish\",\n    \"unpublishFailed\": \"Failed to unpublish\",\n    \"noAccessToVisit\": \"No access to this page...\",\n    \"createWithAppFlowy\": \"Create a website with AppFlowy\",\n    \"fastWithAI\": \"Fast and easy with AI.\",\n    \"tryItNow\": \"Try it now\",\n    \"onlyGridViewCanBePublished\": \"Only Grid view can be published\",\n    \"database\": {\n      \"zero\": \"Publish {} selected view\",\n      \"one\": \"Publish {} selected views\",\n      \"many\": \"Publish {} selected views\",\n      \"other\": \"Publish {} selected views\"\n    },\n    \"mustSelectPrimaryDatabase\": \"The primary view must be selected\",\n    \"noDatabaseSelected\": \"No database selected, please select at least one database.\",\n    \"unableToDeselectPrimaryDatabase\": \"Unable to deselect primary database\",\n    \"saveThisPage\": \"Start with this template\",\n    \"duplicateTitle\": \"Where would you like to add\",\n    \"selectWorkspace\": \"Select a workspace\",\n    \"addTo\": \"Add to\",\n    \"duplicateSuccessfully\": \"Added to your workspace\",\n    \"duplicateSuccessfullyDescription\": \"Don't have AppFlowy installed? The download will start automatically after you click 'Download'.\",\n    \"downloadIt\": \"Download\",\n    \"openApp\": \"Open in app\",\n    \"duplicateFailed\": \"Duplicated failed\",\n    \"membersCount\": {\n      \"zero\": \"No members\",\n      \"one\": \"1 member\",\n      \"many\": \"{count} members\",\n      \"other\": \"{count} members\"\n    },\n    \"useThisTemplate\": \"Use the template\"\n  },\n  \"web\": {\n    \"continue\": \"Continue\",\n    \"or\": \"or\",\n    \"continueWithGoogle\": \"Continue with Google\",\n    \"continueWithGithub\": \"Continue with GitHub\",\n    \"continueWithDiscord\": \"Continue with Discord\",\n    \"continueWithApple\": \"Continue with Apple \",\n    \"moreOptions\": \"More options\",\n    \"collapse\": \"Collapse\",\n    \"signInAgreement\": \"By clicking \\\"Continue\\\" above, you agreed to \\nAppFlowy's \",\n    \"signInLocalAgreement\": \"By clicking \\\"Get Started\\\" above, you agreed to \\nAppFlowy's \",\n    \"and\": \"and\",\n    \"termOfUse\": \"Terms\",\n    \"privacyPolicy\": \"Privacy Policy\",\n    \"signInError\": \"Sign in error\",\n    \"login\": \"Sign up or log in\",\n    \"fileBlock\": {\n      \"uploadedAt\": \"Uploaded on {time}\",\n      \"linkedAt\": \"Link added on {time}\",\n      \"empty\": \"Upload or embed a file\",\n      \"uploadFailed\": \"Upload failed, please try again\",\n      \"retry\": \"Retry\"\n    },\n    \"importNotion\": \"Import from Notion\",\n    \"import\": \"Import\",\n    \"importSuccess\": \"Uploaded successfully\",\n    \"importSuccessMessage\": \"We'll notify you when the import is complete. After that, you can view your imported pages in the sidebar.\",\n    \"importFailed\": \"Import failed, please check the file format\",\n    \"dropNotionFile\": \"Drop your Notion zip file here to upload, or click to browse\",\n    \"error\": {\n      \"pageNameIsEmpty\": \"The page name is empty, please try another one\"\n    }\n  },\n  \"globalComment\": {\n    \"comments\": \"Comments\",\n    \"addComment\": \"Add a comment\",\n    \"reactedBy\": \"reacted by\",\n    \"addReaction\": \"Add reaction\",\n    \"reactedByMore\": \"and {count} others\",\n    \"showSeconds\": {\n      \"one\": \"1 second ago\",\n      \"other\": \"{count} seconds ago\",\n      \"zero\": \"Just now\",\n      \"many\": \"{count} seconds ago\"\n    },\n    \"showMinutes\": {\n      \"one\": \"1 minute ago\",\n      \"other\": \"{count} minutes ago\",\n      \"many\": \"{count} minutes ago\"\n    },\n    \"showHours\": {\n      \"one\": \"1 hour ago\",\n      \"other\": \"{count} hours ago\",\n      \"many\": \"{count} hours ago\"\n    },\n    \"showDays\": {\n      \"one\": \"1 day ago\",\n      \"other\": \"{count} days ago\",\n      \"many\": \"{count} days ago\"\n    },\n    \"showMonths\": {\n      \"one\": \"1 month ago\",\n      \"other\": \"{count} months ago\",\n      \"many\": \"{count} months ago\"\n    },\n    \"showYears\": {\n      \"one\": \"1 year ago\",\n      \"other\": \"{count} years ago\",\n      \"many\": \"{count} years ago\"\n    },\n    \"reply\": \"Reply\",\n    \"deleteComment\": \"Delete comment\",\n    \"youAreNotOwner\": \"You are not the owner of this comment\",\n    \"confirmDeleteDescription\": \"Are you sure you want to delete this comment?\",\n    \"hasBeenDeleted\": \"Deleted\",\n    \"replyingTo\": \"Replying to\",\n    \"noAccessDeleteComment\": \"You're not allowed to delete this comment\",\n    \"collapse\": \"Collapse\",\n    \"readMore\": \"Read more\",\n    \"failedToAddComment\": \"Failed to add comment\",\n    \"commentAddedSuccessfully\": \"Comment added successfully.\",\n    \"commentAddedSuccessTip\": \"You've just added or replied to a comment. Would you like to jump to the top to see the latest comments?\"\n  },\n  \"template\": {\n    \"asTemplate\": \"Save as template\",\n    \"name\": \"Template name\",\n    \"description\": \"Template Description\",\n    \"about\": \"Template About\",\n    \"deleteFromTemplate\": \"Delete from templates\",\n    \"preview\": \"Template Preview\",\n    \"categories\": \"Template Categories\",\n    \"isNewTemplate\": \"PIN to New template\",\n    \"featured\": \"PIN to Featured\",\n    \"relatedTemplates\": \"Related Templates\",\n    \"requiredField\": \"{field} is required\",\n    \"addCategory\": \"Add \\\"{category}\\\"\",\n    \"addNewCategory\": \"Add new category\",\n    \"addNewCreator\": \"Add new creator\",\n    \"deleteCategory\": \"Delete category\",\n    \"editCategory\": \"Edit category\",\n    \"editCreator\": \"Edit creator\",\n    \"category\": {\n      \"name\": \"Category name\",\n      \"icon\": \"Category icon\",\n      \"bgColor\": \"Category background color\",\n      \"priority\": \"Category priority\",\n      \"desc\": \"Category description\",\n      \"type\": \"Category type\",\n      \"icons\": \"Category Icons\",\n      \"colors\": \"Category Colors\",\n      \"byUseCase\": \"By Use Case\",\n      \"byFeature\": \"By Feature\",\n      \"deleteCategory\": \"Delete category\",\n      \"deleteCategoryDescription\": \"Are you sure you want to delete this category?\",\n      \"typeToSearch\": \"Type to search categories...\"\n    },\n    \"creator\": {\n      \"label\": \"Template Creator\",\n      \"name\": \"Creator name\",\n      \"avatar\": \"Creator avatar\",\n      \"accountLinks\": \"Creator account links\",\n      \"uploadAvatar\": \"Click to upload avatar\",\n      \"deleteCreator\": \"Delete creator\",\n      \"deleteCreatorDescription\": \"Are you sure you want to delete this creator?\",\n      \"typeToSearch\": \"Type to search creators...\"\n    },\n    \"uploadSuccess\": \"Template uploaded successfully\",\n    \"uploadSuccessDescription\": \"Your template has been uploaded successfully. You can now view it in the template gallery.\",\n    \"viewTemplate\": \"View template\",\n    \"deleteTemplate\": \"Delete template\",\n    \"deleteSuccess\": \"Template deleted successfully\",\n    \"deleteTemplateDescription\": \"This won't affect the current page or published status. Are you sure you want to delete this template?\",\n    \"addRelatedTemplate\": \"Add related template\",\n    \"removeRelatedTemplate\": \"Remove related template\",\n    \"uploadAvatar\": \"Upload avatar\",\n    \"searchInCategory\": \"Search in {category}\",\n    \"label\": \"Templates\"\n  },\n  \"fileDropzone\": {\n    \"dropFile\": \"Click or drag file to this area to upload\",\n    \"uploading\": \"Uploading...\",\n    \"uploadFailed\": \"Upload failed\",\n    \"uploadSuccess\": \"Upload success\",\n    \"uploadSuccessDescription\": \"The file has been uploaded successfully\",\n    \"uploadFailedDescription\": \"The file upload failed\",\n    \"uploadingDescription\": \"The file is being uploaded\"\n  },\n  \"gallery\": {\n    \"preview\": \"Open in full screen\",\n    \"copy\": \"Copy\",\n    \"download\": \"Download\",\n    \"prev\": \"Previous\",\n    \"next\": \"Next\",\n    \"resetZoom\": \"Reset zoom\",\n    \"zoomIn\": \"Zoom in\",\n    \"zoomOut\": \"Zoom out\"\n  },\n  \"invitation\": {\n    \"join\": \"Join\",\n    \"on\": \"on\",\n    \"invitedBy\": \"Invited by\",\n    \"membersCount\": {\n      \"zero\": \"{count} members\",\n      \"one\": \"{count} member\",\n      \"many\": \"{count} members\",\n      \"other\": \"{count} members\"\n    },\n    \"tip\": \"You've been invited to Join this workspace with the contact information below. If this is incorrect, contact your administrator to resend the invite.\",\n    \"joinWorkspace\": \"Join workspace\",\n    \"success\": \"You've successfully joined the workspace\",\n    \"successMessage\": \"You can now access all the pages and workspaces within it.\",\n    \"openWorkspace\": \"Open AppFlowy\",\n    \"alreadyAccepted\": \"You've already accepted the invitation\",\n    \"errorModal\": {\n      \"title\": \"Something went wrong\",\n      \"description\": \"Your current account {email} may not have access to this workspace. Please log in with the correct account or contact the workspace owner for help.\",\n      \"contactOwner\": \"Contact owner\",\n      \"close\": \"Back to home\",\n      \"changeAccount\": \"Change account\"\n    }\n  },\n  \"requestAccess\": {\n    \"title\": \"No access to this page\",\n    \"subtitle\": \"You can request access from the owner of this page. Once approved, you can view the page.\",\n    \"requestAccess\": \"Request access\",\n    \"backToHome\": \"Back to home\",\n    \"tip\": \"You're currently logged in as <link/>.\",\n    \"mightBe\": \"You might need to <login/> with a different account.\",\n    \"successful\": \"Request sent successfully\",\n    \"successfulMessage\": \"You will be notified once the owner approves your request.\",\n    \"requestError\": \"Failed to request access\",\n    \"repeatRequestError\": \"You've already requested access to this page\"\n  },\n  \"approveAccess\": {\n    \"title\": \"Approve Workspace Join Request\",\n    \"requestSummary\": \"<user/> requests to join <workspace/> and access <page/>\",\n    \"upgrade\": \"upgrade\",\n    \"downloadApp\": \"Download AppFlowy\",\n    \"approveButton\": \"Approve\",\n    \"approveSuccess\": \"Approved successfully\",\n    \"approveError\": \"Failed to approve, ensure the workspace plan limit is not exceeded\",\n    \"getRequestInfoError\": \"Failed to get request info\",\n    \"memberCount\": {\n      \"zero\": \"No members\",\n      \"one\": \"1 member\",\n      \"many\": \"{count} members\",\n      \"other\": \"{count} members\"\n    },\n    \"alreadyProTitle\": \"You've reached the workspace plan limit\",\n    \"alreadyProMessage\": \"Ask them to contact <email/> to unlock more members\",\n    \"repeatApproveError\": \"You've already approved this request\",\n    \"ensurePlanLimit\": \"Ensure the workspace plan limit is not exceeded. If the limit is exceeded, consider <upgrade/> the workspace plan or <download/>.\",\n    \"requestToJoin\": \"requested to join\",\n    \"asMember\": \"as a member\"\n  },\n  \"upgradePlanModal\": {\n    \"title\": \"Upgrade to Pro\",\n    \"message\": \"{name} has reached the free member limit. Upgrade to the Pro Plan to invite more members.\",\n    \"upgradeSteps\": \"How to upgrade your plan on AppFlowy:\",\n    \"step1\": \"1. Go to Settings\",\n    \"step2\": \"2. Click on 'Plan'\",\n    \"step3\": \"3. Select 'Change Plan'\",\n    \"appNote\": \"Note: \",\n    \"actionButton\": \"Upgrade\",\n    \"downloadLink\": \"Download App\",\n    \"laterButton\": \"Later\",\n    \"refreshNote\": \"After successful upgrade, click <refresh/> to activate your new features.\",\n    \"refresh\": \"here\"\n  },\n  \"breadcrumbs\": {\n    \"label\": \"Breadcrumbs\"\n  },\n  \"time\": {\n    \"justNow\": \"Just now\",\n    \"seconds\": {\n      \"one\": \"1 second\",\n      \"other\": \"{count} seconds\"\n    },\n    \"minutes\": {\n      \"one\": \"1 minute\",\n      \"other\": \"{count} minutes\"\n    },\n    \"hours\": {\n      \"one\": \"1 hour\",\n      \"other\": \"{count} hours\"\n    },\n    \"days\": {\n      \"one\": \"1 day\",\n      \"other\": \"{count} days\"\n    },\n    \"weeks\": {\n      \"one\": \"1 week\",\n      \"other\": \"{count} weeks\"\n    },\n    \"months\": {\n      \"one\": \"1 month\",\n      \"other\": \"{count} months\"\n    },\n    \"years\": {\n      \"one\": \"1 year\",\n      \"other\": \"{count} years\"\n    },\n    \"ago\": \"ago\",\n    \"yesterday\": \"Yesterday\",\n    \"today\": \"Today\"\n  },\n  \"members\": {\n    \"zero\": \"No members\",\n    \"one\": \"1 member\",\n    \"many\": \"{count} members\",\n    \"other\": \"{count} members\"\n  },\n  \"tabMenu\": {\n    \"close\": \"Close\",\n    \"closeDisabledHint\": \"Cannot close a pinned tab, please unpin first\",\n    \"closeOthers\": \"Close other tabs\",\n    \"closeOthersHint\": \"This will close all unpinned tabs except this one\",\n    \"closeOthersDisabledHint\": \"All tabs are pinned, cannot find any tabs to close\",\n    \"favorite\": \"Favorite\",\n    \"unfavorite\": \"Unfavorite\",\n    \"favoriteDisabledHint\": \"Cannot favorite this view\",\n    \"pinTab\": \"Pin\",\n    \"unpinTab\": \"Unpin\"\n  },\n  \"openFileMessage\": {\n    \"success\": \"File opened successfully\",\n    \"fileNotFound\": \"File not found\",\n    \"noAppToOpenFile\": \"No app to open this file\",\n    \"permissionDenied\": \"No permission to open this file\",\n    \"unknownError\": \"File open failed\"\n  },\n  \"inviteMember\": {\n    \"requestInviteMembers\": \"Invite to your workspace\",\n    \"inviteFailedMemberLimit\": \"Member limit has been reached, please \",\n    \"upgrade\": \"upgrade\",\n    \"addEmail\": \"email@example.com, email2@example.com...\",\n    \"requestInvites\": \"Send invites\",\n    \"inviteAlready\": \"You've already invited this email: {email}\",\n    \"inviteSuccess\": \"Invitation sent successfully\",\n    \"description\": \"Input emails below with commas between them. Charges are based on member count.\",\n    \"emails\": \"Email\"\n  },\n  \"quickNote\": {\n    \"label\": \"Quick Note\",\n    \"quickNotes\": \"Quick Notes\",\n    \"search\": \"Search Quick Notes\",\n    \"collapseFullView\": \"Collapse full view\",\n    \"expandFullView\": \"Expand full view\",\n    \"createFailed\": \"Failed to create Quick Note\",\n    \"quickNotesEmpty\": \"No Quick Notes\",\n    \"emptyNote\": \"Empty note\",\n    \"deleteNotePrompt\": \"The selected note will be deleted permanently. Are you sure you want to delete it?\",\n    \"addNote\": \"New Note\",\n    \"noAdditionalText\": \"No additional text\"\n  },\n  \"subscribe\": {\n    \"upgradePlanTitle\": \"Compare & select plan\",\n    \"yearly\": \"Yearly\",\n    \"save\": \"Save {discount}%\",\n    \"monthly\": \"Monthly\",\n    \"priceIn\": \"Price in \",\n    \"free\": \"Free\",\n    \"pro\": \"Pro\",\n    \"freeDescription\": \"For individuals up to 2 members to organize everything\",\n    \"proDescription\": \"For small teams to manage projects and team knowledge\",\n    \"proDuration\": {\n      \"monthly\": \"per member per month\\nbilled monthly\",\n      \"yearly\": \"per member per month\\nbilled annually\"\n    },\n    \"cancel\": \"Downgrade\",\n    \"changePlan\": \"Upgrade to Pro Plan\",\n    \"everythingInFree\": \"Everything in Free +\",\n    \"currentPlan\": \"Current\",\n    \"freeDuration\": \"forever\",\n    \"freePoints\": {\n      \"first\": \"1 collaborative workspace up to 2 members\",\n      \"second\": \"Unlimited pages & blocks\",\n      \"three\": \"5 GB storage\",\n      \"four\": \"Intelligent search\",\n      \"five\": \"20 AI responses\",\n      \"six\": \"Mobile app\",\n      \"seven\": \"Real-time collaboration\"\n    },\n    \"proPoints\": {\n      \"first\": \"Unlimited storage\",\n      \"second\": \"Up to 10 workspace members\",\n      \"three\": \"Unlimited AI responses\",\n      \"four\": \"Unlimited file uploads\",\n      \"five\": \"Custom namespace\"\n    },\n    \"cancelPlan\": {\n      \"title\": \"Sorry to see you go\",\n      \"success\": \"Your subscription has been canceled successfully\",\n      \"description\": \"We're sorry to see you go. We'd love to hear your feedback to help us improve AppFlowy. Please take a moment to answer a few questions.\",\n      \"commonOther\": \"Other\",\n      \"otherHint\": \"Write your answer here\",\n      \"questionOne\": {\n        \"question\": \"What prompted you to cancel your AppFlowy Pro subscription?\",\n        \"answerOne\": \"Cost too high\",\n        \"answerTwo\": \"Features did not meet expectations\",\n        \"answerThree\": \"Found a better alternative\",\n        \"answerFour\": \"Did not use it enough to justify the expense\",\n        \"answerFive\": \"Service issue or technical difficulties\"\n      },\n      \"questionTwo\": {\n        \"question\": \"How likely are you to consider re-subscribing to AppFlowy Pro in the future?\",\n        \"answerOne\": \"Very likely\",\n        \"answerTwo\": \"Somewhat likely\",\n        \"answerThree\": \"Not sure\",\n        \"answerFour\": \"Unlikely\",\n        \"answerFive\": \"Very unlikely\"\n      },\n      \"questionThree\": {\n        \"question\": \"Which Pro feature did you value the most during your subscription?\",\n        \"answerOne\": \"Multi-user collaboration\",\n        \"answerTwo\": \"Longer time version history\",\n        \"answerThree\": \"Unlimited AI responses\",\n        \"answerFour\": \"Access to local AI models\"\n      },\n      \"questionFour\": {\n        \"question\": \"How would you describe your overall experience with AppFlowy?\",\n        \"answerOne\": \"Great\",\n        \"answerTwo\": \"Good\",\n        \"answerThree\": \"Average\",\n        \"answerFour\": \"Below average\",\n        \"answerFive\": \"Unsatisfied\"\n      }\n    }\n  },\n  \"ai\": {\n    \"contentPolicyViolation\": \"Image generation failed due to sensitive content. Please rephrase your input and try again\",\n    \"textLimitReachedDescription\": \"Your workspace has run out of free AI responses. Upgrade to the Pro Plan or purchase an AI add-on to unlock unlimited responses\",\n    \"imageLimitReachedDescription\": \"You've used up your free AI image quota. Please upgrade to the Pro Plan or purchase an AI add-on to unlock unlimited responses\",\n    \"limitReachedAction\": {\n      \"textDescription\": \"Your workspace has run out of free AI responses. To get more responses, please\",\n      \"imageDescription\": \"You've used up your free AI image quota. Please\",\n      \"upgrade\": \"upgrade\",\n      \"toThe\": \"to the\",\n      \"proPlan\": \"Pro Plan\",\n      \"orPurchaseAn\": \"or purchase an\",\n      \"aiAddon\": \"AI add-on\"\n    },\n    \"editing\": \"Editing\",\n    \"analyzing\": \"Analyzing\",\n    \"continueWritingEmptyDocumentTitle\": \"Continue writing error\",\n    \"continueWritingEmptyDocumentDescription\": \"We are having trouble expanding on the content in your document. Write a small intro and we can take it from there!\",\n    \"more\": \"More\",\n    \"customPrompt\": {\n      \"browsePrompts\": \"Browse prompts\",\n      \"usePrompt\": \"Use prompt\",\n      \"featured\": \"Featured\",\n      \"custom\": \"Custom\",\n      \"customPrompt\": \"Custom Prompts\",\n      \"databasePrompts\": \"Load prompts from your own database\",\n      \"selectDatabase\": \"Select database\",\n      \"promptDatabase\": \"Prompt database\",\n      \"configureDatabase\": \"Configure Database\",\n      \"title\": \"Title\",\n      \"content\": \"Content\",\n      \"example\": \"Example\",\n      \"category\": \"Category\",\n      \"selectField\": \"Select field\",\n      \"loading\": \"Loading\",\n      \"invalidDatabase\": \"Invalid Database\",\n      \"invalidDatabaseHelp\": \"Ensure that the database has at least two text properties:\\n ◦ One used for the prompt name\\n ◦ One used for the prompt content\\nYou can also optionally add properties for the prompt example and category.\",\n      \"noResults\": \"No prompts found\",\n      \"all\": \"All\",\n      \"development\": \"Development\",\n      \"writing\": \"Writing\",\n      \"healthAndFitness\": \"Health & fitness\",\n      \"business\": \"Business\",\n      \"marketing\": \"Marketing\",\n      \"travel\": \"Travel\",\n      \"others\": \"Other\",\n      \"prompt\": \"Prompt\",\n      \"promptExample\": \"Prompt Example\",\n      \"sampleOutput\": \"Sample output\",\n      \"contentSeo\": \"Content/SEO\",\n      \"emailMarketing\": \"Email Marketing\",\n      \"paidAds\": \"Paid Ads\",\n      \"prCommunication\": \"PR/Communication\",\n      \"recruiting\": \"Recruiting\",\n      \"sales\": \"Sales\",\n      \"socialMedia\": \"Social Media\",\n      \"strategy\": \"Strategy\",\n      \"caseStudies\": \"Case Studies\",\n      \"salesCopy\": \"Sales Copy\",\n      \"education\": \"Education\",\n      \"work\": \"Work\",\n      \"podcastProduction\": \"Podcast Production\",\n      \"copyWriting\": \"Copy Writing\",\n      \"customerSuccess\": \"Customer Success\"\n    }\n  },\n  \"autoUpdate\": {\n    \"criticalUpdateTitle\": \"Update required to continue\",\n    \"criticalUpdateDescription\": \"We've made improvements to enhance your experience! Please update from {currentVersion} to {newVersion} to keep using the app.\",\n    \"criticalUpdateButton\": \"Update\",\n    \"bannerUpdateTitle\": \"New Version Available!\",\n    \"bannerUpdateDescription\": \"Get the latest features and fixes. Click \\\"Update\\\" to install now\",\n    \"bannerUpdateButton\": \"Update\",\n    \"settingsUpdateTitle\": \"New Version ({newVersion}) Available!\",\n    \"settingsUpdateDescription\": \"Current version: {currentVersion} (Official build) → {newVersion}\",\n    \"settingsUpdateButton\": \"Update\",\n    \"settingsUpdateWhatsNew\": \"What's new\"\n  },\n  \"lockPage\": {\n    \"lockPage\": \"Locked\",\n    \"reLockPage\": \"Re-lock\",\n    \"lockTooltip\": \"Page locked to prevent accidental editing. Click to unlock.\",\n    \"pageLockedToast\": \"Page locked. Editing is disabled until someone unlocks it.\",\n    \"lockedOperationTooltip\": \"Page locked to prevent accidental editing.\"\n  },\n  \"suggestion\": {\n    \"accept\": \"Accept\",\n    \"keep\": \"Keep\",\n    \"discard\": \"Discard\",\n    \"close\": \"Close\",\n    \"tryAgain\": \"Try again\",\n    \"rewrite\": \"Rewrite\",\n    \"insertBelow\": \"Insert below\"\n  },\n  \"shareTab\": {\n    \"accessLevel\": {\n      \"view\": \"View\",\n      \"comment\": \"Comment\",\n      \"edit\": \"Edit\",\n      \"fullAccess\": \"Full access\"\n    },\n    \"inviteByEmail\": \"Invite by email\",\n    \"invite\": \"Invite\",\n    \"anyoneAtWorkspace\": \"Anyone at {workspace}\",\n    \"anyoneInGroupWithLinkCanEdit\": \"Anyone in this group with the link can edit\",\n    \"copyLink\": \"Copy link\",\n    \"copiedLinkToClipboard\": \"Copied link to clipboard\",\n    \"removeAccess\": \"Remove access\",\n    \"turnIntoMember\": \"Turn into Member\",\n    \"you\": \"(You)\",\n    \"guest\": \"Guest\",\n    \"onlyFullAccessCanInvite\": \"Only user with full access can invite others\",\n    \"invitationSent\": \"Invitation sent\",\n    \"emailAlreadyInList\": \"The email is already in the list\",\n    \"upgradeToProToInviteGuests\": \"Please upgrade to a Pro plan to invite more guests\",\n    \"maxGuestsReached\": \"You have reached the maximum number of guests\",\n    \"removedGuestSuccessfully\": \"Removed guest successfully\",\n    \"updatedAccessLevelSuccessfully\": \"Updated access level successfully\",\n    \"turnedIntoMemberSuccessfully\": \"Turned into member successfully\",\n    \"peopleAboveCanAccessWithLink\": \"People above can access with the link\",\n    \"cantMakeChanges\": \"Can't make changes\",\n    \"canMakeAnyChanges\": \"Can make any changes\",\n    \"generalAccess\": \"General access\",\n    \"peopleWithAccess\": \"People with access\",\n    \"peopleAboveCanAccessWithTheLink\": \"People above can access with the link\",\n    \"upgrade\": \"Upgrade\",\n    \"toProPlanToInviteGuests\": \" to the Pro plan to invite guests to this page\",\n    \"upgradeToInviteGuest\": {\n      \"title\": {\n        \"owner\": \"Upgrade to invite guest\",\n        \"member\": \"Upgrade to invite guest\",\n        \"guest\": \"Upgrade to invite guest\"\n      },\n      \"description\": {\n        \"owner\": \"Your workspace is currently on the Free plan. Upgrade to the Pro plan to allow external users to access this page as guests.\",\n        \"member\": \"Some invitees are outside your workspace. To invite them as guests, please contact your workspace owner to upgrade to the Pro plan.\",\n        \"guest\": \"Some invitees are outside your workspace. To invite them as guests, please contact your workspace owner to upgrade to the Pro plan.\"\n      }\n    }\n  },\n  \"shareSection\": {\n    \"shared\": \"Shared with me\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/es-VE.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Mi\",\n  \"welcomeText\": \"Bienvenido a @:appName\",\n  \"welcomeTo\": \"Bienvenido a\",\n  \"githubStarText\": \"Favorito en GitHub\",\n  \"subscribeNewsletterText\": \"Suscribir al boletín\",\n  \"letsGoButtonText\": \"Inicio rápido\",\n  \"title\": \"Título\",\n  \"youCanAlso\": \"Tú también puedes\",\n  \"and\": \"y\",\n  \"failedToOpenUrl\": \"No se pudo abrir la URL: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Haga clic para agregar a continuación\",\n    \"addAboveCmd\": \"Alt+clic\",\n    \"addAboveMacCmd\": \"Opción+clic\",\n    \"addAboveTooltip\": \"para agregar arriba\",\n    \"dragTooltip\": \"Arrastre para mover\",\n    \"openMenuTooltip\": \"Haga clic para abrir el menú\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Registro\",\n    \"title\": \"Registro en @:appName\",\n    \"getStartedText\": \"Empezar\",\n    \"emptyPasswordError\": \"La contraseña no puede estar en blanco\",\n    \"repeatPasswordEmptyError\": \"La contraseña repetida no puede estar vacía\",\n    \"unmatchedPasswordError\": \"Las contraseñas no coinciden\",\n    \"alreadyHaveAnAccount\": \"¿Ya posee una cuenta?\",\n    \"emailHint\": \"Correo electrónico\",\n    \"passwordHint\": \"Contraseña\",\n    \"repeatPasswordHint\": \"Repetir contraseña\",\n    \"signUpWith\": \"Registro con:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Ingresa a @:appName\",\n    \"loginButtonText\": \"Ingresar\",\n    \"loginStartWithAnonymous\": \"Comience una sesión anónima\",\n    \"continueAnonymousUser\": \"Continuar con una sesión anónima\",\n    \"buttonText\": \"Ingresar\",\n    \"signingInText\": \"Iniciando sesión...\",\n    \"forgotPassword\": \"¿Olvidó su contraseña?\",\n    \"emailHint\": \"Correo\",\n    \"passwordHint\": \"Contraseña\",\n    \"dontHaveAnAccount\": \"¿No posee credenciales?\",\n    \"createAccount\": \"Crear una cuenta\",\n    \"repeatPasswordEmptyError\": \"La contraseña no puede estar en blanco\",\n    \"unmatchedPasswordError\": \"Las contraseñas no coinciden\",\n    \"syncPromptMessage\": \"La sincronización de los datos puede tardar un poco. Por favor no cierres esta página\",\n    \"or\": \"O\",\n    \"signInWithGoogle\": \"Iniciar sesión con Google\",\n    \"signInWithGithub\": \"Iniciar sesión con Github\",\n    \"signInWithDiscord\": \"Iniciar sesión con Discord\",\n    \"signInWithApple\": \"Continuar con Apple\",\n    \"continueAnotherWay\": \"Continuar por otro camino\",\n    \"signUpWithGoogle\": \"Registrarse con Google\",\n    \"signUpWithGithub\": \"Registrarse con Github\",\n    \"signUpWithDiscord\": \"Registrarse con Discord\",\n    \"signInWith\": \"Inicia sesión con:\",\n    \"signInWithEmail\": \"Iniciar sesión con correo electrónico\",\n    \"signInWithMagicLink\": \"Iniciar sesión con enlace mágico\",\n    \"signUpWithMagicLink\": \"Registrarse con enlace mágico\",\n    \"pleaseInputYourEmail\": \"Por favor, introduzca su dirección de correo electrónico\",\n    \"settings\": \"Configuración\",\n    \"magicLinkSent\": \"Enlace mágico enviado a tu correo electrónico, por favor revisa tu bandeja de entrada\",\n    \"invalidEmail\": \"Por favor, introduce una dirección de correo electrónico válida\",\n    \"alreadyHaveAnAccount\": \"¿Ya tienes cuenta?\",\n    \"logIn\": \"Iniciar sesión\",\n    \"generalError\": \"Algo ha salido mal. Por favor, inténtalo más tarde\",\n    \"limitRateError\": \"Por razones de seguridad, solo puedes solicitar un enlace mágico cada 60 segundos\",\n    \"anonymous\": \"Anónimo\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Elige tu espacio de trabajo\",\n    \"defaultName\": \"Mi espacio de trabajo\",\n    \"create\": \"Crear espacio de trabajo\",\n    \"new\": \"Nuevo espacio de trabajo\",\n    \"learnMore\": \"Más información\",\n    \"reset\": \"Restablecer espacio de trabajo\",\n    \"renameWorkspace\": \"Cambiar el nombre del espacio de trabajo\",\n    \"resetWorkspacePrompt\": \"Al restablecer el espacio de trabajo se eliminarán todas las páginas y datos que contiene. ¿Está seguro de que desea restablecer el espacio de trabajo? Alternativamente, puede comunicarse con el equipo de soporte para restaurar el espacio de trabajo.\",\n    \"hint\": \"Espacio de trabajo\",\n    \"notFoundError\": \"Espacio de trabajo no encontrado\",\n    \"failedToLoad\": \"¡Algo salió mal! No se pudo cargar el espacio de trabajo. Intente cerrar cualquier instancia abierta de @:appName y vuelva a intentarlo.\",\n    \"errorActions\": {\n      \"reportIssue\": \"Reportar un problema\",\n      \"reportIssueOnGithub\": \"Informar un problema en Github\",\n      \"exportLogFiles\": \"Exportar archivos de registro (logs)\",\n      \"reachOut\": \"Comuníquese con Discord\"\n    },\n    \"menuTitle\": \"Espacios de trabajo\",\n    \"deleteWorkspaceHintText\": \"¿Está seguro de que desea eliminar el espacio de trabajo? Esta acción no se puede deshacer.\",\n    \"createSuccess\": \"Espacio de trabajo creado exitosamente\",\n    \"createFailed\": \"No se pudo crear el espacio de trabajo\",\n    \"createLimitExceeded\": \"Has alcanzado el límite máximo de espacio de trabajo permitido para su cuenta. Si necesita espacios de trabajo adicionales para continuar su trabajo, solicítelos en Github\",\n    \"deleteSuccess\": \"Espacio de trabajo eliminado correctamente\",\n    \"deleteFailed\": \"No se pudo eliminar el espacio de trabajo\",\n    \"openSuccess\": \"Espacio de trabajo abierto correctamente\",\n    \"openFailed\": \"No se pudo abrir el espacio de trabajo\",\n    \"renameSuccess\": \"Espacio de trabajo renombrado exitosamente\",\n    \"renameFailed\": \"No se pudo cambiar el nombre del espacio de trabajo\",\n    \"updateIconSuccess\": \"Icono de espacio de trabajo actualizado correctamente\",\n    \"updateIconFailed\": \"Fallo actualizando el icono del espacio de trabajo\",\n    \"cannotDeleteTheOnlyWorkspace\": \"No se puede eliminar el único espacio de trabajo\",\n    \"fetchWorkspacesFailed\": \"No se pudieron recuperar los espacios de trabajo\",\n    \"leaveCurrentWorkspace\": \"Salir del espacio de trabajo\",\n    \"leaveCurrentWorkspacePrompt\": \"¿Está seguro de que desea abandonar el espacio de trabajo actual?\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Compartir\",\n    \"workInProgress\": \"Próximamente\",\n    \"markdown\": \"Marcador\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"Copiar al portapapeles\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Copiar enlace\",\n    \"publish\": \"Publicar\",\n    \"publishTab\": \"Publicar\"\n  },\n  \"moreAction\": {\n    \"small\": \"pequeño\",\n    \"medium\": \"medio\",\n    \"large\": \"grande\",\n    \"fontSize\": \"Tamaño de fuente\",\n    \"import\": \"Importar\",\n    \"moreOptions\": \"Más opciones\",\n    \"wordCount\": \"El recuento de palabras: {}\",\n    \"charCount\": \"Número de caracteres : {}\",\n    \"createdAt\": \"Creado: {}\",\n    \"deleteView\": \"Borrar\",\n    \"duplicateView\": \"Duplicar\",\n    \"createdAtLabel\": \"Creado: \",\n    \"syncedAtLabel\": \"Sincronizado: \"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Texto y Markdown\",\n    \"documentFromV010\": \"Documento de v0.1.0\",\n    \"databaseFromV010\": \"Base de datos desde v0.1.0\",\n    \"csv\": \"CSV\",\n    \"database\": \"Base de datos\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Renombrar\",\n    \"delete\": \"Eliminar\",\n    \"duplicate\": \"Duplicar\",\n    \"unfavorite\": \"Quitar de los favoritos\",\n    \"favorite\": \"Añadir a los favoritos\",\n    \"openNewTab\": \"Abrir en una pestaña nueva\",\n    \"moveTo\": \"Mover a\",\n    \"addToFavorites\": \"Añadira los favoritos\",\n    \"copyLink\": \"Copiar Enlace\",\n    \"move\": \"Mover\"\n  },\n  \"blankPageTitle\": \"Página en blanco\",\n  \"newPageText\": \"Nueva página\",\n  \"newDocumentText\": \"Nuevo documento\",\n  \"newGridText\": \"Nueva patrón\",\n  \"newCalendarText\": \"Nuevo calendario\",\n  \"newBoardText\": \"Nuevo tablero\",\n  \"chat\": {\n    \"newChat\": \"Chat de IA\",\n    \"relatedQuestion\": \"Relacionado\",\n    \"serverUnavailable\": \"Servicio temporalmente no disponible. Por favor, inténtelo de nuevo más tarde.\",\n    \"aiServerUnavailable\": \"🌈 ¡Uh-oh! 🌈. Un unicornio se comió nuestra respuesta. ¡Por favor, intenta de nuevo!\",\n    \"retry\": \"Rever\",\n    \"clickToRetry\": \"Haga clic para volver a intentarlo\",\n    \"regenerateAnswer\": \"Regenerar\",\n    \"question1\": \"Cómo utilizar Kanban para gestionar tareas\",\n    \"question2\": \"Explica el método GTD\",\n    \"question3\": \"¿Por qué usar Rust?\",\n    \"aiMistakePrompt\": \"La IA puede cometer errores. Consulta información importante.\",\n    \"referenceSource\": {\n      \"one\": \"Se encontró {count} fuente\",\n      \"other\": \"Se encontraron {count} fuentes\"\n    },\n    \"regenerate\": \"Intentar otra vez\",\n    \"addToNewPage\": \"Crear nueva página\",\n    \"changeFormat\": {\n      \"textOnly\": \"Texto\",\n      \"text\": \"Párrafo\"\n    },\n    \"selectBanner\": {\n      \"saveButton\": \"Añadir …\"\n    }\n  },\n  \"trash\": {\n    \"text\": \"Papelera\",\n    \"restoreAll\": \"Recuperar todo\",\n    \"restore\": \"Restaurar\",\n    \"deleteAll\": \"Eliminar todo\",\n    \"pageHeader\": {\n      \"fileName\": \"Nombre de archivo\",\n      \"lastModified\": \"Última modificación\",\n      \"created\": \"Creado\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"¿Estás seguro de eliminar todas las páginas de la Papelera?\",\n      \"caption\": \"Esta acción no se puede deshacer.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"¿Estás seguro de restaurar todas las páginas en la Papelera?\",\n      \"caption\": \"Esta acción no se puede deshacer.\"\n    },\n    \"mobile\": {\n      \"actions\": \"Acciones de papelera\",\n      \"empty\": \"La papelera está vacío\",\n      \"emptyDescription\": \"No tienes ningún archivo eliminado\",\n      \"isDeleted\": \"esta eliminado\",\n      \"isRestored\": \"esta restaurado\"\n    },\n    \"confirmDeleteTitle\": \"¿Está seguro de que desea eliminar esta página de forma permanente?\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Esta página está en la Papelera\",\n    \"restore\": \"Recuperar página\",\n    \"deletePermanent\": \"Eliminar permanentemente\"\n  },\n  \"dialogCreatePageNameHint\": \"Nombre de página\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Atajos\",\n    \"whatsNew\": \"¿Qué hay de nuevo?\",\n    \"markdown\": \"Reducción\",\n    \"debug\": {\n      \"name\": \"Información de depuración\",\n      \"success\": \"¡Información copiada!\",\n      \"fail\": \"No fue posible copiar la información\"\n    },\n    \"feedback\": \"Comentario\",\n    \"help\": \"Ayuda y Soporte\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Eliminar, renombrar y más...\",\n    \"addPageTooltip\": \"Inserta una página\",\n    \"defaultNewPageName\": \"Sin Título\",\n    \"renameDialog\": \"Renombrar\",\n    \"pageNameSuffix\": \"Copiar\"\n  },\n  \"noPagesInside\": \"No hay páginas dentro\",\n  \"toolbar\": {\n    \"undo\": \"Deshacer\",\n    \"redo\": \"Rehacer\",\n    \"bold\": \"Negrita\",\n    \"italic\": \"Cursiva\",\n    \"underline\": \"Subrayado\",\n    \"strike\": \"Tachado\",\n    \"numList\": \"Lista numerada\",\n    \"bulletList\": \"Lista con viñetas\",\n    \"checkList\": \"Lista de verificación\",\n    \"inlineCode\": \"Código embebido\",\n    \"quote\": \"Cita\",\n    \"header\": \"Título\",\n    \"highlight\": \"Resaltado\",\n    \"color\": \"Color\",\n    \"addLink\": \"Añadir enlace\",\n    \"link\": \"Enlace\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Cambiar a modo Claro\",\n    \"darkMode\": \"Cambiar a modo Oscuro\",\n    \"openAsPage\": \"Abrir como una página\",\n    \"addNewRow\": \"Agregar una nueva fila\",\n    \"openMenu\": \"Haga clic para abrir el menú\",\n    \"dragRow\": \"Pulsación larga para reordenar la fila\",\n    \"viewDataBase\": \"Ver base de datos\",\n    \"referencePage\": \"Se hace referencia a este {nombre}\",\n    \"addBlockBelow\": \"Añadir un bloque a continuación\",\n    \"aiGenerate\": \"Generar\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Cerrar panel lateral\",\n    \"openSidebar\": \"Abrir panel lateral\",\n    \"personal\": \"Personal\",\n    \"private\": \"Privado\",\n    \"workspace\": \"Espacio de trabajo\",\n    \"favorites\": \"Favoritos\",\n    \"clickToHidePrivate\": \"Haz clic para ocultar el espacio privado\\nLas páginas que creaste aquí solo son visibles para ti\",\n    \"clickToHideWorkspace\": \"Haga clic para ocultar el espacio de trabajo\\nLas páginas que creaste aquí son visibles para todos los miembros\",\n    \"clickToHidePersonal\": \"Haga clic para ocultar la sección personal\",\n    \"clickToHideFavorites\": \"Haga clic para ocultar la sección de favoritos\",\n    \"addAPage\": \"Añadir una página\",\n    \"addAPageToPrivate\": \"Agregar una página al espacio privado\",\n    \"addAPageToWorkspace\": \"Agregar una página al espacio de trabajo\",\n    \"recent\": \"Reciente\",\n    \"today\": \"Hoy\",\n    \"thisWeek\": \"Esta semana\",\n    \"others\": \"Otros favoritos\",\n    \"justNow\": \"En este momento\",\n    \"lastViewed\": \"Visto por última vez\",\n    \"emptyRecent\": \"Sin documentos recientes\",\n    \"favoriteSpace\": \"Favoritos\",\n    \"RecentSpace\": \"Reciente\",\n    \"Spaces\": \"Espacios\",\n    \"aiImageResponseLimit\": \"Se ha quedado sin respuestas de imágenes de IA.\\n\\nVaya a Configuración -> Plan -> Haga clic en AI Max para obtener más respuestas de imágenes de IA\",\n    \"purchaseStorageSpace\": \"Comprar espacio de almacenamiento\",\n    \"purchaseAIResponse\": \"Compra \"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Nota exportada a Markdown\",\n      \"path\": \"Documentos/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Contactos\",\n    \"whatsHappening\": \"¿Qué está pasando esta semana?\",\n    \"addContact\": \"Agregar Contacto\",\n    \"editContact\": \"Editar Contacto\"\n  },\n  \"button\": {\n    \"ok\": \"OK\",\n    \"confirm\": \"Confirmar\",\n    \"done\": \"Hecho\",\n    \"cancel\": \"Cancelar\",\n    \"signIn\": \"Ingresar\",\n    \"signOut\": \"Salir\",\n    \"complete\": \"Completar\",\n    \"save\": \"Guardar\",\n    \"generate\": \"Generar\",\n    \"esc\": \"ESC\",\n    \"keep\": \"Mantener\",\n    \"tryAgain\": \"Intentar otra vez\",\n    \"discard\": \"Desechar\",\n    \"replace\": \"Reemplazar\",\n    \"insertBelow\": \"Insertar a continuación\",\n    \"insertAbove\": \"Insertar arriba\",\n    \"upload\": \"Subir\",\n    \"edit\": \"Editar\",\n    \"delete\": \"Borrar\",\n    \"copy\": \"Copiar\",\n    \"duplicate\": \"Duplicar\",\n    \"putback\": \"Volver\",\n    \"update\": \"Actualizar\",\n    \"share\": \"Compartir\",\n    \"removeFromFavorites\": \"Quitar de favoritos\",\n    \"removeFromRecent\": \"Eliminar de los recientes\",\n    \"addToFavorites\": \"Añadir a favoritos\",\n    \"rename\": \"Renombrar\",\n    \"helpCenter\": \"Centro de ayuda\",\n    \"add\": \"Añadir\",\n    \"yes\": \"Si\",\n    \"no\": \"No\",\n    \"clear\": \"Limpiar\",\n    \"remove\": \"Eliminar\",\n    \"dontRemove\": \"no quitar\",\n    \"copyLink\": \"Copiar enlace\",\n    \"align\": \"Alinear\",\n    \"login\": \"Inciar sessión\",\n    \"logout\": \"Cerrar sesión\",\n    \"deleteAccount\": \"Borrar cuenta\",\n    \"back\": \"Atrás\",\n    \"signInGoogle\": \"Inicia sesión con Google\",\n    \"signInGithub\": \"Iniciar sesión con Github\",\n    \"signInDiscord\": \"Iniciar sesión con discordia\",\n    \"more\": \"Más\",\n    \"create\": \"Crear\",\n    \"close\": \"Cerca\",\n    \"next\": \"Próximo\",\n    \"previous\": \"Anterior\",\n    \"submit\": \"Entregar\",\n    \"download\": \"Descargar\",\n    \"backToHome\": \"Volver a Inicio\"\n  },\n  \"label\": {\n    \"welcome\": \"¡Bienvenido!\",\n    \"firstName\": \"Primer nombre\",\n    \"middleName\": \"Segundo nombre\",\n    \"lastName\": \"Apellido\",\n    \"stepX\": \"Paso {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Imposible conectarse con sus credenciales.\",\n      \"failedMsg\": \"Por favor asegurese haber completado el proceso de ingreso en su navegador.\"\n    },\n    \"google\": {\n      \"title\": \"Ingresar con Google\",\n      \"instruction1\": \"Para importar sus contactos de Google, debe autorizar esta aplicación usando su navegador web.\",\n      \"instruction2\": \"Copie este código al presionar el icono o al seleccionar el texto:\",\n      \"instruction3\": \"Navege al siguiente enlace en su navegador web, e ingrese el código anterior:\",\n      \"instruction4\": \"Presione el botón de abajo cuando haya completado su registro:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Ajustes\",\n    \"popupMenuItem\": {\n      \"settings\": \"Ajustes\"\n    },\n    \"accountPage\": {\n      \"menuLabel\": \"Mi cuenta\",\n      \"title\": \"Mi cuenta\",\n      \"general\": {\n        \"title\": \"Nombre de cuenta e imagen de perfil\",\n        \"changeProfilePicture\": \"Cambiar\"\n      },\n      \"email\": {\n        \"title\": \"Email\",\n        \"actions\": {\n          \"change\": \"Cambiar email\"\n        }\n      },\n      \"login\": {\n        \"title\": \"Inicio de sesión en la cuenta\",\n        \"loginLabel\": \"Inicio de sesión\",\n        \"logoutLabel\": \"Cerrar sesión\"\n      },\n      \"description\": \"Personaliza tu perfil, administra la seguridad de la cuenta y las claves API de IA, o inicia sesión en tu cuenta.\"\n    },\n    \"menu\": {\n      \"appearance\": \"Apariencia\",\n      \"language\": \"Lenguaje\",\n      \"user\": \"Usuario\",\n      \"files\": \"archivos\",\n      \"notifications\": \"Notificaciones\",\n      \"open\": \"Abrir ajustes\",\n      \"logout\": \"Cerrar session\",\n      \"logoutPrompt\": \"¿Estás seguro de cerrar sesión?\",\n      \"selfEncryptionLogoutPrompt\": \"¿Está seguro de que quieres cerrar la sesión? Asegúrese de haber copiado el codigo de cifrado.\",\n      \"syncSetting\": \"Configuración de sincronización\",\n      \"cloudSettings\": \"Configuración de la nube\",\n      \"enableSync\": \"Habilitar sincronización\",\n      \"enableEncrypt\": \"Cifrar datos\",\n      \"cloudURL\": \"URL base\",\n      \"invalidCloudURLScheme\": \"Esquema no válido\",\n      \"cloudServerType\": \"servidor en la nube\",\n      \"cloudServerTypeTip\": \"Tenga en cuenta que es posible que se cierre la sesión de su cuenta actual después de cambiar el servidor en la nube.\",\n      \"cloudLocal\": \"Local\",\n      \"cloudAppFlowy\": \"Nube @:appName\",\n      \"cloudAppFlowySelfHost\": \"@:appName Cloud autohospedado\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"La URL de la nube no puede estar vacía\",\n      \"clickToCopy\": \"Haga clic para copiar\",\n      \"selfHostStart\": \"Si no tiene un servidor, consulte la\",\n      \"selfHostContent\": \"documento\",\n      \"selfHostEnd\": \"para obtener orientación sobre cómo autohospedar su propio servidor\",\n      \"cloudURLHint\": \"Ingrese la URL base de su servidor\",\n      \"cloudWSURL\": \"URL del conector web / websocket\",\n      \"cloudWSURLHint\": \"Ingrese la dirección websocket de su servidor\",\n      \"restartApp\": \"Reiniciar\",\n      \"restartAppTip\": \"Reinicie la aplicación para que se apliquen los cambios. Tenga en cuenta que esto podría cerrar la sesión de su cuenta actual.\",\n      \"changeServerTip\": \"Después de cambiar el servidor, debes hacer clic en el botón reiniciar para que los cambios surtan efecto\",\n      \"enableEncryptPrompt\": \"Activa el cifrado para proteger tus datos con esta clave. Guárdalo de forma segura; una vez habilitado, no se puede desactivar. Si se pierden, tus datos se vuelven irrecuperables. Haz clic para copiar\",\n      \"inputEncryptPrompt\": \"Introduzca su secreto de cifrado para\",\n      \"clickToCopySecret\": \"Haga clic para copiar el código secreto\",\n      \"configServerSetting\": \"Configure los ajustes de su servidor\",\n      \"configServerGuide\": \"Después de seleccionar \\\"Inicio rápido\\\", navega hasta \\\"Configuración\\\" y luego \\\"Configuración de la nube\\\" para configurar tu servidor autoalojado.\",\n      \"inputTextFieldHint\": \"Su código secreto\",\n      \"historicalUserList\": \"Historial de inicio de sesión del usuario\",\n      \"historicalUserListTooltip\": \"Esta lista muestra tus cuentas anónimas. Puedes hacer clic en una cuenta para ver sus detalles. Las cuentas anónimas se crean haciendo clic en el botón \\\"Comenzar\\\".\",\n      \"openHistoricalUser\": \"Haga clic para abrir la cuenta anónima\",\n      \"customPathPrompt\": \"Almacenar la carpeta de datos de @:appName en una carpeta sincronizada en la nube, como Google Drive, puede presentar riesgos. Si se accede a la base de datos dentro de esta carpeta o se modifica desde varias ubicaciones al mismo tiempo, se pueden producir conflictos de sincronización y posibles daños en los datos\",\n      \"importAppFlowyData\": \"Importar datos desde una carpeta externa de @:appName\",\n      \"importingAppFlowyDataTip\": \"La importación de datos está en curso. Por favor no cierres la aplicación.\",\n      \"importAppFlowyDataDescription\": \"Copia los datos de una carpeta de datos externa de @:appName e impórtalos a la carpeta de datos actual de @:appName\",\n      \"importSuccess\": \"Importó exitosamente la carpeta de datos de @:appName\",\n      \"importFailed\": \"Error al importar la carpeta de datos de @:appName\",\n      \"importGuide\": \"Para obtener más detalles, consulte el documento de referencia.\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Permitir notificaciones\",\n        \"hint\": \"Desactívelo para evitar que aparezcan notificaciones locales.\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"restaurar\",\n      \"fontFamily\": {\n        \"label\": \"Familia tipográfica\",\n        \"search\": \"Buscar\",\n        \"defaultFont\": \"Fuente predeterminada\"\n      },\n      \"themeMode\": {\n        \"label\": \"Theme Mode\",\n        \"light\": \"Modo Claro\",\n        \"dark\": \"Modo Oscuro\",\n        \"system\": \"Adapt to System\"\n      },\n      \"fontScaleFactor\": \"Factor de escala de fuente\",\n      \"documentSettings\": {\n        \"cursorColor\": \"Color del cursor del documento\",\n        \"selectionColor\": \"Color de selección de documento\",\n        \"hexEmptyError\": \"El color hexadecimal no puede estar vacío\",\n        \"hexLengthError\": \"El valor hexadecimal debe tener 6 dígitos\",\n        \"hexInvalidError\": \"Valor hexadecimal no válido\",\n        \"opacityEmptyError\": \"La opacidad no puede estar vacía\",\n        \"opacityRangeError\": \"La opacidad debe estar entre 1 y 100.\",\n        \"app\": \"Aplicación\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"Aplicar\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Dirección de diseño\",\n        \"hint\": \"Controla el flujo de contenido en tu pantalla, de izquierda a derecha o de derecha a izquierda.\",\n        \"ltr\": \"LTR (de izquierda hacia derecha)\",\n        \"rtl\": \"RTL  (de derecha hacia izquierda)\"\n      },\n      \"textDirection\": {\n        \"label\": \"Dirección de texto predeterminada\",\n        \"hint\": \"Especifica si el texto debe comenzar desde la izquierda o desde la derecha de forma predeterminada.\",\n        \"ltr\": \"LTR (de izquierda hacia derecha)\",\n        \"rtl\": \"RTL  (de derecha hacia izquierda)\",\n        \"auto\": \"AUTO\",\n        \"fallback\": \"Igual que la dirección del diseño\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Subir\",\n        \"uploadTheme\": \"Subir tema\",\n        \"description\": \"Cargue su propio tema @:appName usando el botón de abajo.\",\n        \"loading\": \"Espere mientras validamos y cargamos su tema...\",\n        \"uploadSuccess\": \"Su tema se ha subido con éxito\",\n        \"deletionFailure\": \"No se pudo eliminar el tema. Intenta eliminarlo manualmente.\",\n        \"filePickerDialogTitle\": \"Elija un archivo .flowy_plugin\",\n        \"urlUploadFailure\": \"No se pudo abrir la URL: {}\",\n        \"failure\": \"El tema que se cargó tenía un formato no válido.\"\n      },\n      \"theme\": \"Tema\",\n      \"builtInsLabel\": \"Temas incorporados\",\n      \"pluginsLabel\": \"Complementos\",\n      \"dateFormat\": {\n        \"label\": \"Formato de fecha\",\n        \"local\": \"Local\",\n        \"us\": \"US (Estados Unidos)\",\n        \"iso\": \"ISO \",\n        \"friendly\": \"Amigable\",\n        \"dmy\": \"D/M/A\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Formato de la hora\",\n        \"twelveHour\": \"doce horas\",\n        \"twentyFourHour\": \"veinticuatro horas\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"Mostrar diálogo de nombres al crear una página\",\n      \"enableRTLToolbarItems\": \"Habilitar elementos de la barra de herramientas RTL\",\n      \"members\": {\n        \"title\": \"Configuración de miembros\",\n        \"inviteMembers\": \"Invitar a los miembros\",\n        \"sendInvite\": \"Enviar invitación\",\n        \"copyInviteLink\": \"Copiar enlace de invitación\",\n        \"label\": \"Miembros\",\n        \"user\": \"Usuario\",\n        \"role\": \"Rol\",\n        \"removeFromWorkspace\": \"Quitar del espacio de trabajo\",\n        \"owner\": \"Dueño\",\n        \"guest\": \"Invitado\",\n        \"member\": \"Miembro\",\n        \"memberHintText\": \"Un miembro puede leer, comentar y editar páginas. Invitar a miembros e invitados.\",\n        \"emailInvalidError\": \"Correo electrónico no válido, compruébalo y vuelve a intentarlo.\",\n        \"emailSent\": \"Email enviado, por favor revisa la bandeja de entrada\",\n        \"members\": \"miembros\",\n        \"membersCount\": {\n          \"zero\": \"{} miembros\",\n          \"one\": \"{} miembro\",\n          \"other\": \"{} miembros\"\n        },\n        \"memberLimitExceeded\": \"Has alcanzado el límite máximo de miembros permitidos para tu cuenta. Si deseas agregar más miembros adicionales para continuar con tu trabajo, solicítalo en Github.\",\n        \"failedToAddMember\": \"No se pudo agregar el miembro\",\n        \"addMemberSuccess\": \"Miembro agregado con éxito\",\n        \"removeMember\": \"Eliminar miembro\",\n        \"areYouSureToRemoveMember\": \"¿Estás seguro de que deseas eliminar a este miembro?\",\n        \"inviteMemberSuccess\": \"La invitación ha sido enviada con éxito\",\n        \"failedToInviteMember\": \"No se pudo invitar al miembro\"\n      }\n    },\n    \"files\": {\n      \"copy\": \"Copiar\",\n      \"defaultLocation\": \"Leer archivos y ubicación de almacenamiento de datos\",\n      \"exportData\": \"Exporta tus datos\",\n      \"doubleTapToCopy\": \"Toca dos veces para copiar la ruta\",\n      \"restoreLocation\": \"Restaurar a la ruta predeterminada de @:appName\",\n      \"customizeLocation\": \"Abrir otra carpeta\",\n      \"restartApp\": \"Reinicie la aplicación para que los cambios surtan efecto.\",\n      \"exportDatabase\": \"Exportar base de datos\",\n      \"selectFiles\": \"Seleccione los archivos que necesitan ser exportados\",\n      \"selectAll\": \"Seleccionar todo\",\n      \"deselectAll\": \"Deseleccionar todo\",\n      \"createNewFolder\": \"Crear una nueva carpeta\",\n      \"createNewFolderDesc\": \"Dinos dónde quieres almacenar tus datos\",\n      \"defineWhereYourDataIsStored\": \"Defina dónde se almacenan sus datos\",\n      \"open\": \"Abierto\",\n      \"openFolder\": \"Abrir una carpeta existente\",\n      \"openFolderDesc\": \"Léalo y escríbalo en su carpeta @:appName existente\",\n      \"folderHintText\": \"nombre de la carpeta\",\n      \"location\": \"Creando una nueva carpeta\",\n      \"locationDesc\": \"Elija un nombre para su carpeta de datos de @:appName\",\n      \"browser\": \"Navegar\",\n      \"create\": \"Crear\",\n      \"set\": \"Colocar\",\n      \"folderPath\": \"Ruta para almacenar su carpeta\",\n      \"locationCannotBeEmpty\": \"La ruta no puede estar vacía\",\n      \"pathCopiedSnackbar\": \"¡La ruta de almacenamiento de archivos se copió al portapapeles!\",\n      \"changeLocationTooltips\": \"Cambiar el directorio de datos\",\n      \"change\": \"Cambiar\",\n      \"openLocationTooltips\": \"Abrir otro directorio de datos\",\n      \"openCurrentDataFolder\": \"Abrir el directorio de datos actual\",\n      \"recoverLocationTooltips\": \"Restablecer al directorio de datos predeterminado de @:appName\",\n      \"exportFileSuccess\": \"¡Exportar archivo con éxito!\",\n      \"exportFileFail\": \"¡Error en la exportación del archivo!\",\n      \"export\": \"Exportar\",\n      \"clearCache\": \"Limpiar caché\",\n      \"clearCacheDesc\": \"Si tienes problemas con las imágenes que no cargan o las fuentes no se muestran correctamente, intenta limpiar la caché. Esta acción no eliminará tus datos de usuario.\",\n      \"areYouSureToClearCache\": \"¿Estás seguro de limpiar el caché?\",\n      \"clearCacheSuccess\": \"¡Caché limpiada exitosamente!\"\n    },\n    \"user\": {\n      \"name\": \"Nombre\",\n      \"email\": \"Correo electrónico\",\n      \"tooltipSelectIcon\": \"Seleccionar icono\",\n      \"selectAnIcon\": \"Seleccione un icono\",\n      \"pleaseInputYourOpenAIKey\": \"por favor ingrese su clave AI\",\n      \"clickToLogout\": \"Haga clic para cerrar la sesión del usuario actual.\",\n      \"pleaseInputYourStabilityAIKey\": \"por favor ingrese su clave de estabilidad AI\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"Informacion personal\",\n      \"username\": \"Nombre de usuario\",\n      \"usernameEmptyError\": \"El nombre de usuario no puede estar vacío\",\n      \"about\": \"Acerca de\",\n      \"pushNotifications\": \"Notificaciones \",\n      \"support\": \"Soporte\",\n      \"joinDiscord\": \"Únete a nosotros en Discord\",\n      \"privacyPolicy\": \"política de privacidad\",\n      \"userAgreement\": \"Acuerdo del Usuario\",\n      \"termsAndConditions\": \"Términos y condiciones\",\n      \"userprofileError\": \"No se pudo cargar el perfil de usuario\",\n      \"userprofileErrorDescription\": \"Intente cerrar sesión y volver a iniciarla para comprobar si el problema persiste.\",\n      \"selectLayout\": \"Seleccionar diseño\",\n      \"selectStartingDay\": \"Seleccione el día de inicio\",\n      \"version\": \"Versión\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"Atajos\",\n      \"command\": \"Commando\",\n      \"keyBinding\": \"Atajos\",\n      \"addNewCommand\": \"Añadir nuevo comando\",\n      \"updateShortcutStep\": \"Presione la combinación de teclas deseada y presione ENTER\",\n      \"shortcutIsAlreadyUsed\": \"Este atajo ya se utiliza para: {conflict}\",\n      \"resetToDefault\": \"Restablecer los atajos predeterminados\",\n      \"couldNotLoadErrorMsg\": \"No se pudieron cargar los atajos. Inténtalo de nuevo.\",\n      \"couldNotSaveErrorMsg\": \"No se pudieron guardar los atajos. Inténtalo de nuevo.\",\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"Insertar un nuevo párrafo al lado del bloque de código\",\n        \"codeBlockIndentLines\": \"Insertar dos espacios al inicio de la línea en el bloque de código\",\n        \"codeBlockOutdentLines\": \"Eliminar dos espacios al inicio de la línea en el bloque de código\",\n        \"codeBlockAddTwoSpaces\": \"Insertar dos espacios en la posición del cursor en el bloque de código\",\n        \"codeBlockSelectAll\": \"Seleccionar todo el contenido dentro de un bloque de código\",\n        \"textAlignLeft\": \"Alinear texto a la izquierda\",\n        \"textAlignCenter\": \"Alinear el texto al centro\",\n        \"textAlignRight\": \"Alinear el texto a la derecha\"\n      }\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"¿Está seguro de que desea eliminar esta vista?\",\n    \"createView\": \"Nuevo\",\n    \"title\": {\n      \"placeholder\": \"Sin títutlo\"\n    },\n    \"settings\": {\n      \"filter\": \"Filtrar\",\n      \"sort\": \"Clasificar\",\n      \"sortBy\": \"Ordenar por\",\n      \"properties\": \"Propiedades\",\n      \"reorderPropertiesTooltip\": \"Arrastre para reordenar las propiedades\",\n      \"group\": \"Grupo\",\n      \"addFilter\": \"Añadir filtro\",\n      \"deleteFilter\": \"Eliminar filtro\",\n      \"filterBy\": \"Filtrado por...\",\n      \"typeAValue\": \"Escriba un valor...\",\n      \"layout\": \"Disposición\",\n      \"databaseLayout\": \"Disposición\",\n      \"editView\": \"Editar vista\",\n      \"boardSettings\": \"Configuración del tablero\",\n      \"calendarSettings\": \"Configuración del calendario\",\n      \"createView\": \"Nueva vista\",\n      \"duplicateView\": \"Duplicar vista\",\n      \"deleteView\": \"Eliminar vista\",\n      \"numberOfVisibleFields\": \"{} mostrado\",\n      \"Properties\": \"Propiedades\",\n      \"viewList\": \"Vistas de base de datos\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Contiene\",\n      \"doesNotContain\": \"No contiene\",\n      \"endsWith\": \"Termina con\",\n      \"startWith\": \"Comienza con\",\n      \"is\": \"Es\",\n      \"isNot\": \"No es\",\n      \"isEmpty\": \"Esta vacio\",\n      \"isNotEmpty\": \"No está vacío\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"No\",\n        \"startWith\": \"Comienza con\",\n        \"endWith\": \"Termina con\",\n        \"isEmpty\": \"esta vacio\",\n        \"isNotEmpty\": \"no está vacío\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Comprobado\",\n      \"isUnchecked\": \"Desenfrenado\",\n      \"choicechipPrefix\": {\n        \"is\": \"es\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"Esta completo\",\n      \"isIncomplted\": \"esta incompleto\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"Es\",\n      \"isNot\": \"No es\",\n      \"contains\": \"Contiene\",\n      \"doesNotContain\": \"No contiene\",\n      \"isEmpty\": \"Esta vacio\",\n      \"isNotEmpty\": \"No está vacío\"\n    },\n    \"dateFilter\": {\n      \"is\": \"Es\",\n      \"before\": \"es antes\",\n      \"after\": \"Es despues\",\n      \"onOrBefore\": \"Es en o antes\",\n      \"onOrAfter\": \"Es en o después\",\n      \"between\": \"Está entre\",\n      \"empty\": \"Esta vacio\",\n      \"notEmpty\": \"No está vacío\",\n      \"choicechipPrefix\": {\n        \"before\": \"Antes\",\n        \"after\": \"Después\",\n        \"onOrBefore\": \"En o antes\",\n        \"onOrAfter\": \"Sobre o después\",\n        \"isEmpty\": \"Está vacio\",\n        \"isNotEmpty\": \"No está vacío\"\n      }\n    },\n    \"numberFilter\": {\n      \"equal\": \"Es igual\",\n      \"notEqual\": \"No es igual\",\n      \"lessThan\": \"Es menor que\",\n      \"greaterThan\": \"Es mayor que\",\n      \"lessThanOrEqualTo\": \"Es menor o igual que\",\n      \"greaterThanOrEqualTo\": \"Es mayor o igual que\",\n      \"isEmpty\": \"Está vacío\",\n      \"isNotEmpty\": \"No está vacío\"\n    },\n    \"field\": {\n      \"hide\": \"Ocultar\",\n      \"show\": \"Mostrar\",\n      \"insertLeft\": \"Insertar a la Izquierda\",\n      \"insertRight\": \"Insertar a la Derecha\",\n      \"duplicate\": \"Duplicar\",\n      \"delete\": \"Eliminar\",\n      \"wrapCellContent\": \"Ajustar texto\",\n      \"clear\": \"Borrar celdas\",\n      \"textFieldName\": \"Texto\",\n      \"checkboxFieldName\": \"Casilla de verificación\",\n      \"dateFieldName\": \"Fecha\",\n      \"updatedAtFieldName\": \"Última hora de modificación\",\n      \"createdAtFieldName\": \"tiempo creado\",\n      \"numberFieldName\": \"Números\",\n      \"singleSelectFieldName\": \"Seleccionar\",\n      \"multiSelectFieldName\": \"Selección múltiple\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Lista de Verificación\",\n      \"relationFieldName\": \"Relación\",\n      \"numberFormat\": \"Formato numérico\",\n      \"dateFormat\": \"Formato de fecha\",\n      \"includeTime\": \"Incluir tiempo\",\n      \"isRange\": \"Fecha final\",\n      \"dateFormatFriendly\": \"Mes Día, Año\",\n      \"dateFormatISO\": \"Año-Mes-Día\",\n      \"dateFormatLocal\": \"Mes/Día/Año\",\n      \"dateFormatUS\": \"Año/Mes/Día\",\n      \"dateFormatDayMonthYear\": \"Día mes año\",\n      \"timeFormat\": \"Formato de tiempo\",\n      \"invalidTimeFormat\": \"Formato de tiempo inválido\",\n      \"timeFormatTwelveHour\": \"12 horas\",\n      \"timeFormatTwentyFourHour\": \"24 horas\",\n      \"clearDate\": \"Borrar fecha\",\n      \"dateTime\": \"Fecha y hora\",\n      \"startDateTime\": \"Fecha de inicio hora\",\n      \"endDateTime\": \"Fecha de finalización hora\",\n      \"failedToLoadDate\": \"No se pudo cargar el valor de la fecha\",\n      \"selectTime\": \"Seleccionar hora\",\n      \"selectDate\": \"Seleccione fecha\",\n      \"visibility\": \"Visibilidad\",\n      \"propertyType\": \"Tipo de propiedad\",\n      \"addSelectOption\": \"Añadir una opción\",\n      \"typeANewOption\": \"Escribe una nueva opción\",\n      \"optionTitle\": \"Opciones\",\n      \"addOption\": \"Añadir opción\",\n      \"editProperty\": \"Editar propiedad\",\n      \"newProperty\": \"Nueva propiedad\",\n      \"deleteFieldPromptMessage\": \"¿Está seguro? Esta propiedad será eliminada\",\n      \"clearFieldPromptMessage\": \"¿Estás seguro? Se vaciarán todas las celdas de esta columna.\",\n      \"newColumn\": \"Nueva columna\",\n      \"format\": \"Formato\",\n      \"reminderOnDateTooltip\": \"Esta celda tiene un recordatorio programado\",\n      \"optionAlreadyExist\": \"La opción ya existe\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Agregar un nuevo campo\",\n      \"fieldDragElementTooltip\": \"Haga clic para abrir el menú\",\n      \"showHiddenFields\": {\n        \"one\": \"Mostrar {count} campo oculto\",\n        \"many\": \"Mostrar {count} campos ocultos\",\n        \"other\": \"Mostrar {count} campos ocultos\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"Ocultar {count} campo oculto\",\n        \"many\": \"Ocultar {count} campos ocultos\",\n        \"other\": \"Ocultar {count} campos ocultos\"\n      }\n    },\n    \"sort\": {\n      \"ascending\": \"ascendente\",\n      \"descending\": \"Descendente\",\n      \"by\": \"Por\",\n      \"empty\": \"Sin ordenamiento activo\",\n      \"cannotFindCreatableField\": \"No se encuentra un campo adecuado para ordenar\",\n      \"deleteAllSorts\": \"Eliminar todos filtros\",\n      \"addSort\": \"Agregar clasificación\",\n      \"removeSorting\": \"¿Le gustaría eliminar la ordenación?\",\n      \"fieldInUse\": \"Ya estás ordenando por este campo\",\n      \"deleteSort\": \"Borrar ordenar\"\n    },\n    \"row\": {\n      \"duplicate\": \"Duplicar\",\n      \"delete\": \"Eliminar\",\n      \"titlePlaceholder\": \"Intitulado\",\n      \"textPlaceholder\": \"Vacío\",\n      \"copyProperty\": \"Propiedad copiada al portapapeles\",\n      \"count\": \"Contar\",\n      \"newRow\": \"Nueva fila\",\n      \"action\": \"Acción\",\n      \"add\": \"Haga clic en agregar a continuación\",\n      \"drag\": \"Arrastre para mover\",\n      \"dragAndClick\": \"Arrastra para mover, haz clic para abrir el menú\",\n      \"insertRecordAbove\": \"Insertar registro arriba\",\n      \"insertRecordBelow\": \"Insertar registro a continuación\"\n    },\n    \"selectOption\": {\n      \"create\": \"Crear\",\n      \"purpleColor\": \"Morado\",\n      \"pinkColor\": \"Rosa\",\n      \"lightPinkColor\": \"Rosa Claro\",\n      \"orangeColor\": \"Naranja\",\n      \"yellowColor\": \"Amarillo\",\n      \"limeColor\": \"Lima\",\n      \"greenColor\": \"Verde\",\n      \"aquaColor\": \"Agua\",\n      \"blueColor\": \"Azul\",\n      \"deleteTag\": \"Borrar etiqueta\",\n      \"colorPanelTitle\": \"Colores\",\n      \"panelTitle\": \"Selecciona una opción o crea una\",\n      \"searchOption\": \"Buscar una opción\",\n      \"searchOrCreateOption\": \"Buscar o crear una opción...\",\n      \"createNew\": \"Crear un nuevo\",\n      \"orSelectOne\": \"O seleccione una opción\",\n      \"typeANewOption\": \"Escribe una nueva opción\",\n      \"tagName\": \"Nombre de etiqueta\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"Descripción de la tarea\",\n      \"addNew\": \"Agregar un elemento\",\n      \"submitNewTask\": \"Crear\",\n      \"hideComplete\": \"Ocultar tareas completadas\",\n      \"showComplete\": \"Mostrar todas las tareas\"\n    },\n    \"url\": {\n      \"launch\": \"Abrir en el navegador\",\n      \"copy\": \"Copiar URL\",\n      \"textFieldHint\": \"Introduce una URL\"\n    },\n    \"relation\": {\n      \"relatedDatabasePlaceLabel\": \"Base de datos relacionada\",\n      \"relatedDatabasePlaceholder\": \"Ninguno\",\n      \"inRelatedDatabase\": \"En\",\n      \"rowSearchTextFieldPlaceholder\": \"Buscar\",\n      \"noDatabaseSelected\": \"No se seleccionó ninguna base de datos, seleccione una primero de la lista a continuación:\",\n      \"emptySearchResult\": \"No se encontraron registros\",\n      \"linkedRowListLabel\": \"{count} filas vinculadas\",\n      \"unlinkedRowListLabel\": \"Vincular otra fila\"\n    },\n    \"menuName\": \"Cuadrícula\",\n    \"referencedGridPrefix\": \"Vista de\",\n    \"calculate\": \"Calcular\",\n    \"calculationTypeLabel\": {\n      \"none\": \"Ninguno\",\n      \"average\": \"Promedio\",\n      \"max\": \"Max\",\n      \"median\": \"Media\",\n      \"min\": \"Min\",\n      \"sum\": \"Suma\",\n      \"count\": \"Contar\",\n      \"countEmpty\": \"Contar vacío\",\n      \"countEmptyShort\": \"VACÍO\",\n      \"countNonEmpty\": \"Contar no vacíos\",\n      \"countNonEmptyShort\": \"RELLENO\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"Documento\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Seleccione un tablero para vincular\",\n        \"createANewBoard\": \"Crear un nuevo tablero\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Seleccione una cuadrícula para vincular\",\n        \"createANewGrid\": \"Crear una nueva cuadrícula\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Seleccione un calendario para vincular\",\n        \"createANewCalendar\": \"Crear un nuevo Calendario\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"Seleccione un documento para vincularlo\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Describir\",\n      \"codeBlock\": \"Bloque de código\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Tablero referenciado\",\n      \"referencedGrid\": \"Cuadrícula referenciada\",\n      \"referencedCalendar\": \"Calendario referenciado\",\n      \"referencedDocument\": \"Documento referenciado\",\n      \"autoGeneratorMenuItemName\": \"Escritor de AI\",\n      \"autoGeneratorTitleName\": \"AI: Pídele a AI que escriba cualquier cosa...\",\n      \"autoGeneratorLearnMore\": \"Aprende más\",\n      \"autoGeneratorGenerate\": \"Generar\",\n      \"autoGeneratorHintText\": \"Pregúntale a AI...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"No puedo obtener la clave de AI\",\n      \"autoGeneratorRewrite\": \"Volver a escribir\",\n      \"smartEdit\": \"Asistentes de IA\",\n      \"smartEditFixSpelling\": \"Corregir ortografía\",\n      \"warning\": \"⚠️ Las respuestas de la IA pueden ser inexactas o engañosas.\",\n      \"smartEditSummarize\": \"Resumir\",\n      \"smartEditImproveWriting\": \"Mejorar la escritura\",\n      \"smartEditMakeLonger\": \"hacer más largo\",\n      \"smartEditCouldNotFetchResult\": \"No se pudo obtener el resultado de AI\",\n      \"smartEditCouldNotFetchKey\": \"No se pudo obtener la clave de AI\",\n      \"smartEditDisabled\": \"Conectar AI en Configuración\",\n      \"discardResponse\": \"¿Quieres descartar las respuestas de IA?\",\n      \"createInlineMathEquation\": \"Crear ecuación\",\n      \"fonts\": \"Tipo de letra\",\n      \"insertDate\": \"Insertar fecha\",\n      \"emoji\": \"Emoji\",\n      \"toggleList\": \"Alternar lista\",\n      \"quoteList\": \"Lista de citas\",\n      \"numberedList\": \"lista numerada\",\n      \"bulletedList\": \"Lista con viñetas\",\n      \"todoList\": \"Lista de tareas\",\n      \"callout\": \"Callout\",\n      \"cover\": {\n        \"changeCover\": \"Cubierta de cambio\",\n        \"colors\": \"Colores\",\n        \"images\": \"Imágenes\",\n        \"clearAll\": \"Limpiar todo\",\n        \"abstract\": \"Abstracto\",\n        \"addCover\": \"Agregar portada\",\n        \"addLocalImage\": \"Agregar imagen local\",\n        \"invalidImageUrl\": \"URL de imagen no válida\",\n        \"failedToAddImageToGallery\": \"No se pudo agregar la imagen a la galería\",\n        \"enterImageUrl\": \"Introduce la URL de la imagen\",\n        \"add\": \"Agregar\",\n        \"back\": \"Atrás\",\n        \"saveToGallery\": \"Guardar en la galería\",\n        \"removeIcon\": \"Eliminar icono\",\n        \"pasteImageUrl\": \"Pegar URL de imagen\",\n        \"or\": \"O\",\n        \"pickFromFiles\": \"Seleccionar de archivos\",\n        \"couldNotFetchImage\": \"No se pudo obtener la imagen\",\n        \"imageSavingFailed\": \"Error al guardar la imagen\",\n        \"addIcon\": \"Añadir icono\",\n        \"changeIcon\": \"Cambiar el ícono\",\n        \"coverRemoveAlert\": \"Se eliminará de la portada después de que se elimine.\",\n        \"alertDialogConfirmation\": \"¿Estás seguro de que quieres continuar?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"Ecuación matemática\",\n        \"addMathEquation\": \"Agregar ecuación matemática\",\n        \"editMathEquation\": \"Editar ecuación matemática\"\n      },\n      \"optionAction\": {\n        \"click\": \"Hacer clic\",\n        \"toOpenMenu\": \" para abrir menú\",\n        \"delete\": \"Borrar\",\n        \"duplicate\": \"Duplicar\",\n        \"turnInto\": \"convertir en\",\n        \"moveUp\": \"Ascender\",\n        \"moveDown\": \"Mover hacia abajo\",\n        \"color\": \"Color\",\n        \"align\": \"Alinear\",\n        \"left\": \"Izquierda\",\n        \"center\": \"Centro\",\n        \"right\": \"Bien\",\n        \"defaultColor\": \"Por defecto\",\n        \"depth\": \"Profundidad\"\n      },\n      \"image\": {\n        \"addAnImage\": \"Añadir una imagen\",\n        \"copiedToPasteBoard\": \"El enlace de la imagen se ha copiado en el portapapeles.\",\n        \"imageUploadFailed\": \"Error al subir la imagen\",\n        \"errorCode\": \"Código de error\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"El enlace ha sido copiado al portapapeles.\",\n        \"convertToLink\": \"Convertir en enlace incrustado\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Agregue encabezados para crear una tabla de contenido.\",\n        \"noMatchHeadings\": \"No se han encontrado títulos coincidentes.\"\n      },\n      \"table\": {\n        \"addAfter\": \"Agregar después\",\n        \"addBefore\": \"Añadir antes\",\n        \"delete\": \"Borrar\",\n        \"clear\": \"Borrar contenido\",\n        \"duplicate\": \"Duplicar\",\n        \"bgColor\": \"Color de fondo\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Copiar\",\n        \"cut\": \"Cortar\",\n        \"paste\": \"Pegar\"\n      },\n      \"action\": \"Comportamiento\",\n      \"database\": {\n        \"selectDataSource\": \"Seleccionar fuente de datos\",\n        \"noDataSource\": \"No hay fuente de datos\",\n        \"selectADataSource\": \"Seleccione una fuente de datos\",\n        \"toContinue\": \"continuar\",\n        \"newDatabase\": \"Nueva base de datos\",\n        \"linkToDatabase\": \"Enlace a la base de datos\"\n      },\n      \"date\": \"Fecha\",\n      \"openAI\": \"IA abierta\"\n    },\n    \"outlineBlock\": {\n      \"placeholder\": \"Tabla de contenidos\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Escriba '/' para comandos\"\n    },\n    \"title\": {\n      \"placeholder\": \"Intitulado\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Haga clic para agregar imagen\",\n      \"upload\": {\n        \"label\": \"Subir\",\n        \"placeholder\": \"Haga clic para cargar la imagen\"\n      },\n      \"url\": {\n        \"label\": \"URL de la imagen\",\n        \"placeholder\": \"Introduce la URL de la imagen\"\n      },\n      \"ai\": {\n        \"label\": \"Generar imagen desde AI\",\n        \"placeholder\": \"Ingrese el prompt para que AI genere una imagen\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Generar imagen desde Stability AI\",\n        \"placeholder\": \"Ingrese el prompt para que Stability AI genere una imagen\"\n      },\n      \"support\": \"El límite de tamaño de la imagen es de 5 MB. Formatos admitidos: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Imagen inválida\",\n        \"invalidImageSize\": \"El tamaño de la imagen debe ser inferior a 5 MB\",\n        \"invalidImageFormat\": \"El formato de imagen no es compatible. Formatos admitidos: JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"URL de imagen no válida\",\n        \"noImage\": \"El fichero o directorio no existe\"\n      },\n      \"embedLink\": {\n        \"label\": \"Insertar enlace\",\n        \"placeholder\": \"Pega o escribe el enlace de una imagen\"\n      },\n      \"unsplash\": {\n        \"label\": \"Desempaquetar\"\n      },\n      \"searchForAnImage\": \"Buscar una imagen\",\n      \"pleaseInputYourOpenAIKey\": \"ingresa tu clave AI en la página de Configuración\",\n      \"saveImageToGallery\": \"Guardar imagen\",\n      \"failedToAddImageToGallery\": \"No se pudo agregar la imagen a la galería\",\n      \"successToAddImageToGallery\": \"Imagen agregada a la galería con éxito\",\n      \"unableToLoadImage\": \"No se puede cargar la imagen\",\n      \"maximumImageSize\": \"El tamaño máximo de imagen es de 10 MB\",\n      \"uploadImageErrorImageSizeTooBig\": \"El tamaño de la imagen debe ser inferior a 10 MB.\",\n      \"imageIsUploading\": \"La imagen se está subiendo\",\n      \"pleaseInputYourStabilityAIKey\": \"ingresa tu clave de Stability AI en la página de configuración\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Idioma\",\n        \"placeholder\": \"Seleccione el idioma\",\n        \"auto\": \"Auto\"\n      },\n      \"copyTooltip\": \"Copiar el contenido del bloque de código.\",\n      \"searchLanguageHint\": \"Buscar un idioma\",\n      \"codeCopiedSnackbar\": \"¡Código copiado al portapapeles!\"\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Pegar o escribir un enlace\",\n      \"openInNewTab\": \"Abrir en una pestaña nueva\",\n      \"copyLink\": \"Copiar link\",\n      \"removeLink\": \"Remover enlace\",\n      \"url\": {\n        \"label\": \"URL del enlace\",\n        \"placeholder\": \"Introduzca la URL del enlace\"\n      },\n      \"title\": {\n        \"label\": \"Título del enlace\",\n        \"placeholder\": \"Introduce el título del enlace\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"Menciona una persona, una página o fecha...\",\n      \"page\": {\n        \"label\": \"Enlace a la página\",\n        \"tooltip\": \"Haga clic para abrir la página\"\n      },\n      \"deleted\": \"Eliminado\",\n      \"deletedContent\": \"Este contenido no existe o ha sido eliminado.\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"Restablecer a los predeterminados\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"La versión actual no admite este bloque.\",\n      \"blockContentHasBeenCopied\": \"El contenido del bloque ha sido copiado.\"\n    },\n    \"mobilePageSelector\": {\n      \"title\": \"Seleccionar página\",\n      \"failedToLoad\": \"No se pudo cargar la lista de páginas\",\n      \"noPagesFound\": \"No se encontraron páginas\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"Nuevo\",\n      \"renameGroupTooltip\": \"Presione para cambiar el nombre del grupo\",\n      \"createNewColumn\": \"Agregar un nuevo grupo\",\n      \"addToColumnTopTooltip\": \"Añade una nueva tarjeta en la parte superior\",\n      \"addToColumnBottomTooltip\": \"Añade una nueva tarjeta en la parte inferior.\",\n      \"renameColumn\": \"Renombrar\",\n      \"hideColumn\": \"Ocultar\",\n      \"newGroup\": \"Nuevo grupo\",\n      \"deleteColumn\": \"Borrar\",\n      \"deleteColumnConfirmation\": \"Esto eliminará este grupo y todas las tarjetas que contiene.\\n¿Estás seguro de que quieres continuar?\",\n      \"groupActions\": \"Acciones grupales\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"Grupos ocultos\",\n      \"collapseTooltip\": \"Ocultar los grupos ocultos\",\n      \"expandTooltip\": \"Ver los grupos ocultos\"\n    },\n    \"cardDetail\": \"Detalle de la tarjeta\",\n    \"cardActions\": \"Acciones de tarjeta\",\n    \"cardDuplicated\": \"La tarjeta ha sido duplicada.\",\n    \"cardDeleted\": \"La tarjeta ha sido eliminada\",\n    \"showOnCard\": \"Mostrar en el detalle de la tarjeta\",\n    \"setting\": \"Configuración\",\n    \"propertyName\": \"Nombre de la propiedad\",\n    \"menuName\": \"Junta\",\n    \"showUngrouped\": \"Mostrar elementos desagrupados\",\n    \"ungroupedButtonText\": \"Desagrupados\",\n    \"ungroupedButtonTooltip\": \"Contiene tarjetas que no pertenecen a ningún grupo.\",\n    \"ungroupedItemsTitle\": \"Haga clic para agregar al tablero\",\n    \"groupBy\": \"Agrupar por\",\n    \"referencedBoardPrefix\": \"Vista de\",\n    \"notesTooltip\": \"Notas en el interior\",\n    \"mobile\": {\n      \"editURL\": \"Editar URL\",\n      \"showGroup\": \"Mostrar grupo\",\n      \"showGroupContent\": \"¿Estás seguro de que quieres mostrar este grupo en el tablero?\",\n      \"failedToLoad\": \"No se pudo cargar la vista del tablero\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Calendario\",\n    \"defaultNewCalendarTitle\": \"Intitulado\",\n    \"newEventButtonTooltip\": \"Agregar un nuevo evento\",\n    \"navigation\": {\n      \"today\": \"Hoy\",\n      \"jumpToday\": \"Saltar a hoy\",\n      \"previousMonth\": \"Mes anterior\",\n      \"nextMonth\": \"Próximo mes\"\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"No hay eventos\",\n      \"emptyBody\": \"Presiona el botón más para crear un evento en este día.\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Mostrar números de semana\",\n      \"showWeekends\": \"Mostrar fines de semana\",\n      \"firstDayOfWeek\": \"Empieza la semana en\",\n      \"layoutDateField\": \"Diseño de calendario por\",\n      \"changeLayoutDateField\": \"Cambiar campo de diseño\",\n      \"noDateTitle\": \"Sin cita\",\n      \"unscheduledEventsTitle\": \"Eventos no programados\",\n      \"clickToAdd\": \"Haga clic para agregar al calendario\",\n      \"name\": \"Diseño de calendario\",\n      \"noDateHint\": \"Los eventos no programados se mostrarán aquí\"\n    },\n    \"referencedCalendarPrefix\": \"Vista de\",\n    \"quickJumpYear\": \"Ir a\",\n    \"duplicateEvent\": \"duplicar evento\"\n  },\n  \"errorDialog\": {\n    \"title\": \"Error de flujo de aplicación\",\n    \"howToFixFallback\": \"¡Lamentamos las molestias! Envíe un problema en nuestra página de GitHub que describa su error.\",\n    \"github\": \"Ver en GitHub\"\n  },\n  \"search\": {\n    \"label\": \"Buscar\",\n    \"placeholder\": {\n      \"actions\": \"Acciones de búsqueda...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"¡Copiado!\",\n      \"fail\": \"no se puede copiar\"\n    }\n  },\n  \"unSupportBlock\": \"La versión actual no es compatible con este bloque.\",\n  \"views\": {\n    \"deleteContentTitle\": \"¿Está seguro de que desea eliminar {pageType}?\",\n    \"deleteContentCaption\": \"si elimina este {pageType}, puede restaurarlo de la papelera.\"\n  },\n  \"colors\": {\n    \"custom\": \"Costumbre\",\n    \"default\": \"Por defecto\",\n    \"red\": \"Rojo\",\n    \"orange\": \"Naranja\",\n    \"yellow\": \"Amarillo\",\n    \"green\": \"Verde\",\n    \"blue\": \"Azul\",\n    \"purple\": \"Púrpura\",\n    \"pink\": \"Rosa\",\n    \"brown\": \"Marrón\",\n    \"gray\": \"Gris\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"emojis\",\n    \"search\": \"buscar emojis\",\n    \"noRecent\": \"Ningún emoji reciente\",\n    \"noEmojiFound\": \"No se encontraron emojis\",\n    \"filter\": \"Filtrar\",\n    \"random\": \"Aleatorio\",\n    \"selectSkinTone\": \"Selecciona el tono de piel\",\n    \"remove\": \"Quitar emojis\",\n    \"categories\": {\n      \"smileys\": \"Emoticonos y emociones\",\n      \"people\": \"Personas y cuerpo\",\n      \"animals\": \"Animales y naturaleza\",\n      \"food\": \"Comida y bebida\",\n      \"activities\": \"Actividades\",\n      \"places\": \"Viajes y lugares\",\n      \"objects\": \"Objetos\",\n      \"symbols\": \"Símbolos\",\n      \"flags\": \"Banderas\",\n      \"nature\": \"Naturaleza\",\n      \"frequentlyUsed\": \"Usado frecuentemente\"\n    },\n    \"skinTone\": {\n      \"default\": \"Por defecto\",\n      \"light\": \"Luz\",\n      \"mediumLight\": \"Luz media\",\n      \"medium\": \"Medio\",\n      \"mediumDark\": \"Medio-Oscuro\",\n      \"dark\": \"Oscuro\"\n    }\n  },\n  \"inlineActions\": {\n    \"noResults\": \"No hay resultados\",\n    \"recentPages\": \"Paginas recientes\",\n    \"pageReference\": \"Referencia de página\",\n    \"docReference\": \"Referencia de documento\",\n    \"boardReference\": \"Referencia del tablero\",\n    \"calReference\": \"Referencia del calendario\",\n    \"gridReference\": \"Referencia de cuadrícula\",\n    \"date\": \"Fecha\",\n    \"reminder\": {\n      \"groupTitle\": \"Recordatorio\",\n      \"shortKeyword\": \"recordar\"\n    }\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"Cambiar el formato de fecha y hora en la configuración\",\n    \"dateFormat\": \"Formato de fecha\",\n    \"includeTime\": \"incluir tiempo\",\n    \"isRange\": \"Fecha final\",\n    \"timeFormat\": \"Formato de tiempo\",\n    \"clearDate\": \"Borrar fecha\",\n    \"reminderLabel\": \"Recordatorio\",\n    \"selectReminder\": \"Seleccionar recordatorio\",\n    \"reminderOptions\": {\n      \"none\": \"Ninguno\",\n      \"atTimeOfEvent\": \"Hora del evento\",\n      \"fiveMinsBefore\": \"5 minutos antes\",\n      \"tenMinsBefore\": \"10 minutos antes\",\n      \"fifteenMinsBefore\": \"15 minutos antes\",\n      \"thirtyMinsBefore\": \"30 minutos antes\",\n      \"oneHourBefore\": \"1 hora antes\",\n      \"twoHoursBefore\": \"2 horas antes\",\n      \"onDayOfEvent\": \"El día del evento\",\n      \"oneDayBefore\": \"1 dia antes\",\n      \"twoDaysBefore\": \"2 dias antes\",\n      \"oneWeekBefore\": \"1 semana antes\",\n      \"custom\": \"Personalizado\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"Ayer\",\n    \"today\": \"Hoy\",\n    \"tomorrow\": \"Mañana\",\n    \"oneWeek\": \"1 semana\"\n  },\n  \"notificationHub\": {\n    \"title\": \"Notificaciones\",\n    \"mobile\": {\n      \"title\": \"Actualizaciones\"\n    },\n    \"emptyTitle\": \"¡Todo al día!\",\n    \"emptyBody\": \"No hay notificaciones ni acciones pendientes. Disfruta de la calma.\",\n    \"tabs\": {\n      \"inbox\": \"Bandeja de entrada\",\n      \"upcoming\": \"Próximo\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"marcar todo como leido\",\n      \"showAll\": \"Todo\",\n      \"showUnreads\": \"No leído\"\n    },\n    \"filters\": {\n      \"ascending\": \"Ascendente\",\n      \"descending\": \"Descendente\",\n      \"groupByDate\": \"Agrupar por fecha\",\n      \"showUnreadsOnly\": \"Mostrar solo no leídos\",\n      \"resetToDefault\": \"Restablecen a los predeterminados\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"Recordatorio\",\n    \"message\": \"¡Recuerda comprobar esto antes de que lo olvides!\",\n    \"tooltipDelete\": \"Borrar\",\n    \"tooltipMarkRead\": \"Marcar como leído\",\n    \"tooltipMarkUnread\": \"marcar como no leído\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"Encontrar\",\n    \"previousMatch\": \"Partido anterior\",\n    \"nextMatch\": \"Próximo partido\",\n    \"close\": \"Cerca\",\n    \"replace\": \"Reemplazar\",\n    \"replaceAll\": \"Reemplaza todo\",\n    \"noResult\": \"No hay resultados\",\n    \"caseSensitive\": \"Distingue mayúsculas y minúsculas\",\n    \"searchMore\": \"Busca para encontrar más resultados\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"Lo lamentamos\",\n    \"loadingViewError\": \"Estamos teniendo problemas para cargar esta vista. Verifica tu conexión a Internet, actualiza la aplicación y no dudes en comunicarte con el equipo si el problema continúa.\"\n  },\n  \"editor\": {\n    \"bold\": \"Negrita\",\n    \"bulletedList\": \"Lista con viñetas\",\n    \"bulletedListShortForm\": \"Con viñetas\",\n    \"checkbox\": \"Checkbox\",\n    \"embedCode\": \"Código de inserción\",\n    \"heading1\": \"H1\",\n    \"heading2\": \"H2\",\n    \"heading3\": \"H3\",\n    \"highlight\": \"Destacar\",\n    \"color\": \"Color\",\n    \"image\": \"Imagen\",\n    \"date\": \"Fecha\",\n    \"page\": \"Página\",\n    \"italic\": \"Itálico\",\n    \"link\": \"Enlace\",\n    \"numberedList\": \"Lista numerada\",\n    \"numberedListShortForm\": \"Numerado\",\n    \"quote\": \"Cita\",\n    \"strikethrough\": \"Tachado\",\n    \"text\": \"Texto\",\n    \"underline\": \"Subrayar\",\n    \"fontColorDefault\": \"Por defecto\",\n    \"fontColorGray\": \"Gris\",\n    \"fontColorBrown\": \"Marrón\",\n    \"fontColorOrange\": \"Naranja\",\n    \"fontColorYellow\": \"Amarillo\",\n    \"fontColorGreen\": \"Verde\",\n    \"fontColorBlue\": \"Azul\",\n    \"fontColorPurple\": \"Púrpura\",\n    \"fontColorPink\": \"Rosa\",\n    \"fontColorRed\": \"Rojo\",\n    \"backgroundColorDefault\": \"Fondo predeterminado\",\n    \"backgroundColorGray\": \"fondo gris\",\n    \"backgroundColorBrown\": \"fondo marrón\",\n    \"backgroundColorOrange\": \"fondo naranja\",\n    \"backgroundColorYellow\": \"fondo amarillo\",\n    \"backgroundColorGreen\": \"fondo verde\",\n    \"backgroundColorBlue\": \"Fondo azul\",\n    \"backgroundColorPurple\": \"fondo morado\",\n    \"backgroundColorPink\": \"fondo rosa\",\n    \"backgroundColorRed\": \"fondo rojo\",\n    \"backgroundColorLime\": \"Fondo lima\",\n    \"backgroundColorAqua\": \"Fondo aguamarina\",\n    \"done\": \"Hecho\",\n    \"cancel\": \"Cancelar\",\n    \"tint1\": \"Tono 1\",\n    \"tint2\": \"Tono 2\",\n    \"tint3\": \"Tono 3\",\n    \"tint4\": \"Tono 4\",\n    \"tint5\": \"Tono 5\",\n    \"tint6\": \"Tono 6\",\n    \"tint7\": \"Tono 7\",\n    \"tint8\": \"Tono 8\",\n    \"tint9\": \"Tono 9\",\n    \"lightLightTint1\": \"Morado\",\n    \"lightLightTint2\": \"Rosa\",\n    \"lightLightTint3\": \"Rosa claro\",\n    \"lightLightTint4\": \"Naranja\",\n    \"lightLightTint5\": \"Amarillo\",\n    \"lightLightTint6\": \"Lima\",\n    \"lightLightTint7\": \"Verde\",\n    \"lightLightTint8\": \"Aqua\",\n    \"lightLightTint9\": \"Azul\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"Encabezado 1\",\n    \"mobileHeading2\": \"Encabezado 2\",\n    \"mobileHeading3\": \"Encabezado 3\",\n    \"textColor\": \"Color de texto\",\n    \"backgroundColor\": \"Color de fondo\",\n    \"addYourLink\": \"Añadir enlace\",\n    \"openLink\": \"Abrir enlace\",\n    \"copyLink\": \"Copiar enlace\",\n    \"removeLink\": \"Quitar enlace\",\n    \"editLink\": \"Editar enlace\",\n    \"linkText\": \"Texto\",\n    \"linkTextHint\": \"Introduce un texto\",\n    \"linkAddressHint\": \"Introduce una URL\",\n    \"highlightColor\": \"Color de resaltado\",\n    \"clearHighlightColor\": \"Quitar color de resaltado\",\n    \"customColor\": \"Color personalizado\",\n    \"hexValue\": \"Valor Hex\",\n    \"opacity\": \"Transparencia\",\n    \"resetToDefaultColor\": \"Reestablecer color predeterminado\",\n    \"ltr\": \"LTR\",\n    \"rtl\": \"RTL\",\n    \"auto\": \"Auto\",\n    \"cut\": \"Cortar\",\n    \"copy\": \"Copiar\",\n    \"paste\": \"Pegar\",\n    \"find\": \"Buscar\",\n    \"select\": \"Seleccionar\",\n    \"selectAll\": \"Seleccionar todo\",\n    \"previousMatch\": \"Resultado anterior\",\n    \"nextMatch\": \"Siguiente resultado\",\n    \"closeFind\": \"Cerrar\",\n    \"replace\": \"Reemplazar\",\n    \"replaceAll\": \"Reemplazar todo\",\n    \"regex\": \"Expresión regular\",\n    \"caseSensitive\": \"Distingue mayúsculas y minúsculas\",\n    \"uploadImage\": \"Subir imagen\",\n    \"urlImage\": \"URL de la Imagen\",\n    \"incorrectLink\": \"Enlace incorrecto\",\n    \"upload\": \"Subir\",\n    \"chooseImage\": \"Elige una imagen\",\n    \"loading\": \"Cargando\",\n    \"imageLoadFailed\": \"Error al subir la imagen\",\n    \"divider\": \"Divisor\",\n    \"table\": \"Tabla\",\n    \"colAddBefore\": \"Añadir antes\",\n    \"rowAddBefore\": \"Añadir antes\",\n    \"colAddAfter\": \"Añadir después\",\n    \"rowAddAfter\": \"Añadir después\",\n    \"colRemove\": \"Quitar\",\n    \"rowRemove\": \"Quitar\",\n    \"colDuplicate\": \"Duplicar\",\n    \"rowDuplicate\": \"Duplicar\",\n    \"colClear\": \"Borrar contenido\",\n    \"rowClear\": \"Borrar contenido\",\n    \"slashPlaceHolder\": \"Escribe '/' para insertar un bloque o comienza a escribir\",\n    \"typeSomething\": \"Escribe algo...\",\n    \"toggleListShortForm\": \"Alternar\",\n    \"quoteListShortForm\": \"Cita\",\n    \"mathEquationShortForm\": \"Fórmula\",\n    \"codeBlockShortForm\": \"Código\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"Ninguna página favorita\",\n    \"noFavoriteHintText\": \"Desliza la página hacia la izquierda para agregarla a tus favoritos\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"Escribe una / para insertar un bloque o comienza a escribir\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"Por hacer\",\n    \"bulletList\": \"Lista\",\n    \"numberList\": \"Lista\",\n    \"quote\": \"Cita\",\n    \"heading\": \"Título {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"Icono de página\",\n    \"language\": \"Idioma\",\n    \"font\": \"Fuente\",\n    \"actions\": \"Acciones\",\n    \"date\": \"Fecha\",\n    \"addField\": \"Añadir campo\",\n    \"userIcon\": \"Icono de usuario\"\n  },\n  \"noLogFiles\": \"No hay archivos de registro\",\n  \"newSettings\": {\n    \"myAccount\": {\n      \"title\": \"Mi cuenta\",\n      \"subtitle\": \"Personaliza tu perfil, administra la seguridad de la cuenta, abre claves IA o inicia sesión en tu cuenta.\",\n      \"profileLabel\": \"Nombre de cuenta e imagen de perfil\",\n      \"profileNamePlaceholder\": \"Introduce tu nombre\",\n      \"accountSecurity\": \"Seguridad de la cuenta\",\n      \"2FA\": \"Autenticación de 2 pasos\",\n      \"aiKeys\": \"Claves IA\",\n      \"accountLogin\": \"Inicio de sesión de la cuenta\",\n      \"updateNameError\": \"No se pudo actualizar el nombre\",\n      \"updateIconError\": \"No se pudo actualizar el ícono\",\n      \"deleteAccount\": {\n        \"title\": \"Borrar cuenta\",\n        \"subtitle\": \"Elimina permanentemente tu cuenta y todos tus datos.\",\n        \"deleteMyAccount\": \"Borrar mi cuenta\",\n        \"dialogTitle\": \"Borrar cuenta\",\n        \"dialogContent1\": \"¿Estás seguro de que deseas eliminar permanentemente tu cuenta?\",\n        \"dialogContent2\": \"Esta acción no se puede deshacer y eliminará el acceso a todos los espacios de equipo, borrará toda tu cuenta, incluidos los espacios de trabajo privados, y lo eliminará de todos los espacios de trabajo compartidos.\"\n      }\n    },\n    \"workplace\": {\n      \"name\": \"Espacio de trabajo\",\n      \"title\": \"Configuración del espacio de trabajo\",\n      \"subtitle\": \"Personaliza la apariencia, el tema, la fuente, el diseño del texto, la fecha, la hora y el idioma de tu espacio de trabajo.\",\n      \"workplaceName\": \"Nombre del espacio de trabajo\",\n      \"workplaceNamePlaceholder\": \"Introduce el nombre del espacio de trabajo\",\n      \"workplaceIcon\": \"Icono del espacio de trabajo\",\n      \"workplaceIconSubtitle\": \"Sube una imagen o usa un emoji para tu espacio de trabajo. El icono se mostrará en la barra lateral y en las notificaciones.\",\n      \"renameError\": \"Error al renombrar el espacio de trabajo\",\n      \"updateIconError\": \"Error al actualizar el ícono\",\n      \"appearance\": {\n        \"name\": \"Apariencia\",\n        \"themeMode\": {\n          \"auto\": \"Auto\",\n          \"light\": \"Claro\",\n          \"dark\": \"Oscuro\"\n        },\n        \"language\": \"Idioma\"\n      }\n    },\n    \"syncState\": {\n      \"syncing\": \"Sincronización\",\n      \"synced\": \"Sincronizado\",\n      \"noNetworkConnected\": \"Ninguna red conectada\"\n    }\n  },\n  \"pageStyle\": {\n    \"title\": \"Estilo de página\",\n    \"layout\": \"Disposición\",\n    \"coverImage\": \"Imagen de portada\",\n    \"pageIcon\": \"Icono de página\",\n    \"colors\": \"Colores\",\n    \"gradient\": \"Degradado\",\n    \"backgroundImage\": \"Imagen de fondo\",\n    \"presets\": \"Preajustes\",\n    \"photo\": \"Foto\",\n    \"unsplash\": \"Desempaquetar\",\n    \"pageCover\": \"Portada de página\",\n    \"none\": \"Ninguno\"\n  },\n  \"commandPalette\": {\n    \"placeholder\": \"Escribe para buscar vistas...\",\n    \"bestMatches\": \"Mejores resultados\",\n    \"recentHistory\": \"Historial reciente\",\n    \"navigateHint\": \"para navegar\",\n    \"loadingTooltip\": \"Buscando resultados...\",\n    \"betaLabel\": \"BETA\",\n    \"betaTooltip\": \"Actualmente solo admitimos la búsqueda de páginas.\",\n    \"fromTrashHint\": \"De la papelera\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/eu-ES.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Ni\",\n  \"welcomeText\": \"Ongietorri @:appName -ra\",\n  \"githubStarText\": \"Izarra GitHub-en\",\n  \"subscribeNewsletterText\": \"Harpidetu buletinera\",\n  \"letsGoButtonText\": \"Hasi\",\n  \"title\": \"Izenburua\",\n  \"youCanAlso\": \"Zuk ere egin dezakezu\",\n  \"and\": \"eta\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Egin klik behean gehitzeko\",\n    \"addAboveCmd\": \"Alt+klik\",\n    \"addAboveMacCmd\": \"Aukera+klik\",\n    \"addAboveTooltip\": \"goian gehitzeko\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Izena eman\",\n    \"title\": \"Izena eman @:appName -ra\",\n    \"getStartedText\": \"Hasi\",\n    \"emptyPasswordError\": \"Pasahitzak ezin du hutsik egon\",\n    \"repeatPasswordEmptyError\": \"Pasahitz errepikapenak ezin du hutsik egon\",\n    \"unmatchedPasswordError\": \"Pasahitz errepikapena ez da berdina\",\n    \"alreadyHaveAnAccount\": \"Kontu bat duzu jada?\",\n    \"emailHint\": \"Emaila\",\n    \"passwordHint\": \"Pasahitza\",\n    \"repeatPasswordHint\": \"Pasahitza errepikatu\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Hasi saioa @:appName -n\",\n    \"loginButtonText\": \"Hasi saioa\",\n    \"buttonText\": \"Sartu\",\n    \"forgotPassword\": \"Pasahitza ahaztu duzu?\",\n    \"emailHint\": \"Emaila\",\n    \"passwordHint\": \"Pasahitza\",\n    \"dontHaveAnAccount\": \"Ez daukazu konturik?\",\n    \"repeatPasswordEmptyError\": \"Pasahitz errepikapenak ezin du hutsik egon\",\n    \"unmatchedPasswordError\": \"Pasahitz errepikapena ez da berdina\",\n    \"loginAsGuestButtonText\": \"Hasi\"\n  },\n  \"workspace\": {\n    \"create\": \"Lan-eremua\",\n    \"hint\": \"lan-eremua\",\n    \"notFoundError\": \"Lan-eremurik ez da aurkitu\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Konpartitu\",\n    \"workInProgress\": \"Laister\",\n    \"markdown\": \"Markdown\",\n    \"copyLink\": \"Esteka kopiatu\"\n  },\n  \"moreAction\": {\n    \"small\": \"txikia\",\n    \"medium\": \"ertaina\",\n    \"large\": \"handia\",\n    \"fontSize\": \"Letra tamaina\",\n    \"import\": \"Inportatu\",\n    \"moreOptions\": \"Aukera gehiago\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Testua eta Markdown\",\n    \"documentFromV010\": \"v0.1.0 dokumentua\",\n    \"databaseFromV010\": \"0.1.0 bertsioko datu-basea\",\n    \"csv\": \"CSV\",\n    \"database\": \"Datu-basea\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Izena aldatu\",\n    \"delete\": \"Ezabatu\",\n    \"duplicate\": \"Duplikatu\",\n    \"openNewTab\": \"Ireki fitxa berri batean\"\n  },\n  \"blankPageTitle\": \"Orri zuria\",\n  \"newPageText\": \"Orri berria\",\n  \"trash\": {\n    \"text\": \"Zaborrontzia\",\n    \"restoreAll\": \"Guztia berreskuratu\",\n    \"deleteAll\": \"Guztia ezabatu\",\n    \"pageHeader\": {\n      \"fileName\": \"Fitxategi izena\",\n      \"lastModified\": \"Azken aldaketa\",\n      \"created\": \"Sortua\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Ziur zaborrontziko orrialde guztiak ezabatuko dituzula?\",\n      \"caption\": \"Ekintza hau ezin da desegin.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Ziur zaborrontziko orrialde guztiak leheneratuko dituzula?\",\n      \"caption\": \"Ekintza hau ezin da desegin.\"\n    }\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Orri hau zaborrontzian dago\",\n    \"restore\": \"Orria berreskuratu\",\n    \"deletePermanent\": \"Betirako ezabatu\"\n  },\n  \"dialogCreatePageNameHint\": \"Orriaren izena\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Lasterbideak\",\n    \"whatsNew\": \"Ze berri?\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"Debug informazioa\",\n      \"success\": \"Debug informazioa kopiatu da!\",\n      \"fail\": \"Ezin izan da debug informazioa kopiatu\"\n    },\n    \"feedback\": \"Iritzia\",\n    \"help\": \"Laguntza\"\n  },\n  \"menuAppHeader\": {\n    \"addPageTooltip\": \"Gehitu orri bat\",\n    \"defaultNewPageName\": \"Izenbururik ez\",\n    \"renameDialog\": \"Izena aldatu\"\n  },\n  \"toolbar\": {\n    \"undo\": \"Desegin\",\n    \"redo\": \"Berregin\",\n    \"bold\": \"Lodia\",\n    \"italic\": \"Etzana\",\n    \"underline\": \"Azpimarratua\",\n    \"strike\": \"Markatua\",\n    \"numList\": \"Zembakidun zerrenda\",\n    \"bulletList\": \"Buletetako zerrenda\",\n    \"checkList\": \"Egiaztapen zerrenda\",\n    \"inlineCode\": \"Lerroko kodea\",\n    \"quote\": \"Aipamena\",\n    \"header\": \"Goiburua\",\n    \"highlight\": \"Nabarmendu\",\n    \"color\": \"Kolorea\",\n    \"addLink\": \"Gehitu esteka\",\n    \"link\": \"Esteka\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Modu argira aldatu\",\n    \"darkMode\": \"Modu ilunera aldatu\",\n    \"openAsPage\": \"Orri gisa ireki\",\n    \"addNewRow\": \"Ilara berri bat gehitu\",\n    \"openMenu\": \"Egin klik menua irekitzeko\",\n    \"dragRow\": \"Luze sakatu errenkada berrantolatzeko\",\n    \"viewDataBase\": \"Ikusi datu-basea\",\n    \"referencePage\": \"{izena} honi erreferentzia egiten zaio\",\n    \"addBlockBelow\": \"Gehitu bloke bat behean\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Alboko barra itxi\",\n    \"openSidebar\": \"Alboko barra ireki\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Oharra markdownera esportatuta\",\n      \"path\": \"Documents/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Kontaktuak\",\n    \"whatsHappening\": \"Ze berri aste honetan?\",\n    \"addContact\": \"Kontaktua gehitu\",\n    \"editContact\": \"Kontaktua editatu\"\n  },\n  \"button\": {\n    \"ok\": \"OK\",\n    \"done\": \"Eginda\",\n    \"cancel\": \"Ezteztatu\",\n    \"signIn\": \"Saioa hasi\",\n    \"signOut\": \"Saioa itxi\",\n    \"complete\": \"Burututa\",\n    \"save\": \"Gorde\",\n    \"generate\": \"Sortu\",\n    \"esc\": \"ESC\",\n    \"keep\": \"Gorde\",\n    \"tryAgain\": \"Saiatu berriro\",\n    \"discard\": \"Baztertu\",\n    \"replace\": \"Ordezkatu\",\n    \"insertBelow\": \"Txertatu behean\",\n    \"upload\": \"Kargatu\",\n    \"edit\": \"Editatu\",\n    \"delete\": \"Ezabatu\",\n    \"duplicate\": \"Bikoiztu\",\n    \"putback\": \"Jarri Atzera\"\n  },\n  \"label\": {\n    \"welcome\": \"Ongi etorri!\",\n    \"firstName\": \"Izena\",\n    \"middleName\": \"Bigarren izena\",\n    \"lastName\": \"Abizena\",\n    \"stepX\": \"{X}. pausoa\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Ezin izan da kontura sartu.\",\n      \"failedMsg\": \"Mesedez, ziurtatu zure arakatzailean saioa hasteko prozesua amaitu duzula.\"\n    },\n    \"google\": {\n      \"title\": \"GOOGLE SAIOA HASI\",\n      \"instruction1\": \"Zure Google Kontaktuak inportatzeko, zure web arakatzailea erabiliz aplikazio hau baimendu beharko duzu.\",\n      \"instruction2\": \"Kopiatu kode hau ikonoan klik eginez edo testua hautatuz:\",\n      \"instruction3\": \"Nabigatu zure web arakatzailean esteka honetara eta idatzi goiko kodea:\",\n      \"instruction4\": \"Sakatu beheko botoia erregistroa amaitzean:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Ezarpenak\",\n    \"menu\": {\n      \"appearance\": \"Itxura\",\n      \"language\": \"Hizkuntza\",\n      \"user\": \"Erabiltzailea\",\n      \"files\": \"Fitxategiak\",\n      \"open\": \"Ezarpenak ireki\"\n    },\n    \"appearance\": {\n      \"fontFamily\": {\n        \"label\": \"Letra-tipoen familia\",\n        \"search\": \"Bilatu\"\n      },\n      \"themeMode\": {\n        \"label\": \"Itxura modua\",\n        \"light\": \"Modu argia\",\n        \"dark\": \"Modu iluna\",\n        \"system\": \"Zure sistemara moldatu\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Kargatu\",\n        \"description\": \"Kargatu zure @:appName gaia beheko botoia erabiliz.\",\n        \"loading\": \"Mesedez, itxaron zure gaia balioztatzen eta kargatzen dugun bitartean...\",\n        \"uploadSuccess\": \"Zure gaia behar bezala kargatu da\",\n        \"deletionFailure\": \"Ezin izan da gaia ezabatu. Saiatu eskuz ezabatzen.\",\n        \"filePickerDialogTitle\": \"Aukeratu .flowy_plugin fitxategi bat\",\n        \"urlUploadFailure\": \"Ezin izan da ireki URLa: {}\",\n        \"failure\": \"Kargatu zen gaiak formatu baliogabea zuen.\"\n      },\n      \"theme\": \"Itxura\",\n      \"builtInsLabel\": \"Eraikitako gaiak\",\n      \"pluginsLabel\": \"Pluginak\"\n    },\n    \"files\": {\n      \"copy\": \"Kopiatu\",\n      \"defaultLocation\": \"Non gordetzen diren zure datuak\",\n      \"exportData\": \"Esportatu zure datuak\",\n      \"doubleTapToCopy\": \"Sakatu birritan bidea kopiatzeko\",\n      \"restoreLocation\": \"Berrezarri @:appName-ren biden lehenetsira\",\n      \"customizeLocation\": \"Beste karpeta bat ireki\",\n      \"restartApp\": \"Mesedez, berrabiarazi aplikazioa aldaketak indarrean egon daitezen.\",\n      \"exportDatabase\": \"Datubasea exportatu\",\n      \"selectFiles\": \"Aukeratu exportatu nahi dituzun fitxategiak\",\n      \"selectAll\": \"Hautatu guztiak\",\n      \"deselectAll\": \"Deshautatu guztiak\",\n      \"createNewFolder\": \"Karpeta berri bat sortu\",\n      \"createNewFolderDesc\": \"Non nahi dituzu datuak gorde ...\",\n      \"defineWhereYourDataIsStored\": \"Zure datuak non gordetzen diren zehaztu\",\n      \"open\": \"Oreki\",\n      \"openFolder\": \"Ireki karpeta bat\",\n      \"openFolderDesc\": \"Irakurri eta idatzi zure @:appName karpetan...\",\n      \"folderHintText\": \"karpetaren izena\",\n      \"location\": \"Karpeta berria sortzen\",\n      \"locationDesc\": \"Aukeratu izen bat @:appName datuen karpetarako\",\n      \"browser\": \"Bilatu\",\n      \"create\": \"Sortu\",\n      \"set\": \"Ezarri\",\n      \"folderPath\": \"Zure karpeta gordetzeko bidea\",\n      \"locationCannotBeEmpty\": \"Bideak ezin du hutsa egon\",\n      \"pathCopiedSnackbar\": \"Fitxategiak biltegiratzeko bidea arbelean kopiatu da!\",\n      \"changeLocationTooltips\": \"Aldatu datuen direktorioa\",\n      \"change\": \"Aldatu\",\n      \"openLocationTooltips\": \"Ireki beste datu-direktorio bat\",\n      \"openCurrentDataFolder\": \"Ireki uneko datuen direktorioa\",\n      \"recoverLocationTooltips\": \"Berrezarri @:appNameren datu-direktorio lehenetsira\",\n      \"exportFileSuccess\": \"Esportatu fitxategia behar bezala!\",\n      \"exportFileFail\": \"Ezin izan da esportatu fitxategia!\",\n      \"export\": \"Esportatu\"\n    },\n    \"user\": {\n      \"name\": \"Izena\",\n      \"selectAnIcon\": \"Hautatu ikono bat\",\n      \"pleaseInputYourOpenAIKey\": \"mesedez sartu zure AI gakoa\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Ziur ikuspegi hau ezabatu nahi duzula?\",\n    \"createView\": \"Berria\",\n    \"settings\": {\n      \"filter\": \"Filtroa\",\n      \"sort\": \"Ordenatu\",\n      \"sortBy\": \"Ordenatu honekiko\",\n      \"properties\": \"Propietateak\",\n      \"reorderPropertiesTooltip\": \"Arrastatu propietateak berrantolatzeko\",\n      \"group\": \"Taldea\",\n      \"addFilter\": \"Gehitu iragazkia\",\n      \"deleteFilter\": \"Ezabatu iragazkia\",\n      \"filterBy\": \"Iragazi arabera...\",\n      \"typeAValue\": \"Idatzi balio bat...\",\n      \"layout\": \"Diseinua\",\n      \"databaseLayout\": \"Diseinua\",\n      \"Properties\": \"Propietateak\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Dauka\",\n      \"doesNotContain\": \"Ez dauka\",\n      \"endsWith\": \"Honez amaitzen da\",\n      \"startWith\": \"Honez hasten da\",\n      \"is\": \"da\",\n      \"isNot\": \"Ez da\",\n      \"isEmpty\": \"Hutsa dago\",\n      \"isNotEmpty\": \"Ez dago hutsik\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Ez da\",\n        \"startWith\": \"Honez hasten da\",\n        \"endWith\": \"Honez amaitzen da\",\n        \"isEmpty\": \"hutsik dago\",\n        \"isNotEmpty\": \"ez dago hutsik\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Egiaztatuta\",\n      \"isUnchecked\": \"Desmarkatua\",\n      \"choicechipPrefix\": {\n        \"is\": \"da\",\n        \"da\": \"da\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"osatu da\",\n      \"isIncomplted\": \"osatu gabe dago\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"da\",\n      \"isNot\": \"Ez da\",\n      \"contains\": \"Duen\",\n      \"doesNotContain\": \"Ez dauka\",\n      \"isEmpty\": \"Hutsa dago\",\n      \"isNotEmpty\": \"Ez dago hutsik\"\n    },\n    \"field\": {\n      \"hide\": \"Ezkutatu\",\n      \"insertLeft\": \"Txertatu ezkerrera\",\n      \"insertRight\": \"Txertatu eskuinera\",\n      \"duplicate\": \"Bikoiztu\",\n      \"delete\": \"Ezabatu\",\n      \"textFieldName\": \"Testua\",\n      \"checkboxFieldName\": \"Markatu laukia\",\n      \"dateFieldName\": \"Data\",\n      \"updatedAtFieldName\": \"Azken aldaketaren ordua\",\n      \"createdAtFieldName\": \"Sortutako denbora\",\n      \"numberFieldName\": \"Zenbakiak\",\n      \"singleSelectFieldName\": \"Hautatu\",\n      \"multiSelectFieldName\": \"Multi-hautaketa\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Kontrol zerrenda\",\n      \"numberFormat\": \"Zenbaki formatua\",\n      \"dateFormat\": \"Data formatua\",\n      \"includeTime\": \"Sartu ordua\",\n      \"dateFormatFriendly\": \"Hilabete Eguna, Urtea\",\n      \"dateFormatISO\": \"Urtea-Hilabetea-Eguna\",\n      \"dateFormatLocal\": \"Hilabetea/Eguna/Urtea\",\n      \"dateFormatUS\": \"Urtea/Hilabetea/Eguna\",\n      \"dateFormatDayMonthYear\": \"Eguna/Hilabetea/Urtea\",\n      \"timeFormat\": \"Denboraren formatua\",\n      \"invalidTimeFormat\": \"Formatu baliogabea\",\n      \"timeFormatTwelveHour\": \"12 ordu\",\n      \"timeFormatTwentyFourHour\": \"24 ordu\",\n      \"addSelectOption\": \"Gehitu aukera bat\",\n      \"optionTitle\": \"Aukerak\",\n      \"addOption\": \"Gehitu aukera\",\n      \"editProperty\": \"Editatu propietatea\",\n      \"newProperty\": \"Zutabe berria\",\n      \"deleteFieldPromptMessage\": \"Ziur al zaude? Propietate hau ezabatu egingo da\"\n    },\n    \"sort\": {\n      \"ascending\": \"Gorarantz\",\n      \"descending\": \"Jaisten\",\n      \"addSort\": \"Gehitu ordenatu\",\n      \"deleteSort\": \"Ezabatu ordena\"\n    },\n    \"row\": {\n      \"duplicate\": \"Bikoiztu\",\n      \"delete\": \"Ezabatu\",\n      \"textPlaceholder\": \"Hutsik\",\n      \"copyProperty\": \"Propietatea arbelean kopiatu da\",\n      \"count\": \"Kontatu\",\n      \"newRow\": \"Errenkada berria\",\n      \"action\": \"Ekintza\"\n    },\n    \"selectOption\": {\n      \"create\": \"Sortu\",\n      \"purpleColor\": \"Purple\",\n      \"pinkColor\": \"Rosa\",\n      \"lightPinkColor\": \"Arrosa argia\",\n      \"orangeColor\": \"Laranja\",\n      \"yellowColor\": \"Horia\",\n      \"limeColor\": \"Lima\",\n      \"greenColor\": \"Berdea\",\n      \"aquaColor\": \"Aqua\",\n      \"blueColor\": \"Urdina\",\n      \"deleteTag\": \"Ezabatu etiketa\",\n      \"colorPanelTitle\": \"Koloreak\",\n      \"panelTitle\": \"Hautatu aukera bat edo sortu bat\",\n      \"searchOption\": \"Aukera bat bilatu\"\n    },\n    \"checklist\": {\n      \"addNew\": \"Gehitu elementu bat\"\n    },\n    \"menuName\": \"Sareta\",\n    \"referencedGridPrefix\": \"-ren ikuspegia\"\n  },\n  \"document\": {\n    \"menuName\": \"Dokumentua\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00etan\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Hautatu estekatzeko taula bat\",\n        \"createANewBoard\": \"Sortu taula berri bat\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Hautatu estekatzeko sare bat\",\n        \"createANewGrid\": \"Sortu sare berri bat\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Hautatu estekatzeko egutegia\",\n        \"createANewCalendar\": \"Sortu egutegi berri bat\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Eskema\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Erreferentziazko Batzordea\",\n      \"referencedGrid\": \"Erreferentziazko Sarea\",\n      \"referencedCalendar\": \"Erreferentziazko Egutegia\",\n      \"autoGeneratorMenuItemName\": \"AI Writer\",\n      \"autoGeneratorTitleName\": \"AI: Eskatu AIri edozer idazteko...\",\n      \"autoGeneratorLearnMore\": \"Gehiago ikasi\",\n      \"autoGeneratorGenerate\": \"Sortu\",\n      \"autoGeneratorHintText\": \"Galdetu AI...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Ezin da lortu AI gakoa\",\n      \"autoGeneratorRewrite\": \"Berridatzi\",\n      \"smartEdit\": \"AI Laguntzaileak\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"Ortografia konpondu\",\n      \"warning\": \"⚠️ AI erantzunak okerrak edo engainagarriak izan daitezke.\",\n      \"smartEditSummarize\": \"Laburtu\",\n      \"smartEditImproveWriting\": \"Hobetu idazkera\",\n      \"smartEditMakeLonger\": \"Luzatu\",\n      \"smartEditCouldNotFetchResult\": \"Ezin izan da emaitzarik eskuratu AI-tik\",\n      \"smartEditCouldNotFetchKey\": \"Ezin izan da AI gakoa eskuratu\",\n      \"smartEditDisabled\": \"Konektatu AI Ezarpenetan\",\n      \"discardResponse\": \"AI erantzunak baztertu nahi dituzu?\",\n      \"createInlineMathEquation\": \"Sortu ekuazioa\",\n      \"toggleList\": \"Aldatu zerrenda\",\n      \"cover\": {\n        \"changeCover\": \"Aldatu Azala\",\n        \"colors\": \"Koloreak\",\n        \"images\": \"Irudiak\",\n        \"clearAll\": \"Garbitu guztiak\",\n        \"abstract\": \"Abstraktua\",\n        \"addCover\": \"Gehitu estalkia\",\n        \"addLocalImage\": \"Gehitu tokiko irudia\",\n        \"invalidImageUrl\": \"Irudiaren URL baliogabea\",\n        \"failedToAddImageToGallery\": \"Ezin izan da irudia galerian gehitu\",\n        \"enterImageUrl\": \"Sartu irudiaren URLa\",\n        \"add\": \"Gehitu\",\n        \"back\": \"Itzuli\",\n        \"saveToGallery\": \"Gorde galerian\",\n        \"removeIcon\": \"Kendu ikonoa\",\n        \"pasteImageUrl\": \"Itsatsi irudiaren URLa\",\n        \"or\": \"EDO\",\n        \"pickFromFiles\": \"Aukeratu fitxategietatik\",\n        \"couldNotFetchImage\": \"Ezin izan da irudia eskuratu\",\n        \"imageSavingFailed\": \"Irudiak gordetzean huts egin du\",\n        \"addIcon\": \"Gehitu ikonoa\",\n        \"coverRemoveAlert\": \"Ezabatu ondoren estalkitik kenduko da.\",\n        \"alertDialogConfirmation\": \"Ziur al zaude, jarraitu nahi duzula?\"\n      },\n      \"mathEquation\": {\n        \"addMathEquation\": \"Gehitu Matematika Ekuazioa\",\n        \"editMathEquation\": \"Editatu Matematika Ekuazioa\"\n      },\n      \"optionAction\": {\n        \"click\": \"Egin klik\",\n        \"toOpenMenu\": \" menua irekitzeko\",\n        \"delete\": \"Ezabatu\",\n        \"duplicate\": \"Bikoiztu\",\n        \"turnInto\": \"Bihurtu\",\n        \"moveUp\": \"Gora\",\n        \"moveDown\": \"Mugitu behera\",\n        \"color\": \"Kolore\",\n        \"align\": \"Lerrokatu\",\n        \"left\": \"Ezkerra\",\n        \"center\": \"Zentroa\",\n        \"right\": \"Eskuin\",\n        \"defaultColor\": \"Lehenetsia\"\n      },\n      \"image\": {\n        \"copiedToPasteBoard\": \"Irudiaren esteka arbelean kopiatu da\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Gehitu goiburuak aurkibidea sortzeko.\"\n      }\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Idatzi '/' komandoetarako\"\n    },\n    \"title\": {\n      \"placeholder\": \"Izenbururik gabe\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Egin klik irudia gehitzeko\",\n      \"upload\": {\n        \"label\": \"Kargatu\",\n        \"placeholder\": \"Egin klik irudia igotzeko\"\n      },\n      \"url\": {\n        \"label\": \"Irudiaren URLa\",\n        \"placeholder\": \"Sartu irudiaren URLa\"\n      },\n      \"support\": \"Irudiaren tamainaren muga 5 MB da. Onartutako formatuak: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Irudi baliogabea\",\n        \"invalidImageSize\": \"Irudiaren tamaina 5 MB baino txikiagoa izan behar da\",\n        \"invalidImageFormat\": \"Irudi formatua ez da onartzen. Onartutako formatuak: JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"Irudiaren URL baliogabea\"\n      }\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Hizkuntza\",\n        \"placeholder\": \"Hautatu hizkuntza\"\n      }\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Itsatsi edo idatzi esteka bat\",\n      \"url\": {\n        \"label\": \"Estekaren URLa\",\n        \"placeholder\": \"Sartu estekaren URLa\"\n      },\n      \"title\": {\n        \"label\": \"Estekaren izenburua\",\n        \"placeholder\": \"Sartu estekaren izenburua\"\n      }\n    },\n    \"data\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"Berria\"\n    },\n    \"menuName\": \"Kontseilua\",\n    \"referencedBoardPrefix\": \"-ren ikuspegia\",\n    \"mobile\": {\n      \"showGroup\": \"Erakutsi taldea\",\n      \"showGroupContent\": \"Ziur talde hau arbelean erakutsi nahi duzula?\",\n      \"failedToLoad\": \"Ezin izan da kargatu arbelaren ikuspegia\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Egutegia\",\n    \"defaultNewCalendarTitle\": \"Izenbururik gabe\",\n    \"navigation\": {\n      \"today\": \"Gaur\",\n      \"jumpToday\": \"Gaurko egunera salto egin\",\n      \"previousMonth\": \"Aurreko hilabetea\",\n      \"nextMonth\": \"Hurrengo hilabetea\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Erakutsi asteko zenbakiak\",\n      \"showWeekends\": \"Asteburuetan erakutsi\",\n      \"firstDayOfWeek\": \"Hasi astea\",\n      \"layoutDateField\": \"Egutegiaren diseinua arabera\",\n      \"noDateTitle\": \"Datarik ez\",\n      \"clickToAdd\": \"Egin klik egutegian gehitzeko\",\n      \"name\": \"Egutegiaren diseinua\",\n      \"noDateHint\": \"Programatu gabeko gertaerak hemen agertuko dira\"\n    },\n    \"referencedCalendarPrefix\": \"-ren ikuspegia\"\n  },\n  \"errorDialog\": {\n    \"title\": \"@:appName errorea\",\n    \"howToFixFallback\": \"Sentitzen dugu eragozpenak! Bidali zure errorea deskribatzen duen arazo bat gure GitHub orrian.\",\n    \"github\": \"Ikusi GitHub-en\"\n  },\n  \"search\": {\n    \"label\": \"Bilatu\",\n    \"placeholder\": {\n      \"actions\": \"Bilatu ekintzak...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Kopiatu!\",\n      \"fail\": \"Ezin da kopiatu\"\n    }\n  },\n  \"unSupportBlock\": \"Uneko bertsioak ez du bloke hau onartzen.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Ziur {pageType} ezabatu nahi duzula?\",\n    \"deleteContentCaption\": \"{pageType} hau ezabatzen baduzu, zaborrontzitik leheneratu dezakezu.\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/fa.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"من\",\n  \"welcomeText\": \"به @:appName خوش آمدید\",\n  \"welcomeTo\": \"خوش آمدید به\",\n  \"githubStarText\": \"به گیت‌هاب ما ستاره دهید\",\n  \"subscribeNewsletterText\": \"اشتراک در خبرنامه\",\n  \"letsGoButtonText\": \"شروع کنید\",\n  \"title\": \"عنوان\",\n  \"youCanAlso\": \"همچنین می‌توانید\",\n  \"and\": \"و\",\n  \"failedToOpenUrl\": \"خطا در بازکردن نشانی وب: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"برای افزودن در زیر کلیک کنید\",\n    \"addAboveCmd\": \"Alt+click\",\n    \"addAboveMacCmd\": \"Option+click\",\n    \"addAboveTooltip\": \"برای افزودن در بالا\",\n    \"dragTooltip\": \"برای حرکت بکشید\",\n    \"openMenuTooltip\": \"برای باز کردن منو کلیک کنید\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"ثبت نام\",\n    \"title\": \"ثبت نام در @:appName\",\n    \"getStartedText\": \"شروع کنید\",\n    \"emptyPasswordError\": \"رمز عبور نمی تواند خالی باشد\",\n    \"repeatPasswordEmptyError\": \"تکرار رمز عبور نمی‌تواند خالی باشد\",\n    \"unmatchedPasswordError\": \"تکرار رمز عبور مشابه رمز عبور نیست\",\n    \"alreadyHaveAnAccount\": \"از قبل حساب دارید؟\",\n    \"emailHint\": \"ایمیل\",\n    \"passwordHint\": \"رمز عبور\",\n    \"repeatPasswordHint\": \"تکرار رمز عبور\",\n    \"signUpWith\": \"ثبت نام با:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"ورود به @:appName\",\n    \"loginButtonText\": \"ورود\",\n    \"loginStartWithAnonymous\": \"ادامه دادن با یک جلسه ناشناس\",\n    \"continueAnonymousUser\": \"ادامه دادن به صورت کاربر مهمان\",\n    \"buttonText\": \"ورود\",\n    \"signingInText\": \"در حال ورود...\",\n    \"forgotPassword\": \"رمز عبور را فراموش کرده اید؟\",\n    \"emailHint\": \"ایمیل\",\n    \"passwordHint\": \"رمز عبور\",\n    \"dontHaveAnAccount\": \"آیا حساب کاربری ندارید؟\",\n    \"createAccount\": \"ساخت حساب کاربری\",\n    \"repeatPasswordEmptyError\": \"تکرار رمز عبور نمی‌تواند خالی باشد\",\n    \"unmatchedPasswordError\": \"تکرار رمز عبور مشابه رمز عبور نیست\",\n    \"syncPromptMessage\": \"همگام سازی داده ها ممکن است کمی طول بکشد. لطفا این صفحه را نبندید\",\n    \"or\": \"یا\",\n    \"signInWithGoogle\": \"ادامه دادن با گوگل\",\n    \"signInWithGithub\": \"ادامه دادن با گیتهاب\",\n    \"signInWithDiscord\": \"ادامه دادن با دیسکورد\",\n    \"signInWithApple\": \"ادامه دادن با اپل\",\n    \"continueAnotherWay\": \"ادامه دادن از طریق دیگر\",\n    \"signUpWithGoogle\": \"ثبت نام با گوگل\",\n    \"signUpWithGithub\": \"ثبت نام با گیتهاب\",\n    \"signUpWithDiscord\": \"ثبت نام با دیسکورد\",\n    \"signInWith\": \"ثبت نام با:\",\n    \"signInWithEmail\": \"ادامه دادن با ایمیل\",\n    \"signInWithMagicLink\": \"ادامه\",\n    \"pleaseInputYourEmail\": \"لطفا آدرس ایمیل خود را وارد کنید\",\n    \"settings\": \"تنظیمات\",\n    \"invalidEmail\": \"لطفا یک آدرس ایمیل معتبر وارد کنید\",\n    \"alreadyHaveAnAccount\": \"حساب کاربری دارید؟\",\n    \"logIn\": \"ورود\",\n    \"generalError\": \"مشکلی پیش آمد. لطفاً بعداً دوباره امتحان کنید\",\n    \"anonymous\": \"ناشناس\",\n    \"loginAsGuestButtonText\": \"شروع کنید\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"فضای کار خود را انتخاب کنید\",\n    \"defaultName\": \"فضای کار من\",\n    \"create\": \"ایجاد فضای کار\",\n    \"new\": \"فضای کار جدید\",\n    \"learnMore\": \"بیشتر بدانید\",\n    \"renameWorkspace\": \"حذف فضای کار\",\n    \"workspaceNameCannotBeEmpty\": \"اسم فضای کار نمی‌تواند خالی باشد\",\n    \"hint\": \"فضای کار\",\n    \"notFoundError\": \"فضای کاری پیدا نشد\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"اشتراک گذاری\",\n    \"workInProgress\": \"به زودی\",\n    \"markdown\": \"Markdown\",\n    \"copyLink\": \"کپی کردن لینک\"\n  },\n  \"moreAction\": {\n    \"small\": \"کوچک\",\n    \"medium\": \"متوسط\",\n    \"large\": \"بزرگ\",\n    \"fontSize\": \"اندازه قلم\",\n    \"import\": \"اضافه کردن\",\n    \"moreOptions\": \"گزینه های بیشتر\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Text & Markdown\",\n    \"documentFromV010\": \"سند از نسخه 0.1.0\",\n    \"databaseFromV010\": \"پایگاه داده از نسخه 0.1.0\",\n    \"csv\": \"CSV\",\n    \"database\": \"پایگاه داده\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"تغییر نام\",\n    \"delete\": \"حذف\",\n    \"duplicate\": \"تکرار کردن\",\n    \"unfavorite\": \"حذف از موارد دلخواه\",\n    \"favorite\": \"افزودن به موارد دلخواه\",\n    \"openNewTab\": \"باز کردن در یک برگه جدید\",\n    \"moveTo\": \"انتقال به\",\n    \"addToFavorites\": \"افزودن به موارد دلخواه\",\n    \"copyLink\": \"کپی کردن لینک\"\n  },\n  \"blankPageTitle\": \"صفحه خالی\",\n  \"newPageText\": \"صفحه جدید\",\n  \"trash\": {\n    \"text\": \"سطل زباله\",\n    \"restoreAll\": \"بازیابی همه\",\n    \"deleteAll\": \"حذف همه\",\n    \"pageHeader\": {\n      \"fileName\": \"نام فایل\",\n      \"lastModified\": \"آخرین بازنگری\",\n      \"created\": \"ایجاد شده\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"آیا می‌خواهید که همه صفحه‌ها را در سطل زباله حذف کنید؟\",\n      \"caption\": \"این عمل قابل بازگشت نیست.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"آیا می‌خواهید که همه صفحه‌ها را در سطل زباله بازیابی کنید؟\",\n      \"caption\": \"این عمل قابل بازگشت نیست.\"\n    }\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"این صفحه در سطل زباله است\",\n    \"restore\": \"بازیابی صفحه\",\n    \"deletePermanent\": \"حذف دائمی\"\n  },\n  \"dialogCreatePageNameHint\": \"نام صفحه\",\n  \"questionBubble\": {\n    \"shortcuts\": \"میانبرها\",\n    \"whatsNew\": \"تازه‌ترین‌ها\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"اطلاعات اشکال‌زدایی\",\n      \"success\": \"طلاعات اشکال زدایی در کلیپ بورد کپی شد!\",\n      \"fail\": \"نمی توان اطلاعات اشکال زدایی را در کلیپ بورد کپی کرد\"\n    },\n    \"feedback\": \"بازخورد\",\n    \"help\": \"پشتیبانی و مستندات\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"حذف، تغییر نام، و موارد دیگر...\",\n    \"addPageTooltip\": \"یک صفحه در داخل اضافه کنید\",\n    \"defaultNewPageName\": \"بدون عنوان\",\n    \"renameDialog\": \"تغییر نام\"\n  },\n  \"toolbar\": {\n    \"undo\": \"Undo\",\n    \"redo\": \"Redo\",\n    \"bold\": \"پررنگ\",\n    \"italic\": \"ایتالیک\",\n    \"underline\": \"با خط در زیر\",\n    \"strike\": \"با خط در وسط\",\n    \"numList\": \"فهرست شماره‌گذاری شده\",\n    \"bulletList\": \"فهرست موردی\",\n    \"checkList\": \"چک‌لیست\",\n    \"inlineCode\": \"کد درونی\",\n    \"quote\": \"Quote Block\",\n    \"header\": \"سربرگ\",\n    \"highlight\": \"برجسته کردن\",\n    \"color\": \"رنگ\",\n    \"addLink\": \"افزودن لینک\",\n    \"link\": \"لینک\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"تغییر به مود روشن\",\n    \"darkMode\": \"تغییر به مود تاریک\",\n    \"openAsPage\": \"باز کردن به عنوان صفحه\",\n    \"addNewRow\": \"اضافه کردن سطر جدید\",\n    \"openMenu\": \"برای باز کردن منو کلیک کنید\",\n    \"dragRow\": \"برای مرتب کردن مجدد ردیف فشار طولانی دهید\",\n    \"viewDataBase\": \"مشاهده پایگاه داده\",\n    \"referencePage\": \"این {name} ارجاع داده شده است\",\n    \"addBlockBelow\": \"یک بلوک در زیر اضافه کنید\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"بستن نوار کناری\",\n    \"openSidebar\": \"باز کردن نوار کناری\",\n    \"personal\": \"شخصی\",\n    \"favorites\": \"مورد علاقه\",\n    \"clickToHidePersonal\": \"برای پنهان کردن قسمت شخصی کلیک کنید\",\n    \"clickToHideFavorites\": \"برای پنهان کردن بخش دلخواه کلیک کنید\",\n    \"addAPage\": \"افزودن یک صفحه\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"متن به یادداشت تبدیل شود\",\n      \"path\": \"Documents/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"مخاطبین\",\n    \"whatsHappening\": \"این هفته چه اتفاقی می‌افتد؟\",\n    \"addContact\": \"افزودن مخاطب\",\n    \"editContact\": \"ویرایش مخاطب\"\n  },\n  \"button\": {\n    \"ok\": \"باشه\",\n    \"done\": \"انجام شد\",\n    \"cancel\": \"لغو\",\n    \"signIn\": \"ورود\",\n    \"signOut\": \"خروج\",\n    \"complete\": \"کامل شد\",\n    \"save\": \"ذخیره‌سازی\",\n    \"generate\": \"تولید‌کردن\",\n    \"esc\": \"ESC\",\n    \"keep\": \"نگه داشتن\",\n    \"tryAgain\": \"دوباره تلاش کنید\",\n    \"discard\": \"در نظر نگرفتن\",\n    \"replace\": \"جایگزین کردن\",\n    \"insertBelow\": \"جاگذاری در پایین\",\n    \"upload\": \"بارگذاری\",\n    \"edit\": \"ویرایش\",\n    \"delete\": \"حذف کردن\",\n    \"duplicate\": \"تکرار کردن\",\n    \"putback\": \"بازگشت\"\n  },\n  \"label\": {\n    \"welcome\": \"خوش آمدید!\",\n    \"firstName\": \"نام\",\n    \"middleName\": \"نام میانی\",\n    \"lastName\": \"نام خانوادگی\",\n    \"stepX\": \"Step {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"امکان اتصال به حساب شما وجود ندارد.\",\n      \"failedMsg\": \"لطفا مطمئن شوید که فرآیند ورود را در مرورگر خود تکمیل کرده اید.\"\n    },\n    \"google\": {\n      \"title\": \"ورود با اکانت گوگل\",\n      \"instruction1\": \"برای دسترسی به مخاطبان خود در گوگل، می‌بایست به به این برنامه از طریق مرورگر خود دسترسی دهید.\",\n      \"instruction2\": \"این کد را با کلیک کردن روی آیکون یا انتخاب متن در کلیپ بورد خود کپی کنید:\",\n      \"instruction3\": \"به لینک زیر در مرورگر وب خود بروید و کد بالا را وارد کنید:\",\n      \"instruction4\": \"پس از تکمیل ثبت نام، دکمه زیر را فشار دهید:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"تنظیمات\",\n    \"menu\": {\n      \"appearance\": \"ظاهر برنامه\",\n      \"language\": \"زبان‌ها\",\n      \"user\": \"کاربر\",\n      \"files\": \"فایل‌ها\",\n      \"open\": \"باز کردن تنظیمات\",\n      \"logout\": \"خروج\",\n      \"logoutPrompt\": \"آیا مطمئن هستید که می‌خواهید خارج شوید؟\",\n      \"syncSetting\": \"تنظیمات همگام‌سازی\",\n      \"enableSync\": \"فعال کردن همگام‌سازی\",\n      \"historicalUserList\": \"سابقه ورود کاربر\",\n      \"historicalUserListTooltip\": \"این لیست اکانت‌های ناشناس شما را نمایش می‌دهد. می‌توانید روی یک حساب برای مشاهده جزییات آن کلیک کنید. حساب‌های ناشناس با کلیک کردن روی دکمه شروع‌کنید ایجاد می‌شوند\",\n      \"openHistoricalUser\": \"برای باز کردن حساب ناشناس کلیک کنید\"\n    },\n    \"appearance\": {\n      \"resetSetting\": \"تنظیم کردن از اول\",\n      \"fontFamily\": {\n        \"label\": \"خانواده فونت\",\n        \"search\": \"جستجو\"\n      },\n      \"themeMode\": {\n        \"label\": \"حالت تم\",\n        \"light\": \"حالت روشن\",\n        \"dark\": \"حالت تاریک\",\n        \"system\": \"اعمال حالت\"\n      },\n      \"themeUpload\": {\n        \"button\": \"بارگذاری\",\n        \"description\": \"تم قالب @:appName خود را با استفاده از دکمه زیر آپلود کنید.\",\n        \"loading\": \"لطفاً منتظر بمانید تا تم قالب شما را اعتبارسنجی و آپلود کنیم...\",\n        \"uploadSuccess\": \"تم قالب شما با موفقیت آپلود شد\",\n        \"deletionFailure\": \"تم حذف نشد. سعی کنید آن را به صورت دستی حذف کنید.\",\n        \"filePickerDialogTitle\": \"یک فایل .flowy_plugin را انتخاب کنید\",\n        \"urlUploadFailure\": \"نشانی اینترنتی باز نشد: {}\",\n        \"failure\": \"تم قالب آپلود شده نامعتبر است.\"\n      },\n      \"theme\": \"تم قالب\",\n      \"builtInsLabel\": \"قالب‌های پیش‌ساخته\",\n      \"pluginsLabel\": \"پلاگین‌ها\"\n    },\n    \"files\": {\n      \"copy\": \"کپی\",\n      \"defaultLocation\": \"خواندن فایل‌ها و مکان ذخیره داده‌ها\",\n      \"exportData\": \"از داده‌های خود خروجی بگیرید\",\n      \"doubleTapToCopy\": \"برای کپی کردن دوبار کلیک کنید\",\n      \"restoreLocation\": \"بازیابی به مسیر پیش فرض @:appName\",\n      \"customizeLocation\": \"پوشه دیگری باز کنید\",\n      \"restartApp\": \"لطفاً برنامه را مجدداً راه اندازی کنید تا تغییرات اعمال شوند.\",\n      \"exportDatabase\": \"از پایگاه داده‌ها خروجی بگیرید\",\n      \"selectFiles\": \"فایل‌هایی را که می‌خواهید از آنها خروجی بگیرید انتخاب کنید\",\n      \"selectAll\": \"انتخاب همه\",\n      \"deselectAll\": \"لغو انتخاب همه\",\n      \"createNewFolder\": \"ایجاد یک پوشه جدید\",\n      \"createNewFolderDesc\": \"کجا می خواهید داده‌های خود را ذخیره کنید\",\n      \"defineWhereYourDataIsStored\": \"محل ذخیره داده های خود را مشخص کنید\",\n      \"open\": \"باز کردن\",\n      \"openFolder\": \"باز کردن یک پوشه موجود\",\n      \"openFolderDesc\": \"خواندن و نوشتن آن در یک پوشه @:appName موجود\",\n      \"folderHintText\": \"نام پوشه\",\n      \"location\": \"ایجاد یک پوشه جدید\",\n      \"locationDesc\": \"یک نام برای پوشه داده @:appName خود انتخاب کنید\",\n      \"browser\": \"مرورگر\",\n      \"create\": \"ایجاد کردن\",\n      \"set\": \"تنظیم کردن\",\n      \"folderPath\": \"مسیری برای ذخیره پوشه\",\n      \"locationCannotBeEmpty\": \"مسیر نمی‌تواند خالی باشد\",\n      \"pathCopiedSnackbar\": \"مسیر ذخیره‌سازی فایل در کلیپ‌بورد کپی شد!\",\n      \"changeLocationTooltips\": \"تغییر دایرکتوری داده\",\n      \"change\": \"تغییر\",\n      \"openLocationTooltips\": \"باز کردن یک فهرست پوشه دیگر\",\n      \"openCurrentDataFolder\": \"باز کردن فهرست پوشه فعلی\",\n      \"recoverLocationTooltips\": \"بازنشانی به فهرست داده های پیش فرض @:appName\",\n      \"exportFileSuccess\": \"خروجی گرفتن از فایل با موفقیت انجام شد.\",\n      \"exportFileFail\": \"خروجی گرفتن از فایل انجام نشد!\",\n      \"export\": \"خروجی گرفتن\"\n    },\n    \"user\": {\n      \"name\": \"نام\",\n      \"selectAnIcon\": \"انتخاب یک آیکون\",\n      \"pleaseInputYourOpenAIKey\": \"لطفا کلید AI خود را وارد کنید\",\n      \"clickToLogout\": \"برای خروج از کاربر فعلی کلیک کنید\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"میانبرها\",\n      \"command\": \"دستور\",\n      \"keyBinding\": \"میانبرهای کیبورد\",\n      \"addNewCommand\": \"اضافه کردن فرمان جدید\",\n      \"updateShortcutStep\": \"کلید ترکیبی دلخواه را فشار دهید و ENTER را فشار دهید\",\n      \"shortcutIsAlreadyUsed\": \"این میانبر قبلاً برای: {conflict} استفاده شده است\",\n      \"resetToDefault\": \"بازگشت به میانبرهای پیش‌فرض\",\n      \"couldNotLoadErrorMsg\": \"میانبرها بارگذاری نشد، دوباره امتحان کنید\",\n      \"couldNotSaveErrorMsg\": \"میانبرها ذخیره نشد، دوباره امتحان کنید\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"آیا مطمئن هستید که می خواهید این نما را حذف کنید؟\",\n    \"createView\": \"جدید\",\n    \"settings\": {\n      \"filter\": \"فیلتر\",\n      \"sort\": \"مرتب کردن\",\n      \"sortBy\": \"مرتب سازی بر اساس\",\n      \"properties\": \"ویژگی‌ها\",\n      \"reorderPropertiesTooltip\": \"برای مرتب کردن مجدد بکشید\",\n      \"group\": \"گروه\",\n      \"addFilter\": \"افزودن فیلتر\",\n      \"deleteFilter\": \"حذف فیلتر\",\n      \"filterBy\": \"فیلتر بر اساس...\",\n      \"typeAValue\": \"یک مقدار را تایپ کنید...\",\n      \"layout\": \"طرح‌بندی\",\n      \"databaseLayout\": \"طرح‌بندی\"\n    },\n    \"textFilter\": {\n      \"contains\": \"شامل\",\n      \"doesNotContain\": \"شامل نمی‌شود\",\n      \"endsWith\": \"پایان با\",\n      \"startWith\": \"شروع با\",\n      \"is\": \"هست\",\n      \"isNot\": \"نیست\",\n      \"isEmpty\": \"خالی است\",\n      \"isNotEmpty\": \"خالی نیست\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"مخالف\",\n        \"startWith\": \"شروع با\",\n        \"endWith\": \"پایان با\",\n        \"isEmpty\": \"خالی است\",\n        \"isNotEmpty\": \"خالی نیست\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"بررسی شده\",\n      \"isUnchecked\": \"بررسی نشده\",\n      \"choicechipPrefix\": {\n        \"is\": \"است\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"کامل است\",\n      \"isIncomplted\": \"کامل نیست\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"است\",\n      \"isNot\": \"نیست\",\n      \"contains\": \"شامل\",\n      \"doesNotContain\": \"شامل نیست\",\n      \"isEmpty\": \"خالی است\",\n      \"isNotEmpty\": \"خالی نیست\"\n    },\n    \"field\": {\n      \"hide\": \"پنهان کردن\",\n      \"insertLeft\": \"درج در چپ\",\n      \"insertRight\": \"درج در راست\",\n      \"duplicate\": \"تکرار کردن\",\n      \"delete\": \"حذف کردن\",\n      \"textFieldName\": \"متن\",\n      \"checkboxFieldName\": \"موارد انتخابی\",\n      \"dateFieldName\": \"تاریخ\",\n      \"updatedAtFieldName\": \"آخرین زمان بازنگری\",\n      \"createdAtFieldName\": \"زمان ایجاد\",\n      \"numberFieldName\": \"شماره‌ها\",\n      \"singleSelectFieldName\": \"انتخاب\",\n      \"multiSelectFieldName\": \"چند‌انتخابی\",\n      \"urlFieldName\": \"نشانی اینترنتی\",\n      \"checklistFieldName\": \"چک لیست\",\n      \"numberFormat\": \"قالب شماره\",\n      \"dateFormat\": \"قالب تاریخ\",\n      \"includeTime\": \"شامل کردن زمان\",\n      \"dateFormatFriendly\": \"Month Day, Year\",\n      \"dateFormatISO\": \"Year-Month-Day\",\n      \"dateFormatLocal\": \"Month/Day/Year\",\n      \"dateFormatUS\": \"Year/Month/Day\",\n      \"dateFormatDayMonthYear\": \"Day/Month/Year\",\n      \"timeFormat\": \"قالب زمان\",\n      \"invalidTimeFormat\": \"قالب نامعتبر\",\n      \"timeFormatTwelveHour\": \"دوازده ساعته\",\n      \"timeFormatTwentyFourHour\": \"بیست‌و‌چهار ساعته\",\n      \"clearDate\": \"پاک کردن\",\n      \"addSelectOption\": \"افزودن یک گزینه\",\n      \"optionTitle\": \"گزینه‌ها\",\n      \"addOption\": \"افزودن گزینه\",\n      \"editProperty\": \"ویرایش ویژگی\",\n      \"newProperty\": \"ویژگی جدید\",\n      \"deleteFieldPromptMessage\": \"آیا مطمئن هستید؟ این ویژگی حذف خواهد شد\",\n      \"newColumn\": \"ستون جدید\"\n    },\n    \"sort\": {\n      \"ascending\": \"صعودی\",\n      \"descending\": \"نزولی\",\n      \"deleteAllSorts\": \"حذف همه مرتب‌سازی‌ها\",\n      \"addSort\": \"اضافه کردن مرتب‌سازی\"\n    },\n    \"row\": {\n      \"duplicate\": \"تکرار کردن\",\n      \"delete\": \"حذف کردن\",\n      \"textPlaceholder\": \"خالی\",\n      \"copyProperty\": \"ویژگی در کلیپ‌بورد کپی شد.\",\n      \"count\": \"شمارش\",\n      \"newRow\": \"سطر جدید\",\n      \"action\": \"اعمال\"\n    },\n    \"selectOption\": {\n      \"create\": \"ایجاد\",\n      \"purpleColor\": \"بنفش\",\n      \"pinkColor\": \"صورتی\",\n      \"lightPinkColor\": \"صورتی روشن\",\n      \"orangeColor\": \"نارنجی\",\n      \"yellowColor\": \"زرد\",\n      \"limeColor\": \"لیمویی\",\n      \"greenColor\": \"سبز\",\n      \"aquaColor\": \"آکوا\",\n      \"blueColor\": \"آبی\",\n      \"deleteTag\": \"حذف برچسب\",\n      \"colorPanelTitle\": \"رنگ‌ها\",\n      \"panelTitle\": \"یک گزینه انتخاب یا ایجاد کنید.\",\n      \"searchOption\": \"جستجوی یک گزینه\"\n    },\n    \"checklist\": {\n      \"addNew\": \"یک مورد اضافه کنید\"\n    },\n    \"menuName\": \"شبکه‌ای\",\n    \"referencedGridPrefix\": \"نمایش\"\n  },\n  \"document\": {\n    \"menuName\": \"سند\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"یک بورد برای لینک کردن انتخاب کنید.\",\n        \"createANewBoard\": \"ایجاد یک بورد جدید\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"یک شبکه‌ نمایش برای لینک کردن انتخاب کنید.\",\n        \"createANewGrid\": \"ایجاد یک شبکه نمایش جدید\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"یک تقویم برای لینک کردن انتخاب کنید.\",\n        \"createANewCalendar\": \"ایجاد یک تقویم جدید\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"طرح کلی\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"بورد مرجع\",\n      \"referencedGrid\": \"شبکه‌نمایش مرجع\",\n      \"referencedCalendar\": \"تقویم مرجع\",\n      \"autoGeneratorMenuItemName\": \"AI نویسنده\",\n      \"autoGeneratorTitleName\": \"از هوش مصنوعی بخواهید هر چیزی بنویسد...\",\n      \"autoGeneratorLearnMore\": \"بیشتر بدانید\",\n      \"autoGeneratorGenerate\": \"بنویس\",\n      \"autoGeneratorHintText\": \"از AI بپرسید ...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"کلید AI را نمی توان دریافت کرد\",\n      \"autoGeneratorRewrite\": \"بازنویس\",\n      \"smartEdit\": \"دستیاران هوشمند\",\n      \"smartEditFixSpelling\": \"اصلاح نگارش\",\n      \"warning\": \"⚠️ پاسخ‌های هوش مصنوعی می‌توانند نادرست یا گمراه‌کننده باشند\",\n      \"smartEditSummarize\": \"خلاصه‌نویسی\",\n      \"smartEditImproveWriting\": \"بهبود نگارش\",\n      \"smartEditMakeLonger\": \"به نوشته اضافه کن\",\n      \"smartEditCouldNotFetchResult\": \"نتیجه‌ای از AI گرفته نشد\",\n      \"smartEditCouldNotFetchKey\": \"کلید AI واکشی نشد\",\n      \"smartEditDisabled\": \"به AI در تنظیمات وصل شوید\",\n      \"discardResponse\": \"آیا می خواهید پاسخ های هوش مصنوعی را حذف کنید؟\",\n      \"createInlineMathEquation\": \"ایجاد معادله\",\n      \"toggleList\": \"Toggle لیست\",\n      \"cover\": {\n        \"changeCover\": \"تغییر جلد\",\n        \"colors\": \"رنگ‌ها\",\n        \"images\": \"تصویر‌ها\",\n        \"clearAll\": \"پاک کردن همه\",\n        \"abstract\": \"چکیده\",\n        \"addCover\": \"افزودن جلد\",\n        \"addLocalImage\": \"افزودن تصویر\",\n        \"invalidImageUrl\": \"مسیر تصویر نامعتبر است\",\n        \"failedToAddImageToGallery\": \"افزودن تصویر به گالری انجام نشد\",\n        \"enterImageUrl\": \"مسیر تصویر را وارد کنید\",\n        \"add\": \"افزودن\",\n        \"back\": \"بازگشت\",\n        \"saveToGallery\": \"ذخیره در گالری\",\n        \"removeIcon\": \"حذف Icon\",\n        \"pasteImageUrl\": \"وارد کردن مسیر تصویر\",\n        \"or\": \"یا\",\n        \"pickFromFiles\": \"انتخاب از فایل‌ها\",\n        \"couldNotFetchImage\": \"تصویر واکشی نشد\",\n        \"imageSavingFailed\": \"ذخیره تصویر انجام نشد\",\n        \"addIcon\": \"افزودن آیکون\",\n        \"coverRemoveAlert\": \"پس از حذف از روی جلد حذف خواهد شد.\",\n        \"alertDialogConfirmation\": \"آیا مطمئن هستید که می‌خواهید ادامه دهید؟\"\n      },\n      \"mathEquation\": {\n        \"addMathEquation\": \"اضافه کردن معادله ریاضی\",\n        \"editMathEquation\": \"ویرایش کردن معادله ریاضی\"\n      },\n      \"optionAction\": {\n        \"click\": \"کلیک کنید\",\n        \"toOpenMenu\": \" برای باز کردن منو\",\n        \"delete\": \"حذف کردن\",\n        \"duplicate\": \"تکرار کردن\",\n        \"turnInto\": \"تبدیل به\",\n        \"moveUp\": \"بالا بردن\",\n        \"moveDown\": \"پایین آوردن\",\n        \"color\": \"رنگ\",\n        \"align\": \"هم‌تراز کردن\",\n        \"left\": \"چپ\",\n        \"center\": \"وسط\",\n        \"right\": \"راست\",\n        \"defaultColor\": \"پیش فرض\"\n      },\n      \"image\": {\n        \"copiedToPasteBoard\": \"لینک تصویر در کلیپ‌بورد کپی شده است\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"برای ایجاد فهرست مطالب سر‌فصل‌ها را وارد کنید\"\n      },\n      \"openAI\": \"AI\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"برای دستورات '/' را تایپ کنید\"\n    },\n    \"title\": {\n      \"placeholder\": \"بدون عنوان\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"برای افزودن تصویر کلیک کنید\",\n      \"upload\": {\n        \"label\": \"بارگذاری\",\n        \"placeholder\": \"برای بارگذاری تصویر کلیک کنید\"\n      },\n      \"url\": {\n        \"label\": \"لینک تصویر\",\n        \"placeholder\": \"لینک تصویر را وارد کنید\"\n      },\n      \"support\": \"محدودیت اندازه تصویر 5 مگابایت است. فرمت‌های پشتیبانی شده: JPEG، PNG، GIF، SVG\",\n      \"error\": {\n        \"invalidImage\": \"تصویر نامعتبر\",\n        \"invalidImageSize\": \"اندازه تصویر باید کمتر از 5 مگابایت باشد\",\n        \"invalidImageFormat\": \"فرمت تصویر پشتیبانی نمی‌شود. فرمت‌های پشتیبانی شده: JPEG، PNG، GIF، SVG\",\n        \"invalidImageUrl\": \"مسیر تصویر نامعتبر است\"\n      }\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"زبان\",\n        \"placeholder\": \"انتخاب زبان\"\n      }\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"پیست کنید یا مسیر را تایپ کنید\",\n      \"openInNewTab\": \"باز کردن در برگه جدید\",\n      \"copyLink\": \"کپی لینک\",\n      \"removeLink\": \"حذف لینک\",\n      \"url\": {\n        \"label\": \"لینک\",\n        \"placeholder\": \"لینک را وارد کنید\"\n      },\n      \"title\": {\n        \"label\": \"عنوان لینک\",\n        \"placeholder\": \"عنوان لینک را وارد کنید\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"یک شخص یا یک صفحه یا تاریخ را ذکر کنید...\",\n      \"page\": {\n        \"label\": \"لینک به صفحه\",\n        \"tooltip\": \"برای باز کردن صفحه کلیک کنید\"\n      }\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"ایجاد\"\n    },\n    \"menuName\": \"بورد\",\n    \"referencedBoardPrefix\": \"نمای\",\n    \"mobile\": {\n      \"showGroup\": \"نمایش گروه\",\n      \"showGroupContent\": \"آیا مطمئن هستید که می خواهید این گروه را روی تابلو نشان دهید؟\",\n      \"failedToLoad\": \"نمای تخته بارگیری نشد\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"تقویم\",\n    \"defaultNewCalendarTitle\": \"بدون عنوان\",\n    \"navigation\": {\n      \"today\": \"امروز\",\n      \"jumpToday\": \"برو به امروز\",\n      \"previousMonth\": \"ماه قبل\",\n      \"nextMonth\": \"ماه بعد\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"نمایش اعداد هفته\",\n      \"showWeekends\": \"نمایش تعطیلات آخر هفته\",\n      \"firstDayOfWeek\": \"شروع هفته در\",\n      \"layoutDateField\": \"طرح‌بندی تقویم با\",\n      \"noDateTitle\": \"بدون تاریخ\",\n      \"clickToAdd\": \"برای افزودن به تقویم کلیک کنید\",\n      \"name\": \"طرح‌بندی تقویم\",\n      \"noDateHint\": \"رویدادهای برنامه‌ریزی نشده در اینجا نشان داده می‌شوند\"\n    },\n    \"referencedCalendarPrefix\": \"نمای\"\n  },\n  \"errorDialog\": {\n    \"title\": \"خطای @:appName\",\n    \"howToFixFallback\": \"بابت مشکل پیش آمده متأسفیم! مشکل و شرح آن را در صفحه GitHub ما ارسال کنید.\",\n    \"github\": \"مشاهده در GitHub\"\n  },\n  \"search\": {\n    \"label\": \"جستجو\",\n    \"placeholder\": {\n      \"actions\": \"جستجوی اعمال...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"کپی شد!\",\n      \"fail\": \"نمی‌توان کپی کرد\"\n    }\n  },\n  \"unSupportBlock\": \"نسخه فعلی از این بلوک پشتیبانی نمی‌کند.\",\n  \"views\": {\n    \"deleteContentTitle\": \"آیا مطمئن هستید که می‌خواهید {pageType} را حذف کنید؟\",\n    \"deleteContentCaption\": \"اگر این {pageType} را حذف کنید، می‌توانید آن را از سطل زباله بازیابی کنید.\"\n  },\n  \"colors\": {\n    \"custom\": \"سفارشی\",\n    \"default\": \"پیش‌فرض\",\n    \"red\": \"قرمز\",\n    \"orange\": \"نارنجی\",\n    \"yellow\": \"زرد\",\n    \"green\": \"سبز\",\n    \"blue\": \"آبی\",\n    \"purple\": \"بنفش\",\n    \"pink\": \"صورتی\",\n    \"brown\": \"قهوه‌ای\",\n    \"gray\": \"خاکستری\"\n  },\n  \"emoji\": {\n    \"filter\": \"فیلتر\",\n    \"random\": \"تصادفی\",\n    \"selectSkinTone\": \"انتخاب رنگ پوست\",\n    \"remove\": \"حذف ایموجی\",\n    \"categories\": {\n      \"smileys\": \"لبخندی‌ها\",\n      \"people\": \"آدمک‌ها\",\n      \"animals\": \"حیوانات و طبیعت\",\n      \"food\": \"غذا و نوشیدنی\",\n      \"activities\": \"فعالیت‌ها\",\n      \"places\": \"مسافرت\",\n      \"objects\": \"اشیا\",\n      \"symbols\": \"نماد‌ها\",\n      \"flags\": \"پرچم‌ها\",\n      \"nature\": \"طبیعت\",\n      \"frequentlyUsed\": \"استفاده‌شده\"\n    }\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/fr-CA.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Moi\",\n  \"welcomeText\": \"Bienvenue sur @:appName\",\n  \"welcomeTo\": \"Bienvenue à\",\n  \"githubStarText\": \"Étoiler sur GitHub\",\n  \"subscribeNewsletterText\": \"Abonnez-vous à notre courriel\",\n  \"letsGoButtonText\": \"Allons-y\",\n  \"title\": \"Titre\",\n  \"youCanAlso\": \"Vous pouvez aussi\",\n  \"and\": \"et\",\n  \"failedToOpenUrl\": \"Échec de l'ouverture de l'URL : {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Cliquez pour ajouter ci-dessous\",\n    \"addAboveCmd\": \"Alt+clic\",\n    \"addAboveMacCmd\": \"Option+clic\",\n    \"addAboveTooltip\": \"à ajouter au dessus\",\n    \"dragTooltip\": \"Glisser pour déplacer\",\n    \"openMenuTooltip\": \"Cliquez pour ouvrir le menu\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"S'inscrire\",\n    \"title\": \"S'inscrire à @:appName\",\n    \"getStartedText\": \"Commencer\",\n    \"emptyPasswordError\": \"Vous n'avez pas saisi votre mot de passe\",\n    \"repeatPasswordEmptyError\": \"Vous n'avez pas ressaisi votre mot de passe\",\n    \"unmatchedPasswordError\": \"Les deux mots de passe ne sont pas identiques\",\n    \"alreadyHaveAnAccount\": \"Avez-vous déjà un compte?\",\n    \"emailHint\": \"Courriel\",\n    \"passwordHint\": \"Mot de passe\",\n    \"repeatPasswordHint\": \"Ressaisir votre mot de passe\",\n    \"signUpWith\": \"Se connecter avec:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Connexion à @:appName\",\n    \"loginButtonText\": \"Connexion\",\n    \"loginStartWithAnonymous\": \"Lancer avec une session anonyme\",\n    \"continueAnonymousUser\": \"Continuer avec une session anonyme\",\n    \"buttonText\": \"Se connecter\",\n    \"signingInText\": \"Connexion en cours...\",\n    \"forgotPassword\": \"Mot de passe oublié?\",\n    \"emailHint\": \"Courriel\",\n    \"passwordHint\": \"Mot de passe\",\n    \"dontHaveAnAccount\": \"Vous n'avez pas encore de compte?\",\n    \"createAccount\": \"Créer un compte\",\n    \"repeatPasswordEmptyError\": \"Vous n'avez pas ressaisi votre mot de passe\",\n    \"unmatchedPasswordError\": \"Les deux mots de passe ne sont pas identiques\",\n    \"syncPromptMessage\": \"La synchronisation des données peut prendre un certain temps. Merci de ne pas fermer pas cette page.\",\n    \"or\": \"OU\",\n    \"signInWithGoogle\": \"Continuer avec Google\",\n    \"signInWithGithub\": \"Continuer avec Github\",\n    \"signInWithDiscord\": \"Continuer avec Discord\",\n    \"signInWithApple\": \"Se connecter avec Apple\",\n    \"continueAnotherWay\": \"Continuer avec une autre méthode\",\n    \"signUpWithGoogle\": \"S'inscrire avec Google\",\n    \"signUpWithGithub\": \"S'inscrire avec Github\",\n    \"signUpWithDiscord\": \"S'inscrire avec Discord\",\n    \"signInWith\": \"Se connecter avec:\",\n    \"signInWithEmail\": \"Se connecter avec e-mail\",\n    \"signInWithMagicLink\": \"Continuer\",\n    \"signUpWithMagicLink\": \"S'inscrire avec un lien spécial\",\n    \"pleaseInputYourEmail\": \"Veuillez entrer votre adresse e-mail\",\n    \"settings\": \"Paramètres\",\n    \"magicLinkSent\": \"Lien spécial envoyé à votre email, veuillez vérifier votre boîte de réception\",\n    \"invalidEmail\": \"S'il vous plaît, mettez une adresse email valide\",\n    \"alreadyHaveAnAccount\": \"Déjà un compte ?\",\n    \"logIn\": \"Connexion\",\n    \"generalError\": \"Une erreur s'est produite. Veuillez réessayer plus tard\",\n    \"limitRateError\": \"Pour des raisons de sécurité, vous ne pouvez demander un lien spécial que toutes les 60 secondes\",\n    \"magicLinkSentDescription\": \"Un lien spécial vous a été envoyé par e-mail. Cliquez sur le lien pour vous connecter. Le lien expirera dans 5 minutes.\",\n    \"anonymous\": \"Anonyme\",\n    \"LogInWithGoogle\": \"Se connecter avec Google\",\n    \"LogInWithGithub\": \"Se connecter avec Github\",\n    \"LogInWithDiscord\": \"Se connecter avec Discord\",\n    \"loginAsGuestButtonText\": \"Commencer\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Choisissez votre espace de travail\",\n    \"defaultName\": \"Mon espace de travail\",\n    \"create\": \"Créer un espace de travail\",\n    \"new\": \"Nouveau espace de travail\",\n    \"importFromNotion\": \"Importer depuis Notion\",\n    \"learnMore\": \"En savoir plus\",\n    \"reset\": \"Réinitialiser l'espace de travail\",\n    \"renameWorkspace\": \"Renommer l'espace de travail\",\n    \"workspaceNameCannotBeEmpty\": \"Le nom de l'espace de travail ne peut être vide\",\n    \"resetWorkspacePrompt\": \"La réinitialisation de l'espace de travail supprimera toutes les pages et données qu'elles contiennent. Êtes-vous sûr de vouloir réinitialiser l'espace de travail ? Alternativement, vous pouvez contacter l'équipe d'assistance pour restaurer l'espace de travail\",\n    \"hint\": \"Espace de travail\",\n    \"notFoundError\": \"Espace de travail introuvable\",\n    \"failedToLoad\": \"Quelque chose s'est mal passé ! Échec du chargement de l'espace de travail. Essayez de fermer toute instance ouverte d'@:appName et réessayez.\",\n    \"errorActions\": {\n      \"reportIssue\": \"Signaler un problème\",\n      \"reportIssueOnGithub\": \"Signaler un bug sur Github\",\n      \"exportLogFiles\": \"Exporter les logs\",\n      \"reachOut\": \"Contactez-nous sur Discord\"\n    },\n    \"menuTitle\": \"Espaces de travail\",\n    \"deleteWorkspaceHintText\": \"Êtes-vous sûr de vouloir supprimer l'espace de travail ? Cette action ne peut pas être annulée.\",\n    \"createSuccess\": \"Espace de travail créé avec succès\",\n    \"createFailed\": \"Échec de la création de l'espace de travail\",\n    \"createLimitExceeded\": \"Vous avez atteint la limite maximale d'espace de travail autorisée pour votre compte. Si vous avez besoin d'espaces de travail supplémentaires pour continuer votre travail, veuillez en faire la demande sur Github.\",\n    \"deleteSuccess\": \"Espace de travail supprimé avec succès\",\n    \"deleteFailed\": \"Échec de la suppression de l'espace de travail\",\n    \"openSuccess\": \"Ouverture de l'espace de travail réussie\",\n    \"openFailed\": \"Échec de l'ouverture de l'espace de travail\",\n    \"renameSuccess\": \"Espace de travail renommé avec succès\",\n    \"renameFailed\": \"Échec du renommage de l'espace de travail\",\n    \"updateIconSuccess\": \"L'icône de l'espace de travail a été mise à jour avec succès\",\n    \"updateIconFailed\": \"La mise a jour de l'icône de l'espace de travail a échoué\",\n    \"cannotDeleteTheOnlyWorkspace\": \"Impossible de supprimer le seul espace de travail\",\n    \"fetchWorkspacesFailed\": \"Échec de la récupération des espaces de travail\",\n    \"leaveCurrentWorkspace\": \"Quitter l'espace de travail\",\n    \"leaveCurrentWorkspacePrompt\": \"Êtes-vous sûr de vouloir quitter l'espace de travail actuel ?\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Partager\",\n    \"workInProgress\": \"Bientôt disponible\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"Copier dans le presse-papier\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Copier le lien\",\n    \"publishToTheWeb\": \"Publier sur le Web\",\n    \"publishToTheWebHint\": \"Créer un site Internet avec AppFlowy\",\n    \"publish\": \"Partager\",\n    \"unPublish\": \"Annuler la publication\",\n    \"visitSite\": \"Visitez le site\",\n    \"exportAsTab\": \"Exporter en tant que\",\n    \"publishTab\": \"Partager\",\n    \"shareTab\": \"Partager\",\n    \"publishOnAppFlowy\": \"Partager sur AppFlowy\"\n  },\n  \"moreAction\": {\n    \"small\": \"petit\",\n    \"medium\": \"moyen\",\n    \"large\": \"grand\",\n    \"fontSize\": \"Taille de police\",\n    \"import\": \"Importer\",\n    \"moreOptions\": \"Plus d'options\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Texte et Markdown\",\n    \"documentFromV010\": \"Document de la v0.1.0\",\n    \"databaseFromV010\": \"Base de données à partir de la v0.1.0\",\n    \"csv\": \"CSV\",\n    \"database\": \"Base de données\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Renommer\",\n    \"delete\": \"Supprimer\",\n    \"duplicate\": \"Dupliquer\",\n    \"unfavorite\": \"Retirer des favoris\",\n    \"favorite\": \"Ajouter aux favoris\",\n    \"openNewTab\": \"Ouvrir dans un nouvel onglet\",\n    \"moveTo\": \"Déplacer vers\",\n    \"addToFavorites\": \"Ajouter aux Favoris\",\n    \"copyLink\": \"Copier le lien\"\n  },\n  \"blankPageTitle\": \"Page vierge\",\n  \"newPageText\": \"Nouvelle page\",\n  \"newDocumentText\": \"Nouveau document\",\n  \"newGridText\": \"Nouvelle grille\",\n  \"newCalendarText\": \"Nouveau calendrier\",\n  \"newBoardText\": \"Nouveau tableau\",\n  \"trash\": {\n    \"text\": \"Corbeille\",\n    \"restoreAll\": \"Tout récupérer\",\n    \"deleteAll\": \"Tout supprimer\",\n    \"pageHeader\": {\n      \"fileName\": \"Nom de fichier\",\n      \"lastModified\": \"Dernière modification\",\n      \"created\": \"Créé\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Voulez-vous vraiment supprimer toutes les pages de la corbeille ?\",\n      \"caption\": \"Cette action ne peut pas être annulée.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Êtes-vous sûr de vouloir restaurer toutes les pages dans la corbeille ?\",\n      \"caption\": \"Cette action ne peut pas être annulée.\"\n    },\n    \"mobile\": {\n      \"actions\": \"Actions de la corbeille\",\n      \"empty\": \"La corbeille est vide\",\n      \"emptyDescription\": \"Vous n'avez aucun fichier supprimé\",\n      \"isDeleted\": \"a été supprimé\",\n      \"isRestored\": \"a été restauré\"\n    }\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Cette page se trouve dans la corbeille\",\n    \"restore\": \"Récupérer la page\",\n    \"deletePermanent\": \"Supprimer définitivement\"\n  },\n  \"dialogCreatePageNameHint\": \"Nom de la page\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Raccourcis\",\n    \"whatsNew\": \"Nouveautés\",\n    \"markdown\": \"Réduction\",\n    \"debug\": {\n      \"name\": \"Infos du système\",\n      \"success\": \"Informations de débogage copiées dans le presse-papiers !\",\n      \"fail\": \"Impossible de copier les informations de débogage dans le presse-papiers\"\n    },\n    \"feedback\": \"Retour\",\n    \"help\": \"Aide et Support Technique\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Supprimer, renommer et plus...\",\n    \"addPageTooltip\": \"Ajouter rapidement une page à l'intérieur\",\n    \"defaultNewPageName\": \"Sans titre\",\n    \"renameDialog\": \"Renommer\"\n  },\n  \"noPagesInside\": \"Aucune page à l'intérieur\",\n  \"toolbar\": {\n    \"undo\": \"Annuler\",\n    \"redo\": \"Rétablir\",\n    \"bold\": \"Gras\",\n    \"italic\": \"Italique\",\n    \"underline\": \"Souligner\",\n    \"strike\": \"Barré\",\n    \"numList\": \"Liste numérotée\",\n    \"bulletList\": \"Liste à puces\",\n    \"checkList\": \"Liste de contrôle\",\n    \"inlineCode\": \"Code en ligne\",\n    \"quote\": \"Citation\",\n    \"header\": \"En-tête\",\n    \"highlight\": \"Surligner\",\n    \"color\": \"Couleur\",\n    \"addLink\": \"Ajouter un lien\",\n    \"link\": \"Lien\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Passer en mode clair\",\n    \"darkMode\": \"Passer en mode sombre\",\n    \"openAsPage\": \"Ouvrir en tant que page\",\n    \"addNewRow\": \"Ajouter une ligne\",\n    \"openMenu\": \"Cliquez pour ouvrir le menu\",\n    \"dragRow\": \"Appuyez longuement pour réorganiser la ligne\",\n    \"viewDataBase\": \"Voir la base de données\",\n    \"referencePage\": \"Ce {nom} est référencé\",\n    \"addBlockBelow\": \"Ajouter un bloc ci-dessous\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Fermer le menu latéral\",\n    \"openSidebar\": \"Ouvrir le menu latéral\",\n    \"personal\": \"Personnel\",\n    \"favorites\": \"Favoris\",\n    \"clickToHidePersonal\": \"Cliquez pour cacher la section personnelle\",\n    \"clickToHideFavorites\": \"Cliquez pour cacher la section favorite\",\n    \"addAPage\": \"Ajouter une page\",\n    \"recent\": \"Récent\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Note exportée en Markdown\",\n      \"path\": \"Documents/fluide\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Contacts\",\n    \"whatsHappening\": \"Que se passe-t-il cette semaine ?\",\n    \"addContact\": \"Ajouter un contact\",\n    \"editContact\": \"Modifier le contact\"\n  },\n  \"button\": {\n    \"ok\": \"OK\",\n    \"done\": \"Fait\",\n    \"cancel\": \"Annuler\",\n    \"signIn\": \"Se connecter\",\n    \"signOut\": \"Se déconnecter\",\n    \"complete\": \"Achevé\",\n    \"save\": \"Sauvegarder\",\n    \"generate\": \"Générer\",\n    \"esc\": \"ESC\",\n    \"keep\": \"Garder\",\n    \"tryAgain\": \"Essayer à nouveau\",\n    \"discard\": \"Jeter\",\n    \"replace\": \"Remplacer\",\n    \"insertBelow\": \"Insérer ci-dessous\",\n    \"insertAbove\": \"Insérer ci-dessus\",\n    \"upload\": \"Télécharger\",\n    \"edit\": \"Modifier\",\n    \"delete\": \"Supprimer\",\n    \"duplicate\": \"Dupliquer\",\n    \"putback\": \"Remettre\",\n    \"update\": \"Mettre à jour\",\n    \"share\": \"Partager\",\n    \"removeFromFavorites\": \"Retirer des favoris\",\n    \"addToFavorites\": \"Ajouter aux favoris\",\n    \"rename\": \"Renommer\",\n    \"helpCenter\": \"Centre d'aide\",\n    \"add\": \"Ajouter\",\n    \"yes\": \"Oui\",\n    \"tryAGain\": \"Réessayer\"\n  },\n  \"label\": {\n    \"welcome\": \"Bienvenue!\",\n    \"firstName\": \"Prénom\",\n    \"middleName\": \"Deuxième nom\",\n    \"lastName\": \"Nom de famille\",\n    \"stepX\": \"Étape {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Incapable de se connecter à votre compte.\",\n      \"failedMsg\": \"SVP assurez-vous d'avoir complèté le processus d'enregistrement dans votre fureteur.\"\n    },\n    \"google\": {\n      \"title\": \"S'identifier avec Google\",\n      \"instruction1\": \"Pour importer vos contacts Google, vous devez autoriser cette application à l'aide de votre navigateur Web.\",\n      \"instruction2\": \"Copiez ce code dans votre presse-papiers en cliquant sur l'icône ou en sélectionnant le texte:\",\n      \"instruction3\": \"Accédez au lien suivant dans votre navigateur Web et saisissez le code ci-dessus:\",\n      \"instruction4\": \"Appuyez sur le bouton ci-dessous lorsque vous avez terminé votre inscription:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Paramètres\",\n    \"menu\": {\n      \"appearance\": \"Apparence\",\n      \"language\": \"Langue\",\n      \"user\": \"Utilisateur\",\n      \"files\": \"Dossiers\",\n      \"notifications\": \"Notifications\",\n      \"open\": \"Ouvrir les paramètres\",\n      \"logout\": \"Se déconnecter\",\n      \"logoutPrompt\": \"Êtes-vous sûr de vouloir vous déconnecter ?\",\n      \"selfEncryptionLogoutPrompt\": \"Êtes-vous sûr de vouloir vous déconnecter ? Veuillez vous assurer d'avoir copié la clé de chiffrement.\",\n      \"syncSetting\": \"Paramètres de synchronisation\",\n      \"cloudSettings\": \"Paramètres cloud\",\n      \"enableSync\": \"Activer la synchronisation\",\n      \"enableEncrypt\": \"Chiffrer les données\",\n      \"cloudURL\": \"URL de base\",\n      \"invalidCloudURLScheme\": \"Schéma invalide\",\n      \"cloudServerType\": \"Serveur cloud\",\n      \"cloudServerTypeTip\": \"Veuillez noter qu'il est possible que votre compte actuel soit déconnecté après avoir changé de serveur cloud.\",\n      \"cloudLocal\": \"Local\",\n      \"cloudAppFlowy\": \"@:appName Cloud Bêta\",\n      \"cloudAppFlowySelfHost\": \"@:appName Cloud auto-hébergé\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"L'URL cloud ne peut pas être vide\",\n      \"clickToCopy\": \"Cliquez pour copier\",\n      \"selfHostStart\": \"Si vous n'avez pas de serveur, veuillez vous référer au\",\n      \"selfHostContent\": \"document\",\n      \"selfHostEnd\": \"pour obtenir des conseils sur la façon d'auto-héberger votre propre serveur\",\n      \"cloudURLHint\": \"Saisissez l'URL de base de votre serveur\",\n      \"cloudWSURL\": \"URL du websocket\",\n      \"cloudWSURLHint\": \"Saisissez l'adresse websocket de votre serveur\",\n      \"restartApp\": \"Redémarer\",\n      \"restartAppTip\": \"Redémarrez l'application pour que les modifications prennent effet. Veuillez noter que cela pourrait déconnecter votre compte actuel.\",\n      \"changeServerTip\": \"Après avoir changé de serveur, vous devez cliquer sur le bouton de redémarrer pour que les modifications prennent effet\",\n      \"enableEncryptPrompt\": \"Activez le chiffrement pour sécuriser vos données avec cette clé. Rangez-la en toute sécurité ; une fois activé, il ne peut pas être désactivé. En cas de perte, vos données deviennent irrécupérables. Cliquez pour copier\",\n      \"inputEncryptPrompt\": \"Veuillez saisir votre mot ou phrase de passe pour\",\n      \"clickToCopySecret\": \"Cliquez pour copier le mot ou la phrase de passe\",\n      \"configServerSetting\": \"Configurez les paramètres de votre serveur\",\n      \"configServerGuide\": \"Après avoir sélectionné « Démarrage rapide », accédez à « Paramètres » puis « Paramètres Cloud » pour configurer votre serveur auto-hébergé.\",\n      \"inputTextFieldHint\": \"Votre mot ou phrase de passe\",\n      \"historicalUserList\": \"Historique de connexion d'utilisateurs\",\n      \"historicalUserListTooltip\": \"Cette liste affiche vos comptes anonymes. Vous pouvez cliquer sur un compte pour afficher ses détails. Les comptes anonymes sont créés en cliquant sur le bouton « Commencer »\",\n      \"openHistoricalUser\": \"Cliquez pour ouvrir le compte anonyme\",\n      \"customPathPrompt\": \"Le stockage du dossier de données @:appName dans un dossier synchronisé avec le cloud tel que Google Drive peut présenter des risques. Si la base de données de ce dossier est consultée ou modifiée à partir de plusieurs emplacements en même temps, cela peut entraîner des conflits de synchronisation et une corruption potentielle des données.\",\n      \"importAppFlowyData\": \"Importer des données à partir du dossier @:appName externe\",\n      \"importingAppFlowyDataTip\": \"L'importation des données est en cours. Veuillez ne pas fermer l'application\",\n      \"importAppFlowyDataDescription\": \"Copiez les données d'un dossier de données @:appName externe et importez-les dans le dossier de données @:appName actuel\",\n      \"importSuccess\": \"Importation réussie du dossier de données @:appName\",\n      \"importFailed\": \"L'importation du dossier de données @:appName a échoué\",\n      \"importGuide\": \"Pour plus de détails, veuillez consulter le document référencé\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Activer les notifications\",\n        \"hint\": \"Désactivez-la pour empêcher l'affichage des notifications locales.\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"Réinitialiser ce paramètre\",\n      \"fontFamily\": {\n        \"label\": \"Famille de polices\",\n        \"search\": \"Recherche\"\n      },\n      \"themeMode\": {\n        \"label\": \" Mode du Thème\",\n        \"light\": \"Mode clair\",\n        \"dark\": \"Mode sombre\",\n        \"system\": \"S'adapter au système\"\n      },\n      \"documentSettings\": {\n        \"cursorColor\": \"Couleur du curseur du document\",\n        \"selectionColor\": \"Couleur de sélection du document\",\n        \"hexEmptyError\": \"La couleur hexadécimale ne peut pas être vide\",\n        \"hexLengthError\": \"La valeur hexadécimale doit comporter 6 chiffres\",\n        \"hexInvalidError\": \"Valeur hexadécimale invalide\",\n        \"opacityEmptyError\": \"L'opacité ne peut pas être vide\",\n        \"opacityRangeError\": \"L'opacité doit être comprise entre 1 et 100\",\n        \"app\": \"Application\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"Appliquer\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Orientation de la mise en page\",\n        \"hint\": \"Contrôlez l'orientation du contenu sur votre écran, de gauche à droite ou de droite à gauche.\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\"\n      },\n      \"textDirection\": {\n        \"label\": \"Direction du texte par défaut\",\n        \"hint\": \"Spécifiez si le texte doit commencer à gauche ou à droite par défaut.\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\",\n        \"auto\": \"AUTO\",\n        \"fallback\": \"Identique au sens de mise en page\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Téléverser\",\n        \"uploadTheme\": \"Téléverser le thème\",\n        \"description\": \"Téléversez votre propre thème @:appName en utilisant le bouton ci-dessous.\",\n        \"loading\": \"Veuillez patienter pendant que nous validons et téléchargeons votre thème...\",\n        \"uploadSuccess\": \"Votre thème a été téléversé avec succès\",\n        \"deletionFailure\": \"Échec de la suppression du thème. Essayez de le supprimer manuellement.\",\n        \"filePickerDialogTitle\": \"Choisissez un fichier .flowy_plugin\",\n        \"urlUploadFailure\": \"Échec de l'ouverture de l'URL : {}\",\n        \"failure\": \"Le thème qui a été téléchargé avait un format non valide.\"\n      },\n      \"theme\": \"Thème\",\n      \"builtInsLabel\": \"Thèmes intégrés\",\n      \"pluginsLabel\": \"Plugins\",\n      \"dateFormat\": {\n        \"label\": \"Format de la date\",\n        \"local\": \"Local\",\n        \"us\": \"US\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"Convivial\",\n        \"dmy\": \"J/M/A\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Format de l'heure\",\n        \"twelveHour\": \"Douze heures\",\n        \"twentyFourHour\": \"Vingt-quatre heures\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"Afficher la boîte de dialogue de nommage lors de la création d'une page\"\n    },\n    \"files\": {\n      \"copy\": \"Copie\",\n      \"defaultLocation\": \"Lire les fichiers et l'emplacement de stockage des données\",\n      \"exportData\": \"Exportez vos données\",\n      \"doubleTapToCopy\": \"Appuyez deux fois pour copier le chemin\",\n      \"restoreLocation\": \"Restaurer le chemin par défaut d'@:appName\",\n      \"customizeLocation\": \"Ouvrir un autre dossier\",\n      \"restartApp\": \"Veuillez redémarrer l'application pour que les modifications prennent effet.\",\n      \"exportDatabase\": \"Exporter la base de données\",\n      \"selectFiles\": \"Sélectionnez les fichiers qui doivent être exportés\",\n      \"selectAll\": \"Tout sélectionner\",\n      \"deselectAll\": \"Tout déselectionner\",\n      \"createNewFolder\": \"Créer un nouveau dossier\",\n      \"createNewFolderDesc\": \"Dites-nous où vous souhaitez stocker vos données\",\n      \"defineWhereYourDataIsStored\": \"Définissez où vos données sont stockées\",\n      \"open\": \"Ouvrir\",\n      \"openFolder\": \"Ouvrir un dossier existant\",\n      \"openFolderDesc\": \"Lisez-le et écrivez-le dans votre dossier @:appName existant\",\n      \"folderHintText\": \"Nom de dossier\",\n      \"location\": \"Création d'un nouveau dossier\",\n      \"locationDesc\": \"Choisissez un nom pour votre dossier de données @:appName\",\n      \"browser\": \"Parcourir\",\n      \"create\": \"Créer\",\n      \"set\": \"Définir\",\n      \"folderPath\": \"Chemin pour stocker votre dossier\",\n      \"locationCannotBeEmpty\": \"Le chemin ne peut pas être vide\",\n      \"pathCopiedSnackbar\": \"Chemin de stockage des fichiers copié dans le presse-papier !\",\n      \"changeLocationTooltips\": \"Changer le répertoire de données\",\n      \"change\": \"Changer\",\n      \"openLocationTooltips\": \"Ouvrir un autre répertoire de données\",\n      \"openCurrentDataFolder\": \"Ouvrir le répertoire de données actuel\",\n      \"recoverLocationTooltips\": \"Réinitialiser au répertoire de données par défaut d'@:appName\",\n      \"exportFileSuccess\": \"Exporter le fichier avec succès !\",\n      \"exportFileFail\": \"Échec de l'export du fichier !\",\n      \"export\": \"Exporter\"\n    },\n    \"user\": {\n      \"name\": \"Nom\",\n      \"email\": \"Courriel\",\n      \"tooltipSelectIcon\": \"Sélectionner l'icône\",\n      \"selectAnIcon\": \"Sélectionnez une icône\",\n      \"pleaseInputYourOpenAIKey\": \"Veuillez entrer votre clé AI\",\n      \"clickToLogout\": \"Cliquez pour déconnecter l'utilisateur actuel\",\n      \"pleaseInputYourStabilityAIKey\": \"Veuillez saisir votre clé de Stability AI\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"Informations personnelles\",\n      \"username\": \"Nom d'utilisateur\",\n      \"usernameEmptyError\": \"Le nom d'utilisateur ne peut pas être vide\",\n      \"about\": \"À propos\",\n      \"pushNotifications\": \"Notifications push\",\n      \"support\": \"Support\",\n      \"joinDiscord\": \"Rejoignez-nous sur Discord\",\n      \"privacyPolicy\": \"Politique de Confidentialité\",\n      \"userAgreement\": \"Accord de l'utilisateur\",\n      \"termsAndConditions\": \"Termes et conditions\",\n      \"userprofileError\": \"Échec du chargement du profil utilisateur\",\n      \"userprofileErrorDescription\": \"Veuillez essayer de vous déconnecter et de vous reconnecter pour vérifier si le problème persiste.\",\n      \"selectLayout\": \"Sélectionner la mise en page\",\n      \"selectStartingDay\": \"Sélectionnez le jour de début\",\n      \"version\": \"Version\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"Raccourcis\",\n      \"command\": \"Commande\",\n      \"keyBinding\": \"Racourcis clavier\",\n      \"addNewCommand\": \"Ajouter une Nouvelle Commande\",\n      \"updateShortcutStep\": \"Appuyez sur la combinaison de touches souhaitée et appuyez sur ENTER\",\n      \"shortcutIsAlreadyUsed\": \"Ce raccourci est déjà utilisé pour : {conflict}\",\n      \"resetToDefault\": \"Réinitialiser les raccourcis clavier par défaut\",\n      \"couldNotLoadErrorMsg\": \"Impossible de charger les raccourcis, réessayez\",\n      \"couldNotSaveErrorMsg\": \"Impossible d'enregistrer les raccourcis. Réessayez\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Voulez-vous vraiment supprimer cette vue ?\",\n    \"createView\": \"Nouveau\",\n    \"title\": {\n      \"placeholder\": \"Sans titre\"\n    },\n    \"settings\": {\n      \"filter\": \"Filtrer\",\n      \"sort\": \"Trier\",\n      \"sortBy\": \"Trier par\",\n      \"properties\": \"Propriétés\",\n      \"reorderPropertiesTooltip\": \"Faites glisser pour réorganiser les propriétés\",\n      \"group\": \"Groupe\",\n      \"addFilter\": \"Ajouter un filtre\",\n      \"deleteFilter\": \"Supprimer le filtre\",\n      \"filterBy\": \"Filtrer par...\",\n      \"typeAValue\": \"Tapez une valeur...\",\n      \"layout\": \"Mise en page\",\n      \"databaseLayout\": \"Mise en page\",\n      \"viewList\": {\n        \"zero\": \"0 vue\",\n        \"one\": \"{count} vue\",\n        \"other\": \"{count} vues\"\n      },\n      \"editView\": \"Modifier vue\",\n      \"boardSettings\": \"Paramètres du tableau\",\n      \"calendarSettings\": \"Paramètres du calendrier\",\n      \"createView\": \"Nouvelle vue\",\n      \"duplicateView\": \"Dupliquer la vue\",\n      \"deleteView\": \"Supprimer la vue\",\n      \"numberOfVisibleFields\": \"{} affiché(s)\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Contient\",\n      \"doesNotContain\": \"Ne contient pas\",\n      \"endsWith\": \"Se termine par\",\n      \"startWith\": \"Commence par\",\n      \"is\": \"Est\",\n      \"isNot\": \"N'est pas\",\n      \"isEmpty\": \"Est vide\",\n      \"isNotEmpty\": \"N'est pas vide\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Pas\",\n        \"startWith\": \"Commence par\",\n        \"endWith\": \"Se termine par\",\n        \"isEmpty\": \"est vide\",\n        \"isNotEmpty\": \"n'est pas vide\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Coché\",\n      \"isUnchecked\": \"Décoché\",\n      \"choicechipPrefix\": {\n        \"is\": \"est\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"fait\",\n      \"isIncomplted\": \"pas fait\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"Est\",\n      \"isNot\": \"N'est pas\",\n      \"contains\": \"Contient\",\n      \"doesNotContain\": \"Ne contient pas\",\n      \"isEmpty\": \"Est vide\",\n      \"isNotEmpty\": \"N'est pas vide\"\n    },\n    \"dateFilter\": {\n      \"is\": \"Est\",\n      \"before\": \"Est avant\",\n      \"after\": \"Est après\",\n      \"onOrBefore\": \"Est le ou avant\",\n      \"onOrAfter\": \"Est le ou après\",\n      \"between\": \"Est entre\",\n      \"empty\": \"Est vide\",\n      \"notEmpty\": \"N'est pas vide\"\n    },\n    \"field\": {\n      \"hide\": \"Cacher\",\n      \"show\": \"Afficher\",\n      \"insertLeft\": \"Insérer à gauche\",\n      \"insertRight\": \"Insérer à droite\",\n      \"duplicate\": \"Dupliquer\",\n      \"delete\": \"Supprimer\",\n      \"textFieldName\": \"Texte\",\n      \"checkboxFieldName\": \"Case à cocher\",\n      \"dateFieldName\": \"Date\",\n      \"updatedAtFieldName\": \"Dernière modification\",\n      \"createdAtFieldName\": \"Créé le\",\n      \"numberFieldName\": \"Nombre\",\n      \"singleSelectFieldName\": \"Sélectionner\",\n      \"multiSelectFieldName\": \"Sélection multiple\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Liste de contrôle\",\n      \"numberFormat\": \"Format de nombre\",\n      \"dateFormat\": \"Format de la date\",\n      \"includeTime\": \"Inclure l'heure\",\n      \"isRange\": \"Date de fin\",\n      \"dateFormatFriendly\": \"Mois Jour, Année\",\n      \"dateFormatISO\": \"Année-Mois-Jour\",\n      \"dateFormatLocal\": \"Mois/Jour/Année\",\n      \"dateFormatUS\": \"Année/Mois/Jour\",\n      \"dateFormatDayMonthYear\": \"Jour/Mois/Année\",\n      \"timeFormat\": \"Format de l'heure\",\n      \"invalidTimeFormat\": \"Format invalide\",\n      \"timeFormatTwelveHour\": \"12 heures\",\n      \"timeFormatTwentyFourHour\": \"24 heures\",\n      \"clearDate\": \"Effacer la date\",\n      \"dateTime\": \"Date et heure\",\n      \"startDateTime\": \"Date et heure de début\",\n      \"endDateTime\": \"Date et heure de fin\",\n      \"failedToLoadDate\": \"Échec du chargement de la valeur de la date\",\n      \"selectTime\": \"Sélectionnez l'heure\",\n      \"selectDate\": \"Sélectionner une date\",\n      \"visibility\": \"Visibilité\",\n      \"propertyType\": \"Type de propriété\",\n      \"addSelectOption\": \"Ajouter une option\",\n      \"typeANewOption\": \"Saisissez une nouvelle option\",\n      \"optionTitle\": \"Choix\",\n      \"addOption\": \"Ajouter un choix\",\n      \"editProperty\": \"Modifier la propriété\",\n      \"newProperty\": \"Nouvelle propriété\",\n      \"deleteFieldPromptMessage\": \"Vous voulez supprimer cette propriété ?\",\n      \"newColumn\": \"Nouvelle colonne\",\n      \"format\": \"Format\",\n      \"reminderOnDateTooltip\": \"Cette cellule a un rappel programmé\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Ajouter un nouveau champ\",\n      \"fieldDragElementTooltip\": \"Cliquez pour ouvrir le menu\",\n      \"showHiddenFields\": {\n        \"one\": \"Afficher {count} champ masqué\",\n        \"many\": \"Afficher {count} champs masqués\",\n        \"other\": \"Afficher {count} champs masqués\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"Cacher {count} champ caché\",\n        \"many\": \"Cacher {count} champs masqués\",\n        \"other\": \"Cacher {count} champs masqués\"\n      }\n    },\n    \"sort\": {\n      \"ascending\": \"Ascendant\",\n      \"descending\": \"Descendant\",\n      \"deleteAllSorts\": \"Supprimer tous les tris\",\n      \"addSort\": \"Ajouter un tri\",\n      \"deleteSort\": \"Supprimer le tri\"\n    },\n    \"row\": {\n      \"duplicate\": \"Dupliquer\",\n      \"delete\": \"Supprimer\",\n      \"titlePlaceholder\": \"Sans titre\",\n      \"textPlaceholder\": \"Vide\",\n      \"copyProperty\": \"Propriété copiée dans le presse-papiers\",\n      \"count\": \"Compte\",\n      \"newRow\": \"Nouvelle ligne\",\n      \"action\": \"Action\",\n      \"add\": \"Cliquez sur ajouter ci-dessous\",\n      \"drag\": \"Glisser pour déplacer\",\n      \"dragAndClick\": \"Faites glisser pour déplacer, cliquez pour ouvrir le menu\",\n      \"insertRecordAbove\": \"Insérer l'enregistrement ci-dessus\",\n      \"insertRecordBelow\": \"Insérer l'enregistrement ci-dessous\"\n    },\n    \"selectOption\": {\n      \"create\": \"Créer\",\n      \"purpleColor\": \"Violet\",\n      \"pinkColor\": \"Rose\",\n      \"lightPinkColor\": \"Rose clair\",\n      \"orangeColor\": \"Orange\",\n      \"yellowColor\": \"Jaune\",\n      \"limeColor\": \"Lime\",\n      \"greenColor\": \"Vert\",\n      \"aquaColor\": \"Turquoise\",\n      \"blueColor\": \"Bleu\",\n      \"deleteTag\": \"Supprimer l'étiquette\",\n      \"colorPanelTitle\": \"Couleurs\",\n      \"panelTitle\": \"Sélectionnez une option ou créez-en une\",\n      \"searchOption\": \"Rechercher une option\",\n      \"searchOrCreateOption\": \"Rechercher ou créer une option...\",\n      \"createNew\": \"Créer une nouvelle\",\n      \"orSelectOne\": \"Ou sélectionnez une option\",\n      \"typeANewOption\": \"Saisissez une nouvelle option\",\n      \"tagName\": \"Nom de l'étiquette\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"Description de la tâche\",\n      \"addNew\": \"Ajouter un élément\",\n      \"submitNewTask\": \"Créer\",\n      \"hideComplete\": \"Cacher les tâches terminées\",\n      \"showComplete\": \"Afficher toutes les tâches\"\n    },\n    \"url\": {\n      \"launch\": \"Ouvrir dans le navigateur\",\n      \"copy\": \"Copier l'URL\"\n    },\n    \"menuName\": \"Grille\",\n    \"referencedGridPrefix\": \"Vue\",\n    \"calculate\": \"Calculer\",\n    \"calculationTypeLabel\": {\n      \"none\": \"Aucun\",\n      \"average\": \"Moyenne\",\n      \"max\": \"Maximum\",\n      \"median\": \"Médiane\",\n      \"min\": \"Minimum\",\n      \"sum\": \"Somme\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"Document\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Sélectionnez un tableau à lier\",\n        \"createANewBoard\": \"Créer un nouveau tableau\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Sélectionnez une grille à lier\",\n        \"createANewGrid\": \"Créer une nouvelle Grille\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Sélectionnez un calendrier à lier\",\n        \"createANewCalendar\": \"Créer un nouveau calendrier\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"Sélectionnez un Document vers lequel créer un lien\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Contour\",\n      \"codeBlock\": \"Bloc de code\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Tableau référencé\",\n      \"referencedGrid\": \"Grille référencée\",\n      \"referencedCalendar\": \"Calendrier référencé\",\n      \"referencedDocument\": \"Document référencé\",\n      \"autoGeneratorMenuItemName\": \"Rédacteur AI\",\n      \"autoGeneratorTitleName\": \"AI : Demandez à l'IA d'écrire quelque chose...\",\n      \"autoGeneratorLearnMore\": \"Apprendre encore plus\",\n      \"autoGeneratorGenerate\": \"Générer\",\n      \"autoGeneratorHintText\": \"Demandez à AI...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Impossible d'obtenir la clé AI\",\n      \"autoGeneratorRewrite\": \"Réécrire\",\n      \"smartEdit\": \"Assistants IA\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"Corriger l'orthographe\",\n      \"warning\": \"⚠️ Les réponses de l'IA peuvent être inexactes ou trompeuses.\",\n      \"smartEditSummarize\": \"Résumer\",\n      \"smartEditImproveWriting\": \"Améliorer l'écriture\",\n      \"smartEditMakeLonger\": \"Rallonger\",\n      \"smartEditCouldNotFetchResult\": \"Impossible de récupérer le résultat d'AI\",\n      \"smartEditCouldNotFetchKey\": \"Impossible de récupérer la clé AI\",\n      \"smartEditDisabled\": \"Connectez AI dans les paramètres\",\n      \"discardResponse\": \"Voulez-vous supprimer les réponses de l'IA ?\",\n      \"createInlineMathEquation\": \"Créer une équation\",\n      \"fonts\": \"Polices\",\n      \"emoji\": \"Emoji\",\n      \"toggleList\": \"Liste pliable\",\n      \"quoteList\": \"Liste de citations\",\n      \"numberedList\": \"Liste numérotée\",\n      \"bulletedList\": \"Liste à puces\",\n      \"todoList\": \"Liste de tâches\",\n      \"callout\": \"Encadré\",\n      \"cover\": {\n        \"changeCover\": \"Changer la couverture\",\n        \"colors\": \"Couleurs\",\n        \"images\": \"Images\",\n        \"clearAll\": \"Tout effacer\",\n        \"abstract\": \"Abstrait\",\n        \"addCover\": \"Ajouter une couverture\",\n        \"addLocalImage\": \"Ajouter une image locale\",\n        \"invalidImageUrl\": \"URL d'image non valide\",\n        \"failedToAddImageToGallery\": \"Impossible d'ajouter l'image à la galerie\",\n        \"enterImageUrl\": \"Entrez l'URL de l'image\",\n        \"add\": \"Ajouter\",\n        \"back\": \"Dos\",\n        \"saveToGallery\": \"Sauvegarder dans la gallerie\",\n        \"removeIcon\": \"Supprimer l'icône\",\n        \"pasteImageUrl\": \"Coller l'URL de l'image\",\n        \"or\": \"OU\",\n        \"pickFromFiles\": \"Choisissez parmi les fichiers\",\n        \"couldNotFetchImage\": \"Impossible de récupérer l'image\",\n        \"imageSavingFailed\": \"Échec de l'enregistrement de l'image\",\n        \"addIcon\": \"Ajouter une icône\",\n        \"changeIcon\": \"Changer l'icône\",\n        \"coverRemoveAlert\": \"Il sera retiré de la couverture après sa suppression.\",\n        \"alertDialogConfirmation\": \"Voulez-vous vraiment continuer?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"Équation mathématique\",\n        \"addMathEquation\": \"Ajouter une équation mathématique\",\n        \"editMathEquation\": \"Modifier l'équation mathématique\"\n      },\n      \"optionAction\": {\n        \"click\": \"Cliquez sur\",\n        \"toOpenMenu\": \" pour ouvrir le menu\",\n        \"delete\": \"Supprimer\",\n        \"duplicate\": \"Dupliquer\",\n        \"turnInto\": \"Changer en\",\n        \"moveUp\": \"Déplacer vers le haut\",\n        \"moveDown\": \"Descendre\",\n        \"color\": \"Couleur\",\n        \"align\": \"Aligner\",\n        \"left\": \"Gauche\",\n        \"center\": \"Centre\",\n        \"right\": \"Droite\",\n        \"defaultColor\": \"Défaut\"\n      },\n      \"image\": {\n        \"addAnImage\": \"Ajouter une image\",\n        \"copiedToPasteBoard\": \"Le lien de l'image a été copié dans le presse-papiers\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"Le lien a été copié dans le presse-papier\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Ajoutez des titres pour créer une table des matières.\"\n      },\n      \"table\": {\n        \"addAfter\": \"Ajouter après\",\n        \"addBefore\": \"Ajouter avant\",\n        \"delete\": \"Supprimer\",\n        \"clear\": \"Éffacer contenu\",\n        \"duplicate\": \"Dupliquer\",\n        \"bgColor\": \"Couleur de fond\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Copier\",\n        \"cut\": \"Couper\",\n        \"paste\": \"Coller\"\n      },\n      \"action\": \"Actions\",\n      \"database\": {\n        \"selectDataSource\": \"Sélectionnez la source de données\",\n        \"noDataSource\": \"Aucune source de données\",\n        \"selectADataSource\": \"Sélectionnez une source de données\",\n        \"toContinue\": \"pour continuer\",\n        \"newDatabase\": \"Nouvelle Base de données\",\n        \"linkToDatabase\": \"Lien vers la Base de données\"\n      },\n      \"date\": \"Date\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Tapez '/' pour les commandes\"\n    },\n    \"title\": {\n      \"placeholder\": \"Sans titre\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Cliquez pour ajouter une image\",\n      \"upload\": {\n        \"label\": \"Téléverser\",\n        \"placeholder\": \"Cliquez pour téléverser l'image\"\n      },\n      \"url\": {\n        \"label\": \"URL de l'image\",\n        \"placeholder\": \"Entrez l'URL de l'image\"\n      },\n      \"ai\": {\n        \"label\": \"Générer une image à partir d'AI\",\n        \"placeholder\": \"Veuillez saisir l'invite pour qu'AI génère l'image\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Générer une image à partir de Stability AI\",\n        \"placeholder\": \"Veuillez saisir l'invite permettant à Stability AI de générer une image.\"\n      },\n      \"support\": \"La limite de taille d'image est de 5 Mo. Formats pris en charge : JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Image invalide\",\n        \"invalidImageSize\": \"La taille de l'image doit être inférieure à 5 Mo\",\n        \"invalidImageFormat\": \"Le format d'image n'est pas pris en charge. Formats pris en charge : JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"URL d'image non valide\"\n      },\n      \"embedLink\": {\n        \"label\": \"Lien intégré\",\n        \"placeholder\": \"Collez ou saisissez un lien d'image\"\n      },\n      \"unsplash\": {\n        \"label\": \"Unsplash\"\n      },\n      \"searchForAnImage\": \"Rechercher une image\",\n      \"pleaseInputYourOpenAIKey\": \"veuillez saisir votre clé AI dans la page Paramètres\",\n      \"saveImageToGallery\": \"Enregistrer l'image\",\n      \"failedToAddImageToGallery\": \"Échec de l'ajout d'une image à la galerie\",\n      \"successToAddImageToGallery\": \"Image ajoutée à la galerie avec succès\",\n      \"unableToLoadImage\": \"Impossible de charger l'image\",\n      \"maximumImageSize\": \"La taille d'image maximale est 10Mo\",\n      \"uploadImageErrorImageSizeTooBig\": \"L'image doit faire moins de 10Mo\",\n      \"pleaseInputYourStabilityAIKey\": \"veuillez saisir votre clé Stability AI dans la page Paramètres\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Langue\",\n        \"placeholder\": \"Choisir la langue\"\n      }\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Coller ou saisir un lien\",\n      \"openInNewTab\": \"Ouvrir dans un nouvel onglet\",\n      \"copyLink\": \"Copier le lien\",\n      \"removeLink\": \"Supprimer le lien\",\n      \"url\": {\n        \"label\": \"URL du lien\",\n        \"placeholder\": \"Entrez l'URL du lien\"\n      },\n      \"title\": {\n        \"label\": \"Titre du lien\",\n        \"placeholder\": \"Entrez le titre du lien\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"Mentionner une personne ou une page ou une date...\",\n      \"page\": {\n        \"label\": \"Lien vers la page\",\n        \"tooltip\": \"Cliquez pour ouvrir la page\"\n      },\n      \"deleted\": \"Supprimé\",\n      \"deletedContent\": \"Ce document n'existe pas ou a été supprimé\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"Réinitialiser aux valeurs par défaut\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"La version actuelle ne prend pas en charge ce bloc.\",\n      \"blockContentHasBeenCopied\": \"Le contenu du bloc a été copié.\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"Nouveau\",\n      \"renameGroupTooltip\": \"Appuyez pour renommer le groupe\",\n      \"createNewColumn\": \"Ajouter un nouveau groupe\",\n      \"addToColumnTopTooltip\": \"Ajouter une nouvelle carte en haut\",\n      \"addToColumnBottomTooltip\": \"Ajouter une nouvelle carte en bas\",\n      \"renameColumn\": \"Renommer\",\n      \"hideColumn\": \"Cacher\",\n      \"newGroup\": \"Nouveau groupe\",\n      \"deleteColumn\": \"Supprimer\",\n      \"deleteColumnConfirmation\": \"Cela supprimera ce groupe et toutes les cartes qu'il contient. \\nÊtes-vous sûr de vouloir continuer?\",\n      \"groupActions\": \"Actions de groupe\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"Groupes cachés\",\n      \"collapseTooltip\": \"Cacher les groupes cachés\",\n      \"expandTooltip\": \"Afficher les groupes cachés\"\n    },\n    \"cardDetail\": \"Détail de la Carte\",\n    \"cardActions\": \"Actions des Cartes\",\n    \"cardDuplicated\": \"La carte a été dupliquée\",\n    \"cardDeleted\": \"La carte a été supprimée\",\n    \"showOnCard\": \"Afficher les détails de la carte\",\n    \"setting\": \"Paramètre\",\n    \"propertyName\": \"Nom de la propriété\",\n    \"menuName\": \"Tableau\",\n    \"showUngrouped\": \"Afficher les éléments non regroupés\",\n    \"ungroupedButtonText\": \"Non groupé\",\n    \"ungroupedButtonTooltip\": \"Contient des cartes qui n'appartiennent à aucun groupe\",\n    \"ungroupedItemsTitle\": \"Cliquez pour ajouter au tableau\",\n    \"groupBy\": \"Regrouper par\",\n    \"referencedBoardPrefix\": \"Vue\",\n    \"notesTooltip\": \"Notes à l'intérieur\",\n    \"mobile\": {\n      \"editURL\": \"Modifier l'URL\",\n      \"showGroup\": \"Afficher le groupe\",\n      \"showGroupContent\": \"Êtes-vous sûr de vouloir afficher ce groupe sur le tableau ?\",\n      \"failedToLoad\": \"Échec du chargement de la vue du tableau\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Calendrier\",\n    \"defaultNewCalendarTitle\": \"Sans titre\",\n    \"newEventButtonTooltip\": \"Ajouter un nouvel événement\",\n    \"navigation\": {\n      \"today\": \"Aujourd'hui\",\n      \"jumpToday\": \"Aller à Aujourd'hui\",\n      \"previousMonth\": \"Mois précédent\",\n      \"nextMonth\": \"Mois prochain\"\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"Pas d'événements\",\n      \"emptyBody\": \"Cliquez sur le bouton plus pour créer un événement à cette date.\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Afficher les numéros de semaine\",\n      \"showWeekends\": \"Afficher les week-ends\",\n      \"firstDayOfWeek\": \"Commencer la semaine le\",\n      \"layoutDateField\": \"Calendrier de mise en page par\",\n      \"changeLayoutDateField\": \"Modifier le champ de mise en page\",\n      \"noDateTitle\": \"Pas de date\",\n      \"unscheduledEventsTitle\": \"Événements non planifiés\",\n      \"clickToAdd\": \"Cliquez pour ajouter au calendrier\",\n      \"name\": \"Disposition du calendrier\",\n      \"noDateHint\": \"Les événements non planifiés s'afficheront ici\"\n    },\n    \"referencedCalendarPrefix\": \"Vue\",\n    \"quickJumpYear\": \"Sauter à\"\n  },\n  \"errorDialog\": {\n    \"title\": \"Erreur @:appName\",\n    \"howToFixFallback\": \"Nous sommes désolés pour le désagrément ! Soumettez un problème sur notre page GitHub qui décrit votre erreur.\",\n    \"github\": \"Afficher sur GitHub\"\n  },\n  \"search\": {\n    \"label\": \"Recherche\",\n    \"placeholder\": {\n      \"actions\": \"Actions de recherche...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Copié !\",\n      \"fail\": \"Impossible de copier\"\n    }\n  },\n  \"unSupportBlock\": \"La version actuelle ne prend pas en charge ce bloc.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Voulez-vous vraiment supprimer le {pageType} ?\",\n    \"deleteContentCaption\": \"si vous supprimez ce {pageType}, vous pouvez le restaurer à partir de la corbeille.\"\n  },\n  \"colors\": {\n    \"custom\": \"Personnalisé\",\n    \"default\": \"Défaut\",\n    \"red\": \"Rouge\",\n    \"orange\": \"Orange\",\n    \"yellow\": \"Jaune\",\n    \"green\": \"Vert\",\n    \"blue\": \"Bleu\",\n    \"purple\": \"Violet\",\n    \"pink\": \"Rose\",\n    \"brown\": \"Marron\",\n    \"gray\": \"Gris\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"Émoji\",\n    \"search\": \"Chercher un émoji\",\n    \"noRecent\": \"Aucun émoji récent\",\n    \"noEmojiFound\": \"Aucun émoji trouvé\",\n    \"filter\": \"Filtrer\",\n    \"random\": \"Aléatoire\",\n    \"selectSkinTone\": \"Choisir le teint de la peau\",\n    \"remove\": \"Supprimer l'émoji\",\n    \"categories\": {\n      \"smileys\": \"Smileys & émoticônes\",\n      \"people\": \"Personnes & corps\",\n      \"animals\": \"Animaux & Nature\",\n      \"food\": \"Nourriture & Boisson\",\n      \"activities\": \"Activités\",\n      \"places\": \"Voyages & Lieux\",\n      \"objects\": \"Objets\",\n      \"symbols\": \"Symboles\",\n      \"flags\": \"Drapeaux\",\n      \"nature\": \"Nature\",\n      \"frequentlyUsed\": \"Fréquemment utilisés\"\n    },\n    \"skinTone\": {\n      \"default\": \"Défaut\",\n      \"light\": \"Claire\",\n      \"mediumLight\": \"Moyennement claire\",\n      \"medium\": \"Moyen\",\n      \"mediumDark\": \"Moyennement foncé\",\n      \"dark\": \"Foncé\"\n    }\n  },\n  \"inlineActions\": {\n    \"noResults\": \"Aucun résultat\",\n    \"pageReference\": \"Référence de page\",\n    \"docReference\": \"Référence de document\",\n    \"boardReference\": \"Référence du tableau\",\n    \"calReference\": \"Référence du calendrier\",\n    \"gridReference\": \"Référence de grille\",\n    \"date\": \"Date\",\n    \"reminder\": {\n      \"groupTitle\": \"Rappel\",\n      \"shortKeyword\": \"rappeler\"\n    }\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"Modifier le format de la date et de l'heure dans les paramètres\",\n    \"dateFormat\": \"Format de date\",\n    \"includeTime\": \"Inclure l'heure\",\n    \"isRange\": \"Date de fin\",\n    \"timeFormat\": \"Format de l'heure\",\n    \"clearDate\": \"Effacer la date\",\n    \"reminderLabel\": \"Rappel\",\n    \"selectReminder\": \"Sélectionnez un rappel\",\n    \"reminderOptions\": {\n      \"none\": \"Aucun\",\n      \"atTimeOfEvent\": \"Heure de l'événement\",\n      \"fiveMinsBefore\": \"5 minutes avant\",\n      \"tenMinsBefore\": \"10 minutes avant\",\n      \"fifteenMinsBefore\": \"15 minutes avant\",\n      \"thirtyMinsBefore\": \"30 minutes avant\",\n      \"oneHourBefore\": \"1 heure avant\",\n      \"twoHoursBefore\": \"2 heures avant\",\n      \"onDayOfEvent\": \"Le jour de l'événement\",\n      \"oneDayBefore\": \"1 jour avant\",\n      \"twoDaysBefore\": \"2 jours avant\",\n      \"oneWeekBefore\": \"1 semaine avant\",\n      \"custom\": \"Personnalisé\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"Hier\",\n    \"today\": \"Aujourd'hui\",\n    \"tomorrow\": \"Demain\",\n    \"oneWeek\": \"1 semaine\"\n  },\n  \"notificationHub\": {\n    \"title\": \"Notifications\",\n    \"mobile\": {\n      \"title\": \"Mises à jour\"\n    },\n    \"emptyTitle\": \"Vous êtes à jour !\",\n    \"emptyBody\": \"Aucune notification ou action en attente. Profitez du calme.\",\n    \"tabs\": {\n      \"inbox\": \"Boîte de réception\",\n      \"upcoming\": \"A venir\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"Tout marquer comme lu\",\n      \"showAll\": \"Tous\",\n      \"showUnreads\": \"Non lu\"\n    },\n    \"filters\": {\n      \"ascending\": \"Ascendant\",\n      \"descending\": \"Descendant\",\n      \"groupByDate\": \"Regrouper par date\",\n      \"showUnreadsOnly\": \"Afficher uniquement les éléments non lus\",\n      \"resetToDefault\": \"Réinitialiser aux valeurs par défaut\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"Rappel\",\n    \"message\": \"Pensez à vérifier cela avant d'oublier !\",\n    \"tooltipDelete\": \"Supprimer\",\n    \"tooltipMarkRead\": \"Marquer comme lu\",\n    \"tooltipMarkUnread\": \"Marquer comme non lu\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"Chercher\",\n    \"previousMatch\": \"Occurence précedente\",\n    \"nextMatch\": \"Prochaine occurence\",\n    \"close\": \"Fermer\",\n    \"replace\": \"Remplacer\",\n    \"replaceAll\": \"Tout remplacer\",\n    \"noResult\": \"Aucun résultat\",\n    \"caseSensitive\": \"Sensible à la casse\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"Nous sommes désolés\",\n    \"loadingViewError\": \"Nous rencontrons des difficultés pour charger cette vue. Veuillez vérifier votre connexion Internet, actualiser l'application et n'hésitez pas à contacter l'équipe si le problème persiste.\"\n  },\n  \"editor\": {\n    \"bold\": \"Gras\",\n    \"bulletedList\": \"Liste à puces\",\n    \"bulletedListShortForm\": \"Puces\",\n    \"checkbox\": \"Case à cocher\",\n    \"embedCode\": \"Code intégré\",\n    \"heading1\": \"H1\",\n    \"heading2\": \"H2\",\n    \"heading3\": \"H3\",\n    \"highlight\": \"Surligner\",\n    \"color\": \"Couleur\",\n    \"image\": \"Image\",\n    \"date\": \"Date\",\n    \"italic\": \"Italique\",\n    \"link\": \"Lien\",\n    \"numberedList\": \"Liste numérotée\",\n    \"numberedListShortForm\": \"Numéroté\",\n    \"quote\": \"Citation\",\n    \"strikethrough\": \"Barré\",\n    \"text\": \"Texte\",\n    \"underline\": \"Souligner\",\n    \"fontColorDefault\": \"Défaut\",\n    \"fontColorGray\": \"Gris\",\n    \"fontColorBrown\": \"Marron\",\n    \"fontColorOrange\": \"Orange\",\n    \"fontColorYellow\": \"Jaune\",\n    \"fontColorGreen\": \"Vert\",\n    \"fontColorBlue\": \"Bleu\",\n    \"fontColorPurple\": \"Violet\",\n    \"fontColorPink\": \"Rose\",\n    \"fontColorRed\": \"Rouge\",\n    \"backgroundColorDefault\": \"Fond par défaut\",\n    \"backgroundColorGray\": \"Fond gris\",\n    \"backgroundColorBrown\": \"Fond marron\",\n    \"backgroundColorOrange\": \"Fond orange\",\n    \"backgroundColorYellow\": \"Fond jaune\",\n    \"backgroundColorGreen\": \"Fond vert\",\n    \"backgroundColorBlue\": \"Fond bleu\",\n    \"backgroundColorPurple\": \"Fond violet\",\n    \"backgroundColorPink\": \"Fond rose\",\n    \"backgroundColorRed\": \"Fond rouge\",\n    \"done\": \"Fait\",\n    \"cancel\": \"Annuler\",\n    \"tint1\": \"Teinte 1\",\n    \"tint2\": \"Teinte 2\",\n    \"tint3\": \"Teinte 3\",\n    \"tint4\": \"Teinte 4\",\n    \"tint5\": \"Teinte 5\",\n    \"tint6\": \"Teinte 6\",\n    \"tint7\": \"Teinte 7\",\n    \"tint8\": \"Teinte 8\",\n    \"tint9\": \"Teinte 9\",\n    \"lightLightTint1\": \"Violet\",\n    \"lightLightTint2\": \"Rose\",\n    \"lightLightTint3\": \"Rose clair\",\n    \"lightLightTint4\": \"Orange\",\n    \"lightLightTint5\": \"Jaune\",\n    \"lightLightTint6\": \"Lime\",\n    \"lightLightTint7\": \"Vert\",\n    \"lightLightTint8\": \"Turquoise\",\n    \"lightLightTint9\": \"Bleu\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"Titre 1\",\n    \"mobileHeading2\": \"Titre 2\",\n    \"mobileHeading3\": \"Titre 3\",\n    \"textColor\": \"Couleur du texte\",\n    \"backgroundColor\": \"Couleur du fond\",\n    \"addYourLink\": \"Ajoutez votre lien\",\n    \"openLink\": \"Ouvrir le lien\",\n    \"copyLink\": \"Copier le lien\",\n    \"removeLink\": \"Supprimer le lien\",\n    \"editLink\": \"Modifier le lien\",\n    \"linkText\": \"Texte\",\n    \"linkTextHint\": \"Veuillez saisir du texte\",\n    \"linkAddressHint\": \"Veuillez entrer l'URL\",\n    \"highlightColor\": \"Couleur de surlignage\",\n    \"clearHighlightColor\": \"Effacer la couleur de surlignage\",\n    \"customColor\": \"Couleur personnalisée\",\n    \"hexValue\": \"Valeur hexadécimale\",\n    \"opacity\": \"Opacité\",\n    \"resetToDefaultColor\": \"Réinitialiser la couleur par défaut\",\n    \"ltr\": \"LTR\",\n    \"rtl\": \"RTL\",\n    \"auto\": \"Auto\",\n    \"cut\": \"Couper\",\n    \"copy\": \"Copier\",\n    \"paste\": \"Color\",\n    \"find\": \"Chercher\",\n    \"previousMatch\": \"Occurence précédente\",\n    \"nextMatch\": \"Occurence suivante\",\n    \"closeFind\": \"Fermer\",\n    \"replace\": \"Remplacer\",\n    \"replaceAll\": \"Tout remplacer\",\n    \"regex\": \"Regex\",\n    \"caseSensitive\": \"Sensible à la casse\",\n    \"uploadImage\": \"Téléverser une image\",\n    \"urlImage\": \"URL de l'image \",\n    \"incorrectLink\": \"Lien incorrect\",\n    \"upload\": \"Téléverser\",\n    \"chooseImage\": \"Choisissez une image\",\n    \"loading\": \"Chargement\",\n    \"imageLoadFailed\": \"Impossible de charger l'image\",\n    \"divider\": \"Séparateur\",\n    \"table\": \"Tableau\",\n    \"colAddBefore\": \"Ajouter avant\",\n    \"rowAddBefore\": \"Ajouter avant\",\n    \"colAddAfter\": \"Ajouter après\",\n    \"rowAddAfter\": \"Ajouter après\",\n    \"colRemove\": \"Retirer\",\n    \"rowRemove\": \"Retirer\",\n    \"colDuplicate\": \"Dupliquer\",\n    \"rowDuplicate\": \"Dupliquer\",\n    \"colClear\": \"Effacer le ontenu\",\n    \"rowClear\": \"Effacer le ontenu\",\n    \"slashPlaceHolder\": \"Tapez '/' pour insérer un bloc ou commencez à écrire\",\n    \"typeSomething\": \"Écrivez quelque chose...\",\n    \"toggleListShortForm\": \"Plier / Déplier\",\n    \"quoteListShortForm\": \"Citation\",\n    \"mathEquationShortForm\": \"Formule\",\n    \"codeBlockShortForm\": \"Code\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"Aucune page favorite\",\n    \"noFavoriteHintText\": \"Faites glisser la page vers la gauche pour l'ajouter à vos favoris\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"Entrez un / pour insérer un bloc ou commencez à taper\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"À faire\",\n    \"bulletList\": \"Liste\",\n    \"numberList\": \"Liste\",\n    \"quote\": \"Citation\",\n    \"heading\": \"Titre {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"Icône de page\",\n    \"language\": \"Langue\",\n    \"font\": \"Police \",\n    \"actions\": \"Actions\",\n    \"date\": \"Date\",\n    \"addField\": \"Ajouter un champ\",\n    \"userIcon\": \"Icône utilisateur\"\n  },\n  \"noLogFiles\": \"Il n'y a pas de log\"\n}"
  },
  {
    "path": "frontend/resources/translations/fr-FR.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Moi\",\n  \"welcomeText\": \"Bienvenue sur @:appName\",\n  \"welcomeTo\": \"Bienvenue à\",\n  \"githubStarText\": \"Favoriser sur GitHub\",\n  \"subscribeNewsletterText\": \"S'inscrire à la Newsletter\",\n  \"letsGoButtonText\": \"Allons-y\",\n  \"title\": \"Titre\",\n  \"youCanAlso\": \"Vous pouvez aussi\",\n  \"and\": \"et\",\n  \"failedToOpenUrl\": \"Échec de l'ouverture de l'URL: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Cliquez pour ajouter ci-dessous\",\n    \"addAboveCmd\": \"Alt+clic\",\n    \"addAboveMacCmd\": \"Option+clic\",\n    \"addAboveTooltip\": \"à ajouter au dessus\",\n    \"dragTooltip\": \"Glisser pour déplacer\",\n    \"openMenuTooltip\": \"Cliquez pour ouvrir le menu\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"S'inscrire\",\n    \"title\": \"Inscrivez-vous sur @:appName\",\n    \"getStartedText\": \"Commencer\",\n    \"emptyPasswordError\": \"Vous n'avez pas saisi votre mot de passe\",\n    \"repeatPasswordEmptyError\": \"Vous n'avez pas ressaisi votre mot de passe\",\n    \"unmatchedPasswordError\": \"Les deux mots de passe ne sont pas identiques\",\n    \"alreadyHaveAnAccount\": \"Avez-vous déjà un compte ?\",\n    \"emailHint\": \"Courriel\",\n    \"passwordHint\": \"Mot de passe\",\n    \"repeatPasswordHint\": \"Ressaisir votre mot de passe\",\n    \"signUpWith\": \"Se connecter avec :\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Connexion à @:appName\",\n    \"loginButtonText\": \"Connexion\",\n    \"loginStartWithAnonymous\": \"Lancer avec une session anonyme\",\n    \"continueAnonymousUser\": \"Continuer avec une session anonyme\",\n    \"continueWithLocalModel\": \"Continuer avec le modèle local\",\n    \"switchToAppFlowyCloud\": \"AppFlowy Cloud\",\n    \"anonymousMode\": \"Mode anonyme\",\n    \"buttonText\": \"Se connecter\",\n    \"signingInText\": \"Connexion en cours...\",\n    \"forgotPassword\": \"Mot de passe oublié ?\",\n    \"emailHint\": \"Courriel\",\n    \"passwordHint\": \"Mot de passe\",\n    \"dontHaveAnAccount\": \"Vous n'avez pas encore de compte ?\",\n    \"createAccount\": \"Créer un compte\",\n    \"repeatPasswordEmptyError\": \"Vous n'avez pas ressaisi votre mot de passe\",\n    \"unmatchedPasswordError\": \"Les deux mots de passe ne sont pas identiques\",\n    \"passwordMustContain\": \"Votre mot de passe doit contenir au moins une lettre, un chiffre et un symbole\",\n    \"syncPromptMessage\": \"La synchronisation des données peut prendre un certain temps. Merci de ne pas fermer pas cette page.\",\n    \"or\": \"OU\",\n    \"signInWithGoogle\": \"Continuer avec Google\",\n    \"signInWithGithub\": \"Continuer avec Github\",\n    \"signInWithDiscord\": \"Continuer avec Discord\",\n    \"signInWithApple\": \"Se connecter via Apple\",\n    \"continueAnotherWay\": \"Continuer via une autre méthode\",\n    \"signUpWithGoogle\": \"S'inscrire avec Google\",\n    \"signUpWithGithub\": \"S'inscrire avec Github\",\n    \"signUpWithDiscord\": \"S'inscrire avec Discord\",\n    \"signInWith\": \"Se connecter avec :\",\n    \"signInWithEmail\": \"Se connecter via e-mail\",\n    \"signInWithMagicLink\": \"Continuer\",\n    \"signUpWithMagicLink\": \"S'inscrire avec un lien magique\",\n    \"pleaseInputYourEmail\": \"Veuillez entrer votre adresse e-mail\",\n    \"settings\": \"Paramètres\",\n    \"magicLinkSent\": \"Lien magique envoyé à votre email, veuillez vérifier votre boîte de réception\",\n    \"invalidEmail\": \"S'il vous plaît, mettez une adresse email valide\",\n    \"alreadyHaveAnAccount\": \"Déjà un compte ?\",\n    \"logIn\": \"Connexion\",\n    \"generalError\": \"Une erreur s'est produite. Veuillez réessayer plus tard\",\n    \"limitRateError\": \"Pour des raisons de sécurité, vous ne pouvez demander un lien magique que toutes les 60 secondes\",\n    \"magicLinkSentDescription\": \"Un lien magique vous a été envoyé par e-mail. Cliquez sur le lien pour vous connecter. Le lien expirera dans 5 minutes.\",\n    \"tokenHasExpiredOrInvalid\": \"Le code a expiré ou est invalide. Veuillez réessayer.\",\n    \"signingIn\": \"Connexion...\",\n    \"checkYourEmail\": \"Vérifiez votre courrier électronique\",\n    \"temporaryVerificationLinkSent\": \"Un lien de vérification temporaire a été envoyé.\\nVeuillez vérifier votre boîte de réception sur\",\n    \"temporaryVerificationCodeSent\": \"Un code de vérification temporaire a été envoyé.\\nVeuillez vérifier votre boîte de réception sur\",\n    \"continueToSignIn\": \"Continuer à se connecter\",\n    \"continueWithLoginCode\": \"Continuer avec vos identifiants\",\n    \"backToLogin\": \"Retour à la connexion\",\n    \"enterCode\": \"Entrez le code\",\n    \"enterCodeManually\": \"Entrez le code manuellement\",\n    \"continueWithEmail\": \"Continuer avec l'e-mail\",\n    \"enterPassword\": \"Entrez le mot de passe\",\n    \"loginAs\": \"Connectez-vous en tant que\",\n    \"invalidVerificationCode\": \"Veuillez saisir un code de vérification valide\",\n    \"tooFrequentVerificationCodeRequest\": \"Vous avez fait trop de demandes. Veuillez réessayer plus tard.\",\n    \"invalidLoginCredentials\": \"Votre mot de passe est incorrect, veuillez réessayer\",\n    \"resetPassword\": \"Réinitialiser le mot de passe\",\n    \"resetPasswordDescription\": \"Entrer votre courriel pour réinitialiser votre mot de passe\",\n    \"continueToResetPassword\": \"Continuer pour réinitialiser le mot de passe\",\n    \"resetPasswordSuccess\": \"Le mot de passe a été réinitialisé avec succès\",\n    \"resetPasswordFailed\": \"Échec de la réinitialisation du mot de passe\",\n    \"resetPasswordLinkSent\": \"Un mail permettant de réinitialiser le mot de passe a été envoyé à votre adresse.\",\n    \"resetPasswordLinkExpired\": \"Ce lien de renouvellement de mot de passe est obsolète. Veuillez redemander un lien de réinitialisation.\",\n    \"resetPasswordLinkInvalid\": \"Ce lien de renouvellement de mot de passe est erroné. Veuillez redemander un lien de réinitialisation.\",\n    \"enterNewPasswordFor\": \"Entrez votre nouveau mot de passe\",\n    \"newPassword\": \"Nouveau mot de passe\",\n    \"enterNewPassword\": \"Entrer votre nouveau mot de passe\",\n    \"confirmPassword\": \"Confirmer votre mot de passe\",\n    \"confirmNewPassword\": \"Confirmer votre nouveau mot de passe\",\n    \"newPasswordCannotBeEmpty\": \"Le champ \\\"nouveau mot de passe\\\" ne peut pas être laissé vide.\",\n    \"confirmPasswordCannotBeEmpty\": \"Le champ \\\"confirmation du nouveau mot de passe\\\" ne peut pas être laissé vide.\",\n    \"passwordsDoNotMatch\": \"Le mot de passe et sa confirmation ne correspondent pas.\",\n    \"verifying\": \"En cours de vérification\",\n    \"youAreInLocalMode\": \"Vous êtes en mode local\",\n    \"loginToAppFlowyCloud\": \"Connexion au nuage d'AppFlowy\",\n    \"anonymous\": \"Anonyme\",\n    \"LogInWithGoogle\": \"Se connecter avec Google\",\n    \"LogInWithGithub\": \"Se connecter avec Github\",\n    \"LogInWithDiscord\": \"Se connecter avec Discord\",\n    \"loginAsGuestButtonText\": \"Commencer\",\n    \"logInWithMagicLink\": \"Connectez-vous avec Magic Link\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Choisissez votre espace de travail\",\n    \"defaultName\": \"Mon espace de travail\",\n    \"create\": \"Créer un espace de travail\",\n    \"new\": \"Nouveau espace de travail\",\n    \"importFromNotion\": \"Importer depuis Notion\",\n    \"learnMore\": \"En savoir plus\",\n    \"reset\": \"Réinitialiser l'espace de travail\",\n    \"renameWorkspace\": \"Renommer l'espace de travail\",\n    \"workspaceNameCannotBeEmpty\": \"Le nom de l'espace de travail ne peut être vide\",\n    \"resetWorkspacePrompt\": \"La réinitialisation de l'espace de travail supprimera toutes les pages et données qu'elles contiennent. Êtes-vous sûr de vouloir réinitialiser l'espace de travail ? Alternativement, vous pouvez contacter l'équipe d'assistance pour restaurer l'espace de travail\",\n    \"hint\": \"Espace de travail\",\n    \"notFoundError\": \"Espace de travail introuvable\",\n    \"failedToLoad\": \"Quelque chose s'est mal passé ! Échec du chargement de l'espace de travail. Essayez de fermer toute instance ouverte d'@:appName et réessayez.\",\n    \"errorActions\": {\n      \"reportIssue\": \"Signaler un problème\",\n      \"reportIssueOnGithub\": \"Signaler un bug sur Github\",\n      \"exportLogFiles\": \"Exporter les logs\",\n      \"reachOut\": \"Contactez-nous sur Discord\"\n    },\n    \"menuTitle\": \"Espaces de travail\",\n    \"deleteWorkspaceHintText\": \"Êtes-vous sûr de vouloir supprimer l'espace de travail ? Cette action ne peut pas être annulée.\",\n    \"createSuccess\": \"Espace de travail créé avec succès\",\n    \"createFailed\": \"Échec de la création de l'espace de travail\",\n    \"createLimitExceeded\": \"Vous avez atteint la limite maximale d'espace de travail autorisée pour votre compte. Si vous avez besoin d'espaces de travail supplémentaires pour continuer votre travail, veuillez en faire la demande sur Github.\",\n    \"deleteSuccess\": \"Espace de travail supprimé avec succès\",\n    \"deleteFailed\": \"Échec de la suppression de l'espace de travail\",\n    \"openSuccess\": \"Ouverture de l'espace de travail réussie\",\n    \"openFailed\": \"Échec de l'ouverture de l'espace de travail\",\n    \"renameSuccess\": \"Espace de travail renommé avec succès\",\n    \"renameFailed\": \"Échec du renommage de l'espace de travail\",\n    \"updateIconSuccess\": \"L'icône de l'espace de travail a été mise à jour avec succès\",\n    \"updateIconFailed\": \"La mise a jour de l'icône de l'espace de travail a échoué\",\n    \"cannotDeleteTheOnlyWorkspace\": \"Impossible de supprimer le seul espace de travail\",\n    \"fetchWorkspacesFailed\": \"Échec de la récupération des espaces de travail\",\n    \"leaveCurrentWorkspace\": \"Quitter l'espace de travail\",\n    \"leaveCurrentWorkspacePrompt\": \"Êtes-vous sûr de vouloir quitter l'espace de travail actuel ?\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Partager\",\n    \"workInProgress\": \"Bientôt disponible\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"Copier dans le presse-papier\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Copier le lien\",\n    \"publishToTheWeb\": \"Publier sur le Web\",\n    \"publishToTheWebHint\": \"Créer un site Web avec AppFlowy\",\n    \"publish\": \"Publier\",\n    \"unPublish\": \"Annuler la publication\",\n    \"visitSite\": \"Visitez le site\",\n    \"exportAsTab\": \"Exporter en tant que\",\n    \"publishTab\": \"Publier\",\n    \"shareTab\": \"Partager\",\n    \"publishOnAppFlowy\": \"Publier sur AppFlowy\",\n    \"shareTabTitle\": \"Inviter à collaborer\",\n    \"shareTabDescription\": \"Pour faciliter la collaboration avec n'importe qui\",\n    \"copyLinkSuccess\": \"Lien copié\",\n    \"copyShareLink\": \"Copier le lien de partage\",\n    \"copyLinkFailed\": \"Impossible de copier le lien dans le presse-papiers\",\n    \"copyLinkToBlockSuccess\": \"Lien de bloc copié dans le presse-papiers\",\n    \"copyLinkToBlockFailed\": \"Impossible de copier le lien du bloc dans le presse-papiers\",\n    \"manageAllSites\": \"Gérer tous les sites\",\n    \"updatePathName\": \"Mettre à jour le nom du chemin\"\n  },\n  \"moreAction\": {\n    \"small\": \"petit\",\n    \"medium\": \"moyen\",\n    \"large\": \"grand\",\n    \"fontSize\": \"Taille de police\",\n    \"import\": \"Importer\",\n    \"moreOptions\": \"Plus d'options\",\n    \"wordCount\": \"Compteur de mot: {}\",\n    \"charCount\": \"Compteur de caractère: {}\",\n    \"createdAt\": \"Créé à: {}\",\n    \"deleteView\": \"Supprimer\",\n    \"duplicateView\": \"Dupliquer\",\n    \"wordCountLabel\": \"Mots:\",\n    \"charCountLabel\": \"Charactères: \",\n    \"createdAtLabel\": \"Créé:\",\n    \"syncedAtLabel\": \"Synchronisé\",\n    \"saveAsNewPage\": \"Ajouter des messages à la page\",\n    \"saveAsNewPageDisabled\": \"Aucun message disponible\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Texte et Markdown\",\n    \"documentFromV010\": \"Document de la v0.1.0\",\n    \"databaseFromV010\": \"Base de données à partir de la v0.1.0\",\n    \"notionZip\": \"Fichier ZIP exporté depuis Notion\",\n    \"csv\": \"CSV\",\n    \"database\": \"Base de données\"\n  },\n  \"emojiIconPicker\": {\n    \"iconUploader\": {\n      \"placeholderLeft\": \"Faites glisser et déposez un fichier, cliquez pour \",\n      \"placeholderUpload\": \"Télécharger\",\n      \"placeholderRight\": \", ou collez un lien d'image.\",\n      \"dropToUpload\": \"Déposez un fichier à télécharger\",\n      \"change\": \"Changement\"\n    }\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Renommer\",\n    \"delete\": \"Supprimer\",\n    \"duplicate\": \"Dupliquer\",\n    \"unfavorite\": \"Retirer des favoris\",\n    \"favorite\": \"Ajouter aux favoris\",\n    \"openNewTab\": \"Ouvrir dans un nouvel onglet\",\n    \"moveTo\": \"Déplacer vers\",\n    \"addToFavorites\": \"Ajouter aux Favoris\",\n    \"copyLink\": \"Copier le lien\",\n    \"changeIcon\": \"Changer d'icône\",\n    \"collapseAllPages\": \"Réduire toutes les sous-pages\",\n    \"movePageTo\": \"Déplacer vers\",\n    \"move\": \"Déplacer\",\n    \"lockPage\": \"Verrouiller la page\"\n  },\n  \"blankPageTitle\": \"Page vierge\",\n  \"newPageText\": \"Nouvelle page\",\n  \"newDocumentText\": \"Nouveau document\",\n  \"newGridText\": \"Nouvelle grille\",\n  \"newCalendarText\": \"Nouveau calendrier\",\n  \"newBoardText\": \"Nouveau tableau\",\n  \"chat\": {\n    \"newChat\": \"Chat IA\",\n    \"inputMessageHint\": \"Demandez à l'IA @:appName\",\n    \"inputLocalAIMessageHint\": \"Demander l'IA locale @:appName\",\n    \"unsupportedCloudPrompt\": \"Cette fonctionnalité n'est disponible que lors de l'utilisation du cloud @:appName\",\n    \"relatedQuestion\": \"Questions Associées\",\n    \"serverUnavailable\": \"Service temporairement indisponible. Veuillez réessayer ultérieurement.\",\n    \"aiServerUnavailable\": \"🌈 Oh-oh ! 🌈. Une licorne a mangé notre réponse. Veuillez réessayer !\",\n    \"retry\": \"Réessayer\",\n    \"clickToRetry\": \"Cliquez pour réessayer\",\n    \"regenerateAnswer\": \"Régénérer\",\n    \"question1\": \"Comment utiliser Kanban pour gérer les tâches\",\n    \"question2\": \"Expliquez la méthode GTD\",\n    \"question3\": \"Pourquoi utiliser Rust\",\n    \"question4\": \"Recette avec ce qu'il y a dans ma cuisine\",\n    \"question5\": \"Créer une illustration pour ma page\",\n    \"question6\": \"Dresser une liste de choses à faire pour ma semaine à venir\",\n    \"aiMistakePrompt\": \"L'IA peut faire des erreurs. Vérifiez les informations importantes.\",\n    \"chatWithFilePrompt\": \"Voulez-vous discuter avec le fichier ?\",\n    \"indexFileSuccess\": \"Indexation du fichier réussie\",\n    \"inputActionNoPages\": \"Aucun résultat de page\",\n    \"referenceSource\": {\n      \"zero\": \"0 sources trouvées\",\n      \"one\": \"{count} source trouvée\",\n      \"other\": \"{count} sources trouvées\"\n    },\n    \"clickToMention\": \"Cliquez pour mentionner une page\",\n    \"uploadFile\": \"Téléchargez des fichiers PDF, MD ou TXT pour discuter avec\",\n    \"questionDetail\": \"Bonjour {}! Comment puis-je vous aider aujourd'hui?\",\n    \"indexingFile\": \"Indexation {}\",\n    \"generatingResponse\": \"Générer une réponse\",\n    \"selectSources\": \"Sélectionner Sources\",\n    \"currentPage\": \"Page actuelle\",\n    \"sourcesLimitReached\": \"Vous ne pouvez sélectionner que jusqu'à 3 documents de niveau supérieur et leurs enfants\",\n    \"sourceUnsupported\": \"Nous ne prenons pas en charge le chat avec des bases de données pour le moment\",\n    \"regenerate\": \"Réessayer\",\n    \"addToPageButton\": \"Ajouter à la page\",\n    \"addToPageTitle\": \"Ajouter un message à...\",\n    \"addToNewPage\": \"Ajouter à une nouvelle page\",\n    \"addToNewPageName\": \"Messages extraits de \\\"{}\\\"\",\n    \"addToNewPageSuccessToast\": \"Message ajouté à\",\n    \"openPagePreviewFailedToast\": \"Échec de l'ouverture de la page\",\n    \"changeFormat\": {\n      \"actionButton\": \"Changer de format\",\n      \"confirmButton\": \"Régénérer avec ce format\",\n      \"textOnly\": \"Texte\",\n      \"imageOnly\": \"Image seulement\",\n      \"textAndImage\": \"Texte et image\",\n      \"text\": \"Paragraphe\",\n      \"bullet\": \"Liste à puces\",\n      \"number\": \"Liste numérotée\",\n      \"table\": \"Tableau\",\n      \"blankDescription\": \"Format de réponse\",\n      \"defaultDescription\": \"Format de réponse automatique\",\n      \"textWithImageDescription\": \"@:chat .changeFormat.text avec image\",\n      \"numberWithImageDescription\": \"@:chat .changeFormat.number avec image\",\n      \"bulletWithImageDescription\": \"@:chat .changeFormat.bullet avec image\",\n      \"tableWithImageDescription\": \"@:chat .changeFormat.table avec image\"\n    },\n    \"switchModel\": {\n      \"label\": \"Modèle de commutateur\",\n      \"localModel\": \"Modèle local\",\n      \"cloudModel\": \"Modèle de nuage\",\n      \"autoModel\": \"Auto\"\n    },\n    \"selectBanner\": {\n      \"saveButton\": \"Ajouter à …\",\n      \"selectMessages\": \"Sélectionner les messages\",\n      \"nSelected\": \"{} sélectionné\",\n      \"allSelected\": \"Tous sélectionnés\"\n    },\n    \"stopTooltip\": \"Arrêter de générer\"\n  },\n  \"trash\": {\n    \"text\": \"Corbeille\",\n    \"restoreAll\": \"Tout restaurer\",\n    \"restore\": \"Restaurer\",\n    \"deleteAll\": \"Tout supprimer\",\n    \"pageHeader\": {\n      \"fileName\": \"Nom de fichier\",\n      \"lastModified\": \"Dernière modification\",\n      \"created\": \"Créé\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Voulez-vous vraiment supprimer toutes les pages de la corbeille ?\",\n      \"caption\": \"Cette action ne peut pas être annulée.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Êtes-vous sûr de vouloir restaurer toutes les pages dans la corbeille ?\",\n      \"caption\": \"Cette action ne peut pas être annulée.\"\n    },\n    \"restorePage\": {\n      \"title\": \"Restaurer: {}\",\n      \"caption\": \"Etes-vous sûr de vouloir restaurer cette page ?\"\n    },\n    \"mobile\": {\n      \"actions\": \"Actions de la corbeille\",\n      \"empty\": \"La corbeille est vide\",\n      \"emptyDescription\": \"Vous n'avez aucun fichier supprimé\",\n      \"isDeleted\": \"a été supprimé\",\n      \"isRestored\": \"a été restauré\"\n    },\n    \"confirmDeleteTitle\": \"Etes-vous sûr de vouloir supprimer définitivement cette page ?\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Cette page se trouve dans la corbeille\",\n    \"restore\": \"Restaurer la page\",\n    \"deletePermanent\": \"Supprimer définitivement\",\n    \"deletePermanentDescription\": \"Etes-vous sûr de vouloir supprimer définitivement cette page ? Cette action est irréversible.\"\n  },\n  \"dialogCreatePageNameHint\": \"Nom de la page\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Raccourcis\",\n    \"whatsNew\": \"Nouveautés\",\n    \"helpAndDocumentation\": \"Aide et documentation\",\n    \"getSupport\": \"Obtenir de l'aide\",\n    \"markdown\": \"Rédaction\",\n    \"debug\": {\n      \"name\": \"Informations de Débogage\",\n      \"success\": \"Informations de débogage copiées dans le presse-papiers !\",\n      \"fail\": \"Impossible de copier les informations de débogage dans le presse-papiers\"\n    },\n    \"feedback\": \"Retour\",\n    \"help\": \"Aide et Support\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Supprimer, renommer et plus...\",\n    \"addPageTooltip\": \"Ajoutez rapidement une page à l'intérieur\",\n    \"defaultNewPageName\": \"Sans-titre\",\n    \"renameDialog\": \"Renommer\",\n    \"pageNameSuffix\": \"Copier\"\n  },\n  \"noPagesInside\": \"Aucune page à l'intérieur\",\n  \"toolbar\": {\n    \"undo\": \"Annuler\",\n    \"redo\": \"Rétablir\",\n    \"bold\": \"Gras\",\n    \"italic\": \"Italique\",\n    \"underline\": \"Souligner\",\n    \"strike\": \"Barré\",\n    \"numList\": \"Liste numérotée\",\n    \"bulletList\": \"Liste à puces\",\n    \"checkList\": \"To-Do list\",\n    \"inlineCode\": \"Code en ligne\",\n    \"quote\": \"Citation\",\n    \"header\": \"En-tête\",\n    \"highlight\": \"Surligner\",\n    \"color\": \"Couleur\",\n    \"addLink\": \"Ajouter un lien\",\n    \"link\": \"Lien\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Passer en mode clair\",\n    \"darkMode\": \"Passer en mode sombre\",\n    \"openAsPage\": \"Ouvrir en tant que page\",\n    \"addNewRow\": \"Ajouter une ligne\",\n    \"openMenu\": \"Cliquer pour ouvrir le menu\",\n    \"dragRow\": \"Appuyez longuement pour réorganiser la ligne\",\n    \"viewDataBase\": \"Voir la base de données\",\n    \"referencePage\": \"Ce {nom} est référencé\",\n    \"addBlockBelow\": \"Ajouter un bloc ci-dessous\",\n    \"aiGenerate\": \"Générer\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Fermer le menu latéral\",\n    \"openSidebar\": \"Ouvrir le menu latéral\",\n    \"expandSidebar\": \"Agrandir la page\",\n    \"personal\": \"Personnel\",\n    \"private\": \"Privé\",\n    \"workspace\": \"Espace de travail\",\n    \"favorites\": \"Favoris\",\n    \"clickToHidePrivate\": \"Cliquez pour masquer l'espace privé\\nLes pages que vous avez créées ici ne sont visibles que par vous\",\n    \"clickToHideWorkspace\": \"Cliquez pour masquer l'espace de travail\\nLes pages que vous avez créées ici sont visibles par tous les membres\",\n    \"clickToHidePersonal\": \"Cliquez pour cacher la section personnelle\",\n    \"clickToHideFavorites\": \"Cliquez pour cacher la section favorite\",\n    \"addAPage\": \"Ajouter une page\",\n    \"addAPageToPrivate\": \"Ajouter une page à l'espace privé\",\n    \"addAPageToWorkspace\": \"Ajouter une page à l'espace de travail\",\n    \"recent\": \"Récent\",\n    \"today\": \"Aujourd'hui\",\n    \"thisWeek\": \"Cette semaine\",\n    \"others\": \"Favoris précédents\",\n    \"earlier\": \"Plus tôt\",\n    \"justNow\": \"tout à l' heure\",\n    \"minutesAgo\": \"Il y a {count} minutes\",\n    \"lastViewed\": \"Dernière consultation\",\n    \"favoriteAt\": \"Favoris\",\n    \"emptyRecent\": \"Aucun document récent\",\n    \"emptyRecentDescription\": \"Quand vous consultez des documents, ils apparaîtront ici pour les retrouver facilement\",\n    \"emptyFavorite\": \"Aucun document favori\",\n    \"emptyFavoriteDescription\": \"Commencez à explorer et marquez les documents comme favoris. Ils seront répertoriés ici pour un accès rapide !\",\n    \"removePageFromRecent\": \"Supprimer cette page des Récents ?\",\n    \"removeSuccess\": \"Supprimé avec succès\",\n    \"favoriteSpace\": \"Favoris\",\n    \"RecentSpace\": \"Récent\",\n    \"Spaces\": \"Espaces\",\n    \"upgradeToPro\": \"Passer à Pro\",\n    \"upgradeToAIMax\": \"Débloquez une l'IA illimitée\",\n    \"storageLimitDialogTitle\": \"Vous n'avez plus d'espace de stockage gratuit. Effectuez une mise à niveau pour débloquer un espace de stockage illimité\",\n    \"storageLimitDialogTitleIOS\": \"Vous n'avez plus d'espace de stockage gratuit.\",\n    \"aiResponseLimitTitle\": \"Vous n'avez plus de réponses d'IA gratuites. Passez au plan Pro ou achetez un module complémentaire d'IA pour débloquer des réponses illimitées\",\n    \"aiResponseLimitDialogTitle\": \"La limite des réponses de l'IA a été atteinte\",\n    \"aiResponseLimit\": \"Vous n'avez plus de réponses IA gratuites.\\n\\nAccédez à Paramètres -> Plans -> Cliquez sur AI Max ou Pro Plan pour obtenir plus de réponses AI\",\n    \"askOwnerToUpgradeToPro\": \"Votre espace de stockage gratuit est presque plein. Demandez au propriétaire de votre espace de travail de passer au plan Pro\",\n    \"askOwnerToUpgradeToProIOS\": \"Votre espace de travail manque d’espace de stockage gratuit.\",\n    \"askOwnerToUpgradeToAIMax\": \"Votre espace de travail est à court de réponses d'IA gratuites. Demandez au propriétaire de votre espace de travail de mettre à niveau le plan ou d'acheter des modules complémentaires d'IA\",\n    \"askOwnerToUpgradeToAIMaxIOS\": \"Votre espace de travail est à court de réponses IA gratuites.\",\n    \"purchaseAIMax\": \"Votre espace de travail est à court de réponses AI Image. Veuillez demander au propriétaire de votre espace d'acheter AI Max.\",\n    \"aiImageResponseLimit\": \"Vous n’avez plus de réponses d’image IA.<inlang-LineFeed>\\nAccédez à Paramètres -> Plan -> Cliquez sur AI Max pour obtenir plus de réponses d'images AI\",\n    \"purchaseStorageSpace\": \"Acheter un espace de stockage\",\n    \"singleFileProPlanLimitationDescription\": \"Vous avez dépassé la taille maximale de téléchargement de fichiers autorisée dans le plan gratuit. Veuillez passer au plan Pro pour télécharger des fichiers plus volumineux\",\n    \"purchaseAIResponse\": \"Acheter\",\n    \"askOwnerToUpgradeToLocalAI\": \"Demander au propriétaire de l'espace de travail d'activer l'IA locale\",\n    \"upgradeToAILocal\": \"Exécutez des modèles locaux sur votre appareil pour une confidentialité optimale\",\n    \"upgradeToAILocalDesc\": \"Discutez avec des PDF, améliorez votre écriture et remplissez automatiquement des tableaux à l'aide de l'IA locale\",\n    \"public\": \"Publique\",\n    \"clickToHidePublic\": \"Cliquez pour masquer l'espace public\\nLes pages que vous avez créées ici sont visibles par tous les membres\",\n    \"addAPageToPublic\": \"Ajouter une page à l'espace public\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Note exportée en Markdown\",\n      \"path\": \"Documents/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Contacts\",\n    \"whatsHappening\": \"Que se passe-t-il cette semaine ?\",\n    \"addContact\": \"Ajouter un contact\",\n    \"editContact\": \"Modifier le contact\"\n  },\n  \"button\": {\n    \"ok\": \"OK\",\n    \"confirm\": \"Confirmer\",\n    \"done\": \"Fait\",\n    \"cancel\": \"Annuler\",\n    \"signIn\": \"Se connecter\",\n    \"signOut\": \"Se déconnecter\",\n    \"complete\": \"Achevé\",\n    \"save\": \"Enregistrer\",\n    \"generate\": \"Générer\",\n    \"esc\": \"ESC\",\n    \"keep\": \"Garder\",\n    \"tryAgain\": \"Essayer à nouveau\",\n    \"discard\": \"Jeter\",\n    \"replace\": \"Remplacer\",\n    \"insertBelow\": \"Insérer ci-dessous\",\n    \"insertAbove\": \"Insérer ci-dessus\",\n    \"upload\": \"Télécharger\",\n    \"edit\": \"Modifier\",\n    \"delete\": \"Supprimer\",\n    \"copy\": \"Copier\",\n    \"duplicate\": \"Dupliquer\",\n    \"putback\": \"Remettre\",\n    \"update\": \"Mettre à jour\",\n    \"share\": \"Partager\",\n    \"removeFromFavorites\": \"Retirer des favoris\",\n    \"removeFromRecent\": \"Supprimer des récents\",\n    \"addToFavorites\": \"Ajouter aux favoris\",\n    \"favoriteSuccessfully\": \"Succès en favoris\",\n    \"unfavoriteSuccessfully\": \"Succès retiré des favoris\",\n    \"duplicateSuccessfully\": \"Dupliqué avec succès\",\n    \"rename\": \"Renommer\",\n    \"helpCenter\": \"Centre d'aide\",\n    \"add\": \"Ajouter\",\n    \"yes\": \"Oui\",\n    \"no\": \"Non\",\n    \"clear\": \"Nettoyer\",\n    \"remove\": \"Retirer\",\n    \"dontRemove\": \"Ne pas retirer\",\n    \"copyLink\": \"Copier le lien\",\n    \"align\": \"Aligner\",\n    \"login\": \"Se connecter\",\n    \"logout\": \"Se déconnecter\",\n    \"deleteAccount\": \"Supprimer le compte\",\n    \"back\": \"Retour\",\n    \"signInGoogle\": \"Se connecter avec Google\",\n    \"signInGithub\": \"Se connecter avec Github\",\n    \"signInDiscord\": \"Se connecter avec Discord\",\n    \"more\": \"Plus\",\n    \"create\": \"Créer\",\n    \"close\": \"Fermer\",\n    \"next\": \"Suivant\",\n    \"previous\": \"Précédent\",\n    \"submit\": \"Soumettre\",\n    \"download\": \"Télécharger\",\n    \"backToHome\": \"Retour à l'accueil\",\n    \"viewing\": \"Affichage\",\n    \"editing\": \"Édition\",\n    \"gotIt\": \"Compris\",\n    \"retry\": \"Réessayer \",\n    \"uploadFailed\": \"Échec du téléchargement.\",\n    \"copyLinkOriginal\": \"Copier le lien vers l'original\",\n    \"tryAGain\": \"Réessayer\"\n  },\n  \"label\": {\n    \"welcome\": \"Bienvenue !\",\n    \"firstName\": \"Prénom\",\n    \"middleName\": \"Deuxième prénom\",\n    \"lastName\": \"Nom\",\n    \"stepX\": \"Étape {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Impossible de se connecter à votre compte.\",\n      \"failedMsg\": \"Assurez-vous d'avoir terminé le processus de connexion dans votre navigateur.\"\n    },\n    \"google\": {\n      \"title\": \"CONNEXION VIA GOOGLE\",\n      \"instruction1\": \"Pour importer vos contacts Google, vous devez autoriser cette application à l'aide de votre navigateur web.\",\n      \"instruction2\": \"Copiez ce code dans votre presse-papiers en cliquant sur l'icône ou en sélectionnant le texte :\",\n      \"instruction3\": \"Accédez au lien suivant dans votre navigateur web et saisissez le code ci-dessus :\",\n      \"instruction4\": \"Appuyez sur le bouton ci-dessous lorsque vous avez terminé votre inscription :\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Paramètres\",\n    \"popupMenuItem\": {\n      \"settings\": \"Paramètres\",\n      \"members\": \"Membres\",\n      \"trash\": \"Corbeille\",\n      \"helpAndDocumentation\": \"Aide et documentation\",\n      \"getSupport\": \"Obtenir de l'aide\",\n      \"helpAndSupport\": \"Aide & Support\"\n    },\n    \"sites\": {\n      \"title\": \"Sites\",\n      \"namespaceTitle\": \"Espace\",\n      \"namespaceDescription\": \"Gérez votre espace et votre page d'accueil\",\n      \"namespaceHeader\": \"Espace de nom\",\n      \"homepageHeader\": \"Page d'accueil\",\n      \"updateNamespace\": \"Mettre à jour l'espace\",\n      \"removeHomepage\": \"Supprimer la page d'accueil\",\n      \"selectHomePage\": \"Sélectionnez une page\",\n      \"clearHomePage\": \"Effacer la page d'accueil pour cet espace\",\n      \"customUrl\": \"URL personnalisée\",\n      \"homePage\": {\n        \"upgradeToPro\": \"Passez à la formule pro pour désigner une page d'accueil\"\n      },\n      \"namespace\": {\n        \"description\": \"Ce changement s'appliquera à toutes les pages publiées en direct sur cet espace\",\n        \"tooltip\": \"Nous nous réservons le droit de supprimer tout espace inapproprié\",\n        \"updateExistingNamespace\": \"Mettre à jour l'espace existant\",\n        \"upgradeToPro\": \"Passez au plan Pro pour définir une page d'accueil\",\n        \"redirectToPayment\": \"Redirection vers la page de paiement...\",\n        \"onlyWorkspaceOwnerCanSetHomePage\": \"Seul le propriétaire de l'espace de travail peut définir une page d'accueil\",\n        \"pleaseAskOwnerToSetHomePage\": \"Veuillez demander au propriétaire de l'espace de travail de passer au plan Pro\"\n      },\n      \"publishedPage\": {\n        \"title\": \"Toutes les pages publiées\",\n        \"description\": \"Gérez vos pages publiées\",\n        \"page\": \"Page\",\n        \"pathName\": \"Nom du chemin\",\n        \"date\": \"Date de publication\",\n        \"emptyHinText\": \"Vous n'avez aucune page publiée dans cet espace de travail\",\n        \"noPublishedPages\": \"Aucune page publiée\",\n        \"settings\": \"Paramètres de publication\",\n        \"clickToOpenPageInApp\": \"Ouvrir la page dans l'application\",\n        \"clickToOpenPageInBrowser\": \"Ouvrir la page dans le navigateur\"\n      },\n      \"error\": {\n        \"failedToGeneratePaymentLink\": \"Impossible de générer le lien de paiement pour le plan Pro\",\n        \"failedToUpdateNamespace\": \"Échec de la mise à jour de l'espace\",\n        \"proPlanLimitation\": \"Vous devez effectuer une mise à niveau vers le plan Pro pour mettre à jour l'espace\",\n        \"namespaceAlreadyInUse\": \"Ce nom d'espace déjà pris, veuillez en essayer un autre\",\n        \"invalidNamespace\": \"Nom d'espace invalide, veuillez en essayer un autre\",\n        \"namespaceLengthAtLeast2Characters\": \"Le nom de l'espace doit comporter au moins 2 caractères\",\n        \"onlyWorkspaceOwnerCanUpdateNamespace\": \"Seul le propriétaire de l'espace de travail peut mettre à jour l'espace\",\n        \"onlyWorkspaceOwnerCanRemoveHomepage\": \"Seul le propriétaire de l'espace de travail peut supprimer la page d'accueil\",\n        \"setHomepageFailed\": \"Impossible de définir la page d'accueil\",\n        \"namespaceTooLong\": \"Le nom de l'espace est trop long, veuillez en essayer un autre\",\n        \"namespaceTooShort\": \"Le nom de l'espace est trop court, veuillez en essayer un autre\",\n        \"namespaceIsReserved\": \"Ce nom d'espace est réservé, veuillez en essayer un autre\",\n        \"updatePathNameFailed\": \"Échec de la mise à jour du nom du chemin\",\n        \"removeHomePageFailed\": \"Impossible de supprimer la page d'accueil\",\n        \"publishNameContainsInvalidCharacters\": \"Le nom du chemin contient des caractères non valides, veuillez en essayer un autre\",\n        \"publishNameTooShort\": \"Le nom du chemin est trop court, veuillez en essayer un autre\",\n        \"publishNameTooLong\": \"Le nom du chemin est trop long, veuillez en essayer un autre\",\n        \"publishNameAlreadyInUse\": \"Le nom du chemin est déjà utilisé, veuillez en essayer un autre\",\n        \"namespaceContainsInvalidCharacters\": \"Le nom d'espace contient des caractères non valides, veuillez en essayer un autre\",\n        \"publishPermissionDenied\": \"Seul le propriétaire de l'espace de travail ou l'éditeur de la page peut gérer les paramètres de publication\",\n        \"publishNameCannotBeEmpty\": \"Le nom du chemin ne peut pas être vide, veuillez en essayer un autre\"\n      },\n      \"success\": {\n        \"namespaceUpdated\": \"Espace mis à jour avec succès\",\n        \"setHomepageSuccess\": \"Définir la page d'accueil avec succès\",\n        \"updatePathNameSuccess\": \"Nom du chemin mis à jour avec succès\",\n        \"removeHomePageSuccess\": \"Supprimer la page d'accueil avec succès\"\n      }\n    },\n    \"accountPage\": {\n      \"menuLabel\": \"Mon compte\",\n      \"title\": \"Mon compte\",\n      \"general\": {\n        \"title\": \"Nom du compte et image de profil\",\n        \"changeProfilePicture\": \"Changer la photo de profil\"\n      },\n      \"email\": {\n        \"title\": \"Email\",\n        \"actions\": {\n          \"change\": \"Modifier l'email\"\n        }\n      },\n      \"login\": {\n        \"title\": \"Connexion au compte\",\n        \"loginLabel\": \"Connexion\",\n        \"logoutLabel\": \"Déconnexion\"\n      },\n      \"isUpToDate\": \"@:appName est à jour !\",\n      \"officialVersion\": \"Version {version} (version officielle)\"\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"Espace de travail\",\n      \"title\": \"Espace de travail\",\n      \"description\": \"Personnalisez l'apparence, le thème, la police, la disposition du texte, le format de la date/heure et la langue de votre espace de travail.\",\n      \"workspaceName\": {\n        \"title\": \"Nom de l'espace de travail\",\n        \"savedMessage\": \"Nom de l'espace de travail enregistré\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"Icône de l'espace de travail\",\n        \"description\": \"Personnalisez l'apparence, le thème, la police, la disposition du texte, la date, l'heure et la langue de votre espace de travail.\"\n      },\n      \"appearance\": {\n        \"title\": \"Apparence\",\n        \"description\": \"Personnalisez l'apparence, le thème, la police, la disposition du texte, la date, l'heure et la langue de votre espace de travail.\",\n        \"options\": {\n          \"system\": \"Auto\",\n          \"light\": \"Clair\",\n          \"dark\": \"Foncé\"\n        }\n      },\n      \"resetCursorColor\": {\n        \"title\": \"Réinitialiser la couleur du curseur du document\",\n        \"description\": \"Êtes-vous sûr de vouloir réinitialiser la couleur du curseur ?\"\n      },\n      \"resetSelectionColor\": {\n        \"title\": \"Réinitialiser la couleur de sélection du document\",\n        \"description\": \"Êtes-vous sûr de vouloir réinitialiser la couleur de sélection ?\"\n      },\n      \"resetWidth\": {\n        \"resetSuccess\": \"Réinitialisation réussie de la largeur du document\"\n      },\n      \"theme\": {\n        \"title\": \"Thème\",\n        \"description\": \"Sélectionnez un thème prédéfini ou téléchargez votre propre thème personnalisé.\",\n        \"uploadCustomThemeTooltip\": \"Télécharger un thème personnalisé\",\n        \"failedToLoadThemes\": \"Échec du chargement des thèmes. Veuillez vérifier vos paramètres d'autorisation dans Paramètres système > Confidentialité et sécurité > Fichiers et dossiers > @:appName\"\n      },\n      \"workspaceFont\": {\n        \"title\": \"Police de caractère de l'espace de travail\",\n        \"noFontHint\": \"Aucune police trouvée, essayez un autre terme.\"\n      },\n      \"textDirection\": {\n        \"title\": \"Sens du texte\",\n        \"leftToRight\": \"De gauche à droite\",\n        \"rightToLeft\": \"De droite à gauche\",\n        \"auto\": \"Auto\",\n        \"enableRTLItems\": \"Activer les éléments de la barre d'outils RTL\"\n      },\n      \"layoutDirection\": {\n        \"title\": \"Sens de mise en page\",\n        \"leftToRight\": \"De gauche à droite\",\n        \"rightToLeft\": \"De droite à gauche\"\n      },\n      \"dateTime\": {\n        \"title\": \"Date et heure\",\n        \"example\": \"{} à {} ({})\",\n        \"24HourTime\": \"Heure sur 24 heures\",\n        \"dateFormat\": {\n          \"label\": \"Format de date\",\n          \"local\": \"Locale\",\n          \"us\": \"US\",\n          \"iso\": \"ISO\",\n          \"friendly\": \"Facile à lire\",\n          \"dmy\": \"J/M/A\"\n        }\n      },\n      \"language\": {\n        \"title\": \"Langue\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"Supprimer l'espace de travail\",\n        \"content\": \"Êtes-vous sûr de vouloir supprimer cet espace de travail ? Cette action ne peut pas être annulée.\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"Quitter l'espace de travail\",\n        \"content\": \"Êtes-vous sûr de vouloir quitter cet espace de travail ? Vous allez perdre l’accès à toutes les pages et données qu’il contient.\",\n        \"success\": \"Vous avez quitté l'espace de travail avec succès.\",\n        \"fail\": \"Impossible de quitter l'espace de travail.\"\n      },\n      \"manageWorkspace\": {\n        \"title\": \"Gérer l'espace de travail\",\n        \"leaveWorkspace\": \"Quitter l'espace de travail\",\n        \"deleteWorkspace\": \"Supprimer l'espace de travail\"\n      }\n    },\n    \"manageDataPage\": {\n      \"menuLabel\": \"Gérer les données\",\n      \"title\": \"Gérer les données\",\n      \"description\": \"Gérez le stockage local des données ou importez vos données existantes dans @:appName .\",\n      \"dataStorage\": {\n        \"title\": \"Emplacement de stockage des fichiers\",\n        \"tooltip\": \"L'emplacement où vos fichiers sont stockés\",\n        \"actions\": {\n          \"change\": \"Changer de chemin\",\n          \"open\": \"Ouvrir le répertoire\",\n          \"openTooltip\": \"Ouvrir l’emplacement actuel du dossier de données\",\n          \"copy\": \"Copier le chemin\",\n          \"copiedHint\": \"Lien copié !\",\n          \"resetTooltip\": \"Réinitialiser à l'emplacement par défaut\"\n        },\n        \"resetDialog\": {\n          \"title\": \"Êtes-vous sûr ?\",\n          \"description\": \"La réinitialisation du chemin d'accès à l'emplacement de données par défaut ne supprimera pas vos données. Si vous souhaitez réimporter vos données actuelles, vous devriez sauvegarder le chemin d'accès actuel.\"\n        }\n      },\n      \"importData\": {\n        \"title\": \"Importer des données\",\n        \"tooltip\": \"Importer des données depuis les dossiers de sauvegarde/données @:appName\",\n        \"description\": \"Copier les données à partir d'un dossier de données externe @:appName\",\n        \"action\": \"Parcourir le dossier\"\n      },\n      \"encryption\": {\n        \"title\": \"Chiffrement\",\n        \"tooltip\": \"Gérez la manière dont vos données sont stockées et cryptées\",\n        \"descriptionNoEncryption\": \"L'activation du cryptage crypte toutes les données. Cette opération ne peut pas être annulée.\",\n        \"descriptionEncrypted\": \"Vos données sont cryptées.\",\n        \"action\": \"Crypter les données\",\n        \"dialog\": {\n          \"title\": \"Crypter toutes vos données ?\",\n          \"description\": \"Le cryptage de toutes vos données permettra de les protéger et de les sécuriser. Cette action NE PEUT PAS être annulée. Êtes-vous sûr de vouloir continuer ?\"\n        }\n      },\n      \"cache\": {\n        \"title\": \"Vider le cache\",\n        \"description\": \"Aide à résoudre des problèmes tels que des image qui ne se chargent pas, des pages manquantes dans un espace ou les polices qui ne se chargent pas. Cela n'affectera pas vos données.\",\n        \"dialog\": {\n          \"title\": \"Vider le cache\",\n          \"description\": \"Aide à résoudre des problèmes tels que des image qui ne se chargent pas, des pages manquantes dans un espace ou les polices qui ne se chargent pas. Cela n'affectera pas vos données.\",\n          \"successHint\": \"Cache vidé !\"\n        }\n      },\n      \"data\": {\n        \"fixYourData\": \"Corrigez vos données\",\n        \"fixButton\": \"Réparer\",\n        \"fixYourDataDescription\": \"Si vous rencontrez des problèmes avec vos données, vous pouvez essayer de les résoudre ici.\"\n      }\n    },\n    \"shortcutsPage\": {\n      \"menuLabel\": \"Raccourcis\",\n      \"title\": \"Raccourcis\",\n      \"editBindingHint\": \"Saisir une nouvelle liaison\",\n      \"searchHint\": \"Rechercher\",\n      \"actions\": {\n        \"resetDefault\": \"Réinitialiser les paramètres par défaut\"\n      },\n      \"errorPage\": {\n        \"message\": \"Échec du chargement des raccourcis : {}\",\n        \"howToFix\": \"Veuillez réessayer. Si le problème persiste, veuillez nous contacter sur GitHub.\"\n      },\n      \"resetDialog\": {\n        \"title\": \"Réinitialiser les raccourcis\",\n        \"description\": \"Cela réinitialisera tous vos raccourcis clavier aux valeurs par défaut, vous ne pourrez pas annuler cette opération plus tard, êtes-vous sûr de vouloir continuer ?\",\n        \"buttonLabel\": \"Réinitialiser\"\n      },\n      \"conflictDialog\": {\n        \"title\": \"{} est actuellement utilisé\",\n        \"descriptionPrefix\": \"Ce raccourci clavier est actuellement utilisé par \",\n        \"descriptionSuffix\": \". Si vous remplacez ce raccourci clavier, il sera supprimé de {}.\",\n        \"confirmLabel\": \"Continuer\"\n      },\n      \"editTooltip\": \"Appuyez pour commencer à modifier le raccourci clavier\",\n      \"keybindings\": {\n        \"toggleToDoList\": \"Basculer vers la liste des tâches\",\n        \"insertNewParagraphInCodeblock\": \"Insérer un nouveau paragraphe\",\n        \"pasteInCodeblock\": \"Coller dans le bloc de code\",\n        \"selectAllCodeblock\": \"Sélectionner tout\",\n        \"indentLineCodeblock\": \"Insérer deux espaces au début de la ligne\",\n        \"outdentLineCodeblock\": \"Supprimer deux espaces au début de la ligne\",\n        \"twoSpacesCursorCodeblock\": \"Insérer deux espaces au niveau du curseur\",\n        \"copy\": \"Copier la sélection\",\n        \"paste\": \"Coller le contenu\",\n        \"cut\": \"Couper la sélection\",\n        \"alignLeft\": \"Aligner le texte à gauche\",\n        \"alignCenter\": \"Aligner le texte au centre\",\n        \"alignRight\": \"Aligner le texte à droite\",\n        \"insertInlineMathEquation\": \"Insérer une équation mathématique en ligne\",\n        \"undo\": \"Annuler\",\n        \"redo\": \"Rétablir\",\n        \"convertToParagraph\": \"Convertir un bloc en paragraphe\",\n        \"backspace\": \"Supprimer\",\n        \"deleteLeftWord\": \"Supprimer le mot de gauche\",\n        \"deleteLeftSentence\": \"Supprimer la phrase de gauche\",\n        \"delete\": \"Supprimer le caractère de droite\",\n        \"deleteMacOS\": \"Supprimer le caractère de gauche\",\n        \"deleteRightWord\": \"Supprimer le mot de droite\",\n        \"moveCursorLeft\": \"Déplacer le curseur vers la gauche\",\n        \"moveCursorBeginning\": \"Déplacer le curseur au début\",\n        \"moveCursorLeftWord\": \"Déplacer le curseur d'un mot vers la gauche\",\n        \"moveCursorLeftSelect\": \"Sélectionnez et déplacez le curseur vers la gauche\",\n        \"moveCursorBeginSelect\": \"Sélectionnez et déplacez le curseur au début\",\n        \"moveCursorLeftWordSelect\": \"Sélectionnez et déplacez le curseur d'un mot vers la gauche\",\n        \"moveCursorRight\": \"Déplacer le curseur vers la droite\",\n        \"moveCursorEnd\": \"Déplacer le curseur jusqu'à la fin\",\n        \"moveCursorRightWord\": \"Déplacer le curseur d'un mot vers la droite\",\n        \"moveCursorRightSelect\": \"Sélectionnez et déplacez le curseur vers la droite\",\n        \"moveCursorEndSelect\": \"Sélectionnez et déplacez le curseur jusqu'à la fin\",\n        \"moveCursorRightWordSelect\": \"Sélectionnez et déplacez le curseur vers la droite d'un mot\",\n        \"moveCursorUp\": \"Déplacer le curseur vers le haut\",\n        \"moveCursorTopSelect\": \"Sélectionnez et déplacez le curseur vers le haut\",\n        \"moveCursorTop\": \"Déplacer le curseur vers le haut\",\n        \"moveCursorUpSelect\": \"Sélectionnez et déplacez le curseur vers le haut\",\n        \"moveCursorBottomSelect\": \"Sélectionnez et déplacez le curseur vers le bas\",\n        \"moveCursorBottom\": \"Déplacer le curseur vers le bas\",\n        \"moveCursorDown\": \"Déplacer le curseur vers le bas\",\n        \"moveCursorDownSelect\": \"Sélectionnez et déplacez le curseur vers le bas\",\n        \"home\": \"Faites défiler vers le haut\",\n        \"end\": \"Faites défiler vers le bas\",\n        \"toggleBold\": \"Inverser le gras\",\n        \"toggleItalic\": \"Inverser l'italique\",\n        \"toggleUnderline\": \"Inverser le soulignement\",\n        \"toggleStrikethrough\": \"Inverser le barré\",\n        \"toggleCode\": \"Inverser la mise en forme code\",\n        \"toggleHighlight\": \"Inverser la surbrillance\",\n        \"showLinkMenu\": \"Afficher le menu des liens\",\n        \"openInlineLink\": \"Ouvrir le lien en ligne\",\n        \"openLinks\": \"Ouvrir tous les liens sélectionnés\",\n        \"indent\": \"Augmenter le retrait\",\n        \"outdent\": \"Diminuer le retrait\",\n        \"exit\": \"Quitter l'édition\",\n        \"pageUp\": \"Faites défiler une page vers le haut\",\n        \"pageDown\": \"Faites défiler une page vers le bas\",\n        \"selectAll\": \"Sélectionner tout\",\n        \"pasteWithoutFormatting\": \"Coller le contenu sans formatage\",\n        \"showEmojiPicker\": \"Afficher le sélecteur d'emoji\",\n        \"enterInTableCell\": \"Ajouter un saut de ligne dans le tableau\",\n        \"leftInTableCell\": \"Déplacer d'une cellule vers la gauche dans le tableau\",\n        \"rightInTableCell\": \"Déplacer d'une cellule vers la droite dans le tableau\",\n        \"upInTableCell\": \"Déplacer d'une cellule vers le haut dans le tableau\",\n        \"downInTableCell\": \"Déplacer d'une cellule vers le bas dans le tableau\",\n        \"tabInTableCell\": \"Aller à la prochaine cellule vide dans le tableau\",\n        \"shiftTabInTableCell\": \"Aller à la précédente cellule vide dans le tableau\",\n        \"backSpaceInTableCell\": \"S'arrêter au début de la cellule\"\n      },\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"Insérer un nouveau paragraphe à côté du bloc de code\",\n        \"codeBlockIndentLines\": \"Insérer deux retraits au début du bloc de code\",\n        \"codeBlockOutdentLines\": \"Supprimer deux retraits au début du bloc de code\",\n        \"codeBlockAddTwoSpaces\": \"Insérer deux retraits au niveau du curseur dans le bloc de code\",\n        \"codeBlockSelectAll\": \"Sélectionner tout le contenu d'un bloc de code\",\n        \"codeBlockPasteText\": \"Coller du texte dans le bloc de code\",\n        \"textAlignLeft\": \"Aligner le texte à gauche\",\n        \"textAlignCenter\": \"Aligner le texte au centre\",\n        \"textAlignRight\": \"Aligner le texte à droite\"\n      },\n      \"couldNotLoadErrorMsg\": \"Impossible de charger les raccourcis, réessayez\",\n      \"couldNotSaveErrorMsg\": \"Impossible d'enregistrer les raccourcis, réessayez\"\n    },\n    \"aiPage\": {\n      \"title\": \"Paramètres de l'IA\",\n      \"menuLabel\": \"Paramètres IA\",\n      \"keys\": {\n        \"enableAISearchTitle\": \"Recherche IA\",\n        \"aiSettingsDescription\": \"Choisissez votre modèle préféré pour alimenter AppFlowy AI. Inclut désormais GPT 4-o, Claude 3,5, Llama 3.1 et Mistral 7B\",\n        \"loginToEnableAIFeature\": \"Les fonctionnalités d'IA sont accessibles uniquement après s'être connecté avec @:appName Cloud. Pour créer un compte @:appName, voir dans la rubrique 'Mon Compte'.\",\n        \"llmModel\": \"Modèle de langage\",\n        \"readOnlyField\": \"Ce champ est en lecture seule\",\n        \"llmModelType\": \"Type de modèle de langue\",\n        \"downloadLLMPrompt\": \"Télécharger {}\",\n        \"downloadAppFlowyOfflineAI\": \"Le téléchargement du package hors ligne AI permettra à AI de fonctionner sur votre appareil. Voulez-vous continuer ?\",\n        \"downloadLLMPromptDetail\": \"Le téléchargement du modèle local {} prendra jusqu'à {} d'espace de stockage. Voulez-vous continuer ?\",\n        \"downloadBigFilePrompt\": \"Le téléchargement peut prendre environ 10 minutes.\",\n        \"downloadAIModelButton\": \"Télécharger\",\n        \"downloadingModel\": \"Téléchargement\",\n        \"localAILoaded\": \"Modèle d'IA local ajouté avec succès et prêt à être utilisé\",\n        \"localAIStart\": \"Démarrage du chat avec l'IA locale...\",\n        \"localAILoading\": \"Chargement du modèle d'IA locale...\",\n        \"localAIStopped\": \"IA locale arrêtée\",\n        \"localAIRunning\": \"L'IA locale est en cours d'exécution\",\n        \"localAINotReadyRetryLater\": \"L'IA locale est en cours d'initialisation, veuillez réessayer plus tard\",\n        \"localAIDisabled\": \"Vous utilisez l'IA locale, mais elle est désactivée. Veuillez accéder aux paramètres pour l'activer ou essayer un autre modèle.\",\n        \"localAIInitializing\": \"L'IA locale est en cours de chargement. Cela peut prendre quelques secondes selon votre appareil.\",\n        \"localAINotReadyTextFieldPrompt\": \"Vous ne pouvez pas modifier pendant le chargement de l'IA locale\",\n        \"localAIDisabledTextFieldPrompt\": \"Vous ne pouvez pas modifier lorsque l'IA locale est désactivée\",\n        \"failToLoadLocalAI\": \"Impossible de démarrer l'IA locale\",\n        \"restartLocalAI\": \"Redémarrer l'IA locale\",\n        \"disableLocalAITitle\": \"Désactiver l'IA locale\",\n        \"disableLocalAIDescription\": \"Voulez-vous désactiver l'IA locale ?\",\n        \"localAIToggleTitle\": \"Basculer pour activer ou désactiver l'IA locale\",\n        \"localAIToggleSubTitle\": \"Exécutez les modèles d'IA locaux les plus avancés dans AppFlowy pour une confidentialité et une sécurité optimales\",\n        \"offlineAIInstruction1\": \"Suivre les\",\n        \"offlineAIInstruction2\": \"instructions\",\n        \"offlineAIInstruction3\": \"pour activer l'IA hors ligne.\",\n        \"offlineAIDownload1\": \"Si vous n'avez pas téléchargé l'IA AppFlowy, veuillez\",\n        \"offlineAIDownload2\": \"télécharger\",\n        \"offlineAIDownload3\": \"d'abord\",\n        \"activeOfflineAI\": \"Activer\",\n        \"downloadOfflineAI\": \"Télécharger\",\n        \"openModelDirectory\": \"Ouvrir le dossier\",\n        \"laiNotReady\": \"L'application Local AI n'a pas été installée correctement.\",\n        \"ollamaNotReady\": \"Le serveur Ollama n'est pas prêt.\",\n        \"pleaseFollowThese\": \"Veuillez suivre ces\",\n        \"instructions\": \"instructions\",\n        \"installOllamaLai\": \"pour configurer Ollama et AppFlowy Local AI.\",\n        \"modelsMissing\": \"Impossible de trouver les modèles requis : \",\n        \"downloadModel\": \"pour les télécharger.\"\n      }\n    },\n    \"planPage\": {\n      \"menuLabel\": \"Offre\",\n      \"title\": \"Tarif de l'offre\",\n      \"planUsage\": {\n        \"title\": \"Résumé de l'utilisation du plan\",\n        \"storageLabel\": \"Stockage\",\n        \"storageUsage\": \"{} sur {} GB\",\n        \"unlimitedStorageLabel\": \"Stockage illimité\",\n        \"collaboratorsLabel\": \"Membres\",\n        \"collaboratorsUsage\": \"{} sur {}\",\n        \"aiResponseLabel\": \"Réponses de l'IA\",\n        \"aiResponseUsage\": \"{} sur {}\",\n        \"unlimitedAILabel\": \"Réponses illimitées\",\n        \"proBadge\": \"Pro\",\n        \"aiMaxBadge\": \"AI Max\",\n        \"aiOnDeviceBadge\": \"IA sur appareil pour Mac\",\n        \"memberProToggle\": \"Plus de membres et une IA illimitée\",\n        \"aiMaxToggle\": \"IA illimitée et accès à des modèles avancés\",\n        \"aiOnDeviceToggle\": \"IA locale pour une confidentialité ultime\",\n        \"aiCredit\": {\n          \"title\": \"Ajoutez des crédit IA @:appName \",\n          \"price\": \"{}\",\n          \"priceDescription\": \"pour 1 000 crédits\",\n          \"purchase\": \"Acheter l'IA\",\n          \"info\": \"Ajoutez 1 000 crédits d'IA par espace de travail et intégrez de manière transparente une IA personnalisable dans votre flux de travail pour des résultats plus intelligents et plus rapides avec jusqu'à :\",\n          \"infoItemOne\": \"10 000 réponses par base de données\",\n          \"infoItemTwo\": \"1 000 réponses par espace de travail\"\n        },\n        \"currentPlan\": {\n          \"bannerLabel\": \"Plan actuel\",\n          \"freeTitle\": \"Gratuit\",\n          \"proTitle\": \"Pro\",\n          \"teamTitle\": \"Équipe\",\n          \"freeInfo\": \"Idéal pour les particuliers jusqu'à 2 membres pour tout organiser\",\n          \"proInfo\": \"Idéal pour les petites et moyennes équipes jusqu'à 10 membres.\",\n          \"teamInfo\": \"Parfait pour toutes les équipes productives et bien organisées.\",\n          \"upgrade\": \"Changer de plan\",\n          \"canceledInfo\": \"Votre forfait est annulé, vous serez rétrogradé au plan gratuit le {}.\"\n        },\n        \"addons\": {\n          \"title\": \"Compléments\",\n          \"addLabel\": \"Ajouter\",\n          \"activeLabel\": \"Ajouté\",\n          \"aiMax\": {\n            \"title\": \"AI Max\",\n            \"description\": \"Réponses IA illimitées alimentées par GPT-4o, Claude 3.5 Sonnet et plus\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"par utilisateur et par mois, facturé annuellement\"\n          },\n          \"aiOnDevice\": {\n            \"title\": \"IA sur appareil pour Mac\",\n            \"description\": \"Exécutez Mistral 7B, LLAMA 3 et d'autres modèles locaux sur votre machine\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"par utilisateur et par mois, facturé annuellement\",\n            \"recommend\": \"Recommand2 M1 ou plus récent\"\n          }\n        },\n        \"deal\": {\n          \"bannerLabel\": \"Offre de nouvelle année !\",\n          \"title\": \"Développez votre équipe !\",\n          \"info\": \"Effectuez une mise à niveau et économisez 10 % sur les forfaits Pro et Team ! Boostez la productivité de votre espace de travail avec de nouvelles fonctionnalités puissantes, notamment l'IA @:appName .\",\n          \"viewPlans\": \"Voir les plans\"\n        }\n      }\n    },\n    \"billingPage\": {\n      \"menuLabel\": \"Facturation\",\n      \"title\": \"Facturation\",\n      \"plan\": {\n        \"title\": \"Plan\",\n        \"freeLabel\": \"Gratuit\",\n        \"proLabel\": \"Pro\",\n        \"planButtonLabel\": \"Changer de plan\",\n        \"billingPeriod\": \"Période de facturation\",\n        \"periodButtonLabel\": \"Changer la période \"\n      },\n      \"paymentDetails\": {\n        \"title\": \"Détails de paiement\",\n        \"methodLabel\": \"Mode de paiement\",\n        \"methodButtonLabel\": \"Changer de mode de paiement\"\n      },\n      \"addons\": {\n        \"title\": \"Compléments\",\n        \"addLabel\": \"Ajouter\",\n        \"removeLabel\": \"Retirer\",\n        \"renewLabel\": \"Renouveler\",\n        \"aiMax\": {\n          \"label\": \"IA Max\",\n          \"description\": \"Débloquez une IA illimitée et des modèles avancés\",\n          \"activeDescription\": \"Prochaine facture due le {}\",\n          \"canceledDescription\": \"IA Max sera disponible jusqu'au {}\"\n        },\n        \"aiOnDevice\": {\n          \"label\": \"IA local pour Mac\",\n          \"description\": \"Débloquez une IA illimitée locale sur votre appareil\",\n          \"activeDescription\": \"Prochaine facture due le {}\",\n          \"canceledDescription\": \"IA locale pour Mac sera disponible jusqu'au {}\"\n        },\n        \"removeDialog\": {\n          \"title\": \"Supprimer\",\n          \"description\": \"Êtes-vous sûr de vouloir supprimer {plan}? Vous perdrez l'accès aux fonctionnalités et bénéfices de {plan} de manière immédiate.\"\n        }\n      },\n      \"currentPeriodBadge\": \"ACTUEL\",\n      \"changePeriod\": \"Changer de période\",\n      \"planPeriod\": \"{} période\",\n      \"monthlyInterval\": \"Mensuel\",\n      \"monthlyPriceInfo\": \"par personne, facturé mensuellement\",\n      \"annualInterval\": \"Annuellement\",\n      \"annualPriceInfo\": \"par personne, facturé annuellement\"\n    },\n    \"comparePlanDialog\": {\n      \"title\": \"Comparer et sélectionner un plan\",\n      \"planFeatures\": \"Plan\\nCaractéristiques\",\n      \"current\": \"Actuel\",\n      \"actions\": {\n        \"upgrade\": \"Améliorer\",\n        \"downgrade\": \"Rétrograder\",\n        \"current\": \"Actuel\"\n      },\n      \"freePlan\": {\n        \"title\": \"Gratuit\",\n        \"description\": \"Pour les particuliers jusqu'à 2 membres pour tout organiser\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"gratuit pour toujours\"\n      },\n      \"proPlan\": {\n        \"title\": \"Pro\",\n        \"description\": \"Pour les petites équipes pour gérer les projets et les bases de connaissance\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"par utilisateur et par mois\\nfacturé annuellement\\n\\n{} facturé mensuellement\"\n      },\n      \"planLabels\": {\n        \"itemOne\": \"Espaces de travail\",\n        \"itemTwo\": \"Membres\",\n        \"itemThree\": \"Stockage\",\n        \"itemFour\": \"Collaboration en temps réel\",\n        \"itemFive\": \"Application mobile\",\n        \"itemSix\": \"Réponses de l'IA\",\n        \"itemSeven\": \"Images IA\",\n        \"itemFileUpload\": \"Téléchargements de fichiers\",\n        \"customNamespace\": \"Nom d'espace personnalisé\",\n        \"tooltipFive\": \"Collaborer sur des pages spécifiques avec des personnes qui ne sont pas membres\",\n        \"tooltipSix\": \"La durée de vie signifie que le nombre de réponses n'est jamais réinitialisé\",\n        \"intelligentSearch\": \"Recherche intelligente\",\n        \"tooltipSeven\": \"Vous permet de personnaliser une partie de l'URL de votre espace de travail\",\n        \"customNamespaceTooltip\": \"URL de site publiée personnalisée\"\n      },\n      \"freeLabels\": {\n        \"itemOne\": \"facturé par espace de travail\",\n        \"itemTwo\": \"jusqu'à 2\",\n        \"itemThree\": \"5 Go\",\n        \"itemFour\": \"Oui\",\n        \"itemFive\": \"Oui\",\n        \"itemSix\": \"10 à vie\",\n        \"itemSeven\": \"à vie\",\n        \"itemFileUpload\": \"Jusqu'à 7 Mo\",\n        \"intelligentSearch\": \"Recherche intelligente\"\n      },\n      \"proLabels\": {\n        \"itemOne\": \"facturé par espace de travail\",\n        \"itemTwo\": \"jusqu'à 10\",\n        \"itemThree\": \"illimité\",\n        \"itemFour\": \"Oui\",\n        \"itemFive\": \"Oui\",\n        \"itemSix\": \"illimité\",\n        \"itemSeven\": \"10 images par mois\",\n        \"itemFileUpload\": \"Illimité\",\n        \"intelligentSearch\": \"Recherche intelligente\"\n      },\n      \"paymentSuccess\": {\n        \"title\": \"Vous êtes maintenant sur le plan {} !\",\n        \"description\": \"Votre paiement a été traité avec succès et votre forfait est mis à niveau vers @:appName {}. Vous pouvez consulter les détails de votre forfait sur la page Forfait\"\n      },\n      \"downgradeDialog\": {\n        \"title\": \"Êtes-vous sûr de vouloir rétrograder votre forfait ?\",\n        \"description\": \"La baisse de votre offre vous ramènera au forfait gratuit. Les membres peuvent perdre l'accès à cet espace de travail et vous devrez peut-être libérer de l'espace pour respecter les limites de stockage du forfait gratuit.\",\n        \"downgradeLabel\": \"Baisser l'offre\"\n      }\n    },\n    \"cancelSurveyDialog\": {\n      \"title\": \"Désolé de vous voir partir\",\n      \"description\": \"Nous sommes désolés de vous voir partir. Nous aimerions connaître votre avis pour nous aider à améliorer @:appName . Veuillez prendre un moment pour répondre à quelques questions.\",\n      \"commonOther\": \"Autre\",\n      \"otherHint\": \"Écrivez votre réponse ici\",\n      \"questionOne\": {\n        \"question\": \"Qu'est-ce qui vous a poussé à annuler votre @:appName Pro ?\",\n        \"answerOne\": \"Coût trop élevé\",\n        \"answerTwo\": \"Les fonctionnalités ne répondent pas à mes attentes\",\n        \"answerThree\": \"J'ai trouvé une meilleure alternative\",\n        \"answerFour\": \"Je ne l'ai pas suffisamment utilisé pour justifier la dépense\",\n        \"answerFive\": \"Problème de service ou difficultés techniques\"\n      },\n      \"questionTwo\": {\n        \"question\": \"Quelle est la probabilité que vous envisagiez de vous réabonner à @:appName Pro à l'avenir ?\",\n        \"answerOne\": \"Très probablement\",\n        \"answerTwo\": \"Assez probable\",\n        \"answerThree\": \"Pas sûr\",\n        \"answerFour\": \"Peu probable\",\n        \"answerFive\": \"Très peu probable\"\n      },\n      \"questionThree\": {\n        \"question\": \"Quelle fonctionnalité Pro avez-vous le plus appréciée lors de votre abonnement ?\",\n        \"answerOne\": \"Collaboration multi-utilisateurs\",\n        \"answerTwo\": \"Historique des versions plus long\",\n        \"answerThree\": \"Réponses IA illimitées\",\n        \"answerFour\": \"Accès aux modèles d'IA locaux\"\n      },\n      \"questionFour\": {\n        \"question\": \"Comment décririez-vous votre expérience globale avec @:appName ?\",\n        \"answerOne\": \"Super\",\n        \"answerTwo\": \"Bien\",\n        \"answerThree\": \"Moyenne\",\n        \"answerFour\": \"En dessous de la moyenne\",\n        \"answerFive\": \"Insatisfait\"\n      }\n    },\n    \"common\": {\n      \"uploadingFile\": \"Le fichier est en cours de téléchargement. Veuillez ne pas quitter l'application\",\n      \"uploadNotionSuccess\": \"Votre fichier zip Notion a été téléchargé avec succès. Une fois l'importation terminée, vous recevrez un e-mail de confirmation\",\n      \"reset\": \"Réinitialiser\"\n    },\n    \"menu\": {\n      \"appearance\": \"Apparence\",\n      \"language\": \"Langue\",\n      \"user\": \"Utilisateur\",\n      \"files\": \"Dossiers\",\n      \"notifications\": \"Notifications\",\n      \"open\": \"Ouvrir les paramètres\",\n      \"logout\": \"Se déconnecter\",\n      \"logoutPrompt\": \"Êtes-vous sûr de vouloir vous déconnecter ?\",\n      \"selfEncryptionLogoutPrompt\": \"Êtes-vous sûr de vouloir vous déconnecter ? Veuillez vous assurer d'avoir copié la clé de chiffrement.\",\n      \"syncSetting\": \"Paramètres de synchronisation\",\n      \"cloudSettings\": \"Paramètres cloud\",\n      \"enableSync\": \"Activer la synchronisation\",\n      \"enableSyncLog\": \"Activer la journalisation de synchronisation\",\n      \"enableSyncLogWarning\": \"Merci de nous aider à diagnostiquer les problèmes de synchronisation. Cela enregistrera les modifications de votre document dans un fichier local. Veuillez quitter et rouvrir l'application après l'avoir activée\",\n      \"enableEncrypt\": \"Chiffrer les données\",\n      \"cloudURL\": \"URL de base\",\n      \"webURL\": \"Web URL\",\n      \"invalidCloudURLScheme\": \"Schéma invalide\",\n      \"cloudServerType\": \"Serveur cloud\",\n      \"cloudServerTypeTip\": \"Veuillez noter qu'il est possible que votre compte actuel soit déconnecté après avoir changé de serveur cloud.\",\n      \"cloudLocal\": \"Local\",\n      \"cloudAppFlowy\": \"@:appName Cloud Bêta\",\n      \"cloudAppFlowySelfHost\": \"@:appName Cloud auto-hébergé\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"L'URL cloud ne peut pas être vide\",\n      \"clickToCopy\": \"Cliquez pour copier\",\n      \"selfHostStart\": \"Si vous n'avez pas de serveur, veuillez vous référer au\",\n      \"selfHostContent\": \"document\",\n      \"selfHostEnd\": \"pour obtenir des conseils sur la façon d'auto-héberger votre propre serveur\",\n      \"pleaseInputValidURL\": \"Veuillez saisir une URL valide\",\n      \"changeUrl\": \"Changer l'URL auto-hébergée en {}\",\n      \"cloudURLHint\": \"Saisissez l'URL de base de votre serveur\",\n      \"webURLHint\": \"Saisissez l'URL de base de votre serveur web\",\n      \"cloudWSURL\": \"URL du websocket\",\n      \"cloudWSURLHint\": \"Saisissez l'adresse websocket de votre serveur\",\n      \"restartApp\": \"Redémarrer\",\n      \"restartAppTip\": \"Redémarrez l'application pour que les modifications prennent effet. Veuillez noter que cela pourrait déconnecter votre compte actuel.\",\n      \"changeServerTip\": \"Après avoir changé de serveur, vous devez cliquer sur le bouton de redémarrer pour que les modifications prennent effet\",\n      \"enableEncryptPrompt\": \"Activez le chiffrement pour sécuriser vos données avec cette clé. Rangez-la en toute sécurité ; une fois activé, il ne peut pas être désactivé. En cas de perte, vos données deviennent irrécupérables. Cliquez pour copier\",\n      \"inputEncryptPrompt\": \"Veuillez saisir votre mot ou phrase de passe pour\",\n      \"clickToCopySecret\": \"Cliquez pour copier le mot ou la phrase de passe\",\n      \"configServerSetting\": \"Configurez les paramètres de votre serveur\",\n      \"configServerGuide\": \"Après avoir sélectionné « Démarrage rapide », accédez à « Paramètres » puis « Paramètres Cloud » pour configurer votre serveur auto-hébergé.\",\n      \"inputTextFieldHint\": \"Votre mot ou phrase de passe\",\n      \"historicalUserList\": \"Historique de connexion d'utilisateurs\",\n      \"historicalUserListTooltip\": \"Cette liste affiche vos comptes anonymes. Vous pouvez cliquer sur un compte pour afficher ses détails. Les comptes anonymes sont créés en cliquant sur le bouton « Commencer »\",\n      \"openHistoricalUser\": \"Cliquez pour ouvrir le compte anonyme\",\n      \"customPathPrompt\": \"Le stockage du dossier de données @:appName dans un dossier synchronisé avec le cloud tel que Google Drive peut présenter des risques. Si la base de données de ce dossier est consultée ou modifiée à partir de plusieurs emplacements en même temps, cela peut entraîner des conflits de synchronisation et une corruption potentielle des données.\",\n      \"importAppFlowyData\": \"Importer des données à partir du dossier @:appName externe\",\n      \"importingAppFlowyDataTip\": \"L'importation des données est en cours. Veuillez ne pas fermer l'application\",\n      \"importAppFlowyDataDescription\": \"Copiez les données d'un dossier de données @:appName externe et importez-les dans le dossier de données @:appName actuel\",\n      \"importSuccess\": \"Importation réussie du dossier de données @:appName\",\n      \"importFailed\": \"L'importation du dossier de données @:appName a échoué\",\n      \"importGuide\": \"Pour plus de détails, veuillez consulter le document référencé\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Activer les notifications\",\n        \"hint\": \"Désactivez-la pour empêcher l'affichage des notifications locales.\"\n      },\n      \"showNotificationsIcon\": {\n        \"label\": \"Afficher l'icône des notifications\",\n        \"hint\": \"Désactiver pour masquer l'icône de notification dans la barre latérale.\"\n      },\n      \"archiveNotifications\": {\n        \"allSuccess\": \"Toutes les notifications ont été archivées avec succès\",\n        \"success\": \"Notification archivée avec succès\"\n      },\n      \"markAsReadNotifications\": {\n        \"allSuccess\": \"Tout a été marqué comme lu avec succès\",\n        \"success\": \"Marqué comme lu avec succès\"\n      },\n      \"action\": {\n        \"markAsRead\": \"Marquer comme lu\",\n        \"multipleChoice\": \"Sélectionnez plus\",\n        \"archive\": \"Archiver\"\n      },\n      \"settings\": {\n        \"settings\": \"Paramètres\",\n        \"markAllAsRead\": \"Marquer tout comme lu\",\n        \"archiveAll\": \"Archiver tout\"\n      },\n      \"emptyInbox\": {\n        \"title\": \"Aucune notification pour le moment\",\n        \"description\": \"Vous serez averti ici des @mentions\"\n      },\n      \"emptyUnread\": {\n        \"title\": \"Aucune notification non lue\",\n        \"description\": \"Vous êtes à jour !\"\n      },\n      \"emptyArchived\": {\n        \"title\": \"Aucune notification archivée\",\n        \"description\": \"Vous n'avez pas encore archivé de notifications\"\n      },\n      \"tabs\": {\n        \"inbox\": \"Boîte de réception\",\n        \"unread\": \"Non lu\",\n        \"archived\": \"Archivé\"\n      },\n      \"refreshSuccess\": \"Les notifications ont été actualisées avec succès\",\n      \"titles\": {\n        \"notifications\": \"Notifications\",\n        \"reminder\": \"Rappel\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"Réinitialiser ce paramètre\",\n      \"fontFamily\": {\n        \"label\": \"Famille de polices\",\n        \"search\": \"Recherche\",\n        \"defaultFont\": \"Système\"\n      },\n      \"themeMode\": {\n        \"label\": \" Mode du Thème\",\n        \"light\": \"Mode clair\",\n        \"dark\": \"Mode sombre\",\n        \"system\": \"S'adapter au système\"\n      },\n      \"fontScaleFactor\": \"Facteur d'échelle de police\",\n      \"displaySize\": \"Taille de l'écran\",\n      \"documentSettings\": {\n        \"cursorColor\": \"Couleur du curseur du document\",\n        \"selectionColor\": \"Couleur de sélection du document\",\n        \"width\": \"Largeur du document\",\n        \"changeWidth\": \"Changement\",\n        \"pickColor\": \"Sélectionnez une couleur\",\n        \"colorShade\": \"Nuance de couleur\",\n        \"opacity\": \"Opacité\",\n        \"hexEmptyError\": \"La couleur hexadécimale ne peut pas être vide\",\n        \"hexLengthError\": \"La valeur hexadécimale doit comporter 6 chiffres\",\n        \"hexInvalidError\": \"Valeur hexadécimale invalide\",\n        \"opacityEmptyError\": \"L'opacité ne peut pas être vide\",\n        \"opacityRangeError\": \"L'opacité doit être comprise entre 1 et 100\",\n        \"app\": \"Application\",\n        \"flowy\": \"Fluide\",\n        \"apply\": \"Appliquer\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Orientation de la mise en page\",\n        \"hint\": \"Contrôlez l'orientation du contenu sur votre écran, de gauche à droite ou de droite à gauche.\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\"\n      },\n      \"textDirection\": {\n        \"label\": \"Direction du texte par défaut\",\n        \"hint\": \"Spécifiez si le texte doit commencer à gauche ou à droite par défaut.\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\",\n        \"auto\": \"AUTO\",\n        \"fallback\": \"Identique au sens de mise en page\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Téléverser\",\n        \"uploadTheme\": \"Téléverser le thème\",\n        \"description\": \"Téléversez votre propre thème @:appName en utilisant le bouton ci-dessous.\",\n        \"loading\": \"Veuillez patienter pendant que nous validons et téléchargeons votre thème...\",\n        \"uploadSuccess\": \"Votre thème a été téléversé avec succès\",\n        \"deletionFailure\": \"Échec de la suppression du thème. Essayez de le supprimer manuellement.\",\n        \"filePickerDialogTitle\": \"Choisissez un fichier .flowy_plugin\",\n        \"urlUploadFailure\": \"Échec de l'ouverture de l'URL : {}\",\n        \"failure\": \"Le thème qui a été téléchargé avait un format non valide.\"\n      },\n      \"theme\": \"Thème\",\n      \"builtInsLabel\": \"Thèmes intégrés\",\n      \"pluginsLabel\": \"Plugins\",\n      \"dateFormat\": {\n        \"label\": \"Format de la date\",\n        \"local\": \"Local\",\n        \"us\": \"US\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"Convivial\",\n        \"dmy\": \"J/M/A\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Format de l'heure\",\n        \"twelveHour\": \"Douze heures\",\n        \"twentyFourHour\": \"Vingt-quatre heures\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"Afficher la boîte de dialogue de nommage lors de la création d'une page\",\n      \"enableRTLToolbarItems\": \"Activer les éléments de la barre d'outils RTL\",\n      \"members\": {\n        \"title\": \"Paramètres des membres\",\n        \"inviteMembers\": \"Inviter des membres\",\n        \"inviteHint\": \"Invitation par email\",\n        \"sendInvite\": \"Envoyer une invitation\",\n        \"copyInviteLink\": \"Copier le lien d'invitation\",\n        \"label\": \"Membres\",\n        \"user\": \"Utilisateur\",\n        \"role\": \"Rôle\",\n        \"removeFromWorkspace\": \"Retirer de l'espace de travail\",\n        \"removeFromWorkspaceSuccess\": \"Retiré de l'espace de travail avec succès\",\n        \"removeFromWorkspaceFailed\": \"Suppression du membre échouée \",\n        \"owner\": \"Propriétaire\",\n        \"guest\": \"Invité\",\n        \"member\": \"Membre\",\n        \"memberHintText\": \"Un membre peut lire, commenter, et éditer des pages. Inviter des membres et des invités.\",\n        \"guestHintText\": \"Un invité peut lire, réagir, commenter, et peut éditer certaines pages avec une permission\",\n        \"emailInvalidError\": \"Email invalide, veuillez le vérifier et recommencer\",\n        \"emailSent\": \"Email envoyé, veuillez vérifier dans votre boîte mail.\",\n        \"members\": \"membres\",\n        \"membersCount\": {\n          \"zero\": \"{} membres\",\n          \"one\": \"{} membre\",\n          \"other\": \"{} membres\"\n        },\n        \"inviteFailedDialogTitle\": \"Échec de l'envoi de l'invitation\",\n        \"inviteFailedMemberLimit\": \"La limite de membres a été atteinte, veuillez effectuer une mise à niveau pour inviter plus de membres.\",\n        \"inviteFailedMemberLimitMobile\": \"Votre espace de travail a atteint la limite de membres. Utilisez l'application sur PC pour effectuez une mise à niveau et débloquer plus de fonctionnalités.\",\n        \"memberLimitExceeded\": \"Vous avez atteint la limite maximale de membres autorisée pour votre compte. Si vous souhaitez ajouter d'autres membres pour continuer votre travail, veuillez en faire la demande sur Github.\",\n        \"memberLimitExceededUpgrade\": \"mise à niveau\",\n        \"memberLimitExceededPro\": \"Limite de membres atteinte, si vous avez besoin de plus de membres, contactez \",\n        \"memberLimitExceededProContact\": \"support@appflowy.io\",\n        \"failedToAddMember\": \"Échec de l'ajout d'un membre\",\n        \"addMemberSuccess\": \"Membre ajouté avec succès\",\n        \"removeMember\": \"Supprimer un membre\",\n        \"areYouSureToRemoveMember\": \"Êtes-vous sûr de vouloir supprimer ce membre ?\",\n        \"inviteMemberSuccess\": \"L'invitation a été envoyée avec succès\",\n        \"failedToInviteMember\": \"Impossible d'inviter un membre\",\n        \"workspaceMembersError\": \"Une erreur s'est produite\",\n        \"workspaceMembersErrorDescription\": \"Nous n'avons pas pu charger la liste des membres. Veuillez essayer plus tard s'il vous plait\",\n        \"inviteLinkToAddMember\": \"Lien d'invitation pour ajouter un membre\",\n        \"clickToCopyLink\": \"Cliquez pour copier le lien\",\n        \"or\": \"ou\",\n        \"generateANewLink\": \"générer un nouveau lien\",\n        \"inviteMemberByEmail\": \"Inviter un membre par e-mail\",\n        \"inviteMemberHintText\": \"Invitation par email\",\n        \"resetInviteLink\": \"Réinitialiser le lien d'invitation ?\",\n        \"resetInviteLinkDescription\": \"La réinitialisation désactivera le lien actuel pour tous les membres de l'espace et en générera un nouveau. L'ancien lien ne sera plus disponible.\",\n        \"adminPanel\": \"Panneau d'administration\",\n        \"reset\": \"Réinitialiser\",\n        \"resetInviteLinkSuccess\": \"Le lien d'invitation a été réinitialisé avec succès\",\n        \"resetInviteLinkFailed\": \"Échec de la réinitialisation du lien d'invitation\",\n        \"resetInviteLinkFailedDescription\": \"Veuillez réessayer plus tard\",\n        \"memberPageDescription1\": \"Accéder au\",\n        \"memberPageDescription2\": \"pour la gestion des invités et des utilisateurs avancés.\",\n        \"noInviteLink\": \"Vous n'avez pas généré de lien d'invitation pour le moment\",\n        \"copyLink\": \"Copier le lien\",\n        \"generatedLinkSuccessfully\": \"Lien généré avec succès\",\n        \"generatedLinkFailed\": \"Échec de génération du lien\"\n      }\n    },\n    \"files\": {\n      \"copy\": \"Copie\",\n      \"defaultLocation\": \"Lire les fichiers et l'emplacement de stockage des données\",\n      \"exportData\": \"Exportez vos données\",\n      \"doubleTapToCopy\": \"Appuyez deux fois pour copier le chemin\",\n      \"restoreLocation\": \"Restaurer le chemin par défaut d'@:appName\",\n      \"customizeLocation\": \"Ouvrir un autre dossier\",\n      \"restartApp\": \"Veuillez redémarrer l'application pour que les modifications prennent effet.\",\n      \"exportDatabase\": \"Exporter la base de données\",\n      \"selectFiles\": \"Sélectionnez les fichiers qui doivent être exportés\",\n      \"selectAll\": \"Tout sélectionner\",\n      \"deselectAll\": \"Tout déselectionner\",\n      \"createNewFolder\": \"Créer un nouveau dossier\",\n      \"createNewFolderDesc\": \"Dites-nous où vous souhaitez stocker vos données\",\n      \"defineWhereYourDataIsStored\": \"Définissez où vos données sont stockées\",\n      \"open\": \"Ouvrir\",\n      \"openFolder\": \"Ouvrir un dossier existant\",\n      \"openFolderDesc\": \"Lisez-le et écrivez-le dans votre dossier @:appName existant\",\n      \"folderHintText\": \"Nom de dossier\",\n      \"location\": \"Création d'un nouveau dossier\",\n      \"locationDesc\": \"Choisissez un nom pour votre dossier de données @:appName\",\n      \"browser\": \"Parcourir\",\n      \"create\": \"Créer\",\n      \"set\": \"Définir\",\n      \"folderPath\": \"Chemin pour stocker votre dossier\",\n      \"locationCannotBeEmpty\": \"Le chemin ne peut pas être vide\",\n      \"pathCopiedSnackbar\": \"Chemin de stockage des fichiers copié dans le presse-papier !\",\n      \"changeLocationTooltips\": \"Changer le répertoire de données\",\n      \"change\": \"Changer\",\n      \"openLocationTooltips\": \"Ouvrir un autre répertoire de données\",\n      \"openCurrentDataFolder\": \"Ouvrir le répertoire de données actuel\",\n      \"recoverLocationTooltips\": \"Réinitialiser au répertoire de données par défaut d'@:appName\",\n      \"exportFileSuccess\": \"Exporter le fichier avec succès !\",\n      \"exportFileFail\": \"Échec de l'export  du fichier !\",\n      \"export\": \"Exporter\",\n      \"clearCache\": \"Vider le cache\",\n      \"clearCacheDesc\": \"Si vous rencontrez des problèmes avec des images qui ne se chargent pas ou des polices qui ne s'affichent pas correctement, essayez de vider votre cache. Cette action ne supprimera pas vos données utilisateur.\",\n      \"areYouSureToClearCache\": \"Êtes-vous sûr de vider le cache ?\",\n      \"clearCacheSuccess\": \"Cache vidé avec succès !\"\n    },\n    \"user\": {\n      \"name\": \"Nom\",\n      \"email\": \"Courriel\",\n      \"tooltipSelectIcon\": \"Sélectionner l'icône\",\n      \"selectAnIcon\": \"Sélectionnez une icône\",\n      \"pleaseInputYourOpenAIKey\": \"Veuillez entrer votre clé AI\",\n      \"clickToLogout\": \"Cliquez pour déconnecter l'utilisateur actuel\",\n      \"pleaseInputYourStabilityAIKey\": \"Veuillez saisir votre clé de Stability AI\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"Informations personnelles\",\n      \"username\": \"Nom d'utilisateur\",\n      \"usernameEmptyError\": \"Le nom d'utilisateur ne peut pas être vide\",\n      \"about\": \"À propos\",\n      \"pushNotifications\": \"Notifications push\",\n      \"support\": \"Support\",\n      \"joinDiscord\": \"Rejoignez-nous sur Discord\",\n      \"privacyPolicy\": \"Politique de Confidentialité\",\n      \"userAgreement\": \"Accord de l'utilisateur\",\n      \"termsAndConditions\": \"Termes et conditions\",\n      \"userprofileError\": \"Échec du chargement du profil utilisateur\",\n      \"userprofileErrorDescription\": \"Veuillez essayer de vous déconnecter et de vous reconnecter pour vérifier si le problème persiste.\",\n      \"selectLayout\": \"Sélectionner la mise en page\",\n      \"selectStartingDay\": \"Sélectionnez le jour de début\",\n      \"version\": \"Version\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"Raccourcis\",\n      \"command\": \"Commande\",\n      \"keyBinding\": \"Racourcis clavier\",\n      \"addNewCommand\": \"Ajouter une Nouvelle Commande\",\n      \"updateShortcutStep\": \"Appuyez sur la combinaison de touches souhaitée et appuyez sur ENTER\",\n      \"shortcutIsAlreadyUsed\": \"Ce raccourci est déjà utilisé pour : {conflict}\",\n      \"resetToDefault\": \"Réinitialiser les raccourcis clavier par défaut\",\n      \"couldNotLoadErrorMsg\": \"Impossible de charger les raccourcis, réessayez\",\n      \"couldNotSaveErrorMsg\": \"Impossible d'enregistrer les raccourcis. Réessayez\",\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"Insérez un nouveau paragraphe à côté du bloc de code\",\n        \"codeBlockAddTwoSpaces\": \"Insérez deux espaces au début de la ligne dans le bloc de code\",\n        \"codeBlockSelectAll\": \"Sélectionnez tout le contenu à l'intérieur d'un bloc de code\",\n        \"codeBlockPasteText\": \"Coller le texte dans le bloc de code\",\n        \"textAlignLeft\": \"Aligner le texte à gauche\",\n        \"textAlignCenter\": \"Aligner le texte au centre\",\n        \"textAlignRight\": \"Aligner le texte à droite\",\n        \"codeBlockDeleteTwoSpaces\": \"Supprimez deux espaces au début de la ligne dans le bloc de code\"\n      }\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Voulez-vous vraiment supprimer cette vue ?\",\n    \"createView\": \"Nouveau\",\n    \"title\": {\n      \"placeholder\": \"Sans titre\"\n    },\n    \"settings\": {\n      \"filter\": \"Filtrer\",\n      \"sort\": \"Trier\",\n      \"sortBy\": \"Trier par\",\n      \"properties\": \"Propriétés\",\n      \"reorderPropertiesTooltip\": \"Faites glisser pour réorganiser les propriétés\",\n      \"group\": \"Groupe\",\n      \"addFilter\": \"Ajouter un filtre\",\n      \"deleteFilter\": \"Supprimer le filtre\",\n      \"filterBy\": \"Filtrer par...\",\n      \"typeAValue\": \"Tapez une valeur...\",\n      \"layout\": \"Mise en page\",\n      \"compactMode\": \"Mode compact\",\n      \"databaseLayout\": \"Mise en page\",\n      \"editView\": \"Modifier vue\",\n      \"boardSettings\": \"Paramètres du tableau\",\n      \"calendarSettings\": \"Paramètres du calendrier\",\n      \"createView\": \"Nouvelle vue\",\n      \"duplicateView\": \"Dupliquer la vue\",\n      \"deleteView\": \"Supprimer la vue\",\n      \"numberOfVisibleFields\": \"{} affiché(s)\",\n      \"Properties\": \"Propriétés\",\n      \"viewList\": \"Vues de base de données\"\n    },\n    \"filter\": {\n      \"empty\": \"Aucun filtre actif\",\n      \"addFilter\": \"Ajouter un filtre\",\n      \"cannotFindCreatableField\": \"Impossible de trouver un champ approprié pour filtrer\",\n      \"conditon\": \"Condition\",\n      \"where\": \"Où\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Contient\",\n      \"doesNotContain\": \"Ne contient pas\",\n      \"endsWith\": \"Se termine par\",\n      \"startWith\": \"Commence par\",\n      \"is\": \"Est\",\n      \"isNot\": \"N'est pas\",\n      \"isEmpty\": \"Est vide\",\n      \"isNotEmpty\": \"N'est pas vide\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Pas\",\n        \"startWith\": \"Commence par\",\n        \"endWith\": \"Se termine par\",\n        \"isEmpty\": \"est vide\",\n        \"isNotEmpty\": \"n'est pas vide\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Coché\",\n      \"isUnchecked\": \"Décoché\",\n      \"choicechipPrefix\": {\n        \"is\": \"est\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"fait\",\n      \"isIncomplted\": \"pas fait\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"Est\",\n      \"isNot\": \"N'est pas\",\n      \"contains\": \"Contient\",\n      \"doesNotContain\": \"Ne contient pas\",\n      \"isEmpty\": \"Est vide\",\n      \"isNotEmpty\": \"N'est pas vide\"\n    },\n    \"dateFilter\": {\n      \"is\": \"Est\",\n      \"before\": \"Est avant\",\n      \"after\": \"Est après\",\n      \"onOrBefore\": \"Est le ou avant\",\n      \"onOrAfter\": \"Est le ou après\",\n      \"between\": \"Est entre\",\n      \"empty\": \"Est vide\",\n      \"notEmpty\": \"N'est pas vide\",\n      \"startDate\": \"Date de début\",\n      \"endDate\": \"Date de fin\",\n      \"choicechipPrefix\": {\n        \"before\": \"Avant\",\n        \"after\": \"Après\",\n        \"between\": \"Entre\",\n        \"onOrBefore\": \"Pendant ou avant\",\n        \"onOrAfter\": \"Pendant ou après\",\n        \"isEmpty\": \"Est vide\",\n        \"isNotEmpty\": \"N'est pas vide\"\n      }\n    },\n    \"numberFilter\": {\n      \"equal\": \"Égal\",\n      \"notEqual\": \"N'est pas égal\",\n      \"lessThan\": \"Est moins que\",\n      \"greaterThan\": \"Est plus que\",\n      \"lessThanOrEqualTo\": \"Est inférieur ou égal à\",\n      \"greaterThanOrEqualTo\": \"Est supérieur ou égal à \",\n      \"isEmpty\": \"Est vide\",\n      \"isNotEmpty\": \"N'est pas vide\"\n    },\n    \"field\": {\n      \"label\": \"Propriété\",\n      \"hide\": \"Cacher\",\n      \"show\": \"Afficher\",\n      \"insertLeft\": \"Insérer à gauche\",\n      \"insertRight\": \"Insérer à droite\",\n      \"duplicate\": \"Dupliquer\",\n      \"delete\": \"Supprimer\",\n      \"wrapCellContent\": \"Envelopper le texte\",\n      \"clear\": \"Effacer les cellules\",\n      \"switchPrimaryFieldTooltip\": \"Impossible de modifier le type de champ du champ principal\",\n      \"textFieldName\": \"Texte\",\n      \"checkboxFieldName\": \"Case à cocher\",\n      \"dateFieldName\": \"Date\",\n      \"updatedAtFieldName\": \"Dernière modification\",\n      \"createdAtFieldName\": \"Créé le\",\n      \"numberFieldName\": \"Nombre\",\n      \"singleSelectFieldName\": \"Sélectionner\",\n      \"multiSelectFieldName\": \"Sélection multiple\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Check-list\",\n      \"relationFieldName\": \"Relation\",\n      \"summaryFieldName\": \"Résume IA\",\n      \"timeFieldName\": \"Horaire\",\n      \"mediaFieldName\": \"Fichiers et médias\",\n      \"translateFieldName\": \"Traduction IA\",\n      \"translateTo\": \"Traduire en\",\n      \"numberFormat\": \"Format du nombre\",\n      \"dateFormat\": \"Format de la date\",\n      \"includeTime\": \"Inclure l'heure\",\n      \"isRange\": \"Date de fin\",\n      \"dateFormatFriendly\": \"Mois Jour, Année\",\n      \"dateFormatISO\": \"Année-Mois-Jour\",\n      \"dateFormatLocal\": \"Mois/Jour/Année\",\n      \"dateFormatUS\": \"Année/Mois/Jour\",\n      \"dateFormatDayMonthYear\": \"Jour/Mois/Année\",\n      \"timeFormat\": \"Format de l'heure\",\n      \"invalidTimeFormat\": \"Format invalide\",\n      \"timeFormatTwelveHour\": \"12 heures\",\n      \"timeFormatTwentyFourHour\": \"24 heures\",\n      \"clearDate\": \"Effacer la date\",\n      \"dateTime\": \"Date et heure\",\n      \"startDateTime\": \"Date et heure de début\",\n      \"endDateTime\": \"Date et heure de fin\",\n      \"failedToLoadDate\": \"Échec du chargement de la valeur de la date\",\n      \"selectTime\": \"Sélectionnez l'heure\",\n      \"selectDate\": \"Sélectionner une date\",\n      \"visibility\": \"Visibilité\",\n      \"propertyType\": \"Type de propriété\",\n      \"addSelectOption\": \"Ajouter une option\",\n      \"typeANewOption\": \"Saisissez une nouvelle option\",\n      \"optionTitle\": \"Options\",\n      \"addOption\": \"Ajouter une option\",\n      \"editProperty\": \"Modifier la propriété\",\n      \"newProperty\": \"Nouvelle colonne\",\n      \"openRowDocument\": \"Ouvrir en tant que page\",\n      \"deleteFieldPromptMessage\": \"Vous voulez supprimer cette propriété ?\",\n      \"clearFieldPromptMessage\": \"Es-tu sûr? Toutes les cellules de cette colonne seront vidées\",\n      \"newColumn\": \"Nouvelle colonne\",\n      \"format\": \"Format\",\n      \"reminderOnDateTooltip\": \"Cette cellule a un rappel programmé\",\n      \"optionAlreadyExist\": \"L'option existe déjà\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Ajouter un nouveau champ\",\n      \"fieldDragElementTooltip\": \"Cliquez pour ouvrir le menu\",\n      \"showHiddenFields\": {\n        \"one\": \"Afficher {count} champ masqué\",\n        \"many\": \"Afficher {count} champs masqués\",\n        \"other\": \"Afficher {count} champs masqués\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"Cacher {count} champ caché\",\n        \"many\": \"Cacher {count} champs masqués\",\n        \"other\": \"Cacher {count} champs masqués\"\n      },\n      \"openAsFullPage\": \"Ouvrir en pleine page\",\n      \"moreRowActions\": \"Plus d'actions de ligne\"\n    },\n    \"sort\": {\n      \"ascending\": \"Ascendant\",\n      \"descending\": \"Descendant\",\n      \"by\": \"Par\",\n      \"empty\": \"Tri pas actif\",\n      \"cannotFindCreatableField\": \"Impossible de trouver un champ approprié pour trier\",\n      \"deleteAllSorts\": \"Supprimer tous les tris\",\n      \"addSort\": \"Ajouter un tri\",\n      \"sortsActive\": \"Impossible {intention} lors du tri\",\n      \"removeSorting\": \"Voulez-vous supprimer le tri ?\",\n      \"fieldInUse\": \"Vous êtes déjà en train de trier par ce champ\",\n      \"deleteSort\": \"Supprimer le tri\"\n    },\n    \"row\": {\n      \"label\": \"Ligne\",\n      \"duplicate\": \"Dupliquer\",\n      \"delete\": \"Supprimer\",\n      \"titlePlaceholder\": \"Sans titre\",\n      \"textPlaceholder\": \"Vide\",\n      \"copyProperty\": \"Copie de la propriété dans le presse-papiers\",\n      \"count\": \"Compte\",\n      \"newRow\": \"Nouvelle ligne\",\n      \"loadMore\": \"Charger plus\",\n      \"action\": \"Action\",\n      \"add\": \"Cliquez sur ajouter ci-dessous\",\n      \"drag\": \"Glisser pour déplacer\",\n      \"deleteRowPrompt\": \"Etes-vous sûr de vouloir supprimer cette ligne ? Cette action ne peut pas être annulée\",\n      \"deleteCardPrompt\": \"Etes-vous sûr de vouloir supprimer cette carte ? Cette action ne peut pas être annulée\",\n      \"dragAndClick\": \"Faites glisser pour déplacer, cliquez pour ouvrir le menu\",\n      \"insertRecordAbove\": \"Insérer l'enregistrement ci-dessus\",\n      \"insertRecordBelow\": \"Insérer l'enregistrement ci-dessous\",\n      \"noContent\": \"Aucun contenu\",\n      \"reorderRowDescription\": \"réorganiser la ligne\",\n      \"createRowAboveDescription\": \"créer une ligne au dessus\",\n      \"createRowBelowDescription\": \"insérer une ligne ci-dessous\"\n    },\n    \"selectOption\": {\n      \"create\": \"Créer\",\n      \"purpleColor\": \"Violet\",\n      \"pinkColor\": \"Rose\",\n      \"lightPinkColor\": \"Rose clair\",\n      \"orangeColor\": \"Orange\",\n      \"yellowColor\": \"Jaune\",\n      \"limeColor\": \"Citron vert\",\n      \"greenColor\": \"Vert\",\n      \"aquaColor\": \"Turquoise\",\n      \"blueColor\": \"Bleu\",\n      \"deleteTag\": \"Supprimer l'étiquette\",\n      \"colorPanelTitle\": \"Couleurs\",\n      \"panelTitle\": \"Sélectionnez une option ou créez-en une\",\n      \"searchOption\": \"Rechercher une option\",\n      \"searchOrCreateOption\": \"Rechercher ou créer une option...\",\n      \"createNew\": \"Créer une nouvelle\",\n      \"orSelectOne\": \"Ou sélectionnez une option\",\n      \"typeANewOption\": \"Saisissez une nouvelle option\",\n      \"tagName\": \"Nom de l'étiquette\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"Description de la tâche\",\n      \"addNew\": \"Ajouter un élément\",\n      \"submitNewTask\": \"Créer\",\n      \"hideComplete\": \"Cacher les tâches terminées\",\n      \"showComplete\": \"Afficher toutes les tâches\"\n    },\n    \"url\": {\n      \"launch\": \"Ouvrir dans le navigateur\",\n      \"copy\": \"Copier l'URL\",\n      \"textFieldHint\": \"Entrez une URL\"\n    },\n    \"relation\": {\n      \"relatedDatabasePlaceLabel\": \"Base de données associée\",\n      \"relatedDatabasePlaceholder\": \"Aucun\",\n      \"inRelatedDatabase\": \"Dans\",\n      \"rowSearchTextFieldPlaceholder\": \"Recherche\",\n      \"noDatabaseSelected\": \"Aucune base de données sélectionnée, veuillez d'abord en sélectionner une dans la liste ci-dessous :\",\n      \"emptySearchResult\": \"Aucun enregistrement trouvé\",\n      \"linkedRowListLabel\": \"{count} lignes liées\",\n      \"unlinkedRowListLabel\": \"Lier une autre ligne\"\n    },\n    \"menuName\": \"Grille\",\n    \"referencedGridPrefix\": \"Vue\",\n    \"calculate\": \"Calculer\",\n    \"calculationTypeLabel\": {\n      \"none\": \"Vide\",\n      \"average\": \"Moyenne\",\n      \"max\": \"Maximum\",\n      \"median\": \"Médiane\",\n      \"min\": \"Minimum\",\n      \"sum\": \"Somme\",\n      \"count\": \"Compter\",\n      \"countEmpty\": \"Compter les cellules vides\",\n      \"countEmptyShort\": \"VIDE\",\n      \"countNonEmpty\": \"Compter les cellules non vides\",\n      \"countNonEmptyShort\": \"REMPLI\"\n    },\n    \"media\": {\n      \"rename\": \"Rebaptiser\",\n      \"download\": \"Télécharger\",\n      \"expand\": \"Développer\",\n      \"delete\": \"Supprimer\",\n      \"moreFilesHint\": \"+{}\",\n      \"addFileOrImage\": \"Ajouter un fichier ou un lien\",\n      \"attachmentsHint\": \"{}\",\n      \"addFileMobile\": \"Ajouter un fichier\",\n      \"extraCount\": \"+{}\",\n      \"deleteFileDescription\": \"Etes-vous sûr de vouloir supprimer ce fichier ? Cette action est irréversible.\",\n      \"showFileNames\": \"Afficher le nom du fichier\",\n      \"downloadSuccess\": \"Fichier téléchargé\",\n      \"downloadFailedToken\": \"Échec du téléchargement du fichier, jeton utilisateur indisponible\",\n      \"setAsCover\": \"Définir comme couverture\",\n      \"openInBrowser\": \"Ouvrir dans le navigateur\",\n      \"embedLink\": \"Intégrer le lien du fichier\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"Document\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"creating\": \"Création...\",\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Sélectionnez un tableau à lier\",\n        \"createANewBoard\": \"Créer un nouveau tableau\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Sélectionnez une grille à lier\",\n        \"createANewGrid\": \"Créer une nouvelle Grille\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Sélectionnez un calendrier à lier\",\n        \"createANewCalendar\": \"Créer un nouveau calendrier\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"Sélectionnez un Document vers lequel créer un lien\"\n      },\n      \"name\": {\n        \"textStyle\": \"Style de texte\",\n        \"list\": \"Liste\",\n        \"toggle\": \"Basculer\",\n        \"fileAndMedia\": \"Fichiers et médias\",\n        \"simpleTable\": \"Tableau simple\",\n        \"visuals\": \"Visuels\",\n        \"document\": \"Document\",\n        \"advanced\": \"Avancé\",\n        \"text\": \"Texte\",\n        \"heading1\": \"Titre 1\",\n        \"heading2\": \"Titre 2\",\n        \"heading3\": \"Titre 3\",\n        \"image\": \"Image\",\n        \"bulletedList\": \"Liste à puces\",\n        \"numberedList\": \"Liste numérotée\",\n        \"todoList\": \"Liste de choses à faire\",\n        \"doc\": \"Doc\",\n        \"linkedDoc\": \"Lien vers la page\",\n        \"grid\": \"Grille\",\n        \"linkedGrid\": \"Grille liée\",\n        \"kanban\": \"Kanban\",\n        \"linkedKanban\": \"Kanban lié\",\n        \"calendar\": \"Calendrier\",\n        \"linkedCalendar\": \"Calendrier lié\",\n        \"quote\": \"Citation\",\n        \"divider\": \"Diviseur\",\n        \"table\": \"Tableau\",\n        \"callout\": \"Appeler\",\n        \"outline\": \"Table des matières\",\n        \"mathEquation\": \"Équation mathématique\",\n        \"code\": \"Code\",\n        \"toggleList\": \"Menu dépliant\",\n        \"toggleHeading1\": \"Basculer en titre 1\",\n        \"toggleHeading2\": \"Basculer en titre 2\",\n        \"toggleHeading3\": \"Basculer en titre 3\",\n        \"emoji\": \"Émoji\",\n        \"aiWriter\": \"Rédacteur IA\",\n        \"dateOrReminder\": \"Date ou rappel\",\n        \"photoGallery\": \"Galerie de photos\",\n        \"file\": \"Fichier\",\n        \"twoColumns\": \"2 colonnes\",\n        \"threeColumns\": \"3 colonnes\",\n        \"fourColumns\": \"4 colonnes\",\n        \"checkbox\": \"Case à cocher\"\n      },\n      \"subPage\": {\n        \"name\": \"Document\",\n        \"keyword1\": \"sous-page\",\n        \"keyword2\": \"page\",\n        \"keyword3\": \"page enfant\",\n        \"keyword4\": \"insérer une page\",\n        \"keyword5\": \"page intégrée\",\n        \"keyword6\": \"nouvelle page\",\n        \"keyword7\": \"créer une page\",\n        \"keyword8\": \"document\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Contour\",\n      \"codeBlock\": \"Bloc de code\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Tableau référencé\",\n      \"referencedGrid\": \"Grille référencée\",\n      \"referencedCalendar\": \"Calendrier référencé\",\n      \"referencedDocument\": \"Document référencé\",\n      \"aiWriter\": {\n        \"userQuestion\": \"Demandez n'importe quoi à l'IA\",\n        \"continueWriting\": \"Continuer à écrire\",\n        \"fixSpelling\": \"Corriger l'orthographe et la grammaire\",\n        \"improveWriting\": \"Améliorer l'écriture\",\n        \"summarize\": \"Résumer\",\n        \"explain\": \"Expliquer\",\n        \"makeShorter\": \"Rendre plus court\",\n        \"makeLonger\": \"Rallonger\"\n      },\n      \"autoGeneratorMenuItemName\": \"Rédacteur AI\",\n      \"autoGeneratorTitleName\": \"AI : Demandez à l'IA d'écrire quelque chose...\",\n      \"autoGeneratorLearnMore\": \"Apprendre encore plus\",\n      \"autoGeneratorGenerate\": \"Générer\",\n      \"autoGeneratorHintText\": \"Demandez à AI...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Impossible d'obtenir la clé AI\",\n      \"autoGeneratorRewrite\": \"Réécrire\",\n      \"smartEdit\": \"Assistants IA\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"Corriger l'orthographe\",\n      \"warning\": \"⚠️ Les réponses de l'IA peuvent être inexactes ou trompeuses.\",\n      \"smartEditSummarize\": \"Résumer\",\n      \"smartEditImproveWriting\": \"Améliorer l'écriture\",\n      \"smartEditMakeLonger\": \"Rallonger\",\n      \"smartEditCouldNotFetchResult\": \"Impossible de récupérer le résultat d'AI\",\n      \"smartEditCouldNotFetchKey\": \"Impossible de récupérer la clé AI\",\n      \"smartEditDisabled\": \"Connectez AI dans les paramètres\",\n      \"appflowyAIEditDisabled\": \"Connectez-vous pour activer les fonctionnalités de l'IA\",\n      \"discardResponse\": \"Voulez-vous supprimer les réponses de l'IA ?\",\n      \"createInlineMathEquation\": \"Créer une équation\",\n      \"fonts\": \"Polices\",\n      \"insertDate\": \"Insérer la date\",\n      \"emoji\": \"Emoji\",\n      \"toggleList\": \"Liste pliable\",\n      \"emptyToggleHeading\": \"Bouton vide h{}. Cliquez pour ajouter du contenu.\",\n      \"emptyToggleList\": \"Liste vide. Cliquez pour ajouter du contenu.\",\n      \"emptyToggleHeadingWeb\": \"Bascule h{level} vide. Cliquer pour ajouter du contenu \",\n      \"quoteList\": \"Liste de citations\",\n      \"numberedList\": \"Liste numérotée\",\n      \"bulletedList\": \"Liste à puces\",\n      \"todoList\": \"Liste de tâches\",\n      \"callout\": \"Encadré\",\n      \"simpleTable\": {\n        \"moreActions\": {\n          \"color\": \"Couleur\",\n          \"align\": \"Aligner\",\n          \"delete\": \"Supprimer\",\n          \"duplicate\": \"Dupliquer\",\n          \"insertLeft\": \"Insérer à gauche\",\n          \"insertRight\": \"Insérer à droite\",\n          \"insertAbove\": \"Insérer ci-dessus\",\n          \"insertBelow\": \"Insérer ci-dessous\",\n          \"headerColumn\": \"Colonne d'en-tête\",\n          \"headerRow\": \"Ligne d'en-tête\",\n          \"clearContents\": \"Supprimer le contenu\",\n          \"setToPageWidth\": \"Définir sur la largeur de la page\",\n          \"distributeColumnsWidth\": \"Répartir les colonnes uniformément\",\n          \"duplicateRow\": \"Ligne dupliquée\",\n          \"duplicateColumn\": \"Colonne dupliquée\",\n          \"textColor\": \"Couleur du texte\",\n          \"cellBackgroundColor\": \"Couleur d'arrière-plan de la cellule\",\n          \"duplicateTable\": \"Tableau dupliqué\"\n        },\n        \"clickToAddNewRow\": \"Cliquez pour ajouter une nouvelle ligne\",\n        \"clickToAddNewColumn\": \"Cliquez pour ajouter une nouvelle colonne\",\n        \"clickToAddNewRowAndColumn\": \"Cliquez pour ajouter une nouvelle ligne et une nouvelle colonne\",\n        \"headerName\": {\n          \"table\": \"Tableau\",\n          \"alignText\": \"Aligner le texte\"\n        }\n      },\n      \"cover\": {\n        \"changeCover\": \"Changer la couverture\",\n        \"colors\": \"Couleurs\",\n        \"images\": \"Images\",\n        \"clearAll\": \"Tout Effacer\",\n        \"abstract\": \"Abstrait\",\n        \"addCover\": \"Ajouter une couverture\",\n        \"addLocalImage\": \"Ajouter une image locale\",\n        \"invalidImageUrl\": \"URL d'image non valide\",\n        \"failedToAddImageToGallery\": \"Impossible d'ajouter l'image à la galerie\",\n        \"enterImageUrl\": \"Entrez l'URL de l'image\",\n        \"add\": \"Ajouter\",\n        \"back\": \"Dos\",\n        \"saveToGallery\": \"Sauvegarder dans la gallerie\",\n        \"removeIcon\": \"Supprimer l'icône\",\n        \"removeCover\": \"Supprimer la couverture\",\n        \"pasteImageUrl\": \"Coller l'URL de l'image\",\n        \"or\": \"OU\",\n        \"pickFromFiles\": \"Choisissez parmi les fichiers\",\n        \"couldNotFetchImage\": \"Impossible de récupérer l'image\",\n        \"imageSavingFailed\": \"Échec de l'enregistrement de l'image\",\n        \"addIcon\": \"Ajouter une icône\",\n        \"changeIcon\": \"Changer l'icône\",\n        \"coverRemoveAlert\": \"Il sera retiré de la couverture après sa suppression.\",\n        \"alertDialogConfirmation\": \"Voulez-vous vraiment continuer?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"Équation mathématique\",\n        \"addMathEquation\": \"Ajouter une équation mathématique\",\n        \"editMathEquation\": \"Modifier l'équation mathématique\"\n      },\n      \"optionAction\": {\n        \"click\": \"Cliquez sur\",\n        \"toOpenMenu\": \" pour ouvrir le menu\",\n        \"drag\": \"Glisser\",\n        \"toMove\": \" à déplacer\",\n        \"delete\": \"Supprimer\",\n        \"duplicate\": \"Dupliquer\",\n        \"turnInto\": \"Changer en\",\n        \"moveUp\": \"Déplacer vers le haut\",\n        \"moveDown\": \"Descendre\",\n        \"color\": \"Couleur\",\n        \"align\": \"Aligner\",\n        \"left\": \"Gauche\",\n        \"center\": \"Centre\",\n        \"right\": \"Droite\",\n        \"defaultColor\": \"Défaut\",\n        \"depth\": \"Profond\",\n        \"copyLinkToBlock\": \"Copier le lien pour bloquer\"\n      },\n      \"image\": {\n        \"addAnImage\": \"Ajouter une image\",\n        \"copiedToPasteBoard\": \"Le lien de l'image a été copié dans le presse-papiers\",\n        \"addAnImageDesktop\": \"Ajouter une image\",\n        \"addAnImageMobile\": \"Cliquez pour ajouter une ou plusieurs images\",\n        \"dropImageToInsert\": \"Déposez les images à insérer\",\n        \"imageUploadFailed\": \"Téléchargement de l'image échoué\",\n        \"imageDownloadFailed\": \"Le téléchargement de l'image a échoué, veuillez réessayer\",\n        \"imageDownloadFailedToken\": \"Le téléchargement de l'image a échoué en raison d'un jeton d'utilisateur manquant, veuillez réessayer\",\n        \"errorCode\": \"Code erreur\"\n      },\n      \"photoGallery\": {\n        \"name\": \"Galerie de photos\",\n        \"imageKeyword\": \"image\",\n        \"imageGalleryKeyword\": \"Galerie d'images\",\n        \"photoKeyword\": \"photo\",\n        \"photoBrowserKeyword\": \"navigateur de photos\",\n        \"galleryKeyword\": \"galerie\",\n        \"addImageTooltip\": \"Ajouter une image\",\n        \"changeLayoutTooltip\": \"Changer la mise en page\",\n        \"browserLayout\": \"Navigateur\",\n        \"gridLayout\": \"Grille\",\n        \"deleteBlockTooltip\": \"Supprimer toute la galerie\"\n      },\n      \"math\": {\n        \"copiedToPasteBoard\": \"L'équation mathématique a été copiée dans le presse-papiers\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"Le lien a été copié dans le presse-papier\",\n        \"convertToLink\": \"Convertir en lien intégré\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Ajoutez des titres pour créer une table des matières.\",\n        \"noMatchHeadings\": \"Aucune rubrique correspondante trouvée.\"\n      },\n      \"table\": {\n        \"addAfter\": \"Ajouter après\",\n        \"addBefore\": \"Ajouter avant\",\n        \"delete\": \"Supprimer\",\n        \"clear\": \"Éffacer contenu\",\n        \"duplicate\": \"Dupliquer\",\n        \"bgColor\": \"Couleur de fond\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Copier\",\n        \"cut\": \"Couper\",\n        \"paste\": \"Coller\",\n        \"pasteAsPlainText\": \"Coller en tant que texte brut\"\n      },\n      \"action\": \"Actions\",\n      \"database\": {\n        \"selectDataSource\": \"Sélectionnez la source de données\",\n        \"noDataSource\": \"Aucune source de données\",\n        \"selectADataSource\": \"Sélectionnez une source de données\",\n        \"toContinue\": \"pour continuer\",\n        \"newDatabase\": \"Nouvelle Base de données\",\n        \"linkToDatabase\": \"Lien vers la Base de données\"\n      },\n      \"date\": \"Date\",\n      \"video\": {\n        \"label\": \"Vidéo\",\n        \"emptyLabel\": \"Ajouter une vidéo\",\n        \"placeholder\": \"Collez le lien vidéo\",\n        \"copiedToPasteBoard\": \"Le lien vidéo a été copié dans le presse-papiers\",\n        \"insertVideo\": \"Ajouter une vidéo\",\n        \"invalidVideoUrl\": \"L'URL source n'est pas encore prise en charge.\",\n        \"invalidVideoUrlYouTube\": \"YouTube n'est pas encore pris en charge.\",\n        \"supportedFormats\": \"Formats pris en charge : MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264\"\n      },\n      \"file\": {\n        \"name\": \"Fichier\",\n        \"uploadTab\": \"Télécharger\",\n        \"uploadMobile\": \"Choisissez un fichier\",\n        \"uploadMobileGallery\": \"Depuis la galerie photo\",\n        \"networkTab\": \"Intégrer un lien\",\n        \"placeholderText\": \"Télécharger ou intégrer un fichier\",\n        \"placeholderDragging\": \"Glisser le fichier à télécharger\",\n        \"dropFileToUpload\": \"Glisser le fichier à télécharger\",\n        \"fileUploadHint\": \"Glisser un fichier ici pour le télécharger\\nou cliquez pour parcourir\",\n        \"fileUploadHintSuffix\": \"Parcourir\",\n        \"networkHint\": \"Coller un lien de fichier\",\n        \"networkUrlInvalid\": \"URL non valide, veuillez corriger l'URL et réessayer\",\n        \"networkAction\": \"Intégrer\",\n        \"fileTooBigError\": \"La taille du fichier est trop grande, veuillez télécharger un fichier d'une taille inférieure à 10 Mo\",\n        \"renameFile\": {\n          \"title\": \"Renommer le fichier\",\n          \"description\": \"Entrez le nouveau nom pour ce fichier\",\n          \"nameEmptyError\": \"Le nom du fichier ne peut pas être laissé vide.\"\n        },\n        \"uploadedAt\": \"Mis en ligne le {}\",\n        \"linkedAt\": \"Lien ajouté le {}\",\n        \"failedToOpenMsg\": \"Impossible d'ouvrir, fichier non trouvé\"\n      },\n      \"subPage\": {\n        \"errors\": {\n          \"failedDeletePage\": \"Impossible de supprimer la page\",\n          \"failedCreatePage\": \"Échec de la création de la page\",\n          \"failedMovePage\": \"Impossible de déplacer la page vers ce document\",\n          \"failedDuplicatePage\": \"Impossible de dupliquer la page\",\n          \"failedDuplicateFindView\": \"Impossible de dupliquer la page - vue d'origine non trouvée\"\n        }\n      },\n      \"cannotMoveToItsChildren\": \"Ne peut pas se déplacer vers ses enfants\",\n      \"linkPreview\": {\n        \"typeSelection\": {\n          \"pasteAs\": \"Coller comme\",\n          \"mention\": \"Mention\",\n          \"URL\": \"URL\",\n          \"bookmark\": \"Signet\",\n          \"embed\": \"Intégrer\"\n        },\n        \"linkPreviewMenu\": {\n          \"toMetion\": \"Convertir en mention\",\n          \"toUrl\": \"Convertir en URL\",\n          \"toEmbed\": \"Convertir en intégration\",\n          \"toBookmark\": \"Convertir en signet\",\n          \"copyLink\": \"Copier le lien\",\n          \"replace\": \"Remplacer\",\n          \"reload\": \"Recharger\",\n          \"removeLink\": \"Supprimer le lien\",\n          \"pasteHint\": \"Coller en https://...\",\n          \"unableToDisplay\": \"impossible d'afficher\"\n        }\n      }\n    },\n    \"outlineBlock\": {\n      \"placeholder\": \"Table de contenu\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Tapez '/' pour les commandes\"\n    },\n    \"title\": {\n      \"placeholder\": \"Sans titre\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Cliquez pour ajouter une image\",\n      \"upload\": {\n        \"label\": \"Téléverser\",\n        \"placeholder\": \"Cliquez pour téléverser l'image\"\n      },\n      \"url\": {\n        \"label\": \"URL de l'image\",\n        \"placeholder\": \"Entrez l'URL de l'image\"\n      },\n      \"ai\": {\n        \"label\": \"Générer une image à partir d'AI\",\n        \"placeholder\": \"Veuillez saisir l'invite pour qu'AI génère l'image\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Générer une image à partir de Stability AI\",\n        \"placeholder\": \"Veuillez saisir l'invite permettant à Stability AI de générer une image.\"\n      },\n      \"support\": \"La limite de taille d'image est de 5 Mo. Formats pris en charge : JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Image invalide\",\n        \"invalidImageSize\": \"La taille de l'image doit être inférieure à 5 Mo\",\n        \"invalidImageFormat\": \"Le format d'image n'est pas pris en charge. Formats pris en charge : JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"URL d'image non valide\",\n        \"noImage\": \"Aucun fichier ou répertoire de ce nom\",\n        \"multipleImagesFailed\": \"Une ou plusieurs images n'ont pas pu être téléchargées, veuillez réessayer\"\n      },\n      \"embedLink\": {\n        \"label\": \"Lien intégré\",\n        \"placeholder\": \"Collez ou saisissez un lien d'image\"\n      },\n      \"unsplash\": {\n        \"label\": \"Unsplash\"\n      },\n      \"searchForAnImage\": \"Rechercher une image\",\n      \"pleaseInputYourOpenAIKey\": \"veuillez saisir votre clé AI dans la page Paramètres\",\n      \"saveImageToGallery\": \"Enregistrer l'image\",\n      \"failedToAddImageToGallery\": \"Échec de l'ajout d'une image à la galerie\",\n      \"successToAddImageToGallery\": \"Image ajoutée à la galerie avec succès\",\n      \"unableToLoadImage\": \"Impossible de charger l'image\",\n      \"maximumImageSize\": \"La taille d'image maximale est 10Mo\",\n      \"uploadImageErrorImageSizeTooBig\": \"L'image doit faire moins de 10Mo\",\n      \"imageIsUploading\": \"L'image est en cours de téléchargement\",\n      \"openFullScreen\": \"Ouvrir en plein écran\",\n      \"interactiveViewer\": {\n        \"toolbar\": {\n          \"previousImageTooltip\": \"Image précédente\",\n          \"nextImageTooltip\": \"Image suivante\",\n          \"zoomOutTooltip\": \"Zoom arrière\",\n          \"zoomInTooltip\": \"Agrandir\",\n          \"changeZoomLevelTooltip\": \"Changer le niveau de zoom\",\n          \"openLocalImage\": \"Ouvrir l'image\",\n          \"downloadImage\": \"Télécharger l'image\",\n          \"closeViewer\": \"Fermer la visionneuse\",\n          \"scalePercentage\": \"{}%\",\n          \"deleteImageTooltip\": \"Supprimer l'image\"\n        }\n      },\n      \"pleaseInputYourStabilityAIKey\": \"veuillez saisir votre clé Stability AI dans la page Paramètres\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Langue\",\n        \"placeholder\": \"Choisir la langue\",\n        \"auto\": \"Auto\"\n      },\n      \"copyTooltip\": \"Copier le contenu du bloc de code\",\n      \"searchLanguageHint\": \"Rechercher une langue\",\n      \"codeCopiedSnackbar\": \"Code copié dans le presse-papier !\"\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Coller ou saisir un lien\",\n      \"openInNewTab\": \"Ouvrir dans un nouvel onglet\",\n      \"copyLink\": \"Copier le lien\",\n      \"removeLink\": \"Supprimer le lien\",\n      \"url\": {\n        \"label\": \"URL du lien\",\n        \"placeholder\": \"Entrez l'URL du lien\"\n      },\n      \"title\": {\n        \"label\": \"Titre du lien\",\n        \"placeholder\": \"Entrez le titre du lien\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"Mentionner une personne ou une page ou une date...\",\n      \"page\": {\n        \"label\": \"Lien vers la page\",\n        \"tooltip\": \"Cliquez pour ouvrir la page\"\n      },\n      \"deleted\": \"Supprimer\",\n      \"deletedContent\": \"Ce document n'existe pas ou a été supprimé\",\n      \"noAccess\": \"Pas d'accès\",\n      \"deletedPage\": \"Page supprimée\",\n      \"trashHint\": \" - à la corbeille\",\n      \"morePages\": \"plus de pages\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"Réinitialiser aux valeurs par défaut\",\n      \"textSize\": \"Taille du texte\",\n      \"textColor\": \"Couleur du texte\",\n      \"h1\": \"Titre 1\",\n      \"h2\": \"Titre 2\",\n      \"h3\": \"Titre 3\",\n      \"alignLeft\": \"Aligner à gauche\",\n      \"alignRight\": \"Aligner à droite\",\n      \"alignCenter\": \"Aligner le centre\",\n      \"link\": \"Lien\",\n      \"textAlign\": \"Alignement du texte\",\n      \"moreOptions\": \"Plus d'options\",\n      \"font\": \"Police\",\n      \"inlineCode\": \"Code en ligne\",\n      \"suggestions\": \"Suggestions\",\n      \"turnInto\": \"Devenir\",\n      \"equation\": \"Équation\",\n      \"insert\": \"Insérer\",\n      \"linkInputHint\": \"Coller un lien ou rechercher des pages\",\n      \"pageOrURL\": \"Page ou URL\",\n      \"linkName\": \"Nom du lien\",\n      \"linkNameHint\": \"Nom du lien d'entrée\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"La version actuelle ne prend pas en charge ce bloc.\",\n      \"clickToCopyTheBlockContent\": \"Cliquez pour copier le contenu du bloc\",\n      \"blockContentHasBeenCopied\": \"Le contenu du bloc a été copié.\",\n      \"parseError\": \"Une erreur s'est produite lors de l'analyse du bloc {}.\",\n      \"copyBlockContent\": \"Copier le contenu du bloc\"\n    },\n    \"mobilePageSelector\": {\n      \"title\": \"Sélectionner une page\",\n      \"failedToLoad\": \"Impossible de charger la liste des pages\",\n      \"noPagesFound\": \"Aucune page trouvée\"\n    },\n    \"attachmentMenu\": {\n      \"choosePhoto\": \"Choisir une photo\",\n      \"takePicture\": \"Prendre une photo\",\n      \"chooseFile\": \"Choisir le fichier\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"label\": \"Colonne\",\n      \"createNewCard\": \"Nouveau\",\n      \"renameGroupTooltip\": \"Appuyez pour renommer le groupe\",\n      \"createNewColumn\": \"Ajouter un nouveau groupe\",\n      \"addToColumnTopTooltip\": \"Ajouter une nouvelle carte en haut\",\n      \"addToColumnBottomTooltip\": \"Ajouter une nouvelle carte en bas\",\n      \"renameColumn\": \"Renommer\",\n      \"hideColumn\": \"Cacher\",\n      \"newGroup\": \"Nouveau groupe\",\n      \"deleteColumn\": \"Supprimer\",\n      \"deleteColumnConfirmation\": \"Cela supprimera ce groupe et toutes les cartes qu'il contient. \\nÊtes-vous sûr de vouloir continuer?\",\n      \"groupActions\": \"Actions de groupe\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"Groupes cachés\",\n      \"collapseTooltip\": \"Cacher les groupes cachés\",\n      \"expandTooltip\": \"Afficher les groupes cachés\"\n    },\n    \"cardDetail\": \"Détail de la Carte\",\n    \"cardActions\": \"Actions des Cartes\",\n    \"cardDuplicated\": \"La carte a été dupliquée\",\n    \"cardDeleted\": \"La carte a été supprimée\",\n    \"showOnCard\": \"Afficher les détails de la carte\",\n    \"setting\": \"Paramètre\",\n    \"propertyName\": \"Nom de la propriété\",\n    \"menuName\": \"Tableau\",\n    \"showUngrouped\": \"Afficher les éléments non regroupés\",\n    \"ungroupedButtonText\": \"Non groupé\",\n    \"ungroupedButtonTooltip\": \"Contient des cartes qui n'appartiennent à aucun groupe\",\n    \"ungroupedItemsTitle\": \"Cliquez pour ajouter au tableau\",\n    \"groupBy\": \"Regrouper par\",\n    \"groupCondition\": \"Condition de groupe\",\n    \"referencedBoardPrefix\": \"Vue\",\n    \"notesTooltip\": \"Notes à l'intérieur\",\n    \"mobile\": {\n      \"editURL\": \"Modifier l'URL\",\n      \"showGroup\": \"Afficher le groupe\",\n      \"showGroupContent\": \"Êtes-vous sûr de vouloir afficher ce groupe sur le tableau ?\",\n      \"failedToLoad\": \"Échec du chargement de la vue du tableau\"\n    },\n    \"dateCondition\": {\n      \"weekOf\": \"Semaine de {} - {}\",\n      \"today\": \"Aujourd'hui\",\n      \"yesterday\": \"Hier\",\n      \"tomorrow\": \"Demain\",\n      \"lastSevenDays\": \"7 derniers jours\",\n      \"nextSevenDays\": \"7 prochains jours\",\n      \"lastThirtyDays\": \"30 derniers jours\",\n      \"nextThirtyDays\": \"30 prochains jours\"\n    },\n    \"noGroup\": \"Pas de groupe par propriété\",\n    \"noGroupDesc\": \"Les vues du tableau nécessitent une propriété de regroupement pour pouvoir s'afficher\",\n    \"media\": {\n      \"cardText\": \"{} {}\",\n      \"fallbackName\": \"fichiers\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Calendrier\",\n    \"defaultNewCalendarTitle\": \"Sans titre\",\n    \"newEventButtonTooltip\": \"Ajouter un nouvel événement\",\n    \"navigation\": {\n      \"today\": \"Aujourd'hui\",\n      \"jumpToday\": \"Aller à aujourd'hui\",\n      \"previousMonth\": \"Mois précédent\",\n      \"nextMonth\": \"Mois prochain\",\n      \"views\": {\n        \"day\": \"Jour\",\n        \"week\": \"Semaine\",\n        \"month\": \"Mois\",\n        \"year\": \"Année\"\n      }\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"Pas d'événements\",\n      \"emptyBody\": \"Cliquez sur le bouton plus pour créer un événement à cette date.\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Afficher les numéros de semaine\",\n      \"showWeekends\": \"Afficher les week-ends\",\n      \"firstDayOfWeek\": \"Commencer la semaine le\",\n      \"layoutDateField\": \"Calendrier de mise en page par\",\n      \"changeLayoutDateField\": \"Modifier le champ de mise en page\",\n      \"noDateTitle\": \"Pas de date\",\n      \"unscheduledEventsTitle\": \"Événements non planifiés\",\n      \"clickToAdd\": \"Cliquez pour ajouter au calendrier\",\n      \"name\": \"Disposition du calendrier\",\n      \"clickToOpen\": \"Cliquez pour ouvrir l'évènement\",\n      \"noDateHint\": \"Les événements non planifiés s'afficheront ici\"\n    },\n    \"referencedCalendarPrefix\": \"Vue\",\n    \"quickJumpYear\": \"Sauter à\",\n    \"duplicateEvent\": \"Événement en double\"\n  },\n  \"errorDialog\": {\n    \"title\": \"Erreur @:appName\",\n    \"howToFixFallback\": \"Nous sommes désolés pour le désagrément ! Soumettez un problème sur notre page GitHub qui décrit votre erreur.\",\n    \"howToFixFallbackHint1\": \"Nous sommes désolés pour la gêne occasionnée ! Soumettez un problème sur notre \",\n    \"howToFixFallbackHint2\": \" page qui décrit votre erreur.\",\n    \"github\": \"Afficher sur GitHub\"\n  },\n  \"search\": {\n    \"label\": \"Recherche\",\n    \"sidebarSearchIcon\": \"Rechercher et accéder rapidement à une page\",\n    \"searchOrAskAI\": \"Rechercher ou demander à l'IA\",\n    \"askAIAnything\": \"Demandez n'importe quoi à l'IA\",\n    \"askAIFor\": \"Demandez à l'IA\",\n    \"noResultForSearching\": \"Aucun résultat pour « {} »\",\n    \"noResultForSearchingHint\": \"Certains résultats peuvent être dans vos pages supprimées\",\n    \"bestMatch\": \"Meilleure correspondance\",\n    \"placeholder\": {\n      \"actions\": \"Actions de recherche...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Copié !\",\n      \"fail\": \"Impossible de copier\"\n    }\n  },\n  \"unSupportBlock\": \"La version actuelle ne prend pas en charge ce bloc.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Voulez-vous vraiment supprimer le {pageType}?\",\n    \"deleteContentCaption\": \"si vous supprimez ce {pageType}, vous pouvez le restaurer à partir de la corbeille.\"\n  },\n  \"colors\": {\n    \"custom\": \"Personnalisé\",\n    \"default\": \"Défaut\",\n    \"red\": \"Rouge\",\n    \"orange\": \"Orange\",\n    \"yellow\": \"Jaune\",\n    \"green\": \"Vert\",\n    \"blue\": \"Bleu\",\n    \"purple\": \"Violet\",\n    \"pink\": \"Rose\",\n    \"brown\": \"Marron\",\n    \"gray\": \"Gris\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"Émoji\",\n    \"search\": \"Chercher un émoji\",\n    \"noRecent\": \"Aucun émoji récent\",\n    \"noEmojiFound\": \"Aucun émoji trouvé\",\n    \"filter\": \"Filtrer\",\n    \"random\": \"Aléatoire\",\n    \"selectSkinTone\": \"Choisir le teint de la peau\",\n    \"remove\": \"Supprimer l'émoji\",\n    \"categories\": {\n      \"smileys\": \"Smileys & émoticônes\",\n      \"people\": \"Personnes & corps\",\n      \"animals\": \"Animaux & Nature\",\n      \"food\": \"Nourriture & Boisson\",\n      \"activities\": \"Activités\",\n      \"places\": \"Voyages & Lieux\",\n      \"objects\": \"Objets\",\n      \"symbols\": \"Symboles\",\n      \"flags\": \"Drapeaux\",\n      \"nature\": \"Nature\",\n      \"frequentlyUsed\": \"Fréquemment utilisés\"\n    },\n    \"skinTone\": {\n      \"default\": \"Défaut\",\n      \"light\": \"Claire\",\n      \"mediumLight\": \"Moyennement claire\",\n      \"medium\": \"Moyen\",\n      \"mediumDark\": \"Moyennement foncé\",\n      \"dark\": \"Foncé\"\n    },\n    \"openSourceIconsFrom\": \"Icônes open source de\"\n  },\n  \"inlineActions\": {\n    \"noResults\": \"Aucun résultat\",\n    \"recentPages\": \"Pages récentes\",\n    \"pageReference\": \"Référence de page\",\n    \"docReference\": \"Référence de document\",\n    \"boardReference\": \"Référence du tableau\",\n    \"calReference\": \"Référence du calendrier\",\n    \"gridReference\": \"Référence de grille\",\n    \"date\": \"Date\",\n    \"reminder\": {\n      \"groupTitle\": \"Rappel\",\n      \"shortKeyword\": \"rappeler\"\n    },\n    \"createPage\": \"Créer une sous-page « {} »\"\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"Modifier le format de la date et de l'heure dans les paramètres\",\n    \"dateFormat\": \"Format de date\",\n    \"includeTime\": \"Inclure l'heure\",\n    \"isRange\": \"Date de fin\",\n    \"timeFormat\": \"Format de l'heure\",\n    \"clearDate\": \"Effacer la date\",\n    \"reminderLabel\": \"Rappel\",\n    \"selectReminder\": \"Sélectionnez un rappel\",\n    \"reminderOptions\": {\n      \"none\": \"Aucun\",\n      \"atTimeOfEvent\": \"Heure de l'événement\",\n      \"fiveMinsBefore\": \"5 minutes avant\",\n      \"tenMinsBefore\": \"10 minutes avant\",\n      \"fifteenMinsBefore\": \"15 minutes avant\",\n      \"thirtyMinsBefore\": \"30 minutes avant\",\n      \"oneHourBefore\": \"1 heure avant\",\n      \"twoHoursBefore\": \"2 heures avant\",\n      \"onDayOfEvent\": \"Le jour de l'événement\",\n      \"oneDayBefore\": \"1 jour avant\",\n      \"twoDaysBefore\": \"2 jours avant\",\n      \"oneWeekBefore\": \"1 semaine avant\",\n      \"custom\": \"Personnalisé\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"Hier\",\n    \"today\": \"Aujourd'hui\",\n    \"tomorrow\": \"Demain\",\n    \"oneWeek\": \"1 semaine\"\n  },\n  \"notificationHub\": {\n    \"title\": \"Notifications\",\n    \"closeNotification\": \"Fermer la notification\",\n    \"viewNotifications\": \"Afficher les notifications\",\n    \"noNotifications\": \"Aucune notification pour le moment\",\n    \"mentionedYou\": \"vous a mentionné\",\n    \"markAsReadTooltip\": \"Marquer comme lu cette notification\",\n    \"markAsReadSucceedToast\": \"Marquer comme lu avec succès\",\n    \"markAllAsReadSucceedToast\": \"Tout marquer comme lu avec succès\",\n    \"today\": \"Aujourd'hui\",\n    \"older\": \"Plus vieux\",\n    \"mobile\": {\n      \"title\": \"Mises à jour\"\n    },\n    \"emptyTitle\": \"Vous êtes à jour !\",\n    \"emptyBody\": \"Aucune notification ou action en attente. Profitez du calme.\",\n    \"tabs\": {\n      \"inbox\": \"Boîte de réception\",\n      \"upcoming\": \"A venir\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"Tout marquer comme lu\",\n      \"showAll\": \"Tous\",\n      \"showUnreads\": \"Non lu\"\n    },\n    \"filters\": {\n      \"ascending\": \"Ascendant\",\n      \"descending\": \"Descendant\",\n      \"groupByDate\": \"Regrouper par date\",\n      \"showUnreadsOnly\": \"Afficher uniquement les éléments non lus\",\n      \"resetToDefault\": \"Réinitialiser aux valeurs par défaut\"\n    },\n    \"archievedTooltip\": \"Archiver cette notification\",\n    \"unarchievedTooltip\": \"Cette notification n'a pas été archivée.\",\n    \"markAsArchievedSucceedToast\": \"Archivage réussi\",\n    \"markAllAsArchievedSucceedToast\": \"Tout archiver avec succès\"\n  },\n  \"reminderNotification\": {\n    \"title\": \"Rappel\",\n    \"message\": \"Pensez à vérifier cela avant d'oublier !\",\n    \"tooltipDelete\": \"Supprimer\",\n    \"tooltipMarkRead\": \"Marquer comme lu\",\n    \"tooltipMarkUnread\": \"Marquer comme non lu\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"Chercher\",\n    \"previousMatch\": \"Occurence précedente\",\n    \"nextMatch\": \"Prochaine occurence\",\n    \"close\": \"Fermer\",\n    \"replace\": \"Remplacer\",\n    \"replaceAll\": \"Tout remplacer\",\n    \"noResult\": \"Aucun résultat\",\n    \"caseSensitive\": \"Sensible à la casse\",\n    \"searchMore\": \"Chercher pour trouver plus de résultat\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"Nous sommes désolés\",\n    \"loadingViewError\": \"Nous rencontrons des difficultés pour charger cette vue. Veuillez vérifier votre connexion Internet, actualiser l'application et n'hésitez pas à contacter l'équipe si le problème persiste.\",\n    \"syncError\": \"Les données ne sont pas synchronisées depuis un autre appareil\",\n    \"syncErrorHint\": \"Veuillez rouvrir cette page sur l'appareil sur lequel elle a été modifiée pour la dernière fois, puis l'ouvrir à nouveau sur l'appareil actuel.\",\n    \"clickToCopy\": \"Cliquez pour copier le code d'erreur\"\n  },\n  \"editor\": {\n    \"bold\": \"Gras\",\n    \"bulletedList\": \"Liste à puces\",\n    \"bulletedListShortForm\": \"Puces\",\n    \"checkbox\": \"Case à cocher\",\n    \"embedCode\": \"Code intégré\",\n    \"heading1\": \"H1\",\n    \"heading2\": \"H2\",\n    \"heading3\": \"H3\",\n    \"highlight\": \"Surligner\",\n    \"color\": \"Couleur\",\n    \"image\": \"Image\",\n    \"date\": \"Date\",\n    \"page\": \"Page\",\n    \"italic\": \"Italique\",\n    \"link\": \"Lien\",\n    \"numberedList\": \"Liste numérotée\",\n    \"numberedListShortForm\": \"Numéroté\",\n    \"toggleHeading1ShortForm\": \"Bascule h1\",\n    \"toggleHeading2ShortForm\": \"Bascule h2\",\n    \"toggleHeading3ShortForm\": \"Bascule h3\",\n    \"quote\": \"Citation\",\n    \"strikethrough\": \"Barré\",\n    \"text\": \"Texte\",\n    \"underline\": \"Souligner\",\n    \"fontColorDefault\": \"Défaut\",\n    \"fontColorGray\": \"Gris\",\n    \"fontColorBrown\": \"Marron\",\n    \"fontColorOrange\": \"Orange\",\n    \"fontColorYellow\": \"Jaune\",\n    \"fontColorGreen\": \"Vert\",\n    \"fontColorBlue\": \"Bleu\",\n    \"fontColorPurple\": \"Violet\",\n    \"fontColorPink\": \"Rose\",\n    \"fontColorRed\": \"Rouge\",\n    \"backgroundColorDefault\": \"Fond par défaut\",\n    \"backgroundColorGray\": \"Fond gris\",\n    \"backgroundColorBrown\": \"Fond marron\",\n    \"backgroundColorOrange\": \"Fond orange\",\n    \"backgroundColorYellow\": \"Fond jaune\",\n    \"backgroundColorGreen\": \"Fond vert\",\n    \"backgroundColorBlue\": \"Fond bleu\",\n    \"backgroundColorPurple\": \"Fond violet\",\n    \"backgroundColorPink\": \"Fond rose\",\n    \"backgroundColorRed\": \"Fond rouge\",\n    \"backgroundColorLime\": \"Fond citron vert\",\n    \"backgroundColorAqua\": \"Fond turquoise \",\n    \"done\": \"Fait\",\n    \"cancel\": \"Annuler\",\n    \"tint1\": \"Teinte 1\",\n    \"tint2\": \"Teinte 2\",\n    \"tint3\": \"Teinte 3\",\n    \"tint4\": \"Teinte 4\",\n    \"tint5\": \"Teinte 5\",\n    \"tint6\": \"Teinte 6\",\n    \"tint7\": \"Teinte 7\",\n    \"tint8\": \"Teinte 8\",\n    \"tint9\": \"Teinte 9\",\n    \"lightLightTint1\": \"Violet\",\n    \"lightLightTint2\": \"Rose\",\n    \"lightLightTint3\": \"Rose clair\",\n    \"lightLightTint4\": \"Orange\",\n    \"lightLightTint5\": \"Jaune\",\n    \"lightLightTint6\": \"Citron vert\",\n    \"lightLightTint7\": \"Vert\",\n    \"lightLightTint8\": \"Turquoise\",\n    \"lightLightTint9\": \"Bleu\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"Titre 1\",\n    \"mobileHeading2\": \"Titre 2\",\n    \"mobileHeading3\": \"Titre 3\",\n    \"mobileHeading4\": \"Titre 4\",\n    \"mobileHeading5\": \"Titre 5 \",\n    \"mobileHeading6\": \"Titre 6\",\n    \"textColor\": \"Couleur du texte\",\n    \"backgroundColor\": \"Couleur du fond\",\n    \"addYourLink\": \"Ajoutez votre lien\",\n    \"openLink\": \"Ouvrir le lien\",\n    \"copyLink\": \"Copier le lien\",\n    \"removeLink\": \"Supprimer le lien\",\n    \"editLink\": \"Modifier le lien\",\n    \"convertTo\": \"Convertir en\",\n    \"linkText\": \"Texte\",\n    \"linkTextHint\": \"Veuillez saisir du texte\",\n    \"linkAddressHint\": \"Veuillez entrer l'URL\",\n    \"highlightColor\": \"Couleur de surlignage\",\n    \"clearHighlightColor\": \"Effacer la couleur de surlignage\",\n    \"customColor\": \"Couleur personnalisée\",\n    \"hexValue\": \"Valeur hexadécimale\",\n    \"opacity\": \"Opacité\",\n    \"resetToDefaultColor\": \"Réinitialiser la couleur par défaut\",\n    \"ltr\": \"LTR\",\n    \"rtl\": \"RTL\",\n    \"auto\": \"Auto\",\n    \"cut\": \"Couper\",\n    \"copy\": \"Copier\",\n    \"paste\": \"Coller\",\n    \"find\": \"Chercher\",\n    \"select\": \"Sélectionner\",\n    \"selectAll\": \"Tout sélectionner\",\n    \"previousMatch\": \"Occurence précédente\",\n    \"nextMatch\": \"Occurence suivante\",\n    \"closeFind\": \"Fermer\",\n    \"replace\": \"Remplacer\",\n    \"replaceAll\": \"Tout remplacer\",\n    \"regex\": \"Regex\",\n    \"caseSensitive\": \"Sensible à la casse\",\n    \"uploadImage\": \"Téléverser une image\",\n    \"urlImage\": \"URL de l'image \",\n    \"incorrectLink\": \"Lien incorrect\",\n    \"upload\": \"Téléverser\",\n    \"chooseImage\": \"Choisissez une image\",\n    \"loading\": \"Chargement\",\n    \"imageLoadFailed\": \"Impossible de charger l'image\",\n    \"divider\": \"Séparateur\",\n    \"table\": \"Tableau\",\n    \"colAddBefore\": \"Ajouter avant\",\n    \"rowAddBefore\": \"Ajouter avant\",\n    \"colAddAfter\": \"Ajouter après\",\n    \"rowAddAfter\": \"Ajouter après\",\n    \"colRemove\": \"Retirer\",\n    \"rowRemove\": \"Retirer\",\n    \"colDuplicate\": \"Dupliquer\",\n    \"rowDuplicate\": \"Dupliquer\",\n    \"colClear\": \"Effacer le ontenu\",\n    \"rowClear\": \"Effacer le ontenu\",\n    \"slashPlaceHolder\": \"Tapez '/' pour insérer un bloc ou commencez à écrire\",\n    \"typeSomething\": \"Écrivez quelque chose...\",\n    \"toggleListShortForm\": \"Plier / Déplier\",\n    \"quoteListShortForm\": \"Citation\",\n    \"mathEquationShortForm\": \"Formule\",\n    \"codeBlockShortForm\": \"Code\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"Aucune page favorite\",\n    \"noFavoriteHintText\": \"Faites glisser la page vers la gauche pour l'ajouter à vos favoris\",\n    \"removeFromSidebar\": \"Supprimer de la barre latérale\",\n    \"addToSidebar\": \"Épingler sur la barre latérale\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"Entrez un / pour insérer un bloc ou commencez à taper\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"À faire\",\n    \"bulletList\": \"Liste\",\n    \"numberList\": \"Liste\",\n    \"quote\": \"Citation\",\n    \"heading\": \"Titre {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"Icône de page\",\n    \"language\": \"Langue\",\n    \"font\": \"Police \",\n    \"actions\": \"Actions\",\n    \"date\": \"Date\",\n    \"addField\": \"Ajouter un champ\",\n    \"userIcon\": \"Icône utilisateur\"\n  },\n  \"noLogFiles\": \"Il n'y a pas de log\",\n  \"newSettings\": {\n    \"myAccount\": {\n      \"title\": \"Mon compte\",\n      \"subtitle\": \"Personnalisez votre profil, gérez la sécurité de votre compte, ouvrez les clés AI ou connectez-vous à votre compte.\",\n      \"profileLabel\": \"Nom du compte et image de profil\",\n      \"profileNamePlaceholder\": \"Entrez votre nom\",\n      \"accountSecurity\": \"Sécurité du compte\",\n      \"2FA\": \"Authentification en 2 étapes\",\n      \"aiKeys\": \"Clés IA\",\n      \"accountLogin\": \"Connexion au compte\",\n      \"updateNameError\": \"Échec de la mise à jour du nom\",\n      \"updateIconError\": \"Échec de la mise à jour de l'icône\",\n      \"aboutAppFlowy\": \"À propos de @:appName\",\n      \"deleteAccount\": {\n        \"title\": \"Supprimer le compte\",\n        \"subtitle\": \"Supprimez définitivement votre compte et toutes vos données.\",\n        \"description\": \"Supprimez définitivement votre compte et supprimez l'accès à tous les espaces de travail.\",\n        \"deleteMyAccount\": \"Supprimer mon compte\",\n        \"dialogTitle\": \"Supprimer le compte\",\n        \"dialogContent1\": \"Êtes-vous sûr de vouloir supprimer définitivement votre compte ?\",\n        \"dialogContent2\": \"Cette action ne peut pas être annulée et supprimera l'accès à tous les espaces d'équipe, effaçant l'intégralité de votre compte, y compris les espaces de travail privés, et vous supprimant de tous les espaces de travail partagés.\",\n        \"confirmHint1\": \"Veuillez taper « @:newSettings.myAccount.deleteAccount.confirmHint3 » pour confirmer.\",\n        \"confirmHint2\": \"Je comprends que cette action est irréversible et supprimera définitivement mon compte et toutes les données associées.\",\n        \"confirmHint3\": \"SUPPRIMER MON COMPTE\",\n        \"checkToConfirmError\": \"Vous devez cocher la case pour confirmer la suppression\",\n        \"failedToGetCurrentUser\": \"Impossible d'obtenir l'e-mail de l'utilisateur actuel\",\n        \"confirmTextValidationFailed\": \"Votre texte de confirmation ne correspond pas à « @:newSettings.myAccount.deleteAccount.confirmHint3 »\",\n        \"deleteAccountSuccess\": \"Compte supprimé avec succès\"\n      },\n      \"password\": {\n        \"title\": \"Mot de passe\",\n        \"changePassword\": \"Changer le mot de passe\",\n        \"currentPassword\": \"Mot de passe actuel\",\n        \"newPassword\": \"Nouveau mot de passe\",\n        \"confirmNewPassword\": \"Confirmer le nouveau mot de passe\",\n        \"setupPassword\": \"Configurer le mot de passe\",\n        \"error\": {\n          \"newPasswordIsRequired\": \"Un nouveau mot de passe est requis\",\n          \"confirmPasswordIsRequired\": \"Confirmer le mot de passe est requis\",\n          \"passwordsDoNotMatch\": \"Les mots de passe ne correspondent pas\",\n          \"newPasswordIsSameAsCurrent\": \"Le nouveau mot de passe est identique au mot de passe actuel\"\n        },\n        \"toast\": {\n          \"passwordUpdatedSuccessfully\": \"Mot de passe mis à jour avec succès\",\n          \"passwordUpdatedFailed\": \"Échec de la mise à jour du mot de passe\",\n          \"passwordSetupSuccessfully\": \"Configuration du mot de passe réussie\",\n          \"passwordSetupFailed\": \"Échec de la configuration du mot de passe\"\n        },\n        \"hint\": {\n          \"enterYourCurrentPassword\": \"Entrez votre mot de passe actuel\",\n          \"enterYourNewPassword\": \"Entrez votre nouveau mot de passe\",\n          \"confirmYourNewPassword\": \"Confirmez votre nouveau mot de passe\"\n        }\n      },\n      \"myAccount\": \"Mon compte\",\n      \"myProfile\": \"Mon profil\"\n    },\n    \"workplace\": {\n      \"name\": \"Lieu de travail\",\n      \"title\": \"Paramètres du lieu de travail\",\n      \"subtitle\": \"Personnalisez l'apparence, le thème, la police, la disposition du texte, la date, l'heure et la langue de votre espace de travail.\",\n      \"workplaceName\": \"Nom du lieu de travail\",\n      \"workplaceNamePlaceholder\": \"Entrez le nom du lieu de travail\",\n      \"workplaceIcon\": \"Icône du lieu de travail\",\n      \"workplaceIconSubtitle\": \"Téléchargez une image ou utilisez un emoji pour votre espace de travail. L'icône s'affichera dans votre barre latérale et dans vos notifications\",\n      \"renameError\": \"Échec du changement de nom du lieu de travail\",\n      \"updateIconError\": \"Échec de la mise à jour de l'icône\",\n      \"chooseAnIcon\": \"Choisissez une icône\",\n      \"appearance\": {\n        \"name\": \"Apparence\",\n        \"themeMode\": {\n          \"auto\": \"Auto\",\n          \"light\": \"Claire\",\n          \"dark\": \"Sombre\"\n        },\n        \"language\": \"Langue\"\n      }\n    },\n    \"syncState\": {\n      \"syncing\": \"Synchronisation\",\n      \"synced\": \"Synchronisé\",\n      \"noNetworkConnected\": \"Aucun réseau connecté\"\n    }\n  },\n  \"pageStyle\": {\n    \"title\": \"Style de page\",\n    \"layout\": \"Mise en page\",\n    \"coverImage\": \"Image de couverture\",\n    \"pageIcon\": \"Icône de page\",\n    \"colors\": \"Couleurs\",\n    \"gradient\": \"Dégradé\",\n    \"backgroundImage\": \"Image d'arrière-plan\",\n    \"presets\": \"Préréglages\",\n    \"photo\": \"Photo\",\n    \"unsplash\": \"Unsplash\",\n    \"pageCover\": \"Couverture de page\",\n    \"none\": \"Aucun\",\n    \"openSettings\": \"Ouvrir les paramètres\",\n    \"photoPermissionTitle\": \"@:appName souhaite accéder à votre photothèque\",\n    \"photoPermissionDescription\": \"Autoriser l'accès à la photothèque pour le téléchargement d'images.\",\n    \"cameraPermissionTitle\": \"@:appName souhaite accéder à votre caméra\",\n    \"cameraPermissionDescription\": \"@:appName a besoin d'accéder à votre appareil photo pour vous permettre d'ajouter des images à vos documents à partir de l'appareil photo\",\n    \"doNotAllow\": \"Ne pas autoriser\",\n    \"image\": \"Image\"\n  },\n  \"commandPalette\": {\n    \"placeholder\": \"Tapez pour rechercher des vues...\",\n    \"bestMatches\": \"Meilleurs résultats\",\n    \"aiOverview\": \"Présentation de l'IA\",\n    \"aiOverviewSource\": \"Sources de référence\",\n    \"aiOverviewMoreDetails\": \"Plus de détails\",\n    \"pagePreview\": \"Aperçu du contenu\",\n    \"clickToOpenPage\": \"Cliquez pour ouvrir la page\",\n    \"recentHistory\": \"Historique récent\",\n    \"navigateHint\": \"naviguer\",\n    \"loadingTooltip\": \"Nous recherchons des résultats...\",\n    \"betaLabel\": \"BÊTA\",\n    \"betaTooltip\": \"Nous ne prenons actuellement en charge que la recherche de pages\",\n    \"fromTrashHint\": \"Depuis la poubelle\",\n    \"noResultsHint\": \"Nous n'avons pas trouvé ce que vous cherchez, essayez avec un autre terme.\",\n    \"clearSearchTooltip\": \"Effacer le champ de recherche\"\n  },\n  \"space\": {\n    \"delete\": \"Supprimer\",\n    \"deleteConfirmation\": \"Supprimer: \",\n    \"deleteConfirmationDescription\": \"Toutes les pages de cet espace seront supprimées et déplacées vers la corbeille, et toutes les pages publiées seront dépubliées.\",\n    \"rename\": \"Renommer l'espace\",\n    \"changeIcon\": \"Changer d'icône\",\n    \"manage\": \"Gérer l'espace\",\n    \"addNewSpace\": \"Créer un espace\",\n    \"collapseAllSubPages\": \"Réduire toutes les sous-pages\",\n    \"createNewSpace\": \"Créer un nouvel espace\",\n    \"createSpaceDescription\": \"Créez plusieurs espaces publics et privés pour mieux organiser votre travail.\",\n    \"spaceName\": \"Nom de l'espace\",\n    \"spaceNamePlaceholder\": \"par exemple, marketing, ingénierie, ressources humaines\",\n    \"permission\": \"Autorisation\",\n    \"publicPermission\": \"Publique\",\n    \"publicPermissionDescription\": \"Tous les membres de l'espace de travail avec un accès complet\",\n    \"privatePermission\": \"Privé\",\n    \"privatePermissionDescription\": \"Vous seul pouvez accéder à cet espace\",\n    \"spaceIconBackground\": \"Couleur d'arrière-plan\",\n    \"spaceIcon\": \"Icône\",\n    \"dangerZone\": \"Zone de danger\",\n    \"unableToDeleteLastSpace\": \"Impossible de supprimer le dernier espace\",\n    \"unableToDeleteSpaceNotCreatedByYou\": \"Impossible de supprimer les espaces créés par d'autres\",\n    \"enableSpacesForYourWorkspace\": \"Activer les espaces pour votre espace de travail\",\n    \"title\": \"Espaces\",\n    \"defaultSpaceName\": \"Général\",\n    \"upgradeSpaceTitle\": \"Activer les espaces\",\n    \"upgradeSpaceDescription\": \"Créez plusieurs espaces publics et privés pour mieux organiser votre espace de travail.\",\n    \"upgrade\": \"Mettre à jour\",\n    \"upgradeYourSpace\": \"Créer plusieurs espaces\",\n    \"quicklySwitch\": \"Passer rapidement à l’espace suivant\",\n    \"duplicate\": \"dupliquer l'espace \",\n    \"movePageToSpace\": \"Déplacer la page vers l'espace\",\n    \"cannotMovePageToDatabase\": \"Impossible de déplacer la page vers la base de données\",\n    \"switchSpace\": \"Changer d'espace\",\n    \"spaceNameCannotBeEmpty\": \"Le nom de l'espace ne peut pas être vide\",\n    \"success\": {\n      \"deleteSpace\": \"Espace supprimé avec succès\",\n      \"renameSpace\": \"Espace renommé avec succès\",\n      \"duplicateSpace\": \"Espace dupliqué avec succès\",\n      \"updateSpace\": \"Espace mis à jour avec succès\"\n    },\n    \"error\": {\n      \"deleteSpace\": \"Impossible de supprimer l'espace\",\n      \"renameSpace\": \"Impossible de renommer l'espace\",\n      \"duplicateSpace\": \"Impossible de dupliquer l'espace\",\n      \"updateSpace\": \"Échec de la mise à jour de l'espace\"\n    },\n    \"createSpace\": \"Créer de l'espace\",\n    \"manageSpace\": \"Gérer l'espace\",\n    \"renameSpace\": \"Renommer l'espace\",\n    \"mSpaceIconColor\": \"Couleur de l'icône de l'espace\",\n    \"mSpaceIcon\": \"Icône de l'espace\"\n  },\n  \"publish\": {\n    \"hasNotBeenPublished\": \"Cette page n'a pas encore été publiée\",\n    \"spaceHasNotBeenPublished\": \"Je n'ai pas encore pris en charge la publication d'un espace\",\n    \"reportPage\": \"Page de rapport\",\n    \"databaseHasNotBeenPublished\": \"La publication d'une base de données n'est pas encore prise en charge.\",\n    \"createdWith\": \"Créé avec\",\n    \"downloadApp\": \"Télécharger AppFlowy\",\n    \"copy\": {\n      \"codeBlock\": \"Le contenu du bloc de code a été copié dans le presse-papiers\",\n      \"imageBlock\": \"Le lien de l'image a été copié dans le presse-papiers\",\n      \"mathBlock\": \"L'équation mathématique a été copiée dans le presse-papiers\",\n      \"fileBlock\": \"Le lien du fichier a été copié dans le presse-papiers\"\n    },\n    \"containsPublishedPage\": \"Cette page contient une ou plusieurs pages publiées. Si vous continuez, elles ne seront plus publiées. Voulez-vous procéder à la suppression ?\",\n    \"publishSuccessfully\": \"Publié avec succès\",\n    \"unpublishSuccessfully\": \"Dépublié avec succès\",\n    \"publishFailed\": \"Impossible de publier\",\n    \"unpublishFailed\": \"Impossible de dépublier\",\n    \"noAccessToVisit\": \"Pas d'accès à cette page...\",\n    \"createWithAppFlowy\": \"Créer un site Web avec AppFlowy\",\n    \"fastWithAI\": \"Rapide et facile avec l'IA.\",\n    \"tryItNow\": \"Essayez maintenant\",\n    \"onlyGridViewCanBePublished\": \"Seule la vue Grille peut être publiée\",\n    \"database\": {\n      \"zero\": \"Publier {} vue sélectionné\",\n      \"one\": \"Publier {} vues sélectionnées\",\n      \"many\": \"Publier {} vues sélectionnées\",\n      \"other\": \"Publier {} vues sélectionnées\"\n    },\n    \"mustSelectPrimaryDatabase\": \"La vue principale doit être sélectionnée\",\n    \"noDatabaseSelected\": \"Aucune base de données sélectionnée, veuillez sélectionner au moins une base de données.\",\n    \"unableToDeselectPrimaryDatabase\": \"Impossible de désélectionner la base de données principale\",\n    \"saveThisPage\": \"Sauvegarder cette page\",\n    \"duplicateTitle\": \"Où souhaitez-vous ajouter\",\n    \"selectWorkspace\": \"Sélectionnez un espace de travail\",\n    \"addTo\": \"Ajouter à\",\n    \"duplicateSuccessfully\": \"Dupliqué avec succès. Vous souhaitez consulter les documents ?\",\n    \"duplicateSuccessfullyDescription\": \"Vous n'avez pas l'application ? Le téléchargement commencera automatiquement après avoir cliqué sur « Télécharger ».\",\n    \"downloadIt\": \"Télécharger\",\n    \"openApp\": \"Ouvrir dans l'application\",\n    \"duplicateFailed\": \"Duplication échouée\",\n    \"membersCount\": {\n      \"zero\": \"Aucun membre\",\n      \"one\": \"1 membre\",\n      \"many\": \"{count} membres\",\n      \"other\": \"{count} membres\"\n    },\n    \"useThisTemplate\": \"Utiliser le modèle\"\n  },\n  \"web\": {\n    \"continue\": \"Continuer\",\n    \"or\": \"ou\",\n    \"continueWithGoogle\": \"Continuer avec Google\",\n    \"continueWithGithub\": \"Continuer avec GitHub\",\n    \"continueWithDiscord\": \"Continuer avec Discord\",\n    \"continueWithApple\": \"Continuer avec Apple \",\n    \"moreOptions\": \"Plus d'options\",\n    \"collapse\": \"Réduire\",\n    \"signInAgreement\": \"En cliquant sur « Continuer » ci-dessus, vous avez accepté les conditions d'utilisation d'AppFlowy.\",\n    \"and\": \"et\",\n    \"termOfUse\": \"Termes\",\n    \"privacyPolicy\": \"politique de confidentialité\",\n    \"signInError\": \"Erreur de connexion\",\n    \"login\": \"Inscrivez-vous ou connectez-vous\",\n    \"fileBlock\": {\n      \"uploadedAt\": \"Mis en ligne le {time}\",\n      \"linkedAt\": \"Lien ajouté le {time}\",\n      \"empty\": \"Envoyer ou intégrer un fichier\",\n      \"uploadFailed\": \"Échec du téléchargement, veuillez réessayer\",\n      \"retry\": \"Réessayer\"\n    },\n    \"importNotion\": \"Importer depuis Notion\",\n    \"import\": \"Importer\",\n    \"importSuccess\": \"Téléchargé avec succès\",\n    \"importSuccessMessage\": \"Nous vous informerons lorsque l'importation sera terminée. Vous pourrez ensuite visualiser vos pages importées dans la barre latérale.\",\n    \"importFailed\": \"L'importation a échoué, veuillez vérifier le format du fichier\",\n    \"dropNotionFile\": \"Déposez votre fichier zip Notion ici pour le télécharger, ou cliquez pour parcourir\",\n    \"error\": {\n      \"pageNameIsEmpty\": \"Le nom de la page est vide, veuillez réessayer\"\n    }\n  },\n  \"globalComment\": {\n    \"comments\": \"Commentaires\",\n    \"addComment\": \"Ajouter un commentaire\",\n    \"reactedBy\": \"réagi par\",\n    \"addReaction\": \"Ajouter une réaction\",\n    \"reactedByMore\": \"et {count} autres\",\n    \"showSeconds\": {\n      \"one\": \"Il y a 1 seconde\",\n      \"other\": \"Il y a {count} secondes\",\n      \"zero\": \"Tout à l' heure\",\n      \"many\": \"Il y a {count} secondes\"\n    },\n    \"showMinutes\": {\n      \"one\": \"Il y a 1 minute\",\n      \"other\": \"Il y a {count} minutes\",\n      \"many\": \"Il y a {count} minutes\"\n    },\n    \"showHours\": {\n      \"one\": \"il y a 1 heure\",\n      \"other\": \"Il y a {count} heures\",\n      \"many\": \"Il y a {count} heures\"\n    },\n    \"showDays\": {\n      \"one\": \"Il y a 1 jour\",\n      \"other\": \"Il y a {count} jours\",\n      \"many\": \"Il y a {count} jours\"\n    },\n    \"showMonths\": {\n      \"one\": \"Il y a 1 mois\",\n      \"other\": \"Il y a {count} mois\",\n      \"many\": \"Il y a {count} mois\"\n    },\n    \"showYears\": {\n      \"one\": \"Il y a 1 an\",\n      \"other\": \"Il y a {count} ans\",\n      \"many\": \"Il y a {count} ans\"\n    },\n    \"reply\": \"Répondre\",\n    \"deleteComment\": \"Supprimer le commentaire\",\n    \"youAreNotOwner\": \"Vous n'êtes pas le propriétaire de ce commentaire\",\n    \"confirmDeleteDescription\": \"Etes-vous sûr de vouloir supprimer ce commentaire ?\",\n    \"hasBeenDeleted\": \"Supprimé\",\n    \"replyingTo\": \"En réponse à\",\n    \"noAccessDeleteComment\": \"Vous n'êtes pas autorisé à supprimer ce commentaire\",\n    \"collapse\": \"Réduire\",\n    \"readMore\": \"En savoir plus\",\n    \"failedToAddComment\": \"Problème lors de l'ajout du commentaire\",\n    \"commentAddedSuccessfully\": \"Commentaire ajouté avec succès.\",\n    \"commentAddedSuccessTip\": \"Vous venez d'ajouter ou de répondre à un commentaire. Souhaitez-vous passer en haut de la page pour voir les derniers commentaires ?\"\n  },\n  \"template\": {\n    \"asTemplate\": \"En tant que modèle\",\n    \"name\": \"Nom du modèle\",\n    \"description\": \"Description du modèle\",\n    \"about\": \"À propos du modèle\",\n    \"deleteFromTemplate\": \"Supprimer des modèles\",\n    \"preview\": \"Aperçu du modèle\",\n    \"categories\": \"Catégories de modèles\",\n    \"isNewTemplate\": \"PIN vers un nouveau modèle\",\n    \"featured\": \"PIN pour les éléments en vedette\",\n    \"relatedTemplates\": \"Modèles associés\",\n    \"requiredField\": \"{field} est requis\",\n    \"addCategory\": \"Ajouter \\\"{category}\\\"\",\n    \"addNewCategory\": \"Ajouter une nouvelle catégorie\",\n    \"addNewCreator\": \"Ajouter un nouvel auteur\",\n    \"deleteCategory\": \"Supprimer la catégorie\",\n    \"editCategory\": \"Modifier la catégorie\",\n    \"editCreator\": \"Modifier le créateur\",\n    \"category\": {\n      \"name\": \"Nom de la catégorie\",\n      \"icon\": \"Icône de la catégorie\",\n      \"bgColor\": \"Couleur d'arrière-plan de la catégorie\",\n      \"priority\": \"Priorité de la catégorie\",\n      \"desc\": \"Description de la catégorie\",\n      \"type\": \"Type de catégorie\",\n      \"icons\": \"Icônes de catégorie\",\n      \"colors\": \"Couleurs de catégorie \",\n      \"byUseCase\": \"Par cas d'utilisation\",\n      \"byFeature\": \"Par fonctionnalité\",\n      \"deleteCategory\": \"Supprimer la catégorie\",\n      \"deleteCategoryDescription\": \"Êtes-vous sûr de vouloir supprimer cette catégorie ?\",\n      \"typeToSearch\": \"Tapez pour rechercher des catégories...\"\n    },\n    \"creator\": {\n      \"label\": \"Auteur du modèle\",\n      \"name\": \"Nom de l'auteur\",\n      \"avatar\": \"Avatar de l'auteur\",\n      \"accountLinks\": \"Liens vers le compte de l'auteur\",\n      \"uploadAvatar\": \"Cliquez pour ajouter un avatar\",\n      \"deleteCreator\": \"Supprimer l'auteur\",\n      \"deleteCreatorDescription\": \"Êtes-vous sûr de vouloir supprimer cet auteur ?\",\n      \"typeToSearch\": \"Tapez pour rechercher des auteurs...\"\n    },\n    \"uploadSuccess\": \"Modèle envoyé avec succès\",\n    \"uploadSuccessDescription\": \"Votre modèle a été envoyé avec succès. Vous pouvez maintenant le visualiser dans la galerie de modèles.\",\n    \"viewTemplate\": \"Voir le modèle\",\n    \"deleteTemplate\": \"Supprimer le modèle\",\n    \"deleteSuccess\": \"Modèle supprimé avec succès\",\n    \"deleteTemplateDescription\": \"Êtes-vous sûr de vouloir supprimer ce modèle ?\",\n    \"addRelatedTemplate\": \"Ajouter un modèle associé\",\n    \"removeRelatedTemplate\": \"Supprimer le modèle associé\",\n    \"uploadAvatar\": \"Envoyer l'avatar\",\n    \"searchInCategory\": \"Rechercher dans {category}\",\n    \"label\": \"Modèle\"\n  },\n  \"fileDropzone\": {\n    \"dropFile\": \"Cliquez ou faites glisser le fichier vers cette zone pour l'envoyer\",\n    \"uploading\": \"Envoi en cours...\",\n    \"uploadFailed\": \"Envoi échoué\",\n    \"uploadSuccess\": \"Envoi réussi\",\n    \"uploadSuccessDescription\": \"Le fichier a été envoyé avec succès\",\n    \"uploadFailedDescription\": \"L'envoi du fichier a échoué\",\n    \"uploadingDescription\": \"Le fichier est en cours d'envoi\"\n  },\n  \"gallery\": {\n    \"preview\": \"Ouvrir en plein écran\",\n    \"copy\": \"Copier\",\n    \"download\": \"Télécharger\",\n    \"prev\": \"Précédent\",\n    \"next\": \"Suivant\",\n    \"resetZoom\": \"Réinitialiser le zoom\",\n    \"zoomIn\": \"Agrandir\",\n    \"zoomOut\": \"Rétrécir\"\n  },\n  \"invitation\": {\n    \"join\": \"Rejoindre\",\n    \"on\": \"sur\",\n    \"invitedBy\": \"Invité par\",\n    \"membersCount\": {\n      \"zero\": \"{count} membres\",\n      \"one\": \"{count} membre\",\n      \"many\": \"{count} membres\",\n      \"other\": \"{count} membres\"\n    },\n    \"tip\": \"Vous avez été invité à rejoindre cet espace de travail avec les coordonnées ci-dessous. Si celles-ci sont incorrectes, contactez votre administrateur pour renvoyer l'invitation.\",\n    \"joinWorkspace\": \"Rejoindre l'espace de travail\",\n    \"success\": \"Vous avez rejoint avec succès l'espace de travail\",\n    \"successMessage\": \"Vous pouvez désormais accéder à toutes les pages et espaces de travail qu'il contient.\",\n    \"openWorkspace\": \"Ouvrir AppFlowy\",\n    \"alreadyAccepted\": \"Vous avez déjà accepté l'invitation\",\n    \"errorModal\": {\n      \"title\": \"Quelque chose s'est mal passé\",\n      \"description\": \"Il est possible que votre compte actuel {email} n'ait pas accès à cet espace de travail. Veuillez vous connecter avec le compte approprié ou contacter le propriétaire de l'espace de travail pour obtenir de l'aide.\",\n      \"contactOwner\": \"Contacter le propriétaire\",\n      \"close\": \"Retour à l'accueil\",\n      \"changeAccount\": \"Changer de compte\"\n    }\n  },\n  \"requestAccess\": {\n    \"title\": \"Pas d'accès à cette page\",\n    \"subtitle\": \"Vous pouvez demander l'accès au propriétaire de cette page. Une fois approuvé, vous pourrez consulter la page.\",\n    \"requestAccess\": \"Demande d'accès\",\n    \"backToHome\": \"Retour à l'accueil\",\n    \"tip\": \"Vous êtes actuellement connecté en tant que<link/> .\",\n    \"mightBe\": \"Vous pourriez avoir besoin de<login/> avec un compte différent.\",\n    \"successful\": \"Demande envoyée avec succès\",\n    \"successfulMessage\": \"Vous serez averti une fois que le propriétaire aura approuvé votre demande.\",\n    \"requestError\": \"Échec de la demande d'accès\",\n    \"repeatRequestError\": \"Vous avez déjà demandé l'accès à cette page\"\n  },\n  \"approveAccess\": {\n    \"title\": \"Approuver la demande d'adhésion à l'espace de travail\",\n    \"requestSummary\": \"<user/>demandes d'adhésion<workspace/> et accès<page/>\",\n    \"upgrade\": \"mise à niveau\",\n    \"downloadApp\": \"Télécharger AppFlowy\",\n    \"approveButton\": \"Approuver\",\n    \"approveSuccess\": \"Approuvé avec succès\",\n    \"approveError\": \"Échec de l'approbation, assurez-vous que la limite du plan d'espace de travail n'est pas dépassée\",\n    \"getRequestInfoError\": \"Impossible d'obtenir les informations de la demande\",\n    \"memberCount\": {\n      \"zero\": \"Aucun membre\",\n      \"one\": \"1 membre\",\n      \"many\": \"{count} membres\",\n      \"other\": \"{count} membres\"\n    },\n    \"alreadyProTitle\": \"Vous avez atteint la limite du plan d'espace de travail\",\n    \"alreadyProMessage\": \"Demandez-leur de contacter<email/> pour débloquer plus de membres\",\n    \"repeatApproveError\": \"Vous avez déjà approuvé cette demande\",\n    \"ensurePlanLimit\": \"Assurez-vous que la limite du plan d'espace de travail n'est pas dépassée. Si la limite est dépassée, envisagez<upgrade/> le plan de l'espace de travail ou<download/> .\",\n    \"requestToJoin\": \"demandé à rejoindre\",\n    \"asMember\": \"en tant que membre\"\n  },\n  \"upgradePlanModal\": {\n    \"title\": \"Passer à Pro\",\n    \"message\": \"{name} a atteint la limite de membres gratuits. Passez au plan Pro pour inviter plus de membres.\",\n    \"upgradeSteps\": \"Comment mettre à niveau votre plan sur AppFlowy :\",\n    \"step1\": \"1. Accédez aux paramètres\",\n    \"step2\": \"2. Cliquez sur « Offre »\",\n    \"step3\": \"3. Sélectionnez « Changer d'offre »\",\n    \"appNote\": \"Note: \",\n    \"actionButton\": \"Mettre à niveau\",\n    \"downloadLink\": \"Télécharger l'application\",\n    \"laterButton\": \"Plus tard\",\n    \"refreshNote\": \"Après une mise à niveau réussie, cliquez sur<refresh/> pour activer vos nouvelles fonctionnalités.\",\n    \"refresh\": \"ici\"\n  },\n  \"breadcrumbs\": {\n    \"label\": \"Fil d'Ariane\"\n  },\n  \"time\": {\n    \"justNow\": \"A l'instant\",\n    \"seconds\": {\n      \"one\": \"1 seconde\",\n      \"other\": \"{count} secondes\"\n    },\n    \"minutes\": {\n      \"one\": \"1 minute\",\n      \"other\": \"{compter} minutes\"\n    },\n    \"hours\": {\n      \"one\": \"1 heure\",\n      \"other\": \"{count} heures\"\n    },\n    \"days\": {\n      \"one\": \"1 jour\",\n      \"other\": \"{count} jours\"\n    },\n    \"weeks\": {\n      \"one\": \"1 semaine\",\n      \"other\": \"{count} semaines\"\n    },\n    \"months\": {\n      \"one\": \"1 mois\",\n      \"other\": \"{count} mois\"\n    },\n    \"years\": {\n      \"one\": \"1 an\",\n      \"other\": \"{count} années\"\n    },\n    \"ago\": \"il y a\",\n    \"yesterday\": \"Hier\",\n    \"today\": \"Aujourd'hui\"\n  },\n  \"members\": {\n    \"zero\": \"Aucun membre\",\n    \"one\": \"1 membre\",\n    \"many\": \"{count} membres\",\n    \"other\": \"{count} membres\"\n  },\n  \"tabMenu\": {\n    \"close\": \"Fermer\",\n    \"closeDisabledHint\": \"Impossible de fermer un onglet épinglé, veuillez d'abord le désépingler\",\n    \"closeOthers\": \"Fermer les autres onglets\",\n    \"closeOthersHint\": \"Cela fermera tous les onglets non épinglés sauf celui-ci\",\n    \"closeOthersDisabledHint\": \"Tous les onglets sont épinglés, je ne trouve aucun onglet à fermer\",\n    \"favorite\": \"Favori\",\n    \"unfavorite\": \"Non favori\",\n    \"favoriteDisabledHint\": \"Impossible de mettre en favori cette vue\",\n    \"pinTab\": \"Épingler\",\n    \"unpinTab\": \"Désépingler\"\n  },\n  \"openFileMessage\": {\n    \"success\": \"Fichier ouvert avec succès\",\n    \"fileNotFound\": \"Fichier introuvable\",\n    \"noAppToOpenFile\": \"Aucune application pour ouvrir ce fichier\",\n    \"permissionDenied\": \"Aucune autorisation d'ouvrir ce fichier\",\n    \"unknownError\": \"Échec de l'ouverture du fichier\"\n  },\n  \"inviteMember\": {\n    \"requestInviteMembers\": \"Inviter à votre espace de travail\",\n    \"inviteFailedMemberLimit\": \"La limite de membres a été atteinte, veuillez \",\n    \"upgrade\": \"mettre à niveau\",\n    \"addEmail\": \"email@example.com, email2@example.com...\",\n    \"requestInvites\": \"Envoyer des invitations\",\n    \"inviteAlready\": \"Vous avez déjà inviter cet email: {email}\",\n    \"inviteSuccess\": \"Invitation envoyée avec succès\",\n    \"description\": \"Saisissez les adresses emails en les séparant par une virgule. Les frais sont basés sur le nombre de membres.\",\n    \"emails\": \"Email\"\n  },\n  \"quickNote\": {\n    \"label\": \"Note Rapide\",\n    \"quickNotes\": \"Notes Rapides\",\n    \"search\": \"Chercher les Notes Rapides\",\n    \"collapseFullView\": \"Fermer la vue d'ensemble\",\n    \"expandFullView\": \"Ouvrir la vue d'ensemble\",\n    \"createFailed\": \"Échec de la création de la Note Rapide\",\n    \"quickNotesEmpty\": \"Aucune Notes Rapides\",\n    \"emptyNote\": \"Note vide\",\n    \"deleteNotePrompt\": \"La note sélectionnée sera supprimée définitivement. Êtes-vous sûr de vouloir la supprimer ?\",\n    \"addNote\": \"Nouvelle Note\",\n    \"noAdditionalText\": \"Aucun texte supplémentaire\"\n  },\n  \"subscribe\": {\n    \"upgradePlanTitle\": \"Comparez et sélectionnez le plan\",\n    \"yearly\": \"Annuel\",\n    \"save\": \"Économisez {discount}%\",\n    \"monthly\": \"Mensuel\",\n    \"priceIn\": \"Prix en \",\n    \"free\": \"Gratuit\",\n    \"pro\": \"Pro\",\n    \"freeDescription\": \"Pour les particuliers jusqu'à 2 membres pour tout organiser\",\n    \"proDescription\": \"Pour les petites équipes pour gérer les projets et les connaissances de l'équipe\",\n    \"proDuration\": {\n      \"monthly\": \"par membre et par mois\\nfacturé mensuellement\",\n      \"yearly\": \"par membre et par mois\\nfacturé annuellement\"\n    },\n    \"cancel\": \"Rétrograder\",\n    \"changePlan\": \"Passer au plan Pro\",\n    \"everythingInFree\": \"Tout en Gratuit +\",\n    \"currentPlan\": \"Actuel\",\n    \"freeDuration\": \"pour toujours\",\n    \"freePoints\": {\n      \"first\": \"1 espace de travail collaboratif jusqu'à 2 membres\",\n      \"second\": \"Pages et blocs illimités\",\n      \"three\": \"5 Go de stockage\",\n      \"four\": \"Recherche intelligente\",\n      \"five\": \"20 réponses de l'IA\",\n      \"six\": \"Application mobile\",\n      \"seven\": \"Collaboration en temps réel\"\n    },\n    \"proPoints\": {\n      \"first\": \"Stockage illimité\",\n      \"second\": \"Jusqu'à 10 membres de l'espace de travail\",\n      \"three\": \"Réponses IA illimitées\",\n      \"four\": \"Téléchargements de fichiers illimités\",\n      \"five\": \"Espace de noms personnalisé\"\n    },\n    \"cancelPlan\": {\n      \"title\": \"Désolé de te voir partir\",\n      \"success\": \"Votre abonnement a été annulé avec succès\",\n      \"description\": \"Nous sommes désolés de votre départ. Vos commentaires nous aideront à améliorer AppFlowy. Veuillez prendre quelques instants pour répondre à quelques questions.\",\n      \"commonOther\": \"Autre\",\n      \"otherHint\": \"Écrivez votre réponse ici\",\n      \"questionOne\": {\n        \"question\": \"Qu'est-ce qui vous a poussé à annuler votre abonnement AppFlowy Pro ?\",\n        \"answerOne\": \"Coût trop élevé\",\n        \"answerTwo\": \"Les fonctionnalités ne répondent pas aux attentes\",\n        \"answerThree\": \"J'ai trouvé une meilleure alternative\",\n        \"answerFour\": \"Je ne l'ai pas suffisamment utilisé pour justifier la dépense\",\n        \"answerFive\": \"Problème de service ou difficultés techniques\"\n      },\n      \"questionTwo\": {\n        \"question\": \"Quelle est la probabilité que vous envisagiez de vous réabonner à AppFlowy Pro à l’avenir ?\",\n        \"answerOne\": \"Très probable\",\n        \"answerTwo\": \"Assez probable\",\n        \"answerThree\": \"Pas sûr\",\n        \"answerFour\": \"Peu probable\",\n        \"answerFive\": \"Très peu probable\"\n      },\n      \"questionThree\": {\n        \"question\": \"Quelle fonctionnalité Pro avez-vous le plus appréciée pendant votre abonnement ?\",\n        \"answerOne\": \"Collaboration multi-utilisateurs\",\n        \"answerTwo\": \"Historique des versions à plus long terme\",\n        \"answerThree\": \"Réponses IA illimitées\",\n        \"answerFour\": \"Accès aux modèles d'IA locaux\"\n      },\n      \"questionFour\": {\n        \"question\": \"Comment décririez-vous votre expérience globale avec AppFlowy ?\",\n        \"answerOne\": \"Super\",\n        \"answerTwo\": \"Bien\",\n        \"answerThree\": \"Moyenne\",\n        \"answerFour\": \"En dessous de la moyenne\",\n        \"answerFive\": \"Insatisfait\"\n      }\n    }\n  },\n  \"ai\": {\n    \"contentPolicyViolation\": \"La génération de l'image a échoué en raison d'un contenu sensible. Veuillez reformuler votre saisie et réessayer.\",\n    \"textLimitReachedDescription\": \"Votre espace de travail est à court de réponses IA gratuites. Passez à l'offre Pro ou achetez une extension IA pour accéder à des réponses illimitées.\",\n    \"imageLimitReachedDescription\": \"Vous avez épuisé votre quota d'images IA gratuites. Passez à l'offre Pro ou achetez une extension IA pour accéder à des réponses illimitées.\",\n    \"limitReachedAction\": {\n      \"textDescription\": \"Votre espace de travail est à court de réponses IA gratuites. Pour obtenir plus de réponses, veuillez\",\n      \"imageDescription\": \"Vous avez épuisé votre quota d'images IA gratuites. Veuillez\",\n      \"upgrade\": \"mise à niveau\",\n      \"toThe\": \"au\",\n      \"proPlan\": \"Plan Pro\",\n      \"orPurchaseAn\": \"ou acheter un\",\n      \"aiAddon\": \"module complémentaire d'IA\"\n    },\n    \"editing\": \"Édition\",\n    \"analyzing\": \"Analyser\",\n    \"continueWritingEmptyDocumentTitle\": \"Continuer l'écriture d'erreur\",\n    \"continueWritingEmptyDocumentDescription\": \"Nous avons du mal à développer le contenu de votre document. Rédigez une courte introduction et nous pourrons nous en occuper !\",\n    \"more\": \"Plus\",\n    \"customPrompt\": {\n      \"browsePrompts\": \"Parcourir les invites\",\n      \"usePrompt\": \"Utiliser l'invite\",\n      \"featured\": \"En vedette\",\n      \"example\": \"Exemple\",\n      \"all\": \"Tous\",\n      \"development\": \"Développement\",\n      \"writing\": \"En écrivant\",\n      \"healthAndFitness\": \"Santé et forme physique\",\n      \"business\": \"Entreprise\",\n      \"marketing\": \"Commercialisation\",\n      \"travel\": \"Voyage\",\n      \"others\": \"Autre\",\n      \"sampleOutput\": \"Exemple de sortie\",\n      \"contentSeo\": \"Contenu/SEO\",\n      \"emailMarketing\": \"Marketing par e-mail\",\n      \"paidAds\": \"Publicités payantes\",\n      \"prCommunication\": \"Relations publiques/Communication\",\n      \"recruiting\": \"Recrutement\",\n      \"sales\": \"Ventes\",\n      \"socialMedia\": \"Réseaux sociaux\",\n      \"strategy\": \"Stratégie\",\n      \"caseStudies\": \"Études de cas\",\n      \"salesCopy\": \"Texte de vente\",\n      \"learning\": \"Apprentissage\"\n    }\n  },\n  \"autoUpdate\": {\n    \"criticalUpdateTitle\": \"Mise à jour requise pour continuer\",\n    \"criticalUpdateDescription\": \"Nous avons apporté des améliorations pour améliorer votre expérience ! Veuillez mettre à jour la version {currentVersion} vers la version {newVersion} pour continuer à utiliser l'application.\",\n    \"criticalUpdateButton\": \"Mise à jour\",\n    \"bannerUpdateTitle\": \"Nouvelle version disponible !\",\n    \"bannerUpdateDescription\": \"Obtenez les dernières fonctionnalités et correctifs. Cliquez sur « Mettre à jour » pour installer.\",\n    \"bannerUpdateButton\": \"Mise à jour\",\n    \"settingsUpdateTitle\": \"Nouvelle version ({newVersion}) disponible !\",\n    \"settingsUpdateDescription\": \"Version actuelle : {currentVersion} (version officielle) → {newVersion}\",\n    \"settingsUpdateButton\": \"Mise à jour\",\n    \"settingsUpdateWhatsNew\": \"Quoi de neuf\"\n  },\n  \"lockPage\": {\n    \"lockPage\": \"Fermé\",\n    \"reLockPage\": \"Reverrouiller\",\n    \"lockTooltip\": \"Page verrouillée pour éviter toute modification accidentelle. Cliquez pour la déverrouiller.\",\n    \"pageLockedToast\": \"Page verrouillée. La modification est impossible jusqu'à ce que quelqu'un la déverrouille.\",\n    \"lockedOperationTooltip\": \"Page verrouillée pour éviter toute modification accidentelle.\"\n  },\n  \"suggestion\": {\n    \"accept\": \"Accepter\",\n    \"keep\": \"Garder\",\n    \"discard\": \"Jeter\",\n    \"close\": \"Fermer\",\n    \"tryAgain\": \"Essayer à nouveau\",\n    \"rewrite\": \"Récrire\",\n    \"insertBelow\": \"Insérer ci-dessous\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/ga-IE.json",
    "content": "{\n  \"appName\": \"Appflowy\",\n  \"defaultUsername\": \"Liom\",\n  \"welcomeText\": \"Fáilte go @:appName\",\n  \"welcomeTo\": \"Fáilte chuig\",\n  \"githubStarText\": \"Réalta ar GitHub\",\n  \"subscribeNewsletterText\": \"Liostáil le Nuachtlitir\",\n  \"letsGoButtonText\": \"Tús Tapa\",\n  \"title\": \"Teideal\",\n  \"youCanAlso\": \"Is féidir leat freisin\",\n  \"and\": \"agus\",\n  \"failedToOpenUrl\": \"Theip ar oscailt an url: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Cliceáil chun cur leis thíos\",\n    \"addAboveCmd\": \"Alt+cliceáil\",\n    \"addAboveMacCmd\": \"Option+cliceáil\",\n    \"addAboveTooltip\": \"a chur thuas\",\n    \"dragTooltip\": \"Tarraing chun bogadh\",\n    \"openMenuTooltip\": \"Cliceáil chun an roghchlár a oscailt\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Cláraigh\",\n    \"title\": \"Cláraigh le @:appName\",\n    \"getStartedText\": \"Faigh Tosaigh\",\n    \"emptyPasswordError\": \"Ní féidir le pasfhocal a bheith folamh\",\n    \"repeatPasswordEmptyError\": \"Ní féidir an pasfhocal athdhéanta a bheith folamh\",\n    \"unmatchedPasswordError\": \"Ní ionann pasfhocal athdhéanta agus pasfhocal\",\n    \"alreadyHaveAnAccount\": \"An bhfuil cuntas agat cheana féin?\",\n    \"emailHint\": \"Ríomhphost\",\n    \"passwordHint\": \"Pasfhocal\",\n    \"repeatPasswordHint\": \"Déan pasfhocal arís\",\n    \"signUpWith\": \"Cláraigh le:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Logáil isteach ar @:appName\",\n    \"loginButtonText\": \"Logáil isteach\",\n    \"loginStartWithAnonymous\": \"Lean ar aghaidh le seisiún gan ainm\",\n    \"continueAnonymousUser\": \"Lean ar aghaidh le seisiún gan ainm\",\n    \"buttonText\": \"Sínigh Isteach\",\n    \"signingInText\": \"Ag síniú isteach...\",\n    \"forgotPassword\": \"Pasfhocal Dearmadta?\",\n    \"emailHint\": \"Ríomhphost\",\n    \"passwordHint\": \"Pasfhocal\",\n    \"dontHaveAnAccount\": \"Nach bhfuil cuntas agat?\",\n    \"createAccount\": \"Cruthaigh cuntas\",\n    \"repeatPasswordEmptyError\": \"Ní féidir an pasfhocal athdhéanta a bheith folamh\",\n    \"unmatchedPasswordError\": \"Ní hionann pasfhocal athdhéanta agus pasfhocal\",\n    \"syncPromptMessage\": \"Seans go dtógfaidh sé tamall na sonraí a shioncronú. Ná dún an leathanach seo, le do thoil\",\n    \"or\": \"NÓ\",\n    \"signInWithGoogle\": \"Lean ar aghaidh le Google\",\n    \"signInWithGithub\": \"Lean ar aghaidh le GitHub\",\n    \"signInWithDiscord\": \"Lean ar aghaidh le Discord\",\n    \"signInWithApple\": \"Lean ar aghaidh le Apple\",\n    \"continueAnotherWay\": \"Lean ar aghaidh ar bhealach eile\",\n    \"signUpWithGoogle\": \"Cláraigh le Google\",\n    \"signUpWithGithub\": \"Cláraigh le Github\",\n    \"signUpWithDiscord\": \"Cláraigh le Discord\",\n    \"signInWith\": \"Lean ar aghaidh le:\",\n    \"signInWithEmail\": \"Lean ar aghaidh le Ríomhphost\",\n    \"signInWithMagicLink\": \"Lean ort\",\n    \"signUpWithMagicLink\": \"Cláraigh le Magic Link\",\n    \"pleaseInputYourEmail\": \"Cuir isteach do sheoladh ríomhphoist\",\n    \"settings\": \"Socruithe\",\n    \"magicLinkSent\": \"Magic Link seolta!\",\n    \"invalidEmail\": \"Cuir isteach seoladh ríomhphoist bailí\",\n    \"alreadyHaveAnAccount\": \"An bhfuil cuntas agat cheana féin?\",\n    \"logIn\": \"Logáil isteach\",\n    \"generalError\": \"Chuaigh rud éigin mícheart. Bain triail eile as ar ball\",\n    \"limitRateError\": \"Ar chúiseanna slándála, ní féidir leat nasc draíochta a iarraidh ach gach 60 soicind\",\n    \"anonymous\": \"Gan ainm\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Roghnaigh do spás oibre\",\n    \"defaultName\": \"Mo Spás Oibre\",\n    \"create\": \"Cruthaigh spás oibre\",\n    \"new\": \"Spás oibre nua\",\n    \"importFromNotion\": \"Iompórtáil ó Notion\",\n    \"learnMore\": \"Foghlaim níos mó\",\n    \"reset\": \"Athshocraigh spás oibre\",\n    \"renameWorkspace\": \"Athainmnigh spás oibre\",\n    \"workspaceNameCannotBeEmpty\": \"Ní féidir leis an ainm spás oibre a bheith folamh\",\n    \"hint\": \"spás oibre\",\n    \"notFoundError\": \"Spás oibre gan aimsiú\",\n    \"errorActions\": {\n      \"reportIssue\": \"Tuairiscigh saincheist\",\n      \"reportIssueOnGithub\": \"Tuairiscigh ceist faoi Github\",\n      \"exportLogFiles\": \"Easpórtáil comhaid logáil\",\n      \"reachOut\": \"Bhaint amach le Discord\"\n    },\n    \"menuTitle\": \"Spásanna oibre\",\n    \"createSuccess\": \"Cruthaíodh spás oibre go rathúil\",\n    \"leaveCurrentWorkspace\": \"Fág spás óibre\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Comhroinn\",\n    \"workInProgress\": \"Ag teacht go luath\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"Cóipeáil chuig an ngearrthaisce\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Cóipeáil nasc\",\n    \"publishToTheWeb\": \"Foilsigh don Ghréasán\",\n    \"publishToTheWebHint\": \"Cruthaigh suíomh Gréasáin le AppFlowy\",\n    \"publish\": \"Foilsigh\",\n    \"unPublish\": \"Dífhoilsiú\",\n    \"visitSite\": \"Tabhair cuairt ar an suíomh\",\n    \"publishTab\": \"Foilsigh\",\n    \"shareTab\": \"Comhroinn\"\n  },\n  \"moreAction\": {\n    \"small\": \"beag\",\n    \"medium\": \"meánach\",\n    \"large\": \"mór\",\n    \"fontSize\": \"Clómhéid\",\n    \"import\": \"Iompórtáil\",\n    \"createdAt\": \"Cruthaithe: {}\",\n    \"deleteView\": \"Scrios\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/he.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"אני\",\n  \"welcomeText\": \"ברוך בואך אל @:appName\",\n  \"welcomeTo\": \"ברוך בואך אל\",\n  \"githubStarText\": \"כוכב ב־GitHub\",\n  \"subscribeNewsletterText\": \"הרשמה לרשימת הדיוור\",\n  \"letsGoButtonText\": \"התחלה זריזה\",\n  \"title\": \"כותרת\",\n  \"youCanAlso\": \"אפשר גם\",\n  \"and\": \"וגם\",\n  \"failedToOpenUrl\": \"פתיחת הכתובת נכשלה: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"יש ללחוץ כדי להוסיף להלן\",\n    \"addAboveCmd\": \"Alt+לחיצה\",\n    \"addAboveMacCmd\": \"Option+לחיצה\",\n    \"addAboveTooltip\": \"כדי להוסיף מעל\",\n    \"dragTooltip\": \"יש לגרור כדי להזיז\",\n    \"openMenuTooltip\": \"יש ללחוץ כדי לפתוח תפריט\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"הרשמה\",\n    \"title\": \"הרשמה ל־@:appName\",\n    \"getStartedText\": \"מאיפה מתחילים\",\n    \"emptyPasswordError\": \"הסיסמה לא יכולה להיות ריקה\",\n    \"repeatPasswordEmptyError\": \"הסיסמה החוזרת לא יכולה להיות ריקה\",\n    \"unmatchedPasswordError\": \"הסיסמה החוזרת לא זהה לסיסמה\",\n    \"alreadyHaveAnAccount\": \"כבר יש לך חשבון?\",\n    \"emailHint\": \"דוא״ל\",\n    \"passwordHint\": \"סיסמה\",\n    \"repeatPasswordHint\": \"סיסמה חוזרת\",\n    \"signUpWith\": \"הרשמה עם:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"כניסה אל @:appName\",\n    \"loginButtonText\": \"כניסה\",\n    \"loginStartWithAnonymous\": \"התחלה אלמונית\",\n    \"continueAnonymousUser\": \"המשך התהליך בצורה אלמונית\",\n    \"buttonText\": \"כניסה\",\n    \"signingInText\": \"מתבצעת כניסה…\",\n    \"forgotPassword\": \"שכחת סיסמה?\",\n    \"emailHint\": \"דוא״ל\",\n    \"passwordHint\": \"סיסמה\",\n    \"dontHaveAnAccount\": \"אין לך חשבון?\",\n    \"createAccount\": \"ליצור חשבון\",\n    \"repeatPasswordEmptyError\": \"הסיסמה החוזרת לא יכולה להיות ריקה\",\n    \"unmatchedPasswordError\": \"הסיסמה החוזרת לא זהה לסיסמה\",\n    \"syncPromptMessage\": \"סנכרון הנתונים עלול לארוך זמן מה. נא לא לסגור את העמוד הזה\",\n    \"or\": \"או\",\n    \"signInWithGoogle\": \"להיכנס עם Google\",\n    \"signInWithGithub\": \"להיכנס עם Github\",\n    \"signInWithDiscord\": \"להיכנס עם Discord\",\n    \"signUpWithGoogle\": \"להירשם עם Google\",\n    \"signUpWithGithub\": \"להירשם עם Github\",\n    \"signUpWithDiscord\": \"להירשם עם Discord\",\n    \"signInWith\": \"להיכנס עם:\",\n    \"signInWithEmail\": \"להיכנס עם דוא״ל\",\n    \"signInWithMagicLink\": \"כניסה עם קישור קסם\",\n    \"signUpWithMagicLink\": \"הרשמה עם קישור קסם\",\n    \"pleaseInputYourEmail\": \"נא למלא את כתובת הדוא״ל שלך\",\n    \"settings\": \"הגדרות\",\n    \"magicLinkSent\": \"קישור קסם נשלח!\",\n    \"invalidEmail\": \"נא למלא כתובת דוא״ל תקפה\",\n    \"alreadyHaveAnAccount\": \"כבר יש לך חשבון?\",\n    \"logIn\": \"להיכנס\",\n    \"generalError\": \"משהו השתבש. נא לנסות שוב מאוחר יותר\",\n    \"limitRateError\": \"מטעמי אבטחת מידע, אפשר לבקש קישור קסם כל 60 שניות\",\n    \"magicLinkSentDescription\": \"קישור קסם נשלח לדוא״ל שלך. יש ללחוץ על הקישור כדי להשלים את הכניסה שלך למערכת.הקישור יפוג תוך 5 דקות.\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"נא לבחור את מרחב העבודה שלך\",\n    \"create\": \"יצירת מרחב עבודה\",\n    \"reset\": \"איפוס מרחב עבודה\",\n    \"renameWorkspace\": \"שינוי שם מרחב עבודה\",\n    \"resetWorkspacePrompt\": \"איפוס מרחב העבודה ימחק את כל הדפים והנתונים שבו. לאפס את מרחב העבודה? לחלופין, אפשר ליצור קשר עם צוות התמיכה לשחזור מרחב העבודה\",\n    \"hint\": \"מרחב עבודה\",\n    \"notFoundError\": \"לא נמצא מרחב עבודה\",\n    \"failedToLoad\": \"משהו השתבש! טעינת מרחב העבודה נכשלה. כדאי לנסות לסגור את כל העותקים הפתוחים של @:appName ולנסות שוב.\",\n    \"errorActions\": {\n      \"reportIssue\": \"דיווח על תקלה\",\n      \"reportIssueOnGithub\": \"דיווח על תקלה דרך GitHub\",\n      \"exportLogFiles\": \"ייצוא קובצי יומנים\",\n      \"reachOut\": \"פנייה אלינו דרך Discord\"\n    },\n    \"menuTitle\": \"מרחבי עבודה\",\n    \"deleteWorkspaceHintText\": \"למחוק את מרחב העבודה? הפעולה הזאת היא בלתי הפיכה, ועמודים שפרסמת יוסתרו.\",\n    \"createSuccess\": \"מרחב העבודה נוצר בהצלחה\",\n    \"createFailed\": \"יצירת מרחב העבודה נכשלה\",\n    \"createLimitExceeded\": \"הגעת לכמות מרחבי העבודה המרבית המותרת לחשבון שלך. כדי להוסיף מרחבי עבודה נוספים ולהמשיך בעבודה שלך, נא לבקש ב־GitHub\",\n    \"deleteSuccess\": \"מרחב העבודה נמחק בהצלחה\",\n    \"deleteFailed\": \"מחיקת מרחב העבודה נכשלה\",\n    \"openSuccess\": \"פתיחת מרחב העבודה הצליחה\",\n    \"openFailed\": \"פתיחת מרחב העבודה נכשלה\",\n    \"renameSuccess\": \"שם מרחב העבודה השתנה בהצלחה\",\n    \"renameFailed\": \"שינוי שם מרחב העבודה נכשל\",\n    \"updateIconSuccess\": \"סמל מרחב העבודה עודכן בהצלחה\",\n    \"updateIconFailed\": \"עדכון סמל מרחב העבודה נכשל\",\n    \"cannotDeleteTheOnlyWorkspace\": \"לא ניתן למחוק את מרחב העבודה היחיד\",\n    \"fetchWorkspacesFailed\": \"משיכת מרחבי העבודה נכשלה\",\n    \"leaveCurrentWorkspace\": \"יציאה ממרחב העבודה\",\n    \"leaveCurrentWorkspacePrompt\": \"לעזוב את סביבת העבודה הנוכחית?\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"שיתוף\",\n    \"workInProgress\": \"בקרוב\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"העתקה ללוח הגזירים\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"העתקת קישור\",\n    \"publishToTheWeb\": \"פרסום לאינטרנט\",\n    \"publishToTheWebHint\": \"יצירת אתר עם\",\n    \"publish\": \"פרסום\",\n    \"unPublish\": \"הסתרה\",\n    \"visitSite\": \"ביקור באתר\",\n    \"exportAsTab\": \"ייצוא בתור\",\n    \"publishTab\": \"פרסום\",\n    \"shareTab\": \"שיתוף\"\n  },\n  \"moreAction\": {\n    \"small\": \"קטן\",\n    \"medium\": \"בינוני\",\n    \"large\": \"גדול\",\n    \"fontSize\": \"גודל גופן\",\n    \"import\": \"ייבוא\",\n    \"moreOptions\": \"אפשרויות נוספות\",\n    \"wordCount\": \"כמות מילים: {}\",\n    \"charCount\": \"כמות תווים: {}\",\n    \"createdAt\": \"מועד יצירה: {}\",\n    \"deleteView\": \"מחיקה\",\n    \"duplicateView\": \"שכפול\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"טקסט ו־Markdown\",\n    \"documentFromV010\": \"מסמך מגרסה 0.1.0\",\n    \"databaseFromV010\": \"מסד נתונים מגרסה 0.1.0\",\n    \"csv\": \"CSV\",\n    \"database\": \"מסד נתונים\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"שינוי שם\",\n    \"delete\": \"מחיקה\",\n    \"duplicate\": \"שכפול\",\n    \"unfavorite\": \"הסרה מהמועדפים\",\n    \"favorite\": \"הוספה למועדפים\",\n    \"openNewTab\": \"פתיחה בלשונית חדשה\",\n    \"moveTo\": \"העברה אל\",\n    \"addToFavorites\": \"הוספה למועדפים\",\n    \"copyLink\": \"העתקת קישור\",\n    \"changeIcon\": \"החלפת סמל\",\n    \"collapseAllPages\": \"צמצום כל תת־העמודים\"\n  },\n  \"blankPageTitle\": \"עמוד ריק\",\n  \"newPageText\": \"עמוד חדש\",\n  \"newDocumentText\": \"מסמך חדש\",\n  \"newGridText\": \"טבלה חדשה\",\n  \"newCalendarText\": \"לוח שנה חדש\",\n  \"newBoardText\": \"לוח חדש\",\n  \"chat\": {\n    \"newChat\": \"שיחה עם בינה מלאכותית\",\n    \"inputMessageHint\": \"שליחת הודעה לבינה המלאכותית של @:appName\",\n    \"unsupportedCloudPrompt\": \"היכולת הזאת זמינה רק עם @:appName בענן\",\n    \"relatedQuestion\": \"קשורים\",\n    \"serverUnavailable\": \"השירות אינו זמין באופן זמני. נא לנסות שוב מאוחר יותר.\",\n    \"aiServerUnavailable\": \"🌈 אוי לא! 🌈. חד־קרן אכל לנו את התגובה. נא לנסות שוב!\",\n    \"clickToRetry\": \"נא ללחוץ לניסיון חוזר\",\n    \"regenerateAnswer\": \"יצירה מחדש\",\n    \"question1\": \"איך להשתמש בקנבן לניהול משימות\",\n    \"question2\": \"הסבר על שיטת החתימה למטרה\",\n    \"question3\": \"למה להשתמש ב־Rust\",\n    \"question4\": \"מתכון ממה שיש לי במטבח\",\n    \"aiMistakePrompt\": \"בינה מלאכותית יכולה לטעות. כדאי לאשש את המידע.\"\n  },\n  \"trash\": {\n    \"text\": \"אשפה\",\n    \"restoreAll\": \"שחזור של הכול\",\n    \"deleteAll\": \"מחיקה של הכול\",\n    \"pageHeader\": {\n      \"fileName\": \"שם קובץ\",\n      \"lastModified\": \"שינוי אחרון\",\n      \"created\": \"נוצר\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"למחוק את כל העמודים שבאשפה?\",\n      \"caption\": \"זאת פעולה בלתי הפיכה.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"לשחזר את כל העמודים מהאשפה?\",\n      \"caption\": \"זאת פעולה בלתי הפיכה.\"\n    },\n    \"mobile\": {\n      \"actions\": \"פעולות אשפה\",\n      \"empty\": \"סל האשפה ריק\",\n      \"emptyDescription\": \"אין לך קבצים שנמחקו\",\n      \"isDeleted\": \"נמחק\",\n      \"isRestored\": \"משוחזר\"\n    },\n    \"confirmDeleteTitle\": \"למחוק את העמוד הזה לצמיתות?\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"העמוד הזה הוא באשפה\",\n    \"restore\": \"שחזור עמוד\",\n    \"deletePermanent\": \"למחוק לצמיתות\"\n  },\n  \"dialogCreatePageNameHint\": \"שם העמוד\",\n  \"questionBubble\": {\n    \"shortcuts\": \"מקשי קיצור\",\n    \"whatsNew\": \"מה חדש?\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"פרטי ניפוי שגיאות\",\n      \"success\": \"פרטי ניפוי השגיאות הועתקו ללוח הגזירים!\",\n      \"fail\": \"לא ניתן להעתיק את פרטי ניפוי השגיאות ללוח הגזירים\"\n    },\n    \"feedback\": \"משוב\",\n    \"help\": \"עזרה ותמיכה\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"הסרה, שינוי שם ועוד…\",\n    \"addPageTooltip\": \"הוספת עמוד בפנים במהירות\",\n    \"defaultNewPageName\": \"ללא שם\",\n    \"renameDialog\": \"שינוי שם\"\n  },\n  \"noPagesInside\": \"אין עמודים בפנים\",\n  \"toolbar\": {\n    \"undo\": \"הסגה\",\n    \"redo\": \"ביצוע מחדש\",\n    \"bold\": \"מודגש\",\n    \"italic\": \"נטוי\",\n    \"underline\": \"קו תחתי\",\n    \"strike\": \"קו חוצה\",\n    \"numList\": \"רשימה ממוספרת\",\n    \"bulletList\": \"רשימת תבליטים\",\n    \"checkList\": \"רשימת סימונים\",\n    \"inlineCode\": \"קוד מוטבע\",\n    \"quote\": \"מקטע ציטוט\",\n    \"header\": \"כותרת עליונה\",\n    \"highlight\": \"הדגשה\",\n    \"color\": \"צבע\",\n    \"addLink\": \"הוספת קישור\",\n    \"link\": \"קישור\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"מעבר למצב בהיר\",\n    \"darkMode\": \"מעבר למצב כהה\",\n    \"openAsPage\": \"פתיחה כעמוד\",\n    \"addNewRow\": \"הוספת שורה חדשה\",\n    \"openMenu\": \"לחיצה תפתח את התפריט\",\n    \"dragRow\": \"לחיצה ארוכה תסדר את השורה מחדש\",\n    \"viewDataBase\": \"הצגת מסד הנתונים\",\n    \"referencePage\": \"זאת הפנייה אל {name}\",\n    \"addBlockBelow\": \"הוספת מקטע למטה\",\n    \"aiGenerate\": \"יצירה\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"סגירת סרגל צד\",\n    \"openSidebar\": \"פתיחת סרגל צד\",\n    \"personal\": \"אישי\",\n    \"private\": \"פרטי\",\n    \"workspace\": \"מרחב עבודה\",\n    \"favorites\": \"מועדפים\",\n    \"clickToHidePrivate\": \"לחיצה תסתיר את המרחב הפרטי\\nעמודים שיצרת כאן הם לעיניך בלבד\",\n    \"clickToHideWorkspace\": \"לחיצה תסתיר את מרחב העבודה\\nעמודים שיצרת כאן יהיו גלויים בפני כל החברים\",\n    \"clickToHidePersonal\": \"לחיצה תסתיר את המרחב האישי\",\n    \"clickToHideFavorites\": \"לחיצה תסתיר מרחב מועדף\",\n    \"addAPage\": \"הוספת עמוד חדש\",\n    \"addAPageToPrivate\": \"הוספת עמוד למרחב פרטי\",\n    \"addAPageToWorkspace\": \"הוספת עמוד למרחב עבודה פרטי\",\n    \"recent\": \"אחרונים\",\n    \"today\": \"היום\",\n    \"thisWeek\": \"השבוע\",\n    \"others\": \"מועדפים אחרים\",\n    \"justNow\": \"כרגע\",\n    \"minutesAgo\": \"לפני {count} דקות\",\n    \"lastViewed\": \"צפייה אחרונה\",\n    \"favoriteAt\": \"הוספה למועדפים ב־\",\n    \"emptyRecent\": \"אין מסמכים אחרונים\",\n    \"emptyRecentDescription\": \"בעת צפייה במסמכים הם יופיעו כאן לאיתור פשוט יותר\",\n    \"emptyFavorite\": \"אין מסמכים מועדפים\",\n    \"emptyFavoriteDescription\": \"אפשר להתחיל לעיין ולסמן מסמכים כמועדפים. הם יופיעו כאן כדי להקל על הגישה אליהם!\",\n    \"removePageFromRecent\": \"להסיר את העמוד הזה מהאחרונים?\",\n    \"removeSuccess\": \"הוסר בהצלחה\",\n    \"favoriteSpace\": \"מועדפים\",\n    \"RecentSpace\": \"אחרונים\",\n    \"Spaces\": \"מרחבים\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"פתקית יוצאה ל־Markdown\",\n      \"path\": \"מסמכיםflowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"אנשי קשר\",\n    \"whatsHappening\": \"מה קורה השבוע?\",\n    \"addContact\": \"הוספת איש קשר\",\n    \"editContact\": \"עריכת איש קשר\"\n  },\n  \"button\": {\n    \"ok\": \"אישור\",\n    \"confirm\": \"אישור\",\n    \"done\": \"בוצע\",\n    \"cancel\": \"ביטול\",\n    \"signIn\": \"כניסה\",\n    \"signOut\": \"יציאה\",\n    \"complete\": \"השלמה\",\n    \"save\": \"שמירה\",\n    \"generate\": \"יצירה\",\n    \"esc\": \"ESC\",\n    \"keep\": \"להשאיר\",\n    \"tryAgain\": \"לנסות שוב\",\n    \"discard\": \"התעלמות\",\n    \"replace\": \"החלפה\",\n    \"insertBelow\": \"הוספה מתחת\",\n    \"insertAbove\": \"הוספה מעל\",\n    \"upload\": \"העלאה\",\n    \"edit\": \"עריכה\",\n    \"delete\": \"מחיקה\",\n    \"duplicate\": \"שכפול\",\n    \"putback\": \"החזרה למקום\",\n    \"update\": \"עדכון\",\n    \"share\": \"שיתוף\",\n    \"removeFromFavorites\": \"הסרה מהמועדפים\",\n    \"removeFromRecent\": \"הסרה מהאחרונים\",\n    \"addToFavorites\": \"הוספה למועדפים\",\n    \"rename\": \"שינוי שם\",\n    \"helpCenter\": \"מרכז העזרה\",\n    \"add\": \"הוספה\",\n    \"yes\": \"כן\",\n    \"clear\": \"פינוי\",\n    \"remove\": \"להסיר\",\n    \"dontRemove\": \"לא להסיר\",\n    \"copyLink\": \"העתקת קישור\",\n    \"align\": \"יישור\",\n    \"login\": \"כניסה\",\n    \"logout\": \"יציאה\",\n    \"deleteAccount\": \"מחיקת חשבון\",\n    \"back\": \"חזרה\",\n    \"signInGoogle\": \"המשך עם Google\",\n    \"signInGithub\": \"המשך עם GitHub\",\n    \"signInDiscord\": \"המשך עם Discord\",\n    \"more\": \"עוד\",\n    \"create\": \"יצירה\",\n    \"close\": \"סגירה\"\n  },\n  \"label\": {\n    \"welcome\": \"ברוך בואך!\",\n    \"firstName\": \"שם פרטי\",\n    \"middleName\": \"שם אמצעי\",\n    \"lastName\": \"שם משפחה\",\n    \"stepX\": \"שלב {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"לא ניתן להתחבר לחשבון שלך.\",\n      \"failedMsg\": \"נא לוודא שהשלמת את תהליך הכניסה בדפדפן שלך.\"\n    },\n    \"google\": {\n      \"title\": \"כניסה עם GOOGLE\",\n      \"instruction1\": \"כדי לייבא את אנשי הקשר שלך מ־Google, צריך לאמת את היישום הזה בעזרת הדפדפן שלך.\",\n      \"instruction2\": \"יש להעתיק את הקוד ללוח הגזירים שלך בלחיצה על הסמל או על ידי בחירת הטקסט:\",\n      \"instruction3\": \"יש לנווט לקישור הבא בדפדפן שלך ולמלא את הקוד הבא:\",\n      \"instruction4\": \"יש ללחוץ על הכפתור שלהלן לאחר השלמת ההרשמה:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"הגדרות\",\n    \"accountPage\": {\n      \"menuLabel\": \"החשבון שלי\",\n      \"title\": \"החשבון שלי\",\n      \"general\": {\n        \"title\": \"שם חשבון ותמונת פרופיל\",\n        \"changeProfilePicture\": \"החלפת תמונת פרופיל\"\n      },\n      \"email\": {\n        \"title\": \"דוא״ל\",\n        \"actions\": {\n          \"change\": \"החלפת כתובת דוא״ל\"\n        }\n      },\n      \"login\": {\n        \"title\": \"כניסה לחשבון\",\n        \"loginLabel\": \"כניסה\",\n        \"logoutLabel\": \"יציאה\"\n      }\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"מרחב עבודה\",\n      \"title\": \"מרחב עבודה\",\n      \"description\": \"התאמת מראה, ערכת העיצוב, הגופן, תבנית התאריך והשעה והשפה של מרחב העבודה שלך.\",\n      \"workspaceName\": {\n        \"title\": \"שם מרחב העבודה\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"סמל מרחב העבודה\",\n        \"description\": \"אפשר להעלות תמונה או להשתמש באמוג׳י למרחב העבודה שלך. הסמל יופיע בסרגל הצד ובהתראות שלך.\"\n      },\n      \"appearance\": {\n        \"title\": \"מראה\",\n        \"description\": \"התאמת המראה, ערכת העיצוב, גופן, פריסת הטקסט, התאריך, השעה והשפה של מרחב העבודה שלך.\",\n        \"options\": {\n          \"system\": \"אוטו׳\",\n          \"light\": \"בהיר\",\n          \"dark\": \"כהה\"\n        }\n      },\n      \"theme\": {\n        \"title\": \"ערכת עיצוב\",\n        \"description\": \"נא לבחור ערכת עיצוב מוגדרת מראש או להעלות ערכת עיצוב משופרת משלך.\",\n        \"uploadCustomThemeTooltip\": \"העלאת ערכת עיצוב משופרת\"\n      },\n      \"workspaceFont\": {\n        \"title\": \"גופן מרחב עבודה\",\n        \"noFontHint\": \"הגופן לא נמצא, נא לנסות ביטוי אחר.\"\n      },\n      \"textDirection\": {\n        \"title\": \"כיוון טקסט\",\n        \"leftToRight\": \"משמאל לימין\",\n        \"rightToLeft\": \"מימין לשמאל\",\n        \"auto\": \"אוטו׳\",\n        \"enableRTLItems\": \"הפעלת פריטי סרגל כלים לכתיבה מימין לשמאל\"\n      },\n      \"layoutDirection\": {\n        \"title\": \"כיוון פריסה\",\n        \"leftToRight\": \"משמאל לימין\",\n        \"rightToLeft\": \"מימין לשמאל\"\n      },\n      \"dateTime\": {\n        \"title\": \"תאריך ושעה\",\n        \"example\": \"{} ב־{} ({})\",\n        \"24HourTime\": \"שעון 24 שעות\",\n        \"dateFormat\": {\n          \"label\": \"תבנית תאריך\",\n          \"local\": \"מקומית\",\n          \"us\": \"אמריקאית\",\n          \"iso\": \"ISO\",\n          \"friendly\": \"ידידותית\",\n          \"dmy\": \"D/M/Y\"\n        }\n      },\n      \"language\": {\n        \"title\": \"שפה\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"מחיקת מרחב עבודה\",\n        \"content\": \"למחוק את מרחב העבודה הזה? זאת פעולה בלתי הפיכה וכל הדפים שפרסמת יוסתרו.\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"עזיבת מרחב העבודה\",\n        \"content\": \"לעזוב את מרחב העבודה הזה? הגישה שלך לכל העמודים והנתונים שבתוכו תלך לאיבוד.\"\n      },\n      \"manageWorkspace\": {\n        \"title\": \"ניהול מרחב עבודה\",\n        \"leaveWorkspace\": \"עזיבת מרחב עבודה\",\n        \"deleteWorkspace\": \"מחיקת מרחב עבודה\"\n      }\n    },\n    \"manageDataPage\": {\n      \"menuLabel\": \"ניהול נתונים\",\n      \"title\": \"ניהול נתונים\",\n      \"description\": \"ניהול אחסון הנתונים מקומית או ייבוא הנתונים הקיימים שלך אל @:appName.\",\n      \"dataStorage\": {\n        \"title\": \"מקום אחסון קבצים\",\n        \"tooltip\": \"המקום בו יאוחסנו הקבצים שלך\",\n        \"actions\": {\n          \"change\": \"החלפת נתיב\",\n          \"open\": \"פתיחת תיקייה\",\n          \"openTooltip\": \"פתיחת מקום תיקיית הנתונים הנוכחית\",\n          \"copy\": \"העתקת נתיב\",\n          \"copiedHint\": \"הנתיב הועתק!\",\n          \"resetTooltip\": \"איפוס למקום ברירת המחדל\"\n        },\n        \"resetDialog\": {\n          \"title\": \"להמשיך?\",\n          \"description\": \"איפוס הנתיב למקום ברירת המחדל לאחסון הנתונים שלך. כדי לייבא את הנתונים הנוכחיים שלך מחדש, יש להעתיק את נתיב המקום הנוכחי שלך תחילה.\"\n        }\n      },\n      \"importData\": {\n        \"title\": \"ייבוא נתונים\",\n        \"tooltip\": \"ייבוא נתונים מתיקיות גיבויים/נתונים של @:appName\",\n        \"description\": \"העתקת נתונים מתיקיית נתונים חיצונית של @:appName\",\n        \"action\": \"עיון בקובץ\"\n      },\n      \"encryption\": {\n        \"title\": \"הצפנה\",\n        \"tooltip\": \"ניהול אופן האחסון וההצפנה של הנתוים שלך\",\n        \"descriptionNoEncryption\": \"הפעלת הצפנה תצפין את כל הנתונים. זה תהליך בלתי הפיך.\",\n        \"descriptionEncrypted\": \"הנתונים שלך מוצפנים.\",\n        \"action\": \"הצפנת נתונים\",\n        \"dialog\": {\n          \"title\": \"להצפין את כל הנתונים שלך?\",\n          \"description\": \"להצפין את כל הנתונים שלך ישמור עליהם בבטחה ובצורה מאובטחת. זאת פעולה בלתי הפיכה. להמשיך?\"\n        }\n      },\n      \"cache\": {\n        \"title\": \"ביטול זיכרון המטמון\",\n        \"description\": \"פינוי זיכרון המטמון של היישום, הפינוי יכול לסייע בפתרון תקלות כמו תמונות או גופנים שלא נטענים. אין לזה השפעה על הנתונים שלך.\",\n        \"dialog\": {\n          \"title\": \"להמשיך?\",\n          \"description\": \"פינוי זיכרון המטמון יגרום להורדת כל התמונות מחדש עם הטעינה. הפעולה הזאת לא תסיר או תשנה את הנתונים שלך.\",\n          \"successHint\": \"זיכרון המטמון התפנה!\"\n        }\n      },\n      \"data\": {\n        \"fixYourData\": \"תיקון הנתונים שלך\",\n        \"fixButton\": \"תיקון\",\n        \"fixYourDataDescription\": \"אם נתקלת בבעיות עם הנתונים שלך, אפשר לנסות לתקן אותן כאן.\"\n      }\n    },\n    \"shortcutsPage\": {\n      \"menuLabel\": \"מקשי קיצור\",\n      \"title\": \"מקשי קיצור\",\n      \"editBindingHint\": \"נא להקליד צירוף חדש\",\n      \"searchHint\": \"חיפוש\",\n      \"actions\": {\n        \"resetDefault\": \"איפוס ברירת מחדל\"\n      },\n      \"errorPage\": {\n        \"message\": \"טעינת מקשי הקיצור נכשלה: {}\",\n        \"howToFix\": \"נא לנסות שוב, אם הבעיה נמשכת נא לפנות אלינו דרך GitHub.\"\n      },\n      \"resetDialog\": {\n        \"title\": \"איפוס מקשי קיצור\",\n        \"description\": \"הפעולה הזאת תאפס את כל צירופי המקשים שלך לברירת המחדל, היא בלתי הפיכה, להמשיך?\",\n        \"buttonLabel\": \"איפוס\"\n      },\n      \"conflictDialog\": {\n        \"title\": \"{} נמצא בשימוש כרגע\",\n        \"descriptionPrefix\": \"צירוף המקשים הזה כבר משמש לטובת \",\n        \"descriptionSuffix\": \". החלפת צירוף המקשים הזה יסיר אותו מהשליטה ב־{}.\",\n        \"confirmLabel\": \"המשך\"\n      },\n      \"editTooltip\": \"יש ללחוץ כדי להתחיל לערוך את צירוף המקשים\",\n      \"keybindings\": {\n        \"toggleToDoList\": \"הצגת/הסתרת רשימת מטלות\",\n        \"insertNewParagraphInCodeblock\": \"הוספת פסקה חדשה\",\n        \"pasteInCodeblock\": \"הדבקה במקטע קוד\",\n        \"selectAllCodeblock\": \"בחירה בהכול\",\n        \"indentLineCodeblock\": \"הוספת שני רווחים בתחילת השורה\",\n        \"outdentLineCodeblock\": \"מחיקת שני רווחים בתחילת השורה\",\n        \"twoSpacesCursorCodeblock\": \"הוספת שני רווחים איפה שהסמן\",\n        \"copy\": \"העתקת בחירה\",\n        \"paste\": \"הדבקה בתוכן\",\n        \"cut\": \"גזירת הבחירה\",\n        \"alignLeft\": \"יישור הטקסט לשמאל\",\n        \"alignCenter\": \"מירכוז הטקסט\",\n        \"alignRight\": \"יישור הטקסט לימין\",\n        \"undo\": \"הסגה\",\n        \"redo\": \"ביצוע מחדש\",\n        \"convertToParagraph\": \"המרת מקטע לפסקה\",\n        \"backspace\": \"מחיקה\",\n        \"deleteLeftWord\": \"מחיקת המילה משמאל\",\n        \"deleteLeftSentence\": \"מחיקת המשפט משמאל\",\n        \"delete\": \"מחיקת התו מימין\",\n        \"deleteMacOS\": \"מחיקת השתו משמאל\",\n        \"deleteRightWord\": \"מחיקת המילה מימין\",\n        \"moveCursorLeft\": \"הזזת הסמל שמאלה\",\n        \"moveCursorBeginning\": \"הזזת הסמן להתחלה\",\n        \"moveCursorLeftWord\": \"הזזת הסמן מילה שמאלה\",\n        \"moveCursorLeftSelect\": \"בחירה והזזת הסמן שמאלה\",\n        \"moveCursorBeginSelect\": \"בחירה והזזת הסמן להתחלה\",\n        \"moveCursorLeftWordSelect\": \"בחירה והזזת הסמן שמאלה במילה\",\n        \"moveCursorRight\": \"הזזת הסמן ימינה\",\n        \"moveCursorEnd\": \"הזזת הסמן לסוף\",\n        \"moveCursorRightWord\": \"הזזת הסמן ימינה במילה\",\n        \"moveCursorRightSelect\": \"בחירה והזזת הסמן אחד ימינה\",\n        \"moveCursorEndSelect\": \"בחירה והזזת הסמן לסוף\",\n        \"moveCursorRightWordSelect\": \"בחירה והזזת הסמן ימינה במילה\",\n        \"moveCursorUp\": \"הזזת הסמן למעלה\",\n        \"moveCursorTopSelect\": \"בחירה והזזת הסמן לראש\",\n        \"moveCursorTop\": \"הזזת הסמן לראש\",\n        \"moveCursorUpSelect\": \"בחירה והזזת הסמן למעלה\",\n        \"moveCursorBottomSelect\": \"בחירה והזזת הסמן לתחתית\",\n        \"moveCursorBottom\": \"הזזת הסמן לתחתית\",\n        \"moveCursorDown\": \"הזזת הסמן למטה\",\n        \"moveCursorDownSelect\": \"בחירה והזזת הסמן למטה\",\n        \"home\": \"גלילה למעלה\",\n        \"end\": \"גלילה לתחתית\",\n        \"toggleBold\": \"החלפת מצב מודגש\",\n        \"toggleItalic\": \"החלפת מצב הטייה\",\n        \"toggleUnderline\": \"החלפת מצב קו תחתי\",\n        \"toggleStrikethrough\": \"החלפת קו חוצה\",\n        \"toggleCode\": \"החלפת קוד כחלק מהשורה\",\n        \"toggleHighlight\": \"החלפת מצב הדגשה\",\n        \"showLinkMenu\": \"הצגת תפריט קישורים\",\n        \"openInlineLink\": \"פתיחת קישור מובנה\",\n        \"openLinks\": \"פתיחת כל הקישורים הנבחרים\",\n        \"indent\": \"הגדלת הזחה\",\n        \"outdent\": \"הקטנת הזחה\",\n        \"exit\": \"יציאה מעריכה\",\n        \"pageUp\": \"גלילה עמוד למעלה\",\n        \"pageDown\": \"גלילה עמוד למטה\",\n        \"selectAll\": \"בחירה בהכול\",\n        \"pasteWithoutFormatting\": \"הדבקת תוכן בלי עיצוב\",\n        \"showEmojiPicker\": \"הצגת בורר אמוג׳י\",\n        \"enterInTableCell\": \"הוספת מעבר שורה בטבלה\",\n        \"leftInTableCell\": \"מעבר תא שמאלה בטבלה\",\n        \"rightInTableCell\": \"מעבר תא ימינה בטבלה\",\n        \"upInTableCell\": \"מעבר תא למעלה בטבלה\",\n        \"downInTableCell\": \"מעבר תא למטה בטבלה\",\n        \"tabInTableCell\": \"מעבר לתא הזמין הבא בטבלה\",\n        \"shiftTabInTableCell\": \"מעבר לתא הזמין הקודם בטבלה\",\n        \"backSpaceInTableCell\": \"עצירה בתחילת התא\"\n      },\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"הוספת פסקה חדשה ליד מקטע הקוד\",\n        \"codeBlockIndentLines\": \"הוספת שני רווחים בתחילת השורה במקטע קוד\",\n        \"codeBlockOutdentLines\": \"מחיקת שני רווחים מתחילת השורה במקטע קוד\",\n        \"codeBlockAddTwoSpaces\": \"הוספת שני רווחים במקום של הסמן במקטע קוד\",\n        \"codeBlockSelectAll\": \"בחירת כל התוכן בתוך מקטע קוד\",\n        \"codeBlockPasteText\": \"הדבקת טקסט במקטע קוד\",\n        \"textAlignLeft\": \"יישור טקסט לשמאל\",\n        \"textAlignCenter\": \"מירכוז טקסט\",\n        \"textAlignRight\": \"יישור טקסט לימין\"\n      },\n      \"couldNotLoadErrorMsg\": \"לא ניתן לטעון את קיצורי המקשים, נא לנסות שוב\",\n      \"couldNotSaveErrorMsg\": \"לא ניתן לשמור את קיצורי המקשים, נא לנסות שוב\"\n    },\n    \"aiPage\": {\n      \"title\": \"הגדרת בינה מלאכותית\",\n      \"menuLabel\": \"הגדרות בינה מלאכותית\",\n      \"keys\": {\n        \"enableAISearchTitle\": \"חיפוש בינה מלאכותית\",\n        \"aiSettingsDescription\": \"בחירת או הגדרת מודלים של בינה מלאכותית לשימוש עם @:appName. לביצועים מיטביים אנו ממליצים להשתמש באפשרויות ברירת המחדל של המודל\",\n        \"loginToEnableAIFeature\": \"יכולות בינה מלאכותית מופעלות רק לאחר כניסה עם @:appName בענן. אם אין לך חשבון של @:appName, יש לגשת אל ‚החשבון שלי’ כדי להירשם\",\n        \"llmModel\": \"מודל שפה\",\n        \"llmModelType\": \"סוג מודל שפה\",\n        \"downloadLLMPrompt\": \"הורדת {}\",\n        \"downloadLLMPromptDetail\": \"הורדת המודל המקומי {} תתפוס {} מהאחסון. להמשיך?\",\n        \"downloadAIModelButton\": \"הורדת מודל בינה מלאכותית\",\n        \"downloadingModel\": \"מתבצעת הורדה\",\n        \"localAILoaded\": \"מודל הבינה המלאכותית המקומי נוסף והוא מוכן לשימוש\",\n        \"localAILoading\": \"מודל הבינה המלאכותית המקומי נטען…\",\n        \"localAIStopped\": \"מודל הבינה המלאכותית המקומי נעצר\",\n        \"title\": \"מפתחות API לבינה מלאכותית\",\n        \"openAILabel\": \"מפתח API ל־OpenAI\",\n        \"openAITooltip\": \"אפשר למצוא את מפתח ה־API הסודי שלך בעמוד מפתח ה־API\",\n        \"openAIHint\": \"מילוי מפתח ה־API שלך ב־OpenAI\",\n        \"stabilityAILabel\": \"מפתח API של Stability\",\n        \"stabilityAITooltip\": \"מפתח ה־API שלך ב־Stability, משמש לאימות הבקשות שלך\",\n        \"stabilityAIHint\": \"נא למלא את מפתח ה־API שלך ב־Stability\"\n      }\n    },\n    \"planPage\": {\n      \"menuLabel\": \"תוכנית\",\n      \"title\": \"תוכנית חיוב\",\n      \"planUsage\": {\n        \"title\": \"תקציר שימוש בתוכנית\",\n        \"storageLabel\": \"אחסון\",\n        \"storageUsage\": \"{} מתוך {} ג״ב\",\n        \"collaboratorsLabel\": \"שותפים\",\n        \"collaboratorsUsage\": \"{} מתוך {}\",\n        \"aiResponseLabel\": \"תגובות בינה מלאכותית\",\n        \"aiResponseUsage\": \"{} מתוך {}\",\n        \"proBadge\": \"פרו\",\n        \"memberProToggle\": \"אין הגבלה על כמות חברים\",\n        \"aiCredit\": {\n          \"title\": \"הוספת קרדיט בינה מלאכותית ל־@:appName\",\n          \"price\": \"{}\",\n          \"priceDescription\": \"ל־1,000 קרדיטים\",\n          \"purchase\": \"רכישת בינה מלאכותית\",\n          \"info\": \"הוספת 1,000 קרדיטים של בינה מלאכותית לכל סביבת עבודה ושילוב בינה מלאכותית מותאמת למרחב העבודה שלך לתוצאות חכמות ומהירות יותר עם עד:\",\n          \"infoItemOne\": \"10,000 תגובות למסד נתונים\",\n          \"infoItemTwo\": \"1,000 תגובות לסביבת עבודה\"\n        },\n        \"currentPlan\": {\n          \"bannerLabel\": \"תוכנית נוכחית\",\n          \"freeTitle\": \"חינם\",\n          \"proTitle\": \"פרו\",\n          \"teamTitle\": \"צוות\",\n          \"freeInfo\": \"מעולה לעצמאיים או צוותים קטנים של עד 2 חברים.\",\n          \"proInfo\": \"מעולה לצוות קטנים ובינוניים עד 10 חברים.\",\n          \"teamInfo\": \"מעולה לכל סוג של צוות משימתי שמאורגן היטב.\",\n          \"upgrade\": \"השוואה\\n ושדרוג\",\n          \"canceledInfo\": \"התוכנית שלך בוטלה, יבוצע שנמוך לתוכנית החופשית ב־{}.\",\n          \"freeProOne\": \"מרחב עבודה שיתופי\",\n          \"freeProTwo\": \"עד 2 חברים (כולל הבעלים)\",\n          \"freeProThree\": \"כמות אורחים בלתי מוגבלת (צפייה בלבד)\",\n          \"freeProFour\": \"אחסון של 5 ג״ב\",\n          \"freeProFive\": \"30 ימי היסטוריית מהדורות\",\n          \"freeConOne\": \"משתתפי אורח (גישת עריכה)\",\n          \"freeConTwo\": \"ללא הגבלת אחסון\",\n          \"freeConThree\": \"6 חודשי היסטוריית מהדורות\",\n          \"professionalProOne\": \"מרחב עבודה שיתופי\",\n          \"professionalProTwo\": \"כמות חברים בלתי מוגבלת\",\n          \"professionalProThree\": \"כמות אורחים בלתי מוגבלת (צפייה בלבד)\",\n          \"professionalProFour\": \"אחסון ללא הגבלה\",\n          \"professionalProFive\": \"6 חודשי היסטוריית מהדורות\",\n          \"professionalConOne\": \"משתתפי אורח ללא הגבלה (גישת עריכה)\",\n          \"professionalConTwo\": \"תגובות מבינה מלאכותית ללא הגבלה\",\n          \"professionalConThree\": \"שנה של היסטוריית מהדורות\"\n        },\n        \"deal\": {\n          \"bannerLabel\": \"מבצע לשנה החדשה!\",\n          \"title\": \"הגדלת הצוות שלך!\",\n          \"info\": \"שדרוג כעת יזכה אותך ב־10% הנחה מתוכניות פרו ולצוותים! אפשר לחזק את יעילות סביבת העבודה שלך עם יכולות חדשות ורבות עוצמה כולל הבינה המלאכותית של @:appName.\",\n          \"viewPlans\": \"הצגת תוכניות\"\n        },\n        \"guestCollabToggle\": \"10 משתתפי אורח\",\n        \"storageUnlimited\": \"אחסון ללא הגבלה עם תוכנית הפרו שלך\"\n      }\n    },\n    \"billingPage\": {\n      \"menuLabel\": \"חיוב\",\n      \"title\": \"חיוב\",\n      \"plan\": {\n        \"title\": \"תוכנית\",\n        \"freeLabel\": \"חינם\",\n        \"proLabel\": \"פרו\",\n        \"planButtonLabel\": \"החלפת תוכנית\",\n        \"billingPeriod\": \"תקופת חיוב\",\n        \"periodButtonLabel\": \"עריכת תקופה\"\n      },\n      \"paymentDetails\": {\n        \"title\": \"פרטי תשלום\",\n        \"methodLabel\": \"שיטת תשלום\",\n        \"methodButtonLabel\": \"עריכת שיטה\"\n      }\n    },\n    \"comparePlanDialog\": {\n      \"title\": \"השוואה ובחירת תוכנית\",\n      \"planFeatures\": \"יכולות\\nהתוכנית\",\n      \"current\": \"נוכחית\",\n      \"actions\": {\n        \"upgrade\": \"שדרוג\",\n        \"downgrade\": \"שנמוך\",\n        \"current\": \"נוכחית\",\n        \"downgradeDisabledTooltip\": \"השנמוך יתבצע בסוף מחזור החיוב\"\n      },\n      \"freePlan\": {\n        \"title\": \"חינם\",\n        \"description\": \"לארגון כל פינה בעבודה ובחיים שלך.\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"חינם לנצח\"\n      },\n      \"proPlan\": {\n        \"title\": \"מקצועי\",\n        \"description\": \"מקום קטן לתכנות והתארגנות של קבוצות קטנות.\",\n        \"price\": \"{} לחודש\",\n        \"priceInfo\": \"בחיוב שנתי\"\n      },\n      \"planLabels\": {\n        \"itemOne\": \"מרחבי עבודה\",\n        \"itemTwo\": \"חברים\",\n        \"itemThree\": \"אורחים\",\n        \"itemFour\": \"משתתפי אורח\",\n        \"itemFive\": \"אחסון\",\n        \"itemSix\": \"שיתוף בזמן אמת\",\n        \"itemSeven\": \"יישומון לניידים\",\n        \"tooltipThree\": \"לאורחים יש הרשאות לקריאה בלבד לתוכן המסוים ששותף\",\n        \"tooltipFour\": \"משתתפי אורח מחויבים כמושב אחד\",\n        \"itemEight\": \"תגובות בינה מלאכותית\",\n        \"tooltipEight\": \"הכוונה בביטוי „לכל החיים” כלומר שמספר התגובות לעולם לא יתאפס\"\n      },\n      \"freeLabels\": {\n        \"itemOne\": \"חיוב לפני מרחבי עבודה\",\n        \"itemTwo\": \"3\",\n        \"itemFour\": \"0\",\n        \"itemFive\": \"5 ג״ב\",\n        \"itemSix\": \"כן\",\n        \"itemSeven\": \"כן\",\n        \"itemEight\": \"1,000 לכל החיים\"\n      },\n      \"proLabels\": {\n        \"itemOne\": \"מחויב לפי מרחב עבודה\",\n        \"itemTwo\": \"עד 10\",\n        \"itemFour\": \"10 אורחים חויבו כמושב אחד\",\n        \"itemFive\": \"ללא הגבלה\",\n        \"itemSix\": \"כן\",\n        \"itemSeven\": \"כן\",\n        \"itemEight\": \"10,000 בחודש\"\n      },\n      \"paymentSuccess\": {\n        \"title\": \"עברת לתוכנית {}!\",\n        \"description\": \"התשלום שלך עבר עיבוד והתוכנית שלך שודרגה ל־@:appName {}. אפשר לצפות בפרטי התוכנית שלך בעמוד התוכנית\"\n      },\n      \"downgradeDialog\": {\n        \"title\": \"לשנמך את התוכנית שלך?\",\n        \"description\": \"שנמוך התוכנית שלך יחזיר אותך לתוכנית החינמית. חברים עלולים לאבד גישה למרחבי העבודה ויהיה עליך לפנות מקום אחסון כדי לעמוד במגבלות האחסון של התוכנית החינמית.\",\n        \"downgradeLabel\": \"שנמוך תוכנית\"\n      }\n    },\n    \"common\": {\n      \"reset\": \"איפוס\"\n    },\n    \"menu\": {\n      \"appearance\": \"מראה\",\n      \"language\": \"שפה\",\n      \"user\": \"משתמש\",\n      \"files\": \"קבצים\",\n      \"notifications\": \"התראות\",\n      \"open\": \"פתיחת ההגדרות\",\n      \"logout\": \"יציאה\",\n      \"logoutPrompt\": \"לצאת?\",\n      \"selfEncryptionLogoutPrompt\": \"לצאת מהמערכת? נא לוודא שהעתקת את סוד ההצפנה\",\n      \"syncSetting\": \"סנכרון הגדרות\",\n      \"cloudSettings\": \"הגדרות ענן\",\n      \"enableSync\": \"הפעלת סנכרון\",\n      \"enableEncrypt\": \"הצפנת נתונים\",\n      \"cloudURL\": \"כתובת בסיס\",\n      \"invalidCloudURLScheme\": \"סכמה שגויה\",\n      \"cloudServerType\": \"שרת ענן\",\n      \"cloudServerTypeTip\": \"נא לשים לב שהפעולה הזאת עלולה להוציא אותך מהחשבון הנוכחי שלך לאחר מעבר לשרת הענן\",\n      \"cloudLocal\": \"מקומי\",\n      \"cloudAppFlowy\": \"@:appName בענן בטא\",\n      \"cloudAppFlowySelfHost\": \"@:appName בענן באירוח עצמי\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"כתובת הענן לא יכולה להישאר ריקה\",\n      \"clickToCopy\": \"לחיצה להעתקה\",\n      \"selfHostStart\": \"אם אין לך שרת, נא לפנות אל\",\n      \"selfHostContent\": \"המסמך\",\n      \"selfHostEnd\": \"להנחיה בנוגע לאירוח בשרת משלך\",\n      \"cloudURLHint\": \"נא למלא את כתובת הבסיס של השרת שלך\",\n      \"cloudWSURL\": \"כתובת Websocket\",\n      \"cloudWSURLHint\": \"נא למלא את כתובת השקע המקוון (websocket) של השרת שלך\",\n      \"restartApp\": \"הפעלה מחדש\",\n      \"restartAppTip\": \"יש להפעיל את היישום מחדש כדי להחיל את השינויים. נא לשים לב שיכול להיות שתתבצע יציאה מהחשבון הנוכחי שלך.\",\n      \"changeServerTip\": \"לאחר החלפת השרת, יש ללחוץ על כפתור ההפעלה מחדש כדי שהשינויים ייכנסו לתוקף\",\n      \"enableEncryptPrompt\": \"אפשר להפעיל הצפנה כדי להגן על הנתונים שלך עם הסוד הזה. יש לאחסן אותו בצורה בטוחה, לאחר שהופעלה, אי אפשר לכבות אותה. אם הסוד אבד, לא תהיה עוד גישה לנתונים שלך. לחיצה להעתקה\",\n      \"inputEncryptPrompt\": \"נא למלא את סוד ההצפנה שלך עבור\",\n      \"clickToCopySecret\": \"לחיצה להעתקת סוד\",\n      \"configServerSetting\": \"הגדרת השרת שלך\",\n      \"configServerGuide\": \"לאחר בחירה ב`התחלה מהירה`, יש לנווט אל `הגדרות` ואז ל„הגדרות ענן” כדי להגדיר שרת באירוח עצמי.\",\n      \"inputTextFieldHint\": \"הסוד שלך\",\n      \"historicalUserList\": \"היסטוריית כניסת משתמשים\",\n      \"historicalUserListTooltip\": \"הרשימה הזאת מציגה את החשבונות האלמוניים שלך. אפשר ללחוץ על חשבון כדי לצפות בפרטים שלו. חשבונות אלמוניים נוצרים בלחיצה על הכפתור ‚איך מתחילים’\",\n      \"openHistoricalUser\": \"נא ללחוץ כדי לפתוח את החשבון האלמוני\",\n      \"customPathPrompt\": \"אחסון תיקיית הנתונים של @:appName בתיקייה שמסונכרנת עם הענן כגון Google Drive יכול להוות סיכון. אם למסד הנתונים בתיקייה הזאת ניגשים או עורכים ממגוון מקומות בו־זמנית, יכולות לקרות סתירות סנכרון והנתונים יכולים להינזק\",\n      \"importAppFlowyData\": \"ייבוא נתונים מתיקיית @:appName חיצונית\",\n      \"importingAppFlowyDataTip\": \"מתבצע ייבוא נתונים. נא לא לסגור את היישום\",\n      \"importAppFlowyDataDescription\": \"העתקת נתונים מתיקיית נתונים חיצונית של @:appName ולייבא אותה לתיקיית הנתונים הנוכחית של AppFlowy\",\n      \"importSuccess\": \"תיקיית הנתונים של @:appName יובאה בהצלחה\",\n      \"importFailed\": \"ייבוא תיקיית הנתונים של @:appName נכשל\",\n      \"importGuide\": \"לפרטים נוספים, נא לעיין במסמך המוזכר\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"הפעלת התראות\",\n        \"hint\": \"יש לכבות כדי לעצור הצגת התראות מקומיות.\"\n      },\n      \"showNotificationsIcon\": {\n        \"label\": \"הצגת סמל התראות\",\n        \"hint\": \"יש לכבות כדי להסתיר את סמל ההתראות בסרגל הצד.\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"איפוס\",\n      \"fontFamily\": {\n        \"label\": \"משפחת גופנים\",\n        \"search\": \"חיפוש\",\n        \"defaultFont\": \"מערכת\"\n      },\n      \"themeMode\": {\n        \"label\": \"מצב ערכת עיצוב\",\n        \"light\": \"מצב בהיר\",\n        \"dark\": \"מצב כהה\",\n        \"system\": \"התאמה למערכת\"\n      },\n      \"fontScaleFactor\": \"מקדם קנה מידה לגופנים\",\n      \"documentSettings\": {\n        \"cursorColor\": \"צבע סמן מסמך\",\n        \"selectionColor\": \"צבע בחירת מסמך\",\n        \"pickColor\": \"נא לבחור צבע\",\n        \"colorShade\": \"צללית צבע\",\n        \"opacity\": \"שקיפות\",\n        \"hexEmptyError\": \"צבע הקס׳ לא יכול להיות ריק\",\n        \"hexLengthError\": \"ערך הקס׳ חייב להיות באורך 6 תווים לפחות\",\n        \"hexInvalidError\": \"ערך הקס׳ שגוי\",\n        \"opacityEmptyError\": \"האטימות לא יכולה להיות ריקה\",\n        \"opacityRangeError\": \"האטימות חייבת להיות בין 1 ל־100\",\n        \"app\": \"יישום\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"החלה\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"כיוון פריסה\",\n        \"hint\": \"שליטה בזרימת התוכן על המסך שלך, משמאל לימין או מימין לשמאל.\",\n        \"ltr\": \"משמאל לימין\",\n        \"rtl\": \"מימין לשמאל\"\n      },\n      \"textDirection\": {\n        \"label\": \"כיוון ברירת המחדל של טקסט\",\n        \"hint\": \"נא לציין האם טקסט אמור להתחיל משמאל או מימין כברירת מחדל.\",\n        \"ltr\": \"משמאל לימין\",\n        \"rtl\": \"מימין לשמאל\",\n        \"auto\": \"אוטו׳\",\n        \"fallback\": \"כמו כיוון הפריסה\"\n      },\n      \"themeUpload\": {\n        \"button\": \"העלאה\",\n        \"uploadTheme\": \"העלאת ערכת עיצוב\",\n        \"description\": \"אפשר להעלות ערכת עיצוב משלך ל־@:appName בעזרת הכפתור שלהלן.\",\n        \"loading\": \"נא להמתין בזמן תיקוף והעלאת ערכת העיצוב שלך…\",\n        \"uploadSuccess\": \"ערכת העיצוב שלך הועלתה בהצלחה\",\n        \"deletionFailure\": \"מחיקת ערכת העיצוב נכשלה. נא לנסות למחוק אותה ידנית.\",\n        \"filePickerDialogTitle\": \"נא לבחור קובץ ‎.flowy_plugin\",\n        \"urlUploadFailure\": \"פתיחת הכתובת נכשלה: {}\"\n      },\n      \"theme\": \"ערכת עיצוב\",\n      \"builtInsLabel\": \"ערכות עיצוב מובנות\",\n      \"pluginsLabel\": \"תוספים\",\n      \"dateFormat\": {\n        \"label\": \"תבנית תאריך\",\n        \"local\": \"מקומית\",\n        \"us\": \"אמריקאית\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"ידידותית\",\n        \"dmy\": \"D/M/Y\"\n      },\n      \"timeFormat\": {\n        \"label\": \"תבנית שעה\",\n        \"twelveHour\": \"12 שעות\",\n        \"twentyFourHour\": \"24 שעות\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"הצגת חלונית מתן שם בעת יצירת עמוד\",\n      \"enableRTLToolbarItems\": \"הפעלת פריטי ימין לשמאל בסרגל הכלים\",\n      \"members\": {\n        \"title\": \"הגדרות חברים\",\n        \"inviteMembers\": \"הזמנת חברים\",\n        \"inviteHint\": \"הזמנה בדוא״ל\",\n        \"sendInvite\": \"שליחת הזמנה\",\n        \"copyInviteLink\": \"העתקת קישור הזמנה\",\n        \"label\": \"חברים\",\n        \"user\": \"משתמש\",\n        \"role\": \"תפקיד\",\n        \"removeFromWorkspace\": \"הסרה ממרחב העבודה\",\n        \"owner\": \"בעלים\",\n        \"guest\": \"אורח\",\n        \"member\": \"חבר\",\n        \"memberHintText\": \"חברים יכולים לקרוא ולערוך עמודים\",\n        \"guestHintText\": \"אורחים יכולים לקרוא, להגיב עם סמל, לכתוב הערות ולערוך עמודים מסוימים לפי ההרשאה.\",\n        \"emailInvalidError\": \"כתובת דוא״ל שגויה, נא לבדוק ולנסות שוב\",\n        \"emailSent\": \"ההודעה נשלחה בדוא״ל, נא לבדוק את תיבת הדוא״ל הנכנס\",\n        \"members\": \"חברים\",\n        \"membersCount\": {\n          \"zero\": \"{} חברים\",\n          \"one\": \"חבר\",\n          \"other\": \"{} חברים\"\n        },\n        \"memberLimitExceeded\": \"הגעת למגבלת החברים המרבית לחשבון שלך. כדי להוסיף חברים נוספים ולהמשיך בעבודתך, נא להגיש בקשה ב־GitHub\",\n        \"failedToAddMember\": \"הוספת החבר נכשלה\",\n        \"addMemberSuccess\": \"החבר נוסף בהצלחה\",\n        \"removeMember\": \"הסרת חבר\",\n        \"areYouSureToRemoveMember\": \"להסיר את החבר הזה?\",\n        \"inviteMemberSuccess\": \"ההזמנה נשלחה בהצלחה\",\n        \"failedToInviteMember\": \"הזמנת החבר נכשלה\"\n      }\n    },\n    \"files\": {\n      \"copy\": \"העתקה\",\n      \"defaultLocation\": \"קריאת הקבצים ומקום אחסון הנתונים\",\n      \"exportData\": \"ייצוא הנתונים שלך\",\n      \"doubleTapToCopy\": \"הנתיב יועתק בלחיצה כפולה\",\n      \"restoreLocation\": \"שחזור לנתיב ברירת המחדל של @:appName\",\n      \"customizeLocation\": \"פתיחת תיקייה אחרת\",\n      \"restartApp\": \"נא להפעיל את היישום מחדש כדי שהשינויים ייכנסו לתוקף.\",\n      \"exportDatabase\": \"ייצוא מסד נתונים\",\n      \"selectFiles\": \"נא לבחור את הקבצים לייצור\",\n      \"selectAll\": \"בחירה בהכול\",\n      \"deselectAll\": \"ביטול בחירה\",\n      \"createNewFolder\": \"יצירת תיקייה חדשה\",\n      \"createNewFolderDesc\": \"נא הגדיר לנו איפה לאחסן את הנתונים שלך\",\n      \"defineWhereYourDataIsStored\": \"הגדרת מקום אחסון הנתונים שלך\",\n      \"open\": \"פתיחה\",\n      \"openFolder\": \"פתיחת תיקייה קיימת\",\n      \"openFolderDesc\": \"קריאה וכתיבה אל תיקיית ה־@:appName הקיימת שלך\",\n      \"folderHintText\": \"שם התיקייה\",\n      \"location\": \"יצירת תיקייה חדשה\",\n      \"locationDesc\": \"נא לבחור שם לתיקיית הנתונים של @:appName\",\n      \"browser\": \"עיון\",\n      \"create\": \"יצירה\",\n      \"set\": \"הגדרה\",\n      \"folderPath\": \"נתיב לאחסון התיקייה שלך\",\n      \"locationCannotBeEmpty\": \"הנתיב לא יכול להיות ריק\",\n      \"pathCopiedSnackbar\": \"נתיב אחסון הקבצים הועתק ללוח הגזירים!\",\n      \"changeLocationTooltips\": \"החלפת תיקיית הנתונים\",\n      \"change\": \"שינוי\",\n      \"openLocationTooltips\": \"פתיחת תיקיית נתונים אחרת\",\n      \"openCurrentDataFolder\": \"פתיחת תיקיית הנתונים הנוכחית\",\n      \"recoverLocationTooltips\": \"איפוס לתיקיית הנתונים כברירת המחדל של @:appName\",\n      \"exportFileSuccess\": \"ייצוא הקובץ הצליח!\",\n      \"exportFileFail\": \"ייצוא הקובץ נכשל!\",\n      \"export\": \"ייצוא\",\n      \"clearCache\": \"פינוי זיכרון המטמון\",\n      \"clearCacheDesc\": \"אם נתקלת בבעיות עם תמונות שלא נטענות או גופנים שלא מופיעים כראוי, כדאי לנסות לפנות את זיכרון המטמון. הפעולה הזאת לא תסיר את נתוני המשתמש שלך.\",\n      \"areYouSureToClearCache\": \"לפנות את זיכרון המטמון?\",\n      \"clearCacheSuccess\": \"זיכרון המטמון התפנה בהצלחה!\"\n    },\n    \"user\": {\n      \"name\": \"שם\",\n      \"email\": \"דוא״ל\",\n      \"tooltipSelectIcon\": \"בחירת סמל\",\n      \"selectAnIcon\": \"נא לבחור סמל\",\n      \"pleaseInputYourOpenAIKey\": \"נא למלא את המפתח שלך ב־OpenAI\",\n      \"clickToLogout\": \"לחיצה תוציא את המשתמש הנוכחי\",\n      \"pleaseInputYourStabilityAIKey\": \"נא למלא את המפתח שלך ב־Stability AI\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"פרטים אישיים\",\n      \"username\": \"שם משתמש\",\n      \"usernameEmptyError\": \"שם שמשתמש לא יכול להישאר ריק\",\n      \"about\": \"על אודות\",\n      \"pushNotifications\": \"התראות בדחיפה\",\n      \"support\": \"תמיכה\",\n      \"joinDiscord\": \"להצטרף אלינו ב־Discord\",\n      \"privacyPolicy\": \"מדיניות פרטיות\",\n      \"userAgreement\": \"הסכם שימוש\",\n      \"termsAndConditions\": \"תנאים והתניות\",\n      \"userprofileError\": \"טעינת פרופיל המשתמש נכשלה\",\n      \"userprofileErrorDescription\": \"נא לנסות לצאת ולהיכנס בחזרה אם הבעיה נמשכת.\",\n      \"selectLayout\": \"בחירת פריסה\",\n      \"selectStartingDay\": \"בחירת יום ההתחלה\",\n      \"version\": \"גרסה\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"למחוק את התצוגה הזאת?\",\n    \"createView\": \"חדש\",\n    \"title\": {\n      \"placeholder\": \"ללא שם\"\n    },\n    \"settings\": {\n      \"filter\": \"מסנן\",\n      \"sort\": \"מיון\",\n      \"sortBy\": \"מיון לפי\",\n      \"properties\": \"מאפיינים\",\n      \"reorderPropertiesTooltip\": \"אפשר לשנות את סדר המאפיינים בגרירה\",\n      \"group\": \"קבוצה\",\n      \"addFilter\": \"הוספת מסנן\",\n      \"deleteFilter\": \"מחיקת מסנן\",\n      \"filterBy\": \"סינון לפי…\",\n      \"typeAValue\": \"נא להקליד ערך…\",\n      \"layout\": \"פריסה\",\n      \"databaseLayout\": \"פריסה\",\n      \"viewList\": {\n        \"zero\": \"0 צפיות\",\n        \"one\": \"צפייה\",\n        \"other\": \"{count} צפיות\"\n      },\n      \"editView\": \"עריכת תצוגה\",\n      \"boardSettings\": \"הגדרות לוח\",\n      \"calendarSettings\": \"הגדרות לוח שנה\",\n      \"createView\": \"תצוגה חדשה\",\n      \"duplicateView\": \"שכפול תצוגה\",\n      \"deleteView\": \"מחיקת תצוגה\",\n      \"numberOfVisibleFields\": \"{} מופיע\"\n    },\n    \"textFilter\": {\n      \"contains\": \"מכיל\",\n      \"doesNotContain\": \"לא מכיל\",\n      \"endsWith\": \"מסתיים ב־\",\n      \"startWith\": \"נפתח ב־\",\n      \"is\": \"הוא\",\n      \"isNot\": \"אינו\",\n      \"isEmpty\": \"ריק\",\n      \"isNotEmpty\": \"לא ריק\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"לא\",\n        \"startWith\": \"נפתח ב־\",\n        \"endWith\": \"מסתיים ב־\",\n        \"isEmpty\": \"ריק\",\n        \"isNotEmpty\": \"לא ריק\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"מסומן\",\n      \"isUnchecked\": \"לא מסומן\",\n      \"choicechipPrefix\": {\n        \"is\": \"הוא\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"הושלם\",\n      \"isIncomplted\": \"טרם הושלם\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"הוא\",\n      \"isNot\": \"אינו\",\n      \"contains\": \"מכיל\",\n      \"doesNotContain\": \"לא מכיל\",\n      \"isEmpty\": \"ריק\",\n      \"isNotEmpty\": \"לא ריק\"\n    },\n    \"dateFilter\": {\n      \"is\": \"הוא\",\n      \"before\": \"הוא לפני\",\n      \"after\": \"הוא אחרי\",\n      \"onOrBefore\": \"בתאריך או לפני\",\n      \"onOrAfter\": \"בתאריך או אחרי\",\n      \"between\": \"בין\",\n      \"empty\": \"ריק\",\n      \"notEmpty\": \"לא ריק\",\n      \"choicechipPrefix\": {\n        \"before\": \"לפני\",\n        \"after\": \"אחרי\",\n        \"onOrBefore\": \"בתאריך או לפני\",\n        \"onOrAfter\": \"בתאריך או אחרי\",\n        \"isEmpty\": \"ריק\",\n        \"isNotEmpty\": \"לא ריק\"\n      }\n    },\n    \"numberFilter\": {\n      \"equal\": \"שווה ל־\",\n      \"notEqual\": \"לא שווה ל־\",\n      \"lessThan\": \"קטן מ־\",\n      \"greaterThan\": \"גדול מ־\",\n      \"lessThanOrEqualTo\": \"קטן או שווה ל־\",\n      \"greaterThanOrEqualTo\": \"גדול או שווה ל־\",\n      \"isEmpty\": \"ריק\",\n      \"isNotEmpty\": \"לא ריק\"\n    },\n    \"field\": {\n      \"hide\": \"הסתרה\",\n      \"show\": \"הצגה\",\n      \"insertLeft\": \"הוספה משמאל\",\n      \"insertRight\": \"הוספה מימין\",\n      \"duplicate\": \"שכפול\",\n      \"delete\": \"מחיקה\",\n      \"wrapCellContent\": \"גלישת טקסט\",\n      \"clear\": \"פינוי תאים\",\n      \"textFieldName\": \"טקסט\",\n      \"checkboxFieldName\": \"תיבת סימון\",\n      \"dateFieldName\": \"תאריך\",\n      \"updatedAtFieldName\": \"שינוי אחרון\",\n      \"createdAtFieldName\": \"יצירה\",\n      \"numberFieldName\": \"מספרים\",\n      \"singleSelectFieldName\": \"בחירה\",\n      \"multiSelectFieldName\": \"ריבוי בחירות\",\n      \"urlFieldName\": \"כתובת\",\n      \"checklistFieldName\": \"רשימת סימונים\",\n      \"relationFieldName\": \"יחס\",\n      \"summaryFieldName\": \"תקציר בינה מלאכותית\",\n      \"timeFieldName\": \"שעה\",\n      \"translateFieldName\": \"תרגום בינה מלאכותית\",\n      \"translateTo\": \"תרגום ל־\",\n      \"numberFormat\": \"תבנית מספר\",\n      \"dateFormat\": \"תבנית תאריך\",\n      \"includeTime\": \"כולל השעה\",\n      \"isRange\": \"תאריך סיום\",\n      \"dateFormatFriendly\": \"חודש יום, שנה\",\n      \"dateFormatISO\": \"שנה-חודש-יום\",\n      \"dateFormatLocal\": \"חודש/יום/שנה\",\n      \"dateFormatUS\": \"שנה/חודש/יום\",\n      \"dateFormatDayMonthYear\": \"יום/חודש/שנה\",\n      \"timeFormat\": \"תבנית שעה\",\n      \"invalidTimeFormat\": \"תבנית שגויה\",\n      \"timeFormatTwelveHour\": \"12 שעות\",\n      \"timeFormatTwentyFourHour\": \"24 שעות\",\n      \"clearDate\": \"פינוי התאריך\",\n      \"dateTime\": \"תאריך שעה\",\n      \"startDateTime\": \"תאריך ושעת התחלה\",\n      \"endDateTime\": \"תאריך ושעת סיום\",\n      \"failedToLoadDate\": \"טעינת ערך התאריך נכשלה\",\n      \"selectTime\": \"נא לבחור שעה\",\n      \"selectDate\": \"נא לבחור תאריך\",\n      \"visibility\": \"חשיפה\",\n      \"propertyType\": \"סוג המאפיין\",\n      \"addSelectOption\": \"הוספת אפשרות\",\n      \"typeANewOption\": \"נא להקליד אפשרות חדשה\",\n      \"optionTitle\": \"אפשרויות\",\n      \"addOption\": \"הוספת אפשרות\",\n      \"editProperty\": \"עריכת מאפיין\",\n      \"newProperty\": \"מאפיין חדש\",\n      \"deleteFieldPromptMessage\": \"להמשיך? המאפיין יימחק\",\n      \"clearFieldPromptMessage\": \"להמשיך? כל התאים בעמודה הזאת יתרוקנו\",\n      \"newColumn\": \"עמודה חדשה\",\n      \"format\": \"תבנית\",\n      \"reminderOnDateTooltip\": \"בתא הזה יש תזכורת מתוזמנת\",\n      \"optionAlreadyExist\": \"האפשרות כבר קיימת\"\n    },\n    \"rowPage\": {\n      \"newField\": \"הוספת שדה חדש\",\n      \"fieldDragElementTooltip\": \"נא ללחוץ לפתיחת התפריט\",\n      \"showHiddenFields\": {\n        \"one\": \"הצגת שדה מוסתר\",\n        \"many\": \"הצגת {count} שדות מוסתרים\",\n        \"other\": \"הצגת {count} שדות מוסתרים\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"הסתרת שדה מוסתר\",\n        \"many\": \"הסתרת {count} שדות מוסתרים\",\n        \"other\": \"הסתרת {count} שדות מוסתרים\"\n      },\n      \"openAsFullPage\": \"Open as full page\",\n      \"moreRowActions\": \"פעולות שורה נוספות\"\n    },\n    \"sort\": {\n      \"ascending\": \"עולה\",\n      \"descending\": \"יורד\",\n      \"by\": \"לפי\",\n      \"empty\": \"אין מיונים פעילים\",\n      \"cannotFindCreatableField\": \"לא ניתן למצוא שדה מתאים למיין לפיו\",\n      \"deleteAllSorts\": \"מחיקת כל המיונים\",\n      \"addSort\": \"הוספת מיון חדש\",\n      \"removeSorting\": \"להסיר מיון?\",\n      \"fieldInUse\": \"כבר בחרת למיין לפי השדה הזה\"\n    },\n    \"row\": {\n      \"duplicate\": \"שכפול\",\n      \"delete\": \"מחיקה\",\n      \"titlePlaceholder\": \"ללא שם\",\n      \"textPlaceholder\": \"ריק\",\n      \"copyProperty\": \"המאפיין הועתק ללוח הגזירים\",\n      \"count\": \"ספירה\",\n      \"newRow\": \"שורה חדשה\",\n      \"action\": \"פעולה\",\n      \"add\": \"לחיצה תוסיף להלן\",\n      \"drag\": \"גרירה להזזה\",\n      \"deleteRowPrompt\": \"למחוק את השורה הזאת? זאת פעולה בלתי הפיכה\",\n      \"deleteCardPrompt\": \"למחוק את הכרטיס הזה? זאת פעולה בלתי הפיכה\",\n      \"dragAndClick\": \"גרירה להזזה, לחיצה לפתיחת התפריט\",\n      \"insertRecordAbove\": \"הוספת רשומה למעלה\",\n      \"insertRecordBelow\": \"הוספת רשומה מתחת\",\n      \"noContent\": \"אין תוכן\"\n    },\n    \"selectOption\": {\n      \"create\": \"יצירה\",\n      \"purpleColor\": \"סגול\",\n      \"pinkColor\": \"ורוד\",\n      \"lightPinkColor\": \"ורוד בהיר\",\n      \"orangeColor\": \"כתום\",\n      \"yellowColor\": \"צהוב\",\n      \"limeColor\": \"ליים\",\n      \"greenColor\": \"ירוק\",\n      \"aquaColor\": \"אוקיינוס\",\n      \"blueColor\": \"כחול\",\n      \"deleteTag\": \"מחיקת תגית\",\n      \"colorPanelTitle\": \"צבע\",\n      \"panelTitle\": \"נא לבחור אפשרות או ליצור אחת\",\n      \"searchOption\": \"חיפוש אפשרות\",\n      \"searchOrCreateOption\": \"חיפוש אפשרות או ליצור אחת\",\n      \"createNew\": \"ליצור חדשה\",\n      \"orSelectOne\": \"או לבחור אפשרות\",\n      \"typeANewOption\": \"נא למלא אפשרות חדשה\",\n      \"tagName\": \"שם תגית\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"תיאור משימה\",\n      \"addNew\": \"הוספת משימה חדשה\",\n      \"submitNewTask\": \"יצירה\",\n      \"hideComplete\": \"הסתרת משימות שהושלמו\",\n      \"showComplete\": \"הצגת כל המשימות\"\n    },\n    \"url\": {\n      \"launch\": \"פתיחת קישור בדפדפן\",\n      \"copy\": \"העתקת קישור ללוח הגזירים\",\n      \"textFieldHint\": \"נא למלא כתובת\"\n    },\n    \"relation\": {\n      \"relatedDatabasePlaceLabel\": \"מסד נתונים קשור\",\n      \"relatedDatabasePlaceholder\": \"אין\",\n      \"inRelatedDatabase\": \"בתוך\",\n      \"rowSearchTextFieldPlaceholder\": \"חיפוש\",\n      \"noDatabaseSelected\": \"לא נבחר מסד נתונים, נא לבחור אחד מהרשימה להלן תחילה:\",\n      \"emptySearchResult\": \"לא נמצאו רשומות\",\n      \"linkedRowListLabel\": \"{count} שורות מקושרות\",\n      \"unlinkedRowListLabel\": \"קישור שורה נוספת\"\n    },\n    \"menuName\": \"רשת\",\n    \"referencedGridPrefix\": \"View of\",\n    \"calculate\": \"חישוב\",\n    \"calculationTypeLabel\": {\n      \"none\": \"אין\",\n      \"average\": \"ממוצע\",\n      \"max\": \"מרבי\",\n      \"median\": \"חציוני\",\n      \"min\": \"מזערי\",\n      \"sum\": \"סכום\",\n      \"count\": \"ספירה\",\n      \"countEmpty\": \"ספירת הריקים\",\n      \"countEmptyShort\": \"ריק\",\n      \"countNonEmpty\": \"ספירת הלא ריקים\",\n      \"countNonEmptyShort\": \"מלאים\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"מסמך\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"‎01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"נא לבחור לוח לקשר אליו\",\n        \"createANewBoard\": \"יצירת לוח חדש\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"נא לבחור רשת לקשר אליה\",\n        \"createANewGrid\": \"יצירת רשת חדשה\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"נא לבחור לוח שנה לקשר אליו\",\n        \"createANewCalendar\": \"יצירת לוח שנה חדש\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"נא לבחור מסמך לקשר אליו\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"קו מתאר\",\n      \"codeBlock\": \"מקטע קוד\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"לוח מופנה\",\n      \"referencedGrid\": \"טבלה מופנית\",\n      \"referencedCalendar\": \"לוח שנה מופנה\",\n      \"referencedDocument\": \"מסמך מופנה\",\n      \"autoGeneratorMenuItemName\": \"כותב OpenAI\",\n      \"autoGeneratorTitleName\": \"OpenAI: לבקש מהבינה המלאכותית לכתוב כל דבר שהוא…\",\n      \"autoGeneratorLearnMore\": \"מידע נוסף\",\n      \"autoGeneratorGenerate\": \"יצירה\",\n      \"autoGeneratorHintText\": \"לבקש מ־OpenAI…\",\n      \"autoGeneratorCantGetOpenAIKey\": \"לא ניתן למשוך את המפתח של OpenAI\",\n      \"autoGeneratorRewrite\": \"שכתוב\",\n      \"smartEdit\": \"סייעני בינה מלאכותית\",\n      \"smartEditFixSpelling\": \"תיקון איות\",\n      \"warning\": \"⚠️ תגובות הבינה המלאכותית יכולות להיות מסולפות או מטעות.\",\n      \"smartEditSummarize\": \"סיכום\",\n      \"smartEditImproveWriting\": \"שיפור הכתיבה\",\n      \"smartEditMakeLonger\": \"הארכה\",\n      \"smartEditCouldNotFetchResult\": \"לא ניתן למשוך את התוצאה מ־OpenAI\",\n      \"smartEditCouldNotFetchKey\": \"לא ניתן למשוך מפתח OpenAI\",\n      \"smartEditDisabled\": \"חיבור OpenAI בהגדרות\",\n      \"appflowyAIEditDisabled\": \"יש להיכנס כדי להפעיל יכולות בינה מלאכותית\",\n      \"discardResponse\": \"להתעלם מתגובות הבינה המלאכותית?\",\n      \"createInlineMathEquation\": \"יצירת משוואה\",\n      \"fonts\": \"גופנים\",\n      \"insertDate\": \"הוספת תאריך\",\n      \"emoji\": \"אמוג׳י\",\n      \"toggleList\": \"רשימת מתגים\",\n      \"quoteList\": \"רשימת ציטוטים\",\n      \"numberedList\": \"רשימה ממוספרת\",\n      \"bulletedList\": \"רשימת תבליטים\",\n      \"todoList\": \"רשימת מטלות\",\n      \"callout\": \"מסר\",\n      \"cover\": {\n        \"changeCover\": \"החלפת עטיפה\",\n        \"colors\": \"צבעים\",\n        \"images\": \"תמונות\",\n        \"clearAll\": \"פינוי של הכול\",\n        \"abstract\": \"מופשט\",\n        \"addCover\": \"הוספת עטיפה\",\n        \"addLocalImage\": \"הוספת תמונה מקומית\",\n        \"invalidImageUrl\": \"כתובת תמונה שגויה\",\n        \"failedToAddImageToGallery\": \"הוספת תמונה לגלריה נכשלה\",\n        \"enterImageUrl\": \"נא למלא כתובת תמונה\",\n        \"add\": \"הוספה\",\n        \"back\": \"חזרה\",\n        \"saveToGallery\": \"שמירה לגלריה\",\n        \"removeIcon\": \"הסרת סמל\",\n        \"pasteImageUrl\": \"הדבקת כתובת תמונה\",\n        \"or\": \"או\",\n        \"pickFromFiles\": \"בחירה מהקבצים\",\n        \"couldNotFetchImage\": \"לא ניתן למשוך תמונה\",\n        \"imageSavingFailed\": \"שמירת התמונה נכשלה\",\n        \"addIcon\": \"הוספת סמל\",\n        \"changeIcon\": \"החלפת סמל\",\n        \"coverRemoveAlert\": \"הוא יוסר מהעטיפה לאחר מחיקתו.\",\n        \"alertDialogConfirmation\": \"להמשיך?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"משוואה מתמטית\",\n        \"addMathEquation\": \"הוספת משוואת TeX\",\n        \"editMathEquation\": \"עריכת משוואה מתמטית\"\n      },\n      \"optionAction\": {\n        \"click\": \"יש ללחוץ על\",\n        \"toOpenMenu\": \" כדי לפתוח תפריט\",\n        \"delete\": \"מחיקה\",\n        \"duplicate\": \"שכפול\",\n        \"turnInto\": \"הפיכה ל־\",\n        \"moveUp\": \"העלאה למעלה\",\n        \"moveDown\": \"הורדה למטה\",\n        \"color\": \"צבע\",\n        \"align\": \"יישור\",\n        \"left\": \"שמאל\",\n        \"center\": \"מרכז\",\n        \"right\": \"ימין\",\n        \"defaultColor\": \"ברירת מחדל\",\n        \"depth\": \"עומק\"\n      },\n      \"image\": {\n        \"addAnImage\": \"הוספת תמונה\",\n        \"copiedToPasteBoard\": \"קישור התמונה הועתק ללוח הגזירים\",\n        \"imageUploadFailed\": \"העלאת התמונה נכשלה\",\n        \"errorCode\": \"קוד שגיאה\"\n      },\n      \"math\": {\n        \"copiedToPasteBoard\": \"המשוואה המתמטית הועתקה ללוח הגזירים\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"הקישור הועתק ללוח הגזירים\",\n        \"convertToLink\": \"המרה לקישור להטמעה\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"יש להוסיף כותרות ראשיות כדי ליצור תוכן עניינים.\",\n        \"noMatchHeadings\": \"לא נמצאו כותרות ראשויות תואמות.\"\n      },\n      \"table\": {\n        \"addAfter\": \"הוספה לאחר\",\n        \"addBefore\": \"הוספה לפני\",\n        \"delete\": \"מחיקה\",\n        \"clear\": \"פינוי התוכן\",\n        \"duplicate\": \"שכפול\",\n        \"bgColor\": \"צבע רקע\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"העתקה\",\n        \"cut\": \"גזירה\",\n        \"paste\": \"הדבקה\"\n      },\n      \"action\": \"פעולות\",\n      \"database\": {\n        \"selectDataSource\": \"בחירת מקור נתונים\",\n        \"noDataSource\": \"אין מקור נתונים\",\n        \"selectADataSource\": \"נא לבחור מקור נתונים\",\n        \"toContinue\": \"כדי להמשיך\",\n        \"newDatabase\": \"מסד נתונים חדש\",\n        \"linkToDatabase\": \"קישור למסד נתונים\"\n      },\n      \"date\": \"תאריך\",\n      \"video\": {\n        \"label\": \"סרטון\",\n        \"emptyLabel\": \"הוספת סרטון\",\n        \"placeholder\": \"הדבקת הקישור לסרטון\",\n        \"copiedToPasteBoard\": \"הקישור לסרטון הועתק ללוח הגזירים\",\n        \"insertVideo\": \"הוספת סרטון\",\n        \"invalidVideoUrl\": \"כתובת המקור לא נתמכת עדיין.\",\n        \"invalidVideoUrlYouTube\": \"עדיין אין תמיכה ב־YouTube.\",\n        \"supportedFormats\": \"סוגים נתמכים: MP4,‏ WebM,‏ MOV,‏ AVI,‏ FLV,‏ MPEG/M4V,‏ H.264\"\n      },\n      \"openAI\": \"OpenAI\"\n    },\n    \"outlineBlock\": {\n      \"placeholder\": \"תוכן עניינים\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"נא להקליד ‚/’ לקבלת פקודות\"\n    },\n    \"title\": {\n      \"placeholder\": \"ללא שם\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"נא ללחוץ כדי להוסיף תמונה\",\n      \"upload\": {\n        \"label\": \"העלאה\",\n        \"placeholder\": \"נא ללחוץ כדי להעלות תמונה\"\n      },\n      \"url\": {\n        \"label\": \"כתובת תמונה\",\n        \"placeholder\": \"נא למלא כתובת תמונה\"\n      },\n      \"ai\": {\n        \"label\": \"יצירת תמונה מ־OpenAI\",\n        \"placeholder\": \"נא למלא את הקלט ל־OpenAI כדי לייצר תמונה\"\n      },\n      \"stability_ai\": {\n        \"label\": \"יצירת תמונה מ־Stability AI\",\n        \"placeholder\": \"נא למלא את הבקשה ל־Stability AI כדי לייצר תמונה\"\n      },\n      \"support\": \"מגבלת גודל התמונה היא 5 מ״ב. הסוגים הנתמכים הם: JPEG,‏ PNG,‏ GIF,‏ SVG\",\n      \"error\": {\n        \"invalidImage\": \"תמונה שגויה\",\n        \"invalidImageSize\": \"גודל התמונה חייב להיות קטן מ־5 מ״ב\",\n        \"invalidImageFormat\": \"סוג התמונה לא נתמך. הסוגים הנתמכים: JPEG,‏ PNG,‏ JPG,‏ GIF,‏ SVG,‏ WEBP\",\n        \"invalidImageUrl\": \"כתובת תמונה שגויה\",\n        \"noImage\": \"אין קובץ או תיקייה כאלה\"\n      },\n      \"embedLink\": {\n        \"label\": \"קישור להטמעה\",\n        \"placeholder\": \"נא להדביק או להקליד קישור לתמונה\"\n      },\n      \"unsplash\": {\n        \"label\": \"Unsplash\"\n      },\n      \"searchForAnImage\": \"חיפוש אחר תמונה\",\n      \"pleaseInputYourOpenAIKey\": \"נא למלא את מפתח ה־OpenAI שלך בעמוד ההגדרות\",\n      \"saveImageToGallery\": \"שמירת תמונה\",\n      \"failedToAddImageToGallery\": \"הוספת התמונה לגלריה נכשלה\",\n      \"successToAddImageToGallery\": \"התמונה נוספה לגלריה בהצלחה\",\n      \"unableToLoadImage\": \"לא ניתן לטעון את התמונה\",\n      \"maximumImageSize\": \"גודל ההעלאה המרבי הנתמך הוא 10 מ״ב\",\n      \"uploadImageErrorImageSizeTooBig\": \"גודל התמונה חייב להיות גדול מ־10 מ״ב\",\n      \"imageIsUploading\": \"התמונה נשלחת\",\n      \"pleaseInputYourStabilityAIKey\": \"נא למלא את המפתח ל־Stability AI בעמוד ההגדרות\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"שפה\",\n        \"placeholder\": \"בחירת שפה\",\n        \"auto\": \"אוטו׳\"\n      },\n      \"copyTooltip\": \"העתקת תוכן למקטע קוד\",\n      \"searchLanguageHint\": \"חיפוש אחר שפה\",\n      \"codeCopiedSnackbar\": \"הקוד הועתק ללוח הגזירים!\"\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"נא להדביק או להקליד קישור\",\n      \"openInNewTab\": \"פתיחה בלשונית חדשה\",\n      \"copyLink\": \"העתקת קישור\",\n      \"removeLink\": \"הסרת קישור\",\n      \"url\": {\n        \"label\": \"כתובת קישור\",\n        \"placeholder\": \"נא למלא כתובת לקישור\"\n      },\n      \"title\": {\n        \"label\": \"כותרת קישור\",\n        \"placeholder\": \"נא למלא כותרת לקישור\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"אזכור משתמשים או עמוד או תאריך…\",\n      \"page\": {\n        \"label\": \"קישור לעמוד\",\n        \"tooltip\": \"לחיצה תפתח את העמוד\"\n      },\n      \"deleted\": \"נמחק\",\n      \"deletedContent\": \"התוכן הזה לא קיים או שנמחק\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"איפוס לברירת מחדל\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"לא ניתן לפענח את תוכן המקטע\",\n      \"clickToCopyTheBlockContent\": \"לחיצה תעתיק את תוכן המקטע\",\n      \"blockContentHasBeenCopied\": \"תוכן המקטע הועתק.\"\n    },\n    \"mobilePageSelector\": {\n      \"title\": \"נא לבחור עמוד\",\n      \"failedToLoad\": \"טעינת רשימת העמודים נכשלה\",\n      \"noPagesFound\": \"לא נמצאו עמודים\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"חדש\",\n      \"renameGroupTooltip\": \"נא ללחוץ לשינוי שם הקבוצה\",\n      \"createNewColumn\": \"הוספת קבוצה חדשה\",\n      \"addToColumnTopTooltip\": \"הוספת כרטיס חדש בראש\",\n      \"addToColumnBottomTooltip\": \"הוספת כרטיס חדש בתחתית\",\n      \"renameColumn\": \"שינוי שם\",\n      \"hideColumn\": \"הסתרה\",\n      \"newGroup\": \"קבוצה חדשה\",\n      \"deleteColumn\": \"מחיקה\",\n      \"deleteColumnConfirmation\": \"הפעולה הזאת תמחק את הקבוצה הזאת ואת כל הכרטיסים שבה.\\nלהמשיך?\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"קבוצות מוסתרות\",\n      \"collapseTooltip\": \"הסתרת הקבוצות המוסתרות\",\n      \"expandTooltip\": \"הצגת הקבוצות המוסתרות\"\n    },\n    \"cardDetail\": \"פרטי הכרטיס\",\n    \"cardActions\": \"פעולות על הכרטיס\",\n    \"cardDuplicated\": \"הכרטיס שוכפל\",\n    \"cardDeleted\": \"הכרטיס נמחק\",\n    \"showOnCard\": \"הצגה על פרט הכרטיס\",\n    \"setting\": \"הגדרה\",\n    \"propertyName\": \"שם מאפיין\",\n    \"menuName\": \"לוח\",\n    \"showUngrouped\": \"הצגת פריטים מחוץ לקבוצות\",\n    \"ungroupedButtonText\": \"מחוץ לקבוצה\",\n    \"ungroupedButtonTooltip\": \"מכיל כרטיסים שלא שייכים לאף קבוצה\",\n    \"ungroupedItemsTitle\": \"נא ללחוץ כדי להוסיף ללוח\",\n    \"groupBy\": \"קיבוץ לפי\",\n    \"groupCondition\": \"תנאי קיבוץ\",\n    \"referencedBoardPrefix\": \"View of\",\n    \"notesTooltip\": \"הערות בפנים\",\n    \"mobile\": {\n      \"editURL\": \"עריכת כתובת\",\n      \"showGroup\": \"הצגת קבוצה\",\n      \"showGroupContent\": \"להציג את הקבוצה הזאת בלוח?\",\n      \"failedToLoad\": \"טעינת תצוגת הלוח נכשלה\"\n    },\n    \"dateCondition\": {\n      \"weekOf\": \"שבוע {} - {}\",\n      \"today\": \"היום\",\n      \"yesterday\": \"אתמול\",\n      \"tomorrow\": \"מחר\",\n      \"lastSevenDays\": \"7 הימים האחרונים\",\n      \"nextSevenDays\": \"7 הימים הבאים\",\n      \"lastThirtyDays\": \"30 הימים האחרונים\",\n      \"nextThirtyDays\": \"30 הימים הבאים\"\n    },\n    \"noGroup\": \"אין קבוצה לפי מאפיין\",\n    \"noGroupDesc\": \"תצוגות הלוח דורשות מאפיין לקיבוץ כדי שתוצגנה\"\n  },\n  \"calendar\": {\n    \"menuName\": \"לוח שנה\",\n    \"defaultNewCalendarTitle\": \"ללא שם\",\n    \"newEventButtonTooltip\": \"הוספת אירוע חדש\",\n    \"navigation\": {\n      \"today\": \"היום\",\n      \"jumpToday\": \"דילוג להיום\",\n      \"previousMonth\": \"החודש הקודם\",\n      \"nextMonth\": \"החודש הבא\",\n      \"views\": {\n        \"day\": \"יום\",\n        \"week\": \"שבוע\",\n        \"month\": \"חודש\",\n        \"year\": \"שנה\"\n      }\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"אין אירועים עדיין\",\n      \"emptyBody\": \"יש ללחוץ על כפתור הפלוס כדי להוסיף אירוע ביום הזה.\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"הצגת מספרי השבועות\",\n      \"showWeekends\": \"הצגת סופי שבוע\",\n      \"firstDayOfWeek\": \"השבוע מתחיל ב־\",\n      \"layoutDateField\": \"פריסת לוח השנה לפי\",\n      \"changeLayoutDateField\": \"החלפת שדה פריסה\",\n      \"noDateTitle\": \"אין תאריך\",\n      \"noDateHint\": {\n        \"zero\": \"אירועים לא מתוזמנים יופיעו כאן\",\n        \"one\": \"אירוע לא מתוזמן\",\n        \"other\": \"{count} אירועים לא מתוזמנים\"\n      },\n      \"unscheduledEventsTitle\": \"אירועים לא מתוזמנים\",\n      \"clickToAdd\": \"נא ללחוץ כדי להוסיף ללוח השנה\",\n      \"name\": \"הגדרות לוח שנה\",\n      \"clickToOpen\": \"לחיצה תפתח את הרשומה\"\n    },\n    \"referencedCalendarPrefix\": \"תצוגה של\",\n    \"quickJumpYear\": \"דילוג אל\",\n    \"duplicateEvent\": \"שכפול אירוע\"\n  },\n  \"errorDialog\": {\n    \"title\": \"שגיאה ב־@:appName\",\n    \"howToFixFallback\": \"סליחה על חוסר הנעימות! נא להגיש תיעוד תקלה בעמוד ה־GitHub שלנו שמתאר את השגיאה שלך.\",\n    \"github\": \"הצגה ב־GitHub\"\n  },\n  \"search\": {\n    \"label\": \"חיפוש\",\n    \"placeholder\": {\n      \"actions\": \"חיפוש פעולות…\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"הועתק!\",\n      \"fail\": \"לא ניתן להעתיק\"\n    }\n  },\n  \"unSupportBlock\": \"הגרסה הנוכחית לא תומכת במקטע הזה.\",\n  \"views\": {\n    \"deleteContentTitle\": \"למחוק את {pageType}?\",\n    \"deleteContentCaption\": \"ניתן יהיה לשחזר את ה{pageType} הזה מהאשפה אם בחרת למחוק.\"\n  },\n  \"colors\": {\n    \"custom\": \"משלך\",\n    \"default\": \"ברירת מחדל\",\n    \"red\": \"אדום\",\n    \"orange\": \"כתום\",\n    \"yellow\": \"צהוב\",\n    \"green\": \"ירוק\",\n    \"blue\": \"כחול\",\n    \"purple\": \"סגול\",\n    \"pink\": \"ורוד\",\n    \"brown\": \"חום\",\n    \"gray\": \"אפור\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"אמוג׳י\",\n    \"search\": \"חיפוש אמוג׳י\",\n    \"noRecent\": \"אין אמוג׳י אחרונים\",\n    \"noEmojiFound\": \"לא נמצא אמוג׳י\",\n    \"filter\": \"מסנן\",\n    \"random\": \"אקראי\",\n    \"selectSkinTone\": \"בחירת גוון עור\",\n    \"remove\": \"הסרת אמוג׳י\",\n    \"categories\": {\n      \"smileys\": \"חייכנים וסמלי רגש\",\n      \"people\": \"אנשים וגוף\",\n      \"animals\": \"חיות וטבע\",\n      \"food\": \"מזון ומשקאות\",\n      \"activities\": \"פעילויות\",\n      \"places\": \"טיול ומקומות\",\n      \"objects\": \"חפצים\",\n      \"symbols\": \"סמלים\",\n      \"flags\": \"דגלים\",\n      \"nature\": \"טבע\",\n      \"frequentlyUsed\": \"בשימוש תדיר\"\n    },\n    \"skinTone\": {\n      \"default\": \"ברירת מחדל\",\n      \"light\": \"בהיר\",\n      \"mediumLight\": \"בהיר ממוצע\",\n      \"medium\": \"ממוצע\",\n      \"mediumDark\": \"כהה ממוצע\",\n      \"dark\": \"כהה\"\n    }\n  },\n  \"inlineActions\": {\n    \"noResults\": \"אין תוצאות\",\n    \"recentPages\": \"עמודים אחרונים\",\n    \"pageReference\": \"הפנייה לעמוד\",\n    \"docReference\": \"הפנייה למסמך\",\n    \"boardReference\": \"הפנייה ללוח\",\n    \"calReference\": \"הפנייה ללוח שנה\",\n    \"gridReference\": \"הפנייה לטבלה\",\n    \"date\": \"תאריך\",\n    \"reminder\": {\n      \"groupTitle\": \"תזכורת\",\n      \"shortKeyword\": \"להזכיר\"\n    }\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"אפשר להחליף את התאריך והשעה בהגדרות\",\n    \"dateFormat\": \"תבנית תאריך\",\n    \"includeTime\": \"כולל השעה\",\n    \"isRange\": \"תאריך סיום\",\n    \"timeFormat\": \"תבנית שעה\",\n    \"clearDate\": \"פינוי התאריך\",\n    \"reminderLabel\": \"תזכורת\",\n    \"selectReminder\": \"בחירת תזכורת\",\n    \"reminderOptions\": {\n      \"none\": \"בלי\",\n      \"atTimeOfEvent\": \"בזמן האירוע\",\n      \"fiveMinsBefore\": \"5 דק׳ לפני\",\n      \"tenMinsBefore\": \"10 דק׳ לפני\",\n      \"fifteenMinsBefore\": \"15 דק׳ לפני\",\n      \"thirtyMinsBefore\": \"30 דק׳ לפני\",\n      \"oneHourBefore\": \"שעה לפני\",\n      \"twoHoursBefore\": \"שעתיים לפני\",\n      \"onDayOfEvent\": \"ביום האירוע\",\n      \"oneDayBefore\": \"יום לפני\",\n      \"twoDaysBefore\": \"יומיים לפני\",\n      \"oneWeekBefore\": \"שבוע לפני\",\n      \"custom\": \"אחר\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"אתמול\",\n    \"today\": \"היום\",\n    \"tomorrow\": \"מחר\",\n    \"oneWeek\": \"שבוע\"\n  },\n  \"notificationHub\": {\n    \"title\": \"התראות\",\n    \"mobile\": {\n      \"title\": \"עדכונים\"\n    },\n    \"emptyTitle\": \"התעדכנת בהכול!\",\n    \"emptyBody\": \"אין התראות ממתינות לפעולות. אפשר פשוט להירגע.\",\n    \"tabs\": {\n      \"inbox\": \"הודעות נכנסות\",\n      \"upcoming\": \"בקרוב\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"סימון של כולם כנקראו\",\n      \"showAll\": \"הכול\",\n      \"showUnreads\": \"לא נקראו\"\n    },\n    \"filters\": {\n      \"ascending\": \"עולה\",\n      \"descending\": \"יורד\",\n      \"groupByDate\": \"קיבוץ לפי תאריך\",\n      \"showUnreadsOnly\": \"להציג כאלו שלא נקראו בלבד\",\n      \"resetToDefault\": \"איפוס לברירת המחדל\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"תזכורת\",\n    \"message\": \"חשוב לסמן את זה לפני ששוכחים!\",\n    \"tooltipDelete\": \"מחיקה\",\n    \"tooltipMarkRead\": \"סימון כנקראה\",\n    \"tooltipMarkUnread\": \"סימון כלא נקראה\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"איתור\",\n    \"previousMatch\": \"התוצאה הקודמת\",\n    \"nextMatch\": \"התוצאה הבאה\",\n    \"close\": \"סגירה\",\n    \"replace\": \"החלפה\",\n    \"replaceAll\": \"החלפה של הכול\",\n    \"noResult\": \"אין תוצאות\",\n    \"caseSensitive\": \"תלוי רישיות\",\n    \"searchMore\": \"אפשר לחפש לאיתור תוצאות נוספות\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"אנו מתנצלים\",\n    \"loadingViewError\": \"טעינת התצוגה הזאת נתקלת בקשיים. נא לבדוק שהחיבור שלך לאינטרנט תקין, לאחר מכן לרענן את היישום ולא להסס לפנות לצוות אם המצב הזה נמשך.\"\n  },\n  \"editor\": {\n    \"bold\": \"מודגש\",\n    \"bulletedList\": \"רשימת תבליטים\",\n    \"bulletedListShortForm\": \"תבליטים\",\n    \"checkbox\": \"תיבת סימון\",\n    \"embedCode\": \"הטמעת קוד\",\n    \"heading1\": \"כ1\",\n    \"heading2\": \"כ2\",\n    \"heading3\": \"כ3\",\n    \"highlight\": \"הדגשה\",\n    \"color\": \"צבע\",\n    \"image\": \"תמונה\",\n    \"date\": \"תאריך\",\n    \"page\": \"עמוד\",\n    \"italic\": \"נטוי\",\n    \"link\": \"קישור\",\n    \"numberedList\": \"רשימה ממוספרת\",\n    \"numberedListShortForm\": \"ממוספר\",\n    \"quote\": \"ציטוט\",\n    \"strikethrough\": \"קו חוצה\",\n    \"text\": \"טקסט\",\n    \"underline\": \"קו תחתי\",\n    \"fontColorDefault\": \"ברירת מחדל\",\n    \"fontColorGray\": \"אפור\",\n    \"fontColorBrown\": \"חום\",\n    \"fontColorOrange\": \"כתום\",\n    \"fontColorYellow\": \"צהוב\",\n    \"fontColorGreen\": \"ירוק\",\n    \"fontColorBlue\": \"כחול\",\n    \"fontColorPurple\": \"סגול\",\n    \"fontColorPink\": \"ורוד\",\n    \"fontColorRed\": \"אדום\",\n    \"backgroundColorDefault\": \"רקע ברירת מחדל\",\n    \"backgroundColorGray\": \"רקע אפור\",\n    \"backgroundColorBrown\": \"רקע חום\",\n    \"backgroundColorOrange\": \"רקע כתום\",\n    \"backgroundColorYellow\": \"רקע צהוב\",\n    \"backgroundColorGreen\": \"רקע ירוק\",\n    \"backgroundColorBlue\": \"רקע כחול\",\n    \"backgroundColorPurple\": \"רקע סגול\",\n    \"backgroundColorPink\": \"רקע ורוד\",\n    \"backgroundColorRed\": \"רקע אדום\",\n    \"backgroundColorLime\": \"רקע ליים\",\n    \"backgroundColorAqua\": \"רקע אוקיינוס\",\n    \"done\": \"בוצע\",\n    \"cancel\": \"ביטול\",\n    \"tint1\": \"גוון 1\",\n    \"tint2\": \"גוון 2\",\n    \"tint3\": \"גוון 3\",\n    \"tint4\": \"גוון 4\",\n    \"tint5\": \"גוון 5\",\n    \"tint6\": \"גוון 6\",\n    \"tint7\": \"גוון 7\",\n    \"tint8\": \"גוון 8\",\n    \"tint9\": \"גוון 9\",\n    \"lightLightTint1\": \"סגול\",\n    \"lightLightTint2\": \"ורוד\",\n    \"lightLightTint3\": \"ורוד בהיר\",\n    \"lightLightTint4\": \"כתום\",\n    \"lightLightTint5\": \"צהוב\",\n    \"lightLightTint6\": \"ליים\",\n    \"lightLightTint7\": \"ירוק\",\n    \"lightLightTint8\": \"אוקיינוס\",\n    \"lightLightTint9\": \"כחול\",\n    \"urlHint\": \"כתובת\",\n    \"mobileHeading1\": \"כותרת 1\",\n    \"mobileHeading2\": \"כותרת 2\",\n    \"mobileHeading3\": \"כותרת 3\",\n    \"textColor\": \"צבע טקסט\",\n    \"backgroundColor\": \"צבע רקע\",\n    \"addYourLink\": \"הוספת הקישור שלך\",\n    \"openLink\": \"פתיחת קישור\",\n    \"copyLink\": \"העתקת קישור\",\n    \"removeLink\": \"הסרת קישור\",\n    \"editLink\": \"עריכת קישור\",\n    \"linkText\": \"טקסט\",\n    \"linkTextHint\": \"נא למלא טקסט\",\n    \"linkAddressHint\": \"נא למלא כתובת\",\n    \"highlightColor\": \"צבע הדגשה\",\n    \"clearHighlightColor\": \"איפוס צבע הדגשה\",\n    \"customColor\": \"צבע משלך\",\n    \"hexValue\": \"ערך הקס׳\",\n    \"opacity\": \"אטימות\",\n    \"resetToDefaultColor\": \"איפוס לצבע ברירת המחדל\",\n    \"ltr\": \"משמאל לימין\",\n    \"rtl\": \"מימין לשמאל\",\n    \"auto\": \"אוטו׳\",\n    \"cut\": \"גזירה\",\n    \"copy\": \"העתקה\",\n    \"paste\": \"הדבקה\",\n    \"find\": \"איתור\",\n    \"select\": \"בחירה\",\n    \"selectAll\": \"בחירה בהכול\",\n    \"previousMatch\": \"התוצאה הקודמת\",\n    \"nextMatch\": \"התוצאה הבאה\",\n    \"closeFind\": \"סגירה\",\n    \"replace\": \"החלפה\",\n    \"replaceAll\": \"החלפה של הכול\",\n    \"regex\": \"ביטוי רגולרי\",\n    \"caseSensitive\": \"תלוי רישיות\",\n    \"uploadImage\": \"העלאת תמונה\",\n    \"urlImage\": \"קישור לתמונה\",\n    \"incorrectLink\": \"קישור שגוי\",\n    \"upload\": \"העלאה\",\n    \"chooseImage\": \"בחירת תמונה\",\n    \"loading\": \"בטעינה\",\n    \"imageLoadFailed\": \"טעינת התמונה נכשלה\",\n    \"divider\": \"מפריד\",\n    \"table\": \"טבלה\",\n    \"colAddBefore\": \"הוספה לפני\",\n    \"rowAddBefore\": \"הוספה לפני\",\n    \"colAddAfter\": \"הוספה אחרי\",\n    \"rowAddAfter\": \"הוספה אחרי\",\n    \"colRemove\": \"הסרה\",\n    \"rowRemove\": \"הסרה\",\n    \"colDuplicate\": \"שכפול\",\n    \"rowDuplicate\": \"שכפול\",\n    \"colClear\": \"פינוי תוכן\",\n    \"rowClear\": \"פינוי תוכן\",\n    \"slashPlaceHolder\": \"נא להקליד ‚/’ כדי להוסיף מקטע או להתחיל להקליד\",\n    \"typeSomething\": \"נא להקליד משהו…\",\n    \"toggleListShortForm\": \"מתג\",\n    \"quoteListShortForm\": \"ציטוט\",\n    \"mathEquationShortForm\": \"נוסחה\",\n    \"codeBlockShortForm\": \"קוד\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"לא עמוד מועדף\",\n    \"noFavoriteHintText\": \"החלקת העמוד שמאלה תוסיף אותו למועדפים שלך\",\n    \"removeFromSidebar\": \"הסרה מסרגל הצד\",\n    \"addToSidebar\": \"הצמדה לסרגל צד\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"יש להקליד / כדי להוסיף מקטע, או להתחיל להקליד\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"מטלה\",\n    \"bulletList\": \"רשימה\",\n    \"numberList\": \"רשימה\",\n    \"quote\": \"ציטוט\",\n    \"heading\": \"כותרת {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"סמל עמוד\",\n    \"language\": \"שפה\",\n    \"font\": \"גופן\",\n    \"actions\": \"פעולות\",\n    \"date\": \"תאריך\",\n    \"addField\": \"הוספת שדה\",\n    \"userIcon\": \"סמל משתמש\"\n  },\n  \"noLogFiles\": \"אין קובצי יומנים\",\n  \"newSettings\": {\n    \"myAccount\": {\n      \"title\": \"החשבון שלי\",\n      \"subtitle\": \"התאמת הפרופיל שלך, ניהול אבטחת החשבון, מפתחות בינה מלאכותית או כניסה לחשבון שלך.\",\n      \"profileLabel\": \"שם חשבון ותמונת פרופיל\",\n      \"profileNamePlaceholder\": \"נא למלא את השם שלך\",\n      \"accountSecurity\": \"אבטחת חשבון\",\n      \"2FA\": \"אימות דו־שלבי\",\n      \"aiKeys\": \"מפתחות בינה מלאכותית\",\n      \"accountLogin\": \"כניסה לחשבון\",\n      \"updateNameError\": \"עדכון השם נכשל\",\n      \"updateIconError\": \"עדכון הסמל נכשל\",\n      \"deleteAccount\": {\n        \"title\": \"מחיקת חשבון\",\n        \"subtitle\": \"מחיקת החשבון וכל הנתונים שלך לצמיתות.\",\n        \"deleteMyAccount\": \"מחיקת החשבון שלי\",\n        \"dialogTitle\": \"מחיקת חשבון\",\n        \"dialogContent1\": \"למחוק את החשבון שלך לצמיתות?\",\n        \"dialogContent2\": \"זאת פעולה בלתי הפיכה והיא תסיר את הגישה מכל מרחבי הצוותים תוך מחיקת החשבון כולו לרבות מרחבי עבודה פרטיים והסרתך מכל מרחבי העבודה המשותפים.\"\n      }\n    },\n    \"workplace\": {\n      \"name\": \"מקום עבודה\",\n      \"title\": \"הגדרות מקום עבודה\",\n      \"subtitle\": \"התאמת המראה, ערכת העיצוב, גופן, פריסת הטקסט, התאריך, השעה והשפה של מרחב העבודה שלך.\",\n      \"workplaceName\": \"שם מקום העבודה\",\n      \"workplaceNamePlaceholder\": \"נא למלא שם למקום העבודה\",\n      \"workplaceIcon\": \"סמל מקום עבודה\",\n      \"workplaceIconSubtitle\": \"אפשר להעלות תמונה או להשתמש באמוג׳י למרחב העבודה שלך. הסמל יופיע בסרגל הצד ובהתראות שלך.\",\n      \"renameError\": \"שינוי שם מקום העבודה נכשל\",\n      \"updateIconError\": \"עדכון הסמל נכשל\",\n      \"chooseAnIcon\": \"נא לבחור סמל\",\n      \"appearance\": {\n        \"name\": \"מראה\",\n        \"themeMode\": {\n          \"auto\": \"אוטו׳\",\n          \"light\": \"בהיר\",\n          \"dark\": \"כהה\"\n        },\n        \"language\": \"שפה\"\n      }\n    },\n    \"syncState\": {\n      \"syncing\": \"סנכרון\",\n      \"synced\": \"מסונכרן\",\n      \"noNetworkConnected\": \"אין רשת מחוברת\"\n    }\n  },\n  \"pageStyle\": {\n    \"title\": \"סגנון עמוד\",\n    \"layout\": \"פריסה\",\n    \"coverImage\": \"תמונת כריכה\",\n    \"pageIcon\": \"סמל עמוד\",\n    \"colors\": \"צבעים\",\n    \"gradient\": \"מדרג\",\n    \"backgroundImage\": \"תמונת רקע\",\n    \"presets\": \"ערכות מוגדרות\",\n    \"photo\": \"צילום\",\n    \"unsplash\": \"Unsplash\",\n    \"pageCover\": \"כריכת עמוד\",\n    \"none\": \"אין\",\n    \"openSettings\": \"פתיחת הגדרות\",\n    \"photoPermissionTitle\": \"@:appName רוצה לגשת לספריית התמונות שלך\",\n    \"photoPermissionDescription\": \"לאפשר גישה לספריית התמונות כדי להעלות תמונות.\",\n    \"doNotAllow\": \"לא להרשות\",\n    \"image\": \"תמונה\"\n  },\n  \"commandPalette\": {\n    \"placeholder\": \"נא להקליד כדי לחפש…\",\n    \"bestMatches\": \"התוצאות הטובות ביותר\",\n    \"recentHistory\": \"היסטוריה עדכנית\",\n    \"navigateHint\": \"לניווט\",\n    \"loadingTooltip\": \"אנו מחפשים תוצאות…\",\n    \"betaLabel\": \"בטא\",\n    \"betaTooltip\": \"אנו תומכים רק בחיפוש אחר עמודים ותוכן במסמכים\",\n    \"fromTrashHint\": \"מהאשפה\",\n    \"noResultsHint\": \"לא מצאנו מה שחיפשת, כדאי לנסות לחפש ביטוי אחר.\",\n    \"clearSearchTooltip\": \"פינוי שדה חיפוש\"\n  },\n  \"space\": {\n    \"delete\": \"מחיקה\",\n    \"deleteConfirmation\": \"מחיקה: \",\n    \"deleteConfirmationDescription\": \"כל העמודים במרחב הזה יימחקו ויעברו לאשפה, ועמודים שפורסמו יוסתרו.\",\n    \"rename\": \"שינוי שם מרחב\",\n    \"changeIcon\": \"החלפת סמל\",\n    \"manage\": \"ניהול מרחב\",\n    \"addNewSpace\": \"יצירת מרחב\",\n    \"collapseAllSubPages\": \"צמצום כל תת־העמודים\",\n    \"createNewSpace\": \"יצירת מרחב חדש\",\n    \"createSpaceDescription\": \"אפשר ליצור מגוון מרחבים ציבוריים ופרטיים כדי לארגן את העבודה שלך בצורה טובה יותר.\",\n    \"spaceName\": \"שם המרחב\",\n    \"permission\": \"הרשאה\",\n    \"publicPermission\": \"ציבורי\",\n    \"publicPermissionDescription\": \"כל החברים במרחב העבודה שיש להם גישה מלאה\",\n    \"privatePermission\": \"פרטי\",\n    \"privatePermissionDescription\": \"רק לך יש גישה למרחב הזה\",\n    \"spaceIconBackground\": \"צבע הרקע\",\n    \"spaceIcon\": \"סמל\",\n    \"dangerZone\": \"אזור הסכנה\",\n    \"unableToDeleteLastSpace\": \"לא ניתן למחוק את המרחב האחרון\",\n    \"unableToDeleteSpaceNotCreatedByYou\": \"לא ניתן למחוק מרחבים שנוצרו על ידי אחרים\",\n    \"enableSpacesForYourWorkspace\": \"הפעלת מרחבים למרחב העבודה שלך\",\n    \"title\": \"מרחבים\",\n    \"defaultSpaceName\": \"כללי\",\n    \"upgradeSpaceTitle\": \"הפעלת מרחבים\",\n    \"upgradeSpaceDescription\": \"אפשר ליצור מגוון מרחבים ציבוריים ופרטיים כדי לארגן את מרחב העבודה שלך בצורה טובה יותר.\",\n    \"upgrade\": \"עדכון\",\n    \"upgradeYourSpace\": \"יצירת מרחבים במרוכז\",\n    \"quicklySwitch\": \"מעבר למרחב הבא במהירות\",\n    \"duplicate\": \"שכפול מרחב\",\n    \"movePageToSpace\": \"העבר עמוד למרחב\",\n    \"switchSpace\": \"החלפת מרחב\"\n  },\n  \"publish\": {\n    \"hasNotBeenPublished\": \"העמוד הזה לא פורסם עדיין\",\n    \"reportPage\": \"דיווח על עמוד\",\n    \"databaseHasNotBeenPublished\": \"עדיין אין תמיכה בפרסום למסדי נתונים.\",\n    \"createdWith\": \"נוצר עם\",\n    \"downloadApp\": \"הורדת AppFlowy\",\n    \"copy\": {\n      \"codeBlock\": \"תוכן מקטע הקוד הועתק ללוח הגזירים\",\n      \"imageBlock\": \"קישור התמונה הועתק ללוח הגזירים\",\n      \"mathBlock\": \"הנוסחה המתמטית הועתקה ללוח הגזירים\"\n    },\n    \"containsPublishedPage\": \"העמוד הזה מכיל עמוד או יותר שפורסמו. המשך בתהליך יסתיר אותם. להמשיך במחיקה?\",\n    \"publishSuccessfully\": \"הפרסום הצליח\",\n    \"unpublishSuccessfully\": \"ההסתרה הצליחה\",\n    \"publishFailed\": \"הפרסום נכשל\",\n    \"unpublishFailed\": \"ההסתרה נכשלה\",\n    \"noAccessToVisit\": \"אין גישה לעמוד הזה…\",\n    \"createWithAppFlowy\": \"יצירת אתר עם AppFlowy\",\n    \"fastWithAI\": \"מהיר וקל עם בינה מלאכותית.\",\n    \"tryItNow\": \"לנסות כעת\"\n  },\n  \"web\": {\n    \"continue\": \"המשך\",\n    \"or\": \"או\",\n    \"continueWithGoogle\": \"להמשיך עם Google\",\n    \"continueWithGithub\": \"להמשיך עם GitHub\",\n    \"continueWithDiscord\": \"להמשיך עם Discord\",\n    \"signInAgreement\": \"לחיצה על „המשך” להלן, מהווה את אישורך\\nלכך שקראת, הבנת והסכמת לתנאים של AppFlowy\",\n    \"and\": \"וגם\",\n    \"termOfUse\": \"תנאים\",\n    \"privacyPolicy\": \"מדיניות פרטיות\",\n    \"signInError\": \"שגיאת כניסה\",\n    \"login\": \"הרשמה או כניסה\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/hin.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"मैं\",\n  \"welcomeText\": \" @:appName में आपका स्वागत है\",\n  \"githubStarText\": \"गिटहब पर स्टार करे\",\n  \"subscribeNewsletterText\": \"समाचार पत्रिका के लिए सदस्यता लें\",\n  \"letsGoButtonText\": \"जल्दी शुरू करे\",\n  \"title\": \"शीर्षक\",\n  \"youCanAlso\": \"आप भी कर सकते हैं\",\n  \"and\": \"और\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"नीचे जोड़ने के लिए क्लिक करें\",\n    \"addAboveCmd\": \"Alt+click \",\n    \"addAboveMacCmd\": \"Option+click\",\n    \"addAboveTooltip\": \"ऊपर जोड़ने के लिए\",\n    \"dragTooltip\": \"ले जाने के लिए ड्रैग करें\",\n    \"openMenuTooltip\": \"मेनू खोलने के लिए क्लिक करें\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"साइन अप करें\",\n    \"title\": \"साइन अप करें @:appName\",\n    \"getStartedText\": \"शुरू करे\",\n    \"emptyPasswordError\": \"पासवर्ड खाली नहीं हो सकता\",\n    \"repeatPasswordEmptyError\": \"रिपीट पासवर्ड खाली नहीं हो सकता\",\n    \"unmatchedPasswordError\": \"रिपीट पासवर्ड और पासवर्ड एक नहीं है\",\n    \"alreadyHaveAnAccount\": \"क्या आपके पास पहले से एक खाता मौजूद है?\",\n    \"emailHint\": \"ईमेल\",\n    \"passwordHint\": \"पासवर्ड\",\n    \"repeatPasswordHint\": \"रिपीट पासवर्ड\",\n    \"signUpWith\": \"इसके साथ साइन अप करें:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"लॉग इन करें @:appName\",\n    \"loginButtonText\": \"लॉग इन करें\",\n    \"loginStartWithAnonymous\": \"एक अज्ञात सत्र से प्रारंभ करें\",\n    \"continueAnonymousUser\": \"अज्ञात सत्र जारी रखें\",\n    \"buttonText\": \"साइन इन\",\n    \"forgotPassword\": \"पासवर्ड भूल गए?\",\n    \"emailHint\": \"ईमेल\",\n    \"passwordHint\": \"पासवर्ड\",\n    \"dontHaveAnAccount\": \"कोई खाता नहीं है?\",\n    \"repeatPasswordEmptyError\": \"रिपीट पासवर्ड खाली नहीं हो सकता\",\n    \"unmatchedPasswordError\": \"रिपीट पासवर्ड और पासवर्ड एक नहीं है\",\n    \"syncPromptMessage\": \"डेटा को सिंक करने में कुछ समय लग सकता है. कृपया इस पेज को बंद न करें\",\n    \"or\": \"या\",\n    \"LogInWithGoogle\": \"गूगल से लॉग इन करें\",\n    \"LogInWithGithub\": \"गिटहब से लॉग इन करें\",\n    \"LogInWithDiscord\": \"डिस्कॉर्ड से लॉग इन करें\",\n    \"signInWith\": \"इसके साथ साइन इन करें:\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"अपना कार्यक्षेत्र चुनें\",\n    \"create\": \"कार्यक्षेत्र बनाएं\",\n    \"reset\": \"कार्यक्षेत्र रीसेट करें\",\n    \"resetWorkspacePrompt\": \"कार्यक्षेत्र को रीसेट करने से उसमें मौजूद सभी पृष्ठ और डेटा हट जाएंगे। क्या आप वाकई कार्यक्षेत्र को रीसेट करना चाहते हैं? वैकल्पिक रूप से, आप कार्यक्षेत्र को पुनर्स्थापित करने के लिए सहायता टीम से संपर्क कर सकते हैं\",\n    \"hint\": \"कार्यक्षेत्र\",\n    \"notFoundError\": \"कार्यस्थल नहीं मिला\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"शेयर\",\n    \"workInProgress\": \"जल्द आ रहा है\",\n    \"markdown\": \"markdown\",\n    \"csv\": \"csv\",\n    \"copyLink\": \"लिंक कॉपी करें\"\n  },\n  \"moreAction\": {\n    \"small\": \"छोटा\",\n    \"medium\": \"मध्यम\",\n    \"large\": \"बड़ा\",\n    \"fontSize\": \"अक्षर का आकर\",\n    \"import\": \"आयात\",\n    \"moreOptions\": \"अधिक विकल्प\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Text & Markdown\",\n    \"documentFromV010\": \"Document from v0.1.0\",\n    \"databaseFromV010\": \"Database from v0.1.0\",\n    \"csv\": \"CSV\",\n    \"database\": \"Database\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"नाम बदलें\",\n    \"delete\": \"हटाएं\",\n    \"duplicate\": \"डुप्लीकेट\",\n    \"unfavorite\": \"पसंदीदा से हटाएँ\",\n    \"favorite\": \"पसंदीदा में जोड़ें\",\n    \"openNewTab\": \"एक नए टैब में खोलें\",\n    \"moveTo\": \"स्थानांतरित करें\",\n    \"addToFavorites\": \"पसंदीदा में जोड़ें\",\n    \"copyLink\": \"कॉपी लिंक\"\n  },\n  \"blankPageTitle\": \"रिक्त पेज\",\n  \"newPageText\": \"नया पेज\",\n  \"newDocumentText\": \"नया दस्तावेज़\",\n  \"newGridText\": \"नया ग्रिड\",\n  \"newCalendarText\": \"नया कैलेंडर\",\n  \"newBoardText\": \"नया बोर्ड\",\n  \"trash\": {\n    \"text\": \"कचरा\",\n    \"restoreAll\": \"सभी पुनर्स्थापित करें\",\n    \"deleteAll\": \"सभी हटाएँ\",\n    \"pageHeader\": {\n      \"fileName\": \"फ़ाइलनाम\",\n      \"lastModified\": \"अंतिम संशोधित\",\n      \"created\": \"बनाया गया\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"क्या आप निश्चित रूप से ट्रैश में मौजूद सभी पेज को हटाना चाहते हैं?\",\n      \"caption\": \"यह कार्रवाई पूर्ववत नहीं की जा सकती।\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"क्या आप निश्चित रूप से ट्रैश में सभी पेज को पुनर्स्थापित करना चाहते हैं?\",\n      \"caption\": \"यह कार्रवाई पूर्ववत नहीं की जा सकती।\"\n    }\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"यह पेज कूड़ेदान में है\",\n    \"restore\": \"पुनर्स्थापित पेज\",\n    \"deletePermanent\": \"स्थायी रूप से हटाएँ\"\n  },\n  \"dialogCreatePageNameHint\": \"पेज का नाम\",\n  \"questionBubble\": {\n    \"shortcuts\": \"शॉर्टकट\",\n    \"whatsNew\": \"क्या नया है?\",\n    \"help\": \"सहायता\",\n    \"markdown\": \"markdown\",\n    \"debug\": {\n      \"name\": \"डीबग जानकारी\",\n      \"success\": \"डिबग जानकारी क्लिपबोर्ड पर कॉपी की गई!\",\n      \"fail\": \"डिबग जानकारी को क्लिपबोर्ड पर कॉपी करने में असमर्थ\"\n    },\n    \"feedback\": \"जानकारी देना\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"निकालें, नाम बदलें, और भी बहुत कुछ...\",\n    \"addPageTooltip\": \"जल्दी से अंदर एक पेज जोड़ें\",\n    \"defaultNewPageName\": \"शीर्षकहीन\",\n    \"renameDialog\": \"नाम बदलें\"\n  },\n  \"toolbar\": {\n    \"undo\": \"अनडू\",\n    \"redo\": \"रीडू\",\n    \"bold\": \"बोल्ड\",\n    \"italic\": \"इटैलिक\",\n    \"underline\": \"अंडरलाइन\",\n    \"strike\": \"स्ट्राइकथ्रू\",\n    \"numList\": \"क्रमांकित सूची\",\n    \"bulletList\": \"बुलेट सूची\",\n    \"checkList\": \"चेकलिस्ट\",\n    \"inlineCode\": \"इनलाइन कोड\",\n    \"quote\": \"कोट\",\n    \"header\": \"हेडर\",\n    \"highlight\": \"हाइलाइट करें\",\n    \"color\": \"रंग\",\n    \"addLink\": \"लिंक जोड़ें\",\n    \"link\": \"लिंक\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"लाइट मोड पर स्विच करें\",\n    \"darkMode\": \"डार्क मोड पर स्विच करें\",\n    \"openAsPage\": \"पेज के रूप में खोलें\",\n    \"addNewRow\": \"एक नई पंक्ति जोड़ें\",\n    \"openMenu\": \"मेनू खोलने के लिए क्लिक करें\",\n    \"dragRow\": \"पंक्ति को पुनः व्यवस्थित करने के लिए देर तक दबाएँ\",\n    \"viewDataBase\": \"डेटाबेस देखें\",\n    \"referencePage\": \"यह {name} रफेरेंसेड है\",\n    \"addBlockBelow\": \"नीचे एक ब्लॉक जोड़ें\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"साइड बार बंद करें\",\n    \"openSidebar\": \"साइड बार खोलें\",\n    \"personal\": \"व्यक्तिगत\",\n    \"favorites\": \"पसंदीदा\",\n    \"clickToHidePersonal\": \"व्यक्तिगत अनुभाग को छिपाने के लिए क्लिक करें\",\n    \"clickToHideFavorites\": \"पसंदीदा अनुभाग को छिपाने के लिए क्लिक करें\",\n    \"addAPage\": \"एक पेज जोड़ें\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"आपका नोट मार्कडाउन के रूप में सफलतापूर्वक निर्यात कर दिया गया है।\",\n      \"path\": \"दस्तावेज़/प्रवाह\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"संपर्क\",\n    \"whatsHappening\": \"इस सप्ताह क्या हो रहा है?\",\n    \"addContact\": \"संपर्क जोड़ें\",\n    \"editContact\": \"संपर्क संपादित करें\"\n  },\n  \"button\": {\n    \"ok\": \"ठीक है\",\n    \"cancel\": \"रद्द करें\",\n    \"signIn\": \"साइन इन करें\",\n    \"signOut\": \"साइन आउट करें\",\n    \"complete\": \"पूर्ण\",\n    \"save\": \"सेव\",\n    \"generate\": \"उत्पन्न करें\",\n    \"esc\": \"एस्केप\",\n    \"keep\": \"रखें\",\n    \"tryAgain\": \"फिर से प्रयास करें\",\n    \"discard\": \"त्यागें\",\n    \"replace\": \"बदलें\",\n    \"insertBelow\": \"नीचे डालें\",\n    \"upload\": \"अपलोड करें\",\n    \"edit\": \"संपादित करें\",\n    \"delete\": \"हटाएं\",\n    \"duplicate\": \"डुप्लिकेट\",\n    \"done\": \"किया\",\n    \"putback\": \"पुन्हा डालिए\"\n  },\n  \"label\": {\n    \"welcome\": \"आपका स्वागत है\",\n    \"firstName\": \"पहला नाम\",\n    \"middleName\": \"मध्य नाम\",\n    \"lastName\": \"अंतिम नाम\",\n    \"stepX\": \"स्टेप {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"आपके खाते से जुड़ने में असमर्थ।\",\n      \"failedMsg\": \"कृपया सुनिश्चित करें कि आपने अपने ब्राउज़र में साइन-इन प्रक्रिया पूरी कर ली है।\"\n    },\n    \"google\": {\n      \"title\": \"Google साइन-इन\",\n      \"instruction1\": \"अपने Google संपर्कों को आयात करने के लिए, आपको अपने वेब ब्राउज़र का उपयोग करके इस एप्लिकेशन को अधिकृत करना होगा।\",\n      \"instruction2\": \"आइकन पर क्लिक करके या टेक्स्ट का चयन करके इस कोड को अपने क्लिपबोर्ड पर कॉपी करें:\",\n      \"instruction3\": \"अपने वेब ब्राउज़र में निम्नलिखित लिंक पर जाएँ, और उपरोक्त कोड दर्ज करें\",\n      \"instruction4\": \"साइनअप पूरा होने पर नीचे दिया गया बटन दबाएँ:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"सेटिंग्स\",\n    \"menu\": {\n      \"appearance\": \"दृश्य\",\n      \"language\": \"भाषा\",\n      \"user\": \"उपयोगकर्ता\",\n      \"files\": \"फ़ाइलें\",\n      \"open\": \"सेटिंग्स खोलें\",\n      \"logout\": \"लॉगआउट\",\n      \"logoutPrompt\": \"क्या आप निश्चित रूप से लॉगआउट करना चाहते हैं?\",\n      \"selfEncryptionLogoutPrompt\": \"क्या आप वाकई लॉग आउट करना चाहते हैं? कृपया सुनिश्चित करें कि आपने एन्क्रिप्शन रहस्य की कॉपी बना ली है\",\n      \"syncSetting\": \"सिंक सेटिंग\",\n      \"enableSync\": \"सिंक इनेबल करें\",\n      \"enableEncrypt\": \"डेटा एन्क्रिप्ट करें\",\n      \"enableEncryptPrompt\": \"इस रहस्य के साथ अपने डेटा को सुरक्षित करने के लिए एन्क्रिप्शन सक्रिय करें। इसे सुरक्षित रूप से संग्रहीत करें; एक बार सक्षम होने के बाद, इसे बंद नहीं किया जा सकता है। यदि खो जाता है, तो आपका डेटा पुनर्प्राप्त नहीं किया जा सकता है। कॉपी करने के लिए क्लिक करें\",\n      \"inputEncryptPrompt\": \"कृपया अपना एन्क्रिप्शन रहस्य दर्ज करें\",\n      \"clickToCopySecret\": \"गुप्त कॉपी बनाने के लिए क्लिक करें\",\n      \"inputTextFieldHint\": \"आपका रहस्य\",\n      \"historicalUserList\": \"उपयोगकर्ता लॉगिन इतिहास\",\n      \"historicalUserListTooltip\": \"यह सूची आपके अज्ञात खातों को प्रदर्शित करती है। आप किसी खाते का विवरण देखने के लिए उस पर क्लिक कर सकते हैं। 'आरंभ करें' बटन पर क्लिक करके अज्ञात खाते बनाए जाते हैं\",\n      \"openHistoricalUser\": \"अज्ञात खाता खोलने के लिए क्लिक करें\"\n    },\n    \"appearance\": {\n      \"resetSetting\": \"इस सेटिंग को रीसेट करें\",\n      \"fontFamily\": {\n        \"label\": \"फ़ॉन्ट फॅमिली\",\n        \"search\": \"खोजें\"\n      },\n      \"themeMode\": {\n        \"label\": \"थीम मोड\",\n        \"light\": \"लाइट मोड\",\n        \"dark\": \"डार्क मोड\",\n        \"system\": \"सिस्टम के अनुसार अनुकूलित करें\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"लेआउट दिशा\",\n        \"hint\": \"अपनी स्क्रीन पर सामग्री के प्रवाह को बाएँ से दाएँ या दाएँ से बाएँ नियंत्रित करें।\",\n        \"ltr\": \"एलटीआर\",\n        \"rtl\": \"आरटीएल\"\n      },\n      \"textDirection\": {\n        \"label\": \"डिफ़ॉल्ट वाक्य दिशा\",\n        \"hint\": \"निर्दिष्ट करें कि वाक्य को डिफ़ॉल्ट के रूप में बाएँ या दाएँ से प्रारंभ करना चाहिए।\",\n        \"ltr\": \"एलटीआर\",\n        \"rtl\": \"आरटीएल\",\n        \"auto\": \"ऑटो\",\n        \"fallback\": \"लेआउट दिशा के समान\"\n      },\n      \"themeUpload\": {\n        \"button\": \"अपलोड करें\",\n        \"uploadTheme\": \"थीम अपलोड करें\",\n        \"description\": \"नीचे दिए गए बटन का उपयोग करके अपनी खुद की AppFlowy थीम अपलोड करें।\",\n        \"failure\": \"जो थीम अपलोड किया गया था उसका प्रारूप अमान्य था।\",\n        \"loading\": \"कृपया तब तक प्रतीक्षा करें जब तक हम आपकी थीम को सत्यापित और अपलोड नहीं कर देते...\",\n        \"uploadSuccess\": \"आपका थीम सफलतापूर्वक अपलोड किया गया\",\n        \"deletionFailure\": \"थीम को हटाने में विफल। इसे मैन्युअल रूप से हटाने का प्रयास करें।\",\n        \"filePickerDialogTitle\": \"एक .flowy_plugin फ़ाइल चुनें\",\n        \"urlUploadFailure\": \"URL खोलने में विफल: {}\"\n      },\n      \"theme\": \"थीम\",\n      \"builtInsLabel\": \"डिफ़ॉल्ट थीम\",\n      \"pluginsLabel\": \"प्लगइन्स\",\n      \"showNamingDialogWhenCreatingPage\": \"पेज बनाते समय उसका नाम लेने के लिए डायलॉग देखे\"\n    },\n    \"files\": {\n      \"copy\": \"कॉपी करें\",\n      \"defaultLocation\": \"फ़ाइलें और डेटा संग्रहण स्थान पढ़ें\",\n      \"exportData\": \"अपना डेटा निर्यात करें\",\n      \"doubleTapToCopy\": \"पथ को कॉपी करने के लिए दो बार टैप करें\",\n      \"restoreLocation\": \"AppFlowy डिफ़ॉल्ट पथ पर रीस्टार्ट करें\",\n      \"customizeLocation\": \"कोई अन्य फ़ोल्डर खोलें\",\n      \"restartApp\": \"परिवर्तनों को प्रभावी बनाने के लिए कृपया ऐप को रीस्टार्ट करें।\",\n      \"exportDatabase\": \"डेटाबेस निर्यात करें\",\n      \"selectFiles\": \"उन फ़ाइलों का चयन करें जिन्हें निर्यात करने की आवश्यकता है\",\n      \"selectAll\": \"सभी का चयन करें\",\n      \"deselectAll\": \"सभी को अचयनित करें\",\n      \"createNewFolder\": \"एक नया फ़ोल्डर बनाएँ\",\n      \"createNewFolderDesc\": \"हमें बताएं कि आप अपना डेटा कहां संग्रहीत करना चाहते हैं\",\n      \"defineWhereYourDataIsStored\": \"परिभाषित करें कि आपका डेटा कहाँ संग्रहीत है\",\n      \"open\": \"खोलें\",\n      \"openFolder\": \"मौजूदा फ़ोल्डर खोलें\",\n      \"openFolderDesc\": \"इसे पढ़ें और इसे अपने मौजूदा AppFlowy फ़ोल्डर में लिखें\",\n      \"folderHintText\": \"फ़ोल्डर का नाम\",\n      \"location\": \"एक नया फ़ोल्डर बनाना\",\n      \"locationDesc\": \"अपने AppFlowy डेटा फ़ोल्डर के लिए एक नाम चुनें\",\n      \"browser\": \"ब्राउज़ करें\",\n      \"create\": \"बनाएँ\",\n      \"set\": \"सेट\",\n      \"folderPath\": \"आपके फ़ोल्डर को संग्रहीत करने का पथ\",\n      \"locationCannotBeEmpty\": \"पथ खाली नहीं हो सकता\",\n      \"pathCopiedSnackbar\": \"फ़ाइल संग्रहण पथ क्लिपबोर्ड पर कॉपी किया गया!\",\n      \"changeLocationTooltips\": \"डेटा निर्देशिका बदलें\",\n      \"change\": \"परिवर्तन\",\n      \"openLocationTooltips\": \"अन्य डेटा निर्देशिका खोलें\",\n      \"openCurrentDataFolder\": \"वर्तमान डेटा निर्देशिका खोलें\",\n      \"recoverLocationTooltips\": \"AppFlowy की डिफ़ॉल्ट डेटा निर्देशिका पर रीसेट करें\",\n      \"exportFileSuccess\": \"फ़ाइल सफलतापूर्वक निर्यात हुई\",\n      \"exportFileFail\": \"फ़ाइल निर्यात विफल रहा!\",\n      \"export\": \"निर्यात\"\n    },\n    \"user\": {\n      \"name\": \"नाम\",\n      \"email\": \"ईमेल\",\n      \"tooltipSelectIcon\": \"आइकन चुनें\",\n      \"selectAnIcon\": \"एक आइकन चुनें\",\n      \"pleaseInputYourOpenAIKey\": \"कृपया अपनी AI key इनपुट करें\",\n      \"clickToLogout\": \"वर्तमान उपयोगकर्ता को लॉगआउट करने के लिए क्लिक करें\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"शॉर्टकट\",\n      \"command\": \"कमांड\",\n      \"keyBinding\": \"कीबाइंडिंग\",\n      \"addNewCommand\": \"नया कमांड जोड़ें\",\n      \"updateShortcutStep\": \"इच्छित key संयोजन दबाएँ और ENTER दबाएँ\",\n      \"shortcutIsAlreadyUsed\": \"यह शॉर्टकट पहले से ही इसके लिए उपयोग किया जा चुका है: {conflict}\",\n      \"resetToDefault\": \"डिफ़ॉल्ट कीबाइंडिंग पर रीसेट करें\",\n      \"couldNotLoadErrorMsg\": \"शॉर्टकट लोड नहीं हो सका, पुनः प्रयास करें\",\n      \"couldNotSaveErrorMsg\": \"शॉर्टकट सेव नहीं किये जा सके, पुनः प्रयास करें\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"क्या आप वाकई इस दृश्य को हटाना चाहते हैं?\",\n    \"createView\": \"नया\",\n    \"title\": {\n      \"placeholder\": \"शीर्षकहीन\"\n    },\n    \"settings\": {\n      \"filter\": \"फ़िल्टर\",\n      \"sort\": \"क्रमबद्ध करें\",\n      \"sortBy\": \"क्रमबद्ध करें\",\n      \"properties\": \"गुण\",\n      \"reorderPropertiesTooltip\": \"गुणों को पुनः व्यवस्थित करने के लिए खींचें\",\n      \"group\": \"समूह\",\n      \"addFilter\": \"फ़िल्टर करें...\",\n      \"deleteFilter\": \"फ़िल्टर हटाएँ\",\n      \"filterBy\": \"फ़िल्टरबाय...\",\n      \"typeAValue\": \"एक वैल्यू टाइप करें...\",\n      \"layout\": \"लेआउट\",\n      \"databaseLayout\": \"लेआउट\"\n    },\n    \"textFilter\": {\n      \"contains\": \"शामिल है\",\n      \"doesNotContain\": \"इसमें शामिल नहीं है\",\n      \"endsWith\": \"समाप्त होता है\",\n      \"startWith\": \"से प्रारंभ होता है\",\n      \"is\": \"है\",\n      \"isNot\": \"नहीं है\",\n      \"isEmpty\": \"खाली है\",\n      \"isNotEmpty\": \"खाली नहीं है\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"नहीं है\",\n        \"startWith\": \"से प्रारंभ होता है\",\n        \"endWith\": \"के साथ समाप्त होता है\",\n        \"isEmpty\": \"खाली है\",\n        \"isNotEmpty\": \"खाली नहीं है\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"चेक किया गया\",\n      \"isUnchecked\": \"अनचेक किया हुआ\",\n      \"choicechipPrefix\": {\n        \"is\": \"है\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"पूर्ण है\",\n      \"isIncomplted\": \"अपूर्ण है\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"है\",\n      \"isNot\": \"नहीं है\",\n      \"contains\": \"शामिल है\",\n      \"doesNotContain\": \"इसमें शामिल नहीं है\",\n      \"isEmpty\": \"खाली है\",\n      \"isNotEmpty\": \"खाली नहीं है\"\n    },\n    \"field\": {\n      \"hide\": \"छिपाएँ\",\n      \"insertLeft\": \"बायाँ सम्मिलित करें\",\n      \"insertRight\": \"दाएँ सम्मिलित करें\",\n      \"duplicate\": \"डुप्लिकेट\",\n      \"delete\": \"हटाएं\",\n      \"textFieldName\": \"लेख\",\n      \"checkboxFieldName\": \"चेकबॉक्स\",\n      \"dateFieldName\": \"दिनांक\",\n      \"updatedAtFieldName\": \"अंतिम संशोधित समय\",\n      \"createdAtFieldName\": \"बनाने का समय\",\n      \"numberFieldName\": \"संख्या\",\n      \"singleSelectFieldName\": \"चुनाव\",\n      \"multiSelectFieldName\": \"बहु चुनाव\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"चेकलिस्ट\",\n      \"numberFormat\": \"संख्या प्रारूप\",\n      \"dateFormat\": \"दिनांक प्रारूप\",\n      \"includeTime\": \"समय शामिल करें\",\n      \"isRange\": \"अंतिम तिथि\",\n      \"dateFormatFriendly\": \"माह दिन, वर्ष\",\n      \"dateFormatISO\": \"वर्ष-महीना-दिन\",\n      \"dateFormatLocal\": \"महीना/दिन/वर्ष\",\n      \"dateFormatUS\": \"वर्ष/महीना/दिन\",\n      \"dateFormatDayMonthYear\": \"दिन/माह/वर्ष\",\n      \"timeFormat\": \"समय प्रारूप\",\n      \"invalidTimeFormat\": \"अमान्य प्रारूप\",\n      \"timeFormatTwelveHour\": \"१२ घंटा\",\n      \"timeFormatTwentyFourHour\": \"२४ घंटे\",\n      \"clearDate\": \"तिथि मिटाए\",\n      \"addSelectOption\": \"एक विकल्प जोड़ें\",\n      \"optionTitle\": \"विकल्प\",\n      \"addOption\": \"विकल्प जोड़ें\",\n      \"editProperty\": \"डेटा का प्रकार संपादित करें\",\n      \"newProperty\": \"नया डेटा का प्रकार\",\n      \"deleteFieldPromptMessage\": \"क्या आप निश्चित हैं? यह डेटा का प्रकार हटा दी जाएगी\",\n      \"newColumn\": \"नया कॉलम\"\n    },\n    \"sort\": {\n      \"ascending\": \"असेंडिंग\",\n      \"descending\": \"डिसेंडिंग\",\n      \"deleteAllSorts\": \"सभी प्रकार हटाएँ\",\n      \"addSort\": \"सॉर्ट जोड़ें\"\n    },\n    \"row\": {\n      \"duplicate\": \"डुप्लिकेट\",\n      \"delete\": \"डिलीट\",\n      \"titlePlaceholder\": \"शीर्षकहीन\",\n      \"textPlaceholder\": \"रिक्त\",\n      \"copyProperty\": \"डेटा के प्रकार को क्लिपबोर्ड पर कॉपी किया गया\",\n      \"count\": \"गिनती\",\n      \"newRow\": \"नई पंक्ति\",\n      \"action\": \"कार्रवाई\",\n      \"add\": \"नीचे जोड़ें पर क्लिक करें\",\n      \"drag\": \"स्थानांतरित करने के लिए खींचें\"\n    },\n    \"selectOption\": {\n      \"create\": \"बनाएँ\",\n      \"purpleColor\": \"बैंगनी\",\n      \"pinkColor\": \"गुलाबी\",\n      \"lightPinkColor\": \"हल्का गुलाबी\",\n      \"orangeColor\": \"नारंगी\",\n      \"yellowColor\": \"पीला\",\n      \"limeColor\": \"नींबू\",\n      \"greenColor\": \"हरा\",\n      \"aquaColor\": \"एक्वा\",\n      \"blueColor\": \"नीला\",\n      \"deleteTag\": \"टैग हटाएँ\",\n      \"colorPanelTitle\": \"रंग\",\n      \"panelTitle\": \"एक विकल्प चुनें या एक बनाएं\",\n      \"searchOption\": \"एक विकल्प खोजें\",\n      \"searchOrCreateOption\": \"कोई विकल्प खोजें या बनाएँ...\",\n      \"createNew\": \"एक नया बनाएँ\",\n      \"orSelectOne\": \"या एक विकल्प चुनें\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"कार्य विवरण\",\n      \"addNew\": \"एक नया कार्य जोड़ें\",\n      \"submitNewTask\": \"बनाएँ\"\n    },\n    \"menuName\": \"ग्रिड\",\n    \"referencedGridPrefix\": \"का दृश्य\"\n  },\n  \"document\": {\n    \"menuName\": \"दस्तावेज़ \",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"लिंक करने के लिए एक बोर्ड चुनें\",\n        \"createANewBoard\": \"एक नया बोर्ड बनाएं\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"लिंक करने के लिए एक ग्रिड चुनें\",\n        \"createANewGrid\": \"एक नया ग्रिड बनाएं\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"लिंक करने के लिए एक कैलेंडर चुनें\",\n        \"createANewCalendar\": \"एक नया कैलेंडर बनाएं\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"रूपरेखा\",\n      \"codeBlock\": \"कोड ब्लॉक\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"रेफेरेंस बोर्ड\",\n      \"referencedGrid\": \"रेफेरेंस ग्रिड\",\n      \"referencedCalendar\": \"रेफेरेंस कैलेंडर\",\n      \"autoGeneratorMenuItemName\": \"AI लेखक\",\n      \"autoGeneratorTitleName\": \"AI: AI को कुछ भी लिखने के लिए कहें...\",\n      \"autoGeneratorLearnMore\": \"और जानें\",\n      \"autoGeneratorGenerate\": \"उत्पन्न करें\",\n      \"autoGeneratorHintText\": \"AI से पूछें...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"AI key नहीं मिल सकी\",\n      \"autoGeneratorRewrite\": \"पुनः लिखें\",\n      \"smartEdit\": \"AI सहायक\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"वर्तनी ठीक करें\",\n      \"warning\": \"⚠️ AI प्रतिक्रियाएँ गलत या भ्रामक हो सकती हैं।\",\n      \"smartEditSummarize\": \"सारांश\",\n      \"smartEditImproveWriting\": \"लेख में सुधार करें\",\n      \"smartEditMakeLonger\": \"लंबा बनाएं\",\n      \"smartEditCouldNotFetchResult\": \"AI से परिणाम प्राप्त नहीं किया जा सका\",\n      \"smartEditCouldNotFetchKey\": \"AI key नहीं लायी जा सकी\",\n      \"smartEditDisabled\": \"सेटिंग्स में AI कनेक्ट करें\",\n      \"discardResponse\": \"क्या आप AI प्रतिक्रियाओं को छोड़ना चाहते हैं?\",\n      \"createInlineMathEquation\": \"समीकरण बनाएं\",\n      \"toggleList\": \"सूची टॉगल करें\",\n      \"cover\": {\n        \"changeCover\": \"कवर बदलें\",\n        \"colors\": \"रंग\",\n        \"images\": \"छवियां\",\n        \"clearAll\": \"सभी साफ़ करें\",\n        \"abstract\": \"सार\",\n        \"addCover\": \"कवर जोड़ें\",\n        \"addLocalImage\": \"स्थानीय छवि जोड़ें\",\n        \"invalidImageUrl\": \"अमान्य छवि URL\",\n        \"failedToAddImageToGallery\": \"गैलरी में छवि जोड़ने में विफल\",\n        \"enterImageUrl\": \"छवि URL दर्ज करें\",\n        \"add\": \"जोड़ें\",\n        \"back\": \"पीछे\",\n        \"saveToGallery\": \"गैलरी में सेव करे\",\n        \"removeIcon\": \"आइकन हटाएँ\",\n        \"pasteImageUrl\": \"छवि URL चिपकाएँ\",\n        \"or\": \"या\",\n        \"pickFromFiles\": \"फ़ाइलों में से चुनें\",\n        \"couldNotFetchImage\": \"छवि नहीं लाया जा सका\",\n        \"imageSavingFailed\": \"छवि सहेजना विफल\",\n        \"addIcon\": \"आइकन जोड़ें\",\n        \"coverRemoveAlert\": \"हटाने के बाद इसे कवर से हटा दिया जाएगा।\",\n        \"alertDialogConfirmation\": \"क्या आप निश्चित हैं, आप जारी रखना चाहते हैं?\"\n      },\n      \"mathEquation\": {\n        \"addMathEquation\": \"गणित समीकरण जोड़ें\",\n        \"editMathEquation\": \"गणित समीकरण संपादित करें\"\n      },\n      \"optionAction\": {\n        \"click\": \"क्लिक करें\",\n        \"toOpenMenu\": \"मेनू खोलने के लिए\",\n        \"delete\": \"हटाएं\",\n        \"duplicate\": \"डुप्लिकेट\",\n        \"turnInto\": \"टर्नइनटू\",\n        \"moveUp\": \"ऊपर बढ़ें\",\n        \"moveDown\": \"नीचे जाएँ\",\n        \"color\": \"रंग\",\n        \"align\": \"संरेखित करें\",\n        \"left\": \"बांया\",\n        \"center\": \"केंद्र\",\n        \"right\": \"सही\",\n        \"defaultColor\": \"डिफ़ॉल्ट\"\n      },\n      \"image\": {\n        \"copiedToPasteBoard\": \"छवि लिंक को क्लिपबोर्ड पर कॉपी कर दिया गया है\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"सामग्री की तालिका बनाने के लिए शीर्षक जोड़ें।\"\n      },\n      \"table\": {\n        \"addAfter\": \"बाद में जोड़ें\",\n        \"addBefore\": \"पहले जोड़ें\",\n        \"delete\": \"हटाएं\",\n        \"clear\": \"साफ़ करें\",\n        \"duplicate\": \"डुप्लिकेट\",\n        \"bgColor\": \"पृष्ठभूमि रंग\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"कॉपी करें\",\n        \"cut\": \"कट करे\",\n        \"paste\": \"पेस्ट करें\"\n      }\n    },\n    \"textBlock\": {\n      \"placeholder\": \"कमांड के लिए '/' टाइप करें\"\n    },\n    \"title\": {\n      \"placeholder\": \"शीर्षकहीन\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"छवि जोड़ने के लिए क्लिक करें\",\n      \"अपलोड करें\": {\n        \"label\": \"अपलोड करें\",\n        \"placeholder\": \"छवि अपलोड करने के लिए क्लिक करें\"\n      },\n      \"url\": {\n        \"label\": \"छवि URL \",\n        \"placeholder\": \"छवि URL दर्ज करें\"\n      },\n      \"support\": \"छवि आकार सीमा 5 एमबी है। समर्थित प्रारूप: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"अमान्य छवि\",\n        \"invalidImageSize\": \"छवि का आकार 5MB से कम होना चाहिए\",\n        \"invalidImageFormat\": \"छवि प्रारूप समर्थित नहीं है। समर्थित प्रारूप: JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"अमान्य छवि URL\"\n      }\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"भाषा\",\n        \"placeholder\": \"भाषा चुनें\"\n      }\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"लिंक चिपकाएँ या टाइप करें\",\n      \"openInNewTab\": \"नए टैब में खोलें\",\n      \"copyLink\": \"लिंक कॉपी करें\",\n      \"removeLink\": \"लिंक हटाएँ\",\n      \"url\": {\n        \"label\": \"लिंक URL\",\n        \"placeholder\": \"लिंक URL दर्ज करें\"\n      },\n      \"title\": {\n        \"label\": \"लिंक शीर्षक\",\n        \"placeholder\": \"लिंक शीर्षक दर्ज करें\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"किसी व्यक्ति या पेज या दिनांक का उल्लेख करें...\",\n      \"page\": {\n        \"label\": \"पेज से लिंक करें\",\n        \"tooltip\": \"पेज खोलने के लिए क्लिक करें\"\n      }\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"डिफ़ॉल्ट पर रीसेट करें\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"नया\"\n    },\n    \"menuName\": \"बोर्ड\",\n    \"referencedBoardPrefix\": \"का दृश्य\"\n  },\n  \"calendar\": {\n    \"menuName\": \"कैलेंडर\",\n    \"defaultNewCalendarTitle\": \"शीर्षकहीन\",\n    \"newEventButtonTooltip\": \"एक नया ईवेंट जोड़ें\",\n    \"navigation\": {\n      \"today\": \"आज\",\n      \"jumpToday\": \"जम्प टू टुडे\",\n      \"previousMonth\": \"पिछला महीना\",\n      \"nextMonth\": \"अगले महीने\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"सप्ताह संख्याएँ दिखाएँ\",\n      \"showWeekends\": \"सप्ताहांत दिखाएँ\",\n      \"firstDayOfWeek\": \"सप्ताह प्रारंभ करें\",\n      \"layoutDateField\": \"लेआउट कैलेंडर\",\n      \"noDateTitle\": \"कोई दिनांक नहीं\",\n      \"noDateHint\": \"अनिर्धारित घटनाएँ यहाँ दिखाई देंगी\",\n      \"clickToAdd\": \"कैलेंडर में जोड़ने के लिए क्लिक करें\",\n      \"name\": \"कैलेंडर लेआउट\"\n    },\n    \"referencedCalendarPrefix\": \"का दृश्य\"\n  },\n  \"errorDialog\": {\n    \"title\": \"AppFlowy error\",\n    \"howToFixFallback\": \"असुविधा के लिए हमें खेद है! हमारे GitHub पेज पर एक मुद्दा सबमिट करें जो आपकी error का वर्णन करता है।\",\n    \"github\": \"GitHub पर देखें  \"\n  },\n  \"search\": {\n    \"label\": \"खोजें\",\n    \"placeholder\": {\n      \"actions\": \"खोज क्रियाएँ...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"कॉपी सफलता पूर्ण हुआ!\",\n      \"fail\": \"कॉपी करने में असमर्थ\"\n    }\n  },\n  \"unSupportBlock\": \"वर्तमान संस्करण इस ब्लॉक का समर्थन नहीं करता है।\",\n  \"views\": {\n    \"deleteContentTitle\": \"क्या आप वाकई {pageType} को हटाना चाहते हैं?\",\n    \"deleteContentCaption\": \"यदि आप इस {pageType} को हटाते हैं, तो आप इसे ट्रैश से पुनर्स्थापित कर सकते हैं।\"\n  },\n  \"colors\": {\n    \"custom\": \"कस्टम\",\n    \"default\": \"डिफ़ॉल्ट\",\n    \"red\": \"लाल\",\n    \"orange\": \"नारंगी\",\n    \"yellow\": \"पीला\",\n    \"green\": \"हरा\",\n    \"blue\": \"नीला\",\n    \"purple\": \"बैंगनी\",\n    \"pink\": \"गुलाबी\",\n    \"brown\": \"भूरा\",\n    \"gray\": \"ग्रे\"\n  },\n  \"emoji\": {\n    \"filter\": \"फ़िल्टर\",\n    \"random\": \"रैंडम\",\n    \"selectSkinTone\": \"त्वचा का रंग चुनें\",\n    \"remove\": \"इमोजी हटाएं\",\n    \"categories\": {\n      \"smileys\": \"स्माइलीज़ एंड इमोशन\",\n      \"people\": \"लोग और शरीर\",\n      \"animals\": \"जानवर और प्रकृति\",\n      \"food\": \"खाद्य और पेय\",\n      \"activities\": \"गतिविधियाँ\",\n      \"places\": \"यात्रा एवं स्थान\",\n      \"objects\": \"ऑब्जेक्ट्स\",\n      \"symbols\": \"प्रतीक\",\n      \"flags\": \"झंडे\",\n      \"nature\": \"प्रकृति\",\n      \"frequentlyUsed\": \"अक्सर उपयोग किया जाता है\"\n    }\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/hu-HU.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Én\",\n  \"welcomeText\": \"Üdvözöl az @:appName\",\n  \"githubStarText\": \"GitHub csillagozás\",\n  \"subscribeNewsletterText\": \"Iratkozzon fel a hírlevelünkre\",\n  \"letsGoButtonText\": \"Vágjunk bele\",\n  \"title\": \"Cím\",\n  \"youCanAlso\": \"Te is\",\n  \"and\": \"és\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Kattintson a hozzáadáshoz lent\",\n    \"addAboveCmd\": \"Alt+kattintás\",\n    \"addAboveMacCmd\": \"Option+kattintás\",\n    \"addAboveTooltip\": \"a fentiek hozzáadásához\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Regisztráció\",\n    \"title\": \"Regisztrálj az @:appName -ra\",\n    \"getStartedText\": \"Kezdés\",\n    \"emptyPasswordError\": \"A jelszó nem lehet üres\",\n    \"repeatPasswordEmptyError\": \"A jelszó megerősítése nem lehet üres\",\n    \"unmatchedPasswordError\": \"A jelszavak nem egyeznek\",\n    \"alreadyHaveAnAccount\": \"Rendelkezik már fiókkal?\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Jelszó\",\n    \"repeatPasswordHint\": \"Jelszó megerősítése\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Bejelentkezés az @:appName -ba\",\n    \"loginButtonText\": \"Belépés\",\n    \"buttonText\": \"Bejelentkezés\",\n    \"forgotPassword\": \"Elfelejtett jelszó?\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Jelszó\",\n    \"dontHaveAnAccount\": \"Még nincs fiókod?\",\n    \"repeatPasswordEmptyError\": \"A jelszó megerősítése nem lehet üres\",\n    \"unmatchedPasswordError\": \"A jelszavak nem egyeznek\",\n    \"loginAsGuestButtonText\": \"Fogj neki\"\n  },\n  \"workspace\": {\n    \"create\": \"Új munkaterület létrehozása\",\n    \"hint\": \"munkaterület\",\n    \"notFoundError\": \"munkaterület nem található\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Megosztás\",\n    \"workInProgress\": \"Hamarosan érkezik...\",\n    \"markdown\": \"Markdown\",\n    \"copyLink\": \"Link másolása\"\n  },\n  \"moreAction\": {\n    \"small\": \"kicsi\",\n    \"medium\": \"közepes\",\n    \"large\": \"nagy\",\n    \"fontSize\": \"Betűméret\",\n    \"import\": \"Importálás\",\n    \"moreOptions\": \"Több lehetőség\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Szöveg & Markdown\",\n    \"documentFromV010\": \"Dokumentum a 0.1.0 verzióból\",\n    \"databaseFromV010\": \"Adatbázis a 0.1.0 verziótól\",\n    \"csv\": \"CSV\",\n    \"database\": \"Adatbázis\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Átnevezés\",\n    \"delete\": \"Törlés\",\n    \"duplicate\": \"Duplikálás\",\n    \"openNewTab\": \"Nyissa meg egy új lapon\"\n  },\n  \"blankPageTitle\": \"Üres oldal\",\n  \"newPageText\": \"Új oldal\",\n  \"newDocumentText\": \"Új dokumentum\",\n  \"newGridText\": \"Új táblázat\",\n  \"newCalendarText\": \"Új naptár\",\n  \"newBoardText\": \"Új feladat tábla\",\n  \"trash\": {\n    \"text\": \"Kuka\",\n    \"restoreAll\": \"Összes visszaállítása\",\n    \"deleteAll\": \"Összes törlése\",\n    \"pageHeader\": {\n      \"fileName\": \"Fájlnév\",\n      \"lastModified\": \"Utoljára módosítva\",\n      \"created\": \"Létrehozva\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Biztosan törli a kukában lévő összes oldalt?\",\n      \"caption\": \"Ez a művelet nem visszavonható.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Biztosan visszaállítja a kukában lévő összes oldalt?\",\n      \"caption\": \"Ez a művelet nem visszavonható.\"\n    }\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Ez az oldal a kukában van\",\n    \"restore\": \"Oldal visszaállítása\",\n    \"deletePermanent\": \"Végleges törlés\"\n  },\n  \"dialogCreatePageNameHint\": \"Oldalnév\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Parancsikonok\",\n    \"whatsNew\": \"Újdonságok\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"Debug Információ\",\n      \"success\": \"Debug információ a vágólapra másolva\",\n      \"fail\": \"A Debug információ nem másolható a vágólapra\"\n    },\n    \"feedback\": \"Visszacsatolás\",\n    \"help\": \"Segítség & Támogatás\"\n  },\n  \"menuAppHeader\": {\n    \"addPageTooltip\": \"Belső oldal hozzáadása\",\n    \"defaultNewPageName\": \"Névtelen\",\n    \"renameDialog\": \"Átnevezés\"\n  },\n  \"toolbar\": {\n    \"undo\": \"Vissza\",\n    \"redo\": \"Előre\",\n    \"bold\": \"Félkövér\",\n    \"italic\": \"Dőlt\",\n    \"underline\": \"Aláhúzott\",\n    \"strike\": \"Áthúzott\",\n    \"numList\": \"Számozott lista\",\n    \"bulletList\": \"Felsorolás\",\n    \"checkList\": \"Ellenőrző lista\",\n    \"inlineCode\": \"Inline kód\",\n    \"quote\": \"Idézet\",\n    \"header\": \"Címsor\",\n    \"highlight\": \"Kiemelés\",\n    \"color\": \"Szín\",\n    \"addLink\": \"Link hozzáadása\",\n    \"link\": \"Link\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Világos mód\",\n    \"darkMode\": \"Éjjeli mód\",\n    \"openAsPage\": \"Megnyitás oldalként\",\n    \"addNewRow\": \"Új sor hozzáadása\",\n    \"openMenu\": \"Kattintson a menü megnyitásához\",\n    \"dragRow\": \"Nyomja meg hosszan a sor átrendezéséhez\",\n    \"viewDataBase\": \"Adatbázis megtekintése\",\n    \"referencePage\": \"Erre a(z) {name} címre hivatkoznak\",\n    \"addBlockBelow\": \"Adjon hozzá egy blokkot alább\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Close sidebar\",\n    \"openSidebar\": \"Open sidebar\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Megjegyzés exportálva a Markdownba\",\n      \"path\": \"Dokumentumok/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Kontaktok\",\n    \"whatsHappening\": \"Heti újdonságok\",\n    \"addContact\": \"Új Kontakt\",\n    \"editContact\": \"Kontakt Szerkesztése\"\n  },\n  \"button\": {\n    \"ok\": \"OK\",\n    \"done\": \"Kész\",\n    \"cancel\": \"Mégse\",\n    \"signIn\": \"Bejelentkezés\",\n    \"signOut\": \"Kijelentkezés\",\n    \"complete\": \"Kész\",\n    \"save\": \"Mentés\",\n    \"generate\": \"generál\",\n    \"esc\": \"KILÉPÉS\",\n    \"keep\": \"Tart\",\n    \"tryAgain\": \"Próbáld újra\",\n    \"discard\": \"Eldobni\",\n    \"replace\": \"Cserélje ki\",\n    \"insertBelow\": \"Beszúrás alább\",\n    \"upload\": \"Feltöltés\",\n    \"edit\": \"Szerkesztés\",\n    \"delete\": \"Töröl\",\n    \"duplicate\": \"Másolat\",\n    \"putback\": \"Visszatesz\"\n  },\n  \"label\": {\n    \"welcome\": \"Üdvözlünk!\",\n    \"firstName\": \"Keresztnév\",\n    \"middleName\": \"Középső név\",\n    \"lastName\": \"Vezetéknév\",\n    \"stepX\": \"{X}. lépés\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Sikertelen bejelentkezés.\",\n      \"failedMsg\": \"Kérjük győződjön meg róla, hogy elvégezte a bejelentkezési folyamatot a böngészőben!\"\n    },\n    \"google\": {\n      \"title\": \"Bejelentkezés Google-lal\",\n      \"instruction1\": \"Ahhoz, hogy hozzáférjen a Google Kontaktjaihoz, kérjük hatalmazza fel ezt az alkalmazást a böngészőben.\",\n      \"instruction2\": \"Másolja ezt a kódot a vágólapra az ikonra kattintással vagy a szöveg kijelölésével:\",\n      \"instruction3\": \"Nyissa meg ezt a linket a böngészőben, és írja be a fenti kódot:\",\n      \"instruction4\": \"Nyomja meg az alábbi gombot, ha elvégezte a regisztrációt:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Beállítások\",\n    \"menu\": {\n      \"appearance\": \"Megjelenés\",\n      \"language\": \"Nyelv\",\n      \"user\": \"Felhasználó\",\n      \"files\": \"Fájlok\",\n      \"open\": \"Beállítások megnyitása\"\n    },\n    \"appearance\": {\n      \"fontFamily\": {\n        \"label\": \"Betűtípus család\",\n        \"search\": \"Keresés\"\n      },\n      \"themeMode\": {\n        \"label\": \"Téma\",\n        \"light\": \"Világos mód\",\n        \"dark\": \"Éjjeli mód\",\n        \"system\": \"Rendszerbeállítás követése\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Feltöltés\",\n        \"description\": \"Töltse fel saját @:appName témáját az alábbi gomb segítségével.\",\n        \"loading\": \"Kérjük, várjon, amíg ellenőrizzük és feltöltjük a témát...\",\n        \"uploadSuccess\": \"A témát sikeresen feltöltötte\",\n        \"deletionFailure\": \"Nem sikerült törölni a témát. Próbálja meg manuálisan törölni.\",\n        \"filePickerDialogTitle\": \"Válasszon egy .flowy_plugin fájlt\",\n        \"urlUploadFailure\": \"Nem sikerült megnyitni az URL-t: {}\",\n        \"failure\": \"A feltöltött téma formátuma érvénytelen.\"\n      },\n      \"theme\": \"Téma\",\n      \"builtInsLabel\": \"Beépített témák\",\n      \"pluginsLabel\": \"Beépülő modulok\"\n    },\n    \"files\": {\n      \"copy\": \"Másolat\",\n      \"defaultLocation\": \"Fájlok és adattárolási hely olvasása\",\n      \"exportData\": \"Exportálja adatait\",\n      \"doubleTapToCopy\": \"Koppintson duplán az útvonal másolásához\",\n      \"restoreLocation\": \"Visszaállítás az @:appName alapértelmezett elérési útjára\",\n      \"customizeLocation\": \"Nyisson meg egy másik mappát\",\n      \"restartApp\": \"Kérjük, indítsa újra az alkalmazást, hogy a változtatások életbe lépjenek.\",\n      \"exportDatabase\": \"Adatbázis exportálása\",\n      \"selectFiles\": \"Válassza ki az exportálandó fájlokat\",\n      \"selectAll\": \"Mindet kiválaszt\",\n      \"deselectAll\": \"Törölje az összes kijelölését\",\n      \"createNewFolder\": \"Hozzon létre egy új mappát\",\n      \"createNewFolderDesc\": \"Mondja el, hol szeretné tárolni adatait\",\n      \"defineWhereYourDataIsStored\": \"Határozza meg, hol tárolják adatait\",\n      \"open\": \"Nyisd ki\",\n      \"openFolder\": \"Nyisson meg egy meglévő mappát\",\n      \"openFolderDesc\": \"Olvassa el és írja be a meglévő @:appName mappájába\",\n      \"folderHintText\": \"mappa neve\",\n      \"location\": \"Új mappa létrehozása\",\n      \"locationDesc\": \"Válasszon nevet az @:appName adatmappájának\",\n      \"browser\": \"Tallózás\",\n      \"create\": \"Teremt\",\n      \"set\": \"Készlet\",\n      \"folderPath\": \"A mappa tárolásának elérési útja\",\n      \"locationCannotBeEmpty\": \"Az útvonal nem lehet üres\",\n      \"pathCopiedSnackbar\": \"A fájl tárolási útvonala a vágólapra másolva!\",\n      \"changeLocationTooltips\": \"Módosítsa az adatkönyvtárat\",\n      \"change\": \"változás\",\n      \"openLocationTooltips\": \"Nyisson meg egy másik adatkönyvtárat\",\n      \"openCurrentDataFolder\": \"Nyissa meg az aktuális adatkönyvtárat\",\n      \"recoverLocationTooltips\": \"Állítsa vissza az @:appName alapértelmezett adatkönyvtárát\",\n      \"exportFileSuccess\": \"A fájl exportálása sikeres volt!\",\n      \"exportFileFail\": \"A fájl exportálása nem sikerült!\",\n      \"export\": \"Export\"\n    },\n    \"user\": {\n      \"name\": \"Név\",\n      \"selectAnIcon\": \"Válasszon ki egy ikont\",\n      \"pleaseInputYourOpenAIKey\": \"kérjük, adja meg AI kulcsát\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Biztosan törli ezt a nézetet?\",\n    \"createView\": \"Új\",\n    \"settings\": {\n      \"filter\": \"Szűrő\",\n      \"sort\": \"Fajta\",\n      \"sortBy\": \"Rendezés\",\n      \"properties\": \"Tulajdonságok\",\n      \"reorderPropertiesTooltip\": \"Húzza a tulajdonságok átrendezéséhez\",\n      \"group\": \"Csoport\",\n      \"addFilter\": \"Szűrő hozzáadása\",\n      \"deleteFilter\": \"Szűrő törlése\",\n      \"filterBy\": \"Szűrés vlami alapján...\",\n      \"typeAValue\": \"Írjon be egy értéket...\",\n      \"layout\": \"Elrendezés\",\n      \"databaseLayout\": \"Elrendezés\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Tartalmaz\",\n      \"doesNotContain\": \"Nem tartalmaz\",\n      \"endsWith\": \"Végződik\",\n      \"startWith\": \"Ezzel kezdődik\",\n      \"is\": \"Is\",\n      \"isNot\": \"Nem\",\n      \"isEmpty\": \"Üres\",\n      \"isNotEmpty\": \"Nem üres\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Nem\",\n        \"startWith\": \"Ezzel kezdődik\",\n        \"endWith\": \"Végződik\",\n        \"isEmpty\": \"üres\",\n        \"isNotEmpty\": \"nem üres\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Ellenőrizve\",\n      \"isUnchecked\": \"Nincs bejelölve\",\n      \"choicechipPrefix\": {\n        \"is\": \"van\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"teljes\",\n      \"isIncomplted\": \"hiányos\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"Is\",\n      \"isNot\": \"Nem\",\n      \"contains\": \"Tartalmaz\",\n      \"doesNotContain\": \"Nem tartalmaz\",\n      \"isEmpty\": \"Üres\",\n      \"isNotEmpty\": \"Nem üres\"\n    },\n    \"field\": {\n      \"hide\": \"Elrejt\",\n      \"insertLeft\": \"Beszúrás balra\",\n      \"insertRight\": \"Jobb beszúrás\",\n      \"duplicate\": \"Másolat\",\n      \"delete\": \"Töröl\",\n      \"textFieldName\": \"Szöveg\",\n      \"checkboxFieldName\": \"Jelölőnégyzet\",\n      \"dateFieldName\": \"Dátum\",\n      \"updatedAtFieldName\": \"Utolsó módosítás időpontja\",\n      \"createdAtFieldName\": \"Létrehozott idő\",\n      \"numberFieldName\": \"Számok\",\n      \"singleSelectFieldName\": \"Válassza ki\",\n      \"multiSelectFieldName\": \"Többszörös választás\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Ellenőrzőlista\",\n      \"numberFormat\": \"Számformátum\",\n      \"dateFormat\": \"Dátum formátum\",\n      \"includeTime\": \"Tartalmazzon időt\",\n      \"dateFormatFriendly\": \"Hónap nap év\",\n      \"dateFormatISO\": \"Év hónap nap\",\n      \"dateFormatLocal\": \"Hónap nap év\",\n      \"dateFormatUS\": \"Év hónap nap\",\n      \"dateFormatDayMonthYear\": \"Nap hónap év\",\n      \"timeFormat\": \"Idő formátum\",\n      \"invalidTimeFormat\": \"Érvénytelen formátum\",\n      \"timeFormatTwelveHour\": \"12 óra\",\n      \"timeFormatTwentyFourHour\": \"24 óra\",\n      \"addSelectOption\": \"Adjon hozzá egy lehetőséget\",\n      \"optionTitle\": \"Lehetőségek\",\n      \"addOption\": \"Opció hozzáadása\",\n      \"editProperty\": \"Tulajdonság szerkesztése\",\n      \"newProperty\": \"Új tulajdonság\",\n      \"deleteFieldPromptMessage\": \"Biztos ebben? Ez a tulajdonság törlésre kerül\"\n    },\n    \"sort\": {\n      \"ascending\": \"Emelkedő\",\n      \"descending\": \"Csökkenő\",\n      \"addSort\": \"Rendezés hozzáadása\",\n      \"deleteSort\": \"Rendezés törlése\"\n    },\n    \"row\": {\n      \"duplicate\": \"Másolat\",\n      \"delete\": \"Töröl\",\n      \"textPlaceholder\": \"Üres\",\n      \"copyProperty\": \"Tulajdonság a vágólapra másolva\",\n      \"count\": \"Számol\",\n      \"newRow\": \"Új sor\",\n      \"action\": \"Akció\"\n    },\n    \"selectOption\": {\n      \"create\": \"Teremt\",\n      \"purpleColor\": \"Lila\",\n      \"pinkColor\": \"Rózsaszín\",\n      \"lightPinkColor\": \"Világos rózsaszín\",\n      \"orangeColor\": \"narancs\",\n      \"yellowColor\": \"Sárga\",\n      \"limeColor\": \"Mész\",\n      \"greenColor\": \"Zöld\",\n      \"aquaColor\": \"Aqua\",\n      \"blueColor\": \"Kék\",\n      \"deleteTag\": \"Címke törlése\",\n      \"colorPanelTitle\": \"Színek\",\n      \"panelTitle\": \"Válasszon egy lehetőséget, vagy hozzon létre egyet\",\n      \"searchOption\": \"Keressen egy lehetőséget\"\n    },\n    \"checklist\": {\n      \"addNew\": \"Adjon hozzá egy elemet\"\n    },\n    \"menuName\": \"Táblázat\",\n    \"referencedGridPrefix\": \"Nézet\"\n  },\n  \"document\": {\n    \"menuName\": \"Dokumentum\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Válassza ki azt a feladat táblát, amelyre hivatkozni szeretne\",\n        \"createANewBoard\": \"Hozzon létre egy új feladat táblát\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Válassza ki azt a táblázatot, amelyre hivatkozni szeretne\",\n        \"createANewGrid\": \"Hozzon létre egy új táblázatot\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Válasszon egy naptárt a hivatkozáshoz\",\n        \"createANewCalendar\": \"Hozzon létre egy új naptárat\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Vázlat\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Hivatkozott feladat tábla\",\n      \"referencedGrid\": \"Hivatkozott táblázat\",\n      \"referencedCalendar\": \"Hivatkozott naptár\",\n      \"autoGeneratorMenuItemName\": \"AI Writer\",\n      \"autoGeneratorTitleName\": \"AI: Kérd meg az AI-t, hogy írjon bármit...\",\n      \"autoGeneratorLearnMore\": \"Tudj meg többet\",\n      \"autoGeneratorGenerate\": \"generál\",\n      \"autoGeneratorHintText\": \"Kérdezd meg az AI-t...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Nem lehet beszerezni az AI kulcsot\",\n      \"autoGeneratorRewrite\": \"Újraírni\",\n      \"smartEdit\": \"AI asszisztensek\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"Helyesírás javítása\",\n      \"warning\": \"⚠️ Az AI-válaszok pontatlanok vagy félrevezetőek lehetnek.\",\n      \"smartEditSummarize\": \"Összesít\",\n      \"smartEditImproveWriting\": \"Az írás javítása\",\n      \"smartEditMakeLonger\": \"Hosszabb legyen\",\n      \"smartEditCouldNotFetchResult\": \"Nem sikerült lekérni az eredményt az AI-ból\",\n      \"smartEditCouldNotFetchKey\": \"Nem sikerült lekérni az AI kulcsot\",\n      \"smartEditDisabled\": \"Csatlakoztassa az AI-t a Beállításokban\",\n      \"discardResponse\": \"El szeretné vetni az AI-válaszokat?\",\n      \"createInlineMathEquation\": \"Hozzon létre egyenletet\",\n      \"toggleList\": \"Lista váltása\",\n      \"cover\": {\n        \"changeCover\": \"Borítót változatni\",\n        \"colors\": \"Színek\",\n        \"images\": \"Képek\",\n        \"clearAll\": \"Mindent kitöröl\",\n        \"abstract\": \"Absztrakt\",\n        \"addCover\": \"Fedő hozzáadása\",\n        \"addLocalImage\": \"Helyi kép hozzáadása\",\n        \"invalidImageUrl\": \"Érvénytelen kép URL-je\",\n        \"failedToAddImageToGallery\": \"Nem sikerült hozzáadni a képet a galériához\",\n        \"enterImageUrl\": \"Adja meg a kép URL-jét\",\n        \"add\": \"Hozzáadás\",\n        \"back\": \"Vissza\",\n        \"saveToGallery\": \"Mentés a galériába\",\n        \"removeIcon\": \"Ikon eltávolítása\",\n        \"pasteImageUrl\": \"Illessze be a kép URL-jét\",\n        \"or\": \"VAGY\",\n        \"pickFromFiles\": \"Válasszon fájlokból\",\n        \"couldNotFetchImage\": \"Nem sikerült lekérni a képet\",\n        \"imageSavingFailed\": \"A kép mentése nem sikerült\",\n        \"addIcon\": \"Ikon hozzáadása\",\n        \"coverRemoveAlert\": \"A törlés után eltávolítjuk a borítóról.\",\n        \"alertDialogConfirmation\": \"Biztos vagy benne, hogy folytatni akarod?\"\n      },\n      \"mathEquation\": {\n        \"addMathEquation\": \"Adja hozzá a matematikai egyenletet\",\n        \"editMathEquation\": \"Matematikai egyenlet szerkesztése\"\n      },\n      \"optionAction\": {\n        \"click\": \"Kattintson\",\n        \"toOpenMenu\": \" menü megnyitásához\",\n        \"delete\": \"Töröl\",\n        \"duplicate\": \"Másolat\",\n        \"turnInto\": \"Válik\",\n        \"moveUp\": \"Lépj felfelé\",\n        \"moveDown\": \"Mozgás lefelé\",\n        \"color\": \"Szín\",\n        \"align\": \"Igazítsa\",\n        \"left\": \"Bal\",\n        \"center\": \"Központ\",\n        \"right\": \"Jobb\",\n        \"defaultColor\": \"Alapértelmezett\"\n      },\n      \"image\": {\n        \"copiedToPasteBoard\": \"A kép linkjét a vágólapra másolta\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Adjon hozzá címeket a tartalomjegyzék létrehozásához.\"\n      }\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Írja be a '/' karaktert a parancsokhoz\"\n    },\n    \"title\": {\n      \"placeholder\": \"Névtelen\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Kattintson a kép hozzáadásához\",\n      \"upload\": {\n        \"label\": \"Feltöltés\",\n        \"placeholder\": \"Kattintson a kép feltöltéséhez\"\n      },\n      \"url\": {\n        \"label\": \"Kép URL-je\",\n        \"placeholder\": \"Adja meg a kép URL-jét\"\n      },\n      \"support\": \"A képméret korlátja 5 MB. Támogatott formátumok: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Érvénytelen kép\",\n        \"invalidImageSize\": \"A kép méretének 5 MB-nál kisebbnek kell lennie\",\n        \"invalidImageFormat\": \"A képformátum nem támogatott. Támogatott formátumok: JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"Érvénytelen kép URL-je\"\n      }\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Nyelv\",\n        \"placeholder\": \"Válasszon nyelvet\"\n      }\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Illesszen be vagy írjon be egy hivatkozást\",\n      \"url\": {\n        \"label\": \"Link URL\",\n        \"placeholder\": \"Adja meg a link URL-jét\"\n      },\n      \"title\": {\n        \"label\": \"Link címe\",\n        \"placeholder\": \"Adja meg a link címét\"\n      }\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"Új\"\n    },\n    \"menuName\": \"Feladat Tábla\",\n    \"referencedBoardPrefix\": \"Nézet\",\n    \"mobile\": {\n      \"showGroup\": \"Csoport megjelenítése\",\n      \"showGroupContent\": \"Biztos, hogy meg szeretnéd jeleníteni ezt a csoportot a táblán?\",\n      \"failedToLoad\": \"Nem sikerült betölteni a tábla nézetét\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Naptár\",\n    \"defaultNewCalendarTitle\": \"Névtelen\",\n    \"navigation\": {\n      \"today\": \"Ma\",\n      \"jumpToday\": \"Ugrás a mai napra\",\n      \"previousMonth\": \"Előző hónap\",\n      \"nextMonth\": \"Következő hónap\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Heti számok megjelenítése\",\n      \"showWeekends\": \"Hétvégén mutatják be\",\n      \"firstDayOfWeek\": \"Kezdje a hetet\",\n      \"layoutDateField\": \"Elrendezés naptár által\",\n      \"noDateTitle\": \"Nincs dátum\",\n      \"clickToAdd\": \"Kattintson a naptárhoz való hozzáadáshoz\",\n      \"name\": \"Naptár elrendezés\",\n      \"noDateHint\": \"A nem tervezett események itt jelennek meg\"\n    },\n    \"referencedCalendarPrefix\": \"Nézet\"\n  },\n  \"errorDialog\": {\n    \"title\": \"@:appName hiba\",\n    \"howToFixFallback\": \"Elnézést kérünk a kellemetlenségért! Nyújtsa be a problémát a GitHub-oldalunkon, amely leírja a hibát.\",\n    \"github\": \"Megtekintés a GitHubon\"\n  },\n  \"search\": {\n    \"label\": \"Keresés\",\n    \"placeholder\": {\n      \"actions\": \"Keresési műveletek...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Másolva!\",\n      \"fail\": \"Nem lehet másolni\"\n    }\n  },\n  \"unSupportBlock\": \"A jelenlegi verzió nem támogatja ezt a blokkot.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Biztosan törli a következőt: {pageType}?\",\n    \"deleteContentCaption\": \"ha törli ezt a {pageType} oldalt, visszaállíthatja a kukából.\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/id-ID.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Saya\",\n  \"welcomeText\": \"Selamat datang di @:appName\",\n  \"welcomeTo\": \"Selamat datang di\",\n  \"githubStarText\": \"Bintangi GitHub\",\n  \"subscribeNewsletterText\": \"Berlangganan buletin\",\n  \"letsGoButtonText\": \"Mulai\",\n  \"title\": \"Judul\",\n  \"youCanAlso\": \"Anda juga bisa\",\n  \"and\": \"Dan\",\n  \"failedToOpenUrl\": \"Gagal membuka url: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Klik untuk menambahkan di bawah\",\n    \"addAboveCmd\": \"Alt+klik\",\n    \"addAboveMacCmd\": \"Opsi+klik\",\n    \"addAboveTooltip\": \"menambahkan di atas\",\n    \"dragTooltip\": \"Seret untuk pindahkan\",\n    \"openMenuTooltip\": \"Klik untuk membuka menu\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Daftar\",\n    \"title\": \"Daftar ke @:appName\",\n    \"getStartedText\": \"Mulai\",\n    \"emptyPasswordError\": \"Kata sandi tidak boleh kosong\",\n    \"repeatPasswordEmptyError\": \"Mohon ulangi, sandi tidak boleh kosong\",\n    \"unmatchedPasswordError\": \"Kata sandi konfirmasi tidak sesuai dengan kata sandi awal\",\n    \"alreadyHaveAnAccount\": \"Sudah punya akun?\",\n    \"emailHint\": \"Surat elektronik\",\n    \"passwordHint\": \"Kata sandi\",\n    \"repeatPasswordHint\": \"Kata sandi ulang\",\n    \"signUpWith\": \"Daftar menggunakan:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Masuk ke @:appName\",\n    \"loginButtonText\": \"Masuk\",\n    \"loginStartWithAnonymous\": \"Mulai dengan sesi anonim\",\n    \"continueAnonymousUser\": \"Lanjutkan dengan sesi anonim\",\n    \"buttonText\": \"Masuk\",\n    \"signingInText\": \"Sedang masuk\",\n    \"forgotPassword\": \"Lupa kata sandi?\",\n    \"emailHint\": \"Surat elektronik\",\n    \"passwordHint\": \"Kata sandi\",\n    \"dontHaveAnAccount\": \"Belum punya akun?\",\n    \"createAccount\": \"Membuat akun\",\n    \"repeatPasswordEmptyError\": \"Sandi ulang tidak boleh kosong\",\n    \"unmatchedPasswordError\": \"Kata sandi konfirmasi tidak sesuai dengan kata sandi awal\",\n    \"syncPromptMessage\": \"Menyinkronkan data mungkin memerlukan waktu beberapa saat. Mohon jangan tutup halaman ini\",\n    \"or\": \"ATAU\",\n    \"signInWithGoogle\": \"Lanjutkan dengan Google\",\n    \"signInWithGithub\": \"Lanjutkan dengan GitHub\",\n    \"signInWithDiscord\": \"Lanjutkan dengan Discord\",\n    \"signInWithApple\": \"Lanjutkan dengan Apple\",\n    \"continueAnotherWay\": \"Lanjutkan dengan cara lain\",\n    \"signUpWithGoogle\": \"Mendaftar dengan Google\",\n    \"signUpWithGithub\": \"Mendaftar dengan GitHub\",\n    \"signUpWithDiscord\": \"Mendaftar dengan Discord\",\n    \"signInWith\": \"Masuk dengan:\",\n    \"signInWithEmail\": \"Lanjutkan dengan Email\",\n    \"signInWithMagicLink\": \"Lanjut\",\n    \"signUpWithMagicLink\": \"Mendaftar dengan Magic Link\",\n    \"pleaseInputYourEmail\": \"Masukkan alamat email Anda\",\n    \"settings\": \"Pengaturan\",\n    \"magicLinkSent\": \"Magic Link terkirim!\",\n    \"invalidEmail\": \"Masukkan alamat email yang valid\",\n    \"alreadyHaveAnAccount\": \"Sudah memiliki akun?\",\n    \"logIn\": \"Masuk\",\n    \"generalError\": \"Ada yang salah. Mohon coba kembali nanti\",\n    \"limitRateError\": \"Demi keamanan, Anda hanya dapat meminta magic link setiap 60 detik\",\n    \"magicLinkSentDescription\": \"Magic Link sudah terkirim ke email Anda. Klik pada tautan untuk menyelesaikan proses masuk Anda. Tautan akan kedaluwarsa setelah 5 menit.\",\n    \"anonymous\": \"Anonim\",\n    \"LogInWithGoogle\": \"Masuk dengan Google\",\n    \"LogInWithGithub\": \"Masuk dengan Github\",\n    \"LogInWithDiscord\": \"Masuk dengan Discord\",\n    \"loginAsGuestButtonText\": \"Memulai\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Pilih workspace anda\",\n    \"create\": \"Buat workspace\",\n    \"reset\": \"Mengatur ulang area kerja\",\n    \"resetWorkspacePrompt\": \"Mengatur ulang area kerja akan menghapus semua halaman dan data di dalamnya. Apakah anda yakin ingin Mengatur ulang area kerja? Selain itu, anda bisa menghubungi tim dukungan untuk mengembalikan area kerja\",\n    \"hint\": \"Area kerja\",\n    \"notFoundError\": \"Area kerja tidak ditemukan\",\n    \"failedToLoad\": \"Ada yang tidak beres! Gagal memuat area kerja. Coba tutup @:appName yang terbuka dan coba lagi.\",\n    \"errorActions\": {\n      \"reportIssue\": \"Melaporkan isu\",\n      \"reachOut\": \"Hubungi di Discord\"\n    }\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Bagikan\",\n    \"workInProgress\": \"Segera\",\n    \"markdown\": \"Markdown\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Salin tautan\"\n  },\n  \"moreAction\": {\n    \"small\": \"kecil\",\n    \"medium\": \"sedang\",\n    \"large\": \"besar\",\n    \"fontSize\": \"Ukuran huruf\",\n    \"import\": \"Impor\",\n    \"moreOptions\": \"Lebih banyak pilihan\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Teks & Markdown\",\n    \"documentFromV010\": \"Dokumen dari v0.1.0\",\n    \"databaseFromV010\": \"Basis data dari v0.1.0\",\n    \"csv\": \"CSV\",\n    \"database\": \"Basis data\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Ganti nama\",\n    \"delete\": \"Hapus\",\n    \"duplicate\": \"Duplikat\",\n    \"unfavorite\": \"Menghapus dari favorit\",\n    \"favorite\": \"Masukkan ke favorit\",\n    \"openNewTab\": \"Buka di tab baru\",\n    \"moveTo\": \"Pindahkan ke\",\n    \"addToFavorites\": \"Masukkan ke Favorit\",\n    \"copyLink\": \"Copy Link\"\n  },\n  \"blankPageTitle\": \"Halaman kosong\",\n  \"newPageText\": \"Halaman baru\",\n  \"newDocumentText\": \"Dokumen Baru\",\n  \"newGridText\": \"Grid baru\",\n  \"newCalendarText\": \"Kalendar baru\",\n  \"newBoardText\": \"Papan baru\",\n  \"trash\": {\n    \"text\": \"Tempat sampah\",\n    \"restoreAll\": \"Pulihkan Semua\",\n    \"deleteAll\": \"Hapus semua\",\n    \"pageHeader\": {\n      \"fileName\": \"Nama file\",\n      \"lastModified\": \"Terakhir diubah\",\n      \"created\": \"Dibuat\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Yakin ingin menghapus semua laman di Sampah?\",\n      \"caption\": \"Tindakan ini tidak bisa dibatalkan.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Apakah Anda yakin akan memulihkan semua laman di Sampah?\",\n      \"caption\": \"Tindakan ini tidak bisa dibatalkan.\"\n    },\n    \"mobile\": {\n      \"actions\": \"Tindakan Sampah\",\n      \"empty\": \"Tempat Sampah Kosong\",\n      \"emptyDescription\": \"Anda tidak memiliki file yang dihapus\",\n      \"isDeleted\": \"Telah dihapus\",\n      \"isRestored\": \"Telah dipulihkan\"\n    }\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Halaman ini di tempat sampah\",\n    \"restore\": \"Pulihkan halaman\",\n    \"deletePermanent\": \"Hapus secara permanen\"\n  },\n  \"dialogCreatePageNameHint\": \"Nama halaman\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Pintasan\",\n    \"whatsNew\": \"Apa yang baru?\",\n    \"markdown\": \"Penurunan harga\",\n    \"debug\": {\n      \"name\": \"Info debug\",\n      \"success\": \"Info debug disalin ke papan klip!\",\n      \"fail\": \"Tidak dapat menyalin info debug ke papan klip\"\n    },\n    \"feedback\": \"Masukan\",\n    \"help\": \"Bantuan & Dukungan\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Menghapus, merubah nama, dan banyak lagi...\",\n    \"addPageTooltip\": \"Menambahkan halaman di dalam dengan cepat\",\n    \"defaultNewPageName\": \"Tanpa Judul\",\n    \"renameDialog\": \"Ganti nama\"\n  },\n  \"noPagesInside\": \"Tidak ada halaman di dalamnya\",\n  \"toolbar\": {\n    \"undo\": \"Kembalikan ke sebelumnya\",\n    \"redo\": \"Kemblalikan ke setelahnya\",\n    \"bold\": \"Tebal\",\n    \"italic\": \"Miring\",\n    \"underline\": \"Garis bawah\",\n    \"strike\": \"Dicoret\",\n    \"numList\": \"Daftar bernomor\",\n    \"bulletList\": \"Daftar berpoin\",\n    \"checkList\": \"Daftar periksa\",\n    \"inlineCode\": \"Kode sebaris\",\n    \"quote\": \"Blok kutipan\",\n    \"header\": \"Tajuk\",\n    \"highlight\": \"Sorotan\",\n    \"color\": \"Warna\",\n    \"addLink\": \"Tambahkan Tautan\",\n    \"link\": \"Tautan\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Ganti mode terang\",\n    \"darkMode\": \"Ganti mode gelap\",\n    \"openAsPage\": \"Buka sebagai Halaman\",\n    \"addNewRow\": \"Tambahkan baris baru\",\n    \"openMenu\": \"Klik untuk membuka menu\",\n    \"dragRow\": \"Tekan lama untuk menyusun ulang baris\",\n    \"viewDataBase\": \"Lihat basis data\",\n    \"referencePage\": \"{nama} ini direferensikan\",\n    \"addBlockBelow\": \"Tambahkan blok di bawah ini\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Tutup sidebar\",\n    \"openSidebar\": \"Buka sidebar\",\n    \"personal\": \"Pribadi\",\n    \"favorites\": \"Favorit\",\n    \"clickToHidePersonal\": \"Klik untuk menutup Pribadi\",\n    \"clickToHideFavorites\": \"Klik untuk menutup Favorit\",\n    \"addAPage\": \"Tambah halaman baru\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Mengekspor Catatan ke Markdown\",\n      \"path\": \"Documents/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Kontak\",\n    \"whatsHappening\": \"Apa yang terjadi minggu ini?\",\n    \"addContact\": \"Tambahkan Kontak\",\n    \"editContact\": \"Ubah Kontak\"\n  },\n  \"button\": {\n    \"ok\": \"OKE\",\n    \"done\": \"Selesai\",\n    \"cancel\": \"Batal\",\n    \"signIn\": \"Masuk\",\n    \"signOut\": \"Keluar\",\n    \"complete\": \"Selesai\",\n    \"save\": \"Simpan\",\n    \"generate\": \"Menghasilkan\",\n    \"esc\": \"ESC\",\n    \"keep\": \"Menyimpan\",\n    \"tryAgain\": \"Coba lagi\",\n    \"discard\": \"Membuang\",\n    \"replace\": \"Mengganti\",\n    \"insertBelow\": \"Sisipkan Di Bawah\",\n    \"upload\": \"Mengunggah\",\n    \"edit\": \"Sunting\",\n    \"delete\": \"Menghapus\",\n    \"duplicate\": \"Duplikat\",\n    \"putback\": \"Taruh kembali\",\n    \"update\": \"Perbarui\",\n    \"share\": \"Bagikan\",\n    \"removeFromFavorites\": \"Hapus dari favorit\",\n    \"addToFavorites\": \"Tambahkan ke Favorit\",\n    \"rename\": \"Ganti nama\",\n    \"helpCenter\": \"Pusat Bantuan\"\n  },\n  \"label\": {\n    \"welcome\": \"Selamat datang!\",\n    \"firstName\": \"Nama Depan\",\n    \"middleName\": \"Nama Tengah\",\n    \"lastName\": \"Nama Akhir\",\n    \"stepX\": \"Langkah {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Tidak dapat terhubung ke akun anda\",\n      \"failedMsg\": \"Mohon pastikan anda menyelesaikan proses pendaftaran pada browser anda.\"\n    },\n    \"google\": {\n      \"title\": \"MASUK GOOGLE\",\n      \"instruction1\": \"Untuk mengimpor kontak Google Contacts anda, anda harus mengizinkan aplikasi ini menggunakan browser web anda.\",\n      \"instruction2\": \"Salin kode ini ke papan klip anda dengan cara mengklik ikon atau memilih teks:\",\n      \"instruction3\": \"Arahkan ke tautan berikut di browser web Anda, dan masukkan kode di atas:\",\n      \"instruction4\": \"Tekan tombol di bawah ini setelah Anda menyelesaikan pendaftaran:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Pengaturan\",\n    \"menu\": {\n      \"appearance\": \"Tampilan\",\n      \"language\": \"Bahasa\",\n      \"user\": \"Pengguna\",\n      \"files\": \"File\",\n      \"notifications\": \"Notifikasi\",\n      \"open\": \"Buka Pengaturan\",\n      \"logout\": \"Keluar\",\n      \"logoutPrompt\": \"Apakah anda yakin untuk keluar?\",\n      \"selfEncryptionLogoutPrompt\": \"Apakah anda yakin mau keluar? Dimohonkan anda telah mengcopy enkripsi rahasianya\",\n      \"syncSetting\": \"Sinkronisasi setting\",\n      \"enableSync\": \"Aktifkan sinkronisasi\",\n      \"enableEncrypt\": \"Enkripsi data\",\n      \"enableEncryptPrompt\": \"Aktifkan enkripsi untuk mensecure data anda dengan rahasia ini. Simpan dengan aman; stelah di aktifkan, tidak bisa di matikan lagi. Jika hilang, data anda akan tidak bisa terdapatkan lagi. Klik untuk mengcopy\",\n      \"inputEncryptPrompt\": \"Silahkan masukan enkripsi rahasia anda untuk\",\n      \"clickToCopySecret\": \"Klik untuk mengcopy rahasia\",\n      \"inputTextFieldHint\": \"Rahasia anda\",\n      \"historicalUserList\": \"History masuk user\",\n      \"historicalUserListTooltip\": \"Daftar ini menampilkan akun anonim Anda. Anda dapat mengeklik salah satu akun untuk melihat detailnya. Akun anonim dibuat dengan mengeklik tombol 'Memulai'\",\n      \"openHistoricalUser\": \"Klik untuk membuka akun anonim\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Mengaktifkan notifikasi\",\n        \"hint\": \"Matikan untuk menghentikan munculnya notifikasi lokal.\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"Mengatur ulang pengaturan ini\",\n      \"fontFamily\": {\n        \"label\": \"Jenis Font\",\n        \"search\": \"Cari\"\n      },\n      \"themeMode\": {\n        \"label\": \"Tema\",\n        \"light\": \"Terang\",\n        \"dark\": \"Gelap\",\n        \"system\": \"Sesuai Sistem\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Arah Tampilan\",\n        \"hint\": \"Mengatur arah tampilan konten, apakah dari kiri ke kanan atau kanan ke kiri.\",\n        \"ltr\": \"Kiri ke Kanan\",\n        \"rtl\": \"Kanan ke Kiri\"\n      },\n      \"textDirection\": {\n        \"label\": \"Arah Teks Bawaan\",\n        \"hint\": \"Atur arah teks bawaan, apakah dari kiri ke kanan atau kanan ke kiri.\",\n        \"ltr\": \"Kiri ke Kanan\",\n        \"rtl\": \"Kanan ke Kiri\",\n        \"auto\": \"Otomatis\",\n        \"fallback\": \"Sesuai Arah Tampilan\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Mengunggah\",\n        \"uploadTheme\": \"Unggah tema\",\n        \"description\": \"Unggah tema @:appName Anda sendiri menggunakan tombol di bawah ini.\",\n        \"loading\": \"Harap tunggu sementara kami memvalidasi dan mengunggah tema Anda...\",\n        \"uploadSuccess\": \"Tema Anda berhasil diunggah\",\n        \"deletionFailure\": \"Gagal menghapus tema. Cobalah untuk menghapusnya secara manual.\",\n        \"filePickerDialogTitle\": \"Pilih file .flowy_plugin\",\n        \"urlUploadFailure\": \"Gagal membuka url: {}\",\n        \"failure\": \"Tema yang diunggah memiliki format yang tidak valid.\"\n      },\n      \"theme\": \"Tema\",\n      \"builtInsLabel\": \"Tema Bawaan\",\n      \"pluginsLabel\": \"Plugin\",\n      \"dateFormat\": {\n        \"label\": \"Format tanggal\",\n        \"local\": \"Lokal\",\n        \"us\": \"US\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"Friendly\",\n        \"dmy\": \"D/M/Y\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Format Waktu\",\n        \"twelveHour\": \"Dua belas jam\",\n        \"twentyFourHour\": \"Dua puluh empat jam\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"Menampilkan dialog penamaan saat membuat halaman\"\n    },\n    \"files\": {\n      \"copy\": \"Menyalin\",\n      \"defaultLocation\": \"Baca file dan lokasi penyimpanan data\",\n      \"exportData\": \"Ekspor data Anda\",\n      \"doubleTapToCopy\": \"Ketuk dua kali untuk menyalin jalur\",\n      \"restoreLocation\": \"Pulihkan ke jalur default @:appName\",\n      \"customizeLocation\": \"Buka folder lain\",\n      \"restartApp\": \"Harap mulai ulang aplikasi agar perubahan diterapkan.\",\n      \"exportDatabase\": \"Ekspor basis data\",\n      \"selectFiles\": \"Pilih file yang perlu diekspor\",\n      \"selectAll\": \"Pilih Semua\",\n      \"deselectAll\": \"Batalkan pilihan semua\",\n      \"createNewFolder\": \"Buat folder baru\",\n      \"createNewFolderDesc\": \"Beritahu kami di mana Anda ingin menyimpan data Anda\",\n      \"defineWhereYourDataIsStored\": \"Tentukan di mana data Anda disimpan\",\n      \"open\": \"Membuka\",\n      \"openFolder\": \"Buka folder yang ada\",\n      \"openFolderDesc\": \"Baca dan tulis ke folder @:appName Anda yang sudah ada\",\n      \"folderHintText\": \"nama folder\",\n      \"location\": \"Membuat folder baru\",\n      \"locationDesc\": \"Pilih nama untuk folder data @:appName Anda\",\n      \"browser\": \"Jelajahi\",\n      \"create\": \"Membuat\",\n      \"set\": \"Mengatur\",\n      \"folderPath\": \"Jalur untuk menyimpan folder Anda\",\n      \"locationCannotBeEmpty\": \"Jalur tidak boleh kosong\",\n      \"pathCopiedSnackbar\": \"Jalur penyimpanan file disalin ke clipboard!\",\n      \"changeLocationTooltips\": \"Ubah direktori data\",\n      \"change\": \"Mengubah\",\n      \"openLocationTooltips\": \"Buka direktori data lain\",\n      \"openCurrentDataFolder\": \"Buka direktori data saat ini\",\n      \"recoverLocationTooltips\": \"Setel ulang ke direktori data default @:appName\",\n      \"exportFileSuccess\": \"Ekspor file berhasil!\",\n      \"exportFileFail\": \"File ekspor gagal!\",\n      \"export\": \"Ekspor\"\n    },\n    \"user\": {\n      \"name\": \"Nama\",\n      \"email\": \"Surel\",\n      \"tooltipSelectIcon\": \"Pilih ikon\",\n      \"selectAnIcon\": \"Pilih ikon\",\n      \"pleaseInputYourOpenAIKey\": \"silakan masukkan kunci AI Anda\",\n      \"clickToLogout\": \"Klik untuk keluar dari pengguna saat ini\",\n      \"pleaseInputYourStabilityAIKey\": \"Masukkan kunci Stability AI anda\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"Informasi pribadi\",\n      \"username\": \"Nama Pengguna\",\n      \"usernameEmptyError\": \"Nama pengguna tidak boleh kosong\",\n      \"about\": \"Tentang\",\n      \"pushNotifications\": \"Pemberitahuan Dorong\",\n      \"support\": \"Dukungan\",\n      \"joinDiscord\": \"Bergabunglah dengan kami di Discord\",\n      \"privacyPolicy\": \"Kebijakan Privasi\",\n      \"userAgreement\": \"Perjanjian Pengguna\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"Pintasan\",\n      \"command\": \"Perintah\",\n      \"keyBinding\": \"Keybindin\",\n      \"addNewCommand\": \"Tambah Perintah Baru\",\n      \"updateShortcutStep\": \"Tekan kombinasi tombol yang diinginkan dan tekan ENTER\",\n      \"shortcutIsAlreadyUsed\": \"Pintasan ini sudah digunakan untuk: {conflict}\",\n      \"resetToDefault\": \"Mengatur ulang ke keybinding default\",\n      \"couldNotLoadErrorMsg\": \"Tidak dapat memuat pintasan, Coba lagi\",\n      \"couldNotSaveErrorMsg\": \"Tidak dapat menyimpan pintasan, Coba lagi\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Yakin ingin menghapus tampilan ini?\",\n    \"createView\": \"Baru\",\n    \"title\": {\n      \"placeholder\": \"Tanpa judul\"\n    },\n    \"settings\": {\n      \"filter\": \"Filter\",\n      \"sort\": \"Menyortir\",\n      \"sortBy\": \"Sortir dengan\",\n      \"properties\": \"Properti\",\n      \"reorderPropertiesTooltip\": \"Seret untuk mengurutkan ulang properti\",\n      \"group\": \"Kelompok\",\n      \"addFilter\": \"Tambahkan Filter\",\n      \"deleteFilter\": \"Hapus filter\",\n      \"filterBy\": \"Saring menurut...\",\n      \"typeAValue\": \"Ketik nilai...\",\n      \"layout\": \"Tata letak\",\n      \"databaseLayout\": \"Tata letak\",\n      \"Properties\": \"Properti\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Mengandung\",\n      \"doesNotContain\": \"Tidak mengandung\",\n      \"endsWith\": \"Berakhir dengan\",\n      \"startWith\": \"Dimulai dengan\",\n      \"is\": \"Adalah\",\n      \"isNot\": \"Tidak\",\n      \"isEmpty\": \"Kosong\",\n      \"isNotEmpty\": \"Tidak kosong\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Bukan\",\n        \"startWith\": \"Dimulai dengan\",\n        \"endWith\": \"Berakhir dengan\",\n        \"isEmpty\": \"kosong\",\n        \"isNotEmpty\": \"tidak kosong\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Diperiksa\",\n      \"isUnchecked\": \"Tidak dicentang\",\n      \"choicechipPrefix\": {\n        \"is\": \"adalah\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"selesai\",\n      \"isIncomplted\": \"tidak lengkap\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"Adalah\",\n      \"isNot\": \"Tidak\",\n      \"contains\": \"Mengandung\",\n      \"doesNotContain\": \"Tidak mengandung\",\n      \"isEmpty\": \"Kosong\",\n      \"isNotEmpty\": \"Tidak kosong\"\n    },\n    \"field\": {\n      \"hide\": \"Sembunyikan\",\n      \"show\": \"Tampilkan\",\n      \"insertLeft\": \"Sisipkan Kiri\",\n      \"insertRight\": \"Sisipkan Kanan\",\n      \"duplicate\": \"Duplikasi\",\n      \"delete\": \"Hapus\",\n      \"textFieldName\": \"Teks\",\n      \"checkboxFieldName\": \"Kotak Centang\",\n      \"dateFieldName\": \"Tanggal\",\n      \"updatedAtFieldName\": \"Waktu terakhir diubah\",\n      \"createdAtFieldName\": \"Waktu yang diciptakan\",\n      \"numberFieldName\": \"Angka\",\n      \"singleSelectFieldName\": \"seleksi\",\n      \"multiSelectFieldName\": \"Multi seleksi\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Daftar periksa\",\n      \"numberFormat\": \"Format angka\",\n      \"dateFormat\": \"Format tanggal\",\n      \"includeTime\": \"Sertakan waktu\",\n      \"isRange\": \"Tanggal akhir\",\n      \"dateFormatFriendly\": \"Bulan Hari, Tahun\",\n      \"dateFormatISO\": \"Tahun-Bulan-Hari\",\n      \"dateFormatLocal\": \"Bulan/Hari/Tahun\",\n      \"dateFormatUS\": \"Tahun/Bulan/Hari\",\n      \"dateFormatDayMonthYear\": \"Hari bulan tahun\",\n      \"timeFormat\": \"Format waktu\",\n      \"invalidTimeFormat\": \"Format yang tidak valid\",\n      \"timeFormatTwelveHour\": \"12 jam\",\n      \"timeFormatTwentyFourHour\": \"24 jam\",\n      \"clearDate\": \"Hapus tanggal\",\n      \"addSelectOption\": \"Tambahkan opsi\",\n      \"optionTitle\": \"Opsi\",\n      \"addOption\": \"Tambahkan opsi\",\n      \"editProperty\": \"Ubah properti\",\n      \"newProperty\": \"Properti baru\",\n      \"deleteFieldPromptMessage\": \"Apa kamu yakin? Properti ini akan dihapus\",\n      \"newColumn\": \"Kolom baru\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Tambah bidang baru\",\n      \"fieldDragElementTooltip\": \"Klik untuk membuka menu\",\n      \"showHiddenFields\": {\n        \"one\": \"Tampilkan {} bidang tersembunyi\",\n        \"many\": \"Tampilkan {} bidang tersembunyi\",\n        \"other\": \"Tampilkan {} bidang tersembunyi\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"Sembunyikan {} bidang tersembunyi\",\n        \"many\": \"Sembunyikan {} bidang tersembunyi\",\n        \"other\": \"Sembunyikan {} bidang tersembunyi\"\n      }\n    },\n    \"sort\": {\n      \"ascending\": \"Naik\",\n      \"descending\": \"Menurun\",\n      \"deleteAllSorts\": \"Hapus semua sortir\",\n      \"addSort\": \"Tambahkan semacam\",\n      \"deleteSort\": \"Hapus urutan\"\n    },\n    \"row\": {\n      \"duplicate\": \"Duplikasi\",\n      \"delete\": \"Hapus\",\n      \"titlePlaceholder\": \"Tanpa judul\",\n      \"textPlaceholder\": \"Kosong\",\n      \"copyProperty\": \"Salin properti ke papan klip\",\n      \"count\": \"Menghitung\",\n      \"newRow\": \"Baris baru\",\n      \"action\": \"Tindakan\",\n      \"add\": \"Klik tambah ke bawah\",\n      \"drag\": \"Seret untuk pindah\"\n    },\n    \"selectOption\": {\n      \"create\": \"Buat\",\n      \"purpleColor\": \"Ungu\",\n      \"pinkColor\": \"Merah Jambu\",\n      \"lightPinkColor\": \"Merah Jambu Muda\",\n      \"orangeColor\": \"Oranye\",\n      \"yellowColor\": \"Kuning\",\n      \"limeColor\": \"Limau\",\n      \"greenColor\": \"Hijau\",\n      \"aquaColor\": \"Air\",\n      \"blueColor\": \"Biru\",\n      \"deleteTag\": \"Hapus tag\",\n      \"colorPanelTitle\": \"Warna\",\n      \"panelTitle\": \"Pilih opsi atau buat baru\",\n      \"searchOption\": \"Cari opsi\",\n      \"searchOrCreateOption\": \"Cari atau buat opsi\",\n      \"createNew\": \"Buat baru\",\n      \"orSelectOne\": \"atau pilih opsi\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"Deskripsi tugas\",\n      \"addNew\": \"Tambahkan item\",\n      \"submitNewTask\": \"Buat\",\n      \"hideComplete\": \"Sembunyikan tugas yang sudah selesai\",\n      \"showComplete\": \"Tampilkan semua tugas\"\n    },\n    \"menuName\": \"Grid\",\n    \"referencedGridPrefix\": \"Pemandangan dari\"\n  },\n  \"document\": {\n    \"menuName\": \"Dokter\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Pilih Papan untuk ditautkan\",\n        \"createANewBoard\": \"Buat Dewan baru\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Pilih Grid untuk ditautkan\",\n        \"createANewGrid\": \"Buat Kotak baru\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Pilih Kalender untuk ditautkan\",\n        \"createANewCalendar\": \"Buat Kalender baru\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Garis besar\",\n      \"codeBlock\": \"Blok Kode\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Papan Referensi\",\n      \"referencedGrid\": \"Kisi yang Direferensikan\",\n      \"referencedCalendar\": \"Kalender Referensi\",\n      \"autoGeneratorMenuItemName\": \"Penulis AI\",\n      \"autoGeneratorTitleName\": \"AI: Minta AI untuk menulis apa saja...\",\n      \"autoGeneratorLearnMore\": \"Belajarlah lagi\",\n      \"autoGeneratorGenerate\": \"Menghasilkan\",\n      \"autoGeneratorHintText\": \"Tanya AI...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Tidak bisa mendapatkan kunci AI\",\n      \"autoGeneratorRewrite\": \"Menulis kembali\",\n      \"smartEdit\": \"Asisten AI\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"Perbaiki ejaan\",\n      \"warning\": \"⚠️ Respons AI bisa jadi tidak akurat atau menyesatkan.\",\n      \"smartEditSummarize\": \"Meringkaskan\",\n      \"smartEditImproveWriting\": \"Perbaiki tulisan\",\n      \"smartEditMakeLonger\": \"Buat lebih lama\",\n      \"smartEditCouldNotFetchResult\": \"Tidak dapat mengambil hasil dari AI\",\n      \"smartEditCouldNotFetchKey\": \"Tidak dapat mengambil kunci AI\",\n      \"smartEditDisabled\": \"Hubungkan AI di Pengaturan\",\n      \"discardResponse\": \"Apakah Anda ingin membuang respons AI?\",\n      \"createInlineMathEquation\": \"Buat persamaan\",\n      \"toggleList\": \"Beralih Daftar\",\n      \"cover\": {\n        \"changeCover\": \"Ganti Sampul\",\n        \"colors\": \"Warna\",\n        \"images\": \"Gambar-gambar\",\n        \"clearAll\": \"Bersihkan semua\",\n        \"abstract\": \"Abstrak\",\n        \"addCover\": \"Tambahkan Penutup\",\n        \"addLocalImage\": \"Tambahkan gambar lokal\",\n        \"invalidImageUrl\": \"URL gambar tidak valid\",\n        \"failedToAddImageToGallery\": \"Gagal menambahkan gambar ke galeri\",\n        \"enterImageUrl\": \"Masukkan URL gambar\",\n        \"add\": \"Menambahkan\",\n        \"back\": \"Kembali\",\n        \"saveToGallery\": \"Simpan ke galeri\",\n        \"removeIcon\": \"Hapus Ikon\",\n        \"pasteImageUrl\": \"Tempel URL gambar\",\n        \"or\": \"ATAU\",\n        \"pickFromFiles\": \"Pilih dari file\",\n        \"couldNotFetchImage\": \"Tidak dapat mengambil gambar\",\n        \"imageSavingFailed\": \"Penyimpanan Gambar Gagal\",\n        \"addIcon\": \"Tambahkan Ikon\",\n        \"coverRemoveAlert\": \"Itu akan dihapus dari sampul setelah dihapus.\",\n        \"alertDialogConfirmation\": \"Apakah anda yakin ingin melanjutkan?\"\n      },\n      \"mathEquation\": {\n        \"addMathEquation\": \"Tambahkan Persamaan Matematika\",\n        \"editMathEquation\": \"Edit Persamaan Matematika\"\n      },\n      \"optionAction\": {\n        \"click\": \"Klik\",\n        \"toOpenMenu\": \" untuk membuka menu\",\n        \"delete\": \"Menghapus\",\n        \"duplicate\": \"Duplikat\",\n        \"turnInto\": \"Berubah menjadi\",\n        \"moveUp\": \"Naik\",\n        \"moveDown\": \"Turunkan\",\n        \"color\": \"Warna\",\n        \"align\": \"Meluruskan\",\n        \"left\": \"Kiri\",\n        \"center\": \"Tengah\",\n        \"right\": \"Benar\",\n        \"defaultColor\": \"Bawaan\"\n      },\n      \"image\": {\n        \"addAnImage\": \"Tambah gambar\",\n        \"copiedToPasteBoard\": \"Tautan gambar telah disalin ke papan klip\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Tambahkan judul untuk membuat daftar isi.\"\n      },\n      \"table\": {\n        \"addAfter\": \"Tambahkan setelah\",\n        \"addBefore\": \"Tambahkan sebelum\",\n        \"delete\": \"Hapus\",\n        \"clear\": \"Hapus konten\",\n        \"duplicate\": \"Duplikat\",\n        \"bgColor\": \"Warna latar belakang\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Salin\",\n        \"cut\": \"Potong\",\n        \"paste\": \"Tempel\"\n      }\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Ketik '/' untuk perintah\"\n    },\n    \"title\": {\n      \"placeholder\": \"Tanpa judul\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Klik untuk menambahkan gambar\",\n      \"upload\": {\n        \"label\": \"Mengunggah\",\n        \"placeholder\": \"Klik untuk mengunggah gambar\"\n      },\n      \"url\": {\n        \"label\": \"URL gambar\",\n        \"placeholder\": \"Masukkan URL gambar\"\n      },\n      \"ai\": {\n        \"label\": \"Buat gambar dari AI\",\n        \"placeholder\": \"Masukkan perintah agar AI menghasilkan gambar\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Buat gambar dari Stability AI\",\n        \"placeholder\": \"Masukkan perintah agar Stability AI menghasilkan gambar\"\n      },\n      \"support\": \"Batas ukuran gambar adalah 5 MB. Format yang didukung: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Gambar tidak valid\",\n        \"invalidImageSize\": \"Ukuran gambar harus kurang dari 5MB\",\n        \"invalidImageFormat\": \"Format gambar tidak didukung. Format yang didukung: JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"URL gambar tidak valid\"\n      },\n      \"embedLink\": {\n        \"label\": \"Sematkan tautan\",\n        \"placeholder\": \"Tempel atau ketik tautan gambar\"\n      },\n      \"searchForAnImage\": \"Mencari gambar\",\n      \"pleaseInputYourOpenAIKey\": \"masukkan kunci AI Anda di halaman Pengaturan\",\n      \"pleaseInputYourStabilityAIKey\": \"masukkan kunci AI Stabilitas Anda di halaman Pengaturan\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Bahasa\",\n        \"placeholder\": \"Pilih bahasa\"\n      }\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Tempel atau ketik tautan\",\n      \"openInNewTab\": \"Buka di tab baru\",\n      \"copyLink\": \"Salin tautan\",\n      \"removeLink\": \"Hapus tautan\",\n      \"url\": {\n        \"label\": \"URL tautan\",\n        \"placeholder\": \"Masukkan URL tautan\"\n      },\n      \"title\": {\n        \"label\": \"Judul Tautan\",\n        \"placeholder\": \"Masukkan judul tautan\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"Menyebutkan seseorang atau halaman atau tanggal...\",\n      \"page\": {\n        \"label\": \"Tautan ke halaman\",\n        \"tooltip\": \"Klik untuk membuka halaman\"\n      }\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"Mengatur ulang ke default\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"Versi saat ini tidak mendukung blok ini.\",\n      \"blockContentHasBeenCopied\": \"Konten blok telah disalin.\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"Baru\",\n      \"renameGroupTooltip\": \"Tekan untuk mengganti nama grup\"\n    },\n    \"menuName\": \"Papan\",\n    \"showUngrouped\": \"Tampilkan item yang tidak dikelompokkan\",\n    \"ungroupedButtonText\": \"Tidak dikelompokkan\",\n    \"ungroupedButtonTooltip\": \"Berisi kartu yang tidak termasuk dalam grup mana pun\",\n    \"ungroupedItemsTitle\": \"Klik untuk menambahkan ke papan\",\n    \"groupBy\": \"Kelompokkan berdasarkan\",\n    \"referencedBoardPrefix\": \"Pemandangan dari\",\n    \"mobile\": {\n      \"showGroup\": \"Tampilkan grup\",\n      \"showGroupContent\": \"Apakah Anda yakin ingin menampilkan grup ini di papan?\",\n      \"failedToLoad\": \"Gagal memuat tampilan papan\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Kalender\",\n    \"defaultNewCalendarTitle\": \"Tanpa judul\",\n    \"newEventButtonTooltip\": \"Menambahkan acara baru\",\n    \"navigation\": {\n      \"today\": \"Hari ini\",\n      \"jumpToday\": \"Lompat ke Hari Ini\",\n      \"previousMonth\": \"Bulan sebelumnya\",\n      \"nextMonth\": \"Bulan depan\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Tampilkan nomor minggu\",\n      \"showWeekends\": \"Tampilkan akhir pekan\",\n      \"firstDayOfWeek\": \"Mulai minggu\",\n      \"layoutDateField\": \"Tata letak kalender oleh\",\n      \"noDateTitle\": \"Tidak ada tanggal\",\n      \"clickToAdd\": \"Klik untuk menambahkan ke kalender\",\n      \"name\": \"Tata letak kalender\",\n      \"noDateHint\": \"Acara yang tidak dijadwalkan akan muncul di sini\"\n    },\n    \"referencedCalendarPrefix\": \"Pemandangan dari\"\n  },\n  \"errorDialog\": {\n    \"title\": \"Kesalahan @:appName\",\n    \"howToFixFallback\": \"Kami mohon maaf atas ketidaknyamanan ini! Kirimkan masalah di halaman GitHub kami yang menjelaskan kesalahan Anda.\",\n    \"github\": \"Lihat di GitHub\"\n  },\n  \"search\": {\n    \"label\": \"Mencari\",\n    \"placeholder\": {\n      \"actions\": \"Tindakan penelusuran...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Disalin!\",\n      \"fail\": \"Tidak dapat menyalin\"\n    }\n  },\n  \"unSupportBlock\": \"Versi saat ini tidak mendukung Blok ini.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Anda yakin ingin menghapus {pageType}?\",\n    \"deleteContentCaption\": \"jika Anda menghapus {pageType} ini, Anda dapat memulihkannya dari sampah.\"\n  },\n  \"colors\": {\n    \"custom\": \"Kustom\",\n    \"default\": \"Default\",\n    \"red\": \"Merah\",\n    \"orange\": \"Oranye\",\n    \"yellow\": \"Kuning\",\n    \"green\": \"Hijau\",\n    \"blue\": \"Biru\",\n    \"purple\": \"Ungu\",\n    \"pink\": \"Merah Muda\",\n    \"brown\": \"Coklat\",\n    \"gray\": \"Abu-abu\"\n  },\n  \"emoji\": {\n    \"search\": \"Cari emoji\",\n    \"noRecent\": \"Tidak ada emoji terbaru\",\n    \"noEmojiFound\": \"Emoji tidak ditemukan\",\n    \"filter\": \"Saring\",\n    \"random\": \"Acak\",\n    \"selectSkinTone\": \"Pilih warna kulit\",\n    \"remove\": \"Hapus emoji\",\n    \"categories\": {\n      \"smileys\": \"Senyum & Emosi\",\n      \"people\": \"Orang & Tubuh\",\n      \"animals\": \"Hewan & Alam\",\n      \"food\": \"Makanan & Minuman\",\n      \"activities\": \"Aktivitas\",\n      \"places\": \"Travel & Tempat\",\n      \"objects\": \"Objek\",\n      \"symbols\": \"Simbol\",\n      \"flags\": \"Bendera\",\n      \"nature\": \"Alam\",\n      \"frequentlyUsed\": \"Sering Digunakan\"\n    }\n  },\n  \"inlineActions\": {\n    \"noResults\": \"Tidak ada hasil\",\n    \"pageReference\": \"Referensi halaman\",\n    \"date\": \"Tanggal\",\n    \"reminder\": {\n      \"groupTitle\": \"Pengingat\",\n      \"shortKeyword\": \"mengingatkan\"\n    }\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"Ubah format tanggal dan waktu di pengaturan\"\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"Kemarin\",\n    \"today\": \"Hari ini\",\n    \"tomorrow\": \"Besok\",\n    \"oneWeek\": \"1 minggu\"\n  },\n  \"notificationHub\": {\n    \"title\": \"Notifikasi\",\n    \"emptyTitle\": \"Semua sudah ketahuan!\",\n    \"emptyBody\": \"Tidak ada pemberitahuan atau tindakan yang tertunda. Nikmati ketenangannya.\",\n    \"tabs\": {\n      \"inbox\": \"Kotak masuk\",\n      \"upcoming\": \"Mendatang\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"tandai semua telah dibaca\",\n      \"showAll\": \"Semua\",\n      \"showUnreads\": \"Belum dibaca\"\n    },\n    \"filters\": {\n      \"ascending\": \"Ascending\",\n      \"descending\": \"Descending\",\n      \"groupByDate\": \"Kelompokkan berdasarkan tanggal\",\n      \"showUnreadsOnly\": \"Tampilkan yang belum dibaca saja\",\n      \"resetToDefault\": \"Mengatur ulang ke default\"\n    },\n    \"empty\": \"Tidak ada yang dilihat disini!\"\n  },\n  \"reminderNotification\": {\n    \"title\": \"Pengingat\",\n    \"message\": \"Ingatlah untuk memeriksa ini sebelum Anda lupa!\",\n    \"tooltipDelete\": \"Hapus\",\n    \"tooltipMarkRead\": \"Tandai telah dibaca\",\n    \"tooltipMarkUnread\": \"Tandai belum dibaca\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"Cari\",\n    \"previousMatch\": \"Pencarian sebelumnya\",\n    \"nextMatch\": \"Pencarian setelahnya\",\n    \"close\": \"Tutup\",\n    \"replace\": \"Timpa\",\n    \"replaceAll\": \"Timpa semua\",\n    \"noResult\": \"Tidak ada hasil\",\n    \"caseSensitive\": \"Case sensitive\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"Kami meminta maaf\",\n    \"loadingViewError\": \"Kami mengalami masalah saat memuat tampilan ini. Silakan periksa koneksi internet Anda, segarkan aplikasi, dan jangan ragu untuk menghubungi tim jika masalah terus berlanjut.\"\n  },\n  \"editor\": {\n    \"bold\": \"Tebal\",\n    \"bulletedList\": \"Daftar Berpoin\",\n    \"checkbox\": \"Kotak centang\",\n    \"embedCode\": \"Sematkan Kode\",\n    \"heading1\": \"H1\",\n    \"heading2\": \"H2\",\n    \"heading3\": \"H3\",\n    \"highlight\": \"Sorotan\",\n    \"color\": \"Warna\",\n    \"image\": \"Gambar\",\n    \"italic\": \"Miring\",\n    \"link\": \"Tautan\",\n    \"numberedList\": \"Daftar Bernomor\",\n    \"quote\": \"Kutipan\",\n    \"strikethrough\": \"Dicoret\",\n    \"text\": \"Teks\",\n    \"underline\": \"Garis Bawah\",\n    \"fontColorDefault\": \"Bawaan\",\n    \"fontColorGray\": \"Abu-abu\",\n    \"fontColorBrown\": \"Cokelat\",\n    \"fontColorOrange\": \"Oranye\",\n    \"fontColorYellow\": \"Kuning\",\n    \"fontColorGreen\": \"Hijau\",\n    \"fontColorBlue\": \"Biru\",\n    \"fontColorPurple\": \"Ungu\",\n    \"fontColorPink\": \"Merah Jambu\",\n    \"fontColorRed\": \"Merah\",\n    \"backgroundColorDefault\": \"Latar belakang bawaan\",\n    \"backgroundColorGray\": \"Latar belakang abu-abu\",\n    \"backgroundColorBrown\": \"Latar belakang coklat\",\n    \"backgroundColorOrange\": \"Latar belakang oranye\",\n    \"backgroundColorYellow\": \"Latar belakang kuning\",\n    \"backgroundColorGreen\": \"Latar belakang hijau\",\n    \"backgroundColorBlue\": \"Latar belakang biru\",\n    \"backgroundColorPurple\": \"Latar belakang ungu\",\n    \"backgroundColorPink\": \"Latar belakang merah muda\",\n    \"backgroundColorRed\": \"Latar belakang merah\",\n    \"done\": \"Selesai\",\n    \"cancel\": \"Batalkan\",\n    \"tint1\": \"Warna 1\",\n    \"tint2\": \"Warna 2\",\n    \"tint3\": \"Warna 3\",\n    \"tint4\": \"Warna 4\",\n    \"tint5\": \"Warna 5\",\n    \"tint6\": \"Warna 6\",\n    \"tint7\": \"Warna 7\",\n    \"tint8\": \"Warna 8\",\n    \"tint9\": \"Warna 9\",\n    \"lightLightTint1\": \"Ungu\",\n    \"lightLightTint2\": \"Merah Jambu\",\n    \"lightLightTint3\": \"Merah Jambu Muda\",\n    \"lightLightTint4\": \"Oranye\",\n    \"lightLightTint5\": \"Kuning\",\n    \"lightLightTint6\": \"Hijau Jeruk Nipis\",\n    \"lightLightTint7\": \"Hijau\",\n    \"lightLightTint8\": \"Biru Air\",\n    \"lightLightTint9\": \"Biru\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"Judul 1\",\n    \"mobileHeading2\": \"Judul 2\",\n    \"mobileHeading3\": \"Judul 3\",\n    \"textColor\": \"Warna Teks\",\n    \"backgroundColor\": \"Warna Latar Belakang\",\n    \"addYourLink\": \"Tambahkan tautan Anda\",\n    \"openLink\": \"Buka tautan\",\n    \"copyLink\": \"Salin tautan\",\n    \"removeLink\": \"Hapus tautan\",\n    \"editLink\": \"Sunting tautan\",\n    \"linkText\": \"Teks\",\n    \"linkTextHint\": \"Silakan masukkan teks\",\n    \"linkAddressHint\": \"Silakan masukkan URL\",\n    \"highlightColor\": \"Sorot warna\",\n    \"clearHighlightColor\": \"Hapus warna sorotan\",\n    \"customColor\": \"Warna khusus\",\n    \"hexValue\": \"Nilai hex\",\n    \"opacity\": \"Kegelapan\",\n    \"resetToDefaultColor\": \"Atur ulang ke warna default\",\n    \"ltr\": \"LTR\",\n    \"rtl\": \"RTL\",\n    \"auto\": \"Otomatis\",\n    \"cut\": \"Potong\",\n    \"copy\": \"Salin\",\n    \"paste\": \"Tempel\",\n    \"find\": \"Temukan\",\n    \"previousMatch\": \"Padanan sebelumnya\",\n    \"nextMatch\": \"Padanan selanjutnya\",\n    \"closeFind\": \"Tutup\",\n    \"replace\": \"Ganti\",\n    \"replaceAll\": \"Ganti semua\",\n    \"regex\": \"Regex\",\n    \"caseSensitive\": \"Huruf besar kecil dibedakan\",\n    \"uploadImage\": \"Unggah Gambar\",\n    \"urlImage\": \"Gambar URL\",\n    \"incorrectLink\": \"Tautan Salah\",\n    \"upload\": \"Unggah\",\n    \"chooseImage\": \"Pilih gambar\",\n    \"loading\": \"Memuat\",\n    \"imageLoadFailed\": \"Tidak dapat memuat gambar\",\n    \"divider\": \"Pembagi\",\n    \"table\": \"Tabel\",\n    \"colAddBefore\": \"Tambahkan sebelumnya\",\n    \"rowAddBefore\": \"Tambahkan sebelumnya\",\n    \"colAddAfter\": \"Tambahkan setelahnya\",\n    \"rowAddAfter\": \"Tambahkan setelahnya\",\n    \"colRemove\": \"Hapus\",\n    \"rowRemove\": \"Hapus\",\n    \"colDuplicate\": \"Duplikat\",\n    \"rowDuplicate\": \"Duplikat\",\n    \"colClear\": \"Hapus Konten\",\n    \"rowClear\": \"Hapus Konten\",\n    \"slashPlaceHolder\": \"Masukkan / untuk menyisipkan blok, atau mulai mengetik\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"Tidak ada halaman favorit\",\n    \"noFavoriteHintText\": \"Geser halaman ke kiri untuk menambahkannya ke favorit Anda\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/it-IT.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Me\",\n  \"welcomeText\": \"Benvenuto in @:appName\",\n  \"welcomeTo\": \"Benvenuto a\",\n  \"githubStarText\": \"Vota su GitHub\",\n  \"subscribeNewsletterText\": \"Sottoscrivi la Newsletter\",\n  \"letsGoButtonText\": \"Andiamo\",\n  \"title\": \"Titolo\",\n  \"youCanAlso\": \"Puoi anche\",\n  \"and\": \"E\",\n  \"failedToOpenUrl\": \"Apertura URL fallita: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Fare clic per aggiungere di seguito\",\n    \"addAboveCmd\": \"Alt+clic\",\n    \"addAboveMacCmd\": \"Opzione+clic\",\n    \"addAboveTooltip\": \"aggiungere sopra\",\n    \"dragTooltip\": \"Trascina per spostare\",\n    \"openMenuTooltip\": \"Cliccare per aprire il menu\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Registrati\",\n    \"title\": \"Registrati per @:appName\",\n    \"getStartedText\": \"Iniziamo\",\n    \"emptyPasswordError\": \"La password non può essere vuota\",\n    \"repeatPasswordEmptyError\": \"La password ripetuta non può essere vuota\",\n    \"unmatchedPasswordError\": \"La password ripetuta non è uguale alla password\",\n    \"alreadyHaveAnAccount\": \"Hai già un account?\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Password\",\n    \"repeatPasswordHint\": \"Ripeti password\",\n    \"signUpWith\": \"Registrati con:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Accedi a @:appName\",\n    \"loginButtonText\": \"Login\",\n    \"loginStartWithAnonymous\": \"Inizia con una sessione anonima\",\n    \"continueAnonymousUser\": \"Continua con una sessione anonima\",\n    \"continueWithLocalModel\": \"Continua con il modello locale\",\n    \"switchToAppFlowyCloud\": \"AppFlowy Cloud\",\n    \"anonymousMode\": \"Modalità anonima\",\n    \"buttonText\": \"Accedi\",\n    \"signingInText\": \"Accesso in corso...\",\n    \"forgotPassword\": \"Password Dimentica?\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Password\",\n    \"dontHaveAnAccount\": \"Non hai un account?\",\n    \"createAccount\": \"Crea account\",\n    \"repeatPasswordEmptyError\": \"La password ripetuta non può essere vuota\",\n    \"unmatchedPasswordError\": \"La password ripetuta non è uguale alla password\",\n    \"passwordMustContain\": \"La password deve contenere almeno una lettera, un numero e un simbolo.\",\n    \"syncPromptMessage\": \"La sincronizzazione dei dati potrebbe richiedere del tempo. Per favore, non chiudere questa pagina\",\n    \"or\": \"O\",\n    \"signInWithGoogle\": \"Continua con Google\",\n    \"signInWithGithub\": \"Continua con Github\",\n    \"signInWithDiscord\": \"Continua con Discord\",\n    \"signInWithApple\": \"Continua con Apple\",\n    \"continueAnotherWay\": \"Continua in un altro modo\",\n    \"signUpWithGoogle\": \"Registrati con Google\",\n    \"signUpWithGithub\": \"Registrati con Github\",\n    \"signUpWithDiscord\": \"Registrati con Discord\",\n    \"signInWith\": \"Loggati con:\",\n    \"signInWithEmail\": \"Continua con Email\",\n    \"signInWithMagicLink\": \"Continua\",\n    \"signUpWithMagicLink\": \"Registrati con un Link Magico\",\n    \"pleaseInputYourEmail\": \"Per favore, inserisci il tuo indirizzo email\",\n    \"settings\": \"Impostazioni\",\n    \"magicLinkSent\": \"Link Magico inviato!\",\n    \"invalidEmail\": \"Per favore, inserisci un indirizzo email valido\",\n    \"alreadyHaveAnAccount\": \"Hai già un account?\",\n    \"logIn\": \"Accedi\",\n    \"generalError\": \"Qualcosa è andato storto. Per favore, riprova più tardi\",\n    \"limitRateError\": \"Per ragioni di sicurezza, puoi richiedere un link magico ogni 60 secondi\",\n    \"magicLinkSentDescription\": \"Un Link Magico è stato inviato alla tua email. Clicca il link per completare il tuo accesso. Il link scadrà dopo 5 minuti.\",\n    \"tokenHasExpiredOrInvalid\": \"Il codice è scaduto o non è valido. Riprova.\",\n    \"signingIn\": \"Accesso in corso...\",\n    \"checkYourEmail\": \"Controlla la tua posta elettronica\",\n    \"temporaryVerificationLinkSent\": \"È stato inviato un link di verifica temporaneo.\\nSi prega di controllare la posta in arrivo di\",\n    \"temporaryVerificationCodeSent\": \"È stato inviato un codice di verifica temporaneo.\\nSi prega di controllare la posta in arrivo di\",\n    \"continueToSignIn\": \"Continua ad accedere\",\n    \"continueWithLoginCode\": \"Continua con il codice di accesso\",\n    \"backToLogin\": \"Torna al login\",\n    \"enterCode\": \"Inserisci il codice\",\n    \"enterCodeManually\": \"Inserisci il codice manualmente\",\n    \"continueWithEmail\": \"Continua con l'email\",\n    \"enterPassword\": \"Inserisci la password\",\n    \"loginAs\": \"Accedi come\",\n    \"invalidVerificationCode\": \"Inserisci un codice di verifica valido\",\n    \"tooFrequentVerificationCodeRequest\": \"Hai effettuato troppe richieste. Riprova più tardi.\",\n    \"invalidLoginCredentials\": \"La tua password non è corretta, riprova\",\n    \"resetPassword\": \"Reimposta password\",\n    \"resetPasswordDescription\": \"Inserisci la tua email per reimpostare la password\",\n    \"continueToResetPassword\": \"Continua per reimpostare la password\",\n    \"resetPasswordSuccess\": \"Reimpostazione password riuscita\",\n    \"resetPasswordFailed\": \"Impossibile reimpostare la password\",\n    \"resetPasswordLinkSent\": \"Un link per reimpostare la password è stato inviato alla tua email. Controlla la tua casella di posta all'indirizzo\",\n    \"resetPasswordLinkExpired\": \"Il link per reimpostare la password è scaduto. Richiedi un nuovo link.\",\n    \"resetPasswordLinkInvalid\": \"Il link per reimpostare la password non è valido. Richiedi un nuovo link.\",\n    \"enterNewPasswordFor\": \"Inserisci la nuova password per \",\n    \"newPassword\": \"Nuova password\",\n    \"enterNewPassword\": \"Inserisci la nuova password\",\n    \"confirmPassword\": \"Conferma password\",\n    \"confirmNewPassword\": \"Inserisci la nuova password\",\n    \"newPasswordCannotBeEmpty\": \"La nuova password non può essere vuota\",\n    \"confirmPasswordCannotBeEmpty\": \"La password di conferma non può essere vuota\",\n    \"passwordsDoNotMatch\": \"Le password non corrispondono\",\n    \"verifying\": \"Verifica in corso...\",\n    \"continueWithPassword\": \"Continua con la password\",\n    \"youAreInLocalMode\": \"Sei in modalità locale.\",\n    \"loginToAppFlowyCloud\": \"Accedi ad AppFlowy Cloud\",\n    \"LogInWithGoogle\": \"Accedi con Google\",\n    \"LogInWithGithub\": \"Accedi con Github\",\n    \"LogInWithDiscord\": \"Accedi con Discord\",\n    \"loginAsGuestButtonText\": \"Iniziare\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Scegli il tuo spazio di lavoro\",\n    \"defaultName\": \"Il mio spazio di lavoro\",\n    \"create\": \"Crea spazio di lavoro\",\n    \"new\": \"Nuovo spazio di lavoro\",\n    \"importFromNotion\": \"Importa da Notion\",\n    \"learnMore\": \"Per saperne di più\",\n    \"reset\": \"Ripristina lo spazio di lavoro\",\n    \"renameWorkspace\": \"Rinomina workspace\",\n    \"workspaceNameCannotBeEmpty\": \"Il nome dell'area di lavoro non può essere vuoto\",\n    \"resetWorkspacePrompt\": \"Il ripristino dello spazio di lavoro eliminerà tutte le pagine e i dati al suo interno. Sei sicuro di voler ripristinare lo spazio di lavoro? In alternativa, puoi contattare il team di supporto per ristabilire lo spazio di lavoro\",\n    \"hint\": \"spazio di lavoro\",\n    \"notFoundError\": \"Spazio di lavoro non trovato\",\n    \"failedToLoad\": \"Qualcosa è andato storto! Impossibile caricare lo spazio di lavoro. Prova a chiudere qualsiasi istanza aperta di @:appName e riprova.\",\n    \"errorActions\": {\n      \"reportIssue\": \"Segnala un problema\",\n      \"reportIssueOnGithub\": \"Segnalate un problema su Github\",\n      \"exportLogFiles\": \"Esporta i file di log\",\n      \"reachOut\": \"Contattaci su Discord\"\n    },\n    \"menuTitle\": \"Spazi di lavoro\",\n    \"deleteWorkspaceHintText\": \"Sei sicuro di voler cancellare la workspace? Questa azione non è reversibile, e ogni pagina che hai pubblicato sarà rimossa.\",\n    \"createSuccess\": \"Workspace creata con successo\",\n    \"createFailed\": \"Creazione workspace fallita\",\n    \"createLimitExceeded\": \"Hai raggiunto il numero massimo di workspace permesse per il tuo account. Se hai bisogno di ulteriori workspace per continuare il tuo lavoro, per favore fai richiesta su Github\",\n    \"deleteSuccess\": \"Workspace cancellata con successo\",\n    \"deleteFailed\": \"Cancellazione workspace fallita\",\n    \"openSuccess\": \"Workspace aperta con successo\",\n    \"openFailed\": \"Apertura workspace fallita\",\n    \"renameSuccess\": \"Workspace rinominata con successo\",\n    \"renameFailed\": \"Rinomina workspace fallita\",\n    \"updateIconSuccess\": \"Icona della workspace aggiornata con successo\",\n    \"updateIconFailed\": \"Aggiornamento icona della workspace fallito\",\n    \"cannotDeleteTheOnlyWorkspace\": \"Impossibile cancellare l'unica workspace\",\n    \"fetchWorkspacesFailed\": \"Recupero workspaces fallito\",\n    \"leaveCurrentWorkspace\": \"Lascia workspace\",\n    \"leaveCurrentWorkspacePrompt\": \"Sei sicuro di voler lasciare la workspace corrente?\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Condividi\",\n    \"workInProgress\": \"Prossimamente\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"Copia\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Copia Link\",\n    \"publishToTheWeb\": \"Pubblica sul Web\",\n    \"publishToTheWebHint\": \"Crea un sito con AppFlowy\",\n    \"publish\": \"Pubblica\",\n    \"unPublish\": \"Annulla la pubblicazione\",\n    \"visitSite\": \"Visita sito\",\n    \"exportAsTab\": \"Esporta come\",\n    \"publishTab\": \"Pubblica\",\n    \"shareTab\": \"Condividi\",\n    \"publishOnAppFlowy\": \"Pubblica su AppFlowy\",\n    \"shareTabTitle\": \"Invita a collaborare\",\n    \"shareTabDescription\": \"Per una facile collaborazione con chiunque\",\n    \"copyLinkSuccess\": \"Collegamento copiato negli appunti\",\n    \"copyShareLink\": \"Copia il link di condivisione\",\n    \"copyLinkFailed\": \"Impossibile copiare il collegamento negli appunti\",\n    \"copyLinkToBlockSuccess\": \"Collegamento al blocco copiato negli appunti\",\n    \"copyLinkToBlockFailed\": \"Impossibile copiare il collegamento del blocco negli appunti\",\n    \"manageAllSites\": \"Gestisci tutti i siti\",\n    \"updatePathName\": \"Aggiorna il nome del percorso\"\n  },\n  \"moreAction\": {\n    \"small\": \"piccolo\",\n    \"medium\": \"medio\",\n    \"large\": \"grande\",\n    \"fontSize\": \"Dimensione del font\",\n    \"import\": \"Importare\",\n    \"moreOptions\": \"Più opzioni\",\n    \"wordCount\": \"Conteggio parole: {}\",\n    \"charCount\": \"Numero di caratteri: {}\",\n    \"createdAt\": \"Creata: {}\",\n    \"deleteView\": \"Cancella\",\n    \"duplicateView\": \"Duplica\",\n    \"wordCountLabel\": \"Numero di parole: \",\n    \"charCountLabel\": \"Numero di caratteri: \",\n    \"createdAtLabel\": \"Creato: \",\n    \"syncedAtLabel\": \"Sincronizzato: \",\n    \"saveAsNewPage\": \"Aggiungi messaggi alla pagina\",\n    \"saveAsNewPageDisabled\": \"Nessun messaggio disponibile\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Testo e markdown\",\n    \"documentFromV010\": \"Documento dalla v0.1.0\",\n    \"databaseFromV010\": \"Database dalla v0.1.0\",\n    \"notionZip\": \"File zip esportato da Notion\",\n    \"csv\": \"CSV\",\n    \"database\": \"Banca dati\"\n  },\n  \"emojiIconPicker\": {\n    \"iconUploader\": {\n      \"placeholderLeft\": \"Trascina e rilascia un file, fai clic per \",\n      \"placeholderUpload\": \"Carica\",\n      \"placeholderRight\": \"oppure incolla il collegamento di un'immagine.\",\n      \"dropToUpload\": \"Trascina un file per caricarlo\",\n      \"change\": \"Modifica\"\n    }\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Rinomina\",\n    \"delete\": \"Cancella\",\n    \"duplicate\": \"Duplica\",\n    \"unfavorite\": \"Rimuovi dai preferiti\",\n    \"favorite\": \"Aggiungi ai preferiti\",\n    \"openNewTab\": \"Apri in una nuova scheda\",\n    \"moveTo\": \"Sposta in\",\n    \"addToFavorites\": \"Aggiungi ai preferiti\",\n    \"copyLink\": \"Copia link\",\n    \"changeIcon\": \"Cambia icona\",\n    \"collapseAllPages\": \"Comprimi le sottopagine\",\n    \"movePageTo\": \"Sposta la pagina in\",\n    \"move\": \"Sposta\",\n    \"lockPage\": \"Blocca la pagina\"\n  },\n  \"blankPageTitle\": \"Pagina vuota\",\n  \"newPageText\": \"Nuova pagina\",\n  \"newDocumentText\": \"Nuovo documento\",\n  \"newGridText\": \"Nuova griglia\",\n  \"newCalendarText\": \"Nuovo calendario\",\n  \"newBoardText\": \"Nuova bacheca\",\n  \"chat\": {\n    \"newChat\": \"Chat AI\",\n    \"inputMessageHint\": \"Chiedi a @:appName AI\",\n    \"inputLocalAIMessageHint\": \"Chiedi a @:appName Local AI\",\n    \"unsupportedCloudPrompt\": \"Questa funzione è disponibile solo quando si usa @:appName Cloud\",\n    \"relatedQuestion\": \"Correlato\",\n    \"serverUnavailable\": \"Servizio temporaneamente non disponibile. Per favore, riprova più tardi.\",\n    \"aiServerUnavailable\": \"🌈 Uh-oh! 🌈. Un unicorno ha mangiato la nostra risposta. Per favore, riprova!\",\n    \"retry\": \"Riprova\",\n    \"clickToRetry\": \"Clicca per riprovare\",\n    \"regenerateAnswer\": \"Rigenera\",\n    \"question1\": \"Come usare Kanban per organizzare le attività\",\n    \"question2\": \"Spiega il metodo GTD\",\n    \"question3\": \"Perché usare Rust\",\n    \"question4\": \"Ricetta con cos'è presente nella mia cucina\",\n    \"question5\": \"Crea un'illustrazione per la mia pagina\",\n    \"question6\": \"Stila una lista di cose da fare per la mia prossima settimana\",\n    \"aiMistakePrompt\": \"Le IA possono fare errori. Controlla le informazioni importanti.\",\n    \"chatWithFilePrompt\": \"Vuoi chattare col file?\",\n    \"indexFileSuccess\": \"Indicizzazione file completata con successo\",\n    \"inputActionNoPages\": \"Nessuna pagina risultante\",\n    \"referenceSource\": {\n      \"zero\": \"0 fonti trovate\",\n      \"one\": \"{count} fonte trovata\",\n      \"other\": \"{count} fonti trovate\"\n    },\n    \"clickToMention\": \"Clicca per menzionare una pagina\",\n    \"uploadFile\": \"Carica file PDF, MD o TXT con la quale chattare\",\n    \"questionDetail\": \"Salve {}! Come posso aiutarti oggi?\",\n    \"indexingFile\": \"Indicizzazione {}\",\n    \"generatingResponse\": \"Sto generando una risposta\",\n    \"selectSources\": \"Seleziona fonti\",\n    \"currentPage\": \"Pagina corrente\",\n    \"sourcesLimitReached\": \"È possibile selezionare solo fino a 3 documenti di livello superiore e i relativi figli\",\n    \"sourceUnsupported\": \"Al momento non supportiamo la chat con i database\",\n    \"regenerate\": \"Riprova\",\n    \"addToPageButton\": \"Aggiungi messaggio alla pagina\",\n    \"addToPageTitle\": \"Aggiungi messaggio a...\",\n    \"addToNewPage\": \"Crea una nuova pagina\",\n    \"addToNewPageName\": \"Messaggi estratti da \\\"{}\\\"\",\n    \"addToNewPageSuccessToast\": \"Messaggio aggiunto a\",\n    \"openPagePreviewFailedToast\": \"Impossibile aprire la pagina\",\n    \"changeFormat\": {\n      \"actionButton\": \"Cambia formato\",\n      \"confirmButton\": \"Rigenera con questo formato\",\n      \"textOnly\": \"Testo\",\n      \"imageOnly\": \"Solo immagine\",\n      \"textAndImage\": \"Testo e immagine\",\n      \"text\": \"Paragrafo\",\n      \"bullet\": \"Elenco puntato\",\n      \"number\": \"Elenco numerato\",\n      \"table\": \"Tavolo\",\n      \"blankDescription\": \"Formato risposta\",\n      \"defaultDescription\": \"Formato di risposta automatica\",\n      \"textWithImageDescription\": \"@:chat .changeFormat.text con immagine\",\n      \"numberWithImageDescription\": \"@:chat .changeFormat.number con immagine\",\n      \"bulletWithImageDescription\": \"@:chat .changeFormat.bullet con immagine\",\n      \"tableWithImageDescription\": \"@:chat .changeFormat.table con immagine\"\n    },\n    \"switchModel\": {\n      \"label\": \"Cambia modello\",\n      \"localModel\": \"Modello locale\",\n      \"cloudModel\": \"Modello in cloud\",\n      \"autoModel\": \"Automatico\"\n    },\n    \"selectBanner\": {\n      \"saveButton\": \"Aggiungi a …\",\n      \"selectMessages\": \"Seleziona i messaggi\",\n      \"nSelected\": \"{} selezionati\",\n      \"allSelected\": \"Tutti selezionati\"\n    },\n    \"stopTooltip\": \"Smetti di generare\"\n  },\n  \"trash\": {\n    \"text\": \"Cestino\",\n    \"restoreAll\": \"Ripristina Tutto\",\n    \"restore\": \"Ripristina\",\n    \"deleteAll\": \"Elimina Tutto\",\n    \"pageHeader\": {\n      \"fileName\": \"Nome file\",\n      \"lastModified\": \"Ultima Modifica\",\n      \"created\": \"Creato\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Sei sicuro di eliminare tutte le pagine nel Cestino?\",\n      \"caption\": \"Questa azione non può essere annullata.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Sei sicuro di ripristinare tutte le pagine nel Cestino?\",\n      \"caption\": \"Questa azione non può essere annullata.\"\n    },\n    \"restorePage\": {\n      \"title\": \"Ripristina: {}\",\n      \"caption\": \"Sei sicuro di voler ripristinare questa pagina?\"\n    },\n    \"mobile\": {\n      \"actions\": \"Azioni del cestino\",\n      \"empty\": \"Il cestino è vuoto\",\n      \"emptyDescription\": \"Non hai alcun file eliminato\",\n      \"isDeleted\": \"è stato cancellato\",\n      \"isRestored\": \"è stato ripristinato\"\n    },\n    \"confirmDeleteTitle\": \"Sei sicuro di voler eliminare questa pagina permanentemente?\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Questa pagina è nel Cestino\",\n    \"restore\": \"Ripristina pagina\",\n    \"deletePermanent\": \"Elimina definitivamente\",\n    \"deletePermanentDescription\": \"Vuoi davvero eliminare definitivamente questa pagina? L'operazione è irreversibile.\"\n  },\n  \"dialogCreatePageNameHint\": \"Nome pagina\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Scorciatoie\",\n    \"whatsNew\": \"Cosa c'è di nuovo?\",\n    \"helpAndDocumentation\": \"Aiuto e documentazione\",\n    \"getSupport\": \"Ottieni supporto\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"Informazioni di debug\",\n      \"success\": \"Informazioni di debug copiate negli appunti!\",\n      \"fail\": \"Impossibile copiare le informazioni di debug negli appunti\"\n    },\n    \"feedback\": \"Feedback\",\n    \"help\": \"Aiuto & Supporto\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Rimuovi, rinomina e altro...\",\n    \"addPageTooltip\": \"Aggiungi velocemente una pagina all'interno\",\n    \"defaultNewPageName\": \"Senza titolo\",\n    \"renameDialog\": \"Rinomina\",\n    \"pageNameSuffix\": \"Copia\"\n  },\n  \"noPagesInside\": \"Nessuna pagina all'interno\",\n  \"toolbar\": {\n    \"undo\": \"Undo\",\n    \"redo\": \"Redo\",\n    \"bold\": \"Grassetto\",\n    \"italic\": \"Italico\",\n    \"underline\": \"Sottolineato\",\n    \"strike\": \"Barrato\",\n    \"numList\": \"Lista numerata\",\n    \"bulletList\": \"Lista a punti\",\n    \"checkList\": \"Lista Controllo\",\n    \"inlineCode\": \"Codice in linea\",\n    \"quote\": \"Cita Blocco\",\n    \"header\": \"intestazione\",\n    \"highlight\": \"Evidenziare\",\n    \"color\": \"Colore\",\n    \"addLink\": \"Aggiungi collegamento\",\n    \"link\": \"Collegamento\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Passa alla modalità Chiara\",\n    \"darkMode\": \"Passa alla modalità Scura\",\n    \"openAsPage\": \"Apri come pagina\",\n    \"addNewRow\": \"Aggiungi una nuova riga\",\n    \"openMenu\": \"Fare clic per aprire il menu\",\n    \"dragRow\": \"Premere a lungo per riordinare la riga\",\n    \"viewDataBase\": \"Visualizza banca dati\",\n    \"referencePage\": \"Questo {nome} è referenziato\",\n    \"addBlockBelow\": \"Aggiungi un blocco qui sotto\",\n    \"aiGenerate\": \"Genera\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Close sidebar\",\n    \"openSidebar\": \"Open sidebar\",\n    \"expandSidebar\": \"Espandi a pagina intera\",\n    \"personal\": \"Personale\",\n    \"private\": \"Privato\",\n    \"workspace\": \"Area di lavoro\",\n    \"favorites\": \"Preferiti\",\n    \"clickToHidePrivate\": \"Clicca per nascondere l'area privata\\nLe pagine create qui sono visibili solo a te\",\n    \"clickToHideWorkspace\": \"Clicca per nascondere la workspace\\nLe pagine che crei qui sono visibili a ogni membro\",\n    \"clickToHidePersonal\": \"Fare clic per nascondere la sezione personale\",\n    \"clickToHideFavorites\": \"Fare clic per nascondere la sezione dei preferiti\",\n    \"addAPage\": \"Aggiungi una pagina\",\n    \"addAPageToPrivate\": \"Aggiungi pagina all'area privata\",\n    \"addAPageToWorkspace\": \"Aggiungi pagina alla workspace\",\n    \"recent\": \"Recente\",\n    \"today\": \"Oggi\",\n    \"thisWeek\": \"Questa settimana\",\n    \"others\": \"Preferiti precedenti\",\n    \"earlier\": \"Prima\",\n    \"justNow\": \"poco fa\",\n    \"minutesAgo\": \"{count} minuti fa\",\n    \"lastViewed\": \"Visto per ultimo\",\n    \"favoriteAt\": \"Aggiunto tra ai preferiti\",\n    \"emptyRecent\": \"Nessun documento recente\",\n    \"emptyRecentDescription\": \"Quando vedrai documenti, appariranno qui per accesso facilitato\",\n    \"emptyFavorite\": \"Nessun documento preferito\",\n    \"emptyFavoriteDescription\": \"Comincia a esplorare e marchia documenti come preferiti. Verranno elencati qui per accesso rapido!\",\n    \"removePageFromRecent\": \"Rimuovere questa pagina da Recenti?\",\n    \"removeSuccess\": \"Rimozione effettuata con successo\",\n    \"favoriteSpace\": \"Preferiti\",\n    \"RecentSpace\": \"Recenti\",\n    \"Spaces\": \"Aree\",\n    \"upgradeToPro\": \"Aggiorna a Pro\",\n    \"upgradeToAIMax\": \"Sblocca AI illimitata\",\n    \"storageLimitDialogTitle\": \"Hai esaurito lo spazio d'archiviazione gratuito. Aggiorna per avere spazio d'archiviazione illimitato!\",\n    \"storageLimitDialogTitleIOS\": \"Hai esaurito lo spazio di archiviazione gratuito.\",\n    \"aiResponseLimitTitle\": \"Hai esaurito le risposte AI gratuite. Aggiorna al Piano Pro per acquistare un add-on AI per avere risposte illimitate\",\n    \"aiResponseLimitDialogTitle\": \"Numero di riposte AI raggiunto\",\n    \"aiResponseLimit\": \"Hai esaurito le risposte AI gratuite.\\n\\nVai su Impostazioni -> Piano -> Fai clic su AI Max o Pro Plan per ottenere più risposte AI\",\n    \"askOwnerToUpgradeToPro\": \"Lo spazio di archiviazione gratuito del tuo spazio di lavoro sta esaurendo. Chiedi al proprietario del tuo spazio di lavoro di passare al piano Pro.\",\n    \"askOwnerToUpgradeToProIOS\": \"Lo spazio di archiviazione gratuito nel tuo spazio di lavoro sta esaurendo.\",\n    \"askOwnerToUpgradeToAIMax\": \"Il tuo spazio di lavoro ha esaurito le risposte AI gratuite. Chiedi al proprietario del tuo spazio di lavoro di aggiornare il piano o di acquistare componenti aggiuntivi AI.\",\n    \"askOwnerToUpgradeToAIMaxIOS\": \"Il tuo spazio di lavoro sta esaurendo le risposte AI gratuite.\",\n    \"purchaseAIMax\": \"Il tuo spazio di lavoro ha esaurito le risposte delle immagini AI. Chiedi al proprietario del tuo spazio di lavoro di acquistare AI Max\",\n    \"aiImageResponseLimit\": \"Hai esaurito le risposte delle immagini dell'IA.\\n\\nVai su Impostazioni -> Piano -> Fai clic su AI Max per ottenere più risposte di immagini AI\",\n    \"purchaseStorageSpace\": \"Acquista spazio di archiviazione\",\n    \"singleFileProPlanLimitationDescription\": \"Hai superato la dimensione massima di caricamento file consentita nel piano gratuito. Passa al piano Pro per caricare file più grandi.\",\n    \"purchaseAIResponse\": \"Acquista\",\n    \"askOwnerToUpgradeToLocalAI\": \"Chiedi al proprietario dell'area di lavoro di abilitare l'intelligenza artificiale sul dispositivo\",\n    \"upgradeToAILocal\": \"Esegui modelli locali sul tuo dispositivo per la massima privacy\",\n    \"upgradeToAILocalDesc\": \"Chatta con i PDF, migliora la tua scrittura e compila automaticamente le tabelle utilizzando l'intelligenza artificiale locale\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Nota esportata in Markdown\",\n      \"path\": \"Documenti/fluido\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Contatti\",\n    \"whatsHappening\": \"Cosa accadrà la prossima settimana?\",\n    \"addContact\": \"Aggiungi Contatti\",\n    \"editContact\": \"Modifica Contatti\"\n  },\n  \"button\": {\n    \"ok\": \"OK\",\n    \"confirm\": \"Conferma\",\n    \"done\": \"Fatto\",\n    \"cancel\": \"Annulla\",\n    \"signIn\": \"Accedi\",\n    \"signOut\": \"Esci\",\n    \"complete\": \"Completa\",\n    \"change\": \"Modifica\",\n    \"save\": \"Salva\",\n    \"generate\": \"creare\",\n    \"esc\": \"ESC\",\n    \"keep\": \"Mantenere\",\n    \"tryAgain\": \"Riprova\",\n    \"discard\": \"Scartare\",\n    \"replace\": \"Sostituire\",\n    \"insertBelow\": \"Inserisci sotto\",\n    \"insertAbove\": \"Inserisci sopra\",\n    \"upload\": \"Caricamento\",\n    \"edit\": \"Modificare\",\n    \"delete\": \"Eliminare\",\n    \"copy\": \"Copia\",\n    \"duplicate\": \"Duplicare\",\n    \"putback\": \"Rimettere a posto\",\n    \"update\": \"Aggiorna\",\n    \"share\": \"Condividi\",\n    \"removeFromFavorites\": \"Rimuovi dai preferiti\",\n    \"removeFromRecent\": \"Rimuovi da Recenti\",\n    \"addToFavorites\": \"Aggiungi ai preferiti\",\n    \"favoriteSuccessfully\": \"Preferito aggiunto con successo\",\n    \"unfavoriteSuccessfully\": \"Preferito rimosso con successo\",\n    \"duplicateSuccessfully\": \"Duplicato con successo\",\n    \"rename\": \"Rinomina\",\n    \"helpCenter\": \"Centro assistenza\",\n    \"add\": \"Aggiungi\",\n    \"yes\": \"SÌ\",\n    \"no\": \"No\",\n    \"clear\": \"Pulisci\",\n    \"remove\": \"Rimuovi\",\n    \"dontRemove\": \"Non rimuovere\",\n    \"copyLink\": \"Copia collegamento\",\n    \"align\": \"Allinea\",\n    \"login\": \"Login\",\n    \"logout\": \"Disconnettiti\",\n    \"deleteAccount\": \"Elimina l'account\",\n    \"back\": \"Indietro\",\n    \"signInGoogle\": \"Continua con Google\",\n    \"signInGithub\": \"Continua con GitHub\",\n    \"signInDiscord\": \"Continua con Discord\",\n    \"more\": \"Di più\",\n    \"create\": \"Creare\",\n    \"close\": \"Chiudi\",\n    \"next\": \"Prossimo\",\n    \"previous\": \"Precedente\",\n    \"submit\": \"Invia\",\n    \"download\": \"Scarica\",\n    \"backToHome\": \"Torna alla Home\",\n    \"viewing\": \"Visualizzazione\",\n    \"editing\": \"Modifica\",\n    \"gotIt\": \"Fatto\",\n    \"retry\": \"Riprova\",\n    \"uploadFailed\": \"Caricamento non riuscito.\",\n    \"copyLinkOriginal\": \"Copia il collegamento all'originale\"\n  },\n  \"label\": {\n    \"welcome\": \"Benvenuto!\",\n    \"firstName\": \"Nome\",\n    \"middleName\": \"Secondo Nome\",\n    \"lastName\": \"Cognome\",\n    \"stepX\": \"Passo {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Impossibile collegarsi al tuo account.\",\n      \"failedMsg\": \"Si prega di verificare di aver completato il processo di iscrizione nel tuo browser.\"\n    },\n    \"google\": {\n      \"title\": \"GOOGLE SIGN-IN\",\n      \"instruction1\": \"Al fine di importare i tuoi Contatti Google è necessario autorizzare questa applicazione ad utilizzare il tuo beowser web.\",\n      \"instruction2\": \"Copia questo codice negli appunti premendo l'icona o selezionando il testo:\",\n      \"instruction3\": \"Naviga sul seguente link con il tuo browser web e inserisci il codice seguente:\",\n      \"instruction4\": \"Premi il bottone qui sotto quando hai completato l'iscrizione:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Impostazioni\",\n    \"popupMenuItem\": {\n      \"settings\": \"Impostazioni\",\n      \"members\": \"Membri\",\n      \"trash\": \"Spazzatura\",\n      \"helpAndDocumentation\": \"Aiuto e documentazione\",\n      \"getSupport\": \"Ottieni supporto\"\n    },\n    \"sites\": {\n      \"title\": \"Siti\",\n      \"namespaceTitle\": \"Namespace\",\n      \"namespaceDescription\": \"Gestisci il tuo namespace e la tua homepage\",\n      \"namespaceHeader\": \"Namespace\",\n      \"homepageHeader\": \"Homepage\",\n      \"updateNamespace\": \"Aggiorna il namespace\",\n      \"removeHomepage\": \"Rimuovi la homepage\",\n      \"selectHomePage\": \"Seleziona una pagina\",\n      \"clearHomePage\": \"Cancella la homepage per questo namespace\",\n      \"customUrl\": \"URL personalizzato\",\n      \"homePage\": {\n        \"upgradeToPro\": \"Passa al piano Pro per impostare una homepage\"\n      },\n      \"namespace\": {\n        \"description\": \"Questa modifica verrà applicata a tutte le pagine pubblicate in questo namespace\",\n        \"tooltip\": \"Ci riserviamo il diritto di rimuovere qualsiasi namespace inappropriato\",\n        \"updateExistingNamespace\": \"Aggiorna il namexpace esistente\",\n        \"upgradeToPro\": \"Passa al piano Pro per richiedere uno spazio dei nomi personalizzato\",\n        \"redirectToPayment\": \"Reindirizzamento alla pagina di pagamento...\",\n        \"onlyWorkspaceOwnerCanSetHomePage\": \"Solo il proprietario dell'area di lavoro può impostare una homepage\",\n        \"pleaseAskOwnerToSetHomePage\": \"Chiedi al proprietario dell'area di lavoro di passare al piano Pro\"\n      },\n      \"publishedPage\": {\n        \"title\": \"Tutte le pagine pubblicate\",\n        \"description\": \"Gestisci le tue pagine pubblicate\",\n        \"page\": \"Pagina\",\n        \"pathName\": \"Nome del percorso\",\n        \"date\": \"Data di pubblicazione\",\n        \"emptyHinText\": \"Non hai pagine pubblicate in questo spazio di lavoro\",\n        \"noPublishedPages\": \"Nessuna pagina pubblicata\",\n        \"settings\": \"Impostazioni di pubblicazione\",\n        \"clickToOpenPageInApp\": \"Apri la pagina nell'app\",\n        \"clickToOpenPageInBrowser\": \"Apri la pagina nel browser\"\n      },\n      \"error\": {\n        \"failedToGeneratePaymentLink\": \"Impossibile generare il collegamento di pagamento per il piano Pro\",\n        \"failedToUpdateNamespace\": \"Impossibile aggiornare il namespace\",\n        \"proPlanLimitation\": \"È necessario eseguire l'aggiornamento al piano Pro per aggiornare lo spazio dei nomi\",\n        \"namespaceAlreadyInUse\": \"Il namespace è già stato preso, provane un altro\",\n        \"invalidNamespace\": \"Namespace non valido, provane un altro\",\n        \"namespaceLengthAtLeast2Characters\": \"Il namespace deve essere lungo almeno 2 caratteri\",\n        \"onlyWorkspaceOwnerCanUpdateNamespace\": \"Solo il proprietario dell'area di lavoro può aggiornare il namespace\",\n        \"onlyWorkspaceOwnerCanRemoveHomepage\": \"Solo il proprietario dell'area di lavoro può rimuovere la home page\",\n        \"setHomepageFailed\": \"Impossibile impostare la homepage\",\n        \"namespaceTooLong\": \"Il namespace è troppo lungo, provane un altro\",\n        \"namespaceTooShort\": \"Il namespace è troppo breve, provane un altro\",\n        \"namespaceIsReserved\": \"Il namespace è riservato, provane un altro\",\n        \"updatePathNameFailed\": \"Impossibile aggiornare il percorso\",\n        \"removeHomePageFailed\": \"Impossibile rimuovere la homepage\",\n        \"publishNameContainsInvalidCharacters\": \"Il percorso contiene caratteri non validi, provane un altro\",\n        \"publishNameTooShort\": \"Il percorso è troppo breve, provane un altro\",\n        \"publishNameTooLong\": \"Il percorso è troppo lungo, provane un altro\",\n        \"publishNameAlreadyInUse\": \"Il percorso è già in uso, provane un altro\",\n        \"namespaceContainsInvalidCharacters\": \"Il namespace contiene caratteri non validi, provane un altro\",\n        \"publishPermissionDenied\": \"Solo il proprietario dell'area di lavoro o l'editore della pagina può gestire le impostazioni di pubblicazione\",\n        \"publishNameCannotBeEmpty\": \"Il percorso non può essere vuoto, provane un altro\"\n      },\n      \"success\": {\n        \"namespaceUpdated\": \"Namespace aggiornato correttamente\",\n        \"setHomepageSuccess\": \"La homepage è stata impostata correttamente\",\n        \"updatePathNameSuccess\": \"Percorso aggiornato correttamente\",\n        \"removeHomePageSuccess\": \"Homepage rimossa con successo\"\n      }\n    },\n    \"accountPage\": {\n      \"menuLabel\": \"Account e app\",\n      \"title\": \"Il mio account\",\n      \"general\": {\n        \"title\": \"Nome dell'account e immagine del profilo\",\n        \"changeProfilePicture\": \"Cambia l'immagine del profilo\"\n      },\n      \"email\": {\n        \"title\": \"Email\",\n        \"actions\": {\n          \"change\": \"Cambia l'email\"\n        }\n      },\n      \"login\": {\n        \"title\": \"Accedi all'account\",\n        \"loginLabel\": \"Login\",\n        \"logoutLabel\": \"Disconnettiti\"\n      },\n      \"isUpToDate\": \"@:appName è aggiornato!\",\n      \"officialVersion\": \"Versione {versione} (官方構建)\"\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"Area di lavoro\",\n      \"title\": \"Area di lavoro\",\n      \"description\": \"Personalizza l'aspetto, il tema, il carattere, il layout del testo, il formato data/ora e la lingua del tuo spazio di lavoro.\",\n      \"workspaceName\": {\n        \"title\": \"Nome dell'area di lavoro\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"Icona dell'area di lavoro\",\n        \"description\": \"Carica un'immagine o usa un'emoji per il tuo spazio di lavoro. L'icona verrà visualizzata nella barra laterale e nelle notifiche.\"\n      },\n      \"appearance\": {\n        \"title\": \"Aspetto\",\n        \"description\": \"Personalizza l'aspetto, il tema, il carattere, il layout del testo, la data, l'ora e la lingua del tuo spazio di lavoro.\",\n        \"options\": {\n          \"system\": \"Automatico\",\n          \"light\": \"Chiaro\",\n          \"dark\": \"Scuro\"\n        }\n      },\n      \"resetCursorColor\": {\n        \"title\": \"Reimposta il colore del cursore del documento\",\n        \"description\": \"Vuoi davvero reimpostare il colore del cursore?\"\n      },\n      \"resetSelectionColor\": {\n        \"title\": \"Reimposta il colore di selezione del documento\",\n        \"description\": \"Vuoi davvero reimpostare il colore di selezione?\"\n      },\n      \"resetWidth\": {\n        \"resetSuccess\": \"Reimposta correttamente la larghezza del documento\"\n      },\n      \"theme\": {\n        \"title\": \"Tema\",\n        \"description\": \"Seleziona un tema preimpostato o carica il tuo tema personalizzato.\",\n        \"uploadCustomThemeTooltip\": \"Carica un tema personalizzato\",\n        \"failedToLoadThemes\": \"Impossibile caricare i temi, controlla le impostazioni dei permessi in Impostazioni di sistema -> Privacy e sicurezza -> File e cartelle -> @:appName\"\n      },\n      \"workspaceFont\": {\n        \"title\": \"Carattere dell'area di lavoro\",\n        \"noFontHint\": \"Nessun font trovato, prova un altro termine.\"\n      },\n      \"textDirection\": {\n        \"title\": \"Direzione del testo\",\n        \"leftToRight\": \"Da sinistra a destra\",\n        \"rightToLeft\": \"Da destra a sinistra\",\n        \"auto\": \"Auto\",\n        \"enableRTLItems\": \"Abilita gli elementi della barra degli strumenti RTL\"\n      },\n      \"layoutDirection\": {\n        \"title\": \"Direzione del layout\",\n        \"leftToRight\": \"Da sinistra a destra\",\n        \"rightToLeft\": \"Da destra a sinistra\"\n      },\n      \"dateTime\": {\n        \"title\": \"Data e ora\",\n        \"example\": \"{} alle {} ({})\",\n        \"24HourTime\": \"formato 24 ore\",\n        \"dateFormat\": {\n          \"label\": \"Formato data\",\n          \"local\": \"Locale\",\n          \"us\": \"US\",\n          \"iso\": \"ISO\",\n          \"friendly\": \"Amichevole\",\n          \"dmy\": \"G/M/A\"\n        }\n      },\n      \"language\": {\n        \"title\": \"Lingua\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"Elimina area di lavoro\",\n        \"content\": \"Vuoi davvero eliminare questo spazio di lavoro? Questa azione non può essere annullata e tutte le pagine che hai pubblicato verranno rimosse dalla pubblicazione.\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"Lascia l'area di lavoro\",\n        \"content\": \"Vuoi davvero uscire da questa area di lavoro? Perderai l'accesso a tutte le pagine e ai dati in essa contenuti.\",\n        \"success\": \"Hai abbandonato l'area di lavoro con successo.\",\n        \"fail\": \"Impossibile uscire dall'area di lavoro.\"\n      },\n      \"manageWorkspace\": {\n        \"title\": \"Gestisci l'area di lavoro\",\n        \"leaveWorkspace\": \"Lascia l'area di lavoro\",\n        \"deleteWorkspace\": \"Elimina area di lavoro\"\n      }\n    },\n    \"manageDataPage\": {\n      \"menuLabel\": \"Gestisci i dati\",\n      \"title\": \"Gestisci i dati\",\n      \"description\": \"Gestisci l'archiviazione locale dei dati o importa i tuoi dati esistenti in @:appName .\",\n      \"dataStorage\": {\n        \"title\": \"Posizione di archiviazione dei file\",\n        \"tooltip\": \"La posizione in cui sono archiviati i tuoi file\",\n        \"actions\": {\n          \"change\": \"Cambia percorso\",\n          \"open\": \"Apri cartella\",\n          \"openTooltip\": \"Apri la posizione della cartella dati corrente\",\n          \"copy\": \"Copia percorso\",\n          \"copiedHint\": \"Percorso copiato!\",\n          \"resetTooltip\": \"Ripristina la posizione predefinita\"\n        },\n        \"resetDialog\": {\n          \"title\": \"Sei sicuro?\",\n          \"description\": \"Reimpostare il percorso alla posizione predefinita dei dati non eliminerà i dati. Se desideri reimportare i dati correnti, devi prima copiare il percorso della posizione corrente.\"\n        }\n      },\n      \"importData\": {\n        \"title\": \"Importa dati\",\n        \"tooltip\": \"Importa dati da cartelle di backup/dati @:appName\",\n        \"description\": \"Copia i dati da una cartella dati esterna @:appName\",\n        \"action\": \"Sfoglia file\"\n      },\n      \"encryption\": {\n        \"title\": \"Crittografia\",\n        \"tooltip\": \"Gestisci il modo in cui i tuoi dati vengono archiviati e crittografati\",\n        \"descriptionNoEncryption\": \"L'attivazione della crittografia crittograferà tutti i dati. Questa operazione non può essere annullata.\",\n        \"descriptionEncrypted\": \"I tuoi dati sono crittografati.\",\n        \"action\": \"Crittografare i dati\",\n        \"dialog\": {\n          \"title\": \"Vuoi crittografare tutti i tuoi dati?\",\n          \"description\": \"La crittografia di tutti i tuoi dati li manterrà al sicuro e protetti. Questa azione NON può essere annullata. Vuoi continuare?\"\n        }\n      },\n      \"cache\": {\n        \"title\": \"Cancella cache\",\n        \"description\": \"Aiuta a risolvere problemi come il mancato caricamento delle immagini, pagine mancanti in uno spazio e caratteri non caricati. Questo non influirà sui tuoi dati.\",\n        \"dialog\": {\n          \"title\": \"Cancella cache\",\n          \"description\": \"Aiuta a risolvere problemi come il mancato caricamento delle immagini, pagine mancanti in uno spazio e caratteri non caricati. Questo non influirà sui tuoi dati.\",\n          \"successHint\": \"Cache svuotata!\"\n        }\n      },\n      \"data\": {\n        \"fixYourData\": \"Correggi i tuoi dati\",\n        \"fixButton\": \"Correggi\",\n        \"fixYourDataDescription\": \"Se riscontri problemi con i tuoi dati, puoi provare a risolverli qui.\"\n      }\n    },\n    \"shortcutsPage\": {\n      \"menuLabel\": \"Scorciatoie\",\n      \"title\": \"Scorciatoie\",\n      \"editBindingHint\": \"Inserisci nuova associazione\",\n      \"searchHint\": \"Ricerca\",\n      \"actions\": {\n        \"resetDefault\": \"Ripristina predefinito\"\n      },\n      \"errorPage\": {\n        \"message\": \"Impossibile caricare i collegamenti: {}\",\n        \"howToFix\": \"Riprova. Se il problema persiste, contattaci su GitHub.\"\n      },\n      \"resetDialog\": {\n        \"title\": \"Reimposta le scorciatoie\",\n        \"description\": \"Questa operazione ripristinerà tutte le combinazioni di tasti predefinite; non sarà possibile annullare questa operazione in seguito. Vuoi procedere?\",\n        \"buttonLabel\": \"Ripristina\"\n      },\n      \"conflictDialog\": {\n        \"title\": \"{} è attualmente in uso\",\n        \"descriptionPrefix\": \"Questa combinazione di tasti è attualmente utilizzata da \",\n        \"descriptionSuffix\": \". Se si sostituisce questa combinazione di tasti, questa verrà rimossa da {}.\",\n        \"confirmLabel\": \"Continua\"\n      },\n      \"editTooltip\": \"Premere per iniziare a modificare la combinazione di tasti\",\n      \"keybindings\": {\n        \"toggleToDoList\": \"Attiva/disattiva l'elenco delle cose da fare\",\n        \"insertNewParagraphInCodeblock\": \"Inserisci nuovo paragrafo\",\n        \"pasteInCodeblock\": \"Incolla nel blocco di codice\",\n        \"selectAllCodeblock\": \"Seleziona tutto\",\n        \"indentLineCodeblock\": \"Inserisci due spazi all'inizio della riga\",\n        \"outdentLineCodeblock\": \"Elimina due spazi all'inizio della riga\",\n        \"twoSpacesCursorCodeblock\": \"Inserisci due spazi al cursore\",\n        \"copy\": \"Copia selezione\",\n        \"paste\": \"Incolla nel contenuto\",\n        \"cut\": \"Taglia la selezione\",\n        \"alignLeft\": \"Allinea il testo a sinistra\",\n        \"alignCenter\": \"Allinea il testo al centro\",\n        \"alignRight\": \"Allinea il testo a destra\",\n        \"insertInlineMathEquation\": \"Inserisci equazione matematica in linea\",\n        \"undo\": \"Annulla\",\n        \"redo\": \"Rifai\",\n        \"convertToParagraph\": \"Converti blocco in paragrafo\",\n        \"backspace\": \"Elimina\",\n        \"deleteLeftWord\": \"Elimina la parola a sinistra\",\n        \"deleteLeftSentence\": \"Elimina la frase a sinistra\",\n        \"delete\": \"Elimina il carattere corretto\",\n        \"deleteMacOS\": \"Elimina il carattere a sinistra\",\n        \"deleteRightWord\": \"Elimina la parola a destra\",\n        \"moveCursorLeft\": \"Sposta il cursore a sinistra\",\n        \"moveCursorBeginning\": \"Sposta il cursore all'inizio\",\n        \"moveCursorLeftWord\": \"Sposta il cursore a sinistra di una parola\",\n        \"moveCursorLeftSelect\": \"Seleziona e sposta il cursore a sinistra\",\n        \"moveCursorBeginSelect\": \"Seleziona e sposta il cursore all'inizio\",\n        \"moveCursorLeftWordSelect\": \"Seleziona e sposta il cursore a sinistra di una parola\",\n        \"moveCursorRight\": \"Sposta il cursore a destra\",\n        \"moveCursorEnd\": \"Sposta il cursore alla fine\",\n        \"moveCursorRightWord\": \"Sposta il cursore a destra di una parola\",\n        \"moveCursorRightSelect\": \"Seleziona e sposta il cursore a destra\",\n        \"moveCursorEndSelect\": \"Seleziona e sposta il cursore alla fine\",\n        \"moveCursorRightWordSelect\": \"Seleziona e sposta il cursore a destra di una parola\",\n        \"moveCursorUp\": \"Sposta il cursore verso l'alto\",\n        \"moveCursorTopSelect\": \"Seleziona e sposta il cursore in alto\",\n        \"moveCursorTop\": \"Sposta il cursore in alto\",\n        \"moveCursorUpSelect\": \"Seleziona e sposta il cursore verso l'alto\",\n        \"moveCursorBottomSelect\": \"Seleziona e sposta il cursore in basso\",\n        \"moveCursorBottom\": \"Sposta il cursore in basso\",\n        \"moveCursorDown\": \"Sposta il cursore verso il basso\",\n        \"moveCursorDownSelect\": \"Seleziona e sposta il cursore verso il basso\",\n        \"home\": \"Scorri verso l'alto\",\n        \"end\": \"Scorri fino in fondo\",\n        \"toggleBold\": \"Attiva/disattiva grassetto\",\n        \"toggleItalic\": \"Attiva/disattiva il corsivo\",\n        \"toggleUnderline\": \"Attiva/disattiva la sottolineatura\",\n        \"toggleStrikethrough\": \"Attiva/disattiva barrato\",\n        \"toggleCode\": \"Attiva/disattiva il codice in linea\",\n        \"toggleHighlight\": \"Attiva/disattiva l'evidenziazione\",\n        \"showLinkMenu\": \"Mostra il menu dei link\",\n        \"openInlineLink\": \"Apri collegamento in linea\",\n        \"openLinks\": \"Apri tutti i link selezionati\",\n        \"indent\": \"Aumenta rientro\",\n        \"outdent\": \"Riduci rientro\",\n        \"exit\": \"Esci dalla modifica\",\n        \"pageUp\": \"Scorri una pagina verso l'alto\",\n        \"pageDown\": \"Scorri una pagina verso il basso\",\n        \"selectAll\": \"Seleziona tutto\",\n        \"pasteWithoutFormatting\": \"Incolla il contenuto senza formattazione\",\n        \"showEmojiPicker\": \"Mostra selettore emoji\",\n        \"enterInTableCell\": \"Aggiungi interruzione di riga nella tabella\",\n        \"leftInTableCell\": \"Sposta a sinistra di una cella nella tabella\",\n        \"rightInTableCell\": \"Spostati a destra di una cella nella tabella\",\n        \"upInTableCell\": \"Spostarsi su di una cella nella tabella\",\n        \"downInTableCell\": \"Spostarsi verso il basso di una cella nella tabella\",\n        \"tabInTableCell\": \"Vai alla cella disponibile successiva nella tabella\",\n        \"shiftTabInTableCell\": \"Vai alla cella precedentemente disponibile nella tabella\",\n        \"backSpaceInTableCell\": \"Fermati all'inizio della cella\"\n      },\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"Inserisci un nuovo paragrafo accanto al blocco di codice\",\n        \"codeBlockIndentLines\": \"Inserire due spazi all'inizio della riga nel blocco di codice\",\n        \"codeBlockOutdentLines\": \"Elimina due spazi all'inizio della riga nel blocco di codice\",\n        \"codeBlockAddTwoSpaces\": \"Inserisci due spazi nella posizione del cursore nel blocco di codice\",\n        \"codeBlockSelectAll\": \"Seleziona tutto il contenuto all'interno di un blocco di codice\",\n        \"codeBlockPasteText\": \"Incolla il testo nel blocco di codice\",\n        \"textAlignLeft\": \"Allinea il testo a sinistra\",\n        \"textAlignCenter\": \"Allinea il testo al centro\",\n        \"textAlignRight\": \"Allinea il testo a destra\"\n      },\n      \"couldNotLoadErrorMsg\": \"Impossibile caricare i collegamenti. Riprova.\",\n      \"couldNotSaveErrorMsg\": \"Impossibile salvare i collegamenti. Riprova.\"\n    },\n    \"aiPage\": {\n      \"title\": \"Impostazioni AI\",\n      \"menuLabel\": \"Impostazioni AI\",\n      \"keys\": {\n        \"enableAISearchTitle\": \"Ricerca AI\",\n        \"aiSettingsDescription\": \"Scegli il modello che preferisci per potenziare AppFlowy AI. Ora include GPT-4o, GPT-o3-mini, DeepSeek R1, Claude 3.5 Sonnet e modelli disponibili in Ollama.\",\n        \"loginToEnableAIFeature\": \"Le funzionalità di intelligenza artificiale sono abilitate solo dopo aver effettuato l'accesso con @:appName Cloud. Se non hai un @:appName , vai su \\\"Il mio account\\\" per registrarti.\",\n        \"llmModel\": \"Modello linguistico\",\n        \"globalLLMModel\": \"Modello linguistico globale\",\n        \"readOnlyField\": \"Questo campo è di sola lettura\",\n        \"llmModelType\": \"Tipo di modello linguistico\",\n        \"downloadLLMPrompt\": \"Scarica {}\",\n        \"downloadAppFlowyOfflineAI\": \"Scaricando il pacchetto AI offline, l'AI potrà essere eseguita sul tuo dispositivo. Vuoi continuare?\",\n        \"downloadLLMPromptDetail\": \"Il download del modello locale {} occuperà fino a {} di spazio di archiviazione. Vuoi continuare?\",\n        \"downloadBigFilePrompt\": \"Potrebbero essere necessari circa 10 minuti per completare il download\",\n        \"downloadAIModelButton\": \"Scarica\",\n        \"downloadingModel\": \"Scaricando\",\n        \"localAILoaded\": \"Modello AI locale aggiunto correttamente e pronto per l'uso\",\n        \"localAIStart\": \"L'intelligenza artificiale locale si sta avviando. Se è lenta, prova a disattivarla e riattivarla.\",\n        \"localAILoading\": \"Caricamento del modello di chat AI locale...\",\n        \"localAIStopped\": \"L'IA locale si è fermata\",\n        \"localAIRunning\": \"L'intelligenza artificiale locale è in esecuzione\",\n        \"localAINotReadyRetryLater\": \"L'IA locale è in fase di inizializzazione, riprova più tardi\",\n        \"localAIDisabled\": \"Stai utilizzando l'intelligenza artificiale locale, ma è disabilitata. Vai alle impostazioni per abilitarla o prova un modello diverso.\",\n        \"localAIInitializing\": \"L'intelligenza artificiale locale è in fase di caricamento. Potrebbero volerci alcuni secondi, a seconda del dispositivo.\",\n        \"localAINotReadyTextFieldPrompt\": \"Non è possibile modificare mentre l'IA locale è in fase di caricamento\",\n        \"localAIDisabledTextFieldPrompt\": \"Non puoi modificare mentre l'IA locale è disabilitata\",\n        \"failToLoadLocalAI\": \"Impossibile avviare l'IA locale.\",\n        \"restartLocalAI\": \"Riavvia\",\n        \"disableLocalAITitle\": \"Disabilita l'IA locale\",\n        \"disableLocalAIDescription\": \"Vuoi disattivare l'IA locale?\",\n        \"localAIToggleTitle\": \"AppFlowy IA locale (LAI)\",\n        \"localAIToggleSubTitle\": \"Esegui i modelli di intelligenza artificiale locale più avanzati all'interno di AppFlowy per la massima privacy e sicurezza\",\n        \"offlineAIInstruction1\": \"Segui l'\",\n        \"offlineAIInstruction2\": \"istruzione\",\n        \"offlineAIInstruction3\": \"per abilitare l'intelligenza artificiale offline.\",\n        \"offlineAIDownload1\": \"Se non hai scaricato AppFlowy AI, per favore\",\n        \"offlineAIDownload2\": \"scaricala\",\n        \"offlineAIDownload3\": \"prima di tutto\",\n        \"activeOfflineAI\": \"Attiva\",\n        \"downloadOfflineAI\": \"Scarica\",\n        \"openModelDirectory\": \"Apri cartella\",\n        \"laiNotReady\": \"L'app Local AI non è stata installata correttamente.\",\n        \"ollamaNotReady\": \"Il server Ollama non è pronto.\",\n        \"pleaseFollowThese\": \"Per favore segui queste\",\n        \"instructions\": \"istruzioni\",\n        \"installOllamaLai\": \"per configurare Ollama e AppFlowy Local AI.\",\n        \"modelsMissing\": \"Impossibile trovare i modelli richiesti: \",\n        \"downloadModel\": \"per scaricarli.\"\n      }\n    },\n    \"planPage\": {\n      \"menuLabel\": \"Piano\",\n      \"title\": \"Piano tariffario\",\n      \"planUsage\": {\n        \"title\": \"Riepilogo dell'utilizzo del piano\",\n        \"storageLabel\": \"Archiviazione\",\n        \"storageUsage\": \"{} di {} GB\",\n        \"unlimitedStorageLabel\": \"Spazio di archiviazione illimitato\",\n        \"collaboratorsLabel\": \"Membri\",\n        \"collaboratorsUsage\": \"{} di {}\",\n        \"aiResponseLabel\": \"Risposte IA\",\n        \"aiResponseUsage\": \"{} di {}\",\n        \"unlimitedAILabel\": \"Risposte illimitate\",\n        \"proBadge\": \"Pro\",\n        \"aiMaxBadge\": \"AI Max\",\n        \"aiOnDeviceBadge\": \"AI sul dispositivo per Mac\",\n        \"memberProToggle\": \"Più membri e accesso illimitato all'IA e agli ospiti\",\n        \"aiMaxToggle\": \"Intelligenza artificiale illimitata e accesso a modelli avanzati\",\n        \"aiOnDeviceToggle\": \"Intelligenza artificiale locale per la massima privacy\",\n        \"aiCredit\": {\n          \"title\": \"Aggiungi credito AI @:appName\",\n          \"price\": \"{}\",\n          \"priceDescription\": \"per 1.000 crediti\",\n          \"purchase\": \"Acquista AI\",\n          \"info\": \"Aggiungi 1.000 crediti AI per area di lavoro e integra perfettamente l'AI personalizzabile nel tuo flusso di lavoro per risultati più intelligenti e rapidi con un massimo di:\",\n          \"infoItemOne\": \"10.000 risposte per database\",\n          \"infoItemTwo\": \"1.000 risposte per spazio di lavoro\"\n        },\n        \"currentPlan\": {\n          \"bannerLabel\": \"Piano attuale\",\n          \"freeTitle\": \"Gratuito\",\n          \"proTitle\": \"Pro\",\n          \"teamTitle\": \"Team\",\n          \"freeInfo\": \"Perfetto per singoli fino a 2 membri per organizzare tutto\",\n          \"proInfo\": \"Perfetto per team piccoli e medi fino a 10 membri.\",\n          \"teamInfo\": \"Perfetto per tutti i team produttivi e ben organizzati.\",\n          \"upgrade\": \"Cambia piano\",\n          \"canceledInfo\": \"Il tuo piano è stato annullato e il {} passerà al piano gratuito.\"\n        },\n        \"addons\": {\n          \"title\": \"Componenti aggiuntivi\",\n          \"addLabel\": \"Aggiungi\",\n          \"activeLabel\": \"Aggiunto\",\n          \"aiMax\": {\n            \"title\": \"AI Max\",\n            \"description\": \"Risposte AI illimitate basate su modelli AI avanzati e 50 immagini AI al mese\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"Per utente, al mese, fatturato annualmente\"\n          },\n          \"aiOnDevice\": {\n            \"title\": \"AI sul dispositivo per Mac\",\n            \"description\": \"Esegui Mistral 7B, LLAMA 3 e altri modelli locali sul tuo computer\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"Per utente al mese fatturato annualmente\",\n            \"recommend\": \"Si consiglia M1 o più recente\"\n          }\n        },\n        \"deal\": {\n          \"bannerLabel\": \"Offerta per il nuovo anno!\",\n          \"title\": \"Fai crescere il tuo team!\",\n          \"info\": \"Esegui l'upgrade e risparmia il 10% sui piani Pro e Team! Aumenta la produttività del tuo spazio di lavoro con nuove potenti funzionalità, tra cui l'intelligenza artificiale @:appName .\",\n          \"viewPlans\": \"Visualizza i piani\"\n        }\n      }\n    },\n    \"billingPage\": {\n      \"menuLabel\": \"Fatturazione\",\n      \"title\": \"Fatturazione\",\n      \"plan\": {\n        \"title\": \"Piano\",\n        \"freeLabel\": \"Gratuito\",\n        \"proLabel\": \"Pro\",\n        \"planButtonLabel\": \"Cambia piano\",\n        \"billingPeriod\": \"Periodo di fatturazione\",\n        \"periodButtonLabel\": \"Periodo di modifica\"\n      },\n      \"paymentDetails\": {\n        \"title\": \"Dettagli di pagamento\",\n        \"methodLabel\": \"Metodo di pagamento\",\n        \"methodButtonLabel\": \"Modifica il metodo\"\n      },\n      \"addons\": {\n        \"title\": \"Componenti aggiuntivi\",\n        \"addLabel\": \"Aggiungi\",\n        \"removeLabel\": \"Rimuovi\",\n        \"renewLabel\": \"Rinnova\",\n        \"aiMax\": {\n          \"label\": \"AI Max\",\n          \"description\": \"Sblocca IA illimitata e modelli avanzati\",\n          \"activeDescription\": \"Prossima fattura in scadenza il {}\",\n          \"canceledDescription\": \"AI Max sarà disponibile fino al {}\"\n        },\n        \"aiOnDevice\": {\n          \"label\": \"AI sul dispositivo per Mac\",\n          \"description\": \"Sblocca l'intelligenza artificiale illimitata sul tuo dispositivo\",\n          \"activeDescription\": \"Prossima fattura in scadenza il {}\",\n          \"canceledDescription\": \"AI On-device per Mac sarà disponibile fino al {}\"\n        },\n        \"removeDialog\": {\n          \"title\": \"Rimuovi {}\",\n          \"description\": \"Vuoi davvero rimuovere {plan}? Perderai immediatamente l'accesso alle funzionalità e ai vantaggi di {plan}.\"\n        }\n      },\n      \"currentPeriodBadge\": \"ATTUALE\",\n      \"changePeriod\": \"Modifica il periodo\",\n      \"planPeriod\": \"{} periodo\",\n      \"monthlyInterval\": \"Mensile\",\n      \"monthlyPriceInfo\": \"per posto fatturato mensilmente\",\n      \"annualInterval\": \"Annualmente\",\n      \"annualPriceInfo\": \"per posto fatturato annualmente\"\n    },\n    \"comparePlanDialog\": {\n      \"title\": \"Confronta e seleziona il piano\",\n      \"planFeatures\": \"Piano\\nCaratteristiche\",\n      \"current\": \"Attuale\",\n      \"actions\": {\n        \"upgrade\": \"Aggiorna\",\n        \"downgrade\": \"Downgrade\",\n        \"current\": \"Attuale\"\n      },\n      \"freePlan\": {\n        \"title\": \"Gratuito\",\n        \"description\": \"Per singoli fino a 2 membri per organizzare tutto\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"Gratuito per sempre\"\n      },\n      \"proPlan\": {\n        \"title\": \"Pro\",\n        \"description\": \"Per piccoli team per gestire progetti e conoscenze di squadra\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"Per utente al mese\\nfatturato annualmente<inlang-LineFeed>\\n{} fatturato mensilmente\"\n      },\n      \"planLabels\": {\n        \"itemOne\": \"Spazi di lavoro\",\n        \"itemTwo\": \"Membri\",\n        \"itemThree\": \"Archiviazione\",\n        \"itemFour\": \"Collaborazione in tempo reale\",\n        \"itemFive\": \"Editor ospiti\",\n        \"itemSix\": \"Risposte AI\",\n        \"itemSeven\": \"Immagini AI\",\n        \"itemFileUpload\": \"Caricamento di file\",\n        \"customNamespace\": \"Namespace personalizzato\",\n        \"tooltipFive\": \"Collaborare su pagine specifiche con i non membri\",\n        \"tooltipSix\": \"Per sempre significa che il numero di risposte non viene mai azzerato\",\n        \"intelligentSearch\": \"Ricerca intelligente\",\n        \"tooltipSeven\": \"Ti consente di personalizzare parte dell'URL per il tuo spazio di lavoro\",\n        \"customNamespaceTooltip\": \"URL del sito pubblicato personalizzato\"\n      },\n      \"freeLabels\": {\n        \"itemOne\": \"Addebitato per spazio di lavoro\",\n        \"itemTwo\": \"Fino a 2\",\n        \"itemThree\": \"5 GB\",\n        \"itemFour\": \"si\",\n        \"itemFive\": \"si\",\n        \"itemSix\": \"10 a vita\",\n        \"itemSeven\": \"2 a vita\",\n        \"itemFileUpload\": \"Fino a 7 MB\",\n        \"intelligentSearch\": \"Ricerca intelligente\"\n      },\n      \"proLabels\": {\n        \"itemOne\": \"Addebitato per spazio di lavoro\",\n        \"itemTwo\": \"Fino a 10\",\n        \"itemThree\": \"Illimitato\",\n        \"itemFour\": \"si\",\n        \"itemFive\": \"Fino a 100\",\n        \"itemSix\": \"Illimitato\",\n        \"itemSeven\": \"50 immagini al mese\",\n        \"itemFileUpload\": \"Illimitato\",\n        \"intelligentSearch\": \"Ricerca intelligente\"\n      },\n      \"paymentSuccess\": {\n        \"title\": \"Ora sei sul piano {}!\",\n        \"description\": \"Il tuo pagamento è stato elaborato con successo e il tuo piano è stato aggiornato a @:appName {}. Puoi visualizzare i dettagli del tuo piano nella pagina Piano\"\n      },\n      \"downgradeDialog\": {\n        \"title\": \"Sei sicuro di voler effettuare il downgrade del tuo piano?\",\n        \"description\": \"Effettuando il downgrade del tuo piano tornerai al piano gratuito. Gli utenti potrebbero perdere l'accesso a questo spazio di lavoro e potresti dover liberare spazio per rispettare i limiti di archiviazione del piano gratuito.\",\n        \"downgradeLabel\": \"Effettua il downgrade del piano\"\n      }\n    },\n    \"cancelSurveyDialog\": {\n      \"title\": \"Ci dispiace vederti andare\",\n      \"description\": \"Ci dispiace vederti andare via. Ci piacerebbe ricevere il tuo feedback per aiutarci a migliorare @:appName . Ti preghiamo di dedicare un momento per rispondere ad alcune domande.\",\n      \"commonOther\": \"Altro\",\n      \"otherHint\": \"Scrivi qui la tua risposta\",\n      \"questionOne\": {\n        \"question\": \"Cosa ti ha spinto ad annullare il tuo abbonamento Pro @:appName ?\",\n        \"answerOne\": \"Costo troppo alto\",\n        \"answerTwo\": \"Le caratteristiche non hanno soddisfatto le aspettative\",\n        \"answerThree\": \"Ho trovato un'alternativa migliore\",\n        \"answerFour\": \"Non l'ho usato abbastanza per giustificare la spesa\",\n        \"answerFive\": \"Problema di servizio o difficoltà tecniche\"\n      },\n      \"questionTwo\": {\n        \"question\": \"Quanto è probabile che in futuro prenderai in considerazione l'idea di riabbonarti a @:appName Pro?\",\n        \"answerOne\": \"Molto probabile\",\n        \"answerTwo\": \"Abbastanza probabile\",\n        \"answerThree\": \"Non so\",\n        \"answerFour\": \"Improbabile\",\n        \"answerFive\": \"Molto improbabile\"\n      },\n      \"questionThree\": {\n        \"question\": \"Quale funzionalità Pro hai apprezzato di più durante il tuo abbonamento?\",\n        \"answerOne\": \"Collaborazione multiutente\",\n        \"answerTwo\": \"Cronologia delle versioni più lunghe\",\n        \"answerThree\": \"Risposte AI illimitate\",\n        \"answerFour\": \"Accesso ai modelli di intelligenza artificiale locali\"\n      },\n      \"questionFour\": {\n        \"question\": \"Come descriveresti la tua esperienza complessiva con @:appName ?\",\n        \"answerOne\": \"Eccezionale\",\n        \"answerTwo\": \"Buona\",\n        \"answerThree\": \"Nella media\",\n        \"answerFour\": \"Sotto la media\",\n        \"answerFive\": \"Insoddisfatto\"\n      }\n    },\n    \"common\": {\n      \"uploadingFile\": \"Il file è in fase di caricamento. Non uscire dall'app.\",\n      \"uploadNotionSuccess\": \"Il tuo file zip Notion è stato caricato correttamente. Una volta completata l'importazione, riceverai un'email di conferma.\",\n      \"reset\": \"Reset\"\n    },\n    \"menu\": {\n      \"appearance\": \"Aspetto\",\n      \"language\": \"Lingua\",\n      \"user\": \"Utente\",\n      \"files\": \"File\",\n      \"notifications\": \"Notifiche\",\n      \"open\": \"Apri le impostazioni\",\n      \"logout\": \"Disconnettersi\",\n      \"logoutPrompt\": \"Sei sicuro di disconnetterti?\",\n      \"selfEncryptionLogoutPrompt\": \"Sei sicuro di volerti disconnettere? Assicurati di aver copiato il segreto di crittografia\",\n      \"syncSetting\": \"Impostazioni di sincronizzazione\",\n      \"cloudSettings\": \"Impostazioni cloud\",\n      \"enableSync\": \"Abilita la sincronizzazione\",\n      \"enableEncrypt\": \"Crittografare i dati\",\n      \"cloudURL\": \"URL di base\",\n      \"invalidCloudURLScheme\": \"Schema non valido\",\n      \"cloudServerType\": \"Server cloud\",\n      \"cloudServerTypeTip\": \"Tieni presente che dopo aver cambiato il server cloud il tuo account corrente potrebbe disconnettersi\",\n      \"cloudLocal\": \"Locale\",\n      \"cloudAppFlowy\": \"@:appName Cloud Beta\",\n      \"cloudAppFlowySelfHost\": \"@:appName Cloud Self-hosted (autogestito)\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"L'url del cloud non può essere vuoto\",\n      \"clickToCopy\": \"Fare clic per copiare\",\n      \"selfHostStart\": \"Se non disponi di un server, fai riferimento a\",\n      \"selfHostContent\": \"documento\",\n      \"selfHostEnd\": \"per indicazioni su come fare il self-host del proprio server\",\n      \"cloudURLHint\": \"Inserisci l'URL di base del tuo server\",\n      \"cloudWSURL\": \"URL del Websocket\",\n      \"cloudWSURLHint\": \"Inserisci l'indirizzo websocket del tuo server\",\n      \"restartApp\": \"Riavvia\",\n      \"restartAppTip\": \"Riavvia l'applicazione affinché le modifiche abbiano effetto. Tieni presente che ciò potrebbe disconnettere il tuo account corrente\",\n      \"changeServerTip\": \"Dopo aver modificato il server, è necessario fare clic sul pulsante di riavvio affinché le modifiche abbiano effetto.\",\n      \"enableEncryptPrompt\": \"Attiva la crittografia per proteggere i tuoi dati con questo segreto. Conservarlo in modo sicuro; una volta abilitato, non può essere spento. In caso di perdita, i tuoi dati diventano irrecuperabili. Fare clic per copiare\",\n      \"inputEncryptPrompt\": \"Inserisci il tuo segreto di crittografia per\",\n      \"clickToCopySecret\": \"Fare clic per copiare il segreto\",\n      \"configServerSetting\": \"Configura le impostazioni del tuo server\",\n      \"configServerGuide\": \"Dopo aver selezionato \\\"Avvio rapido\\\", vai a \\\"Impostazioni\\\" e quindi a \\\"Impostazioni cloud\\\" per configurare il tuo server self-hosted.\",\n      \"inputTextFieldHint\": \"Il tuo segreto\",\n      \"historicalUserList\": \"Cronologia di accesso dell'utente\",\n      \"historicalUserListTooltip\": \"Questo elenco mostra i tuoi account anonimi. È possibile fare clic su un account per visualizzarne i dettagli. Gli account anonimi vengono creati facendo clic sul pulsante \\\"Inizia\\\".\",\n      \"openHistoricalUser\": \"Fare clic per aprire l'account anonimo\",\n      \"customPathPrompt\": \"L'archiviazione della cartella dati di @:appName in una cartella sincronizzata sul cloud come Google Drive può comportare rischi. Se si accede o si modifica il database all'interno di questa cartella da più posizioni contemporaneamente, potrebbero verificarsi conflitti di sincronizzazione e potenziale danneggiamento dei dati\",\n      \"importAppFlowyData\": \"Importa dati dalla cartella @:appName esterna\",\n      \"importingAppFlowyDataTip\": \"L'importazione dei dati è in corso. Non chiudere l'applicazione\",\n      \"importAppFlowyDataDescription\": \"Copia i dati da una cartella dati @:appName esterna e importali nella cartella dati @:appName corrente\",\n      \"importSuccess\": \"Importazione della cartella dati @:appName riuscita\",\n      \"importFailed\": \"L'importazione della cartella dati di @:appName non è riuscita\",\n      \"importGuide\": \"Per ulteriori dettagli si prega di consultare il documento di riferimento\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Abilita le notifiche\",\n        \"hint\": \"Disattiva per impedire la visualizzazione delle notifiche locali.\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"Ripristina\",\n      \"fontFamily\": {\n        \"label\": \"Famiglia di font\",\n        \"search\": \"Ricerca\"\n      },\n      \"themeMode\": {\n        \"label\": \"Modalità tema\",\n        \"light\": \"Modalità Chiara\",\n        \"dark\": \"Modalità Scura\",\n        \"system\": \"Segui il sistema\"\n      },\n      \"documentSettings\": {\n        \"cursorColor\": \"Colore del cursore del documento\",\n        \"selectionColor\": \"Colore di selezione del documento\",\n        \"hexEmptyError\": \"Il colore hex non può essere vuoto\",\n        \"hexLengthError\": \"Il valore hex deve essere lungo 6 cifre\",\n        \"hexInvalidError\": \"Valore hex non valido\",\n        \"opacityRangeError\": \"L'opacità deve essere compresa tra 1 e 100\",\n        \"app\": \"App\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"Applica\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Direzione dell'impaginazione\",\n        \"hint\": \"Controlla il flusso dei contenuti sullo schermo, da sinistra a destra o da destra a sinistra.\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\"\n      },\n      \"textDirection\": {\n        \"label\": \"Direzione del testo predefinita\",\n        \"hint\": \"Specifica se il testo deve iniziare da sinistra o da destra come impostazione predefinita.\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\",\n        \"auto\": \"AUTO\",\n        \"fallback\": \"Uguale alla direzione del layout\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Caricamento\",\n        \"uploadTheme\": \"Carica tema\",\n        \"description\": \"Carica il tuo tema @:appName utilizzando il pulsante in basso.\",\n        \"loading\": \"Attendi mentre convalidiamo e carichiamo il tuo tema...\",\n        \"uploadSuccess\": \"Il tuo tema è stato caricato correttamente\",\n        \"deletionFailure\": \"Impossibile eliminare il tema. Prova a eliminarlo manualmente.\",\n        \"filePickerDialogTitle\": \"Scegli un file .flowy_plugin\",\n        \"urlUploadFailure\": \"Impossibile aprire l'URL: {}\",\n        \"failure\": \"Il tema che è stato caricato aveva un formato non valido.\"\n      },\n      \"theme\": \"Tema\",\n      \"builtInsLabel\": \"Temi incorporati\",\n      \"pluginsLabel\": \"Plugin\",\n      \"dateFormat\": {\n        \"label\": \"Formato data\",\n        \"local\": \"Locale\",\n        \"us\": \"US\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"Amichevole\",\n        \"dmy\": \"G/M/A\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Formato orario\",\n        \"twelveHour\": \"Dodici ore\",\n        \"twentyFourHour\": \"Ventiquattro ore\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"Mostra la finestra di dialogo per la denominazione durante la creazione di una pagina\"\n    },\n    \"files\": {\n      \"copy\": \"copia\",\n      \"defaultLocation\": \"Leggi i file e la posizione di archiviazione dei dati\",\n      \"exportData\": \"Esporta i tuoi dati\",\n      \"doubleTapToCopy\": \"Tocca due volte per copiare il percorso\",\n      \"restoreLocation\": \"Ripristina nel percorso predefinito di @:appName\",\n      \"customizeLocation\": \"Apri un'altra cartella\",\n      \"restartApp\": \"Riavvia l'app per rendere effettive le modifiche.\",\n      \"exportDatabase\": \"Esporta banca dati\",\n      \"selectFiles\": \"Seleziona i file che devono essere esportati\",\n      \"selectAll\": \"Seleziona tutto\",\n      \"deselectAll\": \"Deselezionare tutto\",\n      \"createNewFolder\": \"Crea una nuova cartella\",\n      \"createNewFolderDesc\": \"Dicci dove vuoi salvare i tuoi dati\",\n      \"defineWhereYourDataIsStored\": \"Definisci dove sono archiviati i tuoi dati\",\n      \"open\": \"Aprire\",\n      \"openFolder\": \"Apri una cartella esistente\",\n      \"openFolderDesc\": \"Leggilo e scrivilo nella tua cartella @:appName esistente\",\n      \"folderHintText\": \"nome della cartella\",\n      \"location\": \"Creazione di una nuova cartella\",\n      \"locationDesc\": \"Scegli un nome per la cartella dei dati di @:appName\",\n      \"browser\": \"Navigare\",\n      \"create\": \"Creare\",\n      \"set\": \"Impostato\",\n      \"folderPath\": \"Percorso per memorizzare la tua cartella\",\n      \"locationCannotBeEmpty\": \"Il percorso non può essere vuoto\",\n      \"pathCopiedSnackbar\": \"Percorso di archiviazione file copiato negli appunti!\",\n      \"changeLocationTooltips\": \"Modificare la directory dei dati\",\n      \"change\": \"Modifica\",\n      \"openLocationTooltips\": \"Apri un'altra directory di dati\",\n      \"openCurrentDataFolder\": \"Apre la directory dei dati corrente\",\n      \"recoverLocationTooltips\": \"Ripristina la directory dei dati predefinita di @:appName\",\n      \"exportFileSuccess\": \"Esporta file con successo!\",\n      \"exportFileFail\": \"File di esportazione non riuscito!\",\n      \"export\": \"Esportare\"\n    },\n    \"user\": {\n      \"name\": \"Nome\",\n      \"email\": \"E-mail\",\n      \"tooltipSelectIcon\": \"Seleziona l'icona\",\n      \"selectAnIcon\": \"Seleziona un'icona\",\n      \"pleaseInputYourOpenAIKey\": \"inserisci la tua chiave AI\",\n      \"clickToLogout\": \"Fare clic per disconnettere l'utente corrente\",\n      \"pleaseInputYourStabilityAIKey\": \"per favore inserisci la tua chiave Stability AI\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"Informazione personale\",\n      \"username\": \"Nome utente\",\n      \"usernameEmptyError\": \"Il nome utente non può essere vuoto\",\n      \"about\": \"Informazioni\",\n      \"pushNotifications\": \"Notifiche push\",\n      \"support\": \"Supporto\",\n      \"joinDiscord\": \"Unisciti a noi su Discord\",\n      \"privacyPolicy\": \"Politica sulla riservatezza\",\n      \"userAgreement\": \"Accordo per gli utenti\",\n      \"termsAndConditions\": \"Termini e Condizioni\",\n      \"userprofileError\": \"Impossibile caricare il profilo utente\",\n      \"userprofileErrorDescription\": \"Prova a disconnetterti e ad accedere nuovamente per verificare se il problema persiste.\",\n      \"selectLayout\": \"Seleziona disposizione\",\n      \"selectStartingDay\": \"Seleziona il giorno di inizio\",\n      \"version\": \"Versione\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"Scorciatoie\",\n      \"command\": \"Comando\",\n      \"keyBinding\": \"Combinazione\",\n      \"addNewCommand\": \"Aggiungi un nuovo comando\",\n      \"updateShortcutStep\": \"Premi la combinazione di tasti e poi premi INVIO\",\n      \"shortcutIsAlreadyUsed\": \"Questa scorciatoia è già usata per: {conflict}\",\n      \"resetToDefault\": \"Ripristina le combinazioni di default\",\n      \"couldNotLoadErrorMsg\": \"Impossibile caricare le scorciatoie, Riprova.\",\n      \"couldNotSaveErrorMsg\": \"Impossibile salvare le scorciatoie, Riprova.\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Sei sicuro di voler eliminare questa vista?\",\n    \"createView\": \"Nuovo\",\n    \"title\": {\n      \"placeholder\": \"Senza titolo\"\n    },\n    \"settings\": {\n      \"filter\": \"Filtro\",\n      \"sort\": \"Ordinare\",\n      \"sortBy\": \"Ordina per\",\n      \"properties\": \"Proprietà\",\n      \"reorderPropertiesTooltip\": \"Trascina per riordinare le proprietà\",\n      \"group\": \"Gruppo\",\n      \"addFilter\": \"Aggiungi filtro\",\n      \"deleteFilter\": \"Elimina filtro\",\n      \"filterBy\": \"Filtra per...\",\n      \"typeAValue\": \"Digita un valore...\",\n      \"layout\": \"Disposizione\",\n      \"databaseLayout\": \"Disposizione\",\n      \"editView\": \"Modifica vista\",\n      \"boardSettings\": \"Impostazioni della bacheca\",\n      \"calendarSettings\": \"Impostazioni del calendario\",\n      \"createView\": \"Nuova vista\",\n      \"duplicateView\": \"Duplica vista\",\n      \"deleteView\": \"Elimina vista\",\n      \"numberOfVisibleFields\": \"{} mostrato\",\n      \"viewList\": \"Viste del database\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Contiene\",\n      \"doesNotContain\": \"Non contiene\",\n      \"endsWith\": \"Finisce con\",\n      \"startWith\": \"Inizia con\",\n      \"is\": \"È\",\n      \"isNot\": \"Non è\",\n      \"isEmpty\": \"È vuoto\",\n      \"isNotEmpty\": \"Non è vuoto\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Non\",\n        \"startWith\": \"Inizia con\",\n        \"endWith\": \"Finisce con\",\n        \"isEmpty\": \"è vuoto\",\n        \"isNotEmpty\": \"non è vuoto\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Controllato\",\n      \"isUnchecked\": \"Deselezionato\",\n      \"choicechipPrefix\": {\n        \"is\": \"È\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"è completo\",\n      \"isIncomplted\": \"è incompleto\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"È\",\n      \"isNot\": \"Non è\",\n      \"contains\": \"Contiene\",\n      \"doesNotContain\": \"Non contiene\",\n      \"isEmpty\": \"È vuoto\",\n      \"isNotEmpty\": \"Non è vuoto\"\n    },\n    \"dateFilter\": {\n      \"is\": \"È\",\n      \"before\": \"È prima\",\n      \"after\": \"È dopo\",\n      \"onOrBefore\": \"È in corrispondenza o prima\",\n      \"onOrAfter\": \"È in corrispondenza o dopo\",\n      \"between\": \"È tra\",\n      \"empty\": \"È vuoto\",\n      \"notEmpty\": \"Non è vuoto\"\n    },\n    \"numberFilter\": {\n      \"equal\": \"Uguale\",\n      \"notEqual\": \"Non è uguale\",\n      \"lessThan\": \"È meno di\",\n      \"greaterThan\": \"È maggiore di\",\n      \"lessThanOrEqualTo\": \"È inferiore o uguale a\",\n      \"greaterThanOrEqualTo\": \"È maggiore o uguale a\",\n      \"isEmpty\": \"È vuoto\",\n      \"isNotEmpty\": \"Non è vuoto\"\n    },\n    \"field\": {\n      \"hide\": \"Nascondere\",\n      \"show\": \"Mostra\",\n      \"insertLeft\": \"Inserisci a sinistra\",\n      \"insertRight\": \"Inserisci a destra\",\n      \"duplicate\": \"Duplicare\",\n      \"delete\": \"Eliminare\",\n      \"textFieldName\": \"Testo\",\n      \"checkboxFieldName\": \"Casella di controllo\",\n      \"dateFieldName\": \"Data\",\n      \"updatedAtFieldName\": \"Ora dell'ultima modifica\",\n      \"createdAtFieldName\": \"Tempo creato\",\n      \"numberFieldName\": \"Numeri\",\n      \"singleSelectFieldName\": \"Selezionare\",\n      \"multiSelectFieldName\": \"Selezione multipla\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Lista di controllo\",\n      \"numberFormat\": \"Formato numerico\",\n      \"dateFormat\": \"Formato data\",\n      \"includeTime\": \"Includi il tempo\",\n      \"isRange\": \"Data di fine\",\n      \"dateFormatFriendly\": \"Mese giorno anno\",\n      \"dateFormatISO\": \"Anno mese giorno\",\n      \"dateFormatLocal\": \"Mese giorno anno\",\n      \"dateFormatUS\": \"Anno mese giorno\",\n      \"dateFormatDayMonthYear\": \"Giorno mese Anno\",\n      \"timeFormat\": \"Formato orario\",\n      \"invalidTimeFormat\": \"Formato non valido\",\n      \"timeFormatTwelveHour\": \"12 ore\",\n      \"timeFormatTwentyFourHour\": \"24 ore\",\n      \"clearDate\": \"Pulisci data\",\n      \"dateTime\": \"Data e ora\",\n      \"failedToLoadDate\": \"Impossibile caricare il valore della data\",\n      \"selectTime\": \"Seleziona l'ora\",\n      \"selectDate\": \"Seleziona la data\",\n      \"visibility\": \"Visibilità\",\n      \"propertyType\": \"Tipo di proprietà\",\n      \"addSelectOption\": \"Aggiungi un'opzione\",\n      \"typeANewOption\": \"Digita una nuova opzione\",\n      \"optionTitle\": \"Opzioni\",\n      \"addOption\": \"Aggiungi opzione\",\n      \"editProperty\": \"Modifica proprietà\",\n      \"newProperty\": \"Nuova proprietà\",\n      \"deleteFieldPromptMessage\": \"Sei sicuro? Questa proprietà verrà eliminata\",\n      \"newColumn\": \"Nuova colonna\",\n      \"format\": \"Formato\",\n      \"reminderOnDateTooltip\": \"Questa cella ha un promemoria programmato\",\n      \"optionAlreadyExist\": \"L'opzione esiste già\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Aggiungi un nuovo campo\",\n      \"fieldDragElementTooltip\": \"Fare clic per aprire il menu\",\n      \"showHiddenFields\": {\n        \"one\": \"Mostra {count} campo nascosto\",\n        \"many\": \"Mostra {count} campi nascosti\",\n        \"other\": \"Mostra {count} campi nascosti\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"Nascondi {count} campo nascosto\",\n        \"many\": \"Nascondi {count} campi nascosti\",\n        \"other\": \"Nascondi {count} campi nascosti\"\n      }\n    },\n    \"sort\": {\n      \"ascending\": \"Ascendente\",\n      \"descending\": \"Discendente\",\n      \"cannotFindCreatableField\": \"Impossibile trovare un campo adatto per l'ordinamento\",\n      \"deleteAllSorts\": \"Elimina tutti gli ordinamenti\",\n      \"addSort\": \"Aggiungi ordinamento\",\n      \"removeSorting\": \"Si desidera rimuovere l'ordinamento?\",\n      \"deleteSort\": \"Elimina ordinamento\"\n    },\n    \"row\": {\n      \"duplicate\": \"Duplicare\",\n      \"delete\": \"Eliminare\",\n      \"titlePlaceholder\": \"Senza titolo\",\n      \"textPlaceholder\": \"Vuoto\",\n      \"copyProperty\": \"Proprietà copiata negli appunti\",\n      \"count\": \"Contare\",\n      \"newRow\": \"Nuova fila\",\n      \"action\": \"Azione\",\n      \"add\": \"Fai clic su Aggiungi qui sotto\",\n      \"drag\": \"Trascina per spostare\",\n      \"dragAndClick\": \"Trascina per spostare, fai clic per aprire il menu\",\n      \"insertRecordAbove\": \"Inserisci registrazione sopra\",\n      \"insertRecordBelow\": \"Inserisci registazione sotto\"\n    },\n    \"selectOption\": {\n      \"create\": \"Creare\",\n      \"purpleColor\": \"Viola\",\n      \"pinkColor\": \"Rosa\",\n      \"lightPinkColor\": \"Rosa chiaro\",\n      \"orangeColor\": \"Arancia\",\n      \"yellowColor\": \"Giallo\",\n      \"limeColor\": \"Lime\",\n      \"greenColor\": \"Verde\",\n      \"aquaColor\": \"Acqua\",\n      \"blueColor\": \"Blu\",\n      \"deleteTag\": \"Elimina etichetta\",\n      \"colorPanelTitle\": \"Colori\",\n      \"panelTitle\": \"Seleziona un'opzione o creane una\",\n      \"searchOption\": \"Cerca un'opzione\",\n      \"searchOrCreateOption\": \"Cerca o crea un'opzione...\",\n      \"createNew\": \"Crea un nuovo\",\n      \"orSelectOne\": \"Oppure seleziona un'opzione\",\n      \"typeANewOption\": \"Digita una nuova opzione\",\n      \"tagName\": \"Nome dell'etichetta\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"Descrizione dell'attività\",\n      \"addNew\": \"Aggiungi un elemento\",\n      \"submitNewTask\": \"Crea\",\n      \"hideComplete\": \"Nascondi le attività completate\",\n      \"showComplete\": \"Mostra tutte le attività\"\n    },\n    \"url\": {\n      \"launch\": \"Apri nel browser\",\n      \"copy\": \"Copia l'URL\"\n    },\n    \"menuName\": \"Griglia\",\n    \"referencedGridPrefix\": \"Vista di\",\n    \"calculate\": \"Calcolare\",\n    \"calculationTypeLabel\": {\n      \"average\": \"Media\",\n      \"max\": \"Massimo\",\n      \"median\": \"Medio\",\n      \"min\": \"Minimo\",\n      \"sum\": \"Somma\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"Documento\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"13:00\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Seleziona una bacheca a cui collegarti\",\n        \"createANewBoard\": \"Crea una nuova bacheca\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Seleziona una griglia a cui collegarti\",\n        \"createANewGrid\": \"Crea una nuova griglia\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Seleziona un calendario a cui collegarti\",\n        \"createANewCalendar\": \"Crea un nuovo calendario\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"Selezionare un documento a cui collegare\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Contorno\",\n      \"codeBlock\": \"Blocco di codice\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Bacheca referenziata\",\n      \"referencedGrid\": \"Griglia di riferimento\",\n      \"referencedCalendar\": \"Calendario referenziato\",\n      \"referencedDocument\": \"Documento riferito\",\n      \"autoGeneratorMenuItemName\": \"Scrittore AI\",\n      \"autoGeneratorTitleName\": \"AI: chiedi all'AI di scrivere qualsiasi cosa...\",\n      \"autoGeneratorLearnMore\": \"Saperne di più\",\n      \"autoGeneratorGenerate\": \"creare\",\n      \"autoGeneratorHintText\": \"Chiedi a AI...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Impossibile ottenere la chiave AI\",\n      \"autoGeneratorRewrite\": \"Riscrivere\",\n      \"smartEdit\": \"Assistenti AI\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"Correggi l'ortografia\",\n      \"warning\": \"⚠️ Le risposte AI possono essere imprecise o fuorvianti.\",\n      \"smartEditSummarize\": \"Riassumere\",\n      \"smartEditImproveWriting\": \"Migliora la scrittura\",\n      \"smartEditMakeLonger\": \"Rendi più lungo\",\n      \"smartEditCouldNotFetchResult\": \"Impossibile recuperare il risultato da AI\",\n      \"smartEditCouldNotFetchKey\": \"Impossibile recuperare la chiave AI\",\n      \"smartEditDisabled\": \"Connetti AI in Impostazioni\",\n      \"discardResponse\": \"Vuoi scartare le risposte AI?\",\n      \"createInlineMathEquation\": \"Crea un'equazione\",\n      \"fonts\": \"Caratteri\",\n      \"emoji\": \"Emoji\",\n      \"toggleList\": \"Elenco alternato\",\n      \"quoteList\": \"Elenco citazioni\",\n      \"numberedList\": \"Elenco numerato\",\n      \"bulletedList\": \"Elenco puntato\",\n      \"todoList\": \"Lista di cose da fare\",\n      \"cover\": {\n        \"changeCover\": \"Cambia copertina\",\n        \"colors\": \"Colori\",\n        \"images\": \"immagini\",\n        \"clearAll\": \"Cancella tutto\",\n        \"abstract\": \"Astratto\",\n        \"addCover\": \"Aggiungi copertina\",\n        \"addLocalImage\": \"Aggiungi immagine locale\",\n        \"invalidImageUrl\": \"URL dell'immagine non valido\",\n        \"failedToAddImageToGallery\": \"Impossibile aggiungere l'immagine alla galleria\",\n        \"enterImageUrl\": \"Inserisci l'URL dell'immagine\",\n        \"add\": \"Aggiungere\",\n        \"back\": \"Indietro\",\n        \"saveToGallery\": \"Salva nella galleria\",\n        \"removeIcon\": \"Rimuovi icona\",\n        \"pasteImageUrl\": \"Incolla l'URL dell'immagine\",\n        \"or\": \"O\",\n        \"pickFromFiles\": \"Scegli dai file\",\n        \"couldNotFetchImage\": \"Impossibile recuperare l'immagine\",\n        \"imageSavingFailed\": \"Salvataggio dell'immagine non riuscito\",\n        \"addIcon\": \"Aggiungi icona\",\n        \"changeIcon\": \"Cambia icona\",\n        \"coverRemoveAlert\": \"Verrà rimosso dalla copertura dopo essere stato eliminato.\",\n        \"alertDialogConfirmation\": \"Sei sicuro di voler continuare?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"Equazione matematica\",\n        \"addMathEquation\": \"Aggiungi equazione matematica\",\n        \"editMathEquation\": \"Modifica equazione matematica\"\n      },\n      \"optionAction\": {\n        \"click\": \"Clic\",\n        \"toOpenMenu\": \" per aprire il menu\",\n        \"delete\": \"Eliminare\",\n        \"duplicate\": \"Duplicare\",\n        \"turnInto\": \"Trasforma in\",\n        \"moveUp\": \"Andare avanti\",\n        \"moveDown\": \"Abbassati\",\n        \"color\": \"Colore\",\n        \"align\": \"Allineare\",\n        \"left\": \"Sinistra\",\n        \"center\": \"Centro\",\n        \"right\": \"Giusto\",\n        \"defaultColor\": \"Predefinito\"\n      },\n      \"image\": {\n        \"addAnImage\": \"Aggiungi un'immagine\",\n        \"copiedToPasteBoard\": \"Il link dell'immagine è stato copiato negli appunti\",\n        \"imageUploadFailed\": \"Caricamento dell'immagine non riuscito\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"Il link è stato copiato negli appunti\",\n        \"convertToLink\": \"Convertire in link da incorporare\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Aggiungi intestazioni per creare un sommario.\",\n        \"noMatchHeadings\": \"Non sono stati trovati titoli corrispondenti.\"\n      },\n      \"table\": {\n        \"addAfter\": \"Aggiungi dopo\",\n        \"addBefore\": \"Aggiungi prima\",\n        \"delete\": \"Cancella\",\n        \"clear\": \"Rimuovi il contenuto\",\n        \"duplicate\": \"Duplica\",\n        \"bgColor\": \"Colore di sfondo\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Copia\",\n        \"cut\": \"Taglia\",\n        \"paste\": \"Incolla\"\n      },\n      \"action\": \"Azioni\",\n      \"database\": {\n        \"selectDataSource\": \"Seleziona fonte dati\",\n        \"noDataSource\": \"Nessuna fonte dati\",\n        \"selectADataSource\": \"Seleziona una fonte dati\",\n        \"toContinue\": \"continuare\",\n        \"newDatabase\": \"Nuova banca dati\",\n        \"linkToDatabase\": \"Collegamento alla banca dati\"\n      },\n      \"date\": \"Data\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Digita '/' per i comandi\"\n    },\n    \"title\": {\n      \"placeholder\": \"Senza titolo\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Fare clic per aggiungere l'immagine\",\n      \"upload\": {\n        \"label\": \"Caricamento\",\n        \"placeholder\": \"Clicca per caricare l'immagine\"\n      },\n      \"url\": {\n        \"label\": \"URL dell'immagine\",\n        \"placeholder\": \"Inserisci l'URL dell'immagine\"\n      },\n      \"ai\": {\n        \"label\": \"Genera immagine da AI\",\n        \"placeholder\": \"Inserisci la richiesta affinché AI generi l'immagine\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Genera immagine da Stability AI\",\n        \"placeholder\": \"Inserisci la richiesta affinché Stability AI generi l'immagine\"\n      },\n      \"support\": \"Il limite della dimensione dell'immagine è di 5 MB. Formati supportati: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Immagine non valida\",\n        \"invalidImageSize\": \"La dimensione dell'immagine deve essere inferiore a 5 MB\",\n        \"invalidImageFormat\": \"Il formato dell'immagine non è supportato. Formati supportati: JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"URL dell'immagine non valido\"\n      },\n      \"embedLink\": {\n        \"label\": \"Incorpora link\",\n        \"placeholder\": \"Incolla o digita un link immagine\"\n      },\n      \"unsplash\": {\n        \"label\": \"Unsplash\"\n      },\n      \"searchForAnImage\": \"Cerca un'immagine\",\n      \"pleaseInputYourOpenAIKey\": \"inserisci la tua chiave AI nella pagina Impostazioni\",\n      \"saveImageToGallery\": \"Salva immagine\",\n      \"failedToAddImageToGallery\": \"Impossibile aggiungere l'immagine alla galleria\",\n      \"successToAddImageToGallery\": \"Immagine aggiunta alla galleria con successo\",\n      \"unableToLoadImage\": \"Impossibile caricare l'immagine\",\n      \"maximumImageSize\": \"La dimensione massima supportata per il caricamento delle immagini è di 10 MB\",\n      \"uploadImageErrorImageSizeTooBig\": \"Le dimensioni dell'immagine devono essere inferiori a 10 MB\",\n      \"imageIsUploading\": \"L'immagine si sta caricando\",\n      \"pleaseInputYourStabilityAIKey\": \"inserisci la chiave Stability AI nella pagina Impostazioni\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Lingua\",\n        \"placeholder\": \"Seleziona la lingua\"\n      }\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Incolla o digita un link\",\n      \"openInNewTab\": \"Apri in una nuova scheda\",\n      \"copyLink\": \"Copia link\",\n      \"removeLink\": \"Rimuovi link\",\n      \"url\": {\n        \"label\": \"URL di collegamento\",\n        \"placeholder\": \"Inserisci l'URL del collegamento\"\n      },\n      \"title\": {\n        \"label\": \"Titolo collegamento\",\n        \"placeholder\": \"Inserisci il titolo del collegamento\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"Menziona una persona, una pagina o una data...\",\n      \"page\": {\n        \"label\": \"Collegamento alla pagina\",\n        \"tooltip\": \"Fare clic per aprire la pagina\"\n      },\n      \"deleted\": \"Eliminato\",\n      \"deletedContent\": \"Questo contenuto non esiste o è stato cancellato\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"Ripristina alle condizioni predefinite\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"La versione attuale non supporta questo blocco.\",\n      \"blockContentHasBeenCopied\": \"Il contenuto del blocco è stato copiato.\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"Nuovo\",\n      \"renameGroupTooltip\": \"Premi per rinominare il gruppo\",\n      \"createNewColumn\": \"Aggiungi un nuovo gruppo\",\n      \"addToColumnTopTooltip\": \"Aggiungi una nuova carta in alto\",\n      \"addToColumnBottomTooltip\": \"Aggiungi una nuova carta in basso\",\n      \"renameColumn\": \"Rinomina\",\n      \"hideColumn\": \"Nascondi\",\n      \"newGroup\": \"Nuovo gruppo\",\n      \"deleteColumn\": \"Elimina\",\n      \"deleteColumnConfirmation\": \"Ciò eliminerà questo gruppo e tutte le carte in esso contenute. Sei sicuro di voler continuare?\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"Gruppi nascosti\",\n      \"collapseTooltip\": \"Nascondi i gruppi nascosti\",\n      \"expandTooltip\": \"Visualizza i gruppi nascosti\"\n    },\n    \"cardDetail\": \"Dettagli della carta\",\n    \"cardDuplicated\": \"La carta è stata duplicata\",\n    \"cardDeleted\": \"La carta è stata eliminata\",\n    \"showOnCard\": \"Mostra sui dettagli della carta\",\n    \"setting\": \"Impostazioni\",\n    \"propertyName\": \"Nome della proprietà\",\n    \"menuName\": \"Bacheca\",\n    \"showUngrouped\": \"Mostra elementi non raggruppati\",\n    \"ungroupedButtonText\": \"Non raggruppato\",\n    \"ungroupedButtonTooltip\": \"Contiene carte che non appartengono a nessun gruppo\",\n    \"ungroupedItemsTitle\": \"Fare clic per aggiungere alla bacheca\",\n    \"groupBy\": \"Raggruppa per\",\n    \"referencedBoardPrefix\": \"Vista di\",\n    \"notesTooltip\": \"Note all'interno\",\n    \"mobile\": {\n      \"editURL\": \"Modifica URL\",\n      \"showGroup\": \"Mostra gruppo\",\n      \"showGroupContent\": \"Sei sicuro di voler mostrare questo gruppo sulla bacheca?\",\n      \"failedToLoad\": \"Impossibile caricare la visualizzazione della bacheca\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Calendario\",\n    \"defaultNewCalendarTitle\": \"Senza titolo\",\n    \"newEventButtonTooltip\": \"Aggiungi un nuovo evento\",\n    \"navigation\": {\n      \"today\": \"Oggi\",\n      \"jumpToday\": \"Vai a Oggi\",\n      \"previousMonth\": \"Il mese scorso\",\n      \"nextMonth\": \"Il prossimo mese\"\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"Non ci sono ancora eventi\",\n      \"emptyBody\": \"Premere il pulsante più per creare un evento in questo giorno.\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Mostra i numeri della settimana\",\n      \"showWeekends\": \"Mostra i fine settimana\",\n      \"firstDayOfWeek\": \"Inizia la settimana\",\n      \"layoutDateField\": \"Layout calendario per\",\n      \"changeLayoutDateField\": \"Modifica del campo di layout\",\n      \"noDateTitle\": \"Nessuna data\",\n      \"unscheduledEventsTitle\": \"Eventi non programmati\",\n      \"clickToAdd\": \"Fare clic per aggiungere al calendario\",\n      \"name\": \"Disposizione del calendario\",\n      \"noDateHint\": \"Gli eventi non programmati verranno visualizzati qui\"\n    },\n    \"referencedCalendarPrefix\": \"Vista di\",\n    \"quickJumpYear\": \"Salta a\"\n  },\n  \"errorDialog\": {\n    \"title\": \"Errore @:appName\",\n    \"howToFixFallback\": \"Ci scusiamo per l'inconveniente! Invia un problema sulla nostra pagina GitHub che descriva il tuo errore.\",\n    \"github\": \"Visualizza su GitHub\"\n  },\n  \"search\": {\n    \"label\": \"Ricerca\",\n    \"placeholder\": {\n      \"actions\": \"Cerca azioni...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Copiato!\",\n      \"fail\": \"Impossibile copiare\"\n    }\n  },\n  \"unSupportBlock\": \"La versione attuale non supporta questo blocco.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Sei sicuro di voler eliminare {pageType}?\",\n    \"deleteContentCaption\": \"se elimini questo {pageType}, puoi ripristinarlo dal cestino.\"\n  },\n  \"colors\": {\n    \"custom\": \"Personalizzato\",\n    \"default\": \"Predefinito\",\n    \"red\": \"Rosso\",\n    \"orange\": \"Arancione\",\n    \"yellow\": \"Giallo\",\n    \"green\": \"Verde\",\n    \"blue\": \"Blu\",\n    \"purple\": \"Viola\",\n    \"pink\": \"Rosa\",\n    \"brown\": \"Marrone\",\n    \"gray\": \"Grigio\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"Emoji\",\n    \"search\": \"Cerca emoji\",\n    \"noRecent\": \"Nessuna emoji recente\",\n    \"noEmojiFound\": \"Nessuna emoji trovata\",\n    \"filter\": \"Filtro\",\n    \"random\": \"Casuale\",\n    \"selectSkinTone\": \"Seleziona la tonalità della pelle\",\n    \"remove\": \"Rimuovi emoji\",\n    \"categories\": {\n      \"people\": \"Persone e corpo\",\n      \"animals\": \"Animali e natura\",\n      \"food\": \"Cibo e bevande\",\n      \"activities\": \"Attività\",\n      \"places\": \"Viaggi e luoghi\",\n      \"objects\": \"Oggetti\",\n      \"symbols\": \"Simboli\",\n      \"flags\": \"Bandiere\",\n      \"nature\": \"Natura\",\n      \"frequentlyUsed\": \"Usate di frequente\"\n    },\n    \"skinTone\": {\n      \"default\": \"Predefinito\",\n      \"light\": \"Chiaro\",\n      \"mediumLight\": \"Medio-chiaro\",\n      \"medium\": \"Medio\",\n      \"mediumDark\": \"Medio-scuro\",\n      \"dark\": \"Scuro\"\n    }\n  },\n  \"inlineActions\": {\n    \"noResults\": \"Nessun risultato\",\n    \"pageReference\": \"Riferimento della pagina\",\n    \"docReference\": \"Riferimento al documento\",\n    \"calReference\": \"Calendario di riferimento\",\n    \"gridReference\": \"Riferimento griglia\",\n    \"date\": \"Data\",\n    \"reminder\": {\n      \"groupTitle\": \"Promemoria\",\n      \"shortKeyword\": \"ricordare\"\n    }\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"Modifica il formato della data e dell'ora nelle impostazioni\",\n    \"dateFormat\": \"Formato data\",\n    \"includeTime\": \"Includere l'orario\",\n    \"isRange\": \"Data di fine\",\n    \"timeFormat\": \"Formato orario\",\n    \"clearDate\": \"Rimuovi data\",\n    \"reminderLabel\": \"Promemoria\",\n    \"selectReminder\": \"Seleziona il promemoria\",\n    \"reminderOptions\": {\n      \"atTimeOfEvent\": \"Ora dell'evento\",\n      \"fiveMinsBefore\": \"5 minuti prima\",\n      \"tenMinsBefore\": \"10 minuti prima\",\n      \"fifteenMinsBefore\": \"15 minuti prima\",\n      \"thirtyMinsBefore\": \"30 minuti prima\",\n      \"oneHourBefore\": \"1 ora prima\",\n      \"twoHoursBefore\": \"2 hours before\",\n      \"onDayOfEvent\": \"Il giorno dell'evento\",\n      \"oneDayBefore\": \"1 giorno prima\",\n      \"twoDaysBefore\": \"2 giorni prima\",\n      \"oneWeekBefore\": \"1 settimana prima\",\n      \"custom\": \"Personalizzato\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"Ieri\",\n    \"today\": \"Oggi\",\n    \"tomorrow\": \"Domani\",\n    \"oneWeek\": \"1 settimana\"\n  },\n  \"notificationHub\": {\n    \"title\": \"Notifiche\",\n    \"mobile\": {\n      \"title\": \"Aggiornamenti\"\n    },\n    \"emptyTitle\": \"Tutto a posto!\",\n    \"emptyBody\": \"Nessuna notifica o azione in sospeso. Goditi la calma.\",\n    \"tabs\": {\n      \"inbox\": \"Posta in arrivo\",\n      \"upcoming\": \"Prossimamente\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"Segna tutto come letto\",\n      \"showAll\": \"Tutto\",\n      \"showUnreads\": \"Non letto\"\n    },\n    \"filters\": {\n      \"ascending\": \"Ascendente\",\n      \"descending\": \"Discendente\",\n      \"groupByDate\": \"Raggruppa per data\",\n      \"showUnreadsOnly\": \"Mostra solo non letti\",\n      \"resetToDefault\": \"Riportare alle condizioni predefinite\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"Promemoria\",\n    \"message\": \"Ricordati di controllare questo prima di dimenticarlo!\",\n    \"tooltipDelete\": \"Elimina\",\n    \"tooltipMarkRead\": \"Segna come letto\",\n    \"tooltipMarkUnread\": \"Segna come non letto\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"Cerca\",\n    \"previousMatch\": \"Corrispondenza precedente\",\n    \"nextMatch\": \"Corrispondenza seguente\",\n    \"close\": \"Chiudi\",\n    \"replace\": \"Sostituisci\",\n    \"replaceAll\": \"Sostituisci tutto\",\n    \"noResult\": \"Nessun risultato\",\n    \"caseSensitive\": \"Distinzione tra maiuscole e minuscole\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"Ci dispiace\",\n    \"loadingViewError\": \"Stiamo riscontrando problemi nel caricare questa visualizzazione. Controlla la tua connessione Internet, aggiorna l'app e non esitare a contattare il team se il problema persiste.\"\n  },\n  \"editor\": {\n    \"bold\": \"Grassetto\",\n    \"bulletedList\": \"Elenco puntato\",\n    \"checkbox\": \"Casella di controllo\",\n    \"embedCode\": \"Incorpora codice\",\n    \"heading1\": \"H1\",\n    \"heading2\": \"H2\",\n    \"heading3\": \"H3\",\n    \"highlight\": \"Evidenzia\",\n    \"color\": \"Colore\",\n    \"image\": \"Immagine\",\n    \"date\": \"Data\",\n    \"italic\": \"Corsivo\",\n    \"link\": \"Link\",\n    \"numberedList\": \"Elenco numerato\",\n    \"quote\": \"Citazione\",\n    \"strikethrough\": \"Barrato\",\n    \"text\": \"Testo\",\n    \"underline\": \"Sottolinea\",\n    \"fontColorDefault\": \"Predefinito\",\n    \"fontColorGray\": \"Grigio\",\n    \"fontColorBrown\": \"Marrone\",\n    \"fontColorOrange\": \"Arancione\",\n    \"fontColorYellow\": \"Giallo\",\n    \"fontColorGreen\": \"Verde\",\n    \"fontColorBlue\": \"Blu\",\n    \"fontColorPurple\": \"Viola\",\n    \"fontColorPink\": \"Rosa\",\n    \"fontColorRed\": \"Rosso\",\n    \"backgroundColorDefault\": \"Sfondo predefinito\",\n    \"backgroundColorGray\": \"Sfondo grigio\",\n    \"backgroundColorBrown\": \"Sfondo marrone\",\n    \"backgroundColorOrange\": \"Sfondo arancione\",\n    \"backgroundColorYellow\": \"Sfondo giallo\",\n    \"backgroundColorGreen\": \"Sfondo verde\",\n    \"backgroundColorBlue\": \"Sfondo blu\",\n    \"backgroundColorPurple\": \"Sfondo viola\",\n    \"backgroundColorPink\": \"Sfondo rosa\",\n    \"backgroundColorRed\": \"Sfondo rosso\",\n    \"done\": \"Fatto\",\n    \"cancel\": \"Annulla\",\n    \"tint1\": \"Tinta 1\",\n    \"tint2\": \"Tinta 2\",\n    \"tint3\": \"Tinta 3\",\n    \"tint4\": \"Tinta 4\",\n    \"tint5\": \"Tinta 5\",\n    \"tint6\": \"Tinta 6\",\n    \"tint7\": \"Tinta 7\",\n    \"tint8\": \"Tinta 8\",\n    \"tint9\": \"Tinta 9\",\n    \"lightLightTint1\": \"Viola\",\n    \"lightLightTint2\": \"Rosa\",\n    \"lightLightTint3\": \"Rosa chiaro\",\n    \"lightLightTint4\": \"Arancione\",\n    \"lightLightTint5\": \"Giallo\",\n    \"lightLightTint6\": \"Lime\",\n    \"lightLightTint7\": \"Verde\",\n    \"lightLightTint8\": \"Acqua\",\n    \"lightLightTint9\": \"Blu\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"Intestazione 1\",\n    \"mobileHeading2\": \"Intestazione 2\",\n    \"mobileHeading3\": \"Intestazione 3\",\n    \"textColor\": \"Colore del testo\",\n    \"backgroundColor\": \"Colore di sfondo\",\n    \"addYourLink\": \"Aggiungi il tuo link\",\n    \"openLink\": \"Apri link\",\n    \"copyLink\": \"Copia link\",\n    \"removeLink\": \"Rimuovi link\",\n    \"editLink\": \"Modifica link\",\n    \"linkText\": \"Testo\",\n    \"linkTextHint\": \"Inserisci il testo\",\n    \"linkAddressHint\": \"Inserisci l'URL\",\n    \"highlightColor\": \"Colore evidenziazione\",\n    \"clearHighlightColor\": \"Rimuovi colore evidenziazione\",\n    \"customColor\": \"Colore personalizzato\",\n    \"hexValue\": \"Valore hex\",\n    \"opacity\": \"Opacità\",\n    \"resetToDefaultColor\": \"Ripristina il colore predefinito\",\n    \"ltr\": \"LTR\",\n    \"rtl\": \"RTL\",\n    \"auto\": \"Auto\",\n    \"cut\": \"Taglia\",\n    \"copy\": \"Copia\",\n    \"paste\": \"Incolla\",\n    \"find\": \"Cerca\",\n    \"previousMatch\": \"Corrispondenza precedente\",\n    \"nextMatch\": \"Corrispondenza seguente\",\n    \"closeFind\": \"Chiudi\",\n    \"replace\": \"Sostituisci\",\n    \"replaceAll\": \"Sostituisci tutto\",\n    \"regex\": \"Regex\",\n    \"caseSensitive\": \"Distinzione tra maiuscole e minuscole\",\n    \"uploadImage\": \"Carica immagine\",\n    \"urlImage\": \"URL dell'immagine\",\n    \"incorrectLink\": \"Link errato\",\n    \"upload\": \"Carica\",\n    \"chooseImage\": \"Scegli un'immagine\",\n    \"loading\": \"Caricamento\",\n    \"imageLoadFailed\": \"Impossibile caricare l'immagine\",\n    \"divider\": \"Divisore\",\n    \"table\": \"Tabella\",\n    \"colAddBefore\": \"Aggiungi prima\",\n    \"rowAddBefore\": \"Aggiungi prima\",\n    \"colAddAfter\": \"Aggiungi dopo\",\n    \"rowAddAfter\": \"Aggiungi dopo\",\n    \"colRemove\": \"Rimuovi\",\n    \"rowRemove\": \"Rimuovi\",\n    \"colDuplicate\": \"Duplica\",\n    \"rowDuplicate\": \"Duplica\",\n    \"colClear\": \"Rimuovi contenuto\",\n    \"rowClear\": \"Rimuovi contenuto\",\n    \"slashPlaceHolder\": \"Digita \\\"/\\\" per inserire un blocco o inizia a digitare\",\n    \"typeSomething\": \"Scrivi qualcosa...\",\n    \"quoteListShortForm\": \"Citazione\",\n    \"mathEquationShortForm\": \"Formula\",\n    \"codeBlockShortForm\": \"Codice\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"Nessuna pagina preferita\",\n    \"noFavoriteHintText\": \"Scorri la pagina verso sinistra per aggiungerla ai preferiti\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"Inserisci un / per inserire un blocco o inizia a digitare\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"Da fare\",\n    \"bulletList\": \"Elenco\",\n    \"numberList\": \"Elenco\",\n    \"quote\": \"Citazione\",\n    \"heading\": \"Intestazione {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"Icona della pagina\",\n    \"language\": \"Lingua\",\n    \"font\": \"Font\",\n    \"actions\": \"Azioni\",\n    \"date\": \"Data\",\n    \"addField\": \"Aggiungi campo\",\n    \"userIcon\": \"Icona utente\"\n  },\n  \"noLogFiles\": \"Non ci sono file di log\"\n}"
  },
  {
    "path": "frontend/resources/translations/ja-JP.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"ユーザー\",\n  \"welcomeText\": \"Welcome to @:appName\",\n  \"welcomeTo\": \"ようこそ\",\n  \"githubStarText\": \"Star on GitHub\",\n  \"subscribeNewsletterText\": \"新着情報を受け取る\",\n  \"letsGoButtonText\": \"Let's Go\",\n  \"title\": \"タイトル\",\n  \"youCanAlso\": \"他にもこんなことができます\",\n  \"and\": \"と\",\n  \"failedToOpenUrl\": \"URLを開けませんでした: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"クリックして下に追加してください\",\n    \"addAboveCmd\": \"Alt+クリック\",\n    \"addAboveMacCmd\": \"Option+クリック\",\n    \"addAboveTooltip\": \"上に追加する\",\n    \"dragTooltip\": \"ドラッグして移動\",\n    \"openMenuTooltip\": \"クリックしてメニューを開く\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"新規登録\",\n    \"title\": \"@:appNameに新規登録\",\n    \"getStartedText\": \"はじめる\",\n    \"emptyPasswordError\": \"パスワードを空にはできません\",\n    \"repeatPasswordEmptyError\": \"パスワード（確認用）を空にはできません\",\n    \"unmatchedPasswordError\": \"パスワード（確認用）が一致しません\",\n    \"alreadyHaveAnAccount\": \"すでにアカウントを登録済ですか？\",\n    \"emailHint\": \"メールアドレス\",\n    \"passwordHint\": \"パスワード\",\n    \"repeatPasswordHint\": \"パスワード（確認用）\",\n    \"signUpWith\": \"サインアップ:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"@:appNameにログイン\",\n    \"loginButtonText\": \"ログイン\",\n    \"loginStartWithAnonymous\": \"匿名で続行\",\n    \"continueAnonymousUser\": \"匿名で続行\",\n    \"buttonText\": \"サインイン\",\n    \"signingInText\": \"サインイン中...\",\n    \"forgotPassword\": \"パスワードを忘れましたか？\",\n    \"emailHint\": \"メールアドレス\",\n    \"passwordHint\": \"パスワード\",\n    \"dontHaveAnAccount\": \"アカウントをお持ちでないですか？\",\n    \"createAccount\": \"アカウントを作成\",\n    \"repeatPasswordEmptyError\": \"パスワードを再入力してください\",\n    \"unmatchedPasswordError\": \"パスワードが一致しません\",\n    \"syncPromptMessage\": \"データの同期には時間がかかる場合があります。このページを閉じないでください\",\n    \"or\": \"または\",\n    \"signInWithGoogle\": \"Googleで続行\",\n    \"signInWithGithub\": \"GitHubで続行\",\n    \"signInWithDiscord\": \"Discordで続行\",\n    \"signInWithApple\": \"Appleで続行\",\n    \"continueAnotherWay\": \"別の方法で続行\",\n    \"signUpWithGoogle\": \"Googleで登録\",\n    \"signUpWithGithub\": \"GitHubで登録\",\n    \"signUpWithDiscord\": \"Discordで登録\",\n    \"signInWith\": \"続行方法:\",\n    \"signInWithEmail\": \"メールで続行\",\n    \"signInWithMagicLink\": \"続行\",\n    \"signUpWithMagicLink\": \"Magic Linkで登録\",\n    \"pleaseInputYourEmail\": \"メールアドレスを入力してください\",\n    \"settings\": \"設定\",\n    \"magicLinkSent\": \"Magic Linkが送信されました！\",\n    \"invalidEmail\": \"有効なメールアドレスを入力してください\",\n    \"alreadyHaveAnAccount\": \"すでにアカウントをお持ちですか？\",\n    \"logIn\": \"ログイン\",\n    \"generalError\": \"エラーが発生しました。後でもう一度お試しください\",\n    \"limitRateError\": \"セキュリティのため、Magic Linkは60秒に1回しかリクエストできません\",\n    \"magicLinkSentDescription\": \"Magic Linkがあなたのメールアドレスに送信されました。リンクをクリックしてログインを完了してください。リンクは5分後に無効になります。\",\n    \"anonymous\": \"匿名\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"ワークスペースを選択\",\n    \"defaultName\": \"私のワークスペース\",\n    \"create\": \"ワークスペースを作成\",\n    \"new\": \"新しいワークスペース\",\n    \"importFromNotion\": \"Notionからインポート\",\n    \"learnMore\": \"もっと詳しく知る\",\n    \"reset\": \"ワークスペースをリセット\",\n    \"renameWorkspace\": \"ワークスペースの名前を変更\",\n    \"workspaceNameCannotBeEmpty\": \"ワークスペース名は空にできません\",\n    \"resetWorkspacePrompt\": \"ワークスペースをリセットすると、すべてのページとデータが削除されます。本当にリセットしますか？代わりに、サポートチームに連絡してワークスペースを復元することもできます。\",\n    \"hint\": \"ワークスペース\",\n    \"notFoundError\": \"ワークスペースが見つかりません\",\n    \"failedToLoad\": \"問題が発生しました！ワークスペースの読み込みに失敗しました。@:appNameの開いているインスタンスをすべて閉じて、再試行してください。\",\n    \"errorActions\": {\n      \"reportIssue\": \"問題を報告\",\n      \"reportIssueOnGithub\": \"Githubで問題を報告\",\n      \"exportLogFiles\": \"ログファイルをエクスポート\",\n      \"reachOut\": \"Discordで連絡\"\n    },\n    \"menuTitle\": \"ワークスペース\",\n    \"deleteWorkspaceHintText\": \"ワークスペースを削除してもよろしいですか？この操作は元に戻せません。公開したページはすべて非公開になります。\",\n    \"createSuccess\": \"ワークスペースが正常に作成されました\",\n    \"createFailed\": \"ワークスペースの作成に失敗しました\",\n    \"createLimitExceeded\": \"アカウントで許可されている最大数のワークスペースに達しました。追加のワークスペースが必要な場合は、Githubでリクエストしてください。\",\n    \"deleteSuccess\": \"ワークスペースが正常に削除されました\",\n    \"deleteFailed\": \"ワークスペースの削除に失敗しました\",\n    \"openSuccess\": \"ワークスペースを正常に開きました\",\n    \"openFailed\": \"ワークスペースを開くことができませんでした\",\n    \"renameSuccess\": \"ワークスペース名が正常に変更されました\",\n    \"renameFailed\": \"ワークスペース名の変更に失敗しました\",\n    \"updateIconSuccess\": \"ワークスペースアイコンを正常に更新しました\",\n    \"updateIconFailed\": \"ワークスペースアイコンの更新に失敗しました\",\n    \"cannotDeleteTheOnlyWorkspace\": \"ワークスペースは削除できません\",\n    \"fetchWorkspacesFailed\": \"ワークスペースの取得に失敗しました\",\n    \"leaveCurrentWorkspace\": \"ワークスペースを離れる\",\n    \"leaveCurrentWorkspacePrompt\": \"現在のワークスペースから退出してもよろしいですか？\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"共有\",\n    \"workInProgress\": \"近日公開\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"クリップボードにコピー\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"リンクをコピー\",\n    \"publishToTheWeb\": \"Webに公開\",\n    \"publishToTheWebHint\": \"AppFlowyでウェブサイトを作成\",\n    \"publish\": \"公開\",\n    \"unPublish\": \"非公開\",\n    \"visitSite\": \"サイトを訪問\",\n    \"exportAsTab\": \"エクスポート形式\",\n    \"publishTab\": \"公開\",\n    \"shareTab\": \"共有\",\n    \"publishOnAppFlowy\": \"AppFlowyで公開\",\n    \"shareTabTitle\": \"コラボレーションに招待\",\n    \"shareTabDescription\": \"誰とでも簡単にコラボレーション\",\n    \"copyLinkSuccess\": \"リンクをクリップボードにコピーしました\",\n    \"copyShareLink\": \"共有リンクをコピー\",\n    \"copyLinkFailed\": \"リンクをクリップボードにコピーできませんでした\",\n    \"copyLinkToBlockSuccess\": \"ブロックリンクをクリップボードにコピーしました\",\n    \"copyLinkToBlockFailed\": \"ブロックリンクをクリップボードにコピーできませんでした\",\n    \"manageAllSites\": \"すべてのサイトを管理する\",\n    \"updatePathName\": \"パス名を更新\"\n  },\n  \"moreAction\": {\n    \"small\": \"小\",\n    \"medium\": \"中\",\n    \"large\": \"大\",\n    \"fontSize\": \"フォントサイズ\",\n    \"import\": \"インポート\",\n    \"moreOptions\": \"その他のオプション\",\n    \"wordCount\": \"単語数: {}\",\n    \"charCount\": \"文字数: {}\",\n    \"createdAt\": \"作成日: {}\",\n    \"deleteView\": \"削除\",\n    \"duplicateView\": \"複製\",\n    \"wordCountLabel\": \"単語数：\",\n    \"charCountLabel\": \"文字数：\",\n    \"createdAtLabel\": \"作成日：\",\n    \"syncedAtLabel\": \"同期済み：\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"テキストとマークダウン\",\n    \"documentFromV010\": \"v0.1.0 以降のドキュメント\",\n    \"databaseFromV010\": \"v0.1.0 以降のデータベース\",\n    \"notionZip\": \"Notion エクスポートされた Zip ファイル\",\n    \"csv\": \"CSV\",\n    \"database\": \"データベース\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"名前を変更\",\n    \"delete\": \"削除\",\n    \"duplicate\": \"複製\",\n    \"unfavorite\": \"お気に入りから削除\",\n    \"favorite\": \"お気に入りに追加\",\n    \"openNewTab\": \"新しいタブで開く\",\n    \"moveTo\": \"移動\",\n    \"addToFavorites\": \"お気に入りに追加\",\n    \"copyLink\": \"リンクをコピー\",\n    \"changeIcon\": \"アイコンを変更\",\n    \"collapseAllPages\": \"すべてのサブページを折りたたむ\",\n    \"movePageTo\": \"ページを移動\",\n    \"move\": \"動く\"\n  },\n  \"blankPageTitle\": \"空白のページ\",\n  \"newPageText\": \"新しいページ\",\n  \"newDocumentText\": \"新しいドキュメント\",\n  \"newGridText\": \"新しいグリッド\",\n  \"newCalendarText\": \"新しいカレンダー\",\n  \"newBoardText\": \"新しいボード\",\n  \"chat\": {\n    \"newChat\": \"AIチャット\",\n    \"inputMessageHint\": \"@:appName AIに質問\",\n    \"inputLocalAIMessageHint\": \"@:appName ローカルAIに質問\",\n    \"unsupportedCloudPrompt\": \"この機能は@:appName Cloudでのみ利用可能です\",\n    \"relatedQuestion\": \"関連質問\",\n    \"serverUnavailable\": \"サービスが一時的に利用できません。後でもう一度お試しください。\",\n    \"aiServerUnavailable\": \"🌈 おっと！ 🌈 ユニコーンが返答を食べちゃいました。再試行してください！\",\n    \"retry\": \"リトライ\",\n    \"clickToRetry\": \"再試行をクリック\",\n    \"regenerateAnswer\": \"回答を再生成\",\n    \"question1\": \"タスク管理にKanbanを使用する方法\",\n    \"question2\": \"GTDメソッドを説明してください\",\n    \"question3\": \"なぜRustを使うのか\",\n    \"question4\": \"キッチンにある材料のレシピ\",\n    \"question5\": \"マイページ用のイラストを作成する\",\n    \"question6\": \"来週のやることリストを作成する\",\n    \"aiMistakePrompt\": \"AIは間違いを犯すことがあります。重要な情報は確認してください。\",\n    \"chatWithFilePrompt\": \"ファイルとチャットしますか？\",\n    \"indexFileSuccess\": \"ファイルのインデックス作成が成功しました\",\n    \"inputActionNoPages\": \"ページ結果がありません\",\n    \"referenceSource\": {\n      \"zero\": \"0 件のソースが見つかりました\",\n      \"one\": \"{count} 件のソースが見つかりました\",\n      \"other\": \"{count} 件のソースが見つかりました\"\n    },\n    \"clickToMention\": \"ページをメンションするにはクリック\",\n    \"uploadFile\": \"PDF、md、txtファイルをアップロードしてチャット\",\n    \"questionDetail\": \"こんにちは、{}！今日はどんなお手伝いをしましょうか？\",\n    \"indexingFile\": \"{}をインデックス作成中\",\n    \"generatingResponse\": \"レスポンスの作成\",\n    \"selectSources\": \"ソースを選択\",\n    \"sourcesLimitReached\": \"選択できるのは最上位のドキュメントとその子ドキュメントの3つまでです。\",\n    \"sourceUnsupported\": \"現時点ではデータベースとのチャットはサポートされていません\",\n    \"regenerate\": \"もう一度やり直してください\",\n    \"addToPageButton\": \"ページにメッセージを追加\",\n    \"addToPageTitle\": \"メッセージを追加...\",\n    \"addToNewPageName\": \"\\\"{}\\\"から抽出されたメッセージ\",\n    \"addToNewPageSuccessToast\": \"メッセージが追加されました\",\n    \"openPagePreviewFailedToast\": \"ページを開けませんでした\"\n  },\n  \"trash\": {\n    \"text\": \"ゴミ箱\",\n    \"restoreAll\": \"すべて復元\",\n    \"restore\": \"復元する\",\n    \"deleteAll\": \"すべて削除\",\n    \"pageHeader\": {\n      \"fileName\": \"ファイル名\",\n      \"lastModified\": \"最終更新日\",\n      \"created\": \"作成日\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"ゴミ箱内のすべてのページを削除してもよろしいですか？\",\n      \"caption\": \"この操作は元に戻せません。\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"ゴミ箱内のすべてのページを復元してもよろしいですか？\",\n      \"caption\": \"この操作は元に戻せません。\"\n    },\n    \"restorePage\": {\n      \"title\": \"復元する： {}\",\n      \"caption\": \"このページを復元してもよろしいですか?\"\n    },\n    \"mobile\": {\n      \"actions\": \"ゴミ箱の操作\",\n      \"empty\": \"ゴミ箱にページやスペースはありません\",\n      \"emptyDescription\": \"不要なものをゴミ箱に移動します。\",\n      \"isDeleted\": \"が削除されました\",\n      \"isRestored\": \"が復元されました\"\n    },\n    \"confirmDeleteTitle\": \"このページを完全に削除してもよろしいですか？\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"このページはごみ箱にあります\",\n    \"restore\": \"ページを元に戻す\",\n    \"deletePermanent\": \"削除する\",\n    \"deletePermanentDescription\": \"このページを完全に削除してもよろしいですか? 削除すると元に戻せません。\"\n  },\n  \"dialogCreatePageNameHint\": \"ページ名\",\n  \"questionBubble\": {\n    \"shortcuts\": \"ショートカット\",\n    \"whatsNew\": \"新着情報\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"デバッグ情報\",\n      \"success\": \"デバッグ情報をクリップボードにコピーしました！\",\n      \"fail\": \"デバッグ情報をクリップボードにコピーできませんでした\"\n    },\n    \"feedback\": \"フィードバック\",\n    \"help\": \"ヘルプ & サポート\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"削除、名前の変更、その他...\",\n    \"addPageTooltip\": \"ページを追加\",\n    \"defaultNewPageName\": \"無題\",\n    \"renameDialog\": \"名前を変更\",\n    \"pageNameSuffix\": \"コピー\"\n  },\n  \"noPagesInside\": \"中にページがありません\",\n  \"toolbar\": {\n    \"undo\": \"元に戻す\",\n    \"redo\": \"やり直し\",\n    \"bold\": \"太字\",\n    \"italic\": \"斜体\",\n    \"underline\": \"下線\",\n    \"strike\": \"取り消し線\",\n    \"numList\": \"番号付きリスト\",\n    \"bulletList\": \"箇条書きリスト\",\n    \"checkList\": \"チェックリスト\",\n    \"inlineCode\": \"インラインコード\",\n    \"quote\": \"引用ブロック\",\n    \"header\": \"ヘッダー\",\n    \"highlight\": \"ハイライト\",\n    \"color\": \"カラー\",\n    \"addLink\": \"リンクを追加\",\n    \"link\": \"リンク\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"ライトモードに切り替える\",\n    \"darkMode\": \"ダークモードに切り替える\",\n    \"openAsPage\": \"ページとして開く\",\n    \"addNewRow\": \"新しい行を追加\",\n    \"openMenu\": \"クリックしてメニューを開く\",\n    \"dragRow\": \"長押しして行を並べ替える\",\n    \"viewDataBase\": \"データベースを表示\",\n    \"referencePage\": \"この {name} は参照されています\",\n    \"addBlockBelow\": \"下にブロックを追加\",\n    \"aiGenerate\": \"生成する\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"サイドバーを閉じる\",\n    \"openSidebar\": \"サイドバーを開く\",\n    \"expandSidebar\": \"全ページ展開\",\n    \"personal\": \"個人\",\n    \"private\": \"プライベート\",\n    \"workspace\": \"ワークスペース\",\n    \"favorites\": \"お気に入り\",\n    \"clickToHidePrivate\": \"クリックしてプライベートスペースを非表示にする\\nここで作成したページはあなただけが見えます\",\n    \"clickToHideWorkspace\": \"クリックしてワークスペースを非表示にする\\nここで作成したページはすべてのメンバーが閲覧できます\",\n    \"clickToHidePersonal\": \"クリックして個人スペースを非表示にする\",\n    \"clickToHideFavorites\": \"クリックしてお気に入りスペースを非表示にする\",\n    \"addAPage\": \"新しいページを追加\",\n    \"addAPageToPrivate\": \"プライベートスペースにページを追加\",\n    \"addAPageToWorkspace\": \"ワークスペースにページを追加\",\n    \"recent\": \"最近\",\n    \"today\": \"今日\",\n    \"thisWeek\": \"今週\",\n    \"others\": \"以前のお気に入り\",\n    \"earlier\": \"以前\",\n    \"justNow\": \"たった今\",\n    \"minutesAgo\": \"{count}分前\",\n    \"lastViewed\": \"最後に表示したページ\",\n    \"favoriteAt\": \"お気に入り登録\",\n    \"emptyRecent\": \"最近表示したページはありません\",\n    \"emptyRecentDescription\": \"ページを閲覧すると、ここに簡単にアクセスできるよう表示されます。\",\n    \"emptyFavorite\": \"お気に入りのページはありません\",\n    \"emptyFavoriteDescription\": \"ページをお気に入りに追加すると、ここに表示されて素早くアクセスできます！\",\n    \"removePageFromRecent\": \"最近表示したページから削除しますか？\",\n    \"removeSuccess\": \"削除に成功しました\",\n    \"favoriteSpace\": \"お気に入り\",\n    \"RecentSpace\": \"最近\",\n    \"Spaces\": \"スペース\",\n    \"upgradeToPro\": \"Proプランにアップグレード\",\n    \"upgradeToAIMax\": \"無制限のAIを解放\",\n    \"storageLimitDialogTitle\": \"無料のストレージが不足しています。無制限のストレージを解放するにはアップグレードしてください\",\n    \"aiResponseLimitTitle\": \"無料のAIレスポンスが不足しています。Proプランにアップグレードするか、AIアドオンを購入して無制限のレスポンスを解放してください\",\n    \"aiResponseLimitDialogTitle\": \"AIレスポンスの制限に達しました\",\n    \"aiResponseLimit\": \"無料のAIレスポンスが不足しています。\\n\\n設定 -> プラン -> AI MaxまたはProプランをクリックして、さらにAIレスポンスを取得してください\",\n    \"askOwnerToUpgradeToPro\": \"ワークスペースの無料ストレージが不足しています。ワークスペースのオーナーにProプランへのアップグレードを依頼してください\",\n    \"askOwnerToUpgradeToProIOS\": \"ワークスペースの無料ストレージが不足しています。\",\n    \"askOwnerToUpgradeToAIMax\": \"ワークスペースのAIレスポンスが不足しています。ワークスペースのオーナーにプランのアップグレードかAIアドオンの購入を依頼してください\",\n    \"askOwnerToUpgradeToAIMaxIOS\": \"ワークスペースの無料AIレスポンスが不足しています。\",\n    \"purchaseStorageSpace\": \"ストレージスペースを購入\",\n    \"singleFileProPlanLimitationDescription\": \"無料プランで許可されている最大ファイルアップロードサイズを超えました。より大きなファイルをアップロードするには、プロプランにアップグレードしてください。\",\n    \"purchaseAIResponse\": \"AIレスポンスを購入\",\n    \"askOwnerToUpgradeToLocalAI\": \"ワークスペースのオーナーにオンデバイスAIを有効にするよう依頼してください\",\n    \"upgradeToAILocal\": \"デバイス上でローカルモデルを実行して究極のプライバシーを実現\",\n    \"upgradeToAILocalDesc\": \"PDFとチャットしたり、文章を改善したり、ローカルAIでテーブルを自動入力したりできます\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"ノートをMarkdownにエクスポート\",\n      \"path\": \"Documents/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"連絡先\",\n    \"whatsHappening\": \"今週は何が起こっている？\",\n    \"addContact\": \"連絡先を追加\",\n    \"editContact\": \"連絡先を編集\"\n  },\n  \"button\": {\n    \"ok\": \"OK\",\n    \"confirm\": \"確認\",\n    \"done\": \"完了\",\n    \"cancel\": \"キャンセル\",\n    \"signIn\": \"サインイン\",\n    \"signOut\": \"サインアウト\",\n    \"complete\": \"完了\",\n    \"save\": \"保存\",\n    \"generate\": \"生成\",\n    \"esc\": \"ESC\",\n    \"keep\": \"保持\",\n    \"tryAgain\": \"再試行\",\n    \"discard\": \"破棄\",\n    \"replace\": \"置き換え\",\n    \"insertBelow\": \"下に挿入\",\n    \"insertAbove\": \"上に挿入\",\n    \"upload\": \"アップロード\",\n    \"edit\": \"編集\",\n    \"delete\": \"削除\",\n    \"copy\": \"コピー\",\n    \"duplicate\": \"複製\",\n    \"putback\": \"元に戻す\",\n    \"update\": \"更新\",\n    \"share\": \"共有\",\n    \"removeFromFavorites\": \"お気に入りから削除\",\n    \"removeFromRecent\": \"最近の項目から削除\",\n    \"addToFavorites\": \"お気に入りに追加\",\n    \"favoriteSuccessfully\": \"お気に入りに追加しました\",\n    \"unfavoriteSuccessfully\": \"お気に入りから削除しました\",\n    \"duplicateSuccessfully\": \"複製が成功しました\",\n    \"rename\": \"名前を変更\",\n    \"helpCenter\": \"ヘルプセンター\",\n    \"add\": \"追加\",\n    \"yes\": \"はい\",\n    \"no\": \"いいえ\",\n    \"clear\": \"クリア\",\n    \"remove\": \"削除\",\n    \"dontRemove\": \"削除しない\",\n    \"copyLink\": \"リンクをコピー\",\n    \"align\": \"整列\",\n    \"login\": \"ログイン\",\n    \"logout\": \"ログアウト\",\n    \"deleteAccount\": \"アカウントを削除\",\n    \"back\": \"戻る\",\n    \"signInGoogle\": \"Googleで続行\",\n    \"signInGithub\": \"GitHubで続行\",\n    \"signInDiscord\": \"Discordで続行\",\n    \"more\": \"もっと見る\",\n    \"create\": \"作成\",\n    \"close\": \"閉じる\",\n    \"next\": \"次へ\",\n    \"previous\": \"前へ\",\n    \"submit\": \"送信\",\n    \"download\": \"ダウンロード\",\n    \"backToHome\": \"Homeに戻る\",\n    \"viewing\": \"閲覧\",\n    \"editing\": \"編集\",\n    \"gotIt\": \"わかった\",\n    \"retry\": \"リトライ\",\n    \"uploadFailed\": \"アップロードに失敗しました。\",\n    \"copyLinkOriginal\": \"元のリンクをコピー\"\n  },\n  \"label\": {\n    \"welcome\": \"ようこそ！\",\n    \"firstName\": \"名\",\n    \"middleName\": \"ミドルネーム\",\n    \"lastName\": \"姓\",\n    \"stepX\": \"ステップ {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"アカウントに接続できません\",\n      \"failedMsg\": \"サインインが完了したことをブラウザーで確認してください\"\n    },\n    \"google\": {\n      \"title\": \"Googleでサインイン\",\n      \"instruction1\": \"Googleでのサインインを有効にするためには、Webブラウザーを使用してこのアプリケーションを認証する必要があります。\",\n      \"instruction2\": \"アイコンをクリックするかテキストを選択して、このコードをクリップボードにコピーします。\",\n      \"instruction3\": \"以下のリンク先をブラウザーで開いて、次のコードを入力します。\",\n      \"instruction4\": \"登録が完了したら以下のボタンを押してください。\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"設定\",\n    \"popupMenuItem\": {\n      \"settings\": \"設定\",\n      \"members\": \"メンバー\",\n      \"trash\": \"ゴミ箱\",\n      \"helpAndSupport\": \"ヘルプ & サポート\"\n    },\n    \"sites\": {\n      \"title\": \"サイト\",\n      \"namespaceTitle\": \"名前空間\",\n      \"namespaceDescription\": \"名前空間とホームページを管理する\",\n      \"namespaceHeader\": \"名前空間\",\n      \"homepageHeader\": \"ホームページ\",\n      \"updateNamespace\": \"名前空間を更新する\",\n      \"removeHomepage\": \"ホームページを削除する\",\n      \"selectHomePage\": \"ページを選択\",\n      \"clearHomePage\": \"この名前空間のホームページをクリアする\",\n      \"customUrl\": \"カスタム URL\",\n      \"namespace\": {\n        \"description\": \"この変更は、この名前空間で公開されているすべてのページに適用されます。\",\n        \"tooltip\": \"不適切な名前空間を削除する権利を留保します\",\n        \"updateExistingNamespace\": \"既存の名前空間を更新する\",\n        \"upgradeToPro\": \"ホームページを設定するにはプロプランにアップグレードしてください\",\n        \"redirectToPayment\": \"支払いページにリダイレクトしています...\",\n        \"onlyWorkspaceOwnerCanSetHomePage\": \"ワークスペースの所有者のみがホームページを設定できます\",\n        \"pleaseAskOwnerToSetHomePage\": \"ワークスペースのオーナーにプロプランへのアップグレードを依頼してください\"\n      },\n      \"publishedPage\": {\n        \"title\": \"公開されたすべてのページ\",\n        \"description\": \"公開したページを管理する\",\n        \"page\": \"ページ\",\n        \"pathName\": \"パス名\",\n        \"date\": \"公開日\",\n        \"emptyHinText\": \"このワークスペースには公開されたページはありません\",\n        \"noPublishedPages\": \"公開ページはありません\",\n        \"settings\": \"公開設定\",\n        \"clickToOpenPageInApp\": \"アプリでページを開く\",\n        \"clickToOpenPageInBrowser\": \"ブラウザでページを開く\"\n      },\n      \"error\": {\n        \"failedToGeneratePaymentLink\": \"プロプランの支払いリンクを生成できませんでした\",\n        \"failedToUpdateNamespace\": \"名前空間の更新に失敗しました\",\n        \"proPlanLimitation\": \"名前空間を更新するには、Proプランにアップグレードする必要があります\",\n        \"namespaceAlreadyInUse\": \"名前空間はすでに使用されています。別の名前空間を試してください\",\n        \"invalidNamespace\": \"名前空間が無効です。別の名前空間を試してください\",\n        \"namespaceLengthAtLeast2Characters\": \"名前空間は少なくとも 2 文字の長さである必要があります\",\n        \"onlyWorkspaceOwnerCanUpdateNamespace\": \"ワークスペースの所有者のみが名前空間を更新できます\",\n        \"onlyWorkspaceOwnerCanRemoveHomepage\": \"ワークスペースの所有者のみがホームページを削除できます\",\n        \"setHomepageFailed\": \"ホームページの設定に失敗しました\",\n        \"namespaceTooLong\": \"名前空間が長すぎます。別の名前空間を試してください\",\n        \"namespaceTooShort\": \"名前空間が短すぎます。別の名前空間を試してください。\",\n        \"namespaceIsReserved\": \"名前空間は予約されています。別の名前空間を試してください\",\n        \"updatePathNameFailed\": \"パス名の更新に失敗しました\",\n        \"removeHomePageFailed\": \"ホームページを削除できませんでした\",\n        \"publishNameContainsInvalidCharacters\": \"パス名に無効な文字が含まれています。別のパス名を試してください。\",\n        \"publishNameTooShort\": \"パス名が短すぎます。別のパス名を試してください。\",\n        \"publishNameTooLong\": \"パス名が長すぎます。別のパス名を試してください。\",\n        \"publishNameAlreadyInUse\": \"パス名はすでに使用されています。別のパス名を試してください\",\n        \"namespaceContainsInvalidCharacters\": \"名前空間に無効な文字が含まれています。別の名前空間を試してください。\",\n        \"publishPermissionDenied\": \"公開設定を管理できるのはワークスペースの所有者またはページ発行者のみです。\",\n        \"publishNameCannotBeEmpty\": \"パス名は空にできません。別のパス名を試してください。\"\n      },\n      \"success\": {\n        \"namespaceUpdated\": \"名前空間が正常に更新されました\",\n        \"setHomepageSuccess\": \"ホームページの設定に成功しました\",\n        \"updatePathNameSuccess\": \"パス名が正常に更新されました\",\n        \"removeHomePageSuccess\": \"ホームページを削除しました\"\n      }\n    },\n    \"accountPage\": {\n      \"menuLabel\": \"マイアカウント\",\n      \"title\": \"マイアカウント\",\n      \"general\": {\n        \"title\": \"アカウント名 & プロフィール画像\",\n        \"changeProfilePicture\": \"プロフィール画像を変更\"\n      },\n      \"email\": {\n        \"title\": \"メールアドレス\",\n        \"actions\": {\n          \"change\": \"メールアドレスを変更\"\n        }\n      },\n      \"login\": {\n        \"title\": \"アカウントログイン\",\n        \"loginLabel\": \"ログイン\",\n        \"logoutLabel\": \"ログアウト\"\n      }\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"ワークスペース\",\n      \"title\": \"ワークスペース\",\n      \"description\": \"ワークスペースの外観、テーマ、フォント、テキストレイアウト、日付/時間形式、言語をカスタマイズします。\",\n      \"workspaceName\": {\n        \"title\": \"ワークスペース名\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"ワークスペースアイコン\",\n        \"description\": \"画像をアップロードするか、絵文字を使用してワークスペースにアイコンを設定します。アイコンはサイドバーや通知に表示されます。\"\n      },\n      \"appearance\": {\n        \"title\": \"外観\",\n        \"description\": \"ワークスペースの外観、テーマ、フォント、テキストレイアウト、日付、時間、言語をカスタマイズします。\",\n        \"options\": {\n          \"system\": \"自動\",\n          \"light\": \"ライト\",\n          \"dark\": \"ダーク\"\n        }\n      },\n      \"resetCursorColor\": {\n        \"title\": \"ドキュメントのカーソル色をリセット\",\n        \"description\": \"カーソルの色をリセットしてもよろしいですか？\"\n      },\n      \"resetSelectionColor\": {\n        \"title\": \"ドキュメントの選択色をリセット\",\n        \"description\": \"選択色をリセットしてもよろしいですか？\"\n      },\n      \"resetWidth\": {\n        \"resetSuccess\": \"ドキュメントの幅を正常にリセットしました\"\n      },\n      \"theme\": {\n        \"title\": \"テーマ\",\n        \"description\": \"プリセットテーマを選択するか、独自のカスタムテーマをアップロードします。\",\n        \"uploadCustomThemeTooltip\": \"カスタムテーマをアップロード\"\n      },\n      \"workspaceFont\": {\n        \"title\": \"ワークスペースのフォント\",\n        \"noFontHint\": \"フォントが見つかりません。別のキーワードをお試しください。\"\n      },\n      \"textDirection\": {\n        \"title\": \"テキストの方向\",\n        \"leftToRight\": \"左から右\",\n        \"rightToLeft\": \"右から左\",\n        \"auto\": \"自動\",\n        \"enableRTLItems\": \"RTLツールバー項目を有効にする\"\n      },\n      \"layoutDirection\": {\n        \"title\": \"レイアウトの方向\",\n        \"leftToRight\": \"左から右\",\n        \"rightToLeft\": \"右から左\"\n      },\n      \"dateTime\": {\n        \"title\": \"日付と時間\",\n        \"example\": \"{} at {} ({})\",\n        \"24HourTime\": \"24時間表記\",\n        \"dateFormat\": {\n          \"label\": \"日付形式\",\n          \"local\": \"ローカル\",\n          \"us\": \"米国\",\n          \"iso\": \"ISO\",\n          \"friendly\": \"読み易さ\",\n          \"dmy\": \"日/月/年\"\n        }\n      },\n      \"language\": {\n        \"title\": \"言語\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"ワークスペースの削除\",\n        \"content\": \"このワークスペースを削除してもよろしいですか？この操作は元に戻せません。公開しているページはすべて非公開になります。\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"ワークスペースを退出\",\n        \"content\": \"このワークスペースを退出してもよろしいですか？ワークスペース内のすべてのページやデータへのアクセスを失います。\"\n      },\n      \"manageWorkspace\": {\n        \"title\": \"ワークスペースを管理\",\n        \"leaveWorkspace\": \"ワークスペースを退出\",\n        \"deleteWorkspace\": \"ワークスペースを削除\"\n      }\n    },\n    \"manageDataPage\": {\n      \"menuLabel\": \"データ管理\",\n      \"title\": \"データ管理\",\n      \"description\": \"ローカルストレージデータの管理または既存のデータを@:appNameにインポートします。\",\n      \"dataStorage\": {\n        \"title\": \"ファイル保存場所\",\n        \"tooltip\": \"ファイルが保存されている場所\",\n        \"actions\": {\n          \"change\": \"パスを変更\",\n          \"open\": \"フォルダを開く\",\n          \"openTooltip\": \"現在のデータフォルダの場所を開く\",\n          \"copy\": \"パスをコピー\",\n          \"copiedHint\": \"パスをコピーしました！\",\n          \"resetTooltip\": \"デフォルトの場所にリセット\"\n        },\n        \"resetDialog\": {\n          \"title\": \"本当にリセットしますか？\",\n          \"description\": \"パスをデフォルトのデータ保存場所にリセットしてもデータは削除されません。現在のデータを再インポートする場合、最初に現在の場所のパスをコピーしてください。\"\n        }\n      },\n      \"importData\": {\n        \"title\": \"データのインポート\",\n        \"tooltip\": \"@:appNameのバックアップ/データフォルダからデータをインポート\",\n        \"description\": \"外部の@:appNameデータフォルダからデータをコピー\",\n        \"action\": \"ファイルを参照\"\n      },\n      \"encryption\": {\n        \"title\": \"暗号化\",\n        \"tooltip\": \"データの保存と暗号化方法を管理\",\n        \"descriptionNoEncryption\": \"暗号化をオンにするとすべてのデータが暗号化されます。この操作は元に戻せません。\",\n        \"descriptionEncrypted\": \"データは暗号化されています。\",\n        \"action\": \"データを暗号化\",\n        \"dialog\": {\n          \"title\": \"すべてのデータを暗号化しますか？\",\n          \"description\": \"すべてのデータを暗号化すると、データは安全かつ保護されます。この操作は元に戻せません。本当に続行しますか？\"\n        }\n      },\n      \"cache\": {\n        \"title\": \"キャッシュをクリア\",\n        \"description\": \"画像が読み込まれない、スペース内のページが見つからない、フォントが読み込まれないなどの問題を解決します。データには影響しません。\",\n        \"dialog\": {\n          \"title\": \"キャッシュをクリア\",\n          \"description\": \"画像が読み込まれない、スペース内のページが見つからない、フォントが読み込まれないなどの問題を解決します。データには影響しません。\",\n          \"successHint\": \"キャッシュをクリアしました！\"\n        }\n      },\n      \"data\": {\n        \"fixYourData\": \"データを修復\",\n        \"fixButton\": \"修復\",\n        \"fixYourDataDescription\": \"データに問題がある場合、ここで修復を試みることができます。\"\n      }\n    },\n    \"shortcutsPage\": {\n      \"menuLabel\": \"ショートカット\",\n      \"title\": \"ショートカット\",\n      \"editBindingHint\": \"新しいバインディングを入力\",\n      \"searchHint\": \"検索\",\n      \"actions\": {\n        \"resetDefault\": \"デフォルトにリセット\"\n      },\n      \"errorPage\": {\n        \"message\": \"ショートカットの読み込みに失敗しました: {}\",\n        \"howToFix\": \"もう一度お試しください。問題が続く場合はGitHubでお問い合わせください。\"\n      },\n      \"resetDialog\": {\n        \"title\": \"ショートカットをリセット\",\n        \"description\": \"これにより、すべてのキー設定がデフォルトにリセットされます。この操作は元に戻せません。続行してもよろしいですか？\",\n        \"buttonLabel\": \"リセット\"\n      },\n      \"conflictDialog\": {\n        \"title\": \"{}は現在使用されています\",\n        \"descriptionPrefix\": \"このキー設定は現在 \",\n        \"descriptionSuffix\": \" によって使用されています。このキー設定を置き換えると、{} から削除されます。\",\n        \"confirmLabel\": \"続行\"\n      },\n      \"editTooltip\": \"キー設定の編集を開始するには押してください\",\n      \"keybindings\": {\n        \"toggleToDoList\": \"To-Doリストを切り替え\",\n        \"insertNewParagraphInCodeblock\": \"新しい段落を挿入\",\n        \"pasteInCodeblock\": \"コードブロックに貼り付け\",\n        \"selectAllCodeblock\": \"すべて選択\",\n        \"indentLineCodeblock\": \"行の先頭にスペースを2つ挿入\",\n        \"outdentLineCodeblock\": \"行の先頭のスペースを2つ削除\",\n        \"twoSpacesCursorCodeblock\": \"カーソル位置にスペースを2つ挿入\",\n        \"copy\": \"選択をコピー\",\n        \"paste\": \"コンテンツに貼り付け\",\n        \"cut\": \"選択をカット\",\n        \"alignLeft\": \"テキストを左揃え\",\n        \"alignCenter\": \"テキストを中央揃え\",\n        \"alignRight\": \"テキストを右揃え\",\n        \"undo\": \"元に戻す\",\n        \"redo\": \"やり直し\",\n        \"convertToParagraph\": \"ブロックを段落に変換\",\n        \"backspace\": \"削除\",\n        \"deleteLeftWord\": \"左の単語を削除\",\n        \"deleteLeftSentence\": \"左の文を削除\",\n        \"delete\": \"右の文字を削除\",\n        \"deleteMacOS\": \"左の文字を削除\",\n        \"deleteRightWord\": \"右の単語を削除\",\n        \"moveCursorLeft\": \"カーソルを左に移動\",\n        \"moveCursorBeginning\": \"カーソルを先頭に移動\",\n        \"moveCursorLeftWord\": \"カーソルを左に1単語移動\",\n        \"moveCursorLeftSelect\": \"選択してカーソルを左に移動\",\n        \"moveCursorBeginSelect\": \"選択してカーソルを先頭に移動\",\n        \"moveCursorLeftWordSelect\": \"選択してカーソルを左に1単語移動\",\n        \"moveCursorRight\": \"カーソルを右に移動\",\n        \"moveCursorEnd\": \"カーソルを末尾に移動\",\n        \"moveCursorRightWord\": \"カーソルを右に1単語移動\",\n        \"moveCursorRightSelect\": \"選択してカーソルを右に1つ移動\",\n        \"moveCursorEndSelect\": \"選択してカーソルを末尾に移動\",\n        \"moveCursorRightWordSelect\": \"選択してカーソルを右に1単語移動\",\n        \"moveCursorUp\": \"カーソルを上に移動\",\n        \"moveCursorTopSelect\": \"選択してカーソルを最上部に移動\",\n        \"moveCursorTop\": \"カーソルを最上部に移動\",\n        \"moveCursorUpSelect\": \"選択してカーソルを上に移動\",\n        \"moveCursorBottomSelect\": \"選択してカーソルを最下部に移動\",\n        \"moveCursorBottom\": \"カーソルを最下部に移動\",\n        \"moveCursorDown\": \"カーソルを下に移動\",\n        \"moveCursorDownSelect\": \"選択してカーソルを下に移動\",\n        \"home\": \"最上部にスクロール\",\n        \"end\": \"最下部にスクロール\",\n        \"toggleBold\": \"太字を切り替え\",\n        \"toggleItalic\": \"斜体を切り替え\",\n        \"toggleUnderline\": \"下線を切り替え\",\n        \"toggleStrikethrough\": \"取り消し線を切り替え\",\n        \"toggleCode\": \"インラインコードを切り替え\",\n        \"toggleHighlight\": \"ハイライトを切り替え\",\n        \"showLinkMenu\": \"リンクメニューを表示\",\n        \"openInlineLink\": \"インラインリンクを開く\",\n        \"openLinks\": \"選択されたすべてのリンクを開く\",\n        \"indent\": \"インデント\",\n        \"outdent\": \"アウトデント\",\n        \"exit\": \"編集を終了\",\n        \"pageUp\": \"1ページ上にスクロール\",\n        \"pageDown\": \"1ページ下にスクロール\",\n        \"selectAll\": \"すべて選択\",\n        \"pasteWithoutFormatting\": \"フォーマットなしで貼り付け\",\n        \"showEmojiPicker\": \"絵文字ピッカーを表示\",\n        \"enterInTableCell\": \"表に改行を追加\",\n        \"leftInTableCell\": \"表の左隣のセルに移動\",\n        \"rightInTableCell\": \"表の右隣のセルに移動\",\n        \"upInTableCell\": \"表の上隣のセルに移動\",\n        \"downInTableCell\": \"表の下隣のセルに移動\",\n        \"tabInTableCell\": \"表の次の利用可能なセルに移動\",\n        \"shiftTabInTableCell\": \"表の前の利用可能なセルに移動\",\n        \"backSpaceInTableCell\": \"セルの先頭で停止\"\n      },\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"コードブロックの隣に新しい段落を挿入\",\n        \"codeBlockIndentLines\": \"コードブロック内の行の先頭にスペースを2つ挿入\",\n        \"codeBlockOutdentLines\": \"コードブロック内の行の先頭のスペースを2つ削除\",\n        \"codeBlockAddTwoSpaces\": \"コードブロック内のカーソル位置にスペースを2つ挿入\",\n        \"codeBlockSelectAll\": \"コードブロック内のすべてのコンテンツを選択\",\n        \"codeBlockPasteText\": \"コードブロック内にテキストを貼り付け\",\n        \"textAlignLeft\": \"テキストを左揃え\",\n        \"textAlignCenter\": \"テキストを中央揃え\",\n        \"textAlignRight\": \"テキストを右揃え\"\n      },\n      \"couldNotLoadErrorMsg\": \"ショートカットを読み込めませんでした。もう一度お試しください。\",\n      \"couldNotSaveErrorMsg\": \"ショートカットを保存できませんでした。もう一度お試しください。\"\n    },\n    \"aiPage\": {\n      \"title\": \"AI設定\",\n      \"menuLabel\": \"AI設定\",\n      \"keys\": {\n        \"enableAISearchTitle\": \"AI検索\",\n        \"aiSettingsDescription\": \"AppFlowy AIを駆動するモデルを選択します。GPT 4-o、Claude 3.5、Llama 3.1、Mistral 7Bが含まれます。\",\n        \"loginToEnableAIFeature\": \"AI機能は@:appName Cloudにログインした後に有効になります。@:appNameアカウントがない場合は、「マイアカウント」でサインアップしてください。\",\n        \"llmModel\": \"言語モデル\",\n        \"llmModelType\": \"言語モデルのタイプ\",\n        \"downloadLLMPrompt\": \"{}をダウンロード\",\n        \"downloadAppFlowyOfflineAI\": \"AIオフラインパッケージをダウンロードすると、デバイス上でAIが動作します。続行しますか？\",\n        \"downloadLLMPromptDetail\": \"{}のローカルモデルをダウンロードすると、{}のストレージを使用します。続行しますか？\",\n        \"downloadBigFilePrompt\": \"ダウンロードが完了するまで約10分かかることがあります\",\n        \"downloadAIModelButton\": \"ダウンロード\",\n        \"downloadingModel\": \"ダウンロード中\",\n        \"localAILoaded\": \"ローカルAIモデルが正常に追加され、使用可能です\",\n        \"localAIStart\": \"ローカルAIチャットが開始しています...\",\n        \"localAILoading\": \"ローカルAIチャットモデルを読み込み中...\",\n        \"localAIStopped\": \"ローカルAIが停止しました\",\n        \"failToLoadLocalAI\": \"ローカルAIの起動に失敗しました\",\n        \"restartLocalAI\": \"ローカルAIを再起動\",\n        \"disableLocalAITitle\": \"ローカルAIを無効化\",\n        \"disableLocalAIDescription\": \"ローカルAIを無効にしますか？\",\n        \"localAIToggleTitle\": \"ローカルAIを有効化または無効化\",\n        \"offlineAIInstruction1\": \"次の手順に従って\",\n        \"offlineAIInstruction2\": \"指示\",\n        \"offlineAIInstruction3\": \"でオフラインAIを有効にします。\",\n        \"offlineAIDownload1\": \"AppFlowy AIをまだダウンロードしていない場合は、\",\n        \"offlineAIDownload2\": \"ダウンロード\",\n        \"offlineAIDownload3\": \"してください\",\n        \"activeOfflineAI\": \"アクティブ\",\n        \"downloadOfflineAI\": \"ダウンロード\",\n        \"openModelDirectory\": \"フォルダを開く\"\n      }\n    },\n    \"planPage\": {\n      \"menuLabel\": \"プラン\",\n      \"title\": \"料金プラン\",\n      \"planUsage\": {\n        \"title\": \"プラン使用状況の概要\",\n        \"storageLabel\": \"ストレージ\",\n        \"storageUsage\": \"{} / {} GB\",\n        \"unlimitedStorageLabel\": \"無制限のストレージ\",\n        \"collaboratorsLabel\": \"メンバー\",\n        \"collaboratorsUsage\": \"{} / {}\",\n        \"aiResponseLabel\": \"AIレスポンス\",\n        \"aiResponseUsage\": \"{} / {}\",\n        \"unlimitedAILabel\": \"無制限のAIレスポンス\",\n        \"proBadge\": \"プロ\",\n        \"aiMaxBadge\": \"AI Max\",\n        \"aiOnDeviceBadge\": \"Mac用AIオンデバイス\",\n        \"memberProToggle\": \"メンバーを追加 & 無制限のAI\",\n        \"aiMaxToggle\": \"無制限のAI & 高度なモデルにアクセス\",\n        \"aiOnDeviceToggle\": \"究極のプライバシーのためのローカルAI\",\n        \"aiCredit\": {\n          \"title\": \"@:appName AIクレジットを追加\",\n          \"price\": \"{}\",\n          \"priceDescription\": \"1,000クレジットの価格\",\n          \"purchase\": \"AIを購入\",\n          \"info\": \"ワークスペースごとに1,000 AIクレジットを追加し、より賢く迅速な結果を得るためにAIをワークフローにシームレスに統合します。最大で：\",\n          \"infoItemOne\": \"データベースごとに10,000レスポンス\",\n          \"infoItemTwo\": \"ワークスペースごとに1,000レスポンス\"\n        },\n        \"currentPlan\": {\n          \"bannerLabel\": \"現在のプラン\",\n          \"freeTitle\": \"無料\",\n          \"proTitle\": \"プロ\",\n          \"teamTitle\": \"チーム\",\n          \"freeInfo\": \"個人や2名までのメンバーに最適\",\n          \"proInfo\": \"小規模・中規模チームに最適（最大10名）\",\n          \"teamInfo\": \"生産的かつ整理されたチームに最適\",\n          \"upgrade\": \"プランを変更\",\n          \"canceledInfo\": \"プランはキャンセルされました。{}に無料プランにダウングレードされます。\"\n        },\n        \"addons\": {\n          \"title\": \"アドオン\",\n          \"addLabel\": \"追加\",\n          \"activeLabel\": \"追加済み\",\n          \"aiMax\": {\n            \"title\": \"AI Max\",\n            \"description\": \"GPT-4o、Claude 3.5 Sonnetなどによる無制限のAIレスポンス\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"1ユーザーあたり月額、年間請求\"\n          },\n          \"aiOnDevice\": {\n            \"title\": \"Mac用AIオンデバイス\",\n            \"description\": \"Mistral 7B、LLAMA 3、その他のローカルモデルをマシンで実行\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"1ユーザーあたり月額、年間請求\",\n            \"recommend\": \"M1以上を推奨\"\n          }\n        },\n        \"deal\": {\n          \"bannerLabel\": \"新年キャンペーン！\",\n          \"title\": \"チームを成長させましょう！\",\n          \"info\": \"Proプランとチームプランを10％オフでアップグレード！@:appName AIを含む強力な新機能でワークスペースの生産性を向上させましょう。\",\n          \"viewPlans\": \"プランを表示\"\n        }\n      }\n    },\n    \"billingPage\": {\n      \"menuLabel\": \"請求\",\n      \"title\": \"請求\",\n      \"plan\": {\n        \"title\": \"プラン\",\n        \"freeLabel\": \"無料\",\n        \"proLabel\": \"プロ\",\n        \"planButtonLabel\": \"プランを変更\",\n        \"billingPeriod\": \"請求期間\",\n        \"periodButtonLabel\": \"期間を編集\"\n      },\n      \"paymentDetails\": {\n        \"title\": \"支払い詳細\",\n        \"methodLabel\": \"支払い方法\",\n        \"methodButtonLabel\": \"方法を編集\"\n      },\n      \"addons\": {\n        \"title\": \"アドオン\",\n        \"addLabel\": \"追加\",\n        \"removeLabel\": \"削除\",\n        \"renewLabel\": \"更新\",\n        \"aiMax\": {\n          \"label\": \"AI Max\",\n          \"description\": \"無制限のAIと高度なモデルを解放\",\n          \"activeDescription\": \"次の請求日は{}\",\n          \"canceledDescription\": \"AI Maxは{}まで利用可能です\"\n        },\n        \"aiOnDevice\": {\n          \"label\": \"Mac用AIオンデバイス\",\n          \"description\": \"デバイス上で無制限のAIを解放\",\n          \"activeDescription\": \"次の請求日は{}\",\n          \"canceledDescription\": \"Mac用AIオンデバイスは{}まで利用可能です\"\n        },\n        \"removeDialog\": {\n          \"title\": \"{}を削除\",\n          \"description\": \"{plan}を削除してもよろしいですか？{plan}の機能と特典へのアクセスをすぐに失います。\"\n        }\n      },\n      \"currentPeriodBadge\": \"現在の期間\",\n      \"changePeriod\": \"期間を変更\",\n      \"planPeriod\": \"{}期間\",\n      \"monthlyInterval\": \"毎月\",\n      \"monthlyPriceInfo\": \"毎月請求される座席あたりの料金\",\n      \"annualInterval\": \"年次\",\n      \"annualPriceInfo\": \"年間請求される座席あたりの料金\"\n    },\n    \"comparePlanDialog\": {\n      \"title\": \"プランの比較と選択\",\n      \"planFeatures\": \"プラン\\n機能\",\n      \"current\": \"現在のプラン\",\n      \"actions\": {\n        \"upgrade\": \"アップグレード\",\n        \"downgrade\": \"ダウングレード\",\n        \"current\": \"現在のプラン\"\n      },\n      \"freePlan\": {\n        \"title\": \"無料\",\n        \"description\": \"個人や2名までのメンバーに最適\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"永久に無料\"\n      },\n      \"proPlan\": {\n        \"title\": \"プロ\",\n        \"description\": \"小規模チームのプロジェクト管理とチーム知識の管理に最適\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"1ユーザーあたり月額 \\n年間請求\\n\\n{} 毎月請求\"\n      },\n      \"planLabels\": {\n        \"itemOne\": \"ワークスペース\",\n        \"itemTwo\": \"メンバー\",\n        \"itemThree\": \"ストレージ\",\n        \"itemFour\": \"リアルタイムコラボレーション\",\n        \"itemFive\": \"モバイルアプリ\",\n        \"itemSix\": \"AIレスポンス\",\n        \"itemFileUpload\": \"ファイルアップロード\",\n        \"customNamespace\": \"カスタム名前空間\",\n        \"tooltipSix\": \"ライフタイムはレスポンスの数がリセットされないことを意味します\",\n        \"intelligentSearch\": \"インテリジェント検索\",\n        \"tooltipSeven\": \"ワークスペースのURLの一部をカスタマイズできます\",\n        \"customNamespaceTooltip\": \"カスタム公開サイト URL\"\n      },\n      \"freeLabels\": {\n        \"itemOne\": \"ワークスペースごとの料金\",\n        \"itemTwo\": \"最大2名\",\n        \"itemThree\": \"5 GB\",\n        \"itemFour\": \"はい\",\n        \"itemFive\": \"はい\",\n        \"itemSix\": \"10回のライフタイムレスポンス\",\n        \"itemFileUpload\": \"最大7 MB\",\n        \"intelligentSearch\": \"インテリジェント検索\"\n      },\n      \"proLabels\": {\n        \"itemOne\": \"ワークスペースごとの料金\",\n        \"itemTwo\": \"最大10名\",\n        \"itemThree\": \"無制限\",\n        \"itemFour\": \"はい\",\n        \"itemFive\": \"はい\",\n        \"itemSix\": \"無制限\",\n        \"itemFileUpload\": \"無制限\",\n        \"intelligentSearch\": \"インテリジェント検索\"\n      },\n      \"paymentSuccess\": {\n        \"title\": \"あなたは現在{}プランに加入しています！\",\n        \"description\": \"お支払いが正常に処理され、プランが@:appName {}にアップグレードされました。プランの詳細はプランページで確認できます。\"\n      },\n      \"downgradeDialog\": {\n        \"title\": \"本当にプランをダウングレードしますか？\",\n        \"description\": \"プランをダウングレードすると、無料プランに戻ります。メンバーがこのワークスペースにアクセスできなくなり、無料プランのストレージ制限を満たすためにスペースを空ける必要がある場合があります。\",\n        \"downgradeLabel\": \"プランをダウングレード\"\n      }\n    },\n    \"cancelSurveyDialog\": {\n      \"title\": \"ご利用いただきありがとうございました\",\n      \"description\": \"ご解約されるのは残念です。@:appNameを改善するために、お客様のフィードバックをお聞かせください。いくつかの質問にお答えいただけますと幸いです。\",\n      \"commonOther\": \"その他\",\n      \"otherHint\": \"ここに回答を入力してください\",\n      \"questionOne\": {\n        \"question\": \"@:appName Proサブスクリプションをキャンセルした理由は何ですか？\",\n        \"answerOne\": \"料金が高すぎる\",\n        \"answerTwo\": \"機能が期待に応えなかった\",\n        \"answerThree\": \"より良い代替案を見つけた\",\n        \"answerFour\": \"コストに見合うほど使用しなかった\",\n        \"answerFive\": \"サービスや技術的な問題があった\"\n      },\n      \"questionTwo\": {\n        \"question\": \"将来的に@:appName Proに再加入する可能性はどのくらいですか？\",\n        \"answerOne\": \"非常に高い\",\n        \"answerTwo\": \"やや高い\",\n        \"answerThree\": \"わからない\",\n        \"answerFour\": \"低い\",\n        \"answerFive\": \"非常に低い\"\n      },\n      \"questionThree\": {\n        \"question\": \"サブスクリプション中に最も価値を感じたPro機能は何ですか？\",\n        \"answerOne\": \"複数ユーザーのコラボレーション\",\n        \"answerTwo\": \"長期間のバージョン履歴\",\n        \"answerThree\": \"無制限のAIレスポンス\",\n        \"answerFour\": \"ローカルAIモデルへのアクセス\"\n      },\n      \"questionFour\": {\n        \"question\": \"全体的な@:appNameの体験はどのようなものでしたか？\",\n        \"answerOne\": \"素晴らしい\",\n        \"answerTwo\": \"良い\",\n        \"answerThree\": \"普通\",\n        \"answerFour\": \"平均以下\",\n        \"answerFive\": \"満足していない\"\n      }\n    },\n    \"common\": {\n      \"uploadingFile\": \"ファイルをアップロード中です。アプリを終了しないでください\",\n      \"uploadNotionSuccess\": \"Notion zipファイルが正常にアップロードされました。インポートが完了すると、確認メールが届きます。\",\n      \"reset\": \"リセット\"\n    },\n    \"menu\": {\n      \"appearance\": \"外観\",\n      \"language\": \"言語\",\n      \"user\": \"ユーザー\",\n      \"files\": \"ファイル\",\n      \"notifications\": \"通知\",\n      \"open\": \"設定を開く\",\n      \"logout\": \"ログアウト\",\n      \"logoutPrompt\": \"本当にログアウトしますか？\",\n      \"selfEncryptionLogoutPrompt\": \"ログアウトしますか？暗号化キーをコピーしていることを確認してください。\",\n      \"syncSetting\": \"同期設定\",\n      \"cloudSettings\": \"クラウド設定\",\n      \"enableSync\": \"同期を有効化\",\n      \"enableSyncLog\": \"同期ログを有効にする\",\n      \"enableSyncLogWarning\": \"同期の問題の診断にご協力いただきありがとうございます。これにより、ドキュメントの編集内容がローカルファイルに記録されます。有効にした後、アプリを終了して再度開いてください。\",\n      \"enableEncrypt\": \"データを暗号化\",\n      \"cloudURL\": \"基本URL\",\n      \"webURL\": \"ウェブURL\",\n      \"invalidCloudURLScheme\": \"無効なスキーム\",\n      \"cloudServerType\": \"クラウドサーバー\",\n      \"cloudServerTypeTip\": \"クラウドサーバーを変更すると現在のアカウントがログアウトされる可能性があります。\",\n      \"cloudLocal\": \"ローカル\",\n      \"cloudAppFlowy\": \"@:appName Cloud\",\n      \"cloudAppFlowySelfHost\": \"@:appName クラウドセルフホスト\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"クラウドのURLを空にすることはできません\",\n      \"clickToCopy\": \"クリックしてコピー\",\n      \"selfHostStart\": \"サーバーをお持ちでない場合は、\",\n      \"selfHostContent\": \"ドキュメント\",\n      \"selfHostEnd\": \"を参照してセルフホストサーバーの設定方法をご確認ください\",\n      \"pleaseInputValidURL\": \"有効なURLを入力してください\",\n      \"changeUrl\": \"セルフホスト URL を {} に変更します\",\n      \"cloudURLHint\": \"サーバーの基本URLを入力してください\",\n      \"webURLHint\": \"ウェブサーバーのベースURLを入力してください\",\n      \"cloudWSURL\": \"Websocket URL\",\n      \"cloudWSURLHint\": \"サーバーのWebsocketアドレスを入力してください\",\n      \"restartApp\": \"再起動\",\n      \"restartAppTip\": \"変更を反映するにはアプリケーションを再起動してください。再起動すると現在のアカウントがログアウトされる可能性があります。\",\n      \"changeServerTip\": \"サーバーを変更後、変更を反映するには再起動ボタンをクリックしてください。\",\n      \"enableEncryptPrompt\": \"この秘密キーでデータを暗号化します。安全に保管してください。有効にすると解除できません。キーを紛失するとデータが回復不可能になります。コピーするにはクリックしてください。\",\n      \"inputEncryptPrompt\": \"暗号化キーを入力してください\",\n      \"clickToCopySecret\": \"クリックして秘密キーをコピー\",\n      \"configServerSetting\": \"サーバー設定を構成\",\n      \"configServerGuide\": \"「クイックスタート」を選択後、「設定」→「クラウド設定」に進み、セルフホストサーバーを構成します。\",\n      \"inputTextFieldHint\": \"秘密キー\",\n      \"historicalUserList\": \"ユーザーログイン履歴\",\n      \"historicalUserListTooltip\": \"このリストには匿名アカウントが表示されます。アカウントをクリックすると詳細が表示されます。匿名アカウントは「始める」ボタンをクリックすることで作成されます。\",\n      \"openHistoricalUser\": \"匿名アカウントを開くにはクリック\",\n      \"customPathPrompt\": \"Google Driveなどのクラウド同期フォルダに@:appNameデータフォルダを保存することはリスクを伴います。このフォルダ内のデータベースが複数の場所から同時にアクセスまたは変更されると、同期の競合やデータ破損が発生する可能性があります。\",\n      \"importAppFlowyData\": \"外部@:appNameフォルダからデータをインポート\",\n      \"importingAppFlowyDataTip\": \"データインポート中です。アプリを閉じないでください。\",\n      \"importAppFlowyDataDescription\": \"外部@:appNameデータフォルダからデータをコピーし、現在のAppFlowyデータフォルダにインポートします。\",\n      \"importSuccess\": \"@:appNameデータフォルダのインポートに成功しました\",\n      \"importFailed\": \"@:appNameデータフォルダのインポートに失敗しました\",\n      \"importGuide\": \"詳細については、参照されたドキュメントをご確認ください。\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"通知を有効化\",\n        \"hint\": \"ローカル通知の表示を停止するにはオフにします。\"\n      },\n      \"showNotificationsIcon\": {\n        \"label\": \"通知アイコンを表示\",\n        \"hint\": \"サイドバーに通知アイコンを表示するにはオンにします。\"\n      },\n      \"archiveNotifications\": {\n        \"allSuccess\": \"すべての通知を正常にアーカイブしました\",\n        \"success\": \"通知を正常にアーカイブしました\"\n      },\n      \"markAsReadNotifications\": {\n        \"allSuccess\": \"すべて既読にしました\",\n        \"success\": \"正常に既読にしました\"\n      },\n      \"action\": {\n        \"markAsRead\": \"既読にする\",\n        \"multipleChoice\": \"複数選択\",\n        \"archive\": \"アーカイブ\"\n      },\n      \"settings\": {\n        \"settings\": \"設定\",\n        \"markAllAsRead\": \"すべて既読にする\",\n        \"archiveAll\": \"すべてアーカイブ\"\n      },\n      \"emptyInbox\": {\n        \"title\": \"インボックスゼロ！\",\n        \"description\": \"リマインダーを設定してここで通知を受け取ります。\"\n      },\n      \"emptyUnread\": {\n        \"title\": \"未読の通知はありません\",\n        \"description\": \"すべてが完了しました！\"\n      },\n      \"emptyArchived\": {\n        \"title\": \"アーカイブなし\",\n        \"description\": \"アーカイブされた通知がここに表示されます。\"\n      },\n      \"tabs\": {\n        \"inbox\": \"受信箱\",\n        \"unread\": \"未読\",\n        \"archived\": \"アーカイブ\"\n      },\n      \"refreshSuccess\": \"通知が正常に更新されました\",\n      \"titles\": {\n        \"notifications\": \"通知\",\n        \"reminder\": \"リマインダー\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"リセット\",\n      \"fontFamily\": {\n        \"label\": \"フォントファミリー\",\n        \"search\": \"検索\",\n        \"defaultFont\": \"システム\"\n      },\n      \"themeMode\": {\n        \"label\": \"テーマモード\",\n        \"light\": \"ライトモード\",\n        \"dark\": \"ダークモード\",\n        \"system\": \"システムに適応\"\n      },\n      \"fontScaleFactor\": \"フォントの拡大率\",\n      \"documentSettings\": {\n        \"cursorColor\": \"ドキュメントのカーソル色\",\n        \"selectionColor\": \"ドキュメントの選択色\",\n        \"width\": \"文書の幅\",\n        \"changeWidth\": \"変更\",\n        \"pickColor\": \"色を選択\",\n        \"colorShade\": \"色の濃淡\",\n        \"opacity\": \"不透明度\",\n        \"hexEmptyError\": \"Hexカラーコードは空にできません\",\n        \"hexLengthError\": \"Hex値は6桁である必要があります\",\n        \"hexInvalidError\": \"無効なHex値です\",\n        \"opacityEmptyError\": \"不透明度は空にできません\",\n        \"opacityRangeError\": \"不透明度は1から100の間である必要があります\",\n        \"app\": \"アプリ\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"適用\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"レイアウト方向\",\n        \"hint\": \"画面上のコンテンツの流れを、左から右、または右から左に制御します。\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\"\n      },\n      \"textDirection\": {\n        \"label\": \"デフォルトのテキスト方向\",\n        \"hint\": \"テキストがデフォルトで左から始まるか右から始まるかを指定します。\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\",\n        \"auto\": \"自動\",\n        \"fallback\": \"レイアウト方向と同じ\"\n      },\n      \"themeUpload\": {\n        \"button\": \"アップロード\",\n        \"uploadTheme\": \"テーマをアップロード\",\n        \"description\": \"下のボタンを使用して独自の@:appNameテーマをアップロードします。\",\n        \"loading\": \"テーマの検証とアップロード中です。しばらくお待ちください...\",\n        \"uploadSuccess\": \"テーマが正常にアップロードされました\",\n        \"deletionFailure\": \"テーマの削除に失敗しました。手動で削除をお試しください。\",\n        \"filePickerDialogTitle\": \".flowy_pluginファイルを選択\",\n        \"urlUploadFailure\": \"URLのオープンに失敗しました: {}\"\n      },\n      \"theme\": \"テーマ\",\n      \"builtInsLabel\": \"組み込みテーマ\",\n      \"pluginsLabel\": \"プラグイン\",\n      \"dateFormat\": {\n        \"label\": \"日付形式\",\n        \"local\": \"ローカル\",\n        \"us\": \"US\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"読み易さ\",\n        \"dmy\": \"日/月/年\"\n      },\n      \"timeFormat\": {\n        \"label\": \"時間形式\",\n        \"twelveHour\": \"12時間制\",\n        \"twentyFourHour\": \"24時間制\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"ページ作成時に名前付けダイアログを表示\",\n      \"enableRTLToolbarItems\": \"RTLツールバー項目を有効にする\",\n      \"members\": {\n        \"title\": \"メンバー設定\",\n        \"inviteMembers\": \"メンバーを招待\",\n        \"inviteHint\": \"メールで招待\",\n        \"sendInvite\": \"招待を送信\",\n        \"copyInviteLink\": \"招待リンクをコピー\",\n        \"label\": \"メンバー\",\n        \"user\": \"ユーザー\",\n        \"role\": \"役割\",\n        \"removeFromWorkspace\": \"ワークスペースから削除\",\n        \"removeFromWorkspaceSuccess\": \"ワークスペースから正常に削除されました\",\n        \"removeFromWorkspaceFailed\": \"ワークスペースからの削除に失敗しました\",\n        \"owner\": \"オーナー\",\n        \"guest\": \"ゲスト\",\n        \"member\": \"メンバー\",\n        \"memberHintText\": \"メンバーはページを閲覧および編集できます\",\n        \"guestHintText\": \"ゲストはページを閲覧、反応、コメントし、許可があれば一部のページを編集できます。\",\n        \"emailInvalidError\": \"無効なメールアドレスです。確認してもう一度お試しください。\",\n        \"emailSent\": \"メールが送信されました。受信箱を確認してください。\",\n        \"members\": \"メンバー\",\n        \"membersCount\": {\n          \"zero\": \"{} 人のメンバー\",\n          \"one\": \"{} 人のメンバー\",\n          \"other\": \"{} 人のメンバー\"\n        },\n        \"inviteFailedDialogTitle\": \"招待の送信に失敗しました\",\n        \"inviteFailedMemberLimit\": \"メンバー制限に達しました。メンバーを追加するにはアップグレードしてください。\",\n        \"inviteFailedMemberLimitMobile\": \"ワークスペースのメンバー制限に達しました。\",\n        \"memberLimitExceeded\": \"メンバー制限に達しました。さらにメンバーを招待するには \",\n        \"memberLimitExceededUpgrade\": \"アップグレード\",\n        \"memberLimitExceededPro\": \"メンバー制限に達しました。さらにメンバーが必要な場合は \",\n        \"memberLimitExceededProContact\": \"support@appflowy.io にご連絡ください\",\n        \"failedToAddMember\": \"メンバーの追加に失敗しました\",\n        \"addMemberSuccess\": \"メンバーが正常に追加されました\",\n        \"removeMember\": \"メンバーを削除\",\n        \"areYouSureToRemoveMember\": \"本当にこのメンバーを削除しますか？\",\n        \"inviteMemberSuccess\": \"招待が正常に送信されました\",\n        \"failedToInviteMember\": \"メンバーの招待に失敗しました\",\n        \"workspaceMembersError\": \"問題が発生しました\",\n        \"workspaceMembersErrorDescription\": \"現在、メンバーリストを読み込むことができません。後でもう一度お試しください\"\n      }\n    },\n    \"files\": {\n      \"copy\": \"コピー\",\n      \"defaultLocation\": \"ファイルとデータ保存場所を読み取る\",\n      \"exportData\": \"データをエクスポート\",\n      \"doubleTapToCopy\": \"パスをコピーするにはダブルタップ\",\n      \"restoreLocation\": \"@:appName のデフォルトパスに復元\",\n      \"customizeLocation\": \"別のフォルダを開く\",\n      \"restartApp\": \"変更を反映するにはアプリを再起動してください。\",\n      \"exportDatabase\": \"データベースをエクスポート\",\n      \"selectFiles\": \"エクスポートするファイルを選択\",\n      \"selectAll\": \"すべて選択\",\n      \"deselectAll\": \"すべて選択解除\",\n      \"createNewFolder\": \"新しいフォルダを作成\",\n      \"createNewFolderDesc\": \"データを保存する場所を指定してください\",\n      \"defineWhereYourDataIsStored\": \"データが保存される場所を設定\",\n      \"open\": \"開く\",\n      \"openFolder\": \"既存のフォルダを開く\",\n      \"openFolderDesc\": \"既存の@:appName フォルダに読み書き\",\n      \"folderHintText\": \"フォルダ名\",\n      \"location\": \"新しいフォルダを作成\",\n      \"locationDesc\": \"@:appName データフォルダの名前を指定\",\n      \"browser\": \"参照\",\n      \"create\": \"作成\",\n      \"set\": \"設定\",\n      \"folderPath\": \"フォルダを保存するパス\",\n      \"locationCannotBeEmpty\": \"パスを空にすることはできません\",\n      \"pathCopiedSnackbar\": \"ファイル保存パスがクリップボードにコピーされました！\",\n      \"changeLocationTooltips\": \"データディレクトリを変更\",\n      \"change\": \"変更\",\n      \"openLocationTooltips\": \"別のデータディレクトリを開く\",\n      \"openCurrentDataFolder\": \"現在のデータディレクトリを開く\",\n      \"recoverLocationTooltips\": \"@:appName のデフォルトデータディレクトリにリセット\",\n      \"exportFileSuccess\": \"ファイルのエクスポートに成功しました！\",\n      \"exportFileFail\": \"ファイルのエクスポートに失敗しました！\",\n      \"export\": \"エクスポート\",\n      \"clearCache\": \"キャッシュをクリア\",\n      \"clearCacheDesc\": \"画像が読み込まれない、フォントが表示されないなどの問題がある場合は、キャッシュをクリアしてください。この操作はユーザーデータを削除しません。\",\n      \"areYouSureToClearCache\": \"キャッシュをクリアしますか？\",\n      \"clearCacheSuccess\": \"キャッシュが正常にクリアされました！\"\n    },\n    \"user\": {\n      \"name\": \"名前\",\n      \"email\": \"メールアドレス\",\n      \"tooltipSelectIcon\": \"アイコンを選択\",\n      \"selectAnIcon\": \"アイコンを選択\",\n      \"pleaseInputYourOpenAIKey\": \"AIキーを入力してください\",\n      \"clickToLogout\": \"現在のユーザーをログアウトするにはクリック\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"個人情報\",\n      \"username\": \"ユーザー名\",\n      \"usernameEmptyError\": \"ユーザー名は空にできません\",\n      \"about\": \"概要\",\n      \"pushNotifications\": \"プッシュ通知\",\n      \"support\": \"サポート\",\n      \"joinDiscord\": \"Discordに参加\",\n      \"privacyPolicy\": \"プライバシーポリシー\",\n      \"userAgreement\": \"利用規約\",\n      \"termsAndConditions\": \"利用条件\",\n      \"userprofileError\": \"ユーザープロフィールの読み込みに失敗しました\",\n      \"userprofileErrorDescription\": \"ログアウトして再度ログインして、問題が解決するか確認してください。\",\n      \"selectLayout\": \"レイアウトを選択\",\n      \"selectStartingDay\": \"開始日を選択\",\n      \"version\": \"バージョン\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"このビューを削除してもよろしいですか？\",\n    \"createView\": \"新規\",\n    \"title\": {\n      \"placeholder\": \"無題\"\n    },\n    \"settings\": {\n      \"filter\": \"フィルター\",\n      \"sort\": \"並べ替え\",\n      \"sortBy\": \"並べ替え基準\",\n      \"properties\": \"プロパティ\",\n      \"reorderPropertiesTooltip\": \"プロパティをドラッグして並べ替え\",\n      \"group\": \"グループ\",\n      \"addFilter\": \"フィルターを追加\",\n      \"deleteFilter\": \"フィルターを削除\",\n      \"filterBy\": \"フィルター条件...\",\n      \"typeAValue\": \"値を入力...\",\n      \"layout\": \"レイアウト\",\n      \"databaseLayout\": \"レイアウト\",\n      \"viewList\": {\n        \"zero\": \"0 ビュー\",\n        \"one\": \"{count} ビュー\",\n        \"other\": \"{count} ビュー\"\n      },\n      \"editView\": \"ビューを編集\",\n      \"boardSettings\": \"ボード設定\",\n      \"calendarSettings\": \"カレンダー設定\",\n      \"createView\": \"新しいビュー\",\n      \"duplicateView\": \"ビューを複製\",\n      \"deleteView\": \"ビューを削除\",\n      \"numberOfVisibleFields\": \"{} 表示中\"\n    },\n    \"filter\": {\n      \"empty\": \"アクティブなフィルターはありません\",\n      \"addFilter\": \"フィルターを追加\",\n      \"cannotFindCreatableField\": \"フィルタリングに適したフィールドが見つかりません\",\n      \"conditon\": \"状態\",\n      \"where\": \"どこ\"\n    },\n    \"textFilter\": {\n      \"contains\": \"含む\",\n      \"doesNotContain\": \"含まない\",\n      \"endsWith\": \"で終わる\",\n      \"startWith\": \"で始まる\",\n      \"is\": \"である\",\n      \"isNot\": \"でない\",\n      \"isEmpty\": \"空\",\n      \"isNotEmpty\": \"空でない\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"ではない\",\n        \"startWith\": \"で始まる\",\n        \"endWith\": \"で終わる\",\n        \"isEmpty\": \"空\",\n        \"isNotEmpty\": \"空でない\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"チェック済み\",\n      \"isUnchecked\": \"未チェック\",\n      \"choicechipPrefix\": {\n        \"is\": \"である\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"完了\",\n      \"isIncomplted\": \"未完了\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"である\",\n      \"isNot\": \"ではない\",\n      \"contains\": \"含む\",\n      \"doesNotContain\": \"含まない\",\n      \"isEmpty\": \"空\",\n      \"isNotEmpty\": \"空でない\"\n    },\n    \"dateFilter\": {\n      \"is\": \"である\",\n      \"before\": \"以前\",\n      \"after\": \"以後\",\n      \"onOrBefore\": \"以前またはその日\",\n      \"onOrAfter\": \"以後またはその日\",\n      \"between\": \"の間\",\n      \"empty\": \"空\",\n      \"notEmpty\": \"空でない\",\n      \"startDate\": \"開始日\",\n      \"endDate\": \"終了日\",\n      \"choicechipPrefix\": {\n        \"before\": \"以前\",\n        \"after\": \"以後\",\n        \"between\": \"間\",\n        \"onOrBefore\": \"以前またはその日\",\n        \"onOrAfter\": \"以後またはその日\",\n        \"isEmpty\": \"空\",\n        \"isNotEmpty\": \"空でない\"\n      }\n    },\n    \"numberFilter\": {\n      \"equal\": \"等しい\",\n      \"notEqual\": \"等しくない\",\n      \"lessThan\": \"より小さい\",\n      \"greaterThan\": \"より大きい\",\n      \"lessThanOrEqualTo\": \"以下\",\n      \"greaterThanOrEqualTo\": \"以上\",\n      \"isEmpty\": \"空\",\n      \"isNotEmpty\": \"空でない\"\n    },\n    \"field\": {\n      \"label\": \"プロパティ\",\n      \"hide\": \"非表示\",\n      \"show\": \"表示\",\n      \"insertLeft\": \"左に挿入\",\n      \"insertRight\": \"右に挿入\",\n      \"duplicate\": \"複製\",\n      \"delete\": \"削除\",\n      \"wrapCellContent\": \"テキストを折り返し\",\n      \"clear\": \"セルをクリア\",\n      \"switchPrimaryFieldTooltip\": \"プライマリフィールドのフィールドタイプを変更できません\",\n      \"textFieldName\": \"テキスト\",\n      \"checkboxFieldName\": \"チェックボックス\",\n      \"dateFieldName\": \"日付\",\n      \"updatedAtFieldName\": \"最終更新日\",\n      \"createdAtFieldName\": \"作成日\",\n      \"numberFieldName\": \"数字\",\n      \"singleSelectFieldName\": \"選択\",\n      \"multiSelectFieldName\": \"マルチセレクト\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"チェックリスト\",\n      \"relationFieldName\": \"リレーション\",\n      \"summaryFieldName\": \"AIサマリー\",\n      \"timeFieldName\": \"時間\",\n      \"mediaFieldName\": \"ファイルとメディア\",\n      \"translateFieldName\": \"AI翻訳\",\n      \"translateTo\": \"翻訳先\",\n      \"numberFormat\": \"数字の形式\",\n      \"dateFormat\": \"日付の形式\",\n      \"includeTime\": \"時間を含む\",\n      \"isRange\": \"終了日\",\n      \"dateFormatFriendly\": \"月 日, 年\",\n      \"dateFormatISO\": \"年-月-日\",\n      \"dateFormatLocal\": \"月/日/年\",\n      \"dateFormatUS\": \"年/月/日\",\n      \"dateFormatDayMonthYear\": \"日/月/年\",\n      \"timeFormat\": \"時間形式\",\n      \"invalidTimeFormat\": \"無効な形式\",\n      \"timeFormatTwelveHour\": \"12時間制\",\n      \"timeFormatTwentyFourHour\": \"24時間制\",\n      \"clearDate\": \"日付をクリア\",\n      \"dateTime\": \"日時\",\n      \"startDateTime\": \"開始日時\",\n      \"endDateTime\": \"終了日時\",\n      \"failedToLoadDate\": \"日付の読み込みに失敗しました\",\n      \"selectTime\": \"時間を選択\",\n      \"selectDate\": \"日付を選択\",\n      \"visibility\": \"表示\",\n      \"propertyType\": \"プロパティの種類\",\n      \"addSelectOption\": \"オプションを追加\",\n      \"typeANewOption\": \"新しいオプションを入力\",\n      \"optionTitle\": \"オプション\",\n      \"addOption\": \"オプションを追加\",\n      \"editProperty\": \"プロパティを編集\",\n      \"newProperty\": \"新しいプロパティ\",\n      \"openRowDocument\": \"ページとして開く\",\n      \"deleteFieldPromptMessage\": \"本当によろしいですか？このプロパティとそのすべてのデータが削除されます\",\n      \"clearFieldPromptMessage\": \"本当に空にしますか？この列のすべてのセルが空になります\",\n      \"newColumn\": \"新しい列\",\n      \"format\": \"形式\",\n      \"reminderOnDateTooltip\": \"このセルにはリマインダーが設定されています\",\n      \"optionAlreadyExist\": \"オプションはすでに存在します\"\n    },\n    \"rowPage\": {\n      \"newField\": \"新しいフィールドを追加\",\n      \"fieldDragElementTooltip\": \"メニューを開くにはクリック\",\n      \"showHiddenFields\": {\n        \"one\": \"{count} 非表示フィールドを表示\",\n        \"many\": \"{count} 非表示フィールドを表示\",\n        \"other\": \"{count} 非表示フィールドを表示\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"{count} 非表示フィールドを隠す\",\n        \"many\": \"{count} 非表示フィールドを隠す\",\n        \"other\": \"{count} 非表示フィールドを隠す\"\n      },\n      \"openAsFullPage\": \"全画面で開く\",\n      \"moreRowActions\": \"その他の行アクション\"\n    },\n    \"sort\": {\n      \"ascending\": \"昇順\",\n      \"descending\": \"降順\",\n      \"by\": \"基準\",\n      \"empty\": \"アクティブな並べ替えがありません\",\n      \"cannotFindCreatableField\": \"並べ替え可能なフィールドが見つかりません\",\n      \"deleteAllSorts\": \"すべての並べ替えを削除\",\n      \"addSort\": \"新しい並べ替えを追加\",\n      \"sortsActive\": \"並べ替え中に{intention}できません\",\n      \"removeSorting\": \"並べ替えを削除しますか？\",\n      \"fieldInUse\": \"このフィールドですでに並べ替えが行われています\"\n    },\n    \"row\": {\n      \"label\": \"行\",\n      \"duplicate\": \"複製\",\n      \"delete\": \"削除\",\n      \"titlePlaceholder\": \"無題\",\n      \"textPlaceholder\": \"空\",\n      \"copyProperty\": \"プロパティをクリップボードにコピー\",\n      \"count\": \"カウント\",\n      \"newRow\": \"新しい行\",\n      \"loadMore\": \"さらに読み込む\",\n      \"action\": \"アクション\",\n      \"add\": \"下に追加をクリック\",\n      \"drag\": \"ドラッグして移動\",\n      \"deleteRowPrompt\": \"この行を削除してもよろしいですか？この操作は元に戻せません\",\n      \"deleteCardPrompt\": \"このカードを削除してもよろしいですか？この操作は元に戻せません\",\n      \"dragAndClick\": \"ドラッグして移動、クリックしてメニューを開く\",\n      \"insertRecordAbove\": \"上にレコードを挿入\",\n      \"insertRecordBelow\": \"下にレコードを挿入\",\n      \"noContent\": \"コンテンツなし\",\n      \"reorderRowDescription\": \"行を並べ替える\",\n      \"createRowAboveDescription\": \"上に行を作成\",\n      \"createRowBelowDescription\": \"下に行を挿入\"\n    },\n    \"selectOption\": {\n      \"create\": \"作成\",\n      \"purpleColor\": \"パープル\",\n      \"pinkColor\": \"ピンク\",\n      \"lightPinkColor\": \"ライトピンク\",\n      \"orangeColor\": \"オレンジ\",\n      \"yellowColor\": \"イエロー\",\n      \"limeColor\": \"ライム\",\n      \"greenColor\": \"グリーン\",\n      \"aquaColor\": \"アクア\",\n      \"blueColor\": \"ブルー\",\n      \"deleteTag\": \"タグを削除\",\n      \"colorPanelTitle\": \"カラー\",\n      \"panelTitle\": \"オプションを選択するか作成\",\n      \"searchOption\": \"オプションを検索\",\n      \"searchOrCreateOption\": \"オプションを検索するか作成\",\n      \"createNew\": \"新しいものを作成\",\n      \"orSelectOne\": \"またはオプションを選択\",\n      \"typeANewOption\": \"新しいオプションを入力\",\n      \"tagName\": \"タグ名\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"タスクの説明\",\n      \"addNew\": \"新しいタスクを追加\",\n      \"submitNewTask\": \"作成\",\n      \"hideComplete\": \"完了したタスクを非表示\",\n      \"showComplete\": \"すべてのタスクを表示\"\n    },\n    \"url\": {\n      \"launch\": \"リンクをブラウザで開く\",\n      \"copy\": \"リンクをクリップボードにコピー\",\n      \"textFieldHint\": \"URLを入力\"\n    },\n    \"relation\": {\n      \"relatedDatabasePlaceLabel\": \"関連データベース\",\n      \"relatedDatabasePlaceholder\": \"なし\",\n      \"inRelatedDatabase\": \"に\",\n      \"rowSearchTextFieldPlaceholder\": \"検索\",\n      \"noDatabaseSelected\": \"データベースが選択されていません。まず以下のリストから1つ選択してください:\",\n      \"emptySearchResult\": \"レコードが見つかりません\",\n      \"linkedRowListLabel\": \"{count} リンクされた行\",\n      \"unlinkedRowListLabel\": \"他の行をリンク\"\n    },\n    \"menuName\": \"グリッド\",\n    \"referencedGridPrefix\": \"ビュー\",\n    \"calculate\": \"計算\",\n    \"calculationTypeLabel\": {\n      \"none\": \"なし\",\n      \"average\": \"平均\",\n      \"max\": \"最大\",\n      \"median\": \"中央値\",\n      \"min\": \"最小\",\n      \"sum\": \"合計\",\n      \"count\": \"カウント\",\n      \"countEmpty\": \"空のカウント\",\n      \"countEmptyShort\": \"空\",\n      \"countNonEmpty\": \"空でないカウント\",\n      \"countNonEmptyShort\": \"入力済み\"\n    },\n    \"media\": {\n      \"rename\": \"名前を変更\",\n      \"download\": \"ダウンロード\",\n      \"expand\": \"拡大\",\n      \"delete\": \"削除\",\n      \"moreFilesHint\": \"+{} ファイル\",\n      \"addFileOrImage\": \"ファイル、画像、またはリンクを追加\",\n      \"attachmentsHint\": \"{} 添付ファイル\",\n      \"addFileMobile\": \"ファイルを追加\",\n      \"extraCount\": \"+{}\",\n      \"deleteFileDescription\": \"このファイルを削除してもよろしいですか? この操作は元に戻せません。\",\n      \"showFileNames\": \"ファイル名を表示\",\n      \"downloadSuccess\": \"ファイルをダウンロードしました\",\n      \"downloadFailedToken\": \"ファイルのダウンロードに失敗しました。ユーザー トークンが利用できません\",\n      \"setAsCover\": \"カバーとして設定\",\n      \"openInBrowser\": \"ブラウザで開く\",\n      \"embedLink\": \"ファイルリンクを埋め込む\",\n      \"open\": \"開く\",\n      \"showMore\": \"{} ファイルがさらにあります。クリックして表示\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"ドキュメント\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"creating\": \"作成...\",\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"リンクするボードを選択\",\n        \"createANewBoard\": \"新しいボードを作成\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"リンクするグリッドを選択\",\n        \"createANewGrid\": \"新しいグリッドを作成\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"リンクするカレンダーを選択\",\n        \"createANewCalendar\": \"新しいカレンダーを作成\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"リンクするドキュメントを選択\"\n      },\n      \"name\": {\n        \"text\": \"テキスト\",\n        \"heading1\": \"見出し1\",\n        \"heading2\": \"見出し2\",\n        \"heading3\": \"見出し3\",\n        \"image\": \"画像\",\n        \"bulletedList\": \"箇条書きリスト\",\n        \"numberedList\": \"番号付きリスト\",\n        \"todoList\": \"To-do リスト\",\n        \"doc\": \"ドキュメント\",\n        \"linkedDoc\": \"ページへのリンク\",\n        \"grid\": \"グリッド\",\n        \"linkedGrid\": \"リンクされたグリッド\",\n        \"kanban\": \"カンバン\",\n        \"linkedKanban\": \"リンクされたカンバン\",\n        \"calendar\": \"カレンダー\",\n        \"linkedCalendar\": \"リンクされたカレンダー\",\n        \"quote\": \"引用\",\n        \"divider\": \"区切り線\",\n        \"table\": \"テーブル\",\n        \"callout\": \"呼び出し\",\n        \"outline\": \"アウトライン\",\n        \"mathEquation\": \"数式\",\n        \"code\": \"コード\",\n        \"toggleList\": \"折りたたみリスト\",\n        \"toggleHeading1\": \"見出し 1 を切り替える\",\n        \"toggleHeading2\": \"見出し 2 を切り替える\",\n        \"toggleHeading3\": \"見出し 3 を切り替える\",\n        \"emoji\": \"絵文字\",\n        \"aiWriter\": \"AIライター\",\n        \"dateOrReminder\": \"日付またはリマインダー\",\n        \"photoGallery\": \"フォトギャラリー\",\n        \"file\": \"ファイル\"\n      },\n      \"subPage\": {\n        \"name\": \"書類\",\n        \"keyword1\": \"サブページ\",\n        \"keyword2\": \"ページ\",\n        \"keyword3\": \"子ページ\",\n        \"keyword4\": \"ページを挿入\",\n        \"keyword5\": \"埋め込みページ\",\n        \"keyword6\": \"新しいページ\",\n        \"keyword7\": \"ページを作成\",\n        \"keyword8\": \"書類\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"アウトライン\",\n      \"codeBlock\": \"コードブロック\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"参照されたボード\",\n      \"referencedGrid\": \"参照されたグリッド\",\n      \"referencedCalendar\": \"参照されたカレンダー\",\n      \"referencedDocument\": \"参照されたドキュメント\",\n      \"autoGeneratorMenuItemName\": \"AIライター\",\n      \"autoGeneratorTitleName\": \"AI: 任意の文章をAIに依頼...\",\n      \"autoGeneratorLearnMore\": \"詳細を読む\",\n      \"autoGeneratorGenerate\": \"生成\",\n      \"autoGeneratorHintText\": \"AIに質問 ...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"AIキーを取得できません\",\n      \"autoGeneratorRewrite\": \"書き直し\",\n      \"smartEdit\": \"AIに依頼\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"スペルと文法を修正\",\n      \"warning\": \"⚠️ AIの回答は不正確または誤解を招く可能性があります。\",\n      \"smartEditSummarize\": \"要約\",\n      \"smartEditImproveWriting\": \"文章を改善\",\n      \"smartEditMakeLonger\": \"長くする\",\n      \"smartEditCouldNotFetchResult\": \"AIから結果を取得できませんでした\",\n      \"smartEditCouldNotFetchKey\": \"AIキーを取得できませんでした\",\n      \"smartEditDisabled\": \"設定でAIを接続してください\",\n      \"appflowyAIEditDisabled\": \"AI機能を有効にするにはサインインしてください\",\n      \"discardResponse\": \"AIの回答を破棄しますか？\",\n      \"createInlineMathEquation\": \"数式を作成\",\n      \"fonts\": \"フォント\",\n      \"insertDate\": \"日付を挿入\",\n      \"emoji\": \"絵文字\",\n      \"toggleList\": \"折りたたみリスト\",\n      \"emptyToggleHeading\": \"トグル h{} が空です。クリックしてコンテンツを追加します。\",\n      \"emptyToggleList\": \"トグル リストが空です。クリックしてコンテンツを追加します。\",\n      \"emptyToggleHeadingWeb\": \"トグル h{level} が空です。クリックしてコンテンツを追加してください\",\n      \"quoteList\": \"引用リスト\",\n      \"numberedList\": \"番号付きリスト\",\n      \"bulletedList\": \"箇条書きリスト\",\n      \"todoList\": \"To-do リスト\",\n      \"callout\": \"呼び出し\",\n      \"simpleTable\": {\n        \"moreActions\": {\n          \"color\": \"色\",\n          \"align\": \"整列\",\n          \"delete\": \"消去\",\n          \"duplicate\": \"重複\",\n          \"insertLeft\": \"左に挿入\",\n          \"insertRight\": \"右に挿入\",\n          \"insertAbove\": \"上に挿入\",\n          \"insertBelow\": \"以下に挿入\",\n          \"headerColumn\": \"ヘッダー列\",\n          \"headerRow\": \"ヘッダー行\",\n          \"clearContents\": \"内容をクリア\",\n          \"setToPageWidth\": \"ページ幅に設定\",\n          \"distributeColumnsWidth\": \"列を均等に分散する\",\n          \"duplicateRow\": \"重複行\",\n          \"duplicateColumn\": \"重複した列\",\n          \"textColor\": \"テキストの色\",\n          \"cellBackgroundColor\": \"セルの背景色\",\n          \"duplicateTable\": \"重複テーブル\"\n        },\n        \"clickToAddNewRow\": \"クリックして新しい行を追加します\",\n        \"clickToAddNewColumn\": \"クリックして新しい列を追加します\",\n        \"clickToAddNewRowAndColumn\": \"クリックして新しい行と列を追加します\",\n        \"headerName\": {\n          \"table\": \"テーブル\",\n          \"alignText\": \"テキストを揃える\"\n        }\n      },\n      \"cover\": {\n        \"changeCover\": \"カバーを変更\",\n        \"colors\": \"色\",\n        \"images\": \"画像\",\n        \"clearAll\": \"すべてクリア\",\n        \"abstract\": \"抽象的\",\n        \"addCover\": \"カバーを追加\",\n        \"addLocalImage\": \"ローカル画像を追加\",\n        \"invalidImageUrl\": \"無効な画像URL\",\n        \"failedToAddImageToGallery\": \"ギャラリーに画像を追加できませんでした\",\n        \"enterImageUrl\": \"画像URLを入力\",\n        \"add\": \"追加\",\n        \"back\": \"戻る\",\n        \"saveToGallery\": \"ギャラリーに保存\",\n        \"removeIcon\": \"アイコンを削除\",\n        \"removeCover\": \"カバーを削除\",\n        \"pasteImageUrl\": \"画像URLを貼り付け\",\n        \"or\": \"または\",\n        \"pickFromFiles\": \"ファイルから選択\",\n        \"couldNotFetchImage\": \"画像を取得できませんでした\",\n        \"imageSavingFailed\": \"画像の保存に失敗しました\",\n        \"addIcon\": \"アイコンを追加\",\n        \"changeIcon\": \"アイコンを変更\",\n        \"coverRemoveAlert\": \"削除するとカバーからも削除されます。\",\n        \"alertDialogConfirmation\": \"本当に続けますか？\"\n      },\n      \"mathEquation\": {\n        \"name\": \"数式\",\n        \"addMathEquation\": \"TeX数式を追加\",\n        \"editMathEquation\": \"数式を編集\"\n      },\n      \"optionAction\": {\n        \"click\": \"クリック\",\n        \"toOpenMenu\": \" でメニューを開く\",\n        \"drag\": \"ドラッグ\",\n        \"toMove\": \" 移動する\",\n        \"delete\": \"削除\",\n        \"duplicate\": \"複製\",\n        \"turnInto\": \"変換\",\n        \"moveUp\": \"上に移動\",\n        \"moveDown\": \"下に移動\",\n        \"color\": \"色\",\n        \"align\": \"整列\",\n        \"left\": \"左\",\n        \"center\": \"中央\",\n        \"right\": \"右\",\n        \"defaultColor\": \"デフォルト\",\n        \"depth\": \"深さ\",\n        \"copyLinkToBlock\": \"リンクをブロックにコピーする\"\n      },\n      \"image\": {\n        \"addAnImage\": \"画像を追加\",\n        \"copiedToPasteBoard\": \"画像リンクがクリップボードにコピーされました\",\n        \"addAnImageDesktop\": \"画像を追加\",\n        \"addAnImageMobile\": \"クリックして画像を追加\",\n        \"dropImageToInsert\": \"挿入する画像をドロップ\",\n        \"imageUploadFailed\": \"画像のアップロードに失敗しました\",\n        \"imageDownloadFailed\": \"画像のダウンロードに失敗しました。もう一度お試しください\",\n        \"imageDownloadFailedToken\": \"ユーザートークンがないため、画像のアップロードに失敗しました。もう一度お試しください\",\n        \"errorCode\": \"エラーコード\"\n      },\n      \"photoGallery\": {\n        \"name\": \"フォトギャラリー\",\n        \"imageKeyword\": \"画像\",\n        \"imageGalleryKeyword\": \"画像ギャラリー\",\n        \"photoKeyword\": \"写真\",\n        \"photoBrowserKeyword\": \"フォトブラウザ\",\n        \"galleryKeyword\": \"ギャラリー\",\n        \"addImageTooltip\": \"画像を追加\",\n        \"changeLayoutTooltip\": \"レイアウトを変更\",\n        \"browserLayout\": \"ブラウザ\",\n        \"gridLayout\": \"グリッド\",\n        \"deleteBlockTooltip\": \"ギャラリー全体を削除\"\n      },\n      \"math\": {\n        \"copiedToPasteBoard\": \"数式がクリップボードにコピーされました\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"リンクがクリップボードにコピーされました\",\n        \"convertToLink\": \"埋め込みリンクに変換\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"目次を作成するには見出しを追加してください。\",\n        \"noMatchHeadings\": \"一致する見出しが見つかりませんでした。\"\n      },\n      \"table\": {\n        \"addAfter\": \"後に追加\",\n        \"addBefore\": \"前に追加\",\n        \"delete\": \"削除\",\n        \"clear\": \"内容をクリア\",\n        \"duplicate\": \"複製\",\n        \"bgColor\": \"背景色\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"コピー\",\n        \"cut\": \"切り取り\",\n        \"paste\": \"貼り付け\",\n        \"pasteAsPlainText\": \"プレーンテキストとして貼り付け\"\n      },\n      \"action\": \"アクション\",\n      \"database\": {\n        \"selectDataSource\": \"データソースを選択\",\n        \"noDataSource\": \"データソースなし\",\n        \"selectADataSource\": \"データソースを選択\",\n        \"toContinue\": \"続行する\",\n        \"newDatabase\": \"新しいデータベース\",\n        \"linkToDatabase\": \"データベースにリンク\"\n      },\n      \"date\": \"日付\",\n      \"video\": {\n        \"label\": \"ビデオ\",\n        \"emptyLabel\": \"ビデオを追加\",\n        \"placeholder\": \"ビデオリンクを貼り付け\",\n        \"copiedToPasteBoard\": \"ビデオリンクがクリップボードにコピーされました\",\n        \"insertVideo\": \"ビデオを追加\",\n        \"invalidVideoUrl\": \"ソースURLはサポートされていません。\",\n        \"invalidVideoUrlYouTube\": \"YouTubeはまだサポートされていません。\",\n        \"supportedFormats\": \"サポートされている形式: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264\"\n      },\n      \"file\": {\n        \"name\": \"ファイル\",\n        \"uploadTab\": \"アップロード\",\n        \"uploadMobile\": \"ファイルを選択\",\n        \"uploadMobileGallery\": \"フォトギャラリーより\",\n        \"networkTab\": \"リンクを埋め込む\",\n        \"placeholderText\": \"ファイルをアップロードまたは埋め込む\",\n        \"placeholderDragging\": \"アップロードするファイルをドロップ\",\n        \"dropFileToUpload\": \"アップロードするファイルをドロップ\",\n        \"fileUploadHint\": \"ファイルをここにドロップ\\nまたはクリックして参照\",\n        \"fileUploadHintSuffix\": \"ブラウズ\",\n        \"networkHint\": \"ファイルリンクを貼り付け\",\n        \"networkUrlInvalid\": \"無効なURLです。URLを修正してもう一度お試しください\",\n        \"networkAction\": \"ファイルリンクを埋め込む\",\n        \"fileTooBigError\": \"ファイルサイズが大きすぎます。10MB未満のファイルをアップロードしてください\",\n        \"renameFile\": {\n          \"title\": \"ファイルの名前を変更\",\n          \"description\": \"このファイルの新しい名前を入力してください\",\n          \"nameEmptyError\": \"ファイル名を空にすることはできません。\"\n        },\n        \"uploadedAt\": \"アップロード日: {}\",\n        \"linkedAt\": \"リンク追加日: {}\",\n        \"failedToOpenMsg\": \"開けませんでした。ファイルが見つかりません\"\n      },\n      \"subPage\": {\n        \"handlingPasteHint\": \" - （ペーストの取り扱い）\",\n        \"errors\": {\n          \"failedDeletePage\": \"ページの削除に失敗しました\",\n          \"failedCreatePage\": \"ページの作成に失敗しました\",\n          \"failedMovePage\": \"このドキュメントにページを移動できませんでした\",\n          \"failedDuplicatePage\": \"ページの複製に失敗しました\",\n          \"failedDuplicateFindView\": \"ページの複製に失敗しました - 元のビューが見つかりません\"\n        }\n      },\n      \"cannotMoveToItsChildren\": \"子に移動できません\"\n    },\n    \"outlineBlock\": {\n      \"placeholder\": \"目次\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"'/' を入力してコマンドを使用\"\n    },\n    \"title\": {\n      \"placeholder\": \"無題\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"クリックして画像を追加\",\n      \"upload\": {\n        \"label\": \"アップロード\",\n        \"placeholder\": \"クリックして画像をアップロード\"\n      },\n      \"url\": {\n        \"label\": \"画像URL\",\n        \"placeholder\": \"画像URLを入力\"\n      },\n      \"ai\": {\n        \"label\": \"AIから画像を生成\",\n        \"placeholder\": \"AIに画像を生成させるプロンプトを入力してください\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Stability AIから画像を生成\",\n        \"placeholder\": \"Stability AIに画像を生成させるプロンプトを入力してください\"\n      },\n      \"support\": \"画像サイズの上限は5MBです。サポートされている形式: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"無効な画像です\",\n        \"invalidImageSize\": \"画像サイズは5MB未満である必要があります\",\n        \"invalidImageFormat\": \"サポートされていない画像形式です。サポートされている形式: JPEG, PNG, JPG, GIF, SVG, WEBP\",\n        \"invalidImageUrl\": \"無効な画像URLです\",\n        \"noImage\": \"ファイルまたはディレクトリが見つかりません\",\n        \"multipleImagesFailed\": \"1つ以上の画像のアップロードに失敗しました。再試行してください\"\n      },\n      \"embedLink\": {\n        \"label\": \"リンクを埋め込む\",\n        \"placeholder\": \"画像リンクを貼り付けるか入力\"\n      },\n      \"unsplash\": {\n        \"label\": \"Unsplash\"\n      },\n      \"searchForAnImage\": \"画像を検索\",\n      \"pleaseInputYourOpenAIKey\": \"設定ページでAIキーを入力してください\",\n      \"saveImageToGallery\": \"画像をギャラリーに保存\",\n      \"failedToAddImageToGallery\": \"ギャラリーに画像を追加できませんでした\",\n      \"successToAddImageToGallery\": \"画像がギャラリーに正常に追加されました\",\n      \"unableToLoadImage\": \"画像を読み込めませんでした\",\n      \"maximumImageSize\": \"サポートされている画像の最大サイズは10MBです\",\n      \"uploadImageErrorImageSizeTooBig\": \"画像サイズは10MB未満である必要があります\",\n      \"imageIsUploading\": \"画像をアップロード中\",\n      \"openFullScreen\": \"全画面で開く\",\n      \"interactiveViewer\": {\n        \"toolbar\": {\n          \"previousImageTooltip\": \"前の画像\",\n          \"nextImageTooltip\": \"次の画像\",\n          \"zoomOutTooltip\": \"ズームアウト\",\n          \"zoomInTooltip\": \"ズームイン\",\n          \"changeZoomLevelTooltip\": \"ズームレベルを変更\",\n          \"openLocalImage\": \"画像を開く\",\n          \"downloadImage\": \"画像をダウンロード\",\n          \"closeViewer\": \"インタラクティブビューアを閉じる\",\n          \"scalePercentage\": \"{}%\",\n          \"deleteImageTooltip\": \"画像を削除\"\n        }\n      }\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"言語\",\n        \"placeholder\": \"言語を選択\",\n        \"auto\": \"自動\"\n      },\n      \"copyTooltip\": \"コピー\",\n      \"searchLanguageHint\": \"言語を検索\",\n      \"codeCopiedSnackbar\": \"コードがクリップボードにコピーされました！\"\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"リンクを貼り付けるか入力\",\n      \"openInNewTab\": \"新しいタブで開く\",\n      \"copyLink\": \"リンクをコピー\",\n      \"removeLink\": \"リンクを削除\",\n      \"url\": {\n        \"label\": \"リンクURL\",\n        \"placeholder\": \"リンクURLを入力\"\n      },\n      \"title\": {\n        \"label\": \"リンクタイトル\",\n        \"placeholder\": \"リンクタイトルを入力\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"人物、ページ、日付をメンション...\",\n      \"page\": {\n        \"label\": \"ページへのリンク\",\n        \"tooltip\": \"クリックしてページを開く\"\n      },\n      \"deleted\": \"削除済み\",\n      \"deletedContent\": \"このコンテンツは存在しないか、削除されました\",\n      \"noAccess\": \"アクセス権がありません\",\n      \"deletedPage\": \"削除されたページ\",\n      \"trashHint\": \" - ゴミ箱に入れる\",\n      \"morePages\": \"その他のページ\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"デフォルトに戻す\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"ブロックコンテンツを解析できません\",\n      \"clickToCopyTheBlockContent\": \"クリックしてブロックコンテンツをコピー\",\n      \"blockContentHasBeenCopied\": \"ブロックコンテンツがコピーされました。\",\n      \"parseError\": \"{}ブロックの解析中にエラーが発生しました。\",\n      \"copyBlockContent\": \"ブロックコンテンツをコピー\"\n    },\n    \"mobilePageSelector\": {\n      \"title\": \"ページを選択\",\n      \"failedToLoad\": \"ページリストの読み込みに失敗しました\",\n      \"noPagesFound\": \"ページが見つかりません\"\n    },\n    \"attachmentMenu\": {\n      \"choosePhoto\": \"写真を選択\",\n      \"takePicture\": \"写真を撮る\",\n      \"chooseFile\": \"ファイルを選択\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"label\": \"カラム\",\n      \"createNewCard\": \"新規\",\n      \"renameGroupTooltip\": \"押してグループ名を変更\",\n      \"createNewColumn\": \"新しいグループを追加\",\n      \"addToColumnTopTooltip\": \"上に新しいカードを追加\",\n      \"addToColumnBottomTooltip\": \"下に新しいカードを追加\",\n      \"renameColumn\": \"名前を変更\",\n      \"hideColumn\": \"非表示\",\n      \"newGroup\": \"新しいグループ\",\n      \"deleteColumn\": \"削除\",\n      \"deleteColumnConfirmation\": \"このグループとその中のすべてのカードが削除されます。\\n続行してもよろしいですか？\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"非表示のグループ\",\n      \"collapseTooltip\": \"非表示グループを隠す\",\n      \"expandTooltip\": \"非表示グループを表示\"\n    },\n    \"cardDetail\": \"カードの詳細\",\n    \"cardActions\": \"カードアクション\",\n    \"cardDuplicated\": \"カードが複製されました\",\n    \"cardDeleted\": \"カードが削除されました\",\n    \"showOnCard\": \"カードの詳細に表示\",\n    \"setting\": \"設定\",\n    \"propertyName\": \"プロパティ名\",\n    \"menuName\": \"ボード\",\n    \"showUngrouped\": \"グループ化されていない項目を表示\",\n    \"ungroupedButtonText\": \"グループ化されていない\",\n    \"ungroupedButtonTooltip\": \"どのグループにも属していないカードが含まれています\",\n    \"ungroupedItemsTitle\": \"クリックしてボードに追加\",\n    \"groupBy\": \"グループ化\",\n    \"groupCondition\": \"グループ条件\",\n    \"referencedBoardPrefix\": \"表示元\",\n    \"notesTooltip\": \"内部のメモ\",\n    \"mobile\": {\n      \"editURL\": \"URLを編集\",\n      \"showGroup\": \"グループを表示\",\n      \"showGroupContent\": \"このグループをボード上に表示してもよろしいですか？\",\n      \"failedToLoad\": \"ボードビューの読み込みに失敗しました\"\n    },\n    \"dateCondition\": {\n      \"weekOf\": \"{}週 - {}\",\n      \"today\": \"今日\",\n      \"yesterday\": \"昨日\",\n      \"tomorrow\": \"明日\",\n      \"lastSevenDays\": \"過去7日間\",\n      \"nextSevenDays\": \"次の7日間\",\n      \"lastThirtyDays\": \"過去30日間\",\n      \"nextThirtyDays\": \"次の30日間\"\n    },\n    \"noGroup\": \"プロパティによるグループ化なし\",\n    \"noGroupDesc\": \"ボードビューを表示するには、グループ化するプロパティが必要です\",\n    \"media\": {\n      \"cardText\": \"{} {}\",\n      \"fallbackName\": \"ファイル\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"カレンダー\",\n    \"defaultNewCalendarTitle\": \"無題\",\n    \"newEventButtonTooltip\": \"新しいイベントを追加\",\n    \"navigation\": {\n      \"today\": \"今日\",\n      \"jumpToday\": \"今日にジャンプ\",\n      \"previousMonth\": \"前の月\",\n      \"nextMonth\": \"次の月\",\n      \"views\": {\n        \"day\": \"日\",\n        \"week\": \"週\",\n        \"month\": \"月\",\n        \"year\": \"年\"\n      }\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"まだイベントがありません\",\n      \"emptyBody\": \"プラスボタンを押してこの日にイベントを作成してください。\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"週番号を表示\",\n      \"showWeekends\": \"週末を表示\",\n      \"firstDayOfWeek\": \"週の開始日\",\n      \"layoutDateField\": \"カレンダーのレイアウト\",\n      \"changeLayoutDateField\": \"レイアウトフィールドを変更\",\n      \"noDateTitle\": \"日付なし\",\n      \"noDateHint\": {\n        \"zero\": \"予定されていないイベントがここに表示されます\",\n        \"one\": \"{count} 件の予定されていないイベント\",\n        \"other\": \"{count} 件の予定されていないイベント\"\n      },\n      \"unscheduledEventsTitle\": \"予定されていないイベント\",\n      \"clickToAdd\": \"クリックしてカレンダーに追加\",\n      \"name\": \"カレンダー設定\",\n      \"clickToOpen\": \"クリックしてレコードを開く\"\n    },\n    \"referencedCalendarPrefix\": \"表示元\",\n    \"quickJumpYear\": \"ジャンプ\",\n    \"duplicateEvent\": \"イベントを複製\"\n  },\n  \"errorDialog\": {\n    \"title\": \"@:appName エラー\",\n    \"howToFixFallback\": \"ご不便をおかけして申し訳ありません！エラー内容をGitHubページに報告してください。\",\n    \"howToFixFallbackHint1\": \"ご不便をおかけして申し訳ありません！エラー内容を報告するには、\",\n    \"howToFixFallbackHint2\": \"ページにアクセスしてください。\",\n    \"github\": \"GitHubで表示\"\n  },\n  \"search\": {\n    \"label\": \"検索\",\n    \"sidebarSearchIcon\": \"検索してページに素早く移動\",\n    \"placeholder\": {\n      \"actions\": \"アクションを検索...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"コピー完了！\",\n      \"fail\": \"コピーに失敗しました\"\n    }\n  },\n  \"unSupportBlock\": \"このバージョンではこのブロックはサポートされていません。\",\n  \"views\": {\n    \"deleteContentTitle\": \"{pageType}を削除してもよろしいですか？\",\n    \"deleteContentCaption\": \"この{pageType}を削除すると、ゴミ箱から復元できます。\"\n  },\n  \"colors\": {\n    \"custom\": \"カスタム\",\n    \"default\": \"デフォルト\",\n    \"red\": \"赤\",\n    \"orange\": \"オレンジ\",\n    \"yellow\": \"黄色\",\n    \"green\": \"緑\",\n    \"blue\": \"青\",\n    \"purple\": \"紫\",\n    \"pink\": \"ピンク\",\n    \"brown\": \"茶色\",\n    \"gray\": \"灰色\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"絵文字\",\n    \"search\": \"絵文字を検索\",\n    \"noRecent\": \"最近使用された絵文字なし\",\n    \"noEmojiFound\": \"絵文字が見つかりません\",\n    \"filter\": \"フィルター\",\n    \"random\": \"ランダム\",\n    \"selectSkinTone\": \"スキントーンを選択\",\n    \"remove\": \"絵文字を削除\",\n    \"categories\": {\n      \"smileys\": \"スマイリー＆感情\",\n      \"people\": \"人々\",\n      \"animals\": \"自然\",\n      \"food\": \"食べ物\",\n      \"activities\": \"アクティビティ\",\n      \"places\": \"場所\",\n      \"objects\": \"オブジェクト\",\n      \"symbols\": \"記号\",\n      \"flags\": \"旗\",\n      \"nature\": \"自然\",\n      \"frequentlyUsed\": \"よく使われる\"\n    },\n    \"skinTone\": {\n      \"default\": \"デフォルト\",\n      \"light\": \"明るい\",\n      \"mediumLight\": \"やや明るい\",\n      \"medium\": \"普通\",\n      \"mediumDark\": \"やや暗い\",\n      \"dark\": \"暗い\"\n    },\n    \"openSourceIconsFrom\": \"オープンソースのアイコン提供元\"\n  },\n  \"inlineActions\": {\n    \"noResults\": \"結果なし\",\n    \"recentPages\": \"最近のページ\",\n    \"pageReference\": \"ページ参照\",\n    \"docReference\": \"ドキュメント参照\",\n    \"boardReference\": \"ボード参照\",\n    \"calReference\": \"カレンダー参照\",\n    \"gridReference\": \"グリッド参照\",\n    \"date\": \"日付\",\n    \"reminder\": {\n      \"groupTitle\": \"リマインダー\",\n      \"shortKeyword\": \"リマインド\"\n    },\n    \"createPage\": \"\\\"{}\\\" サブページを作成\"\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"設定で日付と時刻の形式を変更\",\n    \"dateFormat\": \"日付形式\",\n    \"includeTime\": \"時刻を含む\",\n    \"isRange\": \"終了日\",\n    \"timeFormat\": \"時刻形式\",\n    \"clearDate\": \"日付をクリア\",\n    \"reminderLabel\": \"リマインダー\",\n    \"selectReminder\": \"リマインダーを選択\",\n    \"reminderOptions\": {\n      \"none\": \"なし\",\n      \"atTimeOfEvent\": \"イベント時\",\n      \"fiveMinsBefore\": \"5分前\",\n      \"tenMinsBefore\": \"10分前\",\n      \"fifteenMinsBefore\": \"15分前\",\n      \"thirtyMinsBefore\": \"30分前\",\n      \"oneHourBefore\": \"1時間前\",\n      \"twoHoursBefore\": \"2時間前\",\n      \"onDayOfEvent\": \"イベント当日\",\n      \"oneDayBefore\": \"1日前\",\n      \"twoDaysBefore\": \"2日前\",\n      \"oneWeekBefore\": \"1週間前\",\n      \"custom\": \"カスタム\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"昨日\",\n    \"today\": \"今日\",\n    \"tomorrow\": \"明日\",\n    \"oneWeek\": \"1週間\"\n  },\n  \"notificationHub\": {\n    \"title\": \"通知\",\n    \"mobile\": {\n      \"title\": \"更新\"\n    },\n    \"emptyTitle\": \"すべて完了！\",\n    \"emptyBody\": \"保留中の通知やアクションはありません。\",\n    \"tabs\": {\n      \"inbox\": \"受信箱\",\n      \"upcoming\": \"今後\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"すべて既読にする\",\n      \"showAll\": \"すべて表示\",\n      \"showUnreads\": \"未読のみ表示\"\n    },\n    \"filters\": {\n      \"ascending\": \"昇順\",\n      \"descending\": \"降順\",\n      \"groupByDate\": \"日付でグループ化\",\n      \"showUnreadsOnly\": \"未読のみ表示\",\n      \"resetToDefault\": \"デフォルトにリセット\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"リマインダー\",\n    \"message\": \"忘れないうちに確認してください！\",\n    \"tooltipDelete\": \"削除\",\n    \"tooltipMarkRead\": \"既読にする\",\n    \"tooltipMarkUnread\": \"未読にする\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"検索\",\n    \"previousMatch\": \"前の一致\",\n    \"nextMatch\": \"次の一致\",\n    \"close\": \"閉じる\",\n    \"replace\": \"置換\",\n    \"replaceAll\": \"すべて置換\",\n    \"noResult\": \"結果なし\",\n    \"caseSensitive\": \"大文字と小文字を区別\",\n    \"searchMore\": \"さらに検索して結果を探す\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"申し訳ありません\",\n    \"loadingViewError\": \"このビューの読み込みに問題があります。インターネット接続を確認し、アプリを更新してください。問題が続く場合は、チームにお問い合わせください。\",\n    \"syncError\": \"別のデバイスからデータが同期されていません\",\n    \"syncErrorHint\": \"最後に編集したデバイスでこのページを再度開き、次に現在のデバイスで再度開いてください。\",\n    \"clickToCopy\": \"クリックしてエラーコードをコピー\"\n  },\n  \"editor\": {\n    \"bold\": \"太字\",\n    \"bulletedList\": \"箇条書き\",\n    \"bulletedListShortForm\": \"箇条書き\",\n    \"checkbox\": \"チェックボックス\",\n    \"embedCode\": \"コードを埋め込む\",\n    \"heading1\": \"見出し1\",\n    \"heading2\": \"見出し2\",\n    \"heading3\": \"見出し3\",\n    \"highlight\": \"ハイライト\",\n    \"color\": \"色\",\n    \"image\": \"画像\",\n    \"date\": \"日付\",\n    \"page\": \"ページ\",\n    \"italic\": \"斜体\",\n    \"link\": \"リンク\",\n    \"numberedList\": \"番号付きリスト\",\n    \"numberedListShortForm\": \"番号付き\",\n    \"toggleHeading1ShortForm\": \"h1　トグル\",\n    \"toggleHeading2ShortForm\": \"h2　トグル\",\n    \"toggleHeading3ShortForm\": \"h3　トグル\",\n    \"quote\": \"引用\",\n    \"strikethrough\": \"取り消し線\",\n    \"text\": \"テキスト\",\n    \"underline\": \"下線\",\n    \"fontColorDefault\": \"デフォルト\",\n    \"fontColorGray\": \"灰色\",\n    \"fontColorBrown\": \"茶色\",\n    \"fontColorOrange\": \"オレンジ\",\n    \"fontColorYellow\": \"黄色\",\n    \"fontColorGreen\": \"緑\",\n    \"fontColorBlue\": \"青\",\n    \"fontColorPurple\": \"紫\",\n    \"fontColorPink\": \"ピンク\",\n    \"fontColorRed\": \"赤\",\n    \"backgroundColorDefault\": \"デフォルト背景\",\n    \"backgroundColorGray\": \"灰色背景\",\n    \"backgroundColorBrown\": \"茶色背景\",\n    \"backgroundColorOrange\": \"オレンジ背景\",\n    \"backgroundColorYellow\": \"黄色背景\",\n    \"backgroundColorGreen\": \"緑背景\",\n    \"backgroundColorBlue\": \"青背景\",\n    \"backgroundColorPurple\": \"紫背景\",\n    \"backgroundColorPink\": \"ピンク背景\",\n    \"backgroundColorRed\": \"赤背景\",\n    \"backgroundColorLime\": \"ライム背景\",\n    \"backgroundColorAqua\": \"アクア背景\",\n    \"done\": \"完了\",\n    \"cancel\": \"キャンセル\",\n    \"tint1\": \"ティント1\",\n    \"tint2\": \"ティント2\",\n    \"tint3\": \"ティント3\",\n    \"tint4\": \"ティント4\",\n    \"tint5\": \"ティント5\",\n    \"tint6\": \"ティント6\",\n    \"tint7\": \"ティント7\",\n    \"tint8\": \"ティント8\",\n    \"tint9\": \"ティント9\",\n    \"lightLightTint1\": \"紫\",\n    \"lightLightTint2\": \"ピンク\",\n    \"lightLightTint3\": \"ライトピンク\",\n    \"lightLightTint4\": \"オレンジ\",\n    \"lightLightTint5\": \"黄色\",\n    \"lightLightTint6\": \"ライム\",\n    \"lightLightTint7\": \"緑\",\n    \"lightLightTint8\": \"アクア\",\n    \"lightLightTint9\": \"青\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"見出し1\",\n    \"mobileHeading2\": \"見出し2\",\n    \"mobileHeading3\": \"見出し3\",\n    \"mobileHeading4\": \"見出し4\",\n    \"mobileHeading5\": \"見出し5\",\n    \"mobileHeading6\": \"見出し6\",\n    \"textColor\": \"テキスト色\",\n    \"backgroundColor\": \"背景色\",\n    \"addYourLink\": \"リンクを追加\",\n    \"openLink\": \"リンクを開く\",\n    \"copyLink\": \"リンクをコピー\",\n    \"removeLink\": \"リンクを削除\",\n    \"editLink\": \"リンクを編集\",\n    \"linkText\": \"テキスト\",\n    \"linkTextHint\": \"テキストを入力してください\",\n    \"linkAddressHint\": \"URLを入力してください\",\n    \"highlightColor\": \"ハイライト色\",\n    \"clearHighlightColor\": \"ハイライト色をクリア\",\n    \"customColor\": \"カスタムカラー\",\n    \"hexValue\": \"16進数値\",\n    \"opacity\": \"不透明度\",\n    \"resetToDefaultColor\": \"デフォルト色にリセット\",\n    \"ltr\": \"左から右\",\n    \"rtl\": \"右から左\",\n    \"auto\": \"自動\",\n    \"cut\": \"切り取り\",\n    \"copy\": \"コピー\",\n    \"paste\": \"貼り付け\",\n    \"find\": \"検索\",\n    \"select\": \"選択\",\n    \"selectAll\": \"すべて選択\",\n    \"previousMatch\": \"前の一致\",\n    \"nextMatch\": \"次の一致\",\n    \"closeFind\": \"閉じる\",\n    \"replace\": \"置換\",\n    \"replaceAll\": \"すべて置換\",\n    \"regex\": \"正規表現\",\n    \"caseSensitive\": \"大文字小文字を区別\",\n    \"uploadImage\": \"画像をアップロード\",\n    \"urlImage\": \"URL画像\",\n    \"incorrectLink\": \"不正なリンク\",\n    \"upload\": \"アップロード\",\n    \"chooseImage\": \"画像を選択\",\n    \"loading\": \"読み込み中\",\n    \"imageLoadFailed\": \"画像の読み込みに失敗しました\",\n    \"divider\": \"区切り線\",\n    \"table\": \"テーブル\",\n    \"colAddBefore\": \"前に追加\",\n    \"rowAddBefore\": \"前に追加\",\n    \"colAddAfter\": \"後に追加\",\n    \"rowAddAfter\": \"後に追加\",\n    \"colRemove\": \"削除\",\n    \"rowRemove\": \"削除\",\n    \"colDuplicate\": \"複製\",\n    \"rowDuplicate\": \"複製\",\n    \"colClear\": \"内容をクリア\",\n    \"rowClear\": \"内容をクリア\",\n    \"slashPlaceHolder\": \"'/'を入力してブロックを挿入、または入力を開始\",\n    \"typeSomething\": \"何かを入力...\",\n    \"toggleListShortForm\": \"トグル\",\n    \"quoteListShortForm\": \"引用\",\n    \"mathEquationShortForm\": \"数式\",\n    \"codeBlockShortForm\": \"コード\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"お気に入りページがありません\",\n    \"noFavoriteHintText\": \"ページを左にスワイプして、お気に入りに追加します\",\n    \"removeFromSidebar\": \"サイドバーから削除\",\n    \"addToSidebar\": \"サイドバーにピン留め\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"ブロックを挿入するには「/」を入力、または入力を開始\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"To-do\",\n    \"bulletList\": \"リスト\",\n    \"numberList\": \"リスト\",\n    \"quote\": \"引用\",\n    \"heading\": \"見出し {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"ページアイコン\",\n    \"language\": \"言語\",\n    \"font\": \"フォント\",\n    \"actions\": \"アクション\",\n    \"date\": \"日付\",\n    \"addField\": \"フィールドを追加\",\n    \"userIcon\": \"ユーザーアイコン\"\n  },\n  \"noLogFiles\": \"ログファイルがありません\",\n  \"newSettings\": {\n    \"myAccount\": {\n      \"title\": \"アカウント\",\n      \"subtitle\": \"プロフィールのカスタマイズ、アカウントセキュリティ管理、AIキーの設定、アカウントへのログイン管理。\",\n      \"profileLabel\": \"アカウント名とプロフィール画像\",\n      \"profileNamePlaceholder\": \"名前を入力\",\n      \"accountSecurity\": \"アカウントセキュリティ\",\n      \"2FA\": \"2段階認証\",\n      \"aiKeys\": \"AIキー\",\n      \"accountLogin\": \"アカウントログイン\",\n      \"updateNameError\": \"名前の更新に失敗しました\",\n      \"updateIconError\": \"アイコンの更新に失敗しました\",\n      \"deleteAccount\": {\n        \"title\": \"アカウント削除\",\n        \"subtitle\": \"アカウントとすべてのデータを完全に削除します。\",\n        \"description\": \"アカウントを削除し、すべてのワークスペースへのアクセスを削除します。\",\n        \"deleteMyAccount\": \"アカウントを削除\",\n        \"dialogTitle\": \"アカウント削除\",\n        \"dialogContent1\": \"アカウントを完全に削除してよろしいですか？\",\n        \"dialogContent2\": \"この操作は元に戻せません。すべてのワークスペースへのアクセスが削除され、アカウント全体とプライベートワークスペースを含むすべてが削除されます。\",\n        \"confirmHint1\": \"確認のために「DELETE MY ACCOUNT」と入力してください。\",\n        \"confirmHint2\": \"この操作が元に戻せないことを理解しました。\",\n        \"confirmHint3\": \"DELETE MY ACCOUNT\",\n        \"checkToConfirmError\": \"削除の確認チェックボックスを選択してください\",\n        \"failedToGetCurrentUser\": \"現在のユーザーの取得に失敗しました\",\n        \"confirmTextValidationFailed\": \"確認テキストが「DELETE MY ACCOUNT」と一致しません\",\n        \"deleteAccountSuccess\": \"アカウントが正常に削除されました\"\n      }\n    },\n    \"workplace\": {\n      \"name\": \"ワークプレース\",\n      \"title\": \"ワークプレース設定\",\n      \"subtitle\": \"ワークスペースの外観、テーマ、フォント、テキストレイアウト、日付、時刻、言語をカスタマイズします。\",\n      \"workplaceName\": \"ワークプレース名\",\n      \"workplaceNamePlaceholder\": \"ワークプレース名を入力\",\n      \"workplaceIcon\": \"ワークプレースアイコン\",\n      \"workplaceIconSubtitle\": \"画像をアップロードするか、絵文字を使用してワークスペースを表現します。アイコンはサイドバーや通知で表示されます。\",\n      \"renameError\": \"ワークプレースの名前変更に失敗しました\",\n      \"updateIconError\": \"アイコンの更新に失敗しました\",\n      \"chooseAnIcon\": \"アイコンを選択\",\n      \"appearance\": {\n        \"name\": \"外観\",\n        \"themeMode\": {\n          \"auto\": \"自動\",\n          \"light\": \"ライト\",\n          \"dark\": \"ダーク\"\n        },\n        \"language\": \"言語\"\n      }\n    },\n    \"syncState\": {\n      \"syncing\": \"同期中\",\n      \"synced\": \"同期済み\",\n      \"noNetworkConnected\": \"ネットワークに接続されていません\"\n    }\n  },\n  \"pageStyle\": {\n    \"title\": \"ページスタイル\",\n    \"layout\": \"レイアウト\",\n    \"coverImage\": \"カバー画像\",\n    \"pageIcon\": \"ページアイコン\",\n    \"colors\": \"色\",\n    \"gradient\": \"グラデーション\",\n    \"backgroundImage\": \"背景画像\",\n    \"presets\": \"プリセット\",\n    \"photo\": \"写真\",\n    \"unsplash\": \"Unsplash\",\n    \"pageCover\": \"ページカバー\",\n    \"none\": \"なし\",\n    \"openSettings\": \"設定を開く\",\n    \"photoPermissionTitle\": \"@:appName はフォトライブラリへのアクセスを希望します\",\n    \"photoPermissionDescription\": \"@:appName に写真へのアクセス権が必要です。写真をドキュメントに追加するには許可が必要です。\",\n    \"cameraPermissionTitle\": \"@:appNameカメラにアクセスしようとしています\",\n    \"cameraPermissionDescription\": \"カメラからドキュメントに画像を追加するには、 @:appNameにカメラへのアクセスを許可する必要があります\",\n    \"doNotAllow\": \"許可しない\",\n    \"image\": \"画像\"\n  },\n  \"commandPalette\": {\n    \"placeholder\": \"検索ワードを入力...\",\n    \"bestMatches\": \"最適な候補\",\n    \"recentHistory\": \"最近の履歴\",\n    \"navigateHint\": \"移動するには\",\n    \"loadingTooltip\": \"結果を検索中...\",\n    \"betaLabel\": \"ベータ\",\n    \"betaTooltip\": \"現在はページやドキュメントの内容のみの検索をサポートしています\",\n    \"fromTrashHint\": \"ゴミ箱から\",\n    \"noResultsHint\": \"目的の結果が見つかりませんでした。別のキーワードで検索してください。\",\n    \"clearSearchTooltip\": \"検索フィールドをクリア\"\n  },\n  \"space\": {\n    \"delete\": \"削除\",\n    \"deleteConfirmation\": \"削除: \",\n    \"deleteConfirmationDescription\": \"このスペース内のすべてのページが削除され、ゴミ箱に移動されます。公開されたページは公開が取り消されます。\",\n    \"rename\": \"スペースの名前変更\",\n    \"changeIcon\": \"アイコンを変更\",\n    \"manage\": \"スペースの管理\",\n    \"addNewSpace\": \"スペースを作成\",\n    \"collapseAllSubPages\": \"すべてのサブページを折りたたむ\",\n    \"createNewSpace\": \"新しいスペースを作成\",\n    \"createSpaceDescription\": \"複数のパブリックおよびプライベートスペースを作成し、作業を整理しましょう。\",\n    \"spaceName\": \"スペース名\",\n    \"spaceNamePlaceholder\": \"例: マーケティング、エンジニアリング、HR\",\n    \"permission\": \"権限\",\n    \"publicPermission\": \"パブリック\",\n    \"publicPermissionDescription\": \"フルアクセスを持つすべてのワークスペースメンバー\",\n    \"privatePermission\": \"プライベート\",\n    \"privatePermissionDescription\": \"あなたのみがこのスペースにアクセス可能\",\n    \"spaceIconBackground\": \"アイコンの背景色\",\n    \"spaceIcon\": \"アイコン\",\n    \"dangerZone\": \"危険ゾーン\",\n    \"unableToDeleteLastSpace\": \"最後のスペースは削除できません\",\n    \"unableToDeleteSpaceNotCreatedByYou\": \"他のユーザーが作成したスペースは削除できません\",\n    \"enableSpacesForYourWorkspace\": \"ワークスペースでスペースを有効にする\",\n    \"title\": \"スペース\",\n    \"defaultSpaceName\": \"全般\",\n    \"upgradeSpaceTitle\": \"スペースを有効にする\",\n    \"upgradeSpaceDescription\": \"複数のパブリックおよびプライベートスペースを作成し、ワークスペースを整理しましょう。\",\n    \"upgrade\": \"アップグレード\",\n    \"upgradeYourSpace\": \"複数のスペースを作成\",\n    \"quicklySwitch\": \"次のスペースに素早く切り替える\",\n    \"duplicate\": \"スペースを複製\",\n    \"movePageToSpace\": \"ページをスペースに移動\",\n    \"cannotMovePageToDatabase\": \"ページをデータベースに移動できません\",\n    \"switchSpace\": \"スペースを切り替え\",\n    \"spaceNameCannotBeEmpty\": \"スペース名は空にできません\",\n    \"success\": {\n      \"deleteSpace\": \"スペースが正常に削除されました\",\n      \"renameSpace\": \"スペース名の変更に成功しました\",\n      \"duplicateSpace\": \"スペースが正常に複製されました\",\n      \"updateSpace\": \"スペースが正常に更新されました\"\n    },\n    \"error\": {\n      \"deleteSpace\": \"スペースを削除できませんでした\",\n      \"renameSpace\": \"スペースの名前を変更できませんでした\",\n      \"duplicateSpace\": \"スペースの複製に失敗しました\",\n      \"updateSpace\": \"スペースの更新に失敗しました\"\n    },\n    \"createSpace\": \"スペースを作る\",\n    \"manageSpace\": \"スペースを管理する\",\n    \"renameSpace\": \"スペースの名前を変更する\",\n    \"mSpaceIconColor\": \"スペースアイコンの色\",\n    \"mSpaceIcon\": \"スペースアイコン\"\n  },\n  \"publish\": {\n    \"hasNotBeenPublished\": \"このページはまだ公開されていません\",\n    \"spaceHasNotBeenPublished\": \"スペースの公開はまだサポートされていません\",\n    \"reportPage\": \"ページを報告\",\n    \"databaseHasNotBeenPublished\": \"データベースの公開はまだサポートされていません。\",\n    \"createdWith\": \"作成元\",\n    \"downloadApp\": \"AppFlowy をダウンロード\",\n    \"copy\": {\n      \"codeBlock\": \"コードブロックの内容がクリップボードにコピーされました\",\n      \"imageBlock\": \"画像リンクがクリップボードにコピーされました\",\n      \"mathBlock\": \"数式がクリップボードにコピーされました\",\n      \"fileBlock\": \"ファイルリンクがクリップボードにコピーされました\"\n    },\n    \"containsPublishedPage\": \"このページには公開済みのページが含まれています。続行すると公開が解除されます。削除してもよろしいですか？\",\n    \"publishSuccessfully\": \"正常に公開されました\",\n    \"unpublishSuccessfully\": \"正常に非公開にされました\",\n    \"publishFailed\": \"公開に失敗しました\",\n    \"unpublishFailed\": \"非公開に失敗しました\",\n    \"noAccessToVisit\": \"このページへのアクセス権がありません...\",\n    \"createWithAppFlowy\": \"AppFlowy でウェブサイトを作成\",\n    \"fastWithAI\": \"AIを使って迅速かつ簡単に。\",\n    \"tryItNow\": \"今すぐ試してみる\",\n    \"onlyGridViewCanBePublished\": \"グリッドビューのみが公開可能です\",\n    \"database\": {\n      \"zero\": \"{} 選択したビューを公開\",\n      \"one\": \"{} 選択したビューを公開\",\n      \"many\": \"{} 選択したビューを公開\",\n      \"other\": \"{} 選択したビューを公開\"\n    },\n    \"mustSelectPrimaryDatabase\": \"プライマリビューを選択する必要があります\",\n    \"noDatabaseSelected\": \"少なくとも1つのデータベースを選択してください。\",\n    \"unableToDeselectPrimaryDatabase\": \"プライマリデータベースの選択を解除できません\",\n    \"saveThisPage\": \"このテンプレートを使用\",\n    \"duplicateTitle\": \"どこに追加しますか？\",\n    \"selectWorkspace\": \"ワークスペースを選択\",\n    \"addTo\": \"追加\",\n    \"duplicateSuccessfully\": \"ワークスペースに追加されました\",\n    \"duplicateSuccessfullyDescription\": \"AppFlowy がインストールされていませんか？ 「ダウンロード」をクリックすると、自動的にダウンロードが開始されます。\",\n    \"downloadIt\": \"ダウンロード\",\n    \"openApp\": \"アプリで開く\",\n    \"duplicateFailed\": \"複製に失敗しました\",\n    \"membersCount\": {\n      \"zero\": \"メンバーなし\",\n      \"one\": \"1人のメンバー\",\n      \"many\": \"{count}人のメンバー\",\n      \"other\": \"{count}人のメンバー\"\n    },\n    \"useThisTemplate\": \"このテンプレートを使用\"\n  },\n  \"web\": {\n    \"continue\": \"続ける\",\n    \"or\": \"または\",\n    \"continueWithGoogle\": \"Googleで続ける\",\n    \"continueWithGithub\": \"GitHubで続ける\",\n    \"continueWithDiscord\": \"Discordで続ける\",\n    \"continueWithApple\": \"Appleで続ける\",\n    \"moreOptions\": \"他のオプション\",\n    \"collapse\": \"折りたたむ\",\n    \"signInAgreement\": \"「続ける」をクリックすると、AppFlowyの\",\n    \"and\": \"および\",\n    \"termOfUse\": \"利用規約\",\n    \"privacyPolicy\": \"プライバシーポリシー\",\n    \"signInError\": \"サインインエラー\",\n    \"login\": \"サインアップまたはログイン\",\n    \"fileBlock\": {\n      \"uploadedAt\": \"{time}にアップロード\",\n      \"linkedAt\": \"{time}にリンク追加\",\n      \"empty\": \"ファイルをアップロードまたは埋め込み\",\n      \"uploadFailed\": \"アップロードに失敗しました。もう一度お試しください。\",\n      \"retry\": \"リトライ\"\n    },\n    \"importNotion\": \"Notionからインポート\",\n    \"import\": \"輸入\",\n    \"importSuccess\": \"アップロードに成功しました\",\n    \"importSuccessMessage\": \"インポートが完了すると通知されます。その後、インポートしたページをサイドバーで表示できます。\",\n    \"importFailed\": \"インポートに失敗しました。ファイル形式を確認してください\",\n    \"dropNotionFile\": \"Notionのzipファイルをここにドロップしてアップロードするか、クリックして参照してください\",\n    \"error\": {\n      \"pageNameIsEmpty\": \"ページ名が空です。別の名前を試してください\"\n    }\n  },\n  \"globalComment\": {\n    \"comments\": \"コメント\",\n    \"addComment\": \"コメントを追加\",\n    \"reactedBy\": \"リアクションした人\",\n    \"addReaction\": \"リアクションを追加\",\n    \"reactedByMore\": \"他 {count} 人\",\n    \"showSeconds\": {\n      \"one\": \"1秒前\",\n      \"other\": \"{count}秒前\",\n      \"zero\": \"たった今\",\n      \"many\": \"{count}秒前\"\n    },\n    \"showMinutes\": {\n      \"one\": \"1分前\",\n      \"other\": \"{count}分前\",\n      \"many\": \"{count}分前\"\n    },\n    \"showHours\": {\n      \"one\": \"1時間前\",\n      \"other\": \"{count}時間前\",\n      \"many\": \"{count}時間前\"\n    },\n    \"showDays\": {\n      \"one\": \"1日前\",\n      \"other\": \"{count}日前\",\n      \"many\": \"{count}日前\"\n    },\n    \"showMonths\": {\n      \"one\": \"1ヶ月前\",\n      \"other\": \"{count}ヶ月前\",\n      \"many\": \"{count}ヶ月前\"\n    },\n    \"showYears\": {\n      \"one\": \"1年前\",\n      \"other\": \"{count}年前\",\n      \"many\": \"{count}年前\"\n    },\n    \"reply\": \"返信\",\n    \"deleteComment\": \"コメントを削除\",\n    \"youAreNotOwner\": \"このコメントの所有者ではありません\",\n    \"confirmDeleteDescription\": \"このコメントを削除してもよろしいですか？\",\n    \"hasBeenDeleted\": \"削除済み\",\n    \"replyingTo\": \"返信先\",\n    \"noAccessDeleteComment\": \"このコメントを削除する権限がありません\",\n    \"collapse\": \"折りたたむ\",\n    \"readMore\": \"続きを読む\",\n    \"failedToAddComment\": \"コメントの追加に失敗しました...\",\n    \"commentAddedSuccessfully\": \"コメントが正常に追加されました。\",\n    \"commentAddedSuccessTip\": \"コメントまたは返信を追加しました。最新のコメントを確認するためにトップに移動しますか？\"\n  },\n  \"template\": {\n    \"asTemplate\": \"テンプレートとして保存\",\n    \"name\": \"テンプレート名\",\n    \"description\": \"テンプレートの説明\",\n    \"about\": \"テンプレートについて\",\n    \"preview\": \"テンプレートのプレビュー\",\n    \"categories\": \"テンプレートのカテゴリー\",\n    \"isNewTemplate\": \"新しいテンプレートとしてピン留め\",\n    \"featured\": \"おすすめとしてピン留め\",\n    \"relatedTemplates\": \"関連テンプレート\",\n    \"requiredField\": \"{field}は必須です\",\n    \"addCategory\": \"「{category}」を追加\",\n    \"addNewCategory\": \"新しいカテゴリーを追加\",\n    \"addNewCreator\": \"新しい作成者を追加\",\n    \"deleteCategory\": \"カテゴリーを削除\",\n    \"editCategory\": \"カテゴリーを編集\",\n    \"editCreator\": \"作成者を編集\",\n    \"category\": {\n      \"name\": \"カテゴリー名\",\n      \"icon\": \"カテゴリーアイコン\",\n      \"bgColor\": \"カテゴリー背景色\",\n      \"priority\": \"カテゴリーの優先度\",\n      \"desc\": \"カテゴリーの説明\",\n      \"type\": \"カテゴリーの種類\",\n      \"icons\": \"カテゴリーアイコン\",\n      \"colors\": \"カテゴリーの色\",\n      \"byUseCase\": \"ユースケース別\",\n      \"byFeature\": \"機能別\",\n      \"deleteCategory\": \"カテゴリーを削除\",\n      \"deleteCategoryDescription\": \"このカテゴリーを削除してもよろしいですか？\",\n      \"typeToSearch\": \"カテゴリーを検索...\"\n    },\n    \"creator\": {\n      \"label\": \"テンプレート作成者\",\n      \"name\": \"作成者名\",\n      \"avatar\": \"作成者のアバター\",\n      \"accountLinks\": \"作成者のアカウントリンク\",\n      \"uploadAvatar\": \"クリックしてアバターをアップロード\",\n      \"deleteCreator\": \"作成者を削除\",\n      \"deleteCreatorDescription\": \"この作成者を削除してもよろしいですか？\",\n      \"typeToSearch\": \"作成者を検索...\"\n    },\n    \"uploadSuccess\": \"テンプレートが正常にアップロードされました。\",\n    \"uploadSuccessDescription\": \"テンプレートが正常にアップロードされました。テンプレートギャラリーで確認できます。\",\n    \"viewTemplate\": \"テンプレートを表示\",\n    \"deleteTemplate\": \"テンプレートを削除\",\n    \"deleteSuccess\": \"テンプレートが正常に削除されました\",\n    \"deleteTemplateDescription\": \"このテンプレートを削除してもよろしいですか？\",\n    \"addRelatedTemplate\": \"関連テンプレートを追加\",\n    \"removeRelatedTemplate\": \"関連テンプレートを削除\",\n    \"uploadAvatar\": \"アバターをアップロード\",\n    \"searchInCategory\": \"{category}で検索\",\n    \"label\": \"テンプレート\"\n  },\n  \"fileDropzone\": {\n    \"dropFile\": \"クリックまたはドラッグしてファイルをアップロード\",\n    \"uploading\": \"アップロード中...\",\n    \"uploadFailed\": \"アップロードに失敗しました\",\n    \"uploadSuccess\": \"アップロード成功\",\n    \"uploadSuccessDescription\": \"ファイルが正常にアップロードされました\",\n    \"uploadFailedDescription\": \"ファイルのアップロードに失敗しました\",\n    \"uploadingDescription\": \"ファイルをアップロード中\"\n  },\n  \"gallery\": {\n    \"preview\": \"全画面表示で開く\",\n    \"copy\": \"コピー\",\n    \"download\": \"ダウンロード\",\n    \"prev\": \"前へ\",\n    \"next\": \"次へ\",\n    \"resetZoom\": \"ズームをリセット\",\n    \"zoomIn\": \"ズームイン\",\n    \"zoomOut\": \"ズームアウト\"\n  },\n  \"invitation\": {\n    \"join\": \"参加する\",\n    \"on\": \"の上\",\n    \"invitedBy\": \"招待者\",\n    \"membersCount\": {\n      \"zero\": \"{count} 人のメンバー\",\n      \"one\": \"{count} 人のメンバー\",\n      \"many\": \"{count} 人のメンバー\",\n      \"other\": \"{count} 人のメンバー\"\n    },\n    \"tip\": \"以下の連絡先情報を使用して、このワークスペースに参加するよう招待されました。これが間違っている場合は、管理者に連絡して招待を再送信してください。\",\n    \"joinWorkspace\": \"ワークスペースに参加\",\n    \"success\": \"ワークスペースへの参加が完了しました\",\n    \"successMessage\": \"これで、その中のすべてのページとワークスペースにアクセスできるようになります。\",\n    \"openWorkspace\": \"AppFlowyを開く\",\n    \"alreadyAccepted\": \"すでに招待を承諾しています\",\n    \"errorModal\": {\n      \"title\": \"問題が発生しました\",\n      \"description\": \"現在のアカウント {email} ではこのワークスペースにアクセスできない可能性があります。正しいアカウントでログインするか、ワークスペースの所有者に問い合わせてください。\",\n      \"contactOwner\": \"所有者に連絡する\",\n      \"close\": \"家に戻る\",\n      \"changeAccount\": \"アカウントを変更する\"\n    }\n  },\n  \"requestAccess\": {\n    \"title\": \"このページにアクセスできません\",\n    \"subtitle\": \"このページの所有者にアクセスをリクエストできます。承認されると、ページを表示できるようになります。\",\n    \"requestAccess\": \"アクセスをリクエスト\",\n    \"backToHome\": \"homeに戻る\",\n    \"tip\": \"現在ログインしているのは<link/>。\",\n    \"mightBe\": \"別のアカウントでのログイン<login/>が必要になるかもしれません。\",\n    \"successful\": \"リクエストは正常に送信されました\",\n    \"successfulMessage\": \"所有者がリクエストを承認すると通知されます。\",\n    \"requestError\": \"アクセスをリクエストできませんでした\",\n    \"repeatRequestError\": \"このページへのアクセスはすでにリクエストされています\"\n  },\n  \"approveAccess\": {\n    \"title\": \"ワークスペース参加リクエストを承認\",\n    \"requestSummary\": \"<user/>参加リクエスト<workspace/>アクセス<page/>\",\n    \"upgrade\": \"アップグレード\",\n    \"downloadApp\": \"AppFlowyをダウンロード\",\n    \"approveButton\": \"承認する\",\n    \"approveSuccess\": \"承認されました\",\n    \"approveError\": \"承認に失敗しました。ワークスペース プランの制限を超えていないことを確認してください。\",\n    \"getRequestInfoError\": \"リクエスト情報を取得できませんでした\",\n    \"memberCount\": {\n      \"zero\": \"メンバーなし\",\n      \"one\": \"メンバー 1 人\",\n      \"many\": \"{count} 人のメンバー\",\n      \"other\": \"{count} 人のメンバー\"\n    },\n    \"alreadyProTitle\": \"ワークスペースプランの制限に達しました\",\n    \"alreadyProMessage\": \"連絡を取るよう依頼する<email/>より多くのメンバーのロックを解除する\",\n    \"repeatApproveError\": \"このリクエストはすでに承認されています\",\n    \"ensurePlanLimit\": \"ワークスペースプランの制限を超えていないことを確認してください。制限を超えた場合は、<upgrade/>ワークスペースプランまたは<download/>。\",\n    \"requestToJoin\": \"参加をリクエストされた\",\n    \"asMember\": \"メンバーとして\"\n  },\n  \"upgradePlanModal\": {\n    \"title\": \"プロにアップグレード\",\n    \"message\": \"{name} は無料メンバーの上限に達しました。より多くのメンバーを招待するには、Pro プランにアップグレードしてください。\",\n    \"upgradeSteps\": \"AppFlowyでプランをアップグレードする方法:\",\n    \"step1\": \"1.設定へ移動\",\n    \"step2\": \"2. 「プラン」をクリック\",\n    \"step3\": \"3. 「プランの変更」を選択します\",\n    \"appNote\": \"注記： \",\n    \"actionButton\": \"アップグレード\",\n    \"downloadLink\": \"アプリをダウンロード\",\n    \"laterButton\": \"後で\",\n    \"refreshNote\": \"アップグレードが成功したら、<refresh/>新しい機能を有効にします。\",\n    \"refresh\": \"ここ\"\n  },\n  \"breadcrumbs\": {\n    \"label\": \"パンくず\"\n  },\n  \"time\": {\n    \"justNow\": \"たった今\",\n    \"seconds\": {\n      \"one\": \"1秒\",\n      \"other\": \"{count} 秒\"\n    },\n    \"minutes\": {\n      \"one\": \"1分\",\n      \"other\": \"{count} 分\"\n    },\n    \"hours\": {\n      \"one\": \"1時間\",\n      \"other\": \"{count} 時間\"\n    },\n    \"days\": {\n      \"one\": \"1日\",\n      \"other\": \"{count} 日\"\n    },\n    \"weeks\": {\n      \"one\": \"1週間\",\n      \"other\": \"{count} 週間\"\n    },\n    \"months\": {\n      \"one\": \"1ヶ月\",\n      \"other\": \"{count} ヶ月\"\n    },\n    \"years\": {\n      \"one\": \"1年\",\n      \"other\": \"{count} 年\"\n    },\n    \"ago\": \"以前\",\n    \"yesterday\": \"昨日\",\n    \"today\": \"今日\"\n  },\n  \"members\": {\n    \"zero\": \"メンバーなし\",\n    \"one\": \"メンバー 1 人\",\n    \"many\": \"{count} 人のメンバー\",\n    \"other\": \"{count} 人のメンバー\"\n  },\n  \"tabMenu\": {\n    \"close\": \"閉じる\",\n    \"closeDisabledHint\": \"固定されたタブを閉じることはできません。まず固定を解除してください\",\n    \"closeOthers\": \"他のタブを閉じる\",\n    \"closeOthersHint\": \"これにより、このタブを除くすべての固定されていないタブが閉じられます\",\n    \"closeOthersDisabledHint\": \"すべてのタブが固定されており、閉じるタブが見つかりません\",\n    \"favorite\": \"お気に入り\",\n    \"unfavorite\": \"お気に入りを解除\",\n    \"favoriteDisabledHint\": \"このビューをお気に入りに登録できません\",\n    \"pinTab\": \"ピン\",\n    \"unpinTab\": \"ピンを外す\"\n  },\n  \"openFileMessage\": {\n    \"success\": \"ファイルは正常に開かれました\",\n    \"fileNotFound\": \"ファイルが見つかりません\",\n    \"noAppToOpenFile\": \"このファイルを開くアプリはありません\",\n    \"permissionDenied\": \"このファイルを開く権限がありません\",\n    \"unknownError\": \"ファイルのオープンに失敗しました\"\n  },\n  \"inviteMember\": {\n    \"requestInviteMembers\": \"ワークスペースに招待する\",\n    \"inviteFailedMemberLimit\": \"メンバーの上限に達しました。 \",\n    \"upgrade\": \"アップグレード\",\n    \"addEmail\": \"email@example.com, email2@example.com...\",\n    \"requestInvites\": \"招待状を送る\",\n    \"inviteAlready\": \"このメールアドレスはすでに招待されています: {email}\",\n    \"inviteSuccess\": \"招待状は正常に送信されました\",\n    \"description\": \"以下にカンマで区切ってメールアドレスを入力してください。料金はメンバー数に基づいて決まります。\",\n    \"emails\": \"メール\"\n  },\n  \"quickNote\": {\n    \"label\": \"クイックノート\",\n    \"quickNotes\": \"クイックノート\",\n    \"search\": \"クイックノートを検索\",\n    \"collapseFullView\": \"全画面表示を折りたたむ\",\n    \"expandFullView\": \"全画面表示を拡大\",\n    \"createFailed\": \"クイックノートの作成に失敗しました\",\n    \"quickNotesEmpty\": \"クイックノートなし\",\n    \"emptyNote\": \"空のメモ\",\n    \"deleteNotePrompt\": \"選択したメモは完全に削除されます。削除してもよろしいですか？\",\n    \"addNote\": \"新しいメモ\",\n    \"noAdditionalText\": \"追加テキストなし\"\n  },\n  \"subscribe\": {\n    \"upgradePlanTitle\": \"プランを比較して選択\",\n    \"yearly\": \"年間\",\n    \"save\": \"{discount}% 節約\",\n    \"monthly\": \"月次\",\n    \"priceIn\": \"価格\",\n    \"free\": \"無料\",\n    \"pro\": \"プロ\",\n    \"freeDescription\": \"個人で最大2人までがすべてを管理\",\n    \"proDescription\": \"小規模なチームがプロジェクトとチームの知識を管理する\",\n    \"proDuration\": {\n      \"monthly\": \"会員あたり月額\\n毎月請求\",\n      \"yearly\": \"メンバーあたり月額\\n毎年請求\"\n    },\n    \"cancel\": \"ダウングレード\",\n    \"changePlan\": \"プロプランにアップグレード\",\n    \"everythingInFree\": \"すべて無料+\",\n    \"currentPlan\": \"現在\",\n    \"freeDuration\": \"永遠\",\n    \"freePoints\": {\n      \"first\": \"最大 2 人のメンバーが参加できる 1 つの共同ワークスペース\",\n      \"second\": \"ページとブロック無制限\",\n      \"three\": \"5 GBのストレージ\",\n      \"four\": \"インテリジェント検索\",\n      \"five\": \"20件のAI回答\",\n      \"six\": \"モバイルアプリ\",\n      \"seven\": \"リアルタイムコラボレーション\"\n    },\n    \"proPoints\": {\n      \"first\": \"無制限のストレージ\",\n      \"second\": \"ワークスペースメンバー最大10人\",\n      \"three\": \"無制限のAI応答\",\n      \"four\": \"無制限のファイルアップロード\",\n      \"five\": \"カスタム名前空間\"\n    },\n    \"cancelPlan\": {\n      \"title\": \"去ってしまうのは残念です\",\n      \"success\": \"サブスクリプションは正常にキャンセルされました\",\n      \"description\": \"ご利用いただけなくなるのは残念です。AppFlowy の改善に役立てていただけるよう、皆様のフィードバックをお待ちしています。お時間をいただき、いくつかの質問にお答えください。\",\n      \"commonOther\": \"他\",\n      \"otherHint\": \"ここに答えを書いてください\",\n      \"questionOne\": {\n        \"question\": \"AppFlowy Pro サブスクリプションをキャンセルした理由は何ですか？\",\n        \"answerOne\": \"コストが高すぎる\",\n        \"answerTwo\": \"機能が期待に応えられなかった\",\n        \"answerThree\": \"より良い代替案を見つけた\",\n        \"answerFour\": \"費用に見合うほど使用しなかった\",\n        \"answerFive\": \"サービスの問題または技術的な問題\"\n      },\n      \"questionTwo\": {\n        \"question\": \"将来的にAppFlowy Proへの再加入を検討する可能性はどのくらいありますか？\",\n        \"answerOne\": \"非常に可能性が高い\",\n        \"answerTwo\": \"可能性が高い\",\n        \"answerThree\": \"わからない\",\n        \"answerFour\": \"可能性が低い\",\n        \"answerFive\": \"非常に可能性が低い\"\n      },\n      \"questionThree\": {\n        \"question\": \"サブスクリプション期間中に最も価値を感じた Pro 機能はどれですか？\",\n        \"answerOne\": \"複数ユーザーコラボレーション\",\n        \"answerTwo\": \"長期間のバージョン履歴\",\n        \"answerThree\": \"無制限のAI応答\",\n        \"answerFour\": \"ローカルAIモデルへのアクセス\"\n      },\n      \"questionFour\": {\n        \"question\": \"AppFlowy の全体的な使用感と満足感をどう思いますか？\",\n        \"answerOne\": \"秀\",\n        \"answerTwo\": \"優\",\n        \"answerThree\": \"良\",\n        \"answerFour\": \"可\",\n        \"answerFive\": \"不可\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/ko-KR.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"나\",\n  \"welcomeText\": \"@:appName에 오신 것을 환영합니다\",\n  \"welcomeTo\": \"환영합니다\",\n  \"githubStarText\": \"GitHub에서 별표\",\n  \"subscribeNewsletterText\": \"뉴스레터 구독\",\n  \"letsGoButtonText\": \"빠른 시작\",\n  \"title\": \"제목\",\n  \"youCanAlso\": \"또한 할 수 있습니다\",\n  \"and\": \"그리고\",\n  \"failedToOpenUrl\": \"URL을 열지 못했습니다: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"아래에 추가하려면 클릭\",\n    \"addAboveCmd\": \"Alt+클릭\",\n    \"addAboveMacCmd\": \"Option+클릭\",\n    \"addAboveTooltip\": \"위에 추가하려면\",\n    \"dragTooltip\": \"이동하려면 드래그\",\n    \"openMenuTooltip\": \"메뉴를 열려면 클릭\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"가입하기\",\n    \"title\": \"@:appName에 가입하기\",\n    \"getStartedText\": \"시작하기\",\n    \"emptyPasswordError\": \"비밀번호는 비워둘 수 없습니다\",\n    \"repeatPasswordEmptyError\": \"비밀번호 확인은 비워둘 수 없습니다\",\n    \"unmatchedPasswordError\": \"비밀번호 확인이 비밀번호와 일치하지 않습니다\",\n    \"alreadyHaveAnAccount\": \"이미 계정이 있으신가요?\",\n    \"emailHint\": \"이메일\",\n    \"passwordHint\": \"비밀번호\",\n    \"repeatPasswordHint\": \"비밀번호 확인\",\n    \"signUpWith\": \"다음으로 가입:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"@:appName에 로그인\",\n    \"loginButtonText\": \"로그인\",\n    \"loginStartWithAnonymous\": \"익명 세션으로 계속\",\n    \"continueAnonymousUser\": \"익명 세션으로 계속\",\n    \"buttonText\": \"로그인\",\n    \"signingInText\": \"로그인 중...\",\n    \"forgotPassword\": \"비밀번호를 잊으셨나요?\",\n    \"emailHint\": \"이메일\",\n    \"passwordHint\": \"비밀번호\",\n    \"dontHaveAnAccount\": \"계정이 없으신가요?\",\n    \"createAccount\": \"계정 만들기\",\n    \"repeatPasswordEmptyError\": \"비밀번호 확인은 비워둘 수 없습니다\",\n    \"unmatchedPasswordError\": \"비밀번호 확인이 비밀번호와 일치하지 않습니다\",\n    \"syncPromptMessage\": \"데이터 동기화에는 시간이 걸릴 수 있습니다. 이 페이지를 닫지 마세요\",\n    \"or\": \"또는\",\n    \"signInWithGoogle\": \"Google로 계속\",\n    \"signInWithGithub\": \"GitHub로 계속\",\n    \"signInWithDiscord\": \"Discord로 계속\",\n    \"signInWithApple\": \"Apple로 계속\",\n    \"continueAnotherWay\": \"다른 방법으로 계속\",\n    \"signUpWithGoogle\": \"Google로 가입\",\n    \"signUpWithGithub\": \"GitHub로 가입\",\n    \"signUpWithDiscord\": \"Discord로 가입\",\n    \"signInWith\": \"다음으로 계속:\",\n    \"signInWithEmail\": \"이메일로 계속\",\n    \"signInWithMagicLink\": \"계속\",\n    \"signUpWithMagicLink\": \"Magic Link로 가입\",\n    \"pleaseInputYourEmail\": \"이메일 주소를 입력하세요\",\n    \"settings\": \"설정\",\n    \"magicLinkSent\": \"Magic Link가 전송되었습니다!\",\n    \"invalidEmail\": \"유효한 이메일 주소를 입력하세요\",\n    \"alreadyHaveAnAccount\": \"이미 계정이 있으신가요?\",\n    \"logIn\": \"로그인\",\n    \"generalError\": \"문제가 발생했습니다. 나중에 다시 시도하세요\",\n    \"limitRateError\": \"보안상의 이유로, 매 60초마다 한 번씩만 Magic Link를 요청할 수 있습니다\",\n    \"magicLinkSentDescription\": \"Magic Link가 이메일로 전송되었습니다. 링크를 클릭하여 로그인을 완료하세요. 링크는 5분 후에 만료됩니다.\",\n    \"anonymous\": \"익명\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"작업 공간 선택\",\n    \"defaultName\": \"내 작업 공간\",\n    \"create\": \"작업 공간 생성\",\n    \"new\": \"새 작업 공간\",\n    \"importFromNotion\": \"Notion에서 가져오기\",\n    \"learnMore\": \"자세히 알아보기\",\n    \"reset\": \"작업 공간 재설정\",\n    \"renameWorkspace\": \"작업 공간 이름 변경\",\n    \"workspaceNameCannotBeEmpty\": \"작업 공간 이름은 비워둘 수 없습니다\",\n    \"resetWorkspacePrompt\": \"작업 공간을 재설정하면 모든 페이지와 데이터가 삭제됩니다. 작업 공간을 재설정하시겠습니까? 또는 지원 팀에 문의하여 작업 공간을 복원할 수 있습니다\",\n    \"hint\": \"작업 공간\",\n    \"notFoundError\": \"작업 공간을 찾을 수 없습니다\",\n    \"failedToLoad\": \"문제가 발생했습니다! 작업 공간을 로드하지 못했습니다. @:appName의 모든 열린 인스턴스를 닫고 다시 시도하세요.\",\n    \"errorActions\": {\n      \"reportIssue\": \"문제 보고\",\n      \"reportIssueOnGithub\": \"GitHub에서 문제 보고\",\n      \"exportLogFiles\": \"로그 파일 내보내기\",\n      \"reachOut\": \"Discord에서 문의\"\n    },\n    \"menuTitle\": \"작업 공간\",\n    \"deleteWorkspaceHintText\": \"작업 공간을 삭제하시겠습니까? 이 작업은 되돌릴 수 없으며, 게시한 모든 페이지가 게시 취소됩니다.\",\n    \"createSuccess\": \"작업 공간이 성공적으로 생성되었습니다\",\n    \"createFailed\": \"작업 공간 생성 실패\",\n    \"createLimitExceeded\": \"계정에 허용된 최대 작업 공간 수에 도달했습니다. 추가 작업 공간이 필요하면 GitHub에 요청하세요\",\n    \"deleteSuccess\": \"작업 공간이 성공적으로 삭제되었습니다\",\n    \"deleteFailed\": \"작업 공간 삭제 실패\",\n    \"openSuccess\": \"작업 공간이 성공적으로 열렸습니다\",\n    \"openFailed\": \"작업 공간 열기 실패\",\n    \"renameSuccess\": \"작업 공간 이름이 성공적으로 변경되었습니다\",\n    \"renameFailed\": \"작업 공간 이름 변경 실패\",\n    \"updateIconSuccess\": \"작업 공간 아이콘이 성공적으로 업데이트되었습니다\",\n    \"updateIconFailed\": \"작업 공간 아이콘 업데이트 실패\",\n    \"cannotDeleteTheOnlyWorkspace\": \"유일한 작업 공간을 삭제할 수 없습니다\",\n    \"fetchWorkspacesFailed\": \"작업 공간을 가져오지 못했습니다\",\n    \"leaveCurrentWorkspace\": \"작업 공간 나가기\",\n    \"leaveCurrentWorkspacePrompt\": \"현재 작업 공간을 나가시겠습니까?\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"공유\",\n    \"workInProgress\": \"곧 출시 예정\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"클립보드에 복사\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"링크 복사\",\n    \"publishToTheWeb\": \"웹에 게시\",\n    \"publishToTheWebHint\": \"AppFlowy로 웹사이트 만들기\",\n    \"publish\": \"게시\",\n    \"unPublish\": \"게시 취소\",\n    \"visitSite\": \"사이트 방문\",\n    \"exportAsTab\": \"다음으로 내보내기\",\n    \"publishTab\": \"게시\",\n    \"shareTab\": \"공유\",\n    \"publishOnAppFlowy\": \"AppFlowy에 게시\",\n    \"shareTabTitle\": \"협업 초대\",\n    \"shareTabDescription\": \"누구와도 쉽게 협업할 수 있습니다\",\n    \"copyLinkSuccess\": \"링크가 클립보드에 복사되었습니다\",\n    \"copyShareLink\": \"공유 링크 복사\",\n    \"copyLinkFailed\": \"링크를 클립보드에 복사하지 못했습니다\",\n    \"copyLinkToBlockSuccess\": \"블록 링크가 클립보드에 복사되었습니다\",\n    \"copyLinkToBlockFailed\": \"블록 링크를 클립보드에 복사하지 못했습니다\",\n    \"manageAllSites\": \"모든 사이트 관리\",\n    \"updatePathName\": \"경로 이름 업데이트\"\n  },\n  \"moreAction\": {\n    \"small\": \"작게\",\n    \"medium\": \"중간\",\n    \"large\": \"크게\",\n    \"fontSize\": \"글꼴 크기\",\n    \"import\": \"가져오기\",\n    \"moreOptions\": \"더 많은 옵션\",\n    \"wordCount\": \"단어 수: {}\",\n    \"charCount\": \"문자 수: {}\",\n    \"createdAt\": \"생성일: {}\",\n    \"deleteView\": \"삭제\",\n    \"duplicateView\": \"복제\",\n    \"wordCountLabel\": \"단어 수: \",\n    \"charCountLabel\": \"문자 수: \",\n    \"createdAtLabel\": \"생성일: \",\n    \"syncedAtLabel\": \"동기화됨: \",\n    \"saveAsNewPage\": \"페이지에 메시지 추가\",\n    \"saveAsNewPageDisabled\": \"사용 가능한 메시지가 없습니다\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"텍스트 & Markdown\",\n    \"documentFromV010\": \"v0.1.0에서 문서 가져오기\",\n    \"databaseFromV010\": \"v0.1.0에서 데이터베이스 가져오기\",\n    \"notionZip\": \"Notion 내보낸 Zip 파일\",\n    \"csv\": \"CSV\",\n    \"database\": \"데이터베이스\"\n  },\n  \"emojiIconPicker\": {\n    \"iconUploader\": {\n      \"placeholderLeft\": \"파일을 드래그 앤 드롭하거나 클릭하여 \",\n      \"placeholderUpload\": \"업로드\",\n      \"placeholderRight\": \"하거나 이미지 링크를 붙여넣으세요.\",\n      \"dropToUpload\": \"업로드할 파일을 드롭하세요\",\n      \"change\": \"변경\"\n    }\n  },\n  \"disclosureAction\": {\n    \"rename\": \"이름 변경\",\n    \"delete\": \"삭제\",\n    \"duplicate\": \"복제\",\n    \"unfavorite\": \"즐겨찾기에서 제거\",\n    \"favorite\": \"즐겨찾기에 추가\",\n    \"openNewTab\": \"새 탭에서 열기\",\n    \"moveTo\": \"이동\",\n    \"addToFavorites\": \"즐겨찾기에 추가\",\n    \"copyLink\": \"링크 복사\",\n    \"changeIcon\": \"아이콘 변경\",\n    \"collapseAllPages\": \"모든 하위 페이지 접기\",\n    \"movePageTo\": \"페이지 이동\",\n    \"move\": \"이동\",\n    \"lockPage\": \"페이지 잠금\"\n  },\n  \"blankPageTitle\": \"빈 페이지\",\n  \"newPageText\": \"새 페이지\",\n  \"newDocumentText\": \"새 문서\",\n  \"newGridText\": \"새 그리드\",\n  \"newCalendarText\": \"새 캘린더\",\n  \"newBoardText\": \"새 보드\",\n  \"chat\": {\n    \"newChat\": \"AI 채팅\",\n    \"inputMessageHint\": \"@:appName AI에게 물어보세요\",\n    \"inputLocalAIMessageHint\": \"@:appName 로컬 AI에게 물어보세요\",\n    \"unsupportedCloudPrompt\": \"이 기능은 @:appName Cloud를 사용할 때만 사용할 수 있습니다\",\n    \"relatedQuestion\": \"추천 질문\",\n    \"serverUnavailable\": \"연결이 끊어졌습니다. 인터넷을 확인하고\",\n    \"aiServerUnavailable\": \"AI 서비스가 일시적으로 사용할 수 없습니다. 나중에 다시 시도하세요.\",\n    \"retry\": \"다시 시도\",\n    \"clickToRetry\": \"다시 시도하려면 클릭\",\n    \"regenerateAnswer\": \"다시 생성\",\n    \"question1\": \"Kanban을 사용하여 작업 관리하는 방법\",\n    \"question2\": \"GTD 방법 설명\",\n    \"question3\": \"Rust를 사용하는 이유\",\n    \"question4\": \"내 주방에 있는 재료로 요리법 만들기\",\n    \"question5\": \"내 페이지에 대한 일러스트레이션 만들기\",\n    \"question6\": \"다가오는 주의 할 일 목록 작성\",\n    \"aiMistakePrompt\": \"AI는 실수를 할 수 있습니다. 중요한 정보를 확인하세요.\",\n    \"chatWithFilePrompt\": \"파일과 채팅하시겠습니까?\",\n    \"indexFileSuccess\": \"파일 색인화 성공\",\n    \"inputActionNoPages\": \"페이지 결과 없음\",\n    \"referenceSource\": {\n      \"zero\": \"0개의 출처 발견\",\n      \"one\": \"{count}개의 출처 발견\",\n      \"other\": \"{count}개의 출처 발견\"\n    },\n    \"clickToMention\": \"페이지 언급\",\n    \"uploadFile\": \"PDF, 텍스트 또는 마크다운 파일 첨부\",\n    \"questionDetail\": \"안녕하세요 {}! 오늘 어떻게 도와드릴까요?\",\n    \"indexingFile\": \"{} 색인화 중\",\n    \"generatingResponse\": \"응답 생성 중\",\n    \"selectSources\": \"출처 선택\",\n    \"currentPage\": \"현재 페이지\",\n    \"sourcesLimitReached\": \"최대 3개의 최상위 문서와 그 하위 문서만 선택할 수 있습니다\",\n    \"sourceUnsupported\": \"현재 데이터베이스와의 채팅을 지원하지 않습니다\",\n    \"regenerate\": \"다시 시도\",\n    \"addToPageButton\": \"페이지에 메시지 추가\",\n    \"addToPageTitle\": \"메시지 추가...\",\n    \"addToNewPage\": \"새 페이지 만들기\",\n    \"addToNewPageName\": \"\\\"{}\\\"에서 추출한 메시지\",\n    \"addToNewPageSuccessToast\": \"메시지가 추가되었습니다\",\n    \"openPagePreviewFailedToast\": \"페이지를 열지 못했습니다\",\n    \"changeFormat\": {\n      \"actionButton\": \"형식 변경\",\n      \"confirmButton\": \"이 형식으로 다시 생성\",\n      \"textOnly\": \"텍스트\",\n      \"imageOnly\": \"이미지 전용\",\n      \"textAndImage\": \"텍스트와 이미지\",\n      \"text\": \"단락\",\n      \"bullet\": \"글머리 기호 목록\",\n      \"number\": \"번호 매기기 목록\",\n      \"table\": \"표\",\n      \"blankDescription\": \"응답 형식\",\n      \"defaultDescription\": \"자동 모드\",\n      \"textWithImageDescription\": \"@:chat.changeFormat.text와 이미지\",\n      \"numberWithImageDescription\": \"@:chat.changeFormat.number와 이미지\",\n      \"bulletWithImageDescription\": \"@:chat.changeFormat.bullet와 이미지\",\n      \"tableWithImageDescription\": \"@:chat.changeFormat.table와 이미지\"\n    },\n    \"selectBanner\": {\n      \"saveButton\": \"추가 ...\",\n      \"selectMessages\": \"메시지 선택\",\n      \"nSelected\": \"{}개 선택됨\",\n      \"allSelected\": \"모두 선택됨\"\n    },\n    \"stopTooltip\": \"생성 중지\"\n  },\n  \"trash\": {\n    \"text\": \"휴지통\",\n    \"restoreAll\": \"모두 복원\",\n    \"restore\": \"복원\",\n    \"deleteAll\": \"모두 삭제\",\n    \"pageHeader\": {\n      \"fileName\": \"파일 이름\",\n      \"lastModified\": \"마지막 수정\",\n      \"created\": \"생성됨\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"휴지통의 모든 페이지\",\n      \"caption\": \"휴지통의 모든 항목을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"휴지통의 모든 페이지 복원\",\n      \"caption\": \"이 작업은 되돌릴 수 없습니다.\"\n    },\n    \"restorePage\": {\n      \"title\": \"복원: {}\",\n      \"caption\": \"이 페이지를 복원하시겠습니까?\"\n    },\n    \"mobile\": {\n      \"actions\": \"휴지통 작업\",\n      \"empty\": \"휴지통에 페이지나 공간이 없습니다\",\n      \"emptyDescription\": \"필요 없는 항목을 휴지통으로 이동하세요.\",\n      \"isDeleted\": \"삭제됨\",\n      \"isRestored\": \"복원됨\"\n    },\n    \"confirmDeleteTitle\": \"이 페이지를 영구적으로 삭제하시겠습니까?\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"이 페이지는 휴지통에 있습니다\",\n    \"restore\": \"페이지 복원\",\n    \"deletePermanent\": \"영구적으로 삭제\",\n    \"deletePermanentDescription\": \"이 페이지를 영구적으로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.\"\n  },\n  \"dialogCreatePageNameHint\": \"페이지 이름\",\n  \"questionBubble\": {\n    \"shortcuts\": \"단축키\",\n    \"whatsNew\": \"새로운 기능\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"디버그 정보\",\n      \"success\": \"디버그 정보를 클립보드에 복사했습니다!\",\n      \"fail\": \"디버그 정보를 클립보드에 복사할 수 없습니다\"\n    },\n    \"feedback\": \"피드백\",\n    \"help\": \"도움말 및 지원\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"제거, 이름 변경 등...\",\n    \"addPageTooltip\": \"빠르게 페이지 추가\",\n    \"defaultNewPageName\": \"제목 없음\",\n    \"renameDialog\": \"이름 변경\",\n    \"pageNameSuffix\": \"복사본\"\n  },\n  \"noPagesInside\": \"내부에 페이지가 없습니다\",\n  \"toolbar\": {\n    \"undo\": \"실행 취소\",\n    \"redo\": \"다시 실행\",\n    \"bold\": \"굵게\",\n    \"italic\": \"기울임꼴\",\n    \"underline\": \"밑줄\",\n    \"strike\": \"취소선\",\n    \"numList\": \"번호 매기기 목록\",\n    \"bulletList\": \"글머리 기호 목록\",\n    \"checkList\": \"체크리스트\",\n    \"inlineCode\": \"인라인 코드\",\n    \"quote\": \"인용 블록\",\n    \"header\": \"헤더\",\n    \"highlight\": \"강조\",\n    \"color\": \"색상\",\n    \"addLink\": \"링크 추가\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"라이트 모드로 전환\",\n    \"darkMode\": \"다크 모드로 전환\",\n    \"openAsPage\": \"페이지로 열기\",\n    \"addNewRow\": \"새 행 추가\",\n    \"openMenu\": \"메뉴 열기\",\n    \"dragRow\": \"행 순서 변경\",\n    \"viewDataBase\": \"데이터베이스 보기\",\n    \"referencePage\": \"이 {name}이 참조됨\",\n    \"addBlockBelow\": \"아래에 블록 추가\",\n    \"aiGenerate\": \"생성\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"사이드바 닫기\",\n    \"openSidebar\": \"사이드바 열기\",\n    \"expandSidebar\": \"전체 페이지로 확장\",\n    \"personal\": \"개인\",\n    \"private\": \"비공개\",\n    \"workspace\": \"작업 공간\",\n    \"favorites\": \"즐겨찾기\",\n    \"clickToHidePrivate\": \"비공개 공간 숨기기\\n여기에서 만든 페이지는 본인만 볼 수 있습니다\",\n    \"clickToHideWorkspace\": \"작업 공간 숨기기\\n여기에서 만든 페이지는 모든 멤버가 볼 수 있습니다\",\n    \"clickToHidePersonal\": \"개인 공간 숨기기\",\n    \"clickToHideFavorites\": \"즐겨찾기 공간 숨기기\",\n    \"addAPage\": \"새 페이지 추가\",\n    \"addAPageToPrivate\": \"비공개 공간에 페이지 추가\",\n    \"addAPageToWorkspace\": \"작업 공간에 페이지 추가\",\n    \"recent\": \"최근\",\n    \"today\": \"오늘\",\n    \"thisWeek\": \"이번 주\",\n    \"others\": \"이전 즐겨찾기\",\n    \"earlier\": \"이전\",\n    \"justNow\": \"방금\",\n    \"minutesAgo\": \"{count}분 전\",\n    \"lastViewed\": \"마지막으로 본\",\n    \"favoriteAt\": \"즐겨찾기한\",\n    \"emptyRecent\": \"최근 페이지 없음\",\n    \"emptyRecentDescription\": \"페이지를 보면 여기에서 쉽게 찾을 수 있습니다.\",\n    \"emptyFavorite\": \"즐겨찾기 페이지 없음\",\n    \"emptyFavoriteDescription\": \"페이지를 즐겨찾기에 추가하면 여기에서 빠르게 접근할 수 있습니다!\",\n    \"removePageFromRecent\": \"최근 항목에서 이 페이지를 제거하시겠습니까?\",\n    \"removeSuccess\": \"성공적으로 제거되었습니다\",\n    \"favoriteSpace\": \"즐겨찾기\",\n    \"RecentSpace\": \"최근\",\n    \"Spaces\": \"공간\",\n    \"upgradeToPro\": \"Pro로 업그레이드\",\n    \"upgradeToAIMax\": \"무제한 AI 잠금 해제\",\n    \"storageLimitDialogTitle\": \"무료 저장 공간이 부족합니다. 무제한 저장 공간을 잠금 해제하려면 업그레이드하세요\",\n    \"storageLimitDialogTitleIOS\": \"무료 저장 공간이 부족합니다.\",\n    \"aiResponseLimitTitle\": \"무료 AI 응답이 부족합니다. Pro 플랜으로 업그레이드하거나 AI 애드온을 구매하여 무제한 응답을 잠금 해제하세요\",\n    \"aiResponseLimitDialogTitle\": \"AI 응답 한도에 도달했습니다\",\n    \"aiResponseLimit\": \"무료 AI 응답이 부족합니다.\\n\\n설정 -> 플랜 -> AI Max 또는 Pro 플랜을 클릭하여 더 많은 AI 응답을 받으세요\",\n    \"askOwnerToUpgradeToPro\": \"작업 공간의 무료 저장 공간이 부족합니다. 작업 공간 소유자에게 Pro 플랜으로 업그레이드하도록 요청하세요\",\n    \"askOwnerToUpgradeToProIOS\": \"작업 공간의 무료 저장 공간이 부족합니다.\",\n    \"askOwnerToUpgradeToAIMax\": \"작업 공간의 무료 AI 응답이 부족합니다. 작업 공간 소유자에게 플랜을 업그레이드하거나 AI 애드온을 구매하도록 요청하세요\",\n    \"askOwnerToUpgradeToAIMaxIOS\": \"작업 공간의 무료 AI 응답이 부족합니다.\",\n    \"purchaseAIMax\": \"작업 공간의 AI 이미지 응답이 부족합니다. 작업 공간 소유자에게 AI Max를 구매하도록 요청하세요\",\n    \"aiImageResponseLimit\": \"AI 이미지 응답이 부족합니다.\\n\\n설정 -> 플랜 -> AI Max를 클릭하여 더 많은 AI 이미지 응답을 받으세요\",\n    \"purchaseStorageSpace\": \"저장 공간 구매\",\n    \"singleFileProPlanLimitationDescription\": \"무료 플랜에서 허용되는 최대 파일 업로드 크기를 초과했습니다. 더 큰 파일을 업로드하려면 Pro 플랜으로 업그레이드하세요\",\n    \"purchaseAIResponse\": \"구매 \",\n    \"askOwnerToUpgradeToLocalAI\": \"작업 공간 소유자에게 AI On-device를 활성화하도록 요청하세요\",\n    \"upgradeToAILocal\": \"최고의 프라이버시를 위해 로컬 모델을 장치에서 실행\",\n    \"upgradeToAILocalDesc\": \"PDF와 채팅하고, 글쓰기를 개선하고, 로컬 AI를 사용하여 테이블을 자동으로 채우세요\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"노트를 Markdown으로 내보냈습니다\",\n      \"path\": \"Documents/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"연락처\",\n    \"whatsHappening\": \"이번 주에 무슨 일이 있나요?\",\n    \"addContact\": \"연락처 추가\",\n    \"editContact\": \"연락처 수정\"\n  },\n  \"button\": {\n    \"ok\": \"확인\",\n    \"confirm\": \"확인\",\n    \"done\": \"완료\",\n    \"cancel\": \"취소\",\n    \"signIn\": \"로그인\",\n    \"signOut\": \"로그아웃\",\n    \"complete\": \"완료\",\n    \"save\": \"저장\",\n    \"generate\": \"생성\",\n    \"esc\": \"ESC\",\n    \"keep\": \"유지\",\n    \"tryAgain\": \"다시 시도\",\n    \"discard\": \"버리기\",\n    \"replace\": \"교체\",\n    \"insertBelow\": \"아래에 삽입\",\n    \"insertAbove\": \"위에 삽입\",\n    \"upload\": \"업로드\",\n    \"edit\": \"편집\",\n    \"delete\": \"삭제\",\n    \"copy\": \"복사\",\n    \"duplicate\": \"복제\",\n    \"putback\": \"되돌리기\",\n    \"update\": \"업데이트\",\n    \"share\": \"공유\",\n    \"removeFromFavorites\": \"즐겨찾기에서 제거\",\n    \"removeFromRecent\": \"최근 항목에서 제거\",\n    \"addToFavorites\": \"즐겨찾기에 추가\",\n    \"favoriteSuccessfully\": \"즐겨찾기에 성공적으로 추가되었습니다\",\n    \"unfavoriteSuccessfully\": \"즐겨찾기에서 성공적으로 제거되었습니다\",\n    \"duplicateSuccessfully\": \"성공적으로 복제되었습니다\",\n    \"rename\": \"이름 변경\",\n    \"helpCenter\": \"도움말 센터\",\n    \"add\": \"추가\",\n    \"yes\": \"예\",\n    \"no\": \"아니요\",\n    \"clear\": \"지우기\",\n    \"remove\": \"제거\",\n    \"dontRemove\": \"제거하지 않음\",\n    \"copyLink\": \"링크 복사\",\n    \"align\": \"정렬\",\n    \"login\": \"로그인\",\n    \"logout\": \"로그아웃\",\n    \"deleteAccount\": \"계정 삭제\",\n    \"back\": \"뒤로\",\n    \"signInGoogle\": \"Google로 계속\",\n    \"signInGithub\": \"GitHub로 계속\",\n    \"signInDiscord\": \"Discord로 계속\",\n    \"more\": \"더 보기\",\n    \"create\": \"생성\",\n    \"close\": \"닫기\",\n    \"next\": \"다음\",\n    \"previous\": \"이전\",\n    \"submit\": \"제출\",\n    \"download\": \"다운로드\",\n    \"backToHome\": \"홈으로 돌아가기\",\n    \"viewing\": \"보기\",\n    \"editing\": \"편집 중\",\n    \"gotIt\": \"알겠습니다\",\n    \"retry\": \"다시 시도\",\n    \"uploadFailed\": \"업로드 실패.\",\n    \"copyLinkOriginal\": \"원본 링크 복사\"\n  },\n  \"label\": {\n    \"welcome\": \"환영합니다!\",\n    \"firstName\": \"이름\",\n    \"middleName\": \"중간 이름\",\n    \"lastName\": \"성\",\n    \"stepX\": \"단계 {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"계정에 연결할 수 없습니다.\",\n      \"failedMsg\": \"브라우저에서 로그인 프로세스를 완료했는지 확인하세요.\"\n    },\n    \"google\": {\n      \"title\": \"GOOGLE 로그인\",\n      \"instruction1\": \"Google 연락처를 가져오려면 웹 브라우저를 사용하여 이 애플리케이션을 인증해야 합니다.\",\n      \"instruction2\": \"아이콘을 클릭하거나 텍스트를 선택하여 이 코드를 클립보드에 복사하세요:\",\n      \"instruction3\": \"웹 브라우저에서 다음 링크로 이동하고 위의 코드를 입력하세요:\",\n      \"instruction4\": \"가입을 완료했으면 아래 버튼을 누르세요:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"설정\",\n    \"popupMenuItem\": {\n      \"settings\": \"설정\",\n      \"members\": \"멤버\",\n      \"trash\": \"휴지통\",\n      \"helpAndSupport\": \"도움말 및 지원\"\n    },\n    \"sites\": {\n      \"title\": \"사이트\",\n      \"namespaceTitle\": \"네임스페이스\",\n      \"namespaceDescription\": \"네임스페이스 및 홈페이지 관리\",\n      \"namespaceHeader\": \"네임스페이스\",\n      \"homepageHeader\": \"홈페이지\",\n      \"updateNamespace\": \"네임스페이스 업데이트\",\n      \"removeHomepage\": \"홈페이지 제거\",\n      \"selectHomePage\": \"페이지 선택\",\n      \"clearHomePage\": \"이 네임스페이스의 홈페이지를 지웁니다\",\n      \"customUrl\": \"맞춤 URL\",\n      \"namespace\": {\n        \"description\": \"이 변경 사항은 이 네임스페이스에 라이브로 게시된 모든 페이지에 적용됩니다\",\n        \"tooltip\": \"부적절한 네임스페이스를 제거할 권리를 보유합니다\",\n        \"updateExistingNamespace\": \"기존 네임스페이스 업데이트\",\n        \"upgradeToPro\": \"Pro 플랜으로 업그레이드하여 홈페이지 설정\",\n        \"redirectToPayment\": \"결제 페이지로 리디렉션 중...\",\n        \"onlyWorkspaceOwnerCanSetHomePage\": \"작업 공간 소유자만 홈페이지를 설정할 수 있습니다\",\n        \"pleaseAskOwnerToSetHomePage\": \"작업 공간 소유자에게 Pro 플랜으로 업그레이드하도록 요청하세요\"\n      },\n      \"publishedPage\": {\n        \"title\": \"모든 게시된 페이지\",\n        \"description\": \"게시된 페이지 관리\",\n        \"page\": \"페이지\",\n        \"pathName\": \"경로 이름\",\n        \"date\": \"게시 날짜\",\n        \"emptyHinText\": \"이 작업 공간에 게시된 페이지가 없습니다\",\n        \"noPublishedPages\": \"게시된 페이지 없음\",\n        \"settings\": \"게시 설정\",\n        \"clickToOpenPageInApp\": \"앱에서 페이지 열기\",\n        \"clickToOpenPageInBrowser\": \"브라우저에서 페이지 열기\"\n      },\n      \"error\": {\n        \"failedToGeneratePaymentLink\": \"Pro 플랜 결제 링크 생성 실패\",\n        \"failedToUpdateNamespace\": \"네임스페이스 업데이트 실패\",\n        \"proPlanLimitation\": \"네임스페이스를 업데이트하려면 Pro 플랜으로 업그레이드해야 합니다\",\n        \"namespaceAlreadyInUse\": \"네임스페이스가 이미 사용 중입니다. 다른 네임스페이스를 시도하세요\",\n        \"invalidNamespace\": \"잘못된 네임스페이스입니다. 다른 네임스페이스를 시도하세요\",\n        \"namespaceLengthAtLeast2Characters\": \"네임스페이스는 최소 2자 이상이어야 합니다\",\n        \"onlyWorkspaceOwnerCanUpdateNamespace\": \"작업 공간 소유자만 네임스페이스를 업데이트할 수 있습니다\",\n        \"onlyWorkspaceOwnerCanRemoveHomepage\": \"작업 공간 소유자만 홈페이지를 제거할 수 있습니다\",\n        \"setHomepageFailed\": \"홈페이지 설정 실패\",\n        \"namespaceTooLong\": \"네임스페이스가 너무 깁니다. 다른 네임스페이스를 시도하세요\",\n        \"namespaceTooShort\": \"네임스페이스가 너무 짧습니다. 다른 네임스페이스를 시도하세요\",\n        \"namespaceIsReserved\": \"네임스페이스가 예약되어 있습니다. 다른 네임스페이스를 시도하세요\",\n        \"updatePathNameFailed\": \"경로 이름 업데이트 실패\",\n        \"removeHomePageFailed\": \"홈페이지 제거 실패\",\n        \"publishNameContainsInvalidCharacters\": \"경로 이름에 잘못된 문자가 포함되어 있습니다. 다른 이름을 시도하세요\",\n        \"publishNameTooShort\": \"경로 이름이 너무 짧습니다. 다른 이름을 시도하세요\",\n        \"publishNameTooLong\": \"경로 이름이 너무 깁니다. 다른 이름을 시도하세요\",\n        \"publishNameAlreadyInUse\": \"경로 이름이 이미 사용 중입니다. 다른 이름을 시도하세요\",\n        \"namespaceContainsInvalidCharacters\": \"네임스페이스에 잘못된 문자가 포함되어 있습니다. 다른 네임스페이스를 시도하세요\",\n        \"publishPermissionDenied\": \"작업 공간 소유자 또는 페이지 게시자만 게시 설정을 관리할 수 있습니다\",\n        \"publishNameCannotBeEmpty\": \"경로 이름은 비워둘 수 없습니다. 다른 이름을 시도하세요\"\n      },\n      \"success\": {\n        \"namespaceUpdated\": \"네임스페이스가 성공적으로 업데이트되었습니다\",\n        \"setHomepageSuccess\": \"홈페이지가 성공적으로 설정되었습니다\",\n        \"updatePathNameSuccess\": \"경로 이름이 성공적으로 업데이트되었습니다\",\n        \"removeHomePageSuccess\": \"홈페이지가 성공적으로 제거되었습니다\"\n      }\n    },\n    \"accountPage\": {\n      \"menuLabel\": \"계정 및 앱\",\n      \"title\": \"내 계정\",\n      \"general\": {\n        \"title\": \"계정 이름 및 프로필 이미지\",\n        \"changeProfilePicture\": \"프로필 사진 변경\"\n      },\n      \"email\": {\n        \"title\": \"이메일\",\n        \"actions\": {\n          \"change\": \"이메일 변경\"\n        }\n      },\n      \"login\": {\n        \"title\": \"계정 로그인\",\n        \"loginLabel\": \"로그인\",\n        \"logoutLabel\": \"로그아웃\"\n      },\n      \"isUpToDate\": \"@:appName이 최신 상태입니다!\",\n      \"officialVersion\": \"버전 {version} (공식 빌드)\"\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"작업 공간\",\n      \"title\": \"작업 공간\",\n      \"description\": \"작업 공간의 외관, 테마, 글꼴, 텍스트 레이아웃, 날짜/시간 형식 및 언어를 사용자 정의합니다.\",\n      \"workspaceName\": {\n        \"title\": \"작업 공간 이름\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"작업 공간 아이콘\",\n        \"description\": \"작업 공간에 대한 이미지를 업로드하거나 이모지를 사용하세요. 아이콘은 사이드바와 알림에 표시됩니다.\"\n      },\n      \"appearance\": {\n        \"title\": \"외관\",\n        \"description\": \"작업 공간의 외관, 테마, 글꼴, 텍스트 레이아웃, 날짜, 시간 및 언어를 사용자 정의합니다.\",\n        \"options\": {\n          \"system\": \"자동\",\n          \"light\": \"라이트\",\n          \"dark\": \"다크\"\n        }\n      },\n      \"resetCursorColor\": {\n        \"title\": \"문서 커서 색상 재설정\",\n        \"description\": \"커서 색상을 재설정하시겠습니까?\"\n      },\n      \"resetSelectionColor\": {\n        \"title\": \"문서 선택 색상 재설정\",\n        \"description\": \"선택 색상을 재설정하시겠습니까?\"\n      },\n      \"resetWidth\": {\n        \"resetSuccess\": \"문서 너비가 성공적으로 재설정되었습니다\"\n      },\n      \"theme\": {\n        \"title\": \"테마\",\n        \"description\": \"미리 설정된 테마를 선택하거나 사용자 정의 테마를 업로드하세요.\",\n        \"uploadCustomThemeTooltip\": \"사용자 정의 테마 업로드\"\n      },\n      \"workspaceFont\": {\n        \"title\": \"작업 공간 글꼴\",\n        \"noFontHint\": \"글꼴을 찾을 수 없습니다. 다른 용어를 시도하세요.\"\n      },\n      \"textDirection\": {\n        \"title\": \"텍스트 방향\",\n        \"leftToRight\": \"왼쪽에서 오른쪽으로\",\n        \"rightToLeft\": \"오른쪽에서 왼쪽으로\",\n        \"auto\": \"자동\",\n        \"enableRTLItems\": \"RTL 도구 모음 항목 활성화\"\n      },\n      \"layoutDirection\": {\n        \"title\": \"레이아웃 방향\",\n        \"leftToRight\": \"왼쪽에서 오른쪽으로\",\n        \"rightToLeft\": \"오른쪽에서 왼쪽으로\"\n      },\n      \"dateTime\": {\n        \"title\": \"날짜 및 시간\",\n        \"example\": \"{}에 {} ({})\",\n        \"24HourTime\": \"24시간 형식\",\n        \"dateFormat\": {\n          \"label\": \"날짜 형식\",\n          \"local\": \"로컬\",\n          \"us\": \"미국\",\n          \"iso\": \"ISO\",\n          \"friendly\": \"친숙한\",\n          \"dmy\": \"일/월/년\"\n        }\n      },\n      \"language\": {\n        \"title\": \"언어\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"작업 공간 삭제\",\n        \"content\": \"이 작업 공간을 삭제하시겠습니까? 이 작업은 되돌릴 수 없으며, 게시한 모든 페이지가 게시 취소됩니다.\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"작업 공간 나가기\",\n        \"content\": \"이 작업 공간을 나가시겠습니까? 모든 페이지와 데이터에 대한 액세스를 잃게 됩니다.\",\n        \"success\": \"작업 공간을 성공적으로 나갔습니다.\",\n        \"fail\": \"작업 공간 나가기 실패.\"\n      },\n      \"manageWorkspace\": {\n        \"title\": \"작업 공간 관리\",\n        \"leaveWorkspace\": \"작업 공간 나가기\",\n        \"deleteWorkspace\": \"작업 공간 삭제\"\n      }\n    },\n    \"manageDataPage\": {\n      \"menuLabel\": \"데이터 관리\",\n      \"title\": \"데이터 관리\",\n      \"description\": \"로컬 저장소 데이터를 관리하거나 기존 데이터를 @:appName에 가져옵니다.\",\n      \"dataStorage\": {\n        \"title\": \"파일 저장 위치\",\n        \"tooltip\": \"파일이 저장되는 위치\",\n        \"actions\": {\n          \"change\": \"경로 변경\",\n          \"open\": \"폴더 열기\",\n          \"openTooltip\": \"현재 데이터 폴더 위치 열기\",\n          \"copy\": \"경로 복사\",\n          \"copiedHint\": \"경로가 복사되었습니다!\",\n          \"resetTooltip\": \"기본 위치로 재설정\"\n        },\n        \"resetDialog\": {\n          \"title\": \"확실합니까?\",\n          \"description\": \"경로를 기본 데이터 위치로 재설정해도 데이터가 삭제되지 않습니다. 현재 데이터를 다시 가져오려면 현재 위치의 경로를 먼저 복사해야 합니다.\"\n        }\n      },\n      \"importData\": {\n        \"title\": \"데이터 가져오기\",\n        \"tooltip\": \"@:appName 백업/데이터 폴더에서 데이터 가져오기\",\n        \"description\": \"외부 @:appName 데이터 폴더에서 데이터 복사\",\n        \"action\": \"파일 찾아보기\"\n      },\n      \"encryption\": {\n        \"title\": \"암호화\",\n        \"tooltip\": \"데이터 저장 및 암호화 방법 관리\",\n        \"descriptionNoEncryption\": \"암호화를 켜면 모든 데이터가 암호화됩니다. 이 작업은 되돌릴 수 없습니다.\",\n        \"descriptionEncrypted\": \"데이터가 암호화되었습니다.\",\n        \"action\": \"데이터 암호화\",\n        \"dialog\": {\n          \"title\": \"모든 데이터를 암호화하시겠습니까?\",\n          \"description\": \"모든 데이터를 암호화하면 데이터가 안전하고 보호됩니다. 이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?\"\n        }\n      },\n      \"cache\": {\n        \"title\": \"캐시 지우기\",\n        \"description\": \"이미지가 로드되지 않거나 공간에 페이지가 누락되거나 글꼴이 로드되지 않는 등의 문제를 해결하는 데 도움이 됩니다. 데이터에는 영향을 미치지 않습니다.\",\n        \"dialog\": {\n          \"title\": \"캐시 지우기\",\n          \"description\": \"이미지가 로드되지 않거나 공간에 페이지가 누락되거나 글꼴이 로드되지 않는 등의 문제를 해결하는 데 도움이 됩니다. 데이터에는 영향을 미치지 않습니다.\",\n          \"successHint\": \"캐시가 지워졌습니다!\"\n        }\n      },\n      \"data\": {\n        \"fixYourData\": \"데이터 수정\",\n        \"fixButton\": \"수정\",\n        \"fixYourDataDescription\": \"데이터에 문제가 있는 경우 여기에서 수정할 수 있습니다.\"\n      }\n    },\n    \"shortcutsPage\": {\n      \"menuLabel\": \"단축키\",\n      \"title\": \"단축키\",\n      \"editBindingHint\": \"새 바인딩 입력\",\n      \"searchHint\": \"검색\",\n      \"actions\": {\n        \"resetDefault\": \"기본값으로 재설정\"\n      },\n      \"errorPage\": {\n        \"message\": \"단축키를 로드하지 못했습니다: {}\",\n        \"howToFix\": \"다시 시도해 보세요. 문제가 계속되면 GitHub에 문의하세요.\"\n      },\n      \"resetDialog\": {\n        \"title\": \"단축키 재설정\",\n        \"description\": \"모든 키 바인딩을 기본값으로 재설정합니다. 나중에 되돌릴 수 없습니다. 계속하시겠습니까?\",\n        \"buttonLabel\": \"재설정\"\n      },\n      \"conflictDialog\": {\n        \"title\": \"{}가 이미 사용 중입니다\",\n        \"descriptionPrefix\": \"이 키 바인딩은 현재 \",\n        \"descriptionSuffix\": \"에서 사용 중입니다. 이 키 바인딩을 교체하면 {}에서 제거됩니다.\",\n        \"confirmLabel\": \"계속\"\n      },\n      \"editTooltip\": \"키 바인딩 편집을 시작하려면 누르세요\",\n      \"keybindings\": {\n        \"toggleToDoList\": \"할 일 목록 전환\",\n        \"insertNewParagraphInCodeblock\": \"새 단락 삽입\",\n        \"pasteInCodeblock\": \"코드 블록에 붙여넣기\",\n        \"selectAllCodeblock\": \"모두 선택\",\n        \"indentLineCodeblock\": \"줄 시작에 두 칸 삽입\",\n        \"outdentLineCodeblock\": \"줄 시작에서 두 칸 삭제\",\n        \"twoSpacesCursorCodeblock\": \"커서 위치에 두 칸 삽입\",\n        \"copy\": \"선택 항목 복사\",\n        \"paste\": \"내용 붙여넣기\",\n        \"cut\": \"선택 항목 잘라내기\",\n        \"alignLeft\": \"텍스트 왼쪽 정렬\",\n        \"alignCenter\": \"텍스트 가운데 정렬\",\n        \"alignRight\": \"텍스트 오른쪽 정렬\",\n        \"insertInlineMathEquation\": \"인라인 수학 방정식 삽입\",\n        \"undo\": \"실행 취소\",\n        \"redo\": \"다시 실행\",\n        \"convertToParagraph\": \"블록을 단락으로 변환\",\n        \"backspace\": \"삭제\",\n        \"deleteLeftWord\": \"왼쪽 단어 삭제\",\n        \"deleteLeftSentence\": \"왼쪽 문장 삭제\",\n        \"delete\": \"오른쪽 문자 삭제\",\n        \"deleteMacOS\": \"왼쪽 문자 삭제\",\n        \"deleteRightWord\": \"오른쪽 단어 삭제\",\n        \"moveCursorLeft\": \"커서를 왼쪽으로 이동\",\n        \"moveCursorBeginning\": \"커서를 시작으로 이동\",\n        \"moveCursorLeftWord\": \"커서를 왼쪽 단어로 이동\",\n        \"moveCursorLeftSelect\": \"선택하고 커서를 왼쪽으로 이동\",\n        \"moveCursorBeginSelect\": \"선택하고 커서를 시작으로 이동\",\n        \"moveCursorLeftWordSelect\": \"선택하고 커서를 왼쪽 단어로 이동\",\n        \"moveCursorRight\": \"커서를 오른쪽으로 이동\",\n        \"moveCursorEnd\": \"커서를 끝으로 이동\",\n        \"moveCursorRightWord\": \"커서를 오른쪽 단어로 이동\",\n        \"moveCursorRightSelect\": \"선택하고 커서를 오른쪽으로 이동\",\n        \"moveCursorEndSelect\": \"선택하고 커서를 끝으로 이동\",\n        \"moveCursorRightWordSelect\": \"선택하고 커서를 오른쪽 단어로 이동\",\n        \"moveCursorUp\": \"커서를 위로 이동\",\n        \"moveCursorTopSelect\": \"선택하고 커서를 위로 이동\",\n        \"moveCursorTop\": \"커서를 위로 이동\",\n        \"moveCursorUpSelect\": \"선택하고 커서를 위로 이동\",\n        \"moveCursorBottomSelect\": \"선택하고 커서를 아래로 이동\",\n        \"moveCursorBottom\": \"커서를 아래로 이동\",\n        \"moveCursorDown\": \"커서를 아래로 이동\",\n        \"moveCursorDownSelect\": \"선택하고 커서를 아래로 이동\",\n        \"home\": \"맨 위로 스크롤\",\n        \"end\": \"맨 아래로 스크롤\",\n        \"toggleBold\": \"굵게 전환\",\n        \"toggleItalic\": \"기울임꼴 전환\",\n        \"toggleUnderline\": \"밑줄 전환\",\n        \"toggleStrikethrough\": \"취소선 전환\",\n        \"toggleCode\": \"인라인 코드 전환\",\n        \"toggleHighlight\": \"강조 전환\",\n        \"showLinkMenu\": \"링크 메뉴 표시\",\n        \"openInlineLink\": \"인라인 링크 열기\",\n        \"openLinks\": \"선택한 모든 링크 열기\",\n        \"indent\": \"들여쓰기\",\n        \"outdent\": \"내어쓰기\",\n        \"exit\": \"편집 종료\",\n        \"pageUp\": \"한 페이지 위로 스크롤\",\n        \"pageDown\": \"한 페이지 아래로 스크롤\",\n        \"selectAll\": \"모두 선택\",\n        \"pasteWithoutFormatting\": \"서식 없이 붙여넣기\",\n        \"showEmojiPicker\": \"이모지 선택기 표시\",\n        \"enterInTableCell\": \"테이블에 줄 바꿈 추가\",\n        \"leftInTableCell\": \"테이블에서 왼쪽 셀로 이동\",\n        \"rightInTableCell\": \"테이블에서 오른쪽 셀로 이동\",\n        \"upInTableCell\": \"테이블에서 위쪽 셀로 이동\",\n        \"downInTableCell\": \"테이블에서 아래쪽 셀로 이동\",\n        \"tabInTableCell\": \"테이블에서 다음 사용 가능한 셀로 이동\",\n        \"shiftTabInTableCell\": \"테이블에서 이전 사용 가능한 셀로 이동\",\n        \"backSpaceInTableCell\": \"셀의 시작 부분에서 멈춤\"\n      },\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"코드 블록 옆에 새 단락 삽입\",\n        \"codeBlockIndentLines\": \"코드 블록에서 줄 시작에 두 칸 삽입\",\n        \"codeBlockOutdentLines\": \"코드 블록에서 줄 시작에서 두 칸 삭제\",\n        \"codeBlockAddTwoSpaces\": \"코드 블록에서 커서 위치에 두 칸 삽입\",\n        \"codeBlockSelectAll\": \"코드 블록 내의 모든 내용 선택\",\n        \"codeBlockPasteText\": \"코드 블록에 텍스트 붙여넣기\",\n        \"textAlignLeft\": \"텍스트를 왼쪽으로 정렬\",\n        \"textAlignCenter\": \"텍스트를 가운데로 정렬\",\n        \"textAlignRight\": \"텍스트를 오른쪽으로 정렬\"\n      },\n      \"couldNotLoadErrorMsg\": \"단축키를 로드할 수 없습니다. 다시 시도하세요\",\n      \"couldNotSaveErrorMsg\": \"단축키를 저장할 수 없습니다. 다시 시도하세요\"\n    },\n    \"aiPage\": {\n      \"title\": \"AI 설정\",\n      \"menuLabel\": \"AI 설정\",\n      \"keys\": {\n        \"enableAISearchTitle\": \"AI 검색\",\n        \"aiSettingsDescription\": \"선호하는 모델을 선택하여 AppFlowy AI를 구동하세요. 이제 GPT 4-o, Claude 3,5, Llama 3.1 및 Mistral 7B를 포함합니다\",\n        \"loginToEnableAIFeature\": \"AI 기능은 @:appName Cloud에 로그인한 후에만 활성화됩니다. @:appName 계정이 없는 경우 '내 계정'에서 가입하세요\",\n        \"llmModel\": \"언어 모델\",\n        \"llmModelType\": \"언어 모델 유형\",\n        \"downloadLLMPrompt\": \"{} 다운로드\",\n        \"downloadAppFlowyOfflineAI\": \"AI 오프라인 패키지를 다운로드하면 AI가 장치에서 실행됩니다. 계속하시겠습니까?\",\n        \"downloadLLMPromptDetail\": \"{} 로컬 모델을 다운로드하면 최대 {}의 저장 공간이 필요합니다. 계속하시겠습니까?\",\n        \"downloadBigFilePrompt\": \"다운로드 완료까지 약 10분이 소요될 수 있습니다\",\n        \"downloadAIModelButton\": \"다운로드\",\n        \"downloadingModel\": \"다운로드 중\",\n        \"localAILoaded\": \"로컬 AI 모델이 성공적으로 추가되어 사용할 준비가 되었습니다\",\n        \"localAIStart\": \"로컬 AI가 시작 중입니다. 느리다면 껐다가 다시 켜보세요\",\n        \"localAILoading\": \"로컬 AI 채팅 모델이 로드 중입니다...\",\n        \"localAIStopped\": \"로컬 AI가 중지되었습니다\",\n        \"localAIRunning\": \"로컬 AI가 실행 중입니다\",\n        \"localAIInitializing\": \"로컬 AI가 로드 중이며 장치에 따라 몇 분이 소요될 수 있습니다\",\n        \"localAINotReadyTextFieldPrompt\": \"로컬 AI가 로드되는 동안 편집할 수 없습니다\",\n        \"failToLoadLocalAI\": \"로컬 AI를 시작하지 못했습니다\",\n        \"restartLocalAI\": \"로컬 AI 다시 시작\",\n        \"disableLocalAITitle\": \"로컬 AI 비활성화\",\n        \"disableLocalAIDescription\": \"로컬 AI를 비활성화하시겠습니까?\",\n        \"localAIToggleTitle\": \"로컬 AI를 활성화 또는 비활성화하려면 전환\",\n        \"offlineAIInstruction1\": \"다음을 따르세요\",\n        \"offlineAIInstruction2\": \"지침\",\n        \"offlineAIInstruction3\": \"오프라인 AI를 활성화하려면\",\n        \"offlineAIDownload1\": \"AppFlowy AI를 다운로드하지 않은 경우 먼저\",\n        \"offlineAIDownload2\": \"다운로드\",\n        \"offlineAIDownload3\": \"하세요\",\n        \"activeOfflineAI\": \"활성화됨\",\n        \"downloadOfflineAI\": \"다운로드\",\n        \"openModelDirectory\": \"폴더 열기\",\n        \"pleaseFollowThese\": \"지침\",\n        \"instructions\": \"이 지침을 따르세요\",\n        \"installOllamaLai\": \"Ollama 및 AppFlowy 로컬 AI를 설정합니다. 이미 설정한 경우 건너뛰세요\",\n        \"startLocalAI\": \"로컬 AI를 시작하는 데 몇 초가 소요될 수 있습니다\"\n      }\n    },\n    \"planPage\": {\n      \"menuLabel\": \"플랜\",\n      \"title\": \"가격 플랜\",\n      \"planUsage\": {\n        \"title\": \"플랜 사용 요약\",\n        \"storageLabel\": \"저장 공간\",\n        \"storageUsage\": \"{} / {} GB\",\n        \"unlimitedStorageLabel\": \"무제한 저장 공간\",\n        \"collaboratorsLabel\": \"멤버\",\n        \"collaboratorsUsage\": \"{} / {}\",\n        \"aiResponseLabel\": \"AI 응답\",\n        \"aiResponseUsage\": \"{} / {}\",\n        \"unlimitedAILabel\": \"무제한 응답\",\n        \"proBadge\": \"Pro\",\n        \"aiMaxBadge\": \"AI Max\",\n        \"aiOnDeviceBadge\": \"Mac용 AI On-device\",\n        \"memberProToggle\": \"더 많은 멤버 및 무제한 AI\",\n        \"aiMaxToggle\": \"무제한 AI 및 고급 모델 액세스\",\n        \"aiOnDeviceToggle\": \"최고의 프라이버시를 위한 로컬 AI\",\n        \"aiCredit\": {\n          \"title\": \"@:appName AI 크레딧 추가\",\n          \"price\": \"{}\",\n          \"priceDescription\": \"1,000 크레딧당\",\n          \"purchase\": \"AI 구매\",\n          \"info\": \"작업 공간당 1,000개의 AI 크레딧을 추가하여 더 스마트하고 빠른 결과를 위해 AI를 워크플로에 원활하게 통합하세요:\",\n          \"infoItemOne\": \"데이터베이스당 최대 10,000개의 응답\",\n          \"infoItemTwo\": \"작업 공간당 최대 1,000개의 응답\"\n        },\n        \"currentPlan\": {\n          \"bannerLabel\": \"현재 플랜\",\n          \"freeTitle\": \"무료\",\n          \"proTitle\": \"Pro\",\n          \"teamTitle\": \"팀\",\n          \"freeInfo\": \"모든 것을 정리하기 위한 최대 2명의 개인용\",\n          \"proInfo\": \"최대 10명의 소규모 팀을 위한 완벽한 솔루션.\",\n          \"teamInfo\": \"모든 생산적이고 잘 조직된 팀을 위한 완벽한 솔루션.\",\n          \"upgrade\": \"플랜 변경\",\n          \"canceledInfo\": \"플랜이 취소되었습니다. {}에 무료 플랜으로 다운그레이드됩니다.\"\n        },\n        \"addons\": {\n          \"title\": \"애드온\",\n          \"addLabel\": \"추가\",\n          \"activeLabel\": \"추가됨\",\n          \"aiMax\": {\n            \"title\": \"AI Max\",\n            \"description\": \"무제한 AI 응답 및 고급 AI 모델로 구동되는 50개의 AI 이미지를 매월 제공합니다\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"연간 청구되는 사용자당 월별\"\n          },\n          \"aiOnDevice\": {\n            \"title\": \"Mac용 AI On-device\",\n            \"description\": \"장치에서 Mistral 7B, LLAMA 3 및 기타 로컬 모델 실행\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"연간 청구되는 사용자당 월별\",\n            \"recommend\": \"M1 이상 권장\"\n          }\n        },\n        \"deal\": {\n          \"bannerLabel\": \"새해 할인!\",\n          \"title\": \"팀을 성장시키세요!\",\n          \"info\": \"Pro 및 팀 플랜을 업그레이드하고 10% 할인 혜택을 받으세요! @:appName AI를 포함한 강력한 새로운 기능으로 작업 공간 생산성을 높이세요.\",\n          \"viewPlans\": \"플랜 보기\"\n        }\n      }\n    },\n    \"billingPage\": {\n      \"menuLabel\": \"청구\",\n      \"title\": \"청구\",\n      \"plan\": {\n        \"title\": \"플랜\",\n        \"freeLabel\": \"무료\",\n        \"proLabel\": \"Pro\",\n        \"planButtonLabel\": \"플랜 변경\",\n        \"billingPeriod\": \"청구 기간\",\n        \"periodButtonLabel\": \"기간 수정\"\n      },\n      \"paymentDetails\": {\n        \"title\": \"결제 세부 정보\",\n        \"methodLabel\": \"결제 방법\",\n        \"methodButtonLabel\": \"방법 수정\"\n      },\n      \"addons\": {\n        \"title\": \"애드온\",\n        \"addLabel\": \"추가\",\n        \"removeLabel\": \"제거\",\n        \"renewLabel\": \"갱신\",\n        \"aiMax\": {\n          \"label\": \"AI Max\",\n          \"description\": \"무제한 AI 및 고급 모델 잠금 해제\",\n          \"activeDescription\": \"다음 청구서가 {}에 만료됩니다\",\n          \"canceledDescription\": \"AI Max는 {}까지 사용할 수 있습니다\"\n        },\n        \"aiOnDevice\": {\n          \"label\": \"Mac용 AI On-device\",\n          \"description\": \"장치에서 무제한 AI 잠금 해제\",\n          \"activeDescription\": \"다음 청구서가 {}에 만료됩니다\",\n          \"canceledDescription\": \"Mac용 AI On-device는 {}까지 사용할 수 있습니다\"\n        },\n        \"removeDialog\": {\n          \"title\": \"{} 제거\",\n          \"description\": \"{plan}을 제거하시겠습니까? {plan}의 기능과 혜택에 대한 액세스를 즉시 잃게 됩니다.\"\n        }\n      },\n      \"currentPeriodBadge\": \"현재\",\n      \"changePeriod\": \"기간 변경\",\n      \"planPeriod\": \"{} 기간\",\n      \"monthlyInterval\": \"월별\",\n      \"monthlyPriceInfo\": \"월별 청구되는 좌석당\",\n      \"annualInterval\": \"연간\",\n      \"annualPriceInfo\": \"연간 청구되는 좌석당\"\n    },\n    \"comparePlanDialog\": {\n      \"title\": \"플랜 비교 및 선택\",\n      \"planFeatures\": \"플랜\\n기능\",\n      \"current\": \"현재\",\n      \"actions\": {\n        \"upgrade\": \"업그레이드\",\n        \"downgrade\": \"다운그레이드\",\n        \"current\": \"현재\"\n      },\n      \"freePlan\": {\n        \"title\": \"무료\",\n        \"description\": \"모든 것을 정리하기 위한 최대 2명의 개인용\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"영원히 무료\"\n      },\n      \"proPlan\": {\n        \"title\": \"Pro\",\n        \"description\": \"프로젝트 및 팀 지식을 관리하기 위한 소규모 팀용\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"연간 청구되는 사용자당 월별\\n\\n{} 월별 청구\"\n      },\n      \"planLabels\": {\n        \"itemOne\": \"작업 공간\",\n        \"itemTwo\": \"멤버\",\n        \"itemThree\": \"저장 공간\",\n        \"itemFour\": \"실시간 협업\",\n        \"itemFive\": \"모바일 앱\",\n        \"itemSix\": \"AI 응답\",\n        \"itemSeven\": \"AI 이미지\",\n        \"itemFileUpload\": \"파일 업로드\",\n        \"customNamespace\": \"맞춤 네임스페이스\",\n        \"tooltipSix\": \"평생 동안 응답 수는 재설정되지 않습니다\",\n        \"intelligentSearch\": \"지능형 검색\",\n        \"tooltipSeven\": \"작업 공간의 URL 일부를 사용자 정의할 수 있습니다\",\n        \"customNamespaceTooltip\": \"맞춤 게시 사이트 URL\"\n      },\n      \"freeLabels\": {\n        \"itemOne\": \"작업 공간당 청구\",\n        \"itemTwo\": \"최대 2명\",\n        \"itemThree\": \"5 GB\",\n        \"itemFour\": \"예\",\n        \"itemFive\": \"예\",\n        \"itemSix\": \"평생 10회\",\n        \"itemSeven\": \"평생 2회\",\n        \"itemFileUpload\": \"최대 7 MB\",\n        \"intelligentSearch\": \"지능형 검색\"\n      },\n      \"proLabels\": {\n        \"itemOne\": \"작업 공간당 청구\",\n        \"itemTwo\": \"최대 10명\",\n        \"itemThree\": \"무제한\",\n        \"itemFour\": \"예\",\n        \"itemFive\": \"예\",\n        \"itemSix\": \"무제한\",\n        \"itemSeven\": \"월별 10개 이미지\",\n        \"itemFileUpload\": \"무제한\",\n        \"intelligentSearch\": \"지능형 검색\"\n      },\n      \"paymentSuccess\": {\n        \"title\": \"이제 {} 플랜을 사용 중입니다!\",\n        \"description\": \"결제가 성공적으로 처리되었으며 플랜이 @:appName {}로 업그레이드되었습니다. 플랜 세부 정보를 플랜 페이지에서 확인할 수 있습니다\"\n      },\n      \"downgradeDialog\": {\n        \"title\": \"플랜을 다운그레이드하시겠습니까?\",\n        \"description\": \"플랜을 다운그레이드하면 무료 플랜으로 돌아갑니다. 멤버는 이 작업 공간에 대한 액세스를 잃을 수 있으며 저장 공간 제한을 충족하기 위해 공간을 확보해야 할 수 있습니다.\",\n        \"downgradeLabel\": \"플랜 다운그레이드\"\n      }\n    },\n    \"cancelSurveyDialog\": {\n      \"title\": \"떠나셔서 아쉽습니다\",\n      \"description\": \"@:appName을 개선하는 데 도움이 되도록 피드백을 듣고 싶습니다. 몇 가지 질문에 답변해 주세요.\",\n      \"commonOther\": \"기타\",\n      \"otherHint\": \"여기에 답변을 작성하세요\",\n      \"questionOne\": {\n        \"question\": \"@:appName Pro 구독을 취소한 이유는 무엇입니까?\",\n        \"answerOne\": \"비용이 너무 높음\",\n        \"answerTwo\": \"기능이 기대에 미치지 못함\",\n        \"answerThree\": \"더 나은 대안을 찾음\",\n        \"answerFour\": \"비용을 정당화할 만큼 충분히 사용하지 않음\",\n        \"answerFive\": \"서비스 문제 또는 기술적 어려움\"\n      },\n      \"questionTwo\": {\n        \"question\": \"미래에 @:appName Pro를 다시 구독할 가능성은 얼마나 됩니까?\",\n        \"answerOne\": \"매우 가능성이 높음\",\n        \"answerTwo\": \"어느 정도 가능성이 있음\",\n        \"answerThree\": \"잘 모르겠음\",\n        \"answerFour\": \"가능성이 낮음\",\n        \"answerFive\": \"매우 가능성이 낮음\"\n      },\n      \"questionThree\": {\n        \"question\": \"구독 기간 동안 가장 가치 있게 여긴 Pro 기능은 무엇입니까?\",\n        \"answerOne\": \"다중 사용자 협업\",\n        \"answerTwo\": \"더 긴 시간 버전 기록\",\n        \"answerThree\": \"무제한 AI 응답\",\n        \"answerFour\": \"로컬 AI 모델 액세스\"\n      },\n      \"questionFour\": {\n        \"question\": \"@:appName에 대한 전반적인 경험을 어떻게 설명하시겠습니까?\",\n        \"answerOne\": \"훌륭함\",\n        \"answerTwo\": \"좋음\",\n        \"answerThree\": \"보통\",\n        \"answerFour\": \"평균 이하\",\n        \"answerFive\": \"불만족\"\n      }\n    },\n    \"common\": {\n      \"uploadingFile\": \"파일 업로드 중입니다. 앱을 종료하지 마세요\",\n      \"uploadNotionSuccess\": \"Notion zip 파일이 성공적으로 업로드되었습니다. 가져오기가 완료되면 확인 이메일을 받게 됩니다\",\n      \"reset\": \"재설정\"\n    },\n    \"menu\": {\n      \"appearance\": \"외관\",\n      \"language\": \"언어\",\n      \"user\": \"사용자\",\n      \"files\": \"파일\",\n      \"notifications\": \"알림\",\n      \"open\": \"설정 열기\",\n      \"logout\": \"로그아웃\",\n      \"logoutPrompt\": \"로그아웃하시겠습니까?\",\n      \"selfEncryptionLogoutPrompt\": \"로그아웃하시겠습니까? 암호화 비밀을 복사했는지 확인하세요\",\n      \"syncSetting\": \"동기화 설정\",\n      \"cloudSettings\": \"클라우드 설정\",\n      \"enableSync\": \"동기화 활성화\",\n      \"enableSyncLog\": \"동기화 로그 활성화\",\n      \"enableSyncLogWarning\": \"동기화 문제를 진단하는 데 도움을 주셔서 감사합니다. 이 작업은 문서 편집 내용을 로컬 파일에 기록합니다. 활성화 후 앱을 종료하고 다시 열어야 합니다\",\n      \"enableEncrypt\": \"데이터 암호화\",\n      \"cloudURL\": \"기본 URL\",\n      \"webURL\": \"웹 URL\",\n      \"invalidCloudURLScheme\": \"잘못된 스키마\",\n      \"cloudServerType\": \"클라우드 서버\",\n      \"cloudServerTypeTip\": \"클라우드 서버를 변경한 후 현재 계정에서 로그아웃될 수 있습니다\",\n      \"cloudLocal\": \"로컬\",\n      \"cloudAppFlowy\": \"@:appName Cloud\",\n      \"cloudAppFlowySelfHost\": \"@:appName Cloud 셀프 호스팅\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"클라우드 URL은 비워둘 수 없습니다\",\n      \"clickToCopy\": \"클립보드에 복사\",\n      \"selfHostStart\": \"서버가 없는 경우\",\n      \"selfHostContent\": \"문서\",\n      \"selfHostEnd\": \"를 참조하여 셀프 호스팅 서버를 설정하는 방법을 확인하세요\",\n      \"pleaseInputValidURL\": \"유효한 URL을 입력하세요\",\n      \"changeUrl\": \"셀프 호스팅 URL을 {}로 변경\",\n      \"cloudURLHint\": \"서버의 기본 URL을 입력하세요\",\n      \"webURLHint\": \"웹 서버의 기본 URL을 입력하세요\",\n      \"cloudWSURL\": \"웹소켓 URL\",\n      \"cloudWSURLHint\": \"서버의 웹소켓 주소를 입력하세요\",\n      \"restartApp\": \"재시작\",\n      \"restartAppTip\": \"변경 사항을 적용하려면 애플리케이션을 재시작하세요. 현재 계정에서 로그아웃될 수 있습니다.\",\n      \"changeServerTip\": \"서버를 변경한 후 변경 사항을 적용하려면 재시작 버튼을 클릭해야 합니다\",\n      \"enableEncryptPrompt\": \"이 비밀로 데이터를 보호하려면 암호화를 활성화하세요. 안전하게 보관하세요. 활성화 후에는 비활성화할 수 없습니다. 비밀을 잃어버리면 데이터를 복구할 수 없습니다. 복사하려면 클릭하세요\",\n      \"inputEncryptPrompt\": \"암호화 비밀을 입력하세요\",\n      \"clickToCopySecret\": \"비밀을 복사하려면 클릭\",\n      \"configServerSetting\": \"서버 설정 구성\",\n      \"configServerGuide\": \"`빠른 시작`을 선택한 후 `설정`으로 이동하여 \\\"클라우드 설정\\\"을 구성하세요.\",\n      \"inputTextFieldHint\": \"비밀\",\n      \"historicalUserList\": \"사용자 로그인 기록\",\n      \"historicalUserListTooltip\": \"이 목록에는 익명 계정이 표시됩니다. 계정을 클릭하여 세부 정보를 확인할 수 있습니다. 익명 계정은 '시작하기' 버튼을 클릭하여 생성됩니다\",\n      \"openHistoricalUser\": \"익명 계정을 열려면 클릭\",\n      \"customPathPrompt\": \"Google Drive와 같은 클라우드 동기화 폴더에 @:appName 데이터 폴더를 저장하면 위험이 발생할 수 있습니다. 이 폴더 내의 데이터베이스에 여러 위치에서 동시에 액세스하거나 수정하면 동기화 충돌 및 데이터 손상이 발생할 수 있습니다\",\n      \"importAppFlowyData\": \"외부 @:appName 폴더에서 데이터 가져오기\",\n      \"importingAppFlowyDataTip\": \"데이터 가져오는 중입니다. 앱을 종료하지 마세요\",\n      \"importAppFlowyDataDescription\": \"외부 @:appName 데이터 폴더에서 데이터를 복사하여 현재 AppFlowy 데이터 폴더에 가져옵니다\",\n      \"importSuccess\": \"성공적으로 @:appName 데이터 폴더를 가져왔습니다\",\n      \"importFailed\": \"@:appName 데이터 폴더 가져오기 실패\",\n      \"importGuide\": \"자세한 내용은 참조 문서를 확인하세요\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"알림 활성화\",\n        \"hint\": \"로컬 알림이 나타나지 않도록 하려면 끄세요.\"\n      },\n      \"showNotificationsIcon\": {\n        \"label\": \"알림 아이콘 표시\",\n        \"hint\": \"사이드바에서 알림 아이콘을 숨기려면 끄세요.\"\n      },\n      \"archiveNotifications\": {\n        \"allSuccess\": \"모든 알림이 성공적으로 보관되었습니다\",\n        \"success\": \"알림이 성공적으로 보관되었습니다\"\n      },\n      \"markAsReadNotifications\": {\n        \"allSuccess\": \"모두 읽음으로 표시되었습니다\",\n        \"success\": \"읽음으로 표시되었습니다\"\n      },\n      \"action\": {\n        \"markAsRead\": \"읽음으로 표시\",\n        \"multipleChoice\": \"더 선택\",\n        \"archive\": \"보관\"\n      },\n      \"settings\": {\n        \"settings\": \"설정\",\n        \"markAllAsRead\": \"모두 읽음으로 표시\",\n        \"archiveAll\": \"모두 보관\"\n      },\n      \"emptyInbox\": {\n        \"title\": \"받은 편지함 비어 있음!\",\n        \"description\": \"알림을 받으려면 알림을 설정하세요.\"\n      },\n      \"emptyUnread\": {\n        \"title\": \"읽지 않은 알림 없음\",\n        \"description\": \"모든 알림을 확인했습니다!\"\n      },\n      \"emptyArchived\": {\n        \"title\": \"보관된 항목 없음\",\n        \"description\": \"보관된 알림이 여기에 표시됩니다.\"\n      },\n      \"tabs\": {\n        \"inbox\": \"받은 편지함\",\n        \"unread\": \"읽지 않음\",\n        \"archived\": \"보관됨\"\n      },\n      \"refreshSuccess\": \"알림이 성공적으로 새로고침되었습니다\",\n      \"titles\": {\n        \"notifications\": \"알림\",\n        \"reminder\": \"알림\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"재설정\",\n      \"fontFamily\": {\n        \"label\": \"글꼴\",\n        \"search\": \"검색\",\n        \"defaultFont\": \"시스템\"\n      },\n      \"themeMode\": {\n        \"label\": \"테마 모드\",\n        \"light\": \"라이트 모드\",\n        \"dark\": \"다크 모드\",\n        \"system\": \"시스템에 맞춤\"\n      },\n      \"fontScaleFactor\": \"글꼴 크기 비율\",\n      \"displaySize\": \"디스플레이 크기\",\n      \"documentSettings\": {\n        \"cursorColor\": \"문서 커서 색상\",\n        \"selectionColor\": \"문서 선택 색상\",\n        \"width\": \"문서 너비\",\n        \"changeWidth\": \"변경\",\n        \"pickColor\": \"색상 선택\",\n        \"colorShade\": \"색상 음영\",\n        \"opacity\": \"불투명도\",\n        \"hexEmptyError\": \"16진수 색상은 비워둘 수 없습니다\",\n        \"hexLengthError\": \"16진수 값은 6자리여야 합니다\",\n        \"hexInvalidError\": \"잘못된 16진수 값\",\n        \"opacityEmptyError\": \"불투명도는 비워둘 수 없습니다\",\n        \"opacityRangeError\": \"불투명도는 1에서 100 사이여야 합니다\",\n        \"app\": \"앱\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"적용\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"레이아웃 방향\",\n        \"hint\": \"화면의 콘텐츠 흐름을 왼쪽에서 오른쪽 또는 오른쪽에서 왼쪽으로 제어합니다.\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\"\n      },\n      \"textDirection\": {\n        \"label\": \"기본 텍스트 방향\",\n        \"hint\": \"텍스트가 기본적으로 왼쪽에서 시작할지 오른쪽에서 시작할지 지정합니다.\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\",\n        \"auto\": \"자동\",\n        \"fallback\": \"레이아웃 방향과 동일\"\n      },\n      \"themeUpload\": {\n        \"button\": \"업로드\",\n        \"uploadTheme\": \"테마 업로드\",\n        \"description\": \"아래 버튼을 사용하여 사용자 정의 @:appName 테마를 업로드하세요.\",\n        \"loading\": \"테마를 검증하고 업로드하는 동안 기다려주세요...\",\n        \"uploadSuccess\": \"테마가 성공적으로 업로드되었습니다\",\n        \"deletionFailure\": \"테마를 삭제하지 못했습니다. 수동으로 삭제해 보세요.\",\n        \"filePickerDialogTitle\": \".flowy_plugin 파일 선택\",\n        \"urlUploadFailure\": \"URL을 열지 못했습니다: {}\"\n      },\n      \"theme\": \"테마\",\n      \"builtInsLabel\": \"내장 테마\",\n      \"pluginsLabel\": \"플러그인\",\n      \"dateFormat\": {\n        \"label\": \"날짜 형식\",\n        \"local\": \"로컬\",\n        \"us\": \"미국\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"친숙한\",\n        \"dmy\": \"일/월/년\"\n      },\n      \"timeFormat\": {\n        \"label\": \"시간 형식\",\n        \"twelveHour\": \"12시간 형식\",\n        \"twentyFourHour\": \"24시간 형식\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"페이지 생성 시 이름 지정 대화 상자 표시\",\n      \"enableRTLToolbarItems\": \"RTL 도구 모음 항목 활성화\",\n      \"members\": {\n        \"title\": \"멤버 설정\",\n        \"inviteMembers\": \"멤버 초대\",\n        \"inviteHint\": \"이메일로 초대\",\n        \"sendInvite\": \"초대 보내기\",\n        \"copyInviteLink\": \"초대 링크 복사\",\n        \"label\": \"멤버\",\n        \"user\": \"사용자\",\n        \"role\": \"역할\",\n        \"removeFromWorkspace\": \"작업 공간에서 제거\",\n        \"removeFromWorkspaceSuccess\": \"작업 공간에서 성공적으로 제거되었습니다\",\n        \"removeFromWorkspaceFailed\": \"작업 공간에서 제거 실패\",\n        \"owner\": \"소유자\",\n        \"guest\": \"게스트\",\n        \"member\": \"멤버\",\n        \"memberHintText\": \"멤버는 페이지를 읽고 편집할 수 있습니다\",\n        \"guestHintText\": \"게스트는 페이지를 읽고, 반응하고, 댓글을 달 수 있으며, 권한이 있는 특정 페이지를 편집할 수 있습니다.\",\n        \"emailInvalidError\": \"잘못된 이메일입니다. 확인하고 다시 시도하세요\",\n        \"emailSent\": \"이메일이 전송되었습니다. 받은 편지함을 확인하세요\",\n        \"members\": \"멤버\",\n        \"membersCount\": {\n          \"zero\": \"{}명의 멤버\",\n          \"one\": \"{}명의 멤버\",\n          \"other\": \"{}명의 멤버\"\n        },\n        \"inviteFailedDialogTitle\": \"초대 전송 실패\",\n        \"inviteFailedMemberLimit\": \"멤버 한도에 도달했습니다. 더 많은 멤버를 초대하려면 업그레이드하세요.\",\n        \"inviteFailedMemberLimitMobile\": \"작업 공간의 멤버 한도에 도달했습니다.\",\n        \"memberLimitExceeded\": \"멤버 한도에 도달했습니다. 더 많은 멤버를 초대하려면 \",\n        \"memberLimitExceededUpgrade\": \"업그레이드\",\n        \"memberLimitExceededPro\": \"멤버 한도에 도달했습니다. 더 많은 멤버가 필요하면 \",\n        \"memberLimitExceededProContact\": \"support@appflowy.io에 문의하세요\",\n        \"failedToAddMember\": \"멤버 추가 실패\",\n        \"addMemberSuccess\": \"멤버가 성공적으로 추가되었습니다\",\n        \"removeMember\": \"멤버 제거\",\n        \"areYouSureToRemoveMember\": \"이 멤버를 제거하시겠습니까?\",\n        \"inviteMemberSuccess\": \"초대가 성공적으로 전송되었습니다\",\n        \"failedToInviteMember\": \"멤버 초대 실패\",\n        \"workspaceMembersError\": \"문제가 발생했습니다\",\n        \"workspaceMembersErrorDescription\": \"현재 멤버 목록을 로드할 수 없습니다. 나중에 다시 시도하세요\"\n      }\n    },\n    \"files\": {\n      \"copy\": \"복사\",\n      \"defaultLocation\": \"파일 및 데이터 저장 위치 읽기\",\n      \"exportData\": \"데이터 내보내기\",\n      \"doubleTapToCopy\": \"경로를 복사하려면 두 번 탭하세요\",\n      \"restoreLocation\": \"@:appName 기본 경로로 복원\",\n      \"customizeLocation\": \"다른 폴더 열기\",\n      \"restartApp\": \"변경 사항을 적용하려면 앱을 재시작하세요.\",\n      \"exportDatabase\": \"데이터베이스 내보내기\",\n      \"selectFiles\": \"내보낼 파일 선택\",\n      \"selectAll\": \"모두 선택\",\n      \"deselectAll\": \"모두 선택 해제\",\n      \"createNewFolder\": \"새 폴더 만들기\",\n      \"createNewFolderDesc\": \"데이터를 저장할 위치를 알려주세요\",\n      \"defineWhereYourDataIsStored\": \"데이터가 저장되는 위치 정의\",\n      \"open\": \"열기\",\n      \"openFolder\": \"기존 폴더 열기\",\n      \"openFolderDesc\": \"기존 @:appName 폴더를 읽고 쓰기\",\n      \"folderHintText\": \"폴더 이름\",\n      \"location\": \"새 폴더 만들기\",\n      \"locationDesc\": \"@:appName 데이터 폴더의 이름을 지정하세요\",\n      \"browser\": \"찾아보기\",\n      \"create\": \"생성\",\n      \"set\": \"설정\",\n      \"folderPath\": \"폴더를 저장할 경로\",\n      \"locationCannotBeEmpty\": \"경로는 비워둘 수 없습니다\",\n      \"pathCopiedSnackbar\": \"파일 저장 경로가 클립보드에 복사되었습니다!\",\n      \"changeLocationTooltips\": \"데이터 디렉토리 변경\",\n      \"change\": \"변경\",\n      \"openLocationTooltips\": \"다른 데이터 디렉토리 열기\",\n      \"openCurrentDataFolder\": \"현재 데이터 디렉토리 열기\",\n      \"recoverLocationTooltips\": \"@:appName의 기본 데이터 디렉토리로 재설정\",\n      \"exportFileSuccess\": \"파일이 성공적으로 내보내졌습니다!\",\n      \"exportFileFail\": \"파일 내보내기 실패!\",\n      \"export\": \"내보내기\",\n      \"clearCache\": \"캐시 지우기\",\n      \"clearCacheDesc\": \"이미지가 로드되지 않거나 글꼴이 제대로 표시되지 않는 등의 문제가 발생하면 캐시를 지워보세요. 이 작업은 사용자 데이터에는 영향을 미치지 않습니다.\",\n      \"areYouSureToClearCache\": \"캐시를 지우시겠습니까?\",\n      \"clearCacheSuccess\": \"캐시가 성공적으로 지워졌습니다!\"\n    },\n    \"user\": {\n      \"name\": \"이름\",\n      \"email\": \"이메일\",\n      \"tooltipSelectIcon\": \"아이콘 선택\",\n      \"selectAnIcon\": \"아이콘 선택\",\n      \"pleaseInputYourOpenAIKey\": \"AI 키를 입력하세요\",\n      \"clickToLogout\": \"현재 사용자 로그아웃\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"개인 정보\",\n      \"username\": \"사용자 이름\",\n      \"usernameEmptyError\": \"사용자 이름은 비워둘 수 없습니다\",\n      \"about\": \"정보\",\n      \"pushNotifications\": \"푸시 알림\",\n      \"support\": \"지원\",\n      \"joinDiscord\": \"Discord에 참여\",\n      \"privacyPolicy\": \"개인정보 보호정책\",\n      \"userAgreement\": \"사용자 계약\",\n      \"termsAndConditions\": \"이용 약관\",\n      \"userprofileError\": \"사용자 프로필을 로드하지 못했습니다\",\n      \"userprofileErrorDescription\": \"로그아웃 후 다시 로그인하여 문제가 계속되는지 확인하세요.\",\n      \"selectLayout\": \"레이아웃 선택\",\n      \"selectStartingDay\": \"시작일 선택\",\n      \"version\": \"버전\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"이 보기를 삭제하시겠습니까?\",\n    \"createView\": \"새로 만들기\",\n    \"title\": {\n      \"placeholder\": \"제목 없음\"\n    },\n    \"settings\": {\n      \"filter\": \"필터\",\n      \"sort\": \"정렬\",\n      \"sortBy\": \"정렬 기준\",\n      \"properties\": \"속성\",\n      \"reorderPropertiesTooltip\": \"속성 순서 변경\",\n      \"group\": \"그룹\",\n      \"addFilter\": \"필터 추가\",\n      \"deleteFilter\": \"필터 삭제\",\n      \"filterBy\": \"필터 기준\",\n      \"typeAValue\": \"값 입력...\",\n      \"layout\": \"레이아웃\",\n      \"compactMode\": \"압축 모드\",\n      \"databaseLayout\": \"레이아웃\",\n      \"viewList\": {\n        \"zero\": \"0개의 보기\",\n        \"one\": \"{count}개의 보기\",\n        \"other\": \"{count}개의 보기\"\n      },\n      \"editView\": \"보기 편집\",\n      \"boardSettings\": \"보드 설정\",\n      \"calendarSettings\": \"캘린더 설정\",\n      \"createView\": \"새 보기\",\n      \"duplicateView\": \"보기 복제\",\n      \"deleteView\": \"보기 삭제\",\n      \"numberOfVisibleFields\": \"{}개 표시됨\"\n    },\n    \"filter\": {\n      \"empty\": \"활성 필터 없음\",\n      \"addFilter\": \"필터 추가\",\n      \"cannotFindCreatableField\": \"필터링할 적절한 필드를 찾을 수 없습니다\",\n      \"conditon\": \"조건\",\n      \"where\": \"조건\"\n    },\n    \"textFilter\": {\n      \"contains\": \"포함\",\n      \"doesNotContain\": \"포함하지 않음\",\n      \"endsWith\": \"끝남\",\n      \"startWith\": \"시작\",\n      \"is\": \"일치\",\n      \"isNot\": \"일치하지 않음\",\n      \"isEmpty\": \"비어 있음\",\n      \"isNotEmpty\": \"비어 있지 않음\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"일치하지 않음\",\n        \"startWith\": \"시작\",\n        \"endWith\": \"끝남\",\n        \"isEmpty\": \"비어 있음\",\n        \"isNotEmpty\": \"비어 있지 않음\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"체크됨\",\n      \"isUnchecked\": \"체크되지 않음\",\n      \"choicechipPrefix\": {\n        \"is\": \"체크됨\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"완료됨\",\n      \"isIncomplted\": \"미완료\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"일치\",\n      \"isNot\": \"일치하지 않음\",\n      \"contains\": \"포함\",\n      \"doesNotContain\": \"포함하지 않음\",\n      \"isEmpty\": \"비어 있음\",\n      \"isNotEmpty\": \"비어 있지 않음\"\n    },\n    \"dateFilter\": {\n      \"is\": \"일치\",\n      \"before\": \"이전\",\n      \"after\": \"이후\",\n      \"onOrBefore\": \"이전 또는 일치\",\n      \"onOrAfter\": \"이후 또는 일치\",\n      \"between\": \"사이\",\n      \"empty\": \"비어 있음\",\n      \"notEmpty\": \"비어 있지 않음\",\n      \"startDate\": \"시작 날짜\",\n      \"endDate\": \"종료 날짜\",\n      \"choicechipPrefix\": {\n        \"before\": \"이전\",\n        \"after\": \"이후\",\n        \"between\": \"사이\",\n        \"onOrBefore\": \"이전 또는 일치\",\n        \"onOrAfter\": \"이후 또는 일치\",\n        \"isEmpty\": \"비어 있음\",\n        \"isNotEmpty\": \"비어 있지 않음\"\n      }\n    },\n    \"numberFilter\": {\n      \"equal\": \"일치\",\n      \"notEqual\": \"일치하지 않음\",\n      \"lessThan\": \"미만\",\n      \"greaterThan\": \"초과\",\n      \"lessThanOrEqualTo\": \"이하\",\n      \"greaterThanOrEqualTo\": \"이상\",\n      \"isEmpty\": \"비어 있음\",\n      \"isNotEmpty\": \"비어 있지 않음\"\n    },\n    \"field\": {\n      \"label\": \"속성\",\n      \"hide\": \"속성 숨기기\",\n      \"show\": \"속성 표시\",\n      \"insertLeft\": \"왼쪽에 삽입\",\n      \"insertRight\": \"오른쪽에 삽입\",\n      \"duplicate\": \"복제\",\n      \"delete\": \"삭제\",\n      \"wrapCellContent\": \"텍스트 줄 바꿈\",\n      \"clear\": \"셀 지우기\",\n      \"switchPrimaryFieldTooltip\": \"기본 필드의 필드 유형을 변경할 수 없습니다\",\n      \"textFieldName\": \"텍스트\",\n      \"checkboxFieldName\": \"체크박스\",\n      \"dateFieldName\": \"날짜\",\n      \"updatedAtFieldName\": \"마지막 수정\",\n      \"createdAtFieldName\": \"생성일\",\n      \"numberFieldName\": \"숫자\",\n      \"singleSelectFieldName\": \"선택\",\n      \"multiSelectFieldName\": \"다중 선택\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"체크리스트\",\n      \"relationFieldName\": \"관계\",\n      \"summaryFieldName\": \"AI 요약\",\n      \"timeFieldName\": \"시간\",\n      \"mediaFieldName\": \"파일 및 미디어\",\n      \"translateFieldName\": \"AI 번역\",\n      \"translateTo\": \"번역 대상\",\n      \"numberFormat\": \"숫자 형식\",\n      \"dateFormat\": \"날짜 형식\",\n      \"includeTime\": \"시간 포함\",\n      \"isRange\": \"종료 날짜\",\n      \"dateFormatFriendly\": \"월 일, 년\",\n      \"dateFormatISO\": \"년-월-일\",\n      \"dateFormatLocal\": \"월/일/년\",\n      \"dateFormatUS\": \"년/월/일\",\n      \"dateFormatDayMonthYear\": \"일/월/년\",\n      \"timeFormat\": \"시간 형식\",\n      \"invalidTimeFormat\": \"잘못된 형식\",\n      \"timeFormatTwelveHour\": \"12시간\",\n      \"timeFormatTwentyFourHour\": \"24시간\",\n      \"clearDate\": \"날짜 지우기\",\n      \"dateTime\": \"날짜 시간\",\n      \"startDateTime\": \"시작 날짜 시간\",\n      \"endDateTime\": \"종료 날짜 시간\",\n      \"failedToLoadDate\": \"날짜 값을 로드하지 못했습니다\",\n      \"selectTime\": \"시간 선택\",\n      \"selectDate\": \"날짜 선택\",\n      \"visibility\": \"가시성\",\n      \"propertyType\": \"속성 유형\",\n      \"addSelectOption\": \"옵션 추가\",\n      \"typeANewOption\": \"새 옵션 입력\",\n      \"optionTitle\": \"옵션\",\n      \"addOption\": \"옵션 추가\",\n      \"editProperty\": \"속성 편집\",\n      \"newProperty\": \"새 속성\",\n      \"openRowDocument\": \"페이지로 열기\",\n      \"deleteFieldPromptMessage\": \"확실합니까? 이 속성과 모든 데이터가 삭제됩니다\",\n      \"clearFieldPromptMessage\": \"확실합니까? 이 열의 모든 셀이 비워집니다\",\n      \"newColumn\": \"새 열\",\n      \"format\": \"형식\",\n      \"reminderOnDateTooltip\": \"이 셀에는 예약된 알림이 있습니다\",\n      \"optionAlreadyExist\": \"옵션이 이미 존재합니다\"\n    },\n    \"rowPage\": {\n      \"newField\": \"새 필드 추가\",\n      \"fieldDragElementTooltip\": \"메뉴 열기\",\n      \"showHiddenFields\": {\n        \"one\": \"숨겨진 {count}개의 필드 표시\",\n        \"many\": \"숨겨진 {count}개의 필드 표시\",\n        \"other\": \"숨겨진 {count}개의 필드 표시\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"숨겨진 {count}개의 필드 숨기기\",\n        \"many\": \"숨겨진 {count}개의 필드 숨기기\",\n        \"other\": \"숨겨진 {count}개의 필드 숨기기\"\n      },\n      \"openAsFullPage\": \"전체 페이지로 열기\",\n      \"moreRowActions\": \"더 많은 행 작업\"\n    },\n    \"sort\": {\n      \"ascending\": \"오름차순\",\n      \"descending\": \"내림차순\",\n      \"by\": \"기준\",\n      \"empty\": \"활성 정렬 없음\",\n      \"cannotFindCreatableField\": \"정렬할 적절한 필드를 찾을 수 없습니다\",\n      \"deleteAllSorts\": \"모든 정렬 삭제\",\n      \"addSort\": \"정렬 추가\",\n      \"sortsActive\": \"정렬 중에는 {intention}할 수 없습니다\",\n      \"removeSorting\": \"이 보기의 모든 정렬을 제거하고 계속하시겠습니까?\",\n      \"fieldInUse\": \"이미 이 필드로 정렬 중입니다\"\n    },\n    \"row\": {\n      \"label\": \"행\",\n      \"duplicate\": \"복제\",\n      \"delete\": \"삭제\",\n      \"titlePlaceholder\": \"제목 없음\",\n      \"textPlaceholder\": \"비어 있음\",\n      \"copyProperty\": \"속성이 클립보드에 복사되었습니다\",\n      \"count\": \"개수\",\n      \"newRow\": \"새 행\",\n      \"loadMore\": \"더 로드\",\n      \"action\": \"작업\",\n      \"add\": \"아래에 추가하려면 클릭\",\n      \"drag\": \"이동하려면 드래그\",\n      \"deleteRowPrompt\": \"이 행을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.\",\n      \"deleteCardPrompt\": \"이 카드를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.\",\n      \"dragAndClick\": \"이동하려면 드래그, 메뉴를 열려면 클릭\",\n      \"insertRecordAbove\": \"위에 레코드 삽입\",\n      \"insertRecordBelow\": \"아래에 레코드 삽입\",\n      \"noContent\": \"내용 없음\",\n      \"reorderRowDescription\": \"행 순서 변경\",\n      \"createRowAboveDescription\": \"위에 행 생성\",\n      \"createRowBelowDescription\": \"아래에 행 삽입\"\n    },\n    \"selectOption\": {\n      \"create\": \"생성\",\n      \"purpleColor\": \"보라색\",\n      \"pinkColor\": \"분홍색\",\n      \"lightPinkColor\": \"연분홍색\",\n      \"orangeColor\": \"주황색\",\n      \"yellowColor\": \"노란색\",\n      \"limeColor\": \"라임색\",\n      \"greenColor\": \"녹색\",\n      \"aquaColor\": \"청록색\",\n      \"blueColor\": \"파란색\",\n      \"deleteTag\": \"태그 삭제\",\n      \"colorPanelTitle\": \"색상\",\n      \"panelTitle\": \"옵션 선택 또는 생성\",\n      \"searchOption\": \"옵션 검색\",\n      \"searchOrCreateOption\": \"옵션 검색 또는 생성\",\n      \"createNew\": \"새로 생성\",\n      \"orSelectOne\": \"또는 옵션 선택\",\n      \"typeANewOption\": \"새 옵션 입력\",\n      \"tagName\": \"태그 이름\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"작업 설명\",\n      \"addNew\": \"새 작업 추가\",\n      \"submitNewTask\": \"생성\",\n      \"hideComplete\": \"완료된 작업 숨기기\",\n      \"showComplete\": \"모든 작업 표시\"\n    },\n    \"url\": {\n      \"launch\": \"브라우저에서 링크 열기\",\n      \"copy\": \"링크를 클립보드에 복사\",\n      \"textFieldHint\": \"URL 입력\"\n    },\n    \"relation\": {\n      \"relatedDatabasePlaceLabel\": \"관련 데이터베이스\",\n      \"relatedDatabasePlaceholder\": \"없음\",\n      \"inRelatedDatabase\": \"에\",\n      \"rowSearchTextFieldPlaceholder\": \"검색\",\n      \"noDatabaseSelected\": \"선택된 데이터베이스가 없습니다. 아래 목록에서 하나를 먼저 선택하세요:\",\n      \"emptySearchResult\": \"레코드를 찾을 수 없습니다\",\n      \"linkedRowListLabel\": \"{count}개의 연결된 행\",\n      \"unlinkedRowListLabel\": \"다른 행 연결\"\n    },\n    \"menuName\": \"그리드\",\n    \"referencedGridPrefix\": \"보기\",\n    \"calculate\": \"계산\",\n    \"calculationTypeLabel\": {\n      \"none\": \"없음\",\n      \"average\": \"평균\",\n      \"max\": \"최대\",\n      \"median\": \"중앙값\",\n      \"min\": \"최소\",\n      \"sum\": \"합계\",\n      \"count\": \"개수\",\n      \"countEmpty\": \"비어 있는 개수\",\n      \"countEmptyShort\": \"비어 있음\",\n      \"countNonEmpty\": \"비어 있지 않은 개수\",\n      \"countNonEmptyShort\": \"채워짐\"\n    },\n    \"media\": {\n      \"rename\": \"이름 변경\",\n      \"download\": \"다운로드\",\n      \"expand\": \"확장\",\n      \"delete\": \"삭제\",\n      \"moreFilesHint\": \"+{}\",\n      \"addFileOrImage\": \"파일 또는 링크 추가\",\n      \"attachmentsHint\": \"{}\",\n      \"addFileMobile\": \"파일 추가\",\n      \"extraCount\": \"+{}\",\n      \"deleteFileDescription\": \"이 파일을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.\",\n      \"showFileNames\": \"파일 이름 표시\",\n      \"downloadSuccess\": \"파일이 다운로드되었습니다\",\n      \"downloadFailedToken\": \"파일을 다운로드하지 못했습니다. 사용자 토큰이 없습니다\",\n      \"setAsCover\": \"표지로 설정\",\n      \"openInBrowser\": \"브라우저에서 열기\",\n      \"embedLink\": \"파일 링크 삽입\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"문서\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"오후 01:00\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"creating\": \"생성 중...\",\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"연결할 보드 선택\",\n        \"createANewBoard\": \"새 보드 생성\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"연결할 그리드 선택\",\n        \"createANewGrid\": \"새 그리드 생성\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"연결할 캘린더 선택\",\n        \"createANewCalendar\": \"새 캘린더 생성\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"연결할 문서 선택\"\n      },\n      \"name\": {\n        \"textStyle\": \"텍스트 스타일\",\n        \"list\": \"목록\",\n        \"toggle\": \"토글\",\n        \"fileAndMedia\": \"파일 및 미디어\",\n        \"simpleTable\": \"간단한 테이블\",\n        \"visuals\": \"시각 자료\",\n        \"document\": \"문서\",\n        \"advanced\": \"고급\",\n        \"text\": \"텍스트\",\n        \"heading1\": \"헤딩 1\",\n        \"heading2\": \"헤딩 2\",\n        \"heading3\": \"헤딩 3\",\n        \"image\": \"이미지\",\n        \"bulletedList\": \"글머리 기호 목록\",\n        \"numberedList\": \"번호 매기기 목록\",\n        \"todoList\": \"할 일 목록\",\n        \"doc\": \"문서\",\n        \"linkedDoc\": \"페이지로 연결\",\n        \"grid\": \"그리드\",\n        \"linkedGrid\": \"연결된 그리드\",\n        \"kanban\": \"칸반\",\n        \"linkedKanban\": \"연결된 칸반\",\n        \"calendar\": \"캘린더\",\n        \"linkedCalendar\": \"연결된 캘린더\",\n        \"quote\": \"인용\",\n        \"divider\": \"구분선\",\n        \"table\": \"테이블\",\n        \"callout\": \"콜아웃\",\n        \"outline\": \"개요\",\n        \"mathEquation\": \"수학 방정식\",\n        \"code\": \"코드\",\n        \"toggleList\": \"토글 목록\",\n        \"toggleHeading1\": \"토글 헤딩 1\",\n        \"toggleHeading2\": \"토글 헤딩 2\",\n        \"toggleHeading3\": \"토글 헤딩 3\",\n        \"emoji\": \"이모지\",\n        \"aiWriter\": \"AI에게 물어보기\",\n        \"dateOrReminder\": \"날짜 또는 알림\",\n        \"photoGallery\": \"사진 갤러리\",\n        \"file\": \"파일\",\n        \"twoColumns\": \"2열\",\n        \"threeColumns\": \"3열\",\n        \"fourColumns\": \"4열\"\n      },\n      \"subPage\": {\n        \"name\": \"문서\",\n        \"keyword1\": \"하위 페이지\",\n        \"keyword2\": \"페이지\",\n        \"keyword3\": \"자식 페이지\",\n        \"keyword4\": \"페이지 삽입\",\n        \"keyword5\": \"페이지 포함\",\n        \"keyword6\": \"새 페이지\",\n        \"keyword7\": \"페이지 생성\",\n        \"keyword8\": \"문서\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"개요\",\n      \"codeBlock\": \"코드 블록\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"참조된 보드\",\n      \"referencedGrid\": \"참조된 그리드\",\n      \"referencedCalendar\": \"참조된 캘린더\",\n      \"referencedDocument\": \"참조된 문서\",\n      \"aiWriter\": {\n        \"userQuestion\": \"AI에게 물어보기\",\n        \"continueWriting\": \"계속 작성\",\n        \"fixSpelling\": \"맞춤법 및 문법 수정\",\n        \"improveWriting\": \"글쓰기 개선\",\n        \"summarize\": \"요약\",\n        \"explain\": \"설명\",\n        \"makeShorter\": \"짧게 만들기\",\n        \"makeLonger\": \"길게 만들기\"\n      },\n      \"autoGeneratorMenuItemName\": \"AI 작성기\",\n      \"autoGeneratorTitleName\": \"AI: AI에게 무엇이든 물어보세요...\",\n      \"autoGeneratorLearnMore\": \"자세히 알아보기\",\n      \"autoGeneratorGenerate\": \"생성\",\n      \"autoGeneratorHintText\": \"AI에게 물어보기 ...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"AI 키를 가져올 수 없습니다\",\n      \"autoGeneratorRewrite\": \"다시 작성\",\n      \"smartEdit\": \"AI에게 물어보기\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"맞춤법 및 문법 수정\",\n      \"warning\": \"⚠️ AI 응답은 부정확하거나 오해의 소지가 있을 수 있습니다.\",\n      \"smartEditSummarize\": \"요약\",\n      \"smartEditImproveWriting\": \"글쓰기 개선\",\n      \"smartEditMakeLonger\": \"길게 만들기\",\n      \"smartEditCouldNotFetchResult\": \"AI에서 결과를 가져올 수 없습니다\",\n      \"smartEditCouldNotFetchKey\": \"AI 키를 가져올 수 없습니다\",\n      \"smartEditDisabled\": \"설정에서 AI 연결\",\n      \"appflowyAIEditDisabled\": \"AI 기능을 활성화하려면 로그인하세요\",\n      \"discardResponse\": \"AI 응답을 버리시겠습니까?\",\n      \"createInlineMathEquation\": \"방정식 생성\",\n      \"fonts\": \"글꼴\",\n      \"insertDate\": \"날짜 삽입\",\n      \"emoji\": \"이모지\",\n      \"toggleList\": \"토글 목록\",\n      \"emptyToggleHeading\": \"빈 토글 h{}. 내용을 추가하려면 클릭하세요.\",\n      \"emptyToggleList\": \"빈 토글 목록. 내용을 추가하려면 클릭하세요.\",\n      \"emptyToggleHeadingWeb\": \"빈 토글 h{level}. 내용을 추가하려면 클릭하세요\",\n      \"quoteList\": \"인용 목록\",\n      \"numberedList\": \"번호 매기기 목록\",\n      \"bulletedList\": \"글머리 기호 목록\",\n      \"todoList\": \"할 일 목록\",\n      \"callout\": \"콜아웃\",\n      \"simpleTable\": {\n        \"moreActions\": {\n          \"color\": \"색상\",\n          \"align\": \"정렬\",\n          \"delete\": \"삭제\",\n          \"duplicate\": \"복제\",\n          \"insertLeft\": \"왼쪽에 삽입\",\n          \"insertRight\": \"오른쪽에 삽입\",\n          \"insertAbove\": \"위에 삽입\",\n          \"insertBelow\": \"아래에 삽입\",\n          \"headerColumn\": \"헤더 열\",\n          \"headerRow\": \"헤더 행\",\n          \"clearContents\": \"내용 지우기\",\n          \"setToPageWidth\": \"페이지 너비로 설정\",\n          \"distributeColumnsWidth\": \"열 너비 균등 분배\",\n          \"duplicateRow\": \"행 복제\",\n          \"duplicateColumn\": \"열 복제\",\n          \"textColor\": \"텍스트 색상\",\n          \"cellBackgroundColor\": \"셀 배경 색상\",\n          \"duplicateTable\": \"테이블 복제\"\n        },\n        \"clickToAddNewRow\": \"새 행을 추가하려면 클릭\",\n        \"clickToAddNewColumn\": \"새 열을 추가하려면 클릭\",\n        \"clickToAddNewRowAndColumn\": \"새 행과 열을 추가하려면 클릭\",\n        \"headerName\": {\n          \"table\": \"테이블\",\n          \"alignText\": \"텍스트 정렬\"\n        }\n      },\n      \"cover\": {\n        \"changeCover\": \"표지 변경\",\n        \"colors\": \"색상\",\n        \"images\": \"이미지\",\n        \"clearAll\": \"모두 지우기\",\n        \"abstract\": \"추상\",\n        \"addCover\": \"표지 추가\",\n        \"addLocalImage\": \"로컬 이미지 추가\",\n        \"invalidImageUrl\": \"잘못된 이미지 URL\",\n        \"failedToAddImageToGallery\": \"갤러리에 이미지를 추가하지 못했습니다\",\n        \"enterImageUrl\": \"이미지 URL 입력\",\n        \"add\": \"추가\",\n        \"back\": \"뒤로\",\n        \"saveToGallery\": \"갤러리에 저장\",\n        \"removeIcon\": \"아이콘 제거\",\n        \"removeCover\": \"표지 제거\",\n        \"pasteImageUrl\": \"이미지 URL 붙여넣기\",\n        \"or\": \"또는\",\n        \"pickFromFiles\": \"파일에서 선택\",\n        \"couldNotFetchImage\": \"이미지를 가져올 수 없습니다\",\n        \"imageSavingFailed\": \"이미지 저장 실패\",\n        \"addIcon\": \"아이콘 추가\",\n        \"changeIcon\": \"아이콘 변경\",\n        \"coverRemoveAlert\": \"삭제 후 표지에서 제거됩니다.\",\n        \"alertDialogConfirmation\": \"계속하시겠습니까?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"수학 방정식\",\n        \"addMathEquation\": \"TeX 방정식 추가\",\n        \"editMathEquation\": \"수학 방정식 편집\"\n      },\n      \"optionAction\": {\n        \"click\": \"클릭\",\n        \"toOpenMenu\": \" 메뉴 열기\",\n        \"drag\": \"드래그\",\n        \"toMove\": \" 이동\",\n        \"delete\": \"삭제\",\n        \"duplicate\": \"복제\",\n        \"turnInto\": \"변환\",\n        \"moveUp\": \"위로 이동\",\n        \"moveDown\": \"아래로 이동\",\n        \"color\": \"색상\",\n        \"align\": \"정렬\",\n        \"left\": \"왼쪽\",\n        \"center\": \"가운데\",\n        \"right\": \"오른쪽\",\n        \"defaultColor\": \"기본\",\n        \"depth\": \"깊이\",\n        \"copyLinkToBlock\": \"블록 링크 복사\"\n      },\n      \"image\": {\n        \"addAnImage\": \"이미지 추가\",\n        \"copiedToPasteBoard\": \"이미지 링크가 클립보드에 복사되었습니다\",\n        \"addAnImageDesktop\": \"이미지 추가\",\n        \"addAnImageMobile\": \"하나 이상의 이미지를 추가하려면 클릭\",\n        \"dropImageToInsert\": \"이미지를 드롭하여 삽입\",\n        \"imageUploadFailed\": \"이미지 업로드 실패\",\n        \"imageDownloadFailed\": \"이미지 다운로드 실패, 다시 시도하세요\",\n        \"imageDownloadFailedToken\": \"사용자 토큰이 없어 이미지 다운로드 실패, 다시 시도하세요\",\n        \"errorCode\": \"오류 코드\"\n      },\n      \"photoGallery\": {\n        \"name\": \"사진 갤러리\",\n        \"imageKeyword\": \"이미지\",\n        \"imageGalleryKeyword\": \"이미지 갤러리\",\n        \"photoKeyword\": \"사진\",\n        \"photoBrowserKeyword\": \"사진 브라우저\",\n        \"galleryKeyword\": \"갤러리\",\n        \"addImageTooltip\": \"이미지 추가\",\n        \"changeLayoutTooltip\": \"레이아웃 변경\",\n        \"browserLayout\": \"브라우저\",\n        \"gridLayout\": \"그리드\",\n        \"deleteBlockTooltip\": \"전체 갤러리 삭제\"\n      },\n      \"math\": {\n        \"copiedToPasteBoard\": \"수학 방정식이 클립보드에 복사되었습니다\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"링크가 클립보드에 복사되었습니다\",\n        \"convertToLink\": \"링크로 변환\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"목차를 만들려면 헤딩을 추가하세요.\",\n        \"noMatchHeadings\": \"일치하는 헤딩이 없습니다.\"\n      },\n      \"table\": {\n        \"addAfter\": \"뒤에 추가\",\n        \"addBefore\": \"앞에 추가\",\n        \"delete\": \"삭제\",\n        \"clear\": \"내용 지우기\",\n        \"duplicate\": \"복제\",\n        \"bgColor\": \"배경 색상\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"복사\",\n        \"cut\": \"잘라내기\",\n        \"paste\": \"붙여넣기\",\n        \"pasteAsPlainText\": \"서식 없이 붙여넣기\"\n      },\n      \"action\": \"작업\",\n      \"database\": {\n        \"selectDataSource\": \"데이터 소스 선택\",\n        \"noDataSource\": \"데이터 소스 없음\",\n        \"selectADataSource\": \"데이터 소스 선택\",\n        \"toContinue\": \"계속하려면\",\n        \"newDatabase\": \"새 데이터베이스\",\n        \"linkToDatabase\": \"데이터베이스로 연결\"\n      },\n      \"date\": \"날짜\",\n      \"video\": {\n        \"label\": \"비디오\",\n        \"emptyLabel\": \"비디오 추가\",\n        \"placeholder\": \"비디오 링크 붙여넣기\",\n        \"copiedToPasteBoard\": \"비디오 링크가 클립보드에 복사되었습니다\",\n        \"insertVideo\": \"비디오 추가\",\n        \"invalidVideoUrl\": \"지원되지 않는 소스 URL입니다.\",\n        \"invalidVideoUrlYouTube\": \"YouTube는 아직 지원되지 않습니다.\",\n        \"supportedFormats\": \"지원되는 형식: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264\"\n      },\n      \"file\": {\n        \"name\": \"파일\",\n        \"uploadTab\": \"업로드\",\n        \"uploadMobile\": \"파일 선택\",\n        \"uploadMobileGallery\": \"사진 갤러리에서\",\n        \"networkTab\": \"링크 삽입\",\n        \"placeholderText\": \"파일 업로드 또는 삽입\",\n        \"placeholderDragging\": \"업로드할 파일을 드롭하세요\",\n        \"dropFileToUpload\": \"업로드할 파일을 드롭하세요\",\n        \"fileUploadHint\": \"파일을 드래그 앤 드롭하거나 클릭하여 \",\n        \"fileUploadHintSuffix\": \"찾아보기\",\n        \"networkHint\": \"파일 링크 붙여넣기\",\n        \"networkUrlInvalid\": \"잘못된 URL입니다. URL을 확인하고 다시 시도하세요.\",\n        \"networkAction\": \"삽입\",\n        \"fileTooBigError\": \"파일 크기가 너무 큽니다. 10MB 미만의 파일을 업로드하세요\",\n        \"renameFile\": {\n          \"title\": \"파일 이름 변경\",\n          \"description\": \"이 파일의 새 이름을 입력하세요\",\n          \"nameEmptyError\": \"파일 이름은 비워둘 수 없습니다.\"\n        },\n        \"uploadedAt\": \"{}에 업로드됨\",\n        \"linkedAt\": \"{}에 링크 추가됨\",\n        \"failedToOpenMsg\": \"열지 못했습니다. 파일을 찾을 수 없습니다\"\n      },\n      \"subPage\": {\n        \"handlingPasteHint\": \" - (붙여넣기 처리 중)\",\n        \"errors\": {\n          \"failedDeletePage\": \"페이지 삭제 실패\",\n          \"failedCreatePage\": \"페이지 생성 실패\",\n          \"failedMovePage\": \"이 문서로 페이지 이동 실패\",\n          \"failedDuplicatePage\": \"페이지 복제 실패\",\n          \"failedDuplicateFindView\": \"페이지 복제 실패 - 원본 보기를 찾을 수 없습니다\"\n        }\n      },\n      \"cannotMoveToItsChildren\": \"자식으로 이동할 수 없습니다\"\n    },\n    \"outlineBlock\": {\n      \"placeholder\": \"목차\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"명령어를 입력하려면 '/'를 입력하세요\"\n    },\n    \"title\": {\n      \"placeholder\": \"제목 없음\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"이미지 추가하려면 클릭\",\n      \"upload\": {\n        \"label\": \"업로드\",\n        \"placeholder\": \"이미지 업로드하려면 클릭\"\n      },\n      \"url\": {\n        \"label\": \"이미지 URL\",\n        \"placeholder\": \"이미지 URL 입력\"\n      },\n      \"ai\": {\n        \"label\": \"AI로 이미지 생성\",\n        \"placeholder\": \"AI가 이미지를 생성할 프롬프트를 입력하세요\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Stability AI로 이미지 생성\",\n        \"placeholder\": \"Stability AI가 이미지를 생성할 프롬프트를 입력하세요\"\n      },\n      \"support\": \"이미지 크기 제한은 5MB입니다. 지원되는 형식: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"잘못된 이미지\",\n        \"invalidImageSize\": \"이미지 크기는 5MB 미만이어야 합니다\",\n        \"invalidImageFormat\": \"지원되지 않는 이미지 형식입니다. 지원되는 형식: JPEG, PNG, JPG, GIF, SVG, WEBP\",\n        \"invalidImageUrl\": \"잘못된 이미지 URL\",\n        \"noImage\": \"파일 또는 디렉토리가 없습니다\",\n        \"multipleImagesFailed\": \"하나 이상의 이미지 업로드 실패, 다시 시도하세요\"\n      },\n      \"embedLink\": {\n        \"label\": \"링크 삽입\",\n        \"placeholder\": \"이미지 링크를 붙여넣거나 입력하세요\"\n      },\n      \"unsplash\": {\n        \"label\": \"Unsplash\"\n      },\n      \"searchForAnImage\": \"이미지 검색\",\n      \"pleaseInputYourOpenAIKey\": \"설정 페이지에서 AI 키를 입력하세요\",\n      \"saveImageToGallery\": \"이미지 저장\",\n      \"failedToAddImageToGallery\": \"이미지 저장 실패\",\n      \"successToAddImageToGallery\": \"이미지가 사진에 저장되었습니다\",\n      \"unableToLoadImage\": \"이미지를 로드할 수 없습니다\",\n      \"maximumImageSize\": \"최대 지원 업로드 이미지 크기는 10MB입니다\",\n      \"uploadImageErrorImageSizeTooBig\": \"이미지 크기는 10MB 미만이어야 합니다\",\n      \"imageIsUploading\": \"이미지 업로드 중\",\n      \"openFullScreen\": \"전체 화면으로 열기\",\n      \"interactiveViewer\": {\n        \"toolbar\": {\n          \"previousImageTooltip\": \"이전 이미지\",\n          \"nextImageTooltip\": \"다음 이미지\",\n          \"zoomOutTooltip\": \"축소\",\n          \"zoomInTooltip\": \"확대\",\n          \"changeZoomLevelTooltip\": \"확대/축소 수준 변경\",\n          \"openLocalImage\": \"이미지 열기\",\n          \"downloadImage\": \"이미지 다운로드\",\n          \"closeViewer\": \"인터랙티브 뷰어 닫기\",\n          \"scalePercentage\": \"{}%\",\n          \"deleteImageTooltip\": \"이미지 삭제\"\n        }\n      }\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"언어\",\n        \"placeholder\": \"언어 선택\",\n        \"auto\": \"자동\"\n      },\n      \"copyTooltip\": \"복사\",\n      \"searchLanguageHint\": \"언어 검색\",\n      \"codeCopiedSnackbar\": \"코드가 클립보드에 복사되었습니다!\"\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"링크를 붙여넣거나 입력하세요\",\n      \"openInNewTab\": \"새 탭에서 열기\",\n      \"copyLink\": \"링크 복사\",\n      \"removeLink\": \"링크 제거\",\n      \"url\": {\n        \"label\": \"링크 URL\",\n        \"placeholder\": \"링크 URL 입력\"\n      },\n      \"title\": {\n        \"label\": \"링크 제목\",\n        \"placeholder\": \"링크 제목 입력\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"사람, 페이지 또는 날짜 언급...\",\n      \"page\": {\n        \"label\": \"페이지로 연결\",\n        \"tooltip\": \"페이지 열기\"\n      },\n      \"deleted\": \"삭제됨\",\n      \"deletedContent\": \"이 콘텐츠는 존재하지 않거나 삭제되었습니다\",\n      \"noAccess\": \"액세스 불가\",\n      \"deletedPage\": \"삭제된 페이지\",\n      \"trashHint\": \" - 휴지통에 있음\",\n      \"morePages\": \"더 많은 페이지\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"기본값으로 재설정\",\n      \"textSize\": \"텍스트 크기\",\n      \"h1\": \"헤딩 1\",\n      \"h2\": \"헤딩 2\",\n      \"h3\": \"헤딩 3\",\n      \"alignLeft\": \"왼쪽 정렬\",\n      \"alignRight\": \"오른쪽 정렬\",\n      \"alignCenter\": \"가운데 정렬\",\n      \"link\": \"링크\",\n      \"textAlign\": \"텍스트 정렬\",\n      \"moreOptions\": \"더 많은 옵션\",\n      \"font\": \"글꼴\",\n      \"suggestions\": \"제안\",\n      \"turnInto\": \"변환\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"블록 콘텐츠를 구문 분석할 수 없습니다\",\n      \"clickToCopyTheBlockContent\": \"블록 콘텐츠를 복사하려면 클릭\",\n      \"blockContentHasBeenCopied\": \"블록 콘텐츠가 복사되었습니다.\",\n      \"parseError\": \"{} 블록을 구문 분석하는 동안 오류가 발생했습니다.\",\n      \"copyBlockContent\": \"블록 콘텐츠 복사\"\n    },\n    \"mobilePageSelector\": {\n      \"title\": \"페이지 선택\",\n      \"failedToLoad\": \"페이지 목록을 로드하지 못했습니다\",\n      \"noPagesFound\": \"페이지를 찾을 수 없습니다\"\n    },\n    \"attachmentMenu\": {\n      \"choosePhoto\": \"사진 선택\",\n      \"takePicture\": \"사진 찍기\",\n      \"chooseFile\": \"파일 선택\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"label\": \"열\",\n      \"createNewCard\": \"새로 만들기\",\n      \"renameGroupTooltip\": \"그룹 이름 변경\",\n      \"createNewColumn\": \"새 그룹 추가\",\n      \"addToColumnTopTooltip\": \"맨 위에 새 카드 추가\",\n      \"addToColumnBottomTooltip\": \"맨 아래에 새 카드 추가\",\n      \"renameColumn\": \"이름 변경\",\n      \"hideColumn\": \"숨기기\",\n      \"newGroup\": \"새 그룹\",\n      \"deleteColumn\": \"삭제\",\n      \"deleteColumnConfirmation\": \"이 그룹과 그룹 내 모든 카드를 삭제합니다. 계속하시겠습니까?\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"숨겨진 그룹\",\n      \"collapseTooltip\": \"숨겨진 그룹 숨기기\",\n      \"expandTooltip\": \"숨겨진 그룹 보기\"\n    },\n    \"cardDetail\": \"카드 세부 정보\",\n    \"cardActions\": \"카드 작업\",\n    \"cardDuplicated\": \"카드가 복제되었습니다\",\n    \"cardDeleted\": \"카드가 삭제되었습니다\",\n    \"showOnCard\": \"카드 세부 정보에 표시\",\n    \"setting\": \"설정\",\n    \"propertyName\": \"속성 이름\",\n    \"menuName\": \"보드\",\n    \"showUngrouped\": \"그룹화되지 않은 항목 표시\",\n    \"ungroupedButtonText\": \"그룹화되지 않음\",\n    \"ungroupedButtonTooltip\": \"어떤 그룹에도 속하지 않는 카드가 포함되어 있습니다\",\n    \"ungroupedItemsTitle\": \"보드에 추가하려면 클릭\",\n    \"groupBy\": \"그룹 기준\",\n    \"groupCondition\": \"그룹 조건\",\n    \"referencedBoardPrefix\": \"보기\",\n    \"notesTooltip\": \"내부에 노트 있음\",\n    \"mobile\": {\n      \"editURL\": \"URL 편집\",\n      \"showGroup\": \"그룹 표시\",\n      \"showGroupContent\": \"이 그룹을 보드에 표시하시겠습니까?\",\n      \"failedToLoad\": \"보드 보기를 로드하지 못했습니다\"\n    },\n    \"dateCondition\": {\n      \"weekOf\": \"{} - {} 주\",\n      \"today\": \"오늘\",\n      \"yesterday\": \"어제\",\n      \"tomorrow\": \"내일\",\n      \"lastSevenDays\": \"지난 7일\",\n      \"nextSevenDays\": \"다음 7일\",\n      \"lastThirtyDays\": \"지난 30일\",\n      \"nextThirtyDays\": \"다음 30일\"\n    },\n    \"noGroup\": \"그룹화할 속성 없음\",\n    \"noGroupDesc\": \"보드 보기를 표시하려면 그룹화할 속성이 필요합니다\",\n    \"media\": {\n      \"cardText\": \"{} {}\",\n      \"fallbackName\": \"파일\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"캘린더\",\n    \"defaultNewCalendarTitle\": \"제목 없음\",\n    \"newEventButtonTooltip\": \"새 이벤트 추가\",\n    \"navigation\": {\n      \"today\": \"오늘\",\n      \"jumpToday\": \"오늘로 이동\",\n      \"previousMonth\": \"이전 달\",\n      \"nextMonth\": \"다음 달\",\n      \"views\": {\n        \"day\": \"일\",\n        \"week\": \"주\",\n        \"month\": \"월\",\n        \"year\": \"년\"\n      }\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"이벤트 없음\",\n      \"emptyBody\": \"이 날에 이벤트를 생성하려면 더하기 버튼을 누르세요.\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"주 번호 표시\",\n      \"showWeekends\": \"주말 표시\",\n      \"firstDayOfWeek\": \"주 시작일\",\n      \"layoutDateField\": \"캘린더 레이아웃 기준\",\n      \"changeLayoutDateField\": \"레이아웃 필드 변경\",\n      \"noDateTitle\": \"날짜 없음\",\n      \"noDateHint\": {\n        \"zero\": \"일정이 없는 이벤트가 여기에 표시됩니다\",\n        \"one\": \"{count}개의 일정이 없는 이벤트\",\n        \"other\": \"{count}개의 일정이 없는 이벤트\"\n      },\n      \"unscheduledEventsTitle\": \"일정이 없는 이벤트\",\n      \"clickToAdd\": \"캘린더에 추가하려면 클릭\",\n      \"name\": \"캘린더 설정\",\n      \"clickToOpen\": \"레코드를 열려면 클릭\"\n    },\n    \"referencedCalendarPrefix\": \"보기\",\n    \"quickJumpYear\": \"이동\",\n    \"duplicateEvent\": \"이벤트 복제\"\n  },\n  \"errorDialog\": {\n    \"title\": \"@:appName 오류\",\n    \"howToFixFallback\": \"불편을 드려 죄송합니다! GitHub 페이지에 오류를 설명하는 문제를 제출하세요.\",\n    \"howToFixFallbackHint1\": \"불편을 드려 죄송합니다! \",\n    \"howToFixFallbackHint2\": \" 페이지에 오류를 설명하는 문제를 제출하세요.\",\n    \"github\": \"GitHub에서 보기\"\n  },\n  \"search\": {\n    \"label\": \"검색\",\n    \"sidebarSearchIcon\": \"검색하고 페이지로 빠르게 이동\",\n    \"placeholder\": {\n      \"actions\": \"작업 검색...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"복사됨!\",\n      \"fail\": \"복사할 수 없음\"\n    }\n  },\n  \"unSupportBlock\": \"현재 버전에서는 이 블록을 지원하지 않습니다.\",\n  \"views\": {\n    \"deleteContentTitle\": \"{pageType}을(를) 삭제하시겠습니까?\",\n    \"deleteContentCaption\": \"이 {pageType}을(를) 삭제하면 휴지통에서 복원할 수 있습니다.\"\n  },\n  \"colors\": {\n    \"custom\": \"사용자 정의\",\n    \"default\": \"기본\",\n    \"red\": \"빨간색\",\n    \"orange\": \"주황색\",\n    \"yellow\": \"노란색\",\n    \"green\": \"녹색\",\n    \"blue\": \"파란색\",\n    \"purple\": \"보라색\",\n    \"pink\": \"분홍색\",\n    \"brown\": \"갈색\",\n    \"gray\": \"회색\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"이모지\",\n    \"search\": \"이모지 검색\",\n    \"noRecent\": \"최근 사용한 이모지 없음\",\n    \"noEmojiFound\": \"이모지를 찾을 수 없음\",\n    \"filter\": \"필터\",\n    \"random\": \"무작위\",\n    \"selectSkinTone\": \"피부 톤 선택\",\n    \"remove\": \"이모지 제거\",\n    \"categories\": {\n      \"smileys\": \"스마일리 및 감정\",\n      \"people\": \"사람\",\n      \"animals\": \"자연\",\n      \"food\": \"음식\",\n      \"activities\": \"활동\",\n      \"places\": \"장소\",\n      \"objects\": \"사물\",\n      \"symbols\": \"기호\",\n      \"flags\": \"깃발\",\n      \"nature\": \"자연\",\n      \"frequentlyUsed\": \"자주 사용됨\"\n    },\n    \"skinTone\": {\n      \"default\": \"기본\",\n      \"light\": \"밝은\",\n      \"mediumLight\": \"중간 밝은\",\n      \"medium\": \"중간\",\n      \"mediumDark\": \"중간 어두운\",\n      \"dark\": \"어두운\"\n    },\n    \"openSourceIconsFrom\": \"오픈 소스 아이콘 제공\"\n  },\n  \"inlineActions\": {\n    \"noResults\": \"결과 없음\",\n    \"recentPages\": \"최근 페이지\",\n    \"pageReference\": \"페이지 참조\",\n    \"docReference\": \"문서 참조\",\n    \"boardReference\": \"보드 참조\",\n    \"calReference\": \"캘린더 참조\",\n    \"gridReference\": \"그리드 참조\",\n    \"date\": \"날짜\",\n    \"reminder\": {\n      \"groupTitle\": \"알림\",\n      \"shortKeyword\": \"알림\"\n    },\n    \"createPage\": \"\\\"{}\\\" 하위 페이지 생성\"\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"설정에서 날짜 및 시간 형식 변경\",\n    \"dateFormat\": \"날짜 형식\",\n    \"includeTime\": \"시간 포함\",\n    \"isRange\": \"종료 날짜\",\n    \"timeFormat\": \"시간 형식\",\n    \"clearDate\": \"날짜 지우기\",\n    \"reminderLabel\": \"알림\",\n    \"selectReminder\": \"알림 선택\",\n    \"reminderOptions\": {\n      \"none\": \"없음\",\n      \"atTimeOfEvent\": \"이벤트 시간\",\n      \"fiveMinsBefore\": \"5분 전\",\n      \"tenMinsBefore\": \"10분 전\",\n      \"fifteenMinsBefore\": \"15분 전\",\n      \"thirtyMinsBefore\": \"30분 전\",\n      \"oneHourBefore\": \"1시간 전\",\n      \"twoHoursBefore\": \"2시간 전\",\n      \"onDayOfEvent\": \"이벤트 당일\",\n      \"oneDayBefore\": \"1일 전\",\n      \"twoDaysBefore\": \"2일 전\",\n      \"oneWeekBefore\": \"1주일 전\",\n      \"custom\": \"사용자 정의\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"어제\",\n    \"today\": \"오늘\",\n    \"tomorrow\": \"내일\",\n    \"oneWeek\": \"1주일\"\n  },\n  \"notificationHub\": {\n    \"title\": \"알림\",\n    \"mobile\": {\n      \"title\": \"업데이트\"\n    },\n    \"emptyTitle\": \"모두 확인했습니다!\",\n    \"emptyBody\": \"대기 중인 알림이나 작업이 없습니다. 평온을 즐기세요.\",\n    \"tabs\": {\n      \"inbox\": \"받은 편지함\",\n      \"upcoming\": \"다가오는\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"모두 읽음으로 표시\",\n      \"showAll\": \"모두\",\n      \"showUnreads\": \"읽지 않음\"\n    },\n    \"filters\": {\n      \"ascending\": \"오름차순\",\n      \"descending\": \"내림차순\",\n      \"groupByDate\": \"날짜별 그룹\",\n      \"showUnreadsOnly\": \"읽지 않은 항목만 표시\",\n      \"resetToDefault\": \"기본값으로 재설정\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"알림\",\n    \"message\": \"잊기 전에 확인하세요!\",\n    \"tooltipDelete\": \"삭제\",\n    \"tooltipMarkRead\": \"읽음으로 표시\",\n    \"tooltipMarkUnread\": \"읽지 않음으로 표시\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"찾기\",\n    \"previousMatch\": \"이전 일치 항목\",\n    \"nextMatch\": \"다음 일치 항목\",\n    \"close\": \"닫기\",\n    \"replace\": \"교체\",\n    \"replaceAll\": \"모두 교체\",\n    \"noResult\": \"결과 없음\",\n    \"caseSensitive\": \"대소문자 구분\",\n    \"searchMore\": \"더 많은 결과를 찾으려면 검색\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"죄송합니다\",\n    \"loadingViewError\": \"이 보기를 로드하는 데 문제가 있습니다. 인터넷 연결을 확인하고 앱을 새로 고침하세요. 문제가 계속되면 팀에 문의하세요.\",\n    \"syncError\": \"다른 장치에서 데이터가 동기화되지 않음\",\n    \"syncErrorHint\": \"마지막으로 편집한 장치에서 이 페이지를 다시 열고 현재 장치에서 다시 열어보세요.\",\n    \"clickToCopy\": \"오류 코드를 복사하려면 클릭\"\n  },\n  \"editor\": {\n    \"bold\": \"굵게\",\n    \"bulletedList\": \"글머리 기호 목록\",\n    \"bulletedListShortForm\": \"글머리 기호\",\n    \"checkbox\": \"체크박스\",\n    \"embedCode\": \"코드 삽입\",\n    \"heading1\": \"H1\",\n    \"heading2\": \"H2\",\n    \"heading3\": \"H3\",\n    \"highlight\": \"강조\",\n    \"color\": \"색상\",\n    \"image\": \"이미지\",\n    \"date\": \"날짜\",\n    \"page\": \"페이지\",\n    \"italic\": \"기울임꼴\",\n    \"link\": \"링크\",\n    \"numberedList\": \"번호 매기기 목록\",\n    \"numberedListShortForm\": \"번호 매기기\",\n    \"toggleHeading1ShortForm\": \"토글 h1\",\n    \"toggleHeading2ShortForm\": \"토글 h2\",\n    \"toggleHeading3ShortForm\": \"토글 h3\",\n    \"quote\": \"인용\",\n    \"strikethrough\": \"취소선\",\n    \"text\": \"텍스트\",\n    \"underline\": \"밑줄\",\n    \"fontColorDefault\": \"기본\",\n    \"fontColorGray\": \"회색\",\n    \"fontColorBrown\": \"갈색\",\n    \"fontColorOrange\": \"주황색\",\n    \"fontColorYellow\": \"노란색\",\n    \"fontColorGreen\": \"녹색\",\n    \"fontColorBlue\": \"파란색\",\n    \"fontColorPurple\": \"보라색\",\n    \"fontColorPink\": \"분홍색\",\n    \"fontColorRed\": \"빨간색\",\n    \"backgroundColorDefault\": \"기본 배경\",\n    \"backgroundColorGray\": \"회색 배경\",\n    \"backgroundColorBrown\": \"갈색 배경\",\n    \"backgroundColorOrange\": \"주황색 배경\",\n    \"backgroundColorYellow\": \"노란색 배경\",\n    \"backgroundColorGreen\": \"녹색 배경\",\n    \"backgroundColorBlue\": \"파란색 배경\",\n    \"backgroundColorPurple\": \"보라색 배경\",\n    \"backgroundColorPink\": \"분홍색 배경\",\n    \"backgroundColorRed\": \"빨간색 배경\",\n    \"backgroundColorLime\": \"라임색 배경\",\n    \"backgroundColorAqua\": \"청록색 배경\",\n    \"done\": \"완료\",\n    \"cancel\": \"취소\",\n    \"tint1\": \"색조 1\",\n    \"tint2\": \"색조 2\",\n    \"tint3\": \"색조 3\",\n    \"tint4\": \"색조 4\",\n    \"tint5\": \"색조 5\",\n    \"tint6\": \"색조 6\",\n    \"tint7\": \"색조 7\",\n    \"tint8\": \"색조 8\",\n    \"tint9\": \"색조 9\",\n    \"lightLightTint1\": \"보라색\",\n    \"lightLightTint2\": \"분홍색\",\n    \"lightLightTint3\": \"연분홍색\",\n    \"lightLightTint4\": \"주황색\",\n    \"lightLightTint5\": \"노란색\",\n    \"lightLightTint6\": \"라임색\",\n    \"lightLightTint7\": \"녹색\",\n    \"lightLightTint8\": \"청록색\",\n    \"lightLightTint9\": \"파란색\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"헤딩 1\",\n    \"mobileHeading2\": \"헤딩 2\",\n    \"mobileHeading3\": \"헤딩 3\",\n    \"mobileHeading4\": \"헤딩 4\",\n    \"mobileHeading5\": \"헤딩 5\",\n    \"mobileHeading6\": \"헤딩 6\",\n    \"textColor\": \"텍스트 색상\",\n    \"backgroundColor\": \"배경 색상\",\n    \"addYourLink\": \"링크 추가\",\n    \"openLink\": \"링크 열기\",\n    \"copyLink\": \"링크 복사\",\n    \"removeLink\": \"링크 제거\",\n    \"editLink\": \"링크 편집\",\n    \"linkText\": \"텍스트\",\n    \"linkTextHint\": \"텍스트를 입력하세요\",\n    \"linkAddressHint\": \"URL을 입력하세요\",\n    \"highlightColor\": \"강조 색상\",\n    \"clearHighlightColor\": \"강조 색상 지우기\",\n    \"customColor\": \"사용자 정의 색상\",\n    \"hexValue\": \"16진수 값\",\n    \"opacity\": \"불투명도\",\n    \"resetToDefaultColor\": \"기본 색상으로 재설정\",\n    \"ltr\": \"LTR\",\n    \"rtl\": \"RTL\",\n    \"auto\": \"자동\",\n    \"cut\": \"잘라내기\",\n    \"copy\": \"복사\",\n    \"paste\": \"붙여넣기\",\n    \"find\": \"찾기\",\n    \"select\": \"선택\",\n    \"selectAll\": \"모두 선택\",\n    \"previousMatch\": \"이전 일치 항목\",\n    \"nextMatch\": \"다음 일치 항목\",\n    \"closeFind\": \"닫기\",\n    \"replace\": \"교체\",\n    \"replaceAll\": \"모두 교체\",\n    \"regex\": \"정규식\",\n    \"caseSensitive\": \"대소문자 구분\",\n    \"uploadImage\": \"이미지 업로드\",\n    \"urlImage\": \"URL 이미지\",\n    \"incorrectLink\": \"잘못된 링크\",\n    \"upload\": \"업로드\",\n    \"chooseImage\": \"이미지 선택\",\n    \"loading\": \"로드 중\",\n    \"imageLoadFailed\": \"이미지 로드 실패\",\n    \"divider\": \"구분선\",\n    \"table\": \"테이블\",\n    \"colAddBefore\": \"앞에 추가\",\n    \"rowAddBefore\": \"앞에 추가\",\n    \"colAddAfter\": \"뒤에 추가\",\n    \"rowAddAfter\": \"뒤에 추가\",\n    \"colRemove\": \"제거\",\n    \"rowRemove\": \"제거\",\n    \"colDuplicate\": \"복제\",\n    \"rowDuplicate\": \"복제\",\n    \"colClear\": \"내용 지우기\",\n    \"rowClear\": \"내용 지우기\",\n    \"slashPlaceHolder\": \"'/'를 입력하여 블록을 삽입하거나 입력 시작\",\n    \"typeSomething\": \"무언가 입력...\",\n    \"toggleListShortForm\": \"토글\",\n    \"quoteListShortForm\": \"인용\",\n    \"mathEquationShortForm\": \"수식\",\n    \"codeBlockShortForm\": \"코드\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"즐겨찾기 페이지 없음\",\n    \"noFavoriteHintText\": \"페이지를 왼쪽으로 스와이프하여 즐겨찾기에 추가하세요\",\n    \"removeFromSidebar\": \"사이드바에서 제거\",\n    \"addToSidebar\": \"사이드바에 고정\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"/를 입력하여 블록을 삽입하거나 입력 시작\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"할 일\",\n    \"bulletList\": \"목록\",\n    \"numberList\": \"목록\",\n    \"quote\": \"인용\",\n    \"heading\": \"헤딩 {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"페이지 아이콘\",\n    \"language\": \"언어\",\n    \"font\": \"글꼴\",\n    \"actions\": \"작업\",\n    \"date\": \"날짜\",\n    \"addField\": \"필드 추가\",\n    \"userIcon\": \"사용자 아이콘\"\n  },\n  \"noLogFiles\": \"로그 파일이 없습니다\",\n  \"newSettings\": {\n    \"myAccount\": {\n      \"title\": \"내 계정\",\n      \"subtitle\": \"프로필을 사용자 정의하고, 계정 보안을 관리하고, AI 키를 열거나 계정에 로그인하세요.\",\n      \"profileLabel\": \"계정 이름 및 프로필 이미지\",\n      \"profileNamePlaceholder\": \"이름 입력\",\n      \"accountSecurity\": \"계정 보안\",\n      \"2FA\": \"2단계 인증\",\n      \"aiKeys\": \"AI 키\",\n      \"accountLogin\": \"계정 로그인\",\n      \"updateNameError\": \"이름 업데이트 실패\",\n      \"updateIconError\": \"아이콘 업데이트 실패\",\n      \"aboutAppFlowy\": \"@:appName 정보\",\n      \"deleteAccount\": {\n        \"title\": \"계정 삭제\",\n        \"subtitle\": \"계정과 모든 데이터를 영구적으로 삭제합니다.\",\n        \"description\": \"계정을 영구적으로 삭제하고 모든 작업 공간에서 액세스를 제거합니다.\",\n        \"deleteMyAccount\": \"내 계정 삭제\",\n        \"dialogTitle\": \"계정 삭제\",\n        \"dialogContent1\": \"계정을 영구적으로 삭제하시겠습니까?\",\n        \"dialogContent2\": \"이 작업은 되돌릴 수 없으며, 모든 작업 공간에서 액세스를 제거하고, 개인 작업 공간을 포함한 전체 계정을 삭제하며, 모든 공유 작업 공간에서 제거됩니다.\",\n        \"confirmHint1\": \"\\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\"를 입력하여 확인하세요.\",\n        \"confirmHint2\": \"이 작업은 되돌릴 수 없으며, 계정과 모든 관련 데이터를 영구적으로 삭제합니다.\",\n        \"confirmHint3\": \"내 계정 삭제\",\n        \"checkToConfirmError\": \"삭제를 확인하려면 확인란을 선택해야 합니다\",\n        \"failedToGetCurrentUser\": \"현재 사용자 이메일을 가져오지 못했습니다\",\n        \"confirmTextValidationFailed\": \"확인 텍스트가 \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\"와 일치하지 않습니다\",\n        \"deleteAccountSuccess\": \"계정이 성공적으로 삭제되었습니다\"\n      }\n    },\n    \"workplace\": {\n      \"name\": \"작업 공간\",\n      \"title\": \"작업 공간 설정\",\n      \"subtitle\": \"작업 공간의 외관, 테마, 글꼴, 텍스트 레이아웃, 날짜, 시간 및 언어를 사용자 정의합니다.\",\n      \"workplaceName\": \"작업 공간 이름\",\n      \"workplaceNamePlaceholder\": \"작업 공간 이름 입력\",\n      \"workplaceIcon\": \"작업 공간 아이콘\",\n      \"workplaceIconSubtitle\": \"작업 공간에 대한 이미지를 업로드하거나 이모지를 사용하세요. 아이콘은 사이드바와 알림에 표시됩니다.\",\n      \"renameError\": \"작업 공간 이름 변경 실패\",\n      \"updateIconError\": \"아이콘 업데이트 실패\",\n      \"chooseAnIcon\": \"아이콘 선택\",\n      \"appearance\": {\n        \"name\": \"외관\",\n        \"themeMode\": {\n          \"auto\": \"자동\",\n          \"light\": \"라이트\",\n          \"dark\": \"다크\"\n        },\n        \"language\": \"언어\"\n      }\n    },\n    \"syncState\": {\n      \"syncing\": \"동기화 중\",\n      \"synced\": \"동기화됨\",\n      \"noNetworkConnected\": \"네트워크에 연결되지 않음\"\n    }\n  },\n  \"pageStyle\": {\n    \"title\": \"페이지 스타일\",\n    \"layout\": \"레이아웃\",\n    \"coverImage\": \"표지 이미지\",\n    \"pageIcon\": \"페이지 아이콘\",\n    \"colors\": \"색상\",\n    \"gradient\": \"그라데이션\",\n    \"backgroundImage\": \"배경 이미지\",\n    \"presets\": \"프리셋\",\n    \"photo\": \"사진\",\n    \"unsplash\": \"Unsplash\",\n    \"pageCover\": \"페이지 표지\",\n    \"none\": \"없음\",\n    \"openSettings\": \"설정 열기\",\n    \"photoPermissionTitle\": \"@:appName가 사진 라이브러리에 접근하려고 합니다\",\n    \"photoPermissionDescription\": \"@:appName가 문서에 이미지를 추가할 수 있도록 사진에 접근해야 합니다\",\n    \"cameraPermissionTitle\": \"@:appName가 카메라에 접근하려고 합니다\",\n    \"cameraPermissionDescription\": \"카메라에서 문서에 이미지를 추가하려면 @:appName가 카메라에 액세스할 수 있어야 합니다.\",\n    \"doNotAllow\": \"허용하지 않음\",\n    \"image\": \"이미지\"\n  },\n  \"commandPalette\": {\n    \"placeholder\": \"검색하거나 질문하세요...\",\n    \"bestMatches\": \"최고의 일치 항목\",\n    \"recentHistory\": \"최근 기록\",\n    \"navigateHint\": \"탐색하려면\",\n    \"loadingTooltip\": \"결과를 찾고 있습니다...\",\n    \"betaLabel\": \"베타\",\n    \"betaTooltip\": \"현재 문서의 페이지 및 콘텐츠 검색만 지원합니다\",\n    \"fromTrashHint\": \"휴지통에서\",\n    \"noResultsHint\": \"찾고 있는 항목을 찾지 못했습니다. 다른 용어로 검색해 보세요.\",\n    \"clearSearchTooltip\": \"검색 필드 지우기\"\n  },\n  \"space\": {\n    \"delete\": \"삭제\",\n    \"deleteConfirmation\": \"삭제: \",\n    \"deleteConfirmationDescription\": \"이 공간 내의 모든 페이지가 삭제되어 휴지통으로 이동되며, 게시한 모든 페이지가 게시 취소됩니다.\",\n    \"rename\": \"공간 이름 변경\",\n    \"changeIcon\": \"아이콘 변경\",\n    \"manage\": \"공간 관리\",\n    \"addNewSpace\": \"새 공간 생성\",\n    \"collapseAllSubPages\": \"모든 하위 페이지 접기\",\n    \"createNewSpace\": \"새 공간 생성\",\n    \"createSpaceDescription\": \"작업을 더 잘 조직하기 위해 여러 공용 및 비공개 공간을 생성하세요.\",\n    \"spaceName\": \"공간 이름\",\n    \"spaceNamePlaceholder\": \"예: 마케팅, 엔지니어링, 인사\",\n    \"permission\": \"공간 권한\",\n    \"publicPermission\": \"공용\",\n    \"publicPermissionDescription\": \"전체 액세스 권한이 있는 모든 작업 공간 멤버\",\n    \"privatePermission\": \"비공개\",\n    \"privatePermissionDescription\": \"이 공간에 접근할 수 있는 사람은 본인뿐입니다\",\n    \"spaceIconBackground\": \"배경 색상\",\n    \"spaceIcon\": \"아이콘\",\n    \"dangerZone\": \"위험 구역\",\n    \"unableToDeleteLastSpace\": \"마지막 공간을 삭제할 수 없습니다\",\n    \"unableToDeleteSpaceNotCreatedByYou\": \"다른 사람이 생성한 공간을 삭제할 수 없습니다\",\n    \"enableSpacesForYourWorkspace\": \"작업 공간에 공간을 활성화하세요\",\n    \"title\": \"공간\",\n    \"defaultSpaceName\": \"일반\",\n    \"upgradeSpaceTitle\": \"공간 활성화\",\n    \"upgradeSpaceDescription\": \"작업 공간을 더 잘 조직하기 위해 여러 공용 및 비공개 공간을 생성하세요.\",\n    \"upgrade\": \"업데이트\",\n    \"upgradeYourSpace\": \"여러 공간 생성\",\n    \"quicklySwitch\": \"다음 공간으로 빠르게 전환\",\n    \"duplicate\": \"공간 복제\",\n    \"movePageToSpace\": \"페이지를 공간으로 이동\",\n    \"cannotMovePageToDatabase\": \"페이지를 데이터베이스로 이동할 수 없습니다\",\n    \"switchSpace\": \"공간 전환\",\n    \"spaceNameCannotBeEmpty\": \"공간 이름은 비워둘 수 없습니다\",\n    \"success\": {\n      \"deleteSpace\": \"공간이 성공적으로 삭제되었습니다\",\n      \"renameSpace\": \"공간 이름이 성공적으로 변경되었습니다\",\n      \"duplicateSpace\": \"공간이 성공적으로 복제되었습니다\",\n      \"updateSpace\": \"공간이 성공적으로 업데이트되었습니다\"\n    },\n    \"error\": {\n      \"deleteSpace\": \"공간 삭제 실패\",\n      \"renameSpace\": \"공간 이름 변경 실패\",\n      \"duplicateSpace\": \"공간 복제 실패\",\n      \"updateSpace\": \"공간 업데이트 실패\"\n    },\n    \"createSpace\": \"공간 생성\",\n    \"manageSpace\": \"공간 관리\",\n    \"renameSpace\": \"공간 이름 변경\",\n    \"mSpaceIconColor\": \"공간 아이콘 색상\",\n    \"mSpaceIcon\": \"공간 아이콘\"\n  },\n  \"publish\": {\n    \"hasNotBeenPublished\": \"이 페이지는 아직 게시되지 않았습니다\",\n    \"spaceHasNotBeenPublished\": \"아직 공간 게시를 지원하지 않습니다\",\n    \"reportPage\": \"페이지 신고\",\n    \"databaseHasNotBeenPublished\": \"데이터베이스 게시를 아직 지원하지 않습니다.\",\n    \"createdWith\": \"제작\",\n    \"downloadApp\": \"AppFlowy 다운로드\",\n    \"copy\": {\n      \"codeBlock\": \"코드 블록의 내용이 클립보드에 복사되었습니다\",\n      \"imageBlock\": \"이미지 링크가 클립보드에 복사되었습니다\",\n      \"mathBlock\": \"수학 방정식이 클립보드에 복사되었습니다\",\n      \"fileBlock\": \"파일 링크가 클립보드에 복사되었습니다\"\n    },\n    \"containsPublishedPage\": \"이 페이지에는 하나 이상의 게시된 페이지가 포함되어 있습니다. 계속하면 게시가 취소됩니다. 삭제를 진행하시겠습니까?\",\n    \"publishSuccessfully\": \"성공적으로 게시되었습니다\",\n    \"unpublishSuccessfully\": \"성공적으로 게시 취소되었습니다\",\n    \"publishFailed\": \"게시 실패\",\n    \"unpublishFailed\": \"게시 취소 실패\",\n    \"noAccessToVisit\": \"이 페이지에 접근할 수 없습니다...\",\n    \"createWithAppFlowy\": \"AppFlowy로 웹사이트 만들기\",\n    \"fastWithAI\": \"AI로 빠르고 쉽게.\",\n    \"tryItNow\": \"지금 시도해보세요\",\n    \"onlyGridViewCanBePublished\": \"그리드 보기만 게시할 수 있습니다\",\n    \"database\": {\n      \"zero\": \"선택한 {} 보기 게시\",\n      \"one\": \"선택한 {} 보기 게시\",\n      \"many\": \"선택한 {} 보기 게시\",\n      \"other\": \"선택한 {} 보기 게시\"\n    },\n    \"mustSelectPrimaryDatabase\": \"기본 보기를 선택해야 합니다\",\n    \"noDatabaseSelected\": \"선택된 데이터베이스가 없습니다. 최소 하나의 데이터베이스를 선택하세요.\",\n    \"unableToDeselectPrimaryDatabase\": \"기본 보기를 선택 해제할 수 없습니다\",\n    \"saveThisPage\": \"이 템플릿으로 시작\",\n    \"duplicateTitle\": \"추가할 위치 선택\",\n    \"selectWorkspace\": \"작업 공간 선택\",\n    \"addTo\": \"추가\",\n    \"duplicateSuccessfully\": \"작업 공간에 추가되었습니다\",\n    \"duplicateSuccessfullyDescription\": \"AppFlowy가 설치되어 있지 않습니까? '다운로드'를 클릭하면 다운로드가 자동으로 시작됩니다.\",\n    \"downloadIt\": \"다운로드\",\n    \"openApp\": \"앱에서 열기\",\n    \"duplicateFailed\": \"복제 실패\",\n    \"membersCount\": {\n      \"zero\": \"멤버 없음\",\n      \"one\": \"1명의 멤버\",\n      \"many\": \"{count}명의 멤버\",\n      \"other\": \"{count}명의 멤버\"\n    },\n    \"useThisTemplate\": \"템플릿 사용\"\n  },\n  \"web\": {\n    \"continue\": \"계속\",\n    \"or\": \"또는\",\n    \"continueWithGoogle\": \"Google로 계속\",\n    \"continueWithGithub\": \"GitHub로 계속\",\n    \"continueWithDiscord\": \"Discord로 계속\",\n    \"continueWithApple\": \"Apple로 계속\",\n    \"moreOptions\": \"더 많은 옵션\",\n    \"collapse\": \"접기\",\n    \"signInAgreement\": \"\\\"계속\\\"을 클릭하면 AppFlowy의\",\n    \"and\": \"및\",\n    \"termOfUse\": \"이용 약관\",\n    \"privacyPolicy\": \"개인정보 보호정책\",\n    \"signInError\": \"로그인 오류\",\n    \"login\": \"가입 또는 로그인\",\n    \"fileBlock\": {\n      \"uploadedAt\": \"{time}에 업로드됨\",\n      \"linkedAt\": \"{time}에 링크 추가됨\",\n      \"empty\": \"파일 업로드 또는 삽입\",\n      \"uploadFailed\": \"업로드 실패, 다시 시도하세요\",\n      \"retry\": \"다시 시도\"\n    },\n    \"importNotion\": \"Notion에서 가져오기\",\n    \"import\": \"가져오기\",\n    \"importSuccess\": \"성공적으로 업로드되었습니다\",\n    \"importSuccessMessage\": \"가져오기가 완료되면 알림을 받게 됩니다. 이후 사이드바에서 가져온 페이지를 확인할 수 있습니다.\",\n    \"importFailed\": \"가져오기 실패, 파일 형식을 확인하세요\",\n    \"dropNotionFile\": \"Notion zip 파일을 여기에 드롭하여 업로드하거나 클릭하여 찾아보기\",\n    \"error\": {\n      \"pageNameIsEmpty\": \"페이지 이름이 비어 있습니다. 다른 이름을 시도하세요\"\n    }\n  },\n  \"globalComment\": {\n    \"comments\": \"댓글\",\n    \"addComment\": \"댓글 추가\",\n    \"reactedBy\": \"반응한 사람\",\n    \"addReaction\": \"반응 추가\",\n    \"reactedByMore\": \"및 {count}명\",\n    \"showSeconds\": {\n      \"one\": \"1초 전\",\n      \"other\": \"{count}초 전\",\n      \"zero\": \"방금\",\n      \"many\": \"{count}초 전\"\n    },\n    \"showMinutes\": {\n      \"one\": \"1분 전\",\n      \"other\": \"{count}분 전\",\n      \"many\": \"{count}분 전\"\n    },\n    \"showHours\": {\n      \"one\": \"1시간 전\",\n      \"other\": \"{count}시간 전\",\n      \"many\": \"{count}시간 전\"\n    },\n    \"showDays\": {\n      \"one\": \"1일 전\",\n      \"other\": \"{count}일 전\",\n      \"many\": \"{count}일 전\"\n    },\n    \"showMonths\": {\n      \"one\": \"1개월 전\",\n      \"other\": \"{count}개월 전\",\n      \"many\": \"{count}개월 전\"\n    },\n    \"showYears\": {\n      \"one\": \"1년 전\",\n      \"other\": \"{count}년 전\",\n      \"many\": \"{count}년 전\"\n    },\n    \"reply\": \"답글\",\n    \"deleteComment\": \"댓글 삭제\",\n    \"youAreNotOwner\": \"이 댓글의 소유자가 아닙니다\",\n    \"confirmDeleteDescription\": \"이 댓글을 삭제하시겠습니까?\",\n    \"hasBeenDeleted\": \"삭제됨\",\n    \"replyingTo\": \"답글 대상\",\n    \"noAccessDeleteComment\": \"이 댓글을 삭제할 수 없습니다\",\n    \"collapse\": \"접기\",\n    \"readMore\": \"더 읽기\",\n    \"failedToAddComment\": \"댓글 추가 실패\",\n    \"commentAddedSuccessfully\": \"댓글이 성공적으로 추가되었습니다.\",\n    \"commentAddedSuccessTip\": \"댓글을 추가하거나 답글을 달았습니다. 최신 댓글을 보려면 상단으로 이동하시겠습니까?\"\n  },\n  \"template\": {\n    \"asTemplate\": \"템플릿으로 저장\",\n    \"name\": \"템플릿 이름\",\n    \"description\": \"템플릿 설명\",\n    \"about\": \"템플릿 정보\",\n    \"deleteFromTemplate\": \"템플릿에서 삭제\",\n    \"preview\": \"템플릿 미리보기\",\n    \"categories\": \"템플릿 카테고리\",\n    \"isNewTemplate\": \"새 템플릿에 고정\",\n    \"featured\": \"추천에 고정\",\n    \"relatedTemplates\": \"관련 템플릿\",\n    \"requiredField\": \"{field}은(는) 필수 항목입니다\",\n    \"addCategory\": \"\\\"{category}\\\" 추가\",\n    \"addNewCategory\": \"새 카테고리 추가\",\n    \"addNewCreator\": \"새 제작자 추가\",\n    \"deleteCategory\": \"카테고리 삭제\",\n    \"editCategory\": \"카테고리 편집\",\n    \"editCreator\": \"제작자 편집\",\n    \"category\": {\n      \"name\": \"카테고리 이름\",\n      \"icon\": \"카테고리 아이콘\",\n      \"bgColor\": \"카테고리 배경 색상\",\n      \"priority\": \"카테고리 우선순위\",\n      \"desc\": \"카테고리 설명\",\n      \"type\": \"카테고리 유형\",\n      \"icons\": \"카테고리 아이콘\",\n      \"colors\": \"카테고리 색상\",\n      \"byUseCase\": \"사용 사례별\",\n      \"byFeature\": \"기능별\",\n      \"deleteCategory\": \"카테고리 삭제\",\n      \"deleteCategoryDescription\": \"이 카테고리를 삭제하시겠습니까?\",\n      \"typeToSearch\": \"카테고리 검색...\"\n    },\n    \"creator\": {\n      \"label\": \"템플릿 제작자\",\n      \"name\": \"제작자 이름\",\n      \"avatar\": \"제작자 아바타\",\n      \"accountLinks\": \"제작자 계정 링크\",\n      \"uploadAvatar\": \"아바타 업로드\",\n      \"deleteCreator\": \"제작자 삭제\",\n      \"deleteCreatorDescription\": \"이 제작자를 삭제하시겠습니까?\",\n      \"typeToSearch\": \"제작자 검색...\"\n    },\n    \"uploadSuccess\": \"템플릿이 성공적으로 업로드되었습니다\",\n    \"uploadSuccessDescription\": \"템플릿이 성공적으로 업로드되었습니다. 이제 템플릿 갤러리에서 확인할 수 있습니다.\",\n    \"viewTemplate\": \"템플릿 보기\",\n    \"deleteTemplate\": \"템플릿 삭제\",\n    \"deleteSuccess\": \"템플릿이 성공적으로 삭제되었습니다\",\n    \"deleteTemplateDescription\": \"현재 페이지나 게시 상태에는 영향을 미치지 않습니다. 이 템플릿을 삭제하시겠습니까?\",\n    \"addRelatedTemplate\": \"관련 템플릿 추가\",\n    \"removeRelatedTemplate\": \"관련 템플릿 제거\",\n    \"uploadAvatar\": \"아바타 업로드\",\n    \"searchInCategory\": \"{category}에서 검색\",\n    \"label\": \"템플릿\"\n  },\n  \"fileDropzone\": {\n    \"dropFile\": \"업로드하려면 파일을 클릭하거나 드래그하세요\",\n    \"uploading\": \"업로드 중...\",\n    \"uploadFailed\": \"업로드 실패\",\n    \"uploadSuccess\": \"업로드 성공\",\n    \"uploadSuccessDescription\": \"파일이 성공적으로 업로드되었습니다\",\n    \"uploadFailedDescription\": \"파일 업로드 실패\",\n    \"uploadingDescription\": \"파일이 업로드 중입니다\"\n  },\n  \"gallery\": {\n    \"preview\": \"전체 화면으로 열기\",\n    \"copy\": \"복사\",\n    \"download\": \"다운로드\",\n    \"prev\": \"이전\",\n    \"next\": \"다음\",\n    \"resetZoom\": \"확대/축소 재설정\",\n    \"zoomIn\": \"확대\",\n    \"zoomOut\": \"축소\"\n  },\n  \"invitation\": {\n    \"join\": \"참여\",\n    \"on\": \"에\",\n    \"invitedBy\": \"초대자\",\n    \"membersCount\": {\n      \"zero\": \"{count}명의 멤버\",\n      \"one\": \"{count}명의 멤버\",\n      \"many\": \"{count}명의 멤버\",\n      \"other\": \"{count}명의 멤버\"\n    },\n    \"tip\": \"아래 연락처 정보로 이 작업 공간에 참여하도록 초대되었습니다. 정보가 잘못된 경우 관리자에게 연락하여 초대를 다시 보내달라고 요청하세요.\",\n    \"joinWorkspace\": \"작업 공간 참여\",\n    \"success\": \"작업 공간에 성공적으로 참여했습니다\",\n    \"successMessage\": \"이제 작업 공간 내의 모든 페이지와 작업 공간에 접근할 수 있습니다.\",\n    \"openWorkspace\": \"AppFlowy 열기\",\n    \"alreadyAccepted\": \"이미 초대를 수락했습니다\",\n    \"errorModal\": {\n      \"title\": \"문제가 발생했습니다\",\n      \"description\": \"현재 계정 {email}이(가) 이 작업 공간에 접근할 수 없을 수 있습니다. 올바른 계정으로 로그인하거나 작업 공간 소유자에게 도움을 요청하세요.\",\n      \"contactOwner\": \"소유자에게 연락\",\n      \"close\": \"홈으로 돌아가기\",\n      \"changeAccount\": \"계정 변경\"\n    }\n  },\n  \"requestAccess\": {\n    \"title\": \"이 페이지에 접근할 수 없습니다\",\n    \"subtitle\": \"이 페이지의 소유자에게 접근을 요청할 수 있습니다. 승인되면 페이지를 볼 수 있습니다.\",\n    \"requestAccess\": \"접근 요청\",\n    \"backToHome\": \"홈으로 돌아가기\",\n    \"tip\": \"현재 <link/>로 로그인 중입니다.\",\n    \"mightBe\": \"다른 계정으로 <login/>해야 할 수 있습니다.\",\n    \"successful\": \"요청이 성공적으로 전송되었습니다\",\n    \"successfulMessage\": \"소유자가 요청을 승인하면 알림을 받게 됩니다.\",\n    \"requestError\": \"접근 요청 실패\",\n    \"repeatRequestError\": \"이미 이 페이지에 접근을 요청했습니다\"\n  },\n  \"approveAccess\": {\n    \"title\": \"작업 공간 참여 요청 승인\",\n    \"requestSummary\": \"<user/>이(가) <workspace/>에 참여하고 <page/>에 접근하려고 요청합니다\",\n    \"upgrade\": \"업그레이드\",\n    \"downloadApp\": \"AppFlowy 다운로드\",\n    \"approveButton\": \"승인\",\n    \"approveSuccess\": \"성공적으로 승인되었습니다\",\n    \"approveError\": \"승인 실패, 작업 공간 플랜 한도를 초과하지 않았는지 확인하세요\",\n    \"getRequestInfoError\": \"요청 정보를 가져오지 못했습니다\",\n    \"memberCount\": {\n      \"zero\": \"멤버 없음\",\n      \"one\": \"1명의 멤버\",\n      \"many\": \"{count}명의 멤버\",\n      \"other\": \"{count}명의 멤버\"\n    },\n    \"alreadyProTitle\": \"작업 공간 플랜 한도에 도달했습니다\",\n    \"alreadyProMessage\": \"<email/>에 연락하여 더 많은 멤버를 잠금 해제하도록 요청하세요\",\n    \"repeatApproveError\": \"이미 이 요청을 승인했습니다\",\n    \"ensurePlanLimit\": \"작업 공간 플랜 한도를 초과하지 않았는지 확인하세요. 한도를 초과한 경우 작업 공간 플랜을 <upgrade/>하거나 <download/>를 고려하세요.\",\n    \"requestToJoin\": \"참여 요청\",\n    \"asMember\": \"멤버로\"\n  },\n  \"upgradePlanModal\": {\n    \"title\": \"Pro로 업그레이드\",\n    \"message\": \"{name}이(가) 무료 멤버 한도에 도달했습니다. 더 많은 멤버를 초대하려면 Pro 플랜으로 업그레이드하세요.\",\n    \"upgradeSteps\": \"AppFlowy에서 플랜을 업그레이드하는 방법:\",\n    \"step1\": \"1. 설정으로 이동\",\n    \"step2\": \"2. '플랜' 클릭\",\n    \"step3\": \"3. '플랜 변경' 선택\",\n    \"appNote\": \"참고: \",\n    \"actionButton\": \"업그레이드\",\n    \"downloadLink\": \"앱 다운로드\",\n    \"laterButton\": \"나중에\",\n    \"refreshNote\": \"성공적으로 업그레이드한 후 새 기능을 활성화하려면 <refresh/>를 클릭하세요.\",\n    \"refresh\": \"여기\"\n  },\n  \"breadcrumbs\": {\n    \"label\": \"탐색 경로\"\n  },\n  \"time\": {\n    \"justNow\": \"방금\",\n    \"seconds\": {\n      \"one\": \"1초\",\n      \"other\": \"{count}초\"\n    },\n    \"minutes\": {\n      \"one\": \"1분\",\n      \"other\": \"{count}분\"\n    },\n    \"hours\": {\n      \"one\": \"1시간\",\n      \"other\": \"{count}시간\"\n    },\n    \"days\": {\n      \"one\": \"1일\",\n      \"other\": \"{count}일\"\n    },\n    \"weeks\": {\n      \"one\": \"1주일\",\n      \"other\": \"{count}주일\"\n    },\n    \"months\": {\n      \"one\": \"1개월\",\n      \"other\": \"{count}개월\"\n    },\n    \"years\": {\n      \"one\": \"1년\",\n      \"other\": \"{count}년\"\n    },\n    \"ago\": \"전\",\n    \"yesterday\": \"어제\",\n    \"today\": \"오늘\"\n  },\n  \"members\": {\n    \"zero\": \"멤버 없음\",\n    \"one\": \"1명의 멤버\",\n    \"many\": \"{count}명의 멤버\",\n    \"other\": \"{count}명의 멤버\"\n  },\n  \"tabMenu\": {\n    \"close\": \"닫기\",\n    \"closeDisabledHint\": \"고정된 탭은 닫을 수 없습니다. 먼저 고정을 해제하세요\",\n    \"closeOthers\": \"다른 탭 닫기\",\n    \"closeOthersHint\": \"이 탭을 제외한 모든 고정되지 않은 탭을 닫습니다\",\n    \"closeOthersDisabledHint\": \"모든 탭이 고정되어 있어 닫을 탭을 찾을 수 없습니다\",\n    \"favorite\": \"즐겨찾기\",\n    \"unfavorite\": \"즐겨찾기 해제\",\n    \"favoriteDisabledHint\": \"이 보기를 즐겨찾기에 추가할 수 없습니다\",\n    \"pinTab\": \"고정\",\n    \"unpinTab\": \"고정 해제\"\n  },\n  \"openFileMessage\": {\n    \"success\": \"파일이 성공적으로 열렸습니다\",\n    \"fileNotFound\": \"파일을 찾을 수 없습니다\",\n    \"noAppToOpenFile\": \"이 파일을 열 수 있는 앱이 없습니다\",\n    \"permissionDenied\": \"이 파일을 열 수 있는 권한이 없습니다\",\n    \"unknownError\": \"파일 열기 실패\"\n  },\n  \"inviteMember\": {\n    \"requestInviteMembers\": \"작업 공간에 초대\",\n    \"inviteFailedMemberLimit\": \"멤버 한도에 도달했습니다. \",\n    \"upgrade\": \"업그레이드\",\n    \"addEmail\": \"email@example.com, email2@example.com...\",\n    \"requestInvites\": \"초대 보내기\",\n    \"inviteAlready\": \"이미 이 이메일을 초대했습니다: {email}\",\n    \"inviteSuccess\": \"초대가 성공적으로 전송되었습니다\",\n    \"description\": \"아래에 이메일을 쉼표로 구분하여 입력하세요. 멤버 수에 따라 요금이 부과됩니다.\",\n    \"emails\": \"이메일\"\n  },\n  \"quickNote\": {\n    \"label\": \"빠른 노트\",\n    \"quickNotes\": \"빠른 노트\",\n    \"search\": \"빠른 노트 검색\",\n    \"collapseFullView\": \"전체 보기 축소\",\n    \"expandFullView\": \"전체 보기 확장\",\n    \"createFailed\": \"빠른 노트 생성 실패\",\n    \"quickNotesEmpty\": \"빠른 노트 없음\",\n    \"emptyNote\": \"빈 노트\",\n    \"deleteNotePrompt\": \"선택한 노트가 영구적으로 삭제됩니다. 삭제하시겠습니까?\",\n    \"addNote\": \"새 노트\",\n    \"noAdditionalText\": \"추가 텍스트 없음\"\n  },\n  \"subscribe\": {\n    \"upgradePlanTitle\": \"플랜 비교 및 선택\",\n    \"yearly\": \"연간\",\n    \"save\": \"{discount}% 절약\",\n    \"monthly\": \"월별\",\n    \"priceIn\": \"가격 \",\n    \"free\": \"무료\",\n    \"pro\": \"Pro\",\n    \"freeDescription\": \"모든 것을 정리하기 위한 최대 2명의 개인용\",\n    \"proDescription\": \"프로젝트 및 팀 지식을 관리하기 위한 소규모 팀용\",\n    \"proDuration\": {\n      \"monthly\": \"월별 청구되는 멤버당 월별\",\n      \"yearly\": \"연간 청구되는 멤버당 월별\"\n    },\n    \"cancel\": \"다운그레이드\",\n    \"changePlan\": \"Pro 플랜으로 업그레이드\",\n    \"everythingInFree\": \"무료 플랜의 모든 기능 +\",\n    \"currentPlan\": \"현재\",\n    \"freeDuration\": \"영원히\",\n    \"freePoints\": {\n      \"first\": \"최대 2명의 협업 작업 공간\",\n      \"second\": \"무제한 페이지 및 블록\",\n      \"three\": \"5 GB 저장 공간\",\n      \"four\": \"지능형 검색\",\n      \"five\": \"20 AI 응답\",\n      \"six\": \"모바일 앱\",\n      \"seven\": \"실시간 협업\"\n    },\n    \"proPoints\": {\n      \"first\": \"무제한 저장 공간\",\n      \"second\": \"최대 10명의 작업 공간 멤버\",\n      \"three\": \"무제한 AI 응답\",\n      \"four\": \"무제한 파일 업로드\",\n      \"five\": \"맞춤 네임스페이스\"\n    },\n    \"cancelPlan\": {\n      \"title\": \"떠나셔서 아쉽습니다\",\n      \"success\": \"구독이 성공적으로 취소되었습니다\",\n      \"description\": \"@:appName을 개선하는 데 도움이 되도록 피드백을 듣고 싶습니다. 몇 가지 질문에 답변해 주세요.\",\n      \"commonOther\": \"기타\",\n      \"otherHint\": \"여기에 답변을 작성하세요\",\n      \"questionOne\": {\n        \"question\": \"@:appName Pro 구독을 취소한 이유는 무엇입니까?\",\n        \"answerOne\": \"비용이 너무 높음\",\n        \"answerTwo\": \"기능이 기대에 미치지 못함\",\n        \"answerThree\": \"더 나은 대안을 찾음\",\n        \"answerFour\": \"비용을 정당화할 만큼 충분히 사용하지 않음\",\n        \"answerFive\": \"서비스 문제 또는 기술적 어려움\"\n      },\n      \"questionTwo\": {\n        \"question\": \"미래에 @:appName Pro를 다시 구독할 가능성은 얼마나 됩니까?\",\n        \"answerOne\": \"매우 가능성이 높음\",\n        \"answerTwo\": \"어느 정도 가능성이 있음\",\n        \"answerThree\": \"잘 모르겠음\",\n        \"answerFour\": \"가능성이 낮음\",\n        \"answerFive\": \"매우 가능성이 낮음\"\n      },\n      \"questionThree\": {\n        \"question\": \"구독 기간 동안 가장 가치 있게 여긴 Pro 기능은 무엇입니까?\",\n        \"answerOne\": \"다중 사용자 협업\",\n        \"answerTwo\": \"더 긴 시간 버전 기록\",\n        \"answerThree\": \"무제한 AI 응답\",\n        \"answerFour\": \"로컬 AI 모델 액세스\"\n      },\n      \"questionFour\": {\n        \"question\": \"@:appName에 대한 전반적인 경험을 어떻게 설명하시겠습니까?\",\n        \"answerOne\": \"훌륭함\",\n        \"answerTwo\": \"좋음\",\n        \"answerThree\": \"보통\",\n        \"answerFour\": \"평균 이하\",\n        \"answerFive\": \"불만족\"\n      }\n    }\n  },\n  \"ai\": {\n    \"contentPolicyViolation\": \"민감한 콘텐츠로 인해 이미지 생성에 실패했습니다. 입력을 다시 작성하고 다시 시도하세요\",\n    \"textLimitReachedDescription\": \"작업 공간의 무료 AI 응답이 부족합니다. Pro 플랜으로 업그레이드하거나 AI 애드온을 구매하여 무제한 응답을 잠금 해제하세요\",\n    \"imageLimitReachedDescription\": \"무료 AI 이미지 할당량을 모두 사용했습니다. Pro 플랜으로 업그레이드하거나 AI 애드온을 구매하여 무제한 응답을 잠금 해제하세요\",\n    \"limitReachedAction\": {\n      \"textDescription\": \"작업 공간의 무료 AI 응답이 부족합니다. 더 많은 응답을 받으려면 \",\n      \"imageDescription\": \"무료 AI 이미지 할당량을 모두 사용했습니다. \",\n      \"upgrade\": \"업그레이드\",\n      \"toThe\": \" \",\n      \"proPlan\": \"Pro 플랜\",\n      \"orPurchaseAn\": \" 또는 \",\n      \"aiAddon\": \"AI 애드온을 구매하세요\"\n    },\n    \"editing\": \"편집 중\",\n    \"analyzing\": \"분석 중\",\n    \"continueWritingEmptyDocumentTitle\": \"계속 작성 오류\",\n    \"continueWritingEmptyDocumentDescription\": \"문서의 내용을 확장하는 데 문제가 있습니다. 간단한 소개를 작성하면 나머지는 우리가 처리할 수 있습니다!\"\n  },\n  \"autoUpdate\": {\n    \"criticalUpdateTitle\": \"계속하려면 업데이트가 필요합니다\",\n    \"criticalUpdateDescription\": \"경험을 향상시키기 위해 개선 사항을 추가했습니다! 앱을 계속 사용하려면 {currentVersion}에서 {newVersion}으로 업데이트하세요.\",\n    \"criticalUpdateButton\": \"업데이트\",\n    \"bannerUpdateTitle\": \"새 버전 사용 가능!\",\n    \"bannerUpdateDescription\": \"최신 기능 및 수정 사항을 받으세요. 지금 설치하려면 \\\"업데이트\\\"를 클릭하세요\",\n    \"bannerUpdateButton\": \"업데이트\",\n    \"settingsUpdateTitle\": \"새 버전 ({newVersion}) 사용 가능!\",\n    \"settingsUpdateDescription\": \"현재 버전: {currentVersion} (공식 빌드) → {newVersion}\",\n    \"settingsUpdateButton\": \"업데이트\",\n    \"settingsUpdateWhatsNew\": \"새로운 기능\"\n  },\n  \"lockPage\": {\n    \"lockPage\": \"잠금\",\n    \"reLockPage\": \"다시 잠금\",\n    \"lockTooltip\": \"실수로 편집하지 않도록 페이지가 잠겨 있습니다. 잠금 해제하려면 클릭하세요.\",\n    \"pageLockedToast\": \"페이지가 잠겼습니다. 누군가 잠금을 해제할 때까지 편집이 비활성화됩니다.\",\n    \"lockedOperationTooltip\": \"실수로 편집하지 않도록 페이지가 잠겨 있습니다.\"\n  },\n  \"suggestion\": {\n    \"accept\": \"수락\",\n    \"keep\": \"유지\",\n    \"discard\": \"버리기\",\n    \"close\": \"닫기\",\n    \"tryAgain\": \"다시 시도\",\n    \"rewrite\": \"다시 작성\",\n    \"insertBelow\": \"아래에 삽입\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/mr-IN.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"मी\",\n  \"welcomeText\": \"@:appName मध्ये आ पले स्वागत आहे.\",\n  \"welcomeTo\": \"मध्ये आ पले स्वागत आ हे\",\n  \"githubStarText\": \"GitHub वर स्टार करा\",\n  \"subscribeNewsletterText\": \"वृत्तपत्राची सदस्यता घ्या\",\n  \"letsGoButtonText\": \"क्विक स्टार्ट\",\n  \"title\": \"Title\",\n  \"youCanAlso\": \"तुम्ही देखील\",\n  \"and\": \"आ णि\",\n  \"failedToOpenUrl\": \"URL उघडण्यात अयशस्वी: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"खाली जोडण्यासाठी क्लिक करा\",\n    \"addAboveCmd\": \"Alt+click\",\n    \"addAboveMacCmd\": \"Option+click\",\n    \"addAboveTooltip\": \"वर जोडण्यासाठी\",\n    \"dragTooltip\": \"Drag to move\",\n    \"openMenuTooltip\": \"मेनू उघडण्यासाठी क्लिक करा\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"साइन अप\",\n    \"title\": \"साइन अप to @:appName\",\n    \"getStartedText\": \"सुरुवात करा\",\n    \"emptyPasswordError\": \"पासवर्ड रिकामा असू शकत नाही\",\n    \"repeatPasswordEmptyError\": \"Repeat पासवर्ड रिकामा असू शकत नाही\",\n    \"unmatchedPasswordError\": \"पुन्हा लिहिलेला पासवर्ड मूळ पासवर्डशी जुळत नाही\",\n    \"alreadyHaveAnAccount\": \"आधीच खाते आहे?\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Password\",\n    \"repeatPasswordHint\": \"पासवर्ड पुन्हा लिहा\",\n    \"signUpWith\": \"यामध्ये साइन अप करा:\"\n  },\n  \"signIn\": {\n  \"loginTitle\": \"@:appName मध्ये लॉगिन करा\",\n  \"loginButtonText\": \"लॉगिन\",\n  \"loginStartWithAnonymous\": \"अनामिक सत्रासह पुढे जा\",\n  \"continueAnonymousUser\": \"अनामिक सत्रासह पुढे जा\",\n  \"anonymous\": \"अनामिक\",\n  \"buttonText\": \"साइन इन\",\n  \"signingInText\": \"साइन इन होत आहे...\",\n  \"forgotPassword\": \"पासवर्ड विसरलात?\",\n  \"emailHint\": \"ईमेल\",\n  \"passwordHint\": \"पासवर्ड\",\n  \"dontHaveAnAccount\": \"तुमचं खाते नाही?\",\n  \"createAccount\": \"खाते तयार करा\",\n  \"repeatPasswordEmptyError\": \"पुन्हा पासवर्ड रिकामा असू शकत नाही\",\n  \"unmatchedPasswordError\": \"पुन्हा लिहिलेला पासवर्ड मूळ पासवर्डशी जुळत नाही\",\n  \"syncPromptMessage\": \"डेटा सिंक होण्यास थोडा वेळ लागू शकतो. कृपया हे पृष्ठ बंद करू नका\",\n  \"or\": \"किंवा\",\n  \"signInWithGoogle\": \"Google सह पुढे जा\",\n  \"signInWithGithub\": \"GitHub सह पुढे जा\",\n  \"signInWithDiscord\": \"Discord सह पुढे जा\",\n  \"signInWithApple\": \"Apple सह पुढे जा\",\n  \"continueAnotherWay\": \"इतर पर्यायांनी पुढे जा\",\n  \"signUpWithGoogle\": \"Google सह साइन अप करा\",\n  \"signUpWithGithub\": \"GitHub सह साइन अप करा\",\n  \"signUpWithDiscord\": \"Discord सह साइन अप करा\",\n  \"signInWith\": \"यासह पुढे जा:\",\n  \"signInWithEmail\": \"ईमेलसह पुढे जा\",\n  \"signInWithMagicLink\": \"पुढे जा\",\n  \"signUpWithMagicLink\": \"Magic Link सह साइन अप करा\",\n  \"pleaseInputYourEmail\": \"कृपया तुमचा ईमेल पत्ता टाका\",\n  \"settings\": \"सेटिंग्ज\",\n  \"magicLinkSent\": \"Magic Link पाठवण्यात आली आहे!\",\n  \"invalidEmail\": \"कृपया वैध ईमेल पत्ता टाका\",\n  \"alreadyHaveAnAccount\": \"आधीच खाते आहे?\",\n  \"logIn\": \"लॉगिन\",\n  \"generalError\": \"काहीतरी चुकलं. कृपया नंतर प्रयत्न करा\",\n  \"limitRateError\": \"सुरक्षेच्या कारणास्तव, तुम्ही दर ६० सेकंदांतून एकदाच Magic Link मागवू शकता\",\n  \"magicLinkSentDescription\": \"तुमच्या ईमेलवर Magic Link पाठवण्यात आली आहे. लॉगिन पूर्ण करण्यासाठी लिंकवर क्लिक करा. ही लिंक ५ मिनिटांत कालबाह्य होईल.\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"तुमचे workspace निवडा\",\n    \"defaultName\": \"माझे Workspace\",\n    \"create\": \"नवीन workspace तयार करा\",\n    \"new\": \"नवीन workspace\",\n    \"importFromNotion\": \"Notion मधून आयात करा\",\n    \"learnMore\": \"अधिक जाणून घ्या\",\n    \"reset\": \"workspace रीसेट करा\",\n    \"renameWorkspace\": \"workspace चे नाव बदला\",\n    \"workspaceNameCannotBeEmpty\": \"workspace चे नाव रिकामे असू शकत नाही\",\n    \"resetWorkspacePrompt\": \"workspace रीसेट केल्यास त्यातील सर्व पृष्ठे आणि डेटा हटवले जातील. तुम्हाला workspace रीसेट करायचे आहे का? पर्यायी म्हणून तुम्ही सपोर्ट टीमशी संपर्क करू शकता.\",\n    \"hint\": \"workspace\",\n    \"notFoundError\": \"workspace सापडले नाही\",\n    \"failedToLoad\": \"काहीतरी चूक झाली! workspace लोड होण्यात अयशस्वी. कृपया @:appName चे कोणतेही उघडे instance बंद करा आणि पुन्हा प्रयत्न करा.\",\n    \"errorActions\": {\n      \"reportIssue\": \"समस्या नोंदवा\",\n      \"reportIssueOnGithub\": \"Github वर समस्या नोंदवा\",\n      \"exportLogFiles\": \"लॉग फाइल्स निर्यात करा\",\n      \"reachOut\": \"Discord वर संपर्क करा\"\n    },\n    \"menuTitle\": \"कार्यक्षेत्रे\",\n  \"deleteWorkspaceHintText\": \"तुम्हाला हे कार्यक्षेत्र हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही आणि तुम्ही प्रकाशित केलेली कोणतीही पृष्ठे अप्रकाशित होतील.\",\n  \"createSuccess\": \"कार्यक्षेत्र यशस्वीरित्या तयार झाले\",\n  \"createFailed\": \"कार्यक्षेत्र तयार करण्यात अयशस्वी\",\n  \"createLimitExceeded\": \"तुम्ही तुमच्या खात्यासाठी परवानगी दिलेल्या कार्यक्षेत्र मर्यादेपर्यंत पोहोचलात. अधिक कार्यक्षेत्रासाठी GitHub वर विनंती करा.\",\n  \"deleteSuccess\": \"कार्यक्षेत्र यशस्वीरित्या हटवले गेले\",\n  \"deleteFailed\": \"कार्यक्षेत्र हटवण्यात अयशस्वी\",\n  \"openSuccess\": \"कार्यक्षेत्र यशस्वीरित्या उघडले\",\n  \"openFailed\": \"कार्यक्षेत्र उघडण्यात अयशस्वी\",\n  \"renameSuccess\": \"कार्यक्षेत्राचे नाव यशस्वीरित्या बदलले\",\n  \"renameFailed\": \"कार्यक्षेत्राचे नाव बदलण्यात अयशस्वी\",\n  \"updateIconSuccess\": \"कार्यक्षेत्राचे चिन्ह यशस्वीरित्या अद्यतनित केले\",\n  \"updateIconFailed\": \"कार्यक्षेत्राचे चिन्ह अद्यतनित करण्यात अयशस्वी\",\n  \"cannotDeleteTheOnlyWorkspace\": \"फक्त एकच कार्यक्षेत्र असल्यास ते हटवता येत नाही\",\n  \"fetchWorkspacesFailed\": \"कार्यक्षेत्रे मिळवण्यात अयशस्वी\",\n  \"leaveCurrentWorkspace\": \"कार्यक्षेत्र सोडा\",\n  \"leaveCurrentWorkspacePrompt\": \"तुम्हाला हे कार्यक्षेत्र सोडायचे आहे का?\"\n  },\n  \"shareAction\": {\n  \"buttonText\": \"शेअर करा\",\n  \"workInProgress\": \"लवकरच येत आहे\",\n  \"markdown\": \"Markdown\",\n  \"html\": \"HTML\",\n  \"clipboard\": \"क्लिपबोर्डवर कॉपी करा\",\n  \"csv\": \"CSV\",\n  \"copyLink\": \"लिंक कॉपी करा\",\n  \"publishToTheWeb\": \"वेबवर प्रकाशित करा\",\n  \"publishToTheWebHint\": \"AppFlowy सह वेबसाइट तयार करा\",\n  \"publish\": \"प्रकाशित करा\",\n  \"unPublish\": \"अप्रकाशित करा\",\n  \"visitSite\": \"साइटला भेट द्या\",\n  \"exportAsTab\": \"या स्वरूपात निर्यात करा\",\n  \"publishTab\": \"प्रकाशित करा\",\n  \"shareTab\": \"शेअर करा\",\n  \"publishOnAppFlowy\": \"AppFlowy वर प्रकाशित करा\",\n  \"shareTabTitle\": \"सहकार्य करण्यासाठी आमंत्रित करा\",\n  \"shareTabDescription\": \"कोणासही सहज सहकार्य करण्यासाठी\",\n  \"copyLinkSuccess\": \"लिंक क्लिपबोर्डवर कॉपी केली\",\n  \"copyShareLink\": \"शेअर लिंक कॉपी करा\",\n  \"copyLinkFailed\": \"लिंक क्लिपबोर्डवर कॉपी करण्यात अयशस्वी\",\n  \"copyLinkToBlockSuccess\": \"ब्लॉकची लिंक क्लिपबोर्डवर कॉपी केली\",\n  \"copyLinkToBlockFailed\": \"ब्लॉकची लिंक क्लिपबोर्डवर कॉपी करण्यात अयशस्वी\",\n  \"manageAllSites\": \"सर्व साइट्स व्यवस्थापित करा\",\n  \"updatePathName\": \"पथाचे नाव अपडेट करा\"\n  },\n  \"moreAction\": {\n    \"small\": \"लहान\",\n    \"medium\": \"मध्यम\",\n    \"large\": \"मोठा\",\n    \"fontSize\": \"फॉन्ट आकार\",\n    \"import\": \"Import\",\n    \"moreOptions\": \"अधिक पर्याय\",\n    \"wordCount\": \"शब्द संख्या: {}\",\n    \"charCount\": \"अक्षर संख्या: {}\",\n    \"createdAt\": \"निर्मिती: {}\",\n    \"deleteView\": \"हटवा\",\n    \"duplicateView\": \"प्रत बनवा\",\n    \"wordCountLabel\": \"शब्द संख्या: \",\n    \"charCountLabel\": \"अक्षर संख्या: \",\n    \"createdAtLabel\": \"निर्मिती: \",\n    \"syncedAtLabel\": \"सिंक केले: \",\n    \"saveAsNewPage\": \"संदेश पृष्ठात जोडा\",\n    \"saveAsNewPageDisabled\": \"उपलब्ध संदेश नाहीत\"\n  },\n  \"importPanel\": {\n  \"textAndMarkdown\": \"मजकूर आणि Markdown\",\n  \"documentFromV010\": \"v0.1.0 पासून दस्तऐवज\",\n  \"databaseFromV010\": \"v0.1.0 पासून डेटाबेस\",\n  \"notionZip\": \"Notion निर्यात केलेली Zip फाईल\",\n  \"csv\": \"CSV\",\n  \"database\": \"डेटाबेस\"\n  },\n  \"emojiIconPicker\": {\n  \"iconUploader\": {\n    \"placeholderLeft\": \"फाईल ड्रॅग आणि ड्रॉप करा, क्लिक करा \",\n    \"placeholderUpload\": \"अपलोड\",\n    \"placeholderRight\": \", किंवा इमेज लिंक पेस्ट करा.\",\n    \"dropToUpload\": \"अपलोडसाठी फाईल ड्रॉप करा\",\n    \"change\": \"बदला\"\n   }\n  },\n  \"disclosureAction\": {\n  \"rename\": \"नाव बदला\",\n  \"delete\": \"हटवा\",\n  \"duplicate\": \"प्रत बनवा\",\n  \"unfavorite\": \"आवडतीतून काढा\",\n  \"favorite\": \"आवडतीत जोडा\",\n  \"openNewTab\": \"नवीन टॅबमध्ये उघडा\",\n  \"moveTo\": \"या ठिकाणी हलवा\",\n  \"addToFavorites\": \"आवडतीत जोडा\",\n  \"copyLink\": \"लिंक कॉपी करा\",\n  \"changeIcon\": \"आयकॉन बदला\",\n  \"collapseAllPages\": \"सर्व उपपृष्ठे संकुचित करा\",\n  \"movePageTo\": \"पृष्ठ हलवा\",\n  \"move\": \"हलवा\",\n  \"lockPage\": \"पृष्ठ लॉक करा\"\n  },\n  \"blankPageTitle\": \"रिक्त पृष्ठ\",\n  \"newPageText\": \"नवीन पृष्ठ\",\n  \"newDocumentText\": \"नवीन दस्तऐवज\",\n  \"newGridText\": \"नवीन ग्रिड\",\n  \"newCalendarText\": \"नवीन कॅलेंडर\",\n  \"newBoardText\": \"नवीन बोर्ड\",\n  \"chat\": {\n    \"newChat\": \"AI गप्पा\",\n    \"inputMessageHint\": \"@:appName AI ला विचार करा\",\n    \"inputLocalAIMessageHint\": \"@:appName लोकल AI ला विचार करा\",\n    \"unsupportedCloudPrompt\": \"ही सुविधा फक्त @:appName Cloud वापरताना उपलब्ध आहे\",\n    \"relatedQuestion\": \"सूचवलेले\",\n    \"serverUnavailable\": \"कनेक्शन गमावले. कृपया तुमचे इंटरनेट तपासा आणि पुन्हा प्रयत्न करा\",\n    \"aiServerUnavailable\": \"AI सेवा सध्या अनुपलब्ध आहे. कृपया नंतर पुन्हा प्रयत्न करा.\",\n    \"retry\": \"पुन्हा प्रयत्न करा\",\n    \"clickToRetry\": \"पुन्हा प्रयत्न करण्यासाठी क्लिक करा\",\n    \"regenerateAnswer\": \"उत्तर पुन्हा तयार करा\",\n    \"question1\": \"Kanban वापरून कामे कशी व्यवस्थापित करायची\",\n    \"question2\": \"GTD पद्धत समजावून सांगा\",\n    \"question3\": \"Rust का वापरावा\",\n    \"question4\": \"माझ्या स्वयंपाकघरात असलेल्या वस्तूंनी रेसिपी\",\n    \"question5\": \"माझ्या पृष्ठासाठी एक चित्र तयार करा\",\n    \"question6\": \"या आठवड्याची माझी कामांची यादी तयार करा\",\n    \"aiMistakePrompt\": \"AI चुकू शकतो. महत्त्वाची माहिती तपासा.\",\n    \"chatWithFilePrompt\": \"तुम्हाला फाइलसोबत गप्पा मारायच्या आहेत का?\",\n    \"indexFileSuccess\": \"फाईल यशस्वीरित्या अनुक्रमित केली गेली\",\n    \"inputActionNoPages\": \"काहीही पृष्ठे सापडली नाहीत\",\n    \"referenceSource\": {\n    \"zero\": \"0 स्रोत सापडले\",\n    \"one\": \"{count} स्रोत सापडला\",\n    \"other\": \"{count} स्रोत सापडले\"\n    }\n  },\n    \"clickToMention\": \"पृष्ठाचा उल्लेख करा\",\n    \"uploadFile\": \"PDFs, मजकूर किंवा markdown फाइल्स जोडा\",\n    \"questionDetail\": \"नमस्कार {}! मी तुम्हाला कशी मदत करू शकतो?\",\n    \"indexingFile\": \"{} अनुक्रमित करत आहे\",\n    \"generatingResponse\": \"उत्तर तयार होत आहे\",\n    \"selectSources\": \"स्रोत निवडा\",\n    \"currentPage\": \"सध्याचे पृष्ठ\",\n    \"sourcesLimitReached\": \"तुम्ही फक्त ३ मुख्य दस्तऐवज आणि त्यांची उपदस्तऐवज निवडू शकता\",\n    \"sourceUnsupported\": \"सध्या डेटाबेससह चॅटिंगसाठी आम्ही समर्थन करत नाही\",\n    \"regenerate\": \"पुन्हा प्रयत्न करा\",\n    \"addToPageButton\": \"संदेश पृष्ठावर जोडा\",\n    \"addToPageTitle\": \"या पृष्ठात संदेश जोडा...\",\n    \"addToNewPage\": \"नवीन पृष्ठ तयार करा\",\n    \"addToNewPageName\": \"\\\"{}\\\" मधून काढलेले संदेश\",\n    \"addToNewPageSuccessToast\": \"संदेश जोडण्यात आला\",\n    \"openPagePreviewFailedToast\": \"पृष्ठ उघडण्यात अयशस्वी\",\n    \"changeFormat\": {\n      \"actionButton\": \"फॉरमॅट बदला\",\n      \"confirmButton\": \"या फॉरमॅटसह पुन्हा तयार करा\",\n      \"textOnly\": \"मजकूर\",\n      \"imageOnly\": \"फक्त प्रतिमा\",\n      \"textAndImage\": \"मजकूर आणि प्रतिमा\",\n      \"text\": \"परिच्छेद\",\n      \"bullet\": \"बुलेट यादी\",\n      \"number\": \"क्रमांकित यादी\",\n      \"table\": \"सारणी\",\n      \"blankDescription\": \"उत्तराचे फॉरमॅट ठरवा\",\n      \"defaultDescription\": \"स्वयंचलित उत्तर फॉरमॅट\",\n      \"textWithImageDescription\": \"@:chat.changeFormat.text प्रतिमेसह\",\n      \"numberWithImageDescription\": \"@:chat.changeFormat.number प्रतिमेसह\",\n      \"bulletWithImageDescription\": \"@:chat.changeFormat.bullet प्रतिमेसह\",\n  \"   tableWithImageDescription\": \"@:chat.changeFormat.table प्रतिमेसह\"\n  },\n    \"switchModel\": {\n    \"label\": \"मॉडेल बदला\",\n    \"localModel\": \"स्थानिक मॉडेल\",\n    \"cloudModel\": \"क्लाऊड मॉडेल\",\n    \"autoModel\": \"स्वयंचलित\"\n  },\n    \"selectBanner\": {\n    \"saveButton\": \"… मध्ये जोडा\",\n    \"selectMessages\": \"संदेश निवडा\",\n    \"nSelected\": \"{} निवडले गेले\",\n    \"allSelected\": \"सर्व निवडले गेले\"\n  },\n    \"stopTooltip\": \"उत्पन्न करणे थांबवा\",\n    \"trash\": {\n    \"text\": \"कचरा\",\n    \"restoreAll\": \"सर्व पुनर्संचयित करा\",\n    \"restore\": \"पुनर्संचयित करा\",\n    \"deleteAll\": \"सर्व हटवा\",\n    \"pageHeader\": {\n    \"fileName\": \"फाईलचे नाव\",\n    \"lastModified\": \"शेवटचा बदल\",\n    \"created\": \"निर्मिती\"\n    }\n  },\n    \"confirmDeleteAll\": {\n    \"title\": \"कचरापेटीतील सर्व पृष्ठे\",\n    \"caption\": \"तुम्हाला कचरापेटीतील सर्व काही हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही.\"\n  },\n    \"confirmRestoreAll\": {\n    \"title\": \"कचरापेटीतील सर्व पृष्ठे पुनर्संचयित करा\",\n    \"caption\": \"ही कृती पूर्ववत केली जाऊ शकत नाही.\"\n  },\n    \"restorePage\": {\n    \"title\": \"पुनर्संचयित करा: {}\",\n    \"caption\": \"तुम्हाला हे पृष्ठ पुनर्संचयित करायचे आहे का?\"\n  },\n    \"mobile\": {\n    \"actions\": \"कचरा क्रिया\",\n    \"empty\": \"कचरापेटीत कोणतीही पृष्ठे किंवा जागा नाहीत\",\n    \"emptyDescription\": \"जे आवश्यक नाही ते कचरापेटीत हलवा.\",\n    \"isDeleted\": \"हटवले गेले आहे\",\n    \"isRestored\": \"पुनर्संचयित केले गेले आहे\"\n  },\n    \"confirmDeleteTitle\": \"तुम्हाला हे पृष्ठ कायमचे हटवायचे आहे का?\",\n    \"deletePagePrompt\": {\n    \"text\": \"हे पृष्ठ कचरापेटीत आहे\",\n    \"restore\": \"पृष्ठ पुनर्संचयित करा\",\n    \"deletePermanent\": \"कायमचे हटवा\",\n    \"deletePermanentDescription\": \"तुम्हाला हे पृष्ठ कायमचे हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही.\"\n  },\n  \"dialogCreatePageNameHint\": \"पृष्ठाचे नाव\",\n    \"questionBubble\": {\n    \"shortcuts\": \"शॉर्टकट्स\",\n    \"whatsNew\": \"नवीन काय आहे?\",\n    \"help\": \"मदत आणि समर्थन\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n    \"name\": \"डीबग माहिती\",\n    \"success\": \"डीबग माहिती क्लिपबोर्डवर कॉपी केली!\",\n    \"fail\": \"डीबग माहिती कॉपी करता आली नाही\"\n   },\n   \"feedback\": \"अभिप्राय\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"हटवा, नाव बदला आणि अधिक...\",\n    \"addPageTooltip\": \"तत्काळ एक पृष्ठ जोडा\",\n    \"defaultNewPageName\": \"शीर्षक नसलेले\",\n    \"renameDialog\": \"नाव बदला\",\n    \"pageNameSuffix\": \"प्रत\"\n  },\n  \"noPagesInside\": \"अंदर कोणतीही पृष्ठे नाहीत\",\n    \"toolbar\": {\n    \"undo\": \"पूर्ववत करा\",\n    \"redo\": \"पुन्हा करा\",\n    \"bold\": \"ठळक\",\n    \"italic\": \"तिरकस\",\n    \"underline\": \"अधोरेखित\",\n    \"strike\": \"मागे ओढलेले\",\n    \"numList\": \"क्रमांकित यादी\",\n    \"bulletList\": \"बुलेट यादी\",\n    \"checkList\": \"चेक यादी\",\n    \"inlineCode\": \"इनलाइन कोड\",\n    \"quote\": \"उद्धरण ब्लॉक\",\n    \"header\": \"शीर्षक\",\n    \"highlight\": \"हायलाइट\",\n    \"color\": \"रंग\",\n    \"addLink\": \"लिंक जोडा\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"लाइट मोडमध्ये स्विच करा\",\n    \"darkMode\": \"डार्क मोडमध्ये स्विच करा\",\n    \"openAsPage\": \"पृष्ठ म्हणून उघडा\",\n    \"addNewRow\": \"नवीन पंक्ती जोडा\",\n    \"openMenu\": \"मेनू उघडण्यासाठी क्लिक करा\",\n    \"dragRow\": \"पंक्तीचे स्थान बदलण्यासाठी ड्रॅग करा\",\n    \"viewDataBase\": \"डेटाबेस पहा\",\n    \"referencePage\": \"हे {name} संदर्भित आहे\",\n    \"addBlockBelow\": \"खाली एक ब्लॉक जोडा\",\n    \"aiGenerate\": \"निर्मिती करा\"\n  },\n  \"sideBar\": {\n  \"closeSidebar\": \"साइडबार बंद करा\",\n  \"openSidebar\": \"साइडबार उघडा\",\n  \"expandSidebar\": \"पूर्ण पृष्ठावर विस्तारित करा\",\n  \"personal\": \"वैयक्तिक\",\n  \"private\": \"खाजगी\",\n  \"workspace\": \"कार्यक्षेत्र\",\n  \"favorites\": \"आवडती\",\n  \"clickToHidePrivate\": \"खाजगी जागा लपवण्यासाठी क्लिक करा\\nयेथे तयार केलेली पाने फक्त तुम्हाला दिसतील\",\n  \"clickToHideWorkspace\": \"कार्यक्षेत्र लपवण्यासाठी क्लिक करा\\nयेथे तयार केलेली पाने सर्व सदस्यांना दिसतील\",\n  \"clickToHidePersonal\": \"वैयक्तिक जागा लपवण्यासाठी क्लिक करा\",\n  \"clickToHideFavorites\": \"आवडती जागा लपवण्यासाठी क्लिक करा\",\n  \"addAPage\": \"नवीन पृष्ठ जोडा\",\n  \"addAPageToPrivate\": \"खाजगी जागेत पृष्ठ जोडा\",\n  \"addAPageToWorkspace\": \"कार्यक्षेत्रात पृष्ठ जोडा\",\n  \"recent\": \"अलीकडील\",\n  \"today\": \"आज\",\n  \"thisWeek\": \"या आठवड्यात\",\n  \"others\": \"पूर्वीच्या आवडती\",\n  \"earlier\": \"पूर्वीचे\",\n  \"justNow\": \"आत्ताच\",\n  \"minutesAgo\": \"{count} मिनिटांपूर्वी\",\n  \"lastViewed\": \"शेवटी पाहिलेले\",\n  \"favoriteAt\": \"आवडते म्हणून चिन्हांकित\",\n  \"emptyRecent\": \"अलीकडील पृष्ठे नाहीत\",\n  \"emptyRecentDescription\": \"तुम्ही पाहिलेली पृष्ठे येथे सहज पुन्हा मिळवण्यासाठी दिसतील.\",\n  \"emptyFavorite\": \"आवडती पृष्ठे नाहीत\",\n  \"emptyFavoriteDescription\": \"पृष्ठांना आवडते म्हणून चिन्हांकित करा—ते येथे झपाट्याने प्रवेशासाठी दिसतील!\",\n  \"removePageFromRecent\": \"हे पृष्ठ अलीकडील यादीतून काढायचे?\",\n  \"removeSuccess\": \"यशस्वीरित्या काढले गेले\",\n  \"favoriteSpace\": \"आवडती\",\n  \"RecentSpace\": \"अलीकडील\",\n  \"Spaces\": \"जागा\",\n  \"upgradeToPro\": \"Pro मध्ये अपग्रेड करा\",\n  \"upgradeToAIMax\": \"अमर्यादित AI अनलॉक करा\",\n  \"storageLimitDialogTitle\": \"तुमचा मोफत स्टोरेज संपला आहे. अमर्यादित स्टोरेजसाठी अपग्रेड करा\",\n  \"storageLimitDialogTitleIOS\": \"तुमचा मोफत स्टोरेज संपला आहे.\",\n  \"aiResponseLimitTitle\": \"तुमचे मोफत AI प्रतिसाद संपले आहेत. कृपया Pro प्लॅनला अपग्रेड करा किंवा AI add-on खरेदी करा\",\n  \"aiResponseLimitDialogTitle\": \"AI प्रतिसाद मर्यादा गाठली आहे\",\n  \"aiResponseLimit\": \"तुमचे मोफत AI प्रतिसाद संपले आहेत.\\n\\nसेटिंग्ज -> प्लॅन -> AI Max किंवा Pro प्लॅन क्लिक करा\",\n  \"askOwnerToUpgradeToPro\": \"तुमच्या कार्यक्षेत्राचे मोफत स्टोरेज संपत चालले आहे. कृपया कार्यक्षेत्र मालकाला Pro प्लॅनमध्ये अपग्रेड करण्यास सांगा\",\n  \"askOwnerToUpgradeToProIOS\": \"तुमच्या कार्यक्षेत्राचे मोफत स्टोरेज संपत चालले आहे.\",\n  \"askOwnerToUpgradeToAIMax\": \"तुमच्या कार्यक्षेत्राचे मोफत AI प्रतिसाद संपले आहेत. कृपया कार्यक्षेत्र मालकाला प्लॅन अपग्रेड किंवा AI add-ons खरेदी करण्यास सांगा\",\n  \"askOwnerToUpgradeToAIMaxIOS\": \"तुमच्या कार्यक्षेत्राचे मोफत AI प्रतिसाद संपले आहेत.\",\n  \"purchaseAIMax\": \"तुमच्या कार्यक्षेत्राचे AI प्रतिमा प्रतिसाद संपले आहेत. कृपया कार्यक्षेत्र मालकाला AI Max खरेदी करण्यास सांगा\",\n  \"aiImageResponseLimit\": \"तुमचे AI प्रतिमा प्रतिसाद संपले आहेत.\\n\\nसेटिंग्ज -> प्लॅन -> AI Max क्लिक करा\",\n  \"purchaseStorageSpace\": \"स्टोरेज स्पेस खरेदी करा\",\n  \"singleFileProPlanLimitationDescription\": \"तुम्ही मोफत प्लॅनमध्ये परवानगी दिलेल्या फाइल अपलोड आकाराची मर्यादा ओलांडली आहे. कृपया Pro प्लॅनमध्ये अपग्रेड करा\",\n  \"purchaseAIResponse\": \"AI प्रतिसाद खरेदी करा\",\n  \"askOwnerToUpgradeToLocalAI\": \"कार्यक्षेत्र मालकाला ऑन-डिव्हाइस AI सक्षम करण्यास सांगा\",\n  \"upgradeToAILocal\": \"अत्याधिक गोपनीयतेसाठी तुमच्या डिव्हाइसवर लोकल मॉडेल चालवा\",\n  \"upgradeToAILocalDesc\": \"PDFs सोबत गप्पा मारा, तुमचे लेखन सुधारा आणि लोकल AI वापरून टेबल्स आपोआप भरा\"\n},\n  \"notifications\": {\n    \"export\": {\n    \"markdown\": \"टीप Markdown मध्ये निर्यात केली\",\n    \"path\": \"Documents/flowy\"\n    }\n  },\n    \"contactsPage\": {\n    \"title\": \"संपर्क\",\n    \"whatsHappening\": \"या आठवड्यात काय घडत आहे?\",\n    \"addContact\": \"संपर्क जोडा\",\n    \"editContact\": \"संपर्क संपादित करा\"\n  },\n  \"button\": {\n    \"ok\": \"ठीक आहे\",\n    \"confirm\": \"खात्री करा\",\n    \"done\": \"पूर्ण\",\n    \"cancel\": \"रद्द करा\",\n    \"signIn\": \"साइन इन\",\n    \"signOut\": \"साइन आउट\",\n    \"complete\": \"पूर्ण करा\",\n    \"save\": \"जतन करा\",\n    \"generate\": \"निर्माण करा\",\n    \"esc\": \"ESC\",\n    \"keep\": \"ठेवा\",\n    \"tryAgain\": \"पुन्हा प्रयत्न करा\",\n    \"discard\": \"टाका\",\n    \"replace\": \"बदला\",\n    \"insertBelow\": \"खाली घाला\",\n    \"insertAbove\": \"वर घाला\",\n    \"upload\": \"अपलोड करा\",\n    \"edit\": \"संपादित करा\",\n    \"delete\": \"हटवा\",\n    \"copy\": \"कॉपी करा\",\n    \"duplicate\": \"प्रत बनवा\",\n    \"putback\": \"परत ठेवा\",\n    \"update\": \"अद्यतनित करा\",\n    \"share\": \"शेअर करा\",\n    \"removeFromFavorites\": \"आवडतीतून काढा\",\n    \"removeFromRecent\": \"अलीकडील यादीतून काढा\",\n    \"addToFavorites\": \"आवडतीत जोडा\",\n    \"favoriteSuccessfully\": \"आवडतीत यशस्वीरित्या जोडले\",\n    \"unfavoriteSuccessfully\": \"आवडतीतून यशस्वीरित्या काढले\",\n    \"duplicateSuccessfully\": \"प्रत यशस्वीरित्या तयार झाली\",\n    \"rename\": \"नाव बदला\",\n    \"helpCenter\": \"मदत केंद्र\",\n    \"add\": \"जोड़ा\",\n    \"yes\": \"होय\",\n    \"no\": \"नाही\",\n    \"clear\": \"साफ करा\",\n    \"remove\": \"काढा\",\n    \"dontRemove\": \"काढू नका\",\n    \"copyLink\": \"लिंक कॉपी करा\",\n    \"align\": \"जुळवा\",\n    \"login\": \"लॉगिन\",\n    \"logout\": \"लॉगआउट\",\n    \"deleteAccount\": \"खाते हटवा\",\n    \"back\": \"मागे\",\n    \"signInGoogle\": \"Google सह पुढे जा\",\n    \"signInGithub\": \"GitHub सह पुढे जा\",\n    \"signInDiscord\": \"Discord सह पुढे जा\",\n    \"more\": \"अधिक\",\n    \"create\": \"तयार करा\",\n    \"close\": \"बंद करा\",\n    \"next\": \"पुढे\",\n    \"previous\": \"मागील\",\n    \"submit\": \"सबमिट करा\",\n    \"download\": \"डाउनलोड करा\",\n    \"backToHome\": \"मुख्यपृष्ठावर परत जा\",\n    \"viewing\": \"पाहत आहात\",\n    \"editing\": \"संपादन करत आहात\",\n    \"gotIt\": \"समजले\",\n    \"retry\": \"पुन्हा प्रयत्न करा\",\n    \"uploadFailed\": \"अपलोड अयशस्वी.\",\n    \"copyLinkOriginal\": \"मूळ दुव्याची कॉपी करा\"\n  },\n  \"label\": {\n    \"welcome\": \"स्वागत आहे!\",\n    \"firstName\": \"पहिले नाव\",\n    \"middleName\": \"मधले नाव\",\n    \"lastName\": \"आडनाव\",\n    \"stepX\": \"पायरी {X}\"\n  },\n  \"oAuth\": {\n  \"err\": {\n    \"failedTitle\": \"तुमच्या खात्याशी कनेक्ट होता आले नाही.\",\n    \"failedMsg\": \"कृपया खात्री करा की तुम्ही ब्राउझरमध्ये साइन-इन प्रक्रिया पूर्ण केली आहे.\"\n  },\n  \"google\": {\n    \"title\": \"GOOGLE साइन-इन\",\n    \"instruction1\": \"तुमचे Google Contacts आयात करण्यासाठी, तुम्हाला तुमच्या वेब ब्राउझरचा वापर करून या अ‍ॅप्लिकेशनला अधिकृत करणे आवश्यक आहे.\",\n    \"instruction2\": \"ही कोड आयकॉनवर क्लिक करून किंवा मजकूर निवडून क्लिपबोर्डवर कॉपी करा:\",\n    \"instruction3\": \"तुमच्या वेब ब्राउझरमध्ये खालील दुव्यावर जा आणि वरील कोड टाका:\",\n    \"instruction4\": \"साइनअप पूर्ण झाल्यावर खालील बटण क्लिक करा:\"\n    }\n  },\n  \"settings\": {\n  \"title\": \"सेटिंग्ज\",\n  \"popupMenuItem\": {\n    \"settings\": \"सेटिंग्ज\",\n    \"members\": \"सदस्य\",\n    \"trash\": \"कचरा\",\n    \"helpAndSupport\": \"मदत आणि समर्थन\"\n  },\n  \"sites\": {\n      \"title\": \"साइट्स\",\n      \"namespaceTitle\": \"नेमस्पेस\",\n      \"namespaceDescription\": \"तुमचा नेमस्पेस आणि मुख्यपृष्ठ व्यवस्थापित करा\",\n      \"namespaceHeader\": \"नेमस्पेस\",\n      \"homepageHeader\": \"मुख्यपृष्ठ\",\n      \"updateNamespace\": \"नेमस्पेस अद्यतनित करा\",\n      \"removeHomepage\": \"मुख्यपृष्ठ हटवा\",\n      \"selectHomePage\": \"एक पृष्ठ निवडा\",\n      \"clearHomePage\": \"या नेमस्पेससाठी मुख्यपृष्ठ साफ करा\",\n      \"customUrl\": \"स्वतःची URL\",\n      \"namespace\": {\n      \"description\": \"हे बदल सर्व प्रकाशित पृष्ठांवर लागू होतील जे या नेमस्पेसवर चालू आहेत\",\n      \"tooltip\": \"कोणताही अनुचित नेमस्पेस आम्ही काढून टाकण्याचा अधिकार राखून ठेवतो\",\n      \"updateExistingNamespace\": \"विद्यमान नेमस्पेस अद्यतनित करा\",\n      \"upgradeToPro\": \"मुख्यपृष्ठ सेट करण्यासाठी Pro प्लॅनमध्ये अपग्रेड करा\",\n      \"redirectToPayment\": \"पेमेंट पृष्ठावर वळवत आहे...\",\n      \"onlyWorkspaceOwnerCanSetHomePage\": \"फक्त कार्यक्षेत्र मालकच मुख्यपृष्ठ सेट करू शकतो\",\n      \"pleaseAskOwnerToSetHomePage\": \"कृपया कार्यक्षेत्र मालकाला Pro प्लॅनमध्ये अपग्रेड करण्यास सांगा\"\n      },\n      \"publishedPage\": {\n      \"title\": \"सर्व प्रकाशित पृष्ठे\",\n      \"description\": \"तुमची प्रकाशित पृष्ठे व्यवस्थापित करा\",\n      \"page\": \"पृष्ठ\",\n      \"pathName\": \"पथाचे नाव\",\n      \"date\": \"प्रकाशन तारीख\",\n      \"emptyHinText\": \"या कार्यक्षेत्रात तुमच्याकडे कोणतीही प्रकाशित पृष्ठे नाहीत\",\n      \"noPublishedPages\": \"प्रकाशित पृष्ठे नाहीत\",\n      \"settings\": \"प्रकाशन सेटिंग्ज\",\n      \"clickToOpenPageInApp\": \"पृष्ठ अ‍ॅपमध्ये उघडा\",\n      \"clickToOpenPageInBrowser\": \"पृष्ठ ब्राउझरमध्ये उघडा\"\n      }\n    }\n  },\n    \"error\": {\n    \"failedToGeneratePaymentLink\": \"Pro प्लॅनसाठी पेमेंट लिंक तयार करण्यात अयशस्वी\",\n    \"failedToUpdateNamespace\": \"नेमस्पेस अद्यतनित करण्यात अयशस्वी\",\n    \"proPlanLimitation\": \"नेमस्पेस अद्यतनित करण्यासाठी तुम्हाला Pro प्लॅनमध्ये अपग्रेड करणे आवश्यक आहे\",\n    \"namespaceAlreadyInUse\": \"नेमस्पेस आधीच वापरात आहे, कृपया दुसरे प्रयत्न करा\",\n    \"invalidNamespace\": \"अवैध नेमस्पेस, कृपया दुसरे प्रयत्न करा\",\n    \"namespaceLengthAtLeast2Characters\": \"नेमस्पेस किमान २ अक्षरे लांब असावे\",\n    \"onlyWorkspaceOwnerCanUpdateNamespace\": \"फक्त कार्यक्षेत्र मालकच नेमस्पेस अद्यतनित करू शकतो\",\n    \"onlyWorkspaceOwnerCanRemoveHomepage\": \"फक्त कार्यक्षेत्र मालकच मुख्यपृष्ठ हटवू शकतो\",\n    \"setHomepageFailed\": \"मुख्यपृष्ठ सेट करण्यात अयशस्वी\",\n    \"namespaceTooLong\": \"नेमस्पेस खूप लांब आहे, कृपया दुसरे प्रयत्न करा\",\n    \"namespaceTooShort\": \"नेमस्पेस खूप लहान आहे, कृपया दुसरे प्रयत्न करा\",\n    \"namespaceIsReserved\": \"हा नेमस्पेस राखीव आहे, कृपया दुसरे प्रयत्न करा\",\n    \"updatePathNameFailed\": \"पथाचे नाव अद्यतनित करण्यात अयशस्वी\",\n    \"removeHomePageFailed\": \"मुख्यपृष्ठ हटवण्यात अयशस्वी\",\n    \"publishNameContainsInvalidCharacters\": \"पथाच्या नावामध्ये अवैध अक्षरे आहेत, कृपया दुसरे प्रयत्न करा\",\n    \"publishNameTooShort\": \"पथाचे नाव खूप लहान आहे, कृपया दुसरे प्रयत्न करा\",\n    \"publishNameTooLong\": \"पथाचे नाव खूप लांब आहे, कृपया दुसरे प्रयत्न करा\",\n    \"publishNameAlreadyInUse\": \"हे पथाचे नाव आधीच वापरले गेले आहे, कृपया दुसरे प्रयत्न करा\",\n    \"namespaceContainsInvalidCharacters\": \"नेमस्पेसमध्ये अवैध अक्षरे आहेत, कृपया दुसरे प्रयत्न करा\",\n    \"publishPermissionDenied\": \"फक्त कार्यक्षेत्र मालक किंवा पृष्ठ प्रकाशकच प्रकाशन सेटिंग्ज व्यवस्थापित करू शकतो\",\n    \"publishNameCannotBeEmpty\": \"पथाचे नाव रिकामे असू शकत नाही, कृपया दुसरे प्रयत्न करा\"\n  },\n      \"success\": {\n    \"namespaceUpdated\": \"नेमस्पेस यशस्वीरित्या अद्यतनित केला\",\n    \"setHomepageSuccess\": \"मुख्यपृष्ठ यशस्वीरित्या सेट केले\",\n    \"updatePathNameSuccess\": \"पथाचे नाव यशस्वीरित्या अद्यतनित केले\",\n    \"removeHomePageSuccess\": \"मुख्यपृष्ठ यशस्वीरित्या हटवले\"\n  },\n    \"accountPage\": {\n    \"menuLabel\": \"खाते आणि अ‍ॅप\",\n    \"title\": \"माझे खाते\",\n    \"general\": {\n    \"title\": \"खात्याचे नाव आणि प्रोफाइल प्रतिमा\",\n    \"changeProfilePicture\": \"प्रोफाइल प्रतिमा बदला\"\n  },\n  \"email\": {\n    \"title\": \"ईमेल\",\n    \"actions\": {\n      \"change\": \"ईमेल बदला\"\n    }\n  },\n  \"login\": {\n    \"title\": \"खाते लॉगिन\",\n    \"loginLabel\": \"लॉगिन\",\n    \"logoutLabel\": \"लॉगआउट\"\n  },\n  \"isUpToDate\": \"@:appName अद्ययावत आहे!\",\n  \"officialVersion\": \"आवृत्ती {version} (अधिकृत बिल्ड)\"\n},\n    \"workspacePage\": {\n  \"menuLabel\": \"कार्यक्षेत्र\",\n  \"title\": \"कार्यक्षेत्र\",\n  \"description\": \"तुमचे कार्यक्षेत्र स्वरूप, थीम, फॉन्ट, मजकूर रचना, दिनांक/वेळ फॉरमॅट आणि भाषा सानुकूलित करा.\",\n  \"workspaceName\": {\n    \"title\": \"कार्यक्षेत्राचे नाव\"\n  },\n  \"workspaceIcon\": {\n    \"title\": \"कार्यक्षेत्राचे चिन्ह\",\n    \"description\": \"तुमच्या कार्यक्षेत्रासाठी प्रतिमा अपलोड करा किंवा इमोजी वापरा. हे चिन्ह साइडबार आणि सूचना मध्ये दिसेल.\"\n  },\n  \"appearance\": {\n    \"title\": \"दृश्यरूप\",\n    \"description\": \"कार्यक्षेत्राचे दृश्यरूप, थीम, फॉन्ट, मजकूर रचना, दिनांक, वेळ आणि भाषा सानुकूलित करा.\",\n    \"options\": {\n      \"system\": \"स्वयंचलित\",\n      \"light\": \"लाइट\",\n      \"dark\": \"डार्क\"\n      }\n    }\n  },\n  \"resetCursorColor\": {\n  \"title\": \"दस्तऐवज कर्सरचा रंग रीसेट करा\",\n  \"description\": \"तुम्हाला कर्सरचा रंग रीसेट करायचा आहे का?\"\n  },\n  \"resetSelectionColor\": {\n  \"title\": \"दस्तऐवज निवडीचा रंग रीसेट करा\",\n  \"description\": \"तुम्हाला निवडीचा रंग रीसेट करायचा आहे का?\"\n  },\n  \"resetWidth\": {\n  \"resetSuccess\": \"दस्तऐवजाची रुंदी यशस्वीरित्या रीसेट केली\"\n  },\n    \"theme\": {\n    \"title\": \"थीम\",\n    \"description\": \"पूर्व-निर्धारित थीम निवडा किंवा तुमची स्वतःची थीम अपलोड करा.\",\n    \"uploadCustomThemeTooltip\": \"स्वतःची थीम अपलोड करा\"\n  },\n    \"workspaceFont\": {\n    \"title\": \"कार्यक्षेत्र फॉन्ट\",\n    \"noFontHint\": \"कोणताही फॉन्ट सापडला नाही, कृपया दुसरा शब्द वापरून पहा.\"\n  },\n      \"textDirection\": {\n     \"title\": \"मजकूर दिशा\",\n    \"leftToRight\": \"डावीकडून उजवीकडे\",\n    \"rightToLeft\": \"उजवीकडून डावीकडे\",\n    \"auto\": \"स्वयंचलित\",\n    \"enableRTLItems\": \"RTL टूलबार घटक सक्षम करा\"\n  },\n      \"layoutDirection\": {\n    \"title\": \"लेआउट दिशा\",\n    \"leftToRight\": \"डावीकडून उजवीकडे\",\n    \"rightToLeft\": \"उजवीकडून डावीकडे\"\n  },\n      \"dateTime\": {\n    \"title\": \"दिनांक आणि वेळ\",\n    \"example\": \"{} वाजता {} ({})\",\n    \"24HourTime\": \"२४-तास वेळ\",\n    \"dateFormat\": {\n    \"label\": \"दिनांक फॉरमॅट\",\n    \"local\": \"स्थानिक\",\n    \"us\": \"US\",\n    \"iso\": \"ISO\",\n    \"friendly\": \"सुलभ\",\n    \"dmy\": \"D/M/Y\"\n    }\n  },\n    \"language\": {\n    \"title\": \"भाषा\"\n  },\n    \"deleteWorkspacePrompt\": {\n    \"title\": \"कार्यक्षेत्र हटवा\",\n    \"content\": \"तुम्हाला हे कार्यक्षेत्र हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही, आणि तुम्ही प्रकाशित केलेली कोणतीही पृष्ठे अप्रकाशित होतील.\"\n  },\n    \"leaveWorkspacePrompt\": {\n    \"title\": \"कार्यक्षेत्र सोडा\",\n    \"content\": \"तुम्हाला हे कार्यक्षेत्र सोडायचे आहे का? यानंतर तुम्हाला यामधील सर्व पृष्ठे आणि डेटावर प्रवेश मिळणार नाही.\",\n    \"success\": \"तुम्ही कार्यक्षेत्र यशस्वीरित्या सोडले.\",\n    \"fail\": \"कार्यक्षेत्र सोडण्यात अयशस्वी.\"\n  },\n      \"manageWorkspace\": {\n    \"title\": \"कार्यक्षेत्र व्यवस्थापित करा\",\n    \"leaveWorkspace\": \"कार्यक्षेत्र सोडा\",\n    \"deleteWorkspace\": \"कार्यक्षेत्र हटवा\"\n  },\n    \"manageDataPage\": {\n  \"menuLabel\": \"डेटा व्यवस्थापित करा\",\n  \"title\": \"डेटा व्यवस्थापन\",\n  \"description\": \"@:appName मध्ये स्थानिक डेटा संचयन व्यवस्थापित करा किंवा तुमचा विद्यमान डेटा आयात करा.\",\n  \"dataStorage\": {\n    \"title\": \"फाइल संचयन स्थान\",\n    \"tooltip\": \"जिथे तुमच्या फाइल्स संग्रहित आहेत ते स्थान\",\n    \"actions\": {\n      \"change\": \"मार्ग बदला\",\n      \"open\": \"फोल्डर उघडा\",\n      \"openTooltip\": \"सध्याच्या डेटा फोल्डरचे स्थान उघडा\",\n      \"copy\": \"मार्ग कॉपी करा\",\n      \"copiedHint\": \"मार्ग कॉपी केला!\",\n      \"resetTooltip\": \"मूलभूत स्थानावर रीसेट करा\"\n    },\n    \"resetDialog\": {\n      \"title\": \"तुम्हाला खात्री आहे का?\",\n      \"description\": \"पथ मूलभूत डेटा स्थानावर रीसेट केल्याने तुमचा डेटा हटवला जाणार नाही. तुम्हाला सध्याचा डेटा पुन्हा आयात करायचा असल्यास, कृपया आधी त्याचा पथ कॉपी करा.\"\n    }\n  },\n  \"importData\": {\n    \"title\": \"डेटा आयात करा\",\n    \"tooltip\": \"@:appName बॅकअप/डेटा फोल्डरमधून डेटा आयात करा\",\n    \"description\": \"बाह्य @:appName डेटा फोल्डरमधून डेटा कॉपी करा\",\n    \"action\": \"फाइल निवडा\"\n  },\n  \"encryption\": {\n    \"title\": \"एनक्रिप्शन\",\n    \"tooltip\": \"तुमचा डेटा कसा संग्रहित आणि एनक्रिप्ट केला जातो ते व्यवस्थापित करा\",\n    \"descriptionNoEncryption\": \"एनक्रिप्शन चालू केल्याने सर्व डेटा एनक्रिप्ट केला जाईल. ही क्रिया पूर्ववत केली जाऊ शकत नाही.\",\n    \"descriptionEncrypted\": \"तुमचा डेटा एनक्रिप्टेड आहे.\",\n    \"action\": \"डेटा एनक्रिप्ट करा\",\n    \"dialog\": {\n      \"title\": \"संपूर्ण डेटा एनक्रिप्ट करायचा?\",\n      \"description\": \"तुमचा संपूर्ण डेटा एनक्रिप्ट केल्याने तो सुरक्षित राहील. ही क्रिया पूर्ववत केली जाऊ शकत नाही. तुम्हाला पुढे जायचे आहे का?\"\n    }\n  },\n  \"cache\": {\n    \"title\": \"कॅशे साफ करा\",\n    \"description\": \"प्रतिमा न दिसणे, पृष्ठे हरवणे, फॉन्ट लोड न होणे अशा समस्यांचे निराकरण करण्यासाठी मदत करा. याचा तुमच्या डेटावर परिणाम होणार नाही.\",\n    \"dialog\": {\n      \"title\": \"कॅशे साफ करा\",\n      \"description\": \"प्रतिमा न दिसणे, पृष्ठे हरवणे, फॉन्ट लोड न होणे अशा समस्यांचे निराकरण करण्यासाठी मदत करा. याचा तुमच्या डेटावर परिणाम होणार नाही.\",\n      \"successHint\": \"कॅशे साफ झाली!\"\n    }\n  },\n  \"data\": {\n    \"fixYourData\": \"तुमचा डेटा सुधारा\",\n    \"fixButton\": \"सुधारा\",\n    \"fixYourDataDescription\": \"तुमच्या डेटामध्ये काही अडचण येत असल्यास, तुम्ही येथे ती सुधारू शकता.\"\n    }\n  },\n    \"shortcutsPage\": {\n  \"menuLabel\": \"शॉर्टकट्स\",\n  \"title\": \"शॉर्टकट्स\",\n  \"editBindingHint\": \"नवीन बाइंडिंग टाका\",\n  \"searchHint\": \"शोधा\",\n  \"actions\": {\n    \"resetDefault\": \"मूलभूत रीसेट करा\"\n  },\n  \"errorPage\": {\n    \"message\": \"शॉर्टकट्स लोड करण्यात अयशस्वी: {}\",\n    \"howToFix\": \"कृपया पुन्हा प्रयत्न करा. समस्या कायम राहिल्यास GitHub वर संपर्क साधा.\"\n  },\n  \"resetDialog\": {\n    \"title\": \"शॉर्टकट्स रीसेट करा\",\n    \"description\": \"हे सर्व कीबाइंडिंग्जना मूळ स्थितीत रीसेट करेल. ही क्रिया पूर्ववत करता येणार नाही. तुम्हाला खात्री आहे का?\",\n    \"buttonLabel\": \"रीसेट करा\"\n  },\n  \"conflictDialog\": {\n    \"title\": \"{} आधीच वापरले जात आहे\",\n    \"descriptionPrefix\": \"हे कीबाइंडिंग सध्या \",\n    \"descriptionSuffix\": \" यामध्ये वापरले जात आहे. तुम्ही हे कीबाइंडिंग बदलल्यास, ते {} मधून काढले जाईल.\",\n    \"confirmLabel\": \"पुढे जा\"\n  },\n  \"editTooltip\": \"कीबाइंडिंग संपादित करण्यासाठी क्लिक करा\",\n  \"keybindings\": {\n    \"toggleToDoList\": \"टू-डू सूची चालू/बंद करा\",\n    \"insertNewParagraphInCodeblock\": \"नवीन परिच्छेद टाका\",\n    \"pasteInCodeblock\": \"कोडब्लॉकमध्ये पेस्ट करा\",\n    \"selectAllCodeblock\": \"सर्व निवडा\",\n    \"indentLineCodeblock\": \"ओळीच्या सुरुवातीला दोन स्पेस टाका\",\n    \"outdentLineCodeblock\": \"ओळीच्या सुरुवातीची दोन स्पेस काढा\",\n    \"twoSpacesCursorCodeblock\": \"कर्सरवर दोन स्पेस टाका\",\n    \"copy\": \"निवड कॉपी करा\",\n    \"paste\": \"मजकुरात पेस्ट करा\",\n    \"cut\": \"निवड कट करा\",\n    \"alignLeft\": \"मजकूर डावीकडे संरेखित करा\",\n    \"alignCenter\": \"मजकूर मधोमध संरेखित करा\",\n    \"alignRight\": \"मजकूर उजवीकडे संरेखित करा\",\n    \"insertInlineMathEquation\": \"इनलाइन गणितीय सूत्र टाका\",\n    \"undo\": \"पूर्ववत करा\",\n    \"redo\": \"पुन्हा करा\",\n    \"convertToParagraph\": \"ब्लॉक परिच्छेदात रूपांतरित करा\",\n    \"backspace\": \"हटवा\",\n    \"deleteLeftWord\": \"डावीकडील शब्द हटवा\",\n    \"deleteLeftSentence\": \"डावीकडील वाक्य हटवा\",\n    \"delete\": \"उजवीकडील अक्षर हटवा\",\n    \"deleteMacOS\": \"डावीकडील अक्षर हटवा\",\n    \"deleteRightWord\": \"उजवीकडील शब्द हटवा\",\n    \"moveCursorLeft\": \"कर्सर डावीकडे हलवा\",\n    \"moveCursorBeginning\": \"कर्सर सुरुवातीला हलवा\",\n    \"moveCursorLeftWord\": \"कर्सर एक शब्द डावीकडे हलवा\",\n    \"moveCursorLeftSelect\": \"निवडा आणि कर्सर डावीकडे हलवा\",\n    \"moveCursorBeginSelect\": \"निवडा आणि कर्सर सुरुवातीला हलवा\",\n    \"moveCursorLeftWordSelect\": \"निवडा आणि कर्सर एक शब्द डावीकडे हलवा\",\n    \"moveCursorRight\": \"कर्सर उजवीकडे हलवा\",\n    \"moveCursorEnd\": \"कर्सर शेवटी हलवा\",\n    \"moveCursorRightWord\": \"कर्सर एक शब्द उजवीकडे हलवा\",\n    \"moveCursorRightSelect\": \"निवडा आणि कर्सर उजवीकडे हलवा\",\n    \"moveCursorEndSelect\": \"निवडा आणि कर्सर शेवटी हलवा\",\n    \"moveCursorRightWordSelect\": \"निवडा आणि कर्सर एक शब्द उजवीकडे हलवा\",\n    \"moveCursorUp\": \"कर्सर वर हलवा\",\n    \"moveCursorTopSelect\": \"निवडा आणि कर्सर वर हलवा\",\n    \"moveCursorTop\": \"कर्सर वर हलवा\",\n    \"moveCursorUpSelect\": \"निवडा आणि कर्सर वर हलवा\",\n    \"moveCursorBottomSelect\": \"निवडा आणि कर्सर खाली हलवा\",\n    \"moveCursorBottom\": \"कर्सर खाली हलवा\",\n    \"moveCursorDown\": \"कर्सर खाली हलवा\",\n    \"moveCursorDownSelect\": \"निवडा आणि कर्सर खाली हलवा\",\n    \"home\": \"वर स्क्रोल करा\",\n    \"end\": \"खाली स्क्रोल करा\",\n    \"toggleBold\": \"बोल्ड चालू/बंद करा\",\n    \"toggleItalic\": \"इटालिक चालू/बंद करा\",\n    \"toggleUnderline\": \"अधोरेखित चालू/बंद करा\",\n    \"toggleStrikethrough\": \"स्ट्राईकथ्रू चालू/बंद करा\",\n    \"toggleCode\": \"इनलाइन कोड चालू/बंद करा\",\n    \"toggleHighlight\": \"हायलाईट चालू/बंद करा\",\n    \"showLinkMenu\": \"लिंक मेनू दाखवा\",\n    \"openInlineLink\": \"इनलाइन लिंक उघडा\",\n    \"openLinks\": \"सर्व निवडलेले लिंक उघडा\",\n    \"indent\": \"इंडेंट\",\n    \"outdent\": \"आउटडेंट\",\n    \"exit\": \"संपादनातून बाहेर पडा\",\n    \"pageUp\": \"एक पृष्ठ वर स्क्रोल करा\",\n    \"pageDown\": \"एक पृष्ठ खाली स्क्रोल करा\",\n    \"selectAll\": \"सर्व निवडा\",\n    \"pasteWithoutFormatting\": \"फॉरमॅटिंगशिवाय पेस्ट करा\",\n    \"showEmojiPicker\": \"इमोजी निवडकर्ता दाखवा\",\n    \"enterInTableCell\": \"टेबलमध्ये नवीन ओळ जोडा\",\n    \"leftInTableCell\": \"टेबलमध्ये डावीकडील सेलमध्ये जा\",\n    \"rightInTableCell\": \"टेबलमध्ये उजवीकडील सेलमध्ये जा\",\n    \"upInTableCell\": \"टेबलमध्ये वरील सेलमध्ये जा\",\n    \"downInTableCell\": \"टेबलमध्ये खालील सेलमध्ये जा\",\n    \"tabInTableCell\": \"टेबलमध्ये पुढील सेलमध्ये जा\",\n    \"shiftTabInTableCell\": \"टेबलमध्ये मागील सेलमध्ये जा\",\n    \"backSpaceInTableCell\": \"सेलच्या सुरुवातीला थांबा\"\n  },\n  \"commands\": {\n    \"codeBlockNewParagraph\": \"कोडब्लॉकच्या शेजारी नवीन परिच्छेद टाका\",\n    \"codeBlockIndentLines\": \"कोडब्लॉकमध्ये ओळीच्या सुरुवातीला दोन स्पेस टाका\",\n    \"codeBlockOutdentLines\": \"कोडब्लॉकमध्ये ओळीच्या सुरुवातीची दोन स्पेस काढा\",\n    \"codeBlockAddTwoSpaces\": \"कोडब्लॉकमध्ये कर्सरच्या ठिकाणी दोन स्पेस टाका\",\n    \"codeBlockSelectAll\": \"कोडब्लॉकमधील सर्व मजकूर निवडा\",\n    \"codeBlockPasteText\": \"कोडब्लॉकमध्ये मजकूर पेस्ट करा\",\n    \"textAlignLeft\": \"मजकूर डावीकडे संरेखित करा\",\n    \"textAlignCenter\": \"मजकूर मधोमध संरेखित करा\",\n    \"textAlignRight\": \"मजकूर उजवीकडे संरेखित करा\"\n  },\n  \"couldNotLoadErrorMsg\": \"शॉर्टकट लोड करता आले नाहीत. पुन्हा प्रयत्न करा\",\n  \"couldNotSaveErrorMsg\": \"शॉर्टकट सेव्ह करता आले नाहीत. पुन्हा प्रयत्न करा\"\n},\n    \"aiPage\": {\n  \"title\": \"AI सेटिंग्ज\",\n  \"menuLabel\": \"AI सेटिंग्ज\",\n  \"keys\": {\n    \"enableAISearchTitle\": \"AI शोध\",\n    \"aiSettingsDescription\": \"AppFlowy AI चालवण्यासाठी तुमचा पसंतीचा मॉडेल निवडा. आता GPT-4o, GPT-o3-mini, DeepSeek R1, Claude 3.5 Sonnet आणि Ollama मधील मॉडेल्सचा समावेश आहे.\",\n    \"loginToEnableAIFeature\": \"AI वैशिष्ट्ये फक्त @:appName Cloud मध्ये लॉगिन केल्यानंतरच सक्षम होतात. तुमच्याकडे @:appName खाते नसेल तर 'माय अकाउंट' मध्ये जाऊन साइन अप करा.\",\n    \"llmModel\": \"भाषा मॉडेल\",\n    \"llmModelType\": \"भाषा मॉडेल प्रकार\",\n    \"downloadLLMPrompt\": \"{} डाउनलोड करा\",\n    \"downloadAppFlowyOfflineAI\": \"AI ऑफलाइन पॅकेज डाउनलोड केल्याने तुमच्या डिव्हाइसवर AI चालवता येईल. तुम्हाला पुढे जायचे आहे का?\",\n    \"downloadLLMPromptDetail\": \"{} स्थानिक मॉडेल डाउनलोड करण्यासाठी सुमारे {} संचयन लागेल. तुम्हाला पुढे जायचे आहे का?\",\n    \"downloadBigFilePrompt\": \"डाउनलोड पूर्ण होण्यासाठी सुमारे १० मिनिटे लागू शकतात\",\n    \"downloadAIModelButton\": \"डाउनलोड करा\",\n    \"downloadingModel\": \"डाउनलोड करत आहे\",\n    \"localAILoaded\": \"स्थानिक AI मॉडेल यशस्वीरित्या जोडले गेले आणि वापरण्यास सज्ज आहे\",\n    \"localAIStart\": \"स्थानिक AI सुरू होत आहे. जर हळू वाटत असेल, तर ते बंद करून पुन्हा सुरू करून पहा\",\n    \"localAILoading\": \"स्थानिक AI Chat मॉडेल लोड होत आहे...\",\n    \"localAIStopped\": \"स्थानिक AI थांबले आहे\",\n    \"localAIRunning\": \"स्थानिक AI चालू आहे\",\n    \"localAINotReadyRetryLater\": \"स्थानिक AI सुरू होत आहे, कृपया नंतर पुन्हा प्रयत्न करा\",\n    \"localAIDisabled\": \"तुम्ही स्थानिक AI वापरत आहात, पण ते अकार्यक्षम आहे. कृपया सेटिंग्जमध्ये जाऊन ते सक्षम करा किंवा दुसरे मॉडेल वापरून पहा\",\n    \"localAIInitializing\": \"स्थानिक AI लोड होत आहे. तुमच्या डिव्हाइसवर अवलंबून याला काही मिनिटे लागू शकतात\",\n    \"localAINotReadyTextFieldPrompt\": \"स्थानिक AI लोड होत असताना संपादन करता येणार नाही\",\n    \"failToLoadLocalAI\": \"स्थानिक AI सुरू करण्यात अयशस्वी.\",\n    \"restartLocalAI\": \"पुन्हा सुरू करा\",\n    \"disableLocalAITitle\": \"स्थानिक AI अकार्यक्षम करा\",\n    \"disableLocalAIDescription\": \"तुम्हाला स्थानिक AI अकार्यक्षम करायचा आहे का?\",\n    \"localAIToggleTitle\": \"AppFlowy स्थानिक AI (LAI)\",\n    \"localAIToggleSubTitle\": \"AppFlowy मध्ये अत्याधुनिक स्थानिक AI मॉडेल्स वापरून गोपनीयता आणि सुरक्षा सुनिश्चित करा\",\n    \"offlineAIInstruction1\": \"हे अनुसरा\",\n    \"offlineAIInstruction2\": \"सूचना\",\n    \"offlineAIInstruction3\": \"ऑफलाइन AI सक्षम करण्यासाठी.\",\n    \"offlineAIDownload1\": \"जर तुम्ही AppFlowy AI डाउनलोड केले नसेल, तर कृपया\",\n    \"offlineAIDownload2\": \"डाउनलोड\",\n    \"offlineAIDownload3\": \"करा\",\n    \"activeOfflineAI\": \"सक्रिय\",\n    \"downloadOfflineAI\": \"डाउनलोड करा\",\n    \"openModelDirectory\": \"फोल्डर उघडा\",\n    \"laiNotReady\": \"स्थानिक AI अ‍ॅप योग्य प्रकारे इन्स्टॉल झालेले नाही.\",\n    \"ollamaNotReady\": \"Ollama सर्व्हर तयार नाही.\",\n    \"pleaseFollowThese\": \"कृपया हे अनुसरा\",\n    \"instructions\": \"सूचना\",\n    \"installOllamaLai\": \"Ollama आणि AppFlowy स्थानिक AI सेटअप करण्यासाठी.\",\n    \"modelsMissing\": \"आवश्यक मॉडेल्स सापडत नाहीत.\",\n    \"downloadModel\": \"त्यांना डाउनलोड करण्यासाठी.\"\n  }\n},\n    \"planPage\": {\n  \"menuLabel\": \"योजना\",\n  \"title\": \"दर योजना\",\n  \"planUsage\": {\n    \"title\": \"योजनेचा वापर सारांश\",\n    \"storageLabel\": \"स्टोरेज\",\n    \"storageUsage\": \"{} पैकी {} GB\",\n    \"unlimitedStorageLabel\": \"अमर्यादित स्टोरेज\",\n    \"collaboratorsLabel\": \"सदस्य\",\n    \"collaboratorsUsage\": \"{} पैकी {}\",\n    \"aiResponseLabel\": \"AI प्रतिसाद\",\n    \"aiResponseUsage\": \"{} पैकी {}\",\n    \"unlimitedAILabel\": \"अमर्यादित AI प्रतिसाद\",\n    \"proBadge\": \"प्रो\",\n    \"aiMaxBadge\": \"AI Max\",\n    \"aiOnDeviceBadge\": \"मॅकसाठी ऑन-डिव्हाइस AI\",\n    \"memberProToggle\": \"अधिक सदस्य आणि अमर्यादित AI\",\n    \"aiMaxToggle\": \"अमर्यादित AI आणि प्रगत मॉडेल्सचा प्रवेश\",\n    \"aiOnDeviceToggle\": \"जास्त गोपनीयतेसाठी स्थानिक AI\",\n    \"aiCredit\": {\n      \"title\": \"@:appName AI क्रेडिट जोडा\",\n      \"price\": \"{}\",\n      \"priceDescription\": \"1,000 क्रेडिट्ससाठी\",\n      \"purchase\": \"AI खरेदी करा\",\n      \"info\": \"प्रत्येक कार्यक्षेत्रासाठी 1,000 AI क्रेडिट्स जोडा आणि आपल्या कामाच्या प्रवाहात सानुकूलनीय AI सहजपणे एकत्र करा — अधिक स्मार्ट आणि जलद निकालांसाठी:\",\n      \"infoItemOne\": \"प्रत्येक डेटाबेससाठी 10,000 प्रतिसाद\",\n      \"infoItemTwo\": \"प्रत्येक कार्यक्षेत्रासाठी 1,000 प्रतिसाद\"\n    },\n    \"currentPlan\": {\n      \"bannerLabel\": \"सद्य योजना\",\n      \"freeTitle\": \"फ्री\",\n      \"proTitle\": \"प्रो\",\n      \"teamTitle\": \"टीम\",\n      \"freeInfo\": \"2 सदस्यांपर्यंत वैयक्तिक वापरासाठी उत्तम\",\n      \"proInfo\": \"10 सदस्यांपर्यंत लहान आणि मध्यम टीमसाठी योग्य\",\n      \"teamInfo\": \"सर्व उत्पादनक्षम आणि सुव्यवस्थित टीमसाठी योग्य\",\n      \"upgrade\": \"योजना बदला\",\n      \"canceledInfo\": \"तुमची योजना रद्द करण्यात आली आहे, {} रोजी तुम्हाला फ्री योजनेवर डाऊनग्रेड केले जाईल.\"\n    },\n    \"addons\": {\n      \"title\": \"ऍड-ऑन्स\",\n      \"addLabel\": \"जोडा\",\n      \"activeLabel\": \"जोडले गेले\",\n      \"aiMax\": {\n        \"title\": \"AI Max\",\n        \"description\": \"प्रगत AI मॉडेल्ससह अमर्यादित AI प्रतिसाद आणि दरमहा 50 AI प्रतिमा\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"प्रति वापरकर्ता प्रति महिना (वार्षिक बिलिंग)\"\n      },\n      \"aiOnDevice\": {\n        \"title\": \"मॅकसाठी ऑन-डिव्हाइस AI\",\n        \"description\": \"तुमच्या डिव्हाइसवर Mistral 7B, LLAMA 3 आणि अधिक स्थानिक मॉडेल्स चालवा\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"प्रति वापरकर्ता प्रति महिना (वार्षिक बिलिंग)\",\n        \"recommend\": \"M1 किंवा नवीनतम शिफारस केली जाते\"\n      }\n    },\n    \"deal\": {\n      \"bannerLabel\": \"नववर्षाचे विशेष ऑफर!\",\n      \"title\": \"तुमची टीम वाढवा!\",\n      \"info\": \"Pro आणि Team योजनांवर 10% सूट मिळवा! @:appName AI सह शक्तिशाली नवीन वैशिष्ट्यांसह तुमचे कार्यक्षेत्र अधिक कार्यक्षम बनवा.\",\n      \"viewPlans\": \"योजना पहा\"\n    }\n  }\n},\n    \"billingPage\": {\n  \"menuLabel\": \"बिलिंग\",\n  \"title\": \"बिलिंग\",\n  \"plan\": {\n    \"title\": \"योजना\",\n    \"freeLabel\": \"फ्री\",\n    \"proLabel\": \"प्रो\",\n    \"planButtonLabel\": \"योजना बदला\",\n    \"billingPeriod\": \"बिलिंग कालावधी\",\n    \"periodButtonLabel\": \"कालावधी संपादित करा\"\n  },\n  \"paymentDetails\": {\n    \"title\": \"पेमेंट तपशील\",\n    \"methodLabel\": \"पेमेंट पद्धत\",\n    \"methodButtonLabel\": \"पद्धत संपादित करा\"\n  },\n  \"addons\": {\n    \"title\": \"ऍड-ऑन्स\",\n    \"addLabel\": \"जोडा\",\n    \"removeLabel\": \"काढा\",\n    \"renewLabel\": \"नवीन करा\",\n    \"aiMax\": {\n      \"label\": \"AI Max\",\n      \"description\": \"अमर्यादित AI आणि प्रगत मॉडेल्स अनलॉक करा\",\n      \"activeDescription\": \"पुढील बिलिंग तारीख {} आहे\",\n      \"canceledDescription\": \"AI Max {} पर्यंत उपलब्ध असेल\"\n    },\n    \"aiOnDevice\": {\n      \"label\": \"मॅकसाठी ऑन-डिव्हाइस AI\",\n      \"description\": \"तुमच्या डिव्हाइसवर अमर्यादित ऑन-डिव्हाइस AI अनलॉक करा\",\n      \"activeDescription\": \"पुढील बिलिंग तारीख {} आहे\",\n      \"canceledDescription\": \"मॅकसाठी ऑन-डिव्हाइस AI {} पर्यंत उपलब्ध असेल\"\n    },\n    \"removeDialog\": {\n      \"title\": \"{} काढा\",\n      \"description\": \"तुम्हाला {plan} काढायचे आहे का? तुम्हाला तत्काळ {plan} चे सर्व फिचर्स आणि फायदे वापरण्याचा अधिकार गमावावा लागेल.\"\n    }\n  },\n  \"currentPeriodBadge\": \"सद्य कालावधी\",\n  \"changePeriod\": \"कालावधी बदला\",\n  \"planPeriod\": \"{} कालावधी\",\n  \"monthlyInterval\": \"मासिक\",\n  \"monthlyPriceInfo\": \"प्रति सदस्य, मासिक बिलिंग\",\n  \"annualInterval\": \"वार्षिक\",\n  \"annualPriceInfo\": \"प्रति सदस्य, वार्षिक बिलिंग\"\n},\n    \"comparePlanDialog\": {\n  \"title\": \"योजना तुलना आणि निवड\",\n  \"planFeatures\": \"योजनेची\\nवैशिष्ट्ये\",\n  \"current\": \"सध्याची\",\n  \"actions\": {\n    \"upgrade\": \"अपग्रेड करा\",\n    \"downgrade\": \"डाऊनग्रेड करा\",\n    \"current\": \"सध्याची\"\n  },\n  \"freePlan\": {\n    \"title\": \"फ्री\",\n    \"description\": \"२ सदस्यांपर्यंत व्यक्तींसाठी सर्व काही व्यवस्थित करण्यासाठी\",\n    \"price\": \"{}\",\n    \"priceInfo\": \"सदैव फ्री\"\n  },\n  \"proPlan\": {\n    \"title\": \"प्रो\",\n    \"description\": \"लहान टीम्ससाठी प्रोजेक्ट्स व ज्ञान व्यवस्थापनासाठी\",\n    \"price\": \"{}\",\n    \"priceInfo\": \"प्रति सदस्य प्रति महिना\\nवार्षिक बिलिंग\\n\\n{} मासिक बिलिंगसाठी\"\n  },\n  \"planLabels\": {\n    \"itemOne\": \"वर्कस्पेसेस\",\n    \"itemTwo\": \"सदस्य\",\n    \"itemThree\": \"स्टोरेज\",\n    \"itemFour\": \"रिअल-टाइम सहकार्य\",\n    \"itemFive\": \"मोबाईल अ‍ॅप\",\n    \"itemSix\": \"AI प्रतिसाद\",\n    \"itemSeven\": \"AI प्रतिमा\",\n    \"itemFileUpload\": \"फाइल अपलोड\",\n    \"customNamespace\": \"सानुकूल नेमस्पेस\",\n    \"tooltipSix\": \"‘Lifetime’ म्हणजे या प्रतिसादांची मर्यादा कधीही रीसेट केली जात नाही\",\n    \"intelligentSearch\": \"स्मार्ट शोध\",\n    \"tooltipSeven\": \"तुमच्या कार्यक्षेत्रासाठी URL चा भाग सानुकूलित करू देते\",\n    \"customNamespaceTooltip\": \"सानुकूल प्रकाशित साइट URL\"\n  },\n  \"freeLabels\": {\n    \"itemOne\": \"प्रत्येक वर्कस्पेसवर शुल्क\",\n    \"itemTwo\": \"२ पर्यंत\",\n    \"itemThree\": \"५ GB\",\n    \"itemFour\": \"होय\",\n    \"itemFive\": \"होय\",\n    \"itemSix\": \"१० कायमस्वरूपी\",\n    \"itemSeven\": \"२ कायमस्वरूपी\",\n    \"itemFileUpload\": \"७ MB पर्यंत\",\n    \"intelligentSearch\": \"स्मार्ट शोध\"\n  },\n  \"proLabels\": {\n    \"itemOne\": \"प्रत्येक वर्कस्पेसवर शुल्क\",\n    \"itemTwo\": \"१० पर्यंत\",\n    \"itemThree\": \"अमर्यादित\",\n    \"itemFour\": \"होय\",\n    \"itemFive\": \"होय\",\n    \"itemSix\": \"अमर्यादित\",\n    \"itemSeven\": \"दर महिन्याला १० प्रतिमा\",\n    \"itemFileUpload\": \"अमर्यादित\",\n    \"intelligentSearch\": \"स्मार्ट शोध\"\n  },\n  \"paymentSuccess\": {\n    \"title\": \"तुम्ही आता {} योजनेवर आहात!\",\n    \"description\": \"तुमचे पेमेंट यशस्वीरित्या पूर्ण झाले असून तुमची योजना @:appName {} मध्ये अपग्रेड झाली आहे. तुम्ही 'योजना' पृष्ठावर तपशील पाहू शकता.\"\n  },\n  \"downgradeDialog\": {\n    \"title\": \"तुम्हाला योजना डाऊनग्रेड करायची आहे का?\",\n    \"description\": \"तुमची योजना डाऊनग्रेड केल्यास तुम्ही फ्री योजनेवर परत जाल. काही सदस्यांना या कार्यक्षेत्राचा प्रवेश मिळणार नाही आणि तुम्हाला स्टोरेज मर्यादेप्रमाणे जागा रिकामी करावी लागू शकते.\",\n    \"downgradeLabel\": \"योजना डाऊनग्रेड करा\"\n  }\n},\n    \"cancelSurveyDialog\": {\n  \"title\": \"तुम्ही जात आहात याचे दुःख आहे\",\n  \"description\": \"तुम्ही जात आहात याचे आम्हाला खरोखरच दुःख वाटते. @:appName सुधारण्यासाठी तुमचा अभिप्राय आम्हाला महत्त्वाचा वाटतो. कृपया काही क्षण घेऊन काही प्रश्नांची उत्तरे द्या.\",\n  \"commonOther\": \"इतर\",\n  \"otherHint\": \"तुमचे उत्तर येथे लिहा\",\n  \"questionOne\": {\n    \"question\": \"तुम्ही तुमची @:appName Pro सदस्यता का रद्द केली?\",\n    \"answerOne\": \"खर्च खूप जास्त आहे\",\n    \"answerTwo\": \"वैशिष्ट्ये अपेक्षांनुसार नव्हती\",\n    \"answerThree\": \"यापेक्षा चांगला पर्याय सापडला\",\n    \"answerFour\": \"वापर फारसा केला नाही, त्यामुळे खर्च योग्य वाटला नाही\",\n    \"answerFive\": \"सेवा समस्या किंवा तांत्रिक अडचणी\"\n  },\n  \"questionTwo\": {\n    \"question\": \"भविष्यात @:appName Pro सदस्यता पुन्हा घेण्याची शक्यता किती आहे?\",\n    \"answerOne\": \"खूप शक्यता आहे\",\n    \"answerTwo\": \"काहीशी शक्यता आहे\",\n    \"answerThree\": \"निश्चित नाही\",\n    \"answerFour\": \"अल्प शक्यता\",\n    \"answerFive\": \"एकदम कमी शक्यता\"\n  },\n  \"questionThree\": {\n    \"question\": \"तुमच्या सदस्यत्वादरम्यान कोणते Pro वैशिष्ट्य सर्वात उपयुक्त वाटले?\",\n    \"answerOne\": \"अनेक वापरकर्त्यांशी सहकार्य\",\n    \"answerTwo\": \"लांब कालावधीची आवृत्ती इतिहास\",\n    \"answerThree\": \"अमर्यादित AI प्रतिसाद\",\n    \"answerFour\": \"स्थानिक AI मॉडेल्सचा प्रवेश\"\n  },\n  \"questionFour\": {\n    \"question\": \"@:appName वापरण्याचा तुमचा एकूण अनुभव कसा होता?\",\n    \"answerOne\": \"खूप छान\",\n    \"answerTwo\": \"चांगला\",\n    \"answerThree\": \"सरासरी\",\n    \"answerFour\": \"सरासरीपेक्षा कमी\",\n    \"answerFive\": \"असंतोषजनक\"\n  }\n},\n    \"common\": {\n  \"uploadingFile\": \"फाईल अपलोड होत आहे. कृपया अ‍ॅप बंद करू नका\",\n  \"uploadNotionSuccess\": \"तुमची Notion zip फाईल यशस्वीरित्या अपलोड झाली आहे. आयात पूर्ण झाल्यानंतर तुम्हाला पुष्टीकरण ईमेल मिळेल\",\n  \"reset\": \"रीसेट करा\"\n},\n    \"menu\": {\n  \"appearance\": \"दृश्यरूप\",\n  \"language\": \"भाषा\",\n  \"user\": \"वापरकर्ता\",\n  \"files\": \"फाईल्स\",\n  \"notifications\": \"सूचना\",\n  \"open\": \"सेटिंग्ज उघडा\",\n  \"logout\": \"लॉगआउट\",\n  \"logoutPrompt\": \"तुम्हाला नक्की लॉगआउट करायचे आहे का?\",\n  \"selfEncryptionLogoutPrompt\": \"तुम्हाला खात्रीने लॉगआउट करायचे आहे का? कृपया खात्री करा की तुम्ही एनक्रिप्शन गुप्तकी कॉपी केली आहे\",\n  \"syncSetting\": \"सिंक्रोनायझेशन सेटिंग\",\n  \"cloudSettings\": \"क्लाऊड सेटिंग्ज\",\n  \"enableSync\": \"सिंक्रोनायझेशन सक्षम करा\",\n  \"enableSyncLog\": \"सिंक लॉगिंग सक्षम करा\",\n  \"enableSyncLogWarning\": \"सिंक समस्यांचे निदान करण्यात मदतीसाठी धन्यवाद. हे तुमचे डॉक्युमेंट संपादन स्थानिक फाईलमध्ये लॉग करेल. कृपया सक्षम केल्यानंतर अ‍ॅप बंद करून पुन्हा उघडा\",\n  \"enableEncrypt\": \"डेटा एन्क्रिप्ट करा\",\n  \"cloudURL\": \"बेस URL\",\n  \"webURL\": \"वेब URL\",\n  \"invalidCloudURLScheme\": \"अवैध स्कीम\",\n  \"cloudServerType\": \"क्लाऊड सर्व्हर\",\n  \"cloudServerTypeTip\": \"कृपया लक्षात घ्या की क्लाऊड सर्व्हर बदलल्यास तुम्ही सध्या लॉगिन केलेले खाते लॉगआउट होऊ शकते\",\n  \"cloudLocal\": \"स्थानिक\",\n  \"cloudAppFlowy\": \"@:appName Cloud\",\n  \"cloudAppFlowySelfHost\": \"@:appName क्लाऊड सेल्फ-होस्टेड\",\n  \"appFlowyCloudUrlCanNotBeEmpty\": \"क्लाऊड URL रिकामा असू शकत नाही\",\n  \"clickToCopy\": \"क्लिपबोर्डवर कॉपी करा\",\n  \"selfHostStart\": \"जर तुमच्याकडे सर्व्हर नसेल, तर कृपया हे पाहा\",\n  \"selfHostContent\": \"दस्तऐवज\",\n  \"selfHostEnd\": \"तुमचा स्वतःचा सर्व्हर कसा होस्ट करावा यासाठी मार्गदर्शनासाठी\",\n  \"pleaseInputValidURL\": \"कृपया वैध URL टाका\",\n  \"changeUrl\": \"सेल्फ-होस्टेड URL {} मध्ये बदला\",\n  \"cloudURLHint\": \"तुमच्या सर्व्हरचा बेस URL टाका\",\n  \"webURLHint\": \"तुमच्या वेब सर्व्हरचा बेस URL टाका\",\n  \"cloudWSURL\": \"वेबसॉकेट URL\",\n  \"cloudWSURLHint\": \"तुमच्या सर्व्हरचा वेबसॉकेट पत्ता टाका\",\n  \"restartApp\": \"अ‍ॅप रीस्टार्ट करा\",\n  \"restartAppTip\": \"बदल प्रभावी होण्यासाठी अ‍ॅप रीस्टार्ट करा. कृपया लक्षात घ्या की यामुळे सध्याचे खाते लॉगआउट होऊ शकते.\",\n  \"changeServerTip\": \"सर्व्हर बदलल्यानंतर, बदल लागू करण्यासाठी तुम्हाला 'रीस्टार्ट' बटणावर क्लिक करणे आवश्यक आहे\",\n  \"enableEncryptPrompt\": \"तुमचा डेटा सुरक्षित करण्यासाठी एन्क्रिप्शन सक्रिय करा. ही गुप्तकी सुरक्षित ठेवा; एकदा सक्षम केल्यावर ती बंद करता येणार नाही. जर हरवली तर तुमचा डेटा पुन्हा मिळवता येणार नाही. कॉपी करण्यासाठी क्लिक करा\",\n  \"inputEncryptPrompt\": \"कृपया खालीलसाठी तुमची एनक्रिप्शन गुप्तकी टाका:\",\n  \"clickToCopySecret\": \"गुप्तकी कॉपी करण्यासाठी क्लिक करा\",\n  \"configServerSetting\": \"तुमच्या सर्व्हर सेटिंग्ज कॉन्फिगर करा\",\n  \"configServerGuide\": \"`Quick Start` निवडल्यानंतर, `Settings` → \\\"Cloud Settings\\\" मध्ये जा आणि तुमचा सेल्फ-होस्टेड सर्व्हर कॉन्फिगर करा.\",\n  \"inputTextFieldHint\": \"तुमची गुप्तकी\",\n  \"historicalUserList\": \"वापरकर्ता लॉगिन इतिहास\",\n  \"historicalUserListTooltip\": \"ही यादी तुमची अनामिक खाती दर्शवते. तपशील पाहण्यासाठी खात्यावर क्लिक करा. अनामिक खाती 'सुरुवात करा' बटणावर क्लिक करून तयार केली जातात\",\n  \"openHistoricalUser\": \"अनामिक खाते उघडण्यासाठी क्लिक करा\",\n  \"customPathPrompt\": \"@:appName डेटा फोल्डर Google Drive सारख्या क्लाऊड-सिंक फोल्डरमध्ये साठवणे धोकादायक ठरू शकते. फोल्डरमधील डेटाबेस अनेक ठिकाणांवरून एकाच वेळी ऍक्सेस/संपादित केल्यास डेटा सिंक समस्या किंवा भ्रष्ट होण्याची शक्यता असते.\",\n  \"importAppFlowyData\": \"बाह्य @:appName फोल्डरमधून डेटा आयात करा\",\n  \"importingAppFlowyDataTip\": \"डेटा आयात सुरू आहे. कृपया अ‍ॅप बंद करू नका\",\n  \"importAppFlowyDataDescription\": \"बाह्य @:appName फोल्डरमधून डेटा कॉपी करून सध्याच्या AppFlowy डेटा फोल्डरमध्ये आयात करा\",\n  \"importSuccess\": \"@:appName डेटा फोल्डर यशस्वीरित्या आयात झाला\",\n  \"importFailed\": \"@:appName डेटा फोल्डर आयात करण्यात अयशस्वी\",\n  \"importGuide\": \"अधिक माहितीसाठी, कृपया संदर्भित दस्तऐवज पहा\"\n},\n  \"notifications\": {\n  \"enableNotifications\": {\n    \"label\": \"सूचना सक्षम करा\",\n    \"hint\": \"स्थानिक सूचना दिसू नयेत यासाठी बंद करा.\"\n  },\n  \"showNotificationsIcon\": {\n    \"label\": \"सूचना चिन्ह दाखवा\",\n    \"hint\": \"साइडबारमध्ये सूचना चिन्ह लपवण्यासाठी बंद करा.\"\n  },\n  \"archiveNotifications\": {\n    \"allSuccess\": \"सर्व सूचना यशस्वीरित्या संग्रहित केल्या\",\n    \"success\": \"सूचना यशस्वीरित्या संग्रहित केली\"\n  },\n  \"markAsReadNotifications\": {\n    \"allSuccess\": \"सर्व वाचलेल्या म्हणून चिन्हांकित केल्या\",\n    \"success\": \"वाचलेले म्हणून चिन्हांकित केले\"\n  },\n  \"action\": {\n    \"markAsRead\": \"वाचलेले म्हणून चिन्हांकित करा\",\n    \"multipleChoice\": \"अधिक निवडा\",\n    \"archive\": \"संग्रहित करा\"\n  },\n  \"settings\": {\n    \"settings\": \"सेटिंग्ज\",\n    \"markAllAsRead\": \"सर्व वाचलेले म्हणून चिन्हांकित करा\",\n    \"archiveAll\": \"सर्व संग्रहित करा\"\n  },\n  \"emptyInbox\": {\n    \"title\": \"इनबॉक्स झिरो!\",\n    \"description\": \"इथे सूचना मिळवण्यासाठी रिमाइंडर सेट करा.\"\n  },\n  \"emptyUnread\": {\n    \"title\": \"कोणतीही न वाचलेली सूचना नाही\",\n    \"description\": \"तुम्ही सर्व वाचले आहे!\"\n  },\n  \"emptyArchived\": {\n    \"title\": \"कोणतीही संग्रहित सूचना नाही\",\n    \"description\": \"संग्रहित सूचना इथे दिसतील.\"\n  },\n  \"tabs\": {\n    \"inbox\": \"इनबॉक्स\",\n    \"unread\": \"न वाचलेले\",\n    \"archived\": \"संग्रहित\"\n  },\n  \"refreshSuccess\": \"सूचना यशस्वीरित्या रीफ्रेश केल्या\",\n  \"titles\": {\n    \"notifications\": \"सूचना\",\n    \"reminder\": \"रिमाइंडर\"\n  }\n},\n    \"appearance\": {\n  \"resetSetting\": \"रीसेट\",\n  \"fontFamily\": {\n    \"label\": \"फॉन्ट फॅमिली\",\n    \"search\": \"शोध\",\n    \"defaultFont\": \"सिस्टम\"\n  },\n  \"themeMode\": {\n    \"label\": \"थीम मोड\",\n    \"light\": \"लाइट मोड\",\n    \"dark\": \"डार्क मोड\",\n    \"system\": \"सिस्टमशी जुळवा\"\n  },\n  \"fontScaleFactor\": \"फॉन्ट स्केल घटक\",\n  \"displaySize\": \"डिस्प्ले आकार\",\n  \"documentSettings\": {\n    \"cursorColor\": \"डॉक्युमेंट कर्सरचा रंग\",\n    \"selectionColor\": \"डॉक्युमेंट निवडीचा रंग\",\n    \"width\": \"डॉक्युमेंटची रुंदी\",\n    \"changeWidth\": \"बदला\",\n    \"pickColor\": \"रंग निवडा\",\n    \"colorShade\": \"रंगाची छटा\",\n    \"opacity\": \"अपारदर्शकता\",\n    \"hexEmptyError\": \"Hex रंग रिकामा असू शकत नाही\",\n    \"hexLengthError\": \"Hex व्हॅल्यू 6 अंकांची असावी\",\n    \"hexInvalidError\": \"अवैध Hex व्हॅल्यू\",\n    \"opacityEmptyError\": \"अपारदर्शकता रिकामी असू शकत नाही\",\n    \"opacityRangeError\": \"अपारदर्शकता 1 ते 100 दरम्यान असावी\",\n    \"app\": \"अ‍ॅप\",\n    \"flowy\": \"Flowy\",\n    \"apply\": \"लागू करा\"\n  },\n  \"layoutDirection\": {\n    \"label\": \"लेआउट दिशा\",\n    \"hint\": \"तुमच्या स्क्रीनवरील कंटेंटचा प्रवाह नियंत्रित करा — डावीकडून उजवीकडे किंवा उजवीकडून डावीकडे.\",\n    \"ltr\": \"LTR\",\n    \"rtl\": \"RTL\"\n  },\n  \"textDirection\": {\n    \"label\": \"मूलभूत मजकूर दिशा\",\n    \"hint\": \"मजकूर डावीकडून सुरू व्हावा की उजवीकडून याचे पूर्वनिर्धारित निर्धारण करा.\",\n    \"ltr\": \"LTR\",\n    \"rtl\": \"RTL\",\n    \"auto\": \"स्वयं\",\n    \"fallback\": \"लेआउट दिशेशी जुळवा\"\n  },\n  \"themeUpload\": {\n    \"button\": \"अपलोड\",\n    \"uploadTheme\": \"थीम अपलोड करा\",\n    \"description\": \"खालील बटण वापरून तुमची स्वतःची @:appName थीम अपलोड करा.\",\n    \"loading\": \"कृपया प्रतीक्षा करा. आम्ही तुमची थीम पडताळत आहोत आणि अपलोड करत आहोत...\",\n    \"uploadSuccess\": \"तुमची थीम यशस्वीरित्या अपलोड झाली आहे\",\n    \"deletionFailure\": \"थीम हटवण्यात अयशस्वी. कृपया ती मॅन्युअली हटवून पाहा.\",\n    \"filePickerDialogTitle\": \".flowy_plugin फाईल निवडा\",\n    \"urlUploadFailure\": \"URL उघडण्यात अयशस्वी: {}\"\n  },\n  \"theme\": \"थीम\",\n  \"builtInsLabel\": \"अंतर्गत थीम्स\",\n  \"pluginsLabel\": \"प्लगइन्स\",\n  \"dateFormat\": {\n    \"label\": \"दिनांक फॉरमॅट\",\n    \"local\": \"स्थानिक\",\n    \"us\": \"US\",\n    \"iso\": \"ISO\",\n    \"friendly\": \"अनौपचारिक\",\n    \"dmy\": \"D/M/Y\"\n  },\n  \"timeFormat\": {\n    \"label\": \"वेळ फॉरमॅट\",\n    \"twelveHour\": \"१२ तास\",\n    \"twentyFourHour\": \"२४ तास\"\n  },\n  \"showNamingDialogWhenCreatingPage\": \"पृष्ठ तयार करताना नाव विचारणारा डायलॉग दाखवा\",\n  \"enableRTLToolbarItems\": \"RTL टूलबार आयटम्स सक्षम करा\",\n  \"members\": {\n    \"title\": \"सदस्य सेटिंग्ज\",\n    \"inviteMembers\": \"सदस्यांना आमंत्रण द्या\",\n    \"inviteHint\": \"ईमेलद्वारे आमंत्रण द्या\",\n    \"sendInvite\": \"आमंत्रण पाठवा\",\n    \"copyInviteLink\": \"आमंत्रण दुवा कॉपी करा\",\n    \"label\": \"सदस्य\",\n    \"user\": \"वापरकर्ता\",\n    \"role\": \"भूमिका\",\n    \"removeFromWorkspace\": \"वर्कस्पेसमधून काढा\",\n    \"removeFromWorkspaceSuccess\": \"वर्कस्पेसमधून यशस्वीरित्या काढले\",\n    \"removeFromWorkspaceFailed\": \"वर्कस्पेसमधून काढण्यात अयशस्वी\",\n    \"owner\": \"मालक\",\n    \"guest\": \"अतिथी\",\n    \"member\": \"सदस्य\",\n    \"memberHintText\": \"सदस्य पृष्ठे वाचू व संपादित करू शकतो\",\n    \"guestHintText\": \"अतिथी पृष्ठे वाचू शकतो, प्रतिक्रिया देऊ शकतो, टिप्पणी करू शकतो, आणि परवानगी असल्यास काही पृष्ठे संपादित करू शकतो.\",\n    \"emailInvalidError\": \"अवैध ईमेल, कृपया तपासा व पुन्हा प्रयत्न करा\",\n    \"emailSent\": \"ईमेल पाठवला गेला आहे, कृपया इनबॉक्स तपासा\",\n    \"members\": \"सदस्य\",\n    \"membersCount\": {\n      \"zero\": \"{} सदस्य\",\n      \"one\": \"{} सदस्य\",\n      \"other\": \"{} सदस्य\"\n    },\n    \"inviteFailedDialogTitle\": \"आमंत्रण पाठवण्यात अयशस्वी\",\n    \"inviteFailedMemberLimit\": \"सदस्य मर्यादा गाठली आहे, अधिक सदस्यांसाठी कृपया अपग्रेड करा.\",\n    \"inviteFailedMemberLimitMobile\": \"तुमच्या वर्कस्पेसने सदस्य मर्यादा गाठली आहे.\",\n    \"memberLimitExceeded\": \"सदस्य मर्यादा गाठली आहे, अधिक सदस्य आमंत्रित करण्यासाठी कृपया \",\n    \"memberLimitExceededUpgrade\": \"अपग्रेड करा\",\n    \"memberLimitExceededPro\": \"सदस्य मर्यादा गाठली आहे, अधिक सदस्य आवश्यक असल्यास संपर्क करा\",\n    \"memberLimitExceededProContact\": \"support@appflowy.io\",\n    \"failedToAddMember\": \"सदस्य जोडण्यात अयशस्वी\",\n    \"addMemberSuccess\": \"सदस्य यशस्वीरित्या जोडला गेला\",\n    \"removeMember\": \"सदस्य काढा\",\n    \"areYouSureToRemoveMember\": \"तुम्हाला हा सदस्य काढायचा आहे का?\",\n    \"inviteMemberSuccess\": \"आमंत्रण यशस्वीरित्या पाठवले गेले\",\n    \"failedToInviteMember\": \"सदस्य आमंत्रित करण्यात अयशस्वी\",\n    \"workspaceMembersError\": \"अरे! काहीतरी चूक झाली आहे\",\n    \"workspaceMembersErrorDescription\": \"आम्ही सध्या सदस्यांची यादी लोड करू शकत नाही. कृपया नंतर पुन्हा प्रयत्न करा\"\n  }\n},\n    \"files\": {\n  \"copy\": \"कॉपी करा\",\n  \"defaultLocation\": \"फाईल्स आणि डेटाचा संचय स्थान\",\n  \"exportData\": \"तुमचा डेटा निर्यात करा\",\n  \"doubleTapToCopy\": \"पथ कॉपी करण्यासाठी दोनदा टॅप करा\",\n  \"restoreLocation\": \"@:appName चे मूळ स्थान पुनर्संचयित करा\",\n  \"customizeLocation\": \"इतर फोल्डर उघडा\",\n  \"restartApp\": \"बदल लागू करण्यासाठी कृपया अ‍ॅप रीस्टार्ट करा.\",\n  \"exportDatabase\": \"डेटाबेस निर्यात करा\",\n  \"selectFiles\": \"निर्यात करण्यासाठी फाईल्स निवडा\",\n  \"selectAll\": \"सर्व निवडा\",\n  \"deselectAll\": \"सर्व निवड रद्द करा\",\n  \"createNewFolder\": \"नवीन फोल्डर तयार करा\",\n  \"createNewFolderDesc\": \"तुमचा डेटा कुठे साठवायचा हे सांगा\",\n  \"defineWhereYourDataIsStored\": \"तुमचा डेटा कुठे साठवला जातो हे ठरवा\",\n  \"open\": \"उघडा\",\n  \"openFolder\": \"आधीक फोल्डर उघडा\",\n  \"openFolderDesc\": \"तुमच्या विद्यमान @:appName फोल्डरमध्ये वाचन व लेखन करा\",\n  \"folderHintText\": \"फोल्डरचे नाव\",\n  \"location\": \"नवीन फोल्डर तयार करत आहे\",\n  \"locationDesc\": \"तुमच्या @:appName डेटासाठी नाव निवडा\",\n  \"browser\": \"ब्राउझ करा\",\n  \"create\": \"तयार करा\",\n  \"set\": \"सेट करा\",\n  \"folderPath\": \"फोल्डर साठवण्याचा मार्ग\",\n  \"locationCannotBeEmpty\": \"मार्ग रिकामा असू शकत नाही\",\n  \"pathCopiedSnackbar\": \"फाईल संचय मार्ग क्लिपबोर्डवर कॉपी केला!\",\n  \"changeLocationTooltips\": \"डेटा डिरेक्टरी बदला\",\n  \"change\": \"बदला\",\n  \"openLocationTooltips\": \"इतर डेटा डिरेक्टरी उघडा\",\n  \"openCurrentDataFolder\": \"सध्याची डेटा डिरेक्टरी उघडा\",\n  \"recoverLocationTooltips\": \"@:appName च्या मूळ डेटा डिरेक्टरीवर रीसेट करा\",\n  \"exportFileSuccess\": \"फाईल यशस्वीरित्या निर्यात झाली!\",\n  \"exportFileFail\": \"फाईल निर्यात करण्यात अयशस्वी!\",\n  \"export\": \"निर्यात करा\",\n  \"clearCache\": \"कॅशे साफ करा\",\n  \"clearCacheDesc\": \"प्रतिमा लोड होत नाहीत किंवा फॉन्ट अयोग्यरित्या दिसत असल्यास, कॅशे साफ करून पाहा. ही क्रिया तुमचा वापरकर्ता डेटा हटवणार नाही.\",\n  \"areYouSureToClearCache\": \"तुम्हाला नक्की कॅशे साफ करायची आहे का?\",\n  \"clearCacheSuccess\": \"कॅशे यशस्वीरित्या साफ झाली!\"\n},\n    \"user\": {\n  \"name\": \"नाव\",\n  \"email\": \"ईमेल\",\n  \"tooltipSelectIcon\": \"चिन्ह निवडा\",\n  \"selectAnIcon\": \"चिन्ह निवडा\",\n  \"pleaseInputYourOpenAIKey\": \"कृपया तुमची AI की टाका\",\n  \"clickToLogout\": \"सध्याचा वापरकर्ता लॉगआउट करण्यासाठी क्लिक करा\"\n},\n    \"mobile\": {\n  \"personalInfo\": \"वैयक्तिक माहिती\",\n  \"username\": \"वापरकर्तानाव\",\n  \"usernameEmptyError\": \"वापरकर्तानाव रिकामे असू शकत नाही\",\n  \"about\": \"विषयी\",\n  \"pushNotifications\": \"पुश सूचना\",\n  \"support\": \"सपोर्ट\",\n  \"joinDiscord\": \"Discord मध्ये सहभागी व्हा\",\n  \"privacyPolicy\": \"गोपनीयता धोरण\",\n  \"userAgreement\": \"वापरकर्ता करार\",\n  \"termsAndConditions\": \"अटी व शर्ती\",\n  \"userprofileError\": \"वापरकर्ता प्रोफाइल लोड करण्यात अयशस्वी\",\n  \"userprofileErrorDescription\": \"कृपया लॉगआउट करून पुन्हा लॉगिन करा आणि त्रुटी अजूनही येते का ते पहा.\",\n  \"selectLayout\": \"लेआउट निवडा\",\n  \"selectStartingDay\": \"सप्ताहाचा प्रारंभ दिवस निवडा\",\n  \"version\": \"आवृत्ती\"\n},\n  \"grid\": {\n  \"deleteView\": \"तुम्हाला हे दृश्य हटवायचे आहे का?\",\n  \"createView\": \"नवीन\",\n  \"title\": {\n    \"placeholder\": \"नाव नाही\"\n  },\n  \"settings\": {\n    \"filter\": \"फिल्टर\",\n    \"sort\": \"क्रमवारी\",\n    \"sortBy\": \"यावरून क्रमवारी लावा\",\n    \"properties\": \"गुणधर्म\",\n    \"reorderPropertiesTooltip\": \"गुणधर्मांचे स्थान बदला\",\n    \"group\": \"समूह\",\n    \"addFilter\": \"फिल्टर जोडा\",\n    \"deleteFilter\": \"फिल्टर हटवा\",\n    \"filterBy\": \"यावरून फिल्टर करा\",\n    \"typeAValue\": \"मूल्य लिहा...\",\n    \"layout\": \"लेआउट\",\n    \"compactMode\": \"कॉम्पॅक्ट मोड\",\n    \"databaseLayout\": \"लेआउट\",\n    \"viewList\": {\n      \"zero\": \"० दृश्ये\",\n      \"one\": \"{count} दृश्य\",\n      \"other\": \"{count} दृश्ये\"\n    },\n    \"editView\": \"दृश्य संपादित करा\",\n    \"boardSettings\": \"बोर्ड सेटिंग\",\n    \"calendarSettings\": \"कॅलेंडर सेटिंग\",\n    \"createView\": \"नवीन दृश्य\",\n    \"duplicateView\": \"दृश्याची प्रत बनवा\",\n    \"deleteView\": \"दृश्य हटवा\",\n    \"numberOfVisibleFields\": \"{} दर्शविले\"\n  },\n  \"filter\": {\n    \"empty\": \"कोणतेही सक्रिय फिल्टर नाहीत\",\n    \"addFilter\": \"फिल्टर जोडा\",\n    \"cannotFindCreatableField\": \"फिल्टर करण्यासाठी योग्य फील्ड सापडले नाही\",\n    \"conditon\": \"अट\",\n    \"where\": \"जिथे\"\n  },\n  \"textFilter\": {\n    \"contains\": \"अंतर्भूत आहे\",\n    \"doesNotContain\": \"अंतर्भूत नाही\",\n    \"endsWith\": \"याने समाप्त होते\",\n    \"startWith\": \"याने सुरू होते\",\n    \"is\": \"आहे\",\n    \"isNot\": \"नाही\",\n    \"isEmpty\": \"रिकामे आहे\",\n    \"isNotEmpty\": \"रिकामे नाही\",\n    \"choicechipPrefix\": {\n      \"isNot\": \"नाही\",\n      \"startWith\": \"याने सुरू होते\",\n      \"endWith\": \"याने समाप्त होते\",\n      \"isEmpty\": \"रिकामे आहे\",\n      \"isNotEmpty\": \"रिकामे नाही\"\n    }\n  },\n  \"checkboxFilter\": {\n    \"isChecked\": \"निवडलेले आहे\",\n    \"isUnchecked\": \"निवडलेले नाही\",\n    \"choicechipPrefix\": {\n      \"is\": \"आहे\"\n    }\n  },\n  \"checklistFilter\": {\n    \"isComplete\": \"पूर्ण झाले आहे\",\n    \"isIncomplted\": \"अपूर्ण आहे\"\n  },\n    \"selectOptionFilter\": {\n  \"is\": \"आहे\",\n  \"isNot\": \"नाही\",\n  \"contains\": \"अंतर्भूत आहे\",\n  \"doesNotContain\": \"अंतर्भूत नाही\",\n  \"isEmpty\": \"रिकामे आहे\",\n  \"isNotEmpty\": \"रिकामे नाही\"\n},\n\"dateFilter\": {\n  \"is\": \"या दिवशी आहे\",\n  \"before\": \"पूर्वी आहे\",\n  \"after\": \"नंतर आहे\",\n  \"onOrBefore\": \"या दिवशी किंवा त्याआधी आहे\",\n  \"onOrAfter\": \"या दिवशी किंवा त्यानंतर आहे\",\n  \"between\": \"दरम्यान आहे\",\n  \"empty\": \"रिकामे आहे\",\n  \"notEmpty\": \"रिकामे नाही\",\n  \"startDate\": \"सुरुवातीची तारीख\",\n  \"endDate\": \"शेवटची तारीख\",\n  \"choicechipPrefix\": {\n    \"before\": \"पूर्वी\",\n    \"after\": \"नंतर\",\n    \"between\": \"दरम्यान\",\n    \"onOrBefore\": \"या दिवशी किंवा त्याआधी\",\n    \"onOrAfter\": \"या दिवशी किंवा त्यानंतर\",\n    \"isEmpty\": \"रिकामे आहे\",\n    \"isNotEmpty\": \"रिकामे नाही\"\n  }\n},\n\"numberFilter\": {\n  \"equal\": \"बरोबर आहे\",\n  \"notEqual\": \"बरोबर नाही\",\n  \"lessThan\": \"पेक्षा कमी आहे\",\n  \"greaterThan\": \"पेक्षा जास्त आहे\",\n  \"lessThanOrEqualTo\": \"किंवा कमी आहे\",\n  \"greaterThanOrEqualTo\": \"किंवा जास्त आहे\",\n  \"isEmpty\": \"रिकामे आहे\",\n  \"isNotEmpty\": \"रिकामे नाही\"\n},\n\"field\": {\n  \"label\": \"गुणधर्म\",\n  \"hide\": \"गुणधर्म लपवा\",\n  \"show\": \"गुणधर्म दर्शवा\",\n  \"insertLeft\": \"डावीकडे जोडा\",\n  \"insertRight\": \"उजवीकडे जोडा\",\n  \"duplicate\": \"प्रत बनवा\",\n  \"delete\": \"हटवा\",\n  \"wrapCellContent\": \"पाठ लपेटा\",\n  \"clear\": \"सेल्स रिकामे करा\",\n  \"switchPrimaryFieldTooltip\": \"प्राथमिक फील्डचा प्रकार बदलू शकत नाही\",\n  \"textFieldName\": \"मजकूर\",\n  \"checkboxFieldName\": \"चेकबॉक्स\",\n  \"dateFieldName\": \"तारीख\",\n  \"updatedAtFieldName\": \"शेवटचे अपडेट\",\n  \"createdAtFieldName\": \"तयार झाले\",\n  \"numberFieldName\": \"संख्या\",\n  \"singleSelectFieldName\": \"सिंगल सिलेक्ट\",\n  \"multiSelectFieldName\": \"मल्टीसिलेक्ट\",\n  \"urlFieldName\": \"URL\",\n  \"checklistFieldName\": \"चेकलिस्ट\",\n  \"relationFieldName\": \"संबंध\",\n  \"summaryFieldName\": \"AI सारांश\",\n  \"timeFieldName\": \"वेळ\",\n  \"mediaFieldName\": \"फाईल्स आणि मीडिया\",\n  \"translateFieldName\": \"AI भाषांतर\",\n  \"translateTo\": \"मध्ये भाषांतर करा\",\n  \"numberFormat\": \"संख्या स्वरूप\",\n  \"dateFormat\": \"तारीख स्वरूप\",\n  \"includeTime\": \"वेळ जोडा\",\n  \"isRange\": \"शेवटची तारीख\",\n  \"dateFormatFriendly\": \"महिना दिवस, वर्ष\",\n  \"dateFormatISO\": \"वर्ष-महिना-दिनांक\",\n  \"dateFormatLocal\": \"महिना/दिवस/वर्ष\",\n  \"dateFormatUS\": \"वर्ष/महिना/दिवस\",\n  \"dateFormatDayMonthYear\": \"दिवस/महिना/वर्ष\",\n  \"timeFormat\": \"वेळ स्वरूप\",\n  \"invalidTimeFormat\": \"अवैध स्वरूप\",\n  \"timeFormatTwelveHour\": \"१२ तास\",\n  \"timeFormatTwentyFourHour\": \"२४ तास\",\n  \"clearDate\": \"तारीख हटवा\",\n  \"dateTime\": \"तारीख व वेळ\",\n  \"startDateTime\": \"सुरुवातीची तारीख व वेळ\",\n  \"endDateTime\": \"शेवटची तारीख व वेळ\",\n  \"failedToLoadDate\": \"तारीख मूल्य लोड करण्यात अयशस्वी\",\n  \"selectTime\": \"वेळ निवडा\",\n  \"selectDate\": \"तारीख निवडा\",\n  \"visibility\": \"दृश्यता\",\n  \"propertyType\": \"गुणधर्माचा प्रकार\",\n  \"addSelectOption\": \"पर्याय जोडा\",\n  \"typeANewOption\": \"नवीन पर्याय लिहा\",\n  \"optionTitle\": \"पर्याय\",\n  \"addOption\": \"पर्याय जोडा\",\n  \"editProperty\": \"गुणधर्म संपादित करा\",\n  \"newProperty\": \"नवीन गुणधर्म\",\n  \"openRowDocument\": \"पृष्ठ म्हणून उघडा\",\n  \"deleteFieldPromptMessage\": \"तुम्हाला खात्री आहे का? हा गुणधर्म आणि त्याचा डेटा हटवला जाईल\",\n  \"clearFieldPromptMessage\": \"तुम्हाला खात्री आहे का? या कॉलममधील सर्व सेल्स रिकामे होतील\",\n  \"newColumn\": \"नवीन कॉलम\",\n  \"format\": \"स्वरूप\",\n  \"reminderOnDateTooltip\": \"या सेलमध्ये अनुस्मारक शेड्यूल केले आहे\",\n  \"optionAlreadyExist\": \"पर्याय आधीच अस्तित्वात आहे\"\n},\n    \"rowPage\": {\n  \"newField\": \"नवीन फील्ड जोडा\",\n  \"fieldDragElementTooltip\": \"मेनू उघडण्यासाठी क्लिक करा\",\n  \"showHiddenFields\": {\n    \"one\": \"{count} लपलेले फील्ड दाखवा\",\n    \"many\": \"{count} लपलेली फील्ड दाखवा\",\n    \"other\": \"{count} लपलेली फील्ड दाखवा\"\n  },\n  \"hideHiddenFields\": {\n    \"one\": \"{count} लपलेले फील्ड लपवा\",\n    \"many\": \"{count} लपलेली फील्ड लपवा\",\n    \"other\": \"{count} लपलेली फील्ड लपवा\"\n  },\n  \"openAsFullPage\": \"पूर्ण पृष्ठ म्हणून उघडा\",\n  \"moreRowActions\": \"अधिक पंक्ती क्रिया\"\n},\n\"sort\": {\n  \"ascending\": \"चढत्या क्रमाने\",\n  \"descending\": \"उतरत्या क्रमाने\",\n  \"by\": \"द्वारे\",\n  \"empty\": \"सक्रिय सॉर्ट्स नाहीत\",\n  \"cannotFindCreatableField\": \"सॉर्टसाठी योग्य फील्ड सापडले नाही\",\n  \"deleteAllSorts\": \"सर्व सॉर्ट्स हटवा\",\n  \"addSort\": \"सॉर्ट जोडा\",\n  \"sortsActive\": \"सॉर्टिंग करत असताना {intention} करू शकत नाही\",\n  \"removeSorting\": \"या दृश्यातील सर्व सॉर्ट्स हटवायचे आहेत का?\",\n  \"fieldInUse\": \"या फील्डवर आधीच सॉर्टिंग चालू आहे\"\n},\n\"row\": {\n  \"label\": \"पंक्ती\",\n  \"duplicate\": \"प्रत बनवा\",\n  \"delete\": \"हटवा\",\n  \"titlePlaceholder\": \"शीर्षक नाही\",\n  \"textPlaceholder\": \"रिक्त\",\n  \"copyProperty\": \"गुणधर्म क्लिपबोर्डवर कॉपी केला\",\n  \"count\": \"संख्या\",\n  \"newRow\": \"नवीन पंक्ती\",\n  \"loadMore\": \"अधिक लोड करा\",\n  \"action\": \"क्रिया\",\n  \"add\": \"खाली जोडा वर क्लिक करा\",\n  \"drag\": \"हलवण्यासाठी ड्रॅग करा\",\n  \"deleteRowPrompt\": \"तुम्हाला खात्री आहे का की ही पंक्ती हटवायची आहे? ही क्रिया पूर्ववत करता येणार नाही.\",\n  \"deleteCardPrompt\": \"तुम्हाला खात्री आहे का की हे कार्ड हटवायचे आहे? ही क्रिया पूर्ववत करता येणार नाही.\",\n  \"dragAndClick\": \"हलवण्यासाठी ड्रॅग करा, मेनू उघडण्यासाठी क्लिक करा\",\n  \"insertRecordAbove\": \"वर रेकॉर्ड जोडा\",\n  \"insertRecordBelow\": \"खाली रेकॉर्ड जोडा\",\n  \"noContent\": \"माहिती नाही\",\n  \"reorderRowDescription\": \"पंक्तीचे पुन्हा क्रमांकन\",\n  \"createRowAboveDescription\": \"वर पंक्ती तयार करा\",\n  \"createRowBelowDescription\": \"खाली पंक्ती जोडा\"\n},\n\"selectOption\": {\n  \"create\": \"तयार करा\",\n  \"purpleColor\": \"जांभळा\",\n  \"pinkColor\": \"गुलाबी\",\n  \"lightPinkColor\": \"फिकट गुलाबी\",\n  \"orangeColor\": \"नारंगी\",\n  \"yellowColor\": \"पिवळा\",\n  \"limeColor\": \"लिंबू\",\n  \"greenColor\": \"हिरवा\",\n  \"aquaColor\": \"आक्वा\",\n  \"blueColor\": \"निळा\",\n  \"deleteTag\": \"टॅग हटवा\",\n  \"colorPanelTitle\": \"रंग\",\n  \"panelTitle\": \"पर्याय निवडा किंवा नवीन तयार करा\",\n  \"searchOption\": \"पर्याय शोधा\",\n  \"searchOrCreateOption\": \"पर्याय शोधा किंवा तयार करा\",\n  \"createNew\": \"नवीन तयार करा\",\n  \"orSelectOne\": \"किंवा पर्याय निवडा\",\n  \"typeANewOption\": \"नवीन पर्याय टाइप करा\",\n  \"tagName\": \"टॅग नाव\"\n},\n\"checklist\": {\n  \"taskHint\": \"कार्याचे वर्णन\",\n  \"addNew\": \"नवीन कार्य जोडा\",\n  \"submitNewTask\": \"तयार करा\",\n  \"hideComplete\": \"पूर्ण कार्ये लपवा\",\n  \"showComplete\": \"सर्व कार्ये दाखवा\"\n},\n\"url\": {\n  \"launch\": \"बाह्य ब्राउझरमध्ये लिंक उघडा\",\n  \"copy\": \"लिंक क्लिपबोर्डवर कॉपी करा\",\n  \"textFieldHint\": \"URL टाका\",\n  \"copiedNotification\": \"क्लिपबोर्डवर कॉपी केले!\"\n},\n\"relation\": {\n  \"relatedDatabasePlaceLabel\": \"संबंधित डेटाबेस\",\n  \"relatedDatabasePlaceholder\": \"काही नाही\",\n  \"inRelatedDatabase\": \"या मध्ये\",\n  \"rowSearchTextFieldPlaceholder\": \"शोध\",\n  \"noDatabaseSelected\": \"कोणताही डेटाबेस निवडलेला नाही, कृपया खालील यादीतून एक निवडा:\",\n  \"emptySearchResult\": \"कोणतीही नोंद सापडली नाही\",\n  \"linkedRowListLabel\": \"{count} लिंक केलेल्या पंक्ती\",\n  \"unlinkedRowListLabel\": \"आणखी एक पंक्ती लिंक करा\"\n},\n\"menuName\": \"ग्रिड\",\n\"referencedGridPrefix\": \"दृश्य\",\n\"calculate\": \"गणना करा\",\n\"calculationTypeLabel\": {\n  \"none\": \"काही नाही\",\n  \"average\": \"सरासरी\",\n  \"max\": \"कमाल\",\n  \"median\": \"मध्यम\",\n  \"min\": \"किमान\",\n  \"sum\": \"बेरीज\",\n  \"count\": \"मोजणी\",\n  \"countEmpty\": \"रिकाम्यांची मोजणी\",\n  \"countEmptyShort\": \"रिक्त\",\n  \"countNonEmpty\": \"रिक्त नसलेल्यांची मोजणी\",\n  \"countNonEmptyShort\": \"भरलेले\"\n},\n\"media\": {\n  \"rename\": \"पुन्हा नाव द्या\",\n  \"download\": \"डाउनलोड करा\",\n  \"expand\": \"मोठे करा\",\n  \"delete\": \"हटवा\",\n  \"moreFilesHint\": \"+{}\",\n  \"addFileOrImage\": \"फाईल किंवा लिंक जोडा\",\n  \"attachmentsHint\": \"{}\",\n  \"addFileMobile\": \"फाईल जोडा\",\n  \"extraCount\": \"+{}\",\n  \"deleteFileDescription\": \"तुम्हाला खात्री आहे का की ही फाईल हटवायची आहे? ही क्रिया पूर्ववत करता येणार नाही.\",\n  \"showFileNames\": \"फाईलचे नाव दाखवा\",\n  \"downloadSuccess\": \"फाईल डाउनलोड झाली\",\n  \"downloadFailedToken\": \"फाईल डाउनलोड अयशस्वी, वापरकर्ता टोकन अनुपलब्ध\",\n  \"setAsCover\": \"कव्हर म्हणून सेट करा\",\n  \"openInBrowser\": \"ब्राउझरमध्ये उघडा\",\n  \"embedLink\": \"फाईल लिंक एम्बेड करा\"\n  }\n},\n  \"document\": {\n  \"menuName\": \"दस्तऐवज\",\n  \"date\": {\n    \"timeHintTextInTwelveHour\": \"01:00 PM\",\n    \"timeHintTextInTwentyFourHour\": \"13:00\"\n  },\n  \"creating\": \"तयार करत आहे...\",\n  \"slashMenu\": {\n    \"board\": {\n      \"selectABoardToLinkTo\": \"लिंक करण्यासाठी बोर्ड निवडा\",\n      \"createANewBoard\": \"नवीन बोर्ड तयार करा\"\n    },\n    \"grid\": {\n      \"selectAGridToLinkTo\": \"लिंक करण्यासाठी ग्रिड निवडा\",\n      \"createANewGrid\": \"नवीन ग्रिड तयार करा\"\n    },\n    \"calendar\": {\n      \"selectACalendarToLinkTo\": \"लिंक करण्यासाठी दिनदर्शिका निवडा\",\n      \"createANewCalendar\": \"नवीन दिनदर्शिका तयार करा\"\n    },\n    \"document\": {\n      \"selectADocumentToLinkTo\": \"लिंक करण्यासाठी दस्तऐवज निवडा\"\n    },\n    \"name\": {\n      \"textStyle\": \"मजकुराची शैली\",\n      \"list\": \"यादी\",\n      \"toggle\": \"टॉगल\",\n      \"fileAndMedia\": \"फाईल व मीडिया\",\n      \"simpleTable\": \"सोपे टेबल\",\n      \"visuals\": \"दृश्य घटक\",\n      \"document\": \"दस्तऐवज\",\n      \"advanced\": \"प्रगत\",\n      \"text\": \"मजकूर\",\n      \"heading1\": \"शीर्षक 1\",\n      \"heading2\": \"शीर्षक 2\",\n      \"heading3\": \"शीर्षक 3\",\n      \"image\": \"प्रतिमा\",\n      \"bulletedList\": \"बुलेट यादी\",\n      \"numberedList\": \"क्रमांकित यादी\",\n      \"todoList\": \"करण्याची यादी\",\n      \"doc\": \"दस्तऐवज\",\n      \"linkedDoc\": \"पृष्ठाशी लिंक करा\",\n      \"grid\": \"ग्रिड\",\n      \"linkedGrid\": \"लिंक केलेला ग्रिड\",\n      \"kanban\": \"कानबन\",\n      \"linkedKanban\": \"लिंक केलेला कानबन\",\n      \"calendar\": \"दिनदर्शिका\",\n      \"linkedCalendar\": \"लिंक केलेली दिनदर्शिका\",\n      \"quote\": \"उद्धरण\",\n      \"divider\": \"विभाजक\",\n      \"table\": \"टेबल\",\n      \"callout\": \"महत्त्वाचा मजकूर\",\n      \"outline\": \"रूपरेषा\",\n      \"mathEquation\": \"गणिती समीकरण\",\n      \"code\": \"कोड\",\n      \"toggleList\": \"टॉगल यादी\",\n      \"toggleHeading1\": \"टॉगल शीर्षक 1\",\n      \"toggleHeading2\": \"टॉगल शीर्षक 2\",\n      \"toggleHeading3\": \"टॉगल शीर्षक 3\",\n      \"emoji\": \"इमोजी\",\n      \"aiWriter\": \"AI ला काहीही विचारा\",\n      \"dateOrReminder\": \"दिनांक किंवा स्मरणपत्र\",\n      \"photoGallery\": \"फोटो गॅलरी\",\n      \"file\": \"फाईल\",\n      \"twoColumns\": \"२ स्तंभ\",\n      \"threeColumns\": \"३ स्तंभ\",\n      \"fourColumns\": \"४ स्तंभ\"\n    },\n    \"subPage\": {\n      \"name\": \"दस्तऐवज\",\n      \"keyword1\": \"उपपृष्ठ\",\n      \"keyword2\": \"पृष्ठ\",\n      \"keyword3\": \"चाइल्ड पृष्ठ\",\n      \"keyword4\": \"पृष्ठ जोडा\",\n      \"keyword5\": \"एम्बेड पृष्ठ\",\n      \"keyword6\": \"नवीन पृष्ठ\",\n      \"keyword7\": \"पृष्ठ तयार करा\",\n      \"keyword8\": \"दस्तऐवज\"\n    }\n  },\n  \"selectionMenu\": {\n    \"outline\": \"रूपरेषा\",\n    \"codeBlock\": \"कोड ब्लॉक\"\n  },\n  \"plugins\": {\n    \"referencedBoard\": \"संदर्भित बोर्ड\",\n    \"referencedGrid\": \"संदर्भित ग्रिड\",\n    \"referencedCalendar\": \"संदर्भित दिनदर्शिका\",\n    \"referencedDocument\": \"संदर्भित दस्तऐवज\",\n    \"aiWriter\": {\n      \"userQuestion\": \"AI ला काहीही विचारा\",\n      \"continueWriting\": \"लेखन सुरू ठेवा\",\n      \"fixSpelling\": \"स्पेलिंग व व्याकरण सुधारणा\",\n      \"improveWriting\": \"लेखन सुधारित करा\",\n      \"summarize\": \"सारांश द्या\",\n      \"explain\": \"स्पष्टीकरण द्या\",\n      \"makeShorter\": \"लहान करा\",\n      \"makeLonger\": \"मोठे करा\"\n    },\n      \"autoGeneratorMenuItemName\": \"AI लेखक\",\n\"autoGeneratorTitleName\": \"AI: काहीही लिहिण्यासाठी AI ला विचारा...\",\n\"autoGeneratorLearnMore\": \"अधिक जाणून घ्या\",\n\"autoGeneratorGenerate\": \"उत्पन्न करा\",\n\"autoGeneratorHintText\": \"AI ला विचारा...\",\n\"autoGeneratorCantGetOpenAIKey\": \"AI की मिळवू शकलो नाही\",\n\"autoGeneratorRewrite\": \"पुन्हा लिहा\",\n\"smartEdit\": \"AI ला विचारा\",\n\"aI\": \"AI\",\n\"smartEditFixSpelling\": \"स्पेलिंग आणि व्याकरण सुधारा\",\n\"warning\": \"⚠️ AI उत्तरं चुकीची किंवा दिशाभूल करणारी असू शकतात.\",\n\"smartEditSummarize\": \"सारांश द्या\",\n\"smartEditImproveWriting\": \"लेखन सुधारित करा\",\n\"smartEditMakeLonger\": \"लांब करा\",\n\"smartEditCouldNotFetchResult\": \"AI कडून उत्तर मिळवता आले नाही\",\n\"smartEditCouldNotFetchKey\": \"AI की मिळवता आली नाही\",\n\"smartEditDisabled\": \"सेटिंग्जमध्ये AI कनेक्ट करा\",\n\"appflowyAIEditDisabled\": \"AI वैशिष्ट्ये सक्षम करण्यासाठी साइन इन करा\",\n\"discardResponse\": \"AI उत्तर फेकून द्यायचं आहे का?\",\n\"createInlineMathEquation\": \"समीकरण तयार करा\",\n\"fonts\": \"फॉन्ट्स\",\n\"insertDate\": \"तारीख जोडा\",\n\"emoji\": \"इमोजी\",\n\"toggleList\": \"टॉगल यादी\",\n\"emptyToggleHeading\": \"रिकामे टॉगल h{}. मजकूर जोडण्यासाठी क्लिक करा.\",\n\"emptyToggleList\": \"रिकामी टॉगल यादी. मजकूर जोडण्यासाठी क्लिक करा.\",\n\"emptyToggleHeadingWeb\": \"रिकामे टॉगल h{level}. मजकूर जोडण्यासाठी क्लिक करा\",\n\"quoteList\": \"उद्धरण यादी\",\n\"numberedList\": \"क्रमांकित यादी\",\n\"bulletedList\": \"बुलेट यादी\",\n\"todoList\": \"करण्याची यादी\",\n\"callout\": \"ठळक मजकूर\",\n\"simpleTable\": {\n  \"moreActions\": {\n    \"color\": \"रंग\",\n    \"align\": \"पंक्तिबद्ध करा\",\n    \"delete\": \"हटा\",\n    \"duplicate\": \"डुप्लिकेट करा\",\n    \"insertLeft\": \"डावीकडे घाला\",\n    \"insertRight\": \"उजवीकडे घाला\",\n    \"insertAbove\": \"वर घाला\",\n    \"insertBelow\": \"खाली घाला\",\n    \"headerColumn\": \"हेडर स्तंभ\",\n    \"headerRow\": \"हेडर ओळ\",\n    \"clearContents\": \"सामग्री साफ करा\",\n    \"setToPageWidth\": \"पृष्ठाच्या रुंदीप्रमाणे सेट करा\",\n    \"distributeColumnsWidth\": \"स्तंभ समान करा\",\n    \"duplicateRow\": \"ओळ डुप्लिकेट करा\",\n    \"duplicateColumn\": \"स्तंभ डुप्लिकेट करा\",\n    \"textColor\": \"मजकूराचा रंग\",\n    \"cellBackgroundColor\": \"सेलचा पार्श्वभूमी रंग\",\n    \"duplicateTable\": \"टेबल डुप्लिकेट करा\"\n  },\n  \"clickToAddNewRow\": \"नवीन ओळ जोडण्यासाठी क्लिक करा\",\n  \"clickToAddNewColumn\": \"नवीन स्तंभ जोडण्यासाठी क्लिक करा\",\n  \"clickToAddNewRowAndColumn\": \"नवीन ओळ आणि स्तंभ जोडण्यासाठी क्लिक करा\",\n  \"headerName\": {\n    \"table\": \"टेबल\",\n    \"alignText\": \"मजकूर पंक्तिबद्ध करा\"\n  }\n},\n\"cover\": {\n  \"changeCover\": \"कव्हर बदला\",\n  \"colors\": \"रंग\",\n  \"images\": \"प्रतिमा\",\n  \"clearAll\": \"सर्व साफ करा\",\n  \"abstract\": \"ऍबस्ट्रॅक्ट\",\n  \"addCover\": \"कव्हर जोडा\",\n  \"addLocalImage\": \"स्थानिक प्रतिमा जोडा\",\n  \"invalidImageUrl\": \"अवैध प्रतिमा URL\",\n  \"failedToAddImageToGallery\": \"प्रतिमा गॅलरीत जोडता आली नाही\",\n  \"enterImageUrl\": \"प्रतिमा URL लिहा\",\n  \"add\": \"जोडा\",\n  \"back\": \"मागे\",\n  \"saveToGallery\": \"गॅलरीत जतन करा\",\n  \"removeIcon\": \"आयकॉन काढा\",\n  \"removeCover\": \"कव्हर काढा\",\n  \"pasteImageUrl\": \"प्रतिमा URL पेस्ट करा\",\n  \"or\": \"किंवा\",\n  \"pickFromFiles\": \"फाईल्समधून निवडा\",\n  \"couldNotFetchImage\": \"प्रतिमा मिळवता आली नाही\",\n  \"imageSavingFailed\": \"प्रतिमा जतन करणे अयशस्वी\",\n  \"addIcon\": \"आयकॉन जोडा\",\n  \"changeIcon\": \"आयकॉन बदला\",\n  \"coverRemoveAlert\": \"हे हटवल्यानंतर कव्हरमधून काढले जाईल.\",\n  \"alertDialogConfirmation\": \"तुम्हाला खात्री आहे का? तुम्हाला पुढे जायचे आहे?\"\n},\n\"mathEquation\": {\n  \"name\": \"गणिती समीकरण\",\n  \"addMathEquation\": \"TeX समीकरण जोडा\",\n  \"editMathEquation\": \"गणिती समीकरण संपादित करा\"\n},\n\"optionAction\": {\n  \"click\": \"क्लिक\",\n  \"toOpenMenu\": \"मेनू उघडण्यासाठी\",\n  \"drag\": \"ओढा\",\n  \"toMove\": \"हलवण्यासाठी\",\n  \"delete\": \"हटा\",\n  \"duplicate\": \"डुप्लिकेट करा\",\n  \"turnInto\": \"मध्ये बदला\",\n  \"moveUp\": \"वर हलवा\",\n  \"moveDown\": \"खाली हलवा\",\n  \"color\": \"रंग\",\n  \"align\": \"पंक्तिबद्ध करा\",\n  \"left\": \"डावीकडे\",\n  \"center\": \"मध्यभागी\",\n  \"right\": \"उजवीकडे\",\n  \"defaultColor\": \"डिफॉल्ट\",\n  \"depth\": \"खोली\",\n  \"copyLinkToBlock\": \"ब्लॉकसाठी लिंक कॉपी करा\"\n},\n      \"image\": {\n  \"addAnImage\": \"प्रतिमा जोडा\",\n  \"copiedToPasteBoard\": \"प्रतिमेची लिंक क्लिपबोर्डवर कॉपी केली गेली आहे\",\n  \"addAnImageDesktop\": \"प्रतिमा जोडा\",\n  \"addAnImageMobile\": \"एक किंवा अधिक प्रतिमा जोडण्यासाठी क्लिक करा\",\n  \"dropImageToInsert\": \"प्रतिमा ड्रॉप करून जोडा\",\n  \"imageUploadFailed\": \"प्रतिमा अपलोड करण्यात अयशस्वी\",\n  \"imageDownloadFailed\": \"प्रतिमा डाउनलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा\",\n  \"imageDownloadFailedToken\": \"युजर टोकन नसल्यामुळे प्रतिमा डाउनलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा\",\n  \"errorCode\": \"त्रुटी कोड\"\n},\n\"photoGallery\": {\n  \"name\": \"फोटो गॅलरी\",\n  \"imageKeyword\": \"प्रतिमा\",\n  \"imageGalleryKeyword\": \"प्रतिमा गॅलरी\",\n  \"photoKeyword\": \"फोटो\",\n  \"photoBrowserKeyword\": \"फोटो ब्राउझर\",\n  \"galleryKeyword\": \"गॅलरी\",\n  \"addImageTooltip\": \"प्रतिमा जोडा\",\n  \"changeLayoutTooltip\": \"लेआउट बदला\",\n  \"browserLayout\": \"ब्राउझर\",\n  \"gridLayout\": \"ग्रिड\",\n  \"deleteBlockTooltip\": \"संपूर्ण गॅलरी हटवा\"\n},\n\"math\": {\n  \"copiedToPasteBoard\": \"समीकरण क्लिपबोर्डवर कॉपी केले गेले आहे\"\n},\n\"urlPreview\": {\n  \"copiedToPasteBoard\": \"लिंक क्लिपबोर्डवर कॉपी केली गेली आहे\",\n  \"convertToLink\": \"एंबेड लिंकमध्ये रूपांतर करा\"\n},\n\"outline\": {\n  \"addHeadingToCreateOutline\": \"सामग्री यादी तयार करण्यासाठी शीर्षके जोडा.\",\n  \"noMatchHeadings\": \"जुळणारी शीर्षके आढळली नाहीत.\"\n},\n\"table\": {\n  \"addAfter\": \"नंतर जोडा\",\n  \"addBefore\": \"आधी जोडा\",\n  \"delete\": \"हटा\",\n  \"clear\": \"सामग्री साफ करा\",\n  \"duplicate\": \"डुप्लिकेट करा\",\n  \"bgColor\": \"पार्श्वभूमीचा रंग\"\n},\n\"contextMenu\": {\n  \"copy\": \"कॉपी करा\",\n  \"cut\": \"कापा\",\n  \"paste\": \"पेस्ट करा\",\n  \"pasteAsPlainText\": \"साध्या मजकूराच्या स्वरूपात पेस्ट करा\"\n},\n\"action\": \"कृती\",\n\"database\": {\n  \"selectDataSource\": \"डेटा स्रोत निवडा\",\n  \"noDataSource\": \"डेटा स्रोत नाही\",\n  \"selectADataSource\": \"डेटा स्रोत निवडा\",\n  \"toContinue\": \"पुढे जाण्यासाठी\",\n  \"newDatabase\": \"नवीन डेटाबेस\",\n  \"linkToDatabase\": \"डेटाबेसशी लिंक करा\"\n},\n\"date\": \"तारीख\",\n\"video\": {\n  \"label\": \"व्हिडिओ\",\n  \"emptyLabel\": \"व्हिडिओ जोडा\",\n  \"placeholder\": \"व्हिडिओ लिंक पेस्ट करा\",\n  \"copiedToPasteBoard\": \"व्हिडिओ लिंक क्लिपबोर्डवर कॉपी केली गेली आहे\",\n  \"insertVideo\": \"व्हिडिओ जोडा\",\n  \"invalidVideoUrl\": \"ही URL सध्या समर्थित नाही.\",\n  \"invalidVideoUrlYouTube\": \"YouTube सध्या समर्थित नाही.\",\n  \"supportedFormats\": \"समर्थित स्वरूप: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264\"\n},\n\"file\": {\n  \"name\": \"फाईल\",\n  \"uploadTab\": \"अपलोड\",\n  \"uploadMobile\": \"फाईल निवडा\",\n  \"uploadMobileGallery\": \"फोटो गॅलरीमधून\",\n  \"networkTab\": \"लिंक एम्बेड करा\",\n  \"placeholderText\": \"फाईल अपलोड किंवा एम्बेड करा\",\n  \"placeholderDragging\": \"फाईल ड्रॉप करून अपलोड करा\",\n  \"dropFileToUpload\": \"फाईल ड्रॉप करून अपलोड करा\",\n  \"fileUploadHint\": \"फाईल ड्रॅग आणि ड्रॉप करा किंवा क्लिक करा \",\n  \"fileUploadHintSuffix\": \"ब्राउझ करा\",\n  \"networkHint\": \"फाईल लिंक पेस्ट करा\",\n  \"networkUrlInvalid\": \"अवैध URL. कृपया तपासा आणि पुन्हा प्रयत्न करा.\",\n  \"networkAction\": \"एम्बेड\",\n  \"fileTooBigError\": \"फाईल खूप मोठी आहे, कृपया 10MB पेक्षा कमी फाईल अपलोड करा\",\n  \"renameFile\": {\n    \"title\": \"फाईलचे नाव बदला\",\n    \"description\": \"या फाईलसाठी नवीन नाव लिहा\",\n    \"nameEmptyError\": \"फाईलचे नाव रिकामे असू शकत नाही.\"\n  },\n  \"uploadedAt\": \"{} रोजी अपलोड केले\",\n  \"linkedAt\": \"{} रोजी लिंक जोडली\",\n  \"failedToOpenMsg\": \"उघडण्यात अयशस्वी, फाईल सापडली नाही\"\n},\n\"subPage\": {\n  \"handlingPasteHint\": \" - (पेस्ट प्रक्रिया करत आहे)\",\n  \"errors\": {\n    \"failedDeletePage\": \"पृष्ठ हटवण्यात अयशस्वी\",\n    \"failedCreatePage\": \"पृष्ठ तयार करण्यात अयशस्वी\",\n    \"failedMovePage\": \"हे पृष्ठ हलवण्यात अयशस्वी\",\n    \"failedDuplicatePage\": \"पृष्ठ डुप्लिकेट करण्यात अयशस्वी\",\n    \"failedDuplicateFindView\": \"पृष्ठ डुप्लिकेट करण्यात अयशस्वी - मूळ दृश्य सापडले नाही\"\n  }\n},\n     \"cannotMoveToItsChildren\": \"मुलांमध्ये हलवू शकत नाही\"\n},\n\"outlineBlock\": {\n  \"placeholder\": \"सामग्री सूची\"\n},\n\"textBlock\": {\n  \"placeholder\": \"कमांडसाठी '/' टाइप करा\"\n},\n\"title\": {\n  \"placeholder\": \"शीर्षक नाही\"\n},\n\"imageBlock\": {\n  \"placeholder\": \"प्रतिमा जोडण्यासाठी क्लिक करा\",\n  \"upload\": {\n    \"label\": \"अपलोड\",\n    \"placeholder\": \"प्रतिमा अपलोड करण्यासाठी क्लिक करा\"\n  },\n  \"url\": {\n    \"label\": \"प्रतिमेची URL\",\n    \"placeholder\": \"प्रतिमेची URL टाका\"\n  },\n  \"ai\": {\n    \"label\": \"AI द्वारे प्रतिमा तयार करा\",\n    \"placeholder\": \"AI द्वारे प्रतिमा तयार करण्यासाठी कृपया संकेत द्या\"\n  },\n  \"stability_ai\": {\n    \"label\": \"Stability AI द्वारे प्रतिमा तयार करा\",\n    \"placeholder\": \"Stability AI द्वारे प्रतिमा तयार करण्यासाठी कृपया संकेत द्या\"\n  },\n  \"support\": \"प्रतिमेचा कमाल आकार 5MB आहे. समर्थित स्वरूप: JPEG, PNG, GIF, SVG\",\n  \"error\": {\n    \"invalidImage\": \"अवैध प्रतिमा\",\n    \"invalidImageSize\": \"प्रतिमेचा आकार 5MB पेक्षा कमी असावा\",\n    \"invalidImageFormat\": \"प्रतिमेचे स्वरूप समर्थित नाही. समर्थित स्वरूप: JPEG, PNG, JPG, GIF, SVG, WEBP\",\n    \"invalidImageUrl\": \"अवैध प्रतिमेची URL\",\n    \"noImage\": \"अशी फाईल किंवा निर्देशिका नाही\",\n    \"multipleImagesFailed\": \"एक किंवा अधिक प्रतिमा अपलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा\"\n  },\n  \"embedLink\": {\n    \"label\": \"लिंक एम्बेड करा\",\n    \"placeholder\": \"प्रतिमेची लिंक पेस्ट करा किंवा टाका\"\n  },\n  \"unsplash\": {\n    \"label\": \"Unsplash\"\n  },\n  \"searchForAnImage\": \"प्रतिमा शोधा\",\n  \"pleaseInputYourOpenAIKey\": \"कृपया सेटिंग्ज पृष्ठात आपला AI की प्रविष्ट करा\",\n  \"saveImageToGallery\": \"प्रतिमा जतन करा\",\n  \"failedToAddImageToGallery\": \"प्रतिमा जतन करण्यात अयशस्वी\",\n  \"successToAddImageToGallery\": \"प्रतिमा 'Photos' मध्ये जतन केली\",\n  \"unableToLoadImage\": \"प्रतिमा लोड करण्यात अयशस्वी\",\n  \"maximumImageSize\": \"कमाल प्रतिमा अपलोड आकार 10MB आहे\",\n  \"uploadImageErrorImageSizeTooBig\": \"प्रतिमेचा आकार 10MB पेक्षा कमी असावा\",\n  \"imageIsUploading\": \"प्रतिमा अपलोड होत आहे\",\n  \"openFullScreen\": \"पूर्ण स्क्रीनमध्ये उघडा\",\n  \"interactiveViewer\": {\n    \"toolbar\": {\n      \"previousImageTooltip\": \"मागील प्रतिमा\",\n      \"nextImageTooltip\": \"पुढील प्रतिमा\",\n      \"zoomOutTooltip\": \"लहान करा\",\n      \"zoomInTooltip\": \"मोठी करा\",\n      \"changeZoomLevelTooltip\": \"झूम पातळी बदला\",\n      \"openLocalImage\": \"प्रतिमा उघडा\",\n      \"downloadImage\": \"प्रतिमा डाउनलोड करा\",\n      \"closeViewer\": \"इंटरअॅक्टिव्ह व्ह्युअर बंद करा\",\n      \"scalePercentage\": \"{}%\",\n      \"deleteImageTooltip\": \"प्रतिमा हटवा\"\n    }\n  }\n},\n    \"codeBlock\": {\n  \"language\": {\n    \"label\": \"भाषा\",\n    \"placeholder\": \"भाषा निवडा\",\n    \"auto\": \"स्वयंचलित\"\n  },\n  \"copyTooltip\": \"कॉपी करा\",\n  \"searchLanguageHint\": \"भाषा शोधा\",\n  \"codeCopiedSnackbar\": \"कोड क्लिपबोर्डवर कॉपी झाला!\"\n},\n\"inlineLink\": {\n  \"placeholder\": \"लिंक पेस्ट करा किंवा टाका\",\n  \"openInNewTab\": \"नवीन टॅबमध्ये उघडा\",\n  \"copyLink\": \"लिंक कॉपी करा\",\n  \"removeLink\": \"लिंक काढा\",\n  \"url\": {\n    \"label\": \"लिंक URL\",\n    \"placeholder\": \"लिंक URL टाका\"\n  },\n  \"title\": {\n    \"label\": \"लिंक शीर्षक\",\n    \"placeholder\": \"लिंक शीर्षक टाका\"\n  }\n},\n\"mention\": {\n  \"placeholder\": \"कोणीतरी, पृष्ठ किंवा तारीख नमूद करा...\",\n  \"page\": {\n    \"label\": \"पृष्ठाला लिंक करा\",\n    \"tooltip\": \"पृष्ठ उघडण्यासाठी क्लिक करा\"\n  },\n  \"deleted\": \"हटवले गेले\",\n  \"deletedContent\": \"ही सामग्री अस्तित्वात नाही किंवा हटवण्यात आली आहे\",\n  \"noAccess\": \"प्रवेश नाही\",\n  \"deletedPage\": \"हटवलेले पृष्ठ\",\n  \"trashHint\": \" - ट्रॅशमध्ये\",\n  \"morePages\": \"अजून पृष्ठे\"\n},\n\"toolbar\": {\n  \"resetToDefaultFont\": \"डीफॉल्ट फॉन्टवर परत जा\",\n  \"textSize\": \"मजकूराचा आकार\",\n  \"textColor\": \"मजकूराचा रंग\",\n  \"h1\": \"मथळा 1\",\n  \"h2\": \"मथळा 2\",\n  \"h3\": \"मथळा 3\",\n  \"alignLeft\": \"डावीकडे संरेखित करा\",\n  \"alignRight\": \"उजवीकडे संरेखित करा\",\n  \"alignCenter\": \"मध्यभागी संरेखित करा\",\n  \"link\": \"लिंक\",\n  \"textAlign\": \"मजकूर संरेखन\",\n  \"moreOptions\": \"अधिक पर्याय\",\n  \"font\": \"फॉन्ट\",\n  \"inlineCode\": \"इनलाइन कोड\",\n  \"suggestions\": \"सूचना\",\n  \"turnInto\": \"मध्ये रूपांतरित करा\",\n  \"equation\": \"समीकरण\",\n  \"insert\": \"घाला\",\n  \"linkInputHint\": \"लिंक पेस्ट करा किंवा पृष्ठे शोधा\",\n  \"pageOrURL\": \"पृष्ठ किंवा URL\",\n  \"linkName\": \"लिंकचे नाव\",\n  \"linkNameHint\": \"लिंकचे नाव प्रविष्ट करा\"\n},\n\"errorBlock\": {\n  \"theBlockIsNotSupported\": \"ब्लॉक सामग्री पार्स करण्यात अक्षम\",\n  \"clickToCopyTheBlockContent\": \"ब्लॉक सामग्री कॉपी करण्यासाठी क्लिक करा\",\n  \"blockContentHasBeenCopied\": \"ब्लॉक सामग्री कॉपी केली आहे.\",\n  \"parseError\": \"{} ब्लॉक पार्स करताना त्रुटी आली.\",\n  \"copyBlockContent\": \"ब्लॉक सामग्री कॉपी करा\"\n},\n\"mobilePageSelector\": {\n  \"title\": \"पृष्ठ निवडा\",\n  \"failedToLoad\": \"पृष्ठ यादी लोड करण्यात अयशस्वी\",\n  \"noPagesFound\": \"कोणतीही पृष्ठे सापडली नाहीत\"\n},\n\"attachmentMenu\": {\n  \"choosePhoto\": \"फोटो निवडा\",\n  \"takePicture\": \"फोटो काढा\",\n  \"chooseFile\": \"फाईल निवडा\"\n  }\n  },\n  \"board\": {\n  \"column\": {\n    \"label\": \"स्तंभ\",\n    \"createNewCard\": \"नवीन\",\n    \"renameGroupTooltip\": \"गटाचे नाव बदलण्यासाठी क्लिक करा\",\n    \"createNewColumn\": \"नवीन गट जोडा\",\n    \"addToColumnTopTooltip\": \"वर नवीन कार्ड जोडा\",\n    \"addToColumnBottomTooltip\": \"खाली नवीन कार्ड जोडा\",\n    \"renameColumn\": \"स्तंभाचे नाव बदला\",\n    \"hideColumn\": \"लपवा\",\n    \"newGroup\": \"नवीन गट\",\n    \"deleteColumn\": \"हटवा\",\n    \"deleteColumnConfirmation\": \"हा गट आणि त्यामधील सर्व कार्ड्स हटवले जातील. तुम्हाला खात्री आहे का?\"\n  },\n  \"hiddenGroupSection\": {\n    \"sectionTitle\": \"लपवलेले गट\",\n    \"collapseTooltip\": \"लपवलेले गट लपवा\",\n    \"expandTooltip\": \"लपवलेले गट पाहा\"\n  },\n  \"cardDetail\": \"कार्ड तपशील\",\n  \"cardActions\": \"कार्ड क्रिया\",\n  \"cardDuplicated\": \"कार्डची प्रत तयार झाली\",\n  \"cardDeleted\": \"कार्ड हटवले गेले\",\n  \"showOnCard\": \"कार्ड तपशिलावर दाखवा\",\n  \"setting\": \"सेटिंग\",\n  \"propertyName\": \"गुणधर्माचे नाव\",\n  \"menuName\": \"बोर्ड\",\n  \"showUngrouped\": \"गटात नसलेली कार्ड्स दाखवा\",\n  \"ungroupedButtonText\": \"गट नसलेली\",\n  \"ungroupedButtonTooltip\": \"ज्या कार्ड्स कोणत्याही गटात नाहीत\",\n  \"ungroupedItemsTitle\": \"बोर्डमध्ये जोडण्यासाठी क्लिक करा\",\n  \"groupBy\": \"या आधारावर गट करा\",\n  \"groupCondition\": \"गट स्थिती\",\n  \"referencedBoardPrefix\": \"याचे दृश्य\",\n  \"notesTooltip\": \"नोट्स आहेत\",\n  \"mobile\": {\n    \"editURL\": \"URL संपादित करा\",\n    \"showGroup\": \"गट दाखवा\",\n    \"showGroupContent\": \"हा गट बोर्डवर दाखवायचा आहे का?\",\n    \"failedToLoad\": \"बोर्ड दृश्य लोड होण्यात अयशस्वी\"\n  },\n  \"dateCondition\": {\n    \"weekOf\": \"{} - {} ची आठवडा\",\n    \"today\": \"आज\",\n    \"yesterday\": \"काल\",\n    \"tomorrow\": \"उद्या\",\n    \"lastSevenDays\": \"शेवटचे ७ दिवस\",\n    \"nextSevenDays\": \"पुढील ७ दिवस\",\n    \"lastThirtyDays\": \"शेवटचे ३० दिवस\",\n    \"nextThirtyDays\": \"पुढील ३० दिवस\"\n  },\n  \"noGroup\": \"गटासाठी कोणताही गुणधर्म निवडलेला नाही\",\n  \"noGroupDesc\": \"बोर्ड दृश्य दाखवण्यासाठी गट करण्याचा एक गुणधर्म आवश्यक आहे\",\n  \"media\": {\n    \"cardText\": \"{} {}\",\n    \"fallbackName\": \"फायली\"\n  }\n},\n  \"calendar\": {\n  \"menuName\": \"कॅलेंडर\",\n  \"defaultNewCalendarTitle\": \"नाव नाही\",\n  \"newEventButtonTooltip\": \"नवीन इव्हेंट जोडा\",\n  \"navigation\": {\n    \"today\": \"आज\",\n    \"jumpToday\": \"आजवर जा\",\n    \"previousMonth\": \"मागील महिना\",\n    \"nextMonth\": \"पुढील महिना\",\n    \"views\": {\n      \"day\": \"दिवस\",\n      \"week\": \"आठवडा\",\n      \"month\": \"महिना\",\n      \"year\": \"वर्ष\"\n    }\n  },\n  \"mobileEventScreen\": {\n    \"emptyTitle\": \"सध्या कोणतेही इव्हेंट नाहीत\",\n    \"emptyBody\": \"या दिवशी इव्हेंट तयार करण्यासाठी प्लस बटणावर क्लिक करा.\"\n  },\n  \"settings\": {\n    \"showWeekNumbers\": \"आठवड्याचे क्रमांक दाखवा\",\n    \"showWeekends\": \"सप्ताहांत दाखवा\",\n    \"firstDayOfWeek\": \"आठवड्याची सुरुवात\",\n    \"layoutDateField\": \"कॅलेंडर मांडणी दिनांकानुसार\",\n    \"changeLayoutDateField\": \"मांडणी फील्ड बदला\",\n    \"noDateTitle\": \"तारीख नाही\",\n    \"noDateHint\": {\n      \"zero\": \"नियोजित नसलेली इव्हेंट्स येथे दिसतील\",\n      \"one\": \"{count} नियोजित नसलेली इव्हेंट\",\n      \"other\": \"{count} नियोजित नसलेल्या इव्हेंट्स\"\n    },\n    \"unscheduledEventsTitle\": \"नियोजित नसलेल्या इव्हेंट्स\",\n    \"clickToAdd\": \"कॅलेंडरमध्ये जोडण्यासाठी क्लिक करा\",\n    \"name\": \"कॅलेंडर सेटिंग्ज\",\n    \"clickToOpen\": \"रेकॉर्ड उघडण्यासाठी क्लिक करा\"\n  },\n  \"referencedCalendarPrefix\": \"याचे दृश्य\",\n  \"quickJumpYear\": \"या वर्षावर जा\",\n  \"duplicateEvent\": \"इव्हेंट डुप्लिकेट करा\"\n},\n  \"errorDialog\": {\n  \"title\": \"@:appName त्रुटी\",\n  \"howToFixFallback\": \"या गैरसोयीबद्दल आम्ही दिलगीर आहोत! कृपया GitHub पेजवर त्रुटीबद्दल माहिती देणारे एक issue सबमिट करा.\",\n  \"howToFixFallbackHint1\": \"या गैरसोयीबद्दल आम्ही दिलगीर आहोत! कृपया \",\n  \"howToFixFallbackHint2\": \" पेजवर त्रुटीचे वर्णन करणारे issue सबमिट करा.\",\n  \"github\": \"GitHub वर पहा\"\n},\n\"search\": {\n  \"label\": \"शोध\",\n  \"sidebarSearchIcon\": \"पृष्ठ शोधा आणि पटकन जा\",\n  \"placeholder\": {\n    \"actions\": \"कृती शोधा...\"\n  }\n},\n\"message\": {\n  \"copy\": {\n    \"success\": \"कॉपी झाले!\",\n    \"fail\": \"कॉपी करू शकत नाही\"\n  }\n},\n\"unSupportBlock\": \"सध्याचा व्हर्जन या ब्लॉकला समर्थन देत नाही.\",\n\"views\": {\n  \"deleteContentTitle\": \"तुम्हाला हे {pageType} हटवायचे आहे का?\",\n  \"deleteContentCaption\": \"हे {pageType} हटवल्यास, तुम्ही ते trash मधून पुनर्संचयित करू शकता.\"\n},\n  \"colors\": {\n  \"custom\": \"सानुकूल\",\n  \"default\": \"डीफॉल्ट\",\n  \"red\": \"लाल\",\n  \"orange\": \"संत्रा\",\n  \"yellow\": \"पिवळा\",\n  \"green\": \"हिरवा\",\n  \"blue\": \"निळा\",\n  \"purple\": \"जांभळा\",\n  \"pink\": \"गुलाबी\",\n  \"brown\": \"तपकिरी\",\n  \"gray\": \"करड्या रंगाचा\"\n},\n  \"emoji\": {\n  \"emojiTab\": \"इमोजी\",\n  \"search\": \"इमोजी शोधा\",\n  \"noRecent\": \"अलीकडील कोणतेही इमोजी नाहीत\",\n  \"noEmojiFound\": \"कोणतेही इमोजी सापडले नाहीत\",\n  \"filter\": \"फिल्टर\",\n  \"random\": \"योगायोगाने\",\n  \"selectSkinTone\": \"त्वचेचा टोन निवडा\",\n  \"remove\": \"इमोजी काढा\",\n  \"categories\": {\n    \"smileys\": \"स्मायली आणि भावना\",\n    \"people\": \"लोक\",\n    \"animals\": \"प्राणी आणि निसर्ग\",\n    \"food\": \"अन्न\",\n    \"activities\": \"क्रिया\",\n    \"places\": \"स्थळे\",\n    \"objects\": \"वस्तू\",\n    \"symbols\": \"चिन्हे\",\n    \"flags\": \"ध्वज\",\n    \"nature\": \"निसर्ग\",\n    \"frequentlyUsed\": \"नेहमी वापरलेले\"\n  },\n  \"skinTone\": {\n    \"default\": \"डीफॉल्ट\",\n    \"light\": \"हलका\",\n    \"mediumLight\": \"मध्यम-हलका\",\n    \"medium\": \"मध्यम\",\n    \"mediumDark\": \"मध्यम-गडद\",\n    \"dark\": \"गडद\"\n  },\n  \"openSourceIconsFrom\": \"खुल्या स्रोताचे आयकॉन्स\"\n},\n  \"inlineActions\": {\n  \"noResults\": \"निकाल नाही\",\n  \"recentPages\": \"अलीकडील पृष्ठे\",\n  \"pageReference\": \"पृष्ठ संदर्भ\",\n  \"docReference\": \"दस्तऐवज संदर्भ\",\n  \"boardReference\": \"बोर्ड संदर्भ\",\n  \"calReference\": \"कॅलेंडर संदर्भ\",\n  \"gridReference\": \"ग्रिड संदर्भ\",\n  \"date\": \"तारीख\",\n  \"reminder\": {\n    \"groupTitle\": \"स्मरणपत्र\",\n    \"shortKeyword\": \"remind\"\n  },\n  \"createPage\": \"\\\"{}\\\" उप-पृष्ठ तयार करा\"\n},\n  \"datePicker\": {\n  \"dateTimeFormatTooltip\": \"सेटिंग्जमध्ये तारीख आणि वेळ फॉरमॅट बदला\",\n  \"dateFormat\": \"तारीख फॉरमॅट\",\n  \"includeTime\": \"वेळ समाविष्ट करा\",\n  \"isRange\": \"शेवटची तारीख\",\n  \"timeFormat\": \"वेळ फॉरमॅट\",\n  \"clearDate\": \"तारीख साफ करा\",\n  \"reminderLabel\": \"स्मरणपत्र\",\n  \"selectReminder\": \"स्मरणपत्र निवडा\",\n  \"reminderOptions\": {\n    \"none\": \"काहीही नाही\",\n    \"atTimeOfEvent\": \"इव्हेंटच्या वेळी\",\n    \"fiveMinsBefore\": \"५ मिनिटे आधी\",\n    \"tenMinsBefore\": \"१० मिनिटे आधी\",\n    \"fifteenMinsBefore\": \"१५ मिनिटे आधी\",\n    \"thirtyMinsBefore\": \"३० मिनिटे आधी\",\n    \"oneHourBefore\": \"१ तास आधी\",\n    \"twoHoursBefore\": \"२ तास आधी\",\n    \"onDayOfEvent\": \"इव्हेंटच्या दिवशी\",\n    \"oneDayBefore\": \"१ दिवस आधी\",\n    \"twoDaysBefore\": \"२ दिवस आधी\",\n    \"oneWeekBefore\": \"१ आठवडा आधी\",\n    \"custom\": \"सानुकूल\"\n  }\n},\n  \"relativeDates\": {\n  \"yesterday\": \"काल\",\n  \"today\": \"आज\",\n  \"tomorrow\": \"उद्या\",\n  \"oneWeek\": \"१ आठवडा\"\n},\n  \"notificationHub\": {\n  \"title\": \"सूचना\",\n  \"mobile\": {\n    \"title\": \"अपडेट्स\"\n  },\n  \"emptyTitle\": \"सर्व पूर्ण झाले!\",\n  \"emptyBody\": \"कोणतीही प्रलंबित सूचना किंवा कृती नाहीत. शांततेचा आनंद घ्या.\",\n  \"tabs\": {\n    \"inbox\": \"इनबॉक्स\",\n    \"upcoming\": \"आगामी\"\n  },\n  \"actions\": {\n    \"markAllRead\": \"सर्व वाचलेल्या म्हणून चिन्हित करा\",\n    \"showAll\": \"सर्व\",\n    \"showUnreads\": \"न वाचलेल्या\"\n  },\n  \"filters\": {\n    \"ascending\": \"आरोही\",\n    \"descending\": \"अवरोही\",\n    \"groupByDate\": \"तारीखेनुसार गटबद्ध करा\",\n    \"showUnreadsOnly\": \"फक्त न वाचलेल्या दाखवा\",\n    \"resetToDefault\": \"डीफॉल्टवर रीसेट करा\"\n  }\n},\n  \"reminderNotification\": {\n  \"title\": \"स्मरणपत्र\",\n  \"message\": \"तुम्ही विसरण्याआधी हे तपासण्याचे लक्षात ठेवा!\",\n  \"tooltipDelete\": \"हटवा\",\n  \"tooltipMarkRead\": \"वाचले म्हणून चिन्हित करा\",\n  \"tooltipMarkUnread\": \"न वाचले म्हणून चिन्हित करा\"\n},\n  \"findAndReplace\": {\n  \"find\": \"शोधा\",\n  \"previousMatch\": \"मागील जुळणारे\",\n  \"nextMatch\": \"पुढील जुळणारे\",\n  \"close\": \"बंद करा\",\n  \"replace\": \"बदला\",\n  \"replaceAll\": \"सर्व बदला\",\n  \"noResult\": \"कोणतेही निकाल नाहीत\",\n  \"caseSensitive\": \"केस सेंसिटिव्ह\",\n  \"searchMore\": \"अधिक निकालांसाठी शोधा\"\n},\n  \"error\": {\n  \"weAreSorry\": \"आम्ही क्षमस्व आहोत\",\n  \"loadingViewError\": \"हे दृश्य लोड करण्यात अडचण येत आहे. कृपया तुमचे इंटरनेट कनेक्शन तपासा, अ‍ॅप रीफ्रेश करा, आणि समस्या कायम असल्यास आमच्याशी संपर्क साधा.\",\n  \"syncError\": \"इतर डिव्हाइसमधून डेटा सिंक झाला नाही\",\n  \"syncErrorHint\": \"कृपया हे पृष्ठ शेवटचे जिथे संपादित केले होते त्या डिव्हाइसवर उघडा, आणि मग सध्याच्या डिव्हाइसवर पुन्हा उघडा.\",\n  \"clickToCopy\": \"एरर कोड कॉपी करण्यासाठी क्लिक करा\"\n},\n  \"editor\": {\n  \"bold\": \"जाड\",\n  \"bulletedList\": \"बुलेट यादी\",\n  \"bulletedListShortForm\": \"बुलेट\",\n  \"checkbox\": \"चेकबॉक्स\",\n  \"embedCode\": \"कोड एम्बेड करा\",\n  \"heading1\": \"H1\",\n  \"heading2\": \"H2\",\n  \"heading3\": \"H3\",\n  \"highlight\": \"हायलाइट\",\n  \"color\": \"रंग\",\n  \"image\": \"प्रतिमा\",\n  \"date\": \"तारीख\",\n  \"page\": \"पृष्ठ\",\n  \"italic\": \"तिरका\",\n  \"link\": \"लिंक\",\n  \"numberedList\": \"क्रमांकित यादी\",\n  \"numberedListShortForm\": \"क्रमांकित\",\n  \"toggleHeading1ShortForm\": \"Toggle H1\",\n  \"toggleHeading2ShortForm\": \"Toggle H2\",\n  \"toggleHeading3ShortForm\": \"Toggle H3\",\n  \"quote\": \"कोट\",\n  \"strikethrough\": \"ओढून टाका\",\n  \"text\": \"मजकूर\",\n  \"underline\": \"अधोरेखित\",\n  \"fontColorDefault\": \"डीफॉल्ट\",\n  \"fontColorGray\": \"धूसर\",\n  \"fontColorBrown\": \"तपकिरी\",\n  \"fontColorOrange\": \"केशरी\",\n  \"fontColorYellow\": \"पिवळा\",\n  \"fontColorGreen\": \"हिरवा\",\n  \"fontColorBlue\": \"निळा\",\n  \"fontColorPurple\": \"जांभळा\",\n  \"fontColorPink\": \"पिंग\",\n  \"fontColorRed\": \"लाल\",\n  \"backgroundColorDefault\": \"डीफॉल्ट पार्श्वभूमी\",\n  \"backgroundColorGray\": \"धूसर पार्श्वभूमी\",\n  \"backgroundColorBrown\": \"तपकिरी पार्श्वभूमी\",\n  \"backgroundColorOrange\": \"केशरी पार्श्वभूमी\",\n  \"backgroundColorYellow\": \"पिवळी पार्श्वभूमी\",\n  \"backgroundColorGreen\": \"हिरवी पार्श्वभूमी\",\n  \"backgroundColorBlue\": \"निळी पार्श्वभूमी\",\n  \"backgroundColorPurple\": \"जांभळी पार्श्वभूमी\",\n  \"backgroundColorPink\": \"पिंग पार्श्वभूमी\",\n  \"backgroundColorRed\": \"लाल पार्श्वभूमी\",\n  \"backgroundColorLime\": \"लिंबू पार्श्वभूमी\",\n  \"backgroundColorAqua\": \"पाण्याचा पार्श्वभूमी\",\n  \"done\": \"पूर्ण\",\n  \"cancel\": \"रद्द करा\",\n  \"tint1\": \"टिंट 1\",\n  \"tint2\": \"टिंट 2\",\n  \"tint3\": \"टिंट 3\",\n  \"tint4\": \"टिंट 4\",\n  \"tint5\": \"टिंट 5\",\n  \"tint6\": \"टिंट 6\",\n  \"tint7\": \"टिंट 7\",\n  \"tint8\": \"टिंट 8\",\n  \"tint9\": \"टिंट 9\",\n  \"lightLightTint1\": \"जांभळा\",\n  \"lightLightTint2\": \"पिंग\",\n  \"lightLightTint3\": \"फिकट पिंग\",\n  \"lightLightTint4\": \"केशरी\",\n  \"lightLightTint5\": \"पिवळा\",\n  \"lightLightTint6\": \"लिंबू\",\n  \"lightLightTint7\": \"हिरवा\",\n  \"lightLightTint8\": \"पाणी\",\n  \"lightLightTint9\": \"निळा\",\n  \"urlHint\": \"URL\",\n  \"mobileHeading1\": \"Heading 1\",\n  \"mobileHeading2\": \"Heading 2\",\n  \"mobileHeading3\": \"Heading 3\",\n  \"mobileHeading4\": \"Heading 4\",\n  \"mobileHeading5\": \"Heading 5\",\n  \"mobileHeading6\": \"Heading 6\",\n  \"textColor\": \"मजकूराचा रंग\",\n  \"backgroundColor\": \"पार्श्वभूमीचा रंग\",\n  \"addYourLink\": \"तुमची लिंक जोडा\",\n  \"openLink\": \"लिंक उघडा\",\n  \"copyLink\": \"लिंक कॉपी करा\",\n  \"removeLink\": \"लिंक काढा\",\n  \"editLink\": \"लिंक संपादित करा\",\n  \"linkText\": \"मजकूर\",\n  \"linkTextHint\": \"कृपया मजकूर प्रविष्ट करा\",\n  \"linkAddressHint\": \"कृपया URL प्रविष्ट करा\",\n  \"highlightColor\": \"हायलाइट रंग\",\n  \"clearHighlightColor\": \"हायलाइट काढा\",\n  \"customColor\": \"स्वतःचा रंग\",\n  \"hexValue\": \"Hex मूल्य\",\n  \"opacity\": \"अपारदर्शकता\",\n  \"resetToDefaultColor\": \"डीफॉल्ट रंगावर रीसेट करा\",\n  \"ltr\": \"LTR\",\n  \"rtl\": \"RTL\",\n  \"auto\": \"स्वयंचलित\",\n  \"cut\": \"कट\",\n  \"copy\": \"कॉपी\",\n  \"paste\": \"पेस्ट\",\n  \"find\": \"शोधा\",\n  \"select\": \"निवडा\",\n  \"selectAll\": \"सर्व निवडा\",\n  \"previousMatch\": \"मागील जुळणारे\",\n  \"nextMatch\": \"पुढील जुळणारे\",\n  \"closeFind\": \"बंद करा\",\n  \"replace\": \"बदला\",\n  \"replaceAll\": \"सर्व बदला\",\n  \"regex\": \"Regex\",\n  \"caseSensitive\": \"केस सेंसिटिव्ह\",\n  \"uploadImage\": \"प्रतिमा अपलोड करा\",\n  \"urlImage\": \"URL प्रतिमा\",\n  \"incorrectLink\": \"चुकीची लिंक\",\n  \"upload\": \"अपलोड\",\n  \"chooseImage\": \"प्रतिमा निवडा\",\n  \"loading\": \"लोड करत आहे\",\n  \"imageLoadFailed\": \"प्रतिमा लोड करण्यात अयशस्वी\",\n  \"divider\": \"विभाजक\",\n  \"table\": \"तक्त्याचे स्वरूप\",\n  \"colAddBefore\": \"यापूर्वी स्तंभ जोडा\",\n  \"rowAddBefore\": \"यापूर्वी पंक्ती जोडा\",\n  \"colAddAfter\": \"यानंतर स्तंभ जोडा\",\n  \"rowAddAfter\": \"यानंतर पंक्ती जोडा\",\n  \"colRemove\": \"स्तंभ काढा\",\n  \"rowRemove\": \"पंक्ती काढा\",\n  \"colDuplicate\": \"स्तंभ डुप्लिकेट\",\n  \"rowDuplicate\": \"पंक्ती डुप्लिकेट\",\n  \"colClear\": \"सामग्री साफ करा\",\n  \"rowClear\": \"सामग्री साफ करा\",\n  \"slashPlaceHolder\": \"'/' टाइप करा आणि घटक जोडा किंवा टाइप सुरू करा\",\n  \"typeSomething\": \"काहीतरी लिहा...\",\n  \"toggleListShortForm\": \"टॉगल\",\n  \"quoteListShortForm\": \"कोट\",\n  \"mathEquationShortForm\": \"सूत्र\",\n  \"codeBlockShortForm\": \"कोड\"\n},\n  \"favorite\": {\n  \"noFavorite\": \"कोणतेही आवडते पृष्ठ नाही\",\n  \"noFavoriteHintText\": \"पृष्ठाला डावीकडे स्वाइप करा आणि ते आवडत्या यादीत जोडा\",\n  \"removeFromSidebar\": \"साइडबारमधून काढा\",\n  \"addToSidebar\": \"साइडबारमध्ये पिन करा\"\n},\n\"cardDetails\": {\n  \"notesPlaceholder\": \"/ टाइप करा ब्लॉक घालण्यासाठी, किंवा टाइप करायला सुरुवात करा\"\n},\n\"blockPlaceholders\": {\n  \"todoList\": \"करण्याची यादी\",\n  \"bulletList\": \"यादी\",\n  \"numberList\": \"क्रमांकित यादी\",\n  \"quote\": \"कोट\",\n  \"heading\": \"मथळा {}\"\n},\n\"titleBar\": {\n  \"pageIcon\": \"पृष्ठ चिन्ह\",\n  \"language\": \"भाषा\",\n  \"font\": \"फॉन्ट\",\n  \"actions\": \"क्रिया\",\n  \"date\": \"तारीख\",\n  \"addField\": \"फील्ड जोडा\",\n  \"userIcon\": \"वापरकर्त्याचे चिन्ह\"\n},\n\"noLogFiles\": \"कोणतीही लॉग फाइल्स नाहीत\",\n\"newSettings\": {\n  \"myAccount\": {\n    \"title\": \"माझे खाते\",\n    \"subtitle\": \"तुमचा प्रोफाइल सानुकूल करा, खाते सुरक्षा व्यवस्थापित करा, AI कीज पहा किंवा लॉगिन करा.\",\n    \"profileLabel\": \"खाते नाव आणि प्रोफाइल चित्र\",\n    \"profileNamePlaceholder\": \"तुमचे नाव प्रविष्ट करा\",\n    \"accountSecurity\": \"खाते सुरक्षा\",\n    \"2FA\": \"2-स्टेप प्रमाणीकरण\",\n    \"aiKeys\": \"AI कीज\",\n    \"accountLogin\": \"खाते लॉगिन\",\n    \"updateNameError\": \"नाव अपडेट करण्यात अयशस्वी\",\n    \"updateIconError\": \"चिन्ह अपडेट करण्यात अयशस्वी\",\n    \"aboutAppFlowy\": \"@:appName विषयी\",\n    \"deleteAccount\": {\n      \"title\": \"खाते हटवा\",\n      \"subtitle\": \"तुमचे खाते आणि सर्व डेटा कायमचे हटवा.\",\n      \"description\": \"तुमचे खाते कायमचे हटवले जाईल आणि सर्व वर्कस्पेसमधून प्रवेश काढून टाकला जाईल.\",\n      \"deleteMyAccount\": \"माझे खाते हटवा\",\n      \"dialogTitle\": \"खाते हटवा\",\n      \"dialogContent1\": \"तुम्हाला खात्री आहे की तुम्ही तुमचे खाते कायमचे हटवू इच्छिता?\",\n      \"dialogContent2\": \"ही क्रिया पूर्ववत केली जाऊ शकत नाही. हे सर्व वर्कस्पेसमधून प्रवेश हटवेल, खाजगी वर्कस्पेस मिटवेल, आणि सर्व शेअर्ड वर्कस्पेसमधून काढून टाकेल.\",\n      \"confirmHint1\": \"कृपया पुष्टीसाठी \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\" टाइप करा.\",\n      \"confirmHint2\": \"मला समजले आहे की ही क्रिया अपरिवर्तनीय आहे आणि माझे खाते व सर्व संबंधित डेटा कायमचा हटवला जाईल.\",\n      \"confirmHint3\": \"DELETE MY ACCOUNT\",\n      \"checkToConfirmError\": \"हटवण्यासाठी पुष्टी बॉक्स निवडणे आवश्यक आहे\",\n      \"failedToGetCurrentUser\": \"वर्तमान वापरकर्त्याचा ईमेल मिळवण्यात अयशस्वी\",\n      \"confirmTextValidationFailed\": \"तुमचा पुष्टी मजकूर \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\" शी जुळत नाही\",\n      \"deleteAccountSuccess\": \"खाते यशस्वीरित्या हटवले गेले\"\n    }\n  },\n  \"workplace\": {\n    \"name\": \"वर्कस्पेस\",\n    \"title\": \"वर्कस्पेस सेटिंग्स\",\n    \"subtitle\": \"तुमचा वर्कस्पेस लुक, थीम, फॉन्ट, मजकूर लेआउट, तारीख, वेळ आणि भाषा सानुकूल करा.\",\n    \"workplaceName\": \"वर्कस्पेसचे नाव\",\n    \"workplaceNamePlaceholder\": \"वर्कस्पेसचे नाव टाका\",\n    \"workplaceIcon\": \"वर्कस्पेस चिन्ह\",\n    \"workplaceIconSubtitle\": \"एक प्रतिमा अपलोड करा किंवा इमोजी वापरा. हे साइडबार आणि सूचना मध्ये दर्शवले जाईल.\",\n    \"renameError\": \"वर्कस्पेसचे नाव बदलण्यात अयशस्वी\",\n    \"updateIconError\": \"चिन्ह अपडेट करण्यात अयशस्वी\",\n    \"chooseAnIcon\": \"चिन्ह निवडा\",\n    \"appearance\": {\n      \"name\": \"दृश्यरूप\",\n      \"themeMode\": {\n        \"auto\": \"स्वयंचलित\",\n        \"light\": \"प्रकाश मोड\",\n        \"dark\": \"गडद मोड\"\n      },\n      \"language\": \"भाषा\"\n    }\n  },\n  \"syncState\": {\n    \"syncing\": \"सिंक्रोनायझ करत आहे\",\n    \"synced\": \"सिंक्रोनायझ झाले\",\n    \"noNetworkConnected\": \"नेटवर्क कनेक्ट केलेले नाही\"\n  }\n},\n  \"pageStyle\": {\n  \"title\": \"पृष्ठ शैली\",\n  \"layout\": \"लेआउट\",\n  \"coverImage\": \"मुखपृष्ठ प्रतिमा\",\n  \"pageIcon\": \"पृष्ठ चिन्ह\",\n  \"colors\": \"रंग\",\n  \"gradient\": \"ग्रेडियंट\",\n  \"backgroundImage\": \"पार्श्वभूमी प्रतिमा\",\n  \"presets\": \"पूर्वनियोजित\",\n  \"photo\": \"फोटो\",\n  \"unsplash\": \"Unsplash\",\n  \"pageCover\": \"पृष्ठ कव्हर\",\n  \"none\": \"काही नाही\",\n  \"openSettings\": \"सेटिंग्स उघडा\",\n  \"photoPermissionTitle\": \"@:appName तुमच्या फोटो लायब्ररीमध्ये प्रवेश करू इच्छित आहे\",\n  \"photoPermissionDescription\": \"तुमच्या कागदपत्रांमध्ये प्रतिमा जोडण्यासाठी @:appName ला तुमच्या फोटोंमध्ये प्रवेश आवश्यक आहे\",\n  \"cameraPermissionTitle\": \"@:appName तुमच्या कॅमेऱ्याला प्रवेश करू इच्छित आहे\",\n  \"cameraPermissionDescription\": \"कॅमेऱ्यातून प्रतिमा जोडण्यासाठी @:appName ला तुमच्या कॅमेऱ्याचा प्रवेश आवश्यक आहे\",\n  \"doNotAllow\": \"परवानगी देऊ नका\",\n  \"image\": \"प्रतिमा\"\n},\n\"commandPalette\": {\n  \"placeholder\": \"शोधा किंवा प्रश्न विचारा...\",\n  \"bestMatches\": \"सर्वोत्तम जुळवणी\",\n  \"recentHistory\": \"अलीकडील इतिहास\",\n  \"navigateHint\": \"नेव्हिगेट करण्यासाठी\",\n  \"loadingTooltip\": \"आम्ही निकाल शोधत आहोत...\",\n  \"betaLabel\": \"बेटा\",\n  \"betaTooltip\": \"सध्या आम्ही फक्त दस्तऐवज आणि पृष्ठ शोध समर्थन करतो\",\n  \"fromTrashHint\": \"कचरापेटीतून\",\n  \"noResultsHint\": \"आपण जे शोधत आहात ते सापडले नाही, कृपया दुसरा शब्द वापरून शोधा.\",\n  \"clearSearchTooltip\": \"शोध फील्ड साफ करा\"\n},\n\"space\": {\n  \"delete\": \"हटवा\",\n  \"deleteConfirmation\": \"हटवा: \",\n  \"deleteConfirmationDescription\": \"या स्पेसमधील सर्व पृष्ठे हटवली जातील आणि कचरापेटीत टाकली जातील, आणि प्रकाशित पृष्ठे अनपब्लिश केली जातील.\",\n  \"rename\": \"स्पेसचे नाव बदला\",\n  \"changeIcon\": \"चिन्ह बदला\",\n  \"manage\": \"स्पेस व्यवस्थापित करा\",\n  \"addNewSpace\": \"स्पेस तयार करा\",\n  \"collapseAllSubPages\": \"सर्व उपपृष्ठे संकुचित करा\",\n  \"createNewSpace\": \"नवीन स्पेस तयार करा\",\n  \"createSpaceDescription\": \"तुमचे कार्य अधिक चांगल्या प्रकारे आयोजित करण्यासाठी अनेक सार्वजनिक व खाजगी स्पेस तयार करा.\",\n  \"spaceName\": \"स्पेसचे नाव\",\n  \"spaceNamePlaceholder\": \"उदा. मार्केटिंग, अभियांत्रिकी, HR\",\n  \"permission\": \"स्पेस परवानगी\",\n  \"publicPermission\": \"सार्वजनिक\",\n  \"publicPermissionDescription\": \"पूर्ण प्रवेशासह सर्व वर्कस्पेस सदस्य\",\n  \"privatePermission\": \"खाजगी\",\n  \"privatePermissionDescription\": \"फक्त तुम्हाला या स्पेसमध्ये प्रवेश आहे\",\n  \"spaceIconBackground\": \"पार्श्वभूमीचा रंग\",\n  \"spaceIcon\": \"चिन्ह\",\n  \"dangerZone\": \"धोकादायक क्षेत्र\",\n  \"unableToDeleteLastSpace\": \"शेवटची स्पेस हटवता येणार नाही\",\n  \"unableToDeleteSpaceNotCreatedByYou\": \"तुमच्याद्वारे तयार न केलेली स्पेस हटवता येणार नाही\",\n  \"enableSpacesForYourWorkspace\": \"तुमच्या वर्कस्पेससाठी स्पेस सक्षम करा\",\n  \"title\": \"स्पेसेस\",\n  \"defaultSpaceName\": \"सामान्य\",\n  \"upgradeSpaceTitle\": \"स्पेस सक्षम करा\",\n  \"upgradeSpaceDescription\": \"तुमच्या वर्कस्पेसचे अधिक चांगल्या प्रकारे व्यवस्थापन करण्यासाठी अनेक सार्वजनिक आणि खाजगी स्पेस तयार करा.\",\n  \"upgrade\": \"अपग्रेड\",\n  \"upgradeYourSpace\": \"अनेक स्पेस तयार करा\",\n  \"quicklySwitch\": \"पुढील स्पेसवर पटकन स्विच करा\",\n  \"duplicate\": \"स्पेस डुप्लिकेट करा\",\n  \"movePageToSpace\": \"पृष्ठ स्पेसमध्ये हलवा\",\n  \"cannotMovePageToDatabase\": \"पृष्ठ डेटाबेसमध्ये हलवता येणार नाही\",\n  \"switchSpace\": \"स्पेस स्विच करा\",\n  \"spaceNameCannotBeEmpty\": \"स्पेसचे नाव रिकामे असू शकत नाही\",\n  \"success\": {\n    \"deleteSpace\": \"स्पेस यशस्वीरित्या हटवली\",\n    \"renameSpace\": \"स्पेसचे नाव यशस्वीरित्या बदलले\",\n    \"duplicateSpace\": \"स्पेस यशस्वीरित्या डुप्लिकेट केली\",\n    \"updateSpace\": \"स्पेस यशस्वीरित्या अपडेट केली\"\n  },\n  \"error\": {\n    \"deleteSpace\": \"स्पेस हटवण्यात अयशस्वी\",\n    \"renameSpace\": \"स्पेसचे नाव बदलण्यात अयशस्वी\",\n    \"duplicateSpace\": \"स्पेस डुप्लिकेट करण्यात अयशस्वी\",\n    \"updateSpace\": \"स्पेस अपडेट करण्यात अयशस्वी\"\n  },\n  \"createSpace\": \"स्पेस तयार करा\",\n  \"manageSpace\": \"स्पेस व्यवस्थापित करा\",\n  \"renameSpace\": \"स्पेसचे नाव बदला\",\n  \"mSpaceIconColor\": \"स्पेस चिन्हाचा रंग\",\n  \"mSpaceIcon\": \"स्पेस चिन्ह\"\n},\n  \"publish\": {\n  \"hasNotBeenPublished\": \"हे पृष्ठ अजून प्रकाशित केलेले नाही\",\n  \"spaceHasNotBeenPublished\": \"स्पेस प्रकाशित करण्यासाठी समर्थन नाही\",\n  \"reportPage\": \"पृष्ठाची तक्रार करा\",\n  \"databaseHasNotBeenPublished\": \"डेटाबेस प्रकाशित करण्यास समर्थन नाही.\",\n  \"createdWith\": \"यांनी तयार केले\",\n  \"downloadApp\": \"AppFlowy डाउनलोड करा\",\n  \"copy\": {\n    \"codeBlock\": \"कोड ब्लॉकची सामग्री क्लिपबोर्डवर कॉपी केली गेली आहे\",\n    \"imageBlock\": \"प्रतिमा लिंक क्लिपबोर्डवर कॉपी केली गेली आहे\",\n    \"mathBlock\": \"गणितीय समीकरण क्लिपबोर्डवर कॉपी केले गेले आहे\",\n    \"fileBlock\": \"फाइल लिंक क्लिपबोर्डवर कॉपी केली गेली आहे\"\n  },\n  \"containsPublishedPage\": \"या पृष्ठात एक किंवा अधिक प्रकाशित पृष्ठे आहेत. तुम्ही पुढे गेल्यास ती अनपब्लिश होतील. तुम्हाला हटवणे चालू ठेवायचे आहे का?\",\n  \"publishSuccessfully\": \"यशस्वीरित्या प्रकाशित झाले\",\n  \"unpublishSuccessfully\": \"यशस्वीरित्या अनपब्लिश झाले\",\n  \"publishFailed\": \"प्रकाशित करण्यात अयशस्वी\",\n  \"unpublishFailed\": \"अनपब्लिश करण्यात अयशस्वी\",\n  \"noAccessToVisit\": \"या पृष्ठावर प्रवेश नाही...\",\n  \"createWithAppFlowy\": \"AppFlowy ने वेबसाइट तयार करा\",\n  \"fastWithAI\": \"AI सह जलद आणि सोपे.\",\n  \"tryItNow\": \"आत्ताच वापरून पहा\",\n  \"onlyGridViewCanBePublished\": \"फक्त Grid view प्रकाशित केला जाऊ शकतो\",\n  \"database\": {\n    \"zero\": \"{} निवडलेले दृश्य प्रकाशित करा\",\n    \"one\": \"{} निवडलेली दृश्ये प्रकाशित करा\",\n    \"many\": \"{} निवडलेली दृश्ये प्रकाशित करा\",\n    \"other\": \"{} निवडलेली दृश्ये प्रकाशित करा\"\n  },\n  \"mustSelectPrimaryDatabase\": \"प्राथमिक दृश्य निवडणे आवश्यक आहे\",\n  \"noDatabaseSelected\": \"कोणताही डेटाबेस निवडलेला नाही, कृपया किमान एक डेटाबेस निवडा.\",\n  \"unableToDeselectPrimaryDatabase\": \"प्राथमिक डेटाबेस अननिवड करता येणार नाही\",\n  \"saveThisPage\": \"या टेम्पलेटपासून सुरू करा\",\n  \"duplicateTitle\": \"तुम्हाला हे कुठे जोडायचे आहे\",\n  \"selectWorkspace\": \"वर्कस्पेस निवडा\",\n  \"addTo\": \"मध्ये जोडा\",\n  \"duplicateSuccessfully\": \"तुमच्या वर्कस्पेसमध्ये जोडले गेले\",\n  \"duplicateSuccessfullyDescription\": \"AppFlowy स्थापित केले नाही? तुम्ही 'डाउनलोड' वर क्लिक केल्यावर डाउनलोड आपोआप सुरू होईल.\",\n  \"downloadIt\": \"डाउनलोड करा\",\n  \"openApp\": \"अ‍ॅपमध्ये उघडा\",\n  \"duplicateFailed\": \"डुप्लिकेट करण्यात अयशस्वी\",\n  \"membersCount\": {\n    \"zero\": \"सदस्य नाहीत\",\n    \"one\": \"1 सदस्य\",\n    \"many\": \"{count} सदस्य\",\n    \"other\": \"{count} सदस्य\"\n  },\n  \"useThisTemplate\": \"हा टेम्पलेट वापरा\"\n},\n\"web\": {\n  \"continue\": \"पुढे जा\",\n  \"or\": \"किंवा\",\n  \"continueWithGoogle\": \"Google सह पुढे जा\",\n  \"continueWithGithub\": \"GitHub सह पुढे जा\",\n  \"continueWithDiscord\": \"Discord सह पुढे जा\",\n  \"continueWithApple\": \"Apple सह पुढे जा\",\n  \"moreOptions\": \"अधिक पर्याय\",\n  \"collapse\": \"आकुंचन\",\n  \"signInAgreement\": \"\\\"पुढे जा\\\" क्लिक करून, तुम्ही AppFlowy च्या अटींना सहमती दिली आहे\",\n  \"signInLocalAgreement\": \"\\\"सुरुवात करा\\\" क्लिक करून, तुम्ही AppFlowy च्या अटींना सहमती दिली आहे\",\n  \"and\": \"आणि\",\n  \"termOfUse\": \"वापर अटी\",\n  \"privacyPolicy\": \"गोपनीयता धोरण\",\n  \"signInError\": \"साइन इन त्रुटी\",\n  \"login\": \"साइन अप किंवा लॉग इन करा\",\n  \"fileBlock\": {\n    \"uploadedAt\": \"{time} रोजी अपलोड केले\",\n    \"linkedAt\": \"{time} रोजी लिंक जोडली\",\n    \"empty\": \"फाईल अपलोड करा किंवा एम्बेड करा\",\n    \"uploadFailed\": \"अपलोड अयशस्वी, कृपया पुन्हा प्रयत्न करा\",\n    \"retry\": \"पुन्हा प्रयत्न करा\"\n  },\n  \"importNotion\": \"Notion वरून आयात करा\",\n  \"import\": \"आयात करा\",\n  \"importSuccess\": \"यशस्वीरित्या अपलोड केले\",\n  \"importSuccessMessage\": \"आम्ही तुम्हाला आयात पूर्ण झाल्यावर सूचित करू. त्यानंतर, तुम्ही साइडबारमध्ये तुमची आयात केलेली पृष्ठे पाहू शकता.\",\n  \"importFailed\": \"आयात अयशस्वी, कृपया फाईल फॉरमॅट तपासा\",\n  \"dropNotionFile\": \"तुमची Notion zip फाईल येथे ड्रॉप करा किंवा ब्राउझ करा\",\n  \"error\": {\n    \"pageNameIsEmpty\": \"पृष्ठाचे नाव रिकामे आहे, कृपया दुसरे नाव वापरून पहा\"\n  }\n},\n  \"globalComment\": {\n  \"comments\": \"टिप्पण्या\",\n  \"addComment\": \"टिप्पणी जोडा\",\n  \"reactedBy\": \"यांनी प्रतिक्रिया दिली\",\n  \"addReaction\": \"प्रतिक्रिया जोडा\",\n  \"reactedByMore\": \"आणि {count} इतर\",\n  \"showSeconds\": {\n    \"one\": \"1 सेकंदापूर्वी\",\n    \"other\": \"{count} सेकंदांपूर्वी\",\n    \"zero\": \"आत्ताच\",\n    \"many\": \"{count} सेकंदांपूर्वी\"\n  },\n  \"showMinutes\": {\n    \"one\": \"1 मिनिटापूर्वी\",\n    \"other\": \"{count} मिनिटांपूर्वी\",\n    \"many\": \"{count} मिनिटांपूर्वी\"\n  },\n  \"showHours\": {\n    \"one\": \"1 तासापूर्वी\",\n    \"other\": \"{count} तासांपूर्वी\",\n    \"many\": \"{count} तासांपूर्वी\"\n  },\n  \"showDays\": {\n    \"one\": \"1 दिवसापूर्वी\",\n    \"other\": \"{count} दिवसांपूर्वी\",\n    \"many\": \"{count} दिवसांपूर्वी\"\n  },\n  \"showMonths\": {\n    \"one\": \"1 महिन्यापूर्वी\",\n    \"other\": \"{count} महिन्यांपूर्वी\",\n    \"many\": \"{count} महिन्यांपूर्वी\"\n  },\n  \"showYears\": {\n    \"one\": \"1 वर्षापूर्वी\",\n    \"other\": \"{count} वर्षांपूर्वी\",\n    \"many\": \"{count} वर्षांपूर्वी\"\n  },\n  \"reply\": \"उत्तर द्या\",\n  \"deleteComment\": \"टिप्पणी हटवा\",\n  \"youAreNotOwner\": \"तुम्ही या टिप्पणीचे मालक नाही\",\n  \"confirmDeleteDescription\": \"तुम्हाला ही टिप्पणी हटवायची आहे याची खात्री आहे का?\",\n  \"hasBeenDeleted\": \"हटवले गेले\",\n  \"replyingTo\": \"याला उत्तर देत आहे\",\n  \"noAccessDeleteComment\": \"तुम्हाला ही टिप्पणी हटवण्याची परवानगी नाही\",\n  \"collapse\": \"संकुचित करा\",\n  \"readMore\": \"अधिक वाचा\",\n  \"failedToAddComment\": \"टिप्पणी जोडण्यात अयशस्वी\",\n  \"commentAddedSuccessfully\": \"टिप्पणी यशस्वीरित्या जोडली गेली.\",\n  \"commentAddedSuccessTip\": \"तुम्ही नुकतीच एक टिप्पणी जोडली किंवा उत्तर दिले आहे. वर जाऊन ताजी टिप्पण्या पाहायच्या का?\"\n},\n  \"template\": {\n  \"asTemplate\": \"टेम्पलेट म्हणून जतन करा\",\n  \"name\": \"टेम्पलेट नाव\",\n  \"description\": \"टेम्पलेट वर्णन\",\n  \"about\": \"टेम्पलेट माहिती\",\n  \"deleteFromTemplate\": \"टेम्पलेटमधून हटवा\",\n  \"preview\": \"टेम्पलेट पूर्वदृश्य\",\n  \"categories\": \"टेम्पलेट श्रेणी\",\n  \"isNewTemplate\": \"नवीन टेम्पलेटमध्ये पिन करा\",\n  \"featured\": \"वैशिष्ट्यीकृतमध्ये पिन करा\",\n  \"relatedTemplates\": \"संबंधित टेम्पलेट्स\",\n  \"requiredField\": \"{field} आवश्यक आहे\",\n  \"addCategory\": \"\\\"{category}\\\" जोडा\",\n  \"addNewCategory\": \"नवीन श्रेणी जोडा\",\n  \"addNewCreator\": \"नवीन निर्माता जोडा\",\n  \"deleteCategory\": \"श्रेणी हटवा\",\n  \"editCategory\": \"श्रेणी संपादित करा\",\n  \"editCreator\": \"निर्माता संपादित करा\",\n  \"category\": {\n    \"name\": \"श्रेणीचे नाव\",\n    \"icon\": \"श्रेणी चिन्ह\",\n    \"bgColor\": \"श्रेणी पार्श्वभूमीचा रंग\",\n    \"priority\": \"श्रेणी प्राधान्य\",\n    \"desc\": \"श्रेणीचे वर्णन\",\n    \"type\": \"श्रेणी प्रकार\",\n    \"icons\": \"श्रेणी चिन्हे\",\n    \"colors\": \"श्रेणी रंग\",\n    \"byUseCase\": \"वापराच्या आधारे\",\n    \"byFeature\": \"वैशिष्ट्यांनुसार\",\n    \"deleteCategory\": \"श्रेणी हटवा\",\n    \"deleteCategoryDescription\": \"तुम्हाला ही श्रेणी हटवायची आहे का?\",\n    \"typeToSearch\": \"श्रेणी शोधण्यासाठी टाइप करा...\"\n  },\n  \"creator\": {\n    \"label\": \"टेम्पलेट निर्माता\",\n    \"name\": \"निर्मात्याचे नाव\",\n    \"avatar\": \"निर्मात्याचा अवतार\",\n    \"accountLinks\": \"निर्मात्याचे खाते दुवे\",\n    \"uploadAvatar\": \"अवतार अपलोड करण्यासाठी क्लिक करा\",\n    \"deleteCreator\": \"निर्माता हटवा\",\n    \"deleteCreatorDescription\": \"तुम्हाला हा निर्माता हटवायचा आहे का?\",\n    \"typeToSearch\": \"निर्माते शोधण्यासाठी टाइप करा...\"\n  },\n  \"uploadSuccess\": \"टेम्पलेट यशस्वीरित्या अपलोड झाले\",\n  \"uploadSuccessDescription\": \"तुमचे टेम्पलेट यशस्वीरित्या अपलोड झाले आहे. आता तुम्ही ते टेम्पलेट गॅलरीमध्ये पाहू शकता.\",\n  \"viewTemplate\": \"टेम्पलेट पहा\",\n  \"deleteTemplate\": \"टेम्पलेट हटवा\",\n  \"deleteSuccess\": \"टेम्पलेट यशस्वीरित्या हटवले गेले\",\n  \"deleteTemplateDescription\": \"याचा वर्तमान पृष्ठ किंवा प्रकाशित स्थितीवर परिणाम होणार नाही. तुम्हाला हे टेम्पलेट हटवायचे आहे का?\",\n  \"addRelatedTemplate\": \"संबंधित टेम्पलेट जोडा\",\n  \"removeRelatedTemplate\": \"संबंधित टेम्पलेट हटवा\",\n  \"uploadAvatar\": \"अवतार अपलोड करा\",\n  \"searchInCategory\": \"{category} मध्ये शोधा\",\n  \"label\": \"टेम्पलेट्स\"\n},\n  \"fileDropzone\": {\n  \"dropFile\": \"फाइल अपलोड करण्यासाठी येथे क्लिक करा किंवा ड्रॅग करा\",\n  \"uploading\": \"अपलोड करत आहे...\",\n  \"uploadFailed\": \"अपलोड अयशस्वी\",\n  \"uploadSuccess\": \"अपलोड यशस्वी\",\n  \"uploadSuccessDescription\": \"फाइल यशस्वीरित्या अपलोड झाली आहे\",\n  \"uploadFailedDescription\": \"फाइल अपलोड अयशस्वी झाली आहे\",\n  \"uploadingDescription\": \"फाइल अपलोड होत आहे\"\n},\n  \"gallery\": {\n  \"preview\": \"पूर्ण स्क्रीनमध्ये उघडा\",\n  \"copy\": \"कॉपी करा\",\n  \"download\": \"डाउनलोड\",\n  \"prev\": \"मागील\",\n  \"next\": \"पुढील\",\n  \"resetZoom\": \"झूम रिसेट करा\",\n  \"zoomIn\": \"झूम इन\",\n  \"zoomOut\": \"झूम आउट\"\n},\n  \"invitation\": {\n  \"join\": \"सामील व्हा\",\n  \"on\": \"वर\",\n  \"invitedBy\": \"यांनी आमंत्रित केले\",\n  \"membersCount\": {\n    \"zero\": \"{count} सदस्य\",\n    \"one\": \"{count} सदस्य\",\n    \"many\": \"{count} सदस्य\",\n    \"other\": \"{count} सदस्य\"\n  },\n  \"tip\": \"तुम्हाला खालील माहितीच्या आधारे या कार्यक्षेत्रात सामील होण्यासाठी आमंत्रित करण्यात आले आहे. ही माहिती चुकीची असल्यास, कृपया प्रशासकाशी संपर्क साधा.\",\n  \"joinWorkspace\": \"वर्कस्पेसमध्ये सामील व्हा\",\n  \"success\": \"तुम्ही यशस्वीरित्या वर्कस्पेसमध्ये सामील झाला आहात\",\n  \"successMessage\": \"आता तुम्ही सर्व पृष्ठे आणि कार्यक्षेत्रे वापरू शकता.\",\n  \"openWorkspace\": \"AppFlowy उघडा\",\n  \"alreadyAccepted\": \"तुम्ही आधीच आमंत्रण स्वीकारले आहे\",\n  \"errorModal\": {\n    \"title\": \"काहीतरी चुकले आहे\",\n    \"description\": \"तुमचे सध्याचे खाते {email} कदाचित या वर्कस्पेसमध्ये प्रवेशासाठी पात्र नाही. कृपया योग्य खात्याने लॉग इन करा किंवा वर्कस्पेस मालकाशी संपर्क साधा.\",\n    \"contactOwner\": \"मालकाशी संपर्क करा\",\n    \"close\": \"मुख्यपृष्ठावर परत जा\",\n    \"changeAccount\": \"खाते बदला\"\n  }\n},\n  \"requestAccess\": {\n  \"title\": \"या पृष्ठासाठी प्रवेश नाही\",\n  \"subtitle\": \"तुम्ही या पृष्ठाच्या मालकाकडून प्रवेशासाठी विनंती करू शकता. मंजुरीनंतर तुम्हाला हे पृष्ठ पाहता येईल.\",\n  \"requestAccess\": \"प्रवेशाची विनंती करा\",\n  \"backToHome\": \"मुख्यपृष्ठावर परत जा\",\n  \"tip\": \"तुम्ही सध्या <link/> म्हणून लॉग इन आहात.\",\n  \"mightBe\": \"कदाचित तुम्हाला <login/> दुसऱ्या खात्याने लॉग इन करणे आवश्यक आहे.\",\n  \"successful\": \"विनंती यशस्वीपणे पाठवली गेली\",\n  \"successfulMessage\": \"मालकाने मंजुरी दिल्यावर तुम्हाला सूचित केले जाईल.\",\n  \"requestError\": \"प्रवेशाची विनंती अयशस्वी\",\n  \"repeatRequestError\": \"तुम्ही यासाठी आधीच विनंती केली आहे\"\n},\n  \"approveAccess\": {\n  \"title\": \"वर्कस्पेसमध्ये सामील होण्यासाठी विनंती मंजूर करा\",\n  \"requestSummary\": \"<user/> यांनी <workspace/> मध्ये सामील होण्यासाठी आणि <page/> पाहण्यासाठी विनंती केली आहे\",\n  \"upgrade\": \"अपग्रेड\",\n  \"downloadApp\": \"AppFlowy डाउनलोड करा\",\n  \"approveButton\": \"मंजूर करा\",\n  \"approveSuccess\": \"मंजूर यशस्वी\",\n  \"approveError\": \"मंजुरी अयशस्वी. कृपया वर्कस्पेस मर्यादा ओलांडलेली नाही याची खात्री करा\",\n  \"getRequestInfoError\": \"विनंतीची माहिती मिळवण्यात अयशस्वी\",\n  \"memberCount\": {\n    \"zero\": \"कोणतेही सदस्य नाहीत\",\n    \"one\": \"1 सदस्य\",\n    \"many\": \"{count} सदस्य\",\n    \"other\": \"{count} सदस्य\"\n  },\n  \"alreadyProTitle\": \"वर्कस्पेस योजना मर्यादा गाठली आहे\",\n  \"alreadyProMessage\": \"अधिक सदस्यांसाठी <email/> शी संपर्क साधा\",\n  \"repeatApproveError\": \"तुम्ही ही विनंती आधीच मंजूर केली आहे\",\n  \"ensurePlanLimit\": \"कृपया खात्री करा की योजना मर्यादा ओलांडलेली नाही. जर ओलांडली असेल तर वर्कस्पेस योजना <upgrade/> करा किंवा <download/> करा.\",\n  \"requestToJoin\": \"मध्ये सामील होण्यासाठी विनंती केली\",\n  \"asMember\": \"सदस्य म्हणून\"\n},\n  \"upgradePlanModal\": {\n  \"title\": \"Pro प्लॅनवर अपग्रेड करा\",\n  \"message\": \"{name} ने फ्री सदस्य मर्यादा गाठली आहे. अधिक सदस्य आमंत्रित करण्यासाठी Pro प्लॅनवर अपग्रेड करा.\",\n  \"upgradeSteps\": \"AppFlowy वर तुमची योजना कशी अपग्रेड करावी:\",\n  \"step1\": \"1. सेटिंग्जमध्ये जा\",\n  \"step2\": \"2. 'योजना' वर क्लिक करा\",\n  \"step3\": \"3. 'योजना बदला' निवडा\",\n  \"appNote\": \"नोंद:\",\n  \"actionButton\": \"अपग्रेड करा\",\n  \"downloadLink\": \"अ‍ॅप डाउनलोड करा\",\n  \"laterButton\": \"नंतर\",\n  \"refreshNote\": \"यशस्वी अपग्रेडनंतर, तुमची नवीन वैशिष्ट्ये सक्रिय करण्यासाठी <refresh/> वर क्लिक करा.\",\n  \"refresh\": \"येथे\"\n},\n  \"breadcrumbs\": {\n  \"label\": \"ब्रेडक्रम्स\"\n},\n  \"time\": {\n  \"justNow\": \"आत्ताच\",\n  \"seconds\": {\n    \"one\": \"1 सेकंद\",\n    \"other\": \"{count} सेकंद\"\n  },\n  \"minutes\": {\n    \"one\": \"1 मिनिट\",\n    \"other\": \"{count} मिनिटे\"\n  },\n  \"hours\": {\n    \"one\": \"1 तास\",\n    \"other\": \"{count} तास\"\n  },\n  \"days\": {\n    \"one\": \"1 दिवस\",\n    \"other\": \"{count} दिवस\"\n  },\n  \"weeks\": {\n    \"one\": \"1 आठवडा\",\n    \"other\": \"{count} आठवडे\"\n  },\n  \"months\": {\n    \"one\": \"1 महिना\",\n    \"other\": \"{count} महिने\"\n  },\n  \"years\": {\n    \"one\": \"1 वर्ष\",\n    \"other\": \"{count} वर्षे\"\n  },\n  \"ago\": \"पूर्वी\",\n  \"yesterday\": \"काल\",\n  \"today\": \"आज\"\n},\n  \"members\": {\n  \"zero\": \"सदस्य नाहीत\",\n  \"one\": \"1 सदस्य\",\n  \"many\": \"{count} सदस्य\",\n  \"other\": \"{count} सदस्य\"\n},\n  \"tabMenu\": {\n  \"close\": \"बंद करा\",\n  \"closeDisabledHint\": \"पिन केलेले टॅब बंद करता येत नाही, कृपया आधी अनपिन करा\",\n  \"closeOthers\": \"इतर टॅब बंद करा\",\n  \"closeOthersHint\": \"हे सर्व अनपिन केलेले टॅब्स बंद करेल या टॅब वगळता\",\n  \"closeOthersDisabledHint\": \"सर्व टॅब पिन केलेले आहेत, बंद करण्यासाठी कोणतेही टॅब सापडले नाहीत\",\n  \"favorite\": \"आवडते\",\n  \"unfavorite\": \"आवडते काढा\",\n  \"favoriteDisabledHint\": \"हे दृश्य आवडते म्हणून जतन करता येत नाही\",\n  \"pinTab\": \"पिन करा\",\n  \"unpinTab\": \"अनपिन करा\"\n},\n  \"openFileMessage\": {\n  \"success\": \"फाइल यशस्वीरित्या उघडली\",\n  \"fileNotFound\": \"फाइल सापडली नाही\",\n  \"noAppToOpenFile\": \"ही फाइल उघडण्यासाठी कोणतेही अ‍ॅप उपलब्ध नाही\",\n  \"permissionDenied\": \"ही फाइल उघडण्यासाठी परवानगी नाही\",\n  \"unknownError\": \"फाइल उघडण्यात अयशस्वी\"\n},\n  \"inviteMember\": {\n  \"requestInviteMembers\": \"तुमच्या वर्कस्पेसमध्ये आमंत्रित करा\",\n  \"inviteFailedMemberLimit\": \"सदस्य मर्यादा गाठली आहे, कृपया \",\n  \"upgrade\": \"अपग्रेड करा\",\n  \"addEmail\": \"email@example.com, email2@example.com...\",\n  \"requestInvites\": \"आमंत्रण पाठवा\",\n  \"inviteAlready\": \"तुम्ही या ईमेलला आधीच आमंत्रित केले आहे: {email}\",\n  \"inviteSuccess\": \"आमंत्रण यशस्वीपणे पाठवले\",\n  \"description\": \"खाली ईमेल कॉमा वापरून टाका. शुल्क सदस्य संख्येवर आधारित असते.\",\n  \"emails\": \"ईमेल\"\n},\n  \"quickNote\": {\n  \"label\": \"झटपट नोंद\",\n  \"quickNotes\": \"झटपट नोंदी\",\n  \"search\": \"झटपट नोंदी शोधा\",\n  \"collapseFullView\": \"पूर्ण दृश्य लपवा\",\n  \"expandFullView\": \"पूर्ण दृश्य उघडा\",\n  \"createFailed\": \"झटपट नोंद तयार करण्यात अयशस्वी\",\n  \"quickNotesEmpty\": \"कोणत्याही झटपट नोंदी नाहीत\",\n  \"emptyNote\": \"रिकामी नोंद\",\n  \"deleteNotePrompt\": \"निवडलेली नोंद कायमची हटवली जाईल. तुम्हाला नक्की ती हटवायची आहे का?\",\n  \"addNote\": \"नवीन नोंद\",\n  \"noAdditionalText\": \"अधिक माहिती नाही\"\n},\n  \"subscribe\": {\n  \"upgradePlanTitle\": \"योजना तुलना करा आणि निवडा\",\n  \"yearly\": \"वार्षिक\",\n  \"save\": \"{discount}% बचत\",\n  \"monthly\": \"मासिक\",\n  \"priceIn\": \"किंमत येथे: \",\n  \"free\": \"फ्री\",\n  \"pro\": \"प्रो\",\n  \"freeDescription\": \"2 सदस्यांपर्यंत वैयक्तिक वापरकर्त्यांसाठी सर्व काही आयोजित करण्यासाठी\",\n  \"proDescription\": \"प्रकल्प आणि टीम नॉलेज व्यवस्थापित करण्यासाठी लहान टीमसाठी\",\n  \"proDuration\": {\n    \"monthly\": \"प्रति सदस्य प्रति महिना\\nमासिक बिलिंग\",\n    \"yearly\": \"प्रति सदस्य प्रति महिना\\nवार्षिक बिलिंग\"\n  },\n  \"cancel\": \"खालच्या योजनेवर जा\",\n  \"changePlan\": \"प्रो योजनेवर अपग्रेड करा\",\n  \"everythingInFree\": \"फ्री योजनेतील सर्व काही +\",\n  \"currentPlan\": \"सध्याची योजना\",\n  \"freeDuration\": \"कायम\",\n  \"freePoints\": {\n    \"first\": \"1 सहकार्यात्मक वर्कस्पेस (2 सदस्यांपर्यंत)\",\n    \"second\": \"अमर्यादित पृष्ठे आणि ब्लॉक्स\",\n    \"three\": \"5 GB संचयन\",\n    \"four\": \"बुद्धिमान शोध\",\n    \"five\": \"20 AI प्रतिसाद\",\n    \"six\": \"मोबाईल अ‍ॅप\",\n    \"seven\": \"रिअल-टाइम सहकार्य\"\n  },\n  \"proPoints\": {\n    \"first\": \"अमर्यादित संचयन\",\n    \"second\": \"10 वर्कस्पेस सदस्यांपर्यंत\",\n    \"three\": \"अमर्यादित AI प्रतिसाद\",\n    \"four\": \"अमर्यादित फाइल अपलोड्स\",\n    \"five\": \"कस्टम नेमस्पेस\"\n  },\n  \"cancelPlan\": {\n    \"title\": \"आपल्याला जाताना पाहून वाईट वाटते\",\n    \"success\": \"आपली सदस्यता यशस्वीरित्या रद्द झाली आहे\",\n    \"description\": \"आपल्याला जाताना वाईट वाटते. कृपया आम्हाला सुधारण्यासाठी तुमचे अभिप्राय कळवा. खालील काही प्रश्नांना उत्तर द्या.\",\n    \"commonOther\": \"इतर\",\n    \"otherHint\": \"आपले उत्तर येथे लिहा\",\n    \"questionOne\": {\n      \"question\": \"तुम्ही AppFlowy Pro सदस्यता का रद्द केली?\",\n      \"answerOne\": \"खर्च खूप जास्त आहे\",\n      \"answerTwo\": \"वैशिष्ट्ये अपेक्षेनुसार नव्हती\",\n      \"answerThree\": \"चांगला पर्याय सापडला\",\n      \"answerFour\": \"खर्च योग्य ठरण्यासाठी वापर पुरेसा नव्हता\",\n      \"answerFive\": \"सेवा समस्या किंवा तांत्रिक अडचणी\"\n    },\n    \"questionTwo\": {\n      \"question\": \"तुम्ही भविष्यात AppFlowy Pro पुन्हा घेण्याची शक्यता किती आहे?\",\n      \"answerOne\": \"खूप शक्यता आहे\",\n      \"answerTwo\": \"काहीशी शक्यता आहे\",\n      \"answerThree\": \"निश्चित नाही\",\n      \"answerFour\": \"अल्प शक्यता आहे\",\n      \"answerFive\": \"शक्यता नाही\"\n    },\n    \"questionThree\": {\n      \"question\": \"सदस्यतेदरम्यान कोणते Pro फिचर तुम्हाला सर्वात जास्त उपयोगी वाटले?\",\n      \"answerOne\": \"मल्टी-यूजर सहकार्य\",\n      \"answerTwo\": \"लांब कालावधीसाठी आवृत्ती इतिहास\",\n      \"answerThree\": \"अमर्यादित AI प्रतिसाद\",\n      \"answerFour\": \"स्थानिक AI मॉडेल्सचा प्रवेश\"\n    },\n    \"questionFour\": {\n      \"question\": \"AppFlowy बाबत तुमचा एकूण अनुभव कसा होता?\",\n      \"answerOne\": \"छान\",\n      \"answerTwo\": \"चांगला\",\n      \"answerThree\": \"सामान्य\",\n      \"answerFour\": \"थोडासा वाईट\",\n      \"answerFive\": \"असंतोषजनक\"\n    }\n  }\n},\n  \"ai\": {\n  \"contentPolicyViolation\": \"संवेदनशील सामग्रीमुळे प्रतिमा निर्मिती अयशस्वी झाली. कृपया तुमचे इनपुट पुन्हा लिहा आणि पुन्हा प्रयत्न करा.\",\n  \"textLimitReachedDescription\": \"तुमच्या वर्कस्पेसमध्ये मोफत AI प्रतिसाद संपले आहेत. अनलिमिटेड प्रतिसादांसाठी प्रो योजना घेण्याचा किंवा AI अ‍ॅड-ऑन खरेदी करण्याचा विचार करा.\",\n  \"imageLimitReachedDescription\": \"तुमचे मोफत AI प्रतिमा कोटा संपले आहे. कृपया प्रो योजना घ्या किंवा AI अ‍ॅड-ऑन खरेदी करा.\",\n  \"limitReachedAction\": {\n    \"textDescription\": \"तुमच्या वर्कस्पेसमध्ये मोफत AI प्रतिसाद संपले आहेत. अधिक प्रतिसाद मिळवण्यासाठी कृपया\",\n    \"imageDescription\": \"तुमचे मोफत AI प्रतिमा कोटा संपले आहे. कृपया\",\n    \"upgrade\": \"अपग्रेड करा\",\n    \"toThe\": \"या योजनेवर\",\n    \"proPlan\": \"प्रो योजना\",\n    \"orPurchaseAn\": \"किंवा खरेदी करा\",\n    \"aiAddon\": \"AI अ‍ॅड-ऑन\"\n  },\n  \"editing\": \"संपादन करत आहे\",\n  \"analyzing\": \"विश्लेषण करत आहे\",\n  \"continueWritingEmptyDocumentTitle\": \"लेखन सुरू ठेवता आले नाही\",\n  \"continueWritingEmptyDocumentDescription\": \"तुमच्या दस्तऐवजातील मजकूर वाढवण्यात अडचण येत आहे. कृपया एक छोटं परिचय लिहा, मग आम्ही पुढे नेऊ!\",\n  \"more\": \"अधिक\"\n},\n  \"autoUpdate\": {\n  \"criticalUpdateTitle\": \"अद्यतन आवश्यक आहे\",\n  \"criticalUpdateDescription\": \"तुमचा अनुभव सुधारण्यासाठी आम्ही सुधारणा केल्या आहेत! कृपया {currentVersion} वरून {newVersion} वर अद्यतन करा.\",\n  \"criticalUpdateButton\": \"अद्यतन करा\",\n  \"bannerUpdateTitle\": \"नवीन आवृत्ती उपलब्ध!\",\n  \"bannerUpdateDescription\": \"नवीन वैशिष्ट्ये आणि सुधारणांसाठी. अद्यतनासाठी 'Update' क्लिक करा.\",\n  \"bannerUpdateButton\": \"अद्यतन करा\",\n  \"settingsUpdateTitle\": \"नवीन आवृत्ती ({newVersion}) उपलब्ध!\",\n  \"settingsUpdateDescription\": \"सध्याची आवृत्ती: {currentVersion} (अधिकृत बिल्ड) → {newVersion}\",\n  \"settingsUpdateButton\": \"अद्यतन करा\",\n  \"settingsUpdateWhatsNew\": \"काय नवीन आहे\"\n},\n  \"lockPage\": {\n  \"lockPage\": \"लॉक केलेले\",\n  \"reLockPage\": \"पुन्हा लॉक करा\",\n  \"lockTooltip\": \"अनवधानाने संपादन टाळण्यासाठी पृष्ठ लॉक केले आहे. अनलॉक करण्यासाठी क्लिक करा.\",\n  \"pageLockedToast\": \"पृष्ठ लॉक केले आहे. कोणी अनलॉक करेपर्यंत संपादन अक्षम आहे.\",\n  \"lockedOperationTooltip\": \"पृष्ठ अनवधानाने संपादनापासून वाचवण्यासाठी लॉक केले आहे.\"\n},\n  \"suggestion\": {\n  \"accept\": \"स्वीकारा\",\n  \"keep\": \"जसे आहे तसे ठेवा\",\n  \"discard\": \"रद्द करा\",\n  \"close\": \"बंद करा\",\n  \"tryAgain\": \"पुन्हा प्रयत्न करा\",\n  \"rewrite\": \"पुन्हा लिहा\",\n  \"insertBelow\": \"खाली टाका\"\n}\n}\n"
  },
  {
    "path": "frontend/resources/translations/pl-PL.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Ja\",\n  \"welcomeText\": \"Witaj w @:appName\",\n  \"welcomeTo\": \"Witamy w\",\n  \"githubStarText\": \"Gwiazdka na GitHub-ie\",\n  \"subscribeNewsletterText\": \"Zapisz się do naszego Newslettera\",\n  \"letsGoButtonText\": \"Start!\",\n  \"title\": \"Tytuł\",\n  \"youCanAlso\": \"Możesz również\",\n  \"and\": \"i\",\n  \"failedToOpenUrl\": \"Nie udało się otworzyć adresu URL: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Kliknij, aby dodać poniżej\",\n    \"addAboveCmd\": \"Alt+kliknięcie\",\n    \"addAboveMacCmd\": \"Opcja + kliknięcie\",\n    \"addAboveTooltip\": \"aby dodać powyżej\",\n    \"dragTooltip\": \"Przeciągnij aby przenieść\",\n    \"openMenuTooltip\": \"Kliknij żeby otworzyć menu\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Rejestracja\",\n    \"title\": \"Zarejestruj się w @:appName\",\n    \"getStartedText\": \"Rozpocznij\",\n    \"emptyPasswordError\": \"Hasło nie moze być puste\",\n    \"repeatPasswordEmptyError\": \"Powtórzone hasło nie moze być puste\",\n    \"unmatchedPasswordError\": \"Hasła nie są takie same\",\n    \"alreadyHaveAnAccount\": \"Masz juz konto?\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Hasło\",\n    \"repeatPasswordHint\": \"Powtórz hasło\",\n    \"signUpWith\": \"Zarejestruj się za pomocą:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Zaloguj do @:appName\",\n    \"loginButtonText\": \"Logowanie\",\n    \"loginStartWithAnonymous\": \"Rozpocznij sesję anonimową\",\n    \"continueAnonymousUser\": \"Kontynuuj sesję anonimową\",\n    \"buttonText\": \"Zaloguj\",\n    \"signingInText\": \"Logowanie się...\",\n    \"forgotPassword\": \"Zapomniałeś hasła?\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Hasło\",\n    \"dontHaveAnAccount\": \"Nie masz konta?\",\n    \"createAccount\": \"Utwórz konto\",\n    \"repeatPasswordEmptyError\": \"Powtórzone hasło nie moze być puste\",\n    \"unmatchedPasswordError\": \"Hasła nie są takie same\",\n    \"syncPromptMessage\": \"Synchronizacja danych może chwilę potrwać. Proszę nie zamykać tej strony\",\n    \"or\": \"ALBO\",\n    \"signInWithGoogle\": \"Zaloguj się za pomocą Google\",\n    \"signInWithGithub\": \"Zaloguj się za pomocą Githuba\",\n    \"signInWithDiscord\": \"Zaloguj się za pomocą Discorda\",\n    \"signUpWithGoogle\": \"Zarejestruj się za pomocą Google\",\n    \"signUpWithGithub\": \"Zarejestruj się za pomocą Githuba\",\n    \"signUpWithDiscord\": \"Zarejestruj się za pomocą Discorda\",\n    \"signInWith\": \"Zaloguj się korzystając z:\",\n    \"pleaseInputYourEmail\": \"Podaj swój adres e-mail\",\n    \"settings\": \"Ustawienia\",\n    \"logIn\": \"Zaloguj się\",\n    \"generalError\": \"Coś poszło nie tak. Spróbuj ponownie później\",\n    \"LogInWithGoogle\": \"Zaloguj się za pomocą Google\",\n    \"LogInWithGithub\": \"Zaloguj się za pomocą Githuba\",\n    \"LogInWithDiscord\": \"Zaloguj się za pomocą Discorda\",\n    \"loginAsGuestButtonText\": \"Rozpocznij\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Wybierz swoją przestrzeń do pracy\",\n    \"create\": \"Utwórz przestrzeń\",\n    \"reset\": \"Zresetuj przestrzeń roboczą\",\n    \"renameWorkspace\": \"Zmień nazwę obszaru roboczego\",\n    \"resetWorkspacePrompt\": \"Zresetowanie przestrzeni roboczej spowoduje usunięcie wszystkich znajdujących się w niej stron i danych. Czy na pewno chcesz zresetować przestrzeń roboczą? Alternatywnie możesz skontaktować się z zespołem pomocy technicznej, aby przywrócić przestrzeń roboczą\",\n    \"hint\": \"przestrzeń robocza\",\n    \"notFoundError\": \"Przestrzeni nie znaleziono\",\n    \"failedToLoad\": \"Coś poszło nie tak! Wczytywanie przestrzeni roboczej nie powiodło się. Spróbuj wyłączyć wszystkie otwarte instancje @:appName i spróbuj ponownie.\",\n    \"errorActions\": {\n      \"reportIssue\": \"Zgłoś problem\",\n      \"reportIssueOnGithub\": \"Zgłoś problem na Githubie\",\n      \"reachOut\": \"Skontaktuj się na Discord\"\n    },\n    \"menuTitle\": \"Obszary robocze\",\n    \"deleteWorkspaceHintText\": \"Czy na pewno chcesz usunąć obszar roboczy? Tej akcji nie można cofnąć.\",\n    \"createSuccess\": \"Obszar roboczy został utworzony pomyślnie\",\n    \"createFailed\": \"Nie udało się utworzyć obszaru roboczego\",\n    \"deleteSuccess\": \"Obszar roboczy został pomyślnie usunięty\",\n    \"deleteFailed\": \"Nie udało się usunąć obszaru roboczego\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Udostępnij\",\n    \"workInProgress\": \"Dostępne wkrótce\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"Skopiuj do schowka\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Skopiuj link\"\n  },\n  \"moreAction\": {\n    \"small\": \"mały\",\n    \"medium\": \"średni\",\n    \"large\": \"duży\",\n    \"fontSize\": \"Rozmiar czcionki\",\n    \"import\": \"Import\",\n    \"moreOptions\": \"Więcej opcji\",\n    \"wordCount\": \"Liczba słów: {}\",\n    \"charCount\": \"Liczba znaków: {}\",\n    \"createdAt\": \"Utworzony: {}\",\n    \"deleteView\": \"Usuń\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Tekst i Markdown\",\n    \"documentFromV010\": \"Dokument z wersji 0.1.0\",\n    \"databaseFromV010\": \"Baza danych z wersji 0.1.0\",\n    \"csv\": \"CSV\",\n    \"database\": \"Baza danych\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Zmień nazwę\",\n    \"delete\": \"Usuń\",\n    \"duplicate\": \"Duplikuj\",\n    \"unfavorite\": \"Usuń z ulubionych\",\n    \"favorite\": \"Dodaj do ulubionych\",\n    \"openNewTab\": \"Otwórz w nowej karcie\",\n    \"moveTo\": \"Przenieś do\",\n    \"addToFavorites\": \"Dodaj do ulubionych\",\n    \"copyLink\": \"Skopiuj link\",\n    \"changeIcon\": \"Zmień ikonę\"\n  },\n  \"blankPageTitle\": \"Pusta strona\",\n  \"newPageText\": \"Nowa strona\",\n  \"newDocumentText\": \"Nowy dokument\",\n  \"newGridText\": \"Nowa siatka\",\n  \"newCalendarText\": \"Nowy kalendarz\",\n  \"newBoardText\": \"Nowa tablica\",\n  \"trash\": {\n    \"text\": \"Kosz\",\n    \"restoreAll\": \"Przywróć Wszystko\",\n    \"deleteAll\": \"Usuń Wszystko\",\n    \"pageHeader\": {\n      \"fileName\": \"Nazwa Pliku\",\n      \"lastModified\": \"Ostatnio Zmodyfikowano\",\n      \"created\": \"Utworzono\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Czy na pewno chcesz usunąć wszystkie strony z Kosza?\",\n      \"caption\": \"Tej czynności nie można cofnąć.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Czy na pewno chcesz przywrócić wszystkie strony w Koszu?\",\n      \"caption\": \"Tej czynności nie można cofnąć.\"\n    },\n    \"mobile\": {\n      \"actions\": \"Kosz - Akcje\",\n      \"empty\": \"Koszt jest pusty\",\n      \"emptyDescription\": \"Nie masz żadnych usuniętych plików\",\n      \"isDeleted\": \"jest usunięty\",\n      \"isRestored\": \"jest przywrócony\"\n    }\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Ta strona jest w Koszu\",\n    \"restore\": \"Przywróć strone\",\n    \"deletePermanent\": \"Usuń bezpowrotnie\"\n  },\n  \"dialogCreatePageNameHint\": \"Nazwa Strony\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Skróty\",\n    \"whatsNew\": \"Co nowego?\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"Informacje Debugowania\",\n      \"success\": \"Skopiowano informacje debugowania do schowka!\",\n      \"fail\": \"Nie mozna skopiować informacji debugowania do schowka\"\n    },\n    \"feedback\": \"Feedback\",\n    \"help\": \"Pomoc & Wsparcie\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Usuń, zmień nazwę i więcej...\",\n    \"addPageTooltip\": \"Szybko dodaj stronę\",\n    \"defaultNewPageName\": \"Brak tytułu\",\n    \"renameDialog\": \"Zmień nazwę\"\n  },\n  \"noPagesInside\": \"Brak stron wewnątrz\",\n  \"toolbar\": {\n    \"undo\": \"Cofnij\",\n    \"redo\": \"Powtórz\",\n    \"bold\": \"Pogrubiony\",\n    \"italic\": \"Kursywa\",\n    \"underline\": \"Podkreślenie\",\n    \"strike\": \"Przekreślenie\",\n    \"numList\": \"Lista Numerowana\",\n    \"bulletList\": \"Lista Punktowana\",\n    \"checkList\": \"Lista Kontrolna\",\n    \"inlineCode\": \"Wbudowany Kod\",\n    \"quote\": \"Blok cytatu\",\n    \"header\": \"Nagłówek\",\n    \"highlight\": \"Podświetlenie\",\n    \"color\": \"Kolor\",\n    \"addLink\": \"Dodaj link\",\n    \"link\": \"Połącz\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Przełącz w Tryb Jasny\",\n    \"darkMode\": \"Przełącz w Tryb Ciemny\",\n    \"openAsPage\": \"Otwórz jako stronę\",\n    \"addNewRow\": \"Dodaj nowy wiersz\",\n    \"openMenu\": \"Kliknij, aby otworzyć menu\",\n    \"dragRow\": \"Naciśnij i przytrzymaj, aby zmienić kolejność wierszy\",\n    \"viewDataBase\": \"Zobacz bazę danych\",\n    \"referencePage\": \"Odwołuje się do tego {nazwa}\",\n    \"addBlockBelow\": \"Dodaj blok poniżej\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Zamknij menu boczne\",\n    \"openSidebar\": \"Otwórz menu boczne\",\n    \"personal\": \"Osobisty\",\n    \"favorites\": \"Ulubione\",\n    \"clickToHidePersonal\": \"Kliknij, aby ukryć sekcję osobistą\",\n    \"clickToHideFavorites\": \"Kliknij, aby ukryć ulubioną sekcję\",\n    \"addAPage\": \"Dodaj stronę\",\n    \"recent\": \"Najnowsze\",\n    \"today\": \"Dziś\",\n    \"thisWeek\": \"Ten tydzień\",\n    \"favoriteSpace\": \"Ulubione\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Wyeksportowano notatkę do Markdown\",\n      \"path\": \"Dokumenty/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Kontakty\",\n    \"whatsHappening\": \"Co się dzieje w tym tygodniu?\",\n    \"addContact\": \"Dodaj Kontakt\",\n    \"editContact\": \"Edytuj Kontakt\"\n  },\n  \"button\": {\n    \"ok\": \"OK\",\n    \"confirm\": \"Potwierdź\",\n    \"done\": \"Zrobione\",\n    \"cancel\": \"Anuluj\",\n    \"signIn\": \"Zaloguj\",\n    \"signOut\": \"Wyloguj\",\n    \"complete\": \"Zakończono\",\n    \"save\": \"Zapisz\",\n    \"generate\": \"Wygeneruj\",\n    \"esc\": \"WYJŚCIE\",\n    \"keep\": \"Trzymać\",\n    \"tryAgain\": \"Spróbuj ponownie\",\n    \"discard\": \"Odrzuć\",\n    \"replace\": \"Zastąp\",\n    \"insertBelow\": \"Wstaw poniżej\",\n    \"insertAbove\": \"Wstaw powyżej\",\n    \"upload\": \"Prześlij\",\n    \"edit\": \"Edytuj\",\n    \"delete\": \"Usuń\",\n    \"duplicate\": \"Duplikuj\",\n    \"putback\": \"Odłóż z powrotem\",\n    \"update\": \"Zaktualizuj\",\n    \"share\": \"Udostępnij\",\n    \"removeFromFavorites\": \"Usuń z ulubionych\",\n    \"addToFavorites\": \"Dodaj do ulubionych\",\n    \"rename\": \"Zmień nazwę\",\n    \"helpCenter\": \"Centrum Pomocy\",\n    \"add\": \"Dodaj\",\n    \"yes\": \"Tak\",\n    \"clear\": \"Wyczyść\",\n    \"remove\": \"Usuń\",\n    \"login\": \"Zaloguj się\",\n    \"logout\": \"Wyloguj\",\n    \"deleteAccount\": \"Usuń konto\",\n    \"back\": \"Wstecz\",\n    \"signInGoogle\": \"Zaloguj się za pomocą Google\",\n    \"signInGithub\": \"Zaloguj się za pomocą Githuba\",\n    \"signInDiscord\": \"Zaloguj się za pomocą Discorda\",\n    \"more\": \"Więcej\",\n    \"create\": \"Utwórz\",\n    \"close\": \"Zamknij\"\n  },\n  \"label\": {\n    \"welcome\": \"Witaj!\",\n    \"firstName\": \"Imię\",\n    \"middleName\": \"Drugie Imię\",\n    \"lastName\": \"Nazwisko\",\n    \"stepX\": \"Krok {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Nie udało się połączyć z Twoim kontem.\",\n      \"failedMsg\": \"Upewnij się, że zakończyłeś proces logowania w przeglądarce.\"\n    },\n    \"google\": {\n      \"title\": \"LOGOWANIE GOOGLE\",\n      \"instruction1\": \"Aby zaimportować Kontakty Google, musisz autoryzować tę aplikację za pomocą przeglądarki internetowej.\",\n      \"instruction2\": \"Skopiuj ten kod do schowka, klikając ikonę lub zaznaczając tekst:\",\n      \"instruction3\": \"Przejdź do następującego linku w przeglądarce internetowej i wprowadź powyższy kod:\",\n      \"instruction4\": \"Naciśnij poniższy przycisk po zakończeniu rejestracji:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Ustawienia\",\n    \"accountPage\": {\n      \"menuLabel\": \"Moje konto\",\n      \"title\": \"Moje konto\",\n      \"general\": {\n        \"title\": \"Nazwa konta i zdjęcie profilowe\",\n        \"changeProfilePicture\": \"Zmień zdjęcie profilowe\"\n      },\n      \"email\": {\n        \"title\": \"E-mail\",\n        \"actions\": {\n          \"change\": \"Zmień adres e-mail\"\n        }\n      },\n      \"login\": {\n        \"loginLabel\": \"Zaloguj się\",\n        \"logoutLabel\": \"Wyloguj\"\n      }\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"Obszar roboczy\",\n      \"title\": \"Obszar roboczy\"\n    },\n    \"menu\": {\n      \"appearance\": \"Wygląd\",\n      \"language\": \"Język\",\n      \"user\": \"Użytkownik\",\n      \"files\": \"Pliki\",\n      \"notifications\": \"Powiadomienia\",\n      \"open\": \"Otwórz Ustawienia\",\n      \"logout\": \"Wyloguj\",\n      \"logoutPrompt\": \"Czy na pewno chcesz się wylogować?\",\n      \"selfEncryptionLogoutPrompt\": \"Czy na pewno chcesz się wylogować? Upewnij się, że skopiowałeś sekret szyfrowania\",\n      \"syncSetting\": \"Ustawienie synchronizacji\",\n      \"enableSync\": \"Włącz synchronizację\",\n      \"enableEncrypt\": \"Szyfruj dane\",\n      \"cloudURL\": \"URL Serwera\",\n      \"enableEncryptPrompt\": \"Aktywuj szyfrowanie, aby zabezpieczyć swoje dane tym sekretem. Przechowuj go bezpiecznie; po włączeniu nie można go wyłączyć. Jeśli zostanie utracony, nie będziesz w stanie odzyskać danych. Kliknij, aby skopiować\",\n      \"inputEncryptPrompt\": \"Wprowadź swój sekret szyfrowania dla\",\n      \"clickToCopySecret\": \"Kliknij, aby skopiować sekret\",\n      \"inputTextFieldHint\": \"Twój sekret\",\n      \"historicalUserList\": \"Historia logowania użytkownika\",\n      \"historicalUserListTooltip\": \"Na tej liście wyświetlane są Twoje anonimowe konta. Możesz kliknąć konto, aby wyświetlić jego szczegóły. Konta anonimowe tworzy się poprzez kliknięcie przycisku „Rozpocznij”.\",\n      \"openHistoricalUser\": \"Kliknij, aby otworzyć anonimowe konto\",\n      \"cloudSetting\": \"Ustawienia Chmury\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Włącz powiadomienia\",\n        \"hint\": \"Wyłącz jeśli nie chcesz otrzymywać powiadomień.\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"Zresetuj to ustawienie\",\n      \"fontFamily\": {\n        \"label\": \"Rodzina czcionek\",\n        \"search\": \"Szukaj\"\n      },\n      \"themeMode\": {\n        \"label\": \"Rodzaj motywu\",\n        \"light\": \"Tryb Jasny\",\n        \"dark\": \"Tryb Ciemny\",\n        \"system\": \"Dostosuj do Systemu\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Kierunek układu\",\n        \"hint\": \"Kontroluj przepływ treści na ekranie, od lewej do prawej lub od prawej do lewej.\",\n        \"ltr\": \"LDP\",\n        \"rtl\": \"PDL\"\n      },\n      \"textDirection\": {\n        \"label\": \"Domyślny kierunek tekstu\",\n        \"hint\": \"Określ, czy tekst ma domyślnie zaczynać się od lewej czy prawej strony.\",\n        \"ltr\": \"LDP\",\n        \"rtl\": \"PDL\",\n        \"auto\": \"AUTOMATYCZNY\",\n        \"fallback\": \"Taki sam jak kierunek układu\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Prześlij\",\n        \"description\": \"Prześlij własny motyw @:appName za pomocą przycisku poniżej.\",\n        \"loading\": \"Poczekaj, aż zweryfikujemy i prześlemy Twój motyw...\",\n        \"uploadSuccess\": \"Twój motyw został przesłany pomyślnie\",\n        \"deletionFailure\": \"Nie udało się usunąć motywu. Spróbuj usunąć go ręcznie.\",\n        \"filePickerDialogTitle\": \"Wybierz plik .flowy_plugin\",\n        \"urlUploadFailure\": \"Nie udało się otworzyć adresu URL: {}\",\n        \"failure\": \"Przesłany motyw miał nieprawidłowy format.\"\n      },\n      \"theme\": \"Motyw\",\n      \"builtInsLabel\": \"Wbudowane motywy\",\n      \"pluginsLabel\": \"Wtyczki\",\n      \"dateFormat\": {\n        \"label\": \"Format daty\",\n        \"local\": \"Lokalny\",\n        \"us\": \"US\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"Przyjazny\",\n        \"dmy\": \"D/M/R\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Format godziny\",\n        \"twelveHour\": \"12 godzinny\",\n        \"twentyFourHour\": \"24 godzinny\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"Pokaż okno dialogowe nazewnictwa podczas tworzenia strony\"\n    },\n    \"files\": {\n      \"copy\": \"Kopiuj\",\n      \"defaultLocation\": \"Ścieżka katalogu z plikami\",\n      \"exportData\": \"Eksportuj swoje dane\",\n      \"doubleTapToCopy\": \"Kliknij dwukrotnie, aby skopiować ścieżkę\",\n      \"restoreLocation\": \"Przywróć domyślną ścieżkę @:appName\",\n      \"customizeLocation\": \"Otwórz inny folder\",\n      \"restartApp\": \"Uruchom ponownie aplikację, aby zmiany zaczęły obowiązywać.\",\n      \"exportDatabase\": \"Eksportuj bazę danych\",\n      \"selectFiles\": \"Wybierz pliki, które mają zostać wyeksportowane\",\n      \"selectAll\": \"Zaznacz wszystko\",\n      \"deselectAll\": \"Odznacz wszystkie\",\n      \"createNewFolder\": \"Stwórz nowy folder\",\n      \"createNewFolderDesc\": \"Powiedz nam, gdzie chcesz przechowywać swoje dane\",\n      \"defineWhereYourDataIsStored\": \"Zdefiniuj miejsce przechowywania Twoich danych\",\n      \"open\": \"Otwórz\",\n      \"openFolder\": \"Otwórz istniejący folder\",\n      \"openFolderDesc\": \"Przeczytaj i zapisz go w istniejącym folderze @:appName\",\n      \"folderHintText\": \"Nazwa folderu\",\n      \"location\": \"Tworzenie nowego folderu\",\n      \"locationDesc\": \"Wybierz nazwę folderu danych @:appName\",\n      \"browser\": \"Przeglądaj\",\n      \"create\": \"Stwórz\",\n      \"set\": \"Ustaw\",\n      \"folderPath\": \"Ścieżka do przechowywania folderu\",\n      \"locationCannotBeEmpty\": \"Ścieżka nie może być pusta\",\n      \"pathCopiedSnackbar\": \"Ścieżka przechowywania plików została skopiowana do schowka!\",\n      \"changeLocationTooltips\": \"Zmień katalog danych\",\n      \"change\": \"Zmień\",\n      \"openLocationTooltips\": \"Otwórz inny katalog danych\",\n      \"openCurrentDataFolder\": \"Otwórz bieżący katalog danych\",\n      \"recoverLocationTooltips\": \"Zresetuj do domyślnego katalogu danych @:appName\",\n      \"exportFileSuccess\": \"Eksportowanie pliku zakończono pomyślnie!\",\n      \"exportFileFail\": \"Eksport pliku nie powiódł się!\",\n      \"export\": \"Eksport\"\n    },\n    \"user\": {\n      \"name\": \"Nazwa\",\n      \"email\": \"E-mail\",\n      \"tooltipSelectIcon\": \"Wybierz ikonę\",\n      \"selectAnIcon\": \"Wybierz ikonę\",\n      \"pleaseInputYourOpenAIKey\": \"wprowadź swój klucz AI\",\n      \"clickToLogout\": \"Kliknij, aby wylogować bieżącego użytkownika\",\n      \"pleaseInputYourStabilityAIKey\": \"wprowadź swój klucz Stability AI\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"Informacje Osobiste\",\n      \"username\": \"Nazwa użytkownika\",\n      \"usernameEmptyError\": \"Nazwa użytkownika nie może być pusta\",\n      \"about\": \"About\",\n      \"pushNotifications\": \"Powiadomienia Push\",\n      \"support\": \"Wsparcie\",\n      \"joinDiscord\": \"Dołącz do nas na Discord\",\n      \"privacyPolicy\": \"Polityka prywatności\",\n      \"userAgreement\": \"Regulamin Użytkowania\",\n      \"userprofileError\": \"Nie udało się wczytać profilu użytkownika\",\n      \"userprofileErrorDescription\": \"Spróbuj wylogować się i zalogować ponownie, aby zobaczyć czy problem nadal występuje.\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"Skróty\",\n      \"command\": \"Polecenie\",\n      \"keyBinding\": \"Skróty klawiszowe\",\n      \"addNewCommand\": \"Dodaj nowe polecenie\",\n      \"updateShortcutStep\": \"Naciśnij żądaną kombinację klawiszy i naciśnij ENTER\",\n      \"shortcutIsAlreadyUsed\": \"Ten skrót jest już używany w przypadku: {conflict}\",\n      \"resetToDefault\": \"Przywróć domyślne skróty klawiszowe\",\n      \"couldNotLoadErrorMsg\": \"Nie udało się wczytać skrótów. Spróbuj ponownie\",\n      \"couldNotSaveErrorMsg\": \"Nie udało się zapisać skrótów. Spróbuj ponownie\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Czy na pewno chcesz usunąć ten widok?\",\n    \"createView\": \"Nowy\",\n    \"title\": {\n      \"placeholder\": \"Bez tytułu\"\n    },\n    \"settings\": {\n      \"filter\": \"Filtr\",\n      \"sort\": \"Sortuj\",\n      \"sortBy\": \"Sortuj według\",\n      \"properties\": \"Właściwości\",\n      \"reorderPropertiesTooltip\": \"Przeciągnij, aby zmienić kolejność właściwości\",\n      \"group\": \"Grupa\",\n      \"addFilter\": \"Dodaj filtr\",\n      \"deleteFilter\": \"Usuń filtr\",\n      \"filterBy\": \"Filtruj według...\",\n      \"typeAValue\": \"Wpisz wartość...\",\n      \"layout\": \"Układ\",\n      \"databaseLayout\": \"Układ\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Zawiera\",\n      \"doesNotContain\": \"Nie zawiera\",\n      \"endsWith\": \"Kończy się na\",\n      \"startWith\": \"Zaczyna się od\",\n      \"is\": \"Jest\",\n      \"isNot\": \"Nie jest\",\n      \"isEmpty\": \"Jest pusty\",\n      \"isNotEmpty\": \"Nie jest pusty\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Nie\",\n        \"startWith\": \"Zaczyna się na\",\n        \"endWith\": \"Kończy się na\",\n        \"isEmpty\": \"jest pusty\",\n        \"isNotEmpty\": \"nie jest pusty\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Zaznaczony\",\n      \"isUnchecked\": \"Odznaczony\",\n      \"choicechipPrefix\": {\n        \"is\": \"Jest\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"jest kompletna\",\n      \"isIncomplted\": \"jest niekompletna\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"Jest\",\n      \"isNot\": \"Nie jest\",\n      \"contains\": \"Zawiera\",\n      \"doesNotContain\": \"Nie zawiera\",\n      \"isEmpty\": \"Jest pusty\",\n      \"isNotEmpty\": \"Nie jest pusty\"\n    },\n    \"field\": {\n      \"hide\": \"Ukryj\",\n      \"show\": \"Pokaż\",\n      \"insertLeft\": \"Wstaw w lewo\",\n      \"insertRight\": \"Wstaw w prawo\",\n      \"duplicate\": \"Duplikuj\",\n      \"delete\": \"Usuń\",\n      \"textFieldName\": \"Tekst\",\n      \"checkboxFieldName\": \"Pole wyboru\",\n      \"dateFieldName\": \"Data\",\n      \"updatedAtFieldName\": \"Czas ostatniej modyfikacji\",\n      \"createdAtFieldName\": \"Czas utworzenia\",\n      \"numberFieldName\": \"Liczby\",\n      \"singleSelectFieldName\": \"Jednokrotny wybór\",\n      \"multiSelectFieldName\": \"Wielokrotny wybór\",\n      \"urlFieldName\": \"Adres URL\",\n      \"checklistFieldName\": \"Lista kontrolna\",\n      \"numberFormat\": \"Format liczbowy\",\n      \"dateFormat\": \"Format daty\",\n      \"includeTime\": \"Uwzględnij czas\",\n      \"isRange\": \"Końcowa data\",\n      \"dateFormatFriendly\": \"Miesiąc Dzień, Rok\",\n      \"dateFormatISO\": \"Rok-Miesiąc-Dzień\",\n      \"dateFormatLocal\": \"Miesiąc/Dzień/Rok\",\n      \"dateFormatUS\": \"Rok miesiąc dzień\",\n      \"dateFormatDayMonthYear\": \"Dzień/Miesiąc/Rok\",\n      \"timeFormat\": \"Format czasu\",\n      \"invalidTimeFormat\": \"Niepoprawny format\",\n      \"timeFormatTwelveHour\": \"12 godzinny\",\n      \"timeFormatTwentyFourHour\": \"24 godzinny\",\n      \"clearDate\": \"Wyczyść datę\",\n      \"addSelectOption\": \"Dodaj opcję\",\n      \"optionTitle\": \"Opcje\",\n      \"addOption\": \"Dodaj opcję\",\n      \"editProperty\": \"Edytuj właściwość\",\n      \"newProperty\": \"Nowa właściwość\",\n      \"deleteFieldPromptMessage\": \"Jesteś pewny? Ta właściwość zostanie usunięta\",\n      \"newColumn\": \"Nowa kolumna\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Dodaj nowe pole\",\n      \"fieldDragElementTooltip\": \"Kliknij by otworzyć menu\",\n      \"showHiddenFields\": {\n        \"one\": \"Pokaż {} ukryte pole\",\n        \"many\": \"Pokaż {} ukryte pola\",\n        \"other\": \"Pokaż {} ukryte pola\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"Ukryj {} ukryte pole\",\n        \"many\": \"Ukryj {} ukryte pola\",\n        \"other\": \"Ukryj {} ukryte pola\"\n      }\n    },\n    \"sort\": {\n      \"ascending\": \"Rosnąco\",\n      \"descending\": \"Malejąco\",\n      \"deleteAllSorts\": \"Usuń wszystkie sortowania\",\n      \"addSort\": \"Dodaj sortowanie\",\n      \"deleteSort\": \"Usuń sortowanie\"\n    },\n    \"row\": {\n      \"duplicate\": \"Duplikuj\",\n      \"delete\": \"Usuń\",\n      \"titlePlaceholder\": \"Brak tytułu\",\n      \"textPlaceholder\": \"Pusty\",\n      \"copyProperty\": \"Skopiowano właściwość do schowka\",\n      \"count\": \"Liczba\",\n      \"newRow\": \"Nowy rząd\",\n      \"action\": \"Akcja\",\n      \"add\": \"Kliknij by dodać poniżej\",\n      \"drag\": \"Przeciągnij aby przenieść\",\n      \"dragAndClick\": \"Przeciągnij aby przenieść, kliknij by otworzyć menu\",\n      \"insertRecordAbove\": \"Dodaj wpis powyżej\",\n      \"insertRecordBelow\": \"Dodaj wpis poniżej\"\n    },\n    \"selectOption\": {\n      \"create\": \"Stwórz\",\n      \"purpleColor\": \"Fioletowy\",\n      \"pinkColor\": \"Różowy\",\n      \"lightPinkColor\": \"Jasnoróżowy\",\n      \"orangeColor\": \"Pomarańczowy\",\n      \"yellowColor\": \"Żółty\",\n      \"limeColor\": \"Limonka\",\n      \"greenColor\": \"Zielony\",\n      \"aquaColor\": \"Wodny\",\n      \"blueColor\": \"Niebieski\",\n      \"deleteTag\": \"Usuń tag\",\n      \"colorPanelTitle\": \"Kolory\",\n      \"panelTitle\": \"Wybierz opcję lub utwórz ją\",\n      \"searchOption\": \"Wyszukaj opcję\",\n      \"searchOrCreateOption\": \"Wyszukaj lub utwórz opcję...\",\n      \"createNew\": \"Utwórz nowy\",\n      \"orSelectOne\": \"Lub wybierz opcję\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"Opis zadania\",\n      \"addNew\": \"Dodaj element\",\n      \"submitNewTask\": \"Stwórz\",\n      \"hideComplete\": \"Ukryj zakończone zadania\",\n      \"showComplete\": \"Pokaż wszystkie zadania\"\n    },\n    \"menuName\": \"Siatka\",\n    \"referencedGridPrefix\": \"Widok\"\n  },\n  \"document\": {\n    \"menuName\": \"Dokument\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"13:00\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Wybierz tablicę, do której chcesz utworzyć łącze\",\n        \"createANewBoard\": \"Utwórz nową tablicę\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Wybierz siatkę do połączenia\",\n        \"createANewGrid\": \"Utwórz nową siatkę\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Wybierz kalendarz do połączenia\",\n        \"createANewCalendar\": \"Utwórz nowy kalendarz\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"Wybierz dokument do połączenia\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Zarys\",\n      \"codeBlock\": \"Blok kodu\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Tablica referencyjna\",\n      \"referencedGrid\": \"Siatka referencyjna\",\n      \"referencedCalendar\": \"Kalendarz referencyjny\",\n      \"referencedDocument\": \"Dokument referencyjny\",\n      \"autoGeneratorMenuItemName\": \"Pisarz AI\",\n      \"autoGeneratorTitleName\": \"AI: Poproś AI o napisanie czegokolwiek...\",\n      \"autoGeneratorLearnMore\": \"Dowiedz się więcej\",\n      \"autoGeneratorGenerate\": \"Generuj\",\n      \"autoGeneratorHintText\": \"Zapytaj AI...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Nie można uzyskać klucza AI\",\n      \"autoGeneratorRewrite\": \"Przepisz\",\n      \"smartEdit\": \"Asystenci AI\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"Popraw pisownię\",\n      \"warning\": \"⚠️ Odpowiedzi AI mogą być niedokładne lub mylące.\",\n      \"smartEditSummarize\": \"Podsumuj\",\n      \"smartEditImproveWriting\": \"Popraw pisanie\",\n      \"smartEditMakeLonger\": \"Dłużej\",\n      \"smartEditCouldNotFetchResult\": \"Nie można pobrać wyniku z AI\",\n      \"smartEditCouldNotFetchKey\": \"Nie można pobrać klucza AI\",\n      \"smartEditDisabled\": \"Połącz AI w Ustawieniach\",\n      \"discardResponse\": \"Czy chcesz odrzucić odpowiedzi AI?\",\n      \"createInlineMathEquation\": \"Utwórz równanie\",\n      \"toggleList\": \"Przełącz listę\",\n      \"quoteList\": \"Lista cytatów\",\n      \"numberedList\": \"Numerowana lista\",\n      \"bulletedList\": \"Wypunktowana lista\",\n      \"todoList\": \"Lista rzeczy do zrobienia\",\n      \"callout\": \"Wywołanie\",\n      \"cover\": {\n        \"changeCover\": \"Zmień okładkę\",\n        \"colors\": \"Kolory\",\n        \"images\": \"Obrazy\",\n        \"clearAll\": \"Wyczyść wszystko\",\n        \"abstract\": \"Abstrakcyjny\",\n        \"addCover\": \"Dodaj okładkę\",\n        \"addLocalImage\": \"Dodaj obraz lokalny\",\n        \"invalidImageUrl\": \"Nieprawidłowy adres URL obrazu\",\n        \"failedToAddImageToGallery\": \"Nie udało się dodać obrazu do galerii\",\n        \"enterImageUrl\": \"Wprowadź adres URL obrazu\",\n        \"add\": \"Dodać\",\n        \"back\": \"Z powrotem\",\n        \"saveToGallery\": \"Zapisz w galerii\",\n        \"removeIcon\": \"Usuń ikonę\",\n        \"pasteImageUrl\": \"Wklej adres URL obrazu\",\n        \"or\": \"LUB\",\n        \"pickFromFiles\": \"Wybierz z plików\",\n        \"couldNotFetchImage\": \"Nie można pobrać obrazu\",\n        \"imageSavingFailed\": \"Zapisywanie obrazu nie powiodło się\",\n        \"addIcon\": \"Dodaj ikonę\",\n        \"changeIcon\": \"Zmień ikonę\",\n        \"coverRemoveAlert\": \"Zostanie usunięty z okładki.\",\n        \"alertDialogConfirmation\": \"Jesteś pewien, że chcesz kontynuować?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"Równanie matematyczne\",\n        \"addMathEquation\": \"Dodaj równanie matematyczne\",\n        \"editMathEquation\": \"Edytuj równanie matematyczne\"\n      },\n      \"optionAction\": {\n        \"click\": \"Kliknij\",\n        \"toOpenMenu\": \" aby otworzyć menu\",\n        \"delete\": \"Usuń\",\n        \"duplicate\": \"Duplikuj\",\n        \"turnInto\": \"Zmień w\",\n        \"moveUp\": \"Podnieś\",\n        \"moveDown\": \"Upuść\",\n        \"color\": \"Kolor\",\n        \"align\": \"Wyrównaj\",\n        \"left\": \"Lewo\",\n        \"center\": \"Centrum\",\n        \"right\": \"Prawo\",\n        \"defaultColor\": \"Domyślny\"\n      },\n      \"image\": {\n        \"addAnImage\": \"Dodaj obraz\",\n        \"copiedToPasteBoard\": \"Link do obrazu został skopiowany do schowka\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Dodaj nagłówki, aby utworzyć spis treści.\"\n      },\n      \"table\": {\n        \"addAfter\": \"Dodaj po\",\n        \"addBefore\": \"Dodaj przed\",\n        \"delete\": \"Usuń\",\n        \"clear\": \"Wyczyść treść\",\n        \"duplicate\": \"Duplikuj\",\n        \"bgColor\": \"Kolor tła\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Kopiuj\",\n        \"cut\": \"Wytnij\",\n        \"paste\": \"Wklej\"\n      },\n      \"action\": \"Akcje\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Wpisz „/” dla poleceń\"\n    },\n    \"title\": {\n      \"placeholder\": \"Brak nazwy\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Kliknij, aby dodać obraz\",\n      \"upload\": {\n        \"label\": \"Prześlij\",\n        \"placeholder\": \"Kliknij, aby przesłać obraz\"\n      },\n      \"url\": {\n        \"label\": \"URL obrazu\",\n        \"placeholder\": \"Wprowadź adres URL obrazu\"\n      },\n      \"ai\": {\n        \"label\": \"Wygeneruj obraz z AI\",\n        \"placeholder\": \"Wpisz treść podpowiedzi dla AI, aby wygenerować obraz\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Wygeneruj obraz z Stability AI\",\n        \"placeholder\": \"Wpisz treść podpowiedzi dla Stability AI, aby wygenerować obraz\"\n      },\n      \"support\": \"Limit rozmiaru obrazu wynosi 5 MB. Obsługiwane formaty: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Nieprawidłowy obraz\",\n        \"invalidImageSize\": \"Rozmiar obrazu musi być mniejszy niż 5 MB\",\n        \"invalidImageFormat\": \"Format obrazu nie jest obsługiwany. Obsługiwane formaty: JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"Nieprawidłowy adres URL obrazu\"\n      },\n      \"embedLink\": {\n        \"label\": \"Osadź link\",\n        \"placeholder\": \"Wklej lub wpisz link obrazu\"\n      },\n      \"searchForAnImage\": \"Szukaj obrazu\",\n      \"pleaseInputYourOpenAIKey\": \"wpisz swój klucz AI w ustawieniach\",\n      \"saveImageToGallery\": \"Zapisz obraz\",\n      \"failedToAddImageToGallery\": \"Nie udało się dodać obrazu do galerii\",\n      \"successToAddImageToGallery\": \"Pomyślnie dodano obraz do galerii\",\n      \"unableToLoadImage\": \"Nie udało się wczytać obrazu\",\n      \"pleaseInputYourStabilityAIKey\": \"wpisz swój klucz Stability AI w ustawieniach\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Język\",\n        \"placeholder\": \"Wybierz język\"\n      }\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Wklej lub wpisz link\",\n      \"openInNewTab\": \"Otwórz w nowej karcie\",\n      \"copyLink\": \"Skopiuj link\",\n      \"removeLink\": \"Usuń link\",\n      \"url\": {\n        \"label\": \"Adres URL łącza\",\n        \"placeholder\": \"Wprowadź adres URL łącza\"\n      },\n      \"title\": {\n        \"label\": \"Tytuł linku\",\n        \"placeholder\": \"Wpisz tytuł linku\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"Wspomnij o osobie, stronie lub dacie...\",\n      \"page\": {\n        \"label\": \"Link do strony\",\n        \"tooltip\": \"Kliknij, aby otworzyć stronę\"\n      }\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"Przywróć ustawienia domyślne\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"Bieżąca wersja nie wspiera tego bloku.\",\n      \"blockContentHasBeenCopied\": \"Treść bloku została skopiowana.\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"Nowy\",\n      \"renameGroupTooltip\": \"Wciśnij by zmienić nazwę grupy\",\n      \"createNewColumn\": \"Dodaj nową grupę\",\n      \"addToColumnTopTooltip\": \"Dodaj nową kartę na górze\",\n      \"renameColumn\": \"Zmień nazwę\",\n      \"hideColumn\": \"Ukryj\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"Ukryte Grupy\",\n      \"collapseTooltip\": \"Ukryj ukryte grupy\",\n      \"expandTooltip\": \"Pokaż ukryte grupy\"\n    },\n    \"menuName\": \"Tablica\",\n    \"ungroupedButtonText\": \"Niepogrupowane\",\n    \"ungroupedButtonTooltip\": \"Zawiera karty, które nie należą do żadnej grupy\",\n    \"ungroupedItemsTitle\": \"Kliknij by dodać tablicę\",\n    \"groupBy\": \"Grupuj zgodnie z\",\n    \"referencedBoardPrefix\": \"Widok\",\n    \"mobile\": {\n      \"showGroup\": \"Pokaż grupę\",\n      \"showGroupContent\": \"Czy na pewno chcesz pokazać tę grupę na tablicy?\",\n      \"failedToLoad\": \"Nie udało się załadować widoku tablicy\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Kalendarz\",\n    \"defaultNewCalendarTitle\": \"Brak nazwy\",\n    \"newEventButtonTooltip\": \"Dodaj nowe wydarzenie\",\n    \"navigation\": {\n      \"today\": \"Dzisiaj\",\n      \"jumpToday\": \"Przejdź do dzisiaj\",\n      \"previousMonth\": \"Poprzedni miesiac\",\n      \"nextMonth\": \"W następnym miesiącu\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Pokaż numery tygodni\",\n      \"showWeekends\": \"Pokaż weekendy\",\n      \"firstDayOfWeek\": \"Rozpocznij tydzień\",\n      \"layoutDateField\": \"Układ kalendarza wg\",\n      \"noDateTitle\": \"Brak daty\",\n      \"noDateHint\": {\n        \"zero\": \"Tutaj pojawią się nieplanowane wydarzenia\",\n        \"one\": \"{} nieplanowane wydarzenie\",\n        \"other\": \"{} nieplanowane wydarzenia\"\n      },\n      \"clickToAdd\": \"Kliknij, aby dodać do kalendarza\",\n      \"name\": \"Układ kalendarza\"\n    },\n    \"referencedCalendarPrefix\": \"Widok\"\n  },\n  \"errorDialog\": {\n    \"title\": \"Błąd @:appName\",\n    \"howToFixFallback\": \"Przepraszamy za niedogodności! Zgłoś problem na naszej stronie GitHub, który opisuje Twój błąd.\",\n    \"github\": \"Zobacz na GitHubie\"\n  },\n  \"search\": {\n    \"label\": \"Szukaj\",\n    \"placeholder\": {\n      \"actions\": \"Wyszukiwanie działań...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Skopiowane!\",\n      \"fail\": \"Nie można skopiować\"\n    }\n  },\n  \"unSupportBlock\": \"Bieżąca wersja nie obsługuje tego bloku.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Czy na pewno chcesz usunąć {pageType}?\",\n    \"deleteContentCaption\": \"jeśli usuniesz ten {pageType}, możesz go przywrócić z kosza.\"\n  },\n  \"colors\": {\n    \"custom\": \"Niestandardowy\",\n    \"default\": \"Domyślny\",\n    \"red\": \"Czerwony\",\n    \"orange\": \"Pomarańczowy\",\n    \"green\": \"Zielony\",\n    \"blue\": \"Niebieski\",\n    \"purple\": \"Fioletowy\",\n    \"pink\": \"Różowy\",\n    \"brown\": \"brązowy\",\n    \"gray\": \"Szary\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"Emoji\",\n    \"search\": \"Szukaj emoji\",\n    \"noRecent\": \"Brak aktualnych emoji\",\n    \"noEmojiFound\": \"Nie odnaleziono emoji\",\n    \"filter\": \"Filtr\",\n    \"random\": \"Losowy\",\n    \"selectSkinTone\": \"Wybierz kolor skóry\",\n    \"remove\": \"Usuń emoji\",\n    \"categories\": {\n      \"smileys\": \"Buźki i Emocje\",\n      \"people\": \"Ludzie i Ciało\",\n      \"animals\": \"Zwierzęta i Przyroda\",\n      \"food\": \"Jedzenie i Picie\",\n      \"activities\": \"Aktywności\",\n      \"places\": \"Podróże i Miejsca\",\n      \"objects\": \"Obiekty\",\n      \"symbols\": \"Symbole\",\n      \"flags\": \"Flagi\",\n      \"nature\": \"Natura\",\n      \"frequentlyUsed\": \"Często używany\"\n    },\n    \"skinTone\": {\n      \"default\": \"Domyślny\",\n      \"light\": \"Jasny\",\n      \"mediumLight\": \"Średnio-Jasny\",\n      \"medium\": \"Średni\",\n      \"mediumDark\": \"Średnio-Ciemny\",\n      \"dark\": \"Ciemny\"\n    }\n  },\n  \"inlineActions\": {\n    \"noResults\": \"Brak wyników\",\n    \"pageReference\": \"Strona referencyjna\",\n    \"date\": \"Data\",\n    \"reminder\": {\n      \"groupTitle\": \"Przypomnienie\",\n      \"shortKeyword\": \"przypomnij\"\n    }\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"Zmień format daty i godziny w ustawieniach\"\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"Wczoraj\",\n    \"today\": \"Dziś\",\n    \"tomorrow\": \"Jutro\",\n    \"oneWeek\": \"1 tydzień\"\n  },\n  \"notificationHub\": {\n    \"title\": \"Powiadomienia\",\n    \"emptyTitle\": \"Wszystko nadrobione!\",\n    \"emptyBody\": \"Brak oczekujących powiadomień lub działań. Ciesz się ciszą.\",\n    \"tabs\": {\n      \"inbox\": \"Skrzynka odbiorcza\",\n      \"upcoming\": \"Nadchodzące\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"Oznacz wszystkie jako przeczytane\",\n      \"showAll\": \"Wszystkie\",\n      \"showUnreads\": \"Nieprzeczytane\"\n    },\n    \"filters\": {\n      \"ascending\": \"Rosnąco\",\n      \"descending\": \"Malejąco\",\n      \"groupByDate\": \"Grupuj według daty\",\n      \"showUnreadsOnly\": \"Pokaż tylko nieprzeczytane\",\n      \"resetToDefault\": \"Zresetuj do domyślnych\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"Przypomnienie\",\n    \"message\": \"Pamiętaj aby to sprawdzić zanim zapomnisz!\",\n    \"tooltipDelete\": \"Usuń\",\n    \"tooltipMarkRead\": \"Oznacz jako przeczytane\",\n    \"tooltipMarkUnread\": \"Oznacz jako nieprzeczytane\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"Znajdź\",\n    \"previousMatch\": \"Poprzednie dopasowanie\",\n    \"nextMatch\": \"Kolejne dopasowanie\",\n    \"close\": \"Zamknij\",\n    \"replace\": \"Zastąp\",\n    \"replaceAll\": \"Zastąp wszystkie\",\n    \"noResult\": \"Brak wyników\",\n    \"caseSensitive\": \"Uwzględnij wielkość znaków\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"Przykro nam\",\n    \"loadingViewError\": \"Mamy problem z wyświetleniem tego widoku. Sprawdź połączenie z internetem, odśwież stronę i nie wahaj się zgłosić problem naszemu zespołowi jesli nadal będzie występował.\"\n  },\n  \"editor\": {\n    \"bold\": \"Pogrubienie\",\n    \"bulletedList\": \"Wypunktowana Lista\",\n    \"checkbox\": \"Pole wyboru\",\n    \"embedCode\": \"Osadź Kod\",\n    \"heading1\": \"H1\",\n    \"heading2\": \"H2\",\n    \"heading3\": \"H3\",\n    \"highlight\": \"Podświetlenie\",\n    \"color\": \"Kolor\",\n    \"image\": \"Obraz\",\n    \"italic\": \"Kursywa\",\n    \"link\": \"Link\",\n    \"numberedList\": \"Numerowana Lista\",\n    \"quote\": \"Cytat\",\n    \"strikethrough\": \"Przekreślenie\",\n    \"text\": \"Tekst\",\n    \"underline\": \"Podkreślenie\",\n    \"fontColorDefault\": \"Domyślny\",\n    \"fontColorGray\": \"Szary\",\n    \"fontColorBrown\": \"Brązowy\",\n    \"fontColorOrange\": \"Pomarańczony\",\n    \"fontColorYellow\": \"Żółty\",\n    \"fontColorGreen\": \"Zielony\",\n    \"fontColorBlue\": \"Niebieski\",\n    \"fontColorPurple\": \"Fioletowy\",\n    \"fontColorPink\": \"Różowy\",\n    \"fontColorRed\": \"Czerwony\",\n    \"backgroundColorDefault\": \"Domyślne tło\",\n    \"backgroundColorGray\": \"Szare tło\",\n    \"backgroundColorBrown\": \"Brązowe tło\",\n    \"backgroundColorOrange\": \"Pomarańczowe tło\",\n    \"backgroundColorYellow\": \"Żółte tło\",\n    \"backgroundColorGreen\": \"Zielone tło\",\n    \"backgroundColorBlue\": \"Niebieskie tło\",\n    \"backgroundColorPurple\": \"Fioletowe tło\",\n    \"backgroundColorPink\": \"Różowe tło\",\n    \"backgroundColorRed\": \"Czerwone tło\",\n    \"done\": \"Zrobione\",\n    \"cancel\": \"Anuluj\",\n    \"tint1\": \"Odcień 1\",\n    \"tint2\": \"Odcień 2\",\n    \"tint3\": \"Odcień 3\",\n    \"tint4\": \"Odcień 4\",\n    \"tint5\": \"Odcień 5\",\n    \"tint6\": \"Odcień 6\",\n    \"tint7\": \"Odcień 7\",\n    \"tint8\": \"Odcień 8\",\n    \"tint9\": \"Odcień 9\",\n    \"lightLightTint1\": \"Fioletowy\",\n    \"lightLightTint2\": \"Różowy\",\n    \"lightLightTint3\": \"Jasnoróżowy\",\n    \"lightLightTint4\": \"Pomarańczony\",\n    \"lightLightTint5\": \"Żółty\",\n    \"lightLightTint6\": \"Limonkowy\",\n    \"lightLightTint7\": \"Zielony\",\n    \"lightLightTint8\": \"Morski\",\n    \"lightLightTint9\": \"Niebieski\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"Nagłówek 1\",\n    \"mobileHeading2\": \"Nagłówek 2\",\n    \"mobileHeading3\": \"Nagłówek 3\",\n    \"textColor\": \"Kolor tekstu\",\n    \"backgroundColor\": \"Kolor tła\",\n    \"addYourLink\": \"Dodaj swój link\",\n    \"openLink\": \"Otwórz link\",\n    \"copyLink\": \"Skopiuj link\",\n    \"removeLink\": \"Usuń link\",\n    \"editLink\": \"Edytuj link\",\n    \"linkText\": \"Tekst\",\n    \"linkTextHint\": \"Wpisz tekst\",\n    \"linkAddressHint\": \"Podaj URL\",\n    \"highlightColor\": \"Kolor podświetlenia\",\n    \"clearHighlightColor\": \"Usuń kolor podświetlenia\",\n    \"customColor\": \"Niestandardowy kolor\",\n    \"hexValue\": \"Wartość hex\",\n    \"opacity\": \"Przezroczystość\",\n    \"resetToDefaultColor\": \"Przywróc domyślny kolor\",\n    \"ltr\": \"LDP\",\n    \"rtl\": \"PDL\",\n    \"auto\": \"Auto\",\n    \"cut\": \"Wytnij\",\n    \"copy\": \"Skopiuj\",\n    \"paste\": \"Wklej\",\n    \"find\": \"Znajdź\",\n    \"previousMatch\": \"Poprzednie dopasowanie\",\n    \"nextMatch\": \"Kolejne dopasowanie\",\n    \"closeFind\": \"Zamknij\",\n    \"replace\": \"Zastąp\",\n    \"replaceAll\": \"Zastąp wszystko\",\n    \"regex\": \"Regex\",\n    \"caseSensitive\": \"Uwzględnij wielkość znaków\",\n    \"uploadImage\": \"Prześlij Obraz\",\n    \"urlImage\": \"URL Obrazu\",\n    \"incorrectLink\": \"Nieprawidłowy Link\",\n    \"upload\": \"Prześlij\",\n    \"chooseImage\": \"Wybierz obraz\",\n    \"loading\": \"Wczytywanie\",\n    \"imageLoadFailed\": \"Nie udało się wczytać obrazu\",\n    \"divider\": \"Rozdzielacz\",\n    \"table\": \"Tabela\",\n    \"colAddBefore\": \"Dodaj przed\",\n    \"rowAddBefore\": \"Dodaj przed\",\n    \"colAddAfter\": \"Dodaj za\",\n    \"rowAddAfter\": \"Dodaj za\",\n    \"colRemove\": \"Usuń\",\n    \"rowRemove\": \"Usuń\",\n    \"colDuplicate\": \"Duplikuj\",\n    \"rowDuplicate\": \"Duplikuj\",\n    \"colClear\": \"Wyczyść Zawartość\",\n    \"rowClear\": \"Wyczyść Zawartość\",\n    \"slashPlaceHolder\": \"Wciśnij / by dodać blok, lub zacznij pisać\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"Brak ulubionej strony\",\n    \"noFavoriteHintText\": \"Przesuń stronę do lewej aby dodać ją do ulubionych\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"Wciśnij / by dodać blok, lub zacznij pisać\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"Do zrobienia\",\n    \"bulletList\": \"Lista\",\n    \"numberList\": \"Lista\",\n    \"quote\": \"Cytat\",\n    \"heading\": \"Nagłówek {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"Ikona strony\",\n    \"language\": \"Język\",\n    \"font\": \"Czcionka\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/pt-BR.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Eu\",\n  \"welcomeText\": \"Bem-vindo ao @:appName\",\n  \"welcomeTo\": \"Bem-vindo ao\",\n  \"githubStarText\": \"Dar uma estrela no Github\",\n  \"subscribeNewsletterText\": \"Inscreva-se para receber novidades\",\n  \"letsGoButtonText\": \"Vamos lá\",\n  \"title\": \"Título\",\n  \"youCanAlso\": \"Você também pode\",\n  \"and\": \"e\",\n  \"failedToOpenUrl\": \"Falha ao abrir url: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Clique para adicionar abaixo\",\n    \"addAboveCmd\": \"Alt+clique\",\n    \"addAboveMacCmd\": \"Opção+clique\",\n    \"addAboveTooltip\": \"para adicionar acima\",\n    \"dragTooltip\": \"Arraste para mover\",\n    \"openMenuTooltip\": \"Clique para abrir o menu\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Inscreva-se\",\n    \"title\": \"Inscreva-se no @:appName\",\n    \"getStartedText\": \"Começar\",\n    \"emptyPasswordError\": \"Senha não pode estar em branco.\",\n    \"repeatPasswordEmptyError\": \"Senha não estar em branco.\",\n    \"unmatchedPasswordError\": \"As senhas não conferem.\",\n    \"alreadyHaveAnAccount\": \"Já possui uma conta?\",\n    \"emailHint\": \"E-mail\",\n    \"passwordHint\": \"Dica de senha\",\n    \"repeatPasswordHint\": \"Confirme a senha\",\n    \"signUpWith\": \"Inscreva-se com:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Conectar-se ao @:appName\",\n    \"loginButtonText\": \"Conectar-se\",\n    \"loginStartWithAnonymous\": \"Iniciar com uma sessão anônima\",\n    \"continueAnonymousUser\": \"Continuar em uma sessão anônima\",\n    \"buttonText\": \"Entre\",\n    \"signingInText\": \"Entrando...\",\n    \"forgotPassword\": \"Esqueceu sua senha?\",\n    \"emailHint\": \"E-mail\",\n    \"passwordHint\": \"Senha\",\n    \"dontHaveAnAccount\": \"Não possui uma conta?\",\n    \"createAccount\": \"Criar uma conta\",\n    \"repeatPasswordEmptyError\": \"Senha não pode estar em branco.\",\n    \"unmatchedPasswordError\": \"As senhas não conferem.\",\n    \"syncPromptMessage\": \"A sincronização dos dados pode demorar um pouco. Por favor não feche esta página\",\n    \"or\": \"OU\",\n    \"signInWithGoogle\": \"Continuar com o Google\",\n    \"signInWithGithub\": \"Continuar com o Github\",\n    \"signInWithDiscord\": \"Continuar com o Discord\",\n    \"signInWithApple\": \"Continuar com a Apple\",\n    \"continueAnotherWay\": \"Continuar de outra forma\",\n    \"signUpWithGoogle\": \"Cadastro com o Google\",\n    \"signUpWithGithub\": \"Cadastro com o Github\",\n    \"signUpWithDiscord\": \"Cadastro com o Discord\",\n    \"signInWith\": \"Entrar com:\",\n    \"signInWithEmail\": \"Continuar com e-mail\",\n    \"signInWithMagicLink\": \"Continuar\",\n    \"signUpWithMagicLink\": \"Cadastro com um Link Mágico\",\n    \"pleaseInputYourEmail\": \"Por favor, insira seu endereço de e-mail\",\n    \"settings\": \"Configurações\",\n    \"magicLinkSent\": \"Link Mágico enviado!\",\n    \"invalidEmail\": \"Por favor, insira um endereço de e-mail válido\",\n    \"alreadyHaveAnAccount\": \"Já tem uma conta?\",\n    \"logIn\": \"Entrar\",\n    \"generalError\": \"Algo deu errado. Tente novamente mais tarde.\",\n    \"limitRateError\": \"Por razões de segurança, você só pode solicitar um link mágico a cada 60 segundos\",\n    \"magicLinkSentDescription\": \"Um Link Mágico foi enviado para seu e-mail. Clique no link para concluir seu login. O link expirará após 5 minutos.\",\n    \"anonymous\": \"Anônimo\",\n    \"LogInWithGoogle\": \"Entrar com o Google\",\n    \"LogInWithGithub\": \"Entrar com o Github\",\n    \"LogInWithDiscord\": \"Entrar com o Discord\",\n    \"loginAsGuestButtonText\": \"Iniciar\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Escolha seu espaço de trabalho\",\n    \"create\": \"Crie um espaço de trabalho\",\n    \"reset\": \"Redefinir espaço de trabalho\",\n    \"renameWorkspace\": \"Renomear espaço de trabalho\",\n    \"resetWorkspacePrompt\": \"A redefinição do espaço de trabalho excluirá todas as páginas e dados contidos nele. Tem certeza de que deseja redefinir o espaço de trabalho? Alternativamente, você pode entrar em contato com a equipe de suporte para restaurar o espaço de trabalho\",\n    \"hint\": \"Espaço de trabalho\",\n    \"notFoundError\": \"Espaço de trabalho não encontrado\",\n    \"failedToLoad\": \"Algo deu errado! Falha ao carregar o espaço de trabalho. Tente fechar qualquer instância aberta do @:appName e tente novamente.\",\n    \"errorActions\": {\n      \"reportIssue\": \"Reporte um problema\",\n      \"reportIssueOnGithub\": \"Reportar um problema no Github\",\n      \"exportLogFiles\": \"Exportar arquivos de log\",\n      \"reachOut\": \"Entre em contato no Discord\"\n    },\n    \"menuTitle\": \"Espaços de trabalho\",\n    \"deleteWorkspaceHintText\": \"Tem certeza de que deseja excluir o espaço de trabalho? Esta ação não pode ser desfeita, e quaisquer páginas que você tenha publicado deixarão de estar publicadas.\",\n    \"createSuccess\": \"Espaço de trabalho criado com sucesso\",\n    \"createFailed\": \"Falha ao criar espaço de trabalho\",\n    \"createLimitExceeded\": \"Você atingiu o limite máximo de espaços de trabalho permitido para sua conta. Se precisar de espaços de trabalho adicionais para continuar seu trabalho, solicite no Github\",\n    \"deleteSuccess\": \"Espaço de trabalho excluído com sucesso\",\n    \"deleteFailed\": \"Falha ao excluir o espaço de trabalho\",\n    \"openSuccess\": \"Espaço de trabalho aberto com sucesso\",\n    \"openFailed\": \"Falha ao abrir o espaço de trabalho\",\n    \"renameSuccess\": \"Espaço de trabalho renomeado com sucesso\",\n    \"renameFailed\": \"Falha ao renomear o espaço de trabalho\",\n    \"updateIconSuccess\": \"Ícone do espaço de trabalho atualizado com sucesso\",\n    \"updateIconFailed\": \"Falha ao atualizar ícone do espaço de trabalho\",\n    \"cannotDeleteTheOnlyWorkspace\": \"Não é possível excluir o único espaço de trabalho\",\n    \"fetchWorkspacesFailed\": \"Falha ao buscar espaços de trabalho\",\n    \"leaveCurrentWorkspace\": \"Sair do espaço de trabalho\",\n    \"leaveCurrentWorkspacePrompt\": \"Tem certeza de que deseja sair do espaço de trabalho atual?\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Compartilhar\",\n    \"workInProgress\": \"Em breve\",\n    \"markdown\": \"Marcador\",\n    \"clipboard\": \"Copiar para área de transferência\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Copiar link\",\n    \"publishToTheWeb\": \"Publicar na Web\",\n    \"publishToTheWebHint\": \"Crie um site com AppFlowy\",\n    \"publish\": \"Publicar\",\n    \"unPublish\": \"Remover publicação\",\n    \"visitSite\": \"Visitar site\",\n    \"exportAsTab\": \"Exportar como\",\n    \"publishTab\": \"Publicar\",\n    \"shareTab\": \"Compartilhar\"\n  },\n  \"moreAction\": {\n    \"small\": \"pequeno\",\n    \"medium\": \"médio\",\n    \"large\": \"grande\",\n    \"fontSize\": \"Tamanho da fonte\",\n    \"import\": \"Importar\",\n    \"moreOptions\": \"Mais opções\",\n    \"wordCount\": \"Contagem de palavras: {}\",\n    \"charCount\": \"Contagem de caracteres: {}\",\n    \"createdAt\": \"Criado: {}\",\n    \"deleteView\": \"Excluir\",\n    \"duplicateView\": \"Duplicar\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Texto e Remarcação\",\n    \"documentFromV010\": \"Documento de v0.1.0\",\n    \"databaseFromV010\": \"Banco de dados de v0.1.0\",\n    \"csv\": \"CSV\",\n    \"database\": \"Base de dados\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Renomear\",\n    \"delete\": \"Apagar\",\n    \"duplicate\": \"Duplicar\",\n    \"unfavorite\": \"Remover dos favoritos\",\n    \"favorite\": \"Adicionar aos favoritos\",\n    \"openNewTab\": \"Abrir em uma nova guia\",\n    \"moveTo\": \"Mover para\",\n    \"addToFavorites\": \"Adicionar aos favoritos\",\n    \"copyLink\": \"Copiar link\",\n    \"changeIcon\": \"Alterar ícone\",\n    \"collapseAllPages\": \"Recolher todas as subpáginas\"\n  },\n  \"blankPageTitle\": \"Página em branco\",\n  \"newPageText\": \"Nova página\",\n  \"newDocumentText\": \"Novo Documento\",\n  \"newGridText\": \"Nova grelha\",\n  \"newCalendarText\": \"Novo calendário\",\n  \"newBoardText\": \"Novo quadro\",\n  \"chat\": {\n    \"newChat\": \"Bate-papo com IA\",\n    \"inputMessageHint\": \"Pergunte a IA @:appName\",\n    \"inputLocalAIMessageHint\": \"Pergunte a IA local @:appName\",\n    \"unsupportedCloudPrompt\": \"Este recurso só está disponível ao usar a nuvem @:appName\",\n    \"relatedQuestion\": \"Relacionado\",\n    \"serverUnavailable\": \"Serviço Temporariamente Indisponível. Tente novamente mais tarde.\",\n    \"aiServerUnavailable\": \"🌈 Uh-oh! 🌈. Um unicórnio comeu nossa resposta. Por favor, tente novamente!\",\n    \"clickToRetry\": \"Clique para tentar novamente\",\n    \"regenerateAnswer\": \"Gerar novamente\",\n    \"question1\": \"Como usar Kanban para gerenciar tarefas\",\n    \"question2\": \"Explique o método GTD\",\n    \"question3\": \"Por que usar Rust\",\n    \"question4\": \"Receita com o que tenho na cozinha\",\n    \"aiMistakePrompt\": \"A IA pode cometer erros. Verifique informações importantes.\",\n    \"chatWithFilePrompt\": \"Você quer conversar com o arquivo?\",\n    \"indexFileSuccess\": \"Arquivo indexado com sucesso\",\n    \"inputActionNoPages\": \"Nenhum resultado\",\n    \"referenceSource\": {\n      \"zero\": \"0 fontes encontradas\",\n      \"one\": \"{count} fonte encontrada\",\n      \"other\": \"{count} fontes encontradas\"\n    },\n    \"clickToMention\": \"Clique para mencionar uma página\",\n    \"uploadFile\": \"Carregue arquivos PDFs, md ou txt para conversar\",\n    \"questionDetail\": \"Olá {}! Como posso te ajudar hoje?\",\n    \"indexingFile\": \"Indexando {}\"\n  },\n  \"trash\": {\n    \"text\": \"Lixeira\",\n    \"restoreAll\": \"Restaurar tudo\",\n    \"deleteAll\": \"Apagar tudo\",\n    \"pageHeader\": {\n      \"fileName\": \"Nome do arquivo\",\n      \"lastModified\": \"Última modificação\",\n      \"created\": \"Criado\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Tem certeza de que deseja excluir todas as páginas da Lixeira?\",\n      \"caption\": \"Essa ação não pode ser desfeita.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Tem certeza de que deseja restaurar todas as páginas da Lixeira?\",\n      \"caption\": \"Essa ação não pode ser desfeita.\"\n    },\n    \"mobile\": {\n      \"actions\": \"Ações de lixo\",\n      \"empty\": \"A lixeira está vazia\",\n      \"emptyDescription\": \"Você não tem nenhum arquivo excluído\",\n      \"isDeleted\": \"foi deletado\",\n      \"isRestored\": \"foi restaurado\"\n    },\n    \"confirmDeleteTitle\": \"Tem certeza de que deseja excluir esta página permanentemente?\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Está página está na lixeira\",\n    \"restore\": \"Restaurar a página\",\n    \"deletePermanent\": \"Apagar permanentemente\"\n  },\n  \"dialogCreatePageNameHint\": \"Nome da página\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Atalhos\",\n    \"whatsNew\": \"O que há de novo?\",\n    \"markdown\": \"Remarcação\",\n    \"debug\": {\n      \"name\": \"Informação de depuração\",\n      \"success\": \"Informação de depuração copiada para a área de transferência!\",\n      \"fail\": \"Falha ao copiar a informação de depuração para a área de transferência\"\n    },\n    \"feedback\": \"Opinião\",\n    \"help\": \"Ajuda e Suporte\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Remover, renomear e muito mais...\",\n    \"addPageTooltip\": \"Adicionar uma nova página.\",\n    \"defaultNewPageName\": \"Sem título\",\n    \"renameDialog\": \"Renomear\"\n  },\n  \"noPagesInside\": \"Sem páginas internas\",\n  \"toolbar\": {\n    \"undo\": \"Desfazer\",\n    \"redo\": \"Refazer\",\n    \"bold\": \"Negrito\",\n    \"italic\": \"Itálico\",\n    \"underline\": \"Sublinhado\",\n    \"strike\": \"Tachado\",\n    \"numList\": \"Lista numerada\",\n    \"bulletList\": \"Lista com marcadores\",\n    \"checkList\": \"Check list\",\n    \"inlineCode\": \"Embutir código\",\n    \"quote\": \"Citação em bloco\",\n    \"header\": \"Cabeçalho\",\n    \"highlight\": \"Destacar\",\n    \"color\": \"Cor\",\n    \"addLink\": \"Adicionar link\",\n    \"link\": \"Link\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Mudar para o modo claro\",\n    \"darkMode\": \"Mudar para o modo escuro\",\n    \"openAsPage\": \"Abrir como uma página\",\n    \"addNewRow\": \"Adicionar uma nova linha\",\n    \"openMenu\": \"Clique para abrir o menu\",\n    \"dragRow\": \"Pressione e segure para reordenar a linha\",\n    \"viewDataBase\": \"Visualizar banco de dados\",\n    \"referencePage\": \"Esta {name} é uma referência\",\n    \"addBlockBelow\": \"Adicione um bloco abaixo\",\n    \"aiGenerate\": \"Gerar\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Fechar barra lateral\",\n    \"openSidebar\": \"Abrir barra lateral\",\n    \"personal\": \"Pessoal\",\n    \"private\": \"Privado\",\n    \"workspace\": \"Espaço de trabalho\",\n    \"favorites\": \"Favoritos\",\n    \"clickToHidePrivate\": \"Clique para ocultar o espaço privado\\nAs páginas que você criou aqui são visíveis apenas para você\",\n    \"clickToHideWorkspace\": \"Clique para ocultar o espaço de trabalho\\nAs páginas que você criou aqui são visíveis para todos os membros\",\n    \"clickToHidePersonal\": \"Clique para ocultar a seção pessoal\",\n    \"clickToHideFavorites\": \"Clique para ocultar a seção favorita\",\n    \"addAPage\": \"Adicionar uma página\",\n    \"addAPageToPrivate\": \"Adicionar uma página ao espaço privado\",\n    \"addAPageToWorkspace\": \"Adicionar uma página ao espaço de trabalho\",\n    \"recent\": \"Recentes\",\n    \"today\": \"Hoje\",\n    \"thisWeek\": \"Essa semana\",\n    \"others\": \"Favoritos anteriores\",\n    \"justNow\": \"agora mesmo\",\n    \"minutesAgo\": \"{count} minutos atrás\",\n    \"lastViewed\": \"Última visualização\",\n    \"favoriteAt\": \"Favorito\",\n    \"emptyRecent\": \"Nenhum documento recente\",\n    \"emptyRecentDescription\": \"Conforme você visualiza os documentos, eles aparecerão aqui para fácil recuperação\",\n    \"emptyFavorite\": \"Nenhum documento favorito\",\n    \"emptyFavoriteDescription\": \"Comece a explorar e marque os documentos como favoritos. Eles serão listados aqui para acesso rápido!\",\n    \"removePageFromRecent\": \"Remover esta página dos Recentes?\",\n    \"removeSuccess\": \"Removido com sucesso\",\n    \"favoriteSpace\": \"Favoritos\",\n    \"RecentSpace\": \"Recente\",\n    \"Spaces\": \"Espaços\",\n    \"upgradeToPro\": \"Atualizar para Pro\",\n    \"upgradeToAIMax\": \"Desbloqueie IA ilimitada\",\n    \"storageLimitDialogTitle\": \"Você ficou sem armazenamento gratuito. Atualize para desbloquear armazenamento ilimitado\",\n    \"aiResponseLimitTitle\": \"Você ficou sem respostas de IA gratuitas. Atualize para o Plano Pro ou adquira um complemento de IA para desbloquear respostas ilimitadas\",\n    \"aiResponseLimitDialogTitle\": \"Limite de respostas de IA atingido\",\n    \"aiResponseLimit\": \"Você ficou sem respostas de IA gratuitas.\\n\\nVá para Configurações -> Plano -> Clique em AI Max ou Plano Pro para obter mais respostas de IA\",\n    \"askOwnerToUpgradeToPro\": \"Seu espaço de trabalho está ficando sem armazenamento gratuito. Peça ao proprietário do seu espaço de trabalho para atualizar para o Plano Pro\",\n    \"askOwnerToUpgradeToAIMax\": \"Seu espaço de trabalho está ficando sem respostas de IA gratuitas. Peça ao proprietário do seu espaço de trabalho para atualizar o plano ou adquirir complementos de IA\",\n    \"purchaseStorageSpace\": \"Adquirir espaço de armazenamento\",\n    \"purchaseAIResponse\": \"Adquirir \",\n    \"askOwnerToUpgradeToLocalAI\": \"Peça ao proprietário do espaço de trabalho para habilitar a IA no dispositivo\",\n    \"upgradeToAILocal\": \"Execute modelos locais no seu dispositivo para máxima privacidade\",\n    \"upgradeToAILocalDesc\": \"Converse com PDFs, melhore sua escrita e preencha tabelas automaticamente usando IA local\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Nota exportada como um marcador\",\n      \"path\": \"Documentos/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Contatos\",\n    \"whatsHappening\": \"O que está acontecendo essa semana?\",\n    \"addContact\": \"Adicionar um contato\",\n    \"editContact\": \"Editar um contato\"\n  },\n  \"button\": {\n    \"ok\": \"OK\",\n    \"confirm\": \"Confirmar\",\n    \"done\": \"Feito\",\n    \"cancel\": \"Cancelar\",\n    \"signIn\": \"Conectar\",\n    \"signOut\": \"Desconectar\",\n    \"complete\": \"Completar\",\n    \"save\": \"Salvar\",\n    \"generate\": \"Gerar\",\n    \"esc\": \"Sair\",\n    \"keep\": \"Manter\",\n    \"tryAgain\": \"Tente novamente\",\n    \"discard\": \"Descartar\",\n    \"replace\": \"substituir\",\n    \"insertBelow\": \"Inserir Abaixo\",\n    \"insertAbove\": \"Insira acima\",\n    \"upload\": \"Carregar\",\n    \"edit\": \"Editar\",\n    \"delete\": \"Excluir\",\n    \"duplicate\": \"Duplicado\",\n    \"putback\": \"Por de volta\",\n    \"update\": \"Atualizar\",\n    \"share\": \"Compartilhar\",\n    \"removeFromFavorites\": \"Remover dos favoritos\",\n    \"removeFromRecent\": \"Remover dos recentes\",\n    \"addToFavorites\": \"Adicionar aos favoritos\",\n    \"favoriteSuccessfully\": \"Adicionado aos favoritos\",\n    \"unfavoriteSuccessfully\": \"Removido dos favoritos\",\n    \"duplicateSuccessfully\": \"Duplicado com sucesso\",\n    \"rename\": \"Renomear\",\n    \"helpCenter\": \"Central de Ajuda\",\n    \"add\": \"Adicionar\",\n    \"yes\": \"Sim\",\n    \"no\": \"Não\",\n    \"clear\": \"Limpar\",\n    \"remove\": \"Remover\",\n    \"dontRemove\": \"Não remova\",\n    \"copyLink\": \"Copiar Link\",\n    \"align\": \"Alinhar\",\n    \"login\": \"Entrar\",\n    \"logout\": \"Sair\",\n    \"deleteAccount\": \"Deletar conta\",\n    \"back\": \"Voltar\",\n    \"signInGoogle\": \"Continuar com o Google\",\n    \"signInGithub\": \"Continuar com o Github\",\n    \"signInDiscord\": \"Continuar com o Discord\",\n    \"more\": \"Mais\",\n    \"create\": \"Criar\",\n    \"close\": \"Fechar\",\n    \"next\": \"Próximo\",\n    \"previous\": \"Anterior\",\n    \"submit\": \"Enviar\",\n    \"tryAGain\": \"Tentar novamente\"\n  },\n  \"label\": {\n    \"welcome\": \"Bem-vindo!\",\n    \"firstName\": \"Nome\",\n    \"middleName\": \"Sobrenome\",\n    \"lastName\": \"Último nome\",\n    \"stepX\": \"Passo {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Erro ao conectar a sua conta.\",\n      \"failedMsg\": \"Verifique se você concluiu o processo de conexão em seu navegador.\"\n    },\n    \"google\": {\n      \"title\": \"Conexão com o GOOGLE\",\n      \"instruction1\": \"Para importar seus Contatos do Google, você precisará autorizar este aplicativo usando seu navegador.\",\n      \"instruction2\": \"Copie este código para sua área de transferência clicando no ícone ou selecionando o texto:\",\n      \"instruction3\": \"Navegue até o link a seguir em seu navegador e digite o código acima:\",\n      \"instruction4\": \"Pressione o botão abaixo ao concluir a inscrição:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Configurações\",\n    \"popupMenuItem\": {\n      \"settings\": \"Configurações\",\n      \"members\": \"Membros\",\n      \"trash\": \"Lixo\",\n      \"helpAndSupport\": \"Ajuda e Suporte\"\n    },\n    \"accountPage\": {\n      \"menuLabel\": \"Minha conta\",\n      \"title\": \"Minha conta\",\n      \"general\": {\n        \"title\": \"Nome da conta e foto de perfil\",\n        \"changeProfilePicture\": \"Alterar foto do perfil\"\n      },\n      \"email\": {\n        \"title\": \"E-mail\",\n        \"actions\": {\n          \"change\": \"Alterar e-mail\"\n        }\n      },\n      \"login\": {\n        \"title\": \"Entrar com uma conta\",\n        \"loginLabel\": \"Entrar\",\n        \"logoutLabel\": \"Sair\"\n      }\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"Espaço de trabalho\",\n      \"title\": \"Espaço de trabalho\",\n      \"description\": \"Personalize a aparência do seu espaço de trabalho, tema, fonte, layout de texto, formato de data/hora e idioma.\",\n      \"workspaceName\": {\n        \"title\": \"Nome do espaço de trabalho\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"Ícone do espaço de trabalho\",\n        \"description\": \"Carregue uma imagem ou use um emoji para seu espaço de trabalho. O ícone será exibido na sua barra lateral e notificações.\"\n      },\n      \"appearance\": {\n        \"title\": \"Aparência\",\n        \"description\": \"Personalize a aparência do seu espaço de trabalho, tema, fonte, layout de texto, data, hora e idioma.\",\n        \"options\": {\n          \"system\": \"Automático\",\n          \"light\": \"Claro\",\n          \"dark\": \"Escuro\"\n        }\n      },\n      \"resetCursorColor\": {\n        \"title\": \"Redefinir a cor do cursor do documento\",\n        \"description\": \"Tem certeza de que deseja redefinir a cor do cursor?\"\n      },\n      \"resetSelectionColor\": {\n        \"title\": \"Redefinir cor de seleção de documento\",\n        \"description\": \"Tem certeza de que deseja redefinir a cor de seleção?\"\n      },\n      \"theme\": {\n        \"title\": \"Tema\",\n        \"description\": \"Selecione um tema predefinido ou carregue seu próprio tema personalizado.\",\n        \"uploadCustomThemeTooltip\": \"Carregar um tema personalizado\"\n      },\n      \"workspaceFont\": {\n        \"title\": \"Fonte do espaço de trabalho\",\n        \"noFontHint\": \"Nenhuma fonte encontrada, tente outro termo.\"\n      },\n      \"textDirection\": {\n        \"title\": \"Direção do texto\",\n        \"leftToRight\": \"Da esquerda para a direita\",\n        \"rightToLeft\": \"Da direita para a esquerda\",\n        \"auto\": \"Automático\",\n        \"enableRTLItems\": \"Habilitar items da barra de ferramenta da direita para a esquerda\"\n      },\n      \"layoutDirection\": {\n        \"title\": \"Direção do layout\",\n        \"leftToRight\": \"Da esquerda para a direita\",\n        \"rightToLeft\": \"Da direita para a esquerda\"\n      },\n      \"dateTime\": {\n        \"title\": \"Data e hora\",\n        \"example\": \"{} as {} ({})\",\n        \"24HourTime\": \"Tempo de 24 horas\",\n        \"dateFormat\": {\n          \"label\": \"Formato de data\",\n          \"us\": \"EUA\",\n          \"friendly\": \"Amigável\"\n        }\n      },\n      \"language\": {\n        \"title\": \"Língua\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"Excluir espaço de trabalho\",\n        \"content\": \"Tem certeza de que deseja excluir este espaço de trabalho? Esta ação não pode ser desfeita, e quaisquer páginas que você tenha publicado deixarão de estar publicadas.\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"Sair do espaço de trabalho\",\n        \"content\": \"Tem certeza de que deseja sair deste espaço de trabalho? Você perderá o acesso a todas as páginas e dados dentro dele.\"\n      },\n      \"manageWorkspace\": {\n        \"title\": \"Gerenciar espaço de trabalho\",\n        \"leaveWorkspace\": \"Sair do espaço de trabalho\",\n        \"deleteWorkspace\": \"Excluir espaço de trabalho\"\n      }\n    },\n    \"manageDataPage\": {\n      \"menuLabel\": \"Gerenciar dados\",\n      \"title\": \"Gerenciar dados\",\n      \"description\": \"Gerencie o armazenamento local de dados ou importe seus dados existentes para @:appName .\",\n      \"dataStorage\": {\n        \"title\": \"Local de armazenamento de arquivos\",\n        \"tooltip\": \"O local onde seus arquivos são armazenados\",\n        \"actions\": {\n          \"change\": \"Mudar caminho\",\n          \"open\": \"Abrir pasta\",\n          \"openTooltip\": \"Abrir local da pasta de dados atual\",\n          \"copy\": \"Copiar caminho\",\n          \"copiedHint\": \"Caminho copiado!\",\n          \"resetTooltip\": \"Redefinir para o local padrão\"\n        },\n        \"resetDialog\": {\n          \"title\": \"Tem certeza?\",\n          \"description\": \"Redefinir o caminho para o local de dados padrão não excluirá seus dados. Se você quiser reimportar seus dados atuais, você deve copiar o caminho do seu local atual primeiro.\"\n        }\n      },\n      \"importData\": {\n        \"title\": \"Importar dados\",\n        \"tooltip\": \"Importar dados das pastas de backups/dados de @:appName\",\n        \"description\": \"Copiar dados de uma pasta de dados externa ao @:appName\",\n        \"action\": \"Selecionar arquivo\"\n      },\n      \"encryption\": {\n        \"title\": \"Criptografia\",\n        \"tooltip\": \"Gerencie como seus dados são armazenados e criptografados\",\n        \"descriptionNoEncryption\": \"Ativar a criptografia criptografará todos os dados. Isso não pode ser desfeito.\",\n        \"descriptionEncrypted\": \"Seus dados estão criptografados.\",\n        \"action\": \"Criptografar dados\",\n        \"dialog\": {\n          \"title\": \"Criptografar todos os seus dados?\",\n          \"description\": \"Criptografar todos os seus dados manterá seus dados seguros e protegidos. Esta ação NÃO pode ser desfeita. Tem certeza de que deseja continuar?\"\n        }\n      },\n      \"cache\": {\n        \"title\": \"Limpar cache\",\n        \"description\": \"Ajude a resolver problemas como imagem não carregando, páginas faltando em um espaço e fontes não carregando. Isso não afetará seus dados.\",\n        \"dialog\": {\n          \"title\": \"Limpar cache\",\n          \"description\": \"Ajude a resolver problemas como imagem não carregando, páginas faltando em um espaço e fontes não carregando. Isso não afetará seus dados.\",\n          \"successHint\": \"Cache limpo!\"\n        }\n      },\n      \"data\": {\n        \"fixYourData\": \"Corrija seus dados\",\n        \"fixButton\": \"Corrigir\",\n        \"fixYourDataDescription\": \"Se estiver com problemas com seus dados, você pode tentar corrigi-los aqui.\"\n      }\n    },\n    \"shortcutsPage\": {\n      \"menuLabel\": \"Atalhos\",\n      \"title\": \"Atalhos\",\n      \"editBindingHint\": \"Insira uma nova combinação\",\n      \"searchHint\": \"Pesquisar\",\n      \"actions\": {\n        \"resetDefault\": \"Redefinir padrão\"\n      },\n      \"errorPage\": {\n        \"message\": \"Falha ao carregar atalhos: {}\",\n        \"howToFix\": \"Por favor, tente novamente. Se o problema persistir, entre em contato pelo GitHub.\"\n      },\n      \"resetDialog\": {\n        \"title\": \"Redefinir atalhos\",\n        \"description\": \"Isso redefinirá todas as suas combinações de teclas para o padrão. Você não poderá desfazer isso depois. Tem certeza de que deseja continuar?\",\n        \"buttonLabel\": \"Redefinir\"\n      },\n      \"conflictDialog\": {\n        \"title\": \"{} já está em uso\",\n        \"descriptionPrefix\": \"Esta combinação de teclas está sendo usada atualmente por \",\n        \"descriptionSuffix\": \". Se você substituir esta combinação de teclas, ela será removida de {}.\",\n        \"confirmLabel\": \"Continuar\"\n      },\n      \"editTooltip\": \"Pressione para começar a editar a combinação de teclas\",\n      \"keybindings\": {\n        \"toggleToDoList\": \"Alternar para a lista de tarefas\",\n        \"insertNewParagraphInCodeblock\": \"Inserir novo parágrafo\",\n        \"pasteInCodeblock\": \"Colar bloco de código\",\n        \"selectAllCodeblock\": \"Selecionar tudo\"\n      }\n    },\n    \"menu\": {\n      \"appearance\": \"Aparência\",\n      \"language\": \"Idioma\",\n      \"user\": \"Usuário\",\n      \"files\": \"Arquivos\",\n      \"notifications\": \"Notificações\",\n      \"open\": \"Abrir Configurações\",\n      \"logout\": \"Sair\",\n      \"logoutPrompt\": \"Tem certeza de que deseja sair?\",\n      \"selfEncryptionLogoutPrompt\": \"Tem certeza que deseja sair? Certifique-se de ter copiado o segredo de criptografia\",\n      \"syncSetting\": \"Configuração de sincronização\",\n      \"cloudSettings\": \"Configurações de nuvem\",\n      \"enableSync\": \"Ativar sincronização\",\n      \"enableEncrypt\": \"Criptografar dados\",\n      \"cloudURL\": \"URL base\",\n      \"invalidCloudURLScheme\": \"Esquema inválido\",\n      \"cloudServerType\": \"Servidor em nuvem\",\n      \"cloudServerTypeTip\": \"Observe que ele pode desconectar sua conta atual após mudar o servidor em nuvem\",\n      \"cloudLocal\": \"Local\",\n      \"cloudAppFlowy\": \"@:appName Cloud Beta\",\n      \"clickToCopy\": \"Clique para copiar\",\n      \"selfHostStart\": \"Se você não possui um servidor, consulte o\",\n      \"selfHostContent\": \"documento\",\n      \"selfHostEnd\": \"para obter orientação sobre como hospedar seu próprio servidor\",\n      \"cloudURLHint\": \"Insira o URL base do seu servidor\",\n      \"cloudWSURL\": \"URL do WebSocket\",\n      \"cloudWSURLHint\": \"Insira o endereço do websocket do seu servidor\",\n      \"restartApp\": \"Reiniciar\",\n      \"restartAppTip\": \"Reinicie o aplicativo para que as alterações tenham efeito. Observe que isso pode desconectar sua conta atual\",\n      \"enableEncryptPrompt\": \"Ative a criptografia para proteger seus dados com este segredo. Armazene-o com segurança; uma vez ativado, não pode ser desativado. Se perdidos, seus dados se tornarão irrecuperáveis. Clique para copiar\",\n      \"inputEncryptPrompt\": \"Por favor, insira seu segredo de criptografia para\",\n      \"clickToCopySecret\": \"Clique para copiar o segredo\",\n      \"configServerSetting\": \"Configure seu servidor\",\n      \"configServerGuide\": \"Após selecionar `Quick Start`, navegue até `Settings` e depois \\\"Cloud Settings\\\" para configurar seu servidor auto-hospedado.\",\n      \"inputTextFieldHint\": \"Seu segredo\",\n      \"historicalUserList\": \"Histórico de login do usuário\",\n      \"historicalUserListTooltip\": \"Esta lista exibe suas contas anônimas. Você pode clicar em uma conta para ver seus detalhes. Contas anônimas são criadas clicando no botão ‘Começar’\",\n      \"openHistoricalUser\": \"Clique para abrir a conta anônima\",\n      \"customPathPrompt\": \"Armazenar a pasta de dados do @:appName em uma pasta sincronizada na nuvem, como o Google Drive, pode representar riscos. Se o banco de dados nesta pasta for acessado ou modificado de vários locais ao mesmo tempo, isso poderá resultar em conflitos de sincronização e possível corrupção de dados\",\n      \"importAppFlowyData\": \"Importar dados da pasta @:appName externa\",\n      \"importAppFlowyDataDescription\": \"Copie dados de uma pasta de dados externa do @:appName e importe-os para a pasta de dados atual do @:appName\",\n      \"importSuccess\": \"Importou com sucesso a pasta de dados do @:appName\",\n      \"importFailed\": \"Falha ao importar a pasta de dados do @:appName\",\n      \"importGuide\": \"Para mais detalhes, consulte o documento referenciado\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Habilitar notificações\",\n        \"hint\": \"Desligue para impedir notificações locais de aparecer\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"Redefinir esta configuração\",\n      \"fontFamily\": {\n        \"label\": \"Família de fontes\",\n        \"search\": \"Procurar\"\n      },\n      \"themeMode\": {\n        \"label\": \"Tema\",\n        \"light\": \"Modo claro\",\n        \"dark\": \"Modo escuro\",\n        \"system\": \"Adaptar-se ao sistema\"\n      },\n      \"documentSettings\": {\n        \"cursorColor\": \"Cor do cursor do documento\",\n        \"selectionColor\": \"Cor de seleção do documento\",\n        \"hexEmptyError\": \"A cor hexadecimal não pode estar vazia\",\n        \"hexLengthError\": \"O valor hexadecimal deve ter 6 dígitos\",\n        \"hexInvalidError\": \"Valor hexadecimal inválido\",\n        \"opacityEmptyError\": \"A opacidade não pode estar vazia\",\n        \"opacityRangeError\": \"A opacidade deve estar entre 1 e 100\",\n        \"app\": \"App\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"Aplicar\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Direção do Layout\",\n        \"hint\": \"Controle o fluxo do conteúdo na tela, da esquerda para a direita ou da direita para a esquerda.\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\"\n      },\n      \"textDirection\": {\n        \"label\": \"Direção de texto padrão\",\n        \"hint\": \"Especifique se o texto deve começar da esquerda ou da direita como padrão.\",\n        \"ltr\": \"LTR\",\n        \"rtl\": \"RTL\",\n        \"auto\": \"AUTO\",\n        \"fallback\": \"Igual à direção do layout\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Carregar\",\n        \"uploadTheme\": \"Carregar tema\",\n        \"description\": \"Carregue seu próprio tema @:appName usando o botão abaixo.\",\n        \"loading\": \"Aguarde enquanto validamos e carregamos seu tema...\",\n        \"uploadSuccess\": \"Seu tema foi carregado com sucesso\",\n        \"deletionFailure\": \"Falha ao excluir o tema. Tente excluí-lo manualmente.\",\n        \"filePickerDialogTitle\": \"Escolha um arquivo .flowy_plugin\",\n        \"urlUploadFailure\": \"Falha ao abrir url: {}\",\n        \"failure\": \"O tema que foi carregado tinha um formato inválido.\"\n      },\n      \"theme\": \"Tema\",\n      \"builtInsLabel\": \"Temas embutidos\",\n      \"pluginsLabel\": \"Plugins\",\n      \"dateFormat\": {\n        \"label\": \"Formato de data\",\n        \"local\": \"Local\",\n        \"us\": \"US\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"Amigável\",\n        \"dmy\": \"D/M/A\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Formato de hora\",\n        \"twelveHour\": \"Doze horas\",\n        \"twentyFourHour\": \"Vinte e quatro horas\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"Mostrar caixa de diálogo de nomenclatura ao criar uma página\"\n    },\n    \"files\": {\n      \"copy\": \"cópia de\",\n      \"defaultLocation\": \"Onde os seus dados ficam armazenados\",\n      \"exportData\": \"Exporte seus dados\",\n      \"doubleTapToCopy\": \"Clique duas vezes para copiar o caminho\",\n      \"restoreLocation\": \"Restaurar para o caminho padrão do @:appName\",\n      \"customizeLocation\": \"Abrir outra pasta\",\n      \"restartApp\": \"Reinicie o aplicativo para que as alterações entrem em vigor.\",\n      \"exportDatabase\": \"Exportar banco de dados\",\n      \"selectFiles\": \"Escolha os arquivos que precisam ser exportados\",\n      \"selectAll\": \"Selecionar tudo\",\n      \"deselectAll\": \"Desmarcar todos\",\n      \"createNewFolder\": \"Criar uma nova pasta\",\n      \"createNewFolderDesc\": \"Diga-nos onde pretende armazenar os seus dados ...\",\n      \"defineWhereYourDataIsStored\": \"Defina onde seus dados são armazenados\",\n      \"open\": \"Abrir\",\n      \"openFolder\": \"Abra uma pasta existente\",\n      \"openFolderDesc\": \"Gravar na pasta @:appName existente ...\",\n      \"folderHintText\": \"nome da pasta\",\n      \"location\": \"Criando nova pasta\",\n      \"locationDesc\": \"Escolha um nome para sua pasta de dados do @:appName\",\n      \"browser\": \"Navegar\",\n      \"create\": \"Criar\",\n      \"set\": \"Definir\",\n      \"folderPath\": \"Caminho para armazenar sua pasta\",\n      \"locationCannotBeEmpty\": \"O caminho não pode estar vazio\",\n      \"pathCopiedSnackbar\": \"Caminho de armazenamento de arquivo copiado para a área de transferência!\",\n      \"changeLocationTooltips\": \"Alterar o diretório de dados\",\n      \"change\": \"Mudar\",\n      \"openLocationTooltips\": \"Abra outro diretório de dados\",\n      \"openCurrentDataFolder\": \"Abra o diretório de dados atual\",\n      \"recoverLocationTooltips\": \"Redefinir para o diretório de dados padrão do @:appName\",\n      \"exportFileSuccess\": \"Exportar arquivo com sucesso!\",\n      \"exportFileFail\": \"Falha na exportação do arquivo!\",\n      \"export\": \"Exportar\"\n    },\n    \"user\": {\n      \"name\": \"Nome\",\n      \"email\": \"E-mail\",\n      \"tooltipSelectIcon\": \"Selecionar ícone\",\n      \"selectAnIcon\": \"Escolha um ícone\",\n      \"pleaseInputYourOpenAIKey\": \"por favor insira sua chave AI\",\n      \"clickToLogout\": \"Clique para sair do usuário atual\",\n      \"pleaseInputYourStabilityAIKey\": \"insira sua chave Stability AI\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"Informações pessoais\",\n      \"username\": \"Nome de usuário\",\n      \"usernameEmptyError\": \"O nome de usuário não pode ficar vazio\",\n      \"about\": \"Sobre\",\n      \"pushNotifications\": \"Notificações via push\",\n      \"support\": \"Apoiar\",\n      \"joinDiscord\": \"Junte-se a nós no Discord\",\n      \"privacyPolicy\": \"Política de Privacidade\",\n      \"userAgreement\": \"Termo de Acordo do Usuário\",\n      \"termsAndConditions\": \"Termos e Condições\",\n      \"userprofileError\": \"Falha ao carregar o perfil do usuário\",\n      \"userprofileErrorDescription\": \"Tente sair e fazer login novamente para verificar se o problema ainda persiste.\",\n      \"selectLayout\": \"Selecione o layout\",\n      \"selectStartingDay\": \"Selecione o dia de início\",\n      \"version\": \"Versão\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"Atalhos\",\n      \"command\": \"Comando\",\n      \"keyBinding\": \"Atalhos de teclado\",\n      \"addNewCommand\": \"Adicionar novo comando\",\n      \"updateShortcutStep\": \"Pressione a combinação de teclas desejada e pressione ENTER\",\n      \"shortcutIsAlreadyUsed\": \"Este atalho já é usado para: {conflict}\",\n      \"resetToDefault\": \"Redefinir para atalhos de teclado padrão\",\n      \"couldNotLoadErrorMsg\": \"Não foi possível carregar os atalhos. Tente novamente\",\n      \"couldNotSaveErrorMsg\": \"Não foi possível salvar os atalhos. Tente novamente\",\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"Bloco de código: insirir um novo parágrafo\",\n        \"codeBlockAddTwoSpaces\": \"Bloco de código: insirir dois espaços no início da linha\",\n        \"codeBlockSelectAll\": \"Bloco de código: selecionar tudo\",\n        \"codeBlockPasteText\": \"Bloco de códito: Colar texto\",\n        \"textAlignLeft\": \"Alinhar texto à esquerda\",\n        \"textAlignCenter\": \"Alinhar texto ao centro\",\n        \"textAlignRight\": \"Alinhar texto à direita\",\n        \"codeBlockDeleteTwoSpaces\": \"Bloco de código: excluir dois espaços no início da linha\"\n      }\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Tem certeza de que deseja excluir esta visualização?\",\n    \"createView\": \"Novo\",\n    \"title\": {\n      \"placeholder\": \"Sem título\"\n    },\n    \"settings\": {\n      \"filter\": \"Filtro\",\n      \"sort\": \"Organizar\",\n      \"sortBy\": \"Organizar por\",\n      \"properties\": \"Propriedades\",\n      \"reorderPropertiesTooltip\": \"Arraste para reordenar as propriedades\",\n      \"group\": \"Grupo\",\n      \"addFilter\": \"Adicionar filtro\",\n      \"deleteFilter\": \"Apagar filtro\",\n      \"filterBy\": \"Filtrar por...\",\n      \"typeAValue\": \"Digite um valor...\",\n      \"layout\": \"Disposição\",\n      \"databaseLayout\": \"Disposição\",\n      \"editView\": \"Editar visualização\",\n      \"boardSettings\": \"Configurações do quadro\",\n      \"calendarSettings\": \"Configurações do calendário\",\n      \"createView\": \"Nova visualização\",\n      \"duplicateView\": \"Visualização duplicada\",\n      \"deleteView\": \"Excluir visualização\",\n      \"numberOfVisibleFields\": \"{} mostrando\",\n      \"Properties\": \"Propriedades\",\n      \"viewList\": \"Visualizações de banco de dados\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Contém\",\n      \"doesNotContain\": \"Não contém\",\n      \"endsWith\": \"Termina com\",\n      \"startWith\": \"Inicia com\",\n      \"is\": \"É\",\n      \"isNot\": \"Não é\",\n      \"isEmpty\": \"Está vazio\",\n      \"isNotEmpty\": \"Não está vazio\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Não\",\n        \"startWith\": \"Inicia com\",\n        \"endWith\": \"Termina com\",\n        \"isEmpty\": \"está vazio\",\n        \"isNotEmpty\": \"não está vazio\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Marcado\",\n      \"isUnchecked\": \"Desmarcado\",\n      \"choicechipPrefix\": {\n        \"is\": \"está\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"está completo\",\n      \"isIncomplted\": \"está imcompleto\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"Está\",\n      \"isNot\": \"Não está\",\n      \"contains\": \"Contém\",\n      \"doesNotContain\": \"Não contém\",\n      \"isEmpty\": \"Está vazio\",\n      \"isNotEmpty\": \"Está vazio\"\n    },\n    \"dateFilter\": {\n      \"is\": \"É\",\n      \"before\": \"É antes\",\n      \"after\": \"é depois\",\n      \"between\": \"Está entre\",\n      \"empty\": \"Está vazia\",\n      \"notEmpty\": \"Não está vazio\"\n    },\n    \"field\": {\n      \"hide\": \"Ocultar\",\n      \"show\": \"Mostrar\",\n      \"insertLeft\": \"Inserir a esquerda\",\n      \"insertRight\": \"Inserir a direita\",\n      \"duplicate\": \"Duplicar\",\n      \"delete\": \"Apagar\",\n      \"textFieldName\": \"Texto\",\n      \"checkboxFieldName\": \"Caixa de seleção\",\n      \"dateFieldName\": \"Data\",\n      \"updatedAtFieldName\": \"Hora da última modificação\",\n      \"createdAtFieldName\": \"hora criada\",\n      \"numberFieldName\": \"Números\",\n      \"singleSelectFieldName\": \"Selecionar\",\n      \"multiSelectFieldName\": \"Multi seleção\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Lista\",\n      \"numberFormat\": \"Formato numérico\",\n      \"dateFormat\": \"Formato de data\",\n      \"includeTime\": \"Incluir hora\",\n      \"isRange\": \"Data final\",\n      \"dateFormatFriendly\": \"Mês Dia, Ano\",\n      \"dateFormatISO\": \"Ano-Mês-Dia\",\n      \"dateFormatLocal\": \"Mês/Dia/Ano\",\n      \"dateFormatUS\": \"Ano/Mês/Dia\",\n      \"dateFormatDayMonthYear\": \"Dia mês ano\",\n      \"timeFormat\": \"Formato de hora\",\n      \"invalidTimeFormat\": \"Formato inválido\",\n      \"timeFormatTwelveHour\": \"12 horas\",\n      \"timeFormatTwentyFourHour\": \"24 horas\",\n      \"clearDate\": \"Limpar data\",\n      \"dateTime\": \"Data hora\",\n      \"startDateTime\": \"Data e hora de início\",\n      \"endDateTime\": \"Data e hora de término\",\n      \"failedToLoadDate\": \"Falha ao carregar o valor da data\",\n      \"selectTime\": \"Selecione o horário\",\n      \"selectDate\": \"Selecione a data\",\n      \"visibility\": \"Visibilidade\",\n      \"propertyType\": \"Tipo de Propriedade\",\n      \"addSelectOption\": \"Adicionar uma opção\",\n      \"typeANewOption\": \"Digite uma nova opção\",\n      \"optionTitle\": \"Opções\",\n      \"addOption\": \"Adicioar opção\",\n      \"editProperty\": \"Editar propriedade\",\n      \"newProperty\": \"Nova coluna\",\n      \"deleteFieldPromptMessage\": \"Tem certeza? Esta propriedade será excluída\",\n      \"newColumn\": \"Nova Coluna\",\n      \"format\": \"Formato\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Adicione um novo campo\",\n      \"fieldDragElementTooltip\": \"Clique para abrir o menu\",\n      \"showHiddenFields\": {\n        \"one\": \"Mostrar campo oculto {count}\",\n        \"many\": \"Mostrar {count} campos ocultos\",\n        \"other\": \"Mostrar {count} campos ocultos\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"Ocultar {count} campo oculto\",\n        \"many\": \"Ocultar {count} campos ocultos\",\n        \"other\": \"Ocultar {count} campos ocultos\"\n      }\n    },\n    \"sort\": {\n      \"ascending\": \"Crescente\",\n      \"descending\": \"Decrescente\",\n      \"deleteAllSorts\": \"Apagar todas as ordenações\",\n      \"addSort\": \"Adicionar ordenação\",\n      \"deleteSort\": \"Apagar ordenação\"\n    },\n    \"row\": {\n      \"duplicate\": \"Duplicar\",\n      \"delete\": \"Apagar\",\n      \"titlePlaceholder\": \"Sem título\",\n      \"textPlaceholder\": \"Vazio\",\n      \"copyProperty\": \"Propriedade copiada para a área de transferência\",\n      \"count\": \"Contagem\",\n      \"newRow\": \"Nova linha\",\n      \"action\": \"Ação\",\n      \"add\": \"Clique em adicionar abaixo\",\n      \"drag\": \"Arraste para mover\",\n      \"dragAndClick\": \"Arraste para mover, clique para abrir o menu\",\n      \"insertRecordAbove\": \"Insira o registro acima\",\n      \"insertRecordBelow\": \"Insira o registro abaixo\"\n    },\n    \"selectOption\": {\n      \"create\": \"Criar\",\n      \"purpleColor\": \"Roxo\",\n      \"pinkColor\": \"Rosa\",\n      \"lightPinkColor\": \"Rosa claro\",\n      \"orangeColor\": \"Laranja\",\n      \"yellowColor\": \"Amarelo\",\n      \"limeColor\": \"Verde limão\",\n      \"greenColor\": \"Verde\",\n      \"aquaColor\": \"Áqua\",\n      \"blueColor\": \"Azul\",\n      \"deleteTag\": \"Apagar etiqueta\",\n      \"colorPanelTitle\": \"cores\",\n      \"panelTitle\": \"Selecione uma opção ou crie uma\",\n      \"searchOption\": \"Procurar uma opção\",\n      \"searchOrCreateOption\": \"Pesquise ou crie uma opção...\",\n      \"createNew\": \"Crie um novo\",\n      \"orSelectOne\": \"Ou selecione uma opção\",\n      \"typeANewOption\": \"Digite uma nova opção\",\n      \"tagName\": \"Nome da etiqueta\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"Descrição da tarefa\",\n      \"addNew\": \"Adicionar um item\",\n      \"submitNewTask\": \"Criar\",\n      \"hideComplete\": \"Ocultar tarefas concluídas\",\n      \"showComplete\": \"Mostrar todas as tarefas\"\n    },\n    \"url\": {\n      \"launch\": \"Abrir com o navegador\",\n      \"copy\": \"Copiar URL\"\n    },\n    \"menuName\": \"Grade\",\n    \"referencedGridPrefix\": \"Vista de\"\n  },\n  \"document\": {\n    \"menuName\": \"Documento\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Selecione um quadro para vincular\",\n        \"createANewBoard\": \"Criar um novo Conselho\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Selecione um grade para vincular\",\n        \"createANewGrid\": \"Criar uma nova grade\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Selecione um calendário para vincular\",\n        \"createANewCalendar\": \"Criar um novo calendário\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"Selecione um documento para vincular\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Contorno\",\n      \"codeBlock\": \"Bloco de código\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Quadro referenciado\",\n      \"referencedGrid\": \"Grade referenciada\",\n      \"referencedCalendar\": \"Calendário referenciado\",\n      \"referencedDocument\": \"Referenciar um documento\",\n      \"autoGeneratorMenuItemName\": \"Gerar nome automaticamente\",\n      \"autoGeneratorTitleName\": \"Gerar por IA\",\n      \"autoGeneratorLearnMore\": \"Saiba mais\",\n      \"autoGeneratorGenerate\": \"Gerar\",\n      \"autoGeneratorHintText\": \"Diga-nos o que você deseja gerar por IA ...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Não foi possível obter a chave da AI\",\n      \"autoGeneratorRewrite\": \"Reescrever\",\n      \"smartEdit\": \"Assistentes de IA\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"Corrigir ortografia\",\n      \"warning\": \"⚠️ As respostas da IA podem ser imprecisas ou enganosas.\",\n      \"smartEditSummarize\": \"Resumir\",\n      \"smartEditImproveWriting\": \"melhorar a escrita\",\n      \"smartEditMakeLonger\": \"Faça mais\",\n      \"smartEditCouldNotFetchResult\": \"Não foi possível obter o resultado do AI\",\n      \"smartEditCouldNotFetchKey\": \"Não foi possível obter a chave AI\",\n      \"smartEditDisabled\": \"Conecte AI em Configurações\",\n      \"discardResponse\": \"Deseja descartar as respostas de IA?\",\n      \"createInlineMathEquation\": \"Criar equação\",\n      \"fonts\": \"Fontes\",\n      \"insertDate\": \"Inserir data\",\n      \"emoji\": \"Emoji\",\n      \"toggleList\": \"Alternar lista\",\n      \"quoteList\": \"Lista de citações\",\n      \"numberedList\": \"Lista numerada\",\n      \"bulletedList\": \"Lista com marcadores\",\n      \"todoList\": \"Lista de afazeres\",\n      \"callout\": \"Destacar\",\n      \"cover\": {\n        \"changeCover\": \"Mudar capa\",\n        \"colors\": \"cores\",\n        \"images\": \"Imagens\",\n        \"clearAll\": \"Limpar tudo\",\n        \"abstract\": \"Abstrato\",\n        \"addCover\": \"Adicionar Capa\",\n        \"addLocalImage\": \"Adicionar imagem local\",\n        \"invalidImageUrl\": \"URL de imagem inválido\",\n        \"failedToAddImageToGallery\": \"Falha ao adicionar imagem à galeria\",\n        \"enterImageUrl\": \"Insira o URL da imagem\",\n        \"add\": \"Adicionar\",\n        \"back\": \"Voltar\",\n        \"saveToGallery\": \"Salvar na galeria\",\n        \"removeIcon\": \"Remover ícone\",\n        \"pasteImageUrl\": \"Colar URL da imagem\",\n        \"or\": \"OU\",\n        \"pickFromFiles\": \"Escolha entre os arquivos\",\n        \"couldNotFetchImage\": \"Não foi possível obter a imagem\",\n        \"imageSavingFailed\": \"Falha ao salvar a imagem\",\n        \"addIcon\": \"Adicionar ícone\",\n        \"changeIcon\": \"Alterar ícone\",\n        \"coverRemoveAlert\": \"Ele será removido da capa após ser excluído.\",\n        \"alertDialogConfirmation\": \"Você tem certeza que quer continuar?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"Equação Matemática\",\n        \"addMathEquation\": \"Adicionar equação matemática\",\n        \"editMathEquation\": \"Editar equação matemática\"\n      },\n      \"optionAction\": {\n        \"click\": \"Clique\",\n        \"toOpenMenu\": \" para abrir o menu\",\n        \"delete\": \"Excluir\",\n        \"duplicate\": \"Duplicado\",\n        \"turnInto\": \"Transformar-se em\",\n        \"moveUp\": \"Subir\",\n        \"moveDown\": \"Mover para baixo\",\n        \"color\": \"Cor\",\n        \"align\": \"Alinhar\",\n        \"left\": \"Esquerda\",\n        \"center\": \"Centro\",\n        \"right\": \"Certo\",\n        \"defaultColor\": \"Padrão\"\n      },\n      \"image\": {\n        \"addAnImage\": \"Adicione uma imagem\",\n        \"copiedToPasteBoard\": \"O link da imagem foi copiado para a área de transferência\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"O link foi copiado para a área de transferência\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Adicione títulos para criar um sumário.\"\n      },\n      \"table\": {\n        \"addAfter\": \"Adicionar depois\",\n        \"addBefore\": \"Adicionar antes\",\n        \"delete\": \"Excluir\",\n        \"clear\": \"Limpar conteúdo\",\n        \"duplicate\": \"Duplicado\",\n        \"bgColor\": \"Cor de fundo\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Cópia\",\n        \"cut\": \"Corte\",\n        \"paste\": \"Colar\"\n      },\n      \"action\": \"Ações\",\n      \"database\": {\n        \"selectDataSource\": \"Selecione a fonte de dados\",\n        \"noDataSource\": \"Nenhuma fonte de dados\",\n        \"selectADataSource\": \"Selecione uma fonte de dados\",\n        \"toContinue\": \"continuar\",\n        \"newDatabase\": \"Novo banco de dados\",\n        \"linkToDatabase\": \"Link para banco de dados\"\n      },\n      \"autoCompletionMenuItemName\": \"Preenchimento Automático\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Digite '/' para comandos\"\n    },\n    \"title\": {\n      \"placeholder\": \"Sem título\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Clique para adicionar imagem\",\n      \"upload\": {\n        \"label\": \"Carregar\",\n        \"placeholder\": \"Clique para carregar a imagem\"\n      },\n      \"url\": {\n        \"label\": \"imagem URL\",\n        \"placeholder\": \"Insira o URL da imagem\"\n      },\n      \"ai\": {\n        \"label\": \"Gerar imagem da AI\",\n        \"placeholder\": \"Insira o prompt para AI gerar imagem\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Gerar imagem da Stability AI\",\n        \"placeholder\": \"Insira o prompt para Stability AI gerar imagem\"\n      },\n      \"support\": \"O limite de tamanho da imagem é de 5 MB. Formatos suportados: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"imagem inválida\",\n        \"invalidImageSize\": \"O tamanho da imagem deve ser inferior a 5 MB\",\n        \"invalidImageFormat\": \"O formato da imagem não é suportado. Formatos suportados: JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"URL de imagem inválido\"\n      },\n      \"embedLink\": {\n        \"label\": \"Incorporar link\",\n        \"placeholder\": \"Cole ou digite um link de imagem\"\n      },\n      \"unsplash\": {\n        \"label\": \"Remover respingo\"\n      },\n      \"searchForAnImage\": \"Procurar uma imagem\",\n      \"pleaseInputYourOpenAIKey\": \"insira sua chave AI na página configurações\",\n      \"saveImageToGallery\": \"Salvar imagem\",\n      \"failedToAddImageToGallery\": \"Falha ao adicionar imagem à galeria\",\n      \"successToAddImageToGallery\": \"Imagem adicionada à galeria com sucesso\",\n      \"unableToLoadImage\": \"Não foi possível carregar a imagem\",\n      \"pleaseInputYourStabilityAIKey\": \"insira sua chave Stability AI na página Configurações\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Linguagem\",\n        \"placeholder\": \"Selecione o idioma\"\n      }\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Cole ou digite um link\",\n      \"openInNewTab\": \"Abrir em nova aba\",\n      \"copyLink\": \"Cópia do link\",\n      \"removeLink\": \"Remover link\",\n      \"url\": {\n        \"label\": \"URL do link\",\n        \"placeholder\": \"Insira o URL do link\"\n      },\n      \"title\": {\n        \"label\": \"Título do link\",\n        \"placeholder\": \"Insira o título do link\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"Mencione uma pessoa, uma página ou uma data...\",\n      \"page\": {\n        \"label\": \"Link para a página\",\n        \"tooltip\": \"Clique para abrir a página\"\n      }\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"Restaurar ao padrão\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"A versão atual não suporta este bloco.\",\n      \"blockContentHasBeenCopied\": \"O conteúdo do bloco foi copiado.\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"Novo\",\n      \"renameGroupTooltip\": \"Pressione para renomear o grupo\",\n      \"createNewColumn\": \"Adicionar um novo grupo\",\n      \"addToColumnTopTooltip\": \"Adicione um novo cartão na parte superior\",\n      \"addToColumnBottomTooltip\": \"Adicione um novo cartão na parte inferior\",\n      \"renameColumn\": \"Renomear\",\n      \"hideColumn\": \"Esconder\",\n      \"newGroup\": \"Novo grupo\",\n      \"deleteColumn\": \"Excluir\",\n      \"deleteColumnConfirmation\": \"Isso excluirá este grupo e todos os cartões nele contidos. Você tem certeza que quer continuar?\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"Grupos ocultos\",\n      \"collapseTooltip\": \"Ocultar os grupos ocultos\",\n      \"expandTooltip\": \"Ver os grupos ocultos\"\n    },\n    \"cardDetail\": \"Detalhe do cartão\",\n    \"cardActions\": \"Ações de cartão\",\n    \"cardDuplicated\": \"O cartão foi duplicado\",\n    \"cardDeleted\": \"O cartão foi excluído\",\n    \"showOnCard\": \"Mostrar detalhes no cartão\",\n    \"setting\": \"Configurações\",\n    \"propertyName\": \"Nome da propriedade\",\n    \"menuName\": \"Quadro\",\n    \"showUngrouped\": \"Mostrar itens desagrupados\",\n    \"ungroupedButtonText\": \"Desagrupado\",\n    \"ungroupedButtonTooltip\": \"Contém cartões que não pertencem a nenhum grupo\",\n    \"ungroupedItemsTitle\": \"Clique para adicionar ao quadro\",\n    \"groupBy\": \"Agrupar por\",\n    \"referencedBoardPrefix\": \"Vista de\",\n    \"notesTooltip\": \"Notas dentro\",\n    \"mobile\": {\n      \"editURL\": \"Editar URL\",\n      \"showGroup\": \"Mostrar grupo\",\n      \"showGroupContent\": \"Tem certeza de que deseja mostrar este grupo no quadro?\",\n      \"failedToLoad\": \"Falha ao carregar a visualização do quadro\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Calendário\",\n    \"defaultNewCalendarTitle\": \"Sem título\",\n    \"newEventButtonTooltip\": \"Adicionar um novo evento\",\n    \"navigation\": {\n      \"today\": \"Hoje\",\n      \"jumpToday\": \"Pular para hoje\",\n      \"previousMonth\": \"Mês anterior\",\n      \"nextMonth\": \"Próximo mês\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Mostrar números da semana\",\n      \"showWeekends\": \"Mostrar fins de semana\",\n      \"firstDayOfWeek\": \"Comece a semana em\",\n      \"layoutDateField\": \"Calendário de layout por\",\n      \"changeLayoutDateField\": \"Alterar campo de layout\",\n      \"noDateTitle\": \"sem data\",\n      \"unscheduledEventsTitle\": \"Eventos não agendados\",\n      \"clickToAdd\": \"Clique para adicionar ao calendário\",\n      \"name\": \"Layout do calendário\",\n      \"noDateHint\": \"Eventos não agendados aparecerão aqui\"\n    },\n    \"referencedCalendarPrefix\": \"Vista de\",\n    \"quickJumpYear\": \"Ir para\"\n  },\n  \"errorDialog\": {\n    \"title\": \"Erro do @:appName\",\n    \"howToFixFallback\": \"Lamentamos o inconveniente! Envie um problema em nossa página do GitHub que descreva seu erro.\",\n    \"github\": \"Ver no GitHub\"\n  },\n  \"search\": {\n    \"label\": \"Procurar\",\n    \"placeholder\": {\n      \"actions\": \"Pesquisar ações...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Copiado!\",\n      \"fail\": \"Não é possível copiar\"\n    }\n  },\n  \"unSupportBlock\": \"A versão atual não suporta este bloco.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Tem certeza de que deseja excluir {pageType}?\",\n    \"deleteContentCaption\": \"se você excluir este {pageType}, poderá restaurá-lo da lixeira.\"\n  },\n  \"colors\": {\n    \"custom\": \"Personalizado\",\n    \"default\": \"Padrão\",\n    \"red\": \"Vermelho\",\n    \"orange\": \"Laranja\",\n    \"yellow\": \"Amarelo\",\n    \"green\": \"Verde\",\n    \"blue\": \"Azul\",\n    \"purple\": \"Roxo\",\n    \"pink\": \"Rosa\",\n    \"brown\": \"Marrom\",\n    \"gray\": \"Cinza\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"Emoji\",\n    \"search\": \"Buscar emoji\",\n    \"noRecent\": \"Nenhum emoji recente\",\n    \"noEmojiFound\": \"Nenhum emoji encontrado\",\n    \"filter\": \"Filtro\",\n    \"random\": \"Aleatório\",\n    \"selectSkinTone\": \"Selecione o tom do tema\",\n    \"remove\": \"Remover emoji\",\n    \"categories\": {\n      \"smileys\": \"Smileys e Emoções\",\n      \"people\": \"Pessoas e Corpo\",\n      \"animals\": \"Animais e Natureza\",\n      \"food\": \"Comida e bebida\",\n      \"activities\": \"Atividades\",\n      \"places\": \"Viagens e lugares\",\n      \"objects\": \"Objetos\",\n      \"symbols\": \"Símbolos\",\n      \"flags\": \"Bandeiras\",\n      \"nature\": \"Natureza\",\n      \"frequentlyUsed\": \"Usado frequentemente\"\n    },\n    \"skinTone\": {\n      \"default\": \"Padrão\",\n      \"light\": \"Luz\",\n      \"mediumLight\": \"Médio-leve\",\n      \"medium\": \"Médio\",\n      \"mediumDark\": \"Médio-escuro\",\n      \"dark\": \"Escuro\"\n    }\n  },\n  \"inlineActions\": {\n    \"noResults\": \"Nenhum resultado\",\n    \"pageReference\": \"Referenciar pagina \",\n    \"docReference\": \"Referenciar documento\",\n    \"boardReference\": \"Referenciar quadro\",\n    \"calReference\": \"Referenciar calendário \",\n    \"gridReference\": \"Referenciar grade\",\n    \"date\": \"Data\",\n    \"reminder\": {\n      \"groupTitle\": \"Lembrete\",\n      \"shortKeyword\": \"lembrar\"\n    }\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"Altere o formato de data e hora nas configurações\",\n    \"dateFormat\": \"Formato de data\",\n    \"includeTime\": \"Incluir tempo\",\n    \"isRange\": \"Data final\",\n    \"timeFormat\": \"Formato de hora\",\n    \"clearDate\": \"Limpar data\"\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"Ontem\",\n    \"today\": \"Hoje\",\n    \"tomorrow\": \"Amanhã\",\n    \"oneWeek\": \"1 semana\"\n  },\n  \"notificationHub\": {\n    \"title\": \"Notificações\",\n    \"mobile\": {\n      \"title\": \"Atualizações\"\n    },\n    \"emptyTitle\": \"A par de tudo!\",\n    \"emptyBody\": \"Nenhuma notificação ou ação pendente. Aproveite a calmaria.\",\n    \"tabs\": {\n      \"inbox\": \"Caixa de entrada\",\n      \"upcoming\": \"Por vir\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"Marcar tudo como lido\",\n      \"showAll\": \"Todos\",\n      \"showUnreads\": \"Não lida\"\n    },\n    \"filters\": {\n      \"ascending\": \"Ascendente\",\n      \"descending\": \"Descendente\",\n      \"groupByDate\": \"Agrupar por data\",\n      \"showUnreadsOnly\": \"Mostrar apenas os não lidos\",\n      \"resetToDefault\": \"Restaurar ao padrão\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"Lembrete\",\n    \"message\": \"Lembre-se de marcar isso antes que você esqueça!\",\n    \"tooltipDelete\": \"Excluir\",\n    \"tooltipMarkRead\": \"Marcar como lido\",\n    \"tooltipMarkUnread\": \"Marcar como não lido\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"Localizar\",\n    \"previousMatch\": \"Localizar anterior\",\n    \"nextMatch\": \"Localizar proxímo\",\n    \"close\": \"Fechar\",\n    \"replace\": \"Substituir\",\n    \"replaceAll\": \"Substituir tudo\",\n    \"noResult\": \"Nenhum resultado\",\n    \"caseSensitive\": \"Sensível a maiúsculas e minúsculas\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"Nós lamentamos\",\n    \"loadingViewError\": \"Estamos tendo problemas para carregar esta visualização. Verifique sua conexão com a Internet, atualize o aplicativo e não hesite em entrar em contato com a equipe se o problema persistir.\"\n  },\n  \"editor\": {\n    \"bold\": \"Negrito\",\n    \"bulletedList\": \"Lista com marcadores\",\n    \"checkbox\": \"Caixa de seleção\",\n    \"embedCode\": \"Incorporar Código\",\n    \"heading1\": \"H1\",\n    \"heading2\": \"H2\",\n    \"heading3\": \"H3\",\n    \"highlight\": \"Destacar\",\n    \"color\": \"Cor\",\n    \"image\": \"Imagem\",\n    \"italic\": \"Itálico\",\n    \"link\": \"Link\",\n    \"numberedList\": \"Lista Numerada\",\n    \"quote\": \"Citar\",\n    \"strikethrough\": \"Tachado\",\n    \"text\": \"Texto\",\n    \"underline\": \"Sublinhado\",\n    \"fontColorDefault\": \"Padrão\",\n    \"fontColorGray\": \"Cinza\",\n    \"fontColorBrown\": \"Marrom\",\n    \"fontColorOrange\": \"Laranja\",\n    \"fontColorYellow\": \"Amarelo\",\n    \"fontColorGreen\": \"Verde\",\n    \"fontColorBlue\": \"Azul\",\n    \"fontColorPurple\": \"Roxo\",\n    \"fontColorPink\": \"Rosa\",\n    \"fontColorRed\": \"Vermelho\",\n    \"backgroundColorDefault\": \"Plano de fundo padrão\",\n    \"backgroundColorGray\": \"Fundo cinza\",\n    \"backgroundColorBrown\": \"Fundo marrom\",\n    \"backgroundColorOrange\": \"Fundo laranja\",\n    \"backgroundColorYellow\": \"Fundo amarelo\",\n    \"backgroundColorGreen\": \"Fundo verde\",\n    \"backgroundColorBlue\": \"Fundo azul\",\n    \"backgroundColorPurple\": \"Fundo roxo\",\n    \"backgroundColorPink\": \"Fundo rosa\",\n    \"backgroundColorRed\": \"Fundo vermelho\",\n    \"done\": \"Feito\",\n    \"cancel\": \"Cancelar\",\n    \"tint1\": \"Matiz 1\",\n    \"tint2\": \"Matiz 2\",\n    \"tint3\": \"Matiz 3\",\n    \"tint4\": \"Matiz 4\",\n    \"tint5\": \"Matiz 5\",\n    \"tint6\": \"Matiz 6\",\n    \"tint7\": \"Matiz 7\",\n    \"tint8\": \"Matiz 8\",\n    \"tint9\": \"Matiz 9\",\n    \"lightLightTint1\": \"Roxo\",\n    \"lightLightTint2\": \"Rosa\",\n    \"lightLightTint3\": \"Rosa Claro\",\n    \"lightLightTint4\": \"Laranja\",\n    \"lightLightTint5\": \"Amarelo\",\n    \"lightLightTint6\": \"Lima\",\n    \"lightLightTint7\": \"Verde\",\n    \"lightLightTint8\": \"Aqua\",\n    \"lightLightTint9\": \"Azul\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"Cabeçallho 1\",\n    \"mobileHeading2\": \"Título 2\",\n    \"mobileHeading3\": \"Título 3\",\n    \"textColor\": \"Cor do texto\",\n    \"backgroundColor\": \"Cor de fundo\",\n    \"addYourLink\": \"Adicionar seu link\",\n    \"openLink\": \"Abrir link\",\n    \"copyLink\": \"Copiar link\",\n    \"removeLink\": \"Remover link\",\n    \"editLink\": \"Editar link\",\n    \"linkText\": \"Texto\",\n    \"linkTextHint\": \"Por favor insira o texto\",\n    \"linkAddressHint\": \"Por favor insira o URL\",\n    \"highlightColor\": \"Cor de destaque\",\n    \"clearHighlightColor\": \"Cor de destaque clara\",\n    \"customColor\": \"Cor customizada\",\n    \"hexValue\": \"Valor hexadecimal\",\n    \"opacity\": \"Opacidade\",\n    \"resetToDefaultColor\": \"Redefinir para a cor padrão\",\n    \"ltr\": \"LTR\",\n    \"rtl\": \"RTL\",\n    \"auto\": \"Auto\",\n    \"cut\": \"Cortar\",\n    \"copy\": \"Copiar\",\n    \"paste\": \"Colar\",\n    \"find\": \"Encontrar\",\n    \"previousMatch\": \"Localizar anterior\",\n    \"nextMatch\": \"Localizar próxima\",\n    \"closeFind\": \"Fechar\",\n    \"replace\": \"Substituir\",\n    \"replaceAll\": \"Substituir tudo\",\n    \"regex\": \"Regex\",\n    \"caseSensitive\": \"Maiúsculas e minúsculas\",\n    \"uploadImage\": \"Enviar Imagem\",\n    \"urlImage\": \"URL da imagem\",\n    \"incorrectLink\": \"Link incorreto\",\n    \"upload\": \"Carregar\",\n    \"chooseImage\": \"Escolha uma imagem\",\n    \"loading\": \"Carregando\",\n    \"imageLoadFailed\": \"Não foi possível carregar a imagem\",\n    \"divider\": \"Divisor\",\n    \"table\": \"Tabela\",\n    \"colAddBefore\": \"Acrescentar antes\",\n    \"rowAddBefore\": \"Acrescentar antes\",\n    \"colAddAfter\": \"Adicionar após\",\n    \"rowAddAfter\": \"Adicionar após\",\n    \"colRemove\": \"Remover\",\n    \"rowRemove\": \"Remover\",\n    \"colDuplicate\": \"Duplicado\",\n    \"rowDuplicate\": \"Duplicado\",\n    \"colClear\": \"Limpar conteúdo\",\n    \"rowClear\": \"Limpar conteúdo\",\n    \"slashPlaceHolder\": \"Digite '/' para inserir um bloco ou comece a digitar\",\n    \"typeSomething\": \"Digite algo...\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"Nenhuma página favorita\",\n    \"noFavoriteHintText\": \"Deslize a página para a esquerda para adicioná-la aos seus favoritos\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"Digite um / para inserir um bloco ou comece a digitar\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"A fazer\",\n    \"bulletList\": \"Lista\",\n    \"numberList\": \"Lista\",\n    \"quote\": \"Citar\",\n    \"heading\": \"Cabeçalho {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"Ícone de página\",\n    \"language\": \"Idioma\",\n    \"font\": \"Fonte\",\n    \"actions\": \"Ações\",\n    \"date\": \"Data\",\n    \"addField\": \"Adicionar campo\",\n    \"userIcon\": \"Ícone do usuário\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/pt-PT.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Eu\",\n  \"welcomeText\": \"Bem vindo ao @:appName\",\n  \"githubStarText\": \"Dar uma estrela no Github\",\n  \"subscribeNewsletterText\": \"Inscreve-te no Newsletter\",\n  \"letsGoButtonText\": \"Bora\",\n  \"title\": \"Título\",\n  \"youCanAlso\": \"Você também pode\",\n  \"and\": \"e\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Clique para adicionar abaixo\",\n    \"addAboveCmd\": \"Alt+clique\",\n    \"addAboveMacCmd\": \"Opção+clique\",\n    \"addAboveTooltip\": \"para adicionar acima\",\n    \"dragTooltip\": \"Arraste para mover\",\n    \"openMenuTooltip\": \"Clique para abrir o menu\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Inscreve-te\",\n    \"title\": \"Inscreve-te ao @:appName\",\n    \"getStartedText\": \"Começar\",\n    \"emptyPasswordError\": \"A palavra-passe não pode estar em branco.\",\n    \"repeatPasswordEmptyError\": \"Confirmar a palavra-passe não pode estar em branco.\",\n    \"unmatchedPasswordError\": \"As palavras-passes não coincidem.\",\n    \"alreadyHaveAnAccount\": \"Já possuis uma conta?\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Password\",\n    \"repeatPasswordHint\": \"Confirma a tua password\",\n    \"signUpWith\": \"Inscreve-te com:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Entre no @:appName\",\n    \"loginButtonText\": \"Login\",\n    \"loginStartWithAnonymous\": \"Começar com uma sessão anónima\",\n    \"continueAnonymousUser\": \"Continuar com uma sessão anónima\",\n    \"buttonText\": \"Entre\",\n    \"forgotPassword\": \"Esqueceste-te da tua palavra-passe?\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Palavra-passe\",\n    \"dontHaveAnAccount\": \"Não possuis uma conta?\",\n    \"repeatPasswordEmptyError\": \"Confirmar a palavra-passe não pode estar em branco.\",\n    \"unmatchedPasswordError\": \"As palavras-passes não conferem.\",\n    \"syncPromptMessage\": \"A sincronização dos dados pode demorar um pouco. Por favor não feche esta página\",\n    \"or\": \"OU\",\n    \"signInWith\": \"Entrar com:\",\n    \"LogInWithGoogle\": \"Iniciar sessão com o Google\",\n    \"LogInWithGithub\": \"Iniciar sessão com o Github\",\n    \"LogInWithDiscord\": \"Iniciar sessão com o Discord\",\n    \"loginAsGuestButtonText\": \"Iniciar\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Escolha o seu espaço de trabalho\",\n    \"create\": \"Cria um ambiente de trabalho\",\n    \"reset\": \"Reiniciar o ambiente de trabalho\",\n    \"resetWorkspacePrompt\": \"Reinciar do espaço de trabalho excluirá todas as páginas e dados contidos. Tem certeza de que deseja reiniciar o espaço de trabalho? Alternativamente, podes entrar em contato com a equipe de suporte para restaurar o espaço de trabalho\",\n    \"hint\": \"ambiente de trabalho\",\n    \"notFoundError\": \"Ambiente de trabalho não encontrada\",\n    \"failedToLoad\": \"Algo correu mal! Falha ao carregar o espaço de trabalho. Tente fechar qualquer instância aberta do @:appName e tente novamente.\",\n    \"errorActions\": {\n      \"reportIssue\": \"Relatar problema\",\n      \"reachOut\": \"Entre em contacto no Discord\"\n    }\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Partilhar\",\n    \"workInProgress\": \"Em breve\",\n    \"markdown\": \"Markdown\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Copiar o link\"\n  },\n  \"moreAction\": {\n    \"small\": \"pequeno\",\n    \"medium\": \"médio\",\n    \"large\": \"grande\",\n    \"fontSize\": \"Tamanho da fonte\",\n    \"import\": \"Importar\",\n    \"moreOptions\": \"Mais opções\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Texto e Remarcação\",\n    \"documentFromV010\": \"Documento de v0.1.0\",\n    \"databaseFromV010\": \"Banco de dados de v0.1.0\",\n    \"csv\": \"CSV\",\n    \"database\": \"Base de dados\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Renomear\",\n    \"delete\": \"Apagar\",\n    \"duplicate\": \"Duplicar\",\n    \"unfavorite\": \"Remover dos favoritos\",\n    \"favorite\": \"Adicionar aos favoritos\",\n    \"openNewTab\": \"Abrir em uma nova guia\",\n    \"moveTo\": \"Mover para\",\n    \"addToFavorites\": \"Adicionar aos favoritos\",\n    \"copyLink\": \"Copiar o lin\"\n  },\n  \"blankPageTitle\": \"Página em branco\",\n  \"newPageText\": \"Nova página\",\n  \"newDocumentText\": \"Novo Documento\",\n  \"newGridText\": \"Nova grelha\",\n  \"newCalendarText\": \"Novo calendário\",\n  \"newBoardText\": \"Novo quadro\",\n  \"trash\": {\n    \"text\": \"Lixo\",\n    \"restoreAll\": \"Restaurar todos\",\n    \"deleteAll\": \"Apagar todos\",\n    \"pageHeader\": {\n      \"fileName\": \"Nome do ficheiro\",\n      \"lastModified\": \"Última modificação\",\n      \"created\": \"Criado\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Tem certeza de que deseja excluir todas as páginas da Lixeira?\",\n      \"caption\": \"Essa ação não pode ser desfeita.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Tem certeza de que deseja restaurar todas as páginas da Lixeira?\",\n      \"caption\": \"Essa ação não pode ser desfeita.\"\n    }\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Esta página está no lixo\",\n    \"restore\": \"Restaurar a página\",\n    \"deletePermanent\": \"Apagar permanentemente\"\n  },\n  \"dialogCreatePageNameHint\": \"Nome da página\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Atalhos\",\n    \"whatsNew\": \"O que há de novo?\",\n    \"markdown\": \"Remarcação\",\n    \"debug\": {\n      \"name\": \"Informação de depuração\",\n      \"success\": \"Copiar informação de depuração para o clipboard!\",\n      \"fail\": \"Falha em copiar a informação de depuração para o clipboard\"\n    },\n    \"feedback\": \"Opinião\",\n    \"help\": \"Ajuda & Suporte\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Remover, renomear e muito mais...\",\n    \"addPageTooltip\": \"Adiciona uma nova página.\",\n    \"defaultNewPageName\": \"Sem título\",\n    \"renameDialog\": \"Renomear\"\n  },\n  \"toolbar\": {\n    \"undo\": \"Desfazer\",\n    \"redo\": \"Refazer\",\n    \"bold\": \"Negrito\",\n    \"italic\": \"Itálico\",\n    \"underline\": \"Sublinhado\",\n    \"strike\": \"Riscado\",\n    \"numList\": \"Lista numerada\",\n    \"bulletList\": \"Lista com marcadores\",\n    \"checkList\": \"Lista de verificação\",\n    \"inlineCode\": \"Embutir código\",\n    \"quote\": \"Citação em bloco\",\n    \"header\": \"Cabeçalho\",\n    \"highlight\": \"Realçar\",\n    \"color\": \"Cor\",\n    \"addLink\": \"Adicionar link\",\n    \"link\": \"Link\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Mudar para o modo Claro.\",\n    \"darkMode\": \"Mudar para o modo Escuro.\",\n    \"openAsPage\": \"Abrir como uma página\",\n    \"addNewRow\": \"Adicionar uma nova linha\",\n    \"openMenu\": \"Clique para abrir o menu\",\n    \"dragRow\": \"Pressione e segure para reordenar a linha\",\n    \"viewDataBase\": \"Ver banco de dados\",\n    \"referencePage\": \"Este {name} é referenciado\",\n    \"addBlockBelow\": \"Adicione um bloco abaixo\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Close sidebar\",\n    \"openSidebar\": \"Open sidebar\",\n    \"personal\": \"Pessoal\",\n    \"favorites\": \"Favoritos\",\n    \"clickToHidePersonal\": \"Clique para ocultar a seção pessoal\",\n    \"clickToHideFavorites\": \"Clique para ocultar a seção favorita\",\n    \"addAPage\": \"Adicionar uma página\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Nota exportada para remarcação\",\n      \"path\": \"Documentos/fluidos\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Conctatos\",\n    \"whatsHappening\": \"O que está a acontecer nesta semana?\",\n    \"addContact\": \"Adicionar um conctato\",\n    \"editContact\": \"Editar um conctato\"\n  },\n  \"button\": {\n    \"ok\": \"OK\",\n    \"done\": \"Feito\",\n    \"cancel\": \"Cancelar\",\n    \"signIn\": \"Entrar\",\n    \"signOut\": \"Sair\",\n    \"complete\": \"Completar\",\n    \"save\": \"Guardar\",\n    \"generate\": \"Gerar\",\n    \"esc\": \"ESC\",\n    \"keep\": \"Manter\",\n    \"tryAgain\": \"Tente novamente\",\n    \"discard\": \"Descartar\",\n    \"replace\": \"Substituir\",\n    \"insertBelow\": \"Inserir Abaixo\",\n    \"upload\": \"Carregar\",\n    \"edit\": \"Editar\",\n    \"delete\": \"Excluir\",\n    \"duplicate\": \"Duplicado\",\n    \"putback\": \"Por de volta\"\n  },\n  \"label\": {\n    \"welcome\": \"Bem vindo!\",\n    \"firstName\": \"Nome\",\n    \"middleName\": \"Nome do Meio\",\n    \"lastName\": \"Apelido\",\n    \"stepX\": \"Passo {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Erro ao conectar à sua conta.\",\n      \"failedMsg\": \"Verifica se concluiste o processo de login no teu navegador.\"\n    },\n    \"google\": {\n      \"title\": \"GOOGLE SIGN-IN\",\n      \"instruction1\": \"Para importar os teus Conctatos do Google, tens de autorizar esta aplicação usando o teu navegador web.\",\n      \"instruction2\": \"Copia este código para a tua área de transferências clicando no ícone ou selecionando o texto:\",\n      \"instruction3\": \"Navega até o link a seguir no seu navegador e digite o código acima:\",\n      \"instruction4\": \"Clica no botão abaixo ao concluir a inscrição:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Definições\",\n    \"menu\": {\n      \"appearance\": \"Aparência\",\n      \"language\": \"Idioma\",\n      \"user\": \"Do utilizador\",\n      \"files\": \"arquivos\",\n      \"notifications\": \"Notificações\",\n      \"open\": \"Abrir as Definições\",\n      \"logout\": \"Sair\",\n      \"logoutPrompt\": \"Tem certeza de que deseja sair?\",\n      \"selfEncryptionLogoutPrompt\": \"Tem certeza que deseja sair? Certifique-se de ter copiado o código de encriptação\",\n      \"syncSetting\": \"Configuração da sincronização\",\n      \"enableSync\": \"Ativar sincronização\",\n      \"enableEncrypt\": \"Encriptar dados dados\",\n      \"enableEncryptPrompt\": \"Ative a encriptação para proteger seus dados com esta palavra-chave. Armazene-o com segurança; uma vez ativado, não pode ser desativado. Se perdidos, seus dados se tornarão irrecuperáveis. Clique para copiar\",\n      \"inputEncryptPrompt\": \"Por favor, insira a sua palavra-chave de encriptação para\",\n      \"clickToCopySecret\": \"Clique para copiar a palvra-chave\",\n      \"inputTextFieldHint\": \"A sua palavra-chave\",\n      \"historicalUserList\": \"Histórico de login do utilizador\",\n      \"historicalUserListTooltip\": \"Esta lista mostrs suas contas anónimas. Pode clicar numa conta para ver os detalhes. Contas anónimas são criadas clicando no botão ‘Começar’\",\n      \"openHistoricalUser\": \"Clique para abrir a conta anónima\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Ativar notificações\",\n        \"hint\": \"Desligue para impedir que notificações locais apareçam.\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"Reiniciar esta configuração\",\n      \"fontFamily\": {\n        \"label\": \"Família de fontes\",\n        \"search\": \"Procurar\"\n      },\n      \"themeMode\": {\n        \"label\": \"Modo Tema\",\n        \"light\": \"Modo claro\",\n        \"dark\": \"Modo Escuro\",\n        \"system\": \"Adaptar ao sistema\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Direção da disposição\",\n        \"hint\": \"Controle o fluxo do conteúdo no ecrã, da esquerda para a direita ou da direita para a esquerda.\",\n        \"ltr\": \"EPD\",\n        \"rtl\": \"DPE\"\n      },\n      \"textDirection\": {\n        \"label\": \"Direção de texto padrão\",\n        \"hint\": \"Especifique se o texto deve começar da esquerda ou da direita como padrão.\",\n        \"ltr\": \"EPD\",\n        \"rtl\": \"DPE\",\n        \"auto\": \"AUTO\",\n        \"fallback\": \"Igual à direção da disposição\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Carregar\",\n        \"uploadTheme\": \"Carregar tema\",\n        \"description\": \"Carregue seu próprio tema @:appName usando o botão abaixo.\",\n        \"loading\": \"Aguarde enquanto validamos e carregamos seu tema...\",\n        \"uploadSuccess\": \"Seu tema foi carregado com sucesso\",\n        \"deletionFailure\": \"Falha ao excluir o tema. Tente excluí-lo manualmente.\",\n        \"filePickerDialogTitle\": \"Escolha um arquivo .flowy_plugin\",\n        \"urlUploadFailure\": \"Falha ao abrir url: {}\",\n        \"failure\": \"O tema que foi carregado tinha um formato inválido.\"\n      },\n      \"theme\": \"Tema\",\n      \"builtInsLabel\": \"Temas integrados\",\n      \"pluginsLabel\": \"Plugins\",\n      \"dateFormat\": {\n        \"label\": \"Formato de data\",\n        \"local\": \"Local\",\n        \"us\": \"EUA\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"Amigável\",\n        \"dmy\": \"D/M/A\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Formato de hora\",\n        \"twelveHour\": \"Doze horas\",\n        \"twentyFourHour\": \"Vinte e quatro horas\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"Mostrar caixa de diálogo de nomenclatura ao criar uma página\",\n      \"lightLabel\": \"Modo Claro\",\n      \"darkLabel\": \"Modo Escuro\"\n    },\n    \"files\": {\n      \"copy\": \"cópia de\",\n      \"defaultLocation\": \"Leia arquivos e local de armazenamento de dados\",\n      \"exportData\": \"Exporte seus dados\",\n      \"doubleTapToCopy\": \"Toque duas vezes para copiar o caminho\",\n      \"restoreLocation\": \"Restaurar para o caminho padrão do @:appName\",\n      \"customizeLocation\": \"Abra outra pasta\",\n      \"restartApp\": \"Reinicie o aplicativo para que as alterações entrem em vigor.\",\n      \"exportDatabase\": \"Exportar banco de dados\",\n      \"selectFiles\": \"Selecione os arquivos que precisam ser exportados\",\n      \"selectAll\": \"Selecionar tudo\",\n      \"deselectAll\": \"Desmarcar todos\",\n      \"createNewFolder\": \"Criar uma nova pasta\",\n      \"createNewFolderDesc\": \"Diga-nos onde pretende armazenar os seus dados\",\n      \"defineWhereYourDataIsStored\": \"Defina onde seus dados são armazenados\",\n      \"open\": \"Abrir\",\n      \"openFolder\": \"Abra uma pasta existente\",\n      \"openFolderDesc\": \"Leia e grave-o em sua pasta @:appName existente\",\n      \"folderHintText\": \"nome da pasta\",\n      \"location\": \"Criando uma nova pasta\",\n      \"locationDesc\": \"Escolha um nome para sua pasta de dados do @:appName\",\n      \"browser\": \"Navegar\",\n      \"create\": \"Criar\",\n      \"set\": \"Definir\",\n      \"folderPath\": \"Caminho para armazenar sua pasta\",\n      \"locationCannotBeEmpty\": \"O caminho não pode estar vazio\",\n      \"pathCopiedSnackbar\": \"Caminho de armazenamento de arquivo copiado para a área de transferência!\",\n      \"changeLocationTooltips\": \"Alterar o diretório de dados\",\n      \"change\": \"Mudar\",\n      \"openLocationTooltips\": \"Abra outro diretório de dados\",\n      \"openCurrentDataFolder\": \"Abra o diretório de dados atual\",\n      \"recoverLocationTooltips\": \"Redefinir para o diretório de dados padrão do @:appName\",\n      \"exportFileSuccess\": \"Exportar arquivo com sucesso!\",\n      \"exportFileFail\": \"Falha na exportação do arquivo!\",\n      \"export\": \"Exportar\"\n    },\n    \"user\": {\n      \"name\": \"Nome\",\n      \"email\": \"E-mail\",\n      \"tooltipSelectIcon\": \"Selecione o ícone\",\n      \"selectAnIcon\": \"Selecione um ícone\",\n      \"pleaseInputYourOpenAIKey\": \"por favor insira sua chave AI\",\n      \"clickToLogout\": \"Clique para fazer logout\",\n      \"pleaseInputYourStabilityAIKey\": \"por favor, insira a sua chave Stability AI\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"Atalhos\",\n      \"command\": \"Comando\",\n      \"keyBinding\": \"Atalhos de teclado\",\n      \"addNewCommand\": \"Adicionar novo comando\",\n      \"updateShortcutStep\": \"Pressione a combinação de teclas desejada e pressione ENTER\",\n      \"shortcutIsAlreadyUsed\": \"Este atalho já é usado para: {conflict}\",\n      \"resetToDefault\": \"Redefinir para atalhos de teclado padrão\",\n      \"couldNotLoadErrorMsg\": \"Não foi possível carregar os atalhos. Tente novamente\",\n      \"couldNotSaveErrorMsg\": \"Não foi possível salvar os atalhos. Tente novamente\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Tem certeza de que deseja excluir esta visualização?\",\n    \"createView\": \"Novo\",\n    \"title\": {\n      \"placeholder\": \"Sem título\"\n    },\n    \"settings\": {\n      \"filter\": \"Filtro\",\n      \"sort\": \"Organizar\",\n      \"sortBy\": \"Ordenar por\",\n      \"properties\": \"Propriedades\",\n      \"reorderPropertiesTooltip\": \"Arraste para reordenar as propriedades\",\n      \"group\": \"Grupo\",\n      \"addFilter\": \"Adicionar filtro\",\n      \"deleteFilter\": \"Excluir filtro\",\n      \"filterBy\": \"Filtrar por...\",\n      \"typeAValue\": \"Digite um valor...\",\n      \"layout\": \"Disposição\",\n      \"databaseLayout\": \"Disposição\"\n    },\n    \"textFilter\": {\n      \"contains\": \"contém\",\n      \"doesNotContain\": \"Não contém\",\n      \"endsWith\": \"Termina com\",\n      \"startWith\": \"Começa com\",\n      \"is\": \"É\",\n      \"isNot\": \"não é\",\n      \"isEmpty\": \"Está vazia\",\n      \"isNotEmpty\": \"Não está vazio\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Não\",\n        \"startWith\": \"Começa com\",\n        \"endWith\": \"Termina com\",\n        \"isEmpty\": \"está vazia\",\n        \"isNotEmpty\": \"não está vazio\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Verificado\",\n      \"isUnchecked\": \"desmarcado\",\n      \"choicechipPrefix\": {\n        \"is\": \"é\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"está completo\",\n      \"isIncomplted\": \"está incompleto\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"É\",\n      \"isNot\": \"não é\",\n      \"contains\": \"contém\",\n      \"doesNotContain\": \"Não contém\",\n      \"isEmpty\": \"Está vazia\",\n      \"isNotEmpty\": \"Não está vazio\"\n    },\n    \"field\": {\n      \"hide\": \"Esconder\",\n      \"show\": \"Mostrar\",\n      \"insertLeft\": \"Inserir Esquerda\",\n      \"insertRight\": \"Inserir à Direita\",\n      \"duplicate\": \"Duplicado\",\n      \"delete\": \"Excluir\",\n      \"textFieldName\": \"Texto\",\n      \"checkboxFieldName\": \"Caixa de seleção\",\n      \"dateFieldName\": \"Data\",\n      \"updatedAtFieldName\": \"Hora da última modificação\",\n      \"createdAtFieldName\": \"hora criada\",\n      \"numberFieldName\": \"Números\",\n      \"singleSelectFieldName\": \"Selecione\",\n      \"multiSelectFieldName\": \"Seleção múltipla\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Lista de controle\",\n      \"numberFormat\": \"Formato numérico\",\n      \"dateFormat\": \"Formato de data\",\n      \"includeTime\": \"Incluir tempo\",\n      \"isRange\": \"Data de fim\",\n      \"dateFormatFriendly\": \"Mês dia ano\",\n      \"dateFormatISO\": \"Ano mês dia\",\n      \"dateFormatLocal\": \"Mês dia ano\",\n      \"dateFormatUS\": \"Ano mês dia\",\n      \"dateFormatDayMonthYear\": \"Dia mês ano\",\n      \"timeFormat\": \"Formato de hora\",\n      \"invalidTimeFormat\": \"Formato Inválido\",\n      \"timeFormatTwelveHour\": \"12 horas\",\n      \"timeFormatTwentyFourHour\": \"24 horas\",\n      \"clearDate\": \"Limpar data\",\n      \"addSelectOption\": \"Adicionar uma opção\",\n      \"optionTitle\": \"Opções\",\n      \"addOption\": \"Adicionar opção\",\n      \"editProperty\": \"Editar propriedade\",\n      \"newProperty\": \"Nova propriedade\",\n      \"deleteFieldPromptMessage\": \"Tem certeza? Esta propriedade será excluída\",\n      \"newColumn\": \"Nova Coluna\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Adicionar um novo campo\",\n      \"fieldDragElementTooltip\": \"Clique para abrir o menu\",\n      \"showHiddenFields\": {\n        \"one\": \"Mostrar campo oculto {}\",\n        \"many\": \"Mostrar campos ocultos {}\",\n        \"other\": \"Mostrar campos ocultos {}\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"Ocultar campo oculto {}\",\n        \"many\": \"Ocultar campos ocultos {}\",\n        \"other\": \"Ocultar campos ocultos {}\"\n      }\n    },\n    \"sort\": {\n      \"ascending\": \"Ascendente\",\n      \"descending\": \"descendente\",\n      \"deleteAllSorts\": \"Apagar todas as ordenações\",\n      \"addSort\": \"Adicionar classificação\",\n      \"deleteSort\": \"Excluir classificação\"\n    },\n    \"row\": {\n      \"duplicate\": \"Duplicado\",\n      \"delete\": \"Excluir\",\n      \"titlePlaceholder\": \"Sem título\",\n      \"textPlaceholder\": \"Vazio\",\n      \"copyProperty\": \"Propriedade copiada para a área de transferência\",\n      \"count\": \"Contar\",\n      \"newRow\": \"Nova linha\",\n      \"action\": \"Ação\",\n      \"add\": \"Clique em adicionar abaixo\",\n      \"drag\": \"Arraste para mover\"\n    },\n    \"selectOption\": {\n      \"create\": \"Criar\",\n      \"purpleColor\": \"Roxo\",\n      \"pinkColor\": \"Rosa\",\n      \"lightPinkColor\": \"Luz rosa\",\n      \"orangeColor\": \"Laranja\",\n      \"yellowColor\": \"Amarelo\",\n      \"limeColor\": \"Lima\",\n      \"greenColor\": \"Verde\",\n      \"aquaColor\": \"Aqua\",\n      \"blueColor\": \"Azul\",\n      \"deleteTag\": \"Excluir etiqueta\",\n      \"colorPanelTitle\": \"cores\",\n      \"panelTitle\": \"Selecione uma opção ou crie uma\",\n      \"searchOption\": \"Pesquise uma opção\",\n      \"searchOrCreateOption\": \"Pesquise ou crie uma opção...\",\n      \"createNew\": \"Crie um novo\",\n      \"orSelectOne\": \"Ou selecione uma opção\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"Descrição da tarefa\",\n      \"addNew\": \"Adicionar um item\",\n      \"submitNewTask\": \"Criar\"\n    },\n    \"menuName\": \"Grade\",\n    \"referencedGridPrefix\": \"Vista de\"\n  },\n  \"document\": {\n    \"menuName\": \"Documento\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"13:00\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Selecione um quadro para vincular\",\n        \"createANewBoard\": \"Criar um novo Conselho\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Selecione uma grade para vincular\",\n        \"createANewGrid\": \"Criar uma nova grade\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Selecione um calendário para vincular\",\n        \"createANewCalendar\": \"Criar um novo calendário\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Contorno\",\n      \"codeBlock\": \"Bloco de código\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Conselho Referenciado\",\n      \"referencedGrid\": \"grade referenciada\",\n      \"referencedCalendar\": \"calendário referenciado\",\n      \"autoGeneratorMenuItemName\": \"AI Writer\",\n      \"autoGeneratorTitleName\": \"AI: Peça à IA para escrever qualquer coisa...\",\n      \"autoGeneratorLearnMore\": \"Saber mais\",\n      \"autoGeneratorGenerate\": \"Gerar\",\n      \"autoGeneratorHintText\": \"Pergunte ao AI...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Não é possível obter a chave AI\",\n      \"autoGeneratorRewrite\": \"Reescrever\",\n      \"smartEdit\": \"Assistentes de IA\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"corrigir ortografia\",\n      \"warning\": \"⚠️ As respostas da IA podem ser imprecisas ou enganosas.\",\n      \"smartEditSummarize\": \"Resumir\",\n      \"smartEditImproveWriting\": \"melhorar a escrita\",\n      \"smartEditMakeLonger\": \"Faça mais\",\n      \"smartEditCouldNotFetchResult\": \"Não foi possível obter o resultado do AI\",\n      \"smartEditCouldNotFetchKey\": \"Não foi possível obter a chave AI\",\n      \"smartEditDisabled\": \"Conecte AI em Configurações\",\n      \"discardResponse\": \"Deseja descartar as respostas de IA?\",\n      \"createInlineMathEquation\": \"Criar equação\",\n      \"toggleList\": \"Alternar lista\",\n      \"cover\": {\n        \"changeCover\": \"Mudar capa\",\n        \"colors\": \"cores\",\n        \"images\": \"Imagens\",\n        \"clearAll\": \"Limpar tudo\",\n        \"abstract\": \"Abstrato\",\n        \"addCover\": \"Adicionar capa\",\n        \"addLocalImage\": \"Adicionar imagem local\",\n        \"invalidImageUrl\": \"URL de imagem inválido\",\n        \"failedToAddImageToGallery\": \"Falha ao adicionar imagem à galeria\",\n        \"enterImageUrl\": \"Insira o URL da imagem\",\n        \"add\": \"Adicionar\",\n        \"back\": \"Voltar\",\n        \"saveToGallery\": \"Salvar na galeria\",\n        \"removeIcon\": \"Remover ícone\",\n        \"pasteImageUrl\": \"Colar URL da imagem\",\n        \"or\": \"OU\",\n        \"pickFromFiles\": \"Escolha entre os arquivos\",\n        \"couldNotFetchImage\": \"Não foi possível obter a imagem\",\n        \"imageSavingFailed\": \"Falha ao salvar a imagem\",\n        \"addIcon\": \"Adicionar ícone\",\n        \"coverRemoveAlert\": \"Ele será removido da capa após ser excluído.\",\n        \"alertDialogConfirmation\": \"Você tem certeza que quer continuar?\"\n      },\n      \"mathEquation\": {\n        \"addMathEquation\": \"Adicionar equação matemática\",\n        \"editMathEquation\": \"Editar equação matemática\"\n      },\n      \"optionAction\": {\n        \"click\": \"Clique\",\n        \"toOpenMenu\": \" para abrir o menu\",\n        \"delete\": \"Excluir\",\n        \"duplicate\": \"Duplicado\",\n        \"turnInto\": \"Transformar-se em\",\n        \"moveUp\": \"Subir\",\n        \"moveDown\": \"Mover para baixo\",\n        \"color\": \"Cor\",\n        \"align\": \"Alinhar\",\n        \"left\": \"Esquerda\",\n        \"center\": \"Centro\",\n        \"right\": \"Certo\",\n        \"defaultColor\": \"Padrão\"\n      },\n      \"image\": {\n        \"addAnImage\": \"Adicione uma imagem\",\n        \"copiedToPasteBoard\": \"O link da imagem foi copiado para a área de transferência\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Adicione títulos para criar um sumário.\"\n      },\n      \"table\": {\n        \"addAfter\": \"Adicionar depois\",\n        \"addBefore\": \"Adicionar antes\",\n        \"delete\": \"Eliminar\",\n        \"clear\": \"Limpar conteúdo\",\n        \"duplicate\": \"Duplicado\",\n        \"bgColor\": \"Cor de fundo\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Cópia\",\n        \"cut\": \"Corte\",\n        \"paste\": \"Colar\"\n      }\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Digite '/' para comandos\"\n    },\n    \"title\": {\n      \"placeholder\": \"Sem título\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Clique para adicionar imagem\",\n      \"upload\": {\n        \"label\": \"Carregar\",\n        \"placeholder\": \"Clique para carregar a imagem\"\n      },\n      \"url\": {\n        \"label\": \"imagem URL\",\n        \"placeholder\": \"Insira o URL da imagem\"\n      },\n      \"ai\": {\n        \"label\": \"Gerar imagem da AI\",\n        \"placeholder\": \"Por favor, insira o comando para a AI gerar a imagem\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Gerar imagem da Stability AI\",\n        \"placeholder\": \"Por favor, insira o comando para a Stability AI gerar a imagem\"\n      },\n      \"support\": \"O limite de tamanho da imagem é de 5 MB. Formatos suportados: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"imagem inválida\",\n        \"invalidImageSize\": \"O tamanho da imagem deve ser inferior a 5 MB\",\n        \"invalidImageFormat\": \"O formato da imagem não é suportado. Formatos suportados: JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"URL de imagem inválido\"\n      },\n      \"embedLink\": {\n        \"label\": \"Incorporar hiperligação\",\n        \"placeholder\": \"Cole ou digite uma hiperligação de imagem\"\n      },\n      \"searchForAnImage\": \"Procure uma imagem\",\n      \"pleaseInputYourOpenAIKey\": \"por favor, insira a sua chave AI na página Configurações\",\n      \"pleaseInputYourStabilityAIKey\": \"por favor, insira a sua chave Stability AI na página Configurações\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Linguagem\",\n        \"placeholder\": \"Selecione o idioma\"\n      }\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Cole ou digite um link\",\n      \"openInNewTab\": \"Abrir numa nova aba\",\n      \"copyLink\": \"Cópia do link\",\n      \"removeLink\": \"Remover link\",\n      \"url\": {\n        \"label\": \"URL do link\",\n        \"placeholder\": \"Insira o URL do link\"\n      },\n      \"title\": {\n        \"label\": \"Título do Link\",\n        \"placeholder\": \"Insira o título do link\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"Mencione uma pessoa, uma página ou uma data...\",\n      \"page\": {\n        \"label\": \"Link para a página\",\n        \"tooltip\": \"Clique para abrir a página\"\n      }\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"Restaurar para o padrão\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"A versão atual não suporta este bloco.\",\n      \"blockContentHasBeenCopied\": \"O conteúdo do bloco foi copiado.\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"Novo\"\n    },\n    \"menuName\": \"Quadro\",\n    \"referencedBoardPrefix\": \"Vista de\",\n    \"mobile\": {\n      \"showGroup\": \"Mostrar grupo\",\n      \"showGroupContent\": \"Tem certeza de que deseja mostrar este grupo no quadro?\",\n      \"failedToLoad\": \"Falha ao carregar a visualização do quadro\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Calendário\",\n    \"defaultNewCalendarTitle\": \"Sem título\",\n    \"newEventButtonTooltip\": \"Adicionar um novo evento\",\n    \"navigation\": {\n      \"today\": \"Hoje\",\n      \"jumpToday\": \"Ir para hoje\",\n      \"previousMonth\": \"Mês anterior\",\n      \"nextMonth\": \"Próximo mês\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Mostrar números da semana\",\n      \"showWeekends\": \"Mostrar fins de semana\",\n      \"firstDayOfWeek\": \"Comece a semana em\",\n      \"layoutDateField\": \"Calendário de layout por\",\n      \"noDateTitle\": \"sem data\",\n      \"clickToAdd\": \"Clique para adicionar ao calendário\",\n      \"name\": \"Layout do calendário\",\n      \"noDateHint\": \"Eventos não agendados aparecerão aqui\"\n    },\n    \"referencedCalendarPrefix\": \"Vista de\"\n  },\n  \"errorDialog\": {\n    \"title\": \"Erro do @:appName\",\n    \"howToFixFallback\": \"Lamentamos o inconveniente! Envie um problema em nossa página do GitHub que descreva seu erro.\",\n    \"github\": \"Ver no GitHub\"\n  },\n  \"search\": {\n    \"label\": \"Procurar\",\n    \"placeholder\": {\n      \"actions\": \"Pesquisar ações...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Copiado!\",\n      \"fail\": \"Não foi possível copiar\"\n    }\n  },\n  \"unSupportBlock\": \"A versão atual não suporta este bloco.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Tem certeza de que deseja excluir {pageType}?\",\n    \"deleteContentCaption\": \"se você excluir este {pageType}, poderá restaurá-lo da lixeira.\"\n  },\n  \"colors\": {\n    \"custom\": \"Personalizado\",\n    \"default\": \"Padrão\",\n    \"red\": \"Vermelho\",\n    \"orange\": \"Laranja\",\n    \"yellow\": \"Amarelo\",\n    \"green\": \"Verde\",\n    \"blue\": \"Azul\",\n    \"purple\": \"Roxo\",\n    \"pink\": \"Rosa\",\n    \"brown\": \"Castanho\",\n    \"gray\": \"Cinzento\"\n  },\n  \"emoji\": {\n    \"search\": \"Pesquisar emoji\",\n    \"noRecent\": \"Nenhum emoji recente\",\n    \"noEmojiFound\": \"Nenhum emoji encontrado\",\n    \"filter\": \"Filtro\",\n    \"random\": \"Aleatório\",\n    \"selectSkinTone\": \"Selecione o tom do tema\",\n    \"remove\": \"Remover emoji\",\n    \"categories\": {\n      \"smileys\": \"Smileys e Emoções\",\n      \"people\": \"Pessoas e Corpo\",\n      \"animals\": \"Animais e Natureza\",\n      \"food\": \"Comida e bebida\",\n      \"activities\": \"Atividades\",\n      \"places\": \"Viagens e lugares\",\n      \"objects\": \"Objetos\",\n      \"symbols\": \"Símbolos\",\n      \"flags\": \"Bandeiras\",\n      \"nature\": \"Natureza\",\n      \"frequentlyUsed\": \"Usado frequentemente\"\n    }\n  },\n  \"inlineActions\": {\n    \"noResults\": \"Nenhum resultado\",\n    \"pageReference\": \"Referência de página\",\n    \"date\": \"Data\",\n    \"reminder\": {\n      \"groupTitle\": \"Lembrete\",\n      \"shortKeyword\": \"lembrete\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"Ontem\",\n    \"today\": \"Hoje\",\n    \"tomorrow\": \"Amanhã\",\n    \"oneWeek\": \"1 semana\"\n  },\n  \"notificationHub\": {\n    \"title\": \"Notificações\",\n    \"tabs\": {\n      \"inbox\": \"Caixa de entrada\",\n      \"upcoming\": \"A chegar\"\n    },\n    \"filters\": {\n      \"ascending\": \"Ascendente\",\n      \"descending\": \"Descendente\",\n      \"groupByDate\": \"Agrupar por data\",\n      \"showUnreadsOnly\": \"Mostrar apenas os não lidos\",\n      \"resetToDefault\": \"Restaurar para o padrão\"\n    },\n    \"empty\": \"Nada para ver aqui!\"\n  },\n  \"reminderNotification\": {\n    \"title\": \"Lembrete\",\n    \"message\": \"Lembre-se de verificar isto antes que esqueça!\",\n    \"tooltipDelete\": \"Apagar\",\n    \"tooltipMarkRead\": \"Marcar como lido\",\n    \"tooltipMarkUnread\": \"Marcar como não lido\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"Encontrar\",\n    \"previousMatch\": \"Correspondência anterior\",\n    \"nextMatch\": \"Correspondência seguinte\",\n    \"close\": \"Fechar\",\n    \"replace\": \"Substituir\",\n    \"replaceAll\": \"Substituir tudo\",\n    \"noResult\": \"Nenhum resultado\",\n    \"caseSensitive\": \"Maiúsculas e minúsculas\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/ru-RU.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Я\",\n  \"welcomeText\": \"Добро пожаловать в @:appName\",\n  \"welcomeTo\": \"Добро пожаловать в\",\n  \"githubStarText\": \"Поставить звезду на GitHub\",\n  \"subscribeNewsletterText\": \"Подписаться на рассылку\",\n  \"letsGoButtonText\": \"Быстрый старт\",\n  \"title\": \"Заголовок\",\n  \"youCanAlso\": \"Вы также можете\",\n  \"and\": \"и\",\n  \"failedToOpenUrl\": \"Не удалось открыть URL: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Нажмите, чтобы добавить ниже\",\n    \"addAboveCmd\": \"Alt+клик\",\n    \"addAboveMacCmd\": \"Option+клик\",\n    \"addAboveTooltip\": \"чтобы добавить выше\",\n    \"dragTooltip\": \"Перетащите, чтобы переместить\",\n    \"openMenuTooltip\": \"Нажмите, чтобы открыть меню\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Зарегистрироваться\",\n    \"title\": \"Зарегистрироваться в @:appName\",\n    \"getStartedText\": \"Начать\",\n    \"emptyPasswordError\": \"Пароль не может быть пустым\",\n    \"repeatPasswordEmptyError\": \"Повторный пароль не может быть пустым\",\n    \"unmatchedPasswordError\": \"Повторный пароль не совпадает с паролем\",\n    \"alreadyHaveAnAccount\": \"Уже есть аккаунт?\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Пароль\",\n    \"repeatPasswordHint\": \"Повторить пароль\",\n    \"signUpWith\": \"Зарегистрироваться с помощью:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Войти в @:appName\",\n    \"loginButtonText\": \"Войти\",\n    \"loginStartWithAnonymous\": \"Продолжить в анонимной сессии\",\n    \"continueAnonymousUser\": \"Продолжить в анонимной сессии\",\n    \"continueWithLocalModel\": \"Продолжить с локальной моделью\",\n    \"switchToAppFlowyCloud\": \"AppFlowy Cloud\",\n    \"anonymousMode\": \"Анонимный режим\",\n    \"buttonText\": \"Войти\",\n    \"signingInText\": \"Выполняется вход...\",\n    \"forgotPassword\": \"Забыли пароль?\",\n    \"emailHint\": \"Email\",\n    \"passwordHint\": \"Пароль\",\n    \"dontHaveAnAccount\": \"Нет аккаунта?\",\n    \"createAccount\": \"Создать аккаунт\",\n    \"repeatPasswordEmptyError\": \"Повторный пароль не может быть пустым\",\n    \"unmatchedPasswordError\": \"Повторный пароль не совпадает с паролем\",\n    \"syncPromptMessage\": \"Синхронизация данных может занять некоторое время. Пожалуйста, не закрывайте эту страницу\",\n    \"or\": \"или\",\n    \"signInWithGoogle\": \"Продолжить с Google\",\n    \"signInWithGithub\": \"Продолжить с GitHub\",\n    \"signInWithDiscord\": \"Продолжить с Discord\",\n    \"signInWithApple\": \"Продолжить с Apple\",\n    \"continueAnotherWay\": \"Продолжить другим способом\",\n    \"signUpWithGoogle\": \"Зарегистрироваться с Google\",\n    \"signUpWithGithub\": \"Зарегистрироваться с Github\",\n    \"signUpWithDiscord\": \"Зарегистрироваться с Discord\",\n    \"signInWith\": \"Продолжить с помощью:\",\n    \"signInWithEmail\": \"Продолжить с Email\",\n    \"signInWithMagicLink\": \"Продолжить\",\n    \"signUpWithMagicLink\": \"Зарегистрироваться с Magic Link\",\n    \"pleaseInputYourEmail\": \"Пожалуйста, введите ваш адрес электронной почты\",\n    \"settings\": \"Настройки\",\n    \"magicLinkSent\": \"Magic Link отправлен!\",\n    \"invalidEmail\": \"Пожалуйста, введите действительный адрес электронной почты\",\n    \"alreadyHaveAnAccount\": \"Уже есть аккаунт?\",\n    \"logIn\": \"Войти\",\n    \"generalError\": \"Что-то пошло не так. Пожалуйста, попробуйте позже\",\n    \"limitRateError\": \"По соображениям безопасности вы можете запрашивать magic link только раз в 60 секунд\",\n    \"magicLinkSentDescription\": \"Magic Link был отправлен на вашу электронную почту. Нажмите на ссылку, чтобы завершить вход. Ссылка истекает через 5 минут.\",\n    \"tokenHasExpiredOrInvalid\": \"Код истек или недействителен. Пожалуйста, попробуйте снова.\",\n    \"signingIn\": \"Выполняется вход...\",\n    \"checkYourEmail\": \"Проверьте свою почту\",\n    \"temporaryVerificationLinkSent\": \"Временная ссылка для верификации отправлена.\\nПожалуйста, проверьте свой почтовый ящик по адресу\",\n    \"temporaryVerificationCodeSent\": \"Временный код верификации отправлен.\\nПожалуйста, проверьте свой почтовый ящик по адресу\",\n    \"continueToSignIn\": \"Продолжить вход\",\n    \"backToLogin\": \"Назад ко входу\",\n    \"enterCode\": \"Введите код\",\n    \"enterCodeManually\": \"Ввести код вручную\",\n    \"continueWithEmail\": \"Продолжить с email\",\n    \"enterPassword\": \"Введите пароль\",\n    \"loginAs\": \"Войти как\",\n    \"invalidVerificationCode\": \"Пожалуйста, введите действительный код верификации\",\n    \"tooFrequentVerificationCodeRequest\": \"Вы сделали слишком много запросов. Пожалуйста, попробуйте позже.\",\n    \"invalidLoginCredentials\": \"Ваш пароль неверный, пожалуйста, попробуйте снова\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Выберите ваше рабочее пространство\",\n    \"defaultName\": \"Мое рабочее пространство\",\n    \"create\": \"Создать рабочее пространство\",\n    \"new\": \"Новое рабочее пространство\",\n    \"importFromNotion\": \"Импорт из Notion\",\n    \"learnMore\": \"Узнать больше\",\n    \"reset\": \"Сбросить рабочее пространство\",\n    \"renameWorkspace\": \"Переименовать рабочее пространство\",\n    \"workspaceNameCannotBeEmpty\": \"Имя рабочего пространства не может быть пустым\",\n    \"resetWorkspacePrompt\": \"Сброс рабочего пространства приведет к удалению всех страниц и данных в нем. Вы уверены, что хотите сбросить рабочее пространство? Как вариант, вы можете связаться со службой поддержки для восстановления рабочего пространства\",\n    \"hint\": \"рабочее пространство\",\n    \"notFoundError\": \"Рабочее пространство не найдено\",\n    \"failedToLoad\": \"Что-то пошло не так! Не удалось загрузить рабочее пространство. Попробуйте закрыть все открытые экземпляры @:appName и повторить попытку.\",\n    \"errorActions\": {\n      \"reportIssue\": \"Сообщить о проблеме\",\n      \"reportIssueOnGithub\": \"Сообщить о проблеме на Github\",\n      \"exportLogFiles\": \"Экспортировать файлы логов\",\n      \"reachOut\": \"Связаться через Discord\"\n    },\n    \"menuTitle\": \"Рабочие пространства\",\n    \"deleteWorkspaceHintText\": \"Вы уверены, что хотите удалить рабочее пространство? Это действие нельзя отменить, и все опубликованные вами страницы будут сняты с публикации.\",\n    \"createSuccess\": \"Рабочее пространство успешно создано\",\n    \"createFailed\": \"Не удалось создать рабочее пространство\",\n    \"createLimitExceeded\": \"Вы достигли максимального лимита рабочих пространств для вашей учетной записи. Если вам нужно больше рабочих пространств для продолжения работы, пожалуйста, запросите это на Github\",\n    \"deleteSuccess\": \"Рабочее пространство успешно удалено\",\n    \"deleteFailed\": \"Не удалось удалить рабочее пространство\",\n    \"openSuccess\": \"Рабочее пространство успешно открыто\",\n    \"openFailed\": \"Не удалось открыть рабочее пространство\",\n    \"renameSuccess\": \"Рабочее пространство успешно переименовано\",\n    \"renameFailed\": \"Не удалось переименовать рабочее пространство\",\n    \"updateIconSuccess\": \"Иконка рабочего пространства успешно обновлена\",\n    \"updateIconFailed\": \"Не удалось обновить иконку рабочего пространства\",\n    \"cannotDeleteTheOnlyWorkspace\": \"Нельзя удалить единственное рабочее пространство\",\n    \"fetchWorkspacesFailed\": \"Не удалось получить список рабочих пространств\",\n    \"leaveCurrentWorkspace\": \"Покинуть рабочее пространство\",\n    \"leaveCurrentWorkspacePrompt\": \"Вы уверены, что хотите покинуть текущее рабочее пространство?\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Поделиться\",\n    \"workInProgress\": \"Скоро будет доступно\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"Скопировать в буфер обмена\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Скопировать ссылку\",\n    \"publishToTheWeb\": \"Опубликовать в Интернете\",\n    \"publishToTheWebHint\": \"Создать сайт с помощью AppFlowy\",\n    \"publish\": \"Опубликовать\",\n    \"unPublish\": \"Снять с публикации\",\n    \"visitSite\": \"Посетить сайт\",\n    \"exportAsTab\": \"Экспортировать как\",\n    \"publishTab\": \"Опубликовать\",\n    \"shareTab\": \"Поделиться\",\n    \"publishOnAppFlowy\": \"Опубликовать на AppFlowy\",\n    \"shareTabTitle\": \"Пригласить к совместной работе\",\n    \"shareTabDescription\": \"Для простой совместной работы с кем угодно\",\n    \"copyLinkSuccess\": \"Ссылка скопирована в буфер обмена\",\n    \"copyShareLink\": \"Скопировать ссылку для доступа\",\n    \"copyLinkFailed\": \"Не удалось скопировать ссылку в буфер обмена\",\n    \"copyLinkToBlockSuccess\": \"Ссылка на блок скопирована в буфер обмена\",\n    \"copyLinkToBlockFailed\": \"Не удалось скопировать ссылку на блок в буфер обмена\",\n    \"manageAllSites\": \"Управление всеми сайтами\",\n    \"updatePathName\": \"Обновить имя пути\"\n  },\n  \"moreAction\": {\n    \"small\": \"маленький\",\n    \"medium\": \"средний\",\n    \"large\": \"большой\",\n    \"fontSize\": \"Размер шрифта\",\n    \"import\": \"Импорт\",\n    \"moreOptions\": \"Больше опций\",\n    \"wordCount\": \"Количество слов: {}\",\n    \"charCount\": \"Количество символов: {}\",\n    \"createdAt\": \"Создано: {}\",\n    \"deleteView\": \"Удалить\",\n    \"duplicateView\": \"Дублировать\",\n    \"wordCountLabel\": \"Количество слов: \",\n    \"charCountLabel\": \"Количество символов: \",\n    \"createdAtLabel\": \"Создано: \",\n    \"syncedAtLabel\": \"Синхронизировано: \",\n    \"saveAsNewPage\": \"Добавить сообщения на страницу\",\n    \"saveAsNewPageDisabled\": \"Нет доступных сообщений\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Текст и Markdown\",\n    \"documentFromV010\": \"Документ из v0.1.0\",\n    \"databaseFromV010\": \"База данных из v0.1.0\",\n    \"notionZip\": \"Экспортированный Zip-файл Notion\",\n    \"csv\": \"CSV\",\n    \"database\": \"База данных\"\n  },\n  \"emojiIconPicker\": {\n    \"iconUploader\": {\n      \"placeholderLeft\": \"Перетащите файл, нажмите, чтобы \",\n      \"placeholderUpload\": \"Загрузить\",\n      \"placeholderRight\": \", или вставьте ссылку на изображение.\",\n      \"dropToUpload\": \"Перетащите файл для загрузки\",\n      \"change\": \"Изменить\"\n    }\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Переименовать\",\n    \"delete\": \"Удалить\",\n    \"duplicate\": \"Дублировать\",\n    \"unfavorite\": \"Удалить из избранного\",\n    \"favorite\": \"Добавить в избранное\",\n    \"openNewTab\": \"Открыть в новой вкладке\",\n    \"moveTo\": \"Переместить в\",\n    \"addToFavorites\": \"Добавить в избранное\",\n    \"copyLink\": \"Скопировать ссылку\",\n    \"changeIcon\": \"Изменить иконку\",\n    \"collapseAllPages\": \"Свернуть все подстраницы\",\n    \"movePageTo\": \"Переместить страницу в\",\n    \"move\": \"Переместить\",\n    \"lockPage\": \"Заблокировать страницу\"\n  },\n  \"blankPageTitle\": \"Пустая страница\",\n  \"newPageText\": \"Новая страница\",\n  \"newDocumentText\": \"Новый документ\",\n  \"newGridText\": \"Новая таблица\",\n  \"newCalendarText\": \"Новый календарь\",\n  \"newBoardText\": \"Новая доска\",\n  \"chat\": {\n    \"newChat\": \"AI Чат\",\n    \"inputMessageHint\": \"Спросить AI @:appName\",\n    \"inputLocalAIMessageHint\": \"Спросить локальный AI @:appName\",\n    \"unsupportedCloudPrompt\": \"Эта функция доступна только при использовании AppFlowy Cloud\",\n    \"relatedQuestion\": \"Предложенные\",\n    \"serverUnavailable\": \"Соединение потеряно. Пожалуйста, проверьте интернет и\",\n    \"aiServerUnavailable\": \"Служба AI временно недоступна. Пожалуйста, попробуйте позже.\",\n    \"retry\": \"Повторить\",\n    \"clickToRetry\": \"Нажмите, чтобы повторить\",\n    \"regenerateAnswer\": \"Перегенерировать\",\n    \"question1\": \"Как использовать Kanban для управления задачами\",\n    \"question2\": \"Объясните метод GTD\",\n    \"question3\": \"Зачем использовать Rust\",\n    \"question4\": \"Рецепт из того, что есть на кухне\",\n    \"question5\": \"Создать иллюстрацию для моей страницы\",\n    \"question6\": \"Составить список дел на следующую неделю\",\n    \"aiMistakePrompt\": \"AI может совершать ошибки. Проверяйте важную информацию.\",\n    \"chatWithFilePrompt\": \"Хотите пообщаться с файлом?\",\n    \"indexFileSuccess\": \"Файл успешно проиндексирован\",\n    \"inputActionNoPages\": \"Нет результатов по страницам\",\n    \"referenceSource\": {\n      \"zero\": \"Найдено 0 источников\",\n      \"one\": \"Найден {count} источник\",\n      \"other\": \"Найдено {count} источников\"\n    },\n    \"clickToMention\": \"Упомянуть страницу\",\n    \"uploadFile\": \"Прикрепить PDF, текстовые файлы или файлы markdown\",\n    \"questionDetail\": \"Привет, {}! Чем могу помочь сегодня?\",\n    \"indexingFile\": \"Индексация {}\",\n    \"generatingResponse\": \"Генерация ответа\",\n    \"selectSources\": \"Выбрать источники\",\n    \"currentPage\": \"Текущая страница\",\n    \"sourcesLimitReached\": \"Вы можете выбрать до 3 документов верхнего уровня и их дочерние элементы\",\n    \"sourceUnsupported\": \"Мы пока не поддерживаем общение с базами данных\",\n    \"regenerate\": \"Попробовать снова\",\n    \"addToPageButton\": \"Добавить сообщение на страницу\",\n    \"addToPageTitle\": \"Добавить сообщение в...\",\n    \"addToNewPage\": \"Создать новую страницу\",\n    \"addToNewPageName\": \"Сообщения, извлеченные из \\\"{}\\\"\",\n    \"addToNewPageSuccessToast\": \"Сообщение добавлено на\",\n    \"openPagePreviewFailedToast\": \"Не удалось открыть страницу\",\n    \"changeFormat\": {\n      \"actionButton\": \"Изменить формат\",\n      \"confirmButton\": \"Перегенерировать в этом формате\",\n      \"textOnly\": \"Только текст\",\n      \"imageOnly\": \"Только изображение\",\n      \"textAndImage\": \"Текст и изображение\",\n      \"text\": \"Параграф\",\n      \"bullet\": \"Маркированный список\",\n      \"number\": \"Нумерованный список\",\n      \"table\": \"Таблица\",\n      \"blankDescription\": \"Формат ответа\",\n      \"defaultDescription\": \"Автоматический формат ответа\",\n      \"textWithImageDescription\": \"@:chat.changeFormat.text с изображением\",\n      \"numberWithImageDescription\": \"@:chat.changeFormat.number с изображением\",\n      \"bulletWithImageDescription\": \"@:chat.changeFormat.bullet с изображением\",\n      \"tableWithImageDescription\": \"@:chat.changeFormat.table с изображением\"\n    },\n    \"switchModel\": {\n      \"label\": \"Сменить модель\",\n      \"localModel\": \"Локальная модель\",\n      \"cloudModel\": \"Облачная модель\",\n      \"autoModel\": \"Авто\"\n    },\n    \"selectBanner\": {\n      \"saveButton\": \"Добавить в…\",\n      \"selectMessages\": \"Выбрать сообщения\",\n      \"nSelected\": \"Выбрано {}\",\n      \"allSelected\": \"Все выбраны\"\n    },\n    \"stopTooltip\": \"Остановить генерацию\"\n  },\n  \"trash\": {\n    \"text\": \"Корзина\",\n    \"restoreAll\": \"Восстановить все\",\n    \"restore\": \"Восстановить\",\n    \"deleteAll\": \"Удалить все\",\n    \"pageHeader\": {\n      \"fileName\": \"Имя файла\",\n      \"lastModified\": \"Последнее изменение\",\n      \"created\": \"Создано\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Все страницы в корзине\",\n      \"caption\": \"Вы уверены, что хотите удалить все в Корзине? Это действие нельзя отменить.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Восстановить все страницы в корзине\",\n      \"caption\": \"Это действие нельзя отменить.\"\n    },\n    \"restorePage\": {\n      \"title\": \"Восстановить: {}\",\n      \"caption\": \"Вы уверены, что хотите восстановить эту страницу?\"\n    },\n    \"mobile\": {\n      \"actions\": \"Действия с корзиной\",\n      \"empty\": \"Нет страниц или пространств в Корзине\",\n      \"emptyDescription\": \"Перемещайте ненужные вещи в Корзину.\",\n      \"isDeleted\": \"удалено\",\n      \"isRestored\": \"восстановлено\"\n    },\n    \"confirmDeleteTitle\": \"Вы уверены, что хотите удалить эту страницу навсегда?\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Эта страница в Корзине\",\n    \"restore\": \"Восстановить страницу\",\n    \"deletePermanent\": \"Удалить навсегда\",\n    \"deletePermanentDescription\": \"Вы уверены, что хотите удалить эту страницу навсегда? Это необратимо.\"\n  },\n  \"dialogCreatePageNameHint\": \"Название страницы\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Быстрые клавиши\",\n    \"whatsNew\": \"Что нового?\",\n    \"helpAndDocumentation\": \"Помощь и документация\",\n    \"getSupport\": \"Получить поддержку\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"Отладочная информация\",\n      \"success\": \"Отладочная информация скопирована в буфер обмена!\",\n      \"fail\": \"Не удалось скопировать отладочную информацию в буфер обмена\"\n    },\n    \"feedback\": \"Обратная связь\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Удалить, переименовать и другое...\",\n    \"addPageTooltip\": \"Быстро добавить страницу внутри\",\n    \"defaultNewPageName\": \"Без названия\",\n    \"renameDialog\": \"Переименовать\",\n    \"pageNameSuffix\": \"Копия\"\n  },\n  \"noPagesInside\": \"Нет страниц внутри\",\n  \"toolbar\": {\n    \"undo\": \"Отменить\",\n    \"redo\": \"Повторить\",\n    \"bold\": \"Жирный\",\n    \"italic\": \"Курсив\",\n    \"underline\": \"Подчеркнутый\",\n    \"strike\": \"Зачеркнутый\",\n    \"numList\": \"Нумерованный список\",\n    \"bulletList\": \"Маркированный список\",\n    \"checkList\": \"Список задач\",\n    \"inlineCode\": \"Встроенный код\",\n    \"quote\": \"Блок цитаты\",\n    \"header\": \"Заголовок\",\n    \"highlight\": \"Выделить\",\n    \"color\": \"Цвет\",\n    \"addLink\": \"Добавить ссылку\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Переключиться в светлый режим\",\n    \"darkMode\": \"Переключиться в темный режим\",\n    \"openAsPage\": \"Открыть как страницу\",\n    \"addNewRow\": \"Добавить новую строку\",\n    \"openMenu\": \"Нажмите, чтобы открыть меню\",\n    \"dragRow\": \"Перетащите, чтобы изменить порядок строк\",\n    \"viewDataBase\": \"Просмотр базы данных\",\n    \"referencePage\": \"На эту {name} есть ссылки\",\n    \"addBlockBelow\": \"Добавить блок ниже\",\n    \"aiGenerate\": \"Сгенерировать\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Закрыть боковую панель\",\n    \"openSidebar\": \"Открыть боковую панель\",\n    \"expandSidebar\": \"Развернуть на всю страницу\",\n    \"personal\": \"Личное\",\n    \"private\": \"Приватное\",\n    \"workspace\": \"Рабочее пространство\",\n    \"favorites\": \"Избранное\",\n    \"clickToHidePrivate\": \"Нажмите, чтобы скрыть приватное пространство\\nСтраницы, созданные здесь, видны только вам\",\n    \"clickToHideWorkspace\": \"Нажмите, чтобы скрыть рабочее пространство\\nСтраницы, созданные здесь, видны всем участникам\",\n    \"clickToHidePersonal\": \"Нажмите, чтобы скрыть личное пространство\",\n    \"clickToHideFavorites\": \"Нажмите, чтобы скрыть избранное\",\n    \"addAPage\": \"Добавить новую страницу\",\n    \"addAPageToPrivate\": \"Добавить страницу в приватное пространство\",\n    \"addAPageToWorkspace\": \"Добавить страницу в рабочее пространство\",\n    \"recent\": \"Последние\",\n    \"today\": \"Сегодня\",\n    \"thisWeek\": \"На этой неделе\",\n    \"others\": \"Более раннее избранное\",\n    \"earlier\": \"Раньше\",\n    \"justNow\": \"только что\",\n    \"minutesAgo\": \"{count} мин назад\",\n    \"lastViewed\": \"Последний просмотр\",\n    \"favoriteAt\": \"Добавлено в избранное\",\n    \"emptyRecent\": \"Нет последних страниц\",\n    \"emptyRecentDescription\": \"При просмотре страниц они будут появляться здесь для быстрого доступа.\",\n    \"emptyFavorite\": \"Нет избранных страниц\",\n    \"emptyFavoriteDescription\": \"Отмечайте страницы как избранные — они будут перечислены здесь для быстрого доступа!\",\n    \"removePageFromRecent\": \"Удалить эту страницу из последних?\",\n    \"removeSuccess\": \"Успешно удалено\",\n    \"favoriteSpace\": \"Избранное\",\n    \"RecentSpace\": \"Последние\",\n    \"Spaces\": \"Пространства\",\n    \"upgradeToPro\": \"Обновиться до Pro\",\n    \"upgradeToAIMax\": \"Разблокировать неограниченный AI\",\n    \"storageLimitDialogTitle\": \"У вас закончилось свободное хранилище. Обновитесь, чтобы разблокировать неограниченное хранилище\",\n    \"storageLimitDialogTitleIOS\": \"У вас закончилось свободное хранилище.\",\n    \"aiResponseLimitTitle\": \"У вас закончились бесплатные ответы AI. Обновитесь до плана Pro или купите дополнение AI, чтобы разблокировать неограниченные ответы\",\n    \"aiResponseLimitDialogTitle\": \"Достигнут лимит ответов AI\",\n    \"aiResponseLimit\": \"У вас закончились бесплатные ответы AI.\\n\\nПерейдите в Настройки -> План -> Нажмите AI Max или план Pro, чтобы получить больше ответов AI\",\n    \"askOwnerToUpgradeToPro\": \"В вашем рабочем пространстве заканчивается свободное хранилище. Пожалуйста, попросите владельца рабочего пространства обновиться до плана Pro\",\n    \"askOwnerToUpgradeToProIOS\": \"В вашем рабочем пространстве заканчивается свободное хранилище.\",\n    \"askOwnerToUpgradeToAIMax\": \"В вашем рабочем пространстве закончились бесплатные ответы AI. Пожалуйста, попросите владельца рабочего пространства обновить план или приобрести дополнения AI\",\n    \"askOwnerToUpgradeToAIMaxIOS\": \"В вашем рабочем пространстве заканчиваются бесплатные ответы AI.\",\n    \"purchaseAIMax\": \"В вашем рабочем пространстве закончились ответы AI Image. Пожалуйста, попросите владельца рабочего пространства приобрести AI Max\",\n    \"aiImageResponseLimit\": \"У вас закончились ответы AI image.\\n\\nПерейдите в Настройки -> План -> Нажмите AI Max, чтобы получить больше ответов AI image\",\n    \"purchaseStorageSpace\": \"Купить место для хранения\",\n    \"singleFileProPlanLimitationDescription\": \"Вы превысили максимальный размер файла, разрешенный в бесплатном плане. Пожалуйста, обновитесь до плана Pro, чтобы загружать файлы большего размера\",\n    \"purchaseAIResponse\": \"Купить\",\n    \"askOwnerToUpgradeToLocalAI\": \"Попросите владельца рабочего пространства включить AI на устройстве\",\n    \"upgradeToAILocal\": \"Запускайте локальные модели на вашем устройстве для максимальной конфиденциальности\",\n    \"upgradeToAILocalDesc\": \"Общайтесь с PDF, улучшайте свое письмо и автоматически заполняйте таблицы с помощью локального AI\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Заметка экспортирована в Markdown\",\n      \"path\": \"Документы/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Контакты\",\n    \"whatsHappening\": \"Что происходит на этой неделе?\",\n    \"addContact\": \"Добавить контакт\",\n    \"editContact\": \"Редактировать контакт\"\n  },\n  \"button\": {\n    \"ok\": \"Ок\",\n    \"confirm\": \"Подтвердить\",\n    \"done\": \"Готово\",\n    \"cancel\": \"Отмена\",\n    \"signIn\": \"Войти\",\n    \"signOut\": \"Выйти\",\n    \"complete\": \"Готово\",\n    \"save\": \"Сохранить\",\n    \"generate\": \"Сгенерировать\",\n    \"esc\": \"ESC\",\n    \"keep\": \"Сохранить\",\n    \"tryAgain\": \"Попробовать снова\",\n    \"discard\": \"Отменить\",\n    \"replace\": \"Заменить\",\n    \"insertBelow\": \"Вставить ниже\",\n    \"insertAbove\": \"Вставить выше\",\n    \"upload\": \"Загрузить\",\n    \"edit\": \"Редактировать\",\n    \"delete\": \"Удалить\",\n    \"copy\": \"Копировать\",\n    \"duplicate\": \"Дублировать\",\n    \"putback\": \"Вернуть\",\n    \"update\": \"Обновить\",\n    \"share\": \"Поделиться\",\n    \"removeFromFavorites\": \"Удалить из избранного\",\n    \"removeFromRecent\": \"Удалить из последних\",\n    \"addToFavorites\": \"Добавить в избранное\",\n    \"favoriteSuccessfully\": \"Успешно добавлено в избранное\",\n    \"unfavoriteSuccessfully\": \"Успешно удалено из избранного\",\n    \"duplicateSuccessfully\": \"Успешно дублировано\",\n    \"rename\": \"Переименовать\",\n    \"helpCenter\": \"Справочный центр\",\n    \"add\": \"Добавить\",\n    \"yes\": \"Да\",\n    \"no\": \"Нет\",\n    \"clear\": \"Очистить\",\n    \"remove\": \"Удалить\",\n    \"dontRemove\": \"Не удалять\",\n    \"copyLink\": \"Скопировать ссылку\",\n    \"align\": \"Выровнять\",\n    \"login\": \"Вход\",\n    \"logout\": \"Выход\",\n    \"deleteAccount\": \"Удалить аккаунт\",\n    \"back\": \"Назад\",\n    \"signInGoogle\": \"Продолжить с Google\",\n    \"signInGithub\": \"Продолжить с GitHub\",\n    \"signInDiscord\": \"Продолжить с Discord\",\n    \"more\": \"Еще\",\n    \"create\": \"Создать\",\n    \"close\": \"Закрыть\",\n    \"next\": \"Далее\",\n    \"previous\": \"Назад\",\n    \"submit\": \"Отправить\",\n    \"download\": \"Скачать\",\n    \"backToHome\": \"На главную\",\n    \"viewing\": \"Просмотр\",\n    \"editing\": \"Редактирование\",\n    \"gotIt\": \"Понял\",\n    \"retry\": \"Повторить\",\n    \"uploadFailed\": \"Загрузка не удалась.\",\n    \"copyLinkOriginal\": \"Скопировать ссылку на оригинал\"\n  },\n  \"label\": {\n    \"welcome\": \"Добро пожаловать!\",\n    \"firstName\": \"Имя\",\n    \"middleName\": \"Отчество\",\n    \"lastName\": \"Фамилия\",\n    \"stepX\": \"Шаг {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Не удалось подключиться к вашей учетной записи.\",\n      \"failedMsg\": \"Пожалуйста, убедитесь, что вы завершили процесс входа в браузере.\"\n    },\n    \"google\": {\n      \"title\": \"ВХОД ЧЕРЕЗ GOOGLE\",\n      \"instruction1\": \"Для импорта контактов Google вам необходимо авторизовать это приложение через веб-браузер.\",\n      \"instruction2\": \"Скопируйте этот код в буфер обмена, нажав на значок или выделив текст:\",\n      \"instruction3\": \"Перейдите по следующей ссылке в браузере и введите указанный выше код:\",\n      \"instruction4\": \"Нажмите кнопку ниже, когда завершите регистрацию:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Настройки\",\n    \"popupMenuItem\": {\n      \"settings\": \"Настройки\",\n      \"members\": \"Участники\",\n      \"trash\": \"Корзина\",\n      \"helpAndDocumentation\": \"Помощь и документация\",\n      \"getSupport\": \"Получить поддержку\"\n    },\n    \"sites\": {\n      \"title\": \"Сайты\",\n      \"namespaceTitle\": \"Пространство имен\",\n      \"namespaceDescription\": \"Управляйте своим пространством имен и домашней страницей\",\n      \"namespaceHeader\": \"Пространство имен\",\n      \"homepageHeader\": \"Домашняя страница\",\n      \"updateNamespace\": \"Обновить пространство имен\",\n      \"removeHomepage\": \"Удалить домашнюю страницу\",\n      \"selectHomePage\": \"Выбрать страницу\",\n      \"clearHomePage\": \"Очистить домашнюю страницу для этого пространства имен\",\n      \"customUrl\": \"Пользовательский URL\",\n      \"namespace\": {\n        \"description\": \"Это изменение будет применено ко всем опубликованным страницам в этом пространстве имен\",\n        \"tooltip\": \"Мы оставляем за собой право удалять любые неприемлемые пространства имен\",\n        \"updateExistingNamespace\": \"Обновить существующее пространство имен\",\n        \"upgradeToPro\": \"Обновитесь до плана Pro, чтобы установить домашнюю страницу\",\n        \"redirectToPayment\": \"Перенаправление на страницу оплаты...\",\n        \"onlyWorkspaceOwnerCanSetHomePage\": \"Только владелец рабочего пространства может установить домашнюю страницу\",\n        \"pleaseAskOwnerToSetHomePage\": \"Пожалуйста, попросите владельца рабочего пространства обновиться до плана Pro\"\n      },\n      \"publishedPage\": {\n        \"title\": \"Все опубликованные страницы\",\n        \"description\": \"Управляйте своими опубликованными страницами\",\n        \"page\": \"Страница\",\n        \"pathName\": \"Имя пути\",\n        \"date\": \"Дата публикации\",\n        \"emptyHinText\": \"У вас нет опубликованных страниц в этом рабочем пространстве\",\n        \"noPublishedPages\": \"Нет опубликованных страниц\",\n        \"settings\": \"Настройки публикации\",\n        \"clickToOpenPageInApp\": \"Открыть страницу в приложении\",\n        \"clickToOpenPageInBrowser\": \"Открыть страницу в браузере\"\n      },\n      \"error\": {\n        \"failedToGeneratePaymentLink\": \"Не удалось сгенерировать ссылку для оплаты плана Pro\",\n        \"failedToUpdateNamespace\": \"Не удалось обновить пространство имен\",\n        \"proPlanLimitation\": \"Для обновления пространства имен вам нужно обновиться до плана Pro\",\n        \"namespaceAlreadyInUse\": \"Пространство имен уже используется, пожалуйста, попробуйте другое\",\n        \"invalidNamespace\": \"Недействительное пространство имен, пожалуйста, попробуйте другое\",\n        \"namespaceLengthAtLeast2Characters\": \"Длина пространства имен должна быть не менее 2 символов\",\n        \"onlyWorkspaceOwnerCanUpdateNamespace\": \"Только владелец рабочего пространства может обновить пространство имен\",\n        \"onlyWorkspaceOwnerCanRemoveHomepage\": \"Только владелец рабочего пространства может удалить домашнюю страницу\",\n        \"setHomepageFailed\": \"Не удалось установить домашнюю страницу\",\n        \"namespaceTooLong\": \"Имя пространства слишком длинное, пожалуйста, попробуйте другое\",\n        \"namespaceTooShort\": \"Имя пространства слишком короткое, пожалуйста, попробуйте другое\",\n        \"namespaceIsReserved\": \"Пространство имен зарезервировано, пожалуйста, попробуйте другое\",\n        \"updatePathNameFailed\": \"Не удалось обновить имя пути\",\n        \"removeHomePageFailed\": \"Не удалось удалить домашнюю страницу\",\n        \"publishNameContainsInvalidCharacters\": \"Имя пути содержит недопустимые символы, пожалуйста, попробуйте другое\",\n        \"publishNameTooShort\": \"Имя пути слишком короткое, пожалуйста, попробуйте другое\",\n        \"publishNameTooLong\": \"Имя пути слишком длинное, пожалуйста, попробуйте другое\",\n        \"publishNameAlreadyInUse\": \"Имя пути уже используется, пожалуйста, попробуйте другое\",\n        \"namespaceContainsInvalidCharacters\": \"Пространство имен содержит недопустимые символы, пожалуйста, попробуйте другое\",\n        \"publishPermissionDenied\": \"Только владелец рабочего пространства или издатель страницы может управлять настройками публикации\",\n        \"publishNameCannotBeEmpty\": \"Имя пути не может быть пустым, пожалуйста, попробуйте другое\"\n      },\n      \"success\": {\n        \"namespaceUpdated\": \"Пространство имен успешно обновлено\",\n        \"setHomepageSuccess\": \"Домашняя страница успешно установлена\",\n        \"updatePathNameSuccess\": \"Имя пути успешно обновлено\",\n        \"removeHomePageSuccess\": \"Домашняя страница успешно удалена\"\n      }\n    },\n    \"accountPage\": {\n      \"menuLabel\": \"Аккаунт и Приложение\",\n      \"title\": \"Мой аккаунт\",\n      \"general\": {\n        \"title\": \"Имя аккаунта и изображение профиля\",\n        \"changeProfilePicture\": \"Изменить фото профиля\"\n      },\n      \"email\": {\n        \"title\": \"Email\",\n        \"actions\": {\n          \"change\": \"Изменить email\"\n        }\n      },\n      \"login\": {\n        \"title\": \"Вход в аккаунт\",\n        \"loginLabel\": \"Войти\",\n        \"logoutLabel\": \"Выйти\"\n      },\n      \"isUpToDate\": \"@:appName обновлен!\",\n      \"officialVersion\": \"Версия {version} (Официальная сборка)\"\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"Рабочее пространство\",\n      \"title\": \"Рабочее пространство\",\n      \"description\": \"Настройте внешний вид рабочего пространства, тему, шрифт, макет текста, формат даты/времени и язык.\",\n      \"workspaceName\": {\n        \"title\": \"Название рабочего пространства\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"Иконка рабочего пространства\",\n        \"description\": \"Загрузите изображение или используйте эмодзи для вашего рабочего пространства. Иконка будет отображаться в боковой панели и уведомлениях.\"\n      },\n      \"appearance\": {\n        \"title\": \"Внешний вид\",\n        \"description\": \"Настройте внешний вид рабочего пространства, тему, шрифт, макет текста, дату, время и язык.\",\n        \"options\": {\n          \"system\": \"Авто\",\n          \"light\": \"Светлая\",\n          \"dark\": \"Темная\"\n        }\n      },\n      \"resetCursorColor\": {\n        \"title\": \"Сбросить цвет курсора документа\",\n        \"description\": \"Вы уверены, что хотите сбросить цвет курсора?\"\n      },\n      \"resetSelectionColor\": {\n        \"title\": \"Сбросить цвет выделения документа\",\n        \"description\": \"Вы уверены, что хотите сбросить цвет выделения?\"\n      },\n      \"resetWidth\": {\n        \"resetSuccess\": \"Ширина документа успешно сброшена\"\n      },\n      \"theme\": {\n        \"title\": \"Тема\",\n        \"description\": \"Выберите предустановленную тему или загрузите свою пользовательскую тему.\",\n        \"uploadCustomThemeTooltip\": \"Загрузить пользовательскую тему\",\n        \"failedToLoadThemes\": \"Не удалось загрузить темы, пожалуйста, проверьте настройки разрешений в Системные настройки > Конфиденциальность и безопасность > Файлы и папки > @:appName\"\n      },\n      \"workspaceFont\": {\n        \"title\": \"Шрифт рабочего пространства\",\n        \"noFontHint\": \"Шрифт не найден, попробуйте другой запрос.\"\n      },\n      \"textDirection\": {\n        \"title\": \"Направление текста\",\n        \"leftToRight\": \"Слева направо\",\n        \"rightToLeft\": \"Справа налево\",\n        \"auto\": \"Авто\",\n        \"enableRTLItems\": \"Включить элементы панели инструментов RTL\"\n      },\n      \"layoutDirection\": {\n        \"title\": \"Направление макета\",\n        \"leftToRight\": \"Слева направо\",\n        \"rightToLeft\": \"Справа налево\"\n      },\n      \"dateTime\": {\n        \"title\": \"Дата и время\",\n        \"example\": \"{} в {} ({})\",\n        \"24HourTime\": \"24-часовой формат\",\n        \"dateFormat\": {\n          \"label\": \"Формат даты\",\n          \"local\": \"Локальный\",\n          \"us\": \"США\",\n          \"iso\": \"ISO\",\n          \"friendly\": \"Удобный\",\n          \"dmy\": \"Д/М/Г\"\n        }\n      },\n      \"language\": {\n        \"title\": \"Язык\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"Удалить рабочее пространство\",\n        \"content\": \"Вы уверены, что хотите удалить это рабочее пространство? Это действие нельзя отменить, и все опубликованные вами страницы будут сняты с публикации.\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"Покинуть рабочее пространство\",\n        \"content\": \"Вы уверены, что хотите покинуть это рабочее пространство? Вы потеряете доступ ко всем страницам и данным в нем.\",\n        \"success\": \"Вы успешно покинули рабочее пространство.\",\n        \"fail\": \"Не удалось покинуть рабочее пространство.\"\n      },\n      \"manageWorkspace\": {\n        \"title\": \"Управление рабочим пространством\",\n        \"leaveWorkspace\": \"Покинуть рабочее пространство\",\n        \"deleteWorkspace\": \"Удалить рабочее пространство\"\n      }\n    },\n    \"manageDataPage\": {\n      \"menuLabel\": \"Управление данными\",\n      \"title\": \"Управление данными\",\n      \"description\": \"Управление локальным хранилищем данных или импорт существующих данных в @:appName.\",\n      \"dataStorage\": {\n        \"title\": \"Местоположение хранилища файлов\",\n        \"tooltip\": \"Место, где хранятся ваши файлы\",\n        \"actions\": {\n          \"change\": \"Изменить путь\",\n          \"open\": \"Открыть папку\",\n          \"openTooltip\": \"Открыть текущее местоположение папки с данными\",\n          \"copy\": \"Скопировать путь\",\n          \"copiedHint\": \"Путь скопирован!\",\n          \"resetTooltip\": \"Сбросить до местоположения по умолчанию\"\n        },\n        \"resetDialog\": {\n          \"title\": \"Вы уверены?\",\n          \"description\": \"Сброс пути к местоположению данных по умолчанию не приведет к удалению ваших данных. Если вы хотите повторно импортировать текущие данные, сначала скопируйте путь к текущему местоположению.\"\n        }\n      },\n      \"importData\": {\n        \"title\": \"Импорт данных\",\n        \"tooltip\": \"Импорт данных из резервных копий/папок данных @:appName\",\n        \"description\": \"Скопировать данные из внешней папки данных @:appName\",\n        \"action\": \"Обзор файлов\"\n      },\n      \"encryption\": {\n        \"title\": \"Шифрование\",\n        \"tooltip\": \"Управление способом хранения и шифрования ваших данных\",\n        \"descriptionNoEncryption\": \"Включение шифрования зашифрует все данные. Это нельзя отменить.\",\n        \"descriptionEncrypted\": \"Ваши данные зашифрованы.\",\n        \"action\": \"Зашифровать данные\",\n        \"dialog\": {\n          \"title\": \"Зашифровать все ваши данные?\",\n          \"description\": \"Шифрование всех ваших данных обеспечит их безопасность и защиту. Это действие НЕЛЬЗЯ отменить. Вы уверены, что хотите продолжить?\"\n        }\n      },\n      \"cache\": {\n        \"title\": \"Очистить кеш\",\n        \"description\": \"Помогает решить проблемы, такие как незагружающиеся изображения, отсутствующие страницы в пространстве и незагружающиеся шрифты. Это не повлияет на ваши данные.\",\n        \"dialog\": {\n          \"title\": \"Очистить кеш\",\n          \"description\": \"Помогает решить проблемы, такие как незагружающиеся изображения, отсутствующие страницы в пространстве и незагружающиеся шрифты. Это не повлияет на ваши данные.\",\n          \"successHint\": \"Кеш очищен!\"\n        }\n      },\n      \"data\": {\n        \"fixYourData\": \"Исправить ваши данные\",\n        \"fixButton\": \"Исправить\",\n        \"fixYourDataDescription\": \"Если у вас возникли проблемы с данными, вы можете попробовать исправить их здесь.\"\n      }\n    },\n    \"shortcutsPage\": {\n      \"menuLabel\": \"Быстрые клавиши\",\n      \"title\": \"Быстрые клавиши\",\n      \"editBindingHint\": \"Введите новую привязку\",\n      \"searchHint\": \"Поиск\",\n      \"actions\": {\n        \"resetDefault\": \"Сбросить до значений по умолчанию\"\n      },\n      \"errorPage\": {\n        \"message\": \"Не удалось загрузить быстрые клавиши: {}\",\n        \"howToFix\": \"Попробуйте снова, если проблема сохраняется, пожалуйста, свяжитесь с нами на GitHub.\"\n      },\n      \"resetDialog\": {\n        \"title\": \"Сбросить быстрые клавиши\",\n        \"description\": \"Это сбросит все ваши привязки клавиш до значений по умолчанию, вы не сможете отменить это позже, вы уверены, что хотите продолжить?\",\n        \"buttonLabel\": \"Сбросить\"\n      },\n      \"conflictDialog\": {\n        \"title\": \"{} используется\",\n        \"descriptionPrefix\": \"Эта привязка клавиш в настоящее время используется \",\n        \"descriptionSuffix\": \". Если вы замените эту привязку, она будет удалена из {}.\",\n        \"confirmLabel\": \"Продолжить\"\n      },\n      \"editTooltip\": \"Нажмите, чтобы начать редактирование привязки клавиш\",\n      \"keybindings\": {\n        \"toggleToDoList\": \"Переключить список задач\",\n        \"insertNewParagraphInCodeblock\": \"Вставить новый абзац\",\n        \"pasteInCodeblock\": \"Вставить в блок кода\",\n        \"selectAllCodeblock\": \"Выбрать все\",\n        \"indentLineCodeblock\": \"Вставить два пробела в начале строки\",\n        \"outdentLineCodeblock\": \"Удалить два пробела в начале строки\",\n        \"twoSpacesCursorCodeblock\": \"Вставить два пробела у курсора\",\n        \"copy\": \"Копировать выделенное\",\n        \"paste\": \"Вставить содержимое\",\n        \"cut\": \"Вырезать выделенное\",\n        \"alignLeft\": \"Выровнять текст по левому краю\",\n        \"alignCenter\": \"Выровнять текст по центру\",\n        \"alignRight\": \"Выровнять текст по правому краю\",\n        \"insertInlineMathEquation\": \"Вставить встроенное математическое уравнение\",\n        \"undo\": \"Отменить\",\n        \"redo\": \"Повторить\",\n        \"convertToParagraph\": \"Преобразовать блок в абзац\",\n        \"backspace\": \"Удалить\",\n        \"deleteLeftWord\": \"Удалить слово слева\",\n        \"deleteLeftSentence\": \"Удалить предложение слева\",\n        \"delete\": \"Удалить символ справа\",\n        \"deleteMacOS\": \"Удалить символ слева\",\n        \"deleteRightWord\": \"Удалить слово справа\",\n        \"moveCursorLeft\": \"Переместить курсор влево\",\n        \"moveCursorBeginning\": \"Переместить курсор в начало\",\n        \"moveCursorLeftWord\": \"Переместить курсор на одно слово влево\",\n        \"moveCursorLeftSelect\": \"Выделить и переместить курсор влево\",\n        \"moveCursorBeginSelect\": \"Выделить и переместить курсор в начало\",\n        \"moveCursorLeftWordSelect\": \"Выделить и переместить курсор на одно слово влево\",\n        \"moveCursorRight\": \"Переместить курсор вправо\",\n        \"moveCursorEnd\": \"Переместить курсор в конец\",\n        \"moveCursorRightWord\": \"Переместить курсор на одно слово вправо\",\n        \"moveCursorRightSelect\": \"Выделить и переместить курсор вправо\",\n        \"moveCursorEndSelect\": \"Выделить и переместить курсор в конец\",\n        \"moveCursorRightWordSelect\": \"Выделить и переместить курсор на одно слово вправо\",\n        \"moveCursorUp\": \"Переместить курсор вверх\",\n        \"moveCursorTopSelect\": \"Выделить и переместить курсор вверх\",\n        \"moveCursorTop\": \"Переместить курсор вверх\",\n        \"moveCursorUpSelect\": \"Выделить и переместить курсор вверх\",\n        \"moveCursorBottomSelect\": \"Выделить и переместить курсор вниз\",\n        \"moveCursorBottom\": \"Переместить курсор вниз\",\n        \"moveCursorDown\": \"Переместить курсор вниз\",\n        \"moveCursorDownSelect\": \"Выделить и переместить курсор вниз\",\n        \"home\": \"Прокрутить вверх\",\n        \"end\": \"Прокрутить вниз\",\n        \"toggleBold\": \"Переключить жирный\",\n        \"toggleItalic\": \"Переключить курсив\",\n        \"toggleUnderline\": \"Переключить подчеркнутый\",\n        \"toggleStrikethrough\": \"Переключить зачеркнутый\",\n        \"toggleCode\": \"Переключить встроенный код\",\n        \"toggleHighlight\": \"Переключить выделение\",\n        \"showLinkMenu\": \"Показать меню ссылок\",\n        \"openInlineLink\": \"Открыть встроенную ссылку\",\n        \"openLinks\": \"Открыть все выделенные ссылки\",\n        \"indent\": \"Сделать отступ\",\n        \"outdent\": \"Убрать отступ\",\n        \"exit\": \"Выйти из редактирования\",\n        \"pageUp\": \"Прокрутить на одну страницу вверх\",\n        \"pageDown\": \"Прокрутить на одну страницу вниз\",\n        \"selectAll\": \"Выбрать все\",\n        \"pasteWithoutFormatting\": \"Вставить содержимое без форматирования\",\n        \"showEmojiPicker\": \"Показать выбор эмодзи\",\n        \"enterInTableCell\": \"Добавить перенос строки в ячейке таблицы\",\n        \"leftInTableCell\": \"Переместить влево на одну ячейку в таблице\",\n        \"rightInTableCell\": \"Переместить вправо на одну ячейку в таблице\",\n        \"upInTableCell\": \"Переместить вверх на одну ячейку в таблице\",\n        \"downInTableCell\": \"Переместить вниз на одну ячейку в таблице\",\n        \"tabInTableCell\": \"Перейти к следующей доступной ячейке в таблице\",\n        \"shiftTabInTableCell\": \"Перейти к предыдущей доступной ячейке в таблице\",\n        \"backSpaceInTableCell\": \"Остановиться в начале ячейки\"\n      },\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"Вставить новый абзац рядом с блоком кода\",\n        \"codeBlockIndentLines\": \"Вставить два пробела в начале строки в блоке кода\",\n        \"codeBlockOutdentLines\": \"Удалить два пробела в начале строки в блоке кода\",\n        \"codeBlockAddTwoSpaces\": \"Вставить два пробела в позиции курсора в блоке кода\",\n        \"codeBlockSelectAll\": \"Выбрать все содержимое блока кода\",\n        \"codeBlockPasteText\": \"Вставить текст в блок кода\",\n        \"textAlignLeft\": \"Выровнять текст по левому краю\",\n        \"textAlignCenter\": \"Выровнять текст по центру\",\n        \"textAlignRight\": \"Выровнять текст по правому краю\"\n      },\n      \"couldNotLoadErrorMsg\": \"Не удалось загрузить быстрые клавиши, попробуйте снова\",\n      \"couldNotSaveErrorMsg\": \"Не удалось сохранить быстрые клавиши, попробуйте снова\"\n    },\n    \"aiPage\": {\n      \"title\": \"Настройки AI\",\n      \"menuLabel\": \"Настройки AI\",\n      \"keys\": {\n        \"enableAISearchTitle\": \"Поиск AI\",\n        \"aiSettingsDescription\": \"Выберите предпочтительную модель для работы AppFlowy AI. Включает GPT-4o, GPT-o3-mini, DeepSeek R1, Claude 3.5 Sonnet и модели, доступные в Ollama\",\n        \"loginToEnableAIFeature\": \"Функции AI включены только после входа с AppFlowy Cloud. Если у вас нет учетной записи AppFlowy, перейдите в 'Мой аккаунт' для регистрации\",\n        \"llmModel\": \"Языковая модель\",\n        \"llmModelType\": \"Тип языковой модели\",\n        \"downloadLLMPrompt\": \"Загрузить {}\",\n        \"downloadAppFlowyOfflineAI\": \"Загрузка офлайн-пакета AI позволит AI работать на вашем устройстве. Вы хотите продолжить?\",\n        \"downloadLLMPromptDetail\": \"Загрузка локальной модели {} займет до {} места на диске. Вы хотите продолжить?\",\n        \"downloadBigFilePrompt\": \"Загрузка может занять около 10 минут\",\n        \"downloadAIModelButton\": \"Загрузить\",\n        \"downloadingModel\": \"Загрузка\",\n        \"localAILoaded\": \"Локальная модель AI успешно добавлена и готова к использованию\",\n        \"localAIStart\": \"Локальный AI запускается. Если работает медленно, попробуйте выключить и включить его\",\n        \"localAILoading\": \"Загружается локальная модель AI Chat...\",\n        \"localAIStopped\": \"Локальный AI остановлен\",\n        \"localAIRunning\": \"Локальный AI запущен\",\n        \"localAINotReadyRetryLater\": \"Локальный AI инициализируется, пожалуйста, попробуйте позже\",\n        \"localAIDisabled\": \"Вы используете локальный AI, но он отключен. Пожалуйста, перейдите в настройки, чтобы включить его или попробуйте другую модель\",\n        \"localAIInitializing\": \"Локальный AI загружается. Это может занять несколько секунд в зависимости от вашего устройства\",\n        \"localAINotReadyTextFieldPrompt\": \"Вы не можете редактировать, пока загружается локальный AI\",\n        \"localAIDisabledTextFieldPrompt\": \"Вы не можете редактировать, пока локальный AI отключен\",\n        \"failToLoadLocalAI\": \"Не удалось запустить локальный AI.\",\n        \"restartLocalAI\": \"Перезапустить\",\n        \"disableLocalAITitle\": \"Отключить локальный AI\",\n        \"disableLocalAIDescription\": \"Вы хотите отключить локальный AI?\",\n        \"localAIToggleTitle\": \"AppFlowy Локальный AI (LAI)\",\n        \"localAIToggleSubTitle\": \"Запускайте самые продвинутые локальные модели AI в AppFlowy для максимальной конфиденциальности и безопасности\",\n        \"offlineAIInstruction1\": \"Следуйте\",\n        \"offlineAIInstruction2\": \"инструкции\",\n        \"offlineAIInstruction3\": \"чтобы включить офлайн-AI.\",\n        \"offlineAIDownload1\": \"Если вы еще не скачали AppFlowy AI, пожалуйста, \",\n        \"offlineAIDownload2\": \"скачайте\",\n        \"offlineAIDownload3\": \"его сначала\",\n        \"activeOfflineAI\": \"Активно\",\n        \"downloadOfflineAI\": \"Скачать\",\n        \"openModelDirectory\": \"Открыть папку\",\n        \"laiNotReady\": \"Приложение Local AI не установлено корректно.\",\n        \"ollamaNotReady\": \"Сервер Ollama не готов.\",\n        \"pleaseFollowThese\": \"Пожалуйста, следуйте этим\",\n        \"instructions\": \"инструкциям\",\n        \"installOllamaLai\": \"для настройки Ollama и AppFlowy Local AI.\",\n        \"modelsMissing\": \"Не удалось найти необходимые модели: \",\n        \"downloadModel\": \"чтобы скачать их.\"\n      }\n    },\n    \"planPage\": {\n      \"menuLabel\": \"План\",\n      \"title\": \"Тарифный план\",\n      \"planUsage\": {\n        \"title\": \"Сводка использования плана\",\n        \"storageLabel\": \"Хранилище\",\n        \"storageUsage\": \"{} из {} ГБ\",\n        \"unlimitedStorageLabel\": \"Неограниченное хранилище\",\n        \"collaboratorsLabel\": \"Участники\",\n        \"collaboratorsUsage\": \"{} из {}\",\n        \"aiResponseLabel\": \"Ответы AI\",\n        \"aiResponseUsage\": \"{} из {}\",\n        \"unlimitedAILabel\": \"Неограниченные ответы\",\n        \"proBadge\": \"Pro\",\n        \"aiMaxBadge\": \"AI Max\",\n        \"aiOnDeviceBadge\": \"AI на устройстве для Mac\",\n        \"memberProToggle\": \"Больше участников и неограниченный AI\",\n        \"aiMaxToggle\": \"Неограниченный AI и доступ к продвинутым моделям\",\n        \"aiOnDeviceToggle\": \"Локальный AI для максимальной конфиденциальности\",\n        \"aiCredit\": {\n          \"title\": \"Добавить кредиты AI @:appName\",\n          \"price\": \"{}\",\n          \"priceDescription\": \"за 1000 кредитов\",\n          \"purchase\": \"Купить AI\",\n          \"info\": \"Добавьте 1000 кредитов AI на каждое рабочее пространство и легко интегрируйте настраиваемый AI в свой рабочий процесс для получения более умных и быстрых результатов с помощью:\",\n          \"infoItemOne\": \"10 000 ответов на базу данных\",\n          \"infoItemTwo\": \"1 000 ответов на рабочее пространство\"\n        },\n        \"currentPlan\": {\n          \"bannerLabel\": \"Текущий план\",\n          \"freeTitle\": \"Бесплатно\",\n          \"proTitle\": \"Pro\",\n          \"teamTitle\": \"Команда\",\n          \"freeInfo\": \"Идеально подходит для индивидуального использования до 2 участников для организации всего\",\n          \"proInfo\": \"Идеально подходит для небольших и средних команд до 10 участников.\",\n          \"teamInfo\": \"Идеально подходит для всех продуктивных и хорошо организованных команд.\",\n          \"upgrade\": \"Изменить план\",\n          \"canceledInfo\": \"Ваш план отменен, вы будете переведены на бесплатный план {}.\"\n        },\n        \"addons\": {\n          \"title\": \"Дополнения\",\n          \"addLabel\": \"Добавить\",\n          \"activeLabel\": \"Добавлено\",\n          \"aiMax\": {\n            \"title\": \"AI Max\",\n            \"description\": \"Неограниченные ответы AI на базе продвинутых моделей AI и 50 изображений AI в месяц\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"За пользователя в месяц при ежегодной оплате\"\n          },\n          \"aiOnDevice\": {\n            \"title\": \"AI на устройстве для Mac\",\n            \"description\": \"Запускайте Mistral 7B, LLAMA 3 и другие локальные модели на вашем компьютере\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"За пользователя в месяц при ежегодной оплате\",\n            \"recommend\": \"Рекомендуется M1 или новее\"\n          }\n        },\n        \"deal\": {\n          \"bannerLabel\": \"Предложение Нового года!\",\n          \"title\": \"Развивайте свою команду!\",\n          \"info\": \"Обновитесь и сэкономьте 10% на планах Pro и Team! Повысьте продуктивность своего рабочего пространства с помощью новых мощных функций, включая AppFlowy AI.\",\n          \"viewPlans\": \"Посмотреть планы\"\n        }\n      }\n    },\n    \"billingPage\": {\n      \"menuLabel\": \"Оплата\",\n      \"title\": \"Оплата\",\n      \"plan\": {\n        \"title\": \"План\",\n        \"freeLabel\": \"Бесплатно\",\n        \"proLabel\": \"Pro\",\n        \"planButtonLabel\": \"Изменить план\",\n        \"billingPeriod\": \"Платежный период\",\n        \"periodButtonLabel\": \"Редактировать период\"\n      },\n      \"paymentDetails\": {\n        \"title\": \"Реквизиты платежа\",\n        \"methodLabel\": \"Способ оплаты\",\n        \"methodButtonLabel\": \"Редактировать способ\"\n      },\n      \"addons\": {\n        \"title\": \"Дополнения\",\n        \"addLabel\": \"Добавить\",\n        \"removeLabel\": \"Удалить\",\n        \"renewLabel\": \"Продлить\",\n        \"aiMax\": {\n          \"label\": \"AI Max\",\n          \"description\": \"Разблокируйте неограниченный AI и продвинутые модели\",\n          \"activeDescription\": \"Следующий счет оплачивается {}\",\n          \"canceledDescription\": \"AI Max будет доступен до {}\"\n        },\n        \"aiOnDevice\": {\n          \"label\": \"AI на устройстве для Mac\",\n          \"description\": \"Разблокируйте неограниченный AI на вашем устройстве\",\n          \"activeDescription\": \"Следующий счет оплачивается {}\",\n          \"canceledDescription\": \"AI на устройстве для Mac будет доступен до {}\"\n        },\n        \"removeDialog\": {\n          \"title\": \"Удалить {}\",\n          \"description\": \"Вы уверены, что хотите удалить {plan}? Вы сразу же потеряете доступ к функциям и преимуществам {plan}.\"\n        }\n      },\n      \"currentPeriodBadge\": \"ТЕКУЩИЙ\",\n      \"changePeriod\": \"Изменить период\",\n      \"planPeriod\": \"{} период\",\n      \"monthlyInterval\": \"Ежемесячно\",\n      \"monthlyPriceInfo\": \"за место ежемесячно\",\n      \"annualInterval\": \"Ежегодно\",\n      \"annualPriceInfo\": \"за место ежегодно\"\n    },\n    \"comparePlanDialog\": {\n      \"title\": \"Сравнить и выбрать план\",\n      \"planFeatures\": \"План\\nФункции\",\n      \"current\": \"Текущий\",\n      \"actions\": {\n        \"upgrade\": \"Обновить\",\n        \"downgrade\": \"Понизить\",\n        \"current\": \"Текущий\"\n      },\n      \"freePlan\": {\n        \"title\": \"Бесплатно\",\n        \"description\": \"Для индивидуального использования до 2 участников для организации всего\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"Бесплатно навсегда\"\n      },\n      \"proPlan\": {\n        \"title\": \"Pro\",\n        \"description\": \"Для небольших команд для управления проектами и знаниями команды\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"За пользователя в месяц\\nпри ежегодной оплате\\n\\n{} при ежемесячной оплате\"\n      },\n      \"planLabels\": {\n        \"itemOne\": \"Рабочие пространства\",\n        \"itemTwo\": \"Участники\",\n        \"itemThree\": \"Хранилище\",\n        \"itemFour\": \"Совместная работа в реальном времени\",\n        \"itemFive\": \"Мобильное приложение\",\n        \"itemSix\": \"Ответы AI\",\n        \"itemSeven\": \"Изображения AI\",\n        \"itemFileUpload\": \"Загрузка файлов\",\n        \"customNamespace\": \"Пользовательское пространство имен\",\n        \"tooltipSix\": \"Пожизненно означает, что количество ответов никогда не сбрасывается\",\n        \"intelligentSearch\": \"Интеллектуальный поиск\",\n        \"tooltipSeven\": \"Позволяет настроить часть URL для вашего рабочего пространства\",\n        \"customNamespaceTooltip\": \"Пользовательский URL опубликованного сайта\"\n      },\n      \"freeLabels\": {\n        \"itemOne\": \"Оплата за рабочее пространство\",\n        \"itemTwo\": \"До 2\",\n        \"itemThree\": \"5 ГБ\",\n        \"itemFour\": \"да\",\n        \"itemFive\": \"да\",\n        \"itemSix\": \"10 пожизненно\",\n        \"itemSeven\": \"2 пожизненно\",\n        \"itemFileUpload\": \"До 7 МБ\",\n        \"intelligentSearch\": \"Интеллектуальный поиск\"\n      },\n      \"proLabels\": {\n        \"itemOne\": \"Оплата за рабочее пространство\",\n        \"itemTwo\": \"До 10\",\n        \"itemThree\": \"Неограниченно\",\n        \"itemFour\": \"да\",\n        \"itemFive\": \"да\",\n        \"itemSix\": \"Неограниченно\",\n        \"itemSeven\": \"10 изображений в месяц\",\n        \"itemFileUpload\": \"Неограниченно\",\n        \"intelligentSearch\": \"Интеллектуальный поиск\"\n      },\n      \"paymentSuccess\": {\n        \"title\": \"Вы теперь на плане {}!\",\n        \"description\": \"Ваш платеж успешно обработан, и ваш план обновлен до AppFlowy {}. Вы можете просмотреть детали вашего плана на странице Плана\"\n      },\n      \"downgradeDialog\": {\n        \"title\": \"Вы уверены, что хотите понизить план?\",\n        \"description\": \"Понижение плана приведет вас обратно к бесплатному плану. Участники могут потерять доступ к этому рабочему пространству, и вам может потребоваться освободить место, чтобы соответствовать лимитам хранилища бесплатного плана.\",\n        \"downgradeLabel\": \"Понизить план\"\n      }\n    },\n    \"cancelSurveyDialog\": {\n      \"title\": \"Жаль видеть, что вы уходите\",\n      \"description\": \"Нам жаль, что вы уходите. Мы будем рады услышать ваш отзыв, чтобы помочь нам улучшить @:appName. Пожалуйста, уделите минутку, чтобы ответить на несколько вопросов.\",\n      \"commonOther\": \"Другое\",\n      \"otherHint\": \"Напишите свой ответ здесь\",\n      \"questionOne\": {\n        \"question\": \"Что побудило вас отменить подписку на @:appName Pro?\",\n        \"answerOne\": \"Слишком высокая стоимость\",\n        \"answerTwo\": \"Функции не соответствовали ожиданиям\",\n        \"answerThree\": \"Нашел лучшую альтернативу\",\n        \"answerFour\": \"Использовал недостаточно для оправдания расходов\",\n        \"answerFive\": \"Проблема с сервисом или технические трудности\"\n      },\n      \"questionTwo\": {\n        \"question\": \"Насколько вероятно, что вы рассмотрите возможность повторной подписки на @:appName Pro в будущем?\",\n        \"answerOne\": \"Очень вероятно\",\n        \"answerTwo\": \"Несколько вероятно\",\n        \"answerThree\": \"Не уверен\",\n        \"answerFour\": \"Маловероятно\",\n        \"answerFive\": \"Очень маловероятно\"\n      },\n      \"questionThree\": {\n        \"question\": \"Какую функцию Pro вы ценили больше всего во время подписки?\",\n        \"answerOne\": \"Совместная работа нескольких пользователей\",\n        \"answerTwo\": \"Более долгая история версий\",\n        \"answerThree\": \"Неограниченные ответы AI\",\n        \"answerFour\": \"Доступ к локальным моделям AI\"\n      },\n      \"questionFour\": {\n        \"question\": \"Как бы вы описали свой общий опыт использования @:appName?\",\n        \"answerOne\": \"Отлично\",\n        \"answerTwo\": \"Хорошо\",\n        \"answerThree\": \"Средне\",\n        \"answerFour\": \"Ниже среднего\",\n        \"answerFive\": \"Недоволен\"\n      }\n    },\n    \"common\": {\n      \"uploadingFile\": \"Файл загружается. Пожалуйста, не закрывайте приложение\",\n      \"uploadNotionSuccess\": \"Ваш zip-файл Notion успешно загружен. Как только импорт завершится, вы получите письмо с подтверждением\",\n      \"reset\": \"Сбросить\"\n    },\n    \"menu\": {\n      \"appearance\": \"Внешний вид\",\n      \"language\": \"Язык\",\n      \"user\": \"Пользователь\",\n      \"files\": \"Файлы\",\n      \"notifications\": \"Уведомления\",\n      \"open\": \"Открыть настройки\",\n      \"logout\": \"Выйти\",\n      \"logoutPrompt\": \"Вы уверены, что хотите выйти?\",\n      \"selfEncryptionLogoutPrompt\": \"Вы уверены, что хотите выйти? Пожалуйста, убедитесь, что вы скопировали секрет шифрования\",\n      \"syncSetting\": \"Настройки синхронизации\",\n      \"cloudSettings\": \"Настройки облака\",\n      \"enableSync\": \"Включить синхронизацию\",\n      \"enableSyncLog\": \"Включить логирование синхронизации\",\n      \"enableSyncLogWarning\": \"Спасибо за помощь в диагностике проблем синхронизации. Это будет записывать ваши изменения документов в локальный файл. Пожалуйста, закройте и снова откройте приложение после включения\",\n      \"enableEncrypt\": \"Зашифровать данные\",\n      \"cloudURL\": \"Базовый URL\",\n      \"webURL\": \"Веб URL\",\n      \"invalidCloudURLScheme\": \"Недействительная схема\",\n      \"cloudServerType\": \"Тип облачного сервера\",\n      \"cloudServerTypeTip\": \"Обратите внимание, что после смены облачного сервера может потребоваться выход из текущей учетной записи\",\n      \"cloudLocal\": \"Локальный\",\n      \"cloudAppFlowy\": \"AppFlowy Cloud\",\n      \"cloudAppFlowySelfHost\": \"AppFlowy Cloud Self-hosted\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"URL облака не может быть пустым\",\n      \"clickToCopy\": \"Скопировать в буфер обмена\",\n      \"selfHostStart\": \"Если у вас нет сервера, пожалуйста, обратитесь к\",\n      \"selfHostContent\": \"документации\",\n      \"selfHostEnd\": \"для руководства по самостоятельному размещению своего сервера\",\n      \"pleaseInputValidURL\": \"Пожалуйста, введите действительный URL\",\n      \"changeUrl\": \"Изменить URL самостоятельного хостинга на {}\",\n      \"cloudURLHint\": \"Введите базовый URL вашего сервера\",\n      \"webURLHint\": \"Введите базовый URL вашего веб-сервера\",\n      \"cloudWSURL\": \"Websocket URL\",\n      \"cloudWSURLHint\": \"Введите адрес websocket вашего сервера\",\n      \"restartApp\": \"Перезапустить\",\n      \"restartAppTip\": \"Перезапустите приложение, чтобы изменения вступили в силу. Обратите внимание, что это может привести к выходу из текущей учетной записи.\",\n      \"changeServerTip\": \"После смены сервера необходимо нажать кнопку перезапуска, чтобы изменения вступили в силу\",\n      \"enableEncryptPrompt\": \"Активируйте шифрование для защиты ваших данных с помощью этого секрета. Храните его в надежном месте; после включения его нельзя отключить. В случае потери ваши данные станут недоступными. Нажмите, чтобы скопировать\",\n      \"inputEncryptPrompt\": \"Пожалуйста, введите секрет шифрования для\",\n      \"clickToCopySecret\": \"Нажмите, чтобы скопировать секрет\",\n      \"configServerSetting\": \"Настройте параметры вашего сервера\",\n      \"configServerGuide\": \"После выбора «Быстрый старт», перейдите в «Настройки», затем «Настройки облака», чтобы настроить свой самостоятельно размещенный сервер.\",\n      \"inputTextFieldHint\": \"Ваш секрет\",\n      \"historicalUserList\": \"История входов пользователей\",\n      \"historicalUserListTooltip\": \"Этот список отображает ваши анонимные учетные записи. Вы можете нажать на учетную запись, чтобы просмотреть ее детали. Анонимные учетные записи создаются при нажатии кнопки «Начать»\",\n      \"openHistoricalUser\": \"Нажмите, чтобы открыть анонимную учетную запись\",\n      \"customPathPrompt\": \"Хранение папки данных @:appName в облачной синхронизируемой папке, такой как Google Drive, может представлять риски. Если доступ к базе данных в этой папке или ее изменение осуществляется из нескольких мест одновременно, это может привести к конфликтам синхронизации и потенциальному повреждению данных.\",\n      \"importAppFlowyData\": \"Импорт данных из внешней папки @:appName\",\n      \"importingAppFlowyDataTip\": \"Идет импорт данных. Пожалуйста, не закрывайте приложение\",\n      \"importAppFlowyDataDescription\": \"Копирование данных из внешней папки данных @:appName и импорт их в текущую папку данных AppFlowy\",\n      \"importSuccess\": \"Папка данных @:appName успешно импортирована\",\n      \"importFailed\": \"Не удалось импортировать папку данных @:appName\",\n      \"importGuide\": \"Для получения более подробной информации, пожалуйста, ознакомьтесь с указанным документом\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Включить уведомления\",\n        \"hint\": \"Отключите, чтобы остановить появление локальных уведомлений.\"\n      },\n      \"showNotificationsIcon\": {\n        \"label\": \"Показать значок уведомлений\",\n        \"hint\": \"Отключите, чтобы скрыть значок уведомлений на боковой панели.\"\n      },\n      \"archiveNotifications\": {\n        \"allSuccess\": \"Все уведомления успешно заархивированы\",\n        \"success\": \"Уведомление успешно заархивировано\"\n      },\n      \"markAsReadNotifications\": {\n        \"allSuccess\": \"Все отмечены как прочитанные успешно\",\n        \"success\": \"Отмечено как прочитанное успешно\"\n      },\n      \"action\": {\n        \"markAsRead\": \"Отметить как прочитанное\",\n        \"multipleChoice\": \"Выбрать больше\",\n        \"archive\": \"Архивировать\"\n      },\n      \"settings\": {\n        \"settings\": \"Настройки\",\n        \"markAllAsRead\": \"Отметить все как прочитанное\",\n        \"archiveAll\": \"Архивировать все\"\n      },\n      \"emptyInbox\": {\n        \"title\": \"Ящик пуст!\",\n        \"description\": \"Установите напоминания, чтобы получать уведомления здесь.\"\n      },\n      \"emptyUnread\": {\n        \"title\": \"Нет непрочитанных уведомлений\",\n        \"description\": \"Вы все прочитали!\"\n      },\n      \"emptyArchived\": {\n        \"title\": \"Нет заархивированных\",\n        \"description\": \"Заархивированные уведомления появятся здесь.\"\n      },\n      \"tabs\": {\n        \"inbox\": \"Входящие\",\n        \"unread\": \"Непрочитанные\",\n        \"archived\": \"Архивированные\"\n      },\n      \"refreshSuccess\": \"Уведомления успешно обновлены\",\n      \"titles\": {\n        \"notifications\": \"Уведомления\",\n        \"reminder\": \"Напоминание\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"Сбросить\",\n      \"fontFamily\": {\n        \"label\": \"Семейство шрифтов\",\n        \"search\": \"Поиск\",\n        \"defaultFont\": \"Системный\"\n      },\n      \"themeMode\": {\n        \"label\": \"Режим темы\",\n        \"light\": \"Светлый режим\",\n        \"dark\": \"Темный режим\",\n        \"system\": \"Адаптироваться к системе\"\n      },\n      \"fontScaleFactor\": \"Коэффициент масштабирования шрифта\",\n      \"displaySize\": \"Размер отображения\",\n      \"documentSettings\": {\n        \"cursorColor\": \"Цвет курсора документа\",\n        \"selectionColor\": \"Цвет выделения документа\",\n        \"width\": \"Ширина документа\",\n        \"changeWidth\": \"Изменить\",\n        \"pickColor\": \"Выберите цвет\",\n        \"colorShade\": \"Оттенок цвета\",\n        \"opacity\": \"Непрозрачность\",\n        \"hexEmptyError\": \"Значение Hex не может быть пустым\",\n        \"hexLengthError\": \"Значение Hex должно состоять из 6 цифр\",\n        \"hexInvalidError\": \"Недопустимое значение Hex\",\n        \"opacityEmptyError\": \"Непрозрачность не может быть пустой\",\n        \"opacityRangeError\": \"Непрозрачность должна быть от 1 до 100\",\n        \"app\": \"Приложение\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"Применить\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Направление макета\",\n        \"hint\": \"Управляйте потоком контента на экране, слева направо или справа налево.\",\n        \"ltr\": \"Слева направо\",\n        \"rtl\": \"Справа налево\"\n      },\n      \"textDirection\": {\n        \"label\": \"Направление текста по умолчанию\",\n        \"hint\": \"Укажите, должно ли текст начинаться слева или справа по умолчанию.\",\n        \"ltr\": \"Слева направо\",\n        \"rtl\": \"Справа налево\",\n        \"auto\": \"АВТО\",\n        \"fallback\": \"Как направление макета\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Загрузить\",\n        \"uploadTheme\": \"Загрузить тему\",\n        \"description\": \"Загрузите свою собственную тему @:appName, используя кнопку ниже.\",\n        \"loading\": \"Пожалуйста, подождите, пока мы проверим и загрузим вашу тему...\",\n        \"uploadSuccess\": \"Ваша тема успешно загружена\",\n        \"deletionFailure\": \"Не удалось удалить тему. Попробуйте удалить ее вручную.\",\n        \"filePickerDialogTitle\": \"Выберите файл .flowy_plugin\",\n        \"urlUploadFailure\": \"Не удалось открыть url: {}\"\n      },\n      \"theme\": \"Тема\",\n      \"builtInsLabel\": \"Встроенные темы\",\n      \"pluginsLabel\": \"Плагины\",\n      \"dateFormat\": {\n        \"label\": \"Формат даты\",\n        \"local\": \"Локальный\",\n        \"us\": \"США\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"Удобный\",\n        \"dmy\": \"Д/М/Г\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Формат времени\",\n        \"twelveHour\": \"12-часовой\",\n        \"twentyFourHour\": \"24-часовой\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"Показывать диалог именования при создании страницы\",\n      \"enableRTLToolbarItems\": \"Включить элементы панели инструментов RTL\",\n      \"members\": {\n        \"title\": \"Участники\",\n        \"inviteMembers\": \"Пригласить участников\",\n        \"inviteHint\": \"Пригласить по email\",\n        \"sendInvite\": \"Пригласить\",\n        \"copyInviteLink\": \"Скопировать ссылку для приглашения\",\n        \"label\": \"Участники\",\n        \"user\": \"Пользователь\",\n        \"role\": \"Роль\",\n        \"removeFromWorkspace\": \"Удалить из рабочего пространства\",\n        \"removeFromWorkspaceSuccess\": \"Успешно удалено из рабочего пространства\",\n        \"removeFromWorkspaceFailed\": \"Не удалось удалить из рабочего пространства\",\n        \"owner\": \"Владелец\",\n        \"guest\": \"Гость\",\n        \"member\": \"Участник\",\n        \"memberHintText\": \"Участник может читать и редактировать страницы\",\n        \"guestHintText\": \"Гость может читать, реагировать, комментировать и редактировать определенные страницы с разрешением.\",\n        \"emailInvalidError\": \"Недействительный email, пожалуйста, проверьте и попробуйте снова\",\n        \"emailSent\": \"Email отправлен, пожалуйста, проверьте входящие\",\n        \"members\": \"участников\",\n        \"membersCount\": {\n          \"zero\": \"{} участников\",\n          \"one\": \"{} участник\",\n          \"other\": \"{} участников\"\n        },\n        \"inviteFailedDialogTitle\": \"Не удалось отправить приглашение\",\n        \"inviteFailedMemberLimit\": \"Достигнут лимит участников, пожалуйста, обновитесь, чтобы пригласить больше.\",\n        \"inviteFailedMemberLimitMobile\": \"Ваше рабочее пространство достигло лимита участников.\",\n        \"memberLimitExceeded\": \"Достигнут лимит участников, чтобы пригласить больше, пожалуйста,\",\n        \"memberLimitExceededUpgrade\": \"обновитесь\",\n        \"memberLimitExceededPro\": \"Достигнут лимит участников, если вам нужно больше участников, свяжитесь с\",\n        \"memberLimitExceededProContact\": \"support@appflowy.io\",\n        \"failedToAddMember\": \"Не удалось добавить участника\",\n        \"addMemberSuccess\": \"Участник успешно добавлен\",\n        \"removeMember\": \"Удалить участника\",\n        \"areYouSureToRemoveMember\": \"Вы уверены, что хотите удалить этого участника?\",\n        \"inviteMemberSuccess\": \"Приглашение успешно отправлено\",\n        \"failedToInviteMember\": \"Не удалось пригласить участника\",\n        \"workspaceMembersError\": \"Ой, что-то пошло не так\",\n        \"workspaceMembersErrorDescription\": \"Не удалось загрузить список участников сейчас. Пожалуйста, попробуйте позже\",\n        \"inviteLinkToAddMember\": \"Ссылка для приглашения участника\",\n        \"clickToCopyLink\": \"Нажмите, чтобы скопировать ссылку\",\n        \"or\": \"или\",\n        \"generateANewLink\": \"сгенерировать новую ссылку\",\n        \"inviteMemberByEmail\": \"Пригласить участника по email\",\n        \"inviteMemberHintText\": \"Пригласить по email\",\n        \"resetInviteLink\": \"Сбросить ссылку для приглашения?\",\n        \"resetInviteLinkDescription\": \"Сброс деактивирует текущую ссылку для всех участников пространства и сгенерирует новую. Старая ссылка больше не будет доступна.\",\n        \"adminPanel\": \"Панель администратора\",\n        \"reset\": \"Сбросить\",\n        \"resetInviteLinkSuccess\": \"Ссылка для приглашения успешно сброшена\",\n        \"resetInviteLinkFailed\": \"Не удалось сбросить ссылку для приглашения\",\n        \"resetInviteLinkFailedDescription\": \"Пожалуйста, попробуйте позже\",\n        \"memberPageDescription1\": \"Доступ к\",\n        \"memberPageDescription2\": \"для управления гостями и продвинутыми пользователями.\",\n        \"noInviteLink\": \"Вы еще не сгенерировали ссылку для приглашения.\",\n        \"copyLink\": \"Скопировать ссылку\"\n      }\n    },\n    \"files\": {\n      \"copy\": \"Копировать\",\n      \"defaultLocation\": \"Местоположение хранилища файлов и данных\",\n      \"exportData\": \"Экспортировать ваши данные\",\n      \"doubleTapToCopy\": \"Дважды коснитесь, чтобы скопировать путь\",\n      \"restoreLocation\": \"Восстановить путь по умолчанию @:appName\",\n      \"customizeLocation\": \"Открыть другую папку\",\n      \"restartApp\": \"Пожалуйста, перезапустите приложение, чтобы изменения вступили в силу.\",\n      \"exportDatabase\": \"Экспортировать базу данных\",\n      \"selectFiles\": \"Выберите файлы для экспорта\",\n      \"selectAll\": \"Выбрать все\",\n      \"deselectAll\": \"Отменить выбор всех\",\n      \"createNewFolder\": \"Создать новую папку\",\n      \"createNewFolderDesc\": \"Укажите, где вы хотите хранить свои данные\",\n      \"defineWhereYourDataIsStored\": \"Определите, где хранятся ваши данные\",\n      \"open\": \"Открыть\",\n      \"openFolder\": \"Открыть существующую папку\",\n      \"openFolderDesc\": \"Читать и записывать в существующую папку @:appName\",\n      \"folderHintText\": \"название папки\",\n      \"location\": \"Создание новой папки\",\n      \"locationDesc\": \"Выберите имя для папки данных @:appName\",\n      \"browser\": \"Обзор\",\n      \"create\": \"Создать\",\n      \"set\": \"Установить\",\n      \"folderPath\": \"Путь для хранения вашей папки\",\n      \"locationCannotBeEmpty\": \"Путь не может быть пустым\",\n      \"pathCopiedSnackbar\": \"Путь к хранилищу файлов скопирован в буфер обмена!\",\n      \"changeLocationTooltips\": \"Изменить каталог данных\",\n      \"change\": \"Изменить\",\n      \"openLocationTooltips\": \"Открыть другой каталог данных\",\n      \"openCurrentDataFolder\": \"Открыть текущий каталог данных\",\n      \"recoverLocationTooltips\": \"Сбросить до каталога данных по умолчанию @:appName\",\n      \"exportFileSuccess\": \"Файл успешно экспортирован!\",\n      \"exportFileFail\": \"Экспорт файла не удался!\",\n      \"export\": \"Экспорт\",\n      \"clearCache\": \"Очистить кеш\",\n      \"clearCacheDesc\": \"Если вы столкнулись с проблемами загрузки изображений или отображения шрифтов, попробуйте очистить кеш. Это действие не приведет к удалению ваших пользовательских данных.\",\n      \"areYouSureToClearCache\": \"Вы уверены, что хотите очистить кеш?\",\n      \"clearCacheSuccess\": \"Кеш успешно очищен!\"\n    },\n    \"user\": {\n      \"name\": \"Имя\",\n      \"email\": \"Email\",\n      \"tooltipSelectIcon\": \"Выбрать иконку\",\n      \"selectAnIcon\": \"Выберите иконку\",\n      \"pleaseInputYourOpenAIKey\": \"пожалуйста, введите ваш ключ AI\",\n      \"clickToLogout\": \"Нажмите, чтобы выйти из текущей учетной записи\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"Личная информация\",\n      \"username\": \"Имя пользователя\",\n      \"usernameEmptyError\": \"Имя пользователя не может быть пустым\",\n      \"about\": \"О программе\",\n      \"pushNotifications\": \"Push-уведомления\",\n      \"support\": \"Поддержка\",\n      \"joinDiscord\": \"Присоединяйтесь к нам в Discord\",\n      \"privacyPolicy\": \"Политика конфиденциальности\",\n      \"userAgreement\": \"Пользовательское соглашение\",\n      \"termsAndConditions\": \"Правила и условия\",\n      \"userprofileError\": \"Не удалось загрузить профиль пользователя\",\n      \"userprofileErrorDescription\": \"Пожалуйста, попробуйте выйти и снова войти, чтобы проверить, сохраняется ли проблема.\",\n      \"selectLayout\": \"Выбрать макет\",\n      \"selectStartingDay\": \"Выбрать день начала\",\n      \"version\": \"Версия\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Вы уверены, что хотите удалить этот вид?\",\n    \"createView\": \"Новый\",\n    \"title\": {\n      \"placeholder\": \"Без названия\"\n    },\n    \"settings\": {\n      \"filter\": \"Фильтр\",\n      \"sort\": \"Сортировка\",\n      \"sortBy\": \"Сортировать по\",\n      \"properties\": \"Свойства\",\n      \"reorderPropertiesTooltip\": \"Перетащите, чтобы изменить порядок свойств\",\n      \"group\": \"Группировать\",\n      \"addFilter\": \"Добавить фильтр\",\n      \"deleteFilter\": \"Удалить фильтр\",\n      \"filterBy\": \"Фильтровать по\",\n      \"typeAValue\": \"Введите значение...\",\n      \"layout\": \"Макет\",\n      \"compactMode\": \"Компактный режим\",\n      \"databaseLayout\": \"Макет\",\n      \"viewList\": {\n        \"zero\": \"0 видов\",\n        \"one\": \"{count} вид\",\n        \"other\": \"{count} видов\"\n      },\n      \"editView\": \"Редактировать вид\",\n      \"boardSettings\": \"Настройки доски\",\n      \"calendarSettings\": \"Настройки календаря\",\n      \"createView\": \"Новый вид\",\n      \"duplicateView\": \"Дублировать вид\",\n      \"deleteView\": \"Удалить вид\",\n      \"numberOfVisibleFields\": \"{} показано\"\n    },\n    \"filter\": {\n      \"empty\": \"Нет активных фильтров\",\n      \"addFilter\": \"Добавить фильтр\",\n      \"cannotFindCreatableField\": \"Не удалось найти подходящее поле для фильтрации\",\n      \"conditon\": \"Условие\",\n      \"where\": \"Где\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Содержит\",\n      \"doesNotContain\": \"Не содержит\",\n      \"endsWith\": \"Заканчивается на\",\n      \"startWith\": \"Начинается с\",\n      \"is\": \"Есть\",\n      \"isNot\": \"Нет\",\n      \"isEmpty\": \"Пусто\",\n      \"isNotEmpty\": \"Не пусто\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Не\",\n        \"startWith\": \"Начинается с\",\n        \"endWith\": \"Заканчивается на\",\n        \"isEmpty\": \"пусто\",\n        \"isNotEmpty\": \"не пусто\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Отмечено\",\n      \"isUnchecked\": \"Не отмечено\",\n      \"choicechipPrefix\": {\n        \"is\": \"есть\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"Завершено\",\n      \"isIncomplted\": \"Не завершено\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"Есть\",\n      \"isNot\": \"Нет\",\n      \"contains\": \"Содержит\",\n      \"doesNotContain\": \"Не содержит\",\n      \"isEmpty\": \"Пусто\",\n      \"isNotEmpty\": \"Не пусто\"\n    },\n    \"dateFilter\": {\n      \"is\": \"Приходится на\",\n      \"before\": \"До\",\n      \"after\": \"После\",\n      \"onOrBefore\": \"Приходится на или до\",\n      \"onOrAfter\": \"Приходится на или после\",\n      \"between\": \"Между\",\n      \"empty\": \"Пусто\",\n      \"notEmpty\": \"Не пусто\",\n      \"startDate\": \"Дата начала\",\n      \"endDate\": \"Дата окончания\",\n      \"choicechipPrefix\": {\n        \"before\": \"До\",\n        \"after\": \"После\",\n        \"between\": \"Между\",\n        \"onOrBefore\": \"На или до\",\n        \"onOrAfter\": \"На или после\",\n        \"isEmpty\": \"Пусто\",\n        \"isNotEmpty\": \"Не пусто\"\n      }\n    },\n    \"numberFilter\": {\n      \"equal\": \"Равно\",\n      \"notEqual\": \"Не равно\",\n      \"lessThan\": \"Меньше чем\",\n      \"greaterThan\": \"Больше чем\",\n      \"lessThanOrEqualTo\": \"Меньше или равно\",\n      \"greaterThanOrEqualTo\": \"Больше или равно\",\n      \"isEmpty\": \"Пусто\",\n      \"isNotEmpty\": \"Не пусто\"\n    },\n    \"field\": {\n      \"label\": \"Свойство\",\n      \"hide\": \"Скрыть свойство\",\n      \"show\": \"Показать свойство\",\n      \"insertLeft\": \"Вставить слева\",\n      \"insertRight\": \"Вставить справа\",\n      \"duplicate\": \"Дублировать\",\n      \"delete\": \"Удалить\",\n      \"wrapCellContent\": \"Переносить текст\",\n      \"clear\": \"Очистить ячейки\",\n      \"switchPrimaryFieldTooltip\": \"Невозможно изменить тип первичного поля\",\n      \"textFieldName\": \"Текст\",\n      \"checkboxFieldName\": \"Чекбокс\",\n      \"dateFieldName\": \"Дата\",\n      \"updatedAtFieldName\": \"Последнее изменение\",\n      \"createdAtFieldName\": \"Дата создания\",\n      \"numberFieldName\": \"Числа\",\n      \"singleSelectFieldName\": \"Выбор\",\n      \"multiSelectFieldName\": \"Множественный выбор\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Список задач\",\n      \"relationFieldName\": \"Связь\",\n      \"summaryFieldName\": \"AI Сводка\",\n      \"timeFieldName\": \"Время\",\n      \"mediaFieldName\": \"Файлы и медиа\",\n      \"translateFieldName\": \"AI Перевод\",\n      \"translateTo\": \"Перевести на\",\n      \"numberFormat\": \"Формат чисел\",\n      \"dateFormat\": \"Формат даты\",\n      \"includeTime\": \"Включать время\",\n      \"isRange\": \"Дата окончания\",\n      \"dateFormatFriendly\": \"Месяц День, Год\",\n      \"dateFormatISO\": \"Год-Месяц-День\",\n      \"dateFormatLocal\": \"Месяц/День/Год\",\n      \"dateFormatUS\": \"Год/Месяц/День\",\n      \"dateFormatDayMonthYear\": \"День/Месяц/Год\",\n      \"timeFormat\": \"Формат времени\",\n      \"invalidTimeFormat\": \"Неверный формат\",\n      \"timeFormatTwelveHour\": \"12 часов\",\n      \"timeFormatTwentyFourHour\": \"24 часа\",\n      \"clearDate\": \"Очистить дату\",\n      \"dateTime\": \"Дата время\",\n      \"startDateTime\": \"Дата и время начала\",\n      \"endDateTime\": \"Дата и время окончания\",\n      \"failedToLoadDate\": \"Не удалось загрузить значение даты\",\n      \"selectTime\": \"Выберите время\",\n      \"selectDate\": \"Выберите дату\",\n      \"visibility\": \"Видимость\",\n      \"propertyType\": \"Тип свойства\",\n      \"addSelectOption\": \"Добавить вариант\",\n      \"typeANewOption\": \"Введите новый вариант\",\n      \"optionTitle\": \"Варианты\",\n      \"addOption\": \"Добавить вариант\",\n      \"editProperty\": \"Редактировать свойство\",\n      \"newProperty\": \"Новое свойство\",\n      \"openRowDocument\": \"Открыть как страницу\",\n      \"deleteFieldPromptMessage\": \"Вы уверены? Это свойство и все его данные будут удалены\",\n      \"clearFieldPromptMessage\": \"Вы уверены? Все ячейки в этом столбце будут очищены\",\n      \"newColumn\": \"Новый столбец\",\n      \"format\": \"Формат\",\n      \"reminderOnDateTooltip\": \"Эта ячейка имеет запланированное напоминание\",\n      \"optionAlreadyExist\": \"Вариант уже существует\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Добавить новое поле\",\n      \"fieldDragElementTooltip\": \"Нажмите, чтобы открыть меню\",\n      \"showHiddenFields\": {\n        \"one\": \"Показать {count} скрытое поле\",\n        \"many\": \"Показать {count} скрытых полей\",\n        \"other\": \"Показать {count} скрытых полей\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"Скрыть {count} скрытое поле\",\n        \"many\": \"Скрыть {count} скрытых полей\",\n        \"other\": \"Скрыть {count} скрытых полей\"\n      },\n      \"openAsFullPage\": \"Открыть как полную страницу\",\n      \"moreRowActions\": \"Больше действий со строкой\"\n    },\n    \"sort\": {\n      \"ascending\": \"По возрастанию\",\n      \"descending\": \"По убыванию\",\n      \"by\": \"По\",\n      \"empty\": \"Нет активных сортировок\",\n      \"cannotFindCreatableField\": \"Не удалось найти подходящее поле для сортировки\",\n      \"deleteAllSorts\": \"Удалить все сортировки\",\n      \"addSort\": \"Добавить сортировку\",\n      \"sortsActive\": \"Невозможно {intention} при сортировке\",\n      \"removeSorting\": \"Вы хотите удалить все сортировки в этом виде и продолжить?\",\n      \"fieldInUse\": \"Вы уже сортируете по этому полю\"\n    },\n    \"row\": {\n      \"label\": \"Строка\",\n      \"duplicate\": \"Дублировать\",\n      \"delete\": \"Удалить\",\n      \"titlePlaceholder\": \"Без названия\",\n      \"textPlaceholder\": \"Пусто\",\n      \"copyProperty\": \"Свойство скопировано в буфер обмена\",\n      \"count\": \"Количество\",\n      \"newRow\": \"Новая строка\",\n      \"loadMore\": \"Загрузить еще\",\n      \"action\": \"Действие\",\n      \"add\": \"Нажмите, чтобы добавить ниже\",\n      \"drag\": \"Перетащите, чтобы переместить\",\n      \"deleteRowPrompt\": \"Вы уверены, что хотите удалить эту строку? Это действие нельзя отменить.\",\n      \"deleteCardPrompt\": \"Вы уверены, что хотите удалить эту карточку? Это действие нельзя отменить.\",\n      \"dragAndClick\": \"Перетащите, чтобы переместить, нажмите, чтобы открыть меню\",\n      \"insertRecordAbove\": \"Вставить запись выше\",\n      \"insertRecordBelow\": \"Вставить запись ниже\",\n      \"noContent\": \"Нет содержимого\",\n      \"reorderRowDescription\": \"изменить порядок строки\",\n      \"createRowAboveDescription\": \"создать строку выше\",\n      \"createRowBelowDescription\": \"вставить строку ниже\"\n    },\n    \"selectOption\": {\n      \"create\": \"Создать\",\n      \"purpleColor\": \"Фиолетовый\",\n      \"pinkColor\": \"Розовый\",\n      \"lightPinkColor\": \"Светло-розовый\",\n      \"orangeColor\": \"Оранжевый\",\n      \"yellowColor\": \"Желтый\",\n      \"limeColor\": \"Лайм\",\n      \"greenColor\": \"Зеленый\",\n      \"aquaColor\": \"Аква\",\n      \"blueColor\": \"Синий\",\n      \"deleteTag\": \"Удалить тег\",\n      \"colorPanelTitle\": \"Цвет\",\n      \"panelTitle\": \"Выберите вариант или создайте новый\",\n      \"searchOption\": \"Поиск варианта\",\n      \"searchOrCreateOption\": \"Поиск варианта или создание нового\",\n      \"createNew\": \"Создать новый\",\n      \"orSelectOne\": \"Или выберите вариант\",\n      \"typeANewOption\": \"Введите новый вариант\",\n      \"tagName\": \"Название тега\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"Описание задачи\",\n      \"addNew\": \"Добавить новую задачу\",\n      \"submitNewTask\": \"Создать\",\n      \"hideComplete\": \"Скрыть выполненные задачи\",\n      \"showComplete\": \"Показать все задачи\"\n    },\n    \"url\": {\n      \"launch\": \"Открыть ссылку в браузере\",\n      \"copy\": \"Скопировать ссылку в буфер обмена\",\n      \"textFieldHint\": \"Введите URL\"\n    },\n    \"relation\": {\n      \"relatedDatabasePlaceLabel\": \"Связанная база данных\",\n      \"relatedDatabasePlaceholder\": \"Нет\",\n      \"inRelatedDatabase\": \"В\",\n      \"rowSearchTextFieldPlaceholder\": \"Поиск\",\n      \"noDatabaseSelected\": \"База данных не выбрана, пожалуйста, выберите ее из списка ниже:\",\n      \"emptySearchResult\": \"Результаты не найдены\",\n      \"linkedRowListLabel\": \"{count} связанных строк\",\n      \"unlinkedRowListLabel\": \"Связать другую строку\"\n    },\n    \"menuName\": \"Таблица\",\n    \"referencedGridPrefix\": \"Вид\",\n    \"calculate\": \"Вычислить\",\n    \"calculationTypeLabel\": {\n      \"none\": \"Нет\",\n      \"average\": \"Среднее\",\n      \"max\": \"Макс.\",\n      \"median\": \"Медиана\",\n      \"min\": \"Мин.\",\n      \"sum\": \"Сумма\",\n      \"count\": \"Количество\",\n      \"countEmpty\": \"Количество пустых\",\n      \"countEmptyShort\": \"ПУСТО\",\n      \"countNonEmpty\": \"Количество не пустых\",\n      \"countNonEmptyShort\": \"ЗАПОЛНЕНО\"\n    },\n    \"media\": {\n      \"rename\": \"Переименовать\",\n      \"download\": \"Скачать\",\n      \"expand\": \"Развернуть\",\n      \"delete\": \"Удалить\",\n      \"moreFilesHint\": \"+{}\",\n      \"addFileOrImage\": \"Добавить файл или ссылку\",\n      \"attachmentsHint\": \"{}\",\n      \"addFileMobile\": \"Добавить файл\",\n      \"extraCount\": \"+{}\",\n      \"deleteFileDescription\": \"Вы уверены, что хотите удалить этот файл? Это действие необратимо.\",\n      \"showFileNames\": \"Показать имена файлов\",\n      \"downloadSuccess\": \"Файл скачан\",\n      \"downloadFailedToken\": \"Не удалось скачать файл, токен пользователя недоступен\",\n      \"setAsCover\": \"Установить как обложку\",\n      \"openInBrowser\": \"Открыть в браузере\",\n      \"embedLink\": \"Встроить ссылку на файл\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"Документ\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"creating\": \"Создается...\",\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Выберите доску для ссылки\",\n        \"createANewBoard\": \"Создать новую доску\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Выберите таблицу для ссылки\",\n        \"createANewGrid\": \"Создать новую таблицу\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Выберите календарь для ссылки\",\n        \"createANewCalendar\": \"Создать новый календарь\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"Выберите документ для ссылки\"\n      },\n      \"name\": {\n        \"textStyle\": \"Стиль текста\",\n        \"list\": \"Список\",\n        \"toggle\": \"Переключатель\",\n        \"fileAndMedia\": \"Файл и медиа\",\n        \"simpleTable\": \"Простая таблица\",\n        \"visuals\": \"Визуальные элементы\",\n        \"document\": \"Документ\",\n        \"advanced\": \"Дополнительно\",\n        \"text\": \"Текст\",\n        \"heading1\": \"Заголовок 1\",\n        \"heading2\": \"Заголовок 2\",\n        \"heading3\": \"Заголовок 3\",\n        \"image\": \"Изображение\",\n        \"bulletedList\": \"Маркированный список\",\n        \"numberedList\": \"Нумерованный список\",\n        \"todoList\": \"Список дел\",\n        \"doc\": \"Документ\",\n        \"linkedDoc\": \"Ссылка на страницу\",\n        \"grid\": \"Таблица\",\n        \"linkedGrid\": \"Связанная таблица\",\n        \"kanban\": \"Канбан\",\n        \"linkedKanban\": \"Связанный Канбан\",\n        \"calendar\": \"Календарь\",\n        \"linkedCalendar\": \"Связанный календарь\",\n        \"quote\": \"Цитата\",\n        \"divider\": \"Разделитель\",\n        \"table\": \"Таблица\",\n        \"callout\": \"Врезка\",\n        \"outline\": \"Структура\",\n        \"mathEquation\": \"Математическое уравнение\",\n        \"code\": \"Код\",\n        \"toggleList\": \"Список-переключатель\",\n        \"toggleHeading1\": \"Переключатель заголовка 1\",\n        \"toggleHeading2\": \"Переключатель заголовка 2\",\n        \"toggleHeading3\": \"Переключатель заголовка 3\",\n        \"emoji\": \"Эмодзи\",\n        \"aiWriter\": \"Спросить AI что угодно\",\n        \"dateOrReminder\": \"Дата или напоминание\",\n        \"photoGallery\": \"Фотогалерея\",\n        \"file\": \"Файл\",\n        \"twoColumns\": \"2 столбца\",\n        \"threeColumns\": \"3 столбца\",\n        \"fourColumns\": \"4 столбца\"\n      },\n      \"subPage\": {\n        \"name\": \"Документ\",\n        \"keyword1\": \"подстраница\",\n        \"keyword2\": \"страница\",\n        \"keyword3\": \"дочерняя страница\",\n        \"keyword4\": \"вставить страницу\",\n        \"keyword5\": \"встроить страницу\",\n        \"keyword6\": \"новая страница\",\n        \"keyword7\": \"создать страницу\",\n        \"keyword8\": \"документ\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Структура\",\n      \"codeBlock\": \"Блок кода\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Связанная доска\",\n      \"referencedGrid\": \"Связанная таблица\",\n      \"referencedCalendar\": \"Связанный календарь\",\n      \"referencedDocument\": \"Связанный документ\",\n      \"aiWriter\": {\n        \"userQuestion\": \"Спросить AI что угодно\",\n        \"continueWriting\": \"Продолжить писать\",\n        \"fixSpelling\": \"Исправить орфографию и грамматику\",\n        \"improveWriting\": \"Улучшить написание\",\n        \"summarize\": \"Сводка\",\n        \"explain\": \"Объяснить\",\n        \"makeShorter\": \"Сделать короче\",\n        \"makeLonger\": \"Сделать длиннее\"\n      },\n      \"autoGeneratorMenuItemName\": \"AI Писатель\",\n      \"autoGeneratorTitleName\": \"AI: Попросить AI написать что угодно...\",\n      \"autoGeneratorLearnMore\": \"Узнать больше\",\n      \"autoGeneratorGenerate\": \"Сгенерировать\",\n      \"autoGeneratorHintText\": \"Спросить AI...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Не удается получить ключ AI\",\n      \"autoGeneratorRewrite\": \"Переписать\",\n      \"smartEdit\": \"Спросить AI\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"Исправить орфографию и грамматику\",\n      \"warning\": \"⚠️ Ответы AI могут быть неточными или вводящими в заблуждение.\",\n      \"smartEditSummarize\": \"Сводка\",\n      \"smartEditImproveWriting\": \"Улучшить написание\",\n      \"smartEditMakeLonger\": \"Сделать длиннее\",\n      \"smartEditCouldNotFetchResult\": \"Не удалось получить результат от AI\",\n      \"smartEditCouldNotFetchKey\": \"Не удалось получить ключ AI\",\n      \"smartEditDisabled\": \"Подключить AI в настройках\",\n      \"appflowyAIEditDisabled\": \"Войдите, чтобы включить функции AI\",\n      \"discardResponse\": \"Вы уверены, что хотите отменить ответ AI?\",\n      \"createInlineMathEquation\": \"Создать уравнение\",\n      \"fonts\": \"Шрифты\",\n      \"insertDate\": \"Вставить дату\",\n      \"emoji\": \"Эмодзи\",\n      \"toggleList\": \"Список-переключатель\",\n      \"emptyToggleHeading\": \"Пустой заголовок-переключатель h{}. Нажмите, чтобы добавить содержимое.\",\n      \"emptyToggleList\": \"Пустой список-переключатель. Нажмите, чтобы добавить содержимое.\",\n      \"emptyToggleHeadingWeb\": \"Пустой заголовок-переключатель h{level}. Нажмите, чтобы добавить содержимое\",\n      \"quoteList\": \"Список цитат\",\n      \"numberedList\": \"Нумерованный список\",\n      \"bulletedList\": \"Маркированный список\",\n      \"todoList\": \"Список дел\",\n      \"callout\": \"Врезка\",\n      \"simpleTable\": {\n        \"moreActions\": {\n          \"color\": \"Цвет\",\n          \"align\": \"Выровнять\",\n          \"delete\": \"Удалить\",\n          \"duplicate\": \"Дублировать\",\n          \"insertLeft\": \"Вставить слева\",\n          \"insertRight\": \"Вставить справа\",\n          \"insertAbove\": \"Вставить выше\",\n          \"insertBelow\": \"Вставить ниже\",\n          \"headerColumn\": \"Столбец заголовка\",\n          \"headerRow\": \"Строка заголовка\",\n          \"clearContents\": \"Очистить содержимое\",\n          \"setToPageWidth\": \"Установить ширину страницы\",\n          \"distributeColumnsWidth\": \"Распределить ширину столбцов равномерно\",\n          \"duplicateRow\": \"Дублировать строку\",\n          \"duplicateColumn\": \"Дублировать столбец\",\n          \"textColor\": \"Цвет текста\",\n          \"cellBackgroundColor\": \"Цвет фона ячейки\",\n          \"duplicateTable\": \"Дублировать таблицу\"\n        },\n        \"clickToAddNewRow\": \"Нажмите, чтобы добавить новую строку\",\n        \"clickToAddNewColumn\": \"Нажмите, чтобы добавить новый столбец\",\n        \"clickToAddNewRowAndColumn\": \"Нажмите, чтобы добавить новую строку и столбец\",\n        \"headerName\": {\n          \"table\": \"Таблица\",\n          \"alignText\": \"Выровнять текст\"\n        }\n      },\n      \"cover\": {\n        \"changeCover\": \"Сменить обложку\",\n        \"colors\": \"Цвета\",\n        \"images\": \"Изображения\",\n        \"clearAll\": \"Очистить все\",\n        \"abstract\": \"Абстракция\",\n        \"addCover\": \"Добавить обложку\",\n        \"addLocalImage\": \"Добавить локальное изображение\",\n        \"invalidImageUrl\": \"Недействительный URL изображения\",\n        \"failedToAddImageToGallery\": \"Не удалось добавить изображение в галерею\",\n        \"enterImageUrl\": \"Введите URL изображения\",\n        \"add\": \"Добавить\",\n        \"back\": \"Назад\",\n        \"saveToGallery\": \"Сохранить в галерею\",\n        \"removeIcon\": \"Удалить иконку\",\n        \"removeCover\": \"Удалить обложку\",\n        \"pasteImageUrl\": \"Вставьте URL изображения\",\n        \"or\": \"ИЛИ\",\n        \"pickFromFiles\": \"Выбрать из файлов\",\n        \"couldNotFetchImage\": \"Не удалось загрузить изображение\",\n        \"imageSavingFailed\": \"Не удалось сохранить изображение\",\n        \"addIcon\": \"Добавить иконку\",\n        \"changeIcon\": \"Изменить иконку\",\n        \"coverRemoveAlert\": \"Изображение будет удалено с обложки после удаления.\",\n        \"alertDialogConfirmation\": \"Вы уверены, что хотите продолжить?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"Математическое уравнение\",\n        \"addMathEquation\": \"Добавить уравнение TeX\",\n        \"editMathEquation\": \"Редактировать математическое уравнение\"\n      },\n      \"optionAction\": {\n        \"click\": \"Нажмите\",\n        \"toOpenMenu\": \"чтобы открыть меню\",\n        \"drag\": \"Перетащите\",\n        \"toMove\": \"чтобы переместить\",\n        \"delete\": \"Удалить\",\n        \"duplicate\": \"Дублировать\",\n        \"turnInto\": \"Превратить в\",\n        \"moveUp\": \"Переместить вверх\",\n        \"moveDown\": \"Переместить вниз\",\n        \"color\": \"Цвет\",\n        \"align\": \"Выровнять\",\n        \"left\": \"По левому краю\",\n        \"center\": \"По центру\",\n        \"right\": \"По правому краю\",\n        \"defaultColor\": \"По умолчанию\",\n        \"depth\": \"Глубина\",\n        \"copyLinkToBlock\": \"Скопировать ссылку на блок\"\n      },\n      \"image\": {\n        \"addAnImage\": \"Добавить изображения\",\n        \"copiedToPasteBoard\": \"Ссылка на изображение скопирована в буфер обмена\",\n        \"addAnImageDesktop\": \"Добавить изображение\",\n        \"addAnImageMobile\": \"Нажмите, чтобы добавить одно или несколько изображений\",\n        \"dropImageToInsert\": \"Перетащите изображения для вставки\",\n        \"imageUploadFailed\": \"Не удалось загрузить изображение\",\n        \"imageDownloadFailed\": \"Не удалось загрузить изображение, пожалуйста, попробуйте снова\",\n        \"imageDownloadFailedToken\": \"Не удалось загрузить изображение из-за отсутствия токена пользователя, пожалуйста, попробуйте снова\",\n        \"errorCode\": \"Код ошибки\",\n        \"invalidImage\": \"Недействительное изображение\",\n        \"invalidImageSize\": \"Размер изображения должен быть менее 5 МБ\",\n        \"invalidImageFormat\": \"Формат изображения не поддерживается. Поддерживаемые форматы: JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"Недействительный URL изображения\",\n        \"noImage\": \"Нет такого файла или каталога\",\n        \"multipleImagesFailed\": \"Не удалось загрузить одно или несколько изображений, пожалуйста, попробуйте снова\",\n        \"embedLink\": {\n          \"label\": \"Встроить ссылку\",\n          \"placeholder\": \"Вставьте или введите ссылку на изображение\"\n        },\n        \"unsplash\": {\n          \"label\": \"Unsplash\"\n        },\n        \"searchForAnImage\": \"Поиск изображения\",\n        \"pleaseInputYourOpenAIKey\": \"пожалуйста, введите ваш ключ AI на странице настроек\",\n        \"saveImageToGallery\": \"Сохранить изображение\",\n        \"failedToAddImageToGallery\": \"Не удалось сохранить изображение\",\n        \"successToAddImageToGallery\": \"Изображение сохранено в Фото\",\n        \"unableToLoadImage\": \"Невозможно загрузить изображение\",\n        \"maximumImageSize\": \"Максимальный поддерживаемый размер загружаемого изображения - 10 МБ\",\n        \"uploadImageErrorImageSizeTooBig\": \"Размер изображения должен быть меньше 10 МБ\",\n        \"imageIsUploading\": \"Изображение загружается\",\n        \"openFullScreen\": \"Открыть на весь экран\",\n        \"interactiveViewer\": {\n          \"toolbar\": {\n            \"previousImageTooltip\": \"Предыдущее изображение\",\n            \"nextImageTooltip\": \"Следующее изображение\",\n            \"zoomOutTooltip\": \"Уменьшить\",\n            \"zoomInTooltip\": \"Увеличить\",\n            \"changeZoomLevelTooltip\": \"Изменить уровень масштабирования\",\n            \"openLocalImage\": \"Открыть изображение\",\n            \"downloadImage\": \"Скачать изображение\",\n            \"closeViewer\": \"Закрыть интерактивный просмотрщик\",\n            \"scalePercentage\": \"{}%\",\n            \"deleteImageTooltip\": \"Удалить изображение\"\n          }\n        }\n      },\n      \"photoGallery\": {\n        \"name\": \"Фотогалерея\",\n        \"imageKeyword\": \"изображение\",\n        \"imageGalleryKeyword\": \"галерея изображений\",\n        \"photoKeyword\": \"фото\",\n        \"photoBrowserKeyword\": \"фотобраузер\",\n        \"galleryKeyword\": \"галерея\",\n        \"addImageTooltip\": \"Добавить изображение\",\n        \"changeLayoutTooltip\": \"Изменить макет\",\n        \"browserLayout\": \"Браузер\",\n        \"gridLayout\": \"Сетка\",\n        \"deleteBlockTooltip\": \"Удалить всю галерею\"\n      },\n      \"math\": {\n        \"copiedToPasteBoard\": \"Математическое уравнение скопировано в буфер обмена\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"Ссылка скопирована в буфер обмена\",\n        \"convertToLink\": \"Преобразовать в ссылку для встраивания\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Добавьте заголовки для создания оглавления.\",\n        \"noMatchHeadings\": \"Не найдено соответствующих заголовков.\"\n      },\n      \"table\": {\n        \"addAfter\": \"Добавить после\",\n        \"addBefore\": \"Добавить до\",\n        \"delete\": \"Удалить\",\n        \"clear\": \"Очистить содержимое\",\n        \"duplicate\": \"Дублировать\",\n        \"bgColor\": \"Цвет фона\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Копировать\",\n        \"cut\": \"Вырезать\",\n        \"paste\": \"Вставить\",\n        \"pasteAsPlainText\": \"Вставить как обычный текст\"\n      },\n      \"action\": \"Действия\",\n      \"database\": {\n        \"selectDataSource\": \"Выбрать источник данных\",\n        \"noDataSource\": \"Нет источника данных\",\n        \"selectADataSource\": \"Выберите источник данных\",\n        \"toContinue\": \"для продолжения\",\n        \"newDatabase\": \"Новая база данных\",\n        \"linkToDatabase\": \"Ссылка на базу данных\"\n      },\n      \"date\": \"Дата\",\n      \"video\": {\n        \"label\": \"Видео\",\n        \"emptyLabel\": \"Добавить видео\",\n        \"placeholder\": \"Вставьте ссылку на видео\",\n        \"copiedToPasteBoard\": \"Ссылка на видео скопирована в буфер обмена\",\n        \"insertVideo\": \"Добавить видео\",\n        \"invalidVideoUrl\": \"Источник URL пока не поддерживается.\",\n        \"invalidVideoUrlYouTube\": \"YouTube пока не поддерживается.\",\n        \"supportedFormats\": \"Поддерживаемые форматы: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264\"\n      },\n      \"file\": {\n        \"name\": \"Файл\",\n        \"uploadTab\": \"Загрузить\",\n        \"uploadMobile\": \"Выберите файл\",\n        \"uploadMobileGallery\": \"Из фотогалереи\",\n        \"networkTab\": \"Встроить ссылку\",\n        \"placeholderText\": \"Загрузить или встроить файл\",\n        \"placeholderDragging\": \"Перетащите файл для загрузки\",\n        \"dropFileToUpload\": \"Перетащите файл для загрузки\",\n        \"fileUploadHint\": \"Перетащите файл или нажмите, чтобы \",\n        \"fileUploadHintSuffix\": \"Обзор\",\n        \"networkHint\": \"Вставьте ссылку на файл\",\n        \"networkUrlInvalid\": \"Недействительный URL. Проверьте URL и попробуйте снова.\",\n        \"networkAction\": \"Встроить\",\n        \"fileTooBigError\": \"Размер файла слишком большой, пожалуйста, загрузите файл размером менее 10 МБ\",\n        \"renameFile\": {\n          \"title\": \"Переименовать файл\",\n          \"description\": \"Введите новое имя для этого файла\",\n          \"nameEmptyError\": \"Имя файла не может быть пустым.\"\n        },\n        \"uploadedAt\": \"Загружено {}\",\n        \"linkedAt\": \"Ссылка добавлена {}\",\n        \"failedToOpenMsg\": \"Не удалось открыть, файл не найден\"\n      },\n      \"subPage\": {\n        \"handlingPasteHint\": \" - (обработка вставки)\",\n        \"errors\": {\n          \"failedDeletePage\": \"Не удалось удалить страницу\",\n          \"failedCreatePage\": \"Не удалось создать страницу\",\n          \"failedMovePage\": \"Не удалось переместить страницу в этот документ\",\n          \"failedDuplicatePage\": \"Не удалось дублировать страницу\",\n          \"failedDuplicateFindView\": \"Не удалось дублировать страницу - оригинальный вид не найден\"\n        }\n      },\n      \"cannotMoveToItsChildren\": \"Невозможно переместить в его дочерние элементы\",\n      \"linkPreview\": {\n        \"typeSelection\": {\n          \"pasteAs\": \"Вставить как\",\n          \"mention\": \"Упоминание\",\n          \"URL\": \"URL\",\n          \"bookmark\": \"Закладка\",\n          \"embed\": \"Встроить\"\n        },\n        \"linkPreviewMenu\": {\n          \"toMetion\": \"Преобразовать в упоминание\",\n          \"toUrl\": \"Преобразовать в URL\",\n          \"toEmbed\": \"Преобразовать во встраивание\",\n          \"toBookmark\": \"Преобразовать в закладку\",\n          \"copyLink\": \"Скопировать ссылку\",\n          \"replace\": \"Заменить\",\n          \"reload\": \"Перезагрузить\",\n          \"removeLink\": \"Удалить ссылку\",\n          \"pasteHint\": \"Вставить https://...\",\n          \"unableToDisplay\": \"невозможно отобразить\"\n        }\n      }\n    },\n    \"outlineBlock\": {\n      \"placeholder\": \"Содержание\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Введите '/' для команд\"\n    },\n    \"title\": {\n      \"placeholder\": \"Без названия\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Нажмите, чтобы добавить изображение(я)\",\n      \"upload\": {\n        \"label\": \"Загрузить\",\n        \"placeholder\": \"Нажмите, чтобы загрузить изображение\"\n      },\n      \"url\": {\n        \"label\": \"URL изображения\",\n        \"placeholder\": \"Введите URL изображения\"\n      },\n      \"ai\": {\n        \"label\": \"Сгенерировать изображение с помощью AI\",\n        \"placeholder\": \"Пожалуйста, введите запрос для AI для генерации изображения\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Сгенерировать изображение с помощью Stability AI\",\n        \"placeholder\": \"Пожалуйста, введите запрос для Stability AI для генерации изображения\"\n      },\n      \"support\": \"Размер изображения ограничен 5 МБ. Поддерживаемые форматы: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Недействительное изображение\",\n        \"invalidImageSize\": \"Размер изображения должен быть меньше 5 МБ\",\n        \"invalidImageFormat\": \"Формат изображения не поддерживается. Поддерживаемые форматы: JPEG, PNG, JPG, GIF, SVG, WEBP\",\n        \"invalidImageUrl\": \"Недействительный URL изображения\",\n        \"noImage\": \"Нет такого файла или каталога\",\n        \"multipleImagesFailed\": \"Одно или несколько изображений не удалось загрузить, пожалуйста, попробуйте снова\"\n      },\n      \"embedLink\": {\n        \"label\": \"Встроить ссылку\",\n        \"placeholder\": \"Вставьте или введите ссылку на изображение\"\n      },\n      \"unsplash\": {\n        \"label\": \"Unsplash\"\n      },\n      \"searchForAnImage\": \"Поиск изображения\",\n      \"pleaseInputYourOpenAIKey\": \"пожалуйста, введите ваш ключ AI на странице настроек\",\n      \"saveImageToGallery\": \"Сохранить изображение\",\n      \"failedToAddImageToGallery\": \"Не удалось сохранить изображение\",\n      \"successToAddImageToGallery\": \"Изображение сохранено в Фото\",\n      \"unableToLoadImage\": \"Невозможно загрузить изображение\",\n      \"maximumImageSize\": \"Максимальный поддерживаемый размер загружаемого изображения - 10 МБ\",\n      \"uploadImageErrorImageSizeTooBig\": \"Размер изображения должен быть меньше 10 МБ\",\n      \"imageIsUploading\": \"Изображение загружается\",\n      \"openFullScreen\": \"Открыть на весь экран\",\n      \"interactiveViewer\": {\n        \"toolbar\": {\n          \"previousImageTooltip\": \"Предыдущее изображение\",\n          \"nextImageTooltip\": \"Следующее изображение\",\n          \"zoomOutTooltip\": \"Уменьшить\",\n          \"zoomInTooltip\": \"Увеличить\",\n          \"changeZoomLevelTooltip\": \"Изменить уровень масштабирования\",\n          \"openLocalImage\": \"Открыть изображение\",\n          \"downloadImage\": \"Скачать изображение\",\n          \"closeViewer\": \"Закрыть интерактивный просмотрщик\",\n          \"scalePercentage\": \"{}%\",\n          \"deleteImageTooltip\": \"Удалить изображение\"\n        }\n      }\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Язык\",\n        \"placeholder\": \"Выбрать язык\",\n        \"auto\": \"Авто\"\n      },\n      \"copyTooltip\": \"Копировать\",\n      \"searchLanguageHint\": \"Поиск языка\",\n      \"codeCopiedSnackbar\": \"Код скопирован в буфер обмена!\"\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Вставьте или введите ссылку\",\n      \"openInNewTab\": \"Открыть в новой вкладке\",\n      \"copyLink\": \"Скопировать ссылку\",\n      \"removeLink\": \"Удалить ссылку\",\n      \"url\": {\n        \"label\": \"URL ссылки\",\n        \"placeholder\": \"Введите URL ссылки\"\n      },\n      \"title\": {\n        \"label\": \"Название ссылки\",\n        \"placeholder\": \"Введите название ссылки\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"Упомянуть человека или страницу или дату...\",\n      \"page\": {\n        \"label\": \"Ссылка на страницу\",\n        \"tooltip\": \"Нажмите, чтобы открыть страницу\"\n      },\n      \"deleted\": \"Удалено\",\n      \"deletedContent\": \"Этот контент не существует или был удален\",\n      \"noAccess\": \"Нет доступа\",\n      \"deletedPage\": \"Удаленная страница\",\n      \"trashHint\": \" - в корзине\",\n      \"morePages\": \"больше страниц\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"Сбросить до шрифта по умолчанию\",\n      \"textSize\": \"Размер текста\",\n      \"textColor\": \"Цвет текста\",\n      \"h1\": \"Заголовок 1\",\n      \"h2\": \"Заголовок 2\",\n      \"h3\": \"Заголовок 3\",\n      \"alignLeft\": \"Выровнять по левому краю\",\n      \"alignRight\": \"Выровнять по правому краю\",\n      \"alignCenter\": \"Выровнять по центру\",\n      \"link\": \"Ссылка\",\n      \"textAlign\": \"Выравнивание текста\",\n      \"moreOptions\": \"Больше опций\",\n      \"font\": \"Шрифт\",\n      \"inlineCode\": \"Встроенный код\",\n      \"suggestions\": \"Предложения\",\n      \"turnInto\": \"Превратить в\",\n      \"equation\": \"Уравнение\",\n      \"insert\": \"Вставить\",\n      \"linkInputHint\": \"Вставьте ссылку или поиск по страницам\",\n      \"pageOrURL\": \"Страница или URL\",\n      \"linkName\": \"Название ссылки\",\n      \"linkNameHint\": \"Введите название ссылки\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"Не удалось разобрать содержимое блока\",\n      \"clickToCopyTheBlockContent\": \"Нажмите, чтобы скопировать содержимое блока\",\n      \"blockContentHasBeenCopied\": \"Содержимое блока скопировано.\",\n      \"parseError\": \"Произошла ошибка при разборе блока {}.\",\n      \"copyBlockContent\": \"Скопировать содержимое блока\"\n    },\n    \"mobilePageSelector\": {\n      \"title\": \"Выбрать страницу\",\n      \"failedToLoad\": \"Не удалось загрузить список страниц\",\n      \"noPagesFound\": \"Страницы не найдены\"\n    },\n    \"attachmentMenu\": {\n      \"choosePhoto\": \"Выбрать фото\",\n      \"takePicture\": \"Сделать снимок\",\n      \"chooseFile\": \"Выбрать файл\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"label\": \"Столбец\",\n      \"createNewCard\": \"Новый\",\n      \"renameGroupTooltip\": \"Нажмите, чтобы переименовать группу\",\n      \"createNewColumn\": \"Добавить новую группу\",\n      \"addToColumnTopTooltip\": \"Добавить новую карточку в начало\",\n      \"addToColumnBottomTooltip\": \"Добавить новую карточку в конец\",\n      \"renameColumn\": \"Переименовать\",\n      \"hideColumn\": \"Скрыть\",\n      \"newGroup\": \"Новая группа\",\n      \"deleteColumn\": \"Удалить\",\n      \"deleteColumnConfirmation\": \"Это приведет к удалению этой группы и всех карточек в ней. Вы уверены, что хотите продолжить?\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"Скрытые группы\",\n      \"collapseTooltip\": \"Скрыть скрытые группы\",\n      \"expandTooltip\": \"Просмотреть скрытые группы\"\n    },\n    \"cardDetail\": \"Детали карточки\",\n    \"cardActions\": \"Действия с карточкой\",\n    \"cardDuplicated\": \"Карточка дублирована\",\n    \"cardDeleted\": \"Карточка удалена\",\n    \"showOnCard\": \"Показать на карточке\",\n    \"setting\": \"Настройки\",\n    \"propertyName\": \"Название свойства\",\n    \"menuName\": \"Доска\",\n    \"showUngrouped\": \"Показать негруппированные элементы\",\n    \"ungroupedButtonText\": \"Негруппированные\",\n    \"ungroupedButtonTooltip\": \"Содержит карточки, которые не принадлежат ни одной группе\",\n    \"ungroupedItemsTitle\": \"Нажмите, чтобы добавить на доску\",\n    \"groupBy\": \"Группировать по\",\n    \"groupCondition\": \"Условие группировки\",\n    \"referencedBoardPrefix\": \"Вид\",\n    \"notesTooltip\": \"Заметки внутри\",\n    \"mobile\": {\n      \"editURL\": \"Редактировать URL\",\n      \"showGroup\": \"Показать группу\",\n      \"showGroupContent\": \"Вы уверены, что хотите показать эту группу на доске?\",\n      \"failedToLoad\": \"Не удалось загрузить вид доски\"\n    },\n    \"dateCondition\": {\n      \"weekOf\": \"Неделя с {} по {}\",\n      \"today\": \"Сегодня\",\n      \"yesterday\": \"Вчера\",\n      \"tomorrow\": \"Завтра\",\n      \"lastSevenDays\": \"Последние 7 дней\",\n      \"nextSevenDays\": \"Следующие 7 дней\",\n      \"lastThirtyDays\": \"Последние 30 дней\",\n      \"nextThirtyDays\": \"Следующие 30 дней\"\n    },\n    \"noGroup\": \"Нет свойства для группировки\",\n    \"noGroupDesc\": \"Виды доски требуют свойства для группировки для отображения\",\n    \"media\": {\n      \"cardText\": \"{} {}\",\n      \"fallbackName\": \"файлы\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Календарь\",\n    \"defaultNewCalendarTitle\": \"Без названия\",\n    \"newEventButtonTooltip\": \"Добавить новое событие\",\n    \"navigation\": {\n      \"today\": \"Сегодня\",\n      \"jumpToday\": \"Перейти на сегодня\",\n      \"previousMonth\": \"Предыдущий месяц\",\n      \"nextMonth\": \"Следующий месяц\",\n      \"views\": {\n        \"day\": \"День\",\n        \"week\": \"Неделя\",\n        \"month\": \"Месяц\",\n        \"year\": \"Год\"\n      }\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"Событий пока нет\",\n      \"emptyBody\": \"Нажмите кнопку плюса, чтобы создать событие в этот день.\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Показать номера недель\",\n      \"showWeekends\": \"Показать выходные\",\n      \"firstDayOfWeek\": \"Начать неделю с\",\n      \"layoutDateField\": \"Разместить календарь по\",\n      \"changeLayoutDateField\": \"Изменить поле макета\",\n      \"noDateTitle\": \"Нет даты\",\n      \"noDateHint\": {\n        \"zero\": \"Незапланированные события появятся здесь\",\n        \"one\": \"{count} незапланированное событие\",\n        \"other\": \"{count} незапланированных событий\"\n      },\n      \"unscheduledEventsTitle\": \"Незапланированные события\",\n      \"clickToAdd\": \"Нажмите, чтобы добавить в календарь\",\n      \"name\": \"Настройки календаря\",\n      \"clickToOpen\": \"Нажмите, чтобы открыть запись\"\n    },\n    \"referencedCalendarPrefix\": \"Вид\",\n    \"quickJumpYear\": \"Перейти на\",\n    \"duplicateEvent\": \"Дублировать событие\"\n  },\n  \"errorDialog\": {\n    \"title\": \"Ошибка @:appName\",\n    \"howToFixFallback\": \"Нам очень жаль! Сообщите о проблеме на нашей странице GitHub с описанием вашей ошибки.\",\n    \"howToFixFallbackHint1\": \"Нам очень жаль! Сообщите о проблеме на нашей \",\n    \"howToFixFallbackHint2\": \" странице с описанием вашей ошибки.\",\n    \"github\": \"Просмотреть на GitHub\"\n  },\n  \"search\": {\n    \"label\": \"Поиск\",\n    \"sidebarSearchIcon\": \"Поиск и быстрый переход на страницу\",\n    \"searchOrAskAI\": \"Поиск или спросить AI\",\n    \"askAIAnything\": \"Спросить AI что угодно\",\n    \"askAIFor\": \"Спросить AI\",\n    \"noResultForSearching\": \"Нет результатов для \\\"{}\\\"\",\n    \"noResultForSearchingHint\": \"Некоторые результаты могут находиться в удаленных страницах\",\n    \"bestMatch\": \"Наилучшее соответствие\",\n    \"placeholder\": {\n      \"actions\": \"Поиск действий...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Скопировано в буфер обмена\",\n      \"fail\": \"Невозможно скопировать\"\n    }\n  },\n  \"unSupportBlock\": \"Текущая версия не поддерживает этот блок.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Вы уверены, что хотите удалить {pageType}?\",\n    \"deleteContentCaption\": \"если вы удалите этот {pageType}, вы сможете восстановить его из корзины.\"\n  },\n  \"colors\": {\n    \"custom\": \"Пользовательский\",\n    \"default\": \"По умолчанию\",\n    \"red\": \"Красный\",\n    \"orange\": \"Оранжевый\",\n    \"yellow\": \"Желтый\",\n    \"green\": \"Зеленый\",\n    \"blue\": \"Синий\",\n    \"purple\": \"Фиолетовый\",\n    \"pink\": \"Розовый\",\n    \"brown\": \"Коричневый\",\n    \"gray\": \"Серый\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"Эмодзи\",\n    \"search\": \"Поиск эмодзи\",\n    \"noRecent\": \"Нет последних эмодзи\",\n    \"noEmojiFound\": \"Эмодзи не найдено\",\n    \"filter\": \"Фильтр\",\n    \"random\": \"Случайно\",\n    \"selectSkinTone\": \"Выбрать оттенок кожи\",\n    \"remove\": \"Удалить эмодзи\",\n    \"categories\": {\n      \"smileys\": \"Смайлики и эмоции\",\n      \"people\": \"люди\",\n      \"animals\": \"природа\",\n      \"food\": \"еда\",\n      \"activities\": \"занятия\",\n      \"places\": \"места\",\n      \"objects\": \"объекты\",\n      \"symbols\": \"символы\",\n      \"flags\": \"флаги\",\n      \"nature\": \"природа\",\n      \"frequentlyUsed\": \"часто используемые\"\n    },\n    \"skinTone\": {\n      \"default\": \"По умолчанию\",\n      \"light\": \"Светлый\",\n      \"mediumLight\": \"Средне-светлый\",\n      \"medium\": \"Средний\",\n      \"mediumDark\": \"Средне-темный\",\n      \"dark\": \"Темный\"\n    },\n    \"openSourceIconsFrom\": \"Иконки с открытым исходным кодом из\"\n  },\n  \"inlineActions\": {\n    \"noResults\": \"Нет результатов\",\n    \"recentPages\": \"Последние страницы\",\n    \"pageReference\": \"Ссылка на страницу\",\n    \"docReference\": \"Ссылка на документ\",\n    \"boardReference\": \"Ссылка на доску\",\n    \"calReference\": \"Ссылка на календарь\",\n    \"gridReference\": \"Ссылка на таблицу\",\n    \"date\": \"Дата\",\n    \"reminder\": {\n      \"groupTitle\": \"Напоминание\",\n      \"shortKeyword\": \"напомнить\"\n    },\n    \"createPage\": \"Создать подстраницу \\\"{}\\\"\"\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"Изменить формат даты и времени в настройках\",\n    \"dateFormat\": \"Формат даты\",\n    \"includeTime\": \"Включать время\",\n    \"isRange\": \"Дата окончания\",\n    \"timeFormat\": \"Формат времени\",\n    \"clearDate\": \"Очистить дату\",\n    \"reminderLabel\": \"Напоминание\",\n    \"selectReminder\": \"Выбрать напоминание\",\n    \"reminderOptions\": {\n      \"none\": \"Нет\",\n      \"atTimeOfEvent\": \"Время события\",\n      \"fiveMinsBefore\": \"За 5 минут до\",\n      \"tenMinsBefore\": \"За 10 минут до\",\n      \"fifteenMinsBefore\": \"За 15 минут до\",\n      \"thirtyMinsBefore\": \"За 30 минут до\",\n      \"oneHourBefore\": \"За 1 час до\",\n      \"twoHoursBefore\": \"За 2 часа до\",\n      \"onDayOfEvent\": \"В день события\",\n      \"oneDayBefore\": \"За 1 день до\",\n      \"twoDaysBefore\": \"За 2 дня до\",\n      \"oneWeekBefore\": \"За 1 неделю до\",\n      \"custom\": \"Пользовательский\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"Вчера\",\n    \"today\": \"Сегодня\",\n    \"tomorrow\": \"Завтра\",\n    \"oneWeek\": \"1 неделя\"\n  },\n  \"notificationHub\": {\n    \"title\": \"Уведомления\",\n    \"closeNotification\": \"Закрыть уведомление\",\n    \"viewNotifications\": \"Просмотреть уведомления\",\n    \"noNotifications\": \"Уведомлений пока нет\",\n    \"mentionedYou\": \"Упомянул(а) вас\",\n    \"markAsReadTooltip\": \"Отметить это уведомление как прочитанное\",\n    \"markAsReadSucceedToast\": \"Отмечено как прочитанное успешно\",\n    \"markAllAsReadSucceedToast\": \"Все успешно отмечено как прочитанное\",\n    \"today\": \"Сегодня\",\n    \"older\": \"Старше\",\n    \"mobile\": {\n      \"title\": \"Обновления\"\n    },\n    \"emptyTitle\": \"Все прочитано!\",\n    \"emptyBody\": \"Нет ожидающих уведомлений или действий. Наслаждайтесь спокойствием.\",\n    \"tabs\": {\n      \"inbox\": \"Входящие\",\n      \"upcoming\": \"Предстоящие\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"Отметить все как прочитанное\",\n      \"showAll\": \"Все\",\n      \"showUnreads\": \"Непрочитанные\"\n    },\n    \"filters\": {\n      \"ascending\": \"По возрастанию\",\n      \"descending\": \"По убыванию\",\n      \"groupByDate\": \"Группировать по дате\",\n      \"showUnreadsOnly\": \"Показать только непрочитанные\",\n      \"resetToDefault\": \"Сбросить до значений по умолчанию\"\n    },\n    \"archievedTooltip\": \"Архивировать это уведомление\",\n    \"unarchievedTooltip\": \"Разархивировать это уведомление\",\n    \"markAsArchievedSucceedToast\": \"Успешно заархивировано\",\n    \"markAllAsArchievedSucceedToast\": \"Все успешно заархивировано\"\n  },\n  \"reminderNotification\": {\n    \"title\": \"Напоминание\",\n    \"message\": \"Не забудьте проверить это, прежде чем забудете!\",\n    \"tooltipDelete\": \"Удалить\",\n    \"tooltipMarkRead\": \"Отметить как прочитанное\",\n    \"tooltipMarkUnread\": \"Отметить как непрочитанное\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"Найти\",\n    \"previousMatch\": \"Предыдущее совпадение\",\n    \"nextMatch\": \"Следующее совпадение\",\n    \"close\": \"Закрыть\",\n    \"replace\": \"Заменить\",\n    \"replaceAll\": \"Заменить все\",\n    \"noResult\": \"Нет результатов\",\n    \"caseSensitive\": \"Учитывать регистр\",\n    \"searchMore\": \"Ищите, чтобы найти больше результатов\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"Просим прощения\",\n    \"loadingViewError\": \"У нас возникли проблемы с загрузкой этого вида. Пожалуйста, проверьте подключение к интернету, обновите приложение и не стесняйтесь обратиться к команде, если проблема сохранится.\",\n    \"syncError\": \"Данные не синхронизированы с другого устройства\",\n    \"syncErrorHint\": \"Пожалуйста, откройте эту страницу на устройстве, где она была последний раз отредактирована, затем снова откройте ее на текущем устройстве.\",\n    \"clickToCopy\": \"Нажмите, чтобы скопировать код ошибки\"\n  },\n  \"editor\": {\n    \"bold\": \"Жирный\",\n    \"bulletedList\": \"Маркированный список\",\n    \"bulletedListShortForm\": \"Маркированный\",\n    \"checkbox\": \"Чекбокс\",\n    \"embedCode\": \"Встроить код\",\n    \"heading1\": \"H1\",\n    \"heading2\": \"H2\",\n    \"heading3\": \"H3\",\n    \"highlight\": \"Выделить\",\n    \"color\": \"Цвет\",\n    \"image\": \"Изображение\",\n    \"date\": \"Дата\",\n    \"page\": \"Страница\",\n    \"italic\": \"Курсив\",\n    \"link\": \"Ссылка\",\n    \"numberedList\": \"Нумерованный список\",\n    \"numberedListShortForm\": \"Нумерованный\",\n    \"toggleHeading1ShortForm\": \"Переключить H1\",\n    \"toggleHeading2ShortForm\": \"Переключить H2\",\n    \"toggleHeading3ShortForm\": \"Переключить H3\",\n    \"quote\": \"Цитата\",\n    \"strikethrough\": \"Зачеркнутый\",\n    \"text\": \"Текст\",\n    \"underline\": \"Подчеркнутый\",\n    \"fontColorDefault\": \"По умолчанию\",\n    \"fontColorGray\": \"Серый\",\n    \"fontColorBrown\": \"Коричневый\",\n    \"fontColorOrange\": \"Оранжевый\",\n    \"fontColorYellow\": \"Желтый\",\n    \"fontColorGreen\": \"Зеленый\",\n    \"fontColorBlue\": \"Синий\",\n    \"fontColorPurple\": \"Фиолетовый\",\n    \"fontColorPink\": \"Розовый\",\n    \"fontColorRed\": \"Красный\",\n    \"backgroundColorDefault\": \"Фон по умолчанию\",\n    \"backgroundColorGray\": \"Серый фон\",\n    \"backgroundColorBrown\": \"Коричневый фон\",\n    \"backgroundColorOrange\": \"Оранжевый фон\",\n    \"backgroundColorYellow\": \"Желтый фон\",\n    \"backgroundColorGreen\": \"Зеленый фон\",\n    \"backgroundColorBlue\": \"Синий фон\",\n    \"backgroundColorPurple\": \"Фиолетовый фон\",\n    \"backgroundColorPink\": \"Розовый фон\",\n    \"backgroundColorRed\": \"Красный фон\",\n    \"backgroundColorLime\": \"Фон лайм\",\n    \"backgroundColorAqua\": \"Фон аква\",\n    \"done\": \"Готово\",\n    \"cancel\": \"Отмена\",\n    \"tint1\": \"Оттенок 1\",\n    \"tint2\": \"Оттенок 2\",\n    \"tint3\": \"Оттенок 3\",\n    \"tint4\": \"Оттенок 4\",\n    \"tint5\": \"Оттенок 5\",\n    \"tint6\": \"Оттенок 6\",\n    \"tint7\": \"Оттенок 7\",\n    \"tint8\": \"Оттенок 8\",\n    \"tint9\": \"Оттенок 9\",\n    \"lightLightTint1\": \"Фиолетовый\",\n    \"lightLightTint2\": \"Розовый\",\n    \"lightLightTint3\": \"Светло-розовый\",\n    \"lightLightTint4\": \"Оранжевый\",\n    \"lightLightTint5\": \"Желтый\",\n    \"lightLightTint6\": \"Лайм\",\n    \"lightLightTint7\": \"Зеленый\",\n    \"lightLightTint8\": \"Аква\",\n    \"lightLightTint9\": \"Синий\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"Заголовок 1\",\n    \"mobileHeading2\": \"Заголовок 2\",\n    \"mobileHeading3\": \"Заголовок 3\",\n    \"mobileHeading4\": \"Заголовок 4\",\n    \"mobileHeading5\": \"Заголовок 5\",\n    \"mobileHeading6\": \"Заголовок 6\",\n    \"textColor\": \"Цвет текста\",\n    \"backgroundColor\": \"Цвет фона\",\n    \"addYourLink\": \"Добавьте свою ссылку\",\n    \"openLink\": \"Открыть ссылку\",\n    \"copyLink\": \"Скопировать ссылку\",\n    \"removeLink\": \"Удалить ссылку\",\n    \"editLink\": \"Редактировать ссылку\",\n    \"convertTo\": \"Преобразовать в\",\n    \"linkText\": \"Текст\",\n    \"linkTextHint\": \"Пожалуйста, введите текст\",\n    \"linkAddressHint\": \"Пожалуйста, введите URL\",\n    \"highlightColor\": \"Цвет выделения\",\n    \"clearHighlightColor\": \"Очистить цвет выделения\",\n    \"customColor\": \"Пользовательский цвет\",\n    \"hexValue\": \"Значение Hex\",\n    \"opacity\": \"Непрозрачность\",\n    \"resetToDefaultColor\": \"Сбросить до цвета по умолчанию\",\n    \"ltr\": \"Слева направо\",\n    \"rtl\": \"Справа налево\",\n    \"auto\": \"Авто\",\n    \"cut\": \"Вырезать\",\n    \"copy\": \"Копировать\",\n    \"paste\": \"Вставить\",\n    \"find\": \"Найти\",\n    \"select\": \"Выбрать\",\n    \"selectAll\": \"Выбрать все\",\n    \"previousMatch\": \"Предыдущее совпадение\",\n    \"nextMatch\": \"Следующее совпадение\",\n    \"closeFind\": \"Закрыть\",\n    \"replace\": \"Заменить\",\n    \"replaceAll\": \"Заменить все\",\n    \"regex\": \"Регулярное выражение\",\n    \"caseSensitive\": \"Учитывать регистр\",\n    \"uploadImage\": \"Загрузить изображение\",\n    \"urlImage\": \"Изображение по URL\",\n    \"incorrectLink\": \"Неверная ссылка\",\n    \"upload\": \"Загрузить\",\n    \"chooseImage\": \"Выберите изображение\",\n    \"loading\": \"Загрузка\",\n    \"imageLoadFailed\": \"Не удалось загрузить изображение\",\n    \"divider\": \"Разделитель\",\n    \"table\": \"Таблица\",\n    \"colAddBefore\": \"Добавить до\",\n    \"rowAddBefore\": \"Добавить до\",\n    \"colAddAfter\": \"Добавить после\",\n    \"rowAddAfter\": \"Добавить после\",\n    \"colRemove\": \"Удалить\",\n    \"rowRemove\": \"Удалить\",\n    \"colDuplicate\": \"Дублировать\",\n    \"rowDuplicate\": \"Дублировать\",\n    \"colClear\": \"Очистить содержимое\",\n    \"rowClear\": \"Очистить содержимое\",\n    \"slashPlaceHolder\": \"Введите '/', чтобы вставить блок, или начните печатать\",\n    \"typeSomething\": \"Введите что-нибудь...\",\n    \"toggleListShortForm\": \"Переключить\",\n    \"quoteListShortForm\": \"Цитата\",\n    \"mathEquationShortForm\": \"Формула\",\n    \"codeBlockShortForm\": \"Код\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"Нет избранных страниц\",\n    \"noFavoriteHintText\": \"Смахните страницу влево, чтобы добавить ее в избранное\",\n    \"removeFromSidebar\": \"Удалить из боковой панели\",\n    \"addToSidebar\": \"Закрепить на боковой панели\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"Введите / для вставки блока, или начните печатать\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"Список дел\",\n    \"bulletList\": \"Список\",\n    \"numberList\": \"Список\",\n    \"quote\": \"Цитата\",\n    \"heading\": \"Заголовок {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"Иконка страницы\",\n    \"language\": \"Язык\",\n    \"font\": \"Шрифт\",\n    \"actions\": \"Действия\",\n    \"date\": \"Дата\",\n    \"addField\": \"Добавить поле\",\n    \"userIcon\": \"Иконка пользователя\"\n  },\n  \"noLogFiles\": \"Нет файлов логов\",\n  \"newSettings\": {\n    \"myAccount\": {\n      \"title\": \"Аккаунт и Приложение\",\n      \"subtitle\": \"Настройте свой профиль, управляйте безопасностью аккаунта, ключами AI или войдите в свой аккаунт.\",\n      \"profileLabel\": \"Имя аккаунта и фото профиля\",\n      \"profileNamePlaceholder\": \"Введите ваше имя\",\n      \"accountSecurity\": \"Безопасность аккаунта\",\n      \"2FA\": \"Двухфакторная аутентификация\",\n      \"aiKeys\": \"Ключи AI\",\n      \"accountLogin\": \"Вход в аккаунт\",\n      \"updateNameError\": \"Не удалось обновить имя\",\n      \"updateIconError\": \"Не удалось обновить иконку\",\n      \"aboutAppFlowy\": \"О @:appName\",\n      \"deleteAccount\": {\n        \"title\": \"Удалить аккаунт\",\n        \"subtitle\": \"Окончательно удалить ваш аккаунт и все ваши данные.\",\n        \"description\": \"Окончательно удалить ваш аккаунт и удалить доступ из всех рабочих пространств.\",\n        \"deleteMyAccount\": \"Удалить мой аккаунт\",\n        \"dialogTitle\": \"Удалить аккаунт\",\n        \"dialogContent1\": \"Вы уверены, что хотите окончательно удалить свой аккаунт?\",\n        \"dialogContent2\": \"Это действие нельзя отменить, и оно удалит доступ из всех рабочих пространств, стерев весь ваш аккаунт, включая приватные рабочие пространства, и удалив вас из всех общих рабочих пространств.\",\n        \"confirmHint1\": \"Пожалуйста, введите \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\" для подтверждения.\",\n        \"confirmHint2\": \"Я понимаю, что это действие необратимо и навсегда удалит мой аккаунт и все связанные данные.\",\n        \"confirmHint3\": \"УДАЛИТЬ МОЙ АККАУНТ\",\n        \"checkToConfirmError\": \"Вы должны отметить галочкой для подтверждения удаления\",\n        \"failedToGetCurrentUser\": \"Не удалось получить email текущего пользователя\",\n        \"confirmTextValidationFailed\": \"Ваш текст подтверждения не совпадает с \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\"\",\n        \"deleteAccountSuccess\": \"Аккаунт успешно удален\"\n      },\n      \"password\": {\n        \"title\": \"Пароль\",\n        \"confirmPassword\": \"Подтвердить пароль\",\n        \"changePassword\": \"Изменить пароль\",\n        \"currentPassword\": \"Текущий пароль\",\n        \"newPassword\": \"Новый пароль\",\n        \"confirmNewPassword\": \"Подтвердить новый пароль\",\n        \"setupPassword\": \"Установить пароль\",\n        \"error\": {\n          \"currentPasswordIsRequired\": \"Текущий пароль обязателен\",\n          \"newPasswordIsRequired\": \"Новый пароль обязателен\",\n          \"confirmPasswordIsRequired\": \"Подтверждение пароля обязательно\",\n          \"passwordsDoNotMatch\": \"Пароли не совпадают\",\n          \"newPasswordIsSameAsCurrent\": \"Новый пароль совпадает с текущим\"\n        },\n        \"toast\": {\n          \"passwordUpdatedSuccessfully\": \"Пароль успешно обновлен\",\n          \"passwordUpdatedFailed\": \"Не удалось обновить пароль\",\n          \"passwordSetupSuccessfully\": \"Пароль успешно установлен\",\n          \"passwordSetupFailed\": \"Не удалось установить пароль\"\n        },\n        \"hint\": {\n          \"enterYourPassword\": \"Введите ваш пароль\",\n          \"confirmYourPassword\": \"Подтвердите ваш пароль\",\n          \"enterYourCurrentPassword\": \"Введите ваш текущий пароль\",\n          \"enterYourNewPassword\": \"Введите ваш новый пароль\",\n          \"confirmYourNewPassword\": \"Подтвердите ваш новый пароль\"\n        }\n      },\n      \"myAccount\": \"Мой аккаунт\",\n      \"myProfile\": \"Мой профиль\"\n    },\n    \"workplace\": {\n      \"name\": \"Рабочее место\",\n      \"title\": \"Настройки рабочего места\",\n      \"subtitle\": \"Настройте внешний вид рабочего пространства, тему, шрифт, макет текста, дату, время и язык.\",\n      \"workplaceName\": \"Название рабочего места\",\n      \"workplaceNamePlaceholder\": \"Введите название рабочего места\",\n      \"workplaceIcon\": \"Иконка рабочего места\",\n      \"workplaceIconSubtitle\": \"Загрузите изображение или используйте эмодзи для вашего рабочего пространства. Иконка будет отображаться в боковой панели и уведомлениях.\",\n      \"renameError\": \"Не удалось переименовать рабочее место\",\n      \"updateIconError\": \"Не удалось обновить иконку\",\n      \"chooseAnIcon\": \"Выберите иконку\",\n      \"appearance\": {\n        \"name\": \"Внешний вид\",\n        \"themeMode\": {\n          \"auto\": \"Авто\",\n          \"light\": \"Светлая\",\n          \"dark\": \"Темная\"\n        },\n        \"language\": \"Язык\"\n      }\n    },\n    \"syncState\": {\n      \"syncing\": \"Синхронизация\",\n      \"synced\": \"Синхронизировано\",\n      \"noNetworkConnected\": \"Нет подключения к сети\"\n    }\n  },\n  \"pageStyle\": {\n    \"title\": \"Стиль страницы\",\n    \"layout\": \"Макет\",\n    \"coverImage\": \"Обложка\",\n    \"pageIcon\": \"Иконка страницы\",\n    \"colors\": \"Цвета\",\n    \"gradient\": \"Градиент\",\n    \"backgroundImage\": \"Фоновое изображение\",\n    \"presets\": \"Пресеты\",\n    \"photo\": \"Фото\",\n    \"unsplash\": \"Unsplash\",\n    \"pageCover\": \"Обложка страницы\",\n    \"none\": \"Нет\",\n    \"openSettings\": \"Открыть настройки\",\n    \"photoPermissionTitle\": \"@:appName хочет получить доступ к вашей фотогалерее\",\n    \"photoPermissionDescription\": \"@:appName нужен доступ к вашим фото, чтобы вы могли добавлять изображения в документы\",\n    \"cameraPermissionTitle\": \"@:appName хочет получить доступ к вашей камере\",\n    \"cameraPermissionDescription\": \"@:appName нужен доступ к вашей камере, чтобы вы могли добавлять изображения в документы с камеры\",\n    \"doNotAllow\": \"Не разрешать\",\n    \"image\": \"Изображение\"\n  },\n  \"commandPalette\": {\n    \"placeholder\": \"Поиск или задать вопрос...\",\n    \"bestMatches\": \"Лучшие совпадения\",\n    \"aiOverview\": \"Обзор AI\",\n    \"aiOverviewSource\": \"Источники ссылок\",\n    \"aiOverviewMoreDetails\": \"Подробнее\",\n    \"pagePreview\": \"Предпросмотр содержимого\",\n    \"clickToOpenPage\": \"Нажмите, чтобы открыть страницу\",\n    \"recentHistory\": \"Недавняя история\",\n    \"navigateHint\": \"для навигации\",\n    \"loadingTooltip\": \"Идет поиск результатов...\",\n    \"betaLabel\": \"БЕТА\",\n    \"betaTooltip\": \"В настоящее время поддерживается поиск только страниц и содержимого документов\",\n    \"fromTrashHint\": \"Из корзины\",\n    \"noResultsHint\": \"Мы не нашли то, что вы ищете, попробуйте поискать другой запрос.\",\n    \"clearSearchTooltip\": \"Очистить поле поиска\"\n  },\n  \"space\": {\n    \"delete\": \"Удалить\",\n    \"deleteConfirmation\": \"Удалить: \",\n    \"deleteConfirmationDescription\": \"Все страницы в этом пространстве будут удалены и перемещены в корзину, а все опубликованные страницы будут сняты с публикации.\",\n    \"rename\": \"Переименовать пространство\",\n    \"changeIcon\": \"Изменить иконку\",\n    \"manage\": \"Управлять пространством\",\n    \"addNewSpace\": \"Создать пространство\",\n    \"collapseAllSubPages\": \"Свернуть все подстраницы\",\n    \"createNewSpace\": \"Создать новое пространство\",\n    \"createSpaceDescription\": \"Создавайте несколько публичных и приватных пространств для лучшей организации работы.\",\n    \"spaceName\": \"Название пространства\",\n    \"spaceNamePlaceholder\": \"например, Маркетинг, Разработка, HR\",\n    \"permission\": \"Разрешение пространства\",\n    \"publicPermission\": \"Публичное\",\n    \"publicPermissionDescription\": \"Всем участникам рабочего пространства с полным доступом\",\n    \"privatePermission\": \"Приватное\",\n    \"privatePermissionDescription\": \"Только вы можете получить доступ к этому пространству\",\n    \"spaceIconBackground\": \"Цвет фона\",\n    \"spaceIcon\": \"Иконка\",\n    \"dangerZone\": \"Опасная зона\",\n    \"unableToDeleteLastSpace\": \"Не удалось удалить последнее пространство\",\n    \"unableToDeleteSpaceNotCreatedByYou\": \"Невозможно удалить пространства, созданные не вами\",\n    \"enableSpacesForYourWorkspace\": \"Включить пространства для вашего рабочего пространства\",\n    \"title\": \"Пространства\",\n    \"defaultSpaceName\": \"Общее\",\n    \"upgradeSpaceTitle\": \"Включить пространства\",\n    \"upgradeSpaceDescription\": \"Создавайте несколько публичных и приватных пространств для лучшей организации вашего рабочего пространства.\",\n    \"upgrade\": \"Обновить\",\n    \"upgradeYourSpace\": \"Создайте несколько пространств\",\n    \"quicklySwitch\": \"Быстро переключиться на следующее пространство\",\n    \"duplicate\": \"Дублировать пространство\",\n    \"movePageToSpace\": \"Переместить страницу в пространство\",\n    \"cannotMovePageToDatabase\": \"Невозможно переместить страницу в базу данных\",\n    \"switchSpace\": \"Переключить пространство\",\n    \"spaceNameCannotBeEmpty\": \"Имя пространства не может быть пустым\",\n    \"success\": {\n      \"deleteSpace\": \"Пространство успешно удалено\",\n      \"renameSpace\": \"Пространство успешно переименовано\",\n      \"duplicateSpace\": \"Пространство успешно дублировано\",\n      \"updateSpace\": \"Пространство успешно обновлено\"\n    },\n    \"error\": {\n      \"deleteSpace\": \"Не удалось удалить пространство\",\n      \"renameSpace\": \"Не удалось переименовать пространство\",\n      \"duplicateSpace\": \"Не удалось дублировать пространство\",\n      \"updateSpace\": \"Не удалось обновить пространство\"\n    },\n    \"createSpace\": \"Создать пространство\",\n    \"manageSpace\": \"Управлять пространством\",\n    \"renameSpace\": \"Переименовать пространство\",\n    \"mSpaceIconColor\": \"Цвет иконки пространства\",\n    \"mSpaceIcon\": \"Иконка пространства\"\n  },\n  \"publish\": {\n    \"hasNotBeenPublished\": \"Эта страница еще не опубликована\",\n    \"spaceHasNotBeenPublished\": \"Публикация пространства пока не поддерживается\",\n    \"reportPage\": \"Пожаловаться на страницу\",\n    \"databaseHasNotBeenPublished\": \"Публикация базы данных пока не поддерживается.\",\n    \"createdWith\": \"Создано с помощью\",\n    \"downloadApp\": \"Скачать AppFlowy\",\n    \"copy\": {\n      \"codeBlock\": \"Содержимое блока кода скопировано в буфер обмена\",\n      \"imageBlock\": \"Ссылка на изображение скопирована в буфер обмена\",\n      \"mathBlock\": \"Математическое уравнение скопировано в буфер обмена\",\n      \"fileBlock\": \"Ссылка на файл скопирована в буфер обмена\"\n    },\n    \"containsPublishedPage\": \"Эта страница содержит одну или несколько опубликованных страниц. Если вы продолжите, они будут сняты с публикации. Вы хотите продолжить удаление?\",\n    \"publishSuccessfully\": \"Успешно опубликовано\",\n    \"unpublishSuccessfully\": \"Успешно снято с публикации\",\n    \"publishFailed\": \"Не удалось опубликовать\",\n    \"unpublishFailed\": \"Не удалось снять с публикации\",\n    \"noAccessToVisit\": \"Нет доступа к этой странице...\",\n    \"createWithAppFlowy\": \"Создать сайт с помощью AppFlowy\",\n    \"fastWithAI\": \"Быстро и легко с помощью AI.\",\n    \"tryItNow\": \"Попробуйте сейчас\",\n    \"onlyGridViewCanBePublished\": \"Может быть опубликован только вид Таблица\",\n    \"database\": {\n      \"zero\": \"Опубликовать {} выбранных видов\",\n      \"one\": \"Опубликовать {} выбранный вид\",\n      \"many\": \"Опубликовать {} выбранных видов\",\n      \"other\": \"Опубликовать {} выбранных видов\"\n    },\n    \"mustSelectPrimaryDatabase\": \"Необходимо выбрать основной вид\",\n    \"noDatabaseSelected\": \"База данных не выбрана, выберите хотя бы одну.\",\n    \"unableToDeselectPrimaryDatabase\": \"Невозможно отменить выбор основной базы данных\",\n    \"saveThisPage\": \"Начать с этого шаблона\",\n    \"duplicateTitle\": \"Куда вы хотите добавить\",\n    \"selectWorkspace\": \"Выберите рабочее пространство\",\n    \"addTo\": \"Добавить в\",\n    \"duplicateSuccessfully\": \"Добавлено в ваше рабочее пространство\",\n    \"duplicateSuccessfullyDescription\": \"Не установлен AppFlowy? Загрузка начнется автоматически после нажатия 'Скачать'.\",\n    \"downloadIt\": \"Скачать\",\n    \"openApp\": \"Открыть в приложении\",\n    \"duplicateFailed\": \"Дублирование не удалось\",\n    \"membersCount\": {\n      \"zero\": \"Нет участников\",\n      \"one\": \"1 участник\",\n      \"many\": \"{count} участников\",\n      \"other\": \"{count} участников\"\n    },\n    \"useThisTemplate\": \"Использовать этот шаблон\"\n  },\n  \"web\": {\n    \"continue\": \"Продолжить\",\n    \"or\": \"или\",\n    \"continueWithGoogle\": \"Продолжить с Google\",\n    \"continueWithGithub\": \"Продолжить с GitHub\",\n    \"continueWithDiscord\": \"Продолжить с Discord\",\n    \"continueWithApple\": \"Продолжить с Apple\",\n    \"moreOptions\": \"Больше опций\",\n    \"collapse\": \"Свернуть\",\n    \"signInAgreement\": \"Нажимая «Продолжить» выше, вы соглашаетесь с \\n@:appName \",\n    \"signInLocalAgreement\": \"Нажимая «Начать» выше, вы соглашаетесь с \\n@:appName \",\n    \"and\": \"и\",\n    \"termOfUse\": \"Условиями использования\",\n    \"privacyPolicy\": \"Политикой конфиденциальности\",\n    \"signInError\": \"Ошибка входа\",\n    \"login\": \"Зарегистрироваться или войти\",\n    \"fileBlock\": {\n      \"uploadedAt\": \"Загружено {time}\",\n      \"linkedAt\": \"Ссылка добавлена {time}\",\n      \"empty\": \"Загрузить или встроить файл\",\n      \"uploadFailed\": \"Загрузка не удалась, пожалуйста, попробуйте снова\",\n      \"retry\": \"Повторить\"\n    },\n    \"importNotion\": \"Импорт из Notion\",\n    \"import\": \"Импорт\",\n    \"importSuccess\": \"Загружено успешно\",\n    \"importSuccessMessage\": \"Мы уведомим вас, когда импорт завершится. После этого вы сможете просмотреть импортированные страницы в боковой панели.\",\n    \"importFailed\": \"Импорт не удался, пожалуйста, проверьте формат файла\",\n    \"dropNotionFile\": \"Перетащите сюда ваш zip-файл Notion для загрузки или нажмите для обзора\",\n    \"error\": {\n      \"pageNameIsEmpty\": \"Имя страницы пусто, пожалуйста, попробуйте другое\"\n    }\n  },\n  \"globalComment\": {\n    \"comments\": \"Комментарии\",\n    \"addComment\": \"Добавить комментарий\",\n    \"reactedBy\": \"отреагировали:\",\n    \"addReaction\": \"Добавить реакцию\",\n    \"reactedByMore\": \"и еще {count}\",\n    \"showSeconds\": {\n      \"one\": \"1 секунду назад\",\n      \"other\": \"{count} секунд назад\",\n      \"zero\": \"Только что\",\n      \"many\": \"{count} секунд назад\"\n    },\n    \"showMinutes\": {\n      \"one\": \"1 минуту назад\",\n      \"other\": \"{count} минут назад\",\n      \"many\": \"{count} минут назад\"\n    },\n    \"showHours\": {\n      \"one\": \"1 час назад\",\n      \"other\": \"{count} часов назад\",\n      \"many\": \"{count} часов назад\"\n    },\n    \"showDays\": {\n      \"one\": \"1 день назад\",\n      \"other\": \"{count} дней назад\",\n      \"many\": \"{count} дней назад\"\n    },\n    \"showMonths\": {\n      \"one\": \"1 месяц назад\",\n      \"other\": \"{count} месяцев назад\",\n      \"many\": \"{count} месяцев назад\"\n    },\n    \"showYears\": {\n      \"one\": \"1 год назад\",\n      \"other\": \"{count} лет назад\",\n      \"many\": \"{count} лет назад\"\n    },\n    \"reply\": \"Ответить\",\n    \"deleteComment\": \"Удалить комментарий\",\n    \"youAreNotOwner\": \"Вы не владелец этого комментария\",\n    \"confirmDeleteDescription\": \"Вы уверены, что хотите удалить этот комментарий?\",\n    \"hasBeenDeleted\": \"Удалено\",\n    \"replyingTo\": \"Ответ на\",\n    \"noAccessDeleteComment\": \"Вам не разрешено удалять этот комментарий\",\n    \"collapse\": \"Свернуть\",\n    \"readMore\": \"Читать далее\",\n    \"failedToAddComment\": \"Не удалось добавить комментарий\",\n    \"commentAddedSuccessfully\": \"Комментарий успешно добавлен.\",\n    \"commentAddedSuccessTip\": \"Вы только что добавили или ответили на комментарий. Хотите перейти вверх, чтобы увидеть последние комментарии?\"\n  },\n  \"template\": {\n    \"asTemplate\": \"Сохранить как шаблон\",\n    \"name\": \"Название шаблона\",\n    \"description\": \"Описание шаблона\",\n    \"about\": \"О шаблоне\",\n    \"deleteFromTemplate\": \"Удалить из шаблонов\",\n    \"preview\": \"Предпросмотр шаблона\",\n    \"categories\": \"Категории шаблонов\",\n    \"isNewTemplate\": \"ЗАКРЕПИТЬ в Новые шаблоны\",\n    \"featured\": \"ЗАКРЕПИТЬ в Рекомендуемые\",\n    \"relatedTemplates\": \"Связанные шаблоны\",\n    \"requiredField\": \"{field} обязателен\",\n    \"addCategory\": \"Добавить \\\"{category}\\\"\",\n    \"addNewCategory\": \"Добавить новую категорию\",\n    \"addNewCreator\": \"Добавить нового автора\",\n    \"deleteCategory\": \"Удалить категорию\",\n    \"editCategory\": \"Редактировать категорию\",\n    \"editCreator\": \"Редактировать автора\",\n    \"category\": {\n      \"name\": \"Название категории\",\n      \"icon\": \"Иконка категории\",\n      \"bgColor\": \"Цвет фона категории\",\n      \"priority\": \"Приоритет категории\",\n      \"desc\": \"Описание категории\",\n      \"type\": \"Тип категории\",\n      \"icons\": \"Иконки категорий\",\n      \"colors\": \"Цвета категорий\",\n      \"byUseCase\": \"По варианту использования\",\n      \"byFeature\": \"По функции\",\n      \"deleteCategory\": \"Удалить категорию\",\n      \"deleteCategoryDescription\": \"Вы уверены, что хотите удалить эту категорию?\",\n      \"typeToSearch\": \"Введите для поиска категорий...\"\n    },\n    \"creator\": {\n      \"label\": \"Автор шаблона\",\n      \"name\": \"Имя автора\",\n      \"avatar\": \"Аватар автора\",\n      \"accountLinks\": \"Ссылки на аккаунт автора\",\n      \"uploadAvatar\": \"Нажмите, чтобы загрузить аватар\",\n      \"deleteCreator\": \"Удалить автора\",\n      \"deleteCreatorDescription\": \"Вы уверены, что хотите удалить этого автора?\",\n      \"typeToSearch\": \"Введите для поиска авторов...\"\n    },\n    \"uploadSuccess\": \"Шаблон успешно загружен\",\n    \"uploadSuccessDescription\": \"Ваш шаблон успешно загружен. Теперь вы можете просмотреть его в галерее шаблонов.\",\n    \"viewTemplate\": \"Просмотреть шаблон\",\n    \"deleteTemplate\": \"Удалить шаблон\",\n    \"deleteSuccess\": \"Шаблон успешно удален\",\n    \"deleteTemplateDescription\": \"Это не повлияет на текущую страницу или статус публикации. Вы уверены, что хотите удалить этот шаблон?\",\n    \"addRelatedTemplate\": \"Добавить связанный шаблон\",\n    \"removeRelatedTemplate\": \"Удалить связанный шаблон\",\n    \"uploadAvatar\": \"Загрузить аватар\",\n    \"searchInCategory\": \"Поиск в {category}\",\n    \"label\": \"Шаблоны\"\n  },\n  \"fileDropzone\": {\n    \"dropFile\": \"Нажмите или перетащите файл в эту область для загрузки\",\n    \"uploading\": \"Загрузка...\",\n    \"uploadFailed\": \"Загрузка не удалась\",\n    \"uploadSuccess\": \"Загрузка успешно\",\n    \"uploadSuccessDescription\": \"Файл успешно загружен\",\n    \"uploadFailedDescription\": \"Загрузка файла не удалась\",\n    \"uploadingDescription\": \"Файл загружается\"\n  },\n  \"gallery\": {\n    \"preview\": \"Открыть на весь экран\",\n    \"copy\": \"Копировать\",\n    \"download\": \"Скачать\",\n    \"prev\": \"Предыдущий\",\n    \"next\": \"Следующий\",\n    \"resetZoom\": \"Сбросить масштаб\",\n    \"zoomIn\": \"Увеличить\",\n    \"zoomOut\": \"Уменьшить\"\n  },\n  \"invitation\": {\n    \"join\": \"Присоединиться\",\n    \"on\": \"в\",\n    \"invitedBy\": \"Приглашен(а)\",\n    \"membersCount\": {\n      \"zero\": \"{count} участников\",\n      \"one\": \"{count} участник\",\n      \"many\": \"{count} участников\",\n      \"other\": \"{count} участников\"\n    },\n    \"tip\": \"Вас пригласили присоединиться к этому рабочему пространству, используя указанную ниже контактную информацию. Если это неверно, свяжитесь с администратором, чтобы отправить приглашение повторно.\",\n    \"joinWorkspace\": \"Присоединиться к рабочему пространству\",\n    \"success\": \"Вы успешно присоединились к рабочему пространству\",\n    \"successMessage\": \"Теперь у вас есть доступ ко всем страницам и рабочим пространствам в нем.\",\n    \"openWorkspace\": \"Открыть AppFlowy\",\n    \"alreadyAccepted\": \"Вы уже приняли приглашение\",\n    \"errorModal\": {\n      \"title\": \"Что-то пошло не так\",\n      \"description\": \"Ваш текущий аккаунт {email} может не иметь доступа к этому рабочему пространству. Пожалуйста, войдите с правильным аккаунтом или свяжитесь с владельцем рабочего пространства за помощью.\",\n      \"contactOwner\": \"Связаться с владельцем\",\n      \"close\": \"Назад на главную\",\n      \"changeAccount\": \"Сменить аккаунт\"\n    }\n  },\n  \"requestAccess\": {\n    \"title\": \"Нет доступа к этой странице\",\n    \"subtitle\": \"Вы можете запросить доступ у владельца этой страницы. После одобрения вы сможете просмотреть страницу.\",\n    \"requestAccess\": \"Запросить доступ\",\n    \"backToHome\": \"Назад на главную\",\n    \"tip\": \"Вы вошли как <link/>.\",\n    \"mightBe\": \"Возможно, вам нужно <login/> с другой учетной записью.\",\n    \"successful\": \"Запрос успешно отправлен\",\n    \"successfulMessage\": \"Вы будете уведомлены, как только владелец одобрит ваш запрос.\",\n    \"requestError\": \"Не удалось запросить доступ\",\n    \"repeatRequestError\": \"Вы уже запросили доступ к этой странице\"\n  },\n  \"approveAccess\": {\n    \"title\": \"Одобрить запрос на присоединение к рабочему пространству\",\n    \"requestSummary\": \"<user/> запрашивает присоединение к <workspace/> и доступ к <page/>\",\n    \"upgrade\": \"обновиться\",\n    \"downloadApp\": \"Скачать AppFlowy\",\n    \"approveButton\": \"Одобрить\",\n    \"approveSuccess\": \"Успешно одобрено\",\n    \"approveError\": \"Не удалось одобрить, убедитесь, что лимит плана рабочего пространства не превышен\",\n    \"getRequestInfoError\": \"Не удалось получить информацию о запросе\",\n    \"memberCount\": {\n      \"zero\": \"Нет участников\",\n      \"one\": \"1 участник\",\n      \"many\": \"{count} участников\",\n      \"other\": \"{count} участников\"\n    },\n    \"alreadyProTitle\": \"Вы достигли лимита плана рабочего пространства\",\n    \"alreadyProMessage\": \"Попросите их связаться с <email/>, чтобы разблокировать больше участников\",\n    \"repeatApproveError\": \"Вы уже одобрили этот запрос\",\n    \"ensurePlanLimit\": \"Убедитесь, что лимит плана рабочего пространства не превышен. Если лимит превышен, рассмотрите возможность <upgrade/> плана рабочего пространства или <download/>.\",\n    \"requestToJoin\": \"запросил(а) присоединиться\",\n    \"asMember\": \"как участник\"\n  },\n  \"upgradePlanModal\": {\n    \"title\": \"Обновиться до Pro\",\n    \"message\": \"{name} достиг бесплатного лимита участников. Обновитесь до плана Pro, чтобы пригласить больше участников.\",\n    \"upgradeSteps\": \"Как обновить план в AppFlowy:\",\n    \"step1\": \"1. Перейдите в Настройки\",\n    \"step2\": \"2. Нажмите на 'План'\",\n    \"step3\": \"3. Выберите 'Изменить план'\",\n    \"appNote\": \"Примечание: \",\n    \"actionButton\": \"Обновить\",\n    \"downloadLink\": \"Скачать приложение\",\n    \"laterButton\": \"Позже\",\n    \"refreshNote\": \"После успешного обновления нажмите <refresh/>, чтобы активировать новые функции.\",\n    \"refresh\": \"здесь\"\n  },\n  \"breadcrumbs\": {\n    \"label\": \"Хлебные крошки\"\n  },\n  \"time\": {\n    \"justNow\": \"Только что\",\n    \"seconds\": {\n      \"one\": \"1 секунда\",\n      \"other\": \"{count} секунд\"\n    },\n    \"minutes\": {\n      \"one\": \"1 минута\",\n      \"other\": \"{count} минут\"\n    },\n    \"hours\": {\n      \"one\": \"1 час\",\n      \"other\": \"{count} часов\"\n    },\n    \"days\": {\n      \"one\": \"1 день\",\n      \"other\": \"{count} дней\"\n    },\n    \"weeks\": {\n      \"one\": \"1 неделя\",\n      \"other\": \"{count} недель\"\n    },\n    \"months\": {\n      \"one\": \"1 месяц\",\n      \"other\": \"{count} месяцев\"\n    },\n    \"years\": {\n      \"one\": \"1 год\",\n      \"other\": \"{count} лет\"\n    },\n    \"ago\": \"назад\",\n    \"yesterday\": \"Вчера\",\n    \"today\": \"Сегодня\"\n  },\n  \"members\": {\n    \"zero\": \"Нет участников\",\n    \"one\": \"1 участник\",\n    \"many\": \"{count} участников\",\n    \"other\": \"{count} участников\"\n  },\n  \"tabMenu\": {\n    \"close\": \"Закрыть\",\n    \"closeDisabledHint\": \"Нельзя закрыть закрепленную вкладку, сначала открепите\",\n    \"closeOthers\": \"Закрыть другие вкладки\",\n    \"closeOthersHint\": \"Это закроет все незакрепленные вкладки, кроме этой\",\n    \"closeOthersDisabledHint\": \"Все вкладки закреплены, нет вкладок для закрытия\",\n    \"favorite\": \"Избранное\",\n    \"unfavorite\": \"Удалить из избранного\",\n    \"favoriteDisabledHint\": \"Невозможно добавить этот вид в избранное\",\n    \"pinTab\": \"Закрепить\",\n    \"unpinTab\": \"Открепить\"\n  },\n  \"openFileMessage\": {\n    \"success\": \"Файл успешно открыт\",\n    \"fileNotFound\": \"Файл не найден\",\n    \"noAppToOpenFile\": \"Нет приложения для открытия этого файла\",\n    \"permissionDenied\": \"Нет разрешения на открытие этого файла\",\n    \"unknownError\": \"Ошибка открытия файла\"\n  },\n  \"inviteMember\": {\n    \"requestInviteMembers\": \"Пригласить в рабочее пространство\",\n    \"inviteFailedMemberLimit\": \"Достигнут лимит участников, пожалуйста, \",\n    \"upgrade\": \"обновитесь\",\n    \"addEmail\": \"email@example.com, email2@example.com...\",\n    \"requestInvites\": \"Отправить приглашения\",\n    \"inviteAlready\": \"Вы уже приглашали этот email: {email}\",\n    \"inviteSuccess\": \"Приглашение успешно отправлено\",\n    \"description\": \"Введите адреса электронной почты ниже через запятую. Плата рассчитывается исходя из количества участников.\",\n    \"emails\": \"Email\"\n  },\n  \"quickNote\": {\n    \"label\": \"Быстрая заметка\",\n    \"quickNotes\": \"Быстрые заметки\",\n    \"search\": \"Поиск быстрых заметок\",\n    \"collapseFullView\": \"Свернуть полный вид\",\n    \"expandFullView\": \"Развернуть полный вид\",\n    \"createFailed\": \"Не удалось создать быструю заметку\",\n    \"quickNotesEmpty\": \"Нет быстрых заметок\",\n    \"emptyNote\": \"Пустая заметка\",\n    \"deleteNotePrompt\": \"Выбранная заметка будет удалена навсегда. Вы уверены, что хотите ее удалить?\",\n    \"addNote\": \"Новая заметка\",\n    \"noAdditionalText\": \"Нет дополнительного текста\"\n  },\n  \"subscribe\": {\n    \"upgradePlanTitle\": \"Сравнить и выбрать план\",\n    \"yearly\": \"Ежегодно\",\n    \"save\": \"Сэкономить {discount}%\",\n    \"monthly\": \"Ежемесячно\",\n    \"priceIn\": \"Цена в \",\n    \"free\": \"Бесплатно\",\n    \"pro\": \"Pro\",\n    \"freeDescription\": \"Для индивидуального использования до 2 участников для организации всего\",\n    \"proDescription\": \"Для небольших команд для управления проектами и знаниями команды\",\n    \"proDuration\": {\n      \"monthly\": \"за участника в месяц\\nежемесячно\",\n      \"yearly\": \"за участника в месяц\\nежегодно\"\n    },\n    \"cancel\": \"Понизить\",\n    \"changePlan\": \"Обновить до плана Pro\",\n    \"everythingInFree\": \"Все в бесплатном плане +\",\n    \"currentPlan\": \"Текущий\",\n    \"freeDuration\": \"навсегда\",\n    \"freePoints\": {\n      \"first\": \"1 совместное рабочее пространство до 2 участников\",\n      \"second\": \"Неограниченное количество страниц и блоков\",\n      \"three\": \"5 ГБ хранилища\",\n      \"four\": \"Интеллектуальный поиск\",\n      \"five\": \"20 ответов AI\",\n      \"six\": \"Мобильное приложение\",\n      \"seven\": \"Совместная работа в реальном времени\"\n    },\n    \"proPoints\": {\n      \"first\": \"Неограниченное хранилище\",\n      \"second\": \"До 10 участников рабочего пространства\",\n      \"three\": \"Неограниченные ответы AI\",\n      \"four\": \"Неограниченная загрузка файлов\",\n      \"five\": \"Пользовательское пространство имен\"\n    },\n    \"cancelPlan\": {\n      \"title\": \"Жаль видеть, что вы уходите\",\n      \"success\": \"Ваша подписка успешно отменена\",\n      \"description\": \"Нам жаль, что вы уходите. Мы будем рады услышать ваш отзыв, чтобы помочь нам улучшить AppFlowy. Пожалуйста, уделите минутку, чтобы ответить на несколько вопросов.\",\n      \"commonOther\": \"Другое\",\n      \"otherHint\": \"Напишите свой ответ здесь\",\n      \"questionOne\": {\n        \"question\": \"Что побудило вас отменить подписку на AppFlowy Pro?\",\n        \"answerOne\": \"Слишком высокая стоимость\",\n        \"answerTwo\": \"Функции не соответствовали ожиданиям\",\n        \"answerThree\": \"Нашел лучшую альтернативу\",\n        \"answerFour\": \"Использовал недостаточно для оправдания расходов\",\n        \"answerFive\": \"Проблема с сервисом или технические трудности\"\n      },\n      \"questionTwo\": {\n        \"question\": \"Насколько вероятно, что вы рассмотрите возможность повторной подписки на AppFlowy Pro в будущем?\",\n        \"answerOne\": \"Очень вероятно\",\n        \"answerTwo\": \"Несколько вероятно\",\n        \"answerThree\": \"Не уверен\",\n        \"answerFour\": \"Маловероятно\",\n        \"answerFive\": \"Очень маловероятно\"\n      },\n      \"questionThree\": {\n        \"question\": \"Какую функцию Pro вы ценили больше всего во время подписки?\",\n        \"answerOne\": \"Совместная работа нескольких пользователей\",\n        \"answerTwo\": \"Более долгая история версий\",\n        \"answerThree\": \"Неограниченные ответы AI\",\n        \"answerFour\": \"Доступ к локальным моделям AI\"\n      },\n      \"questionFour\": {\n        \"question\": \"Как бы вы описали свой общий опыт использования AppFlowy?\",\n        \"answerOne\": \"Отлично\",\n        \"answerTwo\": \"Хорошо\",\n        \"answerThree\": \"Средне\",\n        \"answerFour\": \"Ниже среднего\",\n        \"answerFive\": \"Недоволен\"\n      }\n    }\n  },\n  \"ai\": {\n    \"contentPolicyViolation\": \"Не удалось сгенерировать изображение из-за конфиденциального содержимого. Пожалуйста, переформулируйте запрос и попробуйте снова\",\n    \"textLimitReachedDescription\": \"В вашем рабочем пространстве закончились бесплатные ответы AI. Обновитесь до плана Pro или купите дополнение AI, чтобы разблокировать неограниченные ответы\",\n    \"imageLimitReachedDescription\": \"Вы использовали квоту на бесплатные изображения AI. Обновитесь до плана Pro или купите дополнение AI, чтобы разблокировать неограниченные ответы\",\n    \"limitReachedAction\": {\n      \"textDescription\": \"В вашем рабочем пространстве закончились бесплатные ответы AI. Чтобы получить больше ответов, пожалуйста, \",\n      \"imageDescription\": \"Вы использовали квоту на бесплатные изображения AI. Пожалуйста, \",\n      \"upgrade\": \"обновитесь\",\n      \"toThe\": \"до\",\n      \"proPlan\": \"плана Pro\",\n      \"orPurchaseAn\": \"или купите\",\n      \"aiAddon\": \"дополнение AI\"\n    },\n    \"editing\": \"Редактирование\",\n    \"analyzing\": \"Анализ\",\n    \"continueWritingEmptyDocumentTitle\": \"Ошибка продолжения написания\",\n    \"continueWritingEmptyDocumentDescription\": \"У нас возникли проблемы с продолжением контента в вашем документе. Напишите небольшое введение, и мы сможем продолжить с него!\",\n    \"more\": \"Еще\",\n    \"customPrompt\": {\n      \"browsePrompts\": \"Обзор запросов\",\n      \"usePrompt\": \"Использовать запрос\",\n      \"featured\": \"Рекомендуемые\",\n      \"example\": \"Пример\",\n      \"all\": \"Все\",\n      \"development\": \"Разработка\",\n      \"writing\": \"Написание\",\n      \"healthAndFitness\": \"Здоровье и фитнес\",\n      \"business\": \"Бизнес\",\n      \"marketing\": \"Маркетинг\",\n      \"travel\": \"Путешествия\",\n      \"others\": \"Другое\",\n      \"prompt\": \"Запрос\",\n      \"sampleOutput\": \"Пример вывода\",\n      \"contentSeo\": \"Контент/SEO\",\n      \"emailMarketing\": \"Email маркетинг\",\n      \"paidAds\": \"Платная реклама\",\n      \"prCommunication\": \"PR/Коммуникации\",\n      \"recruiting\": \"Подбор персонала\",\n      \"sales\": \"Продажи\",\n      \"socialMedia\": \"Социальные сети\",\n      \"strategy\": \"Стратегия\",\n      \"caseStudies\": \"Кейсы\",\n      \"salesCopy\": \"Продающий текст\",\n      \"learning\": \"Обучение\"\n    }\n  },\n  \"autoUpdate\": {\n    \"criticalUpdateTitle\": \"Требуется обновление для продолжения\",\n    \"criticalUpdateDescription\": \"Мы внесли улучшения для повышения удобства использования! Пожалуйста, обновите с {currentVersion} до {newVersion}, чтобы продолжить использовать приложение.\",\n    \"criticalUpdateButton\": \"Обновить\",\n    \"bannerUpdateTitle\": \"Доступна новая версия!\",\n    \"bannerUpdateDescription\": \"Получите последние функции и исправления. Нажмите \\\"Обновить\\\" для установки сейчас\",\n    \"bannerUpdateButton\": \"Обновить\",\n    \"settingsUpdateTitle\": \"Доступна новая версия ({newVersion})!\",\n    \"settingsUpdateDescription\": \"Текущая версия: {currentVersion} (Официальная сборка) → {newVersion}\",\n    \"settingsUpdateButton\": \"Обновить\",\n    \"settingsUpdateWhatsNew\": \"Что нового\"\n  },\n  \"lockPage\": {\n    \"lockPage\": \"Заблокировано\",\n    \"reLockPage\": \"Переблокировать\",\n    \"lockTooltip\": \"Страница заблокирована для предотвращения случайного редактирования. Нажмите, чтобы разблокировать.\",\n    \"pageLockedToast\": \"Страница заблокирована. Редактирование отключено, пока кто-нибудь не разблокирует.\",\n    \"lockedOperationTooltip\": \"Страница заблокирована для предотвращения случайного редактирования.\"\n  },\n  \"suggestion\": {\n    \"accept\": \"Принять\",\n    \"keep\": \"Сохранить\",\n    \"discard\": \"Отменить\",\n    \"close\": \"Закрыть\",\n    \"tryAgain\": \"Попробовать снова\",\n    \"rewrite\": \"Переписать\",\n    \"insertBelow\": \"Вставить ниже\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/sv-SE.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Jag\",\n  \"welcomeText\": \"Välkommen till @:appName\",\n  \"welcomeTo\": \"Välkommen till\",\n  \"githubStarText\": \"Stjärna på GitHub\",\n  \"subscribeNewsletterText\": \"Prenumerera på nyhetsbrev\",\n  \"letsGoButtonText\": \"Snabbstart\",\n  \"title\": \"Titel\",\n  \"youCanAlso\": \"Du kan också\",\n  \"and\": \"och\",\n  \"failedToOpenUrl\": \"Det gick inte att öppna webbadressen: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Klicka för att lägga till nedan\",\n    \"addAboveCmd\": \"Alt+klicka\",\n    \"addAboveMacCmd\": \"Alternativ+klicka\",\n    \"addAboveTooltip\": \"att lägga till ovan\",\n    \"dragTooltip\": \"Dra för att flytta\",\n    \"openMenuTooltip\": \"Klicka för att öppna menyn\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Registera\",\n    \"title\": \"Registrera dig för @:appName\",\n    \"getStartedText\": \"Kom igång\",\n    \"emptyPasswordError\": \"Lösenordet får inte vara tomt\",\n    \"repeatPasswordEmptyError\": \"Upprepa lösenordet får inte vara tomt\",\n    \"unmatchedPasswordError\": \"Upprepa lösenord är inte detsamma som lösenord\",\n    \"alreadyHaveAnAccount\": \"Har du redan ett konto?\",\n    \"emailHint\": \"Epost\",\n    \"passwordHint\": \"Lösenord\",\n    \"repeatPasswordHint\": \"Upprepa lösenord\",\n    \"signUpWith\": \"Registrera med\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Logga in till @:appName\",\n    \"loginButtonText\": \"Logga in\",\n    \"loginStartWithAnonymous\": \"Börja med en anonym session\",\n    \"continueAnonymousUser\": \"Fortsätt med en anonym session\",\n    \"buttonText\": \"Registrera\",\n    \"signingInText\": \"Loggar in...\",\n    \"forgotPassword\": \"Glömt ditt lösenord?\",\n    \"emailHint\": \"Epost\",\n    \"passwordHint\": \"Lösenord\",\n    \"dontHaveAnAccount\": \"Har du inget konto?\",\n    \"repeatPasswordEmptyError\": \"Upprepat lösenord kan inte vara tomt\",\n    \"unmatchedPasswordError\": \"Upprepat lösenord är inte samma som det första\",\n    \"loginAsGuestButtonText\": \"Komma igång\"\n  },\n  \"workspace\": {\n    \"create\": \"Skapa arbetsyta\",\n    \"hint\": \"Arbetsyta\",\n    \"notFoundError\": \"Hittade ingen arbetsyta\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Dela\",\n    \"workInProgress\": \"Kommer snart\",\n    \"markdown\": \"Markdown\",\n    \"copyLink\": \"Kopiera länk\"\n  },\n  \"moreAction\": {\n    \"small\": \"små\",\n    \"medium\": \"medium\",\n    \"large\": \"stor\",\n    \"fontSize\": \"Textstorlek\",\n    \"import\": \"Importera\",\n    \"moreOptions\": \"Fler alternativ\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Text & Markdown\",\n    \"documentFromV010\": \"Dokument från v0.1.0\",\n    \"databaseFromV010\": \"Databas från v0.1.0\",\n    \"csv\": \"CSV\",\n    \"database\": \"Databas\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Byt namn\",\n    \"delete\": \"Ta bort\",\n    \"duplicate\": \"Klona\",\n    \"openNewTab\": \"Öppna i en ny flik\"\n  },\n  \"blankPageTitle\": \"Tom sida\",\n  \"newPageText\": \"Ny sida\",\n  \"trash\": {\n    \"text\": \"Skräp\",\n    \"restoreAll\": \"Återställ alla\",\n    \"deleteAll\": \"Ta bort alla\",\n    \"pageHeader\": {\n      \"fileName\": \"Filnamn\",\n      \"lastModified\": \"Ändrad\",\n      \"created\": \"Skapad\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Är du säker på att ta bort alla sidor i papperskorgen?\",\n      \"caption\": \"Denna åtgärd kan inte ångras.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Är du säker på att återställa alla sidor i papperskorgen?\",\n      \"caption\": \"Denna åtgärd kan inte ångras.\"\n    }\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Denna sida är i skräpmappen\",\n    \"restore\": \"Återställ sida\",\n    \"deletePermanent\": \"Radera permanent\"\n  },\n  \"dialogCreatePageNameHint\": \"Sidnamn\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Genvägar\",\n    \"whatsNew\": \"Vad nytt?\",\n    \"markdown\": \"Prissänkning\",\n    \"debug\": {\n      \"name\": \"Felsökningsinfo\",\n      \"success\": \"Kopierade felsökningsinfo till urklipp!\",\n      \"fail\": \"Kunde inte kopiera felsökningsinfo till urklipp\"\n    },\n    \"feedback\": \"Återkoppling\",\n    \"help\": \"Hjälp & Support\"\n  },\n  \"menuAppHeader\": {\n    \"addPageTooltip\": \"Lägg till en underliggande sida\",\n    \"defaultNewPageName\": \"Namnlös\",\n    \"renameDialog\": \"Byt namn\"\n  },\n  \"toolbar\": {\n    \"undo\": \"Ångra\",\n    \"redo\": \"Upprepa\",\n    \"bold\": \"Fet\",\n    \"italic\": \"Kursiv\",\n    \"underline\": \"Understruken\",\n    \"strike\": \"Genomstruken\",\n    \"numList\": \"Numrerad lista\",\n    \"bulletList\": \"Punktlista\",\n    \"checkList\": \"Checklista\",\n    \"inlineCode\": \"Infogad kod\",\n    \"quote\": \"Citatblock\",\n    \"header\": \"Rubrik\",\n    \"highlight\": \"Färgmarkera\",\n    \"color\": \"Färg\",\n    \"addLink\": \"Lägg till länk\",\n    \"link\": \"Länk\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Växla till ljust läge\",\n    \"darkMode\": \"Växla till mörkt läge\",\n    \"openAsPage\": \"Öppna som sida\",\n    \"addNewRow\": \"Lägg till ny rad\",\n    \"openMenu\": \"Klicka för att öppna meny\",\n    \"dragRow\": \"Långt tryck för att ändra ordningen på raden\",\n    \"viewDataBase\": \"Visa databas\",\n    \"referencePage\": \"Det här {name} refereras till\",\n    \"addBlockBelow\": \"Lägg till ett block nedan\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Stäng sidofältet\",\n    \"openSidebar\": \"Öppna sidofältet\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Exporterade anteckning till Markdown\",\n      \"path\": \"Dokument/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Kontakter\",\n    \"whatsHappening\": \"Vad händer denna vecka?\",\n    \"addContact\": \"Lägg till kontakt\",\n    \"editContact\": \"Redigera kontakt\"\n  },\n  \"button\": {\n    \"ok\": \"OK\",\n    \"done\": \"Gjort\",\n    \"cancel\": \"Avbryt\",\n    \"signIn\": \"Logga in\",\n    \"signOut\": \"Logga ut\",\n    \"complete\": \"Slutfört\",\n    \"save\": \"Spara\",\n    \"generate\": \"Generera\",\n    \"esc\": \"ESC\",\n    \"keep\": \"Ha kvar\",\n    \"tryAgain\": \"Försök igen\",\n    \"discard\": \"Kassera\",\n    \"replace\": \"Byta ut\",\n    \"insertBelow\": \"Infoga nedan\",\n    \"upload\": \"Ladda upp\",\n    \"edit\": \"Redigera\",\n    \"delete\": \"Radera\",\n    \"duplicate\": \"Duplicera\",\n    \"putback\": \"Ställ tillbaka\",\n    \"update\": \"Uppdatering\",\n    \"share\": \"Dela\",\n    \"removeFromFavorites\": \"Ta bort från favoriter\",\n    \"addToFavorites\": \"Lägg till i favoriter\",\n    \"rename\": \"Döp om\",\n    \"helpCenter\": \"Hjälpcenter\",\n    \"add\": \"Lägg till\",\n    \"yes\": \"Ja\",\n    \"clear\": \"Klar\",\n    \"remove\": \"Ta bort\",\n    \"dontRemove\": \"Ta inte bort\",\n    \"copyLink\": \"Kopiera länk\",\n    \"align\": \"Jämka\",\n    \"login\": \"Logga in\",\n    \"logout\": \"Logga ut\",\n    \"back\": \"Tillbaka\",\n    \"signInGoogle\": \"Logga in med Google\",\n    \"signInGithub\": \"Logga in med Github\",\n    \"signInDiscord\": \"Logga in med Discord\"\n  },\n  \"label\": {\n    \"welcome\": \"Välkommen!\",\n    \"firstName\": \"Förnamn\",\n    \"middleName\": \"Mellannamn\",\n    \"lastName\": \"Efternamn\",\n    \"stepX\": \"Steg {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Det går inte att ansluta till ditt konto.\",\n      \"failedMsg\": \"Se till att du har slutfört inloggningsprocessen i din webbläsare.\"\n    },\n    \"google\": {\n      \"title\": \"GOOGLE LOGGA IN\",\n      \"instruction1\": \"För att kunna importera dina Google-kontakter måste du auktorisera denna applikation med din webbläsare.\",\n      \"instruction2\": \"Kopiera den här koden till ditt urklipp genom att klicka på ikonen eller välja texten:\",\n      \"instruction3\": \"Navigera till följande länk i din webbläsare och ange koden ovan:\",\n      \"instruction4\": \"Tryck på knappen nedan när du har slutfört registreringen:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Inställningar\",\n    \"menu\": {\n      \"appearance\": \"Utseende\",\n      \"language\": \"Språk\",\n      \"user\": \"Användare\",\n      \"files\": \"Filer\",\n      \"notifications\": \"Aviseringar\",\n      \"open\": \"Öppna Inställningar\",\n      \"logout\": \"Logga ut\",\n      \"logoutPrompt\": \"Är du säker på att logga ut?\",\n      \"selfEncryptionLogoutPrompt\": \"Är du säker på att du vill logga ut? Se till att du har kopierat krypteringshemligheten\",\n      \"syncSetting\": \"Synkroniseringsinställning\",\n      \"cloudSettings\": \"Molninställningar\",\n      \"enableSync\": \"Aktivera synkronisering\",\n      \"enableEncrypt\": \"Kryptera data\",\n      \"cloudURL\": \"Grund-URL\",\n      \"invalidCloudURLScheme\": \"Ogiltigt schema\",\n      \"cloudServerType\": \"Molnserver\",\n      \"cloudServerTypeTip\": \"Observera att det kan logga ut ditt nuvarande konto efter att ha bytt molnserver\",\n      \"cloudLocal\": \"Lokal\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"Molnets webbadress får inte vara tom\",\n      \"clickToCopy\": \"Klicka för att kopiera\",\n      \"selfHostStart\": \"Om du inte har en server, vänligen se\",\n      \"selfHostContent\": \"dokument\",\n      \"selfHostEnd\": \"för vägledning om hur du själv är värd för din egen server\",\n      \"cloudURLHint\": \"Ange grundadressen till din server\",\n      \"cloudWSURL\": \"Websocket URL\",\n      \"cloudWSURLHint\": \"Infoga websocket-adressen till din server\",\n      \"restartApp\": \"Omstart\",\n      \"restartAppTip\": \"Starta om programmet för att ändringarna ska träda i kraft. Observera att detta kan logga ut ditt nuvarande konto\",\n      \"changeServerTip\": \"När du har bytt server måste du klicka på omstartsknappen för att ändringarna ska träda i kraft\",\n      \"enableEncryptPrompt\": \"Aktivera kryptering för att säkra dina data med denna hemlighet. Förvara det säkert; när den väl är aktiverad kan den inte stängas av. Om din data går förlorad blir den omöjlig att återställa. Klicka för att kopiera\",\n      \"inputEncryptPrompt\": \"Vänligen ange din krypteringshemlighet för\",\n      \"clickToCopySecret\": \"Klicka för att kopiera hemlighet\",\n      \"configServerSetting\": \"Konfigurera dina serverinställningar\",\n      \"configServerGuide\": \"Efter att ha valt `Quick Start`, navigera till `Settings` och sedan \\\"Cloud Settings\\\" för att konfigurera din egen värdserver.\",\n      \"inputTextFieldHint\": \"Din hemlighet\",\n      \"historicalUserList\": \"Användarinloggningshistorik\",\n      \"historicalUserListTooltip\": \"Den här listan visar dina anonyma konton. Du kan klicka på ett konto för att se dess detaljer. Anonyma konton skapas genom att klicka på knappen \\\"Kom igång\\\".\",\n      \"openHistoricalUser\": \"Klicka för att öppna det anonyma kontot\",\n      \"customPathPrompt\": \"Att lagra @:appName-datamappen i en molnsynkroniserad mapp som Google Drive kan innebära risker. Om databasen i den här mappen nås eller ändras från flera platser samtidigt, kan det resultera i synkroniseringskonflikter och potentiell datakorruption\",\n      \"importAppFlowyData\": \"Importera data från extern @:appName-mapp\",\n      \"importingAppFlowyDataTip\": \"Dataimport pågår. Stäng inte appen\",\n      \"importAppFlowyDataDescription\": \"Kopiera data från en extern @:appName-datamapp och importera den till den aktuella @:appName-datamappen\",\n      \"importSuccess\": \"@:appName-datamappen har importerats\",\n      \"importFailed\": \"Det gick inte att importera @:appName-datamappen\",\n      \"importGuide\": \"För ytterligare information, snälla se det refererade dokumentet\"\n    },\n    \"appearance\": {\n      \"fontFamily\": {\n        \"label\": \"Typsnittsfamilj\",\n        \"search\": \"Sök\"\n      },\n      \"themeMode\": {\n        \"label\": \"Temats läge\",\n        \"light\": \"Ljust läge\",\n        \"dark\": \"Mörkt läge\",\n        \"system\": \"Samma som system\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Ladda upp\",\n        \"description\": \"Ladda upp ditt eget @:appName-tema med knappen nedan.\",\n        \"loading\": \"Vänta medan vi validerar och laddar upp ditt tema...\",\n        \"uploadSuccess\": \"Ditt tema laddades upp\",\n        \"deletionFailure\": \"Det gick inte att ta bort temat. Försök att radera det manuellt.\",\n        \"filePickerDialogTitle\": \"Välj en .flowy_plugin-fil\",\n        \"urlUploadFailure\": \"Det gick inte att öppna webbadressen: {}\",\n        \"failure\": \"Temat som laddades upp hade ett ogiltigt format.\"\n      },\n      \"theme\": \"Tema\",\n      \"builtInsLabel\": \"Inbyggda teman\",\n      \"pluginsLabel\": \"Plugin\"\n    },\n    \"files\": {\n      \"copy\": \"Kopiera\",\n      \"defaultLocation\": \"Läs filer och datalagringsplats\",\n      \"exportData\": \"Exportera dina data\",\n      \"doubleTapToCopy\": \"Dubbeltryck för att kopiera sökvägen\",\n      \"restoreLocation\": \"Återställ till @:appName standardsökväg\",\n      \"customizeLocation\": \"Öppna en annan mapp\",\n      \"restartApp\": \"Starta om appen för att ändringarna ska träda i kraft.\",\n      \"exportDatabase\": \"Exportera databas\",\n      \"selectFiles\": \"Välj de filer som behöver exporteras\",\n      \"selectAll\": \"Välj alla\",\n      \"deselectAll\": \"Avmarkera alla\",\n      \"createNewFolder\": \"Skapa en ny mapp\",\n      \"createNewFolderDesc\": \"Berätta för oss var du vill lagra din data\",\n      \"defineWhereYourDataIsStored\": \"Definiera var din data lagras\",\n      \"open\": \"Öppen\",\n      \"openFolder\": \"Öppna en befintlig mapp\",\n      \"openFolderDesc\": \"Läs och skriv det till din befintliga @:appName-mapp\",\n      \"folderHintText\": \"mappnamn\",\n      \"location\": \"Skapar en ny mapp\",\n      \"locationDesc\": \"Välj ett namn för din @:appName-datamapp\",\n      \"browser\": \"Bläddra\",\n      \"create\": \"Skapa\",\n      \"set\": \"Uppsättning\",\n      \"folderPath\": \"Sökväg för att lagra din mapp\",\n      \"locationCannotBeEmpty\": \"Sökvägen får inte vara tom\",\n      \"pathCopiedSnackbar\": \"Fillagringssökväg kopierad till urklipp!\",\n      \"changeLocationTooltips\": \"Byt datakatalog\",\n      \"change\": \"Förändra\",\n      \"openLocationTooltips\": \"Öppna en annan datakatalog\",\n      \"openCurrentDataFolder\": \"Öppna aktuell datakatalog\",\n      \"recoverLocationTooltips\": \"Återställ till @:appNames standarddatakatalog\",\n      \"exportFileSuccess\": \"Exporterade filen framgångsrikt!\",\n      \"exportFileFail\": \"Export av fil misslyckades!\",\n      \"export\": \"Exportera\"\n    },\n    \"user\": {\n      \"name\": \"namn\",\n      \"selectAnIcon\": \"Välj en ikon\",\n      \"pleaseInputYourOpenAIKey\": \"vänligen ange din AI-nyckel\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Är du säker på att du vill ta bort den här vyn?\",\n    \"createView\": \"Ny\",\n    \"settings\": {\n      \"filter\": \"Filter\",\n      \"sort\": \"Sortera\",\n      \"sortBy\": \"Sortera efter\",\n      \"properties\": \"Egenskaper\",\n      \"reorderPropertiesTooltip\": \"Dra för att ändra ordning på egenskaper\",\n      \"group\": \"Grupp\",\n      \"addFilter\": \"Lägg till filter\",\n      \"deleteFilter\": \"Ta bort filter\",\n      \"filterBy\": \"Filtrera efter...\",\n      \"typeAValue\": \"Skriv ett värde...\",\n      \"layout\": \"Layout\",\n      \"databaseLayout\": \"Layout\",\n      \"Properties\": \"Egenskaper\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Innehåller\",\n      \"doesNotContain\": \"Innehåller inte\",\n      \"endsWith\": \"Slutar med\",\n      \"startWith\": \"Börjar med\",\n      \"is\": \"Är\",\n      \"isNot\": \"Är inte\",\n      \"isEmpty\": \"Är tom\",\n      \"isNotEmpty\": \"Är inte tom\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Inte\",\n        \"startWith\": \"Börjar med\",\n        \"endWith\": \"Slutar med\",\n        \"isEmpty\": \"är tom\",\n        \"isNotEmpty\": \"är inte tom\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Kontrollerade\",\n      \"isUnchecked\": \"Okontrollerad\",\n      \"choicechipPrefix\": {\n        \"is\": \"är\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"är komplett\",\n      \"isIncomplted\": \"är ofullständig\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"Är\",\n      \"isNot\": \"Är inte\",\n      \"contains\": \"Innehåller\",\n      \"doesNotContain\": \"Innehåller inte\",\n      \"isEmpty\": \"Är tom\",\n      \"isNotEmpty\": \"Är inte tom\"\n    },\n    \"field\": {\n      \"hide\": \"Dölj\",\n      \"insertLeft\": \"Infoga till vänster\",\n      \"insertRight\": \"Infoga till höger\",\n      \"duplicate\": \"Klona\",\n      \"delete\": \"Ta bort\",\n      \"textFieldName\": \"Text\",\n      \"checkboxFieldName\": \"Checkruta\",\n      \"dateFieldName\": \"Datum\",\n      \"updatedAtFieldName\": \"Senast ändrad tid\",\n      \"createdAtFieldName\": \"Skapat tid\",\n      \"numberFieldName\": \"Siffror\",\n      \"singleSelectFieldName\": \"Välj\",\n      \"multiSelectFieldName\": \"Välj flera\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Checklista\",\n      \"numberFormat\": \"Sifferformat\",\n      \"dateFormat\": \"Datumformat\",\n      \"includeTime\": \"Inkludera tid\",\n      \"dateFormatFriendly\": \"Månad Dag, År\",\n      \"dateFormatISO\": \"År-Månad-Dag\",\n      \"dateFormatLocal\": \"Månad/Dag/År\",\n      \"dateFormatUS\": \"År/Månad/Dag\",\n      \"dateFormatDayMonthYear\": \"Dag månad år\",\n      \"timeFormat\": \"Tidsformat\",\n      \"invalidTimeFormat\": \"Ogiltigt format\",\n      \"timeFormatTwelveHour\": \"12-timmars\",\n      \"timeFormatTwentyFourHour\": \"24-timmars\",\n      \"addSelectOption\": \"Lägg till ett alternativ\",\n      \"optionTitle\": \"Alternativ\",\n      \"addOption\": \"Lägg till alternativ\",\n      \"editProperty\": \"Redigera egenskap\",\n      \"newProperty\": \"Ny kolumn\",\n      \"deleteFieldPromptMessage\": \"Är du säker? Denna egenskap kommer att raderas.\"\n    },\n    \"sort\": {\n      \"ascending\": \"Stigande\",\n      \"descending\": \"Fallande\",\n      \"addSort\": \"Lägg till sortering\",\n      \"deleteSort\": \"Ta bort sortering\"\n    },\n    \"row\": {\n      \"duplicate\": \"Klona\",\n      \"delete\": \"Ta bort\",\n      \"textPlaceholder\": \"Tom\",\n      \"copyProperty\": \"Kopierade egenskap till urklipp\",\n      \"count\": \"Antal\",\n      \"newRow\": \"Ny rad\",\n      \"action\": \"Handling\"\n    },\n    \"selectOption\": {\n      \"create\": \"Skapa\",\n      \"purpleColor\": \"Purpur\",\n      \"pinkColor\": \"Rosa\",\n      \"lightPinkColor\": \"Ljusrosa\",\n      \"orangeColor\": \"Orange\",\n      \"yellowColor\": \"Gul\",\n      \"limeColor\": \"Lime\",\n      \"greenColor\": \"Grön\",\n      \"aquaColor\": \"Vatten\",\n      \"blueColor\": \"Blå\",\n      \"deleteTag\": \"Ta bort tagg\",\n      \"colorPanelTitle\": \"Färger\",\n      \"panelTitle\": \"Välj ett alternativ eller skapa ett\",\n      \"searchOption\": \"Sök efter ett alternativ\"\n    },\n    \"checklist\": {\n      \"addNew\": \"Lägg till ett objekt\"\n    },\n    \"menuName\": \"Tabell\",\n    \"referencedGridPrefix\": \"Utsikt över\"\n  },\n  \"document\": {\n    \"menuName\": \"Dokument\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Välj en tavla att länka till\",\n        \"createANewBoard\": \"Skapa en ny tavla\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Välj en tabell att länka till\",\n        \"createANewGrid\": \"Skapa en ny tabell\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Välj en kalender att länka till\",\n        \"createANewCalendar\": \"Skapa en ny kalender\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Skissera\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Refererad tavla\",\n      \"referencedGrid\": \"Refererade tabell\",\n      \"referencedCalendar\": \"Refererad kalender\",\n      \"autoGeneratorMenuItemName\": \"AI Writer\",\n      \"autoGeneratorTitleName\": \"AI: Be AI skriva vad som helst...\",\n      \"autoGeneratorLearnMore\": \"Läs mer\",\n      \"autoGeneratorGenerate\": \"Generera\",\n      \"autoGeneratorHintText\": \"Fråga AI...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Kan inte hämta AI-nyckeln\",\n      \"autoGeneratorRewrite\": \"Skriva om\",\n      \"smartEdit\": \"AI-assistenter\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"Fixa stavningen\",\n      \"warning\": \"⚠️ AI-svar kan vara felaktiga eller vilseledande.\",\n      \"smartEditSummarize\": \"Sammanfatta\",\n      \"smartEditImproveWriting\": \"Förbättra skrivandet\",\n      \"smartEditMakeLonger\": \"Gör längre\",\n      \"smartEditCouldNotFetchResult\": \"Det gick inte att hämta resultatet från AI\",\n      \"smartEditCouldNotFetchKey\": \"Det gick inte att hämta AI-nyckeln\",\n      \"smartEditDisabled\": \"Anslut AI i Inställningar\",\n      \"discardResponse\": \"Vill du kassera AI-svaren?\",\n      \"createInlineMathEquation\": \"Skapa ekvation\",\n      \"toggleList\": \"Växla lista\",\n      \"cover\": {\n        \"changeCover\": \"Byt omslag\",\n        \"colors\": \"Färger\",\n        \"images\": \"Bilder\",\n        \"clearAll\": \"Rensa alla\",\n        \"abstract\": \"Abstrakt\",\n        \"addCover\": \"Lägg till omslag\",\n        \"addLocalImage\": \"Lägg till lokal bild\",\n        \"invalidImageUrl\": \"Ogiltig bildadress\",\n        \"failedToAddImageToGallery\": \"Det gick inte att lägga till bild i galleriet\",\n        \"enterImageUrl\": \"Ange bildens URL\",\n        \"add\": \"Lägg till\",\n        \"back\": \"Tillbaka\",\n        \"saveToGallery\": \"Spara i galleriet\",\n        \"removeIcon\": \"Ta bort ikon\",\n        \"pasteImageUrl\": \"Klistra in bildens URL\",\n        \"or\": \"ELLER\",\n        \"pickFromFiles\": \"Välj från filer\",\n        \"couldNotFetchImage\": \"Det gick inte att hämta bilden\",\n        \"imageSavingFailed\": \"Det gick inte att spara bild\",\n        \"addIcon\": \"Lägg till ikon\",\n        \"coverRemoveAlert\": \"Den kommer att tas bort från omslaget efter att den har raderats.\",\n        \"alertDialogConfirmation\": \"Är du säker på att du vill fortsätta?\"\n      },\n      \"mathEquation\": {\n        \"addMathEquation\": \"Lägg till matematikekvation\",\n        \"editMathEquation\": \"Redigera matematisk ekvation\"\n      },\n      \"optionAction\": {\n        \"click\": \"Klick\",\n        \"toOpenMenu\": \" för att öppna menyn\",\n        \"delete\": \"Radera\",\n        \"duplicate\": \"Duplicera\",\n        \"turnInto\": \"Bli till\",\n        \"moveUp\": \"Flytta upp\",\n        \"moveDown\": \"Flytta ner\",\n        \"color\": \"Färg\",\n        \"align\": \"Justera\",\n        \"left\": \"Vänster\",\n        \"center\": \"Centrum\",\n        \"right\": \"Höger\",\n        \"defaultColor\": \"Standard\"\n      },\n      \"image\": {\n        \"copiedToPasteBoard\": \"Bildlänken har kopierats till urklipp\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Lägg till rubriker för att skapa en innehållsförteckning.\"\n      }\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Skriv '/' för kommandon\"\n    },\n    \"title\": {\n      \"placeholder\": \"Ofrälse\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Klicka för att lägga till bild\",\n      \"upload\": {\n        \"label\": \"Ladda upp\",\n        \"placeholder\": \"Klicka för att ladda upp bilden\"\n      },\n      \"url\": {\n        \"label\": \"bild URL\",\n        \"placeholder\": \"Ange bildens URL\"\n      },\n      \"support\": \"Bildstorleksgränsen är 5 MB. Format som stöds: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Ogiltig bild\",\n        \"invalidImageSize\": \"Bildstorleken måste vara mindre än 5 MB\",\n        \"invalidImageFormat\": \"Bildformat stöds inte. Format som stöds: JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"Ogiltig bildadress\"\n      }\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Språk\",\n        \"placeholder\": \"Välj språk\"\n      }\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Klistra in eller skriv en länk\",\n      \"url\": {\n        \"label\": \"Länk-URL\",\n        \"placeholder\": \"Ange länkens URL\"\n      },\n      \"title\": {\n        \"label\": \"Länktitel\",\n        \"placeholder\": \"Ange länkens titel\"\n      }\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"Nytt\"\n    },\n    \"menuName\": \"Tavla\",\n    \"referencedBoardPrefix\": \"Utsikt över\",\n    \"mobile\": {\n      \"showGroup\": \"Visa grupp\",\n      \"showGroupContent\": \"Är du säker på att du vill visa den här gruppen på tavlan?\",\n      \"failedToLoad\": \"Det gick inte att ladda tavlan\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Kalender\",\n    \"defaultNewCalendarTitle\": \"Ofrälse\",\n    \"navigation\": {\n      \"today\": \"I dag\",\n      \"jumpToday\": \"Hoppa till Idag\",\n      \"previousMonth\": \"Förra månaden\",\n      \"nextMonth\": \"Nästa månad\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Visa veckonummer\",\n      \"showWeekends\": \"Visa helger\",\n      \"firstDayOfWeek\": \"Börja veckan på\",\n      \"layoutDateField\": \"Layoutkalender av\",\n      \"noDateTitle\": \"Inget datum\",\n      \"clickToAdd\": \"Klicka för att lägga till i kalendern\",\n      \"name\": \"Kalenderlayout\",\n      \"noDateHint\": \"Icke schemalagda händelser kommer att dyka upp här\"\n    },\n    \"referencedCalendarPrefix\": \"Utsikt över\"\n  },\n  \"errorDialog\": {\n    \"title\": \"@:appName-fel\",\n    \"howToFixFallback\": \"Vi ber om ursäkt för besväret! Skapa en felrapport på vår GitHub-sida som beskriver ditt fel.\",\n    \"github\": \"Visa på GitHub\"\n  },\n  \"search\": {\n    \"label\": \"Sök\",\n    \"placeholder\": {\n      \"actions\": \"Sökåtgärder...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Kopierade!\",\n      \"fail\": \"Det går inte att kopiera\"\n    }\n  },\n  \"unSupportBlock\": \"Den aktuella versionen stöder inte detta block.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Är du säker på att du vill ta bort {pageType}?\",\n    \"deleteContentCaption\": \"om du tar bort denna {pageType} kan du återställa den från papperskorgen.\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/th-TH.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"ฉัน\",\n  \"welcomeText\": \"ยินดีต้อนรับเข้าสู่ @:appName\",\n  \"welcomeTo\": \"ยินดีต้อนรับเข้าสู่\",\n  \"githubStarText\": \"กดดาวบน GitHub\",\n  \"subscribeNewsletterText\": \"สมัครรับจดหมายข่าว\",\n  \"letsGoButtonText\": \"เริ่มต้นอย่างรวดเร็ว\",\n  \"title\": \"ชื่อ\",\n  \"youCanAlso\": \"นอกจากนี้คุณยังสามารถ\",\n  \"and\": \"และ\",\n  \"failedToOpenUrl\": \"ไม่สามารถเปิด url ได้: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"คลิกเพื่อเพิ่มด้านล่าง\",\n    \"addAboveCmd\": \"Alt+click\",\n    \"addAboveMacCmd\": \"Option+click\",\n    \"addAboveTooltip\": \"เพื่อเพิ่มด้านบน\",\n    \"dragTooltip\": \"ลากเพื่อย้าย\",\n    \"openMenuTooltip\": \"คลิกเพื่อเปิดเมนู\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"ลงทะเบียน\",\n    \"title\": \"ลงทะเบียนเพื่อใช้ @:appName\",\n    \"getStartedText\": \"เริ่มต้น\",\n    \"emptyPasswordError\": \"รหัสผ่านต้องไม่เว้นว่าง\",\n    \"repeatPasswordEmptyError\": \"ช่องยืนยันรหัสผ่านต้องไม่เว้นว่าง\",\n    \"unmatchedPasswordError\": \"รหัสผ่านที่ยืนยันไม่ตรงกับรหัสผ่าน\",\n    \"alreadyHaveAnAccount\": \"มีบัญชีอยู่แล้วหรือไม่?\",\n    \"emailHint\": \"อีเมล\",\n    \"passwordHint\": \"รหัสผ่าน\",\n    \"repeatPasswordHint\": \"ยืนยันรหัสผ่าน\",\n    \"signUpWith\": \"ลงทะเบียนกับ:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"เข้าสู่ระบบเพื่อใช้งาน @:appName\",\n    \"loginButtonText\": \"เข้าสู่ระบบ\",\n    \"loginStartWithAnonymous\": \"เริ่มต้นด้วยเซสชั่นแบบไม่ระบุตัวตน\",\n    \"continueAnonymousUser\": \"ดำเนินการต่อด้วยเซสชันแบบไม่ระบุตัวตน\",\n    \"buttonText\": \"เข้าสู่ระบบ\",\n    \"signingInText\": \"กำลังลงชื่อเข้าใช้...\",\n    \"forgotPassword\": \"ลืมรหัสผ่านหรือไม่?\",\n    \"emailHint\": \"อีเมล\",\n    \"passwordHint\": \"รหัสผ่าน\",\n    \"dontHaveAnAccount\": \"ยังไม่มีบัญชีใช่หรือไม่?\",\n    \"createAccount\": \"สร้างบัญชี\",\n    \"repeatPasswordEmptyError\": \"ช่องยืนยันรหัสผ่านต้องไม่เว้นว่าง\",\n    \"unmatchedPasswordError\": \"รหัสผ่านที่ยืนยันไม่ตรงกับรหัสผ่าน\",\n    \"syncPromptMessage\": \"กำลังซิงค์ข้อมูล ซึ่งอาจใช้เวลาสักครู่ กรุณาอย่าปิดหน้านี้\",\n    \"or\": \"หรือ\",\n    \"signInWithGoogle\": \"ดำเนินการต่อด้วย Google\",\n    \"signInWithGithub\": \"ดำเนินการต่อด้วย GitHub\",\n    \"signInWithDiscord\": \"ดำเนินการต่อด้วย Discord\",\n    \"signInWithApple\": \"ดำเนินการต่อด้วย Apple\",\n    \"continueAnotherWay\": \"ลองวิธีอื่นเพื่อดำเนินการต่อ\",\n    \"signUpWithGoogle\": \"ลงทะเบียนด้วย Google\",\n    \"signUpWithGithub\": \"ลงทะเบียนด้วย Github\",\n    \"signUpWithDiscord\": \"ลงทะเบียนด้วย Discord\",\n    \"signInWith\": \"ลงชื่อเข้าใช้ด้วย:\",\n    \"signInWithEmail\": \"ดำเนินการต่อด้วยอีเมล์\",\n    \"signInWithMagicLink\": \"ดำเนินการต่อ\",\n    \"signUpWithMagicLink\": \"ลงทะเบียนด้วย Magic Link\",\n    \"pleaseInputYourEmail\": \"กรุณากรอกที่อยู่อีเมล์ของคุณ\",\n    \"settings\": \"การตั้งค่า\",\n    \"magicLinkSent\": \"ส่ง Magic Link แล้ว!\",\n    \"invalidEmail\": \"กรุณากรอกอีเมลที่ถูกต้อง\",\n    \"alreadyHaveAnAccount\": \"มีบัญชีอยู่แล้วใช่ไหม?\",\n    \"logIn\": \"เข้าสู่ระบบ\",\n    \"generalError\": \"มีบางอย่างผิดพลาด โปรดลองอีกครั้งในภายหลัง\",\n    \"limitRateError\": \"ด้วยเหตุผลด้านความปลอดภัย คุณสามารถขอ Magic Link ได้ทุกๆ 60 วินาทีเท่านั้น\",\n    \"magicLinkSentDescription\": \"Magic Link ถูกส่งไปยังอีเมลของคุณแล้ว กรุณาคลิกลิงก์เพื่อเข้าสู่ระบบ ลิงก์จะหมดอายุภายใน 5 นาที\",\n    \"anonymous\": \"ไม่ระบุตัวตน\",\n    \"LogInWithGoogle\": \"เข้าสู่ระบบด้วย Google\",\n    \"LogInWithGithub\": \"เข้าสู่ระบบด้วย Github\",\n    \"LogInWithDiscord\": \"เข้าสู่ระบบด้วย Discord\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"เลือกพื้นที่ทำงานของคุณ\",\n    \"defaultName\": \"พื้นที่ทำงานของฉัน\",\n    \"create\": \"สร้างพื้นที่ทำงาน\",\n    \"importFromNotion\": \"นำเข้าจาก Notion\",\n    \"learnMore\": \"เรียนรู้เพิ่มเติม\",\n    \"reset\": \"รีเซ็ตพื้นที่ทำงาน\",\n    \"renameWorkspace\": \"เปลี่ยนชื่อพื้นที่ทำงาน\",\n    \"workspaceNameCannotBeEmpty\": \"ชื่อพื้นที่ทำงานไม่สามารถเว้นว่างได้\",\n    \"resetWorkspacePrompt\": \"การรีเซ็ตพื้นที่ทำงานจะลบทุกหน้าและข้อมูลภายในนั้น คุณแน่ใจหรือไม่ว่าต้องการรีเซ็ตพื้นที่ทำงาน? หรือคุณสามารถติดต่อทีมสนับสนุนเพื่อกู้คืนพื้นที่ทำงานได้\",\n    \"hint\": \"พื้นที่ทำงาน\",\n    \"notFoundError\": \"ไม่พบพื้นที่ทำงาน\",\n    \"failedToLoad\": \"เกิดข้อผิดพลาด! ไม่สามารถโหลดพื้นที่ทำงานได้ โปรดปิดแอปพลิเคชัน @:appName ที่เปิดอยู่ทั้งหมดแล้วลองอีกครั้ง\",\n    \"errorActions\": {\n      \"reportIssue\": \"รายงานปัญหา\",\n      \"reportIssueOnGithub\": \"รายงานปัญหาบน Github\",\n      \"exportLogFiles\": \"ส่งออกไฟล์บันทึก\",\n      \"reachOut\": \"ติดต่อได้ที่ Discord\"\n    },\n    \"menuTitle\": \"พื้นที่ทำงาน\",\n    \"deleteWorkspaceHintText\": \"คุณแน่ใจหรือไม่ว่าต้องการลบพื้นที่ทำงาน? การกระทำนี้ไม่สามารถย้อนกลับได้ และทุกหน้าที่คุณเผยแพร่จะถูกยกเลิกการเผยแพร่\",\n    \"createSuccess\": \"สร้างพื้นที่ทำงานสำเร็จแล้ว\",\n    \"createFailed\": \"ไม่สามารถสร้างพื้นที่ทำงานได้\",\n    \"createLimitExceeded\": \"คุณถึงขีดจำกัดสูงสุดของพื้นที่ทำงานที่บัญชีของคุณสามารถสร้างได้แล้ว หากคุณต้องการพื้นที่ทำงานเพิ่มเติมเพื่อดำเนินการต่อ โปรดส่งคำขอบน Github\",\n    \"deleteSuccess\": \"ลบพื้นที่ทำงานสำเร็จแล้ว\",\n    \"deleteFailed\": \"ไม่สามารถลบพื้นที่ทำงานได้\",\n    \"openSuccess\": \"เปิดพื้นที่ทำงานได้สำเร็จ\",\n    \"openFailed\": \"ไม่สามารถเปิดพื้นที่ทำงานได้\",\n    \"renameSuccess\": \"เปลี่ยนชื่อพื้นที่ทำงานสำเร็จแล้ว\",\n    \"renameFailed\": \"ไม่สามารถเปลี่ยนชื่อพื้นที่ทำงานได้\",\n    \"updateIconSuccess\": \"อัปเดตไอคอนพื้นที่ทำงานสำเร็จแล้ว\",\n    \"updateIconFailed\": \"ไม่สามารถอัปเดตไอคอนพื้นที่ทำงานได้\",\n    \"cannotDeleteTheOnlyWorkspace\": \"ไม่สามารถลบพื้นที่ทำงานเพียงอย่างเดียวได้\",\n    \"fetchWorkspacesFailed\": \"ไม่สามารถดึงข้อมูลพื้นที่ทำงานได้\",\n    \"leaveCurrentWorkspace\": \"ออกจากพื้นที่ทำงาน\",\n    \"leaveCurrentWorkspacePrompt\": \"คุณแน่ใจหรือไม่ว่าต้องการออกจากพื้นที่ทำงานปัจจุบัน?\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"แชร์\",\n    \"workInProgress\": \"เร็วๆ นี้\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"คัดลอกไปยังคลิปบอร์ด\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"คัดลอกลิงค์\",\n    \"publishToTheWeb\": \"เผยแพร่สู่เว็บ\",\n    \"publishToTheWebHint\": \"สร้างเว็บไซต์ด้วย AppFlowy\",\n    \"publish\": \"เผยแพร่\",\n    \"unPublish\": \"ยกเลิกการเผยแพร่\",\n    \"visitSite\": \"เยี่ยมชมเว็บไซต์\",\n    \"exportAsTab\": \"ส่งออกเป็น\",\n    \"publishTab\": \"เผยแพร่\",\n    \"shareTab\": \"แชร์\",\n    \"publishOnAppFlowy\": \"เผยแพร่บน AppFlowy\",\n    \"shareTabTitle\": \"เชิญเพื่อการทำงานร่วมกัน\",\n    \"shareTabDescription\": \"เพื่อการทำงานร่วมกันอย่างง่ายดายกับใครก็ได้\",\n    \"copyLinkSuccess\": \"คัดลอกลิงก์ไปยังคลิปบอร์ดแล้ว\",\n    \"copyShareLink\": \"คัดลอกลิงก์แชร์\",\n    \"copyLinkFailed\": \"ไม่สามารถคัดลอกลิงก์ไปยังคลิปบอร์ดได้\",\n    \"copyLinkToBlockSuccess\": \"คัดลอกลิงก์บล็อกไปยังคลิปบอร์ดแล้ว\",\n    \"copyLinkToBlockFailed\": \"ไม่สามารถคัดลอกลิงก์บล็อกไปยังคลิปบอร์ด\",\n    \"manageAllSites\": \"จัดการไซต์ทั้งหมด\",\n    \"updatePathName\": \"อัปเดตชื่อเส้นทาง\"\n  },\n  \"moreAction\": {\n    \"small\": \"เล็ก\",\n    \"medium\": \"กลาง\",\n    \"large\": \"ใหญ่\",\n    \"fontSize\": \"ขนาดตัวอักษร\",\n    \"import\": \"นำเข้า\",\n    \"moreOptions\": \"ตัวเลือกเพิ่มเติม\",\n    \"wordCount\": \"จำนวนคำ: {}\",\n    \"charCount\": \"จำนวนตัวอักษร: {}\",\n    \"createdAt\": \"สร้างแล้ว: {}\",\n    \"deleteView\": \"ลบ\",\n    \"duplicateView\": \"ทำสำเนา\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"ข้อความ & Markdown\",\n    \"documentFromV010\": \"เอกสารจาก v0.1.0\",\n    \"databaseFromV010\": \"ฐานข้อมูลจาก v0.1.0\",\n    \"notionZip\": \"ไฟล์ Zip ที่ส่งออกจาก Notion\",\n    \"csv\": \"CSV\",\n    \"database\": \"ฐานข้อมูล\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"เปลี่ยนชื่อ\",\n    \"delete\": \"ลบ\",\n    \"duplicate\": \"ทำสำเนา\",\n    \"unfavorite\": \"ลบออกจากรายการโปรด\",\n    \"favorite\": \"เพิ่มในรายการโปรด\",\n    \"openNewTab\": \"เปิดในแท็บใหม่\",\n    \"moveTo\": \"ย้ายไปยัง\",\n    \"addToFavorites\": \"เพิ่มในรายการโปรด\",\n    \"copyLink\": \"คัดลอกลิงค์\",\n    \"changeIcon\": \"เปลี่ยนไอคอน\",\n    \"collapseAllPages\": \"ยุบหน้าย่อยทั้งหมด\"\n  },\n  \"blankPageTitle\": \"หน้าเปล่า\",\n  \"newPageText\": \"หน้าใหม่\",\n  \"newDocumentText\": \"เอกสารใหม่\",\n  \"newGridText\": \"ตารางใหม่\",\n  \"newCalendarText\": \"ปฏิทินใหม่\",\n  \"newBoardText\": \"กระดานใหม่\",\n  \"chat\": {\n    \"newChat\": \"แชท AI\",\n    \"inputMessageHint\": \"ถาม @:appName AI\",\n    \"inputLocalAIMessageHint\": \"ถาม @:appName Local AI\",\n    \"unsupportedCloudPrompt\": \"คุณสมบัตินี้ใช้ได้เมื่อใช้ @:appName Cloud เท่านั้น\",\n    \"relatedQuestion\": \"ข้อเสนอแนะ\",\n    \"serverUnavailable\": \"บริการไม่สามารถใช้งานได้ชั่วคราว กรุณาลองใหม่อีกครั้งในภายหลัง\",\n    \"aiServerUnavailable\": \"การเชื่อมต่อขาดหาย กรุณาตรวจสอบอินเทอร์เน็ตของคุณ\",\n    \"retry\": \"ลองใหม่อีกครั้ง\",\n    \"clickToRetry\": \"คลิกเพื่อลองใหม่อีกครั้ง\",\n    \"regenerateAnswer\": \"สร้างใหม่\",\n    \"question1\": \"วิธีการใช้ Kanban เพื่อจัดการงาน\",\n    \"question2\": \"อธิบายวิธี GTD\",\n    \"question3\": \"เหตุใดจึงใช้ Rust\",\n    \"question4\": \"สูตรอาหารจากสิ่งที่มีในครัวของฉัน\",\n    \"question5\": \"สร้างภาพประกอบให้กับหน้าของฉัน\",\n    \"question6\": \"จัดทำรายการสิ่งที่ต้องทำสำหรับสัปดาห์หน้าของฉัน\",\n    \"aiMistakePrompt\": \"AI อาจทำผิดพลาดได้ กรุณาตรวจสอบข้อมูลที่สำคัญ\",\n    \"chatWithFilePrompt\": \"คุณต้องการแชทกับไฟล์หรือไม่?\",\n    \"indexFileSuccess\": \"สร้างดัชนีไฟล์สำเร็จแล้ว\",\n    \"inputActionNoPages\": \"ไม่พบผลลัพธ์จากหน้า\",\n    \"referenceSource\": {\n      \"zero\": \"ไม่พบแหล่งข้อมูล 0 รายการ\",\n      \"one\": \"พบแหล่งข้อมูล {count} รายการ\",\n      \"other\": \"พบแหล่งข้อมูล {count} รายการ\"\n    },\n    \"clickToMention\": \"อ้างอิงถึงหน้า\",\n    \"uploadFile\": \"แนบไฟล์ PDF, ข้อความ หรือไฟล์ Markdown\",\n    \"questionDetail\": \"สวัสดี {}! วันนี้ฉันสามารถช่วยอะไรคุณได้บ้าง?\",\n    \"indexingFile\": \"การจัดทำดัชนี {}\",\n    \"generatingResponse\": \"กำลังสร้างการตอบกลับ\"\n  },\n  \"trash\": {\n    \"text\": \"ขยะ\",\n    \"restoreAll\": \"กู้คืนทั้งหมด\",\n    \"restore\": \"กู้คืน\",\n    \"deleteAll\": \"ลบทั้งหมด\",\n    \"pageHeader\": {\n      \"fileName\": \"ชื่อไฟล์\",\n      \"lastModified\": \"แก้ไขครั้งล่าสุด\",\n      \"created\": \"สร้างขึ้น\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"คุณแน่ใจหรือว่าจะลบหน้าทั้งหมดในถังขยะ\",\n      \"caption\": \"การดำเนินการนี้ไม่สามารถยกเลิกได้\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"คุณแน่ใจหรือว่าจะกู้คืนทุกหน้าในถังขยะ\",\n      \"caption\": \"การดำเนินการนี้ไม่สามารถยกเลิกได้\"\n    },\n    \"restorePage\": {\n      \"title\": \"กู้คืน: {}\",\n      \"caption\": \"คุณแน่ใจหรือไม่ว่าต้องการกู้คืนหน้านี้?\"\n    },\n    \"mobile\": {\n      \"actions\": \"การดำเนินการถังขยะ\",\n      \"empty\": \"ถังขยะว่างเปล่า\",\n      \"emptyDescription\": \"คุณไม่มีไฟล์ที่ถูกลบ\",\n      \"isDeleted\": \"ถูกลบแล้ว\",\n      \"isRestored\": \"ถูกกู้คืนแล้ว\"\n    },\n    \"confirmDeleteTitle\": \"คุณแน่ใจหรือไม่ว่าต้องการลบหน้านี้อย่างถาวร?\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"หน้านี้อยู่ในถังขยะ\",\n    \"restore\": \"กู้คืนหน้า\",\n    \"deletePermanent\": \"ลบถาวร\",\n    \"deletePermanentDescription\": \"คุณแน่ใจหรือไม่ว่าต้องการลบหน้านี้อย่างถาวร? การกระทำนี้ไม่สามารถย้อนกลับได้\"\n  },\n  \"dialogCreatePageNameHint\": \"ชื่อหน้า\",\n  \"questionBubble\": {\n    \"shortcuts\": \"ทางลัด\",\n    \"whatsNew\": \"มีอะไรใหม่?\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"ข้อมูลดีบัก\",\n      \"success\": \"คัดลอกข้อมูลดีบักไปยังคลิปบอร์ดแล้ว!\",\n      \"fail\": \"ไม่สามารถคัดลอกข้อมูลดีบักไปยังคลิปบอร์ด\"\n    },\n    \"feedback\": \"ข้อเสนอแนะ\",\n    \"help\": \"ช่วยเหลือและสนับสนุน\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"ลบ เปลี่ยนชื่อ และอื่นๆ...\",\n    \"addPageTooltip\": \"เพิ่มหน้าข้างในอย่างรวดเร็ว\",\n    \"defaultNewPageName\": \"ไม่มีชื่อ\",\n    \"renameDialog\": \"เปลี่ยนชื่อ\"\n  },\n  \"noPagesInside\": \"ไม่มีหน้าข้างใน\",\n  \"toolbar\": {\n    \"undo\": \"เลิกทำ\",\n    \"redo\": \"ทำซ้ำ\",\n    \"bold\": \"ตัวหนา\",\n    \"italic\": \"ตัวเอียง\",\n    \"underline\": \"ขีดเส้นใต้\",\n    \"strike\": \"ขีดฆ่า\",\n    \"numList\": \"รายการลำดับตัวเลข\",\n    \"bulletList\": \"รายการลำดับหัวข้อย่อย\",\n    \"checkList\": \"รายการลำดับ Check\",\n    \"inlineCode\": \"อินไลน์โค้ด\",\n    \"quote\": \"บล็อกคำกล่าว\",\n    \"header\": \"หัวข้อ\",\n    \"highlight\": \"ไฮไลท์\",\n    \"color\": \"สี\",\n    \"addLink\": \"เพิ่มลิงก์\",\n    \"link\": \"ลิงก์\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"สลับไปที่โหมดสว่าง\",\n    \"darkMode\": \"สลับไปที่โหมดมืด\",\n    \"openAsPage\": \"เปิดเป็นหน้า\",\n    \"addNewRow\": \"เพิ่มแถวใหม่\",\n    \"openMenu\": \"คลิกเพื่อเปิดเมนู\",\n    \"dragRow\": \"กดค้างเพื่อเรียงลำดับแถวใหม่\",\n    \"viewDataBase\": \"ดูฐานข้อมูล\",\n    \"referencePage\": \"{name} ถูกอ้างอิงถึง\",\n    \"addBlockBelow\": \"เพิ่มบล็อกด้านล่าง\",\n    \"aiGenerate\": \"สร้าง\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"ปิดแถบด้านข้าง\",\n    \"openSidebar\": \"เปิดแถบด้านข้าง\",\n    \"personal\": \"ส่วนบุคคล\",\n    \"private\": \"ส่วนตัว\",\n    \"workspace\": \"พื้นที่ทำงาน\",\n    \"favorites\": \"รายการโปรด\",\n    \"clickToHidePrivate\": \"คลิกเพื่อซ่อนพื้นที่ส่วนตัว\\nหน้าที่คุณสร้างที่นี่จะแสดงให้เห็นเฉพาะคุณเท่านั้น\",\n    \"clickToHideWorkspace\": \"คลิกเพื่อซ่อนพื้นที่ทำงาน\\nหน้าที่คุณสร้างที่นี่จะแสดงให้สมาชิกทุกคนเห็น\",\n    \"clickToHidePersonal\": \"คลิกเพื่อซ่อนส่วนส่วนบุคคล\",\n    \"clickToHideFavorites\": \"คลิกเพื่อซ่อนส่วนรายการโปรด\",\n    \"addAPage\": \"เพิ่มหน้า\",\n    \"addAPageToPrivate\": \"เพิ่มหน้าไปยังพื้นที่ส่วนตัว\",\n    \"addAPageToWorkspace\": \"เพิ่มหน้าไปยังพื้นที่ทำงาน\",\n    \"recent\": \"ล่าสุด\",\n    \"today\": \"วันนี้\",\n    \"thisWeek\": \"สัปดาห์นี้\",\n    \"others\": \"รายการโปรดก่อนหน้านี้\",\n    \"earlier\": \"ก่อนหน้านี้\",\n    \"justNow\": \"เมื่อสักครู่\",\n    \"minutesAgo\": \"{count} นาทีที่ผ่านมา\",\n    \"lastViewed\": \"ดูล่าสุด\",\n    \"favoriteAt\": \"รายการโปรด\",\n    \"emptyRecent\": \"ไม่มีหน้าล่าสุด\",\n    \"emptyRecentDescription\": \"เมื่อคุณดูหน้าเพจ หน้าเหล่านั้นจะปรากฏที่นี่เพื่อให้คุณสามารถเรียกดูได้ง่าย\",\n    \"emptyFavorite\": \"ไม่มีหน้าโปรด\",\n    \"emptyFavoriteDescription\": \"ทำเครื่องหมายหน้าเป็นรายการโปรด—หน้าเหล่านี้จะแสดงไว้ที่นี่เพื่อการเข้าถึงอย่างรวดเร็ว!\",\n    \"removePageFromRecent\": \"ลบหน้านี้ออกจากรายการล่าสุดหรือไม่?\",\n    \"removeSuccess\": \"ลบสำเร็จ\",\n    \"favoriteSpace\": \"รายการโปรด\",\n    \"RecentSpace\": \"ล่าสุด\",\n    \"Spaces\": \"พื้นที่\",\n    \"upgradeToPro\": \"อัพเกรดเป็น Pro\",\n    \"upgradeToAIMax\": \"ปลดล็อค AI แบบไม่จำกัด\",\n    \"storageLimitDialogTitle\": \"คุณใช้พื้นที่จัดเก็บฟรีหมดแล้ว กรุณาอัปเกรดเพื่อปลดล็อกพื้นที่จัดเก็บแบบไม่จำกัด\",\n    \"storageLimitDialogTitleIOS\": \"คุณใช้พื้นที่จัดเก็บฟรีหมดแล้ว\",\n    \"aiResponseLimitTitle\": \"คุณใช้จำนวนการตอบกลับ AI ฟรีหมดแล้ว กรุณาอัปเกรดเป็นแผน Pro หรือซื้อส่วนเสริม AI เพื่อปลดล็อกการตอบกลับไม่จำกัด\",\n    \"aiResponseLimitDialogTitle\": \"ถึงขีดจำกัดการตอบกลับ AI แล้ว\",\n    \"aiResponseLimit\": \"คุณใช้จำนวนการตอบกลับ AI ฟรีหมดแล้ว\\nไปที่ การตั้งค่า -> แผน -> คลิก AI Max หรือแผน Pro เพื่อรับการตอบกลับ AI เพิ่มเติม\",\n    \"askOwnerToUpgradeToPro\": \"พื้นที่จัดเก็บฟรีในพื้นที่ทำงานของคุณกำลังจะหมด กรุณาขอให้เจ้าของพื้นที่ทำงานของคุณอัปเกรดเป็นแผน Pro\",\n    \"askOwnerToUpgradeToProIOS\": \"พื้นที่จัดเก็บฟรีในพื้นที่ทำงานของคุณกำลังจะหมด\",\n    \"askOwnerToUpgradeToAIMax\": \"พื้นที่ทำงานของคุณใช้จำนวนการตอบกลับ AI ฟรีหมดแล้ว กรุณาขอให้เจ้าของพื้นที่ทำงานของคุณอัปเกรดแผนหรือซื้อส่วนเสริม AI\",\n    \"askOwnerToUpgradeToAIMaxIOS\": \"จำนวนการตอบกลับ AI ฟรี ในพื้นที่ทำงานของคุณใกล้หมดแล้ว\",\n    \"purchaseStorageSpace\": \"ซื้อพื้นที่จัดเก็บข้อมูล\",\n    \"singleFileProPlanLimitationDescription\": \"คุณอัปโหลดไฟล์เกินขนาดสูงสุดที่อนุญาตในแผนฟรี โปรดอัปเกรดเป็นแผน Pro เพื่ออัปโหลดไฟล์ขนาดใหญ่ขึ้น\",\n    \"purchaseAIResponse\": \"ซื้อ \",\n    \"askOwnerToUpgradeToLocalAI\": \"กรุณาขอให้เจ้าของพื้นที่ทำงานเปิดใช้งาน AI บนอุปกรณ์\",\n    \"upgradeToAILocal\": \"เรียกใช้โมเดลบนอุปกรณ์ของคุณเพื่อความเป็นส่วนตัวสูงสุด\",\n    \"upgradeToAILocalDesc\": \"สนทนากับ PDFs ปรับปรุงการเขียนของคุณ และเติมข้อมูลในตารางอัตโนมัติด้วย AI ภายในเครื่อง\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"ส่งออกบันทึกย่อไปยัง Markdown\",\n      \"path\": \"Documents/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"รายชื่อผู้ติดต่อ\",\n    \"whatsHappening\": \"เกิดอะไรขึ้นในสัปดาห์นี้บ้าง?\",\n    \"addContact\": \"เพิ่มผู้ติดต่อ\",\n    \"editContact\": \"แก้ไขผู้ติดต่อ\"\n  },\n  \"button\": {\n    \"ok\": \"ตกลง\",\n    \"confirm\": \"ยืนยัน\",\n    \"done\": \"เสร็จแล้ว\",\n    \"cancel\": \"ยกเลิก\",\n    \"signIn\": \"ลงชื่อเข้าใช้\",\n    \"signOut\": \"ออกจากระบบ\",\n    \"complete\": \"สมบูรณ์\",\n    \"save\": \"บันทึก\",\n    \"generate\": \"สร้าง\",\n    \"esc\": \"ESC\",\n    \"keep\": \"เก็บ\",\n    \"tryAgain\": \"ลองอีกครั้ง\",\n    \"discard\": \"ทิ้ง\",\n    \"replace\": \"แทนที่\",\n    \"insertBelow\": \"ใส่ด้านล่าง\",\n    \"insertAbove\": \"ใส่ด้านบน\",\n    \"upload\": \"อัปโหลด\",\n    \"edit\": \"แก้ไข\",\n    \"delete\": \"ลบ\",\n    \"copy\": \"คัดลอก\",\n    \"duplicate\": \"ทำสำเนา\",\n    \"putback\": \"นำกลับมา\",\n    \"update\": \"อัปโหลด\",\n    \"share\": \"แชร์\",\n    \"removeFromFavorites\": \"ลบออกจากรายการโปรด\",\n    \"removeFromRecent\": \"ลบออกจากรายการล่าสุด\",\n    \"addToFavorites\": \"เพิ่มในรายการโปรด\",\n    \"favoriteSuccessfully\": \"เพิ่มในรายการที่ชื่นชอบสำเร็จ\",\n    \"unfavoriteSuccessfully\": \"นำออกจากรายการที่ชื่นชอบสำเร็จ\",\n    \"duplicateSuccessfully\": \"ทำสำเนาสำเร็จแล้ว\",\n    \"rename\": \"เปลี่ยนชื่อ\",\n    \"helpCenter\": \"ศูนย์ช่วยเหลือ\",\n    \"add\": \"เพิ่ม\",\n    \"yes\": \"ใช่\",\n    \"no\": \"ไม่\",\n    \"clear\": \"ล้าง\",\n    \"remove\": \"ลบ\",\n    \"dontRemove\": \"ไม่ลบ\",\n    \"copyLink\": \"คัดลอกลิงค์\",\n    \"align\": \"จัดตำแหน่ง\",\n    \"login\": \"เข้าสู่ระบบ\",\n    \"logout\": \"ออกจากระบบ\",\n    \"deleteAccount\": \"ลบบัญชี\",\n    \"back\": \"กลับ\",\n    \"signInGoogle\": \"ดำเนินการต่อด้วย Google\",\n    \"signInGithub\": \"ดำเนินการต่อด้วย GitHub\",\n    \"signInDiscord\": \"ดำเนินการต่อด้วย Discord\",\n    \"more\": \"เพิ่มเติม\",\n    \"create\": \"สร้าง\",\n    \"close\": \"ปิด\",\n    \"next\": \"ถัดไป\",\n    \"previous\": \"ก่อนหน้า\",\n    \"submit\": \"ส่ง\",\n    \"download\": \"ดาวน์โหลด\",\n    \"backToHome\": \"กลับสู่หน้าแรก\",\n    \"viewing\": \"การดู\",\n    \"editing\": \"การแก้ไข\",\n    \"gotIt\": \"เข้าใจแล้ว\"\n  },\n  \"label\": {\n    \"welcome\": \"ยินดีต้อนรับ!\",\n    \"firstName\": \"ชื่อจริง\",\n    \"middleName\": \"ชื่อกลาง\",\n    \"lastName\": \"นามสกุล\",\n    \"stepX\": \"ขั้นตอนที่ {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"ไม่สามารถเชื่อมต่อกับบัญชีของคุณได้\",\n      \"failedMsg\": \"โปรดตรวจสอบให้แน่ใจว่าคุณได้ดำเนินการลงชื่อเข้าใช้ในเบราว์เซอร์ของคุณเรียบร้อยแล้ว\"\n    },\n    \"google\": {\n      \"title\": \"ลงชื่อเข้าใช้ GOOGLE\",\n      \"instruction1\": \"ในการนำเข้า Google Contacts คุณจะต้องอนุญาตแอปพลิเคชันนี้โดยใช้เว็บเบราว์เซอร์ของคุณ\",\n      \"instruction2\": \"คัดลอกโค้ดนี้ไปยังคลิปบอร์ดของคุณโดยคลิกที่ไอคอนหรือเลือกข้อความ:\",\n      \"instruction3\": \"ไปที่ลิงก์ต่อไปนี้ในเว็บเบราว์เซอร์ของคุณ และป้อนโค้ดด้านบน:\",\n      \"instruction4\": \"กดปุ่มด้านล่างเมื่อคุณลงทะเบียนสำเร็จแล้ว:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"การตั้งค่า\",\n    \"popupMenuItem\": {\n      \"settings\": \"การตั้งค่า\",\n      \"members\": \"สมาชิก\",\n      \"trash\": \"ถังขยะ\",\n      \"helpAndSupport\": \"ความช่วยเหลือ & การสนับสนุน\"\n    },\n    \"sites\": {\n      \"title\": \"ไซต์\",\n      \"namespaceTitle\": \"เนมสเปซ\",\n      \"namespaceDescription\": \"จัดการเนมสเปซและโฮมเพจของคุณ\",\n      \"namespaceHeader\": \"เนมสเปซ\",\n      \"homepageHeader\": \"หน้าแรก\",\n      \"updateNamespace\": \"อัปเดตเนมสเปซ\",\n      \"removeHomepage\": \"ลบหน้าแรก\",\n      \"selectHomePage\": \"เลือกหน้า\",\n      \"clearHomePage\": \"ล้างโฮมเพจสำหรับเนมสเปซนี้\",\n      \"customUrl\": \"URL แบบกำหนดเอง\",\n      \"namespace\": {\n        \"description\": \"การเปลี่ยนแปลงนี้จะมีผลกับหน้าที่เผยแพร่ทั้งหมดที่เปิดใช้งานในพื้นที่นี้\",\n        \"tooltip\": \"เราขอสงวนสิทธิ์ในการลบเนมสเปซที่ไม่เหมาะสม\",\n        \"updateExistingNamespace\": \"อัปเดตเนมสเปซที่มีอยู่\",\n        \"upgradeToPro\": \"อัปเกรดเป็นแผน Pro เพื่อกำหนดหน้าโฮมเพจ\",\n        \"redirectToPayment\": \"กำลังเปลี่ยนเส้นทางไปยังหน้าชำระเงิน...\",\n        \"onlyWorkspaceOwnerCanSetHomePage\": \"เฉพาะเจ้าของพื้นที่ทำงานเท่านั้นที่สามารถกำหนดหน้าโฮมเพจได้\",\n        \"pleaseAskOwnerToSetHomePage\": \"กรุณาขอให้เจ้าของพื้นที่ทำงานอัปเกรดเป็นแผน Pro\"\n      },\n      \"publishedPage\": {\n        \"title\": \"หน้าที่เผยแพร่ทั้งหมด\",\n        \"description\": \"จัดการหน้าที่เผยแพร่ของคุณ\",\n        \"page\": \"หน้า\",\n        \"pathName\": \"ชื่อเส้นทาง\",\n        \"date\": \"วันที่เผยแพร่\",\n        \"emptyHinText\": \"คุณไม่มีหน้าที่เผยแพร่ในพื้นที่ทำงานนี้\",\n        \"noPublishedPages\": \"ยังไม่มีหน้าที่เผยแพร่\",\n        \"settings\": \"การตั้งค่าการเผยแพร่\",\n        \"clickToOpenPageInApp\": \"เปิดหน้าในแอป\",\n        \"clickToOpenPageInBrowser\": \"เปิดหน้าในเบราว์เซอร์\"\n      },\n      \"error\": {\n        \"failedToGeneratePaymentLink\": \"ไม่สามารถสร้างลิงก์การชำระเงินสำหรับแผน Pro ได้\",\n        \"failedToUpdateNamespace\": \"ไม่สามารถอัปเดตเนมสเปซได้\",\n        \"proPlanLimitation\": \"คุณต้องอัปเกรดเป็นแผน Pro เพื่ออัปเดตเนมสเปซ\",\n        \"namespaceAlreadyInUse\": \"เนมสเปซนี้ถูกใช้ไปแล้ว กรุณาลองชื่ออื่น\",\n        \"invalidNamespace\": \"เนมสเปซไม่ถูกต้อง กรุณาลองชื่ออื่น\",\n        \"namespaceLengthAtLeast2Characters\": \"เนมสเปซต้องมีความยาวอย่างน้อย 2 ตัวอักษร\",\n        \"onlyWorkspaceOwnerCanUpdateNamespace\": \"เฉพาะเจ้าของพื้นที่ทำงานเท่านั้นที่สามารถอัปเดตเนมสเปซได้\",\n        \"onlyWorkspaceOwnerCanRemoveHomepage\": \"เฉพาะเจ้าของพื้นที่ทำงานเท่านั้นที่สามารถลบหน้าโฮมเพจได้\",\n        \"setHomepageFailed\": \"ไม่สามารถตั้งค่าหน้าแรกได้\",\n        \"namespaceTooLong\": \"เนมสเปซยาวเกินไป กรุณาลองชื่ออื่น\",\n        \"namespaceTooShort\": \"เนมสเปซสั้นเกินไป กรุณาลองชื่ออื่น\",\n        \"namespaceIsReserved\": \"เนมสเปซนี้ถูกจองไว้แล้ว กรุณาลองชื่ออื่น\",\n        \"updatePathNameFailed\": \"ไม่สามารถอัปเดตชื่อเส้นทางได้\",\n        \"removeHomePageFailed\": \"ไม่สามารถลบหน้าแรกได้\",\n        \"publishNameContainsInvalidCharacters\": \"ชื่อเส้นทางมีอักขระที่ไม่ถูกต้อง โปรดลองอักขระอื่น\",\n        \"publishNameTooShort\": \"ชื่อเส้นทางสั้นเกินไป กรุณาลองชื่ออื่น\",\n        \"publishNameTooLong\": \"ชื่อเส้นทางยาวเกินไป กรุณาลองชื่ออื่น\",\n        \"publishNameAlreadyInUse\": \"ชื่อเส้นทางนี้ถูกใช้ไปแล้ว กรุณาลองชื่ออื่น\",\n        \"namespaceContainsInvalidCharacters\": \"เนมสเปซมีอักขระที่ไม่ถูกต้อง โปรดลองอักขระอื่น\",\n        \"publishPermissionDenied\": \"เฉพาะเจ้าของพื้นที่ทำงานหรือผู้เผยแพร่เพจเท่านั้นที่สามารถจัดการการตั้งค่าการเผยแพร่ได้\",\n        \"publishNameCannotBeEmpty\": \"ชื่อเส้นทางไม่สามารถเว้นว่างได้ กรุณาลองชื่ออื่น\"\n      },\n      \"success\": {\n        \"namespaceUpdated\": \"อัปเดตเนมสเปซสำเร็จ\",\n        \"setHomepageSuccess\": \"ตั้งค่าหน้าแรกสำเร็จ\",\n        \"updatePathNameSuccess\": \"อัปเดตชื่อเส้นทางสำเร็จ\",\n        \"removeHomePageSuccess\": \"ลบหน้าแรกสำเร็จ\"\n      }\n    },\n    \"accountPage\": {\n      \"menuLabel\": \"บัญชีของฉัน\",\n      \"title\": \"บัญชีของฉัน\",\n      \"general\": {\n        \"title\": \"ชื่อบัญชีและรูปโปรไฟล์\",\n        \"changeProfilePicture\": \"เปลี่ยนรูปโปรไฟล์\"\n      },\n      \"email\": {\n        \"title\": \"อีเมล\",\n        \"actions\": {\n          \"change\": \"เปลี่ยนอีเมล\"\n        }\n      },\n      \"login\": {\n        \"title\": \"การเข้าสู่ระบบบัญชี\",\n        \"loginLabel\": \"เข้าสู่ระบบ\",\n        \"logoutLabel\": \"ออกจากระบบ\"\n      }\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"พื้นที่ทำงาน\",\n      \"title\": \"พื้นที่ทำงาน\",\n      \"description\": \"ปรับแต่งรูปลักษณ์ของพื้นที่ทำงาน ธีม แบบอักษร เค้าโครงข้อความ รูปแบบวันที่/เวลา และภาษา\",\n      \"workspaceName\": {\n        \"title\": \"ชื่อพื้นที่ทำงาน\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"ไอคอนพื้นที่ทำงาน\",\n        \"description\": \"อัปโหลดรูปภาพ หรือใช้อีโมจิสำหรับพื้นที่ทำงานของคุณ ไอคอนจะแสดงในแถบด้านข้าง และการแจ้งเตือนของคุณ\"\n      },\n      \"appearance\": {\n        \"title\": \"ลักษณะการแสดงผล\",\n        \"description\": \"ปรับแต่งรูปลักษณ์พื้นที่ทำงาน ธีม แบบอักษร เค้าโครงข้อความ วันที่ เวลา และภาษาของคุณ\",\n        \"options\": {\n          \"system\": \"อัตโนมัติ\",\n          \"light\": \"สว่าง\",\n          \"dark\": \"มืด\"\n        }\n      },\n      \"resetCursorColor\": {\n        \"title\": \"รีเซ็ตสีของเคอร์เซอร์ในเอกสาร\",\n        \"description\": \"คุณแน่ใจหรือไม่ว่าต้องการรีเซ็ตสีของเคอร์เซอร์?\"\n      },\n      \"resetSelectionColor\": {\n        \"title\": \"รีเซ็ตสีการเลือกในเอกสาร\",\n        \"description\": \"คุณแน่ใจหรือไม่ว่าต้องการรีเซ็ตสีการเลือก?\"\n      },\n      \"resetWidth\": {\n        \"resetSuccess\": \"รีเซ็ตความกว้างของเอกสารสำเร็จ\"\n      },\n      \"theme\": {\n        \"title\": \"ธีม\",\n        \"description\": \"เลือกธีมที่ตั้งไว้ล่วงหน้าหรืออัปโหลดธีมที่คุณกำหนดเอง\",\n        \"uploadCustomThemeTooltip\": \"อัพโหลดธีมที่กำหนดเอง\"\n      },\n      \"workspaceFont\": {\n        \"title\": \"ฟอนต์ของพื้นที่ทำงาน\",\n        \"noFontHint\": \"ไม่พบฟอนต์ กรุณาลองคำค้นหาอื่น\"\n      },\n      \"textDirection\": {\n        \"title\": \"ทิศทางข้อความ\",\n        \"leftToRight\": \"จากซ้ายไปขวา\",\n        \"rightToLeft\": \"จากขวาไปซ้าย\",\n        \"auto\": \"อัตโนมัติ\",\n        \"enableRTLItems\": \"เปิดใช้งานแถบเครื่องมือ RTL (จากขวาไปซ้าย)\"\n      },\n      \"layoutDirection\": {\n        \"title\": \"ทิศทางการจัดวาง\",\n        \"leftToRight\": \"จากซ้ายไปขวา\",\n        \"rightToLeft\": \"จากขวาไปซ้าย\"\n      },\n      \"dateTime\": {\n        \"title\": \"วันที่และเวลา\",\n        \"example\": \"{} ที่ {} ({})\",\n        \"24HourTime\": \"เวลาแบบ 24 ชั่วโมง\",\n        \"dateFormat\": {\n          \"label\": \"รูปแบบวันที่\",\n          \"local\": \"ท้องถิ่น\",\n          \"us\": \"US\",\n          \"iso\": \"ISO\",\n          \"friendly\": \"ที่เข้าใจง่าย\",\n          \"dmy\": \"D/M/Y\"\n        }\n      },\n      \"language\": {\n        \"title\": \"ภาษา\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"ลบพื้นที่ทำงาน\",\n        \"content\": \"คุณแน่ใจหรือไม่ว่าต้องการลบพื้นที่ทำงานนี้? การดำเนินการนี้ไม่สามารถย้อนกลับได้ และหน้าใดๆ ที่คุณเผยแพร่ไปแล้วจะถูกยกเลิกการเผยแพร่\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"ออกจากพื้นที่ทำงาน\",\n        \"content\": \"คุณแน่ใจหรือไม่ว่าต้องการออกจากพื้นที่ทำงานนี้? คุณจะสูญเสียการเข้าถึงหน้าและข้อมูลทั้งหมดภายในพื้นที่นี้\",\n        \"success\": \"คุณได้ออกจากพื้นที่ทำงานเรียบร้อยแล้ว\",\n        \"fail\": \"ไม่สามารถออกจากพื้นที่ทำงานได้\"\n      },\n      \"manageWorkspace\": {\n        \"title\": \"จัดการพื้นที่ทำงาน\",\n        \"leaveWorkspace\": \"ออกจากพื้นที่ทำงาน\",\n        \"deleteWorkspace\": \"ลบพื้นที่ทำงาน\"\n      }\n    },\n    \"manageDataPage\": {\n      \"menuLabel\": \"การจัดการข้อมูล\",\n      \"title\": \"การจัดการข้อมูล\",\n      \"description\": \"จัดการพื้นที่จัดเก็บข้อมูลในเครื่องหรือนำเข้าข้อมูลที่มีอยู่ของคุณลงใน @:appName\",\n      \"dataStorage\": {\n        \"title\": \"ตำแหน่งจัดเก็บไฟล์\",\n        \"tooltip\": \"ตำแหน่งที่จัดเก็บไฟล์ของคุณ\",\n        \"actions\": {\n          \"change\": \"เปลี่ยนเส้นทาง\",\n          \"open\": \"เปิดโฟลเดอร์\",\n          \"openTooltip\": \"เปิดตำแหน่งโฟลเดอร์ข้อมูลปัจจุบัน\",\n          \"copy\": \"คัดลอกเส้นทาง\",\n          \"copiedHint\": \"คัดลอกเส้นทางแล้ว!\",\n          \"resetTooltip\": \"รีเซ็ตเป็นตำแหน่งเริ่มต้น\"\n        },\n        \"resetDialog\": {\n          \"title\": \"คุณแน่ใจหรือไม่?\",\n          \"description\": \"การรีเซ็ตเส้นทางไปยังตำแหน่งข้อมูลเริ่มต้นจะไม่ลบข้อมูลของคุณ หากคุณต้องการนำเข้าข้อมูลปัจจุบันอีกครั้ง ควรคัดลอกเส้นทางของตำแหน่งปัจจุบันก่อน\"\n        }\n      },\n      \"importData\": {\n        \"title\": \"นำเข้าข้อมูล\",\n        \"tooltip\": \"นำเข้าข้อมูลจากการสำรองข้อมูล/โฟลเดอร์ข้อมูล @:appName\",\n        \"description\": \"คัดลอกข้อมูลจากโฟลเดอร์ข้อมูลภายนอกของ @:appName\",\n        \"action\": \"เรียกดูไฟล์\"\n      },\n      \"encryption\": {\n        \"title\": \"การเข้ารหัส\",\n        \"tooltip\": \"จัดการวิธีการจัดเก็บและเข้ารหัสข้อมูลของคุณ\",\n        \"descriptionNoEncryption\": \"การเปิดใช้งานการเข้ารหัสจะทำให้ข้อมูลทั้งหมดถูกเข้ารหัส การกระทำนี้ไม่สามารถย้อนกลับได้\",\n        \"descriptionEncrypted\": \"ข้อมูลของคุณได้รับการเข้ารหัสแล้ว\",\n        \"action\": \"เข้ารหัสข้อมูล\",\n        \"dialog\": {\n          \"title\": \"ต้องการเข้ารหัสข้อมูลทั้งหมดของคุณหรือไม่?\",\n          \"description\": \"การเข้ารหัสข้อมูลทั้งหมดของคุณจะช่วยให้ข้อมูลของคุณปลอดภัยและมั่นคง การดำเนินการนี้ไม่สามารถย้อนกลับได้ คุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?\"\n        }\n      },\n      \"cache\": {\n        \"title\": \"ล้างแคช\",\n        \"description\": \"ช่วยแก้ไขปัญหาต่างๆ เช่น รูปภาพไม่โหลด หน้าหายไปในช่องว่าง และฟอนต์ไม่โหลด ซึ่งจะไม่กระทบกับข้อมูลของคุณ\",\n        \"dialog\": {\n          \"title\": \"ล้างแคช\",\n          \"description\": \"ช่วยแก้ไขปัญหาต่างๆ เช่น รูปภาพไม่โหลด หน้าหายไปในช่องว่าง และฟอนต์ไม่โหลด ซึ่งจะไม่กระทบกับข้อมูลของคุณ\",\n          \"successHint\": \"ล้างแคชแล้ว!\"\n        }\n      },\n      \"data\": {\n        \"fixYourData\": \"ซ่อมแซมข้อมูลของคุณ\",\n        \"fixButton\": \"ซ่อมแซม\",\n        \"fixYourDataDescription\": \"หากคุณพบปัญหากับข้อมูลของคุณ คุณสามารถลองแก้ไขได้ที่นี่\"\n      }\n    },\n    \"shortcutsPage\": {\n      \"menuLabel\": \"ทางลัด\",\n      \"title\": \"ทางลัด\",\n      \"editBindingHint\": \"ป้อนการผูกปุ่มลัดใหม่\",\n      \"searchHint\": \"ค้นหา\",\n      \"actions\": {\n        \"resetDefault\": \"รีเซ็ตค่าเริ่มต้น\"\n      },\n      \"errorPage\": {\n        \"message\": \"ไม่สามารถโหลดทางลัดได้: {}\",\n        \"howToFix\": \"โปรดลองอีกครั้ง หากปัญหายังคงอยู่ โปรดติดต่อบน GitHub\"\n      },\n      \"resetDialog\": {\n        \"title\": \"รีเซ็ตทางลัด\",\n        \"description\": \"การดำเนินการนี้จะรีเซ็ตการตั้งค่าปุ่มทั้งหมดของคุณเป็นค่าเริ่มต้น คุณไม่สามารถย้อนกลับได้ในภายหลัง คุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?\",\n        \"buttonLabel\": \"รีเซ็ต\"\n      },\n      \"conflictDialog\": {\n        \"title\": \"{} กำลังใช้งานอยู่ในขณะนี้\",\n        \"descriptionPrefix\": \"การกำหนดปุ่มลัดนี้กำลังถูกใช้งานโดย\",\n        \"descriptionSuffix\": \". หากคุณแทนที่ปุ่มลัดนี้ ปุ่มลัดนี้จะถูกลบออกจาก {}\",\n        \"confirmLabel\": \"ดำเนินการต่อ\"\n      },\n      \"editTooltip\": \"กดเพื่อเริ่มแก้ไขปุ่มลัด\",\n      \"keybindings\": {\n        \"toggleToDoList\": \"เปิดปิดรายการสิ่งที่ต้องทำ\",\n        \"insertNewParagraphInCodeblock\": \"แทรกย่อหน้าใหม่\",\n        \"pasteInCodeblock\": \"วางในโค้ดบล็อค\",\n        \"selectAllCodeblock\": \"เลือกทั้งหมด\",\n        \"indentLineCodeblock\": \"แทรกช่องว่างสองช่องที่จุดเริ่มต้นของบรรทัด\",\n        \"outdentLineCodeblock\": \"ลบช่องว่างสองช่องที่จุดเริ่มต้นของบรรทัด\",\n        \"twoSpacesCursorCodeblock\": \"แทรกช่องว่างสองช่องที่เคอร์เซอร์\",\n        \"copy\": \"คัดลอกการเลือก\",\n        \"paste\": \"วางในเนื้อหา\",\n        \"cut\": \"ตัดการเลือก\",\n        \"alignLeft\": \"จัดข้อความให้ชิดซ้าย\",\n        \"alignCenter\": \"จัดตำแหน่งข้อความให้อยู่กึ่งกลาง\",\n        \"alignRight\": \"จัดข้อความให้ชิดขวา\",\n        \"undo\": \"เลิกทำ\",\n        \"redo\": \"ทำซ้ำ\",\n        \"convertToParagraph\": \"แปลงบล็อกเป็นย่อหน้า\",\n        \"backspace\": \"ลบ\",\n        \"deleteLeftWord\": \"ลบคำซ้าย\",\n        \"deleteLeftSentence\": \"ลบประโยคซ้าย\",\n        \"delete\": \"ลบตัวอักษรขวา\",\n        \"deleteMacOS\": \"ลบตัวอักษรซ้าย\",\n        \"deleteRightWord\": \"ลบคำขวา\",\n        \"moveCursorLeft\": \"เลื่อนเคอร์เซอร์ไปทางซ้าย\",\n        \"moveCursorBeginning\": \"เลื่อนเคอร์เซอร์ไปที่จุดเริ่มต้น\",\n        \"moveCursorLeftWord\": \"เลื่อนเคอร์เซอร์ไปทางซ้ายหนึ่งคำ\",\n        \"moveCursorLeftSelect\": \"เลือกและเลื่อนเคอร์เซอร์ไปทางซ้าย\",\n        \"moveCursorBeginSelect\": \"เลือกและเลื่อนเคอร์เซอร์ไปที่จุดเริ่มต้น\",\n        \"moveCursorLeftWordSelect\": \"เลือกและเลื่อนเคอร์เซอร์ไปทางซ้ายหนึ่งคำ\",\n        \"moveCursorRight\": \"เลื่อนเคอร์เซอร์ไปทางขวา\",\n        \"moveCursorEnd\": \"เลื่อนเคอร์เซอร์ไปที่ท้ายสุด\",\n        \"moveCursorRightWord\": \"เลื่อนเคอร์เซอร์ไปทางขวาหนึ่งคำ\",\n        \"moveCursorRightSelect\": \"เลือกและเลื่อนเคอร์เซอร์ไปทางขวาหนึ่งช่อง\",\n        \"moveCursorEndSelect\": \"เลือกและเลื่อนเคอร์เซอร์ไปที่จุดสิ้นสุด\",\n        \"moveCursorRightWordSelect\": \"เลือกและเลื่อนเคอร์เซอร์ไปทางขวาหนึ่งคำ\",\n        \"moveCursorUp\": \"เลื่อนเคอร์เซอร์ขึ้น\",\n        \"moveCursorTopSelect\": \"เลือกและเลื่อนเคอร์เซอร์ไปด้านบน\",\n        \"moveCursorTop\": \"เลื่อนเคอร์เซอร์ไปด้านบน\",\n        \"moveCursorUpSelect\": \"เลือกและเลื่อนเคอร์เซอร์ขึ้น\",\n        \"moveCursorBottomSelect\": \"เลือกและเลื่อนเคอร์เซอร์ไปที่ด้านล่าง\",\n        \"moveCursorBottom\": \"เลื่อนเคอร์เซอร์ไปที่ด้านล่าง\",\n        \"moveCursorDown\": \"เลื่อนเคอร์เซอร์ลง\",\n        \"moveCursorDownSelect\": \"เลือกและเลื่อนเคอร์เซอร์ลง\",\n        \"home\": \"เลื่อนไปด้านบน\",\n        \"end\": \"เลื่อนไปด้านล่าง\",\n        \"toggleBold\": \"เปิดปิดตัวหนา\",\n        \"toggleItalic\": \"เปิดปิดตัวเอียง\",\n        \"toggleUnderline\": \"เปิดปิดขีดเส้นใต้\",\n        \"toggleStrikethrough\": \"เปิดปิดการขีดฆ่า\",\n        \"toggleCode\": \"เปิดปิดโค้ดในบรรทัด\",\n        \"toggleHighlight\": \"เปิดปิดไฮไลท์\",\n        \"showLinkMenu\": \"แสดงลิงค์เมนู\",\n        \"openInlineLink\": \"เปิดลิงก์ในบรรทัด\",\n        \"openLinks\": \"เปิดลิงก์ที่เลือกทั้งหมด\",\n        \"indent\": \"เยื้อง\",\n        \"outdent\": \"เยื้องออก\",\n        \"exit\": \"ออกจากการแก้ไข\",\n        \"pageUp\": \"เลื่อนขึ้นไปหนึ่งหน้า\",\n        \"pageDown\": \"เลื่อนลงมาหนึ่งหน้า\",\n        \"selectAll\": \"เลือกทั้งหมด\",\n        \"pasteWithoutFormatting\": \"วางเนื้อหาที่ไม่มีการจัดรูปแบบ\",\n        \"showEmojiPicker\": \"แสดงตัวเลือกอีโมจิ\",\n        \"enterInTableCell\": \"เพิ่มการแบ่งบรรทัดในตาราง\",\n        \"leftInTableCell\": \"เลื่อนไปทางซ้ายหนึ่งเซลล์ในตาราง\",\n        \"rightInTableCell\": \"เลื่อนไปทางขวาหนึ่งเซลล์ในตาราง\",\n        \"upInTableCell\": \"เลื่อนขึ้นหนึ่งเซลล์ในตาราง\",\n        \"downInTableCell\": \"เลื่อนลงหนึ่งเซลล์ในตาราง\",\n        \"tabInTableCell\": \"ไปยังเซลล์ถัดไปที่ว่างในตาราง\",\n        \"shiftTabInTableCell\": \"ไปยังเซลล์ก่อนหน้าที่ว่างในตาราง\",\n        \"backSpaceInTableCell\": \"หยุดที่จุดเริ่มต้นของเซลล์\"\n      },\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"แทรกย่อหน้าใหม่ถัดจากโค้ดบล็อก\",\n        \"codeBlockIndentLines\": \"แทรกช่องว่างสองช่องที่จุดเริ่มต้นบรรทัดในโค้ดบล็อก\",\n        \"codeBlockOutdentLines\": \"ลบช่องว่างสองช่องที่จุดเริ่มต้นบรรทัดในโค้ดบล็อค\",\n        \"codeBlockAddTwoSpaces\": \"แทรกช่องว่างสองช่องที่ตำแหน่งเคอร์เซอร์ในโค้ดบล็อก\",\n        \"codeBlockSelectAll\": \"เลือกเนื้อหาทั้งหมดภายในโค้ดบล็อก\",\n        \"codeBlockPasteText\": \"วางข้อความลงในโค้ดบล็อค\",\n        \"textAlignLeft\": \"จัดตำแหน่งข้อความให้ชิดซ้าย\",\n        \"textAlignCenter\": \"จัดตำแหน่งข้อความให้อยู่กึ่งกลาง\",\n        \"textAlignRight\": \"จัดตำแหน่งข้อความให้ชิดขวา\"\n      },\n      \"couldNotLoadErrorMsg\": \"ไม่สามารถโหลดทางลัดได้ ลองอีกครั้ง\",\n      \"couldNotSaveErrorMsg\": \"ไม่สามารถบันทึกทางลัดได้ โปรดลองอีกครั้ง\"\n    },\n    \"aiPage\": {\n      \"title\": \"การตั้งค่า AI\",\n      \"menuLabel\": \"การตั้งค่า AI\",\n      \"keys\": {\n        \"enableAISearchTitle\": \"การค้นหาด้วย AI\",\n        \"aiSettingsDescription\": \"เลือกโมเดลที่คุณต้องการใช้เพื่อขับเคลื่อน AppFlowy AI ขณะนี้มี GPT-4, Claude 3.5, Llama 3.1, และ Mistral 7B\",\n        \"loginToEnableAIFeature\": \"ฟีเจอร์ AI จะเปิดใช้งานได้หลังจากเข้าสู่ระบบด้วย @:appName Cloud เท่านั้น หากคุณไม่มีบัญชี @:appName ให้ไปที่ 'บัญชีของฉัน' เพื่อลงทะเบียน\",\n        \"llmModel\": \"โมเดลภาษา\",\n        \"llmModelType\": \"ประเภทของโมเดลภาษา\",\n        \"downloadLLMPrompt\": \"ดาวน์โหลด {}\",\n        \"downloadAppFlowyOfflineAI\": \"การดาวน์โหลดแพ็กเกจ AI แบบออฟไลน์จะทำให้ AI สามารถทำงานบนอุปกรณ์ของคุณได้ คุณต้องการดำเนินการต่อหรือไม่?\",\n        \"downloadLLMPromptDetail\": \"การดาวน์โหลดโมเดล {} ในเครื่องของคุณ จะใช้พื้นที่เก็บข้อมูลสูงสุด {} คุณต้องการดำเนินการต่อหรือไม่\",\n        \"downloadBigFilePrompt\": \"การดาวน์โหลดอาจใช้เวลาประมาณ 10 นาทีให้เสร็จสิ้น\",\n        \"downloadAIModelButton\": \"ดาวน์โหลด\",\n        \"downloadingModel\": \"กำลังดาวน์โหลด\",\n        \"localAILoaded\": \"เพิ่มโมเดล AI ในเครื่องสำเร็จ และพร้อมใช้งานแล้ว\",\n        \"localAIStart\": \"การแชท Local AI กำลังเริ่มต้น...\",\n        \"localAILoading\": \"กำลังโหลดโมเดลแชท Local AI...\",\n        \"localAIStopped\": \"Local AI หยุดทำงานแล้ว\",\n        \"failToLoadLocalAI\": \"ไม่สามารถเริ่มต้น Local AI ได้\",\n        \"restartLocalAI\": \"เริ่มต้น Local AI ใหม่\",\n        \"disableLocalAITitle\": \"ปิดการใช้งาน Local AI\",\n        \"disableLocalAIDescription\": \"คุณต้องการปิดการใช้งาน Local AI หรือไม่?\",\n        \"localAIToggleTitle\": \"สลับเพื่อเปิดหรือปิดใช้งาน Local AI\",\n        \"offlineAIInstruction1\": \"ติดตาม\",\n        \"offlineAIInstruction2\": \"คำแนะนำ\",\n        \"offlineAIInstruction3\": \"เพื่อเปิดใช้งาน AI แบบออฟไลน์\",\n        \"offlineAIDownload1\": \"หากคุณยังไม่ได้ดาวน์โหลด AppFlowy AI, กรุณา\",\n        \"offlineAIDownload2\": \"ดาวน์โหลด\",\n        \"offlineAIDownload3\": \"เป็นอันดับแรก\",\n        \"activeOfflineAI\": \"ใช้งานอยู่\",\n        \"downloadOfflineAI\": \"ดาวน์โหลด\",\n        \"openModelDirectory\": \"เปิดโฟลเดอร์\"\n      }\n    },\n    \"planPage\": {\n      \"menuLabel\": \"แผน\",\n      \"title\": \"แผนราคา\",\n      \"planUsage\": {\n        \"title\": \"สรุปการใช้งานแผน\",\n        \"storageLabel\": \"พื้นที่จัดเก็บ\",\n        \"storageUsage\": \"{} จาก {} GB\",\n        \"unlimitedStorageLabel\": \"พื้นที่เก็บข้อมูลไม่จำกัด\",\n        \"collaboratorsLabel\": \"สมาชิก\",\n        \"collaboratorsUsage\": \"{} จาก {}\",\n        \"aiResponseLabel\": \"การตอบกลับของ AI\",\n        \"aiResponseUsage\": \"{} จาก {}\",\n        \"unlimitedAILabel\": \"การตอบกลับแบบไม่จำกัด\",\n        \"proBadge\": \"Pro\",\n        \"aiMaxBadge\": \"AI Max\",\n        \"aiOnDeviceBadge\": \"AI บนอุปกรณ์สำหรับ Mac\",\n        \"memberProToggle\": \"สมาชิกมากขึ้น และ AI ไม่จำกัด\",\n        \"aiMaxToggle\": \"AI ไม่จำกัด และการเข้าถึงโมเดลขั้นสูง\",\n        \"aiOnDeviceToggle\": \"Local AI เพื่อความเป็นส่วนตัวสูงสุด\",\n        \"aiCredit\": {\n          \"title\": \"เพิ่ม @:appName AI เครดิต\",\n          \"price\": \"{}\",\n          \"priceDescription\": \"สำหรับ 1,000 เครดิต\",\n          \"purchase\": \"ซื้อ AI\",\n          \"info\": \"เพิ่มเครดิต AI 1,000 เครดิตต่อพื้นที่ทำงาน และผสานรวม AI ที่สามารถปรับแต่งได้ เข้าไปในกระบวนการทำงานของคุณได้อย่างราบรื่น เพื่อผลลัพธ์ที่ชาญฉลาด และรวดเร็วยิ่งขึ้นด้วย สูงสุดถึง:\",\n          \"infoItemOne\": \"10,000 การตอบกลับต่อฐานข้อมูล\",\n          \"infoItemTwo\": \"1,000  การตอบกลับต่อพื้นที่ทำงาน\"\n        },\n        \"currentPlan\": {\n          \"bannerLabel\": \"แผนปัจจุบัน\",\n          \"freeTitle\": \"Free\",\n          \"proTitle\": \"Pro\",\n          \"teamTitle\": \"Team\",\n          \"freeInfo\": \"เหมาะสำหรับบุคคลที่มีสมาชิกสูงสุด 2 คน เพื่อจัดระเบียบทุกอย่าง\",\n          \"proInfo\": \"เหมาะสำหรับทีมขนาดเล็ก และขนาดกลางที่มีสมาชิกไม่เกิน 10 คน\",\n          \"teamInfo\": \"เหมาะสำหรับทีมที่มีประสิทธิภาพ และการจัดระเบียบที่ดี\",\n          \"upgrade\": \"เปลี่ยนแผน\",\n          \"canceledInfo\": \"แผนของคุณถูกยกเลิก คุณจะถูกปรับลดเป็นแผน Free ในวันที่ {}\"\n        },\n        \"addons\": {\n          \"title\": \"ส่วนเสริม\",\n          \"addLabel\": \"เพิ่ม\",\n          \"activeLabel\": \"เพิ่มแล้ว\",\n          \"aiMax\": {\n            \"title\": \"AI Max\",\n            \"description\": \"การตอบกลับ AI แบบไม่จำกัด ที่ขับเคลื่อนโดย GPT-4o, Claude 3.5 Sonnet และอื่นๆ\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"ต่อผู้ใช้ ต่อเดือน เก็บค่าบริการเป็นรายปี\"\n          },\n          \"aiOnDevice\": {\n            \"title\": \"AI บนอุปกรณ์สำหรับ Mac\",\n            \"description\": \"เรียกใช้ Mistral 7B, LLAMA 3 และโมเดล local อื่น ๆ บนเครื่องของคุณ\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"ต่อผู้ใช้ ต่อเดือน เก็บค่าบริการเป็นรายปี\",\n            \"recommend\": \"แนะนำ M1 หรือใหม่กว่า\"\n          }\n        },\n        \"deal\": {\n          \"bannerLabel\": \"ดีลปีใหม่!\",\n          \"title\": \"ขยายทีมของคุณ!\",\n          \"info\": \"อัปเกรด และรับส่วนลด 10% สำหรับแผน Pro และ Team! เพิ่มประสิทธิภาพการทำงานในพื้นที่ทำงานของคุณด้วยฟีเจอร์ใหม่อันทรงพลัง รวมถึง @:appName AI\",\n          \"viewPlans\": \"ดูแผน\"\n        }\n      }\n    },\n    \"billingPage\": {\n      \"menuLabel\": \"การเรียกเก็บเงิน\",\n      \"title\": \"การเรียกเก็บเงิน\",\n      \"plan\": {\n        \"title\": \"แผน\",\n        \"freeLabel\": \"Free\",\n        \"proLabel\": \"Pro\",\n        \"planButtonLabel\": \"เปลี่ยนแผน\",\n        \"billingPeriod\": \"รอบบิล\",\n        \"periodButtonLabel\": \"แก้ไขช่วงเวลา\"\n      },\n      \"paymentDetails\": {\n        \"title\": \"รายละเอียดการชำระเงิน\",\n        \"methodLabel\": \"วิธีการชำระเงิน\",\n        \"methodButtonLabel\": \"แก้ไขวิธีการ\"\n      },\n      \"addons\": {\n        \"title\": \"ส่วนเสริม\",\n        \"addLabel\": \"เพิ่ม\",\n        \"removeLabel\": \"ลบ\",\n        \"renewLabel\": \"ต่ออายุ\",\n        \"aiMax\": {\n          \"label\": \"AI Max\",\n          \"description\": \"ปลดล็อค AI และโมเดลขั้นสูงแบบไม่จำกัด\",\n          \"activeDescription\": \"ใบแจ้งหนี้ถัดไปจะครบกำหนดในวันที่ {}\",\n          \"canceledDescription\": \"AI Max จะพร้อมใช้งานจนถึงวันที่ {}\"\n        },\n        \"aiOnDevice\": {\n          \"label\": \"AI บนอุปกรณ์สำหรับ Mac\",\n          \"description\": \"ปลดล็อค AI ไม่จำกัดบนอุปกรณ์ของคุณ\",\n          \"activeDescription\": \"ใบแจ้งหนี้ถัดไปจะครบกำหนดในวันที่ {}\",\n          \"canceledDescription\": \"AI บนอุปกรณ์สำหรับ Mac จะพร้อมใช้งานจนถึงวันที่ {}\"\n        },\n        \"removeDialog\": {\n          \"title\": \"ลบ {}\",\n          \"description\": \"คุณแน่ใจหรือไม่ว่าต้องการลบ {plan}? คุณจะสูญเสียการเข้าถึงฟีเจอร์ และสิทธิประโยชน์ของ {plan} ทันที\"\n        }\n      },\n      \"currentPeriodBadge\": \"ปัจจุบัน\",\n      \"changePeriod\": \"การเปลี่ยนแปลงช่วงเวลา\",\n      \"planPeriod\": \"{} รอบ\",\n      \"monthlyInterval\": \"รายเดือน\",\n      \"monthlyPriceInfo\": \"คิดค่าบริการต่อที่นั่งแบบรายเดือน\",\n      \"annualInterval\": \"รายปี\",\n      \"annualPriceInfo\": \"คิดค่าบริการต่อที่นั่งแบบรายปี\"\n    },\n    \"comparePlanDialog\": {\n      \"title\": \"เปรียบเทียบและเลือกแผน\",\n      \"planFeatures\": \"แผน\\nฟีเจอร์\",\n      \"current\": \"ปัจจุบัน\",\n      \"actions\": {\n        \"upgrade\": \"อัพเกรด\",\n        \"downgrade\": \"ลดระดับ\",\n        \"current\": \"ปัจจุบัน\"\n      },\n      \"freePlan\": {\n        \"title\": \"Free\",\n        \"description\": \"สำหรับบุคคลตั้งแต่ 2 คนขึ้นไป เพื่อจัดระเบียบทุกอย่าง\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"ฟรีตลอดไป\"\n      },\n      \"proPlan\": {\n        \"title\": \"Pro\",\n        \"description\": \"สำหรับทีมขนาดเล็กเพื่อจัดการโครงการและความรู้ของทีม\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"ต่อผู้ใช้ ต่อเดือน\\nเรียกเก็บค่าบริการรายปี\\n{} เรียกเก็บค่าบริการรายเดือน\"\n      },\n      \"planLabels\": {\n        \"itemOne\": \"พื้นที่ทำงาน\",\n        \"itemTwo\": \"สมาชิก\",\n        \"itemThree\": \"พื้นที่จัดเก็บ\",\n        \"itemFour\": \"การทำงานร่วมกันแบบเรียลไทม์\",\n        \"itemFive\": \"แอพมือถือ\",\n        \"itemSix\": \"การตอบกลับของ AI\",\n        \"itemFileUpload\": \"การอัพโหลดไฟล์\",\n        \"customNamespace\": \"เนมสเปซที่กำหนดเอง\",\n        \"tooltipSix\": \"ตลอดอายุการใช้งาน หมายถึง จำนวนการตอบกลับที่ไม่รีเซ็ต\",\n        \"intelligentSearch\": \"การค้นหาอัจฉริยะ\",\n        \"tooltipSeven\": \"อนุญาตให้คุณปรับแต่งส่วนหนึ่งของ URL สำหรับพื้นที่ทำงานของคุณ\",\n        \"customNamespaceTooltip\": \"URL ของไซต์ที่เผยแพร่แบบกำหนดเอง\"\n      },\n      \"freeLabels\": {\n        \"itemOne\": \"คิดค่าบริการตามพื้นที่ทำงาน\",\n        \"itemTwo\": \"สูงสุด 2\",\n        \"itemThree\": \"5 GB\",\n        \"itemFour\": \"ใช่\",\n        \"itemFive\": \"ใช่\",\n        \"itemSix\": \"10 ตลอดอายุการใช้งาน\",\n        \"itemFileUpload\": \"ไม่เกิน 7 MB\",\n        \"intelligentSearch\": \"การค้นหาอัจฉริยะ\"\n      },\n      \"proLabels\": {\n        \"itemOne\": \"คิดค่าบริการตามพื้นที่ทำงาน\",\n        \"itemTwo\": \"สูงถึง 10\",\n        \"itemThree\": \"ไม่จำกัด\",\n        \"itemFour\": \"ใช่\",\n        \"itemFive\": \"ใช่\",\n        \"itemSix\": \"ไม่จำกัด\",\n        \"itemFileUpload\": \"ไม่จำกัด\",\n        \"intelligentSearch\": \"การค้นหาอัจฉริยะ\"\n      },\n      \"paymentSuccess\": {\n        \"title\": \"ตอนนี้คุณอยู่ในแผน {} แล้ว!\",\n        \"description\": \"การชำระเงินของคุณได้รับการดำเนินการเรียบร้อย แล้วและแผนของคุณได้รับการอัปเกรดเป็น @:appName {} คุณสามารถดูรายละเอียดแผนของคุณได้ในหน้าแผน\"\n      },\n      \"downgradeDialog\": {\n        \"title\": \"คุณแน่ใจหรือไม่ว่าต้องการลดระดับแผนของคุณ?\",\n        \"description\": \"การลดระดับแผนของคุณจะทำให้คุณกลับไปใช้แผนฟรี สมาชิกอาจสูญเสียสิทธิ์การเข้าถึงพื้นที่ทำงานนี้ และคุณอาจต้องเพิ่มพื้นที่ว่างเพื่อให้ตรงตามขีดจำกัดพื้นที่เก็บข้อมูลของแผนฟรี\",\n        \"downgradeLabel\": \"การลดระดับแผน\"\n      }\n    },\n    \"cancelSurveyDialog\": {\n      \"title\": \"เสียใจที่เห็นคุณไป\",\n      \"description\": \"เราเสียใจที่เห็นคุณไป เรายินดีรับฟังคำติชมของคุณเพื่อช่วยเราปรับปรุง @:appName โปรดใช้เวลาสักครู่เพื่อตอบคำถามสองสามข้อ\",\n      \"commonOther\": \"อื่นๆ\",\n      \"otherHint\": \"เขียนคำตอบของคุณที่นี่\",\n      \"questionOne\": {\n        \"question\": \"อะไรเป็นเหตุผลที่ทำให้คุณยกเลิกการสมัครสมาชิก @:appName Pro?\",\n        \"answerOne\": \"ราคาสูงเกินไป\",\n        \"answerTwo\": \"ฟีเจอร์ไม่เป็นไปตามที่คาดไว้\",\n        \"answerThree\": \"พบทางเลือกที่ดีกว่า\",\n        \"answerFour\": \"ใช้ไม่มากพอที่จะคุ้มกับค่าใช้จ่าย\",\n        \"answerFive\": \"ปัญหาด้านการบริการหรือความยุ่งยากทางเทคนิค\"\n      },\n      \"questionTwo\": {\n        \"question\": \"คุณมีความเป็นไปได้มากแค่ไหนที่จะพิจารณากลับไปสมัครสมาชิก @:appName Pro ในอนาคต?\",\n        \"answerOne\": \"เป็นไปได้มาก\",\n        \"answerTwo\": \"ค่อนข้างจะเป็นไปได้\",\n        \"answerThree\": \"ไม่แน่ใจ\",\n        \"answerFour\": \"ไม่น่าจะเป็นไปได้\",\n        \"answerFive\": \"ไม่น่าจะเป็นไปได้มาก\"\n      },\n      \"questionThree\": {\n        \"question\": \"ฟีเจอร์ Pro อะไรที่คุณคิดว่าคุ้มค่าที่สุดในระหว่างที่ใช้บริการ?\",\n        \"answerOne\": \"การทำงานร่วมกันระหว่างผู้ใช้หลายราย\",\n        \"answerTwo\": \"ประวัติเวอร์ชันที่ยาวนานกว่า\",\n        \"answerThree\": \"การตอบกลับของ AI ไม่จำกัด\",\n        \"answerFour\": \"การเข้าถึงโมเดล AI ในเครื่อง\"\n      },\n      \"questionFour\": {\n        \"question\": \"คุณคิดว่าประสบการณ์โดยรวมกับ @:appName เป็นอย่างไร?\",\n        \"answerOne\": \"ยอดเยี่ยม\",\n        \"answerTwo\": \"ดี\",\n        \"answerThree\": \"ปานกลาง\",\n        \"answerFour\": \"ต่ำกว่าค่าเฉลี่ย\",\n        \"answerFive\": \"ไม่พึงพอใจ\"\n      }\n    },\n    \"common\": {\n      \"uploadingFile\": \"กำลังอัพโหลดไฟล์ กรุณาอย่าออกจากแอป\",\n      \"uploadNotionSuccess\": \"ไฟล์ zip ของ Notion ของคุณได้รับการอัปโหลดเรียบร้อยแล้ว เมื่อการนำเข้าเสร็จสมบูรณ์ คุณจะได้รับอีเมลยืนยัน\",\n      \"reset\": \"รีเซ็ต\"\n    },\n    \"menu\": {\n      \"appearance\": \"รูปลักษณ์\",\n      \"language\": \"ภาษา\",\n      \"user\": \"ผู้ใช้งาน\",\n      \"files\": \"ไฟล์\",\n      \"notifications\": \"การแจ้งเตือน\",\n      \"open\": \"เปิดการตั้งค่า\",\n      \"logout\": \"ออกจากระบบ\",\n      \"logoutPrompt\": \"คุณแน่ใจหรือว่าจะออกจากระบบ?\",\n      \"selfEncryptionLogoutPrompt\": \"คุณแน่ใจหรือไม่ว่าต้องการที่จะออกจากระบบ? โปรดตรวจสอบให้แน่ใจว่าคุณได้คัดลอกการเข้ารหัสลับไว้แล้ว\",\n      \"syncSetting\": \"การตั้งค่าการซิงค์\",\n      \"cloudSettings\": \"การตั้งค่าคลาวด์\",\n      \"enableSync\": \"เปิดใช้งานการซิงค์\",\n      \"enableEncrypt\": \"เข้ารหัสข้อมูล\",\n      \"cloudURL\": \"URL หลัก\",\n      \"invalidCloudURLScheme\": \"รูปแบบไม่ถูกต้อง\",\n      \"cloudServerType\": \"เซิร์ฟเวอร์คลาวด์\",\n      \"cloudServerTypeTip\": \"โปรดทราบว่าอาจมีการออกจากระบบบัญชีปัจจุบันของคุณหลังจากเปลี่ยนเซิร์ฟเวอร์คลาวด์\",\n      \"cloudLocal\": \"ในเครื่อง\",\n      \"cloudAppFlowy\": \"คลาวด์ของ AppFlowy\",\n      \"cloudAppFlowySelfHost\": \"@:appName คลาวด์ที่โฮสต์เอง\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"URL ของคลาวด์ไม่สามารถเว้นว่างได้\",\n      \"clickToCopy\": \"คลิกเพื่อคัดลอก\",\n      \"selfHostStart\": \"หากคุณไม่มีเซิร์ฟเวอร์ โปรดดูที่\",\n      \"selfHostContent\": \"เอกสาร\",\n      \"selfHostEnd\": \"สำหรับคำแนะนำเกี่ยวกับวิธีการโฮสต์เซิร์ฟเวอร์ของคุณเอง\",\n      \"pleaseInputValidURL\": \"กรุณาใส่ URL ที่ถูกต้อง\",\n      \"changeUrl\": \"เปลี่ยน URL ที่โฮสต์เองเป็น {}\",\n      \"cloudURLHint\": \"ป้อน URL หลักของเซิร์ฟเวอร์ของคุณ\",\n      \"cloudWSURL\": \"Websocket URL\",\n      \"cloudWSURLHint\": \"ป้อนที่อยู่ websocket ของเซิร์ฟเวอร์ของคุณ\",\n      \"restartApp\": \"รีสตาร์ท\",\n      \"restartAppTip\": \"รีสตาร์ทแอปพลิเคชันเพื่อให้มีการเปลี่ยนแปลง โปรดทราบไว้ว่าการดำเนินการนี้อาจจะออกจากระบบบัญชีปัจจุบันของคุณ\",\n      \"changeServerTip\": \"หลังจากเปลี่ยนเซิร์ฟเวอร์แล้ว คุณต้องคลิกปุ่มรีสตาร์ทเพื่อให้การเปลี่ยนแปลงมีผล\",\n      \"enableEncryptPrompt\": \"เปิดใช้งานการเข้ารหัสเพื่อรักษาความปลอดภัยข้อมูลของคุณแบบเป็นความลับ จะเก็บไว้อย่างปลอดภัย เมื่อเปิดใช้งานแล้วจะไม่สามารถปิดได้ หากสูญหาย ข้อมูลของคุณจะไม่สามารถเรียกคืนได้ คลิกเพื่อคัดลอก\",\n      \"inputEncryptPrompt\": \"กรุณาระบุการเข้ารหัสลับของคุณสำหรับ\",\n      \"clickToCopySecret\": \"คลิกเพื่อคัดลอกรหัสลับ\",\n      \"configServerSetting\": \"กำหนดการตั้งค่าเซิร์ฟเวอร์ของคุณ\",\n      \"configServerGuide\": \"หลังจากเลือก 'เริ่มต้นอย่างรวดเร็ว' แล้ว ให้ไปที่ 'การตั้งค่า' จากนั้นไปที่ \\\"การตั้งค่าคลาวด์\\\" เพื่อกำหนดค่าเซิร์ฟเวอร์ที่โฮสต์เองของคุณ\",\n      \"inputTextFieldHint\": \"รหัสลับของคุณ\",\n      \"historicalUserList\": \"ประวัติการเข้าสู่ระบบของผู้ใช้\",\n      \"historicalUserListTooltip\": \"รายการนี้จะแสดงบัญชีที่ไม่ระบุตัวตนของคุณ คุณสามารถคลิกที่บัญชีเพื่อดูรายละเอียดได้ บัญชีที่ไม่เปิดเผยตัวตนถูกสร้างขึ้นโดยการคลิกปุ่ม 'เริ่มต้น'\",\n      \"openHistoricalUser\": \"คลิกเพื่อเปิดบัญชีที่ไม่ระบุตัวตน\",\n      \"customPathPrompt\": \"การจัดเก็บโฟลเดอร์ข้อมูลของ AppFlowy ไว้ในโฟลเดอร์ที่ซิงค์บนคลาวด์ เช่น Google Drive อาจทำให้เกิดความเสี่ยงได้ หากมีการเข้าถึงหรือแก้ไขฐานข้อมูลภายในโฟลเดอร์นี้จากหลายตำแหน่งพร้อมกัน อาจส่งผลให้เกิดความขัดแย้งในการซิงโครไนซ์และข้อมูลอาจเสียหายได้\",\n      \"importAppFlowyData\": \"นำเข้าข้อมูลจากโฟลเดอร์ @:appName ภายนอก\",\n      \"importingAppFlowyDataTip\": \"กำลังดำเนินการนำเข้าข้อมูล โปรดอย่าปิดแอป\",\n      \"importAppFlowyDataDescription\": \"คัดลอกข้อมูลจากโฟลเดอร์ข้อมูลภายนอกของ @:appName และนำเข้าไปยังโฟลเดอร์ข้อมูล AppFlowy ปัจจุบัน\",\n      \"importSuccess\": \"นำเข้าโฟลเดอร์ข้อมูล @:appName สำเร็จแล้ว\",\n      \"importFailed\": \"การนำเข้าโฟลเดอร์ข้อมูล @:appName ล้มเหลว\",\n      \"importGuide\": \"สำหรับรายละเอียดเพิ่มเติม โปรดตรวจสอบเอกสารอ้างอิง\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"เปิดใช้งานการแจ้งเตือน\",\n        \"hint\": \"ปิดเพื่อหยุดไม่ให้แสดงการแจ้งเตือนในเครื่องของคุณ\"\n      },\n      \"showNotificationsIcon\": {\n        \"label\": \"แสดงไอคอนการแจ้งเตือน\",\n        \"hint\": \"ปิดเพื่อซ่อนไอคอนการแจ้งเตือนในแถบด้านข้าง\"\n      },\n      \"archiveNotifications\": {\n        \"allSuccess\": \"จัดเก็บการแจ้งเตือนทั้งหมดเรียบร้อยแล้ว\",\n        \"success\": \"จัดเก็บการแจ้งเตือนเรียบร้อยแล้ว\"\n      },\n      \"markAsReadNotifications\": {\n        \"allSuccess\": \"ทำเครื่องหมายทั้งหมดว่าอ่านแล้วสำเร็จแล้ว\",\n        \"success\": \"ทำเครื่องหมายว่าอ่านแล้วสำเร็จ\"\n      },\n      \"action\": {\n        \"markAsRead\": \"ทำเครื่องหมายว่าอ่านแล้ว\",\n        \"multipleChoice\": \"เลือกเพิ่มเติม\",\n        \"archive\": \"คลังเก็บเอกสารสำคัญ\"\n      },\n      \"settings\": {\n        \"settings\": \"การตั้งค่า\",\n        \"markAllAsRead\": \"ทำเครื่องหมายทั้งหมดว่าอ่านแล้ว\",\n        \"archiveAll\": \"เก็บถาวรทั้งหมด\"\n      },\n      \"emptyInbox\": {\n        \"title\": \"กล่องข้อความว่างเปล่า\",\n        \"description\": \"ตั้งค่าการเตือนเพื่อรับการแจ้งเตือนที่นี่\"\n      },\n      \"emptyUnread\": {\n        \"title\": \"ไม่มีการแจ้งเตือนที่ยังไม่ได้อ่าน\",\n        \"description\": \"คุณตามทันทั้งหมดแล้ว!\"\n      },\n      \"emptyArchived\": {\n        \"title\": \"ไม่มีการเก็บถาวร\",\n        \"description\": \"การแจ้งเตือนที่เก็บถาวรจะปรากฏที่นี่\"\n      },\n      \"tabs\": {\n        \"inbox\": \"กล่องข้อความ\",\n        \"unread\": \"ยังไม่ได้อ่าน\",\n        \"archived\": \"เก็บถาวร\"\n      },\n      \"refreshSuccess\": \"รีเฟรชการแจ้งเตือนสำเร็จ\",\n      \"titles\": {\n        \"notifications\": \"การแจ้งเตือน\",\n        \"reminder\": \"การเตือน\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"รีเซ็ตการตั้งค่านี้\",\n      \"fontFamily\": {\n        \"label\": \"แบบตัวอักษร\",\n        \"search\": \"ค้นหา\",\n        \"defaultFont\": \"ระบบ\"\n      },\n      \"themeMode\": {\n        \"label\": \"โหมดธีม\",\n        \"light\": \"โหมดสว่าง\",\n        \"dark\": \"โหมดมืด\",\n        \"system\": \"ใช้ตามระบบ\"\n      },\n      \"fontScaleFactor\": \"ตัวปรับขนาดฟอนต์\",\n      \"documentSettings\": {\n        \"cursorColor\": \"สีเคอร์เซอร์เอกสาร\",\n        \"selectionColor\": \"สีการเลือกเอกสาร\",\n        \"width\": \"ความกว้างของเอกสาร\",\n        \"changeWidth\": \"เปลี่ยน\",\n        \"pickColor\": \"เลือกสี\",\n        \"colorShade\": \"เฉดสี\",\n        \"opacity\": \"ความทึบแสง\",\n        \"hexEmptyError\": \"รหัสสีเลขฐานสิบหกไม่สามารถเว้นว่างได้\",\n        \"hexLengthError\": \"ค่าเลขฐานสิบหกต้องมีความยาว 6 หลัก\",\n        \"hexInvalidError\": \"ค่าเลขฐานสิบหกไม่ถูกต้อง\",\n        \"opacityEmptyError\": \"ความทึบแสงไม่สามารถเว้นว่างได้\",\n        \"opacityRangeError\": \"ความทึบแสงต้องอยู่ระหว่าง 1 และ 100\",\n        \"app\": \"App\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"นำไปใช้\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"ทิศทางของเค้าโครง\",\n        \"hint\": \"ควบคุมการไหลของเนื้อหาบนหน้าจอของคุณ จากซ้ายไปขวาหรือจากขวาไปซ้าย\",\n        \"ltr\": \"ซ้ายไปขวา\",\n        \"rtl\": \"ขวาไปซ้าย\"\n      },\n      \"textDirection\": {\n        \"label\": \"ทิศทางข้อความเริ่มต้น\",\n        \"hint\": \"ระบุว่าข้อความควรเริ่มจากซ้ายหรือขวาเป็นค่าเริ่มต้น\",\n        \"ltr\": \"ซ้ายไปขวา\",\n        \"rtl\": \"ขวาไปซ้าย\",\n        \"auto\": \"อัตโนมัติ\",\n        \"fallback\": \"เหมือนกับทิศทางของเค้าโครง\"\n      },\n      \"themeUpload\": {\n        \"button\": \"อัปโหลด\",\n        \"uploadTheme\": \"อัปโหลดธีม\",\n        \"description\": \"อัปโหลดธีม AppFlowy ของคุณเองโดยใช้ปุ่มด้านล่าง\",\n        \"loading\": \"โปรดรอสักครู่ในขณะที่เราตรวจสอบและอัปโหลดธีมของคุณ...\",\n        \"uploadSuccess\": \"อัปโหลดธีมของคุณสำเร็จแล้ว\",\n        \"deletionFailure\": \"ไม่สามารถลบธีมได้ ลองลบมันด้วยตนเอง\",\n        \"filePickerDialogTitle\": \"เลือกไฟล์ .flowy_plugin\",\n        \"urlUploadFailure\": \"ไม่สามารถเปิด URL: {} ได้\",\n        \"failure\": \"ธีมที่อัปโหลดมีรูปแบบที่ไม่ถูกต้อง\"\n      },\n      \"theme\": \"ธีม\",\n      \"builtInsLabel\": \"ธีมในตัวแอป\",\n      \"pluginsLabel\": \"ปลั๊กอิน\",\n      \"dateFormat\": {\n        \"label\": \"รูปแบบวันที่\",\n        \"local\": \"ใช้ตามเครื่องของคุณ\",\n        \"us\": \"US\",\n        \"iso\": \"สากล\",\n        \"friendly\": \"แบบง่าย\",\n        \"dmy\": \"วัน/เดือน/ปี\"\n      },\n      \"timeFormat\": {\n        \"label\": \"รูปแบบเวลา\",\n        \"twelveHour\": \"สิบสองชั่วโมง\",\n        \"twentyFourHour\": \"ยี่สิบสี่ชั่วโมง\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"แสดงกล่องโต้ตอบการตั้งชื่อเมื่อสร้างหน้า\",\n      \"enableRTLToolbarItems\": \"เปิดใช้งานแถบเครื่องมือ RTL (จากขวาไปซ้าย)\",\n      \"members\": {\n        \"title\": \"การตั้งค่าสมาชิก\",\n        \"inviteMembers\": \"ส่งคำเชิญสมาชิก\",\n        \"inviteHint\": \"ส่งคำเชิญทางอีเมล\",\n        \"sendInvite\": \"ส่งคำเชิญ\",\n        \"copyInviteLink\": \"คัดลอกลิงค์คำเชิญ\",\n        \"label\": \"สมาชิก\",\n        \"user\": \"ผู้ใช้\",\n        \"role\": \"บทบาท\",\n        \"removeFromWorkspace\": \"ลบออกจากพื้นที่ทำงาน\",\n        \"removeFromWorkspaceSuccess\": \"ลบออกจากพื้นที่ทำงานสำเร็จ\",\n        \"removeFromWorkspaceFailed\": \"ลบออกจากพื้นที่ทำงานไม่สำเร็จ\",\n        \"owner\": \"เจ้าของ\",\n        \"guest\": \"ผู้เยี่ยมชม\",\n        \"member\": \"สมาชิก\",\n        \"memberHintText\": \"สมาชิกสามารถอ่านและแก้ไขหน้าได้\",\n        \"guestHintText\": \"ผู้เยี่ยมชมสามารถอ่าน ตอบสนอง แสดงความคิดเห็น และแก้ไขหน้าบางหน้าได้ด้วยการอนุญาต\",\n        \"emailInvalidError\": \"อีเมล์ไม่ถูกต้อง กรุณาตรวจสอบและลองอีกครั้ง\",\n        \"emailSent\": \"ส่งอีเมลแล้ว กรุณาตรวจสอบกล่องจดหมาย\",\n        \"members\": \"สมาชิก\",\n        \"membersCount\": {\n          \"zero\": \"{} สมาชิก\",\n          \"one\": \"{} สมาชิก\",\n          \"other\": \"{} สมาชิก\"\n        },\n        \"inviteFailedDialogTitle\": \"ไม่สามารถส่งคำเชิญได้\",\n        \"inviteFailedMemberLimit\": \"จำนวนสมาชิกถึงขีดจำกัดแล้ว กรุณาอัปเกรดเพื่อเชิญสมาชิกเพิ่มเติม\",\n        \"inviteFailedMemberLimitMobile\": \"พื้นที่ทำงานของคุณถึงขีดจำกัดจำนวนสมาชิกแล้ว\",\n        \"memberLimitExceeded\": \"จำนวนสมาชิกถึงขีดจำกัดแล้ว เพื่อเชิญสมาชิกเพิ่มเติม กรุณา...\",\n        \"memberLimitExceededUpgrade\": \"อัพเกรด\",\n        \"memberLimitExceededPro\": \"จำนวนสมาชิกถึงขีดจำกัดแล้ว หากต้องการสมาชิกเพิ่มเติม กรุณาติดต่อ \",\n        \"memberLimitExceededProContact\": \"support@appflowy.io\",\n        \"failedToAddMember\": \"เพิ่มสมาชิกไม่สำเร็จ\",\n        \"addMemberSuccess\": \"เพิ่มสมาชิกสำเร็จ\",\n        \"removeMember\": \"ลบสมาชิก\",\n        \"areYouSureToRemoveMember\": \"คุณแน่ใจหรือไม่ว่าต้องการลบสมาชิกคนนี้ออก?\",\n        \"inviteMemberSuccess\": \"ส่งคำเชิญเรียบร้อยแล้ว\",\n        \"failedToInviteMember\": \"เชิญสมาชิกไม่สำเร็จ\",\n        \"workspaceMembersError\": \"อุ๊ปส์, มีบางอย่างผิดปกติ\",\n        \"workspaceMembersErrorDescription\": \"เราไม่สามารถโหลดรายชื่อสมาชิกได้ในขณะนี้ กรุณาลองอีกครั้งในภายหลัง\"\n      }\n    },\n    \"files\": {\n      \"copy\": \"คัดลอก\",\n      \"defaultLocation\": \"อ่านไฟล์และสถานที่เก็บข้อมูล\",\n      \"exportData\": \"ส่งออกข้อมูลของคุณ\",\n      \"doubleTapToCopy\": \"แตะสองครั้งเพื่อคัดลอกเส้นทาง\",\n      \"restoreLocation\": \"กู้คืนเป็นเส้นทางเริ่มต้นของ AppFlowy\",\n      \"customizeLocation\": \"เปิดโฟลเดอร์อื่น\",\n      \"restartApp\": \"โปรดรีสตาร์ทแอปเพื่อให้การเปลี่ยนแปลงมีผล\",\n      \"exportDatabase\": \"ส่งออกฐานข้อมูล\",\n      \"selectFiles\": \"เลือกไฟล์ที่ต้องการส่งออก\",\n      \"selectAll\": \"เลือกทั้งหมด\",\n      \"deselectAll\": \"เลิกเลือกทั้งหมด\",\n      \"createNewFolder\": \"สร้างโฟลเดอร์ใหม่\",\n      \"createNewFolderDesc\": \"บอกเราว่าคุณต้องการเก็บข้อมูลไว้ที่ไหน\",\n      \"defineWhereYourDataIsStored\": \"ระบุตำแหน่งที่เก็บข้อมูลของคุณ\",\n      \"open\": \"เปิด\",\n      \"openFolder\": \"เปิดโฟลเดอร์ที่มีอยู่\",\n      \"openFolderDesc\": \"อ่านและเขียนลงในโฟลเดอร์ AppFlowy ที่มีอยู่ของคุณ\",\n      \"folderHintText\": \"ชื่อโฟลเดอร์\",\n      \"location\": \"สร้างโฟลเดอร์ใหม่\",\n      \"locationDesc\": \"เลือกชื่อสำหรับโฟลเดอร์ข้อมูล AppFlowy ของคุณ\",\n      \"browser\": \"เรียกดู\",\n      \"create\": \"สร้าง\",\n      \"set\": \"ตั้งค่า\",\n      \"folderPath\": \"เส้นทางการเก็บโฟลเดอร์ของคุณ\",\n      \"locationCannotBeEmpty\": \"เส้นทางไม่สามารถว่างได้\",\n      \"pathCopiedSnackbar\": \"คัดลอกเส้นทางการเก็บไฟล์ไปยังคลิปบอร์ด!\",\n      \"changeLocationTooltips\": \"เปลี่ยนไดเร็กทอรีข้อมูล\",\n      \"change\": \"เปลี่ยน\",\n      \"openLocationTooltips\": \"เปิดไดเร็กทอรีข้อมูลอื่น\",\n      \"openCurrentDataFolder\": \"เปิดไดเร็กทอรีข้อมูลปัจจุบัน\",\n      \"recoverLocationTooltips\": \"รีเซ็ตเป็นไดเร็กทอรีข้อมูลเริ่มต้นของ AppFlowy\",\n      \"exportFileSuccess\": \"ส่งออกไฟล์สำเร็จ!\",\n      \"exportFileFail\": \"ส่งออกไฟล์ล้มเหลว!\",\n      \"export\": \"ส่งออก\",\n      \"clearCache\": \"ล้างแคช\",\n      \"clearCacheDesc\": \"หากคุณพบปัญหาเกี่ยวกับรูปภาพไม่โหลด หรือฟอนต์ไม่แสดงอย่างถูกต้อง ให้ลองล้างแคช การดำเนินการนี้จะไม่ลบข้อมูลผู้ใช้ของคุณ\",\n      \"areYouSureToClearCache\": \"คุณแน่ใจหรือไม่ที่จะล้างแคช?\",\n      \"clearCacheSuccess\": \"ล้างแคชสำเร็จ!\"\n    },\n    \"user\": {\n      \"name\": \"ชื่อ\",\n      \"email\": \"อีเมล\",\n      \"tooltipSelectIcon\": \"เลือกไอคอน\",\n      \"selectAnIcon\": \"เลือกไอคอน\",\n      \"pleaseInputYourOpenAIKey\": \"โปรดระบุคีย์ AI ของคุณ\",\n      \"clickToLogout\": \"คลิกเพื่อออกจากระบบผู้ใช้ปัจจุบัน\",\n      \"pleaseInputYourStabilityAIKey\": \"โปรดระบุคีย์ Stability AI ของคุณ\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"ข้อมูลส่วนตัว\",\n      \"username\": \"ชื่อผู้ใช้\",\n      \"usernameEmptyError\": \"โปรดกรอกชื่อผู้ใช้\",\n      \"about\": \"เกี่ยวกับ\",\n      \"pushNotifications\": \"การแจ้งเตือนแบบพุช\",\n      \"support\": \"การสนับสนุน\",\n      \"joinDiscord\": \"เข้าร่วมกับเราบน Discord\",\n      \"privacyPolicy\": \"นโยบายความเป็นส่วนตัว\",\n      \"userAgreement\": \"ข้อตกลงผู้ใช้\",\n      \"termsAndConditions\": \"ข้อกำหนดและเงื่อนไข\",\n      \"userprofileError\": \"ไม่สามารถโหลดโปรไฟล์ผู้ใช้ได้\",\n      \"userprofileErrorDescription\": \"โปรดลองออกจากระบบแล้วเข้าสู่ระบบอีกครั้งเพื่อตรวจสอบว่าปัญหายังคงอยู่หรือไม่\",\n      \"selectLayout\": \"เลือกเค้าโครง\",\n      \"selectStartingDay\": \"เลือกวันเริ่มต้น\",\n      \"version\": \"เวอร์ชัน\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"ทางลัด\",\n      \"command\": \"คำสั่ง\",\n      \"keyBinding\": \"การผูกแป้นพิมพ์\",\n      \"addNewCommand\": \"เพิ่มคำสั่งใหม่\",\n      \"updateShortcutStep\": \"กดปุ่มแป้นพิมพ์ที่ต้องการแล้วกด ENTER\",\n      \"shortcutIsAlreadyUsed\": \"ทางลัดนี้ถูกใช้แล้วสำหรับ: {conflict}\",\n      \"resetToDefault\": \"รีเซ็ตการกำหนดแป้นพิมพ์เป็นค่าเริ่มต้น\",\n      \"couldNotLoadErrorMsg\": \"ไม่สามารถโหลดทางลัดได้ ลองอีกครั้ง\",\n      \"couldNotSaveErrorMsg\": \"ไม่สามารถบันทึกทางลัดได้ ลองอีกครั้ง\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"คุณแน่ใจหรือไม่ว่าต้องการลบมุมมองนี้\",\n    \"createView\": \"ใหม่\",\n    \"title\": {\n      \"placeholder\": \"ไม่มีชื่อ\"\n    },\n    \"settings\": {\n      \"filter\": \"ตัวกรอง\",\n      \"sort\": \"เรียงลำดับ\",\n      \"sortBy\": \"เรียงลำดับตาม\",\n      \"properties\": \"คุณสมบัติ\",\n      \"reorderPropertiesTooltip\": \"ลากเพื่อเปลี่ยนลำดับคุณสมบัติ\",\n      \"group\": \"กลุ่ม\",\n      \"addFilter\": \"เพิ่มตัวกรอง\",\n      \"deleteFilter\": \"ลบตัวกรอง\",\n      \"filterBy\": \"กรองตาม...\",\n      \"typeAValue\": \"พิมพ์ค่า...\",\n      \"layout\": \"เค้าโครง\",\n      \"databaseLayout\": \"เค้าโครงฐานข้อมูล\",\n      \"viewList\": {\n        \"zero\": \"0 มุมมอง\",\n        \"one\": \"{count} มุมมอง\",\n        \"other\": \"{count} มุมมอง\"\n      },\n      \"editView\": \"แก้ไขมุมมอง\",\n      \"boardSettings\": \"การตั้งค่าบอร์ด\",\n      \"calendarSettings\": \"การตั้งค่าปฏิทิน\",\n      \"createView\": \"มุมมองใหม่\",\n      \"duplicateView\": \"ทำสำเนามุมมอง\",\n      \"deleteView\": \"ลบมุมมอง\",\n      \"numberOfVisibleFields\": \"{} ที่แสดงอยู่\"\n    },\n    \"filter\": {\n      \"empty\": \"ไม่มีตัวกรองที่ใช้งานอยู่\",\n      \"addFilter\": \"เพิ่มตัวกรอง\",\n      \"cannotFindCreatableField\": \"ไม่พบฟิลด์ที่เหมาะสมในการกรอง\",\n      \"conditon\": \"เงื่อนไข\",\n      \"where\": \"โดยที่\"\n    },\n    \"textFilter\": {\n      \"contains\": \"ประกอบด้วย\",\n      \"doesNotContain\": \"ไม่ประกอบด้วย\",\n      \"endsWith\": \"ลงท้ายด้วย\",\n      \"startWith\": \"ขึ้นต้นด้วย\",\n      \"is\": \"เป็น\",\n      \"isNot\": \"ไม่เป็น\",\n      \"isEmpty\": \"ว่างเปล่า\",\n      \"isNotEmpty\": \"ไม่ว่างเปล่า\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"ไม่\",\n        \"startWith\": \"ขึ้นต้นด้วย\",\n        \"endWith\": \"ลงท้ายด้วย\",\n        \"isEmpty\": \"ว่างเปล่า\",\n        \"isNotEmpty\": \"ไม่ว่างเปล่า\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"เลือกแล้ว\",\n      \"isUnchecked\": \"ไม่เลือก\",\n      \"choicechipPrefix\": {\n        \"is\": \"เป็น\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"เสร็จสมบูรณ์\",\n      \"isIncomplted\": \"ไม่เสร็จสมบูรณ์\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"เป็น\",\n      \"isNot\": \"ไม่เป็น\",\n      \"contains\": \"ประกอบด้วย\",\n      \"doesNotContain\": \"ไม่ประกอบด้วย\",\n      \"isEmpty\": \"ว่างเปล่า\",\n      \"isNotEmpty\": \"ไม่ว่างเปล่า\"\n    },\n    \"dateFilter\": {\n      \"is\": \"เป็น\",\n      \"before\": \"อยู่ก่อน\",\n      \"after\": \"อยู่หลัง\",\n      \"onOrBefore\": \"อยู่ในหรือก่อนหน้า\",\n      \"onOrAfter\": \"อยู่ในหรือหลังจาก\",\n      \"between\": \"อยู่ระหว่าง\",\n      \"empty\": \"มันว่างเปล่า\",\n      \"notEmpty\": \"มันไม่ว่างเปล่า\",\n      \"startDate\": \"วันที่เริ่มต้น\",\n      \"endDate\": \"วันที่สิ้นสุด\",\n      \"choicechipPrefix\": {\n        \"before\": \"ก่อน\",\n        \"after\": \"หลังจาก\",\n        \"between\": \"ระหว่าง\",\n        \"onOrBefore\": \"ภายในหรือก่อน\",\n        \"onOrAfter\": \"ภายในหรือหลัง\",\n        \"isEmpty\": \"ว่างเปล่า\",\n        \"isNotEmpty\": \"ไม่ว่างเปล่า\"\n      }\n    },\n    \"numberFilter\": {\n      \"equal\": \"เท่ากับ\",\n      \"notEqual\": \"ไม่เท่ากับ\",\n      \"lessThan\": \"น้อยกว่า\",\n      \"greaterThan\": \"มากกว่า\",\n      \"lessThanOrEqualTo\": \"น้อยกว่าหรือเท่ากับ\",\n      \"greaterThanOrEqualTo\": \"มากกว่าหรือเท่ากับ\",\n      \"isEmpty\": \"ว่างเปล่า\",\n      \"isNotEmpty\": \"ไม่ว่างเปล่า\"\n    },\n    \"field\": {\n      \"label\": \"คุณสมบัติ\",\n      \"hide\": \"ซ่อน\",\n      \"show\": \"แสดง\",\n      \"insertLeft\": \"แทรกทางซ้าย\",\n      \"insertRight\": \"แทรกทางขวา\",\n      \"duplicate\": \"ทำสำเนา\",\n      \"delete\": \"ลบ\",\n      \"wrapCellContent\": \"ตัดข้อความ\",\n      \"clear\": \"ล้างเซลล์\",\n      \"switchPrimaryFieldTooltip\": \"ไม่สามารถเปลี่ยนประเภทฟิลด์ของฟิลด์หลักได้\",\n      \"textFieldName\": \"ข้อความ\",\n      \"checkboxFieldName\": \"กล่องกาเครื่องหมาย\",\n      \"dateFieldName\": \"วันที่\",\n      \"updatedAtFieldName\": \"เวลาแก้ไขล่าสุด\",\n      \"createdAtFieldName\": \"เวลาสร้าง\",\n      \"numberFieldName\": \"ตัวเลข\",\n      \"singleSelectFieldName\": \"การเลือก\",\n      \"multiSelectFieldName\": \"การเลือกหลายรายการ\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"รายการตรวจสอบ\",\n      \"relationFieldName\": \"ความสัมพันธ์\",\n      \"summaryFieldName\": \"AI สรุป\",\n      \"timeFieldName\": \"เวลา\",\n      \"mediaFieldName\": \"ไฟล์และสื่อ\",\n      \"translateFieldName\": \"AI แปล\",\n      \"translateTo\": \"แปลเป็น\",\n      \"numberFormat\": \"รูปแบบตัวเลข\",\n      \"dateFormat\": \"รูปแบบวันที่\",\n      \"includeTime\": \"รวมเวลา\",\n      \"isRange\": \"วันที่สิ้นสุด\",\n      \"dateFormatFriendly\": \"เดือน วัน, ปี\",\n      \"dateFormatISO\": \"ปี-เดือน-วัน\",\n      \"dateFormatLocal\": \"เดือน/วัน/ปี\",\n      \"dateFormatUS\": \"ปี/เดือน/วัน\",\n      \"dateFormatDayMonthYear\": \"วัน/เดือน/ปี\",\n      \"timeFormat\": \"รูปแบบเวลา\",\n      \"invalidTimeFormat\": \"รูปแบบไม่ถูกต้อง\",\n      \"timeFormatTwelveHour\": \"12 ชั่วโมง\",\n      \"timeFormatTwentyFourHour\": \"24 ชั่วโมง\",\n      \"clearDate\": \"ล้างวันที่\",\n      \"dateTime\": \"วันที่และเวลา\",\n      \"startDateTime\": \"วันที่และเวลาเริ่มต้น\",\n      \"endDateTime\": \"วันที่และเวลาสิ้นสุด\",\n      \"failedToLoadDate\": \"ไม่สามารถโหลดค่าวันที่ได้\",\n      \"selectTime\": \"เลือกเวลา\",\n      \"selectDate\": \"เลือกวันที่\",\n      \"visibility\": \"การมองเห็น\",\n      \"propertyType\": \"ประเภทคุณสมบัติ\",\n      \"addSelectOption\": \"เพิ่มตัวเลือก\",\n      \"typeANewOption\": \"พิมพ์ตัวเลือกใหม่\",\n      \"optionTitle\": \"ตัวเลือก\",\n      \"addOption\": \"เพิ่มตัวเลือก\",\n      \"editProperty\": \"แก้ไขคุณสมบัติ\",\n      \"newProperty\": \"คุณสมบัติใหม่\",\n      \"openRowDocument\": \"เปิดเป็นหน้า\",\n      \"deleteFieldPromptMessage\": \"แน่ใจหรือไม่? คุณสมบัติเหล่านี้จะถูกลบ\",\n      \"clearFieldPromptMessage\": \"คุณแน่ใจหรือไม่ฦ เซลล์ทั้งหมดในคอลัมน์นี้จะถูกล้างข้อมูล\",\n      \"newColumn\": \"คอลัมน์ใหม่\",\n      \"format\": \"รูปแบบ\",\n      \"reminderOnDateTooltip\": \"เซลล์นี้มีการตั้งค่าการเตือนในวันที่กำหนด\",\n      \"optionAlreadyExist\": \"ตัวเลือกมีอยู่แล้ว\"\n    },\n    \"rowPage\": {\n      \"newField\": \"เพิ่มฟิลด์ใหม่\",\n      \"fieldDragElementTooltip\": \"คลิกเพื่อเปิดเมนู\",\n      \"showHiddenFields\": {\n        \"one\": \"แสดงฟิลด์ที่ซ่อนอยู่ {count} ฟิลด์\",\n        \"many\": \"แสดงฟิลด์ที่ซ่อนอยู่ {count} ฟิลด์\",\n        \"other\": \"แสดงฟิลด์ที่ซ่อนอยู่ {count} ฟิลด์\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"ซ่อนฟิลด์ที่ซ่อนอยู่ {count} ฟิลด์\",\n        \"many\": \"ซ่อนฟิลด์ที่ซ่อนอยู่ {count} ฟิลด์\",\n        \"other\": \"ซ่อนฟิลด์ที่ซ่อนอยู่ {count} ฟิลด์\"\n      },\n      \"openAsFullPage\": \"เปิดแบบเต็มหน้า\",\n      \"moreRowActions\": \"การดำเนินการแถวเพิ่มเติม\"\n    },\n    \"sort\": {\n      \"ascending\": \"เรียงลำดับจากน้อยไปมาก\",\n      \"descending\": \"เรียงลำดับจากมากไปน้อย\",\n      \"by\": \"โดย\",\n      \"empty\": \"ไม่มีการเรียงลำดับที่ใช้งานอยู่\",\n      \"cannotFindCreatableField\": \"ไม่พบฟิลด์ที่เหมาะสมในการเรียงลำดับ\",\n      \"deleteAllSorts\": \"ลบการเรียงลำดับทั้งหมด\",\n      \"addSort\": \"เพิ่มการเรียงลำดับ\",\n      \"sortsActive\": \"ไม่สามารถ {intention} ขณะทำการจัดเรียง\",\n      \"removeSorting\": \"คุณต้องการลบการจัดเรียงทั้งหมดในมุมมองนี้ และดำเนินการต่อหรือไม่?\",\n      \"fieldInUse\": \"คุณกำลังเรียงลำดับตามฟิลด์นี้อยู่แล้ว\"\n    },\n    \"row\": {\n      \"label\": \"แถว\",\n      \"duplicate\": \"ทำสำเนา\",\n      \"delete\": \"ลบ\",\n      \"titlePlaceholder\": \"ไม่มีชื่อ\",\n      \"textPlaceholder\": \"ว่างเปล่า\",\n      \"copyProperty\": \"คัดลอกคุณสมบัติไปยังคลิปบอร์ด\",\n      \"count\": \"จำนวน\",\n      \"newRow\": \"แถวใหม่\",\n      \"action\": \"การดำเนินการ\",\n      \"add\": \"คลิกเพิ่มด้านล่าง\",\n      \"drag\": \"ลากเพื่อย้าย\",\n      \"deleteRowPrompt\": \"คุณแน่ใจหรือไม่ว่าต้องการลบแถวนี้? การกระทำนี้ไม่สามารถย้อนกลับได้\",\n      \"deleteCardPrompt\": \"คุณแน่ใจหรือไม่ว่าต้องการลบการ์ดนี้? การกระทำนี้ไม่สามารถย้อนกลับได้\",\n      \"dragAndClick\": \"ลากเพื่อย้ายคลิกเพื่อเปิดเมนู\",\n      \"insertRecordAbove\": \"แทรกเวิ่นระเบียนด้านบน\",\n      \"insertRecordBelow\": \"แทรกเวิ่นระเบียนด้านล่าง\",\n      \"noContent\": \"ไม่มีเนื้อหา\",\n      \"reorderRowDescription\": \"เรียงลำดับแถวใหม่\",\n      \"createRowAboveDescription\": \"สร้างแถวด้านบน\",\n      \"createRowBelowDescription\": \"สร้างแถวด้านล่าง\"\n    },\n    \"selectOption\": {\n      \"create\": \"สร้าง\",\n      \"purpleColor\": \"สีม่วง\",\n      \"pinkColor\": \"สีชมพู\",\n      \"lightPinkColor\": \"ชมพูอ่อน\",\n      \"orangeColor\": \"สีส้ม\",\n      \"yellowColor\": \"สีเหลือง\",\n      \"limeColor\": \"สีมะนาว\",\n      \"greenColor\": \"สีเขียว\",\n      \"aquaColor\": \"สีฟ้าอมเขียว\",\n      \"blueColor\": \"สีน้ำเงิน\",\n      \"deleteTag\": \"ลบแท็ก\",\n      \"colorPanelTitle\": \"สี\",\n      \"panelTitle\": \"เลือกตัวเลือกหรือสร้างตัวเลือกใหม่\",\n      \"searchOption\": \"ค้นหาตัวเลือก\",\n      \"searchOrCreateOption\": \"ค้นหาหรือสร้างตัวเลือก...\",\n      \"createNew\": \"สร้างใหม่\",\n      \"orSelectOne\": \"หรือเลือกตัวเลือก\",\n      \"typeANewOption\": \"พิมพ์ตัวเลือกใหม่\",\n      \"tagName\": \"ชื่อแท็ก\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"คำอธิบายงาน\",\n      \"addNew\": \"เพิ่มงานใหม่\",\n      \"submitNewTask\": \"สร้าง\",\n      \"hideComplete\": \"ซ่อนงานเสร็จ\",\n      \"showComplete\": \"แสดงงานทั้งหมด\"\n    },\n    \"url\": {\n      \"launch\": \"เปิดในเบราว์เซอร์\",\n      \"copy\": \"คัดลอก URL\",\n      \"textFieldHint\": \"ป้อน URL\"\n    },\n    \"relation\": {\n      \"relatedDatabasePlaceLabel\": \"ฐานข้อมูลที่เกี่ยวข้อง\",\n      \"relatedDatabasePlaceholder\": \"ไม่มี\",\n      \"inRelatedDatabase\": \"ใน\",\n      \"rowSearchTextFieldPlaceholder\": \"ค้นหา\",\n      \"noDatabaseSelected\": \"ยังไม่ได้เลือกฐานข้อมูล กรุณาเลือกฐานข้อมูลหนึ่งรายการจากรายการด้านล่างก่อน\",\n      \"emptySearchResult\": \"ไม่พบข้อมูล\",\n      \"linkedRowListLabel\": \"{count} แถวที่เชื่อมโยง\",\n      \"unlinkedRowListLabel\": \"เชื่อมโยงแถวอื่น\"\n    },\n    \"menuName\": \"ตาราง\",\n    \"referencedGridPrefix\": \"มุมมองของ\",\n    \"calculate\": \"คำนวณ\",\n    \"calculationTypeLabel\": {\n      \"none\": \"ไม่มี\",\n      \"average\": \"เฉลี่ย\",\n      \"max\": \"สูงสุด\",\n      \"median\": \"มัธยฐาน\",\n      \"min\": \"ต่ำสุด\",\n      \"sum\": \"ผลรวม\",\n      \"count\": \"นับ\",\n      \"countEmpty\": \"นับที่ว่าง\",\n      \"countEmptyShort\": \"ว่างเปล่า\",\n      \"countNonEmpty\": \"นับที่ไม่ว่าง\",\n      \"countNonEmptyShort\": \"เติมแล้ว\"\n    },\n    \"media\": {\n      \"rename\": \"เปลี่ยนชื่อ\",\n      \"download\": \"ดาวน์โหลด\",\n      \"expand\": \"ขยาย\",\n      \"delete\": \"ลบ\",\n      \"moreFilesHint\": \"+{}\",\n      \"addFileOrImage\": \"เพิ่มไฟล์หรือลิงค์\",\n      \"attachmentsHint\": \"{}\",\n      \"addFileMobile\": \"เพิ่มไฟล์\",\n      \"extraCount\": \"+{}\",\n      \"deleteFileDescription\": \"คุณแน่ใจหรือไม่ว่าต้องการลบไฟล์นี้? การกระทำนี้ไม่สามารถย้อนกลับได้\",\n      \"showFileNames\": \"แสดงชื่อไฟล์\",\n      \"downloadSuccess\": \"ดาวน์โหลดไฟล์แล้ว\",\n      \"downloadFailedToken\": \"ไม่สามารถดาวน์โหลดไฟล์ได้ โทเค็นผู้ใช้ไม่พร้อมใช้งาน\",\n      \"setAsCover\": \"ตั้งเป็นปก\",\n      \"openInBrowser\": \"เปิดในเบราว์เซอร์\",\n      \"embedLink\": \"ฝังลิงค์ไฟล์\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"เอกสาร\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"เลือกกระดานเพื่อเชื่อมโยง\",\n        \"createANewBoard\": \"สร้างกระดานใหม่\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"เลือกตารางเพื่อเชื่อมโยง\",\n        \"createANewGrid\": \"สร้างตารางใหม่\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"เลือกปฏิทินเพื่อเชื่อมโยง\",\n        \"createANewCalendar\": \"สร้างปฏิทินใหม่\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"เลือกเอกสารเพื่อเชื่อมโยง\"\n      },\n      \"name\": {\n        \"text\": \"ข้อความ\",\n        \"heading1\": \"หัวข้อที่ 1\",\n        \"heading2\": \"หัวข้อที่ 2\",\n        \"heading3\": \"หัวข้อที่ 3\",\n        \"image\": \"รูปภาพ\",\n        \"bulletedList\": \"รายการลำดับหัวข้อย่อย\",\n        \"numberedList\": \"รายการลำดับตัวเลข\",\n        \"todoList\": \"รายการสิ่งที่ต้องทำ\",\n        \"doc\": \"เอกสาร\",\n        \"linkedDoc\": \"ลิงค์ไปยังหน้า\",\n        \"grid\": \"กริด\",\n        \"linkedGrid\": \"กริดที่เชื่อมโยง\",\n        \"kanban\": \"คันบัน\",\n        \"linkedKanban\": \"คันบันที่เชื่อมโยง\",\n        \"calendar\": \"ปฏิทิน\",\n        \"linkedCalendar\": \"ปฏิทินที่เชื่อมโยง\",\n        \"quote\": \"คำกล่าว\",\n        \"divider\": \"ตัวคั่น\",\n        \"table\": \"ตาราง\",\n        \"callout\": \"การเน้นข้อความ\",\n        \"outline\": \"โครงร่าง\",\n        \"mathEquation\": \"สมการคณิตศาสตร์\",\n        \"code\": \"โค้ด\",\n        \"toggleList\": \"ตัวเปิดปิดรายการ\",\n        \"toggleHeading1\": \"ตัวเปิดปิดหัวข้อ 1\",\n        \"toggleHeading2\": \"ตัวเปิดปิดหัวข้อ 2\",\n        \"toggleHeading3\": \"ตัวเปิดปิดหัวข้อ 3\",\n        \"emoji\": \"อิโมจิ\",\n        \"aiWriter\": \"นักเขียน AI\",\n        \"dateOrReminder\": \"วันที่หรือการเตือน\",\n        \"photoGallery\": \"แกลอรี่รูปภาพ\",\n        \"file\": \"ไฟล์\"\n      },\n      \"subPage\": {\n        \"name\": \"เอกสาร\",\n        \"keyword1\": \"หน้าย่อย\",\n        \"keyword2\": \"หน้า\",\n        \"keyword3\": \"หน้าย่อย\",\n        \"keyword4\": \"แทรกหน้า\",\n        \"keyword5\": \"ฝังหน้า\",\n        \"keyword6\": \"หน้าใหม่\",\n        \"keyword7\": \"สร้างหน้า\",\n        \"keyword8\": \"เอกสาร\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"โครงร่าง\",\n      \"codeBlock\": \"โค้ดบล็อก\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"กระดานที่อ้างอิง\",\n      \"referencedGrid\": \"ตารางอ้างอิง\",\n      \"referencedCalendar\": \"ปฏิทินที่อ้างอิง\",\n      \"referencedDocument\": \"เอกสารอ้างอิง\",\n      \"autoGeneratorMenuItemName\": \"นักเขียน AI\",\n      \"autoGeneratorTitleName\": \"AI: สอบถาม AI เพื่อให้เขียนอะไรก็ได้...\",\n      \"autoGeneratorLearnMore\": \"เรียนรู้เพิ่มเติม\",\n      \"autoGeneratorGenerate\": \"สร้าง\",\n      \"autoGeneratorHintText\": \"ถาม AI ...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"ไม่สามารถรับคีย์ AI ได้\",\n      \"autoGeneratorRewrite\": \"เขียนใหม่\",\n      \"smartEdit\": \"ผู้ช่วย AI\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"แก้ไขการสะกด\",\n      \"warning\": \"⚠️ คำตอบของ AI อาจจะไม่ถูกต้องหรืออาจจะเข้าใจผิดได้\",\n      \"smartEditSummarize\": \"สรุป\",\n      \"smartEditImproveWriting\": \"ปรับปรุงการเขียน\",\n      \"smartEditMakeLonger\": \"ทำให้ยาวขึ้น\",\n      \"smartEditCouldNotFetchResult\": \"ไม่สามารถดึงผลลัพธ์จาก AI ได้\",\n      \"smartEditCouldNotFetchKey\": \"ไม่สามารถดึงคีย์ AI ได้\",\n      \"smartEditDisabled\": \"เชื่อมต่อ AI ในการตั้งค่า\",\n      \"appflowyAIEditDisabled\": \"ลงชื่อเข้าใช้เพื่อเปิดใช้งานฟีเจอร์ AI\",\n      \"discardResponse\": \"คุณต้องการทิ้งการตอบกลับของ AI หรือไม่\",\n      \"createInlineMathEquation\": \"สร้างสมการ\",\n      \"fonts\": \"แบบอักษร\",\n      \"insertDate\": \"ใส่วันที่\",\n      \"emoji\": \"อิโมจิ\",\n      \"toggleList\": \"ตัวเปิดปิดรายการ\",\n      \"emptyToggleHeading\": \"ตัวเปิดปิด h{} ว่างเปล่า คลิกเพื่อเพิ่มเนื้อหา\",\n      \"emptyToggleList\": \"ตัวเปิดปิดรายการว่างเปล่า คลิกเพื่อเพิ่มเนื้อหา\",\n      \"quoteList\": \"รายการคำกล่าว\",\n      \"numberedList\": \"รายการลำดับตัวเลข\",\n      \"bulletedList\": \"รายการลำดับหัวข้อย่อย\",\n      \"todoList\": \"รายการสิ่งที่ต้องทำ\",\n      \"callout\": \"คำอธิบายประกอบ\",\n      \"simpleTable\": {\n        \"moreActions\": {\n          \"color\": \"สี\",\n          \"align\": \"จัดตำแหน่ง\",\n          \"delete\": \"ลบ\",\n          \"duplicate\": \"ทำสำเนา\",\n          \"insertLeft\": \"แทรกซ้าย\",\n          \"insertRight\": \"แทรกขวา\",\n          \"insertAbove\": \"แทรกด้านบน\",\n          \"insertBelow\": \"แทรกด้านล่าง\",\n          \"headerColumn\": \"ส่วนหัวของคอลัมน์\",\n          \"headerRow\": \"ส่วนหัวของแถว\",\n          \"clearContents\": \"ล้างเนื้อหา\"\n        },\n        \"clickToAddNewRow\": \"คลิกเพื่อเพิ่มแถวใหม่\",\n        \"clickToAddNewColumn\": \"คลิกเพื่อเพิ่มคอลัมน์ใหม่\",\n        \"clickToAddNewRowAndColumn\": \"คลิกเพื่อเพิ่มแถว และคอลัมน์ใหม่\"\n      },\n      \"cover\": {\n        \"changeCover\": \"เปลี่ยนปก\",\n        \"colors\": \"สี\",\n        \"images\": \"รูปภาพ\",\n        \"clearAll\": \"ล้างทั้งหมด\",\n        \"abstract\": \"นามธรรม\",\n        \"addCover\": \"เพิ่มปก\",\n        \"addLocalImage\": \"เพิ่มรูปภาพจากเครื่องของคุณ\",\n        \"invalidImageUrl\": \"URL รูปภาพไม่ถูกต้อง\",\n        \"failedToAddImageToGallery\": \"ไม่สามารถเพิ่มรูปภาพลงในแกลเลอรี่ได้\",\n        \"enterImageUrl\": \"ป้อน URL รูปภาพ\",\n        \"add\": \"เพิ่ม\",\n        \"back\": \"ย้อนกลับ\",\n        \"saveToGallery\": \"บันทึกลงในแกลเลอรี่\",\n        \"removeIcon\": \"ลบไอคอน\",\n        \"removeCover\": \"ลบปก\",\n        \"pasteImageUrl\": \"วาง URL รูปภาพ\",\n        \"or\": \"หรือ\",\n        \"pickFromFiles\": \"เลือกจากไฟล์\",\n        \"couldNotFetchImage\": \"ไม่สามารถดึงรูปภาพได้\",\n        \"imageSavingFailed\": \"บันทึกภาพไม่สำเร็จ\",\n        \"addIcon\": \"เพิ่มไอคอน\",\n        \"changeIcon\": \"เปลี่ยนไอคอน\",\n        \"coverRemoveAlert\": \"มันจะถูกลบจากปกหลังจากที่ถูกลบ\",\n        \"alertDialogConfirmation\": \"คุณแน่ใจหรือไม่ว่าคุณต้องการดำเนินการต่อ?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"สมการทางคณิตศาสตร์\",\n        \"addMathEquation\": \"เพิ่มสมการ TeX\",\n        \"editMathEquation\": \"แก้ไขสมการทางคณิตศาสตร์\"\n      },\n      \"optionAction\": {\n        \"click\": \"คลิก\",\n        \"toOpenMenu\": \" เพื่อเปิดเมนู\",\n        \"drag\": \"ลาก\",\n        \"toMove\": \" เพื่อย้าย\",\n        \"delete\": \"ลบ\",\n        \"duplicate\": \"ทำสำเนา\",\n        \"turnInto\": \"แปลงเป็น\",\n        \"moveUp\": \"เลื่อนขึ้น\",\n        \"moveDown\": \"เลื่อนลง\",\n        \"color\": \"สี\",\n        \"align\": \"จัดตำแหน่ง\",\n        \"left\": \"ซ้าย\",\n        \"center\": \"กึ่งกลาง\",\n        \"right\": \"ขวา\",\n        \"defaultColor\": \"สีเริ่มต้น\",\n        \"depth\": \"ความลึก\",\n        \"copyLinkToBlock\": \"คัดลอกลิงก์ไปยังบล็อก\"\n      },\n      \"image\": {\n        \"addAnImage\": \"เพิ่มรูปภาพ\",\n        \"copiedToPasteBoard\": \"ลิงก์รูปภาพถูกคัดลอกไปที่คลิปบอร์ดแล้ว\",\n        \"addAnImageDesktop\": \"เพิ่มรูปภาพ\",\n        \"addAnImageMobile\": \"คลิกเพื่อเพิ่มรูปภาพหนึ่งรูป หรือมากกว่า\",\n        \"dropImageToInsert\": \"วางภาพเพื่อแทรก\",\n        \"imageUploadFailed\": \"อัพโหลดรูปภาพไม่สำเร็จ\",\n        \"imageDownloadFailed\": \"ดาวน์โหลดรูปภาพล้มเหลว กรุณาลองอีกครั้ง\",\n        \"imageDownloadFailedToken\": \"การดาวน์โหลดภาพล้มเหลวเนื่องจากขาดโทเค็นผู้ใช้ โปรดลองอีกครั้ง\",\n        \"errorCode\": \"รหัสข้อผิดพลาด\"\n      },\n      \"photoGallery\": {\n        \"name\": \"แกลอรี่รูปภาพ\",\n        \"imageKeyword\": \"รูปภาพ\",\n        \"imageGalleryKeyword\": \"แกลอรี่รูปภาพ\",\n        \"photoKeyword\": \"ภาพถ่าย\",\n        \"photoBrowserKeyword\": \"เบราว์เซอร์รูปภาพ\",\n        \"galleryKeyword\": \"แกลเลอรี่\",\n        \"addImageTooltip\": \"เพิ่มรูปภาพ\",\n        \"changeLayoutTooltip\": \"เปลี่ยนเค้าโครง\",\n        \"browserLayout\": \"เบราว์เซอร์\",\n        \"gridLayout\": \"กริด\",\n        \"deleteBlockTooltip\": \"ลบทั้งแกลอรี่\"\n      },\n      \"math\": {\n        \"copiedToPasteBoard\": \"คัดลอกสมการคณิตศาสตร์ไปยังคลิปบอร์ดแล้ว\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"คัดลอกลิงก์ไปยังคลิปบอร์ดแล้ว\",\n        \"convertToLink\": \"แปลงเป็นลิงค์ฝัง\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"เพิ่มหัวข้อเพื่อสร้างสารบัญ\",\n        \"noMatchHeadings\": \"ไม่พบหัวข้อที่ตรงกัน\"\n      },\n      \"table\": {\n        \"addAfter\": \"เพิ่มหลัง\",\n        \"addBefore\": \"เพิ่มก่อน\",\n        \"delete\": \"ลบ\",\n        \"clear\": \"ล้างเนื้อหา\",\n        \"duplicate\": \"ทำสำเนา\",\n        \"bgColor\": \"สีพื้นหลัง\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"คัดลอก\",\n        \"cut\": \"ตัด\",\n        \"paste\": \"วาง\"\n      },\n      \"action\": \"การดำเนินการ\",\n      \"database\": {\n        \"selectDataSource\": \"เลือกแหล่งข้อมูล\",\n        \"noDataSource\": \"ไม่มีแหล่งข้อมูล\",\n        \"selectADataSource\": \"เลือกแหล่งข้อมูล\",\n        \"toContinue\": \"เพื่อดำเนินการต่อ\",\n        \"newDatabase\": \"ฐานข้อมูลใหม่\",\n        \"linkToDatabase\": \"ลิงค์ไปยังฐานข้อมูล\"\n      },\n      \"date\": \"วันที่\",\n      \"video\": {\n        \"label\": \"วีดีโอ\",\n        \"emptyLabel\": \"เพิ่มวิดีโอ\",\n        \"placeholder\": \"วางลิงค์วิดีโอ\",\n        \"copiedToPasteBoard\": \"คัดลอกลิงก์วิดีโอไปยังคลิปบอร์ดแล้ว\",\n        \"insertVideo\": \"เพิ่มวีดีโอ\",\n        \"invalidVideoUrl\": \"URL แหล่งที่มายังไม่รองรับในขณะนี้\",\n        \"invalidVideoUrlYouTube\": \"YouTube ยังไม่รองรับ\",\n        \"supportedFormats\": \"รูปแบบที่รองรับ: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264\"\n      },\n      \"file\": {\n        \"name\": \"ไฟล์\",\n        \"uploadTab\": \"อัพโหลด\",\n        \"uploadMobile\": \"เลือกไฟล์\",\n        \"uploadMobileGallery\": \"จากแกลเลอรี่รูปภาพ\",\n        \"networkTab\": \"ฝังลิงค์\",\n        \"placeholderText\": \"อัพโหลดหรือฝังไฟล์\",\n        \"placeholderDragging\": \"วางไฟล์เพื่ออัพโหลด\",\n        \"dropFileToUpload\": \"วางไฟล์เพื่ออัพโหลด\",\n        \"fileUploadHint\": \"ลากและวางไฟล์หรือคลิกเพื่อ \",\n        \"fileUploadHintSuffix\": \"เรียกดู\",\n        \"networkHint\": \"วางลิงก์ไฟล์\",\n        \"networkUrlInvalid\": \"URL ไม่ถูกต้อง ตรวจสอบ URL และลองอีกครั้ง\",\n        \"networkAction\": \"ฝัง\",\n        \"fileTooBigError\": \"ขนาดไฟล์ใหญ่เกินไป กรุณาอัพโหลดไฟล์ที่มีขนาดน้อยกว่า 10MB\",\n        \"renameFile\": {\n          \"title\": \"เปลี่ยนชื่อไฟล์\",\n          \"description\": \"ป้อนชื่อใหม่สำหรับไฟล์นี้\",\n          \"nameEmptyError\": \"ชื่อไฟล์ไม่สามารถเว้นว่างได้\"\n        },\n        \"uploadedAt\": \"อัพโหลดเมื่อ {}\",\n        \"linkedAt\": \"ลิงก์ถูกเพิ่มเมื่อ {}\",\n        \"failedToOpenMsg\": \"ไม่สามารถเปิดได้ ไม่พบไฟล์\"\n      },\n      \"subPage\": {\n        \"handlingPasteHint\": \" - (การจัดการการวาง)\",\n        \"errors\": {\n          \"failedDeletePage\": \"ลบหน้าไม่สำเร็จ\",\n          \"failedCreatePage\": \"สร้างหน้าไม่สำเร็จ\",\n          \"failedMovePage\": \"ย้ายหน้ามายังเอกสารนี้ไม่สำเร็จ\",\n          \"failedDuplicatePage\": \"ทำสำเนาหน้าไม่สำเร็จ\",\n          \"failedDuplicateFindView\": \"ทำสำเนาหน้าไม่สำเร็จ - ไม่พบมุมมองต้นฉบับ\"\n        }\n      },\n      \"cannotMoveToItsChildren\": \"ไม่สามารถย้ายไปยังหน้าย่อยได้\"\n    },\n    \"outlineBlock\": {\n      \"placeholder\": \"สารบัญ\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"พิมพ์ '/' เพื่อดูคำสั่ง\"\n    },\n    \"title\": {\n      \"placeholder\": \"ไม่มีชื่อเรื่อง\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"คลิกเพื่อเพิ่มรูปภาพ\",\n      \"upload\": {\n        \"label\": \"อัปโหลด\",\n        \"placeholder\": \"คลิกเพื่ออัปโหลดรูปภาพ\"\n      },\n      \"url\": {\n        \"label\": \"URL รูปภาพ\",\n        \"placeholder\": \"ป้อน URL รูปภาพ\"\n      },\n      \"ai\": {\n        \"label\": \"สร้างรูปภาพจาก AI\",\n        \"placeholder\": \"โปรดระบุคำขอให้ AI สร้างรูปภาพ\"\n      },\n      \"stability_ai\": {\n        \"label\": \"สร้างรูปภาพจาก Stability AI\",\n        \"placeholder\": \"โปรดระบุคำขอใช้ Stability AI สร้างรูปภาพ\"\n      },\n      \"support\": \"ขนาดรูปภาพจำกัดอยู่ที่ 5MB รูปแบบที่รองรับ: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"รูปภาพไม่ถูกต้อง\",\n        \"invalidImageSize\": \"ขนาดรูปภาพต้องไม่เกิน 5MB\",\n        \"invalidImageFormat\": \"ไม่รองรับรูปแบบรูปภาพนี้ รูปแบบที่รองรับ: JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"URL รูปภาพไม่ถูกต้อง\",\n        \"noImage\": \"ไม่มีไฟล์หรือไดเร็กทอรีดังกล่าว\",\n        \"multipleImagesFailed\": \"มีภาพหนึ่งหรือมากกว่าที่ไม่สามารถอัปโหลดได้ กรุณาลองใหม่อีกครั้ง\"\n      },\n      \"embedLink\": {\n        \"label\": \"ฝังลิงก์\",\n        \"placeholder\": \"วางหรือพิมพ์ลิงก์รูปภาพ\"\n      },\n      \"unsplash\": {\n        \"label\": \"Unsplash\"\n      },\n      \"searchForAnImage\": \"ค้นหารูปภาพ\",\n      \"pleaseInputYourOpenAIKey\": \"โปรดระบุคีย์ AI ของคุณในหน้าการตั้งค่า\",\n      \"saveImageToGallery\": \"บันทึกภาพ\",\n      \"failedToAddImageToGallery\": \"ไม่สามารถเพิ่มรูปภาพลงในแกลเลอรี่ได้\",\n      \"successToAddImageToGallery\": \"เพิ่มรูปภาพลงในแกลเลอรี่เรียบร้อยแล้ว\",\n      \"unableToLoadImage\": \"ไม่สามารถโหลดรูปภาพได้\",\n      \"maximumImageSize\": \"ขนาดรูปภาพอัปโหลดที่รองรับสูงสุดคือ 10MB\",\n      \"uploadImageErrorImageSizeTooBig\": \"ขนาดรูปภาพต้องน้อยกว่า 10MB\",\n      \"imageIsUploading\": \"กำลังอัพโหลดรูปภาพ\",\n      \"openFullScreen\": \"เปิดแบบเต็มจอ\",\n      \"interactiveViewer\": {\n        \"toolbar\": {\n          \"previousImageTooltip\": \"ภาพก่อนหน้า\",\n          \"nextImageTooltip\": \"ภาพถัดไป\",\n          \"zoomOutTooltip\": \"ซูมออก\",\n          \"zoomInTooltip\": \"ซูมเข้า\",\n          \"changeZoomLevelTooltip\": \"เปลี่ยนระดับการซูม\",\n          \"openLocalImage\": \"เปิดภาพ\",\n          \"downloadImage\": \"ดาวน์โหลดภาพ\",\n          \"closeViewer\": \"ปิดโปรแกรมดูแบบโต้ตอบ\",\n          \"scalePercentage\": \"{}%\",\n          \"deleteImageTooltip\": \"ลบรูปภาพ\"\n        }\n      },\n      \"pleaseInputYourStabilityAIKey\": \"โปรดระบุคีย์ Stability AI ของคุณในหน้าการตั้งค่า\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"ภาษา\",\n        \"placeholder\": \"เลือกภาษา\",\n        \"auto\": \"อัตโนมัติ\"\n      },\n      \"copyTooltip\": \"สำเนา\",\n      \"searchLanguageHint\": \"ค้นหาภาษา\",\n      \"codeCopiedSnackbar\": \"คัดลอกโค้ดไปยังคลิปบอร์ดแล้ว!\"\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"วางหรือพิมพ์ลิงก์\",\n      \"openInNewTab\": \"เปิดในแท็บใหม่\",\n      \"copyLink\": \"คัดลอกลิงก์\",\n      \"removeLink\": \"ลบลิงก์\",\n      \"url\": {\n        \"label\": \"URL ลิงก์\",\n        \"placeholder\": \"ป้อน URL ลิงก์\"\n      },\n      \"title\": {\n        \"label\": \"ชื่อหัวเรื่องลิงก์\",\n        \"placeholder\": \"ป้อนชื่อหัวเรื่องลิงก์\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"ระบุบุคคลหรือหน้าหรือวันที่...\",\n      \"page\": {\n        \"label\": \"ลิงก์ไปยังหน้า\",\n        \"tooltip\": \"คลิกเพื่อเปิดหน้า\"\n      },\n      \"deleted\": \"ลบแล้ว\",\n      \"deletedContent\": \"เนื้อหานี้ไม่มีอยู่หรือถูกลบไปแล้ว\",\n      \"noAccess\": \"ไม่มีการเข้าถึง\",\n      \"deletedPage\": \"หน้าที่ถูกลบ\",\n      \"trashHint\": \" - ในถังขยะ\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"รีเซ็ตเป็นค่าเริ่มต้น\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"เวอร์ชันปัจจุบันไม่รองรับบล็อกนี้\",\n      \"clickToCopyTheBlockContent\": \"คลิกเพื่อคัดลอกเนื้อหาบล็อค\",\n      \"blockContentHasBeenCopied\": \"เนื้อหาบล็อกได้รับการคัดลอกแล้ว\",\n      \"parseError\": \"เกิดข้อผิดพลาดขณะทำการแยกข้อมูลบล็อก {}\",\n      \"copyBlockContent\": \"คัดลอกเนื้อหาบล็อค\"\n    },\n    \"mobilePageSelector\": {\n      \"title\": \"เลือกหน้า\",\n      \"failedToLoad\": \"โหลดรายการหน้าไม่สำเร็จ\",\n      \"noPagesFound\": \"ไม่พบหน้าใดๆ\"\n    },\n    \"attachmentMenu\": {\n      \"choosePhoto\": \"เลือกภาพถ่าย\",\n      \"takePicture\": \"ถ่ายรูป\",\n      \"chooseFile\": \"เลือกไฟล์\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"label\": \"คอลัมน์\",\n      \"createNewCard\": \"สร้างใหม่\",\n      \"renameGroupTooltip\": \"กดเพื่อเปลี่ยนชื่อกลุ่ม\",\n      \"createNewColumn\": \"เพิ่มกลุ่มใหม่\",\n      \"addToColumnTopTooltip\": \"เพิ่มการ์ดใหม่ที่ด้านบนสุด\",\n      \"addToColumnBottomTooltip\": \"เพิ่มการ์ดใหม่ที่ด้านล่างสุด\",\n      \"renameColumn\": \"เปลี่ยนชื่อ\",\n      \"hideColumn\": \"ซ่อน\",\n      \"newGroup\": \"กลุ่มใหม่\",\n      \"deleteColumn\": \"ลบ\",\n      \"deleteColumnConfirmation\": \"การดำเนินการนี้จะลบกลุ่มนี้และการ์ดทั้งหมดในกลุ่ม\\nคุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?\",\n      \"groupActions\": \"กลุ่มการดำเนินการ\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"กลุ่มที่ซ่อนไว้\",\n      \"collapseTooltip\": \"ซ่อนกลุ่มที่ซ่อนไว้\",\n      \"expandTooltip\": \"ดูกลุ่มที่ซ่อนไว้\"\n    },\n    \"cardDetail\": \"รายละเอียดการ์ด\",\n    \"cardActions\": \"การดำเนินการการ์ด\",\n    \"cardDuplicated\": \"การ์ดถูกคัดลอก\",\n    \"cardDeleted\": \"การ์ดถูกลบ\",\n    \"showOnCard\": \"แสดงบนรายละเอียดการ์ด\",\n    \"setting\": \"การตั้งค่า\",\n    \"propertyName\": \"ชื่อคุณสมบัติ\",\n    \"menuName\": \"กระดาน\",\n    \"showUngrouped\": \"แสดงรายการที่ยังไม่ได้จัดกลุ่ม\",\n    \"ungroupedButtonText\": \"ยังไม่ได้จัดกลุ่ม\",\n    \"ungroupedButtonTooltip\": \"ประกอบด้วยการ์ดที่ไม่อยู่ในกลุ่มใดๆ\",\n    \"ungroupedItemsTitle\": \"คลิกเพื่อเพิ่มไปยังกระดาน\",\n    \"groupBy\": \"จัดกลุ่มตาม\",\n    \"groupCondition\": \"เงื่อนไขการจัดกลุ่ม\",\n    \"referencedBoardPrefix\": \"มุมมองของ\",\n    \"notesTooltip\": \"บันทึกย่อข้างใน\",\n    \"mobile\": {\n      \"editURL\": \"แก้ไข URL\",\n      \"showGroup\": \"เลิกซ่อนกลุ่ม\",\n      \"showGroupContent\": \"คุณแน่ใจหรือไม่ว่าต้องการแสดงกลุ่มนี้บนกระดาน?\",\n      \"failedToLoad\": \"โหลดมุมมองกระดานไม่สำเร็จ\"\n    },\n    \"dateCondition\": {\n      \"weekOf\": \"สัปดาห์ที่ {} - {}\",\n      \"today\": \"วันนี้\",\n      \"yesterday\": \"เมื่อวาน\",\n      \"tomorrow\": \"พรุ่งนี้\",\n      \"lastSevenDays\": \"7 วันที่ผ่านมา\",\n      \"nextSevenDays\": \"7 วันถัดไป\",\n      \"lastThirtyDays\": \"30 วันที่ผ่านมา\",\n      \"nextThirtyDays\": \"30 วันถัดไป\"\n    },\n    \"noGroup\": \"ไม่มีการจัดกลุ่มตามคุณสมบัติ\",\n    \"noGroupDesc\": \"มุมมองบอร์ดต้องการคุณสมบัติสำหรับการจัดกลุ่มเพื่อแสดงผล\",\n    \"media\": {\n      \"cardText\": \"{} {}\",\n      \"fallbackName\": \"ไฟล์\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"ปฏิทิน\",\n    \"defaultNewCalendarTitle\": \"ยังไม่ได้ตั้งชื่อ\",\n    \"newEventButtonTooltip\": \"เพิ่มกิจกรรมใหม่\",\n    \"navigation\": {\n      \"today\": \"วันนี้\",\n      \"jumpToday\": \"ข้ามไปยังวันนี้\",\n      \"previousMonth\": \"เดือนก่อนหน้า\",\n      \"nextMonth\": \"เดือนถัดไป\",\n      \"views\": {\n        \"day\": \"วัน\",\n        \"week\": \"สัปดาห์\",\n        \"month\": \"เดือน\",\n        \"year\": \"ปี\"\n      }\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"ยังไม่มีกิจกรรม\",\n      \"emptyBody\": \"กดปุ่มบวกเพื่อสร้างกิจกรรมในวันนี้\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"แสดงหมายเลขสัปดาห์\",\n      \"showWeekends\": \"แสดงวันหยุดสุดสัปดาห์\",\n      \"firstDayOfWeek\": \"เริ่มต้นสัปดาห์ในวัน\",\n      \"layoutDateField\": \"จัดรูปแบบปฏิทินตาม\",\n      \"changeLayoutDateField\": \"เปลี่ยนเค้าโครงฟิลด์\",\n      \"noDateTitle\": \"ไม่มีวันที่\",\n      \"noDateHint\": {\n        \"zero\": \"กิจกรรมที่ไม่ได้กำหนดวันจะแสดงที่นี่\",\n        \"one\": \"{count} กิจกรรมที่ไม่ได้กำหนดวัน\",\n        \"other\": \"{count} กิจกรรมที่ไม่ได้กำหนดวัน\"\n      },\n      \"unscheduledEventsTitle\": \"เหตุการณ์ที่ไม่ได้กำหนดไว้\",\n      \"clickToAdd\": \"คลิกเพื่อเพิ่มไปยังปฏิทิน\",\n      \"name\": \"การตั้งค่าปฏิทิน\",\n      \"clickToOpen\": \"คลิกเพื่อเปิดบันทึก\"\n    },\n    \"referencedCalendarPrefix\": \"มุมมองของ\",\n    \"quickJumpYear\": \"ข้ามไปที่\",\n    \"duplicateEvent\": \"ทำสำเนาเหตุการณ์\"\n  },\n  \"errorDialog\": {\n    \"title\": \"ข้อผิดพลาด AppFlowy\",\n    \"howToFixFallback\": \"ขออภัยในความไม่สะดวก! ส่งปัญหาบนหน้า GitHub ของเราอธิบายถึงข้อผิดพลาดของคุณ\",\n    \"howToFixFallbackHint1\": \"ขออภัยในความไม่สะดวก! ส่งปัญหาของคุณมาที่ \",\n    \"howToFixFallbackHint2\": \" หน้าที่อธิบายข้อผิดพลาดของคุณ\",\n    \"github\": \"ดูบน GitHub\"\n  },\n  \"search\": {\n    \"label\": \"ค้นหา\",\n    \"sidebarSearchIcon\": \"ค้นหาและไปยังหน้านั้นอย่างรวดเร็ว\",\n    \"placeholder\": {\n      \"actions\": \"ค้นหาการการดำเนินการ...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"คัดลอกแล้ว!\",\n      \"fail\": \"ไม่สามารถคัดลอกได้\"\n    }\n  },\n  \"unSupportBlock\": \"เวอร์ชันปัจจุบันไม่รองรับบล็อคนี้\",\n  \"views\": {\n    \"deleteContentTitle\": \"คุณแน่ใจหรือไม่ว่าต้องการลบ {pageType}?\",\n    \"deleteContentCaption\": \"หากคุณลบ {pageType} นี้ คุณสามารถกู้คืนได้จากถังขยะ\"\n  },\n  \"colors\": {\n    \"custom\": \"แบบกำหนดเอง\",\n    \"default\": \"ค่าเริ่มต้น\",\n    \"red\": \"แดง\",\n    \"orange\": \"ส้ม\",\n    \"yellow\": \"เหลือง\",\n    \"green\": \"เขียว\",\n    \"blue\": \"น้ำเงิน\",\n    \"purple\": \"ม่วง\",\n    \"pink\": \"ชมพู\",\n    \"brown\": \"น้ำตาล\",\n    \"gray\": \"เทา\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"อิโมจิ\",\n    \"search\": \"ค้นหาอิโมจิ\",\n    \"noRecent\": \"ไม่มีอิโมจิล่าสุด\",\n    \"noEmojiFound\": \"ไม่พบอิโมจิ\",\n    \"filter\": \"ตัวกรอง\",\n    \"random\": \"แบบสุ่ม\",\n    \"selectSkinTone\": \"เลือกเฉดสี\",\n    \"remove\": \"เอาอิโมจิออก\",\n    \"categories\": {\n      \"smileys\": \"ยิ้มและอารมณ์\",\n      \"people\": \"ผู้คนและรูปร่าง\",\n      \"animals\": \"สัตว์และธรรมชาติ\",\n      \"food\": \"อาหารและเครื่องดื่ม\",\n      \"activities\": \"กิจกรรม\",\n      \"places\": \"การเดินทางและสถานที่\",\n      \"objects\": \"วัตถุ\",\n      \"symbols\": \"สัญลักษณ์\",\n      \"flags\": \"ธงชาติ\",\n      \"nature\": \"ธรรมชาติ\",\n      \"frequentlyUsed\": \"ใช้บ่อย\"\n    },\n    \"skinTone\": {\n      \"default\": \"ค่าเริ่มต้น\",\n      \"light\": \"สีอ่อน\",\n      \"mediumLight\": \"ปานกลางอ่อน\",\n      \"medium\": \"ปานกลาง\",\n      \"mediumDark\": \"ปานกลางเข้ม\",\n      \"dark\": \"เข้ม\"\n    },\n    \"openSourceIconsFrom\": \"ไอคอนโอเพ่นซอร์สจาก\"\n  },\n  \"inlineActions\": {\n    \"noResults\": \"ไม่พบผลลัพธ์\",\n    \"recentPages\": \"หน้าล่าสุด\",\n    \"pageReference\": \"อ้างอิงหน้า\",\n    \"docReference\": \"การอ้างอิงเอกสาร\",\n    \"boardReference\": \"การอ้างอิงบอร์ด\",\n    \"calReference\": \"การอ้างอิงปฏิทิน\",\n    \"gridReference\": \"การอ้างอิงตาราง\",\n    \"date\": \"วันที่\",\n    \"reminder\": {\n      \"groupTitle\": \"ตัวเตือน\",\n      \"shortKeyword\": \"ตัวเตือน\"\n    },\n    \"createPage\": \"สร้าง \\\"{}\\\" หน้าย่อย\"\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"เปลี่ยนรูปแบบวันที่และเวลาในการตั้งค่า\",\n    \"dateFormat\": \"รูปแบบวันที่\",\n    \"includeTime\": \"รวมถึงเวลา\",\n    \"isRange\": \"วันที่สิ้นสุด\",\n    \"timeFormat\": \"รูปแบบเวลา\",\n    \"clearDate\": \"ล้างวันที่\",\n    \"reminderLabel\": \"การเตือน\",\n    \"selectReminder\": \"เลือกการเตือน\",\n    \"reminderOptions\": {\n      \"none\": \"ไม่มี\",\n      \"atTimeOfEvent\": \"เวลาของงาน\",\n      \"fiveMinsBefore\": \"5 นาทีก่อน\",\n      \"tenMinsBefore\": \"10 นาทีก่อน\",\n      \"fifteenMinsBefore\": \"15 นาทีก่อน\",\n      \"thirtyMinsBefore\": \"30 นาทีก่อน\",\n      \"oneHourBefore\": \"1 ชั่วโมงก่อน\",\n      \"twoHoursBefore\": \"2 ชั่วโมงก่อน\",\n      \"onDayOfEvent\": \"ในวันที่มีงาน\",\n      \"oneDayBefore\": \"1 วันก่อน\",\n      \"twoDaysBefore\": \"2 วันก่อน\",\n      \"oneWeekBefore\": \"1 สัปดาห์ก่อน\",\n      \"custom\": \"กำหนดเอง\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"เมื่อวานนี้\",\n    \"today\": \"วันนี้\",\n    \"tomorrow\": \"พรุ่งนี้\",\n    \"oneWeek\": \"1 สัปดาห์\"\n  },\n  \"notificationHub\": {\n    \"title\": \"การแจ้งเตือน\",\n    \"mobile\": {\n      \"title\": \"อัปเดต\"\n    },\n    \"emptyTitle\": \"ตามทันแล้ว!\",\n    \"emptyBody\": \"ไม่มีการแจ้งเตือนหรือการดำเนินการที่รอดำเนินการ โปรดเพลิดเพลินกับความสงบได้\",\n    \"tabs\": {\n      \"inbox\": \"กล่องจดหมายเข้า\",\n      \"upcoming\": \"กำลังจะเกิดขึ้น\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"ทำเครื่องหมายทั้งหมดว่าอ่านแล้ว\",\n      \"showAll\": \"ทั้งหมด\",\n      \"showUnreads\": \"ยังไม่ได้อ่าน\"\n    },\n    \"filters\": {\n      \"ascending\": \"จากน้อยไปมาก\",\n      \"descending\": \"จากมากไปน้อย\",\n      \"groupByDate\": \"จัดกลุ่มตามวันที่\",\n      \"showUnreadsOnly\": \"แสดงเฉพาะเนื้อหาที่ยังไม่ได้อ่าน\",\n      \"resetToDefault\": \"รีเซ็ตเป็นค่าเริ่มต้น\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"แจ้งเตือน\",\n    \"message\": \"โปรดจำไว้ตรวจสอบสิ่งนี้ก่อนที่คุณจะลืม!\",\n    \"tooltipDelete\": \"ลบ\",\n    \"tooltipMarkRead\": \"ทำเครื่องหมายว่าอ่านแล้ว\",\n    \"tooltipMarkUnread\": \"ทำเครื่องหมายว่ายังไม่ได้อ่าน\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"ค้นหา\",\n    \"previousMatch\": \"การจับคู่ก่อนหน้า\",\n    \"nextMatch\": \"การจับคู่ถัดไป\",\n    \"close\": \"ปิด\",\n    \"replace\": \"แทนที่\",\n    \"replaceAll\": \"แทนที่ทั้งหมด\",\n    \"noResult\": \"ไม่มีผลลัพธ์\",\n    \"caseSensitive\": \"แบบเจาะจง\",\n    \"searchMore\": \"ค้นหาเพื่อหาผลลัพธ์เพิ่มเติม\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"ขออภัย\",\n    \"loadingViewError\": \"เรากำลังพบปัญหาในการโหลดมุมมองนี้ โปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณ โหลดแอปซ้ำ และอย่าลังเลที่จะติดต่อทีมงานหากปัญหายังคงไม่หาย\",\n    \"syncError\": \"ข้อมูลไม่ได้รับการซิงค์จากอุปกรณ์อื่น\",\n    \"syncErrorHint\": \"โปรดเปิดหน้านี้อีกครั้งบนอุปกรณ์ที่แก้ไขล่าสุด จากนั้นเปิดอีกครั้งบนอุปกรณ์ปัจจุบัน\",\n    \"clickToCopy\": \"คลิกเพื่อคัดลอกรหัสข้อผิดพลาด\"\n  },\n  \"editor\": {\n    \"bold\": \"ตัวหนา\",\n    \"bulletedList\": \"รายการลำดับหัวข้อย่อย\",\n    \"bulletedListShortForm\": \"รายการสัญลักษณ์จุด\",\n    \"checkbox\": \"กล่องกาเครื่องหมาย\",\n    \"embedCode\": \"ฝังโค้ด\",\n    \"heading1\": \"H1\",\n    \"heading2\": \"H2\",\n    \"heading3\": \"H3\",\n    \"highlight\": \"ไฮไลท์\",\n    \"color\": \"สี\",\n    \"image\": \"รูปภาพ\",\n    \"date\": \"วันที่\",\n    \"page\": \"หน้า\",\n    \"italic\": \"ตัวเอียง\",\n    \"link\": \"ลิงก์\",\n    \"numberedList\": \"รายการลำดับตัวเลข\",\n    \"numberedListShortForm\": \"แบบระบุหมายเลข\",\n    \"toggleHeading1ShortForm\": \"ตัวเปิดปิดหัวข้อ h1\",\n    \"toggleHeading2ShortForm\": \"ตัวเปิดปิดหัวข้อ h2\",\n    \"toggleHeading3ShortForm\": \"ตัวเปิดปิดหัวข้อ h3\",\n    \"quote\": \"คำกล่าว\",\n    \"strikethrough\": \"ขีดฆ่า\",\n    \"text\": \"ข้อความ\",\n    \"underline\": \"ขีดเส้นใต้\",\n    \"fontColorDefault\": \"สีเริ่มต้น\",\n    \"fontColorGray\": \"สีเทา\",\n    \"fontColorBrown\": \"สีน้ำตาล\",\n    \"fontColorOrange\": \"สีส้ม\",\n    \"fontColorYellow\": \"สีเหลือง\",\n    \"fontColorGreen\": \"สีเขียว\",\n    \"fontColorBlue\": \"สีน้ำเงิน\",\n    \"fontColorPurple\": \"สีม่วง\",\n    \"fontColorPink\": \"สีชมพู\",\n    \"fontColorRed\": \"สีแดง\",\n    \"backgroundColorDefault\": \"พื้นหลังเริ่มต้น\",\n    \"backgroundColorGray\": \"พื้นหลังสีเทา\",\n    \"backgroundColorBrown\": \"พื้นหลังสีน้ำตาล\",\n    \"backgroundColorOrange\": \"พื้นหลังสีส้ม\",\n    \"backgroundColorYellow\": \"พื้นหลังสีเหลือง\",\n    \"backgroundColorGreen\": \"พื้นหลังสีเขียว\",\n    \"backgroundColorBlue\": \"พื้นหลังสีน้ำเงิน\",\n    \"backgroundColorPurple\": \"พื้นหลังสีม่วง\",\n    \"backgroundColorPink\": \"พื้นหลังสีชมพู\",\n    \"backgroundColorRed\": \"พื้นหลังสีแดง\",\n    \"backgroundColorLime\": \"พื้นหลังสีเขียวมะนาว\",\n    \"backgroundColorAqua\": \"พื้นหลังสีฟ้าน้ำทะเล\",\n    \"done\": \"เสร็จสิ้น\",\n    \"cancel\": \"ยกเลิก\",\n    \"tint1\": \"สีจาง 1\",\n    \"tint2\": \"สีจาง 2\",\n    \"tint3\": \"สีจาง 3\",\n    \"tint4\": \"สีจาง 4\",\n    \"tint5\": \"สีจาง 5\",\n    \"tint6\": \"สีจาง 6\",\n    \"tint7\": \"สีจาง 7\",\n    \"tint8\": \"สีจาง 8\",\n    \"tint9\": \"สีจาง 9\",\n    \"lightLightTint1\": \"สีม่วง\",\n    \"lightLightTint2\": \"สีชมพู\",\n    \"lightLightTint3\": \"สีชมพูอ่อน\",\n    \"lightLightTint4\": \"สีส้ม\",\n    \"lightLightTint5\": \"สีเหลือง\",\n    \"lightLightTint6\": \"สีมะนาว\",\n    \"lightLightTint7\": \"สีเขียว\",\n    \"lightLightTint8\": \"สีฟ้าอมเขียว\",\n    \"lightLightTint9\": \"สีน้ำเงิน\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"หัวข้อที่ 1\",\n    \"mobileHeading2\": \"หัวข้อที่ 2\",\n    \"mobileHeading3\": \"หัวข้อที่ 3\",\n    \"textColor\": \"สีข้อความ\",\n    \"backgroundColor\": \"สีพื้นหลัง\",\n    \"addYourLink\": \"เพิ่มลิงก์ของคุณ\",\n    \"openLink\": \"เปิดลิงก์\",\n    \"copyLink\": \"คัดลอกลิงก์\",\n    \"removeLink\": \"ลบลิงก์\",\n    \"editLink\": \"แก้ไขลิงก์\",\n    \"linkText\": \"ข้อความ\",\n    \"linkTextHint\": \"โปรดป้อนข้อความ\",\n    \"linkAddressHint\": \"โปรดป้อน URL\",\n    \"highlightColor\": \"สีไฮไลท์\",\n    \"clearHighlightColor\": \"ล้างสีไฮไลท์\",\n    \"customColor\": \"สีแบบกำหนดเอง\",\n    \"hexValue\": \"ค่า Hex\",\n    \"opacity\": \"ความทึบ\",\n    \"resetToDefaultColor\": \"รีเซ็ตเป็นสีเริ่มต้น\",\n    \"ltr\": \"ซ้ายไปขวา\",\n    \"rtl\": \"ขวาไปซ้าย\",\n    \"auto\": \"อัตโนมัติ\",\n    \"cut\": \"ตัด\",\n    \"copy\": \"คัดลอก\",\n    \"paste\": \"วาง\",\n    \"find\": \"ค้นหา\",\n    \"select\": \"เลือก\",\n    \"selectAll\": \"เลือกทั้งหมด\",\n    \"previousMatch\": \"จับคู่ก่อนหน้า\",\n    \"nextMatch\": \"จับคู่ถัดไป\",\n    \"closeFind\": \"ปิด\",\n    \"replace\": \"แทนที่\",\n    \"replaceAll\": \"แทนที่ทั้งหมด\",\n    \"regex\": \"Regex\",\n    \"caseSensitive\": \"แบบเจาะจง\",\n    \"uploadImage\": \"อัปโหลดรูปภาพ\",\n    \"urlImage\": \"URL รูปภาพ\",\n    \"incorrectLink\": \"ลิงก์ผิด\",\n    \"upload\": \"อัปโหลด\",\n    \"chooseImage\": \"เลือกภาพ\",\n    \"loading\": \"กำลังโหลด\",\n    \"imageLoadFailed\": \"ไม่สามารถโหลดรูปภาพได้\",\n    \"divider\": \"ตัวแบ่ง\",\n    \"table\": \"ตาราง\",\n    \"colAddBefore\": \"เพิ่มด้านหน้า\",\n    \"rowAddBefore\": \"เพิ่มด้านหน้า\",\n    \"colAddAfter\": \"เพิ่มด้านหลัง\",\n    \"rowAddAfter\": \"เพิ่มด้านหลัง\",\n    \"colRemove\": \"ลบ\",\n    \"rowRemove\": \"ลบ\",\n    \"colDuplicate\": \"ทำซ้ำ\",\n    \"rowDuplicate\": \"ทำซ้ำ\",\n    \"colClear\": \"ล้างเนื้อหา\",\n    \"rowClear\": \"ล้างเนื้อหา\",\n    \"slashPlaceHolder\": \"พิมพ์ / เพื่อแทรกบล็อก หรือเริ่มพิมพ์\",\n    \"typeSomething\": \"พิมพ์อะไรบางอย่าง...\",\n    \"toggleListShortForm\": \"สลับ\",\n    \"quoteListShortForm\": \"คำกล่าว\",\n    \"mathEquationShortForm\": \"สูตร\",\n    \"codeBlockShortForm\": \"โค้ด\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"ไม่มีหน้ารายการโปรด\",\n    \"noFavoriteHintText\": \"ปัดหน้าไปทางซ้ายเพื่อเพิ่มลงในรายการโปรด\",\n    \"removeFromSidebar\": \"ลบออกจากแถบด้านข้าง\",\n    \"addToSidebar\": \"ปักหมุดไปที่แถบด้านข้าง\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"ป้อน / เพื่อแทรกบล็อก หรือเริ่มพิมพ์\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"รายการสิ่งที่ต้องทำ\",\n    \"bulletList\": \"รายการ\",\n    \"numberList\": \"รายการ\",\n    \"quote\": \"คำกล่าว\",\n    \"heading\": \"หัวข้อ {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"ไอคอนหน้า\",\n    \"language\": \"ภาษา\",\n    \"font\": \"แบบอักษร\",\n    \"actions\": \"การดำเนินการ\",\n    \"date\": \"วันที่\",\n    \"addField\": \"เพิ่มฟิลด์\",\n    \"userIcon\": \"ไอคอนผู้ใช้งาน\"\n  },\n  \"noLogFiles\": \"ไม่มีไฟล์บันทึก\",\n  \"newSettings\": {\n    \"myAccount\": {\n      \"title\": \"บัญชีของฉัน\",\n      \"subtitle\": \"ปรับแต่งโปรไฟล์ของคุณ จัดการความปลอดภัยบัญชี เปิดคีย์ AI หรือเข้าสู่ระบบบัญชีของคุณ\",\n      \"profileLabel\": \"ชื่อบัญชีและรูปโปรไฟล์\",\n      \"profileNamePlaceholder\": \"กรุณากรอกชื่อของคุณ\",\n      \"accountSecurity\": \"ความปลอดภัยของบัญชี\",\n      \"2FA\": \"การยืนยันตัวตน 2 ขั้นตอน\",\n      \"aiKeys\": \"คีย์ AI\",\n      \"accountLogin\": \"การเข้าสู่ระบบบัญชี\",\n      \"updateNameError\": \"อัปเดตชื่อไม่สำเร็จ\",\n      \"updateIconError\": \"อัปเดตไอคอนไม่สำเร็จ\",\n      \"deleteAccount\": {\n        \"title\": \"ลบบัญชี\",\n        \"subtitle\": \"ลบบัญชี และข้อมูลทั้งหมดของคุณอย่างถาวร\",\n        \"description\": \"ลบบัญชีของคุณอย่างถาวร และลบสิทธิ์การเข้าถึงจากพื้นที่ทำงานทั้งหมด\",\n        \"deleteMyAccount\": \"ลบบัญชีของฉัน\",\n        \"dialogTitle\": \"ลบบัญชี\",\n        \"dialogContent1\": \"คุณแน่ใจหรือไม่ว่าต้องการลบบัญชีของคุณอย่างถาวร?\",\n        \"dialogContent2\": \"การดำเนินการนี้ไม่สามารถย้อนกลับได้ และจะลบสิทธิ์การเข้าถึงจากพื้นที่ทำงานทั้งหมด ลบบัญชีของคุณทั้งหมด รวมถึงพื้นที่ทำงานส่วนตัว และลบคุณออกจากพื้นที่ทำงานที่แชร์ทั้งหมด\",\n        \"confirmHint1\": \"กรุณาพิมพ์ \\\"DELETE MY ACCOUNT\\\" เพื่อยืนยัน\",\n        \"confirmHint2\": \"ฉันเข้าใจดีว่าการดำเนินการนี้ไม่สามารถย้อนกลับได้ และจะลบบัญชีของฉันรวมถึงข้อมูลที่เกี่ยวข้องทั้งหมดอย่างถาวร\",\n        \"confirmHint3\": \"DELETE MY ACCOUNT\",\n        \"checkToConfirmError\": \"คุณต้องทำเครื่องหมายในช่องเพื่อยืนยันการลบ\",\n        \"failedToGetCurrentUser\": \"ไม่สามารถดึงอีเมลของผู้ใช้ปัจจุบันได้\",\n        \"confirmTextValidationFailed\": \"ข้อความยืนยันของคุณไม่ตรงกับ \\\"DELETE MY ACCOUNT\\\"\",\n        \"deleteAccountSuccess\": \"ลบบัญชีสำเร็จแล้ว\"\n      }\n    },\n    \"workplace\": {\n      \"name\": \"พื้นไที่ทำงาน\",\n      \"title\": \"การตั้งค่าพื้นไที่ทำงาน\",\n      \"subtitle\": \"ปรับแต่งรูปลักษณ์พื้นที่ทำงาน ธีม แบบอักษร เค้าโครงข้อความ วันที่ เวลา และภาษาของคุณ\",\n      \"workplaceName\": \"ชื่อพื้นไที่ทำงาน\",\n      \"workplaceNamePlaceholder\": \"กรอกชื่อพื้นที่ทำงาน\",\n      \"workplaceIcon\": \"ไอคอนพื้นที่ทำงาน\",\n      \"workplaceIconSubtitle\": \"อัปโหลดรูปภาพ หรือใช้อีโมจิสำหรับพื้นที่ทำงานของคุณ ไอคอนจะแสดงในแถบด้านข้าง และการแจ้งเตือนของคุณ\",\n      \"renameError\": \"ไม่สามารถเปลี่ยนชื่อสถานที่ทำงานได้\",\n      \"updateIconError\": \"ไม่สามารถอัปเดตไอคอนได้\",\n      \"chooseAnIcon\": \"เลือกไอคอน\",\n      \"appearance\": {\n        \"name\": \"ลักษณะการแสดงผล\",\n        \"themeMode\": {\n          \"auto\": \"อัตโนมัติ\",\n          \"light\": \"สว่าง\",\n          \"dark\": \"มืด\"\n        },\n        \"language\": \"ภาษา\"\n      }\n    },\n    \"syncState\": {\n      \"syncing\": \"กำลังซิงค์\",\n      \"synced\": \"ซิงค์แล้ว\",\n      \"noNetworkConnected\": \"ไม่มีการเชื่อมต่อเครือข่าย\"\n    }\n  },\n  \"pageStyle\": {\n    \"title\": \"รูปแบบหน้า\",\n    \"layout\": \"เค้าโครง\",\n    \"coverImage\": \"ภาพปก\",\n    \"pageIcon\": \"ไอคอนหน้าเพจ\",\n    \"colors\": \"สีสัน\",\n    \"gradient\": \"การไล่ระดับ\",\n    \"backgroundImage\": \"ภาพพื้นหลัง\",\n    \"presets\": \"พรีเซ็ต\",\n    \"photo\": \"รูปถ่าย\",\n    \"unsplash\": \"อันสแปลช\",\n    \"pageCover\": \"ปกหน้า\",\n    \"none\": \"ไม่มี\",\n    \"openSettings\": \"เปิดการตั้งค่า\",\n    \"photoPermissionTitle\": \"@:appName ต้องการเข้าถึงคลังรูปภาพของคุณ\",\n    \"photoPermissionDescription\": \"@:appName ต้องการเข้าถึงรูปภาพของคุณเพื่อให้คุณสามารถเพิ่มภาพลงในเอกสารของคุณได้\",\n    \"cameraPermissionTitle\": \"@:appName ต้องการเข้าถึงกล้องของคุณ\",\n    \"cameraPermissionDescription\": \"@:appName ต้องการเข้าถึงกล้องของคุณเพื่อให้คุณสามารถเพิ่มรูปภาพลงในเอกสารจากกล้องได้\",\n    \"doNotAllow\": \"ไม่อนุญาต\",\n    \"image\": \"รูปภาพ\"\n  },\n  \"commandPalette\": {\n    \"placeholder\": \"พิมพ์เพื่อค้นหา...\",\n    \"bestMatches\": \"การจับคู่ที่ดีที่สุด\",\n    \"recentHistory\": \"ประวัติล่าสุด\",\n    \"navigateHint\": \"เพื่อนำทาง\",\n    \"loadingTooltip\": \"เรากำลังมองหาผลลัพธ์...\",\n    \"betaLabel\": \"BETA\",\n    \"betaTooltip\": \"ปัจจุบันเรารองรับเฉพาะการค้นหาหน้า หรือเนื้อหาภายในเอกสารเท่านั้น\",\n    \"fromTrashHint\": \"จากถังขยะ\",\n    \"noResultsHint\": \"เราไม่พบสิ่งที่คุณกำลังมองหา ลองค้นหาด้วยคำอื่นดู\",\n    \"clearSearchTooltip\": \"ล้างช่องค้นหา\"\n  },\n  \"space\": {\n    \"delete\": \"ลบ\",\n    \"deleteConfirmation\": \"ลบ: \",\n    \"deleteConfirmationDescription\": \"หน้าทั้งหมดในพื้นที่นี้จะถูกลบออก และย้ายไปยังถังขยะ และหน้าที่เผยแพร่ทั้งหมดจะถูกยกเลิกการเผยแพร่\",\n    \"rename\": \"เปลี่ยนชื่อพื้นที่\",\n    \"changeIcon\": \"เปลี่ยนไอคอน\",\n    \"manage\": \"จัดการพื้นที่\",\n    \"addNewSpace\": \"สร้างพื้นที่\",\n    \"collapseAllSubPages\": \"ยุบหน้าย่อยทั้งหมด\",\n    \"createNewSpace\": \"สร้างพื้นที่ใหม่\",\n    \"createSpaceDescription\": \"สร้างพื้นที่สาธารณะ และส่วนตัวหลายแห่ง เพื่อจัดระเบียบงานของคุณได้ดีขึ้น\",\n    \"spaceName\": \"ชื่อพื้นที่\",\n    \"spaceNamePlaceholder\": \"เช่น การตลาด วิศวกรรม ทรัพยากรบุคคล\",\n    \"permission\": \"การอนุญาตใช้พื้นที่\",\n    \"publicPermission\": \"สาธารณะ\",\n    \"publicPermissionDescription\": \"สมาชิกพื้นที่ทำงานทั้งหมดที่มีสิทธิ์เข้าถึงเต็มรูปแบบ\",\n    \"privatePermission\": \"ส่วนตัว\",\n    \"privatePermissionDescription\": \"เฉพาะคุณเท่านั้นที่สามารถเข้าถึงพื้นที่นี้ได้\",\n    \"spaceIconBackground\": \"สีพื้นหลัง\",\n    \"spaceIcon\": \"ไอคอน\",\n    \"dangerZone\": \"โซนอันตราย\",\n    \"unableToDeleteLastSpace\": \"ไม่สามารถลบพื้นที่สุดท้ายได้\",\n    \"unableToDeleteSpaceNotCreatedByYou\": \"ไม่สามารถลบพื้นที่ที่สร้างโดยผู้อื่นได้\",\n    \"enableSpacesForYourWorkspace\": \"เปิดใช้งานพื้นที่สำหรับพื้นที่ทำงานของคุณ\",\n    \"title\": \"พื้นที่\",\n    \"defaultSpaceName\": \"ทั่วไป\",\n    \"upgradeSpaceTitle\": \"เปิดใช้งานพื้นที่\",\n    \"upgradeSpaceDescription\": \"สร้างพื้นที่สาธารณะ และส่วนตัวหลายแห่ง เพื่อจัดระเบียบพื้นที่ทำงานของคุณได้ดีขึ้น\",\n    \"upgrade\": \"อัปเดต\",\n    \"upgradeYourSpace\": \"สร้างหลายพื้นที่\",\n    \"quicklySwitch\": \"สลับไปยังพื้นที่ถัดไปอย่างรวดเร็ว\",\n    \"duplicate\": \"ทำสำเนาพื้นที่\",\n    \"movePageToSpace\": \"ย้ายหน้าไปยังพื้นที่\",\n    \"cannotMovePageToDatabase\": \"ไม่สามารถย้ายหน้าไปยังฐานข้อมูลได้\",\n    \"switchSpace\": \"สลับพื้นที่\",\n    \"spaceNameCannotBeEmpty\": \"ชื่อพื้นที่ไม่สามารถปล่อยว่างได้\",\n    \"success\": {\n      \"deleteSpace\": \"ลบพื้นที่สำเร็จ\",\n      \"renameSpace\": \"เปลี่ยนชื่อพื้นที่สำเร็จ\",\n      \"duplicateSpace\": \"คัดลอกพื้นที่สำเร็จ\",\n      \"updateSpace\": \"อัปเดตพื้นที่สำเร็จ\"\n    },\n    \"error\": {\n      \"deleteSpace\": \"ลบพื้นที่ไม่สำเร็จ\",\n      \"renameSpace\": \"เปลี่ยนชื่อพื้นที่ไม่สำเร็จ\",\n      \"duplicateSpace\": \"ทำสำเนาพื้นที่ไม่สำเร็จ\",\n      \"updateSpace\": \"อัปเดตพื้นที่ไม่สำเร็จ\"\n    },\n    \"createSpace\": \"สร้างพื้นที่\",\n    \"manageSpace\": \"จัดการพื้นที่\",\n    \"renameSpace\": \"เปลี่ยนชื่อพื้นที่\",\n    \"mSpaceIconColor\": \"สีไอคอนพื้นที่\",\n    \"mSpaceIcon\": \"ไอคอนพื้นที่\"\n  },\n  \"publish\": {\n    \"hasNotBeenPublished\": \"หน้านี้ยังไม่ได้เผยแพร่\",\n    \"spaceHasNotBeenPublished\": \"ยังไม่รองรับการเผยแพร่พื้นที่\",\n    \"reportPage\": \"รายงานหน้า\",\n    \"databaseHasNotBeenPublished\": \"ยังไม่รองรับการเผยแพร่ฐานข้อมูล\",\n    \"createdWith\": \"สร้างด้วย\",\n    \"downloadApp\": \"ดาวน์โหลด AppFlowy\",\n    \"copy\": {\n      \"codeBlock\": \"คัดลอกเนื้อหาของโค้ดบล็อคไปยังคลิปบอร์ดแล้ว\",\n      \"imageBlock\": \"คัดลอกลิงก์รูปภาพไปยังคลิปบอร์ดแล้ว\",\n      \"mathBlock\": \"คัดลอกสมการคณิตศาสตร์ไปยังคลิปบอร์ดแล้ว\",\n      \"fileBlock\": \"คัดลอกลิงก์ไฟล์ไปยังคลิปบอร์ดแล้ว\"\n    },\n    \"containsPublishedPage\": \"หน้านี้มีหน้าที่เผยแพร่แล้วหนึ่งหน้าขึ้นไป หากคุณดำเนินการต่อ หน้าเหล่านั้นจะถูกยกเลิกการเผยแพร่ คุณต้องการดำเนินการลบหรือไม่\",\n    \"publishSuccessfully\": \"เผยแพร่สำเร็จ\",\n    \"unpublishSuccessfully\": \"ยกเลิกการเผยแพร่สำเร็จ\",\n    \"publishFailed\": \"เผยแพร่ไม่สำเร็จ\",\n    \"unpublishFailed\": \"ยกเลิกการเผยแพร่ไม่สำเร็จ\",\n    \"noAccessToVisit\": \"ไม่มีการเข้าถึงหน้านี้...\",\n    \"createWithAppFlowy\": \"สร้างเว็บไซต์ด้วย AppFlowy\",\n    \"fastWithAI\": \"รวดเร็วและง่ายดายด้วย AI\",\n    \"tryItNow\": \"ลองเลยตอนนี้\",\n    \"onlyGridViewCanBePublished\": \"สามารถเผยแพร่ได้เฉพาะมุมมองแบบกริดเท่านั้น\",\n    \"database\": {\n      \"zero\": \"เผยแพร่ {} มุมมองที่เลือก\",\n      \"one\": \"เผยแพร่ {} มุมมองที่เลือก\",\n      \"many\": \"เผยแพร่ {} มุมมองที่เลือก\",\n      \"other\": \"เผยแพร่ {} มุมมองที่เลือก\"\n    },\n    \"mustSelectPrimaryDatabase\": \"ต้องเลือกมุมมองหลัก\",\n    \"noDatabaseSelected\": \"ยังไม่ได้เลือกฐานข้อมูล กรุณาเลือกอย่างน้อยหนึ่งฐานข้อมูล\",\n    \"unableToDeselectPrimaryDatabase\": \"ไม่สามารถยกเลิกการเลือกฐานข้อมูลหลักได้\",\n    \"saveThisPage\": \"เริ่มต้นด้วยเทมเพลตนี้\",\n    \"duplicateTitle\": \"คุณต้องการเพิ่มที่ไหน\",\n    \"selectWorkspace\": \"เลือกพื้นที่ทำงาน\",\n    \"addTo\": \"เพิ่มไปยัง\",\n    \"duplicateSuccessfully\": \"เพิ่มไปที่พื้นที่ทำงานของคุณแล้ว\",\n    \"duplicateSuccessfullyDescription\": \"ยังไม่ได้ติดตั้ง AppFlowy ใช่ไหม? การดาวน์โหลดจะเริ่มต้นโดยอัตโนมัติหลังจากที่คุณคลิก 'ดาวน์โหลด'\",\n    \"downloadIt\": \"ดาวน์โหลด\",\n    \"openApp\": \"เปิดในแอป\",\n    \"duplicateFailed\": \"ทำสำเนาไม่สำเร็จ\",\n    \"membersCount\": {\n      \"zero\": \"ไม่มีสมาชิก\",\n      \"one\": \"1 สมาชิก\",\n      \"many\": \"{count} สมาชิก\",\n      \"other\": \"{count} สมาชิก\"\n    },\n    \"useThisTemplate\": \"ใช้เทมเพลต\"\n  },\n  \"web\": {\n    \"continue\": \"ดำเนินการต่อ\",\n    \"or\": \"หรือ\",\n    \"continueWithGoogle\": \"ดำเนินการต่อด้วย Google\",\n    \"continueWithGithub\": \"ดำเนินการต่อด้วย GitHub\",\n    \"continueWithDiscord\": \"ดำเนินการต่อด้วย Discord\",\n    \"continueWithApple\": \"ดำเนินการต่อด้วย Apple \",\n    \"moreOptions\": \"ตัวเลือกเพิ่มเติม\",\n    \"collapse\": \"ยุบ\",\n    \"signInAgreement\": \"โดยการคลิก \\\"ดำเนินการต่อ\\\" ด้านบน, คุณตกลงตามข้อกำหนดของ AppFlowy\",\n    \"and\": \"และ\",\n    \"termOfUse\": \"เงื่อนไข\",\n    \"privacyPolicy\": \"นโยบายความเป็นส่วนตัว\",\n    \"signInError\": \"เกิดข้อผิดพลาดในการเข้าสู่ระบบ\",\n    \"login\": \"ลงทะเบียนหรือเข้าสู่ระบบ\",\n    \"fileBlock\": {\n      \"uploadedAt\": \"อัพโหลดเมื่อ {เวลา}\",\n      \"linkedAt\": \"เพิ่มลิงก์เมื่อ {เวลา}\",\n      \"empty\": \"อัพโหลดหรือฝังไฟล์\"\n    },\n    \"importNotion\": \"นำเข้าจาก Notion\",\n    \"import\": \"นำเข้า\",\n    \"importSuccess\": \"อัพโหลดสำเร็จ\",\n    \"importSuccessMessage\": \"เราจะแจ้งให้คุณทราบเมื่อการนำเข้าเสร็จสมบูรณ์ หลังจากนั้นคุณสามารถดูหน้าที่นำเข้าได้ในแถบด้านข้าง\",\n    \"importFailed\": \"การนำเข้าไม่สำเร็จ กรุณาตรวจสอบรูปแบบไฟล์\",\n    \"dropNotionFile\": \"วางไฟล์ zip ของ Notion ไว้ที่นี่เพื่ออัพโหลด หรือคลิกเพื่อเรียกดู\"\n  },\n  \"globalComment\": {\n    \"comments\": \"ความคิดเห็น\",\n    \"addComment\": \"เพิ่มความคิดเห็น\",\n    \"reactedBy\": \"ตอบสนองโดย\",\n    \"addReaction\": \"เพิ่มความรู้สึก\",\n    \"reactedByMore\": \"และอีก {count}\",\n    \"showSeconds\": {\n      \"one\": \"1 วินาทีที่แล้ว\",\n      \"other\": \"{count} วินาทีที่ผ่านมา\",\n      \"zero\": \"เมื่อสักครู่\",\n      \"many\": \"{count} วินาทีที่ผ่านมา\"\n    },\n    \"showMinutes\": {\n      \"one\": \"1 นาทีที่แล้ว\",\n      \"other\": \"{count} นาทีที่ผ่านมา\",\n      \"many\": \"{count} นาทีที่ผ่านมา\"\n    },\n    \"showHours\": {\n      \"one\": \"1 ชั่วโมงที่แล้ว\",\n      \"other\": \"{count} ชั่วโมงที่ผ่านมา\",\n      \"many\": \"{count} ชั่วโมงที่ผ่านมา\"\n    },\n    \"showDays\": {\n      \"one\": \"1 วันที่ผ่านมา\",\n      \"other\": \"{count} วันที่ผ่านมา\",\n      \"many\": \"{count} วันที่ผ่านมา\"\n    },\n    \"showMonths\": {\n      \"one\": \"1 เดือนที่ผ่านมา\",\n      \"other\": \"{count} เดือนที่ผ่านมา\",\n      \"many\": \"{count} เดือนที่ผ่านมา\"\n    },\n    \"showYears\": {\n      \"one\": \"1 ปีที่ผ่านมา\",\n      \"other\": \"{count} ปีที่ผ่านมา\",\n      \"many\": \"{count} ปีที่ผ่านมา\"\n    },\n    \"reply\": \"ตอบกลับ\",\n    \"deleteComment\": \"ลบความคิดเห็น\",\n    \"youAreNotOwner\": \"คุณไม่ใช่เจ้าของความคิดเห็นนี้\",\n    \"confirmDeleteDescription\": \"คุณแน่ใจหรือไม่ว่าต้องการลบความคิดเห็นนี้?\",\n    \"hasBeenDeleted\": \"ลบแล้ว\",\n    \"replyingTo\": \"กำลังตอบกลับถึง\",\n    \"noAccessDeleteComment\": \"คุณไม่มีสิทธิ์ลบความคิดเห็นนี้\",\n    \"collapse\": \"ยุบ\",\n    \"readMore\": \"อ่านเพิ่มเติม\",\n    \"failedToAddComment\": \"เพิ่มความคิดเห็นไม่สำเร็จ\",\n    \"commentAddedSuccessfully\": \"เพิ่มความคิดเห็นสำเร็จ\",\n    \"commentAddedSuccessTip\": \"คุณเพิ่งเพิ่ม หรือตอบกลับความคิดเห็น คุณต้องการข้ามไปที่ด้านบนเพื่อดูความคิดเห็นล่าสุดหรือไม่\"\n  },\n  \"template\": {\n    \"asTemplate\": \"บันทึกเป็นเทมเพลต\",\n    \"name\": \"ชื่อเทมเพลต\",\n    \"description\": \"คำอธิบายเทมเพลต\",\n    \"about\": \"เทมเพลตเกี่ยวกับ\",\n    \"deleteFromTemplate\": \"ลบออกจากเทมเพลต\",\n    \"preview\": \"ตัวอย่างเทมเพลต\",\n    \"categories\": \"หมวดหมู่เทมเพลต\",\n    \"isNewTemplate\": \"PIN ไปยังเทมเพลตใหม่\",\n    \"featured\": \"PIN ไปที่ฟีเจอร์\",\n    \"relatedTemplates\": \"เทมเพลตที่เกี่ยวข้อง\",\n    \"requiredField\": \"{field} จำเป็นต้องกรอก\",\n    \"addCategory\": \"เพิ่ม \\\"{category}\\\"\",\n    \"addNewCategory\": \"เพิ่มหมวดหมู่ใหม่\",\n    \"addNewCreator\": \"เพิ่มผู้สร้างใหม่\",\n    \"deleteCategory\": \"ลบหมวดหมู่\",\n    \"editCategory\": \"แก้ไขหมวดหมู่\",\n    \"editCreator\": \"แก้ไขผู้สร้าง\",\n    \"category\": {\n      \"name\": \"ชื่อหมวดหมู่\",\n      \"icon\": \"ไอคอนหมวดหมู่\",\n      \"bgColor\": \"สีพื้นหลังหมวดหมู่\",\n      \"priority\": \"ลำดับความสำคัญของหมวดหมู่\",\n      \"desc\": \"คำอธิบายหมวดหมู่\",\n      \"type\": \"ประเภทหมวดหมู่\",\n      \"icons\": \"ไอคอนหมวดหมู่\",\n      \"colors\": \"สีหมวดหมู่\",\n      \"byUseCase\": \"ตามลักษณะการใช้งาน\",\n      \"byFeature\": \"ตามคุณสมบัติ\",\n      \"deleteCategory\": \"ลบหมวดหมู่\",\n      \"deleteCategoryDescription\": \"คุณแน่ใจหรือไม่ว่าต้องการลบหมวดหมู่นี้?\",\n      \"typeToSearch\": \"พิมพ์เพื่อค้นหาหมวดหมู่...\"\n    },\n    \"creator\": {\n      \"label\": \"ผู้สร้างเทมเพลต\",\n      \"name\": \"ชื่อผู้สร้าง\",\n      \"avatar\": \"อวตาร์ของผู้สร้าง\",\n      \"accountLinks\": \"ลิงค์บัญชีผู้สร้าง\",\n      \"uploadAvatar\": \"คลิกเพื่ออัพโหลดอวาตาร์\",\n      \"deleteCreator\": \"ลบผู้สร้าง\",\n      \"deleteCreatorDescription\": \"คุณแน่ใจหรือไม่ว่าต้องการลบผู้สร้างรายนี้?\",\n      \"typeToSearch\": \"พิมพ์เพื่อค้นหาผู้สร้าง...\"\n    },\n    \"uploadSuccess\": \"อัพโหลดเทมเพลตสำเร็จ\",\n    \"uploadSuccessDescription\": \"เทมเพลตของคุณได้รับการอัปโหลดสำเร็จแล้ว ขณะนี้คุณสามารถดูเทมเพลตได้ในแกลเลอรีเทมเพลต\",\n    \"viewTemplate\": \"ดูเทมเพลต\",\n    \"deleteTemplate\": \"ลบเทมเพลต\",\n    \"deleteSuccess\": \"ลบเทมเพลตสำเร็จ\",\n    \"deleteTemplateDescription\": \"การกระทำนี้จะไม่ส่งผลกระทบต่อหน้าปัจจุบัน หรือสถานะการเผยแพร่ คุณแน่ใจหรือไม่ว่าต้องการลบเทมเพลตนี้?\",\n    \"addRelatedTemplate\": \"เพิ่มเทมเพลตที่เกี่ยวข้อง\",\n    \"removeRelatedTemplate\": \"ลบเทมเพลตที่เกี่ยวข้อง\",\n    \"uploadAvatar\": \"อัพโหลดอวาตาร์\",\n    \"searchInCategory\": \"ค้นหาใน {category}\",\n    \"label\": \"เทมเพลต\"\n  },\n  \"fileDropzone\": {\n    \"dropFile\": \"คลิก หรือลากไฟล์ ไปยังพื้นที่นี้เพื่ออัพโหลด\",\n    \"uploading\": \"กำลังอัพโหลด...\",\n    \"uploadFailed\": \"อัพโหลดไม่สำเร็จ\",\n    \"uploadSuccess\": \"อัพโหลดสำเร็จ\",\n    \"uploadSuccessDescription\": \"ไฟล์ได้รับการอัพโหลดเรียบร้อยแล้ว\",\n    \"uploadFailedDescription\": \"อัพโหลดไฟล์ไม่สำเร็จ\",\n    \"uploadingDescription\": \"ไฟล์กำลังถูกอัพโหลด\"\n  },\n  \"gallery\": {\n    \"preview\": \"เปิดแบบเต็มจอ\",\n    \"copy\": \"สำเนา\",\n    \"download\": \"ดาวน์โหลด\",\n    \"prev\": \"ก่อนหน้า\",\n    \"next\": \"ถัดไป\",\n    \"resetZoom\": \"รีเซ็ตการซูม\",\n    \"zoomIn\": \"ซูมเข้า\",\n    \"zoomOut\": \"ซูมออก\"\n  },\n  \"invitation\": {\n    \"join\": \"เข้าร่วม\",\n    \"on\": \"บน\",\n    \"invitedBy\": \"ได้รับเชิญโดย\",\n    \"membersCount\": {\n      \"zero\": \"{count} สมาชิก\",\n      \"one\": \"{count} สมาชิก\",\n      \"many\": \"{count} สมาชิก\",\n      \"other\": \"{count} สมาชิก\"\n    },\n    \"tip\": \"คุณได้รับคำเชิญให้เข้าร่วมพื้นที่ทำงานนี้โดยใช้ข้อมูลติดต่อด้านล่าง หากข้อมูลนี้ไม่ถูกต้อง กรุณาติดต่อผู้ดูแลเพื่อขอให้ส่งคำเชิญใหม่\",\n    \"joinWorkspace\": \"เข้าร่วมพื้นที่ทำงาน\",\n    \"success\": \"คุณได้เข้าร่วมพื้นที่ทำงานเรียบร้อยแล้ว\",\n    \"successMessage\": \"ตอนนี้คุณสามารถเข้าถึงหน้า และพื้นที่ทำงานทั้งหมดภายในนั้นได้แล้ว\",\n    \"openWorkspace\": \"เปิด AppFlowy\",\n    \"alreadyAccepted\": \"คุณได้ยอมรับคำเชิญแล้ว\",\n    \"errorModal\": {\n      \"title\": \"มีบางอย่างผิดพลาด\",\n      \"description\": \"บัญชีปัจจุบันของคุณ {email} อาจไม่มีสิทธิ์เข้าถึงพื้นที่ทำงานนี้ กรุณาล็อกอินด้วยบัญชีที่ถูกต้องหรือ ติดต่อเจ้าของพื้นที่ทำงานเพื่อขอความช่วยเหลือ\",\n      \"contactOwner\": \"ติดต่อเจ้าของ\",\n      \"close\": \"กลับสู่หน้าแรก\",\n      \"changeAccount\": \"เปลี่ยนบัญชี\"\n    }\n  },\n  \"requestAccess\": {\n    \"title\": \"ไม่มีการเข้าถึงหน้านี้\",\n    \"subtitle\": \"คุณสามารถขอสิทธิ์การเข้าถึงจากเจ้าขอหน้านี้ได้ เมื่อได้รับการอนุมัติแล้วคุณจะสามารถดูหน้าได้\",\n    \"requestAccess\": \"ขอสิทธิ์การเข้าถึง\",\n    \"backToHome\": \"กลับสู่หน้าแรก\",\n    \"tip\": \"ขณะนี้คุณเข้าสู่ระบบเป็น <link/>\",\n    \"mightBe\": \"คุณอาจต้อง <login/> ด้วยบัญชีอื่น\",\n    \"successful\": \"ส่งคำขอเรียบร้อยแล้ว\",\n    \"successfulMessage\": \"คุณจะได้รับการแจ้งเตือนเมื่อเจ้าของอนุมัติคำขอของคุณ\",\n    \"requestError\": \"ไม่สามารถขอการเข้าถึงได้\",\n    \"repeatRequestError\": \"คุณได้ขอการเข้าถึงหน้านี้แล้ว\"\n  },\n  \"approveAccess\": {\n    \"title\": \"อนุมัติคำขอเข้าร่วมพื้นที่ทำงาน\",\n    \"requestSummary\": \"<user/> ขอเข้าร่วม <workspace/> และเข้าถึง <page/>\",\n    \"upgrade\": \"อัพเกรด\",\n    \"downloadApp\": \"ดาวน์โหลด AppFlowy\",\n    \"approveButton\": \"อนุมัติ\",\n    \"approveSuccess\": \"ได้รับการอนุมัติเรียบร้อยแล้ว\",\n    \"approveError\": \"ไม่สามารถอนุมัติได้ โปรดตรวจสอบให้แน่ใจว่าไม่เกินขีดจำกัดของแผนพื้นที่ทำงาน\",\n    \"getRequestInfoError\": \"ไม่สามารถรับข้อมูลคำขอได้\",\n    \"memberCount\": {\n      \"zero\": \"ไม่มีสมาชิก\",\n      \"one\": \"1 สมาชิก\",\n      \"many\": \"{count} สมาชิก\",\n      \"other\": \"{count} สมาชิก\"\n    },\n    \"alreadyProTitle\": \"คุณได้ถึงขีดจำกัดของแผนพื้นที่ทำงานแล้ว\",\n    \"alreadyProMessage\": \"ขอให้พวกเขาติดต่อ <email/> เพื่อปลดล็อกสมาชิกเพิ่มเติม\",\n    \"repeatApproveError\": \"คุณได้อนุมัติคำขอนี้แล้ว\",\n    \"ensurePlanLimit\": \"โปรดตรวจสอบให้แน่ใจว่าไม่เกินขีดจำกัดของแผนพื้นที่ทำงาน หากเกินขีดจำกัดแล้ว ให้พิจารณา <upgrade/> แผนพื้นที่ทำงานหรือ <download/>\",\n    \"requestToJoin\": \"ขอเข้าร่วม\",\n    \"asMember\": \"ในฐานะสมาชิก\"\n  },\n  \"upgradePlanModal\": {\n    \"title\": \"อัพเกรดเป็น Pro\",\n    \"message\": \"{name} ได้ถึงขีดจำกัดของสมาชิกฟรีแล้ว อัปเกรดเป็นแผน Pro เพื่อเชิญสมาชิกเพิ่ม\",\n    \"upgradeSteps\": \"วิธีอัพเกรดแผนของคุณบน AppFlowy:\",\n    \"step1\": \"1. ไปที่การตั้งค่า\",\n    \"step2\": \"2. คลิกที่ 'แผน'\",\n    \"step3\": \"3. เลือก 'เปลี่ยนแผน'\",\n    \"appNote\": \"บันทึก: \",\n    \"actionButton\": \"อัพเกรด\",\n    \"downloadLink\": \"ดาวน์โหลดแอป\",\n    \"laterButton\": \"ภายหลัง\",\n    \"refreshNote\": \"หลังจากอัปเกรดสำเร็จแล้ว คลิก <refresh/> เพื่อเปิดใช้งานฟีเจอร์ใหม่ของคุณ\",\n    \"refresh\": \"ที่นี่\"\n  },\n  \"breadcrumbs\": {\n    \"label\": \"เส้นทางนำทาง\"\n  },\n  \"time\": {\n    \"justNow\": \"เมื่อสักครู่\",\n    \"seconds\": {\n      \"one\": \"1 วินาที\",\n      \"other\": \"{count} วินาที\"\n    },\n    \"minutes\": {\n      \"one\": \"1 นาที\",\n      \"other\": \"{count} นาที\"\n    },\n    \"hours\": {\n      \"one\": \"1 ชั่วโมง\",\n      \"other\": \"{count} ชั่วโมง\"\n    },\n    \"days\": {\n      \"one\": \"1 วัน\",\n      \"other\": \"{จำนวน} วัน\"\n    },\n    \"weeks\": {\n      \"one\": \"1 สัปดาห์\",\n      \"other\": \"{count} สัปดาห์\"\n    },\n    \"months\": {\n      \"one\": \"1 เดือน\",\n      \"other\": \"{count} เดือน\"\n    },\n    \"years\": {\n      \"one\": \"1 ปี\",\n      \"other\": \"{count} ปี\"\n    },\n    \"ago\": \"ที่ผ่านมา\",\n    \"yesterday\": \"เมื่อวาน\",\n    \"today\": \"วันนี้\"\n  },\n  \"members\": {\n    \"zero\": \"ไม่มีสมาชิก\",\n    \"one\": \"1 สมาชิก\",\n    \"many\": \"{count} สมาชิก\",\n    \"other\": \"{count} สมาชิก\"\n  },\n  \"tabMenu\": {\n    \"close\": \"ปิด\",\n    \"closeOthers\": \"ปิดแท็บอื่น ๆ\",\n    \"favorite\": \"รายการโปรด\",\n    \"unfavorite\": \"ยกเลิกรายการโปรด\",\n    \"favoriteDisabledHint\": \"ไม่สามารถเพิ่มมุมมองนี้เป็นรายการโปรดได้\",\n    \"pinTab\": \"ปักหมุด\",\n    \"unpinTab\": \"ยกเลิกการปักหมุด\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/tr-TR.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Kullanıcı\",\n  \"welcomeText\": \"@:appName'ye Hoş Geldiniz\",\n  \"welcomeTo\": \"Hoş Geldiniz\",\n  \"githubStarText\": \"GitHub'da Yıldız Ver\",\n  \"subscribeNewsletterText\": \"Bültenimize Abone Ol\",\n  \"letsGoButtonText\": \"Hemen Başla\",\n  \"title\": \"Başlık\",\n  \"youCanAlso\": \"Ayrıca\",\n  \"and\": \"ve\",\n  \"failedToOpenUrl\": \"URL açılamadı: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Altına eklemek için tıklayın\",\n    \"addAboveCmd\": \"Alt+tıklama\",\n    \"addAboveMacCmd\": \"Option+tıklama\",\n    \"addAboveTooltip\": \"Üstüne eklemek için\",\n    \"dragTooltip\": \"Sürükleyerek taşıyın\",\n    \"openMenuTooltip\": \"Menüyü aç\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Kayıt Ol\",\n    \"title\": \"@:appName'e Kayıt Ol\",\n    \"getStartedText\": \"Başlayın\",\n    \"emptyPasswordError\": \"Parola boş olamaz\",\n    \"repeatPasswordEmptyError\": \"Parola tekrarı alanı boş olamaz\",\n    \"unmatchedPasswordError\": \"Parola tekrarı, parola ile aynı değil\",\n    \"alreadyHaveAnAccount\": \"Hesabınız zaten var mı?\",\n    \"emailHint\": \"E-posta adresi\",\n    \"passwordHint\": \"Parola\",\n    \"repeatPasswordHint\": \"Parolayı tekrarla\",\n    \"signUpWith\": \"Kayıt ol:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"@:appName'e Oturum Aç\",\n    \"loginButtonText\": \"Oturum Aç\",\n    \"loginStartWithAnonymous\": \"Anonim oturumla başla\",\n    \"continueAnonymousUser\": \"Anonim oturumla devam et\",\n    \"buttonText\": \"Oturum Aç\",\n    \"signingInText\": \"Oturum açılıyor...\",\n    \"forgotPassword\": \"Parolamı Unuttum?\",\n    \"emailHint\": \"E-posta adresi\",\n    \"passwordHint\": \"Parola\",\n    \"dontHaveAnAccount\": \"Hesabınız yok mu?\",\n    \"createAccount\": \"Hesap oluştur\",\n    \"repeatPasswordEmptyError\": \"Parola tekrarı alanı boş bırakılamaz\",\n    \"unmatchedPasswordError\": \"Parola tekrarı parolayla eşleşmiyor\",\n    \"syncPromptMessage\": \"Verilerin senkronize edilmesi biraz zaman alabilir. Lütfen bu sayfayı kapatmayın\",\n    \"or\": \"VEYA\",\n    \"signInWithGoogle\": \"Google ile devam et\",\n    \"signInWithGithub\": \"GitHub ile devam et\",\n    \"signInWithDiscord\": \"Discord ile devam et\",\n    \"signInWithApple\": \"Apple ile devam et\",\n    \"continueAnotherWay\": \"Başka yöntemle devam et\",\n    \"signUpWithGoogle\": \"Google ile kaydol\",\n    \"signUpWithGithub\": \"GitHub ile kaydol\",\n    \"signUpWithDiscord\": \"Discord ile kaydol\",\n    \"signInWith\": \"Devam et:\",\n    \"signInWithEmail\": \"E-posta ile devam et\",\n    \"signInWithMagicLink\": \"Devam et\",\n    \"signUpWithMagicLink\": \"Sihirli Bağlantı ile kaydol\",\n    \"pleaseInputYourEmail\": \"Lütfen e-posta adresinizi girin\",\n    \"settings\": \"Ayarlar\",\n    \"magicLinkSent\": \"Sihirli Bağlantı gönderildi!\",\n    \"invalidEmail\": \"Geçerli bir e-posta adresi girin\",\n    \"alreadyHaveAnAccount\": \"Zaten hesabınız var mı?\",\n    \"logIn\": \"Oturum Aç\",\n    \"generalError\": \"Bir hata oluştu. Lütfen daha sonra tekrar deneyin.\",\n    \"limitRateError\": \"Güvenlik önlemi olarak, sihirli bağlantı talepleri 60 saniyede bir ile sınırlandırılmıştır.\",\n    \"magicLinkSentDescription\": \"E-posta adresinize sihirli bir bağlantı gönderdik. Giriş yapmak için bu bağlantıya tıklayın. Bağlantı 5 dakika içinde geçersiz hale gelecektir.\",\n    \"anonymous\": \"Anonim\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Çalışma alanınızı seçin\",\n    \"defaultName\": \"Çalışma Alanım\",\n    \"create\": \"Çalışma alanı oluştur\",\n    \"new\": \"Yeni çalışma alanı\",\n    \"importFromNotion\": \"Notion'dan içe aktar\",\n    \"learnMore\": \"Daha fazla bilgi\",\n    \"reset\": \"Çalışma alanını sıfırla\",\n    \"renameWorkspace\": \"Çalışma alanını yeniden adlandır\",\n    \"workspaceNameCannotBeEmpty\": \"Çalışma alanı adı boş olamaz\",\n    \"resetWorkspacePrompt\": \"Çalışma alanını sıfırlamak tüm sayfaları ve verileri silecektir. Çalışma alanını sıfırlamak istediğinizden emin misiniz? Geri yüklemek için destek ekibine ulaşabilirsiniz.\",\n    \"hint\": \"çalışma alanı\",\n    \"notFoundError\": \"Çalışma alanı bulunamadı\",\n    \"failedToLoad\": \"Bir şeyler yanlış gitti! Çalışma alanı yüklenemedi. @:appName'in açık olan tüm örneklerini kapatıp tekrar deneyin.\",\n    \"errorActions\": {\n      \"reportIssue\": \"Hata bildir\",\n      \"reportIssueOnGithub\": \"GitHub'da hata bildir\",\n      \"exportLogFiles\": \"Günlük dosyalarını dışa aktar\",\n      \"reachOut\": \"Discord'da iletişime geç\"\n    },\n    \"menuTitle\": \"Çalışma Alanları\",\n    \"deleteWorkspaceHintText\": \"Çalışma alanını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz ve yayınladığınız tüm sayfaların yayını kaldırılacaktır.\",\n    \"createSuccess\": \"Çalışma alanı başarıyla oluşturuldu\",\n    \"createFailed\": \"Çalışma alanı oluşturulamadı\",\n    \"createLimitExceeded\": \"Hesabınız için izin verilen maksimum çalışma alanı limitine ulaştınız. Çalışmanıza devam etmek için ek çalışma alanlarına ihtiyacınız varsa, lütfen GitHub'da talepte bulunun\",\n    \"deleteSuccess\": \"Çalışma alanı başarıyla silindi\",\n    \"deleteFailed\": \"Çalışma alanı silinemedi\",\n    \"openSuccess\": \"Çalışma alanı başarıyla açıldı\",\n    \"openFailed\": \"Çalışma alanı açılamadı\",\n    \"renameSuccess\": \"Çalışma alanı başarıyla yeniden adlandırıldı\",\n    \"renameFailed\": \"Çalışma alanı yeniden adlandırılamadı\",\n    \"updateIconSuccess\": \"Çalışma alanı simgesi başarıyla güncellendi\",\n    \"updateIconFailed\": \"Çalışma alanı simgesi güncellenemedi\",\n    \"cannotDeleteTheOnlyWorkspace\": \"Tek çalışma alanı silinemez\",\n    \"fetchWorkspacesFailed\": \"Çalışma alanları getirilemedi\",\n    \"leaveCurrentWorkspace\": \"Çalışma alanından ayrıl\",\n    \"leaveCurrentWorkspacePrompt\": \"Mevcut çalışma alanından ayrılmak istediğinizden emin misiniz?\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Paylaş\",\n    \"workInProgress\": \"Yakında\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"Panoya kopyala\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Bağlantıyı kopyala\",\n    \"publishToTheWeb\": \"Web'de Yayınla\",\n    \"publishToTheWebHint\": \"AppFlowy ile bir web sitesi oluşturun\",\n    \"publish\": \"Yayınla\",\n    \"unPublish\": \"Yayından kaldır\",\n    \"visitSite\": \"Siteyi ziyaret et\",\n    \"exportAsTab\": \"Farklı dışa aktar\",\n    \"publishTab\": \"Yayınla\",\n    \"shareTab\": \"Paylaş\",\n    \"publishOnAppFlowy\": \"AppFlowy'de Yayınla\",\n    \"shareTabTitle\": \"İşbirliği için davet et\",\n    \"shareTabDescription\": \"Herhangi biriyle kolay işbirliği için\",\n    \"copyLinkSuccess\": \"Bağlantı panoya kopyalandı\",\n    \"copyShareLink\": \"Paylaşım bağlantısını kopyala\",\n    \"copyLinkFailed\": \"Bağlantı panoya kopyalanamadı\",\n    \"copyLinkToBlockSuccess\": \"Blok bağlantısı panoya kopyalandı\",\n    \"copyLinkToBlockFailed\": \"Blok bağlantısı panoya kopyalanamadı\",\n    \"manageAllSites\": \"Tüm siteleri yönet\",\n    \"updatePathName\": \"Yol adını güncelle\"\n  },\n  \"moreAction\": {\n    \"small\": \"küçük\",\n    \"medium\": \"orta\",\n    \"large\": \"büyük\",\n    \"fontSize\": \"Yazı tipi boyutu\",\n    \"import\": \"İçe aktar\",\n    \"moreOptions\": \"Daha fazla seçenek\",\n    \"wordCount\": \"Kelime sayısı: {}\",\n    \"charCount\": \"Karakter sayısı: {}\",\n    \"createdAt\": \"Oluşturulma: {}\",\n    \"deleteView\": \"Sil\",\n    \"duplicateView\": \"Çoğalt\",\n    \"wordCountLabel\": \"Kelime sayısı: \",\n    \"charCountLabel\": \"Karakter sayısı: \",\n    \"createdAtLabel\": \"Oluşturulma: \",\n    \"syncedAtLabel\": \"Senkronize edilme: \",\n    \"saveAsNewPage\": \"Mesajları sayfaya ekle\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Metin ve Markdown\",\n    \"documentFromV010\": \"v0.1.0'dan belge\",\n    \"databaseFromV010\": \"v0.1.0'dan veritabanı\",\n    \"notionZip\": \"Notion Dışa Aktarılmış Zip Dosyası\",\n    \"csv\": \"CSV\",\n    \"database\": \"Veritabanı\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Yeniden adlandır\",\n    \"delete\": \"Sil\",\n    \"duplicate\": \"Çoğalt\",\n    \"unfavorite\": \"Favorilerden kaldır\",\n    \"favorite\": \"Favorilere ekle\",\n    \"openNewTab\": \"Yeni sekmede aç\",\n    \"moveTo\": \"Şuraya taşı\",\n    \"addToFavorites\": \"Favorilere ekle\",\n    \"copyLink\": \"Bağlantıyı kopyala\",\n    \"changeIcon\": \"Simgeyi değiştir\",\n    \"collapseAllPages\": \"Tüm alt sayfaları daralt\",\n    \"movePageTo\": \"Sayfayı şuraya taşı\",\n    \"move\": \"Taşı\"\n  },\n  \"blankPageTitle\": \"Boş sayfa\",\n  \"newPageText\": \"Yeni sayfa\",\n  \"newDocumentText\": \"Yeni belge\",\n  \"newGridText\": \"Yeni ızgara\",\n  \"newCalendarText\": \"Yeni takvim\",\n  \"newBoardText\": \"Yeni pano\",\n  \"chat\": {\n    \"newChat\": \"Yapay Zeka Sohbeti\",\n    \"inputMessageHint\": \"@:appName Yapay Zekasına sorun\",\n    \"inputLocalAIMessageHint\": \"@:appName Yerel Yapay Zekasına sorun\",\n    \"unsupportedCloudPrompt\": \"Bu özellik yalnızca @:appName Cloud kullanırken kullanılabilir\",\n    \"relatedQuestion\": \"Önerilen\",\n    \"serverUnavailable\": \"Bağlantı kesildi. Lütfen internet bağlantınızı kontrol edin ve\",\n    \"aiServerUnavailable\": \"Yapay zeka hizmeti geçici olarak kullanılamıyor. Lütfen daha sonra tekrar deneyin.\",\n    \"retry\": \"Tekrar dene\",\n    \"clickToRetry\": \"Tekrar denemek için tıklayın\",\n    \"regenerateAnswer\": \"Yeniden oluştur\",\n    \"question1\": \"Görevleri yönetmek için Kanban nasıl kullanılır\",\n    \"question2\": \"GTD yöntemini açıkla\",\n    \"question3\": \"Neden Rust kullanmalı\",\n    \"question4\": \"Mutfağımdaki malzemelerle tarif\",\n    \"question5\": \"Sayfam için bir illüstrasyon oluştur\",\n    \"question6\": \"Önümüzdeki hafta için yapılacaklar listesi hazırla\",\n    \"aiMistakePrompt\": \"Yapay zeka hata yapabilir. Önemli bilgileri kontrol edin.\",\n    \"chatWithFilePrompt\": \"Dosya ile sohbet etmek ister misiniz?\",\n    \"indexFileSuccess\": \"Dosya başarıyla indekslendi\",\n    \"inputActionNoPages\": \"Sayfa sonucu yok\",\n    \"referenceSource\": {\n      \"zero\": \"0 kaynak bulundu\",\n      \"one\": \"{count} kaynak bulundu\",\n      \"other\": \"{count} kaynak bulundu\"\n    },\n    \"clickToMention\": \"Bir sayfadan bahset\",\n    \"uploadFile\": \"PDF, metin veya markdown dosyaları ekle\",\n    \"questionDetail\": \"Merhaba {}! Size bugün nasıl yardımcı olabilirim?\",\n    \"indexingFile\": \"{} indeksleniyor\",\n    \"generatingResponse\": \"Yanıt oluşturuluyor\",\n    \"selectSources\": \"Kaynakları Seç\",\n    \"sourcesLimitReached\": \"En fazla 3 üst düzey belge ve alt öğelerini seçebilirsiniz\",\n    \"sourceUnsupported\": \"Şu anda veritabanlarıyla sohbet etmeyi desteklemiyoruz\",\n    \"regenerate\": \"Tekrar dene\",\n    \"addToPageButton\": \"Mesajı sayfaya ekle\",\n    \"addToPageTitle\": \"Mesajı şuraya ekle...\",\n    \"addToNewPage\": \"Yeni sayfa oluştur\",\n    \"addToNewPageName\": \"\\\"{}\\\" kaynağından çıkarılan mesajlar\",\n    \"addToNewPageSuccessToast\": \"Mesaj şuraya eklendi:\",\n    \"openPagePreviewFailedToast\": \"Sayfa açılamadı\",\n    \"changeFormat\": {\n      \"actionButton\": \"Biçimi değiştir\",\n      \"confirmButton\": \"Bu biçimle yeniden oluştur\",\n      \"textOnly\": \"Metin\",\n      \"imageOnly\": \"Sadece görsel\",\n      \"textAndImage\": \"Metin ve Görsel\",\n      \"text\": \"Paragraf\",\n      \"bullet\": \"Madde işaretli liste\",\n      \"number\": \"Numaralı liste\",\n      \"table\": \"Tablo\",\n      \"blankDescription\": \"Yanıt biçimi\",\n      \"defaultDescription\": \"Otomatik mod\",\n      \"textWithImageDescription\": \"@:chat.changeFormat.text ve görsel\",\n      \"numberWithImageDescription\": \"@:chat.changeFormat.number ve görsel\",\n      \"bulletWithImageDescription\": \"@:chat.changeFormat.bullet ve görsel\",\n      \"tableWithImageDescription\": \"@:chat.changeFormat.table ve görsel\"\n    },\n    \"selectBanner\": {\n      \"saveButton\": \"Şuraya ekle …\",\n      \"selectMessages\": \"Mesajları seç\",\n      \"nSelected\": \"{} seçildi\",\n      \"allSelected\": \"Tümü seçildi\"\n    }\n  },\n  \"trash\": {\n    \"text\": \"Çöp Kutusu\",\n    \"restoreAll\": \"Tümünü Geri Yükle\",\n    \"restore\": \"Geri Yükle\",\n    \"deleteAll\": \"Tümünü Sil\",\n    \"pageHeader\": {\n      \"fileName\": \"Dosya adı\",\n      \"lastModified\": \"Son Değiştirilme\",\n      \"created\": \"Oluşturulma\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Çöp kutusundaki tüm sayfalar\",\n      \"caption\": \"Çöp kutusundaki her şeyi silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Çöp kutusundaki tüm sayfaları geri yükle\",\n      \"caption\": \"Bu işlem geri alınamaz.\"\n    },\n    \"restorePage\": {\n      \"title\": \"Geri Yükle: {}\",\n      \"caption\": \"Bu sayfayı geri yüklemek istediğinizden emin misiniz?\"\n    },\n    \"mobile\": {\n      \"actions\": \"Çöp Kutusu İşlemleri\",\n      \"empty\": \"Çöp kutusunda sayfa veya alan yok\",\n      \"emptyDescription\": \"İhtiyacınız olmayan şeyleri Çöp Kutusuna taşıyın.\",\n      \"isDeleted\": \"silindi\",\n      \"isRestored\": \"geri yüklendi\"\n    },\n    \"confirmDeleteTitle\": \"Bu sayfayı kalıcı olarak silmek istediğinizden emin misiniz?\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Bu sayfa Çöp Kutusunda\",\n    \"restore\": \"Sayfayı geri yükle\",\n    \"deletePermanent\": \"Kalıcı olarak sil\",\n    \"deletePermanentDescription\": \"Bu sayfayı kalıcı olarak silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.\"\n  },\n  \"dialogCreatePageNameHint\": \"Sayfa adı\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Kısayollar\",\n    \"whatsNew\": \"Yenilikler\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"Hata Ayıklama Bilgisi\",\n      \"success\": \"Hata ayıklama bilgisi panoya kopyalandı!\",\n      \"fail\": \"Hata ayıklama bilgisi panoya kopyalanamadı\"\n    },\n    \"feedback\": \"Geri Bildirim\",\n    \"help\": \"Yardım ve Destek\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Kaldır, yeniden adlandır ve daha fazlası...\",\n    \"addPageTooltip\": \"Hızlıca içeri sayfa ekle\",\n    \"defaultNewPageName\": \"Başlıksız\",\n    \"renameDialog\": \"Yeniden adlandır\",\n    \"pageNameSuffix\": \"Kopya\"\n  },\n  \"noPagesInside\": \"İçeride sayfa yok\",\n  \"toolbar\": {\n    \"undo\": \"Geri al\",\n    \"redo\": \"Yinele\",\n    \"bold\": \"Kalın\",\n    \"italic\": \"İtalik\",\n    \"underline\": \"Altı çizili\",\n    \"strike\": \"Üstü çizili\",\n    \"numList\": \"Numaralı liste\",\n    \"bulletList\": \"Madde işaretli liste\",\n    \"checkList\": \"Kontrol Listesi\",\n    \"inlineCode\": \"Satır İçi Kod\",\n    \"quote\": \"Alıntı Bloğu\",\n    \"header\": \"Başlık\",\n    \"highlight\": \"Vurgula\",\n    \"color\": \"Renk\",\n    \"addLink\": \"Bağlantı Ekle\",\n    \"link\": \"Bağlantı\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Aydınlık moda geç\",\n    \"darkMode\": \"Karanlık moda geç\",\n    \"openAsPage\": \"Sayfa olarak aç\",\n    \"addNewRow\": \"Yeni satır ekle\",\n    \"openMenu\": \"Menüyü açmak için tıklayın\",\n    \"dragRow\": \"Satırı yeniden sıralamak için sürükleyin\",\n    \"viewDataBase\": \"Veritabanını görüntüle\",\n    \"referencePage\": \"Bu {name} referans alındı\",\n    \"addBlockBelow\": \"Alta blok ekle\",\n    \"aiGenerate\": \"Oluştur\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Kenar çubuğunu kapat\",\n    \"openSidebar\": \"Kenar çubuğunu aç\",\n    \"expandSidebar\": \"Tam sayfa olarak genişlet\",\n    \"personal\": \"Kişisel\",\n    \"private\": \"Özel\",\n    \"workspace\": \"Çalışma Alanı\",\n    \"favorites\": \"Favoriler\",\n    \"clickToHidePrivate\": \"Özel alanı gizlemek için tıklayın\\nBurada oluşturduğunuz sayfalar yalnızca size görünür\",\n    \"clickToHideWorkspace\": \"Çalışma alanını gizlemek için tıklayın\\nBurada oluşturduğunuz sayfalar tüm üyelere görünür\",\n    \"clickToHidePersonal\": \"Kişisel alanı gizlemek için tıklayın\",\n    \"clickToHideFavorites\": \"Favoriler alanını gizlemek için tıklayın\",\n    \"addAPage\": \"Yeni sayfa ekle\",\n    \"addAPageToPrivate\": \"Özel alana sayfa ekle\",\n    \"addAPageToWorkspace\": \"Çalışma alanına sayfa ekle\",\n    \"recent\": \"Son\",\n    \"today\": \"Bugün\",\n    \"thisWeek\": \"Bu hafta\",\n    \"others\": \"Önceki favoriler\",\n    \"earlier\": \"Daha önce\",\n    \"justNow\": \"az önce\",\n    \"minutesAgo\": \"{count} dakika önce\",\n    \"lastViewed\": \"Son görüntüleme\",\n    \"favoriteAt\": \"Favorilere eklendi\",\n    \"emptyRecent\": \"Son Sayfa Yok\",\n    \"emptyRecentDescription\": \"Sayfaları görüntüledikçe, kolay erişim için burada listelenecekler.\",\n    \"emptyFavorite\": \"Favori Sayfa Yok\",\n    \"emptyFavoriteDescription\": \"Sayfaları favori olarak işaretleyin—hızlı erişim için burada listelenecekler!\",\n    \"removePageFromRecent\": \"Bu sayfayı Son'dan kaldır?\",\n    \"removeSuccess\": \"Başarıyla kaldırıldı\",\n    \"favoriteSpace\": \"Favoriler\",\n    \"RecentSpace\": \"Son\",\n    \"Spaces\": \"Alanlar\",\n    \"upgradeToPro\": \"Pro'ya yükselt\",\n    \"upgradeToAIMax\": \"Sınırsız yapay zekayı aç\",\n    \"storageLimitDialogTitle\": \"Ücretsiz depolama alanınız bitti. Sınırsız depolama için yükseltin\",\n    \"storageLimitDialogTitleIOS\": \"Ücretsiz depolama alanınız bitti.\",\n    \"aiResponseLimitTitle\": \"Ücretsiz yapay zeka yanıtlarınız bitti. Sınırsız yanıt için Pro Plana yükseltin veya bir yapay zeka eklentisi satın alın\",\n    \"aiResponseLimitDialogTitle\": \"Yapay zeka yanıt limitine ulaşıldı\",\n    \"aiResponseLimit\": \"Ücretsiz yapay zeka yanıtlarınız bitti.\\n\\nDaha fazla yapay zeka yanıtı almak için Ayarlar -> Plan -> AI Max veya Pro Plan'a tıklayın\",\n    \"askOwnerToUpgradeToPro\": \"Çalışma alanınızın ücretsiz depolama alanı bitiyor. Lütfen çalışma alanı sahibinden Pro Plana yükseltmesini isteyin\",\n    \"askOwnerToUpgradeToProIOS\": \"Çalışma alanınızın ücretsiz depolama alanı bitiyor.\",\n    \"askOwnerToUpgradeToAIMax\": \"Çalışma alanınızın ücretsiz yapay zeka yanıtları bitti. Lütfen çalışma alanı sahibinden planı yükseltmesini veya yapay zeka eklentileri satın almasını isteyin\",\n    \"askOwnerToUpgradeToAIMaxIOS\": \"Çalışma alanınızın ücretsiz yapay zeka yanıtları bitiyor.\",\n    \"purchaseAIMax\": \"Çalışma alanınızın yapay zeka görsel yanıtları bitti. Lütfen çalışma alanı sahibinden AI Max satın almasını isteyin\",\n    \"aiImageResponseLimit\": \"Yapay zeka görsel yanıtlarınız bitti.\\n\\nDaha fazla yapay zeka görsel yanıtı almak için Ayarlar -> Plan -> AI Max'a tıklayın\",\n    \"purchaseStorageSpace\": \"Depolama Alanı Satın Al\",\n    \"singleFileProPlanLimitationDescription\": \"Ücretsiz planda izin verilen maksimum dosya yükleme boyutunu aştınız. Daha büyük dosyalar yüklemek için lütfen Pro Plana yükseltin\",\n    \"purchaseAIResponse\": \"Satın Al \",\n    \"askOwnerToUpgradeToLocalAI\": \"Çalışma alanı sahibinden Cihaz Üzerinde Yapay Zekayı etkinleştirmesini isteyin\",\n    \"upgradeToAILocal\": \"En üst düzey gizlilik için yerel modelleri cihazınızda çalıştırın\",\n    \"upgradeToAILocalDesc\": \"Yerel yapay zeka kullanarak PDF'lerle sohbet edin, yazılarınızı geliştirin ve tabloları otomatik doldurun\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Not Markdown Olarak Dışa Aktarıldı\",\n      \"path\": \"Documents/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Kişiler\",\n    \"whatsHappening\": \"Bu hafta neler oluyor?\",\n    \"addContact\": \"Kişi Ekle\",\n    \"editContact\": \"Kişiyi Düzenle\"\n  },\n  \"button\": {\n    \"ok\": \"Tamam\",\n    \"confirm\": \"Onayla\",\n    \"done\": \"Bitti\",\n    \"cancel\": \"İptal\",\n    \"signIn\": \"Giriş Yap\",\n    \"signOut\": \"Çıkış Yap\",\n    \"complete\": \"Tamamla\",\n    \"save\": \"Kaydet\",\n    \"generate\": \"Oluştur\",\n    \"esc\": \"ESC\",\n    \"keep\": \"Sakla\",\n    \"tryAgain\": \"Tekrar dene\",\n    \"discard\": \"Vazgeç\",\n    \"replace\": \"Değiştir\",\n    \"insertBelow\": \"Alta ekle\",\n    \"insertAbove\": \"Üste ekle\",\n    \"upload\": \"Yükle\",\n    \"edit\": \"Düzenle\",\n    \"delete\": \"Sil\",\n    \"copy\": \"Kopyala\",\n    \"duplicate\": \"Çoğalt\",\n    \"putback\": \"Geri Koy\",\n    \"update\": \"Güncelle\",\n    \"share\": \"Paylaş\",\n    \"removeFromFavorites\": \"Favorilerden kaldır\",\n    \"removeFromRecent\": \"Son'dan kaldır\",\n    \"addToFavorites\": \"Favorilere ekle\",\n    \"favoriteSuccessfully\": \"Favorilere eklendi\",\n    \"unfavoriteSuccessfully\": \"Favorilerden kaldırıldı\",\n    \"duplicateSuccessfully\": \"Başarıyla çoğaltıldı\",\n    \"rename\": \"Yeniden adlandır\",\n    \"helpCenter\": \"Yardım Merkezi\",\n    \"add\": \"Ekle\",\n    \"yes\": \"Evet\",\n    \"no\": \"Hayır\",\n    \"clear\": \"Temizle\",\n    \"remove\": \"Kaldır\",\n    \"dontRemove\": \"Kaldırma\",\n    \"copyLink\": \"Bağlantıyı Kopyala\",\n    \"align\": \"Hizala\",\n    \"login\": \"Giriş yap\",\n    \"logout\": \"Çıkış yap\",\n    \"deleteAccount\": \"Hesabı sil\",\n    \"back\": \"Geri\",\n    \"signInGoogle\": \"Google ile devam et\",\n    \"signInGithub\": \"GitHub ile devam et\",\n    \"signInDiscord\": \"Discord ile devam et\",\n    \"more\": \"Daha fazla\",\n    \"create\": \"Oluştur\",\n    \"close\": \"Kapat\",\n    \"next\": \"İleri\",\n    \"previous\": \"Geri\",\n    \"submit\": \"Gönder\",\n    \"download\": \"İndir\",\n    \"backToHome\": \"Ana Sayfaya Dön\",\n    \"viewing\": \"Görüntüleme\",\n    \"editing\": \"Düzenleme\",\n    \"gotIt\": \"Anladım\",\n    \"retry\": \"Tekrar dene\",\n    \"uploadFailed\": \"Yükleme başarısız.\",\n    \"copyLinkOriginal\": \"Orijinal bağlantıyı kopyala\"\n  },\n  \"label\": {\n    \"welcome\": \"Hoş Geldiniz!\",\n    \"firstName\": \"Ad\",\n    \"middleName\": \"İkinci Ad\",\n    \"lastName\": \"Soyad\",\n    \"stepX\": \"Adım {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Hesabınıza bağlanılamıyor.\",\n      \"failedMsg\": \"Lütfen tarayıcınızda oturum açma işlemini tamamladığınızdan emin olun.\"\n    },\n    \"google\": {\n      \"title\": \"GOOGLE İLE GİRİŞ\",\n      \"instruction1\": \"Google Kişilerinizi içe aktarmak için, bu uygulamaya web tarayıcınızı kullanarak yetki vermeniz gerekecek.\",\n      \"instruction2\": \"Simgeye tıklayarak veya metni seçerek bu kodu panonuza kopyalayın:\",\n      \"instruction3\": \"Web tarayıcınızda aşağıdaki bağlantıya gidin ve yukarıdaki kodu girin:\",\n      \"instruction4\": \"Kaydı tamamladığınızda aşağıdaki düğmeye basın:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Ayarlar\",\n    \"popupMenuItem\": {\n      \"settings\": \"Ayarlar\",\n      \"members\": \"Üyeler\",\n      \"trash\": \"Çöp Kutusu\",\n      \"helpAndSupport\": \"Yardım ve Destek\"\n    },\n    \"sites\": {\n      \"title\": \"Siteler\",\n      \"namespaceTitle\": \"Alan Adı\",\n      \"namespaceDescription\": \"Alan adınızı ve ana sayfanızı yönetin\",\n      \"namespaceHeader\": \"Alan Adı\",\n      \"homepageHeader\": \"Ana Sayfa\",\n      \"updateNamespace\": \"Alan adını güncelle\",\n      \"removeHomepage\": \"Ana sayfayı kaldır\",\n      \"selectHomePage\": \"Bir sayfa seç\",\n      \"clearHomePage\": \"Bu alan adı için ana sayfayı temizle\",\n      \"customUrl\": \"Özel URL\",\n      \"namespace\": {\n        \"description\": \"Bu değişiklik, bu alan adında yayınlanan tüm canlı sayfalara uygulanacak\",\n        \"tooltip\": \"Uygunsuz alan adlarını kaldırma hakkını saklı tutarız\",\n        \"updateExistingNamespace\": \"Mevcut alan adını güncelle\",\n        \"upgradeToPro\": \"Ana sayfa ayarlamak için Pro Plana yükseltin\",\n        \"redirectToPayment\": \"Ödeme sayfasına yönlendiriliyor...\",\n        \"onlyWorkspaceOwnerCanSetHomePage\": \"Yalnızca çalışma alanı sahibi ana sayfa ayarlayabilir\",\n        \"pleaseAskOwnerToSetHomePage\": \"Lütfen çalışma alanı sahibinden Pro Plana yükseltmesini isteyin\"\n      },\n      \"publishedPage\": {\n        \"title\": \"Tüm yayınlanan sayfalar\",\n        \"description\": \"Yayınlanan sayfalarınızı yönetin\",\n        \"page\": \"Sayfa\",\n        \"pathName\": \"Yol adı\",\n        \"date\": \"Yayınlanma tarihi\",\n        \"emptyHinText\": \"Bu çalışma alanında yayınlanmış sayfanız yok\",\n        \"noPublishedPages\": \"Yayınlanmış sayfa yok\",\n        \"settings\": \"Yayın ayarları\",\n        \"clickToOpenPageInApp\": \"Sayfayı uygulamada aç\",\n        \"clickToOpenPageInBrowser\": \"Sayfayı tarayıcıda aç\"\n      },\n      \"error\": {\n        \"failedToGeneratePaymentLink\": \"Pro Plan için ödeme bağlantısı oluşturulamadı\",\n        \"failedToUpdateNamespace\": \"Alan adı güncellenemedi\",\n        \"proPlanLimitation\": \"Alan adını güncellemek için Pro Plana yükseltmeniz gerekiyor\",\n        \"namespaceAlreadyInUse\": \"Bu alan adı zaten alınmış, lütfen başka bir tane deneyin\",\n        \"invalidNamespace\": \"Geçersiz alan adı, lütfen başka bir tane deneyin\",\n        \"namespaceLengthAtLeast2Characters\": \"Alan adı en az 2 karakter uzunluğunda olmalıdır\",\n        \"onlyWorkspaceOwnerCanUpdateNamespace\": \"Alan adını yalnızca çalışma alanı sahibi güncelleyebilir\",\n        \"onlyWorkspaceOwnerCanRemoveHomepage\": \"Ana sayfayı yalnızca çalışma alanı sahibi kaldırabilir\",\n        \"setHomepageFailed\": \"Ana sayfa ayarlanamadı\",\n        \"namespaceTooLong\": \"Alan adı çok uzun, lütfen başka bir tane deneyin\",\n        \"namespaceTooShort\": \"Alan adı çok kısa, lütfen başka bir tane deneyin\",\n        \"namespaceIsReserved\": \"Bu alan adı rezerve edilmiş, lütfen başka bir tane deneyin\",\n        \"updatePathNameFailed\": \"Yol adı güncellenemedi\",\n        \"removeHomePageFailed\": \"Ana sayfa kaldırılamadı\",\n        \"publishNameContainsInvalidCharacters\": \"Yol adı geçersiz karakter(ler) içeriyor, lütfen başka bir tane deneyin\",\n        \"publishNameTooShort\": \"Yol adı çok kısa, lütfen başka bir tane deneyin\",\n        \"publishNameTooLong\": \"Yol adı çok uzun, lütfen başka bir tane deneyin\",\n        \"publishNameAlreadyInUse\": \"Bu yol adı zaten kullanımda, lütfen başka bir tane deneyin\",\n        \"namespaceContainsInvalidCharacters\": \"Alan adı geçersiz karakter(ler) içeriyor, lütfen başka bir tane deneyin\",\n        \"publishPermissionDenied\": \"Yayın ayarlarını yalnızca çalışma alanı sahibi veya sayfa yayıncısı yönetebilir\",\n        \"publishNameCannotBeEmpty\": \"Yol adı boş olamaz, lütfen başka bir tane deneyin\"\n      },\n      \"success\": {\n        \"namespaceUpdated\": \"Alan adı başarıyla güncellendi\",\n        \"setHomepageSuccess\": \"Ana sayfa başarıyla ayarlandı\",\n        \"updatePathNameSuccess\": \"Yol adı başarıyla güncellendi\",\n        \"removeHomePageSuccess\": \"Ana sayfa başarıyla kaldırıldı\"\n      }\n    },\n    \"accountPage\": {\n      \"menuLabel\": \"Hesabım\",\n      \"title\": \"Hesabım\",\n      \"general\": {\n        \"title\": \"Hesap adı ve profil resmi\",\n        \"changeProfilePicture\": \"Profil resmini değiştir\"\n      },\n      \"email\": {\n        \"title\": \"E-posta\",\n        \"actions\": {\n          \"change\": \"E-postayı değiştir\"\n        }\n      },\n      \"login\": {\n        \"title\": \"Hesap girişi\",\n        \"loginLabel\": \"Giriş yap\",\n        \"logoutLabel\": \"Çıkış yap\"\n      }\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"Çalışma Alanı\",\n      \"title\": \"Çalışma Alanı\",\n      \"description\": \"Çalışma alanınızın görünümünü, temasını, yazı tipini, metin düzenini, tarih/saat biçimini ve dilini özelleştirin.\",\n      \"workspaceName\": {\n        \"title\": \"Çalışma alanı adı\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"Çalışma alanı simgesi\",\n        \"description\": \"Çalışma alanınız için bir resim yükleyin veya emoji kullanın. Simge, kenar çubuğunuzda ve bildirimlerinizde görünecektir.\"\n      },\n      \"appearance\": {\n        \"title\": \"Görünüm\",\n        \"description\": \"Çalışma alanınızın görünümünü, temasını, yazı tipini, metin düzenini, tarih, saat ve dilini özelleştirin.\",\n        \"options\": {\n          \"system\": \"Otomatik\",\n          \"light\": \"Aydınlık\",\n          \"dark\": \"Karanlık\"\n        }\n      },\n      \"resetCursorColor\": {\n        \"title\": \"Belge imleç rengini sıfırla\",\n        \"description\": \"İmleç rengini sıfırlamak istediğinizden emin misiniz?\"\n      },\n      \"resetSelectionColor\": {\n        \"title\": \"Belge seçim rengini sıfırla\",\n        \"description\": \"Seçim rengini sıfırlamak istediğinizden emin misiniz?\"\n      },\n      \"resetWidth\": {\n        \"resetSuccess\": \"Belge genişliği başarıyla sıfırlandı\"\n      },\n      \"theme\": {\n        \"title\": \"Tema\",\n        \"description\": \"Önceden ayarlanmış bir tema seçin veya kendi özel temanızı yükleyin.\",\n        \"uploadCustomThemeTooltip\": \"Özel tema yükle\"\n      },\n      \"workspaceFont\": {\n        \"title\": \"Çalışma alanı yazı tipi\",\n        \"noFontHint\": \"Yazı tipi bulunamadı, başka bir terim deneyin.\"\n      },\n      \"textDirection\": {\n        \"title\": \"Metin yönü\",\n        \"leftToRight\": \"Soldan sağa\",\n        \"rightToLeft\": \"Sağdan sola\",\n        \"auto\": \"Otomatik\",\n        \"enableRTLItems\": \"RTL araç çubuğu öğelerini etkinleştir\"\n      },\n      \"layoutDirection\": {\n        \"title\": \"Düzen yönü\",\n        \"leftToRight\": \"Soldan sağa\",\n        \"rightToLeft\": \"Sağdan sola\"\n      },\n      \"dateTime\": {\n        \"title\": \"Tarih ve saat\",\n        \"example\": \"{} {} ({})\",\n        \"24HourTime\": \"24 saat biçimi\",\n        \"dateFormat\": {\n          \"label\": \"Tarih biçimi\",\n          \"local\": \"Yerel\",\n          \"us\": \"ABD\",\n          \"iso\": \"ISO\",\n          \"friendly\": \"Kullanıcı dostu\",\n          \"dmy\": \"G/A/Y\"\n        }\n      },\n      \"language\": {\n        \"title\": \"Dil\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"Çalışma alanını sil\",\n        \"content\": \"Bu çalışma alanını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz ve yayınladığınız tüm sayfaların yayını kaldırılacaktır.\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"Çalışma alanından ayrıl\",\n        \"content\": \"Bu çalışma alanından ayrılmak istediğinizden emin misiniz? İçindeki tüm sayfalara ve verilere erişiminizi kaybedeceksiniz.\",\n        \"success\": \"Çalışma alanından başarıyla ayrıldınız.\",\n        \"fail\": \"Çalışma alanından ayrılınamadı.\"\n      },\n      \"manageWorkspace\": {\n        \"title\": \"Çalışma alanını yönet\",\n        \"leaveWorkspace\": \"Çalışma alanından ayrıl\",\n        \"deleteWorkspace\": \"Çalışma alanını sil\"\n      }\n    },\n    \"manageDataPage\": {\n      \"menuLabel\": \"Verileri yönet\",\n      \"title\": \"Verileri yönet\",\n      \"description\": \"Yerel depolama verilerini yönetin veya mevcut verilerinizi @:appName'e aktarın.\",\n      \"dataStorage\": {\n        \"title\": \"Dosya depolama konumu\",\n        \"tooltip\": \"Dosyalarınızın depolandığı konum\",\n        \"actions\": {\n          \"change\": \"Yolu değiştir\",\n          \"open\": \"Klasörü aç\",\n          \"openTooltip\": \"Mevcut veri klasörü konumunu aç\",\n          \"copy\": \"Yolu kopyala\",\n          \"copiedHint\": \"Yol kopyalandı!\",\n          \"resetTooltip\": \"Varsayılan konuma sıfırla\"\n        },\n        \"resetDialog\": {\n          \"title\": \"Emin misiniz?\",\n          \"description\": \"Yolu varsayılan veri konumuna sıfırlamak verilerinizi silmeyecektir. Mevcut verilerinizi yeniden içe aktarmak istiyorsanız, önce mevcut konumunuzun yolunu kopyalamalısınız.\"\n        }\n      },\n      \"importData\": {\n        \"title\": \"Veri içe aktar\",\n        \"tooltip\": \"@:appName yedeklerinden/veri klasörlerinden veri içe aktar\",\n        \"description\": \"Harici bir @:appName veri klasöründen veri kopyala\",\n        \"action\": \"Dosyaya göz at\"\n      },\n      \"encryption\": {\n        \"title\": \"Şifreleme\",\n        \"tooltip\": \"Verilerinizin nasıl depolandığını ve şifrelendiğini yönetin\",\n        \"descriptionNoEncryption\": \"Şifrelemeyi açmak tüm verileri şifreleyecektir. Bu işlem geri alınamaz.\",\n        \"descriptionEncrypted\": \"Verileriniz şifrelenmiş.\",\n        \"action\": \"Verileri şifrele\",\n        \"dialog\": {\n          \"title\": \"Tüm verileriniz şifrelensin mi?\",\n          \"description\": \"Tüm verilerinizi şifrelemek, verilerinizi güvenli ve emniyetli tutacaktır. Bu işlem GERİ ALINAMAZ. Devam etmek istediğinizden emin misiniz?\"\n        }\n      },\n      \"cache\": {\n        \"title\": \"Önbelleği temizle\",\n        \"description\": \"Görsel yüklenmemesi, bir alanda eksik sayfalar ve yazı tiplerinin yüklenmemesi gibi sorunları çözmeye yardımcı olur. Bu, verilerinizi etkilemeyecektir.\",\n        \"dialog\": {\n          \"title\": \"Önbelleği temizle\",\n          \"description\": \"Görsel yüklenmemesi, bir alanda eksik sayfalar ve yazı tiplerinin yüklenmemesi gibi sorunları çözmeye yardımcı olur. Bu, verilerinizi etkilemeyecektir.\",\n          \"successHint\": \"Önbellek temizlendi!\"\n        }\n      },\n      \"data\": {\n        \"fixYourData\": \"Verilerinizi düzeltin\",\n        \"fixButton\": \"Düzelt\",\n        \"fixYourDataDescription\": \"Verilerinizle ilgili sorunlar yaşıyorsanız, burada düzeltmeyi deneyebilirsiniz.\"\n      }\n    },\n    \"shortcutsPage\": {\n      \"menuLabel\": \"Kısayollar\",\n      \"title\": \"Kısayollar\",\n      \"editBindingHint\": \"Yeni bağlama girin\",\n      \"searchHint\": \"Ara\",\n      \"actions\": {\n        \"resetDefault\": \"Varsayılana sıfırla\"\n      },\n      \"errorPage\": {\n        \"message\": \"Kısayollar yüklenemedi: {}\",\n        \"howToFix\": \"Lütfen tekrar deneyin, sorun devam ederse GitHub üzerinden bize ulaşın.\"\n      },\n      \"resetDialog\": {\n        \"title\": \"Kısayolları sıfırla\",\n        \"description\": \"Bu işlem tüm tuş bağlamalarınızı varsayılana sıfırlayacak, daha sonra geri alamazsınız, devam etmek istediğinizden emin misiniz?\",\n        \"buttonLabel\": \"Sıfırla\"\n      },\n      \"conflictDialog\": {\n        \"title\": \"{} şu anda kullanımda\",\n        \"descriptionPrefix\": \"Bu tuş bağlaması şu anda \",\n        \"descriptionSuffix\": \" tarafından kullanılıyor. Bu tuş bağlamasını değiştirirseniz, {} üzerinden kaldırılacak.\",\n        \"confirmLabel\": \"Devam et\"\n      },\n      \"editTooltip\": \"Tuş bağlamasını düzenlemeye başlamak için basın\",\n      \"keybindings\": {\n        \"toggleToDoList\": \"Yapılacaklar listesini aç/kapat\",\n        \"insertNewParagraphInCodeblock\": \"Yeni paragraf ekle\",\n        \"pasteInCodeblock\": \"Kod bloğuna yapıştır\",\n        \"selectAllCodeblock\": \"Tümünü seç\",\n        \"indentLineCodeblock\": \"Satır başına iki boşluk ekle\",\n        \"outdentLineCodeblock\": \"Satır başından iki boşluk sil\",\n        \"twoSpacesCursorCodeblock\": \"İmleç konumuna iki boşluk ekle\",\n        \"copy\": \"Seçimi kopyala\",\n        \"paste\": \"İçeriği yapıştır\",\n        \"cut\": \"Seçimi kes\",\n        \"alignLeft\": \"Metni sola hizala\",\n        \"alignCenter\": \"Metni ortala\",\n        \"alignRight\": \"Metni sağa hizala\",\n        \"undo\": \"Geri al\",\n        \"redo\": \"Yinele\",\n        \"convertToParagraph\": \"Bloğu paragrafa dönüştür\",\n        \"backspace\": \"Sil\",\n        \"deleteLeftWord\": \"Sol kelimeyi sil\",\n        \"deleteLeftSentence\": \"Sol cümleyi sil\",\n        \"delete\": \"Sağdaki karakteri sil\",\n        \"deleteMacOS\": \"Soldaki karakteri sil\",\n        \"deleteRightWord\": \"Sağdaki kelimeyi sil\",\n        \"moveCursorLeft\": \"İmleci sola taşı\",\n        \"moveCursorBeginning\": \"İmleci başa taşı\",\n        \"moveCursorLeftWord\": \"İmleci bir kelime sola taşı\",\n        \"moveCursorLeftSelect\": \"Seç ve imleci sola taşı\",\n        \"moveCursorBeginSelect\": \"Seç ve imleci başa taşı\",\n        \"moveCursorLeftWordSelect\": \"Seç ve imleci bir kelime sola taşı\",\n        \"moveCursorRight\": \"İmleci sağa taşı\",\n        \"moveCursorEnd\": \"İmleci sona taşı\",\n        \"moveCursorRightWord\": \"İmleci bir kelime sağa taşı\",\n        \"moveCursorRightSelect\": \"Seç ve imleci sağa taşı\",\n        \"moveCursorEndSelect\": \"Seç ve imleci sona taşı\",\n        \"moveCursorRightWordSelect\": \"Seç ve imleci bir kelime sağa taşı\",\n        \"moveCursorUp\": \"İmleci yukarı taşı\",\n        \"moveCursorTopSelect\": \"Seç ve imleci en üste taşı\",\n        \"moveCursorTop\": \"İmleci en üste taşı\",\n        \"moveCursorUpSelect\": \"Seç ve imleci yukarı taşı\",\n        \"moveCursorBottomSelect\": \"Seç ve imleci en alta taşı\",\n        \"moveCursorBottom\": \"İmleci en alta taşı\",\n        \"moveCursorDown\": \"İmleci aşağı taşı\",\n        \"moveCursorDownSelect\": \"Seç ve imleci aşağı taşı\",\n        \"home\": \"En üste kaydır\",\n        \"end\": \"En alta kaydır\",\n        \"toggleBold\": \"Kalın yazıyı aç/kapat\",\n        \"toggleItalic\": \"İtalik yazıyı aç/kapat\",\n        \"toggleUnderline\": \"Altı çizili yazıyı aç/kapat\",\n        \"toggleStrikethrough\": \"Üstü çizili yazıyı aç/kapat\",\n        \"toggleCode\": \"Satır içi kodu aç/kapat\",\n        \"toggleHighlight\": \"Vurgulamayı aç/kapat\",\n        \"showLinkMenu\": \"Bağlantı menüsünü göster\",\n        \"openInlineLink\": \"Satır içi bağlantıyı aç\",\n        \"openLinks\": \"Seçili tüm bağlantıları aç\",\n        \"indent\": \"Girinti ekle\",\n        \"outdent\": \"Girintiyi azalt\",\n        \"exit\": \"Düzenlemeden çık\",\n        \"pageUp\": \"Bir sayfa yukarı kaydır\",\n        \"pageDown\": \"Bir sayfa aşağı kaydır\",\n        \"selectAll\": \"Tümünü seç\",\n        \"pasteWithoutFormatting\": \"İçeriği biçimlendirme olmadan yapıştır\",\n        \"showEmojiPicker\": \"Emoji seçiciyi göster\",\n        \"enterInTableCell\": \"Tabloda satır sonu ekle\",\n        \"leftInTableCell\": \"Tabloda bir hücre sola git\",\n        \"rightInTableCell\": \"Tabloda bir hücre sağa git\",\n        \"upInTableCell\": \"Tabloda bir hücre yukarı git\",\n        \"downInTableCell\": \"Tabloda bir hücre aşağı git\",\n        \"tabInTableCell\": \"Tabloda sonraki kullanılabilir hücreye git\",\n        \"shiftTabInTableCell\": \"Tabloda önceki kullanılabilir hücreye git\",\n        \"backSpaceInTableCell\": \"Hücrenin başında dur\"\n      },\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"Kod bloğunun yanına yeni bir paragraf ekle\",\n        \"codeBlockIndentLines\": \"Kod bloğunda satır başına iki boşluk ekle\",\n        \"codeBlockOutdentLines\": \"Kod bloğunda satır başından iki boşluk sil\",\n        \"codeBlockAddTwoSpaces\": \"Kod bloğunda imleç konumuna iki boşluk ekle\",\n        \"codeBlockSelectAll\": \"Kod bloğu içindeki tüm içeriği seç\",\n        \"codeBlockPasteText\": \"Kod bloğuna metin yapıştır\",\n        \"textAlignLeft\": \"Metni sola hizala\",\n        \"textAlignCenter\": \"Metni ortala\",\n        \"textAlignRight\": \"Metni sağa hizala\"\n      },\n      \"couldNotLoadErrorMsg\": \"Kısayollar yüklenemedi, tekrar deneyin\",\n      \"couldNotSaveErrorMsg\": \"Kısayollar kaydedilemedi, tekrar deneyin\"\n    },\n    \"aiPage\": {\n      \"title\": \"Yapay Zeka Ayarları\",\n      \"menuLabel\": \"Yapay Zeka Ayarları\",\n      \"keys\": {\n        \"enableAISearchTitle\": \"Yapay Zeka Arama\",\n        \"aiSettingsDescription\": \"AppFlowy Yapay Zeka'yı güçlendirmek için tercih ettiğiniz modeli seçin. Şu anda GPT 4-o, Claude 3,5, Llama 3.1 ve Mistral 7B içerir\",\n        \"loginToEnableAIFeature\": \"Yapay Zeka özellikleri yalnızca @:appName Cloud ile giriş yaptıktan sonra etkinleştirilir. Bir @:appName hesabınız yoksa, kaydolmak için 'Hesabım'a gidin\",\n        \"llmModel\": \"Dil Modeli\",\n        \"llmModelType\": \"Dil Modeli Türü\",\n        \"downloadLLMPrompt\": \"{} İndir\",\n        \"downloadAppFlowyOfflineAI\": \"Yapay Zeka çevrimdışı paketini indirmek, Yapay Zeka'nın cihazınızda çalışmasını sağlayacak. Devam etmek istiyor musunuz?\",\n        \"downloadLLMPromptDetail\": \"{} yerel modelini indirmek {} depolama alanı kullanacak. Devam etmek istiyor musunuz?\",\n        \"downloadBigFilePrompt\": \"İndirmenin tamamlanması yaklaşık 10 dakika sürebilir\",\n        \"downloadAIModelButton\": \"İndir\",\n        \"downloadingModel\": \"İndiriliyor\",\n        \"localAILoaded\": \"Yerel Yapay Zeka Modeli başarıyla eklendi ve kullanıma hazır\",\n        \"localAIStart\": \"Yerel Yapay Zeka Sohbeti başlatılıyor...\",\n        \"localAILoading\": \"Yerel Yapay Zeka Sohbet Modeli yükleniyor...\",\n        \"localAIStopped\": \"Yerel Yapay Zeka durduruldu\",\n        \"failToLoadLocalAI\": \"Yerel Yapay Zeka başlatılamadı\",\n        \"restartLocalAI\": \"Yerel Yapay Zeka'yı Yeniden Başlat\",\n        \"disableLocalAITitle\": \"Yerel Yapay Zeka'yı devre dışı bırak\",\n        \"disableLocalAIDescription\": \"Yerel Yapay Zeka'yı devre dışı bırakmak istiyor musunuz?\",\n        \"localAIToggleTitle\": \"Yerel Yapay Zeka'yı etkinleştirmek veya devre dışı bırakmak için değiştirin\",\n        \"offlineAIInstruction1\": \"Çevrimdışı Yapay Zeka'yı etkinleştirmek için\",\n        \"offlineAIInstruction2\": \"talimatları\",\n        \"offlineAIInstruction3\": \"takip edin.\",\n        \"offlineAIDownload1\": \"AppFlowy Yapay Zeka'yı henüz indirmediyseniz, lütfen\",\n        \"offlineAIDownload2\": \"indirin\",\n        \"offlineAIDownload3\": \"önce\",\n        \"activeOfflineAI\": \"Etkin\",\n        \"downloadOfflineAI\": \"İndir\",\n        \"openModelDirectory\": \"Klasörü aç\"\n      }\n    },\n    \"planPage\": {\n      \"menuLabel\": \"Plan\",\n      \"title\": \"Fiyatlandırma planı\",\n      \"planUsage\": {\n        \"title\": \"Plan kullanım özeti\",\n        \"storageLabel\": \"Depolama\",\n        \"storageUsage\": \"{} / {} GB\",\n        \"unlimitedStorageLabel\": \"Sınırsız depolama\",\n        \"collaboratorsLabel\": \"Üyeler\",\n        \"collaboratorsUsage\": \"{} / {}\",\n        \"aiResponseLabel\": \"Yapay Zeka Yanıtları\",\n        \"aiResponseUsage\": \"{} / {}\",\n        \"unlimitedAILabel\": \"Sınırsız yanıt\",\n        \"proBadge\": \"Pro\",\n        \"aiMaxBadge\": \"Yapay Zeka Max\",\n        \"aiOnDeviceBadge\": \"Mac için Cihaz Üzerinde Yapay Zeka\",\n        \"memberProToggle\": \"Daha fazla üye ve sınırsız Yapay Zeka\",\n        \"aiMaxToggle\": \"Sınırsız Yapay Zeka ve gelişmiş modellere erişim\",\n        \"aiOnDeviceToggle\": \"Maksimum gizlilik için yerel Yapay Zeka\",\n        \"aiCredit\": {\n          \"title\": \"@:appName Yapay Zeka Kredisi Ekle\",\n          \"price\": \"{}\",\n          \"priceDescription\": \"1.000 kredi için\",\n          \"purchase\": \"Yapay Zeka Satın Al\",\n          \"info\": \"Çalışma alanı başına 1.000 Yapay Zeka kredisi ekleyin ve özelleştirilebilir Yapay Zeka'yı iş akışınıza sorunsuz bir şekilde entegre ederek daha akıllı, daha hızlı sonuçlar elde edin:\",\n          \"infoItemOne\": \"Veritabanı başına 10.000 yanıt\",\n          \"infoItemTwo\": \"Çalışma alanı başına 1.000 yanıt\"\n        },\n        \"currentPlan\": {\n          \"bannerLabel\": \"Mevcut plan\",\n          \"freeTitle\": \"Ücretsiz\",\n          \"proTitle\": \"Pro\",\n          \"teamTitle\": \"Takım\",\n          \"freeInfo\": \"2 üyeye kadar bireyler için her şeyi düzenlemek için mükemmel\",\n          \"proInfo\": \"10 üyeye kadar küçük ve orta ölçekli takımlar için mükemmel.\",\n          \"teamInfo\": \"Tüm üretken ve iyi organize edilmiş takımlar için mükemmel.\",\n          \"upgrade\": \"Planı değiştir\",\n          \"canceledInfo\": \"Planınız iptal edildi, {} tarihinde Ücretsiz plana düşürüleceksiniz.\"\n        },\n        \"addons\": {\n          \"title\": \"Eklentiler\",\n          \"addLabel\": \"Ekle\",\n          \"activeLabel\": \"Eklendi\",\n          \"aiMax\": {\n            \"title\": \"Yapay Zeka Max\",\n            \"description\": \"Gelişmiş Yapay Zeka modelleri tarafından desteklenen sınırsız Yapay Zeka yanıtları ve ayda 50 Yapay Zeka görüntüsü\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"Kullanıcı başına aylık, yıllık faturalandırma\"\n          },\n          \"aiOnDevice\": {\n            \"title\": \"Mac için Cihaz Üzerinde Yapay Zeka\",\n            \"description\": \"Mistral 7B, LLAMA 3 ve daha fazla yerel modeli makinenizde çalıştırın\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"Kullanıcı başına aylık, yıllık faturalandırma\",\n            \"recommend\": \"M1 veya daha yenisi önerilir\"\n          }\n        },\n        \"deal\": {\n          \"bannerLabel\": \"Yeni yıl fırsatı!\",\n          \"title\": \"Takımınızı büyütün!\",\n          \"info\": \"Yükseltin ve Pro ve Takım planlarında %10 indirim kazanın! @:appName Yapay Zeka dahil güçlü yeni özelliklerle çalışma alanı verimliliğinizi artırın.\",\n          \"viewPlans\": \"Planları görüntüle\"\n        }\n      }\n    },\n    \"billingPage\": {\n      \"menuLabel\": \"Faturalandırma\",\n      \"title\": \"Faturalandırma\",\n      \"plan\": {\n        \"title\": \"Plan\",\n        \"freeLabel\": \"Ücretsiz\",\n        \"proLabel\": \"Pro\",\n        \"planButtonLabel\": \"Planı değiştir\",\n        \"billingPeriod\": \"Faturalandırma dönemi\",\n        \"periodButtonLabel\": \"Dönemi düzenle\"\n      },\n      \"paymentDetails\": {\n        \"title\": \"Ödeme detayları\",\n        \"methodLabel\": \"Ödeme yöntemi\",\n        \"methodButtonLabel\": \"Yöntemi düzenle\"\n      },\n      \"addons\": {\n        \"title\": \"Eklentiler\",\n        \"addLabel\": \"Ekle\",\n        \"removeLabel\": \"Kaldır\",\n        \"renewLabel\": \"Yenile\",\n        \"aiMax\": {\n          \"label\": \"Yapay Zeka Max\",\n          \"description\": \"Sınırsız Yapay Zeka ve gelişmiş modellerin kilidini açın\",\n          \"activeDescription\": \"Sonraki fatura tarihi: {}\",\n          \"canceledDescription\": \"Yapay Zeka Max {} tarihine kadar kullanılabilir olacak\"\n        },\n        \"aiOnDevice\": {\n          \"label\": \"Mac için Cihaz Üzerinde Yapay Zeka\",\n          \"description\": \"Cihazınızda sınırsız Cihaz Üzerinde Yapay Zeka'nın kilidini açın\",\n          \"activeDescription\": \"Sonraki fatura tarihi: {}\",\n          \"canceledDescription\": \"Mac için Cihaz Üzerinde Yapay Zeka {} tarihine kadar kullanılabilir olacak\"\n        },\n        \"removeDialog\": {\n          \"title\": \"{} Kaldır\",\n          \"description\": \"{plan} planını kaldırmak istediğinizden emin misiniz? {plan} planının özelliklerine ve avantajlarına erişiminizi hemen kaybedeceksiniz.\"\n        }\n      },\n      \"currentPeriodBadge\": \"MEVCUT\",\n      \"changePeriod\": \"Dönemi değiştir\",\n      \"planPeriod\": \"{} dönemi\",\n      \"monthlyInterval\": \"Aylık\",\n      \"monthlyPriceInfo\": \"koltuk başına aylık faturalandırma\",\n      \"annualInterval\": \"Yıllık\",\n      \"annualPriceInfo\": \"koltuk başına yıllık faturalandırma\"\n    },\n    \"comparePlanDialog\": {\n      \"title\": \"Plan karşılaştır ve seç\",\n      \"planFeatures\": \"Plan\\nÖzellikleri\",\n      \"current\": \"Mevcut\",\n      \"actions\": {\n        \"upgrade\": \"Yükselt\",\n        \"downgrade\": \"Düşür\",\n        \"current\": \"Mevcut\"\n      },\n      \"freePlan\": {\n        \"title\": \"Ücretsiz\",\n        \"description\": \"2 üyeye kadar bireyler için her şeyi düzenlemek için\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"Sonsuza kadar ücretsiz\"\n      },\n      \"proPlan\": {\n        \"title\": \"Pro\",\n        \"description\": \"Küçük takımların projeleri ve takım bilgisini yönetmesi için\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"Kullanıcı başına aylık \\nyıllık faturalandırma\\n\\n{} aylık faturalandırma\"\n      },\n      \"planLabels\": {\n        \"itemOne\": \"Çalışma Alanları\",\n        \"itemTwo\": \"Üyeler\",\n        \"itemThree\": \"Depolama\",\n        \"itemFour\": \"Gerçek zamanlı işbirliği\",\n        \"itemFive\": \"Mobil uygulama\",\n        \"itemSix\": \"Yapay Zeka Yanıtları\",\n        \"itemFileUpload\": \"Dosya yüklemeleri\",\n        \"customNamespace\": \"Özel alan adı\",\n        \"tooltipSix\": \"Ömür boyu demek, yanıt sayısının asla sıfırlanmayacağı anlamına gelir\",\n        \"intelligentSearch\": \"Akıllı arama\",\n        \"tooltipSeven\": \"Çalışma alanınızın URL'sinin bir kısmını özelleştirmenize olanak tanır\",\n        \"customNamespaceTooltip\": \"Özel yayınlanmış site URL'si\"\n      },\n      \"freeLabels\": {\n        \"itemOne\": \"Çalışma alanı başına ücretlendirilir\",\n        \"itemTwo\": \"2'ye kadar\",\n        \"itemThree\": \"5 GB\",\n        \"itemFour\": \"evet\",\n        \"itemFive\": \"evet\",\n        \"itemSix\": \"10 ömür boyu\",\n        \"itemFileUpload\": \"7 MB'a kadar\",\n        \"intelligentSearch\": \"Akıllı arama\"\n      },\n      \"proLabels\": {\n        \"itemOne\": \"Çalışma alanı başına ücretlendirilir\",\n        \"itemTwo\": \"10'a kadar\",\n        \"itemThree\": \"Sınırsız\",\n        \"itemFour\": \"evet\",\n        \"itemFive\": \"evet\",\n        \"itemSix\": \"Sınırsız\",\n        \"itemFileUpload\": \"Sınırsız\",\n        \"intelligentSearch\": \"Akıllı arama\"\n      },\n      \"paymentSuccess\": {\n        \"title\": \"Artık {} planındasınız!\",\n        \"description\": \"Ödemeniz başarıyla işleme alındı ve planınız @:appName {}'e yükseltildi. Plan detaylarınızı Plan sayfasında görüntüleyebilirsiniz\"\n      },\n      \"downgradeDialog\": {\n        \"title\": \"Planınızı düşürmek istediğinizden emin misiniz?\",\n        \"description\": \"Planınızı düşürmek sizi Ücretsiz plana geri döndürecek. Üyeler bu çalışma alanına erişimlerini kaybedebilir ve Ücretsiz planın depolama sınırlarına uymak için alan açmanız gerekebilir.\",\n        \"downgradeLabel\": \"Planı düşür\"\n      }\n    },\n    \"cancelSurveyDialog\": {\n      \"title\": \"Gitmenize üzüldük\",\n      \"description\": \"Gitmenize üzüldük. @:appName'i geliştirmemize yardımcı olmak için geri bildiriminizi duymak isteriz. Lütfen birkaç soruyu yanıtlamak için zaman ayırın.\",\n      \"commonOther\": \"Diğer\",\n      \"otherHint\": \"Yanıtınızı buraya yazın\",\n      \"questionOne\": {\n        \"question\": \"@:appName Pro aboneliğinizi iptal etmenize ne sebep oldu?\",\n        \"answerOne\": \"Maliyet çok yüksek\",\n        \"answerTwo\": \"Özellikler beklentileri karşılamadı\",\n        \"answerThree\": \"Daha iyi bir alternatif buldum\",\n        \"answerFour\": \"Maliyeti haklı çıkaracak kadar kullanmadım\",\n        \"answerFive\": \"Hizmet sorunu veya teknik zorluklar\"\n      },\n      \"questionTwo\": {\n        \"question\": \"Gelecekte @:appName Pro'ya yeniden abone olma olasılığınız nedir?\",\n        \"answerOne\": \"Çok muhtemel\",\n        \"answerTwo\": \"Biraz muhtemel\",\n        \"answerThree\": \"Emin değilim\",\n        \"answerFour\": \"Muhtemel değil\",\n        \"answerFive\": \"Hiç muhtemel değil\"\n      },\n      \"questionThree\": {\n        \"question\": \"Aboneliğiniz sırasında en çok hangi Pro özelliğine değer verdiniz?\",\n        \"answerOne\": \"Çoklu kullanıcı işbirliği\",\n        \"answerTwo\": \"Daha uzun süreli versiyon geçmişi\",\n        \"answerThree\": \"Sınırsız Yapay Zeka yanıtları\",\n        \"answerFour\": \"Yerel Yapay Zeka modellerine erişim\"\n      },\n      \"questionFour\": {\n        \"question\": \"@:appName ile genel deneyiminizi nasıl tanımlarsınız?\",\n        \"answerOne\": \"Harika\",\n        \"answerTwo\": \"İyi\",\n        \"answerThree\": \"Ortalama\",\n        \"answerFour\": \"Ortalamanın altında\",\n        \"answerFive\": \"Memnun değilim\"\n      }\n    },\n    \"common\": {\n      \"uploadingFile\": \"Dosya yükleniyor. Lütfen uygulamadan çıkmayın\",\n      \"uploadNotionSuccess\": \"Notion zip dosyanız başarıyla yüklendi. İçe aktarma tamamlandığında bir onay e-postası alacaksınız\",\n      \"reset\": \"Sıfırla\"\n    },\n    \"menu\": {\n      \"appearance\": \"Görünüm\",\n      \"language\": \"Dil\",\n      \"user\": \"Kullanıcı\",\n      \"files\": \"Dosyalar\",\n      \"notifications\": \"Bildirimler\",\n      \"open\": \"Ayarları Aç\",\n      \"logout\": \"Çıkış Yap\",\n      \"logoutPrompt\": \"Çıkış yapmak istediğinizden emin misiniz?\",\n      \"selfEncryptionLogoutPrompt\": \"Çıkış yapmak istediğinizden emin misiniz? Lütfen şifreleme anahtarınızı kopyaladığınızdan emin olun\",\n      \"syncSetting\": \"Senkronizasyon Ayarı\",\n      \"cloudSettings\": \"Bulut Ayarları\",\n      \"enableSync\": \"Senkronizasyonu etkinleştir\",\n      \"enableSyncLog\": \"Senkronizasyon günlüğünü etkinleştir\",\n      \"enableSyncLogWarning\": \"Senkronizasyon sorunlarını teşhis etmeye yardımcı olduğunuz için teşekkür ederiz. Bu, belge düzenlemelerinizi yerel bir dosyaya kaydedecek. Lütfen etkinleştirdikten sonra uygulamayı kapatıp yeniden açın\",\n      \"enableEncrypt\": \"Verileri şifrele\",\n      \"cloudURL\": \"Temel URL\",\n      \"webURL\": \"Web URL'si\",\n      \"invalidCloudURLScheme\": \"Geçersiz Şema\",\n      \"cloudServerType\": \"Bulut sunucusu\",\n      \"cloudServerTypeTip\": \"Lütfen bulut sunucusunu değiştirdikten sonra mevcut hesabınızdan çıkış yapabileceğinizi unutmayın\",\n      \"cloudLocal\": \"Yerel\",\n      \"cloudAppFlowy\": \"@:appName Cloud\",\n      \"cloudAppFlowySelfHost\": \"@:appName Cloud Kendi Kendine Barındırma\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"Bulut URL'si boş olamaz\",\n      \"clickToCopy\": \"Panoya kopyala\",\n      \"selfHostStart\": \"Bir sunucunuz yoksa, lütfen\",\n      \"selfHostContent\": \"belgesine\",\n      \"selfHostEnd\": \"bakarak kendi sunucunuzu nasıl barındıracağınızı öğrenin\",\n      \"pleaseInputValidURL\": \"Lütfen geçerli bir URL girin\",\n      \"changeUrl\": \"Kendi kendine barındırılan URL'yi {} olarak değiştir\",\n      \"cloudURLHint\": \"Sunucunuzun temel URL'sini girin\",\n      \"webURLHint\": \"Web sunucunuzun temel URL'sini girin\",\n      \"cloudWSURL\": \"Websocket URL'si\",\n      \"cloudWSURLHint\": \"Sunucunuzun websocket adresini girin\",\n      \"restartApp\": \"Yeniden Başlat\",\n      \"restartAppTip\": \"Değişikliklerin etkili olması için uygulamayı yeniden başlatın. Bu işlemin mevcut hesabınızdan çıkış yapabileceğini unutmayın.\",\n      \"changeServerTip\": \"Sunucuyu değiştirdikten sonra, değişikliklerin etkili olması için yeniden başlat düğmesine tıklamalısınız\",\n      \"enableEncryptPrompt\": \"Verilerinizi bu anahtar ile güvence altına almak için şifrelemeyi etkinleştirin. Güvenli bir şekilde saklayın; etkinleştirildikten sonra kapatılamaz. Kaybedilirse, verileriniz kurtarılamaz hale gelir. Kopyalamak için tıklayın\",\n      \"inputEncryptPrompt\": \"Lütfen şifreleme anahtarlarını girin\",\n      \"clickToCopySecret\": \"Anahtarı kopyalamak için tıklayın\",\n      \"configServerSetting\": \"Sunucu ayarlarınızı yapılandırın\",\n      \"configServerGuide\": \"'Hızlı Başlangıç'ı seçtikten sonra, 'Ayarlar'a ve ardından \\\"Bulut Ayarları\\\"na giderek kendi kendine barındırılan sunucunuzu yapılandırın.\",\n      \"inputTextFieldHint\": \"Anahtarınız\",\n      \"historicalUserList\": \"Kullanıcı giriş geçmişi\",\n      \"historicalUserListTooltip\": \"Bu liste anonim hesaplarınızı gösterir. Detaylarını görüntülemek için bir hesaba tıklayabilirsiniz. Anonim hesaplar 'Başla' düğmesine tıklanarak oluşturulur\",\n      \"openHistoricalUser\": \"Anonim hesabı açmak için tıklayın\",\n      \"customPathPrompt\": \"@:appName veri klasörünü Google Drive gibi bulut senkronizasyonlu bir klasörde saklamak risk oluşturabilir. Bu klasördeki veritabanına aynı anda birden fazla konumdan erişilir veya değiştirilirse, senkronizasyon çakışmaları ve olası veri bozulmaları meydana gelebilir\",\n      \"importAppFlowyData\": \"Harici @:appName Klasöründen Veri İçe Aktar\",\n      \"importingAppFlowyDataTip\": \"Veri içe aktarma devam ediyor. Lütfen uygulamayı kapatmayın\",\n      \"importAppFlowyDataDescription\": \"Harici bir @:appName veri klasöründen veri kopyalayın ve mevcut AppFlowy veri klasörüne aktarın\",\n      \"importSuccess\": \"@:appName veri klasörü başarıyla içe aktarıldı\",\n      \"importFailed\": \"@:appName veri klasörünün içe aktarılması başarısız oldu\",\n      \"importGuide\": \"Daha fazla detay için lütfen referans belgeyi kontrol edin\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Bildirimleri etkinleştir\",\n        \"hint\": \"Yerel bildirimlerin görünmesini durdurmak için kapatın.\"\n      },\n      \"showNotificationsIcon\": {\n        \"label\": \"Bildirim simgesini göster\",\n        \"hint\": \"Kenar çubuğundaki bildirim simgesini gizlemek için kapatın.\"\n      },\n      \"archiveNotifications\": {\n        \"allSuccess\": \"Tüm bildirimler başarıyla arşivlendi\",\n        \"success\": \"Bildirim başarıyla arşivlendi\"\n      },\n      \"markAsReadNotifications\": {\n        \"allSuccess\": \"Tümü okundu olarak işaretlendi\",\n        \"success\": \"Okundu olarak işaretlendi\"\n      },\n      \"action\": {\n        \"markAsRead\": \"Okundu olarak işaretle\",\n        \"multipleChoice\": \"Daha fazla seç\",\n        \"archive\": \"Arşivle\"\n      },\n      \"settings\": {\n        \"settings\": \"Ayarlar\",\n        \"markAllAsRead\": \"Tümünü okundu olarak işaretle\",\n        \"archiveAll\": \"Tümünü arşivle\"\n      },\n      \"emptyInbox\": {\n        \"title\": \"Gelen Kutusu Boş!\",\n        \"description\": \"Burada bildirim almak için hatırlatıcılar ayarlayın.\"\n      },\n      \"emptyUnread\": {\n        \"title\": \"Okunmamış bildirim yok\",\n        \"description\": \"Hepsini okudunuz!\"\n      },\n      \"emptyArchived\": {\n        \"title\": \"Arşivlenmiş öğe yok\",\n        \"description\": \"Arşivlenen bildirimler burada görünecek.\"\n      },\n      \"tabs\": {\n        \"inbox\": \"Gelen Kutusu\",\n        \"unread\": \"Okunmamış\",\n        \"archived\": \"Arşivlenmiş\"\n      },\n      \"refreshSuccess\": \"Bildirimler başarıyla yenilendi\",\n      \"titles\": {\n        \"notifications\": \"Bildirimler\",\n        \"reminder\": \"Hatırlatıcı\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"Sıfırla\",\n      \"fontFamily\": {\n        \"label\": \"Yazı Tipi Ailesi\",\n        \"search\": \"Ara\",\n        \"defaultFont\": \"Sistem\"\n      },\n      \"themeMode\": {\n        \"label\": \"Tema Modu\",\n        \"light\": \"Aydınlık Mod\",\n        \"dark\": \"Karanlık Mod\",\n        \"system\": \"Sisteme Uyum Sağla\"\n      },\n      \"fontScaleFactor\": \"Yazı Tipi Ölçek Faktörü\",\n      \"displaySize\": \"Görüntüleme Boyutu\",\n      \"documentSettings\": {\n        \"cursorColor\": \"Belge imleç rengi\",\n        \"selectionColor\": \"Belge seçim rengi\",\n        \"width\": \"Belge genişliği\",\n        \"changeWidth\": \"Değiştir\",\n        \"pickColor\": \"Bir renk seç\",\n        \"colorShade\": \"Renk tonu\",\n        \"opacity\": \"Opaklık\",\n        \"hexEmptyError\": \"Hex renk boş olamaz\",\n        \"hexLengthError\": \"Hex renk 6 haneli olmalıdır\",\n        \"hexInvalidError\": \"Geçersiz hex renk\",\n        \"opacityEmptyError\": \"Opaklık boş olamaz\",\n        \"opacityRangeError\": \"Opaklık 1 ile 100 arasında olmalıdır\",\n        \"app\": \"Uygulama\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"Uygula\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Düzen Yönü\",\n        \"hint\": \"Ekranınızdaki içeriğin akışını soldan sağa veya sağdan sola olarak kontrol edin.\",\n        \"ltr\": \"Soldan Sağa\",\n        \"rtl\": \"Sağdan Sola\"\n      },\n      \"textDirection\": {\n        \"label\": \"Varsayılan Metin Yönü\",\n        \"hint\": \"Metnin varsayılan olarak soldan mı yoksa sağdan mı başlayacağını belirtin.\",\n        \"ltr\": \"Soldan Sağa\",\n        \"rtl\": \"Sağdan Sola\",\n        \"auto\": \"OTOMATİK\",\n        \"fallback\": \"Düzen yönü ile aynı\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Yükle\",\n        \"uploadTheme\": \"Tema yükle\",\n        \"description\": \"Aşağıdaki düğmeyi kullanarak kendi @:appName temanızı yükleyin.\",\n        \"loading\": \"Temanızı doğrulayıp yüklerken lütfen bekleyin...\",\n        \"uploadSuccess\": \"Temanız başarıyla yüklendi\",\n        \"deletionFailure\": \"Tema silinemedi. Manuel olarak silmeyi deneyin.\",\n        \"filePickerDialogTitle\": \"Bir .flowy_plugin dosyası seçin\",\n        \"urlUploadFailure\": \"URL açılamadı: {}\"\n      },\n      \"theme\": \"Tema\",\n      \"builtInsLabel\": \"Yerleşik Temalar\",\n      \"pluginsLabel\": \"Eklentiler\",\n      \"dateFormat\": {\n        \"label\": \"Tarih biçimi\",\n        \"local\": \"Yerel\",\n        \"us\": \"ABD\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"Kullanıcı dostu\",\n        \"dmy\": \"G/A/Y\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Saat biçimi\",\n        \"twelveHour\": \"12 saat\",\n        \"twentyFourHour\": \"24 saat\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"Sayfa oluştururken adlandırma iletişim kutusunu göster\",\n      \"enableRTLToolbarItems\": \"Sağdan sola araç çubuğu öğelerini etkinleştir\",\n      \"members\": {\n        \"title\": \"Üye ayarları\",\n        \"inviteMembers\": \"Üye davet et\",\n        \"inviteHint\": \"E-posta ile davet et\",\n        \"sendInvite\": \"Davet gönder\",\n        \"copyInviteLink\": \"Davet bağlantısını kopyala\",\n        \"label\": \"Üyeler\",\n        \"user\": \"Kullanıcı\",\n        \"role\": \"Rol\",\n        \"removeFromWorkspace\": \"Çalışma Alanından Kaldır\",\n        \"removeFromWorkspaceSuccess\": \"Çalışma alanından başarıyla kaldırıldı\",\n        \"removeFromWorkspaceFailed\": \"Çalışma alanından kaldırma başarısız oldu\",\n        \"owner\": \"Sahip\",\n        \"guest\": \"Misafir\",\n        \"member\": \"Üye\",\n        \"memberHintText\": \"Bir üye sayfaları okuyabilir ve düzenleyebilir\",\n        \"guestHintText\": \"Bir Misafir okuyabilir, tepki verebilir, yorum yapabilir ve izin verilen belirli sayfaları düzenleyebilir.\",\n        \"emailInvalidError\": \"Geçersiz e-posta, lütfen kontrol edip tekrar deneyin\",\n        \"emailSent\": \"E-posta gönderildi, lütfen gelen kutunuzu kontrol edin\",\n        \"members\": \"üye\",\n        \"membersCount\": {\n          \"zero\": \"{} üye\",\n          \"one\": \"{} üye\",\n          \"other\": \"{} üye\"\n        },\n        \"inviteFailedDialogTitle\": \"Davet gönderilemedi\",\n        \"inviteFailedMemberLimit\": \"Üye sınırına ulaşıldı, daha fazla üye davet etmek için lütfen yükseltin.\",\n        \"inviteFailedMemberLimitMobile\": \"Çalışma alanınız üye sınırına ulaştı.\",\n        \"memberLimitExceeded\": \"Üye sınırına ulaşıldı, daha fazla üye davet etmek için lütfen \",\n        \"memberLimitExceededUpgrade\": \"yükseltin\",\n        \"memberLimitExceededPro\": \"Üye sınırına ulaşıldı, daha fazla üye gerekiyorsa \",\n        \"memberLimitExceededProContact\": \"support@appflowy.io\",\n        \"failedToAddMember\": \"Üye eklenemedi\",\n        \"addMemberSuccess\": \"Üye başarıyla eklendi\",\n        \"removeMember\": \"Üyeyi Kaldır\",\n        \"areYouSureToRemoveMember\": \"Bu üyeyi kaldırmak istediğinizden emin misiniz?\",\n        \"inviteMemberSuccess\": \"Davet başarıyla gönderildi\",\n        \"failedToInviteMember\": \"Üye davet edilemedi\",\n        \"workspaceMembersError\": \"Hay aksi, bir şeyler yanlış gitti\",\n        \"workspaceMembersErrorDescription\": \"Şu anda üye listesini yükleyemedik. Lütfen daha sonra tekrar deneyin\"\n      }\n    },\n    \"files\": {\n      \"copy\": \"Kopyala\",\n      \"defaultLocation\": \"Dosyaları ve veri depolama konumunu oku\",\n      \"exportData\": \"Verilerinizi dışa aktarın\",\n      \"doubleTapToCopy\": \"Yolu kopyalamak için çift dokunun\",\n      \"restoreLocation\": \"@:appName varsayılan yoluna geri yükle\",\n      \"customizeLocation\": \"Başka bir klasör aç\",\n      \"restartApp\": \"Değişikliklerin etkili olması için lütfen uygulamayı yeniden başlatın.\",\n      \"exportDatabase\": \"Veritabanını dışa aktar\",\n      \"selectFiles\": \"Dışa aktarılması gereken dosyaları seçin\",\n      \"selectAll\": \"Tümünü seç\",\n      \"deselectAll\": \"Tüm seçimi kaldır\",\n      \"createNewFolder\": \"Yeni klasör oluştur\",\n      \"createNewFolderDesc\": \"Verilerinizi nerede saklamak istediğinizi bize söyleyin\",\n      \"defineWhereYourDataIsStored\": \"Verilerinizin nerede saklanacağını tanımlayın\",\n      \"open\": \"Aç\",\n      \"openFolder\": \"Mevcut bir klasör aç\",\n      \"openFolderDesc\": \"Mevcut @:appName klasörünüzü okuyun ve yazın\",\n      \"folderHintText\": \"klasör adı\",\n      \"location\": \"Yeni klasör oluşturma\",\n      \"locationDesc\": \"@:appName veri klasörünüz için bir ad seçin\",\n      \"browser\": \"Göz at\",\n      \"create\": \"Oluştur\",\n      \"set\": \"Ayarla\",\n      \"folderPath\": \"Klasörünüzü saklamak için yol\",\n      \"locationCannotBeEmpty\": \"Yol boş olamaz\",\n      \"pathCopiedSnackbar\": \"Dosya depolama yolu panoya kopyalandı!\",\n      \"changeLocationTooltips\": \"Veri dizinini değiştir\",\n      \"change\": \"Değiştir\",\n      \"openLocationTooltips\": \"Başka bir veri dizini aç\",\n      \"openCurrentDataFolder\": \"Mevcut veri dizinini aç\",\n      \"recoverLocationTooltips\": \"@:appName'in varsayılan veri dizinine sıfırla\",\n      \"exportFileSuccess\": \"Dosya başarıyla dışa aktarıldı!\",\n      \"exportFileFail\": \"Dosya dışa aktarılamadı!\",\n      \"export\": \"Dışa aktar\",\n      \"clearCache\": \"Önbelleği temizle\",\n      \"clearCacheDesc\": \"Resimlerin yüklenmemesi veya yazı tiplerinin doğru görüntülenmemesi gibi sorunlarla karşılaşırsanız, önbelleği temizlemeyi deneyin. Bu işlem kullanıcı verilerinizi kaldırmayacaktır.\",\n      \"areYouSureToClearCache\": \"Önbelleği temizlemek istediğinizden emin misiniz?\",\n      \"clearCacheSuccess\": \"Önbellek başarıyla temizlendi!\"\n    },\n    \"user\": {\n      \"name\": \"Ad\",\n      \"email\": \"E-posta\",\n      \"tooltipSelectIcon\": \"Simge seç\",\n      \"selectAnIcon\": \"Bir simge seç\",\n      \"pleaseInputYourOpenAIKey\": \"lütfen Yapay Zeka anahtarınızı girin\",\n      \"clickToLogout\": \"Mevcut kullanıcının oturumunu kapatmak için tıklayın\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"Kişisel Bilgiler\",\n      \"username\": \"Kullanıcı Adı\",\n      \"usernameEmptyError\": \"Kullanıcı adı boş olamaz\",\n      \"about\": \"Hakkında\",\n      \"pushNotifications\": \"Anlık Bildirimler\",\n      \"support\": \"Destek\",\n      \"joinDiscord\": \"Discord'da bize katılın\",\n      \"privacyPolicy\": \"Gizlilik Politikası\",\n      \"userAgreement\": \"Kullanıcı Sözleşmesi\",\n      \"termsAndConditions\": \"Şartlar ve Koşullar\",\n      \"userprofileError\": \"Kullanıcı profili yüklenemedi\",\n      \"userprofileErrorDescription\": \"Lütfen sorunun devam edip etmediğini kontrol etmek için oturumu kapatıp yeniden giriş yapmayı deneyin.\",\n      \"selectLayout\": \"Düzen seç\",\n      \"selectStartingDay\": \"Başlangıç gününü seç\",\n      \"version\": \"Sürüm\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Bu görünümü silmek istediğinizden emin misiniz?\",\n    \"createView\": \"Yeni\",\n    \"title\": {\n      \"placeholder\": \"Başlıksız\"\n    },\n    \"settings\": {\n      \"filter\": \"Filtre\",\n      \"sort\": \"Sırala\",\n      \"sortBy\": \"Sıralama ölçütü\",\n      \"properties\": \"Özellikler\",\n      \"reorderPropertiesTooltip\": \"Özellikleri yeniden sıralamak için sürükleyin\",\n      \"group\": \"Grupla\",\n      \"addFilter\": \"Filtre Ekle\",\n      \"deleteFilter\": \"Filtreyi sil\",\n      \"filterBy\": \"Filtreleme ölçütü\",\n      \"typeAValue\": \"Bir değer yazın...\",\n      \"layout\": \"Düzen\",\n      \"databaseLayout\": \"Düzen\",\n      \"viewList\": {\n        \"zero\": \"0 görünüm\",\n        \"one\": \"{count} görünüm\",\n        \"other\": \"{count} görünüm\"\n      },\n      \"editView\": \"Görünümü Düzenle\",\n      \"boardSettings\": \"Pano ayarları\",\n      \"calendarSettings\": \"Takvim ayarları\",\n      \"createView\": \"Yeni görünüm\",\n      \"duplicateView\": \"Görünümü çoğalt\",\n      \"deleteView\": \"Görünümü sil\",\n      \"numberOfVisibleFields\": \"{} gösteriliyor\"\n    },\n    \"filter\": {\n      \"empty\": \"Aktif filtre yok\",\n      \"addFilter\": \"Filtre ekle\",\n      \"cannotFindCreatableField\": \"Filtrelenecek uygun bir alan bulunamadı\",\n      \"conditon\": \"Koşul\",\n      \"where\": \"Koşul\"\n    },\n    \"textFilter\": {\n      \"contains\": \"İçerir\",\n      \"doesNotContain\": \"İçermez\",\n      \"endsWith\": \"İle biter\",\n      \"startWith\": \"İle başlar\",\n      \"is\": \"Eşittir\",\n      \"isNot\": \"Eşit değildir\",\n      \"isEmpty\": \"Boştur\",\n      \"isNotEmpty\": \"Boş değildir\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Değil\",\n        \"startWith\": \"İle başlar\",\n        \"endWith\": \"İle biter\",\n        \"isEmpty\": \"boştur\",\n        \"isNotEmpty\": \"boş değildir\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"İşaretli\",\n      \"isUnchecked\": \"İşaretsiz\",\n      \"choicechipPrefix\": {\n        \"is\": \"eşittir\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"Tamamlandı\",\n      \"isIncomplted\": \"Tamamlanmadı\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"Eşittir\",\n      \"isNot\": \"Eşit değildir\",\n      \"contains\": \"İçerir\",\n      \"doesNotContain\": \"İçermez\",\n      \"isEmpty\": \"Boştur\",\n      \"isNotEmpty\": \"Boş değildir\"\n    },\n    \"dateFilter\": {\n      \"is\": \"Tarihinde\",\n      \"before\": \"Öncesinde\",\n      \"after\": \"Sonrasında\",\n      \"onOrBefore\": \"Tarihinde veya öncesinde\",\n      \"onOrAfter\": \"Tarihinde veya sonrasında\",\n      \"between\": \"Arasında\",\n      \"empty\": \"Boştur\",\n      \"notEmpty\": \"Boş değildir\",\n      \"startDate\": \"Başlangıç tarihi\",\n      \"endDate\": \"Bitiş tarihi\",\n      \"choicechipPrefix\": {\n        \"before\": \"Önce\",\n        \"after\": \"Sonra\",\n        \"between\": \"Arasında\",\n        \"onOrBefore\": \"Tarihinde veya önce\",\n        \"onOrAfter\": \"Tarihinde veya sonra\",\n        \"isEmpty\": \"Boştur\",\n        \"isNotEmpty\": \"Boş değildir\"\n      }\n    },\n    \"numberFilter\": {\n      \"equal\": \"Eşittir\",\n      \"notEqual\": \"Eşit değildir\",\n      \"lessThan\": \"Küçüktür\",\n      \"greaterThan\": \"Büyüktür\",\n      \"lessThanOrEqualTo\": \"Küçük veya eşittir\",\n      \"greaterThanOrEqualTo\": \"Büyük veya eşittir\",\n      \"isEmpty\": \"Boştur\",\n      \"isNotEmpty\": \"Boş değildir\"\n    },\n    \"field\": {\n      \"label\": \"Özellik\",\n      \"hide\": \"Özelliği gizle\",\n      \"show\": \"Özelliği göster\",\n      \"insertLeft\": \"Sola ekle\",\n      \"insertRight\": \"Sağa ekle\",\n      \"duplicate\": \"Çoğalt\",\n      \"delete\": \"Sil\",\n      \"wrapCellContent\": \"Metni kaydır\",\n      \"clear\": \"Hücreleri temizle\",\n      \"switchPrimaryFieldTooltip\": \"Birincil alanın alan türü değiştirilemez\",\n      \"textFieldName\": \"Metin\",\n      \"checkboxFieldName\": \"Onay kutusu\",\n      \"dateFieldName\": \"Tarih\",\n      \"updatedAtFieldName\": \"Son değiştirilme\",\n      \"createdAtFieldName\": \"Oluşturulma tarihi\",\n      \"numberFieldName\": \"Sayılar\",\n      \"singleSelectFieldName\": \"Seçim\",\n      \"multiSelectFieldName\": \"Çoklu seçim\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Kontrol listesi\",\n      \"relationFieldName\": \"İlişki\",\n      \"summaryFieldName\": \"Yapay Zeka Özeti\",\n      \"timeFieldName\": \"Saat\",\n      \"mediaFieldName\": \"Dosyalar ve medya\",\n      \"translateFieldName\": \"Yapay Zeka Çeviri\",\n      \"translateTo\": \"Şu dile çevir\",\n      \"numberFormat\": \"Sayı biçimi\",\n      \"dateFormat\": \"Tarih biçimi\",\n      \"includeTime\": \"Saati dahil et\",\n      \"isRange\": \"Bitiş tarihi\",\n      \"dateFormatFriendly\": \"Ay Gün, Yıl\",\n      \"dateFormatISO\": \"Yıl-Ay-Gün\",\n      \"dateFormatLocal\": \"Ay/Gün/Yıl\",\n      \"dateFormatUS\": \"Yıl/Ay/Gün\",\n      \"dateFormatDayMonthYear\": \"Gün/Ay/Yıl\",\n      \"timeFormat\": \"Saat biçimi\",\n      \"invalidTimeFormat\": \"Geçersiz biçim\",\n      \"timeFormatTwelveHour\": \"12 saat\",\n      \"timeFormatTwentyFourHour\": \"24 saat\",\n      \"clearDate\": \"Tarihi temizle\",\n      \"dateTime\": \"Tarih saat\",\n      \"startDateTime\": \"Başlangıç tarih saati\",\n      \"endDateTime\": \"Bitiş tarih saati\",\n      \"failedToLoadDate\": \"Tarih değeri yüklenemedi\",\n      \"selectTime\": \"Saat seç\",\n      \"selectDate\": \"Tarih seç\",\n      \"visibility\": \"Görünürlük\",\n      \"propertyType\": \"Özellik türü\",\n      \"addSelectOption\": \"Bir seçenek ekle\",\n      \"typeANewOption\": \"Yeni bir seçenek yazın\",\n      \"optionTitle\": \"Seçenekler\",\n      \"addOption\": \"Seçenek ekle\",\n      \"editProperty\": \"Özelliği düzenle\",\n      \"newProperty\": \"Yeni özellik\",\n      \"openRowDocument\": \"Sayfa olarak aç\",\n      \"deleteFieldPromptMessage\": \"Emin misiniz? Bu özellik ve tüm verileri silinecek\",\n      \"clearFieldPromptMessage\": \"Emin misiniz? Bu sütundaki tüm hücreler boşaltılacak\",\n      \"newColumn\": \"Yeni sütun\",\n      \"format\": \"Biçim\",\n      \"reminderOnDateTooltip\": \"Bu hücrede planlanmış bir hatırlatıcı var\",\n      \"optionAlreadyExist\": \"Seçenek zaten mevcut\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Yeni alan ekle\",\n      \"fieldDragElementTooltip\": \"Menüyü açmak için tıklayın\",\n      \"showHiddenFields\": {\n        \"one\": \"{count} gizli alanı göster\",\n        \"many\": \"{count} gizli alanı göster\",\n        \"other\": \"{count} gizli alanı göster\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"{count} gizli alanı gizle\",\n        \"many\": \"{count} gizli alanı gizle\",\n        \"other\": \"{count} gizli alanı gizle\"\n      },\n      \"openAsFullPage\": \"Tam sayfa olarak aç\",\n      \"moreRowActions\": \"Daha fazla satır işlemi\"\n    },\n    \"sort\": {\n      \"ascending\": \"Artan\",\n      \"descending\": \"Azalan\",\n      \"by\": \"Göre\",\n      \"empty\": \"Aktif sıralama yok\",\n      \"cannotFindCreatableField\": \"Sıralanacak uygun bir alan bulunamadı\",\n      \"deleteAllSorts\": \"Tüm sıralamaları sil\",\n      \"addSort\": \"Sıralama ekle\",\n      \"sortsActive\": \"Sıralama yaparken {intention} yapılamaz\",\n      \"removeSorting\": \"Bu görünümdeki tüm sıralamaları kaldırıp devam etmek istiyor musunuz?\",\n      \"fieldInUse\": \"Bu alana göre zaten sıralama yapıyorsunuz\"\n    },\n    \"row\": {\n      \"label\": \"Satır\",\n      \"duplicate\": \"Çoğalt\",\n      \"delete\": \"Sil\",\n      \"titlePlaceholder\": \"Başlıksız\",\n      \"textPlaceholder\": \"Boş\",\n      \"copyProperty\": \"Özellik panoya kopyalandı\",\n      \"count\": \"Sayı\",\n      \"newRow\": \"Yeni satır\",\n      \"loadMore\": \"Daha fazla yükle\",\n      \"action\": \"İşlem\",\n      \"add\": \"Aşağıya eklemek için tıklayın\",\n      \"drag\": \"Taşımak için sürükleyin\",\n      \"deleteRowPrompt\": \"Bu satırı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.\",\n      \"deleteCardPrompt\": \"Bu kartı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.\",\n      \"dragAndClick\": \"Taşımak için sürükleyin, menüyü açmak için tıklayın\",\n      \"insertRecordAbove\": \"Üste kayıt ekle\",\n      \"insertRecordBelow\": \"Alta kayıt ekle\",\n      \"noContent\": \"İçerik yok\",\n      \"reorderRowDescription\": \"satırı yeniden sırala\",\n      \"createRowAboveDescription\": \"üste bir satır oluştur\",\n      \"createRowBelowDescription\": \"alta bir satır ekle\"\n    },\n    \"selectOption\": {\n      \"create\": \"Oluştur\",\n      \"purpleColor\": \"Mor\",\n      \"pinkColor\": \"Pembe\",\n      \"lightPinkColor\": \"Açık Pembe\",\n      \"orangeColor\": \"Turuncu\",\n      \"yellowColor\": \"Sarı\",\n      \"limeColor\": \"Limon\",\n      \"greenColor\": \"Yeşil\",\n      \"aquaColor\": \"Su Mavisi\",\n      \"blueColor\": \"Mavi\",\n      \"deleteTag\": \"Etiketi sil\",\n      \"colorPanelTitle\": \"Renk\",\n      \"panelTitle\": \"Bir seçenek seçin veya oluşturun\",\n      \"searchOption\": \"Bir seçenek arayın\",\n      \"searchOrCreateOption\": \"Bir seçenek arayın veya oluşturun\",\n      \"createNew\": \"Yeni oluştur\",\n      \"orSelectOne\": \"Veya bir seçenek seçin\",\n      \"typeANewOption\": \"Yeni bir seçenek yazın\",\n      \"tagName\": \"Etiket adı\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"Görev açıklaması\",\n      \"addNew\": \"Yeni görev ekle\",\n      \"submitNewTask\": \"Oluştur\",\n      \"hideComplete\": \"Tamamlanan görevleri gizle\",\n      \"showComplete\": \"Tüm görevleri göster\"\n    },\n    \"url\": {\n      \"launch\": \"Bağlantıyı tarayıcıda aç\",\n      \"copy\": \"Bağlantıyı panoya kopyala\",\n      \"textFieldHint\": \"Bir URL girin\"\n    },\n    \"relation\": {\n      \"relatedDatabasePlaceLabel\": \"İlişkili Veritabanı\",\n      \"relatedDatabasePlaceholder\": \"Yok\",\n      \"inRelatedDatabase\": \"İçinde\",\n      \"rowSearchTextFieldPlaceholder\": \"Ara\",\n      \"noDatabaseSelected\": \"Veritabanı seçilmedi, lütfen aşağıdaki listeden önce bir tane seçin:\",\n      \"emptySearchResult\": \"Kayıt bulunamadı\",\n      \"linkedRowListLabel\": \"{count} bağlantılı satır\",\n      \"unlinkedRowListLabel\": \"Başka bir satır bağla\"\n    },\n    \"menuName\": \"Tablo\",\n    \"referencedGridPrefix\": \"Görünümü\",\n    \"calculate\": \"Hesapla\",\n    \"calculationTypeLabel\": {\n      \"none\": \"Yok\",\n      \"average\": \"Ortalama\",\n      \"max\": \"En büyük\",\n      \"median\": \"Medyan\",\n      \"min\": \"En küçük\",\n      \"sum\": \"Toplam\",\n      \"count\": \"Sayı\",\n      \"countEmpty\": \"Boş sayısı\",\n      \"countEmptyShort\": \"BOŞ\",\n      \"countNonEmpty\": \"Boş olmayan sayısı\",\n      \"countNonEmptyShort\": \"DOLU\"\n    },\n    \"media\": {\n      \"rename\": \"Yeniden adlandır\",\n      \"download\": \"İndir\",\n      \"expand\": \"Genişlet\",\n      \"delete\": \"Sil\",\n      \"moreFilesHint\": \"+{}\",\n      \"addFileOrImage\": \"Dosya veya bağlantı ekle\",\n      \"attachmentsHint\": \"{}\",\n      \"addFileMobile\": \"Dosya ekle\",\n      \"extraCount\": \"+{}\",\n      \"deleteFileDescription\": \"Bu dosyayı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.\",\n      \"showFileNames\": \"Dosya adını göster\",\n      \"downloadSuccess\": \"Dosya indirildi\",\n      \"downloadFailedToken\": \"Dosya indirilemedi, kullanıcı jetonu mevcut değil\",\n      \"setAsCover\": \"Kapak olarak ayarla\",\n      \"openInBrowser\": \"Tarayıcıda aç\",\n      \"embedLink\": \"Dosya bağlantısını yerleştir\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"Belge\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"creating\": \"Oluşturuluyor...\",\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Bağlanacak bir Pano seçin\",\n        \"createANewBoard\": \"Yeni bir Pano oluştur\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Bağlanacak bir Tablo seçin\",\n        \"createANewGrid\": \"Yeni bir Tablo oluştur\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Bağlanacak bir Takvim seçin\",\n        \"createANewCalendar\": \"Yeni bir Takvim oluştur\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"Bağlanacak bir Belge seçin\"\n      },\n      \"name\": {\n        \"text\": \"Metin\",\n        \"heading1\": \"Başlık 1\",\n        \"heading2\": \"Başlık 2\",\n        \"heading3\": \"Başlık 3\",\n        \"image\": \"Görsel\",\n        \"bulletedList\": \"Madde işaretli liste\",\n        \"numberedList\": \"Numaralı liste\",\n        \"todoList\": \"Yapılacaklar listesi\",\n        \"doc\": \"Belge\",\n        \"linkedDoc\": \"Sayfaya bağlantı\",\n        \"grid\": \"Tablo\",\n        \"linkedGrid\": \"Bağlantılı Tablo\",\n        \"kanban\": \"Kanban\",\n        \"linkedKanban\": \"Bağlantılı Kanban\",\n        \"calendar\": \"Takvim\",\n        \"linkedCalendar\": \"Bağlantılı Takvim\",\n        \"quote\": \"Alıntı\",\n        \"divider\": \"Ayırıcı\",\n        \"table\": \"Tablo\",\n        \"callout\": \"Not Kutusu\",\n        \"outline\": \"Ana Hat\",\n        \"mathEquation\": \"Matematik Denklemi\",\n        \"code\": \"Kod\",\n        \"toggleList\": \"Açılır liste\",\n        \"toggleHeading1\": \"Açılır başlık 1\",\n        \"toggleHeading2\": \"Açılır başlık 2\",\n        \"toggleHeading3\": \"Açılır başlık 3\",\n        \"emoji\": \"Emoji\",\n        \"aiWriter\": \"Yapay Zeka Yazar\",\n        \"dateOrReminder\": \"Tarih veya Hatırlatıcı\",\n        \"photoGallery\": \"Fotoğraf Galerisi\",\n        \"file\": \"Dosya\"\n      },\n      \"subPage\": {\n        \"name\": \"Belge\",\n        \"keyword1\": \"alt sayfa\",\n        \"keyword2\": \"sayfa\",\n        \"keyword3\": \"alt sayfa\",\n        \"keyword4\": \"sayfa ekle\",\n        \"keyword5\": \"sayfa yerleştir\",\n        \"keyword6\": \"yeni sayfa\",\n        \"keyword7\": \"sayfa oluştur\",\n        \"keyword8\": \"belge\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Ana Hat\",\n      \"codeBlock\": \"Kod Bloğu\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Referans Pano\",\n      \"referencedGrid\": \"Referans Tablo\",\n      \"referencedCalendar\": \"Referans Takvim\",\n      \"referencedDocument\": \"Referans Belge\",\n      \"autoGeneratorMenuItemName\": \"Yapay Zeka Yazar\",\n      \"autoGeneratorTitleName\": \"Yapay Zeka: Yapay zekadan herhangi bir şey yazmasını isteyin...\",\n      \"autoGeneratorLearnMore\": \"Daha fazla bilgi\",\n      \"autoGeneratorGenerate\": \"Oluştur\",\n      \"autoGeneratorHintText\": \"Yapay zekaya sorun ...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Yapay zeka anahtarı alınamıyor\",\n      \"autoGeneratorRewrite\": \"Yeniden yaz\",\n      \"smartEdit\": \"Yapay Zekaya Sor\",\n      \"aI\": \"Yapay Zeka\",\n      \"smartEditFixSpelling\": \"Yazım ve dilbilgisini düzelt\",\n      \"warning\": \"⚠️ Yapay zeka yanıtları yanlış veya yanıltıcı olabilir.\",\n      \"smartEditSummarize\": \"Özetle\",\n      \"smartEditImproveWriting\": \"Yazımı geliştir\",\n      \"smartEditMakeLonger\": \"Daha uzun yap\",\n      \"smartEditCouldNotFetchResult\": \"Yapay zekadan sonuç alınamadı\",\n      \"smartEditCouldNotFetchKey\": \"Yapay zeka anahtarı alınamadı\",\n      \"smartEditDisabled\": \"Ayarlar'dan Yapay Zeka'yı bağlayın\",\n      \"appflowyAIEditDisabled\": \"Yapay Zeka özelliklerini etkinleştirmek için giriş yapın\",\n      \"discardResponse\": \"Yapay Zeka yanıtlarını silmek istiyor musunuz?\",\n      \"createInlineMathEquation\": \"Denklem oluştur\",\n      \"fonts\": \"Yazı tipleri\",\n      \"insertDate\": \"Tarih ekle\",\n      \"emoji\": \"Emoji\",\n      \"toggleList\": \"Açılır liste\",\n      \"emptyToggleHeading\": \"Boş açılır başlık {}. İçerik eklemek için tıklayın.\",\n      \"emptyToggleList\": \"Boş açılır liste. İçerik eklemek için tıklayın.\",\n      \"emptyToggleHeadingWeb\": \"Boş açılır başlık {level}. İçerik eklemek için tıklayın\",\n      \"quoteList\": \"Alıntı listesi\",\n      \"numberedList\": \"Numaralı liste\",\n      \"bulletedList\": \"Madde işaretli liste\",\n      \"todoList\": \"Yapılacaklar listesi\",\n      \"callout\": \"Not Kutusu\",\n      \"simpleTable\": {\n        \"moreActions\": {\n          \"color\": \"Renk\",\n          \"align\": \"Hizala\",\n          \"delete\": \"Sil\",\n          \"duplicate\": \"Çoğalt\",\n          \"insertLeft\": \"Sola ekle\",\n          \"insertRight\": \"Sağa ekle\",\n          \"insertAbove\": \"Üste ekle\",\n          \"insertBelow\": \"Alta ekle\",\n          \"headerColumn\": \"Başlık sütunu\",\n          \"headerRow\": \"Başlık satırı\",\n          \"clearContents\": \"İçeriği temizle\",\n          \"setToPageWidth\": \"Sayfa genişliğine ayarla\",\n          \"distributeColumnsWidth\": \"Sütunları eşit dağıt\",\n          \"duplicateRow\": \"Satırı çoğalt\",\n          \"duplicateColumn\": \"Sütunu çoğalt\",\n          \"textColor\": \"Metin rengi\",\n          \"cellBackgroundColor\": \"Hücre arka plan rengi\",\n          \"duplicateTable\": \"Tabloyu çoğalt\"\n        },\n        \"clickToAddNewRow\": \"Yeni satır eklemek için tıklayın\",\n        \"clickToAddNewColumn\": \"Yeni sütun eklemek için tıklayın\",\n        \"clickToAddNewRowAndColumn\": \"Yeni satır ve sütun eklemek için tıklayın\",\n        \"headerName\": {\n          \"table\": \"Tablo\",\n          \"alignText\": \"Metni hizala\"\n        }\n      },\n      \"cover\": {\n        \"changeCover\": \"Kapağı Değiştir\",\n        \"colors\": \"Renkler\",\n        \"images\": \"Görseller\",\n        \"clearAll\": \"Tümünü Temizle\",\n        \"abstract\": \"Soyut\",\n        \"addCover\": \"Kapak Ekle\",\n        \"addLocalImage\": \"Yerel görsel ekle\",\n        \"invalidImageUrl\": \"Geçersiz görsel URL'si\",\n        \"failedToAddImageToGallery\": \"Görsel galeriye eklenemedi\",\n        \"enterImageUrl\": \"Görsel URL'si girin\",\n        \"add\": \"Ekle\",\n        \"back\": \"Geri\",\n        \"saveToGallery\": \"Galeriye kaydet\",\n        \"removeIcon\": \"Simgeyi kaldır\",\n        \"removeCover\": \"Kapağı kaldır\",\n        \"pasteImageUrl\": \"Görsel URL'sini yapıştırın\",\n        \"or\": \"VEYA\",\n        \"pickFromFiles\": \"Dosyalardan seç\",\n        \"couldNotFetchImage\": \"Görsel alınamadı\",\n        \"imageSavingFailed\": \"Görsel Kaydedilemedi\",\n        \"addIcon\": \"Simge ekle\",\n        \"changeIcon\": \"Simgeyi değiştir\",\n        \"coverRemoveAlert\": \"Silindikten sonra kapaktan kaldırılacaktır.\",\n        \"alertDialogConfirmation\": \"Devam etmek istediğinizden emin misiniz?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"Matematik Denklemi\",\n        \"addMathEquation\": \"Bir TeX denklemi ekle\",\n        \"editMathEquation\": \"Matematik Denklemini Düzenle\"\n      },\n      \"optionAction\": {\n        \"click\": \"Tıkla\",\n        \"toOpenMenu\": \" menüyü açmak için\",\n        \"drag\": \"Sürükle\",\n        \"toMove\": \" taşımak için\",\n        \"delete\": \"Sil\",\n        \"duplicate\": \"Çoğalt\",\n        \"turnInto\": \"Dönüştür\",\n        \"moveUp\": \"Yukarı taşı\",\n        \"moveDown\": \"Aşağı taşı\",\n        \"color\": \"Renk\",\n        \"align\": \"Hizala\",\n        \"left\": \"Sol\",\n        \"center\": \"Orta\",\n        \"right\": \"Sağ\",\n        \"defaultColor\": \"Varsayılan\",\n        \"depth\": \"Derinlik\",\n        \"copyLinkToBlock\": \"Bloğa bağlantıyı kopyala\"\n      },\n      \"image\": {\n        \"addAnImage\": \"Görsel ekle\",\n        \"copiedToPasteBoard\": \"Görsel bağlantısı panoya kopyalandı\",\n        \"addAnImageDesktop\": \"Bir görsel ekle\",\n        \"addAnImageMobile\": \"Bir veya daha fazla görsel eklemek için tıklayın\",\n        \"dropImageToInsert\": \"Eklemek için görselleri bırakın\",\n        \"imageUploadFailed\": \"Görsel yüklenemedi\",\n        \"imageDownloadFailed\": \"Görsel indirilemedi, lütfen tekrar deneyin\",\n        \"imageDownloadFailedToken\": \"Kullanıcı jetonu eksik olduğu için görsel indirilemedi, lütfen tekrar deneyin\",\n        \"errorCode\": \"Hata kodu\"\n      },\n      \"photoGallery\": {\n        \"name\": \"Fotoğraf galerisi\",\n        \"imageKeyword\": \"görsel\",\n        \"imageGalleryKeyword\": \"görsel galerisi\",\n        \"photoKeyword\": \"fotoğraf\",\n        \"photoBrowserKeyword\": \"fotoğraf tarayıcısı\",\n        \"galleryKeyword\": \"galeri\",\n        \"addImageTooltip\": \"Görsel ekle\",\n        \"changeLayoutTooltip\": \"Düzeni değiştir\",\n        \"browserLayout\": \"Tarayıcı\",\n        \"gridLayout\": \"Izgara\",\n        \"deleteBlockTooltip\": \"Tüm galeriyi sil\"\n      },\n      \"math\": {\n        \"copiedToPasteBoard\": \"Matematik denklemi panoya kopyalandı\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"Bağlantı panoya kopyalandı\",\n        \"convertToLink\": \"Yerleşik bağlantıya dönüştür\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"İçindekiler tablosu oluşturmak için başlıklar ekleyin.\",\n        \"noMatchHeadings\": \"Eşleşen başlık bulunamadı.\"\n      },\n      \"table\": {\n        \"addAfter\": \"Sonrasına ekle\",\n        \"addBefore\": \"Öncesine ekle\",\n        \"delete\": \"Sil\",\n        \"clear\": \"İçeriği temizle\",\n        \"duplicate\": \"Çoğalt\",\n        \"bgColor\": \"Arka plan rengi\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Kopyala\",\n        \"cut\": \"Kes\",\n        \"paste\": \"Yapıştır\",\n        \"pasteAsPlainText\": \"Düz metin olarak yapıştır\"\n      },\n      \"action\": \"İşlemler\",\n      \"database\": {\n        \"selectDataSource\": \"Veri kaynağı seç\",\n        \"noDataSource\": \"Veri kaynağı yok\",\n        \"selectADataSource\": \"Bir veri kaynağı seç\",\n        \"toContinue\": \"devam etmek için\",\n        \"newDatabase\": \"Yeni Veritabanı\",\n        \"linkToDatabase\": \"Veritabanına Bağlantı\"\n      },\n      \"date\": \"Tarih\",\n      \"video\": {\n        \"label\": \"Video\",\n        \"emptyLabel\": \"Video ekle\",\n        \"placeholder\": \"Video bağlantısını yapıştırın\",\n        \"copiedToPasteBoard\": \"Video bağlantısı panoya kopyalandı\",\n        \"insertVideo\": \"Video ekle\",\n        \"invalidVideoUrl\": \"Kaynak URL'si henüz desteklenmiyor.\",\n        \"invalidVideoUrlYouTube\": \"YouTube henüz desteklenmiyor.\",\n        \"supportedFormats\": \"Desteklenen formatlar: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264\"\n      },\n      \"file\": {\n        \"name\": \"Dosya\",\n        \"uploadTab\": \"Yükle\",\n        \"uploadMobile\": \"Bir dosya seç\",\n        \"uploadMobileGallery\": \"Fotoğraf Galerisinden\",\n        \"networkTab\": \"Bağlantı yerleştir\",\n        \"placeholderText\": \"Bir dosya yükleyin veya yerleştirin\",\n        \"placeholderDragging\": \"Yüklemek için dosyayı bırakın\",\n        \"dropFileToUpload\": \"Yüklemek için bir dosya bırakın\",\n        \"fileUploadHint\": \"Bir dosya sürükleyip bırakın veya tıklayarak \",\n        \"fileUploadHintSuffix\": \"Göz atın\",\n        \"networkHint\": \"Bir dosya bağlantısı yapıştırın\",\n        \"networkUrlInvalid\": \"Geçersiz URL. URL'yi kontrol edip tekrar deneyin.\",\n        \"networkAction\": \"Yerleştir\",\n        \"fileTooBigError\": \"Dosya boyutu çok büyük, lütfen 10MB'dan küçük bir dosya yükleyin\",\n        \"renameFile\": {\n          \"title\": \"Dosyayı yeniden adlandır\",\n          \"description\": \"Bu dosya için yeni bir ad girin\",\n          \"nameEmptyError\": \"Dosya adı boş bırakılamaz.\"\n        },\n        \"uploadedAt\": \"{} tarihinde yüklendi\",\n        \"linkedAt\": \"Bağlantısı {} tarihinde eklendi\",\n        \"failedToOpenMsg\": \"Açılamadı, dosya bulunamadı\"\n      },\n      \"subPage\": {\n        \"handlingPasteHint\": \" - (yapıştırma işlemi)\",\n        \"errors\": {\n          \"failedDeletePage\": \"Sayfa silinemedi\",\n          \"failedCreatePage\": \"Sayfa oluşturulamadı\",\n          \"failedMovePage\": \"Sayfa bu belgeye taşınamadı\",\n          \"failedDuplicatePage\": \"Sayfa çoğaltılamadı\",\n          \"failedDuplicateFindView\": \"Sayfa çoğaltılamadı - orijinal görünüm bulunamadı\"\n        }\n      },\n      \"cannotMoveToItsChildren\": \"Alt öğelerine taşınamaz\"\n    },\n    \"outlineBlock\": {\n      \"placeholder\": \"İçindekiler\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Komutlar için '/' yazın\"\n    },\n    \"title\": {\n      \"placeholder\": \"Başlıksız\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Görsel eklemek için tıklayın\",\n      \"upload\": {\n        \"label\": \"Yükle\",\n        \"placeholder\": \"Görsel yüklemek için tıklayın\"\n      },\n      \"url\": {\n        \"label\": \"Görsel URL'si\",\n        \"placeholder\": \"Görsel URL'si girin\"\n      },\n      \"ai\": {\n        \"label\": \"Yapay zeka ile görsel oluştur\",\n        \"placeholder\": \"Yapay zekanın görsel oluşturması için bir istek girin\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Stability AI ile görsel oluştur\",\n        \"placeholder\": \"Stability AI'nın görsel oluşturması için bir istek girin\"\n      },\n      \"support\": \"Görsel boyut sınırı 5MB'dır. Desteklenen formatlar: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Geçersiz görsel\",\n        \"invalidImageSize\": \"Görsel boyutu 5MB'dan küçük olmalıdır\",\n        \"invalidImageFormat\": \"Görsel formatı desteklenmiyor. Desteklenen formatlar: JPEG, PNG, JPG, GIF, SVG, WEBP\",\n        \"invalidImageUrl\": \"Geçersiz görsel URL'si\",\n        \"noImage\": \"Böyle bir dosya veya dizin yok\",\n        \"multipleImagesFailed\": \"Bir veya daha fazla görsel yüklenemedi, lütfen tekrar deneyin\"\n      },\n      \"embedLink\": {\n        \"label\": \"Bağlantı yerleştir\",\n        \"placeholder\": \"Bir görsel bağlantısı yapıştırın veya yazın\"\n      },\n      \"unsplash\": {\n        \"label\": \"Unsplash\"\n      },\n      \"searchForAnImage\": \"Bir görsel ara\",\n      \"pleaseInputYourOpenAIKey\": \"lütfen Ayarlar sayfasından yapay zeka anahtarınızı girin\",\n      \"saveImageToGallery\": \"Görseli kaydet\",\n      \"failedToAddImageToGallery\": \"Görsel galeriye eklenemedi\",\n      \"successToAddImageToGallery\": \"Görsel başarıyla galeriye eklendi\",\n      \"unableToLoadImage\": \"Görsel yüklenemedi\",\n      \"maximumImageSize\": \"Desteklenen maksimum görsel yükleme boyutu 10MB'dır\",\n      \"uploadImageErrorImageSizeTooBig\": \"Görsel boyutu 10MB'dan küçük olmalıdır\",\n      \"imageIsUploading\": \"Görsel yükleniyor\",\n      \"openFullScreen\": \"Tam ekran aç\",\n      \"interactiveViewer\": {\n        \"toolbar\": {\n          \"previousImageTooltip\": \"Önceki görsel\",\n          \"nextImageTooltip\": \"Sonraki görsel\",\n          \"zoomOutTooltip\": \"Uzaklaştır\",\n          \"zoomInTooltip\": \"Yakınlaştır\",\n          \"changeZoomLevelTooltip\": \"Yakınlaştırma seviyesini değiştir\",\n          \"openLocalImage\": \"Görseli aç\",\n          \"downloadImage\": \"Görseli indir\",\n          \"closeViewer\": \"Etkileşimli görüntüleyiciyi kapat\",\n          \"scalePercentage\": \"%{}\",\n          \"deleteImageTooltip\": \"Görseli sil\"\n        }\n      }\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Dil\",\n        \"placeholder\": \"Dil seçin\",\n        \"auto\": \"Otomatik\"\n      },\n      \"copyTooltip\": \"Kopyala\",\n      \"searchLanguageHint\": \"Bir dil arayın\",\n      \"codeCopiedSnackbar\": \"Kod panoya kopyalandı!\"\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Bir bağlantı yapıştırın veya yazın\",\n      \"openInNewTab\": \"Yeni sekmede aç\",\n      \"copyLink\": \"Bağlantıyı kopyala\",\n      \"removeLink\": \"Bağlantıyı kaldır\",\n      \"url\": {\n        \"label\": \"Bağlantı URL'si\",\n        \"placeholder\": \"Bağlantı URL'si girin\"\n      },\n      \"title\": {\n        \"label\": \"Bağlantı Başlığı\",\n        \"placeholder\": \"Bağlantı başlığı girin\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"Bir kişiden, sayfadan veya tarihten bahsedin...\",\n      \"page\": {\n        \"label\": \"Sayfaya bağlantı\",\n        \"tooltip\": \"Sayfayı açmak için tıklayın\"\n      },\n      \"deleted\": \"Silindi\",\n      \"deletedContent\": \"Bu içerik mevcut değil veya silinmiş\",\n      \"noAccess\": \"Erişim Yok\",\n      \"deletedPage\": \"Silinmiş sayfa\",\n      \"trashHint\": \" - çöp kutusunda\",\n      \"morePages\": \"daha fazla sayfa\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"Varsayılana sıfırla\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"Mevcut sürüm bu Bloğu desteklemiyor.\",\n      \"clickToCopyTheBlockContent\": \"Blok içeriğini kopyalamak için tıklayın\",\n      \"blockContentHasBeenCopied\": \"Blok içeriği kopyalandı.\",\n      \"parseError\": \"{} bloğu ayrıştırılırken bir hata oluştu.\",\n      \"copyBlockContent\": \"Blok içeriğini kopyala\"\n    },\n    \"mobilePageSelector\": {\n      \"title\": \"Sayfa seç\",\n      \"failedToLoad\": \"Sayfa listesi yüklenemedi\",\n      \"noPagesFound\": \"Sayfa bulunamadı\"\n    },\n    \"attachmentMenu\": {\n      \"choosePhoto\": \"Fotoğraf seç\",\n      \"takePicture\": \"Fotoğraf çek\",\n      \"chooseFile\": \"Dosya seç\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"label\": \"Sütun\",\n      \"createNewCard\": \"Yeni\",\n      \"renameGroupTooltip\": \"Grubu yeniden adlandırmak için basın\",\n      \"createNewColumn\": \"Yeni bir grup ekle\",\n      \"addToColumnTopTooltip\": \"Üste yeni bir kart ekle\",\n      \"addToColumnBottomTooltip\": \"Alta yeni bir kart ekle\",\n      \"renameColumn\": \"Yeniden adlandır\",\n      \"hideColumn\": \"Gizle\",\n      \"newGroup\": \"Yeni grup\",\n      \"deleteColumn\": \"Sil\",\n      \"deleteColumnConfirmation\": \"Bu işlem bu grubu ve içindeki tüm kartları silecektir. Devam etmek istediğinizden emin misiniz?\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"Gizli Gruplar\",\n      \"collapseTooltip\": \"Gizli grupları gizle\",\n      \"expandTooltip\": \"Gizli grupları görüntüle\"\n    },\n    \"cardDetail\": \"Kart Detayı\",\n    \"cardActions\": \"Kart İşlemleri\",\n    \"cardDuplicated\": \"Kart çoğaltıldı\",\n    \"cardDeleted\": \"Kart silindi\",\n    \"showOnCard\": \"Kart detayında göster\",\n    \"setting\": \"Ayar\",\n    \"propertyName\": \"Özellik adı\",\n    \"menuName\": \"Pano\",\n    \"showUngrouped\": \"Gruplanmamış öğeleri göster\",\n    \"ungroupedButtonText\": \"Gruplanmamış\",\n    \"ungroupedButtonTooltip\": \"Herhangi bir gruba ait olmayan kartları içerir\",\n    \"ungroupedItemsTitle\": \"Panoya eklemek için tıklayın\",\n    \"groupBy\": \"Grupla\",\n    \"groupCondition\": \"Gruplama koşulu\",\n    \"referencedBoardPrefix\": \"Görünümü\",\n    \"notesTooltip\": \"İçerideki notlar\",\n    \"mobile\": {\n      \"editURL\": \"URL'yi düzenle\",\n      \"showGroup\": \"Grubu göster\",\n      \"showGroupContent\": \"Bu grubu panoda göstermek istediğinizden emin misiniz?\",\n      \"failedToLoad\": \"Pano görünümü yüklenemedi\"\n    },\n    \"dateCondition\": {\n      \"weekOf\": \"{} - {} haftası\",\n      \"today\": \"Bugün\",\n      \"yesterday\": \"Dün\",\n      \"tomorrow\": \"Yarın\",\n      \"lastSevenDays\": \"Son 7 gün\",\n      \"nextSevenDays\": \"Gelecek 7 gün\",\n      \"lastThirtyDays\": \"Son 30 gün\",\n      \"nextThirtyDays\": \"Gelecek 30 gün\"\n    },\n    \"noGroup\": \"Özelliğe göre gruplama yok\",\n    \"noGroupDesc\": \"Pano görünümleri görüntülemek için gruplamak üzere bir özellik gerektirir\",\n    \"media\": {\n      \"cardText\": \"{} {}\",\n      \"fallbackName\": \"dosyalar\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Takvim\",\n    \"defaultNewCalendarTitle\": \"Başlıksız\",\n    \"newEventButtonTooltip\": \"Yeni etkinlik ekle\",\n    \"navigation\": {\n      \"today\": \"Bugün\",\n      \"jumpToday\": \"Bugüne git\",\n      \"previousMonth\": \"Önceki Ay\",\n      \"nextMonth\": \"Sonraki Ay\",\n      \"views\": {\n        \"day\": \"Gün\",\n        \"week\": \"Hafta\",\n        \"month\": \"Ay\",\n        \"year\": \"Yıl\"\n      }\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"Henüz etkinlik yok\",\n      \"emptyBody\": \"Bu güne etkinlik eklemek için artı düğmesine basın.\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Hafta numaralarını göster\",\n      \"showWeekends\": \"Hafta sonlarını göster\",\n      \"firstDayOfWeek\": \"Haftanın başlangıç günü\",\n      \"layoutDateField\": \"Takvimi şuna göre düzenle\",\n      \"changeLayoutDateField\": \"Düzen alanını değiştir\",\n      \"noDateTitle\": \"Tarih Yok\",\n      \"noDateHint\": {\n        \"zero\": \"Planlanmamış etkinlikler burada görünecek\",\n        \"one\": \"{count} planlanmamış etkinlik\",\n        \"other\": \"{count} planlanmamış etkinlik\"\n      },\n      \"unscheduledEventsTitle\": \"Planlanmamış etkinlikler\",\n      \"clickToAdd\": \"Takvime eklemek için tıklayın\",\n      \"name\": \"Takvim ayarları\",\n      \"clickToOpen\": \"Kaydı açmak için tıklayın\"\n    },\n    \"referencedCalendarPrefix\": \"Görünümü\",\n    \"quickJumpYear\": \"Şuraya git\",\n    \"duplicateEvent\": \"Etkinliği çoğalt\"\n  },\n  \"errorDialog\": {\n    \"title\": \"@:appName Hatası\",\n    \"howToFixFallback\": \"Rahatsızlık için özür dileriz! GitHub sayfamızda hatanızı açıklayan bir sorun bildirin.\",\n    \"howToFixFallbackHint1\": \"Rahatsızlık için özür dileriz! \",\n    \"howToFixFallbackHint2\": \" sayfamızda hatanızı açıklayan bir sorun bildirin.\",\n    \"github\": \"GitHub'da görüntüle\"\n  },\n  \"search\": {\n    \"label\": \"Ara\",\n    \"sidebarSearchIcon\": \"Ara ve hızlıca bir sayfaya git\",\n    \"placeholder\": {\n      \"actions\": \"Eylemleri ara...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Kopyalandı!\",\n      \"fail\": \"Kopyalanamadı\"\n    }\n  },\n  \"unSupportBlock\": \"Mevcut sürüm bu Bloğu desteklemiyor.\",\n  \"views\": {\n    \"deleteContentTitle\": \"{pageType} silmek istediğinizden emin misiniz?\",\n    \"deleteContentCaption\": \"Bu {pageType} silerseniz, çöp kutusundan geri yükleyebilirsiniz.\"\n  },\n  \"colors\": {\n    \"custom\": \"Özel\",\n    \"default\": \"Varsayılan\",\n    \"red\": \"Kırmızı\",\n    \"orange\": \"Turuncu\",\n    \"yellow\": \"Sarı\",\n    \"green\": \"Yeşil\",\n    \"blue\": \"Mavi\",\n    \"purple\": \"Mor\",\n    \"pink\": \"Pembe\",\n    \"brown\": \"Kahverengi\",\n    \"gray\": \"Gri\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"Emoji\",\n    \"search\": \"Emoji ara\",\n    \"noRecent\": \"Son kullanılan emoji yok\",\n    \"noEmojiFound\": \"Emoji bulunamadı\",\n    \"filter\": \"Filtrele\",\n    \"random\": \"Rastgele\",\n    \"selectSkinTone\": \"Ten rengi seç\",\n    \"remove\": \"Emojiyi kaldır\",\n    \"categories\": {\n      \"smileys\": \"İfadeler ve Duygular\",\n      \"people\": \"insanlar\",\n      \"animals\": \"doğa\",\n      \"food\": \"yiyecekler\",\n      \"activities\": \"aktiviteler\",\n      \"places\": \"yerler\",\n      \"objects\": \"nesneler\",\n      \"symbols\": \"semboller\",\n      \"flags\": \"bayraklar\",\n      \"nature\": \"doğa\",\n      \"frequentlyUsed\": \"sık kullanılanlar\"\n    },\n    \"skinTone\": {\n      \"default\": \"Varsayılan\",\n      \"light\": \"Açık\",\n      \"mediumLight\": \"Orta-Açık\",\n      \"medium\": \"Orta\",\n      \"mediumDark\": \"Orta-Koyu\",\n      \"dark\": \"Koyu\"\n    },\n    \"openSourceIconsFrom\": \"Açık kaynak ikonlar\"\n  },\n  \"inlineActions\": {\n    \"noResults\": \"Sonuç yok\",\n    \"recentPages\": \"Son sayfalar\",\n    \"pageReference\": \"Sayfa referansı\",\n    \"docReference\": \"Belge referansı\",\n    \"boardReference\": \"Pano referansı\",\n    \"calReference\": \"Takvim referansı\",\n    \"gridReference\": \"Tablo referansı\",\n    \"date\": \"Tarih\",\n    \"reminder\": {\n      \"groupTitle\": \"Hatırlatıcı\",\n      \"shortKeyword\": \"hatırlat\"\n    },\n    \"createPage\": \"\\\"{}\\\"-alt sayfası oluştur\"\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"Tarih ve saat biçimini ayarlardan değiştirin\",\n    \"dateFormat\": \"Tarih biçimi\",\n    \"includeTime\": \"Saati dahil et\",\n    \"isRange\": \"Bitiş tarihi\",\n    \"timeFormat\": \"Saat biçimi\",\n    \"clearDate\": \"Tarihi temizle\",\n    \"reminderLabel\": \"Hatırlatıcı\",\n    \"selectReminder\": \"Hatırlatıcı seç\",\n    \"reminderOptions\": {\n      \"none\": \"Yok\",\n      \"atTimeOfEvent\": \"Etkinlik zamanında\",\n      \"fiveMinsBefore\": \"5 dakika önce\",\n      \"tenMinsBefore\": \"10 dakika önce\",\n      \"fifteenMinsBefore\": \"15 dakika önce\",\n      \"thirtyMinsBefore\": \"30 dakika önce\",\n      \"oneHourBefore\": \"1 saat önce\",\n      \"twoHoursBefore\": \"2 saat önce\",\n      \"onDayOfEvent\": \"Etkinlik günü\",\n      \"oneDayBefore\": \"1 gün önce\",\n      \"twoDaysBefore\": \"2 gün önce\",\n      \"oneWeekBefore\": \"1 hafta önce\",\n      \"custom\": \"Özel\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"Dün\",\n    \"today\": \"Bugün\",\n    \"tomorrow\": \"Yarın\",\n    \"oneWeek\": \"1 hafta\"\n  },\n  \"notificationHub\": {\n    \"title\": \"Bildirimler\",\n    \"mobile\": {\n      \"title\": \"Güncellemeler\"\n    },\n    \"emptyTitle\": \"Hepsi tamamlandı!\",\n    \"emptyBody\": \"Bekleyen bildirim veya eylem yok. Huzurun tadını çıkarın.\",\n    \"tabs\": {\n      \"inbox\": \"Gelen Kutusu\",\n      \"upcoming\": \"Yaklaşan\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"Tümünü okundu olarak işaretle\",\n      \"showAll\": \"Tümü\",\n      \"showUnreads\": \"Okunmamış\"\n    },\n    \"filters\": {\n      \"ascending\": \"Artan\",\n      \"descending\": \"Azalan\",\n      \"groupByDate\": \"Tarihe göre grupla\",\n      \"showUnreadsOnly\": \"Sadece okunmamışları göster\",\n      \"resetToDefault\": \"Varsayılana sıfırla\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"Hatırlatıcı\",\n    \"message\": \"Unutmadan önce bunu kontrol etmeyi unutmayın!\",\n    \"tooltipDelete\": \"Sil\",\n    \"tooltipMarkRead\": \"Okundu olarak işaretle\",\n    \"tooltipMarkUnread\": \"Okunmadı olarak işaretle\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"Bul\",\n    \"previousMatch\": \"Önceki eşleşme\",\n    \"nextMatch\": \"Sonraki eşleşme\",\n    \"close\": \"Kapat\",\n    \"replace\": \"Değiştir\",\n    \"replaceAll\": \"Tümünü değiştir\",\n    \"noResult\": \"Sonuç yok\",\n    \"caseSensitive\": \"Büyük/küçük harf duyarlı\",\n    \"searchMore\": \"Daha fazla sonuç bulmak için arama yapın\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"Üzgünüz\",\n    \"loadingViewError\": \"Bu görünümü yüklerken sorun yaşıyoruz. Lütfen internet bağlantınızı kontrol edin, uygulamayı yenileyin ve sorun devam ederse ekiple iletişime geçmekten çekinmeyin.\",\n    \"syncError\": \"Veriler başka bir cihazdan senkronize edilmedi\",\n    \"syncErrorHint\": \"Lütfen bu sayfayı son düzenlemenin yapıldığı cihazda yeniden açın, ardından mevcut cihazda tekrar açın.\",\n    \"clickToCopy\": \"Hata kodunu kopyalamak için tıklayın\"\n  },\n  \"editor\": {\n    \"bold\": \"Kalın\",\n    \"bulletedList\": \"Madde işaretli liste\",\n    \"bulletedListShortForm\": \"Madde işaretli\",\n    \"checkbox\": \"Onay kutusu\",\n    \"embedCode\": \"Kod Yerleştir\",\n    \"heading1\": \"H1\",\n    \"heading2\": \"H2\",\n    \"heading3\": \"H3\",\n    \"highlight\": \"Vurgula\",\n    \"color\": \"Renk\",\n    \"image\": \"Görsel\",\n    \"date\": \"Tarih\",\n    \"page\": \"Sayfa\",\n    \"italic\": \"İtalik\",\n    \"link\": \"Bağlantı\",\n    \"numberedList\": \"Numaralı liste\",\n    \"numberedListShortForm\": \"Numaralı\",\n    \"toggleHeading1ShortForm\": \"B1'i aç/kapat\",\n    \"toggleHeading2ShortForm\": \"B2'yi aç/kapat\",\n    \"toggleHeading3ShortForm\": \"B3'ü aç/kapat\",\n    \"quote\": \"Alıntı\",\n    \"strikethrough\": \"Üstü çizili\",\n    \"text\": \"Metin\",\n    \"underline\": \"Altı çizili\",\n    \"fontColorDefault\": \"Varsayılan\",\n    \"fontColorGray\": \"Gri\",\n    \"fontColorBrown\": \"Kahverengi\",\n    \"fontColorOrange\": \"Turuncu\",\n    \"fontColorYellow\": \"Sarı\",\n    \"fontColorGreen\": \"Yeşil\",\n    \"fontColorBlue\": \"Mavi\",\n    \"fontColorPurple\": \"Mor\",\n    \"fontColorPink\": \"Pembe\",\n    \"fontColorRed\": \"Kırmızı\",\n    \"backgroundColorDefault\": \"Varsayılan arka plan\",\n    \"backgroundColorGray\": \"Gri arka plan\",\n    \"backgroundColorBrown\": \"Kahverengi arka plan\",\n    \"backgroundColorOrange\": \"Turuncu arka plan\",\n    \"backgroundColorYellow\": \"Sarı arka plan\",\n    \"backgroundColorGreen\": \"Yeşil arka plan\",\n    \"backgroundColorBlue\": \"Mavi arka plan\",\n    \"backgroundColorPurple\": \"Mor arka plan\",\n    \"backgroundColorPink\": \"Pembe arka plan\",\n    \"backgroundColorRed\": \"Kırmızı arka plan\",\n    \"backgroundColorLime\": \"Limon yeşili arka plan\",\n    \"backgroundColorAqua\": \"Su mavisi arka plan\",\n    \"done\": \"Tamam\",\n    \"cancel\": \"İptal\",\n    \"tint1\": \"Ton 1\",\n    \"tint2\": \"Ton 2\",\n    \"tint3\": \"Ton 3\",\n    \"tint4\": \"Ton 4\",\n    \"tint5\": \"Ton 5\",\n    \"tint6\": \"Ton 6\",\n    \"tint7\": \"Ton 7\",\n    \"tint8\": \"Ton 8\",\n    \"tint9\": \"Ton 9\",\n    \"lightLightTint1\": \"Mor\",\n    \"lightLightTint2\": \"Pembe\",\n    \"lightLightTint3\": \"Açık Pembe\",\n    \"lightLightTint4\": \"Turuncu\",\n    \"lightLightTint5\": \"Sarı\",\n    \"lightLightTint6\": \"Limon yeşili\",\n    \"lightLightTint7\": \"Yeşil\",\n    \"lightLightTint8\": \"Su mavisi\",\n    \"lightLightTint9\": \"Mavi\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"Başlık 1\",\n    \"mobileHeading2\": \"Başlık 2\",\n    \"mobileHeading3\": \"Başlık 3\",\n    \"mobileHeading4\": \"Başlık 4\",\n    \"mobileHeading5\": \"Başlık 5\",\n    \"mobileHeading6\": \"Başlık 6\",\n    \"textColor\": \"Metin Rengi\",\n    \"backgroundColor\": \"Arka Plan Rengi\",\n    \"addYourLink\": \"Bağlantınızı ekleyin\",\n    \"openLink\": \"Bağlantıyı aç\",\n    \"copyLink\": \"Bağlantıyı kopyala\",\n    \"removeLink\": \"Bağlantıyı kaldır\",\n    \"editLink\": \"Bağlantıyı düzenle\",\n    \"linkText\": \"Metin\",\n    \"linkTextHint\": \"Lütfen metin girin\",\n    \"linkAddressHint\": \"Lütfen URL girin\",\n    \"highlightColor\": \"Vurgulama rengi\",\n    \"clearHighlightColor\": \"Vurgulama rengini temizle\",\n    \"customColor\": \"Özel renk\",\n    \"hexValue\": \"Hex değeri\",\n    \"opacity\": \"Opaklık\",\n    \"resetToDefaultColor\": \"Varsayılan renge sıfırla\",\n    \"ltr\": \"Soldan sağa\",\n    \"rtl\": \"Sağdan sola\",\n    \"auto\": \"Otomatik\",\n    \"cut\": \"Kes\",\n    \"copy\": \"Kopyala\",\n    \"paste\": \"Yapıştır\",\n    \"find\": \"Bul\",\n    \"select\": \"Seç\",\n    \"selectAll\": \"Tümünü seç\",\n    \"previousMatch\": \"Önceki eşleşme\",\n    \"nextMatch\": \"Sonraki eşleşme\",\n    \"closeFind\": \"Kapat\",\n    \"replace\": \"Değiştir\",\n    \"replaceAll\": \"Tümünü değiştir\",\n    \"regex\": \"Düzenli ifade\",\n    \"caseSensitive\": \"Büyük/küçük harf duyarlı\",\n    \"uploadImage\": \"Görsel Yükle\",\n    \"urlImage\": \"URL Görseli\",\n    \"incorrectLink\": \"Hatalı Bağlantı\",\n    \"upload\": \"Yükle\",\n    \"chooseImage\": \"Bir görsel seçin\",\n    \"loading\": \"Yükleniyor\",\n    \"imageLoadFailed\": \"Görsel yüklenemedi\",\n    \"divider\": \"Ayırıcı\",\n    \"table\": \"Tablo\",\n    \"colAddBefore\": \"Önce ekle\",\n    \"rowAddBefore\": \"Önce ekle\",\n    \"colAddAfter\": \"Sonra ekle\",\n    \"rowAddAfter\": \"Sonra ekle\",\n    \"colRemove\": \"Kaldır\",\n    \"rowRemove\": \"Kaldır\",\n    \"colDuplicate\": \"Çoğalt\",\n    \"rowDuplicate\": \"Çoğalt\",\n    \"colClear\": \"İçeriği Temizle\",\n    \"rowClear\": \"İçeriği Temizle\",\n    \"slashPlaceHolder\": \"Blok eklemek için '/' yazın veya yazmaya başlayın\",\n    \"typeSomething\": \"Bir şeyler yazın...\",\n    \"toggleListShortForm\": \"Aç/Kapat\",\n    \"quoteListShortForm\": \"Alıntı\",\n    \"mathEquationShortForm\": \"Formül\",\n    \"codeBlockShortForm\": \"Kod\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"Favori sayfa yok\",\n    \"noFavoriteHintText\": \"Favorilerinize eklemek için sayfayı sola kaydırın\",\n    \"removeFromSidebar\": \"Kenar çubuğundan kaldır\",\n    \"addToSidebar\": \"Kenar çubuğuna sabitle\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"Blok eklemek için / yazın veya yazmaya başlayın\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"Yapılacaklar\",\n    \"bulletList\": \"Liste\",\n    \"numberList\": \"Liste\",\n    \"quote\": \"Alıntı\",\n    \"heading\": \"Başlık {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"Sayfa ikonu\",\n    \"language\": \"Dil\",\n    \"font\": \"Yazı tipi\",\n    \"actions\": \"Eylemler\",\n    \"date\": \"Tarih\",\n    \"addField\": \"Alan ekle\",\n    \"userIcon\": \"Kullanıcı ikonu\"\n  },\n  \"noLogFiles\": \"Günlük dosyası yok\",\n  \"newSettings\": {\n    \"myAccount\": {\n      \"title\": \"Hesabım\",\n      \"subtitle\": \"Profilinizi özelleştirin, hesap güvenliğini yönetin, yapay zeka anahtarlarını ayarlayın veya hesabınıza giriş yapın.\",\n      \"profileLabel\": \"Hesap adı ve Profil resmi\",\n      \"profileNamePlaceholder\": \"Adınızı girin\",\n      \"accountSecurity\": \"Hesap güvenliği\",\n      \"2FA\": \"2 Adımlı Doğrulama\",\n      \"aiKeys\": \"Yapay zeka anahtarları\",\n      \"accountLogin\": \"Hesap Girişi\",\n      \"updateNameError\": \"Ad güncellenemedi\",\n      \"updateIconError\": \"İkon güncellenemedi\",\n      \"deleteAccount\": {\n        \"title\": \"Hesabı Sil\",\n        \"subtitle\": \"Hesabınızı ve tüm verilerinizi kalıcı olarak silin.\",\n        \"description\": \"Hesabınızı kalıcı olarak silin ve tüm çalışma alanlarından erişimi kaldırın.\",\n        \"deleteMyAccount\": \"Hesabımı sil\",\n        \"dialogTitle\": \"Hesabı sil\",\n        \"dialogContent1\": \"Hesabınızı kalıcı olarak silmek istediğinizden emin misiniz?\",\n        \"dialogContent2\": \"Bu işlem GERİ ALINAMAZ ve tüm çalışma alanlarından erişiminizi kaldıracak, özel çalışma alanları dahil tüm hesabınızı silecek ve sizi tüm paylaşılan çalışma alanlarından çıkaracaktır.\",\n        \"confirmHint1\": \"Onaylamak için lütfen \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\" yazın.\",\n        \"confirmHint2\": \"Bu işlemin GERİ ALINAMAZ olduğunu ve hesabımı ve ilişkili tüm verileri kalıcı olarak sileceğini anlıyorum.\",\n        \"confirmHint3\": \"HESABIMI SİL\",\n        \"checkToConfirmError\": \"Silme işlemini onaylamak için kutuyu işaretlemelisiniz\",\n        \"failedToGetCurrentUser\": \"Mevcut kullanıcı e-postası alınamadı\",\n        \"confirmTextValidationFailed\": \"Onay metniniz \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\" ile eşleşmiyor\",\n        \"deleteAccountSuccess\": \"Hesap başarıyla silindi\"\n      }\n    },\n    \"workplace\": {\n      \"name\": \"Çalışma alanı\",\n      \"title\": \"Çalışma Alanı Ayarları\",\n      \"subtitle\": \"Çalışma alanınızın görünümünü, temasını, yazı tipini, metin düzenini, tarih, saat ve dilini özelleştirin.\",\n      \"workplaceName\": \"Çalışma alanı adı\",\n      \"workplaceNamePlaceholder\": \"Çalışma alanı adını girin\",\n      \"workplaceIcon\": \"Çalışma alanı ikonu\",\n      \"workplaceIconSubtitle\": \"Bir görsel yükleyin veya çalışma alanınız için bir emoji kullanın. İkon kenar çubuğunuzda ve bildirimlerde görünecektir.\",\n      \"renameError\": \"Çalışma alanı yeniden adlandırılamadı\",\n      \"updateIconError\": \"İkon güncellenemedi\",\n      \"chooseAnIcon\": \"Bir ikon seçin\",\n      \"appearance\": {\n        \"name\": \"Görünüm\",\n        \"themeMode\": {\n          \"auto\": \"Otomatik\",\n          \"light\": \"Aydınlık\",\n          \"dark\": \"Koyu\"\n        },\n        \"language\": \"Dil\"\n      }\n    },\n    \"syncState\": {\n      \"syncing\": \"Senkronize ediliyor\",\n      \"synced\": \"Senkronize edildi\",\n      \"noNetworkConnected\": \"Ağ bağlantısı yok\"\n    }\n  },\n  \"pageStyle\": {\n    \"title\": \"Sayfa stili\",\n    \"layout\": \"Düzen\",\n    \"coverImage\": \"Kapak görseli\",\n    \"pageIcon\": \"Sayfa ikonu\",\n    \"colors\": \"Renkler\",\n    \"gradient\": \"Gradyan\",\n    \"backgroundImage\": \"Arka plan görseli\",\n    \"presets\": \"Hazır ayarlar\",\n    \"photo\": \"Fotoğraf\",\n    \"unsplash\": \"Unsplash\",\n    \"pageCover\": \"Sayfa kapağı\",\n    \"none\": \"Yok\",\n    \"openSettings\": \"Ayarları Aç\",\n    \"photoPermissionTitle\": \"@:appName fotoğraf kitaplığınıza erişmek istiyor\",\n    \"photoPermissionDescription\": \"@:appName belgelerinize görsel ekleyebilmeniz için fotoğraflarınıza erişmeye ihtiyaç duyuyor\",\n    \"cameraPermissionTitle\": \"@:appName kameranıza erişmek istiyor\",\n    \"cameraPermissionDescription\": \"@:appName belgelerinize kameradan görsel ekleyebilmeniz için kameranıza erişmeye ihtiyaç duyuyor\",\n    \"doNotAllow\": \"İzin Verme\",\n    \"image\": \"Görsel\"\n  },\n  \"commandPalette\": {\n    \"placeholder\": \"Ara veya bir soru sor...\",\n    \"bestMatches\": \"En iyi eşleşmeler\",\n    \"recentHistory\": \"Son geçmiş\",\n    \"navigateHint\": \"gezinmek için\",\n    \"loadingTooltip\": \"Sonuçları arıyoruz...\",\n    \"betaLabel\": \"BETA\",\n    \"betaTooltip\": \"Şu anda yalnızca sayfalarda ve belgelerde içerik aramayı destekliyoruz\",\n    \"fromTrashHint\": \"Çöp kutusundan\",\n    \"noResultsHint\": \"Aradığınızı bulamadık, başka bir terim aramayı deneyin.\",\n    \"clearSearchTooltip\": \"Arama alanını temizle\"\n  },\n  \"space\": {\n    \"delete\": \"Sil\",\n    \"deleteConfirmation\": \"Sil: \",\n    \"deleteConfirmationDescription\": \"Bu Alan içindeki tüm sayfalar silinecek ve Çöp Kutusuna taşınacak, yayınlanmış sayfaların yayını kaldırılacaktır.\",\n    \"rename\": \"Alanı Yeniden Adlandır\",\n    \"changeIcon\": \"İkonu değiştir\",\n    \"manage\": \"Alanı Yönet\",\n    \"addNewSpace\": \"Alan Oluştur\",\n    \"collapseAllSubPages\": \"Tüm alt sayfaları daralt\",\n    \"createNewSpace\": \"Yeni bir alan oluştur\",\n    \"createSpaceDescription\": \"İşlerinizi daha iyi organize etmek için birden fazla genel ve özel alan oluşturun.\",\n    \"spaceName\": \"Alan adı\",\n    \"spaceNamePlaceholder\": \"örn. Pazarlama, Mühendislik, İK\",\n    \"permission\": \"Alan izni\",\n    \"publicPermission\": \"Genel\",\n    \"publicPermissionDescription\": \"Tam erişime sahip tüm çalışma alanı üyeleri\",\n    \"privatePermission\": \"Özel\",\n    \"privatePermissionDescription\": \"Bu alana yalnızca siz erişebilirsiniz\",\n    \"spaceIconBackground\": \"Arka plan rengi\",\n    \"spaceIcon\": \"İkon\",\n    \"dangerZone\": \"Tehlikeli Bölge\",\n    \"unableToDeleteLastSpace\": \"Son Alan silinemez\",\n    \"unableToDeleteSpaceNotCreatedByYou\": \"Başkaları tarafından oluşturulan alanlar silinemez\",\n    \"enableSpacesForYourWorkspace\": \"Çalışma alanınız için Alanları etkinleştirin\",\n    \"title\": \"Alanlar\",\n    \"defaultSpaceName\": \"Genel\",\n    \"upgradeSpaceTitle\": \"Alanları Etkinleştir\",\n    \"upgradeSpaceDescription\": \"Çalışma alanınızı daha iyi organize etmek için birden fazla genel ve özel Alan oluşturun.\",\n    \"upgrade\": \"Güncelle\",\n    \"upgradeYourSpace\": \"Birden fazla Alan oluştur\",\n    \"quicklySwitch\": \"Hızlıca sonraki alana geç\",\n    \"duplicate\": \"Alanı Çoğalt\",\n    \"movePageToSpace\": \"Sayfayı alana taşı\",\n    \"cannotMovePageToDatabase\": \"Sayfa veritabanına taşınamaz\",\n    \"switchSpace\": \"Alan değiştir\",\n    \"spaceNameCannotBeEmpty\": \"Alan adı boş olamaz\",\n    \"success\": {\n      \"deleteSpace\": \"Alan başarıyla silindi\",\n      \"renameSpace\": \"Alan başarıyla yeniden adlandırıldı\",\n      \"duplicateSpace\": \"Alan başarıyla çoğaltıldı\",\n      \"updateSpace\": \"Alan başarıyla güncellendi\"\n    },\n    \"error\": {\n      \"deleteSpace\": \"Alan silinemedi\",\n      \"renameSpace\": \"Alan yeniden adlandırılamadı\",\n      \"duplicateSpace\": \"Alan çoğaltılamadı\",\n      \"updateSpace\": \"Alan güncellenemedi\"\n    },\n    \"createSpace\": \"Alan oluştur\",\n    \"manageSpace\": \"Alanı yönet\",\n    \"renameSpace\": \"Alanı yeniden adlandır\",\n    \"mSpaceIconColor\": \"Alan ikonu rengi\",\n    \"mSpaceIcon\": \"Alan ikonu\"\n  },\n  \"publish\": {\n    \"hasNotBeenPublished\": \"Bu sayfa henüz yayınlanmadı\",\n    \"spaceHasNotBeenPublished\": \"Henüz bir alanın yayınlanması desteklenmiyor\",\n    \"reportPage\": \"Sayfayı bildir\",\n    \"databaseHasNotBeenPublished\": \"Veritabanı yayınlama henüz desteklenmiyor.\",\n    \"createdWith\": \"Şununla oluşturuldu\",\n    \"downloadApp\": \"AppFlowy'yi İndir\",\n    \"copy\": {\n      \"codeBlock\": \"Kod bloğunun içeriği panoya kopyalandı\",\n      \"imageBlock\": \"Görsel bağlantısı panoya kopyalandı\",\n      \"mathBlock\": \"Matematik denklemi panoya kopyalandı\",\n      \"fileBlock\": \"Dosya bağlantısı panoya kopyalandı\"\n    },\n    \"containsPublishedPage\": \"Bu sayfa bir veya daha fazla yayınlanmış sayfa içeriyor. Devam ederseniz, yayından kaldırılacaklar. Silme işlemine devam etmek istiyor musunuz?\",\n    \"publishSuccessfully\": \"Başarıyla yayınlandı\",\n    \"unpublishSuccessfully\": \"Yayından kaldırma başarılı\",\n    \"publishFailed\": \"Yayınlanamadı\",\n    \"unpublishFailed\": \"Yayından kaldırılamadı\",\n    \"noAccessToVisit\": \"Bu sayfaya erişim yok...\",\n    \"createWithAppFlowy\": \"AppFlowy ile bir web sitesi oluşturun\",\n    \"fastWithAI\": \"Yapay zeka ile hızlı ve kolay.\",\n    \"tryItNow\": \"Şimdi deneyin\",\n    \"onlyGridViewCanBePublished\": \"Yalnızca Tablo görünümü yayınlanabilir\",\n    \"database\": {\n      \"zero\": \"{} seçili görünümü yayınla\",\n      \"one\": \"{} seçili görünümü yayınla\",\n      \"many\": \"{} seçili görünümü yayınla\",\n      \"other\": \"{} seçili görünümü yayınla\"\n    },\n    \"mustSelectPrimaryDatabase\": \"Ana görünüm seçilmelidir\",\n    \"noDatabaseSelected\": \"Veritabanı seçilmedi, lütfen en az bir veritabanı seçin.\",\n    \"unableToDeselectPrimaryDatabase\": \"Ana veritabanının seçimi kaldırılamaz\",\n    \"saveThisPage\": \"Bu şablonla başlayın\",\n    \"duplicateTitle\": \"Nereye eklemek istersiniz\",\n    \"selectWorkspace\": \"Bir çalışma alanı seçin\",\n    \"addTo\": \"Şuraya ekle\",\n    \"duplicateSuccessfully\": \"Çalışma alanınıza eklendi\",\n    \"duplicateSuccessfullyDescription\": \"AppFlowy yüklü değil mi? 'İndir'e tıkladıktan sonra indirme otomatik olarak başlayacak.\",\n    \"downloadIt\": \"İndir\",\n    \"openApp\": \"Uygulamada aç\",\n    \"duplicateFailed\": \"Çoğaltma başarısız\",\n    \"membersCount\": {\n      \"zero\": \"Üye yok\",\n      \"one\": \"1 üye\",\n      \"many\": \"{count} üye\",\n      \"other\": \"{count} üye\"\n    },\n    \"useThisTemplate\": \"Bu şablonu kullan\"\n  },\n  \"web\": {\n    \"continue\": \"Devam et\",\n    \"or\": \"veya\",\n    \"continueWithGoogle\": \"Google ile devam et\",\n    \"continueWithGithub\": \"GitHub ile devam et\",\n    \"continueWithDiscord\": \"Discord ile devam et\",\n    \"continueWithApple\": \"Apple ile devam et\",\n    \"moreOptions\": \"Daha fazla seçenek\",\n    \"collapse\": \"Daralt\",\n    \"signInAgreement\": \"Yukarıdaki \\\"Devam et\\\" düğmesine tıklayarak AppFlowy'nin şunlarını kabul etmiş olursunuz:\",\n    \"and\": \"ve\",\n    \"termOfUse\": \"Kullanım Koşulları\",\n    \"privacyPolicy\": \"Gizlilik Politikası\",\n    \"signInError\": \"Giriş hatası\",\n    \"login\": \"Kaydol veya giriş yap\",\n    \"fileBlock\": {\n      \"uploadedAt\": \"{time} tarihinde yüklendi\",\n      \"linkedAt\": \"Bağlantısı {time} tarihinde eklendi\",\n      \"empty\": \"Bir dosya yükleyin veya yerleştirin\",\n      \"uploadFailed\": \"Yükleme başarısız, lütfen tekrar deneyin\",\n      \"retry\": \"Tekrar dene\"\n    },\n    \"importNotion\": \"Notion'dan içe aktar\",\n    \"import\": \"İçe aktar\",\n    \"importSuccess\": \"Başarıyla yüklendi\",\n    \"importSuccessMessage\": \"İçe aktarma tamamlandığında size bildirim göndereceğiz. Bundan sonra, içe aktarılan sayfalarınızı kenar çubuğunda görüntüleyebilirsiniz.\",\n    \"importFailed\": \"İçe aktarma başarısız, lütfen dosya formatını kontrol edin\",\n    \"dropNotionFile\": \"Yüklemek için Notion zip dosyanızı buraya bırakın veya göz atmak için tıklayın\",\n    \"error\": {\n      \"pageNameIsEmpty\": \"Sayfa adı boş, lütfen başka bir tane deneyin\"\n    }\n  },\n  \"globalComment\": {\n    \"comments\": \"Yorumlar\",\n    \"addComment\": \"Yorum ekle\",\n    \"reactedBy\": \"tepki verenler\",\n    \"addReaction\": \"Tepki ekle\",\n    \"reactedByMore\": \"ve {count} diğer\",\n    \"showSeconds\": {\n      \"one\": \"1 saniye önce\",\n      \"other\": \"{count} saniye önce\",\n      \"zero\": \"Az önce\",\n      \"many\": \"{count} saniye önce\"\n    },\n    \"showMinutes\": {\n      \"one\": \"1 dakika önce\",\n      \"other\": \"{count} dakika önce\",\n      \"many\": \"{count} dakika önce\"\n    },\n    \"showHours\": {\n      \"one\": \"1 saat önce\",\n      \"other\": \"{count} saat önce\",\n      \"many\": \"{count} saat önce\"\n    },\n    \"showDays\": {\n      \"one\": \"1 gün önce\",\n      \"other\": \"{count} gün önce\",\n      \"many\": \"{count} gün önce\"\n    },\n    \"showMonths\": {\n      \"one\": \"1 ay önce\",\n      \"other\": \"{count} ay önce\",\n      \"many\": \"{count} ay önce\"\n    },\n    \"showYears\": {\n      \"one\": \"1 yıl önce\",\n      \"other\": \"{count} yıl önce\",\n      \"many\": \"{count} yıl önce\"\n    },\n    \"reply\": \"Yanıtla\",\n    \"deleteComment\": \"Yorumu sil\",\n    \"youAreNotOwner\": \"Bu yorumun sahibi siz değilsiniz\",\n    \"confirmDeleteDescription\": \"Bu yorumu silmek istediğinizden emin misiniz?\",\n    \"hasBeenDeleted\": \"Silindi\",\n    \"replyingTo\": \"Yanıtlanıyor\",\n    \"noAccessDeleteComment\": \"Bu yorumu silme izniniz yok\",\n    \"collapse\": \"Daralt\",\n    \"readMore\": \"Devamını oku\",\n    \"failedToAddComment\": \"Yorum eklenemedi\",\n    \"commentAddedSuccessfully\": \"Yorum başarıyla eklendi.\",\n    \"commentAddedSuccessTip\": \"Az önce bir yorum eklediniz veya yanıtladınız. En son yorumları görmek için başa dönmek ister misiniz?\"\n  },\n  \"template\": {\n    \"asTemplate\": \"Şablon olarak kaydet\",\n    \"name\": \"Şablon adı\",\n    \"description\": \"Şablon Açıklaması\",\n    \"about\": \"Şablon Hakkında\",\n    \"deleteFromTemplate\": \"Şablonlardan sil\",\n    \"preview\": \"Şablon Önizleme\",\n    \"categories\": \"Şablon Kategorileri\",\n    \"isNewTemplate\": \"Yeni şablona SABİTLE\",\n    \"featured\": \"Öne Çıkanlara SABİTLE\",\n    \"relatedTemplates\": \"İlgili Şablonlar\",\n    \"requiredField\": \"{field} gereklidir\",\n    \"addCategory\": \"\\\"{category}\\\" ekle\",\n    \"addNewCategory\": \"Yeni kategori ekle\",\n    \"addNewCreator\": \"Yeni oluşturucu ekle\",\n    \"deleteCategory\": \"Kategoriyi sil\",\n    \"editCategory\": \"Kategoriyi düzenle\",\n    \"editCreator\": \"Oluşturucuyu düzenle\",\n    \"category\": {\n      \"name\": \"Kategori adı\",\n      \"icon\": \"Kategori ikonu\",\n      \"bgColor\": \"Kategori arka plan rengi\",\n      \"priority\": \"Kategori önceliği\",\n      \"desc\": \"Kategori açıklaması\",\n      \"type\": \"Kategori türü\",\n      \"icons\": \"Kategori İkonları\",\n      \"colors\": \"Kategori Renkleri\",\n      \"byUseCase\": \"Kullanım Durumuna Göre\",\n      \"byFeature\": \"Özelliğe Göre\",\n      \"deleteCategory\": \"Kategoriyi sil\",\n      \"deleteCategoryDescription\": \"Bu kategoriyi silmek istediğinizden emin misiniz?\",\n      \"typeToSearch\": \"Kategorilerde arama yapmak için yazın...\"\n    },\n    \"creator\": {\n      \"label\": \"Şablon Oluşturucu\",\n      \"name\": \"Oluşturucu adı\",\n      \"avatar\": \"Oluşturucu avatarı\",\n      \"accountLinks\": \"Oluşturucu hesap bağlantıları\",\n      \"uploadAvatar\": \"Avatar yüklemek için tıklayın\",\n      \"deleteCreator\": \"Oluşturucuyu sil\",\n      \"deleteCreatorDescription\": \"Bu oluşturucuyu silmek istediğinizden emin misiniz?\",\n      \"typeToSearch\": \"Oluşturucularda arama yapmak için yazın...\"\n    },\n    \"uploadSuccess\": \"Şablon başarıyla yüklendi\",\n    \"uploadSuccessDescription\": \"Şablonunuz başarıyla yüklendi. Artık şablon galerisinde görüntüleyebilirsiniz.\",\n    \"viewTemplate\": \"Şablonu görüntüle\",\n    \"deleteTemplate\": \"Şablonu sil\",\n    \"deleteSuccess\": \"Şablon başarıyla silindi\",\n    \"deleteTemplateDescription\": \"Bu işlem mevcut sayfayı veya yayınlanma durumunu etkilemeyecektir. Bu şablonu silmek istediğinizden emin misiniz?\",\n    \"addRelatedTemplate\": \"İlgili şablon ekle\",\n    \"removeRelatedTemplate\": \"İlgili şablonu kaldır\",\n    \"uploadAvatar\": \"Avatar yükle\",\n    \"searchInCategory\": \"{category} içinde ara\",\n    \"label\": \"Şablonlar\"\n  },\n  \"fileDropzone\": {\n    \"dropFile\": \"Yüklemek için dosyayı bu alana tıklayın veya sürükleyin\",\n    \"uploading\": \"Yükleniyor...\",\n    \"uploadFailed\": \"Yükleme başarısız\",\n    \"uploadSuccess\": \"Yükleme başarılı\",\n    \"uploadSuccessDescription\": \"Dosya başarıyla yüklendi\",\n    \"uploadFailedDescription\": \"Dosya yüklenemedi\",\n    \"uploadingDescription\": \"Dosya yükleniyor\"\n  },\n  \"gallery\": {\n    \"preview\": \"Tam ekran aç\",\n    \"copy\": \"Kopyala\",\n    \"download\": \"İndir\",\n    \"prev\": \"Önceki\",\n    \"next\": \"Sonraki\",\n    \"resetZoom\": \"Yakınlaştırmayı sıfırla\",\n    \"zoomIn\": \"Yakınlaştır\",\n    \"zoomOut\": \"Uzaklaştır\"\n  },\n  \"invitation\": {\n    \"join\": \"Katıl\",\n    \"on\": \"tarihinde\",\n    \"invitedBy\": \"Davet eden\",\n    \"membersCount\": {\n      \"zero\": \"{count} üye\",\n      \"one\": \"{count} üye\",\n      \"many\": \"{count} üye\",\n      \"other\": \"{count} üye\"\n    },\n    \"tip\": \"Aşağıdaki iletişim bilgileriyle bu çalışma alanına katılmaya davet edildiniz. Bu bilgiler yanlışsa, davetiyeyi yeniden göndermesi için yöneticinizle iletişime geçin.\",\n    \"joinWorkspace\": \"Çalışma alanına katıl\",\n    \"success\": \"Çalışma alanına başarıyla katıldınız\",\n    \"successMessage\": \"Artık içindeki tüm sayfalara ve çalışma alanlarına erişebilirsiniz.\",\n    \"openWorkspace\": \"AppFlowy'yi Aç\",\n    \"alreadyAccepted\": \"Bu daveti zaten kabul ettiniz\",\n    \"errorModal\": {\n      \"title\": \"Bir hata oluştu\",\n      \"description\": \"Mevcut hesabınızın {email} bu çalışma alanına erişimi olmayabilir. Lütfen doğru hesapla giriş yapın veya yardım için çalışma alanı sahibiyle iletişime geçin.\",\n      \"contactOwner\": \"Sahiple iletişime geç\",\n      \"close\": \"Ana sayfaya dön\",\n      \"changeAccount\": \"Hesap değiştir\"\n    }\n  },\n  \"requestAccess\": {\n    \"title\": \"Bu sayfaya erişim yok\",\n    \"subtitle\": \"Bu sayfanın sahibinden erişim talep edebilirsiniz. Onaylandığında, sayfayı görüntüleyebilirsiniz.\",\n    \"requestAccess\": \"Erişim talep et\",\n    \"backToHome\": \"Ana sayfaya dön\",\n    \"tip\": \"Şu anda <link/> olarak giriş yapmış durumdasınız.\",\n    \"mightBe\": \"Farklı bir hesapla <login/> yapmanız gerekebilir.\",\n    \"successful\": \"Talep başarıyla gönderildi\",\n    \"successfulMessage\": \"Sahibi talebinizi onayladığında size bildirim gönderilecek.\",\n    \"requestError\": \"Erişim talebi başarısız oldu\",\n    \"repeatRequestError\": \"Bu sayfa için zaten erişim talebinde bulundunuz\"\n  },\n  \"approveAccess\": {\n    \"title\": \"Çalışma Alanı Katılım Talebini Onayla\",\n    \"requestSummary\": \"<user/>, <workspace/>'a katılmak ve <page/>'a erişmek istiyor\",\n    \"upgrade\": \"yükselt\",\n    \"downloadApp\": \"AppFlowy'yi İndir\",\n    \"approveButton\": \"Onayla\",\n    \"approveSuccess\": \"Başarıyla onaylandı\",\n    \"approveError\": \"Onaylama başarısız, çalışma alanı plan limitinin aşılmadığından emin olun\",\n    \"getRequestInfoError\": \"Talep bilgisi alınamadı\",\n    \"memberCount\": {\n      \"zero\": \"Üye yok\",\n      \"one\": \"1 üye\",\n      \"many\": \"{count} üye\",\n      \"other\": \"{count} üye\"\n    },\n    \"alreadyProTitle\": \"Çalışma alanı plan limitine ulaştınız\",\n    \"alreadyProMessage\": \"Daha fazla üye eklemek için <email/> ile iletişime geçmelerini isteyin\",\n    \"repeatApproveError\": \"Bu talebi zaten onayladınız\",\n    \"ensurePlanLimit\": \"Çalışma alanı plan limitinin aşılmadığından emin olun. Limit aşılırsa, çalışma alanı planını <upgrade/> veya <download/>.\",\n    \"requestToJoin\": \"katılmak için talep etti\",\n    \"asMember\": \"üye olarak\"\n  },\n  \"upgradePlanModal\": {\n    \"title\": \"Pro'ya Yükselt\",\n    \"message\": \"{name} ücretsiz üye limitine ulaştı. Daha fazla üye davet etmek için Pro Plana yükseltin.\",\n    \"upgradeSteps\": \"AppFlowy'de planınızı nasıl yükseltirsiniz:\",\n    \"step1\": \"1. Ayarlar'a gidin\",\n    \"step2\": \"2. 'Plan'a tıklayın\",\n    \"step3\": \"3. 'Planı Değiştir'i seçin\",\n    \"appNote\": \"Not: \",\n    \"actionButton\": \"Yükselt\",\n    \"downloadLink\": \"Uygulamayı İndir\",\n    \"laterButton\": \"Sonra\",\n    \"refreshNote\": \"Başarılı yükseltmeden sonra, yeni özelliklerinizi etkinleştirmek için <refresh/> tıklayın.\",\n    \"refresh\": \"buraya\"\n  },\n  \"breadcrumbs\": {\n    \"label\": \"Gezinti menüsü\"\n  },\n  \"time\": {\n    \"justNow\": \"Az önce\",\n    \"seconds\": {\n      \"one\": \"1 saniye\",\n      \"other\": \"{count} saniye\"\n    },\n    \"minutes\": {\n      \"one\": \"1 dakika\",\n      \"other\": \"{count} dakika\"\n    },\n    \"hours\": {\n      \"one\": \"1 saat\",\n      \"other\": \"{count} saat\"\n    },\n    \"days\": {\n      \"one\": \"1 gün\",\n      \"other\": \"{count} gün\"\n    },\n    \"weeks\": {\n      \"one\": \"1 hafta\",\n      \"other\": \"{count} hafta\"\n    },\n    \"months\": {\n      \"one\": \"1 ay\",\n      \"other\": \"{count} ay\"\n    },\n    \"years\": {\n      \"one\": \"1 yıl\",\n      \"other\": \"{count} yıl\"\n    },\n    \"ago\": \"önce\",\n    \"yesterday\": \"Dün\",\n    \"today\": \"Bugün\"\n  },\n  \"members\": {\n    \"zero\": \"Üye yok\",\n    \"one\": \"1 üye\",\n    \"many\": \"{count} üye\",\n    \"other\": \"{count} üye\"\n  },\n  \"tabMenu\": {\n    \"close\": \"Kapat\",\n    \"closeDisabledHint\": \"Sabitlenmiş bir sekme kapatılamaz, lütfen önce sabitlemeyi kaldırın\",\n    \"closeOthers\": \"Diğer sekmeleri kapat\",\n    \"closeOthersHint\": \"Bu işlem, bu sekme dışındaki tüm sabitlenmemiş sekmeleri kapatacaktır\",\n    \"closeOthersDisabledHint\": \"Tüm sekmeler sabitlenmiş, kapatılacak sekme bulunamadı\",\n    \"favorite\": \"Favorilere ekle\",\n    \"unfavorite\": \"Favorilerden kaldır\",\n    \"favoriteDisabledHint\": \"Bu görünüm favorilere eklenemez\",\n    \"pinTab\": \"Sabitle\",\n    \"unpinTab\": \"Sabitlemeyi kaldır\"\n  },\n  \"openFileMessage\": {\n    \"success\": \"Dosya başarıyla açıldı\",\n    \"fileNotFound\": \"Dosya bulunamadı\",\n    \"noAppToOpenFile\": \"Bu dosyayı açacak uygulama yok\",\n    \"permissionDenied\": \"Bu dosyayı açma izni yok\",\n    \"unknownError\": \"Dosya açılamadı\"\n  },\n  \"inviteMember\": {\n    \"requestInviteMembers\": \"Çalışma alanınıza davet et\",\n    \"inviteFailedMemberLimit\": \"Üye limitine ulaşıldı, lütfen \",\n    \"upgrade\": \"yükselt\",\n    \"addEmail\": \"email@example.com, email2@example.com...\",\n    \"requestInvites\": \"Davetleri gönder\",\n    \"inviteAlready\": \"Bu e-postayı zaten davet ettiniz: {email}\",\n    \"inviteSuccess\": \"Davet başarıyla gönderildi\",\n    \"description\": \"E-postaları aralarına virgül koyarak aşağıya girin. Ücretlendirme üye sayısına göre yapılır.\",\n    \"emails\": \"E-posta\"\n  },\n  \"quickNote\": {\n    \"label\": \"Hızlı Not\",\n    \"quickNotes\": \"Hızlı Notlar\",\n    \"search\": \"Hızlı Notlarda Ara\",\n    \"collapseFullView\": \"Tam görünümü daralt\",\n    \"expandFullView\": \"Tam görünümü genişlet\",\n    \"createFailed\": \"Hızlı Not oluşturulamadı\",\n    \"quickNotesEmpty\": \"Hızlı Not yok\",\n    \"emptyNote\": \"Boş not\",\n    \"deleteNotePrompt\": \"Seçilen not kalıcı olarak silinecektir. Silmek istediğinizden emin misiniz?\",\n    \"addNote\": \"Yeni Not\",\n    \"noAdditionalText\": \"Ek metin yok\"\n  },\n  \"subscribe\": {\n    \"upgradePlanTitle\": \"Planları karşılaştır ve seç\",\n    \"yearly\": \"Yıllık\",\n    \"save\": \"%{discount} tasarruf\",\n    \"monthly\": \"Aylık\",\n    \"priceIn\": \"Fiyat: \",\n    \"free\": \"Ücretsiz\",\n    \"pro\": \"Pro\",\n    \"freeDescription\": \"2 üyeye kadar bireyler için her şeyi organize etmek için\",\n    \"proDescription\": \"Küçük ekipler için projeleri ve ekip bilgisini yönetmek için\",\n    \"proDuration\": {\n      \"monthly\": \"üye başına aylık\\naylık faturalandırma\",\n      \"yearly\": \"üye başına aylık\\nyıllık faturalandırma\"\n    },\n    \"cancel\": \"Alt plana geç\",\n    \"changePlan\": \"Pro Plana yükselt\",\n    \"everythingInFree\": \"Ücretsiz plandaki her şey +\",\n    \"currentPlan\": \"Mevcut\",\n    \"freeDuration\": \"süresiz\",\n    \"freePoints\": {\n      \"first\": \"2 üyeye kadar 1 işbirliği çalışma alanı\",\n      \"second\": \"Sınırsız sayfa ve blok\",\n      \"three\": \"5 GB depolama\",\n      \"four\": \"Akıllı arama\",\n      \"five\": \"20 yapay zeka yanıtı\",\n      \"six\": \"Mobil uygulama\",\n      \"seven\": \"Gerçek zamanlı işbirliği\"\n    },\n    \"proPoints\": {\n      \"first\": \"Sınırsız depolama\",\n      \"second\": \"10 çalışma alanı üyesine kadar\",\n      \"three\": \"Sınırsız yapay zeka yanıtı\",\n      \"four\": \"Sınırsız dosya yükleme\",\n      \"five\": \"Özel alan adı\"\n    },\n    \"cancelPlan\": {\n      \"title\": \"Gitmenize üzüldük\",\n      \"success\": \"Aboneliğiniz başarıyla iptal edildi\",\n      \"description\": \"Gitmenize üzüldük. AppFlowy'yi geliştirmemize yardımcı olmak için geri bildiriminizi almak isteriz. Lütfen birkaç soruyu yanıtlamak için zaman ayırın.\",\n      \"commonOther\": \"Diğer\",\n      \"otherHint\": \"Yanıtınızı buraya yazın\",\n      \"questionOne\": {\n        \"question\": \"AppFlowy Pro aboneliğinizi iptal etmenize ne sebep oldu?\",\n        \"answerOne\": \"Maliyet çok yüksek\",\n        \"answerTwo\": \"Özellikler beklentileri karşılamadı\",\n        \"answerThree\": \"Daha iyi bir alternatif buldum\",\n        \"answerFour\": \"Maliyeti karşılayacak kadar kullanmadım\",\n        \"answerFive\": \"Hizmet sorunu veya teknik zorluklar\"\n      },\n      \"questionTwo\": {\n        \"question\": \"Gelecekte AppFlowy Pro'ya yeniden abone olma olasılığınız nedir?\",\n        \"answerOne\": \"Çok muhtemel\",\n        \"answerTwo\": \"Biraz muhtemel\",\n        \"answerThree\": \"Emin değilim\",\n        \"answerFour\": \"Muhtemel değil\",\n        \"answerFive\": \"Hiç muhtemel değil\"\n      },\n      \"questionThree\": {\n        \"question\": \"Aboneliğiniz sırasında en çok hangi Pro özelliğine değer verdiniz?\",\n        \"answerOne\": \"Çoklu kullanıcı işbirliği\",\n        \"answerTwo\": \"Daha uzun süreli versiyon geçmişi\",\n        \"answerThree\": \"Sınırsız yapay zeka yanıtları\",\n        \"answerFour\": \"Yerel yapay zeka modellerine erişim\"\n      },\n      \"questionFour\": {\n        \"question\": \"AppFlowy ile genel deneyiminizi nasıl tanımlarsınız?\",\n        \"answerOne\": \"Harika\",\n        \"answerTwo\": \"İyi\",\n        \"answerThree\": \"Ortalama\",\n        \"answerFour\": \"Ortalamanın altında\",\n        \"answerFive\": \"Memnun değilim\"\n      }\n    }\n  },\n  \"ai\": {\n    \"contentPolicyViolation\": \"Hassas içerik nedeniyle görsel oluşturma başarısız oldu. Lütfen girdinizi yeniden düzenleyip tekrar deneyin\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/uk-UA.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Я\",\n  \"welcomeText\": \"Ласкаво просимо в @:appName\",\n  \"welcomeTo\": \"Ласкаво просимо до\",\n  \"githubStarText\": \"Поставити зірку на GitHub\",\n  \"subscribeNewsletterText\": \"Підпишіться на розсилку новин\",\n  \"letsGoButtonText\": \"Почнемо\",\n  \"title\": \"Заголовок\",\n  \"youCanAlso\": \"Ви також можете\",\n  \"and\": \"та\",\n  \"failedToOpenUrl\": \"Не вдалося відкрити URL-адресу: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Клацніть, щоб додати нижче\",\n    \"addAboveCmd\": \"Alt+click\",\n    \"addAboveMacCmd\": \"Виберіть варіант та натисніть\",\n    \"addAboveTooltip\": \"додати вище\",\n    \"dragTooltip\": \"Перетягніть, щоб пересунути\",\n    \"openMenuTooltip\": \"Клацніть, щоб відкрити меню\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Зареєструватись\",\n    \"title\": \"Реєстрація в @:appName\",\n    \"getStartedText\": \"Почати\",\n    \"emptyPasswordError\": \"Пароль не може бути порожнім\",\n    \"repeatPasswordEmptyError\": \"Повтор пароля не може бути порожнім\",\n    \"unmatchedPasswordError\": \"Паролі не збігаються\",\n    \"alreadyHaveAnAccount\": \"Вже маєте обліковий запис?\",\n    \"emailHint\": \"Електронна пошта\",\n    \"passwordHint\": \"Пароль\",\n    \"repeatPasswordHint\": \"Повторіть пароль\",\n    \"signUpWith\": \"Зареєструватися за допомогою:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Увійдіть до @:appName\",\n    \"loginButtonText\": \"Увійти\",\n    \"loginStartWithAnonymous\": \"Почати анонімну сесію\",\n    \"continueAnonymousUser\": \"Продовжити анонімну сесію\",\n    \"buttonText\": \"Увійти\",\n    \"signingInText\": \"Вхід...\",\n    \"forgotPassword\": \"Забули пароль?\",\n    \"emailHint\": \"Електронна пошта\",\n    \"passwordHint\": \"Пароль\",\n    \"dontHaveAnAccount\": \"Немаєте облікового запису?\",\n    \"createAccount\": \"Створити акаунт\",\n    \"repeatPasswordEmptyError\": \"Повторний пароль не може бути порожнім\",\n    \"unmatchedPasswordError\": \"Повторний пароль не співпадає з паролем\",\n    \"syncPromptMessage\": \"Синхронізація даних може зайняти трохи часу. Будь ласка, не закривайте цю сторінку\",\n    \"or\": \"АБО\",\n    \"signInWithGoogle\": \"Продовжуйте з Google\",\n    \"signInWithGithub\": \"Продовжуйте з Github\",\n    \"signInWithDiscord\": \"Продовжуйте з Discord\",\n    \"signInWithApple\": \"Продовжуйте з Apple\",\n    \"continueAnotherWay\": \"Продовжуйте іншим шляхом\",\n    \"signUpWithGoogle\": \"Зареєструватися в Google\",\n    \"signUpWithGithub\": \"Зареєструйтеся на Github\",\n    \"signUpWithDiscord\": \"Зареєструйтеся в Discord\",\n    \"signInWith\": \"Увійти за допомогою:\",\n    \"signInWithEmail\": \"Продовжити з електронною поштою\",\n    \"signInWithMagicLink\": \"Продовжити\",\n    \"signUpWithMagicLink\": \"Зареєструйтеся за допомогою Magic Link\",\n    \"pleaseInputYourEmail\": \"Будь ласка, введіть адресу електронної пошти\",\n    \"settings\": \"Налаштування\",\n    \"magicLinkSent\": \"Magic Link надіслано!\",\n    \"invalidEmail\": \"Будь ласка, введіть дійсну адресу електронної пошти\",\n    \"alreadyHaveAnAccount\": \"Вже є акаунт?\",\n    \"logIn\": \"Авторизуватися\",\n    \"generalError\": \"Щось пішло не так. Будь ласка спробуйте пізніше\",\n    \"limitRateError\": \"З міркувань безпеки ви можете запитувати чарівне посилання лише кожні 60 секунд\",\n    \"magicLinkSentDescription\": \"Чарівне посилання надіслано на вашу електронну адресу. Натисніть посилання, щоб завершити вхід. Термін дії посилання закінчиться через 5 хвилин.\",\n    \"anonymous\": \"Анонім\",\n    \"LogInWithGoogle\": \"Увійти за допомогою Google\",\n    \"LogInWithGithub\": \"Увійти за допомогою Github\",\n    \"LogInWithDiscord\": \"Увійти за допомогою Discord\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Виберіть свій робочий простір\",\n    \"create\": \"Створити робочий простір\",\n    \"reset\": \"Скинути робочий простір\",\n    \"renameWorkspace\": \"Перейменувати робочу область\",\n    \"resetWorkspacePrompt\": \"Скидання робочого простору призведе до видалення всіх сторінок та даних у ньому. Ви впевнені, що хочете скинути робочий простір? Також ви можете звернутися до служби підтримки для відновлення робочого простору\",\n    \"hint\": \"робочий простір\",\n    \"notFoundError\": \"Робочий простір не знайдено\",\n    \"failedToLoad\": \"Щось пішло не так! Не вдалося завантажити робочий простір. Спробуйте закрити будь-який відкритий екземпляр AppFlowy та спробуйте ще раз.\",\n    \"errorActions\": {\n      \"reportIssue\": \"Повідомити про проблему\",\n      \"reportIssueOnGithub\": \"Повідомити про проблему на Github\",\n      \"exportLogFiles\": \"Експорт файлів журналу\",\n      \"reachOut\": \"Звернутися на Discord\"\n    },\n    \"menuTitle\": \"Робочі області\",\n    \"deleteWorkspaceHintText\": \"Ви впевнені, що хочете видалити робочу область? Цю дію неможливо скасувати, і всі опубліковані вами сторінки буде скасовано.\",\n    \"createSuccess\": \"Робочу область успішно створено\",\n    \"createFailed\": \"Не вдалося створити робочу область\",\n    \"createLimitExceeded\": \"Ви досягли максимального ліміту робочого простору, дозволеного для вашого облікового запису. Якщо вам потрібні додаткові робочі області, щоб продовжити роботу, надішліть запит на Github\",\n    \"deleteSuccess\": \"Робочу область успішно видалено\",\n    \"deleteFailed\": \"Не вдалося видалити робочу область\",\n    \"openSuccess\": \"Робочу область відкрито\",\n    \"openFailed\": \"Не вдалося відкрити робочу область\",\n    \"renameSuccess\": \"Робочу область успішно перейменовано\",\n    \"renameFailed\": \"Не вдалося перейменувати робочу область\",\n    \"updateIconSuccess\": \"Значок робочої області успішно оновлено\",\n    \"updateIconFailed\": \"Не вдалося оновити значок робочої області\",\n    \"cannotDeleteTheOnlyWorkspace\": \"Неможливо видалити єдину робочу область\",\n    \"fetchWorkspacesFailed\": \"Не вдалося отримати робочі області\",\n    \"leaveCurrentWorkspace\": \"Залишити робочу область\",\n    \"leaveCurrentWorkspacePrompt\": \"Ви впевнені, що бажаєте залишити поточну робочу область?\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Поділитися\",\n    \"workInProgress\": \"Скоро буде доступно\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"Копіювати в буфер обміну\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Скопіювати посилання\",\n    \"publishToTheWeb\": \"Опублікувати в Інтернеті\",\n    \"publishToTheWebHint\": \"Створіть веб-сайт за допомогою AppFlowy\",\n    \"publish\": \"Опублікувати\",\n    \"unPublish\": \"Скасувати публікацію\",\n    \"visitSite\": \"Відвідайте сайт\",\n    \"exportAsTab\": \"Експортувати як\",\n    \"publishTab\": \"Опублікувати\",\n    \"shareTab\": \"Поділіться\"\n  },\n  \"moreAction\": {\n    \"small\": \"малий\",\n    \"medium\": \"середній\",\n    \"large\": \"великий\",\n    \"fontSize\": \"Розмір шрифту\",\n    \"import\": \"Імпортувати\",\n    \"moreOptions\": \"Більше опцій\",\n    \"wordCount\": \"Кількість слів: {}\",\n    \"charCount\": \"Кількість символів: {}\",\n    \"createdAt\": \"Створено: {}\",\n    \"deleteView\": \"Видалити\",\n    \"duplicateView\": \"Дублікат\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Текст і Markdown\",\n    \"documentFromV010\": \"Документ з v0.1.0\",\n    \"databaseFromV010\": \"База даних з v0.1.0\",\n    \"csv\": \"CSV\",\n    \"database\": \"База даних\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Перейменувати\",\n    \"delete\": \"Видалити\",\n    \"duplicate\": \"Дублювати\",\n    \"unfavorite\": \"Видалити з обраного\",\n    \"favorite\": \"Додати до обраного\",\n    \"openNewTab\": \"Відкрити в новій вкладці\",\n    \"moveTo\": \"Перемістити в\",\n    \"addToFavorites\": \"Додати до обраного\",\n    \"copyLink\": \"Скопіювати посилання\",\n    \"changeIcon\": \"Змінити значок\",\n    \"collapseAllPages\": \"Згорнути всі підсторінки\"\n  },\n  \"blankPageTitle\": \"Порожня сторінка\",\n  \"newPageText\": \"Нова сторінка\",\n  \"newDocumentText\": \"Новий документ\",\n  \"newGridText\": \"Нова таблиця\",\n  \"newCalendarText\": \"Новий календар\",\n  \"newBoardText\": \"Нова дошка\",\n  \"chat\": {\n    \"newChat\": \"ШІ Чат\",\n    \"inputMessageHint\": \"Запитайте @:appName AI\",\n    \"inputLocalAIMessageHint\": \"Запитайте @:appName локальний AI\",\n    \"unsupportedCloudPrompt\": \"Ця функція доступна лише під час використання @:appName Cloud\",\n    \"relatedQuestion\": \"Пов'язані\",\n    \"serverUnavailable\": \"Сервіс тимчасово недоступний. Будь ласка спробуйте пізніше.\",\n    \"aiServerUnavailable\": \"🌈 Ой-ой! 🌈. Єдиноріг з'їв нашу відповідь. Будь ласка, повторіть спробу!\",\n    \"clickToRetry\": \"Натисніть, щоб повторити спробу\",\n    \"regenerateAnswer\": \"Регенерувати\",\n    \"question1\": \"Як використовувати Kanban для керування завданнями\",\n    \"question2\": \"Поясніть метод GTD\",\n    \"question3\": \"Навіщо використовувати Rust\",\n    \"question4\": \"Рецепт з тим, що є на моїй кухні\",\n    \"aiMistakePrompt\": \"ШІ може помилятися. Перевірте важливу інформацію.\",\n    \"chatWithFilePrompt\": \"Хочете поспілкуватися з файлом?\",\n    \"indexFileSuccess\": \"Файл успішно індексується\",\n    \"inputActionNoPages\": \"Сторінка не знайдена\",\n    \"referenceSource\": {\n      \"zero\": \"0 джерел знайдено\",\n      \"one\": \"{count} джерело знайдено\",\n      \"other\": \"{count} джерел знайдено\"\n    },\n    \"clickToMention\": \"Натисніть, щоб згадати сторінку\",\n    \"uploadFile\": \"Завантажуйте PDF-файли, файли md або txt для спілкування в чаті\",\n    \"questionDetail\": \"Привіт {}! Чим я можу тобі допомогти сьогодні?\",\n    \"indexingFile\": \"Індексація {}\"\n  },\n  \"trash\": {\n    \"text\": \"Смітник\",\n    \"restoreAll\": \"Відновити все\",\n    \"deleteAll\": \"Видалити все\",\n    \"pageHeader\": {\n      \"fileName\": \"Ім'я файлу\",\n      \"lastModified\": \"Останній змінений\",\n      \"created\": \"Створено\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Ви впевнені, що хочете видалити всі сторінки у кошику?\",\n      \"caption\": \"Цю дію неможливо скасувати.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Ви впевнені, що хочете відновити всі сторінки у кошику?\",\n      \"caption\": \"Цю дію неможливо скасувати.\"\n    },\n    \"mobile\": {\n      \"actions\": \"Дії щодо сміття\",\n      \"empty\": \"Кошик порожній\",\n      \"emptyDescription\": \"У вас немає видалених файлів\",\n      \"isDeleted\": \"видалено\",\n      \"isRestored\": \"відновлено\"\n    },\n    \"confirmDeleteTitle\": \"Ви впевнені, що хочете остаточно видалити цю сторінку?\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Ця сторінка знаходиться у кошику\",\n    \"restore\": \"Відновити сторінку\",\n    \"deletePermanent\": \"Видалити назавжди\"\n  },\n  \"dialogCreatePageNameHint\": \"Назва сторінки\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Комбінації клавіш\",\n    \"whatsNew\": \"Що нового?\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"Інформація для налагодження\",\n      \"success\": \"Інформацію для налагодження скопійовано в буфер обміну!\",\n      \"fail\": \"Не вдалося скопіювати інформацію для налагодження в буфер обміну\"\n    },\n    \"feedback\": \"Зворотний зв'язок\",\n    \"help\": \"Довідка та підтримка\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Видалити, перейменувати та інше...\",\n    \"addPageTooltip\": \"Швидко додати сторінку всередину\",\n    \"defaultNewPageName\": \"Без назви\",\n    \"renameDialog\": \"Перейменувати\"\n  },\n  \"noPagesInside\": \"Всередині немає сторінок\",\n  \"toolbar\": {\n    \"undo\": \"Скасувати\",\n    \"redo\": \"Повторити\",\n    \"bold\": \"Жирний\",\n    \"italic\": \"Курсив\",\n    \"underline\": \"Підкреслення\",\n    \"strike\": \"Закреслення\",\n    \"numList\": \"Нумерований список\",\n    \"bulletList\": \"Маркований список\",\n    \"checkList\": \"Список із прапорцями\",\n    \"inlineCode\": \"Вбудований код\",\n    \"quote\": \"Цитата\",\n    \"header\": \"Заголовок\",\n    \"highlight\": \"Виділення\",\n    \"color\": \"Колір\",\n    \"addLink\": \"Додати посилання\",\n    \"link\": \"Посилання\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Перейти до світлого режиму\",\n    \"darkMode\": \"Перейти до темного режиму\",\n    \"openAsPage\": \"Відкрити як сторінку\",\n    \"addNewRow\": \"Додати новий рядок\",\n    \"openMenu\": \"Натисніть, щоб відкрити меню\",\n    \"dragRow\": \"Тримайте натиснутим для зміни порядку рядка\",\n    \"viewDataBase\": \"Переглянути базу даних\",\n    \"referencePage\": \"Ця {name} знаходиться у зв'язку\",\n    \"addBlockBelow\": \"Додати блок нижче\",\n    \"aiGenerate\": \"Генерувати\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Закрити бічну панель\",\n    \"openSidebar\": \"Відкрити бічну панель\",\n    \"personal\": \"Особисте\",\n    \"private\": \"Приватний\",\n    \"workspace\": \"Робоча область\",\n    \"favorites\": \"Вибране\",\n    \"clickToHidePrivate\": \"Натисніть, щоб приховати особистий простір\\nСторінки, які ви тут створили, бачите лише ви\",\n    \"clickToHideWorkspace\": \"Натисніть, щоб приховати робочу область\\nСторінки, які ви тут створили, бачать усі учасники\",\n    \"clickToHidePersonal\": \"Натисніть, щоб приховати особистий розділ\",\n    \"clickToHideFavorites\": \"Натисніть, щоб приховати вибраний розділ\",\n    \"addAPage\": \"Додати сторінку\",\n    \"addAPageToPrivate\": \"Додайте сторінку до особистого простору\",\n    \"addAPageToWorkspace\": \"Додати сторінку до робочої області\",\n    \"recent\": \"Останні\",\n    \"today\": \"Сьогодні\",\n    \"thisWeek\": \"Цього тижня\",\n    \"others\": \"Раніше улюблені\",\n    \"justNow\": \"прямо зараз\",\n    \"minutesAgo\": \"{count} хвилин тому\",\n    \"lastViewed\": \"Останній перегляд\",\n    \"favoriteAt\": \"Вибране\",\n    \"emptyRecent\": \"Немає останніх документів\",\n    \"emptyRecentDescription\": \"Під час перегляду документів вони з’являтимуться тут, щоб їх було легко знайти\",\n    \"emptyFavorite\": \"Немає вибраних документів\",\n    \"emptyFavoriteDescription\": \"Почніть досліджувати та позначайте документи як вибрані. Вони будуть перераховані тут для швидкого доступу!\",\n    \"removePageFromRecent\": \"Видалити цю сторінку з останніх?\",\n    \"removeSuccess\": \"Успішно видалено\",\n    \"favoriteSpace\": \"Вибране\",\n    \"RecentSpace\": \"Останні\",\n    \"Spaces\": \"Пробіли\",\n    \"upgradeToPro\": \"Оновлення до Pro\",\n    \"upgradeToAIMax\": \"Розблокуйте необмежений ШІ\",\n    \"storageLimitDialogTitle\": \"У вас вичерпано безкоштовне сховище. Оновіть, щоб розблокувати необмежений обсяг пам’яті\",\n    \"aiResponseLimitTitle\": \"У вас закінчилися безкоштовні відповіді ШІ. Перейдіть до плану Pro або придбайте доповнення AI, щоб розблокувати необмежену кількість відповідей\",\n    \"aiResponseLimitDialogTitle\": \"Досягнуто ліміту відповідей ШІ\",\n    \"aiResponseLimit\": \"У вас закінчилися безкоштовні відповіді ШІ.<inlang-LineFeed>\\nПерейдіть до Налаштування -&gt; План -&gt; Натисніть AI Max або Pro Plan, щоб отримати більше відповідей AI\",\n    \"askOwnerToUpgradeToPro\": \"У вашому робочому просторі закінчується безкоштовна пам’ять. Попросіть свого власника робочого місця перейти на план Pro\",\n    \"askOwnerToUpgradeToAIMax\": \"У вашій робочій області закінчуються безкоштовні відповіді ШІ. Будь ласка, попросіть свого власника робочого простору оновити план або придбати додатки AI\",\n    \"purchaseStorageSpace\": \"Придбайте місце для зберігання\",\n    \"purchaseAIResponse\": \"Придбати\",\n    \"askOwnerToUpgradeToLocalAI\": \"Попросіть власника робочої області ввімкнути ШІ на пристрої\",\n    \"upgradeToAILocal\": \"Запустіть локальні моделі на своєму пристрої для повної конфіденційності\",\n    \"upgradeToAILocalDesc\": \"Спілкуйтеся в чаті з PDF-файлами, вдосконалюйте свій текст і автоматично заповнюйте таблиці за допомогою локального штучного інтелекту\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Експортовано нотатку в Markdown\",\n      \"path\": \"Документи/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Контакти\",\n    \"whatsHappening\": \"Що відбудеться на цьому тижні?\",\n    \"addContact\": \"Додати контакт\",\n    \"editContact\": \"Редагувати контакт\"\n  },\n  \"button\": {\n    \"ok\": \"Oк\",\n    \"confirm\": \"Підтвердити\",\n    \"done\": \"Готово\",\n    \"cancel\": \"Скасувати\",\n    \"signIn\": \"Увійти\",\n    \"signOut\": \"Вийти\",\n    \"complete\": \"Завершити\",\n    \"save\": \"Зберегти\",\n    \"generate\": \"Створити\",\n    \"esc\": \"ESC\",\n    \"keep\": \"Зберегти\",\n    \"tryAgain\": \"Спробувати знову\",\n    \"discard\": \"Відхилити\",\n    \"replace\": \"Замінити\",\n    \"insertBelow\": \"Вставити нижче\",\n    \"insertAbove\": \"Вставте вище\",\n    \"upload\": \"Завантажити\",\n    \"edit\": \"Редагувати\",\n    \"delete\": \"Видалити\",\n    \"duplicate\": \"Дублювати\",\n    \"putback\": \"Повернути\",\n    \"update\": \"Оновлення\",\n    \"share\": \"Поділіться\",\n    \"removeFromFavorites\": \"Видалити з вибраного\",\n    \"removeFromRecent\": \"Вилучити з останніх\",\n    \"addToFavorites\": \"Додати в обране\",\n    \"favoriteSuccessfully\": \"Улюблений успіх\",\n    \"unfavoriteSuccessfully\": \"Неулюблений успіх\",\n    \"duplicateSuccessfully\": \"Продубльовано успішно\",\n    \"rename\": \"Перейменувати\",\n    \"helpCenter\": \"Центр допомоги\",\n    \"add\": \"Додати\",\n    \"yes\": \"Так\",\n    \"no\": \"Ні\",\n    \"clear\": \"Очистити\",\n    \"remove\": \"Видалити\",\n    \"dontRemove\": \"Не видаляйте\",\n    \"copyLink\": \"Копіювати посилання\",\n    \"align\": \"Вирівняти\",\n    \"login\": \"Логін\",\n    \"logout\": \"Вийти\",\n    \"deleteAccount\": \"Видалити аккаунт\",\n    \"back\": \"Назад\",\n    \"signInGoogle\": \"Продовжуйте з Google\",\n    \"signInGithub\": \"Продовжуйте з Github\",\n    \"signInDiscord\": \"Продовжуйте з Discord\",\n    \"more\": \"Більше\",\n    \"create\": \"Створити\",\n    \"close\": \"Закрити\",\n    \"next\": \"Далі\",\n    \"previous\": \"Попередній\",\n    \"submit\": \"Надіслати\",\n    \"download\": \"Завантажити\"\n  },\n  \"label\": {\n    \"welcome\": \"Ласкаво просимо!\",\n    \"firstName\": \"Ім'я\",\n    \"middleName\": \"По батькові\",\n    \"lastName\": \"Прізвище\",\n    \"stepX\": \"Крок {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Не вдалося підключитися до вашого облікового запису.\",\n      \"failedMsg\": \"Будь ласка, переконайтеся, що ви завершили процес увіходу у своєму веб-переглядачі.\"\n    },\n    \"google\": {\n      \"title\": \"УВІЙТИ ЗА ДОПОМОГОЮ GOOGLE\",\n      \"instruction1\": \"Для імпортування ваших контактів Google вам потрібно авторизувати цю програму, використовуючи свій веб-переглядач.\",\n      \"instruction2\": \"Скопіюйте цей код у буфер обміну, натиснувши на значок або вибравши текст:\",\n      \"instruction3\": \"Перейдіть за наступним посиланням у своєму веб-переглядачі та введіть вищезазначений код:\",\n      \"instruction4\": \"Натисніть кнопку нижче, коли завершите реєстрацію:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Налаштування\",\n    \"popupMenuItem\": {\n      \"settings\": \"Налаштування\",\n      \"members\": \"Члени\",\n      \"trash\": \"Cміття\",\n      \"helpAndSupport\": \"Допомога та підтримка\"\n    },\n    \"accountPage\": {\n      \"menuLabel\": \"Мій рахунок\",\n      \"title\": \"Мій рахунок\",\n      \"general\": {\n        \"title\": \"Назва облікового запису та зображення профілю\",\n        \"changeProfilePicture\": \"Змінити зображення профілю\"\n      },\n      \"email\": {\n        \"title\": \"Електронна пошта\",\n        \"actions\": {\n          \"change\": \"Змінити електронну адресу\"\n        }\n      },\n      \"login\": {\n        \"title\": \"Вхід в обліковий запис\",\n        \"loginLabel\": \"Авторизуватися\",\n        \"logoutLabel\": \"Вийти\"\n      }\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"Робоча область\",\n      \"title\": \"Робоча область\",\n      \"description\": \"Налаштуйте зовнішній вигляд робочої області, тему, шрифт, макет тексту, формат дати/часу та мову.\",\n      \"workspaceName\": {\n        \"title\": \"Назва робочої області\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"Значок робочої області\",\n        \"description\": \"Завантажте зображення або використовуйте емодзі для свого робочого простору. Значок відображатиметься на бічній панелі та в сповіщеннях.\"\n      },\n      \"appearance\": {\n        \"title\": \"Зовнішній вигляд\",\n        \"description\": \"Налаштуйте зовнішній вигляд робочої області, тему, шрифт, макет тексту, дату, час і мову.\",\n        \"options\": {\n          \"system\": \"Авто\",\n          \"light\": \"Світлий\",\n          \"dark\": \"Темний\"\n        }\n      },\n      \"resetCursorColor\": {\n        \"title\": \"Скинути колір курсора документа\",\n        \"description\": \"Ви впевнені, що хочете скинути колір курсору?\"\n      },\n      \"resetSelectionColor\": {\n        \"title\": \"Скинути колір вибору документа\",\n        \"description\": \"Ви впевнені, що бажаєте скинути колір виділення?\"\n      },\n      \"theme\": {\n        \"title\": \"Тема\",\n        \"description\": \"Виберіть попередньо встановлену тему або завантажте власну тему.\",\n        \"uploadCustomThemeTooltip\": \"Завантажте спеціальну тему\"\n      },\n      \"workspaceFont\": {\n        \"title\": \"Шрифт робочої області\",\n        \"noFontHint\": \"Шрифт не знайдено, спробуйте інший термін.\"\n      },\n      \"textDirection\": {\n        \"title\": \"Напрямок тексту\",\n        \"leftToRight\": \"Зліва направо\",\n        \"rightToLeft\": \"Справа наліво\",\n        \"auto\": \"Авто\",\n        \"enableRTLItems\": \"Увімкнути елементи панелі інструментів RTL\"\n      },\n      \"layoutDirection\": {\n        \"title\": \"Напрямок макета\",\n        \"leftToRight\": \"Зліва направо\",\n        \"rightToLeft\": \"Справа наліво\"\n      },\n      \"dateTime\": {\n        \"title\": \"Дата, час\",\n        \"example\": \"{} у {} ({})\",\n        \"24HourTime\": \"24-годинний час\",\n        \"dateFormat\": {\n          \"label\": \"Формат дати\",\n          \"local\": \"Місцевий\",\n          \"us\": \"US\",\n          \"iso\": \"ISO\",\n          \"friendly\": \"дружній\",\n          \"dmy\": \"Д/М/Р\"\n        }\n      },\n      \"language\": {\n        \"title\": \"Мова\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"Видалити робочу область\",\n        \"content\": \"Ви впевнені, що хочете видалити цю робочу область? Цю дію неможливо скасувати, і всі опубліковані вами сторінки буде скасовано.\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"Залиште робочу область\",\n        \"content\": \"Ви впевнені, що бажаєте залишити цю робочу область? Ви втратите доступ до всіх сторінок і даних на них.\"\n      },\n      \"manageWorkspace\": {\n        \"title\": \"Керуйте робочим простором\",\n        \"leaveWorkspace\": \"Залиште робочу область\",\n        \"deleteWorkspace\": \"Видалити робочу область\"\n      }\n    },\n    \"manageDataPage\": {\n      \"menuLabel\": \"Керувати даними\",\n      \"title\": \"Керувати даними\",\n      \"description\": \"Керуйте локальним сховищем даних або імпортуйте наявні дані в @:appName .\",\n      \"dataStorage\": {\n        \"title\": \"Місце зберігання файлів\",\n        \"tooltip\": \"Місце, де зберігаються ваші файли\",\n        \"actions\": {\n          \"change\": \"Змінити шлях\",\n          \"open\": \"Відкрити папку\",\n          \"openTooltip\": \"Відкрити поточне розташування папки даних\",\n          \"copy\": \"Копіювати шлях\",\n          \"copiedHint\": \"Шлях скопійовано!\",\n          \"resetTooltip\": \"Відновити розташування за умовчанням\"\n        },\n        \"resetDialog\": {\n          \"title\": \"Ти впевнений?\",\n          \"description\": \"Скидання шляху до розташування даних за умовчанням не призведе до видалення даних. Якщо ви хочете повторно імпортувати поточні дані, вам слід спочатку скопіювати шлях вашого поточного місцезнаходження.\"\n        }\n      },\n      \"importData\": {\n        \"title\": \"Імпорт даних\",\n        \"tooltip\": \"Імпорт даних із резервних копій/папок даних @:appName\",\n        \"description\": \"Скопіюйте дані із зовнішньої папки даних @:appName\",\n        \"action\": \"Переглянути файл\"\n      },\n      \"encryption\": {\n        \"title\": \"Шифрування\",\n        \"tooltip\": \"Керуйте тим, як ваші дані зберігаються та шифруються\",\n        \"descriptionNoEncryption\": \"Якщо ввімкнути шифрування, усі дані будуть зашифровані. Це не може бути скасоване.\",\n        \"descriptionEncrypted\": \"Ваші дані зашифровані.\",\n        \"action\": \"Шифрувати дані\",\n        \"dialog\": {\n          \"title\": \"Зашифрувати всі ваші дані?\",\n          \"description\": \"Шифрування всіх ваших даних збереже ваші дані в безпеці. Цю дію НЕ можна скасувати. Ви впевнені, що бажаєте продовжити?\"\n        }\n      },\n      \"cache\": {\n        \"title\": \"Очистити кеш\",\n        \"description\": \"Допоможіть вирішити такі проблеми, як не завантаження зображення, відсутність сторінок у просторі та не завантаження шрифтів. Це не вплине на ваші дані.\",\n        \"dialog\": {\n          \"title\": \"Очистити кеш\",\n          \"description\": \"Допоможіть вирішити такі проблеми, як не завантаження зображення, відсутність сторінок у просторі та не завантаження шрифтів. Це не вплине на ваші дані.\",\n          \"successHint\": \"Кеш очищено!\"\n        }\n      },\n      \"data\": {\n        \"fixYourData\": \"Виправте свої дані\",\n        \"fixButton\": \"Виправити\",\n        \"fixYourDataDescription\": \"Якщо у вас виникли проблеми з даними, ви можете спробувати їх виправити тут.\"\n      }\n    },\n    \"shortcutsPage\": {\n      \"menuLabel\": \"Ярлики\",\n      \"title\": \"Ярлики\",\n      \"editBindingHint\": \"Введіть нову прив'язку\",\n      \"searchHint\": \"Пошук\",\n      \"actions\": {\n        \"resetDefault\": \"Скинути за замовчуванням\"\n      },\n      \"errorPage\": {\n        \"message\": \"Не вдалося завантажити ярлики: {}\",\n        \"howToFix\": \"Спробуйте ще раз. Якщо проблема не зникне, зв’яжіться з GitHub.\"\n      },\n      \"resetDialog\": {\n        \"title\": \"Скинути ярлики\",\n        \"description\": \"Це скине всі ваші прив’язки клавіш до стандартних, ви не зможете скасувати це пізніше. Ви впевнені, що бажаєте продовжити?\",\n        \"buttonLabel\": \"Скинути\"\n      },\n      \"conflictDialog\": {\n        \"title\": \"{} зараз використовується\",\n        \"descriptionPrefix\": \"Цю комбінацію клавіш зараз використовує \",\n        \"descriptionSuffix\": \". Якщо ви заміните цю комбінацію клавіш, її буде видалено з {}.\",\n        \"confirmLabel\": \"Продовжити\"\n      },\n      \"editTooltip\": \"Натисніть, щоб почати редагування сполучення клавіш\",\n      \"keybindings\": {\n        \"toggleToDoList\": \"Перемкнути список справ\",\n        \"insertNewParagraphInCodeblock\": \"Вставте новий абзац\",\n        \"pasteInCodeblock\": \"Вставте кодовий блок\",\n        \"selectAllCodeblock\": \"Вибрати все\",\n        \"indentLineCodeblock\": \"Вставте два пропуски на початку рядка\",\n        \"outdentLineCodeblock\": \"Видалити два пропуски на початку рядка\",\n        \"twoSpacesCursorCodeblock\": \"Вставте два пробіли біля курсору\",\n        \"copy\": \"Вибір копії\",\n        \"paste\": \"Вставте вміст\",\n        \"cut\": \"Вирізати виділення\",\n        \"alignLeft\": \"Вирівняти текст по лівому краю\",\n        \"alignCenter\": \"Вирівняти текст по центру\",\n        \"alignRight\": \"Вирівняти текст праворуч\",\n        \"undo\": \"Скасувати\",\n        \"redo\": \"Повторити\",\n        \"convertToParagraph\": \"Перетворити блок на абзац\",\n        \"backspace\": \"Видалити\",\n        \"deleteLeftWord\": \"Видалити ліве слово\",\n        \"deleteLeftSentence\": \"Видалити ліве речення\",\n        \"delete\": \"Видалити правильний символ\",\n        \"deleteMacOS\": \"Видалити лівий символ\",\n        \"deleteRightWord\": \"Видалити правильне слово\",\n        \"moveCursorLeft\": \"Перемістити курсор ліворуч\",\n        \"moveCursorBeginning\": \"Перемістіть курсор на початок\",\n        \"moveCursorLeftWord\": \"Перемістити курсор на одне слово вліво\",\n        \"moveCursorLeftSelect\": \"Виберіть і перемістіть курсор ліворуч\",\n        \"moveCursorBeginSelect\": \"Виберіть і перемістіть курсор на початок\",\n        \"moveCursorLeftWordSelect\": \"Виберіть і перемістіть курсор ліворуч на одне слово\",\n        \"moveCursorRight\": \"Перемістіть курсор праворуч\",\n        \"moveCursorEnd\": \"Перемістіть курсор до кінця\",\n        \"moveCursorRightWord\": \"Перемістіть курсор на одне слово вправо\",\n        \"moveCursorRightSelect\": \"Виберіть і перемістіть курсор праворуч\",\n        \"moveCursorEndSelect\": \"Виберіть і перемістіть курсор до кінця\",\n        \"moveCursorRightWordSelect\": \"Виділіть і перемістіть курсор на одне слово вправо\",\n        \"moveCursorUp\": \"Перемістіть курсор вгору\",\n        \"moveCursorTopSelect\": \"Виберіть і перемістіть курсор угору\",\n        \"moveCursorTop\": \"Перемістіть курсор вгору\",\n        \"moveCursorUpSelect\": \"Виберіть і перемістіть курсор вгору\",\n        \"moveCursorBottomSelect\": \"Виберіть і перемістіть курсор униз\",\n        \"moveCursorBottom\": \"Перемістіть курсор униз\",\n        \"moveCursorDown\": \"Перемістіть курсор вниз\",\n        \"moveCursorDownSelect\": \"Виберіть і перемістіть курсор вниз\",\n        \"home\": \"Прокрутіть до верху\",\n        \"end\": \"Прокрутіть донизу\",\n        \"toggleBold\": \"Переключити жирний шрифт\",\n        \"toggleItalic\": \"Перемкнути курсив\",\n        \"toggleUnderline\": \"Перемкнути підкреслення\",\n        \"toggleStrikethrough\": \"Переключити закреслене\",\n        \"toggleCode\": \"Перемкнути вбудований код\",\n        \"toggleHighlight\": \"Перемкнути виділення\",\n        \"showLinkMenu\": \"Показати меню посилань\",\n        \"openInlineLink\": \"Відкрити вбудоване посилання\",\n        \"openLinks\": \"Відкрити всі вибрані посилання\",\n        \"indent\": \"Відступ\",\n        \"outdent\": \"Відступ\",\n        \"exit\": \"Вийти з редагування\",\n        \"pageUp\": \"Прокрутіть одну сторінку вгору\",\n        \"pageDown\": \"Прокрутіть одну сторінку вниз\",\n        \"selectAll\": \"Вибрати все\",\n        \"pasteWithoutFormatting\": \"Вставте вміст без форматування\",\n        \"showEmojiPicker\": \"Показати засіб вибору емодзі\",\n        \"enterInTableCell\": \"Додати розрив рядка в таблицю\",\n        \"leftInTableCell\": \"Перемістити таблицю на одну клітинку вліво\",\n        \"rightInTableCell\": \"Перемістити таблицю на одну клітинку праворуч\",\n        \"upInTableCell\": \"Перейти на одну клітинку вгору в таблиці\",\n        \"downInTableCell\": \"Перемістіть таблицю на одну клітинку вниз\",\n        \"tabInTableCell\": \"Перейти до наступної доступної клітинки в таблиці\",\n        \"shiftTabInTableCell\": \"Перейти до попередньо доступної клітинки в таблиці\",\n        \"backSpaceInTableCell\": \"Зупинка на початку клітини\"\n      },\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"Вставте новий абзац поруч із блоком коду\",\n        \"codeBlockIndentLines\": \"Вставте два пробіли на початку рядка в блоці коду\",\n        \"codeBlockOutdentLines\": \"Видалити два пробіли на початку рядка в блоці коду\",\n        \"codeBlockAddTwoSpaces\": \"Вставте два пробіли в позиції курсора в блоці коду\",\n        \"codeBlockSelectAll\": \"Виберіть весь вміст у блоці коду\",\n        \"codeBlockPasteText\": \"Вставте текст у кодовий блок\",\n        \"textAlignLeft\": \"Вирівняти текст по лівому краю\",\n        \"textAlignCenter\": \"Вирівняти текст по центру\",\n        \"textAlignRight\": \"Вирівняти текст праворуч\"\n      },\n      \"couldNotLoadErrorMsg\": \"Не вдалося завантажити ярлики. Повторіть спробу\",\n      \"couldNotSaveErrorMsg\": \"Не вдалося зберегти ярлики. Повторіть спробу\"\n    },\n    \"aiPage\": {\n      \"title\": \"Налаштування AI\",\n      \"menuLabel\": \"Налаштування AI\",\n      \"keys\": {\n        \"enableAISearchTitle\": \"AI Пошук\",\n        \"aiSettingsDescription\": \"Виберіть або налаштуйте моделі ШІ, які використовуються в @:appName . Для найкращої продуктивності ми рекомендуємо використовувати параметри моделі за замовчуванням\",\n        \"loginToEnableAIFeature\": \"Функції ШІ вмикаються лише після входу в @:appName Cloud. Якщо у вас немає облікового запису @:appName , перейдіть до «Мого облікового запису», щоб зареєструватися\",\n        \"llmModel\": \"Модель мови\",\n        \"llmModelType\": \"Тип мовної моделі\",\n        \"downloadLLMPrompt\": \"Завантажити {}\",\n        \"downloadAppFlowyOfflineAI\": \"Завантаження офлайн-пакету AI дозволить працювати AI на вашому пристрої. Ви хочете продовжити?\",\n        \"downloadLLMPromptDetail\": \"Завантаження {} локальної моделі займе до {} пам’яті. Ви хочете продовжити?\",\n        \"downloadBigFilePrompt\": \"Завантаження може зайняти близько 10 хвилин\",\n        \"downloadAIModelButton\": \"Завантажити\",\n        \"downloadingModel\": \"Завантаження\",\n        \"localAILoaded\": \"Локальну модель AI успішно додано та готово до використання\",\n        \"localAIStart\": \"Розпочинається локальний AI Chat...\",\n        \"localAILoading\": \"Локальна модель чату ШІ завантажується...\",\n        \"localAIStopped\": \"Місцевий ШІ зупинився\",\n        \"failToLoadLocalAI\": \"Не вдалося запустити локальний ШІ\",\n        \"restartLocalAI\": \"Перезапустіть локальний AI\",\n        \"disableLocalAITitle\": \"Вимкнути локальний ШІ\",\n        \"disableLocalAIDescription\": \"Ви бажаєте вимкнути локальний ШІ?\",\n        \"localAIToggleTitle\": \"Перемикач, щоб увімкнути або вимкнути локальний ШІ\",\n        \"offlineAIInstruction1\": \"Дотримуйтесь\",\n        \"offlineAIInstruction2\": \"інструкція\",\n        \"offlineAIInstruction3\": \"увімкнути автономний AI.\",\n        \"offlineAIDownload1\": \"Якщо ви не завантажили AppFlowy AI, будь ласка\",\n        \"offlineAIDownload2\": \"завантажити\",\n        \"offlineAIDownload3\": \"це перше\",\n        \"activeOfflineAI\": \"Активний\",\n        \"downloadOfflineAI\": \"Завантажити\",\n        \"openModelDirectory\": \"Відкрити папку\"\n      }\n    },\n    \"planPage\": {\n      \"menuLabel\": \"План\",\n      \"title\": \"Ціновий план\",\n      \"planUsage\": {\n        \"title\": \"Підсумок використання плану\",\n        \"storageLabel\": \"Зберігання\",\n        \"storageUsage\": \"{} із {} Гб\",\n        \"unlimitedStorageLabel\": \"Необмежене зберігання\",\n        \"collaboratorsLabel\": \"Члени\",\n        \"collaboratorsUsage\": \"{} з {}\",\n        \"aiResponseLabel\": \"Відповіді ШІ\",\n        \"aiResponseUsage\": \"{} з {}\",\n        \"unlimitedAILabel\": \"Необмежена кількість відповідей\",\n        \"proBadge\": \"Pro\",\n        \"aiMaxBadge\": \"AI Макс\",\n        \"aiOnDeviceBadge\": \"ШІ на пристрої для Mac\",\n        \"memberProToggle\": \"Більше учасників і необмежений ШІ\",\n        \"aiMaxToggle\": \"Необмежений ШІ та доступ до вдосконалених моделей\",\n        \"aiOnDeviceToggle\": \"Локальний штучний інтелект для повної конфіденційності\",\n        \"aiCredit\": {\n          \"title\": \"Додати кредит @:appName AI\",\n          \"price\": \"{}\",\n          \"priceDescription\": \"за 1000 кредитів\",\n          \"purchase\": \"Придбайте ШІ\",\n          \"info\": \"Додайте 1000 кредитів штучного інтелекту на робочий простір і плавно інтегруйте настроюваний штучний інтелект у свій робочий процес, щоб отримати розумніші та швидші результати з до:\",\n          \"infoItemOne\": \"10 000 відповідей на базу даних\",\n          \"infoItemTwo\": \"1000 відповідей на робочу область\"\n        },\n        \"currentPlan\": {\n          \"bannerLabel\": \"Поточний план\",\n          \"freeTitle\": \"Безкоштовно\",\n          \"proTitle\": \"Pro\",\n          \"teamTitle\": \"Команда\",\n          \"freeInfo\": \"Ідеально підходить для окремих осіб до 2 учасників, щоб організувати все\",\n          \"proInfo\": \"Ідеально підходить для малих і середніх команд до 10 учасників.\",\n          \"teamInfo\": \"Ідеально підходить для всіх продуктивних і добре організованих команд.\",\n          \"upgrade\": \"Змінити план\",\n          \"canceledInfo\": \"Ваш план скасовано, вас буде переведено на безкоштовний план {}.\"\n        },\n        \"addons\": {\n          \"title\": \"Додатки\",\n          \"addLabel\": \"Додати\",\n          \"activeLabel\": \"Додано\",\n          \"aiMax\": {\n            \"title\": \"AI Макс\",\n            \"description\": \"Необмежені відповіді AI на основі GPT-4o, Claude 3.5 Sonnet тощо\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"на користувача на місяць\",\n            \"billingInfo\": \"рахунок виставляється щорічно або {} щомісяця\"\n          },\n          \"aiOnDevice\": {\n            \"title\": \"ШІ на пристрої для Mac\",\n            \"description\": \"Запустіть Mistral 7B, LLAMA 3 та інші локальні моделі на вашому комп’ютері\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"Оплата за користувача на місяць виставляється щорічно\",\n            \"recommend\": \"Рекомендовано M1 або новіше\",\n            \"billingInfo\": \"рахунок виставляється щорічно або {} щомісяця\"\n          }\n        },\n        \"deal\": {\n          \"bannerLabel\": \"Новорічна угода!\",\n          \"title\": \"Розвивайте свою команду!\",\n          \"info\": \"Оновіть та заощаджуйте 10% на планах Pro та Team! Підвищте продуктивність робочого простору за допомогою нових потужних функцій, зокрема @:appName .\",\n          \"viewPlans\": \"Переглянути плани\"\n        }\n      }\n    },\n    \"billingPage\": {\n      \"menuLabel\": \"Виставлення рахунків\",\n      \"title\": \"Виставлення рахунків\",\n      \"plan\": {\n        \"title\": \"План\",\n        \"freeLabel\": \"Безкоштовно\",\n        \"proLabel\": \"Pro\",\n        \"planButtonLabel\": \"Змінити план\",\n        \"billingPeriod\": \"Розрахунковий період\",\n        \"periodButtonLabel\": \"Період редагування\"\n      },\n      \"paymentDetails\": {\n        \"title\": \"Платіжні реквізити\",\n        \"methodLabel\": \"Спосіб оплати\",\n        \"methodButtonLabel\": \"Метод редагування\"\n      },\n      \"addons\": {\n        \"title\": \"Додатки\",\n        \"addLabel\": \"Додати\",\n        \"removeLabel\": \"Видалити\",\n        \"renewLabel\": \"Відновити\",\n        \"aiMax\": {\n          \"label\": \"AI Макс\",\n          \"description\": \"Розблокуйте необмежену кількість штучного інтелекту та вдосконалених моделей\",\n          \"activeDescription\": \"Наступний рахунок-фактура має бути виставлено {}\",\n          \"canceledDescription\": \"AI Max буде доступний до {}\"\n        },\n        \"aiOnDevice\": {\n          \"label\": \"ШІ на пристрої для Mac\",\n          \"description\": \"Розблокуйте необмежений ШІ офлайн на своєму пристрої\",\n          \"activeDescription\": \"Наступний рахунок-фактура має бути виставлено {}\",\n          \"canceledDescription\": \"AI On-device для Mac буде доступний до {}\"\n        },\n        \"removeDialog\": {\n          \"title\": \"Видалити {}\",\n          \"description\": \"Ви впевнені, що хочете видалити {plan}? Ви негайно втратите доступ до функцій і переваг {plan}.\"\n        }\n      },\n      \"currentPeriodBadge\": \"ПОТОЧНИЙ\",\n      \"changePeriod\": \"Період зміни\",\n      \"planPeriod\": \"{} період\",\n      \"monthlyInterval\": \"Щомісяця\",\n      \"monthlyPriceInfo\": \"за місце сплачується щомісяця\",\n      \"annualInterval\": \"Щорічно\",\n      \"annualPriceInfo\": \"за місце, що виставляється щорічно\"\n    },\n    \"comparePlanDialog\": {\n      \"title\": \"Порівняйте та виберіть план\",\n      \"planFeatures\": \"План\\nособливості\",\n      \"current\": \"Поточний\",\n      \"actions\": {\n        \"upgrade\": \"Оновлення\",\n        \"downgrade\": \"Понизити\",\n        \"current\": \"Поточний\"\n      },\n      \"freePlan\": {\n        \"title\": \"Безкоштовно\",\n        \"description\": \"Для окремих осіб до 2 учасників, щоб організувати все\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"Безплатний назавжди\"\n      },\n      \"proPlan\": {\n        \"title\": \"Pro\",\n        \"description\": \"Для невеликих команд для управління проектами та знаннями команди\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"на користувача на місяць\\nвиставляється щорічно<inlang-LineFeed>\\n{} виставляється щомісяця\"\n      },\n      \"planLabels\": {\n        \"itemOne\": \"Робочі області\",\n        \"itemTwo\": \"Члени\",\n        \"itemThree\": \"Зберігання\",\n        \"itemFour\": \"Співпраця в реальному часі\",\n        \"itemFive\": \"Мобільний додаток\",\n        \"itemSix\": \"Відповіді ШІ\",\n        \"itemSeven\": \"Спеціальний простір імен\",\n        \"itemFileUpload\": \"Завантаження файлів\",\n        \"tooltipSix\": \"Термін служби означає кількість відповідей, які ніколи не скидаються\",\n        \"intelligentSearch\": \"Інтелектуальний пошук\",\n        \"tooltipSeven\": \"Дозволяє налаштувати частину URL-адреси для вашої робочої області\"\n      },\n      \"freeLabels\": {\n        \"itemOne\": \"сплачується за робоче місце\",\n        \"itemTwo\": \"До 2\",\n        \"itemThree\": \"5 ГБ\",\n        \"itemFour\": \"так\",\n        \"itemFive\": \"так\",\n        \"itemSix\": \"10 років життя\",\n        \"itemFileUpload\": \"До 7 Мб\",\n        \"intelligentSearch\": \"Інтелектуальний пошук\"\n      },\n      \"proLabels\": {\n        \"itemOne\": \"Оплата за робоче місце\",\n        \"itemTwo\": \"До 10\",\n        \"itemThree\": \"Необмежений\",\n        \"itemFour\": \"так\",\n        \"itemFive\": \"так\",\n        \"itemSix\": \"Необмежений\",\n        \"itemFileUpload\": \"Необмежений\",\n        \"intelligentSearch\": \"Інтелектуальний пошук\"\n      },\n      \"paymentSuccess\": {\n        \"title\": \"Тепер ви на плані {}!\",\n        \"description\": \"Ваш платіж успішно оброблено, і ваш план оновлено до @:appName {}. Ви можете переглянути деталі свого плану на сторінці План\"\n      },\n      \"downgradeDialog\": {\n        \"title\": \"Ви впевнені, що хочете понизити свій план?\",\n        \"description\": \"Пониження вашого плану призведе до повернення до безкоштовного плану. Учасники можуть втратити доступ до цього робочого простору, і вам може знадобитися звільнити місце, щоб досягти лімітів пам’яті безкоштовного плану.\",\n        \"downgradeLabel\": \"Пониження плану\"\n      }\n    },\n    \"cancelSurveyDialog\": {\n      \"title\": \"Шкода, що ви йдете\",\n      \"description\": \"Нам шкода, що ви йдете. Ми будемо раді почути ваш відгук, щоб допомогти нам покращити @:appName . Знайдіть хвилинку, щоб відповісти на кілька запитань.\",\n      \"commonOther\": \"Інший\",\n      \"otherHint\": \"Напишіть свою відповідь тут\",\n      \"questionOne\": {\n        \"question\": \"Що спонукало вас скасувати підписку на AppFlowy Pro?\",\n        \"answerOne\": \"Зависока вартість\",\n        \"answerTwo\": \"Характеристики не виправдали очікувань\",\n        \"answerThree\": \"Знайшов кращу альтернативу\",\n        \"answerFour\": \"Використовував недостатньо, щоб виправдати витрати\",\n        \"answerFive\": \"Проблема з обслуговуванням або технічні проблеми\"\n      },\n      \"questionTwo\": {\n        \"question\": \"Наскільки ймовірно, що ви подумаєте про повторну підписку на AppFlowy Pro у майбутньому?\",\n        \"answerOne\": \"Ймовірно\",\n        \"answerTwo\": \"Певною мірою ймовірно\",\n        \"answerThree\": \"Не впевнений\",\n        \"answerFour\": \"Малоймовірно\",\n        \"answerFive\": \"Дуже малоймовірно\"\n      },\n      \"questionThree\": {\n        \"question\": \"Яку функцію Pro ви цінували найбільше під час підписки?\",\n        \"answerOne\": \"Багатокористувацька співпраця\",\n        \"answerTwo\": \"Довша історія версій\",\n        \"answerThree\": \"Необмежена кількість відповідей ШІ\",\n        \"answerFour\": \"Доступ до локальних моделей ШІ\"\n      },\n      \"questionFour\": {\n        \"question\": \"Як би ви описали свій загальний досвід роботи з AppFlowy?\",\n        \"answerOne\": \"Чудово\",\n        \"answerTwo\": \"Добре\",\n        \"answerThree\": \"Середній\",\n        \"answerFour\": \"Нижче середнього\",\n        \"answerFive\": \"Незадоволений\"\n      }\n    },\n    \"common\": {\n      \"reset\": \"Скинути\"\n    },\n    \"menu\": {\n      \"appearance\": \"Вигляд\",\n      \"language\": \"Мова\",\n      \"user\": \"Користувач\",\n      \"files\": \"Файли\",\n      \"notifications\": \"Сповіщення\",\n      \"open\": \"Відкрити налаштування\",\n      \"logout\": \"Вийти\",\n      \"logoutPrompt\": \"Ви впевнені, що хочете вийти?\",\n      \"selfEncryptionLogoutPrompt\": \"Ви впевнені, що хочете вийти? Будь ласка, переконайтеся, що ви скопіювали секрет шифрування\",\n      \"syncSetting\": \"Налаштування синхронізації\",\n      \"cloudSettings\": \"Налаштування хмари\",\n      \"enableSync\": \"Увімкнути синхронізацію\",\n      \"enableEncrypt\": \"Шифрувати дані\",\n      \"cloudURL\": \"Базовий URL\",\n      \"invalidCloudURLScheme\": \"Недійсна схема\",\n      \"cloudServerType\": \"Хмарний сервер\",\n      \"cloudServerTypeTip\": \"Зауважте, що після перемикання хмарного сервера ваш поточний обліковий запис може вийти з системи\",\n      \"cloudLocal\": \"Місцевий\",\n      \"cloudAppFlowy\": \"@:appName Cloud Beta\",\n      \"cloudAppFlowySelfHost\": \"@:appName Cloud на власному сервері\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"URL-адреса хмари не може бути пустою\",\n      \"clickToCopy\": \"Натисніть, щоб скопіювати\",\n      \"selfHostStart\": \"Якщо у вас немає сервера, зверніться до\",\n      \"selfHostContent\": \"документ\",\n      \"selfHostEnd\": \"для вказівок щодо самостійного розміщення власного сервера\",\n      \"cloudURLHint\": \"Введіть базову URL-адресу вашого сервера\",\n      \"cloudWSURL\": \"URL-адреса Websocket\",\n      \"cloudWSURLHint\": \"Введіть адресу веб-сокета вашого сервера\",\n      \"restartApp\": \"Перезапустіть\",\n      \"restartAppTip\": \"Перезапустіть програму, щоб зміни набули чинності. Зауважте, що це може привести до виходу з вашого поточного облікового запису.\",\n      \"changeServerTip\": \"Після зміни сервера необхідно натиснути кнопку перезавантаження, щоб зміни набули чинності\",\n      \"enableEncryptPrompt\": \"Активуйте шифрування для захисту ваших даних за допомогою цього секретного ключа. Зберігайте його надійно; після увімкнення його неможливо вимкнути. Якщо втрачено, ваші дані стають недоступними. Клацніть, щоб скопіювати\",\n      \"inputEncryptPrompt\": \"Будь ласка, введіть свій секрет для шифрування для\",\n      \"clickToCopySecret\": \"Клацніть, щоб скопіювати секрет\",\n      \"configServerSetting\": \"Налаштуйте параметри свого сервера\",\n      \"configServerGuide\": \"Вибравши `Швидкий старт`, перейдіть до `Налаштування`, а потім до «Налаштування хмари», щоб налаштувати свій автономний сервер.\",\n      \"inputTextFieldHint\": \"Ваш секрет\",\n      \"historicalUserList\": \"Історія входу користувача\",\n      \"historicalUserListTooltip\": \"У цьому списку відображаються ваші анонімні облікові записи. Ви можете клацнути на обліковий запис, щоб переглянути його деталі. Анонімні облікові записи створюються, натискаючи кнопку 'Розпочати роботу'\",\n      \"openHistoricalUser\": \"Клацніть, щоб відкрити анонімний обліковий запис\",\n      \"customPathPrompt\": \"Зберігання папки даних @:appName у папці, що синхронізується з хмарою, як-от Google Drive, може становити ризик. Якщо база даних у цій папці доступна або змінена з кількох місць одночасно, це може призвести до конфліктів синхронізації та можливого пошкодження даних\",\n      \"importAppFlowyData\": \"Імпорт даних із зовнішньої @:appName\",\n      \"importingAppFlowyDataTip\": \"Виконується імпорт даних. Будь ласка, не закривайте додаток\",\n      \"importAppFlowyDataDescription\": \"Скопіюйте дані із зовнішньої папки @:appName та імпортуйте їх у поточну папку даних AppFlowy\",\n      \"importSuccess\": \"Папку даних @:appName успішно імпортовано\",\n      \"importFailed\": \"Не вдалося імпортувати папку даних @:appName\",\n      \"importGuide\": \"Щоб отримати додаткові відомості, перегляньте документ, на який посилається\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Увімкнути сповіщення\",\n        \"hint\": \"Вимкніть, щоб локальні сповіщення не з’являлися.\"\n      },\n      \"showNotificationsIcon\": {\n        \"label\": \"Показати значок сповіщень\",\n        \"hint\": \"Вимкніть, щоб приховати піктограму сповіщень на бічній панелі.\"\n      },\n      \"archiveNotifications\": {\n        \"allSuccess\": \"Усі сповіщення успішно заархівовано\",\n        \"success\": \"Сповіщення успішно заархівовано\"\n      },\n      \"markAsReadNotifications\": {\n        \"allSuccess\": \"Усі позначені як успішно прочитані\",\n        \"success\": \"Позначено як успішно прочитане\"\n      },\n      \"action\": {\n        \"markAsRead\": \"Відзначити як прочитане\",\n        \"multipleChoice\": \"Виберіть більше\",\n        \"archive\": \"Архів\"\n      },\n      \"settings\": {\n        \"settings\": \"Налаштування\",\n        \"markAllAsRead\": \"Позначити все як прочитане\",\n        \"archiveAll\": \"Архівувати все\"\n      },\n      \"emptyInbox\": {\n        \"title\": \"Ще немає сповіщень\",\n        \"description\": \"Тут ви отримуватимете сповіщення про @згадки\"\n      },\n      \"emptyUnread\": {\n        \"title\": \"Немає непрочитаних сповіщень\",\n        \"description\": \"Ви все наздогнали!\"\n      },\n      \"emptyArchived\": {\n        \"title\": \"Немає архівованих сповіщень\",\n        \"description\": \"Ви ще не заархівували жодного сповіщення\"\n      },\n      \"tabs\": {\n        \"inbox\": \"Вхідні\",\n        \"unread\": \"Непрочитаний\",\n        \"archived\": \"Архівовано\"\n      },\n      \"refreshSuccess\": \"Сповіщення успішно оновлено\",\n      \"titles\": {\n        \"notifications\": \"Сповіщення\",\n        \"reminder\": \"Нагадування\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"Скинути це налаштування\",\n      \"fontFamily\": {\n        \"label\": \"Шрифт\",\n        \"search\": \"Пошук\",\n        \"defaultFont\": \"Система\"\n      },\n      \"themeMode\": {\n        \"label\": \"Режим теми\",\n        \"light\": \"Світлий режим\",\n        \"dark\": \"Темний режим\",\n        \"system\": \"Системна\"\n      },\n      \"fontScaleFactor\": \"Коефіцієнт масштабування шрифту\",\n      \"documentSettings\": {\n        \"cursorColor\": \"Колір курсору документа\",\n        \"selectionColor\": \"Колір виділення документа\",\n        \"pickColor\": \"Виберіть колір\",\n        \"colorShade\": \"Колірний відтінок\",\n        \"opacity\": \"Непрозорість\",\n        \"hexEmptyError\": \"Шістнадцяткове поле кольору не може бути порожнім\",\n        \"hexLengthError\": \"Шістнадцяткове значення має містити 6 цифр\",\n        \"hexInvalidError\": \"Недійсне шістнадцяткове значення\",\n        \"opacityEmptyError\": \"Непрозорість не може бути пустою\",\n        \"opacityRangeError\": \"Непрозорість має бути від 1 до 100\",\n        \"app\": \"Додаток\",\n        \"flowy\": \"Текучий\",\n        \"apply\": \"Застосувати\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Напрямок макету\",\n        \"hint\": \"Контролюйте напрямок контенту на вашому екрані, зліва направо або справа наліво.\",\n        \"ltr\": \"Зліва направо\",\n        \"rtl\": \"Справа наліво\"\n      },\n      \"textDirection\": {\n        \"label\": \"Текстовий напрямок за замовчуванням\",\n        \"hint\": \"Вкажіть, чи має текст починатися зліва чи справа як за замовчуванням.\",\n        \"ltr\": \"Зліва направо\",\n        \"rtl\": \"Справа наліво\",\n        \"auto\": \"АВТО\",\n        \"fallback\": \"Такий же, як і напрямок макету\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Завантажити\",\n        \"uploadTheme\": \"Завантажити тему\",\n        \"description\": \"Завантажте свою власну тему @:appName, скориставшись кнопкою нижче.\",\n        \"loading\": \"Будь ласка, зачекайте, поки ми перевіряємо та завантажуємо вашу тему...\",\n        \"uploadSuccess\": \"Вашу тему успішно завантажено\",\n        \"deletionFailure\": \"Не вдалося видалити тему. Спробуйте видалити її вручну.\",\n        \"filePickerDialogTitle\": \"Виберіть файл .flowy_plugin\",\n        \"urlUploadFailure\": \"Не вдалося відкрити URL: {}\",\n        \"failure\": \"Тему, яка була завантажена, має неправильний формат.\"\n      },\n      \"theme\": \"Тема\",\n      \"builtInsLabel\": \"Вбудовані теми\",\n      \"pluginsLabel\": \"Плагіни\",\n      \"dateFormat\": {\n        \"label\": \"Формат дати\",\n        \"local\": \"Локальний\",\n        \"us\": \"US\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"Дружній\",\n        \"dmy\": \"Д/М/Р\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Формат часу\",\n        \"twelveHour\": \"Дванадцятигодинний\",\n        \"twentyFourHour\": \"Двадцять чотири години\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"Показувати діалогове вікно імені при створенні сторінки\",\n      \"enableRTLToolbarItems\": \"Увімкнути елементи панелі інструментів RTL\",\n      \"members\": {\n        \"title\": \"Налаштування учасників\",\n        \"inviteMembers\": \"Запросити учасників\",\n        \"inviteHint\": \"Запросити електронною поштою\",\n        \"sendInvite\": \"Надіслати запрошення\",\n        \"copyInviteLink\": \"Копіювати посилання для запрошення\",\n        \"label\": \"Члени\",\n        \"user\": \"Користувач\",\n        \"role\": \"Роль\",\n        \"removeFromWorkspace\": \"Видалити з робочої області\",\n        \"removeFromWorkspaceSuccess\": \"Успішно видалено з робочої області\",\n        \"removeFromWorkspaceFailed\": \"Не вдалося видалити з робочої області\",\n        \"owner\": \"Власник\",\n        \"guest\": \"Гість\",\n        \"member\": \"Член\",\n        \"memberHintText\": \"Учасник може читати та редагувати сторінки\",\n        \"guestHintText\": \"Гість може читати, реагувати, коментувати та редагувати певні сторінки з дозволу.\",\n        \"emailInvalidError\": \"Недійсна електронна адреса, будь ласка, перевірте та повторіть спробу\",\n        \"emailSent\": \"Електронний лист надіслано, перевірте папку \\\"Вхідні\\\".\",\n        \"members\": \"членів\",\n        \"membersCount\": {\n          \"zero\": \"{} учасників\",\n          \"one\": \"{} член\",\n          \"other\": \"{} учасників\"\n        },\n        \"inviteFailedDialogTitle\": \"Не вдалося надіслати запрошення\",\n        \"inviteFailedMemberLimit\": \"Досягнуто ліміту учасників. Оновіть його, щоб запросити більше учасників.\",\n        \"inviteFailedMemberLimitMobile\": \"Ваша робоча область досягла ліміту учасників. Оновіть комп’ютер, щоб розблокувати більше функцій.\",\n        \"memberLimitExceeded\": \"Досягнуто обмеження кількості учасників, будь ласка, запросіть більше учасників \",\n        \"memberLimitExceededUpgrade\": \"оновлення\",\n        \"memberLimitExceededPro\": \"Досягнуто ліміту учасників, якщо вам потрібно більше учасників, зв’яжіться \",\n        \"memberLimitExceededProContact\": \"support@appflowy.io\",\n        \"failedToAddMember\": \"Не вдалося додати учасника\",\n        \"addMemberSuccess\": \"Учасника успішно додано\",\n        \"removeMember\": \"Видалити учасника\",\n        \"areYouSureToRemoveMember\": \"Ви впевнені, що хочете видалити цього учасника?\",\n        \"inviteMemberSuccess\": \"Запрошення успішно надіслано\",\n        \"failedToInviteMember\": \"Не вдалося запросити учасника\",\n        \"workspaceMembersError\": \"На жаль, сталася помилка\",\n        \"workspaceMembersErrorDescription\": \"Не вдалося завантажити список учасників. Спробуйте пізніше\"\n      }\n    },\n    \"files\": {\n      \"copy\": \"Копіювати\",\n      \"defaultLocation\": \"Де зараз зберігаються ваші дані\",\n      \"exportData\": \"Експортуйте свої дані\",\n      \"doubleTapToCopy\": \"Подвійний натиск для копіювання шляху\",\n      \"restoreLocation\": \"Відновити до шляху за замовчуванням @:appName\",\n      \"customizeLocation\": \"Відкрити іншу папку\",\n      \"restartApp\": \"Будь ласка, перезапустіть програму для врахування змін.\",\n      \"exportDatabase\": \"Експорт бази даних\",\n      \"selectFiles\": \"Виберіть файли, які потрібно експортувати\",\n      \"selectAll\": \"Вибрати всі\",\n      \"deselectAll\": \"Зняти виділення з усіх\",\n      \"createNewFolder\": \"Створити нову папку\",\n      \"createNewFolderDesc\": \"Повідомте нам, де ви хочете зберегти свої дані\",\n      \"defineWhereYourDataIsStored\": \"Визначте, де зберігаються ваші дані\",\n      \"open\": \"Відкрити\",\n      \"openFolder\": \"Відкрити існуючу папку\",\n      \"openFolderDesc\": \"Читати та записувати в вашу існуючу папку @:appName\",\n      \"folderHintText\": \"ім'я папки\",\n      \"location\": \"Створення нової папки\",\n      \"locationDesc\": \"Оберіть ім'я для папки з даними @:appName\",\n      \"browser\": \"Перегляд\",\n      \"create\": \"Створити\",\n      \"set\": \"Встановити\",\n      \"folderPath\": \"Шлях до зберігання вашої папки\",\n      \"locationCannotBeEmpty\": \"Шлях не може бути порожнім\",\n      \"pathCopiedSnackbar\": \"Шлях збереження файлів скопійовано в буфер обміну!\",\n      \"changeLocationTooltips\": \"Змінити каталог даних\",\n      \"change\": \"Змінити\",\n      \"openLocationTooltips\": \"Відкрити інший каталог даних\",\n      \"openCurrentDataFolder\": \"Відкрити поточний каталог даних\",\n      \"recoverLocationTooltips\": \"Скинути до каталогу даних за замовчуванням @:appName\",\n      \"exportFileSuccess\": \"Файл успішно експортовано!\",\n      \"exportFileFail\": \"Помилка експорту файлу!\",\n      \"export\": \"Експорт\",\n      \"clearCache\": \"Очистити кеш\",\n      \"clearCacheDesc\": \"Якщо у вас виникли проблеми із завантаженням зображень або неправильним відображенням шрифтів, спробуйте очистити кеш. Ця дія не видалить ваші дані користувача.\",\n      \"areYouSureToClearCache\": \"Ви впевнені, що хочете очистити кеш?\",\n      \"clearCacheSuccess\": \"Кеш успішно очищено!\"\n    },\n    \"user\": {\n      \"name\": \"Ім'я\",\n      \"email\": \"Електронна пошта\",\n      \"tooltipSelectIcon\": \"Обрати значок\",\n      \"selectAnIcon\": \"Обрати значок\",\n      \"pleaseInputYourOpenAIKey\": \"Будь ласка, введіть ваш ключ AI\",\n      \"clickToLogout\": \"Натисніть, щоб вийти з поточного облікового запису\",\n      \"pleaseInputYourStabilityAIKey\": \"будь ласка, введіть ключ стабільності ШІ\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"Персональна інформація\",\n      \"username\": \"Ім'я користувача\",\n      \"usernameEmptyError\": \"Ім'я користувача не може бути пустим\",\n      \"about\": \"Про\",\n      \"pushNotifications\": \"Push-сповіщення\",\n      \"support\": \"Підтримка\",\n      \"joinDiscord\": \"Приєднуйтесь до нас у Discord\",\n      \"privacyPolicy\": \"Політика конфіденційності\",\n      \"userAgreement\": \"Угода користувача\",\n      \"termsAndConditions\": \"Правила та умови\",\n      \"userprofileError\": \"Не вдалося завантажити профіль користувача\",\n      \"userprofileErrorDescription\": \"Будь ласка, спробуйте вийти та увійти знову, щоб перевірити, чи проблема не зникає.\",\n      \"selectLayout\": \"Виберіть макет\",\n      \"selectStartingDay\": \"Виберіть день початку\",\n      \"version\": \"Версія\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"Сполучення клавіш\",\n      \"command\": \"Команда\",\n      \"keyBinding\": \"Сполучення клавіш\",\n      \"addNewCommand\": \"Додати нову команду\",\n      \"updateShortcutStep\": \"Натисніть бажану комбінацію клавіш і натисніть ENTER\",\n      \"shortcutIsAlreadyUsed\": \"Це скорочення вже використовується для: {conflict}\",\n      \"resetToDefault\": \"Скинути до стандартних налаштувань скорочень\",\n      \"couldNotLoadErrorMsg\": \"Не вдалося завантажити скорочення. Спробуйте ще раз\",\n      \"couldNotSaveErrorMsg\": \"Не вдалося зберегти скорочення. Спробуйте ще раз\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Ви впевнені, що хочете видалити цей вигляд?\",\n    \"createView\": \"Нова\",\n    \"title\": {\n      \"placeholder\": \"Без назви\"\n    },\n    \"settings\": {\n      \"filter\": \"Фільтр\",\n      \"sort\": \"Сортування\",\n      \"sortBy\": \"Сортувати за\",\n      \"properties\": \"Властивості\",\n      \"reorderPropertiesTooltip\": \"Перетягніть для зміни порядку властивостей\",\n      \"group\": \"Групувати\",\n      \"addFilter\": \"Додати фільтр\",\n      \"deleteFilter\": \"Видалити фільтр\",\n      \"filterBy\": \"Фільтрувати за...\",\n      \"typeAValue\": \"Введіть значення...\",\n      \"layout\": \"Макет\",\n      \"databaseLayout\": \"Вид бази даних\",\n      \"viewList\": {\n        \"zero\": \"0 переглядів\",\n        \"one\": \"{count} перегляд\",\n        \"other\": \"{count} переглядів\"\n      },\n      \"editView\": \"Редагувати перегляд\",\n      \"boardSettings\": \"Налаштування дошки\",\n      \"calendarSettings\": \"Налаштування календаря\",\n      \"createView\": \"Новий вигляд\",\n      \"duplicateView\": \"Дубльований перегляд\",\n      \"deleteView\": \"Видалити перегляд\",\n      \"numberOfVisibleFields\": \"показано {}\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Містить\",\n      \"doesNotContain\": \"Не містить\",\n      \"endsWith\": \"Закінчується на\",\n      \"startWith\": \"Починається з\",\n      \"is\": \"Є\",\n      \"isNot\": \"Не є\",\n      \"isEmpty\": \"Порожнє\",\n      \"isNotEmpty\": \"Не порожнє\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Не\",\n        \"startWith\": \"Починається з\",\n        \"endWith\": \"Закінчується на\",\n        \"isEmpty\": \"порожнє\",\n        \"isNotEmpty\": \"не порожнє\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Відзначено\",\n      \"isUnchecked\": \"Не відзначено\",\n      \"choicechipPrefix\": {\n        \"is\": \"є\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"є завершено\",\n      \"isIncomplted\": \"є незавершено\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"є\",\n      \"isNot\": \"не є\",\n      \"contains\": \"Містить\",\n      \"doesNotContain\": \"Не містить\",\n      \"isEmpty\": \"Порожнє\",\n      \"isNotEmpty\": \"Не порожнє\"\n    },\n    \"dateFilter\": {\n      \"is\": \"Є\",\n      \"before\": \"Є раніше\",\n      \"after\": \"Є після\",\n      \"onOrBefore\": \"Увімкнено або раніше\",\n      \"onOrAfter\": \"Увімкнено або після\",\n      \"between\": \"Знаходиться між\",\n      \"empty\": \"Пусто\",\n      \"notEmpty\": \"Не порожній\",\n      \"choicechipPrefix\": {\n        \"before\": \"Раніше\",\n        \"after\": \"Після\",\n        \"onOrBefore\": \"На або раніше\",\n        \"onOrAfter\": \"На або після\",\n        \"isEmpty\": \"Пусто\",\n        \"isNotEmpty\": \"Не порожній\"\n      }\n    },\n    \"numberFilter\": {\n      \"equal\": \"Дорівнює\",\n      \"notEqual\": \"Не дорівнює\",\n      \"lessThan\": \"Менше ніж\",\n      \"greaterThan\": \"Більше ніж\",\n      \"lessThanOrEqualTo\": \"Менше або дорівнює\",\n      \"greaterThanOrEqualTo\": \"Більше або дорівнює\",\n      \"isEmpty\": \"Пусто\",\n      \"isNotEmpty\": \"Не порожній\"\n    },\n    \"field\": {\n      \"hide\": \"Сховати\",\n      \"show\": \"Показати\",\n      \"insertLeft\": \"Вставити зліва\",\n      \"insertRight\": \"Вставити справа\",\n      \"duplicate\": \"Дублювати\",\n      \"delete\": \"Видалити\",\n      \"wrapCellContent\": \"Обернути текст\",\n      \"clear\": \"Очистити комірки\",\n      \"textFieldName\": \"Текст\",\n      \"checkboxFieldName\": \"Чекбокс\",\n      \"dateFieldName\": \"Дата\",\n      \"updatedAtFieldName\": \"Остання зміна\",\n      \"createdAtFieldName\": \"Створено\",\n      \"numberFieldName\": \"Число\",\n      \"singleSelectFieldName\": \"Вибір\",\n      \"multiSelectFieldName\": \"Вибір кількох\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Чек-лист\",\n      \"relationFieldName\": \"Відношення\",\n      \"summaryFieldName\": \"AI Резюме\",\n      \"timeFieldName\": \"Час\",\n      \"translateFieldName\": \"ШІ Перекладач\",\n      \"translateTo\": \"Перекласти на\",\n      \"numberFormat\": \"Формат числа\",\n      \"dateFormat\": \"Формат дати\",\n      \"includeTime\": \"Включити час\",\n      \"isRange\": \"Кінцева дата\",\n      \"dateFormatFriendly\": \"Місяць День, Рік\",\n      \"dateFormatISO\": \"Рік-Місяць-День\",\n      \"dateFormatLocal\": \"Місяць/День/Рік\",\n      \"dateFormatUS\": \"Рік/Місяць/День\",\n      \"dateFormatDayMonthYear\": \"День/Місяць/Рік\",\n      \"timeFormat\": \"Формат часу\",\n      \"invalidTimeFormat\": \"Неправильний формат\",\n      \"timeFormatTwelveHour\": \"12 годин\",\n      \"timeFormatTwentyFourHour\": \"24 години\",\n      \"clearDate\": \"Очистити дату\",\n      \"dateTime\": \"Дата, час\",\n      \"startDateTime\": \"Дата початку час\",\n      \"endDateTime\": \"Кінцева дата час\",\n      \"failedToLoadDate\": \"Не вдалося завантажити значення дати\",\n      \"selectTime\": \"Виберіть час\",\n      \"selectDate\": \"Виберіть дату\",\n      \"visibility\": \"Видимість\",\n      \"propertyType\": \"Тип власності\",\n      \"addSelectOption\": \"Додати опцію\",\n      \"typeANewOption\": \"Введіть новий параметр\",\n      \"optionTitle\": \"Опції\",\n      \"addOption\": \"Додати опцію\",\n      \"editProperty\": \"Редагувати властивість\",\n      \"newProperty\": \"Додати колонку\",\n      \"openRowDocument\": \"Відкрити як сторінку\",\n      \"deleteFieldPromptMessage\": \"Ви впевнені? Ця властивість буде видалена\",\n      \"clearFieldPromptMessage\": \"Ти впевнений? Усі клітинки в цьому стовпці будуть порожні\",\n      \"newColumn\": \"Нова колонка\",\n      \"format\": \"Формат\",\n      \"reminderOnDateTooltip\": \"Ця клітинка має заплановане нагадування\",\n      \"optionAlreadyExist\": \"Варіант вже існує\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Додати нове поле\",\n      \"fieldDragElementTooltip\": \"Натисніть, щоб відкрити меню\",\n      \"showHiddenFields\": {\n        \"one\": \"Показати {} приховане поле\",\n        \"many\": \"Показати {} приховані поля\",\n        \"other\": \"Показати {} приховані полі\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"Сховати {} приховане поле\",\n        \"many\": \"Сховати {} приховані поля\",\n        \"other\": \"Сховати {} приховані поля\"\n      },\n      \"openAsFullPage\": \"Відкрити як повну сторінку\",\n      \"moreRowActions\": \"Більше дій рядків\"\n    },\n    \"sort\": {\n      \"ascending\": \"У висхідному порядку\",\n      \"descending\": \"У спадному порядку\",\n      \"by\": \"За\",\n      \"empty\": \"Немає активних сортувань\",\n      \"cannotFindCreatableField\": \"Не вдається знайти відповідне поле для сортування\",\n      \"deleteAllSorts\": \"Видалити всі сортування\",\n      \"addSort\": \"Додати сортування\",\n      \"removeSorting\": \"Ви хочете видалити сортування?\",\n      \"fieldInUse\": \"Ви вже сортуєте за цим полем\"\n    },\n    \"row\": {\n      \"duplicate\": \"Дублювати\",\n      \"delete\": \"Видалити\",\n      \"titlePlaceholder\": \"Без назви\",\n      \"textPlaceholder\": \"Порожньо\",\n      \"copyProperty\": \"Скопійовано властивість в буфер обміну\",\n      \"count\": \"Кількість\",\n      \"newRow\": \"Новий рядок\",\n      \"action\": \"Дія\",\n      \"add\": \"Клацніть, щоб додати нижче\",\n      \"drag\": \"Перетягніть для переміщення\",\n      \"deleteRowPrompt\": \"Ви впевнені, що хочете видалити цей рядок? Цю дію не можна скасувати\",\n      \"deleteCardPrompt\": \"Ви впевнені, що хочете видалити цю картку? Цю дію не можна скасувати\",\n      \"dragAndClick\": \"Перетягніть, щоб перемістити, натисніть, щоб відкрити меню\",\n      \"insertRecordAbove\": \"Вставте запис вище\",\n      \"insertRecordBelow\": \"Вставте запис нижче\",\n      \"noContent\": \"Немає вмісту\"\n    },\n    \"selectOption\": {\n      \"create\": \"Створити\",\n      \"purpleColor\": \"Фіолетовий\",\n      \"pinkColor\": \"Рожевий\",\n      \"lightPinkColor\": \"Світло-рожевий\",\n      \"orangeColor\": \"Помаранчевий\",\n      \"yellowColor\": \"Жовтий\",\n      \"limeColor\": \"Лаймовий\",\n      \"greenColor\": \"Зелений\",\n      \"aquaColor\": \"Аква\",\n      \"blueColor\": \"Синій\",\n      \"deleteTag\": \"Видалити тег\",\n      \"colorPanelTitle\": \"Кольори\",\n      \"panelTitle\": \"Виберіть опцію або створіть одну\",\n      \"searchOption\": \"Шукати опцію\",\n      \"searchOrCreateOption\": \"Шукати чи створити опцію...\",\n      \"createNew\": \"Створити нову\",\n      \"orSelectOne\": \"Або виберіть опцію\",\n      \"typeANewOption\": \"Введіть новий параметр\",\n      \"tagName\": \"Назва тегу\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"Опис завдання\",\n      \"addNew\": \"Додати нове завдання\",\n      \"submitNewTask\": \"Створити\",\n      \"hideComplete\": \"Сховати виконані завдання\",\n      \"showComplete\": \"Показати всі завдання\"\n    },\n    \"url\": {\n      \"launch\": \"Відкрити посилання в браузері\",\n      \"copy\": \"Копіювати посилання в буфер обміну\",\n      \"textFieldHint\": \"Введіть URL\"\n    },\n    \"relation\": {\n      \"relatedDatabasePlaceLabel\": \"Пов'язана база даних\",\n      \"relatedDatabasePlaceholder\": \"Жодного\",\n      \"inRelatedDatabase\": \"В\",\n      \"rowSearchTextFieldPlaceholder\": \"Пошук\",\n      \"noDatabaseSelected\": \"Базу даних не вибрано, будь ласка, спочатку виберіть одну зі списку нижче:\",\n      \"emptySearchResult\": \"Записів не знайдено\",\n      \"linkedRowListLabel\": \"{count} пов’язаних рядків\",\n      \"unlinkedRowListLabel\": \"Зв'яжіть інший ряд\"\n    },\n    \"menuName\": \"Сітка\",\n    \"referencedGridPrefix\": \"Вигляд\",\n    \"calculate\": \"Обчислити\",\n    \"calculationTypeLabel\": {\n      \"none\": \"Жодного\",\n      \"average\": \"Середній\",\n      \"max\": \"Макс\",\n      \"median\": \"Медіана\",\n      \"min\": \"Мін\",\n      \"sum\": \"Сума\",\n      \"count\": \"Рахувати\",\n      \"countEmpty\": \"Рахунок порожній\",\n      \"countEmptyShort\": \"ПУСТИЙ\",\n      \"countNonEmpty\": \"Граф не пустий\",\n      \"countNonEmptyShort\": \"ЗАПОВНЕНО\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"Документ\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 PM\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Виберіть дошку для посилання\",\n        \"createANewBoard\": \"Створити нову дошку\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Виберіть сітку для посилання\",\n        \"createANewGrid\": \"Створити нову сітку\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Виберіть календар для посилання\",\n        \"createANewCalendar\": \"Створити новий календар\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"Виберіть документ для посилання\"\n      },\n      \"name\": {\n        \"text\": \"Текст\",\n        \"heading1\": \"Заголовок 1\",\n        \"heading2\": \"Заголовок 2\",\n        \"heading3\": \"Заголовок 3\",\n        \"image\": \"Зображення\",\n        \"bulletedList\": \"Маркірований список\",\n        \"numberedList\": \"Нумерований список\",\n        \"todoList\": \"Список справ\",\n        \"doc\": \"Док\",\n        \"linkedDoc\": \"Посилання на сторінку\",\n        \"grid\": \"Сітка\",\n        \"linkedGrid\": \"Зв'язана сітка\",\n        \"kanban\": \"Канбан\",\n        \"linkedKanban\": \"Пов’язаний канбан\",\n        \"calendar\": \"Календар\",\n        \"linkedCalendar\": \"Пов’язаний календар\",\n        \"quote\": \"Цитата\",\n        \"divider\": \"Роздільник\",\n        \"table\": \"Таблиця\",\n        \"callout\": \"Виноска\",\n        \"outline\": \"Контур\",\n        \"mathEquation\": \"Математичне рівняння\",\n        \"code\": \"Код\",\n        \"toggleList\": \"Переключити список\",\n        \"emoji\": \"Emoji\",\n        \"aiWriter\": \"ШІ Письменник\",\n        \"dateOrReminder\": \"Дата або нагадування\",\n        \"photoGallery\": \"Фотогалерея\",\n        \"file\": \"Файл\",\n        \"checkbox\": \"Прапорець\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"Контур\",\n      \"codeBlock\": \"Блок коду\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Пов'язані дошки\",\n      \"referencedGrid\": \"Пов'язані сітки\",\n      \"referencedCalendar\": \"Календар посилань\",\n      \"referencedDocument\": \"Посилальний документ\",\n      \"autoGeneratorMenuItemName\": \"ШІ Письменник\",\n      \"autoGeneratorTitleName\": \"AI: Запитайте штучний інтелект написати будь-що...\",\n      \"autoGeneratorLearnMore\": \"Дізнатися більше\",\n      \"autoGeneratorGenerate\": \"Генерувати\",\n      \"autoGeneratorHintText\": \"Запитайте AI...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Не вдається отримати ключ AI\",\n      \"autoGeneratorRewrite\": \"Переписати\",\n      \"smartEdit\": \"Запитай ШІ\",\n      \"aI\": \"ШІ\",\n      \"smartEditFixSpelling\": \"Виправити правопис\",\n      \"warning\": \"⚠️ Відповіді ШІ можуть бути неточними або вводити в оману.\",\n      \"smartEditSummarize\": \"Підсумувати\",\n      \"smartEditImproveWriting\": \"Покращити написання\",\n      \"smartEditMakeLonger\": \"Зробити довше\",\n      \"smartEditCouldNotFetchResult\": \"Не вдалося отримати результат від ШІ\",\n      \"smartEditCouldNotFetchKey\": \"Не вдалося отримати ключ ШІ\",\n      \"smartEditDisabled\": \"Підключіть ШІ в Налаштуваннях\",\n      \"appflowyAIEditDisabled\": \"Увійдіть, щоб увімкнути функції ШІ\",\n      \"discardResponse\": \"Ви хочете відкинути відповіді AI?\",\n      \"createInlineMathEquation\": \"Створити рівняння\",\n      \"fonts\": \"Шрифти\",\n      \"insertDate\": \"Вставте дату\",\n      \"emoji\": \"Emoji\",\n      \"toggleList\": \"Перемкнути список\",\n      \"quoteList\": \"Цитатний список\",\n      \"numberedList\": \"Нумерований список\",\n      \"bulletedList\": \"Маркірований список\",\n      \"todoList\": \"Список справ\",\n      \"callout\": \"Виноска\",\n      \"cover\": {\n        \"changeCover\": \"Змінити Обгортку\",\n        \"colors\": \"Кольори\",\n        \"images\": \"Зображення\",\n        \"clearAll\": \"Очистити все\",\n        \"abstract\": \"Абстракція\",\n        \"addCover\": \"Додати Обгортку\",\n        \"addLocalImage\": \"Додати локальне зображення\",\n        \"invalidImageUrl\": \"Невірна URL адреса зображення\",\n        \"failedToAddImageToGallery\": \"Не вдалося додати зображення до галереї\",\n        \"enterImageUrl\": \"Введіть URL адресу зображення\",\n        \"add\": \"Додати\",\n        \"back\": \"Назад\",\n        \"saveToGallery\": \"Зберегти в галерею\",\n        \"removeIcon\": \"Видалити іконку\",\n        \"pasteImageUrl\": \"Вставити URL адресу зображення\",\n        \"or\": \"АБО\",\n        \"pickFromFiles\": \"Вибрати з власних файлів\",\n        \"couldNotFetchImage\": \"Не вдалося отримати зображення\",\n        \"imageSavingFailed\": \"Не вдалося зберегти зображення\",\n        \"addIcon\": \"Додати іконку\",\n        \"changeIcon\": \"Змінити значок\",\n        \"coverRemoveAlert\": \"Це буде видалено з обгортки після видалення.\",\n        \"alertDialogConfirmation\": \"Ви впевнені, що хочете продовжити?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"Математичне рівняння\",\n        \"addMathEquation\": \"Додати математичне рівняння\",\n        \"editMathEquation\": \"Редагувати математичне рівняння\"\n      },\n      \"optionAction\": {\n        \"click\": \"Клацніть\",\n        \"toOpenMenu\": \" для відкриття меню\",\n        \"delete\": \"Видалити\",\n        \"duplicate\": \"Дублювати\",\n        \"turnInto\": \"Перетворити у\",\n        \"moveUp\": \"Перемістити вгору\",\n        \"moveDown\": \"Перемістити вниз\",\n        \"color\": \"Колір\",\n        \"align\": \"Вирівнювання\",\n        \"left\": \"Ліворуч\",\n        \"center\": \"По центру\",\n        \"right\": \"По праву\",\n        \"defaultColor\": \"За замовчуванням\",\n        \"depth\": \"Глибина\"\n      },\n      \"image\": {\n        \"addAnImage\": \"Додати зображення\",\n        \"copiedToPasteBoard\": \"Посилання на зображення скопійовано в буфер обміну\",\n        \"addAnImageDesktop\": \"Перетягніть зображення або натисніть, щоб додати зображення\",\n        \"addAnImageMobile\": \"Натисніть, щоб додати одне або кілька зображень\",\n        \"dropImageToInsert\": \"Перетягніть зображення, щоб вставити\",\n        \"imageUploadFailed\": \"Помилка завантаження зображення\",\n        \"imageDownloadFailed\": \"Помилка завантаження зображення, повторіть спробу\",\n        \"imageDownloadFailedToken\": \"Помилка завантаження зображення через відсутність маркера користувача. Повторіть спробу\",\n        \"errorCode\": \"Код помилки\"\n      },\n      \"photoGallery\": {\n        \"name\": \"Фотогалерея\",\n        \"imageKeyword\": \"зображення\",\n        \"imageGalleryKeyword\": \"галерея зображень\",\n        \"photoKeyword\": \"фото\",\n        \"photoBrowserKeyword\": \"браузер фотографій\",\n        \"galleryKeyword\": \"галерея\",\n        \"addImageTooltip\": \"Додайте зображення\",\n        \"changeLayoutTooltip\": \"Змінити макет\",\n        \"browserLayout\": \"Браузер\",\n        \"gridLayout\": \"Сітка\",\n        \"deleteBlockTooltip\": \"Видалити всю галерею\"\n      },\n      \"math\": {\n        \"copiedToPasteBoard\": \"Математичне рівняння скопійовано в буфер обміну\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"Посилання скопійовано в буфер обміну\",\n        \"convertToLink\": \"Перетворити на вбудоване посилання\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Додайте заголовки, щоб створити зміст.\",\n        \"noMatchHeadings\": \"Відповідних заголовків не знайдено.\"\n      },\n      \"table\": {\n        \"addAfter\": \"Додати після\",\n        \"addBefore\": \"Додати перед\",\n        \"delete\": \"Видалити\",\n        \"clear\": \"Очистити вміст\",\n        \"duplicate\": \"Дублювати\",\n        \"bgColor\": \"Колір фону\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Копіювати\",\n        \"cut\": \"Вирізати\",\n        \"paste\": \"Вставити\"\n      },\n      \"action\": \"Дії\",\n      \"database\": {\n        \"selectDataSource\": \"Виберіть джерело даних\",\n        \"noDataSource\": \"Немає джерела даних\",\n        \"selectADataSource\": \"Виберіть джерело даних\",\n        \"toContinue\": \"продовжувати\",\n        \"newDatabase\": \"Нова база даних\",\n        \"linkToDatabase\": \"Посилання на базу даних\"\n      },\n      \"date\": \"Дата\",\n      \"video\": {\n        \"label\": \"Відео\",\n        \"emptyLabel\": \"Додайте відео\",\n        \"placeholder\": \"Вставте посилання на відео\",\n        \"copiedToPasteBoard\": \"Посилання на відео скопійовано в буфер обміну\",\n        \"insertVideo\": \"Додайте відео\",\n        \"invalidVideoUrl\": \"Вихідна URL-адреса ще не підтримується.\",\n        \"invalidVideoUrlYouTube\": \"YouTube ще не підтримується.\",\n        \"supportedFormats\": \"Підтримувані формати: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264\"\n      },\n      \"file\": {\n        \"name\": \"Файл\",\n        \"uploadTab\": \"Завантажити\",\n        \"networkTab\": \"Інтегрувати посилання\",\n        \"placeholderText\": \"Натисніть або перетягніть, щоб завантажити файл\",\n        \"placeholderDragging\": \"Перетягніть файл, щоб завантажити\",\n        \"dropFileToUpload\": \"Перетягніть файл, щоб завантажити\",\n        \"fileUploadHint\": \"Перетягніть файл сюди\\nабо натисніть, щоб вибрати файл.\",\n        \"networkHint\": \"Введіть посилання на файл\",\n        \"networkUrlInvalid\": \"Недійсна URL-адреса, виправте URL-адресу та повторіть спробу\",\n        \"networkAction\": \"Вставити посилання на файл\",\n        \"fileTooBigError\": \"Розмір файлу завеликий, будь ласка, завантажте файл розміром менше 10 МБ\",\n        \"renameFile\": {\n          \"title\": \"Перейменувати файл\",\n          \"description\": \"Введіть нову назву для цього файлу\",\n          \"nameEmptyError\": \"Ім'я файлу не може бути пустим.\"\n        },\n        \"uploadedAt\": \"Завантажено {}\",\n        \"linkedAt\": \"Посилання додано {}\"\n      }\n    },\n    \"outlineBlock\": {\n      \"placeholder\": \"Зміст\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Тип '/' для команд\"\n    },\n    \"title\": {\n      \"placeholder\": \"Без назви\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Клацніть, щоб додати зображення\",\n      \"upload\": {\n        \"label\": \"Завантажити\",\n        \"placeholder\": \"Клацніть, щоб завантажити зображення\"\n      },\n      \"url\": {\n        \"label\": \"URL зображення\",\n        \"placeholder\": \"Введіть URL зображення\"\n      },\n      \"ai\": {\n        \"label\": \"Створення зображення за допомогою AI\",\n        \"placeholder\": \"Будь ласка, введіть підказку для AI для створення зображення\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Створюйте зображення за допомогою Stability AI\",\n        \"placeholder\": \"Будь ласка, введіть підказку для ШІ стабільності, щоб створити зображення\"\n      },\n      \"support\": \"Розмір зображення обмежений 5 МБ. Підтримувані формати: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Невірне зображення\",\n        \"invalidImageSize\": \"Розмір зображення повинен бути менше 5 МБ\",\n        \"invalidImageFormat\": \"Формат зображення не підтримується. Підтримувані формати: JPEG, PNG, GIF, SVG\",\n        \"invalidImageUrl\": \"Невірний URL зображення\",\n        \"noImage\": \"Такого файлу чи каталогу немає\",\n        \"multipleImagesFailed\": \"Не вдалося завантажити одне чи кілька зображень. Повторіть спробу\"\n      },\n      \"embedLink\": {\n        \"label\": \"Вставити посилання\",\n        \"placeholder\": \"Вставте або введіть посилання на зображення\"\n      },\n      \"unsplash\": {\n        \"label\": \"Unsplash\"\n      },\n      \"searchForAnImage\": \"Пошук зображення\",\n      \"pleaseInputYourOpenAIKey\": \"будь ласка, введіть ключ ШІ на сторінці налаштувань\",\n      \"saveImageToGallery\": \"Зберегти зображення\",\n      \"failedToAddImageToGallery\": \"Не вдалося додати зображення до галереї\",\n      \"successToAddImageToGallery\": \"Зображення додано до галереї\",\n      \"unableToLoadImage\": \"Не вдалося завантажити зображення\",\n      \"maximumImageSize\": \"Максимальний підтримуваний розмір зображення для завантаження становить 10 МБ\",\n      \"uploadImageErrorImageSizeTooBig\": \"Розмір зображення має бути менше 10 Мб\",\n      \"imageIsUploading\": \"Зображення завантажується\",\n      \"openFullScreen\": \"Відкрити на весь екран\",\n      \"interactiveViewer\": {\n        \"toolbar\": {\n          \"previousImageTooltip\": \"Попереднє зображення\",\n          \"nextImageTooltip\": \"Наступне зображення\",\n          \"zoomOutTooltip\": \"Зменшення\",\n          \"zoomInTooltip\": \"Збільшувати\",\n          \"changeZoomLevelTooltip\": \"Змінити рівень масштабування\",\n          \"openLocalImage\": \"Відкрите зображення\",\n          \"downloadImage\": \"Завантажити зображення\",\n          \"closeViewer\": \"Закрити інтерактивний засіб перегляду\",\n          \"scalePercentage\": \"{}%\",\n          \"deleteImageTooltip\": \"Видалити зображення\"\n        }\n      },\n      \"pleaseInputYourStabilityAIKey\": \"будь ласка, введіть ключ стабільності AI на сторінці налаштувань\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Мова\",\n        \"placeholder\": \"Виберіть мову\",\n        \"auto\": \"Авто\"\n      },\n      \"copyTooltip\": \"Скопіюйте вміст блоку коду\",\n      \"searchLanguageHint\": \"Пошук мови\",\n      \"codeCopiedSnackbar\": \"Код скопійовано в буфер обміну!\"\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Вставте або введіть посилання\",\n      \"openInNewTab\": \"Відкрити в новій вкладці\",\n      \"copyLink\": \"Скопіювати посилання\",\n      \"removeLink\": \"Видалити посилання\",\n      \"url\": {\n        \"label\": \"URL посилання\",\n        \"placeholder\": \"Введіть URL посилання\"\n      },\n      \"title\": {\n        \"label\": \"Заголовок посилання\",\n        \"placeholder\": \"Введіть заголовок посилання\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"Згадати особу, сторінку або дату...\",\n      \"page\": {\n        \"label\": \"Посилання на сторінку\",\n        \"tooltip\": \"Клацніть, щоб відкрити сторінку\"\n      },\n      \"deleted\": \"Видалено\",\n      \"deletedContent\": \"Цей вміст не існує або його видалено\",\n      \"noAccess\": \"Немає доступу\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"Скинути до стандартного\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"Не вдалося проаналізувати вміст блоку\",\n      \"clickToCopyTheBlockContent\": \"Натисніть, щоб скопіювати вміст блоку\",\n      \"blockContentHasBeenCopied\": \"Вміст блоку скопійовано.\"\n    },\n    \"mobilePageSelector\": {\n      \"title\": \"Виберіть сторінку\",\n      \"failedToLoad\": \"Не вдалося завантажити список сторінок\",\n      \"noPagesFound\": \"Сторінок не знайдено\"\n    },\n    \"board\": {\n      \"column\": {\n        \"createNewCard\": \"Нова\"\n      },\n      \"menuName\": \"Дошка\",\n      \"referencedBoardPrefix\": \"Вид на\"\n    },\n    \"calendar\": {\n      \"menuName\": \"Календар\",\n      \"defaultNewCalendarTitle\": \"Без назви\",\n      \"newEventButtonTooltip\": \"Додати нову подію\",\n      \"navigation\": {\n        \"today\": \"Сьогодні\",\n        \"jumpToday\": \"Перейти до сьогодні\",\n        \"previousMonth\": \"Попередній місяць\",\n        \"nextMonth\": \"Наступний місяць\"\n      },\n      \"settings\": {\n        \"showWeekNumbers\": \"Показувати номери тижня\",\n        \"showWeekends\": \"Показувати вихідні\",\n        \"firstDayOfWeek\": \"Почати тиждень з\",\n        \"layoutDateField\": \"Календарний вигляд за\",\n        \"noDateTitle\": \"Без дати\",\n        \"noDateHint\": \"Несплановані події будуть відображатися тут\",\n        \"clickToAdd\": \"Клацніть, щоб додати до календаря\",\n        \"name\": \"Календарний вигляд\"\n      },\n      \"referencedCalendarPrefix\": \"Вид на\"\n    },\n    \"errorDialog\": {\n      \"title\": \"Помилка в @:appName\",\n      \"howToFixFallback\": \"Вибачте за незручності! Надішліть звіт про помилку на нашу сторінку GitHub, де ви опишіть свою помилку.\",\n      \"github\": \"Переглянути на GitHub\"\n    },\n    \"search\": {\n      \"label\": \"Пошук\",\n      \"placeholder\": {\n        \"actions\": \"Шукати дії...\"\n      }\n    },\n    \"message\": {\n      \"copy\": {\n        \"success\": \"Скопійовано!\",\n        \"fail\": \"Не вдалося скопіювати\"\n      }\n    },\n    \"unSupportBlock\": \"Поточна версія не підтримує цей блок.\",\n    \"views\": {\n      \"deleteContentTitle\": \"Ви впевнені, що хочете видалити {pageType}?\",\n      \"deleteContentCaption\": \"якщо ви видалите цю {pageType}, ви зможете відновити її з кошика.\"\n    },\n    \"colors\": {\n      \"custom\": \"Власний\",\n      \"default\": \"Стандартний\",\n      \"red\": \"Червоний\",\n      \"orange\": \"Помаранчевий\",\n      \"yellow\": \"Жовтий\",\n      \"green\": \"Зелений\",\n      \"blue\": \"Синій\",\n      \"purple\": \"Фіолетовий\",\n      \"pink\": \"Рожевий\",\n      \"brown\": \"Коричневий\",\n      \"gray\": \"Сірий\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"Новий\",\n      \"renameGroupTooltip\": \"Натисніть, щоб перейменувати групу\",\n      \"createNewColumn\": \"Додайте нову групу\",\n      \"addToColumnTopTooltip\": \"Додайте нову картку вгорі\",\n      \"addToColumnBottomTooltip\": \"Додайте нову картку внизу\",\n      \"renameColumn\": \"Перейменувати\",\n      \"hideColumn\": \"Сховати\",\n      \"newGroup\": \"Нова група\",\n      \"deleteColumn\": \"Видалити\",\n      \"deleteColumnConfirmation\": \"Це призведе до видалення цієї групи та всіх карток у ній.\\nВи впевнені, що бажаєте продовжити?\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"Приховані групи\",\n      \"collapseTooltip\": \"Приховати приховані групи\",\n      \"expandTooltip\": \"Переглянути приховані групи\"\n    },\n    \"cardDetail\": \"Деталі картки\",\n    \"cardActions\": \"Дії з картками\",\n    \"cardDuplicated\": \"Картку дублювали\",\n    \"cardDeleted\": \"Картку видалено\",\n    \"showOnCard\": \"Показати на реквізитах картки\",\n    \"setting\": \"Налаштування\",\n    \"propertyName\": \"Назва власності\",\n    \"menuName\": \"Дошка\",\n    \"showUngrouped\": \"Показати не згруповані елементи\",\n    \"ungroupedButtonText\": \"Розгрупований\",\n    \"ungroupedButtonTooltip\": \"Містить картки, які не належать до жодної групи\",\n    \"ungroupedItemsTitle\": \"Натисніть, щоб додати до дошки\",\n    \"groupBy\": \"Групувати за\",\n    \"groupCondition\": \"Стан групи\",\n    \"referencedBoardPrefix\": \"Вид на\",\n    \"notesTooltip\": \"Нотатки всередині\",\n    \"mobile\": {\n      \"editURL\": \"Редагувати URL\",\n      \"showGroup\": \"Показати групу\",\n      \"showGroupContent\": \"Ви впевнені, що хочете показати цю групу на дошці?\",\n      \"failedToLoad\": \"Не вдалося завантажити вигляд дошки\"\n    },\n    \"dateCondition\": {\n      \"weekOf\": \"Тиждень з {} по {}\",\n      \"today\": \"Сьогодні\",\n      \"yesterday\": \"вчора\",\n      \"tomorrow\": \"Завтра\",\n      \"lastSevenDays\": \"Останні 7 днів\",\n      \"nextSevenDays\": \"Наступні 7 днів\",\n      \"lastThirtyDays\": \"Останні 30 днів\",\n      \"nextThirtyDays\": \"Наступні 30 днів\"\n    },\n    \"noGroup\": \"Немає груп за властивостями\",\n    \"noGroupDesc\": \"Для відображення подання дошки потрібна властивість для групування\"\n  },\n  \"calendar\": {\n    \"menuName\": \"Календар\",\n    \"defaultNewCalendarTitle\": \"Без назви\",\n    \"newEventButtonTooltip\": \"Додайте нову подію\",\n    \"navigation\": {\n      \"today\": \"Сьогодні\",\n      \"jumpToday\": \"Перейти до сьогоднішнього дня\",\n      \"previousMonth\": \"Попередній місяць\",\n      \"nextMonth\": \"Наступного місяця\",\n      \"views\": {\n        \"day\": \"День\",\n        \"week\": \"Тиждень\",\n        \"month\": \"Місяць\",\n        \"year\": \"Рік\"\n      }\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"Подій ще немає\",\n      \"emptyBody\": \"Натисніть кнопку плюс, щоб створити подію в цей день.\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Показати номери тижнів\",\n      \"showWeekends\": \"Показати вихідні\",\n      \"firstDayOfWeek\": \"Почніть тиждень\",\n      \"layoutDateField\": \"Верстка календаря по\",\n      \"changeLayoutDateField\": \"Змінити поле макета\",\n      \"noDateTitle\": \"Без дати\",\n      \"noDateHint\": {\n        \"zero\": \"Тут відображатимуться незаплановані події\",\n        \"one\": \"{count} незапланована подія\",\n        \"other\": \"{count} незапланованих подій\"\n      },\n      \"unscheduledEventsTitle\": \"Позапланові заходи\",\n      \"clickToAdd\": \"Натисніть, щоб додати до календаря\",\n      \"name\": \"Налаштування календаря\",\n      \"clickToOpen\": \"Натисніть, щоб відкрити запис\"\n    },\n    \"referencedCalendarPrefix\": \"Вид на\",\n    \"quickJumpYear\": \"Перейти до\",\n    \"duplicateEvent\": \"Дубльована подія\"\n  },\n  \"errorDialog\": {\n    \"title\": \"@:appName Помилка\",\n    \"howToFixFallback\": \"Просимо вибачення за незручності! Надішліть проблему на нашій сторінці GitHub з описом вашої помилки.\",\n    \"howToFixFallbackHint1\": \"Просимо вибачення за незручності! Надішліть питання на нашому \",\n    \"howToFixFallbackHint2\": \" сторінка, яка описує вашу помилку.\",\n    \"github\": \"Переглянути на GitHub\"\n  },\n  \"search\": {\n    \"label\": \"Пошук\",\n    \"sidebarSearchIcon\": \"Шукайте та швидко переходьте на сторінку\",\n    \"placeholder\": {\n      \"actions\": \"Пошукові дії...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Скопійовано!\",\n      \"fail\": \"Неможливо скопіювати\"\n    }\n  },\n  \"unSupportBlock\": \"Поточна версія не підтримує цей блок.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Ви дійсно хочете видалити {pageType}?\",\n    \"deleteContentCaption\": \"якщо ви видалите цей {pageType}, ви зможете відновити його з кошика.\"\n  },\n  \"colors\": {\n    \"custom\": \"Користувальницькі\",\n    \"default\": \"За замовчуванням\",\n    \"red\": \"Червоний\",\n    \"orange\": \"Помаранчевий\",\n    \"yellow\": \"Жовтий\",\n    \"green\": \"Зелений\",\n    \"blue\": \"Синій\",\n    \"purple\": \"Фіолетовий\",\n    \"pink\": \"Рожевий\",\n    \"brown\": \"Коричневий\",\n    \"gray\": \"Сірий\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"Emoji\",\n    \"search\": \"Пошук емодзі\",\n    \"noRecent\": \"Немає недавніх емодзі\",\n    \"noEmojiFound\": \"Емодзі не знайдено\",\n    \"filter\": \"Фільтр\",\n    \"random\": \"Випадковий\",\n    \"selectSkinTone\": \"Вибрати відтінок шкіри\",\n    \"remove\": \"Видалити емодзі\",\n    \"categories\": {\n      \"smileys\": \"Смайли та емоції\",\n      \"people\": \"люди\",\n      \"animals\": \"природа\",\n      \"food\": \"їжа\",\n      \"activities\": \"активності\",\n      \"places\": \"місця\",\n      \"objects\": \"об'єкти\",\n      \"symbols\": \"символи\",\n      \"flags\": \"прапори\",\n      \"nature\": \"природа\",\n      \"frequentlyUsed\": \"часто використовувані\"\n    },\n    \"skinTone\": {\n      \"default\": \"За замовчуванням\",\n      \"light\": \"Світла\",\n      \"mediumLight\": \"Середньосвітлий\",\n      \"medium\": \"Середній\",\n      \"mediumDark\": \"Середньо-темний\",\n      \"dark\": \"Темний\"\n    },\n    \"openSourceIconsFrom\": \"Піктограми з відкритим кодом\"\n  },\n  \"inlineActions\": {\n    \"noResults\": \"Немає результатів\",\n    \"recentPages\": \"Останні сторінки\",\n    \"pageReference\": \"Посилання на сторінку\",\n    \"docReference\": \"Посилання на документ\",\n    \"boardReference\": \"Довідка дошки\",\n    \"calReference\": \"Довідка календаря\",\n    \"gridReference\": \"Посилання на сітку\",\n    \"date\": \"Дата\",\n    \"reminder\": {\n      \"groupTitle\": \"Нагадування\",\n      \"shortKeyword\": \"нагадати\"\n    }\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"Змініть формат дати та часу в налаштуваннях\",\n    \"dateFormat\": \"Формат дати\",\n    \"includeTime\": \"Включіть час\",\n    \"isRange\": \"Дата закінчення\",\n    \"timeFormat\": \"Формат часу\",\n    \"clearDate\": \"Ясна дата\",\n    \"reminderLabel\": \"Нагадування\",\n    \"selectReminder\": \"Виберіть нагадування\",\n    \"reminderOptions\": {\n      \"none\": \"Жодного\",\n      \"atTimeOfEvent\": \"Час події\",\n      \"fiveMinsBefore\": \"5 хвилин до\",\n      \"tenMinsBefore\": \"за 10 хвилин до\",\n      \"fifteenMinsBefore\": \"15 хвилин до\",\n      \"thirtyMinsBefore\": \"30 хвилин до\",\n      \"oneHourBefore\": \"за 1 годину до\",\n      \"twoHoursBefore\": \"за 2 години до\",\n      \"onDayOfEvent\": \"У день події\",\n      \"oneDayBefore\": \"за 1 день до\",\n      \"twoDaysBefore\": \"за 2 дні до\",\n      \"oneWeekBefore\": \"1 тиждень тому\",\n      \"custom\": \"Налаштовуване\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"Вчора\",\n    \"today\": \"Сьогодні\",\n    \"tomorrow\": \"Завтра\",\n    \"oneWeek\": \"1 тиждень\"\n  },\n  \"notificationHub\": {\n    \"title\": \"Сповіщення\",\n    \"mobile\": {\n      \"title\": \"Оновлення\"\n    },\n    \"emptyTitle\": \"Все наздогнали!\",\n    \"emptyBody\": \"Немає незавершених сповіщень або дій. Насолоджуйся спокоєм.\",\n    \"tabs\": {\n      \"inbox\": \"Вхідні\",\n      \"upcoming\": \"Майбутні\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"Позначити все як прочитане\",\n      \"showAll\": \"Все\",\n      \"showUnreads\": \"Непрочитаний\"\n    },\n    \"filters\": {\n      \"ascending\": \"Висхідний\",\n      \"descending\": \"Спускається\",\n      \"groupByDate\": \"Групувати за датою\",\n      \"showUnreadsOnly\": \"Показати лише непрочитані\",\n      \"resetToDefault\": \"Скинути до замовчування\"\n    },\n    \"empty\": \"Тут нічого немає!\"\n  },\n  \"reminderNotification\": {\n    \"title\": \"Нагадування\",\n    \"message\": \"Не забудьте перевірити це, перш ніж забути!\",\n    \"tooltipDelete\": \"Видалити\",\n    \"tooltipMarkRead\": \"Позначити як прочитане\",\n    \"tooltipMarkUnread\": \"Позначити як непрочитане\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"Знайти\",\n    \"previousMatch\": \"Попередній збіг\",\n    \"nextMatch\": \"Наступний збіг\",\n    \"close\": \"Закрити\",\n    \"replace\": \"Замінити\",\n    \"replaceAll\": \"Замінити всі\",\n    \"noResult\": \"Немає результатів\",\n    \"caseSensitive\": \"З урахуванням регістру\",\n    \"searchMore\": \"Шукайте, щоб знайти більше результатів\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"Нам шкода\",\n    \"loadingViewError\": \"У нас виникла проблема із завантаженням цього перегляду. Перевірте підключення до Інтернету, оновіть програму та не соромтеся зв’язатися з командою, якщо проблема не зникне.\",\n    \"syncError\": \"Дані не синхронізуються з іншого пристрою\",\n    \"syncErrorHint\": \"Будь ласка, повторно відкрийте цю сторінку на пристрої, де її востаннє редагували, а потім знову відкрийте її на поточному пристрої.\",\n    \"clickToCopy\": \"Натисніть, щоб скопіювати код помилки\"\n  },\n  \"editor\": {\n    \"bold\": \"Жирний\",\n    \"bulletedList\": \"Маркірований список\",\n    \"bulletedListShortForm\": \"Маркірований\",\n    \"checkbox\": \"Прапорець\",\n    \"embedCode\": \"Вставити код\",\n    \"heading1\": \"H1\",\n    \"heading2\": \"H2\",\n    \"heading3\": \"H3\",\n    \"highlight\": \"Виділіть\",\n    \"color\": \"Колір\",\n    \"image\": \"Зображення\",\n    \"date\": \"Дата\",\n    \"page\": \"Сторінка\",\n    \"italic\": \"Курсив\",\n    \"link\": \"Посилання\",\n    \"numberedList\": \"Нумерований список\",\n    \"numberedListShortForm\": \"Пронумерований\",\n    \"quote\": \"Цитата\",\n    \"strikethrough\": \"Закреслення\",\n    \"text\": \"Текст\",\n    \"underline\": \"Підкреслити\",\n    \"fontColorDefault\": \"За замовчуванням\",\n    \"fontColorGray\": \"Сірий\",\n    \"fontColorBrown\": \"Коричневий\",\n    \"fontColorOrange\": \"Помаранчевий\",\n    \"fontColorYellow\": \"Жовтий\",\n    \"fontColorGreen\": \"Зелений\",\n    \"fontColorBlue\": \"Синій\",\n    \"fontColorPurple\": \"Фіолетовий\",\n    \"fontColorPink\": \"Рожевий\",\n    \"fontColorRed\": \"Червоний\",\n    \"backgroundColorDefault\": \"Фон за умовчанням\",\n    \"backgroundColorGray\": \"Сірий фон\",\n    \"backgroundColorBrown\": \"Коричневий фон\",\n    \"backgroundColorOrange\": \"Помаранчевий фон\",\n    \"backgroundColorYellow\": \"Жовтий фон\",\n    \"backgroundColorGreen\": \"Зелений фон\",\n    \"backgroundColorBlue\": \"Блакитний фон\",\n    \"backgroundColorPurple\": \"Фіолетовий фон\",\n    \"backgroundColorPink\": \"Рожевий фон\",\n    \"backgroundColorRed\": \"Червоний фон\",\n    \"backgroundColorLime\": \"Вапно фону\",\n    \"backgroundColorAqua\": \"Аква фону\",\n    \"done\": \"Готово\",\n    \"cancel\": \"Скасувати\",\n    \"tint1\": \"Відтінок 1\",\n    \"tint2\": \"Відтінок 2\",\n    \"tint3\": \"Відтінок 3\",\n    \"tint4\": \"Відтінок 4\",\n    \"tint5\": \"Відтінок 5\",\n    \"tint6\": \"Відтінок 6\",\n    \"tint7\": \"Відтінок 7\",\n    \"tint8\": \"Відтінок 8\",\n    \"tint9\": \"Відтінок 9\",\n    \"lightLightTint1\": \"Фіолетовий\",\n    \"lightLightTint2\": \"Рожевий\",\n    \"lightLightTint3\": \"Світло-рожевий\",\n    \"lightLightTint4\": \"Помаранчевий\",\n    \"lightLightTint5\": \"Жовтий\",\n    \"lightLightTint6\": \"Вапно\",\n    \"lightLightTint7\": \"Зелений\",\n    \"lightLightTint8\": \"Аква\",\n    \"lightLightTint9\": \"Синій\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"Заголовок 1\",\n    \"mobileHeading2\": \"Заголовок 2\",\n    \"mobileHeading3\": \"Заголовок 3\",\n    \"textColor\": \"Колір тексту\",\n    \"backgroundColor\": \"Колір фону\",\n    \"addYourLink\": \"Додайте своє посилання\",\n    \"openLink\": \"Відкрити посилання\",\n    \"copyLink\": \"Копіювати посилання\",\n    \"removeLink\": \"Видалити посилання\",\n    \"editLink\": \"Редагувати посилання\",\n    \"linkText\": \"Текст\",\n    \"linkTextHint\": \"Будь ласка, введіть текст\",\n    \"linkAddressHint\": \"Будь ласка, введіть URL\",\n    \"highlightColor\": \"Колір виділення\",\n    \"clearHighlightColor\": \"Чіткий колір виділення\",\n    \"customColor\": \"Індивідуальний колір\",\n    \"hexValue\": \"Шістнадцяткове значення\",\n    \"opacity\": \"Непрозорість\",\n    \"resetToDefaultColor\": \"Відновити колір за замовчуванням\",\n    \"ltr\": \"LTR\",\n    \"rtl\": \"RTL\",\n    \"auto\": \"Авто\",\n    \"cut\": \"Вирізати\",\n    \"copy\": \"Копіювати\",\n    \"paste\": \"Вставити\",\n    \"find\": \"Знайти\",\n    \"select\": \"Виберіть\",\n    \"selectAll\": \"Вибрати все\",\n    \"previousMatch\": \"Попередній матч\",\n    \"nextMatch\": \"Наступний матч\",\n    \"closeFind\": \"Закрити\",\n    \"replace\": \"Замінити\",\n    \"replaceAll\": \"Замінити все\",\n    \"regex\": \"Регулярний вираз\",\n    \"caseSensitive\": \"Чутливий до регістру\",\n    \"uploadImage\": \"Завантажити зображення\",\n    \"urlImage\": \"URL зображення\",\n    \"incorrectLink\": \"Невірне посилання\",\n    \"upload\": \"Завантажити\",\n    \"chooseImage\": \"Виберіть зображення\",\n    \"loading\": \"Завантаження\",\n    \"imageLoadFailed\": \"Не вдалося завантажити зображення\",\n    \"divider\": \"Роздільник\",\n    \"table\": \"Таблиця\",\n    \"colAddBefore\": \"Додати перед\",\n    \"rowAddBefore\": \"Додати перед\",\n    \"colAddAfter\": \"Додайте після\",\n    \"rowAddAfter\": \"Додайте після\",\n    \"colRemove\": \"Видалити\",\n    \"rowRemove\": \"Видалити\",\n    \"colDuplicate\": \"Дублювати\",\n    \"rowDuplicate\": \"Дублювати\",\n    \"colClear\": \"Очистити вміст\",\n    \"rowClear\": \"Очистити вміст\",\n    \"slashPlaceHolder\": \"Введіть '/', щоб вставити блок, або почніть вводити\",\n    \"typeSomething\": \"Введіть щось...\",\n    \"toggleListShortForm\": \"Перемикач\",\n    \"quoteListShortForm\": \"Цитата\",\n    \"mathEquationShortForm\": \"Формула\",\n    \"codeBlockShortForm\": \"Код\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"Немає улюбленої сторінки\",\n    \"noFavoriteHintText\": \"Проведіть пальцем по сторінці вліво, щоб додати її до вибраного\",\n    \"removeFromSidebar\": \"Видалити з бічної панелі\",\n    \"addToSidebar\": \"Закріпити на бічній панелі\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"Введіть /, щоб вставити блок, або почніть вводити текст\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"Зробити\",\n    \"bulletList\": \"Список\",\n    \"numberList\": \"Список\",\n    \"quote\": \"Цитата\",\n    \"heading\": \"Заголовок {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"Значок сторінки\",\n    \"language\": \"Мова\",\n    \"font\": \"Шрифт\",\n    \"actions\": \"Дії\",\n    \"date\": \"Дата\",\n    \"addField\": \"Додати поле\",\n    \"userIcon\": \"Значок користувача\"\n  },\n  \"noLogFiles\": \"Немає файлів журналу\",\n  \"newSettings\": {\n    \"myAccount\": {\n      \"title\": \"Мій рахунок\",\n      \"subtitle\": \"Налаштуйте свій профіль, керуйте безпекою облікового запису, відкривайте ключі AI або ввійдіть у свій обліковий запис.\",\n      \"profileLabel\": \"Назва облікового запису та зображення профілю\",\n      \"profileNamePlaceholder\": \"Введіть ім'я\",\n      \"accountSecurity\": \"Безпека облікового запису\",\n      \"2FA\": \"2-етапна автентифікація\",\n      \"aiKeys\": \"AI ключі\",\n      \"accountLogin\": \"Вхід до облікового запису\",\n      \"updateNameError\": \"Не вдалося оновити назву\",\n      \"updateIconError\": \"Не вдалося оновити значок\",\n      \"deleteAccount\": {\n        \"title\": \"Видалити аккаунт\",\n        \"subtitle\": \"Назавжди видалити свій обліковий запис і всі ваші дані.\",\n        \"deleteMyAccount\": \"Видалити мій обліковий запис\",\n        \"dialogTitle\": \"Видалити аккаунт\",\n        \"dialogContent1\": \"Ви впевнені, що хочете остаточно видалити свій обліковий запис?\",\n        \"dialogContent2\": \"Цю дію неможливо скасувати. Вона призведе до скасування доступу до всіх командних просторів, видалення всього вашого облікового запису, включаючи приватні робочі простори, і видалення вас із усіх спільних робочих просторів.\"\n      }\n    },\n    \"workplace\": {\n      \"name\": \"Робоче місце\",\n      \"title\": \"Налаштування робочого місця\",\n      \"subtitle\": \"Налаштуйте зовнішній вигляд робочої області, тему, шрифт, макет тексту, дату, час і мову.\",\n      \"workplaceName\": \"Назва робочого місця\",\n      \"workplaceNamePlaceholder\": \"Введіть назву робочого місця\",\n      \"workplaceIcon\": \"Значок на робочому місці\",\n      \"workplaceIconSubtitle\": \"Завантажте зображення або використовуйте емодзі для свого робочого простору. Значок з’явиться на бічній панелі та в сповіщеннях.\",\n      \"renameError\": \"Не вдалося перейменувати робоче місце\",\n      \"updateIconError\": \"Не вдалося оновити значок\",\n      \"chooseAnIcon\": \"Виберіть значок\",\n      \"appearance\": {\n        \"name\": \"Зовнішній вигляд\",\n        \"themeMode\": {\n          \"auto\": \"Авто\",\n          \"light\": \"Світла\",\n          \"dark\": \"Темна\"\n        },\n        \"language\": \"Мова\"\n      }\n    },\n    \"syncState\": {\n      \"syncing\": \"Синхронізація\",\n      \"synced\": \"Синхронізовано\",\n      \"noNetworkConnected\": \"Немає підключення до мережі\"\n    }\n  },\n  \"pageStyle\": {\n    \"title\": \"Стиль сторінки\",\n    \"layout\": \"Макет\",\n    \"coverImage\": \"Зображення обкладинки\",\n    \"pageIcon\": \"Значок сторінки\",\n    \"colors\": \"Кольори\",\n    \"gradient\": \"Градієнт\",\n    \"backgroundImage\": \"Фонове зображення\",\n    \"presets\": \"Предустановки\",\n    \"photo\": \"Фото\",\n    \"unsplash\": \"Unsplash\",\n    \"pageCover\": \"Обкладинка сторінки\",\n    \"none\": \"Жодного\",\n    \"openSettings\": \"Відкрийте налаштування\",\n    \"photoPermissionTitle\": \"@:appName хоче отримати доступ до вашої бібліотеки фотографій\",\n    \"photoPermissionDescription\": \"Дозвольте доступ до бібліотеки фотографій для завантаження зображень.\",\n    \"doNotAllow\": \"Не дозволяти\",\n    \"image\": \"Зображення\"\n  },\n  \"commandPalette\": {\n    \"placeholder\": \"Введіть для пошуку...\",\n    \"bestMatches\": \"Найкращі збіги\",\n    \"recentHistory\": \"Новітня історія\",\n    \"navigateHint\": \"для навігації\",\n    \"loadingTooltip\": \"Чекаємо результатів...\",\n    \"betaLabel\": \"БЕТА\",\n    \"betaTooltip\": \"Зараз ми підтримуємо лише пошук сторінок і вмісту в документах\",\n    \"fromTrashHint\": \"Зі сміття\",\n    \"noResultsHint\": \"Ми не знайшли те, що ви шукали, спробуйте знайти інший термін.\",\n    \"clearSearchTooltip\": \"Очистити поле пошуку\"\n  },\n  \"space\": {\n    \"delete\": \"Видалити\",\n    \"deleteConfirmation\": \"Видалити: \",\n    \"deleteConfirmationDescription\": \"Усі сторінки в цьому просторі буде видалено та переміщено до кошика, а опубліковані сторінки буде скасовано.\",\n    \"rename\": \"Перейменувати простір\",\n    \"changeIcon\": \"Змінити значок\",\n    \"manage\": \"Керуйте простором\",\n    \"addNewSpace\": \"Створити простір\",\n    \"collapseAllSubPages\": \"Згорнути всі підсторінки\",\n    \"createNewSpace\": \"Створіть новий простір\",\n    \"createSpaceDescription\": \"Створіть кілька публічних і приватних просторів, щоб краще організувати свою роботу.\",\n    \"spaceName\": \"Назва простору\",\n    \"spaceNamePlaceholder\": \"наприклад, маркетинг, інженерія, HR\",\n    \"permission\": \"Дозвіл\",\n    \"publicPermission\": \"Громадський\",\n    \"publicPermissionDescription\": \"Усі учасники робочої області з повним доступом\",\n    \"privatePermission\": \"Приватний\",\n    \"privatePermissionDescription\": \"Тільки ви маєте доступ до цього простору\",\n    \"spaceIconBackground\": \"Колір фону\",\n    \"spaceIcon\": \"Значок\",\n    \"dangerZone\": \"НЕБЕЗПЕЧНА ЗОНА\",\n    \"unableToDeleteLastSpace\": \"Неможливо видалити останній пробіл\",\n    \"unableToDeleteSpaceNotCreatedByYou\": \"Неможливо видалити простори, створені іншими\",\n    \"enableSpacesForYourWorkspace\": \"Увімкніть Spaces для своєї робочої області\",\n    \"title\": \"Пробіли\",\n    \"defaultSpaceName\": \"Загальний\",\n    \"upgradeSpaceTitle\": \"Увімкнути Spaces\",\n    \"upgradeSpaceDescription\": \"Створіть кілька публічних і приватних просторів, щоб краще організувати свій робочий простір.\",\n    \"upgrade\": \"Оновити\",\n    \"upgradeYourSpace\": \"Створіть кілька просторів\",\n    \"quicklySwitch\": \"Швидко перейдіть до наступного місця\",\n    \"duplicate\": \"Дубльований простір\",\n    \"movePageToSpace\": \"Перемістити сторінку в простір\",\n    \"switchSpace\": \"Переключити простір\",\n    \"spaceNameCannotBeEmpty\": \"Назва групи не може бути пустою\"\n  },\n  \"publish\": {\n    \"hasNotBeenPublished\": \"Ця сторінка ще не опублікована\",\n    \"reportPage\": \"Сторінка звіту\",\n    \"databaseHasNotBeenPublished\": \"Публікація бази даних ще не підтримується.\",\n    \"createdWith\": \"Створено за допомогою\",\n    \"downloadApp\": \"Завантажити AppFlowy\",\n    \"copy\": {\n      \"codeBlock\": \"Вміст блоку коду скопійовано в буфер обміну\",\n      \"imageBlock\": \"Посилання на зображення скопійовано в буфер обміну\",\n      \"mathBlock\": \"Математичне рівняння скопійовано в буфер обміну\",\n      \"fileBlock\": \"Посилання на файл скопійовано в буфер обміну\"\n    },\n    \"containsPublishedPage\": \"Ця сторінка містить одну або кілька опублікованих сторінок. Якщо ви продовжите, їх публікацію буде скасовано. Ви хочете продовжити видалення?\",\n    \"publishSuccessfully\": \"Успішно опубліковано\",\n    \"unpublishSuccessfully\": \"Публікацію скасовано\",\n    \"publishFailed\": \"Не вдалося опублікувати\",\n    \"unpublishFailed\": \"Не вдалося скасувати публікацію\",\n    \"noAccessToVisit\": \"Немає доступу до цієї сторінки...\",\n    \"createWithAppFlowy\": \"Створіть веб-сайт за допомогою AppFlowy\",\n    \"fastWithAI\": \"Швидко та легко з AI.\",\n    \"tryItNow\": \"Спробуй зараз\",\n    \"onlyGridViewCanBePublished\": \"Можна опублікувати лише режим сітки\",\n    \"database\": {\n      \"zero\": \"Опублікувати {} вибране представлення\",\n      \"one\": \"Опублікувати {} вибраних представлень\",\n      \"many\": \"Опублікувати {} вибраних представлень\",\n      \"other\": \"Опублікувати {} вибраних представлень\"\n    },\n    \"mustSelectPrimaryDatabase\": \"Необхідно вибрати основний вид\",\n    \"noDatabaseSelected\": \"Базу даних не вибрано, виберіть принаймні одну базу даних.\",\n    \"unableToDeselectPrimaryDatabase\": \"Неможливо скасувати вибір основної бази даних\",\n    \"saveThisPage\": \"Зберегти цю сторінку\",\n    \"duplicateTitle\": \"Куди б ви хотіли додати\",\n    \"selectWorkspace\": \"Виберіть робочу область\",\n    \"addTo\": \"Додати до\",\n    \"duplicateSuccessfully\": \"Дубльований успіх. Хочете переглянути документи?\",\n    \"duplicateSuccessfullyDescription\": \"Немає програми? Ваше завантаження розпочнеться автоматично після натискання кнопки «Завантажити».\",\n    \"downloadIt\": \"Завантажити\",\n    \"openApp\": \"Відкрити в додатку\",\n    \"duplicateFailed\": \"Не вдалося створити копію\",\n    \"membersCount\": {\n      \"zero\": \"Немає учасників\",\n      \"one\": \"1 учасник\",\n      \"many\": \"{count} учасників\",\n      \"other\": \"{count} учасників\"\n    }\n  },\n  \"web\": {\n    \"continue\": \"Продовжити\",\n    \"or\": \"або\",\n    \"continueWithGoogle\": \"Продовжуйте з Google\",\n    \"continueWithGithub\": \"Продовжити з GitHub\",\n    \"continueWithDiscord\": \"Продовжуйте з Discord\",\n    \"signInAgreement\": \"Натиснувши «Продовжити» вище, ви підтверджуєте це\\nви прочитали, зрозуміли та погодилися\\nAppFlowy\",\n    \"and\": \"і\",\n    \"termOfUse\": \"Умови\",\n    \"privacyPolicy\": \"Політика конфіденційності\",\n    \"signInError\": \"Помилка входу\",\n    \"login\": \"Зареєструйтесь або увійдіть\",\n    \"fileBlock\": {\n      \"uploadedAt\": \"Завантажено {time}\",\n      \"linkedAt\": \"Посилання додано {time}\",\n      \"empty\": \"Завантажте або вставте файл\"\n    }\n  },\n  \"globalComment\": {\n    \"comments\": \"Коментарі\",\n    \"addComment\": \"Додати коментар\",\n    \"reactedBy\": \"відреагував\",\n    \"addReaction\": \"Додайте реакцію\",\n    \"reactedByMore\": \"та {count} інших\",\n    \"showSeconds\": {\n      \"one\": \"1 секунду тому\",\n      \"other\": \"{count} секунд тому\",\n      \"zero\": \"Прямо зараз\",\n      \"many\": \"{count} секунд тому\"\n    },\n    \"showMinutes\": {\n      \"one\": \"1 хвилину тому\",\n      \"other\": \"{count} хвилин тому\",\n      \"many\": \"{count} хвилин тому\"\n    },\n    \"showHours\": {\n      \"one\": \"1 годину тому\",\n      \"other\": \"{count} годин тому\",\n      \"many\": \"{count} годин тому\"\n    },\n    \"showDays\": {\n      \"one\": \"1 день тому\",\n      \"other\": \"{count} днів тому\",\n      \"many\": \"{count} днів тому\"\n    },\n    \"showMonths\": {\n      \"one\": \"1 місяць тому\",\n      \"other\": \"{count} місяців тому\",\n      \"many\": \"{count} місяців тому\"\n    },\n    \"showYears\": {\n      \"one\": \"1 рік тому\",\n      \"other\": \"{count} років тому\",\n      \"many\": \"{count} років тому\"\n    },\n    \"reply\": \"Відповісти\",\n    \"deleteComment\": \"Видалити коментар\",\n    \"youAreNotOwner\": \"Ви не є власником цього коментаря\",\n    \"confirmDeleteDescription\": \"Ви впевнені, що хочете видалити цей коментар?\",\n    \"hasBeenDeleted\": \"Видалено\",\n    \"replyingTo\": \"Відповідаючи на\",\n    \"noAccessDeleteComment\": \"Ви не можете видалити цей коментар\",\n    \"collapse\": \"Згорнути\",\n    \"readMore\": \"Читати далі\",\n    \"failedToAddComment\": \"Не вдалося додати коментар\",\n    \"commentAddedSuccessfully\": \"Коментар успішно додано.\",\n    \"commentAddedSuccessTip\": \"Ви щойно додали коментар або відповіли на нього. Бажаєте перейти вгору, щоб переглянути останні коментарі?\"\n  },\n  \"template\": {\n    \"asTemplate\": \"Як шаблон\",\n    \"name\": \"Назва шаблону\",\n    \"description\": \"Опис шаблону\",\n    \"about\": \"Шаблон Про\",\n    \"preview\": \"Попередній перегляд шаблону\",\n    \"categories\": \"Категорії шаблонів\",\n    \"isNewTemplate\": \"PIN для нового шаблону\",\n    \"featured\": \"PIN-код для рекомендованого\",\n    \"relatedTemplates\": \"Пов’язані шаблони\",\n    \"requiredField\": \"{field} є обов’язковим\",\n    \"addCategory\": \"Додати \\\"{category}\\\"\",\n    \"addNewCategory\": \"Додати нову категорію\",\n    \"addNewCreator\": \"Додати нового творця\",\n    \"deleteCategory\": \"Видалити категорію\",\n    \"editCategory\": \"Редагувати категорію\",\n    \"editCreator\": \"Редагувати творця\",\n    \"category\": {\n      \"name\": \"Назва категорії\",\n      \"icon\": \"Значок категорії\",\n      \"bgColor\": \"Колір фону категорії\",\n      \"priority\": \"Пріоритет категорії\",\n      \"desc\": \"Опис категорії\",\n      \"type\": \"Тип категорії\",\n      \"icons\": \"Іконки категорій\",\n      \"colors\": \"Категорія кольори\",\n      \"byUseCase\": \"За випадком використання\",\n      \"byFeature\": \"За ознакою\",\n      \"deleteCategory\": \"Видалити категорію\",\n      \"deleteCategoryDescription\": \"Ви впевнені, що хочете видалити цю категорію?\",\n      \"typeToSearch\": \"Введіть для пошуку категорій...\"\n    },\n    \"creator\": {\n      \"label\": \"Творець шаблонів\",\n      \"name\": \"Ім'я творця\",\n      \"avatar\": \"Аватар творця\",\n      \"accountLinks\": \"Посилання на акаунт творця\",\n      \"uploadAvatar\": \"Натисніть, щоб завантажити аватар\",\n      \"deleteCreator\": \"Видалити творця\",\n      \"deleteCreatorDescription\": \"Ви впевнені, що хочете видалити цього творця?\",\n      \"typeToSearch\": \"Введіть для пошуку творців...\"\n    },\n    \"uploadSuccess\": \"Шаблон успішно завантажено\",\n    \"uploadSuccessDescription\": \"Ваш шаблон успішно завантажено. Тепер ви можете переглянути його в галереї шаблонів.\",\n    \"viewTemplate\": \"Переглянути шаблон\",\n    \"deleteTemplate\": \"Видалити шаблон\",\n    \"deleteTemplateDescription\": \"Ви впевнені, що хочете видалити цей шаблон?\",\n    \"addRelatedTemplate\": \"Додайте відповідний шаблон\",\n    \"removeRelatedTemplate\": \"Видалити пов’язаний шаблон\",\n    \"uploadAvatar\": \"Завантажити аватар\",\n    \"searchInCategory\": \"Шукати в {category}\",\n    \"label\": \"Шаблон\"\n  },\n  \"fileDropzone\": {\n    \"dropFile\": \"Натисніть або перетягніть файл у цю область, щоб завантажити\",\n    \"uploading\": \"Завантаження...\",\n    \"uploadFailed\": \"Помилка завантаження\",\n    \"uploadSuccess\": \"Завантаження успішне\",\n    \"uploadSuccessDescription\": \"Файл успішно завантажено\",\n    \"uploadFailedDescription\": \"Не вдалося завантажити файл\",\n    \"uploadingDescription\": \"Файл завантажується\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/ur.json",
    "content": "{\r\n    \"appName\": \"AppFlowy\",\r\n    \"defaultUsername\": \"میں\",\r\n    \"welcomeText\": \"میں خوش آمدید @:appName\",\r\n    \"githubStarText\": \"GitHub پر ستارہ دیں\",\r\n    \"subscribeNewsletterText\": \"نیوزلیٹر سبسکرائب کریں\",\r\n    \"letsGoButtonText\": \"چلو\",\r\n    \"title\": \"عنوان\",\r\n    \"youCanAlso\": \"پ یہ بھی کر سکتے ہیں\",\r\n    \"and\": \"اور\",\r\n    \"blockActions\": {\r\n        \"addBelowTooltip\": \"نیچے شامل کرنے کے لیے کلک کریں\",\r\n        \"addAboveCmd\": \"Alt+click\",\r\n        \"addAboveMacCmd\": \"Option+click\",\r\n        \"addAboveTooltip\": \"اوپر شامل کرنے کے لیے\",\r\n        \"dragTooltip\": \"منتقل کرنے کے لیے ڈریگ کریں\",\r\n        \"openMenuTooltip\": \"Click to open menu\"\r\n    },\r\n    \"signUp\": {\r\n        \"buttonText\": \"سائن اپ کریں\",\r\n        \"title\": \"@:appName پر سائن اپ کریں\",\r\n        \"getStartedText\": \"شروع کریں\",\r\n        \"emptyPasswordError\": \"پاس ورڈ خالی نہیں ہونا چاہیے\",\r\n        \"repeatPasswordEmptyError\": \"دہرائیں پاس ورڈ خالی نہیں ہونا چاہیے\",\r\n        \"unmatchedPasswordError\": \"دہرائیں پاس ورڈ پاس ورڈ سے مماثل نہیں ہے\",\r\n        \"alreadyHaveAnAccount\": \"پہلے ہی اکاؤنٹ ہے؟\",\r\n        \"emailHint\": \"ای میل\",\r\n        \"passwordHint\": \"پاس ورڈ\",\r\n        \"repeatPasswordHint\": \"دہرائیں پاس ورڈ\",\r\n        \"signUpWith\": \"اس کے ساتھ سائن اپ کریں:\"\r\n    },\r\n    \"signIn\": {\r\n        \"loginTitle\": \"@:appName پر لاگ ان کریں\",\r\n        \"loginButtonText\": \"لاگ ان کریں\",\r\n        \"loginStartWithAnonymous\": \"ایک ناشناس سیشن سے شروع کریں\",\r\n        \"continueAnonymousUser\": \"ایک ناشناس سیشن کے ساتھ جاری رکھیں\",\r\n        \"buttonText\": \"سائن ان کریں\",\r\n        \"forgotPassword\": \"پاس ورڈ بھول گئے؟\",\r\n        \"emailHint\": \"ای میل\",\r\n        \"passwordHint\": \"پاس ورڈ\",\r\n        \"dontHaveAnAccount\": \"اکاؤنٹ نہیں ہے؟\",\r\n        \"repeatPasswordEmptyError\": \"دہرائیں پاس ورڈ خالی نہیں ہونا چاہیے\",\r\n        \"unmatchedPasswordError\": \"دہرائیں پاس ورڈ پاس ورڈ سے مماثل نہیں ہے\",\r\n        \"syncPromptMessage\": \"ڈیٹا کو ہمگام بنانے میں کچھ وقت لگ سکتا ہے۔ براہ کرم یہ صفحہ بند نہ کریں۔\",\r\n        \"or\": \"یا\",\r\n        \"LogInWithGoogle\": \"Google کے ساتھ لاگ ان کریں\",\r\n        \"LogInWithGithub\": \"Github کے ساتھ لاگ ان کریں\",\r\n        \"LogInWithDiscord\": \"Discord کے ساتھ لاگ ان کریں\",\r\n        \"signInWith\": \"اس کے ساتھ سائن ان کریں:\"\r\n    },\r\n    \"workspace\": {\r\n        \"chooseWorkspace\": \"اپنا ورک اسپیس منتخب کریں\",\r\n        \"create\": \"ورک اسپیس بنائیں\",\r\n        \"reset\": \"ورک اسپیس ری سیٹ کریں\",\r\n        \"resetWorkspacePrompt\": \"ورک اسپیس کو ری سیٹ کرنے سے اس کے اندر تمام صفحات اور ڈیٹا حذف ہو جائے گا۔ کیا آپ یقینی طور پر ورک اسپیس کو ری سیٹ کرنا چاہتے ہیں؟ متبادل طور پر، آپ ورک اسپیس کو بحال کرنے کے لیے سپورٹ ٹیم سے رابطہ کر سکتے ہیں۔\",\r\n        \"hint\": \"ورک اسپیس\",\r\n        \"notFoundError\": \"ورک اسپیس نہیں ملا\"\r\n    },\r\n    \"shareAction\": {\r\n        \"buttonText\": \"اشتراک کریں\",\r\n        \"workInProgress\": \"جلد آرہا ہے\",\r\n        \"markdown\": \"مارک ڈاؤن\",\r\n        \"csv\": \"CSV\",\r\n        \"copyLink\": \"لنک کاپی کریں\"\r\n    },\r\n    \"moreAction\": {\r\n        \"small\": \"چھوٹا\",\r\n        \"medium\": \"متوسط\",\r\n        \"large\": \"بڑا\",\r\n        \"fontSize\": \"فونٹ کا سائز\",\r\n        \"import\": \" درآمد کریں\",\r\n        \"moreOptions\": \"مزید اختیارات\"\r\n    },\r\n    \"importPanel\": {\r\n        \"textAndMarkdown\": \"ٹیکسٹ اور مارک ڈاؤن\",\r\n        \"documentFromV010\": \"v0.1.0 سے دستاویز\",\r\n        \"databaseFromV010\": \"v0.1.0 سے ڈیٹابیس\",\r\n        \"csv\": \"CSV\",\r\n        \"database\": \"ڈیٹابیس\"\r\n    },\r\n    \"disclosureAction\": {\r\n        \"rename\": \"نام بدلیں\",\r\n        \"delete\": \"حذف کریں\",\r\n        \"duplicate\": \"نقل کریں\",\r\n        \"unfavorite\": \"پسندیدہ سے ہٹائیں\",\r\n        \"favorite\": \"پسندیدہ میں شامل کریں\",\r\n        \"openNewTab\": \"نئے ٹیب میں کھولیں\",\r\n        \"moveTo\": \"منتقل کریں\",\r\n        \"addToFavorites\": \"پسندیدہ میں شامل کریں\",\r\n        \"copyLink\": \"لنک کاپی کریں\"\r\n    },\r\n    \"blankPageTitle\": \"خالی صفحہ\",\r\n    \"newPageText\": \"نیا صفحہ\",\r\n    \"newDocumentText\": \"نیا دستاویز\",\r\n    \"newGridText\": \"نیا گرڈ\",\r\n    \"newCalendarText\": \"نیا کیلنڈر\",\r\n    \"newBoardText\": \"نیا بورڈ\",\r\n    \"trash\": {\r\n        \"text\": \"ٹوکری\",\r\n        \"restoreAll\": \"تمام بحال کریں\",\r\n        \"deleteAll\": \"تمام حذف کریں\",\r\n        \"pageHeader\": {\r\n            \"fileName\": \"فائل کا نام\",\r\n            \"lastModified\": \"آخری بار ترمیم کیا گیا\",\r\n            \"created\": \"بنائی گئی\"\r\n        },\r\n        \"confirmDeleteAll\": {\r\n            \"title\": \"کیا آپ ٹوکری میں تمام صفحات حذف کرنا چاہتے ہیں؟\",\r\n            \"caption\": \"اس کارروائی کو کالعدم نہیں کیا جا سکتا۔\"\r\n        },\r\n        \"confirmRestoreAll\": {\r\n            \"title\": \"کیا آپ ٹوکری میں تمام صفحات بحال کرنا چاہتے ہیں؟\",\r\n            \"caption\": \"اس کارروائی کو کالعدم نہیں کیا جا سکتا۔\"\r\n        }\r\n    },\r\n    \"deletePagePrompt\": {\r\n        \"text\": \"یہ صفحہ ٹوکری میں ہے\",\r\n        \"restore\": \"صفحہ بحال کریں\",\r\n        \"deletePermanent\": \"مستقل طور پر حذف کریں\"\r\n    },\r\n    \"dialogCreatePageNameHint\": \"صفحہ کا نام\",\r\n    \"questionBubble\": {\r\n        \"shortcuts\": \"شارٹ کٹس\",\r\n        \"whatsNew\": \"کیا نیا ہے؟\",\r\n        \"help\": \"مدد اور سپورٹ\",\r\n        \"markdown\": \"مارک ڈاؤن\",\r\n        \"debug\": {\r\n            \"name\": \"ڈیبگ معلومات\",\r\n            \"success\": \"ڈیبگ معلومات کلپ بورڈ پر کاپی ہو گئیں!\",\r\n            \"fail\": \"ڈیبگ معلومات کلپ بورڈ پر کاپی نہیں کی جا سکیں\"\r\n        },\r\n        \"feedback\": \"تاثرات\"\r\n    },\r\n    \"menuAppHeader\": {\r\n        \"moreButtonToolTip\": \"حذف کریں، نام بدلیں، اور مزید...\",\r\n        \"addPageTooltip\": \"اندر جلد از جلد ایک صفحہ شامل کریں۔\",\r\n        \"defaultNewPageName\": \"بغیر عنوان\",\r\n        \"renameDialog\": \"نام بدلیں\"\r\n    },\r\n    \"toolbar\": {\r\n        \"undo\": \"واپس لو\",\r\n        \"redo\": \"دوبارہ کرنا\",\r\n        \"bold\": \"موٹا\",\r\n        \"italic\": \"ترچھی\",\r\n        \"underline\": \"اندرونی خط\",\r\n        \"strike\": \"لائن کے ذریعے\",\r\n        \"numList\": \"نمبر والی فہرست\",\r\n        \"bulletList\": \"بلیٹ والی فہرست\",\r\n        \"checkList\": \"چیک لسٹ\",\r\n        \"inlineCode\": \"ان لائن کوڈ\",\r\n        \"quote\": \"اقتباس بلاک\",\r\n        \"header\": \"ہیڈر\",\r\n        \"highlight\": \"ہائی لائٹ\",\r\n        \"color\": \"رنگ\",\r\n        \"addLink\": \"لنک شامل کریں\",\r\n        \"link\": \"لنک\"\r\n    },\r\n    \"tooltip\": {\r\n        \"lightMode\": \"لائٹ موڈ پر سوئچ کریں\",\r\n        \"darkMode\": \"ڈارک موڈ پر سوئچ کریں\",\r\n        \"openAsPage\": \"ایک صفحہ کے طور پر کھولیں\",\r\n        \"addNewRow\": \"ایک نئی قطار شامل کریں\",\r\n        \"openMenu\": \"مینو کھولنے کے لیے کلک کریں۔\",\r\n        \"dragRow\": \"قطار کو دوبارہ ترتیب دینے کے لیے طویل پریس کریں۔\",\r\n        \"viewDataBase\": \"ڈیٹابیس دیکھیں\",\r\n        \"referencePage\": \"اس {name} کا حوالہ دیا گیا ہے\",\r\n        \"addBlockBelow\": \"نیچے ایک بلاک شامل کریں\"\r\n    },\r\n    \"sideBar\": {\r\n        \"closeSidebar\": \"سائیڈ بار بند کریں\",\r\n        \"openSidebar\": \"سائیڈ بار کھولیں\",\r\n        \"personal\": \"ذاتی\",\r\n        \"favorites\": \"پسندیدہ\",\r\n        \"clickToHidePersonal\": \"ذاتی سیکشن چھپانے کے لیے کلک کریں۔\",\r\n        \"clickToHideFavorites\": \"پسندیدہ سیکشن چھپانے کے لیے کلک کریں۔\",\r\n        \"addAPage\": \"صفحہ شامل کریں\"\r\n    },\r\n    \"notifications\": {\r\n        \"export\": {\r\n            \"markdown\": \"مارک ڈاؤن کو نوٹ برآمد کریں\",\r\n            \"path\": \"دستخط/flowy\"\r\n        }\r\n    },\r\n    \"contactsPage\": {\r\n        \"title\": \"رابطے\",\r\n        \"whatsHappening\": \"اس ہفتے کیا ہو رہا ہے؟\",\r\n        \"addContact\": \"رابطہ شامل کریں\",\r\n        \"editContact\": \"رابطہ ایڈیٹ کریں\"\r\n    },\r\n    \"button\": {\r\n        \"ok\": \"ٹھیک ہے\",\r\n        \"cancel\": \"منسوخ کریں\",\r\n        \"signIn\": \"سائن ان کریں\",\r\n        \"signOut\": \"سائن آؤٹ کریں\",\r\n        \"complete\": \"مکمل\",\r\n        \"save\": \"محفوظ کریں\",\r\n        \"generate\": \"جنریٹ کریں\",\r\n        \"esc\": \"ESC\",\r\n        \"keep\": \"رکھیں\",\r\n        \"tryAgain\": \"دوبارہ کوشش کریں\",\r\n        \"discard\": \"چھوڑ دیں\",\r\n        \"replace\": \"تبدیل کریں\",\r\n        \"insertBelow\": \"نیچے داخل کریں\",\r\n        \"upload\": \"اپ لوڈ کریں\",\r\n        \"edit\": \"تبدیل کریں\",\r\n        \"delete\": \"حذف کریں\",\r\n        \"duplicate\": \"نقل کریں\",\r\n        \"done\": \"ہو گیا\",\r\n        \"putback\": \"واپس رکھیں\"\r\n    },\r\n    \"label\": {\r\n        \"welcome\": \"خیر مقدم ہے!\",\r\n        \"firstName\": \"پہلا نام\",\r\n        \"middleName\": \"وسطی نام\",\r\n        \"lastName\": \"آخری نام\",\r\n        \"stepX\": \"مرحلہ {X}\"\r\n    },\r\n    \"oAuth\": {\r\n        \"err\": {\r\n            \"failedTitle\": \"آپ کے اکاؤنٹ سے کنیکٹ نہیں ہو سکا۔\",\r\n            \"failedMsg\": \"براہ کرم یقینی بنائیں کہ آپ نے اپنے ویب براؤزر میں سائن ان کا عمل مکمل کر لیا ہے۔\"\r\n        },\r\n        \"google\": {\r\n            \"title\": \"گوگل سائن ان کریں\",\r\n            \"instruction1\": \"اپنے گوگل رابطے درآمد کرنے کے لیے، آپ کو اپنے ویب براؤزر کا استعمال کرتے ہوئے اس ایپلی کیشن کو مجاز کرنا ہوگا۔\",\r\n            \"instruction2\": \"آئیکن پر کلک کرکے یا متن منتخب کرکے اس کوڈ کو اپنے کلپ بورڈ پر کاپی کریں:\",\r\n            \"instruction3\": \"اپنے ویب براؤزر میں مندرجہ ذیل لنک پر جائیں اور اوپر دیا گیا کوڈ درج کریں:\",\r\n            \"instruction4\": \"جب آپ سائن اپ مکمل کر لیں تو نیچے دیا گیا بٹن دبائیں:\"\r\n        }\r\n    },\r\n    \"settings\": {\r\n        \"title\": \"ترتیبات\",\r\n        \"menu\": {\r\n            \"appearance\": \"ظہور\",\r\n            \"language\": \"زبان\",\r\n            \"user\": \"صارف\",\r\n            \"files\": \"فائلیں\",\r\n            \"open\": \"ترتیبات کھولیں\",\r\n            \"logout\": \"لاگ آؤٹ کریں\",\r\n            \"logoutPrompt\": \"کیا آپ یقینی طور پر لاگ آؤٹ کرنا چاہتے ہیں؟\",\r\n            \"selfEncryptionLogoutPrompt\": \"کیا آپ یقینی طور پر لاگ آؤٹ کرنا چاہتے ہیں؟ براہ کرم یقینی بنائیں کہ آپ نے انکرپشن راز کاپی کر لیا ہے\",\r\n            \"syncSetting\": \"ترتیبات کو ہم وقت کریں\",\r\n            \"enableSync\": \"ہم وقت سازی کو فعال کریں\",\r\n            \"enableEncrypt\": \"ڈیٹا کو انکرپٹ کریں\",\r\n            \"enableEncryptPrompt\": \"اپنے ڈیٹا کو اس راز کے ساتھ محفوظ کرنے کے لیے انکرپشن کو فعال کریں۔ اسے محفوظ طریقے سے ذخیرہ کریں؛ ایک بار فعال ہونے کے بعد، اسے بند نہیں کیا جا سکتا۔ اگر کھو جائے تو، آپ کا ڈیٹا ناقابل بازیافت ہو جاتا ہے۔ کاپی کرنے کے لیے کلک کریں\",\r\n            \"inputEncryptPrompt\": \"براہ کرم کے لیے اپنا انکرپشن راز درج کریں\",\r\n            \"clickToCopySecret\": \"راز کاپی کرنے کے لیے کلک کریں\",\r\n            \"inputTextFieldHint\": \"آپ کا راز\",\r\n            \"historicalUserList\": \"صارف لاگ ان کی تاریخ\",\r\n            \"historicalUserListTooltip\": \"یہ فہرست آپ کے گمنام اکاؤنٹس دکھاتی ہے۔ آپ اکاؤنٹ پر اس کی تفصیلات دیکھنے کے لیے کلک کر سکتے ہیں۔ گمنام اکاؤنٹ 'شروع کریں' بٹن پر کلک کرنے سے بنائے جاتے ہیں\",\r\n            \"openHistoricalUser\": \"گمنام اکاؤنٹ کھولنے کے لیے کلک کریں\"\r\n        },\r\n        \"appearance\": {\r\n            \"resetSetting\": \"اس ترتیب کو ری سیٹ کریں\",\r\n            \"fontFamily\": {\r\n                \"label\": \"فونٹ فیملی\",\r\n                \"search\": \"تلاش کریں\"\r\n            },\r\n            \"themeMode\": {\r\n                \"label\": \"تھیم موڈ\",\r\n                \"light\": \"لائٹ موڈ\",\r\n                \"dark\": \"ڈارک موڈ\",\r\n                \"system\": \"سسٹم کے ساتھ موافقت کریں\"\r\n            },\r\n            \"layoutDirection\": {\r\n                \"label\": \"لے آؤٹ کی سمت\",\r\n                \"hint\": \"اسکرین کے بائیں یا دائیں طرف سے عناصر کو ترتیب دینا شروع کرنے کے لیے۔\",\r\n                \"ltr\": \"ltr\",\r\n                \"rtl\": \"rtl\"\r\n            },\r\n            \"textDirection\": {\r\n                \"label\": \"ڈیفالٹ ٹیکسٹ سمت\",\r\n                \"hint\": \"جب عنصر پر ٹیکسٹ سمت سیٹ نہیں ہو تو ڈیفالٹ ٹیکسٹ سمت۔\",\r\n                \"ltr\": \"LTR\",\r\n                \"rtl\": \"RTL\",\r\n                \"auto\": \"خودکار\",\r\n                \"fallback\": \"لے آؤٹ کی سمت کے مطابق\"\r\n            },\r\n            \"themeUpload\": {\r\n                \"button\": \"اپ لوڈ کریں\",\r\n                \"description\": \"نیچے دیے گئے بٹن کا استعمال کرتے ہوئے اپنا اپنا AppFlowy تھیم اپ لوڈ کریں۔\",\r\n                \"failure\": \"اپ لوڈ کیا گیا تھیم ایک غیر معتبر فارمیٹ میں تھا۔\",\r\n                \"loading\": \"براہ کرم انتظار کریں جب ہم آپ کے تھیم کو تصدیق اور اپ لوڈ کرتے ہیں۔...\",\r\n                \"uploadSuccess\": \"آپ کا تھیم کامیابی کے ساتھ اپ لوڈ کر دیا گیا\",\r\n                \"deletionFailure\": \"تھیم حذف کرنے میں ناکام ہو گیا۔ اسے دستی طور پر حذف کرنے کی کوشش کریں۔\",\r\n                \"filePickerDialogTitle\": \".flowy_plugin فائل منتخب کریں\",\r\n                \"urlUploadFailure\": \"url کھولنے میں ناکام: {}\"\r\n            },\r\n            \"theme\": \"تھیم\",\r\n            \"builtInsLabel\": \"بلٹ ان تھیمز\",\r\n            \"pluginsLabel\": \"پلگ انز\",\r\n            \"showNamingDialogWhenCreatingPage\": \"صفحہ بناتے وقت نامزدگی کا مکالمہ دکھائیں\"\r\n        },\r\n        \"files\": {\r\n            \"copy\": \"کاپی کریں\",\r\n            \"defaultLocation\": \"فائلیں اور ڈیٹا اسٹوریج کی جگہ پڑھیں\",\r\n            \"exportData\": \"اپنے ڈیٹا کو برآمد کریں\",\r\n            \"doubleTapToCopy\": \"راستہ کاپی کرنے کے لیے دو بار ٹیپ کریں\",\r\n            \"restoreLocation\": \"AppFlowy ڈیفالٹ راستے پر بحال کریں\",\r\n            \"customizeLocation\": \"دوسرا فولڈر کھولیں\",\r\n            \"restartApp\": \"تبدیلیوں کو لاگو کرنے کے لیے براہ کرم ایپ کو دوبارہ شروع کریں۔\",\r\n            \"exportDatabase\": \"ڈیٹابیس برآمد کریں\",\r\n            \"selectFiles\": \"ان فائلوں کو منتخب کریں جنہیں برآمد کرنے کی ضرورت ہے\",\r\n            \"selectAll\": \"تمام کا انتخاب کریں\",\r\n            \"deselectAll\": \"تمام کو غیر منتخب کریں\",\r\n            \"createNewFolder\": \"نیا فولڈر بنائیں\",\r\n            \"createNewFolderDesc\": \"ہمیں بتائیں کہ آپ اپنا ڈیٹا کہاں ذخیرہ کرنا چاہتے ہیں\",\r\n            \"defineWhereYourDataIsStored\": \"تعریف کریں کہ آپ کا ڈیٹا کہاں ذخیرہ کیا گیا ہے\",\r\n            \"open\": \"کھولیں\",\r\n            \"openFolder\": \"موجودہ فولڈر کھولیں\",\r\n            \"openFolderDesc\": \"اسے پڑھیں اور اپنے موجودہ AppFlowy فولڈر میں لکھیں\",\r\n            \"folderHintText\": \"فولڈر کا نام\",\r\n            \"location\": \"نیا فولڈر بنانا\",\r\n            \"locationDesc\": \"اپنے AppFlowy ڈیٹا فولڈر کے لیے نام منتخب کریں\",\r\n            \"browser\": \"براؤز کریں\",\r\n            \"create\": \"بنائیں\",\r\n            \"set\": \"سیٹ کریں\",\r\n            \"folderPath\": \"اپنا فولڈر ذخیرہ کرنے کا راستہ\",\r\n            \"locationCannotBeEmpty\": \"راستہ خالی نہیں ہو سکتا\",\r\n            \"pathCopiedSnackbar\": \"فائل اسٹوریج کا راستہ کلپ بورڈ پر کاپی ہو گیا!\",\r\n            \"changeLocationTooltips\": \"ڈیٹا ڈائرکٹری تبدیل کریں\",\r\n            \"change\": \"تبدیل کریں\",\r\n            \"openLocationTooltips\": \"دوسرا ڈیٹا ڈائرکٹری کھولیں\",\r\n            \"openCurrentDataFolder\": \"موجودہ ڈیٹا ڈائرکٹری کھولیں\",\r\n            \"recoverLocationTooltips\": \"AppFlowy کی ڈیفالٹ ڈیٹا ڈائرکٹری پر ری سیٹ کریں\",\r\n            \"exportFileSuccess\": \"فائل کو کامیابی کے ساتھ برآمد کیا!\",\r\n            \"exportFileFail\": \"فائل برآمد کرنے میں ناکام ہو گیا!\",\r\n            \"export\": \"برآمد کریں\"\r\n        },\r\n        \"user\": {\r\n            \"name\": \"نام\",\r\n            \"email\": \"ای میل\",\r\n            \"selectAnIcon\": \"آئیکن منتخب کریں\",\r\n            \"pleaseInputYourOpenAIKey\": \"براہ کرم اپنی AI کی درج کریں\",\r\n            \"clickToLogout\": \"موجودہ صارف سے لاگ آؤٹ کرنے کے لیے کلک کریں\"\r\n        },\r\n        \"shortcuts\": {\r\n            \"shortcutsLabel\": \"شارٹ کٹس\",\r\n            \"command\": \"کمانڈ\",\r\n            \"keyBinding\": \"کی بائنڈنگ\",\r\n            \"addNewCommand\": \"نیا کمانڈ شامل کریں\",\r\n            \"updateShortcutStep\": \"مطلوبہ کی مجموعہ دبائیں اور ENTER دبائیں\",\r\n            \"shortcutIsAlreadyUsed\": \"یہ شارٹ کٹ پہلے ہی اس کے لیے استعمال ہو چکا ہے: {conflict}\",\r\n            \"resetToDefault\": \"ڈیفالٹ کی بائنڈنگ پر ری سیٹ کریں\",\r\n            \"couldNotLoadErrorMsg\": \"شارٹ کٹس لوڈ نہیں کر سکا، دوبارہ کوشش کریں\",\r\n            \"couldNotSaveErrorMsg\": \"شارٹ کٹس محفوظ نہیں کر سکا، دوبارہ کوشش کریں\"\r\n        }\r\n    },\r\n    \"grid\": {\r\n        \"deleteView\": \"کیا آپ یقینی طور پر اس نظارے کو حذف کرنا چاہتے ہیں؟\",\r\n        \"createView\": \"نیا\",\r\n        \"settings\": {\r\n            \"filter\": \"فلٹر\",\r\n            \"sort\": \"مرتب کریں\",\r\n            \"sortBy\": \"مرتب کریں\",\r\n            \"properties\": \"خصوصیات\",\r\n            \"reorderPropertiesTooltip\": \"خصوصیات کو دوبارہ ترتیب دینے کے لیے ڈریگ کریں\",\r\n            \"group\": \"گروپ\",\r\n            \"addFilter\": \"فلٹر شامل کریں\",\r\n            \"deleteFilter\": \"فلٹر حذف کریں\",\r\n            \"filterBy\": \"فلٹر کریں...\",\r\n            \"typeAValue\": \"کوئی قیمت ٹائپ کریں...\",\r\n            \"layout\": \"لے آؤٹ\",\r\n            \"databaseLayout\": \"لے آؤٹ\"\r\n        },\r\n        \"textFilter\": {\r\n            \"contains\": \"شامل ہے\",\r\n            \"doesNotContain\": \"شامل نہیں ہے\",\r\n            \"endsWith\": \"ختم ہوتا ہے\",\r\n            \"startWith\": \"شروع ہوتا ہے\",\r\n            \"is\": \"ہے\",\r\n            \"isNot\": \"نہیں ہے\",\r\n            \"isEmpty\": \"خالی ہے\",\r\n            \"isNotEmpty\": \"خالی نہیں ہے\",\r\n            \"choicechipPrefix\": {\r\n                \"isNot\": \"نہیں\",\r\n                \"startWith\": \"شروع ہوتا ہے\",\r\n                \"endWith\": \"ختم ہوتا ہے\",\r\n                \"isEmpty\": \"خالی ہے\",\r\n                \"isNotEmpty\": \"خالی نہیں ہے\"\r\n            }\r\n        },\r\n        \"checkboxFilter\": {\r\n            \"isChecked\": \"چیک شدہ\",\r\n            \"isUnchecked\": \"غیر چیک شدہ\",\r\n            \"choicechipPrefix\": {\r\n                \"is\": \"ہے\"\r\n            }\r\n        },\r\n        \"checklistFilter\": {\r\n            \"isComplete\": \"مکمل ہے\",\r\n            \"isIncomplted\": \"نامکمل ہے\"\r\n        },\r\n        \"selectOptionFilter\": {\r\n            \"is\": \"ہے\",\r\n            \"isNot\": \"نہیں ہے\",\r\n            \"isEmpty\": \"خالی ہے\",\r\n            \"isNotEmpty\": \"خالی نہیں ہے\"\r\n        },\r\n        \"multiSelectOptionFilter\": {\r\n            \"contains\": \"شامل ہے\",\r\n            \"doesNotContain\": \"شامل نہیں ہے\",\r\n            \"isEmpty\": \"خالی ہے\",\r\n            \"isNotEmpty\": \"خالی نہیں ہے\"\r\n        },\r\n        \"field\": {\r\n            \"hide\": \"چھپائیں\",\r\n            \"insertLeft\": \"بائیں طرف درج کریں\",\r\n            \"insertRight\": \"دائیں طرف درج کریں\",\r\n            \"duplicate\": \"ڈپلیکیٹ کریں\",\r\n            \"delete\": \"حذف کریں\",\r\n            \"textFieldName\": \"متن\",\r\n            \"checkboxFieldName\": \"چیک باکس\",\r\n            \"dateFieldName\": \"تاریخ\",\r\n            \"updatedAtFieldName\": \"آخری بار ترمیم کا وقت\",\r\n            \"createdAtFieldName\": \"بنائے جانے کا وقت\",\r\n            \"numberFieldName\": \"اعداد\",\r\n            \"singleSelectFieldName\": \"منتخب کریں\",\r\n            \"multiSelectFieldName\": \"ملٹی سلیکٹ\",\r\n            \"urlFieldName\": \"URL\",\r\n            \"checklistFieldName\": \"چیک لسٹ\",\r\n            \"numberFormat\": \"نمبر فارمیٹ\",\r\n            \"dateFormat\": \"تاریخ کا فارمیٹ\",\r\n            \"includeTime\": \"وقت شامل کریں\",\r\n            \"dateFormatFriendly\": \"مہینہ دن، سال\",\r\n            \"dateFormatISO\": \"سال-مہینہ-دن\",\r\n            \"dateFormatLocal\": \"مہینہ/دن/سال\",\r\n            \"dateFormatUS\": \"سال/مہینہ/دن\",\r\n            \"dateFormatDayMonthYear\": \"دن/مہینہ/سال\",\r\n            \"timeFormat\": \"وقت کا فارمیٹ\",\r\n            \"invalidTimeFormat\": \"غلط فارمیٹ\",\r\n            \"timeFormatTwelveHour\": \"12 گھنٹے\",\r\n            \"timeFormatTwentyFourHour\": \"24 گھنٹے\",\r\n            \"clearDate\": \"تاریخ صاف کریں\",\r\n            \"addSelectOption\": \"ایک آپشن شامل کریں\",\r\n            \"optionTitle\": \"آپشنز\",\r\n            \"addOption\": \"آپشن شامل کریں\",\r\n            \"editProperty\": \"خاصیت میں ترمیم کریں\",\r\n            \"newProperty\": \"نیا پراپرٹی\",\r\n            \"deleteFieldPromptMessage\": \"کیا آپ یقینی ہیں؟ یہ پراپرٹی حذف کر دی جائے گی\",\r\n            \"newColumn\": \"نیا کالم\"\r\n        },\r\n        \"sort\": {\r\n            \"ascending\": \"چڑھائی\",\r\n            \"descending\": \"اترائی\",\r\n            \"deleteAllSorts\": \"تمام ترتیب کو حذف کریں\",\r\n            \"addSort\": \"ترتیب شامل کریں\"\r\n        },\r\n        \"row\": {\r\n            \"duplicate\": \"ڈپلیکیٹ کریں\",\r\n            \"delete\": \"حذف کریں\",\r\n            \"titlePlaceholder\": \"عنوان\",\r\n            \"textPlaceholder\": \"خالی\",\r\n            \"copyProperty\": \"خاصیت کو کلپ بورڈ پر کاپی کر دیا گیا\",\r\n            \"count\": \"گِنتی\",\r\n            \"newRow\": \"نیا سطر\",\r\n            \"action\": \"عمل\"\r\n        },\r\n        \"selectOption\": {\r\n            \"create\": \"بنائیں\",\r\n            \"purpleColor\": \"جامنی\",\r\n            \"pinkColor\": \"گلابی\",\r\n            \"lightPinkColor\": \"ہلکا گلابی\",\r\n            \"orangeColor\": \"سنتری\",\r\n            \"yellowColor\": \"پیلا\",\r\n            \"limeColor\": \"چونا\",\r\n            \"greenColor\": \"سبز\",\r\n            \"aquaColor\": \"کوا\",\r\n            \"blueColor\": \"نیلا\",\r\n            \"deleteTag\": \"ٹैग حذف کریں\",\r\n            \"colorPanelTitle\": \"رنگ\",\r\n            \"panelTitle\": \"کوئی آپشن منتخب کریں یا بنائیں\",\r\n            \"searchOption\": \"آپشن تلاش کریں\",\r\n            \"searchOrCreateOption\": \"آپشن تلاش کریں یا بنائیں...\",\r\n            \"createNew\": \"نیا بنائیں\",\r\n            \"orSelectOne\": \"یا کوئی آپشن منتخب کریں\"\r\n        },\r\n        \"checklist\": {\r\n            \"taskHint\": \"ٹاسک کی وضاحت\",\r\n            \"addNew\": \"نیا ٹاسک شامل کریں\",\r\n            \"submitNewTask\": \"بنائیں\"\r\n        },\r\n        \"menuName\": \"گرِڈ\",\r\n        \"referencedGridPrefix\": \"کا نظارہ\"\r\n    },\r\n    \"document\": {\r\n        \"menuName\": \"دستایز\",\r\n        \"date\": {\r\n            \"timeHintTextInTwelveHour\": \"01:00 PM\",\r\n            \"timeHintTextInTwentyFourHour\": \"13:00\"\r\n        },\r\n        \"slashMenu\": {\r\n            \"board\": {\r\n                \"selectABoardToLinkTo\": \"جس بورڈ سے لنک کرنا چاہتے ہیں اسے منتخب کریں\",\r\n                \"createANewBoard\": \"نیا بورڈ بنائیں\"\r\n            },\r\n            \"grid\": {\r\n                \"selectAGridToLinkTo\": \"جس گرِڈ سے لنک کرنا چاہتے ہیں اسے منتخب کریں\",\r\n                \"createANewGrid\": \"نیا گرِڈ بنائیں\"\r\n            },\r\n            \"calendar\": {\r\n                \"selectACalendarToLinkTo\": \"جس کیلنڈر سے لنک کرنا چاہتے ہیں اسے منتخب کریں\",\r\n                \"createANewCalendar\": \"نیا کیلنڈر بنائیں\"\r\n            }\r\n        },\r\n        \"selectionMenu\": {\r\n            \"outline\": \"آؤٹ لائن\",\r\n            \"codeBlock\": \"کوڈ بلاک\"\r\n        },\r\n        \"plugins\": {\r\n            \"referencedBoard\": \"حوالہ شدہ بورڈ\",\r\n            \"referencedGrid\": \"حوالہ شدہ گرِڈ\",\r\n            \"referencedCalendar\": \"حوالہ شدہ کیلنڈر\",\r\n            \"autoGeneratorMenuItemName\": \"AI رائٹر\",\r\n            \"autoGeneratorTitleName\": \"AI: AI سے کچھ بھی لکھنے کے لیے کہیں...\",\r\n            \"autoGeneratorLearnMore\": \"مزید جانئے\",\r\n            \"autoGeneratorGenerate\": \"جنریٹ کریں\",\r\n            \"autoGeneratorHintText\": \"AI سے پوچھیں...\",\r\n            \"autoGeneratorCantGetOpenAIKey\": \"AI کی حاصل نہیں کر سکتا\",\r\n            \"autoGeneratorRewrite\": \"دوبارہ لکھیں\",\r\n            \"smartEdit\": \"AI اسسٹنٹ\",\r\n            \"aI\": \"AI\",\r\n            \"smartEditFixSpelling\": \"املا درست کریں\",\r\n            \"warning\": \"⚠️ AI کی پاسخیں غلط یا گمراہ کن ہو سکتی ہیں۔\",\r\n            \"smartEditSummarize\": \"سارے لکھیں\",\r\n            \"smartEditImproveWriting\": \"تحریر بہتر بنائیں\",\r\n            \"smartEditMakeLonger\": \"طویل تر بنائیں\",\r\n            \"smartEditCouldNotFetchResult\": \"AI سے نتیجہ حاصل نہیں کر سکا\",\r\n            \"smartEditCouldNotFetchKey\": \"AI کی حاصل نہیں کر سکا\",\r\n            \"smartEditDisabled\": \"Settings میں AI سے منسلک کریں\",\r\n            \"discardResponse\": \"کیا آپ AI کی پاسخیں حذف کرنا چاہتے ہیں؟\",\r\n            \"createInlineMathEquation\": \"مساوات بنائیں\",\r\n            \"toggleList\": \"فہرست ٹوگل کریں\",\r\n            \"cover\": {\r\n                \"changeCover\": \"سرورق تبدیل کریں\",\r\n                \"colors\": \"رنگ\",\r\n                \"images\": \"تصاویر\",\r\n                \"clearAll\": \"تمام کو صاف کریں\",\r\n                \"abstract\": \"خلاصہ\",\r\n                \"addCover\": \"سرورق شامل کریں\",\r\n                \"addLocalImage\": \"مقامی تصویر شامل کریں\",\r\n                \"invalidImageUrl\": \"غلط تصویر URL\",\r\n                \"failedToAddImageToGallery\": \"تصویر کو گیلری میں شامل کرنے میں ناکام\",\r\n                \"enterImageUrl\": \"تصویر URL درج کریں\",\r\n                \"add\": \"شامل کریں\",\r\n                \"back\": \"پیچھے\",\r\n                \"saveToGallery\": \"گیلری میں محفوظ کریں\",\r\n                \"removeIcon\": \"آئیکن کو ہٹائیں\",\r\n                \"pasteImageUrl\": \"تصویر URL پیسٹ کریں\",\r\n                \"or\": \"یا\",\r\n                \"pickFromFiles\": \"فائلوں سے منتخب کریں\",\r\n                \"couldNotFetchImage\": \"تصویر حاصل نہیں کر سکا\",\r\n                \"imageSavingFailed\": \"تصویر محفوظ کرنے میں ناکام\",\r\n                \"addIcon\": \"آئیکن شامل کریں\",\r\n                \"coverRemoveAlert\": \"اسے ہٹانے کے بعد اسے سرورق سے ہٹا دیا جائے گا۔\",\r\n                \"alertDialogConfirmation\": \"کیا آپ یقینی ہیں کہ آپ جاری رکھنا چاہتے ہیں؟\"\r\n            },\r\n            \"mathEquation\": {\r\n                \"addMathEquation\": \"ریاضی کی مساوات شامل کریں\",\r\n                \"editMathEquation\": \"ریاضی کی مساوات میں ترمیم کریں\"\r\n            },\r\n            \"optionAction\": {\r\n                \"click\": \"کلک کریں\",\r\n                \"toOpenMenu\": \" مینو کھولنے کے لیے\",\r\n                \"delete\": \"حذف کریں\",\r\n                \"duplicate\": \"ڈپلیکیٹ کریں\",\r\n                \"turnInto\": \"میں تبدیل کریں\",\r\n                \"moveUp\": \"اوپر منتقل کریں\",\r\n                \"moveDown\": \"نیچے منتقل کریں\",\r\n                \"color\": \"رنگ\",\r\n                \"align\": \"ترتیب دیں\",\r\n                \"left\": \"بائیں\",\r\n                \"center\": \"وسط\",\r\n                \"right\": \"دائیں\",\r\n                \"defaultColor\": \"ڈیفالٹ\"\r\n            },\r\n            \"image\": {\r\n                \"copiedToPasteBoard\": \"تصویری لنک کو کلپ بورڈ پر کاپی کر دیا گیا ہے\"\r\n            },\r\n            \"outline\": {\r\n                \"addHeadingToCreateOutline\": \"目次 بنانے کے لیے عنوانات شامل کریں۔\"\r\n            },\r\n            \"table\": {\r\n                \"addAfter\": \"بعد میں شامل کریں\",\r\n                \"addBefore\": \"پہلے شامل کریں\",\r\n                \"delete\": \"حذف کریں\",\r\n                \"clear\": \"محتاجات کو صاف کریں\",\r\n                \"duplicate\": \"ڈپلیکیٹ کریں\",\r\n                \"bgColor\": \"پس منظر کا رنگ\"\r\n            },\r\n            \"contextMenu\": {\r\n                \"copy\": \"کاپی کریں\",\r\n                \"cut\": \"کاٹیں\",\r\n                \"paste\": \"چسپاں کریں\"\r\n            }\r\n        },\r\n        \"textBlock\": {\r\n            \"placeholder\": \"کمانڈز کے لیے '/' ٹائپ کریں\"\r\n        },\r\n        \"title\": {\r\n            \"placeholder\": \"بغیر عنوان\"\r\n        },\r\n        \"imageBlock\": {\r\n            \"placeholder\": \"تصویریں شامل کرنے کے لیے کلک کریں\",\r\n            \"upload\": {\r\n                \"label\": \"اپ لوڈ کریں\",\r\n                \"placeholder\": \"تصویریں اپ لوڈ کرنے کے لیے کلک کریں\"\r\n            },\r\n            \"url\": {\r\n                \"label\": \"تصویری URL\",\r\n                \"placeholder\": \"تصویری URL درج کریں\"\r\n            },\r\n            \"support\": \"تصویری سائز کی حد 5MB ہے۔ سپورٹڈ فارمیٹس: JPEG، PNG، GIF، SVG\",\r\n            \"error\": {\r\n                \"invalidImage\": \"غلط تصویر\",\r\n                \"invalidImageSize\": \"تصویری سائز 5MB سے کم ہونا چاہیے\",\r\n                \"invalidImageFormat\": \"تصویری فارمیٹ سپورٹڈ نہیں ہے۔ سپورٹڈ فارمیٹس: JPEG، PNG، GIF، SVG\",\r\n                \"invalidImageUrl\": \"غلط تصویری URL\"\r\n            }\r\n        },\r\n        \"codeBlock\": {\r\n            \"language\": {\r\n                \"label\": \"زبان\",\r\n                \"placeholder\": \"زبان منتخب کریں\"\r\n            }\r\n        },\r\n        \"inlineLink\": {\r\n            \"placeholder\": \"لنک پیسٹ یا ٹائپ کریں\",\r\n            \"openInNewTab\": \"نیا ٹیب میں کھولیں\",\r\n            \"copyLink\": \"لنک کاپی کریں\",\r\n            \"removeLink\": \"لنک ہٹائیں\",\r\n            \"url\": {\r\n                \"label\": \"لنک URL\",\r\n                \"placeholder\": \"لنک URL درج کریں\"\r\n            },\r\n            \"title\": {\r\n                \"label\": \"لنک کا عنوان\",\r\n                \"placeholder\": \"لنک کا عنوان درج کریں\"\r\n            }\r\n        },\r\n        \"mention\": {\r\n            \"placeholder\": \"کسی شخص، صفحے یا تاریخ کا ذکر کریں...\",\r\n            \"page\": {\r\n                \"label\": \"صفحے سے لنک کریں\",\r\n                \"tooltip\": \"صفحہ کھولنے کے لیے کلک کریں\"\r\n            }\r\n        }\r\n    },\r\n    \"board\": {\r\n        \"column\": {\r\n            \"createNewCard\": \"نواں\"\r\n        },\r\n        \"menuName\": \"بورڈ\",\r\n        \"referencedBoardPrefix\": \"کے نظارہ\"\r\n    },\r\n    \"calendar\": {\r\n        \"menuName\": \"کیلنڈر\",\r\n        \"defaultNewCalendarTitle\": \"بغیر عنوان\",\r\n        \"navigation\": {\r\n            \"today\": \"آج\",\r\n            \"jumpToday\": \"آج پر جائیں\",\r\n            \"previousMonth\": \"پچھلا مہینہ\",\r\n            \"nextMonth\": \"اگلا مہینہ\"\r\n        },\r\n        \"settings\": {\r\n            \"showWeekNumbers\": \"ہفتے کے نمبر دکھائیں\",\r\n            \"showWeekends\": \"ہفتے کے آخر میں دکھائیں\",\r\n            \"firstDayOfWeek\": \"ہفتہ شروع کریں\",\r\n            \"layoutDateField\": \"کیلنڈر کی ترتیب دیں\",\r\n            \"noDateTitle\": \"کوئی تاریخ نہیں\",\r\n            \"noDateHint\": \"غیر شیڈول شدہ واقعات یہاں دکھائے جائیں گے\",\r\n            \"clickToAdd\": \"کیلنڈر میں شامل کرنے کے لیے کلک کریں\",\r\n            \"name\": \"کیلنڈر کا ترتیب\"\r\n        },\r\n        \"referencedCalendarPrefix\": \"کے نظارہ\"\r\n    },\r\n    \"errorDialog\": {\r\n        \"title\": \"AppFlowy کی غلطی\",\r\n        \"howToFixFallback\": \"اس پریشانی کے لیے معذرت چاہتے ہیں! اپنی غلطی کی وضاحت کرنے والا ایک ایشو ہماری گٹ ہب پیج پر جمع کریں۔\",\r\n        \"github\": \"گٹ ہب پر دیکھیں\"\r\n    },\r\n    \"search\": {\r\n        \"label\": \"تلاش کریں\",\r\n        \"placeholder\": {\r\n            \"actions\": \"کارروائیاں تلاش کریں...\"\r\n        }\r\n    },\r\n    \"message\": {\r\n        \"copy\": {\r\n            \"success\": \"کاپی ہو چکا ہے!\",\r\n            \"fail\": \"کاپی نہیں کر سکے\"\r\n        }\r\n    },\r\n    \"unSupportBlock\": \"موجودہ ورژن اس بلاک کی حمایت نہیں کرتا۔\",\r\n    \"views\": {\r\n        \"deleteContentTitle\": \"کیا آپ یقینی طور پر {pageType} کو حذف کرنا چاہتے ہیں؟\",\r\n        \"deleteContentCaption\": \"اگر آپ اس {pageType} کو حذف کرتے ہیں، تو آپ اسے ٹریش سے بحال کر سکتے ہیں۔\"\r\n    },\r\n    \"colors\": {\r\n        \"custom\": \"اپنی مرضی کے مطابق\",\r\n        \"default\": \"ڈیفالٹ\",\r\n        \"red\": \"سرخ\",\r\n        \"orange\": \"سنتری\",\r\n        \"yellow\": \"پیلا\",\r\n        \"green\": \"سبز\",\r\n        \"blue\": \"نیلا\",\r\n        \"purple\": \"جامنی\",\r\n        \"pink\": \"گلابی\",\r\n        \"brown\": \"براؤن\",\r\n        \"gray\": \"سرمئی\"\r\n    },\r\n    \"emoji\": {\r\n        \"filter\": \"فلٹر\",\r\n        \"random\": \"رینڈم\",\r\n        \"selectSkinTone\": \"جلد کا رنگ منتخب کریں\",\r\n        \"remove\": \"ایموجی کو ہٹائیں\",\r\n        \"categories\": {\r\n            \"smileys\": \"مسکراہٹیں اور جذبات\",\r\n            \"people\": \"لوگ اور جسم\",\r\n            \"animals\": \"جانور اور فطرت\",\r\n            \"food\": \"کھانا اور پینا\",\r\n            \"activities\": \"سرگرمیاں\",\r\n            \"places\": \"سفر اور مقامات\",\r\n            \"objects\": \"آब्جیکٹس\",\r\n            \"symbols\": \"علامات\",\r\n            \"flags\": \"پرچم\",\r\n            \"nature\": \"فطرت\",\r\n            \"frequentlyUsed\": \"کثرت سے استعمال ہونے والے\"\r\n        }\r\n    }\r\n}"
  },
  {
    "path": "frontend/resources/translations/vi-VN.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"Tôi\",\n  \"welcomeText\": \"Chào mừng bạn đến @:appName\",\n  \"welcomeTo\": \"Chào mừng bạn đến\",\n  \"githubStarText\": \"Sao trên GitHub\",\n  \"subscribeNewsletterText\": \"Đăng ký bản tin\",\n  \"letsGoButtonText\": \"Bắt đầu nhanh\",\n  \"title\": \"Tiêu đề\",\n  \"youCanAlso\": \"Bạn cũng có thể\",\n  \"and\": \"và\",\n  \"failedToOpenUrl\": \"Không mở được url: {}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"Nhấn để thêm vào bên dưới\",\n    \"addAboveCmd\": \"Alt+nhấp chuột\",\n    \"addAboveMacCmd\": \"Option+nhấp chuột\",\n    \"addAboveTooltip\": \"để thêm ở trên\",\n    \"dragTooltip\": \"Kéo để di chuyển\",\n    \"openMenuTooltip\": \"Bấm để mở menu\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"Đăng ký\",\n    \"title\": \"Đăng ký @:appName\",\n    \"getStartedText\": \"Bắt đầu\",\n    \"emptyPasswordError\": \"Mật khẩu không thể trống\",\n    \"repeatPasswordEmptyError\": \"Mật khẩu nhập lại không được để trống\",\n    \"unmatchedPasswordError\": \"Mật khẩu nhập lại không giống mật khẩu\",\n    \"alreadyHaveAnAccount\": \"Đã có tài khoản?\",\n    \"emailHint\": \"E-mail\",\n    \"passwordHint\": \"Mật khẩu\",\n    \"repeatPasswordHint\": \"Nhập lại mật khẩu\",\n    \"signUpWith\": \"Đăng ký với:\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"Đăng nhập vào @:appName\",\n    \"loginButtonText\": \"Đăng nhập\",\n    \"loginStartWithAnonymous\": \"Bắt đầu với một phiên ẩn danh\",\n    \"continueAnonymousUser\": \"Tiếp tục với một phiên ẩn danh\",\n    \"buttonText\": \"Đăng nhập\",\n    \"signingInText\": \"Đang đăng nhập...\",\n    \"forgotPassword\": \"Quên mật khẩu?\",\n    \"emailHint\": \"E-mail\",\n    \"passwordHint\": \"Mật khẩu\",\n    \"dontHaveAnAccount\": \"Bạn chưa có tài khoản?\",\n    \"createAccount\": \"Tạo tài khoản\",\n    \"repeatPasswordEmptyError\": \"Mật khẩu nhập lại không được để trống\",\n    \"unmatchedPasswordError\": \"Mật khẩu nhập lại không giống mật khẩu\",\n    \"syncPromptMessage\": \"Việc đồng bộ hóa dữ liệu có thể mất một lúc. Xin đừng đóng trang này\",\n    \"or\": \"HOẶC\",\n    \"signInWithGoogle\": \"Tiếp tục với Google\",\n    \"signInWithGithub\": \"Tiếp tục với Github\",\n    \"signInWithDiscord\": \"Tiếp tục với Discord\",\n    \"signInWithApple\": \"Tiếp tục với Apple\",\n    \"continueAnotherWay\": \"Tiếp tục theo cách khác\",\n    \"signUpWithGoogle\": \"Đăng ký với Google\",\n    \"signUpWithGithub\": \"Đăng ký với Github\",\n    \"signUpWithDiscord\": \"Đăng ký với Discord\",\n    \"signInWith\": \"Đăng nhập bằng:\",\n    \"signInWithEmail\": \"Đăng nhập bằng Email\",\n    \"signInWithMagicLink\": \"Tiếp tục\",\n    \"signUpWithMagicLink\": \"Đăng ký với Magic Link\",\n    \"pleaseInputYourEmail\": \"Vui lòng nhập địa chỉ email của bạn\",\n    \"settings\": \"Cài đặt\",\n    \"magicLinkSent\": \"Đã gửi Magic Link!\",\n    \"invalidEmail\": \"Vui lòng nhập địa chỉ email hợp lệ\",\n    \"alreadyHaveAnAccount\": \"Bạn đã có tài khoản?\",\n    \"logIn\": \"Đăng nhập\",\n    \"generalError\": \"Có gì đó không ổn. Vui lòng thử lại sau\",\n    \"limitRateError\": \"Vì lý do bảo mật, bạn chỉ có thể yêu cầu Magic Link sau mỗi 60 giây\",\n    \"magicLinkSentDescription\": \"Một Magic Link đã được gửi đến email của bạn. Nhấp vào liên kết để hoàn tất đăng nhập. Liên kết sẽ hết hạn sau 5 phút.\",\n    \"anonymous\": \"Ẩn danh\",\n    \"LogInWithGoogle\": \"Đăng nhập bằng Google\",\n    \"LogInWithGithub\": \"Đăng nhập bằng Github\",\n    \"LogInWithDiscord\": \"Đăng nhập bằng Discord\",\n    \"loginAsGuestButtonText\": \"Đăng nhập với chế độ khách\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"Chọn không gian làm việc của bạn\",\n    \"create\": \"Tạo không gian làm việc\",\n    \"reset\": \"Đặt lại không gian làm việc\",\n    \"renameWorkspace\": \"Đổi tên không gian làm việc\",\n    \"resetWorkspacePrompt\": \"Đặt lại không gian làm việc sẽ xóa tất cả các trang và dữ liệu trong đó. Bạn có chắc chắn muốn đặt lại không gian làm việc? Ngoài ra, bạn có thể liên hệ với nhóm hỗ trợ để khôi phục không gian làm việc\",\n    \"hint\": \"không gian làm việc\",\n    \"notFoundError\": \"Không tìm thấy không gian làm việc\",\n    \"failedToLoad\": \"Đã xảy ra lỗi! Không tải được không gian làm việc. Hãy thử đóng mọi phiên bản đang mở của @:appName và thử lại.\",\n    \"errorActions\": {\n      \"reportIssue\": \"Báo cáo một vấn đề\",\n      \"reportIssueOnGithub\": \"Báo cáo sự cố trên Github\",\n      \"exportLogFiles\": \"Xuất tệp nhật ký\",\n      \"reachOut\": \"Báo cáo trên Discord\"\n    },\n    \"menuTitle\": \"Không gian làm việc\",\n    \"deleteWorkspaceHintText\": \"Bạn có chắc chắn muốn xóa không gian làm việc không? Hành động này không thể được hoàn tác.\",\n    \"createSuccess\": \"Không gian làm việc được tạo thành công\",\n    \"createFailed\": \"Không tạo được không gian làm việc\",\n    \"createLimitExceeded\": \"Bạn đã đạt đến giới hạn không gian làm việc tối đa được phép cho tài khoản của mình. Nếu bạn cần thêm không gian làm việc để tiếp tục công việc của mình, vui lòng yêu cầu trên Github\",\n    \"deleteSuccess\": \"Đã xóa thành công không gian làm việc\",\n    \"deleteFailed\": \"Không thể xóa không gian làm việc\",\n    \"openSuccess\": \"Mở không gian làm việc thành công\",\n    \"openFailed\": \"Không thể mở không gian làm việc\",\n    \"renameSuccess\": \"Đã đổi tên không gian làm việc thành công\",\n    \"renameFailed\": \"Không đổi tên được không gian làm việc\",\n    \"updateIconSuccess\": \"Đã cập nhật biểu tượng không gian làm việc thành công\",\n    \"updateIconFailed\": \"Biểu tượng không gian làm việc không được cập nhật thành công\",\n    \"cannotDeleteTheOnlyWorkspace\": \"Không thể xóa không gian làm việc duy nhất\",\n    \"fetchWorkspacesFailed\": \"Không thể lấy được không gian làm việc\",\n    \"leaveCurrentWorkspace\": \"Rời khỏi không gian làm việc\",\n    \"leaveCurrentWorkspacePrompt\": \"Bạn có chắc chắn muốn rời khỏi không gian làm việc hiện tại không?\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"Chia sẻ\",\n    \"workInProgress\": \"Sắp ra mắt\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"Sao chép vào clipboard\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"Sao chép đường dẫn\",\n    \"publishToTheWeb\": \"Xuất bản lên Web\",\n    \"publishToTheWebHint\": \"Tạo trang web với AppFlowy\",\n    \"publish\": \"Xuất bản\",\n    \"unPublish\": \"Hủy xuất bản\",\n    \"visitSite\": \"Truy cập trang web\",\n    \"exportAsTab\": \"Xuất khẩu dưới dạng\",\n    \"publishTab\": \"Xuất bản\",\n    \"shareTab\": \"Chia sẻ\"\n  },\n  \"moreAction\": {\n    \"small\": \"nhỏ\",\n    \"medium\": \"trung bình\",\n    \"large\": \"lớn\",\n    \"fontSize\": \"Cỡ chữ\",\n    \"import\": \"Nhập\",\n    \"moreOptions\": \"Lựa chọn khác\",\n    \"wordCount\": \"Số từ: {}\",\n    \"charCount\": \"Số ký tự: {}\",\n    \"createdAt\": \"Tạo: {}\",\n    \"deleteView\": \"Xóa\",\n    \"duplicateView\": \"Nhân bản\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"Văn bản &amp; Markdown\",\n    \"documentFromV010\": \"Tài liệu từ v0.1.0\",\n    \"databaseFromV010\": \"Cơ sở dữ liệu từ v0.1.0\",\n    \"csv\": \"CSV\",\n    \"database\": \"Cơ sở dữ liệu\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"Đổi tên\",\n    \"delete\": \"Xóa\",\n    \"duplicate\": \"Nhân bản\",\n    \"unfavorite\": \"Xoá khỏi mục ưa thích\",\n    \"favorite\": \"Thêm vào mục yêu thích\",\n    \"openNewTab\": \"Mở trong tab mới\",\n    \"moveTo\": \"Chuyển tới\",\n    \"addToFavorites\": \"Thêm vào mục yêu thích\",\n    \"copyLink\": \"Sao chép đường dẫn\",\n    \"changeIcon\": \"Thay đổi biểu tượng\",\n    \"collapseAllPages\": \"Thu gọn tất cả các trang con\"\n  },\n  \"blankPageTitle\": \"Trang trống\",\n  \"newPageText\": \"Trang mới\",\n  \"newDocumentText\": \"Tài liệu mới\",\n  \"newGridText\": \"Lưới mới\",\n  \"newCalendarText\": \"Lịch mới\",\n  \"newBoardText\": \"Bảng mới\",\n  \"chat\": {\n    \"newChat\": \"AI Chat\",\n    \"inputMessageHint\": \"Hỏi @:appName AI\",\n    \"inputLocalAIMessageHint\": \"Hỏi @:appName AI cục bộ\",\n    \"unsupportedCloudPrompt\": \"Tính năng này chỉ khả dụng khi sử dụng @:appName Cloud\",\n    \"relatedQuestion\": \"Có liên quan\",\n    \"serverUnavailable\": \"Dịch vụ tạm thời không khả dụng. Vui lòng thử lại sau.\",\n    \"aiServerUnavailable\": \"🌈 Ồ không! 🌈. Một con kỳ lân đã ăn mất câu trả lời của chúng tôi. Vui lòng thử lại!\",\n    \"clickToRetry\": \"Nhấp để thử lại\",\n    \"regenerateAnswer\": \"Tạo lại\",\n    \"question1\": \"Cách sử dụng Kanban để quản lý nhiệm vụ\",\n    \"question2\": \"Giải thích phương pháp GTD\",\n    \"question3\": \"Tại sao sử dụng Rust\",\n    \"question4\": \"Công thức với những gì đang có\",\n    \"aiMistakePrompt\": \"AI có thể mắc lỗi. Hãy kiểm tra thông tin quan trọng.\",\n    \"chatWithFilePrompt\": \"Bạn có muốn trò chuyện với tập tin không?\",\n    \"indexFileSuccess\": \"Đang lập chỉ mục tệp thành công\",\n    \"inputActionNoPages\": \"Không có kết quả trang\",\n    \"referenceSource\": {\n      \"zero\": \"0 nguồn được tìm thấy\",\n      \"one\": \"{count} nguồn đã tìm thấy\",\n      \"other\": \"{count} nguồn được tìm thấy\"\n    },\n    \"clickToMention\": \"Nhấp để đề cập đến một trang\",\n    \"uploadFile\": \"Tải lên các tệp PDF, md hoặc txt để trò chuyện\",\n    \"questionDetail\": \"Xin chào {}! Tôi có thể giúp gì cho bạn hôm nay?\",\n    \"indexingFile\": \"Đang lập chỉ mục {}\"\n  },\n  \"trash\": {\n    \"text\": \"Thùng rác\",\n    \"restoreAll\": \"Khôi phục lại tất cả\",\n    \"deleteAll\": \"Xóa tất cả \",\n    \"pageHeader\": {\n      \"fileName\": \"Tên tập tin\",\n      \"lastModified\": \"Sửa đổi lần cuối\",\n      \"created\": \"Đã tạo\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"Bạn có chắc chắn xóa tất cả các trang trong Thùng rác không?\",\n      \"caption\": \"Hành động này không thể được hoàn tác.\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"Bạn có chắc chắn khôi phục tất cả các trang trong Thùng rác không?\",\n      \"caption\": \"Hành động này không thể được hoàn tác.\"\n    },\n    \"mobile\": {\n      \"actions\": \"Hành động Thùng rác\",\n      \"empty\": \"Thùng rác rỗng\",\n      \"emptyDescription\": \"Bạn không có tập tin nào bị xóa\",\n      \"isDeleted\": \"đã bị xóa\",\n      \"isRestored\": \"đã được phục hồi\"\n    },\n    \"confirmDeleteTitle\": \"Bạn có chắc chắn muốn xóa trang này vĩnh viễn không?\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"Trang này nằm trong Thùng rác\",\n    \"restore\": \"Khôi phục trang\",\n    \"deletePermanent\": \"Xóa vĩnh viễn\"\n  },\n  \"dialogCreatePageNameHint\": \"Tên trang\",\n  \"questionBubble\": {\n    \"shortcuts\": \"Phím tắt\",\n    \"whatsNew\": \"Có gì mới?\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"Thông tin gỡ lỗi\",\n      \"success\": \"Đã sao chép thông tin gỡ lỗi vào khay nhớ tạm!\",\n      \"fail\": \"Không thể sao chép thông tin gỡ lỗi vào khay nhớ tạm\"\n    },\n    \"feedback\": \"Nhận xét\",\n    \"help\": \"Trợ giúp &amp; Hỗ trợ\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"Xóa, đổi tên và hơn thế nữa...\",\n    \"addPageTooltip\": \"Nhanh chóng thêm một trang bên trong\",\n    \"defaultNewPageName\": \"Không tiêu đề\",\n    \"renameDialog\": \"Đổi tên\"\n  },\n  \"noPagesInside\": \"Không có trang bên trong\",\n  \"toolbar\": {\n    \"undo\": \"Hoàn tác\",\n    \"redo\": \"Làm lại\",\n    \"bold\": \"In đậm\",\n    \"italic\": \"In nghiêng\",\n    \"underline\": \"Gạch chân\",\n    \"strike\": \"Gạch ngang\",\n    \"numList\": \"Danh sách đánh số\",\n    \"bulletList\": \"Danh sách có dấu đầu dòng\",\n    \"checkList\": \"Danh mục\",\n    \"inlineCode\": \"Mã nội tuyến\",\n    \"quote\": \"Khối trích dẫn\",\n    \"header\": \"Tiêu đề\",\n    \"highlight\": \"Điểm nổi bật\",\n    \"color\": \"Màu\",\n    \"addLink\": \"Thêm liên kết\",\n    \"link\": \"Liên kết\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"Chuyển sang chế độ sáng\",\n    \"darkMode\": \"Chuyển sang chế độ tối\",\n    \"openAsPage\": \"Mở dưới dạng Trang\",\n    \"addNewRow\": \"Thêm một hàng mới\",\n    \"openMenu\": \"Bấm để mở menu\",\n    \"dragRow\": \"Nhấn và giữ để sắp xếp lại hàng\",\n    \"viewDataBase\": \"Xem cơ sở dữ liệu\",\n    \"referencePage\": \"{name} này được tham chiếu\",\n    \"addBlockBelow\": \"Thêm một khối bên dưới\",\n    \"aiGenerate\": \"Tạo\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"Đóng thanh bên\",\n    \"openSidebar\": \"Thanh bên mở\",\n    \"personal\": \"Riêng tư\",\n    \"private\": \"Riêng tư\",\n    \"workspace\": \"Không gian làm việc\",\n    \"favorites\": \"Yêu thích\",\n    \"clickToHidePrivate\": \"Nhấp để ẩn không gian riêng tư\\nCác trang bạn tạo ở đây chỉ hiển thị với bạn\",\n    \"clickToHideWorkspace\": \"Nhấp để ẩn không gian làm việc\\nCác trang bạn tạo ở đây có thể được mọi thành viên nhìn thấy\",\n    \"clickToHidePersonal\": \"Bấm để ẩn mục riêng tư\",\n    \"clickToHideFavorites\": \"Bấm để ẩn mục yêu thích\",\n    \"addAPage\": \"Thêm một trang\",\n    \"addAPageToPrivate\": \"Thêm một trang vào không gian riêng tư\",\n    \"addAPageToWorkspace\": \"Thêm một trang vào không gian làm việc\",\n    \"recent\": \"Gần đây\",\n    \"today\": \"Hôm nay\",\n    \"thisWeek\": \"Tuần này\",\n    \"others\": \"Yêu thích trước đó\",\n    \"justNow\": \"ngay bây giờ\",\n    \"minutesAgo\": \"{count} phút trước\",\n    \"lastViewed\": \"Đã xem lần cuối\",\n    \"favoriteAt\": \"Đã yêu thích\",\n    \"emptyRecent\": \"Không có tài liệu gần đây\",\n    \"emptyRecentDescription\": \"Khi bạn xem tài liệu, chúng sẽ xuất hiện ở đây để dễ dàng truy xuất\",\n    \"emptyFavorite\": \"Không có tài liệu yêu thích\",\n    \"emptyFavoriteDescription\": \"Bắt đầu khám phá và đánh dấu tài liệu là mục yêu thích. Chúng sẽ được liệt kê ở đây để truy cập nhanh!\",\n    \"removePageFromRecent\": \"Xóa trang này khỏi mục Gần đây?\",\n    \"removeSuccess\": \"Đã xóa thành công\",\n    \"favoriteSpace\": \"Yêu thích\",\n    \"RecentSpace\": \"Gần đây\",\n    \"Spaces\": \"Khoảng cách\",\n    \"upgradeToPro\": \"Nâng cấp lên Pro\",\n    \"upgradeToAIMax\": \"Mở khóa AI không giới hạn\",\n    \"storageLimitDialogTitle\": \"Bạn đã hết dung lượng lưu trữ miễn phí. Nâng cấp để mở khóa dung lượng lưu trữ không giới hạn\",\n    \"storageLimitDialogTitleIOS\": \"Bạn đã hết dung lượng lưu trữ miễn phí.\",\n    \"aiResponseLimitTitle\": \"Bạn đã hết phản hồi AI miễn phí. Nâng cấp lên Gói Pro hoặc mua tiện ích bổ sung AI để mở khóa phản hồi không giới hạn\",\n    \"aiResponseLimitDialogTitle\": \"Đã đạt đến giới hạn sử dụng AI\",\n    \"aiResponseLimit\": \"Bạn đã hết lượt dùng AI miễn phí.\\nVào Cài đặt -> Gói đăng ký -> Nhấp vào AI Max hoặc Gói Pro để có thêm lượt dùng AI\",\n    \"askOwnerToUpgradeToPro\": \"Không gian làm việc của bạn sắp hết dung lượng lưu trữ miễn phí. Vui lòng yêu cầu chủ sở hữu không gian làm việc của bạn nâng cấp lên Gói Pro\",\n    \"askOwnerToUpgradeToProIOS\": \"Không gian làm việc của bạn sắp hết dung lượng lưu trữ miễn phí.\",\n    \"askOwnerToUpgradeToAIMax\": \"Không gian làm việc của bạn sắp hết phản hồi AI miễn phí. Vui lòng yêu cầu chủ sở hữu không gian làm việc của bạn nâng cấp gói hoặc mua tiện ích bổ sung AI\",\n    \"askOwnerToUpgradeToAIMaxIOS\": \"Không gian làm việc của bạn sắp hết lượt sử dụng AI miễn phí.\",\n    \"purchaseStorageSpace\": \"Mua không gian lưu trữ\",\n    \"purchaseAIResponse\": \"Mua \",\n    \"askOwnerToUpgradeToLocalAI\": \"Yêu cầu chủ sở hữu không gian làm việc bật AI trên thiết bị\",\n    \"upgradeToAILocal\": \"Chạy các mô hình cục bộ trên thiết bị của bạn để có quyền riêng tư tối đa\",\n    \"upgradeToAILocalDesc\": \"Trò chuyện với PDF, cải thiện khả năng viết và tự động điền bảng bằng AI cục bộ\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"Đã xuất ghi chú sang Markdown\",\n      \"path\": \"Tài liệu/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"Danh bạ\",\n    \"whatsHappening\": \"Những gì đang xảy ra trong tuần này?\",\n    \"addContact\": \"Thêm liên hệ\",\n    \"editContact\": \"Chỉnh sửa liên hệ\"\n  },\n  \"button\": {\n    \"ok\": \"OK\",\n    \"confirm\": \"Xác nhận\",\n    \"done\": \"Xong\",\n    \"cancel\": \"Hủy\",\n    \"signIn\": \"Đăng nhập\",\n    \"signOut\": \"Đăng xuất\",\n    \"complete\": \"Hoàn thành\",\n    \"save\": \"Lưu\",\n    \"generate\": \"Tạo\",\n    \"esc\": \"ESC\",\n    \"keep\": \"Giữ\",\n    \"tryAgain\": \"Thử lại\",\n    \"discard\": \"Bỏ\",\n    \"replace\": \"Thay thế\",\n    \"insertBelow\": \"Chèn bên dưới\",\n    \"insertAbove\": \"Chèn ở trên\",\n    \"upload\": \"Tải lên\",\n    \"edit\": \"Sửa\",\n    \"delete\": \"Xoá\",\n    \"duplicate\": \"Nhân bản\",\n    \"putback\": \"Để lại\",\n    \"update\": \"Cập nhật\",\n    \"share\": \"Chia sẻ\",\n    \"removeFromFavorites\": \"Loại bỏ khỏi mục yêu thích\",\n    \"removeFromRecent\": \"Xóa khỏi gần đây\",\n    \"addToFavorites\": \"Thêm vào mục yêu thích\",\n    \"favoriteSuccessfully\": \"Đã thêm vào yêu thích\",\n    \"unfavoriteSuccessfully\": \"Đã xoá khỏi yêu thích\",\n    \"duplicateSuccessfully\": \"Đã sao chép thành công\",\n    \"rename\": \"Đổi tên\",\n    \"helpCenter\": \"Trung tâm trợ giúp\",\n    \"add\": \"Thêm\",\n    \"yes\": \"Đúng\",\n    \"no\": \"Không\",\n    \"clear\": \"Xoá\",\n    \"remove\": \"Di chuyển\",\n    \"dontRemove\": \"Không xóa\",\n    \"copyLink\": \"Sao chép đường dẫn\",\n    \"align\": \"Căn chỉnh\",\n    \"login\": \"Đăng nhập\",\n    \"logout\": \"Đăng xuất\",\n    \"deleteAccount\": \"Xóa tài khoản\",\n    \"back\": \"Quay lại\",\n    \"signInGoogle\": \"Đăng nhập bằng Google\",\n    \"signInGithub\": \"Đăng nhập bằng Github\",\n    \"signInDiscord\": \"Đăng nhập bằng Discord\",\n    \"more\": \"Hơn nữa\",\n    \"create\": \"Tạo mới\",\n    \"close\": \"Đóng\",\n    \"next\": \"Kế tiếp\",\n    \"previous\": \"Trước đó\",\n    \"submit\": \"Gửi\",\n    \"download\": \"Tải về\",\n    \"tryAGain\": \"Thử lại\"\n  },\n  \"label\": {\n    \"welcome\": \"Chào mừng!\",\n    \"firstName\": \"Tên\",\n    \"middleName\": \"Tên đệm\",\n    \"lastName\": \"Họ\",\n    \"stepX\": \"Bước {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"Không thể kết nối với tài khoản của bạn.\",\n      \"failedMsg\": \"Vui lòng đảm bảo bạn đã hoàn tất quá trình đăng nhập trong trình duyệt của mình.\"\n    },\n    \"google\": {\n      \"title\": \"ĐĂNG NHẬP GOOGLE\",\n      \"instruction1\": \"Để nhập Danh bạ Google của bạn, bạn cần cấp quyền cho ứng dụng này bằng trình duyệt web của mình.\",\n      \"instruction2\": \"Sao chép mã này vào khay nhớ tạm của bạn bằng cách nhấp vào biểu tượng hoặc chọn văn bản:\",\n      \"instruction3\": \"Điều hướng đến liên kết sau trong trình duyệt web của bạn và nhập mã ở trên:\",\n      \"instruction4\": \"Nhấn nút bên dưới khi bạn hoàn tất đăng ký:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"Cài đặt\",\n    \"popupMenuItem\": {\n      \"settings\": \"Cài đặt\",\n      \"members\": \"Thành viên\",\n      \"trash\": \"Thùng Rác\",\n      \"helpAndSupport\": \"Trợ giúp & Hỗ trợ\"\n    },\n    \"accountPage\": {\n      \"menuLabel\": \"Tài khoản của tôi\",\n      \"title\": \"Tài khoản của tôi\",\n      \"general\": {\n        \"title\": \"Tên tài khoản & ảnh đại diện\",\n        \"changeProfilePicture\": \"Thay đổi ảnh đại diện\"\n      },\n      \"email\": {\n        \"title\": \"E-mail\",\n        \"actions\": {\n          \"change\": \"Thay đổi email\"\n        }\n      },\n      \"login\": {\n        \"title\": \"Đăng nhập tài khoản\",\n        \"loginLabel\": \"Đăng nhập\",\n        \"logoutLabel\": \"Đăng xuất\"\n      }\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"Không gian làm việc\",\n      \"title\": \"Không gian làm việc\",\n      \"description\": \"Tùy chỉnh giao diện không gian làm việc, chủ đề, phông chữ, bố cục văn bản, định dạng ngày/giờ và ngôn ngữ.\",\n      \"workspaceName\": {\n        \"title\": \"Tên không gian làm việc\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"Biểu tượng không gian làm việc\",\n        \"description\": \"Tải lên hình ảnh hoặc sử dụng biểu tượng cảm xúc cho không gian làm việc của bạn. Biểu tượng sẽ hiển thị trên thanh bên và thông báo của bạn.\"\n      },\n      \"appearance\": {\n        \"title\": \"Vẻ bề ngoài\",\n        \"description\": \"Tùy chỉnh giao diện không gian làm việc, chủ đề, phông chữ, bố cục văn bản, ngày, giờ và ngôn ngữ.\",\n        \"options\": {\n          \"system\": \"Tự động\",\n          \"light\": \"Sáng\",\n          \"dark\": \"Tối\"\n        }\n      },\n      \"resetCursorColor\": {\n        \"title\": \"Đặt lại màu con trỏ tài liệu\",\n        \"description\": \"Bạn có chắc chắn muốn thiết lập lại màu con trỏ không?\"\n      },\n      \"resetSelectionColor\": {\n        \"title\": \"Đặt lại màu lựa chọn tài liệu\",\n        \"description\": \"Bạn có chắc chắn muốn thiết lập lại màu đã chọn không?\"\n      },\n      \"theme\": {\n        \"title\": \"Chủ đề\",\n        \"description\": \"Chọn chủ đề có sẵn hoặc tải lên chủ đề tùy chỉnh của riêng bạn.\",\n        \"uploadCustomThemeTooltip\": \"Tải lên một chủ đề tùy chỉnh\"\n      },\n      \"workspaceFont\": {\n        \"title\": \"Phông chữ không gian làm việc\",\n        \"noFontHint\": \"Không tìm thấy phông chữ, hãy thử thuật ngữ khác.\"\n      },\n      \"textDirection\": {\n        \"title\": \"Hướng văn bản\",\n        \"leftToRight\": \"Từ trái sang phải\",\n        \"rightToLeft\": \"Từ phải sang trái\",\n        \"auto\": \"Tự động\",\n        \"enableRTLItems\": \"Bật các mục thanh công cụ RTL\"\n      },\n      \"layoutDirection\": {\n        \"title\": \"Hướng bố trí\",\n        \"leftToRight\": \"Từ trái sang phải\",\n        \"rightToLeft\": \"Từ phải sang trái\"\n      },\n      \"dateTime\": {\n        \"title\": \"Ngày & giờ\",\n        \"example\": \"{} tại {} ({})\",\n        \"24HourTime\": \"thời gian 24 giờ\",\n        \"dateFormat\": {\n          \"label\": \"Định dạng ngày tháng\",\n          \"local\": \"Địa phương\",\n          \"us\": \"US\",\n          \"iso\": \"ISO\",\n          \"friendly\": \"Thân thiện\",\n          \"dmy\": \"D/M/Y\"\n        }\n      },\n      \"language\": {\n        \"title\": \"Ngôn ngữ\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"Xóa không gian làm việc\",\n        \"content\": \"Bạn có chắc chắn muốn xóa không gian làm việc này không? Hành động này không thể hoàn tác và bất kỳ trang nào bạn đã xuất bản sẽ bị hủy xuất bản.\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"Rời khỏi không gian làm việc\",\n        \"content\": \"Bạn có chắc chắn muốn rời khỏi không gian làm việc này không? Bạn sẽ mất quyền truy cập vào tất cả các trang và dữ liệu trong đó.\"\n      },\n      \"manageWorkspace\": {\n        \"title\": \"Quản lý không gian làm việc\",\n        \"leaveWorkspace\": \"Rời khỏi không gian làm việc\",\n        \"deleteWorkspace\": \"Xóa không gian làm việc\"\n      }\n    },\n    \"manageDataPage\": {\n      \"menuLabel\": \"Quản lý dữ liệu\",\n      \"title\": \"Quản lý dữ liệu\",\n      \"description\": \"Quản lý dữ liệu lưu trữ cục bộ hoặc Nhập dữ liệu hiện có của bạn vào @:appName .\",\n      \"dataStorage\": {\n        \"title\": \"Vị trí lưu trữ tập tin\",\n        \"tooltip\": \"Vị trí lưu trữ các tập tin của bạn\",\n        \"actions\": {\n          \"change\": \"Thay đổi đường dẫn\",\n          \"open\": \"Mở thư mục\",\n          \"openTooltip\": \"Mở vị trí thư mục dữ liệu hiện tại\",\n          \"copy\": \"Sao chép đường dẫn\",\n          \"copiedHint\": \"Đã sao chép đường dẫn!\",\n          \"resetTooltip\": \"Đặt lại về vị trí mặc định\"\n        },\n        \"resetDialog\": {\n          \"title\": \"Bạn có chắc không?\",\n          \"description\": \"Đặt lại đường dẫn đến vị trí dữ liệu mặc định sẽ không xóa dữ liệu của bạn. Nếu bạn muốn nhập lại dữ liệu hiện tại, trước tiên bạn nên sao chép đường dẫn đến vị trí hiện tại của mình.\"\n        }\n      },\n      \"importData\": {\n        \"title\": \"Nhập dữ liệu\",\n        \"tooltip\": \"Nhập dữ liệu từ các thư mục sao lưu/dữ liệu @:appName\",\n        \"description\": \"Sao chép dữ liệu từ thư mục dữ liệu @:appName bên ngoài\",\n        \"action\": \"Duyệt tập tin\"\n      },\n      \"encryption\": {\n        \"title\": \"Mã hóa\",\n        \"tooltip\": \"Quản lý cách dữ liệu của bạn được lưu trữ và mã hóa\",\n        \"descriptionNoEncryption\": \"Bật mã hóa sẽ mã hóa toàn bộ dữ liệu. Không thể hoàn tác thao tác này.\",\n        \"descriptionEncrypted\": \"Dữ liệu của bạn được mã hóa.\",\n        \"action\": \"Mã hóa dữ liệu\",\n        \"dialog\": {\n          \"title\": \"Mã hóa toàn bộ dữ liệu của bạn?\",\n          \"description\": \"Mã hóa tất cả dữ liệu của bạn sẽ giữ cho dữ liệu của bạn an toàn và bảo mật. Hành động này KHÔNG THỂ hoàn tác. Bạn có chắc chắn muốn tiếp tục không?\"\n        }\n      },\n      \"cache\": {\n        \"title\": \"Xóa bộ nhớ đệm\",\n        \"description\": \"Giúp giải quyết các vấn đề như hình ảnh không tải được, thiếu trang trong một khoảng trắng và phông chữ không tải được. Điều này sẽ không ảnh hưởng đến dữ liệu của bạn.\",\n        \"dialog\": {\n          \"title\": \"Xóa bộ nhớ đệm\",\n          \"description\": \"Giúp giải quyết các vấn đề như hình ảnh không tải được, thiếu trang trong một khoảng trắng và phông chữ không tải được. Điều này sẽ không ảnh hưởng đến dữ liệu của bạn.\",\n          \"successHint\": \"Đã xóa bộ nhớ đệm!\"\n        }\n      },\n      \"data\": {\n        \"fixYourData\": \"Sửa dữ liệu của bạn\",\n        \"fixButton\": \"Sửa\",\n        \"fixYourDataDescription\": \"Nếu bạn gặp sự cố với dữ liệu, bạn có thể thử khắc phục tại đây.\"\n      }\n    },\n    \"shortcutsPage\": {\n      \"menuLabel\": \"Phím tắt\",\n      \"title\": \"Phím tắt\",\n      \"editBindingHint\": \"Nhập phím tắt mới\",\n      \"searchHint\": \"Tìm kiếm\",\n      \"actions\": {\n        \"resetDefault\": \"Đặt lại mặc định\"\n      },\n      \"errorPage\": {\n        \"message\": \"Không tải được phím tắt: {}\",\n        \"howToFix\": \"Vui lòng thử lại. Nếu sự cố vẫn tiếp diễn, vui lòng liên hệ trên GitHub.\"\n      },\n      \"resetDialog\": {\n        \"title\": \"Đặt lại phím tắt\",\n        \"description\": \"Thao tác này sẽ khôi phục tất cả các phím tắt của bạn về mặc định, bạn không thể hoàn tác thao tác này sau đó, bạn có chắc chắn muốn tiếp tục không?\",\n        \"buttonLabel\": \"Cài lại\"\n      },\n      \"conflictDialog\": {\n        \"title\": \"{} hiện đang được sử dụng\",\n        \"descriptionPrefix\": \"Phím tắt này hiện đang được sử dụng bởi \",\n        \"descriptionSuffix\": \". Nếu bạn thay thế phím tắt này, nó sẽ bị xóa khỏi {}.\",\n        \"confirmLabel\": \"Tiếp tục\"\n      },\n      \"editTooltip\": \"Nhấn để bắt đầu chỉnh sửa phím tắt\",\n      \"keybindings\": {\n        \"toggleToDoList\": \"Chuyển sang danh sách việc cần làm\",\n        \"insertNewParagraphInCodeblock\": \"Chèn đoạn văn mới\",\n        \"pasteInCodeblock\": \"Dán vào codeblock\",\n        \"selectAllCodeblock\": \"Chọn tất cả\",\n        \"indentLineCodeblock\": \"Chèn hai khoảng trắng vào đầu dòng\",\n        \"outdentLineCodeblock\": \"Xóa hai khoảng trắng ở đầu dòng\",\n        \"twoSpacesCursorCodeblock\": \"Chèn hai khoảng trắng vào con trỏ\",\n        \"copy\": \"Sao chép lựa chọn\",\n        \"paste\": \"Dán vào nội dung\",\n        \"cut\": \"Cắt lựa chọn\",\n        \"alignLeft\": \"Căn chỉnh văn bản sang trái\",\n        \"alignCenter\": \"Căn giữa văn bản\",\n        \"alignRight\": \"Căn chỉnh văn bản bên phải\",\n        \"undo\": \"Hoàn tác\",\n        \"redo\": \"Làm lại\",\n        \"convertToParagraph\": \"Chuyển đổi khối thành đoạn văn\",\n        \"backspace\": \"Xóa bỏ\",\n        \"deleteLeftWord\": \"Xóa từ bên trái\",\n        \"deleteLeftSentence\": \"Xóa câu bên trái\",\n        \"delete\": \"Xóa ký tự bên phải\",\n        \"deleteMacOS\": \"Xóa ký tự bên trái\",\n        \"deleteRightWord\": \"Xóa từ bên phải\",\n        \"moveCursorLeft\": \"Di chuyển con trỏ sang trái\",\n        \"moveCursorBeginning\": \"Di chuyển con trỏ đến đầu\",\n        \"moveCursorLeftWord\": \"Di chuyển con trỏ sang trái một từ\",\n        \"moveCursorLeftSelect\": \"Chọn và di chuyển con trỏ sang trái\",\n        \"moveCursorBeginSelect\": \"Chọn và di chuyển con trỏ đến đầu\",\n        \"moveCursorLeftWordSelect\": \"Chọn và di chuyển con trỏ sang trái một từ\",\n        \"moveCursorRight\": \"Di chuyển con trỏ sang phải\",\n        \"moveCursorEnd\": \"Di chuyển con trỏ đến cuối\",\n        \"moveCursorRightWord\": \"Di chuyển con trỏ sang phải một từ\",\n        \"moveCursorRightSelect\": \"Chọn và di chuyển con trỏ sang phải một\",\n        \"moveCursorEndSelect\": \"Chọn và di chuyển con trỏ đến cuối\",\n        \"moveCursorRightWordSelect\": \"Chọn và di chuyển con trỏ sang phải một từ\",\n        \"moveCursorUp\": \"Di chuyển con trỏ lên\",\n        \"moveCursorTopSelect\": \"Chọn và di chuyển con trỏ lên trên cùng\",\n        \"moveCursorTop\": \"Di chuyển con trỏ lên trên cùng\",\n        \"moveCursorUpSelect\": \"Chọn và di chuyển con trỏ lên\",\n        \"moveCursorBottomSelect\": \"Chọn và di chuyển con trỏ xuống dưới cùng\",\n        \"moveCursorBottom\": \"Di chuyển con trỏ xuống dưới cùng\",\n        \"moveCursorDown\": \"Di chuyển con trỏ xuống\",\n        \"moveCursorDownSelect\": \"Chọn và di chuyển con trỏ xuống\",\n        \"home\": \"Cuộn lên đầu trang\",\n        \"end\": \"Cuộn xuống dưới cùng\",\n        \"toggleBold\": \"Chuyển đổi chữ đậm\",\n        \"toggleItalic\": \"Chuyển đổi nghiêng\",\n        \"toggleUnderline\": \"Chuyển đổi gạch chân\",\n        \"toggleStrikethrough\": \"Chuyển đổi gạch ngang\",\n        \"toggleCode\": \"Chuyển đổi mã trong dòng\",\n        \"toggleHighlight\": \"Chuyển đổi nổi bật\",\n        \"showLinkMenu\": \"Hiển thị menu liên kết\",\n        \"openInlineLink\": \"Mở liên kết nội tuyến\",\n        \"openLinks\": \"Mở tất cả các liên kết đã chọn\",\n        \"indent\": \"thụt lề\",\n        \"outdent\": \"Nhô ra ngoài\",\n        \"exit\": \"Thoát khỏi chỉnh sửa\",\n        \"pageUp\": \"Cuộn lên một trang\",\n        \"pageDown\": \"Cuộn xuống một trang\",\n        \"selectAll\": \"Chọn tất cả\",\n        \"pasteWithoutFormatting\": \"Dán nội dung không định dạng\",\n        \"showEmojiPicker\": \"Hiển thị bộ chọn biểu tượng cảm xúc\",\n        \"enterInTableCell\": \"Thêm ngắt dòng trong bảng\",\n        \"leftInTableCell\": \"Di chuyển sang trái một ô trong bảng\",\n        \"rightInTableCell\": \"Di chuyển sang phải một ô trong bảng\",\n        \"upInTableCell\": \"Di chuyển lên một ô trong bảng\",\n        \"downInTableCell\": \"Di chuyển xuống một ô trong bảng\",\n        \"tabInTableCell\": \"Đi tới ô có sẵn tiếp theo trong bảng\",\n        \"shiftTabInTableCell\": \"Đi đến ô có sẵn trước đó trong bảng\",\n        \"backSpaceInTableCell\": \"Dừng lại ở đầu ô\"\n      },\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"Chèn một đoạn văn mới bên cạnh khối mã\",\n        \"codeBlockIndentLines\": \"Chèn hai khoảng trắng vào đầu dòng trong khối mã\",\n        \"codeBlockOutdentLines\": \"Xóa hai khoảng trắng ở đầu dòng trong khối mã\",\n        \"codeBlockAddTwoSpaces\": \"Chèn hai khoảng trắng vào vị trí con trỏ trong khối mã\",\n        \"codeBlockSelectAll\": \"Chọn tất cả nội dung bên trong một khối mã\",\n        \"codeBlockPasteText\": \"Dán văn bản vào codeblock\",\n        \"textAlignLeft\": \"Căn chỉnh văn bản sang trái\",\n        \"textAlignCenter\": \"Căn chỉnh văn bản vào giữa\",\n        \"textAlignRight\": \"Căn chỉnh văn bản sang phải\"\n      },\n      \"couldNotLoadErrorMsg\": \"Không thể tải phím tắt, hãy thử lại\",\n      \"couldNotSaveErrorMsg\": \"Không thể lưu phím tắt, hãy thử lại\"\n    },\n    \"aiPage\": {\n      \"title\": \"Cài đặt AI\",\n      \"menuLabel\": \"Cài đặt AI\",\n      \"keys\": {\n        \"enableAISearchTitle\": \"Tìm kiếm AI\",\n        \"aiSettingsDescription\": \"Chọn mô hình ưa thích của bạn để hỗ trợ AppFlowy AI. Bây giờ bao gồm GPT 4-o, Claude 3,5, Llama 3.1 và Mistral 7B\",\n        \"loginToEnableAIFeature\": \"Các tính năng AI chỉ được bật sau khi đăng nhập bằng @:appName Cloud. Nếu bạn không có tài khoản @:appName , hãy vào 'Tài khoản của tôi' để đăng ký\",\n        \"llmModel\": \"Mô hình ngôn ngữ\",\n        \"llmModelType\": \"Kiểu mô hình ngôn ngữ\",\n        \"downloadLLMPrompt\": \"Tải xuống {}\",\n        \"downloadAppFlowyOfflineAI\": \"Tải xuống gói AI ngoại tuyến sẽ cho phép AI chạy trên thiết bị của bạn. Bạn có muốn tiếp tục không?\",\n        \"downloadLLMPromptDetail\": \"Tải xuống mô hình cục bộ {} sẽ chiếm tới {} dung lượng lưu trữ. Bạn có muốn tiếp tục không?\",\n        \"downloadBigFilePrompt\": \"Có thể mất khoảng 10 phút để hoàn tất việc tải xuống\",\n        \"downloadAIModelButton\": \"Tải về\",\n        \"downloadingModel\": \"Đang tải xuống\",\n        \"localAILoaded\": \"Mô hình AI cục bộ đã được thêm thành công và sẵn sàng sử dụng\",\n        \"localAIStart\": \"Trò chuyện AI cục bộ đang bắt đầu...\",\n        \"localAILoading\": \"Mô hình trò chuyện AI cục bộ đang tải...\",\n        \"localAIStopped\": \"AI cục bộ đã dừng\",\n        \"failToLoadLocalAI\": \"Không thể khởi động AI cục bộ\",\n        \"restartLocalAI\": \"Khởi động lại AI cục bộ\",\n        \"disableLocalAITitle\": \"Vô hiệu hóa AI cục bộ\",\n        \"disableLocalAIDescription\": \"Bạn có muốn tắt AI cục bộ không?\",\n        \"localAIToggleTitle\": \"Chuyển đổi để bật hoặc tắt AI cục bộ\",\n        \"offlineAIInstruction1\": \"Theo dõi\",\n        \"offlineAIInstruction2\": \"chỉ dẫn\",\n        \"offlineAIInstruction3\": \"để kích hoạt AI ngoại tuyến.\",\n        \"offlineAIDownload1\": \"Nếu bạn chưa tải xuống AppFlowy AI, vui lòng\",\n        \"offlineAIDownload2\": \"tải về\",\n        \"offlineAIDownload3\": \"nó đầu tiên\",\n        \"activeOfflineAI\": \"Tích cực\",\n        \"downloadOfflineAI\": \"Tải về\",\n        \"openModelDirectory\": \"Mở thư mục\"\n      }\n    },\n    \"planPage\": {\n      \"menuLabel\": \"Kế hoạch\",\n      \"title\": \"Giá gói\",\n      \"planUsage\": {\n        \"title\": \"Tóm tắt sử dụng kế hoạch\",\n        \"storageLabel\": \"Kho\",\n        \"storageUsage\": \"{} của {} GB\",\n        \"unlimitedStorageLabel\": \"Lưu trữ không giới hạn\",\n        \"collaboratorsLabel\": \"Thành viên\",\n        \"collaboratorsUsage\": \"{} của {}\",\n        \"aiResponseLabel\": \"Phản hồi của AI\",\n        \"aiResponseUsage\": \"{} của {}\",\n        \"unlimitedAILabel\": \"Phản hồi không giới hạn\",\n        \"proBadge\": \"Chuyên nghiệp\",\n        \"aiMaxBadge\": \"AI Tối đa\",\n        \"aiOnDeviceBadge\": \"AI trên thiết bị dành cho máy Mac\",\n        \"memberProToggle\": \"Thêm thành viên và AI không giới hạn\",\n        \"aiMaxToggle\": \"AI không giới hạn và quyền truy cập vào các mô hình tiên tiến\",\n        \"aiOnDeviceToggle\": \"AI cục bộ cho sự riêng tư tối đa\",\n        \"aiCredit\": {\n          \"title\": \"Thêm @:appName Tín dụng AI\",\n          \"price\": \"{}\",\n          \"priceDescription\": \"cho 1.000 tín dụng\",\n          \"purchase\": \"Mua AI\",\n          \"info\": \"Thêm 1.000 tín dụng Ai cho mỗi không gian làm việc và tích hợp AI tùy chỉnh vào quy trình làm việc của bạn một cách liền mạch để có kết quả thông minh hơn, nhanh hơn với tối đa:\",\n          \"infoItemOne\": \"10.000 phản hồi cho mỗi cơ sở dữ liệu\",\n          \"infoItemTwo\": \"1.000 phản hồi cho mỗi không gian làm việc\"\n        },\n        \"currentPlan\": {\n          \"bannerLabel\": \"Gói hiện tại\",\n          \"freeTitle\": \"Miễn phí\",\n          \"proTitle\": \"Pro\",\n          \"teamTitle\": \"Nhóm\",\n          \"freeInfo\": \"Hoàn hảo cho cá nhân có tối đa 2 thành viên để sắp xếp mọi thứ\",\n          \"proInfo\": \"Hoàn hảo cho các nhóm vừa và nhỏ có tối đa 10 thành viên.\",\n          \"teamInfo\": \"Hoàn hảo cho tất cả các nhóm làm việc hiệu quả và có tổ chức tốt.\",\n          \"upgrade\": \"Thay đổi gói đăng ký\",\n          \"canceledInfo\": \"Gói đăng ký của bạn đã bị hủy, bạn sẽ được hạ cấp xuống gói Miễn phí vào ngày {}.\"\n        },\n        \"addons\": {\n          \"title\": \"Tiện ích bổ sung\",\n          \"addLabel\": \"Thêm vào\",\n          \"activeLabel\": \"Đã thêm\",\n          \"aiMax\": {\n            \"title\": \"AI Max\",\n            \"description\": \"Phản hồi AI không giới hạn được hỗ trợ bởi GPT-4o, Claude 3.5 Sonnet và nhiều hơn nữa\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"cho mỗi người dùng mỗi tháng được thanh toán hàng năm\"\n          },\n          \"aiOnDevice\": {\n            \"title\": \"AI trên thiết bị dành cho máy Mac\",\n            \"description\": \"Chạy Mistral 7B, LLAMA 3 và nhiều mô hình cục bộ khác trên máy của bạn\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"cho mỗi người dùng mỗi tháng được thanh toán hàng năm\",\n            \"recommend\": \"Khuyến nghị M1 hoặc mới hơn\"\n          }\n        },\n        \"deal\": {\n          \"bannerLabel\": \"Khuyến mãi năm mới!\",\n          \"title\": \"Phát triển nhóm của bạn!\",\n          \"info\": \"Nâng cấp và tiết kiệm 10% cho gói Pro và Team! Tăng năng suất làm việc của bạn với các tính năng mới mạnh mẽ bao gồm @:appName AI.\",\n          \"viewPlans\": \"Xem các gói đăng ký\"\n        }\n      }\n    },\n    \"billingPage\": {\n      \"menuLabel\": \"Thanh toán\",\n      \"title\": \"Thanh toán\",\n      \"plan\": {\n        \"title\": \"Gói đăng ký\",\n        \"freeLabel\": \"Miễn phí\",\n        \"proLabel\": \"Pro\",\n        \"planButtonLabel\": \"Thay đổi gói đăng ký\",\n        \"billingPeriod\": \"Chu kỳ thanh toán\",\n        \"periodButtonLabel\": \"Chỉnh sửa chu kỳ\"\n      },\n      \"paymentDetails\": {\n        \"title\": \"Chi tiết thanh toán\",\n        \"methodLabel\": \"Phương thức thanh toán\",\n        \"methodButtonLabel\": \"Phương pháp chỉnh sửa\"\n      },\n      \"addons\": {\n        \"title\": \"Tiện ích bổ sung\",\n        \"addLabel\": \"Thêm vào\",\n        \"removeLabel\": \"Xoá\",\n        \"renewLabel\": \"Làm mới\",\n        \"aiMax\": {\n          \"label\": \"AI Max\",\n          \"description\": \"Mở khóa AI không giới hạn và các mô hình tiên tiến\",\n          \"activeDescription\": \"Hóa đơn tiếp theo phải trả vào ngày {}\",\n          \"canceledDescription\": \"AI Max sẽ có sẵn cho đến {}\"\n        },\n        \"aiOnDevice\": {\n          \"label\": \"AI trên thiết bị dành cho máy Mac\",\n          \"description\": \"Mở khóa AI không giới hạn trên thiết bị của bạn\",\n          \"activeDescription\": \"Hóa đơn tiếp theo phải trả vào ngày {}\",\n          \"canceledDescription\": \"AI On-device dành cho Mac sẽ khả dụng cho đến {}\"\n        },\n        \"removeDialog\": {\n          \"title\": \"Xoá {}\",\n          \"description\": \"Bạn có chắc chắn muốn xóa {plan} không? Bạn sẽ mất quyền truy cập vào các tính năng và lợi ích của {plan} ngay lập tức.\"\n        }\n      },\n      \"currentPeriodBadge\": \"HIỆN TẠI\",\n      \"changePeriod\": \"Thay đổi chu kỳ\",\n      \"planPeriod\": \"{} chu kỳ\",\n      \"monthlyInterval\": \"Hàng tháng\",\n      \"monthlyPriceInfo\": \"mỗi thành viên được thanh toán hàng tháng\",\n      \"annualInterval\": \"Hàng năm\",\n      \"annualPriceInfo\": \"mỗi thành viên được thanh toán hàng năm\"\n    },\n    \"comparePlanDialog\": {\n      \"title\": \"So sánh và lựa chọn gói đăng ký\",\n      \"planFeatures\": \"Gói đăng ký\\nTính năng\",\n      \"current\": \"Hiện tại\",\n      \"actions\": {\n        \"upgrade\": \"Nâng cấp\",\n        \"downgrade\": \"Hạ cấp\",\n        \"current\": \"Hiện tại\"\n      },\n      \"freePlan\": {\n        \"title\": \"Miễn phí\",\n        \"description\": \"Dành cho cá nhân có tối đa 2 thành viên để tổ chức mọi thứ\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"miễn phí mãi mãi\"\n      },\n      \"proPlan\": {\n        \"title\": \"Pro\",\n        \"description\": \"Dành cho các nhóm nhỏ để quản lý dự án và kiến thức của nhóm\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"cho mỗi người dùng mỗi tháng\\nđược thanh toán hàng năm\\n\\n{} được thanh toán hàng tháng\"\n      },\n      \"planLabels\": {\n        \"itemOne\": \"Không gian làm việc\",\n        \"itemTwo\": \"Thành viên\",\n        \"itemThree\": \"Kho\",\n        \"itemFour\": \"Chỉnh sửa thời gian thực\",\n        \"itemFive\": \"Ứng dụng di động\",\n        \"itemSix\": \"Phản hồi của AI\",\n        \"itemFileUpload\": \"Tải tập tin lên\",\n        \"tooltipSix\": \"Trọn đời có nghĩa là số lượng phản hồi không bao giờ được thiết lập lại\",\n        \"intelligentSearch\": \"Tìm kiếm thông minh\",\n        \"tooltipSeven\": \"Cho phép bạn tùy chỉnh một phần URL cho không gian làm việc của bạn\"\n      },\n      \"freeLabels\": {\n        \"itemOne\": \"tính phí theo không gian làm việc\",\n        \"itemTwo\": \"lên đến 2\",\n        \"itemThree\": \"5 GB\",\n        \"itemFour\": \"Đúng\",\n        \"itemFive\": \"Đúng\",\n        \"itemSix\": \"10 trọn đời\",\n        \"itemFileUpload\": \"Lên đến 7 MB\",\n        \"intelligentSearch\": \"Tìm kiếm thông minh\"\n      },\n      \"proLabels\": {\n        \"itemOne\": \"tính phí theo không gian làm việc\",\n        \"itemTwo\": \"lên đến 10\",\n        \"itemThree\": \"không giới hạn\",\n        \"itemFour\": \"Đúng\",\n        \"itemFive\": \"Đúng\",\n        \"itemSix\": \"không giới hạn\",\n        \"itemFileUpload\": \"Không giới hạn\",\n        \"intelligentSearch\": \"Tìm kiếm thông minh\"\n      },\n      \"paymentSuccess\": {\n        \"title\": \"Bạn hiện đang sử dụng gói {}!\",\n        \"description\": \"Thanh toán của bạn đã được xử lý thành công và gói của bạn đã được nâng cấp lên @:appName {}. Bạn có thể xem chi tiết gói đăng ký của mình trên trang Gói đăng ký\"\n      },\n      \"downgradeDialog\": {\n        \"title\": \"Bạn có chắc chắn muốn hạ cấp gói đăng ký của mình không?\",\n        \"description\": \"Việc hạ cấp gói đăng ký của bạn sẽ đưa bạn trở lại gói Miễn phí. Các thành viên có thể mất quyền truy cập vào không gian làm việc này và bạn có thể cần giải phóng dung lượng để đáp ứng giới hạn lưu trữ của gói Miễn phí.\",\n        \"downgradeLabel\": \"Hạ cấp gói đăng ký\"\n      }\n    },\n    \"cancelSurveyDialog\": {\n      \"title\": \"Rất tiếc khi phải tạm biệt bạn\",\n      \"description\": \"Chúng tôi rất tiếc khi phải tạm biệt bạn. Chúng tôi rất muốn nghe phản hồi của bạn để giúp chúng tôi cải thiện @:appName . Vui lòng dành chút thời gian để trả lời một vài câu hỏi.\",\n      \"commonOther\": \"Khác\",\n      \"otherHint\": \"Viết câu trả lời của bạn ở đây\",\n      \"questionOne\": {\n        \"question\": \"Điều gì khiến bạn hủy đăng ký @:appName Pro?\",\n        \"answerOne\": \"Chi phí quá cao\",\n        \"answerTwo\": \"Các tính năng không đáp ứng được kỳ vọng\",\n        \"answerThree\": \"Đã tìm thấy một giải pháp thay thế tốt hơn\",\n        \"answerFour\": \"Không sử dụng đủ để bù đắp chi phí\",\n        \"answerFive\": \"Vấn đề dịch vụ hoặc khó khăn kỹ thuật\"\n      },\n      \"questionTwo\": {\n        \"question\": \"Bạn có khả năng cân nhắc đăng ký lại @:appName Pro trong tương lai không?\",\n        \"answerOne\": \"Rất có thể\",\n        \"answerTwo\": \"Có khả năng\",\n        \"answerThree\": \"Không chắc chắn\",\n        \"answerFour\": \"Không có khả năng\",\n        \"answerFive\": \"Rất không có khả năng\"\n      },\n      \"questionThree\": {\n        \"question\": \"Bạn đánh giá cao tính năng Pro nào nhất trong suốt thời gian đăng ký?\",\n        \"answerOne\": \"Sự hợp tác của nhiều người dùng\",\n        \"answerTwo\": \"Lịch sử phiên bản thời gian dài hơn\",\n        \"answerThree\": \"Phản hồi AI không giới hạn\",\n        \"answerFour\": \"Truy cập vào các mô hình AI cục bộ\"\n      },\n      \"questionFour\": {\n        \"question\": \"Bạn sẽ mô tả trải nghiệm chung của bạn với @:appName như thế nào?\",\n        \"answerOne\": \"Tuyệt\",\n        \"answerTwo\": \"Tốt\",\n        \"answerThree\": \"Trung bình\",\n        \"answerFour\": \"Dưới mức trung bình\",\n        \"answerFive\": \"Không hài lòng\"\n      }\n    },\n    \"common\": {\n      \"reset\": \"Đặt lại\"\n    },\n    \"menu\": {\n      \"appearance\": \"Giao diện\",\n      \"language\": \"Ngôn ngữ\",\n      \"user\": \"Người dùng\",\n      \"files\": \"Tập tin\",\n      \"notifications\": \"Thông báo\",\n      \"open\": \"Mở cài đặt\",\n      \"logout\": \"Đăng xuất\",\n      \"logoutPrompt\": \"Bạn chắc chắn muốn đăng xuất?\",\n      \"selfEncryptionLogoutPrompt\": \"Bạn có chắc chắn bạn muốn thoát? Hãy đảm bảo bạn đã sao chép bí mật mã hóa\",\n      \"syncSetting\": \"Cài đặt đồng bộ\",\n      \"cloudSettings\": \"Cài đặt đám mây\",\n      \"enableSync\": \"Bật tính năng đồng bộ\",\n      \"enableEncrypt\": \"Mã hoá dữ liệu\",\n      \"cloudURL\": \"URL\",\n      \"invalidCloudURLScheme\": \"Scheme không hợp lệ\",\n      \"cloudServerType\": \"Máy chủ đám mây\",\n      \"cloudServerTypeTip\": \"Xin lưu ý rằng có thể bạn sẽ bị đăng xuất sau khi chuyển máy chủ đám mây\",\n      \"cloudLocal\": \"Cục bộ\",\n      \"cloudAppFlowy\": \"@:appName Cloud Beta\",\n      \"cloudAppFlowySelfHost\": \"@:appName Cloud Tự lưu trữ\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"URL đám mây không được để trống\",\n      \"clickToCopy\": \"Bấm để sao chép\",\n      \"selfHostStart\": \"Nếu bạn không có máy chủ, vui lòng tham khảo\",\n      \"selfHostContent\": \"tài liệu\",\n      \"selfHostEnd\": \"để được hướng dẫn cách tự lưu trữ máy chủ của riêng bạn\",\n      \"cloudURLHint\": \"Nhập URL của máy chủ của bạn\",\n      \"cloudWSURL\": \"Websocket URL\",\n      \"cloudWSURLHint\": \"Nhập địa chỉ websocket của máy chủ của bạn\",\n      \"restartApp\": \"Khởi động lại\",\n      \"restartAppTip\": \"Khởi động lại ứng dụng để thay đổi có hiệu lực. Xin lưu ý rằng điều này có thể đăng xuất tài khoản hiện tại của bạn\",\n      \"changeServerTip\": \"Sau khi thay đổi máy chủ, bạn phải nhấp vào nút khởi động lại để những thay đổi có hiệu lực\",\n      \"enableEncryptPrompt\": \"Kích hoạt mã hóa để bảo mật dữ liệu của bạn với bí mật này. Lưu trữ nó một cách an toàn; một khi đã bật thì không thể tắt được. Nếu bị mất, dữ liệu của bạn sẽ không thể phục hồi được. Bấm để sao chép\",\n      \"inputEncryptPrompt\": \"Vui lòng nhập bí mật mã hóa của bạn cho\",\n      \"clickToCopySecret\": \"Bấm để sao chép bí mật\",\n      \"configServerSetting\": \"Cài đặt cấu hình máy chủ của bạn\",\n      \"configServerGuide\": \"Sau khi chọn `Bắt đầu nhanh`, điều hướng đến `Cài đặt` rồi đến \\\"Cài đặt đám mây\\\" để định cấu hình máy chủ tự lưu trữ của bạn.\",\n      \"inputTextFieldHint\": \"Bí mật của bạn\",\n      \"historicalUserList\": \"Lịch sử đăng nhập\",\n      \"historicalUserListTooltip\": \"Danh sách này hiển thị các tài khoản ẩn danh của bạn. Bạn có thể nhấp vào một tài khoản để xem thông tin chi tiết của tài khoản đó. Các tài khoản ẩn danh được tạo bằng cách nhấp vào nút 'Bắt đầu'\",\n      \"openHistoricalUser\": \"Ấn để mở tài khoản ẩn danh\",\n      \"customPathPrompt\": \"Lưu trữ thư mục dữ liệu @:appName trong thư mục được đồng bộ hóa trên đám mây như Google Drive có thể gây ra rủi ro. Nếu cơ sở dữ liệu trong thư mục này được truy cập hoặc sửa đổi từ nhiều vị trí cùng một lúc, có thể dẫn đến xung đột đồng bộ hóa và hỏng dữ liệu tiềm ẩn\",\n      \"importAppFlowyData\": \"Nhập dữ liệu từ thư mục @:appName bên ngoài\",\n      \"importingAppFlowyDataTip\": \"Quá trình nhập dữ liệu đang diễn ra. Vui lòng không đóng ứng dụng\",\n      \"importAppFlowyDataDescription\": \"Sao chép dữ liệu từ thư mục dữ liệu @:appName bên ngoài và nhập dữ liệu đó vào thư mục dữ liệu @:appName hiện tại\",\n      \"importSuccess\": \"Đã nhập thành công thư mục dữ liệu @:appName\",\n      \"importFailed\": \"Nhập thư mục dữ liệu @:appName không thành công\",\n      \"importGuide\": \"Để biết thêm chi tiết, vui lòng kiểm tra tài liệu được tham chiếu\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"Bật thông báo\",\n        \"hint\": \"Tắt để ngăn thông báo xuất hiện.\"\n      },\n      \"showNotificationsIcon\": {\n        \"label\": \"Hiển thị biểu tượng thông báo\",\n        \"hint\": \"Tắt để ẩn biểu tượng thông báo ở thanh bên.\"\n      },\n      \"archiveNotifications\": {\n        \"allSuccess\": \"Đã lưu trữ tất cả thông báo thành công\",\n        \"success\": \"Đã lưu trữ thông báo thành công\"\n      },\n      \"markAsReadNotifications\": {\n        \"allSuccess\": \"Đã đánh dấu tất cả là đã đọc thành công\",\n        \"success\": \"Đã đánh dấu là đã đọc thành công\"\n      },\n      \"action\": {\n        \"markAsRead\": \"Đánh dấu là đã đọc\",\n        \"multipleChoice\": \"Chọn thêm\",\n        \"archive\": \"Lưu trữ\"\n      },\n      \"settings\": {\n        \"settings\": \"Cài đặt\",\n        \"markAllAsRead\": \"Đánh dấu tất cả là đã đọc\",\n        \"archiveAll\": \"Lưu trữ tất cả\"\n      },\n      \"emptyInbox\": {\n        \"title\": \"Chưa có thông báo nào\",\n        \"description\": \"Bạn sẽ được thông báo ở đây về @đề cập\"\n      },\n      \"emptyUnread\": {\n        \"title\": \"Không có thông báo chưa đọc\",\n        \"description\": \"Bạn đã hiểu hết rồi!\"\n      },\n      \"emptyArchived\": {\n        \"title\": \"Không có thông báo lưu trữ\",\n        \"description\": \"Bạn chưa lưu trữ bất kỳ thông báo nào\"\n      },\n      \"tabs\": {\n        \"inbox\": \"Hộp thư đến\",\n        \"unread\": \"Chưa đọc\",\n        \"archived\": \"Đã lưu trữ\"\n      },\n      \"refreshSuccess\": \"Thông báo đã được làm mới thành công\",\n      \"titles\": {\n        \"notifications\": \"Thông báo\",\n        \"reminder\": \"Lời nhắc nhở\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"Đặt lại cài đặt\",\n      \"fontFamily\": {\n        \"label\": \"Phông chữ\",\n        \"search\": \"Tìm kiếm\",\n        \"defaultFont\": \"Hệ thống\"\n      },\n      \"themeMode\": {\n        \"label\": \"Chế độ Theme\",\n        \"light\": \"Chế độ sáng\",\n        \"dark\": \"Chế độ tối\",\n        \"system\": \"Thích ứng với hệ thống\"\n      },\n      \"fontScaleFactor\": \"Hệ số tỷ lệ phông chữ\",\n      \"documentSettings\": {\n        \"cursorColor\": \"Màu con trỏ\",\n        \"selectionColor\": \"Màu lựa chọn tài liệu\",\n        \"pickColor\": \"Chọn một màu\",\n        \"colorShade\": \"Màu sắc bóng râm\",\n        \"opacity\": \"Độ mờ đục\",\n        \"hexEmptyError\": \"Màu hex không được để trống\",\n        \"hexLengthError\": \"Giá trị hex phải dài 6 chữ số\",\n        \"hexInvalidError\": \"Giá trị hex không hợp lệ\",\n        \"opacityEmptyError\": \"Độ mờ không được để trống\",\n        \"opacityRangeError\": \"Độ mờ phải nằm trong khoảng từ 1 đến 100\",\n        \"app\": \"App\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"Áp dụng\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"Hướng bố cục\",\n        \"hint\": \"Kiểm soát hướng bố cục nội dung trên màn hình của bạn, từ trái sang phải hoặc phải sang trái.\",\n        \"ltr\": \"Trái sang phải\",\n        \"rtl\": \"Phải sang trái\"\n      },\n      \"textDirection\": {\n        \"label\": \"Hướng kí tự mặc định\",\n        \"hint\": \"Chỉ định xem văn bản nên bắt đầu từ trái hay phải làm mặc định.\",\n        \"ltr\": \"Trái sang phải\",\n        \"rtl\": \"Phải sang trái\",\n        \"auto\": \"TỰ ĐỘNG\",\n        \"fallback\": \"Tương tự như hướng bố cục\"\n      },\n      \"themeUpload\": {\n        \"button\": \"Tải lên\",\n        \"uploadTheme\": \"Tải theme lên\",\n        \"description\": \"Tải lên @:appName theme của riêng bạn bằng nút bên dưới.\",\n        \"loading\": \"Vui lòng đợi trong khi chúng tôi xác thực và tải theme của bạn lên...\",\n        \"uploadSuccess\": \"Theme của bạn đã được tải lên thành công\",\n        \"deletionFailure\": \"Không xóa được theme. Hãy thử xóa nó bằng tay.\",\n        \"filePickerDialogTitle\": \"Chọn tệp .flowy_plugin\",\n        \"urlUploadFailure\": \"Không mở được url: {}\"\n      },\n      \"theme\": \"Chủ đề\",\n      \"builtInsLabel\": \"Theme có sẵn\",\n      \"pluginsLabel\": \"Các plugin\",\n      \"dateFormat\": {\n        \"label\": \"Định dạng ngày\",\n        \"local\": \"Địa phương\",\n        \"us\": \"US\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"Thân thiện\",\n        \"dmy\": \"D/M/Y\"\n      },\n      \"timeFormat\": {\n        \"label\": \"Định dạng giờ\",\n        \"twelveHour\": \"12 tiếng\",\n        \"twentyFourHour\": \"Hai mươi bốn tiếng\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"Hiển thị hộp thoại đặt tên khi tạo trang\",\n      \"enableRTLToolbarItems\": \"Bật các mục thanh công cụ RTL\",\n      \"members\": {\n        \"title\": \"Cài đặt thành viên\",\n        \"inviteMembers\": \"Mời thành viên\",\n        \"inviteHint\": \"Mời qua email\",\n        \"sendInvite\": \"Gửi lời mời\",\n        \"copyInviteLink\": \"Sao chép liên kết mời\",\n        \"label\": \"Các thành viên\",\n        \"user\": \"Người dùng\",\n        \"role\": \"Vai trò\",\n        \"removeFromWorkspace\": \"Xóa khỏi Workspace\",\n        \"removeFromWorkspaceSuccess\": \"Xóa khỏi không gian làm việc thành công\",\n        \"removeFromWorkspaceFailed\": \"Xóa khỏi không gian làm việc không thành công\",\n        \"owner\": \"Người sở hữu\",\n        \"guest\": \"Khách\",\n        \"member\": \"Thành viên\",\n        \"memberHintText\": \"Một thành viên có thể đọc và chỉnh sửa các trang\",\n        \"guestHintText\": \"Khách có thể đọc, phản hồi, bình luận và chỉnh sửa một số trang nhất định khi được phép.\",\n        \"emailInvalidError\": \"Email không hợp lệ, vui lòng kiểm tra và thử lại\",\n        \"emailSent\": \"Email đã được gửi, vui lòng kiểm tra hộp thư đến\",\n        \"members\": \"các thành viên\",\n        \"membersCount\": {\n          \"zero\": \"{} thành viên\",\n          \"one\": \"{} thành viên\",\n          \"other\": \"{} thành viên\"\n        },\n        \"inviteFailedDialogTitle\": \"Không gửi được lời mời\",\n        \"inviteFailedMemberLimit\": \"Đã đạt đến giới hạn thành viên, vui lòng nâng cấp để mời thêm thành viên.\",\n        \"inviteFailedMemberLimitMobile\": \"Không gian làm việc của bạn đã đạt đến giới hạn thành viên. Nâng cấp trên Desktop để mở khóa thêm nhiều tính năng.\",\n        \"memberLimitExceeded\": \"Đã đạt đến giới hạn thành viên, vui lòng mời thêm thành viên \",\n        \"memberLimitExceededUpgrade\": \"nâng cấp\",\n        \"memberLimitExceededPro\": \"Đã đạt đến giới hạn thành viên, nếu bạn cần thêm thành viên hãy liên hệ \",\n        \"memberLimitExceededProContact\": \"support@appflowy.io\",\n        \"failedToAddMember\": \"Không thêm được thành viên\",\n        \"addMemberSuccess\": \"Thành viên đã được thêm thành công\",\n        \"removeMember\": \"Xóa thành viên\",\n        \"areYouSureToRemoveMember\": \"Bạn có chắc chắn muốn xóa thành viên này không?\",\n        \"inviteMemberSuccess\": \"Lời mời đã được gửi thành công\",\n        \"failedToInviteMember\": \"Không mời được thành viên\",\n        \"workspaceMembersError\": \"Ồ, có gì đó không ổn\",\n        \"workspaceMembersErrorDescription\": \"Chúng tôi không thể tải danh sách thành viên vào lúc này. Vui lòng thử lại sau\"\n      }\n    },\n    \"files\": {\n      \"copy\": \"Sao chép\",\n      \"defaultLocation\": \"Đọc tập tin và vị trí lưu trữ dữ liệu\",\n      \"exportData\": \"Xuất dữ liệu của bạn\",\n      \"doubleTapToCopy\": \"Nhấn đúp để sao chép đường dẫn\",\n      \"restoreLocation\": \"Khôi phục về đường dẫn mặc định của @:appName\",\n      \"customizeLocation\": \"Mở thư mục khác\",\n      \"restartApp\": \"Vui lòng khởi động lại ứng dụng để những thay đổi có hiệu lực.\",\n      \"exportDatabase\": \"Xuất cơ sở dữ liệu\",\n      \"selectFiles\": \"Chọn các file cần xuất\",\n      \"selectAll\": \"Chọn tất cả\",\n      \"deselectAll\": \"Bỏ chọn tất cả\",\n      \"createNewFolder\": \"Tạo một thư mục mới\",\n      \"createNewFolderDesc\": \"Hãy cho chúng tôi biết nơi bạn muốn lưu trữ dữ liệu của mình\",\n      \"defineWhereYourDataIsStored\": \"Xác định nơi dữ liệu của bạn được lưu trữ\",\n      \"open\": \"Mở\",\n      \"openFolder\": \"Mở một thư mục hiện có\",\n      \"openFolderDesc\": \"Đọc và ghi nó vào thư mục @:appName hiện có của bạn\",\n      \"folderHintText\": \"tên thư mục\",\n      \"location\": \"Tạo một thư mục mới\",\n      \"locationDesc\": \"Chọn tên cho thư mục dữ liệu @:appName của bạn\",\n      \"browser\": \"Duyệt\",\n      \"create\": \"Tạo\",\n      \"set\": \"Bộ\",\n      \"folderPath\": \"Đường dẫn lưu trữ thư mục của bạn\",\n      \"locationCannotBeEmpty\": \"Đường dẫn không thể trống\",\n      \"pathCopiedSnackbar\": \"Đã sao chép đường dẫn lưu trữ tệp vào khay nhớ tạm!\",\n      \"changeLocationTooltips\": \"Thay đổi thư mục dữ liệu\",\n      \"change\": \"Thay đổi\",\n      \"openLocationTooltips\": \"Mở thư mục dữ liệu khác\",\n      \"openCurrentDataFolder\": \"Mở thư mục dữ liệu hiện tại\",\n      \"recoverLocationTooltips\": \"Đặt lại về thư mục dữ liệu mặc định của @:appName\",\n      \"exportFileSuccess\": \"Xuất tập tin thành công!\",\n      \"exportFileFail\": \"Xuất tập tin thất bại!\",\n      \"export\": \"Xuất\",\n      \"clearCache\": \"Xóa bộ nhớ đệm\",\n      \"clearCacheDesc\": \"Nếu bạn gặp sự cố với hình ảnh không tải hoặc phông chữ không hiển thị đúng, hãy thử xóa bộ nhớ đệm. Hành động này sẽ không xóa dữ liệu người dùng của bạn.\",\n      \"areYouSureToClearCache\": \"Bạn có chắc chắn muốn xóa bộ nhớ đệm không?\",\n      \"clearCacheSuccess\": \"Đã xóa bộ nhớ đệm thành công!\"\n    },\n    \"user\": {\n      \"name\": \"Tên\",\n      \"email\": \"E-mail\",\n      \"tooltipSelectIcon\": \"Chọn biểu tượng\",\n      \"selectAnIcon\": \"Chọn một biểu tượng\",\n      \"pleaseInputYourOpenAIKey\": \"vui lòng nhập khóa AI của bạn\",\n      \"clickToLogout\": \"Nhấn để đăng xuất\",\n      \"pleaseInputYourStabilityAIKey\": \"vui lòng nhập khóa Stability AI của bạn\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"Thông tin cá nhân\",\n      \"username\": \"Tên người dùng \",\n      \"usernameEmptyError\": \"Tên người dùng không được để trống\",\n      \"about\": \"Về\",\n      \"pushNotifications\": \"Thông báo\",\n      \"support\": \"Ủng hộ\",\n      \"joinDiscord\": \"Tham gia cùng chúng tôi trên Discord\",\n      \"privacyPolicy\": \"Chính sách bảo mật\",\n      \"userAgreement\": \"Thoả thuận người dùng\",\n      \"termsAndConditions\": \"Điều khoản và điều kiện\",\n      \"userprofileError\": \"Không tải được hồ sơ người dùng\",\n      \"userprofileErrorDescription\": \"Vui lòng thử đăng xuất và đăng nhập lại để kiểm tra xem sự cố vẫn còn.\",\n      \"selectLayout\": \"Chọn bố cục\",\n      \"selectStartingDay\": \"Chọn ngày bắt đầu\",\n      \"version\": \"Phiên bản\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"Phím tắt\",\n      \"updateShortcutStep\": \"Nhấn tổ hợp phím mong muốn và nhấn ENTER\",\n      \"shortcutIsAlreadyUsed\": \"Phím tắt này đã được sử dụng cho: {conflict}\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"Bạn có chắc chắn muốn xóa chế độ xem này không?\",\n    \"createView\": \"Mới\",\n    \"title\": {\n      \"placeholder\": \"Không tiêu đề\"\n    },\n    \"settings\": {\n      \"filter\": \"Lọc\",\n      \"sort\": \"Sắp xếp\",\n      \"sortBy\": \"Sắp xếp bằng\",\n      \"properties\": \"Thuộc tính\",\n      \"reorderPropertiesTooltip\": \"Kéo để sắp xếp lại thuộc tính\",\n      \"group\": \"Nhóm\",\n      \"addFilter\": \"Thêm bộ lọc\",\n      \"deleteFilter\": \"Xóa bộ lọc\",\n      \"filterBy\": \"Lọc bằng...\",\n      \"typeAValue\": \"Nhập một giá trị...\",\n      \"layout\": \"Bố cục\",\n      \"databaseLayout\": \"Bố cục\",\n      \"editView\": \"Chỉnh sửa chế độ xem\",\n      \"boardSettings\": \"Cài đặt bảng\",\n      \"calendarSettings\": \"Cài đặt lịch\",\n      \"createView\": \"Góc nhìn mới\",\n      \"duplicateView\": \"Xem trùng lặp\",\n      \"deleteView\": \"Xóa chế độ xem\",\n      \"numberOfVisibleFields\": \"{} đã hiển thị\",\n      \"Properties\": \"Thuộc tính\",\n      \"viewList\": \"Database Views\"\n    },\n    \"textFilter\": {\n      \"contains\": \"Chứa\",\n      \"doesNotContain\": \"Không chứa\",\n      \"endsWith\": \"Kết thúc bằng\",\n      \"startWith\": \"Bắt đầu với\",\n      \"is\": \"Là\",\n      \"isNot\": \"Không phải\",\n      \"isEmpty\": \"Rỗng\",\n      \"isNotEmpty\": \"Không rỗng\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"Không phải\",\n        \"startWith\": \"Bắt đầu với\",\n        \"endWith\": \"Kết thúc bằng\",\n        \"isEmpty\": \"rỗng\",\n        \"isNotEmpty\": \"không rỗng\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"Đã chọn\",\n      \"isUnchecked\": \"Không chọn\",\n      \"choicechipPrefix\": {\n        \"is\": \"là\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"hoàn tất\",\n      \"isIncomplted\": \"chưa hoàn tất\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"Là\",\n      \"isNot\": \"Không phải\",\n      \"contains\": \"Chứa\",\n      \"doesNotContain\": \"Không chứa\",\n      \"isEmpty\": \"Rỗng\",\n      \"isNotEmpty\": \"Không rỗng\"\n    },\n    \"dateFilter\": {\n      \"is\": \"Là\",\n      \"before\": \"Trước\",\n      \"after\": \"Sau\",\n      \"onOrBefore\": \"Đang diễn ra hoặc trước đó\",\n      \"onOrAfter\": \"Đang bật hoặc sau\",\n      \"between\": \"Ở giữa\",\n      \"empty\": \"Rỗng\",\n      \"notEmpty\": \"Không rỗng\",\n      \"choicechipPrefix\": {\n        \"before\": \"Trước\",\n        \"after\": \"Sau đó\",\n        \"onOrBefore\": \"Vào hoặc trước\",\n        \"onOrAfter\": \"Vào hoặc sau\",\n        \"isEmpty\": \"Đang trống\",\n        \"isNotEmpty\": \"Không trống\"\n      }\n    },\n    \"numberFilter\": {\n      \"equal\": \"Bằng nhau\",\n      \"notEqual\": \"Không bằng\",\n      \"lessThan\": \"Ít hơn\",\n      \"greaterThan\": \"Lớn hơn\",\n      \"lessThanOrEqualTo\": \"Nhỏ hơn hoặc bằng\",\n      \"greaterThanOrEqualTo\": \"Lớn hơn hoặc bằng\",\n      \"isEmpty\": \"Đang trống\",\n      \"isNotEmpty\": \"Không trống\"\n    },\n    \"field\": {\n      \"label\": \"Thuộc tính\",\n      \"hide\": \"Ẩn\",\n      \"show\": \"Hiện\",\n      \"insertLeft\": \"Chèn trái\",\n      \"insertRight\": \"Chèn phải\",\n      \"duplicate\": \"Nhân bản\",\n      \"delete\": \"Xóa\",\n      \"wrapCellContent\": \"Bao quanh văn bản\",\n      \"clear\": \"Xóa tế bào\",\n      \"textFieldName\": \"Chữ\",\n      \"checkboxFieldName\": \"Hộp kiểm\",\n      \"dateFieldName\": \"Ngày\",\n      \"updatedAtFieldName\": \"Sửa đổi lần cuối\",\n      \"createdAtFieldName\": \"Được tạo vào lúc\",\n      \"numberFieldName\": \"Số\",\n      \"singleSelectFieldName\": \"Lựa chọn\",\n      \"multiSelectFieldName\": \"Chọn nhiều\",\n      \"urlFieldName\": \"URL\",\n      \"checklistFieldName\": \"Danh mục\",\n      \"relationFieldName\": \"Mối quan hệ\",\n      \"summaryFieldName\": \"Tóm tắt AI\",\n      \"timeFieldName\": \"Thời gian\",\n      \"mediaFieldName\": \"Tệp tin & phương tiện\",\n      \"translateFieldName\": \"AI Dịch\",\n      \"translateTo\": \"Dịch sang\",\n      \"numberFormat\": \"Định dạng số\",\n      \"dateFormat\": \"Định dạng ngày tháng\",\n      \"includeTime\": \"Bao gồm thời gian\",\n      \"isRange\": \"Ngày cuối\",\n      \"dateFormatFriendly\": \"Tháng Ngày, Năm\",\n      \"dateFormatISO\": \"Năm-Tháng-Ngày\",\n      \"dateFormatLocal\": \"Tháng/Ngày/Năm\",\n      \"dateFormatUS\": \"Năm/Tháng/Ngày\",\n      \"dateFormatDayMonthYear\": \"Ngày/Tháng/Năm\",\n      \"timeFormat\": \"Định dạng thời gian\",\n      \"invalidTimeFormat\": \"Định dạng không hợp lệ\",\n      \"timeFormatTwelveHour\": \"12 giờ\",\n      \"timeFormatTwentyFourHour\": \"24 giờ\",\n      \"clearDate\": \"Xóa ngày\",\n      \"dateTime\": \"Ngày giờ\",\n      \"startDateTime\": \"Ngày giờ bắt đầu\",\n      \"endDateTime\": \"Ngày giờ kết thúc\",\n      \"failedToLoadDate\": \"Không thể tải giá trị ngày\",\n      \"selectTime\": \"Chọn thời gian\",\n      \"selectDate\": \"Chọn ngày\",\n      \"visibility\": \"Hiển thị\",\n      \"propertyType\": \"Loại thuộc tính\",\n      \"addSelectOption\": \"Thêm một tùy chọn\",\n      \"typeANewOption\": \"Nhập một tùy chọn mới\",\n      \"optionTitle\": \"Tùy chọn\",\n      \"addOption\": \"Thêm tùy chọn\",\n      \"editProperty\": \"Chỉnh sửa thuộc tính\",\n      \"newProperty\": \"Thuộc tính mới\",\n      \"openRowDocument\": \"Mở như một trang\",\n      \"deleteFieldPromptMessage\": \"Bạn có chắc không? Thuộc tính này sẽ bị xóa\",\n      \"clearFieldPromptMessage\": \"Bạn có chắc chắn không? Tất cả các ô trong cột này sẽ được làm trống\",\n      \"newColumn\": \"Cột mới\",\n      \"format\": \"Định dạng\",\n      \"reminderOnDateTooltip\": \"Ô này có lời nhắc được lên lịch\",\n      \"optionAlreadyExist\": \"Tùy chọn đã tồn tại\"\n    },\n    \"rowPage\": {\n      \"newField\": \"Thêm một trường mới\",\n      \"fieldDragElementTooltip\": \"Bấm để mở menu\",\n      \"showHiddenFields\": {\n        \"one\": \"Hiển thị {count} trường ẩn \",\n        \"many\": \"Hiển thị {count} trường ẩn\",\n        \"other\": \"Hiển thị {count} trường ẩn\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"Ẩn {count} trường ẩn\",\n        \"many\": \"Ẩn {count} trường ẩn\",\n        \"other\": \"Ẩn {count} trường ẩn\"\n      },\n      \"openAsFullPage\": \"Mở dưới dạng trang đầy đủ\",\n      \"moreRowActions\": \"Thêm hành động hàng\"\n    },\n    \"sort\": {\n      \"ascending\": \"Tăng dần\",\n      \"descending\": \"Giảm dần\",\n      \"by\": \"Qua\",\n      \"empty\": \"Không có loại hoạt động\",\n      \"cannotFindCreatableField\": \"Không tìm thấy trường phù hợp để sắp xếp theo\",\n      \"deleteAllSorts\": \"Xóa tất cả sắp xếp\",\n      \"addSort\": \"Thêm sắp xếp\",\n      \"removeSorting\": \"Bạn có muốn xóa chế độ sắp xếp không?\",\n      \"fieldInUse\": \"Bạn đang sắp xếp theo trường này\"\n    },\n    \"row\": {\n      \"duplicate\": \"Nhân bản\",\n      \"delete\": \"Xóa\",\n      \"titlePlaceholder\": \"Không tiêu đề\",\n      \"textPlaceholder\": \"Rỗng\",\n      \"copyProperty\": \"Đã sao chép thuộc tính\",\n      \"count\": \"Số lượng\",\n      \"newRow\": \"Hàng mới\",\n      \"action\": \"Hành động\",\n      \"add\": \"Nhấp vào thêm vào bên dưới\",\n      \"drag\": \"Kéo để di chuyển\",\n      \"deleteRowPrompt\": \"Bạn có chắc chắn muốn xóa hàng này không? Hành động này không thể hoàn tác\",\n      \"deleteCardPrompt\": \"Bạn có chắc chắn muốn xóa thẻ này không? Hành động này không thể hoàn tác\",\n      \"dragAndClick\": \"Kéo để di chuyển, nhấp để mở menu\",\n      \"insertRecordAbove\": \"Chèn bản ghi ở trên\",\n      \"insertRecordBelow\": \"Chèn bản ghi bên dưới\",\n      \"noContent\": \"Không có nội dung\"\n    },\n    \"selectOption\": {\n      \"create\": \"Tạo\",\n      \"purpleColor\": \"Tím\",\n      \"pinkColor\": \"Hồng\",\n      \"lightPinkColor\": \"Hồng nhạt\",\n      \"orangeColor\": \"Cam\",\n      \"yellowColor\": \"Vàng\",\n      \"limeColor\": \"Xanh vàng\",\n      \"greenColor\": \"Xanh là cây \",\n      \"aquaColor\": \"Thủy\",\n      \"blueColor\": \"Xanh da trời\",\n      \"deleteTag\": \"Xóa thẻ\",\n      \"colorPanelTitle\": \"Màu\",\n      \"panelTitle\": \"Chọn một tùy chọn hoặc tạo mới\",\n      \"searchOption\": \"Tìm kiếm một lựa chọn\",\n      \"searchOrCreateOption\": \"Tìm kiếm hoặc tạo một tùy chọn...\",\n      \"createNew\": \"Tạo một cái mới\",\n      \"orSelectOne\": \"Hoặc chọn một tùy chọn\",\n      \"typeANewOption\": \"Nhập một tùy chọn mới\",\n      \"tagName\": \"Tên thẻ\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"Mô tả nhiệm vụ\",\n      \"addNew\": \"Thêm một nhiệm vụ mới\",\n      \"submitNewTask\": \"Tạo nên\",\n      \"hideComplete\": \"Ẩn các tác vụ đã hoàn thành\",\n      \"showComplete\": \"Hiển thị tất cả các nhiệm vụ\"\n    },\n    \"url\": {\n      \"launch\": \"Mở liên kết trong trình duyệt\",\n      \"copy\": \"Sao chép URL\",\n      \"textFieldHint\": \"Nhập một URL\"\n    },\n    \"relation\": {\n      \"relatedDatabasePlaceLabel\": \"Cơ sở dữ liệu liên quan\",\n      \"relatedDatabasePlaceholder\": \"Không có\",\n      \"inRelatedDatabase\": \"TRONG\",\n      \"rowSearchTextFieldPlaceholder\": \"Tìm kiếm\",\n      \"noDatabaseSelected\": \"Chưa chọn cơ sở dữ liệu, vui lòng chọn một cơ sở dữ liệu trước từ danh sách bên dưới:\",\n      \"emptySearchResult\": \"Không tìm thấy hồ sơ nào\",\n      \"linkedRowListLabel\": \"{count} hàng được liên kết\",\n      \"unlinkedRowListLabel\": \"Liên kết một hàng khác\"\n    },\n    \"menuName\": \"Lưới\",\n    \"referencedGridPrefix\": \"Xem của\",\n    \"calculate\": \"Tính toán\",\n    \"calculationTypeLabel\": {\n      \"none\": \"Không có\",\n      \"average\": \"Trung bình\",\n      \"max\": \"Tối đa\",\n      \"median\": \"Trung vị\",\n      \"min\": \"Tối thiểu\",\n      \"sum\": \"Tổng\",\n      \"count\": \"Đếm\",\n      \"countEmpty\": \"Đếm trống\",\n      \"countEmptyShort\": \"TRỐNG\",\n      \"countNonEmpty\": \"Đếm không trống\",\n      \"countNonEmptyShort\": \"ĐIỀN\"\n    },\n    \"media\": {\n      \"rename\": \"Đổi tên\",\n      \"download\": \"Tải về\",\n      \"delete\": \"Xóa bỏ\",\n      \"moreFilesHint\": \"+{}\",\n      \"addFileOrImage\": \"Thêm tệp, hình ảnh hoặc liên kết\",\n      \"attachmentsHint\": \"{}\",\n      \"addFileMobile\": \"Thêm tập tin\",\n      \"deleteFileDescription\": \"Bạn có chắc chắn muốn xóa tệp này không? Hành động này không thể hoàn tác.\",\n      \"downloadSuccess\": \"Đã lưu tập tin thành công\",\n      \"downloadFailedToken\": \"Không tải được tệp, mã thông báo người dùng không khả dụng\",\n      \"open\": \"Mở\",\n      \"hideFileNames\": \"Ẩn tên tập tin\",\n      \"showFile\": \"Hiển thị 1 tập tin\",\n      \"showFiles\": \"Hiển thị {} tập tin\",\n      \"hideFile\": \"Ẩn 1 tập tin\",\n      \"hideFiles\": \"Ẩn {} tập tin\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"Tài liệu\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"01:00 Chiều\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"Chọn một bảng để liên kết\",\n        \"createANewBoard\": \"Tạo một bảng mới\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"Chọn một lưới để liên kết đến\",\n        \"createANewGrid\": \"Tạo một lưới mới\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"Chọn Lịch để liên kết đến\",\n        \"createANewCalendar\": \"Tạo một Lịch mới\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"Chọn một Tài liệu để liên kết đến\"\n      },\n      \"name\": {\n        \"text\": \"Chữ\",\n        \"heading1\": \"Tiêu đề 1\",\n        \"heading2\": \"Tiêu đề 2\",\n        \"heading3\": \"Tiêu đề 3\",\n        \"image\": \"Hình ảnh\",\n        \"bulletedList\": \"Danh sách có dấu đầu dòng\",\n        \"numberedList\": \"Danh sách được đánh số\",\n        \"todoList\": \"Danh sách việc cần làm\",\n        \"doc\": \"Tài liệu\",\n        \"linkedDoc\": \"Liên kết đến trang\",\n        \"grid\": \"Lưới\",\n        \"linkedGrid\": \"Lưới liên kết\",\n        \"kanban\": \"Kanban\",\n        \"linkedKanban\": \"Kanban liên kết\",\n        \"calendar\": \"Lịch\",\n        \"linkedCalendar\": \"Lịch liên kết\",\n        \"quote\": \"Trích dẫn\",\n        \"divider\": \"Bộ chia\",\n        \"table\": \"Bàn\",\n        \"callout\": \"Chú thích\",\n        \"outline\": \"phác thảo\",\n        \"mathEquation\": \"Phương trình toán học\",\n        \"code\": \"Mã số\",\n        \"toggleList\": \"Chuyển đổi danh sách\",\n        \"emoji\": \"Biểu tượng cảm xúc\",\n        \"aiWriter\": \"Nhà văn AI\",\n        \"dateOrReminder\": \"Ngày hoặc nhắc nhở\",\n        \"photoGallery\": \"Thư viện ảnh\",\n        \"file\": \"Tài liệu\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"phác thảo\",\n      \"codeBlock\": \"Khối mã\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"Hội đồng tham khảo\",\n      \"referencedGrid\": \"Lưới tham chiếu\",\n      \"referencedCalendar\": \"Lịch tham khảo\",\n      \"referencedDocument\": \"Tài liệu tham khảo\",\n      \"autoGeneratorMenuItemName\": \"Nhà văn AI\",\n      \"autoGeneratorTitleName\": \"AI: Yêu cầu AI viết bất cứ điều gì...\",\n      \"autoGeneratorLearnMore\": \"Tìm hiểu thêm\",\n      \"autoGeneratorGenerate\": \"Phát ra\",\n      \"autoGeneratorHintText\": \"Hỏi AI ...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"Không thể lấy được chìa khóa AI\",\n      \"autoGeneratorRewrite\": \"Viết lại\",\n      \"smartEdit\": \"Hỏi AI\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"Sửa lỗi chính tả và ngữ pháp\",\n      \"warning\": \"⚠️ Phản hồi của AI có thể không chính xác hoặc gây hiểu lầm.\",\n      \"smartEditSummarize\": \"Tóm tắt\",\n      \"smartEditImproveWriting\": \"Cải thiện khả năng viết\",\n      \"smartEditMakeLonger\": \"Làm dài hơn\",\n      \"smartEditCouldNotFetchResult\": \"Không thể lấy kết quả từ AI\",\n      \"smartEditCouldNotFetchKey\": \"Không thể lấy được khóa AI\",\n      \"smartEditDisabled\": \"Kết nối AI trong Cài đặt\",\n      \"appflowyAIEditDisabled\": \"Đăng nhập để bật tính năng AI\",\n      \"discardResponse\": \"Bạn có muốn loại bỏ phản hồi của AI không?\",\n      \"createInlineMathEquation\": \"Tạo phương trình\",\n      \"fonts\": \"Phông chữ\",\n      \"insertDate\": \"Chèn ngày\",\n      \"emoji\": \"Biểu tượng cảm xúc\",\n      \"toggleList\": \"Chuyển đổi danh sách\",\n      \"quoteList\": \"Danh sách trích dẫn\",\n      \"numberedList\": \"Danh sách được đánh số\",\n      \"bulletedList\": \"Danh sách có dấu đầu dòng\",\n      \"todoList\": \"Danh sách việc cần làm\",\n      \"callout\": \"Chú thích\",\n      \"cover\": {\n        \"changeCover\": \"Thay đổi bìa\",\n        \"colors\": \"Màu sắc\",\n        \"images\": \"Hình ảnh\",\n        \"clearAll\": \"Xóa tất cả\",\n        \"abstract\": \"Tóm tắt\",\n        \"addCover\": \"Thêm Bìa\",\n        \"addLocalImage\": \"Thêm hình ảnh cục bộ\",\n        \"invalidImageUrl\": \"URL hình ảnh không hợp lệ\",\n        \"failedToAddImageToGallery\": \"Không thể thêm hình ảnh vào thư viện\",\n        \"enterImageUrl\": \"Nhập URL hình ảnh\",\n        \"add\": \"Thêm vào\",\n        \"back\": \"Quay lại\",\n        \"saveToGallery\": \"Lưu vào thư viện\",\n        \"removeIcon\": \"Xóa biểu tượng\",\n        \"pasteImageUrl\": \"Dán URL hình ảnh\",\n        \"or\": \"HOẶC\",\n        \"pickFromFiles\": \"Chọn từ các tập tin\",\n        \"couldNotFetchImage\": \"Không thể tải hình ảnh\",\n        \"imageSavingFailed\": \"Lưu hình ảnh không thành công\",\n        \"addIcon\": \"Thêm biểu tượng\",\n        \"changeIcon\": \"Thay đổi biểu tượng\",\n        \"coverRemoveAlert\": \"Nó sẽ được gỡ bỏ khỏi trang bìa sau khi bị xóa.\",\n        \"alertDialogConfirmation\": \"Bạn có chắc chắn muốn tiếp tục không?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"Phương trình toán học\",\n        \"addMathEquation\": \"Thêm một phương trình TeX\",\n        \"editMathEquation\": \"Chỉnh sửa phương trình toán học\"\n      },\n      \"optionAction\": {\n        \"click\": \"Nhấp chuột\",\n        \"toOpenMenu\": \" để mở menu\",\n        \"delete\": \"Xóa\",\n        \"duplicate\": \"Nhân bản\",\n        \"turnInto\": \"Biến thành\",\n        \"moveUp\": \"Di chuyển lên\",\n        \"moveDown\": \"Di chuyển xuống\",\n        \"color\": \"Màu\",\n        \"align\": \"Căn chỉnh\",\n        \"left\": \"Trái\",\n        \"center\": \"Giữa\",\n        \"right\": \"Phải\",\n        \"defaultColor\": \"Mặc định\",\n        \"depth\": \"Độ sâu\"\n      },\n      \"image\": {\n        \"addAnImage\": \"Thêm hình ảnh\",\n        \"copiedToPasteBoard\": \"Liên kết hình ảnh đã được sao chép vào clipboard\",\n        \"addAnImageDesktop\": \"Thêm một hình ảnh\",\n        \"addAnImageMobile\": \"Nhấp để thêm một hoặc nhiều hình ảnh\",\n        \"dropImageToInsert\": \"Thả hình ảnh để chèn\",\n        \"imageUploadFailed\": \"Tải hình ảnh lên không thành công\",\n        \"imageDownloadFailed\": \"Tải hình ảnh lên không thành công, vui lòng thử lại\",\n        \"imageDownloadFailedToken\": \"Tải lên hình ảnh không thành công do thiếu mã thông báo người dùng, vui lòng thử lại\",\n        \"errorCode\": \"Mã lỗi\"\n      },\n      \"photoGallery\": {\n        \"name\": \"Thư viện ảnh\",\n        \"imageKeyword\": \"hình ảnh\",\n        \"imageGalleryKeyword\": \"thư viện hình ảnh\",\n        \"photoKeyword\": \"ảnh\",\n        \"photoBrowserKeyword\": \"trình duyệt ảnh\",\n        \"galleryKeyword\": \"phòng trưng bày\",\n        \"addImageTooltip\": \"Thêm hình ảnh\",\n        \"changeLayoutTooltip\": \"Thay đổi bố cục\",\n        \"browserLayout\": \"Trình duyệt\",\n        \"gridLayout\": \"Lưới\",\n        \"deleteBlockTooltip\": \"Xóa toàn bộ thư viện\"\n      },\n      \"math\": {\n        \"copiedToPasteBoard\": \"Phương trình toán học đã được sao chép vào clipboard\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"Liên kết đã được sao chép vào clipboard\",\n        \"convertToLink\": \"Chuyển đổi thành liên kết nhúng\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"Thêm tiêu đề để tạo mục lục.\",\n        \"noMatchHeadings\": \"Không tìm thấy tiêu đề phù hợp.\"\n      },\n      \"table\": {\n        \"addAfter\": \"Thêm sau\",\n        \"addBefore\": \"Thêm trước\",\n        \"delete\": \"Xoá\",\n        \"clear\": \"Xóa nội dung\",\n        \"duplicate\": \"Nhân bản\",\n        \"bgColor\": \"Màu nền\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"Sao chép\",\n        \"cut\": \"Cắt\",\n        \"paste\": \"Dán\"\n      },\n      \"action\": \"Hành động\",\n      \"database\": {\n        \"selectDataSource\": \"Chọn nguồn dữ liệu\",\n        \"noDataSource\": \"Không có nguồn dữ liệu\",\n        \"selectADataSource\": \"Chọn nguồn dữ liệu\",\n        \"toContinue\": \"để tiếp tục\",\n        \"newDatabase\": \"Cơ sở dữ liệu mới\",\n        \"linkToDatabase\": \"Liên kết đến Cơ sở dữ liệu\"\n      },\n      \"date\": \"Ngày\",\n      \"video\": {\n        \"label\": \"Băng hình\",\n        \"emptyLabel\": \"Thêm video\",\n        \"placeholder\": \"Dán liên kết video\",\n        \"copiedToPasteBoard\": \"Liên kết video đã được sao chép vào clipboard\",\n        \"insertVideo\": \"Thêm video\",\n        \"invalidVideoUrl\": \"URL nguồn chưa được hỗ trợ.\",\n        \"invalidVideoUrlYouTube\": \"YouTube hiện chưa được hỗ trợ.\",\n        \"supportedFormats\": \"Các định dạng được hỗ trợ: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264\"\n      },\n      \"file\": {\n        \"name\": \"Tài liệu\",\n        \"uploadTab\": \"Tải lên\",\n        \"uploadMobile\": \"Chọn tệp\",\n        \"networkTab\": \"Nhúng liên kết\",\n        \"placeholderText\": \"Tải lên hoặc nhúng một tập tin\",\n        \"placeholderDragging\": \"Thả tệp để tải lên\",\n        \"dropFileToUpload\": \"Thả tệp để tải lên\",\n        \"fileUploadHint\": \"Thả một tập tin ở đây để tải lên\\nhoặc nhấp để duyệt\",\n        \"networkHint\": \"Dán liên kết tệp\",\n        \"networkUrlInvalid\": \"URL không hợp lệ, vui lòng sửa URL và thử lại\",\n        \"networkAction\": \"Nhúng liên kết tập tin\",\n        \"fileTooBigError\": \"Kích thước tệp quá lớn, vui lòng tải lên tệp có kích thước nhỏ hơn 10MB\",\n        \"renameFile\": {\n          \"title\": \"Đổi tên tập tin\",\n          \"description\": \"Nhập tên mới cho tập tin này\",\n          \"nameEmptyError\": \"Tên tệp không được để trống.\"\n        },\n        \"uploadedAt\": \"Đã tải lên vào {}\",\n        \"linkedAt\": \"Liên kết đã được thêm vào {}\",\n        \"failedToOpenMsg\": \"Không mở được, không tìm thấy tệp\"\n      }\n    },\n    \"outlineBlock\": {\n      \"placeholder\": \"Mục lục\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"Gõ '/' cho lệnh\"\n    },\n    \"title\": {\n      \"placeholder\": \"Trống\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"Ấn để thêm ảnh\",\n      \"upload\": {\n        \"label\": \"Tải lên\",\n        \"placeholder\": \"Ấn để tải lên ảnh\"\n      },\n      \"url\": {\n        \"label\": \"Đường dẫn đến ảnh\",\n        \"placeholder\": \"Nhập đường dẫn đến ảnh\"\n      },\n      \"ai\": {\n        \"label\": \"Tạo hình ảnh từ AI\",\n        \"placeholder\": \"Vui lòng nhập lời nhắc để AI tạo hình ảnh\"\n      },\n      \"stability_ai\": {\n        \"label\": \"Tạo hình ảnh từ Stability AI\",\n        \"placeholder\": \"Vui lòng nhập lời nhắc cho Stability AI để tạo hình ảnh\"\n      },\n      \"support\": \"Giới hạn kích thước hình ảnh là 5MB. Định dạng được hỗ trợ: JPEG, PNG, GIF, SVG\",\n      \"error\": {\n        \"invalidImage\": \"Ảnh không hợp lệ\",\n        \"invalidImageSize\": \"Kích thước hình ảnh phải nhỏ hơn 5MB\",\n        \"invalidImageFormat\": \"Định dạng hình ảnh không được hỗ trợ. Các định dạng được hỗ trợ: JPEG, PNG, JPG, GIF, SVG, WEBP\",\n        \"invalidImageUrl\": \"URL hình ảnh không hợp lệ\",\n        \"noImage\": \"Không có tập tin hoặc thư mục như vậy\",\n        \"multipleImagesFailed\": \"Một hoặc nhiều hình ảnh không tải lên được, vui lòng thử lại\"\n      },\n      \"embedLink\": {\n        \"label\": \"Nhúng liên kết\",\n        \"placeholder\": \"Dán hoặc nhập liên kết hình ảnh\"\n      },\n      \"unsplash\": {\n        \"label\": \"Bỏ qua\"\n      },\n      \"searchForAnImage\": \"Tìm kiếm hình ảnh\",\n      \"pleaseInputYourOpenAIKey\": \"vui lòng nhập khóa AI của bạn vào trang Cài đặt\",\n      \"saveImageToGallery\": \"Lưu hình ảnh\",\n      \"failedToAddImageToGallery\": \"Không thể thêm hình ảnh vào thư viện\",\n      \"successToAddImageToGallery\": \"Hình ảnh đã được thêm vào thư viện thành công\",\n      \"unableToLoadImage\": \"Không thể tải hình ảnh\",\n      \"maximumImageSize\": \"Kích thước hình ảnh tải lên được hỗ trợ tối đa là 10MB\",\n      \"uploadImageErrorImageSizeTooBig\": \"Kích thước hình ảnh phải nhỏ hơn 10MB\",\n      \"imageIsUploading\": \"Hình ảnh đang được tải lên\",\n      \"openFullScreen\": \"Mở toàn màn hình\",\n      \"interactiveViewer\": {\n        \"toolbar\": {\n          \"previousImageTooltip\": \"Hình ảnh trước đó\",\n          \"nextImageTooltip\": \"Hình ảnh tiếp theo\",\n          \"zoomOutTooltip\": \"Thu nhỏ\",\n          \"zoomInTooltip\": \"Phóng to\",\n          \"changeZoomLevelTooltip\": \"Thay đổi mức độ thu phóng\",\n          \"openLocalImage\": \"Mở hình ảnh\",\n          \"downloadImage\": \"Tải xuống hình ảnh\",\n          \"closeViewer\": \"Đóng trình xem tương tác\",\n          \"scalePercentage\": \"{}%\",\n          \"deleteImageTooltip\": \"Xóa hình ảnh\"\n        }\n      }\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"Ngôn ngữ\",\n        \"placeholder\": \"Chọn ngôn ngữ\",\n        \"auto\": \"Tự động\"\n      },\n      \"copyTooltip\": \"Sao chép\",\n      \"searchLanguageHint\": \"Tìm kiếm một ngôn ngữ\",\n      \"codeCopiedSnackbar\": \"Đã sao chép mã vào bảng tạm!\"\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"Dán hoặc nhập liên kết\",\n      \"openInNewTab\": \"Mở trong tab mới\",\n      \"copyLink\": \"Sao chép liên kết\",\n      \"removeLink\": \"Xóa liên kết\",\n      \"url\": {\n        \"label\": \"URL liên kết\",\n        \"placeholder\": \"Nhập URL liên kết\"\n      },\n      \"title\": {\n        \"label\": \"Tiêu đề liên kết\",\n        \"placeholder\": \"Nhập tiêu đề liên kết\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"Nhắc đến một người, một trang hoặc một ngày...\",\n      \"page\": {\n        \"label\": \"Liên kết đến trang\",\n        \"tooltip\": \"Nhấp để mở trang\"\n      },\n      \"deleted\": \"Đã xóa\",\n      \"deletedContent\": \"Nội dung này không tồn tại hoặc đã bị xóa\",\n      \"noAccess\": \"Không có quyền truy cập\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"Chỉnh về mặc định\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"Không thể phân tích nội dung khối\",\n      \"clickToCopyTheBlockContent\": \"Nhấp để sao chép nội dung khối\",\n      \"blockContentHasBeenCopied\": \"Nội dung khối đã được sao chép.\",\n      \"parseError\": \"Đã xảy ra lỗi khi phân tích khối {}.\",\n      \"copyBlockContent\": \"Sao chép nội dung khối\"\n    },\n    \"mobilePageSelector\": {\n      \"title\": \"Chọn trang\",\n      \"failedToLoad\": \"Không tải được danh sách trang\",\n      \"noPagesFound\": \"Không tìm thấy trang nào\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"label\": \"Cột\",\n      \"createNewCard\": \"Mới\",\n      \"renameGroupTooltip\": \"Nhấn để đổi tên nhóm\",\n      \"createNewColumn\": \"Thêm một nhóm mới\",\n      \"addToColumnTopTooltip\": \"Thêm một thẻ mới ở trên cùng\",\n      \"addToColumnBottomTooltip\": \"Thêm một thẻ mới ở phía dưới\",\n      \"renameColumn\": \"Đổi tên\",\n      \"hideColumn\": \"Ẩn\",\n      \"newGroup\": \"Nhóm mới\",\n      \"deleteColumn\": \"Xoá\",\n      \"deleteColumnConfirmation\": \"Thao tác này sẽ xóa nhóm này và tất cả các thẻ trong đó.\\nBạn có chắc chắn muốn tiếp tục không?\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"Nhóm ẩn\",\n      \"collapseTooltip\": \"Ẩn các nhóm ẩn\",\n      \"expandTooltip\": \"Xem các nhóm ẩn\"\n    },\n    \"cardDetail\": \"Chi tiết thẻ\",\n    \"cardActions\": \"Hành động thẻ\",\n    \"cardDuplicated\": \"Thẻ đã được sao chép\",\n    \"cardDeleted\": \"Thẻ đã bị xóa\",\n    \"showOnCard\": \"Hiển thị trên chi tiết thẻ\",\n    \"setting\": \"Cài đặt\",\n    \"propertyName\": \"Tên tài sản\",\n    \"menuName\": \"Bảng\",\n    \"showUngrouped\": \"Hiển thị các mục chưa nhóm\",\n    \"ungroupedButtonText\": \"Không nhóm\",\n    \"ungroupedButtonTooltip\": \"Chứa các thẻ không thuộc bất kỳ nhóm nào\",\n    \"ungroupedItemsTitle\": \"Nhấp để thêm vào bảng\",\n    \"groupBy\": \"Nhóm theo\",\n    \"groupCondition\": \"Điều kiện nhóm\",\n    \"referencedBoardPrefix\": \"Xem của\",\n    \"notesTooltip\": \"Ghi chú bên trong\",\n    \"mobile\": {\n      \"editURL\": \"Chỉnh sửa URL\",\n      \"showGroup\": \"Hiển thị nhóm\",\n      \"showGroupContent\": \"Bạn có chắc chắn muốn hiển thị nhóm này trên bảng không?\",\n      \"failedToLoad\": \"Không tải được chế độ xem bảng\"\n    },\n    \"dateCondition\": {\n      \"weekOf\": \"Tuần của {} - {}\",\n      \"today\": \"Hôm nay\",\n      \"yesterday\": \"Hôm qua\",\n      \"tomorrow\": \"Ngày mai\",\n      \"lastSevenDays\": \"7 ngày qua\",\n      \"nextSevenDays\": \"7 ngày tiếp theo\",\n      \"lastThirtyDays\": \"30 ngày qua\",\n      \"nextThirtyDays\": \"30 ngày tiếp theo\"\n    },\n    \"noGroup\": \"Không có nhóm theo tài sản\",\n    \"noGroupDesc\": \"Các chế độ xem bảng yêu cầu một thuộc tính để nhóm theo để hiển thị\",\n    \"media\": {\n      \"cardText\": \"{} {}\",\n      \"fallbackName\": \"tập tin\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"Lịch\",\n    \"defaultNewCalendarTitle\": \"Trống\",\n    \"newEventButtonTooltip\": \"Thêm sự kiện mới\",\n    \"navigation\": {\n      \"today\": \"Hôm nay\",\n      \"jumpToday\": \"Nhảy tới Hôm nay\",\n      \"previousMonth\": \"Tháng trước\",\n      \"nextMonth\": \"Tháng sau\",\n      \"views\": {\n        \"day\": \"Ngày\",\n        \"week\": \"Tuần\",\n        \"month\": \"Tháng\",\n        \"year\": \"Năm\"\n      }\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"Chưa có sự kiện nào\",\n      \"emptyBody\": \"Nhấn nút dấu cộng để tạo sự kiện vào ngày này.\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"Hiện thứ tự của tuần\",\n      \"showWeekends\": \"Hiện cuối tuần\",\n      \"firstDayOfWeek\": \"Ngày bắt đầu trong tuần\",\n      \"layoutDateField\": \"Bố trí lịch theo\",\n      \"changeLayoutDateField\": \"Thay đổi trường bố trí\",\n      \"noDateTitle\": \"Không có ngày\",\n      \"noDateHint\": {\n        \"zero\": \"Các sự kiện không theo lịch trình sẽ hiển thị ở đây\",\n        \"one\": \"{count} sự kiện không theo lịch trình\",\n        \"other\": \"{count} sự kiện không theo lịch trình\"\n      },\n      \"unscheduledEventsTitle\": \"Sự kiện không theo lịch trình\",\n      \"clickToAdd\": \"Ấn để thêm lịch\",\n      \"name\": \"Cài đặt lịch\",\n      \"clickToOpen\": \"Nhấp để mở hồ sơ\"\n    },\n    \"referencedCalendarPrefix\": \"Xem của\",\n    \"quickJumpYear\": \"Nhảy tới\",\n    \"duplicateEvent\": \"Sự kiện trùng lặp\"\n  },\n  \"errorDialog\": {\n    \"title\": \"Lỗi của @:appName\",\n    \"howToFixFallback\": \"Chúng tôi xin lỗi vì sự cố này! Vui lòng mở sự cố trên GitHub để báo lỗi.\",\n    \"howToFixFallbackHint1\": \"Chúng tôi xin lỗi vì sự bất tiện này! Gửi một vấn đề trên \",\n    \"howToFixFallbackHint2\": \" trang mô tả lỗi của bạn.\",\n    \"github\": \"Xem trên GitHub\"\n  },\n  \"search\": {\n    \"label\": \"Tìm kiếm\",\n    \"sidebarSearchIcon\": \"Tìm kiếm và nhanh chóng chuyển đến một trang\",\n    \"placeholder\": {\n      \"actions\": \"Hành động tìm kiếm...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"Đã sao chép!\",\n      \"fail\": \"Không thể sao chép\"\n    }\n  },\n  \"unSupportBlock\": \"Phiên bản hiện tại không hỗ trợ Block này.\",\n  \"views\": {\n    \"deleteContentTitle\": \"Bạn chắc chắn muốn xoá {pageType}?\",\n    \"deleteContentCaption\": \"Nếu bạn xoá {pageType}, bạn có thể phục hồi từ thùng rác.\"\n  },\n  \"colors\": {\n    \"custom\": \"Tuỳ chỉnh\",\n    \"default\": \"Mặc định\",\n    \"red\": \"Đỏ\",\n    \"orange\": \"Cam\",\n    \"yellow\": \"Vàng\",\n    \"green\": \"Xanh lá cây\",\n    \"blue\": \"Xanh dương\",\n    \"purple\": \"Tím\",\n    \"pink\": \"Hồng\",\n    \"brown\": \"Nâu\",\n    \"gray\": \"Xám\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"Biểu tượng cảm xúc\",\n    \"search\": \"Tìm kiếm biểu tượng\",\n    \"noRecent\": \"Không có biểu tượng cảm xúc gần đây\",\n    \"noEmojiFound\": \"Không tìm thấy biểu tượng\",\n    \"filter\": \"Bộ lọc\",\n    \"random\": \"Ngẫu nhiên\",\n    \"selectSkinTone\": \"Chọn tông màu da\",\n    \"remove\": \"Loại bỏ biểu tượng\",\n    \"categories\": {\n      \"smileys\": \"Biểu tượng mặt cười & Cảm xúc\",\n      \"people\": \"Con người và cơ thể\",\n      \"animals\": \"Động vật và thiên nhiên\",\n      \"food\": \"Đồ ăn và thức uống\",\n      \"activities\": \"Hoạt động\",\n      \"places\": \"Du lịch và địa điểm\",\n      \"objects\": \"Vật thể\",\n      \"symbols\": \"Biểu tượng\",\n      \"flags\": \"Cờ\",\n      \"nature\": \"Tự nhiên\",\n      \"frequentlyUsed\": \"Thường dùng\"\n    },\n    \"skinTone\": {\n      \"default\": \"Mặc định\",\n      \"light\": \"Sáng\",\n      \"mediumLight\": \"Sáng-Vừa\",\n      \"medium\": \"Vừa\",\n      \"mediumDark\": \"Tối-Vừa\",\n      \"dark\": \"Tối\"\n    },\n    \"openSourceIconsFrom\": \"Biểu tượng nguồn mở từ\"\n  },\n  \"inlineActions\": {\n    \"noResults\": \"Không có kết quả\",\n    \"recentPages\": \"Các trang gần đây\",\n    \"pageReference\": \"Trang tham khảo\",\n    \"docReference\": \"Tài liệu tham khảo\",\n    \"boardReference\": \"Tham khảo bảng\",\n    \"calReference\": \"Tham khảo lịch\",\n    \"gridReference\": \"Tham chiếu lưới\",\n    \"date\": \"Ngày\",\n    \"reminder\": {\n      \"groupTitle\": \"Lời nhắc\",\n      \"shortKeyword\": \"nhắc nhở\"\n    }\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"Thay đổi định dạng ngày và giờ trong cài đặt\",\n    \"dateFormat\": \"Định dạng ngày tháng\",\n    \"includeTime\": \"Bao gồm thời gian\",\n    \"isRange\": \"Ngày kết thúc\",\n    \"timeFormat\": \"Định dạng thời gian\",\n    \"clearDate\": \"Ngày xóa\",\n    \"reminderLabel\": \"Lời nhắc nhở\",\n    \"selectReminder\": \"Chọn lời nhắc\",\n    \"reminderOptions\": {\n      \"none\": \"Không có\",\n      \"atTimeOfEvent\": \"Thời gian diễn ra sự kiện\",\n      \"fiveMinsBefore\": \"5 phút trước\",\n      \"tenMinsBefore\": \"10 phút trước\",\n      \"fifteenMinsBefore\": \"15 phút trước\",\n      \"thirtyMinsBefore\": \"30 phút trước\",\n      \"oneHourBefore\": \"1 giờ trước\",\n      \"twoHoursBefore\": \"2 giờ trước\",\n      \"onDayOfEvent\": \"Vào ngày diễn ra sự kiện\",\n      \"oneDayBefore\": \"1 ngày trước\",\n      \"twoDaysBefore\": \"2 ngày trước\",\n      \"oneWeekBefore\": \"1 tuần trước\",\n      \"custom\": \"Phong tục\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"Hôm qua\",\n    \"today\": \"Hôm nay\",\n    \"tomorrow\": \"Ngày mai\",\n    \"oneWeek\": \"1 tuần\"\n  },\n  \"notificationHub\": {\n    \"title\": \"Thông báo\",\n    \"mobile\": {\n      \"title\": \"Cập nhật\"\n    },\n    \"emptyTitle\": \"Đã cập nhật đầy đủ!\",\n    \"emptyBody\": \"Không có thông báo hoặc hành động nào đang chờ xử lý. Hãy tận hưởng sự bình yên.\",\n    \"tabs\": {\n      \"inbox\": \"Hộp thư đến\",\n      \"upcoming\": \"Sắp tới\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"Đánh dấu tất cả là đã đọc\",\n      \"showAll\": \"Tất cả\",\n      \"showUnreads\": \"Chưa đọc\"\n    },\n    \"filters\": {\n      \"ascending\": \"Tăng dần\",\n      \"descending\": \"Giảm dần\",\n      \"groupByDate\": \"Nhóm theo ngày\",\n      \"showUnreadsOnly\": \"Chỉ hiển thị những tin chưa đọc\",\n      \"resetToDefault\": \"Đặt lại về mặc định\"\n    },\n    \"empty\": \"Không có gì ở đây!\"\n  },\n  \"reminderNotification\": {\n    \"title\": \"Lời nhắc\",\n    \"message\": \"Hãy nhớ kiểm tra điều này trước khi bạn quên nhé!\",\n    \"tooltipDelete\": \"Xoá\",\n    \"tooltipMarkRead\": \"Đánh dấu là đã đọc\",\n    \"tooltipMarkUnread\": \"Đánh dấu là chưa đọc\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"Tìm kiếm\",\n    \"previousMatch\": \"Kết quả phía trước\",\n    \"nextMatch\": \"Kết quả tiếp theo\",\n    \"close\": \"Đóng\",\n    \"replace\": \"Thay thế\",\n    \"replaceAll\": \"Thay thế tất cả\",\n    \"noResult\": \"Không thấy kết quả\",\n    \"caseSensitive\": \"Phân biệt chữ hoa chữ thường\",\n    \"searchMore\": \"Tìm kiếm để tìm thêm kết quả\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"Chúng tôi xin lỗi\",\n    \"loadingViewError\": \"Chúng tôi đang gặp sự cố khi tải chế độ xem này. Vui lòng kiểm tra kết nối internet, làm mới ứng dụng và đừng ngần ngại liên hệ với nhóm nếu sự cố vẫn tiếp diễn.\",\n    \"syncError\": \"Dữ liệu không được đồng bộ từ thiết bị khác\",\n    \"syncErrorHint\": \"Vui lòng mở lại trang này trên thiết bị mà bạn đã chỉnh sửa lần cuối, sau đó mở lại trên thiết bị hiện tại.\",\n    \"clickToCopy\": \"Nhấp để sao chép mã lỗi\"\n  },\n  \"editor\": {\n    \"bold\": \"In đậm\",\n    \"bulletedList\": \"Danh sách có dấu đầu dòng\",\n    \"bulletedListShortForm\": \"Có dấu đầu dòng\",\n    \"checkbox\": \"Đánh dấu\",\n    \"embedCode\": \"Mã nhúng\",\n    \"heading1\": \"H1\",\n    \"heading2\": \"H2\",\n    \"heading3\": \"H3\",\n    \"highlight\": \"Điểm nổi bật\",\n    \"color\": \"Màu sắc\",\n    \"image\": \"Hình ảnh\",\n    \"date\": \"Ngày\",\n    \"page\": \"Trang\",\n    \"italic\": \"nghiêng\",\n    \"link\": \"Liên kết\",\n    \"numberedList\": \"Danh sách được đánh số\",\n    \"numberedListShortForm\": \"Đã đánh số\",\n    \"quote\": \"Trích dẫn\",\n    \"strikethrough\": \"gạch xuyên\",\n    \"text\": \"Chữ\",\n    \"underline\": \"Gạch chân\",\n    \"fontColorDefault\": \"Mặc định\",\n    \"fontColorGray\": \"Xám\",\n    \"fontColorBrown\": \"Màu nâu\",\n    \"fontColorOrange\": \"Quả cam\",\n    \"fontColorYellow\": \"Màu vàng\",\n    \"fontColorGreen\": \"Màu xanh lá\",\n    \"fontColorBlue\": \"Màu xanh da trời\",\n    \"fontColorPurple\": \"Màu tím\",\n    \"fontColorPink\": \"Hồng\",\n    \"fontColorRed\": \"Màu đỏ\",\n    \"backgroundColorDefault\": \"Nền mặc định\",\n    \"backgroundColorGray\": \"Nền xám\",\n    \"backgroundColorBrown\": \"Nền màu nâu\",\n    \"backgroundColorOrange\": \"Nền màu cam\",\n    \"backgroundColorYellow\": \"Nền vàng\",\n    \"backgroundColorGreen\": \"Nền xanh\",\n    \"backgroundColorBlue\": \"Nền xanh\",\n    \"backgroundColorPurple\": \"Nền màu tím\",\n    \"backgroundColorPink\": \"Nền màu hồng\",\n    \"backgroundColorRed\": \"Nền đỏ\",\n    \"backgroundColorLime\": \"Nền vôi\",\n    \"backgroundColorAqua\": \"Nền màu nước\",\n    \"done\": \"Xong\",\n    \"cancel\": \"Hủy bỏ\",\n    \"tint1\": \"Màu 1\",\n    \"tint2\": \"Màu 2\",\n    \"tint3\": \"Màu 3\",\n    \"tint4\": \"Màu 4\",\n    \"tint5\": \"Màu 5\",\n    \"tint6\": \"Màu 6\",\n    \"tint7\": \"Màu 7\",\n    \"tint8\": \"Màu 8\",\n    \"tint9\": \"Màu 9\",\n    \"lightLightTint1\": \"Màu tím\",\n    \"lightLightTint2\": \"Hồng\",\n    \"lightLightTint3\": \"Hồng nhạt\",\n    \"lightLightTint4\": \"Quả cam\",\n    \"lightLightTint5\": \"Màu vàng\",\n    \"lightLightTint6\": \"Chanh xanh\",\n    \"lightLightTint7\": \"Màu xanh lá\",\n    \"lightLightTint8\": \"Nước\",\n    \"lightLightTint9\": \"Màu xanh da trời\",\n    \"urlHint\": \"Địa chỉ URL\",\n    \"mobileHeading1\": \"Tiêu đề 1\",\n    \"mobileHeading2\": \"Tiêu đề 2\",\n    \"mobileHeading3\": \"Tiêu đề 3\",\n    \"textColor\": \"Màu chữ\",\n    \"backgroundColor\": \"Màu nền\",\n    \"addYourLink\": \"Thêm liên kết của bạn\",\n    \"openLink\": \"Mở liên kết\",\n    \"copyLink\": \"Sao chép liên kết\",\n    \"removeLink\": \"Xóa liên kết\",\n    \"editLink\": \"Chỉnh sửa liên kết\",\n    \"linkText\": \"Chữ\",\n    \"linkTextHint\": \"Vui lòng nhập văn bản\",\n    \"linkAddressHint\": \"Vui lòng nhập URL\",\n    \"highlightColor\": \"Màu sắc nổi bật\",\n    \"clearHighlightColor\": \"Xóa màu nổi bật\",\n    \"customColor\": \"Màu tùy chỉnh\",\n    \"hexValue\": \"Giá trị hex\",\n    \"opacity\": \"Độ mờ đục\",\n    \"resetToDefaultColor\": \"Đặt lại màu mặc định\",\n    \"ltr\": \"LTR\",\n    \"rtl\": \"RTL\",\n    \"auto\": \"Tự động\",\n    \"cut\": \"Cắt\",\n    \"copy\": \"Sao chép\",\n    \"paste\": \"Dán\",\n    \"find\": \"Tìm thấy\",\n    \"select\": \"Lựa chọn\",\n    \"selectAll\": \"Chọn tất cả\",\n    \"previousMatch\": \"Trùng khớp trước đó\",\n    \"nextMatch\": \"Trùng khớp tiếp theo\",\n    \"closeFind\": \"Đóng\",\n    \"replace\": \"Thay thế\",\n    \"replaceAll\": \"Thay thế tất cả\",\n    \"regex\": \"Biểu thức chính quy\",\n    \"caseSensitive\": \"Phân biệt chữ hoa chữ thường\",\n    \"uploadImage\": \"Tải lên hình ảnh\",\n    \"urlImage\": \"Hình ảnh URL\",\n    \"incorrectLink\": \"Liên kết không đúng\",\n    \"upload\": \"Tải lên\",\n    \"chooseImage\": \"Chọn một hình ảnh\",\n    \"loading\": \"Đang tải\",\n    \"imageLoadFailed\": \"Tải hình ảnh không thành công\",\n    \"divider\": \"Bộ chia\",\n    \"table\": \"Bàn\",\n    \"colAddBefore\": \"Thêm trước\",\n    \"rowAddBefore\": \"Thêm trước\",\n    \"colAddAfter\": \"Thêm sau\",\n    \"rowAddAfter\": \"Thêm sau\",\n    \"colRemove\": \"Xoá\",\n    \"rowRemove\": \"Xoá\",\n    \"colDuplicate\": \"Nhân bản\",\n    \"rowDuplicate\": \"Nhân bản\",\n    \"colClear\": \"Xóa nội dung\",\n    \"rowClear\": \"Xóa nội dung\",\n    \"slashPlaceHolder\": \"Nhập '/' để chèn một khối hoặc bắt đầu nhập\",\n    \"typeSomething\": \"Hãy nhập gì đó...\",\n    \"toggleListShortForm\": \"Chuyển đổi\",\n    \"quoteListShortForm\": \"Trích dẫn\",\n    \"mathEquationShortForm\": \"Công thức\",\n    \"codeBlockShortForm\": \"Mã\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"Không có trang yêu thích\",\n    \"noFavoriteHintText\": \"Vuốt trang sang trái để thêm vào mục yêu thích của bạn\",\n    \"removeFromSidebar\": \"Xóa khỏi thanh bên\",\n    \"addToSidebar\": \"Ghim vào thanh bên\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"Nhập / để chèn một khối hoặc bắt đầu nhập\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"Việc cần làm\",\n    \"bulletList\": \"Danh sách\",\n    \"numberList\": \"Danh sách\",\n    \"quote\": \"Trích dẫn\",\n    \"heading\": \"Tiêu đề {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"Biểu tượng trang\",\n    \"language\": \"Ngôn ngữ\",\n    \"font\": \"Phông chữ\",\n    \"actions\": \"Hành động\",\n    \"date\": \"Ngày\",\n    \"addField\": \"Thêm trường\",\n    \"userIcon\": \"Biểu tượng người dùng\"\n  },\n  \"noLogFiles\": \"Không có tệp nhật ký nào\",\n  \"newSettings\": {\n    \"myAccount\": {\n      \"title\": \"Tài khoản của tôi\",\n      \"subtitle\": \"Tùy chỉnh hồ sơ, quản lý bảo mật tài khoản, mở khóa AI hoặc đăng nhập vào tài khoản của bạn.\",\n      \"profileLabel\": \"Tên tài khoản &amp; Ảnh đại diện\",\n      \"profileNamePlaceholder\": \"Nhập tên của bạn\",\n      \"accountSecurity\": \"Bảo mật tài khoản\",\n      \"2FA\": \"Xác thực 2 bước\",\n      \"aiKeys\": \"Phím AI\",\n      \"accountLogin\": \"Đăng nhập tài khoản\",\n      \"updateNameError\": \"Không cập nhật được tên\",\n      \"updateIconError\": \"Không cập nhật được biểu tượng\",\n      \"deleteAccount\": {\n        \"title\": \"Xóa tài khoản\",\n        \"subtitle\": \"Xóa vĩnh viễn tài khoản và toàn bộ dữ liệu của bạn.\",\n        \"description\": \"Xóa vĩnh viễn tài khoản của bạn và xóa quyền truy cập khỏi mọi không gian làm việc.\",\n        \"deleteMyAccount\": \"Xóa tài khoản của tôi\",\n        \"dialogTitle\": \"Xóa tài khoản\",\n        \"dialogContent1\": \"Bạn có chắc chắn muốn xóa vĩnh viễn tài khoản của mình không?\",\n        \"dialogContent2\": \"Không thể hoàn tác hành động này và sẽ xóa quyền truy cập khỏi mọi không gian làm việc nhóm, xóa toàn bộ tài khoản của bạn, bao gồm cả không gian làm việc riêng tư và xóa bạn khỏi mọi không gian làm việc được chia sẻ.\",\n        \"confirmHint1\": \"Vui lòng nhập \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\" để xác nhận.\",\n        \"confirmHint2\": \"Tôi hiểu rằng hành động này là không thể đảo ngược và sẽ xóa vĩnh viễn tài khoản của tôi cùng mọi dữ liệu liên quan.\",\n        \"confirmHint3\": \"XÓA TÀI KHOẢN CỦA TÔI\",\n        \"checkToConfirmError\": \"Bạn phải đánh dấu vào ô để xác nhận việc xóa\",\n        \"failedToGetCurrentUser\": \"Không lấy được email người dùng hiện tại\",\n        \"confirmTextValidationFailed\": \"Văn bản xác nhận của bạn không khớp với \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\"\",\n        \"deleteAccountSuccess\": \"Tài khoản đã được xóa thành công\"\n      }\n    },\n    \"workplace\": {\n      \"name\": \"Nơi làm việc\",\n      \"title\": \"Cài đặt nơi làm việc\",\n      \"subtitle\": \"Tùy chỉnh giao diện không gian làm việc, chủ đề, phông chữ, bố cục văn bản, ngày, giờ và ngôn ngữ.\",\n      \"workplaceName\": \"Tên nơi làm việc\",\n      \"workplaceNamePlaceholder\": \"Nhập tên nơi làm việc\",\n      \"workplaceIcon\": \"Biểu tượng nơi làm việc\",\n      \"workplaceIconSubtitle\": \"Tải lên hình ảnh hoặc sử dụng biểu tượng cảm xúc cho không gian làm việc của bạn. Biểu tượng sẽ hiển thị trên thanh bên và thông báo của bạn.\",\n      \"renameError\": \"Không đổi được tên nơi làm việc\",\n      \"updateIconError\": \"Không cập nhật được biểu tượng\",\n      \"chooseAnIcon\": \"Chọn một biểu tượng\",\n      \"appearance\": {\n        \"name\": \"Vẻ bề ngoài\",\n        \"themeMode\": {\n          \"auto\": \"Tự động\",\n          \"light\": \"Ánh sáng\",\n          \"dark\": \"Tối tăm\"\n        },\n        \"language\": \"Ngôn ngữ\"\n      }\n    },\n    \"syncState\": {\n      \"syncing\": \"Đồng bộ hóa\",\n      \"synced\": \"Đã đồng bộ\",\n      \"noNetworkConnected\": \"Không có kết nối mạng\"\n    }\n  },\n  \"pageStyle\": {\n    \"title\": \"Kiểu trang\",\n    \"layout\": \"Cách trình bày\",\n    \"coverImage\": \"Ảnh bìa\",\n    \"pageIcon\": \"Biểu tượng trang\",\n    \"colors\": \"Màu sắc\",\n    \"gradient\": \"Độ dốc\",\n    \"backgroundImage\": \"Hình nền\",\n    \"presets\": \"Cài đặt trước\",\n    \"photo\": \"Ảnh\",\n    \"unsplash\": \"Bỏ qua\",\n    \"pageCover\": \"Trang bìa\",\n    \"none\": \"Không có\",\n    \"openSettings\": \"Mở Cài đặt\",\n    \"photoPermissionTitle\": \"@:appName muốn truy cập vào thư viện ảnh của bạn\",\n    \"photoPermissionDescription\": \"Cho phép truy cập vào thư viện ảnh để tải ảnh lên.\",\n    \"doNotAllow\": \"Không cho phép\",\n    \"image\": \"Hình ảnh\"\n  },\n  \"commandPalette\": {\n    \"placeholder\": \"Nhập để tìm kiếm...\",\n    \"bestMatches\": \"Trận đấu hay nhất\",\n    \"recentHistory\": \"Lịch sử gần đây\",\n    \"navigateHint\": \"để điều hướng\",\n    \"loadingTooltip\": \"Chúng tôi đang tìm kiếm kết quả...\",\n    \"betaLabel\": \"BETA\",\n    \"betaTooltip\": \"Hiện tại chúng tôi chỉ hỗ trợ tìm kiếm các trang và nội dung trong tài liệu\",\n    \"fromTrashHint\": \"Từ rác\",\n    \"noResultsHint\": \"Chúng tôi không tìm thấy những gì bạn đang tìm kiếm, hãy thử tìm kiếm thuật ngữ khác.\",\n    \"clearSearchTooltip\": \"Xóa trường tìm kiếm\"\n  },\n  \"space\": {\n    \"delete\": \"Xóa bỏ\",\n    \"deleteConfirmation\": \"Xóa bỏ: \",\n    \"deleteConfirmationDescription\": \"Tất cả các trang trong Không gian này sẽ bị xóa và chuyển vào Thùng rác, và bất kỳ trang nào đã xuất bản sẽ bị hủy xuất bản.\",\n    \"rename\": \"Đổi tên không gian\",\n    \"changeIcon\": \"Thay đổi biểu tượng\",\n    \"manage\": \"Quản lý không gian\",\n    \"addNewSpace\": \"Tạo không gian\",\n    \"collapseAllSubPages\": \"Thu gọn tất cả các trang con\",\n    \"createNewSpace\": \"Tạo không gian mới\",\n    \"createSpaceDescription\": \"Tạo nhiều không gian công cộng và riêng tư để sắp xếp công việc tốt hơn.\",\n    \"spaceName\": \"Tên không gian\",\n    \"spaceNamePlaceholder\": \"ví dụ Marketing, Kỹ thuật, Nhân sự\",\n    \"permission\": \"Sự cho phép\",\n    \"publicPermission\": \"Công cộng\",\n    \"publicPermissionDescription\": \"Tất cả các thành viên không gian làm việc có quyền truy cập đầy đủ\",\n    \"privatePermission\": \"Riêng tư\",\n    \"privatePermissionDescription\": \"Chỉ bạn mới có thể truy cập vào không gian này\",\n    \"spaceIconBackground\": \"Màu nền\",\n    \"spaceIcon\": \"Biểu tượng\",\n    \"dangerZone\": \"Khu vực nguy hiểm\",\n    \"unableToDeleteLastSpace\": \"Không thể xóa khoảng trắng cuối cùng\",\n    \"unableToDeleteSpaceNotCreatedByYou\": \"Không thể xóa các Không gian do người khác tạo\",\n    \"enableSpacesForYourWorkspace\": \"Bật Spaces cho không gian làm việc của bạn\",\n    \"title\": \"Khoảng cách\",\n    \"defaultSpaceName\": \"Tổng quan\",\n    \"upgradeSpaceTitle\": \"Kích hoạt Khoảng cách\",\n    \"upgradeSpaceDescription\": \"Tạo nhiều Không gian công cộng và riêng tư để sắp xếp không gian làm việc của bạn tốt hơn.\",\n    \"upgrade\": \"Cập nhật\",\n    \"upgradeYourSpace\": \"Tạo nhiều khoảng trống\",\n    \"quicklySwitch\": \"Nhanh chóng chuyển sang không gian tiếp theo\",\n    \"duplicate\": \"Không gian trùng lặp\",\n    \"movePageToSpace\": \"Di chuyển trang đến khoảng trống\",\n    \"switchSpace\": \"Chuyển đổi không gian\",\n    \"spaceNameCannotBeEmpty\": \"Tên khoảng trắng không được để trống\"\n  },\n  \"publish\": {\n    \"hasNotBeenPublished\": \"Trang này chưa được xuất bản\",\n    \"spaceHasNotBeenPublished\": \"Chưa hỗ trợ xuất bản không gian\",\n    \"reportPage\": \"Báo cáo trang\",\n    \"databaseHasNotBeenPublished\": \"Việc xuất bản cơ sở dữ liệu hiện chưa được hỗ trợ.\",\n    \"createdWith\": \"Được tạo ra với\",\n    \"downloadApp\": \"Tải AppFlowy\",\n    \"copy\": {\n      \"codeBlock\": \"Nội dung của khối mã đã được sao chép vào bảng tạm\",\n      \"imageBlock\": \"Liên kết hình ảnh đã được sao chép vào clipboard\",\n      \"mathBlock\": \"Phương trình toán học đã được sao chép vào clipboard\",\n      \"fileBlock\": \"Liên kết tập tin đã được sao chép vào clipboard\"\n    },\n    \"containsPublishedPage\": \"Trang này chứa một hoặc nhiều trang đã xuất bản. Nếu bạn tiếp tục, chúng sẽ bị hủy xuất bản. Bạn có muốn tiếp tục xóa không?\",\n    \"publishSuccessfully\": \"Đã xuất bản thành công\",\n    \"unpublishSuccessfully\": \"Đã hủy xuất bản thành công\",\n    \"publishFailed\": \"Không thể xuất bản\",\n    \"unpublishFailed\": \"Không thể hủy xuất bản\",\n    \"noAccessToVisit\": \"Không thể truy cập vào trang này...\",\n    \"createWithAppFlowy\": \"Tạo trang web với AppFlowy\",\n    \"fastWithAI\": \"Nhanh chóng và dễ dàng với AI.\",\n    \"tryItNow\": \"Thử ngay bây giờ\",\n    \"onlyGridViewCanBePublished\": \"Chỉ có thể xuất bản chế độ xem Lưới\",\n    \"database\": {\n      \"zero\": \"Xuất bản {} chế độ xem đã chọn\",\n      \"one\": \"Xuất bản {} chế độ xem đã chọn\",\n      \"many\": \"Xuất bản {} chế độ xem đã chọn\",\n      \"other\": \"Xuất bản {} chế độ xem đã chọn\"\n    },\n    \"mustSelectPrimaryDatabase\": \"Phải chọn chế độ xem chính\",\n    \"noDatabaseSelected\": \"Chưa chọn cơ sở dữ liệu, vui lòng chọn ít nhất một cơ sở dữ liệu.\",\n    \"unableToDeselectPrimaryDatabase\": \"Không thể bỏ chọn cơ sở dữ liệu chính\",\n    \"saveThisPage\": \"Lưu trang này\",\n    \"duplicateTitle\": \"Bạn muốn thêm vào đâu?\",\n    \"selectWorkspace\": \"Chọn không gian làm việc\",\n    \"addTo\": \"Thêm vào\",\n    \"duplicateSuccessfully\": \"Tạo bản sao thành công. Bạn muốn xem tài liệu?\",\n    \"duplicateSuccessfullyDescription\": \"Bạn không có ứng dụng? Quá trình tải xuống của bạn sẽ tự động bắt đầu sau khi nhấp vào 'Tải xuống'.\",\n    \"downloadIt\": \"Tải về\",\n    \"openApp\": \"Mở trong ứng dụng\",\n    \"duplicateFailed\": \"Sao chép không thành công\",\n    \"membersCount\": {\n      \"zero\": \"Không có thành viên\",\n      \"one\": \"1 thành viên\",\n      \"many\": \"{đếm} thành viên\",\n      \"other\": \"{đếm} thành viên\"\n    },\n    \"useThisTemplate\": \"Sử dụng mẫu\"\n  },\n  \"web\": {\n    \"continue\": \"Tiếp tục\",\n    \"or\": \"hoặc\",\n    \"continueWithGoogle\": \"Tiếp tục với Google\",\n    \"continueWithGithub\": \"Tiếp tục với GitHub\",\n    \"continueWithDiscord\": \"Tiếp tục với Discord\",\n    \"continueWithApple\": \"Tiếp tục với Apple \",\n    \"moreOptions\": \"Thêm tùy chọn\",\n    \"collapse\": \"Thu gọn\",\n    \"signInAgreement\": \"Bằng cách nhấp vào \\\"Tiếp tục\\\" ở trên, bạn đã đồng ý với AppFlowy\",\n    \"and\": \"và\",\n    \"termOfUse\": \"Điều khoản\",\n    \"privacyPolicy\": \"Chính sách bảo mật\",\n    \"signInError\": \"Lỗi đăng nhập\",\n    \"login\": \"Đăng ký hoặc đăng nhập\",\n    \"fileBlock\": {\n      \"uploadedAt\": \"Đã tải lên vào {time}\",\n      \"linkedAt\": \"Liên kết được thêm vào {time}\",\n      \"empty\": \"Tải lên hoặc nhúng một tập tin\"\n    }\n  },\n  \"globalComment\": {\n    \"comments\": \"Bình luận\",\n    \"addComment\": \"Thêm bình luận\",\n    \"reactedBy\": \"phản ứng bởi\",\n    \"addReaction\": \"Thêm phản ứng\",\n    \"reactedByMore\": \"và {đếm} người khác\",\n    \"showSeconds\": {\n      \"one\": \"1 giây trước\",\n      \"other\": \"{count} giây trước\",\n      \"zero\": \"Vừa xong\",\n      \"many\": \"{count} giây trước\"\n    },\n    \"showMinutes\": {\n      \"one\": \"1 phút trước\",\n      \"other\": \"{count} phút trước\",\n      \"many\": \"{count} phút trước\"\n    },\n    \"showHours\": {\n      \"one\": \"1 giờ trước\",\n      \"other\": \"{count} giờ trước\",\n      \"many\": \"{count} giờ trước\"\n    },\n    \"showDays\": {\n      \"one\": \"1 ngày trước\",\n      \"other\": \"{count} ngày trước\",\n      \"many\": \"{count} ngày trước\"\n    },\n    \"showMonths\": {\n      \"one\": \"1 tháng trước\",\n      \"other\": \"{count} tháng trước\",\n      \"many\": \"{count} tháng trước\"\n    },\n    \"showYears\": {\n      \"one\": \"1 năm trước\",\n      \"other\": \"{đếm} năm trước\",\n      \"many\": \"{đếm} năm trước\"\n    },\n    \"reply\": \"Hồi đáp\",\n    \"deleteComment\": \"Xóa bình luận\",\n    \"youAreNotOwner\": \"Bạn không phải là chủ sở hữu của bình luận này\",\n    \"confirmDeleteDescription\": \"Bạn có chắc chắn muốn xóa bình luận này không?\",\n    \"hasBeenDeleted\": \"Đã xóa\",\n    \"replyingTo\": \"Trả lời cho\",\n    \"noAccessDeleteComment\": \"Bạn không được phép xóa bình luận này\",\n    \"collapse\": \"Sụp đổ\",\n    \"readMore\": \"Đọc thêm\",\n    \"failedToAddComment\": \"Không thêm được bình luận\",\n    \"commentAddedSuccessfully\": \"Đã thêm bình luận thành công.\",\n    \"commentAddedSuccessTip\": \"Bạn vừa thêm hoặc trả lời bình luận. Bạn có muốn chuyển lên đầu trang để xem các bình luận mới nhất không?\"\n  },\n  \"template\": {\n    \"asTemplate\": \"Như mẫu\",\n    \"name\": \"Tên mẫu\",\n    \"description\": \"Mô tả mẫu\",\n    \"about\": \"Mẫu Giới Thiệu\",\n    \"preview\": \"Bản xem trước mẫu\",\n    \"categories\": \"Danh mục mẫu\",\n    \"isNewTemplate\": \"PIN vào mẫu mới\",\n    \"featured\": \"PIN vào mục Nổi bật\",\n    \"relatedTemplates\": \"Mẫu liên quan\",\n    \"requiredField\": \"{field} là bắt buộc\",\n    \"addCategory\": \"Thêm \\\"{category}\\\"\",\n    \"addNewCategory\": \"Thêm danh mục mới\",\n    \"addNewCreator\": \"Thêm người sáng tạo mới\",\n    \"deleteCategory\": \"Xóa danh mục\",\n    \"editCategory\": \"Chỉnh sửa danh mục\",\n    \"editCreator\": \"Chỉnh sửa người tạo\",\n    \"category\": {\n      \"name\": \"Tên danh mục\",\n      \"icon\": \"Biểu tượng danh mục\",\n      \"bgColor\": \"Màu nền danh mục\",\n      \"priority\": \"Ưu tiên danh mục\",\n      \"desc\": \"Mô tả danh mục\",\n      \"type\": \"Loại danh mục\",\n      \"icons\": \"Biểu tượng danh mục\",\n      \"colors\": \"Thể loại Màu sắc\",\n      \"byUseCase\": \"Theo trường hợp sử dụng\",\n      \"byFeature\": \"Theo tính năng\",\n      \"deleteCategory\": \"Xóa danh mục\",\n      \"deleteCategoryDescription\": \"Bạn có chắc chắn muốn xóa danh mục này không?\",\n      \"typeToSearch\": \"Nhập để tìm kiếm theo danh mục...\"\n    },\n    \"creator\": {\n      \"label\": \"Người tạo mẫu\",\n      \"name\": \"Tên người sáng tạo\",\n      \"avatar\": \"Avatar của người sáng tạo\",\n      \"accountLinks\": \"Liên kết tài khoản người sáng tạo\",\n      \"uploadAvatar\": \"Nhấp để tải lên hình đại diện\",\n      \"deleteCreator\": \"Xóa người tạo\",\n      \"deleteCreatorDescription\": \"Bạn có chắc chắn muốn xóa người sáng tạo này không?\",\n      \"typeToSearch\": \"Nhập để tìm kiếm người sáng tạo...\"\n    },\n    \"uploadSuccess\": \"Mẫu đã được tải lên thành công\",\n    \"uploadSuccessDescription\": \"Mẫu của bạn đã được tải lên thành công. Bây giờ bạn có thể xem nó trong thư viện mẫu.\",\n    \"viewTemplate\": \"Xem mẫu\",\n    \"deleteTemplate\": \"Xóa mẫu\",\n    \"deleteTemplateDescription\": \"Bạn có chắc chắn muốn xóa mẫu này không?\",\n    \"addRelatedTemplate\": \"Thêm mẫu liên quan\",\n    \"removeRelatedTemplate\": \"Xóa mẫu liên quan\",\n    \"uploadAvatar\": \"Tải lên hình đại diện\",\n    \"searchInCategory\": \"Tìm kiếm trong {category}\",\n    \"label\": \"Bản mẫu\"\n  },\n  \"fileDropzone\": {\n    \"dropFile\": \"Nhấp hoặc kéo tệp vào khu vực này để tải lên\",\n    \"uploading\": \"Đang tải lên...\",\n    \"uploadFailed\": \"Tải lên không thành công\",\n    \"uploadSuccess\": \"Tải lên thành công\",\n    \"uploadSuccessDescription\": \"Tệp đã được tải lên thành công\",\n    \"uploadFailedDescription\": \"Tải tệp lên không thành công\",\n    \"uploadingDescription\": \"Tập tin đang được tải lên\"\n  },\n  \"gallery\": {\n    \"preview\": \"Mở toàn màn hình\",\n    \"copy\": \"Sao chép\",\n    \"download\": \"Tải về\",\n    \"prev\": \"Trước\",\n    \"next\": \"Kế tiếp\",\n    \"resetZoom\": \"Đặt lại chế độ thu phóng\",\n    \"zoomIn\": \"Phóng to\",\n    \"zoomOut\": \"Thu nhỏ\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/vi.json",
    "content": "{\n  \"board\": {\n    \"mobile\": {\n      \"showGroup\": \"Hiển thị nhóm\",\n      \"showGroupContent\": \"Bạn có chắc chắn muốn hiển thị nhóm này trên bảng không?\",\n      \"failedToLoad\": \"Không tải được chế độ xem bảng\"\n    }\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/zh-CN.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"我\",\n  \"welcomeText\": \"欢迎使用 @:appName\",\n  \"welcomeTo\": \"欢迎来到\",\n  \"githubStarText\": \"在 GitHub 上 Star\",\n  \"subscribeNewsletterText\": \"消息订阅\",\n  \"letsGoButtonText\": \"开始\",\n  \"title\": \"标题\",\n  \"youCanAlso\": \"您还可以\",\n  \"and\": \"以及\",\n  \"failedToOpenUrl\": \"打开 url:{}失败\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"点击下方添加\",\n    \"addAboveCmd\": \"Alt+单击\",\n    \"addAboveMacCmd\": \"+单击\",\n    \"addAboveTooltip\": \"添加上面\",\n    \"dragTooltip\": \"拖拽以移动\",\n    \"openMenuTooltip\": \"点击打开菜单\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"注册\",\n    \"title\": \"注册 @:appName 账户\",\n    \"getStartedText\": \"开始\",\n    \"emptyPasswordError\": \"密码不能为空\",\n    \"repeatPasswordEmptyError\": \"确认密码不能为空\",\n    \"unmatchedPasswordError\": \"两次密码输入不一致\",\n    \"alreadyHaveAnAccount\": \"已有账户?\",\n    \"emailHint\": \"邮箱\",\n    \"passwordHint\": \"密码\",\n    \"repeatPasswordHint\": \"确认密码\",\n    \"signUpWith\": \"注册方式：\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"登录 @:appName\",\n    \"loginButtonText\": \"登录\",\n    \"loginStartWithAnonymous\": \"从匿名会话开始\",\n    \"continueAnonymousUser\": \"继续匿名会话\",\n    \"buttonText\": \"登录\",\n    \"signingInText\": \"登录中...\",\n    \"forgotPassword\": \"忘记密码?\",\n    \"emailHint\": \"邮箱\",\n    \"passwordHint\": \"密码\",\n    \"dontHaveAnAccount\": \"没有账户?\",\n    \"createAccount\": \"新建账户\",\n    \"repeatPasswordEmptyError\": \"确认密码不能为空\",\n    \"unmatchedPasswordError\": \"两次密码输入不一致\",\n    \"syncPromptMessage\": \"同步数据可能需要一段时间，请不要关闭此页面\",\n    \"or\": \"或\",\n    \"signInWithGoogle\": \"使用 Google 账户登录\",\n    \"signInWithGithub\": \"使用 Github 账户登录\",\n    \"signInWithDiscord\": \"使用 Discord 账户登录\",\n    \"signInWithApple\": \"使用 Apple 账户登录\",\n    \"continueAnotherWay\": \"使用其他方式登录\",\n    \"signUpWithGoogle\": \"使用 Google 账户注册\",\n    \"signUpWithGithub\": \"使用 Github 账户注册\",\n    \"signUpWithDiscord\": \"使用 Discord 账户注册\",\n    \"signInWith\": \"用其他方式登入：\",\n    \"signInWithEmail\": \"使用邮箱登录\",\n    \"signInWithMagicLink\": \"使用魔法链接登录\",\n    \"signUpWithMagicLink\": \"使用魔法链接注册\",\n    \"pleaseInputYourEmail\": \"请输入邮箱地址\",\n    \"settings\": \"设置\",\n    \"magicLinkSent\": \"魔法链接已经发送到您的邮箱，请检查！\",\n    \"invalidEmail\": \"请输入一个有效的邮箱地址\",\n    \"alreadyHaveAnAccount\": \"已经有账户了？\",\n    \"logIn\": \"登陆\",\n    \"generalError\": \"出现错误，请稍后再试\",\n    \"limitRateError\": \"出于安全原因，每60秒仅可以发送一次链接\",\n    \"magicLinkSentDescription\": \"一个验证链接已发送到您的电子邮箱。点击该链接即可完成登录。该链接将在 5 分钟后失效。\",\n    \"anonymous\": \"匿名\",\n    \"LogInWithGoogle\": \"使用 Google 登录\",\n    \"LogInWithGithub\": \"使用 Github 登录\",\n    \"LogInWithDiscord\": \"使用 Discord 登录\",\n    \"loginAsGuestButtonText\": \"开始使用\",\n    \"logInWithMagicLink\": \"使用魔法链接登录\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"选择您的工作区\",\n    \"defaultName\": \"我的工作区\",\n    \"create\": \"新建工作区\",\n    \"new\": \"新的工作空间\",\n    \"importFromNotion\": \"从 Notion 导入\",\n    \"learnMore\": \"了解更多\",\n    \"reset\": \"重置工作区\",\n    \"renameWorkspace\": \"重命名工作区\",\n    \"workspaceNameCannotBeEmpty\": \"工作区名称不可为空\",\n    \"resetWorkspacePrompt\": \"重置工作区将删除其中的所有页面和数据。您确定要重置工作区吗？您也可以联系技术支持团队来恢复工作区\",\n    \"hint\": \"工作区\",\n    \"notFoundError\": \"找不到工作区\",\n    \"failedToLoad\": \"出了些问题！我们无法加载工作区。请尝试关闭所有打开的 @:appName 实例，然后重试。\",\n    \"errorActions\": {\n      \"reportIssue\": \"上报问题\",\n      \"reportIssueOnGithub\": \"在 Github 上报告问题\",\n      \"exportLogFiles\": \"导出日志文件\",\n      \"reachOut\": \"在 Discord 联系我们\"\n    },\n    \"menuTitle\": \"工作区\",\n    \"deleteWorkspaceHintText\": \"您确定要删除这个工作区嘛？这个操作不可恢复。\",\n    \"createSuccess\": \"工作区创建成功\",\n    \"createFailed\": \"工作区创建失败\",\n    \"createLimitExceeded\": \"您已达到账户允许的最大工作空间限制。如果您需要额外的工作空间来继续工作，请在 Github 上申请\",\n    \"deleteSuccess\": \"工作区删除成功\",\n    \"deleteFailed\": \"工作区删除失败\",\n    \"openSuccess\": \"打开工作区成功\",\n    \"openFailed\": \"打开工作区失败\",\n    \"renameSuccess\": \"工作区已成功重命名\",\n    \"renameFailed\": \"工作区重命名失败\",\n    \"updateIconSuccess\": \"工作区图标已修改\",\n    \"updateIconFailed\": \"修改工作区图标失败\",\n    \"cannotDeleteTheOnlyWorkspace\": \"不能删除仅存的工作区\",\n    \"fetchWorkspacesFailed\": \"获取工作区失败\",\n    \"leaveCurrentWorkspace\": \"退出工作区\",\n    \"leaveCurrentWorkspacePrompt\": \"您确定要离开当前工作区吗？\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"分享\",\n    \"workInProgress\": \"敬请期待\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"拷贝到剪贴板\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"复制链接\",\n    \"publishToTheWeb\": \"发布至网络\",\n    \"publishToTheWebHint\": \"利用AppFlowy创建一个网站\",\n    \"publish\": \"发布\",\n    \"unPublish\": \"取消发布\",\n    \"visitSite\": \"访问网站\",\n    \"exportAsTab\": \"导出为\",\n    \"publishTab\": \"发布\",\n    \"shareTab\": \"分享\",\n    \"publishOnAppFlowy\": \"在 AppFlowy 发布\",\n    \"shareTabTitle\": \"邀请他人来协作\",\n    \"shareTabDescription\": \"与任何人轻松协作\",\n    \"copyLinkSuccess\": \"链接已复制到剪贴板\",\n    \"copyShareLink\": \"复制分享链接\",\n    \"copyLinkFailed\": \"无法将链接复制到剪贴板\",\n    \"copyLinkToBlockSuccess\": \"区块链接已复制到剪贴板\",\n    \"copyLinkToBlockFailed\": \"无法将区块链接复制到剪贴板\",\n    \"manageAllSites\": \"管理所有站点\",\n    \"updatePathName\": \"更新路径名称\"\n  },\n  \"moreAction\": {\n    \"small\": \"小\",\n    \"medium\": \"中\",\n    \"large\": \"大\",\n    \"fontSize\": \"字号\",\n    \"import\": \"导入\",\n    \"moreOptions\": \"更多选项\",\n    \"wordCount\": \"字数:{}\",\n    \"charCount\": \"字符数:{}\",\n    \"createdAt\": \"创建于:{}\",\n    \"deleteView\": \"删除\",\n    \"duplicateView\": \"复制\",\n    \"wordCountLabel\": \"词总数:\",\n    \"charCountLabel\": \"字符总数:\",\n    \"createdAtLabel\": \"已创建的:\",\n    \"syncedAtLabel\": \"已同步的:\",\n    \"saveAsNewPage\": \"在页面中添加消息\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"文本 和 Markdown\",\n    \"documentFromV010\": \"来自 v0.1.0 的文档\",\n    \"databaseFromV010\": \"来自 v0.1.0 的数据库\",\n    \"notionZip\": \"Notion 导出的 Zip 文件\",\n    \"csv\": \"CSV\",\n    \"database\": \"数据库\"\n  },\n  \"disclosureAction\": {\n    \"rename\": \"重命名\",\n    \"delete\": \"删除\",\n    \"duplicate\": \"复制\",\n    \"unfavorite\": \"从收藏夹中删除\",\n    \"favorite\": \"添加到收藏夹\",\n    \"openNewTab\": \"在新选项卡中打开\",\n    \"moveTo\": \"移动\",\n    \"addToFavorites\": \"添加到收藏夹\",\n    \"copyLink\": \"复制链接\",\n    \"changeIcon\": \"更改图标\",\n    \"collapseAllPages\": \"收起全部子页面\",\n    \"movePageTo\": \"将页面移动至\",\n    \"move\": \"移动\",\n    \"lockPage\": \"锁定页面\"\n  },\n  \"blankPageTitle\": \"空白页\",\n  \"newPageText\": \"新页面\",\n  \"newDocumentText\": \"新文件\",\n  \"newGridText\": \"新建网格\",\n  \"newCalendarText\": \"新日历\",\n  \"newBoardText\": \"新建看板\",\n  \"chat\": {\n    \"newChat\": \"AI 对话\",\n    \"inputMessageHint\": \"问 @:appName AI\",\n    \"inputLocalAIMessageHint\": \"问 @:appName 本地 AI\",\n    \"unsupportedCloudPrompt\": \"该功能仅在使用 @:appName Cloud 时可用\",\n    \"relatedQuestion\": \"相关问题\",\n    \"serverUnavailable\": \"服务暂时不可用，请稍后再试\",\n    \"aiServerUnavailable\": \"🌈 不妙！🌈 一只独角兽吃掉了我们的回复。请重试！\",\n    \"retry\": \"重试\",\n    \"clickToRetry\": \"点击重试\",\n    \"regenerateAnswer\": \"重新生成\",\n    \"question1\": \"如何使用 Kanban 来管理任务\",\n    \"question2\": \"介绍一下 GTD 工作法\",\n    \"question3\": \"为什么使用 Rust\",\n    \"question4\": \"使用现有食材制定一份菜谱\",\n    \"aiMistakePrompt\": \"AI 可能出错，请验证重要信息\",\n    \"chatWithFilePrompt\": \"你想与文件对话吗？\",\n    \"indexFileSuccess\": \"文件索引成功\",\n    \"inputActionNoPages\": \"无页面结果\",\n    \"clickToMention\": \"点击以提及页面\",\n    \"uploadFile\": \"上传聊天使用的 PDF、md 或 txt 文件\",\n    \"questionDetail\": \"{} 你好！我能怎么帮到你？\",\n    \"indexingFile\": \"正在索引 {}\",\n    \"generatingResponse\": \"正在生成相应\",\n    \"sourceUnsupported\": \"我们当前不支持基于数据库的交流\",\n    \"regenerate\": \"请重试\",\n    \"addToPageTitle\": \"添加消息至......\",\n    \"addToNewPage\": \"创建新的页面\",\n    \"openPagePreviewFailedToast\": \"打开页面失败\",\n    \"changeFormat\": {\n      \"actionButton\": \"变更样式\",\n      \"confirmButton\": \"基于该样式重新生成\",\n      \"imageOnly\": \"仅图片\",\n      \"textAndImage\": \"文本与图片\",\n      \"text\": \"段落\",\n      \"table\": \"表格\"\n    },\n    \"referenceSource\": \"找到 {} 个来源\",\n    \"referenceSources\": \"找到 {} 个来源\",\n    \"questionTitle\": \"想法\"\n  },\n  \"trash\": {\n    \"text\": \"回收站\",\n    \"restoreAll\": \"全部恢复\",\n    \"restore\": \"恢复\",\n    \"deleteAll\": \"全部删除\",\n    \"pageHeader\": {\n      \"fileName\": \"文件名\",\n      \"lastModified\": \"最近修改\",\n      \"created\": \"创建时间\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"您确定要删除回收站中的所有页面吗？\",\n      \"caption\": \"此操作无法撤消。\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"您确定要恢复回收站中的所有页面吗？\",\n      \"caption\": \"此操作无法撤消。\"\n    },\n    \"restorePage\": {\n      \"title\": \"恢复：{}\",\n      \"caption\": \"你确定要恢复此页面吗？\"\n    },\n    \"mobile\": {\n      \"actions\": \"垃圾桶操作\",\n      \"empty\": \"垃圾桶是空的\",\n      \"emptyDescription\": \"您没有任何已删除的文件\",\n      \"isDeleted\": \"已删除\",\n      \"isRestored\": \"已恢复\"\n    },\n    \"confirmDeleteTitle\": \"您确定要永久删除此页面吗？\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"此页面已被移动至垃圾桶\",\n    \"restore\": \"恢复页面\",\n    \"deletePermanent\": \"彻底删除\",\n    \"deletePermanentDescription\": \"你确定要永久删除此页面吗？此操作无法撤销。\"\n  },\n  \"dialogCreatePageNameHint\": \"页面名称\",\n  \"questionBubble\": {\n    \"shortcuts\": \"快捷键\",\n    \"whatsNew\": \"新功能\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"调试信息\",\n      \"success\": \"已将调试信息复制到剪贴板！\",\n      \"fail\": \"无法将调试信息复制到剪贴板\"\n    },\n    \"feedback\": \"反馈\",\n    \"help\": \"帮助和支持\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"更多按钮工具\",\n    \"addPageTooltip\": \"在其中快速添加页面\",\n    \"defaultNewPageName\": \"未命名页面\",\n    \"renameDialog\": \"重命名\",\n    \"pageNameSuffix\": \"复制\"\n  },\n  \"noPagesInside\": \"里面没有页面\",\n  \"toolbar\": {\n    \"undo\": \"撤销\",\n    \"redo\": \"恢复\",\n    \"bold\": \"加粗\",\n    \"italic\": \"斜体\",\n    \"underline\": \"下划线\",\n    \"strike\": \"删除线\",\n    \"numList\": \"有序列表\",\n    \"bulletList\": \"无序列表\",\n    \"checkList\": \"任务列表\",\n    \"inlineCode\": \"内联代码\",\n    \"quote\": \"块引用\",\n    \"header\": \"标题\",\n    \"highlight\": \"高亮\",\n    \"color\": \"颜色\",\n    \"addLink\": \"添加链接\",\n    \"link\": \"关联\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"切换到亮色模式\",\n    \"darkMode\": \"切换到暗色模式\",\n    \"openAsPage\": \"作为页面打开\",\n    \"addNewRow\": \"添加一行\",\n    \"openMenu\": \"点击打开菜单\",\n    \"dragRow\": \"拖动来调整行\",\n    \"viewDataBase\": \"查看数据库\",\n    \"referencePage\": \"这个 {name} 正在被引用\",\n    \"addBlockBelow\": \"在下面添加一个块\",\n    \"aiGenerate\": \"生成\",\n    \"urlLaunchAccessory\": \"在浏览器中打开\",\n    \"urlCopyAccessory\": \"复制链接\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"关闭侧边栏\",\n    \"openSidebar\": \"打开侧边栏\",\n    \"personal\": \"个人的\",\n    \"private\": \"私人的\",\n    \"workspace\": \"工作区\",\n    \"favorites\": \"收藏夹\",\n    \"clickToHidePrivate\": \"点击以隐藏私人空间\\n您在此处创建的页面仅对您可见\",\n    \"clickToHideWorkspace\": \"点击以隐藏私人空间\\n您在此处创建的页面对所有人可见\",\n    \"clickToHidePersonal\": \"点击隐藏个人部分\",\n    \"clickToHideFavorites\": \"单击隐藏收藏夹栏目\",\n    \"addAPage\": \"添加页面\",\n    \"addAPageToPrivate\": \"添加页面到私人空间\",\n    \"addAPageToWorkspace\": \"添加页面到工作空间\",\n    \"recent\": \"最近的\",\n    \"today\": \"今日\",\n    \"thisWeek\": \"本周\",\n    \"others\": \"其他\",\n    \"earlier\": \"更早\",\n    \"justNow\": \"现在\",\n    \"minutesAgo\": \"{count} 分钟以前\",\n    \"lastViewed\": \"最近一次查看\",\n    \"favoriteAt\": \"已收藏\",\n    \"emptyRecent\": \"没有最近页面\",\n    \"emptyRecentDescription\": \"在你查看页面时，它们会出现在这里，方便检索。\",\n    \"emptyFavorite\": \"无收藏页面\",\n    \"emptyFavoriteDescription\": \"将页面收藏起来——它们会列在这里，方便快速访问！\",\n    \"removePageFromRecent\": \"从最近页移除此页面吗？\",\n    \"removeSuccess\": \"成功移除\",\n    \"favoriteSpace\": \"收藏\",\n    \"RecentSpace\": \"最近\",\n    \"Spaces\": \"空间\",\n    \"upgradeToPro\": \"升级至专业版\",\n    \"upgradeToAIMax\": \"解锁无限制 AI\",\n    \"storageLimitDialogTitle\": \"你已用尽免费存储。升级以解锁无限制存储\",\n    \"storageLimitDialogTitleIOS\": \"你已用尽免费存储。\",\n    \"aiResponseLimitTitle\": \"你已用尽免费 AI 回应。升级到专业版或者购买 AI 插件来解锁无限制回应\",\n    \"aiResponseLimitDialogTitle\": \"已达到 AI 回应限额\",\n    \"aiResponseLimit\": \"你已用尽了免费 AI 回应。\\n\\n转到“设置 -> 计划 -> 点击 AI Max 或 Pro 计划”获取更多 AI 回应\",\n    \"askOwnerToUpgradeToPro\": \"你的工作区即将用尽免费存储。请联系工作区所有者升级到专业版计划\",\n    \"askOwnerToUpgradeToProIOS\": \"你的工作区即将用尽免费存储。\",\n    \"askOwnerToUpgradeToAIMax\": \"你的工作区即将用尽免费 AI 回应。请联系工作区所有者升级计划或购买 AI 插件\",\n    \"askOwnerToUpgradeToAIMaxIOS\": \"你的工作区即将用尽免费 AI 回应限额。\",\n    \"aiImageResponseLimit\": \"你已消耗完你的 AI 图像响应额度。\\n转到 设置 -> 方案 -> 点击 AI Max 去获得更多的图像响应额度\",\n    \"purchaseStorageSpace\": \"购买存储空间\",\n    \"purchaseAIResponse\": \"购买\",\n    \"askOwnerToUpgradeToLocalAI\": \"联系工作区所有者启用设备上 AI\",\n    \"upgradeToAILocal\": \"在你的设备上运行本地模型，极致保护隐私\",\n    \"upgradeToAILocalDesc\": \"使用本地 AI 用 PDF 聊天、改善写作、自动填充表格\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"导出笔记为Markdown文档\",\n      \"path\": \"文档/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"联系人\",\n    \"whatsHappening\": \"这周发生了哪些事\",\n    \"addContact\": \"添加联系人\",\n    \"editContact\": \"编辑联系人\"\n  },\n  \"button\": {\n    \"ok\": \"OK\",\n    \"confirm\": \"确认\",\n    \"done\": \"完成\",\n    \"cancel\": \"取消\",\n    \"signIn\": \"登录\",\n    \"signOut\": \"登出\",\n    \"complete\": \"完成\",\n    \"save\": \"保存\",\n    \"generate\": \"生成\",\n    \"esc\": \"退出\",\n    \"keep\": \"保留\",\n    \"tryAgain\": \"重试\",\n    \"discard\": \"放弃\",\n    \"replace\": \"替换\",\n    \"insertBelow\": \"在下方插入\",\n    \"insertAbove\": \"在上方插入\",\n    \"upload\": \"上传\",\n    \"edit\": \"编辑\",\n    \"delete\": \"删除\",\n    \"copy\": \"复制\",\n    \"duplicate\": \"复制\",\n    \"putback\": \"放回去\",\n    \"update\": \"更新\",\n    \"share\": \"分享\",\n    \"removeFromFavorites\": \"从收藏夹中\",\n    \"removeFromRecent\": \"从最近页移除\",\n    \"addToFavorites\": \"添加到收藏夹\",\n    \"favoriteSuccessfully\": \"收藏成功\",\n    \"unfavoriteSuccessfully\": \"取消收藏成功\",\n    \"duplicateSuccessfully\": \"副本创建成功\",\n    \"rename\": \"重命名\",\n    \"helpCenter\": \"帮助中心\",\n    \"add\": \"添加\",\n    \"yes\": \"是\",\n    \"no\": \"否\",\n    \"clear\": \"清空\",\n    \"remove\": \"移除\",\n    \"dontRemove\": \"不移除\",\n    \"copyLink\": \"复制链接\",\n    \"align\": \"对齐\",\n    \"login\": \"登录\",\n    \"logout\": \"退出\",\n    \"deleteAccount\": \"删除账户\",\n    \"back\": \"返回\",\n    \"signInGoogle\": \"使用 Google 账户登录\",\n    \"signInGithub\": \"使用 Github 账户登录\",\n    \"signInDiscord\": \"使用 Discord 账户登录\",\n    \"more\": \"更多\",\n    \"create\": \"创建\",\n    \"close\": \"关闭\",\n    \"next\": \"下一个\",\n    \"previous\": \"上一个\",\n    \"submit\": \"提交\",\n    \"download\": \"下载\",\n    \"backToHome\": \"返回主页\",\n    \"viewing\": \"查看\",\n    \"editing\": \"编辑\",\n    \"gotIt\": \"我知道了\",\n    \"retry\": \"重试\",\n    \"uploadFailed\": \"上传失败\",\n    \"Done\": \"完成\",\n    \"Cancel\": \"取消\",\n    \"OK\": \"确认\"\n  },\n  \"label\": {\n    \"welcome\": \"欢迎!\",\n    \"firstName\": \"名\",\n    \"middleName\": \"中间名\",\n    \"lastName\": \"姓\",\n    \"stepX\": \"第{X}步\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"无法连接到您的账户。\",\n      \"failedMsg\": \"请确认您已在浏览器中完成登录。\"\n    },\n    \"google\": {\n      \"title\": \"Google 账号登录\",\n      \"instruction1\": \"为了导入您的 Google 联系人，您需要在浏览器中给予本程序授权。\",\n      \"instruction2\": \"单击图标或选择文本复制到剪贴板：\",\n      \"instruction3\": \"进入下面的链接，然后输入上面的代码：\",\n      \"instruction4\": \"完成注册后，点击下面的按钮：\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"设置\",\n    \"popupMenuItem\": {\n      \"settings\": \"设置\",\n      \"members\": \"成员\",\n      \"trash\": \"回收站\",\n      \"helpAndSupport\": \"帮助与支持\"\n    },\n    \"sites\": {\n      \"title\": \"站点\",\n      \"namespaceTitle\": \"名字空间\",\n      \"namespaceDescription\": \"管理你的名字空间与主页\",\n      \"namespaceHeader\": \"名字空间\",\n      \"homepageHeader\": \"主页\",\n      \"updateNamespace\": \"更新名字空间\",\n      \"removeHomepage\": \"移除主页\",\n      \"selectHomePage\": \"选择一个页面\",\n      \"customUrl\": \"自定义 URL\",\n      \"namespace\": {\n        \"updateExistingNamespace\": \"更新现有的名称空间\",\n        \"upgradeToPro\": \"请升级至 Pro 订阅计划以设置主页\",\n        \"redirectToPayment\": \"正在重定向至付款页面......\",\n        \"onlyWorkspaceOwnerCanSetHomePage\": \"仅工作空间的所有者能为其设置主页\",\n        \"pleaseAskOwnerToSetHomePage\": \"请联系工作空间所有者更新至 Pro 订阅计划\"\n      },\n      \"publishedPage\": {\n        \"title\": \"所有已发布页面\",\n        \"description\": \"管理您已发布的页面\",\n        \"date\": \"已发布的数据\",\n        \"emptyHinText\": \"在当前工作空间没有已发布的页面\",\n        \"noPublishedPages\": \"没有已发布的页面\",\n        \"settings\": \"发布设置\",\n        \"clickToOpenPageInApp\": \"在 App 中打开页面\",\n        \"clickToOpenPageInBrowser\": \"在浏览器中打开页面\"\n      },\n      \"error\": {\n        \"failedToUpdateNamespace\": \"更新名称空间失败\",\n        \"proPlanLimitation\": \"您需要升级至 Pro 方案以更新名称空间\",\n        \"namespaceAlreadyInUse\": \"该名称空间已被占用你，请尝试其他名称空间\",\n        \"invalidNamespace\": \"无效的名称空间，请尝试其他的名称空间\",\n        \"namespaceLengthAtLeast2Characters\": \"名称空间应不少于 2 个字符长度\",\n        \"onlyWorkspaceOwnerCanUpdateNamespace\": \"仅工作空间所有者可更新名称空间\",\n        \"onlyWorkspaceOwnerCanRemoveHomepage\": \"仅工作空间所有者可移除主页\",\n        \"setHomepageFailed\": \"设置主页失败\",\n        \"namespaceTooLong\": \"该名称空间过长，请尝试其他名称空间\",\n        \"namespaceTooShort\": \"该名称空间过短，请尝试其他名称空间\",\n        \"namespaceIsReserved\": \"该名称空间已被占用，请尝试其他名称空间\",\n        \"updatePathNameFailed\": \"更新路径名称失败\",\n        \"removeHomePageFailed\": \"移除主页失败\",\n        \"publishNameContainsInvalidCharacters\": \"该路径名称包含无效的字符，请尝试其他的路径名称\",\n        \"publishNameTooShort\": \"路径名称过短，请尝试其他路径名称\",\n        \"publishNameTooLong\": \"路径名称过长，请尝试其他路径名称\",\n        \"publishNameAlreadyInUse\": \"该路径名称已被使用，请尝试其他路径名称\",\n        \"namespaceContainsInvalidCharacters\": \"该名称空间包含无效的字符，请尝试其他名称空间\",\n        \"publishPermissionDenied\": \"仅工作空间所有者或页面发布者可管理发布设置\",\n        \"publishNameCannotBeEmpty\": \"该路径名称不能为空，请尝试其他路径名称\"\n      },\n      \"success\": {\n        \"namespaceUpdated\": \"更新名称空间成功\",\n        \"setHomepageSuccess\": \"成功设置主页\",\n        \"updatePathNameSuccess\": \"更新路径名称成功\",\n        \"removeHomePageSuccess\": \"成功删除主页\"\n      }\n    },\n    \"accountPage\": {\n      \"menuLabel\": \"我的账户\",\n      \"title\": \"我的账户\",\n      \"general\": {\n        \"title\": \"账户名与资料图像\",\n        \"changeProfilePicture\": \"更换头像\"\n      },\n      \"email\": {\n        \"title\": \"邮箱\",\n        \"actions\": {\n          \"change\": \"更改邮箱\"\n        }\n      },\n      \"login\": {\n        \"title\": \"登录账户\",\n        \"loginLabel\": \"登录\",\n        \"logoutLabel\": \"退出登录\"\n      },\n      \"description\": \"自定义您的简介，管理账户安全信息和 AI API keys，或登陆您的账户\"\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"工作区\",\n      \"title\": \"工作区\",\n      \"description\": \"自定义你的工作区外观、主题、字体、文本布局、日期/时间格式和语言。\",\n      \"workspaceName\": {\n        \"title\": \"工作区名称\",\n        \"savedMessage\": \"已保存的工作区名称\",\n        \"editTooltip\": \"编辑工作区名称\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"工作区图标\",\n        \"description\": \"上传图片或使用表情符号到你的工作区。图标将显示在您的侧边栏和通知中。\"\n      },\n      \"appearance\": {\n        \"title\": \"外观\",\n        \"description\": \"自定义你的工作区外观、主题、字体、文本布局、日期、时间和语言。\",\n        \"options\": {\n          \"system\": \"自动\",\n          \"light\": \"明亮\",\n          \"dark\": \"黑暗\"\n        }\n      },\n      \"resetCursorColor\": {\n        \"title\": \"重置文档光标颜色\",\n        \"description\": \"确定要重置光标颜色吗？\"\n      },\n      \"resetSelectionColor\": {\n        \"title\": \"重置文档选取颜色\",\n        \"description\": \"确定要重置选区颜色吗？\"\n      },\n      \"resetWidth\": {\n        \"resetSuccess\": \"文档宽度重置成功\"\n      },\n      \"theme\": {\n        \"title\": \"主题\",\n        \"description\": \"选择预设主题，或上传你自己的自定义主题。\",\n        \"uploadCustomThemeTooltip\": \"上传自定义主题\"\n      },\n      \"workspaceFont\": {\n        \"title\": \"工作区字体\",\n        \"noFontHint\": \"找不到字体，换个词试试。\"\n      },\n      \"textDirection\": {\n        \"title\": \"文字方向\",\n        \"leftToRight\": \"从左到右\",\n        \"rightToLeft\": \"从右到左\",\n        \"auto\": \"自动\",\n        \"enableRTLItems\": \"启用从右向左工具栏项目\"\n      },\n      \"layoutDirection\": {\n        \"title\": \"布局方向\",\n        \"leftToRight\": \"从左到右\",\n        \"rightToLeft\": \"从右到左\"\n      },\n      \"dateTime\": {\n        \"title\": \"日期 & 时间\",\n        \"example\": \"{} 于 {} ({})\",\n        \"24HourTime\": \"24 小时制\",\n        \"dateFormat\": {\n          \"label\": \"日期格式\",\n          \"local\": \"本地\",\n          \"us\": \"US\",\n          \"iso\": \"ISO\",\n          \"friendly\": \"友好\",\n          \"dmy\": \"日/月/年\"\n        }\n      },\n      \"language\": {\n        \"title\": \"语言\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"删除工作区\",\n        \"content\": \"你确定要删除此工作区吗？此操作无法撤消。\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"离开工作区\",\n        \"content\": \"你确定要离开此工作区吗？你将无法访问其中的所有页面和数据。\",\n        \"success\": \"你已经成功离开工作区。\",\n        \"fail\": \"无法离开工作区。\"\n      },\n      \"manageWorkspace\": {\n        \"title\": \"管理工作区\",\n        \"leaveWorkspace\": \"离开工作区\",\n        \"deleteWorkspace\": \"删除工作区\"\n      }\n    },\n    \"manageDataPage\": {\n      \"menuLabel\": \"管理数据\",\n      \"title\": \"管理数据\",\n      \"description\": \"管理数据本地存储或将现有数据导入@:appName 。\",\n      \"dataStorage\": {\n        \"title\": \"文件存储位置\",\n        \"tooltip\": \"你的文件存储位置\",\n        \"actions\": {\n          \"change\": \"更改路径\",\n          \"open\": \"打开文件夹\",\n          \"openTooltip\": \"打开当前数据文件夹位置\",\n          \"copy\": \"复制路径\",\n          \"copiedHint\": \"路径已复制！\",\n          \"resetTooltip\": \"重置为默认位置\"\n        },\n        \"resetDialog\": {\n          \"title\": \"你确定吗？\",\n          \"description\": \"将数据路径重置为默认位置不会删除你的数据。如果你想重新导入当前数据，你应该先复制当前位置的路径。\"\n        }\n      },\n      \"importData\": {\n        \"title\": \"导入数据\",\n        \"tooltip\": \"从 @:appName 备份/数据文件夹导入数据\",\n        \"description\": \"从外部 @:appName 数据文件夹复制数据\",\n        \"action\": \"浏览文件夹\"\n      },\n      \"encryption\": {\n        \"title\": \"加密\",\n        \"tooltip\": \"管理你的数据存储和加密方式\",\n        \"descriptionNoEncryption\": \"开启加密将会加密所有数据。此操作无法撤消。\",\n        \"descriptionEncrypted\": \"你的数据已加密。\",\n        \"action\": \"加密数据\",\n        \"dialog\": {\n          \"title\": \"加密您的所有数据？\",\n          \"description\": \"加密所有数据将保证数据安全。此操作无法撤消。您确定要继续吗？\"\n        }\n      },\n      \"cache\": {\n        \"title\": \"清除缓存\",\n        \"description\": \"如果你遇到图片无法加载或字体无法正确显示的问题，请尝试清除缓存。此操作不会删除你的用户数据。\",\n        \"dialog\": {\n          \"title\": \"清除缓存\",\n          \"description\": \"清除缓存会导致加载时重新下载图像和字体。此操作不会删除或修改你的数据。\",\n          \"successHint\": \"缓存已清除！\"\n        }\n      },\n      \"data\": {\n        \"fixYourData\": \"修复数据\",\n        \"fixButton\": \"修复\"\n      }\n    },\n    \"shortcutsPage\": {\n      \"menuLabel\": \"快捷键\",\n      \"title\": \"快捷键\",\n      \"actions\": {\n        \"resetDefault\": \"重置为默认\"\n      },\n      \"errorPage\": {\n        \"howToFix\": \"请再次尝试，如果该问题依然存在，请在 Github 上联系我们\"\n      },\n      \"resetDialog\": {\n        \"title\": \"重置快捷键\",\n        \"description\": \"这将会将所有按键绑定重置为默认，之后无法撤销。你确定要继续吗？\"\n      },\n      \"conflictDialog\": {\n        \"confirmLabel\": \"继续\"\n      },\n      \"keybindings\": {\n        \"insertNewParagraphInCodeblock\": \"插入新的段落\",\n        \"pasteInCodeblock\": \"粘贴为代码块\",\n        \"selectAllCodeblock\": \"全选\",\n        \"copy\": \"复制选中的内容\",\n        \"alignLeft\": \"文本居左对齐\",\n        \"alignCenter\": \"文本居中对齐\",\n        \"alignRight\": \"文本居右对齐\",\n        \"undo\": \"撤销\",\n        \"redo\": \"重做\",\n        \"convertToParagraph\": \"将块转换为段落\",\n        \"backspace\": \"删除\",\n        \"deleteLeftWord\": \"删除左侧文字\",\n        \"deleteLeftSentence\": \"删除左侧句子\",\n        \"delete\": \"删除右侧字符\",\n        \"deleteMacOS\": \"删除左侧字符\",\n        \"deleteRightWord\": \"删除右侧文字\",\n        \"moveCursorLeft\": \"将光标移至左侧\",\n        \"moveCursorBeginning\": \"将光标移至开头\",\n        \"moveCursorLeftWord\": \"将光标移至文字左侧\",\n        \"moveCursorRight\": \"将光标移至右侧\",\n        \"moveCursorEnd\": \"将光标移至末尾\",\n        \"moveCursorRightWord\": \"将光标移至文字右侧\",\n        \"home\": \"滚动至顶部\",\n        \"end\": \"滚动至底部\"\n      }\n    },\n    \"aiPage\": {\n      \"title\": \"AI 设置\",\n      \"menuLabel\": \"AI 设置\",\n      \"keys\": {\n        \"enableAISearchTitle\": \"AI 搜索\",\n        \"aiSettingsDescription\": \"选择你偏好的模型来赋能 AppFlowy AI。目前可以使用 GPT 4-o、Claude 3,5、Llama 3.1 与 Mistral 7B\",\n        \"llmModel\": \"语言模型\",\n        \"llmModelType\": \"语言模型类别\"\n      }\n    },\n    \"planPage\": {\n      \"planUsage\": {\n        \"currentPlan\": {\n          \"freeTitle\": \"免费\"\n        }\n      }\n    },\n    \"comparePlanDialog\": {\n      \"proLabels\": {\n        \"itemTwo\": \"最多十个\",\n        \"itemThree\": \"无限\",\n        \"itemFour\": \"是\",\n        \"itemFive\": \"是\",\n        \"itemSix\": \"无限\",\n        \"itemFileUpload\": \"无限\"\n      }\n    },\n    \"cancelSurveyDialog\": {\n      \"questionOne\": {\n        \"answerOne\": \"价格太高\",\n        \"answerTwo\": \"功能未达预期\",\n        \"answerThree\": \"找到更好替代方案\"\n      },\n      \"questionTwo\": {\n        \"answerOne\": \"非常可能\",\n        \"answerTwo\": \"稍有可能\",\n        \"answerThree\": \"不确定\",\n        \"answerFour\": \"不太可能\",\n        \"answerFive\": \"非常不可能\"\n      },\n      \"questionFour\": {\n        \"answerOne\": \"极好\",\n        \"answerTwo\": \"良好\",\n        \"answerThree\": \"一般\",\n        \"answerFour\": \"较差\",\n        \"answerFive\": \"未满足\"\n      }\n    },\n    \"common\": {\n      \"reset\": \"重置\"\n    },\n    \"menu\": {\n      \"appearance\": \"外观\",\n      \"language\": \"语言\",\n      \"user\": \"用户\",\n      \"files\": \"文件\",\n      \"notifications\": \"通知\",\n      \"open\": \"打开设置\",\n      \"logout\": \"登出\",\n      \"logoutPrompt\": \"您确定要登出吗？\",\n      \"selfEncryptionLogoutPrompt\": \"您确定要登出吗？请确保您已复制加密密钥\",\n      \"syncSetting\": \"同步设置\",\n      \"cloudSettings\": \"云设置\",\n      \"enableSync\": \"启用同步\",\n      \"enableEncrypt\": \"加密数据\",\n      \"cloudURL\": \"服务器 URL\",\n      \"invalidCloudURLScheme\": \"无效主题\",\n      \"cloudServerType\": \"云服务器\",\n      \"cloudServerTypeTip\": \"请注意，切换云服务器后可能会登出您当前的账户\",\n      \"cloudLocal\": \"本地\",\n      \"cloudAppFlowy\": \"@:appName Cloud Beta\",\n      \"cloudAppFlowySelfHost\": \"@:appName Cloud 自托管\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"云地址不能为空\",\n      \"clickToCopy\": \"点击复制\",\n      \"selfHostStart\": \"如果您没有服务器，请参阅\",\n      \"selfHostContent\": \"文档\",\n      \"selfHostEnd\": \"有关如何自行托管自己的服务器的指南\",\n      \"cloudURLHint\": \"输入您的服务器的 URL\",\n      \"cloudWSURL\": \"WebSocket URL\",\n      \"cloudWSURLHint\": \"输入您的服务器的 WebSocket 地址\",\n      \"restartApp\": \"重启\",\n      \"restartAppTip\": \"重新启动应用程序以使更改生效。请注意，这可能会注销您当前的帐户\",\n      \"changeServerTip\": \"更改服务器后，必须单击重新启动按钮才能使更改生效\",\n      \"enableEncryptPrompt\": \"启用加密以使用此密钥保护您的数据。安全存放；一旦启用，就无法关闭。如果丢失，您的数据将无法恢复。点击复制\",\n      \"inputEncryptPrompt\": \"请输入您的加密密钥\",\n      \"clickToCopySecret\": \"点击复制密钥\",\n      \"configServerSetting\": \"配置您的服务器设置\",\n      \"configServerGuide\": \"选择“快速启动”后，导航至“设置”，然后选择“云设置”以配置您的自托管服务器。\",\n      \"inputTextFieldHint\": \"你的秘密\",\n      \"historicalUserList\": \"用户登录历史记录\",\n      \"historicalUserListTooltip\": \"此列表显示您的匿名帐户。您可以单击某个帐户来查看其详细信息。单击“开始”按钮即可创建匿名帐户\",\n      \"openHistoricalUser\": \"点击开设匿名账户\",\n      \"customPathPrompt\": \"将 @:appName 数据文件夹存储在云同步文件夹（例如 Google Drive）中可能会带来风险。如果同时从多个位置访问或修改此文件夹中的数据库，可能会导致同步冲突和潜在的数据损坏\",\n      \"importAppFlowyData\": \"从外部 @:appName 文件夹导入数据\",\n      \"importingAppFlowyDataTip\": \"数据导入正在进行中。请不要关闭应用程序\",\n      \"importAppFlowyDataDescription\": \"从外部 @:appName 数据文件夹复制数据并将其导入到当前 @:appName 数据文件夹中\",\n      \"importSuccess\": \"成功导入@:appName数据文件夹\",\n      \"importFailed\": \"导入 @:appName 数据文件夹失败\",\n      \"importGuide\": \"有关详细信息，请参阅参考文档\",\n      \"cloudSetting\": \"云设置\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"启用通知\",\n        \"hint\": \"关闭以阻止本地通知出现。\"\n      },\n      \"showNotificationsIcon\": {\n        \"label\": \"显示通知图标\",\n        \"hint\": \"关闭开关以隐藏侧边栏中的通知图标。\"\n      },\n      \"markAsReadNotifications\": {\n        \"allSuccess\": \"成功全部标为已读\",\n        \"success\": \"成功标为已读\"\n      },\n      \"action\": {\n        \"markAsRead\": \"标为已读\",\n        \"multipleChoice\": \"选择更多\"\n      },\n      \"settings\": {\n        \"settings\": \"设置\"\n      },\n      \"tabs\": {\n        \"unread\": \"未读\"\n      },\n      \"refreshSuccess\": \"成功刷新通知\",\n      \"titles\": {\n        \"notifications\": \"通知\",\n        \"reminder\": \"提醒\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"重置此设置\",\n      \"fontFamily\": {\n        \"label\": \"字体系列\",\n        \"search\": \"搜索\",\n        \"defaultFont\": \"系统\"\n      },\n      \"themeMode\": {\n        \"label\": \"主题模式\",\n        \"light\": \"日间模式\",\n        \"dark\": \"夜间模式\",\n        \"system\": \"系统自适应\"\n      },\n      \"fontScaleFactor\": \"字体缩放\",\n      \"documentSettings\": {\n        \"cursorColor\": \"文档光标颜色\",\n        \"selectionColor\": \"文档选择颜色\",\n        \"pickColor\": \"选择颜色\",\n        \"colorShade\": \"色深\",\n        \"opacity\": \"透明度\",\n        \"hexEmptyError\": \"十六进制颜色不能为空\",\n        \"hexLengthError\": \"十六进制值必须为 6 位数字\",\n        \"hexInvalidError\": \"十六进制值无效\",\n        \"opacityEmptyError\": \"不透明度不能为空\",\n        \"opacityRangeError\": \"不透明度必须介于 1 到 100 之间\",\n        \"app\": \"应用程序\",\n        \"flowy\": \"弗洛菲\",\n        \"apply\": \"申请\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"布局方向\",\n        \"hint\": \"控制屏幕上内容的流动，从左到右或从右到左。\",\n        \"ltr\": \"从左到右\",\n        \"rtl\": \"从右到左\"\n      },\n      \"textDirection\": {\n        \"label\": \"默认文本方向\",\n        \"hint\": \"指定文本默认从左开始还是从右开始。\",\n        \"ltr\": \"从左到右\",\n        \"rtl\": \"从右到左\",\n        \"auto\": \"汽车\",\n        \"fallback\": \"与布局方向相同\"\n      },\n      \"themeUpload\": {\n        \"button\": \"上传\",\n        \"uploadTheme\": \"上传主题\",\n        \"description\": \"使用下面的按钮上传您自己的 @:appName 主题。\",\n        \"loading\": \"我们正在验证并上传您的主题，请稍候...\",\n        \"uploadSuccess\": \"您的主题已上传成功\",\n        \"deletionFailure\": \"删除主题失败，请尝试手动删除。\",\n        \"filePickerDialogTitle\": \"选择 .flowy_plugin 文件\",\n        \"urlUploadFailure\": \"无法打开网址：{}\",\n        \"failure\": \"上传的主题格式无效。\"\n      },\n      \"theme\": \"主题\",\n      \"builtInsLabel\": \"内置主题\",\n      \"pluginsLabel\": \"插件\",\n      \"dateFormat\": {\n        \"label\": \"日期格式\",\n        \"local\": \"本地\",\n        \"us\": \"美国\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"易读\",\n        \"dmy\": \"日/月/年\"\n      },\n      \"timeFormat\": {\n        \"label\": \"时间格式\",\n        \"twelveHour\": \"十二小时制\",\n        \"twentyFourHour\": \"二十四小时制\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"创建页面时显示命名对话框\",\n      \"enableRTLToolbarItems\": \"显示从右到左按钮\",\n      \"members\": {\n        \"title\": \"成员设置\",\n        \"inviteMembers\": \"添加成员\",\n        \"inviteHint\": \"使用电子邮件邀请\",\n        \"sendInvite\": \"发送邀请\",\n        \"copyInviteLink\": \"复制邀请链接\",\n        \"label\": \"成员\",\n        \"user\": \"用户\",\n        \"role\": \"角色\",\n        \"removeFromWorkspace\": \"从工作区移除\",\n        \"owner\": \"所有者\",\n        \"guest\": \"访客\",\n        \"member\": \"成员\",\n        \"emailSent\": \"邮件已发送，请检查您的邮箱\",\n        \"memberLimitExceededUpgrade\": \"升级\",\n        \"memberLimitExceededProContact\": \"support@appflowy.io\",\n        \"failedToAddMember\": \"添加成员失败\",\n        \"addMemberSuccess\": \"添加成员成功\",\n        \"removeMember\": \"移除成员\",\n        \"areYouSureToRemoveMember\": \"您确定要删除该成员吗？\",\n        \"inviteMemberSuccess\": \"成功发送邀请\",\n        \"failedToInviteMember\": \"邀请成员失败\"\n      }\n    },\n    \"files\": {\n      \"copy\": \"复制\",\n      \"defaultLocation\": \"读取文件和数据存储位置\",\n      \"exportData\": \"导出您的数据\",\n      \"doubleTapToCopy\": \"双击复制路径\",\n      \"restoreLocation\": \"恢复为 @:appName 默认路径\",\n      \"customizeLocation\": \"打开另一个文件夹\",\n      \"restartApp\": \"请重启 App 使设置生效\",\n      \"exportDatabase\": \"导出数据库\",\n      \"selectFiles\": \"选择需要导出的文件\",\n      \"selectAll\": \"全选\",\n      \"deselectAll\": \"取消全选\",\n      \"createNewFolder\": \"新建文件夹\",\n      \"createNewFolderDesc\": \"告诉我们您要将数据存储在何处\",\n      \"defineWhereYourDataIsStored\": \"定义数据存储位置\",\n      \"open\": \"打开\",\n      \"openFolder\": \"打开现有文件夹\",\n      \"openFolderDesc\": \"读取并将其写入您现有的 @:appName 文件夹\",\n      \"folderHintText\": \"文件夹名\",\n      \"location\": \"正在新建文件夹\",\n      \"locationDesc\": \"为您的 @:appName 数据文件夹选择一个名称\",\n      \"browser\": \"浏览\",\n      \"create\": \"新建\",\n      \"set\": \"设置\",\n      \"folderPath\": \"保存文件夹的路径\",\n      \"locationCannotBeEmpty\": \"路径不能为空\",\n      \"pathCopiedSnackbar\": \"文件存储路径已被复制到剪贴板!\",\n      \"changeLocationTooltips\": \"更改数据目录\",\n      \"change\": \"更改\",\n      \"openLocationTooltips\": \"打开另一个数据目录\",\n      \"openCurrentDataFolder\": \"打开当前数据目录\",\n      \"recoverLocationTooltips\": \"恢复为 @:appName 默认数据目录\",\n      \"exportFileSuccess\": \"导出成功!\",\n      \"exportFileFail\": \"导出失败!\",\n      \"export\": \"导出\",\n      \"clearCache\": \"清空缓存\",\n      \"clearCacheDesc\": \"如果您遇到图片无法加载或字体无法正确显示的问题，请尝试清除缓存。此操作不会删除您的用户数据。\",\n      \"areYouSureToClearCache\": \"您确定要清除缓存吗？\",\n      \"clearCacheSuccess\": \"缓存清除成功！\"\n    },\n    \"user\": {\n      \"name\": \"名字\",\n      \"email\": \"电子邮件\",\n      \"tooltipSelectIcon\": \"选择图标\",\n      \"selectAnIcon\": \"选择一个图标\",\n      \"pleaseInputYourOpenAIKey\": \"请输入您的 AI 密钥\",\n      \"clickToLogout\": \"点击退出当前用户\",\n      \"pleaseInputYourStabilityAIKey\": \"请输入您的 Stability AI 密钥\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"个人信息\",\n      \"username\": \"用户名\",\n      \"usernameEmptyError\": \"用户名不能为空\",\n      \"about\": \"关于\",\n      \"pushNotifications\": \"推送通知\",\n      \"support\": \"支持\",\n      \"joinDiscord\": \"在 Discord 中加入我们\",\n      \"privacyPolicy\": \"隐私政策\",\n      \"userAgreement\": \"用户协议\",\n      \"termsAndConditions\": \"条款和条件\",\n      \"userprofileError\": \"无法加载用户配置文件\",\n      \"userprofileErrorDescription\": \"请尝试注销并重新登录以检查问题是否仍然存在。\",\n      \"selectLayout\": \"选择布局\",\n      \"selectStartingDay\": \"选择开始日期\",\n      \"version\": \"版本\"\n    },\n    \"shortcuts\": {\n      \"shortcutsLabel\": \"快捷方式\",\n      \"command\": \"指令\",\n      \"keyBinding\": \"按键绑定\",\n      \"addNewCommand\": \"添加新指令\",\n      \"updateShortcutStep\": \"按所需的组合键并按 ENTER\",\n      \"shortcutIsAlreadyUsed\": \"此快捷方式已用于：{conflict}\",\n      \"resetToDefault\": \"重置为默认组合键\",\n      \"couldNotLoadErrorMsg\": \"无法加载快捷方式，请重试\",\n      \"couldNotSaveErrorMsg\": \"无法保存快捷方式，请重试\",\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"在代码块旁边插入一个新段落\",\n        \"codeBlockAddTwoSpaces\": \"在代码块的行首插入两个空格\",\n        \"codeBlockSelectAll\": \"选择代码块内的所有内容\",\n        \"codeBlockPasteText\": \"在代码块中粘贴文本\",\n        \"textAlignLeft\": \"左对齐文本\",\n        \"textAlignCenter\": \"将文本居中对齐\",\n        \"textAlignRight\": \"右对齐文本\",\n        \"codeBlockDeleteTwoSpaces\": \"删除代码块中行首的两个空格\"\n      }\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"您确定要删除这个表格吗?\",\n    \"createView\": \"新建\",\n    \"title\": {\n      \"placeholder\": \"无标题\"\n    },\n    \"settings\": {\n      \"filter\": \"筛选\",\n      \"sort\": \"以……排序\",\n      \"sortBy\": \"排序\",\n      \"properties\": \"特性\",\n      \"reorderPropertiesTooltip\": \"拖动以重新排序属性\",\n      \"group\": \"组\",\n      \"addFilter\": \"添加筛选\",\n      \"deleteFilter\": \"删除筛选\",\n      \"filterBy\": \"以……筛选\",\n      \"typeAValue\": \"请输入一个值\",\n      \"layout\": \"布局\",\n      \"databaseLayout\": \"布局\",\n      \"viewList\": {\n        \"zero\": \"0 次观看\",\n        \"one\": \"{count} 次查看\",\n        \"other\": \"{count} 次浏览\"\n      },\n      \"editView\": \"编辑视图\",\n      \"boardSettings\": \"看板设置\",\n      \"calendarSettings\": \"日历设置\",\n      \"createView\": \"新视图\",\n      \"duplicateView\": \"复制视图\",\n      \"deleteView\": \"删除视图\",\n      \"numberOfVisibleFields\": \"显示 {}\",\n      \"Properties\": \"属性\"\n    },\n    \"textFilter\": {\n      \"contains\": \"包含\",\n      \"doesNotContain\": \"不包含\",\n      \"endsWith\": \"以……结束\",\n      \"startWith\": \"以……开始\",\n      \"is\": \"等于\",\n      \"isNot\": \"不等于\",\n      \"isEmpty\": \"为空\",\n      \"isNotEmpty\": \"不为空\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"不等于\",\n        \"startWith\": \"以……开始\",\n        \"endWith\": \"以……结束\",\n        \"isEmpty\": \"为空\",\n        \"isNotEmpty\": \"不为空\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"已勾选\",\n      \"isUnchecked\": \"未勾选\",\n      \"choicechipPrefix\": {\n        \"is\": \"是\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"已完成\",\n      \"isIncomplted\": \"未完成\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"是\",\n      \"isNot\": \"不是\",\n      \"contains\": \"包含\",\n      \"doesNotContain\": \"不含\",\n      \"isEmpty\": \"为空\",\n      \"isNotEmpty\": \"不为空\"\n    },\n    \"dateFilter\": {\n      \"is\": \"是\",\n      \"before\": \"之前\",\n      \"after\": \"之后\",\n      \"onOrBefore\": \"在或之前\",\n      \"onOrAfter\": \"在或之后\",\n      \"between\": \"之间\",\n      \"empty\": \"为空\",\n      \"notEmpty\": \"不为空\",\n      \"choicechipPrefix\": {\n        \"before\": \"之前\",\n        \"after\": \"之后\",\n        \"onOrBefore\": \"今天或之前\",\n        \"onOrAfter\": \"今天或之后\",\n        \"isEmpty\": \"为空\",\n        \"isNotEmpty\": \"不为空\"\n      }\n    },\n    \"numberFilter\": {\n      \"equal\": \"等于\",\n      \"notEqual\": \"不相等\",\n      \"lessThan\": \"小于\",\n      \"greaterThan\": \"大于\",\n      \"lessThanOrEqualTo\": \"小于或等于\",\n      \"greaterThanOrEqualTo\": \"大于或等于\",\n      \"isEmpty\": \"为空\",\n      \"isNotEmpty\": \"不为空\"\n    },\n    \"field\": {\n      \"label\": \"属性\",\n      \"hide\": \"隐藏\",\n      \"show\": \"展示\",\n      \"insertLeft\": \"左侧插入\",\n      \"insertRight\": \"右侧插入\",\n      \"duplicate\": \"复制\",\n      \"delete\": \"删除\",\n      \"clear\": \"清空单元格\",\n      \"textFieldName\": \"文本\",\n      \"checkboxFieldName\": \"勾选框\",\n      \"dateFieldName\": \"日期\",\n      \"updatedAtFieldName\": \"修改时间\",\n      \"createdAtFieldName\": \"创建时间\",\n      \"numberFieldName\": \"数字\",\n      \"singleSelectFieldName\": \"单项选择器\",\n      \"multiSelectFieldName\": \"多项选择器\",\n      \"urlFieldName\": \"链接\",\n      \"checklistFieldName\": \"清单\",\n      \"summaryFieldName\": \"AI 总结\",\n      \"timeFieldName\": \"时间\",\n      \"numberFormat\": \"数字格式\",\n      \"dateFormat\": \"日期格式\",\n      \"includeTime\": \"包含时间\",\n      \"isRange\": \"结束日期\",\n      \"dateFormatFriendly\": \"月 日, 年\",\n      \"dateFormatISO\": \"年-月-日\",\n      \"dateFormatLocal\": \"月/日/年\",\n      \"dateFormatUS\": \"年/月/日\",\n      \"dateFormatDayMonthYear\": \"日/月/年\",\n      \"timeFormat\": \"时间格式\",\n      \"invalidTimeFormat\": \"时间格式错误\",\n      \"timeFormatTwelveHour\": \"十二小时制\",\n      \"timeFormatTwentyFourHour\": \"24小时制\",\n      \"clearDate\": \"清除日期\",\n      \"dateTime\": \"日期时间\",\n      \"startDateTime\": \"开始日期和时间\",\n      \"endDateTime\": \"结束日期和时间\",\n      \"failedToLoadDate\": \"无法加载日期值\",\n      \"selectTime\": \"选择时间\",\n      \"selectDate\": \"选择日期\",\n      \"visibility\": \"可见性\",\n      \"propertyType\": \"属性类型\",\n      \"addSelectOption\": \"添加一个标签\",\n      \"typeANewOption\": \"输入新选项\",\n      \"optionTitle\": \"标签\",\n      \"addOption\": \"添加标签\",\n      \"editProperty\": \"编辑列属性\",\n      \"newProperty\": \"添加一列\",\n      \"deleteFieldPromptMessage\": \"确定要删除这个属性吗? \",\n      \"clearFieldPromptMessage\": \"您确定吗？此列中的所有单元格都将被清空\",\n      \"newColumn\": \"新建列\",\n      \"format\": \"格式\",\n      \"reminderOnDateTooltip\": \"此单元格有预定的提醒\",\n      \"optionAlreadyExist\": \"选项已存在\",\n      \"wrap\": \"自动换行\"\n    },\n    \"rowPage\": {\n      \"newField\": \"添加新字段\",\n      \"fieldDragElementTooltip\": \"点击打开菜单\",\n      \"showHiddenFields\": {\n        \"one\": \"显示 {count} 个隐藏字段\",\n        \"many\": \"显示 {count} 个隐藏字段\",\n        \"other\": \"显示 {count} 个隐藏字段\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"隐藏 {count} 个隐藏字段\",\n        \"many\": \"隐藏 {count} 个隐藏字段\",\n        \"other\": \"隐藏 {count} 个隐藏字段\"\n      }\n    },\n    \"sort\": {\n      \"ascending\": \"升序\",\n      \"descending\": \"降序\",\n      \"by\": \"通过\",\n      \"cannotFindCreatableField\": \"找不到合适的排序字段\",\n      \"deleteAllSorts\": \"删除所有排序\",\n      \"addSort\": \"添加排序\",\n      \"removeSorting\": \"您想删除排序吗？\",\n      \"fieldInUse\": \"您已按此字段排序\",\n      \"deleteSort\": \"删除排序\"\n    },\n    \"row\": {\n      \"duplicate\": \"复制\",\n      \"delete\": \"删除\",\n      \"titlePlaceholder\": \"无标题\",\n      \"textPlaceholder\": \"空\",\n      \"copyProperty\": \"复制列\",\n      \"count\": \"数量\",\n      \"newRow\": \"添加一行\",\n      \"action\": \"执行\",\n      \"add\": \"点击添加到下方\",\n      \"drag\": \"拖动以移动\",\n      \"deleteRowPrompt\": \"您确定要删除此行吗？此操作无法撤消\",\n      \"dragAndClick\": \"拖拽移动，点击打开菜单\",\n      \"insertRecordAbove\": \"在上方插入记录\",\n      \"insertRecordBelow\": \"点击添加到下方\"\n    },\n    \"selectOption\": {\n      \"create\": \"新建\",\n      \"purpleColor\": \"紫色\",\n      \"pinkColor\": \"粉色\",\n      \"lightPinkColor\": \"浅粉色\",\n      \"orangeColor\": \"橙色\",\n      \"yellowColor\": \"黄色\",\n      \"limeColor\": \"鲜绿色\",\n      \"greenColor\": \"绿色\",\n      \"aquaColor\": \"水蓝色\",\n      \"blueColor\": \"蓝色\",\n      \"deleteTag\": \"删除标签\",\n      \"colorPanelTitle\": \"颜色\",\n      \"panelTitle\": \"选择或新建一个标签\",\n      \"searchOption\": \"搜索标签\",\n      \"searchOrCreateOption\": \"搜索或创建选项...\",\n      \"createNew\": \"创建一个新的\",\n      \"orSelectOne\": \"或者选择一个选项\",\n      \"typeANewOption\": \"输入一个新的选项\",\n      \"tagName\": \"标签名\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"任务描述\",\n      \"addNew\": \"添加项\",\n      \"submitNewTask\": \"创建\",\n      \"hideComplete\": \"隐藏已完成的任务\",\n      \"showComplete\": \"显示所有任务\"\n    },\n    \"url\": {\n      \"launch\": \"在浏览器中打开链接\",\n      \"copy\": \"将链接复制到剪贴板\",\n      \"textFieldHint\": \"输入 URL\"\n    },\n    \"relation\": {\n      \"rowSearchTextFieldPlaceholder\": \"搜索\"\n    },\n    \"menuName\": \"网格\",\n    \"referencedGridPrefix\": \"视图\",\n    \"calculate\": \"计算\",\n    \"calculationTypeLabel\": {\n      \"none\": \"没有任何\",\n      \"average\": \"平均的\",\n      \"max\": \"最大限度\",\n      \"median\": \"中位数\",\n      \"min\": \"分钟\",\n      \"sum\": \"和\"\n    },\n    \"media\": {\n      \"rename\": \"重命名\",\n      \"download\": \"下载\",\n      \"delete\": \"删除\",\n      \"open\": \"打开\"\n    },\n    \"singleSelectOptionFilter\": {\n      \"is\": \"等于\",\n      \"isNot\": \"不等于\",\n      \"isEmpty\": \"为空\",\n      \"isNotEmpty\": \"不为空\"\n    },\n    \"multiSelectOptionFilter\": {\n      \"contains\": \"包含\",\n      \"doesNotContain\": \"不包含\",\n      \"isEmpty\": \"为空\",\n      \"isNotEmpty\": \"不为空\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"文档\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"下午 01:00\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"选择要链接到的看板\",\n        \"createANewBoard\": \"新建看板\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"选择要链接到的网格\",\n        \"createANewGrid\": \"新建网格\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"选择要链接到的日历\",\n        \"createANewCalendar\": \"新建日历\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"选择要链接到的文档\"\n      },\n      \"name\": {\n        \"textStyle\": \"文本样式\",\n        \"list\": \"列表\",\n        \"toggle\": \"切换\",\n        \"fileAndMedia\": \"文件与媒体\",\n        \"simpleTable\": \"简单表格\",\n        \"visuals\": \"视觉元素\",\n        \"document\": \"文档\",\n        \"advanced\": \"高级\",\n        \"text\": \"文本\",\n        \"heading1\": \"一级标题\",\n        \"heading2\": \"二级标题\",\n        \"heading3\": \"三级标题\",\n        \"image\": \"图片\",\n        \"bulletedList\": \"项目符号列表\",\n        \"numberedList\": \"编号列表\",\n        \"todoList\": \"待办事项列表\",\n        \"doc\": \"文档\",\n        \"linkedDoc\": \"链接到页面\",\n        \"grid\": \"网格\",\n        \"linkedGrid\": \"链接网格\",\n        \"kanban\": \"看板\",\n        \"linkedKanban\": \"链接看板\",\n        \"calendar\": \"日历\",\n        \"linkedCalendar\": \"链接日历\",\n        \"quote\": \"引用\",\n        \"divider\": \"分隔符\",\n        \"table\": \"表格\",\n        \"callout\": \"提示框\",\n        \"outline\": \"大纲\",\n        \"mathEquation\": \"数学公式\",\n        \"code\": \"代码\",\n        \"toggleList\": \"切换列表\",\n        \"toggleHeading1\": \"切换标题1\",\n        \"toggleHeading2\": \"切换标题2\",\n        \"toggleHeading3\": \"切换标题3\",\n        \"emoji\": \"表情符号\",\n        \"aiWriter\": \"向AI提问\",\n        \"dateOrReminder\": \"日期或提醒\",\n        \"photoGallery\": \"图片库\",\n        \"file\": \"文件\",\n        \"twoColumns\": \"两列\",\n        \"threeColumns\": \"三列\",\n        \"fourColumns\": \"四列\"\n      },\n      \"subPage\": {\n        \"name\": \"文档\",\n        \"keyword1\": \"子页面\",\n        \"keyword2\": \"页面\",\n        \"keyword3\": \"子页面\",\n        \"keyword4\": \"插入页面\",\n        \"keyword5\": \"嵌入页面\",\n        \"keyword6\": \"新页面\",\n        \"keyword7\": \"创建页面\",\n        \"keyword8\": \"文档\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"大纲\",\n      \"codeBlock\": \"代码块\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"引用的看板\",\n      \"referencedGrid\": \"引用的网格\",\n      \"referencedCalendar\": \"引用的日历\",\n      \"referencedDocument\": \"参考文档\",\n      \"aiWriter\": {\n        \"userQuestion\": \"向AI提问\",\n        \"continueWriting\": \"继续写作\",\n        \"fixSpelling\": \"修正拼写和语法\",\n        \"improveWriting\": \"优化写作\",\n        \"summarize\": \"总结\",\n        \"explain\": \"解释\",\n        \"makeShorter\": \"缩短\",\n        \"makeLonger\": \"扩展\"\n      },\n      \"autoGeneratorMenuItemName\": \"AI 创作\",\n      \"autoGeneratorTitleName\": \"AI: 让 AI 写些什么...\",\n      \"autoGeneratorLearnMore\": \"学习更多\",\n      \"autoGeneratorGenerate\": \"生成\",\n      \"autoGeneratorHintText\": \"让 AI ...\",\n      \"autoGeneratorCantGetOpenAIKey\": \"无法获得 AI 密钥\",\n      \"autoGeneratorRewrite\": \"重写\",\n      \"smartEdit\": \"AI 助手\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"修正拼写\",\n      \"warning\": \"⚠️ AI 可能不准确或具有误导性.\",\n      \"smartEditSummarize\": \"总结\",\n      \"smartEditImproveWriting\": \"提高写作水平\",\n      \"smartEditMakeLonger\": \"丰富内容\",\n      \"smartEditCouldNotFetchResult\": \"无法从 AI 获取到结果\",\n      \"smartEditCouldNotFetchKey\": \"无法获取到 AI 密钥\",\n      \"smartEditDisabled\": \"在设置中连接 AI\",\n      \"discardResponse\": \"您是否要放弃 AI 继续写作?\",\n      \"createInlineMathEquation\": \"创建方程\",\n      \"fonts\": \"字体\",\n      \"insertDate\": \"插入日期\",\n      \"emoji\": \"表情符号\",\n      \"toggleList\": \"切换列表\",\n      \"quoteList\": \"引用列表\",\n      \"numberedList\": \"编号列表\",\n      \"bulletedList\": \"项目符号列表\",\n      \"todoList\": \"待办事项列表\",\n      \"callout\": \"标注\",\n      \"cover\": {\n        \"changeCover\": \"修改封面\",\n        \"colors\": \"颜色\",\n        \"images\": \"图像\",\n        \"clearAll\": \"清除所有\",\n        \"abstract\": \"摘要\",\n        \"addCover\": \"添加封面\",\n        \"addLocalImage\": \"添加本地图像\",\n        \"invalidImageUrl\": \"无效的图像网址\",\n        \"failedToAddImageToGallery\": \"无法将图片添加到库\",\n        \"enterImageUrl\": \"输入图像网址\",\n        \"add\": \"添加\",\n        \"back\": \"返回\",\n        \"saveToGallery\": \"保存至库\",\n        \"removeIcon\": \"移除图标\",\n        \"pasteImageUrl\": \"粘贴图片网址\",\n        \"or\": \"或\",\n        \"pickFromFiles\": \"从文件中选取\",\n        \"couldNotFetchImage\": \"无法获取到图像\",\n        \"imageSavingFailed\": \"图像保存失败\",\n        \"addIcon\": \"添加图标\",\n        \"changeIcon\": \"更改图标\",\n        \"coverRemoveAlert\": \"删除后将从封面中移除。\",\n        \"alertDialogConfirmation\": \"您确定您要继续吗?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"数学方程\",\n        \"addMathEquation\": \"添加数学公式\",\n        \"editMathEquation\": \"编辑数学公式\"\n      },\n      \"optionAction\": {\n        \"click\": \"点击\",\n        \"toOpenMenu\": \"打开菜单\",\n        \"delete\": \"删除\",\n        \"duplicate\": \"复制\",\n        \"turnInto\": \"变成\",\n        \"moveUp\": \"上移\",\n        \"moveDown\": \"下移\",\n        \"color\": \"颜色\",\n        \"align\": \"对齐\",\n        \"left\": \"左\",\n        \"center\": \"中心\",\n        \"right\": \"右\",\n        \"defaultColor\": \"默认\",\n        \"depth\": \"深度\",\n        \"copyLinkToBlock\": \"粘贴块链接\"\n      },\n      \"image\": {\n        \"addAnImage\": \"添加图像\",\n        \"copiedToPasteBoard\": \"图片链接已复制到剪贴板\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"链接已复制到剪贴板\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"添加标题以创建目录。\"\n      },\n      \"table\": {\n        \"addAfter\": \"在后面添加\",\n        \"addBefore\": \"在前面添加\",\n        \"delete\": \"删除\",\n        \"clear\": \"清空内容\",\n        \"duplicate\": \"创建副本\",\n        \"bgColor\": \"背景颜色\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"复制\",\n        \"cut\": \"剪切\",\n        \"paste\": \"粘贴\"\n      },\n      \"action\": \"操作\",\n      \"database\": {\n        \"selectDataSource\": \"选择数据源\",\n        \"noDataSource\": \"没有数据源\",\n        \"selectADataSource\": \"选择数据源\",\n        \"toContinue\": \"继续\",\n        \"newDatabase\": \"新建数据库\",\n        \"linkToDatabase\": \"链接至数据库\"\n      },\n      \"date\": \"日期\",\n      \"video\": {\n        \"label\": \"视频\",\n        \"emptyLabel\": \"添加视频\",\n        \"placeholder\": \"粘贴视频链接\",\n        \"copiedToPasteBoard\": \"视频链接已复制到剪贴板\",\n        \"insertVideo\": \"添加视频\"\n      },\n      \"linkPreview\": {\n        \"typeSelection\": {\n          \"pasteAs\": \"粘贴为\",\n          \"mention\": \"提及\",\n          \"URL\": \"URL\",\n          \"bookmark\": \"书签\",\n          \"embed\": \"嵌入\"\n        },\n        \"linkPreviewMenu\": {\n          \"toMetion\": \"转换为提及\",\n          \"toUrl\": \"转换为URL\",\n          \"toEmbed\": \"转换为嵌入\",\n          \"toBookmark\": \"转换为书签\",\n          \"copyLink\": \"复制链接\",\n          \"replace\": \"替换\",\n          \"reload\": \"重新加载\",\n          \"removeLink\": \"移除链接\",\n          \"pasteHint\": \"粘贴 https://...\",\n          \"unableToDisplay\": \"无法显示\"\n        }\n      }\n    },\n    \"outlineBlock\": {\n      \"placeholder\": \"目录\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"输入 “/” 作为命令\"\n    },\n    \"title\": {\n      \"placeholder\": \"无标题\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"点击添加图片\",\n      \"upload\": {\n        \"label\": \"上传\",\n        \"placeholder\": \"点击上传图片\"\n      },\n      \"url\": {\n        \"label\": \"图片网址\",\n        \"placeholder\": \"输入图片网址\"\n      },\n      \"ai\": {\n        \"label\": \"从 AI 生成图像\",\n        \"placeholder\": \"请输入 AI 生成图像的提示\"\n      },\n      \"stability_ai\": {\n        \"label\": \"从 Stability AI 生成图像\",\n        \"placeholder\": \"请输入 Stability AI 生成图像的提示\"\n      },\n      \"support\": \"图片大小限制为 5MB。支持的格式：JPEG、PNG、GIF、SVG\",\n      \"error\": {\n        \"invalidImage\": \"图片无效\",\n        \"invalidImageSize\": \"图片大小必须小于 5MB\",\n        \"invalidImageFormat\": \"不支持图像格式。支持的格式：JPEG、PNG、GIF、SVG\",\n        \"invalidImageUrl\": \"图片网址无效\",\n        \"noImage\": \"没有这样的文件或目录\"\n      },\n      \"embedLink\": {\n        \"label\": \"内嵌链接\",\n        \"placeholder\": \"粘贴或输入图像链接\"\n      },\n      \"unsplash\": {\n        \"label\": \"Unsplash\"\n      },\n      \"searchForAnImage\": \"搜索图像\",\n      \"pleaseInputYourOpenAIKey\": \"请在设置页面输入您的 AI 密钥\",\n      \"saveImageToGallery\": \"保存图片\",\n      \"failedToAddImageToGallery\": \"无法将图像添加到图库\",\n      \"successToAddImageToGallery\": \"图片已成功添加到图库\",\n      \"unableToLoadImage\": \"无法加载图像\",\n      \"maximumImageSize\": \"支持的最大上传图片大小为 10MB\",\n      \"uploadImageErrorImageSizeTooBig\": \"图片大小必须小于 10MB\",\n      \"imageIsUploading\": \"图片正在上传\",\n      \"pleaseInputYourStabilityAIKey\": \"请在设置页面输入您的 Stability AI 密钥\"\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"语言\",\n        \"placeholder\": \"选择语言\",\n        \"auto\": \"自动\"\n      },\n      \"copyTooltip\": \"复制代码块的内容\"\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"粘贴或输入链接\",\n      \"openInNewTab\": \"在新选项卡中打开\",\n      \"copyLink\": \"复制链接\",\n      \"removeLink\": \"删除链接\",\n      \"url\": {\n        \"label\": \"链接网址\",\n        \"placeholder\": \"输入链接网址\"\n      },\n      \"title\": {\n        \"label\": \"链接标题\",\n        \"placeholder\": \"输入链接标题\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"提及一个人、或日期...\",\n      \"page\": {\n        \"label\": \"链接到页面\",\n        \"tooltip\": \"点击打开页面\"\n      }\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"重置为默认\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"当前版本不支持该块。\",\n      \"blockContentHasBeenCopied\": \"块内容已被复制。\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"createNewCard\": \"新建\",\n      \"renameGroupTooltip\": \"按下以重命名群组\",\n      \"createNewColumn\": \"添加新组\",\n      \"addToColumnTopTooltip\": \"在顶部添加一张新卡片\",\n      \"addToColumnBottomTooltip\": \"在底部添加一张新卡片\",\n      \"renameColumn\": \"改名\",\n      \"hideColumn\": \"隐藏\",\n      \"newGroup\": \"新建组\",\n      \"deleteColumn\": \"删除\",\n      \"deleteColumnConfirmation\": \"这将删除该组及其中的所有卡片。你确定你要继续吗？\",\n      \"groupActions\": \"组操作\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"隐藏组\",\n      \"collapseTooltip\": \"隐藏隐藏组\",\n      \"expandTooltip\": \"查看隐藏的组\"\n    },\n    \"cardDetail\": \"卡片详情\",\n    \"cardActions\": \"卡片操作\",\n    \"cardDuplicated\": \"卡片已被复制\",\n    \"cardDeleted\": \"卡片已被删除\",\n    \"showOnCard\": \"显示卡片详细信息\",\n    \"setting\": \"设置\",\n    \"propertyName\": \"属性名称\",\n    \"menuName\": \"看板\",\n    \"showUngrouped\": \"显示未分组的项目\",\n    \"ungroupedButtonText\": \"未分组的\",\n    \"ungroupedButtonTooltip\": \"包含不属于任何组的卡片\",\n    \"ungroupedItemsTitle\": \"点击添加到看板\",\n    \"groupBy\": \"通过...分组\",\n    \"referencedBoardPrefix\": \"视图\",\n    \"notesTooltip\": \"内含笔记\",\n    \"mobile\": {\n      \"editURL\": \"编辑 URL\",\n      \"showGroup\": \"“显示”组\",\n      \"showGroupContent\": \"您确定要在公告板上显示该组吗？\",\n      \"failedToLoad\": \"无法加载版面视图\",\n      \"unhideGroup\": \"显示隐藏的组\",\n      \"unhideGroupContent\": \"您确定要在看板上显示该组吗？\",\n      \"faildToLoad\": \"无法加载看板视图\"\n    },\n    \"dateCondition\": {\n      \"today\": \"今天\",\n      \"yesterday\": \"昨天\",\n      \"tomorrow\": \"明天\",\n      \"lastSevenDays\": \"过去 7 天\",\n      \"nextSevenDays\": \"未来 7 天\",\n      \"lastThirtyDays\": \"过去 30 天\",\n      \"nextThirtyDays\": \"未来 30 天\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"日历\",\n    \"defaultNewCalendarTitle\": \"未命名\",\n    \"newEventButtonTooltip\": \"添加新事件\",\n    \"navigation\": {\n      \"today\": \"今天\",\n      \"jumpToday\": \"跳转到今天\",\n      \"previousMonth\": \"上一月\",\n      \"nextMonth\": \"下一月\",\n      \"views\": {\n        \"day\": \"天\",\n        \"week\": \"周\",\n        \"month\": \"月\",\n        \"year\": \"年\"\n      }\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"显示周数\",\n      \"showWeekends\": \"显示周末\",\n      \"firstDayOfWeek\": \"一周开始于\",\n      \"layoutDateField\": \"以……为日历布局\",\n      \"noDateTitle\": \"没有日期\",\n      \"unscheduledEventsTitle\": \"未安排的事件\",\n      \"clickToAdd\": \"单击以添加到日历\",\n      \"name\": \"日历布局\",\n      \"noDateHint\": \"计划外事件将显示在此处\"\n    },\n    \"referencedCalendarPrefix\": \"视图\",\n    \"quickJumpYear\": \"跳转到\"\n  },\n  \"errorDialog\": {\n    \"title\": \"@:appName 错误\",\n    \"howToFixFallback\": \"对于给您带来的不便, 我们深表歉意! 请在我们的 GitHub 页面上提交 issue 并描述您遇到的错误。\",\n    \"github\": \"在 GitHub 查看\"\n  },\n  \"search\": {\n    \"label\": \"搜索\",\n    \"placeholder\": {\n      \"actions\": \"搜索操作...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"已复制\",\n      \"fail\": \"复制失败\"\n    }\n  },\n  \"unSupportBlock\": \"当前版本不支持该块。\",\n  \"views\": {\n    \"deleteContentTitle\": \"您确定要删除 {pageType} 吗？\",\n    \"deleteContentCaption\": \"如果您删除此{pageType}，您可以从回收站中将其恢复。\"\n  },\n  \"colors\": {\n    \"custom\": \"自定义\",\n    \"default\": \"默认\",\n    \"red\": \"红色\",\n    \"orange\": \"橙色\",\n    \"yellow\": \"黄色\",\n    \"green\": \"绿色\",\n    \"blue\": \"蓝色\",\n    \"purple\": \"紫色\",\n    \"pink\": \"粉色\",\n    \"brown\": \"棕色\",\n    \"gray\": \"灰色\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"Emoji 表情\",\n    \"search\": \"查找 Emoji\",\n    \"noRecent\": \"没有最近的 Emoji\",\n    \"noEmojiFound\": \"没有找到 Emoji\",\n    \"filter\": \"筛选\",\n    \"random\": \"随机\",\n    \"selectSkinTone\": \"选择肤色\",\n    \"remove\": \"移除\",\n    \"categories\": {\n      \"smileys\": \"表情与情感\",\n      \"people\": \"人与身体\",\n      \"animals\": \"动物与自然\",\n      \"food\": \"食物和饮料\",\n      \"activities\": \"活动\",\n      \"places\": \"旅行与地点\",\n      \"objects\": \"物体\",\n      \"symbols\": \"符号\",\n      \"flags\": \"旗帜\",\n      \"nature\": \"自然\",\n      \"frequentlyUsed\": \"经常使用\"\n    },\n    \"skinTone\": {\n      \"default\": \"默认\",\n      \"light\": \"亮\",\n      \"mediumLight\": \"偏亮\",\n      \"medium\": \"适中\",\n      \"mediumDark\": \"偏暗\",\n      \"dark\": \"暗\"\n    }\n  },\n  \"inlineActions\": {\n    \"noResults\": \"没有结果\",\n    \"pageReference\": \"页面参考\",\n    \"date\": \"日期\",\n    \"reminder\": {\n      \"groupTitle\": \"提醒\",\n      \"shortKeyword\": \"提醒\"\n    }\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"在设置中更改日期和时间格式\",\n    \"reminderOptions\": {\n      \"fiveMinsBefore\": \"5 分钟以前\",\n      \"tenMinsBefore\": \"10 分钟以前\",\n      \"fifteenMinsBefore\": \"15 分钟以前\",\n      \"thirtyMinsBefore\": \"30 分钟以前\",\n      \"oneHourBefore\": \"1 小时以前\",\n      \"twoHoursBefore\": \"2 小时以前\",\n      \"oneDayBefore\": \"1 天以前\",\n      \"twoDaysBefore\": \"2 天以前\",\n      \"oneWeekBefore\": \"1 周以前\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"昨天\",\n    \"today\": \"今天\",\n    \"tomorrow\": \"明天\",\n    \"oneWeek\": \"一周\"\n  },\n  \"notificationHub\": {\n    \"title\": \"通知\",\n    \"mobile\": {\n      \"title\": \"更新\"\n    },\n    \"emptyTitle\": \"都处理了！\",\n    \"emptyBody\": \"没有待处理的通知或操作。享受平静。\",\n    \"tabs\": {\n      \"inbox\": \"收件箱\",\n      \"upcoming\": \"即将推出\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"标记为已读\",\n      \"showAll\": \"全部\",\n      \"showUnreads\": \"未读\"\n    },\n    \"filters\": {\n      \"ascending\": \"升序\",\n      \"descending\": \"降序\",\n      \"groupByDate\": \"按日期分组\",\n      \"showUnreadsOnly\": \"仅显示未读\",\n      \"resetToDefault\": \"重置为默认\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"提醒\",\n    \"message\": \"记得在你忘记之前检查一下！\",\n    \"tooltipDelete\": \"删除\",\n    \"tooltipMarkRead\": \"标记为已读\",\n    \"tooltipMarkUnread\": \"标记为未读\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"寻找\",\n    \"previousMatch\": \"上一个匹配\",\n    \"nextMatch\": \"下一个匹配\",\n    \"close\": \"关闭\",\n    \"replace\": \"替换\",\n    \"replaceAll\": \"全部替换\",\n    \"noResult\": \"没有结果\",\n    \"caseSensitive\": \"区分大小写\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"我们很抱歉\",\n    \"loadingViewError\": \"我们在加载此视图时遇到问题。请检查您的互联网连接，刷新应用程序，如果问题仍然存在，请随时联系开发团队。\"\n  },\n  \"editor\": {\n    \"bold\": \"加粗\",\n    \"bulletedList\": \"符号列表\",\n    \"checkbox\": \"复选框\",\n    \"embedCode\": \"嵌入代码\",\n    \"heading1\": \"一级标题\",\n    \"heading2\": \"二级标题\",\n    \"heading3\": \"三级标题\",\n    \"highlight\": \"强调\",\n    \"color\": \"颜色\",\n    \"image\": \"图像\",\n    \"italic\": \"斜体\",\n    \"link\": \"链接\",\n    \"numberedList\": \"编号列表\",\n    \"quote\": \"引用\",\n    \"strikethrough\": \"删除线\",\n    \"text\": \"文本\",\n    \"underline\": \"下划线\",\n    \"fontColorDefault\": \"默认\",\n    \"fontColorGray\": \"灰色\",\n    \"fontColorBrown\": \"棕色\",\n    \"fontColorOrange\": \"橙色\",\n    \"fontColorYellow\": \"黄色\",\n    \"fontColorGreen\": \"绿色\",\n    \"fontColorBlue\": \"蓝色\",\n    \"fontColorPurple\": \"紫色\",\n    \"fontColorPink\": \"粉色\",\n    \"fontColorRed\": \"红色\",\n    \"backgroundColorDefault\": \"默认背景颜色\",\n    \"backgroundColorGray\": \"灰色背景\",\n    \"backgroundColorBrown\": \"棕色背景\",\n    \"backgroundColorOrange\": \"橙色背景\",\n    \"backgroundColorYellow\": \"黄色背景\",\n    \"backgroundColorGreen\": \"绿色背景\",\n    \"backgroundColorBlue\": \"蓝色背景\",\n    \"backgroundColorPurple\": \"紫色背景\",\n    \"backgroundColorPink\": \"粉色背景\",\n    \"backgroundColorRed\": \"红色背景\",\n    \"done\": \"完成\",\n    \"cancel\": \"取消\",\n    \"tint1\": \"色彩 1\",\n    \"tint2\": \"色彩 2\",\n    \"tint3\": \"色彩 3\",\n    \"tint4\": \"色彩 4\",\n    \"tint5\": \"色彩 5\",\n    \"tint6\": \"色彩 6\",\n    \"tint7\": \"色彩 7\",\n    \"tint8\": \"色彩 8\",\n    \"tint9\": \"色彩 9\",\n    \"lightLightTint1\": \"紫色\",\n    \"lightLightTint2\": \"粉色\",\n    \"lightLightTint3\": \"浅粉色\",\n    \"lightLightTint4\": \"橙色\",\n    \"lightLightTint5\": \"黄色\",\n    \"lightLightTint6\": \"鲜绿色\",\n    \"lightLightTint7\": \"绿色\",\n    \"lightLightTint8\": \"淡绿色\",\n    \"lightLightTint9\": \"蓝色\",\n    \"urlHint\": \"URL\",\n    \"mobileHeading1\": \"标题 1\",\n    \"mobileHeading2\": \"标题 2\",\n    \"mobileHeading3\": \"标题 3\",\n    \"textColor\": \"文字颜色\",\n    \"backgroundColor\": \"背景颜色\",\n    \"addYourLink\": \"添加您的链接\",\n    \"openLink\": \"打开链接\",\n    \"copyLink\": \"复制链接\",\n    \"removeLink\": \"移除链接\",\n    \"editLink\": \"编辑链接\",\n    \"linkText\": \"文本\",\n    \"linkTextHint\": \"请输入文本\",\n    \"linkAddressHint\": \"请输入 URL\",\n    \"highlightColor\": \"高亮颜色\",\n    \"clearHighlightColor\": \"清除高亮颜色\",\n    \"customColor\": \"定制颜色\",\n    \"hexValue\": \"十六进制值\",\n    \"opacity\": \"不透明度\",\n    \"resetToDefaultColor\": \"重置为默认颜色\",\n    \"ltr\": \"从左到右\",\n    \"rtl\": \"从右到左\",\n    \"auto\": \"自动\",\n    \"cut\": \"剪切\",\n    \"copy\": \"复制\",\n    \"paste\": \"粘贴\",\n    \"find\": \"查找\",\n    \"previousMatch\": \"上一个匹配\",\n    \"nextMatch\": \"下一个匹配\",\n    \"closeFind\": \"关闭\",\n    \"replace\": \"替换\",\n    \"replaceAll\": \"替换全部\",\n    \"regex\": \"正则表达式\",\n    \"caseSensitive\": \"区分大小写\",\n    \"uploadImage\": \"上传图片\",\n    \"urlImage\": \"URL 图片\",\n    \"incorrectLink\": \"错误链接\",\n    \"upload\": \"上传\",\n    \"chooseImage\": \"选择图像\",\n    \"loading\": \"加载中\",\n    \"imageLoadFailed\": \"无法加载图像\",\n    \"divider\": \"分隔线\",\n    \"table\": \"表格\",\n    \"colAddBefore\": \"在前面添加\",\n    \"rowAddBefore\": \"在前面添加\",\n    \"colAddAfter\": \"在后面添加\",\n    \"rowAddAfter\": \"在后面添加\",\n    \"colRemove\": \"移除列\",\n    \"rowRemove\": \"移除行\",\n    \"colDuplicate\": \"复制列\",\n    \"rowDuplicate\": \"复制行\",\n    \"colClear\": \"清空本列内容\",\n    \"rowClear\": \"清空本行内容\",\n    \"slashPlaceHolder\": \"输入 '/' 以插入块，或开始键入\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"没有收藏页面\",\n    \"noFavoriteHintText\": \"向左滑动页面即可将其添加到您的收藏夹\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"输入 “/” 以插入块，或开始键入\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"待办\",\n    \"bulletList\": \"列表\",\n    \"numberList\": \"列表\",\n    \"quote\": \"引用\",\n    \"heading\": \"标题 {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"页面图标\",\n    \"language\": \"语言\",\n    \"font\": \"字体\",\n    \"actions\": \"操作\",\n    \"date\": \"日期\",\n    \"addField\": \"添加字段\",\n    \"userIcon\": \"用户图标\"\n  },\n  \"newSettings\": {\n    \"myAccount\": {\n      \"title\": \"我的账户\",\n      \"subtitle\": \"自定义您的个人资料，管理账户安全，设置 AI keys，或登录您的账户。\",\n      \"profileLabel\": \"帐户名称 & 头像\",\n      \"profileNamePlaceholder\": \"输入你的名字\",\n      \"accountSecurity\": \"帐户安全\",\n      \"2FA\": \"两步验证\",\n      \"aiKeys\": \"AI keys\",\n      \"accountLogin\": \"登录账户\",\n      \"updateNameError\": \"更新名称失败\",\n      \"updateIconError\": \"更新图标失败\",\n      \"deleteAccount\": {\n        \"title\": \"删除帐户\",\n        \"subtitle\": \"永久删除你的帐户和所有数据。\",\n        \"description\": \"永久删除你的账户，并移除所有工作区的访问权限。\",\n        \"deleteMyAccount\": \"删除我的账户\",\n        \"dialogTitle\": \"删除帐户\",\n        \"dialogContent1\": \"你确定要永久删除您的帐户吗？\",\n        \"dialogContent2\": \"此操作无法撤消，并且将删除所有团队空间的访问权限，删除你的整个帐户（包括私人工作区），并将你从所有共享工作区中删除。\",\n        \"confirmHint1\": \"请输入 \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\" 以确认。\",\n        \"confirmHint2\": \"我理解此操作是不可逆的，并且将永久删除我的帐户和所有关联数据。\",\n        \"confirmHint3\": \"删除我的账户\",\n        \"checkToConfirmError\": \"你必须勾选以确认删除。\",\n        \"failedToGetCurrentUser\": \"获取当前用户邮箱失败\",\n        \"confirmTextValidationFailed\": \"你的确认文本不匹配 \\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\"\",\n        \"deleteAccountSuccess\": \"账户删除成功\"\n      }\n    },\n    \"workplace\": {\n      \"updateIconError\": \"图标更新失败\",\n      \"chooseAnIcon\": \"选择一个图标\",\n      \"appearance\": {\n        \"name\": \"外观\",\n        \"themeMode\": {\n          \"auto\": \"自动\",\n          \"light\": \"明亮\",\n          \"dark\": \"黑暗\"\n        },\n        \"language\": \"语言\"\n      }\n    }\n  },\n  \"pageStyle\": {\n    \"pageIcon\": \"页面图标\",\n    \"openSettings\": \"打开设置\",\n    \"image\": \"图像\"\n  },\n  \"commandPalette\": {\n    \"placeholder\": \"输入你要搜索的内容...\"\n  },\n  \"space\": {\n    \"defaultSpaceName\": \"一般\"\n  },\n  \"publish\": {\n    \"saveThisPage\": \"使用此模板创建\"\n  },\n  \"web\": {\n    \"continueWithGoogle\": \"使用 Google 账户登录\",\n    \"continueWithGithub\": \"使用 GitHub 账户登录\",\n    \"continueWithDiscord\": \"使用 Discord 账户登录\"\n  },\n  \"template\": {\n    \"deleteFromTemplate\": \"从模板中删除\",\n    \"relatedTemplates\": \"相关模板\",\n    \"deleteTemplate\": \"删除模板\",\n    \"removeRelatedTemplate\": \"移除相关模板\",\n    \"label\": \"模板\"\n  },\n  \"time\": {\n    \"justNow\": \"刚刚\",\n    \"seconds\": {\n      \"one\": \"1 秒\",\n      \"other\": \"{count} 秒\"\n    },\n    \"minutes\": {\n      \"one\": \"1 分钟\",\n      \"other\": \"{count} 分钟\"\n    },\n    \"hours\": {\n      \"one\": \"1 小时\",\n      \"other\": \"{count} 小时\"\n    },\n    \"days\": {\n      \"one\": \"1 天\",\n      \"other\": \"{count} 天\"\n    },\n    \"weeks\": {\n      \"one\": \"1 周\",\n      \"other\": \"{count} 周\"\n    },\n    \"months\": {\n      \"one\": \"1 月\",\n      \"other\": \"{count} 月\"\n    },\n    \"years\": {\n      \"one\": \"1 年\",\n      \"other\": \"{count} 年\"\n    },\n    \"ago\": \"前\",\n    \"yesterday\": \"昨天\",\n    \"today\": \"今天\"\n  }\n}"
  },
  {
    "path": "frontend/resources/translations/zh-TW.json",
    "content": "{\n  \"appName\": \"AppFlowy\",\n  \"defaultUsername\": \"我\",\n  \"welcomeText\": \"歡迎使用 @:appName\",\n  \"welcomeTo\": \"歡迎來到\",\n  \"githubStarText\": \"在 GitHub 上按讚/星標\",\n  \"subscribeNewsletterText\": \"訂閱電子報\",\n  \"letsGoButtonText\": \"快速開始\",\n  \"title\": \"標題\",\n  \"youCanAlso\": \"你也可以\",\n  \"and\": \"和\",\n  \"failedToOpenUrl\": \"無法開啟網址：{}\",\n  \"blockActions\": {\n    \"addBelowTooltip\": \"點選以在下方新增\",\n    \"addAboveCmd\": \"Alt+點選\",\n    \"addAboveMacCmd\": \"Option+點選\",\n    \"addAboveTooltip\": \"在上方新增\",\n    \"dragTooltip\": \"拖曳以移動\",\n    \"openMenuTooltip\": \"點選以開啟選單\"\n  },\n  \"signUp\": {\n    \"buttonText\": \"註冊\",\n    \"title\": \"註冊 @:appName\",\n    \"getStartedText\": \"開始使用\",\n    \"emptyPasswordError\": \"密碼不能為空\",\n    \"repeatPasswordEmptyError\": \"確認密碼不能為空\",\n    \"unmatchedPasswordError\": \"重複的密碼與密碼不相同。\",\n    \"alreadyHaveAnAccount\": \"已有帳戶？\",\n    \"emailHint\": \"電子郵件\",\n    \"passwordHint\": \"密碼\",\n    \"repeatPasswordHint\": \"確認密碼\",\n    \"signUpWith\": \"透過以下方式註冊：\"\n  },\n  \"signIn\": {\n    \"loginTitle\": \"登入 @:appName\",\n    \"loginButtonText\": \"登入\",\n    \"loginStartWithAnonymous\": \"繼續匿名對話\",\n    \"continueAnonymousUser\": \"繼續匿名對話\",\n    \"continueWithLocalModel\": \"繼續使用本地模型\",\n    \"switchToAppFlowyCloud\": \"AppFlowy 雲端\",\n    \"anonymousMode\": \"匿名模式\",\n    \"buttonText\": \"登入\",\n    \"signingInText\": \"正在登入...\",\n    \"forgotPassword\": \"忘記密碼？\",\n    \"emailHint\": \"電子郵件\",\n    \"passwordHint\": \"密碼\",\n    \"dontHaveAnAccount\": \"還沒有帳號？\",\n    \"createAccount\": \"建立帳戶\",\n    \"repeatPasswordEmptyError\": \"確認密碼不能為空\",\n    \"unmatchedPasswordError\": \"重複的密碼與密碼不相同。請重新輸入。\",\n    \"passwordMustContain\": \"密碼必須包含至少一個字母、一個數字和一個符號。\",\n    \"syncPromptMessage\": \"同步資料可能需要一些時間。請不要關閉此頁面\",\n    \"or\": \"或\",\n    \"signInWithGoogle\": \"使用 Google 登入\",\n    \"signInWithGithub\": \"使用 Github 登入\",\n    \"signInWithDiscord\": \"使用 Discord 登入\",\n    \"signInWithApple\": \"使用 Apple 帳號登入\",\n    \"continueAnotherWay\": \"使用其他方式登入\",\n    \"signUpWithGoogle\": \"使用 Google 註冊\",\n    \"signUpWithGithub\": \"使用 Github 註冊\",\n    \"signUpWithDiscord\": \"使用 Discord 註冊\",\n    \"signInWith\": \"透過以下方式登入:\",\n    \"signInWithEmail\": \"使用電子郵件登入\",\n    \"signInWithMagicLink\": \"繼續\",\n    \"signUpWithMagicLink\": \"使用 Magic Link 註冊\",\n    \"pleaseInputYourEmail\": \"請輸入您的電郵地址\",\n    \"settings\": \"設定\",\n    \"magicLinkSent\": \"我們己發送 Magic Link 到您的電子郵件，點擊連結登入\",\n    \"invalidEmail\": \"請輸入有效的電郵地址\",\n    \"alreadyHaveAnAccount\": \"已經有帳戶？\",\n    \"logIn\": \"登入\",\n    \"generalError\": \"出了些問題。請稍後再試\",\n    \"limitRateError\": \"出於安全原因，您只能每 60 秒申請一次 Magic Link\",\n    \"magicLinkSentDescription\": \"連結已發送到您的電子信箱。點擊連結即可登入。連結將在 5 分鐘後過期。\",\n    \"tokenHasExpiredOrInvalid\": \"此代碼已過期或無效，請重新嘗試。\",\n    \"signingIn\": \"正在登入…\",\n    \"checkYourEmail\": \"檢查您的電子郵件信箱\",\n    \"temporaryVerificationLinkSent\": \"已發送一封暫時驗證連結。 請查看您的收件匣，地址為:\",\n    \"temporaryVerificationCodeSent\": \"已發送一封暫時驗證碼。 請查看您的收件匣，地址為:\",\n    \"continueToSignIn\": \"繼續登入\",\n    \"continueWithLoginCode\": \"繼續使用登入代碼\",\n    \"backToLogin\": \"返回登入頁面\",\n    \"enterCode\": \"輸入代碼\",\n    \"enterCodeManually\": \"手動輸入代碼\",\n    \"continueWithEmail\": \"繼續使用電子郵件信箱\",\n    \"enterPassword\": \"輸入密碼\",\n    \"loginAs\": \"身分登入以\",\n    \"invalidVerificationCode\": \"請輸入有效的驗證碼\",\n    \"tooFrequentVerificationCodeRequest\": \"您發起了過多的請求，請稍後再試。\",\n    \"invalidLoginCredentials\": \"您的密碼不正確，請重新嘗試。\",\n    \"resetPassword\": \"重設密碼\",\n    \"resetPasswordDescription\": \"輸入您的電子郵件信箱以重設您的密碼\",\n    \"continueToResetPassword\": \"繼續重設密碼\",\n    \"resetPasswordSuccess\": \"密碼已成功重設\",\n    \"resetPasswordFailed\": \"密碼重設失敗\",\n    \"resetPasswordLinkSent\": \"一封密碼重置連結已發送至您的電子郵件信箱。請查看您的收件匣於\",\n    \"resetPasswordLinkExpired\": \"密碼重置連結已過期，請重新要求新的連結。\",\n    \"resetPasswordLinkInvalid\": \"密碼重置連結無效，請重新要求新的連結。\",\n    \"enterNewPasswordFor\": \"輸入新密碼為\",\n    \"newPassword\": \"新密碼\",\n    \"enterNewPassword\": \"輸入新密碼\",\n    \"confirmPassword\": \"確認密碼\",\n    \"confirmNewPassword\": \"輸入新密碼\",\n    \"newPasswordCannotBeEmpty\": \"新密碼不能為空\",\n    \"confirmPasswordCannotBeEmpty\": \"確認密碼不能為空\",\n    \"passwordsDoNotMatch\": \"密碼不相符。請重新輸入\",\n    \"verifying\": \"正在驗證…\",\n    \"continueWithPassword\": \"繼續使用密碼\",\n    \"youAreInLocalMode\": \"您目前處於本地模式。\",\n    \"loginToAppFlowyCloud\": \"登入 AppFlowy Cloud\"\n  },\n  \"workspace\": {\n    \"chooseWorkspace\": \"選擇你的工作區\",\n    \"defaultName\": \"我的工作區\",\n    \"create\": \"建立工作區\",\n    \"new\": \"新的工作區\",\n    \"importFromNotion\": \"從 Notion 匯入\",\n    \"learnMore\": \"了解更多\",\n    \"reset\": \"重設工作區\",\n    \"renameWorkspace\": \"重新命名工作區\",\n    \"workspaceNameCannotBeEmpty\": \"工作區名稱不能為空\",\n    \"resetWorkspacePrompt\": \"重設工作區將刪除其中所有頁面和資料。你確定要重設工作區嗎？或者，你可以聯絡支援團隊來恢復工作區。\",\n    \"hint\": \"工作區\",\n    \"notFoundError\": \"找不到工作區\",\n    \"failedToLoad\": \"出了些問題！無法載入工作區。請嘗試關閉 @:appName 的任何開啟執行個體，然後再試一次。\",\n    \"errorActions\": {\n      \"reportIssue\": \"回報問題\",\n      \"reportIssueOnGithub\": \"在 GitHub 上回報問題\",\n      \"exportLogFiles\": \"匯出記錄檔\",\n      \"reachOut\": \"在 Discord 上聯絡我們\"\n    },\n    \"menuTitle\": \"工作區\",\n    \"deleteWorkspaceHintText\": \"您確定要刪除工作區嗎？ 此動作無法復原，您已發布的任何頁面都將會取消發布。\",\n    \"createSuccess\": \"成功創建工作區\",\n    \"createFailed\": \"無法創建工作區\",\n    \"createLimitExceeded\": \"您已達到帳戶允許的最大工作區限制。如果您需要額外的工作空間，請在 Github 上申請\",\n    \"deleteSuccess\": \"工作區刪除成功\",\n    \"deleteFailed\": \"工作區刪除失敗\",\n    \"openSuccess\": \"成功開啟工作區\",\n    \"openFailed\": \"無法開啟工作區\",\n    \"renameSuccess\": \"工作區重新命名成功\",\n    \"renameFailed\": \"無法重新命名工作區\",\n    \"updateIconSuccess\": \"更新工作區圖示成功\",\n    \"updateIconFailed\": \"無法更新工作區圖示\",\n    \"cannotDeleteTheOnlyWorkspace\": \"無法刪除唯一的工作區\",\n    \"fetchWorkspacesFailed\": \"無法取得工作區\",\n    \"leaveCurrentWorkspace\": \"離開工作區\",\n    \"leaveCurrentWorkspacePrompt\": \"您確定要離開當前工作區嗎？\"\n  },\n  \"shareAction\": {\n    \"buttonText\": \"分享\",\n    \"workInProgress\": \"即將推出\",\n    \"markdown\": \"Markdown\",\n    \"html\": \"HTML\",\n    \"clipboard\": \"複製到剪貼簿\",\n    \"csv\": \"CSV\",\n    \"copyLink\": \"複製連結\",\n    \"publishToTheWeb\": \"發佈到網頁\",\n    \"publishToTheWebHint\": \"使用 AppFlowy 建立網站\",\n    \"publish\": \"發佈\",\n    \"unPublish\": \"取消發佈\",\n    \"visitSite\": \"前往網站\",\n    \"exportAsTab\": \"匯出為\",\n    \"publishTab\": \"發佈\",\n    \"shareTab\": \"分享\",\n    \"publishOnAppFlowy\": \"在 AppFlowy 上發布\",\n    \"shareTabTitle\": \"邀請協作\",\n    \"shareTabDescription\": \"方便與任何人協作\",\n    \"copyLinkSuccess\": \"連結已複製到剪貼簿\",\n    \"copyShareLink\": \"複製分享連結\",\n    \"copyLinkFailed\": \"無法將連結複製到剪貼簿\",\n    \"copyLinkToBlockSuccess\": \"區塊連結已複製到剪貼簿\",\n    \"copyLinkToBlockFailed\": \"無法將區塊連結複製到剪貼簿\",\n    \"manageAllSites\": \"管理所有網站\",\n    \"updatePathName\": \"更新路徑名稱\"\n  },\n  \"moreAction\": {\n    \"small\": \"小\",\n    \"medium\": \"中\",\n    \"large\": \"大\",\n    \"fontSize\": \"字型大小\",\n    \"import\": \"匯入\",\n    \"moreOptions\": \"更多選項\",\n    \"wordCount\": \"字數: {}\",\n    \"charCount\": \"字元數: {}\",\n    \"createdAt\": \"建立於: {}\",\n    \"deleteView\": \"刪除\",\n    \"duplicateView\": \"副本\",\n    \"wordCountLabel\": \"字數:\",\n    \"charCountLabel\": \"字元數:\",\n    \"createdAtLabel\": \"建立於:\",\n    \"syncedAtLabel\": \"已同步:\",\n    \"saveAsNewPage\": \"新增訊息到頁面\",\n    \"saveAsNewPageDisabled\": \"沒有可用的訊息\"\n  },\n  \"importPanel\": {\n    \"textAndMarkdown\": \"文字 & Markdown\",\n    \"documentFromV010\": \"文件來自 v0.1.0\",\n    \"databaseFromV010\": \"資料庫來自 v0.1.0\",\n    \"notionZip\": \"Notion 匯出 ZIP 檔案\",\n    \"csv\": \"CSV\",\n    \"database\": \"資料庫\"\n  },\n  \"emojiIconPicker\": {\n    \"iconUploader\": {\n      \"placeholderLeft\": \"拖放檔案，或點擊以\",\n      \"placeholderUpload\": \"上傳\",\n      \"placeholderRight\": \", 或貼上圖片連結。\",\n      \"dropToUpload\": \"將檔案拖放到這裡以上傳\",\n      \"change\": \"變更\\n\"\n    }\n  },\n  \"disclosureAction\": {\n    \"rename\": \"重新命名\",\n    \"delete\": \"刪除\",\n    \"duplicate\": \"副本\",\n    \"unfavorite\": \"從最愛中移除\",\n    \"favorite\": \"加入最愛\",\n    \"openNewTab\": \"在新分頁中開啟\",\n    \"moveTo\": \"移動到\",\n    \"addToFavorites\": \"加入最愛\",\n    \"copyLink\": \"複製連結\",\n    \"changeIcon\": \"更改圖示\",\n    \"collapseAllPages\": \"折疊所有子頁面\",\n    \"movePageTo\": \"將頁面移動到\",\n    \"move\": \"移動\",\n    \"lockPage\": \"鎖定頁面\"\n  },\n  \"blankPageTitle\": \"空白頁面\",\n  \"newPageText\": \"新增頁面\",\n  \"newDocumentText\": \"新增文件\",\n  \"newGridText\": \"新增網格\",\n  \"newCalendarText\": \"新增日曆\",\n  \"newBoardText\": \"新增看板\",\n  \"chat\": {\n    \"newChat\": \"AI 聊天\",\n    \"inputMessageHint\": \"詢問 @:appName AI\",\n    \"inputLocalAIMessageHint\": \"詢問 @:appName 本地 AI\",\n    \"unsupportedCloudPrompt\": \"此功能僅在使用 @:appName Cloud 時可用\",\n    \"relatedQuestion\": \"相關問題\",\n    \"serverUnavailable\": \"服務暫時無法使用，請稍後再試。\",\n    \"aiServerUnavailable\": \"AI 服務目前暫時不可用。請稍後再試一次。\",\n    \"retry\": \"重試\",\n    \"clickToRetry\": \"點擊重試\",\n    \"regenerateAnswer\": \"重新產生\",\n    \"question1\": \"如何使用看板來管理任務\",\n    \"question2\": \"解釋 GTD 方法\",\n    \"question3\": \"為什麼要使用 Rust\",\n    \"question4\": \"用廚房裡現有的食材製作食譜\",\n    \"question5\": \"為我的頁面創建插圖\",\n    \"question6\": \"為我即將到來的下週擬定待辦事項清單\",\n    \"aiMistakePrompt\": \"AI 可能會犯錯，請檢查重要資訊。\",\n    \"chatWithFilePrompt\": \"您想與檔案聊天嗎？\",\n    \"indexFileSuccess\": \"檔案索引成功\",\n    \"inputActionNoPages\": \"沒有頁面結果\",\n    \"referenceSource\": {\n      \"zero\": \"未找到 0 個來源\",\n      \"one\": \"已找到 {count} 個來源\",\n      \"other\": \"已找到 {count} 個來源\"\n    },\n    \"clickToMention\": \"提及頁面\",\n    \"uploadFile\": \"附加 PDF、文字或 Markdown 檔案。\",\n    \"questionDetail\": \"嗨，{}！今天我能如何幫助您？\",\n    \"indexingFile\": \"正在索引 {}\",\n    \"generatingResponse\": \"正在產生回應\",\n    \"selectSources\": \"選擇來源\",\n    \"currentPage\": \"目前頁面\",\n    \"sourcesLimitReached\": \"您只能選取最多 3 個頂層文件及其子文件\",\n    \"sourceUnsupported\": \"目前我們不支援與資料庫聊天\",\n    \"regenerate\": \"再試一次\",\n    \"addToPageButton\": \"新增訊息到頁面\",\n    \"addToPageTitle\": \"新增訊息到...\",\n    \"addToNewPage\": \"建立新頁面\",\n    \"addToNewPageName\": \"從\\\"{}\\\"中提取的訊息\",\n    \"addToNewPageSuccessToast\": \"已將訊息新增至\",\n    \"openPagePreviewFailedToast\": \"開啟頁面失敗\",\n    \"changeFormat\": {\n      \"actionButton\": \"變更格式\",\n      \"confirmButton\": \"用此格式重新產生\",\n      \"textOnly\": \"文字\",\n      \"imageOnly\": \"僅限圖片\",\n      \"textAndImage\": \"文字和圖片\",\n      \"text\": \"段落\",\n      \"bullet\": \"項目符號清單\",\n      \"number\": \"編號清單\",\n      \"table\": \"表格\",\n      \"blankDescription\": \"格式化回應\",\n      \"defaultDescription\": \"自動回應格式\",\n      \"textWithImageDescription\": \"@:chat .changeFormat.text 搭配圖像\",\n      \"numberWithImageDescription\": \"@:chat .changeFormat.number 搭配圖像\",\n      \"bulletWithImageDescription\": \"@:chat .changeFormat.bullet 搭配圖像\",\n      \"tableWithImageDescription\": \"@:chat .changeFormat.table 搭配圖像\"\n    },\n    \"switchModel\": {\n      \"label\": \"切換模型\",\n      \"localModel\": \"本地模型\",\n      \"cloudModel\": \"雲端模型\",\n      \"autoModel\": \"自動\"\n    },\n    \"selectBanner\": {\n      \"saveButton\": \"新增到…\",\n      \"selectMessages\": \"選擇訊息\",\n      \"nSelected\": \" {} 已選取\",\n      \"allSelected\": \"全部已選取\"\n    },\n    \"stopTooltip\": \"停止生成\"\n  },\n  \"trash\": {\n    \"text\": \"垃圾桶\",\n    \"restoreAll\": \"全部還原\",\n    \"restore\": \"還原\",\n    \"deleteAll\": \"全部刪除\",\n    \"pageHeader\": {\n      \"fileName\": \"檔案名稱\",\n      \"lastModified\": \"最近修改時間\",\n      \"created\": \"建立時間\"\n    },\n    \"confirmDeleteAll\": {\n      \"title\": \"垃圾桶中的所有頁面\",\n      \"caption\": \"您確定要刪除垃圾桶中的所有內容嗎？此動作無法復原。\"\n    },\n    \"confirmRestoreAll\": {\n      \"title\": \"還原垃圾桶中的所有頁面\",\n      \"caption\": \"這個動作無法復原。\"\n    },\n    \"restorePage\": {\n      \"title\": \"還原: {}\",\n      \"caption\": \"您確定要還原此頁面嗎？\"\n    },\n    \"mobile\": {\n      \"actions\": \"垃圾桶操作\",\n      \"empty\": \"垃圾桶中沒有頁面或空間\",\n      \"emptyDescription\": \"將不需要的項目移至垃圾桶。\",\n      \"isDeleted\": \"已刪除\",\n      \"isRestored\": \"已還原\"\n    },\n    \"confirmDeleteTitle\": \"您確定要永久刪除此頁面嗎？\"\n  },\n  \"deletePagePrompt\": {\n    \"text\": \"此頁面在垃圾桶中\",\n    \"restore\": \"還原頁面\",\n    \"deletePermanent\": \"永久刪除\",\n    \"deletePermanentDescription\": \"您確定要永久刪除此頁面嗎？這是一個不可逆的動作。\"\n  },\n  \"dialogCreatePageNameHint\": \"頁面名稱\",\n  \"questionBubble\": {\n    \"shortcuts\": \"快捷鍵\",\n    \"whatsNew\": \"最新消息？\",\n    \"helpAndDocumentation\": \"說明與文件。\",\n    \"getSupport\": \"取得支援\",\n    \"markdown\": \"Markdown\",\n    \"debug\": {\n      \"name\": \"除錯資訊\",\n      \"success\": \"已將除錯資訊複製到剪貼簿\",\n      \"fail\": \"無法將除錯資訊複製到剪貼簿\"\n    },\n    \"feedback\": \"意見回饋\"\n  },\n  \"menuAppHeader\": {\n    \"moreButtonToolTip\": \"移除、重新命名等等...\",\n    \"addPageTooltip\": \"快速在此新增頁面\",\n    \"defaultNewPageName\": \"無標題\",\n    \"renameDialog\": \"重新命名\",\n    \"pageNameSuffix\": \"複製\"\n  },\n  \"noPagesInside\": \"裡面沒有頁面。\",\n  \"toolbar\": {\n    \"undo\": \"復原\",\n    \"redo\": \"重做\",\n    \"bold\": \"粗體\",\n    \"italic\": \"斜體\",\n    \"underline\": \"底線\",\n    \"strike\": \"刪除線\",\n    \"numList\": \"編號清單\",\n    \"bulletList\": \"項目符號清單\",\n    \"checkList\": \"核取清單\",\n    \"inlineCode\": \"行內程式碼\",\n    \"quote\": \"區塊引述\",\n    \"header\": \"標題\",\n    \"highlight\": \"醒目提示\",\n    \"color\": \"顏色\",\n    \"addLink\": \"新增連結\"\n  },\n  \"tooltip\": {\n    \"lightMode\": \"切換至淺色模式\",\n    \"darkMode\": \"切換至深色模式\",\n    \"openAsPage\": \"以頁面開啟\",\n    \"addNewRow\": \"新增一列\",\n    \"openMenu\": \"點選以開啟選單\",\n    \"dragRow\": \"長按以重新排序列\",\n    \"viewDataBase\": \"檢視資料庫\",\n    \"referencePage\": \"這個 {name} 已被引用\",\n    \"addBlockBelow\": \"在下方新增一個區塊\",\n    \"aiGenerate\": \"生成\"\n  },\n  \"sideBar\": {\n    \"closeSidebar\": \"關閉側欄\",\n    \"openSidebar\": \"開啟側欄\",\n    \"expandSidebar\": \"展開為完整頁面\",\n    \"personal\": \"個人\",\n    \"private\": \"私人\",\n    \"workspace\": \"工作區\",\n    \"favorites\": \"最愛\",\n    \"clickToHidePrivate\": \"點擊以隱藏私人空間\\n您在此處建立的頁面只有您自己可見\",\n    \"clickToHideWorkspace\": \"點擊以隱藏工作區\\n您在此處建立的頁面對每個成員都可見\",\n    \"clickToHidePersonal\": \"點選以隱藏個人區塊\",\n    \"clickToHideFavorites\": \"點選以隱藏最愛區塊\",\n    \"addAPage\": \"新增頁面\",\n    \"addAPageToPrivate\": \"新增頁面到私人空間\",\n    \"addAPageToWorkspace\": \"將頁面新增至工作區\",\n    \"recent\": \"最近\",\n    \"today\": \"今天\",\n    \"thisWeek\": \"本週\",\n    \"others\": \"較早的收藏夾\",\n    \"earlier\": \"之前\",\n    \"justNow\": \"剛剛\",\n    \"minutesAgo\": \"{count} 分鐘前\",\n    \"lastViewed\": \"上次瀏覽\",\n    \"favoriteAt\": \"已收藏\",\n    \"emptyRecent\": \"沒有最近的頁面\",\n    \"emptyRecentDescription\": \"當您檢視頁面時，它們將會出現在這裡，方便您取回。\",\n    \"emptyFavorite\": \"沒有收藏的頁面\",\n    \"emptyFavoriteDescription\": \"將頁面標記為收藏 - 它們將列在此處以便您可以快速存取!\",\n    \"removePageFromRecent\": \"要從最近的頁面中刪除此頁面嗎?\",\n    \"removeSuccess\": \"刪除成功\",\n    \"favoriteSpace\": \"收藏\",\n    \"RecentSpace\": \"最近的\",\n    \"Spaces\": \"空間\",\n    \"upgradeToPro\": \"升級到專業版\",\n    \"upgradeToAIMax\": \"解鎖無限 AI\",\n    \"storageLimitDialogTitle\": \"您的免費儲存空間已用完，升級以解鎖無限儲存空間\",\n    \"storageLimitDialogTitleIOS\": \"您的免費儲存空間已用完。\",\n    \"aiResponseLimitTitle\": \"您的免費 AI 回覆已用完，升級到專業版或購買 AI 附加方案以解鎖無限回覆\",\n    \"aiResponseLimitDialogTitle\": \"AI 回覆已達到限制\",\n    \"aiResponseLimit\": \"您已用完免費的 AI 回應。\\n前往 設定 -> 方案 -> 點擊 AI Max 或專業版方案以獲得更多 AI 回應。\",\n    \"askOwnerToUpgradeToPro\": \"您的工作區即將耗盡免費儲存空間。請要求您的工作區擁有者升級到專業版\",\n    \"askOwnerToUpgradeToProIOS\": \"您的工作區即將耗盡免費儲存空間。\",\n    \"askOwnerToUpgradeToAIMax\": \"您的工作區已用完免費的 AI 回應。請要求您的工作區擁有者升級方案或購買 AI 附加功能\",\n    \"askOwnerToUpgradeToAIMaxIOS\": \"您的工作區即將耗盡免費的 AI 回應。\",\n    \"purchaseAIMax\": \"您的工作區已用完 AI 圖片回應。請要求您的工作區擁有者購買 AI Max。\",\n    \"aiImageResponseLimit\": \"您已用完 AI 圖像回應。\\n前往 設定 -> 方案 -> 點擊 AI Max 以獲得更多 AI 圖像回應\",\n    \"purchaseStorageSpace\": \"購買儲存空間\",\n    \"singleFileProPlanLimitationDescription\": \"您已超過免費方案允許的最大檔案上傳容量。請升級到專業版以上傳更大的檔案\",\n    \"purchaseAIResponse\": \"購買\",\n    \"askOwnerToUpgradeToLocalAI\": \"要求工作區擁有者啟用 AI 裝置端功能\",\n    \"upgradeToAILocal\": \"在您的設備上執行本地模型，以獲得終極隱私保護\",\n    \"upgradeToAILocalDesc\": \"在您的設備上執行本地模型，以獲得終極隱私保護\"\n  },\n  \"notifications\": {\n    \"export\": {\n      \"markdown\": \"已將筆記匯出成 Markdown\",\n      \"path\": \"文件/flowy\"\n    }\n  },\n  \"contactsPage\": {\n    \"title\": \"聯絡人\",\n    \"whatsHappening\": \"本週有什麼事發生?\",\n    \"addContact\": \"新增聯絡人\",\n    \"editContact\": \"編輯聯絡人\"\n  },\n  \"button\": {\n    \"ok\": \"確定\",\n    \"confirm\": \"確認\",\n    \"done\": \"完成\",\n    \"cancel\": \"取消\",\n    \"signIn\": \"登入\",\n    \"signOut\": \"登出\",\n    \"complete\": \"完成\",\n    \"change\": \"變更\",\n    \"save\": \"儲存\",\n    \"generate\": \"生成\",\n    \"esc\": \"ESC\",\n    \"keep\": \"保留\",\n    \"tryAgain\": \"再試一次\",\n    \"discard\": \"放棄變更\",\n    \"replace\": \"取代\",\n    \"insertBelow\": \"在下方插入\",\n    \"insertAbove\": \"在上方插入\",\n    \"upload\": \"上傳\",\n    \"edit\": \"編輯\",\n    \"delete\": \"刪除\",\n    \"copy\": \"複製\",\n    \"duplicate\": \"複製\",\n    \"putback\": \"放回\",\n    \"update\": \"更新\",\n    \"share\": \"分享\",\n    \"removeFromFavorites\": \"從最愛中移除\",\n    \"removeFromRecent\": \"從最近刪除\",\n    \"addToFavorites\": \"加入最愛\",\n    \"favoriteSuccessfully\": \"收藏成功\",\n    \"unfavoriteSuccessfully\": \"已成功取消收藏\",\n    \"duplicateSuccessfully\": \"複製成功\",\n    \"rename\": \"重新命名\",\n    \"helpCenter\": \"支援中心\",\n    \"add\": \"新增\",\n    \"yes\": \"是\",\n    \"no\": \"否\",\n    \"clear\": \"清除\",\n    \"remove\": \"刪除\",\n    \"dontRemove\": \"不要刪除\",\n    \"copyLink\": \"複製連結\",\n    \"align\": \"對齊\",\n    \"login\": \"登入\",\n    \"logout\": \"登出\",\n    \"deleteAccount\": \"刪除帳號\",\n    \"back\": \"返回\",\n    \"signInGoogle\": \"使用 Google 登入\",\n    \"signInGithub\": \"使用 Github 登入\",\n    \"signInDiscord\": \"使用 Discord 登入\",\n    \"more\": \"更多\",\n    \"create\": \"新增\",\n    \"close\": \"關閉\",\n    \"next\": \"下一步\",\n    \"previous\": \"上一步\",\n    \"submit\": \"提交\",\n    \"download\": \"下載\",\n    \"backToHome\": \"回首頁\",\n    \"viewing\": \"檢視中\",\n    \"editing\": \"編輯中\",\n    \"gotIt\": \"知道了\",\n    \"retry\": \"重試\",\n    \"uploadFailed\": \"上傳失敗。\",\n    \"copyLinkOriginal\": \"複製原始連結\"\n  },\n  \"label\": {\n    \"welcome\": \"歡迎!\",\n    \"firstName\": \"名字\",\n    \"middleName\": \"中間名\",\n    \"lastName\": \"姓氏\",\n    \"stepX\": \"步驟 {X}\"\n  },\n  \"oAuth\": {\n    \"err\": {\n      \"failedTitle\": \"無法連接至您的帳號。\",\n      \"failedMsg\": \"請確認您已在瀏覽器中完成登入程序。\"\n    },\n    \"google\": {\n      \"title\": \"GOOGLE  帳號登入\",\n      \"instruction1\": \"若要匯入您的 Google 聯絡人，您必須透過瀏覽器授權此應用程式。\",\n      \"instruction2\": \"點選圖示或選取文字以複製程式碼到剪貼簿:\",\n      \"instruction3\": \"前往下列網址，並輸入上述程式碼:\",\n      \"instruction4\": \"完成註冊後，請點選下方按鈕:\"\n    }\n  },\n  \"settings\": {\n    \"title\": \"設定\",\n    \"popupMenuItem\": {\n      \"settings\": \"設定\",\n      \"members\": \"成員\",\n      \"trash\": \"垃圾桶\",\n      \"helpAndDocumentation\": \"說明與文件\",\n      \"getSupport\": \"取得支援\"\n    },\n    \"sites\": {\n      \"title\": \"網站\",\n      \"namespaceTitle\": \"命名空間\",\n      \"namespaceDescription\": \"管理您的命名空間和首頁\",\n      \"namespaceHeader\": \"命名空間\",\n      \"homepageHeader\": \"首頁\",\n      \"updateNamespace\": \"更新命名空間\",\n      \"removeHomepage\": \"移除首頁\",\n      \"selectHomePage\": \"選擇一個頁面\",\n      \"clearHomePage\": \"清除此命名空間的首頁\",\n      \"customUrl\": \"自訂網址\",\n      \"homePage\": {\n        \"upgradeToPro\": \"要設定首頁，請升級到專業版\"\n      },\n      \"namespace\": {\n        \"description\": \"此變更將適用於此命名空間中所有已發佈的頁面\",\n        \"tooltip\": \"我們保留刪除任何不當命名空間的權利\",\n        \"updateExistingNamespace\": \"更新現有命名空間\",\n        \"upgradeToPro\": \"要擁有自訂命名空間，請升級到專業版\",\n        \"redirectToPayment\": \"正在重新導向至付款頁面…\",\n        \"onlyWorkspaceOwnerCanSetHomePage\": \"只有工作區擁有者才能設定首頁\",\n        \"pleaseAskOwnerToSetHomePage\": \"請要求工作區擁有者升級到專業版\"\n      },\n      \"publishedPage\": {\n        \"title\": \"所有已發佈的頁面\",\n        \"description\": \"管理您的已發佈頁面\",\n        \"page\": \"頁面\",\n        \"pathName\": \"路徑名稱\",\n        \"date\": \"發布日期\",\n        \"emptyHinText\": \"這個工作區沒有已發佈的頁面\",\n        \"noPublishedPages\": \"沒有已發布的頁面\",\n        \"settings\": \"發布設定\",\n        \"clickToOpenPageInApp\": \"在應用程式中開啟頁面\",\n        \"clickToOpenPageInBrowser\": \"在瀏覽器中開啟頁面\"\n      },\n      \"error\": {\n        \"failedToGeneratePaymentLink\": \"專業版付款連結產生失敗\",\n        \"failedToUpdateNamespace\": \"更新命名空間失敗\",\n        \"proPlanLimitation\": \"您需要升級到專業版才能更新命名空間\",\n        \"namespaceAlreadyInUse\": \"這個命名空間已經被佔用，請嘗試另一個\",\n        \"invalidNamespace\": \"無效的命名空間，請嘗試另一個\",\n        \"namespaceLengthAtLeast2Characters\": \"命名空間至少必須有 2 個字元長度\",\n        \"onlyWorkspaceOwnerCanUpdateNamespace\": \"只有工作區擁有者才能更新命名空間\",\n        \"onlyWorkspaceOwnerCanRemoveHomepage\": \"只有工作區擁有者才能移除首頁\",\n        \"setHomepageFailed\": \"設定首頁失敗\",\n        \"namespaceTooLong\": \"命名空間太長，請嘗試另一個\",\n        \"namespaceTooShort\": \"命名空間太短，請嘗試另一個\",\n        \"namespaceIsReserved\": \"命名空間已保留，請嘗試另一個\",\n        \"updatePathNameFailed\": \"更新路徑名稱失敗\",\n        \"removeHomePageFailed\": \"移除首頁失敗\",\n        \"publishNameContainsInvalidCharacters\": \"路徑名稱包含無效字元，請嘗試另一個\",\n        \"publishNameTooShort\": \"徑名稱太短，請嘗試另一個\",\n        \"publishNameTooLong\": \"路徑名稱太長，請嘗試另一個\",\n        \"publishNameAlreadyInUse\": \"路徑名稱已經在使用中，請嘗試另一個\",\n        \"namespaceContainsInvalidCharacters\": \"命名空間包含無效字元，請嘗試另一個\",\n        \"publishPermissionDenied\": \"只有工作區擁有者或頁面發布者才能管理發佈設定\",\n        \"publishNameCannotBeEmpty\": \"路徑名稱不能為空，請嘗試另一個\"\n      },\n      \"success\": {\n        \"namespaceUpdated\": \"命名空間更新成功\",\n        \"setHomepageSuccess\": \"設定首頁成功\",\n        \"updatePathNameSuccess\": \"路徑名稱已成功更新\",\n        \"removeHomePageSuccess\": \"移除首頁成功\"\n      }\n    },\n    \"accountPage\": {\n      \"menuLabel\": \"我帳戶與應用程式\",\n      \"title\": \"我的帳號\",\n      \"general\": {\n        \"title\": \"帳號名稱和個人資料圖片\",\n        \"changeProfilePicture\": \"更改個人資料圖片\"\n      },\n      \"email\": {\n        \"title\": \"電子郵件\",\n        \"actions\": {\n          \"change\": \"變更電子郵件地址\"\n        }\n      },\n      \"login\": {\n        \"title\": \"帳戶登入\",\n        \"loginLabel\": \"登入\",\n        \"logoutLabel\": \"登出\"\n      },\n      \"isUpToDate\": \"@:appName 已更新!\",\n      \"officialVersion\": \"版本 {version} (Official build)\"\n    },\n    \"workspacePage\": {\n      \"menuLabel\": \"工作區\",\n      \"title\": \"工作區\",\n      \"description\": \"自訂您的工作區外觀，包括主題、字型、文字佈局、日期/時間格式和語言。\",\n      \"workspaceName\": {\n        \"title\": \"工作區名稱\"\n      },\n      \"workspaceIcon\": {\n        \"title\": \"工作區圖示\",\n        \"description\": \"上傳圖片或使用表情符號作為您的工作區圖示。圖示將顯示在側邊欄和通知中。\"\n      },\n      \"appearance\": {\n        \"title\": \"外觀\",\n        \"description\": \"自訂您的工作區外觀，包括主題、字型、文字佈局、日期、時間和語言。\",\n        \"options\": {\n          \"system\": \"自動\",\n          \"light\": \"淺色\",\n          \"dark\": \"深色\"\n        }\n      },\n      \"resetCursorColor\": {\n        \"title\": \"重設文件游標顏色\",\n        \"description\": \"您確定要重設游標顏色嗎?\"\n      },\n      \"resetSelectionColor\": {\n        \"title\": \"重設文件選取顏色\",\n        \"description\": \"您確定要重設選取顏色嗎?\"\n      },\n      \"resetWidth\": {\n        \"resetSuccess\": \"文件寬度已成功重設。\"\n      },\n      \"theme\": {\n        \"title\": \"主題\",\n        \"description\": \"選擇預設主題，或上傳您自己的自訂主題。\",\n        \"uploadCustomThemeTooltip\": \"上傳自訂主題\",\n        \"failedToLoadThemes\": \"載入主題失敗，請檢查您的系統設定中的權限設定：系統設定 > 隱私與安全性 > 檔案和資料夾 > @:appName\"\n      },\n      \"workspaceFont\": {\n        \"title\": \"工作區字型\",\n        \"noFontHint\": \"找不到字型，請嘗試另一個詞彙。\"\n      },\n      \"textDirection\": {\n        \"title\": \"文字方向\",\n        \"leftToRight\": \"從左到右\",\n        \"rightToLeft\": \"從右到左\",\n        \"auto\": \"自動\",\n        \"enableRTLItems\": \"啟用從右到左工具列項目\"\n      },\n      \"layoutDirection\": {\n        \"title\": \"版面配置方向\",\n        \"leftToRight\": \"從左到右\",\n        \"rightToLeft\": \"從右到左\"\n      },\n      \"dateTime\": {\n        \"title\": \"日期與時間\",\n        \"example\": \"{} 在 {} ({})\",\n        \"24HourTime\": \"24 小時制\",\n        \"dateFormat\": {\n          \"label\": \"日期格式\",\n          \"local\": \"本地\",\n          \"us\": \"美國\",\n          \"iso\": \"ISO\",\n          \"friendly\": \"友善\",\n          \"dmy\": \"日/月/年\"\n        }\n      },\n      \"language\": {\n        \"title\": \"語言\"\n      },\n      \"deleteWorkspacePrompt\": {\n        \"title\": \"刪除工作區\",\n        \"content\": \"您確定要刪除這個工作區嗎？此動作無法復原，您發佈的所有頁面都將會取消發佈。\"\n      },\n      \"leaveWorkspacePrompt\": {\n        \"title\": \"離開工作區\",\n        \"content\": \"您確定要離開這個工作區嗎？ 您將失去存取其中所有頁面和資料的權限。\",\n        \"success\": \"您已成功離開工作區。\",\n        \"fail\": \"離開工作區失敗。\"\n      },\n      \"manageWorkspace\": {\n        \"title\": \"管理工作區\",\n        \"leaveWorkspace\": \"離開工作區\",\n        \"deleteWorkspace\": \"刪除工作區\"\n      }\n    },\n    \"manageDataPage\": {\n      \"menuLabel\": \"管理資料\",\n      \"title\": \"管理資料\",\n      \"description\": \"管理資料本機儲存或將現有資料匯入到 @:appName。\",\n      \"dataStorage\": {\n        \"title\": \"檔案儲存位置\",\n        \"tooltip\": \"您的檔案儲存位置\",\n        \"actions\": {\n          \"change\": \"變更路徑\",\n          \"open\": \"開啟資料夾\",\n          \"openTooltip\": \"開啟目前資料夾位置\",\n          \"copy\": \"複製路徑\",\n          \"copiedHint\": \"已複製路徑!\",\n          \"resetTooltip\": \"重設至預設位置\"\n        },\n        \"resetDialog\": {\n          \"title\": \"您確定嗎?\",\n          \"description\": \"將路徑重設為預設資料位置不會刪除您的資料。如果您想重新匯入目前資料，應該先複製您目前位置的路徑。\"\n        }\n      },\n      \"importData\": {\n        \"title\": \"匯入資料\",\n        \"tooltip\": \"從 @:appName 備份/資料夾匯入資料\",\n        \"description\": \"從外部 @:appName 資料夾複製資料\",\n        \"action\": \"瀏覽檔案\"\n      },\n      \"encryption\": {\n        \"title\": \"加密\",\n        \"tooltip\": \"管理您的資料儲存和加密方式\",\n        \"descriptionNoEncryption\": \"啟用加密將會加密所有資料。這無法逆轉。\",\n        \"descriptionEncrypted\": \"您的資料已加密。\",\n        \"action\": \"加密資料\",\n        \"dialog\": {\n          \"title\": \"是否為所有資料加密?\",\n          \"description\": \"加密您的所有資料將能確保您的資料安全。此動作無法復原。您確定要繼續嗎?\"\n        }\n      },\n      \"cache\": {\n        \"title\": \"清除快取\",\n        \"description\": \"協助解決圖像無法載入、空間中缺少頁面和字型無法載入等問題。這不會影響您的資料。\",\n        \"dialog\": {\n          \"title\": \"清除快取\",\n          \"description\": \"協助解決圖像無法載入、空間中缺少頁面和字型無法載入等問題。這不會影響您的資料。\",\n          \"successHint\": \"已清除快取!\"\n        }\n      },\n      \"data\": {\n        \"fixYourData\": \"修復您的資料\",\n        \"fixButton\": \"修復\",\n        \"fixYourDataDescription\": \"如果您在使用資料時遇到問題，您可以在這裡嘗試修復它。\"\n      }\n    },\n    \"shortcutsPage\": {\n      \"menuLabel\": \"快捷鍵\",\n      \"title\": \"快捷鍵\",\n      \"editBindingHint\": \"輸入新的綁定\",\n      \"searchHint\": \"搜尋\",\n      \"actions\": {\n        \"resetDefault\": \"重設預設值\"\n      },\n      \"errorPage\": {\n        \"message\": \"載入快捷鍵失敗: {}\",\n        \"howToFix\": \"請再試一次，如果問題持續發生，請透過 GitHub 聯繫我們。\"\n      },\n      \"resetDialog\": {\n        \"title\": \"重設快捷鍵\",\n        \"description\": \"這將會將您的所有快捷鍵重設為預設值，之後無法復原此動作，您確定要繼續嗎?\",\n        \"buttonLabel\": \"重設\"\n      },\n      \"conflictDialog\": {\n        \"title\": \"{} 目前正在使用中\",\n        \"descriptionPrefix\": \"這個快捷鍵目前使用中由\",\n        \"descriptionSuffix\": \". 如果您取代此快捷鍵，它將從 {} 中移除。\",\n        \"confirmLabel\": \"繼續\"\n      },\n      \"editTooltip\": \"按一下以開始編輯快捷鍵。\",\n      \"keybindings\": {\n        \"toggleToDoList\": \"切換至待辦事項清單\",\n        \"insertNewParagraphInCodeblock\": \"插入新段落\",\n        \"pasteInCodeblock\": \"貼上程式碼區塊\",\n        \"selectAllCodeblock\": \"全選\",\n        \"indentLineCodeblock\": \"在行首插入兩個空格\",\n        \"outdentLineCodeblock\": \"刪除行首的兩個空格\",\n        \"twoSpacesCursorCodeblock\": \"在游標處插入兩個空格\",\n        \"copy\": \"複製選取項目\",\n        \"paste\": \"貼上內容\",\n        \"cut\": \"剪下選取項目\",\n        \"alignLeft\": \"將文字左對齊\",\n        \"alignCenter\": \"將文字置中對齊\",\n        \"alignRight\": \"將文字右對齊\",\n        \"insertInlineMathEquation\": \"插入行內數學方程式\",\n        \"undo\": \"復原\",\n        \"redo\": \"重做\",\n        \"convertToParagraph\": \"將區塊轉換為段落\",\n        \"backspace\": \"刪除\",\n        \"deleteLeftWord\": \"刪除左邊的單字\",\n        \"deleteLeftSentence\": \"刪除左邊的句子\",\n        \"delete\": \"刪除右邊的字元\",\n        \"deleteMacOS\": \"刪除左邊的字元\",\n        \"deleteRightWord\": \"刪除右邊的單字\",\n        \"moveCursorLeft\": \"將游標向左移動\",\n        \"moveCursorBeginning\": \"將游標移至開頭\",\n        \"moveCursorLeftWord\": \"將游標向左移動一個單字\",\n        \"moveCursorLeftSelect\": \"選取並將游標向左移動\",\n        \"moveCursorBeginSelect\": \"選取並將游標移至開頭\",\n        \"moveCursorLeftWordSelect\": \"選取並將游標向左移動一個單字\",\n        \"moveCursorRight\": \"將游標向右移動\",\n        \"moveCursorEnd\": \"將游標移至結尾\",\n        \"moveCursorRightWord\": \"將游標向右移動一個單字\",\n        \"moveCursorRightSelect\": \"選取並將游標向右移動一個位置\",\n        \"moveCursorEndSelect\": \"選取並將游標移至結尾\",\n        \"moveCursorRightWordSelect\": \"選取並將游標向右移動一個單字\",\n        \"moveCursorUp\": \"向上移動游標\",\n        \"moveCursorTopSelect\": \"選取並將游標移至頂端\",\n        \"moveCursorTop\": \"將游標移至頂端\",\n        \"moveCursorUpSelect\": \"選取並將游標向上移動\",\n        \"moveCursorBottomSelect\": \"選取並將游標移到底部\",\n        \"moveCursorBottom\": \"將游標移到底部\",\n        \"moveCursorDown\": \"將游標向下移動\",\n        \"moveCursorDownSelect\": \"選取並將游標向下移動\",\n        \"home\": \"捲動至頂部\",\n        \"end\": \"捲動至底部\",\n        \"toggleBold\": \"切換粗體\",\n        \"toggleItalic\": \"切換斜體\",\n        \"toggleUnderline\": \"切換底線\",\n        \"toggleStrikethrough\": \"切換刪除線\",\n        \"toggleCode\": \"切換行內程式碼\",\n        \"toggleHighlight\": \"切換標示重點\",\n        \"showLinkMenu\": \"顯示連結選單\",\n        \"openInlineLink\": \"開啟行內連結\",\n        \"openLinks\": \"開啟所有已選取的連結\",\n        \"indent\": \"縮排\",\n        \"outdent\": \"取消縮排\",\n        \"exit\": \"結束編輯模式\",\n        \"pageUp\": \"向上滾動一頁\",\n        \"pageDown\": \"向下滾動一頁\",\n        \"selectAll\": \"全選\",\n        \"pasteWithoutFormatting\": \"貼上內容，不含格式\",\n        \"showEmojiPicker\": \"顯示表情符號選擇器\",\n        \"enterInTableCell\": \"在表格中新增換行符號\",\n        \"leftInTableCell\": \"在表格中向左移動一個儲存格\",\n        \"rightInTableCell\": \"在表格中向右移動一個儲存格\",\n        \"upInTableCell\": \"在表格中向上移動一個儲存格\",\n        \"downInTableCell\": \"在表格中向下移動一個儲存格\",\n        \"tabInTableCell\": \"前往表格中的下一個可用儲存格\",\n        \"shiftTabInTableCell\": \"前往表格中先前可用的儲存格\",\n        \"backSpaceInTableCell\": \"在儲存格開頭停止\"\n      },\n      \"commands\": {\n        \"codeBlockNewParagraph\": \"在程式碼區塊旁邊插入一個新段落\",\n        \"codeBlockIndentLines\": \"在程式碼區塊開頭插入兩個空格\",\n        \"codeBlockOutdentLines\": \"刪除程式碼區塊開頭的兩個空格\",\n        \"codeBlockAddTwoSpaces\": \"在程式碼區塊中光標位置插入兩個空格\",\n        \"codeBlockSelectAll\": \"選取程式碼區塊內的所有內容\",\n        \"codeBlockPasteText\": \"將文字貼到程式碼區塊中\",\n        \"textAlignLeft\": \"將文字左對齊\",\n        \"textAlignCenter\": \"將文字置中對齊\",\n        \"textAlignRight\": \"將文字右對齊\"\n      },\n      \"couldNotLoadErrorMsg\": \"無法載入快捷鍵，請再次嘗試\",\n      \"couldNotSaveErrorMsg\": \"無法儲存快捷鍵，請再次嘗試\"\n    },\n    \"aiPage\": {\n      \"title\": \"AI 設定\",\n      \"menuLabel\": \"AI 設定\",\n      \"keys\": {\n        \"enableAISearchTitle\": \"AI 搜尋\",\n        \"aiSettingsDescription\": \"選擇用於支援 AppFlowy AI 的偏好模型。現在包含 GPT-4o、GPT-o3-mini、DeepSeek R1、Claude 3.5 Sonnet，以及在 Ollama 中可用的模型\",\n        \"loginToEnableAIFeature\": \"AI 功能僅在您使用 @:appName Cloud 登入後才能啟用。如果您沒有 @:appName 帳戶，請前往「我的帳戶」註冊。\",\n        \"llmModel\": \"語言模型\",\n        \"globalLLMModel\": \"全域語言模型\",\n        \"readOnlyField\": \"此欄位為唯讀設定\",\n        \"llmModelType\": \"語言模型類型\",\n        \"downloadLLMPrompt\": \"下載 {}\",\n        \"downloadAppFlowyOfflineAI\": \"下載 AI 離線套件將啟用裝置上的 AI 執行。您要繼續嗎?\",\n        \"downloadLLMPromptDetail\": \"下載 {} 本地模型將佔用高達 {} 的儲存空間。您要繼續嗎?\",\n        \"downloadBigFilePrompt\": \"下載完成可能需要大約 10 分鐘的時間\",\n        \"downloadAIModelButton\": \"下載\",\n        \"downloadingModel\": \"正在下載中\",\n        \"localAILoaded\": \"本地 AI 模型已成功新增且可使用\",\n        \"localAIStart\": \"本地 AI 正在啟動中。如果速度較慢，請嘗試關閉並重新開啟它\",\n        \"localAILoading\": \"本地 AI 對話模型正在載入中...\",\n        \"localAIStopped\": \"本地 AI 已停止\",\n        \"localAIRunning\": \"本地 AI 正在執行中\",\n        \"localAINotReadyRetryLater\": \"本地 AI 正在初始化，請稍後重試\",\n        \"localAIDisabled\": \"您正在使用本地 AI，但已停用。請前往設定啟用它或嘗試不同的模型\",\n        \"localAIInitializing\": \"本地 AI 正在載入中。這可能需要幾秒鐘的時間，取決於您的裝置\",\n        \"localAINotReadyTextFieldPrompt\": \"在本地 AI 載入期間，您無法編輯\",\n        \"localAIDisabledTextFieldPrompt\": \"在本地 AI 停用期間，您無法編輯\",\n        \"failToLoadLocalAI\": \"啟動本地 AI 失敗。\",\n        \"restartLocalAI\": \"重新啟動\",\n        \"disableLocalAITitle\": \"停用本地 AI\",\n        \"disableLocalAIDescription\": \"您要停用本地 AI 嗎?\",\n        \"localAIToggleTitle\": \"AppFlowy 本地 AI (LAI)\",\n        \"localAIToggleSubTitle\": \"在 AppFlowy 中執行最先進的本地  AI 模型，以獲得終極隱私和安全性\",\n        \"offlineAIInstruction1\": \"跟隨\",\n        \"offlineAIInstruction2\": \"指示\",\n        \"offlineAIInstruction3\": \"以啟用離線 AI。\",\n        \"offlineAIDownload1\": \"如果您尚未下載 AppFlowy AI，請...\",\n        \"offlineAIDownload2\": \"下載\",\n        \"offlineAIDownload3\": \"先下載它。\",\n        \"activeOfflineAI\": \"啟用\",\n        \"downloadOfflineAI\": \"下載\",\n        \"openModelDirectory\": \"開啟資料夾\",\n        \"laiNotReady\": \"本地 AI 應用程式未正確安裝。\",\n        \"ollamaNotReady\": \"Ollama 伺服器尚未就緒。\",\n        \"pleaseFollowThese\": \"請依照這些...\",\n        \"instructions\": \"指示\",\n        \"installOllamaLai\": \"來設定 Ollama 和 AppFlowy 本地 AI。\",\n        \"modelsMissing\": \"找不到所需的模型:\",\n        \"downloadModel\": \"以下載它們。\"\n      }\n    },\n    \"planPage\": {\n      \"menuLabel\": \"方案\",\n      \"title\": \"價格方案\",\n      \"planUsage\": {\n        \"title\": \"方案使用摘要\",\n        \"storageLabel\": \"儲存空間\",\n        \"storageUsage\": \"{} / {} GB\",\n        \"unlimitedStorageLabel\": \"無限儲存空間\",\n        \"collaboratorsLabel\": \"成員\",\n        \"collaboratorsUsage\": \"{} / {}\",\n        \"aiResponseLabel\": \"AI 回應\",\n        \"aiResponseUsage\": \"{} / {}\",\n        \"unlimitedAILabel\": \"無限回應\",\n        \"proBadge\": \"專業版\",\n        \"aiMaxBadge\": \"AI Max\",\n        \"aiOnDeviceBadge\": \"Mac 上的 AI 本機處理\",\n        \"memberProToggle\": \"更多成員、無限 AI 和來賓存取權\",\n        \"aiMaxToggle\": \"無限的 AI 和存取進階模型\",\n        \"aiOnDeviceToggle\": \"本地 AI 確保極致隱私。\",\n        \"aiCredit\": {\n          \"title\": \"新增 @:appName AI 額度\",\n          \"price\": \"{}\",\n          \"priceDescription\": \"用於 1,000 個額度\",\n          \"purchase\": \"購買 AI\",\n          \"info\": \"每個工作區新增 1,000 個 AI 積分，並無縫整合可自訂的 AI 功能到您的工作流程中，以更聰明、更快速地獲得結果，最高可達:\",\n          \"infoItemOne\": \"每個資料庫 10,000 次回應\",\n          \"infoItemTwo\": \"每個工作區 1,000 次回應\"\n        },\n        \"currentPlan\": {\n          \"bannerLabel\": \"目前方案\",\n          \"freeTitle\": \"免費版\",\n          \"proTitle\": \"專業版\",\n          \"teamTitle\": \"團隊\",\n          \"freeInfo\": \"非常適合最多 2 名個人使用，以整理所有內容\",\n          \"proInfo\": \"非常適合小型和中型團隊，最多 10 名成員。\",\n          \"teamInfo\": \"非常適合所有高效且井然有序的團隊。\",\n          \"upgrade\": \"變更計劃\",\n          \"canceledInfo\": \"您的方案已取消，您將在 {} 時降級為免費方案。\"\n        },\n        \"addons\": {\n          \"title\": \"附加組件\",\n          \"addLabel\": \"新增\",\n          \"activeLabel\": \"已新增\",\n          \"aiMax\": {\n            \"title\": \"AI Max\",\n            \"description\": \"每月享有 50 張人工智慧圖片，並由進階人工智慧模型提供無限的 AI 回應。\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"每位使用者每月，按年計費\"\n          },\n          \"aiOnDevice\": {\n            \"title\": \"Mac 上的 AI 本機處理\",\n            \"description\": \"在您的機器上執行 Mistral 7B、LLAMA 3 和更多本機模型\",\n            \"price\": \"{}\",\n            \"priceInfo\": \"每位使用者每月，按年計費\",\n            \"recommend\": \"建議使用 M1 或更新版本\"\n          }\n        },\n        \"deal\": {\n          \"bannerLabel\": \"新年優惠!\",\n          \"title\": \"擴展您的團隊!\",\n          \"info\": \"升級即可享受專業版和團隊方案 10% 的折扣！透過強大的新功能，包括 @:appName AI，提升工作區生產力。\",\n          \"viewPlans\": \"查看方案\"\n        }\n      }\n    },\n    \"billingPage\": {\n      \"menuLabel\": \"帳單\",\n      \"title\": \"帳單\",\n      \"plan\": {\n        \"title\": \"方案\",\n        \"freeLabel\": \"免費版\",\n        \"proLabel\": \"專業版\",\n        \"planButtonLabel\": \"變更計劃\",\n        \"billingPeriod\": \"計費週期\",\n        \"periodButtonLabel\": \"編輯期間\"\n      },\n      \"paymentDetails\": {\n        \"title\": \"付款資訊\",\n        \"methodLabel\": \"付款方式\",\n        \"methodButtonLabel\": \"編輯方式\"\n      },\n      \"addons\": {\n        \"title\": \"附加組件\",\n        \"addLabel\": \"新增\",\n        \"removeLabel\": \"移除\",\n        \"renewLabel\": \"續訂\",\n        \"aiMax\": {\n          \"label\": \"AI Max\",\n          \"description\": \"解鎖無限 AI 和進階模型\",\n          \"activeDescription\": \"下期帳單到期日為 {}\",\n          \"canceledDescription\": \"AI 最大將於 {} 啟用\"\n        },\n        \"aiOnDevice\": {\n          \"label\": \"Mac 上的 AI 本機處理\",\n          \"description\": \"解鎖裝置上的無限 AI\",\n          \"activeDescription\": \"下期帳單到期日為 {}\",\n          \"canceledDescription\": \"Mac 的 AI 在裝置上可用至 {}\"\n        },\n        \"removeDialog\": {\n          \"title\": \"移除 {}\",\n          \"description\": \"您確定要移除 {plan} 嗎？ 您將立即失去 {plan} 的功能和權益。\"\n        }\n      },\n      \"currentPeriodBadge\": \"目前\",\n      \"changePeriod\": \"變更週期\",\n      \"planPeriod\": \"{} 週期\",\n      \"monthlyInterval\": \"每月\",\n      \"monthlyPriceInfo\": \"每座位每月計費\",\n      \"annualInterval\": \"每年\",\n      \"annualPriceInfo\": \"每座位按年計費\"\n    },\n    \"comparePlanDialog\": {\n      \"title\": \"比較並選擇方案\",\n      \"planFeatures\": \"方案\\n功能\",\n      \"current\": \"目前\",\n      \"actions\": {\n        \"upgrade\": \"升級\",\n        \"downgrade\": \"降級\",\n        \"current\": \"目前\"\n      },\n      \"freePlan\": {\n        \"title\": \"免費版\",\n        \"description\": \"適用於最多 2 名成員，以整理所有事務\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"永遠免費\"\n      },\n      \"proPlan\": {\n        \"title\": \"專業版\",\n        \"description\": \"適合小型團隊，用來管理專案與團隊知識\",\n        \"price\": \"{}\",\n        \"priceInfo\": \"每位用戶每月\\n按年計費\\n\\n{} 按月計費\"\n      },\n      \"planLabels\": {\n        \"itemOne\": \"工作區\",\n        \"itemTwo\": \"成員\",\n        \"itemThree\": \"儲存空間\",\n        \"itemFour\": \"即時協作\",\n        \"itemFive\": \"客戶編輯器\",\n        \"itemSix\": \"AI 回應\",\n        \"itemSeven\": \"AI 圖像\",\n        \"itemFileUpload\": \"檔案上傳\",\n        \"customNamespace\": \"自訂命名空間\",\n        \"tooltipFive\": \"與非會員共同編輯特定頁面\",\n        \"tooltipSix\": \"終身代表回應次數永遠不會重設\",\n        \"intelligentSearch\": \"智慧搜尋\",\n        \"tooltipSeven\": \"允許您自訂工作區部分的網址\",\n        \"customNamespaceTooltip\": \"自訂發佈網站網址\"\n      },\n      \"freeLabels\": {\n        \"itemOne\": \"每個工作區收取費用\",\n        \"itemTwo\": \"最多 2 個\",\n        \"itemThree\": \"5 GB\",\n        \"itemFour\": \"是的\",\n        \"itemFive\": \"是的\",\n        \"itemSix\": \"終身 10 次\",\n        \"itemSeven\": \"終身 2 次\",\n        \"itemFileUpload\": \"最多 7 MB\",\n        \"intelligentSearch\": \"智慧搜尋\"\n      },\n      \"proLabels\": {\n        \"itemOne\": \"每個工作區收取費用\",\n        \"itemTwo\": \"最多 10 個\",\n        \"itemThree\": \"無限\",\n        \"itemFour\": \"是的\",\n        \"itemFive\": \"最多 100 個\",\n        \"itemSix\": \"無限\",\n        \"itemSeven\": \"每月 50 張圖片\",\n        \"itemFileUpload\": \"無限\",\n        \"intelligentSearch\": \"智慧搜尋\"\n      },\n      \"paymentSuccess\": {\n        \"title\": \"您現在在{}方案中!\",\n        \"description\": \"您的付款已成功處理，您的方案已升級至 @:appName {}。 您可以在方案頁面查看您的方案詳細資訊。\"\n      },\n      \"downgradeDialog\": {\n        \"title\": \"您確定要降級您的方案嗎?\",\n        \"description\": \"降級您的方案將會讓您回歸至免費方案。成員可能會失去存取這個工作區的權限，您可能需要釋放空間以符合免費方案的儲存空間限制。\",\n        \"downgradeLabel\": \"降級計劃\"\n      }\n    },\n    \"cancelSurveyDialog\": {\n      \"title\": \"很遺憾看到你離開\",\n      \"description\": \"很遺憾看到您離開。我們非常希望聽到您的回饋，以幫助我們改善 @:appName。請花一點時間回答幾個問題。\",\n      \"commonOther\": \"其他\",\n      \"otherHint\": \"在這裡寫下你的答案\",\n      \"questionOne\": {\n        \"question\": \"是什麼促使您取消了您的 @:appName Pro 訂閱?\",\n        \"answerOne\": \"成本太高\",\n        \"answerTwo\": \"功能未達預期\",\n        \"answerThree\": \"找到更好的替代方案\",\n        \"answerFour\": \"使用量不足以證明費用合理\",\n        \"answerFive\": \"服務問題或技術困難\"\n      },\n      \"questionTwo\": {\n        \"question\": \"您未來考慮重新訂閱 @:appName Pro 的可能性有多大?\",\n        \"answerOne\": \"很有可能\",\n        \"answerTwo\": \"有可能\",\n        \"answerThree\": \"不确定\",\n        \"answerFour\": \"不太可能\",\n        \"answerFive\": \"不太可能\"\n      },\n      \"questionThree\": {\n        \"question\": \"您在訂閱期間最重視哪個Pro功能?\",\n        \"answerOne\": \"多用戶協作\",\n        \"answerTwo\": \"更長的歷史版本記錄時間\",\n        \"answerThree\": \"無限的 AI 回應\",\n        \"answerFour\": \"存取本地 AI 模型\"\n      },\n      \"questionFour\": {\n        \"question\": \"您如何描述您對 @:appName 的整體體驗?\",\n        \"answerOne\": \"很棒\",\n        \"answerTwo\": \"好的\",\n        \"answerThree\": \"平均\",\n        \"answerFour\": \"低於平均水平\",\n        \"answerFive\": \"不滿意\"\n      }\n    },\n    \"common\": {\n      \"uploadingFile\": \"檔案正在上傳中，請勿關閉應用程式\",\n      \"uploadNotionSuccess\": \"您的 Notion 壓縮檔已成功上傳。匯入完成後，您將收到確認電子郵件\",\n      \"reset\": \"重設\"\n    },\n    \"menu\": {\n      \"appearance\": \"外觀\",\n      \"language\": \"語言\",\n      \"user\": \"使用者\",\n      \"files\": \"檔案\",\n      \"notifications\": \"通知\",\n      \"open\": \"開啟設定\",\n      \"logout\": \"登出\",\n      \"logoutPrompt\": \"您確定要登出嗎?\",\n      \"selfEncryptionLogoutPrompt\": \"您確定要登出嗎? 請確保您已複製加密金鑰\",\n      \"syncSetting\": \"同步設定\",\n      \"cloudSettings\": \"雲端設定\",\n      \"enableSync\": \"啟用同步\",\n      \"enableSyncLog\": \"啟用同步記錄\",\n      \"enableSyncLogWarning\": \"感謝您協助診斷同步問題。這將會將您的文件編輯記錄到本機檔案中。啟用後請關閉並重新開啟應用程式。\",\n      \"enableEncrypt\": \"加密資料\",\n      \"cloudURL\": \"基礎網址\",\n      \"webURL\": \"網頁網址\",\n      \"invalidCloudURLScheme\": \"網址格式無效\",\n      \"cloudServerType\": \"雲端伺服器種類\",\n      \"cloudServerTypeTip\": \"請注意，切換雲端伺服器後可能會登出您目前的帳號\",\n      \"cloudLocal\": \"本地\",\n      \"cloudAppFlowy\": \"@:appName Cloud\",\n      \"cloudAppFlowySelfHost\": \"@:appName 雲端 自架設\",\n      \"appFlowyCloudUrlCanNotBeEmpty\": \"雲端網址不能為空\",\n      \"clickToCopy\": \"點選以複製\",\n      \"selfHostStart\": \"若您尚未設定伺服器，請參閱\",\n      \"selfHostContent\": \"文件\",\n      \"selfHostEnd\": \"有關如何自架個人伺服器的指南\",\n      \"pleaseInputValidURL\": \"請輸入有效的網址\",\n      \"changeUrl\": \"將自架設網址變更為 {}\",\n      \"cloudURLHint\": \"請輸入您伺服器的基礎網址\",\n      \"webURLHint\": \"輸入您的網路伺服器基本網址\",\n      \"cloudWSURL\": \"Websocket 網址\",\n      \"cloudWSURLHint\": \"請輸入您伺服器的 Websocket 位址\",\n      \"restartApp\": \"重新啟動\",\n      \"restartAppTip\": \"重新啟動應用程式以使變更生效。請注意，這可能會登出您目前的帳號\",\n      \"changeServerTip\": \"更換伺服器後，必須點選重新啟動按鈕以使變更生效\",\n      \"enableEncryptPrompt\": \"啟用加密以保護您的資料。請妥善保管這個金鑰；一旦啟用，將無法關閉。如果遺失，您的資料將無法恢復。點選這裡複製\",\n      \"inputEncryptPrompt\": \"請為 {username} 輸入您的加密金鑰\",\n      \"clickToCopySecret\": \"點選以複製金鑰\",\n      \"configServerSetting\": \"設定您的伺服器選項\",\n      \"configServerGuide\": \"在選擇「快速開始」之後，請前往「設定」並選擇「雲端設定」，以進行自架伺服器的設定。\",\n      \"inputTextFieldHint\": \"您的金鑰\",\n      \"historicalUserList\": \"使用者登入歷史\",\n      \"historicalUserListTooltip\": \"此列表顯示您的匿名帳號。您可以點選帳號以檢視其詳細資訊。透過點選「開始使用」按鈕來建立匿名帳號\",\n      \"openHistoricalUser\": \"點選以開啟匿名帳號\",\n      \"customPathPrompt\": \"將 @:appName 資料資料夾儲存在如 Google 雲端硬碟等雲端同步資料夾中可能會帶來風險。如果該資料庫在多處同時被存取或修改，可能會導致同步衝突和潛在的資料損壞\",\n      \"importAppFlowyData\": \"從外部 @:appName 資料夾匯入資料\",\n      \"importingAppFlowyDataTip\": \"資料正在匯入中。請勿關閉應用程式\",\n      \"importAppFlowyDataDescription\": \"從外部 @:appName 資料夾複製資料並匯入到目前的 @:appName 資料夾\",\n      \"importSuccess\": \"成功匯入 @:appName 資料夾\",\n      \"importFailed\": \"匯入 @:appName 資料夾失敗\",\n      \"importGuide\": \"欲瞭解更多詳細資訊，請查閱參考文件\"\n    },\n    \"notifications\": {\n      \"enableNotifications\": {\n        \"label\": \"啟用通知\",\n        \"hint\": \"關閉以停止顯示本機通知。\"\n      },\n      \"showNotificationsIcon\": {\n        \"label\": \"顯示通知圖示\",\n        \"hint\": \"關閉以隱藏側邊欄中的通知圖示。\"\n      },\n      \"archiveNotifications\": {\n        \"allSuccess\": \"已成功存檔所有通知\",\n        \"success\": \"已成功存檔通知\"\n      },\n      \"markAsReadNotifications\": {\n        \"allSuccess\": \"已成功標記全部為已讀\",\n        \"success\": \"已成功標記為已讀\"\n      },\n      \"action\": {\n        \"markAsRead\": \"標記為已讀\",\n        \"multipleChoice\": \"選擇更多\",\n        \"archive\": \"歸檔\"\n      },\n      \"settings\": {\n        \"settings\": \"設定\",\n        \"markAllAsRead\": \"全部標記為已讀\",\n        \"archiveAll\": \"全部存檔\"\n      },\n      \"emptyInbox\": {\n        \"title\": \"收件匣清零!\",\n        \"description\": \"在此處設定提醒以接收通知。\"\n      },\n      \"emptyUnread\": {\n        \"title\": \"沒有未讀取的通知\",\n        \"description\": \"您已完成所有更新!\"\n      },\n      \"emptyArchived\": {\n        \"title\": \"沒有歸檔\",\n        \"description\": \"在此設定提醒，以接收通知。\"\n      },\n      \"tabs\": {\n        \"inbox\": \"收件匣\",\n        \"unread\": \"未讀\",\n        \"archived\": \"已歸檔\"\n      },\n      \"refreshSuccess\": \"已成功重新整理通知\",\n      \"titles\": {\n        \"notifications\": \"通知\",\n        \"reminder\": \"提醒事項\"\n      }\n    },\n    \"appearance\": {\n      \"resetSetting\": \"重設\",\n      \"fontFamily\": {\n        \"label\": \"字型\",\n        \"search\": \"搜尋\",\n        \"defaultFont\": \"系統預設\"\n      },\n      \"themeMode\": {\n        \"label\": \"主題模式\",\n        \"light\": \"淺色模式\",\n        \"dark\": \"深色模式\",\n        \"system\": \"調整至系統設定。\"\n      },\n      \"fontScaleFactor\": \"字型縮放比例\",\n      \"displaySize\": \"顯示大小\",\n      \"documentSettings\": {\n        \"cursorColor\": \"文件游標顏色\",\n        \"selectionColor\": \"文件選取顏色\",\n        \"width\": \"文件寬度\",\n        \"changeWidth\": \"變更\",\n        \"pickColor\": \"選擇顏色\",\n        \"colorShade\": \"色彩色調\",\n        \"opacity\": \"不透明度\",\n        \"hexEmptyError\": \"十六進位顏色不能為空\",\n        \"hexLengthError\": \"十六進位值必須為 6 位數\",\n        \"hexInvalidError\": \"無效的十六進位值\",\n        \"opacityEmptyError\": \"不透明度不能為空\",\n        \"opacityRangeError\": \"不透明度必須在 1 到 100 之間\",\n        \"app\": \"App\",\n        \"flowy\": \"Flowy\",\n        \"apply\": \"套用\"\n      },\n      \"layoutDirection\": {\n        \"label\": \"版面配置方向\",\n        \"hint\": \"控制螢幕上內容的流向，從左到右或從右到左。\",\n        \"ltr\": \"從左到右\",\n        \"rtl\": \"從右到左\"\n      },\n      \"textDirection\": {\n        \"label\": \"預設文字方向\",\n        \"hint\": \"預設指定文字應從左邊或右邊開始。\",\n        \"ltr\": \"從左到右\",\n        \"rtl\": \"從右到左\",\n        \"auto\": \"自動\",\n        \"fallback\": \"與版面配置方向相同\"\n      },\n      \"themeUpload\": {\n        \"button\": \"上傳\",\n        \"uploadTheme\": \"上傳主題\",\n        \"description\": \"使用下方的按鈕上傳您自己的 @:appName 主題。\",\n        \"loading\": \"我們正在驗證並上傳您的主題，請稍候...\",\n        \"uploadSuccess\": \"您的主題已成功上傳\",\n        \"deletionFailure\": \"刪除主題失敗。請嘗試手動刪除。\",\n        \"filePickerDialogTitle\": \"選擇 .flowy_plugin 檔案\",\n        \"urlUploadFailure\": \"無法開啟網址: {}\"\n      },\n      \"theme\": \"主題\",\n      \"builtInsLabel\": \"內建主題\",\n      \"pluginsLabel\": \"外掛\",\n      \"dateFormat\": {\n        \"label\": \"日期格式\",\n        \"local\": \"本地\",\n        \"us\": \"美國\",\n        \"iso\": \"ISO\",\n        \"friendly\": \"友善\",\n        \"dmy\": \"日/月/年\"\n      },\n      \"timeFormat\": {\n        \"label\": \"時間格式\",\n        \"twelveHour\": \"12 小時制\",\n        \"twentyFourHour\": \"24 小時制\"\n      },\n      \"showNamingDialogWhenCreatingPage\": \"建立頁面時顯示命名對話框\",\n      \"enableRTLToolbarItems\": \"啟用從右到左工具列項目\",\n      \"members\": {\n        \"title\": \"成員設定\",\n        \"inviteMembers\": \"邀請成員\",\n        \"inviteHint\": \"透過電子郵件邀請\",\n        \"sendInvite\": \"邀請\",\n        \"copyInviteLink\": \"複製邀請連結\",\n        \"label\": \"成員\",\n        \"user\": \"使用者\",\n        \"role\": \"角色\",\n        \"removeFromWorkspace\": \"從工作區中刪除\",\n        \"removeFromWorkspaceSuccess\": \"成功從工作區移除。\",\n        \"removeFromWorkspaceFailed\": \"從工作區移除失敗\",\n        \"owner\": \"擁有者\",\n        \"guest\": \"訪客\",\n        \"member\": \"成員\",\n        \"memberHintText\": \"成員可以閱讀、評論和編輯頁面。邀請其他成員和訪客\",\n        \"guestHintText\": \"訪客可以閱讀、做出回應、發表評論，並且可以在獲得許可的情況下編輯頁面\",\n        \"emailInvalidError\": \"電子郵件無效，請檢查並再次嘗試\",\n        \"emailSent\": \"已發送電子郵件，請查看收件匣\",\n        \"members\": \"成員\",\n        \"membersCount\": {\n          \"zero\": \"{} 個成員\",\n          \"one\": \"{} 個成員\",\n          \"other\": \"{} 個成員\"\n        },\n        \"inviteFailedDialogTitle\": \"升級至專業版方案\",\n        \"inviteFailedMemberLimit\": \"此工作區已達到免費限制。請升級到專業版以解鎖更多成員。\",\n        \"inviteFailedMemberLimitMobile\": \"您的工作區已達到成員上限。\",\n        \"memberLimitExceeded\": \"已達成員上限，若要邀請更多成員，請...\",\n        \"memberLimitExceededUpgrade\": \"升級\",\n        \"memberLimitExceededPro\": \"已達成員上限，若需要更多成員，請聯繫...\",\n        \"memberLimitExceededProContact\": \"support@appflowy.io\",\n        \"failedToAddMember\": \"新增成員失敗\",\n        \"addMemberSuccess\": \"成員新增成功\",\n        \"removeMember\": \"刪除成員\",\n        \"areYouSureToRemoveMember\": \"確定要刪除該成員?\",\n        \"inviteMemberSuccess\": \"邀請已成功發送\",\n        \"failedToInviteMember\": \"邀請成員失敗\",\n        \"workspaceMembersError\": \"哎呀，出錯了\",\n        \"workspaceMembersErrorDescription\": \"我們目前無法載入成員清單。請稍後再試\",\n        \"inviteLinkToAddMember\": \"邀請連結以新增成員\",\n        \"clickToCopyLink\": \"點擊複製連結\",\n        \"or\": \"或\",\n        \"generateANewLink\": \"產生新連結\",\n        \"inviteMemberByEmail\": \"透過電子郵件邀請會員\",\n        \"inviteMemberHintText\": \"透過電子郵件邀請\",\n        \"resetInviteLink\": \"重設邀請連結?\",\n        \"resetInviteLinkDescription\": \"重設將會停用目前所有空間成員的連結，並產生新的連結。舊連結將不再有效。\",\n        \"adminPanel\": \"管理面板\",\n        \"reset\": \"重設\",\n        \"resetInviteLinkSuccess\": \"邀請連結已成功重置\",\n        \"resetInviteLinkFailed\": \"無法重設邀請連結\",\n        \"resetInviteLinkFailedDescription\": \"請稍後再試一次\",\n        \"memberPageDescription1\": \"存取\",\n        \"memberPageDescription2\": \"用於訪客和進階使用者管理。\",\n        \"noInviteLink\": \"您尚未產生邀請連結。\",\n        \"copyLink\": \"複製連結\",\n        \"generatedLinkSuccessfully\": \"連結已成功產生\",\n        \"generatedLinkFailed\": \"連結產生失敗\",\n        \"resetLinkSuccessfully\": \"連結已成功重置\",\n        \"resetLinkFailed\": \"連結重置失敗\"\n      }\n    },\n    \"files\": {\n      \"copy\": \"複製\",\n      \"defaultLocation\": \"@:appName 資料儲存位置\",\n      \"exportData\": \"匯出您的資料\",\n      \"doubleTapToCopy\": \"點選兩下以複製路徑\",\n      \"restoreLocation\": \"恢復為 @:appName 預設路徑\",\n      \"customizeLocation\": \"開啟其他資料夾\",\n      \"restartApp\": \"請重新啟動應用程式以使變更生效。\",\n      \"exportDatabase\": \"匯出資料庫\",\n      \"selectFiles\": \"選擇需要匯出的檔案\",\n      \"selectAll\": \"全選\",\n      \"deselectAll\": \"取消全選\",\n      \"createNewFolder\": \"建立新資料夾\",\n      \"createNewFolderDesc\": \"選擇您想儲存資料的位置\",\n      \"defineWhereYourDataIsStored\": \"定義您的資料儲存位置\",\n      \"open\": \"開啟\",\n      \"openFolder\": \"開啟一個已經存在的資料夾\",\n      \"openFolderDesc\": \"讀取並寫入到現有的 @:appName 資料夾\",\n      \"folderHintText\": \"資料夾名稱\",\n      \"location\": \"建立新資料夾\",\n      \"locationDesc\": \"為您的 @:appName 資料夾選擇一個名稱\",\n      \"browser\": \"瀏覽\",\n      \"create\": \"建立\",\n      \"set\": \"設定\",\n      \"folderPath\": \"儲存資料夾的路徑\",\n      \"locationCannotBeEmpty\": \"路徑不能為空\",\n      \"pathCopiedSnackbar\": \"儲存檔案的路徑已被複製到剪貼簿！\",\n      \"changeLocationTooltips\": \"更改資料目錄\",\n      \"change\": \"變更\",\n      \"openLocationTooltips\": \"開啟另一個資料目錄\",\n      \"openCurrentDataFolder\": \"開啟目前資料目錄\",\n      \"recoverLocationTooltips\": \"重設為 @:appName 的預設資料目錄\",\n      \"exportFileSuccess\": \"匯出檔案成功!\",\n      \"exportFileFail\": \"匯出檔案失敗!\",\n      \"export\": \"匯出\",\n      \"clearCache\": \"清除快取\",\n      \"clearCacheDesc\": \"如果您遇到圖片無法載入或字型顯示不正確的問題，請嘗試清除快取。 此動作不會移除您的使用者資料。\",\n      \"areYouSureToClearCache\": \"確定清除快取?\",\n      \"clearCacheSuccess\": \"快取清除成功!\"\n    },\n    \"user\": {\n      \"name\": \"名稱\",\n      \"email\": \"電子郵件\",\n      \"tooltipSelectIcon\": \"選擇圖示\",\n      \"selectAnIcon\": \"選擇圖示\",\n      \"pleaseInputYourOpenAIKey\": \"請輸入您的 AI 金鑰\",\n      \"clickToLogout\": \"點選以登出目前使用者\"\n    },\n    \"mobile\": {\n      \"personalInfo\": \"個人資料\",\n      \"username\": \"使用者名稱\",\n      \"usernameEmptyError\": \"使用者名稱為必填\",\n      \"about\": \"關於\",\n      \"pushNotifications\": \"推播通知\",\n      \"support\": \"支援\",\n      \"joinDiscord\": \"加入我們的 Discord\",\n      \"privacyPolicy\": \"隱私權政策\",\n      \"userAgreement\": \"使用者協議\",\n      \"termsAndConditions\": \"條款與條件\",\n      \"userprofileError\": \"載入使用者檔案失敗\",\n      \"userprofileErrorDescription\": \"請嘗試登出並重新登入以確認問題是否仍然存在。\",\n      \"selectLayout\": \"選擇版面配置\",\n      \"selectStartingDay\": \"選擇一週的起始日\",\n      \"version\": \"版本\"\n    }\n  },\n  \"grid\": {\n    \"deleteView\": \"您確定要刪除此檢視嗎?\",\n    \"createView\": \"建立\",\n    \"title\": {\n      \"placeholder\": \"無標題\"\n    },\n    \"settings\": {\n      \"filter\": \"篩選\",\n      \"sort\": \"排序\",\n      \"sortBy\": \"排序方式\",\n      \"properties\": \"屬性\",\n      \"reorderPropertiesTooltip\": \"拖曳以重新排序屬性\",\n      \"group\": \"群組\",\n      \"addFilter\": \"新增篩選器\",\n      \"deleteFilter\": \"刪除篩選器\",\n      \"filterBy\": \"依據篩選\",\n      \"typeAValue\": \"輸入一個值……\",\n      \"layout\": \"版面配置\",\n      \"compactMode\": \"緊湊模式\",\n      \"databaseLayout\": \"版面佈局\",\n      \"viewList\": {\n        \"zero\": \"0 次瀏覽\",\n        \"one\": \"{count} 次瀏覽\",\n        \"other\": \"{count} 次瀏覽\"\n      },\n      \"editView\": \"編輯檢視\",\n      \"boardSettings\": \"看板設定\",\n      \"calendarSettings\": \"日曆設定\",\n      \"createView\": \"建立檢視\",\n      \"duplicateView\": \"副本檢視\",\n      \"deleteView\": \"刪除檢視\",\n      \"numberOfVisibleFields\": \"顯示 {}\"\n    },\n    \"filter\": {\n      \"empty\": \"沒有任何作用中的篩選器\",\n      \"addFilter\": \"新增篩選器\",\n      \"cannotFindCreatableField\": \"找不到適合用於篩選的欄位\",\n      \"conditon\": \"條件\",\n      \"where\": \"在哪裡\"\n    },\n    \"textFilter\": {\n      \"contains\": \"包含\",\n      \"doesNotContain\": \"不包含\",\n      \"endsWith\": \"以……結尾\",\n      \"startWith\": \"以……開頭\",\n      \"is\": \"是\",\n      \"isNot\": \"不是\",\n      \"isEmpty\": \"為空\",\n      \"isNotEmpty\": \"不為空\",\n      \"choicechipPrefix\": {\n        \"isNot\": \"不是\",\n        \"startWith\": \"以……開頭\",\n        \"endWith\": \"以……結尾\",\n        \"isEmpty\": \"為空\",\n        \"isNotEmpty\": \"不為空\"\n      }\n    },\n    \"checkboxFilter\": {\n      \"isChecked\": \"已勾選\",\n      \"isUnchecked\": \"未勾選\",\n      \"choicechipPrefix\": {\n        \"is\": \"是\"\n      }\n    },\n    \"checklistFilter\": {\n      \"isComplete\": \"已完成\",\n      \"isIncomplted\": \"未完成\"\n    },\n    \"selectOptionFilter\": {\n      \"is\": \"是\",\n      \"isNot\": \"不是\",\n      \"contains\": \"包含\",\n      \"doesNotContain\": \"不包含\",\n      \"isEmpty\": \"為空\",\n      \"isNotEmpty\": \"不為空\"\n    },\n    \"dateFilter\": {\n      \"is\": \"是\",\n      \"before\": \"在...之前\",\n      \"after\": \"在...之後\",\n      \"onOrBefore\": \"在...之前或當天\",\n      \"onOrAfter\": \"在...之後或當天\",\n      \"between\": \"在...之間\",\n      \"empty\": \"為空\",\n      \"notEmpty\": \"不為空\",\n      \"startDate\": \"開始日期\",\n      \"endDate\": \"結束日期\",\n      \"choicechipPrefix\": {\n        \"before\": \"之前\",\n        \"after\": \"之後\",\n        \"between\": \"之間\",\n        \"onOrBefore\": \"在或之前\",\n        \"onOrAfter\": \"在或之後\",\n        \"isEmpty\": \"為空\",\n        \"isNotEmpty\": \"不為空\"\n      }\n    },\n    \"numberFilter\": {\n      \"equal\": \"等於\",\n      \"notEqual\": \"不等於\",\n      \"lessThan\": \"小於\",\n      \"greaterThan\": \"大於\",\n      \"lessThanOrEqualTo\": \"小於或等於\",\n      \"greaterThanOrEqualTo\": \"大於或等於\",\n      \"isEmpty\": \"為空\",\n      \"isNotEmpty\": \"不為空\"\n    },\n    \"field\": {\n      \"label\": \"屬性\",\n      \"hide\": \"隱藏屬性\",\n      \"show\": \"顯示屬性\",\n      \"insertLeft\": \"左方插入\",\n      \"insertRight\": \"右方插入\",\n      \"duplicate\": \"副本\",\n      \"delete\": \"刪除\",\n      \"wrapCellContent\": \"文字換行\",\n      \"clear\": \"清除儲存格\",\n      \"switchPrimaryFieldTooltip\": \"無法變更主要欄位的類型\",\n      \"textFieldName\": \"文字\",\n      \"checkboxFieldName\": \"核取方塊\",\n      \"dateFieldName\": \"日期\",\n      \"updatedAtFieldName\": \"最後修改時間\",\n      \"createdAtFieldName\": \"建立時間\",\n      \"numberFieldName\": \"數字\",\n      \"singleSelectFieldName\": \"單選\",\n      \"multiSelectFieldName\": \"多選\",\n      \"urlFieldName\": \"網址\",\n      \"checklistFieldName\": \"核取清單\",\n      \"relationFieldName\": \"關係\",\n      \"summaryFieldName\": \"AI 總結\",\n      \"timeFieldName\": \"時間\",\n      \"mediaFieldName\": \"檔案和媒體\",\n      \"translateFieldName\": \"AI 翻譯\",\n      \"translateTo\": \"翻譯為\",\n      \"numberFormat\": \"數字格式\",\n      \"dateFormat\": \"日期格式\",\n      \"includeTime\": \"包含時間\",\n      \"isRange\": \"結束日期\",\n      \"dateFormatFriendly\": \"月 日, 年\",\n      \"dateFormatISO\": \"年-月-日\",\n      \"dateFormatLocal\": \"月/日/年\",\n      \"dateFormatUS\": \"年/月/日\",\n      \"dateFormatDayMonthYear\": \"日/月/年\",\n      \"timeFormat\": \"時間格式\",\n      \"invalidTimeFormat\": \"格式無效\",\n      \"timeFormatTwelveHour\": \"12 小時制\",\n      \"timeFormatTwentyFourHour\": \"24 小時制\",\n      \"clearDate\": \"清除日期\",\n      \"dateTime\": \"日期時間\",\n      \"startDateTime\": \"開始日期時間\",\n      \"endDateTime\": \"結束日期時間\",\n      \"failedToLoadDate\": \"載入日期值失敗\",\n      \"selectTime\": \"選擇時間\",\n      \"selectDate\": \"選擇日期\",\n      \"visibility\": \"可見性\",\n      \"propertyType\": \"屬性類型\",\n      \"addSelectOption\": \"新增選項\",\n      \"typeANewOption\": \"輸入新選項\",\n      \"optionTitle\": \"選項\",\n      \"addOption\": \"新增選項\",\n      \"editProperty\": \"編輯屬性\",\n      \"newProperty\": \"新增屬性\",\n      \"openRowDocument\": \"作為頁面打開\",\n      \"deleteFieldPromptMessage\": \"您確定嗎? 這個屬性將被刪除\",\n      \"clearFieldPromptMessage\": \"確定操作，該列中的所有單元格都將被清空\",\n      \"newColumn\": \"新增欄位\",\n      \"format\": \"格式\",\n      \"reminderOnDateTooltip\": \"此欄位設有預定提醒\",\n      \"optionAlreadyExist\": \"選項已存在\"\n    },\n    \"rowPage\": {\n      \"newField\": \"新增欄位\",\n      \"fieldDragElementTooltip\": \"點選以開啟選單\",\n      \"showHiddenFields\": {\n        \"one\": \"顯示 {count} 個隱藏欄位\",\n        \"many\": \"顯示 {count} 個隱藏欄位\",\n        \"other\": \"顯示 {count} 個隱藏欄位\"\n      },\n      \"hideHiddenFields\": {\n        \"one\": \"隱藏 {count} 個隱藏欄位\",\n        \"many\": \"隱藏 {count} 個隱藏欄位\",\n        \"other\": \"隱藏 {count} 個隱藏欄位\"\n      },\n      \"openAsFullPage\": \"以整頁形式打開\",\n      \"viewDatabase\": \"查看原始資料庫\",\n      \"moreRowActions\": \"更多列動作\"\n    },\n    \"sort\": {\n      \"ascending\": \"升冪\",\n      \"descending\": \"降冪\",\n      \"by\": \"由\",\n      \"empty\": \"沒有任何作用中的排序\",\n      \"cannotFindCreatableField\": \"找不到適合用於排序的欄位\",\n      \"deleteAllSorts\": \"刪除所有排序\",\n      \"addSort\": \"新增排序\",\n      \"sortsActive\": \"在排序時無法 {intention}\",\n      \"removeSorting\": \"您想移除此檢視中的所有排序，並繼續嗎?\",\n      \"fieldInUse\": \"您已經按照此欄位進行排序\"\n    },\n    \"row\": {\n      \"label\": \"列\",\n      \"duplicate\": \"複製\",\n      \"delete\": \"刪除\",\n      \"titlePlaceholder\": \"無標題\",\n      \"textPlaceholder\": \"空白\",\n      \"copyProperty\": \"已將屬性複製到剪貼簿\",\n      \"count\": \"計數\",\n      \"newRow\": \"新增列\",\n      \"loadMore\": \"載入更多\",\n      \"action\": \"操作\",\n      \"add\": \"點選以在下方新增\",\n      \"drag\": \"拖曳以移動\",\n      \"deleteRowPrompt\": \"您確定要刪除這列嗎？此動作無法復原。\",\n      \"deleteCardPrompt\": \"您確定要刪除這張卡片嗎？此動作無法復原。\",\n      \"dragAndClick\": \"拖曳以移動，點選以開啟選單\",\n      \"insertRecordAbove\": \"在上方插入記錄\",\n      \"insertRecordBelow\": \"在下方插入記錄\",\n      \"noContent\": \"無內容\",\n      \"reorderRowDescription\": \"重新排列列\",\n      \"createRowAboveDescription\": \"在上方建立一個列\",\n      \"createRowBelowDescription\": \"在下方插入一個列\"\n    },\n    \"selectOption\": {\n      \"create\": \"建立\",\n      \"purpleColor\": \"紫色\",\n      \"pinkColor\": \"粉紅色\",\n      \"lightPinkColor\": \"淡粉色\",\n      \"orangeColor\": \"橘色\",\n      \"yellowColor\": \"黃色\",\n      \"limeColor\": \"萊姆色\",\n      \"greenColor\": \"綠色\",\n      \"aquaColor\": \"水藍色\",\n      \"blueColor\": \"藍色\",\n      \"deleteTag\": \"刪除標籤\",\n      \"colorPanelTitle\": \"顏色\",\n      \"panelTitle\": \"選擇或建立選項\",\n      \"searchOption\": \"搜尋選項\",\n      \"searchOrCreateOption\": \"搜尋選項或建立一個\",\n      \"createNew\": \"建立新的\",\n      \"orSelectOne\": \"或選擇一個選項\",\n      \"typeANewOption\": \"輸入新選項\",\n      \"tagName\": \"標籤名稱\"\n    },\n    \"checklist\": {\n      \"taskHint\": \"任務描述\",\n      \"addNew\": \"新增任務\",\n      \"submitNewTask\": \"建立\",\n      \"hideComplete\": \"隱藏已完成任務\",\n      \"showComplete\": \"顯示所有任務\"\n    },\n    \"url\": {\n      \"launch\": \"在瀏覽器中開啟\",\n      \"copy\": \"複製連結到剪貼簿\",\n      \"textFieldHint\": \"輸入網址\"\n    },\n    \"relation\": {\n      \"relatedDatabasePlaceLabel\": \"相關資料庫\",\n      \"relatedDatabasePlaceholder\": \"沒有任何\",\n      \"inRelatedDatabase\": \"在\",\n      \"rowSearchTextFieldPlaceholder\": \"搜尋\",\n      \"noDatabaseSelected\": \"未選取資料庫，請先從下方清單中選擇一個:\",\n      \"emptySearchResult\": \"找不到任何記錄\",\n      \"linkedRowListLabel\": \"{} 個關聯資料列\",\n      \"unlinkedRowListLabel\": \"連結另一個資料列\"\n    },\n    \"menuName\": \"網格\",\n    \"referencedGridPrefix\": \"檢視\",\n    \"calculate\": \"計算\",\n    \"calculationTypeLabel\": {\n      \"none\": \"無\",\n      \"average\": \"平均\",\n      \"max\": \"最大值\",\n      \"median\": \"中間值\",\n      \"min\": \"最小值\",\n      \"sum\": \"總和\",\n      \"count\": \"計數\",\n      \"countEmpty\": \"計數為空\",\n      \"countEmptyShort\": \"空的\",\n      \"countNonEmpty\": \"計數不為空\",\n      \"countNonEmptyShort\": \"已滿\"\n    },\n    \"media\": {\n      \"rename\": \"重新命名\",\n      \"download\": \"下載\",\n      \"expand\": \"展開\",\n      \"delete\": \"刪除\",\n      \"moreFilesHint\": \"+{}\",\n      \"addFileOrImage\": \"新增檔案或連結\",\n      \"attachmentsHint\": \"{}\",\n      \"addFileMobile\": \"新增檔案\",\n      \"extraCount\": \"+{}\",\n      \"deleteFileDescription\": \"您確定要刪除此檔案嗎? 此動作不可逆轉。\",\n      \"showFileNames\": \"顯示檔案名稱\",\n      \"downloadSuccess\": \"檔案已下載\",\n      \"downloadFailedToken\": \"下載檔案失敗，使用者權杖無效\",\n      \"setAsCover\": \"設為封面\",\n      \"openInBrowser\": \"在瀏覽器中開啟\",\n      \"embedLink\": \"嵌入檔案連結\"\n    }\n  },\n  \"document\": {\n    \"menuName\": \"文件\",\n    \"date\": {\n      \"timeHintTextInTwelveHour\": \"下午 1:00\",\n      \"timeHintTextInTwentyFourHour\": \"13:00\"\n    },\n    \"creating\": \"正在建立…\",\n    \"slashMenu\": {\n      \"board\": {\n        \"selectABoardToLinkTo\": \"選擇要連結的看板\",\n        \"createANewBoard\": \"建立新的看板\"\n      },\n      \"grid\": {\n        \"selectAGridToLinkTo\": \"選擇要連結的網格\",\n        \"createANewGrid\": \"建立新網格\"\n      },\n      \"calendar\": {\n        \"selectACalendarToLinkTo\": \"選擇要連結到的日曆\",\n        \"createANewCalendar\": \"建立新日曆\"\n      },\n      \"document\": {\n        \"selectADocumentToLinkTo\": \"選擇要連結的文件\"\n      },\n      \"name\": {\n        \"textStyle\": \"文字樣式\",\n        \"list\": \"清單\",\n        \"toggle\": \"切換\",\n        \"fileAndMedia\": \"檔案和媒體\",\n        \"simpleTable\": \"簡單表格\",\n        \"visuals\": \"視覺效果\",\n        \"document\": \"文件\",\n        \"advanced\": \"進階\",\n        \"text\": \"文字\",\n        \"heading1\": \"標題 1\",\n        \"heading2\": \"標題 2\",\n        \"heading3\": \"標題 3\",\n        \"image\": \"圖像\",\n        \"bulletedList\": \"項目符號清單\",\n        \"numberedList\": \"編號清單\",\n        \"todoList\": \"待辦事項清單\",\n        \"doc\": \"文件\",\n        \"linkedDoc\": \"連結到頁面\",\n        \"grid\": \"網格\",\n        \"linkedGrid\": \"連結網格\",\n        \"kanban\": \"看板\",\n        \"linkedKanban\": \"連結看板\",\n        \"calendar\": \"日曆\",\n        \"linkedCalendar\": \"連結日曆\",\n        \"quote\": \"引用\",\n        \"divider\": \"分隔線\",\n        \"table\": \"表格\",\n        \"callout\": \"醒目提示框\",\n        \"outline\": \"大綱\",\n        \"mathEquation\": \"數學方程式\",\n        \"code\": \"程式碼\",\n        \"toggleList\": \"切換列表\",\n        \"toggleHeading1\": \"切換標題 1\",\n        \"toggleHeading2\": \"切換標題 2\",\n        \"toggleHeading3\": \"切換標題 3\",\n        \"emoji\": \"表情符號\",\n        \"aiWriter\": \"向 AI 提問\",\n        \"dateOrReminder\": \"日期或提醒事項\",\n        \"photoGallery\": \"照片圖庫\",\n        \"file\": \"檔案\",\n        \"twoColumns\": \"2 欄\",\n        \"threeColumns\": \"3 欄\",\n        \"fourColumns\": \"4 欄\"\n      },\n      \"subPage\": {\n        \"name\": \"文件\",\n        \"keyword1\": \"子頁面\",\n        \"keyword2\": \"頁面\",\n        \"keyword3\": \"子頁面\",\n        \"keyword4\": \"插入頁面\",\n        \"keyword5\": \"嵌入頁面\",\n        \"keyword6\": \"新頁面\",\n        \"keyword7\": \"建立頁面\",\n        \"keyword8\": \"文件\"\n      }\n    },\n    \"selectionMenu\": {\n      \"outline\": \"大綱\",\n      \"codeBlock\": \"程式碼區塊\"\n    },\n    \"plugins\": {\n      \"referencedBoard\": \"已連結的看板\",\n      \"referencedGrid\": \"已連結的網格\",\n      \"referencedCalendar\": \"已連結的日曆\",\n      \"referencedDocument\": \"已連結的文件\",\n      \"aiWriter\": {\n        \"userQuestion\": \"向 AI提問\",\n        \"continueWriting\": \"繼續寫作\",\n        \"fixSpelling\": \"修正拼字和語法\",\n        \"improveWriting\": \"改善寫作品質\",\n        \"summarize\": \"總結\",\n        \"explain\": \"解釋\",\n        \"makeShorter\": \"縮短篇幅\",\n        \"makeLonger\": \"擴展內容\"\n      },\n      \"autoGeneratorMenuItemName\": \"AI 作者\",\n      \"autoGeneratorTitleName\": \"AI: 請 AI 撰寫任何內容…\",\n      \"autoGeneratorLearnMore\": \"瞭解更多\",\n      \"autoGeneratorGenerate\": \"生成\",\n      \"autoGeneratorHintText\": \"詢問 AI…\",\n      \"autoGeneratorCantGetOpenAIKey\": \"無法取得 AI 金鑰\",\n      \"autoGeneratorRewrite\": \"重新撰寫\",\n      \"smartEdit\": \"詢問 AI\",\n      \"aI\": \"AI\",\n      \"smartEditFixSpelling\": \"修正拼字和文法\",\n      \"warning\": \"⚠️ AI 回應可能不準確或具有誤導性。\",\n      \"smartEditSummarize\": \"總結\",\n      \"smartEditImproveWriting\": \"改善寫作\",\n      \"smartEditMakeLonger\": \"使其更長\",\n      \"smartEditCouldNotFetchResult\": \"無法取得 AI 的結果\",\n      \"smartEditCouldNotFetchKey\": \"無法取得 AI 金鑰\",\n      \"smartEditDisabled\": \"在設定連結 AI \",\n      \"appflowyAIEditDisabled\": \"登入以啟用 AI 功能\",\n      \"discardResponse\": \"您確定要捨棄 AI 回應嗎?\",\n      \"createInlineMathEquation\": \"建立公式\",\n      \"fonts\": \"字型\",\n      \"insertDate\": \"插入日期\",\n      \"emoji\": \"表情符號\",\n      \"toggleList\": \"切換列表\",\n      \"emptyToggleHeading\": \" 空的切換器 h{}。點擊以新增內容。\",\n      \"emptyToggleList\": \"空的切換器清單。點擊以新增內容。\",\n      \"emptyToggleHeadingWeb\": \"空的切換器 h{level}。點擊以新增內容\",\n      \"quoteList\": \"引述列表\",\n      \"numberedList\": \"編號列表\",\n      \"bulletedList\": \"項目符號列表\",\n      \"todoList\": \"待辦事項列表\",\n      \"callout\": \"引述\",\n      \"simpleTable\": {\n        \"moreActions\": {\n          \"color\": \"顏色\",\n          \"align\": \"對齊\",\n          \"delete\": \"刪除\",\n          \"duplicate\": \"副本\",\n          \"insertLeft\": \"插入左側\",\n          \"insertRight\": \"插入右側\",\n          \"insertAbove\": \"插入上方\",\n          \"insertBelow\": \"在下面插入\",\n          \"headerColumn\": \"標題欄位\",\n          \"headerRow\": \"標題列\",\n          \"clearContents\": \"清除內容\",\n          \"setToPageWidth\": \"設定為頁面寬度\",\n          \"distributeColumnsWidth\": \"平均分配欄位\",\n          \"duplicateRow\": \"副本列\",\n          \"duplicateColumn\": \"副本欄位\",\n          \"textColor\": \"文字顏色\",\n          \"cellBackgroundColor\": \"儲存格背景顏色\",\n          \"duplicateTable\": \"副本表格\"\n        },\n        \"clickToAddNewRow\": \"點擊以新增一列\",\n        \"clickToAddNewColumn\": \"點擊以新增一個欄位\",\n        \"clickToAddNewRowAndColumn\": \"點擊新增列和欄\",\n        \"headerName\": {\n          \"table\": \"表格\",\n          \"alignText\": \"對齊文字\"\n        }\n      },\n      \"cover\": {\n        \"changeCover\": \"更換封面\",\n        \"colors\": \"顏色\",\n        \"images\": \"圖片\",\n        \"clearAll\": \"全部清除\",\n        \"abstract\": \"摘要\",\n        \"addCover\": \"新增封面\",\n        \"addLocalImage\": \"新增本地圖片\",\n        \"invalidImageUrl\": \"無效的圖片網址\",\n        \"failedToAddImageToGallery\": \"無法新增圖片到相簿\",\n        \"enterImageUrl\": \"輸入圖片網址\",\n        \"add\": \"新增\",\n        \"back\": \"返回\",\n        \"saveToGallery\": \"儲存到相簿\",\n        \"removeIcon\": \"移除圖示\",\n        \"removeCover\": \"移除封面\",\n        \"pasteImageUrl\": \"貼上圖片網址\",\n        \"or\": \"或\",\n        \"pickFromFiles\": \"從檔案中挑選\",\n        \"couldNotFetchImage\": \"無法抓取圖片\",\n        \"imageSavingFailed\": \"圖片儲存失敗\",\n        \"addIcon\": \"新增圖示\",\n        \"changeIcon\": \"更換圖示\",\n        \"coverRemoveAlert\": \"刪除後將從封面中刪除。\",\n        \"alertDialogConfirmation\": \"你確定你要繼續嗎?\"\n      },\n      \"mathEquation\": {\n        \"name\": \"數學方程式\",\n        \"addMathEquation\": \"新增 TeX 方程式\",\n        \"editMathEquation\": \"編輯數學方程式\"\n      },\n      \"optionAction\": {\n        \"click\": \"點選\",\n        \"toOpenMenu\": \" 開啟選單\",\n        \"drag\": \"拖曳\",\n        \"toMove\": \"移動\",\n        \"delete\": \"刪除\",\n        \"duplicate\": \"複製\",\n        \"turnInto\": \"變成\",\n        \"moveUp\": \"上移\",\n        \"moveDown\": \"下移\",\n        \"color\": \"顏色\",\n        \"align\": \"對齊\",\n        \"left\": \"左\",\n        \"center\": \"中\",\n        \"right\": \"右\",\n        \"defaultColor\": \"預設值\",\n        \"depth\": \"深度\",\n        \"copyLinkToBlock\": \"Copy link to block複製連結到區塊\"\n      },\n      \"image\": {\n        \"addAnImage\": \"新增圖片\",\n        \"copiedToPasteBoard\": \"圖片連結已複製到剪貼簿\",\n        \"addAnImageDesktop\": \"新增圖片\",\n        \"addAnImageMobile\": \"點擊新增一張或多張圖片\",\n        \"dropImageToInsert\": \"將圖片拖曳至此插入\",\n        \"imageUploadFailed\": \"圖片上傳失敗\",\n        \"imageDownloadFailed\": \"圖片下載失敗，請再試一次\",\n        \"imageDownloadFailedToken\": \"圖片下載因缺少用戶憑證而失敗，請再試一次\\n\",\n        \"errorCode\": \"錯誤代碼\"\n      },\n      \"photoGallery\": {\n        \"name\": \"照片庫\",\n        \"imageKeyword\": \"圖像\",\n        \"imageGalleryKeyword\": \"圖片庫\",\n        \"photoKeyword\": \"照片\",\n        \"photoBrowserKeyword\": \"照片瀏覽器\",\n        \"galleryKeyword\": \"圖庫\",\n        \"addImageTooltip\": \"新增圖片\",\n        \"changeLayoutTooltip\": \"變更版面\",\n        \"browserLayout\": \"瀏覽器\",\n        \"gridLayout\": \"網格\",\n        \"deleteBlockTooltip\": \"刪除整個圖庫\"\n      },\n      \"math\": {\n        \"copiedToPasteBoard\": \"數學方程式已複製到剪貼簿\"\n      },\n      \"urlPreview\": {\n        \"copiedToPasteBoard\": \"連結已複製到剪貼簿\",\n        \"convertToLink\": \"轉換為嵌入鏈接\"\n      },\n      \"outline\": {\n        \"addHeadingToCreateOutline\": \"新增標題以建立目錄。\",\n        \"noMatchHeadings\": \"未找到匹配的標題\"\n      },\n      \"table\": {\n        \"addAfter\": \"在後方新增\",\n        \"addBefore\": \"在前方新增\",\n        \"delete\": \"刪除\",\n        \"clear\": \"清除內容\",\n        \"duplicate\": \"複製\",\n        \"bgColor\": \"背景顏色\"\n      },\n      \"contextMenu\": {\n        \"copy\": \"複製\",\n        \"cut\": \"剪下\",\n        \"paste\": \"貼上\",\n        \"pasteAsPlainText\": \"以純文字貼上\"\n      },\n      \"action\": \"操作\",\n      \"database\": {\n        \"selectDataSource\": \"選擇資料來源\",\n        \"noDataSource\": \"無資料來源\",\n        \"selectADataSource\": \"選擇一個資料來源\",\n        \"toContinue\": \"以繼續\",\n        \"newDatabase\": \"新建資料庫\",\n        \"linkToDatabase\": \"連結至資料庫\"\n      },\n      \"date\": \"日期\",\n      \"video\": {\n        \"label\": \"影片\",\n        \"emptyLabel\": \"新增影片\",\n        \"placeholder\": \"貼上影片連結\",\n        \"copiedToPasteBoard\": \"影片連結已複製到剪貼簿\",\n        \"insertVideo\": \"新增影片\",\n        \"invalidVideoUrl\": \"來源網址目前不受支援。\",\n        \"invalidVideoUrlYouTube\": \"目前不支援 YouTube。\",\n        \"supportedFormats\": \"支援格式: MP4、WebM、MOV、AVI、FLV、MPEG/M4V、H.264\"\n      },\n      \"file\": {\n        \"name\": \"檔案\",\n        \"uploadTab\": \"上傳\",\n        \"uploadMobile\": \"選擇一個檔案\",\n        \"uploadMobileGallery\": \"來自照片庫\",\n        \"networkTab\": \"嵌入連結\",\n        \"placeholderText\": \"上傳或嵌入檔案\",\n        \"placeholderDragging\": \"將檔案拖曳以上傳\",\n        \"dropFileToUpload\": \"拖放檔案進行上傳\",\n        \"fileUploadHint\": \"拖放檔案或點擊以\",\n        \"fileUploadHintSuffix\": \"瀏覽\",\n        \"networkHint\": \"貼上檔案連結\",\n        \"networkUrlInvalid\": \"網址無效。請檢查網址並再次嘗試。\",\n        \"networkAction\": \"嵌入\",\n        \"fileTooBigError\": \"檔案容量太大，請上傳容量低於 10MB 的檔案。\",\n        \"renameFile\": {\n          \"title\": \"重新命名檔案\",\n          \"description\": \"輸入此檔案的新名稱\",\n          \"nameEmptyError\": \"檔案名稱不能為空。\"\n        },\n        \"uploadedAt\": \"已上傳至 {}\",\n        \"linkedAt\": \"連結已添加於 {}\",\n        \"failedToOpenMsg\": \"開啟失敗，找不到檔案。\"\n      },\n      \"subPage\": {\n        \"handlingPasteHint\": \"- (處理貼上)\",\n        \"errors\": {\n          \"failedDeletePage\": \"刪除頁面失敗\",\n          \"failedCreatePage\": \"建立頁面失敗\",\n          \"failedMovePage\": \"將頁面移至此文件失敗\",\n          \"failedDuplicatePage\": \"複製頁面失敗\",\n          \"failedDuplicateFindView\": \"複製頁面失敗 - 找不到原始視圖\"\n        }\n      },\n      \"cannotMoveToItsChildren\": \"無法移動到其子代\",\n      \"linkPreview\": {\n        \"typeSelection\": {\n          \"pasteAs\": \"貼上為\",\n          \"mention\": \"提及\",\n          \"URL\": \"網址\",\n          \"bookmark\": \"書籤\",\n          \"embed\": \"嵌入\"\n        },\n        \"linkPreviewMenu\": {\n          \"toMetion\": \"轉換為提及\",\n          \"toUrl\": \"轉換為網址\",\n          \"toEmbed\": \"轉換為嵌入\",\n          \"toBookmark\": \"轉換為書籤\",\n          \"copyLink\": \"複製連結\",\n          \"replace\": \"代替\",\n          \"reload\": \"重新載入\",\n          \"removeLink\": \"移除連結\",\n          \"pasteHint\": \"貼上 https://...\",\n          \"unableToDisplay\": \"無法顯示\"\n        }\n      }\n    },\n    \"outlineBlock\": {\n      \"placeholder\": \"目錄\"\n    },\n    \"textBlock\": {\n      \"placeholder\": \"輸入“/”作為命令\"\n    },\n    \"title\": {\n      \"placeholder\": \"無標題\"\n    },\n    \"imageBlock\": {\n      \"placeholder\": \"點選以新增圖片\",\n      \"upload\": {\n        \"label\": \"上傳\",\n        \"placeholder\": \"點選以上傳圖片\"\n      },\n      \"url\": {\n        \"label\": \"圖片網址\",\n        \"placeholder\": \"輸入圖片網址\"\n      },\n      \"ai\": {\n        \"label\": \"由 AI 生成圖片\",\n        \"placeholder\": \"請輸入提示讓 AI 生成圖片\"\n      },\n      \"stability_ai\": {\n        \"label\": \"由 Stability AI 生成圖片\",\n        \"placeholder\": \"請輸入提示讓 Stability AI 生成圖片\"\n      },\n      \"support\": \"圖片大小限制為 5MB。支援的格式：JPEG、PNG、GIF、SVG\",\n      \"error\": {\n        \"invalidImage\": \"無效的圖片\",\n        \"invalidImageSize\": \"圖片大小必須小於 5MB\",\n        \"invalidImageFormat\": \"不支援的圖片格式。支援的格式：JPEG、PNG、GIF、SVG\",\n        \"invalidImageUrl\": \"無效的圖片網址\",\n        \"noImage\": \"沒有該檔案或目錄\",\n        \"multipleImagesFailed\": \"部分圖片上傳失敗，請再試一次\"\n      },\n      \"embedLink\": {\n        \"label\": \"嵌入連結\",\n        \"placeholder\": \"貼上或輸入圖片連結\"\n      },\n      \"unsplash\": {\n        \"label\": \"Unsplash\"\n      },\n      \"searchForAnImage\": \"搜尋圖片\",\n      \"pleaseInputYourOpenAIKey\": \"請在設定頁面輸入您的 AI 金鑰\",\n      \"saveImageToGallery\": \"儲存圖片\",\n      \"failedToAddImageToGallery\": \"無法將圖片新增到相簿\",\n      \"successToAddImageToGallery\": \"圖片已成功新增到相簿\",\n      \"unableToLoadImage\": \"無法載入圖片\",\n      \"maximumImageSize\": \"支援的最大上傳圖片大小為 10MB\",\n      \"uploadImageErrorImageSizeTooBig\": \"圖片大小必須小於 10MB\",\n      \"imageIsUploading\": \"圖片上傳中\",\n      \"openFullScreen\": \"全螢幕打開\",\n      \"interactiveViewer\": {\n        \"toolbar\": {\n          \"previousImageTooltip\": \"上一張圖片\",\n          \"nextImageTooltip\": \"下一張圖片\",\n          \"zoomOutTooltip\": \"縮小\",\n          \"zoomInTooltip\": \"放大\",\n          \"changeZoomLevelTooltip\": \"更改縮放級別\",\n          \"openLocalImage\": \"打開圖片\",\n          \"downloadImage\": \"下載圖片\",\n          \"closeViewer\": \"關閉互動式檢視器\",\n          \"scalePercentage\": \"{}%\",\n          \"deleteImageTooltip\": \"刪除圖片\"\n        }\n      }\n    },\n    \"codeBlock\": {\n      \"language\": {\n        \"label\": \"語言\",\n        \"placeholder\": \"選擇語言\",\n        \"auto\": \"自動\"\n      },\n      \"copyTooltip\": \"複製\",\n      \"searchLanguageHint\": \"搜尋語言\",\n      \"codeCopiedSnackbar\": \"程式碼已複製到剪貼簿\"\n    },\n    \"inlineLink\": {\n      \"placeholder\": \"貼上或輸入連結\",\n      \"openInNewTab\": \"在新分頁中開啟\",\n      \"copyLink\": \"複製連結\",\n      \"removeLink\": \"移除連結\",\n      \"url\": {\n        \"label\": \"連結網址\",\n        \"placeholder\": \"輸入連結網址\"\n      },\n      \"title\": {\n        \"label\": \"連結標題\",\n        \"placeholder\": \"輸入連結標題\"\n      }\n    },\n    \"mention\": {\n      \"placeholder\": \"提及一個人、頁面或日期...\",\n      \"page\": {\n        \"label\": \"連結到頁面\",\n        \"tooltip\": \"點選以開啟頁面\"\n      },\n      \"deleted\": \"已刪除\",\n      \"deletedContent\": \"該內容不存在或已經刪除\",\n      \"noAccess\": \"無權限\",\n      \"deletedPage\": \"已刪除頁面\",\n      \"trashHint\": \" - 在垃圾桶裡\",\n      \"morePages\": \"更多頁面\"\n    },\n    \"toolbar\": {\n      \"resetToDefaultFont\": \"重設為預設字型\",\n      \"textSize\": \"文字大小\",\n      \"textColor\": \"文字顏色\",\n      \"h1\": \"標題 1\",\n      \"h2\": \"標題 2\",\n      \"h3\": \"標題 3\",\n      \"alignLeft\": \"左對齊\",\n      \"alignRight\": \"右對齊\",\n      \"alignCenter\": \"居中對齊\",\n      \"link\": \"關聯\",\n      \"textAlign\": \"文字對齊\",\n      \"moreOptions\": \"更多選項\",\n      \"font\": \"字型\",\n      \"inlineCode\": \"內聯程式碼\",\n      \"suggestions\": \"建議事項\",\n      \"turnInto\": \"變成\",\n      \"equation\": \"方程式\",\n      \"insert\": \"插入\",\n      \"linkInputHint\": \"貼上連結或搜尋頁面\",\n      \"pageOrURL\": \"頁面或網址\",\n      \"linkName\": \"連結名稱\",\n      \"linkNameHint\": \"輸入連結名稱\"\n    },\n    \"errorBlock\": {\n      \"theBlockIsNotSupported\": \"目前版本不支援此區塊。\",\n      \"clickToCopyTheBlockContent\": \"點擊以複製區塊內容\",\n      \"blockContentHasBeenCopied\": \"區塊內容已被複製。\",\n      \"parseError\": \"解析 {} 區塊時發生錯誤。\",\n      \"copyBlockContent\": \"複製區塊內容\"\n    },\n    \"mobilePageSelector\": {\n      \"title\": \"選擇頁面\",\n      \"failedToLoad\": \"載入頁面清單失敗\",\n      \"noPagesFound\": \"沒有找到該頁面\"\n    },\n    \"attachmentMenu\": {\n      \"choosePhoto\": \"選擇照片\",\n      \"takePicture\": \"拍照\",\n      \"chooseFile\": \"選擇檔案\"\n    }\n  },\n  \"board\": {\n    \"column\": {\n      \"label\": \"欄位\",\n      \"createNewCard\": \"新的\",\n      \"renameGroupTooltip\": \"點選以重新命名群組\",\n      \"createNewColumn\": \"新增群組\",\n      \"addToColumnTopTooltip\": \"在頂部新增卡片\",\n      \"addToColumnBottomTooltip\": \"在底部新增卡片\",\n      \"renameColumn\": \"重新命名\",\n      \"hideColumn\": \"隱藏\",\n      \"newGroup\": \"新群組\",\n      \"deleteColumn\": \"刪除\",\n      \"deleteColumnConfirmation\": \"這將刪除此群組及其中所有卡片。您確定要繼續嗎?\"\n    },\n    \"hiddenGroupSection\": {\n      \"sectionTitle\": \"隱藏的群組\",\n      \"collapseTooltip\": \"隱藏已隱藏的群組\",\n      \"expandTooltip\": \"檢視隱藏的群組\"\n    },\n    \"cardDetail\": \"卡片詳細資訊\",\n    \"cardActions\": \"卡片操作\",\n    \"cardDuplicated\": \"卡片已複製\",\n    \"cardDeleted\": \"卡片已刪除\",\n    \"showOnCard\": \"在卡片詳細資訊中顯示\",\n    \"setting\": \"設定\",\n    \"propertyName\": \"屬性名稱\",\n    \"menuName\": \"看板\",\n    \"showUngrouped\": \"顯示未分組的項目\",\n    \"ungroupedButtonText\": \"未分組\",\n    \"ungroupedButtonTooltip\": \"包含不屬於任何群組的卡片\",\n    \"ungroupedItemsTitle\": \"點選以新增到看板\",\n    \"groupBy\": \"依據分組\",\n    \"groupCondition\": \"群組條件\",\n    \"referencedBoardPrefix\": \"檢視...\",\n    \"notesTooltip\": \"內部備註\",\n    \"mobile\": {\n      \"editURL\": \"編輯網址\",\n      \"showGroup\": \"顯示群組\",\n      \"showGroupContent\": \"您確定要在看板上顯示此群組嗎?\",\n      \"failedToLoad\": \"無法載入看板\"\n    },\n    \"dateCondition\": {\n      \"weekOf\": \"第 {} 週 - {}\",\n      \"today\": \"今天\",\n      \"yesterday\": \"昨天\",\n      \"tomorrow\": \"明天\",\n      \"lastSevenDays\": \"過去 7 天\",\n      \"nextSevenDays\": \"未來 7天\",\n      \"lastThirtyDays\": \"過去 30 天\",\n      \"nextThirtyDays\": \"未來 30 天\"\n    },\n    \"noGroup\": \"沒有按屬性分組\",\n    \"noGroupDesc\": \"看板視圖需要一個用於分組的屬性才能顯示。\",\n    \"media\": {\n      \"cardText\": \"{} {}\",\n      \"fallbackName\": \"檔案\"\n    }\n  },\n  \"calendar\": {\n    \"menuName\": \"日曆\",\n    \"defaultNewCalendarTitle\": \"無標題\",\n    \"newEventButtonTooltip\": \"新增活動\",\n    \"navigation\": {\n      \"today\": \"今天\",\n      \"jumpToday\": \"跳到今天\",\n      \"previousMonth\": \"上個月\",\n      \"nextMonth\": \"下個月\",\n      \"views\": {\n        \"day\": \"天\",\n        \"week\": \"星期\",\n        \"month\": \"月\",\n        \"year\": \"年\"\n      }\n    },\n    \"mobileEventScreen\": {\n      \"emptyTitle\": \"目前尚目前沒有任何活動\",\n      \"emptyBody\": \"點選加號按鈕以在此日新增事件。\"\n    },\n    \"settings\": {\n      \"showWeekNumbers\": \"顯示週數\",\n      \"showWeekends\": \"顯示週末\",\n      \"firstDayOfWeek\": \"週開始於\",\n      \"layoutDateField\": \"排列方式\",\n      \"changeLayoutDateField\": \"更改排列欄位\",\n      \"noDateTitle\": \"未註明日無日期\",\n      \"noDateHint\": {\n        \"zero\": \"未排程的事件將顯示在這裡\",\n        \"one\": \"有 {count} 個未排程的事件\",\n        \"other\": \"有 {count} 個未排程的事件\"\n      },\n      \"unscheduledEventsTitle\": \"未排定的活動\",\n      \"clickToAdd\": \"點選以新增至日曆\",\n      \"name\": \"日曆設定\",\n      \"clickToOpen\": \"點擊以開啟記錄\"\n    },\n    \"referencedCalendarPrefix\": \"檢視\",\n    \"quickJumpYear\": \"跳到\",\n    \"duplicateEvent\": \"重複事件\"\n  },\n  \"errorDialog\": {\n    \"title\": \"@:appName 錯誤\",\n    \"howToFixFallback\": \"對於給您帶來的不便，我們深表歉意！在我們的 GitHub 頁面上提交描述您的錯誤的問題。\",\n    \"howToFixFallbackHint1\": \"對於造成的不便，我們深感抱歉！在我們的...上提交問題。\",\n    \"howToFixFallbackHint2\": \" 頁面描述您的錯誤。\",\n    \"github\": \"在 GitHub 上檢視\"\n  },\n  \"search\": {\n    \"label\": \"搜尋\",\n    \"sidebarSearchIcon\": \"搜尋並快速跳至頁面\",\n    \"searchOrAskAI\": \"搜尋或詢問 AI\",\n    \"searchFieldHint\": \"在{}中搜尋或提問...\",\n    \"askAIAnything\": \"向 AI 提問任何問題\",\n    \"askAIFor\": \"詢問 AI\",\n    \"searching\": \"正在搜尋…\",\n    \"noResultForSearching\": \"沒有找到符合的結果\",\n    \"noResultForSearchingHint\": \"嘗試不同的問題或關鍵字。\\n 部分頁面可能在垃圾桶中。\",\n    \"noResultForSearchingHintWithoutTrash\": \"嘗試不同的問題或關鍵字。\\n 部分頁面可能在\",\n    \"bestMatch\": \"最佳匹配\",\n    \"seeMore\": \"查看更多\",\n    \"showMore\": \"顯示更多\",\n    \"somethingWentWrong\": \"出了點問題\",\n    \"pageNotExist\": \"這個頁面不存在\",\n    \"tryAgainOrLater\": \"請稍後再試一次\",\n    \"placeholder\": {\n      \"actions\": \"搜尋操作...\"\n    }\n  },\n  \"message\": {\n    \"copy\": {\n      \"success\": \"已複製已複製到剪貼簿\",\n      \"fail\": \"無法複製\"\n    }\n  },\n  \"unSupportBlock\": \"目前版本不支援此區塊。\",\n  \"views\": {\n    \"deleteContentTitle\": \"您確定要刪除 {pageType} 嗎?\",\n    \"deleteContentCaption\": \"如果您刪除此 {pageType}，您可以從垃圾桶中將其復原。\"\n  },\n  \"colors\": {\n    \"custom\": \"自訂\",\n    \"default\": \"預設值\",\n    \"red\": \"紅色\",\n    \"orange\": \"橙色\",\n    \"yellow\": \"黃色\",\n    \"green\": \"綠色\",\n    \"blue\": \"藍色\",\n    \"purple\": \"紫色\",\n    \"pink\": \"粉紅色\",\n    \"brown\": \"棕色\",\n    \"gray\": \"灰色\"\n  },\n  \"emoji\": {\n    \"emojiTab\": \"表情符號\",\n    \"search\": \"搜尋表情符號\",\n    \"noRecent\": \"無最近使用的表情符號\",\n    \"noEmojiFound\": \"找不到表情符號\",\n    \"filter\": \"篩選\",\n    \"random\": \"隨機\",\n    \"selectSkinTone\": \"選擇膚色\",\n    \"remove\": \"移除表情符號\",\n    \"categories\": {\n      \"smileys\": \"笑臉與情緒\",\n      \"people\": \"人物\",\n      \"animals\": \"自然\",\n      \"food\": \"食物\",\n      \"activities\": \"活動\",\n      \"places\": \"地點\",\n      \"objects\": \"物件\",\n      \"symbols\": \"符號\",\n      \"flags\": \"旗幟\",\n      \"nature\": \"自然\",\n      \"frequentlyUsed\": \"常用\"\n    },\n    \"skinTone\": {\n      \"default\": \"預設值\",\n      \"light\": \"淺色\",\n      \"mediumLight\": \"中淺色\",\n      \"medium\": \"中色\",\n      \"mediumDark\": \"中深色\",\n      \"dark\": \"深色\"\n    },\n    \"openSourceIconsFrom\": \"從來源取得圖示\"\n  },\n  \"inlineActions\": {\n    \"noResults\": \"無結果\",\n    \"recentPages\": \"最近的頁面\",\n    \"pageReference\": \"頁面參照\",\n    \"docReference\": \"文件參照\",\n    \"boardReference\": \"看板參照\",\n    \"calReference\": \"日曆參照\",\n    \"gridReference\": \"網格參照\",\n    \"date\": \"日期\",\n    \"reminder\": {\n      \"groupTitle\": \"提醒\",\n      \"shortKeyword\": \"提醒\"\n    },\n    \"createPage\": \"建立\\\"{}\\\"子頁面\"\n  },\n  \"datePicker\": {\n    \"dateTimeFormatTooltip\": \"在設定中更改日期和時間格式\",\n    \"dateFormat\": \"日期格式\",\n    \"includeTime\": \"包含時間\",\n    \"isRange\": \"結束日期\",\n    \"timeFormat\": \"時間格式\",\n    \"clearDate\": \"清除日期\",\n    \"reminderLabel\": \"提醒\",\n    \"selectReminder\": \"選擇提醒\",\n    \"reminderOptions\": {\n      \"none\": \"無\",\n      \"atTimeOfEvent\": \"活動時間\",\n      \"fiveMinsBefore\": \"提前 5 分鐘\",\n      \"tenMinsBefore\": \"提前 10 分鐘\",\n      \"fifteenMinsBefore\": \"提前 15 分鐘\",\n      \"thirtyMinsBefore\": \"提前 30 分鐘\",\n      \"oneHourBefore\": \"提前 1 小時\",\n      \"twoHoursBefore\": \"提前 2 小時\",\n      \"onDayOfEvent\": \"活動當天\",\n      \"oneDayBefore\": \"提前 1 天\",\n      \"twoDaysBefore\": \"提前 2 天\",\n      \"oneWeekBefore\": \"一週前\",\n      \"custom\": \"自訂\"\n    }\n  },\n  \"relativeDates\": {\n    \"yesterday\": \"昨天\",\n    \"today\": \"今天\",\n    \"tomorrow\": \"明天\",\n    \"oneWeek\": \"1 週\"\n  },\n  \"notificationHub\": {\n    \"title\": \"通知\",\n    \"closeNotification\": \"關閉通知\",\n    \"viewNotifications\": \"查看通知\",\n    \"noNotifications\": \"目前沒有任何通知\",\n    \"mentionedYou\": \"已提及您\",\n    \"archivedTooltip\": \"將此通知歸檔\",\n    \"unarchiveTooltip\": \"取消將此通知歸檔\",\n    \"markAsReadTooltip\": \"此通知標記為已讀取\",\n    \"markAsArchivedSucceedToast\": \"已成功歸檔\",\n    \"markAllAsArchivedSucceedToast\": \"已成功歸檔全部項目\",\n    \"markAsReadSucceedToast\": \"已成功標記為已讀取\",\n    \"markAllAsReadSucceedToast\": \"已成功將所有項目標記為已讀取\",\n    \"today\": \"今天\",\n    \"older\": \"較舊\",\n    \"mobile\": {\n      \"title\": \"更新\"\n    },\n    \"emptyTitle\": \"全部處理完畢!\",\n    \"emptyBody\": \"沒有待處理的通知或動作。享受這份寧靜。\",\n    \"tabs\": {\n      \"inbox\": \"收件匣\",\n      \"upcoming\": \"即將到來\"\n    },\n    \"actions\": {\n      \"markAllRead\": \"全部標為已讀\",\n      \"showAll\": \"全部\",\n      \"showUnreads\": \"未讀\"\n    },\n    \"filters\": {\n      \"ascending\": \"升冪\",\n      \"descending\": \"降冪\",\n      \"groupByDate\": \"依日期分組\",\n      \"showUnreadsOnly\": \"只顯示未讀\",\n      \"resetToDefault\": \"重設為預設值\"\n    }\n  },\n  \"reminderNotification\": {\n    \"title\": \"提醒\",\n    \"message\": \"記得在忘記之前檢查一下!\",\n    \"tooltipDelete\": \"刪除\",\n    \"tooltipMarkRead\": \"標為已讀\",\n    \"tooltipMarkUnread\": \"標為未讀\"\n  },\n  \"findAndReplace\": {\n    \"find\": \"尋找\",\n    \"previousMatch\": \"上一個符合\",\n    \"nextMatch\": \"下一個符合\",\n    \"close\": \"關閉\",\n    \"replace\": \"取代\",\n    \"replaceAll\": \"全部取代\",\n    \"noResult\": \"無結果\",\n    \"caseSensitive\": \"區分大小寫\",\n    \"searchMore\": \"搜尋以查找更多結果\"\n  },\n  \"error\": {\n    \"weAreSorry\": \"我們很抱歉\",\n    \"loadingViewError\": \"我們在載入此檢視時遇到問題。請檢查您的網路連線，重新整理應用程式，並且如果問題持續，請隨時聯絡我們的團隊。\",\n    \"syncError\": \"資料尚未從其他裝置同步\",\n    \"syncErrorHint\": \"請在上次編輯的設備上重新開啟此頁面，然後在目前的設備上再次開啟它。\",\n    \"clickToCopy\": \"點擊以複製錯誤碼\"\n  },\n  \"editor\": {\n    \"bold\": \"粗體\",\n    \"bulletedList\": \"項目符號清單\",\n    \"bulletedListShortForm\": \"項目符號\",\n    \"checkbox\": \"核取方塊\",\n    \"embedCode\": \"嵌入代碼\",\n    \"heading1\": \"標題 1\",\n    \"heading2\": \"標題 2\",\n    \"heading3\": \"標題 3\",\n    \"highlight\": \"醒目提示\",\n    \"color\": \"顏色\",\n    \"image\": \"圖片\",\n    \"date\": \"日期\",\n    \"page\": \"頁面\",\n    \"italic\": \"斜體\",\n    \"link\": \"連結\",\n    \"numberedList\": \"編號清單\",\n    \"numberedListShortForm\": \"編號\",\n    \"toggleHeading1ShortForm\": \"切換標題 1\",\n    \"toggleHeading2ShortForm\": \"切換標題 2\",\n    \"toggleHeading3ShortForm\": \"切換標題 3\",\n    \"quote\": \"引述\",\n    \"strikethrough\": \"刪除線\",\n    \"text\": \"文字\",\n    \"underline\": \"底線\",\n    \"fontColorDefault\": \"預設值\",\n    \"fontColorGray\": \"灰色\",\n    \"fontColorBrown\": \"棕色\",\n    \"fontColorOrange\": \"橙色\",\n    \"fontColorYellow\": \"黃色\",\n    \"fontColorGreen\": \"綠色\",\n    \"fontColorBlue\": \"藍色\",\n    \"fontColorPurple\": \"紫色\",\n    \"fontColorPink\": \"粉紅色\",\n    \"fontColorRed\": \"紅色\",\n    \"backgroundColorDefault\": \"預設值背景\",\n    \"backgroundColorGray\": \"灰色背景\",\n    \"backgroundColorBrown\": \"棕色背景\",\n    \"backgroundColorOrange\": \"橙色背景\",\n    \"backgroundColorYellow\": \"黃色背景\",\n    \"backgroundColorGreen\": \"綠色背景\",\n    \"backgroundColorBlue\": \"藍色背景\",\n    \"backgroundColorPurple\": \"紫色背景\",\n    \"backgroundColorPink\": \"粉紅色背景\",\n    \"backgroundColorRed\": \"紅色背景\",\n    \"backgroundColorLime\": \"青色背景\",\n    \"backgroundColorAqua\": \"水藍色背景\",\n    \"done\": \"完成\",\n    \"cancel\": \"取消\",\n    \"tint1\": \"色調 1\",\n    \"tint2\": \"色調 2\",\n    \"tint3\": \"色調 3\",\n    \"tint4\": \"色調 4\",\n    \"tint5\": \"色調 5\",\n    \"tint6\": \"色調 6\",\n    \"tint7\": \"色調 7\",\n    \"tint8\": \"色調 8\",\n    \"tint9\": \"色調 9\",\n    \"lightLightTint1\": \"紫色\",\n    \"lightLightTint2\": \"粉紅色\",\n    \"lightLightTint3\": \"淺粉紅色\",\n    \"lightLightTint4\": \"橙色\",\n    \"lightLightTint5\": \"黃色\",\n    \"lightLightTint6\": \"萊姆色\",\n    \"lightLightTint7\": \"綠色\",\n    \"lightLightTint8\": \"水綠色\",\n    \"lightLightTint9\": \"藍色\",\n    \"urlHint\": \"網址\",\n    \"mobileHeading1\": \"標題 1\",\n    \"mobileHeading2\": \"標題 2\",\n    \"mobileHeading3\": \"標題 3\",\n    \"mobileHeading4\": \"標題 4\",\n    \"mobileHeading5\": \"標題 5\",\n    \"mobileHeading6\": \"標題 6\",\n    \"textColor\": \"文字顏色\",\n    \"backgroundColor\": \"背景色\",\n    \"addYourLink\": \"新增你的連結\",\n    \"openLink\": \"開啟連結\",\n    \"copyLink\": \"複製連結\",\n    \"removeLink\": \"移除連結\",\n    \"editLink\": \"編輯連結\",\n    \"convertTo\": \"轉換為\",\n    \"linkText\": \"文字\",\n    \"linkTextHint\": \"請輸入文字\",\n    \"linkAddressHint\": \"請輸入網址\",\n    \"highlightColor\": \"醒目提示色\",\n    \"clearHighlightColor\": \"清除醒目提示色\",\n    \"customColor\": \"自訂顏色\",\n    \"hexValue\": \"十六進位值\",\n    \"opacity\": \"不透明度\",\n    \"resetToDefaultColor\": \"重設為預設值顏色\",\n    \"ltr\": \"由左至右\",\n    \"rtl\": \"由右至左\",\n    \"auto\": \"自動\",\n    \"cut\": \"剪下\",\n    \"copy\": \"複製\",\n    \"paste\": \"貼上\",\n    \"find\": \"尋找\",\n    \"select\": \"選擇\",\n    \"selectAll\": \"全選\",\n    \"previousMatch\": \"上一個符合\",\n    \"nextMatch\": \"下一個符合\",\n    \"closeFind\": \"關閉\",\n    \"replace\": \"取代\",\n    \"replaceAll\": \"全部取代\",\n    \"regex\": \"正則表達式\",\n    \"caseSensitive\": \"區分大小寫\",\n    \"uploadImage\": \"上傳圖片\",\n    \"urlImage\": \"網址圖片\",\n    \"incorrectLink\": \"連結錯誤\",\n    \"upload\": \"上傳\",\n    \"chooseImage\": \"選擇一張圖片\",\n    \"loading\": \"載入中\",\n    \"imageLoadFailed\": \"無法載入圖片\",\n    \"divider\": \"分隔線\",\n    \"table\": \"表格\",\n    \"colAddBefore\": \"新增前\",\n    \"rowAddBefore\": \"新增前\",\n    \"colAddAfter\": \"新增後\",\n    \"rowAddAfter\": \"在後新增後\",\n    \"colRemove\": \"移除\",\n    \"rowRemove\": \"移除\",\n    \"colDuplicate\": \"副本\",\n    \"rowDuplicate\": \"複製\",\n    \"colClear\": \"清除內容\",\n    \"rowClear\": \"清除內容\",\n    \"slashPlaceHolder\": \"輸入 '/' 以插入區塊，或開始輸入\",\n    \"typeSomething\": \"輸入一些東西...\",\n    \"toggleListShortForm\": \"切換\",\n    \"quoteListShortForm\": \"引述\",\n    \"mathEquationShortForm\": \"公式\",\n    \"codeBlockShortForm\": \"程式碼\"\n  },\n  \"favorite\": {\n    \"noFavorite\": \"無最愛頁面\",\n    \"noFavoriteHintText\": \"向左滑動頁面以將其加入你的最愛\",\n    \"removeFromSidebar\": \"從側邊欄移除\",\n    \"addToSidebar\": \"釘選到側邊欄\"\n  },\n  \"cardDetails\": {\n    \"notesPlaceholder\": \"輸入 / 以插入區塊，或開始輸入\"\n  },\n  \"blockPlaceholders\": {\n    \"todoList\": \"待辦事項\",\n    \"bulletList\": \"清單\",\n    \"numberList\": \"清單\",\n    \"quote\": \"引述\",\n    \"heading\": \"標題 {}\"\n  },\n  \"titleBar\": {\n    \"pageIcon\": \"頁面圖示\",\n    \"language\": \"語言\",\n    \"font\": \"字型\",\n    \"actions\": \"動作\",\n    \"date\": \"日期\",\n    \"addField\": \"新增欄位\",\n    \"userIcon\": \"使用者圖示\"\n  },\n  \"noLogFiles\": \"沒有日誌檔案\",\n  \"newSettings\": {\n    \"myAccount\": {\n      \"title\": \"帳戶與應用程式\",\n      \"subtitle\": \"自訂您的個人資料、管理帳戶安全性、Open AI 金鑰或登入您的帳戶\",\n      \"profileLabel\": \"帳戶名稱和個人資料圖片\",\n      \"profileNamePlaceholder\": \"輸入你的名字\",\n      \"accountSecurity\": \"帳戶安全性\",\n      \"2FA\": \"兩步驟驗證\",\n      \"aiKeys\": \"AI 金鑰\",\n      \"accountLogin\": \"帳戶登入\",\n      \"updateNameError\": \"名稱更新失敗\",\n      \"updateIconError\": \"個人頭像更新失敗\",\n      \"aboutAppFlowy\": \"關於 @:appName\",\n      \"deleteAccount\": {\n        \"title\": \"刪除帳號\",\n        \"subtitle\": \"永久刪除您的帳戶和所有資料\",\n        \"description\": \"永久刪除您的帳戶並從所有工作區移除存取權。\",\n        \"deleteMyAccount\": \"刪除我的帳戶\",\n        \"dialogTitle\": \"刪除帳戶\",\n        \"dialogContent1\": \"確定要永久刪除您的帳戶\",\n        \"dialogContent2\": \"此操作無法撤銷，此操作將刪除所有團隊空間的存取權限，刪除您的整個帳戶（包括私人工作區），並將您從所有共用工作區中刪除\",\n        \"confirmHint1\": \"請輸入「@:newSettings.myAccount.deleteAccount.confirmHint3」以確認。\",\n        \"confirmHint2\": \"我了解此動作不可逆轉，且將永久刪除我的帳戶及所有相關資料。\",\n        \"confirmHint3\": \"刪除我的帳戶\",\n        \"checkToConfirmError\": \"您必須勾選方塊以確認刪除。\",\n        \"failedToGetCurrentUser\": \"取得目前使用者電子郵件失敗\",\n        \"confirmTextValidationFailed\": \"您的確認文字不符合\\n\\\"@:newSettings.myAccount.deleteAccount.confirmHint3\\\"\",\n        \"deleteAccountSuccess\": \"帳戶已成功刪除\"\n      },\n      \"password\": {\n        \"title\": \"密碼\",\n        \"confirmPassword\": \"確認密碼\",\n        \"changePassword\": \"變更密碼\",\n        \"currentPassword\": \"目前密碼\",\n        \"newPassword\": \"新密碼\",\n        \"confirmNewPassword\": \"確認新密碼\",\n        \"setupPassword\": \"設定密碼\",\n        \"error\": {\n          \"currentPasswordIsRequired\": \"需要目前的密碼\",\n          \"newPasswordIsRequired\": \"需要新密碼\",\n          \"confirmPasswordIsRequired\": \"確認需要輸入密碼\",\n          \"passwordsDoNotMatch\": \"密碼不相符\",\n          \"newPasswordIsSameAsCurrent\": \"新密碼與目前密碼相同\",\n          \"currentPasswordIsIncorrect\": \"目前的密碼錯誤\",\n          \"passwordShouldBeAtLeast6Characters\": \"密碼至少應有 {min} 個字元\",\n          \"passwordCannotBeLongerThan72Characters\": \"密碼不能長於 {max} 個字元\"\n        },\n        \"toast\": {\n          \"passwordUpdatedSuccessfully\": \"密碼已成功更新\",\n          \"passwordUpdatedFailed\": \"密碼更新失敗\",\n          \"passwordSetupSuccessfully\": \"密碼設定成功\",\n          \"passwordSetupFailed\": \"密碼設定失敗\"\n        },\n        \"hint\": {\n          \"enterYourPassword\": \"輸入您的密碼\",\n          \"confirmYourPassword\": \"確認您的密碼\",\n          \"enterYourCurrentPassword\": \"輸入您的目前密碼\",\n          \"enterYourNewPassword\": \"輸入您的新密碼\",\n          \"confirmYourNewPassword\": \"確認您的新密碼\"\n        }\n      },\n      \"myAccount\": \"我的帳戶\",\n      \"myProfile\": \"我的個人資料\"\n    },\n    \"workplace\": {\n      \"name\": \"工作區\",\n      \"title\": \"工作區設定\",\n      \"subtitle\": \"自訂您的工作區外觀、主題、字體、文字佈局、日期、時間和語言\",\n      \"workplaceName\": \"工作區名稱\",\n      \"workplaceNamePlaceholder\": \"輸入工作區名稱\",\n      \"workplaceIcon\": \"工作區圖標\",\n      \"workplaceIconSubtitle\": \"為您的工作區上傳圖像或表情符號。圖示將顯示在您的側邊欄和通知中\",\n      \"renameError\": \"工作區重新命名失敗\",\n      \"updateIconError\": \"更新圖像失敗\",\n      \"chooseAnIcon\": \"選擇一個圖示\",\n      \"appearance\": {\n        \"name\": \"外觀\",\n        \"themeMode\": {\n          \"auto\": \"自動\",\n          \"light\": \"淺色\",\n          \"dark\": \"深色\"\n        },\n        \"language\": \"語言\"\n      }\n    },\n    \"syncState\": {\n      \"syncing\": \"同步中\",\n      \"synced\": \"已同步\",\n      \"noNetworkConnected\": \"沒有連線網絡\"\n    }\n  },\n  \"pageStyle\": {\n    \"title\": \"頁面樣式\",\n    \"layout\": \"版面配置\",\n    \"coverImage\": \"封面圖片\",\n    \"pageIcon\": \"頁面圖片\",\n    \"colors\": \"顏色\",\n    \"gradient\": \"漸變\",\n    \"backgroundImage\": \"背景圖片\",\n    \"presets\": \"預設\",\n    \"photo\": \"圖片\",\n    \"unsplash\": \"Unsplash\",\n    \"pageCover\": \"頁面封面\",\n    \"none\": \"無\",\n    \"openSettings\": \"打開設定\",\n    \"photoPermissionTitle\": \"@:appName 希望存取您的圖片庫\",\n    \"photoPermissionDescription\": \"允許存取圖片庫以上傳圖片\",\n    \"cameraPermissionTitle\": \"@:appName 想要存取您的攝影機\",\n    \"cameraPermissionDescription\": \"@:appName 需要存取您的相機，才能讓您從相機新增圖片到您的文件。\",\n    \"doNotAllow\": \"不允許\",\n    \"image\": \"圖像\"\n  },\n  \"commandPalette\": {\n    \"placeholder\": \"搜尋或提出問題…\",\n    \"bestMatches\": \"最佳匹配\",\n    \"aiOverview\": \"AI 概覽\",\n    \"aiOverviewSource\": \"參考來源\",\n    \"aiOverviewMoreDetails\": \"更多細節\",\n    \"aiAskFollowUp\": \"詢問後續問題\",\n    \"pagePreview\": \"內容預覽\",\n    \"clickToOpenPage\": \"點擊以開啟頁面\",\n    \"recentHistory\": \"最近歷史\",\n    \"navigateHint\": \"用於導航\",\n    \"loadingTooltip\": \"我們正在尋找結果...\",\n    \"betaLabel\": \"BETA\",\n    \"betaTooltip\": \"目前我們只支援搜尋頁面\",\n    \"fromTrashHint\": \"從垃圾桶\",\n    \"noResultsHint\": \"我們找不到您要搜尋的內容，請嘗試搜尋另一個詞語。\",\n    \"clearSearchTooltip\": \"清除輸入\",\n    \"location\": \"位置\",\n    \"created\": \"已建立\",\n    \"edited\": \"已編輯\"\n  },\n  \"space\": {\n    \"delete\": \"刪除\",\n    \"deleteConfirmation\": \"刪除:\",\n    \"deleteConfirmationDescription\": \"這個空間中的所有頁面都將被刪除並移至垃圾桶，任何已發布的頁面都會取消發布。\",\n    \"rename\": \"重新命名空間\",\n    \"changeIcon\": \"變更圖示\",\n    \"manage\": \"管理空間\",\n    \"addNewSpace\": \"創建空間\",\n    \"collapseAllSubPages\": \"折疊所有子頁面\",\n    \"createNewSpace\": \"創建新空間\",\n    \"createSpaceDescription\": \"建立多個公開與私人空間，以更好地整理您的工作。\",\n    \"spaceName\": \"空間名稱\",\n    \"spaceNamePlaceholder\": \"例如行銷、工程、人力資源\",\n    \"permission\": \"空間許可\",\n    \"publicPermission\": \"民眾\",\n    \"publicPermissionDescription\": \"所有具有完全存取權的工作區成員\",\n    \"privatePermission\": \"私人\",\n    \"privatePermissionDescription\": \"只有您可以訪問此空間\",\n    \"spaceIconBackground\": \"背景色\",\n    \"spaceIcon\": \"圖示\",\n    \"dangerZone\": \"危險區域\",\n    \"unableToDeleteLastSpace\": \"無法刪除最後一個空間\",\n    \"unableToDeleteSpaceNotCreatedByYou\": \"無法刪除他人建立的工作區\",\n    \"enableSpacesForYourWorkspace\": \"啟用工作區的 Spaces\",\n    \"title\": \"空間\",\n    \"defaultSpaceName\": \"一般\",\n    \"upgradeSpaceTitle\": \"啟用空間\",\n    \"upgradeSpaceDescription\": \"建立多個公開和私人空間，以更好地組織您的工作區。\",\n    \"upgrade\": \"更新\",\n    \"upgradeYourSpace\": \"創建多個空間\",\n    \"quicklySwitch\": \"快速切換到下一個空間\",\n    \"duplicate\": \"副本空間\",\n    \"movePageToSpace\": \"將頁面移動到空間\",\n    \"cannotMovePageToDatabase\": \"無法將頁面移至資料庫\",\n    \"switchSpace\": \"切換空間\",\n    \"spaceNameCannotBeEmpty\": \"空間名稱不能為空\",\n    \"success\": {\n      \"deleteSpace\": \"空間刪除成功\",\n      \"renameSpace\": \"已成功重新命名空間\",\n      \"duplicateSpace\": \"已成功複製空間\",\n      \"updateSpace\": \"已成功更新空間\"\n    },\n    \"error\": {\n      \"deleteSpace\": \"刪除空間失敗\",\n      \"renameSpace\": \"重新命名空間失敗\",\n      \"duplicateSpace\": \"複製空間失敗\",\n      \"updateSpace\": \"更新空間失敗\"\n    },\n    \"createSpace\": \"創造空間\",\n    \"manageSpace\": \"管理空間\",\n    \"renameSpace\": \"重新命名空間\",\n    \"mSpaceIconColor\": \"空間圖示顏色\",\n    \"mSpaceIcon\": \"空間圖示\"\n  },\n  \"publish\": {\n    \"hasNotBeenPublished\": \"此頁面尚未發布。\",\n    \"spaceHasNotBeenPublished\": \"目前尚不支援發布工作區\",\n    \"reportPage\": \"報告頁面\",\n    \"databaseHasNotBeenPublished\": \"目前尚不支援發布資料庫。\",\n    \"createdWith\": \"創建於\",\n    \"downloadApp\": \"下載 AppFlowy\",\n    \"copy\": {\n      \"codeBlock\": \"程式碼區塊的內容已複製到剪貼簿\",\n      \"imageBlock\": \"圖片連結已複製到剪貼簿\",\n      \"mathBlock\": \"數學方程式已複製到剪貼簿\",\n      \"fileBlock\": \"檔案連結已複製到剪貼簿\"\n    },\n    \"containsPublishedPage\": \"此頁面包含一個或多個已發佈的頁面。如果您繼續，它們將會被取消發布。您要繼續刪除嗎？\",\n    \"publishSuccessfully\": \"發布成功\",\n    \"unpublishSuccessfully\": \"取消發布成功\",\n    \"publishFailed\": \"發布失敗\",\n    \"unpublishFailed\": \"取消發布失敗\",\n    \"noAccessToVisit\": \"無法存取此頁面...\",\n    \"createWithAppFlowy\": \"使用 AppFlowy 建立網站\",\n    \"fastWithAI\": \"使用 AI 快速且輕鬆。\",\n    \"tryItNow\": \"立即嘗試\",\n    \"onlyGridViewCanBePublished\": \"僅限網格檢視可發布\",\n    \"database\": {\n      \"zero\": \"發布所選取的 {} 檢視\",\n      \"one\": \"發布所選取的 {} 多個檢視\",\n      \"many\": \"發布所選取的 {} 多個檢視\",\n      \"other\": \"發布所選取的 {} 多個檢視\"\n    },\n    \"mustSelectPrimaryDatabase\": \"必須選擇主要檢視\",\n    \"noDatabaseSelected\": \"未選取資料庫，請至少選取一個資料庫。\",\n    \"unableToDeselectPrimaryDatabase\": \"無法取消選擇主要資料庫\",\n    \"saveThisPage\": \"使用此範本開始\",\n    \"duplicateTitle\": \"您想將其新增到哪裡?\",\n    \"selectWorkspace\": \"選擇工作區\",\n    \"addTo\": \"添加\",\n    \"duplicateSuccessfully\": \"已新增至您的工作區\",\n    \"duplicateSuccessfullyDescription\": \"尚未安裝AppFlowy？點擊「下載」後，下載將自動開始。\",\n    \"downloadIt\": \"下載\",\n    \"openApp\": \"在應用程式中打開\",\n    \"duplicateFailed\": \"副本失敗\",\n    \"membersCount\": {\n      \"zero\": \"沒有成員\",\n      \"one\": \"1名成員\",\n      \"many\": \"{count} 名成員\",\n      \"other\": \"{count} 名成員\"\n    },\n    \"useThisTemplate\": \"使用範本\"\n  },\n  \"web\": {\n    \"continue\": \"繼續\",\n    \"or\": \"或\",\n    \"continueWithGoogle\": \"繼續使用 Google\",\n    \"continueWithGithub\": \"繼續使用 GitHub\",\n    \"continueWithDiscord\": \"繼續使用 Discord\",\n    \"continueWithApple\": \"繼續使用 Apple \",\n    \"moreOptions\": \"更多選項\",\n    \"collapse\": \"摺疊\",\n    \"signInAgreement\": \"點擊上方「繼續」，即表示您同意 AppFlowy 的…\",\n    \"signInLocalAgreement\": \"點擊上方「開始」即表示您同意 AppFlowy 的…\",\n    \"and\": \"和\",\n    \"termOfUse\": \"條款\",\n    \"privacyPolicy\": \"隱私權政策\",\n    \"signInError\": \"登入錯誤\",\n    \"login\": \"註冊或登入\",\n    \"fileBlock\": {\n      \"uploadedAt\": \"已上傳於 {time}\",\n      \"linkedAt\": \"連結已添加於 {time}\",\n      \"empty\": \"上傳或嵌入檔案\",\n      \"uploadFailed\": \"上傳失敗，請重試\",\n      \"retry\": \"重試\"\n    },\n    \"importNotion\": \"從 Notion 匯入\",\n    \"import\": \"匯入\",\n    \"importSuccess\": \"上傳成功\",\n    \"importSuccessMessage\": \"匯入完成時，我們會通知您。之後，您可以在側邊欄中查看您的匯入頁面。\",\n    \"importFailed\": \"匯入失敗，請檢查檔案格式。\",\n    \"dropNotionFile\": \"在此處拖放您的Notion zip 檔案以上傳，或點擊瀏覽。\",\n    \"error\": {\n      \"pageNameIsEmpty\": \"頁面名稱為空，請嘗試另一個。\"\n    }\n  },\n  \"globalComment\": {\n    \"comments\": \"評論\",\n    \"addComment\": \"新增評論\",\n    \"reactedBy\": \"反應由\",\n    \"addReaction\": \"新增反應\",\n    \"reactedByMore\": \"和 {count} 其他人\",\n    \"showSeconds\": {\n      \"one\": \"1 秒前\",\n      \"other\": \"{count} 秒前\",\n      \"zero\": \"剛剛\",\n      \"many\": \"{count} 秒前\"\n    },\n    \"showMinutes\": {\n      \"one\": \"1 分鐘前\",\n      \"other\": \"{count} 分鐘前\",\n      \"many\": \"{count} 分鐘前\"\n    },\n    \"showHours\": {\n      \"one\": \"1 小時前\",\n      \"other\": \"{count} 小時前\",\n      \"many\": \"{count} 小時前\"\n    },\n    \"showDays\": {\n      \"one\": \"1 天前\",\n      \"other\": \"{count} 天前\",\n      \"many\": \"{count} 天前\"\n    },\n    \"showMonths\": {\n      \"one\": \"1 個月前\",\n      \"other\": \"{count} 個月前\",\n      \"many\": \"{count} 個月前\"\n    },\n    \"showYears\": {\n      \"one\": \"1 年前\",\n      \"other\": \"{count} 年前\",\n      \"many\": \"{count} 年前\"\n    },\n    \"reply\": \"回覆\",\n    \"deleteComment\": \"刪除評論\",\n    \"youAreNotOwner\": \"您不是此評論的所有者。\",\n    \"confirmDeleteDescription\": \"您確定要刪除這則評論嗎?\",\n    \"hasBeenDeleted\": \"已刪除\",\n    \"replyingTo\": \"回復給\",\n    \"noAccessDeleteComment\": \"您沒有權限刪除此評論。\",\n    \"collapse\": \"摺疊\",\n    \"readMore\": \"閱讀更多\",\n    \"failedToAddComment\": \"新增評論失敗\",\n    \"commentAddedSuccessfully\": \"評論已成功新增。\",\n    \"commentAddedSuccessTip\": \"您剛才新增或回覆了一則評論。是否要跳至頂端，以查看最新的評論?\"\n  },\n  \"template\": {\n    \"asTemplate\": \"另存為範本\",\n    \"name\": \"範本名稱\",\n    \"description\": \"範本說明\",\n    \"about\": \"關於範本\",\n    \"deleteFromTemplate\": \"從範本中刪除\",\n    \"preview\": \"範本預覽\",\n    \"categories\": \"範本類別\",\n    \"isNewTemplate\": \"釘選到新範本\",\n    \"featured\": \"釘選到精選區\",\n    \"relatedTemplates\": \"相關範本\",\n    \"requiredField\": \"{field} 必填\",\n    \"addCategory\": \"新增 “{category}”\",\n    \"addNewCategory\": \"新增類別\",\n    \"addNewCreator\": \"新增創作者\",\n    \"deleteCategory\": \"刪除類別\",\n    \"editCategory\": \"編輯類別\",\n    \"editCreator\": \"編輯創建者\",\n    \"category\": {\n      \"name\": \"類別名稱\",\n      \"icon\": \"類別圖示\",\n      \"bgColor\": \"類別背景顏色\",\n      \"priority\": \"類別優先級\",\n      \"desc\": \"類別描述\",\n      \"type\": \"類別類型\",\n      \"icons\": \"類別圖示\",\n      \"colors\": \"類別顏色\",\n      \"byUseCase\": \"依使用案例\",\n      \"byFeature\": \"按功能\",\n      \"deleteCategory\": \"刪除類別\",\n      \"deleteCategoryDescription\": \"您確定要刪除此類別嗎?\",\n      \"typeToSearch\": \"在搜尋類別時輸入...\"\n    },\n    \"creator\": {\n      \"label\": \"範本創作者\",\n      \"name\": \"創作者姓名\",\n      \"avatar\": \"創作者頭像\",\n      \"accountLinks\": \"創作者帳戶連結\",\n      \"uploadAvatar\": \"點擊上傳頭像\",\n      \"deleteCreator\": \"刪除創建者\",\n      \"deleteCreatorDescription\": \"您確定要刪除此創作者嗎?\",\n      \"typeToSearch\": \"輸入以搜尋創作者...\"\n    },\n    \"uploadSuccess\": \"範本已成功上傳\",\n    \"uploadSuccessDescription\": \"您的範本已成功上傳。您現在可以在範本庫中檢視它。\",\n    \"viewTemplate\": \"查看範本\",\n    \"deleteTemplate\": \"刪除範本\",\n    \"deleteSuccess\": \"範本刪除成功\",\n    \"deleteTemplateDescription\": \"這不會影響目前頁面或發布狀態。您確定要刪除此範本嗎？\",\n    \"addRelatedTemplate\": \"新增相關範本\",\n    \"removeRelatedTemplate\": \"刪除相關範本\",\n    \"uploadAvatar\": \"上傳頭像\",\n    \"searchInCategory\": \"在 {category} 中搜尋\",\n    \"label\": \"範本\"\n  },\n  \"fileDropzone\": {\n    \"dropFile\": \"點擊或拖曳檔案到此區域以上傳\",\n    \"uploading\": \"正在上傳...\",\n    \"uploadFailed\": \"上傳失敗\",\n    \"uploadSuccess\": \"上傳成功\",\n    \"uploadSuccessDescription\": \"檔案已成功上傳\",\n    \"uploadFailedDescription\": \"檔案上傳失敗\",\n    \"uploadingDescription\": \"檔案正在上傳\"\n  },\n  \"gallery\": {\n    \"preview\": \"全螢幕打開\",\n    \"copy\": \"複製\",\n    \"download\": \"下載\",\n    \"prev\": \"上一個\",\n    \"next\": \"下一個\",\n    \"resetZoom\": \"重設縮放\",\n    \"zoomIn\": \"放大\",\n    \"zoomOut\": \"縮小\"\n  },\n  \"invitation\": {\n    \"join\": \"加入\",\n    \"on\": \"在\",\n    \"invitedBy\": \"受邀者\",\n    \"membersCount\": {\n      \"zero\": \"{count} 名成員\",\n      \"one\": \"{count} 名成員\",\n      \"many\": \"{count} 名成員\",\n      \"other\": \"{count} 名成員\"\n    },\n    \"tip\": \"您已收到邀請，使用下方聯絡資訊加入這個工作區。如果資訊有誤，請聯繫您的管理員重新發送邀請。\",\n    \"joinWorkspace\": \"加入工作區\",\n    \"success\": \"您已成功加入工作區\",\n    \"successMessage\": \"現在您可以存取其中的所有頁面和工作區。\",\n    \"openWorkspace\": \"開啟 AppFlowy\",\n    \"alreadyAccepted\": \"您已經接受邀請\",\n    \"errorModal\": {\n      \"title\": \"發生錯誤\",\n      \"description\": \"您目前的帳戶 {email} 可能沒有權限存取這個工作區。請使用正確的帳戶登入，或聯絡工作區擁有者尋求協助。\",\n      \"contactOwner\": \"聯絡擁有者\",\n      \"close\": \"返回首頁\",\n      \"changeAccount\": \"更改帳戶\"\n    }\n  },\n  \"requestAccess\": {\n    \"title\": \"沒有此頁面的存取權\",\n    \"subtitle\": \"您可以向此頁面的擁有者請求存取權。獲得批准後，您就可以查看該頁面。\",\n    \"requestAccess\": \"要求存取\",\n    \"backToHome\": \"返回首頁\",\n    \"tip\": \"您目前以 <link/> 身分登入。\",\n    \"mightBe\": \"您可能需要使用不同的帳戶 <login/>\",\n    \"successful\": \"請求已成功發送\",\n    \"successfulMessage\": \"當擁有者批准您的請求時，您會收到通知。\",\n    \"requestError\": \"請求存取權失敗\",\n    \"repeatRequestError\": \"您已經要求存取此頁面\"\n  },\n  \"approveAccess\": {\n    \"title\": \"批准工作區加入請求\",\n    \"requestSummary\": \"<user/> 請求加入 <workspace/> 並存取 <page/>\",\n    \"upgrade\": \"升級\",\n    \"downloadApp\": \"下載 AppFlowy\",\n    \"approveButton\": \"核准\",\n    \"approveSuccess\": \"已成功批准\",\n    \"approveError\": \"批准失敗，請確認工作區方案限制是否已達上限。\",\n    \"getRequestInfoError\": \"取得請求資訊失敗\",\n    \"memberCount\": {\n      \"zero\": \"沒有成員\",\n      \"one\": \"1 名成員\",\n      \"many\": \"{count} 名成員\",\n      \"other\": \"{count} 名成員\"\n    },\n    \"alreadyProTitle\": \"您已達到工作區方案限制\",\n    \"alreadyProMessage\": \"請他們聯繫 <email/>> 以解鎖更多成員。\",\n    \"repeatApproveError\": \"您已經批准此請求\",\n    \"ensurePlanLimit\": \"請確認工作區方案限制是否已達上限。如果已達上限，請考慮 <upgrade/> 工作區方案或 <ownload/>。\",\n    \"requestToJoin\": \"已請求加入\",\n    \"asMember\": \"作為會員\"\n  },\n  \"upgradePlanModal\": {\n    \"title\": \"升級到專業版\",\n    \"message\": \"{name} 已達到免費成員上限。升級至專業版以邀請更多成員。\",\n    \"upgradeSteps\": \"如何在 AppFlowy 上升級您的方案:\",\n    \"step1\": \"1. 前往設定\",\n    \"step2\": \"2. 按一下「方案」\",\n    \"step3\": \"3. 選擇「變更方案」\",\n    \"appNote\": \"注意:\",\n    \"actionButton\": \"升級\",\n    \"downloadLink\": \"下載應用程式\",\n    \"laterButton\": \"之後\",\n    \"refreshNote\": \"成功升級後，請點擊 <refresh/> 以啟用您的新功能。\",\n    \"refresh\": \"這裡\"\n  },\n  \"breadcrumbs\": {\n    \"label\": \"麵包屑導航\"\n  },\n  \"time\": {\n    \"justNow\": \"剛剛\",\n    \"seconds\": {\n      \"one\": \"1 秒\",\n      \"other\": \"{count} 秒\"\n    },\n    \"minutes\": {\n      \"one\": \"1 分鐘\",\n      \"other\": \"{count} 分鐘\"\n    },\n    \"hours\": {\n      \"one\": \"1 小時\",\n      \"other\": \"{count} 小時\"\n    },\n    \"days\": {\n      \"one\": \"1 天\",\n      \"other\": \"{count} 天\"\n    },\n    \"weeks\": {\n      \"one\": \"1 週\",\n      \"other\": \"{count} 週\"\n    },\n    \"months\": {\n      \"one\": \"1 個月\",\n      \"other\": \"{count} 個月\"\n    },\n    \"years\": {\n      \"one\": \"1 年\",\n      \"other\": \"{count} 年\"\n    },\n    \"ago\": \"前\",\n    \"yesterday\": \"昨天\",\n    \"today\": \"今天\"\n  },\n  \"members\": {\n    \"zero\": \"沒有成員\",\n    \"one\": \"1 名成員\",\n    \"many\": \"{count} 名成員\",\n    \"other\": \"{count} 名成員\"\n  },\n  \"tabMenu\": {\n    \"close\": \"關閉\",\n    \"closeDisabledHint\": \"無法關閉釘選的分頁，請先取消釘選\",\n    \"closeOthers\": \"關閉其他分頁\",\n    \"closeOthersHint\": \"這將關閉所有未釘選的分頁，除了這個\",\n    \"closeOthersDisabledHint\": \"所有分頁都已釘選，找不到要關閉的分頁\",\n    \"favorite\": \"最愛\",\n    \"unfavorite\": \"取消最愛\",\n    \"favoriteDisabledHint\": \"無法將此檢視設為最愛\",\n    \"pinTab\": \"釘選\",\n    \"unpinTab\": \"取消釘選\"\n  },\n  \"openFileMessage\": {\n    \"success\": \"檔案已成功開啟\",\n    \"fileNotFound\": \"未找到檔案\",\n    \"noAppToOpenFile\": \"沒有應用程式可以開啟這個檔案\",\n    \"permissionDenied\": \"沒有權限開啟此檔案\",\n    \"unknownError\": \"檔案開啟失敗\"\n  },\n  \"inviteMember\": {\n    \"requestInviteMembers\": \"邀請加入您的工作區\",\n    \"inviteFailedMemberLimit\": \"成員限制已達到，請...\",\n    \"upgrade\": \"升級\",\n    \"addEmail\": \"email@example.com, email2@example.com...\",\n    \"requestInvites\": \"發送邀請\",\n    \"inviteAlready\": \"您已經邀請了這個電子郵件: {email}\",\n    \"inviteSuccess\": \"邀請已成功發送\",\n    \"description\": \"在下方輸入電子郵件地址，以逗號分隔。費用將根據成員數量計算。\",\n    \"emails\": \"電子郵件\"\n  },\n  \"quickNote\": {\n    \"label\": \"快速筆記\",\n    \"quickNotes\": \"快速筆記\",\n    \"search\": \"搜尋快速筆記\",\n    \"collapseFullView\": \"摺疊全螢幕檢視\",\n    \"expandFullView\": \"展開全螢幕檢視\",\n    \"createFailed\": \"建立快速筆記失敗\",\n    \"quickNotesEmpty\": \"沒有快速筆記\",\n    \"emptyNote\": \"空筆記\",\n    \"deleteNotePrompt\": \"所選筆記將會永久刪除。您確定要刪除嗎？\",\n    \"addNote\": \"新筆記\",\n    \"noAdditionalText\": \"沒有額外文字\"\n  },\n  \"subscribe\": {\n    \"upgradePlanTitle\": \"比較並選擇計劃\",\n    \"yearly\": \"每年\",\n    \"save\": \"節省 {discount}%。\",\n    \"monthly\": \"每月\",\n    \"priceIn\": \"價格\",\n    \"free\": \"免費\",\n    \"pro\": \"專業版\",\n    \"freeDescription\": \"適合最多 2 名成員，用來整理所有事務。\",\n    \"proDescription\": \"適合小型團隊，用來管理專案與團隊知識\",\n    \"proDuration\": {\n      \"monthly\": \"每位成員每月\\n按月計費\",\n      \"yearly\": \"每位成員每月\\n年度計費\"\n    },\n    \"cancel\": \"降級\",\n    \"changePlan\": \"升級到專業計劃\",\n    \"everythingInFree\": \"所有內容免費+\",\n    \"currentPlan\": \"目前\",\n    \"freeDuration\": \"永遠\",\n    \"freePoints\": {\n      \"first\": \"1 個協作工作區，最多 2 名成員\",\n      \"second\": \"無限頁面和區塊\",\n      \"three\": \"5GB 儲存空間\",\n      \"four\": \"智慧搜尋\",\n      \"five\": \"20 次 AI 回應\",\n      \"six\": \"行動應用程式\",\n      \"seven\": \"即時協作\"\n    },\n    \"proPoints\": {\n      \"first\": \"無限儲存空間\",\n      \"second\": \"最多 10 個工作區成員\",\n      \"three\": \"無限的 AI 回應\",\n      \"four\": \"無限檔案上傳\",\n      \"five\": \"自定義命名空間\"\n    },\n    \"cancelPlan\": {\n      \"title\": \"很遺憾看到您離開\",\n      \"success\": \"您的訂閱已成功取消\",\n      \"description\": \"很遺憾看到您離開。我們非常希望聽到您的回饋，以幫助我們改善 AppFlowy。請花一點時間回答幾個問題。\",\n      \"commonOther\": \"其他\",\n      \"otherHint\": \"在此處輸入您的答案\",\n      \"questionOne\": {\n        \"question\": \"是什麼促使你取消你的 AppFlowy Pro 訂閱？\",\n        \"answerOne\": \"成本太高\",\n        \"answerTwo\": \"功能未達預期\",\n        \"answerThree\": \"找到更好的替代方案\",\n        \"answerFour\": \"使用量不足以證明費用合理\",\n        \"answerFive\": \"服務問題或技術困難\"\n      },\n      \"questionTwo\": {\n        \"question\": \"未來您幾何可能考慮重新訂閱?\",\n        \"answerOne\": \"非常有可能\",\n        \"answerTwo\": \"相當有可能\",\n        \"answerThree\": \"不確定\",\n        \"answerFour\": \"不太可能\",\n        \"answerFive\": \"非常不可能\"\n      },\n      \"questionThree\": {\n        \"question\": \"您在訂閱期間最重視哪個專業版功能？\",\n        \"answerOne\": \"多人協作\",\n        \"answerTwo\": \"更長的歷史版本時間\",\n        \"answerThree\": \"無限的 AI 回應\",\n        \"answerFour\": \"存取本地 AI 模型\"\n      },\n      \"questionFour\": {\n        \"question\": \"您如何描述您使用 AppFlowy 的整體體驗？\",\n        \"answerOne\": \"很棒\",\n        \"answerTwo\": \"好的\",\n        \"answerThree\": \"平均\",\n        \"answerFour\": \"低於平均水平\",\n        \"answerFive\": \"不滿意\"\n      }\n    }\n  },\n  \"ai\": {\n    \"contentPolicyViolation\": \"圖片生成因敏感內容而失敗。請重新措辭您的輸入並再次嘗試。\",\n    \"textLimitReachedDescription\": \"您的工作區已用完免費的 AI 回應。升級至專業版或購買 AI 附加組件以解鎖無限回應\",\n    \"imageLimitReachedDescription\": \"您已用完免費的 AI 圖片配額。請升級至專業版或購買 AI 附加組件以解鎖無限回應。\",\n    \"limitReachedAction\": {\n      \"textDescription\": \"您的工作區已用完免費的 AI 回應。要獲得更多回應，請…\",\n      \"imageDescription\": \"您已用完免費的 AI 圖片配額。請…\",\n      \"upgrade\": \"升級\",\n      \"toThe\": \"到\",\n      \"proPlan\": \"專業方案\",\n      \"orPurchaseAn\": \"或購買一個\",\n      \"aiAddon\": \"AI 插件\"\n    },\n    \"editing\": \"編輯中\",\n    \"analyzing\": \"分析中\",\n    \"continueWritingEmptyDocumentTitle\": \"繼續寫作錯誤\",\n    \"continueWritingEmptyDocumentDescription\": \"我們在擴展文件中內容時遇到困難。請寫一個簡短的介紹，然後我們可以從那裡開始！\",\n    \"more\": \"更多\",\n    \"customPrompt\": {\n      \"browsePrompts\": \"瀏覽提示\",\n      \"usePrompt\": \"使用提示\",\n      \"featured\": \"精選\",\n      \"custom\": \"自訂\",\n      \"customPrompt\": \"自訂提示\",\n      \"databasePrompts\": \"從您自己的資料庫載入提示\",\n      \"selectDatabase\": \"選擇資料庫\",\n      \"promptDatabase\": \"提示資料庫\",\n      \"configureDatabase\": \"配置資料庫\",\n      \"title\": \"標題\",\n      \"content\": \"內容\",\n      \"example\": \"範例\",\n      \"category\": \"類別\",\n      \"selectField\": \"選擇欄位\",\n      \"loading\": \"載入中\",\n      \"invalidDatabase\": \"無效資料庫\",\n      \"invalidDatabaseHelp\": \"請確保資料庫至少具有兩個文字屬性:\\n ◦ 一個用於提示名稱 ◦ 一個用於提示內容\\n您也可以選擇性地新增屬性以用於提示範例和類別。\",\n      \"noResults\": \"找不到任何提示\",\n      \"all\": \"全部\",\n      \"development\": \"開發\",\n      \"writing\": \"寫作\",\n      \"healthAndFitness\": \"健康與健身\",\n      \"business\": \"商業\",\n      \"marketing\": \"行銷\",\n      \"travel\": \"旅行\",\n      \"others\": \"其他\",\n      \"prompt\": \"提示\",\n      \"promptExample\": \"提示範例\",\n      \"sampleOutput\": \"範例輸出\",\n      \"contentSeo\": \"內容/搜尋引擎優化\",\n      \"emailMarketing\": \"電子郵件行銷\",\n      \"paidAds\": \"付費廣告\",\n      \"prCommunication\": \"公關/傳播\",\n      \"recruiting\": \"招募\",\n      \"sales\": \"銷售量\",\n      \"socialMedia\": \"社群媒體\",\n      \"strategy\": \"策略\",\n      \"caseStudies\": \"案例研究\",\n      \"salesCopy\": \"銷售副本\",\n      \"education\": \"教育\",\n      \"work\": \"工作\",\n      \"podcastProduction\": \"播客製作\",\n      \"copyWriting\": \"文案撰寫\",\n      \"customerSuccess\": \"客戶成功\"\n    }\n  },\n  \"autoUpdate\": {\n    \"criticalUpdateTitle\": \"需要更新才能繼續\",\n    \"criticalUpdateDescription\": \"我們已進行改進以提升您的體驗！請從 {currentVersion} 更新至 {newVersion} 以繼續使用應用程式。\",\n    \"criticalUpdateButton\": \"更新\",\n    \"bannerUpdateTitle\": \"新版本發布!\",\n    \"bannerUpdateDescription\": \"取得最新功能和修復。點擊「更新」即可立即安裝\",\n    \"bannerUpdateButton\": \"更新\",\n    \"settingsUpdateTitle\": \"版本 ({newVersion}) 可用!\",\n    \"settingsUpdateDescription\": \"目前版本: {currentVersion} (正式發布版)→ {newVersion}\",\n    \"settingsUpdateButton\": \"更新\",\n    \"settingsUpdateWhatsNew\": \"最新消息\"\n  },\n  \"lockPage\": {\n    \"lockPage\": \"已鎖定\",\n    \"reLockPage\": \"重新鎖定\",\n    \"lockTooltip\": \"頁面鎖定以防止意外編輯。點擊以解鎖。\",\n    \"pageLockedToast\": \"頁面已鎖定。除非有人將其解鎖，否則無法編輯。\",\n    \"lockedOperationTooltip\": \"頁面已鎖定以防止意外編輯。\"\n  },\n  \"suggestion\": {\n    \"accept\": \"接受\",\n    \"keep\": \"保留\",\n    \"discard\": \"丟棄\",\n    \"close\": \"關閉\",\n    \"tryAgain\": \"再試一次\",\n    \"rewrite\": \"重新撰寫\",\n    \"insertBelow\": \"在下面插入\"\n  },\n  \"shareTab\": {\n    \"accessLevel\": {\n      \"view\": \"檢視\",\n      \"comment\": \"評論\",\n      \"edit\": \"編輯\",\n      \"fullAccess\": \"完全存取權限\"\n    },\n    \"inviteByEmail\": \"透過電子郵件邀請\",\n    \"invite\": \"邀請\",\n    \"anyoneAtWorkspace\": \"{workspace} 中的任何人\",\n    \"anyoneInGroupWithLinkCanEdit\": \"擁有連結的群組中的任何人都可以編輯\",\n    \"copyLink\": \"複製連結\",\n    \"copiedLinkToClipboard\": \"已將連結複製到剪貼簿\",\n    \"removeAccess\": \"移除存取權\",\n    \"turnIntoMember\": \"轉為會員\",\n    \"you\": \"(你)\",\n    \"guest\": \"客人\",\n    \"onlyFullAccessCanInvite\": \"只有擁有完整存取權的使用者才能邀請其他人\",\n    \"invitationSent\": \"邀請已發送\",\n    \"emailAlreadyInList\": \"這封電子郵件已經在清單中\",\n    \"upgradeToProToInviteGuests\": \"請升級到專業方案以邀請更多訪客\",\n    \"maxGuestsReached\": \"您已達到最大訪客人數\",\n    \"removedGuestSuccessfully\": \"已成功移除訪客\",\n    \"updatedAccessLevelSuccessfully\": \"已成功更新存取層級\",\n    \"turnedIntoMemberSuccessfully\": \"已成功轉為會員\",\n    \"peopleAboveCanAccessWithLink\": \"以上人士可以使用連結存取\",\n    \"cantMakeChanges\": \"無法進行變更\",\n    \"canMakeAnyChanges\": \"可以進行任何變更\",\n    \"generalAccess\": \"一般存取權\",\n    \"peopleWithAccess\": \"擁有存取權的人員\",\n    \"peopleAboveCanAccessWithTheLink\": \"擁有連結的人士可以存取\",\n    \"upgrade\": \"升級\",\n    \"toProPlanToInviteGuests\": \"要邀請訪客到此頁面，需要升級至專業版\",\n    \"upgradeToInviteGuest\": {\n      \"title\": {\n        \"owner\": \"升級為邀請嘉賓\",\n        \"member\": \"升級為邀請嘉賓\",\n        \"guest\": \"升級為邀請嘉賓\"\n      },\n      \"description\": {\n        \"owner\": \"您的工作區目前使用的是免費方案。升級至專業版，以允許外部使用者作為訪客存取此頁面。\",\n        \"member\": \"部分受邀者不在您的工作區內。若要將他們邀請為訪客，請聯繫您的工作區擁有者以升級至專業版。\",\n        \"guest\": \"部分受邀者不在您的工作區內。若要將他們邀請為訪客，請聯繫您的工作區擁有者以升級至專業版。\"\n      }\n    }\n  },\n  \"shareSection\": {\n    \"shared\": \"與我分享\"\n  }\n}"
  },
  {
    "path": "frontend/rust-lib/.gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n\n# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries\n# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html\n\n# These are backup files generated by rustfmt\n**/*.rs.bk\n**/**/*.log*\n**/**/temp\nbin/\n**/src/protobuf\n**/resources/proto\n.idea/\nAppFlowy-Collab/\n.env\n.env.**\n**/unit_test**\njniLibs/\n**/pkg/"
  },
  {
    "path": "frontend/rust-lib/.vscode/launch.json",
    "content": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"AF-desktop: Debug Rust\",\n            \"type\": \"lldb\",\n            // \"request\": \"attach\",\n            // \"pid\": \"${command:pickMyProcess}\"\n            // To launch the application directly, use the following configuration:\n            \"request\": \"launch\",\n            \"program\": \"/Users/lucas.xu/Desktop/appflowy_backup/frontend/appflowy_flutter/build/macos/Build/Products/Debug/AppFlowy.app\",\n        },\n    ]\n  }\n"
  },
  {
    "path": "frontend/rust-lib/Cargo.toml",
    "content": "[workspace]\nmembers = [\n  \"lib-dispatch\",\n  \"lib-log\",\n  \"flowy-core\",\n  \"dart-ffi\",\n  \"flowy-user\",\n  \"flowy-user-pub\",\n  \"event-integration-test\",\n  \"flowy-sqlite\",\n  \"flowy-folder\",\n  \"flowy-folder-pub\",\n  \"flowy-notification\",\n  \"flowy-document\",\n  \"flowy-document-pub\",\n  \"flowy-error\",\n  \"flowy-database2\",\n  \"flowy-database-pub\",\n  \"flowy-server\",\n  \"flowy-server-pub\",\n  \"flowy-storage\",\n  \"collab-integrate\",\n  \"flowy-date\",\n  \"flowy-search\",\n  \"lib-infra\",\n  \"build-tool/flowy-ast\",\n  \"build-tool/flowy-codegen\",\n  \"build-tool/flowy-derive\",\n  \"flowy-search-pub\",\n  \"flowy-ai\",\n  \"flowy-ai-pub\",\n  \"flowy-storage-pub\",\n  \"flowy-sqlite-vec\",\n]\n\nresolver = \"2\"\n\n[workspace.dependencies]\nlib-dispatch = { path = \"lib-dispatch\" }\nlib-log = { path = \"lib-log\" }\nlib-infra = { path = \"lib-infra\" }\nflowy-ast = { path = \"build-tool/flowy-ast\" }\nflowy-codegen = { path = \"build-tool/flowy-codegen\" }\nflowy-derive = { path = \"build-tool/flowy-derive\" }\nflowy-core = { path = \"flowy-core\" }\ndart-ffi = { path = \"dart-ffi\" }\nflowy-user = { path = \"flowy-user\" }\nflowy-user-pub = { path = \"flowy-user-pub\" }\nflowy-sqlite = { path = \"flowy-sqlite\" }\nflowy-folder = { path = \"flowy-folder\" }\nflowy-folder-pub = { path = \"flowy-folder-pub\" }\nflowy-notification = { path = \"flowy-notification\" }\nflowy-document = { path = \"flowy-document\" }\nflowy-document-pub = { path = \"flowy-document-pub\" }\nflowy-error = { path = \"flowy-error\" }\nflowy-database2 = { path = \"flowy-database2\" }\nflowy-database-pub = { path = \"flowy-database-pub\" }\nflowy-server = { path = \"flowy-server\" }\nflowy-server-pub = { path = \"flowy-server-pub\" }\nflowy-storage = { path = \"flowy-storage\" }\nflowy-storage-pub = { path = \"flowy-storage-pub\" }\nflowy-search = { path = \"flowy-search\" }\nflowy-search-pub = { path = \"flowy-search-pub\" }\ncollab-integrate = { path = \"collab-integrate\" }\nflowy-date = { path = \"flowy-date\" }\nflowy-ai = { path = \"flowy-ai\" }\nflowy-ai-pub = { path = \"flowy-ai-pub\" }\nanyhow = \"1.0\"\narc-swap = \"1.7\"\ntracing = \"0.1.40\"\nbytes = \"1.5.0\"\nserde_json = \"1.0.108\"\nserde = \"1.0.194\"\nprotobuf = { version = \"2.28.0\" }\ndiesel = { version = \"2.1.0\", features = [\n  \"sqlite\",\n  \"chrono\",\n  \"r2d2\",\n  \"serde_json\",\n] }\ndiesel_derives = { version = \"2.1.0\", features = [\"sqlite\", \"r2d2\"] }\nuuid = { version = \"1.5.0\", features = [\"serde\", \"v4\", \"v5\"] }\nserde_repr = \"0.1\"\nfutures = \"0.3.31\"\ntokio = \"1.38.0\"\ntokio-stream = \"0.1.17\"\nollama-rs = { version = \"0.3.0\" }\nasync-trait = \"0.1.81\"\nchrono = { version = \"0.4.31\", default-features = false, features = [\"clock\"] }\ncollab = { version = \"0.2\" }\ncollab-entity = { version = \"0.2\" }\ncollab-folder = { version = \"0.2\" }\ncollab-document = { version = \"0.2\" }\ncollab-database = { version = \"0.2\" }\ncollab-plugins = { version = \"0.2\" }\ncollab-user = { version = \"0.2\" }\ncollab-importer = { version = \"0.1\" }\nyrs = \"0.21.0\"\nvalidator = { version = \"0.18\", features = [\"derive\"] }\ntokio-util = \"0.7.11\"\nzip = \"2.2.0\"\ndashmap = \"6.0.1\"\nderive_builder = \"0.20.2\"\ntantivy = { version = \"0.24.1\" }\nnum_enum = \"0.7.3\"\nflowy-sqlite-vec = { path = \"flowy-sqlite-vec\" }\n\n# Please using the following command to update the revision id\n# Current directory: frontend\n# Run the script.add_workspace_members:\n# scripts/tool/update_client_api_rev.sh  new_rev_id\n# ⚠️⚠️⚠️️\nclient-api = { git = \"https://github.com/AppFlowy-IO/AppFlowy-Cloud\", rev = \"592f644\" }\nclient-api-entity = { git = \"https://github.com/AppFlowy-IO/AppFlowy-Cloud\", rev = \"592f644\" }\nworkspace-template = { git = \"https://github.com/AppFlowy-IO/AppFlowy-Cloud\", rev = \"592f644\" }\n\n[profile.dev]\nopt-level = 0\nlto = false\ncodegen-units = 16\ndebug = true\n\n[profile.release]\nlto = true\nopt-level = 3\ncodegen-units = 1\n\n[profile.profiling]\ninherits = \"release\"\ndebug = true\ncodegen-units = 16\nlto = false\n\n#strip = \"debuginfo\"\nincremental = true\n\n[patch.crates-io]\n# We're using a specific commit here because rust-rocksdb doesn't publish the latest version that includes the memory alignment fix.\n# For more details, see https://github.com/rust-rocksdb/rust-rocksdb/pull/868\nrocksdb = { git = \"https://github.com/rust-rocksdb/rust-rocksdb\", rev = \"1710120e4549e04ba3baa6a1ee5a5a801fa45a72\" }\n# Please use the following script to update collab.\n# Working directory: frontend\n#\n# To update the commit ID, run:\n# scripts/tool/update_collab_rev.sh new_rev_id\n#\n# To switch to the local path, run:\n# scripts/tool/update_collab_source.sh\n# ⚠️⚠️⚠️️\ncollab = { version = \"0.2\", git = \"https://github.com/AppFlowy-IO/AppFlowy-Collab\", rev = \"4dfccef\" }\ncollab-entity = { version = \"0.2\", git = \"https://github.com/AppFlowy-IO/AppFlowy-Collab\", rev = \"4dfccef\" }\ncollab-folder = { version = \"0.2\", git = \"https://github.com/AppFlowy-IO/AppFlowy-Collab\", rev = \"4dfccef\" }\ncollab-document = { version = \"0.2\", git = \"https://github.com/AppFlowy-IO/AppFlowy-Collab\", rev = \"4dfccef\" }\ncollab-database = { version = \"0.2\", git = \"https://github.com/AppFlowy-IO/AppFlowy-Collab\", rev = \"4dfccef\" }\ncollab-plugins = { version = \"0.2\", git = \"https://github.com/AppFlowy-IO/AppFlowy-Collab\", rev = \"4dfccef\" }\ncollab-user = { version = \"0.2\", git = \"https://github.com/AppFlowy-IO/AppFlowy-Collab\", rev = \"4dfccef\" }\ncollab-importer = { version = \"0.1\", git = \"https://github.com/AppFlowy-IO/AppFlowy-Collab\", rev = \"4dfccef\" }\n\nlangchain-rust = { version = \"4.6.0\", git = \"https://github.com/appflowy/langchain-rust\", branch = \"af\" }\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-ast/Cargo.toml",
    "content": "[package]\nname = \"flowy-ast\"\nversion = \"0.1.0\"\nedition = \"2018\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nsyn = { version = \"1.0.109\", features = [\"extra-traits\", \"parsing\", \"derive\", \"full\"]}\nquote = \"1.0\"\nproc-macro2 = \"1.0\"\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-ast/src/ast.rs",
    "content": "#![allow(clippy::all)]\n#![allow(unused_attributes)]\n#![allow(unused_assignments)]\n\nuse crate::event_attrs::EventEnumAttrs;\nuse crate::node_attrs::NodeStructAttrs;\nuse crate::{\n  is_recognizable_field, ty_ext::*, ASTResult, PBAttrsContainer, PBStructAttrs, NODE_TYPE,\n};\nuse proc_macro2::Ident;\nuse syn::Meta::NameValue;\nuse syn::{self, punctuated::Punctuated};\n\npub struct ASTContainer<'a> {\n  /// The struct or enum name (without generics).\n  pub ident: syn::Ident,\n\n  pub node_type: Option<String>,\n  /// Attributes on the structure.\n  pub pb_attrs: PBAttrsContainer,\n  /// The contents of the struct or enum.\n  pub data: ASTData<'a>,\n}\n\nimpl<'a> ASTContainer<'a> {\n  pub fn from_ast(ast_result: &ASTResult, ast: &'a syn::DeriveInput) -> Option<ASTContainer<'a>> {\n    let attrs = PBAttrsContainer::from_ast(ast_result, ast);\n    // syn::DeriveInput\n    //  1. syn::DataUnion\n    //  2. syn::DataStruct\n    //  3. syn::DataEnum\n    let data = match &ast.data {\n      syn::Data::Struct(data) => {\n        // https://docs.rs/syn/1.0.48/syn/struct.DataStruct.html\n        let (style, fields) = struct_from_ast(ast_result, &data.fields);\n        ASTData::Struct(style, fields)\n      },\n      syn::Data::Union(_) => {\n        ast_result.error_spanned_by(ast, \"Does not support derive for unions\");\n        return None;\n      },\n      syn::Data::Enum(data) => {\n        // https://docs.rs/syn/1.0.48/syn/struct.DataEnum.html\n        ASTData::Enum(enum_from_ast(\n          ast_result,\n          &ast.ident,\n          &data.variants,\n          &ast.attrs,\n        ))\n      },\n    };\n\n    let ident = ast.ident.clone();\n    let node_type = get_node_type(ast_result, &ident, &ast.attrs);\n    let item = ASTContainer {\n      ident,\n      pb_attrs: attrs,\n      node_type,\n      data,\n    };\n    Some(item)\n  }\n}\n\npub enum ASTData<'a> {\n  Struct(ASTStyle, Vec<ASTField<'a>>),\n  Enum(Vec<ASTEnumVariant<'a>>),\n}\n\nimpl<'a> ASTData<'a> {\n  pub fn all_fields(&'a self) -> Box<dyn Iterator<Item = &'a ASTField<'a>> + 'a> {\n    match self {\n      ASTData::Enum(variants) => {\n        Box::new(variants.iter().flat_map(|variant| variant.fields.iter()))\n      },\n      ASTData::Struct(_, fields) => Box::new(fields.iter()),\n    }\n  }\n\n  pub fn all_variants(&'a self) -> Box<dyn Iterator<Item = &'a EventEnumAttrs> + 'a> {\n    match self {\n      ASTData::Enum(variants) => {\n        let iter = variants.iter().map(|variant| &variant.attrs);\n        Box::new(iter)\n      },\n      ASTData::Struct(_, fields) => {\n        let iter = fields.iter().flat_map(|_| None);\n        Box::new(iter)\n      },\n    }\n  }\n\n  pub fn all_idents(&'a self) -> Box<dyn Iterator<Item = &'a syn::Ident> + 'a> {\n    match self {\n      ASTData::Enum(variants) => Box::new(variants.iter().map(|v| &v.ident)),\n      ASTData::Struct(_, fields) => {\n        let iter = fields.iter().flat_map(|f| match &f.member {\n          syn::Member::Named(ident) => Some(ident),\n          _ => None,\n        });\n        Box::new(iter)\n      },\n    }\n  }\n}\n\n/// A variant of an enum.\npub struct ASTEnumVariant<'a> {\n  pub ident: syn::Ident,\n  pub attrs: EventEnumAttrs,\n  pub style: ASTStyle,\n  pub fields: Vec<ASTField<'a>>,\n  pub original: &'a syn::Variant,\n}\n\nimpl<'a> ASTEnumVariant<'a> {\n  pub fn name(&self) -> String {\n    self.ident.to_string()\n  }\n}\n\npub enum BracketCategory {\n  Other,\n  Opt,\n  Vec,\n  Map((String, String)),\n}\n\npub struct ASTField<'a> {\n  pub member: syn::Member,\n  pub pb_attrs: PBStructAttrs,\n  pub node_attrs: NodeStructAttrs,\n  pub ty: &'a syn::Type,\n  pub original: &'a syn::Field,\n  // If the field is Vec<String>, then the bracket_ty will be Vec\n  pub bracket_ty: Option<syn::Ident>,\n  // If the field is Vec<String>, then the bracket_inner_ty will be String\n  pub bracket_inner_ty: Option<syn::Ident>,\n  pub bracket_category: Option<BracketCategory>,\n}\n\nimpl<'a> ASTField<'a> {\n  pub fn new(cx: &ASTResult, field: &'a syn::Field, index: usize) -> Result<Self, String> {\n    let mut bracket_inner_ty = None;\n    let mut bracket_ty = None;\n    let mut bracket_category = Some(BracketCategory::Other);\n    match parse_ty(cx, &field.ty) {\n      Ok(Some(inner)) => {\n        match inner.primitive_ty {\n          PrimitiveTy::Map(map_info) => {\n            bracket_category = Some(BracketCategory::Map((map_info.key.clone(), map_info.value)))\n          },\n          PrimitiveTy::Vec => {\n            bracket_category = Some(BracketCategory::Vec);\n          },\n          PrimitiveTy::Opt => {\n            bracket_category = Some(BracketCategory::Opt);\n          },\n          PrimitiveTy::Other => {\n            bracket_category = Some(BracketCategory::Other);\n          },\n        }\n\n        match *inner.bracket_ty_info {\n          Some(bracketed_inner_ty) => {\n            bracket_inner_ty = Some(bracketed_inner_ty.ident.clone());\n            bracket_ty = Some(inner.ident.clone());\n          },\n          None => {\n            bracket_ty = Some(inner.ident.clone());\n          },\n        }\n      },\n      Ok(None) => {\n        let msg = format!(\"Fail to get the ty inner type: {:?}\", field);\n        return Err(msg);\n      },\n      Err(e) => {\n        eprintln!(\"ASTField parser failed: {:?} with error: {}\", field, e);\n        return Err(e);\n      },\n    }\n\n    Ok(ASTField {\n      member: match &field.ident {\n        Some(ident) => syn::Member::Named(ident.clone()),\n        None => syn::Member::Unnamed(index.into()),\n      },\n      pb_attrs: PBStructAttrs::from_ast(cx, index, field),\n      node_attrs: NodeStructAttrs::from_ast(cx, index, field),\n      ty: &field.ty,\n      original: field,\n      bracket_ty,\n      bracket_inner_ty,\n      bracket_category,\n    })\n  }\n\n  pub fn ty_as_str(&self) -> String {\n    match self.bracket_inner_ty {\n      Some(ref ty) => ty.to_string(),\n      None => self.bracket_ty.as_ref().unwrap().clone().to_string(),\n    }\n  }\n\n  pub fn name(&self) -> Option<syn::Ident> {\n    if let syn::Member::Named(ident) = &self.member {\n      Some(ident.clone())\n    } else {\n      None\n    }\n  }\n}\n\n#[derive(Copy, Clone)]\npub enum ASTStyle {\n  Struct,\n  /// Many unnamed fields.\n  Tuple,\n  /// One unnamed field.\n  NewType,\n  /// No fields.\n  Unit,\n}\n\npub fn struct_from_ast<'a>(\n  cx: &ASTResult,\n  fields: &'a syn::Fields,\n) -> (ASTStyle, Vec<ASTField<'a>>) {\n  match fields {\n    syn::Fields::Named(fields) => (ASTStyle::Struct, fields_from_ast(cx, &fields.named)),\n    syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {\n      (ASTStyle::NewType, fields_from_ast(cx, &fields.unnamed))\n    },\n    syn::Fields::Unnamed(fields) => (ASTStyle::Tuple, fields_from_ast(cx, &fields.unnamed)),\n    syn::Fields::Unit => (ASTStyle::Unit, Vec::new()),\n  }\n}\n\npub fn enum_from_ast<'a>(\n  cx: &ASTResult,\n  ident: &syn::Ident,\n  variants: &'a Punctuated<syn::Variant, Token![,]>,\n  enum_attrs: &[syn::Attribute],\n) -> Vec<ASTEnumVariant<'a>> {\n  variants\n    .iter()\n    .flat_map(|variant| {\n      let attrs = EventEnumAttrs::from_ast(cx, ident, variant, enum_attrs);\n      let (style, fields) = struct_from_ast(cx, &variant.fields);\n      Some(ASTEnumVariant {\n        ident: variant.ident.clone(),\n        attrs,\n        style,\n        fields,\n        original: variant,\n      })\n    })\n    .collect()\n}\n\nfn fields_from_ast<'a>(\n  cx: &ASTResult,\n  fields: &'a Punctuated<syn::Field, Token![,]>,\n) -> Vec<ASTField<'a>> {\n  fields\n    .iter()\n    .enumerate()\n    .flat_map(|(index, field)| {\n      if is_recognizable_field(field) {\n        ASTField::new(cx, field, index).ok()\n      } else {\n        None\n      }\n    })\n    .collect()\n}\n\nfn get_node_type(\n  ast_result: &ASTResult,\n  struct_name: &Ident,\n  attrs: &[syn::Attribute],\n) -> Option<String> {\n  let mut node_type = None;\n  attrs\n    .iter()\n    .filter(|attr| attr.path.segments.iter().any(|s| s.ident == NODE_TYPE))\n    .for_each(|attr| {\n      if let Ok(NameValue(named_value)) = attr.parse_meta() {\n        if node_type.is_some() {\n          ast_result.error_spanned_by(struct_name, \"Duplicate node type definition\");\n        }\n        if let syn::Lit::Str(s) = named_value.lit {\n          node_type = Some(s.value());\n        }\n      }\n    });\n  node_type\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-ast/src/ctxt.rs",
    "content": "use quote::ToTokens;\nuse std::{cell::RefCell, fmt::Display, thread};\n\n#[derive(Default)]\npub struct ASTResult {\n  errors: RefCell<Option<Vec<syn::Error>>>,\n}\n\nimpl ASTResult {\n  pub fn new() -> Self {\n    ASTResult {\n      errors: RefCell::new(Some(Vec::new())),\n    }\n  }\n\n  pub fn error_spanned_by<A: ToTokens, T: Display>(&self, obj: A, msg: T) {\n    self\n      .errors\n      .borrow_mut()\n      .as_mut()\n      .unwrap()\n      .push(syn::Error::new_spanned(obj.into_token_stream(), msg));\n  }\n\n  pub fn syn_error(&self, err: syn::Error) {\n    self.errors.borrow_mut().as_mut().unwrap().push(err);\n  }\n\n  pub fn check(self) -> Result<(), Vec<syn::Error>> {\n    let errors = self.errors.borrow_mut().take().unwrap();\n    match errors.len() {\n      0 => Ok(()),\n      _ => Err(errors),\n    }\n  }\n}\n\nimpl Drop for ASTResult {\n  fn drop(&mut self) {\n    if !thread::panicking() && self.errors.borrow().is_some() {\n      panic!(\"forgot to check for errors\");\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-ast/src/event_attrs.rs",
    "content": "use crate::{get_event_meta_items, parse_lit_str, symbol::*, ASTResult};\n\nuse syn::{\n  self,\n  Meta::{NameValue, Path},\n  NestedMeta::{Lit, Meta},\n};\n\n#[derive(Debug, Clone)]\npub struct EventAttrs {\n  input: Option<syn::Path>,\n  output: Option<syn::Path>,\n  error_ty: Option<String>,\n  pub ignore: bool,\n}\n\n#[derive(Debug, Clone)]\npub struct EventEnumAttrs {\n  pub enum_name: String,\n  pub enum_item_name: String,\n  pub value: String,\n  pub event_attrs: EventAttrs,\n}\n\nimpl EventEnumAttrs {\n  pub fn from_ast(\n    ast_result: &ASTResult,\n    ident: &syn::Ident,\n    variant: &syn::Variant,\n    enum_attrs: &[syn::Attribute],\n  ) -> Self {\n    let enum_item_name = variant.ident.to_string();\n    let enum_name = ident.to_string();\n    let mut value = String::new();\n    if variant.discriminant.is_some() {\n      if let syn::Expr::Lit(ref expr_list) = variant.discriminant.as_ref().unwrap().1 {\n        let lit_int = if let syn::Lit::Int(ref int_value) = expr_list.lit {\n          int_value\n        } else {\n          unimplemented!()\n        };\n        value = lit_int.base10_digits().to_string();\n      }\n    }\n    let event_attrs = get_event_attrs_from(ast_result, &variant.attrs, enum_attrs);\n    EventEnumAttrs {\n      enum_name,\n      enum_item_name,\n      value,\n      event_attrs,\n    }\n  }\n\n  pub fn event_input(&self) -> Option<syn::Path> {\n    self.event_attrs.input.clone()\n  }\n\n  pub fn event_output(&self) -> Option<syn::Path> {\n    self.event_attrs.output.clone()\n  }\n\n  pub fn event_error(&self) -> String {\n    self.event_attrs.error_ty.as_ref().unwrap().clone()\n  }\n}\n\nfn get_event_attrs_from(\n  ast_result: &ASTResult,\n  variant_attrs: &[syn::Attribute],\n  enum_attrs: &[syn::Attribute],\n) -> EventAttrs {\n  let mut event_attrs = EventAttrs {\n    input: None,\n    output: None,\n    error_ty: None,\n    ignore: false,\n  };\n\n  enum_attrs\n    .iter()\n    .filter(|attr| attr.path.segments.iter().any(|s| s.ident == EVENT_ERR))\n    .for_each(|attr| {\n      if let Ok(NameValue(named_value)) = attr.parse_meta() {\n        if let syn::Lit::Str(s) = named_value.lit {\n          event_attrs.error_ty = Some(s.value());\n        } else {\n          eprintln!(\"❌ {} should not be empty\", EVENT_ERR);\n        }\n      } else {\n        eprintln!(\"❌ Can not find any {} on attr: {:#?}\", EVENT_ERR, attr);\n      }\n    });\n\n  let mut extract_event_attr = |attr: &syn::Attribute, meta_item: &syn::NestedMeta| match &meta_item\n  {\n    Meta(NameValue(name_value)) => {\n      if name_value.path == EVENT_INPUT {\n        if let syn::Lit::Str(s) = &name_value.lit {\n          let input_type = parse_lit_str(s)\n            .map_err(|_| {\n              ast_result.error_spanned_by(\n                s,\n                format!(\"failed to parse request deserializer {:?}\", s.value()),\n              )\n            })\n            .unwrap();\n          event_attrs.input = Some(input_type);\n        }\n      }\n\n      if name_value.path == EVENT_OUTPUT {\n        if let syn::Lit::Str(s) = &name_value.lit {\n          let output_type = parse_lit_str(s)\n            .map_err(|_| {\n              ast_result.error_spanned_by(\n                s,\n                format!(\"failed to parse response deserializer {:?}\", s.value()),\n              )\n            })\n            .unwrap();\n          event_attrs.output = Some(output_type);\n        }\n      }\n    },\n    Meta(Path(word)) => {\n      if word == EVENT_IGNORE && attr.path == EVENT {\n        event_attrs.ignore = true;\n      }\n    },\n    Lit(s) => ast_result.error_spanned_by(s, \"unexpected attribute\"),\n    _ => ast_result.error_spanned_by(meta_item, \"unexpected attribute\"),\n  };\n\n  let attr_meta_items_info = variant_attrs\n    .iter()\n    .flat_map(|attr| match get_event_meta_items(ast_result, attr) {\n      Ok(items) => Some((attr, items)),\n      Err(_) => None,\n    })\n    .collect::<Vec<(&syn::Attribute, Vec<syn::NestedMeta>)>>();\n\n  for (attr, nested_metas) in attr_meta_items_info {\n    nested_metas\n      .iter()\n      .for_each(|meta_item| extract_event_attr(attr, meta_item))\n  }\n\n  // eprintln!(\"😁{:#?}\", event_attrs);\n  event_attrs\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-ast/src/lib.rs",
    "content": "#[macro_use]\nextern crate syn;\n\nmod ast;\nmod ctxt;\nmod pb_attrs;\n\nmod event_attrs;\nmod node_attrs;\npub mod symbol;\npub mod ty_ext;\n\npub use self::{symbol::*, ty_ext::*};\npub use ast::*;\npub use ctxt::ASTResult;\npub use event_attrs::*;\npub use pb_attrs::*;\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-ast/src/node_attrs.rs",
    "content": "use crate::{get_node_meta_items, parse_lit_into_expr_path, symbol::*, ASTAttr, ASTResult};\nuse quote::ToTokens;\nuse syn::{\n  self, LitStr,\n  Meta::NameValue,\n  NestedMeta::{Lit, Meta},\n};\n\npub struct NodeStructAttrs {\n  pub rename: Option<LitStr>,\n  pub has_child: bool,\n  pub child_name: Option<LitStr>,\n  pub child_index: Option<syn::LitInt>,\n  pub get_node_value_with: Option<syn::ExprPath>,\n  pub set_node_value_with: Option<syn::ExprPath>,\n  pub with_children: Option<syn::ExprPath>,\n}\n\nimpl NodeStructAttrs {\n  /// Extract out the `#[node(...)]` attributes from a struct field.\n  pub fn from_ast(ast_result: &ASTResult, _index: usize, field: &syn::Field) -> Self {\n    let mut rename = ASTAttr::none(ast_result, RENAME_NODE);\n    let mut child_name = ASTAttr::none(ast_result, CHILD_NODE_NAME);\n    let mut child_index = ASTAttr::none(ast_result, CHILD_NODE_INDEX);\n    let mut get_node_value_with = ASTAttr::none(ast_result, GET_NODE_VALUE_WITH);\n    let mut set_node_value_with = ASTAttr::none(ast_result, SET_NODE_VALUE_WITH);\n    let mut with_children = ASTAttr::none(ast_result, WITH_CHILDREN);\n\n    for meta_item in field\n      .attrs\n      .iter()\n      .flat_map(|attr| get_node_meta_items(ast_result, attr))\n      .flatten()\n    {\n      match &meta_item {\n        // Parse '#[node(rename = x)]'\n        Meta(NameValue(m)) if m.path == RENAME_NODE => {\n          if let syn::Lit::Str(lit) = &m.lit {\n            rename.set(&m.path, lit.clone());\n          }\n        },\n\n        // Parse '#[node(child_name = x)]'\n        Meta(NameValue(m)) if m.path == CHILD_NODE_NAME => {\n          if let syn::Lit::Str(lit) = &m.lit {\n            child_name.set(&m.path, lit.clone());\n          }\n        },\n\n        // Parse '#[node(child_index = x)]'\n        Meta(NameValue(m)) if m.path == CHILD_NODE_INDEX => {\n          if let syn::Lit::Int(lit) = &m.lit {\n            child_index.set(&m.path, lit.clone());\n          }\n        },\n\n        // Parse `#[node(get_node_value_with = \"...\")]`\n        Meta(NameValue(m)) if m.path == GET_NODE_VALUE_WITH => {\n          if let Ok(path) = parse_lit_into_expr_path(ast_result, GET_NODE_VALUE_WITH, &m.lit) {\n            get_node_value_with.set(&m.path, path);\n          }\n        },\n\n        // Parse `#[node(set_node_value_with= \"...\")]`\n        Meta(NameValue(m)) if m.path == SET_NODE_VALUE_WITH => {\n          if let Ok(path) = parse_lit_into_expr_path(ast_result, SET_NODE_VALUE_WITH, &m.lit) {\n            set_node_value_with.set(&m.path, path);\n          }\n        },\n\n        // Parse `#[node(with_children= \"...\")]`\n        Meta(NameValue(m)) if m.path == WITH_CHILDREN => {\n          if let Ok(path) = parse_lit_into_expr_path(ast_result, WITH_CHILDREN, &m.lit) {\n            with_children.set(&m.path, path);\n          }\n        },\n\n        Meta(meta_item) => {\n          let path = meta_item\n            .path()\n            .into_token_stream()\n            .to_string()\n            .replace(' ', \"\");\n          ast_result.error_spanned_by(\n            meta_item.path(),\n            format!(\"unknown node field attribute `{}`\", path),\n          );\n        },\n\n        Lit(lit) => {\n          ast_result.error_spanned_by(lit, \"unexpected literal in field attribute\");\n        },\n      }\n    }\n    let child_name = child_name.get();\n    NodeStructAttrs {\n      rename: rename.get(),\n      child_index: child_index.get(),\n      has_child: child_name.is_some(),\n      child_name,\n      get_node_value_with: get_node_value_with.get(),\n      set_node_value_with: set_node_value_with.get(),\n      with_children: with_children.get(),\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-ast/src/pb_attrs.rs",
    "content": "#![allow(clippy::all)]\n\nuse crate::{symbol::*, ASTResult};\nuse proc_macro2::{Group, Span, TokenStream, TokenTree};\nuse quote::ToTokens;\nuse syn::{\n  self,\n  parse::{self, Parse},\n  Meta::{List, NameValue, Path},\n  NestedMeta::{Lit, Meta},\n};\n\n#[allow(dead_code)]\npub struct PBAttrsContainer {\n  name: String,\n  pb_struct_type: Option<syn::Type>,\n  pb_enum_type: Option<syn::Type>,\n}\n\nimpl PBAttrsContainer {\n  /// Extract out the `#[pb(...)]` attributes from an item.\n  pub fn from_ast(ast_result: &ASTResult, item: &syn::DeriveInput) -> Self {\n    let mut pb_struct_type = ASTAttr::none(ast_result, PB_STRUCT);\n    let mut pb_enum_type = ASTAttr::none(ast_result, PB_ENUM);\n    for meta_item in item\n      .attrs\n      .iter()\n      .flat_map(|attr| get_pb_meta_items(ast_result, attr))\n      .flatten()\n    {\n      match &meta_item {\n        // Parse `#[pb(struct = \"Type\")]\n        Meta(NameValue(m)) if m.path == PB_STRUCT => {\n          if let Ok(into_ty) = parse_lit_into_ty(ast_result, PB_STRUCT, &m.lit) {\n            pb_struct_type.set_opt(&m.path, Some(into_ty));\n          }\n        },\n\n        // Parse `#[pb(enum = \"Type\")]\n        Meta(NameValue(m)) if m.path == PB_ENUM => {\n          if let Ok(into_ty) = parse_lit_into_ty(ast_result, PB_ENUM, &m.lit) {\n            pb_enum_type.set_opt(&m.path, Some(into_ty));\n          }\n        },\n\n        Meta(meta_item) => {\n          let path = meta_item\n            .path()\n            .into_token_stream()\n            .to_string()\n            .replace(' ', \"\");\n          ast_result.error_spanned_by(\n            meta_item.path(),\n            format!(\"unknown container attribute `{}`\", path),\n          );\n        },\n\n        Lit(lit) => {\n          ast_result.error_spanned_by(lit, \"unexpected literal in container attribute\");\n        },\n      }\n    }\n    match &item.data {\n      syn::Data::Struct(_) => {\n        pb_struct_type.set_if_none(default_pb_type(&ast_result, &item.ident));\n      },\n      syn::Data::Enum(_) => {\n        pb_enum_type.set_if_none(default_pb_type(&ast_result, &item.ident));\n      },\n      _ => {},\n    }\n\n    PBAttrsContainer {\n      name: item.ident.to_string(),\n      pb_struct_type: pb_struct_type.get(),\n      pb_enum_type: pb_enum_type.get(),\n    }\n  }\n\n  pub fn pb_struct_type(&self) -> Option<&syn::Type> {\n    self.pb_struct_type.as_ref()\n  }\n\n  pub fn pb_enum_type(&self) -> Option<&syn::Type> {\n    self.pb_enum_type.as_ref()\n  }\n}\n\npub struct ASTAttr<'c, T> {\n  ast_result: &'c ASTResult,\n  name: Symbol,\n  tokens: TokenStream,\n  value: Option<T>,\n}\n\nimpl<'c, T> ASTAttr<'c, T> {\n  pub(crate) fn none(ast_result: &'c ASTResult, name: Symbol) -> Self {\n    ASTAttr {\n      ast_result,\n      name,\n      tokens: TokenStream::new(),\n      value: None,\n    }\n  }\n\n  pub(crate) fn set<A: ToTokens>(&mut self, obj: A, value: T) {\n    let tokens = obj.into_token_stream();\n\n    if self.value.is_some() {\n      self\n        .ast_result\n        .error_spanned_by(tokens, format!(\"duplicate attribute `{}`\", self.name));\n    } else {\n      self.tokens = tokens;\n      self.value = Some(value);\n    }\n  }\n\n  fn set_opt<A: ToTokens>(&mut self, obj: A, value: Option<T>) {\n    if let Some(value) = value {\n      self.set(obj, value);\n    }\n  }\n\n  pub(crate) fn set_if_none(&mut self, value: T) {\n    if self.value.is_none() {\n      self.value = Some(value);\n    }\n  }\n\n  pub(crate) fn get(self) -> Option<T> {\n    self.value\n  }\n\n  #[allow(dead_code)]\n  fn get_with_tokens(self) -> Option<(TokenStream, T)> {\n    match self.value {\n      Some(v) => Some((self.tokens, v)),\n      None => None,\n    }\n  }\n}\n\npub struct PBStructAttrs {\n  #[allow(dead_code)]\n  name: String,\n  pb_index: Option<syn::LitInt>,\n  pb_one_of: bool,\n  skip_pb_serializing: bool,\n  skip_pb_deserializing: bool,\n  serialize_pb_with: Option<syn::ExprPath>,\n  deserialize_pb_with: Option<syn::ExprPath>,\n}\n\npub fn is_recognizable_field(field: &syn::Field) -> bool {\n  field\n    .attrs\n    .iter()\n    .any(|attr| is_recognizable_attribute(attr))\n}\n\nimpl PBStructAttrs {\n  /// Extract out the `#[pb(...)]` attributes from a struct field.\n  pub fn from_ast(ast_result: &ASTResult, index: usize, field: &syn::Field) -> Self {\n    let mut pb_index = ASTAttr::none(ast_result, PB_INDEX);\n    let mut pb_one_of = BoolAttr::none(ast_result, PB_ONE_OF);\n    let mut serialize_pb_with = ASTAttr::none(ast_result, SERIALIZE_PB_WITH);\n    let mut skip_pb_serializing = BoolAttr::none(ast_result, SKIP_PB_SERIALIZING);\n    let mut deserialize_pb_with = ASTAttr::none(ast_result, DESERIALIZE_PB_WITH);\n    let mut skip_pb_deserializing = BoolAttr::none(ast_result, SKIP_PB_DESERIALIZING);\n\n    let ident = match &field.ident {\n      Some(ident) => ident.to_string(),\n      None => index.to_string(),\n    };\n\n    for meta_item in field\n      .attrs\n      .iter()\n      .flat_map(|attr| get_pb_meta_items(ast_result, attr))\n      .flatten()\n    {\n      match &meta_item {\n        // Parse `#[pb(skip)]`\n        Meta(Path(word)) if word == SKIP => {\n          skip_pb_serializing.set_true(word);\n          skip_pb_deserializing.set_true(word);\n        },\n\n        // Parse '#[pb(index = x)]'\n        Meta(NameValue(m)) if m.path == PB_INDEX => {\n          if let syn::Lit::Int(lit) = &m.lit {\n            pb_index.set(&m.path, lit.clone());\n          }\n        },\n\n        // Parse `#[pb(one_of)]`\n        Meta(Path(path)) if path == PB_ONE_OF => {\n          pb_one_of.set_true(path);\n        },\n\n        // Parse `#[pb(serialize_pb_with = \"...\")]`\n        Meta(NameValue(m)) if m.path == SERIALIZE_PB_WITH => {\n          if let Ok(path) = parse_lit_into_expr_path(ast_result, SERIALIZE_PB_WITH, &m.lit) {\n            serialize_pb_with.set(&m.path, path);\n          }\n        },\n\n        // Parse `#[pb(deserialize_pb_with = \"...\")]`\n        Meta(NameValue(m)) if m.path == DESERIALIZE_PB_WITH => {\n          if let Ok(path) = parse_lit_into_expr_path(ast_result, DESERIALIZE_PB_WITH, &m.lit) {\n            deserialize_pb_with.set(&m.path, path);\n          }\n        },\n\n        Meta(meta_item) => {\n          let path = meta_item\n            .path()\n            .into_token_stream()\n            .to_string()\n            .replace(' ', \"\");\n          ast_result.error_spanned_by(\n            meta_item.path(),\n            format!(\"unknown pb field attribute `{}`\", path),\n          );\n        },\n\n        Lit(lit) => {\n          ast_result.error_spanned_by(lit, \"unexpected literal in field attribute\");\n        },\n      }\n    }\n\n    PBStructAttrs {\n      name: ident,\n      pb_index: pb_index.get(),\n      pb_one_of: pb_one_of.get(),\n      skip_pb_serializing: skip_pb_serializing.get(),\n      skip_pb_deserializing: skip_pb_deserializing.get(),\n      serialize_pb_with: serialize_pb_with.get(),\n      deserialize_pb_with: deserialize_pb_with.get(),\n    }\n  }\n\n  #[allow(dead_code)]\n  pub fn pb_index(&self) -> Option<String> {\n    self\n      .pb_index\n      .as_ref()\n      .map(|lit| lit.base10_digits().to_string())\n  }\n\n  pub fn is_one_of(&self) -> bool {\n    self.pb_one_of\n  }\n\n  pub fn serialize_pb_with(&self) -> Option<&syn::ExprPath> {\n    self.serialize_pb_with.as_ref()\n  }\n\n  pub fn deserialize_pb_with(&self) -> Option<&syn::ExprPath> {\n    self.deserialize_pb_with.as_ref()\n  }\n\n  pub fn skip_pb_serializing(&self) -> bool {\n    self.skip_pb_serializing\n  }\n\n  pub fn skip_pb_deserializing(&self) -> bool {\n    self.skip_pb_deserializing\n  }\n}\n\npub enum Default {\n  /// Field must always be specified because it does not have a default.\n  None,\n  /// The default is given by `std::default::Default::default()`.\n  Default,\n  /// The default is given by this function.\n  Path(syn::ExprPath),\n}\n\npub fn is_recognizable_attribute(attr: &syn::Attribute) -> bool {\n  attr.path == PB_ATTRS || attr.path == EVENT || attr.path == NODE_ATTRS || attr.path == NODES_ATTRS\n}\n\npub fn get_pb_meta_items(\n  cx: &ASTResult,\n  attr: &syn::Attribute,\n) -> Result<Vec<syn::NestedMeta>, ()> {\n  // Only handle the attribute that we have defined\n  if attr.path != PB_ATTRS {\n    return Ok(vec![]);\n  }\n\n  // http://strymon.systems.ethz.ch/typename/syn/enum.Meta.html\n  match attr.parse_meta() {\n    Ok(List(meta)) => Ok(meta.nested.into_iter().collect()),\n    Ok(other) => {\n      cx.error_spanned_by(other, \"expected #[pb(...)]\");\n      Err(())\n    },\n    Err(err) => {\n      cx.error_spanned_by(attr, \"attribute must be str, e.g. #[pb(xx = \\\"xxx\\\")]\");\n      cx.syn_error(err);\n      Err(())\n    },\n  }\n}\n\npub fn get_node_meta_items(\n  cx: &ASTResult,\n  attr: &syn::Attribute,\n) -> Result<Vec<syn::NestedMeta>, ()> {\n  // Only handle the attribute that we have defined\n  if attr.path != NODE_ATTRS && attr.path != NODES_ATTRS {\n    return Ok(vec![]);\n  }\n\n  // http://strymon.systems.ethz.ch/typename/syn/enum.Meta.html\n  match attr.parse_meta() {\n    Ok(List(meta)) => Ok(meta.nested.into_iter().collect()),\n    Ok(_) => Ok(vec![]),\n    Err(err) => {\n      cx.error_spanned_by(attr, \"attribute must be str, e.g. #[node(xx = \\\"xxx\\\")]\");\n      cx.syn_error(err);\n      Err(())\n    },\n  }\n}\n\npub fn get_event_meta_items(\n  cx: &ASTResult,\n  attr: &syn::Attribute,\n) -> Result<Vec<syn::NestedMeta>, ()> {\n  // Only handle the attribute that we have defined\n  if attr.path != EVENT {\n    return Ok(vec![]);\n  }\n\n  // http://strymon.systems.ethz.ch/typename/syn/enum.Meta.html\n  match attr.parse_meta() {\n    Ok(List(meta)) => Ok(meta.nested.into_iter().collect()),\n    Ok(other) => {\n      cx.error_spanned_by(other, \"expected #[event(...)]\");\n      Err(())\n    },\n    Err(err) => {\n      cx.error_spanned_by(attr, \"attribute must be str, e.g. #[event(xx = \\\"xxx\\\")]\");\n      cx.syn_error(err);\n      Err(())\n    },\n  }\n}\n\npub fn parse_lit_into_expr_path(\n  ast_result: &ASTResult,\n  attr_name: Symbol,\n  lit: &syn::Lit,\n) -> Result<syn::ExprPath, ()> {\n  let string = get_lit_str(ast_result, attr_name, lit)?;\n  parse_lit_str(string).map_err(|_| {\n    ast_result.error_spanned_by(lit, format!(\"failed to parse path: {:?}\", string.value()))\n  })\n}\n\nfn get_lit_str<'a>(\n  ast_result: &ASTResult,\n  attr_name: Symbol,\n  lit: &'a syn::Lit,\n) -> Result<&'a syn::LitStr, ()> {\n  if let syn::Lit::Str(lit) = lit {\n    Ok(lit)\n  } else {\n    ast_result.error_spanned_by(\n      lit,\n      format!(\n        \"expected pb {} attribute to be a string: `{} = \\\"...\\\"`\",\n        attr_name, attr_name\n      ),\n    );\n    Err(())\n  }\n}\n\nfn parse_lit_into_ty(\n  ast_result: &ASTResult,\n  attr_name: Symbol,\n  lit: &syn::Lit,\n) -> Result<syn::Type, ()> {\n  let string = get_lit_str(ast_result, attr_name, lit)?;\n\n  parse_lit_str(string).map_err(|_| {\n    ast_result.error_spanned_by(\n      lit,\n      format!(\"failed to parse type: {} = {:?}\", attr_name, string.value()),\n    )\n  })\n}\n\npub fn parse_lit_str<T>(s: &syn::LitStr) -> parse::Result<T>\nwhere\n  T: Parse,\n{\n  let tokens = spanned_tokens(s)?;\n  syn::parse2(tokens)\n}\n\nfn spanned_tokens(s: &syn::LitStr) -> parse::Result<TokenStream> {\n  let stream = syn::parse_str(&s.value())?;\n  Ok(respan_token_stream(stream, s.span()))\n}\n\nfn respan_token_stream(stream: TokenStream, span: Span) -> TokenStream {\n  stream\n    .into_iter()\n    .map(|token| respan_token_tree(token, span))\n    .collect()\n}\n\nfn respan_token_tree(mut token: TokenTree, span: Span) -> TokenTree {\n  if let TokenTree::Group(g) = &mut token {\n    *g = Group::new(g.delimiter(), respan_token_stream(g.stream(), span));\n  }\n  token.set_span(span);\n  token\n}\n\nfn default_pb_type(ast_result: &ASTResult, ident: &syn::Ident) -> syn::Type {\n  let take_ident = ident.to_string();\n  let lit_str = syn::LitStr::new(&take_ident, ident.span());\n  if let Ok(tokens) = spanned_tokens(&lit_str) {\n    if let Ok(pb_struct_ty) = syn::parse2(tokens) {\n      return pb_struct_ty;\n    }\n  }\n  ast_result.error_spanned_by(\n    ident,\n    format!(\"❌ Can't find {} protobuf struct\", take_ident),\n  );\n  panic!()\n}\n\n#[allow(dead_code)]\npub fn is_option(ty: &syn::Type) -> bool {\n  let path = match ungroup(ty) {\n    syn::Type::Path(ty) => &ty.path,\n    _ => {\n      return false;\n    },\n  };\n  let seg = match path.segments.last() {\n    Some(seg) => seg,\n    None => {\n      return false;\n    },\n  };\n  let args = match &seg.arguments {\n    syn::PathArguments::AngleBracketed(bracketed) => &bracketed.args,\n    _ => {\n      return false;\n    },\n  };\n  seg.ident == \"Option\" && args.len() == 1\n}\n\n#[allow(dead_code)]\npub fn ungroup(mut ty: &syn::Type) -> &syn::Type {\n  while let syn::Type::Group(group) = ty {\n    ty = &group.elem;\n  }\n  ty\n}\n\nstruct BoolAttr<'c>(ASTAttr<'c, ()>);\n\nimpl<'c> BoolAttr<'c> {\n  fn none(ast_result: &'c ASTResult, name: Symbol) -> Self {\n    BoolAttr(ASTAttr::none(ast_result, name))\n  }\n\n  fn set_true<A: ToTokens>(&mut self, obj: A) {\n    self.0.set(obj, ());\n  }\n\n  fn get(&self) -> bool {\n    self.0.value.is_some()\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-ast/src/symbol.rs",
    "content": "use std::fmt::{self, Display};\nuse syn::{Ident, Path};\n\n#[derive(Copy, Clone)]\npub struct Symbol(&'static str);\n\n// Protobuf\npub const PB_ATTRS: Symbol = Symbol(\"pb\");\n//#[pb(skip)]\npub const SKIP: Symbol = Symbol(\"skip\");\n//#[pb(index = \"1\")]\npub const PB_INDEX: Symbol = Symbol(\"index\");\n//#[pb(one_of)]\npub const PB_ONE_OF: Symbol = Symbol(\"one_of\");\n//#[pb(skip_pb_deserializing = \"...\")]\npub const SKIP_PB_DESERIALIZING: Symbol = Symbol(\"skip_pb_deserializing\");\n//#[pb(skip_pb_serializing)]\npub const SKIP_PB_SERIALIZING: Symbol = Symbol(\"skip_pb_serializing\");\n//#[pb(serialize_pb_with = \"...\")]\npub const SERIALIZE_PB_WITH: Symbol = Symbol(\"serialize_pb_with\");\n//#[pb(deserialize_pb_with = \"...\")]\npub const DESERIALIZE_PB_WITH: Symbol = Symbol(\"deserialize_pb_with\");\n//#[pb(struct=\"some struct\")]\npub const PB_STRUCT: Symbol = Symbol(\"struct\");\n//#[pb(enum=\"some enum\")]\npub const PB_ENUM: Symbol = Symbol(\"enum\");\n\n// Event\npub const EVENT_INPUT: Symbol = Symbol(\"input\");\npub const EVENT_OUTPUT: Symbol = Symbol(\"output\");\npub const EVENT_IGNORE: Symbol = Symbol(\"ignore\");\npub const EVENT: Symbol = Symbol(\"event\");\npub const EVENT_ERR: Symbol = Symbol(\"event_err\");\n\n// Node\npub const NODE_ATTRS: Symbol = Symbol(\"node\");\npub const NODES_ATTRS: Symbol = Symbol(\"nodes\");\npub const NODE_TYPE: Symbol = Symbol(\"node_type\");\npub const NODE_INDEX: Symbol = Symbol(\"index\");\npub const RENAME_NODE: Symbol = Symbol(\"rename\");\npub const CHILD_NODE_NAME: Symbol = Symbol(\"child_name\");\npub const CHILD_NODE_INDEX: Symbol = Symbol(\"child_index\");\npub const SKIP_NODE_ATTRS: Symbol = Symbol(\"skip_node_attribute\");\npub const GET_NODE_VALUE_WITH: Symbol = Symbol(\"get_value_with\");\npub const SET_NODE_VALUE_WITH: Symbol = Symbol(\"set_value_with\");\npub const GET_VEC_ELEMENT_WITH: Symbol = Symbol(\"get_element_with\");\npub const GET_MUT_VEC_ELEMENT_WITH: Symbol = Symbol(\"get_mut_element_with\");\npub const WITH_CHILDREN: Symbol = Symbol(\"with_children\");\n\nimpl PartialEq<Symbol> for Ident {\n  fn eq(&self, word: &Symbol) -> bool {\n    self == word.0\n  }\n}\n\nimpl PartialEq<Symbol> for &Ident {\n  fn eq(&self, word: &Symbol) -> bool {\n    *self == word.0\n  }\n}\n\nimpl PartialEq<Symbol> for Path {\n  fn eq(&self, word: &Symbol) -> bool {\n    self.is_ident(word.0)\n  }\n}\n\nimpl PartialEq<Symbol> for &Path {\n  fn eq(&self, word: &Symbol) -> bool {\n    self.is_ident(word.0)\n  }\n}\n\nimpl Display for Symbol {\n  fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {\n    formatter.write_str(self.0)\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-ast/src/ty_ext.rs",
    "content": "use crate::ASTResult;\nuse syn::{self, AngleBracketedGenericArguments, PathSegment};\n\n#[derive(Eq, PartialEq, Debug)]\npub enum PrimitiveTy {\n  Map(MapInfo),\n  Vec,\n  Opt,\n  Other,\n}\n\n#[derive(Debug)]\npub struct TyInfo<'a> {\n  pub ident: &'a syn::Ident,\n  pub ty: &'a syn::Type,\n  pub primitive_ty: PrimitiveTy,\n  pub bracket_ty_info: Box<Option<TyInfo<'a>>>,\n}\n\n#[derive(Debug, Eq, PartialEq)]\npub struct MapInfo {\n  pub key: String,\n  pub value: String,\n}\n\nimpl MapInfo {\n  fn new(key: String, value: String) -> Self {\n    MapInfo { key, value }\n  }\n}\n\nimpl<'a> TyInfo<'a> {\n  #[allow(dead_code)]\n  pub fn bracketed_ident(&'a self) -> &'a syn::Ident {\n    match self.bracket_ty_info.as_ref() {\n      Some(b_ty) => b_ty.ident,\n      None => {\n        panic!()\n      },\n    }\n  }\n}\n\npub fn parse_ty<'a>(\n  ast_result: &ASTResult,\n  ty: &'a syn::Type,\n) -> Result<Option<TyInfo<'a>>, String> {\n  // Type -> TypePath -> Path -> PathSegment -> PathArguments ->\n  // AngleBracketedGenericArguments -> GenericArgument -> Type.\n  if let syn::Type::Path(ref p) = ty {\n    if p.path.segments.len() != 1 {\n      return Ok(None);\n    }\n\n    let seg = match p.path.segments.last() {\n      Some(seg) => seg,\n      None => return Ok(None),\n    };\n\n    let _is_option = seg.ident == \"Option\";\n\n    return if let syn::PathArguments::AngleBracketed(ref bracketed) = seg.arguments {\n      match seg.ident.to_string().as_ref() {\n        \"HashMap\" => generate_hashmap_ty_info(ast_result, ty, seg, bracketed),\n        \"Vec\" => generate_vec_ty_info(ast_result, seg, bracketed),\n        \"Option\" => generate_option_ty_info(ast_result, ty, seg, bracketed),\n        _ => {\n          let msg = format!(\"Unsupported type: {}\", seg.ident);\n          ast_result.error_spanned_by(&seg.ident, &msg);\n          return Err(msg);\n        },\n      }\n    } else {\n      return Ok(Some(TyInfo {\n        ident: &seg.ident,\n        ty,\n        primitive_ty: PrimitiveTy::Other,\n        bracket_ty_info: Box::new(None),\n      }));\n    };\n  }\n  Err(\"Unsupported inner type, get inner type fail\".to_string())\n}\n\nfn parse_bracketed(bracketed: &AngleBracketedGenericArguments) -> Vec<&syn::Type> {\n  bracketed\n    .args\n    .iter()\n    .flat_map(|arg| {\n      if let syn::GenericArgument::Type(ref ty_in_bracket) = arg {\n        Some(ty_in_bracket)\n      } else {\n        None\n      }\n    })\n    .collect::<Vec<&syn::Type>>()\n}\n\npub fn generate_hashmap_ty_info<'a>(\n  ast_result: &ASTResult,\n  ty: &'a syn::Type,\n  path_segment: &'a PathSegment,\n  bracketed: &'a AngleBracketedGenericArguments,\n) -> Result<Option<TyInfo<'a>>, String> {\n  // The args of map must greater than 2\n  if bracketed.args.len() != 2 {\n    return Ok(None);\n  }\n  let types = parse_bracketed(bracketed);\n  let key = parse_ty(ast_result, types[0])?.unwrap().ident.to_string();\n  let value = parse_ty(ast_result, types[1])?.unwrap().ident.to_string();\n  let bracket_ty_info = Box::new(parse_ty(ast_result, types[1])?);\n  Ok(Some(TyInfo {\n    ident: &path_segment.ident,\n    ty,\n    primitive_ty: PrimitiveTy::Map(MapInfo::new(key, value)),\n    bracket_ty_info,\n  }))\n}\n\nfn generate_option_ty_info<'a>(\n  ast_result: &ASTResult,\n  ty: &'a syn::Type,\n  path_segment: &'a PathSegment,\n  bracketed: &'a AngleBracketedGenericArguments,\n) -> Result<Option<TyInfo<'a>>, String> {\n  assert_eq!(path_segment.ident.to_string(), \"Option\".to_string());\n  let types = parse_bracketed(bracketed);\n  let bracket_ty_info = Box::new(parse_ty(ast_result, types[0])?);\n  Ok(Some(TyInfo {\n    ident: &path_segment.ident,\n    ty,\n    primitive_ty: PrimitiveTy::Opt,\n    bracket_ty_info,\n  }))\n}\n\nfn generate_vec_ty_info<'a>(\n  ast_result: &ASTResult,\n  path_segment: &'a PathSegment,\n  bracketed: &'a AngleBracketedGenericArguments,\n) -> Result<Option<TyInfo<'a>>, String> {\n  if bracketed.args.len() != 1 {\n    return Ok(None);\n  }\n  if let syn::GenericArgument::Type(ref bracketed_type) = bracketed.args.first().unwrap() {\n    let bracketed_ty_info = Box::new(parse_ty(ast_result, bracketed_type)?);\n    return Ok(Some(TyInfo {\n      ident: &path_segment.ident,\n      ty: bracketed_type,\n      primitive_ty: PrimitiveTy::Vec,\n      bracket_ty_info: bracketed_ty_info,\n    }));\n  }\n  Ok(None)\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/Cargo.toml",
    "content": "[package]\nname = \"flowy-codegen\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nlog = \"0.4.17\"\nserde = { workspace = true, features = [\"derive\"] }\nserde_json.workspace = true\nflowy-ast.workspace = true\nquote = \"1.0\"\n\ncmd_lib = { version = \"1.9.5\", optional = true }\nprotoc-rust = { version = \"2.28.0\", optional = true }\n#protobuf-codegen = { version = \"3.7.1\" }\nwalkdir = { version = \"2\", optional = true }\nsimilar = { version = \"1.3.0\", optional = true }\nsyn = { version = \"1.0.109\", features = [\"extra-traits\", \"parsing\", \"derive\", \"full\"] }\nfancy-regex = { version = \"0.10.0\", optional = true }\nlazy_static = { version = \"1.4.0\", optional = true }\ntera = { version = \"1.17.1\", optional = true }\nitertools = { version = \"0.10\", optional = true }\nphf = { version = \"0.8.0\", features = [\"macros\"], optional = true }\nconsole = { version = \"0.14.1\", optional = true }\nprotoc-bin-vendored = { version = \"3.1.0\", optional = true }\ntoml = { version = \"0.5.11\", optional = true }\n\n\n[features]\nproto_gen = [\n  \"similar\",\n  \"fancy-regex\",\n  \"lazy_static\",\n  \"tera\",\n  \"itertools\",\n  \"phf\",\n  \"walkdir\",\n  \"console\",\n  \"toml\",\n  \"cmd_lib\",\n  \"protoc-rust\",\n  \"walkdir\",\n  \"protoc-bin-vendored\",\n]\ndart_event = [\"walkdir\", \"tera\", ]\ndart = [\"proto_gen\", \"dart_event\"]\nts_event = [\"walkdir\", \"tera\", ]\nts = [\"proto_gen\", \"ts_event\"]\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/ast.rs",
    "content": "use flowy_ast::EventEnumAttrs;\nuse quote::format_ident;\n\n#[allow(dead_code)]\npub struct EventASTContext {\n  pub event: syn::Ident,\n  pub event_ty: syn::Ident,\n  pub event_request_struct: syn::Ident,\n  pub event_input: Option<syn::Path>,\n  pub event_output: Option<syn::Path>,\n  pub event_error: String,\n}\n\nimpl EventASTContext {\n  #[allow(dead_code)]\n  pub fn from(enum_attrs: &EventEnumAttrs) -> EventASTContext {\n    let command_name = enum_attrs.enum_item_name.clone();\n    if command_name.is_empty() {\n      panic!(\"Invalid command name: {}\", enum_attrs.enum_item_name);\n    }\n\n    let event = format_ident!(\"{}\", &command_name);\n    let splits = command_name.split('_').collect::<Vec<&str>>();\n\n    let event_ty = format_ident!(\"{}\", enum_attrs.enum_name);\n    let event_request_struct = format_ident!(\"{}Event\", &splits.join(\"\"));\n\n    let event_input = enum_attrs.event_input();\n    let event_output = enum_attrs.event_output();\n    let event_error = enum_attrs.event_error();\n\n    EventASTContext {\n      event,\n      event_ty,\n      event_request_struct,\n      event_input,\n      event_output,\n      event_error,\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/dart_event/dart_event.rs",
    "content": "use std::fs::File;\nuse std::io::Write;\nuse std::path::PathBuf;\n\nuse syn::Item;\nuse walkdir::WalkDir;\n\nuse flowy_ast::ASTResult;\n\nuse crate::ast::EventASTContext;\nuse crate::flowy_toml::{CrateConfig, parse_crate_config_from};\nuse crate::util::{is_crate_dir, is_hidden, path_string_with_component, read_file};\n\nuse super::event_template::*;\n\npub fn r#gen(crate_name: &str) {\n  if std::env::var(\"CARGO_MAKE_WORKING_DIRECTORY\").is_err() {\n    println!(\"CARGO_MAKE_WORKING_DIRECTORY was not set, skip generate dart pb\");\n    return;\n  }\n\n  if std::env::var(\"FLUTTER_FLOWY_SDK_PATH\").is_err() {\n    println!(\"FLUTTER_FLOWY_SDK_PATH was not set, skip generate dart pb\");\n    return;\n  }\n\n  let crate_path = std::fs::canonicalize(\".\")\n    .unwrap()\n    .as_path()\n    .display()\n    .to_string();\n  let event_crates = parse_dart_event_files(vec![crate_path]);\n  let event_ast = event_crates\n    .iter()\n    .flat_map(parse_event_crate)\n    .collect::<Vec<_>>();\n\n  let event_render_ctx = ast_to_event_render_ctx(event_ast.as_ref());\n  let mut render_result = DART_IMPORTED.to_owned();\n  for (index, render_ctx) in event_render_ctx.into_iter().enumerate() {\n    let mut event_template = EventTemplate::new();\n\n    if let Some(content) = event_template.render(render_ctx, index) {\n      render_result.push_str(content.as_ref())\n    }\n  }\n\n  let dart_event_folder: PathBuf = [\n    &std::env::var(\"CARGO_MAKE_WORKING_DIRECTORY\").unwrap(),\n    &std::env::var(\"FLUTTER_FLOWY_SDK_PATH\").unwrap(),\n    \"lib\",\n    \"dispatch\",\n    \"dart_event\",\n    crate_name,\n  ]\n  .iter()\n  .collect();\n\n  if !dart_event_folder.as_path().exists() {\n    std::fs::create_dir_all(dart_event_folder.as_path()).unwrap();\n  }\n\n  let dart_event_file_path =\n    path_string_with_component(&dart_event_folder, vec![\"dart_event.dart\"]);\n  println!(\"cargo:rerun-if-changed={}\", dart_event_file_path);\n\n  match std::fs::OpenOptions::new()\n    .create(true)\n    .write(true)\n    .append(false)\n    .truncate(true)\n    .open(&dart_event_file_path)\n  {\n    Ok(ref mut file) => {\n      file.write_all(render_result.as_bytes()).unwrap();\n      File::flush(file).unwrap();\n    },\n    Err(err) => {\n      panic!(\"Failed to open file: {}, {:?}\", dart_event_file_path, err);\n    },\n  }\n}\n\nconst DART_IMPORTED: &str = r#\"\n/// Auto generate. Do not edit\npart of '../../dispatch.dart';\n\"#;\n\n#[derive(Debug)]\npub struct DartEventCrate {\n  crate_path: PathBuf,\n  event_files: Vec<String>,\n}\n\nimpl DartEventCrate {\n  pub fn from_config(config: &CrateConfig) -> Self {\n    DartEventCrate {\n      crate_path: config.crate_path.clone(),\n      event_files: config.flowy_config.event_files.clone(),\n    }\n  }\n}\n\npub fn parse_dart_event_files(crate_paths: Vec<String>) -> Vec<DartEventCrate> {\n  let mut dart_event_crates: Vec<DartEventCrate> = vec![];\n  crate_paths.iter().for_each(|path| {\n    let crates = WalkDir::new(path)\n      .into_iter()\n      .filter_entry(|e| !is_hidden(e))\n      .filter_map(|e| e.ok())\n      .filter(is_crate_dir)\n      .flat_map(|e| parse_crate_config_from(&e))\n      .map(|crate_config| DartEventCrate::from_config(&crate_config))\n      .collect::<Vec<DartEventCrate>>();\n    dart_event_crates.extend(crates);\n  });\n  dart_event_crates\n}\n\npub fn parse_event_crate(event_crate: &DartEventCrate) -> Vec<EventASTContext> {\n  event_crate\n    .event_files\n    .iter()\n    .flat_map(|event_file| {\n      let file_path =\n        path_string_with_component(&event_crate.crate_path, vec![event_file.as_str()]);\n\n      let file_content = read_file(file_path.as_ref()).unwrap();\n      let ast = syn::parse_file(file_content.as_ref()).expect(\"Unable to parse file\");\n      ast\n        .items\n        .iter()\n        .flat_map(|item| match item {\n          Item::Enum(item_enum) => {\n            let ast_result = ASTResult::new();\n            let attrs = flowy_ast::enum_from_ast(\n              &ast_result,\n              &item_enum.ident,\n              &item_enum.variants,\n              &item_enum.attrs,\n            );\n            ast_result.check().unwrap();\n            attrs\n              .iter()\n              .filter(|attr| !attr.attrs.event_attrs.ignore)\n              .map(|variant| EventASTContext::from(&variant.attrs))\n              .collect::<Vec<_>>()\n          },\n          _ => vec![],\n        })\n        .collect::<Vec<_>>()\n    })\n    .collect::<Vec<EventASTContext>>()\n}\n\npub fn ast_to_event_render_ctx(ast: &[EventASTContext]) -> Vec<EventRenderContext> {\n  ast\n    .iter()\n    .map(|event_ast| {\n      let input_deserializer = event_ast\n        .event_input\n        .as_ref()\n        .map(|event_input| event_input.get_ident().unwrap().to_string());\n\n      let output_deserializer = event_ast\n        .event_output\n        .as_ref()\n        .map(|event_output| event_output.get_ident().unwrap().to_string());\n\n      EventRenderContext {\n        input_deserializer,\n        output_deserializer,\n        error_deserializer: event_ast.event_error.clone(),\n        event: event_ast.event.to_string(),\n        event_ty: event_ast.event_ty.to_string(),\n      }\n    })\n    .collect::<Vec<EventRenderContext>>()\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/dart_event/event_template.rs",
    "content": "use crate::util::get_tera;\nuse tera::Context;\n\npub struct EventTemplate {\n  tera_context: Context,\n}\n\npub struct EventRenderContext {\n  pub input_deserializer: Option<String>,\n  pub output_deserializer: Option<String>,\n  pub error_deserializer: String,\n  pub event: String,\n  pub event_ty: String,\n}\n\n#[allow(dead_code)]\nimpl EventTemplate {\n  pub fn new() -> Self {\n    EventTemplate {\n      tera_context: Context::new(),\n    }\n  }\n\n  pub fn render(&mut self, ctx: EventRenderContext, index: usize) -> Option<String> {\n    self.tera_context.insert(\"index\", &index);\n    let dart_class_name = format!(\"{}{}\", ctx.event_ty, ctx.event);\n    let event = format!(\"{}.{}\", ctx.event_ty, ctx.event);\n    self.tera_context.insert(\"event_class\", &dart_class_name);\n    self.tera_context.insert(\"event\", &event);\n\n    self\n      .tera_context\n      .insert(\"has_input\", &ctx.input_deserializer.is_some());\n    match ctx.input_deserializer {\n      None => self.tera_context.insert(\"input_deserializer\", \"void\"),\n      Some(ref input) => self.tera_context.insert(\"input_deserializer\", input),\n    }\n\n    // eprintln!(\n    //     \"😁 {:?} / {:?}\",\n    //     &ctx.input_deserializer, &ctx.output_deserializer\n    // );\n\n    let has_output = ctx.output_deserializer.is_some();\n    self.tera_context.insert(\"has_output\", &has_output);\n\n    match ctx.output_deserializer {\n      None => self.tera_context.insert(\"output_deserializer\", \"void\"),\n      Some(ref output) => self.tera_context.insert(\"output_deserializer\", output),\n    }\n\n    self\n      .tera_context\n      .insert(\"error_deserializer\", &ctx.error_deserializer);\n\n    let tera = get_tera(\"dart_event\");\n    match tera.render(\"event_template.tera\", &self.tera_context) {\n      Ok(r) => Some(r),\n      Err(e) => {\n        log::error!(\"{:?}\", e);\n        None\n      },\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/dart_event/event_template.tera",
    "content": "class {{ event_class }} {\n{%- if has_input  %}\n     {{ input_deserializer }} request;\n     {{ event_class }}(this.request);\n{%- else %}\n    {{ event_class }}();\n{%- endif %}\n\n    Future<FlowyResult<{{ output_deserializer }}, {{ error_deserializer }}>> send() {\n\n{%- if has_input  %}\n    final request = FFIRequest.create()\n          ..event = {{ event }}.toString()\n          ..payload = requestToBytes(this.request);\n\n    return Dispatch.asyncRequest(request)\n        .then((bytesResult) => bytesResult.fold(\n\n        {%- if has_output  %}\n           (okBytes) => FlowySuccess({{ output_deserializer }}.fromBuffer(okBytes)),\n        {%- else %}\n           (bytes) => FlowySuccess(null),\n        {%- endif %}\n           (errBytes) => FlowyFailure({{ error_deserializer }}.fromBuffer(errBytes)),\n        ));\n\n{%- else %}\n     final request = FFIRequest.create()\n        ..event = {{ event }}.toString();\n        {%- if has_input  %}\n        ..payload = bytes;\n        {%- endif %}\n\n     return Dispatch.asyncRequest(request).then((bytesResult) => bytesResult.fold(\n     {%- if has_output  %}\n        (okBytes) => FlowySuccess({{ output_deserializer }}.fromBuffer(okBytes)),\n     {%- else %}\n        (bytes) => FlowySuccess(null),\n     {%- endif %}\n        (errBytes) => FlowyFailure({{ error_deserializer }}.fromBuffer(errBytes)),\n      ));\n{%- endif %}\n    }\n}\n\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/dart_event/mod.rs",
    "content": "#![allow(clippy::module_inception)]\n\nmod dart_event;\nmod event_template;\n\npub use dart_event::*;\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/flowy_toml.rs",
    "content": "use std::fs;\nuse std::path::{Path, PathBuf};\n\n#[derive(serde::Deserialize, Clone, Debug)]\npub struct FlowyConfig {\n  #[serde(default)]\n  pub event_files: Vec<String>,\n\n  // Collect AST from the file or directory specified by proto_input to generate the proto files.\n  #[serde(default)]\n  pub proto_input: Vec<String>,\n\n  // Output path for the generated proto files. The default value is default_proto_output()\n  #[serde(default = \"default_proto_output\")]\n  pub proto_output: String,\n\n  // Create a crate that stores the generated protobuf Rust structures. The default value is default_protobuf_crate()\n  #[serde(default = \"default_protobuf_crate\")]\n  pub protobuf_crate_path: String,\n}\n\nfn default_proto_output() -> String {\n  let mut path = PathBuf::from(\"resources\");\n  path.push(\"proto\");\n  path.to_str().unwrap().to_owned()\n}\n\nfn default_protobuf_crate() -> String {\n  let mut path = PathBuf::from(\"src\");\n  path.push(\"protobuf\");\n  path.to_str().unwrap().to_owned()\n}\n\nimpl FlowyConfig {\n  pub fn from_toml_file(path: &Path) -> Self {\n    let content = fs::read_to_string(path).unwrap();\n    let config: FlowyConfig = toml::from_str(content.as_ref()).unwrap();\n    config\n  }\n}\n\npub struct CrateConfig {\n  pub crate_path: PathBuf,\n  pub crate_folder: String,\n  pub flowy_config: FlowyConfig,\n}\n\npub fn parse_crate_config_from(entry: &walkdir::DirEntry) -> Option<CrateConfig> {\n  let mut config_path = entry.path().parent().unwrap().to_path_buf();\n  config_path.push(\"Flowy.toml\");\n  if !config_path.as_path().exists() {\n    return None;\n  }\n  let crate_path = entry.path().parent().unwrap().to_path_buf();\n  let flowy_config = FlowyConfig::from_toml_file(config_path.as_path());\n  let crate_folder = crate_path\n    .file_stem()\n    .unwrap()\n    .to_str()\n    .unwrap()\n    .to_string();\n\n  Some(CrateConfig {\n    crate_path,\n    crate_folder,\n    flowy_config,\n  })\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/lib.rs",
    "content": "#[cfg(feature = \"proto_gen\")]\npub mod protobuf_file;\n\n#[cfg(feature = \"dart_event\")]\npub mod dart_event;\n\n#[cfg(feature = \"ts_event\")]\npub mod ts_event;\n\n#[cfg(any(feature = \"proto_gen\", feature = \"dart_event\", feature = \"ts_event\"))]\nmod flowy_toml;\n\npub(crate) mod ast;\n#[cfg(any(feature = \"proto_gen\", feature = \"dart_event\", feature = \"ts_event\"))]\npub mod util;\n\n#[derive(serde::Serialize, serde::Deserialize)]\npub struct ProtoCache {\n  pub structs: Vec<String>,\n  pub enums: Vec<String>,\n}\n\npub enum Project {\n  Tauri,\n  TauriApp,\n  Native,\n}\n\nimpl Project {\n  pub fn dst(&self) -> String {\n    match self {\n      Project::Tauri => \"appflowy_tauri/src/services/backend\".to_string(),\n      Project::TauriApp => {\n        \"appflowy_web_app/src/application/services/tauri-services/backend\".to_string()\n      },\n      Project::Native => panic!(\"Native project is not supported yet.\"),\n    }\n  }\n\n  pub fn event_root(&self) -> String {\n    match self {\n      Project::Tauri | Project::TauriApp => \"../../\".to_string(),\n      Project::Native => panic!(\"Native project is not supported yet.\"),\n    }\n  }\n\n  pub fn model_root(&self) -> String {\n    match self {\n      Project::Tauri | Project::TauriApp => \"../../\".to_string(),\n      Project::Native => panic!(\"Native project is not supported yet.\"),\n    }\n  }\n\n  pub fn event_imports(&self) -> String {\n    match self {\n      Project::TauriApp | Project::Tauri => r#\"\n/// Auto generate. Do not edit\nimport { Ok, Err, Result } from \"ts-results\";\nimport { invoke } from \"@tauri-apps/api/tauri\";\nimport * as pb from \"../..\";\n\"#\n      .to_string(),\n      Project::Native => panic!(\"Native project is not supported yet.\"),\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/ast.rs",
    "content": "#![allow(unused_attributes)]\n#![allow(dead_code)]\n#![allow(unused_imports)]\n#![allow(unused_results)]\nuse crate::protobuf_file::template::{EnumTemplate, RUST_TYPE_MAP, StructTemplate};\nuse crate::protobuf_file::{ProtoFile, ProtobufCrateContext, parse_crate_info_from_path};\nuse crate::util::*;\nuse fancy_regex::Regex;\nuse flowy_ast::*;\nuse lazy_static::lazy_static;\nuse std::path::PathBuf;\nuse std::{fs::File, io::Read, path::Path};\nuse syn::Item;\nuse walkdir::WalkDir;\n\npub fn parse_protobuf_context_from(crate_paths: Vec<String>) -> Vec<ProtobufCrateContext> {\n  let crate_infos = parse_crate_info_from_path(crate_paths);\n  crate_infos\n    .into_iter()\n    .map(|crate_info| {\n      let proto_output_path = crate_info.proto_output_path();\n      let files = crate_info\n        .proto_input_paths()\n        .iter()\n        .flat_map(|proto_crate_path| parse_files_protobuf(proto_crate_path, &proto_output_path))\n        .collect::<Vec<ProtoFile>>();\n\n      ProtobufCrateContext::from_crate_info(crate_info, files)\n    })\n    .collect::<Vec<ProtobufCrateContext>>()\n}\n\nfn parse_files_protobuf(proto_crate_path: &Path, proto_output_path: &Path) -> Vec<ProtoFile> {\n  let mut gen_proto_vec: Vec<ProtoFile> = vec![];\n  // file_stem https://doc.rust-lang.org/std/path/struct.Path.html#method.file_stem\n  for (path, file_name) in WalkDir::new(proto_crate_path)\n    .into_iter()\n    .filter_entry(|e| !is_hidden(e))\n    .filter_map(|e| e.ok())\n    .filter(|e| !e.file_type().is_dir())\n    .map(|e| {\n      let path = e.path().to_str().unwrap().to_string();\n      let file_name = e.path().file_stem().unwrap().to_str().unwrap().to_string();\n      (path, file_name)\n    })\n  {\n    if file_name == \"mod\" {\n      continue;\n    }\n\n    // https://docs.rs/syn/1.0.54/syn/struct.File.html\n    let ast = syn::parse_file(read_file(&path).unwrap().as_ref())\n      .unwrap_or_else(|_| panic!(\"Unable to parse file at {}\", path));\n    let structs = get_ast_structs(&ast);\n    let proto_file = format!(\"{}.proto\", &file_name);\n    let proto_file_path = path_string_with_component(proto_output_path, vec![&proto_file]);\n    let proto_syntax = find_proto_syntax(proto_file_path.as_ref());\n\n    let mut proto_content = String::new();\n\n    // The types that are not defined in the current file.\n    let mut ref_types: Vec<String> = vec![];\n    structs.iter().for_each(|s| {\n      let mut struct_template = StructTemplate::new();\n      struct_template.set_message_struct_name(&s.name);\n\n      s.fields\n        .iter()\n        .filter(|field| field.pb_attrs.pb_index().is_some())\n        .for_each(|field| {\n          ref_types.push(field.ty_as_str());\n          struct_template.set_field(field);\n        });\n\n      let s = struct_template.render().unwrap();\n\n      proto_content.push_str(s.as_ref());\n      proto_content.push('\\n');\n    });\n\n    let enums = get_ast_enums(&ast);\n    enums.iter().for_each(|e| {\n      let mut enum_template = EnumTemplate::new();\n      enum_template.set_message_enum(e);\n      let s = enum_template.render().unwrap();\n      proto_content.push_str(s.as_ref());\n      ref_types.push(e.name.clone());\n\n      proto_content.push('\\n');\n    });\n\n    if !enums.is_empty() || !structs.is_empty() {\n      let structs: Vec<String> = structs.iter().map(|s| s.name.clone()).collect();\n      let enums: Vec<String> = enums.iter().map(|e| e.name.clone()).collect();\n      ref_types.retain(|s| !structs.contains(s));\n      ref_types.retain(|s| !enums.contains(s));\n\n      let info = ProtoFile {\n        file_path: path.clone(),\n        file_name: file_name.clone(),\n        ref_types,\n        structs,\n        enums,\n        syntax: proto_syntax,\n        content: proto_content,\n      };\n      gen_proto_vec.push(info);\n    }\n  }\n\n  gen_proto_vec\n}\n\npub fn get_ast_structs(ast: &syn::File) -> Vec<Struct> {\n  // let mut content = format!(\"{:#?}\", &ast);\n  // let mut file = File::create(\"./foo.txt\").unwrap();\n  // file.write_all(content.as_bytes()).unwrap();\n  let ast_result = ASTResult::new();\n  let mut proto_structs: Vec<Struct> = vec![];\n  ast.items.iter().for_each(|item| {\n    if let Item::Struct(item_struct) = item {\n      let (_, fields) = struct_from_ast(&ast_result, &item_struct.fields);\n\n      if fields\n        .iter()\n        .filter(|f| f.pb_attrs.pb_index().is_some())\n        .count()\n        > 0\n      {\n        proto_structs.push(Struct {\n          name: item_struct.ident.to_string(),\n          fields,\n        });\n      }\n    }\n  });\n  ast_result.check().unwrap();\n  proto_structs\n}\n\npub fn get_ast_enums(ast: &syn::File) -> Vec<FlowyEnum> {\n  let mut flowy_enums: Vec<FlowyEnum> = vec![];\n  let ast_result = ASTResult::new();\n\n  ast.items.iter().for_each(|item| {\n    // https://docs.rs/syn/1.0.54/syn/enum.Item.html\n    if let Item::Enum(item_enum) = item {\n      let attrs = flowy_ast::enum_from_ast(\n        &ast_result,\n        &item_enum.ident,\n        &item_enum.variants,\n        &ast.attrs,\n      );\n      flowy_enums.push(FlowyEnum {\n        name: item_enum.ident.to_string(),\n        attrs,\n      });\n    }\n  });\n  ast_result.check().unwrap();\n  flowy_enums\n}\n\npub struct FlowyEnum<'a> {\n  pub name: String,\n  pub attrs: Vec<ASTEnumVariant<'a>>,\n}\n\npub struct Struct<'a> {\n  pub name: String,\n  pub fields: Vec<ASTField<'a>>,\n}\n\nlazy_static! {\n    static ref SYNTAX_REGEX: Regex = Regex::new(\"syntax.*;\").unwrap();\n    // static ref IMPORT_REGEX: Regex = Regex::new(\"(import\\\\s).*;\").unwrap();\n}\n\nfn find_proto_syntax(path: &str) -> String {\n  if !Path::new(path).exists() {\n    return String::from(\"syntax = \\\"proto3\\\";\\n\");\n  }\n\n  let mut result = String::new();\n  let mut file = File::open(path).unwrap();\n  let mut content = String::new();\n  file.read_to_string(&mut content).unwrap();\n\n  content.lines().for_each(|line| {\n    ////Result<Option<Match<'t>>>\n    if let Ok(Some(m)) = SYNTAX_REGEX.find(line) {\n      result.push_str(m.as_str());\n    }\n\n    // if let Ok(Some(m)) = IMPORT_REGEX.find(line) {\n    //     result.push_str(m.as_str());\n    //     result.push('\\n');\n    // }\n  });\n\n  result.push('\\n');\n  result\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs",
    "content": "#![allow(unused_imports)]\n#![allow(unused_attributes)]\n#![allow(dead_code)]\nmod ast;\nmod proto_gen;\nmod proto_info;\nmod template;\n\nuse crate::Project;\nuse crate::util::path_string_with_component;\nuse itertools::Itertools;\nuse log::info;\npub use proto_gen::*;\npub use proto_info::*;\nuse std::fs;\nuse std::fs::File;\nuse std::io::{BufRead, BufReader, Write};\nuse std::path::{Path, PathBuf};\nuse std::process::Command;\nuse walkdir::WalkDir;\n\npub fn dart_gen(crate_name: &str) {\n  // 1. generate the proto files to proto_file_dir\n  #[cfg(feature = \"proto_gen\")]\n  let proto_crates = gen_proto_files(crate_name);\n\n  for proto_crate in proto_crates {\n    let mut proto_file_paths = vec![];\n    let mut file_names = vec![];\n    let proto_file_output_path = proto_crate\n      .proto_output_path()\n      .to_str()\n      .unwrap()\n      .to_string();\n    let protobuf_output_path = proto_crate\n      .protobuf_crate_path()\n      .to_str()\n      .unwrap()\n      .to_string();\n\n    for (path, file_name) in WalkDir::new(&proto_file_output_path)\n      .into_iter()\n      .filter_map(|e| e.ok())\n      .map(|e| {\n        let path = e.path().to_str().unwrap().to_string();\n        let file_name = e.path().file_stem().unwrap().to_str().unwrap().to_string();\n        (path, file_name)\n      })\n    {\n      if path.ends_with(\".proto\") {\n        // https://stackoverflow.com/questions/49077147/how-can-i-force-build-rs-to-run-again-without-cleaning-my-whole-project\n        println!(\"cargo:rerun-if-changed={}\", path);\n        proto_file_paths.push(path);\n        file_names.push(file_name);\n      }\n    }\n    let protoc_bin_path = protoc_bin_vendored::protoc_bin_path().unwrap();\n\n    // 2. generate the protobuf files(Dart)\n    #[cfg(feature = \"dart\")]\n    generate_dart_protobuf_files(\n      crate_name,\n      &proto_file_output_path,\n      &proto_file_paths,\n      &file_names,\n      &protoc_bin_path,\n    );\n\n    // 3. generate the protobuf files(Rust)\n    generate_rust_protobuf_files(\n      &protoc_bin_path,\n      &proto_file_paths,\n      &proto_file_output_path,\n      &protobuf_output_path,\n    );\n  }\n}\n\n// #[allow(unused_variables)]\n// fn ts_gen(crate_name: &str, dest_folder_name: &str, project: Project) {\n//   // 1. generate the proto files to proto_file_dir\n//   #[cfg(feature = \"proto_gen\")]\n//   let proto_crates = gen_proto_files(crate_name);\n//\n//   for proto_crate in proto_crates {\n//     let mut proto_file_paths = vec![];\n//     let mut file_names = vec![];\n//     let proto_file_output_path = proto_crate\n//       .proto_output_path()\n//       .to_str()\n//       .unwrap()\n//       .to_string();\n//     let protobuf_output_path = proto_crate\n//       .protobuf_crate_path()\n//       .to_str()\n//       .unwrap()\n//       .to_string();\n//\n//     for (path, file_name) in WalkDir::new(&proto_file_output_path)\n//       .into_iter()\n//       .filter_map(|e| e.ok())\n//       .map(|e| {\n//         let path = e.path().to_str().unwrap().to_string();\n//         let file_name = e.path().file_stem().unwrap().to_str().unwrap().to_string();\n//         (path, file_name)\n//       })\n//     {\n//       if path.ends_with(\".proto\") {\n//         // https://stackoverflow.com/questions/49077147/how-can-i-force-build-rs-to-run-again-without-cleaning-my-whole-project\n//         println!(\"cargo:rerun-if-changed={}\", path);\n//         proto_file_paths.push(path);\n//         file_names.push(file_name);\n//       }\n//     }\n//     let protoc_bin_path = protoc_bin_vendored::protoc_bin_path().unwrap();\n//\n//     // 2. generate the protobuf files(Dart)\n//     #[cfg(feature = \"ts\")]\n//     generate_ts_protobuf_files(\n//       dest_folder_name,\n//       &proto_file_output_path,\n//       &proto_file_paths,\n//       &file_names,\n//       &protoc_bin_path,\n//       &project,\n//     );\n//\n//     // 3. generate the protobuf files(Rust)\n//     generate_rust_protobuf_files(\n//       &protoc_bin_path,\n//       &proto_file_paths,\n//       &proto_file_output_path,\n//       &protobuf_output_path,\n//     );\n//   }\n// }\n\nfn generate_rust_protobuf_files(\n  protoc_bin_path: &Path,\n  proto_file_paths: &[String],\n  proto_file_output_path: &str,\n  protobuf_output_path: &str,\n) {\n  protoc_rust::Codegen::new()\n    .out_dir(protobuf_output_path)\n    .protoc_path(protoc_bin_path)\n    .inputs(proto_file_paths)\n    .include(proto_file_output_path)\n    .run()\n    .expect(\"Running rust protoc failed.\");\n  remove_box_pointers_lint_from_all_except_mod(protobuf_output_path);\n}\nfn remove_box_pointers_lint_from_all_except_mod(dir_path: &str) {\n  let dir = fs::read_dir(dir_path).expect(\"Failed to read directory\");\n  for entry in dir {\n    let entry = entry.expect(\"Failed to read directory entry\");\n    let path = entry.path();\n\n    // Skip directories and mod.rs\n    if path.is_file() {\n      if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) {\n        if file_name != \"mod.rs\" {\n          remove_box_pointers_lint(&path);\n        }\n      }\n    }\n  }\n}\n\nfn remove_box_pointers_lint(file_path: &Path) {\n  let file = File::open(file_path).expect(\"Failed to open file\");\n  let reader = BufReader::new(file);\n  let lines: Vec<String> = reader\n    .lines()\n    .map_while(Result::ok)\n    .filter(|line| !line.contains(\"#![allow(box_pointers)]\"))\n    .collect();\n\n  let mut file = File::create(file_path).expect(\"Failed to create file\");\n  for line in lines {\n    writeln!(file, \"{}\", line).expect(\"Failed to write line\");\n  }\n}\n\n#[cfg(feature = \"ts\")]\nfn generate_ts_protobuf_files(\n  name: &str,\n  proto_file_output_path: &str,\n  paths: &[String],\n  file_names: &Vec<String>,\n  protoc_bin_path: &Path,\n  project: &Project,\n) {\n  let root = project.model_root();\n  let backend_service_path = project.dst();\n\n  let mut output = PathBuf::new();\n  output.push(root);\n  output.push(backend_service_path);\n  output.push(\"models\");\n  output.push(name);\n\n  if !output.as_path().exists() {\n    std::fs::create_dir_all(&output).unwrap();\n  }\n  let protoc_bin_path = protoc_bin_path.to_str().unwrap().to_owned();\n  paths.iter().for_each(|path| {\n    // if let Err(err) = Command::new(protoc_bin_path.clone())\n    //   .arg(format!(\"--ts_out={}\", output.to_str().unwrap()))\n    //   .arg(format!(\"--proto_path={}\", proto_file_output_path))\n    //   .arg(path)\n    //   .spawn()\n    // {\n    //   panic!(\"Generate ts pb file failed: {}, {:?}\", path, err);\n    // }\n\n    println!(\"cargo:rerun-if-changed={}\", output.to_str().unwrap());\n    let result = cmd_lib::run_cmd! {\n        ${protoc_bin_path} --ts_out=${output} --proto_path=${proto_file_output_path} ${path}\n    };\n\n    if result.is_err() {\n      panic!(\"Generate ts pb file failed with: {}, {:?}\", path, result)\n    };\n  });\n\n  let ts_index = path_string_with_component(&output, vec![\"index.ts\"]);\n  match std::fs::OpenOptions::new()\n    .create(true)\n    .write(true)\n    .append(false)\n    .truncate(true)\n    .open(ts_index)\n  {\n    Ok(ref mut file) => {\n      let mut export = String::new();\n      export.push_str(\"// Auto-generated, do not edit \\n\");\n      for file_name in file_names {\n        let c = format!(\"export * from \\\"./{}\\\";\\n\", file_name);\n        export.push_str(c.as_ref());\n      }\n\n      file.write_all(export.as_bytes()).unwrap();\n      File::flush(file).unwrap();\n    },\n    Err(err) => {\n      panic!(\"Failed to open file: {}\", err);\n    },\n  }\n}\n\n#[cfg(feature = \"dart\")]\nfn generate_dart_protobuf_files(\n  name: &str,\n  proto_file_output_path: &str,\n  paths: &[String],\n  file_names: &Vec<String>,\n  protoc_bin_path: &Path,\n) {\n  if std::env::var(\"CARGO_MAKE_WORKING_DIRECTORY\").is_err() {\n    log::error!(\"CARGO_MAKE_WORKING_DIRECTORY was not set, skip generate dart pb\");\n    return;\n  }\n\n  if std::env::var(\"FLUTTER_FLOWY_SDK_PATH\").is_err() {\n    log::error!(\"FLUTTER_FLOWY_SDK_PATH was not set, skip generate dart pb\");\n    return;\n  }\n\n  let mut output = PathBuf::new();\n  output.push(std::env::var(\"CARGO_MAKE_WORKING_DIRECTORY\").unwrap());\n  output.push(std::env::var(\"FLUTTER_FLOWY_SDK_PATH\").unwrap());\n  output.push(\"lib\");\n  output.push(\"protobuf\");\n  output.push(name);\n\n  if !output.as_path().exists() {\n    std::fs::create_dir_all(&output).unwrap();\n  }\n  check_pb_dart_plugin();\n  let protoc_bin_path = protoc_bin_path.to_str().unwrap().to_owned();\n  paths.iter().for_each(|path| {\n    let result = cmd_lib::run_cmd! {\n        ${protoc_bin_path} --dart_out=${output} --proto_path=${proto_file_output_path} ${path}\n    };\n\n    if result.is_err() {\n      panic!(\"Generate dart pb file failed with: {}, {:?}\", path, result)\n    };\n  });\n\n  let protobuf_dart = path_string_with_component(&output, vec![\"protobuf.dart\"]);\n  println!(\"cargo:rerun-if-changed={}\", protobuf_dart);\n  match std::fs::OpenOptions::new()\n    .create(true)\n    .write(true)\n    .append(false)\n    .truncate(true)\n    .open(Path::new(&protobuf_dart))\n  {\n    Ok(ref mut file) => {\n      let mut export = String::new();\n      export.push_str(\"// Auto-generated, do not edit \\n\");\n      for file_name in file_names {\n        let c = format!(\"export './{}.pb.dart';\\n\", file_name);\n        export.push_str(c.as_ref());\n      }\n\n      file.write_all(export.as_bytes()).unwrap();\n      File::flush(file).unwrap();\n    },\n    Err(err) => {\n      panic!(\"Failed to open file: {}\", err);\n    },\n  }\n}\n\npub fn check_pb_dart_plugin() {\n  if cfg!(target_os = \"windows\") {\n    //Command::new(\"cmd\")\n    //    .arg(\"/C\")\n    //    .arg(cmd)\n    //    .status()\n    //    .expect(\"failed to execute process\");\n    //panic!(\"{}\", format!(\"\\n❌ The protoc-gen-dart was not installed correctly.\"))\n  } else {\n    let exit_result = Command::new(\"sh\")\n      .arg(\"-c\")\n      .arg(\"command -v protoc-gen-dart\")\n      .status()\n      .expect(\"failed to execute process\");\n\n    if !exit_result.success() {\n      let mut msg = \"\\n❌ Can't find protoc-gen-dart in $PATH:\\n\".to_string();\n      let output = Command::new(\"sh\").arg(\"-c\").arg(\"echo $PATH\").output();\n      let paths = String::from_utf8(output.unwrap().stdout)\n        .unwrap()\n        .split(':')\n        .map(|s| s.to_string())\n        .collect::<Vec<String>>();\n\n      paths.iter().for_each(|s| msg.push_str(&format!(\"{}\\n\", s)));\n\n      if let Ok(output) = Command::new(\"sh\")\n        .arg(\"-c\")\n        .arg(\"which protoc-gen-dart\")\n        .output()\n      {\n        msg.push_str(&format!(\n          \"Installed protoc-gen-dart path: {:?}\\n\",\n          String::from_utf8(output.stdout).unwrap()\n        ));\n      }\n\n      msg.push_str(\"✅ You can fix that by adding:\");\n      msg.push_str(\"\\n\\texport PATH=\\\"$PATH\\\":\\\"$HOME/.pub-cache/bin\\\"\\n\");\n      msg.push_str(\"to your shell's config file.(.bashrc, .bash, .profile, .zshrc etc.)\");\n      panic!(\"{}\", msg)\n    }\n  }\n}\n\n#[cfg(feature = \"proto_gen\")]\npub fn gen_proto_files(crate_name: &str) -> Vec<ProtobufCrate> {\n  let crate_path = std::fs::canonicalize(\".\")\n    .unwrap()\n    .as_path()\n    .display()\n    .to_string();\n\n  let crate_context = ProtoGenerator::r#gen(crate_name, &crate_path);\n  let proto_crates = crate_context\n    .iter()\n    .map(|info| info.protobuf_crate.clone())\n    .collect::<Vec<_>>();\n\n  crate_context\n    .into_iter()\n    .flat_map(|info| info.files)\n    .for_each(|file| {\n      println!(\"cargo:rerun-if-changed={}\", file.file_path);\n    });\n\n  proto_crates\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/proto_gen.rs",
    "content": "#![allow(unused_attributes)]\n#![allow(dead_code)]\n#![allow(unused_imports)]\n#![allow(unused_results)]\nuse crate::ProtoCache;\nuse crate::protobuf_file::ProtoFile;\nuse crate::protobuf_file::ast::parse_protobuf_context_from;\nuse crate::protobuf_file::proto_info::ProtobufCrateContext;\nuse crate::util::*;\nuse std::collections::HashMap;\nuse std::fs::File;\nuse std::path::Path;\nuse std::{fs::OpenOptions, io::Write};\n\npub struct ProtoGenerator();\nimpl ProtoGenerator {\n  pub fn r#gen(crate_name: &str, crate_path: &str) -> Vec<ProtobufCrateContext> {\n    let crate_contexts = parse_protobuf_context_from(vec![crate_path.to_owned()]);\n    write_proto_files(&crate_contexts);\n    write_rust_crate_mod_file(&crate_contexts);\n\n    let proto_cache = ProtoCache::from_crate_contexts(&crate_contexts);\n    let proto_cache_str = serde_json::to_string(&proto_cache).unwrap();\n\n    let crate_cache_dir = path_buf_with_component(&cache_dir(), vec![crate_name]);\n    if !crate_cache_dir.as_path().exists() {\n      std::fs::create_dir_all(&crate_cache_dir).unwrap();\n    }\n\n    let protobuf_cache_path = path_string_with_component(&crate_cache_dir, vec![\"proto_cache\"]);\n\n    match std::fs::OpenOptions::new()\n      .create(true)\n      .write(true)\n      .append(false)\n      .truncate(true)\n      .open(&protobuf_cache_path)\n    {\n      Ok(ref mut file) => {\n        file.write_all(proto_cache_str.as_bytes()).unwrap();\n        File::flush(file).unwrap();\n      },\n      Err(_err) => {\n        panic!(\"Failed to open file: {}\", protobuf_cache_path);\n      },\n    }\n\n    crate_contexts\n  }\n}\n\nfn write_proto_files(crate_contexts: &[ProtobufCrateContext]) {\n  let file_path_content_map = crate_contexts\n    .iter()\n    .flat_map(|ctx| {\n      ctx\n        .files\n        .iter()\n        .map(|file| {\n          (\n            file.file_path.clone(),\n            ProtoFileSymbol {\n              file_name: file.file_name.clone(),\n              symbols: file.symbols(),\n            },\n          )\n        })\n        .collect::<HashMap<String, ProtoFileSymbol>>()\n    })\n    .collect::<HashMap<String, ProtoFileSymbol>>();\n\n  for context in crate_contexts {\n    let dir = context.protobuf_crate.proto_output_path();\n    context.files.iter().for_each(|file| {\n      // syntax\n      let mut file_content = file.syntax.clone();\n\n      // import\n      file_content.push_str(&gen_import_content(file, &file_path_content_map));\n\n      // content\n      file_content.push_str(&file.content);\n\n      let proto_file = format!(\"{}.proto\", &file.file_name);\n      let proto_file_path = path_string_with_component(&dir, vec![&proto_file]);\n      save_content_to_file_with_diff_prompt(&file_content, proto_file_path.as_ref());\n    });\n  }\n}\n\nfn gen_import_content(\n  current_file: &ProtoFile,\n  file_path_symbols_map: &HashMap<String, ProtoFileSymbol>,\n) -> String {\n  let mut import_files: Vec<String> = vec![];\n  file_path_symbols_map\n    .iter()\n    .for_each(|(file_path, proto_file_symbols)| {\n      if file_path != &current_file.file_path {\n        current_file.ref_types.iter().for_each(|ref_type| {\n          if proto_file_symbols.symbols.contains(ref_type) {\n            let import_file = format!(\"import \\\"{}.proto\\\";\", proto_file_symbols.file_name);\n            if !import_files.contains(&import_file) {\n              import_files.push(import_file);\n            }\n          }\n        });\n      }\n    });\n  if import_files.len() == 1 {\n    format!(\"{}\\n\", import_files.pop().unwrap())\n  } else {\n    import_files.join(\"\\n\")\n  }\n}\n\nstruct ProtoFileSymbol {\n  file_name: String,\n  symbols: Vec<String>,\n}\n\nfn write_rust_crate_mod_file(crate_contexts: &[ProtobufCrateContext]) {\n  for context in crate_contexts {\n    let mod_path = context.protobuf_crate.proto_model_mod_file();\n    match OpenOptions::new()\n      .create(true)\n      .write(true)\n      .append(false)\n      .truncate(true)\n      .open(&mod_path)\n    {\n      Ok(ref mut file) => {\n        let mut mod_file_content = String::new();\n\n        mod_file_content.push_str(\"#![cfg_attr(rustfmt, rustfmt::skip)]\\n\");\n        mod_file_content.push_str(\" #![allow(ambiguous_glob_reexports)]\\n\");\n\n        mod_file_content.push_str(\"// Auto-generated, do not edit\\n\");\n        walk_dir(\n          context.protobuf_crate.proto_output_path(),\n          |e| !e.file_type().is_dir() && !e.file_name().to_string_lossy().starts_with('.'),\n          |_, name| {\n            let c = format!(\"\\nmod {};\\npub use {}::*;\\n\", &name, &name);\n            mod_file_content.push_str(c.as_ref());\n          },\n        );\n        file.write_all(mod_file_content.as_bytes()).unwrap();\n      },\n      Err(err) => {\n        panic!(\"Failed to open file: {}\", err);\n      },\n    }\n  }\n}\n\nimpl ProtoCache {\n  fn from_crate_contexts(crate_contexts: &[ProtobufCrateContext]) -> Self {\n    let proto_files = crate_contexts\n      .iter()\n      .flat_map(|crate_info| &crate_info.files)\n      .collect::<Vec<&ProtoFile>>();\n\n    let structs: Vec<String> = proto_files\n      .iter()\n      .flat_map(|info| info.structs.clone())\n      .collect();\n    let enums: Vec<String> = proto_files\n      .iter()\n      .flat_map(|info| info.enums.clone())\n      .collect();\n    Self { structs, enums }\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/proto_info.rs",
    "content": "#![allow(dead_code)]\nuse crate::flowy_toml::{CrateConfig, FlowyConfig, parse_crate_config_from};\nuse crate::util::*;\nuse std::fs::OpenOptions;\nuse std::io::Write;\nuse std::path::{Path, PathBuf};\nuse std::str::FromStr;\nuse walkdir::WalkDir;\n\n#[derive(Debug)]\npub struct ProtobufCrateContext {\n  pub files: Vec<ProtoFile>,\n  pub protobuf_crate: ProtobufCrate,\n}\n\nimpl ProtobufCrateContext {\n  pub fn from_crate_info(inner: ProtobufCrate, files: Vec<ProtoFile>) -> Self {\n    Self {\n      files,\n      protobuf_crate: inner,\n    }\n  }\n\n  pub fn create_crate_mod_file(&self) {\n    // mod model;\n    // pub use model::*;\n    let mod_file_path =\n      path_string_with_component(&self.protobuf_crate.protobuf_crate_path(), vec![\"mod.rs\"]);\n    let mut content = \"#![cfg_attr(rustfmt, rustfmt::skip)]\\n\".to_owned();\n    content.push_str(\" #![allow(ambiguous_glob_reexports)]\\n\");\n    content.push_str(\"// Auto-generated, do not edit\\n\");\n    content.push_str(\"mod model;\\npub use model::*;\");\n    match OpenOptions::new()\n      .create(true)\n      .write(true)\n      .append(false)\n      .truncate(true)\n      .open(Path::new(&mod_file_path))\n    {\n      Ok(ref mut file) => {\n        file.write_all(content.as_bytes()).unwrap();\n      },\n      Err(err) => {\n        panic!(\"Failed to open protobuf mod file: {}\", err);\n      },\n    }\n  }\n\n  #[allow(dead_code)]\n  pub fn flutter_mod_dir(&self, root: &str) -> String {\n    let crate_module_dir = format!(\"{}/{}\", root, self.protobuf_crate.crate_folder);\n    crate_module_dir\n  }\n\n  #[allow(dead_code)]\n  pub fn flutter_mod_file(&self, root: &str) -> String {\n    let crate_module_dir = format!(\n      \"{}/{}/protobuf.dart\",\n      root, self.protobuf_crate.crate_folder\n    );\n    crate_module_dir\n  }\n}\n\n#[derive(Clone, Debug)]\npub struct ProtobufCrate {\n  pub crate_folder: String,\n  pub crate_path: PathBuf,\n  flowy_config: FlowyConfig,\n}\n\nimpl ProtobufCrate {\n  pub fn from_config(config: CrateConfig) -> Self {\n    ProtobufCrate {\n      crate_path: config.crate_path,\n      crate_folder: config.crate_folder,\n      flowy_config: config.flowy_config,\n    }\n  }\n\n  // Return the file paths for each rust file that used to generate the proto file.\n  pub fn proto_input_paths(&self) -> Vec<PathBuf> {\n    self\n      .flowy_config\n      .proto_input\n      .iter()\n      .map(|name| path_buf_with_component(&self.crate_path, vec![name]))\n      .collect::<Vec<PathBuf>>()\n  }\n\n  // The protobuf_crate_path is used to store the generated protobuf Rust structures.\n  pub fn protobuf_crate_path(&self) -> PathBuf {\n    let crate_path = PathBuf::from(&self.flowy_config.protobuf_crate_path);\n    create_dir_if_not_exist(&crate_path);\n    crate_path\n  }\n\n  // The proto_output_path is used to store the proto files\n  pub fn proto_output_path(&self) -> PathBuf {\n    let output_dir = PathBuf::from(&self.flowy_config.proto_output);\n    create_dir_if_not_exist(&output_dir);\n    output_dir\n  }\n\n  pub fn proto_model_mod_file(&self) -> String {\n    path_string_with_component(&self.protobuf_crate_path(), vec![\"mod.rs\"])\n  }\n}\n\n#[derive(Debug)]\npub struct ProtoFile {\n  pub file_path: String,\n  pub file_name: String,\n  pub structs: Vec<String>,\n  // store the type of current file using\n  pub ref_types: Vec<String>,\n\n  pub enums: Vec<String>,\n  // proto syntax. \"proto3\" or \"proto2\"\n  pub syntax: String,\n\n  // proto message content\n  pub content: String,\n}\n\nimpl ProtoFile {\n  pub fn symbols(&self) -> Vec<String> {\n    let mut symbols = self.structs.clone();\n    let mut enum_symbols = self.enums.clone();\n    symbols.append(&mut enum_symbols);\n    symbols\n  }\n}\n\npub fn parse_crate_info_from_path(roots: Vec<String>) -> Vec<ProtobufCrate> {\n  let mut protobuf_crates: Vec<ProtobufCrate> = vec![];\n  roots.iter().for_each(|root| {\n    let crates = WalkDir::new(root)\n      .into_iter()\n      .filter_entry(|e| !is_hidden(e))\n      .filter_map(|e| e.ok())\n      .filter(is_crate_dir)\n      .flat_map(|e| parse_crate_config_from(&e))\n      .map(ProtobufCrate::from_config)\n      .collect::<Vec<ProtobufCrate>>();\n    protobuf_crates.extend(crates);\n  });\n  protobuf_crates\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/template/derive_meta/derive_meta.rs",
    "content": "use crate::util::get_tera;\nuse itertools::Itertools;\nuse tera::Context;\n\npub struct ProtobufDeriveMeta {\n  context: Context,\n  structs: Vec<String>,\n  enums: Vec<String>,\n}\n\n#[allow(dead_code)]\nimpl ProtobufDeriveMeta {\n  pub fn new(structs: Vec<String>, enums: Vec<String>) -> Self {\n    let enums: Vec<_> = enums.into_iter().unique().collect();\n    ProtobufDeriveMeta {\n      context: Context::new(),\n      structs,\n      enums,\n    }\n  }\n\n  pub fn render(&mut self) -> Option<String> {\n    self.context.insert(\"names\", &self.structs);\n    self.context.insert(\"enums\", &self.enums);\n\n    let tera = get_tera(\"protobuf_file/template/derive_meta\");\n    match tera.render(\"derive_meta.tera\", &self.context) {\n      Ok(r) => Some(r),\n      Err(e) => {\n        log::error!(\"{:?}\", e);\n        None\n      },\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/template/derive_meta/derive_meta.tera",
    "content": "#![cfg_attr(rustfmt, rustfmt::skip)]\npub enum TypeCategory {\n    Array,\n    Map,\n    Str,\n    Protobuf,\n    Bytes,\n    Enum,\n    Opt,\n    Primitive,\n}\n// auto generate, do not edit\npub fn category_from_str(type_str: &str) -> TypeCategory {\n    match type_str {\n        \"Vec\" => TypeCategory::Array,\n        \"HashMap\" => TypeCategory::Map,\n        \"u8\" => TypeCategory::Bytes,\n        \"String\" => TypeCategory::Str,\n{%- for name in names -%}\n    {%- if loop.first %}\n        \"{{ name }}\"\n    {%- else %}\n        | \"{{ name }}\"\n    {%- endif -%}\n    {%- if loop.last %}\n        => TypeCategory::Protobuf,\n    {%- endif %}\n\n{%- endfor %}\n\n{%- for enum in enums -%}\n    {%- if loop.first %}\n        \"{{ enum }}\"\n    {%- else %}\n        | \"{{ enum }}\"\n    {%- endif -%}\n    {%- if loop.last %}\n        => TypeCategory::Enum,\n    {%- endif %}\n{%- endfor %}\n\n        \"Option\" => TypeCategory::Opt,\n        _ => TypeCategory::Primitive,\n    }\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/template/derive_meta/mod.rs",
    "content": "#![allow(clippy::module_inception)]\nmod derive_meta;\n\npub use derive_meta::*;\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/template/mod.rs",
    "content": "mod derive_meta;\nmod proto_file;\n\npub use derive_meta::*;\npub use proto_file::*;\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/template/proto_file/enum.tera",
    "content": "enum {{ enum_name }} {\n    {%- for item in items %}\n    {{ item }}\n    {%- endfor %}\n}"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/template/proto_file/enum_template.rs",
    "content": "use crate::protobuf_file::ast::FlowyEnum;\nuse crate::util::get_tera;\nuse tera::Context;\n\npub struct EnumTemplate {\n  context: Context,\n  items: Vec<String>,\n}\n\n#[allow(dead_code)]\nimpl EnumTemplate {\n  pub fn new() -> Self {\n    EnumTemplate {\n      context: Context::new(),\n      items: vec![],\n    }\n  }\n\n  pub fn set_message_enum(&mut self, flowy_enum: &FlowyEnum) {\n    self.context.insert(\"enum_name\", &flowy_enum.name);\n    flowy_enum.attrs.iter().for_each(|item| {\n      self.items.push(format!(\n        \"{} = {};\",\n        item.attrs.enum_item_name, item.attrs.value\n      ))\n    })\n  }\n\n  pub fn render(&mut self) -> Option<String> {\n    self.context.insert(\"items\", &self.items);\n    let tera = get_tera(\"protobuf_file/template/proto_file\");\n    match tera.render(\"enum.tera\", &self.context) {\n      Ok(r) => Some(r),\n      Err(e) => {\n        log::error!(\"{:?}\", e);\n        None\n      },\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/template/proto_file/mod.rs",
    "content": "mod enum_template;\nmod struct_template;\n\npub use enum_template::*;\npub use struct_template::*;\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/template/proto_file/struct.tera",
    "content": "message {{ struct_name }} {\n    {%- for field in fields %}\n    {{ field }}\n    {%- endfor %}\n}"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/template/proto_file/struct_template.rs",
    "content": "use crate::util::get_tera;\nuse flowy_ast::*;\nuse phf::phf_map;\nuse tera::Context;\n\n// Protobuf data type : https://developers.google.com/protocol-buffers/docs/proto3\npub static RUST_TYPE_MAP: phf::Map<&'static str, &'static str> = phf_map! {\n    \"String\" => \"string\",\n    \"i64\" => \"int64\",\n    \"i32\" => \"int32\",\n    \"u64\" => \"uint64\",\n    \"u32\" => \"uint32\",\n    \"Vec\" => \"repeated\",\n    \"f64\" => \"double\",\n    \"HashMap\" => \"map\",\n};\n\npub struct StructTemplate {\n  context: Context,\n  fields: Vec<String>,\n}\n\n#[allow(dead_code)]\nimpl StructTemplate {\n  pub fn new() -> Self {\n    StructTemplate {\n      context: Context::new(),\n      fields: vec![],\n    }\n  }\n\n  pub fn set_message_struct_name(&mut self, name: &str) {\n    self.context.insert(\"struct_name\", name);\n  }\n\n  pub fn set_field(&mut self, field: &ASTField) {\n    // {{ field_type }} {{ field_name }} = {{index}};\n    let name = field.name().unwrap().to_string();\n    let index = field.pb_attrs.pb_index().unwrap();\n\n    let ty: &str = &field.ty_as_str();\n    let mut mapped_ty: &str = ty;\n\n    if RUST_TYPE_MAP.contains_key(ty) {\n      mapped_ty = RUST_TYPE_MAP[ty];\n    }\n\n    if let Some(ref category) = field.bracket_category {\n      match category {\n        BracketCategory::Opt => match &field.bracket_inner_ty {\n          None => {},\n          Some(inner_ty) => match inner_ty.to_string().as_str() {\n            //TODO: support hashmap or something else wrapped by Option\n            \"Vec\" => {\n              self.fields.push(format!(\n                \"oneof one_of_{} {{ bytes {} = {}; }};\",\n                name, name, index\n              ));\n            },\n            _ => {\n              self.fields.push(format!(\n                \"oneof one_of_{} {{ {} {} = {}; }};\",\n                name, mapped_ty, name, index\n              ));\n            },\n          },\n        },\n        BracketCategory::Map((k, v)) => {\n          let key: &str = k;\n          let value: &str = v;\n          self.fields.push(format!(\n            // map<string, string> attrs = 1;\n            \"map<{}, {}> {} = {};\",\n            RUST_TYPE_MAP.get(key).unwrap_or(&key),\n            RUST_TYPE_MAP.get(value).unwrap_or(&value),\n            name,\n            index\n          ));\n        },\n        BracketCategory::Vec => {\n          let bracket_ty: &str = &field.bracket_ty.as_ref().unwrap().to_string();\n          // Vec<u8>\n          if mapped_ty == \"u8\" && bracket_ty == \"Vec\" {\n            self.fields.push(format!(\"bytes {} = {};\", name, index))\n          } else {\n            self.fields.push(format!(\n              \"{} {} {} = {};\",\n              RUST_TYPE_MAP[bracket_ty], mapped_ty, name, index\n            ))\n          }\n        },\n        BracketCategory::Other => self\n          .fields\n          .push(format!(\"{} {} = {};\", mapped_ty, name, index)),\n      }\n    }\n  }\n\n  pub fn render(&mut self) -> Option<String> {\n    self.context.insert(\"fields\", &self.fields);\n    let tera = get_tera(\"protobuf_file/template/proto_file\");\n    match tera.render(\"struct.tera\", &self.context) {\n      Ok(r) => Some(r),\n      Err(e) => {\n        log::error!(\"{:?}\", e);\n        None\n      },\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/event_template.rs",
    "content": "use crate::util::get_tera;\nuse tera::Context;\n\npub struct EventTemplate {\n  tera_context: Context,\n}\n\npub struct EventRenderContext {\n  pub input_deserializer: Option<String>,\n  pub output_deserializer: Option<String>,\n  pub error_deserializer: String,\n  pub event: String,\n  pub event_ty: String,\n  pub prefix: String,\n}\n\n#[allow(dead_code)]\nimpl EventTemplate {\n  pub fn new() -> Self {\n    EventTemplate {\n      tera_context: Context::new(),\n    }\n  }\n\n  pub fn render(&mut self, ctx: EventRenderContext, index: usize) -> Option<String> {\n    self.tera_context.insert(\"index\", &index);\n    let event_func_name = format!(\"{}{}\", ctx.event_ty, ctx.event);\n    self\n      .tera_context\n      .insert(\"event_func_name\", &event_func_name);\n    self\n      .tera_context\n      .insert(\"event_name\", &format!(\"{}.{}\", ctx.prefix, ctx.event_ty));\n    self.tera_context.insert(\"event\", &ctx.event);\n\n    self\n      .tera_context\n      .insert(\"has_input\", &ctx.input_deserializer.is_some());\n    match ctx.input_deserializer {\n      None => {},\n      Some(ref input) => self\n        .tera_context\n        .insert(\"input_deserializer\", &format!(\"{}.{}\", ctx.prefix, input)),\n    }\n\n    let has_output = ctx.output_deserializer.is_some();\n    self.tera_context.insert(\"has_output\", &has_output);\n\n    match ctx.output_deserializer {\n      None => self.tera_context.insert(\"output_deserializer\", \"void\"),\n      Some(ref output) => self\n        .tera_context\n        .insert(\"output_deserializer\", &format!(\"{}.{}\", ctx.prefix, output)),\n    }\n\n    self.tera_context.insert(\n      \"error_deserializer\",\n      &format!(\"{}.{}\", ctx.prefix, ctx.error_deserializer),\n    );\n\n    let tera = get_tera(\"ts_event\");\n    match tera.render(\"event_template.tera\", &self.tera_context) {\n      Ok(r) => Some(r),\n      Err(e) => {\n        log::error!(\"{:?}\", e);\n        None\n      },\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/event_template.tera",
    "content": "\n{%- if has_input  %}\nexport async function {{ event_func_name }}(payload: {{ input_deserializer }}): Promise<Result<{{ output_deserializer }}, {{ error_deserializer }}>> {\n{%- else %}\nexport async function {{ event_func_name }}(): Promise<Result<{{ output_deserializer }}, {{ error_deserializer }}>> {\n{%- endif %}\n    {%- if has_input  %}\n    let args = {\n        request: {\n            ty: {{ event_name }}[{{ event_name }}.{{ event }}],\n            payload: Array.from(payload.serializeBinary()),\n        },\n    };\n    {%- else %}\n    let args = {\n        request: {\n            ty: {{ event_name }}[{{ event_name }}.{{ event }}],\n            payload: Array.from([]),\n        },\n    };\n    {%- endif %}\n\n    let result: { code: number; payload: Uint8Array } = await invoke(\"invoke_request\", args);\n    if (result.code == 0) {\n    {%- if has_output  %}\n        let object = {{ output_deserializer }}.deserializeBinary(result.payload);\n        return Ok(object);\n    {%- else %}\n        return Ok.EMPTY;\n    {%- endif %}\n    } else {\n        let error = {{ error_deserializer }}.deserializeBinary(result.payload);\n        console.log({{ event_func_name }}.name, error);\n        return Err(error);\n    }\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs",
    "content": "mod event_template;\n\nuse crate::Project;\nuse crate::ast::EventASTContext;\nuse crate::flowy_toml::{CrateConfig, parse_crate_config_from};\nuse crate::ts_event::event_template::{EventRenderContext, EventTemplate};\nuse crate::util::{is_crate_dir, is_hidden, path_string_with_component, read_file};\nuse flowy_ast::ASTResult;\nuse std::collections::HashSet;\nuse std::fs::File;\nuse std::io::Write;\nuse std::path::PathBuf;\nuse syn::Item;\nuse walkdir::WalkDir;\n\npub fn r#gen(dest_folder_name: &str, project: Project) {\n  let root = project.event_root();\n  let backend_service_path = project.dst();\n\n  let crate_path = std::fs::canonicalize(\".\")\n    .unwrap()\n    .as_path()\n    .display()\n    .to_string();\n  let event_crates = parse_ts_event_files(vec![crate_path]);\n  let event_ast = event_crates\n    .iter()\n    .flat_map(parse_event_crate)\n    .collect::<Vec<_>>();\n\n  let event_render_ctx = ast_to_event_render_ctx(event_ast.as_ref());\n  let mut render_result = project.event_imports();\n\n  for (index, render_ctx) in event_render_ctx.into_iter().enumerate() {\n    let mut event_template = EventTemplate::new();\n\n    if let Some(content) = event_template.render(render_ctx, index) {\n      render_result.push_str(content.as_ref())\n    }\n  }\n  render_result.push_str(TS_FOOTER);\n\n  let ts_event_folder: PathBuf = [&root, &backend_service_path, \"events\", dest_folder_name]\n    .iter()\n    .collect();\n  if !ts_event_folder.as_path().exists() {\n    std::fs::create_dir_all(ts_event_folder.as_path()).unwrap();\n  }\n\n  let event_file = \"event\";\n  let event_file_ext = \"ts\";\n  let ts_event_file_path = path_string_with_component(\n    &ts_event_folder,\n    vec![&format!(\"{}.{}\", event_file, event_file_ext)],\n  );\n  println!(\"cargo:rerun-if-changed={}\", ts_event_file_path);\n\n  match std::fs::OpenOptions::new()\n    .create(true)\n    .write(true)\n    .append(false)\n    .truncate(true)\n    .open(&ts_event_file_path)\n  {\n    Ok(ref mut file) => {\n      file.write_all(render_result.as_bytes()).unwrap();\n      File::flush(file).unwrap();\n    },\n    Err(err) => {\n      panic!(\"Failed to open file: {}, {:?}\", ts_event_file_path, err);\n    },\n  }\n\n  let ts_index = path_string_with_component(&ts_event_folder, vec![\"index.ts\"]);\n  match std::fs::OpenOptions::new()\n    .create(true)\n    .write(true)\n    .append(false)\n    .truncate(true)\n    .open(ts_index)\n  {\n    Ok(ref mut file) => {\n      let mut export = String::new();\n      export.push_str(\"// Auto-generated, do not edit \\n\");\n      export.push_str(&format!(\n        \"export * from '../../models/{}';\\n\",\n        dest_folder_name\n      ));\n      export.push_str(&format!(\"export * from './{}';\\n\", event_file));\n      file.write_all(export.as_bytes()).unwrap();\n      File::flush(file).unwrap();\n    },\n    Err(err) => {\n      panic!(\"Failed to open file: {}\", err);\n    },\n  }\n}\n\n#[derive(Debug)]\npub struct TsEventCrate {\n  crate_path: PathBuf,\n  event_files: Vec<String>,\n}\n\nimpl TsEventCrate {\n  pub fn from_config(config: &CrateConfig) -> Self {\n    TsEventCrate {\n      crate_path: config.crate_path.clone(),\n      event_files: config.flowy_config.event_files.clone(),\n    }\n  }\n}\n\npub fn parse_ts_event_files(crate_paths: Vec<String>) -> Vec<TsEventCrate> {\n  let mut ts_event_crates: Vec<TsEventCrate> = vec![];\n  crate_paths.iter().for_each(|path| {\n    let crates = WalkDir::new(path)\n      .into_iter()\n      .filter_entry(|e| !is_hidden(e))\n      .filter_map(|e| e.ok())\n      .filter(is_crate_dir)\n      .flat_map(|e| parse_crate_config_from(&e))\n      .map(|crate_config| TsEventCrate::from_config(&crate_config))\n      .collect::<Vec<TsEventCrate>>();\n    ts_event_crates.extend(crates);\n  });\n  ts_event_crates\n}\n\npub fn parse_event_crate(event_crate: &TsEventCrate) -> Vec<EventASTContext> {\n  event_crate\n    .event_files\n    .iter()\n    .flat_map(|event_file| {\n      let file_path =\n        path_string_with_component(&event_crate.crate_path, vec![event_file.as_str()]);\n\n      let file_content = read_file(file_path.as_ref()).unwrap();\n      let ast = syn::parse_file(file_content.as_ref()).expect(\"Unable to parse file\");\n      ast\n        .items\n        .iter()\n        .flat_map(|item| match item {\n          Item::Enum(item_enum) => {\n            let ast_result = ASTResult::new();\n            let attrs = flowy_ast::enum_from_ast(\n              &ast_result,\n              &item_enum.ident,\n              &item_enum.variants,\n              &item_enum.attrs,\n            );\n            ast_result.check().unwrap();\n            attrs\n              .iter()\n              .filter(|attr| !attr.attrs.event_attrs.ignore)\n              .map(|variant| EventASTContext::from(&variant.attrs))\n              .collect::<Vec<_>>()\n          },\n          _ => vec![],\n        })\n        .collect::<Vec<_>>()\n    })\n    .collect::<Vec<EventASTContext>>()\n}\n\npub fn ast_to_event_render_ctx(ast: &[EventASTContext]) -> Vec<EventRenderContext> {\n  let mut import_objects = HashSet::new();\n  ast.iter().for_each(|event_ast| {\n    if let Some(input) = event_ast.event_input.as_ref() {\n      import_objects.insert(input.get_ident().unwrap().to_string());\n    }\n    if let Some(output) = event_ast.event_output.as_ref() {\n      import_objects.insert(output.get_ident().unwrap().to_string());\n    }\n  });\n\n  ast\n    .iter()\n    .map(|event_ast| {\n      let input_deserializer = event_ast\n        .event_input\n        .as_ref()\n        .map(|event_input| event_input.get_ident().unwrap().to_string());\n\n      let output_deserializer = event_ast\n        .event_output\n        .as_ref()\n        .map(|event_output| event_output.get_ident().unwrap().to_string());\n\n      EventRenderContext {\n        input_deserializer,\n        output_deserializer,\n        error_deserializer: event_ast.event_error.to_string(),\n        event: event_ast.event.to_string(),\n        event_ty: event_ast.event_ty.to_string(),\n        prefix: \"pb\".to_string(),\n      }\n    })\n    .collect::<Vec<EventRenderContext>>()\n}\n\nconst TS_FOOTER: &str = r#\"\n\"#;\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-codegen/src/util.rs",
    "content": "use console::Style;\nuse similar::{ChangeTag, TextDiff};\nuse std::path::{Path, PathBuf};\nuse std::str::FromStr;\nuse std::{\n  fs::{File, OpenOptions},\n  io::{Read, Write},\n};\nuse tera::Tera;\nuse walkdir::WalkDir;\n\npub fn read_file(path: &str) -> Option<String> {\n  let mut file = File::open(path).unwrap_or_else(|_| panic!(\"Unable to open file at {}\", path));\n  let mut content = String::new();\n  match file.read_to_string(&mut content) {\n    Ok(_) => Some(content),\n    Err(e) => {\n      log::error!(\"{}, with error: {:?}\", path, e);\n      Some(\"\".to_string())\n    },\n  }\n}\n\npub fn save_content_to_file_with_diff_prompt(content: &str, output_file: &str) {\n  if Path::new(output_file).exists() {\n    let old_content = read_file(output_file).unwrap();\n    let new_content = content.to_owned();\n    let write_to_file = || match OpenOptions::new()\n      .create(true)\n      .write(true)\n      .append(false)\n      .truncate(true)\n      .open(output_file)\n    {\n      Ok(ref mut file) => {\n        file.write_all(new_content.as_bytes()).unwrap();\n      },\n      Err(err) => {\n        panic!(\"Failed to open log file: {}\", err);\n      },\n    };\n    if new_content != old_content {\n      print_diff(old_content, new_content.clone());\n      write_to_file()\n    }\n  } else {\n    match OpenOptions::new()\n      .create(true)\n      .truncate(true)\n      .write(true)\n      .open(output_file)\n    {\n      Ok(ref mut file) => file.write_all(content.as_bytes()).unwrap(),\n      Err(err) => panic!(\"Open or create to {} fail: {}\", output_file, err),\n    }\n  }\n}\n\npub fn print_diff(old_content: String, new_content: String) {\n  let diff = TextDiff::from_lines(&old_content, &new_content);\n  for op in diff.ops() {\n    for change in diff.iter_changes(op) {\n      let (sign, style) = match change.tag() {\n        ChangeTag::Delete => (\"-\", Style::new().red()),\n        ChangeTag::Insert => (\"+\", Style::new().green()),\n        ChangeTag::Equal => (\" \", Style::new()),\n      };\n\n      match change.tag() {\n        ChangeTag::Delete => {\n          print!(\"{}{}\", style.apply_to(sign).bold(), style.apply_to(change));\n        },\n        ChangeTag::Insert => {\n          print!(\"{}{}\", style.apply_to(sign).bold(), style.apply_to(change));\n        },\n        ChangeTag::Equal => {},\n      };\n    }\n    println!(\"---------------------------------------------------\");\n  }\n}\n\n#[allow(dead_code)]\npub fn is_crate_dir(e: &walkdir::DirEntry) -> bool {\n  let cargo = e.path().file_stem().unwrap().to_str().unwrap().to_string();\n  cargo == *\"Cargo\"\n}\n\n#[allow(dead_code)]\npub fn is_proto_file(e: &walkdir::DirEntry) -> bool {\n  if e.path().extension().is_none() {\n    return false;\n  }\n  let ext = e.path().extension().unwrap().to_str().unwrap().to_string();\n  ext == *\"proto\"\n}\n\npub fn is_hidden(entry: &walkdir::DirEntry) -> bool {\n  entry\n    .file_name()\n    .to_str()\n    .map(|s| s.starts_with('.'))\n    .unwrap_or(false)\n}\n\npub fn create_dir_if_not_exist(dir: &Path) {\n  if !dir.exists() {\n    std::fs::create_dir_all(dir).unwrap();\n  }\n}\n\npub fn path_string_with_component(path: &Path, components: Vec<&str>) -> String {\n  path_buf_with_component(path, components)\n    .to_str()\n    .unwrap()\n    .to_string()\n}\n\n#[allow(dead_code)]\npub fn path_buf_with_component(path: &Path, components: Vec<&str>) -> PathBuf {\n  let mut path_buf = path.to_path_buf();\n  for component in components {\n    path_buf.push(component);\n  }\n  path_buf\n}\n\n#[allow(dead_code)]\npub fn walk_dir<P: AsRef<Path>, F1, F2>(dir: P, filter: F2, mut path_and_name: F1)\nwhere\n  F1: FnMut(String, String),\n  F2: Fn(&walkdir::DirEntry) -> bool,\n{\n  for (path, name) in WalkDir::new(dir)\n    .into_iter()\n    .filter_map(|e| e.ok())\n    .filter(|e| filter(e))\n    .map(|e| {\n      (\n        e.path().to_str().unwrap().to_string(),\n        e.path().file_stem().unwrap().to_str().unwrap().to_string(),\n      )\n    })\n  {\n    path_and_name(path, name);\n  }\n}\n\n#[allow(dead_code)]\npub fn suffix_relative_to_path(path: &str, base: &str) -> String {\n  let base = Path::new(base);\n  let path = Path::new(path);\n  path\n    .strip_prefix(base)\n    .unwrap()\n    .to_str()\n    .unwrap()\n    .to_owned()\n}\n\npub fn get_tera(directory: &str) -> Tera {\n  let mut root = format!(\"{}/src/\", env!(\"CARGO_MANIFEST_DIR\"));\n  root.push_str(directory);\n\n  let root_absolute_path = match std::fs::canonicalize(&root) {\n    Ok(p) => p.as_path().display().to_string(),\n    Err(e) => {\n      panic!(\"❌ Canonicalize file path {} failed {:?}\", root, e);\n    },\n  };\n\n  let mut template_path = format!(\"{}/**/*.tera\", root_absolute_path);\n  if cfg!(windows) {\n    // remove \"\\\\?\\\" prefix on windows\n    template_path = format!(\"{}/**/*.tera\", &root_absolute_path[4..]);\n  }\n\n  match Tera::new(template_path.as_ref()) {\n    Ok(t) => t,\n    Err(e) => {\n      log::error!(\"Parsing error(s): {}\", e);\n      ::std::process::exit(1);\n    },\n  }\n}\n\npub fn cache_dir() -> PathBuf {\n  let mut path_buf = PathBuf::from_str(env!(\"CARGO_MANIFEST_DIR\")).unwrap();\n  path_buf.push(\".cache\");\n  path_buf\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-derive/.gitignore",
    "content": "/target\nCargo.lock\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-derive/Cargo.toml",
    "content": "[package]\nname = \"flowy-derive\"\nversion = \"0.1.0\"\nedition = \"2018\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[lib]\nproc-macro = true\nname = \"flowy_derive\"\n\n[dependencies]\nsyn = { version = \"1.0.109\", features = [\"extra-traits\", \"visit\"] }\nquote = \"1.0\"\nproc-macro2 = \"1.0\"\nflowy-ast.workspace = true\nlazy_static = { version = \"1.4.0\" }\ndashmap.workspace = true\nflowy-codegen.workspace = true\nserde_json.workspace = true\nwalkdir = \"2.3.2\"\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-derive/src/dart_event/mod.rs",
    "content": "use proc_macro2::TokenStream;\n\n// #[proc_macro_derive(DartEvent, attributes(event_ty))]\npub fn expand_enum_derive(_input: &syn::DeriveInput) -> Result<TokenStream, Vec<syn::Error>> {\n  Ok(TokenStream::default())\n}\n\n// use flowy_ast::{ASTContainer, Ctxt};\n// use proc_macro2::TokenStream;\n//\n// // #[proc_macro_derive(DartEvent, attributes(event_ty))]\n// pub fn expand_enum_derive(input: &syn::DeriveInput) -> Result<TokenStream,\n// Vec<syn::Error>> {     let ctxt = Ctxt::new();\n//     let cont = match ASTContainer::from_ast(&ctxt, input) {\n//         Some(cont) => cont,\n//         None => return Err(ctxt.check().unwrap_err()),\n//     };\n//\n//     let enum_ident = &cont.ident;\n//     let pb_enum = cont.attrs.pb_enum_type().unwrap();\n//\n//     let build_display_pb_enum = cont.data.all_idents().map(|i| {\n//         let a = format_ident!(\"{}\", i.to_string());\n//         let token_stream: TokenStream = quote! {\n//             #enum_ident::#i => f.write_str(&#a)?,\n//         };\n//         token_stream\n//     });\n//\n//     ctxt.check()?;\n//\n//     Ok(quote! {\n//         impl std::fmt::Display for #enum_ident {\n//            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result\n// {                 match self {\n//                     #(#build_display_pb_enum)*\n//                 }\n//             }\n//         }\n//     })\n// }\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-derive/src/lib.rs",
    "content": "// https://docs.rs/syn/1.0.48/syn/struct.DeriveInput.html\nextern crate proc_macro;\n\nuse proc_macro::TokenStream;\nuse quote::quote;\nuse syn::{parse_macro_input, DeriveInput};\n\n#[macro_use]\nextern crate quote;\n\nmod dart_event;\nmod node;\nmod proto_buf;\n\n// Inspired by https://serde.rs/attributes.html\n#[proc_macro_derive(ProtoBuf, attributes(pb))]\npub fn derive_proto_buf(input: TokenStream) -> TokenStream {\n  let input = parse_macro_input!(input as DeriveInput);\n  proto_buf::expand_derive(&input)\n    .unwrap_or_else(to_compile_errors)\n    .into()\n}\n\n#[proc_macro_derive(ProtoBuf_Enum, attributes(pb))]\npub fn derive_proto_buf_enum(input: TokenStream) -> TokenStream {\n  let input = parse_macro_input!(input as DeriveInput);\n  proto_buf::expand_enum_derive(&input)\n    .unwrap_or_else(to_compile_errors)\n    .into()\n}\n\n#[proc_macro_derive(Flowy_Event, attributes(event, event_err))]\npub fn derive_dart_event(input: TokenStream) -> TokenStream {\n  let input = parse_macro_input!(input as DeriveInput);\n  dart_event::expand_enum_derive(&input)\n    .unwrap_or_else(to_compile_errors)\n    .into()\n}\n\n#[proc_macro_derive(Node, attributes(node, nodes, node_type))]\npub fn derive_node(input: TokenStream) -> TokenStream {\n  let input = parse_macro_input!(input as DeriveInput);\n  node::expand_derive(&input)\n    .unwrap_or_else(to_compile_errors)\n    .into()\n}\n\nfn to_compile_errors(errors: Vec<syn::Error>) -> proc_macro2::TokenStream {\n  let compile_errors = errors.iter().map(syn::Error::to_compile_error);\n  quote!(#(#compile_errors)*)\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-derive/src/node/mod.rs",
    "content": "use flowy_ast::{ASTContainer, ASTField, ASTResult};\nuse proc_macro2::TokenStream;\n\npub fn expand_derive(input: &syn::DeriveInput) -> Result<TokenStream, Vec<syn::Error>> {\n  let ast_result = ASTResult::new();\n  let cont = match ASTContainer::from_ast(&ast_result, input) {\n    Some(cont) => cont,\n    None => return Err(ast_result.check().unwrap_err()),\n  };\n\n  let mut token_stream: TokenStream = TokenStream::default();\n  token_stream.extend(make_helper_funcs_token_stream(&cont));\n  token_stream.extend(make_to_node_data_token_stream(&cont));\n\n  if let Some(get_value_token_stream) = make_get_set_value_token_steam(&cont) {\n    token_stream.extend(get_value_token_stream);\n  }\n\n  token_stream.extend(make_alter_children_token_stream(&ast_result, &cont));\n  ast_result.check()?;\n  Ok(token_stream)\n}\n\npub fn make_helper_funcs_token_stream(ast: &ASTContainer) -> TokenStream {\n  let mut token_streams = TokenStream::default();\n  let struct_ident = &ast.ident;\n  token_streams.extend(quote! {\n    impl #struct_ident {\n          pub fn get_path(&self) -> Option<Path> {\n              let node_id = &self.node_id?;\n             Some(self.tree.read().path_from_node_id(node_id.clone()))\n          }\n      }\n  });\n  token_streams\n}\n\npub fn make_alter_children_token_stream(ast_result: &ASTResult, ast: &ASTContainer) -> TokenStream {\n  let mut token_streams = TokenStream::default();\n  let children_fields = ast\n    .data\n    .all_fields()\n    .filter(|field| field.node_attrs.has_child)\n    .collect::<Vec<&ASTField>>();\n\n  if !children_fields.is_empty() {\n    let struct_ident = &ast.ident;\n    if children_fields.len() > 1 {\n      ast_result.error_spanned_by(struct_ident, \"Only one children property\");\n      return token_streams;\n    }\n    let children_field = children_fields.first().unwrap();\n    let field_name = children_field.name().unwrap();\n    let child_name = children_field.node_attrs.child_name.as_ref().unwrap();\n    let get_func_name = format_ident!(\"get_{}\", child_name.value());\n    let get_mut_func_name = format_ident!(\"get_mut_{}\", child_name.value());\n    let add_func_name = format_ident!(\"add_{}\", child_name.value());\n    let remove_func_name = format_ident!(\"remove_{}\", child_name.value());\n    let ty = children_field.bracket_inner_ty.as_ref().unwrap().clone();\n\n    token_streams.extend(quote! {\n             impl #struct_ident {\n                pub fn #get_func_name<T: AsRef<str>>(&self, id: T) -> Option<&#ty> {\n                    let id = id.as_ref();\n                    self.#field_name.iter().find(|element| element.id == id)\n                }\n\n                pub fn #get_mut_func_name<T: AsRef<str>>(&mut self, id: T) -> Option<&mut #ty> {\n                    let id = id.as_ref();\n                    self.#field_name.iter_mut().find(|element| element.id == id)\n                }\n\n                pub fn #remove_func_name<T: AsRef<str>>(&mut self, id: T) {\n                    let id = id.as_ref();\n                     if let Some(index) = self.#field_name.iter().position(|element| element.id == id && element.node_id.is_some()) {\n                        let element = self.#field_name.remove(index);\n                        let element_path = element.get_path().unwrap();\n\n                        let mut write_guard = self.tree.write();\n                        let mut nodes = vec![];\n\n                        if let Some(node_data) = element.node_id.and_then(|node_id| write_guard.get_node_data(node_id.clone())) {\n                            nodes.push(node_data);\n                        }\n                        let _ = write_guard.apply_op(NodeOperation::Delete {\n                            path: element_path,\n                            nodes,\n                        });\n                    }\n                }\n\n                pub fn #add_func_name(&mut self, mut value: #ty) -> Result<(), String> {\n                    if self.node_id.is_none() {\n                        return Err(\"The node id is empty\".to_owned());\n                    }\n\n                    let mut transaction = Transaction::new();\n                    let parent_path = self.get_path().unwrap();\n\n                    let path = parent_path.clone_with(self.#field_name.len());\n                    let node_data = value.to_node_data();\n                    transaction.push_operation(NodeOperation::Insert {\n                        path: path.clone(),\n                        nodes: vec![node_data],\n                     });\n\n                    let _ = self.tree.write().apply_transaction(transaction);\n                    let child_node_id = self.tree.read().node_id_at_path(path).unwrap();\n                    value.node_id = Some(child_node_id);\n                    self.#field_name.push(value);\n                    Ok(())\n                }\n             }\n        });\n  }\n\n  token_streams\n}\n\npub fn make_to_node_data_token_stream(ast: &ASTContainer) -> TokenStream {\n  let struct_ident = &ast.ident;\n  let mut token_streams = TokenStream::default();\n  let node_type = ast\n    .node_type\n    .as_ref()\n    .expect(\"Define the type of the node by using #[node_type = \\\"xx\\\" in the struct\");\n  let set_key_values = ast\n    .data\n    .all_fields()\n    .filter(|field| !field.node_attrs.has_child)\n    .flat_map(|field| {\n      let mut field_name = field\n        .name()\n        .expect(\"the name of the field should not be empty\");\n      let original_field_name = field\n        .name()\n        .expect(\"the name of the field should not be empty\");\n      if let Some(rename) = &field.node_attrs.rename {\n        field_name = format_ident!(\"{}\", rename.value());\n      }\n      let field_name_str = field_name.to_string();\n      quote! {\n         .insert_attribute(#field_name_str, self.#original_field_name.clone())\n      }\n    });\n\n  let children_fields = ast\n    .data\n    .all_fields()\n    .filter(|field| field.node_attrs.has_child)\n    .collect::<Vec<&ASTField>>();\n\n  let childrens_token_streams = match children_fields.is_empty() {\n    true => {\n      quote! {\n          let children = vec![];\n      }\n    },\n    false => {\n      let children_field = children_fields.first().unwrap();\n      let original_field_name = children_field\n        .name()\n        .expect(\"the name of the field should not be empty\");\n      quote! {\n          let children = self.#original_field_name.iter().map(|value| value.to_node_data()).collect::<Vec<NodeData>>();\n      }\n    },\n  };\n\n  token_streams.extend(quote! {\n    impl ToNodeData for #struct_ident {\n          fn to_node_data(&self) -> NodeData {\n              #childrens_token_streams\n\n              let builder = NodeDataBuilder::new(#node_type)\n              #(#set_key_values)*\n              .extend_node_data(children);\n\n              builder.build()\n          }\n      }\n  });\n\n  token_streams\n}\n\npub fn make_get_set_value_token_steam(ast: &ASTContainer) -> Option<TokenStream> {\n  let struct_ident = &ast.ident;\n  let mut token_streams = TokenStream::default();\n\n  let tree = format_ident!(\"tree\");\n  for field in ast.data.all_fields() {\n    if field.node_attrs.has_child {\n      continue;\n    }\n\n    let mut field_name = field\n      .name()\n      .expect(\"the name of the field should not be empty\");\n    if let Some(rename) = &field.node_attrs.rename {\n      field_name = format_ident!(\"{}\", rename.value());\n    }\n\n    let field_name_str = field_name.to_string();\n    let get_func_name = format_ident!(\"get_{}\", field_name);\n    let set_func_name = format_ident!(\"set_{}\", field_name);\n    let get_value_return_ty = field.ty;\n    let set_value_input_ty = field.ty;\n\n    if let Some(get_value_with_fn) = &field.node_attrs.get_node_value_with {\n      token_streams.extend(quote! {\n        impl #struct_ident {\n              pub fn #get_func_name(&self) -> Option<#get_value_return_ty> {\n                  let node_id = self.node_id.as_ref()?;\n                  #get_value_with_fn(self.#tree.clone(), node_id, #field_name_str)\n              }\n          }\n      });\n    }\n\n    if let Some(set_value_with_fn) = &field.node_attrs.set_node_value_with {\n      token_streams.extend(quote! {\n              impl #struct_ident {\n                    pub fn #set_func_name(&self, value: #set_value_input_ty) {\n                        if let Some(node_id) = self.node_id.as_ref() {\n                            let _ = #set_value_with_fn(self.#tree.clone(), node_id, #field_name_str, value);\n                        }\n                    }\n                }\n            });\n    }\n  }\n  Some(token_streams)\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-derive/src/proto_buf/deserialize.rs",
    "content": "use proc_macro2::{Span, TokenStream};\n\nuse flowy_ast::*;\n\nuse crate::proto_buf::util::*;\n\npub fn make_de_token_steam(ast_result: &ASTResult, ast: &ASTContainer) -> Option<TokenStream> {\n  let pb_ty = ast.pb_attrs.pb_struct_type()?;\n  let struct_ident = &ast.ident;\n\n  let build_take_fields = ast\n    .data\n    .all_fields()\n    .filter(|f| !f.pb_attrs.skip_pb_deserializing())\n    .flat_map(|field| {\n      if let Some(func) = field.pb_attrs.deserialize_pb_with() {\n        let member = &field.member;\n        Some(quote! { o.#member=#struct_ident::#func(pb); })\n      } else if field.pb_attrs.is_one_of() {\n        token_stream_for_one_of(ast_result, field)\n      } else {\n        token_stream_for_field(ast_result, &field.member, field.ty, false)\n      }\n    });\n\n  let de_token_stream: TokenStream = quote! {\n      impl std::convert::TryFrom<bytes::Bytes> for #struct_ident {\n          type Error = ::protobuf::ProtobufError;\n          fn try_from(bytes: bytes::Bytes) -> Result<Self, Self::Error> {\n              Self::try_from(&bytes)\n          }\n      }\n\n      impl std::convert::TryFrom<&bytes::Bytes> for #struct_ident {\n          type Error = ::protobuf::ProtobufError;\n          fn try_from(bytes: &bytes::Bytes) -> Result<Self, Self::Error> {\n              let pb: crate::protobuf::#pb_ty = ::protobuf::Message::parse_from_bytes(bytes)?;\n              Ok(#struct_ident::from(pb))\n          }\n      }\n\n      impl std::convert::TryFrom<&[u8]> for #struct_ident {\n          type Error = ::protobuf::ProtobufError;\n          fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {\n              let pb: crate::protobuf::#pb_ty = ::protobuf::Message::parse_from_bytes(bytes)?;\n              Ok(#struct_ident::from(pb))\n          }\n      }\n\n      impl std::convert::From<crate::protobuf::#pb_ty> for #struct_ident {\n          fn from(mut pb: crate::protobuf::#pb_ty) -> Self {\n              let mut o = Self::default();\n              #(#build_take_fields)*\n              o\n          }\n      }\n  };\n\n  Some(de_token_stream)\n  // None\n}\n\nfn token_stream_for_one_of(ast_result: &ASTResult, field: &ASTField) -> Option<TokenStream> {\n  let member = &field.member;\n  let ident = get_member_ident(ast_result, member)?;\n  let ty_info = match parse_ty(ast_result, field.ty) {\n    Ok(ty_info) => ty_info,\n    Err(e) => {\n      eprintln!(\n        \"token_stream_for_one_of failed: {:?} with error: {}\",\n        member, e\n      );\n      panic!();\n    },\n  }?;\n  let bracketed_ty_info = ty_info.bracket_ty_info.as_ref().as_ref();\n  let has_func = format_ident!(\"has_{}\", ident.to_string());\n  match ident_category(bracketed_ty_info.unwrap().ident) {\n    TypeCategory::Enum => {\n      let get_func = format_ident!(\"get_{}\", ident.to_string());\n      let ty = bracketed_ty_info.unwrap().ty;\n      Some(quote! {\n          if pb.#has_func() {\n              let enum_de_from_pb = #ty::from(&pb.#get_func());\n              o.#member = Some(enum_de_from_pb);\n          }\n      })\n    },\n    TypeCategory::Primitive => {\n      let get_func = format_ident!(\"get_{}\", ident.to_string());\n      Some(quote! {\n          if pb.#has_func() {\n              o.#member=Some(pb.#get_func());\n          }\n      })\n    },\n    TypeCategory::Str => {\n      let take_func = format_ident!(\"take_{}\", ident.to_string());\n      Some(quote! {\n          if pb.#has_func() {\n              o.#member=Some(pb.#take_func());\n          }\n      })\n    },\n    TypeCategory::Array => {\n      let take_func = format_ident!(\"take_{}\", ident.to_string());\n      Some(quote! {\n          if pb.#has_func() {\n              o.#member=Some(pb.#take_func());\n          }\n      })\n    },\n    _ => {\n      let take_func = format_ident!(\"take_{}\", ident.to_string());\n      let ty = bracketed_ty_info.unwrap().ty;\n      Some(quote! {\n          if pb.#has_func() {\n              let val = #ty::from(pb.#take_func());\n              o.#member=Some(val);\n          }\n      })\n    },\n  }\n}\n\nfn token_stream_for_field(\n  ast_result: &ASTResult,\n  member: &syn::Member,\n  ty: &syn::Type,\n  is_option: bool,\n) -> Option<TokenStream> {\n  let ident = get_member_ident(ast_result, member)?;\n  let ty_info = match parse_ty(ast_result, ty) {\n    Ok(ty_info) => ty_info,\n    Err(e) => {\n      eprintln!(\"token_stream_for_field: {:?} with error: {}\", member, e);\n      panic!()\n    },\n  }?;\n  match ident_category(ty_info.ident) {\n    TypeCategory::Array => {\n      assert_bracket_ty_is_some(ast_result, &ty_info);\n      token_stream_for_vec(ast_result, member, &ty_info.bracket_ty_info.unwrap())\n    },\n    TypeCategory::Map => {\n      assert_bracket_ty_is_some(ast_result, &ty_info);\n      token_stream_for_map(ast_result, member, &ty_info.bracket_ty_info.unwrap())\n    },\n    TypeCategory::Protobuf => {\n      // if the type wrapped by SingularPtrField, should call take first\n      let take = syn::Ident::new(\"take\", Span::call_site());\n      // inner_type_ty would be the type of the field. (e.g value of AnyData)\n      let ty = ty_info.ty;\n      Some(quote! {\n          let some_value = pb.#member.#take();\n          if some_value.is_some() {\n              let struct_de_from_pb = #ty::from(some_value.unwrap());\n              o.#member = struct_de_from_pb;\n          }\n      })\n    },\n\n    TypeCategory::Enum => {\n      let ty = ty_info.ty;\n      Some(quote! {\n          let enum_de_from_pb = #ty::from(&pb.#member);\n           o.#member = enum_de_from_pb;\n\n      })\n    },\n    TypeCategory::Str => {\n      let take_ident = syn::Ident::new(&format!(\"take_{}\", ident), Span::call_site());\n      if is_option {\n        Some(quote! {\n            if pb.#member.is_empty() {\n                o.#member = None;\n            } else {\n                o.#member = Some(pb.#take_ident());\n            }\n        })\n      } else {\n        Some(quote! {\n            o.#member = pb.#take_ident();\n        })\n      }\n    },\n    TypeCategory::Opt => token_stream_for_field(\n      ast_result,\n      member,\n      ty_info.bracket_ty_info.unwrap().ty,\n      true,\n    ),\n    TypeCategory::Primitive | TypeCategory::Bytes => {\n      // eprintln!(\"😄 #{:?}\", &field.name().unwrap());\n      if is_option {\n        Some(quote! { o.#member = Some(pb.#member.clone()); })\n      } else {\n        Some(quote! { o.#member = pb.#member.clone(); })\n      }\n    },\n  }\n}\n\nfn token_stream_for_vec(\n  ctxt: &ASTResult,\n  member: &syn::Member,\n  bracketed_type: &TyInfo,\n) -> Option<TokenStream> {\n  let ident = get_member_ident(ctxt, member)?;\n\n  match ident_category(bracketed_type.ident) {\n    TypeCategory::Protobuf => {\n      let ty = bracketed_type.ty;\n      // Deserialize from pb struct of type vec, should call take_xx(), get the\n      // repeated_field and then calling the into_iter。\n      let take_ident = format_ident!(\"take_{}\", ident.to_string());\n      Some(quote! {\n          o.#member = pb.#take_ident()\n          .into_iter()\n          .map(|m| #ty::from(m))\n          .collect();\n      })\n    },\n    TypeCategory::Bytes => {\n      // Vec<u8>\n      Some(quote! {\n          o.#member = pb.#member.clone();\n      })\n    },\n    TypeCategory::Str => {\n      let take_ident = format_ident!(\"take_{}\", ident.to_string());\n      Some(quote! {\n          o.#member = pb.#take_ident().into_vec();\n      })\n    },\n    _ => {\n      let take_ident = format_ident!(\"take_{}\", ident.to_string());\n      Some(quote! {\n          o.#member = pb.#take_ident();\n      })\n    },\n  }\n}\n\nfn token_stream_for_map(\n  ast_result: &ASTResult,\n  member: &syn::Member,\n  ty_info: &TyInfo,\n) -> Option<TokenStream> {\n  let ident = get_member_ident(ast_result, member)?;\n  let take_ident = format_ident!(\"take_{}\", ident.to_string());\n  let ty = ty_info.ty;\n\n  match ident_category(ty_info.ident) {\n    TypeCategory::Protobuf => Some(quote! {\n         let mut m: std::collections::HashMap<String, #ty> = std::collections::HashMap::new();\n          pb.#take_ident().into_iter().for_each(|(k,v)| {\n                m.insert(k.clone(), #ty::from(v));\n          });\n         o.#member = m;\n    }),\n    _ => Some(quote! {\n        let mut m: std::collections::HashMap<String, #ty> = std::collections::HashMap::new();\n        pb.#take_ident().into_iter().for_each(|(k,mut v)| {\n            m.insert(k.clone(), v);\n        });\n        o.#member = m;\n    }),\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-derive/src/proto_buf/enum_serde.rs",
    "content": "use flowy_ast::*;\nuse proc_macro2::TokenStream;\n\n#[allow(dead_code)]\npub fn make_enum_token_stream(_ast_result: &ASTResult, cont: &ASTContainer) -> Option<TokenStream> {\n  let enum_ident = &cont.ident;\n  let pb_enum = cont.pb_attrs.pb_enum_type()?;\n  let build_to_pb_enum = cont.data.all_idents().map(|i| {\n    let token_stream: TokenStream = quote! {\n        #enum_ident::#i => crate::protobuf::#pb_enum::#i,\n    };\n    token_stream\n  });\n\n  let build_from_pb_enum = cont.data.all_idents().map(|i| {\n    let token_stream: TokenStream = quote! {\n        crate::protobuf::#pb_enum::#i => #enum_ident::#i,\n    };\n    token_stream\n  });\n\n  Some(quote! {\n      impl std::convert::From<&crate::protobuf::#pb_enum> for #enum_ident {\n          fn from(pb:&crate::protobuf::#pb_enum) -> Self {\n              match pb {\n                  #(#build_from_pb_enum)*\n              }\n          }\n      }\n\n      impl std::convert::From<#enum_ident> for  crate::protobuf::#pb_enum{\n          fn from(o: #enum_ident) -> crate::protobuf::#pb_enum  {\n              match o {\n                  #(#build_to_pb_enum)*\n              }\n          }\n      }\n  })\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-derive/src/proto_buf/mod.rs",
    "content": "mod deserialize;\nmod enum_serde;\nmod serialize;\nmod util;\n\nuse crate::proto_buf::{\n  deserialize::make_de_token_steam, enum_serde::make_enum_token_stream,\n  serialize::make_se_token_stream,\n};\nuse flowy_ast::*;\nuse proc_macro2::TokenStream;\nuse std::default::Default;\n\npub fn expand_derive(input: &syn::DeriveInput) -> Result<TokenStream, Vec<syn::Error>> {\n  let ast_result = ASTResult::new();\n  let cont = match ASTContainer::from_ast(&ast_result, input) {\n    Some(cont) => cont,\n    None => return Err(ast_result.check().unwrap_err()),\n  };\n\n  let mut token_stream: TokenStream = TokenStream::default();\n\n  if let Some(de_token_stream) = make_de_token_steam(&ast_result, &cont) {\n    token_stream.extend(de_token_stream);\n  }\n\n  if let Some(se_token_stream) = make_se_token_stream(&ast_result, &cont) {\n    token_stream.extend(se_token_stream);\n  }\n\n  ast_result.check()?;\n  Ok(token_stream)\n}\n\npub fn expand_enum_derive(input: &syn::DeriveInput) -> Result<TokenStream, Vec<syn::Error>> {\n  let ast_result = ASTResult::new();\n  let cont = match ASTContainer::from_ast(&ast_result, input) {\n    Some(cont) => cont,\n    None => return Err(ast_result.check().unwrap_err()),\n  };\n\n  let mut token_stream: TokenStream = TokenStream::default();\n\n  if let Some(enum_token_stream) = make_enum_token_stream(&ast_result, &cont) {\n    token_stream.extend(enum_token_stream);\n  }\n\n  ast_result.check()?;\n  Ok(token_stream)\n}\n// #[macro_use]\n// macro_rules! impl_try_for_primitive_type {\n//     ($target:ident) => {\n//         impl std::convert::TryFrom<&$target> for $target {\n//             type Error = String;\n//             fn try_from(val: &$target) -> Result<Self, Self::Error> {\n// Ok(val.clone()) }         }\n//\n//         impl std::convert::TryInto<$target> for $target {\n//             type Error = String;\n//\n//             fn try_into(self) -> Result<Self, Self::Error> { Ok(self) }\n//         }\n//     };\n// }\n//\n// impl_try_for_primitive_type!(String);\n// impl_try_for_primitive_type!(i64);\n// impl_try_for_primitive_type!(i32);\n// impl_try_for_primitive_type!(i16);\n// impl_try_for_primitive_type!(u64);\n// impl_try_for_primitive_type!(u32);\n// impl_try_for_primitive_type!(u16);\n// impl_try_for_primitive_type!(bool);\n// impl_try_for_primitive_type!(f64);\n// impl_try_for_primitive_type!(f32);\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-derive/src/proto_buf/serialize.rs",
    "content": "#![allow(clippy::while_let_on_iterator)]\n\nuse proc_macro2::TokenStream;\n\nuse flowy_ast::*;\n\nuse crate::proto_buf::util::{get_member_ident, ident_category, TypeCategory};\n\npub fn make_se_token_stream(ast_result: &ASTResult, ast: &ASTContainer) -> Option<TokenStream> {\n  let pb_ty = ast.pb_attrs.pb_struct_type()?;\n  let struct_ident = &ast.ident;\n\n  let build_set_pb_fields = ast\n    .data\n    .all_fields()\n    .filter(|f| !f.pb_attrs.skip_pb_serializing())\n    .flat_map(|field| se_token_stream_for_field(ast_result, field, false));\n\n  let se_token_stream: TokenStream = quote! {\n\n      impl std::convert::TryInto<bytes::Bytes> for #struct_ident {\n          type Error = ::protobuf::ProtobufError;\n          fn try_into(self) -> Result<bytes::Bytes, Self::Error> {\n              use protobuf::Message;\n              let pb: crate::protobuf::#pb_ty = self.into();\n              let bytes = pb.write_to_bytes()?;\n              Ok(bytes::Bytes::from(bytes))\n          }\n      }\n\n      impl std::convert::TryInto<Vec<u8>> for #struct_ident {\n          type Error = ::protobuf::ProtobufError;\n          fn try_into(self) -> Result<Vec<u8>, Self::Error> {\n              use protobuf::Message;\n              let pb: crate::protobuf::#pb_ty = self.into();\n              let bytes = pb.write_to_bytes()?;\n              Ok(bytes)\n          }\n      }\n\n      impl std::convert::From<#struct_ident> for crate::protobuf::#pb_ty {\n          fn from(mut o: #struct_ident) -> crate::protobuf::#pb_ty {\n              let mut pb = crate::protobuf::#pb_ty::new();\n              #(#build_set_pb_fields)*\n              pb\n          }\n      }\n  };\n\n  Some(se_token_stream)\n}\n\nfn se_token_stream_for_field(\n  ast_result: &ASTResult,\n  field: &ASTField,\n  _take: bool,\n) -> Option<TokenStream> {\n  if let Some(func) = &field.pb_attrs.serialize_pb_with() {\n    let member = &field.member;\n    Some(quote! { pb.#member=o.#func(); })\n  } else if field.pb_attrs.is_one_of() {\n    token_stream_for_one_of(ast_result, field)\n  } else {\n    gen_token_stream(ast_result, &field.member, field.ty, false)\n  }\n}\n\nfn token_stream_for_one_of(ast_result: &ASTResult, field: &ASTField) -> Option<TokenStream> {\n  let member = &field.member;\n  let ident = get_member_ident(ast_result, member)?;\n  let ty_info = match parse_ty(ast_result, field.ty) {\n    Ok(ty_info) => ty_info,\n    Err(e) => {\n      eprintln!(\n        \"token_stream_for_one_of failed: {:?} with error: {}\",\n        member, e\n      );\n      panic!();\n    },\n  }?;\n\n  let bracketed_ty_info = ty_info.bracket_ty_info.as_ref().as_ref();\n\n  let set_func = format_ident!(\"set_{}\", ident.to_string());\n\n  match ident_category(bracketed_ty_info.unwrap().ident) {\n    TypeCategory::Protobuf => Some(quote! {\n        match o.#member {\n            Some(s) => { pb.#set_func(s.into()) }\n            None => {}\n        }\n    }),\n    TypeCategory::Enum => Some(quote! {\n        match o.#member {\n            Some(s) => { pb.#set_func(s.into()) }\n            None => {}\n        }\n    }),\n    _ => Some(quote! {\n        match o.#member {\n            Some(ref s) => { pb.#set_func(s.clone()) }\n            None => {}\n        }\n    }),\n  }\n}\n\nfn gen_token_stream(\n  ast_result: &ASTResult,\n  member: &syn::Member,\n  ty: &syn::Type,\n  is_option: bool,\n) -> Option<TokenStream> {\n  let ty_info = match parse_ty(ast_result, ty) {\n    Ok(ty_info) => ty_info,\n    Err(e) => {\n      eprintln!(\"gen_token_stream failed: {:?} with error: {}\", member, e);\n      panic!();\n    },\n  }?;\n  match ident_category(ty_info.ident) {\n    TypeCategory::Array => {\n      token_stream_for_vec(ast_result, member, ty_info.bracket_ty_info.unwrap().ty)\n    },\n    TypeCategory::Map => {\n      token_stream_for_map(ast_result, member, ty_info.bracket_ty_info.unwrap().ty)\n    },\n    TypeCategory::Str => {\n      if is_option {\n        Some(quote! {\n            match o.#member {\n                Some(ref s) => { pb.#member = s.to_string().clone();  }\n                None => {  pb.#member = String::new(); }\n            }\n        })\n      } else {\n        Some(quote! { pb.#member = o.#member.clone(); })\n      }\n    },\n    TypeCategory::Protobuf => {\n      Some(quote! { pb.#member =  ::protobuf::SingularPtrField::some(o.#member.into()); })\n    },\n    TypeCategory::Opt => gen_token_stream(\n      ast_result,\n      member,\n      ty_info.bracket_ty_info.unwrap().ty,\n      true,\n    ),\n    TypeCategory::Enum => {\n      // let pb_enum_ident = format_ident!(\"{}\", ty_info.ident.to_string());\n      // Some(quote! {\n      // flowy_protobuf::#pb_enum_ident::from_i32(self.#member.value()).unwrap();\n      // })\n      Some(quote! {\n          pb.#member = o.#member.into();\n      })\n    },\n    _ => Some(quote! { pb.#member = o.#member; }),\n  }\n}\n\n// e.g. pub cells: Vec<CellData>, the member will be cells, ty would be Vec\nfn token_stream_for_vec(\n  ast_result: &ASTResult,\n  member: &syn::Member,\n  ty: &syn::Type,\n) -> Option<TokenStream> {\n  let ty_info = match parse_ty(ast_result, ty) {\n    Ok(ty_info) => ty_info,\n    Err(e) => {\n      eprintln!(\n        \"token_stream_for_vec failed: {:?} with error: {}\",\n        member, e\n      );\n      panic!();\n    },\n  }?;\n\n  match ident_category(ty_info.ident) {\n    TypeCategory::Protobuf => Some(quote! {\n        pb.#member = ::protobuf::RepeatedField::from_vec(\n            o.#member\n            .into_iter()\n            .map(|m| m.into())\n            .collect());\n    }),\n    TypeCategory::Bytes => Some(quote! { pb.#member = o.#member.clone(); }),\n    TypeCategory::Primitive => Some(quote! {\n        pb.#member = o.#member.clone();\n    }),\n    _ => Some(quote! {\n        pb.#member = ::protobuf::RepeatedField::from_vec(o.#member.clone());\n    }),\n  }\n}\n\n// e.g. pub cells: HashMap<xx, xx>\nfn token_stream_for_map(\n  ast_result: &ASTResult,\n  member: &syn::Member,\n  ty: &syn::Type,\n) -> Option<TokenStream> {\n  // The key of the hashmap must be string\n  let ty_info = match parse_ty(ast_result, ty) {\n    Ok(ty_info) => ty_info,\n    Err(e) => {\n      eprintln!(\n        \"token_stream_for_map failed: {:?} with error: {}\",\n        member, e\n      );\n      panic!();\n    },\n  }?;\n  let value_ty = ty_info.ty;\n  match ident_category(ty_info.ident) {\n    TypeCategory::Protobuf => Some(quote! {\n        let mut m: std::collections::HashMap<String, crate::protobuf::#value_ty> = std::collections::HashMap::new();\n        o.#member.into_iter().for_each(|(k,v)| {\n            m.insert(k.clone(), v.into());\n        });\n        pb.#member = m;\n    }),\n    _ => Some(quote! {\n        let mut m: std::collections::HashMap<String, #value_ty> = std::collections::HashMap::new();\n          o.#member.iter().for_each(|(k,v)| {\n             m.insert(k.clone(), v.clone());\n          });\n        pb.#member = m;\n    }),\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/build-tool/flowy-derive/src/proto_buf/util.rs",
    "content": "use dashmap::{DashMap, DashSet};\nuse flowy_ast::{ASTResult, TyInfo};\nuse flowy_codegen::ProtoCache;\nuse lazy_static::lazy_static;\nuse std::fs::File;\nuse std::io::Read;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse walkdir::WalkDir;\n\npub fn ident_category(ident: &syn::Ident) -> TypeCategory {\n  let ident_str = ident.to_string();\n  category_from_str(ident_str)\n}\n\npub(crate) fn get_member_ident<'a>(\n  ast_result: &ASTResult,\n  member: &'a syn::Member,\n) -> Option<&'a syn::Ident> {\n  if let syn::Member::Named(ref ident) = member {\n    Some(ident)\n  } else {\n    ast_result.error_spanned_by(\n      member,\n      \"Unsupported member, shouldn't be self.0\".to_string(),\n    );\n    None\n  }\n}\n\npub fn assert_bracket_ty_is_some(ast_result: &ASTResult, ty_info: &TyInfo) {\n  if ty_info.bracket_ty_info.is_none() {\n    ast_result.error_spanned_by(\n      ty_info.ty,\n      \"Invalid bracketed type when gen de token steam\".to_string(),\n    );\n  }\n}\n\nlazy_static! {\n  static ref READ_FLAG: DashSet<String> = DashSet::new();\n  static ref CACHE_INFO: DashMap<TypeCategory, Vec<String>> = DashMap::new();\n  static ref IS_LOAD: AtomicBool = AtomicBool::new(false);\n}\n\n#[derive(Eq, Hash, PartialEq)]\npub enum TypeCategory {\n  Array,\n  Map,\n  Str,\n  Protobuf,\n  Bytes,\n  Enum,\n  Opt,\n  Primitive,\n}\n// auto generate, do not edit\npub fn category_from_str(type_str: String) -> TypeCategory {\n  if !IS_LOAD.load(Ordering::SeqCst) {\n    IS_LOAD.store(true, Ordering::SeqCst);\n    // Dependents on another crate file is not good, just leave it here.\n    // Maybe find another way to read the .cache in the future.\n    let cache_dir = format!(\"{}/../flowy-codegen/.cache\", env!(\"CARGO_MANIFEST_DIR\"));\n    for path in WalkDir::new(cache_dir)\n      .into_iter()\n      .filter_map(|e| e.ok())\n      .filter(|e| e.path().file_stem().unwrap().to_str().unwrap() == \"proto_cache\")\n      .map(|e| e.path().to_str().unwrap().to_string())\n    {\n      match read_file(&path) {\n        None => {},\n        Some(s) => {\n          let cache: ProtoCache = serde_json::from_str(&s).unwrap();\n          CACHE_INFO\n            .entry(TypeCategory::Protobuf)\n            .or_default()\n            .extend(cache.structs);\n          CACHE_INFO\n            .entry(TypeCategory::Enum)\n            .or_default()\n            .extend(cache.enums);\n        },\n      }\n    }\n  }\n\n  if let Some(protobuf_tys) = CACHE_INFO.get(&TypeCategory::Protobuf) {\n    if protobuf_tys.contains(&type_str) {\n      return TypeCategory::Protobuf;\n    }\n  }\n\n  if let Some(enum_tys) = CACHE_INFO.get(&TypeCategory::Enum) {\n    if enum_tys.contains(&type_str) {\n      return TypeCategory::Enum;\n    }\n  }\n\n  match type_str.as_str() {\n    \"Vec\" => TypeCategory::Array,\n    \"HashMap\" => TypeCategory::Map,\n    \"u8\" => TypeCategory::Bytes,\n    \"String\" => TypeCategory::Str,\n    \"Option\" => TypeCategory::Opt,\n    _ => TypeCategory::Primitive,\n  }\n}\n\nfn read_file(path: &str) -> Option<String> {\n  match File::open(path) {\n    Ok(mut file) => {\n      let mut content = String::new();\n      match file.read_to_string(&mut content) {\n        Ok(_) => Some(content),\n        Err(_) => None,\n      }\n    },\n    Err(_) => None,\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/collab-integrate/Cargo.toml",
    "content": "[package]\nname = \"collab-integrate\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n\n[dependencies]\ncollab = { workspace = true }\ncollab-plugins = { workspace = true }\ncollab-entity = { workspace = true }\ncollab-document = { workspace = true }\ncollab-folder = { workspace = true }\ncollab-user = { workspace = true }\ncollab-database = { workspace = true }\nserde.workspace = true\nserde_json.workspace = true\nanyhow.workspace = true\ntracing.workspace = true\ntokio = { workspace = true, features = [\"sync\"] }\nlib-infra = { workspace = true }\narc-swap = \"1.7\"\nflowy-error.workspace = true\nuuid.workspace = true\nflowy-ai-pub.workspace = true\n\n[target.'cfg(any(target_os = \"macos\", target_os = \"linux\", target_os = \"windows\"))'.dependencies]\ntwox-hash = { version = \"2.1.0\", features = [\"xxhash64\"] }\n\n[features]\ndefault = []\n"
  },
  {
    "path": "frontend/rust-lib/collab-integrate/src/collab_builder.rs",
    "content": "use std::borrow::BorrowMut;\nuse std::fmt::{Debug, Display};\nuse std::sync::{Arc, Weak};\n\nuse crate::CollabKVDB;\nuse anyhow::{Error, anyhow};\nuse arc_swap::{ArcSwap, ArcSwapOption};\nuse collab::core::collab::DataSource;\nuse collab::core::collab_plugin::CollabPersistence;\nuse collab::entity::EncodedCollab;\nuse collab::error::CollabError;\nuse collab::preclude::{Collab, CollabBuilder};\nuse collab_database::workspace_database::{DatabaseCollabService, WorkspaceDatabaseManager};\nuse collab_document::blocks::DocumentData;\nuse collab_document::document::Document;\nuse collab_entity::{CollabObject, CollabType};\nuse collab_folder::{Folder, FolderData, FolderNotify};\nuse collab_plugins::connect_state::{CollabConnectReachability, CollabConnectState};\nuse collab_plugins::local_storage::kv::snapshot::SnapshotPersistence;\n\nif_native! {\nuse collab_plugins::local_storage::rocksdb::rocksdb_plugin::{RocksdbBackup, RocksdbDiskPlugin};\n}\n\nif_wasm! {\nuse collab_plugins::local_storage::indexeddb::IndexeddbDiskPlugin;\n}\n\npub use crate::plugin_provider::CollabCloudPluginProvider;\nuse collab::lock::RwLock;\nuse collab_plugins::local_storage::CollabPersistenceConfig;\nuse collab_plugins::local_storage::kv::KVTransactionDB;\nuse collab_plugins::local_storage::kv::doc::CollabKVAction;\nuse collab_user::core::{UserAwareness, UserAwarenessNotifier};\n\nuse crate::instant_indexed_data_provider::InstantIndexedDataWriter;\nuse flowy_error::FlowyError;\nuse lib_infra::{if_native, if_wasm};\nuse tracing::{error, instrument, trace, warn};\nuse uuid::Uuid;\n\n#[derive(Clone, Debug)]\npub enum CollabPluginProviderType {\n  Local,\n  AppFlowyCloud,\n}\n\npub enum CollabPluginProviderContext {\n  Local,\n  AppFlowyCloud {\n    uid: i64,\n    collab_object: CollabObject,\n    local_collab: Weak<RwLock<dyn BorrowMut<Collab> + Send + Sync + 'static>>,\n  },\n}\n\nimpl Display for CollabPluginProviderContext {\n  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n    let str = match self {\n      CollabPluginProviderContext::Local => \"Local\".to_string(),\n      CollabPluginProviderContext::AppFlowyCloud {\n        uid: _,\n        collab_object,\n        ..\n      } => collab_object.to_string(),\n    };\n    write!(f, \"{}\", str)\n  }\n}\n\npub trait WorkspaceCollabIntegrate: Send + Sync {\n  fn workspace_id(&self) -> Result<Uuid, FlowyError>;\n  fn device_id(&self) -> Result<String, FlowyError>;\n}\n\npub struct AppFlowyCollabBuilder {\n  network_reachability: CollabConnectReachability,\n  plugin_provider: ArcSwap<Arc<dyn CollabCloudPluginProvider>>,\n  snapshot_persistence: ArcSwapOption<Arc<dyn SnapshotPersistence + 'static>>,\n  #[cfg(not(target_arch = \"wasm32\"))]\n  rocksdb_backup: ArcSwapOption<Arc<dyn RocksdbBackup>>,\n  workspace_integrate: Arc<dyn WorkspaceCollabIntegrate>,\n  embeddings_writer: Option<Weak<InstantIndexedDataWriter>>,\n}\n\nimpl AppFlowyCollabBuilder {\n  pub fn new(\n    storage_provider: impl CollabCloudPluginProvider + 'static,\n    workspace_integrate: impl WorkspaceCollabIntegrate + 'static,\n    embeddings_writer: Option<Weak<InstantIndexedDataWriter>>,\n  ) -> Self {\n    Self {\n      embeddings_writer,\n      network_reachability: CollabConnectReachability::new(),\n      plugin_provider: ArcSwap::new(Arc::new(Arc::new(storage_provider))),\n      snapshot_persistence: Default::default(),\n      #[cfg(not(target_arch = \"wasm32\"))]\n      rocksdb_backup: Default::default(),\n      workspace_integrate: Arc::new(workspace_integrate),\n    }\n  }\n\n  pub fn set_snapshot_persistence(&self, snapshot_persistence: Arc<dyn SnapshotPersistence>) {\n    self\n      .snapshot_persistence\n      .store(Some(snapshot_persistence.into()));\n  }\n\n  #[cfg(not(target_arch = \"wasm32\"))]\n  pub fn set_rocksdb_backup(&self, rocksdb_backup: Arc<dyn RocksdbBackup>) {\n    self.rocksdb_backup.store(Some(rocksdb_backup.into()));\n  }\n\n  pub fn update_network(&self, reachable: bool) {\n    if reachable {\n      self\n        .network_reachability\n        .set_state(CollabConnectState::Connected)\n    } else {\n      self\n        .network_reachability\n        .set_state(CollabConnectState::Disconnected)\n    }\n  }\n\n  pub fn collab_object(\n    &self,\n    workspace_id: &Uuid,\n    uid: i64,\n    object_id: &Uuid,\n    collab_type: CollabType,\n  ) -> Result<CollabObject, Error> {\n    // Compare the workspace_id with the currently opened workspace_id. Return an error if they do not match.\n    // This check is crucial in asynchronous code contexts where the workspace_id might change during operation.\n    let actual_workspace_id = self.workspace_integrate.workspace_id()?;\n    if workspace_id != &actual_workspace_id {\n      return Err(anyhow::anyhow!(\n        \"workspace_id not match when build collab. expect workspace_id: {}, actual workspace_id: {}\",\n        workspace_id,\n        actual_workspace_id\n      ));\n    }\n    let device_id = self.workspace_integrate.device_id()?;\n    Ok(CollabObject::new(\n      uid,\n      object_id.to_string(),\n      collab_type,\n      workspace_id.to_string(),\n      device_id,\n    ))\n  }\n\n  #[allow(clippy::too_many_arguments)]\n  #[instrument(\n    level = \"trace\",\n    skip(self, data_source, collab_db, builder_config, data)\n  )]\n  pub async fn create_document(\n    &self,\n    object: CollabObject,\n    data_source: DataSource,\n    collab_db: Weak<CollabKVDB>,\n    builder_config: CollabBuilderConfig,\n    data: Option<DocumentData>,\n  ) -> Result<Arc<RwLock<Document>>, Error> {\n    let expected_collab_type = CollabType::Document;\n    assert_eq!(object.collab_type, expected_collab_type);\n    let mut collab = self.build_collab(&object, &collab_db, data_source).await?;\n    collab.enable_undo_redo();\n\n    let document = match data {\n      None => Document::open(collab)?,\n      Some(data) => {\n        let document = Document::create_with_data(collab, data)?;\n        if let Err(err) = self.write_collab_to_disk(\n          object.uid,\n          &object.workspace_id,\n          &object.object_id,\n          collab_db.clone(),\n          &object.collab_type,\n          &document,\n        ) {\n          error!(\n            \"build_collab: flush document collab to disk failed: {}\",\n            err\n          );\n        }\n        document\n      },\n    };\n    let document = Arc::new(RwLock::new(document));\n    self.finalize(object, builder_config, document)\n  }\n\n  #[allow(clippy::too_many_arguments)]\n  #[instrument(\n    level = \"trace\",\n    skip(self, object, doc_state, collab_db, builder_config, folder_notifier)\n  )]\n  pub async fn create_folder(\n    &self,\n    object: CollabObject,\n    doc_state: DataSource,\n    collab_db: Weak<CollabKVDB>,\n    builder_config: CollabBuilderConfig,\n    folder_notifier: Option<FolderNotify>,\n    folder_data: Option<FolderData>,\n  ) -> Result<Arc<RwLock<Folder>>, Error> {\n    let expected_collab_type = CollabType::Folder;\n    assert_eq!(object.collab_type, expected_collab_type);\n    let folder = match folder_data {\n      None => {\n        let collab = self.build_collab(&object, &collab_db, doc_state).await?;\n        Folder::open(object.uid, collab, folder_notifier)?\n      },\n      Some(data) => {\n        let collab = self.build_collab(&object, &collab_db, doc_state).await?;\n        let folder = Folder::create(object.uid, collab, folder_notifier, data);\n        if let Err(err) = self.write_collab_to_disk(\n          object.uid,\n          &object.workspace_id,\n          &object.object_id,\n          collab_db.clone(),\n          &object.collab_type,\n          &folder,\n        ) {\n          error!(\"build_collab: flush folder collab to disk failed: {}\", err);\n        }\n        folder\n      },\n    };\n    let folder = Arc::new(RwLock::new(folder));\n    self.finalize(object, builder_config, folder)\n  }\n\n  #[allow(clippy::too_many_arguments)]\n  #[instrument(\n    level = \"trace\",\n    skip(self, object, doc_state, collab_db, builder_config, notifier)\n  )]\n  pub async fn create_user_awareness(\n    &self,\n    object: CollabObject,\n    doc_state: DataSource,\n    collab_db: Weak<CollabKVDB>,\n    builder_config: CollabBuilderConfig,\n    notifier: Option<UserAwarenessNotifier>,\n  ) -> Result<Arc<RwLock<UserAwareness>>, Error> {\n    let expected_collab_type = CollabType::UserAwareness;\n    assert_eq!(object.collab_type, expected_collab_type);\n    let collab = self.build_collab(&object, &collab_db, doc_state).await?;\n    let user_awareness = UserAwareness::create(collab, notifier)?;\n    let user_awareness = Arc::new(RwLock::new(user_awareness));\n    self.finalize(object, builder_config, user_awareness)\n  }\n\n  #[allow(clippy::too_many_arguments)]\n  #[instrument(level = \"trace\", skip_all)]\n  pub fn create_workspace_database_manager(\n    &self,\n    object: CollabObject,\n    collab: Collab,\n    _collab_db: Weak<CollabKVDB>,\n    builder_config: CollabBuilderConfig,\n    collab_service: impl DatabaseCollabService,\n  ) -> Result<Arc<RwLock<WorkspaceDatabaseManager>>, Error> {\n    let expected_collab_type = CollabType::WorkspaceDatabase;\n    assert_eq!(object.collab_type, expected_collab_type);\n    let workspace = WorkspaceDatabaseManager::open(&object.object_id, collab, collab_service)?;\n    let workspace = Arc::new(RwLock::new(workspace));\n    self.finalize(object, builder_config, workspace)\n  }\n\n  pub async fn build_collab(\n    &self,\n    object: &CollabObject,\n    collab_db: &Weak<CollabKVDB>,\n    data_source: DataSource,\n  ) -> Result<Collab, Error> {\n    let object = object.clone();\n    let collab_db = collab_db.clone();\n    let device_id = self.workspace_integrate.device_id()?;\n    let collab = tokio::task::spawn_blocking(move || {\n      let collab = CollabBuilder::new(object.uid, &object.object_id, data_source)\n        .with_device_id(device_id)\n        .build()?;\n      let persistence_config = CollabPersistenceConfig::default();\n      let db_plugin = RocksdbDiskPlugin::new_with_config(\n        object.uid,\n        object.workspace_id.clone(),\n        object.object_id.to_string(),\n        object.collab_type,\n        collab_db,\n        persistence_config,\n      );\n      collab.add_plugin(Box::new(db_plugin));\n      Ok::<_, Error>(collab)\n    })\n    .await??;\n\n    Ok(collab)\n  }\n\n  pub fn finalize<T>(\n    &self,\n    object: CollabObject,\n    build_config: CollabBuilderConfig,\n    collab: Arc<RwLock<T>>,\n  ) -> Result<Arc<RwLock<T>>, Error>\n  where\n    T: BorrowMut<Collab> + Send + Sync + 'static,\n  {\n    let cloned_object = object.clone();\n    let weak_collab = Arc::downgrade(&collab);\n    let weak_embedding_writer = self.embeddings_writer.clone();\n    tokio::spawn(async move {\n      if let Some(embedding_writer) = weak_embedding_writer.and_then(|w| w.upgrade()) {\n        embedding_writer\n          .queue_collab_embed(cloned_object, weak_collab)\n          .await;\n      }\n    });\n\n    let mut write_collab = collab.try_write()?;\n    let has_cloud_plugin = write_collab.borrow().has_cloud_plugin();\n    if has_cloud_plugin {\n      drop(write_collab);\n      return Ok(collab);\n    }\n\n    if build_config.sync_enable {\n      trace!(\"🚀finalize collab:{}\", object);\n      let plugin_provider = self.plugin_provider.load_full();\n      let provider_type = plugin_provider.provider_type();\n      let span =\n        tracing::span!(tracing::Level::TRACE, \"collab_builder\", object_id = %object.object_id);\n      let _enter = span.enter();\n      match provider_type {\n        CollabPluginProviderType::AppFlowyCloud => {\n          let local_collab = Arc::downgrade(&collab);\n          let plugins = plugin_provider.get_plugins(CollabPluginProviderContext::AppFlowyCloud {\n            uid: object.uid,\n            collab_object: object,\n            local_collab,\n          });\n\n          // at the moment when we get the lock, the collab object is not yet exposed outside\n          for plugin in plugins {\n            write_collab.borrow().add_plugin(plugin);\n          }\n        },\n        CollabPluginProviderType::Local => {},\n      }\n    }\n\n    (*write_collab).borrow_mut().initialize();\n    drop(write_collab);\n    Ok(collab)\n  }\n\n  /// Remove all updates in disk and write the final state vector to disk.\n  #[instrument(level = \"trace\", skip_all, err)]\n  pub fn write_collab_to_disk<T>(\n    &self,\n    uid: i64,\n    workspace_id: &str,\n    object_id: &str,\n    collab_db: Weak<CollabKVDB>,\n    collab_type: &CollabType,\n    collab: &T,\n  ) -> Result<(), Error>\n  where\n    T: BorrowMut<Collab> + Send + Sync + 'static,\n  {\n    if let Some(collab_db) = collab_db.upgrade() {\n      let write_txn = collab_db.write_txn();\n      trace!(\n        \"flush workspace: {} {}:collab:{} to disk\",\n        workspace_id, collab_type, object_id\n      );\n      let collab: &Collab = collab.borrow();\n      let encode_collab =\n        collab.encode_collab_v1(|collab| collab_type.validate_require_data(collab))?;\n      write_txn.flush_doc(\n        uid,\n        workspace_id,\n        object_id,\n        encode_collab.state_vector.to_vec(),\n        encode_collab.doc_state.to_vec(),\n      )?;\n      write_txn.commit_transaction()?;\n    } else {\n      error!(\"collab_db is dropped\");\n    }\n\n    Ok(())\n  }\n}\n\npub struct CollabBuilderConfig {\n  pub sync_enable: bool,\n}\n\nimpl Default for CollabBuilderConfig {\n  fn default() -> Self {\n    Self { sync_enable: true }\n  }\n}\n\nimpl CollabBuilderConfig {\n  pub fn sync_enable(mut self, sync_enable: bool) -> Self {\n    self.sync_enable = sync_enable;\n    self\n  }\n}\n\npub struct CollabPersistenceImpl {\n  pub db: Weak<CollabKVDB>,\n  pub uid: i64,\n  pub workspace_id: Uuid,\n}\n\nimpl CollabPersistenceImpl {\n  pub fn new(db: Weak<CollabKVDB>, uid: i64, workspace_id: Uuid) -> Self {\n    Self {\n      db,\n      uid,\n      workspace_id,\n    }\n  }\n\n  pub fn into_data_source(self) -> DataSource {\n    DataSource::Disk(Some(Box::new(self)))\n  }\n}\n\nimpl CollabPersistence for CollabPersistenceImpl {\n  fn load_collab_from_disk(&self, collab: &mut Collab) -> Result<(), CollabError> {\n    let collab_db = self\n      .db\n      .upgrade()\n      .ok_or_else(|| CollabError::Internal(anyhow!(\"collab_db is dropped\")))?;\n\n    let object_id = collab.object_id().to_string();\n    let rocksdb_read = collab_db.read_txn();\n    let workspace_id = self.workspace_id.to_string();\n\n    if rocksdb_read.is_exist(self.uid, &workspace_id, &object_id) {\n      let mut txn = collab.transact_mut();\n      match rocksdb_read.load_doc_with_txn(self.uid, &workspace_id, &object_id, &mut txn) {\n        Ok(update_count) => {\n          trace!(\n            \"did load collab:{}-{} from disk, update_count:{}\",\n            self.uid, object_id, update_count\n          );\n        },\n        Err(err) => {\n          error!(\"🔴 load doc:{} failed: {}\", object_id, err);\n        },\n      }\n      drop(rocksdb_read);\n      txn.commit();\n      drop(txn);\n    }\n    Ok(())\n  }\n\n  fn save_collab_to_disk(\n    &self,\n    object_id: &str,\n    encoded_collab: EncodedCollab,\n  ) -> Result<(), CollabError> {\n    let workspace_id = self.workspace_id.to_string();\n    let collab_db = self\n      .db\n      .upgrade()\n      .ok_or_else(|| CollabError::Internal(anyhow!(\"collab_db is dropped\")))?;\n    let write_txn = collab_db.write_txn();\n    write_txn\n      .flush_doc(\n        self.uid,\n        workspace_id.as_str(),\n        object_id,\n        encoded_collab.state_vector.to_vec(),\n        encoded_collab.doc_state.to_vec(),\n      )\n      .map_err(|err| CollabError::Internal(err.into()))?;\n\n    write_txn\n      .commit_transaction()\n      .map_err(|err| CollabError::Internal(err.into()))?;\n    Ok(())\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/collab-integrate/src/config.rs",
    "content": "use std::str::FromStr;\n\nuse serde::{Deserialize, Serialize};\n\npub enum CollabDBPluginProvider {\n  AWS,\n  Supabase,\n}\n\n#[derive(Clone, Debug, Serialize, Deserialize, Default)]\npub struct CollabPluginConfig {\n  /// Only one of the following two fields should be set.\n  aws_config: Option<AWSDynamoDBConfig>,\n}\n\nimpl CollabPluginConfig {\n  pub fn from_env() -> Self {\n    let aws_config = AWSDynamoDBConfig::from_env();\n    Self { aws_config }\n  }\n\n  pub fn aws_config(&self) -> Option<&AWSDynamoDBConfig> {\n    self.aws_config.as_ref()\n  }\n}\n\nimpl CollabPluginConfig {}\n\nimpl FromStr for CollabPluginConfig {\n  type Err = serde_json::Error;\n\n  fn from_str(s: &str) -> Result<Self, Self::Err> {\n    serde_json::from_str(s)\n  }\n}\n\npub const AWS_ACCESS_KEY_ID: &str = \"AWS_ACCESS_KEY_ID\";\npub const AWS_SECRET_ACCESS_KEY: &str = \"AWS_SECRET_ACCESS_KEY\";\npub const AWS_REGION: &str = \"AWS_REGION\";\n\n// To enable this test, you should set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in your environment variables.\n// or create the ~/.aws/credentials file following the instructions in https://docs.aws.amazon.com/sdk-for-rust/latest/dg/credentials.html\n#[derive(Default, Clone, Debug, Serialize, Deserialize)]\npub struct AWSDynamoDBConfig {\n  pub access_key_id: String,\n  pub secret_access_key: String,\n  // Region list: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html\n  pub region: String,\n  pub enable: bool,\n}\n\nimpl AWSDynamoDBConfig {\n  pub fn from_env() -> Option<Self> {\n    let access_key_id = std::env::var(AWS_ACCESS_KEY_ID).ok()?;\n    let secret_access_key = std::env::var(AWS_SECRET_ACCESS_KEY).ok()?;\n    let region = std::env::var(AWS_REGION).unwrap_or_else(|_| \"us-east-1\".to_string());\n    Some(Self {\n      access_key_id,\n      secret_access_key,\n      region,\n      enable: true,\n    })\n  }\n\n  pub fn write_env(&self) {\n    unsafe {\n      std::env::set_var(AWS_ACCESS_KEY_ID, &self.access_key_id);\n      std::env::set_var(AWS_SECRET_ACCESS_KEY, &self.secret_access_key);\n      std::env::set_var(AWS_REGION, &self.region);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/collab-integrate/src/instant_indexed_data_provider.rs",
    "content": "use collab::core::collab::DataSource;\nuse collab::core::origin::CollabOrigin;\nuse collab::entity::EncodedCollab;\nuse collab::lock::RwLock;\nuse collab::preclude::{Collab, Transact};\nuse collab_document::document::DocumentBody;\nuse collab_entity::{CollabObject, CollabType};\nuse flowy_ai_pub::entities::{UnindexedCollab, UnindexedCollabMetadata, UnindexedData};\nuse flowy_error::{FlowyError, FlowyResult};\nuse lib_infra::async_trait::async_trait;\nuse std::borrow::BorrowMut;\nuse std::collections::HashMap;\nuse std::sync::{Arc, Weak};\nuse std::time::Duration;\nuse tokio::runtime::Runtime;\nuse tokio::time::interval;\nuse tracing::{error, info, trace};\nuse uuid::Uuid;\n\npub struct WriteObject {\n  pub collab_object: CollabObject,\n  pub collab: Weak<dyn CollabIndexedData>,\n}\n\npub struct InstantIndexedDataWriter {\n  collab_by_object: Arc<RwLock<HashMap<String, WriteObject>>>,\n  consumers: Arc<RwLock<Vec<Box<dyn InstantIndexedDataConsumer>>>>,\n}\n\nimpl Default for InstantIndexedDataWriter {\n  fn default() -> Self {\n    Self::new()\n  }\n}\n\nimpl InstantIndexedDataWriter {\n  pub fn new() -> InstantIndexedDataWriter {\n    let collab_by_object = Arc::new(RwLock::new(HashMap::<String, WriteObject>::new()));\n    let consumers = Arc::new(RwLock::new(\n      Vec::<Box<dyn InstantIndexedDataConsumer>>::new(),\n    ));\n\n    InstantIndexedDataWriter {\n      collab_by_object,\n      consumers,\n    }\n  }\n\n  pub async fn num_consumers(&self) -> usize {\n    let consumers = self.consumers.read().await;\n    consumers.len()\n  }\n\n  pub async fn clear_consumers(&self) {\n    let mut consumers = self.consumers.write().await;\n    consumers.clear();\n    info!(\"[Indexing] Cleared all instant index consumers\");\n  }\n\n  pub async fn register_consumer(&self, consumer: Box<dyn InstantIndexedDataConsumer>) {\n    info!(\n      \"[Indexing] Registering instant index consumer: {}\",\n      consumer.consumer_id()\n    );\n    let mut guard = self.consumers.write().await;\n    guard.push(consumer);\n  }\n\n  pub async fn spawn_instant_indexed_provider(&self, runtime: &Runtime) -> FlowyResult<()> {\n    let weak_collab_by_object = Arc::downgrade(&self.collab_by_object);\n    let consumers_weak = Arc::downgrade(&self.consumers);\n    let interval_dur = Duration::from_secs(30);\n\n    runtime.spawn(async move {\n      let mut ticker = interval(interval_dur);\n      ticker.tick().await;\n\n      loop {\n        ticker.tick().await;\n\n        // Upgrade our state holders\n        let collab_by_object = match weak_collab_by_object.upgrade() {\n          Some(m) => m,\n          None => break, // provider dropped\n        };\n        let consumers = match consumers_weak.upgrade() {\n          Some(c) => c,\n          None => break,\n        };\n\n        // Snapshot keys and consumers under read locks\n        let (object_ids, mut to_remove) = {\n          let guard = collab_by_object.read().await;\n          let keys: Vec<_> = guard.keys().cloned().collect();\n          (keys, Vec::new())\n        };\n        let guard = collab_by_object.read().await;\n        for id in object_ids {\n          // Check if the collab is still alive\n          match guard.get(&id) {\n            Some(wo) => {\n              if let Some(collab_rc) = wo.collab.upgrade() {\n                let data = collab_rc\n                  .get_unindexed_data(&wo.collab_object.collab_type)\n                  .await;\n\n                let consumers_guard = consumers.read().await;\n                for consumer in consumers_guard.iter() {\n                  let workspace_id = match Uuid::parse_str(&wo.collab_object.workspace_id) {\n                    Ok(id) => id,\n                    Err(err) => {\n                      error!(\n                        \"Invalid workspace_id {}: {}\",\n                        wo.collab_object.workspace_id, err\n                      );\n                      continue;\n                    },\n                  };\n                  let object_id = match Uuid::parse_str(&wo.collab_object.object_id) {\n                    Ok(id) => id,\n                    Err(err) => {\n                      error!(\"Invalid object_id {}: {}\", wo.collab_object.object_id, err);\n                      continue;\n                    },\n                  };\n                  match consumer\n                    .consume_collab(\n                      &workspace_id,\n                      data.clone(),\n                      &object_id,\n                      wo.collab_object.collab_type,\n                    )\n                    .await\n                  {\n                    Ok(is_indexed) => {\n                      if is_indexed {\n                        trace!(\"[Indexing] {} consumed {}\", consumer.consumer_id(), id);\n                      }\n                    },\n                    Err(err) => {\n                      error!(\n                        \"Consumer {} failed on {}: {}\",\n                        consumer.consumer_id(),\n                        id,\n                        err\n                      );\n                    },\n                  }\n                }\n              } else {\n                // Mark for removal if collab was dropped\n                to_remove.push(id);\n              }\n            },\n            None => continue,\n          }\n        }\n\n        if !to_remove.is_empty() {\n          let mut guard = collab_by_object.write().await;\n          guard.retain(|k, _| !to_remove.contains(k));\n          trace!(\"[Indexing] Removed {} stale entries\", to_remove.len());\n        }\n      }\n\n      info!(\"[Indexing] Instant indexed data provider stopped\");\n    });\n\n    Ok(())\n  }\n\n  pub fn support_collab_type(&self, t: &CollabType) -> bool {\n    matches!(t, CollabType::Document)\n  }\n\n  pub async fn index_encoded_collab(\n    &self,\n    workspace_id: Uuid,\n    object_id: Uuid,\n    data: EncodedCollab,\n    collab_type: CollabType,\n  ) -> FlowyResult<()> {\n    match unindexed_collab_from_encoded_collab(workspace_id, object_id, data, collab_type) {\n      None => Err(FlowyError::internal().with_context(\"Failed to create unindexed collab\")),\n      Some(data) => {\n        self.index_unindexed_collab(data).await?;\n        Ok(())\n      },\n    }\n  }\n\n  pub async fn index_unindexed_collab(&self, data: UnindexedCollab) -> FlowyResult<()> {\n    let consumers_guard = self.consumers.read().await;\n    for consumer in consumers_guard.iter() {\n      match consumer\n        .consume_collab(\n          &data.workspace_id,\n          data.data.clone(),\n          &data.object_id,\n          data.collab_type,\n        )\n        .await\n      {\n        Ok(is_indexed) => {\n          if is_indexed {\n            trace!(\n              \"[Indexing] {} consumed {}\",\n              consumer.consumer_id(),\n              data.object_id\n            );\n          }\n        },\n        Err(err) => {\n          error!(\n            \"Consumer {} failed on {}: {}\",\n            consumer.consumer_id(),\n            data.object_id,\n            err\n          );\n        },\n      }\n    }\n    Ok(())\n  }\n\n  pub async fn queue_collab_embed(\n    &self,\n    collab_object: CollabObject,\n    collab: Weak<dyn CollabIndexedData>,\n  ) {\n    if !self.support_collab_type(&collab_object.collab_type) {\n      return;\n    }\n\n    let mut map = self.collab_by_object.write().await;\n    map.insert(\n      collab_object.object_id.clone(),\n      WriteObject {\n        collab_object,\n        collab,\n      },\n    );\n  }\n}\n\npub fn unindexed_data_form_collab(\n  collab: &Collab,\n  collab_type: &CollabType,\n) -> Option<UnindexedData> {\n  match collab_type {\n    CollabType::Document => {\n      let txn = collab.doc().try_transact().ok()?;\n      let doc = DocumentBody::from_collab(collab)?;\n      let paras = doc.paragraphs(txn);\n      Some(UnindexedData::Paragraphs(paras))\n    },\n    _ => None,\n  }\n}\n\npub fn unindexed_collab_from_encoded_collab(\n  workspace_id: Uuid,\n  object_id: Uuid,\n  encoded_collab: EncodedCollab,\n  collab_type: CollabType,\n) -> Option<UnindexedCollab> {\n  match collab_type {\n    CollabType::Document => {\n      let collab = Collab::new_with_source(\n        CollabOrigin::Empty,\n        &object_id.to_string(),\n        DataSource::DocStateV1(encoded_collab.doc_state.to_vec()),\n        vec![],\n        false,\n      )\n      .ok()?;\n      let data = unindexed_data_form_collab(&collab, &collab_type)?;\n      Some(UnindexedCollab {\n        workspace_id,\n        object_id,\n        collab_type,\n        data: Some(data),\n        metadata: UnindexedCollabMetadata::default(), // default means do not update metadata\n      })\n    },\n    _ => None,\n  }\n}\n\n#[async_trait]\npub trait CollabIndexedData: Send + Sync + 'static {\n  async fn get_unindexed_data(&self, collab_type: &CollabType) -> Option<UnindexedData>;\n}\n\n#[async_trait]\nimpl<T> CollabIndexedData for RwLock<T>\nwhere\n  T: BorrowMut<Collab> + Send + Sync + 'static,\n{\n  async fn get_unindexed_data(&self, collab_type: &CollabType) -> Option<UnindexedData> {\n    let collab = self.try_read().ok()?;\n    unindexed_data_form_collab(collab.borrow(), collab_type)\n  }\n}\n\n/// writer interface\n#[async_trait]\npub trait InstantIndexedDataConsumer: Send + Sync + 'static {\n  fn consumer_id(&self) -> String;\n\n  async fn consume_collab(\n    &self,\n    workspace_id: &Uuid,\n    data: Option<UnindexedData>,\n    object_id: &Uuid,\n    collab_type: CollabType,\n  ) -> Result<bool, FlowyError>;\n\n  async fn did_delete_collab(\n    &self,\n    workspace_id: &Uuid,\n    object_id: &Uuid,\n  ) -> Result<(), FlowyError>;\n}\n"
  },
  {
    "path": "frontend/rust-lib/collab-integrate/src/lib.rs",
    "content": "pub use collab::preclude::Snapshot;\npub use collab_plugins::CollabKVDB;\npub use collab_plugins::local_storage::CollabPersistenceConfig;\n\npub mod collab_builder;\npub mod config;\npub mod instant_indexed_data_provider;\nmod plugin_provider;\n\npub use collab_plugins::local_storage::kv::doc::CollabKVAction;\npub use collab_plugins::local_storage::kv::error::PersistenceError;\npub use collab_plugins::local_storage::kv::snapshot::{CollabSnapshot, SnapshotPersistence};\n"
  },
  {
    "path": "frontend/rust-lib/collab-integrate/src/plugin_provider.rs",
    "content": "use collab::preclude::CollabPlugin;\n\nuse crate::collab_builder::{CollabPluginProviderContext, CollabPluginProviderType};\n\npub trait CollabCloudPluginProvider: Send + Sync + 'static {\n  fn provider_type(&self) -> CollabPluginProviderType;\n\n  fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec<Box<dyn CollabPlugin>>;\n\n  fn is_sync_enabled(&self) -> bool;\n}\n\nimpl<U> CollabCloudPluginProvider for std::sync::Arc<U>\nwhere\n  U: CollabCloudPluginProvider,\n{\n  fn provider_type(&self) -> CollabPluginProviderType {\n    (**self).provider_type()\n  }\n\n  fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec<Box<dyn CollabPlugin>> {\n    (**self).get_plugins(context)\n  }\n\n  fn is_sync_enabled(&self) -> bool {\n    (**self).is_sync_enabled()\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/covtest.rs",
    "content": ""
  },
  {
    "path": "frontend/rust-lib/dart-ffi/Cargo.toml",
    "content": "[package]\nname = \"dart-ffi\"\nversion = \"0.1.0\"\nedition = \"2018\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n[lib]\nname = \"dart_ffi\"\n# this value will change depending on the target os\n# default static library\ncrate-type = [\"staticlib\"]\n\n\n[dependencies]\nallo-isolate = { version = \"^0.1\", features = [\"catch-unwind\"] }\nbyteorder = { version = \"1.4.3\" }\nprotobuf.workspace = true\ntokio = { workspace = true, features = [\"full\", \"rt-multi-thread\", \"tracing\"] }\nserde.workspace = true\nserde_repr.workspace = true\nserde_json.workspace = true\nbytes.workspace = true\ncrossbeam-utils = \"0.8.15\"\nlazy_static = \"1.4.0\"\ntracing.workspace = true\nlib-log.workspace = true\nsemver = \"1.0.22\"\n\n# workspace\nlib-dispatch = { workspace = true, features = [\"local_set\"] }\n\n# Core\n#flowy-core = { workspace = true, features = [\"profiling\"] }\n#flowy-core = { workspace = true, features = [\"verbose_log\"] }\nflowy-core = { workspace = true }\n\nflowy-notification = { workspace = true, features = [\"dart\"] }\nflowy-document = { workspace = true, features = [\"dart\"] }\nflowy-user = { workspace = true, features = [\"dart\"] }\nflowy-date = { workspace = true, features = [\"dart\"] }\nflowy-server = { workspace = true }\nflowy-server-pub = { workspace = true }\ncollab-integrate = { workspace = true }\nflowy-derive.workspace = true\nserde_yaml = \"0.9.27\"\nflowy-error = { workspace = true, features = [\"impl_from_sqlite\", \"impl_from_dispatch_error\", \"impl_from_appflowy_cloud\", \"impl_from_reqwest\", \"impl_from_serde\", \"dart\"] }\nfutures = \"0.3.31\"\n\n[features]\ndefault = [\"dart\"]\ndart = [\"flowy-core/dart\"]\nhttp_sync = [\"flowy-core/http_sync\"]\nopenssl_vendored = [\"flowy-core/openssl_vendored\"]\nverbose_log = []\n\n[build-dependencies]\nflowy-codegen = { workspace = true, features = [\"dart\"] }\n"
  },
  {
    "path": "frontend/rust-lib/dart-ffi/Flowy.toml",
    "content": "# Check out the FlowyConfig (located in flowy_toml.rs) for more details.\nproto_input = [\"src/model\"]\n"
  },
  {
    "path": "frontend/rust-lib/dart-ffi/binding.h",
    "content": "#include <stdarg.h>\n#include <stdbool.h>\n#include <stdint.h>\n#include <stdlib.h>\n\nint64_t init_sdk(int64_t port, char *data);\n\nvoid async_event(int64_t port, const uint8_t *input, uintptr_t len);\n\nconst uint8_t *sync_event(const uint8_t *input, uintptr_t len);\n\nint32_t set_stream_port(int64_t port);\n\nint32_t set_log_stream_port(int64_t port);\n\nvoid link_me_please(void);\n\nvoid rust_log(int64_t level, const char *data);\n\nvoid set_env(const char *data);\n"
  },
  {
    "path": "frontend/rust-lib/dart-ffi/build.rs",
    "content": "fn main() {\n  flowy_codegen::protobuf_file::dart_gen(env!(\"CARGO_PKG_NAME\"));\n}\n"
  },
  {
    "path": "frontend/rust-lib/dart-ffi/src/appflowy_yaml.rs",
    "content": "use std::fs::{File, OpenOptions};\nuse std::io::{Read, Write};\nuse std::path::Path;\n\nuse serde::{Deserialize, Serialize};\n\nuse flowy_server_pub::af_cloud_config::AFCloudConfiguration;\n\n#[derive(Debug, Serialize, Deserialize, Clone, Default)]\npub struct AppFlowyYamlConfiguration {\n  cloud_config: Vec<AFCloudConfiguration>,\n}\n\npub fn save_appflowy_cloud_config(\n  root: impl AsRef<Path>,\n  new_config: &AFCloudConfiguration,\n) -> Result<(), Box<dyn std::error::Error>> {\n  let file_path = root.as_ref().join(\"appflowy.yaml\");\n  let mut config = read_yaml_file(&file_path).unwrap_or_default();\n\n  if !config\n    .cloud_config\n    .iter()\n    .any(|c| c.base_url == new_config.base_url)\n  {\n    config.cloud_config.push(new_config.clone());\n    write_yaml_file(&file_path, &config)?;\n  }\n  Ok(())\n}\n\nfn read_yaml_file(\n  file_path: impl AsRef<Path>,\n) -> Result<AppFlowyYamlConfiguration, Box<dyn std::error::Error>> {\n  let mut file = File::open(file_path)?;\n  let mut contents = String::new();\n  file.read_to_string(&mut contents)?;\n  let config: AppFlowyYamlConfiguration = serde_yaml::from_str(&contents)?;\n  Ok(config)\n}\n\nfn write_yaml_file(\n  file_path: impl AsRef<Path>,\n  config: &AppFlowyYamlConfiguration,\n) -> Result<(), Box<dyn std::error::Error>> {\n  let yaml_string = serde_yaml::to_string(config)?;\n  let mut file = OpenOptions::new()\n    .create(true)\n    .write(true)\n    .truncate(true)\n    .open(file_path)?;\n  file.write_all(yaml_string.as_bytes())?;\n  Ok(())\n}\n"
  },
  {
    "path": "frontend/rust-lib/dart-ffi/src/c.rs",
    "content": "use byteorder::{BigEndian, ByteOrder};\nuse std::mem::forget;\n\npub fn forget_rust(buf: Vec<u8>) -> *const u8 {\n  let ptr = buf.as_ptr();\n  forget(buf);\n  ptr\n}\n\n#[allow(unused_attributes)]\n#[allow(dead_code)]\npub fn reclaim_rust(ptr: *mut u8, length: u32) {\n  unsafe {\n    let len: usize = length as usize;\n    Vec::from_raw_parts(ptr, len, len);\n  }\n}\n\npub fn extend_front_four_bytes_into_bytes(bytes: &[u8]) -> Vec<u8> {\n  let mut output = Vec::with_capacity(bytes.len() + 4);\n  let mut marker_bytes = [0; 4];\n  BigEndian::write_u32(&mut marker_bytes, bytes.len() as u32);\n  output.extend_from_slice(&marker_bytes);\n  output.extend_from_slice(bytes);\n  output\n}\n"
  },
  {
    "path": "frontend/rust-lib/dart-ffi/src/env_serde.rs",
    "content": "use std::collections::HashMap;\n\nuse serde::Deserialize;\n\nuse flowy_server_pub::af_cloud_config::AFCloudConfiguration;\nuse flowy_server_pub::AuthenticatorType;\n\n#[derive(Deserialize, Debug)]\npub struct AppFlowyDartConfiguration {\n  /// The root path of the application\n  pub root: String,\n  pub app_version: String,\n  /// This path will be used to store the user data\n  pub custom_app_path: String,\n  pub origin_app_path: String,\n  pub device_id: String,\n  pub platform: String,\n  pub authenticator_type: AuthenticatorType,\n  pub(crate) appflowy_cloud_config: AFCloudConfiguration,\n  #[serde(default)]\n  pub(crate) envs: HashMap<String, String>,\n}\n\nimpl AppFlowyDartConfiguration {\n  pub fn from_str(s: &str) -> Self {\n    serde_json::from_str::<AppFlowyDartConfiguration>(s).unwrap()\n  }\n\n  pub fn write_env(&self) {\n    self.authenticator_type.write_env();\n    self.appflowy_cloud_config.write_env();\n\n    for (k, v) in self.envs.iter() {\n      std::env::set_var(k, v);\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/dart-ffi/src/lib.rs",
    "content": "#![allow(clippy::not_unsafe_ptr_arg_deref)]\n\nuse allo_isolate::Isolate;\nuse futures::ready;\nuse lazy_static::lazy_static;\nuse semver::Version;\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::sync::{Arc, RwLock};\nuse std::task::{Context, Poll};\nuse std::{ffi::CStr, os::raw::c_char};\nuse tokio::sync::mpsc;\nuse tokio::task::LocalSet;\nuse tracing::{debug, error, info, trace, warn};\n\nuse flowy_core::config::AppFlowyCoreConfig;\nuse flowy_core::*;\nuse flowy_notification::{register_notification_sender, unregister_all_notification_sender};\nuse flowy_server_pub::AuthenticatorType;\nuse lib_dispatch::prelude::ToBytes;\nuse lib_dispatch::prelude::*;\nuse lib_dispatch::runtime::AFPluginRuntime;\nuse lib_log::stream_log::StreamLogSender;\n\nuse crate::appflowy_yaml::save_appflowy_cloud_config;\nuse crate::env_serde::AppFlowyDartConfiguration;\nuse crate::notification::DartNotificationSender;\nuse crate::{\n  c::{extend_front_four_bytes_into_bytes, forget_rust},\n  model::{FFIRequest, FFIResponse},\n};\n\nmod appflowy_yaml;\nmod c;\nmod env_serde;\nmod model;\nmod notification;\nmod protobuf;\n\nlazy_static! {\n  static ref DART_APPFLOWY_CORE: DartAppFlowyCore = DartAppFlowyCore::new();\n  static ref LOG_STREAM_ISOLATE: RwLock<Option<Isolate>> = RwLock::new(None);\n}\n\npub struct Task {\n  dispatcher: Arc<AFPluginDispatcher>,\n  request: AFPluginRequest,\n  port: i64,\n  ret: Option<mpsc::Sender<AFPluginEventResponse>>,\n}\n\nunsafe impl Send for Task {}\nunsafe impl Sync for DartAppFlowyCore {}\n\nstruct DartAppFlowyCore {\n  core: Arc<RwLock<Option<AppFlowyCore>>>,\n  handle: RwLock<Option<std::thread::JoinHandle<()>>>,\n  sender: RwLock<Option<mpsc::UnboundedSender<Task>>>,\n}\n\nimpl DartAppFlowyCore {\n  fn new() -> Self {\n    Self {\n      #[allow(clippy::arc_with_non_send_sync)]\n      core: Arc::new(RwLock::new(None)),\n      handle: RwLock::new(None),\n      sender: RwLock::new(None),\n    }\n  }\n\n  fn dispatcher(&self) -> Option<Arc<AFPluginDispatcher>> {\n    let binding = self\n      .core\n      .read()\n      .expect(\"Failed to acquire read lock for core\");\n    let core = binding.as_ref();\n    core.map(|core| core.event_dispatcher.clone())\n  }\n\n  fn dispatch(\n    &self,\n    request: AFPluginRequest,\n    port: i64,\n    ret: Option<mpsc::Sender<AFPluginEventResponse>>,\n  ) {\n    if let Ok(sender_guard) = self.sender.read() {\n      let dispatcher = match self.dispatcher() {\n        Some(dispatcher) => dispatcher,\n        None => {\n          error!(\"Failed to get dispatcher: dispatcher is None\");\n          return;\n        },\n      };\n\n      if let Some(sender) = sender_guard.as_ref() {\n        if let Err(e) = sender.send(Task {\n          dispatcher,\n          request,\n          port,\n          ret,\n        }) {\n          error!(\"Failed to send task: {}\", e);\n        }\n      } else {\n        error!(\"Failed to send task: sender is None\");\n      }\n    } else {\n      warn!(\"Failed to acquire read lock for sender\");\n    }\n  }\n}\n\n#[no_mangle]\npub extern \"C\" fn init_sdk(_port: i64, data: *mut c_char) -> i64 {\n  let c_str = unsafe {\n    if data.is_null() {\n      return -1;\n    }\n    CStr::from_ptr(data)\n  };\n  let serde_str = c_str\n    .to_str()\n    .expect(\"Failed to convert C string to Rust string\");\n  let configuration = AppFlowyDartConfiguration::from_str(serde_str);\n  configuration.write_env();\n\n  if configuration.authenticator_type == AuthenticatorType::AppFlowyCloud {\n    let _ = save_appflowy_cloud_config(&configuration.root, &configuration.appflowy_cloud_config);\n  }\n\n  let mut app_version =\n    Version::parse(&configuration.app_version).unwrap_or_else(|_| Version::new(0, 5, 8));\n\n  let min_version = Version::new(0, 5, 8);\n  if app_version < min_version {\n    app_version = min_version;\n  }\n\n  let config = AppFlowyCoreConfig::new(\n    app_version,\n    configuration.custom_app_path,\n    configuration.origin_app_path,\n    configuration.device_id,\n    configuration.platform,\n    DEFAULT_NAME.to_string(),\n  );\n\n  if let Some(core) = &*DART_APPFLOWY_CORE.core.write().unwrap() {\n    core.close_db();\n  }\n\n  let log_stream = LOG_STREAM_ISOLATE\n    .write()\n    .unwrap()\n    .take()\n    .map(|isolate| Arc::new(LogStreamSenderImpl { isolate }) as Arc<dyn StreamLogSender>);\n  let (sender, task_rx) = mpsc::unbounded_channel::<Task>();\n  let runtime = Arc::new(AFPluginRuntime::new().unwrap());\n  let cloned_runtime = runtime.clone();\n  let handle = std::thread::spawn(move || {\n    let local_set = LocalSet::new();\n    cloned_runtime.block_on(local_set.run_until(Runner { rx: task_rx }));\n  });\n\n  *DART_APPFLOWY_CORE.sender.write().unwrap() = Some(sender);\n  *DART_APPFLOWY_CORE.handle.write().unwrap() = Some(handle);\n  let cloned_runtime = runtime.clone();\n  *DART_APPFLOWY_CORE.core.write().unwrap() = runtime\n    .block_on(async move { Some(AppFlowyCore::new(config, cloned_runtime, log_stream).await) });\n  0\n}\n\n#[no_mangle]\n#[allow(clippy::let_underscore_future)]\npub extern \"C\" fn async_event(port: i64, input: *const u8, len: usize) {\n  let request: AFPluginRequest = FFIRequest::from_u8_pointer(input, len).into();\n  #[cfg(feature = \"verbose_log\")]\n  trace!(\n    \"[FFI]: {} Async Event: {:?} with {} port\",\n    &request.id,\n    &request.event,\n    port\n  );\n\n  DART_APPFLOWY_CORE.dispatch(request, port, None);\n}\n\n/// A persistent future that processes [Arbiter] commands.\nstruct Runner {\n  rx: mpsc::UnboundedReceiver<Task>,\n}\n\nimpl Future for Runner {\n  type Output = ();\n\n  fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {\n    loop {\n      match ready!(self.rx.poll_recv(cx)) {\n        None => return Poll::Ready(()),\n        Some(task) => {\n          let Task {\n            dispatcher,\n            request,\n            port,\n            ret,\n          } = task;\n\n          tokio::task::spawn_local(async move {\n            let resp = AFPluginDispatcher::boxed_async_send_with_callback(\n              dispatcher.as_ref(),\n              request,\n              move |resp: AFPluginEventResponse| {\n                #[cfg(feature = \"verbose_log\")]\n                trace!(\"[FFI]: Post data to dart through {} port\", port);\n                Box::pin(post_to_flutter(resp, port))\n              },\n            )\n            .await;\n\n            if let Some(ret) = ret {\n              let _ = ret.send(resp).await;\n            }\n          });\n        },\n      }\n    }\n  }\n}\n\n#[no_mangle]\npub extern \"C\" fn sync_event(_input: *const u8, _len: usize) -> *const u8 {\n  error!(\"unimplemented sync_event\");\n\n  let response_bytes = vec![];\n  let result = extend_front_four_bytes_into_bytes(&response_bytes);\n  forget_rust(result)\n}\n\n#[no_mangle]\npub extern \"C\" fn set_stream_port(notification_port: i64) -> i32 {\n  unregister_all_notification_sender();\n  register_notification_sender(DartNotificationSender::new(notification_port));\n  0\n}\n\n#[no_mangle]\npub extern \"C\" fn set_log_stream_port(port: i64) -> i32 {\n  *LOG_STREAM_ISOLATE.write().unwrap() = Some(Isolate::new(port));\n  0\n}\n\n#[inline(never)]\n#[no_mangle]\npub extern \"C\" fn link_me_please() {}\n\n#[inline(always)]\n#[allow(clippy::blocks_in_conditions)]\nasync fn post_to_flutter(response: AFPluginEventResponse, port: i64) {\n  let isolate = allo_isolate::Isolate::new(port);\n  match isolate\n    .catch_unwind(async {\n      let ffi_resp = FFIResponse::from(response);\n      ffi_resp.into_bytes().unwrap().to_vec()\n    })\n    .await\n  {\n    Ok(_) => {\n      #[cfg(feature = \"verbose_log\")]\n      trace!(\"[FFI]: Post data to dart success\");\n    },\n    Err(err) => {\n      error!(\"[FFI]: allo_isolate post failed: {:?}\", err);\n    },\n  }\n}\n\n#[no_mangle]\npub extern \"C\" fn rust_log(level: i64, data: *const c_char) {\n  if data.is_null() {\n    error!(\"[flutter error]: null pointer provided to backend_log\");\n    return;\n  }\n\n  let log_result = unsafe { CStr::from_ptr(data) }.to_str();\n\n  let log_str = match log_result {\n    Ok(str) => str,\n    Err(e) => {\n      error!(\n        \"[flutter error]: Failed to convert C string to Rust string: {:?}\",\n        e\n      );\n      return;\n    },\n  };\n\n  match level {\n    0 => info!(\"[Flutter]: {}\", log_str),\n    1 => debug!(\"[Flutter]: {}\", log_str),\n    2 => trace!(\"[Flutter]: {}\", log_str),\n    3 => warn!(\"[Flutter]: {}\", log_str),\n    4 => error!(\"[Flutter]: {}\", log_str),\n    _ => warn!(\"[flutter error]: Unsupported log level: {}\", level),\n  }\n}\n\n#[no_mangle]\npub extern \"C\" fn set_env(_data: *const c_char) {\n  // Deprecated\n}\n\nstruct LogStreamSenderImpl {\n  isolate: Isolate,\n}\nimpl StreamLogSender for LogStreamSenderImpl {\n  fn send(&self, message: &[u8]) {\n    self.isolate.post(message.to_vec());\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/dart-ffi/src/model/ffi_request.rs",
    "content": "use bytes::Bytes;\nuse flowy_derive::ProtoBuf;\nuse lib_dispatch::prelude::AFPluginRequest;\nuse std::convert::TryFrom;\n\n#[derive(Default, ProtoBuf)]\npub struct FFIRequest {\n  #[pb(index = 1)]\n  pub(crate) event: String,\n\n  #[pb(index = 2)]\n  pub(crate) payload: Vec<u8>,\n}\n\nimpl FFIRequest {\n  pub fn from_u8_pointer(pointer: *const u8, len: usize) -> Self {\n    let buffer = unsafe { std::slice::from_raw_parts(pointer, len) }.to_vec();\n    let bytes = Bytes::from(buffer);\n    let request: FFIRequest = FFIRequest::try_from(bytes).unwrap();\n    request\n  }\n}\n\nimpl std::convert::From<FFIRequest> for AFPluginRequest {\n  fn from(ffi_request: FFIRequest) -> Self {\n    AFPluginRequest::new(ffi_request.event).payload(ffi_request.payload)\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/dart-ffi/src/model/ffi_response.rs",
    "content": "use flowy_derive::{ProtoBuf, ProtoBuf_Enum};\nuse lib_dispatch::prelude::{AFPluginEventResponse, Payload, StatusCode};\n\n#[derive(ProtoBuf_Enum, Clone, Copy, Default)]\npub enum FFIStatusCode {\n  #[default]\n  Ok = 0,\n  Err = 1,\n  Internal = 2,\n}\n\n#[derive(ProtoBuf, Default)]\npub struct FFIResponse {\n  #[pb(index = 1)]\n  payload: Vec<u8>,\n\n  #[pb(index = 2)]\n  code: FFIStatusCode,\n}\n\nimpl std::convert::From<AFPluginEventResponse> for FFIResponse {\n  fn from(resp: AFPluginEventResponse) -> Self {\n    let payload = match resp.payload {\n      Payload::Bytes(bytes) => bytes.to_vec(),\n      Payload::None => vec![],\n    };\n\n    let code = match resp.status_code {\n      StatusCode::Ok => FFIStatusCode::Ok,\n      StatusCode::Err => FFIStatusCode::Err,\n    };\n\n    // let msg = match resp.error {\n    //     None => \"\".to_owned(),\n    //     Some(e) => format!(\"{:?}\", e),\n    // };\n\n    FFIResponse { payload, code }\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/dart-ffi/src/model/mod.rs",
    "content": "mod ffi_request;\nmod ffi_response;\n\npub use ffi_request::*;\npub use ffi_response::*;\n"
  },
  {
    "path": "frontend/rust-lib/dart-ffi/src/notification/mod.rs",
    "content": "mod sender;\n\npub use sender::*;\n"
  },
  {
    "path": "frontend/rust-lib/dart-ffi/src/notification/sender.rs",
    "content": "use allo_isolate::Isolate;\nuse bytes::Bytes;\nuse flowy_notification::entities::SubscribeObject;\nuse flowy_notification::NotificationSender;\nuse std::convert::TryInto;\n\npub struct DartNotificationSender {\n  isolate: Isolate,\n}\n\nimpl DartNotificationSender {\n  pub fn new(port: i64) -> Self {\n    Self {\n      isolate: Isolate::new(port),\n    }\n  }\n}\n\nimpl NotificationSender for DartNotificationSender {\n  fn send_subject(&self, subject: SubscribeObject) -> Result<(), String> {\n    let bytes: Bytes = subject.try_into().unwrap();\n    self.isolate.post(bytes.to_vec());\n    Ok(())\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/event-integration-test/src/chat_event.rs",
    "content": "use crate::event_builder::EventBuilder;\nuse crate::EventIntegrationTest;\nuse flowy_ai::entities::{\n  ChatId, ChatMessageListPB, ChatMessageTypePB, LoadNextChatMessagePB, LoadPrevChatMessagePB,\n  StreamChatPayloadPB, UpdateChatSettingsPB,\n};\nuse flowy_ai::event_map::AIEvent;\nuse flowy_folder::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB};\nuse flowy_folder::event_map::FolderEvent;\n\nimpl EventIntegrationTest {\n  pub async fn create_chat(&self, parent_id: &str) -> ViewPB {\n    let payload = CreateViewPayloadPB {\n      parent_view_id: parent_id.to_string(),\n      name: \"chat\".to_string(),\n      thumbnail: None,\n      layout: ViewLayoutPB::Chat,\n      initial_data: vec![],\n      meta: Default::default(),\n      set_as_current: true,\n      index: None,\n      section: None,\n      view_id: None,\n      extra: None,\n    };\n    EventBuilder::new(self.clone())\n      .event(FolderEvent::CreateView)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<ViewPB>()\n  }\n\n  pub async fn set_chat_rag_ids(&self, chat_id: &str, rag_ids: Vec<String>) {\n    let payload = UpdateChatSettingsPB {\n      chat_id: ChatId {\n        value: chat_id.to_string(),\n      },\n      rag_ids,\n    };\n    EventBuilder::new(self.clone())\n      .event(AIEvent::UpdateChatSettings)\n      .payload(payload)\n      .async_send()\n      .await;\n  }\n\n  pub async fn send_message(\n    &self,\n    chat_id: &str,\n    message: impl ToString,\n    message_type: ChatMessageTypePB,\n  ) {\n    let payload = StreamChatPayloadPB {\n      chat_id: chat_id.to_string(),\n      message: message.to_string(),\n      message_type,\n      answer_stream_port: 0,\n      question_stream_port: 0,\n      format: None,\n      prompt_id: None,\n    };\n\n    EventBuilder::new(self.clone())\n      .event(AIEvent::StreamMessage)\n      .payload(payload)\n      .async_send()\n      .await;\n  }\n\n  pub async fn load_prev_message(\n    &self,\n    chat_id: &str,\n    limit: i64,\n    before_message_id: Option<i64>,\n  ) -> ChatMessageListPB {\n    let payload = LoadPrevChatMessagePB {\n      chat_id: chat_id.to_string(),\n      limit,\n      before_message_id,\n    };\n    EventBuilder::new(self.clone())\n      .event(AIEvent::LoadPrevMessage)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<ChatMessageListPB>()\n  }\n\n  pub async fn load_next_message(\n    &self,\n    chat_id: &str,\n    limit: i64,\n    after_message_id: Option<i64>,\n  ) -> ChatMessageListPB {\n    let payload = LoadNextChatMessagePB {\n      chat_id: chat_id.to_string(),\n      limit,\n      after_message_id,\n    };\n    EventBuilder::new(self.clone())\n      .event(AIEvent::LoadNextMessage)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<ChatMessageListPB>()\n  }\n\n  pub async fn toggle_local_ai(&self) {\n    EventBuilder::new(self.clone())\n      .event(AIEvent::ToggleLocalAI)\n      .async_send()\n      .await;\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/event-integration-test/src/database_event.rs",
    "content": "use std::collections::HashMap;\nuse std::convert::TryFrom;\n\nuse bytes::Bytes;\nuse collab_database::database::timestamp;\nuse collab_database::fields::select_type_option::{\n  MultiSelectTypeOption, SelectOption, SingleSelectTypeOption,\n};\nuse collab_database::fields::Field;\nuse collab_database::rows::{Row, RowId};\nuse flowy_database2::entities::*;\nuse flowy_database2::event_map::DatabaseEvent;\nuse flowy_database2::services::cell::CellBuilder;\nuse flowy_database2::services::field::checklist_filter::ChecklistCellInsertChangeset;\nuse flowy_database2::services::share::csv::CSVFormat;\nuse flowy_folder::entities::*;\nuse flowy_folder::event_map::FolderEvent;\nuse flowy_user::errors::FlowyError;\n\nuse crate::event_builder::EventBuilder;\nuse crate::EventIntegrationTest;\n\nimpl EventIntegrationTest {\n  pub async fn get_database_export_data(&self, database_view_id: &str) -> String {\n    self\n      .appflowy_core\n      .database_manager\n      .get_database_editor_with_view_id(database_view_id)\n      .await\n      .unwrap()\n      .export_csv(CSVFormat::Original)\n      .await\n      .unwrap()\n  }\n\n  /// The initial data can refer to the [FolderOperationHandler::create_view_with_view_data] method.\n  pub async fn create_grid(&self, parent_id: &str, name: String, initial_data: Vec<u8>) -> ViewPB {\n    let payload = CreateViewPayloadPB {\n      parent_view_id: parent_id.to_string(),\n      name,\n      thumbnail: None,\n      layout: ViewLayoutPB::Grid,\n      initial_data,\n      meta: Default::default(),\n      set_as_current: true,\n      index: None,\n      section: None,\n      view_id: None,\n      extra: None,\n    };\n    EventBuilder::new(self.clone())\n      .event(FolderEvent::CreateView)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<ViewPB>()\n  }\n\n  pub async fn open_database(&self, view_id: &str) -> DatabasePB {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::GetDatabase)\n      .payload(DatabaseViewIdPB {\n        value: view_id.to_string(),\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<DatabasePB>()\n  }\n\n  pub async fn create_board(&self, parent_id: &str, name: String, initial_data: Vec<u8>) -> ViewPB {\n    let payload = CreateViewPayloadPB {\n      parent_view_id: parent_id.to_string(),\n      name,\n      thumbnail: None,\n      layout: ViewLayoutPB::Board,\n      initial_data,\n      meta: Default::default(),\n      set_as_current: true,\n      index: None,\n      section: None,\n      view_id: None,\n      extra: None,\n    };\n    EventBuilder::new(self.clone())\n      .event(FolderEvent::CreateView)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<ViewPB>()\n  }\n\n  pub async fn create_calendar(\n    &self,\n    parent_id: &str,\n    name: String,\n    initial_data: Vec<u8>,\n  ) -> ViewPB {\n    let payload = CreateViewPayloadPB {\n      parent_view_id: parent_id.to_string(),\n      name,\n      thumbnail: None,\n      layout: ViewLayoutPB::Calendar,\n      initial_data,\n      meta: Default::default(),\n      set_as_current: true,\n      index: None,\n      section: None,\n      view_id: None,\n      extra: None,\n    };\n    EventBuilder::new(self.clone())\n      .event(FolderEvent::CreateView)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<ViewPB>()\n  }\n\n  pub async fn get_database(&self, view_id: &str) -> DatabasePB {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::GetDatabase)\n      .payload(DatabaseViewIdPB {\n        value: view_id.to_string(),\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<DatabasePB>()\n  }\n\n  pub async fn get_all_database_fields(&self, view_id: &str) -> RepeatedFieldPB {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::GetFields)\n      .payload(GetFieldPayloadPB {\n        view_id: view_id.to_string(),\n        field_ids: None,\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<RepeatedFieldPB>()\n  }\n\n  pub async fn create_field(&self, view_id: &str, field_type: FieldType) -> FieldPB {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::CreateField)\n      .payload(CreateFieldPayloadPB {\n        view_id: view_id.to_string(),\n        field_type,\n        ..Default::default()\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<FieldPB>()\n  }\n\n  pub async fn update_field(&self, changeset: FieldChangesetPB) {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::UpdateField)\n      .payload(changeset)\n      .async_send()\n      .await;\n  }\n\n  pub async fn delete_field(&self, view_id: &str, field_id: &str) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::DeleteField)\n      .payload(DeleteFieldPayloadPB {\n        view_id: view_id.to_string(),\n        field_id: field_id.to_string(),\n      })\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn remove_calculate(\n    &self,\n    changeset: RemoveCalculationChangesetPB,\n  ) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::RemoveCalculation)\n      .payload(changeset)\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn get_all_calculations(&self, database_view_id: &str) -> RepeatedCalculationsPB {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::GetAllCalculations)\n      .payload(DatabaseViewIdPB {\n        value: database_view_id.to_string(),\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<RepeatedCalculationsPB>()\n  }\n\n  pub async fn update_calculation(\n    &self,\n    changeset: UpdateCalculationChangesetPB,\n  ) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::UpdateCalculation)\n      .payload(changeset)\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn update_field_type(\n    &self,\n    view_id: &str,\n    field_id: &str,\n    field_type: FieldType,\n  ) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::UpdateFieldType)\n      .payload(UpdateFieldTypePayloadPB {\n        view_id: view_id.to_string(),\n        field_id: field_id.to_string(),\n        field_type,\n        field_name: None,\n      })\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn duplicate_field(&self, view_id: &str, field_id: &str) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::DuplicateField)\n      .payload(DuplicateFieldPayloadPB {\n        view_id: view_id.to_string(),\n        field_id: field_id.to_string(),\n      })\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn get_primary_field(&self, database_view_id: &str) -> FieldPB {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::GetPrimaryField)\n      .payload(DatabaseViewIdPB {\n        value: database_view_id.to_string(),\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<FieldPB>()\n  }\n  pub async fn summary_row(&self, data: SummaryRowPB) {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::SummarizeRow)\n      .payload(data)\n      .async_send()\n      .await;\n  }\n\n  pub async fn translate_row(&self, data: TranslateRowPB) {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::TranslateRow)\n      .payload(data)\n      .async_send()\n      .await;\n  }\n\n  pub async fn create_row(\n    &self,\n    view_id: &str,\n    row_position: OrderObjectPositionPB,\n    data: Option<HashMap<String, String>>,\n  ) -> RowMetaPB {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::CreateRow)\n      .payload(CreateRowPayloadPB {\n        view_id: view_id.to_string(),\n        row_position,\n        group_id: None,\n        data: data.unwrap_or_default(),\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<RowMetaPB>()\n  }\n\n  pub async fn delete_row(&self, view_id: &str, row_id: &str) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::DeleteRows)\n      .payload(RepeatedRowIdPB {\n        view_id: view_id.to_string(),\n        row_ids: vec![row_id.to_string()],\n      })\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn get_row(&self, view_id: &str, row_id: &str) -> OptionalRowPB {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::GetRow)\n      .payload(DatabaseViewRowIdPB {\n        view_id: view_id.to_string(),\n        row_id: row_id.to_string(),\n        group_id: None,\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<OptionalRowPB>()\n  }\n\n  pub async fn get_row_meta(&self, view_id: &str, row_id: &str) -> RowMetaPB {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::GetRowMeta)\n      .payload(DatabaseViewRowIdPB {\n        view_id: view_id.to_string(),\n        row_id: row_id.to_string(),\n        group_id: None,\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<RowMetaPB>()\n  }\n\n  pub async fn update_row_meta(&self, changeset: UpdateRowMetaChangesetPB) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::UpdateRowMeta)\n      .payload(changeset)\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn duplicate_row(&self, view_id: &str, row_id: &str) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::DuplicateRow)\n      .payload(DatabaseViewRowIdPB {\n        view_id: view_id.to_string(),\n        row_id: row_id.to_string(),\n        group_id: None,\n      })\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn move_row(&self, view_id: &str, row_id: &str, to_row_id: &str) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::MoveRow)\n      .payload(MoveRowPayloadPB {\n        view_id: view_id.to_string(),\n        from_row_id: row_id.to_string(),\n        to_row_id: to_row_id.to_string(),\n      })\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn update_cell(&self, changeset: CellChangesetPB) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::UpdateCell)\n      .payload(changeset)\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn update_date_cell(&self, changeset: DateCellChangesetPB) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::UpdateDateCell)\n      .payload(changeset)\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn get_cell(&self, view_id: &str, row_id: &str, field_id: &str) -> CellPB {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::GetCell)\n      .payload(CellIdPB {\n        view_id: view_id.to_string(),\n        row_id: row_id.to_string(),\n        field_id: field_id.to_string(),\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<CellPB>()\n  }\n\n  pub async fn get_text_cell(&self, view_id: &str, row_id: &str, field_id: &str) -> String {\n    let cell = self.get_cell(view_id, row_id, field_id).await;\n    String::from_utf8(cell.data).unwrap()\n  }\n\n  pub async fn get_date_cell(&self, view_id: &str, row_id: &str, field_id: &str) -> DateCellDataPB {\n    let cell = self.get_cell(view_id, row_id, field_id).await;\n    DateCellDataPB::try_from(Bytes::from(cell.data)).unwrap()\n  }\n\n  pub async fn get_checklist_cell(\n    &self,\n    view_id: &str,\n    field_id: &str,\n    row_id: &str,\n  ) -> ChecklistCellDataPB {\n    let cell = self.get_cell(view_id, row_id, field_id).await;\n    ChecklistCellDataPB::try_from(Bytes::from(cell.data)).unwrap()\n  }\n\n  pub async fn get_relation_cell(\n    &self,\n    view_id: &str,\n    field_id: &str,\n    row_id: &str,\n  ) -> RelationCellDataPB {\n    let cell = self.get_cell(view_id, row_id, field_id).await;\n    RelationCellDataPB::try_from(Bytes::from(cell.data)).unwrap_or_default()\n  }\n\n  pub async fn update_checklist_cell(\n    &self,\n    changeset: ChecklistCellDataChangesetPB,\n  ) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::UpdateChecklistCell)\n      .payload(changeset)\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn insert_option(\n    &self,\n    view_id: &str,\n    field_id: &str,\n    row_id: &str,\n    name: &str,\n  ) -> Option<FlowyError> {\n    let option = EventBuilder::new(self.clone())\n      .event(DatabaseEvent::CreateSelectOption)\n      .payload(CreateSelectOptionPayloadPB {\n        field_id: field_id.to_string(),\n        view_id: view_id.to_string(),\n        option_name: name.to_string(),\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<SelectOptionPB>();\n\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::InsertOrUpdateSelectOption)\n      .payload(RepeatedSelectOptionPayload {\n        view_id: view_id.to_string(),\n        field_id: field_id.to_string(),\n        row_id: row_id.to_string(),\n        items: vec![option],\n      })\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn get_groups(&self, view_id: &str) -> Vec<GroupPB> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::GetGroups)\n      .payload(DatabaseViewIdPB {\n        value: view_id.to_string(),\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<RepeatedGroupPB>()\n      .items\n  }\n\n  pub async fn move_group(&self, view_id: &str, from_id: &str, to_id: &str) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::MoveGroup)\n      .payload(MoveGroupPayloadPB {\n        view_id: view_id.to_string(),\n        from_group_id: from_id.to_string(),\n        to_group_id: to_id.to_string(),\n      })\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn set_group_by_field(\n    &self,\n    view_id: &str,\n    field_id: &str,\n    setting_content: Vec<u8>,\n  ) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::SetGroupByField)\n      .payload(GroupByFieldPayloadPB {\n        field_id: field_id.to_string(),\n        view_id: view_id.to_string(),\n        setting_content,\n      })\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn update_group(\n    &self,\n    view_id: &str,\n    group_id: &str,\n    name: Option<String>,\n    visible: Option<bool>,\n  ) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::UpdateGroup)\n      .payload(UpdateGroupPB {\n        view_id: view_id.to_string(),\n        group_id: group_id.to_string(),\n        name,\n        visible,\n      })\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn delete_group(&self, view_id: &str, group_id: &str) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::DeleteGroup)\n      .payload(DeleteGroupPayloadPB {\n        view_id: view_id.to_string(),\n        group_id: group_id.to_string(),\n      })\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn update_setting(&self, changeset: DatabaseSettingChangesetPB) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::UpdateDatabaseSetting)\n      .payload(changeset)\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn get_all_calendar_events(&self, view_id: &str) -> Vec<CalendarEventPB> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::GetAllCalendarEvents)\n      .payload(CalendarEventRequestPB {\n        view_id: view_id.to_string(),\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<RepeatedCalendarEventPB>()\n      .items\n  }\n\n  pub async fn update_relation_cell(\n    &self,\n    changeset: RelationCellChangesetPB,\n  ) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::UpdateRelationCell)\n      .payload(changeset)\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn get_related_row_data(\n    &self,\n    database_id: String,\n    row_ids: Vec<String>,\n  ) -> Vec<RelatedRowDataPB> {\n    EventBuilder::new(self.clone())\n      .event(DatabaseEvent::GetRelatedRowDatas)\n      .payload(GetRelatedRowDataPB {\n        database_id,\n        row_ids,\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<RepeatedRelatedRowDataPB>()\n      .rows\n  }\n}\n\npub struct TestRowBuilder<'a> {\n  database_id: &'a str,\n  row_id: RowId,\n  fields: &'a [Field],\n  cell_build: CellBuilder<'a>,\n}\n\nimpl<'a> TestRowBuilder<'a> {\n  pub fn new(database_id: &'a str, row_id: RowId, fields: &'a [Field]) -> Self {\n    let cell_build = CellBuilder::with_cells(Default::default(), fields);\n    Self {\n      database_id,\n      row_id,\n      fields,\n      cell_build,\n    }\n  }\n\n  pub fn insert_text_cell(&mut self, data: &str) -> String {\n    let text_field = self.field_with_type(&FieldType::RichText);\n    self\n      .cell_build\n      .insert_text_cell(&text_field.id, data.to_string());\n\n    text_field.id.clone()\n  }\n\n  pub fn insert_number_cell(&mut self, data: &str) -> String {\n    let number_field = self.field_with_type(&FieldType::Number);\n    self\n      .cell_build\n      .insert_text_cell(&number_field.id, data.to_string());\n    number_field.id.clone()\n  }\n\n  pub fn insert_date_cell(\n    &mut self,\n    timestamp: i64,\n    include_time: Option<bool>,\n    field_type: &FieldType,\n  ) -> String {\n    let date_field = self.field_with_type(field_type);\n    self\n      .cell_build\n      .insert_date_cell(&date_field.id, timestamp, include_time);\n    date_field.id.clone()\n  }\n\n  pub fn insert_checkbox_cell(&mut self, data: &str) -> String {\n    let checkbox_field = self.field_with_type(&FieldType::Checkbox);\n    self\n      .cell_build\n      .insert_text_cell(&checkbox_field.id, data.to_string());\n\n    checkbox_field.id.clone()\n  }\n\n  pub fn insert_url_cell(&mut self, content: &str) -> String {\n    let url_field = self.field_with_type(&FieldType::URL);\n    self\n      .cell_build\n      .insert_url_cell(&url_field.id, content.to_string());\n    url_field.id.clone()\n  }\n\n  pub fn insert_single_select_cell<F>(&mut self, f: F) -> String\n  where\n    F: Fn(Vec<SelectOption>) -> SelectOption,\n  {\n    let single_select_field = self.field_with_type(&FieldType::SingleSelect);\n    let type_option = single_select_field\n      .get_type_option::<SingleSelectTypeOption>(FieldType::SingleSelect)\n      .unwrap()\n      .0;\n    let option = f(type_option.options);\n    self\n      .cell_build\n      .insert_select_option_cell(&single_select_field.id, vec![option.id]);\n\n    single_select_field.id.clone()\n  }\n\n  pub fn insert_multi_select_cell<F>(&mut self, f: F) -> String\n  where\n    F: Fn(Vec<SelectOption>) -> Vec<SelectOption>,\n  {\n    let multi_select_field = self.field_with_type(&FieldType::MultiSelect);\n    let type_option = multi_select_field\n      .get_type_option::<MultiSelectTypeOption>(FieldType::MultiSelect)\n      .unwrap()\n      .0;\n    let options = f(type_option.options);\n    let ops_ids = options\n      .iter()\n      .map(|option| option.id.clone())\n      .collect::<Vec<_>>();\n    self\n      .cell_build\n      .insert_select_option_cell(&multi_select_field.id, ops_ids);\n\n    multi_select_field.id.clone()\n  }\n\n  pub fn insert_checklist_cell(&mut self, new_tasks: Vec<ChecklistCellInsertChangeset>) -> String {\n    let checklist_field = self.field_with_type(&FieldType::Checklist);\n    self\n      .cell_build\n      .insert_checklist_cell(&checklist_field.id, new_tasks);\n    checklist_field.id.clone()\n  }\n\n  pub fn insert_time_cell(&mut self, time: i64) -> String {\n    let time_field = self.field_with_type(&FieldType::Time);\n    self.cell_build.insert_number_cell(&time_field.id, time);\n    time_field.id.clone()\n  }\n\n  pub fn insert_media_cell(&mut self, media: String) -> String {\n    let media_field = self.field_with_type(&FieldType::Media);\n    self.cell_build.insert_text_cell(&media_field.id, media);\n    media_field.id.clone()\n  }\n\n  pub fn field_with_type(&self, field_type: &FieldType) -> Field {\n    self\n      .fields\n      .iter()\n      .find(|field| {\n        let t_field_type = FieldType::from(field.field_type);\n        &t_field_type == field_type\n      })\n      .unwrap()\n      .clone()\n  }\n\n  pub fn build(self) -> Row {\n    let timestamp = timestamp();\n    Row {\n      id: self.row_id,\n      database_id: self.database_id.to_string(),\n      cells: self.cell_build.build(),\n      height: 60,\n      visibility: true,\n      modified_at: timestamp,\n      created_at: timestamp,\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/event-integration-test/src/document/document_event.rs",
    "content": "use collab::entity::EncodedCollab;\nuse std::collections::HashMap;\n\nuse flowy_document::entities::*;\nuse flowy_document::event_map::DocumentEvent;\nuse flowy_document::parser::parser_entities::{\n  ConvertDataToJsonPayloadPB, ConvertDataToJsonResponsePB, ConvertDocumentPayloadPB,\n  ConvertDocumentResponsePB,\n};\nuse flowy_folder::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB};\nuse flowy_folder::event_map::FolderEvent;\nuse serde_json::Value;\nuse uuid::Uuid;\n\nuse crate::document::utils::{gen_delta_str, gen_id, gen_text_block_data};\nuse crate::event_builder::EventBuilder;\nuse crate::EventIntegrationTest;\n\nconst TEXT_BLOCK_TY: &str = \"paragraph\";\n\npub struct DocumentEventTest {\n  event_test: EventIntegrationTest,\n}\n\npub struct OpenDocumentData {\n  pub id: String,\n  pub data: DocumentDataPB,\n}\n\nimpl DocumentEventTest {\n  pub async fn new() -> Self {\n    let sdk = EventIntegrationTest::new_anon().await;\n    Self { event_test: sdk }\n  }\n\n  pub fn new_with_core(core: EventIntegrationTest) -> Self {\n    Self { event_test: core }\n  }\n\n  pub async fn get_encoded_v1(&self, doc_id: &Uuid) -> EncodedCollab {\n    let doc = self\n      .event_test\n      .appflowy_core\n      .document_manager\n      .editable_document(doc_id)\n      .await\n      .unwrap();\n    let guard = doc.read().await;\n    guard.encode_collab().unwrap()\n  }\n\n  pub async fn get_encoded_collab(&self, doc_id: &str) -> EncodedCollabPB {\n    let core = &self.event_test;\n    let payload = OpenDocumentPayloadPB {\n      document_id: doc_id.to_string(),\n    };\n    EventBuilder::new(core.clone())\n      .event(DocumentEvent::GetDocEncodedCollab)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<EncodedCollabPB>()\n  }\n\n  pub async fn create_document(&self) -> ViewPB {\n    let core = &self.event_test;\n    let current_workspace = core.get_current_workspace().await;\n    let parent_id = current_workspace.id.clone();\n\n    let payload = CreateViewPayloadPB {\n      parent_view_id: parent_id.to_string(),\n      name: \"document\".to_string(),\n      thumbnail: None,\n      layout: ViewLayoutPB::Document,\n      initial_data: vec![],\n      meta: Default::default(),\n      set_as_current: true,\n      index: None,\n      section: None,\n      view_id: None,\n      extra: None,\n    };\n    EventBuilder::new(core.clone())\n      .event(FolderEvent::CreateView)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<ViewPB>()\n  }\n\n  pub async fn open_document(&self, doc_id: String) -> OpenDocumentData {\n    self.event_test.open_document(doc_id).await\n  }\n\n  pub async fn get_block(&self, doc_id: &str, block_id: &str) -> Option<BlockPB> {\n    let document_data = self.event_test.open_document(doc_id.to_string()).await;\n    document_data.data.blocks.get(block_id).cloned()\n  }\n\n  pub async fn get_page_id(&self, doc_id: &str) -> String {\n    let data = self.get_document_data(doc_id).await;\n    data.page_id\n  }\n\n  pub async fn get_document_data(&self, doc_id: &str) -> DocumentDataPB {\n    let document_data = self.event_test.open_document(doc_id.to_string()).await;\n    document_data.data\n  }\n\n  pub async fn get_block_children(&self, doc_id: &str, block_id: &str) -> Option<Vec<String>> {\n    let block = self.get_block(doc_id, block_id).await;\n    block.as_ref()?;\n    let document_data = self.get_document_data(doc_id).await;\n    let children_map = document_data.meta.children_map;\n    let children_id = block.unwrap().children_id;\n    children_map.get(&children_id).map(|c| c.children.clone())\n  }\n\n  pub async fn get_text_id(&self, doc_id: &str, block_id: &str) -> Option<String> {\n    let block = self.get_block(doc_id, block_id).await?;\n    block.external_id\n  }\n\n  pub async fn get_delta(&self, doc_id: &str, text_id: &str) -> Option<String> {\n    let document_data = self.get_document_data(doc_id).await;\n    document_data.meta.text_map.get(text_id).cloned()\n  }\n\n  pub async fn apply_actions(&self, payload: ApplyActionPayloadPB) {\n    let core = &self.event_test;\n    EventBuilder::new(core.clone())\n      .event(DocumentEvent::ApplyAction)\n      .payload(payload)\n      .async_send()\n      .await;\n  }\n\n  pub async fn convert_document(\n    &self,\n    payload: ConvertDocumentPayloadPB,\n  ) -> ConvertDocumentResponsePB {\n    let core = &self.event_test;\n    EventBuilder::new(core.clone())\n      .event(DocumentEvent::ConvertDocument)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<ConvertDocumentResponsePB>()\n  }\n\n  // convert data to json for document event test\n  pub async fn convert_data_to_json(\n    &self,\n    payload: ConvertDataToJsonPayloadPB,\n  ) -> ConvertDataToJsonResponsePB {\n    let core = &self.event_test;\n    EventBuilder::new(core.clone())\n      .event(DocumentEvent::ConvertDataToJSON)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<ConvertDataToJsonResponsePB>()\n  }\n\n  pub async fn create_text(&self, payload: TextDeltaPayloadPB) {\n    let core = &self.event_test;\n    EventBuilder::new(core.clone())\n      .event(DocumentEvent::CreateText)\n      .payload(payload)\n      .async_send()\n      .await;\n  }\n\n  pub async fn apply_text_delta(&self, payload: TextDeltaPayloadPB) {\n    let core = &self.event_test;\n    EventBuilder::new(core.clone())\n      .event(DocumentEvent::ApplyTextDeltaEvent)\n      .payload(payload)\n      .async_send()\n      .await;\n  }\n\n  pub async fn undo(&self, doc_id: String) -> DocumentRedoUndoResponsePB {\n    let core = &self.event_test;\n    let payload = DocumentRedoUndoPayloadPB {\n      document_id: doc_id.clone(),\n    };\n    EventBuilder::new(core.clone())\n      .event(DocumentEvent::Undo)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<DocumentRedoUndoResponsePB>()\n  }\n\n  pub async fn redo(&self, doc_id: String) -> DocumentRedoUndoResponsePB {\n    let core = &self.event_test;\n    let payload = DocumentRedoUndoPayloadPB {\n      document_id: doc_id.clone(),\n    };\n    EventBuilder::new(core.clone())\n      .event(DocumentEvent::Redo)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<DocumentRedoUndoResponsePB>()\n  }\n\n  pub async fn can_undo_redo(&self, doc_id: String) -> DocumentRedoUndoResponsePB {\n    let core = &self.event_test;\n    let payload = DocumentRedoUndoPayloadPB {\n      document_id: doc_id.clone(),\n    };\n    EventBuilder::new(core.clone())\n      .event(DocumentEvent::CanUndoRedo)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<DocumentRedoUndoResponsePB>()\n  }\n\n  pub async fn apply_delta_for_block(&self, document_id: &str, block_id: &str, delta: String) {\n    let block = self.get_block(document_id, block_id).await;\n    // Here is unsafe, but it should be fine for testing.\n    let text_id = block.unwrap().external_id.unwrap();\n    self\n      .apply_text_delta(TextDeltaPayloadPB {\n        document_id: document_id.to_string(),\n        text_id,\n        delta: Some(delta),\n      })\n      .await;\n  }\n\n  pub async fn get_document_snapshot_metas(&self, doc_id: &str) -> Vec<DocumentSnapshotMetaPB> {\n    let core = &self.event_test;\n    let payload = OpenDocumentPayloadPB {\n      document_id: doc_id.to_string(),\n    };\n    EventBuilder::new(core.clone())\n      .event(DocumentEvent::GetDocumentSnapshotMeta)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<RepeatedDocumentSnapshotMetaPB>()\n      .items\n  }\n\n  pub async fn get_document_snapshot(\n    &self,\n    snapshot_meta: DocumentSnapshotMetaPB,\n  ) -> DocumentSnapshotPB {\n    let core = &self.event_test;\n    EventBuilder::new(core.clone())\n      .event(DocumentEvent::GetDocumentSnapshot)\n      .payload(snapshot_meta)\n      .async_send()\n      .await\n      .parse_or_panic::<DocumentSnapshotPB>()\n  }\n\n  /// Insert a new text block at the index of parent's children.\n  /// return the new block id.\n  pub async fn insert_index(\n    &self,\n    document_id: &str,\n    text: &str,\n    index: usize,\n    parent_id: Option<&str>,\n  ) -> String {\n    let text = text.to_string();\n    let page_id = self.get_page_id(document_id).await;\n    let parent_id = parent_id\n      .map(|id| id.to_string())\n      .unwrap_or_else(|| page_id);\n    let parent_children = self.get_block_children(document_id, &parent_id).await;\n\n    let prev_id = {\n      // If index is 0, then the new block will be the first child of parent.\n      if index == 0 {\n        None\n      } else {\n        parent_children.and_then(|children| {\n          // If index is greater than the length of children, then the new block will be the last child of parent.\n          if index >= children.len() {\n            children.last().cloned()\n          } else {\n            children.get(index - 1).cloned()\n          }\n        })\n      }\n    };\n\n    let new_block_id = gen_id();\n    let data = gen_text_block_data();\n\n    let external_id = gen_id();\n    let external_type = \"text\".to_string();\n\n    self\n      .create_text(TextDeltaPayloadPB {\n        document_id: document_id.to_string(),\n        text_id: external_id.clone(),\n        delta: Some(gen_delta_str(&text)),\n      })\n      .await;\n\n    let new_block = BlockPB {\n      id: new_block_id.clone(),\n      ty: TEXT_BLOCK_TY.to_string(),\n      data,\n      parent_id: parent_id.clone(),\n      children_id: gen_id(),\n      external_id: Some(external_id),\n      external_type: Some(external_type),\n    };\n    let action = BlockActionPB {\n      action: BlockActionTypePB::Insert,\n      payload: BlockActionPayloadPB {\n        block: Some(new_block),\n        prev_id,\n        parent_id: Some(parent_id),\n        text_id: None,\n        delta: None,\n      },\n    };\n    let payload = ApplyActionPayloadPB {\n      document_id: document_id.to_string(),\n      actions: vec![action],\n    };\n    self.apply_actions(payload).await;\n    new_block_id\n  }\n\n  pub async fn update_data(&self, document_id: &str, block_id: &str, data: HashMap<String, Value>) {\n    let block = self.get_block(document_id, block_id).await.unwrap();\n\n    let new_block = {\n      let mut new_block = block.clone();\n      new_block.data = serde_json::to_string(&data).unwrap();\n      new_block\n    };\n    let action = BlockActionPB {\n      action: BlockActionTypePB::Update,\n      payload: BlockActionPayloadPB {\n        block: Some(new_block),\n        prev_id: None,\n        parent_id: Some(block.parent_id.clone()),\n        text_id: None,\n        delta: None,\n      },\n    };\n    let payload = ApplyActionPayloadPB {\n      document_id: document_id.to_string(),\n      actions: vec![action],\n    };\n    self.apply_actions(payload).await;\n  }\n\n  pub async fn delete(&self, document_id: &str, block_id: &str) {\n    let block = self.get_block(document_id, block_id).await.unwrap();\n    let parent_id = block.parent_id.clone();\n    let action = BlockActionPB {\n      action: BlockActionTypePB::Delete,\n      payload: BlockActionPayloadPB {\n        block: Some(block),\n        prev_id: None,\n        parent_id: Some(parent_id),\n        text_id: None,\n        delta: None,\n      },\n    };\n    let payload = ApplyActionPayloadPB {\n      document_id: document_id.to_string(),\n      actions: vec![action],\n    };\n    self.apply_actions(payload).await;\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/event-integration-test/src/document/mod.rs",
    "content": "pub mod document_event;\npub mod utils;\n"
  },
  {
    "path": "frontend/rust-lib/event-integration-test/src/document/utils.rs",
    "content": "use crate::document::document_event::*;\nuse flowy_document::entities::*;\nuse nanoid::nanoid;\nuse serde_json::json;\nuse std::collections::HashMap;\n\npub fn gen_id() -> String {\n  nanoid!(10)\n}\n\npub fn gen_text_block_data() -> String {\n  json!({}).to_string()\n}\n\npub fn gen_delta_str(text: &str) -> String {\n  json!([{ \"insert\": text }]).to_string()\n}\n\npub struct ParseDocumentData {\n  pub doc_id: String,\n  pub page_id: String,\n  pub blocks: HashMap<String, BlockPB>,\n  pub children_map: HashMap<String, ChildrenPB>,\n  pub first_block_id: String,\n}\npub fn parse_document_data(document: OpenDocumentData) -> ParseDocumentData {\n  let doc_id = document.id.clone();\n  let data = document.data;\n  let page_id = data.page_id;\n  let blocks = data.blocks;\n  let children_map = data.meta.children_map;\n  let page_block = blocks.get(&page_id).unwrap();\n  let children_id = page_block.children_id.clone();\n  let children = children_map.get(&children_id).unwrap();\n  let block_id = children.children.first().unwrap().to_string();\n  ParseDocumentData {\n    doc_id,\n    page_id,\n    blocks,\n    children_map,\n    first_block_id: block_id,\n  }\n}\n\npub fn gen_insert_block_action(document: OpenDocumentData) -> BlockActionPB {\n  let parse_data = parse_document_data(document);\n  let first_block_id = parse_data.first_block_id;\n  let block = parse_data.blocks.get(&first_block_id).unwrap();\n  let page_id = parse_data.page_id;\n  let data = block.data.clone();\n  let new_block_id = gen_id();\n  let new_block = BlockPB {\n    id: new_block_id,\n    ty: block.ty.clone(),\n    data,\n    parent_id: page_id.clone(),\n    children_id: gen_id(),\n    external_id: None,\n    external_type: None,\n  };\n  BlockActionPB {\n    action: BlockActionTypePB::Insert,\n    payload: BlockActionPayloadPB {\n      block: Some(new_block),\n      prev_id: Some(first_block_id),\n      parent_id: Some(page_id),\n      text_id: None,\n      delta: None,\n    },\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/event-integration-test/src/document_event.rs",
    "content": "use collab::core::origin::CollabOrigin;\nuse collab::preclude::updates::decoder::Decode;\nuse collab::preclude::{Collab, Update};\nuse collab_document::blocks::DocumentData;\nuse collab_document::document::Document;\nuse collab_entity::CollabType;\n\nuse flowy_document::entities::{DocumentDataPB, OpenDocumentPayloadPB};\nuse flowy_document::event_map::DocumentEvent;\nuse flowy_folder::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB};\nuse flowy_folder::event_map::FolderEvent;\n\nuse crate::document::document_event::{DocumentEventTest, OpenDocumentData};\nuse crate::event_builder::EventBuilder;\nuse crate::EventIntegrationTest;\n\nimpl EventIntegrationTest {\n  pub async fn create_document(&self, name: &str) -> ViewPB {\n    let current_workspace = self.get_current_workspace().await;\n    self\n      .create_and_open_document(&current_workspace.id, name.to_string(), vec![])\n      .await\n  }\n\n  pub async fn create_and_open_document(\n    &self,\n    parent_id: &str,\n    name: String,\n    initial_data: Vec<u8>,\n  ) -> ViewPB {\n    let payload = CreateViewPayloadPB {\n      parent_view_id: parent_id.to_string(),\n      name,\n      thumbnail: None,\n      layout: ViewLayoutPB::Document,\n      initial_data,\n      meta: Default::default(),\n      set_as_current: true,\n      index: None,\n      section: None,\n      view_id: None,\n      extra: None,\n    };\n    let view = EventBuilder::new(self.clone())\n      .event(FolderEvent::CreateView)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<ViewPB>();\n\n    let payload = OpenDocumentPayloadPB {\n      document_id: view.id.clone(),\n    };\n\n    let _ = EventBuilder::new(self.clone())\n      .event(DocumentEvent::OpenDocument)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<DocumentDataPB>();\n\n    view\n  }\n\n  pub async fn open_document(&self, doc_id: String) -> OpenDocumentData {\n    let payload = OpenDocumentPayloadPB {\n      document_id: doc_id.clone(),\n    };\n    let data = EventBuilder::new(self.clone())\n      .event(DocumentEvent::OpenDocument)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<DocumentDataPB>();\n    OpenDocumentData { id: doc_id, data }\n  }\n  pub async fn insert_document_text(&self, document_id: &str, text: &str, index: usize) {\n    let document_event = DocumentEventTest::new_with_core(self.clone());\n    document_event\n      .insert_index(document_id, text, index, None)\n      .await;\n  }\n\n  pub async fn get_document_data(&self, view_id: &str) -> DocumentData {\n    let pb = EventBuilder::new(self.clone())\n      .event(DocumentEvent::GetDocumentData)\n      .payload(OpenDocumentPayloadPB {\n        document_id: view_id.to_string(),\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<DocumentDataPB>();\n\n    DocumentData::from(pb)\n  }\n\n  pub async fn get_document_doc_state(&self, document_id: &str) -> Vec<u8> {\n    self\n      .get_collab_doc_state(document_id, CollabType::Document)\n      .await\n      .unwrap()\n  }\n}\n\npub fn assert_document_data_equal(doc_state: &[u8], doc_id: &str, expected: DocumentData) {\n  let mut collab = Collab::new_with_origin(CollabOrigin::Server, doc_id, vec![], false);\n  {\n    let update = Update::decode_v1(doc_state).unwrap();\n    let mut txn = collab.transact_mut();\n    txn.apply_update(update).unwrap();\n  };\n  let document = Document::open(collab).unwrap();\n  let actual = document.get_document_data().unwrap();\n  assert_eq!(actual, expected);\n}\n"
  },
  {
    "path": "frontend/rust-lib/event-integration-test/src/event_builder.rs",
    "content": "use crate::EventIntegrationTest;\nuse flowy_user::errors::{internal_error, FlowyError, FlowyResult};\nuse lib_dispatch::prelude::{\n  AFPluginDispatcher, AFPluginEventResponse, AFPluginFromBytes, AFPluginRequest, ToBytes, *,\n};\nuse std::sync::Arc;\nuse std::{\n  convert::TryFrom,\n  fmt::{Debug, Display},\n  hash::Hash,\n};\nuse tokio::task::LocalSet;\n\n// #[derive(Clone)]\npub struct EventBuilder {\n  context: TestContext,\n  local_set: LocalSet,\n}\n\nimpl EventBuilder {\n  pub fn new(sdk: EventIntegrationTest) -> Self {\n    Self {\n      context: TestContext::new(sdk),\n      local_set: Default::default(),\n    }\n  }\n\n  pub fn payload<P>(mut self, payload: P) -> Self\n  where\n    P: ToBytes,\n  {\n    match payload.into_bytes() {\n      Ok(bytes) => {\n        let module_request = self.get_request();\n        self.context.request = Some(module_request.payload(bytes))\n      },\n      Err(e) => {\n        tracing::error!(\"Set payload failed: {:?}\", e);\n      },\n    }\n    self\n  }\n\n  pub fn event<Event>(mut self, event: Event) -> Self\n  where\n    Event: Eq + Hash + Debug + Clone + Display,\n  {\n    self.context.request = Some(AFPluginRequest::new(event));\n    self\n  }\n\n  pub async fn async_send(mut self) -> Self {\n    let request = self.get_request();\n    let resp = self\n      .local_set\n      .run_until(AFPluginDispatcher::async_send(\n        self.dispatch().as_ref(),\n        request,\n      ))\n      .await;\n    self.context.response = Some(resp);\n    self\n  }\n\n  pub fn parse_or_panic<R>(self) -> R\n  where\n    R: AFPluginFromBytes,\n  {\n    let response = self.get_response();\n    match response.clone().parse::<R, FlowyError>() {\n      Ok(Ok(data)) => data,\n      Ok(Err(e)) => {\n        panic!(\"Parser {:?} failed: {:?}\", std::any::type_name::<R>(), e)\n      },\n      Err(e) => {\n        panic!(\"Parser {:?} failed: {:?}\", std::any::type_name::<R>(), e)\n      },\n    }\n  }\n\n  pub fn parse<R>(self) -> FlowyResult<R>\n  where\n    R: AFPluginFromBytes,\n  {\n    let response = self.get_response();\n    match response.clone().parse::<R, FlowyError>() {\n      Ok(Ok(data)) => Ok(data),\n      Ok(Err(e)) => Err(e),\n      Err(e) => {\n        panic!(\"Parser {:?} failed: {:?}\", std::any::type_name::<R>(), e)\n      },\n    }\n  }\n\n  pub fn try_parse<R>(self) -> Result<R, FlowyError>\n  where\n    R: AFPluginFromBytes,\n  {\n    let response = self.get_response();\n    response.parse::<R, FlowyError>().map_err(internal_error)?\n  }\n\n  pub fn error(self) -> Option<FlowyError> {\n    let response = self.get_response();\n    <AFPluginData<FlowyError>>::try_from(response.payload)\n      .ok()\n      .map(|data| data.into_inner())\n  }\n\n  fn dispatch(&self) -> Arc<AFPluginDispatcher> {\n    self.context.sdk.dispatcher()\n  }\n\n  fn get_response(&self) -> AFPluginEventResponse {\n    self\n      .context\n      .response\n      .as_ref()\n      .expect(\"must call sync_send/async_send first\")\n      .clone()\n  }\n\n  fn get_request(&mut self) -> AFPluginRequest {\n    self.context.request.take().expect(\"must call event first\")\n  }\n}\n\n#[derive(Clone)]\npub struct TestContext {\n  pub sdk: EventIntegrationTest,\n  request: Option<AFPluginRequest>,\n  response: Option<AFPluginEventResponse>,\n}\n\nimpl TestContext {\n  pub fn new(sdk: EventIntegrationTest) -> Self {\n    Self {\n      sdk,\n      request: None,\n      response: None,\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/event-integration-test/src/folder_event.rs",
    "content": "use flowy_folder::view_operation::{GatherEncodedCollab, ViewData};\nuse std::str::FromStr;\nuse std::sync::Arc;\n\nuse collab_folder::{FolderData, View};\nuse flowy_folder::entities::icon::UpdateViewIconPayloadPB;\nuse flowy_folder::event_map::FolderEvent;\nuse flowy_folder::event_map::FolderEvent::*;\nuse flowy_folder::{entities::*, ViewLayout};\nuse flowy_folder_pub::entities::PublishPayload;\nuse flowy_search::services::manager::{SearchHandler, SearchType};\nuse flowy_user::entities::{\n  AcceptWorkspaceInvitationPB, QueryWorkspacePB, RemoveWorkspaceMemberPB,\n  RepeatedWorkspaceInvitationPB, RepeatedWorkspaceMemberPB, UserWorkspaceIdPB, UserWorkspacePB,\n  WorkspaceMemberInvitationPB, WorkspaceMemberPB,\n};\nuse flowy_user::errors::{FlowyError, FlowyResult};\nuse flowy_user::event_map::UserEvent;\nuse flowy_user_pub::entities::Role;\nuse uuid::Uuid;\n\nuse crate::event_builder::EventBuilder;\nuse crate::EventIntegrationTest;\n\nimpl EventIntegrationTest {\n  pub async fn invite_workspace_member(&self, workspace_id: &str, email: &str, role: Role) {\n    EventBuilder::new(self.clone())\n      .event(UserEvent::InviteWorkspaceMember)\n      .payload(WorkspaceMemberInvitationPB {\n        workspace_id: workspace_id.to_string(),\n        invitee_email: email.to_string(),\n        role: role.into(),\n      })\n      .async_send()\n      .await;\n  }\n\n  // convenient function to add workspace member by inviting and accepting the invitation\n  pub async fn add_workspace_member(&self, workspace_id: &str, other: &EventIntegrationTest) {\n    let other_email = other.get_user_profile().await.unwrap().email;\n\n    self\n      .invite_workspace_member(workspace_id, &other_email, Role::Member)\n      .await;\n\n    let invitations = other.list_workspace_invitations().await;\n    let target_invi = invitations\n      .items\n      .into_iter()\n      .find(|i| i.workspace_id == workspace_id)\n      .unwrap();\n\n    other\n      .accept_workspace_invitation(&target_invi.invite_id)\n      .await;\n  }\n\n  pub async fn list_workspace_invitations(&self) -> RepeatedWorkspaceInvitationPB {\n    EventBuilder::new(self.clone())\n      .event(UserEvent::ListWorkspaceInvitations)\n      .async_send()\n      .await\n      .parse_or_panic()\n  }\n\n  pub async fn accept_workspace_invitation(&self, invitation_id: &str) {\n    if let Some(err) = EventBuilder::new(self.clone())\n      .event(UserEvent::AcceptWorkspaceInvitation)\n      .payload(AcceptWorkspaceInvitationPB {\n        invite_id: invitation_id.to_string(),\n      })\n      .async_send()\n      .await\n      .error()\n    {\n      panic!(\"Accept workspace invitation failed: {:?}\", err)\n    };\n  }\n\n  pub async fn delete_workspace_member(&self, workspace_id: &str, email: &str) {\n    if let Some(err) = EventBuilder::new(self.clone())\n      .event(UserEvent::RemoveWorkspaceMember)\n      .payload(RemoveWorkspaceMemberPB {\n        workspace_id: workspace_id.to_string(),\n        email: email.to_string(),\n      })\n      .async_send()\n      .await\n      .error()\n    {\n      panic!(\"Delete workspace member failed: {:?}\", err)\n    };\n  }\n\n  pub async fn get_workspace_members(&self, workspace_id: &str) -> Vec<WorkspaceMemberPB> {\n    EventBuilder::new(self.clone())\n      .event(UserEvent::GetWorkspaceMembers)\n      .payload(QueryWorkspacePB {\n        workspace_id: workspace_id.to_string(),\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<RepeatedWorkspaceMemberPB>()\n      .items\n  }\n\n  pub async fn get_current_workspace(&self) -> WorkspacePB {\n    EventBuilder::new(self.clone())\n      .event(FolderEvent::ReadCurrentWorkspace)\n      .async_send()\n      .await\n      .parse_or_panic::<WorkspacePB>()\n  }\n\n  pub async fn get_workspace_id(&self) -> Uuid {\n    let a = EventBuilder::new(self.clone())\n      .event(FolderEvent::ReadCurrentWorkspace)\n      .async_send()\n      .await\n      .parse_or_panic::<WorkspacePB>();\n    Uuid::from_str(&a.id).unwrap()\n  }\n\n  pub async fn get_user_workspace(&self, workspace_id: &str) -> UserWorkspacePB {\n    let payload = UserWorkspaceIdPB {\n      workspace_id: workspace_id.to_string(),\n    };\n    EventBuilder::new(self.clone())\n      .event(UserEvent::GetUserWorkspace)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<UserWorkspacePB>()\n  }\n\n  pub fn get_folder_search_handler(&self) -> Arc<dyn SearchHandler> {\n    self\n      .appflowy_core\n      .search_manager\n      .get_handler(SearchType::Folder)\n      .unwrap()\n  }\n\n  /// create views in the folder.\n  pub async fn create_views(&self, views: Vec<View>) {\n    let create_view_params = views\n      .into_iter()\n      .map(|view| CreateViewParams {\n        parent_view_id: Uuid::from_str(&view.parent_view_id).unwrap(),\n        name: view.name,\n        layout: view.layout.into(),\n        view_id: Uuid::from_str(&view.id).unwrap(),\n        initial_data: ViewData::Empty,\n        meta: Default::default(),\n        set_as_current: false,\n        index: None,\n        section: None,\n        icon: view.icon,\n        extra: view.extra,\n      })\n      .collect::<Vec<_>>();\n\n    for params in create_view_params {\n      self\n        .appflowy_core\n        .folder_manager\n        .create_view_with_params(params, true)\n        .await\n        .unwrap();\n    }\n  }\n\n  /// Create orphan views in the folder.\n  /// Orphan view: the parent_view_id equal to the view_id\n  /// Normally, the orphan view will be created in nested database\n  pub async fn create_orphan_view(&self, name: &str, view_id: &str, layout: ViewLayoutPB) {\n    let payload = CreateOrphanViewPayloadPB {\n      name: name.to_string(),\n      layout,\n      view_id: view_id.to_string(),\n      initial_data: vec![],\n    };\n    EventBuilder::new(self.clone())\n      .event(FolderEvent::CreateOrphanView)\n      .payload(payload)\n      .async_send()\n      .await;\n  }\n\n  pub async fn get_folder_data(&self) -> FolderData {\n    self\n      .appflowy_core\n      .folder_manager\n      .get_folder_data()\n      .await\n      .unwrap()\n  }\n\n  pub async fn get_publish_payload(\n    &self,\n    view_id: &str,\n    include_children: bool,\n  ) -> Vec<PublishPayload> {\n    let manager = self.folder_manager.clone();\n    let payload = manager\n      .get_batch_publish_payload(view_id, None, include_children)\n      .await;\n\n    if payload.is_err() {\n      panic!(\"Get publish payload failed\")\n    }\n\n    payload.unwrap()\n  }\n\n  pub async fn gather_encode_collab_from_disk(\n    &self,\n    view_id: &str,\n    layout: ViewLayout,\n  ) -> GatherEncodedCollab {\n    let view_id = Uuid::from_str(view_id).unwrap();\n    self\n      .folder_manager\n      .gather_publish_encode_collab(&view_id, &layout)\n      .await\n      .unwrap()\n  }\n\n  pub async fn get_all_workspace_views(&self) -> Vec<ViewPB> {\n    EventBuilder::new(self.clone())\n      .event(FolderEvent::ReadCurrentWorkspaceViews)\n      .async_send()\n      .await\n      .parse_or_panic::<RepeatedViewPB>()\n      .items\n  }\n\n  // get all the views in the current workspace, including the views in the trash and the orphan views\n  pub async fn get_all_views(&self) -> Vec<ViewPB> {\n    EventBuilder::new(self.clone())\n      .event(FolderEvent::GetAllViews)\n      .async_send()\n      .await\n      .parse_or_panic::<RepeatedViewPB>()\n      .items\n  }\n\n  pub async fn get_trash(&self) -> RepeatedTrashPB {\n    EventBuilder::new(self.clone())\n      .event(FolderEvent::ListTrashItems)\n      .async_send()\n      .await\n      .parse_or_panic::<RepeatedTrashPB>()\n  }\n\n  pub async fn delete_view(&self, view_id: &str) {\n    let payload = RepeatedViewIdPB {\n      items: vec![view_id.to_string()],\n    };\n\n    // delete the view. the view will be moved to trash\n    if let Some(err) = EventBuilder::new(self.clone())\n      .event(FolderEvent::DeleteView)\n      .payload(payload)\n      .async_send()\n      .await\n      .error()\n    {\n      panic!(\"Delete view failed: {:?}\", err)\n    };\n  }\n\n  pub async fn update_view(&self, changeset: UpdateViewPayloadPB) -> Option<FlowyError> {\n    // delete the view. the view will be moved to trash\n    EventBuilder::new(self.clone())\n      .event(FolderEvent::UpdateView)\n      .payload(changeset)\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn update_view_icon(&self, payload: UpdateViewIconPayloadPB) -> Option<FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(FolderEvent::UpdateViewIcon)\n      .payload(payload)\n      .async_send()\n      .await\n      .error()\n  }\n\n  pub async fn create_view(&self, parent_id: &str, name: String) -> ViewPB {\n    self\n      .create_view_with_layout(parent_id, name, Default::default())\n      .await\n  }\n\n  pub async fn create_view_with_layout(\n    &self,\n    parent_id: &str,\n    name: String,\n    layout: ViewLayoutPB,\n  ) -> ViewPB {\n    let payload = CreateViewPayloadPB {\n      parent_view_id: parent_id.to_string(),\n      name,\n      thumbnail: None,\n      layout,\n      initial_data: vec![],\n      meta: Default::default(),\n      set_as_current: false,\n      index: None,\n      section: None,\n      view_id: None,\n      extra: None,\n    };\n    EventBuilder::new(self.clone())\n      .event(FolderEvent::CreateView)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<ViewPB>()\n  }\n\n  pub async fn get_view(&self, view_id: &str) -> ViewPB {\n    EventBuilder::new(self.clone())\n      .event(FolderEvent::GetView)\n      .payload(ViewIdPB {\n        value: view_id.to_string(),\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<ViewPB>()\n  }\n\n  pub async fn import_data(&self, data: ImportPayloadPB) -> FlowyResult<RepeatedViewPB> {\n    EventBuilder::new(self.clone())\n      .event(FolderEvent::ImportData)\n      .payload(data)\n      .async_send()\n      .await\n      .parse::<RepeatedViewPB>()\n  }\n\n  pub async fn get_view_ancestors(&self, view_id: &str) -> Vec<ViewPB> {\n    EventBuilder::new(self.clone())\n      .event(FolderEvent::GetViewAncestors)\n      .payload(ViewIdPB {\n        value: view_id.to_string(),\n      })\n      .async_send()\n      .await\n      .parse_or_panic::<RepeatedViewPB>()\n      .items\n  }\n}\n\npub struct ViewTest {\n  pub sdk: EventIntegrationTest,\n  pub workspace: WorkspacePB,\n  pub child_view: ViewPB,\n}\nimpl ViewTest {\n  #[allow(dead_code)]\n  pub async fn new(sdk: &EventIntegrationTest, layout: ViewLayout, data: Vec<u8>) -> Self {\n    let workspace = sdk.folder_manager.get_current_workspace().await.unwrap();\n\n    let payload = CreateViewPayloadPB {\n      parent_view_id: workspace.id.clone(),\n      name: \"View A\".to_string(),\n      thumbnail: Some(\"http://1.png\".to_string()),\n      layout: layout.into(),\n      initial_data: data,\n      meta: Default::default(),\n      set_as_current: true,\n      index: None,\n      section: None,\n      view_id: None,\n      extra: None,\n    };\n\n    let view = EventBuilder::new(sdk.clone())\n      .event(CreateView)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<ViewPB>();\n\n    Self {\n      sdk: sdk.clone(),\n      workspace,\n      child_view: view,\n    }\n  }\n\n  pub async fn new_grid_view(sdk: &EventIntegrationTest, data: Vec<u8>) -> Self {\n    Self::new(sdk, ViewLayout::Grid, data).await\n  }\n\n  pub async fn new_board_view(sdk: &EventIntegrationTest, data: Vec<u8>) -> Self {\n    Self::new(sdk, ViewLayout::Board, data).await\n  }\n\n  pub async fn new_calendar_view(sdk: &EventIntegrationTest, data: Vec<u8>) -> Self {\n    Self::new(sdk, ViewLayout::Calendar, data).await\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/event-integration-test/src/lib.rs",
    "content": "use crate::user_event::TestNotificationSender;\nuse collab::core::collab::DataSource;\nuse collab::core::origin::CollabOrigin;\nuse collab::preclude::Collab;\nuse collab_document::blocks::DocumentData;\nuse collab_document::document::Document;\nuse collab_entity::CollabType;\nuse flowy_core::config::AppFlowyCoreConfig;\nuse flowy_core::AppFlowyCore;\nuse flowy_notification::register_notification_sender;\nuse flowy_user::entities::AuthTypePB;\nuse flowy_user::errors::FlowyError;\nuse lib_dispatch::runtime::AFPluginRuntime;\nuse nanoid::nanoid;\nuse semver::Version;\nuse std::env::temp_dir;\nuse std::path::PathBuf;\nuse std::str::FromStr;\nuse std::sync::atomic::{AtomicBool, AtomicU8, Ordering};\nuse std::sync::Arc;\nuse std::time::Duration;\nuse tokio::select;\nuse tokio::task::LocalSet;\nuse tokio::time::sleep;\nuse uuid::Uuid;\n\nmod chat_event;\npub mod database_event;\npub mod document;\npub mod document_event;\npub mod event_builder;\npub mod folder_event;\npub mod user_event;\n\n#[derive(Clone)]\npub struct EventIntegrationTest {\n  pub authenticator: Arc<AtomicU8>,\n  pub appflowy_core: AppFlowyCore,\n  #[allow(dead_code)]\n  cleaner: Arc<Cleaner>,\n  pub notification_sender: TestNotificationSender,\n  local_set: Arc<LocalSet>,\n}\n\npub const SINGLE_FILE_UPLOAD_SIZE: usize = 15 * 1024 * 1024;\n\nimpl EventIntegrationTest {\n  pub async fn new() -> Self {\n    Self::new_with_name(nanoid!(6)).await\n  }\n\n  pub async fn new_with_name<T: ToString>(name: T) -> Self {\n    let temp_dir = temp_dir().join(nanoid!(6));\n    std::fs::create_dir_all(&temp_dir).unwrap();\n    Self::new_with_user_data_path(temp_dir, name.to_string()).await\n  }\n\n  pub async fn new_with_config(config: AppFlowyCoreConfig) -> Self {\n    let clean_path = config.storage_path.clone();\n    let inner = init_core(config).await;\n    let notification_sender = TestNotificationSender::new();\n    let authenticator = Arc::new(AtomicU8::new(AuthTypePB::Local as u8));\n    register_notification_sender(notification_sender.clone());\n\n    // In case of dropping the runtime that runs the core, we need to forget the dispatcher\n    std::mem::forget(inner.dispatcher());\n    Self {\n      appflowy_core: inner,\n      authenticator,\n      notification_sender,\n      cleaner: Arc::new(Cleaner::new(PathBuf::from(clean_path))),\n      #[allow(clippy::arc_with_non_send_sync)]\n      local_set: Arc::new(Default::default()),\n    }\n  }\n\n  pub async fn new_with_user_data_path(path_buf: PathBuf, name: String) -> Self {\n    let path = path_buf.to_str().unwrap().to_string();\n    let device_id = uuid::Uuid::new_v4().to_string();\n    let mut config = AppFlowyCoreConfig::new(\n      Version::new(0, 7, 0),\n      path.clone(),\n      path,\n      device_id,\n      \"test\".to_string(),\n      name,\n    )\n    .log_filter(\n      \"trace\",\n      vec![\n        \"flowy_test\".to_string(),\n        \"tokio\".to_string(),\n        // \"lib_dispatch\".to_string(),\n      ],\n    );\n\n    if let Some(cloud_config) = config.cloud_config.as_mut() {\n      cloud_config.maximum_upload_file_size_in_bytes = Some(SINGLE_FILE_UPLOAD_SIZE as u64);\n    }\n    Self::new_with_config(config).await\n  }\n\n  pub fn skip_auto_remove_temp_dir(&mut self) {\n    self.cleaner.should_clean.store(false, Ordering::Release);\n  }\n\n  pub fn instance_name(&self) -> String {\n    self.appflowy_core.config.name.clone()\n  }\n\n  pub fn user_data_path(&self) -> String {\n    self.appflowy_core.config.application_path.clone()\n  }\n\n  pub async fn wait_ws_connected(&self) {\n    if self\n      .appflowy_core\n      .server_provider\n      .get_server()\n      .unwrap()\n      .get_ws_state()\n      .is_connected()\n    {\n      return;\n    }\n\n    let mut ws_state = self\n      .appflowy_core\n      .server_provider\n      .get_server()\n      .unwrap()\n      .subscribe_ws_state()\n      .unwrap();\n    loop {\n      select! {\n        _ = sleep(Duration::from_secs(20)) => {\n          panic!(\"wait_ws_connected timeout\");\n        }\n        state = ws_state.recv() => {\n          if let Ok(state) = &state {\n            if state.is_connected() {\n              break;\n            }\n          }\n        }\n      }\n    }\n  }\n\n  pub async fn get_collab_doc_state(\n    &self,\n    oid: &str,\n    collab_type: CollabType,\n  ) -> Result<Vec<u8>, FlowyError> {\n    let server = self.server_provider.get_server()?;\n\n    let workspace_id = self.get_current_workspace().await.id;\n    let oid = Uuid::from_str(oid)?;\n    let uid = self.get_user_profile().await?.id;\n    let doc_state = server\n      .folder_service()\n      .get_folder_doc_state(\n        &Uuid::from_str(&workspace_id).unwrap(),\n        uid,\n        collab_type,\n        &oid,\n      )\n      .await?;\n\n    Ok(doc_state)\n  }\n}\n\npub fn document_data_from_document_doc_state(doc_id: &str, doc_state: Vec<u8>) -> DocumentData {\n  document_from_document_doc_state(doc_id, doc_state)\n    .get_document_data()\n    .unwrap()\n}\n\npub fn document_from_document_doc_state(doc_id: &str, doc_state: Vec<u8>) -> Document {\n  let collab = Collab::new_with_source(\n    CollabOrigin::Empty,\n    doc_id,\n    DataSource::DocStateV1(doc_state),\n    vec![],\n    true,\n  )\n  .unwrap();\n  Document::open(collab).unwrap()\n}\n\nasync fn init_core(config: AppFlowyCoreConfig) -> AppFlowyCore {\n  let runtime = Arc::new(AFPluginRuntime::new().unwrap());\n  let cloned_runtime = runtime.clone();\n  AppFlowyCore::new(config, cloned_runtime, None).await\n}\n\nimpl std::ops::Deref for EventIntegrationTest {\n  type Target = AppFlowyCore;\n\n  fn deref(&self) -> &Self::Target {\n    &self.appflowy_core\n  }\n}\n\npub struct Cleaner {\n  dir: PathBuf,\n  should_clean: AtomicBool,\n}\n\nimpl Cleaner {\n  pub fn new(dir: PathBuf) -> Self {\n    Self {\n      dir,\n      should_clean: AtomicBool::new(true),\n    }\n  }\n\n  fn cleanup(dir: &PathBuf) {\n    let _ = std::fs::remove_dir_all(dir);\n  }\n}\n\nimpl Drop for Cleaner {\n  fn drop(&mut self) {\n    if self.should_clean.load(Ordering::Acquire) {\n      Self::cleanup(&self.dir)\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/rust-lib/event-integration-test/src/user_event.rs",
    "content": "use std::collections::HashMap;\nuse std::convert::TryFrom;\nuse std::sync::atomic::Ordering;\nuse std::sync::Arc;\n\nuse bytes::Bytes;\nuse flowy_folder::entities::{RepeatedViewPB, WorkspacePB};\nuse protobuf::ProtobufError;\nuse tokio::sync::broadcast::{channel, Sender};\nuse tracing::error;\nuse uuid::Uuid;\n\nuse crate::event_builder::EventBuilder;\nuse crate::EventIntegrationTest;\nuse flowy_folder::event_map::FolderEvent;\nuse flowy_notification::entities::SubscribeObject;\nuse flowy_notification::NotificationSender;\nuse flowy_server::af_cloud::define::{USER_DEVICE_ID, USER_EMAIL, USER_SIGN_IN_URL, USER_UUID};\nuse flowy_server_pub::af_cloud_config::AFCloudConfiguration;\nuse flowy_server_pub::AuthenticatorType;\nuse flowy_user::entities::{\n  AuthTypePB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, ImportAppFlowyDataPB,\n  OauthSignInPB, OpenUserWorkspacePB, RenameWorkspacePB, RepeatedUserWorkspacePB, SignInUrlPB,\n  SignInUrlPayloadPB, SignUpPayloadPB, UpdateCloudConfigPB, UpdateUserProfilePayloadPB,\n  UserProfilePB, UserWorkspaceIdPB, UserWorkspacePB, WorkspaceTypePB,\n};\nuse flowy_user::errors::{FlowyError, FlowyResult};\nuse flowy_user::event_map::UserEvent;\nuse flowy_user_pub::entities::WorkspaceType;\nuse lib_dispatch::prelude::{AFPluginDispatcher, AFPluginRequest, ToBytes};\n\nimpl EventIntegrationTest {\n  pub async fn enable_encryption(&self) -> String {\n    let config = EventBuilder::new(self.clone())\n      .event(UserEvent::GetCloudConfig)\n      .async_send()\n      .await\n      .parse_or_panic::<CloudSettingPB>();\n    let update = UpdateCloudConfigPB {\n      enable_sync: None,\n      enable_encrypt: Some(true),\n    };\n    let error = EventBuilder::new(self.clone())\n      .event(UserEvent::SetCloudConfig)\n      .payload(update)\n      .async_send()\n      .await\n      .error();\n    assert!(error.is_none());\n    config.encrypt_secret\n  }\n\n  /// Create a anonymous user for given test.\n  pub async fn new_anon() -> Self {\n    let test = Self::new().await;\n    test.sign_up_as_anon().await;\n    test\n  }\n\n  pub async fn sign_up_as_anon(&self) -> SignUpContext {\n    let password = login_password();\n    let email = \"anon@appflowy.io\".to_string();\n    let payload = SignUpPayloadPB {\n      email,\n      name: \"appflowy\".to_string(),\n      password: password.clone(),\n      auth_type: AuthTypePB::Local,\n      device_id: uuid::Uuid::new_v4().to_string(),\n    }\n    .into_bytes()\n    .unwrap();\n\n    let request = AFPluginRequest::new(UserEvent::SignUp).payload(payload);\n    let user_profile = self\n      .local_set\n      .run_until(AFPluginDispatcher::async_send(\n        &self.appflowy_core.dispatcher(),\n        request,\n      ))\n      .await\n      .parse::<UserProfilePB, FlowyError>()\n      .unwrap()\n      .unwrap();\n\n    // let _ = create_default_workspace_if_need(dispatch.clone(), &user_profile.id);\n    SignUpContext {\n      user_profile,\n      password,\n    }\n  }\n\n  pub async fn af_cloud_sign_up(&self) -> UserProfilePB {\n    let email = unique_email();\n    match self.af_cloud_sign_in_with_email(&email).await {\n      Ok(profile) => profile,\n      Err(err) => {\n        tracing::warn!(\n          \"Failed to sign up with email: {}, error: {}, retrying\",\n          email,\n          err\n        );\n        tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;\n        self.af_cloud_sign_in_with_email(&email).await.unwrap()\n      },\n    }\n  }\n\n  pub async fn sign_out(&self) {\n    EventBuilder::new(self.clone())\n      .event(UserEvent::SignOut)\n      .async_send()\n      .await;\n  }\n\n  pub fn set_auth_type(&self, auth_type: AuthTypePB) {\n    self.authenticator.store(auth_type as u8, Ordering::Release);\n  }\n\n  pub async fn init_anon_user(&self) -> UserProfilePB {\n    self.sign_up_as_anon().await.user_profile\n  }\n\n  pub async fn get_user_profile(&self) -> Result<UserProfilePB, FlowyError> {\n    EventBuilder::new(self.clone())\n      .event(UserEvent::GetUserProfile)\n      .async_send()\n      .await\n      .try_parse::<UserProfilePB>()\n  }\n\n  pub async fn update_user_profile(&self, params: UpdateUserProfilePayloadPB) {\n    EventBuilder::new(self.clone())\n      .event(UserEvent::UpdateUserProfile)\n      .payload(params)\n      .async_send()\n      .await;\n  }\n\n  pub async fn af_cloud_sign_in_with_email(&self, email: &str) -> FlowyResult<UserProfilePB> {\n    let payload = SignInUrlPayloadPB {\n      email: email.to_string(),\n      authenticator: AuthTypePB::Server,\n    };\n    let sign_in_url = EventBuilder::new(self.clone())\n      .event(UserEvent::GenerateSignInURL)\n      .payload(payload)\n      .async_send()\n      .await\n      .try_parse::<SignInUrlPB>()?\n      .sign_in_url;\n\n    let mut map = HashMap::new();\n    map.insert(USER_SIGN_IN_URL.to_string(), sign_in_url);\n    map.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string());\n    let payload = OauthSignInPB {\n      map,\n      auth_type: AuthTypePB::Server,\n    };\n\n    let user_profile = EventBuilder::new(self.clone())\n      .event(UserEvent::OauthSignIn)\n      .payload(payload)\n      .async_send()\n      .await\n      .try_parse::<UserProfilePB>()?;\n\n    Ok(user_profile)\n  }\n\n  pub async fn import_appflowy_data(\n    &self,\n    path: String,\n    name: Option<String>,\n  ) -> Result<(), FlowyError> {\n    let payload = ImportAppFlowyDataPB {\n      path,\n      import_container_name: name,\n      parent_view_id: None,\n    };\n    match EventBuilder::new(self.clone())\n      .event(UserEvent::ImportAppFlowyDataFolder)\n      .payload(payload)\n      .async_send()\n      .await\n      .error()\n    {\n      Some(err) => Err(err),\n      None => Ok(()),\n    }\n  }\n\n  pub async fn create_workspace(\n    &self,\n    name: &str,\n    workspace_type: WorkspaceType,\n  ) -> UserWorkspacePB {\n    let payload = CreateWorkspacePB {\n      name: name.to_string(),\n      workspace_type: WorkspaceTypePB::from(workspace_type),\n    };\n    EventBuilder::new(self.clone())\n      .event(UserEvent::CreateWorkspace)\n      .payload(payload)\n      .async_send()\n      .await\n      .parse_or_panic::<UserWorkspacePB>()\n  }\n\n  pub async fn rename_workspace(\n    &self,\n    workspace_id: &str,\n    new_name: &str,\n  ) -> Result<(), FlowyError> {\n    let payload = RenameWorkspacePB {\n      workspace_id: workspace_id.to_owned(),\n      new_name: new_name.to_owned(),\n    };\n    match EventBuilder::new(self.clone())\n      .event(UserEvent::RenameWorkspace)\n      .payload(payload)\n      .async_send()\n      .await\n      .error()\n    {\n      Some(err) => Err(err),\n      None => Ok(()),\n    }\n  }\n\n  pub async fn change_workspace_icon(\n    &self,\n    workspace_id: &str,\n    new_icon: &str,\n  ) -> Result<(), FlowyError> {\n    let payload = ChangeWorkspaceIconPB {\n      workspace_id: workspace_id.to_owned(),\n      new_icon: new_icon.to_owned(),\n    };\n    match EventBuilder::new(self.clone())\n      .event(UserEvent::ChangeWorkspaceIcon)\n      .payload(payload)\n      .async_send()\n      .await\n      .error()\n    {\n      Some(err) => Err(err),\n      None => Ok(()),\n    }\n  }\n\n  pub async fn folder_read_current_workspace(&self) -> WorkspacePB {\n    EventBuilder::new(self.clone())\n      .event(FolderEvent::ReadCurrentWorkspace)\n      .async_send()\n      .await\n      .parse_or_panic()\n  }\n\n  pub async fn folder_read_current_workspace_views(&self) -> RepeatedViewPB {\n    EventBuilder::new(self.clone())\n      .event(FolderEvent::ReadCurrentWorkspaceViews)\n      .async_send()\n      .await\n      .parse_or_panic()\n  }\n\n  pub async fn get_all_workspaces(&self) -> RepeatedUserWorkspacePB {\n    EventBuilder::new(self.clone())\n      .event(UserEvent::GetAllWorkspace)\n      .async_send()\n      .await\n      .parse_or_panic::<RepeatedUserWorkspacePB>()\n  }\n\n  pub async fn delete_workspace(&self, workspace_id: &str) {\n    let payload = UserWorkspaceIdPB {\n      workspace_id: workspace_id.to_string(),\n    };\n    EventBuilder::new(self.clone())\n      .event(UserEvent::DeleteWorkspace)\n      .payload(payload)\n      .async_send()\n      .await;\n  }\n\n  pub async fn open_workspace(&self, workspace_id: &str, workspace_type: WorkspaceTypePB) {\n    let payload = OpenUserWorkspacePB {\n      workspace_id: workspace_id.to_string(),\n      workspace_type,\n    };\n    EventBuilder::new(self.clone())\n      .event(UserEvent::OpenWorkspace)\n      .payload(payload)\n      .async_send()\n      .await;\n  }\n\n  pub async fn leave_workspace(&self, workspace_id: &str) {\n    let payload = UserWorkspaceIdPB {\n      workspace_id: workspace_id.to_string(),\n    };\n    EventBuilder::new(self.clone())\n      .event(UserEvent::LeaveWorkspace)\n      .payload(payload)\n      .async_send()\n      .await;\n  }\n}\n\n#[derive(Clone)]\npub struct TestNotificationSender {\n  sender: Arc<Sender<SubscribeObject>>,\n}\n\nimpl Default for TestNotificationSender {\n  fn default() -> Self {\n    let (sender, _) = channel(1000);\n    Self {\n      sender: Arc::new(sender),\n    }\n  }\n}\n\nimpl TestNotificationSender {\n  pub fn new() -> Self {\n    Self::default()\n  }\n\n  pub fn subscribe<T>(&self, id: &str, ty: impl Into<i32> + Send) -> tokio::sync::mpsc::Receiver<T>\n  where\n    T: TryFrom<Bytes, Error = ProtobufError> + Send + 'static,\n  {\n    let id = id.to_string();\n    let (tx, rx) = tokio::sync::mpsc::channel::<T>(10);\n    let mut receiver = self.sender.subscribe();\n    let ty = ty.into();\n    tokio::spawn(async move {\n      // DatabaseNotification::DidUpdateDatabaseSnapshotState\n      while let Ok(value) = receiver.recv().await {\n        if value.id == id && value.ty == ty {\n          if let Some(payload) = value.payload {\n            match T::try_from(Bytes::from(payload)) {\n              Ok(object) => {\n                let _ = tx.send(object).await;\n              },\n              Err(e) => {\n                panic!(\n                  \"Failed to parse notification payload to type: {:?} with error: {}\",\n                  std::any::type_name::<T>(),\n                  e\n                );\n              },\n            }\n          }\n        }\n      }\n    });\n    rx\n  }\n\n  pub fn subscribe_without_payload(\n    &self,\n    id: &str,\n    ty: impl Into<i32> + Send,\n  ) -> tokio::sync::mpsc::Receiver<()> {\n    let id = id.to_string();\n    let (tx, rx) = tokio::sync::mpsc::channel::<()>(10);\n    let mut receiver = self.sender.subscribe();\n    let ty = ty.into();\n    tokio::spawn(async move {\n      // DatabaseNotification::DidUpdateDatabaseSnapshotState\n      while let Ok(value) = receiver.recv().await {\n        if value.id == id && value.ty == ty {\n          let _ = tx.send(()).await;\n        }\n      }\n    });\n    rx\n  }\n\n  pub fn subscribe_with_condition<T, F>(&self, id: &str, when: F) -> tokio::sync::mpsc::Receiver<T>\n  where\n    T: TryFrom<Bytes, Error = ProtobufError> + Send + 'static,\n    F: Fn(&T) -> bool + Send + 'static,\n  {\n    let id = id.to_string();\n    let (tx, rx) = tokio::sync::mpsc::channel::<T>(1);\n    let mut receiver = self.sender.subscribe();\n    tokio::spawn(async move {\n      while let Ok(value) = receiver.recv().await {\n        if value.id == id {\n          if let Some(payload) = value.payload {\n            if let Ok(object) = T::try_from(Bytes::from(payload)) {\n              if when(&object) {\n                let _ = tx.send(object).await;\n              }\n            }\n          }\n        }\n      }\n    });\n    rx\n  }\n}\nimpl NotificationSender for TestNotificationSender {\n  fn send_subject(&self, subject: SubscribeObject) -> Result<(), String> {\n    if let Err(err) = self.sender.send(subject) {\n      error!(\"Failed to send notification: {:?}\", err);\n    }\n    Ok(())\n  }\n}\n\npub fn third_party_sign_up_param(uuid: String) -> HashMap<String, String> {\n  let mut params = HashMap::new();\n  params.insert(USER_UUID.to_string(), uuid);\n  params.insert(\n    USER_EMAIL.to_string(),\n    format!(\"{}@test.com\", Uuid::new_v4()),\n  );\n  params.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string());\n  params\n}\n\npub fn unique_email() -> String {\n  format!(\"{}@appflowy.io\", Uuid::new_v4())\n}\n\npub fn login_email() -> String {\n  \"annie2@appflowy.io\".to_string()\n}\n\npub fn login_password() -> String {\n  \"HelloWorld!123\".to_string()\n}\npub struct SignUpContext {\n  pub user_profile: UserProfilePB,\n  pub password: String,\n}\npub async fn use_local_mode() {\n  AuthenticatorType::Local.write_env();\n  AFCloudConfiguration::default().write_env();\n}\npub async fn use_localhost_af_cloud() {\n  AuthenticatorType::AppFlowyCloud.write_env();\n  let base_url =\n    std::env::var(\"af_cloud_test_base_url\").unwrap_or(\"http://localhost:8000\".to_string());\n  let ws_base_url =\n    std::env::var(\"af_cloud_test_ws_url\").unwrap_or(\"ws://localhost:8000/ws/v1\".to_string());\n  let gotrue_url =\n    std::env::var(\"af_cloud_test_gotrue_url\").unwrap_or(\"http://localhost:9999\".to_string());\n  AFCloudConfiguration {\n    base_url,\n    ws_base_url,\n    gotrue_url,\n    enable_sync_trace: true,\n    maximum_upload_file_size_in_bytes: None,\n  }\n  .write_env();\n  std::env::set_var(\"GOTRUE_ADMIN_EMAIL\", \"admin@example.com\");\n  std::env::set_var(\"GOTRUE_ADMIN_PASSWORD\", \"password\");\n}\n\n#[allow(dead_code)]\npub async fn user_localhost_af_cloud_with_nginx() {\n  std::env::set_var(\"af_cloud_test_base_url\", \"http://localhost\");\n  std::env::set_var(\"af_cloud_test_ws_url\", \"ws://localhost/ws/v1\");\n  std::env::set_var(\"af_cloud_test_gotrue_url\", \"http://localhost/gotrue\");\n  use_localhost_af_cloud().await\n}\n"
  },
  {
    "path": "frontend/rust-lib/event-integration-test/tests/asset/database_template_1.afdb",
    "content": "\"{\"\"id\"\":\"\"2_OVWb\"\",\"\"name\"\":\"\"Name\"\",\"\"field_type\"\":0,\"\"visibility\"\":true,\"\"width\"\":150,\"\"type_options\"\":{\"\"0\"\":{\"\"data\"\":\"\"\"\"}},\"\"is_primary\"\":true}\",\"{\"\"id\"\":\"\"xjmOSi\"\",\"\"name\"\":\"\"Type\"\",\"\"field_type\"\":3,\"\"visibility\"\":true,\"\"width\"\":150,\"\"type_options\"\":{\"\"3\"\":{\"\"content\"\":\"\"{\\\"\"options\\\"\":[{\\\"\"id\\\"\":\\\"\"t1WZ\\\"\",\\\"\"name\\\"\":\\\"\"s6\\\"\",\\\"\"color\\\"\":\\\"\"Lime\\\"\"},{\\\"\"id\\\"\":\\\"\"GzNa\\\"\",\\\"\"name\\\"\":\\\"\"s5\\\"\",\\\"\"color\\\"\":\\\"\"Yellow\\\"\"},{\\\"\"id\\\"\":\\\"\"l_8w\\\"\",\\\"\"name\\\"\":\\\"\"s4\\\"\",\\\"\"color\\\"\":\\\"\"Orange\\\"\"},{\\\"\"id\\\"\":\\\"\"TzVT\\\"\",\\\"\"name\\\"\":\\\"\"s3\\\"\",\\\"\"color\\\"\":\\\"\"LightPink\\\"\"},{\\\"\"id\\\"\":\\\"\"b5WF\\\"\",\\\"\"name\\\"\":\\\"\"s2\\\"\",\\\"\"color\\\"\":\\\"\"Pink\\\"\"},{\\\"\"id\\\"\":\\\"\"AcHA\\\"\",\\\"\"name\\\"\":\\\"\"s1\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"}],\\\"\"disable_color\\\"\":false}\"\"}},\"\"is_primary\"\":false}\",\"{\"\"id\"\":\"\"Hpbiwr\"\",\"\"name\"\":\"\"Done\"\",\"\"field_type\"\":5,\"\"visibility\"\":true,\"\"width\"\":150,\"\"type_options\"\":{\"\"5\"\":{\"\"is_selected\"\":false}},\"\"is_primary\"\":false}\",\"{\"\"id\"\":\"\"F7WLnw\"\",\"\"name\"\":\"\"checklist\"\",\"\"field_type\"\":7,\"\"visibility\"\":true,\"\"width\"\":120,\"\"type_options\"\":{\"\"0\"\":{\"\"data\"\":\"\"\"\"},\"\"7\"\":{}},\"\"is_primary\"\":false}\",\"{\"\"id\"\":\"\"KABhMe\"\",\"\"name\"\":\"\"number\"\",\"\"field_type\"\":1,\"\"visibility\"\":true,\"\"width\"\":120,\"\"type_options\"\":{\"\"1\"\":{\"\"format\"\":0,\"\"symbol\"\":\"\"RUB\"\",\"\"scale\"\":0,\"\"name\"\":\"\"Number\"\"},\"\"0\"\":{\"\"scale\"\":0,\"\"data\"\":\"\"\"\",\"\"format\"\":0,\"\"name\"\":\"\"Number\"\",\"\"symbol\"\":\"\"RUB\"\"}},\"\"is_primary\"\":false}\",\"{\"\"id\"\":\"\"lEn6Bv\"\",\"\"name\"\":\"\"date\"\",\"\"field_type\"\":2,\"\"visibility\"\":true,\"\"width\"\":120,\"\"type_options\"\":{\"\"2\"\":{\"\"field_type\"\":2,\"\"time_format\"\":1,\"\"timezone_id\"\":\"\"\"\",\"\"date_format\"\":3},\"\"0\"\":{\"\"field_type\"\":2,\"\"date_format\"\":3,\"\"time_format\"\":1,\"\"data\"\":\"\"\"\",\"\"timezone_id\"\":\"\"\"\"}},\"\"is_primary\"\":false}\",\"{\"\"id\"\":\"\"B8Prnx\"\",\"\"name\"\":\"\"url\"\",\"\"field_type\"\":6,\"\"visibility\"\":true,\"\"width\"\":120,\"\"type_options\"\":{\"\"6\"\":{\"\"content\"\":\"\"\"\",\"\"url\"\":\"\"\"\"},\"\"0\"\":{\"\"content\"\":\"\"\"\",\"\"data\"\":\"\"\"\",\"\"url\"\":\"\"\"\"}},\"\"is_primary\"\":false}\",\"{\"\"id\"\":\"\"MwUow4\"\",\"\"name\"\":\"\"multi-select\"\",\"\"field_type\"\":4,\"\"visibility\"\":true,\"\"width\"\":240,\"\"type_options\"\":{\"\"0\"\":{\"\"content\"\":\"\"{\\\"\"options\\\"\":[],\\\"\"disable_color\\\"\":false}\"\",\"\"data\"\":\"\"\"\"},\"\"4\"\":{\"\"content\"\":\"\"{\\\"\"options\\\"\":[{\\\"\"id\\\"\":\\\"\"__Us\\\"\",\\\"\"name\\\"\":\\\"\"m7\\\"\",\\\"\"color\\\"\":\\\"\"Green\\\"\"},{\\\"\"id\\\"\":\\\"\"n9-g\\\"\",\\\"\"name\\\"\":\\\"\"m6\\\"\",\\\"\"color\\\"\":\\\"\"Lime\\\"\"},{\\\"\"id\\\"\":\\\"\"KFYu\\\"\",\\\"\"name\\\"\":\\\"\"m5\\\"\",\\\"\"color\\\"\":\\\"\"Yellow\\\"\"},{\\\"\"id\\\"\":\\\"\"KftP\\\"\",\\\"\"name\\\"\":\\\"\"m4\\\"\",\\\"\"color\\\"\":\\\"\"Orange\\\"\"},{\\\"\"id\\\"\":\\\"\"5lWo\\\"\",\\\"\"name\\\"\":\\\"\"m3\\\"\",\\\"\"color\\\"\":\\\"\"LightPink\\\"\"},{\\\"\"id\\\"\":\\\"\"Djrz\\\"\",\\\"\"name\\\"\":\\\"\"m2\\\"\",\\\"\"color\\\"\":\\\"\"Pink\\\"\"},{\\\"\"id\\\"\":\\\"\"2uRu\\\"\",\\\"\"name\\\"\":\\\"\"m1\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"}],\\\"\"disable_color\\\"\":false}\"\"}},\"\"is_primary\"\":false}\"\n\"{\"\"field_type\"\":0,\"\"created_at\"\":1686793246,\"\"data\"\":\"\"A\"\",\"\"last_modified\"\":1686793246}\",\"{\"\"last_modified\"\":1686793275,\"\"created_at\"\":1686793261,\"\"data\"\":\"\"AcHA\"\",\"\"field_type\"\":3}\",\"{\"\"created_at\"\":1686793241,\"\"field_type\"\":5,\"\"last_modified\"\":1686793241,\"\"data\"\":\"\"Yes\"\"}\",\"{\"\"data\"\":\"\"{\\\"\"options\\\"\":[{\\\"\"id\\\"\":\\\"\"pi1A\\\"\",\\\"\"name\\\"\":\\\"\"t1\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"},{\\\"\"id\\\"\":\\\"\"6Pym\\\"\",\\\"\"name\\\"\":\\\"\"t2\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"},{\\\"\"id\\\"\":\\\"\"erEe\\\"\",\\\"\"name\\\"\":\\\"\"t3\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"}],\\\"\"selected_option_ids\\\"\":[\\\"\"pi1A\\\"\",\\\"\"6Pym\\\"\"]}\"\",\"\"created_at\"\":1686793302,\"\"field_type\"\":7,\"\"last_modified\"\":1686793308}\",\"{\"\"created_at\"\":1686793333,\"\"field_type\"\":1,\"\"data\"\":\"\"-1\"\",\"\"last_modified\"\":1686793333}\",\"{\"\"last_modified\"\":1686793370,\"\"field_type\"\":2,\"\"data\"\":\"\"1685583770\"\",\"\"include_time\"\":false,\"\"created_at\"\":1686793370}\",\"{\"\"created_at\"\":1686793395,\"\"data\"\":\"\"appflowy.io\"\",\"\"field_type\"\":6,\"\"last_modified\"\":1686793399,\"\"url\"\":\"\"https://appflowy.io\"\"}\",\"{\"\"last_modified\"\":1686793446,\"\"field_type\"\":4,\"\"data\"\":\"\"2uRu\"\",\"\"created_at\"\":1686793428}\"\n\"{\"\"last_modified\"\":1686793247,\"\"data\"\":\"\"B\"\",\"\"field_type\"\":0,\"\"created_at\"\":1686793247}\",\"{\"\"created_at\"\":1686793278,\"\"data\"\":\"\"b5WF\"\",\"\"field_type\"\":3,\"\"last_modified\"\":1686793278}\",\"{\"\"created_at\"\":1686793292,\"\"last_modified\"\":1686793292,\"\"data\"\":\"\"Yes\"\",\"\"field_type\"\":5}\",\"{\"\"data\"\":\"\"{\\\"\"options\\\"\":[{\\\"\"id\\\"\":\\\"\"YHDO\\\"\",\\\"\"name\\\"\":\\\"\"t1\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"},{\\\"\"id\\\"\":\\\"\"QjtW\\\"\",\\\"\"name\\\"\":\\\"\"t2\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"},{\\\"\"id\\\"\":\\\"\"K2nM\\\"\",\\\"\"name\\\"\":\\\"\"t3\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"}],\\\"\"selected_option_ids\\\"\":[\\\"\"YHDO\\\"\"]}\"\",\"\"field_type\"\":7,\"\"last_modified\"\":1686793318,\"\"created_at\"\":1686793311}\",\"{\"\"data\"\":\"\"-2\"\",\"\"last_modified\"\":1686793335,\"\"created_at\"\":1686793335,\"\"field_type\"\":1}\",\"{\"\"field_type\"\":2,\"\"data\"\":\"\"1685670174\"\",\"\"include_time\"\":false,\"\"created_at\"\":1686793374,\"\"last_modified\"\":1686793374}\",\"{\"\"last_modified\"\":1686793403,\"\"field_type\"\":6,\"\"created_at\"\":1686793399,\"\"url\"\":\"\"\"\",\"\"data\"\":\"\"no url\"\"}\",\"{\"\"data\"\":\"\"2uRu,Djrz\"\",\"\"field_type\"\":4,\"\"last_modified\"\":1686793449,\"\"created_at\"\":1686793449}\"\n\"{\"\"data\"\":\"\"C\"\",\"\"created_at\"\":1686793248,\"\"last_modified\"\":1686793248,\"\"field_type\"\":0}\",\"{\"\"created_at\"\":1686793280,\"\"field_type\"\":3,\"\"data\"\":\"\"TzVT\"\",\"\"last_modified\"\":1686793280}\",\"{\"\"data\"\":\"\"Yes\"\",\"\"last_modified\"\":1686793292,\"\"field_type\"\":5,\"\"created_at\"\":1686793292}\",\"{\"\"last_modified\"\":1686793329,\"\"field_type\"\":7,\"\"created_at\"\":1686793322,\"\"data\"\":\"\"{\\\"\"options\\\"\":[{\\\"\"id\\\"\":\\\"\"iWM1\\\"\",\\\"\"name\\\"\":\\\"\"t1\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"},{\\\"\"id\\\"\":\\\"\"WDvF\\\"\",\\\"\"name\\\"\":\\\"\"t2\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"},{\\\"\"id\\\"\":\\\"\"w3k7\\\"\",\\\"\"name\\\"\":\\\"\"t3\\\"\",\\\"\"color\\\"\":\\\"\"Purple\\\"\"}],\\\"\"selected_option_ids\\\"\":[\\\"\"iWM1\\\"\",\\\"\"WDvF\\\"\",\\\"\"w3k7\\\"\"]}\"\"}\",\"{\"\"field_type\"\":1,\"\"last_modified\"\":1686793339,\"\"data\"\":\"\"0.1\"\",\"\"created_at\"\":1686793339}\",\"{\"\"last_modified\"\":1686793377,\"\"data\"\":\"\"1685756577\"\",\"\"created_at\"\":1686793377,\"\"include_time\"\":false,\"\"field_type\"\":2}\",\"{\"\"created_at\"\":1686793403,\"\"field_type\"\":6,\"\"data\"\":\"\"appflowy.io\"\",\"\"last_modified\"\":1686793408,\"\"url\"\":\"\"https://appflowy.io\"\"}\",\"{\"\"data\"\":\"\"2uRu,Djrz,5lWo\"\",\"\"created_at\"\":1686793453,\"\"last_modified\"\":1686793454,\"\"field_type\"\":4}\"\n\"{\"\"data\"\":\"\"D\"\",\"\"last_modified\"\":1686793249,\"\"created_at\"\":1686793249,\"\"field_type\"\":0}\",\"{\"\"data\"\":\"\"l_8w\"\",\"\"created_at\"\":1686793284,\"\"last_modified\"\":1686793284,\"\"field_type\"\":3}\",\"{\"\"data\"\":\"\"Yes\"\",\"\"created_at\"\":1686793293,\"\"last_modified\"\":1686793293,\"\"field_type\"\":5}\",,\"{\"\"field_type\"\":1,\"\"last_modified\"\":1686793341,\"\"created_at\"\":1686793341,\"\"data\"\":\"\"0.2\"\"}\",\"{\"\"created_at\"\":1686793379,\"\"last_modified\"\":1686793379,\"\"field_type\"\":2,\"\"data\"\":\"\"1685842979\"\",\"\"include_time\"\":false}\",\"{\"\"last_modified\"\":1686793419,\"\"field_type\"\":6,\"\"created_at\"\":1686793408,\"\"data\"\":\"\"https://github.com/AppFlowy-IO/\"\",\"\"url\"\":\"\"https://github.com/AppFlowy-IO/\"\"}\",\"{\"\"data\"\":\"\"2uRu,Djrz,5lWo\"\",\"\"last_modified\"\":1686793459,\"\"field_type\"\":4,\"\"created_at\"\":1686793459}\"\n\"{\"\"field_type\"\":0,\"\"last_modified\"\":1686793250,\"\"created_at\"\":1686793250,\"\"data\"\":\"\"E\"\"}\",\"{\"\"field_type\"\":3,\"\"last_modified\"\":1686793290,\"\"created_at\"\":1686793290,\"\"data\"\":\"\"GzNa\"\"}\",\"{\"\"last_modified\"\":1686793294,\"\"created_at\"\":1686793294,\"\"data\"\":\"\"Yes\"\",\"\"field_type\"\":5}\",,\"{\"\"created_at\"\":1686793346,\"\"field_type\"\":1,\"\"last_modified\"\":1686793346,\"\"data\"\":\"\"1\"\"}\",\"{\"\"last_modified\"\":1686793383,\"\"data\"\":\"\"1685929383\"\",\"\"field_type\"\":2,\"\"include_time\"\":false,\"\"created_at\"\":1686793383}\",\"{\"\"field_type\"\":6,\"\"url\"\":\"\"\"\",\"\"data\"\":\"\"\"\",\"\"last_modified\"\":1686793421,\"\"created_at\"\":1686793419}\",\"{\"\"field_type\"\":4,\"\"last_modified\"\":1686793465,\"\"data\"\":\"\"2uRu,Djrz,5lWo,KFYu,KftP\"\",\"\"created_at\"\":1686793463}\"\n\"{\"\"field_type\"\":0,\"\"created_at\"\":1686793251,\"\"data\"\":\"\"\"\",\"\"last_modified\"\":1686793289}\",,,,\"{\"\"data\"\":\"\"2\"\",\"\"field_type\"\":1,\"\"created_at\"\":1686793347,\"\"last_modified\"\":1686793347}\",\"{\"\"include_time\"\":false,\"\"data\"\":\"\"1685929385\"\",\"\"last_modified\"\":1686793385,\"\"field_type\"\":2,\"\"created_at\"\":1686793385}\",,\n\"{\"\"created_at\"\":1686793254,\"\"field_type\"\":0,\"\"last_modified\"\":1686793288,\"\"data\"\":\"\"\"\"}\",,,,\"{\"\"created_at\"\":1686793351,\"\"last_modified\"\":1686793351,\"\"data\"\":\"\"10\"\",\"\"field_type\"\":1}\",\"{\"\"include_time\"\":false,\"\"data\"\":\"\"1686879792\"\",\"\"field_type\"\":2,\"\"created_at\"\":1686793392,\"\"last_modified\"\":1686793392}\",,\n,,,,\"{\"\"last_modified\"\":1686793354,\"\"created_at\"\":1686793354,\"\"field_type\"\":1,\"\"data\"\":\"\"11\"\"}\",,,\n,,,,\"{\"\"field_type\"\":1,\"\"last_modified\"\":1686793356,\"\"data\"\":\"\"12\"\",\"\"created_at\"\":1686793356}\",,,\n,,,,,,,\n"
  },
  {
    "path": "frontend/rust-lib/event-integration-test/tests/asset/project.csv",
    "content": "﻿Project name,Status,Owner,Dates,Priority,Completion,Tasks,Blocked By,Is Blocking,Summary,Delay,Checkbox,Files & media\r\nResearch study,In Progress,Nate Martins,\"April 23, 2024 → May 22, 2024\",High,0.25,\"Develop survey questions (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Develop%20survey%20questions%2086ce25c6bb214b4b9e35beed8bc8b821.md), Interpret findings (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Interpret%20findings%20c418ae6385f94dcdadca1423ad60146b.md), Write research report (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Write%20research%20report%20cca8f4321cd44dceba9c266cdf544f15.md), Conduct interviews (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Conduct%20interviews%208f40c17883904d769fee19baeb0d46a5.md)\",,Marketing campaign (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Projects%2058b8977d6e4444a98ec4d64176a071e5/Marketing%20campaign%2088ac0cea4cb245efb44d63ace0a37d1e.md),\"A research study is underway to understand customer satisfaction and identify improvement areas. The team is developing a survey to gather data from customers, aiming to analyze insights for strategic decision-making and enhance satisfaction.\",7,No,Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/DO010003572.jpeg\r\nMarketing campaign,In Progress,Sohrab Amin,\"April 24, 2024 → May 7, 2024\",Low,0.5,\"Define target audience (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Define%20target%20audience%2045e2d3342bfe44848009f8e19e65b2af.md), Develop advertising plan (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Develop%20advertising%20plan%20a8e534ad763040029d0feb27fdb1820d.md), Report on marketing ROI (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Report%20on%20marketing%20ROI%202458b6a0f0174f72af3e5daac8b36b08.md), Create social media plan (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Create%20social%20media%20plan%204e70ea0b7d40427a9648bcf554a121f6.md), Create performance marketing plan (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Create%20performance%20marketing%20plan%20b6aa6a9e9cc1446490984eaecc4930c7.md), Develop new creative assets (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Develop%20new%20creative%20assets%203268a1d63424427ba1f7e3cac109cefa.md)\",Research study (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Projects%2058b8977d6e4444a98ec4d64176a071e5/Research%20study%20e445ee1fb7ff4591be2de17d906df97e.md),Website redesign (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Projects%2058b8977d6e4444a98ec4d64176a071e5/Website%20redesign%20bb934cb9f0e44daab4368247d62b0f39.md),\"The marketing campaign, led by Sohrab Amin, focuses on enhancing mobile performance, building on last year's success in reducing page load times by 50%. The team aims to further improve performance by investing in native components for iOS and Android from April 24 to May 7, 2024.\",19,No,Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/appflowy_2x.png\r\nWebsite redesign,Planning,Nate Martins,\"May 10, 2024 → May 26, 2024\",Medium,,\"Conduct website audit (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Conduct%20website%20audit%20aee812f0c962462e8b91e8044a268eb5.md), Write website copy (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Write%20website%20copy%209ab9309ceae34f6f8dc19b2eeda8f8f2.md), Test website functionality (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Test%20website%20functionality%208ccf7153028747b19a351f6d36100f0d.md), Develop creative assets (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Develop%20creative%20assets%206fe86230e30843ebb60c67724f0f922f.md)\",Marketing campaign (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Projects%2058b8977d6e4444a98ec4d64176a071e5/Marketing%20campaign%2088ac0cea4cb245efb44d63ace0a37d1e.md),,\"The website redesign project, led by Nate Martins from May 10 to May 26, 2024, aims to create a more effective onboarding process to enhance user experience and increase 7-day retention by 25%.\",,Yes,\r\nProduct launch,In Progress,Ben Lang,\"May 8, 2024 → May 22, 2024\",High,0.6666666666666666,\"Create product demo video (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Create%20product%20demo%20video%202e2e2eb53df84019a613fe386e4c79da.md), Create product positioning (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Create%20product%20positioning%20c0d2728f79594cc3a1869a3c67bcdf45.md), Monitor launch performance (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Monitor%20launch%20performance%207c0b846d1da2463892eff490f06e0d0b.md)\",,,\"A new product launch is scheduled from May 8 to May 22, 2024, initiated to fill a market gap and expand the product line. The development team is focused on creating a high-quality product, while the marketing team is crafting a strategy to achieve a 10% market share within the first six months post-launch.\",16,Yes,"
  },
  {
    "path": "frontend/rust-lib/event-integration-test/tests/chat/local_chat_test.rs",
    "content": "use crate::util::load_text_file_content;\nuse event_integration_test::user_event::use_localhost_af_cloud;\nuse event_integration_test::EventIntegrationTest;\nuse flowy_user_pub::entities::WorkspaceType;\n\n#[tokio::test]\nasync fn local_ollama_test_create_chat_with_selected_sources() {\n  use_localhost_af_cloud().await;\n  let test = EventIntegrationTest::new().await;\n  test.af_cloud_sign_up().await;\n  test.toggle_local_ai().await;\n\n  let local_workspace = test\n    .create_workspace(\"my workspace\", WorkspaceType::Local)\n    .await;\n\n  // create a chat document\n  test\n    .open_workspace(\n      &local_workspace.workspace_id,\n      local_workspace.workspace_type,\n    )\n    .await;\n  let doc = test\n    .create_and_open_document(\n      &local_workspace.workspace_id,\n      \"japan trip\".to_string(),\n      vec![],\n    )\n    .await;\n  let content = load_text_file_content(\"japan_trip.md\");\n  test.insert_document_text(&doc.id, &content, 0).await;\n\n  //chat with the document\n  let chat = test.create_chat(&local_workspace.workspace_id).await;\n  test\n    .set_chat_rag_ids(&chat.id, vec![doc.id.to_string()])\n    .await;\n\n  // test\n  //   .send_message(&chat.id, \"why use rust?\", ChatMessageTypePB::User)\n  //   .await;\n}\n"
  },
  {
    "path": "frontend/rust-lib/event-integration-test/tests/chat/mod.rs",
    "content": "mod chat_message_test;\n"
  },
  {
    "path": "frontend/rust-lib/event-integration-test/tests/database/af_cloud/translate_row_test.rs",
    "content": "use crate::database::af_cloud::util::make_test_summary_grid;\nuse std::time::Duration;\nuse tokio::time::sleep;\n\nuse event_integration_test::user_event::user_localhost_af_cloud;\nuse event_integration_test::EventIntegrationTest;\nuse flowy_database2::entities::{FieldType, TranslateRowPB};\n\n#[tokio::test]\nasync fn af_cloud_translate_row_test() {\n  user_localhost_af_cloud().await;\n  let test = EventIntegrationTest::new().await;\n  test.af_cloud_sign_up().await;\n\n  // create document and then insert content\n  let current_workspace = test.get_current_workspace().await;\n  let initial_data = make_test_summary_grid().to_json_bytes().unwrap();\n  let view = test\n    .create_grid(\n      &current_workspace.id,\n      \"translate database\".to_string(),\n      initial_data,\n    )\n    .await;\n\n  let database_pb = test.get_database(&view.id).await;\n  let field = test\n    .get_all_database_fields(&view.id)\n    .await\n    .items\n    .into_iter()\n    .find(|field| field.field_type == FieldType::Translate)\n    .unwrap();\n\n  let row_id = database_pb.rows[0].id.clone();\n  let data = TranslateRowPB {\n    view_id: view.id.clone(),\n    row_id: row_id.clone(),\n    field_id: field.id.clone(),\n  };\n  test.translate_row(data).await;\n\n  sleep(Duration::from_secs(1)).await;\n  let cell = test\n    .get_text_cell(&view.id, &row_id, &field.id)\n    .await\n    .to_lowercase();\n  println!(\"cell: {}\", cell);\n  // default translation is in French. So it should be something like this:\n  // Prix:2,6 $,Nom du produit:Pomme,Statut:TERMINÉ\n  assert!(cell.contains(\"pomme\"));\n  assert!(cell.contains(\"produit\"));\n  assert!(cell.contains(\"prix\"));\n}\n"
  },
  {
    "path": "frontend/rust-lib/event-integration-test/tests/database/mod.rs",
    "content": "mod af_cloud;\nmod local_test;\n"
  },
  {
    "path": "frontend/scripts/linux_distribution/appimage/io.appflowy.AppFlowy.desktop",
    "content": ""
  }
]